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/ 下看一眼,比什么都管用。
下面这张图,是我自己总结的驱动匹配流程,你一看就明白:
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 反编译检查。这样能快速发现语法错误,比直接烧到板子上调试快得多。
嗯,这一章的内容就到这里。物理层对比让你知道选哪个总线,驱动模型让你理解匹配机制,设备树语法让你能描述硬件。这三样东西,是后面所有驱动开发的基础。你先把这些消化掉,后面咱们再深入每个总线的驱动实现。