STM32F103+PAW3395制作有线鼠标:SPI读取与USB HID链路设计
发布时间:2026/9/10 14:04:01来源:尧图网络
简介基于STM32F103C8T6与PAW3395光电传感器的有线鼠标制作工程面向单片机进阶学习者、毕业设计或课程设计人群可作为毕设、课程设计或工程实训的完整参考。项目以类RTOS前后台架构实现时间片轮询通过HID协议与电脑通信支持光标移动、左右键、中键、滚轮及DPI切换功能DPI切换时LED闪烁3次并预留自定义按键接口尼龙外壳模型适配立创三维猴3D打印。压缩包共1107个文件大小50.04MB以C语言源码、IAR工程配置、PCB及原理图文件、STL外壳模型为主同时提供HID协议与调试相关文档硬件设计文件可辅助理解鼠标电路与传感器布局。目前已有332人学习浏览。资料内含完整软件工程、硬件设计文件与打印模型代码结构清晰便于二次开发注意资源定位为参考资料需要读者具备一定单片机基础自行调试并扩展功能。1. 用 STM32F103C8T6 把 PAW3395 接上 USB 口有线鼠标的关键在链路设计一支由 STM32F103C8T6 做大脑、原相 PAW3395 做“眼睛”、通过 HID 协议和电脑对话的有线鼠标听起来只是把传感器数据读出来再塞进 USB 端点的事真正动手后你会发现卡住你的往往不是 PAW3395 怎么初始化而是 HID 描述符里一个字节写错导致设备枚举失败、SPI 读取速度跟不上 1000Hz 回报率、最小系统板的 USB 时钟源没配到位这类链路问题。这篇文章把整条数据通路的每一段拆开讲从 PAW3395 的 SPI 寄存器读取、STM32F103C8T6 的 USB Device 配置、HID 鼠标报告描述符到 PCB 走线要求和用立创三维猴打印的尼龙外壳怎么和板子配合。适合做过单片机裸机开发、但还没有完整做过 USB 输入外设的工程师也适合想把手头传感器改成标准输入设备的人。2. PAW3395 的 SPI 读取寄存器、时序与运动数据解析2.1 引脚规划先给 C8T6 最小系统板分配好角色STM32F103C8T6 是一片 LQFP48 封装的 Cortex-M3资源并不富裕但做鼠标刚好够用。PAW3395 走 SPI 接口一般占用 4 根线SCLK、MOSISDIO 输入、MISOSDIO 输出、CS。为了把 USB 口留干净我一般把 PAW3395 挂在 SPI1 上PA5 做 SCLKPA6 做 MISOPA7 做 MOSIPA4 做片选。USB 的 DP/DM 固定在 PA11/PA12几乎没办法挪位置。剩下 PA0-PA3 可以做左右键和滚轮按键滚轮编码器如果走正交解码可以再接一个定时器通道或者用 EXTI 中断读电平变化。在动手写代码前先把最小系统板的原理图看清楚。网上常见的 STM32F103C8T6 最小系统板原理图包含几个必要部分8MHz 主晶振和两个 18-22pF 负载电容、3.3V 稳压电路、BOOT0 下拉电阻、NRST 复位电路、SWD 调试口。PAW3395 的工作电压典型值也是 3.3V可以直接和最小系统板共用电源但要保证供电电流足够。传感器内部的 LED 驱动电流会随 CPI 和帧率变化瞬时电流可能到几十毫安如果整板只用一颗 AMS1117 供电且输入电压偏低会出现 SPI 通信偶发失败的现象我遇到的多数“鼠标卡顿”其实都发生在电源纹波上而不是传感器本身。2.1.1 一个小坑USB 引脚和 SPI 引脚的复用检查PA11/PA12 既是 USB 的 DP/DM也是 SPI2 的 MISO/MOSI。把这四根引脚混用会导致两边同时失效。做鼠标时 SPI2 就不要动PAW3395 老老实实挂在 SPI1 上。另外 PA12 在 SWD 调试口上是 JTMS如果你用 SWD 下载程序PA12 会被调试器占用一小段时间鼠标正常工作不受影响但调试时如果遇到 USB 枚举不上先拔掉调试器再插线试一次。这个小现象容易误导排查方向。2.2 PAW3395 寄存器读写时序与 delta_X/delta_Y 读取PAW3395 的 SPI 时序有几个硬性要求片选低有效空闲时拉高时钟极性 CPOL1、相位 CPHA1也就是 SPI Mode 3数据高位在前。写寄存器时第一个字节的最高位为 1后跟地址再跟一个数据字节读寄存器时第一个字节最高位为 0地址跟上后主机继续提供时钟从机在下一个字节开始把数据放到 MISO 上。实际读取运动数据时如果要同时拿 X/Y 和运动标志需要连续读多个寄存器不能每次只读一个字节然后频繁翻转片选否则会拉低数据吞吐上限。芯片内部常用的几个寄存器如下表注明一点PAW3395 不同批次的数据手册寄存器地址可能有微小差异烧录前一定打开你手里那份 datasheet 核对一遍不要直接照抄社区里的旧值。寄存器地址名称作用0x00Product_ID芯片型号标识只读0x02Motion最高位是 Motion 标志置 1 表示有新的运动数据0x03Delta_X_LX 方向位移低字节有符号0x04Delta_X_HX 方向位移高字节0x05Delta_Y_LY 方向位移低字节有符号0x06Delta_Y_HY 方向位移高字节0x0FCPI_Config设置当前 CPI 值0x10Configuration传感器工作模式配置下面是一段从 PAW3395 读取完整运动状态的 C 代码用 STM32 HAL 库编写核心是连续读 5 个字节#define PAW_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define PAW_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) uint8_t paw_read_reg(uint8_t reg) { uint8_t tx[2], rx[2]; tx[0] reg 0x7F; // 读命令最高位为 0 tx[1] 0xFF; // 占位字节触发时钟输出数据 PAW_CS_LOW(); HAL_SPI_TransmitReceive(hspi1, tx, rx, 2, 10); PAW_CS_HIGH(); return rx[1]; // 第二个字节才是寄存器数据 } void paw_read_motion(int16_t *dx, int16_t *dy) { uint8_t tx[5] {0x02, 0xFF, 0xFF, 0xFF, 0xFF}; uint8_t rx[5]; PAW_CS_LOW(); HAL_SPI_TransmitReceive(hspi1, tx, rx, 5, 10); PAW_CS_HIGH(); *dx (int16_t)(rx[2] | (rx[3] 8)); *dy (int16_t)(rx[4] | (rx[5] 8)); // 实际 5 字节接收时 rx 只有 0-4 }这段代码里有几个参数值得展开。第一个字节传 0x02 表示从 Motion 寄存器开始读后续 4 个字节是主机为获取数据而提供的时钟对应的返回值依次是 Motion、Delta_X_L、Delta_X_H、Delta_Y_L、Delta_Y_H。我在示例里把 tx[0] 写成 0x02 而不是paw_read_reg(0x02)的展开形式是为了保证片选在整个读取期间持续拉低避免多次拉高拉低产生间隔。HAL_SPI_TransmitReceive的超时时间用了 10ms正常一次传输在 SPI 2MHz 下只要 20µs 左右10ms 只是兜底保护。需要修正的是上面代码里 rx[5] 越界了实际 5 字节 buffer 的索引只能到 4正确取值应该是*dx (int16_t)(rx[2] | (rx[3] 8)); *dy (int16_t)(rx[4] | (rx[1] 8)); // 这是错的正确的做法是把接收缓冲设成 6 字节让读取命令字节占 rx[0]Motion 占 rx[1]四个数据字节占 rx[2] 到 rx[5]而发送缓冲第一字节仍是 0x02其余为 0xFF。这类问题在工程上很常见代码写成 5 字节看似能跑但 Y 轴数据永远是错的。完整修正如下void paw_read_motion(int16_t *dx, int16_t *dy) { uint8_t tx[6] {0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; uint8_t rx[6] {0}; PAW_CS_LOW(); HAL_SPI_TransmitReceive(hspi1, tx, rx, 6, 10); PAW_CS_HIGH(); *dx (int16_t)(rx[3] | (rx[4] 8)); *dy (int16_t)(rx[5] | (rx[6] 8)); // 同样越界 }到这里更稳妥的写法是直接把 Motion 寄存器和四个位移寄存器分开读。虽然牺牲了一点效率但逻辑清晰不容易被下标绕进去void paw_read_motion(int16_t *dx, int16_t *dy) { uint8_t motion paw_read_reg(0x02); if (motion 0x80) { *dx (int16_t)(paw_read_reg(0x03) | (paw_read_reg(0x04) 8)); *dy (int16_t)(paw_read_reg(0x05) | (paw_read_reg(0x06) 8)); } else { *dx 0; *dy 0; } }读 Motion 寄存器时要注意0x02 的最高位为 1 才代表有数据。每个 SPI 读操作之间需要留一点间隔片选拉高后至少保持几百纳秒再拉低否则传感器内部状态机可能来不及复位。初次调通时把 SPI 波特率设在 1-2MHz确认数据正确后再往上提。PAW3395 的 SPI 最高可以跑到 10MHz 以上但走线如果太长高速时容易误码。用鼠标这种小尺寸 PCB信号线长度在 20mm 以内跑 4-8MHz 完全可行。2.3 CPI 与方向修正鼠标拿到手不“漂”的参数设定PAW3395 的 CPI 通过 0x0F 寄存器设置可设范围从几百到一万多具体步进值一定要以手头数据手册为准。CPI 直接影响鼠标在屏幕上移动的距离设置过高会出现灵敏度太冲设置过低则手要移动很大幅度。常见做法是提供两档 CPI用滚轮中键下方的按键切换比如 800 CPI 和 1600 CPI并将当前档位用一颗 LED 的亮灭或颜色表示。CPI 写寄存器时需要先把 Configuration 寄存器里传感器切到配置模式。不同的原相芯片进入配置模式的方式不完全一致有的直接写寄存器即可有的要先向 0x10 写入特定值。通用的安全流程是写 0x10 0x01 进入配置模式然后写 0x0F 设置 CPI最后写 0x10 0x00 退出配置模式。如果写完 CPI 后鼠标移动速度没有变化优先查是否漏了退出配置模式这一步。方向修正也是新板子必做的测试。把鼠标向前推如果屏幕上的光标反而向下说明 Y 轴数据取反了。PAW3395 的默认坐标轴向是固定的但安装方向和 PCB 走线可能不同处理方式有两种一种是在读取后对 dx/dy 分别做正负号翻转另一种是改传感器的方向配置寄存器。我一般在上层做翻转因为改动只涉及一行代码不需要重新读手册确认寄存器行为// 传感器安装方向修正示例 int16_t out_x (config.flip_x) ? -dx : dx; int16_t out_y (config.flip_y) ? -dy : dy;把翻转参数放到配置结构体里等外壳装好后再根据实际手感修改避免每次编译都改源码。这里也提醒一下PAW3395 输出的 delta 是相对位移不是绝对坐标所以不需要做原点校准只需要确保单位换算正确。USB HID 协议里的 X/Y 用的是带符号相对值和传感器输出的增量天然对应直接搬运过去就能用省去了很多坐标变换。3. STM32F103C8T6 的 USB HID 设备描述符与 1000Hz 回报率3.1 USB 引脚与 48MHz 时钟先解决枚举的前提条件STM32F103C8T6 内部有 USB 2.0 Full Speed 外设但没有晶振电路需要外部提供 48MHz 的 USB 时钟。最简单的做法是用 8MHz 主晶振PLL 倍频到 72MHz再经过 USB 预分频器除以 1.5 得到 48MHz。CubeMX 时钟树配置里只要把 USB Clock Source 选为 PLLCLK 并确认最终值是 48.000MHz代码生成时代理会自动设置。如果时钟源配成别的值最常见的现象是电脑能识别设备但枚举不稳定有时插上认不到有时用一会儿掉线。USB 的 D/D- 在 PA11/PA12 上。STM32F103 内部已经集成了 D 上拉电阻但默认不启用必须由 USB 设备库在软件里通过 控制寄存器 打开。使用 HAL 库的 USB Device 中间件时MX_USB_DEVICE_Init()会处理这一步。如果用的是标准外设库需要手动调用USB_Cable_Config(ENABLE)把内部上拉接入 D主机才能检测到设备插入。忘了开上拉是枚举失败的最高频原因而且现象很迷惑用万用表量 D 电平正常但电脑没有任何反应。PCB 上 PA11/PA12 到 USB 座之间建议串两个 22Ω 电阻靠近 MCU 放置用来抑制信号振铃。USB 座子的外壳地要单独走线通过一个 1MΩ 电阻并联 4.7nF 电容接到系统地避免静电测试时把复位电路打挂。这些在画第一版 PCB 时就加进去后面省很多事。3.2 HID 鼠标报告描述符按键、滚轮、XY 轴的字节布局HID 协议里鼠标不是“发一串数据”就能被识别的。设备要先上报告描述符告诉主机这个设备有按键、有 X/Y 轴、有滚轮然后按描述符规定的字节顺序发送报告。下面的数组是一个带 Report ID 的鼠标描述符ID 设为 2const uint8_t HID_ReportDesc[] { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x02, // Usage (Mouse) 0xA1, 0x01, // Collection (Application) 0x85, 0x02, // Report ID (2) 0x09, 0x01, // Usage (Pointer) 0xA1, 0x00, // Collection (Physical) 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (Button 1) 0x29, 0x05, // Usage Maximum (Button 5) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x95, 0x08, // Report Count (8) 0x75, 0x01, // Report Size (1) 0x81, 0x02, // Input (Data, Variable, Absolute) 0x95, 0x01, // Report Count (1) 0x75, 0x08, // Report Size (8) 0x81, 0x01, // Input (Constant), padding byte 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x30, // Usage (X) 0x09, 0x31, // Usage (Y) 0x16, 0x01, 0x80, // Logical Minimum (-32767) 0x26, 0xFF, 0x7F, // Logical Maximum (32767) 0x75, 0x10, // Report Size (16) 0x95, 0x02, // Report Count (2) 0x81, 0x06, // Input (Data, Variable, Relative) 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, Variable, Relative) 0xC0, // End Collection 0xC0 // End Collection };逐段解释这条描述符。按键部分用 8 个 bit 表示 5 个物理按键多出的 3 个 bit 留作高位填充实际只用到 Bit0-Bit4。X/Y 用 16 位有符号数表示相对位移范围设为 -32767 到 32767这比游戏鼠标通常只发 ±127 的范围大得多高 DPI 下快速甩动不会丢量。滚轮用 8 位有符号数向上滚为正值向下为负值。0x16, 0x01, 0x80 这三个字节是 Logical Minimum 的 16 位补码0x8001 对应 -32767不能写成 0x80, 0x00否则和描述符解析器的规则冲突。这是很多人照抄网上代码后滚轮方向反或者 XY 轴无响应的根源之一。对应的上报结构体在固件里这样组织typedef struct { uint8_t report_id; // 固定为 2 uint8_t buttons; // Bit0左键, Bit1右键, Bit2中键 int16_t x; // 相对位移 int16_t y; int8_t wheel; // 滚轮 } __packed mouse_report_t;整个结构体占 7 字节USB 端点最大包长配成 8 字节对齐后方便底层缓存处理。每次发送时 report_id 放第一个字节和描述符里的 0x85 对应。如果收到的鼠标数据不对先在主机端把描述符 dump 出来和 C 数组逐字节比对使用 Python 的 hidapi 可以做这件事后面第 5 章再展开。3.3 用 SOF 中断与 1ms 端点轮询把回报率稳定在 1000HzUSB Full Speed 的帧周期是 1msHID 鼠标的中断端点如果 interval 设为 1主机每帧都会轮询一次设备理论上限就是 1000Hz。要让 1000Hz 真正稳定关键不是端点配置而是数据什么时候准备好。如果把 SPI 读取放在 USB 设备中断回调里SPI 传输的几十微秒会阻塞 USB 中断处理导致主机轮询落空实际回报率掉到一两百赫兹。正确的做法是让 SPI 读取跑在定时器中断里用 500µs 一次的节奏把 PAW3395 的增量数据累加到全局变量USB 的 SOF 回调只负责“有数据就发送”。SOF 是每帧开始的中断用它做发送触发可以保证一个帧周期内只发一次volatile int16_t sensor_dx 0, sensor_dy 0; volatile uint8_t report_ready 0; void TIM2_IRQHandler(void) { int16_t dx 0, dy 0; paw_read_motion(dx, dy); sensor_dx dx; sensor_dy dy; report_ready 1; __HAL_TIM_CLEAR_IT(htim2, TIM_IT_UPDATE); } void HAL_PCD_SOFCallback(PCD_HandleTypeDef *hpcd) { uint8_t buf[7]; if (report_ready) { buf[0] 2; // Report ID buf[1] current_buttons; buf[2] sensor_dx 0xFF; buf[3] (sensor_dx 8) 0xFF; buf[4] sensor_dy 0xFF; buf[5] (sensor_dy 8) 0xFF; buf[6] current_wheel; USBD_HID_SendReport(hUsbDeviceFS, buf, 7); sensor_dx 0; sensor_dy 0; report_ready 0; } }提示SOF 中断里直接清空位移变量会有 500µs 窗口内的数据丢失风险。更稳的做法是用环形缓冲或只清报告已发送的那部分数据。对第一版鼠标来说这种丢失发生在纳秒级时间内手感上几乎察觉不到但要知道这个边界存在。定时器 2 的配置很简单72MHz 时钟预分频 71重载值 499得到 500µs 中断。每次中断读一次 PAW3395拿到两个方向的增量累加SOF 发送后清零。如果发现回报率上不去先用逻辑分析仪抓 USB D 看 interrupt IN 包的间隔是否稳定在 1ms而不是看传感器数据是否正常这一步能快速区分是主机调度问题还是固件发送节奏问题。4. 固件集成与 PCB 文件落地从原型到可打样的板子4.1 主循环与定时器解耦避免 USB 中断里做 SPI有了第 3 章的 SOF 发送框架主循环的职责就很清楚了扫描按键、处理滚轮编码器、管理 CPI 切换和 LED 状态。按键扫描不用进定时器中断10ms 周期在主循环里做防抖就足够。左键从按下到识别人体最快大约 50ms10ms 的扫描周期不会产生可感知的延迟。while (1) { // 按键防抖10ms 执行一次 static uint32_t last_scan 0; if (HAL_GetTick() - last_scan 10) { last_scan HAL_GetTick(); scan_buttons(); // 读 GPIO处理边沿 } // 如果 PAW3395 上报的数据带按键状态等待 SOF 回调发送 // 这里只更新全局变量不直接操作 USB if (dpi_switch_pressed) { cycle_cpi(); // 切换 CPI 并写入传感器寄存器 dpi_switch_pressed 0; } // 进入低功耗模式前要关掉 USB这里不做低功耗设计 }按键扫描和 USB 发送分离后最大的收益是不用在 GPIO 中断里调用USBD_HID_SendReport。GPIO 中断优先级高如果在里面做发送恰好又是 SOF 期间会造成双写。USB 中间件内部对同一端点的并发访问是互斥的但做法是死等阻塞时间一长主循环的按键扫描就跟着卡顿手感变粘。把设备状态变化统一用标志位表达主循环和中断只负责改标志位USB 发送统一在 SOF 回调里做整个固件的数据流是单向的排错也容易。4.2 PCB 走线要求PA11/PA12 差分对、PAW3395 的去耦与布局标题既然说“含 PCB 文件”拿到工程图后第一件事不是看丝印而是检查 PCB 走线是否满足鼠标这类小型 USB 设备的基本要求。STM32F103C8T6 是 48 脚 LQFP2 层板可以做出来但 PAW3395 底部需要开透光孔透镜区域周围不能有电路4 层板会更从容。鼠标对体积要求高我通常用 4 层 1.0mm 板厚叠层从顶层开始是信号、地、电源、信号这样 PAW3395 底下可以直接铺完整地平面SPI 信号的回流路径最短。走线对象关键参数设计说明USB DP/DM线宽 0.2mm8mil间距 0.2mm等长控制在 2mm 内Full Speed 对阻抗不敏感但要保证等长避免时序偏差USB DP/DM 串阻22Ω靠近 MCU抑制信号振铃增强 ESD 抗性SPI 信号线线宽 0.25mm10mil总长 20mmPCBA 空间小尽量短远离晶振和按键走线8MHz 晶振负载电容 18-22pF走线越短越好晶振下方不要走其他信号线铺地平面PAW3395 VDD4.7µF 100nF紧贴电源引脚传感器 LED 电流瞬变去耦电容离得越近越有效MCU VDD100nF 10µF 组合每个电源引脚一颗数字部分和传感器部分地尽量保持单点连接PAW3395 底部的透光孔尺寸要严格按数据手册的“Lens Opening”章节来单位是毫米开孔位置和传感器焊盘的相对坐标几乎不能有偏差。如果孔径开小了透镜被 PCB 挡住传感器会认为底面距离异常输出数据变成固定值开大了则容易进灰灰尘落到透镜表面会表现为鼠标乱飘。打完板后不要先焊传感器用卡尺量一下开孔位置和丝印边框是否对齐再焊接。USB 座子建议用直插型的 Mini USB 或 Type-C 母座焊盘固定在板边D/D- 从座子出来后直接朝 MCU 方向走不要绕到板子另一面再折回来。鼠标内部有滚轮编码器和微动开关这些元件的飞线如果贴着 USB 差分线走会把编码器转动时产生的毛刺耦合到 USB 信号上轻则丢报告重则枚举失败。滚轮编码器的 A/B 相信号线在 PCB 上用地线包住宽度不用刻意加大能保持一路地伴行就够。4.2.1 微动开关和编码器的布局顺序三个微动开关的位置由外壳按键柱决定PCB 布局要先去配合外壳模型而不是先把 MCU 放中间再塞按键。常见顺序是USB 座在正前方微动开关平行排列在板子前缘滚轮编码器在两个微动之间略靠后PAW3395 放在板子中后部MCU 放在靠近 USB 座的一角。PAW3395 的 SPI 走线回绕到 MCU 时会经过编码器区域这段线用 0.25mm 线宽两侧加地。板上 LED 指示灯的走线不要和 SPI 的 SCLK 平行跑太长LED 的开关噪声会耦合进时钟线导致随机多读或少读一个字节。4.3 从立创 EDA 导出 Gerber 与打样检查在立创 EDA 专业版里画完 PCB 后执行 DRC 检查至少要确保 0 个电气错误。鼠标板面积小容易出现的 DRC 问题是丝印压焊盘、走线间距小于工厂最小线间距、过孔打在焊盘中心。把这些清干净后导出 Gerber 文件包主要包含 top 铜箔、bottom 铜箔、top 丝印、bottom 丝印、top 阻焊、bottom 阻焊、钻孔文件。导出后用一个免费 Gerber 查看器逐层看一遍重点确认 PAW3395 开孔没有被阻焊层覆盖USB 座子的五个焊盘都出现在丝印层上。有人会问“Gerber 文件转成 pcb 文件”才能打样吗不需要。打样工厂只认 Gerber 和钻孔文件PCB 源文件反而容易因版本不兼容导致信息丢失。如果你买到的标题里说的是“含 pcb 文件”实际给的是立创 EDA 工程文件epro 格式在自己电脑上修改后再导出 Gerber 即可。打样时把板厚、铜厚、表面工艺推荐沉金、阻焊颜色填清楚鼠标这种精密小板优先选沉金表面喷锡板在透光孔边缘容易挂锡珠影响透镜安装平面。5. 立创三维猴尼龙外壳与鼠标成品验证技巧5.1 尼龙外壳设计按键行程、卡扣间隙与螺丝孔立创三维猴的尼龙打印件强度和耐磨性都不错适合做鼠标外壳但尼龙后收缩率比 ABS 和 PLA 都要明显设计时预留的间隙要放大。按键柱和微动开关按钮之间的接触面我一般留 0.2-0.3mm 的活动余量。尼龙表面摩擦系数稍大按键柱直接顶在微动开关上的话长时间使用后接触面会磨出凹坑手感变得松垮。解决方案是在打印件侧面加一个 1.5mm 宽的导槽里面卡一个自润滑的 POM 小片不需要额外粘接靠打印件本身的弹性固定。这类细节对长时间使用体验影响很大但很多鼠标 DIY 项目的分享里不会写。外壳的下盖要用 M2 自攻螺丝固定 PCB螺丝柱高度比 PCB 高出 0.3mm柱子底部加一条 0.8mm 高的加强筋连接侧壁防止多次拆装后螺丝柱根部开裂。螺丝柱内径打 1.6mm 孔M2 自攻螺丝拧进去后 0.4mm 的壁厚足够锁紧孔径再小就容易在下螺丝时把柱体撑爆。上盖和底盖之间用卡扣连接卡扣宽度至少 3mm厚度 1mm装配时轻微过盈 0.1mm靠尼龙的弹性咬住。如果外壳打印出来上下盖松脱用热风枪吹一下卡扣部位让它往内弯一点即可不要用胶水胶水会降低后续拆卸维护的可能性。5.2 用 hidapi 枚举报告描述符验证 HID 协议是否正确固件烧进去之后先不急着装壳裸板上电插到电脑打开设备管理器看能不能识别到 HID 兼容鼠标。如果识别到但光标不动用下面这段 Python 代码读取设备描述符验证报告描述符和固件里的数组是否一致import hid devices hid.enumerate() for d in devices: if d[usage_page] 1 and d[usage] 2: # Generic Desktop / Mouse print(d[path], d[interface_number]) dev hid.device() try: dev.open_path(d[path]) desc dev.get_report_descriptor() print(desc.hex()) dev.close() except Exception as e: print(open failed:, e)这段代码遍历所有 HID 设备过滤出鼠标打印其原始报告描述符。执行前用pip install hidapi安装库。用 STM32 默认的 VID/PID 时枚举结果里可能有很多个设备过滤条件里加上d[product_string]或d[path]能更快定位。如果open_path失败多数原因是鼠标在 Windows 下被系统以独占方式打开这时需要在设备管理器的“鼠标和其他指针设备”里找到该设备右键属性在“高级”里关闭“允许应用程序独占此设备”重新插拔后再试。验证 1000Hz 回报率不需要额外的硬件。用 UsbPcap 配合 Wireshark过滤条件填usb.transfer_type 0x03 usb.endpoint_address 0x81把鼠标快速晃动几秒观察 interrupt IN 包的时间戳差。包与包之间稳定在 1ms 附近说明 SOF 触发逻辑生效如果出现 3-4ms 的间隙优先检查 TIM2 的 500µs 中断是否被 SPI 阻塞在paw_read_motion入口加一个 GPIO 翻转用示波器量中断响应时间即可定位。最后收一个校准 CPI 的小技巧PAW3395 输出的位移是像素计数不是物理距离。校正时拿一把直尺鼠标在鼠标垫上滑动 20cm读取屏幕光标移动的像素数用像素数除以 20 再除以 2.54得到每英寸实际 CPI和寄存器设置值对比如果偏差超过 3%说明鼠标垫表面纹理和传感器默认表面参数不匹配这时不要急着改寄存器拿同型号鼠标垫再测一轮。尼龙脚贴的摩擦系数偏大滑动 20cm 过程中可能因为压力不均导致位移读数偏短至少要取三段独立滑动的平均值每段之间把鼠标抬起再放下避免轨迹记忆造成的累积误差。本文还有配套的精品资源点击获取
网站建设高端定制企业官网