Linux内核SPI控制器驱动:FIFO与DMA传输优化 - 第1节
1.1 SPI 总线物理层与信号定义
SPI(Serial Peripheral Interface,串行外设接口)是一种全双工、同步、主从式的串行通信总线,由 Motorola 公司于 20 世纪 80 年代提出。在物理层上,标准 SPI 总线采用 四线制 结构,分别承担时钟、数据输出、数据输入和片选功能。
| 信号线 | 全称 | 方向(主设备视角) | 功能描述 |
|---|---|---|---|
| SCK | Serial Clock | 输出 | 由主设备产生,同步数据传输的时钟信号 |
| MOSI | Master Out Slave In | 输出 | 主设备发送数据至从设备的数据线 |
| MISO | Master In Slave Out | 输入 | 从设备发送数据至主设备的数据线 |
| CS | Chip Select | 输出 | 片选信号,低电平有效,用于选中目标从设备 |
关键特性: SPI 是全双工通信,在每个 SCK 时钟周期内,主设备通过 MOSI 发送一位数据,同时通过 MISO 接收一位数据。这种对称结构使得 SPI 在寄存器读写、传感器数据采集等场景中具有天然优势。
嵌入式工程师提示: 在硬件设计时,MOSI 与 MISO 不可接反。若主从设备均为 3.3V 逻辑电平,建议在靠近主设备端串联 22Ω~33Ω 电阻以抑制信号过冲。
1.2 四线制时序与数据采样
SPI 的时序核心在于 SCK 时钟边沿 与 数据变化/采样 的配合。下图展示了标准 SPI 单次传输的时序关系:
上图展示了 CPOL=0, CPHA=0 模式下的时序:
- CS 拉低后,SCK 开始产生连续时钟脉冲。
- 主设备在 SCK 的 上升沿 采样 MISO 上的数据(从设备输出)。
- 主设备在 SCK 的 下降沿 更新 MOSI 上的数据(从设备输入)。
- 一次传输通常为 8 位或 16 位,传输完成后 CS 拉高。
时序避坑: 在高速 SPI(≥20MHz)下,信号线之间的走线长度差会导致 时钟偏移。建议在 PCB 布局时使 SCK、MOSI、MISO 三线等长,并在从设备端靠近引脚处放置 10pF~22pF 对地电容以抑制振铃。
1.3 SPI 四种工作模式(CPOL / CPHA)
SPI 协议通过 时钟极性(CPOL) 和 时钟相位(CPHA) 两个参数组合出四种工作模式。CPOL 决定 SCK 空闲时的电平,CPHA 决定数据采样发生在 SCK 的哪个边沿。
| 模式 | CPOL | CPHA | 空闲 SCK 电平 | 数据采样边沿 | 数据变化边沿 | 典型从设备 |
|---|---|---|---|---|---|---|
| 模式 0 | 0 | 0 | 低电平 | 上升沿 | 下降沿 | W25Qxx Flash, ADXL345 |
| 模式 1 | 0 | 1 | 低电平 | 下降沿 | 上升沿 | MAX31855, MCP3008 |
| 模式 2 | 1 | 0 | 高电平 | 下降沿 | 上升沿 | LTC6804, 部分 DAC |
| 模式 3 | 1 | 1 | 高电平 | 上升沿 | 下降沿 | SD 卡(SPI 模式), CC1101 |
记忆口诀: “CPOL 看空闲,低 0 高 1;CPHA 看采样,第一个边沿为 0,第二个边沿为 1。” 模式 0 和模式 3 最为常用,分别对应空闲低电平/上升沿采样和空闲高电平/上升沿采样。
下面通过一张 SVG 结构图对比四种模式的 SCK 波形与采样点:
1.4 多从设备拓扑与片选管理
SPI 总线支持 一主多从 拓扑结构。主设备通过独立的 CS 线分别控制每个从设备,而 SCK、MOSI、MISO 则为所有从设备共享。
驱动开发提示: 在 Linux 内核中,SPI 从设备通过设备树描述,每个从设备节点包含 reg 属性(对应 CS 的 GPIO 编号或硬件片选索引)和 spi-max-frequency 属性。主控制器驱动在发起传输时,会通过 .set_cs() 回调函数控制 CS 电平。
1.5 SPI 传输的基本流程
一次完整的 SPI 传输在软件层面通常包含以下步骤:
- 配置模式与速率: 根据从设备数据手册设置 CPOL、CPHA 以及 SCK 频率。
- 构建消息: 将待发送数据填充到发送缓冲区,准备接收缓冲区。
- 发起传输: 主控制器驱动将消息提交到 SPI 控制器硬件。
- 硬件移位: 每个 SCK 周期,MOSI 移出一位,MISO 移入一位,8/16 位后完成一个字节/半字传输。
- 传输完成中断或 DMA 完成回调: 通知上层数据已就绪。
- 处理接收数据: 从接收缓冲区读取从设备返回的数据。
性能要点: 在 Linux 内核中,SPI 传输通常通过 spi_message 和 spi_transfer 结构体组织。一次 spi_message 可以包含多个 spi_transfer 段,每个段可以有不同的长度、速率和延迟,这为实现复杂协议(如先发送命令再接收数据)提供了灵活性。
下面通过一个简单的代码片段展示 SPI 传输在用户空间的典型调用方式(通过 spidev 驱动):
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int spi_transfer(int fd, uint8_t *tx, uint8_t *rx, int len) {
struct spi_ioc_transfer tr = {
.tx_buf = (unsigned long)tx,
.rx_buf = (unsigned long)rx,
.len = len,
.speed_hz = 1000000, // 1 MHz
.delay_usecs = 0,
.bits_per_word = 8,
.cs_change = 0,
};
return ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
}
int main() {
int fd = open("/dev/spidev0.0", O_RDWR);
if (fd < 0) return -1;
uint8_t mode = SPI_MODE_0;
ioctl(fd, SPI_IOC_WR_MODE, &mode);
uint8_t tx_buf[4] = {0x90, 0x00, 0x00, 0x00}; // 读 ID 命令
uint8_t rx_buf[4] = {0};
spi_transfer(fd, tx_buf, rx_buf, 4);
// rx_buf[1..3] 包含设备 ID
close(fd);
return 0;
}
常见错误: 在 SPI 传输中,如果接收缓冲区未清零,返回数据中可能混入旧数据。建议每次传输前使用 memset 将 rx_buf 清零。另外,spidev 设备节点需要确保用户空间有访问权限,通常通过 udev 规则设置 0666 权限。
1.6 本章小结
本章从物理层信号定义出发,详细讲解了 SPI 四线制的时序关系、