第一次接触SPI总线是在2015年调试一块传感器模块时。当时用示波器抓取信号,看到那四根线上跳动的波形,突然意识到数字通信的物理层原来如此直观。SPI(Serial Peripheral Interface)作为一种同步串行通信协议,以其简单高效的特性在嵌入式领域占据重要地位。
SPI本质上是一个主从架构的同步串行总线,由摩托罗拉在1980年代提出。与I2C不同,SPI采用全双工通信模式,理论上没有传输速率上限(实际受物理线路限制)。其核心优势在于硬件实现简单,不需要复杂的地址分配和应答机制,这使得它在高速数据传输场景中表现优异。
典型应用场景包括:
注意:SPI虽然协议简单,但在实际工程中,时钟极性和相位配置错误是最常见的通信失败原因。我在早期项目中至少三次因为CPOL/CPHA设置不当导致整天调试无果。
标准SPI总线包含四条信号线:
在STM32F4系列MCU上,GPIO初始化代码示例:
c复制GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// CS引脚需单独配置为普通输出
GPIO_InitStruct.Pin = GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
SPI的时钟极性(CPOL)和相位(CPHA)组合出四种工作模式:
| 模式 | CPOL | CPHA | 时钟空闲状态 | 采样时刻 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 第一个时钟边沿 |
| 1 | 0 | 1 | 低电平 | 第二个时钟边沿 |
| 2 | 1 | 0 | 高电平 | 第一个时钟边沿 |
| 3 | 1 | 1 | 高电平 | 第二个时钟边沿 |
在NXP Kinetis系列MCU中,通过SPIx_C1寄存器的CPHA和CPOL位配置:
c复制// 设置模式3 (CPOL=1, CPHA=1)
SPI0_C1 |= SPI_C1_CPOL_MASK | SPI_C1_CPHA_MASK;
经验:多数SPI Flash芯片使用模式0或模式3,而加速度计如MPU6050常用模式3。务必查阅器件手册确认,我曾因默认为模式0导致LIS3DH传感器无法通信。
以STM32 HAL库为例,完整初始化流程:
c复制SPI_HandleTypeDef hspi1;
void SPI1_Init(void)
{
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_HIGH; // CPOL=1
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA=1
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
if (HAL_SPI_Init(&hspi1) != HAL_OK) {
Error_Handler();
}
}
高速数据传输时需采用DMA方式,以STM32传输128字节为例:
c复制// DMA发送配置
HAL_SPI_Transmit_DMA(&hspi1, txBuffer, 128);
// 接收回调函数
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
if(hspi->Instance == SPI1) {
// 处理接收完成事件
}
}
关键参数计算:
在PCB设计阶段常见问题:
实测案例:在电机控制板上,SCLK线长超过15cm时,信号上升沿出现明显畸变。通过将GPIO驱动强度从Low改为High并缩短走线后改善。
三种典型连接方式:
独立片选(推荐)
菊花链(Daisy Chain)
逻辑解码
代码示例(轮询三个设备):
c复制void Read_SPI_Devices(void)
{
// 读取设备1
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
HAL_SPI_Receive(&hspi1, dev1_data, 3, 100);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
// 读取设备2
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
HAL_SPI_Receive(&hspi1, dev2_data, 3, 100);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
// 添加适当延时满足从设备恢复时间
HAL_Delay(1);
}
关键时序参数测量方法:
使用逻辑分析仪(如Saleae)捕获的实际波形分析:
查表法替代实时计算
c复制const uint8_t sin_table[256] = {...};
HAL_SPI_Transmit(&hspi1, (uint8_t*)&sin_table[phase], 1, 100);
使用位带操作加速GPIO控制
c复制#define CS_LOW() (*(__IO uint32_t*)0x42400000 = 0)
#define CS_HIGH() (*(__IO uint32_t*)0x42400000 = 1)
汇编优化关键段
assembly复制MOV R0, #0x40013000 ; SPI1基地址
MOV R1, #0xAA ; 待发送数据
STRB R1, [R0, #0x0C] ; 写入DR寄存器
设计通用的SPI接口结构体:
c复制typedef struct {
void (*Init)(void);
uint8_t (*Transfer)(uint8_t data);
void (*CS_Enable)(void);
void (*CS_Disable)(void);
} SPI_Driver;
// 具体平台实现
#ifdef STM32_PLATFORM
#include "stm32_spi.c"
#elif defined(ESP32_PLATFORM)
#include "esp32_spi.c"
#endif
虚拟SPI实现(软件模拟)
c复制void Soft_SPI_Write(uint8_t data) {
for(int i=0; i<8; i++) {
MOSI = (data & 0x80) ? 1 : 0;
SCLK = 1;
data <<= 1;
SCLK = 0;
}
}
兼容1-wire模式
扩展帧格式
c复制// 添加帧头和CRC校验
uint8_t frame[10] = {0xAA, 0x55, cmd, data0, data1, data2, crc};
HAL_SPI_Transmit(&hspi1, frame, sizeof(frame), 100);
在最近的一个工业HMI项目中,我们通过这种抽象设计实现了同一套显示驱动代码在STM32和GD32平台的无缝切换,节省了约40%的移植工作量。