imx6ull 字符设备驱动
驱动开发
终于进入第四章了。
开始学习linux中的三大类驱动:
- 字符设备驱动
- 最多
- gpio,i2c,spi,sound
- 块设备驱动
- 更复杂,多数原厂写好直接用
- 即存储器设备驱动,emmc,nand,sd卡,u盘。以存储块为基础
- 网络设备驱动
- 有线/无线
一个设备可以属于多种设备驱动类型
USB WIFI, 使用USB接口,属于字符设备,又能上网,所以属于网络设备驱动。
我们的内核版本是:4.1.15, 支持dts
字符设备驱动开发
这里开始学习linux的字符设备驱动开发框架,用一个虚拟的设备为例,讲解如何进行字符设备驱动开发。如何编写测试app来测试驱动工作是否正常。
字符设备驱动 介绍
定义
- linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备
- 读写数据是
分先后顺序的。 - 比如我们最常见的
点灯、按键,IIC、SPI,LCD等等都是字符设备,这些设备的驱动就叫做字符设备驱动
- 读写数据是
应用 调用 驱动程序 逻辑

可以看到,实际上,用户态的用户程序,依赖系统调用,调用内核态的驱动程序的
在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作
举例:
- 现在有个叫做
/dev/led的驱动文件,此文件是 led 灯的驱动文件。- (打开关闭设备文件)
- 应用程序使用
open函数来打开文件/dev/led,使用完成以后使用close函数关闭/dev/led 这个文件。 open和close就是打开和关闭led驱动的函数,
- 应用程序使用
- (写设备数据)
- 如果要点亮或关闭
led,那么就使用write函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。
- 如果要点亮或关闭
- (读设备状态)
- 如果要获取led 灯的状态,就用
read函数从驱动中读取相应的状态
- 如果要获取led 灯的状态,就用
- (打开关闭设备文件)
应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间, 必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作
open、close、write 和 read等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分
在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动 操作函数 集合


常用的函数有:
- owner
- 指向struct module变量的指针,一般设置为THIS_MODULE
- llseek
- 修改文件当前的读写位置
- read
- 读取设备文件
- write
- 向设备文件写入/发送数据
- poll
- 轮询函数,查询设备是否可以进行非阻塞读写
- unlocked_ioctl
- 提供对设备的控制功能,和应用程序中的ioctl函数对应
- 32位系统上,32位应用程序用这个
- compat_ioctl
- 同unlocked_ioctl, 用于64位系统上,32位应用程序用这个函数。
- mmap
- 将设备的内存映射到进程空间中(用户空间),一般帧缓冲设备用这个函数
- 如LCD驱动的显存,将帧缓存(LCD显存)映射到用户空间中后,应用程序可以直接操作显存。不用在用户空间和内核空间来回复制。
- open
- 打开设备文件
- release
- 释放/关闭设备问及那, 和close对应
- fasync
- 刷新待处理的数据,用于将缓冲区的数据刷新到磁盘。
- aio_fsync
- 与fasync类似,这个是异步刷新待处理数据
我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,但是像
open、release、write、read等都是需要实现的当然了,具体需要实现哪些函数还是要看具体的驱动要求
字符设备驱动开发步骤
我们编写裸机驱动,就是
- 使能时钟(打开设备)
- 根据需要,配置设备寄存器。
在linux驱动开发中,肯定也是要干这些的,只不过,我们需要按照其规定的框架来编写驱动
所以说,学linux驱动开发,重点是学习其驱动框架
驱动模块的 加载/卸载
linux驱动,有两种运行方式:
- 驱动编译进linux内核
- linux内核启动后,自动加载驱动程序
- 驱动编译成模块(这里叫模块,
xxx.ko)- 扩展名为.ko
- 内核启动后,
insmod加载驱动模块
一般,调试的时候,编译成模块,这样仅需编译驱动代码,无需重新编译整个linux代码。
驱动模块(xxx.ko)有加载(insmod)和卸载(rmmod)两种操作, 我们在编写驱动的时候,需要注册这两种操作函数
module_init(xxx_init); //注册 模块加载函数 |
- 执行
insmod xxx.ko- 调用
xxx_init()
- 调用
- 执行
rmmod xxx.ko- 调用
xxx_exit()
- 调用
字符设备驱动模块,加载卸载模板如下
/* 驱动入口函数 */ |
注意,入口函数和出口函数,分别使用了
__init,__exit来修饰。
当驱动编译完以后,得到xxx.ko
.ko加载命令:insmod / modprobe
有两种命令可以加载驱动模块:
insmod- 最简单的模块加载命令,用于加载.ko模块
- 举例:
insmod drv.ko - 缺点:
- 不能解决模块的依赖关系
- 如:
drv.ko依赖first.ko, 只能先insmod first.ko, 然后insmod drv.ko
modprobe- 可以分析模块的依赖关系,然后将所有的依赖模块,加载到内核
- 支持模块的依赖性分析,错误检查,错误报告功能
modprobe默认会去/lib/modules/<内核版本>/下查找相应的驱动模块,自己自制的根文件系统,需要手动创建。
.ko卸载命令:rmmod / modprobe
rmmod- 卸载drv.ko:
rmmod drv.ko
- 卸载drv.ko:
modprobe- 卸载drv.ko:
modprobe -r drv.ko
- 卸载drv.ko:
使用
modprobe命令可以卸载掉驱动模块所依赖的其他模块前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。
所以对于模块的卸载,还是推荐使用
rmmod命令
以上就是内核空间的驱动的加载和卸载,目的是让系统调用能够有东西调用
字符设备的注册与注销
对于字符设备驱动而言
- 当驱动模块加载成功以后需要注册字符设备
- 卸载驱动模块的时候也需要注销掉字符设备
注册与注销字符设备的函数原型:
/* |
一般:
- 字符设备的注册在驱动模块的入口函数
xxx_init中进行- 字符设备的注销在驱动模块的出口函数
xxx_exit中进行
模板如下:
static struct file_operations test_fops; //全局区bss的一块内存 |
test_fops,static,相当于在全局区申请了一块内存,存放操作函数的地址。注册设备的主设备号,设成200,具体需要
cat /proc/devices来查看已经被占用的设备号unregister 注销major=200的设备
画图说明
前面说了,要在驱动程序的入口函数里面,增加注册字符设备,那么此时的调用逻辑是:
- insmod xxx.ko
- kernel调用module_init(xxx_init),注册驱动的入口函数。
- 入口函数里面注册了字符设备,指定了设备号,设备名,操作方法内存地址
- kernel调用module_init(xxx_init),注册驱动的入口函数。
用户空间 (User Space) | 内核空间 (Kernel Space) |
实现设备的具体操作函数
这里完成对操作方法变量的初始化, 设置好针对chrtest设备的操作函数。(我们前面在入口函数里面注册了一个chrdev字符设备,名字叫chrtest)。
在初始化操作函数之前,要分析一下需求,即 要对chrtest这个设备,进行哪些操作
假设有如下需求:
- 对chrtest设备进行 打开/关闭操作
- 打开,关闭,是最基本的要求。所有设备都得提供打开关闭功能
- 需要实现:
file_operations中的:open和release
- 对chrtest设备进行 读写操作
chrtest内部有一段内存,要通过read,write, 来进行读写
- 要实现:
file_operations中的open和release
实现
/* 打开设备 */ |
添加LICENSE和作者信息
最后就是要在驱动中加入LICENSE信息和作者信息。
LICENSE必须添加,否则编译会报错。
添加方法
MODULE_LICENSE()- 添加模块LICENSE信息
MODULE_AUTHOR()- 添加模块作者信息
示例
//... 以上是前面写的驱动代码 |
linux设备号
设备号组成
为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分
组成:
- 设备号
- 主设备号
- 表示某一个具体的驱动
- 次设备号
- 表示使用这个驱动的各个设备
- 主设备号
linux内核提供dev_t类型表示设备号, 定义在include/linux/types.h
typedef __u32 __kernel_dev_t; |
dev_t == uint32,高12位为主设备号,低20位为次设备号
所以,linux系统中,主设备号范围0-4095,我们在入口出口函数里面注册注销字符设备的时候,填写的主设备号,表示驱动程序,只能在这个范围内
include/linux/kdev_t.h中提供了关于设备号的宏操作函数
设备号分配
主要有两种分配方式
- 静态分配设备号
- 前面我们注册字符设备
register_chrdev()时手动指定主设备号200 - 需要
cat /proc/devices查看系统已经占用了哪些主设备号 - 容易出现冲突
- 前面我们注册字符设备
- 动态分配设备号
- 注册字符设备之前,先申请一个设备号,系统返回一个没有使用的,卸载驱动的时候释放设备号即可


chrdevbase 字符设备驱动开发实验
他这个实验,设计一个虚拟的设备,然后用用户态的程序,来根据系统调用的规范,来调用驱动程序,驱动字符设备
驱动程序chrdevbase.c
|
用户态的应用测试程序,chrdevbaseApp.c
|
Makefile,因为驱动在内核编译环境外面,所以单独写了一个Makefile脚本, 后面有空再系统学习Makefile语法
KERNELDIR := /home/liangji/linux/kernel/my_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga |
printk函数分析
这个是linux内核打印日志的函数,需要注意的是,它里面对日志消息级别进行了分类,一共8个消息级别,定义在include/linux/kern_levels.h
0优先级最高,7优先级最低,和中断优先级一样
注意:rtos的任务优先级是反过来的
使用格式
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n"); |
显示等级
驱动方法的参数分析
驱动内的
open方法,有一个参数是flip, 里面有一个叫做private_data的成员变量,一般在驱动中,把private_data指向设备结构体,用来存放一些设备的属性。
测试
加载ko模块
启动linux系统后,需要加载驱动程序:
- insmode
- 因为我们的驱动没有依赖驱动,直接insmod即可
- modprobe
- 我们如果要用modprobe来加载驱动程序,需要把我们的ko模块,放到
/lib/module/<内核版本>。不然使用不了。 - 提示
无法打开modules.dep,没有这个文件,需要执行depmod指令来生成,这个就当是个环境,由busybox提供depmod工具
- 我们如果要用modprobe来加载驱动程序,需要把我们的ko模块,放到
lsmod 可以查看当前系统中已经加载的驱动模块(注意是模块,也就是ko)
我们在驱动里面注册了可以在用户空间照应的字符设备,通过下面就可以看到,驱动程序确实已经把chrdevbase设备注册进系统了
cat /proc/devices //查看当前的系统设备 |

以上,内核空间的准备已经结束了,我们在内核空间内,加载了驱动程序,注册了设备。
创建设备文件
下面开始,要开始准备用户空间的东西
驱动加载成功需要在/dev目录下创建一个与之对应的设备节点文件,应用程序需要通过这个设备节点文件来完成对设备的具体操作
mknod /dev/chrdevbase c 200 0 |
执行测试即可
下面就可以开始用用户空间的测试程序,通过对设备节点文件进行系统调用,从而实现对内核空间驱动程序的调用。
关于,用户程序,系统调用,设备节点,设备,驱动程序,内核他们之间的关系,后面有时间再分析。