字符设备是非常普遍的一种设备,这种设备在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
{
int (*open)(FAR struct file *filep);
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);
#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
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
2. 文件操作
字符设备文件操作的模型如下图所示。在应用程序中对文件的操作,最终会通过VFS落实到设备驱动file_operation中对应的操作函数。
![这里写图片描述](https://img-blog.csdn.net/20170511220433422?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2hlbmd3ZW55YW5n/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
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中。
2.8 unlink
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 <nuttx/config.h>
#include <errno.h>
#include <debug.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <time.h>
#include <nuttx/wdog.h>
#include <nuttx/kmalloc.h>
#include <nuttx/semaphore.h>
#include <nuttx/drivers/counter.h>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
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 {
sem_t sem;
sem_t lock;
WDOG_ID wdog;
struct pollfd *pollset[MAX_POLLSET];
};
struct counter_data_s counter_data;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
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);
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
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: 更新系统时间
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);
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
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;
}
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 {
for (i = 0; i < MAX_POLLSET; i++) {
if (cdp->pollset[i] != NULL)
cdp->pollset[i] = NULL;
}
}
return OK;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
4.1.12. 注册驱动
添加该函数到操作系统中的某个位置,该函数必须先于应用程序执行,否则应用程序找不到设备节点。
- 将counter驱动注册到系统路径“/dev/counter”
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 <nuttx/config.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sched.h>
#include <errno.h>
#include <pthread.h>
#include <mqueue.h>
#include <signal.h>
#include <time.h>
#include <poll.h>
#include <nuttx/drivers/counter.h>
#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) {
counter_fd = open("/dev/counter", O_RDWR);
if (counter_fd< 0) {
perror("open device");
return;
}
time.tv_sec = 21 * 60 * 60 + 16 * 60 + 28;
time.tv_nsec = 0;
write(counter_fd, &time, sizeof(time));
int i = 5;
while (i--) {
read(counter_fd, buf, sizeof(buf));
printf("%s", buf);
}
time.tv_sec = 13 * 60 * 60 + 31 * 60 + 28;
time.tv_nsec = 0;
ioctl(counter_fd, IO_SET, &time);
ioctl(counter_fd, IO_GET, buf);
printf("ioctl read: %s", buf);
i = 10;
while(i--) {
memset(buf, 0, sizeof(buf));
read(counter_fd, buf, sizeof(buf));
printf("%s", buf);
}
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--) {
int poll_ret = poll(&pfd, 1, -1);
if (poll_ret > 0) {
if (pfd.revents & POLLIN) {
read(counter_fd, buf, sizeof(buf));
printf("%s", buf);
}
}
}
int ret = close(counter_fd);
if (ret < 0) {
perror("close fd");
}
}
return 0;
}