1. SPI通信协议深度解析
SPI(Serial Peripheral Interface)作为一种高速全双工同步串行通信协议,在嵌入式系统和硬件开发中占据着不可替代的地位。我第一次接触SPI是在2013年开发智能家居控制器时,当时需要驱动一块TFT显示屏,对比了I2C和SPI后,最终选择了SPI方案——这个决定让我深刻体会到了SPI在速度和可靠性上的优势。
SPI协议由Motorola在1980年代提出,经过几十年的发展已经成为硬件工程师的必备技能。与UART和I2C相比,SPI最大的特点是采用主从架构和全双工通信,时钟速度可达数十MHz,特别适合显示器、存储设备、传感器等对实时性要求高的外设连接。在实际项目中,SPI接口的灵活性和可扩展性常常能解决许多棘手的问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. SPI协议核心机制
2.1 四线制基础架构
标准SPI协议采用四线制连接方式:
- SCLK(Serial Clock):主设备产生的同步时钟信号
- MOSI(Master Out Slave In):主设备输出,从设备输入
- MISO(Master In Slave Out):从设备输出,主设备输入
- SS/CS(Slave Select/Chip Select):从设备片选信号(低电平有效)
重要提示:实际布线时,SCLK线要尽可能短并远离高频干扰源,我在多个项目中都遇到过因时钟信号质量问题导致的通信失败案例。
2.2 时钟极性与相位
SPI模式由CPOL(Clock Polarity)和CPHA(Clock Phase)两个参数决定,组合出四种工作模式:
| 模式 | CPOL | CPHA | 时钟空闲状态 | 数据采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 上升沿 |
| 1 | 0 | 1 | 低电平 | 下降沿 |
| 2 | 1 | 0 | 高电平 | 下降沿 |
| 3 | 1 | 1 | 高电平 | 上升沿 |
在STM32开发中,我习惯使用Mode 0(CPOL=0, CPHA=0)作为默认配置,因为大多数SPI Flash和显示模块都兼容这种模式。但遇到ADXL345加速度传感器时,必须切换到Mode 3才能正常通信——这个坑让我调试了整整一个下午。
2.3 多从机扩展方案
SPI支持三种多从机连接方式:
- 独立片选:每个从设备有独立的CS线,主设备通过拉低对应CS线选择通信对象
- 菊花链:多个从设备串联,数据依次传递(需要设备支持)
- 软件片选:通过GPIO模拟片选信号(灵活性高但占用CPU资源)
在树莓派驱动多个SPI设备时,我推荐使用74HC138等译码器扩展片选信号。曾在一个工业控制器项目中,我们成功通过1个SPI接口控制了8个不同的传感器模块。
3. SPI硬件实现细节
3.1 主从设备时钟同步
SPI的同步机制看似简单,实则暗藏玄机。主设备通过SCLK控制整个通信节奏,但从设备必须能在指定时钟沿准确采样数据。在FPGA实现SPI从机时,我通常会这样做:
verilog复制always @(posedge SCLK or negedge SS) begin
if(!SS) begin
// 片选无效时复位状态机
state <= IDLE;
end else begin
case(state)
IDLE: if(MOSI) state <= RECEIVE_CMD;
RECEIVE_CMD: // 命令解析状态...
endcase
end
end
这个代码片段展示了典型的SPI从机状态机设计要点:必须考虑片选信号异步复位,同时根据CPHA决定在哪个时钟沿采样数据。
3.2 电气特性优化
SPI接口的稳定性很大程度上取决于电路设计:
- 上拉电阻:虽然SPI理论上不需要上拉,但在长距离传输时,我习惯在SCLK和CS线上加4.7kΩ上拉
- 阻抗匹配:当频率超过10MHz时,传输线效应明显,需要在末端加50Ω终端电阻
- 信号完整性:使用差分探头测量SPI波形时,要特别注意建立时间和保持时间是否满足器件要求
在一次高速SPI Flash读写测试中,我们发现MOSI信号出现振铃现象,通过缩短走线长度并串联33Ω电阻成功解决了问题。
4. 典型应用场景实现
4.1 STM32驱动SPI Flash
以STM32F103为例,使用HAL库操作W25Q128 Flash的典型流程:
- 初始化SPI外设(注意时钟分频设置)
c复制hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hsp1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
HAL_SPI_Init(&hspi1);
- 实现页编程函数
c复制void FLASH_PageProgram(uint32_t addr, uint8_t *data, uint16_t len)
{
uint8_t cmd[4] = {0x02, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF};
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
HAL_SPI_Transmit(&hspi1, data, len, HAL_MAX_DELAY);
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET);
FLASH_WaitForWriteEnd();
}
经验之谈:在写入操作后必须等待Flash内部编程完成,我见过太多因忽略这个等待导致的数据损坏案例。
4.2 FPGA作为SPI主设备
在Zynq 7000平台上,PL端通过EMIO实现SPI主机的Verilog核心代码:
verilog复制// SPI时钟生成逻辑
always @(posedge clk) begin
if(!enable) begin
sclk <= (CPOL == 1'b1);
bit_cnt <= 0;
end else begin
if(clk_div == DIVIDER/2-1) begin
sclk <= ~sclk;
if(sclk == (CPHA ? 1'b0 : 1'b1)) begin
// 在适当边沿采样/输出数据
if(bit_cnt < 8) begin
mosi <= tx_data[7-bit_cnt];
bit_cnt <= bit_cnt + 1;
end else begin
done <= 1'b1;
end
end
if(sclk == (CPHA ? 1'b1 : 1'b0)) begin
rx_data[7-bit_cnt] <= miso;
end
end
end
end
这段代码实现了可配置CPOL/CPHA的SPI主机,关键点在于精确控制时钟边沿与数据采样的关系。在Xilinx Vivado中综合后,可以达到50MHz的SPI时钟速率。
5. 高级应用与性能优化
5.1 DMA加速SPI传输
在需要高速连续传输的场景(如TFT屏刷新),使用DMA可以大幅降低CPU开销。以STM32H743驱动ST7735S屏幕为例:
c复制// 初始化SPI DMA
hdma_spi1_tx.Instance = DMA2_Stream3;
hdma_spi1_tx.Init.Request = DMA_REQUEST_SPI1_TX;
HAL_DMA_Init(&hdma_spi1_tx);
// 批量发送函数
void TFT_SendData_DMA(uint8_t *data, uint32_t len)
{
HAL_SPI_Transmit_DMA(&hspi1, data, len);
while(hspi1.State != HAL_SPI_STATE_READY);
}
实测显示,使用DMA后SPI传输效率提升约60%,CPU占用率从85%降至30%。但要注意DMA缓冲区地址必须对齐,我曾遇到过因缓冲区未4字节对齐导致的传输错误。
5.2 六线SPI(Quad SPI)应用
现代SPI Flash(如W25Q256)支持Quad SPI模式,将数据传输线扩展到4条(IO0-IO3)。配置要点包括:
- 发送0x35命令进入Quad模式
- 重新配置GPIO为双向模式
- 使用特殊指令(如0xEB Fast Read Quad Output)
在RT-Thread中操作QSPI Flash的典型代码:
c复制struct rt_qspi_device qspi_dev;
rt_qspi_configure(&qspi_dev, RT_QSPI_CFG_DEFAULT);
uint8_t cmd = 0xEB; // Quad Output Fast Read
struct rt_qspi_message msg = {
.instruction.content = cmd,
.instruction.qspi_lines = 1,
.address.qspi_lines = 4,
.qspi_data_lines = 4,
};
rt_qspi_send_message(&qspi_dev, &msg);
六线SPI的理论带宽是标准SPI的4倍,但布线难度也相应增加,需要特别注意信号等长设计。
6. 常见问题与调试技巧
6.1 典型故障排查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 通信完全无响应 | CS线接触不良 | 检查CS线连接和电平 |
| 数据位错位 | CPOL/CPHA设置错误 | 确认主从设备模式匹配 |
| 高频时数据错误 | 信号完整性问题 | 缩短走线,加终端电阻 |
| 只能单向通信 | MISO/MOSI接反 | 交换两条数据线 |
| 随机出现数据错误 | 电源噪声干扰 | 增加去耦电容,检查地线 |
6.2 逻辑分析仪抓包技巧
使用Saleae Logic分析SPI信号时,建议:
- 设置正确的阈值电压(通常3.3V系统设为1.65V)
- 先捕获低速信号(如1MHz)验证协议正确性
- 使用异步捕获模式分析建立/保持时间
- 对CS信号添加触发条件,过滤无关数据
我曾通过逻辑分析仪发现一个隐蔽的问题:某SPI从设备要求在CS拉低后至少延迟100ns才能发送第一个时钟,而主设备没有满足这个要求,导致初始化失败。
6.3 软件模拟SPI的注意事项
当硬件SPI资源不足时,GPIO模拟是常见解决方案,但要注意:
- 时钟速度不要超过1MHz(STM32的IO翻转极限约5-10MHz)
- 禁用中断或确保中断处理非常快
- 适当加入nop延迟保证时序
- 使用查表法优化位操作
以下是模拟SPI读取MAX6675热电偶的代码片段:
c复制uint16_t MAX6675_Read(void)
{
uint16_t temp = 0;
CS_LOW();
delay_us(1);
for(int i=15; i>=0; i--) {
SCK_HIGH();
delay_us(1);
if(SDO_READ()) temp |= (1<<i);
SCK_LOW();
delay_us(1);
}
CS_HIGH();
return temp;
}
这个实现的关键是精确控制延时,MAX6675要求SCK高电平至少100ns,使用delay_us(1)能提供足够余量。
