I/O设备模型
绝大部分的嵌入式系统都包括一些I/O(Input/Output,输入/输出)设备,例如仪器上的数据显示屏、工业设备上的串口通信、数据采集设备上用于保存数据的Flash或SD卡,以及网络设备的以太网接口等,都是嵌入式系统中容易找到的I/O设备例子。
I/O设备模型框架
RTT提供了一套简单的I/O设备模型框架,如图所示,它位于硬件和应用程序之间,共分成三层,从上到下分别是I/O设备管理层,设备驱动框架层,设备驱动层。
应用程序通过I/O设备管理接口获得正确的设备驱动,然后通过这个设备驱动与底层I/O硬件设备进行数据交互。
I/O设备管理层实现了对设备驱动程序的封装。
应用程序通过图中的I/O设备管理层提供的标准接口访问底层设备,设备驱动程序的升级、更替不会对上层应用产生影响。
这种方式使得设备的硬件操作相关的代码能够独立于应用程序而存在,双方只需关注各自的功能实现,从而降低了代码的耦合性、复杂性,提高了系统的可靠性。
设备驱动框架层是对同类硬件设备驱动的抽象,将不同厂家的同类硬件设备驱动中相同的部分抽取出来,将不同部分留出接口,由驱动程序实现。
设备驱动层是一组驱使硬件设备工作的程序,实现访问硬件设备的功能。它负责创建和注册I/O设备,对于操作逻辑简单的设备,可以不经过设备驱动框架层,直接将设备注册到I/O设备管理器中。
- 设备驱动根据设备模型定义,创建出具备硬件访问能力的设备实例,将该设备通过rt_device_register()接口注册到I/O设备管理器中。
- 应用程序通过rt_device_find()接口查找到设备,然后使用I/O设备管理接口来访问硬件。
对于另一些设备,如看门狗等,则会将创建的设备实例先注册到对应的设备驱动框架中,再由设备驱动向I/O设备管理器进行注册,主要有以下几点:
- 看门狗设备驱动程序根据看门狗设备模型定义,创建出具备硬件访问能力的看门狗设备实例,并将该看门狗设备通过rt_hw_watchdog_register()接口注册到看门狗设备驱动框架中。
- 看门狗设备驱动框架
- 应用程序通过I/O设备管理接口来访问看门狗设备硬件。
I/O设备模型
RTT的设备模型是建立在内核对象模型基础之上的,设备被认为是一类对象,被纳入对象管理器的范畴。每个设备对象都是由基对象派生而来,每个具体设备都可以继承其父类对象的属性,并派生其私有属性。
struct rt_object
{if RT_NAME_MAX > 0char name[RT_NAME_MAX]; /**< dynamic name of kernel object */
#elseconst char *name; /**< static name of kernel object */
#endif /* RT_NAME_MAX > 0 */rt_uint8_t type; /**< type of kernel object */rt_uint8_t flag; /**< flag of kernel object */#ifdef RT_USING_MODULEvoid * module_id; /**< id of application module */
#endif /* RT_USING_MODULE */#ifdef RT_USING_SMARTrt_atomic_t lwp_ref_count; /**< ref count for lwp */
#endif /* RT_USING_SMART */rt_list_t list; /**< list node of kernel object */
};
typedef struct rt_object *rt_object_t;
struct rt_device
{struct rt_object parent;enum rt_device_class_type type;rt_uint16_t flag; /**< device flag */rt_uint16_t open_flag; /**< device open flag */rt_uint8_t ref_count; /**< reference count */rt_uint8_t device_id; /**< 0 - 255 *//* 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_ssize_t (*read) (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size);rt_ssize_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);void *user_data;
}
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 /* 杂类设备 */
其中字符设备、块设备是常用的设备类型,它们的分类依据是设备数据与系统之间的传输处理方式。
创建和注册I/O设备
驱动层负责创建设备实例,并注册到I/O设备管理器中。
rt_device_t rt_device_create(int type, int attach_size);
- type:设备类型,可取前面小节列出的设备类型值。
- attach_size:用户数据大小。
创建成功,返回设备句柄。
返回RT_NULL,表示动态内存分配失败。
调用该接口时,系统会从动态堆内存中分配一个设备控制块,大小为 struct rt_device 和 attach_size 的和,设备的类型由参数 type 设定。设备被创建后,需要实现它访问硬件的操作方法。
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设备管理器中,应用程序才能够访问。
rt_err_t rt_device_register(rt_device_t dev, const char *name, rt_uint16_t flags)
{if (dev == RT_NULL)return -RT_ERROR;if (rt_device_find(name) != RT_NULL)return -RT_ERROR;rt_object_init(&(dev->parent), RT_Object_Class_Device, name);dev->flag = flags;dev->ref_count = 0;dev->open_flag = 0;return RT_EOK;
}
flags参数支持下列参数
#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 发送 */
流模式RT_DEVICE_FLAG_STREAM参数用于向串口终端输出字符串:当输出的字符串是’\n’时,自动在前面补一个’\r’做分行。
struct rt_device_pin
{struct rt_device parent;const struct rt_pin_ops *ops;
}
struct rt_pin_ops
{void (*pin_mode)(struct rt_device *device, rt_base_t pin, rt_uint8_t mode);void (*pin_write)(struct rt_device *device, rt_base_t pin, rt_uint8_t value);rt_int8_t (*pin_read)(struct rt_device *device, rt_base_t pin);rt_err_t (*pin_attach_irq)(struct rt_device *device, rt_base_t pin,rt_uint8_t mode, void (*hdr)(void *args), void *args);rt_err_t (*pin_detach_irq)(struct rt_device *device, rt_base_t pin);rt_err_t (*pin_irq_enable)(struct rt_device *device, rt_base_t pin, rt_uint8_t enabled);rt_base_t (*pin_get)(const char *name);
};
int rt_device_pin_register(const char *name, const struct rt_pin_ops *ops, void *user_data)
{_hw_pin.parent.type = RT_Device_Class_Pin;_hw_pin.parent.rx_indicate = RT_NULL;_hw_pin.parent.tx_complete = RT_NULL;#ifdef RT_USING_DEVICE_OPS_hw_pin.parent.ops = &pin_ops;
#else_hw_pin.parent.init = RT_NULL;_hw_pin.parent.open = RT_NULL;_hw_pin.parent.close = RT_NULL;_hw_pin.parent.read = _pin_read;_hw_pin.parent.write = _pin_write;_hw_pin.parent.control = _pin_control;
#endif_hw_pin.ops = ops;_hw_pin.parent.user_data = user_data;/* register a character device */rt_device_register(&_hw_pin.parent, name, RT_DEVICE_FLAG_RDWR);return 0;
}
访问I/O设备
应用程序通过 I/O 设备管理接口来访问硬件设备,当设备驱动实现后,应用程序就可以访问该硬件。I/O 设备管理接口与 I/O 设备的操作方法的映射关系下图所示:
- 查找设备:rt_device_find(const char*name);
- 初始化设备:rt_device_init(rt_device_t dev);
- 打开设备:rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags);
如果上层应用需要设置设备的接收回调函数,则必须以RT_DEVICE_FLAG_INT_RX 或者 RT_DEVICE_FLAG_DMA_RX 的方式打开设备,否则不会回调函数。
数据收发回调
当硬件设备收到数据时,可以通过如下函数回调另一个函数来设置数据接收指示,通知上层应用线程有数据到达:
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));
该函数的回调函数由调用者提供。
当硬件设备接收到数据时,会回调这个函数并把收到的数据长度放在size参数中传递给上层应用。
设备访问示例
#include <rtthread.h>
#include <rtdevice.h>#define IWDG_DEVICE_NAME "wdt"static rt_device_t wdg_dev;static void idle_hook(void)
{/*在空闲线程的回调函数里喂狗*/rt_device_control(wdg_dev, RT_DEVICE_CTRL_WDT_KEEPALIVE, NULL);rt_kprintf("feed dog\n");
}int main(void)
{rt_err_t res = RT_EOK;rt_uint32_t timeout = 10;wdg_dev = rt_device_find(IWDG_DEVICE_NAME);if(!wdg_dev){rt_kprintf("find %s failed]n", IWDG_DEVICE_NAME);return RT_ERROR;}* 初始化设备 */res = rt_device_init(wdg_dev);if (res != RT_EOK){rt_kprintf("initialize %s failed!\n", IWDG_DEVICE_NAME);return res;}/* 设置看门狗溢出时间 */res = rt_device_control(wdg_dev, RT_DEVICE_CTRL_WDT_SET_TIMEOUT, &timeout);if (res != RT_EOK){rt_kprintf("set %s timeout failed!\n", IWDG_DEVICE_NAME);return res;}/* 设置空闲线程回调函数 */rt_thread_idle_sethook(idle_hook);return res;
}
图中各类里的c文件是各类对应的管理接口所在,比如设备基类rt_device的管理接口在device.c中。
开发者只需开发驱动层即可,设备驱动框架层和I/O设备管理层,RTT已经写好了,无需改动,除非发现BUG或增加新的类别。