2026/6/20 13:28:28
网站建设
项目流程
手机端网站开发框架,html网页设计作业成品代码免费下载,做外国的网站卖东西,药物研发网站怎么做用数学“画”出图形#xff1a;在 u8g2 上从零实现圆弧与多边形 你有没有遇到过这样的场景#xff1f;手头是一块12864的OLED屏#xff0c;主控是STM32或ESP32#xff0c;UI需要一个进度弧、仪表盘刻度#xff0c;甚至是一个三角箭头按钮——但翻遍了u8g2的API文档#x…用数学“画”出图形在 u8g2 上从零实现圆弧与多边形你有没有遇到过这样的场景手头是一块128×64的OLED屏主控是STM32或ESP32UI需要一个进度弧、仪表盘刻度甚至是一个三角箭头按钮——但翻遍了u8g2的API文档却发现它只支持画点、线、矩形和整圆没错u8g2 不原生支持圆弧和多边形。这对于追求简洁高效的嵌入式开发来说既是限制也是机会我们完全可以自己动手用几行数学公式补上这块拼图。本文不讲套话不堆概念带你从第一性原理出发亲手实现drawArc和drawPolygon并深入理解背后的坐标变换、性能权衡与工程实践技巧。无论你是正在做智能仪表、工业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[i1][0], points[i1][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 欢迎留言分享你的应用场景你是用来做旋钮菜单健康手环还是工业报警灯