ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CH32V303上LVGL鼠标控制:输入设备驱动与调试

2026/8/31 12:54:18 拓冰建站 浏览量
CH32V303上LVGL鼠标控制:输入设备驱动与调试 嵌入式 GUI 调试时经常遇到一个现实问题开发板上跑起了 CH32V303 和 LVGL但输入设备只有触摸屏或者干脆没有输入。触摸屏确实直观可调试按钮尺寸、列表滑动、图表刷新时鼠标往往更快。这篇文章围绕“CH32V303 跑 LVGL用鼠标控制界面”这条主线讲清楚 LVGL 输入设备驱动的工作机制、CH32V303 接入鼠标的两种硬件路径、驱动代码怎么写、坐标和点击如何映射以及鼠标不动、点击失效、坐标偏移这类问题怎么排查。文章会给出可以放到项目里直接修改的代码骨架和一份排查清单适用于正在做 LVGL 交互开发、想在 CH32V303 上验证鼠标控制的开发者。1. 先理解 LVGL 的输入设备层鼠标不是独立概念很多人在 CH32V303 上接入鼠标时第一反应是去找“鼠标驱动库”这其实找错了方向。LVGL 并不直接认识鼠标它认识的是输入设备抽象层。只要把鼠标的坐标和按键状态喂给 LVGL 的输入设备接口界面上的按钮、列表、滑块就能像触摸屏一样响应。1.1 Pointer、Keypad、Encoder 三种输入类型LVGL 的输入设备机制位于lv_indev模块它把所有输入硬件归纳为三类设备类型。设备类型LVGL 常量典型硬件驱动输出内容指针设备LV_INDEV_TYPE_POINTER触摸屏、鼠标屏幕坐标 x、y按下/释放状态键盘设备LV_INDEV_TYPE_KEYPAD矩阵键盘、遥控器按键编码编码器设备LV_INDEV_TYPE_ENCODER旋转编码器旋转方向和按下事件外部按钮LV_INDEV_TYPE_BUTTON独立按键按键值列表鼠标在 LVGL 中属于 Pointer 设备。Pointer 设备的关键不是“移动方式”而是“最终能输出一个屏幕坐标和一个按下状态”。触摸屏输出的坐标是绝对坐标鼠标则可能是相对位移也可能经过协议层换算成绝对坐标。无论如何到达 LVGL 之前都要统一成“当前指针在屏幕上的绝对位置”。这里的易错点很常见一些人把 USB 鼠标的原始相对位移直接塞进>static lv_indev_drv_t mouse_drv; void mouse_indev_init(void) { lv_indev_drv_init(mouse_drv); mouse_drv.type LV_INDEV_TYPE_POINTER; mouse_drv.read_cb mouse_read_cb; lv_indev_t *mouse lv_indev_drv_register(mouse_drv); (void)mouse; } static void mouse_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { >// report[0]: 按键位图bit0 左键bit1 右键bit2 中键 // report[1]: X 方向相对位移有符号数 // report[2]: Y 方向相对位移有符号数 // report[3]: 滚轮可选特别提醒report[1]和report[2]是相对位移不是绝对坐标。CH32V303 端需要把每次位移累积到当前坐标上。2.2 路径二上位机或无线鼠标模块通过串口转发坐标如果硬件上不方便直接接 USB Host或者板子上已经只留了串口可以选择上位机转发方案。PC 或者手机端读取鼠标位置后通过串口把“绝对坐标 按键状态”发送给 CH32V303。这条路径适合快速验证界面交互也适合无线鼠标方案。2.4G 无线鼠标模块本身往往是 PS/2 或串行接口解读后也能输出坐标。相比 USB HOST串口转发方案的底层代码简单调试时还能直接在串口里看坐标。缺点是需要外部设备参与实时性受串口波特率和帧间隔影响。如果只是让 LVGL 界面完成点击、拖拽、列表滚动14400 或 115200 波特率通常够用。2.3 两条路径如何选型对比项USB HOST 直连串口坐标转发硬件改动需要 USB 插座与 VBUS 供电只需 UART TX/RX底层复杂度高需要枚举与 HID 解析低状态机解析即可实时性高中断回报受波特率和协议影响独立性鼠标直接接 MCU独立运行依赖上位机或转发模块调试便利性需要 USB 分析手段串口日志可以直接打印坐标适用场景产品化 HMI、一体化面板开发调试、快速原型、无线鼠标一般建议如果目的只是验证 CH32V303 上的 LVGL 控件交互优先走串口转发方案代码少、问题容易定位。如果最终产品必须插 USB 鼠标再投入时间调 USB HOST 路径。3. 开发环境、工程模板与 LVGL 版本选择无论选择哪条硬件路径工程基础都是一样的CH32V303 工程里要能跑起 LVGL并且有一个稳定的 tick 源。3.1 CH32V303 的编译与下载工具CH32V303 是基于 RISC-V 内核的 MCU常见开发方式有两种。MounRiver StudioWCH 官方提供的集成开发环境内置 RISC-V GCC 工具链可直接导入 CH32V303 EVT 工程并下载调试。Keil MDK安装 WCH Device Pack 后可以开发 CH32V303工程配置和下载算法需要按芯片型号选择。调试器一般使用 WCH-Link。下载时要确认 Flash 容量、RAM 大小和芯片型号匹配。CH32V303 不同封装的 Flash、SRAM 容量可能有差异以所选型号数据手册为准。工程里如果用了外部晶振还要检查时钟树配置USB 外设需要 48MHz 时钟。3.2 LVGL 源码如何加入工程LVGL 建议从 GitHub 的 Release 页面下载正式版本例如 8.x 或 9.x。不要直接使用 main 分支因为 main 分支可能处于开发状态API 不稳定。一个最小工程至少包含以下内容project/ ├── App/ │ ├── main.c │ ├── mouse_input.c │ └── mouse_input.h ├── lvgl/ │ ├── src/ │ ├── lv_conf.h │ └── lvgl.h └── Core/ ├── ch32v30x_conf.h └── ...在 MounRiver Studio 中把lvgl/src全部加入编译路径并确保编译标准支持 C99。lv_conf.h需要从lv_conf_template.h复制出来并打开#if 1开关。LVGL 的 tick 源使用lv_tick_inc()提供通常放在 SysTick 中断里每毫秒调用一次。void SysTick_Handler(void) { lv_tick_inc(1); lv_timer_handler(); }3.3 lv_conf.h 里需要优先确认的配置项lv_conf.h是 LVGL 的行为总开关。下面几个配置项在 CH32V303 这类 MCU 上直接影响运行结果。配置宏建议值作用与风险LV_COLOR_DEPTH16 或 32颜色深度越高显存和缓冲区占用越大LV_MEM_SIZE8KB 到 32KBLVGL 运行时内存池太小会导致对象创建失败LV_DPI_DEF100 左右影响控件尺寸与字体大小的度量LV_USE_LOG调试时开可以输出 LVGL 内部错误日志发布时关闭LV_USE_PERF_MONITOR调试时开屏幕角落显示 CPU 占用与 FPSLV_FONT_MONTSERRAT_14开内置字体最小示例需要内存方面CH32V303 的部分型号带有较大 Flash 和适量 SRAM。实际分配时LV_MEM_SIZE可以先从 16KB 开始试界面控件少就调小图表、图片多就调大。如果编译通过但运行后控件不显示首先查LV_MEM_SIZE是否足够以及 LVGL 日志是否报内存分配失败。4. USB HOST 直连鼠标的核心实现如果选择 USB HOST 路径CH32V303 底层要完成 USB 枚举和 HID 报告解析。不同版本的 USB 主机库接口不同这里不贴整套库代码而是给出驱动层与 LVGL 之间的接口实现思路。4.1 定义统一的鼠标状态结构体无论底层是 USB 还是串口最后都要写同一个结构体。#define SCREEN_W 320 #define SCREEN_H 240 typedef struct { int16_t x; int16_t y; uint8_t btn; uint8_t is_valid; } mouse_state_t; static mouse_state_t mouse_state;坐标统一存放在以像素为单位的屏幕坐标里范围限制在屏幕宽高之内。这样上层read_cb不需要关心鼠标协议。4.2 从 HID 报告更新坐标USB 鼠标的 HID 报告到达后驱动层把相对位移累加到当前坐标并做边界裁剪。void mouse_hid_report_handler(uint8_t *report, uint8_t len) { int8_t dx; int8_t dy; if (len 3) { return; } dx (int8_t)report[1]; dy (int8_t)report[2]; mouse_state.x dx; mouse_state.y - dy; // 屏幕坐标系与鼠标 Y 轴方向通常相反 if (mouse_state.x 0) { mouse_state.x 0; } if (mouse_state.x SCREEN_W) { mouse_state.x SCREEN_W - 1; } if (mouse_state.y 0) { mouse_state.y 0; } if (mouse_state.y SCREEN_H) { mouse_state.y SCREEN_H - 1; } mouse_state.btn report[0] 0x01U; mouse_state.is_valid 1; }这里有两个常见问题。第一dx和dy必须用有符号数读取错误写法是直接mouse_state.x report[1]鼠标往负方向移动时坐标会变成大正数。第二dy的符号要根据实际鼠标方向和屏幕坐标系调整。LVGL 屏幕原点在左上角Y 向下增大而不同鼠标上报的正负方向可能不同写成- dy后如果发现上下颠倒改成 dy即可。4.3 read_cb 与临界区处理LVGL 的read_cb实现和前面一致。static void mouse_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { >mouse_report_t report; if (os_msg_q_get(mouse_q, report, 0) OS_OK) { mouse_state.x report.x; mouse_state.y report.y; mouse_state.btn report.btn; }这样 USB 中断只负责往队列里放数据LVGL 任务只从队列取数据双方不直接共享变量。5. 用串口转发坐标作为备选方案串口方案的核心优势是协议简单、不需要处理 USB 枚举特别适合作为 LVGL 交互验证的起点。下面给出一个可用的最小协议和解析状态机。5.1 自定义串口鼠标协议上位机或鼠标模块发送固定长度帧每帧包含绝对坐标与按键状态。帧头1 帧头2 数据长度 X高字节 X低字节 Y高字节 Y低字节 按键 校验 0xAA 0x55 0x05 0x00 0x78 0x00 0x50 0x01 ...对应数据含义字段值示例说明X 高字节0x00坐标 120 的高 8 位X 低字节0x78坐标 120 的低 8 位Y 高字节0x00坐标 80 的高 8 位Y 低字节0x50坐标 80 的低 8 位按键0x01bit0 为 1 表示左键按下校验可以用简单累加和。示例帧含义是屏幕坐标 x120、y80左键按下。5.2 MCU 端串口解析状态机串口接收采用状态机方式逐字节处理不依赖中断一次性收到完整帧。#define FRAME_HEAD0 0xAA #define FRAME_HEAD1 0x55 static uint8_t rx_buf[8]; static uint8_t rx_cnt 0; void mouse_uart_byte_handler(uint8_t byte) { if (rx_cnt 0) { if (byte FRAME_HEAD0) { rx_buf[rx_cnt] byte; } return; } if (rx_cnt 1) { if (byte FRAME_HEAD1) { rx_buf[rx_cnt] byte; } else { rx_cnt 0; } return; } rx_buf[rx_cnt] byte; if (rx_cnt 4 rx_cnt rx_buf[2] 3) { if (mouse_uart_frame_check(rx_buf, rx_cnt) 0) { mouse_uart_frame_parse(rx_buf); } rx_cnt 0; } }状态机的好处是能处理粘包和半包。即便串口一次收到多帧或者一帧被拆成多次到达也能通过帧头和长度字段重新对齐。解析时把帧里的坐标写入mouse_statevoid mouse_uart_frame_parse(uint8_t *buf) { uint16_t x (buf[3] 8) | buf[4]; uint16_t y (buf[5] 8) | buf[6]; uint8_t btn buf[7]; if (x SCREEN_W) { x SCREEN_W - 1; } if (y SCREEN_H) { y SCREEN_H - 1; } mouse_state.x x; mouse_state.y y; mouse_state.btn btn; mouse_state.is_valid 1; }这里的mouse_state与 USB HOST 方案共用同一个结构体。也就是说read_cb不需要任何修改两种输入源可以互通。5.3 两种方案的代码取舍USB HOST 方案里鼠标是相对位移需要累积坐标串口方案里上位机直接给绝对坐标MCU 无需累积。但两者到达 LVGL 前都必须完成边界裁剪。如果把边界裁剪放在read_cb里每个 LVGL 读取周期都可能触发多次裁剪效率无损但代码职责不清晰。推荐统一在状态写入时裁剪read_cb只做读操作。6. 运行验证让鼠标真实控制界面驱动代码写完后不能只看编译通过。要真正确认鼠标控制有效需要跑一个能直接观察结果的最小界面并通过日志观察坐标。6.1 最小验证界面按钮点击显示坐标在main.c中创建一个按钮点击按钮时在屏幕上创建一个标签显示鼠标点击时的坐标。static void btn_click_cb(lv_event_t *e) { lv_obj_t *label lv_label_create(lv_scr_act()); lv_label_set_text_fmt(label, x%d y%d, mouse_state.x, mouse_state.y); lv_obj_align(label, LV_ALIGN_TOP_LEFT, mouse_state.x, mouse_state.y); } void ui_mouse_demo_init(void) { lv_obj_t *btn lv_btn_create(lv_scr_act()); lv_obj_set_size(btn, 120, 40); lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0); lv_obj_add_event_cb(btn, btn_click_cb, LV_EVENT_CLICKED, NULL); lv_obj_t *label lv_label_create(btn); lv_label_set_text(label, Click); lv_obj_center(label); }这个例子的验证目标很明确鼠标移动到按钮上按下左键并释放LVGL 如果触发了LV_EVENT_CLICKED就会在鼠标附近显示坐标文本。如果点击事件不触发优先检查按键释放状态是否由PR切回REL。6.2 串口日志验证坐标链路在鼠标状态更新处打印坐标可以快速定位问题所在。void mouse_state_update(int16_t x, int16_t y, uint8_t btn) { mouse_state.x x; mouse_state.y y; mouse_state.btn btn; printf(mouse: x%d y%d btn%d\r\n, x, y, btn); }正常现象是鼠标移动时串口输出随鼠标位置连续变化按下左键时btn1释放后btn0。如果鼠标动了但x/y不变说明底层驱动没有获取到数据问题在 USB 或串口层。如果x/y变化但界面无反应问题在read_cb注册或坐标裁剪。6.3 内存与性能观察LVGL 在 CH32V303 这样的 MCU 上运行需要重点看 RAM 占用。打开LV_USE_PERF_MONITOR后界面角落会显示 FPS 和 CPU 占用率。刷新速度慢时优先检查绘制缓冲区大小。显存配置内存占用界面复杂度适用场景仅使用LV_COLOR_DEPTH16缓冲区 320x40约 25KB简单按钮、文本入门验证缩小到 320x20约 12.5KB低刷新率界面控件较少的 HMI使用 32 位色深和较大缓冲区更高图表、图片、动画需要平滑效果时如果连续运行一段时间后界面卡死考虑是LV_MEM_SIZE耗尽也可能是 LVGL 定时器处理被中断频繁影响。7. 常见问题排查鼠标不动、坐标偏移、点击失效鼠标驱动接入 LVGL 后大部分问题集中在“坐标来源”和“状态翻转”两处。下面整理一份常见问题对照表。问题现象常见原因检查方式处理建议鼠标移动屏幕无反应read_cb 未注册或坐标未更新在 read_cb 里打印 x/y确认lv_indev_drv_register已调用且mouse_state有变化鼠标移动方向错乱Y 轴符号相反上下移动鼠标观察坐标调整 HID 报告里 dy 的取反逻辑坐标越界或到处跳相对位移被当成绝对坐标打印坐标值使用前先累积坐标并裁剪到屏幕范围点击按钮无反应释放状态未更新LVGL 未收到 REL打印 btn 状态检查鼠标状态更新代码确保释放时置 0界面移动卡顿LV_MEM_SIZE 不足或缓冲区太小打开 LV_USE_LOG 和 PERF_MONITOR减小色深、调小缓冲区或优化控件数量编译后 RAM 不足LVGL 内存池和绘制缓冲区过大查看链接 map 文件调小 LV_MEM_SIZE、显存缓冲和动画对象USB 枚举失败时钟配置不对或 VBUS 供电不足检查 48MHz 时钟、USB 引脚参考 EVT 例程逐项核对排查顺序建议按这个清单走能少走很多弯路先确认底层鼠标数据是否有更新。串口方案直接观察mouse_state_update里的日志。再确认read_cb是否被 LVGL 调用。可以在回调中加计数变量。确认坐标是否被正确裁剪到屏幕范围。确认按键状态是否同时包含按下和释放两个动作。最后检查 LVGL 版本 API 是否匹配例如lv_indev_drv_t的字段在 8.x 和 9.x 中有差异。鼠标移动正常但点击失效是新手最容易遇到的坑。很多实现只在按下时把>typedef struct { int16_t x; int16_t y; uint8_t btn; uint8_t is_valid; } mouse_state_t;这种设计的好处是低耦合。今天用串口调试明天换 USB 鼠标主界面代码和read_cb都不会受影响。8.2 发布前检查清单检查项操作要求坐标边界鼠标移动全屏坐标始终位于 0 到宽高减 1 之间按键释放按下和释放都要上报点击事件能连续触发日志关闭发布时关闭 LVGL 日志和串口调试打印内存余量检查 LV_MEM_SIZE 剩余避免运行一段时间后内存耗尽中断安全底层驱动与 read_cb 之间的数据共享有保护屏幕刷新在典型界面上观察 FPS确保刷新周期可接受异常恢复鼠标拔掉或串口断线时界面不应卡死8.3 可以从这里继续扩展的方向鼠标控制 LVGL 的下一步可以从性能和交互两个方向扩展。性能方面可以把鼠标事件放入 FreeRTOS 消息队列由独立输入任务统一处理。这样 USB 中断只负责投递数据LVGL 任务只负责消费能避免长时间关中断影响系统实时性。交互方面鼠标比触摸更容易实现悬停态、拖拽、滚轮滚动。LVGL 的 Pointer 设备也支持按下、长按、拖拽等事件。如果要做图表类界面可以在鼠标点击位置触发某个图表更新例如点击屏幕时刷新一个柱状图数据源就是mouse_state.x和mouse_state.y。这种方式很适合做 HMI 界面原型验证。整体来看CH32V303 上接入鼠标控制 LVGL核心不在 USB 还是串口而在于把鼠标输入抽象成稳定的坐标和按键状态再交给 LVGL 的 input device 层。先跑通一个按钮点击再逐步增加协议解析、内存优化和并发保护整个移植过程会清晰很多。