ARTICLE DETAIL

资讯详情

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

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

从内核模块到设备树:Linux I2C与CAN驱动开发实战 1. 项目整体思路从零到能跑通一条完整的外设驱动链路做Linux设备驱动开发这件事最容易掉进去的坑就是“学了一堆概念结果连一个实际的传感器都点不亮”。我接触过不少转行做嵌入式的朋友他们背了很多面试题知道module_init、知道file_operations但一到真正的板子上面对设备树报错、I2C总线扫描不到设备、CAN接口起不来就完全不知道从哪里下手。这个项目的标题其实已经点出了整条学习路径先从内核模块理解驱动的基本形态再通过设备树搞明白硬件资源是怎么描述和传递给驱动的最后落到具体的I2C和CAN总线设备上把一整套流程走通。我特别认可这个顺序因为它是顺着“驱动到底是怎么被内核加载、怎么知道硬件在哪里、怎么和硬件通信”这条主线来的而不是零散地背知识点。我今天想用一篇长文把这条路径完整拆开。里面会包括我实际调板子的过程、设备树节点的写法、I2C子系统驱动模型的代码框架以及CAN接口从设备树到SocketCAN能正常收发的大致流程。这些内容不是教科书式的照搬而是基于我在多个项目里的真实调试经验希望能帮你少走点弯路。1.1 为什么学驱动要先从“内核模块”入手内核模块是Linux驱动的最小载体。你写一个.ko文件用insmod把它插入内核再用rmmod卸载这个过程中你已经接触到了驱动生命周期的基础加载、初始化、注册设备、卸载清理。我刚入门的时候第一件事不是去写复杂的硬件驱动而是写一个什么都不干的hello模块故意在里面做几件看起来“没用”的事打印日志、注册一个字符设备、导出一个符号给其他模块用。正是因为把这些基础动作跑熟了后面写I2C驱动、CAN驱动的时候才能把注意力集中在总线协议本身而不是被“模块怎么编译”“为什么insmod报错”这些基础问题绊住。1.2 设备树的价值让同一份内核支持不同硬件很多人第一次看到.dts文件都会觉得头疼因为里面全是reg、interrupts、compatible、pinctrl这些字段。但换个角度想如果没有设备树每换一块板子都要重新编译内核把硬件信息硬编码在C代码里那内核的维护成本会失控。设备树把“硬件长什么样”和“内核怎么运行”解耦了同一份内核镜像靠不同的dtb就能适配不同的板卡。在这个项目里设备树承担的角色很具体定义I2C控制器挂在哪里、时钟是多少、引脚复用成什么功能、外部传感器挂在哪个I2C总线的哪个地址上、CAN控制器的时钟源是什么。驱动代码通过compatible字段和硬件节点匹配匹配成功后从设备树节点中读取需要的资源。理解了这条链路你就能明白为什么probe函数能被调用为什么改设备树比改C代码更安全。1.3 I2C和CAN两条有代表性的总线选I2C和CAN作为落地场景是因为这两条总线在嵌入式领域太常见了而且它们的驱动开发路径有代表性I2C适合接传感器、EEPROM、屏幕控制芯片这类低速外设驱动模型工整非常适合理解bus、device、driver这套框架。CAN则是工业控制、车用通信场景里的标准总线Linux下通过SocketCAN协议栈来访问驱动写完后用户态直接用can-utils工具就能收发报文见效非常快。这两个场景一快一慢、一芯片内一总线型能把“字符设备驱动”和“总线协议驱动”的两套思路都覆盖到。可以说把这两条总线调通你的Linux驱动算是真正入了门。2. 环境准备与内核模块实操动手之前先把基础环境搭好。如果你用的是X86开发机那最简单的做法是装一个Ubuntu虚拟机然后在虚拟机里装好内核头文件就能编译模块。如果目标平台是ARM板卡就需要交叉编译工具链和目标平台对应的内核源码。我建议初学者准备以下内容一台Linux开发机虚拟机即可内存至少4GB目标板卡可选先用X86虚拟机也能完成模块实验内核源码版本跟你目标系统一致最好是同一份编译模块需要用到2.1 编译第一个内核模块的完整过程内核模块的编译不依赖传统的gcc hello.c -o hello而是通过内核的Kbuild系统完成。你需要写一个Makefile里面指定模块名和源码文件然后让make命令结合内核源码目录来编译。obj-m : hello_module.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对X86本机来说KDIR指向的是当前系统运行的发行版内核的build目录。如果你的系统没装内核头文件需要先执行sudo apt install linux-headers-$(uname -r)。对应的模块源码也很简单#include linux/init.h #include linux/module.h #include linux/kernel.h static int __init hello_init(void) { pr_info(hello_module: loaded at 0x%px\n, (void *)hello_init); 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 module);编译后生成hello_module.ko。在虚拟机上插入和卸载sudo insmod hello_module.ko sudo dmesg | tail -5 sudo rmmod hello_module你会在内核日志里看到打印信息。这个实验虽然简单但它帮你建立了“驱动代码如何变成内核的一部分”的直觉。有一点特别值得注意pr_info输出的日志级别默认会出现在dmesg里但如果终端上没看到可能是printk控制台级别限制导致的用dmesg查看最稳。2.2 模块传参与符号共享驱动开发里的两个高频基础操作模块不是只能写死逻辑。很多时候你希望在加载时动态指定参数比如指定I2C地址、指定中断号。内核提供了module_param宏。假设我们要给模块传一个整型参数和一个字符串参数static int irq_num 100; static char *dev_name mydev; module_param(irq_num, int, 0644); module_param(dev_name, charp, 0644); MODULE_PARM_DESC(irq_num, IRQ number); MODULE_PARM_DESC(dev_name, device name);0644表示这个参数在/sys/module/hello_module/parameters/下会显示为只读或可写文件方便运行时查看。通过insmod hello_module.ko irq_num200 dev_nametest传入。这种动态配置方式非常适合调试阶段不用反复重新编译模块。符号共享是另一个重要操作。假设模块A想导出一个函数给模块B用你需要用EXPORT_SYMBOL导出int my_add(int a, int b) { return a b; } EXPORT_SYMBOL(my_add);然后在另一个模块里用extern int my_add(int a, int b);声明后直接调用。加载顺序有要求先加载导出符号的模块A再加载使用符号的模块B。如果顺序反了insmod会报Unknown symbol错误。调试这个报错的时候用nm命令查看.ko文件的符号表很有效nm hello_module.ko | grep my_add如果符号类型是T说明它是全局导出的如果是U说明它是未定义引用需要其他模块提供。这个排查思路在模块变多之后特别有用。2.3 字符设备驱动的骨架为后续I2C/CAN做准备I2C和CAN驱动的最终目的要么是给用户态提供文件节点比如/dev/i2c-0、/dev/can0要么是接入内核已有的协议栈。在深入总线驱动前先掌握字符设备驱动的标准写法会让后面的代码更容易理解。最基础的字符设备驱动包含分配设备号、初始化cdev、添加设备到内核、创建设备类及设备节点。以下是我常用的一个最小骨架#include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h static int major 0; static struct cdev test_cdev; static struct class *test_class; static int test_open(struct inode *inode, struct file *filp) { pr_info(test device opened\n); return 0; } static ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *pos) { char kernel_buf[32] hello from kernel\n; size_t len strlen(kernel_buf); if (count len) return -EINVAL; if (copy_to_user(buf, kernel_buf, len)) return -EFAULT; return len; } static const struct file_operations test_fops { .owner THIS_MODULE, .open test_open, .read test_read, }; static int __init test_init(void) { dev_t devno; alloc_chrdev_region(devno, 0, 1, testdev); major MAJOR(devno); cdev_init(test_cdev, test_fops); test_cdev.owner THIS_MODULE; cdev_add(test_cdev, devno, 1); test_class class_create(test_class); device_create(test_class, NULL, devno, NULL, testdev0); pr_info(test driver initialized, major%d\n, major); return 0; } static void __exit test_exit(void) { dev_t devno MKDEV(major, 0); device_destroy(test_class, devno); class_destroy(test_class); cdev_del(test_cdev); unregister_chrdev_region(devno, 1); } module_init(test_init); module_exit(test_exit); MODULE_LICENSE(GPL);这段代码里device_create的作用是在/sys/class/test_class/下创建设备配合udev规则后内核会自动在/dev下生成testdev0节点。如果你在板子上发现/dev节点没生成通常就是缺少设备树里对应的compatible设备或者udev没有识别到设备。这也是后面理解设备树和驱动probe关系的一个伏笔。3. 设备树机制拆解从语法到实际配置设备树是这个项目里最容易被轻视、也最容易出问题的环节。我见过太多人写驱动时根本不看设备树直接在C代码里用ioremap硬编码地址结果换一块板子就完蛋。现代内核里外设资源应该尽量通过设备树描述。3.1 设备树的基本结构从根节点到子节点设备树文件的后缀是.dts编译后生成.dtb。它的基本结构是一棵倒挂的树根节点写作/下面挂各种总线和外设节点。一个典型的I2C设备节点长这样/ { compatible myvendor,myboard; ... i2c0: i2cff610000 { compatible snps,designware-i2c; reg 0x0 0xff610000 0x0 0x1000; clocks clk_i2c0; clock-frequency 400000; pinctrl-names default; pinctrl-0 pinctrl_i2c0; bme28076 { compatible bosch,bme280; reg 0x76; status okay; }; }; };上面这段里i2cff610000是I2C控制器节点它被系统总线识别后会匹配到I2C控制器驱动。bme28076是挂在I2C总线上的设备节点compatible字段用来匹配驱动reg字段表示该设备在I2C总线上的7位地址是0x76。理解节点与驱动的匹配关系是整个设备树的钥匙。每个设备节点都有一个compatible属性驱动侧通过of_device_id表声明自己支持哪些设备。当设备节点和驱动的compatible字段一致时内核会把它们绑定然后调用驱动的probe函数。3.2 常见属性解读reg、interrupts、pinctrl、clocks这几个属性是你配置设备树时绝对绕不开的。reg描述设备占用的地址段第一项是起始地址第二项是长度。它既可以描述片内控制器的寄存器地址也可以描述总线设备的从设备地址。不同总线域下格式不同比如I2C设备只用低7位地址而内存映射设备则给出完整物理地址。interrupts描述设备使用的中断号。这个值的解释依赖父级interrupt-controller节点。常见的有GPIO中断和系统中断控制器中断。你要是配错了中断号驱动一旦request_irq就会收到错误的中断或者干脆收不到中断。pinctrl是引脚复用配置用于把SoC的引脚设置成I2C/UART/SPI等功能。它通过pinctrl-names和pinctrl-0引用SoC内pinctrl子系统的节点。很多I2C总线扫不到设备不是协议问题而是引脚没被复用成I2C功能或者上下拉配置不对。clocks则是设备的时钟来源。I2C总线速率、CAN波特率都是从时钟树里派生出来的时钟源配错了后面所有频率计算都会跟着错。3.3 一个具体的设备树实战配置挂载I2C显示屏控制器假设我们要在板子上通过I2C挂一颗SSD1306 OLED显示屏。设备树里需要做两件事确认I2C控制器节点已经使能然后在这个I2C总线节点下增加SSD1306的子节点。i2c1 { status okay; clock-frequency 100000; ssd13063c { compatible solomon,ssd1306; reg 0x3c; status okay; }; };这里的i2c1表示引用设备树中定义好的i2c1节点在它的作用域内添加属性或子节点。ssd13063c里的3c是设备地址7位地址模式下SSD1306常见的I2C地址是0x3c或0x3d取决于SA0引脚电平。编译设备树之前一定要检查语法是否合法常见错误是少了分号、大括号没闭合、引用了不存在的节点。使用dtc工具可以反编译生成好的.dtb来排查问题dtc -I dtb -O dts -o decompiled.dts myboard.dtb反编译出来的文件能清楚地看到最终生效的设备树配置尤其适合确认你的修改有没有真的进入最后烧录的镜像。3.4 设备树与驱动匹配的完整链路当内核启动时会解析.dtb生成一棵struct device_node树。然后总线驱动比如I2C核心、平台总线遍历设备节点把每个节点注册成设备。举个例子平台总线platform bus上的设备会逐一和平台驱动做匹配匹配规则主要看compatible字段。驱动侧怎么声明自己支持什么设备呢看这段static const struct of_device_id my_driver_of_match[] { { .compatible myvendor,mydevice }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_driver_of_match); static struct platform_driver my_driver { .probe my_probe, .remove my_remove, .driver { .name mydevice, .of_match_table my_driver_of_match, }, }; module_platform_driver(my_driver);在probe函数里你可以通过传入的struct platform_device *pdev拿到设备节点再借助一组of_函数读取属性static int my_probe(struct platform_device *pdev) { struct device_node *np pdev-dev.of_node; u32 val; if (!of_property_read_u32(np, clock-frequency, val)) dev_info(pdev-dev, clock-frequency%d\n, val); return 0; }这套链路的魔力在于驱动本身不关心硬件地址是写死的还是自动分配的一切资源都可以从设备树中动态获取。这意味着同一个驱动只要设备树节点里的寄存器地址或者引脚配置不同就能适配不同硬件。哪怕是同一套SoC的不同板卡改设备树就能完成硬件适配不需要重新编译内核。3.5 设备树调试的实用技巧调设备树阶段我最常做的事就是反复查看/proc/device-tree和/sys/firmware/devicetree/base。它们本质上是同一个东西会把运行时生效的设备树以目录形式导出ls /proc/device-tree/ cat /proc/device-tree/model这两个路径能帮你确认板子上的实际设备树状态。如果你发现某个节点没有出现或者属性值不对优先检查编译和烧录环节看看.dtb是不是真的更新了。另一个常用的调试手法是在设备树节点里故意填一个错误的compatible然后观察probe是否消失。这能帮你确认驱动和节点是否真的成功匹配。如果probe没有执行很可能不是驱动写错了而是设备树里的节点名称/地址/状态有问题。4. I2C子系统驱动开发一条完整的I2C传感器驱动实战I2C驱动开发相比平台设备驱动多了一个总线的概念。Linux内核对I2C抽象出三条路径I2C核心负责总线管理、I2C控制器驱动适配器驱动负责具体收发时序、I2C设备驱动负责具体的从设备比如传感器。我们大部分时间写的是第三类I2C设备驱动。它的好处是几乎不用关心时序怎么实现只需要调用i2c_transfer或i2c_smbus_*系列API读写寄存器即可。4.1 从设备树到I2C设备的注册过程当内核解析设备树时会在对应的I2C控制器节点下创建I2C客户端设备。如果你在设备树里正确添加了传感器节点那么在I2C控制器驱动初始化完成后内核会自动调用匹配到的I2C设备驱动的probe函数。如果没有设备树老式做法是用i2c_new_device或者i2c_board_info手动注册。但在现代内核里设备树方式是绝对主流尤其是ARM和RISC-V平台。建议直接放弃板级注册的写法专注设备树。4.2 I2C client与driver的匹配关系I2C设备驱动的框架里i2c_driver结构体里有一个id_table它用于在没有设备树时匹配设备名。但更常见的是用of_match_table它通过设备树节点的compatible属性和驱动支持的设备ID匹配static const struct i2c_device_id bme280_id[] { { bme280, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, bme280_id); static const struct of_device_id bme280_of_match[] { { .compatible bosch,bme280 }, { } }; MODULE_DEVICE_TABLE(of, bme280_of_match); static struct i2c_driver bme280_driver { .driver { .name bme280, .of_match_table bme280_of_match, }, .probe bme280_probe, .remove bme280_remove, .id_table bme280_id, }; module_i2c_driver(bme280_driver);有个容易踩坑的点老内核用.probe和.remove新的5.x内核逐步改成.probe_new和.remove_new。如果你在新内核上编译发现probe函数类型对不上大概率就是这个问题。4.3 I2C读写寄存器的完整示例以BME280温湿度气压传感器为例它的寄存器比如湿度校准、温度校准等都有固定的寄存器地址。I2C设备的操作手段一般是先发一个寄存器地址然后再读或写数据。下面这段代码展示如何使用i2c_transfer完成一次带寄存器地址的读取static int bme280_read_reg(struct i2c_client *client, u8 reg, u8 *buf, int len) { struct i2c_msg msgs[2]; int ret; msgs[0].addr client-addr; msgs[0].flags 0; msgs[0].len 1; msgs[0].buf reg; msgs[1].addr client-addr; msgs[1].flags I2C_M_RD; msgs[1].len len; msgs[1].buf buf; ret i2c_transfer(client-adapter, msgs, 2); if (ret ! 2) { dev_err(client-dev, read reg 0x%02x failed, ret%d\n, reg, ret); return -EIO; } return 0; }i2c_transfer接受一个i2c_msg数组内核会按照数组顺序在I2C总线上产生对应的START、写地址、写数据、重START、读数据等时序。这就是一次标准的“先写寄存器地址再读数据”流程。实际的BME280读取还需要先从寄存器0xD0读ID确认设备是在线的然后读取校准参数再启动测量等待完成后拼接温度/气压/湿度的原始值。这里不展开完整算法但要注意一点I2C驱动开发时最优先要做的是i2cdetect确认设备地址以及通过i2ctransfer先手动读取设备ID寄存器确认通信没问题再写驱动。4.4 使用i2c-tools进行设备树与驱动调试设备树改完驱动也写了但传感器还是读不到数据怎么办先用i2c-tools验证硬件通路。查看系统上有哪些I2C总线适配器i2cdetect -l扫描指定总线上的设备地址i2cdetect -y -r 1如果设备挂载正常你会看到一个十六进制地址被框出来比如3c或76。扫描不到常见原因包括设备供电没给、I2C引脚复用错误、地址不对、总线上拉电阻没接、设备处于复位状态。直接读设备寄存器确认通信正常i2ctransfer -y 1 w10x76 0xd0 r1这条命令的意思是往I2C设备地址0x76发送1个字节0xd0然后读取1个字节。如果返回的ID和期望值一致说明硬件通路完全没有问题。这时候再回头看驱动代码问题就缩小到软件层面了。4.5 probe函数里的资源获取一旦驱动匹配成功probe函数就会执行。在probe里你可以从设备树中获取设备节点信息、中断号、GPIO、时钟等信息。比如读取设备树中自定义的属性static int bme280_probe(struct i2c_client *client) { struct device *dev client-dev; struct device_node *np dev-of_node; u32 measure_period 1000; if (np) of_property_read_u32(np, measure-period-ms, measure_period); dev_info(dev, BME280 probe ok, addr0x%02x, period%dms\n, client-addr, measure_period); return 0; }这种读取方式好处很明显你可以不修改驱动代码只改设备树就能改变测量周期尤其适合产品原型阶段反复试参数。5. CAN驱动与SocketCAN从设备树配置到报文收发CAN总线在工业化产品里太常见了。Linux对CAN的支持核心是SocketCAN它把CAN接口抽象成了网络接口使用struct sock和网络协议栈的方式来处理CAN报文。驱动开发的意义在于让某个CAN控制器在Linux里被识别成一个can0或can1网络接口。5.1 CAN控制器的两种形态CAN控制器通常分为SoC内部集成和外部独立控制器两种形态。内部集成控制器在SoC内部通过设备树节点描述驱动基于内核的net_device框架实现比如很多芯片自带的FlexCAN、MCAN、D_CAN等。外挂独立通过SPI/I2C等接口连接一个独立的CAN控制器芯片传统上常见MCP2515需要编写SPI设备驱动并向上注册一个CAN网络设备。从开发难度来说外挂独立控制器需要同时处理SPI驱动的设备接入和CAN网络设备注册链路更长调试也更繁琐。内部集成控制器相对直接设备树配好时钟、引脚和中断驱动起来后就直接是can0。5.2 设备树中的CAN节点配置这里我们以内置CAN控制器为例设备树节点一般会配置寄存器地址、时钟、中断、引脚复用以及CAN控制器相关参数can0 { status okay; pinctrl-names default; pinctrl-0 pinctrl_can0; interrupts GIC_SPI 45 IRQ_TYPE_LEVEL_HIGH; clocks clk_can0; clock-frequency 60000000; };其中clock-frequency很关键它决定了CAN波特率的计算基准。比如MCP2515外接晶振是8MHz或16MHz内部CAN控制器的时钟源频率不同同样的bitrate配置会产生不同的位时序。此外某些CAN控制器需要配置rx-fifo-depth、tx-echoes等属性但这些更多是驱动实现细节。大部分情况下设备树只要保证status为okay、引脚复用正确、中断号和时钟正确即可。5.3 SocketCAN的启动流程设备树和驱动都就位后系统启动时你会在dmesg中看到类似日志mcan0: MCAN controller device registered然后使用ip命令启用CAN接口并设置波特率sudo ip link set can0 up type can bitrate 500000正常启动后ip -details link show can0可以看到接口状态。如果启动失败查看dmesg常见报错是bitrate not supported或者bus-off。5.4 CAN报文收发测试收发测试最常用的工具是cansend和candump它们属于can-utils软件包。先在一个终端开启监听candump can0 -L然后在另一个终端发送报文cansend can0 123#DEADBEEF监听端会输出类似can0 123 [4] DE AD BE EF如果你在同一个板子上只看到发送、看不到接收可以尝试自环测试。很多CAN控制器支持loopback模式sudo ip link set can0 up type can bitrate 500000 loopback on在loopback模式下发送的报文会被自己接收到这用来验证驱动和协议栈是否正常比接外部设备方便得多。验证完记得关闭loopback再连真实总线。如果loopback模式下收发都正常但接上真实总线后一直报bus-off那就要重点检查CAN_H和CAN_L接线、终端电阻120欧姆以及收发器芯片的供电和电平。这里有个小经验先确认总线空闲时CAN_H和CAN_L之间的电压差正常应该在0V左右发送时会有约2V的差分电压变化。5.5 从字符设备思维过渡到网络设备思维很多从GPIO和I2C驱动转来做CAN驱动的人最难适应的是思维模式转变。I2C传感器驱动本质上是字符设备驱动用户态open/read/write内核态负责一次I2C事务。CAN驱动则会把数据包化、协议栈化驱动负责的是把网络协议栈的sk_buff转换成CAN帧并处理错误状态和超载状态。这意味着你在调试CAN驱动时不能只盯着驱动代码还要会看ip -details link show can0输出的状态信息包括state是ACTIVE还是BUS-OFF、restart-ms、bitrate、sample-point等。这些信息比单纯打日志更能反映底层问题。6. 过程中的典型问题与排查建议在整个从内核模块到I2C/CAN的链路里我踩过不少坑。这里整理几个高频问题以及我常用的排查思路不一定适用所有平台但思路是通用的。6.1 模块加载报错Invalid module format这个错误最常见的原因是内核版本不匹配。你在开发机上编译模块时用的是当前运行内核的头文件但目标板上的内核版本不同模块格式自然对不上。排查方法modinfo hello_module.ko看vermagic字段再对比目标板上的版本cat /proc/version如果版本不一致就需要到目标板对应的内核源码目录下用该内核的build目录重新编译。还有一种情况内核开启了签名验证或强制模块版本控制非GPL模块或未签名的模块会被拒绝加载。这时候要么关闭配置要么给模块签名。6.2 I2C总线扫描不到设备排查顺序很有讲究。先确认I2C控制器是否已经注册i2cdetect -l看总线是否存在。再确认引脚复用是否正确进入板卡的pinctrl调试目录或者用GPIO工具检查引脚电平状态。检查设备供电和地址冲突很多传感器有多个地址引脚地址不是你想改就能随便改的。用示波器/逻辑分析仪看I2C时钟和数据线上有没有正确的START/ACK时序。如果在地址发送后没有ACLK大概率是设备不存在或地址错误。如果确定硬件没问题再看设备树节点里的status是否为okayreg地址是否和实际设备跳线一致。我在一个项目里就遇到过传感器地址是0x77但设备树里配了0x76结果扫描不到改完立即正常。这种错误在早期用i2cdetect是非常容易发现的。6.3 CAN接口起不来报错BR_OFF或无法设置波特率BR_OFFBus-Off一般不是驱动问题而是总线上有错误帧导致控制器进入离线状态。先断开总线只保留节点单机加上120欧姆终端电阻用手动设置固定波特率然后用candump监听如果只在loopback模式下正常就说明硬件连接或波特率不匹配。设置波特率时报bitrate not supported是因为CAN控制器的时钟源频率与你设置的比特率不能形成有效的位时序。解决办法是检查设备树里的clock-frequency或者用ip命令的sample-point参数微调sudo ip link set can0 up type can bitrate 500000 sample-point 0.8756.4 驱动probe没有执行处理器平台五花八门这个问题的排查套路相对通用。确认驱动被编译进内核还是以模块形式加载。模块形式加载时需要先执行modprobe或insmod。确认设备树节点里compatible和驱动of_device_id一致。字符串逐字比对包括大小写和逗号前后是否有空格。确认设备树节点status不为disabled。看/sys/bus/platform/devices/下有没有对应设备节点或者/sys/bus/i2c/devices/下有没有bus-addr命名的设备目录。确认驱动没有因为依赖其他模块或者module_init顺序问题而加载失败。6.5 常见问题速查表现象可能原因优先排查点insmod 报 Invalid module format内核版本不一致对比 vermagicinsmod 报 Unknown symbol依赖模块未先加载检查依赖顺序查看符号导出设备树编译报错语法错误 / 引用不存在节点用 dtc 反编译检查I2C 扫描不到设备引脚复用、设备地址、供电问题i2cdetect 示波器probe 函数未调用compatible 不匹配 / status 不对检查 /proc/device-treeCAN 接口报 BR_OFF总线错误帧 / 波特率不匹配单机测试 终端电阻add_device 失败时钟未配置 / 中断号冲突查看 dmesg 完整日志7. 最后再说点实际操作上的心得整套流程走完之后我最大的感受是Linux设备驱动开发的核心不是会写C代码而是会拆问题。模块加载不上先查版本设备树不生效先查节点I2C读不到数据先查硬件通路CAN起不来先查波特率和接线。每一步都有对应的工具和日志关键是你知不知道去看哪里。我在实际项目中养成的几个习惯对提升调试效率帮助很大永远先开dmesg -w再加载驱动。日志是驱动开发里最重要的一条线索很多时候probe没跑、资源获取失败都会在日志里留痕迹。设备树改动后优先用dtc反编译检查最终生成的.dtb不要想当然地认为编译通过就等于内容正确。I2C设备调试一定用i2cdetect和i2ctransfer先确认硬件通路不要一上来就专门写复杂的测试程序。CAN调试多用loopback模式隔离问题先把驱动和协议栈验证好再上真实总线。养成阅读内核文档的习惯Documentation/i2c、Documentation/devicetree/bindings、Documentation/networking/can.rst这些目录都是宝贵的资源遇到疑问先查文档。最后再分享一个小技巧写驱动的时候不要一开始就追求功能完整。我从内核模块到I2C驱动都会先写一个“空壳probe”只打印一行dev_info确认驱动和设备能匹配上再逐步填充寄存器读写和数据处理逻辑。这样每一步都能验证不会出现代码写了一大堆却不知道问题出在哪一层的尴尬情况。这条从内核模块到设备树、I2C/CAN的路径走通了之后你会发现Linux驱动开发并没有想象中那么神秘。它更多的是一套工程方法论如何描述硬件、如何匹配驱动、如何调试通信、如何从日志中定位问题。把这几条主线掌握好后续再接触SPI、USB、PCIe驱动时学习路径也会顺畅很多。
返回列表