欢迎光临
我们一直在努力

【飞控系列教程·④】GPIO输入模式与按键控制:消抖、流水灯、软件PWM呼吸灯详解

上一章我们学习了GPIO的输出功能,成功控制了LED的亮灭。本章将进一步学习GPIO的输入功能,通过读取按键状态来实现人机交互,并综合运用输入和输出功能实现多种LED灯光效果。

4.1 GPIO输入模式

4.1.1 数字输入的基本概念

当GPIO配置为输入模式时,芯片内部的高阻抗缓冲器将引脚上的电平信号传递到输入数据寄存器。软件通过读取该寄存器即可获知引脚当前的电平状态。

数字输入的关键在于引脚上的电平必须是确定的——要么是接近VCC的高电平(逻辑"1"),要么是接近GND的低电平(逻辑"0")。如果引脚处于悬空状态(既没有接VCC也没有接GND),引脚电平会受到周围电磁干扰而随机跳变,导致读取结果不稳定。

4.1.2 上拉输入与下拉输入

为了解决悬空引脚的问题,CH582F的GPIO内部集成了可编程的上拉电阻和下拉电阻:

上拉输入(Pull-Up Input)

在引脚内部通过一个弱上拉电阻(约50kΩ~100kΩ)连接到VCC。无外部驱动时,引脚被上拉电阻拉至高电平。当外部电路(如按键接地)将引脚拉低时,引脚变为低电平。

适用场景:按键一端接地,按下时引脚被拉低(低电平有效)。

下拉输入(Pull-Down Input)

在引脚内部通过一个弱下拉电阻连接到GND。无外部驱动时,引脚被下拉至低电平。当外部电路将引脚拉高时,引脚变为高电平。

适用场景:按键一端接VCC,按下时引脚被拉高(高电平有效)。

4.1.3

完整操作流程演示

  • 上电 → 全灭
  • 按 PA8 → 四灯常亮
  • 按 PA9 → 跑马灯滚动
  • 再按 PA9 → 停止滚动,回到四灯常亮
  • 任意状态按 PA8 → 全部熄灭,流水关闭
  • #include "CH58x_common.h"
    #include "LED.h"

    // 可配置参数宏
    #define KEY_DELAY_MS 20 // 按键消抖时间
    #define FLOW_INTERVAL 120 // 跑马灯切换间隔ms,改小速度变快

    // LED引脚定义
    #define RLED_R GPIO_Pin_11 //PA11
    #define RLED_L GPIO_Pin_15 //PB15
    #define GLED_R GPIO_Pin_4 //PB4
    #define GLED_L GPIO_Pin_14 //PA14

    // 按键引脚定义
    #define KEY1_PIN GPIO_Pin_8 //PA8 总开关按键
    #define KEY2_PIN GPIO_Pin_9 //PA9 跑马灯切换按键

    // 全局状态变量
    uint8_t Run_flag=0; //跑马灯标志 1开启流水 0关闭流水
    uint8_t Led_All_Sta=0; //总灯状态 0全灭 1全亮
    uint32_t soft_tick = 0; //软件计时戳
    uint8_t flow_step = 0; //流水灯步骤索引

    /***************************************************************************
    * 函 数:void LED_Init(void)
    * 功 能:用户指示灯引脚初始化
    * 参 数:无
    * 返回值:无
    * 备 注: 无
    ***************************************************************************/

    void LED_Init(void)
    {
    GPIOA_ModeCfg(GPIO_Pin_11, GPIO_ModeOut_PP_5mA);// PA11—RLED_R;
    GPIOB_ModeCfg(GPIO_Pin_15, GPIO_ModeOut_PP_5mA);// PB15—RLED_L;
    GPIOA_ModeCfg(GPIO_Pin_14, GPIO_ModeOut_PP_5mA);// PA14—GLED_L;
    GPIOB_ModeCfg(GPIO_Pin_4, GPIO_ModeOut_PP_5mA); // PB4—-GLED_R;

    GPIOB_SetBits(GPIO_Pin_4|GPIO_Pin_15);
    GPIOA_SetBits(GPIO_Pin_11|GPIO_Pin_14);
    }

    /**************************************************************************
    * 函 数:void RGB_LED_Off(void)
    * 功 能:所有LED灯关闭
    * 参 数:无
    * 返回值:无
    * 备 注: 无
    **************************************************************************/

    void RGB_LED_Off(void)
    {
    GPIOB_SetBits(GPIO_Pin_4|GPIO_Pin_15);
    GPIOA_SetBits(GPIO_Pin_11|GPIO_Pin_14);
    }

    /**************************************************************************
    * 函 数:void RGB_LED_ON(void)
    * 功 能:所有LED灯开启
    * 参 数:无
    * 返回值:无
    * 备 注: 无
    **************************************************************************/

    void RGB_LED_ON(void)
    {
    GPIOB_ResetBits(GPIO_Pin_4|GPIO_Pin_15);
    GPIOA_ResetBits(GPIO_Pin_11|GPIO_Pin_14);
    }

    /**************************************************************************
    * 函 数:void Red_LED_STA(uint8_t OnOff)
    * 功 能:设置红色LED状态
    * 参 数:OnOFF :00 全亮、01 RLED_L亮、10 RLED_R亮、11 全灭
    * 返回值:无
    * 备 注: 无
    **************************************************************************/

    void Red_LED_STA(uint8_t OnOff)
    {
    switch (OnOff) {
    case 0x00:
    GPIOB_ResetBits(GPIO_Pin_15);
    GPIOA_ResetBits(GPIO_Pin_11);
    break;
    case 0x01:
    GPIOB_SetBits(GPIO_Pin_15);
    GPIOA_ResetBits(GPIO_Pin_11);
    break;
    case 0x10:
    GPIOA_SetBits(GPIO_Pin_11);
    GPIOB_ResetBits(GPIO_Pin_15);
    break;
    case 0x11:
    GPIOB_SetBits(GPIO_Pin_15);
    GPIOA_SetBits(GPIO_Pin_11);
    break;
    default:
    break;
    }
    }

    /**************************************************************************
    * 函 数:void Grn_LED_STA(uint8_t OnOff)
    * 功 能:设置绿色LED状态
    * 参 数:OnOFF :00 全亮、01 GLED_L亮、10 GLED_R亮、11 全灭
    * 返回值:无
    * 备 注: 无
    **************************************************************************/

    void Grn_LED_STA(uint8_t OnOff)
    {
    switch (OnOff) {
    case 0x00:
    GPIOB_ResetBits(GPIO_Pin_4);
    GPIOA_ResetBits(GPIO_Pin_14);
    break;
    case 0x01:
    GPIOB_SetBits(GPIO_Pin_4);
    GPIOA_ResetBits(GPIO_Pin_14);
    break;
    case 0x10:
    GPIOB_ResetBits(GPIO_Pin_4);
    GPIOA_SetBits(GPIO_Pin_14);
    break;
    case 0x11:
    GPIOB_SetBits(GPIO_Pin_4);
    GPIOA_SetBits(GPIO_Pin_14);
    break;
    default:
    break;
    }
    }

    //===================== 按键相关 =====================
    /**************************************************************************
    * 函 数:void KEY_Init(void)
    * 功 能:PA8 PA9按键初始化 上拉输入
    * 参 数:无
    * 返回值:无
    **************************************************************************/

    void KEY_Init(void)
    {
    GPIOA_ModeCfg(KEY1_PIN | KEY2_PIN, GPIO_ModeIN_PU);
    }

    /**************************************************************************
    * 函 数:uint8_t KEY_Scan(void)
    * 功 能:按键扫描 带消抖
    * 返回值:0-无按键 1-KEY1按下 2-KEY2按下
    **************************************************************************/

    uint8_t KEY_Scan(void)
    {
    if(GPIOA_ReadPortPin(KEY1_PIN) == 0)
    {
    DelayMs(KEY_DELAY_MS);
    while(GPIOA_ReadPortPin(KEY1_PIN) == 0);
    DelayMs(KEY_DELAY_MS);
    return 1;
    }
    if(GPIOA_ReadPortPin(KEY2_PIN) == 0)
    {
    DelayMs(KEY_DELAY_MS);
    while(GPIOA_ReadPortPin(KEY2_PIN) == 0);
    DelayMs(KEY_DELAY_MS);
    return 2;
    }
    return 0;
    }

    /**************************************************************************
    * 函 数:void KEY_Process(uint8_t key_val)
    * 功 能:按键事件处理逻辑
    * 参 数:key_val:按键扫描返回值
    * 返回值:无
    **************************************************************************/

    void KEY_Process(uint8_t key_val)
    {
    switch(key_val)
    {
    case 1: // PA8 总开关
    Led_All_Sta = !Led_All_Sta;
    Run_flag = 0; // 开灯默认关闭流水
    flow_step = 0;
    soft_tick = 0;
    if(Led_All_Sta)
    {
    RGB_LED_ON();
    }
    else
    {
    RGB_LED_Off();
    }
    break;

    case 2: // PA9 切换流水,仅灯全亮时生效
    if(Led_All_Sta == 1)
    {
    Run_flag = !Run_flag;
    flow_step = 0;
    soft_tick = 0;
    // 关闭流水立刻恢复四灯常亮
    if(Run_flag == 0)
    {
    RGB_LED_ON();
    }
    }
    break;
    default:break;
    }
    }

    /**************************************************************************
    * 函 数:void LED_Running(void)
    * 功 能:非阻塞跑马灯
    * 参 数:无
    * 返回值:无
    **************************************************************************/

    void LED_Running(void)
    {
    // 不满足流水条件直接退出
    if(Run_flag != 1 || Led_All_Sta != 1)
    {
    soft_tick = 0;
    return;
    }

    soft_tick++;
    if(soft_tick >= FLOW_INTERVAL)
    {
    soft_tick = 0;
    switch(flow_step)
    {
    case 0: Red_LED_STA(0x01); break; //左红
    case 1: Red_LED_STA(0x10); break; //右红
    case 2: Grn_LED_STA(0x01); break; //左绿
    case 3: Grn_LED_STA(0x10); break; //右绿
    }
    flow_step = (flow_step + 1) % 4;
    }
    }

    #ifndef __LED_H__
    #define __LED_H__

    #include "CH58x_common.h"

    #define RLED_R GPIO_Pin_11
    #define RLED_L GPIO_Pin_15
    #define GLED_R GPIO_Pin_4
    #define GLED_L GPIO_Pin_14

    #define KEY1_PIN GPIO_Pin_8
    #define KEY2_PIN GPIO_Pin_9

    extern uint8_t Run_flag;
    extern uint8_t Led_All_Sta;

    void LED_Init(void);
    void RGB_LED_Off(void);
    void Red_LED_STA(uint8_t OnOff);
    void Grn_LED_STA(uint8_t OnOff);
    void RGB_LED_ON(void);

    void KEY_Init(void);
    uint8_t KEY_Scan(void);
    void KEY_Process(uint8_t key_val);
    void LED_Running(void);

    #endif

    #include "CH58x_common.h"
    #include "LED.h"
    uint8_t TxBuff[] = "This is a tx exam\\r\\n";
    uint8_t RxBuff[100];
    uint8_t trigB;
    /*********************************************************************
    * @fn main
    *
    * @brief 主函数
    *
    * @return none
    */

    #include "CH58x_common.h"
    #include "LED.h"

    #include "CH58x_common.h"
    #include "LED.h"

    // 修复字符串类型报错

    uint8_t RxBuff[100];
    uint8_t trigB;

    /*********************************************************************
    * @fn main
    *
    * @brief 主函数
    *
    * @return none
    */

    #include "CH58x_common.h"
    #include "LED.h"

    // 字符串用字节数组,彻底解决类型报错

    #include "CH58x_common.h"
    #include "LED.h"

    uint8_t RxBuff[100];
    uint8_t trigB;

    int main(void)
    {

    LED_Init();
    KEY_Init();

    while(1)
    {
    uint8_t key = KEY_Scan();
    KEY_Process(key);
    LED_Running();
    DelayMs(1);
    }
    }

    4.2 按键电路与消抖

    4.2.1 按键电路设计

    本控制板上的按键采用最简单的单键接地接法:

    按键一端连接GPIO引脚(PA9),另一端连接GND。GPIO配置为上拉输入模式。

    按键工作状态:

    (1)按键未按下(释放状态):GPIO引脚通过内部上拉电阻保持高电平,读取值为1。

    (2)按键按下(导通状态):GPIO引脚通过按键直接连接到GND,被拉至低电平,读取值为0。

    4.2.2 按键抖动问题

    机械按键在按下和释放的瞬间,触点之间会发生弹跳现象(Contact Bounce),导致电平信号在高低之间快速切换数次至数十次,持续时间通常为5~20ms。如果不处理抖动,一次按键可能被误判为多次按键。

    按键抖动的波形特征:

    按下瞬间:电平从高→低→高→低→…→稳定低电平(约5~20ms后稳定)。

    释放瞬间:电平从低→高→低→高→…→稳定高电平(约5~20ms后稳定)。

    4.2.3 硬件消抖

    硬件消抖通过在按键两端并联电容(通常0.1μF)来滤除高频抖动信号。电容充放电的惯性使得电平变化变得平缓,从而消除毛刺。硬件消抖的优点是无需软件处理,缺点是增加了元器件成本和PCB面积。

    4.2.4 软件消抖

    软件消抖是更常用的方法,其核心思想是:检测到按键电平变化后,延时一段时间(通常20ms),再次读取按键状态。如果两次读取结果一致,则认为按键状态确实发生了变化。

    软件消抖的标准流程:

    (1)读取按键电平,如果检测到低电平(按键可能按下)。

    (2)延时20ms,等待抖动消失。

    (3)再次读取按键电平,如果仍然是低电平,确认按键已按下。

    (4)执行按键处理逻辑。

    (5)等待按键释放(可选),防止一次按下被重复处理。

    /**

    * 按键扫描函数(带软件消抖)

    * 返回值:0=按键未按下,1=按键已按下

    */

    uint8_t Key_Scan(void)

    {

    /* 第一次读取按键状态 */

    if(GPIOA_ReadPortPin(GPIO_Pin_9) == 0) /* 低电平=可能按下 */

    {

    DelayMs(20); /* 延时20ms消抖 */

    /* 第二次读取,确认按键状态 */

    if(GPIOA_ReadPortPin(GPIO_Pin_9) == 0)

    {

    /* 等待按键释放 */

    while(GPIOA_ReadPortPin(GPIO_Pin_9) == 0);

    return 1; /* 确认按下并已释放 */

    }

    }

    return 0; /* 按键未按下 */

    }

    工程实践 在实际飞控程序中,通常不使用阻塞式的"等待按键释放"(while死等),而是采用非阻塞的定时器扫描方式,每隔10~20ms检测一次按键状态,这样不会阻塞主循环中的飞控算法。本教学示例为简化理解,使用阻塞方式。

    4.3 按键控制LED

    本节将实现一个实用的功能:通过按键切换LED的亮灭状态。每次按下按键,LED的状态翻转一次(亮变灭、灭变亮)。

    4.3.1 完整代码实现

    /**

    * 按键控制LED翻转

    * 功能:每按一次按键(PA9),LED1(PA4)状态翻转

    * 硬件:按键PA0接地,LED1 PA4经1kΩ电阻接PA4

    */

    #include “CH58x_common.h”

    /**

    * @brief 按键扫描函数(带消抖)

    * @return 1=检测到一次按键按下,0=无按键

    */

    uint8_t Key_Scan(void)

    {

    if(GPIOA_ReadPortPin(GPIO_Pin_9) == 0)

    {

    DelayMs(20); /* 消抖延时 */

    if(GPIOA_ReadPortPin(GPIO_Pin_9) == 0)

    {

    while(GPIOA_ReadPortPin(GPIO_Pin_9) == 0); /* 等待释放 */

    return 1;

    }

    }

    return 0;

    }

    int main(void)

    {

    SetSysClock(CLK_SOURCE_PLL_60MHz);

    /* 配置LED引脚为推挽输出 */

    GPIOA_ModeCfg(GPIO_Pin_4, GPIO_ModeOut_PP_20mA);

    GPIOA_ResetBits(GPIO_Pin_4); /* LED初始熄灭 */

    /* 配置按键引脚为上拉输入 */

    GPIOA_ModeCfg(GPIO_Pin_9, GPIO_ModeIN_PU);

    while(1)

    {

    /* 检测按键是否按下 */

    if(Key_Scan() == 1)

    {

    /* 翻转LED状态 */

    GPIOA_InvertBits(GPIO_Pin_4);

    }

    }

    }

    4.3.2 代码解析

    (1)程序初始化阶段分别配置PA4为输出(LED)和PA0为上拉输入(按键)。

    (2)主循环中不断调用Key_Scan()函数检测按键。

    (3)Key_Scan()函数内部实现了完整的消抖逻辑:检测到低电平→延时20ms→再次确认→等待释放→返回1。

    (4)当Key_Scan()返回1时,调用GPIOA_InvertBits()翻转LED的输出电平。

    4.3.3 按键计数器示例

    扩展上述程序,增加一个计数器,用LED闪烁次数来显示按键按下的次数:

    int main(void)

    {

    SetSysClock(CLK_SOURCE_PLL_60MHz);

    GPIOA_ModeCfg(GPIO_Pin_4, GPIO_ModeOut_PP_20mA);

    GPIOA_ModeCfg(GPIO_Pin_9, GPIO_ModeIN_PU);

    uint8_t press_count = 0;

    uint8_t i;

    while(1)

    {

    if(Key_Scan() == 1)

    {

    press_count++;

    if(press_count > 10) press_count = 0;

    /* LED闪烁press_count次来显示按键次数 */

    for(i = 0; i < press_count; i++)

    {

    GPIOA_SetBits(GPIO_Pin_4); /* 亮 */

    DelayMs(200);

    GPIOA_ResetBits(GPIO_Pin_4); /* 灭 */

    DelayMs(200);

    }

    DelayMs(500); /* 两次显示之间的间隔 */

    }

    }

    }

    4.4 多LED花样控制

    控制板上有4个LED(LED1~LED4,分别对应PA4、PA5、PA12、PA13)。本节将利用这4个LED实现多种花样灯光效果,综合运用GPIO输出、延时、循环等编程技巧。

    4.4.1 流水灯效果

    流水灯是最经典的LED花样效果。4个LED依次点亮,形成灯光"流动"的视觉效果。

    /**

    * 流水灯效果

    * LED1→LED2→LED3→LED4→LED1→… 循环

    */

    #include “CH58x_common.h”

    /* LED引脚定义 */

    #define LED1 GPIO_Pin_4

    #define LED2 GPIO_Pin_5

    #define LED3 GPIO_Pin_12

    #define LED4 GPIO_Pin_13

    #define LED_ALL (LED1 | LED2 | LED3 | LED4)

    /* LED引脚数组,方便循环遍历 */

    const uint32_t led_pins[] = {LED1, LED2, LED3, LED4};

    int main(void)

    {

    SetSysClock(CLK_SOURCE_PLL_60MHz);

    GPIOA_ModeCfg(LED_ALL, GPIO_ModeOut_PP_20mA);

    GPIOA_ResetBits(LED_ALL);

    while(1)

    {

    for(int i = 0; i < 4; i++)

    {

    GPIOA_ResetBits(LED_ALL); /* 全灭 */

    GPIOA_SetBits(led_pins[i]); /* 点亮第i个LED */

    DelayMs(200); /* 保持200ms */

    }

    }

    }

    4.4.2 往返流水灯

    在单向流水灯的基础上增加反向流动效果:LED1→LED2→LED3→LED4→LED3→LED2→LED1→…

    /* 往返流水灯序列 */

    const uint32_t sequence[] = {LED1, LED2, LED3, LED4, LED3, LED2};

    const uint8_t seq_len = 6;

    while(1)

    {

    for(int i = 0; i < seq_len; i++)

    {

    GPIOA_ResetBits(LED_ALL);

    GPIOA_SetBits(sequence[i]);

    DelayMs(150);

    }

    }

    4.4.3 同时点亮扩散效果

    从中心向外扩散:中间两个LED同时亮,然后外侧两个LED同时亮,依次类推。

    /* 扩散效果序列 */

    while(1)

    {

    /* 全部熄灭 */

    GPIOA_ResetBits(LED_ALL);

    DelayMs(300);

    /* 中间两个LED亮 (LED2 + LED3) */

    GPIOA_SetBits(LED2 | LED3);

    DelayMs(300);

    /* 四个LED全部亮 */

    GPIOA_SetBits(LED_ALL);

    DelayMs(300);

    /* 全部熄灭 */

    GPIOA_ResetBits(LED_ALL);

    DelayMs(300);

    /* 外侧两个LED亮 (LED1 + LED4) */

    GPIOA_SetBits(LED1 | LED4);

    DelayMs(300);

    }

    4.4.4 软件PWM实现呼吸灯效果

    呼吸灯效果是指LED亮度从无到有、从有到无,缓慢变化,如同呼吸一般。实现呼吸灯效果需要PWM(脉宽调制)来控制LED的亮度。本控制板的LED引脚并未连接到硬件PWM通道,但可以通过软件模拟PWM来实现。

    软件PWM原理:

    在一个固定周期T内,LED亮的时间为Ton,灭的时间为T-Ton。占空比D = Ton / T。当周期T足够短(如2ms以内)时,人眼无法分辨闪烁,只能感受到平均亮度。通过改变Ton的值,就可以调节LED的表观亮度。

    /**

    * 呼吸灯效果 —— 软件PWM实现

    * 利用软件延时模拟PWM占空比变化

    */

    #include “CH58x_common.h”

    #define LED1 GPIO_Pin_4

    #define LED_ALL LED1

    #define PWM_PERIOD 100 /* PWM周期,单位:延时计数 */

    int main(void)

    {

    SetSysClock(CLK_SOURCE_PLL_60MHz);

    GPIOA_ModeCfg(LED_ALL, GPIO_ModeOut_PP_20mA);

    uint16_t brightness; /* 亮度值,范围0~PWM_PERIOD */

    uint16_t cycle;

    while(1)

    {

    /* 渐亮过程:亮度从0增加到PWM_PERIOD */

    for(brightness = 0; brightness <= PWM_PERIOD; brightness++)

    {

    /* 每个PWM周期内,先亮brightness时间,再灭剩余时间 */

    for(cycle = 0; cycle < 10; cycle++) /* 重复10个周期使效果更平滑 */

    {

    GPIOA_SetBits(LED1);

    DelayUs(brightness * 2); /* 亮的时间 */

    GPIOA_ResetBits(LED1);

    DelayUs((PWM_PERIOD – brightness) * 2); /* 灭的时间 */

    }

    }

    /* 渐灭过程:亮度从PWM_PERIOD减少到0 */

    for(brightness = PWM_PERIOD; brightness > 0; brightness–)

    {

    for(cycle = 0; cycle < 10; cycle++)

    {

    GPIOA_SetBits(LED1);

    DelayUs(brightness * 2);

    GPIOA_ResetBits(LED1);

    DelayUs((PWM_PERIOD – brightness) * 2);

    }

    }

    }

    }

    性能提示 软件PWM会占用大量CPU时间,在飞控主程序中不建议使用。实际飞控板上的电机PWM由硬件定时器产生,不占用CPU资源。软件PWM仅用于对实时性要求不高的LED显示场合。

    4.5 综合实验

    本节将综合运用按键输入和LED输出功能,设计一个"模式切换"程序:通过按键在多种LED显示模式之间切换。

    4.5.1 实验要求

    (1)系统上电后,LED处于"全灭"状态(模式0)。

    (2)每按一次按键,切换到下一种LED显示模式:

    模式0:全部LED熄灭。

    模式1:全部LED常亮。

    模式2:流水灯效果。

    模式3:交替闪烁(LED1+LED3 与 LED2+LED4 交替)。

    模式4:回到模式0。

    (3)切换模式时,当前模式的效果应立即停止,无缝切换到新模式。

    4.5.2拓展练习

    (1)增加更多显示模式,如"呼吸灯模式"、"随机闪烁模式"等。

    (2)增加长按按键功能:短按切换模式,长按(>1秒)进入"紧急闪烁"模式(所有LED快速闪烁)。

    (3)使用第二个按键(如果有)实现"加速/减速"功能,调节LED动画的速度。

    (4)将模式编号通过LED闪烁次数来"播报"——切换模式时先用LED闪烁次数表示新模式的编号。


    学习资源

    🎬 B站系列视频教程:手把手教你从零组装无人机

    📦 百度网盘资料包(SDK、教材PDF、完整源码):点击下载 提取码: JZS8


    本系列持续更新,欢迎收藏关注。如有问题欢迎在评论区交流。

    赞(0)
    未经允许不得转载:171主机测评 » 【飞控系列教程·④】GPIO输入模式与按键控制:消抖、流水灯、软件PWM呼吸灯详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址