字符设备是非常普遍的一种设备,这种设备在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中对应的操作函数。
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