新闻详情

新闻详情

首页 / 资讯中心 / 详情

STM32F103 流水灯三种写法对比 + HAL 库按键中断控制(暂停/恢复)

发布时间:2026/9/30 11:28:40来源:尧图网络
STM32F103 流水灯三种写法对比 + HAL 库按键中断控制(暂停/恢复)
STM32F103 流水灯三种写法对比 HAL 库按键中断控制暂停/恢复写在前面这次要交的是用 HAL 库 按键中断控制流水灯。但在动手之前我把同一个流水灯用三种方式各写了一遍 —— 直接操作寄存器、调用标准外设库、调用 HAL 库想搞清楚GPIO_Init()、HAL_GPIO_Init()这些函数背后到底替我们干了什么。三种写法代码看着完全不一样最后写进去的却是同一批寄存器。所有代码都在 Keil MDK 5.24 上编译下载实测过0 Error(s), 0 Warning(s)。目录这次要做什么三种写法到底差在哪① 直接操作寄存器——自己算地址、自己拼位域② 调用标准外设库——地址和位域都由库函数代劳③ 调用 HAL 库 —— 写法类似函数名和宏换了一套三种写法的对应关系该怎么选为什么要把延时改成定时器中断TIM3 的 1ms 是怎么算出来的按键中断是怎么串起来的没有按键怎么办消抖为什么要两级完整代码LED 初始化与显示TIM3 初始化主循环按键初始化中断服务函数与消抖实测结果编译结果运行现象参考资料摘要本文用 STM32F103C8T6Blue Pill实现三只 LED 流水灯每只亮 1 秒并对比了直接操作寄存器、标准外设库、HAL 库三种写法的异同——代码看似不同最终写进去的却是同一批寄存器。随后把Delay()死等改为TIM3 每 1ms 中断计数让主循环不再阻塞再用EXTI 外部中断接一个按键板上没有按键用杜邦线代替实现按一下暂停、再按一下继续。全文代码均在 Keil MDK 5.24 上编译下载实测通过0 Error(s), 0 Warning(s)并附完整代码、编译结果与运行现象。这次要做什么用 STM32F103C8T6 最小系统板Blue Pill 面包板 3 只 LED 做流水灯每只亮 1 秒三只灯接 PA0 / PA1 / PA2低电平点亮同一个功能用寄存器 / 标准外设库 / HAL 库三种写法各实现一遍对比差别把Delay()死等换成TIM3 每 1ms 中断计数让主循环不阻塞用EXTI 外部中断接一个按键板上没有按键用杜邦线代替实现按一下暂停、再按一下继续。## 用到的硬件和软件类别内容开发环境Keil MDK 5.24 ARM Compiler 5.06 update 5库ST 官方 STM32F1xx HAL 库对照用标准外设库 STM32F10x_StdPeriph_Lib V3.5硬件STM32F103C8T6 最小系统板Blue Pill、面包板、3 只 LED、330Ω 电阻 3 个、杜邦线芯片参数Cortex-M372 MHz64 KB Flash20 KB SRAM面包板接线和 Keil 编译界面可以直接对着板子拍两张照片贴在这一节下面图文更直观。三种写法到底差在哪同一个“点亮 PA0”的动作三种写法如下。① 直接操作寄存器——自己算地址、自己拼位域*(unsignedint*)0x40021018|(12);// 开 GPIOA 时钟RCC_APB2ENR*(unsignedint*)0x400108000xFFFFFFF0;// 清 PA0 的配置位GPIOA_CRL*(unsignedint*)0x40010800|0x00000002;// PA0 通用推挽输出 2MHz*(unsignedint*)0x4001080C(10);// PA0 输出高GPIOA_ODR这些地址是从哪来的先把丑话说在前面0x4001080C这种数没人能背下来都是查手册两张表拼出来的。第一张表参考手册《STM32F10xxx RM0008》的外围基地址表 —— 查到GPIOA 0x40010800第二张表同一本手册里 GPIO 的寄存器偏移表 —— 查到CRL 0x00、ODR 0x0C。基地址 偏移才是最终的绝对地址。本项目用到的都在下面这张表里寄存器绝对地址位 / 位域写入值含义RCC_APB2ENR0x40021018bit2IOPAEN1使能 GPIOA 时钟RCC_APB2ENR0x40021018bit0AFIOEN1使能 AFIO 时钟用 EXTI 必须开GPIOA_CRL0x40010800bit3:00x2PA0 通用推挽输出 2MHzGPIOA_CRL0x40010800bit7:40x2PA1 通用推挽输出 2MHzGPIOA_CRL0x40010800bit11:80x2PA2 通用推挽输出 2MHzGPIOA_ODR0x4001080Cbit0 / bit1 / bit21PA0 / PA1 / PA2 输出高灯灭GPIOA_ODR0x4001080Cbit0 / bit1 / bit20PA0 / PA1 / PA2 输出低灯亮GPIOA_BSRR0x40010810bit0~bit21对应引脚输出高原子操作GPIOA_BRR0x40010814bit0~bit21对应引脚输出低原子操作AFIO_EXTICR10x40010008bit7:40b0001EXTI1 线映射到PB10000A、0001BEXTI_FTSR0x4001040Cbit11EXTI1 下降沿触发EXTI_IMR0x40010400bit11打开 EXTI1 中断屏蔽EXTI_PR0x40010414bit11清 EXTI1 挂起标志写 1 清除一个省事的办法每个 GPIO 端口有 7 个寄存器偏移量是固定的——CRL0x00、CRH0x04、IDR0x08、ODR0x0C、BSRR0x10、BRR0x14、LCKR0x18。所以只要记住端口基地址GPIOA 0x40010800、GPIOB 0x40010C00、GPIOC 0x40011000加个偏移就够了不用每个寄存器都去翻手册。为什么别人博客里写的是GPIOA-CRL那其实是查表查得更彻底的写法。ST 官方头文件里已经把这些地址定义好了连加法都替你写好了#defineGPIOA_BASE(APB2PERIPH_BASE0x00000800UL)// → 0x40010800#defineGPIOA((GPIO_TypeDef*)GPIOA_BASE)typedefstruct{__IOuint32_tCRL;// 偏移 0x00__IOuint32_tCRH;// 偏移 0x04__IOuint32_tIDR;// 偏移 0x08__IOuint32_tODR;// 偏移 0x0C...}GPIO_TypeDef;于是GPIOA-ODR就等于0x40010800 0x0C 0x4001080C和手写硬地址完全等价只是让编译器替你做那个加法可读性还更好。本节用硬地址是为了把两张表 一次加法摆到明面上看实际写代码推荐用GPIOA-CRL这种指针写法。② 调用标准外设库——地址和位域都由库函数代劳RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);// 开 GPIOA 时钟GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_ModeGPIO_Mode_Out_PP;// 推挽输出GPIO_InitStructure.GPIO_PinGPIO_Pin_0;GPIO_InitStructure.GPIO_SpeedGPIO_Speed_50MHz;GPIO_Init(GPIOA,GPIO_InitStructure);GPIO_SetBits(GPIOA,GPIO_Pin_0);// PA0 输出高③ 调用 HAL 库 —— 写法类似函数名和宏换了一套__HAL_RCC_GPIOA_CLK_ENABLE();// 开 GPIOA 时钟GPIO_InitTypeDef GPIO_InitStructure{0};GPIO_InitStructure.ModeGPIO_MODE_OUTPUT_PP;// 推挽输出GPIO_InitStructure.PinGPIO_PIN_0;GPIO_InitStructure.PullGPIO_NOPULL;GPIO_InitStructure.SpeedGPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOA,GPIO_InitStructure);HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_SET);// PA0 输出高三种写法的对应关系代码完全不同但最终写进去的是同一批寄存器要做的事直接操作寄存器标准外设库HAL 库开 GPIOA 时钟*(uint32_t*)0x40021018 | (12)RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)__HAL_RCC_GPIOA_CLK_ENABLE()配成推挽输出GPIOA_CRL 0x...2GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PPGPIO_InitStructure.Mode GPIO_MODE_OUTPUT_PP输出高电平GPIOA_ODR | (10)GPIO_SetBits(GPIOA, GPIO_Pin_0)HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET)输出低电平GPIOA_ODR ~(10)GPIO_ResetBits(GPIOA, GPIO_Pin_0)HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET)结论HAL 库并没有让“底层”消失它只是把寄存器操作封装成了函数。寄存器该配的还是要配只是不用自己算地址、不用自己记位域了。不信可以翻HAL_GPIO_WritePin()的源码它内部就是在写 BSRR 寄存器voidHAL_GPIO_WritePin(GPIO_TypeDef*GPIOx,uint16_tGPIO_Pin,GPIO_PinState PinState){if(PinState!GPIO_PIN_RESET)GPIOx-BSRRGPIO_Pin;// 置位 → 输出高elseGPIOx-BSRR(uint32_t)GPIO_Pin16u;// 写高 16 位 → 输出低}该怎么选方式优点缺点寄存器最贴近硬件代码最小要查手册算地址可读性差、易出错标准外设库函数名直观网上资料特别多已停止维护ST 不再更新HAL 库ST 主推、跨系列移植方便、能配 CubeMX 生成代码层次多、体积稍大我这次选 HAL 库因为它是 ST 现在主推的方式后面学 CubeMX 会直接生成 HAL 代码。为什么要把延时改成定时器中断标准外设库那个版本是用Delay_ms(1000)让 CPU 空转着数 1 秒这 1 秒里 CPU 什么都干不了按键按下去也没人理。所以必须换思路TIM3 每 1ms 中断一次 → 在中断里给计数器 1 → 主循环数够 1000 次就换下一只灯Delay_ms()是阻塞等待定时器中断是到点了通知我两种完全不同的思路。换成中断之后主循环不再阻塞按键中断随时都能被响应 —— 这是能加上按键控制的前提。TIM3 的 1ms 是怎么算出来的STM32F103 系统时钟 72 MHz。TIM3 挂在 APB1 上APB1 分频系数是 2所以 APB1 时钟是 36 MHz但只要 APB1 分频系数不为 1定时器时钟就要再乘 2这条规则我第一次看手册时直接漏了所以 TIM3 的实际时钟还是72 MHz。参数计算结果预分频 PSC72 MHz ÷ 72计数频率 1 MHz1 个计数 1 μs自动重装 ARR1 MHz 数 1000 次1000 μs 1 ms代码里 PSC 是按SystemCoreClock实时算的没写死 72这样即使外部晶振没起振、系统退回内部 8 MHz1 ms 也不会跑偏。按键中断是怎么串起来的PB1 碰到 GND电平由高变低产生下降沿 ↓ 硬件 EXTI1 产生中断请求 ↓ NVIC 放行 EXTI1_IRQHandler() ← 中断服务函数 ↓ HAL_GPIO_EXTI_IRQHandler() ← HAL 库清中断标志 ↓ HAL_GPIO_EXTI_Callback() ← 消抖 → 翻转暂停标志 ↓ 主循环读到标志变化 → 暂停 / 继续换灯三个容易搞错的点EXTI 线号 引脚号。PB1 走 EXTI1、PC13 走 EXTI13跟端口没关系但同一条线号同一时间只能映射到一个端口用了 PB1 就不能再用 PA1 做中断。要用 EXTI 必须先开 AFIO 时钟。因为AFIO_EXTICR这个寄存器负责把 EXTI 线“接线”到具体端口。这点 HAL 已经替我们做了 —— 只要把 Mode 配成GPIO_MODE_IT_FALLINGHAL_GPIO_Init()内部就会开 AFIO 时钟、写AFIO_EXTICR、开EXTI_FTSR和EXTI_IMR我们只需要再打开 NVIC。中断服务函数名必须和启动文件向量表里的写法完全一致写错一个字母中断一来就会跑到默认的死循环里而且不报错非常难查。没有按键怎么办最小系统板上只有 RESET 和 BOOT0 两个按键这两个都不能用RESET直接接芯片 NRST 引脚按下整个芯片复位程序从main()重跑BOOT0只在复位瞬间被采样决定启动模式运行中按它没有反应。所以直接用一根杜邦线代替按键杜邦线一头 → PB1插在排针上不动 杜邦线另一头 → 碰一下 GND 再拿开相当于按了一下不需要外接上拉电阻也不需要消抖电容 —— 程序把 PB1 配成内部上拉输入不碰时被内部电阻拉到高电平碰到 GND 变成低电平这个由高变低的瞬间就是下降沿。操作要点碰一下就拿开别一直按着。程序是靠下降沿触发的一直按着只有最开始那一次下降沿只会暂停一次拿开再碰才有第二次。消抖为什么要两级杜邦线插拔和机械按键一样触点接触的瞬间会连续弹跳一次触碰可能产生一串下降沿。如果每个都处理就会出现碰一下就暂停、又立刻恢复。所以做了两级级别参数作用① 电平确认20ms检测到下降沿后等 20ms 再读一次 PB1仍是低电平才认② 间隔限制200ms距上次有效触发不足 200ms 的边沿直接丢掉完整代码LED 初始化与显示staticvoidLed_GPIO_Init(void){GPIO_InitTypeDef GPIO_InitStructure{0};__HAL_RCC_GPIOA_CLK_ENABLE();// 不开时钟后面全白配GPIO_InitStructure.PinGPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2;GPIO_InitStructure.ModeGPIO_MODE_OUTPUT_PP;GPIO_InitStructure.PullGPIO_NOPULL;GPIO_InitStructure.SpeedGPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(LED_GPIO_PORT,GPIO_InitStructure);HAL_GPIO_WritePin(LED_GPIO_PORT,GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2,LED_OFF_STATE);}staticvoidLed_Show(uint8_tindex)// 点亮第 index 只其余熄灭{uint8_ti;for(i0U;iLED_NUM;i){HAL_GPIO_WritePin(LED_GPIO_PORT,LED_PIN_TABLE[i],LED_OFF_STATE);}if(indexLED_NUM){HAL_GPIO_WritePin(LED_GPIO_PORT,LED_PIN_TABLE[index],LED_ON_STATE);}}TIM3 初始化staticvoidLed_Timer_Init(void){__HAL_RCC_TIM3_CLK_ENABLE();htim3.InstanceTIM3;htim3.Init.Prescaler(SystemCoreClock/1000000U)-1U;// 72-1 → 1MHzhtim3.Init.CounterModeTIM_COUNTERMODE_UP;htim3.Init.Period1000U-1U;// 1000 次 1mshtim3.Init.ClockDivisionTIM_CLOCKDIVISION_DIV1;htim3.Init.AutoReloadPreloadTIM_AUTORELOAD_PRELOAD_DISABLE;HAL_TIM_Base_Init(htim3);HAL_NVIC_SetPriority(TIM3_IRQn,1,0);// 分组 2取值 0~3HAL_NVIC_EnableIRQ(TIM3_IRQn);HAL_TIM_Base_Start_IT(htim3);}主循环while(1){if(Led_IsPaused()0U){if(g_led_elapsed_msLED_INTERVAL_MS){g_led_elapsed_ms0U;g_led_index;if(g_led_indexLED_NUM){g_led_index0U;}Led_Show(g_led_index);}}else{g_led_elapsed_ms0U;// 暂停时清零恢复后重新亮满 1 秒}}按键初始化voidBsp_Key_Init(void){GPIO_InitTypeDef GPIO_InitStructure{0};__HAL_RCC_GPIOB_CLK_ENABLE();GPIO_InitStructure.PinBSP_KEY_GPIO_PIN;GPIO_InitStructure.ModeGPIO_MODE_IT_FALLING;// 下降沿触发中断GPIO_InitStructure.PullGPIO_PULLUP;// 内部上拉HAL_GPIO_Init(BSP_KEY_GPIO_PORT,GPIO_InitStructure);HAL_NVIC_SetPriority(BSP_KEY_IRQn,1,1);HAL_NVIC_EnableIRQ(BSP_KEY_IRQn);}中断服务函数与消抖/* 中断入口 */voidTIM3_IRQHandler(void){HAL_TIM_IRQHandler(htim3);}voidBSP_KEY_IRQHandler(void){HAL_GPIO_EXTI_IRQHandler(BSP_KEY_GPIO_PIN);}/* TIM3 每 1 ms 进一次流水计时 按键消抖 */voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef*htim){if(htim-InstanceTIM3){g_led_elapsed_ms;Bsp_Key_Tick();}}/* 按键中断回调只记一笔不做延时 */voidHAL_GPIO_EXTI_Callback(uint16_tGPIO_Pin){if(GPIO_Pin!BSP_KEY_GPIO_PIN)return;if((HAL_GetTick()-key_last_ms)BSP_KEY_MIN_GAP_MS)return;// 防止弹跳连发key_pending1U;key_ticks0U;}/* 消抖由 TIM3 的 1 ms 中断调用 */voidBsp_Key_Tick(void){if(key_pending0U)return;if(key_ticksBSP_KEY_DEBOUNCE_MS)return;key_pending0U;if(HAL_GPIO_ReadPin(BSP_KEY_GPIO_PORT,BSP_KEY_GPIO_PIN)GPIO_PIN_RESET){key_last_msHAL_GetTick();g_key_paused(uint8_t)(!g_key_paused);// 暂停 ⇄ 恢复}}注意消抖不能直接写在按键中断里用HAL_Delay()等待否则会死锁——原因见下面“踩过的坑”。实测结果编译结果三种写法的代码量对比同样三只灯、同样 1 秒间隔版本CodeRO-dataRW-dataZI-data编译结果标准外设库版1160260016320 Error 0 WarningHAL 库版 按键中断25282923210960 Error 0 WarningHAL 库版的完整输出*** Using Compiler V5.06 update 5 (build 528), folder: Keil 安装目录\ARM\ARMCC\Bin Build target Target 1 Program Size: Code2528 RO-data292 RW-data32 ZI-data1096 .\Objects\Project.axf - 0 Error(s), 0 Warning(s).两个版本现象完全一样但 HAL 版多出了定时器配置、中断处理和按键逻辑所以 Code 从 1160 字节涨到 2528 字节。即使这样也只占 64 KB Flash 的 4%资源很宽裕。运行现象操作现象上电复位三只 LED 依次点亮每只亮 1 秒3 秒一循环杜邦线碰一下 GND 再拿开流水灯暂停当前那只灯保持亮着不动再碰一下再拿开流水灯恢复并且当前这只灯重新亮满 1 秒才换下一只用标准库外设控制的流水灯用 HAL 库 按键中断方式控制流水灯运行的暂停、恢复继续参考资料STM32C8T6 寄存器方式与标准库方式流水灯STM32 寄存器/标准库流水灯CSDNST 官方《STM32F10xxx 参考手册 RM0008》ST 官方《STM32F1xx HAL 库用户手册 UM1850》江协科技《STM32 入门教程》—— 工程模板与 HAL 库开发流程
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

DeepSeek私有化部署实战:从硬件选型到企业应用落地 2026/9/30 12:08:38

DeepSeek私有化部署实战:从硬件选型到企业应用落地

简介:这份《程序员实战宝典:DeepSeek中小型企业私有化部署及跨行业业务应用详解》PDF文档,面向中小企业技术负责人、运维工程师及AI应用开发者。文档围绕DeepSeek模型的企业级落地路径,系统梳理了从需求评估、硬件与软件环境准备&…

阅读更多 →
OJ刷题三大经典坑:EOF多组输入、整数溢出与素数判定优化 2026/9/30 12:08:38

OJ刷题三大经典坑:EOF多组输入、整数溢出与素数判定优化

前几天在OJ上刷题,卡在了13题到15题这一组。说实话,这三道题单看都不算难,但它们背后牵扯出的东西——EOF多组输入、整数溢出、枚举范围优化、OJ判题的黑盒机制——几乎把新手会踩的坑全踩了一遍。我把自己完整的解题过程、踩坑记录和最终AC代…

阅读更多 →
IPD落地推不动?产品线模式才是组织底座与投资决策关键 2026/9/30 12:08:37

IPD落地推不动?产品线模式才是组织底座与投资决策关键

最近连续被好几家正在推进IPD的企业问到同一个问题:流程文件、评审模板、项目立项标准全都搭起来了,管理层也很重视,可产品开发项目还是推不动,跨部门沟通依旧靠刷脸,该打的仗打不赢。聊到后面,基本都会落在…

阅读更多 →
新浪微博架构演进:高并发微服务与Feed流设计深度解析 2026/9/30 12:08:37

新浪微博架构演进:高并发微服务与Feed流设计深度解析

做后端这么久,我一直觉得新浪微博架构是最值得反复研究的系统之一。它不是一个教科书式的整洁架构,而是一部在高并发、高热点、海量数据下被逼出来的演化史。从早期一台数据库撑起千万用户的单体应用,到后来颇具代表性的微服务、推拉结合Feed…

阅读更多 →
IPD落地为何必须配套产品线模式:组织形态决定流程成败 2026/9/30 12:08:36

IPD落地为何必须配套产品线模式:组织形态决定流程成败

IPD这三年的热度一直没降过,但真正让我觉得值得聊的,不是流程文件写了多少,而是组织形态怎么跟着变。很多企业拿着华为的IPD模板改了一轮,评审会开了、文档模板上了、重量级团队也挂了牌,到头来研发还是围着功能部门转…

阅读更多 →
Model-Optimizer:大模型推理效能治理方法论 2026/9/30 12:08:29

Model-Optimizer:大模型推理效能治理方法论

1. “Model-Optimizer”不是工具名,而是工程目标的精准表达 很多人第一次看到“Model-Optimizer”这个标题,下意识会以为是个开源项目、某个GitHub仓库名,或者某家公司的商业化产品。我刚接触这个词时也这么想——直到连续三天在NVIDIA开发者…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉