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

    • 项目概述与芯片平台
    • 环境搭建与工具链安装
    • 编译与烧录指南
    • 工程结构总览
  • 应用层与公共模块

    • GP MCU 主应用入口
    • AT 指令与调试模块
    • 电池检测与电源管理
    • EEPROM 与参数存储
    • 按键与 USB 设备驱动
    • 音频解码与 APA 语音播报
  • 外设驱动与示例

    • 高精度 ADC(HADC)
    • 通用 ADC 与定时器
    • UART / SPI / IIC 通信外设
    • MCPWM 与电机控制
    • RTC 与低功耗唤醒
    • 段码 LCD 驱动
    • NOR Flash 与红外编解码组件
  • 显示与 UI 系统

    • LCD 驱动与字库引擎
    • UI 平台与控件绘制
    • UI 工程与资源生成工具
  • 系统底层与芯片平台

    • cd09 芯片平台与预编译库
    • GPIO 与 IIC 底层驱动
    • 系统文件系统与设备模型
  • 启动引导与固件升级

    • UBOOT 引导工程
    • 固件升级机制
  • 开发工具与资源

    • 编译脚本与命令行工具
    • 音频文件转换工具
    • 硬件资料与文档资源

按键与 USB 设备驱动

按键与 USB 设备驱动是 AC82N 系列 MCU SDK 应用层中负责人机交互输入与USB 通信的两大基础子系统:按键驱动通过定时扫描 + 消抖 + 事件判定,把物理按键(IO/ADC/红外/触摸)翻译成统一的系统事件;USB 设备驱动则管理从机(device)与主机(host)两种模式下的枚举、类驱动(MSD/HID/UAC/CDC)与数据传输。

Purpose and Scope

本文档介绍 sdk/apps/common/device/key/ 与 sdk/apps/common/device/usb/ 目录下的驱动实现,覆盖:

  • 按键驱动框架 key_driver:扫描任务、消抖、单击/双击/多击/长按/HOLD 事件判定、按键值重映射。
  • 四类底层按键采集驱动:iokey(IO 按键)、adkey(ADC 按键)、irkey(红外按键)、touch_key(触摸按键)。
  • USB 设备驱动:usb_config 配置层、从机模式类驱动(MSD/UAC/HID/CDC/custom_hid)的初始化与释放、主机模式(host)基础 API 与存储类 usb_storage。

以下内容不在本文档范围,请在对应目录页查阅:上层按键事件消费(应用层事件分发 sys_event 的处理)、文件系统/音频服务与 USB MSD/UAC 的具体交互、以及芯片底层 USB PHY 寄存器操作(include_lib/driver/...)。本页聚焦驱动本身的机制与用法。

Overview

在嵌入式消费类产品(如蓝牙音箱、MCU 控制板)中,按键是最主要的用户输入手段,而 USB 承担充电检测、U 盘读写、PC 通信等角色。SDK 的设计目标是把这两类硬件差异极大的外设抽象为统一、可配置、低 CPU 开销的驱动服务:

  • 按键:所有按键驱动共享一个 key_driver_scan 扫描状态机。不同硬件(GPIO 直接读取、ADC 分压、红外解码、电容触摸)只需实现 get_value() 采集函数和一组时序参数,即可获得一致的消抖、连击、长按语义。扫描由 usr_timer 定时器周期触发,不占用独立线程。
  • USB:usb_config 是上层唯一入口,向下分别对接从机协议栈(usb_stack)与主机协议栈(usb_host)。设备类(MSD/HID/UAC/CDC)通过编译宏 TCFG_USB_SLAVE_XXX_ENABLE 按需裁剪,usb_device_mode() 负责按 class 参数动态挂载/释放类驱动。

这种"一个统一状态机 + 可插拔采集驱动"和"一个配置入口 + 类驱动注册表"的设计,让产品工程师只需修改配置宏与参数表即可适配新硬件,无需改动框架代码。

Architecture

flowchart TD
    subgraph sg_App["应用层"]
        App["应用任务 / sys_event 事件处理"]
    end

    subgraph sg_Key["按键驱动子系统 (device/key)"]
        KeyDriver["key_driver.c 扫描状态机"]
        UsrTimer["usr_timer 周期定时器"]
        IoKey["iokey (IO 按键)"]
        AdKey["adkey (ADC 按键)"]
        IrKey["irkey (红外按键)"]
        TouchKey["touch_key (触摸按键)"]
        SysNotify["sys_event_notify 事件上报"]
        Remap["key_event_remap 弱函数重映射"]
    end

    subgraph sg_USB["USB 设备驱动子系统 (device/usb)"]
        UsbCfg["usb_config 配置层"]
        UsbDev["usb_device 从机模式"]
        UsbHost["usb_host 主机模式"]
        UsbStorage["usb_storage 存储类"]
        ClsDrv["MSD / UAC / HID / CDC / custom_hid 类驱动"]
    end

    subgraph sg_HW["硬件层"]
        KeyHw["GPIO / ADC 分压电路"]
        IrRx["红外接收头"]
        TouchCtl["触摸检测电路"]
        UsbPhy["USB PHY (DM/DP)"]
    end

    UsrTimer -->|"scan_time ms 周期触发"| KeyDriver
    KeyDriver --> IoKey
    KeyDriver --> AdKey
    KeyDriver --> IrKey
    KeyDriver --> TouchKey
    IoKey --> KeyHw
    AdKey --> KeyHw
    IrKey --> IrRx
    TouchKey --> TouchCtl
    IoKey -->|"get_value()"| KeyDriver
    AdKey -->|"get_value()"| KeyDriver
    IrKey -->|"get_value()"| KeyDriver
    TouchKey -->|"get_value()"| KeyDriver
    KeyDriver --> Remap
    Remap --> SysNotify
    SysNotify -->|"SYS_KEY_EVENT"| App

    App -->|"usb_device_mode / usb_host_config"| UsbCfg
    UsbCfg --> UsbDev
    UsbCfg --> UsbHost
    UsbDev --> ClsDrv
    UsbHost --> UsbStorage
    ClsDrv --> UsbPhy
    UsbStorage --> UsbPhy
    UsbHost --> UsbPhy

上图展示了两个子系统的分层结构:

  • 按键子系统:usr_timer 是唯一驱动源,周期调用 key_driver_scan(key_driver.c)。扫描函数通过 struct key_driver_para 中的函数指针 get_value 多态地读取任意一种底层按键驱动;判定结果先经过可重写的 key_event_remap(组合键重映射钩子),再以 SYS_KEY_EVENT 事件投递给应用层。
  • USB 子系统:应用层只面对 usb_config 暴露的 API。从机模式(usb_device)按 class 参数挂载对应类驱动;主机模式(usb_host)负责枚举与批量/控制传输,usb_storage 在其上实现 U 盘读写。

两子系统相互独立、无直接耦合,统一通过系统事件与配置宏与应用层交互——这是 SDK 驱动层"低耦合、可裁剪"设计的关键。

按键驱动子系统详解

驱动类型与统一参数结构

key_driver.h 用枚举定义四类按键硬件来源,并预留扩展位:

typedef enum __KEY_DRIVER_TYPE {
    KEY_DRIVER_TYPE_IO = 0x0,   // IO 直接读取
    KEY_DRIVER_TYPE_AD,         // ADC 分压检测
    KEY_DRIVER_TYPE_IR,         // 红外遥控
    KEY_DRIVER_TYPE_TOUCH,      // 触摸按键
    KEY_DRIVER_TYPE_MAX,
} KEY_DRIVER_TYPE;

Source: key_driver.h

每个按键驱动实例对应一个 struct key_driver_para,它同时是扫描参数表和运行时状态机(const 字段是配置、普通字段是运行状态):

struct key_driver_para {
    const u32 scan_time;        // 按键扫描频率, 单位ms
    u8 last_key;                // 上一次get_value按键值
    //== 用于消抖类参数
    u8 filter_value;            // 用于按键消抖
    u8 filter_cnt;              // 用于按键消抖时的累加值
    const u8 filter_time;       // 当filter_cnt累加到base_cnt值时, 消抖有效
    //== 用于判定长按和HOLD事件参数
    const u8 long_time;         // 按键判定长按数量
    const u8 hold_time;         // 按键判定HOLD数量
    u8 press_cnt;               // 与long_time和hold_time对比, 判断long_event和hold_event
    //== 用于判定连击事件参数
    u8 click_cnt;               // 单击次数
    u8 click_delay_cnt;         // 按键被抬起后等待连击事件延时计数
    const u8 click_delay_time;  // 按键被抬起后等待连击事件延时数量
    u8 notify_value;            // 在延时的待发送按键值
    u8 key_type;
    u8(*get_value)(void);       // 底层采集函数指针
};

Source: key_driver.h

设计要点(WHY):

  • 时间单位是"扫描次数"而非毫秒。long_time、hold_time、click_delay_time 都是"经过多少轮扫描"的计数值,配合每实例独立的 scan_time 换算成真实时间。这样 IO 按键可以 10ms 扫一次、红外按键 20ms 扫一次,各自参数互不干扰。
  • get_value 函数指针实现多态。框架不关心按键是 IO 电平、ADC 电压还是红外码,只约定返回值:NO_KEY(0xff)表示"无按键",其余为有效键值。硬件差异被完全隔离在四个 *_key.c 采集驱动内。
  • key_type 用于事件溯源。同一个系统里 IO 键、触摸键可能都叫 KEY_POWER,事件里携带 key_type 让应用层区分来源。

扫描状态机:消抖 → 连击/长按 → 上报

key_driver_scan 是每个按键驱动的核心状态机,由 usr_timer 按 scan_time 周期调用,完整流程如下:

flowchart TD
    Start([定时器触发 key_driver_scan]) --> Get["cur_key_value = get_value()"]
    Get --> Deb{"cur != filter_value<br/>且 filter_time != 0?"}
    Deb -->|"是 (键值第一次变化)"| ResetCnt["filter_cnt = 0<br/>filter_value = cur"]
    ResetCnt --> End0([本轮返回, last_key = cur])
    Deb -->|"否"| DebCnt{"filter_cnt < filter_time?"}
    DebCnt -->|"是 (消抖中)"| IncCnt["filter_cnt++"]
    IncCnt --> End0
    DebCnt -->|"否 (消抖完成)"| Cmp{"cur == last_key?"}
    Cmp -->|"否, cur == NO_KEY"| Released["按键抬起<br/>press_cnt >= long_time → KEY_EVENT_UP<br/>否则 click_delay_cnt = 1"]
    Released --> End0
    Cmp -->|"否, cur 为新键"| Pressed["新键按下<br/>press_cnt = 1<br/>同键 click_cnt++ / 异键重置"]
    Pressed --> End0
    Cmp -->|"是, cur == NO_KEY"| Idle{"click_cnt > 0?<br/>(抬起后等待连击延时)"}
    Idle -->|"否"| End0
    Idle -->|"是"| Delay{"click_delay_cnt ><br/>click_delay_time?"}
    Delay -->|"否"| IncDelay["click_delay_cnt++"]
    IncDelay --> End0
    Delay -->|"是 (连击窗口关闭)"| Multi{"click_cnt ≥ 5/4/3/2?"}
    Multi -->|"≥5"| Ev5["KEY_EVENT_FIRTH_CLICK 五击"]
    Multi -->|"=4"| Ev4["KEY_EVENT_FOURTH_CLICK 四击"]
    Multi -->|"=3"| Ev3["KEY_EVENT_TRIPLE_CLICK 三击"]
    Multi -->|"=2"| Ev2["KEY_EVENT_DOUBLE_CLICK 双击"]
    Multi -->|"=1"| Ev1["KEY_EVENT_CLICK 单击"]
    Cmp -->|"是, cur 有效 (持续按住)"| Hold["press_cnt++"]
    Hold --> Long{"press_cnt == long_time?"}
    Long -->|"是"| EvLong["KEY_EVENT_LONG 长按"]
    Long -->|"否"| HoldChk{"press_cnt == hold_time?"}
    HoldChk -->|"是"| EvHold["KEY_EVENT_HOLD 持续保持<br/>(press_cnt 重置为 long_time)"]
    HoldChk -->|"否"| End0
    EvLong --> Notify["构造 sys_event (SYS_KEY_EVENT)<br/>click_cnt/notify_value 清 0"]
    EvHold --> Notify
    Ev5 --> Notify
    Ev4 --> Notify
    Ev3 --> Notify
    Ev2 --> Notify
    Ev1 --> Notify
    Notify --> Remap{"key_event_remap(&e)?"}
    Remap -->|"true"| Send["sys_event_notify(&e) 投递事件"]
    Remap -->|"false"| Drop["丢弃 (已被重映射消费)"]
    Send --> End1([结束: last_key = cur_key_value])
    Drop --> End1
    End0 --> End1

对应源码中的关键分支(key_driver.c):

  1. 消抖(去抖):第一次读到新键值时只记录 filter_value 并清零 filter_cnt,直接返回;只有连续 filter_time 轮读到相同值才认为键值稳定(L37-L46)。这是典型的"连续 N 次采样一致"软件消抖,避免机械触点抖动产生误触发。
  2. 按下/抬起沿检测:last_key 记录上一轮稳定值。cur == NO_KEY && last == 有效键 表示抬起沿;反过来表示按下沿。抬起时若 press_cnt >= long_time(说明刚经历过长按/HOLD),立即发 KEY_EVENT_UP,否则进入连击等待(L49-L59)。
  3. 连击计数:按下沿时若新键值与 notify_value(上一次待上报键)相同则 click_cnt++,不同则重置为 1——即快速按同一键才会计数双击/三击(L60-L69)。
  4. 连击窗口:抬起后 click_delay_cnt 从 1 开始逐轮累加,超过 click_delay_time 才把 click_cnt 翻译成 CLICK/DOUBLE/TRIPLE/FOURTH/FIRTH 事件(L76-L97)。代码中保留了 //TODO: 在此可以添加任意多击事件 的扩展注释。
  5. 长按/HOLD:持续按住时 press_cnt 逐轮累加,先到 long_time 触发一次 KEY_EVENT_LONG,继续累加到 hold_time 触发 KEY_EVENT_HOLD,随后把 press_cnt 钳制回 long_time 防止重复触发(L101-L114)。HOLD 语义是"长按之后持续保持"(例如音量连减),而 LONG 是"长按到达"的瞬时事件。
  6. 事件构造与重映射:所有待上报路径汇合到 _notify 标签,构造 sys_event(type = SYS_KEY_EVENT,携带 key_type/event/value),清零 click_cnt 与 notify_value,调用弱函数 key_event_remap 后由 sys_event_notify 投递(L118-L130)。

初始化与定时器注册

key_driver_init 按编译宏逐个初始化已使能的按键驱动,并为每个成功初始化的驱动注册独立扫描定时器:

void key_driver_init(void)
{
#if ((defined TCFG_IOKEY_ENABLE) && (TCFG_IOKEY_ENABLE))
    if (iokey_init() == 0) {
        usr_timer_add((void *)&iokey_scan_para, key_driver_scan, iokey_scan_para.scan_time, 0);
    }
#endif
#if ((defined TCFG_ADKEY_ENABLE) && (TCFG_ADKEY_ENABLE))
    if (adkey_init() == 0) {
        usr_timer_add((void *)&adkey_scan_para, key_driver_scan, adkey_scan_para.scan_time, 0);
    }
#endif
#if ((defined TCFG_IRKEY_ENABLE) && (TCFG_IRKEY_ENABLE))
    if (irkey_init() == 0) {
        usr_timer_add((void *)&irkey_scan_para, key_driver_scan, irkey_scan_para.scan_time, 0);
    }
#endif
#if ((defined TCFG_TOUCH_KEY_ENABLE) && (TCFG_TOUCH_KEY_ENABLE))
    if (touch_key_init() == 0) {
        usr_timer_add((void *)&touch_key_scan_para, key_driver_scan, touch_key_scan_para.scan_time, 0);
    }
#endif
}

Source: key_driver.c

注意几个设计细节:

  • 同一扫描函数复用:四个驱动共用 key_driver_scan,差异完全体现在各自的 *_scan_para(参数表 + get_value)上,代码零冗余。
  • 每驱动独立定时器:每个按键类型有自己的 scan_time(如 IO 键 10ms、红外键 30ms),usr_timer_add 的 _scan_para 指针同时充当定时器回调参数。
  • 编译期裁剪:未定义的宏用 defined(...) 双重判断,保证即使宏未定义也不报错——这是 SDK 中大量使用的配置安全写法。

底层采集驱动(iokey / adkey / irkey / touch_key)

四个采集驱动(iokey.c、adkey.c、irkey.c、touch_key.c)职责单一:把硬件信号翻译成键值,供 get_value 返回:

驱动硬件原理get_value 实现要点
iokeyGPIO 高低电平读引脚电平,多键位映射,支持上拉/下拉配置
adkeyADC 分压电阻网络采样电压,按阈值区间映射不同键位(一个 ADC 通道支持多键)
irkey红外接收头解码解码 NEC 等红外协议,返回遥控码对应的键值
touch_key电容触摸检测读取触摸通道状态,映射触摸键值

它们均导出各自的 *_init() 与全局参数表 *_scan_para,由 key_driver_init 统一注册,应用层无需直接调用。

USB 设备驱动子系统详解

配置层 usb_config:主机与从机的统一入口

usb_config.h 定义了 USB 子系统的对外 API,同时覆盖主机(host)与从机(device/gadget)两种模式。所有 API 都以 usb_dev usb_id 为第一参数,支持多 USB 控制器(如 USB0/USB1)。

从机侧核心 API(usb_config.h):

/**@brief   USB从机初始化配置
  * @param[in]  usb_id USB的id号
  * @return     0:成功
  */
u32 usb_config(const usb_dev usb_id);

/**@brief   USB从机释放
  * @param[in]  usb_id USB的id号
  * @return     0:成功
  */
u32 usb_release(const usb_dev usb_id);

/**@brief   USB内存空间初始化
  * @param[in]  无
  * @return     无
  */
void usb_memory_init();

Source: usb_config.h

主机侧核心 API(usb_config.h):

void usb_host_config(usb_dev usb_id);                 // USB主机模式配置
void usb_host_free(usb_dev usb_id);                   // USB主机模式释放
void *usb_h_get_ep_buffer(const usb_dev usb_id, u32 ep); // 获取端点BUFFER地址
void usb_h_isr_reg(const usb_dev usb_id, u8 priority, u8 cpu_id);  // 主机中断注册
void usb_g_isr_reg(const usb_dev usb_id, u8 priority, u8 cpu_id);  // 从机中断注册
void usb_sof_isr_reg(const usb_dev usb_id, u8 priority, u8 cpu_id);// SOF中断注册
void *usb_alloc_ep_dmabuffer(const usb_dev usb_id, u32 ep, u32 dma_size); // 端点DMA缓冲

Source: usb_config.h

设计意图:把底层 asm/usb.h 的寄存器级操作与上层类驱动隔离。应用层只调用配置/释放/中断注册,端点的 DMA 缓冲分配与中断优先级由框架统一管理,避免各产品各自操作寄存器造成冲突。

从机模式:usb_device 与类驱动挂载

usb_device.c 在 TCFG_USB_SLAVE_ENABLE 编译开关下实现从机模式。初始化时按固定顺序完成控制器配置、SIE 初始化、DMA 缓冲绑定与中断注册(usb_device.c):

static void usb_device_init(const usb_dev usb_id)
{
    usb_config(usb_id);
    usb_g_sie_init(usb_id);
#if defined(FUSB_MODE) && FUSB_MODE
    usb_write_power(usb_id, 0x40);
#elif defined(FUSB_MODE) && (FUSB_MODE == 0)
    usb_write_power(usb_id, 0x60);
#endif
    usb_slave_init(usb_id);
    u8 *ep0_dma_buffer = usb_alloc_ep_dmabuffer(usb_id, 0, 64);
    usb_set_dma_raddr(usb_id, 0, ep0_dma_buffer);
    usb_set_dma_raddr(usb_id, 1, ep0_dma_buffer);
    /* ... ep 2/3/4 绑定同一缓冲 ... */
#if USB_SUSPEND_RESUME
    usb_write_intr_usbe(usb_id, INTRUSB_RESET_BABBLE | INTRUSB_SUSPEND);
#else
    usb_write_intr_usbe(usb_id, INTRUSB_RESET_BABBLE);
#endif
    usb_g_isr_reg(usb_id, 3, 0);
}

Source: usb_device.c

关键点:

  • EP0 用 64 字节 DMA 缓冲,且端点 0~4 复用同一缓冲地址(控制传输使用);usb_set_dma_raddr 设置接收地址,usb_g_isr_reg(usb_id, 3, 0) 以中断优先级 3 注册到 CPU0。
  • 挂起/恢复可选:USB_SUSPEND_RESUME 宏决定是否使能 INTRUSB_SUSPEND 中断,用于低功耗场景。
  • FUSB_MODE 控制收发器功率配置(0x40/0x60),适配不同硬件走线。

usb_device_mode(usb_id, class) 是应用层挂载设备类的入口:class == 0 时按宏逐一释放所有已注册类驱动并进入保持态;class != 0 时先 usb_add_desc_config 添加类描述符,再按使能宏初始化对应类(MSD/UAC/HID/CDC/custom_hid)。释放路径与使能宏一一对应,例如:

#if TCFG_USB_SLAVE_MSD_ENABLE
        msd_release(usb_id);
#endif
#if TCFG_USB_SLAVE_AUDIO_ENABLE
        uac_release(usb_id);
#endif
#if TCFG_USB_SLAVE_CDC_ENABLE
        cdc_release(usb_id);
#endif
#if TCFG_USB_CUSTOM_HID_ENABLE
        custom_hid_release(usb_id);
#endif
#if TCFG_USB_SLAVE_HID_ENABLE
        hid_release(usb_id);
#endif
        usb_device_hold(usb_id);
        return 0;

Source: usb_device.c

usb_device_hold 依次调用 usb_g_hold 与 usb_release,把控制器挂起并释放资源(usb_device.c)。整套机制支持运行期动态切换设备类(如从"U 盘模式"切换到"声卡模式"),因为每次切换都会完整地释放旧类、重建描述符。

主机模式:usb_host 与 usb_storage

主机侧由 usb_host.c 提供枚举与传输管理,usb_host_config(usb_id) 完成主机模式初始化。在主机之上,usb_storage.c/h 实现 Bulk-Only Transport (BOT) 协议的 U 盘读写(usb_bulk_transfer.c 负责批量传输、usb_ctrl_transfer.c 负责控制传输)。这些模块构成典型的协议栈分层:

flowchart LR
    subgraph sg_App2["应用层"]
        FS["文件系统 (fat/挂载)"]
    end
    subgraph sg_Proto["USB 协议栈"]
        SCSI["SCSI 命令层 (usb_storage)"]
        BOT["Bulk-Only 传输 (usb_bulk_transfer)"]
        CTRL["控制传输 (usb_ctrl_transfer)"]
        HOST["USB 主机核心 (usb_host)"]
    end
    subgraph sg_Phy2["硬件"]
        PHY["USB PHY (DM/DP)"]
    end
    FS --> SCSI
    SCSI --> BOT
    BOT --> CTRL
    BOT --> HOST
    CTRL --> HOST
    HOST --> PHY

核心流程:从按键按下到 USB 事件

下图以"按键切换 USB 模式"为例,展示两个子系统与系统事件的协作时序:

sequenceDiagram
    participant T as usr_timer 定时器
    participant S as key_driver_scan
    participant D as 按键采集驱动 (iokey 等)
    participant R as key_event_remap
    participant E as sys_event 分发
    participant A as 应用任务
    participant U as usb_device_mode

    T->>S: 周期触发 (scan_time ms)
    S->>D: get_value()
    D-->>S: 键值 / NO_KEY
    S->>S: 消抖 + 连击/长按/HOLD 判定
    S->>R: key_event_remap(&e) 组合键重映射
    R-->>S: true (继续投递)
    S->>E: sys_event_notify(SYS_KEY_EVENT)
    E->>A: 投递按键消息 (type/event/value)
    A->>A: 应用解析按键 (如"长按切换 USB 模式")
    A->>U: usb_device_mode(usb_id, class)
    U-->>A: 挂载/释放对应类驱动

整个链路的关键在于:按键驱动只负责"键值 → 事件"的翻译,USB 驱动只负责"模式 → 类驱动"的管理,两者通过应用层解耦,互不感知对方存在。

Usage Examples

以下示例均提取自仓库实际源码,展示框架的关键用法。

示例 1:自定义按键参数表(产品适配)

每个按键驱动在各自的 .c 文件中定义参数表,例如 IO 按键的 iokey_scan_para 通过 get_value 绑定底层采集函数,并通过 scan_time/filter_time/long_time/hold_time/click_delay_time 配置时序语义:

struct key_driver_para iokey_scan_para = {
    .scan_time        = 10,       // 10ms 扫描一次
    .filter_time      = 2,        // 连续 2 次采样一致才有效
    .long_time        = 50,       // 500ms 判定长按
    .hold_time        = 80,       // 800ms 触发 HOLD
    .click_delay_time = 30,       // 抬起后 300ms 连击窗口
    .key_type         = KEY_DRIVER_TYPE_IO,
    .get_value        = iokey_get_value,   // 硬件采集函数
};

Source: iokey.h(参数表定义于 iokey.c,结构体原型见 key_driver.h)

示例 2:组合键重映射钩子(扩展点)

框架默认提供空实现的弱函数,产品可覆盖它实现"组合键 → 新键值"的映射。返回 false 将吞掉原事件(例如按键同时按下的第二个键不再单独上报):

int __attribute__((weak)) key_event_remap(struct sys_event *e)
{
    return true;
}

Source: key_driver.c

示例 3:按键扫描主循环(消抖与连击判定)

扫描状态机中的消抖入口与多击判定是框架最核心的算法段:

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

Source: key_driver.c

                    if (scan_para->click_cnt >= 5) {
                        key_event = KEY_EVENT_FIRTH_CLICK;  //五击
                    } else if (scan_para->click_cnt >= 4) {
                        key_event = KEY_EVENT_FOURTH_CLICK;  //4击
                    } else if (scan_para->click_cnt >= 3) {
                        key_event = KEY_EVENT_TRIPLE_CLICK;  //三击
                    } else if (scan_para->click_cnt >= 2) {
                        key_event = KEY_EVENT_DOUBLE_CLICK;  //双击
                    } else {
                        key_event = KEY_EVENT_CLICK;  //单击
                    }

Source: key_driver.c

示例 4:USB 设备类动态切换(应用层调用)

应用层通过 usb_device_mode 在"释放全部类"与"挂载指定类"之间切换,切换前先将 DM/DP 引脚置为高阻并延时 15ms,保证总线复位干净:

int usb_device_mode(const usb_dev usb_id, const u32 class)
{
    u8 class_index = 0;
    if (class == 0) {
        gpio_set_mode(IO_PORT_SPILT(IO_PORT_DM + 2 * usb_id), PORT_HIGHZ);
        gpio_set_mode(IO_PORT_SPILT(IO_PORT_DP + 2 * usb_id), PORT_HIGHZ);
        usb_mdelay(15);
        /* ... 按宏释放 msd/uac/cdc/custom_hid/hid ... */
        usb_device_hold(usb_id);
        return 0;
    }
    usb_add_desc_config(usb_id, MAX_INTERFACE_NUM, NULL);
    /* ... 按 TCFG_USB_SLAVE_XXX_ENABLE 挂载类驱动 ... */
}

Source: usb_device.c

Configuration Options

两类子系统均以编译期宏(定义于 app_config.h 等配置头文件)与运行时参数表双轨配置。

按键驱动配置

配置项类型默认行为说明
TCFG_IOKEY_ENABLE宏 (0/1)由板级配置决定使能 IO 按键驱动并注册扫描定时器
TCFG_ADKEY_ENABLE宏 (0/1)由板级配置决定使能 ADC 分压按键驱动
TCFG_IRKEY_ENABLE宏 (0/1)由板级配置决定使能红外按键驱动
TCFG_TOUCH_KEY_ENABLE宏 (0/1)由板级配置决定使能触摸按键驱动
scan_timeu32 (ms)各驱动自行定义扫描周期,决定消抖/长按的时间分辨率
filter_timeu8各驱动自行定义消抖采样次数,0 表示跳过消抖
long_timeu8各驱动自行定义长按判定所需的扫描轮数
hold_timeu8各驱动自行定义HOLD 事件判定轮数(须大于 long_time)
click_delay_timeu8各驱动自行定义抬起后连击窗口轮数,窗口内再次按下计为连击

USB 驱动配置

配置项类型默认行为说明
TCFG_USB_SLAVE_ENABLE宏 (0/1)由板级配置决定从机模式总开关,usb_device.c 整体编译条件
TCFG_USB_SLAVE_MSD_ENABLE宏 (0/1)由板级配置决定使能 U 盘(Mass Storage)类驱动
TCFG_USB_SLAVE_AUDIO_ENABLE宏 (0/1)由板级配置决定使能声卡(UAC)类驱动
TCFG_USB_SLAVE_CDC_ENABLE宏 (0/1)由板级配置决定使能虚拟串口(CDC)类驱动
TCFG_USB_SLAVE_HID_ENABLE宏 (0/1)由板级配置决定使能标准 HID 类驱动
TCFG_USB_CUSTOM_HID_ENABLE宏 (0/1)由板级配置决定使能自定义 HID 类驱动
USB_SUSPEND_RESUME宏 (0/1)视功耗需求使能挂起/恢复中断(INTRUSB_SUSPEND)
FUSB_MODE宏由硬件决定配置收发器功率(0x40/0x60)
MAX_INTERFACE_NUM常量协议栈默认单控制器最大接口数,用于描述符分配

API Reference

按键驱动

void key_driver_init(void)

  • 初始化所有使能按键驱动并注册扫描定时器(key_driver.c)。
  • 参数:无。返回:无。
  • 在系统启动早期调用一次;每个使能驱动通过 usr_timer_add 注册独立周期任务。

int key_event_remap(struct sys_event *e)(弱函数,可覆盖)

  • 按键事件上报前调用,用于组合键/键值重映射(key_driver.c)。
  • 参数:e — 待发送的 SYS_KEY_EVENT 事件,可修改其 u.key 字段。
  • 返回:true 继续投递,false 丢弃事件。

static void key_driver_scan(void *_scan_para)

  • 每驱动扫描状态机入口,由定时器回调(key_driver.c)。
  • 参数:_scan_para — 指向 struct key_driver_para。返回:无。
  • 内部完成消抖、单击/双击/多击、长按/HOLD、抬起事件判定与上报。

USB 驱动

u32 usb_config(const usb_dev usb_id)

  • USB 从机初始化配置(usb_config.h)。
  • 返回:0 成功,非 0 失败。

u32 usb_release(const usb_dev usb_id)

  • USB 从机资源释放(usb_config.h)。
  • 返回:0 成功,非 0 失败。

void usb_host_config(usb_dev usb_id) / void usb_host_free(usb_dev usb_id)

  • USB 主机模式配置与释放(usb_config.h)。参数:usb_id — USB 控制器编号。

void *usb_alloc_ep_dmabuffer(const usb_dev usb_id, u32 ep, u32 dma_size)

  • 分配指定端点的 DMA 缓冲(usb_config.h)。
  • 参数:ep — 端点号;dma_size — 缓冲字节数。返回:缓冲地址指针。

void usb_h_isr_reg(const usb_dev usb_id, u8 priority, u8 cpu_id) / void usb_g_isr_reg(const usb_dev usb_id, u8 priority, u8 cpu_id) / void usb_sof_isr_reg(const usb_dev usb_id, u8 priority, u8 cpu_id)

  • 分别注册主机/从机/SOF 中断(usb_config.h)。参数:priority — 中断优先级;cpu_id — 目标 CPU 编号。

int usb_device_mode(const usb_dev usb_id, const u32 class)

  • 从机设备类挂载/释放入口(usb_device.c)。class == 0 释放全部类并保持;否则挂载对应类。返回:0 成功。

Failure Modes, Edge Cases & Concurrency

按键驱动的边界与失效场景

  • 消抖跳过陷阱:filter_time == 0 时消抖分支整体跳过(源码 && scan_para->filter_time 判空),抖动信号会直接进入事件判定。若产品按键存在机械抖动,必须保证 filter_time >= 1。
  • 长按/HOLD 参数约束:hold_time 必须大于 long_time,否则 press_cnt == long_time 先触发 LONG 后,press_cnt 继续累加永远无法命中 hold_time,HOLD 事件将丢失。源码在 HOLD 触发后把 press_cnt 钳制为 long_time(key_driver.c),防止按住期间 HOLD 反复触发——这是刻意设计,但也意味着"按住不放"期间只有一次 LONG + 一次 HOLD。
  • 连击窗口竞态:连击判定依赖"抬起后 click_delay_time 轮内再次按下同一键"。若用户按得太慢(超过窗口),前一次点击会先以 CLICK 上报,后续按下重新开始计数——这是所有多击检测方案固有的语义限制。
  • NO_KEY 键值冲突:NO_KEY 固定为 0xff,底层 get_value 返回的有效键值不得等于 0xff,否则会被框架误判为"无按键"。
  • 并发/重入:key_driver_scan 运行在定时器上下文中,事件构造与计数更新均在同一回调内顺序完成,天然串行、无锁。若产品在 key_event_remap 或应用事件回调中调用会阻塞的 API(如长延时、等待信号量),将影响后续扫描周期,表现为按键响应迟钝——重映射钩子应保持轻量。
  • 初始化失败静默跳过:*_init() 返回非 0(如 GPIO 申请失败)时,key_driver_init 仅跳过该驱动注册,不报错也不中断其他驱动,便于部分硬件缺失时降级运行。

USB 驱动的边界与失效场景

  • 类驱动释放顺序:usb_device_mode(class == 0) 依次释放 msd/uac/cdc/custom_hid/hid。若应用在类驱动仍持有文件句柄或音频流时切换模式,可能出现资源泄漏——应在切换前先关闭文件系统挂载与音频通道。
  • DMA 缓冲共享:从机 EP0~EP4 复用同一 64 字节 DMA 缓冲(usb_device.c),控制传输与中断传输共用地址。高吞吐的批量类驱动(MSD)必须另行通过 usb_alloc_ep_dmabuffer 分配独立缓冲,否则数据会被覆盖。
  • 总线复位与挂起:INTRUSB_RESET_BABBLE 中断处理总线复位;USB_SUSPEND_RESUME 关闭时挂起中断被屏蔽,系统无法感知总线挂起,低功耗唤醒需由外部逻辑(如 GPIO 检测 VBUS)补足。
  • 端点冲突风险:多个类驱动同时挂载时共享端点资源。usb_device.c 中保留了 usb_ep_conflict_check 的调用(当前被注释,usb_device.c),多类组合(如 MSD + UAC)时需人工核对端点分配,避免两个类驱动申请同一端点号。
  • 主机侧热插拔:usb_host/usb_storage 需处理设备拔出导致的传输超时/错误返回;usb_bulk_transfer 失败应由上层重试或卸载,避免死等。

Performance & Operational Considerations

  • 按键扫描开销极低:每周期仅执行一次 GPIO/ADC 读取与几个整数比较,scan_time 通常 10~30ms,对 CPU 占用可忽略;多个按键驱动各自独立定时,互不阻塞。
  • 时间分辨率 = scan_time:长按/连击的最短可分辨时间是 scan_time。要求更精确的响应(如 200ms 长按)时需减小 scan_time 或调整对应轮数参数。
  • 中断优先级固定:从机中断以优先级 3 注册在 CPU0(usb_g_isr_reg(usb_id, 3, 0))。若系统存在更高实时性要求的外设(如音频 I2S),需评估 USB 中断对音频中断的抢占影响。
  • 内存段隔离:usb_device.c 通过 #pragma bss_seg/data_seg/code_seg/const_seg 把 USB 代码与数据放入专用段(.usb.data / .usb.text 等,usb_device.c),便于链接脚本管理 DMA 可达内存与低功耗关段。
  • 运行期模式切换延时:usb_device_mode(class == 0) 包含 15ms 的 usb_mdelay 等待总线复位(usb_device.c),频繁切换模式(如每秒来回切)会产生可见的阻塞,应避免在交互热路径中高频调用。

Extension Points

  1. 新增按键硬件类型:在 KEY_DRIVER_TYPE 枚举追加类型 → 新建 xxx_key.c/h 实现 xxx_init() 与 get_value() → 在 key_driver_init 增加对应 TCFG_XXX_KEY_ENABLE 分支。框架的 key_type 与 get_value 多态设计使新增类型无需改动状态机。
  2. 组合键/按键重映射:覆盖弱函数 key_event_remap,可修改 e->u.key.value 或返回 false 吞掉事件,实现"音量+上一曲 → 切换 EQ"等组合语义(key_driver.c)。
  3. 更多击事件:key_driver_scan 的多击分支留有 //TODO: 在此可以添加任意多击事件 注释,可在 click_cnt 判定处扩展六击、七击等自定义事件。
  4. USB 设备类扩展:新增设备类驱动时,遵循"释放路径与使能宏一一对应"的既有模式,在 usb_device_mode 的 class == 0 分支补充 xxx_release、在挂载分支补充 xxx_init,并核对端点冲突。
  5. USB 类驱动注册:usb_add_desc_config(usb_id, MAX_INTERFACE_NUM, NULL) 为动态描述符注册入口,可传入自定义描述符构建回调实现复合设备(如 MSD + HID 组合)。

Tests

仓库中未在 device/key 与 device/usb 目录发现独立单元测试文件(以源码目录结构为准)。按键状态机(消抖/连击/长按)的时序行为建议通过以下手段验证:

  • 用逻辑分析仪抓取 GPIO 波形,对照 scan_time/filter_time 验证消抖窗口;
  • 在 key_event_remap 中断点,确认单击/双击/长按/HOLD 的事件序列与参数表换算的时间一致;
  • USB 模式切换使用真实 U 盘/PC 枚举测试,重点验证 class == 0 释放后再次挂载的描述符完整性。

Related Links

  • 按键参数表与驱动入口:key_driver.h、key_driver.c
  • 底层按键采集驱动:sdk/apps/common/device/key/iokey.c、adkey.c、irkey.c、touch_key.c
  • USB 配置层:usb_config.h、sdk/apps/common/device/usb/usb_config.c
  • USB 从机模式:usb_device.c(类驱动位于 include_lib/driver/device/usb/device/)
  • USB 主机模式:sdk/apps/common/device/usb/host/usb_host.c、usb_storage.c、usb_bulk_transfer.c、usb_ctrl_transfer.c
  • 芯片底层 USB 头文件:sdk/include_lib/driver/cpu/cd09/asm/usb.h、sdk/include_lib/driver/device/usb/usb_phy.h
  • 系统事件机制(按键事件的上游消费者):应用层 sys_event 相关文档
Prev
EEPROM 与参数存储
Next
音频解码与 APA 语音播报