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

    • AD16N 系列芯片与 SDK 能力总览
  • 快速开始

    • 环境搭建与编译工具链
    • 编译构建指南
    • 烧录与固件升级
  • SDK 工程架构

    • SDK 目录结构与模块分层
    • 构建系统与批处理工具
    • BSP 板级支持包
  • mbox_flash 小音箱应用

    • 应用初始化与启动流程
    • 应用配置系统
    • 按键、UI 与用户交互
  • 音频子系统

    • 音频解码框架与调度
    • 音频格式解码器实现
    • MIDI 合成与播放
    • 音频编码与录音
    • EQ/DRC 与音效处理
    • DAC/ADC 音频接口与采样
  • 存储与文件系统

    • 媒体 IO 抽象层 MIO
    • 存储设备驱动
    • 文件系统支持
  • 平台系统库

    • 系统基础服务
    • CPU 平台与运行库
    • 固件升级与更新机制
    • 蓝牙与扩展连接接口
  • 电源与低功耗管理

    • 电源管理与低功耗设计
    • 锂电池充电管理
  • 硬件与文档参考

    • SDK 文档中心与版本发布记录
    • 芯片数据手册与硬件设计参考

按键、UI 与用户交互

本文档介绍 fw-AD16N_GP-MCU_SDK 中按键扫描、事件识别、按键到 UI 消息的映射,以及低功耗与提示音联动的完整用户交互机制,覆盖从硬件驱动采样到应用层消息消费的端到端链路。

Purpose and Scope

本页面向 mbox_flash 应用(含 linein、loudspeaker、midi_dec、midi_keyboard 等模式)的按键与用户交互子系统,重点说明:

  • 按键驱动抽象层(IO / AD / 矩阵 / MIC / 红外 / 触摸 / 低功耗触摸),通过统一的 key_interface_t 接口接入;
  • 按键核心状态机 key_scan():短按、长按、连按(HOLD)、抬键、双击、三击的判定逻辑与时间门槛;
  • 按键 → 消息转换(key2msg_emit + key_msg_filter)以及各模式按键映射表(如 loudspk_mode_key.c 中的 IR 按键表);
  • 消息投递(post_msg)与消息池满的处理、按键提示音(d_key_voice_kick)、低功耗目标(key_lp_target)联动。

以下主题属于兄弟页面,不在本页展开:具体模式的应用逻辑(如 loudspeaker 播放控制)、显示/UI 渲染框架、消息系统(msg.h)本身的设计。相关概念见"Related Links"。

概述

在音频嵌入式应用中,用户交互几乎全部通过按键与红外遥控器完成:单击播放/暂停、双击切歌、长按快进/快退、连按连续调音量等。SDK 将这些交互拆成三层:

  1. 驱动层:各 key_drv_*.c 负责从具体硬件(GPIO、ADC、矩阵、MIC、IR、触摸)读取键值,屏蔽硬件差异;
  2. 核心层:key.c 的 key_scan() 在 10ms 系统 tick 中被周期调用,对键值做去抖计数与时间窗判定,输出标准化的按键状态(KEY_SHORT_UP、KEY_LONG、KEY_HOLD、KEY_DOUBLE 等),并通过注册的过滤函数把"状态 + 键值 + 按键类型"翻译成业务消息;
  3. 应用层:各模式的按键映射表决定"哪个键、什么操作"对应哪条 MSG_* 消息,消息进入系统消息队列后由各模式 UI/逻辑任务消费,最终驱动界面显示、声音提示和功能执行。

这种分层设计的意图是:按键语义与硬件解耦——同一套状态机可以服务于 IR 遥控器、IO 按键、触摸按键;模式间按键定义可切换——key_table_sel() 可在模式切换时更换过滤函数(即更换映射表),而无需改动驱动与状态机。

架构

flowchart TD
    subgraph sg_HW["硬件层"]
        HW_IO["IO 按键<br/>key_drv_io.c"]
        HW_AD["AD 按键<br/>key_drv_ad.c"]
        HW_MATRIX["矩阵按键<br/>key_drv_matrix.c"]
        HW_MIC["MIC 按键<br/>key_drv_mic.c"]
        HW_IR["红外遥控<br/>key_ir.c"]
        HW_TOUCH["触摸<br/>key_touch.c / key_lptouch.c"]
    end

    subgraph sg_CORE["按键核心层 (bsp/common/key)"]
        KEYLIST["key_list[]<br/>key_interface_t 接口数组"]
        SCAN["key_scan()<br/>10ms tick 状态机"]
        FILTER["key_msg_filter<br/>按键→消息过滤函数"]
        EMIT["key2msg_emit()<br/>提示音 + post_msg"]
    end

    subgraph sg_APP["应用层 (src/mbox_flash)"]
        TAB_LOUD["loudspk_mode_key.c<br/>IR/IO 映射表"]
        TAB_LINEIN["linein_mode_key.c"]
        TAB_MIDI["midi_dec / midi_keyboard<br/>mode_key.c"]
        MSGQ["系统消息队列 post_msg"]
        APP["模式 UI / 逻辑任务<br/>消费 MSG_* 消息"]
    end

    subgraph sg_PM["低功耗联动"]
        LP["key_lp_target<br/>key_idle_query()"]
        VOICE["d_key_voice_kick()<br/>按键提示音"]
    end

    HW_IO --> KEYLIST
    HW_AD --> KEYLIST
    HW_MATRIX --> KEYLIST
    HW_MIC --> KEYLIST
    HW_IR --> KEYLIST
    HW_TOUCH --> KEYLIST
    KEYLIST --> SCAN
    SCAN --> FILTER
    FILTER --> EMIT
    TAB_LOUD --> FILTER
    TAB_LINEIN --> FILTER
    TAB_MIDI --> FILTER
    EMIT --> VOICE
    EMIT --> MSGQ
    MSGQ --> APP
    SCAN --> LP
    KEYLIST --> LP

架构要点:

  • key_list[] 是驱动注册表,各驱动以 key_interface_t(含 key_init 与 key_get_value)形式静态注册,由编译宏(KEY_IO_EN 等)决定是否编入,见 key.c;
  • key_scan() 是唯一的状态机入口,由系统 tick 定时器驱动(tick_timer_sys.c 中调用 key_scan()),见 tick_timer_sys.c;
  • key_msg_filter 是一个可热切换的函数指针:key_table_sel() 在模式切换时被调用来更换映射表,从而实现"同一颗芯片、不同模式、不同按键语义";
  • 映射后的消息经 post_msg() 进入系统消息队列,模式逻辑任务统一消费,UI 显示与按键事件解耦。

核心机制

驱动抽象:key_interface_t 与 key_list

所有按键驱动暴露同一接口,核心层通过接口数组轮询,无需感知具体硬件:

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

Source: key.h

KEY_TYPE 枚举定义了七种按键来源(IO、AD、MATRIX、IR、TOUCH、MIC、LPTOUCH),见 key.h。key_init() 遍历 key_list 逐个调用驱动的初始化函数;get_key_value() 则按数组顺序轮询,返回第一个有效键值,因此数组顺序即扫描优先级,见 key.c 与 key.c。

按键状态机:key_scan()

key_scan() 在 10ms tick 中执行,用静态变量 last_key、key_press_counter 记录按键状态。判定阈值定义在 key.h:

宏值含义
KEY_BASE_CNT2基础去抖计数
KEY_SHORT_CNT3短按判定阈值(3 × 10ms)
KEY_LONG_CNT75长按判定阈值(75 × 10ms)
KEY_HOLD_CNT15连按(HOLD)重复间隔

状态机核心逻辑(按下保持计数 → 释放判定):

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;
    }
} 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

设计意图:事件发生在"按下满 N 个 tick"与"抬键"两个时刻——KEY_SHORT 用于按下 30ms 时给 UI 的即时反馈;KEY_SHORT_UP 才是短按的"完成"语义;KEY_LONG 触发一次后进入 KEY_HOLD 周期重复,配合 key_press_counter = KEY_LONG_CNT 实现连发;KEY_LONG_UP 表示长按释放。事件产生顺序为"短按抬起/长按 → 连按",注释见 key.c。

按键状态枚举(KEY_STATUS)定义于 key.h:KEY_SHORT(0x0)、KEY_SHORT_UP(0x1)、KEY_LONG(0x2)、KEY_HOLD(0x3)、KEY_LONG_UP(0x4)、KEY_DOUBLE(0x5)、KEY_TRIPLE(0x6)。

双击 / 三击检测

KEY_DOUBLE_CLICK_EN 开启后,key_scan() 在短按抬起时进入"等待第二次点击"窗口:key_press_flag 计数,超过 KEY_DOUBLE_CLICK_CNT(35 × 10ms = 350ms)窗口后按累计 click_cnt 发射 KEY_SHORT_UP(单击)/ KEY_DOUBLE(双击)/ KEY_TRIPLE(三击),见 key.c 与 key.c。窗口内若换了键,则先补发前一键的单击事件,再以新键重新计数。MIC 按键通过 d_check_suport_double_click(k) 宏(mic_key_check_support_double_click)支持按键过滤,其余按键类型默认支持,见 key.c。

按键 → 消息转换

key2msg_emit:事件出口

所有被状态机判定出来的按键事件统一汇聚到 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

执行顺序即设计顺序:先查表(key_msg_filter 返回映射消息)→ 非 NO_MSG 才播提示音(d_key_voice_kick())→ 投递消息(post_msg(1, msg),1 为消息优先级)。NO_MSG(0xff)表示"该键/该状态无动作",不投递任何消息。post_msg 返回非 0 表示消息池已满,按键事件被丢弃并打印错误,避免阻塞中断/定时器上下文。

key_table_sel:模式切换映射表

映射函数指针 key_msg_filter 是静态变量,key_table_sel() 通过关中断保护地热切换它:

static u16(*key_msg_filter)(u8, u8, u8) = NULL;
...
void key_table_sel(void *msg_filter)
{
    local_irq_disable();
    key_msg_filter = msg_filter;
    local_irq_enable();
}

Source: key.c 与 key.c

模式切换时(如从 loudspeaker 切到 linein),应用调用 key_table_sel(新模式的过滤函数),此后所有按键事件立即按新模式解释——驱动层与状态机完全复用。

模式按键映射表:以 loudspeaker 为例

每个模式目录下的 xxx_mode_key.c 定义若干"键值数组",数组下标即键号,元素即 MSG_* 消息。loudspeaker 模式为 IR 遥控器定义了五张表:短按、长按、连按、长按释放、双击,见 loudspk_mode_key.c:

#define IRFF00_LOUDSPK_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_HW_EQ_SW,\
								/*07*/    MSG_VOL_DOWN,\
								/*08*/    MSG_VOL_UP,\
								/*09*/    MSG_0,\
                                /*10*/    MSG_NEXT_PLAYMODE,\
								/*11*/    MSG_NEXT_DEVICE,\
								/*12*/    MSG_1,\
								/*13*/    MSG_2,\
								/*14*/    MSG_3,\
								/*15*/    MSG_4,\
								/*16*/    MSG_5,\
								/*17*/    MSG_6,\
								/*18*/    MSG_7,\
								/*19*/    MSG_8,\
								/*20*/    MSG_9

Source: loudspk_mode_key.c

短按表把 21 个 IR 键(0~20)映射到播放/暂停(MSG_PP)、模式切换(MSG_NEXT_WORKMODE)、静音(MSG_MUTE)、上下曲(MSG_PREV_FILE/MSG_NEXT_FILE)、EQ 切换(MSG_HW_EQ_SW)、音量(MSG_VOL_UP/MSG_VOL_DOWN)与数字键 0~9。长按表(IRFF00_LOUDSPK_LONG)则把键 4/5 映射为 MSG_MUSIC_FR/MSG_MUSIC_FF(快退/快进),键 7/8 映射为音量连续调节;连按表(IRFF00_LOUDSPK_HOLD)复用同样的快进快退与音量消息实现连续动作;长按释放表(IRFF00_LOUDSPK_LONG_UP)与双击表(IRFF00_LOUDSPK_DOUBLE_KICK)在 loudspeaker 模式下大多为 NO_MSG,见 loudspk_mode_key.c。

同目录下 linein_mode_key.c、midi_dec_mode_key.c、midi_keyboard_mode_key.c 采用相同模式定义各自模式下的按键语义(见仓库文件清单),本页不再逐一展开。

核心流程

一次完整的按键交互从硬件采样到 UI 执行的时序如下:

sequenceDiagram
    participant TICK as tick_timer_sys (10ms)
    participant DRV as key_drv_* / get_key_value
    participant SCAN as key_scan() 状态机
    participant FILTER as key_msg_filter 映射表
    participant EMIT as key2msg_emit()
    participant VOICE as d_key_voice_kick()
    participant MSGQ as 消息队列 post_msg
    participant APP as 模式 UI/逻辑任务

    TICK->>SCAN: 每 10ms 调用 key_scan()
    SCAN->>DRV: get_key_value() 轮询 key_list
    DRV-->>SCAN: key_num / key_type
    SCAN->>SCAN: 计数判定: SHORT/LONG/HOLD/UP/DOUBLE
    SCAN->>FILTER: key_msg_filter(status, num, type)
    FILTER-->>EMIT: MSG_* 或 NO_MSG
    alt msg != NO_MSG
        EMIT->>VOICE: d_key_voice_kick() 按键音
        EMIT->>MSGQ: post_msg(1, msg)
        MSGQ-->>APP: 消息被模式任务取出
        APP->>APP: 执行功能 + 刷新 UI 显示
    else msg == NO_MSG
        EMIT-->>EMIT: 丢弃,无动作
    end

关键路径说明:

  1. 系统 10ms tick 定时器驱动 key_scan()(见 tick_timer_sys.c),因此所有时间门槛(3/75/15/35 tick)的单位都是 10ms;
  2. 按下期间 is_key_active 被置 35 并递减,作为"系统忙"标志供低功耗查询;
  3. 状态机只在"阈值到达"与"抬键"两个时刻产生事件,避免高频轮询带来的消息风暴;
  4. 映射表把事件翻译为业务消息,UI 任务只认 MSG_*,不关心按键来源——同一 MSG_PP 可由 IR 键 0/3、IO 键或触摸键触发。

按键状态机

stateDiagram-v2
    [*] --> IDLE
    IDLE --> PRESSING: 检测到键值 cur_key != NO_KEY
    PRESSING --> SHORT: 计数 == KEY_SHORT_CNT (3)
    PRESSING --> SHORT_UP: 释放且 3 < 计数 < 75
    PRESSING --> LONG: 计数 == KEY_LONG_CNT (75)
    LONG --> HOLD: 计数 == LONG_CNT + HOLD_CNT (90),周期重复
    LONG --> LONG_UP: 释放且计数 >= 75
    SHORT_UP --> WAIT_DCLICK: KEY_DOUBLE_CLICK_EN 且支持双击
    WAIT_DCLICK --> ONE_CLICK: 350ms 窗口内仅 1 次
    WAIT_DCLICK --> DOUBLE: 窗口内 2 次
    WAIT_DCLICK --> TRIPLE: 窗口内 3 次
    SHORT_UP --> IDLE
    ONE_CLICK --> IDLE
    DOUBLE --> IDLE
    TRIPLE --> IDLE
    HOLD --> HOLD: 保持按下,每 15 tick 再发
    HOLD --> IDLE: 释放
    LONG_UP --> IDLE

状态转移依据 key_press_counter 与阈值宏实现,详见 key.c。

低功耗联动

按键子系统同时是低功耗管理的参与方。is_key_active 标志在按键产生事件时被刷新,空闲后递减归零:

static volatile u8 is_key_active = 0;
...
void key_active_set(P33_IO_WKUP_EDGE edge)
{
    is_key_active = 35;      //35*10Ms
}
u8 key_idle_query(void)
{
    return !is_key_active;
}
REGISTER_LP_TARGET(key_lp_target) = {
    .name = "key",
    .is_idle = key_idle_query,
};

Source: key.c 与 key.c

  • key_scan() 中检测到有效键值(key.key_type != NO_KEY)时置 is_key_active = 35(约 350ms 活动窗口),无按键时逐 tick 递减,见 key.c;
  • key_lp_target 通过 REGISTER_LP_TARGET 宏注册进低功耗框架,key_idle_query() 返回"按键是否空闲",供电源管理决定是否允许进入睡眠;P33 引脚唤醒边沿通过 key_active_set() 上报,把系统拉回活动状态。

配置选项

按键子系统的功能开关集中在 app_config.h(各模式可覆写),核心层通过条件编译裁剪驱动与特性:

配置宏类型默认行为说明
KEY_IO_EN布尔取决于板级配置使能 IO 按键驱动 key_drv_io
KEY_AD_EN布尔取决于板级配置使能 ADC 按键驱动 key_drv_ad
KEY_MATRIX_EN布尔取决于板级配置使能矩阵按键驱动 key_drv_matrix
KEY_MIC_EN布尔取决于板级配置使能 MIC 按键驱动 key_drv_mic
KEY_IR_EN布尔取决于板级配置使能红外遥控 key_ir
KEY_TOUCH_EN布尔取决于板级配置使能触摸按键 key_touch
KEY_LPTOUCH_EN布尔取决于板级配置使能低功耗触摸 key_lptouch
KEY_DOUBLE_CLICK_EN布尔视模式而定使能双击/三击检测窗口
KEY_DOUBLE_CLICK_CNT数值35(350ms)双击判定窗口,见 key.h
KEY_SHORT_CNT数值3短按阈值(tick),见 key.h
KEY_LONG_CNT数值75长按阈值(tick),见 key.h
KEY_HOLD_CNT数值15HOLD 连发间隔(tick),见 key.h
KEY_BASE_CNT数值2基础去抖计数,见 key.h
KEY_UART_DEBUG布尔关闭使能 key_puts/key_printf 调试输出,见 key.h
KEY_*_MAX_NUM数值8~21 不等各类按键最大个数(如 IR_KEY_MAX_NUM 21、LPTOUCH_KEY_MAX_NUM 8),见 key.h

说明:驱动使能宏的默认值由各板级 app_config.h 决定,本页未逐一读取板级配置;在特定板卡上以实际配置为准。

API 参考

void key_init(void)

初始化所有已使能按键驱动:遍历 key_list[],对每个接口调用 key_init() 回调。

  • 参数:无
  • 返回:无
  • 调用时机:系统启动阶段,见 key.c

void key_scan(void)

按键状态机主入口,须由 10ms tick 定时器周期调用。内部轮询 get_key_value(),完成去抖计数、长按/短按/连按/双击判定,并经由 key2msg_emit() 发射事件。

  • 参数:无
  • 返回:无
  • 注意:不可在非 tick 上下文随意调用,时间门槛以 10ms 为基准,见 key.c

void key_table_sel(void *msg_filter)

热切换按键 → 消息映射过滤函数(模式切换时使用)。关中断保护赋值,防止状态机并发读。

  • 参数:msg_filter — 形如 u16 (*)(u8 key_status, u8 key_num, u8 key_type) 的过滤函数指针
  • 返回:无,见 key.c

key_io_t get_key_value(void)

轮询 key_list[],按注册顺序返回第一个有效键值(含 key_type 与 key_num);全部无效时返回 {NO_KEY, NO_KEY}。

  • 返回:key_io_t,见 key.c

void key_active_set(P33_IO_WKUP_EDGE edge)

外部唤醒回调:将 is_key_active 置 35,通知系统按键活跃。

  • 参数:edge — P33 唤醒边沿类型
  • 返回:无,见 key.c

u8 key_idle_query(void)

低功耗查询:返回 !is_key_active,注册为 key_lp_target.is_idle。

  • 返回:1 表示按键空闲可休眠,0 表示活跃,见 key.c

使用示例

示例 1:按键状态机与事件出口(核心层)

key_scan() 判定事件后统一走 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

要点:key_msg_filter 返回 NO_MSG(0xff)表示"忽略该事件";返回有效消息则先触发按键音再投递。消息池满时打印 error !!! msg pool full 并丢弃,不阻塞调用上下文。

示例 2:模式按键映射表(应用层)

新模式定义按键语义的标准写法——以 loudspeaker 的 IR 短按表为例,数组下标即 IR 键号(0~20),元素即要投递的业务消息:

#define IRFF00_LOUDSPK_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_HW_EQ_SW,\
								/*07*/    MSG_VOL_DOWN,\
								/*08*/    MSG_VOL_UP,\
								/*09*/    MSG_0,\
                                /*10*/    MSG_NEXT_PLAYMODE,\
								/*11*/    MSG_NEXT_DEVICE

Source: loudspk_mode_key.c

设计意图:把"键号 → 消息"做成纯数据表,过滤函数只需按 key_status 选择对应数组、按 key_num 取元素,即可实现短按/长按/连按/双击各自独立且可读的按键语义。扩展新键或新动作只需改表,无需触碰驱动与状态机。

示例 3:按键接口注册(驱动层抽象)

新增一种按键硬件时,实现 key_interface_t 并在 key_list[] 中按编译宏挂接即可被核心层自动轮询:

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

Source: key.h

失败模式、边界与并发

消息池满

post_msg(1, msg) 返回非 0 时按键事件被丢弃并打印 error !!! msg pool full(见 key.c)。系统设计上按键事件频率受状态机"阈值到达才发事件"的约束(短按最多一次、HOLD 每 150ms 一次),正常情况下不会打满消息池;若出现该日志,应检查消费端任务是否阻塞。

NO_KEY 与空转

NO_KEY = 0xff 表示无按键。key_scan() 在 cur_key == NO_KEY 且持续无键时直接 return,不产生任何事件(见 key.c),保证空闲时零开销。

并发与中断安全

  • key_msg_filter 的切换用 local_irq_disable()/enable() 保护(见 key.c),防止状态机(tick 上下文)与模式切换线程并发读写指针;
  • key_scan() 的状态变量均为 static volatile(key_type、last_key、key_press_counter),确保 tick 中断与主循环读取一致(见 key.c)。

双击窗口竞争

单击事件被延迟到 KEY_DOUBLE_CLICK_CNT(350ms)窗口结束后才发出,意味着"单击响应有 350ms 延迟"是启用双击功能的固有代价;窗口内快速切换按键会补发前一键单击并重置计数(见 key.c)。若产品对单击响应时延敏感且无双击需求,应关闭 KEY_DOUBLE_CLICK_EN。

键值数量上限

各类型按键数量受限:IO/AD/矩阵/触摸各 10、IR 21、MIC 3、LPTOUCH 8(见 key.h)。驱动返回超出上限的键号将导致映射表越界,板级配置时需核对。

性能与运维

  • 轮询成本:key_scan() 每 10ms 执行一次,get_key_value() 顺序轮询各驱动;未使能的驱动通过编译宏裁剪(#if KEY_*_EN),避免无谓调用;
  • 代码段划分:key.c 通过 #pragma code_seg(".key.text") 等将按键代码独立分段(见 key.c),便于链接器做 bank/overlay 优化与低功耗代码裁剪;
  • 调试手段:打开 KEY_UART_DEBUG 可打印 key_status/key_num/key_type 与消息池满错误(见 key.h),是现场定位"按键无反应"的首选工具;
  • 低功耗:key_lp_target 的 is_idle 查询确保按键活跃期间系统不会误入睡眠,P33 唤醒边沿经 key_active_set() 恢复活动(见 key.c)。

扩展点

  1. 新增按键硬件类型:实现 key_interface_t(key_init + key_get_value),在 key_list[] 中按 #if 宏挂接,并补充 KEY_TYPE 枚举与 KEY_*_MAX_NUM(见 key.h 与 key.c);
  2. 新增交互语义:在 key_scan() 状态机中扩展状态判定并在 key.h 的 KEY_STATUS 枚举中登记新状态;
  3. 新增模式按键定义:参照 loudspk_mode_key.c 定义短按/长按/连按/双击表,模式初始化时调用 key_table_sel(过滤函数) 切换生效;
  4. 按键音定制:事件出口调用 d_key_voice_kick(),可在该接口中按 key_type/key_status 播放不同提示音。

测试情况

本仓库未在按键目录中发现独立的单元测试文件;按键行为验证主要依赖板级联调与 KEY_UART_DEBUG 输出(key_status/key_num/key_type 打印)。建议验证用例:短按/长按/连按/双击/三击各状态的时间边界(3、75、90、350 tick)、消息池满场景、模式切换后映射表生效、低功耗下按键唤醒。

Related Links

  • 按键驱动实现:sdk/apps/app/bsp/common/key/key_drv_io.c、key_drv_ad.c、key_drv_matrix.c、key_drv_mic.c、key_ir.c、key_touch.c、key_lptouch.h
  • 模式按键映射:sdk/apps/app/src/mbox_flash/loudspeaker/loudspk_mode_key.c、linein/linein_mode_key.c、midi_dec/midi_dec_mode_key.c、midi_keyboard/midi_keyboard_mode_key.c
  • 系统 tick 驱动:tick_timer_sys.c(key_scan() 调用点)
  • 消息系统:sdk/apps/app/bsp/common/include/msg.h(post_msg 与消息队列)
  • 各模式应用逻辑页(linein / loudspeaker / midi_dec / midi_keyboard):负责消费 MSG_* 并驱动 UI 显示
Prev
应用配置系统