干过嵌入式驱动的人,大概率都有过这种体验:驱动在开发板上跑得行云流水,功能、性能、交互样样正常,演示给领导看,完美。结果一到小批量试产,或者一上老化测试,问题就像雨后春笋一样冒出来——偶发死机、数据错乱、极端温度下复位、连续跑两天后资源耗尽。这时候最头疼的是,问题还不好复现,你盯着串口日志看半天,它就是不出事;你刚转身去喝水,测试那边就报挂了。这就是典型的“能跑”和“会崩”之间的差距,也是嵌入式驱动开发从功能实现走向量产级工程化必须跨过的坎。
这个专栏,我想认真聊聊驱动开发的工程化问题。不聊那些“照着datasheet把寄存器配一遍就跑起来”的入门套路,而是聚焦量产交付场景下真正决定产品成败的细节:并发与竞态、缓存一致性、中断上下文、错误恢复、可观测性、测试验证,以及一堆只有踩过坑才说得清的实操经验。适合正在写驱动但总被测试打回、从应用开发转驱动、以及在团队里负责把方案做成产品的工程师。内容会以Linux平台为主线,但很多思路放到裸机、RTOS甚至DSP开发里,同样适用。
1. 项目概述:从功能演示到量产交付,中间缺了什么
1.1 什么叫做“能跑”
“能跑”这个状态,其实很容易达到。以Linux字符设备驱动为例,你按官方模板register_chrdev、创建class、实现file_operations,再把read/write/ioctl这些回调里填上对应的硬件操作,编译进内核或者insmod加载,应用层open一下、read一下,数据出来了,就觉得自己已经完成了驱动开发。
但“能跑”本质上是在特定条件下验证了主逻辑。这个“特定条件”通常很宽容:环境温度二十几度,总线上只有一个设备,中断频率不高,CPU负载很低,用户程序也只会规规矩矩地按正确流程调用接口。在这种条件下,大部分驱动都能表现出“正常”的样子。
真正的问题在于,量产环境不会这么温柔。温度会到七十度,总线可能有干扰,多个外设会同时抢CPU,用户程序可能异常退出,设备会热插拔,电源可能抖动。所有这些边界条件叠加在一起的时候,驱动里那些没被验证过的路径,就会一个接一个地暴雷。所以“能跑”和“会崩”之间的差距,本质上不是代码写得对不对的问题,而是代码在多大范围内被验证过的问题。
1.2 量产级工程化具体指什么
量产级工程化,在我理解里不是一个模糊的口号,而是一组可以量化的评判维度:确定性、可恢复性、可观测性、可维护性。
确定性指的是在给定的输入和条件下,驱动行为是可预测的。同一个中断在同一时刻反复触发,不会因为并发时序不同而产生不同结果;用户程序在临界时间点调用close,不会让驱动进入一个无法恢复的中间状态。可恢复性指的是出错之后,驱动能够自愈,比如通信超时后重试、DMA错误后重新初始化、设备无响应时热复位。可观测性指的是问题发生后,系统能留下足够的信息帮助你定位,而不是只留下一句“系统挂死”和一片死寂的串口。可维护性则是对代码结构的要求,驱动不是一次性的demo,后续要支持多型号、多版本、多人协作,结构混乱的驱动迟早变成团队的黑洞。
我给很多项目做过评审,看到过太多“能跑”的驱动在评审会上被问几个问题就哑火:掉电的时候你的驱动在做什么?设备热插拔时怎么保证不崩?中断风暴来了怎么办?DMA和CPU共享数据时,缓存怎么处理?这些问题,标准模板里没有答案,得靠自己把工程化要求前置到设计和编码阶段。
2. 为什么功能正常,依旧会崩
2.1 边界条件:崩溃大多藏在主流程之外
驱动开发的崩溃,绝大部分不是发生在主流程里,而是在边界条件下。主流程是代码的“happy path”,它被反复测试,写得很成熟;边界条件则是那些只会在特定时刻出现的场景,一旦没处理好,就会在某个凌晨把设备打死。
举个很常见的例子。SPI驱动里,主流程是“使能片选->发送数据->等待完成->释放片选”,这套流程在开发板上跑一天都不会出错。但你有没有处理过连续传输时CS信号的毛刺问题?有没有考虑过CS被拉低的瞬间,从设备端在复位时序上的要求?还有更隐蔽的:DMA缓冲区有没有按缓存行对齐?最大传输长度超过硬件FIFO深度时,有没有切到非DMA路径?这些全部是边界条件。
我做项目时会习惯性地列一张边界条件清单:输入参数的最大值和最小值、传输长度在临界点附近的行为、设备的枚举顺序变化、供电电压上升/跌落瞬间的行为、时钟频率改变后的时序参数。把这张单子走一遍,大多数“会崩”的驱动都能提前暴露问题。
2.2 并发与竞态:驱动里最狡猾的敌人
并发竞态是驱动崩溃的头号原因,也是“能跑”和“会崩”最典型的分水岭。在开发板上,你一个人、单核、低负载,竞态窗口可能从没被打开过;到了多核主控或者高负载场景,两个中断同时到达,或者用户线程和中断处理路径同时访问同一个寄存器,问题立刻就出来了。
可以用一个生活化的类比理解这件事:电梯门。正常情况下一人一进一出,相安无事;高峰期一群人挤在门口,门夹到人、超重报警、死机,都是因为多个动作在时间上重叠了。驱动代码也是一样,多个执行流同时访问共享资源(寄存器、内存缓冲、设备状态变量),如果没有锁保护或者原子操作,轻则数据错乱,重则系统panic。
Linux内核里并发来源特别多,中断、软中断、workqueue、多核SMP、用户态同时open、procfs读写等,任何一个都可能和主流程竞争。比较常见的错误是:在驱动里用一个全局变量存设备状态,A线程在写,B线程在读,两边没有加锁,结果B读到了中间状态,做了错误分支。这个问题在单核低负载下几乎不可能暴露,因为写和读之间时间间隔太长,但多核环境下可能一个指令周期内就会发生。内核提供了spinlock、mutex、atomic_t、completion等一堆机制,关键不是会不会用,而是要知道在什么场景下必须用。
2.3 缓存一致性:DMA与CPU的经典对抗
缓存一致性是嵌入式里最容易被忽略、又最容易造成诡异故障的点。现象往往是这样的:驱动跑起来之后,数据大部分时间是对的,但偶尔会出现一个字节错误,或者DMA搬运完之后CPU读到的是旧数据,再或者CPU写完数据之后DMA搬运出去的内容不对。这类问题用printf定位根本没用,因为修改代码、加打印本身就可能改变时序。
现在的嵌入式处理器,CPU和DMA对内存的访问路径是独立的。CPU写数据时,数据可能先留在Cache里,还没有刷到内存;DMA读内存时,如果走的是总线直接读,它就读不到Cache里尚未写回的数据。反过来,DMA把数据写进内存之后,CPU如果去读Cache里残留的旧副本,就会忽略DMA写好的新数据。这就产生了数据不一致。
处理方案其实很成熟:DMA接收缓冲在启动DMA之前要clean cache,确保CPU此前写的脏数据被写回内存;DMA完成后要invalidate cache,把CPU Cache里可能存在的旧数据作废,让CPU重新去内存读。很多芯片的DMA接口有缓存一致性属性,但即便有,也要确认驱动里对缓冲区属性、映射方式的处理是否配套。之前在调试一个视频采集驱动时,就遇到图像画面偶尔有“花屏线条”,后来整整查了两天,发现是DMA缓冲区的dma_alloc_coherent和普通kmalloc混用了,一个走一致性的缓冲一个走带Cache的缓冲,数据流中间一旦混搭,就出这种偶发花屏。类似的问题在DSP里也一样存在,比如C674x这类带L1P/L1D/L2的架构,内存映射与缓存配置如果不匹配,性能和对错的差别都很大。
2.4 时序与状态:驱动里“看不见”的时间约束
嵌入式驱动和纯软件最大的区别,就是硬件对时间有硬约束。寄存器写完到状态生效,可能要等几个时钟周期;中断标志位置位之后,必须在下一次操作之前清除;设备上电到稳定,可能需要几十毫秒;总线上发起一个读操作,从设备必须在规定时间内响应,否则就要超时处理。
这些时序约束在datasheet里都写得清清楚楚,但到了实际编码时,很多人却把它抛在脑后。常见错误包括:写寄存器后立即去读状态寄存器,读回来永远是旧值;中断处理函数里第一时间把标志位清掉,结果另一个中断已经把新标志位置起来了,导致事件丢失;设备初始化序列里少了延时,probe阶段就失败;超时处理直接return -ETIMEDOUT,却没有恢复流程。
时序问题的排查手段也很特别,你不能靠printk,因为printk本身会改变时序。通常得靠示波器/逻辑分析仪在硬件层面看信号的实际行为,再和代码逻辑做对照。驱动工程师必须养成“看波形”的习惯,这是和纯软件工程师最大的区别之一。
3. 工程化驱动开发的核心实践
3.1 把驱动当作产品来设计:接口与分层
很多人写驱动,是从datasheet开始,一路写到sysfs或者ioctl,中间没有任何抽象。代码结构就是“一个文件,从上到下,全是寄存器操作”。这种驱动在自己手里没问题,一旦换人维护,或者要支持同一芯片的多个版本,立刻就变成灾难。
工程化的思维方式是先定义接口,再填充实现。拿一个常见的温湿度传感器驱动举例:定义两个接口,sensor_probe和sensor_read_temperature,接口内部再做I2C传输、数据校验、单位转换。上层业务只依赖接口,不感知底层总线协议细节。这样做的直接好处有三个:第一,协议切换时(比如从I2C改成SPI),只需要重新实现底层传输函数,上层逻辑一行都不用动;第二,单元测试变得可行,伪一个底层传输层,业务逻辑在PC上就能跑;第三,代码评审的人不需要对着datasheet逐位核对你的寄存器操作,可以通过接口定义和实现结构快速发现问题。
分层设计还能有效避免一个很典型的问题:业务逻辑和硬件操作强耦合,导致为了测试一个报警功能,必须在板上真实触发一次硬件异常。做了分层之后,你可以在软件层面直接注入一个异常返回值,测试上层逻辑是否正确响应。
3.2 错误处理与恢复路径:让驱动“死不了”
驱动开发的另一个工程化重点是错误处理。很多“能跑”的驱动,错误处理其实就是给每个函数加上 “if (ret < 0) return ret;”,然后呢?然后就没有然后了。
量产环境里,设备一定会出错——总线忙、命令无响应、CRC校验失败、DMA超时。出错不可怕,可怕的是出错后系统没有恢复机制。我最开始做驱动时,给一个工业设备写传感器驱动,I2C通信偶尔会因为电气干扰失败一次。我当时的处理是,读到错误码就直接返回上层,上层收到错误就告警。结果产线一跑,每天都在报故障,因为传感器一旦被干扰,通信就陷入一个“失败-告警-再试-再失败”的循环里,完全没法正常生产。
后来改成工程化方案:通信失败后自动重试三次,每次增加延时;如果三次都失败,执行一次设备复位序列;复位后初始化设备再重新读取;如果仍然失败,才向上层上报定级错误。改完之后,产线故障率立刻降到接近于零。这个设计思路在所有驱动里都应该贯穿:错误不要直接抛出去,要有一个“轻微错误自动恢复、严重错误告警处理”的分级策略。调这样的错误恢复逻辑时,很多人会担心“重试会不会引入不可接受的延迟”,实际上只要把超时和重试参数做成可配置的,就不会有任何问题。对量产来说,稳定运行的优先级永远高于偶尔一次的性能抖动。
3.3 日志与可观测性:出事之后怎么定位
驱动一崩,第一反应就是看日志。可糟糕的是,很多驱动的日志在开发阶段全是动态调试、堆了一堆debug信息,到了量产阶段又全关掉了,结果一出事,日志要么看不到,要么全是有用的没用的混在一起,无从下手。
工程化的做法是把日志按级别和模块规划好。错误级别的问题必须能无条件输出,警告级别的问题应当在生产配置下也保留,信息级别的操作记录做成可开关的动态调试。Linux的printk、dev_dbg、dev_err以及动态调试机制已经给了完整的工具链,问题是你有没有规划好用哪一级打哪些东西。
除了代码层面的日志,驱动里也要考虑可观测性设计:在procfs或sysfs下暴露设备的实时状态、寄存器快照、错误计数值、最后一次出错的上下文。这样即使不能实时抓log,也能事后从系统状态里读出线索。这个思路在排查“偶发”问题时尤其有效——你可以一直开着计数器,等出问题后再去读计数器的变化,定位出错环节。我曾经有一套设备在客户现场偶发死机,客户不让我们过去部署调试工具,最后就是靠一个暴露在sysfs下的错误计数器,让客户在出问题时跑两行命令把状态dump给我们,问题不到两天就定位了。
3.4 测试与验证策略:不要相信“功能没问题”
驱动开发工程师不是只负责把功能写出来,还要设计一套验证方案证明它能在量产环境中存活。功能验证只是第一步,更重要的几种验证类型是压力测试、边界测试、长时间稳定性测试、异常注入测试。
压力测试跑的是并发场景:同时打开多个设备节点、频繁读写、频繁开关设备、中断风暴。边界测试看的是极端参数:最大传输长度、最小timeout、最大设备数量、系统内存紧张时的行为。长时间稳定性测试至少跑48小时以上,重点观察有没有缓慢泄漏(内存、DMA描述符、中断号)、有没有状态漂移。异常注入测试则是在认为制造错误:注入I2C故障、拔插设备、模拟设备复位,验证驱动的错误恢复路径真的能兜底。
这些测试不可能全自动跑,但至少应当把核心流程用脚本自动化。跑之前,内核调试选项全部打开;测试通过之后,再用正式配置跑一轮完整回归。两条腿走路,既保证能快速定位问题,也保证最终版本接近生产配置。
4. 实操:从一个字符设备驱动的崩溃故事开始
4.1 问题场景复现
为了更直观地讲清楚“能跑会崩”的成因,我拆一个具体案例。有一款用GPIO模拟时序的传感器驱动,开发板上一切正常:数据读得出来、中断触发也灵敏、上层应用跑得很稳。到了小批量验证阶段,一个批次几十台设备,大约有三分之一的机器会在运行几小时后出现系统卡死。
这个驱动的主流程其实很简单:GPIO方向设置为输入,使能中断,中断触发后通过软件时序读取数据。第一次拿到问题的时候,我怀疑是中断配置问题,于是在测试环境里人为地反复触发外部信号,发现确实存在不稳定现象。用逻辑分析仪抓到波形后发现了一个异常:中断触发的时候,驱动正在执行读取操作,GPIO状态在两次采样之间发生了变化,导致驱动读到了一个中间值,于是数据乱了。但这些异常只是数据错误,不至于系统卡死。真正让系统卡死的深层原因,出在另一个地方:中断处理函数内部,为了获取传感器数据,驱动调用了一个带信号量锁的函数,而这个锁在正常流程中被用户线程持有。中断一进来,就等锁;用户线程因为等待中断处理结果,进入睡眠等待。这就造成了中断上下文等待锁,而持锁线程在等中断返回——经典死锁。
这个场景在开发板上是极难触发的,因为正常你单独测试时根本没有用户线程同时操作;一旦上层软件把所有功能串起来跑,就有概率出现这个死锁时序。我复盘时第一反应就是:这种写法的代码是怎么通过review的?后来看了一眼,原来是早期版本在probe阶段没有并发访问,后来加了一个sysfs属性操作,又引用了同一个锁,结果中断路径也被带进去用了。
修正方案说起来很简单:中断处理函数里只做标志位记录,实际的数据读取和业务处理全部放到workqueue或者中断线程中去执行,用mutex而不是spinlock来保护慢速数据操作。这个改动也就几十行,但把整个驱动的并发模型理顺了。这个案例再次验证了驱动开发里的一个铁律:中断上下文里面不能等锁,不能调用可能睡眠的函数。
4.2 用户态传参校验:一个指针引发的Panic
另一个特别常见的“能跑会崩”案例,是用户态传参校验不到位。很多驱动新人写ioctl和write回调时,只做了“长度对不对”的检查,用了copy_from_user之后,却不检查返回值是否等于请求的长度。一旦用户程序传进来一个非法指针、或者传一个跨用户态映射边界的缓冲区,copy_from_user就会部分失败,而驱动如果没检测到这个失败,在后面的业务逻辑里就会拿半截数据去操作硬件,轻则数据错乱,重则直接引发内核崩溃。
这里有个经验:所有从用户态进来的数据,都要假设它是不可信的。口令和访问控制、指针和长度校验、机制过滤,缺一不可。copy_from_user成功之后,对数据内容也要做合法性校验——如果期望是一个设备寄存器地址,就要检查这个地址是否在允许的区间内;如果期望是一个缓冲区大小,就要检查是不是超出硬件FIFO能承受的范围。很多安全漏洞和系统崩溃,都是从“用户态传参数时稍微越界”开始的。
我见过最离谱的一个问题,是一个应用工程师在ioctl里把参数传了个负数,驱动没有检查长度字段,直接就调用kmalloc去申请一块长度负数的内存,返回的指针非空,但大小却是个巨大值,后续的memset把整片内存写完,把相邻模块的数据全冲掉了,系统行为变得完全不可预测。量产系统里遇到这种问题,几乎没法在测试阶段暴露出来,只能在上线后稳定复现的时候再去查。
4.3 中断上下文里的“睡眠”大忌
继续聊聊Linux中断处理里最容易犯错的地方。中断处理函数(handler)运行在中断上下文,这个上下文里你不能调用任何可能休眠的函数,包括kmalloc(GFP_KERNEL)、mutex_lock、msleep、甚至copy_to/from_user。因为这些函数可能导致当前执行流被调度或者等待,而中断上下文里没有调度器的概念,一旦休眠,整个系统就可能挂死。
很多人会问,那我中断里要做大量数据处理怎么办?标准答案是:快速处理必要部分,然后把耗时的业务逻辑推迟到更友好的上下文里。常见做法有三种:workqueue、tasklet/softirq(现在内核推荐用 threaded irq)、以及内核线程。其中workqueue和threaded irq是最常用且最不容易出错的。工作中我见过一个USB驱动的bug,中断处理里调用usb_control_msg,而这个函数是可能睡眠的,在正常调试时从来没出过问题,因为USB控制传输完成了,但一旦某个USB设备无响应,usb_control_msg就会进入等待,系统瞬间卡死。后来改成在中断线程里调用这个函数,问题彻底消失。
还有一个相关细节:顶半部(hardirq handler)里不要做过多事情,尽量尽快结束。顶半部执行期间,当前CPU上其他更高优先级的中断请求会被阻塞,影响系统实时性。一个驱动的中断延迟本来就该控制在微秒级别,哪怕你只是在一个循环里读了几百次寄存器,都可能把系统其他关键的实时响应拖垮。
4.4 设备树与platform驱动匹配:为什么probe死活不跑
Linux设备驱动开发里一个非常普及的“能跑但会崩/压根跑不起来”的问题,是设备和驱动匹配不上。我经常收到这类求助:模块加载成功了,但probe函数就是不执行。查半天,发现是设备树节点里的compatible属性和驱动里of_match_table写得不一致,或者节点里少了某个驱动注册时需要的属性字段。
设备树匹配的机制其实很简单:内核遍历设备树,找到每个节点的compatible值,再去和驱动注册的of_device_id列表做匹配,匹配上了就调用probe。通常的坑是在设备树里写成了字符串不详导致的超时或不匹配状态。检查时打开kernel的device tree debug switch,或者直接看/sys/bus/platform/drivers下的symlink,就能知道匹配到了哪个驱动、哪个设备。
还有另一种常见情况是资源获取失败:驱动在probe里调用platform_get_resource,而设备树里没有定义reg和interrupts属性,或者属性名写错,导致probe返回失败。这类问题的识别方式也很直接:加载模块时看dmesg里有没有platform_probe失败的相关日志,通常打印了-ENXIO、-EBUSY这类错误码,顺着错误码排查即可。写驱动之前把设备树模板认真对一遍,这个环节至少能省掉半天排查时间。
5. 常见问题与排查技巧实录
5.1 崩溃日志:从oops/panic中抽丝剥茧
驱动的崩溃信息多数会以“Unable to handle kernel paging request at virtual address xxx”这样的oops信息出现在内核日志里。很多新手一看到一屏十六进制就慌,其实里面包含的信息特别丰富,关键是会读。
看oops日志,首先要关注的是PC(Program Counter)和LR(Link Register)的值,这两个会告诉你在哪里崩的。通过内核崩溃地址,可以定位崩溃的指令位置;接着看调用栈(Call trace),这条链路是崩溃现场的完整记录,从最底层到最上层函数调用关系都会列出来。拿回溯里的函数名和行号信息,结合addr2line工具,可以把它转换到具体代码行。如果开启了CONFIG_FRAME_POINTER,回溯的准确度会更高。
然后要把崩读到的寄存器值记下来。特别值得注意的是崩溃地址附近的东西:有可能会看到CPU在访问一个比较怪异的地址,比如0xdead000000000000之类,这种通常是对已经释放的内存进行了解除引用操作;如果访问的是0x0附近,那就是空指针解引用。两类问题在驱动里都太常见了。遇到oops先冷静下来读日志,而不是上来就改代码,大多数时候,日志里的Call trace已经指出了问题。
5.2 内核调试选项:开发阶段开全,量产阶段权衡
内核提供了非常多的调试选项,开发阶段尽早打开,量产阶段再做裁剪。我最常用的几个:CONFIG_DEBUG_ATOMIC_SLEEP用于检测在原子上下文里调用可能睡眠的函数的错误,这个选项对中断上下文问题的检出率极高;CONFIG_DEBUG_MUTEXES、CONFIG_DEBUG_SPINLOCK用于检测锁使用错误;CONFIG_KASAN(Kernel Address Sanitizer)用来检测内存越界和释放后使用,虽然会让性能大幅下降,但拿它做测试很有价值;CONFIG_KCSAN用于检测数据竞争(data race),对并发问题的排查帮助极大;CONFIG_LOCKDEP用于检测死锁和锁顺序问题。
用了这些工具,你相当于在测试阶段就把生产环境中可能会偶发的问题提前揪出来。注意一个细节:开启了调试选项的内核和正式发布内核的行为不完全一样,可能影响时序导致问题不再复现,也可能因为更慢的执行反而暴露出更多问题。所以规矩是:开发测试版本尽量把调试选项全开,最后做一轮正式配置的回归测试,两者缺一不可。
5.3 五个高频崩溃原因速查表
| 问题类别 | 典型症状 | 排查方向 | 修复思路 |
|---|---|---|---|
| 并发竞态 | 偶发数据错乱、状态异常 | 检查共享变量、中断与线程并发访问路径 | 加锁、原子操作、修改变量访问范围 |
| 缓存一致性 | DMA数据偶尔错误、性能异常 | 检查DMA缓冲区属性和Cache操作 | 用dma_alloc_coherent或做clean/invalidate |
| 原子上下文睡眠 | 系统偶发卡死、panic后Call trace指向睡眠函数 | 检查中断/softirq路径是否调用睡眠函数 | 改用workqueue、threaded irq或保留标志位延迟处理 |
| 资源泄漏 | 长时间运行后内存增长、句柄耗尽 | 检查所有kmalloc、request_irq、注册操作是否有对应释放 | 补release、free_irq、注销操作,配合kmemleak |
| 用户态传参问题 | ioctl传入异常参数后系统崩溃 | 检查copy_from_user返回值和参数范围校验 | 加强校验,拒绝非法指针、非法长度、非预期值 |
5.4 我自己的排查顺序心得
踩了这么多年坑,我养成了一个固定的排查顺序,分享出来供参考。第一步,先做最小化现场保护:收集日志、内存状态、寄存器快照,能用的dump全部留好,绝不急着重启。很多偶发问题一重启就再也复现不了,所以现场数据是整个排查最珍贵的资源。第二步,尽可能还原现场:根据问题规律确定是并发、时序、还是资源类问题,然后去内核日志和代码里找对应路径。第三步,用静态工具和review代码同步推进:sparse、smatch、cppcheck先跑一轮,然后到内核配置里打开相应调试选项跑压力测试。第四步,如果还没定位,就大胆地在关键路径加trace_printk或者临时计数器,但要明确一点:加日志本身可能掩盖问题,所以加完后要用“消除法”逐一确认。
这套流程不保证所有问题都能快速解决,但至少能避免最浪费时间的“瞎改代码碰运气”模式。驱动调试本来就是一件需要耐心的事,工程设计上的健壮性建设,能大量减少后面这种煎熬时光。
结尾:一点个人体会
我自己早年写驱动的时候,心态是“代码能跑就行”,后来被量产项目反复教育之后才明白,驱动开发的真正难点不在功能实现,而在系统性的工程化把控。你不能期待一个在理想条件下能跑的驱动,到了真实硬件环境里自动变得可靠;可靠性是一行一行代码写出来、一轮一轮测试测出来的,甚至更重要的,是一开始设计和编码时就规划出来的。
现在做项目,我会在写第一个寄存器前先问自己几个问题:这个驱动的并发模型是什么?中断路径里允许做什么样的操作?数据在CPU和DMA之间如何保持一致性?异常时如何恢复?这些问题看似增加了很多“额外”思考,但经历过几次半夜被叫起来处理产线问题之后,你会发现,这些思考恰恰是最值钱的部分。希望这个专栏写下的东西,能帮你少走一点我曾经走过的弯路。后面我会持续拆解更多具体的驱动场景和实战细节,大家有想了解的方向也可以留言,我们一条一条来聊。