《STM32 江湖 SPI 双绝:硬件外设与软件模拟的深度解析》
| 章节 | 技术类型 | 核心机制 | 适用场景 | 难度评级 |
|---|---|---|---|---|
| 第一章 | SPI 基础 | 串行通信协议 | 外设互联场景 | ★★☆☆☆ |
| 第二章 | 硬件 SPI | 内置外设实现 | 高速数据传输 | ★★★☆☆ |
| 第三章 | 模拟 SPI | GPIO 软件模拟 | 灵活适配场景 | ★★☆☆☆ |
| 第四章 | 技术对比 | 性能与特性差异 | 方案选型参考 | ★★★★☆ |
| 第五章 | 跨 MCU 移植 | 主频适配方法 | 多平台兼容 | ★★★☆☆ |
| 第六章 | 实战调试 | 问题排查技巧 | 工程实践 | ★★★★☆ |
第一章:SPI 基础・串行通信协议
1.1 SPI 的起源与应用
SPI(Serial Peripheral Interface,串行外设接口)是由摩托罗拉公司在 20 世纪 80 年代提出的一种同步串行通信协议,以高速、全双工、操作简单为特点,广泛应用于微控制器(MCU)与外设的通信场景,如传感器、显示屏、存储芯片(Flash)、AD/DA 转换器等。
在 STM32 系列 MCU 中,SPI 通信有两种实现方式:硬件 SPI(依托芯片内置的 SPI 外设模块)和模拟 SPI(通过 GPIO 引脚手动模拟通信时序)。两种方式各有优劣,适用于不同的工程需求。
1.2 SPI 的核心组成要素
1.2.1 信号线定义
SPI 通信需 4 根信号线完成数据传输,分别是:
- SCLK(Serial Clock):时钟线,由主机产生,用于同步数据传输节奏,控制数据收发的时序。
- MOSI(Master Out Slave In):主机输出 / 从机输入线,主机通过此线向从机发送数据。
- MISO(Master In Slave Out):主机输入 / 从机输出线,从机通过此线向主机返回数据。
- NSS(Chip Select):片选线(通常低电平有效),主机通过此线选择需要通信的从机(同一总线上可连接多个从机,通过 NSS 单独选中)。
1.2.2 时序参数(CPOL 与 CPHA)
SPI 的通信时序由两个核心参数定义,分别是时钟极性(CPOL)和时钟相位(CPHA),两者组合形成 4 种标准时序模式:
- CPOL(时钟极性):定义 SCLK 在空闲状态(未通信时)的电平。CPOL=0 时,SCLK 空闲为低电平;CPOL=1 时,SCLK 空闲为高电平。
- CPHA(时钟相位):定义数据的采样时刻。CPHA=0 时,在 SCLK 的第一个跳变沿(从空闲电平到工作电平的跳变)采样数据;CPHA=1 时,在 SCLK 的第二个跳变沿(从工作电平回到空闲电平的跳变)采样数据。
4 种时序模式的具体参数如下表:
| 模式 | CPOL | CPHA | 空闲时 SCLK 电平 | 数据采样沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 上升沿 |
| 1 | 0 | 1 | 低电平 | 下降沿 |
| 2 | 1 | 0 | 高电平 | 下降沿 |
| 3 | 1 | 1 | 高电平 | 上升沿 |
1.2.3 通信架构(主从模式)
SPI 采用 “一主多从” 的通信架构:
- 主机(Master):负责产生 SCLK 时钟信号,控制 NSS 线选择从机,并主动发起数据传输。
- 从机(Slave):被动响应主机的时钟信号,仅在被 NSS 选中时参与通信,按主机的节奏收发数据。
通信流程可概括为:主机拉低目标从机的 NSS 线(选中从机)→ 主机通过 SCLK 输出时钟信号,同时在 MOSI 线发送数据,从机在 MISO 线返回数据→ 传输完成后,主机拉高 NSS 线(结束通信)。
第二章:硬件 SPI・内置外设实现
硬件 SPI 是通过 STM32 内置的 SPI 外设模块实现通信,依托硬件电路自动生成时序,具有速度快、CPU 占用低的特点,适用于高速数据传输场景。
2.1 硬件 SPI 的内部结构
STM32 的 SPI 外设模块由多个功能单元组成,核心结构包括:
- 时钟发生器:由 APB 总线时钟分频得到 SCLK 信号,支持的分频比为 2/4/8/16/32/64/128/256,可根据需求调整通信速率。
- 数据寄存器(DR):8 位或 16 位的双向缓冲寄存器,发送数据时需写入该寄存器,接收数据时从该寄存器读取。
- 控制寄存器(CR1/CR2):用于配置 SPI 的核心参数,如主从模式、数据长度、CPOL/CPHA、分频比、数据传输方向(全双工 / 半双工)等。
- 状态寄存器(SR):指示 SPI 的工作状态,如发送缓冲区空(TXE)、接收缓冲区满(RXNE)、忙状态(BSY)等,用于判断数据传输进度。
- 中断 / DMA 控制器:支持通过中断或 DMA 方式处理数据收发,减少 CPU 的干预,提高效率。
2.2 硬件 SPI 的初始化配置
以 STM32F103C8T6(主频 72MHz)为例,硬件 SPI 的初始化需完成时钟使能、GPIO 配置、SPI 参数配置三个步骤,具体代码如下:
c
运行
// 步骤1:使能时钟(SPI外设和GPIO端口时钟)
void SPI_Hard_Init(void) {// 使能SPI1时钟(SPI1挂载在APB2总线,时钟72MHz)RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);// 使能GPIOA时钟(SPI1默认引脚为PA5~7,NSS使用PA4)RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);// 步骤2:配置GPIO引脚GPIO_InitTypeDef GPIO_InitStructure;// 配置SCLK(PA5)和MOSI(PA7)为复用推挽输出(由SPI外设控制)GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 高速输出GPIO_Init(GPIOA, &GPIO_InitStructure);// 配置MISO(PA6)为浮空输入(接收从机数据)GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入GPIO_Init(GPIOA, &GPIO_InitStructure);// 配置NSS(PA4)为推挽输出(软件控制片选)GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 通用推挽输出GPIO_Init(GPIOA, &GPIO_InitStructure);GPIO_SetBits(GPIOA, GPIO_Pin_4); // 初始拉高NSS(未选中从机)// 步骤3:配置SPI1参数SPI_InitTypeDef SPI_InitStructure;SPI_InitStructure.SPI_Direction = SPI_Direction_2Line_FullDuplex; // 全双工模式SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机模式SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据长度SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟极性:空闲低电平(模式0)SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 时钟相位:第一个跳变沿采样(模式0)SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件控制NSS(不使用硬件NSS)SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; // 分频2,SCLK=36MHzSPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先传输SPI_InitStructure.SPI_CRCPolynomial = 7; // CRC多项式(不使用CRC时可任意设置)SPI_Init(SPI1, &SPI_InitStructure);// 使能SPI1SPI_Cmd(SPI1, ENABLE);
}
2.3 硬件 SPI 的数据收发实现
硬件 SPI 支持三种数据传输方式:阻塞式、中断式和 DMA 式,分别适用于不同的实时性和效率需求。
2.3.1 阻塞式收发
阻塞式传输通过轮询状态寄存器的标志位,等待数据发送 / 接收完成,实现简单但会占用 CPU 资源,适用于数据量小、对实时性要求不高的场景。
c
运行
// 发送1字节数据(阻塞式)
void SPI_Hard_SendByte(SPI_TypeDef* SPIx, uint8_t data) {// 等待发送缓冲区为空(TXE标志置位)while (SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) == RESET);// 写入数据到SPI数据寄存器,启动发送SPI_I2S_SendData(SPIx, data);
}// 接收1字节数据(阻塞式)
uint8_t SPI_Hard_ReceiveByte(SPI_TypeDef* SPIx) {// 等待接收缓冲区为满(RXNE标志置位)while (SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET);// 从数据寄存器读取接收的数据return SPI_I2S_ReceiveData(SPIx);
}// 带片选的完整通信(发送同时接收)
void SPI_Hard_Transmit(SPI_TypeDef* SPIx, uint8_t* tx_buf, uint8_t* rx_buf, uint16_t len) {GPIO_ResetBits(GPIOA, GPIO_Pin_4); // 拉低NSS,选中从机for (uint16_t i = 0; i < len; i++) {SPI_Hard_SendByte(SPIx, tx_buf[i]);rx_buf[i] = SPI_Hard_ReceiveByte(SPIx);}GPIO_SetBits(GPIOA, GPIO_Pin_4); // 拉高NSS,结束通信
}
2.3.2 中断式收发
中断式传输通过配置 SPI 中断,在数据发送 / 接收完成时触发中断服务程序(ISR),CPU 可在中断中处理数据,减少等待时间,适用于中等数据量、需要兼顾其他任务的场景。
c
运行
// 全局缓冲区与状态变量
uint8_t tx_buffer[100], rx_buffer[100];
uint16_t tx_len = 0, rx_len = 0;
uint16_t tx_index = 0, rx_index = 0;// 初始化SPI中断
void SPI_Hard_Init_IT(void) {// (省略GPIO和SPI基本配置,同2.2)// 配置NVIC中断控制器NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel = SPI1_IRQn; // SPI1中断通道NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能中断NVIC_Init(&NVIC_InitStructure);// 使能SPI发送和接收中断SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_TXE, ENABLE); // 发送缓冲区空中断SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_RXNE, ENABLE); // 接收缓冲区满中断
}// SPI1中断服务函数
void SPI1_IRQHandler(void) {// 处理发送中断(发送缓冲区空)if (SPI_I2S_GetITStatus(SPI1, SPI_I2S_IT_TXE) == SET) {if (tx_index < tx_len) {// 发送下一个字节SPI_I2S_SendData(SPI1, tx_buffer[tx_index++]);} else {// 发送完成,关闭发送中断SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_TXE, DISABLE);}}// 处理接收中断(接收缓冲区满)if (SPI_I2S_GetITStatus(SPI1, SPI_I2S_IT_RXNE) == SET) {if (rx_index < rx_len) {// 接收下一个字节rx_buffer[rx_index++] = SPI_I2S_ReceiveData(SPI1);} else {// 接收完成,关闭接收中断SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_RXNE, DISABLE);}}
}// 启动中断式传输
void SPI_Hard_Start_IT(uint8_t* tx, uint8_t* rx, uint16_t len) {// 复制数据到全局缓冲区memcpy(tx_buffer, tx, len);memcpy(rx_buffer, rx, len); // 清空接收缓冲区tx_len = len;rx_len = len;tx_index = 0;rx_index = 0;GPIO_ResetBits(GPIOA, GPIO_Pin_4); // 拉低NSS// 开启中断,触发发送SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_TXE, ENABLE);SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_RXNE, ENABLE);
}
2.3.3 DMA 式收发
DMA(直接存储器访问)方式通过 DMA 控制器直接在内存与 SPI 外设间传输数据,无需 CPU 干预,适用于大数据量、高速传输场景(如读写 SPI Flash、驱动显示屏)。
c
运行
// 初始化SPI DMA
void SPI_Hard_Init_DMA(void) {// (省略GPIO和SPI基本配置,同2.2)// 使能DMA1时钟(STM32F103的SPI1对应DMA1)RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);// 配置发送DMA(SPI1_TX对应DMA1_Channel3)DMA_InitTypeDef DMA_InitStructure;DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; // 外设地址:SPI数据寄存器DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)0; // 内存地址暂设为0DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 方向:内存→外设DMA_InitStructure.DMA_BufferSize = 0; // 传输长度暂设为0DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 字节传输DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 正常模式(非循环)DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 高优先级DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 非内存到内存传输DMA_Init(DMA1_Channel3, &DMA_InitStructure);// 配置接收DMA(SPI1_RX对应DMA1_Channel2)DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 方向:外设→内存DMA_Init(DMA1_Channel2, &DMA_InitStructure);// 使能SPI的DMA请求SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE); // 发送DMA使能SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE); // 接收DMA使能
}// 启动DMA传输
void SPI_Hard_Start_DMA(uint8_t* tx_buf, uint8_t* rx_buf, uint16_t len) {// 配置发送DMA参数DMA1_Channel3->CMAR = (uint32_t)tx_buf; // 内存地址DMA1_Channel3->CNDTR = len; // 传输长度DMA_Cmd(DMA1_Channel3, ENABLE); // 使能发送DMA// 配置接收DMA参数DMA1_Channel2->CMAR = (uint32_t)rx_buf; // 内存地址DMA1_Channel2->CNDTR = len; // 传输长度DMA_Cmd(DMA1_Channel2, ENABLE); // 使能接收DMAGPIO_ResetBits(GPIOA, GPIO_Pin_4); // 拉低NSS,选中从机// 等待DMA传输完成(也可通过DMA中断判断)while (DMA_GetFlagStatus(DMA1_FLAG_TC3) == RESET); // 等待发送完成while (DMA_GetFlagStatus(DMA1_FLAG_TC2) == RESET); // 等待接收完成GPIO_SetBits(GPIOA, GPIO_Pin_4); // 拉高NSS,结束通信// 关闭DMA通道,清除标志DMA_Cmd(DMA1_Channel3, DISABLE);DMA_Cmd(DMA1_Channel2, DISABLE);DMA_ClearFlag(DMA1_FLAG_TC3 | DMA1_FLAG_TC2);
}
2.4 硬件 SPI 的注意事项
-
时钟频率匹配:SCLK 的频率需低于从机支持的最大频率(如多数传感器的上限为 10MHz)。例如,STM32F103 的 SPI1 挂载在 72MHz 的 APB2 总线上,若从机最大支持 10MHz,则分频比需≥8(72/8=9MHz)。
-
引脚复用冲突:SPI 外设的引脚可能与其他功能(如 USART、TIM)复用,需查阅芯片数据手册的 “引脚定义表” 确认。例如,STM32F103 的 SPI1 默认引脚为 PA5/6/7,也可通过重映射功能使用 PB3/4/5。
-
NSS 线控制:硬件 NSS(SPI_NSS_Hard)在主机模式下需接高电平(否则可能误触发从机模式),实际应用中更推荐使用软件控制 NSS(SPI_NSS_Soft),灵活性更高。
-
数据对齐与长度:若使用 16 位数据模式(SPI_DataSize_16b),需确保从机支持 16 位传输,且收发缓冲区的地址需按 16 位对齐(避免内存访问错误)。
-
中断优先级管理:多 SPI 外设同时使用中断时,需合理设置中断优先级,避免高优先级中断打断低优先级传输导致的数据错乱。
-
DMA 缓冲区要求:DMA 传输的内存缓冲区需为连续地址空间,且建议用
volatile修饰(防止编译器优化导致的数据异常)。
第三章:模拟 SPI・GPIO 软件模拟
模拟 SPI 通过 GPIO 引脚手动控制电平变化,模拟 SPI 的时序逻辑,无需依赖硬件外设,具有引脚灵活、适配性强的特点,适用于低速通信或无 SPI 外设的场景。
3.1 模拟 SPI 的实现原理
模拟 SPI 的核心是通过软件控制 GPIO 引脚的电平变化,复刻 SPI 的通信时序:
- 通过输出引脚(SCLK、MOSI、NSS)模拟时钟信号和发送数据;
- 通过输入引脚(MISO)读取从机返回的数据;
- 用延时函数控制 SCLK 的周期,确保时序符合从机要求。
其优势在于:
- 引脚选择灵活:可使用任意 GPIO 引脚,不受硬件 SPI 引脚限制;
- 时序自定义:可精确控制每个时钟周期的长度,适配非标准 SPI 时序的外设;
- 调试直观:每一步电平变化可通过示波器直接观测,便于问题定位。
3.2 模拟 SPI 的 GPIO 初始化
以 STM32F103C8T6 为例,使用 PA5(SCLK)、PA7(MOSI)、PA6(MISO)、PA4(NSS)模拟 SPI,初始化代码如下:
c
运行
// 宏定义:简化GPIO操作(提高代码可读性)
#define SPI_SCLK_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_5) // SCLK置高
#define SPI_SCLK_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_5) // SCLK置低
#define SPI_MOSI_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_7) // MOSI置高
#define SPI_MOSI_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_7) // MOSI置低
#define SPI_MISO_READ() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) // 读取MISO电平
#define SPI_NSS_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4) // NSS置高
#define SPI_NSS_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4) // NSS置低// 初始化模拟SPI的GPIO引脚
void SPI_Soft_Init(void) {// 使能GPIOA时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);GPIO_InitTypeDef GPIO_InitStructure;// 配置SCLK、MOSI、NSS为推挽输出GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7 | GPIO_Pin_4;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 通用推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 高速输出(减少电平切换延迟)GPIO_Init(GPIOA, &GPIO_InitStructure);// 配置MISO为浮空输入GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入(不接上下拉)GPIO_Init(GPIOA, &GPIO_InitStructure);// 初始化电平(模式0:SCLK空闲低电平,NSS默认拉高)SPI_SCLK_LOW();SPI_NSS_HIGH();
}
3.3 模拟 SPI 的时序实现
模拟 SPI 的核心是通过软件函数复现 SPI 的时序逻辑,需根据目标时序模式(CPOL/CPHA)编写对应的收发函数,同时通过延时控制 SCLK 频率。
3.3.1 延时函数设计
延时函数用于控制 SCLK 的高低电平持续时间,直接影响通信速率。需根据 MCU 主频设计精准的延时,确保时序符合从机要求。
c
运行
// 延时n个CPU周期(STM32F103主频72MHz,1周期≈13.89ns)
static void SPI_Soft_DelayCycles(uint32_t cycles) {while (cycles--); // 空循环延时
}// 定义SCLK半周期的延时(根据目标频率计算)
// 例如:目标SCLK频率1MHz,半周期500ns,72MHz下约需36个周期(500ns / 13.89ns ≈ 36)
#define SCLK_HALF_CYCLE() SPI_Soft_DelayCycles(36)
3.3.2 模式 0(CPOL=0,CPHA=0)的收发函数
模式 0 的时序特点:SCLK 空闲为低电平,数据在 SCLK 的上升沿被采样,在下降沿切换数据。
c
运行
// 发送1字节数据并同时接收1字节(模式0)
uint8_t SPI_Soft_TransmitByte_Mode0(uint8_t tx_data) {uint8_t rx_data = 0; // 接收缓冲区for (uint8_t i = 0; i < 8; i++) { // 逐位处理(8位数据)// 1. 输出当前位数据(高位先传)if (tx_data & 0x80) { // 最高位为1SPI_MOSI_HIGH();} else { // 最高位为0SPI_MOSI_LOW();}tx_data <<= 1; // 左移,准备下一位// 2. 延时,确保数据稳定(建立时间)SCLK_HALF_CYCLE();// 3. 拉高SCLK,上升沿触发从机采样SPI_SCLK_HIGH();// 读取MISO数据(从机在此时输出数据)rx_data <<= 1; // 左移,腾出低位if (SPI_MISO_READ()) {rx_data |= 0x01; // 若MISO为高,置低位为1}// 4. 延时,保持SCLK高电平(保持时间)SCLK_HALF_CYCLE();// 5. 拉低SCLK,准备下一位数据SPI_SCLK_LOW();}return rx_data; // 返回接收的数据
}// 带片选的完整通信(模式0)
void SPI_Soft_Transmit_Mode0(uint8_t* tx_buf, uint8_t* rx_buf, uint16_t len) {SPI_NSS_LOW(); // 拉低NSS,选中从机for (uint16_t i = 0; i < len; i++) {rx_buf[i] = SPI_Soft_TransmitByte_Mode0(tx_buf[i]);}SPI_NSS_HIGH(); // 拉高NSS,结束通信
}
3.3.3 其他模式的适配
若需支持模式 1/2/3,只需调整 SCLK 的初始电平(CPOL)和数据采样的时机(CPHA),以下是模式 1(CPOL=0,CPHA=1)的实现示例:
c
运行
// 模式1:SCLK空闲低电平,下降沿采样数据
uint8_t SPI_Soft_TransmitByte_Mode1(uint8_t tx_data) {uint8_t rx_data = 0;for (uint8_t i = 0; i < 8; i++) {// 1. 拉高SCLK(初始低电平,先置高)SPI_SCLK_HIGH();SCLK_HALF_CYCLE(); // 保持高电平// 2. 输出当前位数据if (tx_data & 0x80) {SPI_MOSI_HIGH();} else {SPI_MOSI_LOW();}tx_data <<= 1;// 3. 拉低SCLK,下降沿采样数据SPI_SCLK_LOW();rx_data <<= 1;if (SPI_MISO_READ()) {rx_data |= 0x01;}SCLK_HALF_CYCLE(); // 保持低电平}return rx_data;
}
3.4 模拟 SPI 的优化与扩展
3.4.1 速度优化
模拟 SPI 的速率受限于 CPU 速度和延时函数的精度,可通过以下方式优化:
- 汇编延时:用汇编指令编写延时函数,减少 C 语言循环的额外开销(如指令跳转耗时):
c
运行
static void SPI_Soft_AsmDelay(void) {__asm volatile ("NOP\n" // 1个CPU周期"NOP\n""NOP\n""NOP\n" // 共4个周期,适用于高频场景); } - 批量传输优化:将多字节传输逻辑合并到一个函数中,减少函数调用的开销:
c
运行
void SPI_Soft_TransmitMultiBytes(uint8_t* tx_buf, uint8_t* rx_buf, uint16_t len) {SPI_NSS_LOW();for (uint16_t i = 0; i < len; i++) {// 直接嵌入单字节传输逻辑,避免函数调用uint8_t tx = tx_buf[i], rx = 0;for (uint8_t j = 0; j < 8; j++) {// ... 单字节传输代码 ...}rx_buf[i] = rx;}SPI_NSS_HIGH(); }
3.4.2 多从机支持
通过定义不同的 NSS 引脚宏,可轻松扩展多从机通信:
c
运行
// 从机1:NSS=PA4
#define SPI_NSS1_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4)
#define SPI_NSS1_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4)// 从机2:NSS=PB0
#define SPI_NSS2_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_0)
#define SPI_NSS2_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_0)// 初始化从机2的NSS引脚
void SPI_Soft_Init_Slave2(void) {RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);SPI_NSS2_HIGH(); // 初始拉高
}// 向从机2发送数据
void SPI_Soft_SendToSlave2(uint8_t* buf, uint16_t len) {SPI_NSS2_LOW(); // 选中从机2// ... 调用传输函数 ...SPI_NSS2_HIGH();
}
3.4.3 非标准时序适配
部分外设可能要求特殊时序(如超长前导时钟、非对称占空比),模拟 SPI 可灵活调整:
c
运行
// 发送带前导时钟的帧(适用于特殊外设)
void SPI_Soft_SendSpecialFrame(uint8_t* buf, uint16_t len) {SPI_NSS_LOW();// 输出10个额外的SCLK脉冲作为前导for (uint8_t i = 0; i < 10; i++) {SPI_SCLK_HIGH();SCLK_HALF_CYCLE();SPI_SCLK_LOW();SCLK_HALF_CYCLE();}// 正常传输数据SPI_Soft_Transmit_Mode0(buf, NULL, len);SPI_NSS_HIGH();
}
3.5 模拟 SPI 的注意事项
-
延时精度:延时函数的精度直接影响时序正确性,需用示波器实测校准。例如,理论计算的 36 个周期可能因编译器优化实际仅执行 30 个周期,导致 SCLK 频率偏高。
-
GPIO 输出速度:SCLK 和 MOSI 引脚需配置为高速输出(50MHz),否则电平切换延迟过大(如低速输出可能导致上升沿 / 下降沿过缓,超出从机的建立时间要求)。
-
中断干扰:传输过程中若有高优先级中断触发,会延长延时时间,导致时序紊乱。需在传输时关闭全局中断(原子操作):
c
运行
void SPI_Soft_Transmit_Atomic(uint8_t* tx, uint8_t* rx, uint16_t len) {__disable_irq(); // 关闭全局中断SPI_Soft_Transmit_Mode0(tx, rx, len);__enable_irq(); // 恢复中断 } -
CPU 资源占用:模拟 SPI 完全由软件实现,传输过程中 CPU 无法执行其他任务,不适用于实时性要求高的场景(如电机控制、高频采样)。
-
电平冲突避免:MISO 引脚必须配置为输入模式,若误配置为输出,可能与从机的输出产生电平冲突(短路),损坏芯片。
第四章:技术对比・性能与特性差异
硬件 SPI 与模拟 SPI 在性能、灵活性、适用场景等方面存在显著差异,工程中需根据需求选择合适的方案。
4.1 核心特性对比
| 对比维度 | 硬件 SPI(内置外设) | 模拟 SPI(GPIO 软件模拟) |
|---|---|---|
| 通信速度 | 高(最高可达 APB 总线时钟 / 2,如 STM32F407 的 SPI1 可达 42MHz) | 低(受 CPU 主频和延时限制,通常≤1MHz) |
| CPU 占用率 | 低(支持 DMA / 中断,传输时 CPU 可处理其他任务) | 高(全软件控制,传输期间 CPU 被完全占用) |
| 引脚灵活性 | 低(固定为 SPI 外设的复用引脚) | 高(可使用任意 GPIO 引脚) |
| 时序适配性 | 有限(仅支持标准分频比和 4 种模式) | 高(可自定义任意时序,适配非标准外设) |
| 代码复杂度 | 高(需配置外设、中断、DMA 等) | 低(仅需 GPIO 操作和时序函数) |
| 功耗 | 低(外设硬件运行,CPU 可进入休眠模式) | 高(CPU 持续运行,无法休眠) |
| 多从机支持 | 需额外控制 NSS 引脚,与模拟方式类似 | 简单(通过宏定义快速切换 NSS 引脚) |
| 调试难度 | 高(时序由硬件生成,问题定位需结合寄存器状态) | 低(每一步电平变化可通过示波器直接观测) |
4.2 适用场景分析
-
硬件 SPI 的适用场景:
- 高速数据传输:如驱动 SPI 显示屏(需快速刷新图像)、读写 SPI Flash(大容量数据存储)、高速 AD 转换器(高频采样)等,硬件 SPI 的高速度可显著提升效率。
- 低功耗设备:如物联网传感器节点、电池供电设备,硬件 SPI 配合 DMA 可减少 CPU 运行时间,降低功耗。
- 多任务系统:在 RTOS(实时操作系统)环境中,硬件 SPI 的中断 / DMA 方式可避免阻塞任务调度,保证系统实时性。
-
模拟 SPI 的适用场景:
- 引脚资源紧张:当硬件 SPI 的引脚被其他功能(如 USART、TIM)占用时,可通过模拟 SPI 使用空闲 GPIO 引脚。
- 非标准时序外设:部分旧款传感器或定制芯片的时序不符合 SPI 标准(如非 2 的幂次分频、特殊前导码),硬件 SPI 无法适配,需用模拟 SPI。
- 快速原型验证:调试初期用模拟 SPI 验证通信逻辑(如指令正确性),无需关注硬件外设配置,缩短开发周期。
- 无 SPI 外设的 MCU:部分低端 MCU(如 8 位单片机)无硬件 SPI 模块,只能通过模拟方式实现。
4.3 混合使用策略
在复杂系统中,可结合两种方式的优势:
- 用硬件 SPI 处理高速、大数据量传输(如显示屏驱动);
- 用模拟 SPI 处理低速、小批量数据(如传感器读取);
- 调试阶段用模拟 SPI 验证时序,稳定后迁移到硬件 SPI 优化性能。
第五章:跨 MCU 移植・主频适配方法
STM32 系列 MCU 的主频差异较大(从 8MHz 到 480MHz),且 SPI 外设的总线挂载、引脚复用等存在差异,移植 SPI 代码时需重点关注时序适配,尤其是模拟 SPI 受主频影响显著。
5.1 主流 STM32 系列的时钟特性
| MCU 系列 | 典型型号 | 最高主频 | APB1 总线时钟上限 | APB2 总线时钟上限 | SPI 外设数量 |
|---|---|---|---|---|---|
| F1 | STM32F103C8T6 | 72MHz | 36MHz | 72MHz | 3 个(SPI1-3) |
| F4 | STM32F407IGH6 | 168MHz | 42MHz | 84MHz | 6 个(SPI1-6) |
| F7 | STM32F767IGT6 | 216MHz | 54MHz | 108MHz | 6 个(SPI1-6) |
| H7 | STM32H743VIT6 | 480MHz | 120MHz | 240MHz | 6 个(SPI1-6) |
| L0 | STM32L053R8T6 | 32MHz | 32MHz | 32MHz | 2 个(SPI1-2) |
注:APB 总线时钟与 SPI 外设时钟的关系:
- F1 系列:SPI 时钟 = APB 总线时钟 / 分频比;
- F4 及以上系列:若 APB 分频系数为 1,SPI 时钟 = APB 总线时钟 / 分频比;否则 SPI 时钟 = 2×APB 总线时钟 / 分频比。
5.2 硬件 SPI 的移植要点
-
时钟配置调整:
- 根据目标 MCU 的 APB 总线时钟重新计算分频比,确保 SCLK 频率符合从机要求。例如:
- STM32F103(SPI1 挂载 APB2,72MHz):若需 18MHz SCLK,分频比 = 4(72/4=18);
- STM32F407(SPI1 挂载 APB2,84MHz):若需 21MHz SCLK,分频比 = 4(84/4=21)。
- 使能对应的 SPI 外设时钟(如 F1 的 SPI1 用
RCC_APB2PeriphClockCmd,F4 的 SPI1 也用RCC_APB2PeriphClockCmd,但总线时钟不同)。
- 根据目标 MCU 的 APB 总线时钟重新计算分频比,确保 SCLK 频率符合从机要求。例如:
-
引脚复用映射:不同系列的 SPI 引脚复用不同,需查阅数据手册的 “Alternate Function Table” 重新配置。例如:
- F103 的 SPI1 默认引脚:PA5(SCLK)、PA6(MISO)、PA7(MOSI);
- F407 的 SPI1 可选引脚:PA5/6/7 或 PB3/4/5(需配置重映射)。
-
库函数与寄存器差异:
- 标准外设库(StdPeriph):函数接口基本一致,但部分寄存器位定义不同(如 F4 的 SPI_CR2 增加了
FRXTH位,用于控制接收缓冲区阈值)。 - HAL 库:接口统一,但初始化结构体成员有扩展(如 F7/H7 支持
TIMode(TI 同步模式)、CRCCalculation(CRC 计算使能)等)。
- 标准外设库(StdPeriph):函数接口基本一致,但部分寄存器位定义不同(如 F4 的 SPI_CR2 增加了
-
DMA 通道映射:SPI 与 DMA 的通道映射随系列变化,需重新配置。例如:
- F103 的 SPI1_TX 对应 DMA1_Channel3;
- F407 的 SPI1_TX 对应 DMA2_Stream3_Channel3;
- H743 的 SPI1_TX 对应 DMA1_Stream0_Channel1。
5.3 模拟 SPI 的移植核心:延时适配
模拟 SPI 的延时函数直接依赖 MCU 主频,若主频变化而延时未调整,会导致 SCLK 频率偏差,通信失败。
5.3.1 通用延时计算方法
根据目标 SCLK 频率和 MCU 主频,计算半周期所需的延时周期数:
c
运行
// 目标SCLK频率:1MHz(半周期500ns)
#define TARGET_HALF_CYCLE_NS 500// 根据目标MCU定义主频(单位:MHz)
#ifdef STM32F103
#define MCU_FREQ_MHZ 72
#elif defined(STM32F407)
#define MCU_FREQ_MHZ 168
#elif defined(STM32H743)
#define MCU_FREQ_MHZ 480
#endif// 计算半周期所需的CPU周期数(1周期 = 1000 / MCU_FREQ_MHZ 纳秒)
#define CYCLES_PER_HALF_CYCLE (TARGET_HALF_CYCLE_NS * MCU_FREQ_MHZ / 1000)// 延时函数(根据计算的周期数延时)
static void SPI_Soft_Delay(void) {volatile uint32_t cycles = CYCLES_PER_HALF_CYCLE;while (cycles--);
}
5.3.2 实测校准
理论计算可能受编译器优化(如循环展开)影响,需用示波器实测校准:
- 用逻辑分析仪或示波器抓取 SCLK 波形,测量实际半周期时间;
- 若实际时间与目标偏差超过 10%,微调
CYCLES_PER_HALF_CYCLE(如增加 / 减少 1~2 个周期)。
例如:STM32H743(480MHz)目标半周期 500ns,理论值 = 500×480/1000=240,但实测可能需 245(因循环指令耗时)。
5.4 跨系列移植实例(F103→H743)
| 移植步骤 | 硬件 SPI 移植流程 | 模拟 SPI 移植流程 |
|---|---|---|
| 1. 时钟配置 | 1. 使能 SPI1 时钟(H7 的 SPI1 挂载 APB2,240MHz)2. 配置分频比(如 240/6=40MHz,适配从机) | 1. 仅需使能 GPIO 时钟(无 SPI 外设时钟)2. 按 H7 主频(480MHz)重新计算延时周期 |
| 2. 引脚配置 | 1. 查阅 H7 数据手册,确认 SPI1 引脚(如 PA5/6/7)2. 配置为复用功能(AF5 或对应复用编号) | 1. 保持引脚定义(如仍用 PA5/6/7)2. 配置 GPIO 为输出 / 输入(无需复用) |
| 3. 外设初始化 | 1. 调整 SPI_InitTypeDef 参数(如分频比)2. 若用 DMA,重新配置 DMA 通道(如 DMA1_Stream0) | 1. 无需修改传输逻辑2. 仅更新延时函数的宏定义 |
| 4. 收发函数 | 保持不变(库函数接口兼容) | 保持不变(传输逻辑与主频无关) |
第六章:实战调试・问题排查技巧
SPI 通信故障在工程中较为常见,需结合硬件检测、波形分析和软件调试定位问题,以下是常见故障的排查方法。
6.1 常见故障与解决方案
| 故障现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 完全无通信(无 SCLK/MOSI 信号) | 1. SPI 外设未使能或初始化错误2. 时钟未使能3. 引脚配置错误(如方向反) | 1. 检查SPI_Cmd是否调用,SPI_Init参数是否正确2. 用万用表测 SPI 外设时钟引脚(如 PA5)是否有电平变化3. 核对 GPIO 模式(SCLK/MOSI 应为输出,MISO 为输入) |
| 数据乱码或部分错误 | 1. 时序模式错误(CPOL/CPHA 不匹配)2. SCLK 频率过高3. 数据位序错误(MSB/LSB) | 1. 用示波器对比从机手册的时序图,调整 CPOL/CPHA2. 降低 SCLK 频率(如增大分频比)3. 切换SPI_FirstBit配置(MSB/LSB) |
| 接收数据全为 0 或 0xFF | 1. MISO 引脚未连接或接触不良2. 从机未被正确选中(NSS 未拉低)3. 从机未上电或故障 | 1. 用示波器测 MISO 引脚是否有信号输出2. 检查 NSS 控制逻辑,确保通信时 NSS 为低3. 测量从机供电电压,确认从机正常工作 |
| 通信偶尔失败(不稳定) | 1. 中断干扰(模拟 SPI)2. 电源噪声过大3. 接线过长导致信号衰减 | 1. 传输时关闭全局中断(__disable_irq)2. 增加电源滤波电容(100nF+10uF)3. 缩短接线长度,或在信号线上增加上拉电阻(4.7kΩ) |
| 硬件 SPI 的 DMA 传输失败 | 1. DMA 通道配置错误2. 缓冲区地址未对齐3. 未使能 SPI 的 DMA 请求 | 1. 核对 SPI 与 DMA 的通道映射关系2. 确保缓冲区地址为 4 字节对齐(如用__attribute__((aligned(4))))3. 检查SPI_I2S_DMACmd是否使能对应 DMA 请求 |
6.2 示波器调试技巧
示波器是排查 SPI 时序问题的核心工具,使用时需注意:
-
四通道同步观测:同时连接 SCLK、MOSI、MISO、NSS 四根信号线,观察完整通信过程:
- 确认 NSS 在通信时是否正确拉低 / 拉高;
- 检查 SCLK 的频率、占空比是否符合要求;
- 对比 MOSI 数据变化与 SCLK 跳变沿的时间关系,验证建立时间(Setup Time)和保持时间(Hold Time)是否满足从机规格(通常要求≥10ns)。
-
模式 0 的波形参考:
- NSS 拉低后,SCLK 初始为低电平;
- MOSI 在 SCLK 低电平时稳定输出数据,上升沿被从机采样;
- MISO 在 SCLK 高电平时输出数据,主机在上升沿后读取。
-
常见波形异常及原因:
- SCLK 波形抖动:电源噪声或接线过长导致;
- MOSI 数据在采样沿跳变:建立时间不足,需增加延时;
- NSS 电平不稳定:GPIO 驱动能力不足,可增加上拉电阻。
6.3 软件调试辅助工具
-
GPIO 调试引脚:在关键步骤翻转空闲 GPIO 引脚,用示波器标记事件(如传输开始 / 结束):
c
运行
// 定义调试引脚(PB1) #define DEBUG_PIN_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_1) #define DEBUG_PIN_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_1)void SPI_Transmit(...) {DEBUG_PIN_HIGH(); // 标记传输开始// ... 传输过程 ...DEBUG_PIN_LOW(); // 标记传输结束 } -
数据日志打印:通过 USART 输出收发数据(适用于低速场景),验证数据正确性:
c
运行
#include <stdio.h>// 打印缓冲区数据(十六进制) void SPI_PrintBuffer(uint8_t* buf, uint16_t len) {printf("Data: ");for (uint16_t i = 0; i < len; i++) {printf("%02X ", buf[i]);}printf("\r\n"); } -
寄存器状态检查:硬件 SPI 故障时,可读取状态寄存器(SR)判断问题:
c
运行
// 检查SPI状态 void SPI_CheckStatus(SPI_TypeDef* SPIx) {uint16_t sr = SPIx->SR;if (sr & SPI_SR_TXE) printf("TXE: 发送缓冲区空\r\n");if (sr & SPI_SR_RXNE) printf("RXNE: 接收缓冲区满\r\n");if (sr & SPI_SR_BSY) printf("BSY: SPI忙\r\n");if (sr & SPI_SR_OVR) printf("OVR: 溢出错误\r\n"); // 常见于接收未及时读取 }
6.4 实战案例:驱动 W25Q128 SPI Flash
以 W25Q128(16MB SPI Flash)为例,对比硬件 SPI 与模拟 SPI 的实现及调试要点:
6.4.1 硬件 SPI 实现
c
运行
// 读取W25Q128的ID(硬件SPI)
uint32_t W25Q_ReadID_Hard(void) {uint8_t tx[4] = {0x90, 0x00, 0x00, 0x00}; // 读ID指令(0x90)+ 地址uint8_t rx[4] = {0};SPI_Hard_Transmit(SPI1, tx, rx, 4);// ID由后3字节组成(制造商ID+设备ID)return (rx[1] << 16) | (rx[2] << 8) | rx[3];
}
6.4.2 模拟 SPI 实现
c
运行
// 读取W25Q128的ID(模拟SPI)
uint32_t W25Q_ReadID_Soft(void) {uint8_t tx[4] = {0x90, 0x00, 0x00, 0x00};uint8_t rx[4] = {0};SPI_Soft_Transmit_Mode0(tx, rx, 4);return (rx[1] << 16) | (rx[2] << 8) | rx[3];
}
6.4.3 调试要点
- 若读取 ID 为 0xFFFFFFFF,可能是 NSS 未拉低或 SPI 模式错误(W25Q128 默认支持模式 0/3);
- 若 ID 部分正确(如前两位正确,后一位错误),可能是 SCLK 频率过高(W25Q128 支持最高 104MHz,建议初期用 10MHz 以下调试);
- 硬件 SPI 需注意 DMA 缓冲区是否对齐,模拟 SPI 需确保延时精度(避免时序偏差)。
结语
硬件 SPI 与模拟 SPI 作为 STM32 中实现 SPI 通信的两种核心方式,各有其适用场景:硬件 SPI 以高速、高效见长,适合大数据量传输;模拟 SPI 以灵活、易适配取胜,适合特殊时序或引脚受限场景。
在实际工程中,需根据通信速率、引脚资源、实时性要求等因素选择方案,必要时可混合使用以兼顾性能与灵活性。同时,跨 MCU 移植时需重点关注时钟特性与时序适配,结合示波器等工具精准调试,才能确保 SPI 通信稳定可靠。
掌握这两种技术,如同掌握了 SPI 通信的 “双绝”,可从容应对各类外设互联需求,为嵌入式系统开发打下坚实基础。
