ARTICLE DETAIL

资讯详情

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

嵌入式Linux驱动开发:内核模块、设备树与I2C/CAN实战指南

嵌入式Linux驱动开发:内核模块、设备树与I2C/CAN实战指南 1. 内核模块驱动开发的起点与内核态思维1.1 驱动在系统里的位置模块机制解决了什么问题做嵌入式Linux驱动开发很多人第一反应是“写代码控制硬件”但真正上手才发现驱动开发的核心不是把寄存器读出来、写进去而是先搞懂你写的代码到底活在系统的哪一层。整个软件栈从上到下大概是应用层用户态- C库 - 系统调用 - VFS/协议层 - 驱动框架 - 硬件。驱动就是贴着硬件那一层负责把寄存器操作、中断响应、数据收发这些脏活累活封装成标准接口让上层不用关心你用的到底是I2C还是SPI也不用管芯片是瑞芯微还是全志。那内核模块呢它的本质就是“可以在运行时加载进内核的代码段”。Linux内核本身是单内核monolithic kernel所有核心功能都编译成一个大的vmlinux镜像但驱动如果也全部编进去每换一个硬件就得重新编译整个内核非常痛苦。模块机制就解决了这个问题驱动以.ko文件存在系统启动后按需insmod加载用不到就rmmod卸载不需要重启。这跟应用层的动态链接库原理类似区别在于它运行在内核态权限更高、崩溃后果也更严重——应用段错误只是一个进程挂掉内核模块写错一个地址直接整个系统panic。对初学者来说模块是进入驱动开发最好的敲门砖因为它的工程结构最简单不依赖具体硬件也能跑起来非常适合先把“内核态编程”的手感练出来。而对做产品的工程师来说模块化是基本盘调试阶段把驱动编成模块可以快速迭代量产阶段再把稳定的部分编入内核启动时直接初始化减少文件系统依赖。1.2 最小模块代码与编译流程先看一个最小可用的模块这段代码我建议新手逐行理解不要光复制#include linux/init.h #include linux/module.h #include linux/kernel.h static int __init hello_init(void) { pr_info(hello_driver: module loaded\n); return 0; } static void __exit hello_exit(void) { pr_info(hello_driver: module unloaded\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A minimal Linux driver module);module_init和module_exit是入口与出口声明__init和__exit是段属性宏告诉内核初始化函数在加载完成后就可以释放内存不用一直占着。MODULE_LICENSE不是摆设如果声明为非GPL很多内核导出的符号比如一些EXPORT_SYMBOL_GPL的函数你是无法使用的实际工程里统一写GPL最省事。编译模块用的不是普通gcc而是内核的Kbuild系统。Makefile长这样obj-m : hello_driver.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 $(KDIR)它会切到内核源码目录去执行内核的顶层Makefile然后用M$(PWD)指定你的模块源码位置。所以编译模块的前提是你安装了对应内核版本的头文件或完整源码。交叉编译场景下KDIR要指向目标平台的Linux源码树并且提前设置好交叉编译工具链比如ARCHarm64 CROSS_COMPILEaarch64-linux-gnu-。加载验证流程是三板斧sudo insmod hello_driver.ko dmesg | tail sudo rmmod hello_driver如果看到hello_driver: module loaded说明模块加载成功。一个常见的坑是insmod后没有任何输出因为pr_info走的是内核日志缓冲不直接打到终端必须用dmesg查看。有些板子上还需要dmesg -n 8或加log_buf_len启动参数不然早期日志会被覆盖。1.3 模块、字符设备与自动化生成框架模块只是“一段能加载的内核代码”它本身不提供任何对应用可见的接口。要让用户态程序真正访问你的硬件比如read、write、ioctl通常要把模块注册成一个设备。字符设备是最常见的一种——数据按字节流读写比如串口、GPIO、传感器都属于字符设备。注册一个字符设备的核心步骤是分配设备号register_chrdev_region静态指定或alloc_chrdev_region动态分配。动态分配的好处是不会跟已有设备号冲突。初始化cdev结构体并添加到内核cdev_initcdev_add。创建设备类和设备节点class_createdevice_create这样/dev/xxx会自动出现应用层才能open。这些步骤每写一个驱动都要重复一遍所以后来有了miscdevice框架——它帮你管理了主设备号统一用10你只需要注册一个struct miscdevice填上次设备号和file_operations即可。对于大多数简单外设直接用它就够了省去一堆样板代码。再往后Linux 6.x时代出现了auxiliary_bus、device_create自动设备节点等机制几个驱动框架越来越标准化但思路没变内核里每增加一个设备核心都是“设备模型”的挂载与文件操作接口的注册。从这种演进可以看出驱动开发的工程化程度很高。新手别一上来就想写一个完整的网卡驱动或者GPU驱动路径应该是最小模块 - 简单字符设备 - 绑定真实硬件 - 接入子系统框架I2C、CAN、SPI等。这个顺序能从“我会写代码”过渡到“我理解内核的设计”。2. 设备树让驱动与硬件解耦的配置语言2.1 设备树的基本结构与匹配流程在设备树Device TreeDT普及之前ARM Linux内核里充满了大量的板级硬编码代码每换一块板子就要改平台代码社区维护成本极高。设备树的核心思路是把“硬件有什么、长什么样、接在哪个地址”这类信息从C代码里剥离出来用一种类似目录树的文本格式描述编译成dtbdevice tree blob后传给内核。驱动代码只负责实现功能至于具体跑在哪块板子上由设备树决定。这跟应用开发里“配置与代码分离”的思路一致但在内核里更彻底。比如同样是I2C总线控制器芯片厂家如瑞芯微RK3568写一份驱动板厂在自己的dts里声明“I2C2上挂了某个触摸屏地址0x38”两者通过compatible这个字符串搭上线。设备树的基本结构节点包括根节点/、cpus、memory、soc等。每个节点有compatible——这是驱动匹配的钥匙reg——寄存器地址或设备地址interrupts——中断号与触发方式status——设备是否启用okay或disabled。一个典型节点的样子i2c2 { status okay; clock-frequency 400000; touchscreen38 { compatible goodix,gt911; reg 0x38; interrupt-parent gpio3; interrupts 5 IRQ_TYPE_LEVEL_LOW; reset-gpios gpio3 6 GPIO_ACTIVE_LOW; }; };驱动侧匹配时of_match_table里写{ .compatible goodix,gt911 }内核遍历挂在I2C总线上的设备发现节点compatible匹配就调用驱动的probe函数。2.2 从零添加一个设备节点的完整配置实例实际干活的时候你大概率不会从空文件写dts而是在芯片厂商提供的dtsi基础上修改。以RK3568的开发板为例芯片原厂会提供rk3568.dtsi里面定义好了各个控制器节点板级文件myboard.dts再通过#include引入并覆盖override需要修改的节点。给一个外设添加设备节点的标准流程大概是原理图确认这个设备挂在哪条I2C/SPI总线上地址是多少复位脚、中断脚接到哪个GPIO有没有使能脚需要拉高在dtsi中找到对应控制器节点确认它默认是disabled还是okay通常需要在自己板级dts里重新打开。在控制器节点下新增子节点填写compatible、reg、中断、GPIO等属性。检查引脚复用pinmux比如I2C2的引脚默认可能被配置成GPIO或UART功能必须在pinctrl里设置为i2c2功能否则总线不通。编译dtb、烧录、启动后用ls /proc/device-tree确认节点是否展开再用i2cdetect扫描设备是否在线。我经常看到初学者只做第3步结果读写失败查了半天才发现是pinmux没配。这里给个通用检查顺序先看电压和接线再看pinmux然后看时钟是否开启最后才是驱动代码。一个比较隐蔽的坑是GPIO属性与复位时序。热词里提到“linux 设备树设置复位信号时间”就是典型场景。比如传感器上电后复位脚要拉低10ms再拉高否则芯片不进入工作状态。你可以在驱动probe里用gpiod_set_value和udelay/msleep实现但如果在设备树里接了reset-gpios内核的gpio子系统会帮你管理配合reset-delay-ms等厂商自定义属性更规范。这类时序问题排查起来很难一是因为示波器不一定随时都有二是因为它只在冷启动特定阶段出现。我的经验是把上电时序相关的操作放在probe里集中处理并且用dev_dbg打印每一步的状态方便后续加延时调试。2.3 设备树编译、反编译与运行时查看设备树源文件后缀是.dts包含文件是.dtsi它们的编译工具叫dtcdevice tree compiler。在内核源码里执行make dtbs # 或单独编译 ./scripts/dtc/dtc -I dts -O dtb -o myboard.dtb myboard.dts日常调试经常需要反编译dtc -I dtb -O dts -o dump.dts myboard.dtb这样可以快速确认烧进去的dtb到底长什么样有没有被编译选项或config影响。很多问题其实在源头就能发现比如你改了dts源码但没重新编译dtb烧进去的永远是旧版本。系统启动后运行时的设备树在/proc/device-tree或更规范的/sys/firmware/devicetree/base下直接以目录树形式展开可以ls、cat查看属性。比如ls /proc/device-tree/soc/i2cfe560000/ cat /proc/device-tree/soc/i2cfe560000/status如果节点没出现说明dtb里根本没编译进去或者节点被disabled了。如果节点出现了但设备没工作再用ls /sys/bus/i2c/devices/看有没有生成对应的设备驱动有没有绑定上。这一层层往下查基本能定位是设备树问题还是驱动问题。2.4 设备树调试中常踩的坑第一个坑是地址冲突。Soc内部的很多外设控制器会映射到一段地址空间比如I2C控制器的寄存器地址段是0xfe560000如果你在dts里写错了范围或者与其他节点重叠内核在request_mem_region时会直接失败驱动probe根本走不到。第二个坑是GPIO编号混乱。在新版内核里GPIO一般用gpio3 5这样的形式但老代码里可能有gpio gpio3 5 GPIO_ACTIVE_LOW和gpios两种写法混用会导致解析失败。统一使用-gpios后缀的属性名如reset-gpios、irq-gpios并开启CONFIG_OF_GPIO。第三个坑是interrupt-cells与触发类型不匹配。有些中断控制器用#interrupt-cells 3有些用2需要看具体控制器的绑定文档。触发类型在dts里写错了不会编译报错但会导致中断完全收不到或者收到假中断风暴。这类问题我建议先在用户态用cat /proc/interrupts观察中断计数如果设备操作后计数不变多半是设备树中断配置的问题。3. I2C驱动开发从用户态试探到内核态驱动3.1 用户态先用i2c-tools验证通路在写第一行内核态I2C驱动代码之前我强烈建议先用用户态工具把硬件链路打通。这能帮你区分三类问题的边界是硬件连接问题、设备树配置问题还是驱动逻辑问题还是寄存器读写问题。有一句老话叫“先确认是硬件问题还是软件问题”I2C尤其如此因为总线时序非常依赖上拉电阻和电压匹配。i2c-tools是标配工具包包含i2cdetect、i2cget、i2cset、i2cdump这几个命令。先扫描总线i2cdetect -l i2cdetect -y 2第一条命令列出系统里所有I2C适配器第二条扫描总线2上的所有设备地址。如果扫描列表里出现预期的地址比如0x3c的OLED屏、0x38的触摸屏说明硬件通路基本正常。如果什么都没扫到先别怀疑工具查一下dts里controller节点的status是不是okaypinmux有没有配对再用示波器量SDA/SCL波形。拿到地址后可以用i2cget单字节读、i2cset写寄存器验证设备是否按datasheet响应。比如SSD1306 OLED初始化序列里的命令可以用i2cset逐条发送屏幕亮了就说明从设备的地址和基础协议没问题。这一步对后面写驱动帮助极大因为你可以拿着已验证过的寄存器序列去对照内核驱动里的初始化代码基本上是一一对应的关系。很多驱动写不好的原因不是代码能力而是从未做过这种底层验证直接把datasheet想象成代码。3.2 内核态I2C client驱动的标准骨架I2C子系统在Linux里分层非常清晰adapterI2C控制器、client挂在上面的从设备、driver从设备的驱动。适配器由芯片厂家的I2C控制器驱动注册多数情况下你不用碰你要写的是client和driver的绑定关系。一个标准I2C驱动骨架如下#include linux/i2c.h #include linux/module.h #include linux/of.h static int my_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id) { // 设备树里的属性读取 int ret 0; u32 value 0; ret device_property_read_u32(client-dev, some-flag, value); if (ret) value 0; // 默认值 // 读寄存器示例 u8 reg 0x00; u8 data 0; data i2c_smbus_read_byte_data(client, reg); if (data 0) { dev_err(client-dev, read reg 0x%02x failed\n, reg); return data; } dev_info(client-dev, probe ok, reg value 0x%02x\n, data); return 0; } static void my_i2c_remove(struct i2c_client *client) { dev_info(client-dev, remove\n); } static const struct of_device_id my_i2c_of_match[] { { .compatible myvendor,mydevice }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_i2c_of_match); static struct i2c_driver my_i2c_driver { .driver { .name my_i2c_driver, .of_match_table my_i2c_of_match, }, .probe_new my_i2c_probe, .remove my_i2c_remove, }; module_i2c_driver(my_i2c_driver); MODULE_LICENSE(GPL);of_match_table是设备树匹配的关键它的compatible必须与dts节点里的完全一致。probe函数里我用了device_property_read_u32这是通用属性读取接口与具体总线无关可以同时兼容设备树和ACPI平台推荐优先使用。i2c_smbus_read_byte_data/i2c_smbus_write_byte_data是SMBus风格的封装适合单字节读写。对于块读写或连续寻址的设备可以直接用i2c_transfer构造i2c_msg数组控制更精细。例如读传感器多字节数据struct i2c_msg msgs[2] { { .addr client-addr, .flags 0, // 写 .len 1, .buf reg, }, { .addr client-addr, .flags I2C_M_RD, // 读 .len 6, .buf rxbuf, }, }; ret i2c_transfer(client-adapter, msgs, 2);这种写法隐藏了一个问题某些I2C设备在读写之间会有地址自动递增如果没有正确处理读出来的数据就是乱的。面向寄存器读取的设备先写寄存器地址再读数据是标准操作但要注意有些设备要求带寄存器地址写操作和读操作必须连续用repeat start这和i2c_transfer两个msg在大多数控制器上的行为一致但在个别控制器上会变成stop-start导致设备跳帧。遇到这种诡异问题时得先用逻辑分析仪抓时序确认控制器的实际行为。3.3 中断与等待队列不要用忙等处理事件I2C驱动如果只是简单读寄存器probe里做一遍初始化就够了。但真实外设往往需要异步事件通知比如触摸屏有触摸事件、加速度计有运动检测、温湿度传感器可以配置阈值报警。这些场景下驱动必须处理中断。中断申请用devm_request_irq中断号可以从设备树里解析——irq_of_parse_and_map或更通用的fwnode_irq_get。中断触发方式由设备树里的interrupts属性决定。中断处理函数要遵守一个原则顶半部hardirq里不能做耗时操作要尽快返回耗时工作放到底半部tasklet或workqueue里。I2C读写本身不能直接在hardirq里做因为I2C事务可能睡眠如果底层控制器使用进程上下文等待。所以常见模式是中断里schedule_work或queue_work然后在work函数里做I2C读取、上报事件。与用户态通信的常见方式有几种miscdevice read/write/ioctl 手动实现input子系统提供标准输入事件上报或者IIO子系统提供统一的传感器接口。具体选哪个取决于设备的归类。按键、触摸屏上报用input_report_key传感器用IIO一般外设用miscdevice最灵活。选型标准就是看你的设备跟内核里现成子系统的语义是否匹配不匹配不要硬套硬套会让上层应用适配变得扭曲。另外一个很多新手会犯的错是在probe里直接做长时间I2C通信。I2C总线在某些状态下会等待总线仲裁直接阻塞会导致系统卡顿。如果初始化序列确实长比如几十条配置寄存器正确做法是放到probe里同步执行但保持单次操作短小或者干脆放到工作队列异步初始化先返回probe成功再慢慢配置。3.4 I2C调试的常用技巧I2C驱动调试时最痛苦的是设备“偶发”读写失败。我遇到过几种典型情况一种是总线频率过高。设备树里clock-frequency配了400kHzfast mode但板子走线较长、上拉电阻偏大信号质量差设备有时应答有时不应答。降到100kHz后一切正常。这种情况不一定是芯片不行单纯是板级硬件没达标。量产阶段遇到率高初期开发经常被忽略。另一种是漏了i2c_delay。有些设备datasheet写明两次操作之间需要若干微秒比如写EEPROM后要等内部写周期完成一般5ms如果驱动里连续读写第二个事务就会失败。解决方案有读出设备状态寄存器的写完成标志或直接加msleep。还有一种是重复寄存器地址冲突。多个client驱动都试图操作同一个adapter在没有正确加锁的情况下i2c_transfer会被打断。标准做法是使用i2c_transfer替代直接操作adapter的底层函数它内部有总线锁保护。如果你实在要搞原子性事务可以用i2c_lock_bus一次锁住多个答案。调试工具方面逻辑分析仪是I2C调试的利器。现在几十块钱的逻辑分析仪配合sigrok/PulseView就能解码I2C协议直接看你发的地址、寄存器地址、数据是否跟预期一致。如果没有逻辑分析仪可以用内核的i2c-trace或者打开CONFIG_I2C_DEBUG_CORE它会打印I2C传输的调用栈和消息内容。不过它打印量大适合定位协议栈层面的问题不适合抓信号时序。4. CAN总线驱动与SocketCAN应用路径4.1 CAN子系统架构与SocketCAN标准接口CAN总线在汽车电子、工业控制里用得极广跟I2C/SPI这种板内总线相比它的特点是可以长时间远距离传输、多节点通信、有完善的错误检测与仲裁机制。简单说I2C是用来“读一个传感器”的CAN是用来“一辆车里的多个ECU互相通信”的。Linux的CAN支持叫SocketCAN思路非常巧妙——它把CAN接口设计成了类似网络套接字的使用方式。应用开发者可以用socket(AF_CAN, SOCK_RAW, CAN_RAW)打开一个socket然后像收发网络包一样收发CAN报文不用关心底层是USB转CAN设备、SPI转CAN芯片还是SoC内置的CAN控制器。这种设计让CAN应用开发门槛大幅降低也意味着驱动开发者的活是把硬件控制器驱动好注册成一个标准的struct net_device让上层SocketCAN框架能识别和操作。CAN驱动的架构分三块控制器驱动通常也走struct can_priv、CAN协议层内核已实现、以及总线状态管理。芯片原厂通常已经写好控制器驱动比如RK3568内部的CAN控制器基于Bosch M_CAN或类似IP你要做的主要是设备树配置、引脚复用、时钟和速率参数而不是从零写一个控制器驱动。只有在使用外部CAN控制器芯片如MCP2515通过SPI连接时才需要自己写一个完整的CAN网络设备驱动那种情况下可以复用内核里的spi-mcp251x.c驱动改改或者参考can-dev框架。4.2 CAN节点设备树配置与控制器初始化以RK3568为例设备树里启用CAN节点通常是这样的can1 { status okay; pinctrl-0 can1m0_pins; pinctrl-names default; };这里的pinctrl可能隐藏着最大的坑。CAN是差分信号收发器transceiver需要把控制器的TX/RX转成CAN_H和CAN_L所以还要确认CAN收发器的使能脚、参考电平、终端电阻。有些板子上收发器使能脚悬空或者默认低电平总线上没有任何波形问题其实根本不在内核驱动而在硬件默认状态。控制器初始化时波特率是从用户态设置的不是编译进内核的。这是SocketCAN跟传统CAN驱动一个很大的区别。你ip link set can0 up type can bitrate 500000内核才会根据bitrate参数计算位时序并配置控制器。有些应用需要listen-only模式比如做总线监控工具可以用ip link set can0 up type can bitrate 500000 listen-only on。数据帧、远程帧的收发都走SocketCAN的接口层驱动本身的任务是保证控制器中断正常、FIFO不溢出、错误状态能上报。4.3 用户态用SocketCAN工具联调驱动配置好之后验证CAN通的步骤很简单modprobe can_dev modprobe can_raw ip link set can0 type can bitrate 500000 ip link set can0 up然后安装can-utils用cansend发送、candump监听candump can0 cansend can0 123#DEADBEEF如果你用另一个CAN设备或者同一块板子的can0和can1对接监听会看到123那条帧成功收到。这个验证过程对驱动调试很重要如果cansend一直报错错误码如果是ENETDOWN大概率是接口没起来EBUSY可能是总线繁忙ECOMM往往是控制器持续报错进入了总线关闭状态需要查硬件接线和终端电阻。CAN总线有一个必须遵守的物理层规则总线两端要各接一个120欧终端电阻。如果没接终端电阻数据收发经常时好时坏而且错误计数会持续增长。这些问题不解决任何驱动层面的努力都是白费。4.4 CAN错误处理与波特率参数计算CAN控制器硬件上会自动处理错误帧检测、错误计数驱动要做的是把错误状态反映到上层。SocketCAN里有ip -details link show can0可以查看当前错误状态。比如ip -details link show can0输出里能看到state ERROR-ACTIVE、restart-ms、berr-counter等。如果错误计数持续升高说明总线上有问题。可能是波特率不匹配、终端电阻缺失、节点间地电位差过大。波特率参数在SocketCAN里可以用以下几个参数配置ip link set can0 type can bitrate 500000 sample-point 0.875 \ tq 50 prop-seg 6 phase-seg1 7 phase-seg2 2 sjw 1如果你直接用bitrate内核会从iproute2的波特率表里选择一套默认位时序参数。对于标准应用这已经足够。如果需要定制就得理解位时序中tq时间量子、sample-point采样点的关系。采样点放在位时间75%到87.5%之间是业内常用值太靠后会因为线缆传播延迟导致采样错误。我在实际项目里遇到过一种情况两块板子A板收发正常B板上同一套代码can0却疯狂报错。最后发现B板的CAN收发器型号不同它的延迟比A板大导致采样点偏早。把sample-point从默认调低一点就通了。这类调参没有捷径只能多做几组对照并且一定要看厂家的参考设计和勘误表。5. 子系统之外的必修课裁剪移植与综合调试5.1 内核裁剪与根文件系统精简驱动开发做到中后期你一定会遇到一个需求这套系统要量产启动速度要快占用空间要小安全等级要提高。这时候就需要做内核裁剪system tailoring和根文件系统精简。内核裁剪的基本原则不是“删代码”而是“关闭CONFIG”。通过make menuconfig打开图形化配置界面把不需要的子系统、驱动、文件系统支持关掉。比如你的产品只用到I2C、CAN、GPIO完全没有USB Host、Wi-Fi、蓝牙需求那就在Device Drivers里把它们disable掉内核体积能明显下降。裁剪时要注意依赖关系。比如说你禁用了CONFIG_DRM有些图形相关的驱动会自动消失这是正常的但你如果禁用了CONFIG_NET那CAN就没法用了因为SocketCAN是基于网络协议栈的。我的建议是裁剪过程中反复执行make ARCHarm64 defconfig的对比版本来跟踪选项差异用diff管理config版本别稀里糊涂删了不该删的。根文件系统精简可以从buildroot入手。buildroot用配置文件定义需要包含哪些库、工具、应用最终生成一个极简的根文件系统镜像。如果使用Yocto则可以用IMAGE_FEATURES和PACKAGE_*变量精确控制。很多新手误以为根文件系统越“完整”越好实际量产时恰恰相反能不要的都不要比如gcc、make、perl这种开发工具绝对不能进量产镜像嵌入式环境下的包管理器也应该去掉攻击面大幅度下降。5.2 从参考板移植设备树的通用步骤实际产品开发中很少有从空白开始写设备树的场景。更常见的路径是找到一个芯片原厂的参考开发板比如RK3568的EVB在上面把系统跑通再迁移到自己的硬件上。移植设备树我总结了一套五步法用原厂的dts做基准保证内核能在这个dts下正常启动这是所有工作的前提。对照自己的硬件原理图逐项检查内存颗粒型号、DDR容量、存储介质eMMC还是SD卡。、以太网PHY型号。这几项的差异最致命配错了系统根本起不来。关掉自己的硬件上没有的外设status disabled比如参考板上的HDMI、PCIe等避免启动时驱动尝试初始化不存在的硬件而产生耗时的超时。添加自己板子独有的外设节点写清楚compatible和reg。烧录后逐个外设验证每验证一个就把一个节点的status从disabled改成okay千万别一次全打开不然出问题很难定位是哪一块导致的。有一个坑值得单独说如果参考板的核心板和自己画的板子在DDR颗粒上不同光改dts里的reg属性是不够的DDR初始化参数通常在内核启动前的loader阶段如U-Boot完成设备树只描述容量。所以这类改动要跨u-boot和设备树两层联调。我自己踩过这种坑当时反复改内核dts里的memory节点根本没效果后来才发现是U-Boot里DDR训练参数的问题。5.3 驱动工程师的调试工具箱一个成熟驱动工程师的工作台通常这类装备逻辑分析仪一台示波器一台JTAG调试器一只以及一个打包了常用调试命令的Linux主机。软件调试工具方面devmem和devkmem用来直接读写物理内存在确认寄存器状态时非常方便。devmem 0xfe560000 32 0x0000ffff devmem 0xfe560004devmem不是万能的它直接绕过设备的驱动层可能导致驱动内部状态与硬件不一致所以只建议在不影响系统稳定性的前提下使用。另一个好用的工具是ftrace它可以跟踪内核里的函数调用。在I2C/CAN驱动调试中我经常用它确认驱动里某个函数到底有没有被调用echo function /sys/kernel/debug/tracing/current_tracer echo i2c_transfer /sys/kernel/debug/tracing/set_ftrace_filter echo 1 /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace/sys/kernel/debug在量产内核里通常关闭开CONFIG_DEBUG_FS和CONFIG_DYNAMIC_DEBUG后一段时间的开发调试会轻松很多。另外/sys/kernel/debug/dri、/sys/kernel/debug/gpio等子目录对特定子系统也很有用。再推荐一个非常经典的动态调试机制——dynamic_debug。在编译时开启了CONFIG_DYNAMIC_DEBUG后可以通过下面命令动态开启某文件的调试输出echo file drivers/i2c/i2c-core-base.c p /sys/kernel/debug/dynamic_debug/control这样不用重新编译内核就能看到对应文件里的dev_dbg输出比加printk再编译快一个量级。5.4 常见问题的排查思路汇总把我在驱动开发里碰到的典型问题汇总成一张速查表供实际操作时对照现象可能原因排查方向insmod加载失败lkm提示“Invalid module format”内核版本不匹配或未开启模块支持用uname -r核对KDIR路径probe没有被调用compatible不匹配或节点disabled查看/proc/device-tree确认节点状态I2C扫描不到设备接线错误、上拉电阻缺失、pinmux错误示波器看SDA/SCL波形I2C读写时好时坏时钟频率过高、电源不稳定、总线竞争降到100kHz试查电源纹波CAN启动报错波特率不匹配、终端电阻缺失用ip -details link show查错误计数中断一直触发设备树触发方式配错、IRQF_TRIGGER不一致比较设备树与request_irq参数系统启动后dtb加载失败dts语法错误、dtc编译告警被忽略用make dtbs检查告警内核panic但日志丢失串口没接到console、log_buf太短加consolettyS0启动参数GPIO操作无效引脚复用被占用、gpiochip编号不对cat /sys/kernel/debug/gpio确认状态这张表解决的是“从现象到方向”的问题真正定位时还要结合dmesg、逻辑分析仪、动态调试输出逐层验证。凡是能用逻辑分析仪看物理信号的就不要只靠猜。凡是要改硬件的先看原理图和勘误表。6. 一些实际工程上的体会最后根据自己的开发经验聊几点心得供正在往嵌入式Linux驱动方向走的朋友参考。第一驱动开发的本质不是写代码而是读文档和读寄存器。datasheet读不懂代码写得再漂亮也没用。尤其I2C/CAN这类时序型协议认真阅读芯片手册里的时序图、电气特性、寄存器定义能省掉大量的盲目尝试。第二要习惯“分层排查”。遇到一个功能没打通先画一条链路图应用 - 接口层 - 驱动 - 总线 - 硬件。然后从最接近硬件的那一层开始验证。用户态能做的验证绝不动内核态内核态能做的验证绝不重新编译整个系统。以I2C为例先用i2c-tools确认再写驱动以CAN为例先配上位时序再分析消息收发。每层通了再往上走。第三版本管理非常重要。内核源码和dts文件要纳入git管理而且每次改动都要有记录。很多时候驱动写好了过几天又坏了一查发现是dts被同事合并分支时覆盖了。没有版本管理这种事你要白干好几天。最后这个领域的学习曲线是真实的但正反馈也很强。当你看到自己写的第一条I2C消息被设备正确响应、第一条CAN帧在总线上被另一个节点收到时那种感觉和“跑通一个Web页面”完全不同。它意味着你在跟真实的物理世界打交道你的代码真的驱动了一颗芯片、一根总线、一台机器。从最简单的内核模块起步到设备树、I2C、CAN一条路走下来你会逐渐熟悉内核的设计哲学一切皆为文件、配置与代码分离、层次分明、机制与策略分离。这套体系不是一天建成的但搞懂底层逻辑之后再换芯片平台、再学SPI/USB/PCIe驱动就只是换一套接口和协议的问题了。
返回列表