【WCH蓝牙系列芯片】-基于CH592开发板—GPIO中断唤醒实现低功耗多功能按键

------------------------------------------------------------------------------------------------------------------------------------

  在低功耗蓝牙产品中,有的时候一个按键往往不只承担“按下/松开”功能,还需要识别单击、双击、长按和长按连发等操作。如果一直使用 5ms 或 20ms 的定时任务轮询 GPIO的状态,虽然程序结构简单,但会让 MCU 周期性地从睡眠中醒来,直接抬高设备的平均功耗。

  在低功耗状态下,如果一直利用TMOS任务轮询查标志位,就会一直有任务运行导致睡眠功耗增加。这次在CH592 BLE 从机例程的基础上,把按键处理改成了“空闲阶段 GPIO 中断唤醒,配合 TMOS 定时扫描”的方式。在没有按键操作时,不存在周期性的按键扫描任务;当检测到按键按下才启动 20ms 扫描,在完成单击、双击、长按等判定后停止扫描,重新等待下一次按键触发。

按键处理初始化
#include "CH59x_common.h"
#include "CH592Key.h"
#define APP_TASK_BUTTON_PIN             GPIO_Pin_4   //按键是PA4

/* ====== 硬件配置宏====== */
#define KEY_ACTIVE_LOW      1               // 1=按下为低电平 0=按下为高电平

/* ====== 内部状态====== */
static uint8_t Key_Flag[KEY_COUNT];         // 事件标志

/*
 * GPIO中断只设置这个标志。
 * 真正的TMOS任务启动放在主循环中处理。
 */
static volatile uint8_t Key_WakeupPending = 0;

#define KEY_PRESSED             1
#define KEY_UNPRESSED           0

/*********************************************************************
 * @fn      CH592Key_Init
 *
 * @brief   初始化按键GPIO和低功耗唤醒
 */
void CH592Key_Init(void)
{
    uint8_t i;

    for(i = 0; i < KEY_COUNT; i++)
    {
        Key_Flag[i] = 0;
    }

    Key_WakeupPending = 0;

    /* PA4上拉输入,按下时接地 */
    GPIOA_ModeCfg(APP_TASK_BUTTON_PIN, GPIO_ModeIN_PU);

#if(defined(HAL_SLEEP)) && (HAL_SLEEP == TRUE)
    /*
     * 允许GPIO唤醒低功耗睡眠。
     * 不会影响BLE已有的RTC/协议栈唤醒源。
     */
    PWR_PeriphWakeUpCfg(ENABLE,
                        RB_SLP_GPIO_WAKE,
                        Long_Delay);
#endif
    /* 开启第一次下降沿唤醒 */
    CH592Key_ArmWakeup();
}
/*********************************************************************
 * @fn      CH592Key_IsPressed
 *
 * @brief   读取按键原始电平
 *
 * @return  1=按下,0=松开
 */
uint8_t CH592Key_IsPressed(void)
{
    if(GPIOA_ReadPortPin(APP_TASK_BUTTON_PIN) == 0)
    {
        return KEY_PRESSED;
    }

    return KEY_UNPRESSED;
}
/*********************************************************************
 * @fn      Key_GetState
 *
 * @brief   获取指定按键状态
 */
static uint8_t Key_GetState(uint8_t n)
{
    if(n == KEY_BROADCAST)
    {
        return CH592Key_IsPressed();
    }

    return KEY_UNPRESSED;
}
/*********************************************************************
 * @fn      CH592Key_ArmWakeup
 *
 * @brief   重新打开PA4下降沿中断
 */
void CH592Key_ArmWakeup(void)
{
    /*
     * 先关闭PA4中断,避免配置过程中产生干扰。
     * 这里只关闭PA4,不影响GPIOA其他引脚。
     */
    R16_PA_INT_EN &= ~APP_TASK_BUTTON_PIN;

    /* 清除PA4旧的中断标志 */
    GPIOA_ClearITFlagBit(APP_TASK_BUTTON_PIN);

    /*
     * PA4低电平有效,所以按下对应下降沿。
     * GPIOA_ITModeCfg会配置并使能该引脚中断。
     */
    GPIOA_ITModeCfg(APP_TASK_BUTTON_PIN,
                    GPIO_ITMode_FallEdge);

    PFIC_EnableIRQ(GPIO_A_IRQn);

    /*
     * 防止重新开启中断时,按键已经被按下,
     * 但没有产生新的下降沿。
     */
    if(CH592Key_IsPressed())
    {
        R16_PA_INT_EN &= ~APP_TASK_BUTTON_PIN;
        GPIOA_ClearITFlagBit(APP_TASK_BUTTON_PIN);
        Key_WakeupPending = 1;
    }
}
/*********************************************************************
 * @fn      GPIOA_IRQHandler
 *
 * @brief   GPIOA中断服务函数
 */
__INTERRUPT
__HIGH_CODE
void GPIOA_IRQHandler(void)
{
    if(GPIOA_ReadITFlagPort() & APP_TASK_BUTTON_PIN)
    {
        /*
         * 检测到第一次按下后关闭PA4中断。
         * 后续消抖、松开、双击和长按由20ms扫描处理。
         */
        R16_PA_INT_EN &= ~APP_TASK_BUTTON_PIN;

        GPIOA_ClearITFlagBit(APP_TASK_BUTTON_PIN);

        /*
         * 中断里只置位。
         * 不要在这里PRINT、启动TMOS任务或者调用BLE接口。
         */
        Key_WakeupPending = 1;
    }
}
/*********************************************************************
 * @fn      CH592Key_TakeWakeup
 *
 * @brief   获取并清除GPIO唤醒请求
 */
uint8_t CH592Key_TakeWakeup(void)
{
    if(Key_WakeupPending)
    {
        Key_WakeupPending = 0;
        return 1;
    }

    return 0;
}
/*********************************************************************
 * @fn      Key_Check
 *
 * @brief   查询按键事件
 */
uint8_t Key_Check(uint8_t n, uint8_t flag)
{
    if(n >= KEY_COUNT)
    {
        return 0;
    }

    if(Key_Flag[n] & flag)
    {
        /*
         * HOLD是持续状态,读取后不能清除。
         * 其他事件都是一次性事件,读取后自动清除。
         */
        if(flag != KEY_HOLD)
        {
            Key_Flag[n] &= ~flag;
        }

        return 1;
    }

    return 0;
}
/*********************************************************************
 * @fn      Key_Tick
 *
 * @brief   按键状态机,每20ms调用一次
 */
void Key_Tick(void)
{
    static uint8_t CurrState[KEY_COUNT] = {0};
    static uint8_t PrevState[KEY_COUNT] = {0};

    /*
     * 状态:
     * 0 = 空闲
     * 1 = 第一次按下,等待松开或长按
     * 2 = 第一次松开,等待第二次按下
     * 3 = 已经确认双击,等待第二次松开
     * 4 = 长按连发
     */
    static uint8_t KeyState[KEY_COUNT] = {0};
    /* 状态机计时器,单位20ms */
    static uint16_t KeyTimer[KEY_COUNT] = {0};
    uint8_t i;

    for(i = 0; i < KEY_COUNT; i++)
    {
        if(KeyTimer[i] > 0)
        {
            KeyTimer[i]--;
        }
        PrevState[i] = CurrState[i];
        CurrState[i] = Key_GetState(i);
        /* 持续按住状态 */
        if(CurrState[i] == KEY_PRESSED)
        {
            Key_Flag[i] |= KEY_HOLD;
        }
        else
        {
            Key_Flag[i] &= ~KEY_HOLD;
        }
        /* 刚按下 */
        if((CurrState[i] == KEY_PRESSED) &&
           (PrevState[i] == KEY_UNPRESSED))
        {
            Key_Flag[i] |= KEY_DOWN;
        }
        /* 刚松开 */
        if((CurrState[i] == KEY_UNPRESSED) &&
           (PrevState[i] == KEY_PRESSED))
        {
            Key_Flag[i] |= KEY_UP;
        }
        switch(KeyState[i])
        {
            case 0:
            {
                /* 空闲状态,等待第一次按下 */
                if(CurrState[i] == KEY_PRESSED)
                {
                    KeyTimer[i] = KEY_TIME_LONG;
                    KeyState[i] = 1;
                }
                break;
            }
            case 1:
            {
                /*
                 * 第一次按下后:
                 * 松开则开始等待第二次点击;
                 * 持续达到2秒则进入长按。
                 */
                if(CurrState[i] == KEY_UNPRESSED)
                {
                    KeyTimer[i] = KEY_TIME_DOUBLE;
                    KeyState[i] = 2;
                }
                else if(KeyTimer[i] == 0)
                {
                    Key_Flag[i] |= KEY_LONG;
                    KeyTimer[i] = KEY_TIME_REPEAT;
                    KeyState[i] = 4;
                }
                break;
            }
            case 2:
            {
                /*
                 * 第一次松开后等待第二次按下。
                 */
                if(CurrState[i] == KEY_PRESSED)
                {
                    Key_Flag[i] |= KEY_DOUBLE;
                    KeyState[i] = 3;
                }
                else if(KeyTimer[i] == 0)
                {
                    /*
                     * 双击等待时间结束仍没有第二次按下,
                     * 确认为单击。
                     */
                    Key_Flag[i] |= KEY_SINGLE;
                    KeyState[i] = 0;
                }
                break;
            }

            case 3:
            {
                /* 双击已经确认,等待第二次松开 */
                if(CurrState[i] == KEY_UNPRESSED)
                {
                    KeyState[i] = 0;
                }
                break;
            }
            case 4:
            {
                /*
                 * 长按连发状态:
                 * 松开退出;
                 * 按住时每100ms产生一次REPEAT。
                 */
                if(CurrState[i] == KEY_UNPRESSED)
                {
                    KeyState[i] = 0;
                }
                else if(KeyTimer[i] == 0)
                {
                    Key_Flag[i] |= KEY_REPEAT;
                    KeyTimer[i] = KEY_TIME_REPEAT;
                }
                break;
            }
            default:
            {
                KeyState[i] = 0;
                KeyTimer[i] = 0;
                break;
            }
        }
    }
}
在CH592Key_Init()初始化函数中,将 PA4 配置为上拉输入,通过 PWR_PeriphWakeUpCfg 允许 GPIO 唤醒睡眠,
让芯片在关闭外设时钟进入深度睡眠后,PA4中断能够唤醒。

image

   调用 CH592Key_ArmWakeup() 开启 PA4 的下降沿中断,重新打开 PA4 中断时,代码先关闭该引脚中断、清除旧标志,再配置下降沿;配置完成后,又立即检查一次 PA4中断,这个是为了如果程序正在重新配置下降沿时,用户已经把按键按成低电平,那么后面可能不会再出现一个新的下降沿。再次读取原始电平,可以保证这种情况下仍然启动扫描任务,不会漏键的现象。

image

  中断触发后立即关闭 PA4 引脚中断,进行标志位处理

image

image

  在主循环中,判断GPIO的中断至的标志位,从而触发Peripheral_KeyScanStart()函数,执行按键扫描任务。

void Peripheral_KeyScanStart(void)
{
    /*
     * 已经扫描时不重复启动。
     */
    if(KeyScanActive)
    {
        return;
    }
    /*
     * 防止Peripheral_Init尚未完成。
     */
    if(Peripheral_TaskID == INVALID_TASK_ID)
    {
        return;
    }
    KeyScanActive = 1;
    KeyReleasedTicks = 0;
    /*
     * 防止有旧的按键定时器残留。
     */
    tmos_stop_task(Peripheral_TaskID,
                   CH592_KEY_TICK_EVT);
    KEY_LOG("[KEY-LP] GPIO wake -> scan start\r\n");
    /*
     * 中断触发20ms后第一次读取按键,
     * 同时起到初步机械消抖作用。
     */
    tmos_start_task(Peripheral_TaskID,
                    CH592_KEY_TICK_EVT,
                    MS1_TO_SYSTEM_TIME(KEY_SCAN_PERIOD_MS));
}

第一次延迟 20ms 后再读取 GPIO,同时起到消抖作用。启动CH592_KEY_TICK_EVT任务事件。

image

在按键事件使用一个字节的不同位表示:

image

按键动作软件事件应用功能
刚按下 KEY_DOWN 输出按下日志、清除本次按压计时状态
持续按住 KEY_HOLD 表示当前物理电平仍处于按下状态
刚松开 KEY_UP 输出松开日志
单击 KEY_SINGLE 数据加 1
双击 KEY_DOUBLE 数据加 10
按住超过2秒 KEY_LONG 产生一次长按事件,数据加 1
长按后继续按住 KEY_REPEAT 每 100ms 数据加 1

核心的按识别是在Key_Tick() 内部中有五个状态,

状态含义进入条件退出条件
0 空闲 初始化或一次动作结束 检测到按下进入状态1
1 第一次按下 状态0检测到低电平 松开进入状态2;达到2秒进入状态4
2 第一次松开,等待第二击 状态1检测到松开 200ms内再次按下进入状态3;超时产生单击
3 已确认双击 状态2检测到第二次按下 第二次松开后回到状态0
4 长按连发 状态1按住达到2秒 每100ms产生连发;松开回到状态0

KEY_DOWNKEY_UPKEY_SINGLEKEY_DOUBLEKEY_LONGKEY_REPEAT 都属于一次性事件。

应用层读取一次后,应立即清除,避免下一次 TMOS 调度重复处理。

image

  KEY_HOLD 是持续状态,不会因为读取而清除;只要 PA4 仍是低电平,每次读取都可以得到 1。如果每次按键事件处理结束后,程序直接读取 PA4 原始状态,连续松开达到300ms之后,就停止按键处理,等待一些的PA4的中断触发。

  通过串口日志可以看到按键的单击、双击和长按,不同效果下的数据计数不同。

image

image

image

  在调用 CH59x_LowPower(),执行 __WFE(),CPU 进入睡眠。按下PA4触发按键,RB_SLP_GPIO_WAKE 唤醒 CPU,恢复系统时钟和电源;重新使能 PFIC 中断,进入 GPIOA_IRQHandler(),Key_WakeupPending = 1后返回 TMOS_SystemProcess();主循环调用 CH592Key_TakeWakeup(),调用 Peripheral_KeyScanStart()中启动 20ms 的 CH592_KEY_TICK_EVT事件任务。利用Key_Tick()函数,识别单击、双击、长按等功能。按键状态机完成后,停止 20ms 扫描重新使能 GPIO 下降沿中断,系统再次进入低功耗。

  通过功耗分析仪来观察,按键使用中的功耗变化。

image

实际应用:通过按键来开关广播信号

image

 

posted on 2026-07-31 15:53  凡仕  阅读(3)  评论(0)    收藏  举报