1. Linux驱动的学习方法
Linux内核中有上百个驱动,知识点多且杂,对于想学习驱动的同学来说,需要尽快掌握基础知识:如开发板的基本使用,硬件基础知识,开发环境的搭建,Linux常用工具,内核的编译以及烧写,Linux shell命令,C语言基础,Linux内核的简单裁减和配置,Linux系统编程等等
没有上面的基础知识,驱动的学习无疑是在建空中楼阁。
Linux操作系统相当于“一个球”,程序员要做的事情就是在这个球上添加驱动来实现具体的功能,不用去管这个球是从哪里开始旋转,转到什么地方了。更简单的理解就是,Linux只是一个工具,学会使用就可以了,就像学习汽车驾驶,没有教练会从发动机原理开始讲解,只会给你发一些指令如“方向盘右转一圈”“方向盘左转一圈”“拉手刹”“换挡”等。
当然学习Linux的最好的方法是阅读内核,但是在没有基础之前不要过多的去研究内核的东西。
在嵌入式Linux驱动工程师的工作中,移植驱动是必须掌握的技能,在学会了如何移植驱动,找到合适的工作之后,如果你对内核源码感兴趣,而且还有富余的时间,可以看一看内核中“精妙”的代码,不过这对于工作并没有太多直接的帮助,可以纯粹的作为一个兴趣爱好来研读。
2. Linux设备驱动的分类
Linux设备驱动程序在Linux的内核源代码中占有很大的比例,源代码的长度日益增加,主要是驱动程序代码的不断充实。在Linux内核的不断升级过程中,驱动程序的结构却是相对稳定的。
Linux系统的设备分为字符设备(char device),块设备(block device)和网络设备(network device)三种。字符设备是指存取时没有缓存的设备。块设备的读写都有缓存来支持,并且块设备必须能够随机存取(random access),字符设备则没有这个要求。典型的字符设备包括鼠标、键盘、串行口等。块设备主要包括硬盘软盘设备、CD-ROM等。一个文件系统要安装(mount)到操作系统的块设备上。
网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSD unix的socket机制。在系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。系统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。
3.以模块的形式编译驱动
模块的加载函数“module_init(function)”,返回整数型,如果执行成功,则返回0。否则返回错误信息。
有时候芯片供应商并不提供芯片驱动的源码,只是提供驱动module的ko文件,这个时候就需要调用request_module(module_name)来加载驱动。
在Linux中,标示为_ _init的函数都是初始化函数,这些函数占用的内存空间,在Linux启动或者模块加载初始化之后,是会被释放掉的。除了函数,数据也是可以被定义为“_ _数据”,这些数据在Linux启动或者或者模块加载初始化之后,也是会被释放掉。这一部分的知识,在后面会逐渐使用到。
模块卸载就是模块加载的“逆向过程”,比较容易理解。
模块的卸载函数“module_init(function)”不返回任何值。
一般说来,我们在卸载函数中要完成和加载函数中相反的功能,例如你调用了系统或者硬件的资源,那么你在卸载函数中,就需要释放掉。
Linux模块一般是使用脚本语言来编译,脚本语言的种类非常多,语法丰富,不过我们只需要学会使用即可,能够仿照着写就可以了,这并不影响我们开发。
如下图所示,模块的编译流程图。
通过上图可以看到编译中,当执行执行make命令之后,调用Makefile文件进行Linux模块的编译。
Linux模块的编译分为两个条线。
红色的线:进入Linux源码中,调用版本信息以及一些头文件等。
这一条线经过的是整个Linux的源码文件。
橙色的线:搜集完Linux源码树的信息之后,Makefile继续执行,调用编译.KO文件的源码文件,这里是mini_linux_module.c这个“iTop4412_Kernel_3.0”整个源码。
这一条线走的是mini_linux_module.c,虽然都是源码,但是此源码非彼源码。
Makefile文件通过执行上面两条线,通过搜集到信息,最终编译生成.KO模块
这里我们要学习和理解的重点就是,编译模块也必须用到内核的源码,因为这涉及到内核版本以及头文件。如果版本不对,那么模块有可能无法加载和运行;如果没有头文件,编译就无法通过。
在单片机或者上位机编程的时候,都有集成开发工具。程序员按照开发工具的规则,将代码放入指定的位置,通常是一个main.c文件加上很多.c文件,代码写好了,开发工具中某个按钮一点,就给你自动编译成了二进制文件。
在Linux中,并没有这样的集成开发工具,这里还需要自己写编译文件Makefile。
编译文件一般是用脚本编写,脚本成千上万,脚本语言学也学不完,脚本的学习最好是用到哪里学到哪。
Makefile编译文件如下,下面的这个文件可以在Linux驱动视频目录“02_HelloDriverModule”下找到。
#!/bin/bash
#通知编译器我们要编译模块的哪些源码
#这里是编译itop4412_hello.c这个文件编译成中间文件mini_linux_module.o
obj-m += mini_linux_module.o
#源码目录变量,这里用户需要根据实际情况选择路径
#作者是将Linux的源码拷贝到目录/home/topeet/android4.0下并解压的
KDIR := /home/topeet/android4.0/iTop4412_Kernel_3.0
#当前目录变量
PWD ?= $(shell pwd)
#make命名默认寻找第一个目标
#make -C就是指调用执行的路径
#$(KDIR)Linux源码目录,作者这里指的是/home/topeet/android4.0/iTop4412_Kernel_3.0
#$(PWD)当前目录变量
#modules要执行的操作
all:
make -C $(KDIR) M=$(PWD) modules
#make clean执行的操作是删除后缀为o的文件
clean:
rm -rf *.o
如上图所示,就是编译mini_linux_module的脚本文件。下面详细的介绍每一句的含义。
#!/bin/bash
通知编译器这个脚本使用的是那个脚本语言
obj-m += mini_linux_module.o
这是一个标准用法,表示要将mini_linux_module.c文件编译成mini_linux_module.o文件,如果还需要编译其它的文件,则在后面添加即可。
KDIR := /home/topeet/android4.0/iTop4412_Kernel_3.0
这一行代码表示内核代码的目录,如果没有内核源码,那么模块的编译无法进行,因为会缺乏版本支持和头文件。KDIR是一个变量。
PWD ?= $(shell pwd)
这一句是提供一个变量,然后将当前目录的路径传给这个变量。pwd是一个命令,表示当前目录,PWD是一个变量。
all:
make -C $(KDIR) M=$(PWD) modules
在使用执行脚本编译命令“make”的时候,它会默认来寻找这一句,make -C 表示调用执行的路径,也就是变量KDIR,变量KDIR中有内核源码目录的路径。
PWD表示当前目录。
modules表示将驱动编译成模块的形式,也是就是最终生成KO文件。
clean:
rm -rf *.o
在重新修改了源码之后,可以执行“make clean”命令来清除一些无用的中间文件,这里选择的是清除后缀为“o”的文件。
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Hcamal");
int hello_init(void)
{
printk(KERN_INFO "Hello World\n");
return 0;
}
void hello_exit(void)
{
printk(KERN_INFO "Goodbye World\n");
}
module_init(hello_init);
module_exit(hello_exit);
Linux下的驱动是使用C语言进行开发的,但是和我们平常写的C语言也有不同,因为我们平常写的C语言使用的是Libc库,但是驱动是跑在内核中的程序,内核中却不存在libc库,所以要使用内核中的库函数。
比如printk
可以类比为libc中的printf
,这是在内核中定义的一个输出函数,但是我觉得更像Python里面logger函数,因为printk
的输出结果是打印在内核的日志中,可以使用dmesg
命令进行查看
驱动代码只有一个入口点和一个出口点,把驱动加载到内核中,会执行module_init
函数定义的函数,在上面代码中就是hello_init
函数。当驱动从内核被卸载时,会调用module_exit
函数定义的函数,在上面代码中就是hello_exit
函数。
上面的代码就很清晰了,当加载驱动时,输出Hello World
,当卸载驱动时,输出Goodbye World
PSS: printk
输出的结果要加一个换行,要不然不会刷新缓冲区
编译驱动
驱动需要通过make命令进行编译,Makefile
如下所示:
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
else
KERN_DIR ?= /usr/src/linux-headers-$(shell uname -r)/
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERN_DIR) M=$(PWD) modules
endif
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
一般情况下,内核的源码都存在与/usr/src/linux-headers-$(shell uname -r)/
目录下
比如:
$ uname -r
4.4.0-135-generic
/usr/src/linux-headers-4.4.0-135/ --> 该内核源码目录
/usr/src/linux-headers-4.4.0-135-generic/ --> 该内核编译好的源码目录
而我们需要的是编译好后的源码的目录,也就是/usr/src/linux-headers-4.4.0-135-generic/
驱动代码的头文件都需要从该目录下进行搜索
M=$(PWD)
该参数表示,驱动编译的结果输出在当前目录下
最后通过命令obj-m := hello.o
,表示把hello.o
编译出hello.ko
, 这个ko文件就是内核模块文件
加载驱动到内核
需要使用到的一些系统命令:
lsmod
: 查看当前已经被加载的内核模块insmod
: 加载内核模块,需要root权限rmmod
: 移除模块
比如:
# insmod hello.ko // 把hello.ko模块加载到内核中
# rmmod hello // 把hello模块从内核中移除
知识点1 -- 驱动分类
驱动分为3类,字符设备、块设备和网口接口,上面代码举例的是字符设备,其他两种,之后再说。
如上图所示,brw-rw----
权限栏,b开头的表示块设备(block),c开头的表示字符设备(char)
知识点2 -- 主次编号
主编号用来区分驱动,一般主编号相同的表示由同一个驱动程序控制。
一个驱动中能创建多个设备,用次编号来区分。
主编号和次编号一起,决定了一个驱动设备。
如上图所示,
brw-rw---- 1 root disk 8, 0 Dec 17 13:02 sda
brw-rw---- 1 root disk 8, 1 Dec 17 13:02 sda1
设备sda
和sda1
的主编号为8,一个此编号为0一个此编号为1
知识点3 -- 驱动是如何提供API的
在我的概念中,驱动提供的接口是/dev/xxx
,在Linux下Everything is File
,所以对驱动设备的操作其实就是对文件的操作,所以一个驱动就是用来定义,打开/读/写/......一个/dev/xxx
将会发生啥,驱动提供的API也就是一系列的文件操作。
有哪些文件操作?都被定义在内核<linux/fs.h>
[5]头文件中,file_operations
结构体
上面我举例的代码中:
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.open = scull_open,
.release = scull_release,
};
我声明了一个该结构体,并赋值,除了owner
,其他成员的值都为函数指针
之后我在scull_setup_cdev
函数中,使用cdev_add
向每个驱动设备,注册该文件操作结构体
比如我对该驱动设备执行open操作,则会去执行scull_open
函数,相当于hook了系统调用中的open
函数
知识点4 -- 在/dev下生成相应的设备
对上面的代码进行编译,得到scull.ko,然后对其进行签名,最后使用insmod
加载进内核中
查看是否成功加载:lsmod
虽然驱动已经加载成功了,但是并不会在/dev目录下创建设备文件,需要我们手动使用mknod
进行设备链接
参考: