当前位置: 首页 > news >正文

从零构建嵌入式菜单库(一):原型探索——从一段单函数代码开始

从零构建嵌入式菜单库(一):原型探索——从一段单函数代码开始

系列定位:这是一套编写教程——我们将一起从零构建一个基于 U8g2 的嵌入式菜单库,分析每一步的设计决策、收益与代价。
最终产物:u8g2_menu,一个 3500+ 行、14 模块、12 示例工程的开源菜单库。


前言:在一切开始之前

2024 年 6 月,我面对一块 128×64 的 OLED 屏幕和几个按键。U8g2 已经正常驱动这块屏幕,能画线、画圆、显示字符。但仅此而已——没有菜单系统,没有页面切换,没有任何交互框架。

当时的代码大概是这样的:

// 主循环里直接硬编码u8g2_ClearBuffer(&u8g2);u8g2_DrawStr(&u8g2,0,10,"1. Settings");u8g2_DrawStr(&u8g2,0,30,"2. About");u8g2_DrawStr(&u8g2,0,50,"3. Exit");u8g2_SendBuffer(&u8g2);

每加一个页面就要在主循环里塞一堆if/else,上下翻页靠全局变量currentPage来回切——不出三天,main.c就变成了意大利面条。

我需要一个菜单库。但我不想只是"用"一个菜单库——我想一个菜单库,并且把这个过程记录下来。


知识点预备

在阅读本文之前,需要先理解几个概念。

1.1 U8g2 的绘制模型

U8g2 是一个面向帧缓冲的图形库。它不是"画一根线屏幕就立刻显示",而是:

ClearBuffer() → [绘制操作] → SendBuffer()

所有绘制操作(DrawStr、DrawLine、DrawBox 等)都作用在一个内存缓冲区上,最后调用SendBuffer()一次性推送到屏幕。这带来一个关键约束:每一帧的绘制逻辑必须集中完成

1.2 裁剪窗口 (Clip Window)

U8g2 提供u8g2_SetClipWindow(u8g2, x0, y0, x1, y1),限制绘制操作只在指定矩形区域内生效。这是实现"菜单在固定窗口内滚动"的基础。

u8g2_SetClipWindow(u8g2,0,0,128,64);// 只在屏幕范围内绘制u8g2_DrawStr(u8g2,0,80,"hidden");// 超出裁剪区,不会显示u8g2_SetMaxClipWindow(u8g2);// 恢复全屏裁剪

1.3 回调函数 (Callback)

回调函数就是把函数指针作为参数传递,让被调用者在合适的时机"回调"这个函数。在 C 中这样声明:

// 声明一个函数指针类型typedefu8g2_uint_t(*menuItem_cb)(u8g2_t*,u8g2_uint_t,u8g2_uint_t,u8g2_uint_t);// 接收这个函数指针voidoled_display_menu(...,menuItem_cb menuItem){totalLength=menuItem(u8g2,x,y,rowHeight);// 不确定调用的是哪个函数}

这就是菜单库"框架"与"业务逻辑"解耦的基石。


2. 原型代码:一段能跑的单函数菜单

以下是比仓库第一次正式提交更早的原型。它只有一个函数,所有逻辑混在一起,但它能跑——这就是一切的开端。

charoutBuf[64];#ifndefABS#defineABS(s)((s)<0?-(s):(s))#endifu8g2_uint_tposition=0;// 目标滚动位置u8g2_uint_tspe=3;// 滚动速度u8g2_uint_tmaxCharHeight=0;// 最大字符高度u8g2_uint_ttotalLength;// 菜单内容总高度u8g2_uint_twindowHeight=0;// 菜单窗口高度// 菜单内容绘制回调u8g2_uint_tmenuItem(u8g2_t*u8g2,u8g2_uint_tx,u8g2_uint_ty,u8g2_uint_trowHeight){sprintf(outBuf,"c:%d",count);u8g2_DrawStr(u8g2,x,y+=rowHeight,outBuf);sprintf(outBuf,"t:%d",timer);u8g2_DrawStr(u8g2,x,y+=rowHeight,outBuf);returny;// 返回最后一行的 Y 坐标}// 垂直滑块条voidu8g2_DrawVSliderBar(u8g2_t*u8g2,u8g2_uint_tx,u8g2_uint_ty,u8g2_uint_tw,u8g2_uint_th,floatschedule){if(schedule>1)schedule=1;if(schedule<0)schedule=0;u8g2_DrawVLine(u8g2,x+w/2,y,h);u8g2_DrawBox(u8g2,x,y+h*0.7*schedule,w,h*0.3);}// 翻页voidpageUp(){if(position)position-=maxCharHeight;}voidpageDown(){if(position<totalLength-windowHeight)position+=maxCharHeight;}// 主绘制函数voidoled_display_menu(u8g2_t*u8g2,u8g2_uint_tx,u8g2_uint_ty,u8g2_uint_tw,u8g2_uint_th,menuItem_cb menuItem){staticu8g2_uint_t_position=0;// 当前实际滚动位置staticu8g2_uint_t_rowHeight=0;// 当前实际行高if(w<10)return;// 第一步:设置裁剪窗口u8g2_SetClipWindow(u8g2,x,y,x+w-6,y+h);// 第二步:平滑滚动动画if(ABS(position-_position)>spe){if(position>_position)_position+=spe;if(position<_position)_position-=spe;}else{_position=position;}// 第三步:行高动画maxCharHeight=u8g2_GetMaxCharHeight(u8g2);if(_rowHeight<maxCharHeight)_rowHeight+=3;if(_rowHeight>maxCharHeight)_rowHeight-=1;// 第四步:绘制菜单内容totalLength=menuItem(u8g2,x,y-_position,_rowHeight)+_position-y;windowHeight=h;// 第五步:恢复裁剪u8g2_SetMaxClipWindow(u8g2);// 第六步:绘制垂直滑块if(totalLength>h){u8g2_DrawVSliderBar(u8g2,x+w-5,y,5,h,(float)_position/(totalLength-h));}}voidoled_display(u8g2_t*u8g2){oled_display_menu(u8g2,0,0,128,32,menuItem);}

3. 逐段拆解:每一行在做什么

3.1 菜单内容回调——“行模型”

u8g2_uint_tmenuItem(u8g2_t*u8g2,u8g2_uint_tx,u8g2_uint_ty,u8g2_uint_trowHeight){sprintf(outBuf,"c:%d",count);u8g2_DrawStr(u8g2,x,y+=rowHeight,outBuf);returny;}

设计思路:把菜单的每一行抽象为"按给定 Y 坐标和行高绘制"。回调函数不需要知道滚动位置,只需要在传入的y坐标上逐行绘制,然后返回最后的 Y。totalLength由这个返回值反算。

优点

  • 简单直观,一个函数指针搞定
  • 调用者完全控制绑定的上下文变量(counttimer等)

缺点

  • 返回 Y 坐标的方式过于原始——如果回调里要绘制不同高度的菜单项,调用者得自己算每行间距
  • sprintf每次都要手动拼字符串,类型不安全

这个"行模型"后来被重构为menuItem_cbvoid返回 +u8g2_MenuDrawItemStart/End的包围模式。

3.2 平滑滚动动画——追击算法

if(ABS(position-_position)>spe){if(position>_position)_position+=spe;if(position<_position)_position-=spe;}else{_position=position;}

知识点:这是一个最简单的"线性追击"算法。position是目标位置,_position是当前实际显示位置。每次调用时_positionposition逼近spe个单位。

时间轴: t0 t1 t2 t3 t4 目标: 100 100 100 100 100 实际: 0 3 6 9 12 ... 最终追到 100

优点

  • 计算量极小(三次比较 + 一次加减)
  • 效果自然——加速启动、减速停止

缺点

  • 追到目标后就"粘住"了,没有弹性或回弹(但这对于菜单来说反而是优点)
  • spe是固定步长,长距离滚动时速度恒定,不够平滑

演化:最终库中这个逻辑被封装进u8g2_menu_effect_trun回调,支持替换。

3.3 行高动画——手写的展开/收起

if(_rowHeight<maxCharHeight)_rowHeight+=3;// 展开if(_rowHeight>maxCharHeight)_rowHeight-=1;// 收起(更慢)

这里+3-1的不对称设计是有意的:菜单展开要快(用户想看到内容),收起稍慢(留一点视觉残留)。

缺点+3-1是魔法数字,不可配置,不可替换。这是原型最需要重构的部分之一。

3.4 垂直滑块条——位置映射

u8g2_DrawVSliderBar(u8g2,x+w-5,y,5,h,(float)_position/(totalLength-h));

滑块位置 = 当前滚动位置 / 可滚动总范围。这是一个归一化到 [0, 1] 的简单映射,最终库中保留了这个核心公式。


4. 原型暴露的核心问题清单

带着这个原型跑了几天后,以下问题开始变得无法忍受:

#问题症状根因
1单实例不能同时有两个菜单static全局变量
2类型混乱变量修改逻辑散落在回调中没有统一的变量绑定接口
3魔法数字+3/-1/spe=3动画硬编码
4无导航子菜单靠全局变量手动管理没有调用链追溯
5按键耦合pageUp/pageDown裸函数没有按键抽象层
6字符串拼装sprintf(outBuf, ...)没有格式化输出封装
7选择器缺失选中的菜单项无视觉反馈没有选择器概念
8无法编辑菜单项只能看不能改没有编辑状态管理

这 8 个问题,就是接下来 6 篇文章要逐个解决的。


5. 为什么原型仍然重要?

原型虽然简陋,但它完成了一件最关键的事:验证了整个模型可行

  • ✅ 裁剪窗口 + 回调模型 → 菜单可以滚动
  • ✅ 追击算法 → 动画可以平滑
  • ✅ 滑块映射 → 滚动位置可视化

验证了这三个核心理念之后,后续所有的重构——结构体化、模块化、事件化——都是在稳固的地基上盖楼。

教训:先写一段能跑的原型代码验证核心假设,再考虑架构和抽象。过早优化是万恶之源,但从不优化是慢性死亡。

在下一篇中,我们将把这堆全局变量和静态变量搬进一个结构体,把单文件拆成多文件,建立菜单库的正式架构。


下一篇:从零构建嵌入式菜单库(二):架构设计——从函数到结构体,从单文件到模块

http://www.jsqmd.com/news/831304/

相关文章:

  • 香橙派Zero全解析:从硬件到应用,打造你的微型Linux服务器
  • 智充兽AI车载快充:车载共享充电模式解析与行业应用研究
  • GitHub合规自动化:法律条款代码化与开源许可证检查实践
  • 到底如何?大跨度“玻璃肋”幕墙,安全吗?
  • 微信数据管理遇难题?本地化方案PyWxDump的合规启示与技术探索
  • 新能源电网电磁暂态仿真方法【附仿真】
  • NVIDIA Profile Inspector终极指南:解锁显卡隐藏设置,游戏性能提升30%!
  • 避开UDS诊断的‘坑’:一次请求多个DID时,为什么ECU的响应和你预期的不一样?
  • 全志T113-i国产核心板开发指南:从硬件选型到软件部署
  • taotoken助力初创团队低成本管理多个ai模型api调用
  • 如何快速构建智能语音交互系统:小智ESP32后端服务实战指南
  • 告别‘夜盲症’:手把手教你用DIAL-Filters提升夜间自动驾驶图像分割精度(附PyTorch代码)
  • 腾讯云秒杀活动是什么?2026年最新参与指南(附抢购技巧)
  • Node.js后端服务快速集成Taotoken,为应用注入大模型能力
  • 别再死记硬背了!用‘上下文无关文法’像搭乐高一样理解编程语言语法
  • 基于555与4013的锁存看门狗设计:嵌入式系统高可靠性的硬件守护方案
  • FSearch终极指南:如何在Linux上实现秒级文件搜索
  • 从公式到代码:用vcftools实战解析Fst群体遗传分化
  • 别再只装单机版了!在Windows上快速搭建Zookeeper伪集群(3节点)实战教程
  • 【ElevenLabs俄文语音合成实战指南】:20年AI语音工程师亲授7大避坑要点与本地化调优秘技
  • Fan Control:免费专业级Windows风扇控制软件终极指南
  • Agent 当裁判光看 Trajectory 不够,它得自己去环境里查证 —— AJ-Bench 论文解读
  • 自学 Vibe Coding 这三个网站就够了!
  • Arduino UNO硬件解析与开发环境搭建:从零开始嵌入式开发
  • Altium Designer20 从零到一:新手必备的安装与核心功能上手指南
  • Spring Boot 多线程场景下 i18n 国际化失效问题排查与解决
  • 浏览器扩展实现AI提示词高效管理:从模板变量到工作流优化
  • 探索Mod Assistant:Beat Saber模组管理工具的高效解决方案
  • day-02
  • Translumo终极指南:打破语言障碍的实时屏幕翻译神器