1. SPI通信基础与STM32实现
SPI(Serial Peripheral Interface)作为嵌入式系统中最常用的通信协议之一,以其简单高效的特性在各类外设连接中占据重要地位。我在多个STM32项目中实际使用SPI驱动过Flash存储器、TFT屏幕和各类传感器,深刻体会到理解SPI底层机制对解决实际问题的重要性。
1.1 SPI协议核心特性
SPI本质上是一个同步串行通信接口,具有以下关键特征:
- 全双工通信:数据可以同时收发(虽然实际应用中很多情况是半双工使用)
- 主从架构:由主机产生时钟信号,从设备同步响应
- 硬件简单:仅需4根信号线(标准配置)即可实现通信
- 高速传输:理论上可达几十MHz(实际受限于器件特性)
在我的项目经验中,SPI最典型的应用场景包括:
- NOR Flash存储器(如W25Q系列)
- 显示屏控制器(如ILI9341)
- 高精度ADC芯片(如ADS131E08)
- 数字传感器(如L3GD20陀螺仪)
实际应用中需注意:虽然SPI标准定义是全双工,但很多器件实际工作在半双工模式,此时MOSI和MISO不会同时有效。
1.2 STM32的SPI外设特点
STM32F1系列提供最多3个独立SPI接口(SPI1-SPI3),各外设具有以下特性:
| 特性 | SPI1 | SPI2/SPI3 |
|---|---|---|
| 时钟源 | APB2(最高72MHz) | APB1(最高36MHz) |
| 功能完整性 | 全功能 | 全功能 |
| DMA支持 | 支持 | 支持 |
| 中断源 | 独立中断向量 | 共享中断向量 |
我在实际项目中发现,SPI1由于挂载在高速APB2总线上,更适合高速数据传输场景,而SPI2/3更适合中低速外设连接。
2. SPI硬件架构深度解析
2.1 STM32 SPI内部结构
STM32的SPI外设由以下几个关键部分组成:
-
控制逻辑单元:
- 包含CR1/CR2两个控制寄存器
- 负责时钟生成、模式配置等核心功能
-
数据通路:
- 发送缓冲区(8位或16位)
- 接收缓冲区(8位或16位)
- 移位寄存器(实现串并转换)
-
时钟系统:
- 可编程预分频器(2-256分频)
- 支持多种极性和相位配置
-
状态监控:
- 通过SR寄存器反映传输状态
- 提供TXE(发送缓冲区空)、RXNE(接收非空)等状态位
2.2 数据收发机制
SPI数据传输遵循严格的同步时序:
-
发送过程:
- 数据写入DR寄存器后进入发送缓冲区
- 移位寄存器逐位将数据输出到MOSI线
- 每移出1位,同时移入1位到接收端
-
接收过程:
- 接收到的数据存入接收缓冲区
- 读取DR寄存器可获取接收数据
- 新数据到达会置位RXNE标志
我在调试中发现一个关键细节:读取DR寄存器会自动清除RXNE标志,这一点在中断处理中尤为重要,忘记读取会导致后续中断无法触发。
3. SPI工作模式详解
3.1 时钟极性与相位
SPI模式由CPOL(时钟极性)和CPHA(时钟相位)两个参数决定:
| 模式 | CPOL | CPHA | 空闲时钟 | 采样边沿 | 更新边沿 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 低 | 上升沿 | 下降沿 |
| 1 | 0 | 1 | 低 | 下降沿 | 上升沿 |
| 2 | 1 | 0 | 高 | 下降沿 | 上升沿 |
| 3 | 1 | 1 | 高 | 上升沿 | 下降沿 |
实际项目中,模式0和模式3最为常用。例如:
- W25Q Flash通常使用模式0
- ADXL345加速度计使用模式3
重要经验:从设备的手册中确认其要求的SPI模式至关重要,模式不匹配会导致通信完全失败且难以诊断。
3.2 多从机连接方案
当系统需要连接多个SPI从设备时,有以下几种典型方案:
-
独立片选方案:
- 每个从设备使用独立的GPIO作为片选
- 优点:各设备完全独立,互不干扰
- 缺点:占用较多GPIO资源
-
菊花链方案:
- 多个设备共用片选信号
- 数据从一个设备的MISO连接到下一个设备的MOSI
- 优点:节省GPIO和布线
- 缺点:所有设备必须支持该模式,且通信效率较低
-
SPI开关方案:
- 使用专用SPI开关芯片(如ADG1412)
- 通过控制信号切换SPI通路
- 优点:灵活性强,支持热插拔
- 缺点:增加硬件成本
在我的一个工业控制器项目中,采用了方案1连接了Flash、显示屏和ADC三个设备,通过精心设计片选时序,实现了稳定可靠的通信。
4. STM32 SPI寄存器级编程
4.1 关键寄存器功能
-
SPI_CR1(控制寄存器1):
- 位15:BIDIMODE(双向模式)
- 位14:BIDIOE(双向输出使能)
- 位13:CRCEN(CRC校验使能)
- 位11:DFF(数据帧格式,0=8位,1=16位)
- 位9:SSM(软件片选管理)
- 位8:SSI(内部片选值)
- 位7:LSBFIRST(数据传输顺序)
- 位5-3:BR[2:0](波特率分频)
- 位1:CPOL(时钟极性)
- 位0:CPHA(时钟相位)
-
SPI_SR(状态寄存器):
- 位7:BSY(忙标志)
- 位6:OVR(溢出错误)
- 位5:MODF(模式错误)
- 位4:CRCERR(CRC错误)
- 位1:TXE(发送缓冲区空)
- 位0:RXNE(接收缓冲区非空)
4.2 典型初始化流程
以下是SPI主机模式的初始化代码示例:
c复制void SPI1_Init(void)
{
// 1. 使能SPI1时钟
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
// 2. 配置GPIO(以PA5/6/7为例)
GPIOA->CRL &= ~(GPIO_CRL_CNF5 | GPIO_CRL_CNF6 | GPIO_CRL_CNF7);
GPIOA->CRL |= GPIO_CRL_CNF5_1 | GPIO_CRL_CNF6_1 | GPIO_CRL_CNF7_1; // 复用推挽输出
GPIOA->CRL |= GPIO_CRL_MODE5 | GPIO_CRL_MODE6 | GPIO_CRL_MODE7; // 50MHz速度
// 3. 复位SPI1
RCC->APB2RSTR |= RCC_APB2RSTR_SPI1RST;
RCC->APB2RSTR &= ~RCC_APB2RSTR_SPI1RST;
// 4. 配置SPI1
SPI1->CR1 = SPI_CR1_MSTR | // 主机模式
SPI_CR1_BR_0 | // 分频系数4 (APB2=72MHz, SPI=18MHz)
SPI_CR1_SSM | // 软件片选管理
SPI_CR1_SSI | // 内部片选信号
SPI_CR1_SPE; // 使能SPI
// 5. 配置片选GPIO(以PA4为例)
GPIOA->CRL &= ~GPIO_CRL_CNF4;
GPIOA->CRL |= GPIO_CRL_MODE4; // 推挽输出
SPI1_CS_HIGH(); // 初始置高
}
5. HAL库驱动开发实践
5.1 HAL_SPI初始化结构体
HAL库提供了SPI_HandleTypeDef结构体来管理SPI配置:
c复制typedef struct {
SPI_TypeDef *Instance; // SPI外设基地址
SPI_InitTypeDef Init; // SPI初始化参数
uint8_t *pTxBuffPtr; // 发送缓冲区指针
uint16_t TxXferSize; // 发送数据大小
__IO uint16_t TxXferCount; // 发送计数器
uint8_t *pRxBuffPtr; // 接收缓冲区指针
uint16_t RxXferSize; // 接收数据大小
__IO uint16_t RxXferCount; // 接收计数器
DMA_HandleTypeDef *hdmatx; // 发送DMA句柄
DMA_HandleTypeDef *hdmarx; // 接收DMA句柄
HAL_LockTypeDef Lock; // 锁对象
__IO HAL_SPI_StateTypeDef State; // SPI状态
__IO uint32_t ErrorCode; // 错误代码
} SPI_HandleTypeDef;
5.2 典型HAL库使用流程
- 初始化示例:
c复制SPI_HandleTypeDef hspi1;
void MX_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_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 7;
if (HAL_SPI_Init(&hspi1) != HAL_OK)
{
Error_Handler();
}
}
- 数据传输函数:
HAL库提供了多种传输方式:
- 阻塞模式:
HAL_SPI_Transmit/Receive/TransmitReceive - 中断模式:
HAL_SPI_Transmit_IT/Receive_IT/TransmitReceive_IT - DMA模式:
HAL_SPI_Transmit_DMA/Receive_DMA/TransmitReceive_DMA
阻塞模式示例:
c复制uint8_t txData[4] = {0x01, 0x02, 0x03, 0x04};
uint8_t rxData[4] = {0};
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // 拉低片选
HAL_SPI_TransmitReceive(&hspi1, txData, rxData, 4, 100); // 超时100ms
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 拉高片选
6. SPI应用中的常见问题与解决方案
6.1 典型问题排查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 通信完全无响应 | 1. 片选信号未正确控制 2. 时钟极性/相位配置错误 3. 从设备未上电 |
1. 检查片选GPIO配置 2. 确认模式匹配 3. 检查电源连接 |
| 数据错位或错误 | 1. 时钟速度过快 2. 信号完整性问题 3. 位序配置错误 |
1. 降低时钟频率 2. 缩短走线或加终端电阻 3. 检查LSBFIRST设置 |
| 偶尔出现数据丢失 | 1. 未正确处理状态标志 2. 中断优先级冲突 3. 缓冲区溢出 |
1. 添加状态检查逻辑 2. 调整中断优先级 3. 增加流控机制 |
| DMA传输不完整 | 1. DMA配置错误 2. 缓冲区未对齐 3. 内存访问冲突 |
1. 检查DMA通道配置 2. 确保缓冲区地址对齐 3. 使用DMA专用内存区域 |
6.2 信号完整性问题处理
在高速SPI通信(>10MHz)时,信号完整性问题尤为突出。以下是我在实践中总结的解决方案:
-
PCB设计要点:
- 保持SPI信号线等长(特别是SCK与数据线)
- 避免90度拐角,使用弧形走线
- 在信号线旁布置地线提供回流路径
-
终端匹配方案:
- 对于长走线(>10cm),在接收端添加33Ω串联电阻
- 在信号线上并联100pF电容可减少振铃
-
电源去耦:
- 每个SPI器件VCC引脚就近放置0.1μF陶瓷电容
- 高频应用可额外添加1μF钽电容
6.3 软件优化技巧
- 高效片选控制:
c复制// 不推荐的通用做法
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(...);
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
// 优化方案:直接操作寄存器
CS_GPIO_Port->BSRR = CS_Pin << 16; // 拉低
__DSB(); // 确保指令执行完成
HAL_SPI_Transmit(...);
CS_GPIO_Port->BSRR = CS_Pin; // 拉高
-
DMA传输优化:
- 使用双缓冲技术避免传输间隙
- 对齐缓冲区到32字节边界提升DMA效率
- 在DMA完成中断中处理数据,避免轮询等待
-
中断处理优化:
c复制void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
if(hspi->Instance == SPI1)
{
// 处理SPI1完成中断
// 避免在此处执行耗时操作
}
}
7. 高级应用:SPI Flash驱动实现
以常见的W25Q128FV Flash芯片为例,展示完整SPI驱动实现:
7.1 基本命令定义
c复制#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
#define W25X_ReadStatusReg1 0x05
#define W25X_ReadStatusReg2 0x35
#define W25X_ReadData 0x03
#define W25X_FastRead 0x0B
#define W25X_PageProgram 0x02
#define W25X_SectorErase 0x20
#define W25X_ChipErase 0xC7
#define W25X_ReadID 0x9F
7.2 关键函数实现
- 读取ID函数:
c复制uint32_t W25Q_ReadID(void)
{
uint8_t cmd = W25X_ReadID;
uint8_t id[3] = {0};
W25Q_CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
HAL_SPI_Receive(&hspi1, id, 3, 100);
W25Q_CS_HIGH();
return (id[0] << 16) | (id[1] << 8) | id[2];
}
- 扇区擦除函数:
c复制void W25Q_SectorErase(uint32_t addr)
{
uint8_t cmd[4] = {
W25X_SectorErase,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF
};
W25Q_WriteEnable();
W25Q_CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, 100);
W25Q_CS_HIGH();
while(W25Q_IsBusy()); // 等待擦除完成
}
- 页编程函数:
c复制void W25Q_PageProgram(uint32_t addr, uint8_t *data, uint16_t len)
{
uint8_t cmd[4] = {
W25X_PageProgram,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF
};
W25Q_WriteEnable();
W25Q_CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, 100);
HAL_SPI_Transmit(&hspi1, data, len, 100);
W25Q_CS_HIGH();
while(W25Q_IsBusy());
}
7.3 性能优化技巧
-
批量操作优化:
- 连续写入时保持片选有效,避免重复发送地址
- 使用
HAL_SPI_Transmit的DMA模式实现后台传输
-
状态轮询优化:
c复制uint8_t W25Q_IsBusy(void)
{
uint8_t cmd = W25X_ReadStatusReg1;
uint8_t status;
W25Q_CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, 10);
HAL_SPI_Receive(&hspi1, &status, 1, 10);
W25Q_CS_HIGH();
return (status & 0x01); // BUSY位
}
- 高速读取实现:
c复制void W25Q_FastRead(uint32_t addr, uint8_t *buf, uint32_t len)
{
uint8_t cmd[5] = {
W25X_FastRead,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF,
0xFF // dummy byte
};
W25Q_CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 5, 10);
HAL_SPI_Receive(&hspi1, buf, len, HAL_MAX_DELAY);
W25Q_CS_HIGH();
}
在实际项目中,合理使用这些优化技巧可以将SPI Flash的读写性能提升2-3倍,特别是在大数据量连续访问的场景下效果尤为明显。