做Linux设备驱动开发这行,入门第一感觉往往不是“难”,而是“乱”。同是写个hello world,应用程序三行代码就能跑,驱动模块却要纠结内核版本、编译器、模块签名、设备号,还没见到效果就先被各种报错劝退。我刚入行那阵,光是让一个LED在开发板上亮起来,就踩了整整两天的坑,最后发现只是设备树里GPIO编号写岔了。
这篇文章不打算把Linux设备驱动开发讲成一本百科全书,而是按我从零到一、从踩坑到熟练的真实路径,把驱动开发里最核心、最绕不开的东西拆开讲清楚:驱动的框架是什么,字符设备怎么写,platform驱动怎么和设备树配合,遇到常见的加载、运行、排查问题怎么处理。内容面向刚接触嵌入式Linux的工程师、准备转驱动方向的开发者,也适合那些应用层写多了、想往底层探一探的朋友。就算你暂时没有开发板,用一台普通的x86 Linux机器跑模块、做实验,这套思路也完全适用。
1. 入门之前,先把驱动开发的整体思路理清楚
1.1 驱动到底是什么
很多人一听到“设备驱动”就本能地发怵,觉得是特别底层、特别神秘的东西。实际上下个不那么严谨但特别好用的定义:驱动就是内核里的一段软件,它一边把硬件能力包装成规范接口,一边把内核下发的指令翻译成硬件能听懂的寄存器操作。
以最简单的LED为例。应用层想点灯,它可以不去管这个LED是接在GPIO上,还是通过I2C接了一个扩展芯片,它只需要向某个设备文件写入一个“1”,剩下的工作全部由驱动完成。驱动要做的,是拿到这个“1”,找到对应的引脚,计算寄存器地址,把电平拉高。整个过程就是软件和硬件之间的“翻译”。
这里有一个初学者最容易迷糊的概念:用户态和内核态。CPU为了安全,把运行环境分成了两个权限等级。应用跑在用户态,权限很有限,不能直接操作硬件地址;内核跑在内核态,有完整的硬件访问权。驱动的代码编译之后是内核模块,它以内核态身份运行,所以才有资格去读寄存器、申请中断、访问DMA。同时,内核又通过系统调用的方式给用户态开了一扇门,让应用可以间接使用硬件能力。没有这层隔离,任何程序都能乱碰硬件,系统早崩了。
1.2 驱动的三大类和应用场景
按设备类型划分,驱动一般分为三类:
- 字符设备:按字节流读写,比如串口、GPIO、按键、ADC。最常见的驱动类型,绝大多数入门教程都从这开始。
- 块设备:以块为单位读写,比如硬盘、SD卡、eMMC,核心指标是吞吐量和IO调度。
- 网络设备:负责数据包的收发,比如以太网卡、WiFi模块,它不走设备文件那套接口,走的是内核网络协议栈。
在实际工作中,嵌入式Linux驱动开发接触最多的就是字符设备,尤其是在做传感器、显示屏、摄像头、电机控制的场景下,一板子下来能挂十几个字符设备。这种驱动不像块设备那么复杂,但五脏俱全,适合作为学习的主线。
1.3 总线-设备-驱动模型:驱动开发的地基
真正写驱动之前,有一个绕不开的概念:内核里的“总线-设备-驱动”模型。你打开/sys/bus目录,能看到平台总线、SPI总线、I2C总线、PCI总线、USB总线等,每一种总线都管理着一批设备和一批驱动。
这里的关键是“匹配”机制。设备侧描述“我是什么硬件”,驱动侧声明“我支持什么硬件”,总线负责在两者之间牵线搭桥,匹配上了就调用驱动的probe函数。也就是说,驱动和硬件不是靠include头文件绑定到一起的,而是由内核在运行时动态匹配出来的。
最常见的platform总线,也叫平台总线,是一种虚拟总线,专门用于整理那些不挂在PCI、USB、I2C这类实体总线上的设备,比如芯片内部集成的GPIO控制器、UART、DMA控制器等。以前大家习惯在BSP板级文件里硬编码设备资源,内核一升级就得跟着改,痛苦不堪。后来引入了设备树,把硬件描述从内核代码里剥离出来,变成一份独立的、可被引导加载程序传递给内核的数据结构。驱动不再关心设备具体在哪块板子上,只关心设备树里有没有匹配的节点。这套解耦思路,是整个现代Linux驱动开发的基石。
2. 字符设备驱动的核心拆解:别被术语吓住
2.1 字符设备的基本结构
一个字符设备驱动,就算功能再简单,也绕不开这几个东西:设备号、file_operations结构体、字符设备对象、设备节点。
先看设备号。Linux用主设备号和次设备号来标识一个设备。主设备号表示设备类型,对应某个驱动;次设备号表示同一个驱动下的不同实例。打个比方,主设备号相当于公司名,次设备号相当于工号,光有公司名定位不到具体的人,必须两个配合。
再看file_operations。这个结构体就是驱动对外服务的“菜单”。应用调用open、read、write、ioctl、release时,虚拟文件系统VFS会根据设备号找到对应的file_operations,然后调用你实现的函数。也就是说,你在驱动里写的read函数,并不是自己决定什么时候执行的,而是用户态程序调用了read()系统调用后,由内核按图索骥调过来的。
最后是设备节点,也就是/dev/目录下那个文件。它本身不包含数据,只是一个入口,记录了设备号。应用打开它,等于通过VFS找到了驱动。
2.2 一个最朴素的字符设备要写哪些东西
拿最精简的代码举例,一个miscdevice(杂项设备)字符设备可以写成这样:
#include <linux/module.h> #include <linux/miscdevice.h> #include <linux/fs.h> static ssize_t demo_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { char data[] = "hello driver\n"; if (*ppos > 0) return 0; if (copy_to_user(buf, data, sizeof(data))) return -EFAULT; *ppos += sizeof(data); return sizeof(data); } static const struct file_operations demo_fops = { .owner = THIS_MODULE, .read = demo_read, }; static struct miscdevice demo_dev = { .minor = MISC_DYNAMIC_MINOR, .name = "demo", .fops = &demo_fops, }; module_misc_device(demo_dev); MODULE_LICENSE("GPL");这段代码虽然短,但信息量不小。demo_read是真正干活的函数,用户态执行read(fd, buf, sizeof(buf))时,数据通过copy_to_user从内核缓冲区拷贝到用户缓冲区。这里有一个新手很容易踩的坑:你以为可以直接把内核里的一个指针返回给用户态,或者直接解引用用户态传进来的指针。不行,内核态和用户态的内存是隔离的,必须用内核提供的copy_to_user和copy_from_user来搬运数据。这也是驱动开发和普通应用开发最大的思维差异之一。
module_misc_device(demo_dev)是一个简化宏,它帮你完成了module_init和module_exit的注册。如果用misc子系统,次设备号设置为MISC_DYNAMIC_MINOR,内核会自动分配一个空闲的次设备号,省去自己管理设备号的麻烦。设备节点在驱动加载后由misc子系统自动创建为/dev/demo,不需要你手动mknod。
编译这个模块,只需要一个简单的Makefile:
obj-m := demo.o KDIR := /lib/modules/$(shell uname -r)/build all: make -C $(KDIR) M=$(PWD) modules clean: make -C $(KDIR) M=$(PWD) clean然后执行make,就会生成demo.ko。insmod demo.ko加载,再看/dev/demo是否存在,用cat /dev/demo就能读到那行“hello driver”。
2.3 为什么现在都推荐用miscdevice
可能有人会问:教材里不都是先register_chrdev_region、再cdev_add、再class_create、再device_create吗?这套流程没错,但步骤多,容易错。misc设备相当于内核给你做好的一个封装:它本质还是一个字符设备,但内核已经帮你处理了设备号分配、设备节点创建这些样板活。因为misc设备有固定的一组主设备号,内核内部已经注册好了对应的字符设备和class,你只需要提供一个名字和file_operations就够了。
当年我刚开始学的时候,照着老教程一路register_chrdev、cdev_init、cdev_add、device_create写下来,每次都在设备节点创建那块出问题,要么忘了创建class,要么device_create参数写错。后来才知道,大部分教学场景用miscdevice完全够用,代码量少一半,还不容易出错。当然,如果设备特别多、需要自管理主设备号和多个次设备号,再走完整的那套流程也不迟。
2.4 write/read在驱动里做的事
read和write在驱动里干的事情,总结起来就三件:参数检查、数据传输、状态更新。
参数检查包括:用户传入的缓冲区指针是否有效、读取位置是否越界、请求的字节数是否合理解释。比如demo_read里检查*ppos > 0就直接返回0,表示没有更多数据了,这是read系统调的约定:返回0表示EOF。
数据传输是核心。写操作通常需要从用户态拿数据进来,驱动拿到这些数据后可以存入缓冲区、配置寄存器、设置某个硬件状态。读操作则是把硬件状态、寄存器值、缓冲区内容拷贝给用户态。数据量小就用copy_to_user/copy_from_user,数据量大、性能要求高的场景会用到mmap、DMA,但那已经是进阶玩法了。
状态更新稍微隐蔽一点,但同样重要。很多驱动在read之后会更新文件偏移*ppos,在write之后会记录累计写入字节数,在open的时候递增模块引用计数,在release的时候递减。这些细节直接决定了设备的行为是否符合应用层的预期。
3. 实操:从零写一个platform驱动的LED例子
3.1 需求定义和方案选型
纸上谈兵聊够了,下面来一个真实的例子:在嵌入式板子上用设备树描述一个GPIO LED,然后写一个platform驱动把它点亮。
为什么用platform驱动而不是直接写个字符设备?因为这里涉及一个核心原则:驱动只负责“逻辑”,不负责“硬件资源写死”。LED接在哪个GPIO、有没有上拉、默认电平是高有效还是低有效,这些都属于硬件描述信息,应该放在设备树里,由驱动运行时去读取。如果把这些写死在代码里,换一块板子、换一个引脚,就得改代码重新编译,维护成本极高。
GPIO API的选型也有讲究。老的gpio_request/gpio_direction_output这套API在如今的内核里还能用,但已经属于遗留接口。我建议新驱动一律使用gpiod_系列API,也就是GPIO描述符接口。它更安全、更灵活,而且天然支持设备树里gpios属性里的active-low标志。什么意思呢?比如你接的LED是低电平点亮,设备树里标了GPIO_ACTIVE_LOW,驱动用gpiod_set_value(desc, 1)就可以直接点亮它,内核会自动帮你反转电平,不用在驱动里自己判断。
3.2 设备树配置
设备树源文件(dts)里,为我们的LED设备添加一个节点:
/ { myled { compatible = "myvendor,myled"; gpios = <&gpio4 17 GPIO_ACTIVE_HIGH>; status = "okay"; }; };逐行解释一下。compatible是设备树匹配驱动的关键字符串,格式通常是“厂商,设备名”。平台总线匹配时,内核会拿设备树节点的compatible和驱动of_match_table里的compatible逐一比较,一致就算匹配成功。
gpios属性定义了引脚。这里<&gpio4 17 GPIO_ACTIVE_HIGH>的意思是这个LED挂在gpio4控制器下编号17的引腿上,高电平有效。设备树里GPIO编号不是0-31的物理引脚号,而是控制器内部编号,具体对应哪个物理引脚,要看芯片手册里的GPIO bank定义。这一步非常容易错,我当年就吃过亏,设备树里写了一个看起来合理的编号,结果亮的是旁边另一颗灯。
status = "okay"是显式声明节点使能。这个字段可以方便地在不同板型上关闭或打开某个设备,而不用删除节点本身。
改完dts之后,需要重新编译设备树并替换原有的dtb文件。这一步很多人会忘,导致改了设备树却没有效果。编译命令一般是:
make dtbs然后把生成的新dtb拷贝到启动分区覆盖旧文件,重启后执行ls /proc/device-tree/myled确认节点已经存在。
3.3 驱动代码实现
看过设备树,再看驱动代码就顺理成章了:
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/gpio/consumer.h> #include <linux/err.h> static struct gpio_desc *led_gpio; static int myled_probe(struct platform_device *pdev) { led_gpio = devm_gpiod_get(&pdev->dev, NULL, GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) { dev_err(&pdev->dev, "failed to get gpio\n"); return PTR_ERR(led_gpio); } gpiod_set_value(led_gpio, 1); dev_info(&pdev->dev, "led on\n"); return 0; } static int myled_remove(struct platform_device *pdev) { if (led_gpio) gpiod_set_value(led_gpio, 0); return 0; } static const struct of_device_id myled_of_match[] = { { .compatible = "myvendor,myled" }, { } }; MODULE_DEVICE_TABLE(of, myled_of_match); static struct platform_driver myled_driver = { .probe = myled_probe, .remove = myled_remove, .driver = { .name = "myled", .of_match_table = myled_of_match, }, }; module_platform_driver(myled_driver); MODULE_LICENSE("GPL");probe函数是驱动的核心入口。它的执行时机是:platform总线在注册驱动时,遍历已注册的所有设备,一旦发现某个设备的compatible和我们驱动的of_match_table匹配,就会调用probe。probe里做的事情通常是:获取硬件资源、初始化寄存器、注册中断、创建设备文件等。
这里用的devm_gpiod_get是带资源管理的API。它的好处是,不需要手动释放GPIO——当设备与驱动分离时,内核会自动释放分配的资源。类似的还有devm_kzalloc、devm_request_irq等等,这类devm前缀的API在驱动开发里强烈推荐优先使用,能减少很多内存泄漏和资源泄漏问题。
module_platform_driver(myled_driver)展开后包含module_init和module_exit,分别注册和注销这个platform驱动。
3.4 编译、加载和验证
Makefile可以直接沿用前面那个模板,只需要把模块名改一下:
obj-m := myled.o KDIR := /lib/modules/$(shell uname -r)/build all: make -C $(KDIR) M=$(PWD) modules clean: make -C $(KDIR) M=$(PWD) clean编译完拿到myled.ko,拷贝到目标板子上,执行insmod myled.ko或者modprobe myled。重点提醒一下:modprobe会做依赖分析和自动加载,但它只会搜索标准模块目录,如果你把ko随手丢在/tmp里,还是得用insmod。开发阶段我习惯用insmod,排查问题更直接。
加载之后,第一时间做三件事:
dmesg | tail -20 ls /sys/bus/platform/drivers/myled/ cat /sys/bus/platform/devices/myled/... 2>/dev/nulldmesg里应该能看到“led on”的打印,说明probe成功了。同时LED应该已经亮起来。如果没有看到,优先检查设备树节点,用find /proc/device-tree -name "myled"确认节点在不在系统里。
3.5 应用层怎么用这个设备
如果只是点亮LED,驱动的probe函数已经做了。但实际项目里,驱动往往要暴露一些操作接口给应用层。
两种常见做法:一是走LED子系统,注册到/sys/class/leds/目录,应用直接echo操作brightness文件;二是自己实现file_operations,创建/dev/myled设备节点,提供read和write。
走LED子系统的优势是内核已经帮你处理好了亮灭、闪烁、亮度等级,甚至支持triggers(触发源),比如心跳灯、网络活动灯。对于一个标准LED设备,我更推荐直接做成一个LED class设备,省心。实现一个自定义字符设备则适合那些需要对LED做复杂控制的场景,比如需要知道当前亮了多少小时,或者要同时控制一组灯带。
一段简单的应用测试程序,打开设备节点并控制它:
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main(int argc, char *argv[]) { int fd; char cmd[16]; if (argc != 2) { fprintf(stderr, "usage: %s <on|off>\n", argv[0]); return -1; } fd = open("/dev/myled", O_WRONLY); if (fd < 0) { perror("open"); return -1; } if (strcmp(argv[1], "on") == 0) strcpy(cmd, "1"); else strcpy(cmd, "0"); write(fd, cmd, 1); close(fd); return 0; }这只是个测试程序,工业项目里建议用ioctl传控制命令,或者直接使用标准LED子系统接口。
4. 常见问题与排查技巧实录
4.1 insmod报错:Invalid module format
这个问题在开发初期出现频率极高,基本原因只有一个:编译模块用的内核源码、工具链和目标机器上的内核不一致。
常见的坑包括:本地是Ubuntu发行版内核,你去下载了一个与当前内核版本号完全一样、但config不同的内核源码来编模块;又或者模块是用主机自带gcc编的,而目标板上内核是用交叉编译器编的,两者版本不兼容。排查时先看完整报错:
insmod: ERROR: could not insert module myled.ko: Invalid module format dmesg | tail -5如果看到version magic相关的日志,说明版本字符串不匹配。解决办法就是严格使用目标机同版本的内核源码和工具链编译模块。很多新人在虚拟机里学习,建议uname -r看一眼,然后用apt install linux-headers-$(uname -r)安装匹配的头文件,不要自己随便下源码编。
4.2 设备文件出现了,但open失败
open失败通常返回ENXIO(没有这个设备)或ENODEV(设备不存在)。这里分两种情况。
如果设备节点是手动mknod创建的,主次设备号和驱动注册的不一致,就会这样。用cat /proc/devices看驱动注册的设备号,再ls -l /dev/xxx比对节点的设备号。
如果设备树驱动的probe没有执行,即使你把设备节点建好了,内核也找不到对应的设备实例,open同样失败。这时候回看dmesg,查一下驱动有没有注册成功、节点compatible是否匹配。
4.3 printk看不到输出
驱动里printk的print是一个很自然的调试手段,但它不像用户态的printf那样一打印就出现在终端上。内核消息有日志级别,只有级别高于控制台设定的日志级别时,才会打印到串口或终端。
实用建议:开发阶段直接写pr_info或dev_info,比裸写printk更规范。dev_info会带上设备名和驱动名,定位问题一目了然。如果终端上看不到任何日志,先cat /proc/sys/kernel/printk查看控制台日志级别,需要时可以临时调整:
echo 8 > /proc/sys/kernel/printk另外,用dmesg总能看到所有日志,不管控制台级别如何。
4.4 设备树改了没生效
设备树修改不生效是嵌入式开发里最常见、也最让人抓狂的问题之一。复盘下来,原因基本逃不出这几个:dtb没重新编译、dtb没写到正确分区、uboot加载的还是旧dtb。
排查路径很明确:先ls /proc/device-tree/myled确认节点在不在;再cat /proc/device-tree/myled/compatible看属性值对不对;如果节点不在,大概率是新的dtb没被加载。另外别忘了设备树会被uboot覆盖或追加,有些板子的uboot会从固定分区读dtb,和内核镜像分开存放,改完dtb单独重烧那个分区即可。
还有一个容易忽略的问题:dts编译时include了dtsi头文件,如果改动写在了一个最终没被包含的dtsi里,同样不会生效。先grep确认你的节点确实被包含到了最终的dts中。
4.5 访问寄存器导致系统崩溃
驱动按内核态运行,权限高,但也意味着一旦出错就是灾难。最常见的崩溃场景是:驱动里直接通过某个指针去访问物理地址,却没有做地址映射。
芯片内部的寄存器物理地址,必须通过ioremap或devm_ioremap_resource映射成内核虚拟地址后才能访问。直接拿一个裸的物理地址去解引用,轻则触发kernel NULL pointer dereference,重则整个系统死机或者触发看门狗复位。
另外还需要注意内存屏障。驱动里的寄存器读写,编译器可能为了优化乱序,导致你写的顺序在硬件层面完全乱掉。访问外设寄存器时,使用readl/writel这类带内存屏障的API,不要直接操作指针。
4.6 中断申请失败
中断是驱动里比GPIO更容易踩坑的地方。申请中断用request_irq或devm_request_irq,失败最常见的原因是中断号错误和中断冲突。
新旧内核的中断号逻辑有差异。旧方式通过gpio_to_irq拿gpio对应的中断号,新方式推荐直接用gpiod_to_irq(desc),从gpio描述符直接拿中断号,避免手工转换。另一个常见问题是,一个引脚同时被两段代码申请了中断,或者寄存器配置成了别的复用功能,导致中断不触发或直接冲突。
排查时先确认中断有没有注册成功,再看/proc/interrupts里有没有对应的设备中断计数在增长。如果计数不涨,多半是硬件触发条件、电平极性配置的问题。
4.7 常用的调试手段
驱动开发没有万能的调试工具,关键是形成自己的排查套路。我自己的习惯,从外到内分几步。
看日志。dmesg永远是第一现场,开发时尽量把probe、remove、操作的日志打完整,宁可多打一点,产品发布前再收。看文件节点。/sys/bus/platform/drivers/myled/目录下能看到驱动绑定了哪些设备,/sys/bus/platform/devices/能看到所有平台设备和它们匹配到的驱动。看内核状态。/proc/interrupts查中断计数,/proc/device-tree查设备树实际生效内容,/proc/modules查模块是否加载。
有条件的话,加一个GPIO调试勾子,写一个简单脚本循环翻转引脚,配合示波器或万用表量电平,能快速把问题定位到硬件还是软件。嵌入式开发里,很多时候不是代码不对,而是上拉没焊、引脚复用错了、线接反了,手边有个表比反复改代码快得多。
5. 关于性能调优和框架的一些经验
驱动开发做到后面,功能通了只是及格,性能调优才是真正拉开差距的地方。
5.1 中断下半部、线程化中断和延迟工作
中断处理程序的要求是“短平快”,因为它在关闭本地中断的上下文里执行,拖得越久,系统的实时性越差。实际项目中,我习惯把中断处理分成两段:上半部只做和硬件强相关的事,比如读取中断状态寄存器、清中断标志;下半部再做耗时操作,比如数据解析、唤醒等待队列。
下半部的选择,老内核常用tasklet,但现在新代码里tasklet已经逐渐被边缘化,推荐用workqueue或者直接线程化中断request_threaded_irq。我后来写的驱动基本都是线程化中断,因为线程里有独立栈、可以睡眠、可以调用各种阻塞API,写起来心智负担小很多。
5.2 休眠与唤醒、阻塞与非阻塞
驱动和应用不同,很多情况下要“等”。比如一个按键驱动,应用调用read想读取键值,但用户还没按键,驱动总不能一直忙等浪费CPU。正确做法是把进程投入到等待队列里睡眠,等中断到来时再唤醒它。
内核提供了一套非常成熟的等待队列机制,wait_event_interruptible加上wake_up_interruptible是标准用法。需要注意的是,使用中断版本,也就是带interruptible后缀的接口时,必须检查返回值,因为进程可能被信号打断。返回值是-ERESTARTSYS时,需要恢复到待机状态,让应用重新发起系统调用。
非阻塞模式下,如果设备没有数据,驱动应该直接返回-EAGAIN,而不是睡眠。
5.3 锁的选择:并发不是玄学,是基本功
驱动运行在内核态,并发访问是绝对绕不开的问题。同一个驱动可能被多个进程同时打开,读操作发生的同时中断服务程序正在写状态,CPU多核环境下不同核同时进入同一个驱动函数。没有锁,就是数据竞争。
驱动里常用的锁按使用场景分:自旋锁用于不能睡眠的短临界区,比如中断上下文里保护共享标志位;互斥锁用于可能睡眠的长临界区,比如缓冲区满时等待写入条件。经验法则是:能睡就睡,能不加锁就不加锁,锁的粒度越小越好。以前我写过一个大临界区,整个probe和读写操作全加了一把mutex,结果一有IO操作其他进程就卡到不行,后来把锁拆细,性能立刻上来了。
用锁还有一个习惯,命名和注释要写清楚,说明这段锁保护的是谁。不然两个月后回看代码,想死的心都有。
5.4 系统裁剪和驱动裁剪的关系
很多人会问,系统优化和驱动开发有什么关系?关系很大。嵌入式产品里,flash和内存都是稀缺资源。一个没有裁剪的发行版内核,光模块就占几百MB,跑在工业设备上既不现实也不安全。
裁剪思路通常分两层。一层是内核本身裁剪,通过menuconfig关掉用不到的子系统、文件系统格式、驱动模块。另一层是驱动模块裁剪,把调试代码用内核配置项包起来,比如#ifdef CONFIG_DEBUG_FS,产品发布时关掉。还有一层是BSP层裁剪,删除用不到的dts节点、外设驱动,让系统启动更快、内存占用更小。
这些工作看起来很杂,但做嵌入式驱动开发迟早会碰到。懂一些内核裁剪和优化技巧,能让你在方案评审、交付的时候更从容。
6. 最后的几句实在话
从零开始学Linux设备驱动开发,说难是难,说不难也不难。门槛主要在环境搭建和术语理解上,一旦把“字符设备、平台驱动、设备树、GPIO子系统”这一串概念串起来,后面学I2C、SPI、USB、PCIe,都是同一个套路换皮而已。
我见过不少人学驱动,一上来就找《Linux设备驱动开发详解》从头啃,啃到第三章就放弃了。我的建议恰恰相反,先照着本文这种最小例子,把模块编译、加载、卸载、查看日志这套流程跑通,再回头看书里的理论,理解速度会快很多。内核源码是最好的参考书,drivers/gpio、drivers/leds、drivers/misc这些目录下全是短小精悍的真实驱动,值得反复读。
还有个小技巧送给大家:如果遇到一个内核API不会用,直接在源码里搜索引用它的驱动,比翻文档快得多。内核开发文档有时更新不及时,但真实驱动代码永远不会骗人。
这条路子走下去,你会发现驱动开发其实很有意思,它处于软件和硬件交汇的那个点,既要懂系统编程,也要会看原理图,是一个需要长期积累但又非常有成就感的领域。