外设驱动(按键、红外、SPI、USB)
本文档系统介绍 AD24N SDK 中 BSP 层的外设驱动能力,覆盖按键(IO/AD 按键、红外遥控按键)、SPI(LED 灯串)与 USB(设备端/主机端协议栈)四类外设的驱动框架、实现机制与配置方式。
Purpose and Scope
本页面向"外设驱动"这一子系统,说明 AD24N 语音 MCU 如何通过统一的驱动接口把物理外设事件转化为系统消息,并解释各驱动的内部实现:
- 按键子系统:
key.c统一框架 +key_drv_io.c(IO 按键)、key_drv_ad.c(AD 按键)、key_ir.c(红外按键)等具体驱动; - SPI 外设:
cpu/led_spi.c中的 SPI 灯串(LED)驱动; - USB 协议栈:
common/usb/下的设备端(CDC/音频/HID/大容量存储)与主机端(控制传输)实现。
不属于本页范围、由其他目录页覆盖的内容:音频解码/播放(decoder/)、电源与低功耗管理(config/lib_power_config.c)、消息系统与任务调度(msg.h/post_msg 仅作为按键事件的消费方被引用)。如需了解这些能力,请参见对应目录页。
Overview
AD24N 的 BSP(板级支持包)位于 sdk/app/bsp/,其中 common/ 存放与具体芯片引脚无关的通用外设逻辑,cpu/ 存放与 CPU 硬件寄存器相关的实现。外设驱动在整个软件栈中处于最底层,向上为应用层提供两类接口:
- 轮询型驱动(按键):应用主循环周期性调用
get_key_value(),驱动返回当前键值,框架再通过消息过滤器把键值翻译成u16系统消息并投递到消息队列。 - 中断/状态机驱动(红外、SPI、USB):依赖硬件定时器捕获(gptimer)、SPI 控制器或 USB PHY 中断驱动。
设计上,按键框架采用**驱动接口表(key_interface_t)**注册机制:每种按键类型导出一个静态接口描述符,框架在编译期用条件编译(KEY_IO_EN/KEY_AD_EN/KEY_IR_EN 等宏)把启用的驱动收集进 key_list 数组,从而做到"新增一种按键类型不改动框架代码"。红外驱动内部又按 SEL_IR_MODE 分成标准 NEC 协议解码与简易自解码两种模式。USB 则按 USB_DEVICE_CLASS_CONFIG 位掩码组合多个设备类(Speaker、MIC、HID、Mass Storage),属于多类复合设备设计。
Architecture
flowchart TD
subgraph sg_App["应用层"]
App["应用主循环 / 消息处理"]
MsgFilter["key_msg_filter 消息过滤器"]
end
subgraph sg_Key["按键子系统 common/key/"]
KeyFramework["key.c 框架<br/>key_init / get_key_value / key2msg_emit"]
KeyIO["key_drv_io.c<br/>KEY_TYPE_IO"]
KeyAD["key_drv_ad.c<br/>KEY_TYPE_AD"]
KeyIR["key_ir.c<br/>KEY_TYPE_IR"]
KeyMatrix["key_matrix.c<br/>KEY_TYPE_MATRIX"]
KeyTouch["key_touch.c<br/>KEY_TYPE_TOUCH"]
end
subgraph sg_Spi["SPI 外设 cpu/"]
LedSpi["led_spi.c<br/>HW_SPI1 灯串"]
end
subgraph sg_Usb["USB 协议栈 common/usb/"]
UsbDevice["device/<br/>CDC / Audio / HID / MassStorage"]
UsbHost["host/<br/>usb_ctrl_transfer"]
UsbClassDef["usb_std_class_def.h<br/>USB_DEVICE_CLASS_CONFIG"]
end
subgraph sg_Hw["硬件层"]
GPIO["GPIO / ADC 引脚"]
Timer["GPTimer 捕获"]
SpiCtrl["SPI 控制器"]
UsbPhy["USB PHY"]
end
App --> MsgFilter
MsgFilter --> KeyFramework
KeyFramework -->|"条件编译收集"| KeyIO
KeyFramework --> KeyAD
KeyFramework --> KeyIR
KeyFramework --> KeyMatrix
KeyFramework --> KeyTouch
KeyIO --> GPIO
KeyAD --> GPIO
KeyIR --> Timer
App --> LedSpi
LedSpi --> SpiCtrl
App --> UsbDevice
App --> UsbHost
UsbDevice --> UsbPhy
UsbClassDef --> UsbDevice
架构说明:按键框架是典型的"注册表 + 轮询"结构——key.c 在编译期把启用的驱动接口(key_interface_t)收集进 key_list,运行期 key_init() 逐个调用 key_init 钩子完成初始化,get_key_value() 按序轮询各驱动的 key_get_value() 钩子,命中即返回。SPI 与 USB 则与应用层直接耦合:led_spi.c 直接操作 SPI 控制器寄存器实现灯串时序,USB 协议栈以 USB_DEVICE_CLASS_CONFIG 位掩码为总开关组织设备类与主机类代码。
按键子系统
统一框架(key.c)
按键框架的核心是驱动接口描述符 key_interface_t 与编译期收集的驱动表。key_list 数组按条件编译宏依次收纳各按键驱动:
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
#if KEY_IR_EN
&key_ir_info,
#endif
#if KEY_TOUCH_EN
&key_touch_info,
#endif
};
Source: key.c
这种"接口表 + 条件编译"的设计意图是:新增按键类型(如触摸)只需要实现一个 key_interface_t 并加上对应 KEY_XXX_EN 宏,框架代码完全不需要改动,从而把驱动扩展成本降到最低。
初始化阶段,框架遍历 key_list 依次调用各驱动的初始化钩子:
void key_init(void)
{
key_puts("key init\n");
#if (KEY_IO_EN || KEY_AD_EN || KEY_MATRIX_EN || KEY_IR_EN || KEY_TOUCH_EN)
for (int i = 0; i < (sizeof(key_list) / sizeof(key_list[0])); i++) {
if (key_list[i]->key_init) {
key_list[i]->key_init();
}
}
#endif
}
Source: key.c
事件上报路径由 key2msg_emit() 完成:它先调用应用注册的消息过滤器 key_msg_filter(通过 key_table_sel() 注入),把 (key_status, key_num, key_type) 三元组翻译成 u16 消息;若消息有效(非 NO_MSG),先唤醒语音任务(d_key_voice_kick()),再调用 post_msg(1, msg) 投递到消息队列。该设计把"硬件键值"与"应用语义"解耦——同一个键值在不同产品上可以映射为不同消息。
static void key2msg_emit(u8 key_status, u8 key_num, u8 key_type)
{
key_printf("key_status:%d key_num:%d key_type:%d \n", key_status, key_num, key_type);
if (key_msg_filter) {
u16 msg = key_msg_filter(key_status, key_num, key_type);
if (msg != NO_MSG) {
d_key_voice_kick();
int ret = post_msg(1, msg);
if (ret != 0) {
key_printf("error !!! msg pool full \n");
}
}
}
}
Source: key.c
轮询入口 get_key_value() 遍历 key_list,逐个调用 key_get_value();驱动返回 -1 表示该类型未启用/无事件,返回 NO_KEY 表示无按键,否则记录 key_num 与 key_type 并返回 true。注意代码中先调用一次 key_get_value() 判 -1,随后再次调用取真实键值,属于"探测 + 取值"两段式设计,避免在未启用驱动上做无谓处理。
IO 按键驱动(key_drv_io.c)
IO 按键直接读取 GPIO 电平,初始化即调用宏 KEY_INIT() 完成引脚配置:
static void io_key_init(void)
{
KEY_INIT();
}
Source: key_drv_io.c
其接口描述符把自身注册为 KEY_TYPE_IO:
.key_type = KEY_TYPE_IO,
.key_init = io_key_init,
.key_get_value = get_iokey_value,
Source: key_drv_io.c
KEY_INIT() 与 get_iokey_value() 通常在 app_config.h 中由板级配置展开,按键引脚、有效电平(高/低有效)都通过配置宏定义,保证同一份驱动代码可复用于不同硬件。
AD 按键驱动(key_drv_ad.c)
AD 按键通过 ADC 采样电压区分多个按键(串联电阻分压),只需一根 ADC 引脚即可扩展多个键:
void ad_key_init(void)
{
...
}
Source: key_drv_ad.c
.key_type = KEY_TYPE_AD,
.key_init = ad_key_init,
.key_get_value = get_adkey_value,
Source: key_drv_ad.c
get_adkey_value() 内部按电压区间把 ADC 采样值映射为键号,不同按键对应不同分压电阻,因此映射表与硬件电阻网络强相关,属于典型的"用模拟量换引脚数"的取舍——牺牲 ADC 通道,节省 GPIO。
红外按键驱动(key_ir.c)
红外驱动支持两种模式,由 SEL_IR_MODE 宏选择:
- 标准 NEC 协议(
STANDARD_IR):自动选择一个空闲 gptimer,配置为下降沿捕获(GPTIMER_MODE_CAPTURE_EDGE_FALL),irq_cb = NULL表示直接使用内置 NEC 解码器(ir_decoder_init)。若用户自定义红外协议,可自行编写中断回调赋给irq_cb:
#if (STANDARD_IR == SEL_IR_MODE)
/* 标准ENC协议红外按键驱动自动选择空闲timer使用 */
static struct gptimer_config if_dec_cfg;
#else
/* 简易红外按键驱动选择IR_KEY_TIMER使用 */
static const struct irflt_platform_data irflt_data = {
.irflt_io = IR_KEY_IO_SEL,
.timer = IR_KEY_TIMER,
};
#endif
Source: key_ir.c
static void ir_key_init(void)
{
#if (STANDARD_IR == SEL_IR_MODE)
log_info("standard ir key init\n");
memset((void *)&if_dec_cfg, 0, sizeof(struct gptimer_config));
if_dec_cfg.mode = GPTIMER_MODE_CAPTURE_EDGE_FALL;
if_dec_cfg.capture.port = (IR_KEY_IO_SEL / 16);
if_dec_cfg.capture.pin = BIT(IR_KEY_IO_SEL % 16);
if_dec_cfg.capture.filter = 0;
if_dec_cfg.capture.max_period = 110 * 1000;
if_dec_cfg.irq_cb = NULL; //NULL即为采用标准NEC协议,若用户自定义红外协议,自行编写该中断即可
if_dec_cfg.irq_priority = IRQ_IRTMR_IP,
ir_decoder_init(&if_dec_cfg);
#else
...
#endif
}
Source: key_ir.c
- 简易模式:使用固定
IR_KEY_TIMER与IR_KEY_IO_SEL引脚,驱动内部自行完成载波滤波与键值解析。
解码得到的键值通过查表 IRTabFF00[] 映射为系统按键号(IR_00、IR_01……):
static const u8 IRTabFF00[] = {
NKEY_00, NKEY_01, NKEY_02, NKEY_03, NKEY_04, NKEY_05, NKEY_06, IR_06, IR_15, IR_08, NKEY_0A, NKEY_0B, IR_12, IR_11, NKEY_0E, NKEY_0F,
...
};
Source: key_ir.c
查表之后还有一级软件去抖 key_filter():只有连续 KEY_BASE_CNT 次采样值相同才认定键值稳定,否则保持旧值:
static u8 key_filter(u8 key)
{
static u8 used_key = NO_KEY;
static u8 old_key;
static u8 key_counter;
if (old_key != key) {
key_counter = 0;
old_key = key;
} else {
key_counter++;
if (key_counter == KEY_BASE_CNT) {
used_key = key;
}
}
return used_key;
}
Source: key_ir.c
该去抖是"计数确认"型:键值变化立即重置计数,连续 N 次相同才更新输出,从而滤除红外信号抖动与边沿毛刺。IR 按键产生的消息顺序为"短按抬起/长按 → 连按",由框架注释明确约定。
SPI 外设驱动(led_spi.c)
led_spi.c 位于 cpu/ 目录,直接面向 CPU 的 SPI 控制器,用于驱动灯串(RGB LED 级联)。其核心是 led_spi_init(),把指定的 SPI 外设与数据输出引脚绑定,配置成适合灯串协议的时序:
void led_spi_init(hw_spi_dev spi, u32 spi_do)
{
...
}
Source: led_spi.c
典型调用把 SPI1 与 IO_PORTA_03 绑定为灯串数据线:
led_spi_init(HW_SPI1, IO_PORTA_03);
Source: led_spi.c
设计意图:灯串协议(如 SK6812/WS2812 类)要求纳秒级精确的"0/1 码"脉冲宽度,用普通 GPIO 翻转难以满足;这里复用 SPI 控制器以固定位速率输出编码后的数据流,把时序精确性交给硬件外设,CPU 只需按字节组织颜色数据。hw_spi_dev 枚举(HW_SPI1 等)与 IO_PORTA_03 引脚宏表明该驱动与具体芯片引脚绑定,属于 BSP 层最贴近硬件的实现。
USB 协议栈(common/usb/)
USB 代码按"设备端 / 主机端"分为两个目录:
usb/device/:设备端各类(CDC 虚拟串口、USB Audio Speaker/MIC、HID、Mass Storage);usb/host/:主机端(控制传输、枚举等);usb_std_class_def.h:设备类位掩码总开关。
设备类组合由 USB_DEVICE_CLASS_CONFIG 位掩码决定,注释中给出参考组合:
// #define USB_DEVICE_CLASS_CONFIG (SPEAKER_CLASS|MIC_CLASS|HID_CLASS|MASSSTORAGE_CLASS)
Source: usb_std_class_def.h
该宏是编译期常量,usb_audio_interface.h 等文件据此裁剪代码:
#if USB_DEVICE_CLASS_CONFIG & MIC_CLASS
extern cbuffer_t cbuf_usb_mic_o;
#endif
Source: usb_audio_interface.h
设备端 CDC 类对外暴露回调注册接口,供应用层注入唤醒处理与数据输出处理:
u32 cdc_desc_config(const usb_dev usb_id, u8 *ptr, u32 *itf);
void cdc_set_wakeup_handler(void (*handle)(struct usb_device_t *usb_device));
void cdc_set_output_handle(void *priv, int (*output_handler)(void *priv, u8 *buf, u32 len));
Source: cdc.h
主机端负责设备枚举阶段的标准请求,例如获取设备描述符:
int usb_get_device_descriptor(struct usb_host_device *usb_dev, struct usb_device_descriptor *desc);
int usb_get_string_descriptor(struct usb_host_device *usb_dev, struct usb_device_descriptor *desc);
int set_address(struct usb_host_device *usb_dev, u8 devnum);
Source: usb_ctrl_transfer.h
设计要点:USB 属于事件/中断驱动型外设,与按键的轮询模型不同——设备端依赖 USB 中断(SOF、EP0 请求、端点完成)推进协议状态机;主机端则通过控制传输完成地址分配(set_address)与描述符读取(usb_get_device_descriptor),随后进入配置阶段。设备类按位掩码组合的方式,使一个 USB 口可以同时呈现为"声卡 + 虚拟串口 + 存储盘"的复合设备,适配 AD24N 的语音类应用(如 U 盘播放 + 声卡回采 + 调试串口)。
核心流程
按键事件端到端流程
以"用户按下 IO 按键 → 应用收到消息"为例,完整链路如下:
sequenceDiagram
participant U as 用户
participant IO as key_drv_io.c<br/>get_iokey_value
participant F as key.c 框架<br/>get_key_value
participant MF as key_msg_filter<br/>消息过滤器
participant Q as 消息队列<br/>post_msg
participant A as 应用消息处理
U->>IO: 按下按键(GPIO 电平变化)
loop 主循环轮询
F->>IO: 调用 key_get_value()
IO-->>F: 返回键值 key_num
F->>F: 记录 key_type = KEY_TYPE_IO
F-->>MF: key2msg_emit(status, num, type)
MF-->>F: 返回翻译后的 u16 消息(NO_MSG 则丢弃)
F->>Q: d_key_voice_kick() + post_msg(1, msg)
Q-->>A: 应用循环取出消息并处理
end
关键点:整个按键识别是轮询模型,由应用主循环驱动;key2msg_emit() 是硬件键值到系统消息的唯一转换点,消息过滤器为 NULL 或返回 NO_MSG 时事件被静默丢弃,这是"按键只做通知、不做业务"的分层设计。
红外按键中断流程
红外路径与 IO 按键不同:标准 NEC 模式下,gptimer 下降沿捕获中断驱动 ir_decoder_init 内置解码器完成 32 位 NEC 帧解析,解码出的键值经 IRTabFF00 查表、key_filter 去抖后,由 get_key_value 轮询入口取走并走相同的 key2msg_emit 上报路径。即:采集用中断,识别用轮询,两者在 key_ir_info 接口处汇合。
配置选项
| 配置宏 | 类型 | 默认/取值 | 说明 |
|---|---|---|---|
KEY_IO_EN | 布尔 | 由板级配置 | 使能 IO 按键驱动,决定 key_io_info 是否进入 key_list |
KEY_AD_EN | 布尔 | 由板级配置 | 使能 AD 分压按键驱动 |
KEY_MATRIX_EN | 布尔 | 由板级配置 | 使能矩阵键盘驱动 |
KEY_IR_EN | 布尔 | 由板级配置 | 使能红外按键驱动 |
KEY_TOUCH_EN | 布尔 | 由板级配置 | 使能触摸按键驱动 |
SEL_IR_MODE | 枚举 | STANDARD_IR 或简易模式 | 选择红外解码方式(标准 NEC / 简易自解码) |
IR_KEY_IO_SEL | 引脚宏 | 板级定义 | 红外接收引脚;标准模式用 port = IO/16, pin = BIT(IO%16) 换算 |
IR_KEY_TIMER | 定时器号 | 板级定义 | 简易模式使用的 gptimer |
KEY_BASE_CNT | 数值 | 板级定义 | 红外去抖连续确认次数 |
IRQ_IRTMR_IP | 中断优先级 | 板级定义 | 红外捕获中断优先级 |
USB_DEVICE_CLASS_CONFIG | 位掩码 | SPEAKER_CLASS|MIC_CLASS|HID_CLASS|MASSSTORAGE_CLASS | 组合使能 USB 设备类 |
STANDARD_IR | 常量 | 板级定义 | 标准 NEC 模式标识 |
KEY_INIT() / get_iokey_value() | 宏/函数 | 板级实现 | IO 按键引脚初始化与读取,由应用配置展开 |
配置均在 app_config.h(或对应板级配置头文件)中定义,框架通过 #if 预编译裁剪代码,因此未使能的驱动不占用任何代码与 RAM 空间。
API 参考
void key_init(void)
初始化所有已使能的按键驱动。遍历 key_list 并调用各驱动的 key_init 钩子。应在系统上电后、主循环轮询前调用一次。
- 参数:无
- 返回:无
void key_table_sel(void *msg_filter)
注册按键消息过滤器。在关中断(local_irq_disable/enable)保护下写入 key_msg_filter,保证与轮询路径并发安全。
- 参数:
msg_filter(u16 (*)(u8, u8, u8))— 过滤器回调,返回翻译后的消息或NO_MSG - 返回:无
bool get_key_value(key_io_t *p_key)
轮询各按键驱动,取回当前有效键值。驱动返回 -1 表示该类型未启用,NO_KEY 表示无按键;命中时填充 p_key->key_num 与 p_key->key_type 并返回 true。
- 参数:
p_key(key_io_t *)— 输出结构,含key_num、key_type - 返回:
true表示取到有效按键,false表示无事件
void led_spi_init(hw_spi_dev spi, u32 spi_do)
初始化 SPI 灯串外设,绑定 SPI 控制器与数据输出引脚。
- 参数:
spi(hw_spi_dev)— SPI 外设枚举,如HW_SPI1;spi_do(u32)— 输出引脚宏,如IO_PORTA_03 - 返回:无
void cdc_set_wakeup_handler(void (*handle)(struct usb_device_t *usb_device))
注册 USB CDC 设备唤醒回调(例如 USB 挂起后的远程唤醒)。
- 参数:
handle— 唤醒回调函数指针 - 返回:无
int usb_get_device_descriptor(struct usb_host_device *usb_dev, struct usb_device_descriptor *desc)
主机端枚举阶段获取设备描述符。
- 参数:
usb_dev— 主机设备句柄;desc— 输出描述符缓冲区 - 返回:
0成功,非零失败
故障模式、边界情况与并发
消息队列满
key2msg_emit() 中 post_msg() 返回非 0 时打印 error !!! msg pool full。这是按键连发风暴下最典型的故障:应用处理不及时导致消息积压。由于按键事件本身是"有损可接受"的(用户重按即可),框架选择直接丢弃并打印日志,而不是阻塞或重试,属于典型的嵌入式"尽力而为"策略。
轮询路径的并发安全
key_table_sel() 通过 local_irq_disable()/local_irq_enable() 保护 key_msg_filter 的写入,与主循环中 key2msg_emit() 的读取形成互斥——因为在单核 MCU 上,唯一可能打断轮询的异步源就是中断,关中断即可保证指针读写的原子性。
红外去抖与连按
key_filter() 使用静态变量保存历史状态,连续 KEY_BASE_CNT 次相同才更新输出。边界情况:若红外信号在阈值附近抖动(偶尔丢一帧),计数会不断复位,表现为按键偶尔不触发;若 KEY_BASE_CNT 过小,则可能把毛刺误判为按键。这是灵敏度与抗干扰之间的经典权衡。
标准/简易红外模式差异
SEL_IR_MODE 切换时,初始化代码路径完全不同(gptimer 捕获 vs 固定 timer 轮询),两种模式不能混用。标准模式 irq_cb = NULL 时使用内置 NEC 解码器,一旦用户自定义协议却忘记实现回调,ir_decoder_init 将按 NEC 解析出错误键值——自定义协议必须自行填充 irq_cb。
AD 按键电压重叠
AD 按键依赖电阻分压,若相邻键分压间隔小于 ADC 噪声幅度,get_adkey_value() 的电压区间映射可能误判为相邻键。该问题由硬件电阻网络决定,软件侧只能通过增大采样稳定时间缓解。
性能与运维考量
- 轮询开销:
get_key_value()每次遍历最多 5 个驱动并各自采样,应用主循环频率直接决定按键响应延迟。IO/AD 按键适合低频轮询(几 ms 级),红外采集则完全由 gptimer 中断承担,不占用轮询时间。 - 红外中断实时性:
max_period = 110 * 1000(µs)限制捕获最大周期,IRQ_IRTMR_IP决定中断优先级;NEC 协议 562.5µs 载波周期要求中断响应必须足够快,否则丢帧。 - SPI 灯串:数据经 SPI 控制器移位输出,CPU 只负责填充颜色缓冲区,刷新率由应用决定;注意
HW_SPI1与IO_PORTA_03的引脚复用是否与其他外设冲突。 - USB 复合设备:
USB_DEVICE_CLASS_CONFIG每增加一个类,描述符与端点资源随之增加;启用前需确认芯片 USB 端点数量与 RAM 缓冲足够。
扩展点
- 新增按键类型:实现一个
key_interface_t(key_type/key_init/key_get_value三件套),在key.c的key_list中加入对应条件编译项即可,框架零改动。 - 自定义红外协议:标准模式下将
if_dec_cfg.irq_cb从NULL改为自写中断回调,即可替换内置 NEC 解码器。 - 按键消息重映射:通过
key_table_sel()注入自定义key_msg_filter,把(status, num, type)翻译为任意系统消息,实现同一硬件键值在不同产品上的差异化行为。 - USB 设备类裁剪:修改
USB_DEVICE_CLASS_CONFIG位掩码组合类,或参照cdc.h的回调注入模式为其他类增加应用层钩子。
测试
测试用例未在 sdk/app/bsp/ 目录中发现独立的单元测试文件;按键、红外与 USB 的验证主要依赖板级 DEMO 与日志(key_printf/log_info)。key_ir.c 的 key_filter 去抖逻辑、IRTabFF00 查表等纯函数部分适合后续补充单元测试。
Related Links
- 按键框架 key.c
- IO 按键驱动 key_drv_io.c
- AD 按键驱动 key_drv_ad.c
- 红外按键驱动 key_ir.c
- SPI 灯串驱动 led_spi.c
- USB 设备类定义 usb_std_class_def.h
- USB CDC 设备接口 cdc.h
- USB 主机控制传输 usb_ctrl_transfer.h
- 相关目录页:解码器与播放链路(
sdk/app/bsp/common/decoder/)、电源配置(sdk/app/bsp/common/config/lib_power_config.c)