Linux内核,驱动开发,你准备好入坑了吗
最近在搞嵌入式开发的时候,总听人说“会写驱动才算真懂Linux”。一开始我觉得挺玄乎的,不就是控制个硬件嘛,写点代码就行。但真上手才发现,这玩意儿跟应用层编程完全不是一回事。光是环境搭建、内核编译、模块加载这些流程,就够折腾好几天了。
我看的是那本《Linux设备驱动开发(第2版)》,作者叫约翰·马迪厄,是个法国的嵌入式工程师。之前第一版2021年出来就挺火的,现在这本第二版把内容全更新了,用的内核版本是5.10,例子也能在树莓派、Jetson Nano这类板子上跑通。我拿的是二手的Raspberry Pi 4B,配了个小液晶屏和几个传感器,照着书一步步试,居然真的跑起来了。
刚开始学,最头疼的是模块怎么装进去。内核模块是.ko文件,得用insmod命令加载。但要是有依赖关系,光用insmod不行,得先跑depmod生成依赖索引,再用modprobe自动解决依赖问题。有一次我没搞清楚顺序,加载失败了还不知道为啥,最后看dmesg输出才找到问题。
字符设备驱动是我写的第一个完整驱动。主设备号、次设备号、注册设备号、初始化cdev结构体,这一套下来感觉像拼乐高。关键是要记住,注册了就得有对应的卸载函数,不然内核内存就泄漏了。书里强调了一点:module_init和module_exit这两个宏必须写,LICENSE声明也不能少,不然编译会警告。
后来学到设备树,更头大了。以前老式驱动把硬件信息全写死在代码里,现在都用设备树来描述。比如I2C挂了个温度传感器,就得在dts文件里写节点,指定地址、中断、兼容性字符串。驱动那边用of_match_table去匹配,才能正确加载。改一次设备树还得重新编译dtb,烧进板子,效率低但没办法。
平台驱动这块,我最迷的是probe机制。系统启动时,内核会遍历所有设备和驱动,一旦compatible字段对上了,就会调用驱动的probe函数。这个过程是自动的,不用手动触发。刚开始不明白为啥我的驱动不执行probe,查了半天发现是设备树里的compatible写错了,差一个字母都不行。
书中花了不少篇幅讲DMA和中断。DMA主要是为了减轻CPU负担,比如传一大段数据,让DMA控制器自己搬,CPU干别的事。实现起来要用到dma_request_chan、dmaengine_submit这些API,还要处理完成回调。中断分上下部,顶半部快进快出,底半部用tasklet或工作队列处理耗时操作。我试过按键中断,用了线程化中断,防止阻塞其他中断。
内存管理也挺关键。kmalloc适合小块连续内存,vmalloc可以申请大块非连续的。高端内存那一块讲得特别细,32位系统下物理内存超过896M的部分叫高端内存,访问方式不一样。虽然现在大多用64位系统了,但理解原理还是有用的。
GPIO部分让我意识到老接口已经被淘汰了。以前用gpio_request/gpio_free那一套整数编号的方式,现在推荐用gpiod_开头的描述符接口。用户空间也不建议直接操作/sys/class/gpio了,得用libgpiod库。书里给了一个LED驱动的例子,用gpiod_set_value_cansleep控制亮灭,简洁明了。
Regmap和IIO这些现代框架也在书里重点介绍了。Regmap帮你封装寄存器读写,带锁、带缓存,特别适合I2C/SPI设备。IIO是工业I/O子系统,处理ADC、加速度计这类模拟量采集设备。我试着写了个虚拟IIO驱动,能通过sysfs和IIO工具读到数据,挺有成就感的。
输入子系统也挺实用。键盘、触摸屏都走input框架,上报事件用input_report_key、input_sync这些函数。书里有个例子是模拟按键输入,加载模块后就像真的按了键一样,可以在终端看到输出。
调试主要靠printk和dmesg。printk打日志要加级别,比如KERN_INFO,不然可能看不到。更高级的可以用ftrace或者KGDB做源码级调试,但我还没搞明白,太复杂了。
总的来说,这本书把驱动开发从底层机制到实战全都串起来了。代码都是经过测试的,照着敲基本都能跑。最难的地方在于硬件环境不稳定,有时候不是代码问题,是线没接好或者电压不稳。
现在我自己搭了个开发环境,Ubuntu 20.04虚拟机,交叉编译工具链配好了,能编译内核,也能烧录。虽然过程中踩了不少坑,但每解决一个问题,对内核的理解就深一层。
学完这些,再去看看别人写的驱动代码,不再是一头雾水了。原来那些看似复杂的结构体和回调,都是有逻辑可循的。驱动开发确实硬核,但也确实值得投入。
