Linux驱动开发(1)概念、环境与代码框架
一、驱动概念
驱动与底层硬件直接打交道,充当了硬件与应用软件中间的桥梁。
1、具体任务
(1)读写设备寄存器(实现控制的方式)
(2)完成设备的轮询、中断处理、DMA通信(CPU与外设通信的方式)
(3)进行物理内存向虚拟内存的映射(在开启硬件MMU的情况下)
2、驱动的定位
Linux系统主要部分:内核、shell、文件系统、应用程序。
内核、shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序、管理文件并使用系统。分层设计的思想让程序间松耦合,有助于适配各种平台。驱动的上面是系统调用,下面是硬件。
3、驱动的分类
Linux驱动分为三个基础大类:字符设备驱动,块设备驱动,网络设备驱动。
(1)字符设备
字符(char)设备是个能够像字节流(类似文件)一样被访问的设备。
对字符设备发出读/写请求时,实际的硬件I/O操作一般紧接着发生。
字符设备驱动程序通常至少要实现open、close、read和write系统调用。
比如我们常见的lcd、触摸屏、键盘、led、串口等等,他们一般对应具体的硬件都是进行出具的采集、处理、传输。
(2)块设备
一个块设备驱动程序主要通过传输固定大小的数据(一般为512或1k)来访问设备。
块设备通过buffer cache(内存缓冲区)访问,可以随机存取,即:任何块都可以读写,不必考虑它在设备的什么地方。
块设备可以通过它们的设备特殊文件访问,但是更常见的是通过文件系统进行访问。
只有一个块设备可以支持一个安装的文件系统。
比如我们常见的电脑硬盘、SD卡、U盘、光盘等。
(3)网络设备
任何网络事务都经过一个网络接口形成,即一个能够和其他主机交换数据的设备。
访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在文件系统中不存在对应的节点。
内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包传输相关的函(socket函数)而不是read、write等。
比如我们常见的网卡设备、蓝牙设备。
4、驱动的功能
(1)对设备初始化和释放
(2)把数据从内核传送到硬件和从硬件读取数据
(3)读取应用程序传送给设备文件的数据和回送应用程序请求的数据
(4)检测和处理设备出现的错误
二、环境搭建——以树莓派开发板为例
树莓派是ARM架构的开发板。在烧录好系统之后(树莓派的系统烧录是将系统烧录进sd卡),配置无线网络(用于和开发的计算机进行通信)。
下一步,通信成功之后,就要在自己的计算机(以下简称PC)上进行开发。大致的流程如下:
1、安装交叉编译工具链
先去软件源刷新一下最新的软件清单:
sudo apt-get update
然后安装交叉编译器以及相应的一些依赖工具:
sudo apt-get install gcc-arm-linux-gnueabihf build-essential bc bison flex libssl-dev
(1)交叉编译器 gcc-arm-linux-gnueabihf
(2)build-essential
(3)其他工具
(4)交叉编译器的选择
①选择标准
②交叉编译器命名规则
2、下载对应内核源码
对应的,开发板上烧录的是什么系统,就需要下载对应的内核源码,这里是为了后续驱动开发做准备。
git clone --depth=1 --single-branch -b rpi-5.10.y https://github.com/raspberrypi/linux.git
3、获取开发板内核配置文件
每个开发板对应的系统有差异,所以配置文件所在的地方也有差异。这里采用的是安装内核头文件从而获取内核配置文件的方式。
sudo apt-get install raspberrypi-kernel-headers
安装完成之后,会得到一个目录:
/usr/src/linux-headers-$(uname -r)/
这里uname-r作为参数,表示的是正在运行的 Linux 操作系统的内核版本。
接下来讲整个目录打包,通过网络传到云服务器上
cd /usr/src
tar czf linux-headers.tar.gz linux-headers-$(uname -r)
scp linux-headers.tar.gz <云服务器用户>@<云服务器IP>:~/
在云服务器上解压:(这里以放到~/目录下为例,实际可以放到一个更干净的目录下)
tar xzf linux-headers.tar.gz -C ~/
4、写驱动的makefile
对于解压后的内核的头文件,需要做的就是在写驱动的makefile的时候放到KDIR里面。
obj-m += mydriver.oKDIR := /home/xxx/linux-headers-5.10.63-v7l+ # 解压的内核头文件目录
PWD := $(shell pwd)all:$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
这样就能正常编译驱动文件了。
三、驱动编译的两种方式
1、编译进内核
将驱动编译进 Linux 内核中,当 Linux 内核启动的时就会自动运行驱动程序。
2、编译成模块
将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用相应命令加载驱动模块。
内核模块是Linux内核向外部提供的一个插口;内核模块是具有独立功能的程序,他可以被单独编译,但不能单独运行。他在运行时被链接到内核作为内核的一部分在内核空间运行;内核模块便于驱动、文件系统等的二次开发。
四、驱动框架
1、模块加载函数
module_init(xxx_init);
module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数(理解是模块的构造函数),当加载驱动的时, xxx_init 这个函数就会被调用。
2、模块卸载函数
module_exit(xxx_exit);
module_exit函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数(理解是模块的析构函数),当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用
3、模块许可证明
MODULE_LICENSE("GPL") //添加模块 LICENSE 信息 ,LICENSE 采用 GPL 协议
4、模块参数(可选)
模块参数是一种内核空间与用户空间的交互方式,只不过是用户空间 --> 内核空间单向的,他对应模块内部的全局变量。
5、模块信息(可选)
MODULE_AUTHOR("songwei") //添加模块作者信息
6、模块打印printk
printk在内核中用来记录日志信息的函数,只能在内核源码范围内使用。和printf非常相似。
printk函数主要做两件事情:①将信息记录到log中 ②调用控制台驱动来将信息输出
printk打印等级
printk 可以根据日志级别对消息进行分类,一共有 8 个日志级别:
#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息 */
以下代码就是设置“gsmi: Log Shutdown Reason\n”这行消息的级别为 KERN_EMERG。
printk(KERN_DEBUG"gsmi: Log Shutdown Reason\n");
如果使用 printk 的时候不显式的设置消息级别,那 么printk 将会采用默认级别MESSAGE_LOGLEVEL_DEFAULT,默认为 4。
对于控制台打印出的日志,如果代码中的打印等级低于默认控制台打印等级,则不会打印到控制台中。在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT,定义如下:
#define CONSOLE_LOGLEVEL_DEFAULT 7
CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上,此宏默认为 7,意味着只有优先级高于 7 的消息才能显示在控制台上。
这个就是 printk 和 printf 的最大区别,可以通过消息级别来决定哪些消息可以显示在控制台上。默认消息级别为 4,4 的级别比 7 高,所示直接使用 printk 输出的信息是可以显示在控制台上的。
但是,所有打印的信息,不管优先级多少,都可以通过dmesg显示,必要时可以通过管道筛选:dmesg | grep hello
7、模块操作命令
(1)加载模块
①insmod XXX.ko
为模块分配内核内存、将模块代码和数据装入内存、通过内核符号表解析模块中的内核引用、调用模块初始化函数(module_init)
insmod要加载的模块有依赖模块,且其依赖的模块尚未加载,那么该insmod操作将失败
②modprobe XXX.ko
加载模块时会同时加载该模块所依赖的其他模块,提供了模块的依赖性分析、错误检查、错误报告
modprobe 提示无法打开“modules.dep”这个文件 ,输入 depmod 命令即可自动生成 modules.dep
(2)卸载模块
rmmod XXX.ko
(3)查看模块信息
- lsmod
- 查看系统中加载的所有模块及模块间的依赖关系
- modinfo (模块路径)
- 查看详细信息,内核模块描述信息,编译系统信息