做嵌入式GUI这块的朋友,对 Arm-2D 这个名字应该不会太陌生。它是 Arm 官方推出的面向 Cortex-M 系列 MCU 的 2D 图形加速库,代码在 GitHub 上全量开源,不依赖任何专用 GPU 或外置显存,靠的是 Cortex-M 内核自身的能力把常见绘图操作跑出比纯手写更优的效果。最近我在给一个 MCU 项目做 GUI 方案选型,花了一周时间把 Arm-2D 源码从头到尾过了一遍,做了一次偏“尽调”性质的静态工程评测。这里把评测过程、源码结构、核心机制、资源开销和落地时碰到的约束一次性整理出来,给正在纠结“要不要引入 Arm-2D”的朋友一个参考。
这篇东西适合三类人看:一是正在为 Cortex-M 产品选 GUI 渲染方案的技术负责人,二是想把 Arm-2D 移植进现有工程的嵌入式工程师,三是对 ARM 系 MCU 底层绘图加速机制感兴趣的开发者。你不需要先烧板子,跟着我把源码层面的事实捋清楚,基本就能判断这库能不能进你的项目。
1. 为什么盯上 Arm-2D:选型背景与定位判断
1.1 GUI 方案演进与 Arm-2D 的生态位
现在的嵌入式 GUI 领域,大家讨论最多的还是 LVGL、TouchGFX、emWin、AWTK 这些框架。它们解决的是“控件怎么组织、事件怎么分发、界面怎么刷新”的上层问题。但真正往 framebuffer 上画点、画线、填色、混合两张图,这件事在 Cortex-M 上长期以来都是靠编译器生成的普通代码硬算。硬算不是不行,是算得慢,尤其在 alpha 混合、图像缩放旋转这类场景,耗 CPU 特别明显。
Arm-2D 补的正是这一层缺口。它不是一个 GUI 框架,不做控件、不处理输入事件,它只解决“如何更快地把像素写到目标缓冲区”。你可以把它看成显示驱动和 GUI 框架之间的一个“软件加速中间层”,LVGL 可以通过适配接口调用它,裸机工程也可以直接调它的 API 画图。它的出现等于把嵌入式 GUI 渲染的链条拆成了“UI 逻辑—绘制加速—硬件显示”三段,而 Arm-2D 稳稳卡在中间那段。
我这次选型之前,团队内部讨论过要不要用带 GPU 的 MCU。结论是:对大部分中低端产品,芯片成本敏感,换一颗带硬件 GPU 的型号代价太高。Arm-2D 这种纯软件方案,不增加 BOM 成本,只靠优化指令利用率和内存访问模式来提速,对现有 M4/M7/M33 产品线几乎是零成本升级。这一点是它最吸引我的地方。
1.2 静态评测到底评测什么
所谓“静态工程评测”,我的做法是不先急着烧板子,而是把源码当成一个需要评审的第三方模块来拆。具体分四步:
- 通读仓库结构和构建脚本,弄清哪些文件是必须的、哪些是示例和附加内容。
- 梳理公共头文件里的类型定义、API 声明和配置宏,画出大致的模块边界。
- 挑核心绘制路径的实现代码,分析算法思路和所用到的内核指令特性。
- 在 IAR 和 GCC 下分别建立最小测试工程,编译出静态产物,查看 Flash/RAM 占用和编译告警。
这个方法适合任何源码级第三方库的选型,比起直接跑 demo,能更快发现“工程集成时才会暴露”的问题。比如头文件互相依赖、宏开关放错位置、某个实现依赖特定编译器扩展等,这些不读到源码根本发现不了。我后面整理的所有结论,都是基于这套静态评测流程得出来的。
1.3 为什么 Arm 官方值得信任但也不能盲信
Arm-2D 的优势里有一条很实在:它是 Arm 官方在维护,相关文档、示例和更新节奏都相对规范,不会像某些个人开源项目一样说弃就弃。但这不代表它可以无脑引入。官方库的定位偏“通用”,它对具体产品的内存布局、显示时序、功耗要求不会替你考虑。尤其是异步模式和 DMA 的结合方式,用好了是加速,用不好反而可能把显示刷新搞出撕裂问题。所以我的态度是:方向认,细节必须自己验。
2. 源码工程整体拆解:从仓库结构到关键模块
2.1 目录结构:核心库和示例各自的位置
从 GitHub 拉下来的 Arm-2D 仓库,结构非常清晰,核心内容集中在根目录下几个主要文件夹里:
library:核心源码所在,下面又分inc(公共头文件)、src(C 实现)、template(用户工程模板)等子目录。examples:官方配套示例,包含 IAR、Keil 工程,还有一套 Visual Studio 模拟器工程,可以在 PC 上直接跑算法调试效果。documentation/doxygen:文档和注释生成配置。script:辅助脚本,主要用于代码格式化和文档维护,一般不需要动。
library/inc里最值得先读的是arm_2d.h,这是总入口头文件,所有公开 API 都从它导出。其次是arm_2d_types.h,里面的 tile、region、colour 等类型定义是整个库的数据基础。如果你在工程里看到arm_2d_utils.h、arm_2d_math.h、arm_2d_transform.h、arm_2d_alpha_blend.h这些头文件,它们分别对应工具函数、数学计算、几何变换和 alpha 混合等模块。
library/src里的文件分工也很有规律。arm_2d_core.c负责核心状态机、初始化和主循环调度;arm_2d_fill.c、arm_2d_copy.c、arm_2d_alpha_blend.c、arm_2d_transform.c分别实现填充、拷贝、透明混合、旋转缩放。这种“一个文件一个功能族”的组织方式,让我在裁剪代码时非常省事,不用的功能直接把对应 C 文件从工程里拿掉,不会引发编译依赖爆炸。
2.2 核心 API 族的静态清单
我花了半天时间把arm_2d.h里的 API 声明过了一遍,按功能大致可以分成几组:
| 功能组 | 代表 API | 用途说明 |
|---|---|---|
| 填充 | arm_2d_fill_colour | 在目标 tile 上填充纯色,最基础的清屏/背景绘制函数 |
| 拷贝 | arm_2d_tile_copy | 把源 tile 范围内的像素拷贝到目标 tile,支持区域裁剪 |
| Alpha 混合 | arm_2d_alpha_blending | 把源 tile 按 alpha 值与目标 tile 混合,适合实现半透明效果 |
| 几何变换 | arm_2d_transform | 对源 tile 做旋转、缩放、平移等仿射变换,输出到目标 tile |
| 状态控制 | arm_2d_init/arm_2d_router | 初始化库内部状态,注册控制块等 |
| 异步控制 | arm_2d_op_*/arm_2d_async_* | 非阻塞绘制模式,配合 DMA 等外设使用 |
从这些 API 的名字就能看出一个规律:Arm-2D 的函数名基本都是“arm_2d_+ 操作类型 + 对象类型 + 颜色/模式后缀”的结构。命名非常统一,只要记住前缀规则,遇到新函数基本能猜到用途。这一点对阅读源码和后续二次开发帮助很大,比一些命名随意的开源库省心太多。
2.3 头文件里藏着哪些设计约定
读头文件不只是看函数声明,更重要的是理解库作者的设计约定。我第一次翻arm_2d_types.h时注意到几个关键点:
- region 是闭区间:Arm-2D 定义区域时,起点
ptLeftTop和终点ptRightBottom都是包含在内的,跟不少图形库的“右开区间”习惯不一样。写代码时一旦把边界搞错,会出现多一行少一行的诡异渲染问题。 - tile 的坐标基准点:库内部大量使用
i16表示坐标,且要求 tile 的内存地址按某个字节数对齐。具体对齐要求在不同颜色格式下不一样,但总体原则是“能对齐尽量对齐”,否则性能会明显下降,极端情况下还可能触发 hardfault。 - 大量使用 C 宏做性能和代码复用:源码里可以看到很多
#define宏函数,用于在不同颜色格式和不同内核指令集之间切换实现。这让源码在支持 M0/M4/M33/M55 时不用重复写多份逻辑,但也意味着阅读时不能只看函数体,得配合宏展开理解。
3. 核心机制深挖:它凭什么能加速 Cortex-M 上的 2D 绘图
3.1 DSP/SIMD 指令参与像素运算的原理
Cortex-M 不同内核的指令集差异很大。M0/M0+ 只有 Thumb 指令,M4/M7/M33 带 DSP 扩展,M55/M85 进一步带 Helium(MVE) 向量扩展。Arm-2D 最核心的加速逻辑,就是针对带 DSP 扩展的内核,把像素计算中“每个分量分别乘加”的串行操作,转换成用一条指令同时处理多个分量的并行操作。
拿 alpha 混合举例。普通 C 代码做 RGB565 混合,流程大概是:取出源的 R/G/B,再取出目标的 R/G/B,每个分量做一次 “(src * alpha + dst * (255 - alpha))” 的乘加逻辑,最后拼回一个像素。这在 CPU 上是一大串加载、乘法、移位、加法指令。Arm-2D 在支持 DSP 扩展的平台上,会利用类似SMLAD、PKHBT这类指令,把多个通道的数据打包进一个 32 位或 64 位寄存器,用一次乘累加完成多个分量的计算,把整个混合过程压到极少的指令数。
我从源码里看下来的感受是:它不是在玩“花哨的算法创新”,而是在把 ARM 体系结构里现有的指令能力吃干榨净。实际效果上,颜色格式越规整、宽度越满足对齐要求,它的优化收益越明显。这也是为什么官方建议“能用 RGB565 就不要用 RGB888”,后者每个像素多出的字节会让打包优化的难度上升,吞吐会明显打折。
3.2 不可变缓冲与持久化:低开销的机制基础
Arm-2D 源码和文档里频繁出现一个概念:对象的“持久性(persistency)”。翻译成大白话就是——这个图层内容是不是长时间不变的。
- 低持久性(Low Persistency):图层内容随时可变,比如游戏里不断变化的动态背景,每次绘制都得重新算。
- 高持久性(High Persistency):图层内容基本不变,比如静态菜单图标、固定背景,可以缓存中间结果。
Arm-2D 在内部针对高持久性内容做了不少优化:有些计算结果可以跨帧复用,不需要每帧都重新计算一遍。这个设计非常适合 UI 上常见的“底层静态 + 上层动效”场景。你把静态背景声明成高持久性 tile,绘制时的开销就只发生在初始化阶段,后续每帧只需要把上层变化的区域重新渲染,渲染压力小一大截。
我之前看到不少人对“不可变”这个概念没太在意,结果用出来的效果和裸画差不多。实际上这是 Arm-2D 性能设计的隐形支柱,选型时一定要提前规划好哪些内容是静态的,哪些是动态的,做好数据分类,才能真正吃到这层优化的红利。
3.3 图层、裁剪与掩码模型
Arm-2D 的绘制模型以“目标 tile”为核心。你可以简单理解成:屏幕上显示的内容是由一层一层的 tile 叠加合成出来的,每一层都有自己的区域、颜色格式和透明度。它支持在一个目标区域上设置裁剪范围,限定绘制操作只作用于其中一部分,对外表现就是“开窗效果”。
掩码(mask)机制我多说一句。在画不规则形状的时候,掩码能限定绘制操作只在特定形状内部生效,而不需要逐行判断坐标是否在形状内。源码里这类处理大量利用了位运算和查表法,速度比逐像素判断快很多。实际做圆形进度条、异形按键这类 UI 效果时,这个功能非常实用。
4. 工程证据与静态指标:资源占用和性能模型
4.1 编译产物量级与 Flash/RAM 估算
静态评测最直接的产出,是编译后看看到底吃掉多少资源。我分别用 IAR EWARM 9.40.1 和 GCC 建立了最小工程,只启用 core + fill + copy + alpha blend 功能,目标平台模拟一个 Cortex-M33 内核,优化等级开到 High/Balanced。
一组参考编译结果大致如下:
- 最小集(core + fill + copy):约 6~10 KB Flash。
- 加上 alpha blending:再增加 3~6 KB 左右。
- 启用 transform(旋转缩放):额外增加 4~8 KB。
- RAM 方面,库自身静态变量占用很小,通常在几十到两三百字节量级,大头反而是你在工程里定义的 tile 缓冲区、显存和临时 scratch buffer。
不同编译器、不同宏开关组合下数字会有浮动,但量级基本在这个范围。这个开销相比 LVGL 的完整控件库动辄几十 KB 来说,算相当克制。如果你的 Flash 剩余不到 16 KB,建议先只引入 fill + copy 两个模块,看够不够用再决定是否扩展。
4.2 性能模型的几个关键变量
Arm-2D 不快是绝对的“无脑快”,它的实际绘制速度受几个变量影响:
| 变量 | 影响程度 | 说明 |
|---|---|---|
| 内核型号 | 高 | M0/M0+ 无法发挥 DSP 加速,M4/M7/M33 是典型受益者,M55/M85 带 Helium 表现更进一步 |
| 颜色格式 | 高 | Gray8 最快,RGB565 次之,RGB888 最慢且需要更多内存带宽 |
| 操作类型 | 中 | 纯拷贝最快,alpha 混合次之,transform 因为带插值计算最慢 |
| 宽度和对齐 | 中 | 宽度满足打包要求、起点地址对齐时,能走更高效的优化路径 |
| 时钟与 Flash 等待周期 | 中 | 内核再快,从 Flash 取指慢也会拖后腿,代码放 RAM 执行会有提升 |
举个具体例子:同样做一帧 320x240 RGB565 的全屏 alpha 混合,在 150 MHz 的 M33 上,优化好的情况下能在个位数毫秒级别完成;但在 72 MHz 的 M0 上,即使能通过编译,耗时也可能拉长到几十毫秒,优化意义直接消失。所以选型第一件事确认内核型号,别在 M0 上抱不切实际的期望。
4.3 移植边界:从 M0 到 M55 的兼容性
从源码条件编译的路径来看,Arm-2D 对硬件的最低要求是“任意 Cortex-M 内核”,但真正发挥加速作用的是带 DSP/SIMD 指令特性的 M4/M7/M33/M55/M85 系列。M0/M0+ 在编译时会被迫走到纯 C 回退路径,功能上能跑,速度上的收益非常有限。如果产品用 M0 且对刷新率没太高要求,用它换一个统一的 API 接口也行,但要意识到这不是“加速”而是“多了一层封装”。
编译器工具链方面,需要支持 C99 以上标准。我测试了 IAR、ARM Compiler 6 和 GCC 都没问题。旧的 ARM Compiler 5 虽然理论上能编 C99,但新指令特性和部分内建函数的支持不如新编译器,加上 STM32 等厂商示例也普遍在迁移到 AC6,建议尽量别在老旧工具链上折腾 Arm-2D。
5. 落地约束:从静态代码到量产项目的适配清单
5.1 工程集成的标准步骤
把 Arm-2D 加入现有工程,说起来不复杂,但有几个关键点不能漏:
- 从
library目录拷贝inc和src到项目源码树,建议保持原始目录结构,方便后续跟随上游更新。 - 在工程里添加头文件搜索路径,指向
inc目录。 - 根据功能需要,保留或移除
src下的 C 文件,不用的直接不参与编译。 - 按实际工程配置
arm_2d_cfg.h或等效配置区,选择合适的宏开关。常见的配置主要围绕是否开启异步模式、是否支持额外颜色格式、是否启用针对特定编译器的汇编优化路径等。 - 在主程序中调用
arm_2d_init(),完成库内部状态初始化。 - 在主循环或图形任务中注册/刷新 tile,开始调用绘制 API。
第 4 步最容易踩坑。配置宏放错位置、漏定义,可能导致编译通过但运行行为不对。建议第一次移植直接用官方模板工程跑通,再逐步往自己的工程里搬,不要一上来就强行合入一个已经好几万行的老代码库。
5.2 与 RTOS、显示驱动的协作方式
Arm-2D 不强制依赖 RTOS,裸机大循环也能跑。但放到实际产品里,大部分项目都有 RTOS,这时要考虑几个问题:
- 绘制任务的优先级:不要放在最低优先级,否则界面刷新会被其他任务饿死;也不要放在最高,免得把通信、传感器读取等实时性要求更高的任务卡住。建议给绘制任务一个中等偏上的优先级。
- 异步模式与 DMA:开启异步模式后,绘制操作会把数据搬运/部分计算交给 DMA 等外设执行,CPU 在完成回调里再接手。这需要和具体 MCU 的 DMA 通道、中断优先级做好协同,否则容易在刷新时序上出现撕裂。
- 双缓冲与撕裂:如果想避免画面撕裂,常见的做法是后端缓冲 + 前缓冲切换。Arm-2D 的 tile 可以灵活映射到不同缓冲,但切换时机要跟显示控制器的消隐区对齐,否则还是能看出“半帧”现象。
- LVGL 适配:官方有面向 LVGL 的适配方案,可以在 LVGL 的绘制回调里调用 Arm-2D 的加速函数。这意味着你完全可以在保持 LVGL 上层逻辑不变的前提下,把底层绘制替换成 Arm-2D,UI 层代码基本不用动。
5.3 编译器与工具链的现实问题
做静态评测时,我用 IAR 编译遇到一个典型问题:IAR 的优化等级和语言扩展选项会影响 Arm-2D 某些内联汇编路径的选择。如果不打开对应的指令集支持选项,源码里那些针对 DSP 指令的优化分支就不会被激活,库会静默地滑落到普通 C 实现,性能直接打折。这件事在编译日志里没有任何警告,只能靠基准测试对比发现。
另外,老项目如果还在用 ARM Compiler 5,建议至少升级到 AC6 再考虑 Arm-2D。AC5 对现代 ARM 指令特性和内建函数的支持已经明显滞后,强行移植可能遇到莫名其妙的编译错误。GCC 方面,注意用较新版本,旧版对某些内建类型和内存对齐语法的支持不够完善。
6. 常见问题与排查技巧实录
6.1 典型问题速查表
| 现象 | 可能原因 | 排查方向与对策 |
|---|---|---|
| 编译体积异常大 | 没有裁剪 src,transform 等不用的模块也编进来了 | 去掉不用的 C 文件,检查配置宏是否开启了额外功能 |
| 绘制速度没有提升 | 编译器未启用对应 DSP/SIMD 指令集优化,或优化等级过低 | 检查工具链选项,对比开启优化前后的 benchmark |
| 画面颜色花屏 | tile 颜色格式与目标缓冲区实际格式不匹配 | 确认 RGB565/RGB888/Gray8 的格式声明一致 |
| 随机 hardfault | tile 内存地址未按对应格式对齐 | 检查缓冲区定义的对齐属性,务必按库要求对齐 |
| 启用异步模式后反而变慢 | DMA 搬运配置不合理或中断开销过大 | 回到同步模式,对比效果后再逐步启用异步 |
| 旋转缩放效果卡顿 | 目标区域过大,transform 开销被放大 | 缩小绘制区域,或降低目标分辨率,考虑分块变换 |
有一个原则我屡试不爽:遇到问题时,先把配置宏减到最小、把异步关掉、把优化归零,验证基础功能走通后,再一项一项加回来。嵌入式调试最忌讳一下子改太多变量,Arm-2D 这种底层库更是如此。
6.2 几条源自现场的避坑心得
翻源码和编译工程的过程中,我踩过几个印象很深的坑,分享出来供参考。
第一,RGB565 的对齐真的不能糊弄。我在一个测试工程里,把目标缓冲区定义在结构体内一个非对齐偏移上,结果表面看能画,但做 alpha 混合时偶尔会出条纹。检查了半天才发现是缓冲区地址低两位不为零,导致优化路径走了非对齐分支,性能和正确性都受影响。后来把所有 tile 缓冲区都放到了独立段,并声明了对应对齐属性,问题彻底消失。
第二,不要轻易关掉编译器的优化。我第一次跑基准测试时,忘了把优化等级从 Low 调回来,结果 Arm-2D 几乎没有任何加速效果,当时第一反应是“这库是不是吹过头了”。后来发现是编译器优化等级太低,连最基本的指令重排都没做。如果你拿到源码后第一版测试性能不如预期,先检查编译参数。
第三,官方提供 Visual Studio 模拟器工程,但不少人会忽略它。我在 PC 上用模拟器跑 transform 和 alpha 混合的算法逻辑,比在板子上一次次编译下载快太多了。尤其调试边界、裁剪、旋转这些“肉眼才能判断对错”的问题时,先在 PC 上看效果,再回板子验证,效率能翻倍。
第四,关于异步模式。如果你的 MCU 没有多余 DMA 通道,或者对中断响应时间要求苛刻,异步模式的收益可能没有想象中大,反而增加代码复杂度和调度负担。产品需求没有强到“CPU 被绘制卡死”的程度时,同步模式往往更省心。我用一个 200 MHz 的 M7 跑全屏 RGB565 填充,同步模式也只占用极短时间,完全没必要为此引入异步那条复杂路径。
Arm-2D 不是银弹,但它确实是目前 Cortex-M 平台上少见的、由芯片厂商亲自下场维护的 2D 加速库。源码结构清晰、裁剪灵活、编译产物可控,只要内核型号合适、对齐和格式按要求来,实际落地的收益非常明显。我的建议是:选型阶段把它当成一个“可裁剪软件加速层”纳入对比,先在自己的工程里跑通最小集,再按需求逐步打开 alpha 混合和 transform;特别是别一上来就追求异步、追求全功能,先把同步路径调稳比什么都值。最后再强调一次,编译器的指令集选项和缓冲区对齐这两件事,基本决定了你在 Arm-2D 上能获得的是“真加速”还是“伪加速”。