ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Linux设备驱动开发实战:从字符设备框架到I2C子系统与设备树

Linux设备驱动开发实战:从字符设备框架到I2C子系统与设备树 前一阵子接了一个Linux设备驱动开发的任务把一块I2C接口的传感器挂到一块基于Zynq-7020的板子上内核跑的是4.19交叉编译链用的arm-linux-gnueabihf。走完整个流程之后最大的感受是很多人学习驱动开发的时候容易一头扎进“抄代码”先把某个现成的驱动拿过来编译一下能加载就觉得自己会了结果换一块外设、换一个平台立刻傻眼。真正决定一个驱动能不能稳定工作的是驱动框架、设备树、总线模型、并发安全这几条主线。这篇文章就按我实际做项目的顺序把Linux设备驱动开发从环境搭建、字符设备框架、设备树配置、I2C子系统实战到调试排障和系统裁剪全部串起来讲一遍适合刚入行嵌入式Linux驱动、或者对字符设备驱动框架和设备树匹配还不太清晰的朋友参考。1. 接手驱动开发先别急着写代码1.1 分清设备类型选对驱动框架很多初学者拿到任务脑子里的第一反应就是“写一个驱动文件里面放open、read、write函数”。这个思路不能说错但太窄了。Linux驱动开发首先要回答的问题是你要驱动的设备属于哪一类按Linux的抽象方式设备大体分三类字符设备、块设备、网络设备。字符设备以字节流方式读写比如串口、GPIO、I2C传感器块设备以块为单位读写比如SD卡、eMMC、SSD网络设备则是以数据包为单位收发比如以太网PHY、WiFi模块。这三类设备在内核里的框架完全不同。我这次做的传感器属于字符设备家族的I2C外设所以主线就是“字符设备驱动框架 总线设备驱动模型”。这里特别想提醒一点I2C设备驱动不完全等于字符设备驱动它是挂在I2C总线上的设备驱动自身并不直接和用户空间打交道。用户空间要访问它要么通过内核生成的/dev节点要么通过I2C子系统提供的通用接口。也就是说I2C设备驱动和字符设备驱动是两套东西很多项目里最终写出来的驱动往往既要实现I2C总线侧的回调也要实现字符设备侧的操作接口。理解这个关系之后再看任何外设驱动都会觉得清晰很多。1.2 硬件信息没核对清楚后面全白写驱动开发的本质是把硬件寄存器读写动作封装成操作系统可以管理的资源。所以在写代码之前硬件信息必须精确到引脚、地址、时序。我这次拿到的传感器芯片控制寄存器简单其实不算复杂但即便这样我也先把下面几项列了一张表设备挂在哪条I2C总线上Zynq-7020从PS端的I2C0引出还是通过PL逻辑模拟出来的I2C控制器设备地址是什么7位地址还是10位地址很多时候芯片手册给的是8位写地址需要右移一位才能得到7位地址寄存器地址宽度寄存器是8位还是16位这决定了I2C消息怎么组织中断需求设备有没有中断引脚接到哪个GPIO是电平触发还是边沿触发电源域和电平转换板子上有没有关断电、是否共地、I2C上拉电阻是否满足时序要求。这些信息里任何一个错了后面调驱动都是白费劲。比如我早期就犯过一个低级错误手册上写着设备地址0x90那是8位写地址直接当成7位地址来用结果i2cdetect扫描不到设备白白排查了大半天。1.3 内核版本与工具链绑定得越死越好嵌入式Linux驱动开发有一个很反直觉的规律版本号差一个小版本编译出来的模块可能都加载不了。原因是内核模块和内核之间有“版本魔法”校验编译模块时内核源码的version magic如果和运行中的内核不一致insmod会直接报错。所以我的习惯是在项目一开始就记录三样东西内核源码的版本、实际烧录到板子上的内核镜像版本、交叉编译工具链版本尽量保证编译模块用的源码树版本和运行内核一致。如果你用的是厂商提供的SDK比如Xilinx的PetaLinux或者其它厂家的BSP通常SDK里带了对应的内核源码和工具链这种情况下直接用SDK的环境是最稳的。自己从内核官网拉一份主线的代码然后硬编一个模块去加载大概率会碰到各种头文件不一致的问题。2. 开发环境这样搭内核源码树、交叉编译与Makefile2.1 搭建交叉编译环境先验证编译器能不能用嵌入式Linux驱动不能直接在PC上编译出能在ARM板子上运行的模块因为宿主机和目标的架构不同、工具链不同。常见的交叉编译工具链名称一般是arm-linux-gnueabihf-gcc、aarch64-linux-gnu-gcc这种格式。安装完工具链之后先不要急着编译驱动先写个最简单的hello world编译一下确认工具链没问题。arm-linux-gnueabihf-gcc -o hello hello.c file hellofile命令输出里如果显示“ARM”架构说明工具链可用。这一步虽然简单但能节省后面大量排查时间。2.2 模块编译Makefile只有四句核心内核模块的编译方式与传统应用程序不同它不是直接用gcc编译而是通过内核源码树里的Kbuild系统完成。一个最基础的驱动模块Makefile长这样obj-m : sensor_drv.o KERNELDIR : /home/user/linux-xlnx CROSS_COMPILE : arm-linux-gnueabihf- CC : $(CROSS_COMPILE)gcc all: $(MAKE) -C $(KERNELDIR) M$(PWD) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) clean这里最关键的是KERNELDIR它指向内核源码树的根目录。编译模块时Kbuild会读取源码树中已经配置好的编译选项和头文件生成与你目标内核匹配的模块。如果KERNELDIR指错地方最常见的结果是编译了一堆PC端用的头文件模块加载时直接报“invalid module format”。2.3 模块的加载与卸载先确认printk能不能输出写完最简单的hello模块后先在板子上验证insmod、rmmod和dmesg流程是通的insmod hello.ko rmmod hello dmesg | tail -20如果日志里能看到module_init里的打印说明开发环境完全打通。很多人会忽略这个验证步骤直接写复杂驱动导致后面出了问题都不知道是环境的问题还是代码的问题。另外还要注意模块加载时需要依赖一些内核符号如果内核配置里把这些符号编译成了模块而不是编进内核可能出现加载顺序问题。像platform驱动这类更是要等设备模型初始化完成所以加载顺序和依赖关系也要理清楚。2.4 自动创建设备节点别再用mknod手动建字符设备驱动注册完设备号之后用户空间要访问设备需要/dev目录下有对应的设备节点。早期的方式是手动mknod现在主流做法是在驱动里动态创建设备节点利用class_create和device_create这两个函数配合用户空间的udev/mdev机制自动生成设备节点。这个机制的原理并不复杂驱动在内核里注册一个device class然后为具体设备创建device内核会向用户空间发送ueventudev监听到之后根据sysfs里的信息在/dev下创建设备节点。驱动里这样写static struct class *sensor_class; static struct device *sensor_device; sensor_class class_create(THIS_MODULE, sensor_class); sensor_device device_create(sensor_class, NULL, devno, NULL, sensor_dev);这样只要驱动加载成功/dev/sensor_dev就会自动出现不用手动建节点。对应卸载的时候要记得device_destroy和class_destroy顺序反了也会出问题。3. 字符设备驱动框架核心API与完整示例3.1 file_operations里到底要实现哪些函数字符设备驱动最核心的数据结构是file_operations它定义了用户空间调用open、read、write、ioctl、release等系统调用时在内核里最终会跳到哪些函数。多数驱动并不会把里面所有回调都实现而是按需填充。以我写的传感器驱动为例主要用到这几个回调open打开设备可以在这里做权限校验、初始化硬件release关闭设备释放资源read把传感器数据从内核拷贝到用户空间write把用户空间的配置参数写进设备unlocked_ioctl处理设备控制命令比如设置采样率、校准llseek如果有需要支持调整读写位置。这里有一个新人容易误解的点read和write函数不一定真的要操作物理硬件它们完全可以只是操作内核里的一个缓冲区。缓冲区和硬件之间的数据交互往往通过workqueue、中断或者内核线程来完成。也就是说字符设备驱动只是给用户空间开了一扇门门后面的业务逻辑可以根据设备特性灵活安排。3.2 设备号分配与cdev注册选对接口字符设备驱动要可被访问必须有设备号。早期内核提供register_chrdev函数它自动分配主设备号并且注册一组默认的file_operations用法简单但不够灵活无法支持多个子设备也无法单独管理某个次设备号。现在主流内核推荐用cdev接口alloc_chrdev_region动态申请设备号cdev_init初始化cdev结构体并绑定file_operationscdev_add把cdev添加到内核卸载时依次cdev_del、unregister_chrdev_region。动态分配设备号的好处是避免主设备号冲突。如果你写的是通用驱动最好用动态分配的方式然后通过类机制创建设备节点。如果一定要固定设备号像很多产品的私有驱动会定义主设备号那你需要确认这个设备号没有被系统占用否则cat /proc/devices可以看到冲突。3.3 一个可以直接参考的字符设备驱动骨架下面这段代码是我在Zynq平台上用过的驱动骨架逻辑不复杂方便理解整体流程#include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #include linux/slab.h #define DRIVER_NAME sensor_drv static int sensor_major; static struct class *sensor_class; static struct cdev sensor_cdev; static char *kernel_buf; static int sensor_open(struct inode *inode, struct file *filp) { pr_info(sensor open\n); return 0; } static ssize_t sensor_read(struct file *filp, char __user *buf, size_t count, loff_t *pos) { size_t size count strlen(kernel_buf) ? strlen(kernel_buf) : count; if (copy_to_user(buf, kernel_buf, size)) return -EFAULT; return size; } static ssize_t sensor_write(struct file *filp, const char __user *buf, size_t count, loff_t *pos) { if (count 1024) return -EINVAL; if (copy_from_user(kernel_buf, buf, count)) return -EFAULT; kernel_buf[count] \0; return count; } static int sensor_release(struct inode *inode, struct file *filp) { pr_info(sensor release\n); return 0; } static struct file_operations sensor_fops { .owner THIS_MODULE, .open sensor_open, .read sensor_read, .write sensor_write, .release sensor_release, }; static int __init sensor_init(void) { dev_t devno; int ret; kernel_buf kzalloc(1024, GFP_KERNEL); if (!kernel_buf) return -ENOMEM; ret alloc_chrdev_region(devno, 0, 1, DRIVER_NAME); if (ret 0) goto err_kfree; sensor_major MAJOR(devno); cdev_init(sensor_cdev, sensor_fops); sensor_cdev.owner THIS_MODULE; ret cdev_add(sensor_cdev, devno, 1); if (ret 0) goto err_unreg; sensor_class class_create(THIS_MODULE, DRIVER_NAME); if (IS_ERR(sensor_class)) { ret PTR_ERR(sensor_class); goto err_cdev_del; } device_create(sensor_class, NULL, devno, NULL, DRIVER_NAME); pr_info(sensor driver init, major%d\n, sensor_major); return 0; err_cdev_del: cdev_del(sensor_cdev); err_unreg: unregister_chrdev_region(devno, 1); err_kfree: kfree(kernel_buf); return ret; } static void __exit sensor_exit(void) { dev_t devno MKDEV(sensor_major, 0); device_destroy(sensor_class, devno); class_destroy(sensor_class); cdev_del(sensor_cdev); unregister_chrdev_region(devno, 1); kfree(kernel_buf); pr_info(sensor driver exit\n); } module_init(sensor_init); module_exit(sensor_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name);这个骨架虽然简单但已经把字符设备驱动的主要生命周期都覆盖了模块加载时申请设备号、注册cdev、创建设备节点卸载时反向操作。我建议初学者把这段代码自己敲一遍而不是直接复制因为敲的过程中你会注意到很多细节比如错误处理时资源释放的顺序。3.4 错误处理顺序决定驱动稳不稳定很多总崩溃的驱动根因都在错误处理顺序错乱。比如class_create失败后你应该cdev_del、unregister_chrdev_region、kfree这三步每一步的先后顺序其实是有讲究的先撤销对外可见的接口cdev和device再释放内存资源。如果先kfree再cdev_del理论上设备节点还能被用户空间打开一打开就会访问已经释放的内核缓冲区自然就是空指针崩溃。写驱动和写应用程序最大的区别就是应用程序崩溃了留下个core dump或者打印个堆栈就结束了驱动崩溃则可能导致整个系统panic。所以在初始化函数里每一步都要检查返回值把错误处理当作正常流程的一部分而不是补丁。4. 设备树从板级代码到描述性配置4.1 设备树到底解决什么问题早期Linux内核里要描述硬件资源和板级信息往往直接在arch/arm/mach-xxx/board-xxx.c里写一堆platform_device结构体代码。一旦硬件改版就要改内核代码重新编译非常痛苦。设备树Device TreeDT的引入就是把“硬件是什么、资源在哪里”从内核代码里剥离出来变成一份独立的数据文件系统启动时由bootloader把这份数据传给内核内核再根据设备树内容动态创建设备。这样带来的好处很直观同一份内核镜像只要设备树文件不同就能适配不同外设配置的板卡。对驱动开发者来说设备树就是硬件资源的“路由表”驱动通过设备树节点查找自己的寄存器地址、中断号、GPIO号、时钟、DMA通道等等。4.2 从设备树节点到platform_device再到驱动probe设备树的节点不会自动变成可用的设备对象中间牵涉到一套复杂的匹配流程。简单说设备树中带有compatible属性的节点在内核启动时会被解析成platform_deviceplatform驱动中通过of_match_table声明自己支持的compatible字符串当一个platform_device的compatible与驱动的of_match_table匹配上时内核就会调用驱动的probe函数。这也就解释了为什么很多新写的驱动根本没有在init里做硬件初始化所有硬件初始化全放在probe里。probe被调用的那一刻才说明驱动程序找到了对应的硬件设备。I2C设备也类似I2C子系统的核心代码会扫描设备树把I2C控制器节点下的子节点解析成i2c_client设备然后和I2C驱动声明的id_table或of_match_table做匹配。4.3 给一个I2C传感器写设备树节点以我这次传感器的设备树配置为例i2c0 { status okay; clock-frequency 100000; pinctrl-names default; pinctrl-0 pinctrl_i2c0_default; sensor: sensor48 { compatible example,temp-sensor; reg 0x48; interrupt-parent gpio0; interrupts 24 2; vcc-supply reg_3v3; }; };这里需要注意reg 0x48是7位I2C地址很多芯片手册上写的是8位地址比如0x90就要右移一位变成0x48。compatible字符串是驱动和设备树匹配的关键驱动里of_match_table里写的名字必须和设备树节点保持一致。interrupts里的第二个数字表示触发类型2一般代表下降沿触发具体含义可以查内核头文件里的IRQ_TYPE_*定义。还有人会疑惑设备树里加了一个节点但是I2C0控制器到底对应物理上哪一组引脚这个由pinctrl和相关硬件配置决定在Xilinx/Zynq这样的平台里往往需要结合PS端引脚复用配置也就是上文的pinctrl-0属性。4.4 设备树调试常用手段设备树本身也是“代码”写错了也会导致驱动不起作用。我常用下面几个手段检查设备树在bootloader里查看设备树是否被正确加载U-Boot下用fdt print /soc/i2cxxx确认节点内容内核起来后查看/proc/device-tree目录下的结构节点是否存在、属性值是否正确直接看/sys/bus/platform/devices/或/sys/bus/i2c/devices/下有没有生成对应设备。很多时候驱动probe没有被调用不是驱动代码写错而是设备树节点本身没生效。先在用户态确认设备树内容和解析结果可以避免在内核代码里瞎猜浪费时间。5. 外设驱动实战I2C设备驱动的注册与读写5.1 i2c_driver结构体和普通字符驱动差别很大I2C设备驱动要挂在I2C总线上所以不能直接写一组file_operations就完事。首先要定义一个i2c_driver结构体里面填充probe、remove、id_table或of_match_table等回调然后通过i2c_add_driver注册到I2C子系统。下面的代码是一个典型的I2C驱动注册骨架#include linux/i2c.h #include linux/module.h static int sensor_probe(struct i2c_client *client, const struct i2c_device_id *id) { pr_info(sensor probe, addr0x%02x\n, client-addr); return 0; } static void sensor_remove(struct i2c_client *client) { pr_info(sensor remove\n); } static const struct of_device_id sensor_of_match[] { { .compatible example,temp-sensor }, { } }; MODULE_DEVICE_TABLE(of, sensor_of_match); static struct i2c_driver sensor_i2c_driver { .driver { .name sensor_drv, .of_match_table sensor_of_match, }, .probe sensor_probe, .remove sensor_remove, }; module_i2c_driver(sensor_i2c_driver); MODULE_LICENSE(GPL);注意module_i2c_driver这个宏它自动把module_init和module_exit封装好了对应的注册函数就是i2c_add_driver卸载函数是i2c_del_driver。如果驱动的probe没有被调用优先怀疑设备树里的compatible字符串和of_match_table里的字符串是否一致。5.2 probe里不要只打印日志还要做资源分配和初始化很多人的probe函数只放一句打印就交差了。但实际项目里probe承担的任务很重申请GPIO、请求中断、映射寄存器、分配缓冲区、注册字符设备、创建设备节点、初始化硬件状态等。我建议probe里面模块化处理把硬件初始化拆成几个子函数比如sensor_pinctrl_init、sensor_register_map、sensor_chip_init、sensor_create_cdev每一步都做错误检查。这样出错时dmesg里能明确告诉你哪一步崩了而不是在一坨代码里靠注释找线索。资源分配之后还要记住释放顺序和字符设备驱动的错误处理逻辑一样申请顺序和释放顺序相反中断请求了就要释放GPIO申请了就要释放字符设备注册了就要删除。5.3 I2C读写数据的正确姿势I2C设备驱动的真正“手艺活”在于组织I2C消息。最简单的方式是使用i2c_master_send和i2c_master_recv适合读写设备内部单个寄存器场景static int sensor_write_reg(struct i2c_client *client, u8 reg, u8 val) { u8 buf[2] { reg, val }; return i2c_master_send(client, buf, 2); } static int sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val) { int ret; ret i2c_master_send(client, reg, 1); if (ret 0) return ret; return i2c_master_recv(client, val, 1); }如果设备数据手册里的读时序是“先写寄存器地址再读数据”那么i2c_master_send和i2c_master_recv的组合就能满足需求。如果一次操作需要“写寄存器地址后连续读多个字节”还需要把这些动作放在同一个交易里避免总线被其它设备打断这时就要用i2c_transfer组织多个i2c_msgstatic int sensor_read_blocks(struct i2c_client *client, u8 reg, u8 *buf, int len) { struct i2c_msg msgs[2] { { .addr client-addr, .flags 0, .len 1, .buf reg, }, { .addr client-addr, .flags I2C_M_RD, .len len, .buf buf, }, }; return i2c_transfer(client-adapter, msgs, 2); }当设备寄存器访问量大且寄存器很多时我更推荐内核的regmap机制。regmap把I2C、SPI、MMIO等接口统一抽象驱动里只关注寄存器地址和值不用关心底层是怎么发的。很多内核子系统都内置了regmap支持用它可以让驱动少写很多重复代码static const struct regmap_config sensor_regmap_cfg { .reg_bits 8, .val_bits 8, .max_register 0x3F, }; struct regmap *sensor_regmap devm_regmap_init_i2c(client, sensor_regmap_cfg); regmap_write(sensor_regmap, 0x01, 0x10); regmap_read(sensor_regmap, 0x00, val);5.4 I2C用户态工具驱动开发调试利器拿到一块未知I2C芯片我习惯先在用户态用i2c-tools验证物理链路再写驱动。常用命令i2cdetect -y -r 0 i2cget -y 0 0x48 0x00 i2cset -y 0 0x48 0x01 0x10i2cdetect扫描I2C0总线上的设备能直接看到0x48这个地址上有ACK响应。i2cget和i2cset可以手动读寄存器、写寄存器。如果这一步能读到正确的设备ID寄存器说明硬件连接、设备树、I2C控制器都没问题再动手写驱动就纯粹是代码层面的工作了。反过来如果你直接开始写驱动最后发现读写总是不对你根本分不清是硬件问题还是驱动问题。6. 调试三板斧日志、内核接口与故障排查6.1 printk日志级别与动态调试开关驱动开发调试printk是永远绕不开的。printk有几个常用级别KERN_DEBUG、KERN_INFO、KERN_WARNING、KERN_ERR。但新手经常遇到一个疑问我用KERN_DEBUG打印dmesg里怎么看不到因为内核默认的console_loglevel会过滤掉低优先级日志。我的习惯是开发阶段先用pr_info或dev_info把重要节点全打出来等驱动稳定后再逐步删除或改成pr_debug。如果日志太多又不想改代码可以打开内核动态调试功能通过debugfs控制某个文件里pr_debug的输出echo file sensor_drv.c p /sys/kernel/debug/dynamic_debug/control这个功能配合CONFIG_DYNAMIC_DEBUG选项内核编译的时候还要打开debugfs正式产品里可以关掉但开发期间强烈建议保留。6.2 /proc和/sys接口排障信息比你想象的丰富内核很多子系统都会在/proc和/sysfs下暴露实时信息排查问题时先看这些接口能省下很多加打印重新编译的时间/proc/devices查看已注册的字符设备和块设备号判断自己的设备号是否注册成功/proc/interrupts查看中断号、中断次数验证明中断是否触发过/proc/iomem查看内存物理地址映射情况确认寄存器地址没有被占用/sys/bus/i2c/devices/查看I2C总线上已经枚举出来的设备确认设备树解析正常/sys/class/你的类/你的设备/查看设备节点是否创建成功。如果I2C设备树节点加载了但/sys/bus/i2c/devices下没有出现对应设备基本可以断定是设备树解析失败或者bus匹配失败。先用这些接口缩小范围再回到设备树和驱动代码里定位效率会高很多。6.3 我踩过的三个最典型的驱动坑现象根因解决思路insmod报version magic不匹配模块编译用的内核源码版本和运行内核版本不一致使用与目标内核完全一致的源码树或者用SDK自带环境编译驱动加载成功但设备节点没有生成class_create或device_create失败或udev/mdev没有设置好先用dmesg查是否有错误打印再用mknod手动建节点验证I2C驱动probe没有被调用设备树compatible与驱动of_match_table不一致或者I2C节点挂在错误的控制器下检查/sys/bus/i2c/devices下设备是否存在核对compatible字符串这三个坑里面第一个是环境问题后面两个基本都是配置问题。我的经验是驱动无法加载和工作先别急着怀疑代码先把“环境、设备树、总线枚举”这三层确认完再埋头看逻辑。6.4 中断、并发与底半部机制的安全选择驱动开发里还有一个绕不开的话题中断和并发。设备中断来了中断处理函数里不能做耗时操作因为中断上下文不能睡眠不能调用任何可能睡眠的函数。所以中断处理通常分上半部和下半部上半部只做快速响应比如读状态寄存器、清除中断标志然后丢给下半部去处理数据。常见的底半部机制有软中断、tasklet、workqueue、threaded irq等。tasklet运行在软中断上下文不能睡眠workqueue运行在进程上下文可以睡眠threaded irq最适合处理那种需要和I2C通信的中断场景因为I2C传输需要睡眠等待必须放在线程化的中断上下文里。我这次驱动用的是request_threaded_irq或devm_request_threaded_irq把中断处理函数放到内核线程中执行这样中断来了以后直接在线程上下文里读写传感器寄存器不用自己额外创建工作队列代码简洁很多。并发保护方面如果驱动里存在多个进程同时读写共享缓冲区就要用互斥锁如果中断和进程上下文都会访问某些变量就要用自旋锁或者采用原子操作。选锁的基本原则是临界区很短、且上下文不允许睡眠用自旋锁临界区较长、允许睡眠用互斥锁。7. 一些值得长期保留的开发习惯7.1 内核裁剪不是删功能是关配置项目收尾时我一般会把内核里用不到的驱动和子系统裁剪掉减少内核镜像体积和启动时间。这里的思路不是“把驱动代码删掉”而是在内核配置阶段把不需要的功能的编译选项关掉。比如纯做工业控制不需要桌面相关驱动不需要网卡驱动不需要多余的文件系统模块就可以在make menuconfig里逐一关闭。裁剪时要注意保留调试接口和内核日志功能至少开发阶段不要全关。我见过有人为了追求启动时间把printk全部关掉结果上电后系统卡死跟个哑巴一样连哪里出错都查不出。裁剪要循序渐进每裁一轮就烧录验证一轮。7.2 配置项优化比抠代码更有效提到系统裁剪优化很多人第一反应是把代码写成汇编级别来优化。但对驱动开发来说真正影响整体性能的反而是内核配置。比如把内核里的printk级别调低、关掉不必要的内核调试选项、合理选择内核抢占模型、打开合适的I2C传输频率、使能DMA传输等这些配置项的效果往往比手动优化代码明显得多。我这次还顺手把内核的动态调试保留了下来但产品交付前会再评估一次是否需要关闭因为动态调试会带来少量运行时开销。裁剪和优化需要记录每一项改动的原因方便以后排查性能问题时回溯。7.3 小步快跑先把路打通再完善最后再分享一个习惯开发驱动千万不要一口吃成胖子。我会分这么几步走第一步先让模块能加载卸载打印日志第二步通过用户态i2c-tools确认物理链路和设备树解析正常第三步在驱动probe里请求资源、初始化外设第四步才去实现字符设备操作接口和中断逻辑第五步数据通路打通后再去考虑性能优化和边界情况。每一步都验证通过后再进入下一步。这种小步快跑的方式问题出现时能快速定位在哪一层不会出现一堆问题叠在一起无从下手的局面。我见过太多人一口气写完几百行驱动然后一次加载系统崩了都不知道从哪开始排查。驱动开发这件事代码量往往不大但每一步都要走得扎实一点系统才会给你正向反馈。
返回列表