news 2026/4/15 12:04:49

u8g2绘制圆弧与多边形的从零实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
u8g2绘制圆弧与多边形的从零实现

用数学“画”出图形:在 u8g2 上从零实现圆弧与多边形

你有没有遇到过这样的场景?手头是一块128×64的OLED屏,主控是STM32或ESP32,UI需要一个进度弧、仪表盘刻度,甚至是一个三角箭头按钮——但翻遍了u8g2的API文档,却发现它只支持画点、线、矩形和整圆?

没错,u8g2 不原生支持圆弧和多边形。这对于追求简洁高效的嵌入式开发来说既是限制,也是机会:我们完全可以自己动手,用几行数学公式补上这块拼图。

本文不讲套话,不堆概念,带你从第一性原理出发,亲手实现drawArcdrawPolygon,并深入理解背后的坐标变换、性能权衡与工程实践技巧。无论你是正在做智能仪表、工业HMI,还是想给自己的小项目加点“设计感”,这篇都能直接用上。


为什么 u8g2 没有 drawArc?先看它的定位

u8g2 是 Oliver Kraus 开发的一款专为单色显示屏优化的图形库,目标非常明确:极简、高效、跨平台

它支持 SSD1306、SH1106、UC1701 等上百种控制器,能在 AVR、ARM Cortex-M、ESP32 等资源极其有限的MCU上运行。为了控制内存占用(尤其是帧缓冲),很多“高级”功能都被有意舍弃了。

比如:
- 没有抗锯齿
- 没有灰阶(仅黑白)
- 没有矢量路径
- 更没有drawArc()fillPolygon()

但这并不意味着我们只能画方框和实心圆。相反,这正是嵌入式图形开发的魅力所在:用最少的资源,靠算法补足表现力。


圆弧怎么“画”?拆解成线段就对了

核心思路:化曲为直

你想画一段圆弧,但库只让你画直线。怎么办?

答案很朴素:把圆弧切成一小段一小段的直线,连起来就是弧

这其实就是计算机图形学中最基础的思想——线段逼近(Line Approximation)。只要切得足够细,人眼就看不出它是“折”的。

数学基础:极坐标转屏幕坐标

我们知道,圆上的任意一点可以用角度 $\theta$ 表示:

$$
x = x_c + r \cdot \cos(\theta) \
y = y_c + r \cdot \sin(\theta)
$$

但注意!这是标准数学坐标系(Y轴向上)。而屏幕坐标系是左上角为原点,Y轴向下为正。所以实际代码中要对 Y 做符号反转:

float x = xc + r * cosf(rad); float y = yc - r * sinf(rad); // 注意这里是减号

这个小小的“负号”,决定了你的弧是不是画反了。

实现一个可靠的 drawArc 函数

下面是你可以直接复制粘贴到项目的版本,已处理边界情况和方向逻辑:

#include "u8g2.h" #include <math.h> void drawArc(u8g2_t *u8g2, uint8_t xc, uint8_t yc, uint8_t r, int16_t start_deg, int16_t end_deg, uint8_t direction) { // 规范化角度到 [0, 360) auto norm_angle = [](int16_t deg) -> int16_t { while (deg >= 360) deg -= 360; while (deg < 0) deg += 360; return deg; }; start_deg = norm_angle(start_deg); end_deg = norm_angle(end_deg); int16_t step = (direction == 1) ? 1 : -1; // 1: 逆时针, 0: 顺时针 // 如果顺时针且起始 > 结束,或逆时针且起始 < 结束,则需跨越0° if ((direction == 0 && start_deg > end_deg) || (direction == 1 && start_deg < end_deg)) { step = -step; } float prev_x = xc + r * cosf(start_deg * M_PI / 180.0f); float prev_y = yc - r * sinf(start_deg * M_PI / 180.0f); for (int16_t deg = start_deg; ; deg += step) { float rad = deg * M_PI / 180.0f; float x = xc + r * cosf(rad); float y = yc - r * sinf(rad); u8g2_DrawLine(u8g2, (uint8_t)prev_x, (uint8_t)prev_y, (uint8_t)x, (uint8_t)y); prev_x = x; prev_y = y; // 终止条件 if (step > 0 ? (deg >= end_deg) : (deg <= end_deg)) break; if (abs(deg - start_deg) % 360 == 0) break; // 防止绕圈死循环 } }

使用示例:画一个从30°到150°的半圆(模拟电压表盘)

drawArc(&u8g2, 64, 32, 30, 30, 150, 1); // 逆时针

性能与精度的平衡

  • 步长设为1°:在128x64屏幕上足够平滑,每段弧最多360次循环,可接受。
  • 浮点运算代价高?如果你的MCU没有FPU(如STM32F1),建议改用查表法
// 预生成 cos/sin 表(0~359°) static const int16_t cos_table[360] = { /* ... */ }; static const int16_t sin_table[360] = { /* ... */ };

这样就能完全避免运行时调用sinf/cosf,速度提升显著。


多边形绘制:闭合的线段序列

如果说圆弧是“曲线”的代表,那么多边形就是“形状”的基石。三角形、五角星、自定义图标……都可以通过顶点连接实现。

最简单的实现:逐边绘制

核心逻辑只有三步:
1. 遍历顶点数组;
2. 用DrawLine连接相邻两点;
3. 最后一条边闭合回起点。

void drawPolygon(u8g2_t *u8g2, const uint8_t (*points)[2], uint8_t num_points) { if (num_points < 2) return; for (uint8_t i = 0; i < num_points - 1; i++) { u8g2_DrawLine(u8g2, points[i][0], points[i][1], points[i+1][0], points[i+1][1]); } // 闭合 u8g2_DrawLine(u8g2, points[num_points-1][0], points[num_points-1][1], points[0][0], points[0][1]); }

💡 提示:参数类型const uint8_t (*points)[2]是指向二维数组的指针,比int*更安全清晰。

动态生成正多边形:不只是三角形

我们可以封装一个通用函数,根据中心、半径、边数生成任意正多边形:

void drawRegularPolygon(u8g2_t *u8g2, uint8_t cx, uint8_t cy, uint8_t radius, uint8_t sides, int16_t rotation_deg) { if (sides < 3) return; uint8_t points[sides][2]; float angle_step = 2.0f * M_PI / sides; float rot_rad = rotation_deg * M_PI / 180.0f; for (int i = 0; i < sides; i++) { float angle = i * angle_step + rot_rad; points[i][0] = cx + radius * cosf(angle); points[i][1] = cy - radius * sinf(angle); // Y轴反转 } drawPolygon(u8g2, points, sides); }

✅ 使用示例:画一个朝上的等边三角形

drawRegularPolygon(&u8g2, 64, 32, 15, 3, 90); // 旋转90°使其朝上

工程实战中的那些“坑”

别以为写完函数就万事大吉。在真实项目中,以下几点才是决定成败的关键。

🛑 1. 浮点运算拖慢帧率?

在无FPU的MCU上,每次sinf/cosf可能耗时数百微秒。解决方案:
-静态图形缓存顶点坐标,只计算一次;
-使用定点数或查表法,例如将角度映射为 0~255 整数范围,配合预计算表;
-降低更新频率,非动画部分不必每帧重绘。

🛑 2. 图形超出屏幕怎么办?

u8g2 的DrawLine内部会做裁剪,但频繁越界仍可能影响性能。建议在调用前加入边界检查:

#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))

或者更进一步,使用 Cohen-Sutherland 裁剪算法处理长线段。

🛑 3. 如何实现“填充”多边形?

轮廓容易,填充难。对于凸多边形,可用扫描线法;凹多边形则推荐“奇偶规则”判断像素是否在内部。

简化版思路:
- 遍历每一行Y;
- 找出该行与多边形边界的交点;
- 按X排序,两两配对填充区间。

虽然u8g2只有黑白模式,但填充依然有意义——比如实现实心底纹按钮或扇区图。


实际应用场景:做个迷你仪表盘

结合上面两个函数,我们可以快速构建一个动态电压指示器:

void drawVoltMeter(u8g2_t *u8g2, float voltage) { const uint8_t cx = 64, cy = 32, r = 30; // 1. 画外框弧(30° ~ 150°) drawArc(u8g2, cx, cy, r, 30, 150, 1); // 2. 画刻度 for (int deg = 30; deg <= 150; deg += 10) { float rad = deg * M_PI / 180.0f; uint8_t x1 = cx + (r - 3) * cosf(rad); uint8_t y1 = cy - (r - 3) * sinf(rad); uint8_t x2 = cx + r * cosf(rad); uint8_t y2 = cy - r * sinf(rad); u8g2_DrawLine(u8g2, x1, y1, x2, y2); } // 3. 计算指针角度(0V→30°, 5V→150°) int16_t ptr_deg = 30 + (int16_t)((voltage / 5.0f) * 120.0f); float ptr_rad = ptr_deg * M_PI / 180.0f; uint8_t px = cx + (r - 5) * cosf(ptr_rad); uint8_t py = cy - (r - 5) * sinf(ptr_rad); u8g2_DrawLine(u8g2, cx, cy, px, py); // 指针 }

整个界面无需任何图片资源,全部由代码实时生成,Flash占用近乎为零,且支持动态缩放与主题切换。


把轮子变成工具箱:建议这样做封装

与其每次重复造轮子,不如建立一个轻量级扩展模块:

u8g2_ext/ ├── u8g2_ext.h ├── u8g2_ext.c └── shapes/ ├── arc.c ├── polygon.c └── pie_chart.c

在头文件中提供高级接口:

// u8g2_ext.h void u8g2_drawArc(u8g2_t *u8g2, ...); void u8g2_drawTriangle(u8g2_t *u8g2, ...); void u8g2_drawClockFace(u8g2_t *u8g2, ...); void u8g2_drawProgressBarArc(u8g2_t *u8g2, uint8_t pcnt); // 圆形进度条

久而久之,你就拥有了一套专属于团队的嵌入式GUI组件库,既节省开发时间,又保证风格统一。


写在最后:为什么我们要“从零实现”?

有人可能会问:为什么不直接用LVGL?毕竟它功能强大,支持复杂控件。

答案是:不是每个项目都需要LVGL

LVGL 至少需要几KB RAM 和 定期刷新机制,在一些超低功耗待机设备中根本不适用。而 u8g2 + 自定义绘图,可以在<1KB RAM下完成漂亮的静态+动态UI。

更重要的是,当你亲手写出第一个drawArc,你会真正理解:
- 图形是怎么从数学变成像素的;
- 为什么有些边缘看起来“锯齿”;
- 如何在资源与效果之间做取舍。

这种能力,远比调用一个API深刻得多。


如果你也在用 u8g2 做产品开发,欢迎试试这些函数。它们小,但够用;简单,但可扩展。下次当你面对一块黑白屏发愁时,记住:只要有坐标和线条,你就能“画”出整个世界

🔧获取完整代码: GitHub Gist - u8g2-arc-polygon
📣 欢迎留言分享你的应用场景:你是用来做旋钮菜单?健康手环?还是工业报警灯?

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

Beyond Compare 5专业激活技术全解析:从基础配置到深度定制

Beyond Compare 5专业激活技术全解析&#xff1a;从基础配置到深度定制 【免费下载链接】BCompare_Keygen Keygen for BCompare 5 项目地址: https://gitcode.com/gh_mirrors/bc/BCompare_Keygen Beyond Compare 5作为业界公认的文件对比工具标杆&#xff0c;其永久授权…

作者头像 李华
网站建设 2026/4/4 3:01:54

深岩银河存档编辑器完全使用指南

深岩银河存档编辑器完全使用指南 【免费下载链接】DRG-Save-Editor Rock and stone! 项目地址: https://gitcode.com/gh_mirrors/dr/DRG-Save-Editor 深岩银河作为一款深受玩家喜爱的合作射击游戏&#xff0c;其丰富的角色成长和资源收集系统是游戏乐趣的重要组成部分。…

作者头像 李华
网站建设 2026/4/12 2:57:23

WaveTools鸣潮工具箱:3大黑科技功能深度解析与实战应用

WaveTools鸣潮工具箱&#xff1a;3大黑科技功能深度解析与实战应用 【免费下载链接】WaveTools &#x1f9f0;鸣潮工具箱 项目地址: https://gitcode.com/gh_mirrors/wa/WaveTools 还在为《鸣潮》游戏体验不佳而苦恼&#xff1f;画面卡顿、账号切换繁琐、抽卡记录混乱……

作者头像 李华
网站建设 2026/4/12 9:47:29

24B多模态AI模型Magistral-Small-1.2强力发布

24B多模态AI模型Magistral-Small-1.2强力发布 【免费下载链接】Magistral-Small-2509-FP8-Dynamic 项目地址: https://ai.gitcode.com/hf_mirrors/unsloth/Magistral-Small-2509-FP8-Dynamic 导语&#xff1a;Mistral AI推出24B参数的多模态大模型Magistral-Small-1.2&…

作者头像 李华
网站建设 2026/4/13 2:54:31

无源蜂鸣器驱动原理:STM32平台全面讲解

如何用STM32精准驱动无源蜂鸣器&#xff1f;一文讲透硬件设计与PWM控制实战在你调试一个智能门锁的固件时&#xff0c;按下按键却只听见“滴”的一声单调提示——用户根本分不清是验证成功、密码错误还是低电量警告。这时候你会意识到&#xff1a;固定音调的声音反馈已经无法满…

作者头像 李华
网站建设 2026/4/11 3:29:59

嵌入式开发第一步:Keil安装与环境搭建实战案例

从零开始搭建Keil开发环境&#xff1a;STM32嵌入式入门实战全记录 你是不是也曾在第一次打开Keil时&#xff0c;面对“找不到设备”“编译失败”“下载超时”这些报错一头雾水&#xff1f; 别担心——每一个嵌入式工程师都曾经历过这个阶段。真正的起点不是写第一行代码&…

作者头像 李华