1、总线基础与驱动框架:SPI/I2C/UART物理层对比、Linux驱动模型概述、设备树基础语法

各位同学,咱们今天正式开课。做嵌入式Linux驱动开发,有三条总线你绕不开——SPI、I2C、UART。我见过不少新人,上来就撸代码,结果连设备树节点都配不对,最后板子跑起来数据全是乱的。说白了,驱动开发的核心不是写代码,而是理解硬件怎么连、数据怎么走。

这一章,咱们先把地基打牢。我会从物理层差异讲起,再带你看看Linux驱动模型的骨架,最后把设备树的基础语法过一遍。嗯,内容不少,但都是干货。

1.1 SPI / I2C / UART 物理层对比

这三条总线,各有各的脾气。我习惯从信号线数量、通信方式、速率这几个维度去记它们。

特性 SPI I2C UART
信号线 4线(SCLK, MOSI, MISO, CS) 2线(SCL, SDA) 2线(TX, RX)
通信方式 同步、全双工 同步、半双工 异步、全双工
主从结构 一主多从(片选区分) 多主多从(地址区分) 点对点
典型速率 10MHz ~ 50MHz+ 100kHz ~ 3.4MHz 9600bps ~ 3Mbps
传输距离 短(板级) 短(板级) 中(可达数米)
硬件开销 线多,但协议简单 线少,但协议复杂 最简单,无时钟线

我的经验:选型时别只看速率。SPI快,但占引脚多;I2C省线,但抗干扰差;UART简单,但只能点对点。我在一个项目中用过SPI接ADC,结果片选信号没处理好,导致多个从设备冲突——排查了整整两天。

1.2 Linux 驱动模型概述

Linux内核的驱动模型,说白了就是一套「设备 + 总线 + 驱动」的匹配机制。你想想看,内核怎么知道你的驱动该去控制哪个硬件?

核心就三个角色:

  • device:描述硬件实体,比如“I2C总线上的某颗温度传感器”。
  • driver:描述驱动代码,比如“温度传感器的读写函数”。
  • bus:负责匹配 device 和 driver,比如 platform_bus、i2c_bus。

匹配成功后,内核会调用 driver 的 probe() 函数。你所有的初始化逻辑,都放在 probe() 里。我曾经犯过一个低级错误——在 probe() 里做耗时操作,导致系统启动变慢。后来才明白,probe() 要快,重活交给 workqueue。

小技巧:调试驱动时,先确认 device 是否注册成功。去 /sys/bus/xxx/devices/ 下看一眼,比什么都管用。

下面这张图,是我自己总结的驱动匹配流程,你一看就明白:

Linux驱动模型匹配流程 Linux 驱动模型:设备-总线-驱动匹配流程 设备 (Device) 设备树 / ACPI / 动态注册 总线 (Bus) platform / i2c / spi / ... 驱动 (Driver) probe() / remove() 注册 注册 匹配成功 → 调用 probe() 💡 匹配依据 设备树 compatible 属性 ↔ 驱动 of_match_table 或:设备名 ↔ 驱动 id_table

1.3 设备树基础语法

设备树(Device Tree)是嵌入式Linux的硬件描述语言。说白了,就是告诉内核:你有哪些外设、它们连在哪个总线上、寄存器地址是多少。

我刚开始学设备树时,最头疼的就是那些节点和属性。后来发现,记住一个模板就够了:

/dts-v1/;

/ {
    model = "My Board";
    compatible = "vendor,board";

    chosen {
        stdout-path = &uart0;
    };

    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";

        uart0: serial@10000000 {
            compatible = "vendor,uart";
            reg = <0x10000000 0x1000>;
            interrupts = <0 33 4>;
            clock-frequency = <24000000>;
            status = "okay";
        };

        i2c0: i2c@10001000 {
            compatible = "vendor,i2c";
            reg = <0x10001000 0x1000>;
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";

            temp_sensor: sensor@48 {
                compatible = "lm75";
                reg = <0x48>;
            };
        };
    };
};

避坑指南:我曾经在设备树里写错了 reg 的地址长度,导致驱动 probe 时 ioremap 失败。检查设备树时,一定确认 #address-cells 和 #size-cells 的值。address-cells=1 表示地址用32位,size-cells=1 表示长度也用32位。

设备树的关键语法,我整理成了一张速查表:

语法元素 说明 示例
节点 用 {} 定义,可嵌套 uart0: serial@10000000 { };
属性 键值对,值可以是字符串、数字、数组 compatible = "vendor,uart";
标签 节点别名,方便引用 uart0: 标签名
引用 &标签 引用节点 &uart0 { status = "okay"; };
包含 #include 导入头文件 #include "imx6ull.dtsi"

我的习惯:写设备树时,先用 dtc 工具编译成 dtb,再用 fdtdump 反编译检查。这样能快速发现语法错误,比直接烧到板子上调试快得多。

嗯,这一章的内容就到这里。物理层对比让你知道选哪个总线,驱动模型让你理解匹配机制,设备树语法让你能描述硬件。这三样东西,是后面所有驱动开发的基础。你先把这些消化掉,后面咱们再深入每个总线的驱动实现。