STM32 实验任务3:HAL 库 + 按键中断控制流水灯暂停/恢复
任务要求
在前面实验(寄存器、标准外设库流水灯)基础上,改用HAL 库 + 按键中断方式控制流水灯运行的暂停、恢复继续。
问 1:没有 CubeMX,HAL 库工程怎么手动搭建?
通常用 HAL 库开发会用 STM32CubeMX 自动生成工程,但我电脑上没有 CubeMX,也没有官方 CubeF1 固件包,所以选择手动下载 HAL 库文件 + 手写工程,顺便把 HAL 库工程结构彻底搞明白。
1.1 下载 HAL 库文件
HAL 库(STM32CubeF1)在 GitHub 上其实拆成了两个仓库(都是 git submodule):
cmsis_device_f1:设备头文件、启动文件、系统时钟源文件stm32f1xx_hal_driver:HAL 外设驱动(.c/.h)
把需要的 16 个文件下载后整理成工程目录:
stm32-led-hal/ ├── main.c ← 用户主程序 ├── stm32f1xx_hal_conf.h ← HAL 配置头文件(外设模块总开关) ├── Device/ │ ├── Include/ ← 芯片头文件(stm32f1xx.h 等) │ ├── Source/ ← system_stm32f1xx.c(系统时钟初始化) │ └── Startup/ ← startup_stm32f103xb.s(启动文件) └── HAL/ ├── Inc/ ← HAL 驱动头文件(hal.h、hal_gpio.h 等) └── Src/ ← HAL 驱动源文件(hal.c、hal_gpio.c 等)1.2 Keil 工程配置
点魔术棒(Options for Target)→C/C++页:
| 配置项 | 值 | 说明 |
|---|---|---|
| Define | STM32F103xB,USE_HAL_DRIVER | 芯片型号宏 + 使用 HAL 库宏 |
| Include Paths | .\;.\HAL\Inc;.\Device\Include | 头文件搜索路径 |
| Language C | c99 | HAL 库/CMSIS 6.x 要求 |
Run-Time Environment 里只勾选CMSIS → CORE,不勾 Device → Startup(启动文件用 HAL 仓库下载的 startup_stm32f103xb.s)。
1.3 踩坑记录(编译/链接报错排查)
坑 1:Legacy/stm32_hal_legacy.h file not found
编译 HAL 驱动时报找不到Legacy/stm32_hal_legacy.h。这个头文件在 HAL 仓库的Inc/Legacy/子目录里,补下载到HAL/Inc/Legacy/下解决。
坑 2:stm32f1xx_hal_rcc_ex.h file not found
RCC 驱动里还引用了_ex(扩展)头文件。预防性把stm32f1xx_hal_flash_ex.h、stm32f1xx_hal_gpio_ex.h、stm32f1xx_hal_rcc_ex.h全部补下载。
坑 3:链接报错Undefined symbol HAL_GPIO_Init等(关键坑)
编译通过,但链接报一串 undefined symbol:FLASH_PageErase、HAL_GPIO_Init、HAL_GPIO_WritePin……
排查发现根因是stm32f1xx_hal_conf.h里漏写了HAL_GPIO_MODULE_ENABLED。新版 HAL 库每个驱动文件整体被这种宏包住:
#ifdefHAL_GPIO_MODULE_ENABLED...gpio 驱动全部内容...#endif宏没定义 → gpio 驱动文件编译出来是空文件(链接时自然找不到函数)。把用到的模块宏全补上:
#defineHAL_FLASH_MODULE_ENABLED#defineHAL_GPIO_MODULE_ENABLED#defineHAL_RCC_MODULE_ENABLED#defineHAL_CORTEX_MODULE_ENABLED并且还需要在工程里添加stm32f1xx_hal_flash_ex.c(FLASH_PageErase在这个文件里)。补完后0 Error 0 Warning。
问 2:按键中断怎么实现?
2.1 硬件接线
| 器件 | 引脚 | 说明 |
|---|---|---|
| 红灯 | PA0 | 高电平点亮 |
| 绿灯 | PB0 | 高电平点亮 |
| 蓝灯 | PC14 | 高电平点亮(PC13/14/15 低速引脚) |
| 板载灯 | PC13 | 低电平点亮(极性相反) |
| 轻触按键 | PA1 → GND | 按下时 PA1 被拉低 |
按键一端接 PA1,另一端接 GND,按下按键 PA1 产生下降沿,正好用 EXTI 外部中断的下降沿触发来检测。
2.2 按键初始化:外部中断 + 上拉
voidKEY_Init(void){GPIO_InitTypeDef GPIO_InitStruct={0};/* PA1:外部中断模式,下降沿触发(按下→低电平),内部上拉(平时为高) */GPIO_InitStruct.Pin=GPIO_PIN_1;GPIO_InitStruct.Mode=GPIO_MODE_IT_FALLING;GPIO_InitStruct.Pull=GPIO_PULLUP;HAL_GPIO_Init(GPIOA,&GPIO_InitStruct);/* 使能 EXTI1 中断,优先级 0(最高) */HAL_NVIC_SetPriority(EXTI1_IRQn,0,0);HAL_NVIC_EnableIRQ(EXTI1_IRQn);}2.3 中断服务函数与回调
HAL 库的中断处理分三层:中断服务函数 → HAL 通用处理函数 → 用户回调:
/* ① 中断服务函数(框架自动调用) */voidEXTI1_IRQHandler(void){HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_1);}/* ② 用户回调:按键按下 → 翻转暂停标志 */voidHAL_GPIO_EXTI_Callback(uint16_tGPIO_Pin){if(GPIO_Pin==GPIO_PIN_1){g_pause=!g_pause;/* 暂停 ↔ 恢复切换 */}}按键按下 → 硬件进入 EXTI1 中断 →HAL_GPIO_EXTI_IRQHandler清中断标志并调用HAL_GPIO_EXTI_Callback→ 我们只在回调里翻转一个标志变量g_pause,不做复杂操作(中断里代码越短越好)。
2.4 踩坑 4:烧录后只亮红灯不流水、按键无反应
烧录后红灯常亮、完全不流水,按键也没反应。排查很久发现根因是main.c 里漏写了SysTick_Handler:
voidSysTick_Handler(void){HAL_IncTick();/* 每 1ms 中断一次,tick 计数 +1 */}HAL_Delay 依赖 SysTick 每 1ms 触发一次中断累计 tick,这个服务函数没实现,HAL_Delay 就永远卡死——红灯点亮后延时"无限长",看起来就是只亮红灯不流水。补上后流水立刻正常。
问 3:怎么做到"按下立即暂停、再按恢复"?
3.1 最初的设计(有缺陷)
一开始只在主循环开头检查暂停标志:
while(1){while(g_pause)HAL_Delay(10);/* 只有每轮开头检查 */红灯亮1s;绿灯亮1s;蓝灯亮1s;板载亮1s;}实测发现问题:流水过程中按按键,不会立即停,而是要等一整轮(4 秒)走完才停。原因:g_pause 只在每轮开头被检查,按键按下时主循环正卡在某一盏灯的 HAL_Delay(1000) 里,要等当前灯亮完 + 后面所有灯亮完,才回到检查点。
3.2 改进:把暂停检查做进延时函数
把每一盏灯的延时改成delay_pause(),延时期间每 10ms 检查一次按键标志,按下立即原地等待,当前灯保持点亮:
voiddelay_pause(uint32_tms){uint32_ti;for(i=0;i<ms;i+=10){if(g_pause){/* 暂停:保持当前灯亮着,等按键恢复 */while(g_pause){HAL_Delay(10);}return;/* 恢复后跳过本灯剩余时间,进入下一盏 */}HAL_Delay(10);}}主循环里把所有HAL_Delay(1000)换成delay_pause(1000)。这样:
- 按下→ 最多 10ms 内响应,当前灯停在点亮状态
- 再按→ 恢复,跳过当前灯剩余时间,进入下一盏灯
3.3 完整代码(main.c)
#include"stm32f1xx_hal.h"volatileuint8_tg_pause=0;/* 暂停标志:0=运行 1=暂停(中断中修改) *//* 系统时钟:HSE 8MHz × PLL9 = 72MHz */voidSystemClock_Config(void){RCC_OscInitTypeDef RCC_OscInitStruct={0};RCC_ClkInitTypeDef RCC_ClkInitStruct={0};RCC_OscInitStruct.OscillatorType=RCC_OSCILLATORTYPE_HSE;RCC_OscInitStruct.HSEState=RCC_HSE_ON;RCC_OscInitStruct.HSEPredivValue=RCC_HSE_PREDIV_DIV1;RCC_OscInitStruct.PLL.PLLState=RCC_PLL_ON;RCC_OscInitStruct.PLL.PLLSource=RCC_PLLSOURCE_HSE;RCC_OscInitStruct.PLL.PLLMUL=RCC_PLL_MUL9;HAL_RCC_OscConfig(&RCC_OscInitStruct);RCC_ClkInitStruct.ClockType=RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;RCC_ClkInitStruct.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;RCC_ClkInitStruct.AHBCLKDivider=RCC_SYSCLK_DIV1;RCC_ClkInitStruct.APB1CLKDivider=RCC_HCLK_DIV2;RCC_ClkInitStruct.APB2CLKDivider=RCC_HCLK_DIV1;HAL_RCC_ClockConfig(&RCC_ClkInitStruct,FLASH_LATENCY_2);}/* LED 初始化:PA0红 PB0绿 PC14蓝 PC13板载 */voidLED_Init(void){GPIO_InitTypeDef GPIO_InitStruct={0};__HAL_RCC_GPIOA_CLK_ENABLE();__HAL_RCC_GPIOB_CLK_ENABLE();__HAL_RCC_GPIOC_CLK_ENABLE();GPIO_InitStruct.Pin=GPIO_PIN_0;GPIO_InitStruct.Mode=GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Speed=GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOA,&GPIO_InitStruct);/* PA0 红灯 */GPIO_InitStruct.Pin=GPIO_PIN_0;HAL_GPIO_Init(GPIOB,&GPIO_InitStruct);/* PB0 绿灯 */GPIO_InitStruct.Pin=GPIO_PIN_14;GPIO_InitStruct.Speed=GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOC,&GPIO_InitStruct);/* PC14 蓝灯(低速引脚) */GPIO_InitStruct.Pin=GPIO_PIN_13;HAL_GPIO_Init(GPIOC,&GPIO_InitStruct);/* PC13 板载灯 */HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_14,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET);}/* 按键初始化:PA1 下降沿中断 */voidKEY_Init(void){GPIO_InitTypeDef GPIO_InitStruct={0};GPIO_InitStruct.Pin=GPIO_PIN_1;GPIO_InitStruct.Mode=GPIO_MODE_IT_FALLING;GPIO_InitStruct.Pull=GPIO_PULLUP;HAL_GPIO_Init(GPIOA,&GPIO_InitStruct);HAL_NVIC_SetPriority(EXTI1_IRQn,0,0);HAL_NVIC_EnableIRQ(EXTI1_IRQn);}/* SysTick 中断服务函数(HAL_Delay 的时基,必须实现) */voidSysTick_Handler(void){HAL_IncTick();}voidEXTI1_IRQHandler(void){HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_1);}/* 中断回调:按键按下 → 翻转暂停标志 */voidHAL_GPIO_EXTI_Callback(uint16_tGPIO_Pin){if(GPIO_Pin==GPIO_PIN_1){g_pause=!g_pause;}}/* 带暂停检测的延时:每 10ms 检查按键,按下立即暂停 */voiddelay_pause(uint32_tms){uint32_ti;for(i=0;i<ms;i+=10){if(g_pause){while(g_pause)HAL_Delay(10);/* 灯保持亮,等恢复 */return;}HAL_Delay(10);}}intmain(void){HAL_Init();/* SysTick 1ms 时基、NVIC 分组 */SystemClock_Config();/* 72MHz */LED_Init();KEY_Init();while(1){HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_SET);/* 红灯 */delay_pause(1000);HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_SET);/* 绿灯 */delay_pause(1000);HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_14,GPIO_PIN_SET);/* 蓝灯 */delay_pause(1000);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_14,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET);/* 板载灯 */delay_pause(1000);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET);}}运行效果
流水灯正常轮流点亮,按下按键立即暂停(当前灯保持点亮),再按一次恢复继续流水:
总结
这个实验让我把 STM32 的开发方式从寄存器、标准外设库推进到了 HAL 库,而且是在没有 CubeMX 的情况下纯手动搭建工程,把 HAL 库"设备层 + 驱动层 + 配置文件"的结构彻底看了一遍,中间解决了三个编译链接坑(legacy 头文件、ex 扩展头文件、HAL_xxx_MODULE_ENABLED 模块开关)和两个运行逻辑坑(SysTick_Handler 漏写导致 HAL_Delay 卡死、暂停检查位置不对导致不能立即暂停)。按键中断的思路也很有价值:中断服务函数里只翻转一个标志,主循环轮询标志,两者通过共享变量协作,避免在中断里做耗时操作。同时 HAL_Delay 基于 SysTick 定时器,与实验 2 的软件循环延时实测 0.88s 对比,体现了"硬件定时器比软件延时精确得多"这一嵌入式开发的重要原则。