设备与驱动:I/O设备模型
大部分的嵌入式系统都包括一些I/O设备,例如仪器上的数据显示屏、工业设备上的串口通信、数据采集设备上模拟数据采样、用于保存数据的Flash/SD卡以及网络设备上的以太网接口等,都是嵌入式系统中容易找到的I/O设备例子。
本专栏主要是分享RT-Thread是如何对不同的I/O设备进行管理的。读完本专栏,我们会了解到如何从宏观角度去管理I/O设备,为我们在编写代码时有一个参考的管理模型,从而提高我们代码的复用性、降低耦合性,从而实现换硬件不动软件的目标。
1、《设备与驱动:I/O设备模型》
2、《设备与驱动:UART设备》
3、《设备与驱动:PIN设备》
目录
一、I/O设备介绍
1.1、I/O设备模型框架
1.2、I/O设备模型
1.3、I/O设备类型
二、创建和注册I/O设备
2.1、访问I/O设备
2.2、查找设备
2.3、初始化设备
2.4、打开或关闭设备
2.5、控制设备
2.6、读写设备
2.7、数据收发回调
一、I/O设备介绍
1.1、I/O设备模型框架
如图所示:,I/O设备位于硬件和应用程序之间,分为三层,从上到下分别是I/O设备管理层、设备驱动框架层、设备驱动层。
应用程序通过I/O设备管理接口获得正确的设备驱动,然后通过这个设备驱动与底层I/O硬件设备进行数据(或控制)交互。
I/O设备管理层实现了对设备驱动程序的封装。应用程序通过图中的“I/O设备管理层”提供的标准接口访问底层设备,设备驱动程序的升级、更替不会对上层应用产生影响。这种方式使得设备的硬件操作相关的代码能够独立于应用程序而存在,双方只需关注各自的功能实现,从而降低了代码的耦合性、复杂性,提高了系统的可靠性。
设备驱动框架是对同类硬件设备驱动的抽象,将不同厂家的同类硬件设备驱动中相同的部分抽取出来,将不同部分留出接口,由驱动程序实现。
设备驱动层是一组驱使硬件设备工作的程序,实现访问硬件设备的功能。它负责创建和注册I/O设备,对于操作逻辑简单的设备,可以不经过设备驱动框架层,直接将设备注册到I/O设备管理器中。
1.2、I/O设备模型
RT-Thread的设备模型是建立在内核对象模型基础之上的,设备被认为是一类对象,被纳入对象管理器的范畴。每个设备对象都是由基对象派生而来,每个具有设备都可以继承父类对象的属性,并派生出其私有属性。
设备对象具体定义如下所示:
struct rt_device{struct rt_object parent; /* 内核对象基类 */enum rt_device_class_type type; /* 设备类型 */rt_uint16_t flag; /* 设备参数 */rt_uint16_t open_flag; /* 设备打开标志 */rt_uint8_t ref_count; /* 设备被引用次数 */rt_uint8_t device_id; /* 设备 ID,0 - 255 *//* 数据收发回调函数 */rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size);rt_err_t (*tx_complete)(rt_device_t dev, void *buffer);const struct rt_device_ops *ops; /* 设备操作方法 *//* 设备的私有数据 */void *user_data;};typedef struct rt_device *rt_device_t;
1.3、I/O设备类型
RT-Thread支持多种I/O设备类型,主要设备类型如下所示:
RT_Device_Class_Char /* 字符设备 */RT_Device_Class_Block /* 块设备 */RT_Device_Class_NetIf /* 网络接口设备 */RT_Device_Class_MTD /* 内存设备 */RT_Device_Class_RTC /* RTC 设备 */RT_Device_Class_Sound /* 声音设备 */RT_Device_Class_Graphic /* 图形设备 */RT_Device_Class_I2CBUS /* I2C 总线设备 */RT_Device_Class_USBDevice /* USB device 设备 */RT_Device_Class_USBHost /* USB host 设备 */RT_Device_Class_SPIBUS /* SPI 总线设备 */RT_Device_Class_SPIDevice /* SPI 设备 */RT_Device_Class_SDIO /* SDIO 设备 */RT_Device_Class_Miscellaneous /* 杂类设备 */
其中字符设备、块设备是常用的设备类型,它们的分类依据是设备数据与系统之间的传输处理方式。字符模式设备允许非结构的数据传输,即通常数据传输采用串行的形式,每次一个字节。字符设备通常是一些简单设备。如串口、按键等。
块设备每次传输一个数据块,例如每次传输512个字节数据。这个数据块是硬件强制性的,数据块可能使用某类数据接口或某些强制性的传输协议,否则就可能发生错误。因此,有时块设备驱动程序对读或写操作必须执行附加的一些工作,如下图所示:
当系统服务于一个具有大量数据的写操作时,设备驱动程序必须首先将数据划分为多个包,每个包采用设备指定的数据尺寸。而在实际过程中,最后一部分数据尺寸有可能小于正常的设备块尺寸。如上图所示,每个块使用单独的写请求写入设备中,头3个直接进行写操作。但最后一个数据块尺寸小于设备块尺寸,设备驱动程序必须使用不同于前3个块的方式处理最后的数据块。通常情况下,设备驱动程序需要首先执行相对应的设备块的读操作,然后把写入数据覆盖到读出的数据上,然后再把这个“合成”的数据块作为一整个块写回到设备中。例如上图中的块4,驱动程序需要先把块4所对应的设备块读出来,然后将需要写入的数据覆盖至从设备块读出的数据上,使其合并成一个新的块,最后再写回到块设备中。
二、创建和注册I/O设备
驱动层负责创建设备实例,并注册到I/O设备管理器中,可以通过静态申明的方式创建设备实例,也可以动态创建。
下述代码为RT-Thread源码定义:
struct rt_device_ops{/* common device interface */rt_err_t (*init) (rt_device_t dev);rt_err_t (*open) (rt_device_t dev, rt_uint16_t oflag);rt_err_t (*close) (rt_device_t dev);rt_size_t (*read) (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size);rt_size_t (*write) (rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size);rt_err_t (*control)(rt_device_t dev, int cmd, void *args);};
操作方法如下表所示:
方法名称 | 方法描述 |
init | 初始化设备。设备初始化完成后,设备控制块的 flag 会被置成已激活状态 (RT_DEVICE_FLAG_ACTIVATED)。如果设备控制块中的 flag 标志已经设置成激活状态,那么再运行初始化接口时会立刻返回,而不会重新进行初始化。 |
open | 打开设备。有些设备并不是系统一启动就已经打开开始运行,或者设备需要进行数据收发,但如果上层应用还未准备好,设备也不应默认已经使能并开始接收数据。所以建议在写底层驱动程序时,在调用 open 接口时才使能设备。 |
close | 关闭设备。在打开设备时,设备控制块会维护一个打开计数,在打开设备时进行 + 1 操作,在关闭设备时进行 - 1 操作,当计数器变为 0 时,才会进行真正的关闭操作。 |
read | 从设备读取数据。参数 pos 是读取数据的偏移量,但是有些设备并不一定需要指定偏移量,例如串口设备,设备驱动应忽略这个参数。而对于块设备来说,pos 以及 size 都是以块设备的数据块大小为单位的。例如块设备的数据块大小是 512,而参数中 pos = 10, size = 2,那么驱动应该返回设备中第 10 个块 (从第 0 个块做为起始),共计 2 个块的数据。这个接口返回的类型是 rt_size_t,即读到的字节数或块数目。正常情况下应该会返回参数中 size 的数值,如果返回零请设置对应的 errno 值。 |
write | 向设备写入数据。参数 pos 是写入数据的偏移量。与读操作类似,对于块设备来说,pos 以及 size 都是以块设备的数据块大小为单位的。这个接口返回的类型是 rt_size_t,即真实写入数据的字节数或块数目。正常情况下应该会返回参数中 size 的数值,如果返回零请设置对应的 errno 值。 |
control | 根据 cmd 命令控制设备。命令往往是由底层各类设备驱动自定义实现。例如参数 RT_DEVICE_CTRL_BLK_GETGEOME,意思是获取块设备的大小信息。 |
当一个动态创建的设备不再需要时,需要通过调用特定的函数进行销毁。
当设备被创建后,需要注册到I/O设备管理器中,应用程序才能够访问。
调用注册函数时可以设置模式标志,标志信息如下:
#define RT_DEVICE_FLAG_RDONLY 0x001 /* 只读 */#define RT_DEVICE_FLAG_WRONLY 0x002 /* 只写 */#define RT_DEVICE_FLAG_RDWR 0x003 /* 读写 */#define RT_DEVICE_FLAG_REMOVABLE 0x004 /* 可移除 */#define RT_DEVICE_FLAG_STANDALONE 0x008 /* 独立 */#define RT_DEVICE_FLAG_SUSPENDED 0x020 /* 挂起 */#define RT_DEVICE_FLAG_STREAM 0x040 /* 流模式 */#define RT_DEVICE_FLAG_INT_RX 0x100 /* 中断接收 */#define RT_DEVICE_FLAG_DMA_RX 0x200 /* DMA 接收 */#define RT_DEVICE_FLAG_INT_TX 0x400 /* 中断发送 */#define RT_DEVICE_FLAG_DMA_TX 0x800 /* DMA 发送 */
2.1、访问I/O设备
应用程序通过I/O设备管理接口来访问硬件设备,当设备驱动实现后,应用程序就可以访问该硬件。
2.2、查找设备
应用程序根据设备名称获取设备句柄,进而可以操作设备。
2.3、初始化设备
获得设备句柄后,应用程序可调用相关函数对设备进行初始化操作。
2.4、打开或关闭设备
通过设备句柄,应用程序可以打开或关闭设备,打开设备时,会检测设备是否已经初始化,没有初始化则默认调用初始化接口对设备进行初始化。
2.5、控制设备
通过命令控制字,应用程序可以对设备进行控制。
设备命令如下:
#define RT_DEVICE_CTRL_RESUME 0x01 /* 恢复设备 */#define RT_DEVICE_CTRL_SUSPEND 0x02 /* 挂起设备 */#define RT_DEVICE_CTRL_CONFIG 0x03 /* 配置设备 */#define RT_DEVICE_CTRL_SET_INT 0x10 /* 设置中断 */#define RT_DEVICE_CTRL_CLR_INT 0x11 /* 清中断 */#define RT_DEVICE_CTRL_GET_INT 0x12 /* 获取中断状态 */
2.6、读写设备
应用程序可以通过相关函数写入设备句柄读写数据。
2.7、数据收发回调
当硬件设备收到数据时,可以通过相关函数回调来设置数据接收指示。通知上层应用线程有数据到达。该回调函数由调用者提供。当硬件设备接收到数据时,会回调这个函数并把接收到的数据长度传递给上层应用。上层应用线程应在收到指示后。立即从设备中读取数据。
应用程序调用写入函数写入数据时,如果底层硬件能够支持自动发送,那么上层应用可以设置一个回调函数。这个函数会在底层硬件数据发哦是那个完成后(例如:DMA传送完成或FIFO已经写入完毕产生完成中断时)调用。调用写入函数时,回调函数由调用者提供,当硬件设备发送完数据时,由驱动程序回调这个函数并把发送完成的数据块地址Buffer作为参数传递给上层应用。上层应用(线程)在收到指示后会根据发送Buffer的情况,释放Buffer内存块或将其作为下一个写数据的缓存。