前言
在嵌入式系统中,设备间的数据传输协议多种多样,SPI(Serial Peripheral Interface,串行外设接口)凭借其高速、全双工、易用性等特点,成为连接STM32与高速外设(如OLED屏、Flash芯片、AD转换器、无线模块等)的首选方案。与UART的异步通信不同,SPI采用同步通信方式,通过时钟信号协调数据传输,适合需要高频数据交换的场景。
本文将从SPI协议基础出发,详细解析STM32 SPI外设的工作原理、硬件设计、软件配置及实战案例,涵盖寄存器级编程、HAL库应用、DMA传输优化等核心内容,同时提供调试技巧与常见问题解决方案,旨在帮助嵌入式开发者全面掌握STM32中SPI的应用。
一、SPI协议基础
1.1 什么是SPI?
SPI是一种同步串行通信协议,由摩托罗拉公司提出,最初用于短距离设备间的高速数据传输。其核心特点是"同步通信"------通信双方通过同一时钟信号(由主机产生)同步数据传输,因此无需像UART那样依赖波特率约定,传输速率更高(通常可达几十Mbps,甚至数百Mbps,取决于硬件支持)。
SPI是一种"主从式"协议,通信系统由一个主机(Master) 和一个或多个从机(Slave) 组成,主机负责产生时钟信号并发起通信,从机被动响应。
1.2 SPI信号线组成
SPI通信至少需要4根信号线(全双工模式),部分场景可简化为3根(单工或半双工),各信号线功能如下:
信号线 | 全称 | 方向(主机视角) | 功能描述 |
---|---|---|---|
SCK | Serial Clock | 输出 | 时钟信号,由主机产生,用于同步数据传输(频率决定传输速率)。 |
MOSI | Master Out Slave In | 输出 | 主机发送数据到从机的信号线(主机输出,从机输入)。 |
MISO | Master In Slave Out | 输入 | 从机发送数据到主机的信号线(从机输出,主机输入)。 |
NSS/CS | Slave Select/Chip Select | 输出 | 从机选择信号(低电平有效,部分设备高电平有效),主机通过该信号指定通信的从机。 |
注意:
- 多从机场景下,所有从机的SCK、MOSI、MISO信号线并联,主机通过独立的NSS引脚分别控制各从机(或通过软件模拟NSS信号);
- 部分简单从机可能省略NSS引脚(如某些OLED屏),需通过硬件接线固定选中状态(如将NSS引脚直接接地)。
1.3 SPI通信时序与工作模式
SPI的核心是"时序同步",数据在SCK的边沿(上升沿或下降沿)被采样或输出,不同设备对时钟边沿的要求不同,因此SPI定义了4种工作模式,由时钟极性(CPOL) 和时钟相位(CPHA) 组合决定。
1.3.1 时钟极性(CPOL)
- CPOL=0:SCK空闲状态为低电平(0);
- CPOL=1:SCK空闲状态为高电平(1)。
1.3.2 时钟相位(CPHA)
- CPHA=0:数据在SCK的第一个边沿(从空闲状态到有效状态的跳变)被采样;
- CPHA=1:数据在SCK的第二个边沿(从有效状态回到空闲状态的跳变)被采样。
1.3.3 4种工作模式组合
模式 | CPOL | CPHA | 采样时机(数据稳定阶段) | 典型应用设备 |
---|---|---|---|---|
0 | 0 | 0 | SCK从低→高(上升沿)采样,数据在下降沿输出 | SD卡、W25Q系列Flash |
1 | 0 | 1 | SCK从高→低(下降沿)采样,数据在上升沿输出 | 部分AD转换器 |
2 | 1 | 0 | SCK从高→低(下降沿)采样,数据在上升沿输出 | 较少见 |
3 | 1 | 1 | SCK从低→高(上升沿)采样,数据在下降沿输出 | OLED屏(如SSD1306)、RFID模块 |
关键:通信双方必须使用相同的工作模式,否则会出现数据错位(例如主机用模式0,从机也必须用模式0)。
1.4 SPI数据传输过程
SPI数据传输以"帧"为单位(通常为8位或16位),全双工模式下,主机和从机的传输同步进行(主机发送1字节的同时,从机也会返回1字节)。典型传输流程如下:
- 主机拉低目标从机的NSS信号(选中从机);
- 主机通过SCK线产生时钟信号,同时在MOSI线上逐位发送数据(高位在前或低位在前,由设备约定,通常为高位在前);
- 从机在SCK的对应边沿采样MOSI线上的数据,同时通过MISO线向主机返回数据;
- 传输完成后,主机拉高NSS信号(释放从机)。
示例:主机向从机发送0x55(二进制01010101),同时从机返回0xAA(10101010),模式0(CPOL=0,CPHA=0)的时序如下:
- 空闲时SCK为低电平,NSS为高电平;
- NSS拉低后,SCK开始跳动:第1个上升沿(低→高)采样第1位(主机发送0,从机返回1);
- 后续每一个上升沿依次采样第2~8位,最终主机接收0xAA,从机接收0x55。
1.5 SPI与其他协议的对比
协议 | 同步方式 | 信号线数量 | 传输速率 | 通信方式 | 典型应用场景 |
---|---|---|---|---|---|
SPI | 同步(时钟) | 3~4根 | 几Mbps~数百Mbps | 全双工/半双工 | 高速外设(Flash、OLED、AD) |
UART | 异步(波特率) | 2根 | 最高数Mbps | 全双工 | 调试、低速传感器 |
I2C | 同步(时钟) | 2根 | 最高几Mbps | 半双工 | 中低速外设(EEPROM、传感器) |
SPI优势 :速率高、全双工、无地址冲突(通过NSS选择)、协议简单(无复杂应答机制);
SPI劣势:信号线较多(多从机时NSS线增加)、无内置纠错机制(需软件实现校验)。
二、STM32 SPI外设详解
STM32系列芯片(如F1、F4、H7等)普遍集成多个SPI外设(如F103有2个SPI,F407有3个SPI,H743有6个SPI),支持主模式、从模式及多种高级特性,满足不同场景需求。
2.1 SPI外设主要特性
以STM32F103(中低端型号)为例,其SPI外设的核心特性如下:
- 支持主模式(Master)和从模式(Slave);
- 传输速率:主模式下最高18Mbps(F103,APB1时钟36MHz时,SPI1挂载APB2最高72MHz,速率可达36Mbps);
- 支持8位或16位数据帧;
- 支持4种工作模式(CPOL/CPHA可配置);
- 支持全双工、半双工和单线模式;
- 支持软件或硬件NSS管理;
- 支持中断和DMA传输(减少CPU占用);
- 支持CRC校验(可选,用于数据完整性检查);
- 支持数据收发中断(TXE:发送缓冲区空;RXNE:接收缓冲区非空)。
高端型号(如F4、H7)的SPI外设性能更强,例如F407的SPI最高速率可达45Mbps(APB2时钟90MHz时),H7系列甚至支持数百Mbps的传输速率,适合更高速的外设通信。
2.2 SPI外设引脚映射
STM32的SPI引脚通过"复用功能"配置,不同型号的引脚映射不同,需参考芯片数据手册的"复用功能表"。以STM32F103C8T6为例,其SPI1(高速SPI,挂载APB2总线)的默认引脚为:
- SCK:PA5(复用推挽输出);
- MOSI:PA7(复用推挽输出);
- MISO:PA6(浮空输入或上拉输入);
- NSS:PA4(推挽输出,硬件NSS时使用)。
若默认引脚被占用,可通过"重映射"功能切换到其他引脚(如SPI1可重映射到PB3/4/5),配置时需注意:
- 重映射需使能AFIO时钟(
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN
); - 通过AFIO重映射寄存器(如
AFIO->MAPR
)配置具体映射关系。
2.3 时钟源与速率计算
SPI的时钟来自APB总线(APB1或APB2),速率由"分频系数"决定:
- SPI挂载在APB1时,时钟源为PCLK1(F103中最高36MHz);
- SPI挂载在APB2时,时钟源为PCLK2(F103中最高72MHz)。
主模式下,SPI的传输速率计算公式为:
SPI时钟频率 = APB总线时钟频率 / 分频系数
分频系数可通过SPI控制寄存器1(SPI_CR1)的BR[2:0]位配置,可选值为2、4、8、16、32、64、128、256。例如:
- 若SPI1挂载APB2(72MHz),BR[2:0]配置为001(分频系数4),则传输速率为72MHz/4=18Mbps;
- 若SPI2挂载APB1(36MHz),BR[2:0]配置为000(分频系数2),则传输速率为36MHz/2=18Mbps。
2.4 核心寄存器解析
SPI的配置与操作主要通过以下寄存器实现(以STM32F103为例),理解这些寄存器是实现底层编程的基础。
2.4.1 SPI控制寄存器1(SPI_CR1)
位段 | 功能描述 |
---|---|
CPOL[1] | 时钟极性:0=空闲低电平;1=空闲高电平。 |
CPHA[0] | 时钟相位:0=第一个边沿采样;1=第二个边沿采样。 |
MSTR[2] | 主从模式选择:1=主模式;0=从模式。 |
BR[5:3] | 波特率分频:000=2分频,001=4分频,...,111=256分频。 |
SPE[6] | SPI使能:1=使能;0=禁用(配置时需先禁用)。 |
LSBFIRST[7] | 数据位顺序:0=高位在前(MSB);1=低位在前(LSB)。 |
SSI[8] | 内部从机选择(软件NSS模式下有效):1=内部拉高;0=内部拉低。 |
SSM[9] | 软件从机管理:1=使能软件NSS(通过SSI控制);0=硬件NSS(通过引脚控制)。 |
RXONLY[10] | 接收-only模式:1=只接收(半双工);0=全双工。 |
DFF[11] | 数据帧格式:0=8位;1=16位。 |
CRCNEXT[12] | 下一个传输为CRC:1=下一次传输发送CRC值;0=正常数据传输。 |
CRCEN[13] | CRC使能:1=启用CRC校验;0=禁用。 |
BIDIOE[14] | 双向模式输出使能(单线模式):1=输出使能;0=输入。 |
BIDIMODE[15] | 双向模式:1=单线双向模式;0=双线全双工模式。 |
2.4.2 SPI控制寄存器2(SPI_CR2)
位段 | 功能描述 |
---|---|
RXDMAEN[0] | 接收DMA使能:1=使能RXNE事件的DMA请求。 |
TXDMAEN[1] | 发送DMA使能:1=使能TXE事件的DMA请求。 |
SSOE[2] | 从机选择输出使能(硬件NSS模式):1=NSS引脚输出使能(主模式下自动拉低)。 |
ERRIE[5] | 错误中断使能:1=使能错误中断(如CRC错、溢出)。 |
TXEIE[7] | 发送缓冲区空中断使能:1=TXE事件触发中断。 |
RXNEIE[6] | 接收缓冲区非空中断使能:1=RXNE事件触发中断。 |
2.4.3 SPI状态寄存器(SPI_SR)
位段 | 功能描述 |
---|---|
RXNE[0] | 接收缓冲区非空:1=有数据待读取(读SPI_DR可清0)。 |
TXE[1] | 发送缓冲区空:1=可发送下一字节(写SPI_DR可清0)。 |
CHSIDE[2] | 通道侧(仅双线单向模式):0=主通道;1=副通道。 |
UDR[3] | 未读数据丢失:1=新数据接收时旧数据未读(溢出)。 |
CRCERR[4] | CRC错误:1=接收CRC与计算值不匹配。 |
MODF[5] | 模式错误(主从冲突):1=主模式下NSS被拉低(硬件NSS时)。 |
OVR[6] | 溢出错误:1=接收缓冲区未空时新数据到来。 |
BSY[7] | 忙标志:1=SPI正在传输数据(禁止配置参数)。 |
2.4.4 SPI数据寄存器(SPI_DR)
- 8位或16位寄存器,发送时写入数据,接收时读取数据;
- 主模式下,写入SPI_DR后,传输自动开始(SCK开始产生时钟);
- 全双工模式下,发送和接收同步进行,每次写入会触发一次传输,同时接收的数据存入DR。
三、SPI硬件设计要点
SPI硬件连接的可靠性直接影响通信稳定性,尤其在高速传输场景下,需注意信号线布局、电平匹配和抗干扰设计。
3.1 基本连接方式
3.1.1 单从机连接
- 主机(STM32)与单个从机的连接只需4根线(SCK、MOSI、MISO、NSS);
- 若从机无需主动发送数据(如只接收命令的OLED屏),可省略MISO线(半双工发送);
- 若从机固定选中(无需切换),可将从机的NSS引脚直接接地(省去主机NSS控制)。
3.1.2 多从机连接
多从机场景下,采用"菊花链"或"独立NSS"两种方式:
-
独立NSS :所有从机的SCK、MOSI、MISO并联,主机通过独立的NSS引脚(如PA4、PB0、PB1)分别控制各从机(推荐,控制灵活);
-
菊花链 :从机1的MISO连接从机2的MOSI,依次串联,仅最后一个从机的MISO返回主机(适合低速、简单场景,如多个移位寄存器)。
3.2 电平匹配与信号完整性
- 电平匹配:STM32的GPIO为3.3V电平,若从机为5V电平(如部分Flash芯片),需通过电平转换芯片(如74HC4050)实现3.3V与5V的转换,避免损坏STM32引脚;
- 信号线长度:SPI速率较高时(>10Mbps),信号线应尽量短(<10cm),减少信号延迟和干扰;
- 阻抗匹配:高速传输时,可在信号线(SCK、MOSI、MISO)串联50Ω电阻(靠近主机端),匹配传输线阻抗,抑制信号反射;
- 接地处理:所有设备共地,避免地电位差导致的电平偏移;敏感场景下可采用屏蔽线包裹信号线,屏蔽层单端接地。
3.3 NSS信号设计
NSS信号是SPI从机选择的核心,其设计需根据场景选择硬件或软件控制:
- 硬件NSS:主机通过SPI_CR2的SSOE位使能NSS输出,主模式下NSS会自动拉低(选中从机),传输完成后自动拉高(需确保从机NSS为低电平有效);优点是无需软件干预,适合高速场景;
- 软件NSS:通过普通GPIO模拟NSS信号(如PA4配置为推挽输出),传输前拉低,完成后拉高;优点是灵活(可任意选择引脚),适合多从机场景。
注意:硬件NSS模式下,若主模式中NSS引脚被外部拉低(如误操作),会触发MODF(模式错误),导致SPI进入从模式,需通过软件清除错误标志并重新配置。
四、SPI软件配置步骤
本节以STM32F103为主机,实现与从机的SPI通信,分别介绍寄存器级和HAL库的配置方法,以"主模式、全双工、8位数据、软件NSS、模式0(CPOL=0,CPHA=0)"为基础配置。
4.1 寄存器级配置(SPI1,主模式,18Mbps)
步骤1:使能时钟
c
// 使能GPIOA、SPI1和AFIO时钟(若需重映射)
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_SPI1EN | RCC_APB2ENR_AFIOEN;
步骤2:配置GPIO引脚
c
// 配置SCK(PA5)、MOSI(PA7)为复用推挽输出(50MHz)
GPIOA->CRL &= ~(GPIO_CRL_MODE5 | GPIO_CRL_CNF5); // 清除PA5配置
GPIOA->CRL |= GPIO_CRL_MODE5_1 | GPIO_CRL_MODE5_0; // 输出速率50MHz
GPIOA->CRL |= GPIO_CRL_CNF5_1; // 复用推挽输出
GPIOA->CRL &= ~(GPIO_CRL_MODE7 | GPIO_CRL_CNF7); // 清除PA7配置
GPIOA->CRL |= GPIO_CRL_MODE7_1 | GPIO_CRL_MODE7_0; // 输出速率50MHz
GPIOA->CRL |= GPIO_CRL_CNF7_1; // 复用推挽输出
// 配置MISO(PA6)为浮空输入
GPIOA->CRL &= ~(GPIO_CRL_MODE6 | GPIO_CRL_CNF6); // 清除PA6配置
GPIOA->CRL |= GPIO_CRL_CNF6_0; // 浮空输入(或上拉输入)
// 配置NSS(PA4)为推挽输出(软件控制)
GPIOA->CRL &= ~(GPIO_CRL_MODE4 | GPIO_CRL_CNF4); // 清除PA4配置
GPIOA->CRL |= GPIO_CRL_MODE4_1 | GPIO_CRL_MODE4_0; // 输出速率50MHz
GPIOA->CRL |= GPIO_CRL_CNF4_0; // 通用推挽输出
GPIOA->BSRR = GPIO_BSRR_BS4; // 初始拉高NSS(释放从机)
步骤3:配置SPI1参数
c
// 禁用SPI1(配置前必须禁用)
SPI1->CR1 &= ~SPI_CR1_SPE;
// 配置主模式、8位数据、高位在前、软件NSS
SPI1->CR1 &= ~(SPI_CR1_DFF | SPI_CR1_LSBFIRST | SPI_CR1_SSM);
SPI1->CR1 |= SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI; // 主模式,软件NSS,内部拉高
// 配置工作模式0(CPOL=0,CPHA=0)
SPI1->CR1 &= ~(SPI_CR1_CPOL | SPI_CR1_CPHA);
// 配置波特率:APB2时钟72MHz,分频系数4(72/4=18Mbps)
SPI1->CR1 &= ~SPI_CR1_BR;
SPI1->CR1 |= SPI_CR1_BR_0; // BR[2:0]=001 → 4分频
// 使能SPI1
SPI1->CR1 |= SPI_CR1_SPE;
步骤4:实现基本收发函数
c
// 拉低NSS,选中从机
void SPI1_SelectSlave(void) {
GPIOA->BSRR = GPIO_BSRR_BR4; // PA4置低
}
// 拉高NSS,释放从机
void SPI1_DeselectSlave(void) {
GPIOA->BSRR = GPIO_BSRR_BS4; // PA4置高
}
// 发送1字节并接收1字节(全双工)
uint8_t SPI1_TransmitReceive(uint8_t tx_data) {
// 等待发送缓冲区空
while (!(SPI1->SR & SPI_SR_TXE));
// 发送数据
SPI1->DR = tx_data;
// 等待接收完成
while (!(SPI1->SR & SPI_SR_RXNE));
// 返回接收数据
return SPI1->DR;
}
// 仅发送1字节(忽略接收数据)
void SPI1_Transmit(uint8_t tx_data) {
SPI1_TransmitReceive(tx_data);
}
// 仅接收1字节(发送无效数据0xFF)
uint8_t SPI1_Receive(void) {
return SPI1_TransmitReceive(0xFF);
}
4.2 HAL库配置(基于STM32CubeMX)
步骤1:创建工程与时钟配置
- 打开STM32CubeMX,选择芯片型号(如STM32F103C8T6);
- 配置RCC:选择HSE时钟,配置系统时钟为72MHz(APB2时钟72MHz,APB1时钟36MHz)。
步骤2:配置SPI1
- 在"Pinout & Configuration"中,左侧选择"Connectivity"→"SPI1";
- 模式选择"Full-Duplex Master"(全双工主模式);
- 参数配置:
- Prescaler:4(分频系数4,72MHz/4=18Mbps);
- Clock Polarity (CPOL):Low(0);
- Clock Phase (CPHA):1 Edge(0);
- Data Size:8 Bits;
- First Bit:MSB First;
- NSS Signal Type:Software(软件控制NSS);
- 确认引脚:SPI1_SCK=PA5,SPI1_MISO=PA6,SPI1_MOSI=PA7;
- 配置NSS引脚(PA4)为GPIO_Output(推挽输出,初始高电平)。
步骤3:生成代码
- 配置工程路径和IDE(如Keil MDK);
- 生成代码(确保SPI初始化函数
MX_SPI1_Init()
被正确生成)。
步骤4:HAL库收发函数实现
c
// 选中从机
void SPI1_SelectSlave(void) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
}
// 释放从机
void SPI1_DeselectSlave(void) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
}
// 全双工传输(发送并接收1字节)
uint8_t SPI1_TransmitReceive(uint8_t tx_data) {
uint8_t rx_data;
// 阻塞式传输,超时100ms
HAL_SPI_TransmitReceive(&hspi1, &tx_data, &rx_data, 1, 100);
return rx_data;
}
// 仅发送
void SPI1_Transmit(uint8_t *tx_buf, uint16_t len) {
HAL_SPI_Transmit(&hspi1, tx_buf, len, 100);
}
// 仅接收
void SPI1_Receive(uint8_t *rx_buf, uint16_t len) {
// 发送0xFF作为无效数据
uint8_t dummy = 0xFF;
for (uint16_t i = 0; i < len; i++) {
rx_buf[i] = SPI1_TransmitReceive(dummy);
}
}
五、实战案例:SPI外设通信
5.1 案例1:与W25Q128 Flash芯片通信
W25Q128是一款128Mbit(16MB)的SPI Flash芯片,支持SPI模式0/3,最高传输速率104Mbps,常用于存储固件、日志等数据。
5.1.1 通信流程
-
读取芯片ID:通过发送指令0x90(读JEDEC ID),可获取厂商ID(0xEF)和设备ID(0x4018),验证通信是否正常。
c// 寄存器级实现 uint32_t W25Q_ReadID(void) { uint32_t id = 0; SPI1_SelectSlave(); SPI1_Transmit(0x90); // 发送读ID指令 SPI1_Transmit(0x00); // 发送3字节地址(0x000000) SPI1_Transmit(0x00); SPI1_Transmit(0x00); id |= (SPI1_Receive() << 16); // 厂商ID(0xEF) id |= (SPI1_Receive() << 8); // 设备ID高8位(0x40) id |= SPI1_Receive(); // 设备ID低8位(0x18) SPI1_DeselectSlave(); return id; }
-
读取数据:发送指令0x03(读数据)+ 3字节地址,后续接收的数据即为地址处的内容。
cvoid W25Q_ReadData(uint32_t addr, uint8_t *buf, uint16_t len) { SPI1_SelectSlave(); SPI1_Transmit(0x03); // 读数据指令 SPI1_Transmit((addr >> 16) & 0xFF); // 地址高位 SPI1_Transmit((addr >> 8) & 0xFF); // 地址中位 SPI1_Transmit(addr & 0xFF); // 地址低位 for (uint16_t i = 0; i < len; i++) { buf[i] = SPI1_Receive(); // 读取数据 } SPI1_DeselectSlave(); }
-
页编程:W25Q128的最小写入单位为"页"(256字节),需先发送0x02指令+地址,再写入数据(不超过256字节)。
cvoid W25Q_PageProgram(uint32_t addr, uint8_t *buf, uint16_t len) { if (len > 256) len = 256; // 不超过页大小 W25Q_WriteEnable(); // 使能写入(发送0x06指令) SPI1_SelectSlave(); SPI1_Transmit(0x02); // 页编程指令 SPI1_Transmit((addr >> 16) & 0xFF); SPI1_Transmit((addr >> 8) & 0xFF); SPI1_Transmit(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { SPI1_Transmit(buf[i]); // 写入数据 } SPI1_DeselectSlave(); W25Q_WaitBusy(); // 等待写入完成(轮询状态寄存器) }
5.2 案例2:与SSD1306 OLED屏通信(SPI模式)
SSD1306是一款常用的OLED控制器,支持SPI通信(模式3:CPOL=1,CPHA=1),需注意其数据/命令区分(通过DC引脚,高电平数据,低电平命令)。
步骤1:硬件连接补充
- OLED的DC引脚连接STM32的PA3(推挽输出),用于区分数据和命令。
步骤2:OLED初始化(发送命令序列)
c
void OLED_Init(void) {
// 初始化前延时(确保电源稳定)
HAL_Delay(100);
SPI1_DeselectSlave();
HAL_Delay(10);
SPI1_SelectSlave();
// 发送初始化命令(参考SSD1306数据手册)
OLED_WriteCmd(0xAE); // 关闭显示
OLED_WriteCmd(0xD5); // 设置时钟分频
OLED_WriteCmd(0x80);
OLED_WriteCmd(0xA8); // 设置多路复用率
OLED_WriteCmd(0x3F);
// ... 其他初始化命令省略
OLED_WriteCmd(0xAF); // 开启显示
SPI1_DeselectSlave();
}
// 向OLED发送命令(DC=0)
void OLED_WriteCmd(uint8_t cmd) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET); // DC=0(命令)
SPI1_Transmit(cmd);
}
// 向OLED发送数据(DC=1)
void OLED_WriteData(uint8_t data) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET); // DC=1(数据)
SPI1_Transmit(data);
}
步骤3:显示字符(填充GRAM)
c
// 在指定位置显示一个字符(8x16字体)
void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t ch) {
// 计算字符在字库中的偏移(假设字库数组为font8x16)
uint8_t *p = &font8x16[(ch - ' ') * 16];
// 设置显示区域
OLED_SetWindow(x, y, x + 7, y + 15);
// 写入16字节数据(8列x16行)
for (uint8_t i = 0; i < 16; i++) {
OLED_WriteData(p[i]);
}
}
5.3 案例3:SPI DMA传输(高速批量数据)
对于大数据量传输(如从Flash读取固件、向显示屏刷新图像),使用DMA可减少CPU干预,提高效率。
步骤1:配置DMA(STM32CubeMX)
- 在SPI1配置中,"DMA Settings"→"Add":
- 接收:Stream选择"DMA1 Stream0"(SPI1_RX对应DMA1_Stream0),方向"Peripheral to Memory";
- 发送:Stream选择"DMA1 Stream3"(SPI1_TX对应DMA1_Stream3),方向"Memory to Peripheral";
- 模式选择"Normal"(单次传输),数据宽度"Byte"。
步骤2:DMA发送示例(刷新OLED全屏图像)
c
// 定义全屏图像缓冲区(128x64像素,共1024字节)
uint8_t OLED_GRAM[1024];
// 使用DMA发送GRAM数据到OLED
void OLED_Refresh(void) {
OLED_SetWindow(0, 0, 127, 7); // 设置全屏显示区域
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET); // DC=1(数据)
SPI1_SelectSlave();
// DMA发送1024字节数据
HAL_SPI_Transmit_DMA(&hspi1, OLED_GRAM, 1024);
// 等待DMA发送完成(可选,根据需求)
while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY);
SPI1_DeselectSlave();
}
// DMA发送完成回调函数(可选)
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
if (hspi == &hspi1) {
// 发送完成后的处理(如置标志位)
}
}
步骤3:DMA接收示例(从Flash读取大数据)
c
// 从W25Q128读取1024字节数据到缓冲区(DMA方式)
void W25Q_ReadData_DMA(uint32_t addr, uint8_t *buf) {
W25Q_WriteEnable();
SPI1_SelectSlave();
// 先发送读指令和地址(阻塞式)
uint8_t cmd[4] = {0x03, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF};
HAL_SPI_Transmit(&hspi1, cmd, 4, 100);
// DMA接收1024字节
HAL_SPI_Receive_DMA(&hspi1, buf, 1024);
// 等待接收完成
while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY);
SPI1_DeselectSlave();
}
六、SPI高级特性与优化
6.1 中断方式传输
对于非阻塞场景,可使用SPI中断实现数据收发,避免CPU轮询等待。
6.1.1 中断配置(寄存器级)
c
void SPI1_IT_Init(void) {
// 使能RXNE中断(接收非空)和TXE中断(发送空)
SPI1->CR2 |= SPI_CR2_RXNEIE | SPI_CR2_TXEIE;
// 配置NVIC(中断优先级)
NVIC_EnableIRQ(SPI1_IRQn);
NVIC_SetPriority(SPI1_IRQn, 2);
}
// SPI1中断服务函数
void SPI1_IRQHandler(void) {
uint8_t data;
// 接收非空中断
if (SPI1->SR & SPI_SR_RXNE) {
data = SPI1->DR; // 读取接收数据(清中断标志)
// 处理接收数据(如存入缓冲区)
}
// 发送空中断
if (SPI1->SR & SPI_SR_TXE) {
// 发送下一字节(若有数据)
if (tx_buf_index < tx_buf_len) {
SPI1->DR = tx_buf[tx_buf_index++];
} else {
// 发送完成,关闭TXE中断
SPI1->CR2 &= ~SPI_CR2_TXEIE;
}
}
}
6.1.2 HAL库中断使用
c
// 开启中断接收
HAL_SPI_Receive_IT(&hspi1, rx_buf, rx_len);
// 中断接收完成回调
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
if (hspi == &hspi1) {
// 处理接收数据
rx_complete_flag = 1;
}
}
6.2 单线模式与半双工
6.2.1 单线双向模式(仅用MOSI/MISO一根线)
- 配置
SPI_CR1->BIDIMODE=1
(单线模式); - 发送时
BIDIOE=1
(输出使能),接收时BIDIOE=0
(输入)。
6.2.2 半双工接收模式
- 配置
SPI_CR1->RXONLY=1
(仅接收),此时MOSI线无效,数据从MISO接收。
6.3 CRC校验
SPI支持硬件CRC校验,用于确保数据传输的完整性,步骤如下:
- 使能CRC:
SPI_CR1->CRCEN=1
; - 配置CRC多项式(默认0x07,可通过
SPI_CRCPR
修改); - 传输完成后,主机发送CRC值(
CRCNEXT=1
),从机比较接收的CRC与本地计算值,若不匹配则置CRCERR
标志。
七、常见问题与调试技巧
SPI通信失败是嵌入式开发中的常见问题,多数源于时序不匹配、硬件连接错误或配置参数错误,以下是排查思路与解决方案。
7.1 通信失败的核心排查点
7.1.1 时序与模式错误
- 现象:接收数据全为0xFF或乱码,发送后从机无响应;
- 原因:主机与从机的CPOL/CPHA不匹配,或波特率过高(从机不支持);
- 排查 :
- 用示波器测量SCK和MOSI波形,确认CPOL(空闲电平)和CPHA(采样边沿)是否与从机一致;
- 降低波特率(如先尝试1Mbps),排除速率不支持问题。
7.1.2 引脚连接错误
- 现象:无任何数据传输,SCK线无波形;
- 原因 :
- SCK/MOSI引脚未配置为复用功能(仍为普通GPIO);
- 引脚接反(如MOSI接MISO);
- NSS未拉低(从机未选中);
- 排查 :
- 用万用表测量NSS引脚电平,确认传输时为低电平;
- 检查SPI初始化代码,确保
SPI_CR1->SPE=1
(SPI已使能)。
7.1.3 数据位与格式错误
- 现象:接收数据错位(如0x55变成0xAA);
- 原因 :
- 数据位长度不匹配(主机8位,从机16位);
- 高位/低位顺序反了(MSB/LSB设置错误);
- 排查 :在从机数据手册中确认数据格式,修改
DFF
和LSBFIRST
参数。
7.1.4 中断与DMA配置错误
- 现象:DMA传输无数据,或中断不触发;
- 原因 :
- DMA通道选择错误(如SPI1_TX对应DMA1_Stream3,而非Stream0);
- 中断优先级被更高优先级中断抢占;
- DMA传输长度超过缓冲区大小;
- 排查 :用
HAL_DMA_GetState()
查看DMA状态,检查NVIC中断使能情况。
7.2 调试工具与方法
-
示波器/逻辑分析仪:
- 测量SCK、MOSI、MISO、NSS四线波形,对比从机数据手册的时序图;
- 确认SCK频率是否正确(如18Mbps对应周期约55.5ns);
- 检查NSS是否在传输期间保持低电平。
-
最小系统验证:
- 用"回环测试"验证SPI外设是否正常:将MOSI与MISO短接,主机发送数据后检查是否接收相同数据;
- 排除从机问题:先确保主机SPI自身工作正常,再连接外设。
-
分步调试:
- 先实现基础收发功能(如读取从机ID),再调试复杂功能(如页编程、DMA传输);
- 在关键步骤添加printf输出(通过UART),打印寄存器状态(如
SPI_SR
的BSY、RXNE位)。
-
参考官方例程:
- STM32Cube固件包中提供了SPI示例(如
STM32Cube_FW_F1_V1.8.4\Examples\SPI
),可对比配置差异。
- STM32Cube固件包中提供了SPI示例(如
7.3 高速传输优化技巧
- 缩短信号线长度:高速下(>10Mbps),信号线长度控制在5cm以内,避免信号反射;
- 使用DMA+中断组合:大数据量传输用DMA,小数据量用中断,平衡效率与实时性;
- 关闭CRC校验:若对数据完整性要求不高,可禁用CRC以减少传输延迟;
- 批量传输:减少NSS切换次数(如一次选中从机后传输多帧数据),避免频繁使能/禁用从机。
八、总结与扩展
SPI作为一种高速同步通信协议,在STM32与外设的交互中扮演着重要角色。本文从协议基础到实战案例,系统讲解了SPI的工作原理、STM32配置方法及调试技巧,核心要点包括:
- SPI的4种工作模式由CPOL和CPHA决定,通信双方必须一致;
- STM32 SPI外设支持主/从模式、中断/DMA传输,配置时需注意时钟源与引脚复用;
- 硬件设计需关注电平匹配、信号线布局和NSS控制;
- 实战中需根据外设特性(如Flash、OLED)调整命令序列和传输方式。
未来学习可扩展至:
- 多从机SPI网络的冲突处理;
- 低功耗模式下的SPI唤醒机制;
- 与其他协议(如I2C、UART)的混合通信系统设计;
- 高速SPI(>50Mbps)的PCBLayout优化。
掌握SPI不仅是嵌入式开发的基础技能,更是理解同步通信原理的关键。通过不断实践与调试,可逐步提升对SPI协议的理解与应用能力。
附录:常用代码片段
- SPI回环测试(验证外设功能):
c
uint8_t SPI1_LoopbackTest(void) {
uint8_t tx = 0xAA, rx;
// 短接MOSI和MISO
SPI1_SelectSlave();
rx = SPI1_TransmitReceive(tx);
SPI1_DeselectSlave();
return (rx == tx) ? 0 : 1; // 0=成功,1=失败
}
- SPI错误处理:
c
void SPI1_ClearError(void) {
// 清除错误标志(OVR、MODF、CRCERR)
if (SPI1->SR & SPI_SR_OVR) {
(void)SPI1->DR; // 读DR清除OVR
}
if (SPI1->SR & SPI_SR_MODF) {
SPI1->CR1 &= ~SPI_CR1_SPE; // 禁用SPI
SPI1->CR1 |= SPI_CR1_SPE; // 重新使能
}
if (SPI1->SR & SPI_SR_CRCERR) {
SPI1->SR &= ~SPI_SR_CRCERR; // 清除CRC错误
}
}
- 多从机管理:
c
// 从机列表(NSS引脚)
#define SLAVE1_NSS_PIN GPIO_PIN_4
#define SLAVE2_NSS_PIN GPIO_PIN_5
// 选择指定从机
void SPI_SelectSlave(uint16_t pin) {
// 先释放所有从机
HAL_GPIO_WritePin(GPIOA, SLAVE1_NSS_PIN | SLAVE2_NSS_PIN, GPIO_PIN_SET);
// 选中目标从机
HAL_GPIO_WritePin(GPIOA, pin, GPIO_PIN_RESET);
}