嵌入式GUI开发实战:emWin数值显示与2D图形绘制详解
1. 嵌入式GUI开发中的数值与图形从原理到实战在嵌入式设备上实现一个清晰、美观的用户界面是每个嵌入式开发者从功能实现迈向产品化过程中必须跨越的一道坎。无论是工业HMI上跳动的温度曲线还是智能手表上精准的步数统计其背后都离不开两个核心基础数值的精确显示与图形的流畅绘制。很多开发者初次接触GUI时往往觉得这是一片布满“黑魔法”的领域各种API函数令人眼花缭乱。但当你拆解开来会发现其核心逻辑非常直接在有限的像素矩阵上通过计算和填充将数据数值和意图图形视觉化。emWin正是为此而生的利器。它不是简单的绘图工具集而是一套经过深度优化、充分考虑嵌入式资源限制的图形中间件。它的价值在于将底层LCD驱动、内存管理、图形算法等复杂细节封装起来为开发者提供了一套统一、高效的API。今天我们不谈空洞的概念直接切入最常用也最核心的部分如何用emWin显示各种格式的数值以及如何运用其2D图形库构建界面骨架。我会结合手册中的函数分享在实际项目中如何选择、使用它们并避开那些我踩过的坑。2. 数值显示不仅仅是调用一个函数在嵌入式界面上显示一个数字远不是printf那么简单。你需要考虑字体、位置、对齐方式、格式整数、浮点、十六进制更重要的是在资源受限的MCU上如何高效、无错地完成这些操作。emWin提供了一系列GUI_Disp*函数它们就是为这个场景量身定做的。2.1 浮点数显示的四种策略与选型逻辑手册里提到了GUI_DispFloat,GUI_DispFloatFix,GUI_DispSFloatFix,GUI_DispFloatMin,GUI_DispSFloatMin。光看名字容易迷糊其实它们的区别核心在于格式控制粒度和是否强制显示符号。选择哪一个取决于你的显示需求。2.1.1 GUI_DispFloat紧凑显示自动抑制前导零这是最“自由”的显示方式。你只需要告诉它数值v和显示的总字符数Len含小数点。它的特点是会自动抑制整数部分的前导零。比如显示123.456Len设为9它会显示为123.456前面没有空格。如果数值是负数它会自动加上负号。float f 123.456; GUI_DispFloat(f, 9); // 显示 “123.456” GUI_DispFloat(-f, 9); // 显示 “-123.456”实操心得Len参数指的是预留的字符位置总数而不是小数点后的位数。如果你设的Len小于实际数字包括小数点、负号所需的宽度显示可能会被截断或出现异常。一个安全的做法是预估你的数值最大可能位数包括负号和小数点并以此作为Len值。例如显示范围在-999.99到999.99之间的数最大需要-999.99共7个字符那么Len至少设为7。2.1.2 GUI_DispFloatFix表格对齐的利器当你的界面上有一列数据需要上下对齐时比如一个参数列表GUI_DispFloatFix就是最佳选择。它需要三个参数数值v、总字符数Len、以及小数点后的位数Decs。关键区别在于它不会抑制前导零而是用空格或零填充到指定长度。float values[] {1.5, 22.34, 100.0}; GUI_SetFont(GUI_Font8x16); for(int i0; i3; i) { GUI_GotoY(i*20); // 每行间隔20像素 GUI_DispFloatFix(values[i], 8, 2); } // 显示效果 // “ 1.50” // “ 22.34” // “ 100.00”可以看到所有数字的小数点都对齐在同一列非常整洁。Len参数在这里确保了每行文本的宽度一致是制作规整表格界面的核心函数。2.1.3 GUI_DispSFloatFix强制显示符号位这个函数是GUI_DispFloatFix的“带符号”版本。无论数值正负它都会在数字前显示一个符号正数为负数为-。这在需要明确显示数值增减方向的场景非常有用比如显示温度变化“2.5°C”或电压偏差“-0.1V”。float delta 0.5; GUI_DispSFloatFix(delta, 6, 2); // 显示 “ 0.50”2.1.4 GUI_DispFloatMin / GUI_DispSFloatMin智能最小长度显示这两个函数是“懒人”福音。你不需要指定总长度Len只需要告诉它最少要显示几位小数Fract。函数会自动计算需要的最小显示宽度。GUI_DispFloatMin自动抑制前导零GUI_DispSFloatMin则始终显示符号。float a 123.4; float b 7.89; GUI_DispFloatMin(a, 2); // 显示 “123.40” GUI_DispFloatMin(b, 2); // 显示 “7.89” // 注意两个数字的宽度不同不适合对齐显示。避坑指南手册里明确提到如果需要对齐显示不要用*Min系列函数而应该使用*Fix系列。因为*Min函数追求的是紧凑宽度不固定。我曾在一个需要刷新多行实时数据的界面上误用了GUI_DispFloatMin导致每次刷新时因为数字宽度变化后面的文本会“抖动”体验非常差。改用GUI_DispFloatFix并固定Len后问题立刻解决。2.2 二进制与十六进制显示调试与状态监控的利器除了十进制嵌入式开发中查看寄存器、数据包原始内容时二进制和十六进制显示不可或缺。2.2.1 GUI_DispBin直观的位状态查看GUI_DispBin(U32 v, U8 Len)会将一个32位无符号整数v以二进制形式显示出来显示Len个位从高位开始不足补零。这在调试GPIO输入状态、通信协议位域时极其直观。U32 gpio_state 0b00010110; // 读取到的GPIO状态 GUI_DispBin(gpio_state, 8); // 显示 “00010110”你可以一眼看出哪一位是0哪一位是1。对应的GUI_DispBinAt函数则可以指定显示的绝对坐标。2.2.2 GUI_DispHex更紧凑的原始数据查看二进制显示虽然直观但太长。十六进制是更常用的底层数据查看格式。GUI_DispHex(U32 v, U8 Len)显示十六进制Len表示显示的十六进制数字位数。U32 adc_value 0x0FFF; // ADC采样值 GUI_DispHex(adc_value, 4); // 显示 “0FFF”经验技巧在显示诸如传感器原始值、CAN报文ID和数据场时我习惯将GUI_DispHex与GUI_DispString结合使用形成“ID: 0x18FF50D4 Data: 0x00123456”这样的格式调试信息一目了然。注意GUI_DispHex显示的是无0x前缀的纯十六进制数字如果需要前缀需要自己用字符串函数拼接。2.3 字体与文本模式数值显示的美学基础所有GUI_Disp*函数都依赖于当前设置的字体和文本模式。这是数值显示美观与否的关键。字体设置 (GUI_SetFont): 在显示数值前务必设置好字体。对于小型屏GUI_Font8x8或GUI_Font8x16是常用选择。对于需要突出显示的关键数值可以使用GUI_Font24B_ASCII等大号字体。文本模式 (GUI_SetTextMode): 最常用的是GUI_TM_NORMAL覆盖模式和GUI_TM_TRANS透明模式。GUI_TM_TRANS只绘制字体的像素不绘制背景这在有复杂背景的界面上非常有用。但要注意透明模式下的绘制速度可能略慢于覆盖模式。颜色设置 (GUI_SetColor,GUI_SetBkColor): 分别设置前景色文字颜色和背景色。在调用GUI_Clear()或GUI_ClearRect()后背景色生效。一个完整的数值显示初始化流程通常如下GUI_SetFont(GUI_Font8x16); // 设置字体 GUI_SetTextMode(GUI_TM_NORMAL); // 设置文本模式覆盖 GUI_SetColor(GUI_WHITE); // 设置文字为白色 GUI_SetBkColor(GUI_BLUE); // 设置背景为蓝色 GUI_Clear(); // 用背景色清屏 // 现在可以开始显示数值了 GUI_DispFloatFix(3.14159, 10, 5);3. 2D图形库构建界面的画笔与颜料如果说数值显示是界面的“信息”那么2D图形就是界面的“骨架”和“皮肤”。emWin的2D图形库非常全面从画一个点到一个复杂的多边形渐变填充应有尽有。理解其分类和底层原理能让你用起来得心应手。3.1 绘图上下文与基础概念在开始画图之前需要理解几个核心状态它们构成了“绘图上下文”当前窗口与裁剪区域 (GUI_GetClientRect,GUI_SetClipRect): 所有绘图操作都发生在“当前窗口”内。如果不使用窗口管理器(WM)整个LCD就是窗口。GUI_SetClipRect可以设置一个裁剪矩形所有绘图超出此区域的部分将被自动裁掉。这在做局部刷新、防止绘制到其他UI区域时非常有用。绘图模式 (GUI_SetDrawMode): 主要是GUI_DM_NORMAL正常覆盖和GUI_DM_XOR异或模式。异或模式可以实现“擦除”效果画两次同一个图形它会消失恢复原背景。但手册警告在颜色数多于2种或画笔大小大于1时XOR模式可能行为异常需谨慎使用。画笔大小 (GUI_SetPenSize): 影响GUI_DrawLine,GUI_DrawPoint等矢量绘图函数的线条粗细。默认是1像素。增大画笔大小可以画更粗的线但不能与线型如虚线同时使用。3.2 基本图形绘制从矩形到圆角这是最常用的一组函数用于绘制界面的基础框、栏、区域。3.2.1 矩形操作框架、填充与清空GUI_DrawRect: 画一个空心矩形框。参数是左上角(x0,y0)和右下角(x1,y1)坐标。GUI_FillRect: 用当前前景色填充一个实心矩形。这是构建进度条、色块、背景区域的主力函数。GUI_ClearRect: 用当前背景色填充一个矩形。本质上是GUI_SetColor为背景色后调用GUI_FillRect的快捷方式常用于局部擦除。GUI_InvertRect: 反转矩形区域内所有像素的颜色。在黑白屏上可以实现“高亮选中”效果效率很高。GUI_CopyRect: 将屏幕上一块矩形区域复制到另一个位置。可以实现简单的动画或窗口拖动效果。手册特别指出源区域和目标区域可以重叠内部会处理好复制顺序。3.2.2 圆角矩形与渐变提升视觉质感现代UI很少使用生硬的直角圆角矩形能显著提升亲和力。GUI_DrawRoundedRect/GUI_FillRoundedRect: 绘制圆角矩形框或填充圆角矩形。需要指定圆角半径r。GUI_DrawGradientH/GUI_DrawGradientV: 绘制水平或垂直渐变填充的矩形。需要指定起始和结束颜色。这是制作漂亮按钮、背景的利器。GUI_DrawGradientRoundedH/GUI_DrawGradientRoundedV: 上述两者的结合绘制渐变填充的圆角矩形。参数计算心得圆角半径r的设置很有讲究。设矩形宽度为W高度为H那么r的最大有效值通常是min(W, H)/2。超过这个值圆角会绘制异常。我通常取W/4到W/3之间视觉效果比较平衡。对于渐变颜色选择建议使用RGB宏如GUI_RED或直接16进制颜色值如0xFF0000。从深色渐变到浅色能营造凸起感反之则有凹陷感。3.3 高级绘图多边形、圆、椭圆与曲线对于更复杂的自定义形状emWin提供了多边形和曲线支持。3.3.1 多边形绘制多边形通过一系列顶点坐标来定义。GUI_DrawPolygon: 按顺序连接各个顶点绘制多边形轮廓。GUI_FillPolygon: 填充多边形内部。emWin使用高效的扫描线填充算法。GUI_EnlargePolygon,GUI_MagnifyPolygon,GUI_RotatePolygon: 分别用于对多边形进行放大、缩放和旋转。这在需要动态变化的图形如仪表指针中非常有用。3.3.2 圆形、椭圆与弧线GUI_DrawCircle/GUI_FillCircle: 绘制圆。参数是圆心(x,y)和半径r。注意这里的圆是基于像素网格的在半径很小时可能不够“圆润”这是所有栅格化显示的通病。GUI_DrawEllipse/GUI_FillEllipse: 绘制椭圆。其算法同样经过优化效率很高。GUI_DrawArc: 绘制圆弧。手册特别提到这是唯一一个需要浮点数运算的绘图函数。如果你的芯片没有FPU且对性能极度敏感需谨慎或避免频繁调用此函数。3.4 Alpha混合实现半透明与高级叠加效果Alpha混合是实现半透明、阴影、光泽等高级视觉效果的核心。emWin的Alpha混合原理很直接每个颜色值32位的高8位24-31位被用作Alpha通道。0表示完全不透明255255表示完全透明0。3.4.1 启用与使用Alpha混合启用全局Alpha:GUI_EnableAlpha(1)。启用后后续绘图操作中颜色值的高8位将被解释为透明度。设置带Alpha的颜色: 颜色值需要通过移位操作组合。例如半透明的红色(0x80uL 24) | GUI_RED。这里0x80即128表示大约50%的透明度。绘制: 像普通绘图一样调用函数如GUI_FillRect绘制出的就是半透明色块。GUI_EnableAlpha(1); GUI_SetBkColor(GUI_WHITE); GUI_Clear(); // 绘制一个50%透明的蓝色矩形覆盖在文字上 GUI_SetColor((0x80uL 24) | GUI_BLUE); GUI_FillRect(20, 20, 100, 60);3.4.2 新旧API对比与选择手册提到了旧的GUI_SetAlpha()函数它为所有后续绘图设置一个全局的、固定的透明度。这种方式不够灵活且是软件模拟性能开销大。强烈建议使用新的GUI_EnableAlpha()配合带Alpha通道的颜色值的方式这种方式更灵活且部分支持硬件加速如果底层驱动支持。3.4.3 用户Alpha值 (GUI_SetUserAlpha)这是一个进阶功能。它设置一个额外的“用户Alpha”值会与物体自带的Alpha值进行二次混合。公式为最终Alpha 物体Alpha ((255 - 物体Alpha) * 用户Alpha) / 255。这允许你对一组已经带有透明度的物体进行整体的透明度调节。使用前后需要配合GUI_RestoreUserAlpha来保存和恢复状态避免影响其他绘图部分。3.5 位图显示集成图标与复杂图片在界面上显示Logo、图标或复杂图片离不开位图功能。emWin支持从1位到32位含Alpha通道的各种位图格式。3.5.1 基本位图显示 (GUI_DrawBitmap)这是最常用的函数将一张已加载到内存的位图显示在指定位置。位图通常由SEGGER提供的Bitmap Converter工具生成包含一个GUI_BITMAP结构体和像素数据数组。extern const GUI_BITMAP bmMyIcon; // 声明外部位图 GUI_DrawBitmap(bmMyIcon, 10, 10); // 在(10,10)位置绘制3.5.2 位图缩放与镜像 (GUI_DrawBitmapEx)这个函数功能强大可以实现位图的缩放和镜像。参数xMag和yMag是缩放因子。正数表示放大如200表示放大2倍负数表示镜像并放大如-100表示水平镜像且大小不变。xCenter和yCenter指定了位图内的“锚点”这个锚点将与屏幕坐标(x0, y0)对齐。// 将位图放大2倍显示 GUI_DrawBitmapEx(bmMyIcon, 50, 50, 0, 0, 200, 200); // 将位图水平镜像后显示 GUI_DrawBitmapEx(bmMyIcon, 100, 100, bmMyIcon.XSize/2, bmMyIcon.YSize/2, -100, 100);3.5.3 流位图 (GUI_DrawStreamedBitmap*)对于大图片一次性加载到内存可能吃不消。emWin支持流位图允许你从低速存储器如SPI Flash中边读取边解码显示极大节省RAM。这是显示全屏背景图或大尺寸图片的关键技术。你需要使用GUI_CreateBitmapFromStream系列函数创建位图对象或者直接使用GUI_DrawStreamedBitmapAuto等函数绘制。性能与内存的权衡GUI_DrawBitmap最快但消耗RAM。流位图省RAM但解码需要CPU时间显示速度慢。对于小图标几十像素见方直接用GUI_DrawBitmap。对于全屏图片务必使用流位图。我曾在一个仅有64KB RAM的STM32F103项目上通过流位图成功显示了320x240的全屏JPEG解码后的位图而内存占用几乎可以忽略不计。4. 实战整合构建一个简单的数据仪表界面了解了各个部分我们来整合一下构建一个常见的嵌入式数据仪表界面显示温度、压力并配有一个简单的柱状图。void DrawDataDashboard(float temperature, float pressure, int adc_raw) { GUI_RECT rect; static int old_bar_height 0; int bar_height, bar_width 30; // 1. 清屏并设置基础属性 GUI_SetBkColor(GUI_BLACK); GUI_Clear(); GUI_SetColor(GUI_WHITE); GUI_SetFont(GUI_Font8x16); GUI_SetTextMode(GUI_TM_TRANS); // 透明文字模式避免覆盖背景图形 // 2. 绘制标题和静态框架使用圆角矩形和渐变 GUI_SetColor(GUI_BLUE); GUI_DrawGradientRoundedH(5, 5, 235, 35, 5, GUI_BLUE, GUI_CYAN); GUI_SetColor(GUI_WHITE); GUI_SetFont(GUI_Font16B_ASCII); GUI_DispStringHCenterAt(Data Monitor, 120, 10); // 3. 显示温度数值固定格式对齐显示 GUI_SetFont(GUI_Font8x16); GUI_DispStringAt(Temperature:, 10, 50); GUI_SetFont(GUI_Font24B_ASCII); GUI_SetColor(GUI_GREEN); // 使用Fix函数确保格式对齐总宽7字符2位小数 GUI_DispFloatFix(temperature, 7, 2); GUI_DispStringAt( C, 160, 50); // 4. 显示压力数值带符号显示变化方向 GUI_SetColor(GUI_WHITE); GUI_SetFont(GUI_Font8x16); GUI_DispStringAt(Pressure:, 10, 90); GUI_SetFont(GUI_Font24B_ASCII); // 假设pressure是压力变化值需要显示正负号 GUI_DispSFloatFix(pressure, 6, 2); GUI_DispStringAt( kPa, 150, 90); // 5. 绘制ADC原始值的柱状图动态更新 // 5.1 先清除旧的柱状图部分局部刷新提高效率 GUI_SetColor(GUI_BLACK); GUI_FillRect(180, 130, 180bar_width, 180); // 5.2 计算新的柱状图高度假设ADC范围0-4095对应高度0-50像素 bar_height 130 (50 - (adc_raw * 50 / 4095)); // 5.3 绘制柱状图边框和填充 GUI_SetColor(GUI_GRAY); GUI_DrawRect(180, 130, 180bar_width, 180); GUI_SetColor(GUI_RED); GUI_FillRect(181, bar_height, 179bar_width, 179); // 内部填充留1像素边框 // 5.4 在柱状图上方显示十六进制原始值用于调试 GUI_SetColor(GUI_YELLOW); GUI_SetFont(GUI_Font6x8); GUI_DispStringAt(ADC:, 180, 125); GUI_DispHexAt(adc_raw, 185, 115, 4); // 在(185,115)显示4位十六进制数 // 6. 绘制一个模拟仪表的表盘使用圆和线 GUI_SetColor(GUI_LIGHTGRAY); GUI_DrawCircle(300, 100, 40); // 外圆 GUI_DrawCircle(300, 100, 35); // 内圆 // 简单绘制刻度这里简化实际可用循环绘制 GUI_DrawLine(300, 60, 300, 65); // 12点刻度 GUI_DrawLine(340, 100, 335, 100); // 3点刻度 // 绘制指针根据某个数据计算角度此处用固定角度示例 DrawMeterNeedle(300, 100, 35, 45); // 假设指向45度 old_bar_height bar_height; // 记录本次高度用于下次局部擦除 } // 一个简单的绘制指针函数极坐标转直角坐标 void DrawMeterNeedle(int center_x, int center_y, int length, int angle_deg) { int end_x, end_y; float rad angle_deg * 3.14159 / 180.0; end_x center_x (int)(length * sin(rad)); end_y center_y - (int)(length * cos(rad)); // 屏幕Y轴向下故用减 GUI_SetColor(GUI_RED); GUI_SetPenSize(2); // 设置指针线条粗细 GUI_DrawLine(center_x, center_y, end_x, end_y); GUI_SetPenSize(1); // 恢复默认画笔大小 }这个例子涵盖了数值显示浮点、十六进制、基本图形矩形、圆、线、渐变填充、局部刷新等关键技巧。注意其中GUI_TM_TRANS文本模式的使用它让文字背景透明不会覆盖掉漂亮的渐变标题栏。5. 常见问题、调试技巧与性能优化在实际项目中仅仅会调用API是不够的更重要的是能解决问题和优化性能。5.1 显示乱码或位置不对检查字体设置确保在显示前已经正确设置了字体GUI_SetFont。如果显示全为乱码或方块很可能是字体设置错误或字体数据未链接到工程中。检查坐标屏幕坐标系原点(0,0)通常在左上角。确保你的坐标值在屏幕物理分辨率或当前窗口客户区范围内。使用GUI_GetClientRect可以获取当前可绘制区域。检查文本模式如果背景色和前景色一样在GUI_TM_NORMAL模式下会看不到文字。尝试改用GUI_TM_TRANS或更改颜色。5.2 绘图闪烁问题这是嵌入式GUI最常见的性能问题。根本原因是直接在前台缓冲区绘图用户能看到绘制过程。双缓冲/多缓冲如果硬件支持如SDRAM足够大启用emWin的多缓冲功能。这是消除闪烁最根本的方法。局部刷新不要动不动就GUI_Clear()全屏。只重绘发生变化的部分区域。例如前文仪表盘例子中只清除并重绘柱状图区域。使用WM的自动脏矩形更新如果使用了emWin的窗口管理器确保正确使用了WM的API创建窗口和回调WM会自动管理脏矩形并优化刷新。5.3 内存不足与崩溃位图资源大尺寸、高色深的位图是内存杀手。务必使用Bitmap Converter转换为适合你屏幕色深如16位色的格式并考虑使用流位图。字体资源只链接项目实际用到的字体。GUI_Font24B_ASCII比GUI_Font24B_1包含更多字符体积小很多。动态内存emWin内部会使用malloc。确保你的堆heap空间足够大。可以在GUIConf.c中调整GUI_NUMBYTES的大小。5.4 提高绘图速度优先使用硬件加速如果MCU有LCD-TFT控制器或2D图形加速器并提供了emWin的底层驱动务必启用。减少复杂绘图调用GUI_DrawArc、带Alpha的填充、复杂的多边形填充都是CPU消耗大户。在低性能MCU上慎用。利用GUI_SetClipRect将绘图限制在尽可能小的区域内可以显著减少底层驱动函数的调用次数。编译优化确保开发环境开启了较高的编译优化等级如-O2。5.5 关于浮点数的特别提醒手册中GUI_DispFloat等函数内部使用了浮点数运算。如果你的芯片没有硬件FPU且需要高频刷新浮点数显示这可能会成为性能瓶颈。一种优化策略是在后台任务中将浮点数转换为字符串使用sprintf或更快的定制函数然后前台直接使用GUI_DispString显示字符串。或者对于固定小数位的显示可以将浮点数放大为整数后手动插入小数点显示。最后emWin的手册虽然详尽但最好的学习方式仍然是动手。从一个清屏、画框、显示“Hello World”开始逐步增加复杂度。多利用其丰富的样例程序它们几乎涵盖了所有API的使用场景。当你熟悉了这些基础绘图和显示函数后构建复杂、流畅的嵌入式GUI界面就不再是遥不可及的事情了。