4. SPI驱动开发(STM32):HAL库SPI初始化、轮询/中断/DMA三种模式实现、数据帧格式配置(8位/16位)
好,咱们进入实战环节。SPI驱动开发,说白了就是让STM32的SPI外设按照你的意愿工作。HAL库帮我们封装了很多底层操作,但如果你不清楚背后的原理,出了问题照样抓瞎。我个人习惯,写SPI驱动前,先想清楚三个问题:用哪种模式(轮询/中断/DMA)?数据帧是8位还是16位?时钟极性相位怎么配?
4.1 HAL库SPI初始化:从结构体到寄存器
初始化是第一步,也是最容易踩坑的地方。HAL库的初始化函数HAL_SPI_Init()依赖一个结构体SPI_HandleTypeDef。我刚开始用的时候,以为填完结构体就万事大吉,结果有一次SPI死活不通,折腾了半天才发现是CLKPolarity和CLKPhase配反了。
核心结构体成员速查表
| 成员 | 可选值 | 说明 |
|---|---|---|
Mode |
SPI_MODE_MASTER / SPI_MODE_SLAVE |
主从模式选择 |
Direction |
SPI_DIRECTION_2LINES / SPI_DIRECTION_2LINES_RXONLY / SPI_DIRECTION_1LINE |
全双工、只接收、半双工 |
DataSize |
SPI_DATASIZE_8BIT / SPI_DATASIZE_16BIT |
数据帧宽度 |
CLKPolarity |
SPI_POLARITY_LOW / SPI_POLARITY_HIGH |
空闲时SCK电平 |
CLKPhase |
SPI_PHASE_1EDGE / SPI_PHASE_2EDGE |
数据采样沿 |
NSS |
SPI_NSS_SOFT / SPI_NSS_HARD_INPUT / SPI_NSS_HARD_OUTPUT |
片选管理方式 |
BaudRatePrescaler |
SPI_BAUDRATEPRESCALER_2 ~ SPI_BAUDRATEPRESCALER_256 |
波特率预分频 |
FirstBit |
SPI_FIRSTBIT_MSB / SPI_FIRSTBIT_LSB |
MSB还是LSB先行 |
TIMode |
SPI_TIMODE_DISABLE / SPI_TIMODE_ENABLE |
TI模式,一般禁用 |
CRCCalculation |
SPI_CRCCALCULATION_DISABLE / SPI_CRCCALCULATION_ENABLE |
硬件CRC,少用 |
嗯,这里要注意:时钟极性和相位必须和从设备匹配。我曾经遇到过一块LCD屏,手册上写的是模式0(CPOL=0, CPHA=0),结果我配成了模式3,屏幕死活不亮。后来用示波器一看,SCK波形完全反了。
我的小技巧:初始化完成后,先读一下SPIx->CR1和SPIx->CR2寄存器,确认配置是否真的写进去了。HAL库有时会因为时钟没使能而静默失败。
4.2 三种传输模式:轮询、中断、DMA
SPI数据传输有三种方式,各有各的适用场景。我画了张图,帮你理清思路。
4.2.1 轮询模式
说白了就是CPU死等。调用HAL_SPI_TransmitReceive()后,函数不会返回,直到数据传完。代码最简单,但效率最低。
// 轮询模式示例:发送1字节并接收
uint8_t tx_data = 0xAA;
uint8_t rx_data = 0;
HAL_SPI_TransmitReceive(&hspi1, &tx_data, &rx_data, 1, 1000);
// 函数返回时,数据已经收发完成
我一般在初始化阶段或者读取单次传感器数据时用轮询。比如配置一个SPI DAC的输出值,发几个字节就完事,没必要上中断。
注意:轮询模式下,如果从设备响应慢,超时时间设得太短会导致HAL_TIMEOUT错误。我曾经把超时设成10ms,结果从设备需要20ms才能准备好,每次通信都超时。后来改成500ms才解决。
4.2.2 中断模式
中断模式让CPU从等待中解放出来。调用HAL_SPI_TransmitReceive_IT()后,函数立即返回,数据在中断服务函数里逐字节处理。
// 中断模式:启动传输
uint8_t tx_buf[16] = {0};
uint8_t rx_buf[16] = {0};
HAL_SPI_TransmitReceive_IT(&hspi1, tx_buf, rx_buf, 16);
// 传输完成回调
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1)
{
// 数据收发完成,可以处理rx_buf了
}
}
嗯,这里有个坑:中断服务函数里不要做耗时操作。我见过有人在回调里直接调用printf,结果串口还没打完,下一个SPI中断又来了,直接导致数据错乱。
4.2.3 DMA模式
DMA模式是终极方案。数据搬运完全由DMA控制器完成,CPU只需要在开始和结束时介入。调用HAL_SPI_TransmitReceive_DMA()即可。
// DMA模式:启动传输
uint8_t tx_buf[256] = {0};
uint8_t rx_buf[256] = {0};
HAL_SPI_TransmitReceive_DMA(&hspi1, tx_buf, rx_buf, 256);
// DMA传输完成回调
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1)
{
// 256字节已通过DMA自动传输完成
}
}
避坑指南:使用DMA时,发送和接收缓冲区必须保持有效,直到传输完成。我曾经犯过一个错误:在局部变量里定义tx_buf,函数返回后栈空间被释放,DMA还在读那块内存,结果全是乱码。解决办法:用全局数组或者动态分配内存。
4.3 数据帧格式配置:8位 vs 16位
SPI的数据帧可以是8位或16位。这个配置看似简单,但影响深远。
| 特性 | 8位数据帧 | 16位数据帧 |
|---|---|---|
| 配置宏 | SPI_DATASIZE_8BIT |
SPI_DATASIZE_16BIT |
| 每次传输位数 | 8位(1字节) | 16位(2字节) |
| DR寄存器访问 | 按uint8_t读写 | 按uint16_t读写 |
| 典型应用 | 大多数传感器、Flash、LCD | 音频Codec、高精度ADC |
| 注意事项 | 字节序由FirstBit决定 | 大端/小端需与从设备匹配 |
为什么会有16位模式?说白了,有些外设的数据寄存器本身就是16位的,比如音频芯片的左右声道数据。用8位模式发16位数据,你得发两次,还要自己拼凑,麻烦不说还容易出错。
// 8位模式:发送两个字节
uint8_t data8[2] = {0x12, 0x34};
HAL_SPI_Transmit(&hspi1, data8, 2, 1000);
// 16位模式:发送一个16位数据
uint16_t data16 = 0x1234;
HAL_SPI_Transmit(&hspi1, (uint8_t*)&data16, 1, 1000); // 注意:长度参数是"帧数"
重要提醒:HAL库的传输函数,第三个参数是数据帧数量,不是字节数。16位模式下传1帧就是2字节。我刚开始没注意,传了长度2,结果发了4个字节,从设备直接懵了。
4.4 实战经验总结
说了这么多,最后分享几个我自己的经验。
- 初始化顺序很重要:先配置GPIO(复用功能、速度、上下拉),再使能SPI时钟,最后调用
HAL_SPI_Init()。顺序反了可能导致初始化失败。 - 片选信号要手动控制:除非你用的是硬件NSS,否则我建议用普通GPIO控制片选。HAL库的
HAL_SPI_TransmitReceive不会自动拉高拉低NSS,你得自己在前后加HAL_GPIO_WritePin。 - 调试时先降速:把预分频设大一点,比如
SPI_BAUDRATEPRESCALER_256,让SPI时钟降到几百kHz。这样用示波器看波形更容易发现问题。 - 别忘了使能DMA时钟:用DMA模式时,很多人只配了SPI,忘了使能DMA时钟。结果SPI初始化成功,但DMA不工作。检查一下
__HAL_RCC_DMAx_CLK_ENABLE()有没有调用。
核心要点:SPI驱动开发,80%的问题出在初始化配置上。时钟极性、相位、数据帧大小、波特率,这四个参数只要有一个不对,通信就失败。我的习惯是:先看从设备手册,确认参数,再写代码。不要凭感觉配。