按键与 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):
- 消抖(去抖):第一次读到新键值时只记录
filter_value并清零filter_cnt,直接返回;只有连续filter_time轮读到相同值才认为键值稳定(L37-L46)。这是典型的"连续 N 次采样一致"软件消抖,避免机械触点抖动产生误触发。 - 按下/抬起沿检测:
last_key记录上一轮稳定值。cur == NO_KEY && last == 有效键表示抬起沿;反过来表示按下沿。抬起时若press_cnt >= long_time(说明刚经历过长按/HOLD),立即发KEY_EVENT_UP,否则进入连击等待(L49-L59)。 - 连击计数:按下沿时若新键值与
notify_value(上一次待上报键)相同则click_cnt++,不同则重置为 1——即快速按同一键才会计数双击/三击(L60-L69)。 - 连击窗口:抬起后
click_delay_cnt从 1 开始逐轮累加,超过click_delay_time才把click_cnt翻译成 CLICK/DOUBLE/TRIPLE/FOURTH/FIRTH 事件(L76-L97)。代码中保留了//TODO: 在此可以添加任意多击事件的扩展注释。 - 长按/HOLD:持续按住时
press_cnt逐轮累加,先到long_time触发一次KEY_EVENT_LONG,继续累加到hold_time触发KEY_EVENT_HOLD,随后把press_cnt钳制回long_time防止重复触发(L101-L114)。HOLD 语义是"长按之后持续保持"(例如音量连减),而 LONG 是"长按到达"的瞬时事件。 - 事件构造与重映射:所有待上报路径汇合到
_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 实现要点 |
|---|---|---|
iokey | GPIO 高低电平 | 读引脚电平,多键位映射,支持上拉/下拉配置 |
adkey | ADC 分压电阻网络 | 采样电压,按阈值区间映射不同键位(一个 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_time | u32 (ms) | 各驱动自行定义 | 扫描周期,决定消抖/长按的时间分辨率 |
filter_time | u8 | 各驱动自行定义 | 消抖采样次数,0 表示跳过消抖 |
long_time | u8 | 各驱动自行定义 | 长按判定所需的扫描轮数 |
hold_time | u8 | 各驱动自行定义 | HOLD 事件判定轮数(须大于 long_time) |
click_delay_time | u8 | 各驱动自行定义 | 抬起后连击窗口轮数,窗口内再次按下计为连击 |
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
- 新增按键硬件类型:在
KEY_DRIVER_TYPE枚举追加类型 → 新建xxx_key.c/h实现xxx_init()与get_value()→ 在key_driver_init增加对应TCFG_XXX_KEY_ENABLE分支。框架的key_type与get_value多态设计使新增类型无需改动状态机。 - 组合键/按键重映射:覆盖弱函数
key_event_remap,可修改e->u.key.value或返回false吞掉事件,实现"音量+上一曲 → 切换 EQ"等组合语义(key_driver.c)。 - 更多击事件:
key_driver_scan的多击分支留有//TODO: 在此可以添加任意多击事件注释,可在click_cnt判定处扩展六击、七击等自定义事件。 - USB 设备类扩展:新增设备类驱动时,遵循"释放路径与使能宏一一对应"的既有模式,在
usb_device_mode的class == 0分支补充xxx_release、在挂载分支补充xxx_init,并核对端点冲突。 - 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相关文档