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

    • 芯片平台与 SDK 概述
    • 环境搭建与编译工具链
    • 快速开始:选型、编译与烧录
    • 烧录与量产工具
  • 构建系统与板级工程

    • 顶层 Makefile 与编译目标
    • 板级工程与配置
    • 后处理与配置工具
  • HID 人机交互应用

    • HID 应用架构总览
    • 键盘、翻页器与遥控应用
    • 鼠标应用:单模、双模与低延迟
    • 空闲应用与初始化流程
  • BLE 透传与数传应用

    • 透传应用总览
    • 多连接与无连接传输
    • AT 命令模组应用
    • Dongle 适配器应用
  • BSP 公共模块

    • 蓝牙公共处理
    • 按键、LED 与红外
    • 传感器与编码器
    • 存储、VM 与文件系统
    • 电源管理与低功耗
    • 消息调度与通信外设
  • 协议栈与预编译库

    • 蓝牙协议栈库
    • 设备驱动与文件系统库
    • 音频、升级与其他库
  • 开发资料与补丁发布

    • 文档资料中心
    • 版本补丁与兼容性修复

按键、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 的扫描任务统一调度。这样业务层永远只面对"按键值 + 按键事件",不关心物理实现。

扫描结果经过三级处理:

  1. 消抖(filter):filter_cnt 累加到 filter_time(基准 KEY_BASE_CNT = 2)才认为按键有效;
  2. 事件判定(press/long/hold/click):press_cnt 与 long_time / hold_time 比较产生长按与 HOLD 事件,click_cnt 与 click_delay_time 配合产生连击事件;
  3. 投递(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 / 触发功能

流程要点:

  1. 扫描节拍:key_driver_scan 由 tick_timer 周期性调度(key_scan() 为对外包装),每个节拍完成一次全驱动轮询。
  2. 首值优先:get_key_value() 返回第一个非 NO_KEY 的驱动值,多按键同时按下时由数组顺序仲裁。
  3. 事件状态机:press_cnt 递增产生按下/长按/HOLD 事件;抬起后 click_delay_time 窗口内的再按构成连击,窗口结束才投递最终键值(notify_value 暂存待发值)。
  4. 可拦截钩子:key_event_remap 返回 false 可丢弃事件(如开机引导阶段屏蔽按键)。
  5. 异步解耦:事件经消息队列投递,扫描任务不阻塞在应用逻辑上,保证低功耗 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_timeuint82消抖累加门限(基准 KEY_BASE_CNT = 2)
key_scan_para.long_timeuint875(鼠标模式 3)长按判定节拍数
key_scan_para.hold_timeuint890(鼠标模式 3)HOLD 事件判定节拍数
key_scan_para.click_delay_timeuint820抬起后连击等待窗口
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),改板型不改代码,量产配置与固件解耦。

扩展点

  1. 新增按键驱动:实现 key_interface_t(key_init + key_get_value),加入 key_list[],并补充对应 KEY_xxx_EN 编译宏。红外键(KEY_TYPE_IR)即按此路径接入。
  2. 键值重映射:覆盖弱函数 key_event_remap,结合 struct key_remap 表实现组合键/功能键定制,无需改动框架。
  3. 按键事件输出:注册 key_emit_t 回调可绕过消息队列直接获得按键事件(低延迟路径),或与 sys_event 并存。
  4. 扫描参数调优:修改 key_scan_para 的 filter_time / long_time / hold_time / click_delay_time,适配不同手感与使用场景(如鼠标模式)。
  5. 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/键盘相关页面);低功耗与电源管理(见电源管理页面)。
Prev
蓝牙公共处理
Next
传感器与编码器