STM32中SPI协议详解

前言

在嵌入式系统中,设备间的数据传输协议多种多样,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字节)。典型传输流程如下:

  1. 主机拉低目标从机的NSS信号(选中从机);
  2. 主机通过SCK线产生时钟信号,同时在MOSI线上逐位发送数据(高位在前或低位在前,由设备约定,通常为高位在前);
  3. 从机在SCK的对应边沿采样MOSI线上的数据,同时通过MISO线向主机返回数据;
  4. 传输完成后,主机拉高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 通信流程
  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;
    }
  2. 读取数据:发送指令0x03(读数据)+ 3字节地址,后续接收的数据即为地址处的内容。

    c 复制代码
    void 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();
    }
  3. 页编程:W25Q128的最小写入单位为"页"(256字节),需先发送0x02指令+地址,再写入数据(不超过256字节)。

    c 复制代码
    void 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校验,用于确保数据传输的完整性,步骤如下:

  1. 使能CRC:SPI_CR1->CRCEN=1
  2. 配置CRC多项式(默认0x07,可通过SPI_CRCPR修改);
  3. 传输完成后,主机发送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设置错误);
  • 排查 :在从机数据手册中确认数据格式,修改DFFLSBFIRST参数。
7.1.4 中断与DMA配置错误
  • 现象:DMA传输无数据,或中断不触发;
  • 原因
    • DMA通道选择错误(如SPI1_TX对应DMA1_Stream3,而非Stream0);
    • 中断优先级被更高优先级中断抢占;
    • DMA传输长度超过缓冲区大小;
  • 排查 :用HAL_DMA_GetState()查看DMA状态,检查NVIC中断使能情况。

7.2 调试工具与方法

  1. 示波器/逻辑分析仪

    • 测量SCK、MOSI、MISO、NSS四线波形,对比从机数据手册的时序图;
    • 确认SCK频率是否正确(如18Mbps对应周期约55.5ns);
    • 检查NSS是否在传输期间保持低电平。
  2. 最小系统验证

    • 用"回环测试"验证SPI外设是否正常:将MOSI与MISO短接,主机发送数据后检查是否接收相同数据;
    • 排除从机问题:先确保主机SPI自身工作正常,再连接外设。
  3. 分步调试

    • 先实现基础收发功能(如读取从机ID),再调试复杂功能(如页编程、DMA传输);
    • 在关键步骤添加printf输出(通过UART),打印寄存器状态(如SPI_SR的BSY、RXNE位)。
  4. 参考官方例程

    • STM32Cube固件包中提供了SPI示例(如STM32Cube_FW_F1_V1.8.4\Examples\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协议的理解与应用能力。

附录:常用代码片段

  1. 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=失败
}
  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错误
    }
}
  1. 多从机管理
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);
}
相关推荐
丁满与彭彭1 小时前
嵌入式学习笔记-MCU阶段-DAY01
笔记·单片机·学习
海海不掉头发2 小时前
【计算机组成原理】-CPU章节学习篇—笔记随笔
笔记·单片机·学习·考研·计算机组成原理
趣多多代言人2 小时前
从零开始手写嵌入式实时操作系统
开发语言·arm开发·单片机·嵌入式硬件·面试·职场和发展·嵌入式
h137286978693 小时前
Type-C PD快充协议智能芯片S312L详解
嵌入式硬件
不想学习\??!5 小时前
STM32-外部中断
stm32·单片机·嵌入式硬件
不想学习\??!5 小时前
STM32-定时器
stm32·单片机·嵌入式硬件
LIN-JUN-WEI6 小时前
[ESP32]VSCODE+ESP-IDF环境搭建及blink例程尝试(win10 win11均配置成功)
c语言·开发语言·ide·vscode·单片机·学习·编辑器
LS_learner6 小时前
嵌入式系统中实现串口重定向
嵌入式硬件
趣多多代言人7 小时前
嵌入式面试八股文100题(二)
单片机·嵌入式硬件