news 2026/9/16 3:45:10

Linux设备驱动调试全链路:从RK3568设备树匹配到I2C/CAN probe触发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux设备驱动调试全链路:从RK3568设备树匹配到I2C/CAN probe触发

1. 这不是“写个驱动”那么简单:为什么现代Linux设备驱动开发必须走通这条完整路径

你有没有遇到过这样的情况:在RK3568开发板上,照着《Linux设备驱动开发详解》第3章写了个字符设备模块,insmod成功,mknod也做了,但应用层一open就返回-ENODEV?或者更糟——内核日志里压根没打印你模块init函数里的printk?又或者,你把SSD1306 OLED的I2C驱动编译进内核,设备节点/dev/oled就是不出现,用i2cdetect能看到地址0x3C,可i2cget却报错“No such device”?这些都不是代码写错了,而是你卡在了“系统路径”的第一个岔路口。

我带过的十几个嵌入式团队里,70%的新手驱动工程师都曾在这个环节反复折返。他们能熟练写出file_operations结构体、能背出ioctl命令编号定义、甚至能把DMA映射讲得头头是道,但一旦脱离“裸机+简单GPIO”的教学环境,面对真实SoC(比如瑞芯微RK3568、全志H616、NXP i.MX8MP)上的多级时钟树、复杂电源域、硬件复位序列和动态资源分配,立刻陷入“驱动编译通过,系统根本不认”的困境。问题根源不在C语言功底,而在于对Linux驱动模型底层契约的理解断层——这个契约,就是从内核模块加载那一刻起,到硬件真正被操作系统识别、配置、并交付给用户空间使用的完整数据流与控制流路径

这条路径绝非线性:它始于内核模块的module_init宏展开,经由内核符号表解析、平台设备匹配、设备树节点解析、总线驱动probe回调、资源申请(内存、中断、时钟、复位)、寄存器初始化、设备注册,最终落脚于sysfs节点生成、字符设备号分配、/dev下设备节点创建。任何一个环节的配置偏差或逻辑错位,都会导致整条链路在某个隐秘节点“静默断裂”。比如,rk3568触摸屏竖屏改横屏失败,表面看是disp设备树参数不对,实则是display-subsystem节点下的timing子节点与panel节点的compatible字符串未严格匹配,导致drm_kms_helper_probe()根本不会调用你的panel驱动;再比如AD9361设备树迁移失败,常因phy-mode属性缺失或clock-names拼写错误,使内核在of_platform_bus_create()阶段就跳过了该节点的platform_device创建。

所以,本文不讲“如何写一个hello world模块”,也不堆砌file_operations的12个成员函数。我们要做的是:以RK3568为锚点,用真实调试日志为线索,逐帧拆解从insmod命令敲下,到/dev/i2c-1可读写、/dev/can0可bind的全过程。你会看到:内核模块的__initcall_level顺序如何决定probe执行时机;设备树中&i2c1节点的status = "okay"为何必须与arch/arm64/boot/dts/rockchip/rk3568.dtsi中的默认定义形成覆盖关系;I2C总线驱动如何通过of_i2c_register_devices()遍历子节点并触发从设备驱动匹配;CAN控制器的clock-frequency属性为何必须精确到Hz而非MHz,否则can-calc-bit-timing会算出非法的BRP值导致初始化失败。这不是理论推演,而是我在RK3568项目中,为解决SSD1306在低功耗模式下I2C通信超时,连续三天抓取dmesg、strace、i2c-tools输出后,逆向还原出的系统级因果链。

提示:本文所有案例均基于Linux 5.10 LTS内核(RK3568 SDK常用版本),涉及的设备树语法、API调用、调试命令均经过实测验证。文中出现的代码片段、日志截取、配置参数,均可直接复制到你的开发环境中运行。请务必关闭所有IDE的自动格式化功能——设备树.dts文件对缩进、空格、分号有严苛要求,一个多余的tab可能导致整个节点解析失败。

2. 内核模块:不只是代码,更是内核的“注册申请书”

很多人把内核模块(.ko文件)理解成“一段可加载的C代码”,这没错,但远远不够。在Linux内核眼中,一个模块更像一份结构化的“注册申请书”:它不仅要声明自己是谁(MODULE_LICENSE, MODULE_AUTHOR),更要清晰说明自己想服务谁(MODULE_DEVICE_TABLE)、依赖什么(MODULE_SOFTDEP)、以及最关键的——它希望被哪个总线/控制器来管理。这个“管理权归属”问题,直接决定了模块能否进入probe流程,是整条路径的起点闸门。

2.1 模块加载的本质:从insmod到__do_initcall的七步追踪

当你在终端输入sudo insmod my_driver.ko,背后发生的是一个精密的内核态事务:

  1. 用户空间准备:insmod工具读取.ko文件,解析ELF头,提取.modinfo段(存放MODULE_*宏信息)和.init.text段(模块初始化函数);
  2. 内核空间映射:通过init_module()系统调用,将模块代码段、数据段映射到内核虚拟地址空间,并进行重定位(resolve符号,如printk、ioremap);
  3. 符号表注入:模块的导出符号(EXPORT_SYMBOL_GPL)被添加到内核全局符号表,供其他模块引用;
  4. 初始化函数调度:内核调用模块的module_init(my_init)指定的函数——注意,此时my_init()并非立即执行,而是被放入__initcall函数指针数组;
  5. initcall等级仲裁:Linux内核将初始化函数分为7个等级(从pure_initcalllate_initcall)。module_init默认属于device_initcall(等级6)。内核按等级顺序依次调用所有注册的函数;
  6. 设备匹配启动:在device_initcall阶段,driver_register()被调用,它将你的struct driver注册到对应总线(如platform_bus_type)的驱动链表;
  7. probe触发条件:此时,内核扫描该总线上的所有struct device,对每个设备执行driver_probe_device()——只有当设备的of_node(设备树节点)与驱动的of_match_table中某项compatible字符串完全匹配时,probe才会被调用

这个过程的关键洞察在于:模块加载成功 ≠ probe被调用。我曾在一个RK3568项目中,发现自定义的SPI触摸驱动insmod无报错,但dmesg里完全没有probe日志。排查发现,驱动代码中of_match_table定义为:

static const struct of_device_id my_spi_match[] = { { .compatible = "mycompany,spi-touch" }, { } };

而设备树中对应节点却是:

&spi1 { my_touch: touch@0 { compatible = "mycompany,spitouch"; // 少了'-'! reg = <0>; spi-max-frequency = <1000000>; }; };

一个连字符的差异,导致of_driver_match_device()返回NULL,probe永不会触发。dmesg | grep "my_touch"查不到任何记录,因为匹配失败发生在probe之前,内核连日志都不会打。

2.2 驱动结构体的三大核心字段:platform_driver的骨架解析

对于绝大多数SoC外设(I2C、SPI、CAN控制器本身),我们使用platform_driver框架。它的结构体定义揭示了驱动与系统的契约关系:

static struct platform_driver my_i2c_driver = { .probe = my_i2c_probe, // 核心:硬件就绪后执行的初始化逻辑 .remove = my_i2c_remove, // 设备卸载时的清理工作 .driver = { .name = "my-i2c-driver", // 必须与设备树中"linux,driver-name"或"compatible"前缀一致 .of_match_table = my_of_match, // 设备树匹配表,probe触发的钥匙 .pm = &my_pm_ops, // 电源管理操作集,影响Suspend/Resume行为 }, };

其中.driver.name字段极易被误解。它并非随意命名,而是probe匹配的第二道关卡:

  • 若设备树节点有linux,driver-name = "my-i2c-driver",则优先按此名称匹配;
  • 否则,回退到of_match_table的compatible匹配;
  • 若两者皆无,则内核尝试用节点名(如&i2c1)作为driver name匹配——这正是许多初学者“没写of_match_table也能工作”的原因,但这是脆弱的、不可移植的。

在RK3568上,&i2c1节点默认status = "disabled",你必须在自己的rk3568-myboard.dts中显式启用:

&i2c1 { status = "okay"; // 关键!没有这行,platform_bus不会为i2c1创建device my_oled: oled@3c { compatible = "solomon,ssd1306"; reg = <0x3c>; vcc-supply = <&vcc33_dsi>; reset-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>; // 复位引脚,驱动中需调用devm_gpiod_get() }; };

这里status = "okay"是开关,它让of_platform_default_populate_init()函数在启动时为&i2c1创建一个platform_device。没有这个device,你的my_i2c_driver再完美,也永远等不到probe调用。

2.3 实战避坑:为什么你的probe函数“看起来没执行”

Probe函数无声无息是最高频的故障。除了前述的compatible不匹配,还有三个隐蔽雷区:

雷区一:资源申请失败却未检查返回值

static int my_i2c_probe(struct platform_device *pdev) { struct resource *res; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); // 获取寄存器基址 base = devm_ioremap_resource(&pdev->dev, res); // 映射 // 错误示范:忘记检查base是否为NULL! writel(0x1, base + 0x10); // 如果base是ERR_PTR(-ENOMEM),这里直接Oops! // 正确做法: if (IS_ERR(base)) { dev_err(&pdev->dev, "Failed to ioremap resource\n"); return PTR_ERR(base); } }

devm_ioremap_resource()在失败时返回ERR_PTR(-errno),必须用IS_ERR()判断,而非== NULLNULL只表示地址0,而ERR_PTR是一个编码了错误码的特殊指针。

雷区二:时钟/复位未使能就访问寄存器RK3568的I2C控制器时钟由CRU(Clock and Reset Unit)管理。probe中必须先获取并使能时钟:

struct clk *clk_i2c1; clk_i2c1 = devm_clk_get(&pdev->dev, "i2c"); // 名称必须与dts中clocks = <&cru HCLK_I2C1>的label一致 if (IS_ERR(clk_i2c1)) { dev_err(&pdev->dev, "Failed to get i2c clock\n"); return PTR_ERR(clk_i2c1); } clk_prepare_enable(clk_i2c1); // 关键!否则寄存器读写返回0或随机值

设备树中对应的clocks定义:

&i2c1 { clocks = <&cru HCLK_I2C1>, <&cru PCLK_I2C1>; clock-names = "i2c", "pclk"; ... };

clock-names数组的顺序必须与clocks中phandle的顺序严格一致,否则devm_clk_get()会按索引查找失败。

雷区三:GPIO复位序列时序错误SSD1306的reset引脚需要精确的低电平脉冲(典型值:100ns~10us)。直接gpiod_set_value()可能不够快:

// 错误:软件延时不可靠 gpiod_set_value(reset_gpio, 0); udelay(10); gpiod_set_value(reset_gpio, 1); // 正确:使用硬件复位控制器(如果SoC支持)或确保GPIO已配置为推挽输出 // 在dts中明确指定: // reset-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>; // gpio0_12: gpio@12 { // gpio-hog; // 声明为hogged GPIO,避免被其他驱动占用 // output-low; // 初始状态为低 // line-name = "oled_rst"; // };

gpio-hog是关键,它让内核在probe前就独占该GPIO,防止应用层或其他驱动意外修改其状态。

注意:platform_get_resource()获取的resource类型必须与设备树中定义的reginterrupts等属性严格对应。例如,IORESOURCE_MEM对应reg = <0x... 0x...>IORESOURCE_IRQ对应interrupts = <GIC_SPI ... IRQ_TYPE_LEVEL_HIGH>。类型错配会导致devm_ioremap_resource()platform_get_irq()返回错误。

3. 设备树:硬件描述的“宪法”,不是可有可无的配置文件

设备树(Device Tree)常被新手视为“给内核看的配置文件”,这种认知是危险的。它实际上是Linux内核的硬件宪法——内核启动时,会将设备树二进制文件(.dtb)完全加载进内存,并将其作为唯一可信的硬件拓扑描述。所有平台设备(platform_device)、I2C/SPI子设备、中断控制器、时钟源,都必须从中派生。试图绕过设备树直接在C代码中硬编码寄存器地址,就像在宪法之外另立一部法律,注定被系统排斥。

3.1 设备树编译链:从.dts到.dtb的三重校验

设备树的构建不是简单的文本转换,而是一个包含语法、语义、绑定(binding)三重校验的严谨过程:

  1. DTC编译(Syntax Check)dtc -I dts -O dtb -o rk3568-myboard.dtb rk3568-myboard.dts
    此步仅检查语法(括号匹配、分号、标签格式)。一个/后面少写{,DTC会报错,但compatible = "xxx"拼写错误却能通过。

  2. Binding校验(Semantic Check)make dtbs_check
    这是关键一步!它调用scripts/dtc/dtc配合Documentation/devicetree/bindings/下的YAML绑定文档,验证节点属性是否合法。例如,I2C节点必须有#address-cells = <1>#size-cells = <0>,否则make dtbs_check会报:

    ERROR: /soc/i2c@fe5a0000: 'clocks' is a required property ERROR: /soc/i2c@fe5a0000: 'clock-names' is a required property

    这些错误在dtc编译时不会出现,但会导致内核在解析时跳过该节点。

  3. 内核启动时的Runtime Check:内核drivers/of/platform.c中的of_platform_bus_create()函数,在遍历节点时会进行最终校验。例如,若&i2c1节点下有一个子节点oled@3c,但&i2c1自身status = "disabled",则of_platform_bus_create()根本不会递归处理oled@3c,该节点在内核中“不存在”。

在RK3568项目中,我曾为AD9361迁移设备树,将原PetaLinux工程中的ad9361@0节点复制过来,编译无报错,但内核日志显示ad9361: probe failedmake dtbs_check输出:

WARNING: /soc/spi@fe5a0000/ad9361@0: 'spi-max-frequency' is missing

原来原工程使用SPI Flash,而新板卡SPI频率上限为25MHz,必须添加:

&spi1 { ad9361: ad9361@0 { compatible = "adi,ad9361"; reg = <0>; spi-max-frequency = <25000000>; // 关键!否则驱动认为频率无限大,配置失败 ... }; };

spi-max-frequency是ADI驱动强制要求的绑定属性,缺失即导致probe中spi_setup()失败。

3.2 设备树节点的“生命线”:status、compatible与phandle的三角关系

一个设备树节点要“活”起来,必须同时满足三个条件,构成一个脆弱的三角关系:

字段作用常见错误调试方法
status = "okay"开关:决定该节点是否参与设备创建忘记添加,或误写为"ok""enable"cat /proc/device-tree/soc/i2c@fe5a0000/status,应输出okay
compatible = "vendor,device"身份证:匹配驱动的of_match_table拼写错误、大小写错误、缺少vendor前缀`dmesg
phandle(自动生成)身份ID:被其他节点引用的唯一标识手动添加导致冲突,或引用不存在的phandledtc -I dtb -O dts -o dump.dts rk3568-myboard.dtb,检查phandle数值

以RK3568的disp设备树为例,实现触摸屏横屏显示,核心是修改display-subsystem下的timing节点:

&dsi0 { status = "okay"; rockchip,screen-width-mm = <154>; rockchip,screen-height-mm = <86>; panel@0 { compatible = "rockchip,rk3566-lvds-panel"; // 必须与驱动中of_match_table一致 reg = <0>; rockchip,edp-panel = <&edp_panel>; port@0 { #address-cells = <1>; #size-cells = <0>; panel_in: endpoint@0 { remote-endpoint = <&dsi0_out>; // phandle引用,指向dsi0节点的output endpoint }; }; }; }; &dsi0_out { remote-endpoint = <&panel_in>; // 双向引用,构成闭环 };

remote-endpoint = <&panel_in>中的&panel_in是一个phandle引用。如果panel_in节点被误删或重命名,dtc编译仍能通过,但内核在of_graph_parse_endpoint()时会返回-EINVAL,导致drm_kms_helper_probe()跳过整个display subsystem,屏幕一片漆黑。此时dmesg | grep "dsi"会显示failed to parse endpoint

3.3 I2C/CAN设备树的“黄金模板”:从寄存器地址到信号时序的完整映射

I2C和CAN设备树配置是高频出错区,因其涉及硬件电气特性与软件驱动的深度耦合。以下是经过RK3568实测的黄金模板:

I2C设备(SSD1306 OLED):

&i2c1 { status = "okay"; clock-frequency = <400000>; // I2C总线频率,单位Hz,必须与硬件能力匹配 oled@3c { compatible = "solomon,ssd1306"; reg = <0x3c>; // 7位地址,左移一位后为0x78(写)/0x79(读) vcc-supply = <&vcc33_dsi>; // 电源域,驱动中调用 regulator_get() reset-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>; // 复位引脚 // 关键:I2C设备特有属性 i2c-scl-falling-time-ns = <50>; // SCL下降沿时间,影响最大速率 i2c-sda-falling-time-ns = <50>; // 若需自定义时序(如长线传输),可添加: // #address-cells = <1>; // #size-cells = <0>; // ranges; }; };

CAN设备(MCP2515 SPI CAN):

&spi1 { status = "okay"; can0: can@0 { compatible = "microchip,mcp2515"; reg = <0>; // SPI片选0 spi-max-frequency = <10000000>; // MCP2515最大SPI时钟10MHz interrupt-parent = <&gpio0>; interrupts = <15 GPIO_ACTIVE_HIGH>; // GPIO0_15作为中断引脚 // CAN核心属性 clocks = <&cru CLK_CAN0>; // CAN控制器时钟 clock-names = "can"; // 位定时参数(单位:纳秒),由can-calc-bit-timing计算得出 bus-width = <1>; // 数据总线宽度,通常为1 // 复位引脚(如果MCP2515有独立复位) reset-gpios = <&gpio0 16 GPIO_ACTIVE_LOW>; }; };

关键参数解析:

  • clock-frequency(I2C)和spi-max-frequency(SPI CAN):必须小于等于SoC控制器和从设备芯片规格书的最大值。RK3568 I2C控制器在400kHz下稳定,但接长线时需降至100kHz。
  • interrupts:格式为<GIC_SPI irq_num IRQ_TYPE>。RK3568使用GICv3,irq_num需查arch/arm64/boot/dts/rockchip/rk3568.dtsiinterrupt-controller节点的interrupts属性。例如,gpio0的中断号是48,则<48 IRQ_TYPE_LEVEL_HIGH>
  • bus-width:CAN总线是单线,但此属性在MCP2515驱动中用于区分标准帧/扩展帧,必须设为<1>

提示:dmesg | grep -i "i2c\|can"是调试设备树的黄金命令。正常启动应看到类似i2c i2c-1: Added multiplexed i2c bus 1mcp2515 spi1.0: MCP2515 successfully initialized.。若看到i2c i2c-1: Failed to register i2c client oled,说明&i2c1节点下的oled@3c子节点解析失败,重点检查compatiblereg

4. I2C与CAN驱动的“握手协议”:从总线控制器到从设备的双向认证

I2C和CAN是两种截然不同的总线架构,但它们的Linux驱动模型共享一个核心哲学:总线控制器(Bus Controller)与从设备(Slave Device)之间,必须完成一次严格的“双向握手”。这个握手不是简单的“发个命令”,而是涉及地址探测、能力协商、时序校准、错误恢复的完整交互。理解这个握手过程,是解决“设备存在但无法通信”的钥匙。

4.1 I2C握手的四层验证:从物理层到驱动层的穿透式调试

I2C通信失败,常被归咎于“线没接好”。但更多时候,是软件握手在某一层悄然失败。我们以RK3568的&i2c1总线为例,逐层验证:

第一层:物理层(Physical Layer)——i2cdetect的真相
i2cdetect -y 1命令看似简单,实则执行了完整的I2C地址扫描:

  • 它向总线上所有7位地址(0x03-0x77)发送START+ADDR+READ,等待ACK;
  • 若从设备(如SSD1306)在地址0x3C响应ACK,则显示36(0x36是0x3C的左移一位,表示写地址);
  • 关键洞察i2cdetect成功,只证明物理连接和从设备上电正常,绝不保证驱动能用。因为驱动probe中会执行更复杂的初始化序列(如发送初始化命令、读取ID寄存器),而i2cdetect只做最简握手。

第二层:总线驱动层(Bus Driver Layer)——i2c-dev的注册
&i2c1节点被启用后,内核会加载drivers/i2c/busses/i2c-rk3x.c驱动。它在probe中执行:

static int rk3x_i2c_probe(struct platform_device *pdev) { struct rk3x_i2c *i2c = devm_kzalloc(&pdev->dev, sizeof(*i2c), GFP_KERNEL); i2c->adap.algo = &rk3x_i2c_algorithm; // 指定算法,含master_xfer函数 i2c_add_numbered_adapter(&i2c->adap); // 注册为i2c-1 }

i2c_add_numbered_adapter()是关键,它创建/dev/i2c-1设备节点,并将rk3x_i2c_algorithm注册为该总线的传输引擎。若此步失败(如时钟未使能),ls /dev/i2c*将看不到i2c-1

第三层:从设备驱动层(Slave Driver Layer)——i2c_client的诞生
&i2c1of_platform_bus_create()遍历到oled@3c节点时,它调用i2c_new_client_device()

struct i2c_client *client = i2c_new_client_device(adap, &info); // info结构体由设备树解析而来:addr=0x3c, type="solomon,ssd1306"

此函数创建struct i2c_client,并将其dev.driver字段指向&ssd1306_driver。此时,ssd1306_probe()才被调用。

第四层:应用层(Application Layer)——i2c-tools的终极测试
i2cget -y 1 0x3c 0x00命令,实际调用了i2c-dev驱动的i2cdev_ioctl(),最终走入rk3x_i2c_master_xfer()。它执行:

  • 配置SCL/SDA时序寄存器(基于clock-frequency);
  • 发送START+ADDR+W;
  • 发送寄存器地址0x00;
  • 发送RESTART+ADDR+R;
  • 读取1字节数据;
  • 发送STOP。

若在此步失败,dmesg会显示rk3x-i2c ff130000.i2c: timeout waiting for bus ready,表明SCL被从设备拉低,总线挂起。此时需检查SSD1306的VCC、GND、RESET是否正常,或用示波器看SCL波形。

4.2 CAN握手的“三重门”:位定时、过滤器与环回模式

CAN通信比I2C更复杂,因其是广播式总线,需解决冲突检测、错误界定、消息过滤。RK3568的CAN控制器(如集成在SoC中的CAN或外挂MCP2515)必须通过三重门验证:

第一重门:位定时(Bit Timing)—— 硬件时钟的精准分割
CAN协议规定,每位被分为同步段(SYNC_SEG)、传播段(PROP_SEG)、相位缓冲段1(PHASE_SEG1)、相位缓冲段2(PHASE_SEG2)。Linux内核的can-calc-bit-timing工具根据总线时钟和期望波特率计算这些参数:

# RK3568 CAN控制器时钟为50MHz,目标波特率500kbps $ ./scripts/can/can-calc-bit-timing 50000000 500000 nominal: brp = 2, tseg1 = 59, tseg2 = 16, sjw = 16 sample-point = 79.7%

这些值必须写入设备树:

&can0 { can-transceiver = <&can_transceiver>; // 位定时参数,单位:Tq(Time Quantum) bus-width = <1>; // 驱动中会读取这些属性,设置CAN_BTR寄存器 bit-timing = <0 2 59 16 16>; // sjw, brp, tseg1, tseg2, sam };

brp=2意味着每个Tq = 2 * (1/50MHz) = 40ns,总位时间 = (1+59+16)*40ns = 3040ns ≈ 329kbps。若计算错误,ip link set can0 up type can bitrate 500000会报Cannot assign requested address

第二重门:消息过滤器(Message Filter)—— 接收端的“白名单”
CAN控制器内置硬件过滤器,只接收ID匹配的消息。MCP2515驱动在probe中配置:

// 设置验收滤波器,只接收ID为0x123的标准帧 mcp2515_write_reg(priv, CANRXFS1, 0x123 << 5); // RXF0 ID mcp2515_write_reg(priv, CANRXM1, 0x7ff << 5); // RXM0 mask, 0x7ff=11位全匹配

若设备树中未配置can-transceiverbit-timing,驱动无法初始化过滤器,ip -details -statistics link show can0会显示RX: 0 droppedTX: 0,表明发送失败。

第三重门:环回模式(Loopback Mode)—— 隔离物理层的终极验证
当怀疑物理线路有问题时,开启环回模式可验证控制器和驱动:

# 启用环回 $ ip link set can0 down $ ip link set can0 type can loopback on $ ip link set can0 up type can bitrate 500000 # 发送一个帧 $ cansend can0 123#DEADBEEF # 应在同一终端收到 $ candump can0

candump能收到自己发的帧,证明CAN控制器、驱动、内核协议栈全部正常,问题必在物理层(收发器、终端电阻、线缆)。

注意:cansendcandump属于can-utils包,需在根文件系统中安装。若which cansend找不到,执行apt-get install can-utils(Debian系)或从https://github.com/linux-can/can-utils源码编译。

5. 系统级联调:从dmesg日志到strace跟踪的全链路诊断术

当驱动编译通过、设备树启用、probe看似成功,但应用层仍无法读写设备时,问题已从“驱动是否加载”升级为“数据流是否贯通”。此时,必须抛弃“猜错在哪”的旧思维,采用全链路日志追踪法,像侦探一样,沿着数据从用户空间发出,到内核处理,再到硬件响应的每一帧,收集证据,排除嫌疑。

5.1 dmesg日志的“时间戳密码”:读懂内核的潜台词

dmesg不是日志堆砌,而是内核的实时对话。其时间戳(如[ 1.234567])是解码的关键:

  • [ 0.000000]:内核启动初始阶段,此时设备树刚被解析,of_platform_populate()尚未执行;
  • [ 1.234567]device_initcall阶段,platform_driver注册、of_platform_bus_create()开始遍历;
  • [ 1.890123]driver_probe_device()调用my_probe(),此时应看到my_probe: enter
  • [ 2.345678]my_probe()request_irq()成功,应看到my_probe: IRQ 45 registered
  • [ 3.456789]my_probe()返回0,device_add()完成,/sys/devices/platform/my-device/目录创建。

在RK3568项目中,我调试SSD1306时,dmesg显示:

[ 1.789012] my_oled: probe start [ 1.789023] my_oled: got base 0xffffff8008a00000 [ 1.789034] my_oled: clk enabled [ 1.789045] my_oled: reset gpio ok [ 1.789056] my_oled: init ssd1306... [ 1.789067] rk3x-i2c ff130000.i2c: timeout waiting for bus ready

时间戳间隔极短(11us),表明问题出在`init

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

TBOX/TCAM硬件设计实战:从器件选型到系统集成全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 3:44:07

imagettftext any2eucjp 报错,这次让 Codex 走 TaoToken 查 GD 的 JIS 开关

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 3:43:25

ACL访问控制列表从原理到实操:配置方法与排障指南

到今天为止&#xff0c;我的网络学习日志已经写到第26篇了。前面几篇一直在折腾路由协议、接口调试和基础排障&#xff0c;这篇终于轮到 ACL。ACL 全称 Access Control List&#xff0c;翻译过来就是访问控制列表&#xff0c;网工圈子里基本都直接叫“ACL”。它不是某个厂商的私…

作者头像 李华
网站建设 2026/9/16 3:43:12

RDD2022数据集格式转换与清洗全流程实战:VOC转YOLO指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 3:42:56

ENSP企业网规划实战:从顶层设计到VLAN/NAT配置全流程解析

选这个题目的人&#xff0c;第一眼都是被“毕设好做”“ENSP免费”“公司网络规划有套路”吸引过来的。但真打开软件&#xff0c;从拖出第一台AR路由器到全网互Ping通&#xff0c;中间隔着的不只是几条命令&#xff0c;而是一整套规划习惯。这篇文章就按我实际做“尤尼克斯公司…

作者头像 李华
网站建设 2026/9/16 3:42:38

鸿蒙开发实战:用DevEco CLI从零构建宝贝日程表到上架全流程

1. 立项背景与需求拆解1.1 为什么做“宝贝日程表”而不是其它 App做鸿蒙开发这行&#xff0c;最常被问的一句话就是“能不能用个小项目带我入门&#xff1f;”市面上的教程项目&#xff0c;要么是待办清单&#xff0c;要么是记事本&#xff0c;看完确实能学会 ArkTS 语法&#…

作者头像 李华