1.2 RT-Thread I/O设备框架
之前没有接触过驱动开发的人,看到“设备框架”这个名词可能会感到迷茫。大家不妨先思考这样一个问题:“为什么不同厂家、不同价格、不同形状的鼠标,插到电脑上之后都能正常工作?”这是因为各家生产的鼠标都遵循同一套标准,操作系统只要按照这个标准去操作鼠标就可以得到它想要的效果。
“设备框架”就是针对某一类外设,抽象出来一套统一的操作方法以及接入标准。有了这一层抽象,框架上层的应用要访问具体外设(比如摄像头)时,就不用关心具体的厂家或者产地了。只要按照框架提供的操作方法,就可以控制摄像头拍照、摄像了。这套设备框架也为生产厂家提供了方便,他们不需要关心应用具体会怎么使用,只要按照设备框架提供的接入标准设计产品,生产出来就可以在市面上销售了。
在嵌入式领域,RT-Thread也提供了这样的一层抽象,用于屏蔽嵌入式上的硬件差异,为应用层提供统一的操作方法,也为底层硬件提供统一的接入标准。
RT-Thread提供了一套简单的I/O设备模型框架,简称设备框架,如图1-3所示。RT-Thread设备框架位于硬件和应用程序之间,共分成3层,从上到下分别是I/O设备管理层、设备驱动框架层、设备驱动层。
图1-3 RT-Thread I/O设备框架
应用程序通过I/O设备管理接口获得正确的设备驱动,然后通过这个设备驱动与底层I/O硬件设备进行数据(或控制)交互。
I/O设备管理层实现了对设备驱动程序的封装。应用程序通过I/O设备层提供的标准接口访问底层设备,因此设备驱动程序的升级、更替不会对上层应用产生影响。这种方式使得设备的硬件操作相关的代码能够独立于应用程序而存在,双方只需关注各自的功能实现,从而降低了代码的耦合性、复杂性,提高了系统的可靠性。I/O设备管理层所包含的I/O设备管理接口有rt_device_find、open、read、write、close、register等。
设备驱动框架层是对同类硬件设备驱动的抽象,将不同厂家的同类硬件设备驱动中相同的部分抽取出来,将不同部分留出接口,由驱动程序实现。
设备驱动层是一组驱使硬件设备工作的程序,实现了访问硬件设备的功能,它负责创建和注册I/O设备。设备驱动层注册设备有以下两种方式。
第一种方式,使用I/O设备管理接口直接注册,在设备驱动文件中通过rt_device_register()接口注册到I/O设备管理器中。这种方式是针对操作逻辑简单的设备,可以不经过设备驱动框架层,直接将设备注册到I/O设备管理器中。这类设备的注册与使用序列图如图1-4所示,其主要步骤如下。
图1-4 I/O设备注册与使用序列图
1)设备驱动根据设备模型定义,创建出具备硬件访问能力的设备实例,将该设备通过rt_device_register()接口注册到I/O设备管理器中。
2)应用程序通过rt_device_find()接口查找到设备,然后使用I/O设备管理接口来访问硬件。
第二种方式,通过设备驱动框架层提供的注册函数进行注册,注册函数名一般命名为rt_hw_xxx_register(),设备驱动框架层的注册函数又调用了I/O设备管理接口的注册函数rt_device_register(),从而进行设备注册。此种注册方式是针对一些不能使用I/O设备管理接口完成操作的设备,如看门狗等。看门狗设备注册的主要步骤如下。
1)看门狗设备驱动程序根据看门狗设备模型定义,创建出具备硬件访问能力的看门狗设备实例,并将该看门狗设备通过rt_hw_watchdog_register()接口注册到看门狗设备驱动框架中。
2)看门狗设备驱动框架通过rt_device_register()接口将看门狗设备注册到I/O设备管理器中。
3)应用程序通过rt_device_find()接口查找到设备,然后使用I/O设备管理接口来访问看门狗设备硬件。
看门狗设备注册与使用序列如图1-5所示。
当然,有的设备驱动框架也会给应用层提供接口,此时应用层可以调用设备驱动框架层的接口对硬件进行操作。
图1-5 看门狗设备注册与使用序列
1.2.1 I/O设备模型与分类
RT-Thread的I/O设备模型(以下简称“设备模型”)是建立在内核对象模型基础之上的,设备被认为是一类对象,被纳入对象管理器的范畴。每个设备对象都是由基对象派生而来的,每个具体设备都可以继承其父类对象的属性,并派生出其私有属性。图1-6是设备对象的继承和派生关系示意。
图1-6 设备对象的继承和派生关系
设备对象struct rt_device的具体定义如下所示:
rt_device_class_type用于RT-Thread对设备进行分类,在每类设备执行注册后,系统会将它们注册为相应类别的设备。rt_device_class_type类型枚举如下。
其中,字符设备、块设备是常用的设备类型,它们的分类依据是设备与系统之间的数据传输处理方式。字符设备允许非结构化的数据传输,通常数据传输采用串行的形式,每次一字节。字符设备通常是一些简单设备,如串口、按键。
块设备每次传输一个数据块,例如每次传输512字节数据。这个数据块大小是硬件强制性要求的,数据块可能使用某类数据接口或某些强制性的传输协议,否则就可能发生错误。因此,有时块设备驱动程序进行读/写操作时必须执行附加的工作,如图1-7所示。
图1-7 块设备操作
当系统服务需要进行大量数据的写操作时,设备驱动程序必须将数据划分为多个包,每个包采用设备指定的数据尺寸。而在实际操作中,最后一部分数据尺寸有可能小于正常的设备块尺寸。如图1-7中每个块使用单独的写请求写入到设备中,前3个块直接进行写操作。但最后一个数据块尺寸小于设备块尺寸,设备驱动程序必须使用不同的方式处理最后的数据块。通常情况下,设备驱动程序需要首先执行相对应的设备块(块4)的读操作,然后把写入数据覆盖到读出数据上,然后把这个“合成”的数据块作为一整个块写回到设备中。
1.2.2 I/O设备管理接口
应用程序通过I/O设备管理接口来访问硬件设备,当设备驱动实现后,应用程序就可以访问该硬件。I/O设备管理接口与I/O设备的操作方法的映射关系如图1-8所示。
图1-8 I/O设备管理接口与I/O设备的操作方法的映射关系
在执行过程中,若未得到正常结果,则需返回相应的错误码,错误码表如下所示。一般来说,在RT-Thread代码中,当返回错误码时,除RT_EOK之外的所有错误码都要加负号。
代码清单1-1 错误码表
1.查找设备
应用程序根据设备名称查找设备,查找接口(即函数)会返回设备的句柄,进而可以操作设备。查找设备的接口rt_device_find如下所示:
rt_device_find接口的参数及返回值如表1-1所示。
表1-1 rt_device_find接口的参数及返回值
2.初始化设备
获得设备句柄后,应用程序可使用rt_device_init接口对设备进行初始化操作:
rt_device_init接口的参数及返回值如表1-2所示。
表1-2 rt_device_init接口的参数及返回值
注意,当一个设备已经初始化成功后,调用这个接口将不再重复进行初始化。
3.打开和关闭设备
通过设备句柄,应用程序可以打开和关闭设备。打开设备时,系统会检测设备是否已经初始化,若没有初始化,会默认调用初始化接口来初始化设备,可通过rt_device_open接口打开设备:
rt_device_open接口的参数及返回值如表1-3所示。
表1-3 rt_device_open接口的参数及返回值
oflags支持以下参数:
注意:如果上层应用程序需要设置设备的接收回调函数,则必须以RT_DEVICE_FLAG_INT_RX或者RT_DEVICE_FLAG_DMA_RX的方式打开设备。
应用程序打开设备完成读写等操作后,如果不需要再对设备进行操作,就可以关闭设备,可通过rt_device_close接口完成:
rt_device_close接口的参数及返回值如表1-4所示。
表1-4 rt_device_close接口的参数及返回值
注意:关闭设备接口和打开设备接口需配对使用,打开一次设备对应要关闭一次设备,这样设备才会被完全关闭,否则设备仍处于未关闭状态。
4.控制设备
通过命令控制字,应用程序也可以对设备进行控制,通过rt_device_control接口完成:
rt_device_control接口的参数及返回值如表1-5所示。
参数cmd的通用设备命令可取如下宏定义:
表1-5 rt_device_control接口的参数及返回值
5.读写设备
应用程序从设备中读取数据可以通过rt_device_read接口完成:
rt_device_read接口的参数及返回值如表1-6所示。
表1-6 rt_device_read接口的参数及返回值
调用这个接口,会从dev中读取数据,并存放在缓冲区中。这个缓冲区的最大长度是size, pos根据不同的设备类别有不同的意义。
如果向设备中写入数据,可以通过rt_device_write接口完成:
rt_device_write接口的参数及返回值如表1-7所示。
表1-7 rt_device_write接口的参数及返回值
调用这个接口,会把缓冲区中的数据写入dev中,写入数据的最大长度是size, pos根据不同的设备类别有不同的意义。
6.数据收发回调
数据收发回调的意思是当硬件设备接收到数据或者发送数据时,可以设置一个回调函数作为数据发送或者接收的通知。当RT-Thread的设备进行数据收发时,也可以通过rt_device_set_rx_indicate接口回调另一个函数来设置数据接收指示,通知上层应用线程有数据到达:
rt_device_set_rx_indicate接口的参数及返回值如表1-8所示。
表1-8 rt_device_set_rx_indicate接口的参数及返回值
该接口的回调函数由调用者提供。当硬件设备接收到数据时,会回调这个接口并把收到的数据长度放在size参数中传递给上层应用。上层应用线程应在收到指示后,立刻从设备中读取数据。
在应用程序调用rt_device_write写入数据时,如果底层硬件能够支持自动发送,那么上层应用可以设置一个回调函数。这个回调函数会在底层硬件数据发送完成后(例如DMA传送完成或FIFO写入完毕产生完成中断时)调用。我们可以通过rt_device_set_tx_complete接口设置设备发送完成指示,接口参数及返回值如下:
rt_device_set_tx_complete接口参数及返回值如表1-9所示。
表1-9 rt_device_set_tx_complete接口参数及返回值
调用这个接口时,回调函数由调用者提供,当硬件设备发送完数据时,由驱动程序回调这个接口并把发送完成的数据块地址buffer作为参数传递给上层应用。上层应用(线程)在收到指示时会根据发送buffer的情况,释放buffer内存块或将其作为下一个写数据的缓存。
7.设备访问示例
下面代码为用程序访问设备的示例,首先通过rt_device_find()查找到看门狗设备,获得设备句柄,然后通过rt_device_init()初始化设备,并通过rt_device_control()设置看门狗设备溢出时间。
1.2.3 驱动编写流程与规范
驱动编写流程是本书中所有设备都要用到的,如图1-9所示。
图1-9 驱动编写流程
RT-Thread设备对应的设备驱动框架源码在rt-thread/components/drivers文件夹中。在该文件夹中查看代码,找到对应框架的注册函数和操作方法即(ops),如图1-10所示。
图1-10 RT-Thread设备驱动框架源码位置
在驱动中完成驱动框架提供的设备结构、操作函数和注册函数,然后进行驱动验证。
编写好的驱动代码风格可以参考RT-Thread代码规范进行格式化。若要提交到RT-Thread仓库,则代码风格必须遵守RT-Thread代码规范:https://gitee.com/rtthread/rt-thread/blob/master/documentation/contribution_guide/coding_style_cn.md。