Nuttx 字符设备驱动

字符设备是非常普遍的一种设备,这种设备在I/O传输过程中只能以字符为单位进行传输,如键盘、鼠标、以及一些传感器设备都是字符设备。

Nuttx采用VFS,和linux一样的设计思路,即“一切设备皆文件”,对设备的操作就如同对文件的操作,Nuttx下的设备驱动就是实现这种对文件操作的接口,设备驱动屏蔽了对设备本身的访问的复杂性。通过VFS对设备的抽象,呈现给用户简单的标准接口,如open(), read(), write()等。

1. 数据结构

设备驱动中的重要数据结构file_operations是联系VFS标准文件操作接口和设备驱动对设备具体操作的重要一环,这个数据结构中是一些函数指针,这些函数与VFS标准文件操作的接口一一对应,用户对文件的操作,最终通过file_operation结构体中对应功能的函数实现。

file_operation定义在include/nuttx/fs/fs.h中。

struct file_operations
{
  /* The device driver open method differs from the mountpoint open method */

  int     (*open)(FAR struct file *filep);

  /* The following methods must be identical in signature and position because
   * the struct file_operations and struct mountp_operations are treated like
   * unions.
   */

  int     (*close)(FAR struct file *filep);
  ssize_t (*read)(FAR struct file *filep, FAR char *buffer, size_t buflen);
  ssize_t (*write)(FAR struct file *filep, FAR const char *buffer, size_t buflen);
  off_t   (*seek)(FAR struct file *filep, off_t offset, int whence);
  int     (*ioctl)(FAR struct file *filep, int cmd, unsigned long arg);

  /* The two structures need not be common after this point */

#ifndef CONFIG_DISABLE_POLL
  int     (*poll)(FAR struct file *filep, struct pollfd *fds, bool setup);
#endif
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
  int     (*unlink)(FAR struct inode *inode);
#endif
};

2. 文件操作

字符设备文件操作的模型如下图所示。在应用程序中对文件的操作,最终会通过VFS落实到设备驱动file_operation中对应的操作函数。

Nuttx 字符设备驱动_第1张图片

2.1 open

open打开设备是操作设备必须的第一步操作,在应用中通过调用open(),系统返回一个整形的非零整数,称这个非零整数位文件描述符fd。打开设备之后对文件的一切操作就可以通过fd来完成。设备驱动中对应的open()函数的实现是非必须的,如果要实现一些对设备的初始化等工作,可以在设备驱动中的open()函数中实现。

应用中open()以设备的节点路径和操作权限为参数,操作进入VFS,调用fs_open.c中的open()函数,通过设备路径找到对应的inode节点,在进程的文件描述符链表中寻找并分配空闲可用的描述符fd和文件file,最后调用设备节点inode中的文件操作file_operation中的函数open()。应用程序调用成功时,返回本次分配的文件描述符fd,发生错误时,返回-1,错误码记录在errno中。

2.2 close

与打开设备文件对应的是关闭设备文件。应用程序中调用close(),执行fs_close.c中的close()函数,调用文件操作file_operation中的close()函数,最后释放文件描述符fd和文件。应用程序调用close()调用成功时,返回0,发生错误时,返回-1, 错误码记录在errno中。

2.3 read

应用程序调用read()函数,执行fs_read.c中read()函数,调用文件操作file_operation中的read()函数,从设备中读取数据。file_operation中的read函数含有三个参数,第一个是文件file指针,第二个是传输数据buffer,第三个是期望读取到的字节数。应用程序读取成功时,该函数返回真实读取到的字节数,如果发生错误,返回错误,错误码记录在errno中。

2.4 write

应用程序中调用write()函数,执行fs_write.c中write()函数,调用文件操作file_operation中的write()函数,往设备中写入数据。file_operation中的write含有三个参数,第一个是文件file指针,第二是传输数据buffer,第三个是期望写入的字节数。应用程序写入成功时,该函数返回真实写入的字节数,如果发生错误,返回-1, 错误码记录在errno中。

2.5 seek

应用程序中调用lseek()函数,对应的VFS中的函数为fs_lseek.c中的lseek()函数,lseek()调用文件操作file_operation中的seek(),调整对文件的读写位置。它带有三个参数,第一个参数是文件file指针,第二个参数是设置的文件位置相对偏移,可正可负。第三个参数是设置位置的起始点,可选择文件头、文件当前位置或者文件尾。应用程序调用lseek()成功时,返回设置后的读写位置点,发生错误时,返回-1, 错误码记录在errno中。

2.6 ioctl

应用程序中调用ioctl()函数,执行fs_ioctl.c中的ioctl()函数,调用文件操作file_operation中的ioctl()函数。ioctl()用于执行设备特定的命令,如设置设备的属性,配置设备的寄存器等。file_operation()中的ioctl()带有三个参数,第一个是文件file指针,第二个参数是控制命令,第三个是命令参数,如果参数为指针,可以作为输入输出参数使用。ioctl()调用成功时,返回非负数,发生错误是,返回-1,错误码记录在errno中。

2.7 poll

应用程序中使用poll()查询指定一组文件是否可读或者可写。VFS中执行fs_poll.c中的poll()函数。首先初始化信号量,用于实现定时。其次调用file_operation中的poll()函数查询文件是否可以读写,如果有文件可以读写,返回应用程序。否则,睡眠等待。如果睡眠时间到,再次查询文件是否可以读写,最后返回。

如果睡眠时间设定为0,那么第一次调用file_operation中的poll()后线程直接返回,无需等待。如果睡眠时间为有限值,那么线程等到睡眠睡眠时间到或者文件可读、写,或者信号量被信号中断,则再一次调用file_operation中的poll()查询一次,然后返回结果。如果睡眠时间设定为负数,那么线程将会永久睡眠,直到文件可读、写或者线程被信号中断。

file_operation的poll()函数设计中,如果文件可读、写,1)修改对应的pollfd中的返回事件标志为对应的事件,2)释放信号量。

应用程序调用poll(),第一个参数使pollfd数组指针,第二个为查询的文件数目,第三个为等待时间。如果poll()成功,返回正数,表明可读写的文件个数。返回0表明超时,返回-1表明发生错误,错误码记录在errno中。

unlink用在文件mount,字符设备一般不涉及该操作。

3. 字符设备驱动的注册、注销

从上节图中可知,文件操作是对设备操作的方法,应用程序只有通过设备节点inode才能访问这些操作方法。那么注册设备驱动,就是生成这个inode,连通应用程序中的open()、read()、wirte()等操作和设备文件操作file_operation中的对应操作。

注册字符设备驱动,使用register_driver()函数,其原型为:

int register_driver(FAR const char *path, FAR const struct file_operations *fops,
mode_t mode, FAR void *priv)

第一个参数是设备路径,比如要注册一个led的驱动到”/dev/led”。第二个参数使设备的文件操作指针,指向文件操作实例。第三个参数是预设的设备访问权限。第四个参数使为设备驱动传递的私有参数。设备驱动注册成功,该函数返回0,注册失败,返回一个负的错误码。

注册过设备驱动后,就在设备树中生成一个对应的节点,该节点关联了设备的文件操作file_operation。

卸载设备驱动使用unregister_driver()函数,其原型为:

int unregister_driver(FAR const char *path)

该函数的输入参数为设备路径。卸载成功放回0, 失败返回一个负的错误码。

4. 字符设备驱动例程

本字符设备驱动例程中,采用看门狗模拟一个时钟字符设备,在看门狗的驱动下,周期性地更新时钟。

首先创建字符设备驱动的主体,即文件操作file_operation。 然后实现open()、close()、read()、write()、ioctl()、poll()等函数。

接着注册这个时钟驱动到系统,将会在系统的/dev目录下生成一个名叫“counter”的设备节点。最后,通过一个应用程序来读取、修改时钟的值,验证我们设计的驱动。

4.1 时钟驱动

4.1.1 头文件

在系统的驱动相关的文件夹中创建时钟驱动文件 counter.c,将所需要的头文件包含在里面。

#include 

#include 
#include 

#include 
#include 
#include 

#include 
#include 

#include 
#include 
#include 
#include 

4.1.2. 变量以及宏定义

  • 使用current_time作为驱动中的当前时钟。最终返回应用的是该变量具体的时钟字符串。
  • 中间变量ticks用于换算时间和CPU时钟的tick值
  • counter_data是时钟驱动的私有数据,其中包括用于通知数据更新的信号量sem,lock用于保护数据的原子操作,wdog作为更新时钟的看门狗,周期性的更新当前时钟current_time, pollfd指针数组用于记录正在poll等待的一些pollfd。
typedef FAR struct file file_t;

static struct timespec current_time;

#define MAX_POLLSET 8

static int ticks;

struct counter_data_s {
    /* notify data is updated */
    sem_t sem;
    /* protect data access */
    sem_t lock;
    /* update data periodically */
    WDOG_ID wdog;
    /* store pollfds */
    struct pollfd *pollset[MAX_POLLSET];
};

struct counter_data_s counter_data;

4.1.3. 全局静态函数声明

static int counter_open(file_t *filep);
static int counter_close(file_t *filep);
static ssize_t counter_read(file_t *filep, FAR char *buf, size_t buflen);
static ssize_t counter_write(file_t *filep, FAR const char *buf, size_t buflen);
static int counter_ioctl(file_t *filep, int cmd, unsigned long arg);
static int counter_poll(struct file *filep, struct pollfd *fds, bool setup);

4.1.4. 时钟驱动的文件操作file_operation

static const struct file_operations counter_ops = {
    .open = counter_open,
    .close = counter_close,
    .read = counter_read,
    .write = counter_write,
    .ioctl = counter_ioctl,
#ifndef CONFIG_DISABLE_POLL
    .poll = counter_poll,
#endif

};

4.1.5. 看门狗服务函数

  • 看门狗服务函数周期性地更新系统当前时间到驱动时钟current_time中,使用信号量锁保证更新操作的原子的。
  • 查询是否有线程在等待驱动时钟更新这一事件,如果有,则唤醒线程。
  • 发出当前时钟被更新的通知。read函数在阻塞等待该通知。
  • 重新调度看门狗,在经过ticks时间后,看门狗函数将被再一次执行。
void counter_wdog_service(int argc, uint32_t arg1, ...)
{
    struct file *filep = (struct file*)arg1;
    struct counter_data_s *cdp = (struct counter_data_s *)filep->f_priv;
    int i;

    sem_wait(&cdp->lock);
    clock_gettime(CLOCK_REALTIME, ¤t_time);
    sem_post(&cdp->lock);

    for (i = 0; i < MAX_POLLSET; i++) {
        if (cdp->pollset[i] != NULL) {
            cdp->pollset[i]->revents |= cdp->pollset[i]->events & POLLIN;
            sem_post(cdp->pollset[i]->sem);
        }
    }

    sem_post(&cdp->sem);    
    wd_start(cdp->wdog, ticks, counter_wdog_service, 1, filep);
}

4.1.6. 打开文件

  • 初始化驱动中的信号量
  • 初始化系统时间
  • 为驱动分配看门狗,调度看门狗
  • 初始化pollfd指针数组
  • 将counter_data作为文件的私有数据,方便对文件操作时获取counter_data数据。
static int counter_open(file_t *filep)
{
    struct timespec tt;

    memset(¤t_time, 0, sizeof(struct timespec));

    sem_init(&counter_data.sem, 0, 0);
    sem_init(&counter_data.lock, 0, 1);

    tt.tv_sec = 0;
    tt.tv_nsec = 0;
    (void)clock_settime(CLOCK_REALTIME, &tt);

    tt.tv_sec = 1;
    tt.tv_nsec = 0;    
    (void)clock_time2ticks(&tt, &ticks);

    counter_data.wdog= wd_create();
    if (!counter_data.wdog) {
        perror("alloc wdog");
        return ERROR;
    }

    int i;
    for (i = 0; i < MAX_POLLSET; i++) {
        counter_data.pollset[i] = NULL;
    }

    filep->f_priv = &counter_data;

    wd_start(counter_data.wdog, ticks, counter_wdog_service, 1, filep);

    return OK;
}

4.1.7. 关闭文件

  • 释放看门狗资源

static int counter_close(file_t *filep)
{
    struct counter_data_s *cdp = (struct counter_data_s *)filep->f_priv;

    (void)wd_delete(cdp->wdog);

    return OK;
}

4.1.8. 文件读操作

  • 数据安全检查
  • 等待时钟更新事件
  • 原子地复制时钟数据,并格式化为字符串,复制字符串到读取数据buffer中
static ssize_t counter_read(file_t *filep, FAR char *buf, size_t buflen)
{
    char time_buf[32];
    struct counter_data_s *cdp = (struct counter_data_s *)filep->f_priv;

    if (buf == NULL || buflen < 1) {
        return -EINVAL;
    }

    memset(time_buf, 0, sizeof(time_buf));

    sem_wait(&cdp->sem);    
    sem_wait(&cdp->lock);
    /* hh:mm:ss */
    sprintf(time_buf, "current time  %02d:%02d:%02d\r\n",
        (current_time.tv_sec/60/60)%24, (current_time.tv_sec/60)%60, current_time.tv_sec%60);
    sem_post(&cdp->lock);

    int len = strlen(time_buf);
    memcpy(buf, time_buf, len);

    return len;
}

4.1.9. 文件写操作

  • 原子地更新系统时间
static ssize_t counter_write(file_t *filep, FAR const char *buf, size_t buflen)
{
    struct timespec *tp = (struct timespec*)buf;
    struct counter_data_s *cdp = (struct counter_data_s *)filep->f_priv;

    sem_wait(&cdp->lock);
    (void)clock_settime(CLOCK_REALTIME, tp);
    (void)clock_gettime(CLOCK_REALTIME, ¤t_time);
    sem_post(&cdp->lock);

    return sizeof(struct timespec);
}

4.1.10. 文件ioctl

  • IO_GET: 读取时钟数据到数据buffer中
  • IO_SET: 更新系统时间
/*
*   counter ioctl
*   IO_GET: read clock data to @arg
*   IO_SET: set clock from @arg 
*/
static int counter_ioctl(file_t *filep, int cmd, unsigned long arg)
{
    int ret = OK;
    struct counter_data_s *cdp = (struct counter_data_s *)filep->f_priv;

    switch(cmd) {
        case IO_GET: {
            char * const buf = (char *)arg;
            sem_wait(&cdp->lock);
            /* hh:mm:ss */
            sprintf(buf, "current time  %02d:%02d:%02d\r\n",
                (current_time.tv_sec/60/60)%24, (current_time.tv_sec/60)%60, current_time.tv_sec%60);
            sem_post(&cdp->lock);
            break;
        }

        case IO_SET: {
            struct timespec *t = (struct timespec *)arg;
            sem_wait(&cdp->lock);
            clock_settime(CLOCK_REALTIME, t);
            clock_gettime(CLOCK_REALTIME, ¤t_time);
            sem_post(&cdp->lock);
            break;            
        }
        default: {
            ret = ERROR;
            break;
        }
    }

    return ret;
}

4.1.11. 文件poll操作

  • 根据poll的原理,第一次进入poll时,setup的值为true。从current_data数据中的pollset里面找到一个可用的位置,将当前的pollfd存储起来。然后通过poll_state()函数检查当前文件是否可读、写,如果可读、写,那么释放信号量,poll操作完成。如果不可读、写,那么,本次从counter_poll()函数退出之后,睡眠等待信号量,直到等待超时,或者得到信号量,或者被信号中断。接着第二次进入counter_poll()函数。

  • 第二次进入poll()函数时,setup的值为false,那么从current_data中移除对应的pollfd。

static pollevent_t poll_state(struct file *filep)
{
    return 0;
}

/*
*   counter poll
*   if @setup is set, store pollfd for post semaphore
*   else, remove pollfd
*/
static int counter_poll(struct file *filep, struct pollfd *fds, bool setup)
{
    struct counter_data_s *cdp = (struct counter_data_s *)filep->f_priv;   
    int i;

    if (setup)
    {
        for (i = 0; i < MAX_POLLSET; i++) {
            if (cdp->pollset[i] == NULL) {
                cdp->pollset[i] = fds;
                break;
            }
        }

        fds->revents |= (fds->events & poll_state(filep));
        if (fds->revents != 0)
        {
            sem_post(fds->sem);
        }
    } else {
        // tear down 
        for (i = 0; i < MAX_POLLSET; i++) {
            if (cdp->pollset[i] != NULL)
                cdp->pollset[i] = NULL;
        }
    }

  return OK;
}

4.1.12. 注册驱动

添加该函数到操作系统中的某个位置,该函数必须先于应用程序执行,否则应用程序找不到设备节点。

  • 将counter驱动注册到系统路径“/dev/counter”
/*
*   register counter driver
*/
void up_counter(void)
{
    int ret = register_driver("/dev/counter", &counter_ops, 0666, NULL);   
    if (ret < 0) {
        perror("register counter driver");
    }
}

4.1.13. 注销驱动

void down_counter(void)
{
    unregister_driver("/dev/counter");
}

4.2 应用测试程序

在应用程序中,通过读写“/dev/counter”设备,测试驱动设计是否正确。

  • 打开设备
  • 通过write()设置系统时间,使用read()读取时钟,验证系统时间是否被修改以及read()、write()函数的功能是否正确。如果时钟没有更新,read()函数将被阻塞,直到时钟更新事件发生。
  • 通过ioctl()函数设置系统时间,并使用ioctl()函数读取时钟。验证ioctl()函数功能。
  • 先使用poll()函数测试时钟是否被更新,如果没有更新,poll()将被无限期地阻塞。poll()函数返回之后,判断是否有事件发生,如果有,检验事件类型是否是可读事件,然后通过read()函数读取时钟。poll()之后调用read()读取时钟,虽然read()函数是阻塞访问,但是poll()返回之后我们知道,驱动已经更新了时钟,那么read()函数将不会被阻塞。
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#ifdef CONFIG_BUILD_KERNEL
int main(int argc, FAR char *argv[])
#else
int counter_main(int argc, FAR char *argv[])
#endif
{
    int counter_fd;
    char buf[32];
    struct timespec time;

    while(1) {
        /* open device */
        counter_fd = open("/dev/counter", O_RDWR);

        if (counter_fd< 0) {
            perror("open device");
            return;
        }

        /* modify current time, 21:16:28 */
        time.tv_sec = 21 * 60 * 60 + 16 * 60 + 28;
        time.tv_nsec = 0;
        write(counter_fd, &time, sizeof(time));

        /* read current time, which will be blocked if data is not availiable */
        int i = 5;
        while (i--) {
            read(counter_fd, buf, sizeof(buf));
            printf("%s", buf);
        }

        /* modify current time by ioctl, 13:31:28 */
        time.tv_sec = 13 * 60 * 60 + 31 * 60 + 28;
        time.tv_nsec = 0;
        ioctl(counter_fd, IO_SET, &time);

        /* read current time by ioctl */
        ioctl(counter_fd, IO_GET, buf);
        printf("ioctl read: %s", buf);

        /* read current time, which will be block if data is not availiable */
        i = 10;
        while(i--) {
            memset(buf, 0, sizeof(buf));
            read(counter_fd, buf, sizeof(buf));
            printf("%s", buf);
        }

        /* poll data at first, then read data if poll success */
        struct pollfd pfd;
        sem_t psem;
        sem_init(&psem, 0, 0);
        pfd.fd = counter_fd;
        pfd.events = POLLIN;
        pfd.sem = &psem;
        pfd.revents = 0;
        pfd.priv = NULL;

        i = 10;
        while(i--) {
            /* wait infinit if data is not availiable */
            int poll_ret = poll(&pfd, 1, -1);
            if (poll_ret > 0) {
                /* data is availiable for read */
                if (pfd.revents & POLLIN) {
                    read(counter_fd, buf, sizeof(buf));
                    printf("%s", buf);
                }
            }
        }

        /* close device */
        int ret = close(counter_fd);
        if (ret < 0) {
            perror("close fd");
        }
    }

    return 0;
}

你可能感兴趣的:(Nuttx,字符设备驱动,nuttx,字符设备驱动)