1. I2C/SPI总线驱动模型概述
在嵌入式Linux开发中,I2C和SPI是最常用的两种串行通信总线。它们被广泛应用于传感器、EEPROM、显示屏等外设的连接。与GPIO直接控制不同,这两种总线都需要完整的协议栈支持,这就引出了Linux内核中的总线驱动模型概念。
我刚开始接触这块时,常常混淆"控制器驱动"、"设备驱动"和"总线驱动"的关系。经过多个项目的实践才明白:总线驱动模型实际上是Linux内核为统一管理各类总线设备而设计的框架。它主要解决三个核心问题:
- 总线枚举:自动识别挂载在总线上的设备
- 设备与驱动的匹配:通过设备树或ACPI表实现绑定
- 通信协议抽象:提供标准的读写接口
以I2C为例,当我们说"I2C总线驱动"时,实际上包含两个层面:
- 适配器驱动(i2c-adapter):对应SoC的I2C控制器硬件
- 设备驱动(i2c-device):对应具体的I2C外设
2. I2C总线驱动架构解析
2.1 I2C核心层实现
Linux内核中的I2C子系统采用典型的四层架构:
code复制应用层
↓
设备驱动层(如lm75温度传感器驱动)
↓
I2C核心层(drivers/i2c/i2c-core.c)
↓
适配器驱动层(如i2c-imx.c)
↓
物理硬件层
核心层的关键数据结构包括:
c复制struct i2c_adapter { // 代表I2C控制器
const struct i2c_algorithm *algo; // 通信算法
struct device dev;
int nr; // 适配器编号
...
};
struct i2c_client { // 代表I2C设备
unsigned short addr; // 7位设备地址
struct i2c_adapter *adapter;
struct device dev;
...
};
注册一个I2C适配器的典型流程:
c复制static int i2c_imx_probe(struct platform_device *pdev)
{
struct imx_i2c_struct *i2c_imx;
struct i2c_adapter *adap;
// 1. 分配适配器结构体
adap = devm_kzalloc(&pdev->dev, sizeof(*i2c_imx), GFP_KERNEL);
// 2. 设置算法操作集
adap->algo = &i2c_imx_algo;
// 3. 设置适配器编号
adap->nr = pdev->id;
// 4. 注册到内核
i2c_add_numbered_adapter(adap);
return 0;
}
2.2 设备树配置实例
现代Linux系统普遍采用设备树描述硬件连接。一个典型的I2C设备树节点如下:
dts复制&i2c1 { // 控制器节点
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
temperature-sensor@48 { // 设备节点
compatible = "ti,tmp75";
reg = <0x48>; // 设备地址
vsupply = <®_3v3>;
};
};
关键点说明:
clock-frequency:指定总线时钟频率(标准模式100kHz,快速模式400kHz)reg:设备的7位I2C地址(实际写入时内核会左移1位)compatible:用于匹配驱动程序的唯一标识符
3. SPI总线驱动实现细节
3.1 SPI子系统架构
与I2C类似,SPI子系统也采用分层设计:
code复制应用层
↓
设备驱动层(如spidev通用驱动)
↓
SPI核心层(drivers/spi/spi.c)
↓
控制器驱动层(如spi-imx.c)
↓
物理硬件层
SPI特有的几个重要概念:
- 片选(CS):每个设备有独立的片选线
- 时钟极性(CPOL)和相位(CPHA):决定数据采样时机
- 传输模式:支持全双工、半双工等
核心数据结构:
c复制struct spi_device {
struct device dev;
struct spi_controller *controller;
u32 max_speed_hz; // 最大时钟频率
u8 chip_select; // 片选号
u8 bits_per_word; // 每字位数
u16 mode; // SPI模式
...
};
struct spi_driver {
const struct spi_device_id *id_table;
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
struct device_driver driver;
};
3.2 SPI数据传输流程
一个完整的SPI传输涉及以下步骤:
- 获取SPI控制器引用
- 配置传输参数(速度、模式等)
- 准备spi_transfer结构体
- 执行同步/异步传输
示例代码:
c复制struct spi_transfer xfer = {
.tx_buf = tx_buffer,
.rx_buf = rx_buffer,
.len = len,
.delay_usecs = delay,
.speed_hz = speed,
.bits_per_word = bits,
};
struct spi_message msg;
spi_message_init(&msg);
spi_message_add_tail(&xfer, &msg);
int status = spi_sync(spi, &msg); // 同步传输
4. 总线驱动开发实战技巧
4.1 调试技巧与工具
在实际开发中,我总结出几个实用的调试方法:
-
I2C工具集:
bash复制# 安装工具 sudo apt install i2c-tools # 检测总线上的设备 i2cdetect -y 1 # 扫描i2c-1总线 # 读取设备寄存器 i2cget -y 1 0x48 0x00 # 读取地址0x48设备的0x00寄存器 -
SPI信号测量:
- 使用逻辑分析仪抓取CLK、MOSI、MISO信号
- 注意检查CS信号是否正常激活
-
内核调试信息:
bash复制# 动态调整日志级别 echo 8 > /proc/sys/kernel/printk # 启用SPI调试 echo 1 > /sys/module/spi/parameters/debug
4.2 性能优化要点
-
I2C优化:
- 合理设置
i2c_algorithm中的functionality标志 - 使用
i2c_transfer代替i2c_smbus接口提升吞吐量 - 考虑使用DMA传输(如果控制器支持)
- 合理设置
-
SPI优化:
- 启用队列传输(
spi_async) - 合理设置
spi_transfer中的delay_usecs - 使用
spi_message合并多个传输
- 启用队列传输(
5. 常见问题排查指南
5.1 I2C典型问题
问题1:设备无响应
- 检查设备地址是否正确(注意7位/8位表示法差异)
- 测量SCL/SDA线电压(正常应为高电平)
- 确认上拉电阻值合适(通常4.7kΩ)
问题2:通信不稳定
bash复制# 使用i2c-tools测试
i2ctransfer -y 1 w1@0x48 0x00 r2
- 降低时钟频率测试
- 检查线路长度和干扰情况
5.2 SPI常见故障
问题1:数据错位
- 确认CPOL/CPHA设置与设备要求一致
- 检查
bits_per_word参数 - 验证字节序(MSB/LSB first)
问题2:传输超时
c复制// 在驱动中添加超时检测
unsigned long timeout = jiffies + msecs_to_jiffies(100);
while (!(readl(reg) & STATUS_READY)) {
if (time_after(jiffies, timeout)) {
dev_err(dev, "SPI timeout\n");
return -ETIMEDOUT;
}
cpu_relax();
}
6. 进阶开发建议
-
设备树覆盖技术:
bash复制# 动态加载设备树 overlay fdtoverlay -i main.dtb -o new.dtb overlay.dtbo适用于需要运行时修改总线配置的场景
-
用户空间驱动开发:
c复制// 通过spidev接口 int fd = open("/dev/spidev0.0", O_RDWR); ioctl(fd, SPI_IOC_MESSAGE(1), &tr);适合快速原型开发阶段
-
多设备管理策略:
- 使用
device_add动态添加设备 - 实现
probe_new回调处理热插拔 - 考虑使用
regmap抽象寄存器操作
- 使用
在实际项目中,我发现总线驱动开发最关键的还是要深入理解硬件时序特性。建议在编写驱动前,先用示波器抓取正常通信波形作为参考,这会大大减少调试时间。另外,内核文档Documentation/i2c/和Documentation/spi/目录下的文档也非常值得一读。
