第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跑起来。到时候你会发现,前面这些基础工作做得越扎实,后面调起来越顺手。