第4章 MCU的SPI外设配置:STM32的SPI初始化、时钟分频计算、DMA传输配置

好,咱们接着聊。上一章我们把SPI协议的原理讲透了,这一章直接上硬菜——怎么在STM32上把SPI配起来,跑起来。

说实话,SPI的配置在STM32里算是比较直观的。但越简单的东西,越容易在细节上翻车。我这些年调试过的SPI问题,十有八九都出在时钟分频和DMA配合上。咱们一个一个说。

4.1 STM32的SPI初始化流程

先看最基本的初始化。STM32的SPI外设,说白了就是一组寄存器。你要告诉它:你是主机还是从机,数据怎么对齐,时钟极性怎么搞。

我个人习惯用STM32CubeMX先生成骨架代码,但关键参数一定要自己心里有数。来看一个典型的SPI1初始化代码:

void SPI1_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    SPI_InitTypeDef SPI_InitStruct = {0};

    // 1. 使能时钟
    __HAL_RCC_SPI1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    // 2. 配置GPIO:SCK, MOSI, MISO, NSS
    GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 3. 配置SPI外设
    SPI_InitStruct.Mode = SPI_MODE_MASTER;          // 主机模式
    SPI_InitStruct.Direction = SPI_DIRECTION_2LINES; // 全双工
    SPI_InitStruct.DataSize = SPI_DATASIZE_8BIT;     // 8位数据
    SPI_InitStruct.CLKPolarity = SPI_POLARITY_LOW;   // 空闲时SCK为低
    SPI_InitStruct.CLKPhase = SPI_PHASE_1EDGE;       // 第一个边沿采样
    SPI_InitStruct.NSS = SPI_NSS_SOFT;               // 软件管理片选
    SPI_InitStruct.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; // 分频系数
    SPI_InitStruct.FirstBit = SPI_FIRSTBIT_MSB;      // 高位先行
    SPI_InitStruct.TIMode = SPI_TIMODE_DISABLE;
    SPI_InitStruct.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    SPI_InitStruct.CRCPolynomial = 10;
    HAL_SPI_Init(&hspi1, &SPI_InitStruct);

    // 4. 使能SPI
    __HAL_SPI_ENABLE(&hspi1);
}

嗯,这里要注意。很多人把GPIO的复用功能配错了,结果SPI死活不通。我遇到过一哥们,调了两天,最后发现是AF号选错了——STM32F4和F1的AF映射不一样,这个坑我踩过。

4.2 时钟分频计算——别被公式绕晕

时钟分频,这是SPI配置里最容易出问题的地方。STM32的SPI时钟源来自APB总线,经过一个预分频器后得到SCK。

公式很简单:

SCK频率 = APB时钟频率 / 分频系数

分频系数可以是2、4、8、16、32、64、128、256。注意,没有1分频,也没有奇数分频。

举个例子。假设你的APB2时钟是84MHz(STM32F407典型值),你想让SPI跑在10.5MHz:

84MHz / 8 = 10.5MHz

那就选分频系数8。对应的宏是 SPI_BAUDRATEPRESCALER_8

但这里有个坑——SPI的最高频率受限于GPIO速度。我在项目中遇到过,配置了20MHz的SCK,但GPIO只配了低速模式,结果波形严重失真。GPIO速度至少要配到 GPIO_SPEED_FREQ_HIGH,否则高频信号会变成正弦波。

⚠️ 避坑指南:

我曾经在一个LoRa项目中,把SPI时钟配到了18MHz,结果LoRa模块死活不响应。用示波器一看,SCK波形已经圆润得像个小山丘。后来把分频系数从4改成8,降到9MHz,一切正常。

记住:不是越快越好。SPI时钟要匹配从器件的规格,LoRa模块通常建议不超过10MHz。

还有一个细节:分频后的SCK频率不能超过从器件的最大时钟频率。SX1278的SPI最高支持10MHz,你配个20MHz上去,它不会报错,但数据全是错的。

4.3 DMA传输配置——解放CPU的关键

SPI的轮询模式,说白了就是CPU在那傻等。每次发一个字节,等它发完,再发下一个。对于LoRa这种需要连续收发大量数据的场景,CPU会被活活拖死。

DMA就是干这个的。它帮CPU把数据从内存搬到SPI的发送寄存器,或者从接收寄存器搬到内存。CPU可以腾出手来处理协议栈、跑RTOS任务。

来看DMA配置的代码:

void SPI1_DMA_Init(void)
{
    // 1. 使能DMA时钟
    __HAL_RCC_DMA2_CLK_ENABLE();

    // 2. 配置DMA发送通道(SPI1_TX 使用 DMA2_Stream3)
    hdma_spi1_tx.Instance = DMA2_Stream3;
    hdma_spi1_tx.Init.Channel = DMA_CHANNEL_3;
    hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_spi1_tx.Init.Mode = DMA_NORMAL;
    hdma_spi1_tx.Init.Priority = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma_spi1_tx);

    // 3. 将DMA句柄关联到SPI
    __HAL_LINKDMA(&hspi1, hdmatx, hdma_spi1_tx);

    // 4. 使能DMA中断(可选)
    HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);
}

然后发送数据时,只需要调用:

HAL_SPI_Transmit_DMA(&hspi1, tx_buffer, length);

CPU就可以去干别的事了。等DMA传输完成,会触发回调函数 HAL_SPI_TxCpltCallback,你在里面处理后续逻辑。

💡 个人经验:

我建议在LoRa项目中,发送用DMA,接收用中断。为什么?因为LoRa模块的应答数据通常很短(几个字节),DMA的启动开销反而比中断大。发送时数据量大(比如要发256字节的payload),DMA的优势就出来了。

4.4 实战中的常见问题

我把这些年调SPI踩过的坑列一下,你遇到了可以直接对号入座:

问题现象 可能原因 解决方案
SCK无波形 GPIO复用功能未配置 检查AF号是否正确
数据全是0xFF MISO/MOSI接反 用万用表量一下通断
偶尔数据错位 时钟相位/极性不匹配 核对从器件datasheet的时序图
DMA传输卡死 DMA通道或流冲突 检查STM32参考手册的DMA映射表
高速时通信失败 分频系数过大或GPIO速度不足 降低分频或提高GPIO速度等级

嗯,这里要特别说一下DMA通道冲突的问题。STM32的DMA控制器资源有限,多个外设可能共用同一个DMA流。我遇到过SPI和USART抢DMA的情况,现象就是SPI发着发着突然停了。解决办法是查参考手册,给SPI分配一个独占的DMA流。

4.5 小结

SPI配置说难不难,说简单也不简单。核心就三点:

  • 时钟分频:算清楚APB时钟和分频系数,别超过从器件规格
  • GPIO配置:复用功能别选错,速度等级要匹配
  • DMA配合:大块数据用DMA,小块数据用中断,别死磕轮询

下一章我们会把这些配置整合到LoRa驱动里,真正让SX1278跑起来。到时候你会发现,前面这些基础工作做得越扎实,后面调起来越顺手。