2069 字
10 分钟
非阻塞式程序设计
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-11000-1这两个参数,即为1ms扫描一次,只要这两个数是三个0就行了8400-110-1也行。

TIP

有关HAL库的定时器相关知识可以参考keysking的stm32系列教程:第16集 动画告诉你, STM32的定时器到底怎么回事学习有关基本定时器的相关功能

3、配置完成后,创建工程,选择Cmake,如果你使用的是Keil就选择MDK,创建工程的配置就不过多赘述了,记得分开.c和.h文件。


二、软件实现#

  1. 原版江科大功能实现代码在江科大官方网站下载,这里就不做赘述,本篇博客将对江科大的代码使用HAL库进行优化编写,实现思路与源代码请前往江科大的官网和blibili视频查看,本文只做优化思路的讲解

  2. 原版代码在按键和LED的函数中将外设参数和引脚写死了,所以在移植到其他MCU上的时候不太友好,需要修改的地方比较多,可维护性比较差,所以我在移植的时候重写了他的函数,修改成了查表法(Lookup Table),查表法是一种通过预定义的数据结构(如数组、结构体)存储配置信息,运行时通过索引或键值快速检索数据的编程技巧。其核心目的是:

    • 减少硬编码,提升可维护性

    • 统一管理配置,便于扩展

    • 牺牲少量ROM空间换取执行效率或代码简洁性

    如今的MCU性能越来越强,多开销的ROM空间几乎可以忽略不计,而两者的执行效率基本上大差不差。

    TIP

    天空星Pro的板上资源有限,移植到天空星只测试了他的单板按钮和LED,有机会我画个底板来测试天空星的多按键和多LED控制,我已经在其他板上验证过了两个LED和两个KEY。

江科大状态机按键检测优化设计逻辑#

  1. 设计目标:

    优化后的按键驱动代码主要解决以下问题:

    • 硬件耦合度高:原代码直接操作 GPIOB 和固定引脚,难以移植到其他硬件平台。

    • 扩展性差:新增按键需修改多处逻辑,违反开闭原则(OCP)。

    • 可读性低:使用魔术数字(如 12)表示按键,代码含义不直观。

  2. 核心设计思想

    • 查表法(Lookup Table):通过结构体数组统一管理按键硬件配置。

    • 模块化设计:分离硬件配置与业务逻辑,提高可维护性。

  3. 具体代码

    在工程中新建一个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;
}
}
}

剩余的优化代码可以看我的GitHub,我全部打包放在我的个人仓库,还有移植到天空星的HAL库例程

非阻塞式程序设计
https://fuwari.vercel.app/posts/stm32-timerkey-led/
作者
L3AAF
发布于
2025-07-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时