STM32HAL 快速入门(三十二):SPI HAL 库编程
前言
大家好,这里是 Hello_Embed。上一篇我们理解了 SPI 的硬件引脚和时序规则,今天进入实战核心 ——SPI HAL 库编程。和 UART、I2C 一致,SPI 也支持 查询、中断、DMA 三种方式,且核心逻辑相通:“启动传输 + 回调判断完成”。特别注意 SPI 的特性:发送数据时必然接收数据,接收数据前必须先发送数据(即使发送无效数据)。本篇将从函数梳理、CubeMX 配置,到中断 / DMA 底层实现,完整拆解 SPI 编程逻辑。
一、SPI 核心特性与 HAL 函数梳理
首先明确 SPI 的关键特性:全双工传输,“发必收、收先发”。基于此,HAL 库为三种方式提供了对应的收发函数,参数和逻辑高度统一。
| 编程方式 | 功能 | HAL库函数 | 关键差异 |
|---|---|---|---|
| 查询方式 | 发送 + 接收 | HAL_SPI_TransmitReceive(&hspi, pTxData, pRxData, Size, Timeout) |
含 Timeout,阻塞等待完成 |
| 查询方式 | 仅发送 | HAL_SPI_Transmit(&hspi, pData, Size, Timeout) |
同上,接收缓冲区无效 |
| 查询方式 | 仅接收 | HAL_SPI_Receive(&hspi, pData, Size, Timeout) |
需先发送无效数据(如 0x00) |
| 中断方式 | 发送 + 接收 | HAL_SPI_TransmitReceive_IT(&hspi, pTxData, pRxData, Size) |
无 Timeout,非阻塞,靠回调 |
| 中断方式 | 仅发送 | HAL_SPI_Transmit_IT(&hspi, pData, Size) |
同上,接收相关参数置空 |
| 中断方式 | 仅接收 | HAL_SPI_Receive_IT(&hspi, pData, Size) |
需先启动发送无效数据的中断 |
| DMA方式 | 发送 + 接收 | HAL_SPI_TransmitReceive_DMA(&hspi, pTxData, pRxData, Size) |
无 Timeout,DMA 自动传输 |
| DMA方式 | 仅发送 | HAL_SPI_Transmit_DMA(&hspi, pData, Size) |
同上,DMA 仅配置发送通道 |
| DMA方式 | 仅接收 | HAL_SPI_Receive_DMA(&hspi, pData, Size) |
需先配置 DMA 发送无效数据 |
2. 函数参数说明
所有函数的核心参数含义一致,以中断方式的 “发送 + 接收” 为例:
| 参数 | 说明 |
|---|---|
SPI_HandleTypeDef *hspi |
指定使用的 SPI 控制器(如 &hspi1,CubeMX 自动生成) |
uint8_t *pTxData |
发送数据的缓冲区指针(仅发送 / 收发函数有效,接收函数可设为 NULL) |
uint8_t *pRxData |
接收数据的缓冲区指针(仅接收 / 收发函数有效,发送函数可设为 NULL) |
uint16_t Size |
传输数据的长度(字节数,发送和接收长度必须一致,因全双工同步) |
uint32_t Timeout |
超时时间(仅查询方式有,单位 ms,超时未完成返回错误) |
二、CubeMX 配置:SPI1 引脚、DMA 与中断
以 “SPI1 主模式 + 中断 + DMA” 为例,配置步骤如下(从设备为 SPI Flash):
1. 引脚配置
SPI1 核心引脚与片选引脚(CS)配置:
- PA5(SPI1_SCK):时钟线(主设备输出);
- PA6(SPI1_MISO):主设备输入线(从设备输出);
- PA7(SPI1_MOSI):主设备输出线(从设备输入);
- PB9(CS):片选引脚(通用 GPIO 输出,手动拉低选中从设备);
引脚配置图:
2. SPI 模式配置
- 进入
Connectivity → SPI1,选择 Full-Duplex Master(全双工主模式); - 配置 SPI 时序(如 CPOL=0、CPHA=0,数据位 = 8 位,MSB 优先),需与从设备匹配;
- 使能 “Hardware NSS Signal”(若用硬件 CS,也可手动控制 GPIO 作为 CS)。
3. DMA 配置
添加 SPI1 的 DMA 发送和接收通道(减少 CPU 干预):
- 发送通道:
DMA1 Channel 3(SPI1_TX 对应通道,方向Memory To Peripheral); - 接收通道:
DMA1 Channel 2(SPI1_RX 对应通道,方向Peripheral To Memory); - 模式均设为
Normal(一次传输完成后停止,需手动重启);
配置图:
4. 中断使能
- 进入
NVIC Settings,勾选 SPI1 global interrupt(SPI 中断)和 DMA1 Channel 2/3 interrupt(DMA 中断); - 设置中断优先级(确保不低于其他低优先级中断);
配置图:
配置完成后生成工程,CubeMX 会自动初始化 SPI1、DMA 及中断向量表。
三、中断方式的底层实现:从启动到回调
中断方式的核心是 “启动传输→中断触发→回调置位标志”,我们以最常用的 HAL_SPI_TransmitReceive_IT 为例,拆解底层逻辑。
1. 启动中断:HAL_SPI_TransmitReceive_IT 函数解析
该函数仅 “启动传输并配置中断”,不等待完成,关键代码如下:
/* 1. 配置传输参数:发送/接收缓冲区、长度、计数 */
hspi->ErrorCode = HAL_SPI_ERROR_NONE; // 初始化错误码
hspi->pTxBuffPtr = (const uint8_t *)pTxData; // 发送缓冲区指针
hspi->TxXferSize = Size; // 总发送长度
hspi->TxXferCount = Size; // 剩余发送计数(初始=总长度)
hspi->pRxBuffPtr = (uint8_t *)pRxData; // 接收缓冲区指针
hspi->RxXferSize = Size; // 总接收长度
hspi->RxXferCount = Size; // 剩余接收计数
/* 2. 配置中断服务函数(根据数据位选择 8 位/16 位) */
if (hspi->Init.DataSize > SPI_DATASIZE_8BIT)
{
hspi->RxISR = SPI_2linesRxISR_16BIT; // 16位接收中断函数
hspi->TxISR = SPI_2linesTxISR_16BIT; // 16位发送中断函数
}
else
{
hspi->RxISR = SPI_2linesRxISR_8BIT; // 8位接收中断函数(常用)
hspi->TxISR = SPI_2linesTxISR_8BIT; // 8位发送中断函数(常用)
}
/* 3. 使能 SPI 发送/接收中断(TXE 和 RXNE 中断) */
__HAL_SPI_ENABLE_IT(hspi, SPI_IT_TXE | SPI_IT_RXNE | SPI_IT_ERR);
return HAL_OK; // 立即返回,不等待传输完成
关键说明:
2lines含义:指 SPI 用 MOSI 和 MISO 两条数据线(全双工模式);TxXferCount/RxXferCount:剩余传输计数,每传输 1 字节减 1,减至 0 表示传输完成;- 中断使能后,硬件会在 “发送缓冲区空(TXE)” 或 “接收缓冲区非空(RXNE)” 时触发中断。
2. 仅发送 / 仅接收的中断配置差异
以 “仅发送” 函数 HAL_SPI_Transmit_IT 为例,与 “收发” 函数的差异在于 “接收参数置空”:
/* 仅发送时,接收相关参数设为 NULL/0 */
hspi->pRxBuffPtr = (uint8_t *)NULL; // 接收缓冲区空
hspi->RxXferSize = 0U; // 接收长度 0
hspi->RxXferCount = 0U;
hspi->RxISR = NULL; // 接收中断函数空
/* 仅配置发送中断函数(无 2lines,因不涉及接收) */
hspi->TxISR = SPI_TxISR_8BIT; // 仅发送的中断函数,无 MISO 相关逻辑
3. 中断服务函数:SPI1_IRQHandler 与回调触发
当 SPI 触发中断时,会先进入中断入口函数 SPI1_IRQHandler,再调用 HAL 库的中断处理函数:
/* SPI1 中断入口(CubeMX 自动生成) */
void SPI1_IRQHandler(void)
{
HAL_SPI_IRQHandler(&hspi1); // 调用 HAL 库中断处理函数
}
/* HAL 库中断处理核心函数 */
void HAL_SPI_IRQHandler(SPI_HandleTypeDef *hspi)
{
uint32_t itflag = hspi->Instance->SR; // 读取 SPI 状态寄存器(SR)
/* 1. 接收中断(RXNE 标志置位) */
if ((itflag & SPI_FLAG_RXNE) && (hspi->RxISR != NULL))
{
hspi->RxISR(hspi); // 调用接收中断函数(如 SPI_2linesRxISR_8BIT)
return;
}
/* 2. 发送中断(TXE 标志置位) */
if ((itflag & SPI_FLAG_TXE) && (hspi->TxISR != NULL))
{
hspi->TxISR(hspi); // 调用发送中断函数(如 SPI_2linesTxISR_8BIT)
return;
}
}
4. 数据传输的实际执行:SPI_2linesRxISR_8BIT 解析
以 8 位接收中断函数为例,看看数据如何存入接收缓冲区:
static void SPI_2linesRxISR_8BIT(SPI_HandleTypeDef *hspi)
{
/* 1. 读取 SPI 数据寄存器(DR)的值,存入接收缓冲区 */
*hspi->pRxBuffPtr = *((__IO uint8_t *)&hspi->Instance->DR);
/* 2. 接收缓冲区指针后移,剩余接收计数减 1 */
hspi->pRxBuffPtr++;
hspi->RxXferCount--;
/* 3. 判断接收是否完成(剩余计数为 0) */
if (hspi->RxXferCount == 0U)
{
/* 禁用接收和错误中断 */
__HAL_SPI_DISABLE_IT(hspi, SPI_IT_RXNE | SPI_IT_ERR);
/* 若发送也完成(收发同步),调用关闭函数并触发回调 */
if (hspi->TxXferCount == 0U)
{
SPI_CloseRxTx_ISR(hspi); // 关闭收发中断
HAL_SPI_TxRxCpltCallback(hspi); // 触发收发完成回调
}
}
}
发送中断函数逻辑对称(以 SPI_2linesTxISR_8BIT 为例):
- 从发送缓冲区读取数据,写入 SPI 数据寄存器(DR);
- 发送缓冲区指针后移,剩余发送计数减 1;
- 发送完成后禁用发送中断,若接收也完成,触发
HAL_SPI_TxRxCpltCallback。
四、DMA 方式的中断逻辑:批量传输更高效
DMA 方式适用于 “大量数据传输”,核心优势是 “CPU 不参与字节级传输”,仅在 “传输一半” 或 “传输完成” 时触发一次中断(而非每字节一次)。
1. DMA 启动函数:HAL_SPI_TransmitReceive_DMA
该函数会配置 DMA 通道的 “传输完成回调”,关键代码如下:
/* 1. 配置 DMA 发送/接收通道的回调函数 */
hspi->hdmatx->XferCpltCallback = SPI_DMATransmitCplt; // 发送完成回调
hspi->hdmarx->XferCpltCallback = SPI_DMAReceiveCplt; // 接收完成回调
hspi->hdmarx->XferHalfCpltCallback = SPI_DMAHalfReceiveCplt; // 接收一半回调
/* 2. 启动 DMA 接收和发送通道 */
HAL_DMA_Start_IT(hspi->hdmarx, (uint32_t)&hspi->Instance->DR, (uint32_t)pRxData, Size);
HAL_DMA_Start_IT(hspi->hdmatx, (uint32_t)pTxData, (uint32_t)&hspi->Instance->DR, Size);
/* 3. 使能 SPI 的 DMA 模式(TX DMA 和 RX DMA 使能) */
__HAL_SPI_ENABLE_IT(hspi, SPI_IT_TXDMAEN | SPI_IT_RXDMAEN);
return HAL_OK;
2. DMA 中断处理:DMA1_Channel2_IRQHandler
DMA 传输完成后,会触发 DMA 通道的中断,进入入口函数后调用 HAL 库 DMA 处理函数:
/* DMA1 接收通道(Channel 2)中断入口 */
void DMA1_Channel2_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_spi1_rx); // 调用 DMA 中断处理函数
}
/* HAL 库 DMA 中断处理核心 */
void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
{
/* 1. 传输一半中断(可选,需配置) */
if (hdma->XferHalfCpltCallback != NULL && (hdma->Instance->CR & DMA_CR_HTIE) && (hdma->Instance->ISR & DMA_ISR_HTIF))
{
hdma->XferHalfCpltCallback(hdma); // 触发传输一半回调
}
/* 2. 传输完成中断 */
if (hdma->XferCpltCallback != NULL && (hdma->Instance->CR & DMA_CR_TCIE) && (hdma->Instance->ISR & DMA_ISR_TCIF))
{
hdma->XferCpltCallback(hdma); // 触发传输完成回调(如 SPI_DMATransmitReceiveCplt)
}
}
3. DMA 与中断的回调统一
无论 DMA 还是中断方式,最终都会调用相同的 “传输完成回调函数”:
- 中断方式:
HAL_SPI_TxRxCpltCallback(hspi); - DMA 方式:
SPI_DMATransmitReceiveCplt内部也会调用HAL_SPI_TxRxCpltCallback(hspi);
因此,我们只需在HAL_SPI_TxRxCpltCallback中设置 “传输完成标志”,即可统一判断三种方式的完成状态。
五、总结:三种方式的共性与差异
| 维度 | 查询方式 | 中断方式 | DMA方式 |
|---|---|---|---|
| CPU 占用 | 高(阻塞等待) | 中(仅中断时占用) | 低(DMA 自动传输) |
| 适用场景 | 少量数据、单任务 | 中量数据、多任务 | 大量数据(如 Flash 读写) |
| 完成判断 | 函数返回值 | 回调函数置位标志 | 回调函数置位标志 |
| 核心优势 | 逻辑简单 | 非阻塞,兼顾多任务 | 高效,解放 CPU |
结尾
本篇我们梳理了 SPI HAL 库的三种编程方式,深入解析了中断和 DMA 的底层逻辑 —— 核心都是 “启动传输 + 回调判断完成”,与 UART、I2C 保持一致。
下一篇,我们将深入Flash W25Q64芯片,理解其工作模式,我们会基于寄存器层面结合时序图看看Flash的芯片是如何读数据、擦除、烧写数据的。
Hello_Embed 继续带你从函数解析到实战应用,逐步掌握 SPI 全场景开发,敬请期待~
更多推荐
所有评论(0)