十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Linux设备驱动开发实战:从内核模块到设备树与I2C/CAN总线

Linux设备驱动开发实战:从内核模块到设备树与I2C/CAN总线 做Linux设备驱动开发这些年我最深的一个体会是这行当不像写单片机裸机代码那样可以“一把梭”它本质上是一条环环相扣的系统路径——从内核模块这个最小单位出发经由设备树的硬件描述再到I2C、CAN这些具体总线上的驱动落地每一层都建立在前一层的正确基础上。很多刚转入嵌入式Linux的朋友拿着一本《linux设备驱动开发详解》从第一页啃到最后一页示例代码抄了一堆可实际拿到一块板子还是点不亮任何外设问题十有八九就出在“只见树木不见森林”你写了I2C设备驱动却不清楚设备树里那个compatible字符串和驱动里的of_match_table是怎么匹配起来的你照着文档配了CAN节点却不知道bitrate和采样点为什么会影响通信稳定性。这篇文章我想用一条主线把这些东西串起来讲先搞清楚内核模块怎么写、怎么加载再看设备树如何描述硬件并把设备“交给”对应的驱动然后分别走进I2C和CAN两条总线的内部看协议层的核心机制和Linux子系统如何把驱动开发变得规范。最后给出一段在瑞芯微RK3568平台上同时验证I2C传感器和CAN通信的实操记录以及我在多次bring-up过程中踩过的坑。如果你正准备从MCU裸机转向嵌入式Linux或者已经入行但总在设备树匹配、I2C枚举、CAN通信这几个环节里打转这篇文章应该能帮你把整条链路理顺。1. 为什么说这是一条“系统路径”1.1 从裸机思维到Linux思维的切换先聊一个很多转行过来的人都会经历的坎。在STM32这类MCU上做驱动你写的是寄存器GPIOx-CRL、I2C1-CR1打开时钟、配置模式、读写数据一切都在你的掌控里整个程序是一个大循环外设是“我的”。到了Linux下这套玩法直接失效。内核是抢占式多任务的你的设备随时可能被调度出去外设资源要用内核的并发机制保护更关键的是硬件描述和驱动代码被彻底拆开了。Linux驱动的开发范式变成了设备树负责“告诉内核硬件长什么样”驱动程序负责“告诉内核怎么操作这类硬件”内核的总线模型负责把两者撮合到一起。如果你还用裸机的思路去写驱动——拿到一个寄存器地址就ioremap然后闷头读写结果往往是驱动模块能加载但一旦和内核里已有的驱动冲突或者换一个板子改一下引脚就全乱套。所以我说理解这条“系统路径”的顺序比背一百个API都重要。从一个模块到一组设备树节点再到一条具体总线上的设备驱动它是一条需要整体理解的技术路线不是几个孤立的知识点。1.2 总线—设备—驱动模型是整个体系的骨架Linux内核的设备模型围绕三条主线展开总线bus、设备device、驱动driver。拿I2C总线举例I2C控制器驱动会注册一个adapter适配器它代表总线本身挂在总线上的传感器芯片则被抽象为client客户端设备而你写的传感器驱动是一个i2c_driver。当内核在设备树里发现一个compatible为“ti,tmp102”的节点时I2C核心会创建一个client结构然后去匹配已经注册的i2c_driver匹配成功就调用驱动的probe函数——你的设备正式“上岗”。平台设备也是一样的套路只不过挂在了platform总线上。CAN控制器、串口控制器、GPIO控制器在设备树里通常都是platform_device对应的驱动是platform_driver匹配方式同样是compatible字符串。这个“总线—设备—驱动”模型就是Linux能支撑上千种不同硬件却保持驱动代码相对独立的原因。读懂它你就能明白设备树里每一个属性最终去哪了也能明白为什么“明明驱动对了设备却不probe”多半是设备树没配对。后面几节的内容其实都是在往这个骨架上填肉。2. 内核模块驱动的起点也是最小的“载体”2.1 模块到底是个什么东西Linux内核本身是一个静态链接的镜像如果所有驱动都编进去内核会臃肿且改起来很麻烦。模块.ko文件就是运行时可加载的代码段它并不是一个独立的进程而是被内核查入到内核地址空间、以内核身份运行的一组函数集合。驱动、文件系统、网络协议都可以做成模块需要时insmod不用时rmmod极大方便了开发和现场维护。写模块先记住入口和出口两个函数module_init()指定加载时执行的函数module_exit()指定卸载时执行的函数。加载函数负责初始化硬件、申请资源、注册设备到子系统卸载函数负责释放资源和反注册。除了这两个函数MODULE_LICENSE(GPL)这类声明也不是摆设没声明GPL的模块在使用某些内核导出符号时会被拒而且会污染内核后续排查问题时别人看到“tainted”标记都会多留个心眼。2.2 最小模块的代码与Makefile这里给出一个最精简但完整的例子我在x86和ARM板子上都验证过// hello.c #include linux/init.h #include linux/module.h #include linux/kernel.h static int __init hello_init(void) { printk(KERN_INFO [hello] driver init, hello from kernel module\n); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO [hello] driver exit, bye\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(your name); MODULE_DESCRIPTION(hello module for demo);配套的Makefile注意内核源码路径要和当前运行的内核版本一致# Makefile obj-m : hello.o KDIR : /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean编译后得到一个hello.ko然后sudo insmod hello.ko dmesg | tail sudo rmmod hello这里有个容易忽略的细节printk的日志级别。KERN_INFO的结果如果没有出现在dmesg里多半是被日志级别过滤了。调试阶段我习惯用pr_info或者直接pr_debug再配合动态调试开关生产代码里别乱刷日志日志刷太狠会拖慢整个系统。2.3 模块参数、版本匹配与引用计数模块开发中常见的三个坑我逐一踩过第一个是vermagic不匹配。加载模块时如果报“version magic 5.10.110 should be 5.10.110-xxx”之类的错误说明你编译模块用的内核源码和你板上运行的内核镜像不一致。解决办法是确保开发板上的内核就是编模块时用的那套源码而不是随便拿一个同版本号但打了不同补丁的内核目录去凑数。第二个是模块引用计数。insmod一个模块后如果它注册了字符设备或文件操作rmmod时内核会检查引用计数被占用时会提示“Module is in use”。这不是bug是保护机制别用rmmod -f硬来先解决谁占用了它。我遇到过不少新手直接暴力卸载结果还在使用设备文件的应用立刻段错误现场很尴尬。第三个是__init和__exit宏的语义。带__init的函数在模块加载后会被释放掉省内存但如果你的init函数里用了__initdata修饰的全局变量在probe阶段再去访问就踩了“use-after-free”。在设备驱动里probe函数不是init函数调用时机完全不同这个细节特别容易被忽略。模块只是起点接下来进入真正的主题设备树。3. 设备树把硬件描述从代码里搬出去3.1 设备树解决什么问题在设备树出现之前ARM平台的内核里充斥着各种board-xxx.c文件硬件稍有变化就要改C代码重新编译内核主板厂商和芯片厂商的代码互相纠缠维护成本越来越高。设备树Device Tree的初衷就是用一套文本描述硬件拓扑——CPU、内存、外设挂在哪条总线上、中断号多少、时钟频率多少——由内核在启动时解析成设备结构再与驱动匹配。硬件变了多数场景下改一个.dts重新编译dtb就行驱动代码不用动。这条链路是.dts源文件用dtc编译器生成.dtb二进制bootloaderU-Boot等在启动内核时把dtb加载到内存并传给内核内核的驱动模型把dtb解析成device节点再按compatible字段匹配驱动。所以调试设备树问题经常要回溯到三个环节dts写没写对、dtb有没有正确传给内核、驱动里的匹配字符串和dts是否一致。很多人在dts上折腾半天最后发现U-Boot根本没加载他改过的dtb这种情况我见得太多了。3.2 设备树的基础语法与常用属性设备树用树形结构描述硬件最基本单元是节点node和属性property。一个节点有名字、路径和若干属性值。举个例子/dts-v1/; / { model MyBoard; compatible myvendor,myboard, rockchip,rk3568; chosen { bootargs ...; }; i2c0: i2cff610000 { compatible snps,designware-i2c; reg 0xff610000 0x1000; interrupts GIC_SPI 95 IRQ_TYPE_LEVEL_HIGH; clocks cru CLK_I2C0; clock-names clk_i2c; pinctrl-names default; pinctrl-0 i2c0_xfer; #address-cells 1; #size-cells 0; tmp10248 { compatible ti,tmp102; reg 0x48; }; }; };几个必须理解的点compatible是匹配驱动的最关键字段格式一般是“厂商,芯片型号”驱动里的of_device_id必须与其完全一致。它也是驱动自动加载的依据之一。reg表示设备的寄存器基地址和长度父节点的#address-cells和#size-cells决定了这里每个cell的含义。在I2C子节点里reg就是7位从机地址。interrupts表示中断号。数值的含义取决于父中断控制器的#interrupt-cellsARM GIC通常是三个cell中断类型、中断号、触发方式。上面代码里的GIC_SPI、IRQ_TYPE_LEVEL_HIGH是头文件里定义的宏展开后就是具体的数值。pinctrl-0用于引脚复用这是ARM平台bring-up的高频出错点。引脚没配对I2C的SCL/SDA就是悬空或冲突的。status属性控制节点是否生效。我见过太多人改了设备树忘了把status设为“okay”结果节点一直没被解析。3.3 I2C设备的设备树写法要点I2C子节点的写法相对固定在父I2C控制器节点下直接放一个子节点子节点名无所谓但compatible和reg必须准确。reg是这个器件在总线上的7位从地址注意设备树里用的是不带读写位的地址。很多国产传感器手册给出的地址是8位格式例如0x90换算成7位就是0x48如果换算错了i2cdetect怎么扫都见不到设备。大小端、地址位宽都要留意这是枚举失败的第一大原因。如果你的设备还用到中断引脚、复位引脚、供电控制引脚都需要在子节点里补上。比如一个触摸屏的I2C设备通常会有interrupt-parent/interrupts还可能有reset-gpios属性。gpios属性用的是“gpioX 引脚号 标志”的形式驱动里用devm_gpiod_get_optional()等接口去获取。设备树里的描述越完整驱动代码越“通用”这也是平台级设备树复用性好、板级dts只需要做小改的原因。3.4 CAN控制器的设备树配置CAN控制器在设备树里通常是挂在SOC内部总线如APB上的平台设备。以常见的M_CAN IP核为例节点里需要reg、interrupts、clocks/clock-names、pinctrl有的还需要“bosch,mram-cfg”这类控制器专用属性。以瑞芯微RK3568为例它的CAN控制器节点在SoC的.dtsi里已经定义好板级.dts里需要做两件事使能对应节点配置引脚复用can0 { status okay; pinctrl-names default; pinctrl-0 can0m0_pins; /* 波特率通常在应用层配置部分平台也可在驱动中预设 */ }; can1 { status disabled; };实际操作中CAN节点bring-up最容易出岔子的地方就是pinctrl。同一组CAN引脚在SoC上往往有多个mux选项m0/m1如果硬件上用的是m1引脚设备树里却配了m0控制器虽然能up但总线上永远没有波形。这时候用示波器量CAN_H/CAN_L之间有没有差分信号是最快的判断手段。设备树这部分讲完下面进入两条总线的正文先看I2C。4. I2C子系统从协议时序到设备驱动落地4.1 I2C协议核心机制I2C是飞利浦现NXP发明的两线制串行总线SCL时钟线和SDA数据线都是开漏结构靠上拉电阻实现高电平。理解I2C协议最核心的是“时序地址应答”三个概念。起始条件SCL为高时SDA由高变低停止条件SCL为高时SDA由低变高。总线空闲时SCL和SDA都保持高。地址字节主机发送起始条件后先发送7位从机地址1位读写标志0写1读组成第一个字节。从机地址匹配后在第9个时钟周期拉低SDA表示ACK。数据字节每个字节高位在前从机每收到一个字节都需要回ACK。ACK0表示接受NACK1表示结束或异常。读操作读多字节通常是“先发寄存器地址再重发起始条件读数据”也就是repeated start。很多驱动就因为时序里少了这个repeated start读出来的数据全错。I2C的时序细节你可以参考各类“i2c时序图”资料这里说一个实战经验在Linux驱动里基本不需要自己操作时序i2c-core会封装好i2c_transfer()但在MCU裸机或I2C IP验证比如用Verilog写I2C读写EEPROM代码时时序就靠手工翻转GPIO模拟对建立时间、保持时间的把控才是真正考验。如果你之前在ESP-IDF这类MCU环境里折腾过I2C会有主从模式的概念但Linux下内核已经把所有通道抽象成adapter两个I2C接口就是i2c-0、i2c-1两个独立总线用法完全统一。Linux下遇到I2C通信不稳定先拿示波器看波形确认SCL/SDA的上升沿是否受上拉电阻影响。通常4.7kΩ上拉到3.3V在400kHz下问题不大走线长或挂载设备多时需要适当减小上拉电阻如2.2kΩ。4.2 Linux I2C子系统分层Linux的I2C子系统分三层I2C核心i2c-core、I2C控制器驱动adapter即I2C主机控制器和I2C设备驱动client。我们平时说的“I2C设备驱动开发”绝大多数场景指的是写client驱动也就是挂在I2C总线上的某个具体芯片的驱动。内核提供了一套完整的APIdevice与driver的匹配、probe调用、读写传输都有标准套路。控制器驱动adapter通常由SoC厂商提供比如DesignWare I2C IP对应的drivers/i2c/busses/i2c-designware-*.c一般不需要我们动。你只要在设备树里把控制器使能、引脚配好然后专注写自己的client驱动就行。这也是我一直建议初学者不要一上来啃控制器驱动的原因——先把client驱动的编写搞清楚传感器、EEPROM、PMIC、显示屏驱动基本都是这个套路一通百通。4.3 一个经典I2C设备驱动的骨架以TI的TMP102温度传感器为例完整的驱动可以拆成这么几块#include linux/module.h #include linux/i2c.h #include linux/of.h #include linux/hwmon.h #include linux/err.h struct tmp102_data { struct i2c_client *client; struct mutex update_lock; int temperature_milli; }; static int tmp102_read_temperature(struct tmp102_data *data) { struct i2c_client *client >i2cdetect -y 0 i2cget -y 0 0x48 0x00 i2cset -y 0 0x48 0x01 0x60如果i2cdetect能枚举出0x48说明硬件链路和I2C控制器工作正常如果枚举不到先怀疑硬件连接、上拉电阻和从机地址换算再怀疑设备树引脚配置。另外市面上最常见的0.96寸OLED屏SSD1306也是典型的I2C设备调试阶段很多人不是先写驱动而是直接通过/dev/i2c-N用i2ctransfer向它发命令序列点亮屏幕确认链路OK后再写正式驱动。记住这个排查顺序能帮你省下大量时间。5. CAN总线从仲裁机制到SocketCAN应用5.1 CAN协议与“非破坏性仲裁”CAN全称是Controller Area Network最早是博世为汽车电子设计的现在几乎成了工业控制、机器人、车载通信的事实标准。CAN是半双工、多主的总线任何节点都可以在总线空闲时发送同时监听总线这就引出了它最精彩的设计——“非破坏性仲裁”。CAN物理层用显性dominant逻辑0和隐性recessive逻辑1表示位显性优先于隐性。当多个节点同时发送时每个节点在发仲裁场报文ID的同时监测总线状态如果自己发送的是隐性位但总线上被别的节点拉成了显性说明有更高优先级ID数值更小的报文在发送这个节点立刻停止发送转入接收。整个过程不会破坏任何报文的完整性这就是“非破坏性”的含义。因此CAN报文ID数值越小优先级越高。设计应用层协议时要把最紧急的控制报文分配小的ID。CAN 2.0A用11位标准IDCAN 2.0B用29位扩展ID。报文格式除了ID还有DLC数据长度代码0~8字节、数据场、CRC校验场、ACK场和EOF。CAN FDFlexible Data-rate把数据场扩展到最多64字节并支持可变波特率的数据段。如果你的网络里混有老节点就要注意把支持FD的节点配置成经典CAN模式否则版本不兼容会导致通信失败。5.2 采样点与位时序BS1/BS2CAN通信速率不是瓶颈真正有讲究的是“采样点”。一个CAN位时间由同步段SYNC_SEG固定1个时间量子、传播段、相位缓冲段1BS1和相位缓冲段2BS2组成采样点就在BS1和BS2的交界处。采样点设置得靠前还是靠后直接影响总线较长、波特率较高时的抗干扰能力业内一般推荐75%到87.5%。ST的STM32系列、NXP的FlexCAN、以及Linux下常见的M_CAN都允许通过寄存器配置BS1、BS2、SJW同步跳转宽度和预分频器。Linux中SocketCAN一般通过设置bitrate和sample-point来间接配置例如ip link set can0 up type can bitrate 500000 sample-point 0.75如果你发现CAN总线偶发错误帧、重传频繁但物理层看起来没问题优先排查采样点。总线长度越长、波特率越高越需要适当提前采样点多个节点必须使用相同的位时序参数否则即使波特率标称一致也可能在总线上产生位错误。关于BS1、BS2的时钟处理和延迟要求需要结合具体控制器的参考手册计算不能只看标称波特率。5.3 Linux SocketCAN架构Linux从2.6.25内核开始正式支持SocketCAN它把CAN总线抽象成了网络设备。驱动层面CAN控制器驱动注册成net_device代表一个can0/can1接口应用层面你可以用标准的socket接口收发CAN帧和读写TCP/IP socket的体验类似。应用编程的基本流程#include stdio.h #include string.h #include unistd.h #include sys/socket.h #include net/if.h #include sys/ioctl.h #include linux/can.h #include linux/can/raw.h int main(void) { int s; struct sockaddr_can addr; struct ifreq ifr; struct can_frame frame; s socket(PF_CAN, SOCK_RAW, CAN_RAW); strcpy(ifr.ifr_name, can0); ioctl(s, SIOCGIFINDEX, ifr); memset(addr, 0, sizeof(addr)); addr.can_family AF_CAN; addr.can_ifindex ifr.ifr_index; bind(s, (struct sockaddr *)addr, sizeof(addr)); /* send a frame */ memset(frame, 0, sizeof(frame)); frame.can_id 0x123; frame.can_dlc 3; frame.data[0] 0x01; frame.data[1] 0x02; frame.data[2] 0x03; write(s, frame, sizeof(frame)); /* receive a frame */ read(s, frame, sizeof(frame)); printf(id0x%03X dlc%d data%x %x %x\n, frame.can_id, frame.can_dlc, frame.data[0], frame.data[1], frame.data[2]); close(s); return 0; }需要提醒的是frame.can_id默认是标准11位格式如果要发送29位扩展ID要设置frame.can_id | CAN_EFF_FLAG如果要发送远程帧要设置CAN_RTR_FLAG。收到报文时也通过这两个标志位区分帧类型。数据场的“大端小端”问题在CAN领域也常有人踩坑——SocketCAN把数据按字节序原样呈现但不同控制器驱动在内部搬运数据时可能做字节交换所以同一块板子上使用不同厂商的控制器应用层解析结果可能相反。协议设计时要约定清楚字节序不能想当然。5.4 CAN驱动框架与实践如果只是使用CAN大多数SoC已经提供了控制器驱动。比如RK3568使用M_CAN IP内核里有对应驱动你要做的是在设备树里使能节点、配好pinctrl然后编译内核或模块。真正需要写CAN设备驱动的情况通常是使用外置CAN控制器芯片SPI接口的MCP2515、并行接口的SJA1000或者自定义FPGA上的CAN IP。以SPI接口的MCP2515为例驱动层次是SPI设备驱动里注册一个mcp251x_platform_data然后调用alloc_candev、register_candev等CAN子系统API并实现struct can_priv里的do_set_bittiming、do_start、do_stop等操作。这类驱动的开发门槛比I2C设备驱动高不少而且现在大批量项目里用外挂CAN控制器芯片的场景越来越少多数用SoC集成CAN控制器所以我更推荐先把SocketCAN的使用和配置吃透。与之类似SPI子系统里也有个spidev的配置方法调试外挂芯片时经常用到属于同一个“设备树子系统驱动”的思路。5.5 测试CAN通信的实用命令在没有真实节点的情况下可以用vcan虚拟CAN网络调试应用层逻辑modprobe vcan ip link add dev vcan0 type vcan ip link set up vcan0 candump vcan0 cansend vcan0 123#010203有真实总线和两个节点时在节点A上ip link set can0 up type can bitrate 500000 candump can0 cansend can0 123#DEADBEEF在节点B上用candump能看到对端发送的帧说明物理链路和波特率都正常。如果candump一直没输出但can0能up优先检查总线有没有接终端电阻CAN总线两端各需要120Ω终端电阻、节点波特率是否一致、有没有配置成listen-only模式。6. 实操实录RK3568平台上I2C传感器与CAN通信的联合验证6.1 需求与硬件背景前阵子我在一块RK3568核心板上做外设bring-up需求是同时验证两路外设一路I2C接口的温度传感器TMP102一路CAN总线用于和另一块板卡通信。硬件连接是传感器挂在I2C0上CAN0引脚选择m0组的复用CAN0_TX/RX对应GPIO4的某两个引脚。板子跑Linux 5.10内核文件系统用Buildroot构建。我特意选这两个外设同时验证因为它们分别代表了Linux驱动的两种典型路径I2C是标准总线设备驱动client驱动CAN是平台设备加网络子系统驱动。6.2 设备树改造步骤首先打开板级.dts在对应总线节点下做修改。RK3568的I2C0节点在SoC的.dtsi里已经存在需要确认它的状态和引脚。在板级dts里加入i2c0 { status okay; pinctrl-names default; pinctrl-0 i2c0_xfer; tmp10248 { compatible ti,tmp102; reg 0x48; }; }; can0 { status okay; pinctrl-names default; pinctrl-0 can0m0_pins; };注意一个细节i2c0_xfer这个pinctrl组在SoC的.dtsi里默认已经定义板级里其实可以省写但我习惯显式写出来方便出问题时一眼看到引脚复用配置。can0m0_pins同样来自SoC的.dtsi。修改完dts之后重新编译dtbmake dtbs然后有两种方式更新一是把新编译的dtb烧写到启动分区二是通过U-Boot的tftp或者fastboot直接加载。我调试时最常用tftp加载dtb配合网络根文件系统启动改dts、编译、重启的迭代周期能压缩到几十秒效率比反复烧写eMMC高很多。6.3 I2C传感器验证过程内核启动后先看I2C总线上是否枚举出设备dmesg | grep i2c i2cdetect -y 0这里有个经验i2cdetect显示“48”说明设备有ACK显示“--”说明没有设备应答显示“UU”说明该地址被某个驱动占用。TMP102被正确识别后加载之前写好的tmp102驱动模块insmod tmp102.ko dmesg | tail cat /sys/class/hwmon/hwmon0/temp1_input实测中温度读数一直在25℃附近波动cat出来的值除以1000就是摄氏度。这次遇到的一个问题是模块加载后dmesg提示“tmp102: Unknown symbol i2c_smbus_read_i2c_block_data”这是因为当时内核配置里I2C的SMBus支持没开启。把CONFIG_I2C_SMBUS和CONFIG_I2C_CHARDEV都打开重新编译内核问题解决。这属于典型的“驱动代码没问题内核配置裁剪过头”——做系统裁剪优化时很多人会把看起来没用的I2C辅助功能关掉结果模块反而编不过或者加载不了。6.4 CAN通信验证过程CAN部分先把接口拉起来ip link set can0 up type can bitrate 500000 ip -details link show can0看到state是UP、bitrate 500000、并且没有error状态说明控制器工作正常。然后开两个终端分别做收和发# 终端1 candump can0 # 终端2 cansend can0 123#0102030405060708终端1收到内容后说明物理层到数据链路层全链路ok。如果要验证两块板卡互联只需把两块板的CAN_H、CAN_L、GND连好两端各加120Ω终端电阻一侧cansend另一侧candump即可。这次把CAN总线和I2C放在一起验证还有一个额外好处两个子系统共用同一套GPIO/时钟框架从内核启动日志里能同时确认pinctrl驱动、clock驱动的加载情况。如果启动阶段某个gpio或clock获取失败dmesg会第一时间暴露出来方便顺藤摸瓜。6.5 从验证到系统集成的几个补充bring-up通过后接下来要考虑的就不是“能不能跑”而是“怎么稳定跑”I2C传感器如果是持续轮询读取建议用内核的hwmon机制加一个读取间隔避免应用层频繁打开/sys节点导致功耗和总线占用。CAN节点在应用里如果使用SocketCAN要注意给socket设置接收超时和错误帧过滤避免bus-off恢复期间的报错刷屏。生产环境的dtb要和内核版本严格绑定管理。我见过最头疼的问题之一就是现场升级内核后忘了同时更新dtb导致一堆设备的compatible对不上外设全部失联。设备树、内核镜像、根文件系统这三者的版本号要一起记录、一起发布。7. 常见问题与排查技巧实录7.1 设备树相关的典型故障问题现象驱动模块加载了但probe没有被调用。这个很大概率是匹配失败。依次排查dmesg里有没有设备节点创建失败的提示/proc/device-tree下能不能找到对应节点路径节点里的compatible和驱动的of_device_id是否字符串完全一致。可以用下面命令确认设备树解析后的设备是否注册成功ls /sys/bus/platform/devices/ | grep ff610000 ls /sys/bus/i2c/devices/0-0048问题现象改了dts重启后没效果。老生常谈但真的常见要么没重新编译dtb就烧了旧固件要么U-Boot并没有用新烧的dtb而是用了FIT镜像里内嵌的dtb要么dtb格式不对。先确认内核收到的到底是哪个dtb再谈其他。问题现象引脚功能不对外设完全无响应。检查pinctrl配置以及是否和其他外设的引脚复用冲突。在RK平台上同一个引脚经常有多个mux选项设备树里配错了组寄存器写进去了但硬件引脚就是不对。可以用devmem2读GPIO的mux寄存器核对这也是调试设备树时最实用的三板斧之一。7.2 I2C调试实战问题I2C总线在树莓派、RK3568这些平台上的典型问题我整理了一张速查表现象可能原因排查方向i2cdetect枚举不到设备硬件连接、上拉电阻、从机地址换算错误示波器看SCL/SDA核对7位地址SDA一直被拉低总线死锁、设备状态异常断电重启检查从机是否进入异常状态偶发NACK时钟太快、上拉太弱、从机忙降低总线速率加大上拉能力检查供电读到的数据全0xFF地址不对、设备未上电、总线没通细化时序单独用i2cget验证probe超时控制器时钟未使能、reset信号未释放dmesg查看clk和reset相关报错再补一个“复位信号时间”的经验设备树里如果有reset-gpios驱动里通常在probe阶段拉低复位引脚再延时释放这能解决很多传感器初始化失败的问题。有些芯片的数据手册会明确规定复位后的稳定时间比如至少10ms这个延时写太短会导致第一次I2C通信失败。在设备树里设置复位信号时间还要配合驱动里的usleep_range或msleep使用别指望内核自动帮你处理。7.3 CAN调试实战问题CAN通信问题不如I2C那么直观因为CAN是差分信号示波器单端探头看不太准最好用差分探头或CAN分析仪。常见问题包括can0 up了但发不出数据先看有没有配置为loopback模式检查波特率是否和总线其他节点一致检查是否只有单节点在总线上。有些控制器在总线上只有自己且无终端电阻时不发送。Bus-off反复出现物理层干扰、位时序不匹配、终端电阻缺失。临时降低波特率或调整采样点验证。错误计数器不断上涨用ip -details link show can0看state和berr计数器结合CAN分析仪的error帧判断是位错误还是ACK错误。ACK错误往往是总线上另一个节点不响应比如波特率不一致。接收不到高ID报文这不是bug而是仲裁规则的正常表现——低ID报文占用了总线高ID报文在等待。如果你确实需要保证高ID报文的实时性就要重新规划ID分配。7.4 调试工具与方法汇总最后把我日常用的一套工具串起来内核日志dmesg -n 8配合printk level调试阶段把级别调高避免日志被过滤。设备树查看/proc/device-tree目录直接看运行时设备树dtc工具可以把dtb反编译回dts方便对照内核实际收到的内容。sysfs与proc/sys/bus/i2c/devices、/sys/bus/platform/devices、/sys/class/net/can0这些目录能直观反映设备注册状态。i2c-toolsi2cdetect、i2cget、i2cset、i2cdumpI2C调试的瑞士军刀。can-utilscansend、candump、canbusload、canechoSocketCAN调试四件套。devmem2直接读写物理寄存器适合排查pinctrl、时钟、复位寄存器。ftrace和trace-cmd内核函数级跟踪想知道probe为什么没被调用、某个函数到底谁在调用一查便知。逻辑分析仪/示波器I2C不难抓CAN信号建议用差分探头或直接上CAN分析仪一步到位。再提一句性能调优驱动开发完成后如果发现I2C大量小包读写的效率低可以看是否支持SMBus的peek/poke或者改用regmap的bulk_readCAN应用如果吞吐量上不去优先检查应用层是否频繁切换blocking和non-blocking模式、是否每收一帧就做一次高耗时处理。要做算法嵌入式部署或协议栈优化时CPU占用、中断频率、内存拷贝次数往往才是性能瓶颈而不是算法本身。所以别急着优化算法先把数据通路的开销摸清楚。我在实际开发驱动时有几个习惯分享给大家收尾第一拿到新板子第一件事不是写驱动而是先把串口打出来、确认内核启动、用i2cdetect和candump这类工具把“现有能力”摸一遍第二每改一次设备树就完整记录一次改动commit信息写清楚“为什么改”而不是只写“改了什么”第三驱动模块的日志从第一天就用统一的pr_fmt前缀后续排查几十个模块混杂的日志时能少死很多脑细胞。这条路看起来长但每一步都踩实了后面换平台、加外设都是熟门熟路的事。
返回列表