前言
我们现在就写一个驱动小demo,环境准备:要有个linux系统,我的demo系统是一台老掉牙的笔记本上,装了个Ubunut18.04LTS 版本的系统。uname -a 命令我们查看一下这个系统的内核信息,如下:
内核版本是 4.15.0. x86_64的系统。
这个有什么用呢?就是下载对应的内核源码?我们要查询源码里面的一个文件。文件的目录是:
linux-source-4.15.0/Documentation/kbuild
在这个路径下有个 modulex.txt文件。很重要,我们要用它。
我们的目的就是让这个驱动加载到我们这台Linux机器的系统里面。
入口函数
驱动需要一个入口函数,在加载期间告知内核,这个是入口,(道理类似于应用程序的main函数)。
该函数的类型必须遵循如下的格式:
int xxxxx_func(void);
注意:
1. 有一个 int 返回值,如果该驱动被成功加载了,那么该接口必须返回0,如果内部出错,可以返回 <0 的值,告诉内核,该模块加载不成功。
2. 一个驱动模块,只能有一个入口函数
3. 告诉内核的方式采用宏 wrap,如下:
module_init(xxxx_funct)
出口函数
如果需要,就添加一个出口函数,如果不需要,那就拉倒。当然,如果没什么资源需要被释放的,那么建议,出口函数就空实现即可。有些驱动,一般被加载,可能不需要被卸载,也就不需要出口函数,但是呢,咱们自己写的时候尽量不要这么干,没什么资源可释放的,那就空实现即可。
函数类型如下:
void xxxx_func(void);
如何无参数,无返回值。告诉内核呢?
module_exit(xxxx_func)
这两个函数,都是固定的,仅仅会在驱动模块被加载或者被卸载的那一刻,被调用一次。
我们现在就自己写一个。
代码
#include <linux/init.h>
#include <linux/module.h>
MODULE_DESCRIPTION("Frocheng: Driver for DEMO!");
MODULE_AUTHOR("Frodo Cheng");
MODULE_LICENSE("GPL");
static int __init hello_init(void)
{
printk("===[frocheng]===[%s]===[%s]===[%d]===[Hello !]===\n",__FILE__, __func__, __LINE__);
return 0;
}
static void __exit hello_exit(void)
{
printk("===[frocheng]===[%s]===[%s]===[%d]===[Bye bye...]===\n",__FILE__, __func__, __LINE__);
}
module_init(hello_init);
module_exit(hello_exit);
头文件:
不用想太多,linux 内核的驱动,最基础的两个头文件。
__init 以及 __exit 实际也是宏,利用的 GNU C extension 中的语法,让代码在编译期间分布到指定的段中去。Linux 内核源码,大多数的驱动,都遵循这个原则,也可以不要。当然,对于嵌入式而言,如果交叉编译器不支持这个特性,也是有可能的,那就不要了,也没问题,不影响我们这个驱动的功能。
打印:printk,内核的打印函数,其实前面还有个参数,就是等级,如果省略,那就采用默认的等级去打印。这个地方,我们采用默认的方式打印即可。
MODULE_LICENSE("GPL");
这个玩意儿吧,也不是必须的,没有这一句,编译期间会有一句警告,就很烦,那就加上。
接下来要做的事儿,就是编译了。
驱动程序,可以单独的编译成模块,也可以静态编译到内核中去。
编译
打开前文所述的modules.txt,里面有编译的方法。
所以Makefile 怎么写呢,如下:
KERNDIR:= /lib/modules/`uname -r`/build/
PWD:= $(shell pwd)
obj-m:= hello.o
all:
make -C $(KERNDIR) M=$(PWD) modules
clean:
make -C $(KERNDIR) M=$(PWD) clean
注意:最好自己用vim或者其他编辑器,自己敲一遍以上 Makefile,因为tab的问题,要注意,要关闭掉expandtab的功能。也就是说一个tab = 4 空格这样的强制转换功能,要关闭掉。
说明一下:
先定义一下 kernel 的位置。就是上面module.txt里面说的位置。
其次是当前目录,就是 Makefile 跟咱们的驱动代码 hello.c的位置。
obj-m 表示,采用动态模块的方式,编译我们的驱动,与之对应的如果是整体的内核代码编译,需要将驱动编译进内核image中,这个地方就写 obj-y就行了。
modules,就是编译模块。
接下来,我们 make 一下就OK了。
make之前, make过程的输出,make之后的结果。
驱动编译出来,最关键的输出,就是那个 hello.ko。
查看,加载,卸载
内核模块操作的相关的命令:
1. modinfo hello.ko
查看信息
2. lsmod 查看当前系统已经加载的模块。
3. insmod hello.ko 加载模块 hello_init 被调用
这个命令需要 管理员权限。(sudo insmod hello.ko)
4. rmmod hello 卸载模块, hello_exit 被调用
这个命令也需要管理员权限,另外,不需要 .ko了。(sudo rmmod hello)
5. dmesg 查看 printk 输出的内容。
(sudo dmesg -c 清除没用的当前我们还看不懂的那些内核日志,这条命令,最好开始就可以执行一遍)
加载时传参
#include <linux/init.h>
#include <linux/module.h>
MODULE_DESCRIPTION("Frocheng: Driver for DEMO!");
MODULE_AUTHOR("Frodo Cheng");
MODULE_LICENSE("GPL");
static int irq;
static char* name;
module_param(irq, int, 0664);
module_param(name, charp, 00);
static int __init hello_init(void)
{
printk("===[frocheng]===[%s]===[%s]===[%d]===[Hello !]===\n",__FILE__, __func__, __LINE__);
printk("name = %s\n", name);
return 0;
}
static void __exit hello_exit(void)
{
printk("irq = %d\n", irq);
printk("===[frocheng]===[%s]===[%s]===[%d]===[Bye bye...]===\n",__FILE__, __func__, __LINE__);
}
module_init(hello_init);
module_exit(hello_exit);
module_param(param_name, param_type, param_perm);
如果需要传参,我们需要进行上述的参数声明语法,并且我们需要先定义参数变量。参考内核其他驱动的代码,这个参数都是静态的全局变量。
param_name:变量名
param_type: 类型,注意,不接受浮点类型。
param_perm:权限,当非0时,每当模块加载完成之后,会在 /sys/module 目录下,生成一个跟模块名同名的目录,该目录下有一个parameters目录,此目录下,会针对每一个变量,生成一个同名的文件,可通过修改该文件中的值,去修改变量的值。所以这个权限的表达方式,其实跟文件的权限的表达方式一致。注意,不要给到 x 权限。当为0时,就不会生成该文件。虽然有文件,该文件会一直存在与内存中,所以为了节约内存,需要传参时,尽量给0权限,禁止参数文件的生成。
module_param_hw(),跟硬件相关。
还有数组参数声明,详细,可参考内核源码中的 include/linux/moduleparam.h文件。