三个实体按键看起来只是 GPIO 输入,但在当前小鸿 WS63 OpenHarmony 固件中,一次短按要经过电平采样、稳定去抖、时长分类、事件回调、主消息队列,再分发到音频、显示或 Agent 任务。直接在 GPIO 中断里改音量、刷新 LVGL 或启动网络,会把硬件时序、UI 线程和会话状态绑在同一个上下文中,后续很难处理抖动、重复事件和耗时操作。

本文从当前 key_config.ckey_config.hmain.c 的真实代码出发,说明 P13、P12、P11 三个按键怎样进入 LiteOS-M/CMSIS-RTOS2 消息队列,以及为什么主队列、音频队列、显示队列和 Agent 队列要分开。源码快照会固定本次文件哈希;本轮属于代码与构建产物核对,没有重新烧录后做三键实机回归。

三键先变成统一的事件编号

key_config.h 不把 P11、P12、P13 直接暴露给业务层,而是定义七种动作:两个音量键各有短按、长按;中键有短按、普通长按和 5 秒长按。eKey_MaxCount 还成为后续设置事件的编号起点,使主队列可以用一个字节承载按键和系统事件。

enum {
    eKey_VolUp_ShortPressed = 0,
    eKey_VolUp_LongPressed,
    eKey_VolDw_ShortPressed,
    eKey_VolDw_LongPressed,
    eKey_Wakeup_ShortPressed,
    eKey_Wakeup_LongPressed,
    eKey_Wakeup_LongPressed_5S,
    eKey_MaxCount,
};

事件枚举的意义是把“哪个引脚变了”转换为“用户完成了什么动作”。主任务不需要重新读 GPIO,也不需要知道中键使用上拉、音量键使用下拉;它只处理已经去抖和分级后的语义事件。

轮询任务负责稳定电平和按压时长

当前实现同时保留 GPIO 双边沿中断日志和一个独立 KeyPoll 任务。真正生成业务事件的是轮询状态机。它每 30 ms 采样一次,电平连续稳定 60 ms 才接受变化;短于 80 ms 的脉冲被忽略;同一按键事件还要经过 400 ms 防重复保护。这些阈值来自当前源码,不是通用键盘参数。

#define KEY_POLL_INTERVAL_MS 30U
#define KEY_DEBOUNCE_MS      60U
#define KEY_MIN_PRESS_MS     80U
#define KEY_EVENT_GUARD_MS  400U
#define KEY_EVENT_NONE        0xFFU
#define KEY_SCAN_FIRST_PIN   11U
#define KEY_SCAN_LAST_PIN    13U

轮询比在 ISR 中直接执行业务更容易表达“按下—保持—释放”的完整过程。中断仍可用于观察边沿和诊断电平,但短按、长按最终以释放时的持续时间决定。这样可以避免按下沿先触发一次,释放沿又误触发一次。

短按、长按和五秒长按在释放时判定

key_poll_handle 在稳定电平进入按下态时记录 down_ms。恢复空闲电平后,用当前时间减去按下时间得到持续时长:小于 80 ms 丢弃;中键达到 5000 ms 选择 5 秒事件;其他达到 500 ms 选择普通长按;否则保留短按事件。最后再检查 400 ms guard,符合条件才调用 send_key_event

if (duration < KEY_MIN_PRESS_MS) {
    log_info("[KeyPoll] %s ignore short pulse duration[%u]\r\n",
             key->name, (unsigned)duration);
    return;
}
if ((key->long5_event != KEY_EVENT_NONE) &&
    (duration >= KEY_LONG_PRESS_INTERVAL_5S)) {
    event = key->long5_event;
} else if (duration >= KEY_LONG_PRESS_INTERVAL) {
    event = key->long_event;
}

这里有一个容易忽略的设计:5 秒事件必须先于普通长按判断,否则 5000 ms 同时满足 500 ms,永远只会进入普通长按。当前代码的判断顺序正确,音量键的 long5_event 则是 KEY_EVENT_NONE,不会凭空产生 5 秒动作。

数据表把引脚与语义动作绑定

三个按键由同一个 key_poll_state_t 数组驱动。P13 的短按/长按对应音量加和亮度加,P12 对应音量减和亮度减,P11 的短按、长按、5 秒长按分别进入会话、语音打断开关和重新配网流程。数据表避免复制三份状态机,也把扫描范围稳定限定在确认过的按键宏上。

key_poll_state_t keys[] = {
    {.pin = KEY_VOLUP_PIN, .id = KEY_VOLUP_PIN,
     .short_event = eKey_VolUp_ShortPressed,
     .long_event = eKey_VolUp_LongPressed,
     .long5_event = KEY_EVENT_NONE, .name = "VolUp"},
    {.pin = KEY_VOLDW_PIN, .id = KEY_VOLDW_PIN,
     .short_event = eKey_VolDw_ShortPressed,
     .long_event = eKey_VolDw_LongPressed,
     .long5_event = KEY_EVENT_NONE, .name = "VolDw"},
    {.pin = KEY_WAKEUP_PIN, .id = KEY_WAKEUP_PIN,
     .short_event = eKey_Wakeup_ShortPressed,
     .long_event = eKey_Wakeup_LongPressed,
     .long5_event = eKey_Wakeup_LongPressed_5S, .name = "Wakeup"},
};

为保证代码块确实来自项目,公开文章保留了字段名和枚举名,只对换行做了排版,没有把它改写成其他框架的伪代码。引脚的实际数值仍由 board_config.h 提供:13、12、11;P14 不在该数组中。

回调只投递,不在按键上下文执行重活

轮询任务调用 send_key_event 后,已注册的 key_event_callback_cb 把一个 uint8_t 放入 g_main_event_qid。回调不直接访问 LVGL,不建立 WebSocket,也不清除文件系统配置。这是关键的线程边界:输入侧只生成事件,主任务负责决定动作。

uint32_t key_event_callback_cb(uint8_t event)
{
    if (g_main_event_qid) {
        osMessageQueuePut(
            g_main_event_qid,
            (const void *) &event,
            0,
            0);
    }
    return 0;
}

当前回调没有处理 osMessageQueuePut 的返回值,因此主队列已满时缺少显式失败日志。这不意味着架构错误,但属于可改进点:输入事件如果要求不可丢,应记录失败计数,或设计合并策略。音量连续短按通常允许后续状态刷新覆盖,5 秒清配网却更值得记录投递失败。

四个队列按消费职责分开

MainTask 创建主、音频、显示和 Agent 四个队列。主、显示队列各 8 槽;音频下行事件较密集,扩为 64 槽;Agent 音频帧和状态事件使用 32 槽。所有消息当前都是一个字节的枚举值,因此 osMessageQueueNew 的元素大小统一为 sizeof(uint8_t)

#define MSG_QUEUE_SIZE         (8)
#define AUD_MSG_QUEUE_SIZE     (64)
#define AGENT_MSG_QUEUE_SIZE   (32)

g_audx_event_qid = osMessageQueueNew(
    AUD_MSG_QUEUE_SIZE, sizeof(uint8_t), NULL);
g_disp_event_qid = osMessageQueueNew(
    MSG_QUEUE_SIZE, sizeof(uint8_t), NULL);
g_main_event_qid = osMessageQueueNew(
    MSG_QUEUE_SIZE, sizeof(uint8_t), NULL);
g_agent_event_qid = osMessageQueueNew(
    AGENT_MSG_QUEUE_SIZE, sizeof(uint8_t), NULL);

队列容量不同来自流量差异,不是任务优先级。音频下行的请求频率高,8 槽可能出现漏喂和卡顿;Agent 发送音频时也会连续投递。按键队列流量低,但还会承载 Wi-Fi、字库等设置事件,所以仍需要在 40 ms 轮询等待中持续消费。

音量短按同时更新持久状态、音频和界面

音量加短按进入主任务后,先判断设备是否正在重新配网。配网态下不改变音量,只要求显示 Wi-Fi 状态。正常状态下调用 increase_volume() 更新设置,再向音频队列发送 eAud_VolUp,向显示队列发送 eDisp_Volume_Update。三个步骤属于不同职责。

increase_volume();

msg_send = eAud_VolUp;
osMessageQueuePut(
    g_audx_event_qid,
    (const void *) &msg_send,
    0,
    0);
msg_send = eDisp_Volume_Update;
osMessageQueuePut(
    g_disp_event_qid,
    (const void *) &msg_send,
    0,
    0);

显示任务收到更新后刷新状态栏,并弹出音量覆盖层;音频任务负责实际音量动作。主任务不调用 LVGL 对象 API,这让 UI 仍只在自己的任务上下文更新。音量减走同一结构,只把设置和音频枚举换成 decrease/VolDw。

长按音量键改变的是背光而不是音量

P13、P12 的长按在主任务中分别调用 increase_blk_level()decrease_blk_level(),随后向显示队列发送 eDisp_Bar_Update。状态栏和背光等级的应用仍留在显示/设置相关代码中。这使同一实体键通过按压时长承担两个动作,而不会在 key_config.c 里硬编码业务。

这个映射也解释了为什么三键测试不能只看中断日志。日志里看到 P13 电平变化,只证明输入链发生了变化;还要确认轮询产生正确枚举、主队列收到、设置值变化、显示队列刷新,以及真实背光或音量产生效果。

中键短按要根据 Agent 当前状态决定动作

中键不是简单的开关。短按先向显示队列发送亮屏事件,再读取 Agent 状态。若当前正在监听、播放、唤醒或连接,短按发送 AGENT_EVT_TOGGLE_CHAT,用于结束输入、取消等待或打断 TTS;若处于空闲,则先向音频队列发送 eAud_WakeUp,由 CI1302 的 0x0102 唤醒事件启动 Agent 会话。

if (agent_st == AGENT_STATE_LISTENING ||
    agent_st == AGENT_STATE_SPEAKING ||
    agent_st == AGENT_STATE_WAKEUP ||
    agent_st == AGENT_STATE_CONNECTING) {
    agent_send_event(AGENT_EVT_TOGGLE_CHAT);
} else {
    msg_send = eAud_WakeUp;
    osMessageQueuePut(
        g_audx_event_qid,
        (const void *) &msg_send,
        0,
        0);
}

普通长按中键切换 voice_interrupt 并持久化,5 秒长按则清除 Wi-Fi 配置后调用 restart_system(1)。把三种动作放在主任务分支而非 GPIO 层,可以访问 Agent、设置和系统重启接口,同时保留明确的状态判断。

消息队列也需要过载与生命周期设计

当前创建队列失败时,主任务记录错误并保留早期显示,然后直接返回。这比继续使用空句柄安全。运行阶段还应关注 Put 失败、消费任务是否存活、队列深度与消息优先级。Agent 音频投递已经有队列满的错误日志,而按键主回调尚未记录 Put 结果,后续可以补充计数但不能在失败时无限阻塞输入任务。

还要避免把大块文本或音频数据直接塞进当前一字节队列。音频帧有自己的缓冲与事件协议,LVGL 文本也通过专用 pending buffer 交接。队列中的字节是“有一件事要处理”的控制消息,不是任意负载容器。

源码确认与实机闭环仍然分开

本文已经从当前源码确认三键映射、30/60/80/400 ms 时序阈值、500/5000 ms 长按边界、事件数据表、四个消息队列的容量,以及音量、亮度和中键的实际分支。第 03 篇记录的构建链已经产出过真实 .fwpkg,但本文没有把源码阅读和历史包直接写成今天的三键实测。

完整设备验收还需要:烧入与本次源码哈希对应的包;启动日志确认固件;分别执行 P13/P12/P11 的短按、长按与中键 5 秒长按;观察设置值、音频、背光、UI、Agent 和重启行为;快速重复按压检查 guard 与队列满日志。只有这些步骤完成,才能把状态从“源码链路已确认”提升为“三键实机闭环”。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐