什么是ARM2D
Arm在Github上发布了一个专门针对“全体” Cortex-M处理器的2D图形加速库——Arm-2D
我们可以简单的把这个2D图形加速库理解为是一个专门针对Cortex-M处理器的标准“显卡驱动”。虽然这里的“显卡驱动”只是一个夸张的说法——似乎没有哪个Cortex-M处理器“配得上”所谓的显卡,但其实也并没有差多远——因为根据最新的趋势,随着单片机资源的逐步丰富(较高级的工艺节点正在逐步降价),处理器不仅跑得越来越快、存储器越来越大,而且大量的厂商已经或者正在考虑给Cortex-M处理器配备专属的2D图形加速引擎
以上摘自公众号裸机思维的文章
首先,arm2d是一个2d引擎库,他是纯软件的东西。很多人可能会被它的arm2d名字给误导。分不清arm2d和DMA2D。实际上DMA2D是硬件,arm2d则是一个软件。arm2d的优秀性能,让我瞠目结舌。在裸机思维的文章中,你不难看到诸如M0+内核、25M主频的主控的平台上跑出各种逆天的效果, 这也是它吸引我的原因。虽然arm开发的初衷是服务于自家的硬件,但是不意味着它不能够移植到别的平台。
以下是我摸索并熟悉arm2d的移植过程
移植前的准备
首先,我们是基于esp-idf 5.0的sdk做的移植。那么,第一需要的肯定是安装环境。这里参考官方手册
就不多赘述。
接下来就应该准备一份驱屏的基础代码了。我们准备了一块esp32s3的开发板,其中屏幕使用了st7789的240X240的spi屏幕。
屏幕驱动
我喜欢以esp-iot-solution中的bus和screen为基础写屏幕驱动。bus中提供了诸如:spi i2c 8080 rgb的通讯层封装,而screen则基于bus的封装提供了st7789 ili9341等lcd芯片封装。
这使得驱屏变得异常简单
cp -r esp-idf/example/get-started/sample_project ./
cd sample_project
新建components文件夹,再复制刚才提到的bus和screen组件放到components文件夹下。接下来开始着手写屏幕驱动代码:
/**
* @file arm_math.h
* @author cangyu (sky.kirto@qq.com)
* @brief
* @version 0.1
* @date 2024-06-06
*
* @copyright Copyright (c) 2024, CorAL. All rights reserved.
*
*/
#include <stdio.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "screen_driver.h"
#include "esp_log.h"
/* ==================== [Defines] =========================================== */
#define BOARD_IO_SPI2_MISO -1
#define BOARD_IO_SPI2_MOSI 11
#define BOARD_IO_SPI2_SCK 12
#define BOARD_LCD_SPI_CS_PIN 10
#define BOARD_LCD_SPI_DC_PIN 9
#define BOARD_LCD_SPI_RESET_PIN -1
#define BOARD_LCD_SPI_BL_PIN 46
#define BOARD_LCD_SPI_CLOCK_FREQ 40000000
/* ==================== [Typedefs] ========================================== */
/* ==================== [Static Prototypes] ================================= */
static void screen_clear(scr_driver_t *lcd, int color);
/* ==================== [Static Variables] ================================== */
static const char *TAG = "screen example";
static scr_driver_t g_lcd;
/* ==================== [Macros] ============================================ */
/* ==================== [Global Functions] ================================== */
void app_main(void)
{
spi_config_t bus_conf = {
.miso_io_num = BOARD_IO_SPI2_MISO,
.mosi_io_num = BOARD_IO_SPI2_MOSI,
.sclk_io_num = BOARD_IO_SPI2_SCK,
.max_transfer_sz = 1024*10
};
spi_bus_handle_t spi2_bus_handle = spi_bus_create(SPI2_HOST, &bus_conf);
scr_interface_spi_config_t spi_lcd_cfg = {
.spi_bus = spi2_bus_handle,
.pin_num_cs = BOARD_LCD_SPI_CS_PIN,
.pin_num_dc = BOARD_LCD_SPI_DC_PIN,
.clk_freq = BOARD_LCD_SPI_CLOCK_FREQ,
.swap_data = true,
};
scr_interface_driver_t *iface_drv;
scr_interface_create(SCREEN_IFACE_SPI, &spi_lcd_cfg, &iface_drv);
scr_find_driver(SCREEN_CONTROLLER_ST7789, &g_lcd);
scr_controller_config_t lcd_cfg = {
.interface_drv = iface_drv,
.pin_num_rst = BOARD_LCD_SPI_RESET_PIN,
.pin_num_bckl = BOARD_LCD_SPI_BL_PIN,
.rst_active_level = 0,
.bckl_active_level = 1,
.offset_hor = 0,
.offset_ver = 0,
.width = 240,
.height = 240,
.rotate = SCR_DIR_LRTB,
};
g_lcd.init(&lcd_cfg);
scr_info_t lcd_info;
g_lcd.get_info(&lcd_info);
ESP_LOGI(TAG, "Screen name:%s | width:%d | height:%d", lcd_info.name,
lcd_info.width, lcd_info.height);
screen_clear(&g_lcd, COLOR_GREEN);
}
/* ==================== [Static Functions] ================================== */
static void screen_clear(scr_driver_t *lcd, int color)
{
scr_info_t lcd_info;
lcd->get_info(&lcd_info);
uint16_t *buffer = malloc(lcd_info.width * sizeof(uint16_t));
for (size_t i = 0; i < lcd_info.width; i++) {
buffer[i] = color;
}
for (int y = 0; y < lcd_info.height; y++) {
lcd->draw_bitmap(0, y, lcd_info.width, 1, buffer);
}
free(buffer);
}
接下来就是编译烧录的事情了
idf.py set-target esp32s3 # 切换芯片
idf.py build # 编译代码
idf.py flash # 烧录
idf.py monitor # 显示串口log
这是运行的效果
如此,我们便得到了一个干净的驱屏的工程。
ARM2D的组件加入
我们在components文件夹下面创建一个arm2d的组件文件夹。再在arm2d文件夹里clone arm2d的仓库
cd components # 进入组件文件夹
mkdir arm2d # 创建arm2d组件件夹
cd arm2d # 进入arm2d组件文件夹
git clone https://github.com/ARM-software/Arm-2D.git # clone arm2d仓库
arm2d的仓库里面很多文件夹,很多文件。我们首要的就是要弄清楚哪些是我们需要的。我们需要的文件主要分布在Library中和Helper中。其中Library是核心部分,而Helper则是后续添加的有帮助的部分。我们在arm2d里面创建一个CMakeLists.txt用于添加编译
touch CMakeLists.txt
CMakeLists.txt内容如下:
idf_component_register(SRC_DIRS "Arm-2D/Library/Source" "Arm-2D/Helper/Source"
INCLUDE_DIRS "Arm-2D/Library/Include" "Arm-2D/Helper/Include"
)
接下来就是退出到工程根目录开始启动上述的编译。
不出所料发生了报错, 找不到arm_2d_cfg.h。那我们在arm2d的文件夹下面添加它
通过搜索这个文件名,我们发现在components/arm2d/Arm-2D/Library/Include/template路径下是有一个同名的config文件。我们把内容复制粘贴过来。大致看一遍配置,值得注意的是,GLCD_CFG_SCEEN_WIDTH 和 GLCD_CFG_SCEEN_HEIGHT 是屏幕的宽和高,别忘记改成我们的屏幕大小:240*240
修改后,别忘记CMakeLists.txt也要修改:
idf_component_register(SRC_DIRS "Arm-2D/Library/Source" "Arm-2D/Helper/Source"
INCLUDE_DIRS "." "Arm-2D/Library/Include" "Arm-2D/Helper/Include"
)
再次进行编译,果然没那么简单。这里告诉我们缺少arm_2d_user_arch_port.h文件。哎,之前的tamplate文件夹里好像有。直接复制过来。再次进行编译。
然后发现缺少arm_math.h文件。这个文件比较棘手,是arm的dsp库。arm2d为了加速图形计算,使用了很多arm的dsp库来加速。我们的esp32s3不是arm架构的根本没法使用。这下只能自己写一个arm_math.h文件,将arm2d内部依赖arm-dsp库的内容提取出来, 并简单的替代。这个过程比较费时费力,需要从报错中找到源头,然后从arm2d中理解。使用math.h进行替换。这里我直接放出我最终的arm_math.h:
/**
* @file arm_math.h
* @author cangyu (sky.kirto@qq.com)
* @brief
* @version 0.1
* @date 2024-06-06
*
* @copyright Copyright (c) 2024, CorAL. All rights reserved.
*
*/
#ifndef __ARM_MATH_H__
#define __ARM_MATH_H__
/* ==================== [Includes] ========================================== */
#include <math.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ==================== [Defines] =========================================== */
/* ==================== [Typedefs] ========================================== */
typedef int16_t q15_t;
typedef int32_t q31_t;
typedef int64_t q63_t;
/* ==================== [Global Prototypes] ================================= */
__STATIC_FORCEINLINE q31_t clip_q63_to_q31(q63_t x)
{
return ((q31_t) (x >> 32) != ((q31_t) x >> 31)) ?
((0x7FFFFFFF ^ ((q31_t) (x >> 63)))) : (q31_t) x;
}
__STATIC_FORCEINLINE float arm_sin_f32(float x)
{
return sin(x);
}
__STATIC_FORCEINLINE float arm_cos_f32(float x)
{
return cos(x);
}
__STATIC_FORCEINLINE q31_t arm_sin_q31(q31_t x)
{
return (q31_t)sin((float)x);
}
__STATIC_FORCEINLINE q31_t arm_cos_q31(q31_t x)
{
return (q31_t)cosl((float)x);
}
__STATIC_FORCEINLINE uint32_t usat(int32_t val, uint8_t sat) {
uint32_t max = (1U << sat) - 1; // 最大值为 2^sat - 1
if (val < 0) {
return 0;
} else if (val > max) {
return max;
} else {
return (uint32_t)val;
}
}
__STATIC_FORCEINLINE int32_t saturate_to_int32(int64_t value) {
if (value > INT32_MAX) {
return INT32_MAX;
} else if (value < INT32_MIN) {
return INT32_MIN;
} else {
return (int32_t)value;
}
}
__STATIC_FORCEINLINE int32_t qadd_impl(int32_t x, int32_t y) {
int64_t result = (int64_t)x + y; // 将x和y相加
return saturate_to_int32(result); // 对结果进行饱和处理
}
/* ==================== [Macros] ============================================ */
// 计算一个32位整数从最高有效位
#define __CLZ(x) __builtin_clz(x)
// 确保一个数值在给定的位宽内
#define __USAT(val, sat) usat(val, sat)
// 它将两个32位有符号整数相加,并在结果超出32位有符号整数范围时进行饱和处理
#define __QADD(x, y) qadd_impl(x, y)
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif // __ARM_MATH_H__
再度编译发现虽然编译过了,但是很多地方有warning,看着十分难受。这里去请教了arm2d的作者,傻孩子大佬。大致了解了原因后按照他的说法在CMakeLists.txt中加入了两行编译器命令
idf_component_register(SRC_DIRS "Arm-2D/Library/Source" "Arm-2D/Helper/Source"
INCLUDE_DIRS "." "Arm-2D/Library/Include" "Arm-2D/Helper/Include"
)
target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-implicit-fallthrough -Wno-unused-variable)
target_compile_options(${COMPONENT_LIB} PRIVATE -fms-extensions)
这样的话编译就没有警告了,nice!
对接arm2d
arm2d的对接十分曲折,由于arm2d的源代码拥有若干个宏堆砌而成。很难读懂,我也是参考了components/arm2d/Arm-2D/examples/[template][pc][vscode]/platform路径下的arm_2d_disp_adapter_0.h和arm_2d_disp_adapter_0.c拉过来放到arm2d文件夹下。
接了这两个文件的代码后,由于引入了.c和一些esp32的代码,其中arm_2d_disp_adapter_0.c的代码还借用了components/arm2d/Arm-2D/examples/common里面的代码。那么CMakeLists.txt自然也要修改如下:
idf_component_register(SRC_DIRS "." "Arm-2D/Library/Source" "Arm-2D/Helper/Source" "Arm-2D/examples/common/controls" "Arm-2D/examples/common/asset"
INCLUDE_DIRS "." "Arm-2D/Library/Include" "Arm-2D/Helper/Include" "Arm-2D/examples/common/controls" "Arm-2D/examples/common/asset"
)
target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-implicit-fallthrough -Wno-unused-variable)
target_compile_options(${COMPONENT_LIB} PRIVATE -fms-extensions)
然而编译后,发现arm_2d_disp_adapter_0.c 和 arm_2d_disp_adapter_0.h里面的代码都是灰色,原来是缺少RTE_Acceleration_Arm_2D_Helper_Disp_Adapter0宏,搜索arm2d文件发现在 components/arm2d/Arm-2D/examples/[template][pc][vscode]/platform/RTE_Components.h 中有定义,那么拉取到arm2d的文件夹内后,还需要
修改CMakeLists.txt添加一个编译宏 _RTE_ 就可以了
idf_component_register(SRC_DIRS "." "Library/Source" "Helper/Source" "common/controls" "common/asset"
INCLUDE_DIRS "." "Library/Include" "Helper/Include" "common/controls" "common/asset"
)
target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-implicit-fallthrough -Wno-unused-variable)
target_compile_options(${COMPONENT_LIB} PRIVATE -fms-extensions)
target_compile_definitions(${COMPONENT_LIB} PRIVATE "_RTE_" )
这部分的代码真的很难理解,作者十分擅长用宏。在这样的基础之下,写下的代码如同自带一层混淆,让人难以读懂和移植。
然后我们编译后发现缺少 :
void Disp0_DrawBitmap(uint32_t x, uint32_t y, uint32_t width, uint32_t height, const uint8_t *bitmap)
int64_t arm_2d_helper_get_system_timestamp(void)
uint32_t arm_2d_helper_get_reference_clock_frequency(void)
这部分是对接esp32底层,我们留在main里面做
主函数调用arm2d
到了主函数了,首先我们要引入头文件
// arm2d的内容
#include "arm_2d.h"
#include "arm_2d_disp_adapter_0.h"
// 对接需要的内容
#include "esp_timer.h"
然后我们要对接Disp0_DrawBitmap函数,这部分是给arm2d底层刷新屏幕使用
void Disp0_DrawBitmap(uint32_t x, uint32_t y, uint32_t width, uint32_t height, const uint8_t *bitmap)
{
g_lcd.draw_bitmap(x, y, width, height, (uint16_t*)bitmap);
}
对接arm_2d_helper_get_system_timestamp函数,这部分给arm2d提供时间戳:
int64_t arm_2d_helper_get_system_timestamp(void)
{
return esp_timer_get_time();
}
对接arm_2d_helper_get_reference_clock_frequency函数,这部分是时间戳频率:
uint32_t arm_2d_helper_get_reference_clock_frequency(void)
{
return 1000000;
}
然后,主函数下面加入代码:
arm_irq_safe {
arm_2d_init();
}
disp_adapter0_init(Disp0_DrawBitmap);
while (1)
{
disp_adapter0_task();
vTaskDelay(1);
}
开始编译,结果最后的链接阶段报错:
A fatal error occurred: Segment loaded at 0x3c030390 lands in same 64KB flash mapping as segment loaded at 0x3c030020. Can't generate binary. Suggest changing linker script or ELF to merge sections.
ninja: build stopped: subcommand failed.
通过翻译软件我们知道,这里的段错误,好像是冲突了。
我们通过指令xtensa-esp32-elf-objdump -h build/lcd_tjpgd.elf查找了所有的段:
$ xtensa-esp32-elf-objdump -h build/lcd_tjpgd.elf
build/lcd_tjpgd.elf: file format elf32-xtensa-le
Sections:
Idx Name Size VMA LMA File off Algn
0 .rtc.text 00000010 600fe000 600fe000 00054000 2**0
ALLOC
1 .rtc.force_fast 00000000 600fe010 600fe010 0005374f 2**0
CONTENTS
2 .rtc_noinit 00000000 50000000 50000000 0005374f 2**0
CONTENTS
3 .rtc.force_slow 00000000 50000000 50000000 0005374f 2**0
CONTENTS
4 .rtc_reserved 00000018 600fffe8 600fffe8 00053fe8 2**3
ALLOC
5 .iram0.vectors 00000403 40374000 40374000 0001d000 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
6 .iram0.text 0000edbb 40374404 40374404 0001d404 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
7 .dram0.dummy 0000b200 3fc88000 3fc88000 0000f000 2**0
ALLOC
8 .dram0.data 0000255c 3fc93200 3fc93200 0001a200 2**4
CONTENTS, ALLOC, LOAD, DATA
9 .noinit 00000000 3fc9575c 3fc9575c 0005374f 2**0
CONTENTS
10 .dram0.bss 00004040 3fc95760 3fc95760 0001c75c 2**3
ALLOC
11 .flash.text 0002672f 42000020 42000020 0002d020 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .flash_rodata_dummy 00030000 3c000020 3c000020 00001020 2**0
ALLOC
13 .flash.appdesc 00000100 3c030020 3c030020 00001020 2**4
CONTENTS, ALLOC, LOAD, READONLY, DATA
14 arm2d.tile.c_tileWhiteDotMask 00000010 3c030120 3c030120 00001120 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
15 arm2d.tile.c_tileWhiteDotRGB565 00000010 3c030130 3c030130 00001130 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
16 arm2d.asset.c_bmpWhiteDotRGB565 00000188 3c030140 3c030140 00001140 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
17 arm2d.asset.c_bmpWhiteDotAlpha 000000c4 3c0302c8 3c0302c8 000012c8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
18 .flash.rodata 0000d01c 3c030390 3c030390 00001390 2**4
CONTENTS, ALLOC, LOAD, DATA
19 .flash.rodata_noload 00000000 3c03d3ac 3c03d3ac 0005374f 2**0
CONTENTS
20 .ext_ram.dummy 0003ffe0 3c000020 3c000020 00001020 2**0
ALLOC
21 .ext_ram.bss 00000000 3c040000 3c040000 0005374f 2**0
CONTENTS
22 .iram0.text_end 00000041 403831bf 403831bf 0002c1bf 2**0
ALLOC
23 .iram0.data 00000000 40383200 40383200 0005374f 2**0
CONTENTS
24 .iram0.bss 00000000 40383200 40383200 0005374f 2**0
CONTENTS
25 .dram0.heap_start 00000000 3fc997a0 3fc997a0 0005374f 2**0
CONTENTS
26 .xt.prop 0002c2f8 00000000 00000000 0005374f 2**0
CONTENTS, READONLY
27 .xt.lit 000013a0 00000000 00000000 0007fa47 2**0
CONTENTS, READONLY
28 .xtensa.info 00000038 00000000 00000000 00080de7 2**0
CONTENTS, READONLY
29 .comment 0000004b 00000000 00000000 00080e1f 2**0
CONTENTS, READONLY
30 .debug_frame 00013cd8 00000000 00000000 00080e6c 2**2
CONTENTS, READONLY, DEBUGGING, OCTETS
31 .debug_info 001d8e2e 00000000 00000000 00094b44 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
32 .debug_abbrev 00028366 00000000 00000000 0026d972 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
33 .debug_loc 000d92c3 00000000 00000000 00295cd8 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
34 .debug_aranges 00007910 00000000 00000000 0036efa0 2**3
CONTENTS, READONLY, DEBUGGING, OCTETS
35 .debug_ranges 0000f150 00000000 00000000 003768b0 2**3
CONTENTS, READONLY, DEBUGGING, OCTETS
36 .debug_line 00176594 00000000 00000000 00385a00 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
37 .debug_str 000474ad 00000000 00000000 004fbf94 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
38 .debug_loclists 0000f07c 00000000 00000000 00543441 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
39 .debug_rnglists 00000418 00000000 00000000 005524bd 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
40 .debug_line_str 00001955 00000000 00000000 005528d5 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
定位了问题出在arm2d里面,好像是arm2d的某个操作导致了内存段覆盖。搜索关键词arm2d.tile,发现很多地方使用了ARM_SECTION(“arm2d.tile.c_tileUTF8UserFontA1Mask”)。知道这里很简单,找到根源然后将它注释,这个宏就不会起作用了。
大约是在components/arm2d/Arm-2D/Library/Include/arm_2d_utils.h文件的620行我找到了这个宏,并在
arm_2d_cfg.h中加入宏定义来替换掉内部的宏:
// 屏蔽内部的段操作
#define ARM_SECTION(__X)
至此,我们编译终于成功。虽然这时候还有一些warning没有消除(arm2d被调用的时候产生的warning)。但是我也无力追求完美了。直接编译,烧录。
结果没有出现想要的动画效果,这里我们通过在arm_2d_cfg.h中打开log分析发现,是因为没有启动arm_2d_disp_adapter_0.h中的默认界面。我们通过arm_2d_disp_adapter_0.h的下面宏:
// <q>Disable the default scene
// <i> Remove the default scene for this display adapter. We highly recommend you to disable the default scene when creating real applications.
#ifndef __DISP0_CFG_DISABLE_DEFAULT_SCENE__
# define __DISP0_CFG_DISABLE_DEFAULT_SCENE__ 0
#endif
这里默认__DISP0_CFG_DISABLE_DEFAULT_SCENE__ 是1,我们设置成0,打开它。再进行编译烧录(别忘记arm_2d_cfg.h中关闭log)。结果如下:
总结
其实我的结果并不是很好。按照傻孩子大佬的话说,还是有很大优化空间。下一步优化应该就是在spi异步传输的方向上。乐鑫的spi分为queue传输和poll传输,其中bus库采用的是poll传输。然而这种方式相当于同步操作,应该用queue去异步等待,这样能在传输的同时计算像素。能够消除LCD-Latency的时间。我这次记录摸索过程相当于是抛砖引玉,希望大家能够优化出更好的版本
关于移植
移植的一个最大的准则就是”不要动别人的源码“。按照傻孩子大佬的说法就是【用扩展替代修改】。我遇到问题,虽然会深入源码,但是会根据源码的情况在配置文件或者是自己写的文件里面进行补充。如果别人的代码让你无法这样操作,那就是提issue的时候。
后记(碎碎念)
arm2d的理念是以mask为中心,所有的东西全都是贴图加上arm2d自带的蒙版(mask)完成的效果。这个理念实际上不是gui的理念,例如lvgl是以控件为中心。arm2d则是更加底层, 从使用者的角度实际上会比较麻烦,但是这种效果能够在性能有限的设备上发挥很大的效果。实际上arm2d的源码一度让我崩溃,以宏构建的内容, 很多情况下无从知晓如何使用。代码的抽象程度已经完全是另一种语言。这次的移植意义也不大,因为esp32性能足够,有更加有好的lvgl,没必要折腾arm2d。主要是想学习学习,也想挑战一下自己。arm2d的代码在我看来我不能评价他是不好的,毕竟恐怖的效率,惊人的效果还是深深折服。但是我个人还是不会学习他的做法,我希望能写出更加清晰易懂的代码。