
1. 从设计稿到MCU屏幕这条链路到底卡在哪做过嵌入式UI的人都有一个共同的痛设计师在Figma里画了一套精美的界面标注了间距、圆角、阴影、渐变交付过来之后你得对着这些标注在LVGL里一个像素一个像素地手搓。按钮的圆角是8px还是10px字体的行高是1.4倍还是1.5倍图标是24×24还是28×28——这些在Figma里一目了然的东西到了代码里全变成了体力活。更让人头疼的是当你辛辛苦苦把界面还原出来烧录到板子上跑起来发现效果跟设计稿差了十万八千里。颜色偏了、字体糊了、布局在240×320的小屏上挤成一团。于是又得回去调调完再烧烧完再看一个界面来回折腾十几遍。这个问题的本质在于Figma和嵌入式UI工程之间缺少一条自动化的转换链路。Figma是面向Web和移动端的设计工具它输出的CSS属性、布局逻辑、资源格式跟LVGL这类嵌入式图形库的运行环境完全不同。MCU上的LVGL要考虑RAM占用、Flash空间、渲染性能、字体裁剪、图片压缩格式比如LVGL 9支持的RGB565A8、ARGB8888等这些约束在Figma里根本不存在。所以“Figma稿直接变成嵌入式UI工程”这件事不是一个简单的格式转换问题而是一个跨领域的工程化问题。它涉及到设计规范的约束、中间格式的定义、代码生成策略、资源管线的搭建以及最终在MCU上的性能验证。我前后尝试过三种不同的方案踩了不少坑下面把完整的思路和实操过程拆开来讲。这篇文章适合两类人看一类是嵌入式工程师手头有LVGL项目想搞清楚怎么把设计稿高效落地另一类是带团队的技术负责人想搭建一套从设计到嵌入式UI的自动化管线。不管你是哪种下面的内容都能直接拿去用。2. 为什么不能直接把Figma的导出结果塞进LVGL2.1 Figma导出格式与LVGL运行时的根本差异很多人第一反应是Figma不是能导出PNG、SVG吗我把整个界面导成一张大图往LVGL里一贴不就完了这个思路在小屏幕上偶尔能用但一旦界面稍微复杂一点就会撞上几堵墙。第一堵墙是内存。一张480×480的ARGB8888全屏图光像素数据就是480×480×4 921,600字节接近900KB。大多数跑LVGL的MCU比如STM32F4系列SRAM总共才192KB连一张全屏图都放不下。就算用RGB565也要450KB依然不现实。第二堵墙是交互。嵌入式UI不是静态图片按钮要响应触摸、列表要滚动、状态要切换。你把整个界面拍成一张图所有交互逻辑都得自己重新算坐标、写事件处理反而比从头写更麻烦。第三堵墙是资源体积。Figma导出的PNG没有针对嵌入式做优化一张带渐变的背景图可能几百KB。而LVGL支持的是经过量化和压缩的二进制格式同样的图可以压到几十KB甚至几KB。所以正确的思路不是“导出图片”而是提取Figma的设计数据生成LVGL能直接使用的C代码和资源文件。具体来说需要从Figma拿到这些东西每个控件的类型按钮、标签、图片、容器每个控件的位置和尺寸相对于父容器样式属性背景色、边框、圆角、阴影、透明度文本内容和字体信息图片资源的引用关系层级结构父子关系、兄弟顺序这些数据在Figma的插件API里都能拿到。Figma提供了完整的Plugin API可以遍历文档树、读取节点属性、导出资源。这就是整条链路的起点。2.2 LVGL的控件模型与Figma图层模型的映射关系要把Figma的数据转成LVGL代码首先得搞清楚两边的模型怎么对应。我整理了一张映射表这是整个转换器的核心逻辑Figma节点类型LVGL控件说明FRAMElv_obj容器作为父容器设置布局和样式RECTANGLElv_obj 样式纯色或渐变背景块TEXTlv_label文本标签需处理字体映射ELLIPSElv_obj 圆角用圆角半径实现圆形GROUPlv_obj容器逻辑分组可设置flex布局COMPONENT自定义控件需要展开为具体子节点VECTORlv_img导出为位图后使用IMAGElv_img导出为LVGL支持的格式这里有几个关键点需要注意。Figma的FRAME默认是裁剪内容的而LVGL的容器默认不裁剪需要手动调用lv_obj_set_style_clip_corner或者设置LV_OBJ_FLAG_OVERFLOW_VISIBLE来控制。另外Figma的自动布局Auto Layout对应LVGL的Flex布局但两者的对齐规则和间距计算方式有差异转换时需要做适配。还有一个容易忽略的点Figma的坐标是相对于画板的绝对坐标而LVGL的坐标是相对于父容器的。转换时必须做坐标归一化否则所有控件的位置都会错乱。我的做法是递归遍历节点树每进入一层就减去父节点的偏移量得到相对坐标。2.3 字体和图片资源在MCU上的特殊处理字体是嵌入式UI里最容易被低估的资源。Figma里用的字体可能是思源黑体、Roboto、PingFang一个完整的中文字体动辄几MB到十几MB。MCU的Flash通常只有512KB到2MB根本放不下。解决办法是字体裁剪。只提取界面中实际用到的字符生成一个子集字体。LVGL官方提供了lv_font_conv工具可以把TTF字体转成C数组并且支持指定字符范围。比如你的界面只用到“确定”“取消”“设置”“返回”这几个词那就只提取这8个汉字加上数字和英文字母生成的字体文件可能只有几KB。图片资源也是同样的道理。Figma导出的PNG需要经过三步处理缩放、量化、转换格式。缩放是调整到实际显示的尺寸避免运行时缩放消耗CPU量化是把颜色数降到LVGL支持的格式比如RGB565、RGB565A8、ARGB8888转换格式是用LVGL的lv_img_conv工具把PNG转成C数组或二进制文件。注意LVGL 9对图片格式的支持有变化新增了对RGB565A8的原生支持这种格式在带透明通道的图标上比ARGB8888省一半内存。如果你的界面有大量半透明图标优先用这个格式。3. 搭建转换管线的三种技术路线3.1 路线一Figma插件直接生成LVGL C代码这是最直接的方案。写一个Figma插件在插件里遍历当前选中的画板读取所有节点的属性然后按照LVGL的API拼接出C代码最后通过插件的下载功能导出.c和.h文件。这个方案的核心是代码生成器。我当时的实现逻辑是这样的// Figma插件端遍历节点树输出中间JSON function traverse(node, parentOffset) { const result { type: mapNodeType(node), name: node.name, x: node.x - parentOffset.x, y: node.y - parentOffset.y, width: node.width, height: node.height, style: extractStyle(node), children: [] }; if (node.children) { for (const child of node.children) { result.children.push(traverse(child, { x: node.x, y: node.y })); } } return result; }拿到JSON之后再用一个Node.js脚本把JSON转成LVGL的C代码。生成的代码大概长这样lv_obj_t *screen lv_obj_create(NULL); lv_obj_set_style_bg_color(screen, lv_color_hex(0x1E1E1E), 0); lv_obj_t *btn lv_btn_create(screen); lv_obj_set_pos(btn, 20, 100); lv_obj_set_size(btn, 120, 48); lv_obj_set_style_radius(btn, 8, 0); lv_obj_set_style_bg_color(btn, lv_color_hex(0x007AFF), 0); lv_obj_t *label lv_label_create(btn); lv_label_set_text(label, 确定); lv_obj_set_style_text_font(label, lv_font_src_16, 0); lv_obj_center(label);这个路线的优点是链路短、可控性强生成的就是纯C代码直接扔进工程就能编译。缺点是灵活性差一旦设计稿改了就得重新生成整个文件手动改过的代码会被覆盖。而且Figma插件运行在浏览器沙箱里没法直接调用本地的字体转换和图片转换工具资源处理还得单独做。3.2 路线二Figma REST API 本地代码生成器如果你不想写Figma插件可以用Figma的REST API。Figma提供了GET /v1/files/:key接口可以拿到整个文件的JSON描述。这个JSON包含了所有节点的属性结构跟插件API拿到的数据基本一致。这个方案的好处是可以集成到CI/CD流程里。设计师在Figma里更新了设计稿触发一个Webhook本地服务器拉取最新的文件JSON跑一遍代码生成器自动提交到Git仓库。整个过程不需要人工干预。但REST API有几个限制需要注意。第一API返回的JSON非常大一个中等复杂度的界面可能几MB解析起来比较慢。第二API有速率限制免费版每分钟只能请求几十次。第三图片资源需要通过GET /v1/images/:key单独下载而且返回的是临时URL过期时间很短。我当时的做法是用Node.js写了一个服务定时拉取Figma文件解析后生成LVGL代码和资源文件然后用lv_font_conv和lv_img_conv处理字体和图片最后输出一个完整的工程目录。这个方案适合团队协作但对个人开发者来说有点重。3.3 路线三设计规范约束 半自动转换前两条路线都是“全自动”思路但实际项目中全自动往往不现实。因为设计师的Figma稿是面向Web的很多效果在MCU上根本实现不了比如复杂的阴影、模糊、渐变叠加。如果强行转换生成的代码要么跑不动要么效果惨不忍睹。所以我在实际项目中采用的是半自动方案先制定一套嵌入式UI设计规范约束设计师在Figma里能用什么、不能用什么然后基于这套规范做转换。规范的内容包括颜色只能用预设的调色板避免渐变色和半透明叠加字体只用指定的字号比如12px、16px、24px避免小数字号圆角半径限定在几个固定值4px、8px、12px阴影只能用简单的单层阴影模糊半径不超过8px图片必须用SVG或纯色图标避免位图缩放布局优先用Auto Layout避免绝对定位有了这套规范转换器的复杂度大大降低生成代码的可用性也高了很多。设计师知道哪些能做哪些不能做沟通成本也降下来了。提示这套规范最好做成Figma的共享样式库Shared Styles设计师直接调用预设样式从源头上保证一致性。4. 手把手实现一个Figma到LVGL的转换器4.1 环境准备与Figma插件开发基础先说一下环境。你需要安装Node.js建议18以上、Figma桌面版插件开发必须用桌面版调试、以及LVGL的开发环境我用的是VS Code PlatformIO STM32F407。Figma插件的开发流程是这样的打开Figma桌面版进入任意文件菜单栏选择Plugins Development New Plugin选择一个模板我选的是“Run once”Figma会生成一个包含manifest.json和code.js的目录。manifest.json是插件的配置文件定义了插件名称、ID、入口文件等信息{ name: Figma to LVGL, id: figma-to-lvgl-exporter, api: 1.0.0, main: code.js, editorType: [figma] }code.js是插件的主逻辑。Figma插件运行在一个沙箱环境里可以调用figma全局对象来访问当前文档。核心API包括figma.currentPage.selection获取当前选中的节点node.children遍历子节点node.type节点类型node.x、node.y、node.width、node.height几何属性node.fills、node.strokes、node.effects样式属性node.exportAsync()导出图片资源4.2 解析Figma节点树并提取样式属性解析节点树是整个转换器最核心的部分。我写了一个递归函数把Figma的节点树转成一个扁平的中间表示IR方便后续生成代码。function extractNode(node, parentX, parentY) { const ir { id: node.id, name: node.name, type: node.type, x: node.x - parentX, y: node.y - parentY, w: node.width, h: node.height, style: {}, text: null, children: [] }; // 提取填充色 if (node.fills node.fills.length 0) { const fill node.fills[0]; if (fill.type SOLID) { ir.style.bgColor rgbToHex(fill.color); ir.style.bgOpacity fill.opacity; } } // 提取圆角 if (node.cornerRadius ! undefined) { ir.style.radius node.cornerRadius; } // 提取文本 if (node.type TEXT) { ir.text node.characters; ir.style.fontSize node.fontSize; ir.style.fontWeight node.fontWeight; ir.style.textColor rgbToHex(node.fills[0].color); } // 递归子节点 if (node.children) { for (const child of node.children) { ir.children.push(extractNode(child, node.x, node.y)); } } return ir; }这里有几个细节需要处理。Figma的颜色是0-1的浮点数需要转成0-255的整数再转十六进制。Figma的圆角可能是四个角分别设置topLeftRadius、topRightRadius等需要判断是统一圆角还是独立圆角。文本节点还要处理对齐方式、行高、字间距等属性。4.3 生成LVGL初始化代码的模板设计拿到IR之后下一步是生成C代码。我采用模板字符串的方式为每种节点类型定义一个生成函数function generateCode(ir, parentVar) { const varName obj_${ir.id.replace(/[:;]/g, _)}; let code ; switch (ir.type) { case FRAME: case GROUP: code lv_obj_t *${varName} lv_obj_create(${parentVar});\n; code lv_obj_set_pos(${varName}, ${ir.x}, ${ir.y});\n; code lv_obj_set_size(${varName}, ${ir.w}, ${ir.h});\n; if (ir.style.bgColor) { code lv_obj_set_style_bg_color(${varName}, lv_color_hex(0x${ir.style.bgColor}), 0);\n; } if (ir.style.radius) { code lv_obj_set_style_radius(${varName}, ${ir.style.radius}, 0);\n; } break; case TEXT: code lv_obj_t *${varName} lv_label_create(${parentVar});\n; code lv_label_set_text(${varName}, ${ir.text});\n; code lv_obj_set_pos(${varName}, ${ir.x}, ${ir.y});\n; if (ir.style.textColor) { code lv_obj_set_style_text_color(${varName}, lv_color_hex(0x${ir.style.textColor}), 0);\n; } break; case RECTANGLE: code lv_obj_t *${varName} lv_obj_create(${parentVar});\n; code lv_obj_set_pos(${varName}, ${ir.x}, ${ir.y});\n; code lv_obj_set_size(${varName}, ${ir.w}, ${ir.h});\n; code lv_obj_set_style_bg_color(${varName}, lv_color_hex(0x${ir.style.bgColor}), 0);\n; code lv_obj_set_style_border_width(${varName}, 0, 0);\n; break; } // 递归生成子节点 for (const child of ir.children) { code generateCode(child, varName); } return code; }生成的代码会放在一个函数里比如void ui_init(void)在main函数里调用即可。对于按钮这类需要交互的控件我会在生成代码后手动添加事件回调或者在Figma里用命名约定来标记比如节点名以btn_开头生成器自动加上lv_obj_add_event_cb。4.4 字体裁剪与图片转换的自动化脚本字体和图片的处理是独立于代码生成的我写了一个Shell脚本把整个流程串起来#!/bin/bash # 1. 从Figma导出的JSON中提取所有用到的字符 node extract_chars.js chars.txt # 2. 用lv_font_conv生成字体 lv_font_conv --font SourceHanSansSC-Regular.ttf \ --range 0x20-0x7F \ --symbols $(cat chars.txt) \ --size 16 \ --format lvgl \ --bpp 4 \ --no-compress \ -o ui_font_16.c # 3. 转换图片资源 for img in assets/*.png; do lv_img_conv --format bin \ --bpp 4 \ --rgb565 \ --alpha 1 \ -o ${img%.png}.bin \ $img donelv_font_conv的--symbols参数可以指定要提取的字符--range指定字符范围。--bpp 4表示4位抗锯齿在MCU上效果和体积比较平衡。lv_img_conv的--rgb565指定输出RGB565格式--alpha 1表示保留透明通道。注意lv_font_conv和lv_img_conv都是Node.js工具需要全局安装。如果网络环境不好可以用npm的淘宝镜像加速。5. 转换效果实测与性能调优5.1 在STM32F407上跑通第一个自动生成的界面我的测试平台是STM32F407VGT6168MHz主频192KB SRAM1MB Flash屏幕是2.8寸240×320的ILI9341SPI接口。LVGL版本是9.1。第一个测试界面是一个简单的设置页顶部标题栏、中间三个设置项每个包含图标、文字、开关、底部一个确认按钮。Figma里画了大概20个节点转换后生成的C代码约300行。编译烧录后界面能正常显示但发现了几个问题第一文字位置偏移。Figma里文本节点的坐标是文本框的左上角但LVGL的lv_label默认是居中对齐的导致文字整体偏右。解决办法是在生成代码时加上lv_obj_set_style_text_align(varName, LV_TEXT_ALIGN_LEFT, 0)并且把标签的宽度设为文本框宽度。第二圆角不生效。Figma里设置的圆角是8px但生成的代码里lv_obj_set_style_radius没有生效。排查后发现是LVGL 9的样式系统改了圆角需要同时设置lv_obj_set_style_bg_radius或者确保没有覆盖默认样式。后来统一用lv_obj_set_style_radius并加上lv_obj_set_style_bg_opa(varName, LV_OPA_COVER, 0)解决。第三图片显示花屏。用lv_img_conv转换的图片显示出来是花屏的。原因是图片的色深和LVGL配置不匹配。LVGL 9默认的色深是RGB565但我的lv_conf.h里设置的是LV_COLOR_DEPTH 16而图片转换时用了--bpp 4导致解析错误。改成--bpp 16后正常。5.2 内存占用与渲染帧率的实测数据跑通之后我做了几组性能测试数据如下测试项数值说明界面节点数23个包含容器、标签、图片、按钮生成代码行数约320行不含字体和图片数据字体Flash占用12KB16px4bpp含常用汉字图片Flash占用48KB5张图标RGB565格式代码资源总Flash约80KB不含LVGL库本身运行时RAM占用约18KB含LVGL对象和缓冲区全屏刷新帧率28fpsSPI 40MHzDMA传输局部刷新帧率45fps仅刷新变化区域这个数据对于STM32F407来说是可以接受的。Flash占用80KB剩下900多KB足够放LVGL库和应用程序。RAM占用18KB剩下170多KB也够用。帧率方面全屏刷新28fps对于设置页这种静态界面完全够用。如果是列表滚动或者动画建议用局部刷新只重绘变化区域帧率能到45fps以上。5.3 常见转换错误的排查思路在实际使用中我遇到过的转换错误大概有这么几类坐标错乱。最常见的原因是父节点偏移没有正确减去。排查方法是把生成的代码里的lv_obj_set_pos参数打印出来跟Figma里的坐标对比。如果所有节点都偏移了同一个值那就是根节点的偏移没处理。颜色不对。Figma的颜色是sRGB空间LVGL默认也是sRGB理论上应该一致。但如果你的屏幕校准有问题或者LVGL的LV_COLOR_16_SWAP配置不对颜色就会偏。排查方法是先用纯红色0xFF0000测试看屏幕显示的是不是正红。字体缺字。如果界面上某些汉字显示为方块说明字体裁剪时没有包含这些字符。解决办法是重新跑一遍字符提取脚本确保所有lv_label_set_text里的字符串都被提取到了。图片不显示。先检查图片的C数组有没有被正确链接到工程里再检查lv_img_set_src的路径对不对。如果是二进制文件需要用LV_IMG_DECLARE声明。提示建议在生成代码时加一个调试模式把每个节点的名称、坐标、尺寸以注释的形式写在代码里方便对照排查。6. 把这套方案用在实际项目里的几点体会6.1 设计规范比转换工具更重要我一开始把大量精力花在写转换器上想让它能处理Figma里的各种复杂效果。后来发现这条路走不通——Figma的设计自由度太高了而MCU的资源太有限两者之间的鸿沟不是靠代码能填平的。真正有效的做法是反过来约束设计。我花了两天时间跟设计师一起制定了一套嵌入式UI规范把能用的颜色、字体、圆角、阴影、布局方式全部限定死。设计师在这个规范内工作转换器的复杂度直接降了一个数量级生成代码的可用性也从60%提升到了90%以上。这套规范后来做成了Figma的共享样式库设计师新建任何界面都直接从样式库里拖根本不需要记规范。这是我觉得最值得投入时间的一件事。6.2 自动生成不是终点而是起点转换器生成的代码我从来不指望它能直接用于生产。它的价值在于把重复性的体力活干掉让你把精力集中在交互逻辑和性能优化上。我的工作流是这样的转换器生成基础布局代码我把它复制到工程里然后手动添加事件回调、状态管理、动画效果。如果设计稿改了重新生成一遍用Git的diff功能对比变化只合并有差异的部分。这样既享受了自动化的效率又保留了手动调整的灵活性。对于节点数超过50个的复杂界面这个工作流能节省至少70%的时间。6.3 后续可以继续优化的方向这套方案目前还有几个可以改进的地方。一是支持Figma的变体组件Variants把不同状态默认、按下、禁用自动映射到LVGL的样式状态LV_STATE_DEFAULT、LV_STATE_PRESSED、LV_STATE_DISABLED。二是集成到CI/CD设计师提交设计稿后自动触发转换和编译生成固件供测试。三是支持多分辨率适配同一套设计稿自动生成不同屏幕尺寸的布局代码。不过这些都是锦上添花。核心的转换链路已经跑通了剩下的就是根据项目需求逐步完善。如果你也在做嵌入式UI开发建议先从一个小界面开始试把链路跑通再逐步扩大适用范围。踩几个坑之后你会对Figma和LVGL两边都有更深的理解。