一、为什么需要 HAL:从寄存器到 CMSIS 到 HAL
问题起点:C 代码本来就能直接读写硬件寄存器(靠内存映射),但写法五花八门、无法移植。
ARM 的解法 ------ CMSIS 标准(Cortex Microcontroller Software Interface Standard),把代码分成三层:
| 层 | 谁负责 | 内容 |
|---|---|---|
| 内核层 | ARM | 统一的寄存器定义、内核操作函数(NVIC、SysTick、__WFI 等) |
| 外设层 | 各芯片厂商(ST / GD / NXP...) | 按统一格式定义自家外设寄存器和中断号 |
| 高级库层 | ST | 在 CMSIS 之上再封一层:HAL 库 |
收益:同一套内核代码可跨厂商复用,外设代码换芯片时改动量大幅下降。
二、HAL / 标准库 / LL 三者对比
| 特点 | 代价 | |
|---|---|---|
| HAL | 统一 API、带参数检查、回调机制、DMA 封装 | 代码量约为标准库的 1.5~2 倍,多几层函数调用,稍慢 |
| 标准库(SPL) | 紧贴寄存器,精简、快 | 绑死单个系列;已停止维护,新芯片不再提供 |
| LL(Low-Layer) | 代码量接近标准库,命名风格与 HAL 一致 | 覆盖的外设不如 HAL 全 |
-
LL 可以和 HAL 混用 (例如 GPIO 用 LL、USART 用 HAL),但同一个外设不要混用。
-
选 HAL 的现实理由不只是"可移植":ST 新芯片只出 HAL/CubeMX,标准库已是历史。
三、内核级 API(CMSIS 提供,HAL 不封装的那一层)
3.1 关 / 开总中断
__disable_irq(); /* PRIMASK = 1 */
/* 临界区:这几行必须一口气执行完,不能被中断打断 */
__enable_irq(); /* PRIMASK = 0 */
-
"总中断"= Cortex-M 内核的
PRIMASK寄存器 。它为 1 时,除 NMI 和 HardFault 外,所有可屏蔽中断全部关闭。 -
用途:进入临界区。
__enable_irq()是"无条件开门"。如果临界区里又嵌了一层临界区,内层的__enable_irq()会在外层还没执行完时就提前开门。正确写法是保存并恢复:
uint32_t pm = __get_PRIMASK(); __disable_irq(); ... __set_PRIMASK(pm); /* 恢复到进入前的状态,而不是无条件开 */RTOS 里另外用
BASEPRI(__set_BASEPRI)只屏蔽低于某优先级的中断,让高优先级中断仍能响应。
3.2 NVIC:使能与优先级
HAL_NVIC_SetPriority(USART1_IRQn, 2, 0); /* 抢占优先级 2,子优先级 0 */
HAL_NVIC_EnableIRQ(USART1_IRQn); /* 内部即 CMSIS 的 NVIC_EnableIRQ */
配置完中断触发条件后,必须调 NVIC_EnableIRQ 开这个门,NVIC 才会真的响应。
| 函数 | 说明 |
|---|---|
NVIC_EnableIRQ(IRQn) |
CMSIS 原版,按中断号使能 |
NVIC_SetPriority(IRQn, priority) |
CMSIS 原版,只收一个拼好的数值(需自己算分组),不直观 |
HAL_NVIC_SetPriority(IRQn, 抢占, 子) |
实际用的是它,两个参数分开传,内部做分组换算 |
HAL_NVIC_SetPriorityGrouping(group) |
设优先级分组 |
优先级规则:
-
抢占优先级:数字越小越优先,高的能打断低的(中断嵌套)
-
子优先级 :抢占相同时决定谁先响应,不能互相打断
-
STM32F4 只实现 4 位优先级 (
__NVIC_PRIO_BITS = 4),所以每档取值 0~15
3.3 SysTick
SysTick_Config(ticks);
HAL_Delay() 和 HAL_GetTick() 背后就是它。
- 链路:
HAL_Init()→HAL_InitTick()配置 SysTick 为 1ms →SysTick_Handler()(在stm32f4xx_it.c)调HAL_IncTick()→ 累加uwTick→HAL_GetTick()读它。
SysTick 优先级默认是最低的 (
TICK_INT_PRIORITY = 0x0F),所以 在中断服务函数里调HAL_Delay()会死等 ------SysTick 抢不进来,uwTick不增加。自己调
SysTick_Config()会覆盖 HAL 的配置,导致HAL_Delay()失效。要用就确认没冲突。
3.4 低功耗与空指令
while (1) {
/* 处理事件 */
__WFI(); /* 没事就睡觉,等按键/串口中断唤醒 */
}
-
__WFI():进入睡眠,低功耗产品(尤其电池供电)的标配。 -
__NOP():一个时钟周期什么都不做。不跟HAL_Delay()竞争 (一个是 ns 级,一个是 ms 级)。用途:模拟 I2C / OLED / SD 卡这类软件时序,用 NOP 凑周期最精确。
四、CubeMX 标准工程的文件地图
4.1 目录结构
工程根/
├── .ioc ← 配置真源,必须进 git
├── Core/
│ ├── Inc/ main.h stm32f4xx_hal_conf.h stm32f4xx_it.h
│ └── Src/ main.c stm32f4xx_hal_msp.c stm32f4xx_it.c system_stm32f4xx.c
├── Drivers/
│ ├── CMSIS/ ← 内核 + 器件头文件(ARM + ST)
│ └── STM32F4xx_HAL_Driver/ ← HAL 源码(Inc + Src)
└── MDK-ARM/ ← .uvprojx + startup_stm32f4xxxx.s
4.2 各文件职责
| 文件 | 位置 | 职责 |
|---|---|---|
main.c / main.h |
Core | main()、SystemClock_Config()、MX_xxx_Init();引脚宏定义 |
stm32f4xx_hal_conf.h |
Core/Inc | HAL 配置:外设模块开关 、HSE_VALUE(晶振值)、TICK_INT_PRIORITY |
stm32f4xx_it.h |
Core/Inc | 中断服务函数的声明 |
stm32f4xx_it.c |
Core/Src | 中断服务函数实现 :SysTick_Handler、USART1_IRQHandler 等 |
stm32f4xx_hal_msp.c |
Core/Src | 外设底层初始化 :GPIO 复用、时钟使能、NVIC 优先级配置------CubeMX 自动生成骨架 |
system_stm32f4xx.c |
Core/Src | SystemInit(),上电时初始化系统时钟、向量表;一般不用改 |
stm32f4xx_hal.c |
Drivers | HAL_Init、HAL_Delay、HAL_GetTick、弱定义的 HAL_MspInit |
外设中断的优先级和使能,是在
stm32f4xx_hal_msp.c里生成的
4.3 HAL 头文件索引
| 功能 | 头文件 |
|---|---|
HAL 总入口(HAL_Init / HAL_Delay) |
stm32f4xx_hal.h |
| GPIO(点灯、读引脚) | stm32f4xx_hal_gpio.h |
| 时钟配置 | stm32f4xx_hal_rcc.h |
| 串口 | stm32f4xx_hal_uart.h / stm32f4xx_hal_usart.h |
| 外部中断 | stm32f4xx_hal_exti.h |
| 定时器 | stm32f4xx_hal_tim.h |
| SPI | stm32f4xx_hal_spi.h |
| I2C | stm32f4xx_hal_i2c.h |
| DMA | stm32f4xx_hal_dma.h |
| NVIC / SysTick 的 HAL 封装 | stm32f4xx_hal_cortex.h |
| ADC | stm32f4xx_hal_adc.h |
| 低功耗 | stm32f4xx_hal_pwr.h |
| Flash 读写 | stm32f4xx_hal_flash.h·· |