当前位置: 首页 > news >正文

《STM32 江湖 SPI 双绝:硬件外设与软件模拟的深度解析》

章节技术类型核心机制适用场景难度评级
第一章SPI 基础串行通信协议外设互联场景★★☆☆☆
第二章硬件 SPI内置外设实现高速数据传输★★★☆☆
第三章模拟 SPIGPIO 软件模拟灵活适配场景★★☆☆☆
第四章技术对比性能与特性差异方案选型参考★★★★☆
第五章跨 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 种时序模式的具体参数如下表:

模式CPOLCPHA空闲时 SCLK 电平数据采样沿
000低电平上升沿
101低电平下降沿
210高电平下降沿
311高电平上升沿
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 的注意事项

  1. 时钟频率匹配:SCLK 的频率需低于从机支持的最大频率(如多数传感器的上限为 10MHz)。例如,STM32F103 的 SPI1 挂载在 72MHz 的 APB2 总线上,若从机最大支持 10MHz,则分频比需≥8(72/8=9MHz)。

  2. 引脚复用冲突:SPI 外设的引脚可能与其他功能(如 USART、TIM)复用,需查阅芯片数据手册的 “引脚定义表” 确认。例如,STM32F103 的 SPI1 默认引脚为 PA5/6/7,也可通过重映射功能使用 PB3/4/5。

  3. NSS 线控制:硬件 NSS(SPI_NSS_Hard)在主机模式下需接高电平(否则可能误触发从机模式),实际应用中更推荐使用软件控制 NSS(SPI_NSS_Soft),灵活性更高。

  4. 数据对齐与长度:若使用 16 位数据模式(SPI_DataSize_16b),需确保从机支持 16 位传输,且收发缓冲区的地址需按 16 位对齐(避免内存访问错误)。

  5. 中断优先级管理:多 SPI 外设同时使用中断时,需合理设置中断优先级,避免高优先级中断打断低优先级传输导致的数据错乱。

  6. 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 的注意事项

  1. 延时精度:延时函数的精度直接影响时序正确性,需用示波器实测校准。例如,理论计算的 36 个周期可能因编译器优化实际仅执行 30 个周期,导致 SCLK 频率偏高。

  2. GPIO 输出速度:SCLK 和 MOSI 引脚需配置为高速输出(50MHz),否则电平切换延迟过大(如低速输出可能导致上升沿 / 下降沿过缓,超出从机的建立时间要求)。

  3. 中断干扰:传输过程中若有高优先级中断触发,会延长延时时间,导致时序紊乱。需在传输时关闭全局中断(原子操作):

    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();  // 恢复中断
    }
    
  4. CPU 资源占用:模拟 SPI 完全由软件实现,传输过程中 CPU 无法执行其他任务,不适用于实时性要求高的场景(如电机控制、高频采样)。

  5. 电平冲突避免: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 适用场景分析

  1. 硬件 SPI 的适用场景

    • 高速数据传输:如驱动 SPI 显示屏(需快速刷新图像)、读写 SPI Flash(大容量数据存储)、高速 AD 转换器(高频采样)等,硬件 SPI 的高速度可显著提升效率。
    • 低功耗设备:如物联网传感器节点、电池供电设备,硬件 SPI 配合 DMA 可减少 CPU 运行时间,降低功耗。
    • 多任务系统:在 RTOS(实时操作系统)环境中,硬件 SPI 的中断 / DMA 方式可避免阻塞任务调度,保证系统实时性。
  2. 模拟 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 外设数量
F1STM32F103C8T672MHz36MHz72MHz3 个(SPI1-3)
F4STM32F407IGH6168MHz42MHz84MHz6 个(SPI1-6)
F7STM32F767IGT6216MHz54MHz108MHz6 个(SPI1-6)
H7STM32H743VIT6480MHz120MHz240MHz6 个(SPI1-6)
L0STM32L053R8T632MHz32MHz32MHz2 个(SPI1-2)

注:APB 总线时钟与 SPI 外设时钟的关系:

  • F1 系列:SPI 时钟 = APB 总线时钟 / 分频比;
  • F4 及以上系列:若 APB 分频系数为 1,SPI 时钟 = APB 总线时钟 / 分频比;否则 SPI 时钟 = 2×APB 总线时钟 / 分频比。

5.2 硬件 SPI 的移植要点

  1. 时钟配置调整

    • 根据目标 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,但总线时钟不同)。
  2. 引脚复用映射:不同系列的 SPI 引脚复用不同,需查阅数据手册的 “Alternate Function Table” 重新配置。例如:

    • F103 的 SPI1 默认引脚:PA5(SCLK)、PA6(MISO)、PA7(MOSI);
    • F407 的 SPI1 可选引脚:PA5/6/7 或 PB3/4/5(需配置重映射)。
  3. 库函数与寄存器差异

    • 标准外设库(StdPeriph):函数接口基本一致,但部分寄存器位定义不同(如 F4 的 SPI_CR2 增加了FRXTH位,用于控制接收缓冲区阈值)。
    • HAL 库:接口统一,但初始化结构体成员有扩展(如 F7/H7 支持TIMode(TI 同步模式)、CRCCalculation(CRC 计算使能)等)。
  4. 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 实测校准

理论计算可能受编译器优化(如循环展开)影响,需用示波器实测校准:

  1. 用逻辑分析仪或示波器抓取 SCLK 波形,测量实际半周期时间;
  2. 若实际时间与目标偏差超过 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 或 0xFF1. 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 时序问题的核心工具,使用时需注意:

  1. 四通道同步观测:同时连接 SCLK、MOSI、MISO、NSS 四根信号线,观察完整通信过程:

    • 确认 NSS 在通信时是否正确拉低 / 拉高;
    • 检查 SCLK 的频率、占空比是否符合要求;
    • 对比 MOSI 数据变化与 SCLK 跳变沿的时间关系,验证建立时间(Setup Time)和保持时间(Hold Time)是否满足从机规格(通常要求≥10ns)。
  2. 模式 0 的波形参考

    • NSS 拉低后,SCLK 初始为低电平;
    • MOSI 在 SCLK 低电平时稳定输出数据,上升沿被从机采样;
    • MISO 在 SCLK 高电平时输出数据,主机在上升沿后读取。
  3. 常见波形异常及原因

    • SCLK 波形抖动:电源噪声或接线过长导致;
    • MOSI 数据在采样沿跳变:建立时间不足,需增加延时;
    • NSS 电平不稳定:GPIO 驱动能力不足,可增加上拉电阻。

6.3 软件调试辅助工具

  1. 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();  // 标记传输结束
    }
    
  2. 数据日志打印:通过 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");
    }
    
  3. 寄存器状态检查:硬件 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 通信的 “双绝”,可从容应对各类外设互联需求,为嵌入式系统开发打下坚实基础。

http://www.dtcms.com/a/596409.html

相关文章:

  • Docker学习笔记---day002
  • F280049C学习笔记之X-BAR
  • Python基础教学:Python的openpyxl和python-docx模块结合Excel和Word模板进行数据写入-由Deepseek产生
  • WebSocket原理及实现详解
  • 网站建设与管理吴振峰pptapp的制作需要多少钱
  • 优雅与极简:将你的屏幕变成复古翻页钟——Fliqlo for Mac 完全指南
  • wsl ubuntu24.04 cuda13 cudnn9 pytorch 显卡加速
  • macos安装mysql
  • 解决 iPhone 和 Mac 之间备忘录无法同步的9种方法
  • 【Ubuntu系统开机后出现:GNU GRUB ,Advanced options for Ubuntu】
  • 江西省建设监督网站电子网特色的企业网站建设
  • Mac上DevEco-Studio功能/使用介绍
  • Redis 配置详解
  • Mac 下载 VMware 11.1.0-1.dmg 后如何安装?超简单教程
  • mac怎么卸载office Powerpoint
  • dz论坛做分类网站wordpress git 7.5
  • Java 大文件上传实战:从底层原理到分布式落地(含分片 / 断点续传 / 秒传)
  • 有趣的网站网址之家wordpress网站中英文切换
  • 「腾讯云NoSQL」技术之Redis篇:精准围剿rehash时延毛刺实践方案揭秘
  • 中控播控系统:一键掌控多媒体空间
  • 遗传算法与粒子群算法优化BP提高分类效果
  • c++ -- 循环依赖解决方案
  • 免费vip网站推广做疏通什么网站推广好
  • 金融智能体具体能做什么?应用场景有哪些?
  • 云手机的核心用途都有哪些?
  • 需求洞察助力战略规划实现潜在市场机会
  • java set和list集合知识
  • 在IPython和PyCharm里通过PySpark实现词频统计
  • 03-node.js webpack
  • 维护_其它进程间通信(IPC Inter-Process communication)和分布式通信框架列述