按键驱动与用户消息处理
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 周期扫描的方式自动完成:
- 消抖:连续
filter_time次采样一致才认为按键状态有效,避免机械抖动误触发; - 时间分级:以
press_cnt累加计数与long_time、hold_time对比,区分短按、长按、连按(HOLD)与抬起; - 消息化:
key_process回调中通过io_key_msg_table把"键值 × 按键状态"映射为消息号,经msg_put_fifo写入 FIFO 消息队列; - 用户处理:主循环调用
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_key | u8 | 上一次 get_value 返回的按键值,用于状态机判断"抬起" |
filter_value | u8 | 消抖用的"上一次有效采样值" |
filter_cnt | u8 | 消抖累加计数,连续采样一致时递增 |
filter_time | u8 | 消抖阈值:filter_cnt 累加到该值后消抖生效 |
long_time | u8 | 长按判定计数(相对 10ms 扫描周期) |
hold_time | u8 | 连按(HOLD)判定计数 |
press_cnt | u8 | 按住持续计数,与 long_time/hold_time 对比 |
get_value | 函数指针 | 按键值获取回调,返回键值或 NO_KEY |
key_init | 函数指针 | 按键初始化回调(GPIO/ADC 配置等) |
key_process | 函数指针 | 按键事件处理回调(key_status, key_value) |
key_init_ok | u8 | 初始化完成标志,为 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
状态机按"当前采样值是否等于上一次值"分两大分支:
- 值未变化(按住中):若为
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 事件,实现"连按"的重复触发效果。 - 值变化(按下或抬起):若新值为
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_UP | 0x0 | 短按 | 按住时间 < long_time 时抬起 |
KEY_LONG | 0x1 | 长按 | press_cnt 达到 long_time |
KEY_HOLD | 0x2 | 连按 | press_cnt 达到 hold_time(周期性重复) |
KEY_LONG_HLOD_UP | 0x3 | 长按/连按抬起 | 长按或连按后松开 |
NO_KEY | 0xff | 无按键 | 用于键值域,非状态 |
设计意图:把"短按/长按/连按"全部建模为时间计数而非阻塞延时,扫描函数永远是非阻塞的,非常适合在定时器中断中运行;同时抬起事件(*_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_time | u8 | 4 | 消抖阈值,10ms 周期下约 40ms |
long_time | u8 | 75 | 长按判定计数,10ms 周期下约 750ms |
hold_time | u8 | 90 (75+15) | 连按判定计数,约 900ms |
press_cnt | u8 | 0 | 按住持续计数(内部状态) |
last_key | u8 | NO_KEY(0xff) | 上次键值(内部状态) |
key_init_ok | u8 | 0 | 初始化完成标志,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
- 替换按键采集硬件:实现新的
get_value/key_init回调即可接入 ADC 按键、矩阵键盘、触摸按键(AC638N 的lp_touch_key.c即属此类)——扫描与状态机代码零改动; - 替换事件消费方式:自定义
key_process可绕过消息队列直接驱动业务,或改为投递到事件组/信号量等其他 IPC 原语; - 扩展消息表:
io_key_msg_table的行列由"键值数 × 4 状态"决定,新增按键只需扩行并补充对应消息宏; - 多套按键参数:
key_driver_scan接受任意struct key_driver_para实例,可在同一芯片上并行运行多套按键配置(如"正常模式"与"低功耗唤醒模式"各一套); 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"日志,评估队列容量是否够用。