RT-thread内核之IO设备管理系统
RT-Thread系统的IO设备管理模块为上层应用提供了一个对设备进行访问的通用抽象接口,而对于下层设备来说则提供了底层设备驱动框架,并通过定义的数据结构对设备信息和底层设备驱动进行管理。从系统整体位置来说I/O设备管理模块相当于底层设备驱动和上层应用之间的一个中间层。
I/O管理模块实现了对设备驱动程序的封装:设备驱动程序的实现与I/O管理模块独立,提高了模块的可移植性。应用程序通过I/O管理模块提供的标准接口访问底层设备,设备驱动程序的升级不会对上层应用产生影响。这种方式使得与设备的硬件操作相关的代码与应用相隔离,双方只需各自关注自己的功能,这降低了代码的复杂性,提高了系统的可靠性。
一、I/O设备管理控制块:在include/rtdef.h中:
typedef struct rt_device *rt_device_t; /** * Device structure */ struct rt_device { struct rt_object parent; /**< inherit from rt_object *///内核对象 enum rt_device_class_type type; /**< device type */ //IO设备类型 rt_uint16_t flag; /**< device flag */ //设备标志 rt_uint16_t open_flag; /**< device open flag */ //设备打开标志 rt_uint8_t ref_count; /**< reference count */ //打开计数值。设备注册时初始为0,每打开一次加1,每关闭一次减1。主要用于设备多次打开后,要关闭时判断是否完全关闭。 rt_uint8_t device_id; /**< 0 - 255 */ //设备ID /* device call back */ rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size); //数据接收回调函数 rt_err_t (*tx_complete)(rt_device_t dev, void *buffer); //数据发送完回调函数 /* 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, rt_uint8_t cmd, void *args); //控制通用接口 void *user_data; /**< device private data */ //私有数据 };
在以前版本的设备控制块中,还有以下:
/* 用于支持电源管理的函数接口 */
#ifdef RT_USING_DEVICE_SUSPEND
rt_err_t (*suspend) (rt_device_t dev); //挂起设备
rt_err_t (*resumed) (rt_device_t dev); //还原设备
#endif
从设备控制块,我们可以看到,每个设备对象都会在内核中维护一个设备控制块结构,这种结构是使设备对象继承rt_object基类,然后形成rt_device设备类型。
其中设备类型type为一枚举变量,在include/rtdef.h中定义:
/** * device (I/O) class type */ enum rt_device_class_type { RT_Device_Class_Char = 0, /**< character device */ //字符设备 RT_Device_Class_Block, /**< block device */ //块设备 RT_Device_Class_NetIf, /**< net interface */ //网络设备 RT_Device_Class_MTD, /**< memory device */ //内存设备 RT_Device_Class_CAN, /**< CAN device */ //CAN设备 RT_Device_Class_RTC, /**< RTC device */ //RTC设备 RT_Device_Class_Sound, /**< Sound device */ //音频设备 RT_Device_Class_Graphic, /**< Graphic device */ //图形显示设备 RT_Device_Class_I2CBUS, /**< I2C bus device */ //I2C总线设备 RT_Device_Class_USBDevice, /**< USB slave device */ //USB从设备 RT_Device_Class_USBHost, /**< USB host bus */ //USB主设备 RT_Device_Class_SPIBUS, /**< SPI bus device */ //SPI总线设备 RT_Device_Class_SPIDevice, /**< SPI device */ //SPI接口设备 RT_Device_Class_SDIO, /**< SDIO bus device */ //SDIO总线设备 RT_Device_Class_PM, /**< PM pseudo device */ //电源管理伪设备 RT_Device_Class_Pipe, /**< Pipe device */ //管道设备 RT_Device_Class_Portal, /**< Portal device */ //传输设备 RT_Device_Class_Miscellaneous, /**< Miscellaneous device *///其他设备 RT_Device_Class_Unknown /**< unknown device */ //未知设备 };
二、I/O设备管理函数接口:在src/device.c中
注册设备: rt_err_t rt_device_register(rt_device_t dev, //设备句柄 const char *name, //设备名称 rt_uint16_t flags); //设备标志 一个设备能够被上层应用访问前,需要先把这个设备注册到系统中,并添加一些相应的属性。这些注册的设备均可以通过设备名,采用“查找设备口”的方式来查找,从而获得该设备控制块(或设备句柄)。 flags参数支持下列参数(可以采用或的方式支持多种参数): #define RT_DEVICE_FLAG_DEACTIVATE 0x000 /**< device is not not initialized *///未初始化设备 #define RT_DEVICE_FLAG_RDONLY 0x001 /**< read only */ //只读设备 #define RT_DEVICE_FLAG_WRONLY 0x002 /**< write only */ //只写设备 #define RT_DEVICE_FLAG_RDWR 0x003 /**< read and write */ //读写设备 #define RT_DEVICE_FLAG_REMOVABLE 0x004 /**< removable device */ //可移除设备 #define RT_DEVICE_FLAG_STANDALONE 0x008 /**< standalone device */ //独立设备 #define RT_DEVICE_FLAG_ACTIVATED 0x010 /**< device is activated */ //已激活设备 #define RT_DEVICE_FLAG_SUSPENDED 0x020 /**< device is suspended */ //挂起设备 #define RT_DEVICE_FLAG_STREAM 0x040 /**< stream mode */ //设备处于流模式 #define RT_DEVICE_FLAG_INT_RX 0x100 /**< INT mode on Rx */ //设备处于中断接收模式 #define RT_DEVICE_FLAG_DMA_RX 0x200 /**< DMA mode on Rx */ //设备处于DMA接收模式 #define RT_DEVICE_FLAG_INT_TX 0x400 /**< INT mode on Tx */ //设备处于中断发送模式 #define RT_DEVICE_FLAG_DMA_TX 0x800 /**< DMA mode on Tx */ //设备处于DMA发送模式 设备流模式RT_DEVICE_FLAG_STREAM参数用于向串口终端输出字符串:当输出的字符是“\n”时,自动在前面补一个“\r”做分行。警告:应当避免重复注册已经注册的设备,以及注册已有名字的设备驱动程序。 卸载设备: rt_err_t rt_device_unregister(rt_device_t dev); 将设备从设备系统中卸载,被卸载的设备将不能再通过“查找设备接口”被查找到。注:卸载设备并不会释放设备控制块所占用的内存。
初始化所有设备: rt_err_t rt_device_init_all(void); 初始化所有注册到设备对象管理器中的未初始化的设备。注:自版本1.2.x,该函数不再需要在系统初始化调用,因为在上层应用中打开设备时会进行相应设备初始化。 初始化设备: rt_err_t rt_device_init(rt_device_t dev); 当一个设备初始化完成后它的flags域中的RT_DEVICE_FLAG_ACTIVATED应该被置位。如果设备的flags域已经是RT_DEVICE_FLAG_ACTIVATED,调用这个接口将不再重复做初始化。返回dev->init函数返回值RT_EOK。
注:在设备打开时rt_device_open时会调用该函数先初始化需要打开的设备。
查找设备: rt_device_t rt_device_find(const char *name); 使用这个函数接口时,系统会在设备对象类型所对应的对象容器中根据设备名称遍历寻找设备对象,然后返回该设备句柄,如果没有找到相应的设备对象,则返回RT_NULL。
打开设备: rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflag); 根据设备控制块来打开设备,其中访问模式oflags支持以下列表中的参数: #define RT_DEVICE_OFLAG_CLOSE 0x000 /**< device is closed */ //已关闭模式 #define RT_DEVICE_OFLAG_RDONLY 0x001 /**< read only access */ //只读模式访问 #define RT_DEVICE_OFLAG_WRONLY 0x002 /**< write only access *///只写模式访问 #define RT_DEVICE_OFLAG_RDWR 0x003 /**< read and write */ //读写模式访问 #define RT_DEVICE_OFLAG_OPEN 0x008 /**< device is opened */ //已打开模式 返回dev->open函数返回值。注: 如果设备flags域包含RT_DEVICE_FLAG_STANDALONE参数,将不允许重复打开。 关闭设备: rt_err_t rt_device_close(rt_device_t dev); 根据设备控制块来关闭设备。返回dev->close函数返回值。
读设备: rt_size_t rt_device_read(rt_device_t dev, //设备句柄 rt_off_t pos, //待读取数据的偏移量 void *buffer, //读取的数据存放地址 rt_size_t size) //读取数据的大小 根据设备控制块来读取设备。根据底层驱动的实现,通常这个接口并不会阻塞上层应用线程。 返回读到数据的实际大小(以字节为单位);如果返回0,则需要读取当前线程的errno来判断错误状态。 写设备: rt_size_t rt_device_write(rt_device_t dev, //设备句柄 rt_off_t pos, //待写入数据的存放偏移量 const void *buffer, //待写入数据源地址 rt_size_t size) //待写入数据大小 根据设备控制块来写入设备。根据底层驱动的实现,通常这个接口也不会阻塞上层应用线程。 返回写入数据的实际大小(以字节为单位);如果返回0,则需要读取当前线程的errno来判断错误状态。
注: 在RT-Thread的块设备中,从1.0.0版本开始,rt_device_read()和rt_device_write()接口的pos、size参数按照以块为单位。0.3.x以前的版本则按字节为单位。
控制设备: rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void *arg); dev:设备句柄 cmd:命令控制字,这个参数通常与设备驱动程序相关 arg:控制的参数 返回dev->control函数返回值。 设置数据接收指示回调函数: rt_err_t rt_device_set_rx_indicate(rt_device_t dev, //设备句柄 rt_err_t (*rx_ind)(rt_device_t dev, rt_size_t size))//接收回调函数 设置一个回调函数,当硬件设备收到数据时回调以通知用程序有数据到达,但一般只是进行些简单的操作,如释放信号量,而让另一个接收线程来处理接收到的数据 在调用这个函数时,回调函数rx_ind由调用者提供。当硬件设备接收到数据时,会回调这个函数并把收到的数据长度放在size参数中传递给上层应用。上层应用线程应在收到指示后,立刻从设备中读取数据。返回RT_EOK。 设置发送完成指示回调函数: rt_err_trt_device_set_tx_complete(rt_device_t dev, //设备句柄 rt_err_t (*tx_done)(rt_device_t dev, void *buffer))//发送回调函数
在上层应用调用rt_device_write写入数据时,如果底层硬件能够支持自动发送,那么上层应用可以设置一个回调函数。这个回调函数会在底层硬件给出的发送完成后(例如DMA传送完成或FIFO已经写入完毕产生完成中断时)被调用。 调用这个函数时,回调函数tx_done参数由调用者提供,当硬件设备发送完数据时,由驱动程序回调这个函数并把发送完成的数据块地址buffer做为参数传递给上层应用。上层应用(线程)在收到指示时应根据发送buffer的情况,释放buffer内存块或将其做为下一个写数据的缓存。返回RT_EOK。
三、设备底层驱动实现:
这些驱动实现的底层接口是上层应用最终访问的落脚点,例如上层应用调用rt_device_read接口进行设备读取数据操作,上层应先调用rt_device_find获得相对应的设备句柄,而在调用rt_device_read时,就是使用这个设备句柄所对应驱动的driver_read。上述的接口是一一对应关系。I/O设备模块提供的这六个接口(rt_device_init/open/read/write/control),对应到设备驱动程序的六个接口(driver_init/open/read/write/control等),可以认为是底层设备驱动必须提供的接口。
init:设备初始化。设备初始化后,设备控制块的flag会被置成激活状态(RT_DEVICE_FLAG_ACTIVATED)。如果设备控制块中的flag标志已经设置成激活状 态,那么再运行初始化接口时,会立刻返回,而不会重新进行初始化。
open:打开设备。有些设备并不是系统一启动就已经打开开始运行或者需要进行数据接收,但如果上层应用还未准备好,设备也不应默认已经使能并开始接收数据。所以建议在写底层驱动程序时,应在调用open接口时才使能设备。
close:关闭设备。建议在打开设备时,设备驱动自行维护一个打开计数,在打开设备时进行+1操作,在关闭设备时进行-1操作, 当计数器变为0时,进行真正的关闭操作。
read:从设备中读取数据。参数pos指出读取数据的偏移量,但是有些设备并不一定需要指定偏移量,例如串口设备,设备驱动应忽略这个参数。而对于块设备来说,pos以及size都是以块设备的 数据块大小做为单位的。这个接口 返回的 类型是rt_size_t,即读到的字节数或块数目。正常情况下应 该会返回参数中size的数值,如果返回零请设置对应的errno值。
write:向设备中写入数据。参数pos指出写入数据的偏移量。与读操作类似,对于块设备来说,pos以及size都是以块设备的数据块 大小做为单位的。这个接口返回的类型是rt_size_t,即真实写入数据的字节数或块数目。正常情况下应该会返回参数中size的数值,如果返回零请设置对应的errno值。
control:根据不同的cmd命令控制设备。命令往往是由底层各类设备驱动自定义实现。例如参数RT_DEVICE_CTRL_BLK_GETGEOME,意思是获取块设备的大小信息。
底层驱动实现步骤:
• 按照RT-Thread的对象模型,扩展一个对象有两种方式:
(1)定义自己的私有数据结构,然后赋值到RT-Thread设备控制块的user_data指针上;
(2)从struct rt_device结构中进行派生。
• 实现RT-Thread I/O设备模块中定义的6个公共设备接口,开始可以是空函数(返回类型是rt_err_t的可默认返回RT_EOK);
• 根据自己的设备类型定义自己的私有数据域。特别是在可能有多个相类似设备的情况下(例如串口1、2),设备接口可以共用同一套接口,不同的只是各自的数据域(例如寄存器基地址);
• 根据设备的类型,注册到RT-Thread设备框架中,即调用rt_device_register接口进行注册。