杰理 SDK 文档中心
首页
首页
  • 概述与入门

    • 项目概览
    • 快速开始与开发环境
  • 应用与运行时

    • 应用入口与主循环
    • 按键驱动与用户消息处理
    • 消息系统
  • 固件升级

    • 双备份升级机制与状态机
    • UART 升级传输
    • 升级校验、启动信息与复位流程
  • 芯片与硬件支持

    • AC63 系列芯片 BSP 结构
    • 外设接口与驱动
    • 低功耗、RTC 与时基唤醒
  • 构建与工具

    • 构建系统与工作区
    • 烧写与量产工具
  • 参考资源

    • 数据手册与原理图
    • 双备份升级文档

按键驱动与用户消息处理

AC63 系列 SDK 中的按键驱动(key_driver)与用户消息处理机制:通过统一的 key_driver_scan 扫描框架完成按键消抖、短按/长按/连按/抬起判定,并将按键事件映射为消息(msg_put_fifo)投递到消息队列,由用户主循环(get_msg)取出并分发处理。

Purpose and Scope

本页面向 AC63 系列(AC632N / AC635N / AC636N / AC638N)BSP 中的按键驱动与用户消息处理能力,覆盖:

  • struct key_driver_para 按键驱动参数模型与回调机制;
  • key_driver_scan 扫描、消抖(filter)与长按/连按(press_cnt / long_time / hold_time)判定算法;
  • IO 按键驱动的 GPIO 初始化、取值与消息映射(io_key_msg_table);
  • 按键事件到用户消息的转换(msg_put_fifo)与主循环消息分发(get_msg);
  • 按键 + 消息联调测试 demo(key_msg_test_demo)。

以下内容属于兄弟页面/其他能力,本页不展开:消息队列机制本身的实现细节(msg.h / msg.c,SDK 基础组件)、AC638N 的低功耗触摸按键驱动(lp_touch_key.c,属于另一套按键采集硬件)、以及具体产品按键布局与功能定义。

Overview

嵌入式设备中按键是最常见的用户输入手段,但其原始信号充满抖动,且用户操作意图(短按、长按、按住不放、松开)需要按时间维度区分。AC63 SDK 为此提供了一套参数化按键驱动框架:开发者只需填充一个 struct key_driver_para 结构体,指定"如何读取按键值"(get_value)、"如何初始化"(key_init)与"如何处理事件"(key_process),框架便以 10ms 周期扫描的方式自动完成:

  1. 消抖:连续 filter_time 次采样一致才认为按键状态有效,避免机械抖动误触发;
  2. 时间分级:以 press_cnt 累加计数与 long_time、hold_time 对比,区分短按、长按、连按(HOLD)与抬起;
  3. 消息化:key_process 回调中通过 io_key_msg_table 把"键值 × 按键状态"映射为消息号,经 msg_put_fifo 写入 FIFO 消息队列;
  4. 用户处理:主循环调用 get_msg() 取出消息并分发,使按键处理与中断/扫描上下文解耦——这正是"按键驱动"与"用户消息处理"两个环节衔接的核心设计意图:中断/定时器上下文只做最小工作,业务逻辑放到主循环。

该框架的典型使用场景:任何需要按键输入的 AC63 产品(玩具、遥控器、家电面板等),只需复用 iokey_para 并修改 GPIO 表与消息映射表即可快速接入。

Architecture

flowchart TD
    subgraph sg_Timer["定时器层 (10ms 周期)"]
        TIMER["定时器中断 / 软件定时器"]
    end

    subgraph sg_KeyDriver["按键驱动层 (key_driver.c)"]
        SCAN["key_driver_scan"]
        GET["get_value 回调<br/>(io_get_key_value)"]
        FILTER["消抖滤波<br/>(filter_cnt / filter_time)"]
        FSM["按键状态机<br/>(press_cnt / long_time / hold_time)"]
        PROC["key_process 回调<br/>(io_key_process)"]
    end

    subgraph sg_Msg["消息机制层 (msg.h)"]
        PUT["msg_put_fifo"]
        QUEUE[("消息 FIFO 队列")]
        GETMSG["get_msg"]
    end

    subgraph sg_App["用户应用层"]
        DEMO["key_msg_test_demo / 产品主循环"]
    end

    TIMER -->|"周期调用"| SCAN
    SCAN --> GET
    GET --> FILTER
    FILTER --> FSM
    FSM -->|"产生按键事件"| PROC
    PROC -->|"查询 io_key_msg_table 映射消息号"| PUT
    PUT --> QUEUE
    QUEUE --> GETMSG
    GETMSG -->|"NO_MSG 则继续轮询"| DEMO
    DEMO -->|"处理完成回到轮询"| GETMSG

架构说明:

  • 定时器层是扫描的时钟源。key_driver.c 中注释明确"扫描频率默认为 10ms 扫描一次",因此 filter_time = 4 对应约 40ms 消抖时间,long_time = 75 对应约 750ms 判定长按,hold_time = 90 对应约 900ms 判定连按。开发者需自行在定时器中断/软件定时器中周期调用 key_driver_scan(&iokey_para)。
  • 按键驱动层是纯状态计算:它不关心按键背后的电气实现,只通过 get_value 回调获取"当前键值"(含 NO_KEY = 0xff 表示无按键),再经过消抖与时间分级输出 (key_status, key_value) 事件对。
  • 消息机制层是驱动与用户的解耦点:io_key_process 只做查表 + msg_put_fifo 投递,不执行任何业务;用户主循环用 get_msg() 阻塞/轮询取消息。这样即使按键扫描运行在中断上下文,业务处理也不会阻塞扫描。
  • 用户应用层通过 key_msg_test_demo(SDK 自带联调 demo)或产品自身的消息循环消费消息,实现音量、菜单、开关等具体功能。

该分层设计的核心动机:把"物理输入采集"与"业务意图处理"彻底分离,使按键硬件变化(改 GPIO、换触摸)不影响业务代码,业务变化也不影响驱动的实时性。

核心数据结构

按键驱动的全部状态与回调都收敛在 struct key_driver_para 中(key_driver.h#L11-L26):

字段类型作用
last_keyu8上一次 get_value 返回的按键值,用于状态机判断"抬起"
filter_valueu8消抖用的"上一次有效采样值"
filter_cntu8消抖累加计数,连续采样一致时递增
filter_timeu8消抖阈值:filter_cnt 累加到该值后消抖生效
long_timeu8长按判定计数(相对 10ms 扫描周期)
hold_timeu8连按(HOLD)判定计数
press_cntu8按住持续计数,与 long_time/hold_time 对比
get_value函数指针按键值获取回调,返回键值或 NO_KEY
key_init函数指针按键初始化回调(GPIO/ADC 配置等)
key_process函数指针按键事件处理回调(key_status, key_value)
key_init_oku8初始化完成标志,为 0 时 key_driver_scan 直接返回

结构体把"数据 + 算法参数 + 回调"打包为一个对象,意味着同一套扫描算法可以驱动任意类型按键(IO 按键、ADC 按键、触摸按键)——这正是该框架的扩展性来源:换一种按键硬件,只需替换三个回调与参数,key_driver_scan 的状态机代码完全复用。

按键扫描与消抖算法(key_driver_scan)

key_driver_scan 是整个驱动的心脏,实现在 key_driver.c#L130-L197。它假定被定时器以固定周期(默认 10ms)调用,每次调用完成"采样 → 消抖 → 状态判定 → 回调"四步。

void  key_driver_scan(void *_scan_para)
{
    struct key_driver_para *scan_para = (struct key_driver_para *)_scan_para;

    u8 key_status = 0;
    u8 cur_key_value = NO_KEY;
    u8 key_value = NO_KEY;

    if (!scan_para->key_init_ok) {
        return;
    }

//===== 按键值获取
    cur_key_value = scan_para->get_value();

Source: key_driver.c#L130-L143

消抖处理(filter)

//===== 按键消抖处理
    if (cur_key_value != scan_para->filter_value && scan_para->filter_time) {	//当前按键值与上一次按键值如果不相等, 重新消抖处理, 注意filter_time != 0;
        scan_para->filter_cnt = 0; 		            //消抖次数清0, 重新开始消抖
        scan_para->filter_value = cur_key_value;	//记录上一次的按键值
        return; 		                            //第一次检测, 返回不做处理
    }

    if (scan_para->filter_cnt < scan_para->filter_time) {//当前按键值与上一次按键值相等, filter_cnt开始累加;
        scan_para->filter_cnt++;
        return;
    }

Source: key_driver.c#L148-L158

消抖算法的设计要点:

  • 两次采样不同则重新计时:cur_key_value != filter_value 时清零 filter_cnt 并记录新值,立即返回。这意味着任何一次抖动都会打断计数,只有连续 filter_time 次采样完全一致才算"稳定"。
  • filter_time == 0 可关闭消抖:判断条件显式要求 filter_time 非零,为需要极快响应的场景留了后门。
  • 消抖期内不产生任何事件:计数期间的 return 保证了抖动毛刺不会被当作真实按键事件上报。

按键状态机(短按 / 长按 / 连按 / 抬起)

//===== 按键类型判断
    if (cur_key_value == scan_para->last_key) {
        if (cur_key_value == NO_KEY) {                                     //没有按键按下
            return;
        }
        scan_para->press_cnt++;
        if (scan_para->press_cnt == scan_para->long_time) {                //长按
            key_status = KEY_LONG;
        } else if (scan_para->press_cnt == scan_para->hold_time) {         //连按(一直按着不放)
            key_status = KEY_HOLD;
            scan_para->press_cnt = scan_para->long_time;
        } else {
            return;//计数不做操作
        }
        key_value = cur_key_value;
    } else {
        if (cur_key_value == NO_KEY) {                                   //按键被抬起
            if (scan_para->press_cnt < scan_para->long_time) {           //短按
                key_status = KEY_SHORT_UP;
                key_value = scan_para->last_key;
            } else if (scan_para->press_cnt >= scan_para->long_time) {   //长按/HOLD状态之后按键抬起;
                key_status = KEY_LONG_HLOD_UP;
                key_value = scan_para->last_key;
            }
            scan_para->last_key = cur_key_value;
            scan_para->press_cnt = 0;
        } else {
            scan_para->last_key = cur_key_value;
            scan_para->press_cnt = 0;
            return;
        }
    }

//====== 按键处理
    //按键处理,中断时间不宜过久,这里仅仅是作按键的区分
    //建议依靠消息机制把按键信息发送到主循环处理按键
    scan_para->key_process(key_status, key_value);

Source: key_driver.c#L160-L197

状态机按"当前采样值是否等于上一次值"分两大分支:

  1. 值未变化(按住中):若为 NO_KEY(本来就无按键)直接返回;否则 press_cnt++。当 press_cnt 恰好等于 long_time 时上报 KEY_LONG(长按,一次性);继续按住直到等于 hold_time 时上报 KEY_HOLD(连按),并把 press_cnt 回拨到 long_time——这是一个巧妙的"周期重置":回拨后 press_cnt 会再次从 long_time+1 递增到 hold_time,从而在按住不放期间周期性地持续产生 HOLD 事件,实现"连按"的重复触发效果。
  2. 值变化(按下或抬起):若新值为 NO_KEY 说明按键被抬起,根据 press_cnt 与 long_time 的关系上报 KEY_SHORT_UP(短按)或 KEY_LONG_HLOD_UP(长按/连按后的抬起);随后复位 last_key 与 press_cnt。若新值是非空键值,说明切换到了另一个按键,更新 last_key 并清零计数。

四种按键状态与消息机制的对应关系(key_driver.c#L7-L18):

宏值含义触发时机
KEY_SHORT_UP0x0短按按住时间 < long_time 时抬起
KEY_LONG0x1长按press_cnt 达到 long_time
KEY_HOLD0x2连按press_cnt 达到 hold_time(周期性重复)
KEY_LONG_HLOD_UP0x3长按/连按抬起长按或连按后松开
NO_KEY0xff无按键用于键值域,非状态

设计意图:把"短按/长按/连按"全部建模为时间计数而非阻塞延时,扫描函数永远是非阻塞的,非常适合在定时器中断中运行;同时抬起事件(*_UP)让应用层能实现"按下-抬起"配对语义(如长按进入配对模式、松开退出)。

IO 按键驱动实现

IO 按键是 key_driver_para 的默认实现,GPIO 定义来自外部数组 io_key_table[2](两个按键,产品代码中定义引脚表)。

初始化 io_key_init

int io_key_init(void *para)
{
    struct key_driver_para *scan_para = (struct key_driver_para *)para;

    gpio_set_direction(io_key_table[0], 1);
    gpio_set_direction(io_key_table[1], 1);

    gpio_set_pull_down(io_key_table[0], 0);
    gpio_set_pull_down(io_key_table[1], 0);

    gpio_set_pull_up(io_key_table[0], 1);
    gpio_set_pull_up(io_key_table[1], 1);

    gpio_set_die(io_key_table[0], 1);
    gpio_set_die(io_key_table[1], 1);

    scan_para->key_init_ok = 1;  //标志置位
    log_info("___io_key_init_ok___");
    return 0;
}

Source: key_driver.c#L33-L52

初始化把按键引脚配置为输入 + 内部上拉(gpio_set_direction(...,1) 输入、gpio_set_pull_up(...,1) 上拉),配合电路上"按键接地"的接法(见 AC6354B_Keyboard_Circuit V1.0.pdf):按键按下时引脚被拉低,gpio_read 读到 0。最后置 key_init_ok = 1——这是扫描函数的总开关,初始化失败/未调用时扫描直接空转。

按键值获取 io_get_key_value

u8 io_get_key_value(void)
{
    //按键接地为按下
    if (!gpio_read(io_key_table[0])) {
        return 0;
    } else if (!gpio_read(io_key_table[1])) {
        return 1;
    }
    return NO_KEY;
}

Source: key_driver.c#L61-L70

采用"低电平 = 按下"约定:任一引脚读到 0 即返回对应键值(0 或 1),全部为高(无按键)返回 NO_KEY(0xff)。键值号即 io_key_msg_table 的第一维下标,因此键值必须从 0 开始连续编号。

按键消息映射表与事件投递

按键事件通过一张二维映射表转换为用户消息,实现在 key_driver.c#L72-L82:

//io 按键消息表
u16 io_key_msg_table[2][4] = {
    //短按                  //长按                 //连按(hold)           //长、连按抬起
    [0] = {
        MSG_TEST_IO_KEY1_SHORT, MSG_TEST_IO_KEY1_LONG, MSG_TEST_IO_KEY1_HOLD, MSG_TEST_IO_KEY1_LONG_HOLD_UP
    },

    [1] = {
        MSG_TEST_IO_KEY2_SHORT, MSG_TEST_IO_KEY2_LONG, MSG_TEST_IO_KEY2_HOLD, MSG_TEST_IO_KEY2_LONG_HOLD_UP
    },
};

Source: key_driver.c#L72-L82

表的行是键值(key_value),列是按键状态(key_status,恰好与 0x0~0x3 的宏值对齐),交叉点就是该"键 × 事件"对应的消息号。消息号 MSG_TEST_IO_KEY1_SHORT 等由 SDK 的 msg.h 定义(本页未展开其定义细节)。这种"表驱动"设计让产品定制按键功能时只改表不改代码。

事件处理回调 io_key_process

void io_key_process(u8 key_status, u8 key_value)
{
    //按键处理,中断时间不宜过久,这里仅仅是作按键的区分
    //建议依靠消息机制把按键信息发送到主循环处理按键
    u16 key_msg;
    u8 err;
    key_msg = io_key_msg_table[key_value][key_status];
    err = msg_put_fifo(key_msg);
    if (err != 0) {
        log_info("can not put msg");
    }
}

Source: key_driver.c#L92-L103

回调的逻辑极简:查表 → msg_put_fifo 入队 → 失败打印日志。注释两次强调"中断时间不宜过久",这正是把消息机制引入按键处理的根本原因——按键事件在扫描上下文只做入队,具体功能全部推迟到主循环,保证扫描周期稳定、抖动判定精确。

默认参数 iokey_para

//注意扫描频率,这里供的时间默认为10ms扫描一次
//则对应的消抖时间为 4*10 =40ms,其他以此类推
struct key_driver_para iokey_para = {
    .last_key 		  = NO_KEY,  		        //上一次get_value按键值, 初始化为NO_KEY;
    .filter_time 	  = 4,			            //按键消抖延时;
    .long_time 		  = 75,  		            //按键判定长按数量
    .hold_time 		  = (75 + 15),  	        //按键判定HOLD数量
    .get_value 		  = io_get_key_value,       //按键值获取
    .key_init         = io_key_init,            //按键状态初始化
    .key_process      = io_key_process,         //按键处理
    .key_init_ok      = 0,                      //按键初始化完成标志
};

Source: key_driver.c#L105-L116

在 10ms 扫描周期下:消抖约 40ms、长按判定约 750ms、连按约 900ms。hold_time 用表达式 (75 + 15) 直接表达"长按之后再过 15 次扫描进入连按",可读性强且便于整体调参。key_init_ok 初值为 0,保证 key_driver_init 之前扫描不会产生事件。

初始化入口 key_driver_init

int key_driver_init(struct key_driver_para *key_para)
{
    //初始化
    if (key_para->key_init) {
        return key_para->key_init(key_para);
    }

    return -1;
}

Source: key_driver.c#L208-L216

框架层只做一件事:转发调用用户回调 key_init,未注册回调则返回 -1。这保持了框架与具体按键硬件解耦。

用户消息处理(key_msg_test_demo)

SDK 在驱动文件末尾提供了按键与消息机制的联调 demo(key_driver.c#L226-L240):

void key_msg_test_demo()
{
    /*******demo实现前提**********/
    //消息机制初始化: msg_init
    //按键初始化:    key_driver_init
    //定时器10ms扫描:key_driver_scan

    u16 msg;
    while (1) {
        wdt_clear();
        msg = get_msg();
        if (msg == NO_MSG) {
            continue;
        }
        // ... 根据 msg 分发处理用户消息
    }
}

Source: key_driver.c#L226-L240

demo 明确列出了三个使用前提:msg_init 初始化消息机制、key_driver_init 初始化按键、定时器 10ms 周期调用 key_driver_scan。主循环采用轮询 + 喂狗模式:wdt_clear() 防止看门狗复位,get_msg() 取消息,NO_MSG 则继续轮询。业务分发位于取消息之后——用户在这里通过 switch(msg) 对 MSG_TEST_IO_KEY1_SHORT 等消息做出响应(如打印、切歌、调音量)。这是"按键驱动 + 用户消息处理"完整链路的用户侧终点。

跨芯片一致性

key_driver.h 在 AC632N、AC635N、AC636N、AC638N 四个 BSP 中接口完全一致(AC632N 版本 与 AC636N 版本 均声明 key_msg_test_demo)。因此本页对 AC635N 的分析可直接套用到整个 AC63 系列;AC638N 额外提供低功耗触摸按键(lp_touch_key_api.h / lp_touch_key.c),属于不同的按键采集硬件,但同样可通过 struct key_driver_para 回调模型接入本框架。

Core Flow

端到端时序

sequenceDiagram
    participant T as 定时器 (10ms)
    participant S as key_driver_scan
    participant G as get_value (io_get_key_value)
    participant P as key_process (io_key_process)
    participant Q as 消息队列 (msg_put_fifo / get_msg)
    participant M as 主循环 (key_msg_test_demo)

    T->>S: 周期调用 key_driver_scan(iokey_para)
    S->>S: 检查 key_init_ok
    S->>G: 读取按键电平
    G-->>S: 键值 0 / 1 / NO_KEY(0xff)
    S->>S: 消抖: filter_cnt 连续累加至 filter_time
    S->>S: 状态机: press_cnt 对比 long_time / hold_time
    S-->>P: key_process(key_status, key_value)
    P->>P: 查 io_key_msg_table 得到消息号
    P->>Q: msg_put_fifo(key_msg)
    Note over Q: FIFO 满时返回非0, 打印 "can not put msg"
    loop 主循环轮询
        M->>Q: get_msg()
        Q-->>M: 消息号 或 NO_MSG
        M->>M: 分发处理 (switch)
    end

按键状态迁移

stateDiagram-v2
    [*] --> IDLE: 复位/初始化
    IDLE --> FILTERING: 采样到新键值 (非NO_KEY)
    FILTERING --> IDLE: 消抖期抖动 (值变化)
    FILTERING --> PRESSED: filter_cnt 达到 filter_time
    PRESSED --> SHORT_UP: 抬起且 press_cnt < long_time
    PRESSED --> LONG: press_cnt 达到 long_time
    LONG --> HOLD: press_cnt 达到 hold_time
    LONG --> LONG_HOLD_UP: 长按后抬起 (press_cnt >= long_time)
    HOLD --> LONG_HOLD_UP: 连按期间抬起
    HOLD --> HOLD: 周期性重复上报 (press_cnt 回拨后继续累加)
    SHORT_UP --> IDLE: last_key/press_cnt 复位
    LONG_HOLD_UP --> IDLE: last_key/press_cnt 复位

状态图反映了扫描算法的关键特性:消抖态可被抖动打断回 IDLE(保证抗干扰),而一旦进入 PRESSED,只有"抬起"才回到 IDLE;LONG 是单次事件,HOLD 是自循环的周期性事件,二者都以上报 KEY_LONG_HLOD_UP 作为终止。

Usage Examples

示例 1:定义产品按键参数(复用框架)

以下展示如何基于 struct key_driver_para 为产品配置按键驱动。SDK 默认提供的 iokey_para 即是最佳模板——只需调整消抖/长按/连按时间与 GPIO 表:

//注意扫描频率,这里供的时间默认为10ms扫描一次
//则对应的消抖时间为 4*10 =40ms,其他以此类推
struct key_driver_para iokey_para = {
    .last_key 		  = NO_KEY,  		        //上一次get_value按键值, 初始化为NO_KEY;
    .filter_time 	  = 4,			            //按键消抖延时;
    .long_time 		  = 75,  		            //按键判定长按数量
    .hold_time 		  = (75 + 15),  	        //按键判定HOLD数量
    .get_value 		  = io_get_key_value,       //按键值获取
    .key_init         = io_key_init,            //按键状态初始化
    .key_process      = io_key_process,         //按键处理
    .key_init_ok      = 0,                      //按键初始化完成标志
};

Source: key_driver.c#L105-L116

示例 2:自定义按键事件处理(消息投递)

替换 key_process 回调即可改变事件去向。默认实现把事件转换为消息入队,这正是"按键驱动 → 用户消息"衔接的核心代码:

void io_key_process(u8 key_status, u8 key_value)
{
    //按键处理,中断时间不宜过久,这里仅仅是作按键的区分
    //建议依靠消息机制把按键信息发送到主循环处理按键
    u16 key_msg;
    u8 err;
    key_msg = io_key_msg_table[key_value][key_status];
    err = msg_put_fifo(key_msg);
    if (err != 0) {
        log_info("can not put msg");
    }
}

Source: key_driver.c#L92-L103

示例 3:主循环消费按键消息

按键消息最终在用户主循环中被 get_msg() 取出并分发,NO_MSG 时继续轮询并喂狗:

    u16 msg;
    while (1) {
        wdt_clear();
        msg = get_msg();
        if (msg == NO_MSG) {
            continue;
        }
        // ... 根据 msg 分发处理用户消息
    }

Source: key_driver.c#L233-L240

示例 4:按键消息映射表定制

产品定义"哪个键、哪个事件 → 什么消息"只需修改此表(行=键值,列=短按/长按/连按/抬起):

//io 按键消息表
u16 io_key_msg_table[2][4] = {
    //短按                  //长按                 //连按(hold)           //长、连按抬起
    [0] = {
        MSG_TEST_IO_KEY1_SHORT, MSG_TEST_IO_KEY1_LONG, MSG_TEST_IO_KEY1_HOLD, MSG_TEST_IO_KEY1_LONG_HOLD_UP
    },

    [1] = {
        MSG_TEST_IO_KEY2_SHORT, MSG_TEST_IO_KEY2_LONG, MSG_TEST_IO_KEY2_HOLD, MSG_TEST_IO_KEY2_LONG_HOLD_UP
    },
};

Source: key_driver.c#L72-L82

Configuration Options

按键驱动的全部可配置项均集中在 struct key_driver_para(见 key_driver.h#L11-L26)与 io_key_msg_table 中:

配置项类型默认值说明
USE_KEY_DRIVER宏1模块使能开关(key_driver.h 顶部)
filter_timeu84消抖阈值,10ms 周期下约 40ms
long_timeu875长按判定计数,10ms 周期下约 750ms
hold_timeu890 (75+15)连按判定计数,约 900ms
press_cntu80按住持续计数(内部状态)
last_keyu8NO_KEY(0xff)上次键值(内部状态)
key_init_oku80初始化完成标志,0 时扫描空转
get_value回调io_get_key_value按键值获取函数
key_init回调io_key_init按键初始化函数
key_process回调io_key_process按键事件处理函数
io_key_table[2]u16[]外部定义两个按键的 GPIO 编号
io_key_msg_table[2][4]u16[][]MSG_TEST_IO_KEYx_*键值 × 状态 → 消息号映射

时间参数的单位是"扫描次数"而非毫秒,修改扫描周期(如改为 5ms)时需同步换算这些参数以保持相同的物理时间语义。

API Reference

int key_driver_init(struct key_driver_para *key_para)

按键驱动初始化入口,转发调用 key_para->key_init 回调(key_driver.c#L208-L216)。

参数: key_para —— 按键参数结构体指针(通常传 &iokey_para) 返回: 0 成功;-1 未注册 key_init 回调 说明: 必须在定时器启动扫描前调用;成功后 key_init_ok 置 1。

void key_driver_scan(void *_scan_para)

按键扫描函数,需由定时器按固定周期(默认 10ms)周期调用(key_driver.c#L130-L197)。

参数: _scan_para —— 按键参数结构体指针 返回: 无 说明: 内部完成采样、消抖、状态判定并调用 key_process;key_init_ok == 0 时直接返回;非阻塞,可在中断上下文运行。

int io_key_init(void *para)

IO 按键 GPIO 初始化:配置输入方向、内部上拉、数字使能,并置 key_init_ok = 1(key_driver.c#L33-L52)。

参数: para —— 按键参数结构体指针 返回: 0 成功 说明: 通过 key_driver_init(&iokey_para) 间接调用,一般不直接调用。

u8 io_get_key_value(void)

读取按键电平并返回键值(key_driver.c#L61-L70)。

返回: 0 / 1 表示对应按键按下;NO_KEY(0xff) 表示无按键 说明: 低电平有效(按键接地);键值即 io_key_msg_table 行下标。

void io_key_process(u8 key_status, u8 key_value)

按键事件处理回调:查 io_key_msg_table 得消息号并 msg_put_fifo 入队(key_driver.c#L92-L103)。

参数: key_status —— 按键状态(KEY_SHORT_UP / KEY_LONG / KEY_HOLD / KEY_LONG_HLOD_UP);key_value —— 键值 返回: 无 失败场景: 消息队列满时 msg_put_fifo 返回非 0,打印 "can not put msg",该事件被丢弃。

Failure Modes、边界情况与并发

消息队列满(FIFO Overflow)

io_key_process 中 msg_put_fifo 返回非 0 时仅打印 "can not put msg" 并丢弃该事件(key_driver.c#L99-L102)。设计上接受事件丢失而非阻塞扫描:在中断/定时器上下文阻塞会直接破坏扫描节拍。产品若对按键响应有强要求,应确保主循环消费速度足够快,或扩大消息队列容量。

未初始化即扫描

key_driver_scan 第一道防线是 key_init_ok 检查(key_driver.c#L138-L140),未初始化时函数直接返回,避免 GPIO 未配置时读到不确定电平导致误事件。iokey_para.key_init_ok 初值 0 保证时序安全。

消抖被关闭(filter_time == 0)

消抖分支显式判断 filter_time 非零(key_driver.c#L149)。若置 0,每次采样都会直接进入状态判定,抖动可能被当作多次按下——仅在按键硬件已无抖动(如触摸、编码器)时使用。

多键同按与快速换键

io_get_key_value 按优先级返回第一个被按下的键(先查 io_key_table[0]),不支持多键组合。状态机的"值变化"分支支持顺序切换(按下键 1 后直接按键 2),但不产生中间抬起事件,组合键语义需在应用层另行设计。

并发与上下文约束

  • 扫描函数运行在定时器/中断上下文,用户回调 key_process 也在该上下文中执行——注释反复强调"处理时间不宜过长",任何阻塞、长循环、延时都应避免;
  • 主循环在任务上下文执行 get_msg/分发,与扫描上下文通过消息队列解耦,二者是典型的生产者-消费者关系;队列本身(msg_put_fifo/get_msg)需具备原子性(由 SDK 消息机制保证);
  • press_cnt、filter_cnt 等状态只被扫描上下文读写,不存在跨上下文共享的竞争问题。

长按/连按的计数回拨

KEY_HOLD 触发后 press_cnt 被回拨到 long_time(key_driver.c#L168-L170),使 HOLD 事件周期性重复。边界影响:长按与连按的时间差由 hold_time - long_time 决定(默认 15 次扫描 ≈ 150ms),过小会导致 HOLD 洪泛,过大则连按响应迟钝。

Performance 与运维考虑

  • 扫描开销极小:单次 key_driver_scan 仅含几次比较与一次回调,10ms 周期下 CPU 占用可忽略;
  • 时间参数换算:所有时间量都是"扫描次数",改扫描周期必须同步换算 filter_time/long_time/hold_time,否则消抖与长按手感全变;
  • 喂狗:key_msg_test_demo 主循环每轮 wdt_clear(),若产品主循环可能长时间阻塞,需保持同样的喂狗节奏,防止看门狗复位;
  • 日志:io_key_init 成功打印 "___io_key_init_ok___"、入队失败打印 "can not put msg",可作为联调与线上问题排查的锚点(log_info 在正式版可被日志框架裁剪)。

Extension Points

  1. 替换按键采集硬件:实现新的 get_value/key_init 回调即可接入 ADC 按键、矩阵键盘、触摸按键(AC638N 的 lp_touch_key.c 即属此类)——扫描与状态机代码零改动;
  2. 替换事件消费方式:自定义 key_process 可绕过消息队列直接驱动业务,或改为投递到事件组/信号量等其他 IPC 原语;
  3. 扩展消息表:io_key_msg_table 的行列由"键值数 × 4 状态"决定,新增按键只需扩行并补充对应消息宏;
  4. 多套按键参数:key_driver_scan 接受任意 struct key_driver_para 实例,可在同一芯片上并行运行多套按键配置(如"正常模式"与"低功耗唤醒模式"各一套);
  5. KEY_DRIVER_TYPE_IO 常量(key_driver.c#L8)暗示了按键类型枚举的设计方向,为将来多类型按键共存预留了扩展位。

Tests 与验证

SDK 以 key_msg_test_demo(key_driver.c#L226-L240)形式提供了按键与消息机制的联调测试:其三个前提(msg_init、key_driver_init、定时器 10ms 扫描)即是完整链路的最小验证步骤。验证要点:

  • 短按应收到 MSG_TEST_IO_KEY1_SHORT(按下约 40ms 后抬起);
  • 长按 750ms 收到 MSG_TEST_IO_KEY1_LONG,松开后收到 MSG_TEST_IO_KEY1_LONG_HOLD_UP;
  • 持续按住超过 900ms 后周期收到 MSG_TEST_IO_KEY1_HOLD;
  • 消息队列满时观察 "can not put msg" 日志,评估队列容量是否够用。

Related Links

  • key_driver.h(AC635N,接口定义)
  • key_driver.c(AC635N,实现)
  • key_driver.h(AC632N,同接口)
  • key_driver.h(AC636N,同接口)
  • key_driver.h(AC638N,同接口)
  • AC638N 低功耗触摸按键(兄弟能力)
  • AC6354B 键盘电路原理图
Prev
应用入口与主循环
Next
消息系统