1. 项目概述KeyboardMouse 是一个面向 STM32F1 系列微控制器的轻量级 USB HIDHuman Interface Device固件库专为实现复合型 USB 键盘与鼠标设备而设计。该库不依赖第三方 USB 协议栈如 ST 的 USB Device Library 或 Keil ARM USB Stack而是基于 STM32F1 标准外设库Standard Peripheral Library, SPL或 HAL 库需适配直接操作 USB 专用寄存器与端点缓冲区以最小资源开销达成符合 USB-IF HID 类规范的设备功能。其核心工程目标明确在 Flash ≤ 16 KB、SRAM ≤ 6 KB 的典型 STM32F103C8T6“Blue Pill”等入门级 MCU 上稳定运行全功能 HID 复合设备——即单个 USB 设备同时声明为HID Keyboard和HID Mouse共享同一 USB 配置描述符通过独立的报告描述符Report Descriptor定义两类输入通道并支持标准 HID 中断传输Interrupt IN Endpoint上报按键与鼠标移动/按键事件。该方案规避了传统 USB 协议栈中冗余的状态机、描述符管理器与中断服务复用逻辑将 USB 底层事务处理压缩至最简路径仅维护端点 0控制传输与端点 1中断 IN的双端点模型所有 HID 报告数据均通过端点 1 的 8 字节 FIFO 直接提交USB 中断服务程序ISR内完成令牌响应、数据收发与状态同步无任何动态内存分配或任务调度介入。这种设计使其天然适配裸机Bare-Metal环境亦可无缝集成至 FreeRTOS 等实时操作系统中作为底层驱动模块。2. 硬件与协议基础2.1 STM32F1 USB 模块特性约束STM32F1 系列 MCU 内置的 USB 2.0 全速12 Mbps设备控制器具有以下关键限制直接决定了 KeyboardMouse 的架构选择固定端点数量仅支持 4 个双向端点EP0–EP3其中 EP0 强制用于控制传输剩余 EP1–EP3 可配置为 IN/OUT 方向端点缓冲区大小受限每个端点最大缓冲区为 64 字节但实际可用性受 USB 控制器 FIFO 分配策略影响无 DMA 支持所有 USB 数据传输必须由 CPU 通过寄存器读写完成中断服务程序需高效处理数据搬移时钟精度要求严苛USB 全速通信要求 48 MHz 精确时钟源通常由 PLL 倍频后经 USB 专用分频器提供时钟偏差 ±0.25% 将导致连接失败。KeyboardMouse 严格遵循上述约束采用 EP0控制 EP1IN双端点精简模型。EP1 配置为中断 IN 端点最大包长MaxPacketSize设为 8 字节——此值是 HID 键盘报告8 字节修饰键 6 个普通键与鼠标报告4 字节按键 X/Y 位移 滚轮的最小公倍数确保单次传输可容纳任一类型完整报告且符合 USB HID 规范对中断端点包长的推荐值键盘常用 8 字节鼠标常用 4 字节。2.2 USB HID 复合设备协议结构USB HID 复合设备并非物理上两个设备而是在单一 USB 配置Configuration中通过一个接口Interface下声明多个 HID 类子接口Interface Association Descriptor, IAD或在同一接口内使用多个报告 IDReport ID区分不同逻辑设备。KeyboardMouse 采用后者——单接口多报告 ID 模式其核心协议要素如下描述符类型关键字段KeyboardMouse 实现值工程意义设备描述符bDeviceClass 0x000x00Use Class Info in Interface表明设备类信息由接口描述符定义配置描述符bNumInterfaces 0x010x01仅使用一个接口降低枚举复杂度接口描述符bInterfaceClass 0x03bInterfaceSubClass 0x01bInterfaceProtocol 0x010x03 (HID)0x01 (Boot Interface Subclass)0x01 (Keyboard Protocol)声明为 Boot Keyboard 接口兼容 BIOS/UEFI 环境HID 描述符bDescriptorType 0x21wDescriptorLength0x21 (HID)包含 Report Descriptor 长度指向后续报告描述符端点描述符 (EP1 IN)bEndpointAddress 0x81wMaxPacketSize 0x00080x81 (IN, EP1)0x0008 (8 bytes)中断 IN 端点用于上报所有 HID 事件报告描述符Report Descriptor是 HID 设备的灵魂它以紧凑的字节序列定义主机如何解析从设备接收的原始字节流。KeyboardMouse 的报告描述符采用Report ID 机制结构如下// 报告描述符精简版十六进制字节流 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x06, // USAGE (Keyboard) 0xA1, 0x01, // COLLECTION (Application) 0x85, 0x01, // REPORT_ID (1) —— 键盘报告 ID 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) —— 8 位修饰键 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x08, // REPORT_SIZE (8) 0x81, 0x03, // INPUT (Const,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) —— 6 个普通键 0xC0, // END_COLLECTION 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x02, // USAGE (Mouse) 0xA1, 0x01, // COLLECTION (Application) 0x85, 0x02, // REPORT_ID (2) —— 鼠标报告 ID 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) —— 3 个鼠标按键 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x05, // REPORT_SIZE (5) 0x81, 0x03, // INPUT (Const,Var,Abs) —— 5 位保留 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 位移有符号 8 位 0x09, 0x38, // USAGE (Wheel) 0x15, 0x81, // LOGICAL_MINIMUM (-127) 0x25, 0x7F, // LOGICAL_MAXIMUM (127) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x06, // INPUT (Data,Var,Rel) —— 滚轮有符号 8 位 0xC0, // END_COLLECTION 0xC0 // END_COLLECTION此描述符定义了两个报告Report ID 18 字节键盘报告与 Report ID 24 字节鼠标报告。主机在解析 EP1 IN 端点数据时首先读取首字节判断 Report ID再按对应格式解析后续字节。例如发送0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00表示键盘左 Shift 键按下发送0x02, 0x01, 0x05, 0xFF, 0x00表示鼠标左键按下、X 轴向右移动 5、Y 轴向下移动 0、滚轮无变化。3. 核心 API 与数据结构KeyboardMouse 库对外暴露极简的 C 函数接口所有操作围绕KeyboardMouse_Report_t结构体展开该结构体是应用层与 USB 底层之间的唯一数据契约。3.1 主要数据结构// 键盘报告结构对应 Report ID 1 typedef struct { uint8_t report_id; // 固定为 0x01 uint8_t modifier; // 修饰键位图bit0Ctrl, bit1Shift, bit2Alt, bit3GUI, bit4Left Ctrl... uint8_t reserved; // 保留字节必须为 0 uint8_t keycodes[6]; // 6 个普通键扫描码0x00 表示无键 } KeyboardMouse_KeyboardReport_t; // 鼠标报告结构对应 Report ID 2 typedef struct { uint8_t report_id; // 固定为 0x02 uint8_t buttons; // 鼠标按键位图bit0Left, bit1Right, bit2Middle int8_t x; // X 轴位移-127 ~ 127 int8_t y; // Y 轴位移-127 ~ 127 int8_t wheel; // 滚轮位移-127 ~ 127 } KeyboardMouse_MouseReport_t; // 统一报告联合体便于统一处理 typedef union { uint8_t raw[8]; // 原始字节数组长度 8 KeyboardMouse_KeyboardReport_t keyboard; // 键盘报告视图 KeyboardMouse_MouseReport_t mouse; // 鼠标报告视图 } KeyboardMouse_Report_t;KeyboardMouse_Report_t的raw[8]成员确保结构体总长为 8 字节与 EP1 IN 端点 MaxPacketSize 完全匹配。应用层只需填充对应子结构体keyboard或mouse库内部自动处理 Report ID 填充与字节序对齐。3.2 关键 API 函数函数原型参数说明返回值功能描述void KeyboardMouse_Init(void)无无初始化 USB 外设使能时钟、配置 USB 专用引脚PA11/PA12、设置 USB 控制器寄存器CNTR、BTABLE、启用 USB 中断USB_LP_IRQn、启动 USB 设备SET_DEV_ADDR(0) CNTRvoid KeyboardMouse_SendReport(const KeyboardMouse_Report_t* report)report: 指向待发送报告的指针无将report-raw数组内容复制到 EP1 IN 端点缓冲区调用SetEPTxStatus(EP1, EP_TX_VALID)置位端点发送状态触发 USB 控制器发起 IN 事务函数为非阻塞立即返回uint8_t KeyboardMouse_IsReady(void)无0: 未就绪1: 就绪查询 EP1 IN 端点当前状态若GetEPTxStatus(EP1) EP_TX_NAK表示端点空闲可发送若为EP_TX_VALID或EP_TX_BUSY则返回 0。此函数用于应用层轮询发送时机void USB_LP_IRQHandler(void)无由 CMSIS 启动文件调用无USB 低优先级中断服务程序。核心逻辑1. 读取ISTR寄存器获取中断源ISTR_CTR,ISTR_PMAOVR,ISTR_ERR,ISTR_WKUP,ISTR_SUSP,ISTR_RESET2. 对ISTR_CTR控制传输完成调用HandleControlTransfer()处理标准请求GET_DESCRIPTOR, SET_ADDRESS, SET_CONFIGURATION 等3. 对ISTR_RESET重置 USB 状态机重新初始化端点SetEPTxStatus(EP0, EP_TX_STALL); SetEPRxStatus(EP0, EP_RX_STALL); SetEPTxStatus(EP1, EP_TX_NAK)4. 对ISTR_SOF帧起始可选用于时间基准3.3 底层寄存器操作封装为屏蔽硬件差异库提供一组宏封装 USB 寄存器访问// 端点状态查询/设置EPx: 0-3 #define GetEPTxStatus(ep) ((uint16_t)(BTABLE[ep*4 2] 0x000C)) #define SetEPTxStatus(ep, stat) do { BTABLE[ep*4 2] (BTABLE[ep*4 2] 0xFFF3) | (stat); } while(0) #define GetEPRxStatus(ep) ((uint16_t)(BTABLE[ep*4 0] 0x000C)) #define SetEPRxStatus(ep, stat) do { BTABLE[ep*4 0] (BTABLE[ep*4 0] 0xFFF3) | (stat); } while(0) // PMAPacket Memory Area缓冲区地址计算EPx TX/RX #define PMAAddr(ep, dir) ((uint16_t*)(PMA_BASE ((ep)*0x80 ((dir)TX ? 0x00 : 0x40)))) // 示例将 report-raw[8] 复制到 EP1 TX 缓冲区 void KeyboardMouse_SendReport(const KeyboardMouse_Report_t* report) { volatile uint16_t* pma_ptr PMAAddr(1, TX); for (int i 0; i 8; i 2) { // PMA 以 16 位字为单位寻址需字节交换 *(pma_ptr i/2) (uint16_t)(report-raw[i1] 8) | report-raw[i]; } // 设置端点包长为 8 字节 BTABLE[1*4 3] 0x08; // 置位发送状态 SetEPTxStatus(1, EP_TX_VALID); }PMAAddr宏计算出端点缓冲区在 Packet Memory AreaPMA中的物理地址。STM32F1 的 PMA 是一块 512 字节的 SRAM被划分为 32 个 16 字节块每个端点的 TX/RX 缓冲区需分配整数个块。EP1 TX 缓冲区通常分配在 PMA 偏移 0x0000 处长度 8 字节占用半个块。4. 典型应用示例4.1 裸机环境下的键盘事件上报以下代码演示如何在主循环中检测 GPIO 按键并上报键盘事件#include stm32f1xx.h #include keyboardmouse.h // 假设按键连接到 GPIOA Pin0-Pin7下拉输入 void GPIO_Init_Key(void) { RCC-APB2ENR | RCC_APB2ENR_IOPAEN; GPIOA-CRL ~(0xF (0*4)); // PA0 清除模式 GPIOA-CRL | (0x08 (0*4)); // PA0 输入浮空 // ... PA1-PA7 同理 } int main(void) { SystemInit(); GPIO_Init_Key(); KeyboardMouse_Init(); // 初始化 USB KeyboardMouse_Report_t report; uint8_t last_key_state[8] {0}; while(1) { // 扫描 8 个按键 uint8_t curr_key_state 0; for (int i 0; i 8; i) { if (GPIOA-IDR (1 i)) { curr_key_state | (1 i); } } // 检测按键按下上升沿 for (int i 0; i 8; i) { if ((curr_key_state (1i)) !(last_key_state[i])) { // 映射到标准键盘扫描码简化示例 uint8_t scancode 0x04 i; // a0x04, b0x05... report.keyboard.report_id 0x01; report.keyboard.modifier 0x00; report.keyboard.reserved 0x00; report.keyboard.keycodes[0] scancode; for (int j 1; j 6; j) report.keyboard.keycodes[j] 0x00; // 等待 EP1 就绪后发送 while (!KeyboardMouse_IsReady()); KeyboardMouse_SendReport(report); // 发送松开报告清空所有键 report.keyboard.keycodes[0] 0x00; while (!KeyboardMouse_IsReady()); KeyboardMouse_SendReport(report); } } last_key_state[0] curr_key_state; // 简化仅记录第一个字节 HAL_Delay(10); // 防抖 } }4.2 FreeRTOS 环境下的鼠标移动上报在 RTOS 中通常将 USB 发送操作封装为任务避免阻塞其他任务#include FreeRTOS.h #include task.h #include queue.h #include keyboardmouse.h // 创建鼠标报告队列深度 10每个元素 8 字节 QueueHandle_t xMouseQueue; void vMouseTask(void *pvParameters) { KeyboardMouse_Report_t report; int16_t x_delta 0, y_delta 0; while(1) { // 模拟鼠标移动算法例如读取 ADC 或编码器 x_delta Read_X_Axis(); // 返回 -10 ~ 10 y_delta Read_Y_Axis(); if (x_delta ! 0 || y_delta ! 0) { report.mouse.report_id 0x02; report.mouse.buttons 0x00; // 无按键 report.mouse.x (int8_t)x_delta; report.mouse.y (int8_t)y_delta; report.mouse.wheel 0x00; // 发送至队列由 USB 任务处理 if (xQueueSend(xMouseQueue, report, portMAX_DELAY) ! pdPASS) { // 队列满丢弃 } } vTaskDelay(pdMS_TO_TICKS(10)); } } // USB 发送任务高优先级 void vUSBSendTask(void *pvParameters) { KeyboardMouse_Report_t report; while(1) { // 从队列接收报告 if (xQueueReceive(xMouseQueue, report, portMAX_DELAY) pdPASS) { // 等待 USB 就绪 while (!KeyboardMouse_IsReady()); KeyboardMouse_SendReport(report); } } } int main(void) { // ... 系统初始化 xMouseQueue xQueueCreate(10, sizeof(KeyboardMouse_Report_t)); xTaskCreate(vMouseTask, Mouse, configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY 1, NULL); xTaskCreate(vUSBSendTask, USB_Send, configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY 3, NULL); vTaskStartScheduler(); }4.3 HAL 库适配要点若项目基于 STM32CubeMX 生成的 HAL 代码需进行以下适配时钟配置在SystemClock_Config()中确保RCC_OscInitStruct.PLL.PLLMUL RCC_PLL_MUL6;8MHz HSE × 6 48MHz并调用__HAL_RCC_USB_CLK_ENABLE()。引脚重映射PA11/PA12 默认为 USB 功能无需额外配置但需确认HAL_GPIO_DeInit(GPIOA, GPIO_PIN_11|GPIO_PIN_12)未被误调用。中断向量重定向在stm32f1xx_it.c中将USB_LP_IRQHandler替换为 KeyboardMouse 提供的版本并在HAL_MspInit()中调用HAL_NVIC_SetPriority(USB_LP_CAN1_RX0_IRQn, 5, 0); HAL_NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);注意STM32F1 的 USB LP 中断向量名为USB_LP_CAN1_RX0_IRQn。禁用 HAL USB 初始化注释掉MX_USB_DEVICE_Init()调用避免与 KeyboardMouse 的寄存器操作冲突。5. 调试与常见问题5.1 USB 枚举失败诊断当主机无法识别设备时按以下顺序排查硬件层使用示波器测量 PA11/PA12 是否有 48 MHz 时钟信号需探头带宽 ≥ 100 MHz检查 USB D 线是否通过 1.5kΩ 电阻上拉至 3.3VSTM32F1 内部无上拉必须外接确认 USB 线缆与主机端口接触良好尝试更换线缆。固件层在USB_LP_IRQHandler开头添加__NOP()并用调试器单步确认中断是否触发检查ISTR寄存器值若ISTR_RESET位持续为 1表明主机反复复位设备可能因CNTR寄存器配置错误如未清除CNTR_PDWN使用逻辑分析仪捕获 USB 通信验证设备是否响应GET_DESCRIPTOR请求特别是wLength0x0012的设备描述符请求。5.2 报告丢失与重复现象按键/鼠标移动偶尔丢失或重复触发。原因与对策发送时机不当在KeyboardMouse_SendReport()前未调用KeyboardMouse_IsReady()导致向非空闲端点写入数据。对策严格遵循“查询就绪 → 发送”流程。报告内容非法键盘报告中keycodes包含非法扫描码如 0x00-0x03或鼠标x/y/wheel超出 -127~127 范围。对策发送前校验数值范围。中断服务程序过长若USB_LP_IRQHandler中执行过多操作如串口打印可能导致 USB 事务超时。对策将耗时操作移至主循环或任务中ISR 内仅做状态标记。5.3 低功耗模式兼容性STM32F1 进入STOP模式时USB 时钟停止设备将断开连接。若需低功耗应使用STANDBY模式需外部唤醒源但 USB 无法工作或在SLEEP模式下保持 HSI/PLL 运行USB 时钟持续此时HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI)可用但功耗节省有限。6. 性能与资源占用在 STM32F103C8T6Flash 64KB, SRAM 20KB上KeyboardMouse 的典型资源占用为项目占用量说明Flash≈ 3.2 KB包含 USB 寄存器操作、描述符、中断服务程序、报告发送逻辑SRAM≈ 128 Bytes主要为 PMA 缓冲区EP0/EP1 共需 641680 字节及少量全局变量CPU 占用率 0.5% (1ms 轮询)在 72MHz 主频下KeyboardMouse_IsReady()耗时约 30 个周期KeyboardMouse_SendReport()约 200 周期该库未使用任何动态内存分配malloc/free所有数据结构均为静态分配满足 ASIL-B 等功能安全要求。其确定性执行时间最坏情况 500 ns使其适用于对实时性敏感的工业人机界面场景。7. 扩展与定制7.1 添加多媒体键支持修改报告描述符在键盘集合末尾追加多媒体用法页// 在键盘 COLLECTION 后、END_COLLECTION 前插入 0x05, 0x0C, // USAGE_PAGE (Consumer) 0x09, 0x23, // USAGE (Audio Volume Up) 0x09, 0x24, // USAGE (Audio Volume Down) 0x09, 0x29, // USAGE (Media Play Pause) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x03, // REPORT_COUNT (3) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x95, 0x05, // REPORT_COUNT (5) —— 填充至字节对齐 0x81, 0x03, // INPUT (Const,Var,Abs)同时扩展KeyboardMouse_KeyboardReport_t增加uint8_t media_keys成员并在发送时设置对应位。7.2 支持 USB 挂起/唤醒在USB_LP_IRQHandler中处理ISTR_SUSP和ISTR_WKUP中断if (istr ISTR_SUSP) { // 进入低功耗关闭 USB PHY保存状态 CNTR ~CNTR_PDWN; // 保持 USB 供电 // ... 执行其他低功耗操作 } if (istr ISTR_WKUP) { // 唤醒恢复 USB 状态 SetEPTxStatus(EP0, EP_TX_NAK); SetEPRxStatus(EP0, EP_RX_VALID); // ... 清除挂起标志 }主机可通过发送远程唤醒请求Remote Wakeup触发此流程需在GET_DESCRIPTOR返回的设备描述符中将bmAttributes的 bit5Remote Wakeup置 1。7.3 与传感器融合将加速度计如 MPU6050数据转换为鼠标移动// 在传感器数据就绪中断中 void MPU6050_DataReady_IRQHandler(void) { int16_t ax, ay; MPU6050_ReadAccel(ax, ay); // 获取原始加速度值 static int16_t x_accum 0, y_accum 0; x_accum ax / 100; // 积分近似位移 y_accum ay / 100; if (abs(x_accum) 1 || abs(y_accum) 1) { KeyboardMouse_MouseReport_t mouse; mouse.report_id 0x02; mouse.buttons 0x00; mouse.x (int8_t)CLAMP(x_accum, -127, 127); mouse.y (int8_t)CLAMP(y_accum, -127, 127); mouse.wheel 0x00; x_accum y_accum 0; KeyboardMouse_SendReport((KeyboardMouse_Report_t*)mouse); } }此方案将惯性导航思想引入 HID 设备为无接触交互提供硬件基础。