
写驱动这件事说难也难说简单也简单。对上过几次内核源码的人来讲Linux驱动开发最大的痛点不是语法不是C语言技巧而是每次都要重复搭架子头文件、模块声明、设备号、cdev、file_operations、platform_driver、设备树匹配……这套流程从2.6内核到现在基本没变过核心思路但细节一直在演进。如果你手上没有一份顺手的内核驱动模板每次拿到一个新的外设芯片光是把“能加载、能卸载、open/read/write都正常”的最小框架跑通就能耗掉一整天。我写Linux驱动也有十几年的量了字符设备、platform驱动、中断底半部、DMA、procfs/debugfs这些全摸过。这篇文章就把我个人最常用的懒人模板完整拆开包含字符设备驱动模板、platform_driver与设备树匹配模板、中断与并发控制模板、Makefile与模块参数模板每一段都解释为什么这么写、哪些地方是坑供直接抄作业。1. 驱动模板的完整骨架先建好目录结构1.1 一个最小字符设备驱动长什么样Linux驱动分很多类型字符设备、块设备、网络设备、platform总线设备、I2C/SPI从设备等。我最常用的是字符设备驱动因为它最通用——GPIO控制、传感器读取、简单的LED、PWM控制、寄存器读写等等都可以用字符设备承载。一个字符设备驱动的原始骨架包含下面几块头文件与模块声明设备号申请动态/静态cdev结构体初始化与注册file_operations结构体定义模块加载/卸载函数Makefile把上面这些串起来就是一个能insmod成功、能在/dev下看到节点的最基础模板。我通常把这份模板保存成一个单独的目录每次新项目直接复制把里面的名字改掉再往里塞自己的业务逻辑。拿最核心的代码来说一个字符设备驱动的主干大概是这个样子的#include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #define DEVICE_NAME mydrv #define CLASS_NAME mydrv_class static int major; static struct class *dev_class; static struct cdev my_cdev; static dev_t dev_num; static int my_open(struct inode *inode, struct file *file) { return 0; } static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *offset) { // 拷贝数据到用户空间注意copy_to_user return 0; } static ssize_t my_write(struct file *file, const char __user *buf, size_t len, loff_t *offset) { // 从用户空间获取数据注意copy_from_user return len; } static int my_release(struct inode *inode, struct file *file) { return 0; } static struct file_operations my_fops { .owner THIS_MODULE, .open my_open, .read my_read, .write my_write, .release my_release, }; static int __init my_init(void) { // 1. 动态分配设备号 alloc_chrdev_region(dev_num, 0, 1, DEVICE_NAME); major MAJOR(dev_num); // 2. 初始化cdev并添加到内核 cdev_init(my_cdev, my_fops); cdev_add(my_cdev, dev_num, 1); // 3. 在/sys/class下创建类并创建设备节点 dev_class class_create(CLASS_NAME); device_create(dev_class, NULL, dev_num, NULL, DEVICE_NAME); return 0; } static void __exit my_exit(void) { device_destroy(dev_class, dev_num); class_destroy(dev_class); cdev_del(my_cdev); unregister_chrdev_region(dev_num, 1); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE(GPL);这个骨架从本质上回答了三个问题设备节点怎么来、用户空间的open/read/write怎么跟驱动对上、卸载驱动时怎么把资源干净地还回去。很多人刚开始写驱动最容易漏掉的是device_create和class_create这一步导致insmod之后/dev下面没有节点还得手动mknod。虽然手动mknod也能用但用udev管理设备的现代系统上不创建class和设备节点权限和自动节点都会出问题强烈不建议绕过这步。1.2 头文件的选择背后有什么讲究驱动模板里的头文件不是随便堆的每一个都有它的用途。linux/module.h是所有模块必须的它声明了module_init、module_exit、MODULE_LICENSE这些基础宏linux/fs.h里面是file_operations结构体、struct file、struct inode这些核心类型linux/cdev.h是字符设备结构体struct cdev和相关操作函数的声明linux/device.h管class_create和device_createlinux/uaccess.h则提供copy_to_user和copy_from_user。很多驱动编译报错报在隐含声明或者类型未定义八成是头文件少了。比如你在驱动里用到了class_create但没包含linux/device.h编译会直接失败。相同的道理如果你用到了ioremap需要包含linux/io.h用到了GPIO子系统需要linux/gpio.h用到了中断需要linux/interrupt.h。这个依赖关系只能靠经验积累遇到编译报错别硬猜先看是哪个函数报的错再去内核源码里搜一下它在哪个头文件声明把对应头文件加进来即可。2. 字符设备驱动的核心环节拆解2.1 设备号的分配策略动态还是静态设备号是Linux内核中字符设备的身份证由主设备号和次设备号组成。主设备号标识驱动类型次设备号标识同一驱动下的不同设备实例或子设备。动态分配用alloc_chrdev_region(dev_num, 0, 1, DEVICE_NAME)主设备号由内核自动分配一般会分到比较大的编号比如240多。动态分配的优点是永远不冲突缺点是设备节点号不固定。静态分配则用register_chrdev_region(MKDEV(major, minor), count, name)需要自己在代码里指定主设备号。我对这两种方式的态度很明确内核社区推荐动态分配但实际产品开发中我倾向使用静态分配或者至少预留静态分配的支持。原因很简单产品里的用户空间脚本、系统配置、权限规则往往写死了设备节点路径和主设备号动态分配每次开机主设备号可能不同虽然udev会自动创建设备节点但一些老旧的busybox环境或嵌入式裁剪系统里没有udev节点全靠手动mknod设备号一变就会出乱子。所以在模板中我通常是这样处理的// 优先使用固定主设备号若未定义则动态分配 #ifdef MY_MAJOR_NUM dev_num MKDEV(MY_MAJOR_NUM, 0); ret register_chrdev_region(dev_num, 1, DEVICE_NAME); #else ret alloc_chrdev_region(dev_num, 0, 1, DEVICE_NAME); #endif这样的模板在开源代码和产品代码之间切换时非常灵活。社区里大多数驱动走动态分配因为要避免占用某个主设备号造成冲突产品里则更看重稳定性。两种方案都保留编译时通过宏开关决定省心。2.2 cdev注册的完整流程与坑cdev是内核中字符设备的内部表示注册流程固定为四步分配设备号、初始化cdev、添加cdev、在/dev下生成节点。代码层面上对应alloc_chrdev_region - cdev_init - cdev_add - device_create。不过有几个细节值得单独强调。cdev_init(my_cdev, my_fops)这一步只是把fops挂到cdev结构体上并没有真正把设备加入系统。真正建立关联的是cdev_add(my_cdev, dev_num, 1)这一步对内核来说才意味着“设备上线”。一旦调用cdev_add成功内核就可以通过这个cdev把文件操作分发给你的驱动了。所以如果cdev_add之前有初始化失败unregister_chrdev_region还是安全的一旦cdev_add成功卸载时就必须cdev_del否则会出现“设备还在但驱动行为异常”的状态。还有一个很坑的点是device_create。这一步会触发udev在用户空间创建设备节点但udev是否能正确识别你的设备跟class_create时传入的class名称、以及device_create传入的device名称都有关系。我踩过最深的一次坑是在类名和设备名使用同一个字符串结果udev规则冲突设备节点意外被创建成另一个路径。所以模板里我坚持用两个不同的名字DEVICE_NAME和CLASS_NAME分开定义避免混淆。另外提醒一句device_create返回的其实是struct device *正规写法应该检查ERR_PTR。模板里为了可读性省略了错误检查但实际工程代码里一定得做。尤其当系统资源紧张时device_create可能失败如果不检查直接返回0后面用户访问设备会直接拿不到节点调试起来非常痛苦。2.3 file_operations 就是你的接口file_operations是字符设备驱动与用户空间之间的契约。用户态调用open()对应驱动里的.open回调调用read()对应.read回调以此类推。我常用的模板字段不止open/read/write/release还包括.read/.write常规的数据读写.unlocked_ioctl命令控制通道用来传非数据类指令.mmap内存映射高性能场景下常用.poll非阻塞IO的多路复用支持.open/.release资源生命周期管理这里特别提一下unlocked_ioctl。老内核里是.ioctl但内核2.6.36之后换成了.unlocked_ioctl。一字之差很多人写旧代码直接搬过来编译报错或者运行时行为不对。在unlocked_ioctl里驱动收到的参数是unsigned long arg这个arg通常是一个指向用户空间结构体的指针驱动必须用copy_from_user把数据拷进内核空间处理完再用copy_to_user传回去绝对禁止直接解引用用户空间的指针。这个坑是驱动安全的重灾区恶意用户程序传一个非法地址驱动直接解引用会触发内核崩溃整机宕机。一个完整IOCTL处理的模板片段如下static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { int ret 0; struct my_data data; switch (cmd) { case MY_IOCTL_SET: if (copy_from_user(data, (void __user *)arg, sizeof(data))) return -EFAULT; // 业务逻辑处理 break; case MY_IOCTL_GET: // 填充data if (copy_to_user((void __user *)arg, data, sizeof(data))) return -EFAULT; break; default: return -EINVAL; } return ret; }每次写新驱动我都把这段直接复制过去只改case分支。IOCTL的命令号定义用_IO、_IOW、_IOR、_IOWR这几个宏它们会把幻数、序号、方向和数据大小编码成一个整数避免不同驱动之间命令号冲突。模板里固定用#define MY_IOCTL_MAGIC M这样的幻数配合_IOW(MY_IOCTL_MAGIC, 1, struct my_data)定义命令既规范又不容易跟别的驱动撞车。3. platform_driver与设备树匹配模板3.1 为什么主流驱动都走platform_bus现在的Linux驱动绝大多数不是直接调用register_chrdev就完事而是挂在platform总线上。原因在于现代嵌入式系统的硬件信息寄存器地址、中断号、时钟频率等大多由设备树描述内核启动时会根据设备树生成一个个platform_device然后通过匹配机制找到对应的platform_driver。把硬件信息和驱动代码分离同一份驱动可以适配多个硬件平台。platform_driver模板的核心结构是#include linux/platform_device.h #include linux/of.h static int my_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; int irq; // 从设备树中获取寄存器地址 res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base); // 从设备树中获取中断号 irq platform_get_irq(pdev, 0); if (irq 0) return irq; return 0; } static int my_remove(struct platform_device *pdev) { // 清理资源devm_ 系列不手动释放 return 0; } static const struct of_device_id my_of_match[] { { .compatible vendor,my-device }, { } }; static struct platform_driver my_pdrv { .probe my_probe, .remove my_remove, .driver { .name my_device, .of_match_table my_of_match, }, }; module_platform_driver(my_pdrv);这里要解释一下module_platform_driver这个宏。它把module_init和module_exit都封装好了自动注册和注销platform_driver。收起手动写platform_driver_register和platform_driver_unregister的啰嗦代码让模板更干净。3.2 设备树匹配与资源获取的实战细节设备树匹配的关键在of_device_id表中的compatible字符串。设备树节点里必须写compatible vendor,my-device驱动的of_match_table里也必须有一模一样的字符串内核才能把设备和驱动配对。实际中我见过大量compatible字符串不匹配导致probe不执行的问题。排查手段很简单板子起来后进入/sys/bus/platform/devices查看设备节点是否存在再进/sys/bus/platform/drivers/my_device/看驱动是否绑定成功。如果设备存在但驱动没有绑定多半是compatible字符串对不上或者驱动没编进内核。节点上还要有reg属性和interrupt属性否则platform_get_resource和platform_get_irq拿不到数据。常见设备树节点模板my_device: my-device1c00000 { compatible vendor,my-device; reg 0x1c00000 0x1000; interrupts 0 42 4; };reg的第一项是寄存器物理基地址第二项是映射长度。interrupts的三项分别代表中断控制器类型、中断号和触发方式。devm_ioremap_resource做的不仅是把物理地址映射为虚拟地址还会检查资源合法性并且自动申请IOMEM区域避免跟其他驱动冲突。用devm_前缀的好处是资源跟随设备生命周期自动释放probe失败或者remove时不会泄漏。我现在的模板全都用devm_版本手动版本只在极少数特殊场景下才用。3.3 从设备树到实际驱动的数据流设备树解析的完整链路是设备树源文件.dts编译成二进制的dtbbootloader把dtb传给内核内核的驱动模型解析dtb生成platform_device然后probe函数被调用。用户在my_probe里拿到的platform_device指针携带了设备树中静态描述的资源信息。有一件事经常被忽略设备树里的寄存器地址写什么、中断号写什么取决于硬件手册和SoC的内存映射而不取决于驱动。举个例子一个UART控制器的寄存器基地址在SoC数据手册里写的是0x01C20000设备树里就写reg 0x01C20000 0x400。驱动不要自己硬编码地址也不要用#define定义寄存器地址而是从设备树里取这样换一颗芯片、改一下设备树就能复用驱动。我之前维护过一套某型号SoC的ADC驱动第一版是把寄存器地址写在驱动里的#define。后来换了同系列另一个型号寄存器基地址整体偏移了0x100000我不得不改代码重新编译。改成设备树传参之后只改了dts驱动一行没动。这就是设备树模板的意义硬件差异被隔离到设备树层驱动专注解决业务逻辑。4. 中断、锁与并发控制的模板化4.1 自旋锁和互斥锁到底怎么选驱动里处理并发是逃不掉的。用户态open/read/write可能被多进程同时调用中断上下文与进程上下文会竞争数据SMP多核环境下多个CPU同时进入驱动。不用锁数据竞争会导致系统随机崩溃或数据错乱用错锁则可能导致死锁或者实时性被破坏。锁选择的第一原则是看上下文如果是进程上下文比如read/write ioctl回调里用互斥锁struct mutex如果是中断上下文或底半部tasklet、softirq用自旋锁spinlock_t。原因在于mutex在竞争失败时会睡眠而睡眠不能发生在中断上下文。spin_lock在竞争失败时自旋忙等不睡眠适合中断上下文。我常用的模板里给驱动结构体加上一把mutex和一把spinlock分别保护不同的数据区域struct my_device { struct mutex lock; // 保护进程上下文的数据面 spinlock_t spin; // 保护中断与进程共享的状态 int status; };在read/write和ioctl入口统一mutex_lock(dev-lock)退出时mutex_unlock。中断服务程序里用spin_lock(dev-spin)保护共享状态。两个锁分工各自保护不同的字段互不交叉可以避免很多隐蔽死锁。4.2 中断申请与底半部的标准写法Linux中断处理分上半部和底半部上半部是硬中断服务程序要求短小快速只处理必须立刻响应的硬件事件耗时的业务处理丢给底半部。底半部实现机制有tasklet、工作队列workqueue、线程化中断threaded_irq几种。我的模板更推荐线程化中断原因很简单request_threaded_irq注册的中断处理函数运行在独立的线程上下文中可以直接sleep可以使用mutex代码逻辑写起来几乎跟普通函数一样不用过多考虑中断上下文限制。虽然线程化中断有额外的调度开销但对于绝大多数Linux驱动场景这个开销可以忽略。使用示例static irqreturn_t my_interrupt_handler(int irq, void *dev_id) { struct my_device *dev (struct my_device *)dev_id; // 屏蔽中断或清除中断状态位 writel(0x1, dev-base MY_IRQ_STATUS_REG); // 通知等待队列或者唤醒工作线程 complete(dev-done); return IRQ_HANDLED; } static int my_probe(struct platform_device *pdev) { struct my_device *dev; int irq, ret; irq platform_get_irq(pdev, 0); if (irq 0) return irq; ret devm_request_threaded_irq(pdev-dev, irq, NULL, my_interrupt_handler, IRQF_TRIGGER_RISING | IRQF_ONESHOT, my_device, dev); if (ret) return ret; return 0; }其中NULL作为threaded_irq的handler传给上半部表示直接使用下半部作为处理逻辑IRQF_ONESHOT则保证中断线程执行期间中断被自动屏蔽减少竞态。用devm_request_threaded_irq的好处依然是自动释放驱动卸载时不用手动调用free_irq。4.3 原子操作与读改写序列的安全保障有些场景仅仅锁还不够比如硬件寄存器中的某个状态位需要“读-改-写”如果不加保护两个CPU同时操作就会互相覆盖。这种问题用锁能解决但更优雅的做法是用内核提供的原子操作API。我举一个具体场景一个状态寄存器bit0是使能位bit1是复位位驱动要“把bit0置1同时保持bit1不变”。如果直接用readl - 修改 - writel三步在SMP下不是原子的可能被另一路并发打断。这时候可以用readl配合atomic_*系列操作或者更直接的使用set_bit/clear_bit这类的寄存器位操作。但要注意set_bit等操作在普通内存上有效不能直接在iomem映射的内存上操作。实际中驱动对寄存器操作最常见的安全方式是加自旋锁保护整段读改写序列spin_lock(dev-spin); val readl(dev-base REG_CTRL); val | BIT(0); writel(val, dev-base REG_CTRL); spin_unlock(dev-spin);这是最保险、最容易移植的做法我所有涉及寄存器读改写的地方都用这个模板。不要把readl/writel裸写到业务逻辑里建议写成封装函数比如my_set_reg_bit(dev, offset, bit)和my_clear_reg_bit(dev, offset, bit)。这样驱动代码的可读性提高不少排查寄存器操作问题时也更直观。5. 内核调试与日志输出的模板化5.1 printk的等级怎么用才不闹心驱动代码里打印日志用的是printk它跟用户态的printf长得像但多了日志等级概念。最常用的几个等级是KERN_ERR3级错误必须显眼KERN_INFO6级一般信息KERN_DEBUG7级调试信息实际开发中我打印能力开关做成模板定义几个宏#define DEV_LOG_ERR(fmt, ...) printk(KERN_ERR mydev: fmt \n, ##__VA_ARGS__) #define DEV_LOG_INFO(fmt, ...) printk(KERN_INFO mydev: fmt \n, ##__VA_ARGS__) #define DEV_LOG_DBG(fmt, ...) printk(KERN_DEBUG mydev: fmt \n, ##__VA_ARGS__)调试信息的坑在于调试完成后不清理驱动加载时刷屏真正的报错信息被淹没。现在内核鼓励用dev_dbg、dev_info、dev_err它们会自动附带设备名、platform驱动名等上下文信息比printk裸用强很多。所以我的模板里统一改用dev_info(pdev-dev, device probed\n); dev_err(pdev-dev, failed to request irq %d, ret%d\n, irq, ret);dev_系列打印在设备未注册时是不能用的这一点注意区分。模块加载早期的module_init里如果没有设备上下文还是得用printk。5.2 debugfs与procfs调试接口模板驱动开发过程中可视化查看内部状态是最有效的调试手段。procfs简单粗暴但结构混乱更推荐debugfs。debugfs的使用非常直接内核开启CONFIG_DEBUG_FS后在probe里创建设备目录在remove里释放。添加一个整型变量文件#include linux/debugfs.h struct dentry *dbgfs_dir; struct dentry *dbgfs_file; static int my_status; static int dbgfs_status_set(void *data, u64 val) { *(u64 *)data val; return 0; } static int dbgfs_status_get(void *data, u64 *val) { *val *(u64 *)data; return 0; } DEFINE_DEBUGFS_ATTRIBUTE(dbgfs_status_fops, dbgfs_status_get, dbgfs_status_set, %llu\n); // 在probe里 dbgfs_dir debugfs_create_dir(my_device, NULL); debugfs_create_file(status, 0644, dbgfs_dir, my_status, dbgfs_status_fops);我习惯在驱动的debugfs接口里导出设备状态寄存器、最近一次中断时间、读写计数器、错误计数。基本不用gdb直接读debugfs文件就能把大部分问题定位个七七八八。需要强调一点DEFINE_DEBUGFS_ATTRIBUTE要求读写函数签名是u64版本十进制读写时格式是%llu十六进制可以用%llx。调试完成启用产品后把debugfs目录创建关掉或加权限限制不要把内部寄存器裸暴露给用户。6. 编译与加载Makefile和模块参数6.1 Makefile模板一个Makefile打通内核模块编译写驱动必须面对Makefile。Linux内核模块的Makefile模板非常固定只需要把目标模块名和源文件对应关系写清楚obj-m : my_driver.o my_driver-y : my_core.o my_platform.o my_ops.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean如果驱动只有一个源文件obj-m : my_driver.o就够了多个源文件则用-y把对象文件列出来目标名取自第一个.o的名字。KDIR是内核源码目录或headers目录在Ubuntu这类发行版上指向/lib/modules/$(uname -r)/build嵌入式交叉编译时需要换成目标板对应的内核源码目录并且加上ARCH和CROSS_COMPILEARCH ? arm CROSS_COMPILE ? arm-linux-gnueabihf-交叉编译环境下最容易踩的坑是忘了指定ARCH和CROSS_COMPILE结果拿x86的编译器去编译arm内核模块编译出来的模块insmod时直接报invalid module format或者unknown symbol。检查方法是在insmod前先跑file命令看模块文件格式file my_driver.ko显示ARM格式才是对的。6.2 模块参数与加载调试驱动模板里加上模块参数可以大幅提升调试便利性。比如调试模式开关、动态指定设备号、设置超时时间static int debug_mode 0; module_param(debug_mode, int, 0644); MODULE_PARM_DESC(debug_mode, Enable debug mode (0disable, 1enable)); static int major_num -1; module_param(major_num, int, 0644);加载时指定参数insmod my_driver.ko debug_mode1 major_num240module_param的第三个参数是权限位0644表示可以在sysfs中读取和修改。调试模式通过参数动态开启后驱动里所有DEV_LOG_DBG都根据debug_mode决定是否打印比每次改代码重编译高效得多。卸载驱动时rmmod my_driver。如果提示Device or resource busy说明有进程占用了设备文件常见原因是某进程没关fd。可以用lsof /dev/my_device或者fuser -v /dev/my_device查到占用者kill掉后重新rmmod。这个排查模板适用于大部分设备占用相关问题。7. 驱动级常见问题速查与避坑手册7.1 加载与匹配阶段的疑难症状写驱动久了你会发现大部分问题集中在那几个地方整理成速查表反而比翻内核源码更高效症状最常见的根因排查与解法insmod提示Invalid module format内核版本不匹配或架构不匹配file my.ko查看模块架构检查KDIR是否对应当前内核insmod提示Unknown symbol驱动依赖的符号未导出或未加载grep symbol_name /proc/kallsyms先加载依赖模块加载成功但/dev下没节点缺少class_create/device_create或udev规则冲突dmesg查看内核日志检查节点是否被错误命名probe不执行compatible字符串不匹配或驱动未编入内核检查/sys/bus/platform/drivers下驱动是否存在open返回ENODEV设备节点存在但没有对应的设备确认cdev_add是否成功查看cat /proc/devicesread/write返回EFAULT用户空间指针非法或copy_to_user/copy_from_user失败检查用户态传入的buffer是否可读写大小是否匹配卸载时系统崩溃cdev_del/device_destroy顺序不对或并发访问未处理完检查卸载函数顺序确保释放前无并发访问每个驱动模板作者都应该保存这样一份速查表自己踩过的坑全部记下来后续项目遇到相同问题直接查表不用重新烧脑。7.2 那些经常被忽略的内核API细节有几类API的用法细节值得单独提醒因为它们的错误用法不会导致编译失败但会导致诡异运行时行为。copy_to_user和copy_from_user的返回值是未拷贝成功的字节数不是错误码。如果返回值非0表示拷贝不完整驱动要返回-EFAULT。很多人把返回值当0/1用判断条件写反导致读出全0数据或者写入了错误数据。ioremap和devm_ioremap_resource映射后的地址都是void __iomem *访问时必须用readl/writel等访问函数不能像普通内存那样直接*(volatile u32 *)addr value。有些架构上直接解引用iomem地址会得到总线错误有些架构上行为不一致表现为写入丢失或者读出错误数据。request_irq和devm_request_threaded_irq的dev_id参数不是摆设。中断共享时内核通过dev_id区分到底是哪个设备产生了中断同时free_irq也需要传递同样的dev_id才能正确释放。团队协作时有人把dev_id传成NULL一旦系统里存在共享中断IRQ线一触发就panic。DMA缓冲区与普通缓冲区差异也常被忽略。DMA操作需要物理连续的内存普通kmalloc分配的内存不一定满足连续性要求拷贝到DMA缓冲区再启动传输是通用做法。这块涉及DMA API代码更多我会在后续专门整理一篇。7.3 从模板到产品级驱动的增量工作上面的模板解决了“驱动能跑”的问题但距离产品级“稳如老狗”还差几项关键工作。第一项是错误路径的完整性。模板中我经常省略错误检查但产品代码里probe函数每一处可能失败的操作都必须检查返回值并且按照初始化逆序清理已分配的资源。用devm_系列API这个逆序清理由内核自动完成能省很多心。第二项是锁的覆盖范围要做到最小且完整。最小是指不持锁做耗时操作比如sleep、大量拷贝完整是指所有访问共享数据的路径都必须持锁包括中断和ioctl。第三项是设备节点权限与安全。产品中/dev/my_device节点的权限不要给0666默认的0644更合理配合udev规则只对特定用户组开放访问。如果设备支持ioctl中对内存的操作一定要校验用户传入的参数范围和指针合法性。第四项是电源管理回调。platform_driver结构体里可以挂.driver.pm my_dev_pm_ops实现suspend/resume回调。即使是简单的enable/disable寄存器操作没有suspend/resume的驱动在系统休眠时可能会把硬件状态搞乱唤醒后设备无法工作。我吃过教训某传感器驱动没实现suspend/resume系统休眠唤醒后传感器变成未初始化状态读数一直是0最后在resume里补了重新初始化代码才算解决。我个人在实际操作中有一个习惯每完成一个新驱动都把这个驱动的模板更新一版把新学到的API用法和踩过的坑补充进去。多年下来这份模板已经从最早的一百行代码长成了几千行的完整工程涵盖了字符设备、platform总线、设备树解析、中断线程化、debugfs、电源管理、IOMMU等一整套可复用的模板代码。新项目从复制这份模板开始核心逻辑往往几天就能写完剩下的时间都花在调硬件和验证边界条件上。驱动开发不是什么高不可攀的东西它更像搭积木内核把设备模型、内存管理、中断机制都给你搭好了你要做的只是通过一组固定的接口把硬件行为翻译给内核。而一个好的模板能让这个过程从“折腾一星期”缩短到“一天出活”。把上面的模板保存下来下次写驱动时直接套你就能把精力放在真正有价值的业务逻辑上了。