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,通信失败。
时钟使能分两步:
- 外设时钟使能:
__HAL_RCC_SPI1_CLK_ENABLE(); - 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 完整初始化流程:一张图说清楚
上面讲了三个独立环节,但实际开发中它们是串在一起的。我画了一张流程图,帮你理清顺序:
你看,整个流程就像流水线。时钟是“供电”,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复用配置的问题。如果有时钟但数据不对,再查结构体参数和从设备配置。