【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中断能够唤醒。

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

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


在主循环中,判断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任务事件。

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

| 按键动作 | 软件事件 | 应用功能 |
|---|---|---|
| 刚按下 | 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_DOWN、KEY_UP、KEY_SINGLE、KEY_DOUBLE、KEY_LONG 和 KEY_REPEAT 都属于一次性事件。
应用层读取一次后,应立即清除,避免下一次 TMOS 调度重复处理。

KEY_HOLD 是持续状态,不会因为读取而清除;只要 PA4 仍是低电平,每次读取都可以得到 1。如果每次按键事件处理结束后,程序直接读取 PA4 原始状态,连续松开达到300ms之后,就停止按键处理,等待一些的PA4的中断触发。
通过串口日志可以看到按键的单击、双击和长按,不同效果下的数据计数不同。



在调用 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 下降沿中断,系统再次进入低功耗。
通过功耗分析仪来观察,按键使用中的功耗变化。

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

浙公网安备 33010602011771号