# MS-RTOS 字符设备驱动模型

本章将介绍 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 支持

为了能让上层对设备进行 selectpoll 操作,驱动需要做相应的支持,首先需要实现以下三个检查函数:

/*
 * 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 辅助函数唤醒因 pollselect 设备而休眠的线程:

/*
 * 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> 头文件,然后通过 openioctlreadwrite 等标准 IO 操作来控制 GPIO 端口。在这一点上,MS-RTOS 借鉴了 Unix 中 “一切皆文件” 的思想。