1. Linux SPI/I2C 驱动开发概述
在嵌入式Linux开发中,SPI和I2C是最常用的两种串行通信协议。作为一名长期从事嵌入式开发的工程师,我经常需要与各种传感器、存储芯片和外设打交道。掌握这两种总线的底层驱动开发技巧,是嵌入式Linux开发者的基本功。
SPI(Serial Peripheral Interface)是一种全双工、高速的同步串行通信接口,采用主从架构,通常需要4根线:SCLK(时钟)、MOSI(主机输出从机输入)、MISO(主机输入从机输出)和SS(片选)。它的优势在于传输速度快、协议简单,适合需要高速数据传输的场景。
I2C(Inter-Integrated Circuit)则是一种半双工、低速的同步串行总线,只需要两根线:SDA(数据线)和SCL(时钟线)。它支持多主多从架构,通过地址寻址方式访问不同设备,适合连接多个低速外设。
2. Linux SPI驱动开发详解
2.1 SPI核心框架解析
Linux内核中的SPI子系统采用分层架构设计,主要包含以下几个核心组件:
- SPI核心层(drivers/spi/spi.c):提供SPI总线注册、设备匹配等基础设施
- SPI控制器驱动(drivers/spi/spi-*.c):实现具体SoC的SPI控制器操作
- SPI设备驱动:实现具体SPI设备的操作逻辑
在开发SPI设备驱动时,我们需要重点关注以下几个数据结构:
c复制struct spi_device {
struct device dev;
struct spi_master *master;
u32 max_speed_hz;
u8 chip_select;
u8 bits_per_word;
u16 mode;
// ...
};
struct spi_driver {
const struct spi_device_id *id_table;
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
void (*shutdown)(struct spi_device *spi);
struct device_driver driver;
};
2.2 SPI设备驱动开发实战
下面以一个常见的SPI Flash芯片(W25Q128)为例,展示完整的驱动开发流程:
- 定义设备ID表:
c复制static const struct spi_device_id w25q_id[] = {
{ "w25q128", 0 },
{ }
};
MODULE_DEVICE_TABLE(spi, w25q_id);
- 实现probe函数:
c复制static int w25q_probe(struct spi_device *spi)
{
struct w25q *flash;
/* 分配设备结构体 */
flash = devm_kzalloc(&spi->dev, sizeof(*flash), GFP_KERNEL);
if (!flash)
return -ENOMEM;
flash->spi = spi;
spi_set_drvdata(spi, flash);
/* 初始化SPI参数 */
spi->mode = SPI_MODE_0;
spi->bits_per_word = 8;
spi->max_speed_hz = 10000000; /* 10MHz */
spi_setup(spi);
/* 识别Flash芯片 */
ret = w25q_read_id(flash);
if (ret < 0) {
dev_err(&spi->dev, "识别Flash失败\n");
return ret;
}
/* 注册MTD设备 */
return w25q_register_mtd(flash);
}
- 实现读写操作:
c复制static int w25q_read(struct spi_device *spi, u8 *cmd, size_t cmd_len,
u8 *data, size_t data_len)
{
struct spi_transfer t[2] = {
{ .tx_buf = cmd, .len = cmd_len },
{ .rx_buf = data, .len = data_len },
};
return spi_sync_transfer(spi, t, ARRAY_SIZE(t));
}
注意事项:SPI传输速度设置需要根据设备规格和PCB布线质量进行调整。过高的速度可能导致通信失败。
2.3 SPI调试技巧
在实际开发中,经常会遇到SPI通信失败的情况。以下是我总结的调试方法:
- 使用逻辑分析仪抓取SPI波形,检查时钟极性、相位是否正确
- 逐步降低SPI时钟频率,排除信号完整性问题
- 检查片选信号是否正常激活
- 使用内核提供的SPI loopback模式测试控制器是否工作正常
bash复制# 启用SPI loopback测试
echo loopback > /sys/devices/platform/soc/2000000.spi/spi_master/spi0/spi0.0/loopback
3. Linux I2C驱动开发详解
3.1 I2C子系统架构
Linux I2C子系统同样采用分层设计:
- I2C核心层(drivers/i2c/i2c-core.c):提供总线注册、设备匹配等基础设施
- I2C适配器驱动(drivers/i2c/busses/i2c-*.c):实现具体SoC的I2C控制器操作
- I2C设备驱动:实现具体I2C设备的操作逻辑
关键数据结构:
c复制struct i2c_client {
unsigned short flags;
unsigned short addr;
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter;
struct device dev;
int irq;
// ...
};
struct i2c_driver {
unsigned int class;
int (*probe)(struct i2c_client *client);
int (*remove)(struct i2c_client *client);
void (*shutdown)(struct i2c_client *client);
struct device_driver driver;
const struct i2c_device_id *id_table;
};
3.2 I2C设备驱动开发示例
以常见的温度传感器LM75为例:
- 定义设备ID表:
c复制static const struct i2c_device_id lm75_id[] = {
{ "lm75", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, lm75_id);
- 实现probe函数:
c复制static int lm75_probe(struct i2c_client *client)
{
struct lm75_data *data;
if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA))
return -EIO;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
i2c_set_clientdata(client, data);
data->client = client;
/* 初始化硬件 */
lm75_init_client(client);
/* 注册hwmon设备 */
data->hwmon_dev = hwmon_device_register(&client->dev);
if (IS_ERR(data->hwmon_dev))
return PTR_ERR(data->hwmon_dev);
return 0;
}
- 实现温度读取函数:
c复制static int lm75_read_temp(struct i2c_client *client)
{
int temp;
temp = i2c_smbus_read_word_swapped(client, LM75_REG_TEMP);
if (temp < 0)
return temp;
/* 转换温度值 */
return (temp >> 7) * 500; /* 0.5°C精度 */
}
3.3 I2C调试技巧
I2C总线常见问题及解决方法:
- 设备无响应:
- 检查设备地址是否正确
- 确认上拉电阻是否合适(通常4.7kΩ)
- 使用i2cdetect工具扫描总线
bash复制# 安装i2c-tools
sudo apt install i2c-tools
# 扫描I2C总线上的设备
i2cdetect -y 1
-
通信不稳定:
- 降低I2C时钟频率
- 检查PCB走线长度和干扰
- 确保电源稳定
-
SMBus兼容性问题:
- 部分设备只支持标准I2C协议
- 尝试使用i2c_transfer代替i2c_smbus接口
4. SPI与I2C对比与选型
4.1 协议特性对比
| 特性 | SPI | I2C |
|---|---|---|
| 通信方式 | 全双工 | 半双工 |
| 信号线数量 | 4线(3线可选) | 2线 |
| 最大速率 | 50MHz+ | 3.4MHz(高速模式) |
| 寻址方式 | 片选信号 | 7/10位地址 |
| 拓扑结构 | 点对点/菊花链 | 多主多从总线 |
| 硬件复杂度 | 较高 | 较低 |
4.2 应用场景选择
根据多年开发经验,我总结出以下选型建议:
-
选择SPI的场景:
- 需要高速数据传输(>1MHz)
- 点对点通信或设备数量少
- 需要全双工通信
- 对实时性要求高
-
选择I2C的场景:
- 设备数量多且速率要求不高
- PCB布线空间受限
- 需要热插拔支持
- 系统复杂度要求低
4.3 性能优化技巧
- SPI性能优化:
- 使用DMA传输减少CPU占用
- 合理设置SPI模式(CPOL/CPHA)
- 批量传输减少片选切换开销
c复制/* 使用spi_message进行批量传输 */
struct spi_message msg;
struct spi_transfer xfers[2];
spi_message_init(&msg);
memset(xfers, 0, sizeof(xfers));
xfers[0].tx_buf = cmd;
xfers[0].len = cmd_len;
spi_message_add_tail(&xfers[0], &msg);
xfers[1].rx_buf = data;
xfers[1].len = data_len;
spi_message_add_tail(&xfers[1], &msg);
ret = spi_sync(spi, &msg);
- I2C性能优化:
- 使用SMBus块传输协议
- 减少不必要的重复地址写入
- 合理设置重试次数和超时时间
5. 常见问题与解决方案
5.1 SPI常见问题
-
数据错位或丢失:
- 检查CPOL/CPHA设置是否与设备匹配
- 确认时钟极性是否正确
- 降低时钟频率测试
-
片选信号异常:
- 确认片选信号极性设置
- 检查GPIO配置是否正确
- 确保片选信号在传输间有足够空闲时间
-
DMA传输失败:
- 检查DMA缓冲区是否cache对齐
- 确认DMA通道配置正确
- 使用dma_alloc_coherent分配缓冲区
5.2 I2C常见问题
- 总线锁死:
- 检查是否有设备拉低SCL/SDA不放
- 尝试发送STOP条件恢复总线
- 必要时复位I2C控制器
c复制/* 恢复I2C总线的示例代码 */
void i2c_recover_bus(struct i2c_adapter *adap)
{
int i;
/* 发送9个时钟脉冲 */
for (i = 0; i < 9; i++) {
gpio_direction_output(adap->scl_gpio, 0);
udelay(5);
gpio_direction_output(adap->scl_gpio, 1);
udelay(5);
}
/* 发送STOP条件 */
gpio_direction_output(adap->sda_gpio, 0);
udelay(5);
gpio_direction_output(adap->scl_gpio, 1);
udelay(5);
gpio_direction_output(adap->sda_gpio, 1);
}
-
从设备无ACK响应:
- 确认设备地址正确
- 检查设备是否上电正常
- 测量总线电压是否达标
-
通信速率不稳定:
- 检查上拉电阻值是否合适
- 确认总线电容是否过大
- 降低时钟频率测试
在实际项目中,我发现很多I2C问题都是由于上拉电阻不合适导致的。根据总线长度和负载情况,通常选择2.2kΩ到10kΩ之间的上拉电阻。过小的电阻会增加功耗,过大的电阻则会影响上升时间。