手把手教你学Linux设备驱动开发:从字符设备到设备树的实战指南

发布时间:2026/9/7 2:39:26
手把手教你学Linux设备驱动开发:从字符设备到设备树的实战指南 如果你在嵌入式Linux这条路上摸爬滚打过大概率有过抱着几千页的《Linux Device Drivers》第三版啃到怀疑人生的经历。那本书确实是经典但终究老了——内核版本还停留在2.6设备树没普及platform框架还没长成今天这副样子更别提你在实际开发里天天要面对的设备树覆盖、中断线程化、gpiod新接口这些玩意儿了。所以当《手把手教你学Linux设备驱动开发》这本书说要出版的时候我第一反应是终于有一本能跟上时代的驱动开发书了。我当时先拿到书稿翻了几个章节直接的感觉是这书写得非常“工程师”——不讲虚的每一章都是冲着让你能上板子写代码去的。它不做那种百度百科式的面面俱到而是把你在真实项目里最高频用到的那套知识讲透彻。从字符设备框架到platform驱动从设备树语法到中断上下半部再往后走还有内核并发管理、内存分配、IO访问、块设备、网络设备、音频和USB这些重头戏覆盖的宽度和深度配得上“硬核宝典”这几个字。这篇文我不打算写读后感而是把这几年在实际驱动开发中踩过的一些关键节点、这本书里对应的讲解思路、以及自己总结的一套学习路线结合起来给你一份可以直接参照的驱动开发实践地图。1. 为什么Linux驱动开发值得专门写一本“手把手”的书Linux驱动是所有嵌入式Linux开发者的分水岭。会写应用的人一堆但能把一个外设从设备树节点到驱动probe再到用户态访问整条链路完整跑通的人往往就是项目里最不好替代的那个。1.1 驱动开发在嵌入式Linux中的位置一个嵌入式产品的软件栈大致是引导加载程序u-boot、Linux内核、根文件系统、用户态应用。驱动开发属于内核这一层的关键部分它的职责就是让硬件和其他模块能够被系统统一管理、被应用层正常调用。驱动写的质量高低直接决定产品的稳定性、实时性和功耗表现。比如同样是控制一个GPIO输出用户态直接操作寄存器的方式虽然快但无法复用、也容易和内核其他机制冲突而正经写成一个小驱动通过标准接口暴露给应用层既能被系统统一管理也方便测试和移植。这就是驱动存在的核心意义。这本书把起点放在“为什么需要驱动”这个最朴素的问题上从Linux设备模型讲起一步步带你搞清楚字符设备、块设备、网络设备之间的区别以及内核空间和用户空间的边界是怎么划分的。这种叙事顺序很重要它会让新手从一开始就知道自己写的代码最终运行在哪个层级上。1.2 市面上现有Linux驱动资料的痛点聊驱动开发资料绕不开几个现状第一内核源码好但是读不懂。read-C-h-f.c一堆结构体套结构体没有长期积累很难建立起直观认知。第二《Linux Device Drivers》经典但版本太老。网上大量文章还是拿它当依据结果编译就过不了。第三视频教程往往只讲一个开发板换个平台就没法迁移。实际上驱动开发的通用方法论远比记住某个板子的寄存器地址更重要。另外还有一个很现实的问题驱动开发的问题场景高度依赖硬件。很多博主的代码是基于自己手里那一套板子写的你没他的环境就跑不起来。这事我在调试音频codec驱动时不止一次遇到过——人家的i2c地址和寄存器配置是基于他们硬件原理图的直接抄过来根本不出声。所以“手把手”“可复现”才那么重要——不是给你贴一段漂亮代码让你膜拜而是从开发环境搭建、内核编译、模块装载到驱动框架填充每一步都能照着走中途出了问题还能照着排。2. 读懂这本书的学习路线设计《手把手教你学Linux设备驱动开发》这套书的结构设计核心逻辑是从“怎么学”倒推“怎么写”。它不是按内核子系统做字典式罗列——而是把驱动开发工程师从入门到接手真实项目所需的技能拆成了一条明确的学习渐进线。2.1 从字符设备设备框架切入是学习驱动的正路多数Linux驱动的入门都是从字符设备开始的。字符设备的抽象逻辑最直观设备就是一个有序字节流你在驱动里实现open、read、write、ioctl这些方法内核通过struct file_operations把你的实现和用户态的open、read、write调用串起来。书中选择从这个点切入我认为有深层的教学考量。字符设备驱动的全貌里涉及了设备号、file_operations结构体、inode结构、cdev注册、自动创建设备节点udev等机制。这套逻辑一旦打通你就理解了Linux里一切驱动共有的底座设备模型加操作集。后面再接触块设备、网络设备、平台驱动时会发现很多概念是一通百通。我在实际带人的过程中发现能独立写明白一个完整字符设备驱动包含ioctl命令构造、copy_to_user / copy_from_user正确使用、并发访问保护的人后续切入复杂驱动会非常顺畅反之如果上来就啃设备树然后试图跳过字符设备直接调寄存器会把很多机制层面的东西学得支离破碎。2.2 书中核心章节的结构安排是怎样的简单拆一下这套书的知识递进环境准备与内核编译配置好开发环境学会编译内核、编译模块、装载卸载。字符设备驱动框架了解设备号、文件操作集、设备节点、class和device创建。platform驱动和设备树理解“驱动-设备分离”的思想从硬编码转向设备树描述。并发与同步内核中锁机制的本质与用法包括自旋锁、信号量、互斥锁以及它们分别在什么场景下该选谁。中断与底半部机制中断申请与释放、中断上下文约束、tasklet、工作队列和线程化中断。内核内存管理和IO访问内存分配的各种标志位含义、MMIO访问过程以及DMA机制的基础概念。高级子系统块设备、网络设备、音频、USB、I2C/SPI等总线驱动的框架。这一路下来呈现的是驱动开发能力的“生长路径”而不是内核源码树的目录搬运。2.3 与直接阅读内核文档相比的差异化优势很多内核开发者会说“阅读官方文档和源码本身才是正道”这个说法对但前提是你已经有相当的基础。而《手册》所承载的教学任务是降低这个门槛。举个例子你直接去看Documentation/devicetree/bindings下的txt或者yaml文件知道某个compatible对应什么寄存器布局但你不知道整个设备树是怎么被打包成二进制传递给内核的、不知道of_*系列API背后到底操作了什么。这种差距就是为什么需要一本好教材走一遍“演示说明代码排错”的完整路径。书里每章都会有实操项目比如基于常见开发板去注册一个按键设备和LED设备从设备的设备树节点编写到驱动的probe回调再到用户态通过sysfs或者字符设备接口去操作。这种流程跑上三遍对设备模型的理解会远超只看源码。3. 驱动开发的核心知识点详解接下来聊聊我在实际代码中反复用到、也是这本书重点着墨的几个核心模块。这些知识点不分学习先后但每一个都能单独拎出来讲几个小时。3.1 字符设备驱动的完整框架一个基础字符设备驱动的骨架大致是这样分配主设备号动态分配或静态指定。初始化cdev并将它加入到内核设备模型。创建device class和device让/dev下自动生成节点。在file_operations中实现read、write、ioctl、open、release方法。在退出函数中完成清理。几个特别容易出问题的地方动态设备号分配时用alloc_chrdev_region获得的主设备号每次启动可能不同。设备节点在/dev下的存在依赖udev或者mdev如果用户态还没看到节点检查device_create是否被调用成功。而我自己最常犯的失误是file_operations没有用.module THIS_MODULE导致模块卸载时还在被引用直接“kernel NULL pointer dereference”。具体代码骨架可以看下面这段static int demo_open(struct inode *inode, struct file *filp) { return 0; } static ssize_t demo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { char kbuf[64]; size_t len; /* 模拟从设备读取数据 */ snprintf(kbuf, sizeof(kbuf), hello from demo driver\n); len strlen(kbuf); if (copy_to_user(buf, kbuf, len)) return -EFAULT; return len; } static ssize_t demo_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { char kbuf[128]; if (count sizeof(kbuf)) return -EINVAL; if (copy_from_user(kbuf, buf, count)) return -EFAULT; kbuf[count] \0; pr_info(demo write: %s\n, kbuf); return count; } static const struct file_operations demo_fops { .owner THIS_MODULE, .open demo_open, .read demo_read, .write demo_write, };上面这段代码看着简单但在面试或者实际review里考的就是你知不知道为什么要用copy_to_user和copy_from_user而不是直接memcpy。因为内核空间不能随随便便解引用用户空间指针那会导致缺页异常以及安全问题。这也属于驱动开发里基础的“空中楼阁”概念理解之后对很多机制都会茅塞顿开。3.2 设备树与platform驱动机制现在的ARM Linux世界里设备树是硬件描述的标配。它的本质是用一种文本结构描述“这个板子上接了哪些外设、每个外设的寄存器地址和中断号是什么”。platform驱动则是Linux设备模型里最关键的一种抽象。它的特点是驱动和设备分离驱动只描述“怎么操作这种设备”设备节点描述“这个硬件在哪里”内核在设备树中匹配到compatible字段后自动调用驱动里的probe函数。我一开始非常抵触设备树觉得它就是给自己找事——改个引脚配置还得查各种bindings文档。后来写了一个需要同时适配好几块板卡的驱动后才体会它的价值通过设备树属性而不是硬编码才能让同一套驱动代码无痛跑在不同的主控上。一个简单的设备树节点例子gpio_led { compatible vendor,gpio-led; gpios gpio1 5 GPIO_ACTIVE_HIGH; label user-led; };对应驱动中static const struct of_device_id led_of_match[] { { .compatible vendor,gpio-led }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match); static int led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct gpio_desc *led_gpio; led_gpio devm_gpiod_get(dev, NULL, GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) return PTR_ERR(led_gpio); /* 触发一个点亮操作 */ gpiod_set_value(led_gpio, 1); return 0; } static struct platform_driver led_driver { .probe led_probe, .driver { .name gpio_led, .of_match_table led_of_match, }, }; module_platform_driver(led_driver);这里用到的devm_gpiod_get是带资源管理的GPIO接口好处是驱动probe失败或者remove时内核自动帮你释放GPIO。这种devm_*家族的API在现代化的驱动里几乎是标配能省掉大量手动清理代码也可以降低资源泄漏的风险。3.3 中断下半部tasklet、工作队列和线程化中断中断处理程序运行在中断上下文中它的约束有两头一是不能被阻塞二是执行时间越短越好。可实际项目的硬件处理往往很耗时比如一个触摸屏控制器上报数据后你要启动I2C读取坐标而I2C传输本身是需要等待的耗时的操作。这就引出了中断下半部的设计。三种经典机制各有适用场景tasklet运行在软中断上下文中不能睡眠适合耗时极短且不能被延迟的任务工作队列运行在进程上下文可以睡眠适合去做I2C传输、操作闪存这类阻塞型工作线程化中断则将整个中断处理作为内核线程运行编写逻辑上最直观在当前内核里使用也更普遍。这本书里对下半部的讲解应该是站在“开发中怎么选型”的角度去写的。不看使用场景谈机制没有意义。我自己的选型经验是能用threaded irq就用threaded irq代码最简单如果中断频率极高且任务量小再用tasklet或者直接靠硬中断处理需要睡眠处理就一定别硬扛。在驱动中注册线程化中断很简单ret devm_request_threaded_irq(dev, irq, NULL, touch_irq_thread, IRQF_TRIGGER_RISING, touch, touch_data); if (ret) return ret;thread_fn如果返回IRQ_WAKE_THREAD内核就会唤醒对应的内核线程在处理函数中可以做耗时的I2C读取等。3.4 并发控制与锁的选择驱动开发新手最怕看见的两类崩溃内核崩溃和莫名其妙的数据错乱。后者大概率是并发问题。Linux内核的并发场景很多多核CPU同时访问、中断打断普通进程、抢占内核切换任务等。锁选择的核心逻辑就一条临界区能不能睡眠。能睡眠就用互斥锁mutex或信号量不能睡眠就用自旋锁spinlock。自旋锁在获取不到时会在原地空转等待保护的是极短临界区。你在tasklet、硬中断上下文里想用互斥锁是不可能的那会直接睡眠然后触发BUG。开发中常见的坑是死锁和重复加锁。用mutex时进入循环调用或者被中断再次调用时非常容易把自己锁死。这个问题的排查往往很难受因为系统卡住了却没日志输出。所以连内核自己也提供了lockdep机制开启CONFIG_PROVE_LOCKING后能在早期就把潜在的死锁风险暴露出来实测下来这个配置在内核调试阶段非常值得打开。看一段自旋锁的使用示意struct demo_dev { spinlock_t lock; unsigned long flags; }; static void demo_write_reg(struct demo_dev *dev, u32 reg, u32 val) { unsigned long irq_flags; spin_lock_irqsave(dev-lock, irq_flags); writel(val, dev-base reg); spin_unlock_irqrestore(dev-lock, irq_flags); }spin_lock_irqsave不只是拿锁它还顺带保存并禁用了本地中断。之所以要这么干是为了防止中断处理函数和普通进程路径同时访问寄存器造成冲突。这个细节是很多驱动“偶尔出问题”的根源。3.5 IO访问和内核内存分配的细节访问硬件寄存器的时候常用ioremap或者devm_ioremap_resource把物理地址映射到内核虚拟地址空间再通过readl/writel系列函数进行访问。res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(dev, res); if (IS_ERR(base)) return PTR_ERR(base); writel(0x1, base 0x00); val readl(base 0x04);devm_ioremap_resource会自动检查资源范围合法性、做ioremap申请并且带资源管理。比老式的request_mem_region ioremap方便很多。内核内存分配方面需要知道kmalloc和kzalloc的区别是后者会清零分配到的内存GFP_KERNEL标志意味着可以睡眠而GFP_ATOMIC用于中断上下文。大量数据交互传递时会用到DMA缓冲区申请时就得考虑内存对齐和连续性要求。kmalloc分配的物理连续内存上限有限通常小于128KB具体取决于系统碎片程度大块连续内存得配合CMA或者sg_table来搞。4. 驱动开发学习环境与调试实战驱动开发毕竟不是纯软件功夫环境配置能力直接决定你的效率。学习的早期不建议急着买开发板更不建议在实体硬件上折腾内核编译——而是先在一台性能普通的PC上把整个工具链和内核模块开发流程跑通。4.1 用QEMU模拟环境打通编译-装载-调试的完整闭环在没有开发板的情况下QEMU可以模拟一个ARM平台帮助你验证内核从编译到引导再加载模块的完整链路。很多初学者一上来就买了一块开发板但连“内核模块如何编译进内核”“如何装载和卸载”“printk输出去哪里看”都还没有掌握然后在交叉编译和启动参数上浪费大量时间体验非常不好。我建议的环境配置路线是在一个Ubuntu虚拟机或者实体Linux主机上安装好build-essential、libncurses-dev、flex、bison、bc等依赖。下载一份内核源码比如5.15 LTS先编译一个x86版本通过启动它来验证自己机器能完成完整编译。再用QEMU模拟vexpress-a9或者virt平台交叉编译ARM内核并用busybox做一个最小根文件系统用nfs或者initramfs启动。在内核目录里写一个hello模块用对应内核的Makefile编译insmod后看printk输出。这套配置虽然初次搭建需要一两个小时但之后调试设备树、GPIO、中断这些驱动机制时效率会比真机高很多尤其是不用反复烧写固件。配置QEMU启动环境时最常用的启动参数大致是qemu-system-arm -M vexpress-a9 -m 512M \ -kernel zImage \ -dtb vexpress-v2p-ca9.dtb \ -drive filerootfs.ext2,ifsd,formatraw \ -append root/dev/mmcblk0 rw consolettyAMA0 loglevel8 \ -nographic当然这只是一个参考骨架。实际操作时根文件系统用什么、内核配置选项如何裁剪都需要配合自己的目的逐渐调整。我在初期经常故意把内核配置成带各种debug选项然后让某个驱动加载失败用log查看栈回溯来练习排错。注意内核开发一定要开启CONFIG_DEBUG_KERNEL、CONFIG_KALLSYMS和CONFIG_KALLSYMS_ALL否则Oops日志只能看到一串地址要手工用addr2line映射回源码行号效率低到让人崩溃。4.2 交叉编译工具链的选择笔者当年第一次接触交叉编译时也困惑过为什么我不能直接拿gcc编出来的模块放到板子上答案在于内核模块的后缀和编译目标平台必须匹配。用x86的gcc编出来的.ko放到ARM板子上insmod会直接提示格式错误这在学习初期非常劝退。所以一开始就宁愿多花点时间把交叉编译器版本、内核源码版本、arch与cross_compile设定齐整能避免大量雾里看花的问题。一种简单的写法是在内核根目录调用export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make vexpress_defconfig make zImage -j$(nproc) make modules -j$(nproc)之后你的外部模块编译就用make -C /path/to/kernel M$(pwd) modules如果在外部模块里用到了设备树相关的API建议直接编译成模块再看效果而不是硬编进内核这样迭代速度会快很多。4.3 动态调试与printk的使用策略驱动开发最常见的调试方式依然是printk但它也是把双刃剑加得太多会刷爆内核日志缓冲区严重时还会影响时序敏感的设备。正确的思路是分类处理——在大概率正常的分支上别乱打日志在错误分支上打印错误寄存器、返回码、设备名称等信息主动提升日志的信息量。还可以用动态调试机制echo file demo_driver.c p /sys/kernel/debug/dynamic_debug/control这条命令可以无需重新编译、按文件和函数开关pr_debug输出。在内核模块上用来留日志非常舒服。实测经验里我最常干的事是在read/write函数里加上进入、退出的trace_printk短时间之内定位到某个寄存器的值被改错的位置。这类问题用静态代码阅读往往费眼力靠日志确实能快速缩小范围。4.4 板级调试时如何高效定位硬件问题模拟环境做得再顺最终仍然要板级调试。真机调试最常见的困难是硬件上电后外设不工作你很难区分是硬件问题还是驱动问题。我的经验是分步排查第一步看设备树节点有没有被内核识别。在/proc/device-tree里查找对应节点查看/sys/bus/platform/devices下是否存在平台设备查看驱动的probe有没有被调用。连这些都没有多半是设备树匹配问题和驱动内部逻辑没关系。第二步看寄存器能不能正常访问。直接在驱动probe里打印资源类型和映射地址用手工读写寄存器的方式来验证寄存器访问路径是否通畅。若读取的值始终是0或0xffffffff先确认电源、时钟和引脚复用。第三步才怀疑代码逻辑。现代SoC的很多外围IP是有功能时钟门控的如果设备树里没有打开对应的时钟寄存器的值读了也白读或者写进去直接无反应。5. 学习驱动开发必须避开的那些坑这几年我看到很多初学者在学习Linux驱动时反复踩同一类坑这里把最典型、代价也最高的几个列出来算是经验之谈。5.1 盲目升级内核版本带来的连锁问题很多教程是基于老内核写的于是有人学的时候便顺手选了4.4或者4.9这样的老版本。这种选择有它的合理之处资料丰富、厂商BSP成熟。但它的问题也明显——新写的代码如果迁移到6.x内核一些API已经变了。比如早期的create_proc_entry被proc_create取代、register_chrdev被cdev_add取代。你需要建立一种“API演进”的意识学会对照内核文档和源码版本做迁移。我建议至少用一个LTS版本比如5.15或6.1以上的长期维护版学完全部内容然后自己试着把一个小驱动从5.15迁移到当前最新主线内核这个过程等于把内核的改动思路重新学了一遍对理解内核架构很有帮助。5.2 设备树语法小错误不易排查设备树语法检查错误最常见的是少写了分号、括号不匹配、地址大小写不一致。这类问题往往不会直接报错到让你一眼看到而是设备节点根本没被解析到。比如reg属性写成reg 0x10010000 0x1000;地址和长度的bit宽度定义取决于父节点的#address-cells和#size-cells。如果是16进制写上了0x之外的大小写不一致解析结果都可能与预期不符。我的建议是写完设备树后首先在主机上使用dtc工具编译一下确保语法没问题其次启动后去/sys/firmware/devicetree/base路径确认节点真的存在于内存中。5.3 模块与内核版本不匹配Linux内核模块和编译它的内核源码是强绑定关系。一个在A内核上编出来的.ko拿到同版本但是配置不同的B内核上未必能加载。因为内核会把 vermagic版本魔术字符串和正在运行的内核做比对。insmod: ERROR: could not insert module hello.ko: Invalid module format出现这个优先执行modinfo hello.ko看vermagic行是否和当前内核的版本一致。实际工作中最优解是永远保持模块的编译环境与目标运行环境是同一份内核源码和.config哪怕是补丁版本不一样也别掉以轻心。5.4 调试时未开lockdep和KASAN内核自带的调试工具很多但初学者往往不知道利用。如果驱动会出现间歇性崩溃、数据错乱、内存被踩这类问题强烈推荐打开如下内核配置CONFIG_DEBUG_KERNELy CONFIG_KASANy CONFIG_KASAN_INLINEy CONFIG_PROVE_LOCKINGy CONFIG_DEBUG_ATOMIC_SLEEPy CONFIG_DEBUG_OBJECTSy CONFIG_DEBUG_SLAByKASAN可以在越界访问或释放后使用发生的第一时间给出报告追溯代码执行路径这对排查驱动中指针或者缓冲区越界几乎就是神器。我在调试一个音频驱动时音频数据缓冲区释放后被DMA再次访问导致系统随机崩溃就是靠KASAN的report锁定了问题线索否则靠人肉看代码不知道要纠结多久。5.5 忽略热拔插和电源管理驱动开发一旦涉及产品量产就绕不开电源管理。很多驱动在板子正常跑的时候没问题但一旦进入suspend再resume外设就废了。设备树里要用power-domains、clocks等属性描述好电源域和时钟关系。驱动里则需要实现suspend/resume回调在suspend时保存必要的寄存器在resume里恢复这些寄存器并重新初始化外设状态。这是教材里最容易被忽略的部分但却是实际入职后项目里最频繁被要求加的需求。6. 这本书对运维和内核应用开发者的参考价值你可能觉得《手把手教你学Linux设备驱动开发》只是给专职驱动开发看的其实不然。Linux运维人员、嵌入式应用开发者、甚至做内核安全的人都能够从这本书里找到对自己有用的东西。6.1 对运维工程师的价值理解内核模块与设备模型运维经常要在Linux服务器上处理硬件设备识别失败、驱动加载失败、模块冲突等问题。这时候如果你知道lsmod、find /sys/devices -name driver、modprobe -c背后的内核设备模型原理定位问题会直接快一个数量级。比如你在没有将模块写进/etc/modules-load.d的情况下重启后设备没挂载你能想到是模块没有自动加载的问题再比如两个模块都声称支持同一个设备ID的时候内核是按什么顺序去匹配的懂得modprobe的别名机制和depmod依赖管理排障思路会完全不一样。6.2 对应用开发者的价值了解系统调用背后的路径应用开发者天天在调open、read、write、ioctl但很多人并不知道这些调用在内核里会经过VFS层、字符设备层、具体设备驱动再到硬件这整条链路。读完设备模型之后你再见到一个设备节点、一个ioctl命令、一段mmap逻辑时会非常有安全感因为你清楚它在内核中到底做了什么。这本书的定位并不是“运维手册”也不是“内核源码分析大全”它就是一个桥梁——从硬件角度让你看懂Linux系统如何工作。6.3 对应届生和转行者的价值简历上一个完整的驱动项目现在嵌入式岗位的面试经常问的就是“你写过完整的驱动吗设备树怎么匹配的中断下半部你怎么处理”如果你照着这本书把每个章节的项目都自己跑通一遍然后把过程整理成博客或者GitHub仓库至少能向面试官证明你不是只会背八股文而是有完整解决问题的能力。同时驱动开发本身就训练一种“从硬件往上想”的思维方式你会逐步习惯从datasheet的寄存器表出发去反推软件设计这对之后做系统级开发帮助极大。7. 最后聊点实际的经验和体会写驱动这件事门槛从来不是难在哪一段代码而是难在你有勇气把一个设备从无到有地在真实系统里跑起来。在我自己写过的几十个驱动里有调试一周后发现只是设备树时钟没配的痛苦也有最后定位到是硬件上电时序不满足导致的无语。最终发现驱动开发最珍贵的能力不是会背什么API而是能够通过逻辑拆解和环境验证快速排除干扰锁定根因的能力。如果你刚入门建议给自己定一个小目标不要总想着一下就写出一个完整的网络驱动或者USB驱动而是把字符设备、platform驱动、设备树解析、中断响应、ioctl交互这几件事彻底吃透再结合一块几十块钱的开发板把按键、LED、PWM这些小设备驱动起来建立起自己能控制的完整闭环。这个阶段能走通后面接触再复杂的子系统也只是时间问题。《手把手教你学Linux设备驱动开发》这本书值得放在手边随时翻。它最大的好处是可以帮你把零散的知识串成一个体系而且文字本身也比较接近有经验的工程师在做“代码走读”时的讲解方式读起来没那么累。我非常建议读者不要只看不做每读一章都要动手敲代码带着问题去读把书里的Demo通过自己的环境重新实现一遍。驱动开发的乐趣就在于你写的每行代码都会实实在在地跟物理世界产生互动一个寄存器的写入可能就让一个电机转起来一个中断的回调可能就让你收到一帧串口数据。别被一时的不理解吓退先把流程跑通再慢慢深挖原理。这条路走下来你的收获会远远超出“学会了写驱动”这件事本身。