news 2026/6/10 22:13:41

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/6/10 13:00:37

Keil安装后无法识别STM32?一文说清解决方法

Keil装好了却找不到STM32芯片&#xff1f;别急&#xff0c;90%的人都忽略了这一步 你是不是也遇到过这种情况&#xff1a; 刚装好Keil MDK&#xff0c;信心满满地打开软件准备新建工程&#xff0c;结果在“Select Device”窗口里输入“STM32F407”&#xff0c;回车——一片空…

作者头像 李华
网站建设 2026/6/10 13:00:59

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/6/10 13:49:14

驾校考试辅助:驾驶动作合规性AI评判系统

驾校考试辅助&#xff1a;驾驶动作合规性AI评判系统 在智能交通与人工智能加速融合的今天&#xff0c;一个看似传统、甚至有些“老旧”的场景——驾校考试&#xff0c;正悄然经历一场技术革命。过去几十年里&#xff0c;学员是否合格&#xff0c;全靠考官一句“打灯了没&#x…

作者头像 李华
网站建设 2026/6/9 23:46:13

网络安全威胁检测:异常行为识别模型推理提速

网络安全威胁检测&#xff1a;异常行为识别模型推理提速 在金融交易监控中心的某次深夜值守中&#xff0c;系统突然接收到每秒超过 5 万条网络流日志。此时&#xff0c;一个基于深度学习的异常行为识别模型正试图从这些数据中捕捉潜在的横向移动攻击痕迹。然而&#xff0c;原生…

作者头像 李华
网站建设 2026/6/10 14:51:24

手把手配置LCD1602初始化命令流(含代码注释)

手把手教你搞定LCD1602初始化&#xff1a;从握手到显示的完整流程&#xff08;含实战代码&#xff09;你有没有遇到过这样的情况&#xff1f;硬件接好了&#xff0c;程序也烧录进去了&#xff0c;结果LCD1602屏幕一片漆黑&#xff0c;或者满屏“方块”乱码&#xff1f;别急——…

作者头像 李华
网站建设 2026/6/10 13:29:57

Windows 11 LTSC微软商店缺失问题深度解决方案评测

Windows 11 LTSC微软商店缺失问题深度解决方案评测 【免费下载链接】LTSC-Add-MicrosoftStore Add Windows Store to Windows 11 24H2 LTSC 项目地址: https://gitcode.com/gh_mirrors/ltscad/LTSC-Add-MicrosoftStore 问题诊断&#xff1a;LTSC版为何"阉割"了…

作者头像 李华