4. STM32 SPI外设:框图、寄存器与HAL库初始化

好,咱们今天来啃硬骨头——STM32的SPI外设。说实话,很多工程师用HAL库调SPI,调通了就完事,一旦出问题就抓瞎。我见过不少同事,SPI通信不稳定,翻来覆去改时钟极性,就是搞不定。其实啊,问题多半出在寄存器配置上,你根本不知道外设内部是怎么工作的。

这一节,我会带着你把SPI外设的框图、关键寄存器、HAL库初始化流程,一个一个拆开看。嗯,看完之后,你至少能回答三个问题:数据怎么从MCU飞到外部设备?寄存器里每个位到底控制什么?HAL_Init()背后干了哪些事?

4.1 SPI框图:数据流动的“高速公路”

先看框图。STM32的SPI模块,说白了就是一条双向数据通道。我习惯把框图分成三个功能区:时钟生成、数据收发、状态控制。

核心数据路径:发送数据时,你把字节写入DR寄存器,数据经过移位寄存器,从MOSI引脚逐位发出;接收时,数据从MISO引脚进入移位寄存器,再送到DR寄存器等你读取。全双工嘛,收发同时进行。

STM32 SPI 外设内部框图 CPU / DMA 总线 DR 数据寄存器 移位寄存器 波特率发生器 控制逻辑 (CR1/CR2) SR 状态寄存器 MOSI MISO SCK NSS 关键路径 DR ↔ 移位寄存器 移位寄存器 ↔ 引脚 时钟驱动移位节奏

从框图你能看到几个关键点:

  • DR寄存器是CPU和移位寄存器之间的缓冲。你写DR,数据进移位寄存器;你读DR,数据从移位寄存器出来。
  • 移位寄存器是真正干活的地方。每个SCK时钟脉冲,它移出一位、移入一位。
  • 波特率发生器决定SCK频率。它从系统时钟分频得到,分频系数由CR1的BR位控制。
  • 控制逻辑根据CR1/CR2的配置,决定时钟极性、相位、数据帧格式、主从模式等。

我的经验:调试SPI时,我习惯先看SCK引脚有没有时钟输出。没时钟?查波特率发生器和CR1的SPE使能位。有时钟但数据不对?查CPOL/CPHA和DR读写时序。这个排查顺序,帮我省了不少时间。

4.2 寄存器配置:CR1、CR2、SR、DR

寄存器是SPI外设的“控制面板”。你想想看,每个位都是一个开关。我刚开始学的时候,对着参考手册一个一个位看,头都大了。后来我总结了一个方法:只记关键位,其他位用默认值。

4.2.1 CR1控制寄存器1

CR1是SPI的核心配置寄存器。下面这几个位,你必须掌握:

位域 名称 功能说明 我的建议
Bit 15 BIDIMODE 双向数据模式使能 一般用0,全双工模式
Bit 10:9 SSI / SSM 软件NSS管理 多设备级联时常用软件管理
Bit 8 DFF 数据帧格式:0=8位,1=16位 大部分传感器用8位
Bit 5:3 BR[2:0] 波特率分频系数 从低速开始调,稳定后再提速
Bit 2 CPOL 时钟极性 与从设备严格匹配
Bit 1 CPHA 时钟相位 与从设备严格匹配
Bit 0 SPE SPI使能 最后再置1,所有配置完成后

我曾经踩过的坑:有一次我把CPOL和CPHA搞反了,结果从设备死活不响应。用逻辑分析仪一看,数据采样点完全错位。后来我养成了一个习惯:先看从设备数据手册的时序图,确认CPOL和CPHA,再配置主设备。这个顺序不能乱。

4.2.2 CR2控制寄存器2

CR2主要控制中断和DMA。常用的位不多:

  • Bit 7: TXEIE:发送缓冲区空中断使能。当DR可写时触发中断。
  • Bit 6: RXNEIE:接收缓冲区非空中断使能。当DR有数据可读时触发中断。
  • Bit 1: FRXTH:RXNE标志阈值。0表示8位数据帧时RXNE在1字节后置位,16位时在2字节后置位。1表示无论帧格式,RXNE在1字节后置位。
  • Bit 0: RXDMAEN / TXDMAEN:DMA请求使能。大数据量传输时用DMA,CPU可以干别的。

我个人习惯:小数据量(几个字节)用轮询,中等数据量(几十到几百字节)用中断,大数据量(KB级别)用DMA。你根据场景选就行。

4.2.3 SR状态寄存器

SR是SPI的“健康仪表盘”。轮询方式下,你主要看这两个标志位:

位域 名称 含义 轮询时的操作
Bit 7 BSY 忙标志:正在通信或移位寄存器未空 发送前等待BSY=0
Bit 1 RXNE 接收缓冲区非空:DR有数据可读 置1时读取DR
Bit 0 TXE 发送缓冲区空:DR可写 置1时写入DR

关键点:全双工模式下,你每写一次DR,同时就会触发一次接收。所以发送和接收是成对出现的。我见过有人只写DR不读DR,结果RXNE一直置1,导致后续数据覆盖——嗯,数据就丢了。

4.2.4 DR数据寄存器

DR是16位的寄存器。8位数据帧时,只使用低8位;16位数据帧时,使用全部16位。读写DR都会自动清除对应的状态标志位:

  • 写DR → 清除TXE标志
  • 读DR → 清除RXNE标志

这里有个细节:写DR之前,一定要等TXE=1;读DR之前,一定要等RXNE=1。别问我为什么,我曾经在高速通信时没检查TXE就直接写DR,结果数据覆盖,波形乱成一团。

4.3 HAL库初始化流程:从配置到使能

HAL库把寄存器操作封装成了结构体和函数。你不需要手动操作每个位,但理解背后的流程很重要。我习惯把初始化分成三步:

  1. 引脚配置:GPIO复用功能、速度、上下拉。
  2. SPI参数配置:填充SPI_InitTypeDef结构体。
  3. 使能SPI:调用HAL_SPI_Init(),内部置位SPE。

下面是一个典型的初始化代码:

SPI_HandleTypeDef hspi1;

void MX_SPI1_Init(void)
{
    hspi1.Instance = SPI1;
    hspi1.Init.Mode = SPI_MODE_MASTER;          // 主模式
    hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 全双工
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;     // 8位数据帧
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;   // CPOL=0
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;       // CPHA=0
    hspi1.Init.NSS = SPI_NSS_SOFT;               // 软件管理NSS
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; // 分频16
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;      // MSB先行
    hspi1.Init.TIMode = SPI_TIMODE_DISABLE;      // 禁用TI模式
    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    hspi1.Init.CRCPolynomial = 10;

    if (HAL_SPI_Init(&hspi1) != HAL_OK)
    {
        Error_Handler(); // 初始化失败处理
    }
}

这段代码背后,HAL_SPI_Init()做了这些事:

  • 调用HAL_SPI_MspInit(),配置GPIO和时钟。
  • 复位SPI外设(写CR1的SPE=0)。
  • 按结构体参数配置CR1、CR2、CRCPR等寄存器。
  • 最后置位CR1的SPE位,使能SPI。

我的习惯:初始化完成后,我会立即做一次自检——发送一个已知字节,看接收到的数据是否相同(环回模式)。如果自检通过,说明SPI硬件通路没问题。这个步骤在调试阶段特别有用。

4.4 数据收发流程:轮询方式示例

初始化完成后,怎么收发数据?我以轮询方式为例,给你一个完整的收发流程:

// 发送一个字节,同时接收一个字节
uint8_t SPI_TransmitReceiveByte(SPI_TypeDef *SPIx, uint8_t data)
{
    // 等待TXE=1,确保发送缓冲区空
    while (!(SPIx->SR & SPI_SR_TXE));

    // 写DR,启动发送
    SPIx->DR = data;

    // 等待RXNE=1,确保接收完成
    while (!(SPIx->SR & SPI_SR_RXNE));

    // 读DR,返回接收数据
    return (uint8_t)SPIx->DR;
}

你看,流程很清晰:等TXE→写DR→等RXNE→读DR。全双工模式下,发送和接收是同步的,一个函数搞定。

注意:高速通信时,轮询方式会占用CPU。如果SPI时钟超过10MHz,我建议用中断或DMA。否则CPU一直在等标志位,别的任务就卡死了。

4.5 常见配置错误与排查

最后,我总结几个我在项目中遇到过的配置错误,你调试时可以参考:

现象 可能原因 排查方法
SCK无时钟输出 SPE未使能、时钟未开启、GPIO复用配置错误 检查RCC时钟使能、CR1的SPE位、GPIO模式
数据全为0xFF或0x00 CPOL/CPHA不匹配、MISO/MOSI接反 用逻辑分析仪看时序,对比从设备手册
数据错位或丢失 DR读写时序不对、未等待TXE/RXNE 在读写DR前后加标志位检查
通信偶尔失败 波特率过高、信号完整性差、NSS管理不当 降低波特率、加长SCK低电平时间、检查NSS电平

嗯,这一节的内容就到这里。SPI外设的框图、寄存器、HAL库初始化,这三块是基础中的基础。你把这些吃透了,后面调试多设备级联的时候,心里就有底了。