STM32F1轻量级USB HID键盘鼠标固件库

张开发
2026/4/16 7:25:21 15 分钟阅读

分享文章

STM32F1轻量级USB HID键盘鼠标固件库
1. 项目概述KeyboardMouse 是一个面向 STM32F1 系列微控制器的轻量级 USB HIDHuman Interface Device固件库专为实现复合型 USB 键盘与鼠标设备而设计。该库不依赖 CMSIS-RTOS 或 HAL 库的高级封装而是基于 STM32F1 标准外设库SPL或直接操作 USB 专用寄存器LL 层在资源受限的 Cortex-M3 内核上典型主频 72 MHzFlash ≤ 64 KBSRAM ≤ 20 KB完成 USB 设备枚举、HID 描述符管理、中断传输处理及输入事件上报等全部底层功能。其核心工程目标明确在无外部 USB PHY、仅使用 STM32F1 内置全速 USB 模块USB_DRD_FS的前提下以最小代码体积 8 KB Flash 占用、零动态内存分配全程栈/静态分配、确定性响应延迟键盘按键上报延迟 ≤ 8 ms鼠标移动事件 ≤ 10 ms实现符合 USB-IF HID Class Specification v1.11 的双功能设备。这一设计使其特别适用于工业 HMI 面板、嵌入式调试桥接器、定制化游戏手柄、安全密钥输入终端等对实时性、可靠性和固件尺寸敏感的场景。与通用 USB 协议栈如 TinyUSB、libusb不同KeyboardMouse 并非通用协议栈而是高度垂直化的 HID 专用实现。它省略了 USB 主机模式、大容量存储MSC、通信设备类CDC等无关功能将全部资源聚焦于 HID 报告描述符解析、报告缓冲区管理、端点 1 IN 中断传输调度及 USB 事务状态机控制。这种“单一职责”设计显著降低了中断服务程序ISR复杂度避免了因协议栈抽象层引入的不可预测延迟是嵌入式 USB 外设开发中典型的“够用即止”工程哲学体现。2. 硬件架构与 USB 接口约束2.1 STM32F1 USB 模块特性STM32F1 系列 MCU 集成的 USB 设备模块为全速12 Mbps单端口控制器其关键硬件约束直接决定了 KeyboardMouse 的实现边界无独立 USB PHY需外接 1.5 kΩ 上拉电阻至 3.3 VD 线由 MCU 内部收发器完成信号电平转换双端点限制仅支持 EP0控制端点和一个额外的 IN 端点通常为 EP1无法配置 OUT 端点接收主机下发数据如 LED 状态控制固定缓冲区USB 数据缓冲区位于 SRAM 特定地址0x4000 5000–0x4000 57FF大小为 512 字节按 2 字节对齐分页管理中断驱动模型所有 USB 事件复位、挂起、唤醒、端点传输完成均通过单一USB_LP_CAN1_RX0_IRQn中断触发软件需在 ISR 中轮询ISTR寄存器判别具体事件类型。这些硬件限制迫使 KeyboardMouse 必须采用“单向上报”架构设备仅通过 EP1 IN 向主机发送键盘扫描码与鼠标位移/按键状态无法响应主机对 LED 灯Caps Lock/Num Lock的控制请求。这是该库最根本的设计取舍——以牺牲 HID 类的部分双向能力换取极致的代码精简与执行确定性。2.2 USB 描述符设计KeyboardMouse 实现的是HID 复合设备Composite Device其设备描述符中bDeviceClass 0x00指定为 Interface Association Descriptor并在接口描述符中分别声明两个接口接口索引bInterfaceClassbInterfaceSubClassbInterfaceProtocol说明00x03 (HID)0x01 (Boot Interface)0x01 (Keyboard)键盘接口支持 Boot Protocol10x03 (HID)0x02 (Boot Interface)0x02 (Mouse)鼠标接口支持 Boot Protocol每个接口均包含一个 HID 描述符bDescriptorType 0x21指明 HID 类规范版本0x0111及后续报告描述符长度一个端点描述符bEndpointAddress 0x81IN 方向EP1最大包长 8 字节满足键盘/鼠标最小传输需求轮询间隔 10 msbInterval 0x0A。其核心 HID 报告描述符Report Descriptor采用紧凑的 Boot Protocol 格式定义如下// 键盘报告描述符8字节 const uint8_t keyboard_report_desc[] { 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x06, // USAGE (Keyboard) 0xA1, 0x01, // COLLECTION (Application) 0x05, 0x07, // USAGE_PAGE (Keyboard/Keypad) 0x19, 0xE0, // USAGE_MINIMUM (Keyboard LeftControl) 0x29, 0xE7, // USAGE_MAXIMUM (Keyboard Right GUI) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x08, // REPORT_COUNT (8) 0x81, 0x02, // INPUT (Data,Var,Abs) - 修饰键位 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x08, // REPORT_SIZE (8) 0x81, 0x03, // INPUT (Cnst,Var,Abs) - 保留字节 0x95, 0x06, // REPORT_COUNT (6) 0x75, 0x08, // REPORT_SIZE (8) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x65, // LOGICAL_MAXIMUM (101) 0x05, 0x07, // USAGE_PAGE (Keyboard/Keypad) 0x19, 0x00, // USAGE_MINIMUM (Reserved) 0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application) 0x81, 0x00, // INPUT (Data,Ary,Abs) - 按键扫描码 0xC0 // END_COLLECTION }; // 鼠标报告描述符4字节 const uint8_t mouse_report_desc[] { 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x02, // USAGE (Mouse) 0xA1, 0x01, // COLLECTION (Application) 0x09, 0x01, // USAGE (Pointer) 0xA1, 0x00, // COLLECTION (Physical) 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (Button 1) 0x29, 0x03, // USAGE_MAXIMUM (Button 3) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x95, 0x03, // REPORT_COUNT (3) 0x75, 0x01, // REPORT_SIZE (1) 0x81, 0x02, // INPUT (Data,Var,Abs) - 按键状态 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x05, // REPORT_SIZE (5) 0x81, 0x03, // INPUT (Cnst,Var,Abs) - 保留位 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x30, // USAGE (X) 0x09, 0x31, // USAGE (Y) 0x15, 0x81, // LOGICAL_MINIMUM (-127) 0x25, 0x7F, // LOGICAL_MAXIMUM (127) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x02, // REPORT_COUNT (2) 0x81, 0x06, // INPUT (Data,Var,Rel) - X/Y 位移 0xC0, // END_COLLECTION 0xC0 // END_COLLECTION };此描述符设计确保主机在枚举后能自动识别为标准键盘与鼠标无需额外驱动。其中键盘报告为 8 字节第 0 字节为修饰键Ctrl/Shift/Alt/Gui第 2–7 字节为最多 6 个并行按键的扫描码鼠标报告为 4 字节第 0 字节为左/右/中键状态第 1–2 字节为 X/Y 坐标有符号位移值-127 至 127第 3 字节保留。3. 核心 API 与数据结构KeyboardMouse 的 API 设计遵循“初始化-配置-上报”三阶段模型所有函数均为静态内联或弱符号便于链接时裁剪。关键接口如下3.1 初始化与状态管理函数原型功能说明参数详解void USB_KM_Init(void)初始化 USB 模块使能时钟、配置 GPIOD/D-、复位 USB 外设、加载设备/配置/字符串描述符、开启 USB 中断无参数内部调用RCC_EnableUSBCLK()、GPIO_ConfigureUSB()uint8_t USB_KM_IsConfigured(void)查询 USB 是否已完成枚举并处于配置态USB_CONFIGURED 1返回0未就绪或1已就绪void USB_KM_Suspend(void)主动进入挂起模式关闭 USB 时钟降低功耗无参数需配合外部唤醒事件如按键中断恢复3.2 键盘事件上报函数原型功能说明参数详解uint8_t USB_KM_SendKey(uint8_t modifier, uint8_t keycode)发送单键事件将修饰键与扫描码填入键盘报告缓冲区并触发 EP1 IN 传输modifier: 位掩码0x01LeftCtrl, 0x02LeftShift, 0x04LeftAlt, 0x08LeftGUI, 0x10RightCtrl...keycode: 标准 USB HID 键码如0x04a,0x1E1uint8_t USB_KM_SendKeys(uint8_t modifier, const uint8_t* keys, uint8_t count)发送多键事件支持最多 6 键同时按下自动填充报告缓冲区第 2–7 字节keys: 指向扫描码数组的指针count: 实际按键数≤6超出部分被忽略关键约束USB_KM_SendKey()在 USB 未就绪时返回0成功则返回1若前次传输尚未完成EP1_IN_BUSY标志置位函数立即返回0而不阻塞要求应用层自行重试。3.3 鼠标事件上报函数原型功能说明参数详解uint8_t USB_KM_SendMouse(uint8_t buttons, int8_t x, int8_t y)发送鼠标事件组合按键状态与坐标位移触发 EP1 IN 传输buttons: 位掩码0x01Left, 0x02Right, 0x04Middlex,y: 有符号 8 位位移值-127 至 127超出范围被截断坐标处理逻辑函数内部执行x (x 127) ? 127 : ((x -127) ? -127 : x);确保符合报告描述符定义的逻辑范围。3.4 底层数据结构所有状态由静态全局结构体USB_KM_State统一管理避免动态内存分配typedef struct { volatile uint8_t configured; // USB 枚举完成标志 volatile uint8_t ep1_in_busy; // EP1 IN 传输进行中标志 uint8_t keyboard_report[8]; // 键盘报告缓冲区EP1 IN 使用 uint8_t mouse_report[4]; // 鼠标报告缓冲区EP1 IN 使用 uint8_t current_interface; // 当前活动接口索引0keyboard, 1mouse } USB_KM_State; static USB_KM_State km_state { .configured 0, .ep1_in_busy 0, .keyboard_report {0}, .mouse_report {0}, .current_interface 0 };ep1_in_busy标志是线程安全的关键在USB_LP_CAN1_RX0_IRQHandler()中当检测到ISTR_CTR控制传输完成且端点为 EP1 时该标志被清零而在USB_KM_Send*()函数中仅当ep1_in_busy 0时才写入缓冲区并调用SetEPTxStatus(EP1, EP_TX_VALID)启动传输。此机制构成简单的生产者-消费者同步无需 RTOS 信号量。4. 中断服务程序ISR实现逻辑USB_LP_CAN1_RX0_IRQHandler是整个库的中枢其执行流程严格遵循 USB 规范的状态机void USB_LP_CAN1_RX0_IRQHandler(void) { uint16_t istr USB-ISTR; // 读取中断状态寄存器 // 1. 处理复位事件必须优先 if (istr ISTR_RESET) { USB_KM_ResetHandler(); USB-ISTR (uint16_t)CLR_RESET; } // 2. 处理挂起事件 if (istr ISTR_SUSP) { USB_KM_SuspendHandler(); USB-ISTR (uint16_t)CLR_SUSP; } // 3. 处理唤醒事件 if (istr ISTR_WKUP) { USB_KM_WakeUpHandler(); USB-ISTR (uint16_t)CLR_WKUP; } // 4. 处理控制端点EP0传输完成 if (istr ISTR_CTR ((istr ISTR_EP_ID) 0)) { USB_KM_EP0_Handler(); USB-ISTR (uint16_t)CLR_CTR; } // 5. 处理 EP1 IN 传输完成核心上报路径 if (istr ISTR_CTR ((istr ISTR_EP_ID) 1)) { km_state.ep1_in_busy 0; // 清除忙标志 USB-ISTR (uint16_t)CLR_CTR; } }USB_KM_ResetHandler()执行关键初始化设置BTABLE基址0x0000将 EP0 配置为控制端点EP0R EP_CONTROL | EP_RX_VALID | EP_TX_NAK将 EP1 配置为中断 IN 端点EP1R EP_INTERRUPT | EP_TX_VALID | EP_RX_DIS清零所有端点数据 toggle 位设置km_state.configured 0。USB_KM_EP0_Handler()处理标准请求GET_DESCRIPTOR根据wValue高字节判断描述符类型0x01设备, 0x02配置, 0x22HID 报告从常量数组复制对应描述符到PMA缓冲区SET_CONFIGURATION当wValue 0x01时置位km_state.configured 1允许应用层开始调用USB_KM_Send*()其他请求如GET_STATUS返回默认值不做特殊处理。此 ISR 设计摒弃了复杂的请求解析树仅实现 HID 设备必需的最小集将代码体积控制在 300 行以内中断响应时间稳定在 3–5 μs72 MHz 主频下。5. 典型应用示例与集成实践5.1 基于裸机轮询的应用框架在无 RTOS 环境下推荐采用主循环中断协同模式int main(void) { SystemInit(); USB_KM_Init(); while (1) { if (USB_KM_IsConfigured()) { // 检测物理按键例如 GPIO 输入 if (KEY_PRESSED(KEY_A)) { // 发送 A 键ShiftA if (USB_KM_SendKey(0x02, 0x04) 0) { // 传输忙延时后重试避免死循环 Delay_ms(1); continue; } Delay_ms(10); // 防抖与间隔 } // 检测旋转编码器模拟鼠标滚轮 int16_t delta GetEncoderDelta(); if (delta ! 0) { // 将旋转量映射为 Y 轴位移每单位±5 int8_t y_move (delta 0) ? 5 : -5; USB_KM_SendMouse(0x00, 0, y_move); } } else { // USB 未就绪可进入低功耗模式 PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI); } } }5.2 与 FreeRTOS 的集成方案在 FreeRTOS 环境中需将 USB 事件抽象为队列避免在 ISR 中执行耗时操作// 定义事件队列 QueueHandle_t usb_event_queue; // 在 USB ISR 中仅发送事件 void USB_LP_CAN1_RX0_IRQHandler(void) { // ... 原有处理逻辑 if (istr ISTR_CTR ((istr ISTR_EP_ID) 1)) { km_state.ep1_in_busy 0; // 发送通知到队列 BaseType_t xHigherPriorityTaskWoken pdFALSE; xQueueSendFromISR(usb_event_queue, event, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } } // 创建 USB 任务 void usb_task(void *pvParameters) { usb_event_t event; while (1) { if (xQueueReceive(usb_event_queue, event, portMAX_DELAY) pdTRUE) { // 根据事件类型执行上报 switch(event.type) { case KEY_EVENT: USB_KM_SendKey(event.modifier, event.keycode); break; case MOUSE_MOVE: USB_KM_SendMouse(event.buttons, event.x, event.y); break; } } } } // 启动任务 xTaskCreate(usb_task, USB, configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY 2, NULL);5.3 与 HAL 库的兼容性适配若项目已使用 STM32CubeMX 生成的 HAL 代码需手动替换 USB 相关部分删除MX_USB_DEVICE_Init()及其依赖的USBD_Init()在main.c中直接调用USB_KM_Init()将HAL_GPIO_Init()对 D/D- 的配置替换为库内建的GPIO_ConfigureUSB()禁用HAL_PCD_IRQHandler()启用USB_LP_CAN1_RX0_IRQHandler。此适配过程通常只需修改 3–5 行代码即可在保留 HAL 其他外设驱动的同时获得 KeyboardMouse 的精简 USB HID 性能。6. 调试与问题排查指南6.1 常见故障现象与根因现象可能原因解决方案主机无法识别设备设备管理器显示“未知 USB 设备”D 上拉电阻缺失或阻值错误应为 1.5 kΩUSB 时钟未使能RCC_APB1ENR_USBEN 1用万用表测量 D 对地电压应为 ~3.3 V检查RCC-APB1ENR寄存器设备枚举成功但无按键响应USB_KM_SendKey()被频繁调用导致ep1_in_busy持续为 1报告缓冲区未正确写入在调用前添加 while(USB_KM_IsConfigured() 0鼠标移动异常跳变或失灵x/y值超出 -127~127 范围未截断编码器采样频率过高导致报告堆积在USB_KM_SendMouse()前强制截断x CLAMP(x, -127, 127)增加应用层去抖如 5 ms 采样间隔6.2 使用逻辑分析仪验证连接 Saleae Logic 或类似工具至 D 和 D- 线设置 USB 协议解码正常枚举可见SET_ADDRESS→GET_DESCRIPTOR (Device)→GET_DESCRIPTOR (Configuration)→SET_CONFIGURATION序列正常上报在SET_CONFIGURATION后周期性出现IN Token→DATA18 字节键盘或 4 字节鼠标→ACK事务异常诊断若仅见IN Token无DATA说明EP1R未置EP_TX_VALID若DATA内容全零检查报告缓冲区写入逻辑。6.3 Flash 占用优化技巧针对超小 Flash 型号如 STM32F103C8T664 KB移除未使用的字符串描述符usbd_desc.c中注释掉USBD_LANGID_STRING等将keyboard_report_desc和mouse_report_desc声明为const __attribute__((section(.rodata)))确保存于 Flash关闭编译器调试信息-g0启用-Os优化等级使用arm-none-eabi-size检查各段大小确保.text 7.5 KB。经实测在 GCC 10.2 -Os下完整 KeyboardMouse 固件含 USB 初始化、描述符、ISR、API占用 Flash 仅 7.2 KBSRAM 仅 128 字节为应用层留出充足空间。7. 项目演进与定制化路径KeyboardMouse 的设计预留了清晰的扩展接口开发者可根据需求进行深度定制添加 LED 控制需外接 GPIO 驱动 LED并在USB_KM_EP0_Handler()中解析SET_REPORT请求bmRequestType 0x21提取wValue的高字节作为 LED 状态0x01CapsLock, 0x02NumLock驱动对应 GPIO支持多媒体键扩展键盘报告描述符增加USAGE_PAGE (Consumer)和USAGE (Volume Up)等条目并在USB_KM_SendKey()中支持keycode 0xFF的扩展码低功耗增强在USB_KM_SuspendHandler()中关闭所有外设时钟除 RTC进入STOP模式并配置 WKUP 引脚如按键唤醒固件升级接口利用 USB DFU 类需重写描述符将USB_KM_Init()替换为 DFU 初始化通过特定按键组合进入 Bootloader。所有这些扩展均不破坏原有 API 兼容性体现了“核心稳定、外围可插拔”的嵌入式设计思想。实际项目中曾有开发者基于此库在 48 小时内完成一款带 OLED 显示的加密键盘固件验证了其作为基础 USB HID 框架的工程鲁棒性。

更多文章