字符设备是非常普遍的一种设备,这种设备在I/O传输过程中只能以字符为单位进行传输,如键盘、鼠标、以及一些传感器设备都是字符设备。
Nuttx采用VFS,和linux一样的设计思路,即“一切设备皆文件”,对设备的操作就如同对文件的操作,Nuttx下的设备驱动就是实现这种对文件操作的接口,设备驱动屏蔽了对设备本身的访问的复杂性。通过VFS对设备的抽象,呈现给用户简单的标准接口,如open(), read(), write()等。
设备驱动中的重要数据结构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
};
字符设备文件操作的模型如下图所示。在应用程序中对文件的操作,最终会通过VFS落实到设备驱动file_operation中对应的操作函数。
open打开设备是操作设备必须的第一步操作,在应用中通过调用open(),系统返回一个整形的非零整数,称这个非零整数位文件描述符fd。打开设备之后对文件的一切操作就可以通过fd来完成。设备驱动中对应的open()函数的实现是非必须的,如果要实现一些对设备的初始化等工作,可以在设备驱动中的open()函数中实现。
应用中open()以设备的节点路径和操作权限为参数,操作进入VFS,调用fs_open.c中的open()函数,通过设备路径找到对应的inode节点,在进程的文件描述符链表中寻找并分配空闲可用的描述符fd和文件file,最后调用设备节点inode中的文件操作file_operation中的函数open()。应用程序调用成功时,返回本次分配的文件描述符fd,发生错误时,返回-1,错误码记录在errno中。
与打开设备文件对应的是关闭设备文件。应用程序中调用close(),执行fs_close.c中的close()函数,调用文件操作file_operation中的close()函数,最后释放文件描述符fd和文件。应用程序调用close()调用成功时,返回0,发生错误时,返回-1, 错误码记录在errno中。
应用程序调用read()函数,执行fs_read.c中read()函数,调用文件操作file_operation中的read()函数,从设备中读取数据。file_operation中的read函数含有三个参数,第一个是文件file指针,第二个是传输数据buffer,第三个是期望读取到的字节数。应用程序读取成功时,该函数返回真实读取到的字节数,如果发生错误,返回错误,错误码记录在errno中。
应用程序中调用write()函数,执行fs_write.c中write()函数,调用文件操作file_operation中的write()函数,往设备中写入数据。file_operation中的write含有三个参数,第一个是文件file指针,第二是传输数据buffer,第三个是期望写入的字节数。应用程序写入成功时,该函数返回真实写入的字节数,如果发生错误,返回-1, 错误码记录在errno中。
应用程序中调用lseek()函数,对应的VFS中的函数为fs_lseek.c中的lseek()函数,lseek()调用文件操作file_operation中的seek(),调整对文件的读写位置。它带有三个参数,第一个参数是文件file指针,第二个参数是设置的文件位置相对偏移,可正可负。第三个参数是设置位置的起始点,可选择文件头、文件当前位置或者文件尾。应用程序调用lseek()成功时,返回设置后的读写位置点,发生错误时,返回-1, 错误码记录在errno中。
应用程序中调用ioctl()函数,执行fs_ioctl.c中的ioctl()函数,调用文件操作file_operation中的ioctl()函数。ioctl()用于执行设备特定的命令,如设置设备的属性,配置设备的寄存器等。file_operation()中的ioctl()带有三个参数,第一个是文件file指针,第二个参数是控制命令,第三个是命令参数,如果参数为指针,可以作为输入输出参数使用。ioctl()调用成功时,返回非负数,发生错误是,返回-1,错误码记录在errno中。
应用程序中使用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,字符设备一般不涉及该操作。
从上节图中可知,文件操作是对设备操作的方法,应用程序只有通过设备节点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, 失败返回一个负的错误码。
本字符设备驱动例程中,采用看门狗模拟一个时钟字符设备,在看门狗的驱动下,周期性地更新时钟。
首先创建字符设备驱动的主体,即文件操作file_operation。 然后实现open()、close()、read()、write()、ioctl()、poll()等函数。
接着注册这个时钟驱动到系统,将会在系统的/dev目录下生成一个名叫“counter”的设备节点。最后,通过一个应用程序来读取、修改时钟的值,验证我们设计的驱动。
在系统的驱动相关的文件夹中创建时钟驱动文件 counter.c
,将所需要的头文件包含在里面。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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;
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);
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
};
current_time
中,使用信号量锁保证更新操作的原子的。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);
}
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;
}
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;
}
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;
}
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);
}
/*
* 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;
}
根据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;
}
添加该函数到操作系统中的某个位置,该函数必须先于应用程序执行,否则应用程序找不到设备节点。
/*
* register counter driver
*/
void up_counter(void)
{
int ret = register_driver("/dev/counter", &counter_ops, 0666, NULL);
if (ret < 0) {
perror("register counter driver");
}
}
void down_counter(void)
{
unregister_driver("/dev/counter");
}
在应用程序中,通过读写“/dev/counter”设备,测试驱动设计是否正确。
#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;
}