按键、LED 与红外
本文档介绍 AW31N BLE SDK 中的人机交互外设能力:按键框架(IO/AD/矩阵按键驱动、消抖、长按/连击事件)、LED 指示控制(led_control / ledc)以及红外编解码(ir_decoder / ir_encoder),覆盖从硬件扫描到应用消息投递的完整链路。
Purpose and Scope
本页面聚焦 SDK 中"人机交互输入输出"这一完整能力边界,包括:
- 按键框架:
key.h/key.c定义的核心扫描、消抖、事件状态机,以及key_drv_io/key_drv_ad/key_drv_matrix三类驱动与key_interface_t注册契约。 - LED 控制:
led_control.c与 LEDC(ledc.c)驱动。 - 红外:
ir_decoder.c/ir_encoder.c,以及按键类型KEY_TYPE_IR如何接入按键框架。 - 配置脚本:
AW31N_config_tool下的key_common.lua、key_msg.lua、adkey_v1.lua、iokey_v1.lua等 Lua 配置源。
以下内容属于兄弟页面,不在本文展开:蓝牙协议栈与 GATT/HID 上报(如 apps/demo/hid 的 app_keyboard.c、app_keyfob.c)、电源/低功耗管理(按键仅作为开机唤醒源时被引用)、音频 MIC 输入(KEY_TYPE_MIC 仅作为枚举成员出现)。这些能力各自有独立的目录与页面。
Overview
AW31N 是低功耗 BLE SoC,其按键系统设计核心是**"驱动-框架分离"**:每种物理按键类型(GPIO、ADC 分压、矩阵、红外)实现一个极小的 key_interface_t 接口(key_init + key_get_value),统一注册进 key_list[],由 key.c 的扫描任务统一调度。这样业务层永远只面对"按键值 + 按键事件",不关心物理实现。
扫描结果经过三级处理:
- 消抖(filter):
filter_cnt累加到filter_time(基准KEY_BASE_CNT = 2)才认为按键有效; - 事件判定(press/long/hold/click):
press_cnt与long_time/hold_time比较产生长按与 HOLD 事件,click_cnt与click_delay_time配合产生连击事件; - 投递(emit):通过
key_emit_t回调或sys_event消息投递给应用,中间可经弱函数key_event_remap重映射组合键键值。
LED 与红外属于同一外设管理层次:LED 通过 PWM/LEDC 输出指示状态,红外解码器可作为按键输入源(KEY_TYPE_IR)接入同一按键框架,也可独立用于红外遥控编解码。整套外设的实例参数(adkey_data、pwm_led_data、key_table[3][10] 等)由 apps/demo/hid/modules/user_cfg.c 集中声明,并可通过 AW31N_config_tool 的 Lua 脚本按板级配置生成。
Architecture
flowchart TD
subgraph sg_HAL["硬件层 (HAL)"]
IO_PIN["GPIO 按键<br/>key_drv_io.c"]
AD_CH["ADC 分压按键<br/>key_drv_ad.c"]
MATRIX["矩阵按键<br/>key_drv_matrix.c"]
IR_RX["红外接收<br/>ir_decoder.c"]
LED_OUT["LED 指示<br/>led_control.c / ledc.c"]
end
subgraph sg_KEY["按键框架 (bsp/common/key)"]
KEY_IF["key_interface_t 契约"]
KEY_CORE["key.c 扫描/消抖/事件状态机"]
KEY_REMAP["key_event_remap (weak)"]
KEY_PARA["key_driver_para 参数"]
end
subgraph sg_EVENT["事件与消息层"]
SYS_EVENT["sys_event"]
MSG["消息队列 msg"]
end
subgraph sg_APP["应用层"]
APP["app 任务 (user_cfg / demo)"]
POWER["开机/电源管理"]
end
IO_PIN --> KEY_IF
AD_CH --> KEY_IF
MATRIX --> KEY_IF
IR_RX --> KEY_IF
KEY_IF --> KEY_CORE
KEY_CORE --> KEY_PARA
KEY_CORE --> KEY_REMAP
KEY_CORE --> SYS_EVENT
SYS_EVENT --> MSG
MSG --> APP
KEY_CORE --> POWER
APP --> LED_OUT
APP --> IR_RX
架构说明:
key_interface_t(key.h)是全部按键输入的统一抽象:key_type标识按键类型,key_init做硬件初始化,key_get_value返回当前键值(NO_KEY = 0xff表示无按键)。IO/AD/矩阵/红外驱动都只是这一接口的具体实现。key.c是状态机核心:它不直接操作寄存器,而是按key_list[]顺序轮询各驱动,取第一个非NO_KEY的值作为本次扫描结果,再进入消抖与事件判定。- 事件通过
sys_event投递到消息队列,应用任务消费后执行业务动作(上报 HID、控制 LED、触发开机等)。 - LED 输出由应用层驱动,
led_control.c/ledc.c提供 PWM/LEDC 底层能力;红外既可作为按键输入,也可由应用主动编码发送。
按键框架核心机制
驱动注册表 key_list[]
key.c 用编译期条件编译构造驱动注册表,每种使能的驱动以 key_interface_t 形式静态注册:
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
};
Source: key.c
设计意图:编译期裁剪而非运行时动态注册——未使能的按键类型完全不占用代码与 RAM;同时 key_init() 遍历注册表统一初始化,新增按键类型只需实现接口并加入数组,框架零改动。注意 KEY_TYPE_IR 在枚举中已存在(见 key.h),红外键可通过同样的接口挂接。
扫描参数 key_driver_para
扫描状态机参数集中定义在全局实例 key_scan_para 中,包含消抖、长按、HOLD、连击五组参数:
struct key_driver_para key_scan_para = {
.last_key = NO_KEY, //上一次get_value按键值, 初始化为NO_KEY;
.filter_time = 2, //按键消抖延时;
.long_time = 75, //按键判定长按数量
.hold_time = (75 + 15), //按键判定HOLD数量
.click_delay_time = 20, //按键被抬起后等待连击延时数量
};
Source: key.c
对应结构体定义(key.h):
| 字段 | 作用 |
|---|---|
filter_value / filter_cnt / filter_time | 消抖:filter_cnt 累加到 filter_time(基准 KEY_BASE_CNT=2)判有效 |
long_time / hold_time / press_cnt | 长按/HOLD:press_cnt 达 long_time 发长按,达 hold_time 发 HOLD |
click_cnt / click_delay_cnt / click_delay_time | 连击:抬起后 click_delay_time 窗口内再次按下则连击数递增 |
notify_value | 延时窗口内待发送的按键值 |
last_key | 上一次 get_value 的键值,用于状态迁移 |
另有 MOUSE_KEY_SCAN_MODE 下的精简参数(long_time=3, hold_time=3),说明鼠标/高频扫描场景会大幅压缩事件判定节奏。
按键轮询与值获取
get_key_value() 按 key_list[] 顺序轮询:某个驱动返回非 NO_KEY 即立即返回其 key_io_t(类型 + 键值),否则最终返回 NO_KEY。这决定了驱动优先级由数组顺序决定——例如 AD 键与 MIC 共用 ADC 通道时,先注册的驱动先得到判定机会。
开机标志位
set_key_poweron_flag() / get_key_poweron_flag() / clear_key_poweron_flag() 三件套(key.c)用于低功耗场景:按键可以作为唤醒源,唤醒后由应用(如 apps/demo/hid/app_main.c 中 extern void set_key_poweron_flag(u8 flag);)标记本次开机由按键触发,供业务区分冷启动与按键唤醒。
键值重映射扩展点
int __attribute__((weak)) key_event_remap(struct sys_event *e)
{
return true;
}
Source: key.c
key_event_remap 是弱符号:默认直接返回 true(放行事件);用户可在应用层提供同名强符号实现,按 sys_event 内容把组合按键(如音量+播放)的键值重映射,配合 struct key_remap / key_remap_data(key.h)的位值与映射表工作。这是 SDK 为"不改框架、只改业务"预留的标准钩子。
LED 指示控制
LED 相关源码位于 apps/app/bsp/common/led/ 与 apps/app/bsp/common/ledc/:
led_control.c:LED 逻辑控制层,管理 LED 亮灭、闪烁、呼吸等指示行为,并向上层提供led_platform_data平台数据接口。其平台数据结构在应用侧声明,例如apps/demo/hid/modules/user_cfg.c中的extern struct led_platform_data pwm_led_data;,由配置工具按板级 Lua 脚本生成。ledc.c/ledc_demo.c:LEDC(LED Controller)底层驱动,利用 PWM 通道输出占空比控制 LED 亮度/渐变;ledc_demo.c提供使用示例。LEDC 相比 GPIO 直驱的优势是硬件定时刷新、CPU 零占用,适合呼吸灯等周期性效果。
LED 与按键同属外设管理层次,但方向相反:按键是"输入 → 事件",LED 是"事件/状态 → 输出"。典型链路为:应用收到按键消息(如播放/暂停)→ 切换播放状态 → 调用 LED 控制接口刷新指示灯;低电量、充电、连接状态等也复用同一输出通道。
红外(IR)
红外模块位于 apps/app/bsp/common/ir/:
ir_decoder.c:红外接收解码,负责从红外接收头(载波解调后的脉冲序列)中解析遥控码。解码得到的键值可作为按键输入接入按键框架——KEY_TYPE_IR已在KEY_TYPE枚举中预留(key.h),因此红外遥控按键与 GPIO/AD 键走完全相同的消抖、长按、连击状态机与消息投递路径。ir_encoder.c:红外编码发送,将按键码/指令编码为载波脉冲序列驱动红外发射管,用于遥控其他设备(如空调、电视)。编码格式(载波频率、引导码、数据位宽、重复码)由实现内部协议表决定。
红外接入按键框架的收益:遥控器按键天然需要防抖(用户快速连按)、需要长按重复(音量连续调节),这些能力由 key.c 状态机统一提供,红外驱动只需实现"读取当前收到的码值 / 返回 NO_KEY"即可。
核心扫描流程
sequenceDiagram
participant T as 定时器 (tick_timer)
participant K as key.c (key_driver_scan)
participant D as key_drv_* 驱动
participant F as 消抖/长按/连击状态机
participant R as key_event_remap (weak)
participant M as 消息队列 (msg)
participant A as 应用任务
T->>K: 周期调用 key_driver_scan(para)
K->>D: key_get_value()
D-->>K: key_io_t(key_type, key_num) 或 NO_KEY
K->>F: 更新 filter_cnt / press_cnt / click_cnt
F-->>K: 判定事件 (按下/弹起/长按/HOLD/连击)
K->>R: key_event_remap(sys_event)
R-->>K: true 放行 / false 拦截
K->>M: 投递 sys_event
M->>A: 应用消费按键消息并执行动作
A->>A: 控制 LED / 上报 HID / 触发功能
流程要点:
- 扫描节拍:
key_driver_scan由tick_timer周期性调度(key_scan()为对外包装),每个节拍完成一次全驱动轮询。 - 首值优先:
get_key_value()返回第一个非NO_KEY的驱动值,多按键同时按下时由数组顺序仲裁。 - 事件状态机:
press_cnt递增产生按下/长按/HOLD 事件;抬起后click_delay_time窗口内的再按构成连击,窗口结束才投递最终键值(notify_value暂存待发值)。 - 可拦截钩子:
key_event_remap返回false可丢弃事件(如开机引导阶段屏蔽按键)。 - 异步解耦:事件经消息队列投递,扫描任务不阻塞在应用逻辑上,保证低功耗 SoC 上的扫描节拍稳定。
使用示例
示例 1:按键接口契约(新增一种按键驱动的模板)
所有按键驱动都实现同一个接口,以下代码展示了 key_interface_t 的完整定义——新驱动只需实现 key_init 与 key_get_value:
typedef struct {
KEY_TYPE key_type;
void (*key_init)(void);
uint8_t(*key_get_value)(void);
} key_interface_t;
typedef struct {
uint8_t key_type;
uint8_t key_num;
} key_io_t;
Source: key.h
key_get_value 的返回值约定:NO_KEY(0xff)表示当前无按键;有效按键返回键号(key_num),由框架封装成 key_io_t 后进入状态机。
示例 2:开机按键标志(按键唤醒后的业务区分)
按键既可作为开机唤醒源,唤醒后框架与应用通过标志位协作:
void set_key_poweron_flag(uint8_t flag)
{
key_poweron_flag = flag;
}
uint8_t get_key_poweron_flag(void)
{
return key_poweron_flag;
}
void clear_key_poweron_flag(void)
{
key_poweron_flag = 0;
}
Source: key.c
典型用法见 apps/demo/hid/app_main.c 中的 extern void set_key_poweron_flag(u8 flag); set_key_poweron_flag(1);——应用在开机流程中查询标志,决定是否直接进入"按键对应的默认功能"。
示例 3:键值重映射钩子(组合键定制)
int __attribute__((weak)) key_event_remap(struct sys_event *e)
{
return true;
}
Source: key.c
应用层提供同名非弱函数即可覆盖:解析 e 中携带的原始键值,查 struct key_remap 映射表(位值 → 重映射值,见 key.h)后改写键值,返回 true 放行或 false 拦截。
配置选项
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
KEY_IO_EN / KEY_AD_EN / KEY_MATRIX_EN | 编译宏 | 按板级使能 | 决定哪些驱动编译进 key_list[],关闭即裁剪 |
key_scan_para.filter_time | uint8 | 2 | 消抖累加门限(基准 KEY_BASE_CNT = 2) |
key_scan_para.long_time | uint8 | 75(鼠标模式 3) | 长按判定节拍数 |
key_scan_para.hold_time | uint8 | 90(鼠标模式 3) | HOLD 事件判定节拍数 |
key_scan_para.click_delay_time | uint8 | 20 | 抬起后连击等待窗口 |
KEY_BASE_CNT | 宏 | 2 | 消抖基准计数 |
KEY_PRESS_CNT | 宏 | 35 | 按下计数的辅助门限 |
NO_KEY | 宏 | 0xff | 无按键哨兵值 |
MOUSE_KEY_SCAN_MODE | 宏 | 关闭 | 鼠标高频扫描精简参数模式 |
板级配置入口:apps/app/post_build/bd47/AW31N_config_tool/conf/source/ 下的 Lua 脚本按板型生成按键参数与映射——board_common/key_common.lua(公共按键参数)、board_common/key_msg.lua(按键消息定义)、board/adkey_v1.lua 与 board/iokey_v1.lua(AD/IO 按键板级实例)。应用侧 apps/demo/hid/modules/user_cfg.c 通过 extern struct adkey_platform_data adkey_data;、extern u8 key_table[3][10]; 引用生成结果,USE_CONFIG_KEY_SETTING = USE_CONFIG_BIN_FILE 控制按键消息设置来自配置文件。
API 参考
void key_init(void)
遍历 key_list[] 调用各驱动的 key_init,完成全部使能按键的硬件初始化。
void key_scan(void) / void key_driver_scan(void *_scan_para)
key_driver_scan 是扫描任务本体,_scan_para 指向 key_driver_para(实际为全局 key_scan_para);key_scan 是对外包装。每个节拍执行:轮询 get_key_value → 更新消抖/长按/连击状态 → 生成 sys_event → 经 key_event_remap 过滤 → 投递消息队列。
void key_active_set(P33_IO_WKUP_EDGE edge) / void key_active_num_set(u8 key_active)
低功耗唤醒配置接口:设置唤醒 IO(P33)的触发边沿与按键激活数量,用于按键唤醒场景的硬件预配置。
void set_key_poweron_flag(uint8_t flag) / uint8_t get_key_poweron_flag(void) / void clear_key_poweron_flag(void)
按键开机标志位的写/读/清。应用在开机流程读取标志,区分"按键唤醒开机"与"上电开机"。
int key_event_remap(struct sys_event *e)(weak,可覆盖)
键值重映射钩子。参数 e 携带按键事件;返回 true 放行事件继续投递,返回 false 拦截。默认实现恒返回 true。
key_emit_t(typedef)
typedef int (*key_emit_t)(uint8_t key_status, uint8_t key_num, uint8_t key_type);
Source: key.h
按键输出注册接口:key_status 事件状态、key_num 键号、key_type 按键类型,供上层注册按键事件回调(替代/补充消息队列投递)。
故障模式、边界情况与并发
按键误触发与消抖
- 机械抖动:GPIO/矩阵按键按下瞬间的抖动由
filter_cnt+filter_time两级消抖吸收,未达门限的抖动不产生事件。设计上采用"累加计数"而非"延时确认",避免在中断/轮询场景中阻塞。 - AD 键与 MIC 共通道干扰:
key_driver_scan内部用静态poweron_cnt计数专门过滤 AD 键与 MIC 连接时电容充放电导致的开机按键误判(源码注释明确说明"一般用于 type-c 耳机"场景,见 key.c)。这是低功耗 SoC 上模拟通道耦合的典型坑,框架已内置规避。 - 首值仲裁:
get_key_value采用"第一个非NO_KEY即返回",多键同时按下时后注册驱动可能被饿死;组合键需通过key_event_remap在事件层合并,而非依赖物理层同时采样。
事件丢失与乱序
- 扫描事件经消息队列异步投递,若应用消费过慢,队列积压可能导致事件延迟;
key_event_remap返回false可主动丢弃(如开机引导阶段)。 - 连击窗口(
click_delay_time=20节拍)内的事件不立即投递,而是暂存于notify_value等待窗口结束,因此连击场景下事件存在固有延迟,业务需容忍。
并发与共享状态
key_scan_para是全局可变状态,扫描任务与应用任务对它的读写需遵循"扫描任务写、业务配置只读"的约定;is_key_active、key_poweron_flag声明为volatile,用于跨任务/中断的标志同步。key_list[]为编译期常量表,运行期不增删,天然线程安全;新增按键类型必须重新编译。
红外解码边界
- 红外接收易受环境光/其他遥控器干扰,解码层需校验引导码与数据位宽,未通过校验应返回
NO_KEY交给框架当作"无按键"处理,避免产生虚假事件。 - 长按重复(如音量连续调节)依赖框架的
long_time/hold_time事件,红外驱动只需持续返回同一键值即可复用该机制。
性能与运维考虑
- 扫描节拍:
key_driver_scan由 tick 定时器驱动,单次扫描仅为数次函数调用与比较,CPU 开销极低;MOUSE_KEY_SCAN_MODE提供更高频扫描的参数集,供鼠标等高响应场景使用。 - 编译期裁剪:未使能的按键驱动不进入
key_list[],同时节省代码段与 RAM——低功耗 BLE 产品对固件体积敏感,按需使能是默认实践。 - 低功耗协同:空闲时按键可配置为唤醒源(
key_active_set/key_active_num_set),避免持续轮询耗电;开机标志位让唤醒后的业务路径与冷启动区分。 - 配置工具化:板级按键参数由
AW31N_config_tool的 Lua 脚本生成(key_common.lua/key_msg.lua/adkey_v1.lua/iokey_v1.lua),改板型不改代码,量产配置与固件解耦。
扩展点
- 新增按键驱动:实现
key_interface_t(key_init+key_get_value),加入key_list[],并补充对应KEY_xxx_EN编译宏。红外键(KEY_TYPE_IR)即按此路径接入。 - 键值重映射:覆盖弱函数
key_event_remap,结合struct key_remap表实现组合键/功能键定制,无需改动框架。 - 按键事件输出:注册
key_emit_t回调可绕过消息队列直接获得按键事件(低延迟路径),或与sys_event并存。 - 扫描参数调优:修改
key_scan_para的filter_time/long_time/hold_time/click_delay_time,适配不同手感与使用场景(如鼠标模式)。 - LED 效果扩展:在
led_control.c之上增加业务级指示模式(充电、连接、低电量等),底层 PWM/LEDC 由ledc.c提供。
相关链接
- 按键框架头文件:key.h
- 按键框架实现:key.c
- 按键驱动:key_drv_io.c、key_drv_ad.c、key_drv_matrix.c
- 红外模块:ir_decoder.c、ir_encoder.c
- LED 模块:led_control.c、ledc.c
- 板级配置脚本:key_common.lua、key_msg.lua、adkey_v1.lua、iokey_v1.lua
- 应用集成示例:user_cfg.c(
adkey_data/pwm_led_data/key_table[3][10]平台数据)、app_main.c(按键开机标志使用) - 兄弟页面:蓝牙 HID 上报与 GATT 服务(见 HID/键盘相关页面);低功耗与电源管理(见电源管理页面)。