恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
USB鼠标接入LVGL嵌入式界面:CH32V303 HID解析与实现
首页
资讯中心
/
USB鼠标接入LVGL嵌入式界面:CH32V303 HID解析与实现
USB鼠标接入LVGL嵌入式界面:CH32V303 HID解析与实现
发布时间:2026/8/31 6:43:19
之前做嵌入式 HMI 项目时大部分时间都在和触摸屏打交道。直到一个展示类项目需要现场用普通 USB 鼠标直接操控 LVGL 界面才开始系统研究如何把鼠标接入 LVGL。这篇文章把整套思路和关键代码整理出来包含 USB HID 鼠标协议解析、LVGL 指针输入设备注册以及几个容易踩的坑适合想在 CH32V303 上做 USB 外设接入的开发者。1. 项目背景与方案选型1.1 为什么用鼠标控制 LVGLLVGL 是嵌入式领域常用的开源图形库绝大多数项目都采用触摸屏作为人机交互入口。触摸屏的优势是直观、节省空间但在某些场景下鼠标反而是更合适的选择工业 HMI 设备中操作人员更习惯用鼠标点选参数精度比手指触摸高开发调试阶段频繁烧录固件、改界面布局鼠标操作比触摸更方便一些桌面化设备例如嵌入式终端、自助查询机、桌面小工具用户预期就是鼠标操作LVGL 本身提供了完善的指针输入设备接口鼠标和触摸本质上都可以映射为指针事件接入成本并不高。在 CH32V303 这类内置 USB 控制器的 MCU 上只需要外接一个普通 USB 鼠标就能实现“鼠标控制 LVGL 界面”的效果。整套方案不依赖复杂触摸芯片也不需要额外的屏幕触摸校准流程硬件成本更低调试也更直观。1.2 CH32V303 在方案中的角色CH32V303 是沁恒推出的一款基于 RISC-V 内核的 MCU主频最高可达 144MHz内置 FPU 和较大的 SRAM并带有完整的 USB 主机HOST控制器。正是这颗 USB 主机控制器让“MCU 直接读取 USB 鼠标数据”成为可能。在这套方案中CH32V303 承担三个任务初始化 USB 主机控制器枚举 USB 鼠标设备周期性读取鼠标的 HID 报告解析位移和按键数据将解析结果写入 LVGL 输入设备驱动由 LVGL 完成界面刷新和事件响应。相比使用 STM32 的方案CH32V303 的优势在于芯片本身具备 USB 主机能力硬件设计上不需要额外的 USB Host 桥接芯片整体 BOM 成本更低。当然本文的方案思路同样适用于其他带 USB 主机的 MCU只要调整底层 USB 库函数即可。1.3 整体方案与技术栈从系统架构上看鼠标控制 LVGL 的链路非常清晰USB 鼠标 - CH32V303 USB 主机控制器 - HID 报告解析 - LVGL 指针输入设备 - 界面响应技术栈主要包括芯片平台CH32V303图形库LVGL 8.3 或更高版本屏幕SPI 接口屏或 RGB 接口屏分辨率常见为 320x240、480x320 等USB 协议USB HID 鼠标类协议开发环境MounRiver Studio 或 VS Code 相关插件。接下来我们从环境准备开始逐步搭建这套工程。2. 环境准备与版本说明2.1 开发工具链CH32V303 的官方开发环境是 MounRiver Studio这是沁恒基于 Eclipse 二次开发的 IDE下载安装后自带 RISC-V GCC 工具链、调试配置和芯片支持包对新手比较友好。如果习惯 VS Code也可以把工程配置成 CMake 工程用 VS Code 编辑代码再通过命令行工具链编译配合 WCH-Link 调试。两种方式各有优劣MounRiver Studio集成度最高官方例程直接导入就能编译适合快速验证VS Code CMake编辑体验更好代码检索、Git 集成更方便适合长期维护的工程。版本方面CH32V303 的 SDK 会随官网更新建议使用较新的 EVT 包。需要说明的是不同版本的 SDK 中 USB 主机库函数名称可能有差异。本文示例代码重点展示实现思路实际编译时以你本地 SDK 中的头文件和 API 为准。2.2 硬件清单与接线在开始写代码之前建议先准备好以下硬件CH32V303 开发板一块WCH-Link 调试器一个普通 USB 鼠标一个最好选经典的三键鼠标方便验证左右键和中键SPI 屏幕模块一个例如 ST7789、ILI9341 驱动芯片的屏幕杜邦线或排线若干。接线时主要关注两部分屏幕与 MCU 之间SCLK、MOSI、CS、DC、RST、BLK、GND、VCCUSB 鼠标与 MCU 之间USB D、D-、VBUS、GND。CH32V303 开发板通常已经引出了 USB 接口如果使用开发板自带的 USB Host 口可以直接插鼠标不需要额外接线。如果是自己画板需要注意 D 和 D- 的差分走线尽量短并在 VBUS 与 GND 之间放置 10uF 和 0.1uF 去耦电容避免鼠标工作时导致电压跌落。2.3 LVGL 版本与配置项LVGL 版本建议使用 8.3 系列。8.3 是 LVGL 8 生命周期中最稳定的版本资料多网上的移植教程也最丰富。如果使用 LVGL 9API 变化较大输入设备注册方式、颜色格式、缓冲配置都需要相应调整。在 lv_conf.h 中需要重点确认几个配置项#define LV_COLOR_DEPTH 16 #define LV_MEM_SIZE (32U * 1024U) #define LV_INDEV_DEF_DRV_LIMIT 1LV_COLOR_DEPTH 根据屏幕驱动芯片决定如果屏幕是 RGB565 屏幕就配置为 16LV_MEM_SIZE 要根据界面复杂度调整界面控件多时建议从 32KB 起步跑复杂动画再往上加LV_INDEV_DEF_DRV_LIMIT 是输入设备数量上限本文只注册一个鼠标指针设备保持默认即可。3. LVGL 输入设备机制拆解3.1 indev 输入设备的三种类型LVGL 将输入设备统称为 indev根据交互方式分为三类指针设备POINTER触摸屏、鼠标提供 XY 坐标和按下状态按键设备KEYPAD矩阵键盘、遥控器通过按键码驱动焦点切换编码器设备ENCODER旋转编码器用于菜单选择和调节数值。鼠标属于指针设备。LVGL 在内部会为指针设备维护一个“当前坐标”和一个“按下状态”并通过这些信息完成控件选中、按下、释放、拖拽等操作。理解这一点很重要无论底层是触摸屏还是鼠标对 LVGL 来说都是“某个坐标点按下去了”或“某个坐标点松开了”。所以我们要做的核心工作就是把 USB 鼠标报告转换成 LVGL 认识的坐标和按键状态。3.2 鼠标与触摸屏的本质区别触摸屏上报给 MCU 的通常是绝对坐标触摸点 X、Y 直接对应屏幕上的位置。鼠标则不同它上报的是相对位移X 方向移动了多少、Y 方向移动了多少而不是最终坐标。举个例子触摸屏手指按在屏幕左上角底层上报 X0Y0鼠标光标当前在屏幕中间鼠标向右移动 50 个单位底层上报 dx50dy0LVGL 需要在原有坐标基础上累加。因此移植鼠标输入设备时必须在驱动层维护一个“光标坐标”变量。每次拿到 HID 报告的位移值后与当前光标坐标相加然后做边界裁剪再把最终坐标传给 LVGL。很多开发者第一次接入鼠标时发现光标不动原因往往就是只把位移值直接传给了 LVGL而没有做坐标累加。3.3 光标显示与坐标映射LVGL 指针设备默认不显示光标。如果想让用户看到鼠标指针需要创建一个图片控件作为光标然后通过 lv_indev_set_cursor 将这个图片绑定到鼠标输入设备上。lv_obj_t *cursor_obj lv_img_create(lv_scr_act()); lv_img_set_src(cursor_obj, mouse_cursor_img); lv_indev_set_cursor(mouse_indev, cursor_obj);光标图片可以自己做一张小箭头图片也可以用 LVGL 自带的字体符号或者使用 LV_SYMBOL 作为图片源。实际项目中一套完整 UI 通常会配套鼠标光标素材。坐标映射同样需要注意。鼠标的位移是相对量但最终坐标是绝对量所以位移累加后必须做边界限制。比较稳妥的做法是在解析 HID 报告的代码里就把坐标限制在屏幕分辨率范围内if (mouse_x 0) mouse_x 0; if (mouse_y 0) mouse_y 0; if (mouse_x SCREEN_WIDTH) mouse_x SCREEN_WIDTH - 1; if (mouse_y SCREEN_HEIGHT) mouse_y SCREEN_HEIGHT - 1;如果屏幕做了旋转还需要根据旋转方向对 XY 做相应变换。例如竖屏改横屏时原始的 x 可能映射到新的 y 轴这部分要在驱动层处理干净否则会出现光标划出屏幕边界的问题。4. USB HID 鼠标协议解析4.1 HID 报告格式USB 鼠标属于 HIDHuman Interface Device类设备。HID 设备通过“报告”向主机发送数据标准三键鼠标的报告格式通常是 4 个字节字节序号含义Byte 0按键状态Bit0 左键、Bit1 右键、Bit2 中键Byte 1X 轴位移带符号 int8Byte 2Y 轴位移带符号 int8Byte 3滚轮位移带符号 int8一般鼠标可没有实际解析时由于不同鼠标的 HID 描述符略有差异报告长度可能是 3 字节、4 字节甚至更多。稳妥做法是先读取设备的 HID 报告描述符确认各个字段的位置和长度再按偏移解析。不过绝大多数普通鼠标都符合上述标准格式先按 4 字节解析即可。4.2 鼠标数据解析过程CH32V303 作为 USB 主机需要完成以下步骤才能拿到鼠标数据检测到设备插入USB 主机控制器产生连接事件主机对设备复位设置设备地址读取设备描述符、配置描述符确认设备类型找到 HID 接口和中断 IN 端点配置设备并向鼠标发送 SET_PROTOCOL、SET_IDLE 等 HID 类请求周期性向中断 IN 端点发起 IN 传输读取 HID 报告。下面是一个简化的解析函数演示如何从 HID 报告中提取坐标和按键/* 文件路径usb_hid_mouse.c */ #include usb_hid_mouse.h mouse_data_t mouse {0}; static uint8_t mouse_report[8]; /* 解析标准 3 键鼠标的 HID 报告 */ void parse_hid_report(const uint8_t *report, uint16_t len) { if (len 3) { return; } /* 第 0 字节为按键状态 */ mouse.left_button (report[0] 0x01) ? 1 : 0; mouse.right_button (report[0] 0x02) ? 1 : 0; mouse.middle_button (report[0] 0x04) ? 1 : 0; /* 第 1、2 字节为相对位移强转为 int8_t 得到有符号数 */ int8_t dx (int8_t)report[1]; int8_t dy (int8_t)report[2]; /* 位移累加到当前坐标 */ mouse.x dx; mouse.y dy; /* 边界裁剪 */ if (mouse.x 0) { mouse.x 0; } if (mouse.y 0) { mouse.y 0; } if (mouse.x SCREEN_WIDTH) { mouse.x SCREEN_WIDTH - 1; } if (mouse.y SCREEN_HEIGHT) { mouse.y SCREEN_HEIGHT - 1; } }代码中把报告的第 1 字节和第 2 字节强制转换为 int8_t这一步非常关键。如果不强转位移 0xFF 会被当成 255鼠标就会在某个方向疯狂飘移看起来像是光标失控。4.3 在 USB 主机模式下读取鼠标不同厂家的 USB 主机库 API 不同但整体流程类似。下面给出一个伪代码风格的主机初始化与轮询流程/* 伪代码具体函数名以 CH32V303 SDK 为准 */ void usb_hid_mouse_init(void) { USBH_Init(HOST_MODE); USBH_DeviceConnect(); USBH_EnumDevice(); USBH_SetInterface(HID_CLASS); USBH_OpenPipe(MOUSE_IN_EP); } void usb_hid_mouse_poll(void) { if (USBH_DeviceIsConnected() 0) { return; } uint16_t len sizeof(mouse_report); if (USBH_GetHidReport(mouse_report, len) USBH_OK) { parse_hid_report(mouse_report, len); } }在实际工程中轮询函数应该放在主循环或 RTOS 任务中周期性调用。不要在主循环里用 while 阻塞等待 USB 数据否则 LVGL 的界面刷新会被卡住鼠标移动时画面会非常卡顿。5. 完整实战鼠标接入 LVGL5.1 初始化 USB 主机与鼠标在 main 函数中先初始化系统时钟、调试串口、屏幕和 USB 主机再初始化 LVGL。/* 文件路径main.c */ #include ch32v30x.h #include lvgl.h #include usb_hid_mouse.h int main(void) { SystemInit(); Delay_Init(); /* 初始化串口用于调试打印 */ USART_Init(115200); /* 初始化屏幕 */ lcd_init(); /* 初始化 LVGL */ lv_init(); lv_disp_drv_register(disp_drv); /* 初始化 USB 主机准备读取鼠标 */ usb_hid_mouse_init(); /* 注册 LVGL 鼠标输入设备 */ lv_port_indev_init(); while (1) { usb_hid_mouse_poll(); lv_task_handler(); Delay_Ms(5); } }5.2 编写 HID 鼠标解析任务鼠标数据解析层需要对外提供一个统一的获取函数供 LVGL 输入设备回调调用。比较推荐的做法是在 usb_hid_mouse.c 中维护一个全局结构体再通过函数返回/* 文件路径usb_hid_mouse.h */ #ifndef __USB_HID_MOUSE_H #define __USB_HID_MOUSE_H #include stdint.h /* 屏幕分辨率按实际工程修改 */ #define SCREEN_WIDTH 320 #define SCREEN_HEIGHT 240 typedef struct { int16_t x; int16_t y; uint8_t left_button; uint8_t right_button; uint8_t middle_button; } mouse_data_t; void usb_hid_mouse_init(void); void usb_hid_mouse_poll(void); mouse_data_t usb_hid_mouse_get_data(void); #endif这样的好处是LVGL 输入设备层不需要关心 USB 细节只需要调用 usb_hid_mouse_get_data 就能拿到当前光标坐标和按键状态分层清晰后续如果改成触摸屏输入只需替换底层实现。5.3 注册 LVGL 指针输入设备在 LVGL 8.3 中注册一个鼠标指针输入设备的代码如下/* 文件路径lv_port_indev.c */ #include lvgl.h #include usb_hid_mouse.h static lv_indev_drv_t mouse_drv; static lv_indev_t *mouse_indev NULL; static lv_obj_t *cursor_obj NULL; static void mouse_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { mouse_data_t m usb_hid_mouse_get_data(); >void SysTick_Handler(void) { lv_tick_inc(1); }如果使用 FreeRTOS则可以在低优先级任务中调用 lv_tick_inc。随后在主循环里周期性调用 usb_hid_mouse_poll 和 lv_task_handlerwhile (1) { usb_hid_mouse_poll(); lv_task_handler(); Delay_Ms(5); }没必要每毫秒都轮询一次 USB 鼠标。USB 鼠标的默认报告周期通常是 10ms因此主循环 5ms 或 10ms 运行一次已经足够还能把 CPU 时间留给 LVGL 刷新。5.5 运行效果与验证程序烧录后插入 USB 鼠标观察现象屏幕上有光标图案移动鼠标时光标跟随移动鼠标左键点击 LVGL 按钮按钮进入按下状态松开后触发点击事件鼠标右键暂未映射时默认不会影响界面鼠标滚轮如果读了滚轮值可以自行映射到 lv_obj_scroll_by 或列表滚动。如果第一步就看不到光标先检查 USB 枚举是否成功再检查光标图片来源是否正确。调试时建议在串口打印鼠标坐标方便快速定位问题printf(x%d y%d left%d\n, mouse.x, mouse.y, mouse.left_button);6. 常见问题与排查思路USB 鼠标接入 LVGL 过程中最容易出问题的环节是 USB 枚举和坐标处理。下面整理了一个排查清单问题现象常见原因解决思路鼠标插入后无任何反应USB 电源不足、D/D- 接反、主机初始化失败检查供电电压确认 D/D- 连接串口打印 USB 状态枚举失败设备描述符读取失败鼠标兼容性差、线材过长、时钟配置不对换一个鼠标测试缩短线缆检查 USB 时钟配置光标不动未做坐标累加或坐标被裁剪到 0确认位移字段强转为有符号数检查 dx/dy 是否持续更新光标只能往一个方向移动位移数据被当成无符号数将报告第 1、2 字节强转为 int8_t光标移动很快飞出屏幕位移累加后没有做边界裁剪在鼠标驱动层限制坐标范围左键点击无效LVGL 坐标与屏幕分辨率不匹配确认 SCREEN_WIDTH 和 SCREEN_HEIGHT 与实际屏幕一致界面刷新卡顿USB 轮询阻塞、LVGL 缓冲区太小轮询改为非阻塞调用增大 LV_MEM_SIZE 和刷新缓冲区鼠标热插拔后失效未处理断开和重新枚举事件增加设备拔出检测和重新枚举逻辑光标有残影屏幕刷新率低或光标图片覆盖层未清除检查屏幕 SPI 速率升级为 RGB 接口屏幕在项目开发阶段建议把 USB 枚举状态、HID 报告原始数据都通过串口打印出来。对比插上鼠标前后的打印信息能快速判断问题出在硬件还是软件。7. 最佳实践与工程建议7.1 内存与性能优化CH32V303 虽然自带较大 SRAM但 LVGL 界面复杂后内存依然吃紧。以下几点优化建议在实际项目中非常有用LV_MEM_SIZE 不要盲目调大。先跑稳定的界面再根据 lv_mem_monitor 的统计数据调整颜色深度优先用 16 位配合 RGB565 屏幕减少显存和缓冲占用LVGL 的刷新缓冲区可以设为屏幕宽度的 1/10 到 1/4用两块缓冲区交替刷新更流畅图片资源尽量转成 C 数组并压缩色深避免使用外置字体大数组界面中不要创建太多透明图层透明混合会显著增加刷新耗时在鼠标快速移动时LVGL 会频繁触发控件重绘可以降低鼠标坐标更新频率到 20ms 一次兼顾流畅度和 CPU 占用。7.2 USB 硬件可靠性USB 鼠标在嵌入式系统中工作稳定性是重中之重。从项目经验来看以下几点值得特别关注给 USB 鼠标单独供电不要与 MCU 共用同一个 LDO鼠标启动瞬间电流较大容易导致 MCU 复位D 和 D- 走线尽量控制阻抗如果在开发板上做实验杜邦线不要超过 10cm在 VBUS 和 GND 之间加 10uF 钽电容或电解电容滤除鼠标移动时产生的电源噪声如果产品对热插拔有要求一定要做拔出检测和重新枚举处理否则用户拔掉鼠标再插上设备可能无法恢复。7.3 功能扩展方向鼠标控制 LVGL 只是第一步基于同一套 USB 主机框架还可以扩展出很多实用功能接入 USB 键盘把按键事件映射到 LVGL 按键输入设备实现键盘焦点操作接入 USB 触摸板相当于一个绝对坐标指针设备支持鼠标滚轮将滚轮值映射到 LVGL 列表滚动或 slider 调节通过鼠标右键实现返回、菜单呼出等快捷操作将 USB 鼠标和触摸屏同时注册为两个指针设备满足不同用户的操作习惯。如果只是开发阶段需要鼠标调试界面更推荐先用 PC 上的 LVGL 模拟器开发界面逻辑再移植到 CH32V303 上。模拟器环境下鼠标默认可用能省去很多硬件调试时间。8. 总结与学习路线这篇文章从 CH32V303 的 USB 主机能力出发介绍了如何把 USB 鼠标作为 LVGL 的指针输入设备。核心要点可以概括为三句话鼠标数据的本质是相对位移必须在驱动层累加成绝对坐标LVGL 的 indev 接口是把硬件输入和图形界面解耦的关键USB HID 报告解析时注意有符号数与无符号数的转换这是最容易踩的坑。建议的实践顺序是先用官方 EVT 包跑通 USB 鼠标枚举和读取打印 HID 报告数据确认数据正确后再注册 LVGL 输入设备最后做 UI 事件响应和界面优化。不要一上来就同时调 USB 和 LVGL分开调试问题会少很多。如果接下来想继续深入可以按照“键盘接入 - 滚轮映射 - 触摸屏双输入 - 复杂界面动画”的顺序逐步扩展。把这块跑通后CH32V303 基本就能当作一个小型嵌入式 HMI 主机来用了。