4. STM32 SPI外设:框图、寄存器与HAL库初始化
好,咱们今天来啃硬骨头——STM32的SPI外设。说实话,很多工程师用HAL库调SPI,调通了就完事,一旦出问题就抓瞎。我见过不少同事,SPI通信不稳定,翻来覆去改时钟极性,就是搞不定。其实啊,问题多半出在寄存器配置上,你根本不知道外设内部是怎么工作的。
这一节,我会带着你把SPI外设的框图、关键寄存器、HAL库初始化流程,一个一个拆开看。嗯,看完之后,你至少能回答三个问题:数据怎么从MCU飞到外部设备?寄存器里每个位到底控制什么?HAL_Init()背后干了哪些事?
4.1 SPI框图:数据流动的“高速公路”
先看框图。STM32的SPI模块,说白了就是一条双向数据通道。我习惯把框图分成三个功能区:时钟生成、数据收发、状态控制。
核心数据路径:发送数据时,你把字节写入DR寄存器,数据经过移位寄存器,从MOSI引脚逐位发出;接收时,数据从MISO引脚进入移位寄存器,再送到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库把寄存器操作封装成了结构体和函数。你不需要手动操作每个位,但理解背后的流程很重要。我习惯把初始化分成三步:
- 引脚配置:GPIO复用功能、速度、上下拉。
- SPI参数配置:填充SPI_InitTypeDef结构体。
- 使能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库初始化,这三块是基础中的基础。你把这些吃透了,后面调试多设备级联的时候,心里就有底了。