原子操作
原子操作(Atomic operation)是指一种不可分割的操作,要么完全执行成功,要么完全不执行。
原子操作的执行过程中不允许有任何中断,如果出现了中断,那么操作的结果就无法保证。
原子操作通常用于多线程编程,保证多个线程之间并发执行不会出现数据竞争等问题。
在实现原子操作时,通常使用硬件指令或操作系统提供的原子操作函数来保证操作的原子性。
在应用层面,原子操作可以用于实现一些高级的同步和并发控制机制。例如,在多线程编程中,如果多个线程都需要访问同一个共享变量,为了避免数据竞争问题,可以使用原子操作来保证对该变量的操作是原子的。
原子操作的优点
在RTT中我们可以采取开关全局中断,调度器上锁方式对临界区资源进行保护,其它OS也会提供类似的操作,若采用原子操作后可以提高临界区代码的执行效率,大幅提升系统的运行效率,
同时也会在一定程度上降低编程的复杂度,下文是一个简单变量自增的示例:
...
int a = 5;
level = rt_hw_interrupt_disable();
a++;
rt_hw_interrupt_enable(level);
...
int a = 5;
rt_atomic_add(&a, 1);
显然采用原子操作的方式更加简单一些,且避免了开关全局中断带来的性能损失。
SMP
对称多处理(Symmetrical Multi-Processing)简称SMP,是指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构。
RTT在对称多核上可以通过使用RT_USING_SMP来开启。
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()接口注册到看门狗设备驱动框架中。
- 看门狗设备驱动框架通过rt_device_register()接口将看门狗设备注册到I/O设备管理器中。
- 应用程序通过I/O设备管理接口来访问看门狗设备硬件。
I/O设备模型
RTT的设备模型是建立在内核对象模型基础之上的,设备被认为是一类对象,被纳入对象管理器的范畴。
每个设备对象都是由基对象派生而来,每个具体设备都可以继承其父类对象的属性,并派生出其私有属性,下图是设备对象的继承和派生关系示意图。
设备基类
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
/*数据收发回调*/
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;
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_device_t rt_device_create(int type, int attach_size);
- type:设备类型,可取前面小节列出的设备类型值
- attach_size:用户数据大小
调用该接口时,系统会从动态堆内存中分配一个设备控制块,大小为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,意思是获取块设备的大小信息。
当一个动态创建的设备不再需要使用时可以通过如下函数来销毁:
void rt_device_destroy(rt_device_t device);
设备被创建后,需要注册到I/O设备管理器中,应用程序才能够访问
rt_err_t rt_device_register(rt_device_t dev, const char* name, rt_uint8_t flags);
应当避免重复注册已经注册的设备,以及注册相同名字的设备。
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’做分行。
注册成功的设备可以在FinSH命令行使用list_device命令查看系统中所有的设备信息,包括设备名称、设备类型和设备被打开次数。
msh />list_device
device type ref count
-------- -------------------- ----------
e0 Network Interface 0
sd0 Block Device 1
rtc RTC 0
uart1 Character Device 0
uart0 Character Device 2
msh />
当设备注销后,设备将从设备管理器中移除,也就不能再通过设备查找搜索到该设备。
注销设备不会释放设备控制块占用的内存。注销设备的函数如下所示:
rt_err_t rt_device_unregister(rt_device_t dev);
看门狗设备的注册实例
调用rt_hw_watchdog_register()接口后,设备通过rt_device_register()接口被注册到I/O设备管理器中。
const static struct rt_device_ops wdt_ops =
{
rt_watchdog_init,
rt_watchdog_open,
rt_watchdog_close,
RT_NULL,
RT_NULL,
rt_watchdog_control,
};
rt_err_t rt_hw_watchdog_register(struct rt_watchdog_device *wtd, const char *name, rt_uint32_t flag, void *data){
struct rt_device *device;
RT_ASSERT(wtd != RT_NULL);
device = &(wtd->parent);
device->type = RT_Device_Class_Miscellaneous;
device->rx_indicate = RT_NULL;
device->tx_complete = RT_NULL;
device->ops = &wdt_ops;
device->user_data = data;
/* register a character device */
return rt_device_register(device, name, flag);
}
访问I/O设备
应用程序通过I/O设备管理接口来访问硬件设备,当设备驱动实现后,应用程序就可以访问该硬件。
I/O设备管理接口与I/O设备的操作方法的映射关系如下图所示:
查找设备
应用程序根据设备名称获取设备句柄,进而可以操作设备。
rt_device_t rt_device_find(const char* name);
初始化设备
获得设备句柄后,对设备进行初始化
rt_err_t rt_device_init(rt_device_t dev);
注:当一个设备已经初始化成功后,调用这个接口将不再重复做初始化 0。
打开和关闭设备
通过设备句柄,应用程序可以打开和关闭设备,打开设备时,会检测设备是否已经初始化,没有初始化则会默认调用初始化接口初始化设备。
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags)
- 返回RT_EOK:设备打开成功
- 返回-RT_EBUSY:如果设备注册时指定的参数中包括RT_DEVICE_FLAG_STANDALONE参数,此设备将不允许重复打开
#define RT_DEVICE_OFLAG_CLOSE 0x000 /* 设备已经关闭(内部使用)*/
#define RT_DEVICE_OFLAG_RDONLY 0x001 /* 以只读方式打开设备 */
#define RT_DEVICE_OFLAG_WRONLY 0x002 /* 以只写方式打开设备 */
#define RT_DEVICE_OFLAG_RDWR 0x003 /* 以读写方式打开设备 */
#define RT_DEVICE_OFLAG_OPEN 0x008 /* 设备已经打开(内部使用)*/
#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_INT_RX或者RT_DEVICE_FLAG_DMA_RX的方式打开设备,否则不会回调函数。
应用程序打开设备完成读写等操作后,如果不需要再对设备进行操作则可以关闭设备,通过如下函数完成:
rt_err_t rt_device_close(rt_device_t dev);
关闭设备接口和打开设备接口需要配对使用,打开一次设备对应需要关闭一次设备,这样设备才会被完全关闭,否则设备仍处于未关闭状态。
控制设备
通过命令控制字,应用程序可以对设备进行控制,通过如下函数完成:
rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void *arg)
- dev:设备句柄
- cmd:命令控制字,这个参数通常与设备驱动程序相关
- 返回:-RT_ENOSYS,表示执行失败,dev为空
参数cmd的通用设备命令可取如下宏定义:
#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 /* 获取中断状态 */
读写设备
rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos,void* buffer, rt_size_t size);
rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos,const void* buffer, rt_size_t size);
数据收发回调
当硬件设备收到数据时,可通过如下函数回调另一个函数来设置数据接收指示,通知上层应用线程有数据到达:
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参数中传递给上层应用。
上层应用线程应在收到指示后,立刻从设备中读取数据。
rt_err_t rt_device_set_tx_complete(rt_device_t dev, rt_err_t (*tx_done)(rt_device_t dev,void *buffer));
调用这个函数时,回调函数由调用者提供,当硬件设备发送完数据时,由驱动程序回调这个函数并把发送完成的数据块地址buffer作为参数传递给上层应用。
上层应用(线程)在收到指示时会根据buffer的情况,释放buffer内存块或将其作为下一个数据的缓存。
访问设备示例
#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_printf("feed the 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_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;
}
I/O设备模型框架补充图
设备基类rt_device的管理接口在device.c中。
开发者只需开发驱动层即可,设备驱动层和I/O设备管理层RTT已经写好了。