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数据传输有三种方式,各有各的适用场景。我画了张图,帮你理清思路。

SPI三种传输模式对比 轮询模式 中断模式 DMA模式 特点 • CPU全程等待 • 代码简单,阻塞 • 适合短数据、低频 特点 • TXE/RXNE触发中断 • 非阻塞,CPU可做其他事 • 适合中等数据量 特点 • 硬件自动搬运数据 • 几乎不占CPU • 适合大数据量、高速 适用场景 • 配置寄存器 • 读取传感器单次值 适用场景 • 实时性要求不高的通信 • 数据量几百字节以内 适用场景 • 高速数据采集 • 音频/图像传输

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%的问题出在初始化配置上。时钟极性、相位、数据帧大小、波特率,这四个参数只要有一个不对,通信就失败。我的习惯是:先看从设备手册,确认参数,再写代码。不要凭感觉配。