3. HAL库SPI初始化:HAL_SPI_Init()结构体参数详解、GPIO复用配置、时钟使能与NVIC设置

说实话,很多初学者拿到STM32开发板,第一件事就是复制粘贴CubeMX生成的代码。跑起来了,很开心。但一旦换个引脚、改个时钟,就懵了。

我刚开始做项目时也踩过这个坑。有一次调试一块SPI Flash,死活读不到ID。折腾了两天,最后发现是GPIO复用配置错了——AF号没选对。嗯,从那以后,我每次写SPI初始化都会把这几步拆开来看清楚。

这一章,我们就来拆解HAL_SPI_Init()背后的三个关键环节:结构体参数、GPIO复用配置、时钟与NVIC。你把这三点吃透了,以后任何SPI外设的初始化都不会慌。

3.1 HAL_SPI_Init() 结构体参数详解

先看核心结构体 SPI_InitTypeDef。它定义在 stm32f4xx_hal_spi.h 里,我习惯把它叫做“SPI的七参数”。

成员变量 可选值 说明
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 时钟极性(CPOL)
CLKPhase SPI_PHASE_1EDGE / SPI_PHASE_2EDGE 时钟相位(CPHA)
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 数据传输顺序
TIMode SPI_TIMODE_DISABLE / SPI_TIMODE_ENABLE TI模式(一般不用)
CRCCalculation SPI_CRCCALCULATION_DISABLE / SPI_CRCCALCULATION_ENABLE 硬件CRC使能
CRCPolynomial 用户自定义(如7、0x21) CRC多项式

核心经验:我一般只改前8个参数。TIMode和CRC在99%的常规SPI通信中都用不到。如果你不确定,就保持默认关闭。

举个例子,配置一个8位数据、全双工、主机模式、CPOL=0/CPHA=0的SPI:

hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 7;
HAL_SPI_Init(&hspi1);

避坑指南:我曾经把BaudRatePrescaler设成了2,结果SPI时钟跑到24MHz,从设备根本跟不上。后来查手册才发现,很多SPI从设备最高只支持10MHz。所以,先看从设备手册,再定分频系数。

3.2 GPIO复用配置:别让引脚“哑巴”了

结构体配好了,但SPI不会自动工作。你还要告诉STM32:哪些引脚是SPI功能,而不是普通GPIO。

这就是GPIO复用功能(Alternate Function,AF)配置。说白了,就是把引脚从“普通IO模式”切换到“外设专用模式”。

以STM32F407为例,SPI1的引脚通常是:

  • PA5 → SPI1_SCK
  • PA6 → SPI1_MISO
  • PA7 → SPI1_MOSI

它们的复用功能编号是AF5。

配置代码示例:

__HAL_RCC_GPIOA_CLK_ENABLE();   // 使能GPIOA时钟

GPIO_InitTypeDef GPIO_InitStruct = {0};
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);

注意:MISO引脚的模式是GPIO_MODE_AF_PP吗?是的。虽然MISO是输入方向,但在复用功能模式下,引脚方向由SPI外设控制,GPIO只负责“连接”。所以统一用AF_PP即可。

你可能会问:我怎么知道某个引脚对应哪个AF号?

查数据手册的“Alternate function mapping”表。或者用CubeMX,点一下引脚,它会自动告诉你AF号。我个人习惯用CubeMX做引脚规划,然后对照手册确认一遍,双重保险。

3.3 时钟使能:不给电,怎么干活?

SPI外设本身也需要时钟。很多人配好了GPIO,配好了结构体,但忘了使能SPI时钟——结果就是SPI寄存器全是0,通信失败。

时钟使能分两步:

  1. 外设时钟使能:__HAL_RCC_SPI1_CLK_ENABLE();
  2. GPIO时钟使能:__HAL_RCC_GPIOA_CLK_ENABLE();

顺序有讲究吗?我建议先使能GPIO时钟,再使能SPI时钟。虽然HAL库内部会处理,但养成这个习惯可以避免一些奇怪的时序问题。

时钟树小提示:SPI1挂载在APB2总线上,最高时钟为84MHz(F407)。SPI2/SPI3挂载在APB1上,最高42MHz。你配BaudRatePrescaler时,要基于这个总线时钟来计算实际SPI频率。

3.4 NVIC设置:中断来了,谁来响应?

如果你要用SPI中断方式(比如SPI接收中断、发送完成中断),就必须配置NVIC。

NVIC配置其实就三件事:

  • 使能中断通道:HAL_NVIC_EnableIRQ(SPI1_IRQn);
  • 设置抢占优先级和子优先级:HAL_NVIC_SetPriority(SPI1_IRQn, 0, 0);
  • 编写中断服务函数:在SPI1_IRQHandler()中调用HAL_SPI_IRQHandler(&hspi1);

代码示例:

// 在主函数或初始化中
HAL_NVIC_SetPriority(SPI1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(SPI1_IRQn);

// 在stm32f4xx_it.c中
void SPI1_IRQHandler(void)
{
    HAL_SPI_IRQHandler(&hspi1);
}

我的经验:如果你只是用HAL_SPI_Transmit()这种阻塞式函数,不需要配置NVIC。只有当你用HAL_SPI_Transmit_IT()或HAL_SPI_Receive_DMA()时,才需要开中断。别一上来就把所有中断都开了,用不到反而增加系统负担。

3.5 完整初始化流程:一张图说清楚

上面讲了三个独立环节,但实际开发中它们是串在一起的。我画了一张流程图,帮你理清顺序:

SPI初始化完整流程 ① 使能GPIO时钟 + SPI时钟 ② 配置GPIO复用功能AF ③ 填充SPI_InitTypeDef结构体 ④ 调用HAL_SPI_Init() ⑤ 配置NVIC(仅中断/DMA方式) 提示:步骤①~④是必须的,步骤⑤按需添加。顺序不要颠倒。

你看,整个流程就像流水线。时钟是“供电”,GPIO是“接线”,结构体是“参数设置”,最后HAL_SPI_Init()是“总装”。NVIC是“可选配件”,需要中断时才装上去。

3.6 常见错误与避坑

最后,我总结几个我在项目中真实遇到过的坑:

  • AF号配错:比如SPI1的AF是5,你配成了AF6。结果引脚没反应,示波器量SCK永远是低电平。解决办法:每次配完都看一眼数据手册的AF表。
  • 时钟顺序反了:先使能SPI时钟,再使能GPIO时钟。虽然HAL库能容错,但有些早期芯片版本会出问题。我建议先GPIO后SPI。
  • NSS模式选错:如果你用软件片选(NSS_SOFT),但配成了硬件NSS,那么每次通信前都要手动拉低CS引脚,否则SPI不会启动。我习惯用软件NSS,灵活可控。
  • 中断优先级冲突:SPI中断优先级设得太高,导致其他低优先级中断被饿死。或者设得太低,SPI数据溢出。建议根据实时性需求合理分配优先级,一般设到1或2即可。

重要提醒:如果你在调试时发现SPI通信完全没反应,先别急着改代码。用示波器或逻辑分析仪量一下SCK引脚有没有时钟输出。如果没有,99%是时钟使能或GPIO复用配置的问题。如果有时钟但数据不对,再查结构体参数和从设备配置。