Linux驱动基础知识 linux驱动是linux内核驱动的全称。对设备驱动最通俗的解释就是“驱使硬件设备行动”,有操作系统的存在则大大降低了应用软件与硬件平台的耦合度。
1.设备驱动分类 linux是文件型系统,所有硬件都会在对应的目录(/dev)下面用相应的文件表示。在文件系统下,都有对应文件与键盘、鼠标、硬盘等实实在在硬件硬件设备关联,访问这些文件就可以访问实际硬件。
按照读写存储数据方式,我们可以把设备分为以下几种:字符设备、块设备和网络设备。 而Linux三大驱动就是指对这些设备的驱动,即字符设备、块设备驱动和网络设备驱动。
1.1 字符设备 字符设备指能够像字节流串行顺序依次进行访问的设备,对它的读写是以字节为单位。
字符设备的特点:
一个字节一个字节读写的设备
读取数据需要按照先后数据(顺序读取)
每个字符设备在/dev目录下对应一个设备文件,linux用户程序通过设备文件(或称 设备节点 )来使用驱动程序操作字符设备。
常见的字符设备有鼠标、键盘、串口、SPI、I2C等
1.2 块设备 块设备是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区。
块设备的特点:
数据以固定长度进行传输,比如512K
块设备能够随机访问,而字符设备则只能顺序访问。
块设备包括硬盘、磁盘、U盘和SD卡等
每个块设备在/dev目录下对应一个设备文件,linux用户程序可以通过设备文件(或称设备节点)来使用驱动程序操作块设备。
块设备可以容纳文件系统,所以一般都通过文件系统来访问,而不是/dev设备节点。
1.3. 网络设备 网络设备驱动不同于字符设备和块设备,不在/dev下以文件节点代表,而是通过单独的网络接口来代表。
网络设备的特点:
网络接口没有像字符设备和块设备一样的设备号和/dev设备节点,只有接口名,如eth0,eth1
对网络设备的访问只能通过socket操作,而不是open、closc、read、write
1.4 对比
驱动类型
数据交换单位
访问模式
典型设备示例
字符设备驱动
字节(Byte)
顺序访问
串口、键盘、鼠标
块设备驱动
固定大小的数据块(如 512B/4KB)
随机访问
硬盘、SSD、U盘
网络设备驱动
数据包(Packet / Frame)
无固定顺序
网卡、Wi-Fi 模块
2.驱动的构建方式(Build Target Type)
内置(Built-in) —— 编译后直接链接进内核镜像(如 vmlinux / zImage / uImage);
模块(Module) —— 编译成 .ko 文件,可动态加载/卸载。关于这一部分的实现原理可用参考linux内核模块;
3.linux内核驱动的实现方式 Linux 内核驱动的实现方式可分为:
传统驱动(Legacy Driver / Board-file based Driver)
现代驱动(Device Tree based Driver / DT-enabled Driver)
特性
传统驱动(Legacy)
现代驱动(Device Tree based)
硬件描述方式
硬编码在驱动或 arch/ 板级文件中
通过设备树(.dts)描述硬件资源
资源获取方式
platform_get_resource(), 宏定义、全局变量
of_get_named_gpio(), of_property_read_u32() 等
设备注册
手动 platform_device_register()
内核自动从设备树生成 platform_device
可移植性
❌ 差,换板子要改驱动或板文件
✅ 好,只需改设备树,驱动通用
维护性
❌ 差,驱动与硬件强耦合
✅ 好,驱动与硬件解耦
内核主线支持
⚠️ 逐步淘汰,仅老平台保留
✅ 推荐,ARM/RISC-V 新平台必须使用
典型内核版本
Linux 2.6 ~ 3.10
Linux 3.7+(ARM强制),4.4+(主流全面支持)
4.一个简单的内核模块加载/卸载的演示 Linux内核模块的代码框架通常由下面几个部分组成:
模块加载函数(必须): 当通过insmod或modprobe命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。
模块卸载函数(必须): 当执行rmmod命令卸载模块时,模块卸载函数就会自动被内核自动执行,完成相关清理工作。
模块许可证声明(必须): 许可证声明描述内核模块的许可权限,如果模块不声明,模块被加载时,将会有内核被污染的警告。
模块参数: 模块参数是模块被加载时,可以传值给模块中的参数。
模块导出符号: 模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。
模块的其他相关信息: 可以声明模块作者等信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <linux/init.h> #include <linux/module.h> static int module_init_func (void ) { printk(KERN_INFO "Init Hello World module...\n" ); return 0 ; } static void module_exit_func (void ) { printk(KERN_INFO "Hello World Exit...\n" ); } module_init(module_init_func); module_exit(module_exit_func); MODULE_LICENSE("GPL v2" ); MODULE_AUTHOR("Gryphon" ); MODULE_DESCRIPTION("A simple hello world module" ); MODULE_ALIAS("Hello world module" );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 obj-m += helloworld.o KDIR := /home/gryphon/SDK/linux PWD := $(shell pwd) CROSS_COMPILE := /home/gryphon/SDK/tools/gcc-linaro-6.3.1-2017.05-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- ARCH := arm CC := $(CROSS_COMPILE) gcc default: $(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) CC=$(CC) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules clean .PHONY : default clean
输出log示例:
1 2 3 4 5 6 7 8 9 10 11 # lsmod Module Size Used by Tainted: G # insmod helloworld.ko [ 7377.171567] Init Hello World module... # # lsmod Module Size Used by Tainted: G helloworld 16384 0 # rmmod helloworld.ko [ 7386.308034] Hello World Exit... #
5.内核模块传参与符号共享 内核模块作为一个可拓展的动态模块,为Linux内核提供了灵活性,但是有时我们需要根据不同的应用场景给内核传递不同的参数,例如在程序中开启调试模式、设置详细输出模式以及制定与具体模块相关的选项,都可以通过参数的形式来改变模块的行为。
Linux内核提供一个宏来实现模块的参数传递。
module_param函数 (内核源码/include/linux/moduleparam.h)
#define module_param(name, type, perm) \
module_param_named(name, name, type, perm)
#define module_param_array(name, type, nump, perm) \
module_param_array_named(name, name, type, nump, perm)
内核支持的参数类型有byte,short,ushort,int,uint,long,ulong,charp,bool,invbool。 其中charp表示的是字符指针,bool是布尔类型,其值只能为0或者是1;invbool是反布尔类型,其值也是只能取0或者是1,但是true值表示0,false表示1。变量是char类型时,传参只能是byte,char * 时只能是charp。
实现模块的参数传递
/include/linux/export.h
#define EXPORT_SYMBOL(sym) __EXPORT_SYMBOL(sym, “”)
//EXPORT_SYMBOL宏用于向内核导出符号,这样的话,其他模块也可以使用我们导出的符号了。
5.1 测试用例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <linux/init.h> #include <linux/module.h> static int itype = 0 ;module_param(itype, int , 0 ); EXPORT_SYMBOL(itype); static bool btype = 0 ;module_param(btype, bool , 0644 ); static char ctype = 0 ;module_param(ctype, byte, 0 ); static char *stype = 0 ;module_param(stype, charp, 0644 ); static int __init param_init (void ) { printk(KERN_ALERT "param init!\n" ); printk(KERN_ALERT "itype=%d\n" , itype); printk(KERN_ALERT "btype=%d\n" , btype); printk(KERN_ALERT "ctype=%d\n" , ctype); printk(KERN_ALERT "stype=%s\n" , stype); return 0 ; } static void param_exit (void ) { printk(KERN_ALERT "param exit!\n" ); printk(KERN_ALERT "itype=%d\n" , itype); printk(KERN_ALERT "btype=%d\n" , btype); printk(KERN_ALERT "ctype=%d\n" , ctype); printk(KERN_ALERT "stype=%s\n" , stype); } int my_add (int a, int b) { return a + b; } EXPORT_SYMBOL(my_add); int my_sub (int a, int b) { return a - b; } EXPORT_SYMBOL(my_sub); module_init(param_init); module_exit(param_exit);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #ifndef __CALCULATION_H__ #define __CALCULATION_H__ extern int itype;int my_add (int a, int b) ;int my_sub (int a, int b) ;#endif #include <linux/init.h> #include <linux/module.h> #include "calculation.h" static int __init calculation_init (void ) { printk(KERN_ALERT "calculation init!\n" ); printk(KERN_ALERT "itype+1 = %d, itype-1 = %d\n" , my_add(itype, 1 ), my_sub(itype, 1 )); return 0 ; } static void calculation_exit (void ) { printk(KERN_ALERT "calculation init!\n" ); printk(KERN_ALERT "itype+1 = %d, itype-1 = %d\n" , my_add(itype, 1 ), my_sub(itype, 1 )); } module_init(calculation_init); module_exit(calculation_exit); MODULE_LICENSE("GPL v2" ); MODULE_AUTHOR("Gryphon" ); MODULE_DESCRIPTION("A simple hello world module with calculation functions" ); MODULE_ALIAS("Hello world module" ); # 目标模块名称(生成 moduleparam_test.ko) obj-m += moduleparam_test.o # 内核源码目录(你提供的路径) KDIR := /home/gryphon/SDK/linux # 当前目录 PWD := $(shell pwd) CROSS_COMPILE := /home/gryphon/SDK/tools/gcc-linaro-6.3 .1 -2017.05 -x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- ARCH := arm CC := $(CROSS_COMPILE)gcc # 默认目标 default : $(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) CC=$(CC) modules # 清理 clean: $(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean .PHONY: default clean
测试log:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # insmod moduleparam_test.ko itype=123 btype=1 ctype=200 stype=abc [ 144.225190] moduleparam_test: module license 'unspecified' taints kernel. [ 144.232153] Disabling lock debugging due to kernel taint [ 144.238806] param init! [ 144.241271] itype=123 [ 144.243539] btype=1 [ 144.245634] ctype=200 [ 144.248043] stype=abc # insmod calculation.ko [ 147.224498] calculation init! [ 147.227575] itype+1 = 124, itype-1 = 122 # # rmmod calculation.ko [ 150.404026] calculation init! [ 150.407110] itype+1 = 124, itype-1 = 122 # rmmod moduleparam_test.ko [ 154.941217] param exit! [ 154.943692] itype=123 [ 154.946010] btype=1 [ 154.948234] ctype=200 [ 154.950513] stype=abc #
参考链接 2. Linux驱动简述 — [野火]嵌入式Linux驱动开发实战指南——基于LubanCat-全志系列板卡 文档
驱动入口点——platform_device解析 - 知乎