ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux设备驱动开发从入门到实战:字符设备、设备树与中断机制全解析

2026/9/7 8:49:54 拓冰建站 浏览量
Linux设备驱动开发从入门到实战:字符设备、设备树与中断机制全解析 看到《手把手教你学Linux设备驱动开发》正式出版的消息我第一反应是终于有人愿意把驱动开发这摊子事掰开揉碎讲清楚了。我在嵌入式这行干了快十年带过不少新人几乎每个人在Linux设备驱动开发这条路上都栽过跟头——不是看不懂代码而是不知道代码为什么会写成这样、遇到问题该怎么查、硬件和内核之间到底是怎么咬合的。这本书的定位恰好就是把那些“没人愿意细讲”的环节补上。这篇文章不打算复述书里的目录而是借这个出版节点把驱动开发的学习路径、核心机制、调试手段和踩坑记录完整捋一遍。无论你是准备入行的学生、想转岗的嵌入式工程师还是正在准备驱动岗位面试的开发者这篇内容都能帮你把零散的知识串成一条线。1. 这本书解决的核心痛点驱动开发的学习门槛到底高在哪1.1 驱动开发的真正难点不是C语言而是内核思维很多人一提到驱动开发第一反应是“C语言要写得溜”。但根据我带新人的经验C语言基础只要达到能看懂指针、结构体、函数指针的程度就够用了真正的门槛在于思维方式的转变。应用层开发面对的是操作系统提供的API你调用open、read、write系统帮你把文件描述符、页缓存、调度、IO这些都处理好了。驱动开发恰恰相反你是这些机制的提供者。你要回答的是内核调用你的open时你到底应该做什么read的时候数据从哪里来到哪里去中断来了之后你如何在几微秒内处理完该处理的逻辑然后把耗时的事扔给下半部这就是内核思维的核心——你写的代码不是“从上往下执行”的流水账而是被各种事件驱动的、运行在特定上下文里的一组回调函数。你要时刻清楚自己现在处于进程上下文还是中断上下文能不能睡眠能不能用锁哪些内核API在这个环境下是禁区。另一个难点是内核代码的运行环境。应用层崩溃了有core dump有gdb你甚至可以直接打断点。内核模块出问题轻则oops重则整个系统卡死重启连打印信息都来不及看。这种情况下你必须学会在“没有调试器”的条件下生存靠日志、靠机制分析、靠经验判断。这本书的价值就在于它把这些“内核思维”相关的知识点讲透了而不是只贴代码。1.2 这类“手把手”书籍的正确用法很多人买技术书籍有个误区从第一页开始当小说读读完一遍就束之高阁。这种学习方法对驱动开发来说效率极低。我的建议是把这类手把手类型的书当成“地图字典”来用。第一遍读的时候重点理解每章的框架和思路——字符设备框架长什么样、设备树节点怎么描述硬件、中断流程怎么走。代码不用逐行背但每个示例的骨架要在脑子里留下印象。真正开始学的时候一定要按着书里的步骤亲手编译一次、加载一次、验证一次。驱动开发是重实操的技术活光看不练等于白看。哪怕是书中跑通的示例代码你在自己环境里复制一遍都会遇到编译器版本、内核头文件路径、权限管理之类的幺蛾子这些“意外收获”恰恰是学习过程中最有价值的部分。这本书我翻了目录和核心章节它的内容是沿着“基础——框架——实战——调试”这条线展开的很适合既想学原理又想上手的读者。接下来我结合自己的经验把驱动开发路上最核心的知识点拆开来说。2. 打地基阶段内核知识储备与学习环境搭建2.1 内核基础从模块机制到编译体系驱动开发首先要过的一关就是理解“模块”这个概念。Linux内核是宏内核理论上所有功能都可以编译进内核镜像但实际开发中驱动通常以模块.ko文件的形式存在按需加载方便调试和升级。模块的生命周期管理是最基础的module_init和module_exit宏指定的两个函数一个在insmod的时候执行一个在rmmod的时候执行。这听起来简单但里面涉及的细节很多。比如__init标记的含义——这个函数在初始化完成后会被释放掉内存可以被回收如果模块编译进内核而非单独加载__init的作用就更加明显。很多新手问“为什么我的init函数前面要加__init”回答就是让内核知道这段代码只在启动阶段用用完可以丢掉。编译体系是另一个需要花时间搞明白的地方。一个最简单的字符设备模块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这里面的关键不是那两行编译命令而是理解KDIR指向的内核构建目录必须与你正在运行的内核源码版本完全一致。如果不一致insmod的时候系统会报“Invalid module format”因为模块内部的vermagic信息和内核版本对不上。我见过大量的新人卡在这个问题上反复编译就是加载不了最后发现是用的内核源码版本和当前运行内核不是同一个。还有一个易被忽视的基础是内核日志系统。printk是驱动开发时最常用的调试手段它不像printf一样输出到终端而是写入内核环形缓冲区通过dmesg命令查看。printk的日志级别从KERN_EMERG到KERN_DEBUG默认级别是KERN_WARNING级别低的日志可能不会打印到控制台。这些细节看似琐碎但真正排错的时候一条被丢掉的日志可能让你多折腾半天。2.2 环境选型虚拟机、QEMU还是开发板学习环境的选择直接决定你入门的速度和积极性。我先说说三个方案各自的优劣再给一个适合大多数人的路线。虚拟机是最低成本的起步方案。在VMware或VirtualBox里装一个Ubuntu安装好build-essential和linux-headers包就可以开始编译和加载模块了。优点是完全隔离系统崩了大不了重启虚拟机缺点是只能在虚拟硬件上运行很多真实的硬件交互逻辑GPIO、中断控制器、外设寄存器接触不到学完字符设备框架之后就有点不够用了。QEMU是一个很好的进阶选择。它可以在普通PC上模拟ARM开发板典型的是qemu-system-arm配合vexpress或virt平台。这种方案的好处是能切身体会到“交叉编译”也就是在x86主机上用arm-linux-gnueabihf-gcc编译代码再放到模拟的ARM环境里运行。这是我比较推荐的进阶路线因为现在嵌入式主流的SoC基本都是ARM架构QEMU可以让你在不买硬件的情况下先熟悉ARM环境下的驱动开发流程同时也可以用设备树来控制外设模拟。开发板则是最终绕不开的一环。正点原子、野火这类厂商的i.MX6ULL或STM32MP1开发板几百块的价格配套资料成熟可以直接操作真实的寄存器、GPIO和中断控制器驱动开发和硬件调试的所有细节都能体验到。如果你未来打算找嵌入式Linux方向的工作开发板建议迟早要入一块。我个人的建议是第一周用虚拟机跑通模块编译和字符设备基本框架第二周转到QEMU研究设备树和platform驱动第三周开始再考虑买开发板做实战项目。循序渐进既不会一开始就被环境折腾得丧失信心又能逐步逼近真实场景。3. 驱动开发四大核心机制拆解3.1 字符设备框架驱动与用户态的桥字符设备是Linux驱动开发最基本的设备类型几乎所有入门书籍都会从它开始讲。它对应的是那些以字节流方式读写的设备比如串口、LED、按键、传感器等等。理解字符设备框架本质上就是理解用户态的应用程序如何通过文件操作接口访问到你的硬件。用户态程序执行open(/dev/demo, O_RDWR)时经过虚拟文件系统VFS层层查找最终会找到设备号对应的cdev结构体然后调用你注册的file_operations结构体中的open函数。所以驱动开发的本质就是实现一个file_operations结构体并把设备号和一个cdev结构体关联起来。当前主流的字符设备注册流程我直接贴一段核心代码#include linux/module.h #include linux/init.h #include linux/fs.h #include linux/cdev.h #include linux/slab.h #include linux/uaccess.h #define DEMO_CNT 1 static int demo_major; static struct cdev demo_cdev; static struct class *demo_class; static ssize_t demo_read(struct file *filp, char __user *buf, size_t count, loff_t *pos) { const char *msg hello from kernel\n; size_t len strlen(msg); if (count len) return -EINVAL; if (copy_to_user(buf, msg, len)) return -EFAULT; return len; } static const struct file_operations demo_fops { .owner THIS_MODULE, .read demo_read, }; static int __init demo_init(void) { dev_t dev; int ret; ret alloc_chrdev_region(dev, 0, DEMO_CNT, demo); if (ret 0) return ret; demo_major MAJOR(dev); cdev_init(demo_cdev, demo_fops); ret cdev_add(demo_cdev, dev, DEMO_CNT); if (ret 0) goto err_cdev; demo_class class_create(THIS_MODULE, demo_class); if (IS_ERR(demo_class)) { ret PTR_ERR(demo_class); goto err_class; } device_create(demo_class, NULL, dev, NULL, demo_dev); pr_info(demo: major %d\n, demo_major); return 0; err_class: cdev_del(demo_cdev); err_cdev: unregister_chrdev_region(dev, DEMO_CNT); return ret; } static void __exit demo_exit(void) { dev_t dev MKDEV(demo_major, 0); device_destroy(demo_class, dev); class_destroy(demo_class); cdev_del(demo_cdev); unregister_chrdev_region(dev, DEMO_CNT); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL);这段代码里有几个地方是新手容易忽略的。注意我用的是alloc_chrdev_region分配动态主设备号而不是老教材里常见的register_chrdev_region指定一个固定主设备号。动态分配的好处是避免不同驱动之间的设备号冲突。很多老书喜欢用register_chrdev_region(dev, 0, 1, demo)然后写死一个主设备号如250这在教学示例里没毛病但实际项目中很容易和别的设备撞号所以现在主流方案都是动态分配。代码里还有一个细节值得琢磨device_create(demo_class, NULL, dev, NULL, demo_dev)。这行代码的作用是在/sys/class/demo_class/下创建设备节点信息配合udev系统自动在/dev/下生成demo_dev节点。如果没有这行你需要手动mknod /dev/demo_dev c 主设备号 0每个模块加载后都要手工敲一遍效率太低。理解class和device_create的作用是理解现代Linux设备模型的第一步。还有一个必须注意的点是copy_to_user的用法。内核空间不能直接拷贝数据到用户空间原因涉及内存隔离和权限控制——用户空间的内存页可能被换出也可能存在无效的指针。copy_to_user不仅要拷贝数据还要负责地址合法性检查。更关键的是在访问用户空间指针时必须考虑到进程可能被信号打断所以copy_to_user返回非0值时大多数情况下要返回-EFAULT。3.2 设备树与platform总线设备和驱动的配对逻辑如果你看过近几年出版的Linux驱动书籍会发现设备树Device Tree占的比重越来越大。设备树本质上是一种描述硬件信息的数据结构它解决的是“驱动的代码里到处都是硬编码的寄存器地址和中断号”这个历史痛点。在没有设备树之前一个驱动源码里往往写死了IO基地址、中断号等硬件信息换了硬件平台就得改代码重新编译。设备树把硬件描述从驱动代码中剥离出来同一份驱动代码通过不同的dts设备树文件就可以适配多种硬件配置。设备树节点很简单一个典型的节点是这样的demo_device: demo1c00000 { compatible vendor,demo; reg 0x1c00000 0x1000; interrupts 0 42 4; status okay; };compatible字段就是驱动和设备之间的“暗号”驱动用of_match_table声明自己能处理哪些设备static const struct of_device_id demo_of_match[] { { .compatible vendor,demo }, { } }; MODULE_DEVICE_TABLE(of, demo_of_match); static struct platform_driver demo_driver { .probe demo_probe, .remove demo_remove, .driver { .name demo, .of_match_table demo_of_match, }, }; module_platform_driver(demo_driver);整个平台设备platform device机制的核心就是“设备、驱动、总线”三者的配对关系。总线负责维护两个链表——设备链表和驱动链表当一个新的设备或驱动注册进来时总线就会遍历另一方的链表看看有没有匹配项。匹配成功后就调用驱动的probe函数probe成功了设备和驱动才算正式“绑定”。这就是Linux设备模型的精髓也是解耦思想的体现。设备负责声明“我有什么资源”驱动负责说明“我能操作什么设备”总线负责撮合。理解了这套机制你以后看任何platform驱动的代码都会有一种“原来如此”的顺畅感。实际操作的时候在probe函数中一般要做这几件事从设备树节点里获取资源platform_get_resource、of_property_read_u32等、映射寄存器的物理地址到内核虚拟地址ioremap或devm_ioremap_resource、申请中断devm_request_irq、初始化硬件、注册字符设备或者杂项设备。devm_前缀的函数是设备资源管理机制可以自动帮你做资源释放能省去大量出错时的清理代码强烈推荐优先使用。3.3 中断处理上半部与下半部、为什么讲究这么多中断几乎是驱动开发中最容易让新人翻车的地方。CPU在收到硬件中断后会立刻跳转到中断处理函数这时系统的状态非常脆弱——当前进程的执行被粗暴打断中断上下文里无法调用任何可能睡眠的函数。为什么中断里不能睡眠很多人只是记住了结论没理解原因。道理其实不难在进程上下文中睡眠调度器会切换到其他进程等条件满足后再回来继续执行但在中断上下文里没有“进程”的概念你一旦睡眠CPU将永远卡在那里因为调度器根本不知道怎么恢复你的执行上下文。更直接的后果是如果中断处理里自旋锁被占用时睡眠整个系统可能直接死锁。所以中断处理必须遵循两条黄金法则第一处理要快第二不能睡眠。但硬件中断常常需要做大量耗时工作比如处理网络数据包、拷贝大数据、操作慢速总线上的设备。这就引出了“中断下半部”机制——上半部执行中断处理函数只做最紧急的、必须立即响应的操作比如清中断标志、保存寄存器状态重活交给下半部在更安全的环境下慢慢执行。下半部的实现方式有几种老式的软中断和tasklet、内核工作队列workqueue、以及线程化中断threaded irq。实际开发中最常用的是后两者。工作队列运行在进程上下文可以睡眠适合做比较耗时的操作线程化中断则把整个中断处理变成一个内核线程在驱动里用起来非常简单static irqreturn_t demo_irq_handler(int irq, void *dev_id) { struct demo_dev *dev dev_id; /* 上半部快速处理硬件细节 */ schedule_work(dev-work); return IRQ_HANDLED; } static void demo_work_handler(struct work_struct *work) { struct demo_dev *dev container_of(work, struct demo_dev, work); /* 下半部做耗时的事情 */ }需要特别提醒的是中断处理里访问的共享数据要加锁保护但又不能使用可能睡眠的锁所以在中断上下文用自旋锁spinlock。自旋锁的原理是在等待锁的过程中原地打转循环检测不会睡眠适合临界区极短、持锁时间很短的场景。如果你的临界区很长在中断上下文里用自旋锁会浪费大量CPU时间就应该考虑换用下半部机制把耗时操作用workqueue去做配合互斥锁使用。3.4 并发与内存管理驱动崩溃的重灾区驱动开发中最难的部分不是把功能写出来而是写出来的代码可以稳定运行在多种并发场景下。一个字符设备的read、write函数可能被多个进程同时调用中断随时可能打断正在执行的内核代码多核CPU上还有真正意义上的并行执行。新手最常见的问题是完全没有并发意识。一个全局变量在不同函数里随便读写一开始测试没发现问题压力一上来或者客户现场一跑就随机崩溃。这就是并发bug的典型特征——不是不崩是时候未到。在内核里并发保护的手段主要有原子变量、自旋锁、互斥锁、读写锁、RCU等等。直接用哪个不是拍脑袋决定的要考虑临界区的上下文环境在进程上下文且临界区会睡眠用mutex在中断上下文或临界区极短的硬件操作场景用spinlock只是操作一个整数用atomic_t。内存管理方面内核空间比用户空间要复杂得多。kmalloc适合分配小块物理连续内存要求GFP_KERNEL标志vmalloc可以分配虚拟地址连续但物理不连续的大块内存DMA操作则必须保证物理连续。对于现代SoC外设和内存之间的数据搬运通常走DMA控制器这时还要考虑缓存一致性的问题CPU可能已经把数据缓存在Cache里了DMA直接写内存会导致数据不一致。解决方法是使用dma_alloc_coherent分配一致性的DMA缓冲区或者手动做Cache的clean和invalidate操作。我见过太多驱动开发者在DMA和缓存上栽跟头。现象多种多样有时候数据传输偶尔出错有时候接收缓冲区里的数据迟迟不更新甚至出现“运行一段时间后彻底卡死”。排查到最后十有八九是DMA buffer的物理地址和Cache一致性处理出了问题。这块知识点书里一定要讲透不然实战的时候会很痛苦。4. 实操踩坑记录与调试手段实录4.1 编译加载阶段的经典报错我总结了一下这么多年见过的驱动开发入门问题几乎一半以上都集中在编译加载阶段。这些问题不难解决但很烦人值得单独拎出来说一遍。第一个高频报错是insmod时提示Invalid module format。前面说过模块的vermagic信息和内核版本必须匹配。出现这个问题的原因要么是/lib/modules/$(uname -r)/build链接指向的内核源码版本和当前内核不一致要么是你手动编译安装过内核重启后用的还是旧内核。排查方法是先uname -r确认版本再看/lib/modules/下有没有对应目录必要时重新安装linux-headers包。第二个高频问题是编译时报unknown symbol或者undefined symbol。这种问题通常是因为依赖的某个内核符号没有导出没有使用EXPORT_SYMBOL导出或者依赖的模块没有先加载。比如你的驱动用到了另一模块的函数insmod时必须先加载那个依赖模块或者直接用modprobe让它自动处理依赖关系。第三个是设备节点相关的问题。模块加载成功了lsmod能看到但是/dev/下面找不到对应节点。大部分原因是代码里没有调用device_create或者/etc/udev/rules.d/下没有匹配的规则。手动验证的时候可以mknod /dev/demo_dev c 240 0先用起来但要记住这只是临时方案。第四个是模块加载后系统直接卡死或重启。这类问题基本是驱动代码里有非法访问比如解引用了空指针、ioremap了无效物理地址、注册中断时写坏了中断号。刚开始做驱动开发建议先不要急着测试真实硬件而是用QEMU这类模拟环境崩了也不影响主系统。我把这些常见问题整理成了一张速查表方便以后排查时对照现场现象可能原因排查方向insmod失败Invalid module format内核版本/编译器不一致检查uname -r与KDIR确认工具链版本insmod失败Unknown symbol依赖符号未导出或依赖模块未加载查看dmesg报错modprobe替代insmod/dev下无设备节点缺少device_create或udev规则不匹配检查/sys/class对应目录手动mknod验证加载后系统卡死非法指针、ioremap地址错误、中断号错误抓串口/内核日志最小化代码定位rmmod失败Device or resource busy设备被进程打开模块引用计数非0用fuser定位占用进程确认release接口正常4.2 运行期崩溃学会读oops驱动开发过程中遇到内核崩溃几乎是必然事件。内核崩溃时输出的一堆信息叫oops英文好的同学可能知道oops的本意是“哎呦”这个命名很形象——内核不小心踩到了地雷发出一个“哎呦”的告警。oops并不是整个内核必然挂掉但它意味着执行到非法上下文相关的进程会被杀掉严重时会panic直接停机。读oops信息是有套路的。核心是看这几行Unable to handle kernel NULL pointer dereference之类的错误类型描述、出错的PC指针地址、函数调用回溯Call trace、以及出错的模块基址。配合模块符号表你可以用addr2line把PC地址转换成源码行号。比如你的模块基址是0xbf000000出错地址是0xbf001234减去基址得到偏移0x1234然后执行arm-linux-addr2line -e demo.ko 0x1234 -f就能直接定位到出错的函数和行号。这个操作是驱动开发调试的基本功书上虽然会讲但我还是建议你早日养成习惯——第一次用的时候可能觉得麻烦但用熟了之后排查速度会快一个量级。除了oops日常调试常用的还有/proc和/sys文件系统。查询设备当前的中断触发情况看/proc/interrupts检查注册的字符设备看/proc/devices查看每个模块加载后的内存占用看/proc/modules。/sys/kernel/debug下的debugfs接口在驱动里如果注册了也可以看到更详细的状态信息。内核还提供fTrace、kprobe、perf等跟踪工具用于分析函数调用流程和性能瓶颈到了项目后期优化阶段会经常用到。这里我要强调一个排查思路先复现后分析。很多新手遇到bug第一反应是盯着代码看对着源码发呆。实际上内核驱动的问题尤其是和时序、并发相关的问题最好的办法是先想办法稳定复现再通过日志定位。如果只能稳定运行几小时才崩一次那就把printk放到关键路径上反复跑直到抓到一个相对完整的崩溃现场再结合oops信息去分析代码逻辑。4.3 排查技巧与面试考点速查面试Linux驱动开发岗位考察的内容通常不会太刁钻但很看重你是否真的理解设备模型和内核机制。这里我整理几个出现频率很高、同时也很能区分知识深度的考点供正在准备面试的同学参考。第一个必考的点是字符设备驱动开发的完整流程。从申请设备号、初始化cdev、注册设备、创建class和device到实现file_operations中的核心方法再到卸载时的清理。面试官一般会顺着你的回答追问比如“为什么用copy_to_user而不是直接拷贝”“cdev_add失败后要注意什么”这些细节积累靠的是真做过几次而不是背下来的。第二个高频考点是platform总线机制。面试官喜欢问“platform设备驱动的匹配方式有哪些”常见的回答是设备树compatible匹配、平台设备名称匹配、ID表匹配等。更深一层的问题可能是“现代SoC为什么普遍使用platform设备驱动”这时候如果能说出它把硬件资源描述和驱动逻辑分离的好处就会比较加分。第三个是中断处理相关的考察。在中断上下文能不能调用kmalloc或mutex_lock这个问题很多人答不全。中断上下文里可以调用带GFP_ATOMIC标志的kmalloc但不能用mutex因为mutex_lock可能睡眠。中断下半部会问tasklet、workqueue、threaded irq各自的适用场景如果你能说出真实项目中的取舍案例面试官会眼前一亮。第四块是内存相关基础。kmalloc和vmalloc的区别、DMA一致性缓冲区的作用、iounmap的配对使用包括设备树里reg属性的解析和ioremap的关系。这些考察的是你有没有真正在ARM平台上调试过硬件。还有一个常常被忽视的加分项是内核日志分析能力。能从容地读oops并解释PC指针和Call trace的含义这种实战能力的信号远比“我在某个教程里写过hello world驱动”强得多。准备面试的同学建议多在虚拟机里故意制造几次模块崩溃把oops显示的每一行含义查清楚这个过程本身就是一次扎实的实战训练。5. 聊点实际的这本书怎么用以及一条完整的学习路线参考很多人买书之后最大的问题不是书不好而是不知道怎么用。针对《手把手教你学Linux设备驱动开发》这类书籍我给一个可执行的学习路线建议。这条路线配合树莓派或任意一块ARM开发板三到四周可以走完。第一周集中精力搭环境。安装虚拟机或直接装好Ubuntu确认内核源码可以编译跑通第一个hello world模块。目标不是学会多少API而是理解模块的编译、加载、卸载、日志输出这条完整链路。这个过程中遇到任何环境问题都要想办法自己解决不要跳过因为后面所有的实验都依赖这套环境。第二周系统学习字符设备框架。把书里的字符设备章节从头到尾敲一遍吃透file_operations结构体的每个成员。然后尝试自己写一个带ioctl接口的设备驱动配合用户态测试程序调用它体验一次完整的“用户态——内核态”数据交互过程。有条件的话试着用一个实际的LED或按键来当测试对象感知立刻完全不同。第三周重点研究设备树和中断。在QEMU或者开发板上尝试修改设备树节点、添加自己的platform驱动并在驱动中申请一个中断配合下半部机制实现功能。这个阶段的目标是理解“设备树描述硬件——内核匹配驱动——驱动的probe完成初始化”这条主线。第四周做一个综合小项目。比如一个简单的按键中断驱动程序按下按键通过工作队列触发数据上报用户态程序读出来并打印。这个小项目会综合用到字符设备、设备树、中断、并发控制、工作队列等知识点做完这一遍驱动开发的核心框架就基本建立起来了。在使用这本书和任何同类资料时还要记住一个原则示例代码只是引子不是标准答案。做驱动的过程中要习惯去内核源码里查证。/lib/modules/$(uname -r)/build目录下就是完整的内核源码include/linux下的头文件、drivers/目录下的真实驱动代码都是最好的学习材料。看书学框架翻源码学细节两者结合进步才会快。根据我个人带项目的经验驱动开发这个方向真正的分水岭往往不在“会不会写驱动代码”而在“遇到设备不正常工作时能不能快速定位是硬件问题、配置问题还是驱动代码问题”。这本书能帮你跨过“会写”这道坎后面“会调”的能力就需要靠更多的实操积累和源码阅读慢慢打磨了。最后再分享一个小技巧每次做完一个驱动实验建议把设备树、驱动代码、测试程序、踩坑记录放到一起整理成一个小文档存成自己的“排错手册”。时间长了这套自己沉淀下来的资料才是最难买的参考书。