SPI(Serial Peripheral Interface)作为一种同步串行通信协议,在嵌入式系统中扮演着重要角色。我最近花了三周时间深入研究了Linux内核中的SPI子系统实现,这个看似简单的四线制接口(SCLK、MOSI、MISO、SS)背后隐藏着令人惊叹的软件架构设计。在实际项目中,无论是连接传感器、存储器还是显示设备,SPI都是工程师们最常打交道的接口之一。
与I2C相比,SPI的最大特点是全双工通信和更高的传输速率(通常可达几十MHz)。不过这种性能优势也带来了更复杂的时序控制需求。Linux内核通过精心设计的SPI子系统,为不同硬件平台提供了统一的抽象接口,让驱动开发者可以专注于业务逻辑而非底层硬件差异。
Linux的SPI子系统采用典型的三层架构设计:
硬件抽象层(Controller Driver):
负责与具体SoC的SPI控制器硬件交互,比如注册中断处理函数、配置DMA通道、设置时钟分频等。以流行的STM32系列为例,其驱动代码位于drivers/spi/spi-stm32.c,需要处理STM32特有的CR1/CR2寄存器配置。
核心层(SPI Core):
提供核心基础设施,包括:
struct bus_type spi_bus_type)message->queue)/sys/kernel/debug/spi/)协议驱动层(Protocol Driver):
实现具体设备的功能逻辑,比如:
理解SPI子系统必须掌握这几个核心结构体:
c复制struct spi_device {
struct device dev;
struct spi_controller *controller;
u32 max_speed_hz; // 设备支持的最大时钟频率
u8 chip_select; // 片选信号线编号
u32 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;
};
struct spi_message {
struct list_head transfers;
void (*complete)(void *context);
void *context;
...
};
特别要注意spi_transfer结构体中的delay_usecs字段,这个参数在实际调试中经常被忽视。当连接不同厂商的设备时,适当的延迟设置可以解决很多时序问题。
SPI子系统的数据传输采用消息队列机制,典型流程如下:
spi_message_init()初始化消息结构spi_message_add_tail()添加一个或多个传输段(transfer)spi_sync()或spi_async()提交消息关键点在于spi_transfer的灵活组合。比如读取SD卡CID时,需要先发送CMD10命令(1字节),等待8个时钟周期,然后接收16字节响应。这可以通过构建包含两个transfer的消息来实现:
c复制struct spi_transfer t[2] = {
{ .tx_buf = &cmd, .len = 1 },
{ .rx_buf = buffer, .len = 16 }
};
SPI有四种基本工作模式,由CPOL和CPHA两个参数决定:
| 模式 | CPOL | CPHA | 时钟极性 | 采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 上升沿 |
| 1 | 0 | 1 | 低电平 | 下降沿 |
| 2 | 1 | 0 | 高电平 | 下降沿 |
| 3 | 1 | 1 | 高电平 | 上升沿 |
在设备树中配置模式的示例如下:
dts复制&spi1 {
status = "okay";
cs-gpios = <&gpioa 4 GPIO_ACTIVE_LOW>;
sensor@0 {
compatible = "vendor,spi-sensor";
spi-max-frequency = <10000000>;
reg = <0>;
spi-cpol; // 模式1或3
spi-cpha; // 模式2或3
};
};
对于高速SPI设备(如QSPI Flash),启用DMA可以显著降低CPU负载。在STM32平台上需要特别注意:
dts复制&spi2 {
dmas = <&dmamux1 1 0x10>,
<&dmamux1 2 0x10>;
dma-names = "tx", "rx";
};
code复制CONFIG_SPI_STM32=y
CONFIG_SPI_STM32_DMA=y
当多个SPI设备共享同一控制器时,片选信号的管理尤为关键。常见问题包括:
片选信号干扰:在切换设备时,确保前一个设备的片选已经完全失效。添加ndelay(100)级别的延迟往往能解决问题。
速度兼容性:总线上的所有设备必须能适应主控设置的时钟频率。建议在probe函数中添加检查:
c复制if (spi->max_speed_hz > DEVICE_MAX_FREQ) {
dev_warn(&spi->dev, "降频至10MHz");
spi->max_speed_hz = 10000000;
}
当通信异常时,逻辑分析仪是最直接的调试工具。推荐配置:
常见异常波形分析:
SPI子系统提供了丰富的调试接口:
bash复制# 查看所有SPI控制器
ls /sys/bus/spi/devices/
# 查看特定设备的传输统计
cat /sys/bus/spi/devices/spi0.0/statistics
bash复制# 启用SPI核心调试信息
echo -n 'file spi.c +p' > /sys/kernel/debug/dynamic_debug/control
# 启用特定控制器的调试
echo 8 > /sys/module/spi_stm32/parameters/debug
bash复制echo function > /sys/kernel/debug/tracing/current_tracer
echo spi_sync >> /sys/kernel/debug/tracing/set_ftrace_filter
cat /sys/kernel/debug/tracing/trace_pipe
Linux 5.10引入的SPI-MEM子系统为存储器类设备提供了统一接口,主要改进包括:
迁移到新框架的驱动示例:
c复制static const struct spi_mem_op spi_nor_read_op = {
.data.dir = SPI_MEM_DATA_IN,
.data.nbytes = 4,
.data.buswidth = SPI_MEM_BUSWIDTH_4,
.addr.nbytes = 3,
.addr.buswidth = SPI_MEM_BUSWIDTH_1,
.dummy.nbytes = 1,
.dummy.buswidth = SPI_MEM_BUSWIDTH_4,
.cmd.opcode = SPINOR_OP_READ_4B,
.cmd.buswidth = SPI_MEM_BUSWIDTH_1,
};
通过spidev驱动可以直接在用户空间操作SPI设备:
dts复制&spi1 {
spidev@0 {
compatible = "rohm,dh2228fv";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
c复制int fd = open("/dev/spidev0.0", O_RDWR);
ioctl(fd, SPI_IOC_WR_MODE, SPI_MODE_0);
ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, 8);
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, 1000000);
c复制struct spi_ioc_transfer tr = {
.tx_buf = (unsigned long)tx_buf,
.rx_buf = (unsigned long)rx_buf,
.len = ARRAY_SIZE(tx_buf),
.delay_usecs = 10,
};
ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
在实际项目中,我发现合理利用SPI子系统的这些特性可以大幅提升开发效率。特别是在混合使用不同厂商的设备时,深入理解SPI核心的工作机制能帮助快速定位各种奇怪的兼容性问题。建议每个嵌入式开发者都花时间研读drivers/spi/spi.c的源码,这比任何文档都更能揭示子系统的设计精髓。