拓冰建站拓冰建站
首页 / 资讯中心 / 正文

CH32V303接入USB鼠标控制LVGL:坐标转换与驱动实现

之前在把 LVGL 界面从触摸屏迁到鼠标控制的场景时踩了不少坑。最典型的问题是鼠标上报的是“相对位移”而 LVGL 的指针设备需要的是“屏幕绝对坐标”这两者之间的转换如果不处理干净光标要么漂出屏幕要么点击永远对不上按钮。本文基于 CH32V303 这颗国产 RISC-V MCU完整拆解用 USB 鼠标控制 LVGL 的实现思路内容包括 USB HID 报文解析、LVGL 输入设备驱动注册、坐标转换与灵敏度控制以及完整的可复制代码。适合正在做嵌入式 GUI、想把 LVGL 界面从触摸交互扩展为鼠标交互的开发者阅读。1. 背景与核心概念1.1 为什么要用鼠标控制 LVGL很多嵌入式 GUI 项目默认使用触摸屏作为人机交互方式但实际产品中未必都有触摸层。工业控制面板、调试工具、桌面端模拟器、带 HDMI 输出的显示盒子更常见的是直接插一个 USB 鼠标。还有一些场景是设备本身有触摸屏但工程师在开发调试阶段不想频繁触碰屏幕更希望用鼠标快速操作界面。这时候 LVGL 就需要接入一个“鼠标输入设备”。LVGL 本身对输入设备有比较清晰的抽象支持触摸屏、鼠标、键盘、编码器等。其中鼠标的逻辑和触摸屏不一样触摸屏上报的是绝对坐标芯片直接告诉 LVGL“手指按在屏幕的哪个位置”鼠标上报的却是相对位移也就是“这次移动了 X 方向多少、Y 方向多少”。所以鼠标接入 LVGL 的核心工作就是把相对位移累加成屏幕绝对坐标并且处理好边界和按键状态。1.2 CH32V303 与 USB HID 鼠标CH32V303 是沁恒推出的一款基于 RISC-V 内核的 32 位通用 MCU。它内置了 USB 主机控制器可以外接键盘、鼠标、U 盘这类标准 USB 设备。对于“用鼠标控制 LVGL”这个需求来说芯片硬件层面的能力已经具备重点在于软件上把 USB 枚举、HID 协议解析和 LVGL 输入事件串起来。USB 鼠标属于 HIDHuman Interface Device人机接口设备类设备。鼠标在连接后主机会向设备发送 Set_Protocol 请求把鼠标切换到 Boot Protocol 模式。在 Boot Protocol 模式下鼠标的输入报告格式非常固定通常是这样字节偏移含义Byte 0按键状态bit0 表示左键bit1 表示右键bit2 表示中键Byte 1X 方向位移有符号数正数表示向右Byte 2Y 方向位移有符号数正数表示向下Byte 3滚轮位移有符号数这个固定格式给解析带来了很大方便不需要去解析 HID Report Descriptor直接按偏移读取即可。1.3 LVGL 输入设备模型LVGL 把输入设备抽象为lv_indev无论底层是触摸屏、鼠标还是键盘最终都要注册一个lv_indev_drv_t驱动结构体并实现对应的read_cb回调函数。LVGL 会周期性调用这个回调从里面读取输入数据。鼠标在 LVGL 中通常注册为LV_INDEV_TYPE_POINTER类型也就是指针设备。指针设备的数据结构是lv_indev_data_t它包含两个关键字段point当前光标在屏幕上的绝对坐标。state当前按键状态LV_INDEV_STATE_PRESSED表示按下LV_INDEV_STATE_RELEASED表示释放。LVGL 拿到坐标和按键状态后会根据坐标推算出当前命中的对象然后向对象发送点击、释放、长按等事件。所以写鼠标驱动时不要把“鼠标事件”直接发给某个控件而是老老实实地维护好坐标和按键状态LVGL 会自动完成事件派发。2. 环境准备与硬件接线2.1 开发环境本文示例使用 CH32V303但代码思路对 CH32V305、CH32V307 同样适用。项目说明芯片CH32V303RISC-V 内核主频 144MHzIDEMounRiver StudioMRS或使用 CH32V30x EVT 工程配合 GCC 工具链官方 SDKCH32V30x EVT官方例程中包含 USB Host HID 键鼠示例LVGL 版本本文以 LVGL 8.3.x 为例9.x 部分 API 有调整但思路相同显示屏本文以 320x240 分辨率 RGB/SPI 屏为例实际分辨率可替换版本需要根据你的项目实际情况调整。LVGL 发展比较快官方代码结构变化也比较大如果使用 9.x需要参考官方手册调整初始化和驱动注册部分核心的输入设备回调机制没有变。2.2 硬件连接方式CH32V303 的 USB 主机接口需要一根 OTG 转接或者直接连接 USB 鼠标。以 USBFS 接口为例连接关系如下CH32V303 引脚USB 鼠标接口USBFS_DMD-USBFS_DPDGNDGND5V或 3.3V 转接VBUS部分开发板上的 USB 接口用的是 Micro USB 或者 Type-C配合 OTG 线即可外接标准 USB 鼠标。注意USB 主机模式需要为鼠标提供 5V 电源如果开发板没有升压电路需要外接 5V 供电否则鼠标可能无法枚举。2.3 软件工程目录整个工程的逻辑分成三层CH32V303_LVGL_Mouse/ ├── User/ │ ├── main.c // 主循环初始化与任务调度 │ ├── lvgl_mouse_indev.c // LVGL 鼠标输入设备驱动 │ └── lvgl_mouse_indev.h ├── LVGL/ │ ├── lv_conf.h // LVGL 配置文件 │ └── src/ ├── USB_Host/ │ ├── usb_host_hid.c // 官方 USB Host HID 驱动 │ └── usb_host_hid.h └── LCD/ └── lcd_driver.c // 屏幕驱动核心工作量在lvgl_mouse_indev.c以及 USB Host 解析出来的鼠标报文如何送到这个文件里。3. 核心原理拆解3.1 LVGL indev 驱动注册流程LVGL 输入设备的使用流程非常固定分为四步第一步初始化一个lv_indev_drv_t结构体使用lv_indev_drv_init完成初始化。第二步设置设备类型为LV_INDEV_TYPE_POINTER。第三步实现read_cb回调函数。这个回调会被 LVGL 周期性调用里面需要填写当前光标坐标和按键状态。第四步调用lv_indev_drv_register注册驱动保存返回的lv_indev_t *指针。在 LVGL 8.3 中read_cb的原型如下void mouse_read_cb(lv_indev_drv_t * drv, lv_indev_data_t * data) { >current_x dx; current_y dy;但这种直接累加有一个实际问题USB 鼠标一次移动 5 个单位如果屏幕是 320 宽从左边移到右边只需要 64 次移动鼠标的物理位移和屏幕光标移动速度不成比例。所以通常要引入一个灵敏度系数senscurrent_x dx * sens; current_y dy * sens;灵敏度的具体值取决于屏幕分辨率和鼠标的 DPI需要实际调试。320x240 的小屏幕上sens取 2 到 4 比较合适如果是 800x480 的大屏幕sens可能需要调到 6 以上。3.3 坐标范围限制与灵敏度控制累加后的坐标必须做边界钳制否则光标会漂出屏幕。LVGL 虽然内部也会尝试限制光标位置但如果在read_cb里直接返回越界坐标可能造成光标消失或点击对象异常。比较稳妥的做法是在累加后立刻做一次钳制if (current_x 0) current_x 0; if (current_x SCREEN_W) current_x SCREEN_W - 1; if (current_y 0) current_y 0; if (current_y SCREEN_H) current_y SCREEN_H - 1;这里要单独提一下 Y 轴方向。LVGL 的屏幕坐标系是左上角为原点Y 轴向下递增。大部分 USB 鼠标的 HID 报告中dy正数表示鼠标向下移动负数表示向上移动和 LVGL 坐标方向一致所以可以直接累加。但如果你用的是某些国产鼠标或者自定义 HID 设备可能会出现 Y 轴反向的情况这时候只需要把dy取反即可。4. 完整实战代码4.1 初始化 USB HID 主机USB Host 部分基于 CH32V30x 官方 EVT 中的 USBFS_HOST_HID_KM 例程修改。不同 SDK 版本的初始化函数名称可能略有差异下面的代码给出核心逻辑实际使用时需要结合官方例程调整。// 文件路径User/main.c #include debug.h #include usb_host_hid.h #include lvgl.h #include lvgl_mouse_indev.h #include lcd_driver.h int main(void) { SystemCoreClockUpdate(); Delay_Init(); USART_Printf_Init(115200); printf(System init ok\r\n); // 初始化 USB FS Host准备接入鼠标 USBFS_Host_Init(); printf(USB HOST init ok\r\n); // 初始化 LCD 与 LVGL lv_init(); LCD_Init(); lv_disp_drv_full_init(); // 显示驱动初始化按自己屏幕驱动实现 // 注册 LVGL 鼠标输入设备 lvgl_mouse_indev_init(); printf(LVGL mouse indev init ok\r\n); while (1) { // USB 主机枚举轮询处理设备插拔 USBH_DeviceEnumProcess(); // HID 数据处理内部会解析鼠标报文并回调到 mouse_parse_hid_report() HID_Process(); // LVGL 事务处理 lv_timer_handler(); Delay_Ms(2); } }USBH_DeviceEnumProcess负责 USB 设备的枚举它必须被周期性调用。HID_Process内部会解析 HID 报告并把鼠标位移和按键信息传递到上层。LVGL 的lv_timer_handler也必须周期性执行否则界面刷新和事件派发都会停止。4.2 解析鼠标 HID 报文鼠标 HID 报文解析函数建议单独放在一个文件中方便和 Official SDK 解耦。下面的函数接收一个 HID 报告缓冲区按 Boot Protocol 标准解析鼠标数据。// 文件路径User/lvgl_mouse_indev.c #include lvgl_mouse_indev.h #define SCREEN_W 320 #define SCREEN_H 240 #define MOUSE_SENS 3 static lv_indev_t *mouse_indev; static bool mouse_left_down; static int16_t mouse_pos_x SCREEN_W / 2; static int16_t mouse_pos_y SCREEN_H / 2; /** * brief 解析 USB HID 鼠标报告 * param buf HID 报告缓冲区 * param len 缓冲区长度 */ void mouse_parse_hid_report(uint8_t *buf, uint16_t len) { if (buf NULL || len 3) { return; } // Byte0按键状态 uint8_t button buf[0]; mouse_left_down (button 0x01) ! 0; // Byte1/Byte2X/Y 相对位移转换为有符号数 int8_t dx (int8_t)buf[1]; int8_t dy (int8_t)buf[2]; // 如果 Y 方向反向在这里取反 // dy -dy; mouse_pos_x dx * MOUSE_SENS; mouse_pos_y dy * MOUSE_SENS; // 坐标边界钳制 if (mouse_pos_x 0) mouse_pos_x 0; if (mouse_pos_x SCREEN_W) mouse_pos_x SCREEN_W - 1; if (mouse_pos_y 0) mouse_pos_y 0; if (mouse_pos_y SCREEN_H) mouse_pos_y SCREEN_H - 1; }这段代码的核心是mouse_parse_hid_report函数它应该被 USB Host 驱动在收到鼠标输入报告时调用。比如在官方例程的HID_MouseReport_Deal或者类似回调中直接把缓冲区传进来即可。4.3 注册 LVGL 鼠标输入设备接下来是 LVGL 侧的核心代码实现read_cb回调并注册驱动。// 文件路径User/lvgl_mouse_indev.c /** * brief LVGL 输入设备读取回调 * param drv 输入设备驱动结构体 * param data 输入数据需要填充坐标和按键状态 */ static void mouse_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { (void)drv; >// 伪代码官方 HID 驱动内部 void HID_MouseReport_Deal(uint8_t *buf, uint16_t len) { // 将解析后的 HID 报告交给鼠标解析函数 mouse_parse_hid_report(buf, len); }主循环的任务顺序也有讲究。USB 枚举和 LVGL 刷新放在同一个循环里没问题但要注意lv_timer_handler的执行频率。LVGL 内部维护了一个心跳时间如果调用间隔太长动画和长按事件会不流畅。可以在主循环里保持每 1 到 5 毫秒调用一次。4.5 运行与验证代码下载到开发板后串口会输出System init ok USB HOST init ok LVGL mouse indev registered此时插入 USB 鼠标如果枚举成功屏幕上应该出现光标。移动鼠标光标会跟随移动按下左键可以点击 LVGL 的按钮。如果插入鼠标后没有任何反应优先检查电源和 USB 接线然后在USBH_DeviceEnumProcess调用处加打印确认鼠标是否完成了枚举。5. 常见问题与排查思路在实际开发中很多问题不一定是 LVGL 配置错了而是底层数据没对上。下面整理几个高频问题。问题现象常见原因解决思路光标完全不动USB 鼠标没有完成枚举或鼠标 HID 报文没有回调检查 USB 电源打印枚举日志确认mouse_parse_hid_report被调用光标移动方向相反HID 报文中的 dx/dy 符号与屏幕坐标方向不一致对 dx 或 dy 取反光标移动太快/太慢灵敏度系数MOUSE_SENS不合适根据屏幕分辨率和鼠标 DPI 调整灵敏度点击按钮没有反应read_cb中state一直返回LV_INDEV_STATE_RELEASED检查左键解析逻辑确认按下时state为LV_INDEV_STATE_PRESSED光标偶尔漂出屏幕坐标累加后没有做边界钳制在累加后立即判断边界并钳制界面卡顿、动画不流畅lv_timer_handler调用频率不够缩短主循环延时或者用定时器周期性调用编译报LV_MEM_SIZE不足LVGL 配置的内存池太小调大LV_MEM_SIZE或启用LV_MEM_CUSTOM使用系统堆中文显示乱码当前字体没有包含中文字库使用字库转换工具生成包含中文的 C 数组字体并在主题中启用下面重点分析两个排查频率较高的点。第一个是点击无反应。很多开发者刚移植成功后发现光标能移动但点击按钮没有高亮也没有事件。问题几乎都出在state字段。LVGL 的点击事件需要经历PRESSED和RELEASED两个状态如果在鼠标按下时没有及时把state改为LV_INDEV_STATE_PRESSEDLVGL 就检测不到按下动作。第二个是鼠标移动缓慢或者发飘。这不是逻辑错误而是灵敏度参数没有调好。可以做一个简单的实验固定屏幕分辨率把MOUSE_SENS从 1 开始逐渐增加观察光标移动是否跟手。注意鼠标的 DPI 也会影响手感不同鼠标需要在同一套代码里单独测一遍。6. 工程建议与优化方向6.1 内存占用控制LVGL 本身对内存的消耗不小尤其是使用中文字库、复杂控件和动画时。在 CH32V303 这种 SRAM 几十 KB 级别的 MCU 上内存规划要提前做。lv_conf.h中的LV_MEM_SIZE决定了 LVGL 内部动态内存池的大小。在 320x240 分辨率下如果界面控件不多可以从 16KB 起步测试。如果屏幕分辨率更大或者控件较多可以适当增加但也要注意芯片剩余 SRAM 是否足够。更推荐的方式是启用LV_MEM_CUSTOM让 LVGL 使用 C 库的malloc/free这样调试内存问题时可以直接配合 GCC 的堆信息分析。#define LV_MEM_CUSTOM 1 #define LV_MEM_CUSTOM_INCLUDE stdlib.h在代码中也可以周期性调用lv_mem_monitor通过串口打印 LVGL 内存使用情况方便确认是否有内存泄漏。6.2 鼠标手感优化鼠标控制 GUI 和触摸屏不一样触摸屏天然是绝对定位鼠标是相对定位。为了提升手感可以考虑加一点简单的“移动平滑”逻辑。比如当dx绝对值较小时可以降低灵敏度当dx绝对值较大时适当提高灵敏度。类似鼠标加速度曲线的效果。int16_t speed abs(dx) abs(dy); int16_t sens MOUSE_SENS; if (speed 5) { sens MOUSE_SENS / 2; } else if (speed 20) { sens MOUSE_SENS * 2; } mouse_pos_x dx * sens; mouse_pos_y dy * sens;这种处理在工业 HMI 界面中非常实用可以让精准点击按钮时更稳定快速跨屏移动时又不会太慢。6.3 扩展需求滚轮、右键、键盘协同除了左键点击鼠标的右键、滚轮也可以被扩展。LVGL 的事件系统中右键通常可以作为“返回”按钮处理滚轮可以用于列表滚动。滚轮数据来自 HID 报告的 Byte3。可以在mouse_parse_hid_report中解析出来通过lv_event_send或者 LVGL 9 的新事件接口发送给当前聚焦控件。也可以简单监测滚轮值把它映射为键盘的上/下键事件int8_t wheel (int8_t)buf[3]; if (wheel 0) { lv_group_send_data(lv_group_get_default(), LV_KEY_UP); } else if (wheel 0) { lv_group_send_data(lv_group_get_default(), LV_KEY_DOWN); }注意lv_group_send_data需要控件有焦点并且已经加入 group。对于大多数鼠标指针交互场景滚轮并不是必须的但如果你在界面里放了很多 list 或者可滚动容器滚轮支持会明显提升体验。7. 总结用 CH32V303 的 USB 主机接口读取鼠标再通过 HID 报文解析、坐标累加和 LVGL 输入设备注册就能实现完整的鼠标控制图形界面。整个链路的关键点有三个HID 鼠标报文的解析、相对坐标到绝对坐标的转换、LVGLread_cb回调中坐标和按键状态的正确上报。这套代码不只在 CH32V303 上能用思路可以直接套用到 CH32V307、STM32 加 USB Host 扩展等场景。建议先跑通官方 USB Host HID 例程确认鼠标数据能打印出来再接 LVGL 做界面交互。每一步都验证通过之后再继续往下走排查问题会轻松很多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门