ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux设备驱动开发实战:字符设备、设备树与I2C框架精讲

Linux设备驱动开发实战:字符设备、设备树与I2C框架精讲 干Linux开发这些年最常被新同事问的问题就是驱动到底怎么学面试造火箭、入职拧螺丝的情况确实多但驱动这个方向不一样它是实打实的计算机系统底层工程从按键到屏幕从I2C传感器到PCIe网卡全都离不开那几套核心框架。这篇文章我就把自己做Linux设备驱动开发的经验理一遍从环境准备到字符设备框架从设备树匹配到I2C总线模型最后把平时踩过的坑和排查套路一块儿说清楚。不管你是刚入行嵌入式的小白还是被驱动开发折腾过的老兵这篇内容都能直接拿来当参考。先说清楚一个概念Linux设备驱动本质上就是内核与硬件之间的翻译官。用户空间的应用程序通过open、read、write这些标准接口访问设备驱动负责把这些调用翻译成硬件能理解的操作再把硬件返回的数据翻译回应用程序认识的格式。这种设计的好处是应用程序不需要关心硬件长什么样驱动的编写也有章可循。Linux驱动类型大体分三种字符设备、块设备和网络设备其中字符设备最基础也最常用理解它就等于拿到了驱动开发的第一把钥匙。1. 动手前的硬准备内核源码树、交叉编译链和最小实验环境1.1 为什么必须准备内核源码树很多人学驱动开发时犯的第一个错误就是直接装个Ubuntu然后开始写模块。在PC上跑Linux内核模块其实可行但实际嵌入式开发中驱动运行在目标设备上硬件资源有限通常需要交叉编译。无论哪种场景有一点是共通的驱动模块的编译必须依赖内核源码树因为Linux内核模块不像普通C程序那样直接链接libc它需要内核头文件、编译配置和模块工具链。说得直白一点内核模块编译时要用到include/linux/module.h、include/linux/fs.h这些头文件还要读取内核的.config配置来决定某些宏开关。如果你用的内核版本和源码树版本对不上编译出来必然报错或者插入时不识别。我就见过有人拿了旧版内核源码编译新内核模块结果vermagic不一致insmod直接拒绝加载。所以在准备工作阶段两步走第一拿到目标设备对应版本的内核源码第二确认/lib/modules/$(uname -r)/build符号链接指向这套源码。在Ubuntu上有一个省事的办法sudo apt-get install linux-headers-$(uname -r)这条命令装的就是当前内核版本对应的头文件与编译配置。装好之后/lib/modules/$(uname -r)/build就能正常使用了。交叉编译的场景稍微复杂一点要在Makefile里指定ARCH和CROSS_COMPILE这两个变量比如ARM平台ARCHarm CROSS_COMPILEarm-linux-gnueabihf- make1.2 三板斧hello world模块、Makefile和insmod验证学习任何驱动第一个程序都应该是内核模块版的hello world。它虽然不操作真实硬件但能把驱动开发的完整编译流程、模块生命周期、日志输出机制全部串起来。模块的入口和出口函数不叫main而是通过module_init和module_exit指定。加载的时候内核调用入口函数卸载的时候调用出口函数。hello world模块长这样#include linux/init.h #include linux/module.h #include linux/kernel.h static int __init hello_init(void) { pr_info(hello: module loaded\n); return 0; } static void __exit hello_exit(void) { pr_info(hello: module unloaded\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple hello world module);配套的Makefile也有一套标准写法obj-m : hello.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这里-C指定进入内核源码树目录M告诉内核源码树在哪个目录编译模块。编译完生成hello.ko然后sudo insmod hello.ko dmesg | tail sudo rmmod hellodmesg能看到模块加载和卸载的日志。这一步能跑通说明内核模块开发的整个工具链没问题了。这里有个小细节__init和__exit宏不是摆设它们把函数放在专门的代码段模块加载完成或卸载之后这部分内存可以被释放对嵌入式设备来说这是实打实的内存优化。1.3 模块参数与内核打印级别的坑模块参数是交互层面的重要工具。比如调试的时候不想重新编译模块只想临时改缓冲区大小那就可以用module_param。声明方式极简单static int debug_level 0; module_param(debug_level, int, 0644);这样加载时就能insmod hello.ko debug_level3。如果参数定义成数组还有module_param_array可以用。打印级别也有讲究。printk分8个级别0是最紧急的KERN_EMERG7是KERN_DEBUG。平时调试建议用pr_info和pr_debug别偷懒直接用裸printk因为级别不写会导致默认级别混乱dmesg里全是噪声。更需要注意的是pr_debug在未打开CONFIG_DYNAMIC_DEBUG时会被编译掉所以调试阶段直接用pr_info更稳妥。2. 字符设备驱动框架把“设备”变成“文件”的完整套路2.1 file_operations应用程序与硬件的接口约定字符设备驱动的核心就是struct file_operations它是应用程序和硬件之间的方法表。应用程序调用的read、write、open、close、ioctl等系统调用最终都会走到这个结构体对应的函数指针上。举个例子应用程序执行fd open(/dev/demo, O_RDWR)时内核根据/dev/demo的设备号找到对应的驱动然后调用你注册的open函数。应用程序执行read(fd, buf, count)时内核调用你注册的read函数。这些函数运行在内核上下文中可以访问硬件寄存器、操作DMA但绝对不能直接访问用户态传进来的指针。所以每个需要交互的接口背后都有一套精心设计的结构体。最典型的写法static const struct file_operations demo_fops { .owner THIS_MODULE, .open demo_open, .release demo_release, .read demo_read, .write demo_write, .unlocked_ioctl demo_ioctl, .llseek demo_llseek, };这里unlocked_ioctl是重点。较早的内核版本用ioctl2.6.36之后改名为unlocked_ioctl名字本身说明了内核不再用全局大锁保护它驱动自己在并发场景下处理同步问题。2.2 设备号字符设备在系统中的“身份证”设备号分主设备号和次设备号。主设备号表示设备类型次设备号表示同类型中的第几个实例。比如一个驱动管理4个串口可以申请一个主设备号加4个次设备号。打印时用MAJOR()和MINOR()宏拆分组合时用MKDEV(major, minor)。申请设备号有两种方式手动指定和动态分配。手动指定就是自己选一个没被占用的主设备号然后register_chrdev_region。动态分配更推荐内核帮你找空闲的主设备号你只要调用alloc_chrdev_regionint alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);返回后*dev里就是分配到的设备号。用动态分配的原因很简单你根本不知道用户系统里已经有哪些设备号占用与其去查表不如让内核来协调。拿到设备号之后把cdev结构和设备号绑定需要三步cdev_init(demo_cdev, demo_fops); demo_cdev.owner THIS_MODULE; cdev_add(demo_cdev, dev_num, count);cdev_add执行完内核就认为这个设备已经存在了。但此时/dev下还没有节点需要device_create在类下面创建设备节点这也是现代内核udv自动生成设备节点的原理。类名的意义在于同一类设备可以共享权限策略和管理逻辑比如/sys/class/leds下面的所有LED设备。2.3 驱动生命周期init、exit与内存回收驱动模块的加载过程就是module_init指定的函数。这个函数里通常做这几件事注册设备号、初始化cdev、建立设备节点。卸载过程则严格逆序销毁设备节点、删除cdev、释放设备号。顺序绝对不能乱否则会出现节点还存在但驱动已经没了的情况用户态一open就崩。这里有一个新手常犯的错误device_create和cdev_add的先后顺序。正确做法是先cdev_add再device_create。因为cdev_add之后设备才算真正在内核里注册完毕此时创建设备节点才能正确关联到这个驱动。顺序反了设备节点可能对应一个不存在的设备号打开必然报错。另一个容易被忽视的点是自动创建设备节点依赖用户空间的udev规则。如果你在嵌入式环境里没有udev或者mdev那device_create不会产生/dev节点这时需要手动mknod /dev/demo c major 0。这个细节在开发板初期移植阶段尤其重要。3. 设备树驱动与硬件之间的“连线图”3.1 compatible匹配驱动如何找到对应硬件设备树Device Tree是现代ARM Linux中描述硬件信息的关键手段。内核通过设备树知道开发板上接了哪些外设、地址是多少、中断线接到哪里、I2C总线上挂了哪些设备。驱动则通过compatible属性来匹配自己负责的设备。设备树节点长得像这样demo_device: demo1c00000 { compatible vendor, demo-device; reg 0x1c00000 0x1000; interrupts 0 23 4; clock-frequency 1000000; };匹配过程分成两步。内核启动时遍历设备树中的compatible字符串当它找到设备时会把这个设备挂到总线上然后遍历总线上的驱动列表把每个驱动的of_match_table中的字符串和设备树节点的compatible一一比对。命中之后驱动框架调用探针函数probe。所以驱动端要做的就是用of_device_id数组声明我能支持哪些设备static const struct of_device_id demo_of_match[] { { .compatible vendor, demo-device }, { /* sentinel */ } }; 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);这里有个细节值得注意MODULE_DEVICE_TABLE宏非常重要。它把匹配表导出到模块的符号表中这样当设备热插拔时内核模块才能通过modprobe机制自动加载对应的驱动模块。偶尔有人发现驱动模块明明写了匹配表但设备来了不自动加载多半就是漏了这一行。3.2 从设备树中读取资源reg属性、中断号和GPIO设备树存在的意义就是让驱动与硬件配置解耦。寄存器地址、中断号这些信息在驱动里写死换个板子就得重新编译这就是传说中的“代码一地鸡毛”。用设备树的话驱动只需要在probe阶段读取属性即可。读取寄存器地址最常用的接口是platform_get_resourcestruct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENODEV; base devm_ioremap(pdev-dev, res-start, resource_size(res));获取中断号int irq platform_get_irq(pdev, 0);读取自定义属性u32 freq; int ret of_property_read_u32(pdev-dev.of_node, clock-frequency, freq);这里用devm_系列接口能省去大量错误处理代码。devm_ioremap、devm_kzalloc这些函数绑定了设备生命周期设备销毁时自动释放对应资源不需要在remove里手动释放少写很多容易出错的清理代码。3.3 设备树改动后的编译与验证设备树文件改完之后要么编译进内核要么以独立dtb形式被bootloader加载。调试阶段建议用后者省得频繁编译内核。编译命令简单make dtbs验证设备树有没有被正确解析几个方式配合使用。设备目录下查看ls /sys/firmware/devicetree/base/驱动程序debugfs里也有信息。还有一个实用技巧/proc/device-tree通常软链接到/sys/firmware/devicetree/base可以直接cat属性文件查看字符串。数值属性是二进制大端格式cat出来可能乱码这时用od -x或者hexdump看十六进制。4. 实操记录从零写一个按键驱动的完整过程4.1 需求定义与硬件假设这个例子我假设目标板上有三个按键GPIO接在编号A、B、C上按下为低电平松开为高电平。需求是驱动需要创建一个字符设备用户空间可以通过read读取当前按键状态也可以通过poll等待按键变化。这个场景在实际项目中很常见按键、摇杆、灯光、IO扩展芯片都走类似的思路。为什么要做成字符设备而不直接用gpio-keys子系统因为gpio-keys适合纯按键上报而我们往往还要扩展成自定义协议、组合键逻辑或者和其他业务联动有自己的字符设备接口会更灵活。驱动开发的原则是标准子系统能覆盖的用标准子系统覆盖不了或需要深度定制的就自己写。4.2 核心代码实现GPIO申请、中断与read接口GPIO申请使用内核GPIO子系统接口。现代内核推荐用gpiod_系列APIstruct gpio_desc *key_gpios[3]; key_gpios[0] devm_gpiod_get(pdev-dev, key0, GPIOD_IN); if (IS_ERR(key_gpios[0])) return PTR_ERR(key_gpios[0]);在设备树里对应key0-gpios gpio1 3 GPIO_ACTIVE_LOW;这里key0就是devm_gpiod_get第二个参数。读到GPIO后申请中断int irq gpiod_to_irq(key_gpios[0]); ret devm_request_irq(pdev-dev, irq, key_isr, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, key0, key_data);devm_request_irq的好处同样是自动释放remove的时候不用操心。中断服务函数里要注意上下文的限制。中断上下文不能调用可能睡眠的函数比如copy_to_user、kmalloc带GFP_KERNEL标志。常见做法是ISR里只是设置标志位唤醒等待队列然后使用workqueue或者tasklet做剩余处理。下面这段read接口的关键逻辑示意了等待队列的使用static ssize_t demo_read(struct file *filp, char __user *buf, size_t count, loff_t *offset) { struct demo_dev *dev filp-private_data; int val; int ret; /* 等待按键事件 */ ret wait_event_interruptible(dev-wq, dev-event_ready); if (ret) return -ERESTARTSYS; val atomic_xchg(dev-event_value, 0); dev-event_ready 0; if (copy_to_user(buf, val, sizeof(val))) return -EFAULT; return sizeof(val); }wait_event_interruptible保证用户线程在没有任何按键事件时进入睡眠而不是疯狂占用CPU轮询。中断到来之后唤醒线程这是Linux驱动中最常见的等待模型之一。poll接口的写法要使用poll_waitstatic unsigned int demo_poll(struct file *filp, struct poll_table_struct *wait) { struct demo_dev *dev filp-private_data; unsigned int mask 0; poll_wait(filp, dev-wq, wait); if (dev-event_ready) mask | POLLIN | POLLRDNORM; return mask; }4.3 编译、加载和用户态验证把代码放到内核源码树下或者按前面Makefile的方式独立编译insmod加载后要确认几件事cat /proc/devices | grep demo ls -l /dev/demo如果/proc/devices里有demo但/dev/demo不存在就是udev没接管手动mknod或者检查驱动里的device_create是否成功。如果设备节点存在那用户态验证就简单了int fd open(/dev/demo, O_RDONLY); if (fd 0) { perror(open); return -1; } int val; read(fd, val, sizeof(val)); printf(key value %d\n, val); close(fd);在有poll机制的情况下用户态可以用select或epoll等待按键事件避免持续read阻塞在其他业务线程上。项目里我用这类框架做过工业面板上的组合按键处理一套逻辑跑在ARM Cortex-A7平台上实测中断响应在微秒级稳定性完全达标。4.4 并发同步atomic、spinlock和mutex的选择驱动开发躲不开并发问题。中断、tasklet、多核同时访问临界区不处理就会数据竞争。简单地用一个原则原子操作能搞定的不要用锁读多写少的用读写锁临界区短或者会在中断里用的用自旋锁临界区长或可能睡眠的用互斥锁。按键处理里面那个event_ready标志位就用了原子操作。如果临界区复杂一些比如要更新一个缓冲区索引那推荐自旋锁spinlock_t lock; spin_lock_init(dev-lock); spin_lock_irqsave(dev-lock, flags); /* 临界区 */ spin_unlock_irqrestore(dev-lock, flags);中断上下文里访问共享数据必须用spin_lock_irqsave屏蔽掉中断避免死锁。用mutex则要小心中断上下文不能持有mutex因为mutex睡眠会导致系统卡死。5. I2C设备驱动开发总线-设备-驱动三层模型实战拆解5.1 理解I2C架构从adapter到client字符设备是我们自己创建的驱动接口而I2C设备驱动则是另一种典型它挂在内核的I2C子系统上。I2C体系分三层adapter层I2C控制器、核心层、设备驱动层。你写的传感器驱动就属于设备驱动层。adapter代表硬件上的I2C控制器它负责具体的时序生成。设备驱动层拿到一个i2c_client结构体里面包含了设备地址、adapter指针等关键信息。写I2C驱动的基本方式是定义一个i2c_driverstatic const struct i2c_device_id demo_id[] { { demo-sensor, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, demo_id); static struct i2c_driver demo_i2c_driver { .probe demo_probe, .remove demo_remove, .id_table demo_id, .driver { .name demo-sensor, .of_match_table demo_of_match, }, }; module_i2c_driver(demo_i2c_driver);如果使用设备树probe函数会在驱动匹配到设备节点时被调用无需手动“找到设备”。5.2 probe函数与设备树节点的绑定probe函数是I2C设备的业务起点。在这个函数里你可以读取设备树配置、初始化私有数据结构、设置寄存器。比如一个温度传感器temp_sensor: temp48 { compatible vendor,demo-sensor; reg 0x48; };reg是7位从机地址。probe里通过client-addr拿到这个地址。I2C标准地址是7位读写时最低位表示方向驱动开发时寄存器地址不要和总线地址搞混。probe中通常为设备分配一个私有数据结构保存client指针、锁、中断号等struct demo_sensor { struct i2c_client *client; struct mutex lock; int irq; u8 cache[16]; }; static int demo_probe(struct i2c_client *client) { struct demo_sensor *sensor; sensor devm_kzalloc(client-dev, sizeof(*sensor), GFP_KERNEL); if (!sensor) return -ENOMEM; sensor-client client; mutex_init(sensor-lock); i2c_set_clientdata(client, sensor); /* 初始化硬件寄存器等 */ return 0; }5.3 一次I2C读操作的完整流程I2C数据传输的本质是主机发送从机地址寄存器地址然后读或写数据。内核提供了i2c_transfer接口它接收一个i2c_msg数组描述一次或多次传输static int demo_read_reg(struct i2c_client *client, u8 reg, u8 *val) { struct i2c_msg msgs[2]; u8 buf reg; int ret; msgs[0].addr client-addr; msgs[0].flags 0; msgs[0].len 1; msgs[0].buf buf; msgs[1].addr client-addr; msgs[1].flags I2C_M_RD; msgs[1].len 1; msgs[1].buf val; ret i2c_transfer(client-adapter, msgs, 2); if (ret ! 2) { dev_err(client-dev, i2c transfer failed\n); return -EIO; } return 0; }写操作更简单一个msg就够static int demo_write_reg(struct i2c_client *client, u8 reg, u8 val) { u8 buf[2] { reg, val }; struct i2c_msg msg { .addr client-addr, .flags 0, .len 2, .buf buf, }; int ret i2c_transfer(client-adapter, msg, 1); return (ret 1) ? 0 : -EIO; }需要注意i2c_smbus_read_byte_data等封装函数同样可用但很多传感器的寄存器长度和地址不标准比如16位寄存器地址这时i2c_transfer更灵活。驱动里我一般直接操作i2c_msg因为它对所有I2C控制器的兼容性最好。5.4 调试I2C设备i2cdetect和逻辑分析仪驱动写完最常遇到的问题就是I2C通信不通。首先确认硬件地址对不对这可以用i2cdetect工具扫描总线i2cdetect -y 1-y跳过确认提示1是I2C总线编号。扫描出来在0x48位置有48字样说明设备在总线上响应正常。如果扫描不到设备就要查硬件连接、上拉电阻、电平匹配。软件层面i2cget和i2cset可以直接读写寄存器不用等驱动的read/write接口编好就能快速确认寄存器行为i2cget -y 1 0x48 0x00 i2cset -y 1 0x48 0x01 0xAA如果数据总是对不上逻辑分析仪是最靠谱的手段抓SDA和SCL的波形可以看起始条件、地址位、ACK/NACK是否正常。越早定位是硬件问题还是软件时序问题越省时间。6. 驱动开发中的疑难杂症与排查实录6.1 模块加载失败的几类典型模块加载失败最常见的就是“version magic不匹配”。insmod报错Invalid module format一种情况是模块编译用内核对不上另一种是没编译modpost信息。解决方法是严格使用目标内核编译模块。检查方法modinfo hello.ko | grep vermagic uname -r两者必须一致。交叉编译时要在Makefile里正确指定KDIR。还有一类是符号找不到。模块依赖其他模块导出的符号加载顺序错了就会报Unknown symbol。这时用nm查模块符号表用/proc/kallsyms确认符号是否存在。模块加载顺序问题可以用modprobe解决它能解析依赖关系。依赖关系在模块根目录下有两个文件modules.dep和modules.symbols由depmod生成。嵌入式环境搭建文件系统后一定要记得跑一遍depmod -a否则modprobe会莫名失败。6.2 设备节点不出现或操作报错设备节点不出现的原因通常是udev规则没匹配上或者也没有mdev。检查/sys/class目录和设备类是否存在ls /sys/class/demo_class/如果有但/dev/demo没生成手动mknod先验证驱动是否工作再排查udev规则。现代系统一般用CONFIG_DEVTMPFS和udev自动完成嵌入式环境里常常需要把/dev挂在内存文件系统上并把/sys挂载好。操作设备节点返回-EINVAL或-ENXIO通常是因为设备号错误或者设备节点属性不对。可以用cat /proc/devices核对主设备号然后mknod时主次设备号写对。次设备号不对也可能导致shutdown、mmap等特定功能失败。6.3 中断不触发或触发频繁中断要不触发先看cat /proc/interrupts里有没有你的中断号没有说明注册或设备树属性有问题。注册成功但一直不触发查GPIO复用配置很多SoC的PIN MUX寄存器默认不是GPIO模式。触发频繁多半是边沿设置反了或按键有抖动硬件上要加RC滤波软件上要消抖。简单消抖的做法是在ISR里加时间戳判断if (time_before(jiffies, dev-last_jiffies msecs_to_jiffies(50))) return IRQ_HANDLED; dev-last_jiffies jiffies;这种做法在生产环境中很常见成本低、效果明显。当然如果硬件允许用带锁存的中断控制器或者使用devm_request_threaded_irq配合线程化处理更稳妥。6.4 驱动调试的实用工具组合工具用得好排障效率翻倍。我的常用组合是dmesg -w监视内核日志驱动里的所有pr_info都在这里strace跟踪用户态系统调用确认read、write返回值是否正确/proc/devices看主设备号注册情况/sys/kernel/debug/下的调试信息内核开着CONFIG_DEBUG_FS时尤其好用ftrace跟踪内核函数调用定位驱动某个函数是否被调用以及执行时间perf做性能分析比如中断处理耗时太长影响系统实时性时ftrace的使用门槛不高挂载tracefs之后echo 1 /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable echo 1 /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace这样能看到所有中断处理函数的进入记录对确认中断风暴很有效。7. 几个值得特写的心得与结论之外的补充7.1 强调试能力的建立路径调试能力的本质是对内核运行机制的理解。出了bug不要急着打补丁先想清楚数据流用户态系统调用如何进入VFSVFS如何调用驱动接口驱动如何操作硬件硬件如何返回结果。这条链路中任何一个环节出错表现可能完全一样。我自己调试过不少莫名其妙的“设备挂死”问题最后定位下来不是驱动代码问题而是DMA缓冲区未对齐或者中断号冲突。这些经验只能通过一次次的现场分析积累。7.2 内核编程中的内存安全心智驱动的内存安全比应用开发严格得多。内核态没有用户态的地址空间隔离一个空指针解引用直接导致整个系统panic。这里几个习惯必须养成用户态传进来的指针用copy_from_user和copy_to_user绝不改用memcpy申请内存使用GFP_KERNEL时要注意上下文中断环境用GFP_ATOMIC注册回调函数时要确保上下文生命周期正确防止设备已经释放但回调还被调用使用container_of时确认结构体嵌入关系没有偏差copy_to_user这类接口本身会检查地址合法性并且处理缺页异常。但注意驱动里在一个临界区中反复调用copy_to_user用户态访问内存页缺页时会休眠这可能导致锁被长时间持有这是性能瓶颈也是死锁隐患。7.3 从驱动到系统设备树、内核裁剪与启动优化驱动开发不是终点一个好的嵌入式产品离不开系统层面的打磨。裁剪内核、按需配置驱动可以减少镜像体积、加快启动时间。设备树里不用的节点就不引用了驱动不编入内核用模块方式按需加载。启动阶段打印开quietinitcall_debug在调试时再开。这些优化要在产品功能稳定后做否则排查问题会寸步难行。裁剪内核不能只依赖make menuconfig手动点推荐用内核自带的最小化配置方式在板级配置基础上执行make localmodconfig生成当前加载模块的配置集再手工增删。但要注意localmodconfig在交叉编译环境里并不总是准确还是需要理解驱动依赖再逐项调整。我自己的经验是先把一个垂直功能做通比如按键驱动再扩展同类的GPIO、中断、等待队列、设备树参数这些知识点。一通百通之后I2C、SPI、平台驱动、DMA这些复杂框架学起来就会快很多因为你已经知道了套路设备注册、驱动匹配、数据通道、中断处理、资源释放。踩过几次坑之后回头看那些所谓“玄学驱动问题”大多只是某个同步条件没满足或者某个资源生命周期没管好。最后再分享一个小技巧写驱动之前先花十分钟把设备树、模块框架、数据流画在纸上哪怕就是几个方块加箭头也比直接开写代码高效得多。我见过太多同事把代码改了一百遍还在打转就是因为对整体数据流没有一个清晰的概念。驱动开发这件事框架大于细节思路大于代码量。你把这套思路拿稳了再复杂的外设都只是按图索骥。
返回列表