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)的BR2:0位配置,可选值为2、4、8、16、32、64、128、256。例如:

  • 若SPI1挂载APB2(72MHz),BR2:0配置为001(分频系数4),则传输速率为72MHz/4=18Mbps;
  • 若SPI2挂载APB1(36MHz),BR2:0配置为000(分频系数2),则传输速率为36MHz/2=18Mbps。

2.4 核心寄存器解析

SPI的配置与操作主要通过以下寄存器实现(以STM32F103为例),理解这些寄存器是实现底层编程的基础。

2.4.1 SPI控制寄存器1(SPI_CR1)
位段 功能描述
CPOL1 时钟极性:0=空闲低电平;1=空闲高电平。
CPHA0 时钟相位:0=第一个边沿采样;1=第二个边沿采样。
MSTR2 主从模式选择:1=主模式;0=从模式。
BR5:3 波特率分频:000=2分频,001=4分频,...,111=256分频。
SPE6 SPI使能:1=使能;0=禁用(配置时需先禁用)。
LSBFIRST7 数据位顺序:0=高位在前(MSB);1=低位在前(LSB)。
SSI8 内部从机选择(软件NSS模式下有效):1=内部拉高;0=内部拉低。
SSM9 软件从机管理:1=使能软件NSS(通过SSI控制);0=硬件NSS(通过引脚控制)。
RXONLY10 接收-only模式:1=只接收(半双工);0=全双工。
DFF11 数据帧格式:0=8位;1=16位。
CRCNEXT12 下一个传输为CRC:1=下一次传输发送CRC值;0=正常数据传输。
CRCEN13 CRC使能:1=启用CRC校验;0=禁用。
BIDIOE14 双向模式输出使能(单线模式):1=输出使能;0=输入。
BIDIMODE15 双向模式:1=单线双向模式;0=双线全双工模式。
2.4.2 SPI控制寄存器2(SPI_CR2)
位段 功能描述
RXDMAEN0 接收DMA使能:1=使能RXNE事件的DMA请求。
TXDMAEN1 发送DMA使能:1=使能TXE事件的DMA请求。
SSOE2 从机选择输出使能(硬件NSS模式):1=NSS引脚输出使能(主模式下自动拉低)。
ERRIE5 错误中断使能:1=使能错误中断(如CRC错、溢出)。
TXEIE7 发送缓冲区空中断使能:1=TXE事件触发中断。
RXNEIE6 接收缓冲区非空中断使能:1=RXNE事件触发中断。
2.4.3 SPI状态寄存器(SPI_SR)
位段 功能描述
RXNE0 接收缓冲区非空:1=有数据待读取(读SPI_DR可清0)。
TXE1 发送缓冲区空:1=可发送下一字节(写SPI_DR可清0)。
CHSIDE2 通道侧(仅双线单向模式):0=主通道;1=副通道。
UDR3 未读数据丢失:1=新数据接收时旧数据未读(溢出)。
CRCERR4 CRC错误:1=接收CRC与计算值不匹配。
MODF5 模式错误(主从冲突):1=主模式下NSS被拉低(硬件NSS时)。
OVR6 溢出错误:1=接收缓冲区未空时新数据到来。
BSY7 忙标志: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设置错误);
  • 排查 :在从机数据手册中确认数据格式,修改DFF和LSBFIRST参数。
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);
}
相关推荐
HQB202612 小时前
EG1198|屹晶微电子 120V 内置 MOS 异步降压 DC‑DC(高性价比)
嵌入式硬件·硬件工程
果壳science14 小时前
张祥前实验如何获取正电荷
笔记·科技·嵌入式硬件·硬件工程·制造
南山电子nscn14 小时前
中科芯CETC汽车电池管理系统BMS进口芯片国产型号替代指南
单片机·汽车·bms
沉淀的.晴天14 小时前
can总线
stm32
桀人15 小时前
认识AT24C02
stm32·单片机·嵌入式硬件
国科安芯16 小时前
星间激光通信终端CANFD控制网络的时延确定性与辐射可靠性
网络·嵌入式硬件·信息与通信·canfd·低轨卫星·商业航天·抗辐射
一直C16 小时前
嵌入式 Linux 驱动开发学习笔记:内核定时器、platform 总线与 I²C 驱动
linux·驱动开发·嵌入式硬件·学习·imx6ull
传感器与混合集成电路16 小时前
150MHz主频、8路DAC与6路PWM:JLH221360伺服控制SIP解析
嵌入式硬件
comyccccc16 小时前
STM32PID控制板设计全解析学习笔记
单片机·嵌入式硬件
碧海银沙音频科技研究院17 小时前
实现炬芯ATS308X的usb cdc 串口IAP 固件升级功能实现
人工智能·嵌入式硬件·深度学习·算法