news 2026/9/17 5:37:56

从内核模块到CAN总线:嵌入式Linux驱动开发主线详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从内核模块到CAN总线:嵌入式Linux驱动开发主线详解

从出租车到无人车:老司机手把手带你走通嵌入式Linux驱动这条主线

写这篇文章的起因其实挺简单——最近连续收到几个私信,都在问同一个问题:嵌入式Linux驱动到底该怎么入门?有人翻了好几周的《Linux设备驱动开发详解》PDF,越看越懵,因为书里讲的是2.6内核时代的写法,和现在实际项目里的设备树、I2C、CAN完全是两套思路;还有人照着网上的教程复制了helloworld模块,编译倒是通过了,加载也成功了,但真到了调一块传感器的驱动,就不知道下一步该怎么走。

这个困惑我很理解。我自己刚入行时也在这个坎上卡了很久。后来带过几届新人、维护过几套量产固件、从内核模块一路改到设备树再到I2C/CAN总线,才慢慢把整条脉络理清楚。这行当的实际情况是:你真正需要的不是某一本教材,而是一条"从内核模块到设备树、再到I2C/CAN总线"的完整路径,以及这条路径上每一步为什么要这么走的逻辑。这篇文章就是专门来讲这条路径的,适合正在入门嵌入式Linux驱动开发、或者已经写了一些模块但想往更深层次走的同学。

1. 内核模块:驱动开发的地基

1.1 为什么非要先写一个hello模块

很多初学者会问:现在的驱动不都是写在设备树里的吗?为什么还要从内核模块开始?这个问法本身就有问题。设备树负责描述硬件的存在和连接方式,真正的行为逻辑仍然是要靠内核代码来执行的。设备树只是给驱动提供了一个"入场券"和"参数表",驱动里怎么实现读、写、中断、DMA这些操作,仍然完全是由C代码决定的。

所以说,内核模块是驱动开发的地基。它决定了你能不能跑通"编译-加载-运行-卸载"这套基本流程,也决定了你对内核运行时的理解程度。连模块都写不明白,后面不管去调I2C还是CAN,都会有一种"脚下没根"的感觉。

先看一段最基础的内核模块代码:

#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 module");

这段代码的每个要素背后都有讲究:

  • __init__exit宏:__init标记的函数在加载完成后会被释放掉,因为再也不需要它了。内核是常驻内存的,能省一点是一点,这是嵌入式系统裁剪优化的基本思路在模块层面的体现。
  • module_initmodule_exit:这是内核模块的入口和出口注册机制。加载时执行init指向的函数,卸载时执行exit指向的函数,这是一个模块能"活着"的基本契约。
  • MODULE_LICENSE("GPL"):没有这行,模块可以编译但不会自动加载,会提示与内核许可证不匹配。实际项目里如果用了非GPL许可的第三方库,这一行经常是法律和工程的双重敏感点。

很多人以为模块写完了就万事大吉,其实编译这一步的水可比想象中深。

1.2 编译Makefile的正确姿势

内核模块的编译和平常的应用程序完全不一样。它不能在宿主机的glibc环境下直接编译,必须用内核构建系统来编译,而且必须使用与目标内核版本、交叉编译工具链完全匹配的配置。

obj-m := hello.o KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) clean

这套Makefile在PC上验证hello模块完全够用,但拿到嵌入式平台上就要改两个地方:KERNELDIR要指向你交叉编译的那份内核源码树(比如~/rk3568/kernel),还需要指定交叉编译工具链前缀,比如ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-

我在帮一个同事排查的时候遇到过这样的事:他不小心用了PC上的内核头文件编译I2C驱动,结果加载到板子上爆了一堆version magic错误。这个错误看起来吓人,本质上就是你在PC上编的东西和板子上跑的kernel不是同一个。遇到这个问题先别慌,它是新手最常踩的坑之一,解决办法也很标准:把KERNELDIR指对,重新编译。

1.3 加载和调试的常规套路

模块编译出来后,常规的加载和调试步骤是:

  • insmod hello.ko:加载模块,执行init函数。
  • rmmod hello:卸载模块,执行exit函数。
  • lsmod:查看已加载的模块列表。
  • dmesg | tail:查看内核打印信息,模块的pr_info输出都是去这里看的。

这里有一个实际工作中很容易踩的坑:printk的日志级别。调试时在驱动里写printk或者pr_info,如果dmesg里看不到任何输出,不代表你的代码没走,可能是console loglevel设置太高,把INFO级别给过滤了。可以临时用echo 8 > /proc/sys/kernel/printk把日志级别拉满再看。

2. 设备树:让内核知道你接了什么

2.1 设备树到底在解决什么问题

设备树(Device Tree,DT)在嵌入式Linux里是一种描述硬件的机制。它的出现和ARM Linux的演进历史紧密相关:以前的内核里,每一块板卡都堆了一大堆arch/arm/mach-xxx下的板级代码,谁接了什么样子的硬件,全靠写死的C宏和板文件来描述。换一块核心板,哪怕是同一个SoC,板文件可能都要重写。

设备树把"硬件是什么"从内核代码里剥离开来,变成一份独立的数据文件。内核在启动时读取这份文件,按照里面的描述去匹配驱动、注册平台设备和各类总线设备。用个不太精确但好理解的类比:设备树是硬件的"户籍档案",每个外设的地址、中断号、时钟、引脚这些关键信息,都登记在这个档案里。驱动不需要关心档案是怎么填的,它只负责在档案里找到自己对应的那一条记录,然后照着做事情。

2.2 设备树的基本语法和关键节点

设备树文件是一棵树形结构,根节点是/,下面挂各种子节点。最常见的几个节点结构大概是这样的:

/dts-v1/; / { model = "MyEmbedded Board"; compatible = "myvendor,myboard"; chosen { stdout-path = &uart0; }; leds { compatible = "gpio-leds"; led0 { label = "power"; gpios = <&gpio4 20 GPIO_ACTIVE_HIGH>; }; }; i2c0: i2c@ff190000 { compatible = "snps,designware-i2c"; reg = <0x0 0xff190000 0x0 0x1000>; interrupts = <0 28 4>; clocks = <&cru PCLK_I2C0>; clock-names = "pclk"; #address-cells = <1>; #size-cells = <0>; ssd1306: oled@3c { compatible = "solomon,ssd1306"; reg = <0x3c>; }; }; };

这段dts虽然简短,但已经包含了设备树几乎所有核心属性:

  • compatible:这是内核匹配驱动的关键字符串。匹配规则是子串匹配,驱动侧只要注册了含有同一个字符串的of_match_table条目,就能被这个节点唤醒。这是我们做驱动移植时最常检查的一个字段。
  • reg:描述节点的寄存器或者设备地址。对于平台设备来说,它就是寄存器区的起始地址和长度。
  • interrupts:中断号、触发类型。不同SoC对中断号的编码不太一样,通常是<中断类型 中断号 触发方式>这样的结构。
  • clocksclock-names:描述设备需要使用的外部时钟。这个东西非常容易漏,漏了之后驱动会挂在时钟请求上。

设备树里还有一个经常被问到的细节:#address-cells#size-cells。它们是用来描述子节点里reg属性占用几个cell的。i2c子节点的reg = <0x3c>只占一个cell,所以#size-cells = <0>,地址单元一个cell,没有长度字段。这个如果不匹配,地址解析就会出错,这也是很隐蔽的低级错误。

2.3 设备树编译与加载

设备树源文件(.dts)在构建系统里会通过dtc(Device Tree Compiler)编译成二进制的.dtb文件。编译之后就烧写到boot分区或者独立的dtb分区里。调试时如果不想每次烧写固件,U-Boot也支持把dtb放到SD卡或内存的特定位置加载。

在实际项目中,我发现一个比较常见的流程是:改dts → 单独编译dtb → 通过U-Boot的tftp或者load mmc命令加载新的dtb →boot。这种迭代方式比整个烧固件快得多,尤其是在做设备树调试的时候特别重要。因为改一个引脚复用、改一个I2C使能,动不动就要反复试验,如果每次都全量烧写固件,一天根本改不了几次。

2.4 设备树相关的坑

引脚复用的问题。很多人搞了半天驱动不工作,最后发现是引脚复用没配置对。SoC里同一组引脚往往有好几个功能可复用,设备树里需要用pinctrl-0来指定当前这个场景下要启用哪个功能。比如你搞I2C驱动,但实际上这组引脚的复用配置还在UART模式,I2C控制器根本没法正常工作。

复位信号时间的问题。热搜词里有人问"linux设备树设置复位信号时间",这个我猜多半是在调reset引脚。设备树里申请reset GPIO然后拉高拉低,驱动里控制时序的方式一般是:

reset-gpios = <&gpio3 10 GPIO_ACTIVE_LOW>; reset-deassert-us = <10000>;

驱动通过devm_gpiod_get+gpiod_set_value来控制复位,reset-deassert-us是描述复位释放后需要延时多久才能访问设备。不同器件对这个时间的要求差异很大,有的几微妙,有的要几毫秒,实际调起来就是一个字——试。靠谱的办法是先看器件手册上的Reset Timing那一页,再在驱动里加延时验证。

2.5 拿RK3568设备树举个实际例子

现在很多项目用的是瑞芯微RK3568这颗SoC,它设备树里的引脚复用信息写得比较规范,也很适合拿来学习。比如把触摸从竖屏改成横屏这样的需求,很多人会去搜"RK3568触摸竖屏改横屏设备树修改",实际上就是改动触摸控制器节点里的touchscreen-inverted-xtouchscreen-inverted-ytouchscreen-swapped-x-y这几个属性。

这类问题的本质是:设备树的属性值决定了驱动对坐标轴方向的解释。触控面板硬件怎么贴、屏的扫描方向怎么走、驱动拿到原始坐标后要不要交换X和Y轴、要不要反转方向的逻辑,全部体现在这几个布尔属性上。

遇到这种需求,我的建议是先查触摸IC的驱动源码,确认它对应解析哪些设备树属性,再去改设备树。不要凭感觉猜。网上很多改法写得很玄学,实际上往往是驱动版本不同,支持的属性名不一样。

3. I2C:总线系统和驱动实现

3.1 I2C协议到底是怎么回事

I2C是一种两线制的串行通信协议,只有时钟线SCL和数据线SDA两根线。和UART不一样,I2C是同步的,有明确的时钟信号;和SPI不一样,I2C只需要两根线就能挂多个从设备。它靠地址寻址,每个挂在总线上的从设备有一个7位或者10位地址。

好多做嵌入式Linux驱动开发的同学,一上来就想直接读内核源码、写驱动,结果连I2C时序图都没看过。这就相当于没学过开车就直接上路,早晚出事。I2C通信的基本过程其实不复杂,拆开来看就这几个关键动作:

  • 起始条件:SCL为高电平时,SDA从高跳到低。
  • 地址字节:主机发出7位从设备地址,加一个读写方向位(0表示写,1表示读)。
  • 从设备应答:从设备在第9个时钟周期把SDA拉低。
  • 数据字节:每次8位,高位在前,发送方在第9个时钟周期释放SDA给接收方应答。
  • 停止条件:SCL为高电平时,SDA从低跳到高。

热搜词里有人问"i2c读写多个字节的完整时序",其实在Linux内核驱动里,这个一般是不需要你关心时序的实现细节的,I2C控制器硬件已经完成了这些底层工作。你只需要调用内核的I2C传输API,传入消息结构体,让控制器硬件按照时序去操作就可以了。真正的"时序细节",更多是在你阅读示波器波形、排查通信失败时才会有用。

3.2 应用层访问方式:i2c-dev和i2c-tools

在Linux下调试I2C设备,最常用的是i2c-tools这套工具。i2cdetect -l可以列出总线上所有I2C适配器,i2cdetect -y 0可以扫描0号总线上所有的从设备地址。

这里有一个非常多新手踩的坑:i2cdetect扫描出来的是7位地址,但你查数据手册的时候,有些手册写的是8位地址(把读写位也算进去了)。比如OLED的SSD1306,手册上写地址0x78,那是8位写地址,实际上7位地址是0x3C。Linux的reg = <0x3c>用的是7位地址。如果你在设备树里写了0x78,内核在遍历总线设备时是匹配不上的。

另外,如果你想快速验证I2C设备工作是否正常,可以直接用i2cgeti2cseti2cdump来读写设备的寄存器。不需要先写一个完整的驱动。这一步在驱动开发前做"探路"非常好用,能帮你先确认硬件通路是不是通的。

3.3 从i2c-dev到内核I2C驱动

在应用层通过/dev/i2c-N访问设备只能算应急手段,正式产品里都要写内核驱动。Linux的I2C驱动框架是一个典型的"总线-设备-驱动"模型:

  • I2C控制器驱动:通常由SoC厂商提供,负责I2C适配器本身的工作,比如DesignWare I2C控制器。
  • I2C设备驱动:由你自己编写,负责与挂载在总线上的具体设备交互。

一个典型的I2C设备驱动的大致结构是这样的:

#include <linux/i2c.h> #include <linux/module.h> #include <linux/delay.h> static struct i2c_client *this_client; static int ssd1306_probe(struct i2c_client *client) { this_client = client; pr_info("ssd1306 probe success, addr=0x%02x\n", client->addr); return 0; } static void ssd1306_remove(struct i2c_client *client) { pr_info("ssd1306 removed\n"); } static const struct i2c_device_id ssd1306_id[] = { { "ssd1306", 0 }, { } }; static const struct of_device_id ssd1306_of_match[] = { { .compatible = "solomon,ssd1306" }, { } }; MODULE_DEVICE_TABLE(of, ssd1306_of_match); static struct i2c_driver ssd1306_driver = { .driver = { .name = "ssd1306", .of_match_table = ssd1306_of_match, }, .probe = ssd1306_probe, .remove = ssd1306_remove, .id_table = ssd1306_id, }; module_i2c_driver(ssd1306_driver); MODULE_LICENSE("GPL");

这里的核心联系在于:设备树里那个compatible = "solomon,ssd1306"节点,会在内核I2C子系统总线匹配的时候,来找这个驱动里注册的of_match_table。两者字符串一致,内核就会自动调用probe函数,把这个节点的信息(包括地址等)作为参数传入struct i2c_client

probe函数里最常见的动作就是往设备里写初始化命令。写寄存器、读寄存器用的API是:

static int ssd1306_write_cmd(struct i2c_client *client, u8 cmd) { u8 buf[2] = { 0x00, cmd }; // 0x00表示后面的字节是命令 struct i2c_msg msg = { .addr = client->addr, .flags = 0, // 0表示写 .len = 2, .buf = buf, }; return i2c_transfer(client->adapter, &msg, 1); }

如果你对i2c_master_write_byte这种细粒度的API不熟悉,也不要紧。内核I2C子系统的传输封装了一个完整消息的机制,直接操作消息结构体才是核心。实践中最常用的有两个层次:

  • i2c_smbus_read_byte_data/i2c_smbus_write_byte_data:读写单字节寄存器,最常用、最方便。
  • i2c_transfer:灵活地组织一组消息,支持重复起始条件,适合做复合操作。

I2C读写失败常见的表现是i2c_transfer返回负数。排查时先拿示波器看SCL和SDA波形,能直接确认起始条件、地址字节、ACK位是否都有。如果没有波形,检查你是否真的i2c_master_send调用了传输,或者是总线被别的设备给拉死了。

3.4 I2C与UART、SPI怎么选型

很多初学者一直搞不清UART、I2C、SPI这三者之间的区别。这个问题其实一句话可以概括为:UART是异步的,点对点,两根线;I2C是同步的,两根线,多设备,靠地址区分,速度一般几MHz以内;SPI是同步的,四根线(MISO、MOSI、SCLK、CS),速度快,一主多从靠片选信号。

选型时的经验是从这几个方面考虑:

  • 需要挂很多从设备、且引脚紧张、速度要求不高:选I2C。
  • 速度要求高,比如读写SD卡、Flash、音频编解码器:选SPI。
  • 设备离得远、走线长、对时钟同步不敏感:选UART。

嵌入式工程师最常见的调试件——0.96寸OLED屏幕(如SSD1306)就同时有I2C和SPI两种接口,这种只占一两个引脚、不怎么需要速度的,确实用I2C更方便,省引脚、可以并行挂多个。

4. CAN总线:从协议到内核驱动

4.1 CAN协议的精髓

CAN(Controller Area Network)总线是汽车电子和工业控制领域使用最广泛的现场总线之一。它与I2C、SPI这些芯片间总线最大的不同在于:CAN的前身是应对汽车这种电磁干扰很强的环境而设计的,所以它的物理层天然就更强调抗干扰能力,使用差分信号;逻辑上也比I2C复杂得多。

CAN报文ID是很多人一开始就卡住的地方。一个标准CAN数据帧(CAN 2.0A)的报文格式大概是这样的:

  • 帧起始位(1 bit)。
  • 仲裁字段:11位标识符(标准帧),或29位标识符(扩展帧)。
  • 控制字段:IDE位、DLC数据长度代码,标识后面数据的字节数(0-8)。
  • 数据字段:最多8字节用户数据。
  • CRC和应答字段。
  • 帧结束。

那个 "ID号" 在CAN里有两个作用:一是标识报文的身份,二是总线仲裁的依据。当两个节点同时往总线上发报文时,谁发的ID小,谁就优先。这就是"can总线仲裁"的本质——不是靠一个独立的仲裁信号,而是依靠在发送过程中,每个节点持续回读总线电平,如果发现实际回读的电平和自己要发的电平不一致(做"线与"处理,显性电平0会覆盖隐性电平1),就认为优先级比自己高的节点正在发送,自动退出发送。ID越小,在仲裁时越容易赢。

所以CAN报文ID一定不能理解成"设备的地址"。它更像是"消息的类型从编号",同一个设备可以收发一堆不同ID的报文。你完全可以在一根总线上挂20个ECU,每个ECU都在发不同ID的报文。

4.2 CAN FD和CAN有什么区别

热搜词里有人问"CAN FD和CAN的区别",这确实是近几年的高频话题。CAN FD(CAN with Flexible Data-rate)是CAN 2.0的升级版,主要有两个变化:

  • 数据长度扩大了,单帧可以最多64字节,而传统CAN最多8字节。
  • 速率提高了,仲裁段和传统CAN一样用保守的波特率,但数据段可以切到更高的波特率。

这就像一条老公路,限速路段(仲裁段)大家都得守规矩,但过去收费站之后有一段不设限的高速路段(数据段),可以跑得更快。CAN FD不能和传统CAN的节点在一起通信,它们物理层一样,但协议帧格式不同,不同帧结构是无法互相解析的。所以在做ECU选型时,如果总线上有老节点,只能用CAN 2.0,而新节点自然选CAN FD。

4.3 Linux下的CAN驱动框架和SocketCAN

Linux对CAN的支持,和I2C/SPI的思路完全不同。它把CAN抽象成了网络接口,这样你写CAN应用的时候,就像在操作一个网口,这一套机制叫SocketCAN。CAN驱动更多的工作是仿照net_device的接口来实现的。

实际工作中其实很少有人需要亲自去从零写一个CAN控制器驱动,大多数情况下使用的是芯片自带的CAN控制器,比如STM32的bxCAN,或者在Linux下用SPI对接MCP2515这样的外置CAN控制器芯片。内核中已经有mcp251x驱动,设备树里正确配置好SPI接口和中断,就能直接用起来。设备树节点大致长这样:

&spi0 { status = "okay"; mcp2515: mcp2515@0 { compatible = "microchip,mcp2515"; reg = <0>; spi-max-frequency = <10000000>; interrupt-parent = <&gpio1>; interrupts = <7 IRQ_TYPE_LEVEL_LOW>; clocks = <&mcp251x_clk>; vdd-supply = <&vcc_3v3>; }; };

时钟在这里很关键。MCP2515需要外部晶振,设备树里要给它一个clock节点,否则驱动在获取时钟时会失败。这个错误的典型表现是模块加载成功但网络接口一直起不来,ip link set can0 up报错。

应用层用SocketCAN操作CAN总线的基本流程:

# 1. 初始化CAN接口 ip link set can0 type can bitrate 500000 # 2. 打开接口 ip link set can0 up # 3. 查看总线上有没有错误 ip -details -statistics link show can0

C程序里收发报文的套路是创建socket、绑定接口、填充struct can_frame结构体、read/write。这个结构体正好能直观看到CAN报文的基本组成:

struct can_frame { canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */ __u8 can_dlc; /* 数据长度:最低4位 */ __u8 __pad; __u8 __res0; __u8 __res1; __u8 data[8] __attribute__((aligned(8))); };

这里有个很容易忽略的细节:数据字节的大端小端问题。你从can_frame里直接读data[0],对应在总线上就是第一个字节,不涉及大小端翻转。但如果你的数据是一个uint16或者uint32的数值(比如车速、电压值),源节点是按照大端还是小端发的,目标节点必须严格对应。不同厂商的车型协议在这个问题上真是五花八门,最常见的坑就是两边都以为对方用的是自己的字节序。

4.4 CAN总线看不到ACK时怎么办

用CAN调试时出现的经典错误路径是这样的:你用cansend can0 123#DEADBEEF发了一帧报文,返回正常,但用candump can0什么都收不到;或者发的时候直接报错no ACK received,一个一帧都没有。

这里面最核心的一句经验是:CAN总线上至少要有一个节点在接收(ACK环节),发送才能成功。CAN协议在帧的应答字段要求接收节点在总线拉低电平给确认的。如果总线上只有你一台设备在发,没有任何接收者,发送节点会觉得没有收到ACK,于是反复重发,甚至报错。

破解办法很简单:在总线上再挂一个CAN节点做"监听"角色,比如另一块开发板、一个USBCAN分析仪,甚至一台跑着Linux的树莓派加一根MCP2515模块。挂在同一个网络里后,再发报文就不会有no ACK的问题了。

4.5 大小端问题、总线仲裁问题和ID分配问题

做多节点系统时,ID分配是个很讲究的活儿。前面说过,ID小的优先级高,所以必须把最紧急、最需要低延时的报文(比如安全相关的状态帧)分配到小ID。同时,不同节点的接收过滤通常基于ID掩码来实现,ID规划不合理,会导致某个节点收了一堆不相干的报文,白白增加CPU负载。

大小端问题再次强调,它不只是CPU架构带来的,更多是协议定义带来的。同一条总线上有不同架构的MCU(ARM、RISC-V、老式都是小端,但有些DSP用大端),协议必须明确每个多字节字段的字节序。写驱动和应用层代码时,该用htons/ntohs或者显式地(data[0] << 8) | data[1],不要偷懒用memcpy+结构体强制转换——那样在不同平台上结果完全不一样。

5. 实操链路总结:从一个外设需求的完整流程说起

5.1 一个假想的完整实战过程

假设产品经理跟你说:"我们要在这块RK3568板子上加一个挂在I2C上的光照传感器,外加一路CAN总线和车机通信。"如果你靠搜索零散答案怕是会乱成一团,但按这条系统路径走会清晰很多:

  1. 先看原理图,确认光照传感器挂在哪个I2C控制器上、地址是多少;CAN收发器对应的CAN控制器是哪个。
  2. 在内核设备树里使能对应的I2C节点和CAN节点,把引脚复用改到硬件实际连的脚上。
  3. 编译新dtb,单独加载验证匹配是否成功。
  4. 用i2c-tools里的i2cdetect扫描总线,确认能扫描到传感器的地址。
  5. 确认无误后,看传感器数据手册是否为I2C设备写驱动:注册i2c_driver,实现probe/remove,对接of_match_table
  6. CAN这边通过SocketCAN直接操作接口,调试时用cansend/candump验证收发。
  7. 最后做系统裁剪优化:去掉依赖的内核模块默认打开的调试打印、去掉不要的驱动编译选项、调整DMA和中断配置、做性能调优。

这种路径的好处是每一步都有明确的验证手段,从硬件到内核到应用都是清晰的边界。而不是一头扎进代码改个不停,最后不知道是哪一环出了问题。

5.2 从algo到driver:把经验沉淀成代码

还有一个小建议:在调试I2C和CAN的时候,建议把流程打包成一套自己的调试工具脚本。比如:

  • 加载dtb的脚本。
  • 清理dmesg、按模块名过滤log的脚本。
  • 用i2cget循环读取传感器寄存器验证bus稳定性的脚本。
  • 用candump过滤指定ID的脚本。

这些脚本平时看似很"低级",但在现场排障时比什么大工具都好使。我到现在还保留着几个最简单的shell函数,项目一开会就source进来用。

5.3 常见问题速查表

把我在实际项目中踩过、以及帮别人排过的坑整理成一张速查表:

现象可能原因解决思路
模块加载报Invalid module format内核版本或符号版本不一致重新用目标内核源码树编译
设备端驱动probe没被调用设备树compatible和驱动of_match_table不一致检查两者字符串是否完全匹配
i2cdetect扫不到设备地址错了、引脚复用错了、设备没上电用示波器看波形;查原理图
设备树改了不生效dtb没重新编译或没加载新版单独编译dtb并确认boot命令行
I2C传输出错总线被拉死、时钟过快、上拉电阻不合适降速、检查外部上拉、检查总线占用
SocketCANno ACK总线上没有其他节点应答挂第二个节点做接收者
CAN收发正常但数据乱波特率不匹配、大小端处理错检查bitrate参数、整理字节序
触摸方向不对设备树坐标翻转属性配错查看驱动解析哪些属性,再改dts

这些问题的共性在于,大部分都不是"代码难写",而是"链路没打通"。所以排查的顺序一定是:硬件链路 → 设备树描述 → 驱动匹配 → 总线通信 → 数据解析。不要跳过步骤直接猜。

6. 最后分享一点个人体会

从内核模块到设备树,再到I2C和CAN,这条路看着长,但每一步都在为下一步打基础。模块开发教会你和内核对话的方式,设备树教会你描述硬件的方法,I2C和CAN带你进入真正的工业总线世界。我在瑞芯微平台和STM32MP1平台上都走过完整的这条路,最深的一个体会是:多试、多备份、多留日志。改dts前先备份,改驱动前先加日志。很多问题看着神秘,日志一开就现原形了。

另外,瑞芯微RK3568这类平台的学习资料相对丰富,厂商提供的SDK里通常有完整的设备树示例和驱动参考代码,是很好的学习素材。如果你想快速建立整体概念,可以把官方的dtsi文件通读一遍,看它一共描述了哪些外设,每个外设用了哪些属性,再去和原理图对照。这套流程走完,再复杂的平台也基本能看懂七七八八。

最后分享一个小技巧:调试I2C从设备时,如果总是复位不成功,可以先查设备树里有没有配置复位引脚,还有复位信号的时间参数。这类器件(比如显示屏、触摸控制器)在电源上电后,经常会因为复位时序不合规而处于不确定状态,表现就是I2C地址扫描不到或者通信超时。调整reset-deassert-us或者在上电时序上增加延时,往往就能解决问题。这算是设备树配置里最容易忽略、也最容易见效的一个点。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 5:36:47

PHP开发者职业转型与技术升级指南

1. 现象背后的行业背景分析PHP作为一门已有28年历史的服务器端脚本语言&#xff0c;曾支撑了全球超过70%的网站&#xff08;根据W3Techs 2022年统计&#xff09;。在Web 2.0时代&#xff0c;LAMP&#xff08;LinuxApacheMySQLPHP&#xff09;技术栈几乎是所有互联网创业公司的标…

作者头像 李华
网站建设 2026/9/17 5:36:23

壁纸电视怎么选:六条硬指标与五款机型避坑指南

壁纸电视这个品类&#xff0c;我从它刚冒头那会儿就在盯着&#xff0c;这几年帮朋友挑过、装过、也踩过雷。它好看是真好看&#xff0c;贴墙上像一幅画&#xff0c;客厅瞬间从"家电卖场"变成"艺术展厅"&#xff1b;但坑也是真坑&#xff0c;装完发现墙不平…

作者头像 李华
网站建设 2026/9/17 5:35:56

大QMT桥接方案横评:从MiniQMT到HTTP API,量化交易架构的进化之路

从MiniQMT换到大QMT&#xff0c;再把桥接层从零搭起来&#xff0c;这条路我走了将近两年。期间试过各种方案&#xff0c;也踩过无数坑&#xff0c;今天这篇就把我最真实的横评结果写出来&#xff1a;四种主流的大QMT桥接方案到底各自适合谁&#xff0c;为什么最后我All In了HTT…

作者头像 李华
网站建设 2026/9/17 5:35:48

用bat批量重命名不同文件夹下的同名文件:三种规则与脚本实战

前几天帮朋友整理移动硬盘&#xff0c;一百多个文件夹都是从手机、相机、网盘里各自导出来的&#xff0c;几乎每个文件夹里都躺着一张 IMG_0001.jpg&#xff0c;还有一堆“新建文本文档.txt”。真要手动一个个改名字&#xff0c;少说也得忙一整晚&#xff0c;眼睛花掉还容易改错…

作者头像 李华
网站建设 2026/9/17 5:34:13

Anaconda 与 Jupyter Notebook 环境配置实战:从安装到内核绑定与排错指南

从本地 Python 环境被折腾到心态爆炸&#xff0c;到下定决心重装整个 Anaconda 生态&#xff0c;再到把 Jupyter Notebook 真正调教成顺手的数据分析工具&#xff0c;这一路我踩过的坑比很多人想象中要多得多。如果你正准备在新电脑上安装 Anaconda&#xff0c;或者已经被“Jup…

作者头像 李华