基于i.MX6ULL字符设备开发学习
扫描二维码
随时随地手机看文章
1 Linux驱动分类
Linux中的外设驱动可以分为三大类:字符设备驱动、块设备驱动和网络设备驱动。- 字符设备驱动:字符设备是能够按照字节流(比如文件)进行读写操作的设备。字符设备最常见,从最简单的点灯到I2C、SPI、音频等都属于字符设备驱动
- 块设备驱动:以存储块为基础的设备驱动,如EMMC、NAND、SD卡等。对用户而言,字符设备与块设备的访问方式没有差别。
- 网络设备驱动:即网络驱动,它同时具有字符设备和块设备的特点,因为它是输入输出是有结构块的(报文,包,帧),但它的块的大小又不是固定的。
2 Linux驱动基本原理
在Linux中一切皆文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”的文件进行相应的操作即可实现对硬件的操作。比如最简单的点灯功能,会有/dev/led这样的驱动文件,应用程序使用open函数来打开文件/dev/led,如果要点亮或关闭led,那么就使用write函数写入开关值,如果要获取led的状态,就用read函数从驱动中读取相应的状态,使用完成以后使用close函数关闭/dev/led这个文件。2.1 Linux软件分层结构
Linux软件从上到下可以分层4层结构,以控制LED为例:- 应用层:应用程序使用库提供的open函数打开LED设备
- 库:库根据open函数传入的参数执行“swi”指令,进而引起CPU异常,进入内核
- 内核:内核的异常处理函数根据传入的参数找到对应的驱动程序,返回文件句柄给库,进而返回给应用层
- 应用层得到文件句柄后,使用库提供的write或ioctl发出控制指令
- 库根据write或ioctl函数传入的参数执行“swi”指令,进入内核
- 内核的异常处理函数根据传入的参数找到对应的驱动程序
- 驱动:驱动程序控制硬件,点亮LED
2.2 Linux内核驱动操作函数
每一个系统调用,在驱动中都有与之对应的一个驱动函数,在Linux内核文件include/linux/fs.h中有个file_operations结构体,就是Linux内核驱动操作函数集合:struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
/*省略若干行...*/
};
其中有关字符设备驱动开发中常用的函数有:- owner:拥有该结构体的模块的指针,一般设置为THIS_MODULE。
- llseek函数:用于修改文件当前的读写位置。
- read函数:用于读取设备文件。
- write函数:用于向设备文件写入(发送)数据。
- poll函数:是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
- unlocked_ioctl函数:提供对于设备的控制功能, 与应用程序中的 ioctl 函数对应。
- compat_ioctl函数:与 unlocked_ioctl功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
- mmap函数:用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数, 比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
- open函数:用于打开设备文件。
- release函数:用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
- fasync函数:用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
- aio_fsync函数:与fasync功能类似,只是 aio_fsync 是异步刷新待处理的
2.3 Linux驱动运行方式
Linux 驱动有两种运行方式:- 将驱动编译进Linux内核中, 这样当Linux内核启动的时候就会自动运行驱动程序。
- 将驱动编译成模块(扩展名为 .ko), 在Linux内核启动以后使用“insmod”命令加载驱动模块。
2.4 Linux设备号
2.4.1 设备号的组成
Linux中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成。- 主设备号:表示某一个具体的驱动
- 次设备号:表示使用这个驱动的各个设备
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev)