字符设备驱动模型
本章将介绍 MS-RTOS 字符设备驱动模型。
为了避免对同一类的设备编写多个相似的驱动,驱动在实现上应该要考虑能够同时应用于多个同类设备上。对于某一系列的驱动,在实现时应尽量使用现有 HAL 库代码,以提高可移植性。
1. 字符类驱动模板
BSP 需要调用 xxx_drv_register
函数完成 xxx
驱动的注册,然后调用 xxx_dev_create
函数完成设备的创建或调用 xxx_dev_register
函数完成设备的注册。注意,如果设备对象使用的是静态内存,则使用形如 xxx_register
的命名形式;如果设备对象是动态分配的,则使用形如 xxx_create
的命名形式。
驱动源码文件如下:
/*
* Copyright (c) 2015-2020 ACOINFO, Inc.
*/
#define __MS_IO
#include "ms_kern.h"
#include "ms_io_core.h"
#include "ms_drv_xxx.h"
#include <string.h>
/**
* @brief Driver template.
*/
#if MS_CFG_IO_MODULE_EN > 0
#define MS_XXX_DRV_NAME "xxx"
/*
* Private info
*/
typedef struct {
ms_pollfd_t *slots[MS_IO_DEF_POLLFD_SLOT_SIZE];
/*
* Other resource handle
* TODO
*/
ms_addr_t base;
} privinfo_t;
/*
* xxx device
*/
typedef struct {
privinfo_t priv;
ms_io_device_t dev;
} xxx_dev_t;
/*
* Open device
*/
static int __xxx_open(ms_ptr_t ctx, ms_io_file_t *file, int oflag, ms_mode_t mode)
{
privinfo_t *priv = ctx;
int ret;
if (ms_atomic_inc(MS_IO_DEV_REF(file)) == 1) {
/*
* Initialize device
* TODO
*/
ret = 0;
} else {
ms_atomic_dec(MS_IO_DEV_REF(file));
ms_thread_set_errno(EBUSY);
ret = -1;
}
return ret;
}
/*
* Close device
*/
static int __xxx_close(ms_ptr_t ctx, ms_io_file_t *file)
{
privinfo_t *priv = ctx;
int ret;
if (ms_atomic_dec(MS_IO_DEV_REF(file)) == 0) {
/*
* Close device
* TODO
*/
}
ret = 0;
return ret;
}
/*
* Read device
*/
static ms_ssize_t __xxx_read(ms_ptr_t ctx, ms_io_file_t *file, ms_ptr_t buf, ms_size_t len)
{
privinfo_t *priv = ctx;
ms_ssize_t ret;
/*
* Read content to buffer from device
* TODO
*/
ret = 0;
return ret;
}
/*
* Write device
*/
static ms_ssize_t __xxx_write(ms_ptr_t ctx, ms_io_file_t *file, ms_const_ptr_t buf, ms_size_t len)
{
privinfo_t *priv = ctx;
ms_ssize_t ret;
/*
* Write content to device from buffer
* TODO
*/
ret = 0;
return ret;
}
/*
* Control device
*/
static int __xxx_ioctl(ms_ptr_t ctx, ms_io_file_t *file, int cmd, ms_ptr_t arg)
{
privinfo_t *priv = ctx;
int ret;
switch (cmd) {
case MS_XXX_CMD_SET_XXX:
if (ms_access_ok(arg, sizeof(ms_uint32_t), MS_ACCESS_R)) {
/*
* TODO
*/
ret = 0;
} else {
ms_thread_set_errno(EFAULT);
ret = -1;
}
break;
case MS_XXX_CMD_GET_XXX:
if (ms_access_ok(arg, sizeof(ms_uint32_t), MS_ACCESS_W)) {
/*
* TODO
*/
ret = 0;
} else {
ms_thread_set_errno(EFAULT);
ret = -1;
}
break;
default:
ms_thread_set_errno(EINVAL);
ret = -1;
break;
}
return ret;
}
/*
* Get device status
*/
static int __xxx_fstat(ms_ptr_t ctx, ms_io_file_t *file, ms_stat_t *buf)
{
privinfo_t *priv = ctx;
int ret;
/*
* Get device status
* TODO
*/
ret = 0;
return ret;
}
/*
* Is device a tty?
*/
static int __xxx_isatty(ms_ptr_t ctx, ms_io_file_t *file)
{
return 0;
}
/*
* Check device readable
*/
static ms_bool_t __xxx_readable_check(ms_ptr_t ctx)
{
privinfo_t *priv = ctx;
/*
* TODO
*/
return MS_FALSE;
}
/*
* Check device writable
*/
static ms_bool_t __xxx_writable_check(ms_ptr_t ctx)
{
privinfo_t *priv = ctx;
/*
* TODO
*/
return MS_FALSE;
}
/*
* Check device exception
*/
static ms_bool_t __xxx_except_check(ms_ptr_t ctx)
{
privinfo_t *priv = ctx;
/*
* TODO
*/
return MS_FALSE;
}
/*
* Device notify
*/
static int __xxx_poll_notify(privinfo_t *priv, ms_pollevent_t event)
{
return ms_io_poll_notify_helper(priv->slots, MS_ARRAY_SIZE(priv->slots), event);
}
/*
* Poll device
*/
static int __xxx_poll(ms_ptr_t ctx, ms_io_file_t *file, ms_pollfd_t *fds, ms_bool_t setup)
{
privinfo_t *priv = ctx;
return ms_io_poll_helper(fds, priv->slots, MS_ARRAY_SIZE(priv->slots), setup, ctx,
__xxx_readable_check, __xxx_writable_check, __xxx_except_check);
}
/*
* Device operating function set
*/
static ms_io_driver_ops_t xxx_drv_ops = {
.type = MS_IO_DRV_TYPE_CHR,
.open = __xxx_open,
.close = __xxx_close,
.write = __xxx_write,
.read = __xxx_read,
.ioctl = __xxx_ioctl,
.fstat = __xxx_fstat,
.isatty = __xxx_isatty,
.poll = __xxx_poll,
};
/*
* Device driver
*/
static ms_io_driver_t xxx_drv = {
.nnode = {
.name = MS_XXX_DRV_NAME,
},
.ops = &xxx_drv_ops,
};
/*
* Register xxx device driver
*/
ms_err_t xxx_drv_register(void)
{
return ms_io_driver_register(&xxx_drv);
}
/*
* Create xxx device file
*/
ms_err_t xxx_dev_create(const char *path, ms_addr_t base)
{
xxx_dev_t *dev;
ms_err_t err;
if (path != MS_NULL) {
dev = ms_kmalloc(sizeof(xxx_dev_t));
if (dev != MS_NULL) {
/*
* Make sure clear priv.slots
*/
bzero(&dev->priv, sizeof(dev->priv));
dev->priv.base = base;
err = ms_io_device_register(&dev->dev, path, MS_XXX_DRV_NAME, &dev->priv);
if (err != MS_ERR_NONE) {
(void)ms_kfree(dev);
}
} else {
err = MS_ERR_KERN_HEAP_NO_MEM;
}
} else {
err = MS_ERR_ARG_NULL_PTR;
}
return err;
}
/*
* Register xxx device file
*/
ms_err_t xxx_dev_register(const char *path, ms_addr_t base)
{
ms_err_t err;
static privinfo_t priv;
static ms_io_device_t dev;
/*
* Make sure clear priv.slots
*/
if (path != MS_NULL) {
priv.base = base;
err = ms_io_device_register(&dev, path, MS_XXX_DRV_NAME, &priv);
} else {
err = MS_ERR_ARG_NULL_PTR;
}
return err;
}
#endif
驱动头文件如下:
#ifndef MS_DRV_XXX_H
#define MS_DRV_XXX_H
#ifdef __cplusplus
extern "C" {
#endif
#define MS_XXX_CMD_XXX _MS_IO('x', 'a')
#define MS_XXX_CMD_SET_XXX _MS_IOW('x', 'b', ms_uint32_t)
#define MS_XXX_CMD_GET_XXX _MS_IOR('x', 'b', ms_uint32_t)
ms_err_t xxx_drv_register(void);
ms_err_t xxx_dev_create(const char *path, ms_addr_t base);
ms_err_t xxx_dev_register(const char *path, ms_addr_t base);
#ifdef __cplusplus
}
#endif
#endif /* MS_DRV_XXX_H */
2. 字符类驱动关键点
(1)引用计数
一个设备有可能支持同时被多次打开,驱动需要在 __xxx_open
和 __xxx_close
函数中做相应支持:
__xxx_open
函数中的代码片断:
static int __xxx_open(ms_ptr_t ctx, ms_io_file_t *file, int oflag, ms_mode_t mode)
{
privinfo_t *priv = ctx;
int ret;
if (ms_atomic_inc(MS_IO_DEV_REF(file)) == 1) {
/*
* Initialize device
* TODO
*/
ret = 0;
} else {
ms_atomic_dec(MS_IO_DEV_REF(file));
ms_thread_set_errno(EBUSY);
ret = -1;
}
return ret;
}
调用 ms_atomic_inc(MS_IO_DEV_REF(file))
函数并判断返回值是否为 1,如果是 1,则说明是第一次打开,便做设备初始化;如果不是 1,则说明不是第一次打开;如果设备不支持同时被多次打开,则需要调用 ms_atomic_dec(MS_IO_DEV_REF(file));
,并将 errno
设置为 EBUSY
;如果设备支持同时被多次打开,则应该返回 0。
__xxx_close
函数中的代码片断:
static int __xxx_close(ms_ptr_t ctx, ms_io_file_t *file)
{
privinfo_t *priv = ctx;
int ret;
if (ms_atomic_dec(MS_IO_DEV_REF(file)) == 0) {
/*
* Close device
* TODO
*/
}
ret = 0;
return ret;
}
调用 ms_atomic_dec(MS_IO_DEV_REF(file))
函数并判断返回值是否为 0,如果是 0,则说明是最后一次关闭,便做设备清理工作,如果不是,则不需要做设备清理工作。
(2)ioctl 命令定义
设备支持的 ioctl
命令应该在驱动的头文件中定义,如下所示:
#define MS_XXX_CMD_XXX _MS_IO('x', 'a')
#define MS_XXX_CMD_SET_XXX _MS_IOW('x', 'b', ms_uint32_t)
#define MS_XXX_CMD_GET_XXX _MS_IOR('x', 'b', ms_uint32_t)
_MS_IO
、 _MS_IOW
、_MS_IOR
、_MS_IORW
宏定义如下所示,其中 x 为设备名称的示意字符,y 为命令的示意字符,t 为传递的数据类型,此方法可有效避免命令冲突(重定义):
#define _MS_IO(x, y) (MS_IOC_VOID | ((x) << 8U) | (y))
#define _MS_IOR(x, y, t) (MS_IOC_OUT | (((ms_ulong_t)sizeof(t) & MS_IOCPARM_MASK) \
<< 16U) | ((x) << 8U) | (y))
#define _MS_IOW(x, y, t) (MS_IOC_IN | (((ms_ulong_t)sizeof(t) & MS_IOCPARM_MASK) \
<< 16U) | ((x) << 8U) | (y))
#define _MS_IORW(x, y, t) (MS_IOC_INOUT | (((ms_ulong_t)sizeof(t) & MS_IOCPARM_MASK) \
<< 16U) | ((x) << 8U) | (y))
(3)ms_access_ok 使用
为了提升 ioctl
的安全性,如果 ioctl
命令带参数的传递,在使用指针参数或地址参数前,应该使用 ms_access_ok
函数判断指针参数或地址参数的合法性,如下所示:
case MS_XXX_CMD_SET_XXX:
if (ms_access_ok(arg, sizeof(ms_uint32_t), MS_ACCESS_R)) {
/*
* TODO
*/
ret = 0;
} else {
ms_thread_set_errno(EFAULT);
ret = -1;
}
break;
case MS_XXX_CMD_GET_XXX:
if (ms_access_ok(arg, sizeof(ms_uint32_t), MS_ACCESS_W)) {
/*
* TODO
*/
ret = 0;
} else {
ms_thread_set_errno(EFAULT);
ret = -1;
}
break;
MS_ACCESS_R
用于检查 arg
指针指向的地址区间是否可读;MS_ACCESS_W
用于检查 arg
指针指向的地址区间是否可写。注意,ioctl
传递到驱动层的 arg
指针不用判断是否为 MS_NULL
,IO 层已经检查过了。
(4)errno 的处理
IO 系统在调用驱动的函数前,已经将 errno
清零,如果设备操作出错或参数错误,应该将 errno
设置为 posix
错误码,如上面的 ms_thread_set_errno(EFAULT);
。
(5)poll 支持
为了能让上层对设备进行 select
和 poll
操作,驱动需要做相应的支持,首先需要实现以下三个检查函数:
/*
* Check device readable
*/
static ms_bool_t __xxx_readable_check(ms_ptr_t ctx)
{
privinfo_t *priv = ctx;
/*
* TODO
*/
return MS_FALSE;
}
/*
* Check device writable
*/
static ms_bool_t __xxx_writable_check(ms_ptr_t ctx)
{
privinfo_t *priv = ctx;
/*
* TODO
*/
return MS_FALSE;
}
/*
* Check device exception
*/
static ms_bool_t __xxx_except_check(ms_ptr_t ctx)
{
privinfo_t *priv = ctx;
/*
* TODO
*/
return MS_FALSE;
}
__xxx_poll
函数调用 ms_io_poll_helper
辅助函数,__xxx_poll
函数基本不需要修改,套用以下范式则可:
/*
* Poll device
*/
static int __xxx_poll(ms_ptr_t ctx, ms_io_file_t *file,
ms_pollfd_t *fds, ms_bool_t setup)
{
privinfo_t *priv = ctx;
return ms_io_poll_helper(fds, priv->slots, MS_ARRAY_SIZE(priv->slots), setup, ctx,
__xxx_readable_check,
__xxx_writable_check,
__xxx_except_check);
}
在设备可读或可写或异常时,调用 __xxx_poll_notify
函数,并传入正确的 ms_pollevent_t
,__xxx_poll_notify
函数会调用 ms_io_poll_notify_helper
辅助函数唤醒因 poll
或 select
设备而休眠的线程:
/*
* Device notify
*/
static int __xxx_poll_notify(privinfo_t *priv, ms_pollevent_t event)
{
return ms_io_poll_notify_helper(priv->slots, MS_ARRAY_SIZE(priv->slots), event);
}
(6)NONBLOCK 支持
当设备没有数据可读或没有空间可写时,如果上层以阻塞模式打开设备,则需要阻塞,如果上层以非阻塞模式打开设备,则不应该阻塞,所以 __xxx_read
和 __xxx_write
函数在检测出没有数据可读或没有空间可写时,应该要判断文件的标志 file->flags
是否置上 FNONBLOCK
标志,如果置上,则不应该阻塞,如果没有置上,则需要阻塞,示意代码如下所示:
if (file->flags & FNONBLOCK) {
break;
} else {
if (ms_semb_wait(priv->r_sembid, priv->r_timeout) != MS_ERR_NONE) {
break;
}
}
file->flags
可通过 ms_io_fcntl
修改。
(7)互斥与信号
在应用层可以使用信号量 PV 操作和互斥锁来实现线程间对临界资源访问的同步与互斥。同样在内核态的驱动程序中,多个执行单元并发执行时也会造成对共享资源的竞争,形成竞态。在内核中,主要的竞态发生于如下几种情况:
对称多处理器 (SMP) 的多个 CPU
SMP 是一种紧密耦合、共享存储的系统模型,它的特点是多个 CPU 使用共同的系统总线,因此可访问共同的外设和存储器。
单 CPU 内进程与抢占它的进程
一个进程在内核执行的时候可能被另一高优先级进程打断。
中断(硬中断、软中断、Tasklet、底半部)与进程之间
中断可以打断正在执行的进程,如果中断处理程序访问进程正在访问的资源,则竞态就会发生。此外,中断也有可能被更高优先级的中断打断,因此,多个中断之间本身也可能引起并发而导致竞态。
MS-RTOS 驱动开发中常用于解决竞态问题的基础设施包括:
- 中断屏蔽
- 原子量
- 互斥锁
- 二值信号量
- 计数信号量
如果某资源同时只准一个任务访问,可以用互斥量保护这个资源。这个资源一定是存在的,所以创建互斥量时会先释放一个互斥量,表示这个资源可以使用。任务想访问资源时,先获取互斥量,等使用完资源后,再释放它。也就是说互斥量一旦创建好后,要先获取,后释放,要在同一个任务中获取和释放。这也是互斥量和二进制信号量的一个重要区别,二进制信号量可以在随便一个任务中获取或释放,然后也可以在任意一个任务中释放或获取。互斥量不同于二进制信号量的还有:互斥量具有优先级继承机制,二进制信号量没有,互斥量不可以用于中断服务程序,二进制信号量可以。
附录(Appendix)
1. FAQ
(1)如何知道当前 MS-RTOS 系统中有哪些设备?应用程序如何操作这些设备?
MS-RTOS 中的所有设备文件都会存放在 /dev
目录下,用户可以在 shell 命令行中输入 ls /dev
命令(或者使用 devs
命令)来查看当前系统所支持的所有设备。应用程序如果需要操作某一设备,可以通过操作设备对应的设备文件来实现,以 GPIO 为例:应用程序需要包含 <driver/ms_drv_gpio.h>
头文件,然后通过 open
、ioctl
、read
、write
等标准 IO 操作来控制 GPIO 端口。在这一点上,MS-RTOS 借鉴了 Unix 中一切皆文件的思想。