【说在前面的话】
在往期的文章《什么是嵌入式系统(下)——沉淀模型》我们曾提到过:
现在的计算机技术差不多领先嵌入式技术大约20年,现在嵌入式系统无论在资源上、理论上还是方法论上,都与上世纪80年代的计算机前沿技术相当。
GorgonMeducer 傻孩子,公众号:裸机思维什么是嵌入式(下)—— “重力”和“沉淀”
这意味着,本世纪初应用在个人电脑上的一些技术也可能会被逐步引入到深度嵌入式系统上(Deep Embedded System)——这类系统的典型也就是大家所熟知的单片机或者说Cortex-M处理器。
Arm是行业内 “老” 生态玩家了——几乎所有的行为都会从生态的角度加以考量——所以要想搞清楚为啥Arm在此时为“Cortex-M”定制显卡驱动,就必须要搞清楚整个深度嵌入式生态究竟发生了什么。要想做到这一点,不妨设想一下:
你觉得智能手表应用应该很火,或者拥有彩屏的智能IoT终端设备应该很火。 考虑到低功耗和低成本,一些产品使用Cortex-M(而不是Cortex-A)来实现应该很合理。于是,你司定义了一款配备有2D图形加速引擎的Cortex-M处理器。 芯片很快就做出来了,开发板配上了一个480*272分辨率的触摸屏,结合特别设计的PCB(以及极高识别度的PCB配色)——怎么看都显得高端!大气!上档次! 问题来了:
市面上完全没有针对单片机(裸机或者RTOS)的第三方2D类跑分软件(专业说法叫Benchmark)——这如何才能体现你硬件的强大呢?测评机构又如何把复杂的2D处理以简单数值的形式呈现在大众面前呢? 市面上有那么多第三方GUI提供商,他们都有针对Cortex-M芯片的GUI产品,但我要如何说服他们增加对我的芯片提供支持呢?
你是一个GUI软件提供商:
你们之前的产品在Cortex-A以及Linux环境下小有名气。
最近看到很多软件公司纷纷瞄准了深度嵌入式市场,提供了定制化的GUI产品,比如微软的GUIX,Qt的Qt for Cortex-M。于是你也很快提供了对应的GUI产品,但问题随之而来:
市面上完全没有针对单片机的第三方2D类跑分软件……
与Cortex-A以及Linux环境较为规范的软件环境不同,深度嵌入环境碎片化太严重了:
LCD规格不同
连接LCD的硬件接口不同(LCD带宽不同)
LCD的操作方式不同(有无LCD外设驱动器,驱动器的类型)
目标芯片的资源不同、系统频率不同
软件环境不同——RTOS千差万别,甚至还有裸机
芯片厂家提供的2D图形加速硬件每个都不一样……
总结来说,如果要支持一款硬件平台,就要针对它的硬件为其做移植和定制……
考虑到团队资源有限,所以能“官方”支持的硬件也有限……
总结来说:
芯片厂家以“定制化的”2D图形加速硬件为芯片提供了“差异化”的卖点——但换个角度看其实就是加剧了硬件的碎片化。同时,芯片厂商也苦于找不到大量的GUI协议栈为芯片提供软件支持;
GUI软件提供商苦于由硬件碎片化所带来的庞大移植工作量。它们希望自己的团队能更多集中精力来开发GUI本身,而不是疲于为新的硬件平台提供“官方支持”
突然有一天,累的吭哧吭哧的两方突然觉得哪里不对劲,然后都默默的把头转向了Arm——这个生态系统中一直号称中立的第三方……
于是Arm在大家灼热的目光下弱弱的在Github上扔了一个叫Arm-2D的显卡驱动标准,提出了这样一个议案:
芯片厂家:你只要按照我的标准为你的2D加速引擎写好驱动就行了——分分钟获得所有以Arm-2D作为底层的GUI的支持;
GUI提供商:但凡你GUI中需要用到硬件加速的地方,都可以直接调用我提供的API就行了——如果芯片实际有硬件加速,自然就能得到加速,如果芯片没有硬件加速,那就用我提供的软件优化算法——总的来说,就是一次移植,哪儿哪儿都能用,有没有实际硬件加速都不要紧。
对裸机用户:你们也不要着急,我知道你们硬件平台资源紧张——用不起专业GUI协议栈:你们一般要么不用GUI、要么就纯粹自己动手直接写简易的GUI——别担心:Arm-2D也给你们也带来了大礼包,让你们有能力简单的就能实现“那些未曾设想过的道路”。
“什么什么?” 裸机用户惊呼:“不是B2B之间的事情么?咋突然有我们的福利了?”
先卖个关子,文章后面再讲。
【面向深度嵌入式的2D处理跑分】
建立一个比较典型的2D图形处理负载来模拟GUI日常应用场景中所需的工作量和复杂度。(该Benchmark的静态截图大约就是下面这个样子,注意这是在模拟器上的效果,所以帧率很低):
让不同的图层以不同的速度和角度飘来飘去以覆盖更多可能的情况——模拟日常GUI中可能出现的不同复杂度;
渲染1000帧,记录下每一帧所消耗的CPU时钟周期数,并提供最小值、最大值和平均值等信息。
以帧渲染所需的平均周期数作为依据,计算 “30FPS所需最小处理器频率”——并以这一数值作为跑分结果。
值得说明的是:
这一跑分软件在统计“渲染一帧所需的周期数”时并不会把 “从RAM向LCD发送数据”所消耗的时间计算在内——因为“刷新显存”所消耗的时间由芯片和LCD之间的连接方式(或者说传输带宽)决定,而与芯片的2D图形处理能力无关。
实际上,经常有小伙伴看到Benchmark的动态效果时会不无讽刺的说:“嗯,很酷炫,我就想知道处理器除了做这个事情还剩下多少能力来处理具体应用。” 其实,“30FPS所需最小时钟频率” 就是为了直观的回答这个问题的。
Cortex-M0+需要大约 194MHz 就够了,换句话说:如果你的Cortex-M0+跑250MHz,你还剩下56MHz的CPU性能可以用于应用。
拥有双核Cortex-M0+跑250MHz的树莓派Pico狂喜
Cortex-M3/M4 大约需要 105MHz,也就是说:如果你的芯片跑个216MHz,就还有大约111MHz用于具体应用(话说,这年月哪个Cortex-M4还不跑个百来十兆的?)
而使用了SIMD指令集加速(Helium指令集)的Cortex-M55居然只要16MHz就能达到30FPS的刷新率——着实让人觉得恐怖。
更直观的,这里是官方提供的2D性能的倍率比较(以Cortex-M4的性能为基准)。Cortex-M55 Helium居然是Cortex-M4性能的近乎6倍、Cortex-M0的10倍!——如果说Arm-2D是显卡驱动的话,那么Cortex-M55 Helium就是一款自带“集成显卡”的Cortex-M处理器了。
【准备工作】
void Disp0_DrawBitmap (uint32_t x,
uint32_t y,
uint32_t width,
uint32_t height,
const uint8_t *bitmap)
这里,5个参数之间的关系如下图所示:
很多LCD都支持一个叫做“操作窗口”的概念,这里的窗口其实就是上图中的矩形区域——一旦你通过指令设置好了窗口,随后连续写入的像素就会被依次自动填充到指定的矩形区域内(而无需用户去考虑何时进行折行的问题)。
此外,如果你有幸使用带LCD控制器的芯片——LCD的显示缓冲区被直接映射到Cortex-M芯片的4GB地址空间中,则我们可以使用简单的存储器读写操作来实现上述函数,以STM32F746G-Discovery开发板为例:
//! STM32F746G-Discovery
#define GLCD_WIDTH 480
#define GLCD_HEIGHT 272
#define LCD_DB_ADDR 0xC0000000
#define LCD_DB_PTR ((volatile uint16_t *)LCD_DB_ADDR)
void Disp0_DrawBitmap (uint32_t x,
uint32_t y,
uint32_t width,
uint32_t height,
const uint8_t *bitmap)
{
volatile uint16_t *phwDes = LCD_DB_PTR + y * GLCD_WIDTH + x;
const uint16_t *phwSrc = (const uint16_t *)bitmap;
for (int_fast16_t i = 0; i < height; i++) {
memcpy ((uint16_t *)phwDes, phwSrc, width * 2);
phwSrc += width;
phwDes += GLCD_WIDTH;
}
}
【如何获取安装包】
也可以通过KEIL官网直接下载:
https://www.keil.arm.com/packs/arm-2d-arm/versions/
Arm-2D 的部署不可谓不简单,基本可以通过在 RTE 中勾选对应选项完成大部分工作。具体请参考下面的手把手教程吧:
在 MDK 工程中依次选择 Project -> Manage -> Run-Time Environment 来打开 RTE 配置窗口:
Core:Arm-2D的核心(必选) Alpha-Blending:大部分与透明度相关的操作,比如基于蒙版的拷贝、透明图层合成、透明色块填充等等 Transform:旋转、缩放等操作(支持蒙版、抠图和透明度) Filter:渲染滤镜(目前支持抗锯齿和模糊效果)
FPB:Partial Frame-Buffer模块,支持部分刷新的核心组件 Display Adapter:一个使用 PFB 来适配 LCD 底层驱动的代码模板,帮我我们快速在上层绘图和底层LCD刷新之间建立桥梁。一般来说 Display Adapter 与 屏幕是一一对应关系:如果你有一块屏幕,这里就选“1”,如果你有两块屏幕,这里就选“2”,以此类推。
为了方便后续的开发,强烈推荐下载并安装 perf_counter 模块,具体步骤请参考文章《【喂到嘴边了的模块】超级嵌入式系统“性能/时间”工具箱》,这里简述下关键步骤:
关注【裸机思维】公众号后发送关键字“perf_counter”来获取对应的 cmsis-pack 下载并安装 cmsis-pack 打开RTE后找到 Utilities,并勾选 perf_counter 中的 Core,推荐以 Source形式进行部署 如果出现橙色警告,单击Resolve按钮来解决
如果因为某些原因,你确实无法(或者不愿意)使用perf_counter来为Arm-2D提供时间基准服务,则需要手动实现下列两个函数:
extern
int64_t arm_2d_helper_get_system_timestamp(void);
extern
uint32_t arm_2d_helper_get_reference_clock_frequency(void)
其中:
arm_2d_helper_get_system_timestamp() 要返回一个int64_t类型的时间戳,它的精度至少要是10KHz级别,并且不允许(在你我的有生之年内)溢出;
arm_2d_helper_get_reference_clock_frequency() 要返回这个时间戳的实际频率,同样,根据要求,它至少要大于等于10KHz。
如果只在乎性能不在乎尺寸,请选择-Ofast + Link Time Optimization 如果只在乎尺寸完全不在乎性能,请选择-Oz + Link Time Optimization 如果在乎尺寸和性能的平衡,请选择-Os + Link Time Optimization 由于Arm Compiler 6 只在 -O0 时拥有较好的单步调试效果(可以观察局部变量,可以在几乎所有代码处下断点),因此-O1、-O2和-O3其实是非常尴尬的存在——他们在优化上比不过-Ofast、-Oz以及-Os,却跟上述三者一样都无法提供很好的单步调试效果。使用-O1、-O2、-O3属于“开了但没有完全开”“开优化好处没有享受到最大,缺点却吃满了”。因此,对于Arm Compiler 6来说,要么不开优化,要开就开最高。 不开Link Time Optimization的Arm Compiler 6 无法拉开和 gcc 的差距。开了Link Time Optimization的 Arm Compiler 6 可以瞬间超越(或者至少战平)IAR。从大量的实际工程中可以看到,相对 Arm Compiler 5也可以轻松实现30%到50%的提升(无论是尺寸还是性能)。
我们会看到类似这样的窗口:
在右半部分的Packs选项卡中,找到ARM::CMSIS,确保它显示“Up to date”,如果没有就单击对应的按钮进行更新。Arm-2D所依赖的CMSIS版本不得低于5.7.0(如果你要用Cortex-M55,则版本不得低于5.8.0)。
2、通过如下图所示工具栏正中间的按钮打开RTE配置窗口:
在Software Component列表中,展开CMSIS,并勾选上CORE和DSP。这里需要注意的是,DSP部分如果有Source的选项请选择Source选项——这将允许我们直接使用源代码的形式来编译CMSIS-DSP的库。
此外,如果你不确定RTE中所使用的CMSIS是否为最新的版本的话,可以单击Select Packs按钮:
看到窗体顶部 “Use latest Software Packs for Target” 被勾选,基本上就可以高枕无忧了。依次单击OK关闭对话框后,我们就成功的将CMSIS加入到了编译中。这里,由于我们选择了使用源代码的方式来编译CMSIS,因此可能还需要对CMSIS-DSP的源代码进行额外的设置。
至此,我们就应该能够成功的完成编译了。
根据你的屏幕填写正确的信息:
颜色位数(Screen Colour Depth)
横向分辨率(Width of the screen)
纵向分辨率(Height of the Screen)
部分刷新缓冲块的宽度(Width of the PFB Block),一般优先考虑为整行或者1/2行像素的宽度
部分刷新缓冲块的高度(Height of the PFB Block),一般推荐为1/10屏幕像素高度。在RAM较为紧缺时,考虑8或者1。
进行帧率计算时,平均多少帧做一次数据更新(Number of iterations),默认是30,选0将关闭帧率实时计算功能。
实时帧率统计模式(FPS Calculation Mode),默认为 Render-Only FPS(只计算Arm-2D渲染部分的帧率)而Real FPS则计算实际的帧率。
如果你的LCD控制芯片无法以硬件的方式交换RGB565像素的高低字节,则你可能需要勾选 Swap the high and low bytes 通过软件来实现这一功能。
此外,Display Adapter 支持用DMA+ISR的方式来实现异步刷新。本文为了降低难度,使用的是直接在 Disp0_DrawBitmap() 函数中直接向LCD传输数据的同步刷新模式。如果你是一个有LCD驱动经验的工程师,想一步到位,可以参考这篇文章《群友看傻了!三个简单步骤我就把LCD刷新率逼到了理论极限》的内容,这里就不再赘述。
保存后关闭窗口。
Enable Asynchronous Programmers' model support:目前推荐关闭 Enable anti-alias support for all transform operations:使能旋转、缩放操作的抗锯齿功能 Enable Support for accessing individual Colour channels:当你的目标屏幕是 RGB888,而你又需要支持 PNG 图片时推荐打开。
保持默认选项 在完成应用后依次尝试:如果对应用没有明显的影响,就勾选以换取一定的性能提升。
在 main() 函数所在的源代码文件中包含头文件:
#include "arm_2d_helper.h"
int main(void)
{
system_init(); // 包括 LCD 在内的系统初始化
...
arm_irq_safe {
arm_2d_init(); // 初始化 arm-2d
}
...
while(1) {
...
}
}
int main(void)
{
system_init(); // 包括 LCD 在内的系统初始化
...
arm_irq_safe {
arm_2d_init(); // 初始化 arm-2d
}
// 初始化 Display Adapter 0
disp_adapter0_init();
while (true) {
...
// 执行 Display Adapter 的刷新任务
disp_adapter0_task();
...
}
}
【常见问题】
问题一:安装 CMSIS-Pack 时失败
值得强调的是,MDK为开源社区提供了 Community 版本,除了不能商用,几乎没有任何限制(对芯片、代码尺寸、调试均没有限制)。Community 版本的本质是一种 License,使用的安装文件与其它版本并无不同。对这一“官方白嫖版”感兴趣的小伙伴可以通过下面的链接来获取:
https://www.keil.arm.com/mdk-community/
如果你的运气特别差,安装了最新MDK也无法解决上述问题,还可以通过Pack-Installer的导入功能最后“搏一搏”——打开 Pack Installer 后依次单击 File->Import:
如果还不能解决,请确认你的 MDK 是否安装在默认的安装目录下(C:\Keil_v5),如果不是,尝试重新安装到默认目录下。
再不行……再不行就换台电脑吧。
这是由于工程中自带的cmsis版本太低,且与RTE所部署的cmsis冲突导致的。具体解决方法请参考文章《CMSIS玩家的“阴间成就”指南》。
这类问题是由于你的 MDK 工程中存在独立的 CMSIS,且该 CMSIS 与 RTE中所添加的 CMSIS 存在冲突(工程中的 CMSIS 版本过于老旧),具体解决方案请参考文章《CMSIS玩家的“阴间成就”指南》,这里就不再赘述。
此外,要检查你是否正确开启了 GNU 扩展和对应的C标准(Arm Compiler 5要开启 C99,Arm Compiler 6要开启 gnu99)
问题四:提示找不到__aeabi_assert
#include "arm_2d.h"
#include "cmsis_compiler.h"
#if defined(__MICROLIB)
void __aeabi_assert(const char *chCond, const char *chLine, int wErrCode)
{
ARM_2D_UNUSED(chCond);
ARM_2D_UNUSED(chLine);
ARM_2D_UNUSED(wErrCode);
while(1) {
__NOP();
}
}
#endif
问题四:提示找不到 Disp0_DrawBitmap
此时,我们可以在 Acceleration 中看到添加的代码文件:
注意到这里每个文件后面都有一个对应的数字,指代对应的 Display Adapter 模板。而每个 Display Adapter 都需要一个属于自己的底层刷新函数:Dispn_DrawBitmap(),具体请参考本文的【准备工作】章节。
问题五:出现Hardfault
检查栈(Stack)的大小,推荐在 0xC00(3K)以上为易,HEAP在1K以上。
【说在后面的话】
每个场景都由(可选的)背景和前景组成 用户可以 事先设定好一连串场景然后依次切换 也可以在运行时刻通过API追加(Append)新的场景 场景的切换会自动避免帧撕裂的问题
虽然 arm_2d_disp_adapter_0.c 已经为我们演示了场景播放器的使用,但为了降低大家的学习门槛,我将在下一篇文章中详细为大家介绍这种“基于场景”的低成本GUI设计方式。
如果你喜欢我的思维、觉得我的文章对你有所启发,
请务必 “点赞、收藏、转发” 三连,这对我很重要!谢谢!
欢迎订阅 裸机思维