杰理 SDK 文档中心
首页
首页
  • 项目概览

    • AD23N SDK 概述与芯片平台
    • 工程结构与模块划分
  • 快速开始

    • 开发环境搭建与工具链
    • 编译构建指南
    • 烧录与固件升级工具
  • 应用框架与产品工作流

    • 应用入口与模式调度
    • 音乐播放应用
    • MIDI 解码与键盘演奏
    • 录音应用
    • LINEIN 与扩音应用
    • USB 从设备应用
    • 待机、软关机与空闲检测
    • 公共 UI 与 LED 显示
  • 音频子系统

    • 音频解码器框架
    • 音频编码器框架
    • 音效算法库
    • 音频管理与输出通路
  • 存储与文件系统

    • 文件系统层
    • NOR Flash 与虚拟机存储
    • 设备与设备管理
  • 系统服务与运行时

    • 消息机制与事件分发
    • 按键扫描与输入处理
    • 电源管理与低功耗控制
    • 定时器与系统任务
  • 外设驱动与平台

    • CPU 平台与启动流程
    • USB 协议栈与主机/设备驱动
    • SPI 与通用外设接口
  • 固件升级与构建工具

    • 固件升级机制
    • 编译后处理与镜像打包
    • 构建系统与命令行工具

按键扫描与输入处理

AD23N SDK 的按键子系统:以 10ms 周期轮询各类按键(IO / AD / 矩阵 / 红外 / 触摸),经统一状态机识别短按、长按、连按、抬起(及可选双击)等事件,再通过消息过滤器转换为系统消息(MSG_xxx)投递到消息队列,驱动上层应用逻辑。

Purpose and Scope

本页面向 sdk/app/bsp/common/key/ 目录下的按键扫描与输入处理机制,完整覆盖:

  • 统一按键抽象 key_interface_t 与编译期注册表 key_list[]
  • 核心扫描状态机 key_scan()(短按 / 长按 / 连按 / 抬起 / 双击)
  • 各类型按键驱动(IO、AD、矩阵、红外、触摸)的接入方式与寄存器级初始化示例
  • 按键事件到系统消息(post_msg)的转换链路(key2msg_emit + key_msg_filter)
  • 各工作模式的按键映射表(如 music_key_table.c 的红外键表)

以下内容刻意留给其他页面:消息队列本身的实现(msg.c / post_msg)、按键唤醒电源逻辑(sdk/app/bsp/cpu/sh59/power/key_wakeup.c)、各音频模式对 MSG_xxx 的具体业务处理,以及 GPIO 驱动层细节。本页聚焦"从按键物理信号到系统消息"这一段完整链路。

概述

按键输入是蓝牙音箱 / MCU 类产品最重要的人机接口,AD23N SDK 将其设计为三层解耦结构:

  1. 驱动层(key_drv_io.c、key_drv_ad.c、key_matrix.c、key_ir.c、key_touch.c):每种物理按键类型实现同一份 key_interface_t 接口(初始化 + 读值),具体差异(寄存器、ADC、红外解码、触摸检测)被完全封装。
  2. 扫描层(key.c):核心状态机 key_scan() 在 10ms 定时器中被周期调用,通过 get_key_value() 轮询驱动层获得当前键值,再用计数状态机把"持续按住时间"翻译成按键事件(短按、长按、连按、抬起)。
  3. 应用层(各模式 *_key_table.c / *_mode_key.c):通过 key_table_sel() 注册一个消息过滤函数,把 (key_status, key_num, key_type) 三元组查表映射为 MSG_xxx 系统消息,经 post_msg() 投递。

设计意图:把"物理按键怎么读"和"按键含义是什么"彻底分离。换板子改驱动、换产品改映射表,扫描状态机与消息机制完全不用动;同时所有按键类型共享一套消抖、长按、连按逻辑,保证行为一致性。

关键时间参数(由 10ms 扫描周期折算):短按判定约 30ms(3 次连续采样,兼作消抖),长按判定约 750ms,长按后每 150ms 重复产生连按事件,双击窗口约 350ms(默认关闭)。

架构

flowchart TD
    subgraph sg_Drv["驱动层 key_drv_*"]
        IO["key_io_info (IO)"]
        AD["key_ad_info (AD)"]
        MX["key_matrix_info (矩阵)"]
        IR["key_ir_info (红外)"]
        TC["key_touch_info (触摸)"]
    end

    subgraph sg_Core["扫描层 key.c"]
        INIT["key_init()"]
        SCAN["key_scan() 10ms 周期"]
        GV["get_key_value()"]
        EMIT["key2msg_emit()"]
    end

    subgraph sg_App["应用层"]
        FILT["key_msg_filter 消息过滤"]
        TABLE["模式按键表 xxx_key_table.c"]
        QUEUE["消息队列 post_msg()"]
        KICK["d_key_voice_kick() 按键音"]
    end

    INIT --> IO
    INIT --> AD
    INIT --> MX
    INIT --> IR
    INIT --> TC
    SCAN --> GV
    GV --> IO
    GV --> AD
    GV --> MX
    GV --> IR
    GV --> TC
    SCAN --> EMIT
    EMIT --> FILT
    TABLE --> FILT
    EMIT --> KICK
    FILT --> QUEUE

各层职责说明:

  • 驱动层:每个驱动导出一个 const key_interface_t 实例(如 key_io_info),内含 key_type、key_init、key_get_value。key_get_value() 返回当前按下的键号(NO_KEY 表示无按键,-1 表示本次读取失败)。驱动是否编译由 KEY_IO_EN / KEY_AD_EN / KEY_MATRIX_EN / KEY_IR_EN / KEY_TOUCH_EN 宏在编译期裁剪。
  • 扫描层:key_init() 遍历 key_list[] 逐个调用驱动的初始化;key_scan() 是唯一的状态机入口,输出按键事件;key2msg_emit() 是事件出口,负责查表与投递。
  • 应用层:模式按键表以"按键号 → 消息"的查表宏定义事件语义;key_msg_filter 函数指针由 key_table_sel() 在模式切换时动态更换,因此同一套物理按键在不同工作模式下可以有完全不同的含义。

核心实现

统一按键接口 key_interface_t

所有按键驱动都实现同一份接口,这是整个框架可扩展的基石:

typedef struct {
    KEY_TYPE key_type;
    void (*key_init)(void);
    u32(*key_get_value)(void);
} key_interface_t;

Source: key.h

  • key_type:声明本驱动的按键类型(IO / AD / MATRIX / IR / TOUCH),由 KEY_TYPE 枚举定义;
  • key_init:上电时被 key_init() 调用,完成 GPIO / ADC / 解码器配置;
  • key_get_value:每次扫描被轮询,返回当前按下的键号。返回值约定:NO_KEY(0xff)表示无按键,-1 表示驱动尚未就绪(本次扫描放弃)。

配套的 key_io_t 结构体用于在驱动层与扫描层之间传递结果:

typedef struct {
    u8 key_type;
    u8 key_num;
} key_io_t;

Source: key.h

按键类型、状态与门槛值

key.h 集中定义了全部按键语义常量:

typedef enum {
    KEY_TYPE_IO,
    KEY_TYPE_AD,
    KEY_TYPE_MATRIX,
    KEY_TYPE_IR,
    KEY_TYPE_TOUCH,
} KEY_TYPE;

#define KEY_BASE_CNT  2
#define KEY_LONG_CNT  75
#define KEY_HOLD_CNT  15
#define KEY_SHORT_CNT 3

enum {
    KEY_SHORT    =   0x0,
    KEY_SHORT_UP =   0x1,
    KEY_LONG     =   0x2,
    KEY_HOLD     =   0x3,
    KEY_LONG_UP  =   0x4,
    KEY_DOUBLE	 =	 0x5,
    KEY_MAX_STATUS,
};

Source: key.h

按键状态机共产生 6 种事件,含义与触发时机如下(均以 10ms 扫描周期折算):

事件值触发条件典型用途
KEY_SHORT0x0按下持续 KEY_SHORT_CNT(3 次 ≈ 30ms)消抖后的"按下沿",一般与 SHORT_UP 成对
KEY_SHORT_UP0x1在短按窗口内抬起(3 < counter < 75)单击动作执行点,如播放/暂停
KEY_LONG0x2持续按住达 KEY_LONG_CNT(75 次 ≈ 750ms)长按动作,如开机/关机
KEY_HOLD0x3长按后再持续 KEY_HOLD_CNT(15 次 ≈ 150ms),且周期重置连续调节,如音量/快进退
KEY_LONG_UP0x4长按后抬起长按结束点,可与 KEY_LONG 配合
KEY_DOUBLE0x5双击窗口(KEY_DOUBLE_CLICK_CNT ≈ 350ms)内第二次短按双击快捷功能(默认关闭)

门槛值设计的意图:KEY_SHORT_CNT = 3 同时充当软件消抖——物理抖动通常小于 30ms,连续 3 次采样一致才认为按键稳定按下;KEY_LONG_CNT = 75 把长按与短按清晰分隔(750ms 足够用户区分两种操作);KEY_HOLD_CNT = 15 是长按后连按的重复周期,过长会感觉迟钝、过短会误触发重复。各模式可自行调整这些宏以适配产品手感。

扫描状态机 key_scan()

key_scan() 是整个按键子系统的核心,使用三个 static 变量跨周期保持状态:

static u8 key_type = NO_KEY;        ///<按键类型
static u8 last_key = NO_KEY;
static u8 key_press_counter = 0;

Source: key.c

每次调用流程:先处理可选的双击计时 → 读取当前键值 cur_key → 与 last_key 比较,分两大分支:

分支一:cur_key == last_key(键值不变,持续按住)——计数递增,命中以下阈值时产生事件:

if (cur_key == last_key) {                          //长时间按键
    if (cur_key == NO_KEY) {
        return;
    }
    key_press_counter++;
    if (key_press_counter == KEY_SHORT_CNT) {
        key_status = KEY_SHORT;
    } else if (key_press_counter == KEY_LONG_CNT) {      //长按
        key_status = KEY_LONG;
    } else if (key_press_counter == (KEY_LONG_CNT + KEY_HOLD_CNT)) {    //连按
        key_status = KEY_HOLD;
        key_press_counter = KEY_LONG_CNT;
    } else {
        return;
    }
}

Source: key.c

注意 KEY_HOLD 分支把计数器重置回 KEY_LONG_CNT:此后每 KEY_HOLD_CNT 个周期再次触发 KEY_HOLD,形成"长按后持续重复"的事件流(这正是音量连续加减的实现基础),直到用户抬手。

分支二:cur_key != last_key(键值变化,通常是抬键)——根据释放前累计的计数判定短按抬起或长按抬起:

} else { //cur_key = NO_KEY, 抬键
    last_key = cur_key;
    if ((key_press_counter > KEY_SHORT_CNT) && (key_press_counter < KEY_LONG_CNT) && (cur_key == NO_KEY)) {   //短按抬起
        key_press_counter = 0;
        key_status = KEY_SHORT_UP;
    } else if ((cur_key == NO_KEY) && (key_press_counter >= KEY_LONG_CNT)) { //长按抬起
        key_press_counter = 0;
        key_status = KEY_LONG_UP;
    } else {
        key_press_counter = 0;
        return;
    }
}

Source: key.c

该分支的设计要点:若按下期间键值发生变化但又不是抬键(例如误触到另一颗键),计数清零并直接返回,不产生任何事件,避免在按键切换瞬间误报。

状态机判定逻辑可概括为:

flowchart TD
    Start(["key_scan() 每 10ms"]) --> Read["get_key_value()"]
    Read -->|"返回 -1 读取失败"| R1["return 放弃本次扫描"]
    Read -->|"成功"| Cmp{"cur_key == last_key ?"}
    Cmp -->|"是 且 NO_KEY"| R2["return 无按键"]
    Cmp -->|"是 且按下"| Inc["key_press_counter++"]
    Inc --> Cnt{"计数命中?"}
    Cnt -->|"KEY_SHORT_CNT(3)"| S["KEY_SHORT 短按"]
    Cnt -->|"KEY_LONG_CNT(75)"| L["KEY_LONG 长按"]
    Cnt -->|"LONG+HOLD(90)"| H["KEY_HOLD 连按<br/>counter 重置回 75"]
    Cmp -->|"否 键值变化"| Rel{"cur_key == NO_KEY ?"}
    Rel -->|"是 且 3<counter<75"| SU["KEY_SHORT_UP 短按抬起"]
    Rel -->|"是 且 counter>=75"| LU["KEY_LONG_UP 长按抬起"]
    Rel -->|"否 换键误触"| R3["counter=0, return"]
    S --> Em["key2msg_emit(status, key_num, key_type)"]
    L --> Em
    H --> Em
    SU --> Em
    LU --> Em

事件产生顺序与业务语义:短按抬起 / 长按 → 连按。也就是说,短按动作在"抬起"时才上报(配合消抖后按下 30ms、抬起判定的时间窗),长按动作在按下 750ms 时上报一次、之后每 150ms 重复一次;应用层通常以 KEY_SHORT_UP 执行单击功能、以 KEY_HOLD 执行连续调节,二者互不干扰。

驱动层注册与轮询

key.c 通过编译期宏裁剪的 key_list[] 数组聚合所有使能的驱动:

static const key_interface_t *key_list[] = {
#if KEY_IO_EN
    &key_io_info,
#endif
#if KEY_AD_EN
    &key_ad_info,
#endif
#if KEY_MATRIX_EN
    &key_matrix_info,
#endif
#if KEY_IR_EN
    &key_ir_info,
#endif
#if KEY_TOUCH_EN
    &key_touch_info,
#endif
};

Source: key.c

初始化入口 key_init() 逐个调用驱动实例的 key_init:

void key_init(void)
{
    key_puts("key init\n");
#if (KEY_IO_EN || KEY_AD_EN || KEY_MATRIX_EN || KEY_IR_EN || KEY_TOUCH_EN)
    for (int i = 0; i < (sizeof(key_list) / sizeof(key_list[0])); i++) {
        if (key_list[i]->key_init) {
            key_list[i]->key_init();
        }
    }
#endif
}

Source: key.c

轮询入口 get_key_value() 按数组顺序遍历驱动,返回第一个有效键值;某驱动返回 -1 表示未就绪,立即放弃本次扫描:

bool get_key_value(key_io_t *p_key)
{
    u8 i;
    p_key->key_type = NO_KEY;
    for (i = 0; i < (sizeof(key_list) / sizeof(key_list[0])); i++) {
        if (key_list[i]->key_get_value) {
            u32 key = key_list[i]->key_get_value();
            if (-1 == key) {
                return false;
            }
            p_key->key_num = key_list[i]->key_get_value();
            if (NO_KEY != p_key->key_num) {
                p_key->key_type = key_list[i]->key_type;
                return true;
            }
        }
    }
    p_key->key_num = NO_KEY;
    return true;
}

Source: key.c

这种"数组 + 宏裁剪"的设计让添加新按键类型只需两步:写驱动、加进数组;未使能的驱动连代码都不链接,ROM 占用最小。

IO 按键驱动示例(key_drv_io)

IO 按键驱动展示了一个典型的驱动层实现:读取 GPIO 电平判断按下,初始化时配置上下拉。AD23N 与早期平台(AD24)的差异点在于:AD23N 用 2 bit 控制一个 IO 的上下拉电阻,上下拉各自集成在同一个寄存器中,寄存器 bit 与 io_index 的对应关系为 bit0 = io_index * 2, bit1 = io_index * 2 + 1:

//AD23需要2bit控制IO上下拉电阻,并且上下拉各自集成在一个寄存器
//所以寄存器bit和io_index对应关系bit0 = io_index * 2, bit1 = io_index * 2 + 1
//比如需要设置PA3的上拉电阻,则配置SFR(JL_PORTA->PU, 3 * 2,  2,  0b01)
#define KEY0_IO_INDEX  10
#define KEY1_IO_INDEX  11
#define KEY2_IO_INDEX  12

#define IS_KEY0_DOWN()    \t(!(JL_PORTA->IN & BIT(KEY0_IO_INDEX)))
#define IS_KEY1_DOWN()    \t(!(JL_PORTA->IN & BIT(KEY1_IO_INDEX)))
#define IS_KEY2_DOWN()    \t(!(JL_PORTA->IN & BIT(KEY2_IO_INDEX)))


#define KEY_INIT()        do{\
                            /**key0 init*/\
                            SFR(JL_PORTA->PU, KEY0_IO_INDEX * 2,  2,  0b01),\
                            SFR(JL_PORTA->PD, KEY0_IO_INDEX * 2,  2,  0b00),\
                            JL_PORTA->DIE |= BIT(KEY0_IO_INDEX),\
                            JL_PORTA->DIR |= BIT(KEY0_IO_INDEX);\
                            ...
                            }while(0)
extern const key_interface_t key_io_info;

Source: key_drv_io.h

要点:

  • IS_KEYx_DOWN() 用低电平有效(!(IN & BIT))表示按键按下,这是按键通常接 GND 的硬件习惯;
  • SFR(JL_PORTA->PU, idx*2, 2, 0b01) 配置上拉、SFR(JL_PORTA->PD, idx*2, 2, 0b00) 关闭下拉,DIE 使能数字输入,DIR 置为输入方向;
  • 驱动文件导出 const key_interface_t key_io_info,内部把 IS_KEYx_DOWN() 组合成键号返回,对上层完全屏蔽寄存器细节。

其余驱动同理:key_drv_ad.c 通过 ADC 采样分压电阻网络识别多键(AD 键位共享一个 ADC 通道,不同电阻对应不同键号);key_matrix.c 通过行列扫描识别矩阵键盘;key_ir.c 解码红外遥控码;key_touch.c 读取触摸检测结果。

按键 → 消息转换

扫描状态机产生的 (key_status, key_num, key_type) 三元组由 key2msg_emit() 统一出口处理:

static void key2msg_emit(u8 key_status, u8 key_num, u8 key_type)
{
    key_printf("key_status:%d key_num:%d key_type:%d \n", key_status, key_num, key_type);

    if (key_msg_filter) {
        u16 msg = key_msg_filter(key_status, key_num, key_type);
        if (msg != NO_MSG) {
            d_key_voice_kick();
            int ret = post_msg(1, msg);
            if (ret != 0) {
                key_printf("error !!! msg pool full \n");
            }
        }
    }
}

Source: key.c

链路语义:

  1. key_msg_filter 是当前模式注册的消息过滤器(函数指针,默认 NULL,未注册时按键事件被丢弃);
  2. 过滤器返回 NO_MSG 表示该按键在该状态无动作,链路直接终止;
  3. 有动作时先 d_key_voice_kick() 触发按键音,再 post_msg(1, msg) 投递到消息队列;
  4. 投递失败(队列满)打印 error !!! msg pool full,本次按键消息丢弃——扫描状态机本身不受影响,下一周期继续工作。

过滤器由 key_table_sel() 动态切换,切换过程关中断保护,避免在中断上下文读到半更新指针:

void key_table_sel(void *msg_filter)
{
    local_irq_disable();
    key_msg_filter = msg_filter;
    local_irq_enable();
}

Source: key.c

模式按键表(查表映射)

应用层以"按键号 → 消息"的查表宏定义每个模式下每个按键的含义。以音乐模式的红外键表为例,IRFF00_MUSIC_SHORT_UP 定义 21 个 IR 键(IR_KEY_MAX_NUM = 21)在短按抬起时的动作:

#define IRFF00_MUSIC_SHORT_UP		\
                                /*00*/    MSG_PP,\
                                /*01*/    MSG_NEXT_WORKMODE,\
                                /*02*/    MSG_MUTE,\
                                /*03*/    MSG_PP,\
                                /*04*/    MSG_PREV_FILE,\
                                /*05*/    MSG_NEXT_FILE,\
                                /*06*/    MSG_DEC_EQ,\
                                /*07*/    MSG_VOL_DOWN,\
                                /*08*/    MSG_VOL_UP,\
                                /*09*/    MSG_0,\
                                /*10*/    MSG_NEXT_PLAYMODE,\
                                /*11*/    MSG_NEXT_DEVICE,\
                                /*12*/    MSG_1,\
                                ...
                                /*20*/    MSG_9

Source: music_key_table.c

同一文件还定义了 IRFF00_MUSIC_LONG、IRFF00_MUSIC_HOLD、IRFF00_MUSIC_LONG_UP 等状态表:长按/连按通常只对音量与快进退有效(如 MSG_VOL_UP、MSG_MUSIC_FF),其余键位填 NO_MSG 保持静默。这种"每状态一张表"的结构与 key_scan 的 6 种事件一一对应,并且按键表用 #pragma 分段指令放入独立的 .music_key_table.* 链接段,便于厂商固件分区管理。

各模式(音乐 / 录音 / 收音 / 对箱 / linein / 等)均有自己的 *_mode_key.c 与按键表,模式切换时调用 key_table_sel() 换表,实现同一颗物理按键在不同模式下复用。

核心流程

sequenceDiagram
    participant T as 系统定时器 (10ms)
    participant S as key_scan()
    participant G as get_key_value()
    participant D as 按键驱动 key_get_value()
    participant E as key2msg_emit()
    participant F as key_msg_filter 查表
    participant Q as post_msg() 消息队列

    T->>S: 周期调用
    S->>G: 读取当前键值
    G->>D: 遍历 key_list[] 查询各驱动
    D-->>G: key_num / key_type (或 -1)
    G-->>S: key_io_t
    S->>S: 状态机判定事件<br/>(短按/长按/连按/抬起)
    S->>E: key2msg_emit(status, num, type)
    E->>F: msg = key_msg_filter(...)
    F-->>E: MSG_xxx 或 NO_MSG
    E->>Q: post_msg(1, msg)
    Q-->>E: 0=成功 / 非0=队列满

端到端时序(以"短按播放键一次"为例):

  1. 10ms 定时器触发 key_scan();
  2. 连续 3 个周期读到同一键号,第 3 个周期产生 KEY_SHORT(此时按键已消抖确认);
  3. 用户抬手,下一周期键值变 NO_KEY,计数器位于 (3, 75) 区间 → 产生 KEY_SHORT_UP;
  4. key2msg_emit(KEY_SHORT_UP, key_num, key_type) 调用音乐模式的过滤器,查 IRFF00_MUSIC_SHORT_UP 表得到 MSG_PP;
  5. 触发按键音 d_key_voice_kick(),post_msg(1, MSG_PP) 入队;
  6. 音乐模式消息循环收到 MSG_PP,执行播放/暂停切换。

若按住不放:750ms 时产生一次 KEY_LONG,之后每 150ms 产生一次 KEY_HOLD,抬起时产生 KEY_LONG_UP——同一颗键在一条时间线上自然形成"短按、长按、连按"三种操作。

配置选项

按键子系统全部通过编译期宏配置,无运行时配置项。宏定义位置:key.h(门槛值、最大键数)与 app_config.h(类型使能开关)。

宏类型默认值说明
KEY_IO_EN宏开关由 app_config.h 定义使能 IO 按键驱动,参与编译
KEY_AD_EN宏开关由 app_config.h 定义使能 AD(ADC 分压)按键驱动
KEY_MATRIX_EN宏开关由 app_config.h 定义使能矩阵键盘驱动
KEY_IR_EN宏开关由 app_config.h 定义使能红外遥控驱动
KEY_TOUCH_EN宏开关由 app_config.h 定义使能触摸按键驱动
KEY_DOUBLE_CLICK_EN宏开关0双击使能;置 1 后还需定义对应 DOUBLE_KICK 表
KEY_BASE_CNT计数2按键基础计数值(消抖前预留)
KEY_SHORT_CNT计数3短按判定阈值,兼作软件消抖(≈30ms)
KEY_LONG_CNT计数75长按判定阈值(≈750ms)
KEY_HOLD_CNT计数15长按后连按重复周期(≈150ms)
KEY_DOUBLE_CLICK_CNT计数35双击判定窗口(≈350ms),仅 KEY_DOUBLE_CLICK_EN 时生效
IO_KEY_MAX_NUM常量10IO 按键最大个数
AD_KEY_MAX_NUM常量10AD 按键最大个数
MATRIX_KEY_MAX_NUM常量10矩阵按键最大个数
IR_KEY_MAX_NUM常量21红外按键最大个数(与遥控器键位表对应)
TOUCH_KEY_MAX_NUM常量10触摸按键最大个数
NO_KEY常量0xff"无按键"哨兵值
KEY_UART_DEBUG宏开关关闭打开后 key_puts/key_printf/key_buf 映射到 log_info,输出扫描与投递日志

Source: key.h

调整建议:手感调校只需改 KEY_SHORT_CNT(消抖灵敏度)、KEY_LONG_CNT(长按时长)、KEY_HOLD_CNT(连按速率)三个值;量产不同硬件平台时通过 app_config.h 开关按键类型,无需改动 key.c。

API 参考

void key_init(void)

初始化所有使能的按键驱动。系统上电阶段调用一次。

说明:遍历 key_list[],调用每个驱动实例的 key_init 函数指针;未使能的驱动不参与编译。

void key_scan(void)

按键扫描状态机入口,需按固定周期(10ms)重复调用。

说明:内部使用 static 变量跨周期保持状态(last_key、key_press_counter、key_type),通过计数判定产生 KEY_SHORT / KEY_SHORT_UP / KEY_LONG / KEY_HOLD / KEY_LONG_UP / KEY_DOUBLE 事件。不可重入,必须在单一上下文(系统定时器或主循环)中周期调用。

void key_table_sel(void *msg_filter)

注册/切换按键消息过滤器。

参数:msg_filter —— 函数指针 u16 (*)(u8 key_status, u8 key_num, u8 key_type),返回 MSG_xxx 或 NO_MSG。传入 NULL 可屏蔽所有按键。

说明:切换过程以 local_irq_disable()/local_irq_enable() 保护,保证与中断上下文切换安全。各模式进入/退出时调用以更换按键语义表。

bool get_key_value(key_io_t *p_key)

轮询驱动层获取当前键值。

参数:p_key —— 输出参数,成功时填充 key_num 与 key_type。

返回:true 表示读取有效(key_num 可能为 NO_KEY);false 表示某驱动返回 -1(未就绪),本次扫描应放弃。

static void key2msg_emit(u8 key_status, u8 key_num, u8 key_type)

按键事件出口:查表 → 触发按键音 → 投递消息。

参数:key_status 为 6 种事件之一;key_num 为键号;key_type 为按键类型。

副作用:过滤器返回非 NO_MSG 时调用 d_key_voice_kick() 播放按键音,并 post_msg(1, msg) 入队;队列满时打印 error !!! msg pool full 并丢弃该消息。

typedef int (*key_emit_t)(u8 key_status, u8 key_num, u8 key_type)

按键输出注册接口类型(key.h 中定义)。当前框架内部通过静态 key2msg_emit 实现,该类型为上层预留的自定义输出通道。

失败模式、边界与并发

消息队列满(按键消息丢失)

post_msg(1, msg) 返回非 0 时打印 error !!! msg pool full 并丢弃本次消息。这是按键链路唯一的"数据丢失"点:快速连按超过消息队列吞吐时,中间事件可能被丢弃,但扫描状态机状态不受影响,后续按键仍正常上报。应用层应把关键操作放在 KEY_SHORT_UP(单击)而非 KEY_SHORT(按下沿),因为抬起事件天然对操作做了节流。

驱动读取失败(-1)

get_key_value() 中任一驱动返回 -1 即放弃本次扫描(return false),key_scan() 直接返回。这在驱动初始化未完成或 ADC/红外解码器忙时保护状态机不被脏数据污染;下一个 10ms 周期自动恢复。

按键切换误触

按下 A 未抬起又触发 B(cur_key != last_key 且 cur_key != NO_KEY):计数器清零并返回,不产生任何事件。设计意图是:换键瞬间的物理状态不可信,宁可漏报一次也不误报,保证单键语义的纯粹性。

长按重复与抬起竞态

KEY_HOLD 触发时计数器被重置回 KEY_LONG_CNT,因此只要不抬手,KEY_HOLD 会以 KEY_HOLD_CNT(150ms)周期无限重复;抬手瞬间计数器 >= KEY_LONG_CNT 触发 KEY_LONG_UP。若在长按与连按之间(counter ∈ [75, 90))抬手,仍归为长按抬起,不会产生歧义。

双击窗口(默认关闭)

KEY_DOUBLE_CLICK_EN = 0 时双击逻辑不编译。置 1 后,第一次 KEY_SHORT_UP 进入等待窗口(KEY_DOUBLE_CLICK_CNT ≈ 350ms):窗口内第二次短按产生 KEY_DOUBLE 并清窗口;窗口超时则补发第一次的 KEY_SHORT_UP。代价是单击响应被延迟最多 350ms,因此默认关闭,产品需要双击功能时需权衡手感。

并发与中断安全

  • key_scan() 依赖 static 状态变量,不可重入,必须保证 10ms 周期内单上下文调用(典型实现是系统定时器回调或主循环,二者选一);
  • key_table_sel() 用 local_irq_disable()/enable() 保护函数指针的赋值,防止中断上下文(如 key_scan 运行于定时器中断)读到半更新的过滤器指针;
  • key2msg_emit 中 key_msg_filter 的读取未加锁——由于 key_scan 与 key_table_sel 通常在同一中断优先级上下文切换,实际不会并发,但若把 key_scan 移入低优先级任务则需自行加锁。

性能与运维

  • 轮询开销:10ms 一次 key_scan,每次最多遍历 5 个驱动、各调用一次 key_get_value,均为常数时间,对 MCU 负载可忽略;
  • ROM 优化:所有按键类型、双击逻辑、调试日志均通过编译期宏裁剪,未使能功能零开销;按键表经 #pragma 放入独立链接段(如 .music_key_table.text.const),便于厂商分区分包;
  • 现场调试:打开 KEY_UART_DEBUG 后,key2msg_emit 会打印 key_status / key_num / key_type 三元组,可快速定位"按键识别正确但无动作"(查表问题)与"状态机乱序"(干扰/抖动问题);
  • 手感调校路径:先调 KEY_SHORT_CNT 消除抖动误触,再调 KEY_LONG_CNT 区分长短按,最后调 KEY_HOLD_CNT 校准连按速率;每次改动后需验证各状态事件在串口日志中的时序。

扩展点

新增按键类型(如霍尔、编码器)

  1. 在 KEY_TYPE 枚举中追加类型;
  2. 实现 key_interface_t 实例(key_init + key_get_value,键号约定 NO_KEY / -1 语义);
  3. 在 key.c 的 key_list[] 中按宏条件加入;
  4. 在 app_config.h 定义对应 KEY_xxx_EN 使能宏,并在 key.h 增加 KEY_xxx_MAX_NUM。 驱动层与状态机完全解耦,新增类型不需要改动 key_scan()。

新增/定制模式按键表

仿照 music_key_table.c:为每个事件状态定义 xxx_SHORT / SHORT_UP / LONG / HOLD / LONG_UP(双击开启时还有 DOUBLE_KICK / TRIPLE_KICK)查表宏,按"键号 → MSG_xxx 或 NO_MSG"填表,在模式入口调用 key_table_sel(过滤器) 注册。同一键在不同模式可映射不同消息,实现按键复用。

使能双击

将 KEY_DOUBLE_CLICK_EN 置 1,并在按键表中补齐 DOUBLE_KICK(双击)与 TRIPLE_KICK(三击,红外键表已预留)条目;注意单击响应会延迟一个双击窗口。

相关链接

  • key.h — 类型/状态/门槛值/接口定义
  • key.c — 扫描状态机与消息转换
  • key_drv_io.h / key_drv_io.c — IO 按键驱动
  • key_drv_ad.h / key_drv_ad.c — AD 按键驱动
  • key_matrix.h / key_matrix.c — 矩阵键盘驱动
  • key_ir.h — 红外遥控驱动
  • key_touch.h — 触摸按键驱动
  • music_key_table.c — 音乐模式按键表示例
  • key_wakeup.c — 按键唤醒(电源域,另见电源管理页面)

相关主题页面:消息系统与消息队列(post_msg / MSG_xxx 处理)、各音频工作模式(音乐 / 录音 / 对箱等模式对按键消息的业务处理)、电源管理与唤醒(低功耗下按键唤醒路径)。

Prev
消息机制与事件分发
Next
电源管理与低功耗控制