news 2026/5/16 23:55:21

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零构建嵌入式菜单库(一):原型探索——从一段单函数代码开始

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

系列定位:这是一套编写教程——我们将一起从零构建一个基于 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. 为什么原型仍然重要?

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

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

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

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

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


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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/16 23:53:12

扩展卡尔曼滤波锂电池SOC估算【附代码】

✨ 长期致力于锂离子电池、SOC估算、锂离子电池建模、EKF算法研究工作&#xff0c;擅长数据搜集与处理、建模仿真、程序编写、仿真设计。 ✅ 专业定制毕设、代码 ✅ 如需沟通交流&#xff0c;点击《获取方式》 &#xff08;1&#xff09;二阶RC等效电路建模与温度自适应参数修正…

作者头像 李华
网站建设 2026/5/16 23:51:06

浏览器扩展实现AI提示词高效管理:从模板变量到工作流优化

1. 项目概述与核心价值最近在折腾AI工具链&#xff0c;发现一个痛点&#xff1a;每次和ChatGPT、Claude或者本地部署的大模型对话时&#xff0c;那些精心调试好的提示词&#xff08;Prompt&#xff09;总是散落在各个聊天窗口里&#xff0c;要么就是得手动复制粘贴&#xff0c;…

作者头像 李华
网站建设 2026/5/16 23:50:08

RKNPU2嵌入式AI部署实战:从模型转换到板端优化的完整指南

1. 项目概述&#xff1a;为什么RKNPU2值得你投入时间学习&#xff1f;如果你正在嵌入式AI的边缘计算领域摸索&#xff0c;或者手头恰好有瑞芯微RK3568、RK3588这类带NPU的开发板&#xff0c;却感觉无从下手&#xff0c;那你来对地方了。我最近花了不少时间&#xff0c;系统地啃…

作者头像 李华
网站建设 2026/5/16 23:48:02

前端架构师面试真题+完整答案解析(2026 超全版,跳槽必备)

前端架构师面试真题完整答案解析&#xff08;2026 超全版&#xff0c;跳槽必备&#xff09; 文章目录前端架构师面试真题完整答案解析&#xff08;2026 超全版&#xff0c;跳槽必备&#xff09;一、JavaScript 核心原理&#xff08;架构师基础必考&#xff09;1. 讲讲原型链、继…

作者头像 李华
网站建设 2026/5/16 23:43:22

告别代码!用Orange 3可视化数据挖掘,5分钟搞定鸢尾花分类分析

零代码数据挖掘实战&#xff1a;用Orange 3快速解锁鸢尾花分类的奥秘 在数据科学领域&#xff0c;传统的数据挖掘往往需要编写复杂的Python或R代码&#xff0c;这对于非技术背景的从业者来说是一道难以逾越的门槛。Orange 3的出现彻底改变了这一局面——这款开源的可视化数据挖…

作者头像 李华