TIP本博客基于江科大的编程技巧系列视频
原文链接:[编程技巧] 第1期 定时器实现非阻塞式程序 按键控制LED闪烁模式
开发环境概述
IMPORTANT测试环境说明:
操作系统:Windows 10
MCU 型号:STM32F407VGT6
开发板:立创天空星Pro
开发工具:STM32CubeMX、CLion
调试接口:ST-Link v2
一、在STM32CubeMX创建一个新的工程
1、根据自己的开发板MCU的型号在CubeMX创建一个初始工程,以我的STM32F407VGT6为例,选择好对应型号创建工程。

2、进行工程的基础设置
-
在RCC设置中选择外部高速时钟
-
在SYS设置中选择Debug模式为SW(Serial Wire)
-
查询手册查看自己开发板的按钮GPIO和LED GPIO来配置GPIO模式,注意自己的LED是共阴还是共阳。

WARNING
此处的PB2引脚的默认电平我设置错了,天空星的LED是高电平触发,所以IO口默认设置为低电平,不然会影响到后续的程序!!!
-
配置定时器的全局中断
-
配置定时器预分频值和自动重装载值,要看自己MCU的时钟树进行配置,例如我的F407VGT6的基本定时器TIM6是在APB1总线上的,而默认到最高频率168MHz的APB1定时器频率为84MHz,所以我的定时器预分频值和自动重装载值就要根据84MHz来进行计算1秒定时,要怎么设定预分频值和自动重装载值就由你自己决定,我这里选择的是84-1和1000-1这两个参数,即为1ms扫描一次,只要这两个数是三个0就行了,8400-1和10-1也行。


TIP有关HAL库的定时器相关知识可以参考keysking的stm32系列教程:第16集 动画告诉你, STM32的定时器到底怎么回事学习有关基本定时器的相关功能
3、配置完成后,创建工程,选择Cmake,如果你使用的是Keil就选择MDK,创建工程的配置就不过多赘述了,记得分开.c和.h文件。
二、软件实现
-
原版江科大功能实现代码在江科大官方网站下载,这里就不做赘述,本篇博客将对江科大的代码使用HAL库进行优化编写,实现思路与源代码请前往江科大的官网和blibili视频查看,本文只做优化思路的讲解。
-
原版代码在按键和LED的函数中将外设参数和引脚写死了,所以在移植到其他MCU上的时候不太友好,需要修改的地方比较多,可维护性比较差,所以我在移植的时候重写了他的函数,修改成了查表法(Lookup Table),查表法是一种通过预定义的数据结构(如数组、结构体)存储配置信息,运行时通过索引或键值快速检索数据的编程技巧。其核心目的是:
-
减少硬编码,提升可维护性
-
统一管理配置,便于扩展
-
牺牲少量ROM空间换取执行效率或代码简洁性
如今的MCU性能越来越强,多开销的ROM空间几乎可以忽略不计,而两者的执行效率基本上大差不差。
TIP
天空星Pro的板上资源有限,移植到天空星只测试了他的单板按钮和LED,有机会我画个底板来测试天空星的多按键和多LED控制,我已经在其他板上验证过了两个LED和两个KEY。
-
江科大状态机按键检测优化设计逻辑
-
设计目标:
优化后的按键驱动代码主要解决以下问题:
-
硬件耦合度高:原代码直接操作
GPIOB和固定引脚,难以移植到其他硬件平台。 -
扩展性差:新增按键需修改多处逻辑,违反开闭原则(OCP)。
-
可读性低:使用魔术数字(如
1、2)表示按键,代码含义不直观。
-
-
核心设计思想
-
查表法(Lookup Table):通过结构体数组统一管理按键硬件配置。
-
模块化设计:分离硬件配置与业务逻辑,提高可维护性。
-
-
具体代码
在工程中新建一个key.c和key.h文件,在头文件中定义一个枚举类型用于表示KEY的编号,还有结构体,例如:
// 枚举:KEY编号typedef enum{KEY1 = 0,KEY2,// 这里添加你的key编号,示例:KEY3....KEY_COUNT // 总KEY数量,用于数组} KEY_Type;// 按键配置结构体typedef struct{GPIO_TypeDef *port; // GPIO端口(GPIOx)uint16_t pin; // 引脚号(如GPIO_PIN_x)} Key_Config;然后在key.c函数中建立一个表用于查询,例如:
static Key_Config key_config[KEY_COUNT] = {[KEY1] = {KEY_GPIO_Port, KEY_Pin},//在这里继续进行添加你的映射表,例如[KEYx] = {KEYx_GPIO_Port, KEYx_Pin},前提是你进行了lable的设置,否则就使用对应的端口和pin。};然后剩下的就是状态机的按键检测逻辑了,详细思路请观看江科大的视频,这里贴出我修改后的代码:
// 存储最近触发的按键编号,初始化为无效值(KEY_COUNT)// 注意:此变量会在按键释放时被更新,通过KeyGetNum()读取后自动清除static KEY_Type key_num = KEY_COUNT;/*** @brief 检测当前被按下的按键* @return 按下的按键编号(KEY1等),若无按键按下返回KEY_COUNT* @note 高电平有效(GPIO_PIN_SET表示按下)*/static KEY_Type Key_GetState(void){// 遍历所有已配置的按键for (KEY_Type i = KEY1; i < KEY_COUNT; i++){// 检测按键引脚是否为高电平(按下状态)if (HAL_GPIO_ReadPin(key_config[i].port, key_config[i].pin) == GPIO_PIN_SET){return i; // 返回按下的按键编号}}return KEY_COUNT; // 无按键按下}/*** @brief 获取被触发的按键编号(单次触发)* @return 触发按键的编号,若无触发返回KEY_COUNT* @note 调用后会清除内部存储的键值,避免重复触发*/KEY_Type KeyGetNum(void){// 检查是否有有效按键触发if (key_num < KEY_COUNT){KEY_Type temp = key_num; // 临时保存键值key_num = KEY_COUNT; // 清除键值(单次触发)return temp; // 返回按键编号}return KEY_COUNT; // 无按键触发}/*** @brief 按键状态检测函数(需定时器周期性调用)* @note 实现功能:* 1. 按键消抖(通过KEY_DEBOUNCE_TICKS控制)* 2. 释放事件检测(从按下到释放的跳变)*/void Key_Tick(void){static uint8_t counter; // 消抖计数器static KEY_Type CurrKeyState = KEY_COUNT; // 当前按键状态static KEY_Type PrevKeyState = KEY_COUNT; // 上一次按键状态counter++; // 计数器递增(每次调用+1)// 达到消抖时间阈值时检测按键状态if (counter >= KEY_DEBOUNCE_TICKS){counter = 0; // 重置计数器// 更新状态记录PrevKeyState = CurrKeyState; // 保存旧状态CurrKeyState = Key_GetState(); // 获取新状态// 检测按键释放事件(之前按下,现在释放)if (CurrKeyState == KEY_COUNT && PrevKeyState != KEY_COUNT){key_num = PrevKeyState; // 记录被释放的按键编号}}}
然后就是LED的实现,其实就是一样的优化方向,都是用查询表的方式实现,但是江科大原版的状态机太多if了,有点长,所以我这里状态机改用了switch,这里贴出我的LED_Tick代码:
/** * @brief LED状态机(需周期性调用,如每1ms调用一次) * @note 实现功能: * 1. 处理常亮/关闭模式 * 2. 管理多种闪烁模式的定时切换 */void LED_Tick(void){ for (int i = 0; i < LED_COUNT; ++i) // 遍历所有LED { LED_Mode mode = leds[i].mode; // 获取当前模式
switch (mode) { case LED_ON: // 常亮模式 led_on(i); break; case LED_OFF: // 关闭模式 led_off(i); break;
// 闪烁模式处理 case LED_BLINK_SLOW: case LED_BLINK_FAST: case LED_BLINK_SHORT: { Mode_Config cfg = modes[mode]; // 获取当前模式的配置参数 leds[i].counter = (leds[i].counter + 1) % cfg.period; // 循环计数
// 根据计数值切换亮灭状态 if (leds[i].counter < cfg.threshold) led_on(i); // 在阈值时间内保持亮 else led_off(i); // 超过阈值后灭 break; } default: // 未知模式默认关闭 led_off(i); break; } }}部分信息可能已经过时