简介:这份资源是《Linux设备驱动开发详解:基于最新的Linux 4.0内核》一书的配套样例代码包,面向具备一定C语言与操作系统基础、希望深入内核驱动开发的工程师与学习者,帮助其通过可运行代码理解驱动注册、硬件交互与内核接口调用等核心问题。压缩包共122个文件,约659KB,以c源码与cmd编译脚本为主,辅以makefile构建文件、o目标文件、ko内核模块及mod、symvers等模块元数据,另含少量头文件与测试程序,覆盖字符设备、块设备、网络设备、中断处理、I/O调度、内存管理、电源管理及模块化设计等典型场景。目前已有197人学习下载。读者可借助这些样例对照书中章节动手编译加载模块,观察设备节点创建与读写流程,排查编译与加载错误,从而把设备模型、globalmem与globalfifo等抽象概念落到可调试的代码上,为实际驱动项目开发积累经验。
1. 从 drivers_drivers_linux_Kernel_ 这个目录名说起:内核驱动到底在管什么
如果你在嵌入式 Linux 项目里翻到过一个叫drivers_drivers_linux_Kernel_的目录或压缩包,第一反应大概率是「这名字怎么像手滑打出来的」。别急着删,这类命名通常来自内核源码树里drivers/目录被反复拷贝、打包、再嵌套的结果,背后往往是一份裁剪过的 BSP(板级支持包)或者某个 SoC 厂商放出的内核驱动集合。它要解决的问题很具体:让你手上那块板子的网卡、串口、I2C 传感器、GPIO 扩展芯片能被 Linux 内核认出来并正常工作。
这篇文章面向三类人:刚拿到开发板、不知道该从内核哪一层下手的嵌入式新手;需要给国产 Linux 发行版(比如 Kylin Linux Advanced Server V10、Debian GNU/Linux 13 trixie 这类环境)适配硬件的驱动工程师;以及被「驱动加载失败但 dmesg 只给一行看不懂的报错」折磨过的运维。核心词 drivers、linux、kernel 会贯穿始终,因为这三个词就是这条链路的全部:kernel 提供框架,drivers 填充实现,linux 把它们组织成可加载的模块或内置代码。读完你应该能自己判断一个驱动目录能不能用、怎么编、怎么加载、加载不上时先看哪里。
2. 内核驱动目录的组成与编译体系:先看懂再动手
2.1 一个驱动目录里通常有什么
拿到drivers_drivers_linux_Kernel_这种目录,先别管名字,进去看结构。一个规范的驱动子目录一般包含这几类文件:*.c源码、*.h头文件、Makefile、Kconfig,有时还有*.dts或*.dtsi设备树片段、*.config默认配置。Kconfig决定这个驱动在make menuconfig里以什么名字、什么依赖出现;Makefile决定它编成内置(obj-y)还是模块(obj-m)。
判断一份驱动能不能用,第一步是看它的Makefile里obj-$(CONFIG_XXX)对应的CONFIG_XXX有没有在Kconfig里定义。如果只有Makefile没有Kconfig,那这份代码大概率是从别处抠出来的,需要你手动补配置项,否则menuconfig里根本找不到它。
# 进入驱动目录,先看文件构成 ls -la drivers_drivers_linux_Kernel_/ # 看 Makefile 里编成模块还是内置 grep -E "obj-\\\$\(CONFIG" drivers_drivers_linux_Kernel_/Makefile # 看 Kconfig 里定义的配置项名和依赖 grep -E "config |depends on|tristate|bool" drivers_drivers_linux_Kernel_/Kconfig上面三条命令的逻辑:ls确认目录完整性;grep obj-找出这个驱动受哪个CONFIG控制,obj-m是模块、obj-y是内置;grep config从Kconfig里读出配置项名、类型(tristate可编模块、bool只能内置)和依赖条件。参数上,tristate意味着你可以选Y(内置)、M(模块)、N(不编),bool只有Y/N。如果依赖里写了depends on I2C,那你的内核必须先开 I2C 子系统,否则这个选项在menuconfig里是灰的。
2.2 把驱动挂进内核源码树的正确姿势
很多人图省事,直接把驱动目录拷到内核源码的drivers/下就make,结果编译报错说找不到符号。原因是内核的编译体系靠层层Makefile和Kconfig串联,你只拷了目录,没在上层Makefile和Kconfig里登记,编译系统根本不会进去。
正确做法是三步:拷贝目录、改上层Makefile、改上层Kconfig。假设你要把驱动挂到drivers/net/下:
# 1. 拷贝驱动目录到内核源码树 cp -r drivers_drivers_linux_Kernel_/ /path/to/linux-source/drivers/net/mydriver/ # 2. 在 drivers/net/Makefile 末尾追加一行 echo 'obj-$(CONFIG_MYDRIVER) += mydriver/' >> /path/to/linux-source/drivers/net/Makefile # 3. 在 drivers/net/Kconfig 里 source 进驱动的 Kconfig # 手动编辑,在合适位置加一行: # source "drivers/net/mydriver/Kconfig"逻辑说明:第一步只是放文件;第二步让上层Makefile知道有个子目录要编,CONFIG_MYDRIVER是你驱动Kconfig里定义的配置项;第三步让menuconfig能递归读到你的Kconfig,否则配置项不会出现。参数上,obj-$(CONFIG_MYDRIVER) += mydriver/里的mydriver/末尾斜杠表示这是个子目录,内核会进去读它自己的Makefile。漏掉斜杠会当成文件处理,直接报错。
提示:改完
Kconfig后,menuconfig里如果还是找不到你的选项,先确认source路径写对,再确认上层Kconfig有没有被更上层的Kconfig引用。内核配置是树状递归,断一层就全断。
2.3 编译与加载:从 .ko 到 dmesg 出现你的驱动名
配置好之后,编译分两种:整内核编译和只编模块。只编模块快得多,适合反复调试。
# 只编译某个模块(前提是内核已经配置过,有 .config) make M=drivers/net/mydriver modules # 编译完成后找 .ko 文件 find drivers/net/mydriver -name "*.ko" # 加载模块 sudo insmod mydriver.ko # 看内核日志确认加载结果 dmesg | tail -20逻辑说明:make M=xxx modules是内核编译体系提供的「只编指定目录模块」入口,它依赖当前目录下的.config,所以你得先make menuconfig或make defconfig生成配置。insmod把.ko加载进内核,如果驱动里有printk或dev_info,dmesg就能看到。参数上,M=后面跟的是相对内核源码根的路径,不是绝对路径。insmod需要 root 权限,加载失败时dmesg最后几行就是原因,常见的是「Unknown symbol」表示依赖模块没加载,「Invalid module format」表示内核版本和编译时用的版本不一致。
3. 设备树与驱动匹配:为什么你的 probe 函数不执行
3.1 设备树里 compatible 和驱动 of_match_table 的对应关系
现代 Linux 驱动(尤其是 ARM、RISC-V 平台)靠设备树(Device Tree)描述硬件,驱动靠compatible字符串匹配。如果你的驱动probe函数死活不执行,九成是compatible对不上。
设备树里一个节点长这样:
&i2c1 { my_sensor: my_sensor@48 { compatible = "vendor,my-sensor"; reg = <0x48>; status = "okay"; }; };驱动里对应的匹配表:
static const struct of_device_id my_sensor_of_match[] = { { .compatible = "vendor,my-sensor" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_sensor_of_match); static struct i2c_driver my_sensor_driver = { .driver = { .name = "my_sensor", .of_match_table = my_sensor_of_match, }, .probe = my_sensor_probe, .remove = my_sensor_remove, }; module_i2c_driver(my_sensor_driver);逻辑说明:设备树节点的compatible必须和驱动of_device_id表里的字符串完全一致,包括大小写和连字符。reg = <0x48>是 I2C 从机地址,驱动里通过client->addr拿到。module_i2c_driver宏把注册和注销封装成模块入口,省去手写init/exit。参数上,compatible推荐用厂商前缀,型号格式,避免和别的厂商撞名。status = "okay"必须写,默认是disabled,不写节点不生效。
3.2 用 of_match 和 sysfs 验证匹配是否成功
匹配成功与否,不用猜,两个地方能看。
# 看设备树里节点有没有被内核解析到 ls /proc/device-tree/ | grep my_sensor # 看驱动有没有绑定到设备 ls /sys/bus/i2c/drivers/my_sensor/ # 看具体设备目录下有没有 driver 软链接 ls -l /sys/bus/i2c/devices/1-0048/driver逻辑说明:/proc/device-tree/是内核解析设备树后暴露的视图,节点在说明设备树本身没问题。/sys/bus/i2c/drivers/my_sensor/下如果有设备名,说明驱动和设备的匹配成功了。/sys/bus/i2c/devices/1-0048/driver是个软链接,指向绑定的驱动,如果这个链接不存在,说明没绑上。参数上,1-0048里的1是 I2C 总线号,0048是 7 位地址的十六进制。总线号对不上,说明设备树里挂错了 I2C 控制器。
注意:
compatible字符串里不要用下划线,设备树规范推荐连字符。有些老驱动用下划线也能跑,但换到新内核或新工具链就可能匹配失败,这是血泪经验。
3.3 平台驱动和字符设备驱动的注册路径差异
不是所有驱动都走设备树匹配。平台驱动(platform driver)走platform_driver_register,字符设备驱动走register_chrdev或alloc_chrdev_region。搞混注册路径,设备节点不会出现在/dev下。
平台驱动典型结构:
static int my_platform_probe(struct platform_device *pdev) { struct resource *res; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(&pdev->dev, "no memory resource\n"); return -ENODEV; } /* 映射寄存器 */ base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(base)) return PTR_ERR(base); return 0; } static struct platform_driver my_platform_driver = { .probe = my_platform_probe, .driver = { .name = "my_platform", .of_match_table = my_platform_of_match, }, }; module_platform_driver(my_platform_driver);逻辑说明:平台驱动从platform_device里拿资源(内存、中断、时钟),platform_get_resource取第 0 个内存资源,devm_ioremap_resource做映射并自动管理释放。参数上,IORESOURCE_MEM表示内存映射资源,0是资源索引,一个设备可能有多个内存段。devm_前缀的函数会在设备卸载时自动释放,不用手写iounmap,这是防止内存泄漏的后悔药。
字符设备驱动则要自己申请设备号、创建设备类、导出设备节点:
static int __init my_char_init(void) { alloc_chrdev_region(&devno, 0, 1, "my_char"); cdev_init(&my_cdev, &my_fops); cdev_add(&my_cdev, devno, 1); my_class = class_create(THIS_MODULE, "my_char"); device_create(my_class, NULL, devno, NULL, "my_char"); return 0; }逻辑说明:alloc_chrdev_region动态申请设备号,cdev_init绑定文件操作集,cdev_add注册到内核,class_create和device_create负责在/dev下生成节点。参数上,0, 1表示从次设备号 0 开始申请 1 个设备。少了device_create,/dev/my_char就不会出现,用户态打不开设备。
4. 驱动调试避坑:dmesg 报错背后的五个真实原因
4.1 现象:insmod 报 Unknown symbol,原因:依赖模块没加载
加载驱动时dmesg出现Unknown symbol in module或disagrees about version of symbol。原因是驱动用到了别的模块导出的符号,但那个模块没先加载,或者内核版本和编译时不一致导致符号 CRC 对不上。
解决:先用modinfo mydriver.ko看depends字段,把依赖模块按顺序insmod。如果是版本不一致,确认编译用的内核头文件和目标板运行的内核是同一份,uname -r和编译目录要对上。
4.2 现象:probe 不执行,原因:compatible 不匹配或 status 为 disabled
设备树节点写了,驱动也加载了,但probe就是不进。先查compatible字符串是否完全一致,再查节点status是不是okay。还有一种情况是节点挂在了错误的父节点下,比如 I2C 设备挂到了 SPI 控制器下,总线类型不对,匹配自然失败。
解决:用ls /proc/device-tree/逐级确认节点路径,用cat /proc/device-tree/.../compatible看实际值。status默认是disabled,不显式写okay不会激活。
4.3 现象:模块加载成功但 /dev 下没节点,原因:设备类没创建
insmod返回 0,dmesg也没报错,但/dev下找不到设备节点。原因是字符设备驱动只注册了cdev,没调class_create和device_create,或者udev/mdev没运行。
解决:确认代码里有class_create和device_create。如果用的是精简根文件系统,可能没有udev,需要手动mknod创建设备节点,或者用mdev -s扫描。
4.4 现象:编译报 implicit declaration,原因:头文件路径或内核版本 API 变了
编译驱动时出现implicit declaration of function,说明函数没声明。常见于跨内核版本移植,老代码用的 API 在新内核里改了名字或参数。
解决:查内核源码里对应函数的头文件,确认#include路径。比如class_create在早期内核是class_create(THIS_MODULE, name),新内核改成class_create(name),参数个数变了。用grep -r "函数名" include/在内核源码里找声明。
4.5 现象:加载后系统卡死或 oops,原因:寄存器映射或中断处理有误
驱动加载后系统直接 oops 或卡死,dmesg里有Unable to handle kernel paging request。原因是访问了未映射的地址,或者中断处理函数里做了耗时操作。
解决:检查ioremap的地址和长度是否和设备树reg一致,中断处理函数里不要用msleep或printk大量输出。用devm_系列函数管理资源,减少手动释放遗漏。调试阶段可以在probe里逐步return,定位到具体哪一行触发 oops。
5. 从能跑到好用:驱动稳定性验证与版本适配技巧
驱动能加载、能出设备节点,只是第一步。真正上线前,得验证它在压力下不崩、在目标内核版本上能编、在国产 Linux 发行版上能跑。我一般会做三件事:压力测试、内核版本适配检查、以及用checkpatch过一遍代码风格。
压力测试最简单的方式是反复读写设备,同时用dmesg -w盯着日志。比如字符设备,写个脚本循环dd读写,跑半小时看有没有 oops 或内存泄漏。内存泄漏可以用slabtop看驱动相关的 slab 有没有持续增长。中断驱动的设备,用watch -n 1 cat /proc/interrupts看中断计数是否正常增长,有没有中断风暴。
内核版本适配是嵌入式 Linux 项目里最容易翻车的地方。同一份驱动,在 4.19 上能编,到 5.10 可能就报错,因为内核 API 在变。我的习惯是先在目标内核源码里grep驱动用到的每个核心 API,确认签名没变。比如platform_get_irq和platform_get_irq_byname在不同版本里行为有差异,gpio_request在新内核里推荐用gpiod_get替代。适配时不要硬改内核源码,优先改驱动本身,保持驱动和内核的边界清晰。
# 用内核自带的 checkpatch 检查驱动代码风格 /path/to/linux-source/scripts/checkpatch.pl --no-tree -f mydriver.c # 看驱动占用的内存情况 slabtop -o | grep mydriver # 监控中断计数变化 watch -n 1 'cat /proc/interrupts | grep my_sensor'逻辑说明:checkpatch.pl是内核社区的标准代码检查工具,--no-tree -f表示检查单个文件而不是整个源码树,能揪出空格、行宽、注释风格等问题,减少上游合入阻力。slabtop看内核对象缓存,如果驱动反复创建销毁对象但缓存不降,可能有泄漏。watch配合/proc/interrupts能直观看到中断是否在涨,涨得太快说明硬件在反复触发,得查硬件或中断标志。
提示:国产 Linux 发行版(如 Kylin V10、Debian 13)的内核版本和主线可能有差异,厂商会打补丁。适配前先
uname -r确认实际内核版本,再找对应的内核头文件包,不要拿主线源码硬套。
最后一个技巧:给驱动加module_param暴露关键参数,调试时不用重编内核就能改行为。比如把日志级别、超时时间做成模块参数,insmod mydriver.ko debug_level=3就能开详细日志。这个习惯帮我省了无数次重编内核的时间,也方便现场排查。驱动这行,能少编一次内核就少一次,希望帮到你。
本文还有配套的精品资源,点击获取