什么是Arm-2D
Arm在Github上发布了一个专门针对“全体” Cortex-M处理器的2D图形加速库——Arm-2D。我们可以简单的把这个2D图形加速库理解为是一个专门针对Cortex-M处理器的标准“显卡驱动”。虽然这里的“显卡驱动”只是一个夸张的说法——似乎没有哪个Cortex-M处理器“配得上”所谓的显卡,但其实也并没有差多远——因为根据最新的趋势,随着单片机资源的逐步丰富(较高级的工艺节点正在逐步降价),处理器不仅跑得越来越快、存储器越来越大,而且大量的厂商已经或者正在考虑给Cortex-M处理器配备专属的2D图形加速引擎
GorgonMeducer 傻孩子,公众号:裸机思维【玩转Arm-2D】入门和移植从未如此简单
移植前的准备
屏幕驱动
cp -r esp-idf/example/get-started/sample_project ./
cd sample_project
/**
* @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
这是运行的效果
如此,我们便得到了一个干净的驱屏的工程。
加入Arm-2D组件
cd components # 进入组件文件夹
mkdir arm2d # 创建arm2d组件件夹
cd arm2d # 进入arm2d组件文件夹
git clone https://github.com/ARM-software/Arm-2D.git # clone arm2d仓库
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文件夹里好像有。直接复制过来。再次进行编译。
/**
* @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__
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!
对接Arm-2D
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)
傻孩子注:
这部分的代码真的很难理解,作者十分擅长用宏。在这样的基础之下,写下的代码如同自带一层混淆,让人难以读懂和移植。然后我们编译后发现缺少 :
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的内容
// 对接需要的内容
然后我们要对接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 1000000ul;
}
然后,主函数下面加入代码:
...
arm_irq_safe {
arm_2d_init();
}
disp_adapter0_init();
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
...
// 屏蔽内部的段操作
...
至此,我们编译终于成功。虽然这时候还有一些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.
这里可能__DISP0_CFG_DISABLE_DEFAULT_SCENE__ 是1,我们确保其设置成0——即打开它。再进行编译烧录(别忘记arm_2d_cfg.h中关闭log)。结果如下:
总结
关于移植
后记(碎碎念)
其实LVGL也在文档中强调,其底层刷新的核心就是“活用mask”。
Masking is the basic concept of LVGL's draw engine. To use LVGL it's not required to know about the mechanisms described here but you might find interesting to know how drawing works under hood. Knowing about masking comes in handy if you want to customize drawing.
https://lvgl.100ask.net/master/overview/draw.html#masking
傻孩子注:
Arm-2D的确提供了很多语法辅助(语法糖),如何看待它决定了你如何对待他。 如果你把它看做是脚本语言的关键字,那么你就会把这些语法糖当做黑盒子——只会专注于它的使用语法和注意事项。关于脚本语言的关键字,只要有文档,就不存在可读性的问题,不是么? 如果你把它看做是宏,那么你就忍不住要去将其展开。这时候,你所专注的就不是Arm-2D的使用,而是Arm-2D是如何构造的。
代码的可读性体现在:单看代码,它是否可以清晰高效的表达自己所要实现的功能。比如:
static
IMPL_PFB_ON_DRAW(__pfb_draw_handler)
{
ARM_2D_PARAM(pTarget);
ARM_2D_PARAM(ptTile);
arm_2d_canvas(ptTile, __top_container) {
arm_2d_align_centre(__top_container, 100, 100) {
draw_round_corner_box( ptTile,
&__centre_region,
GLCD_COLOR_BLACK,
64,
bIsNewFrame);
}
busy_wheel2_show(ptTile, bIsNewFrame);
}
arm_2d_op_wait_async(NULL);
return arm_fsm_rt_cpl;
}
从字面上看,这段“arm-2d脚本语言”的功能即便不借助注释,聪明你一定可以猜个八九不离十:
这似乎是一个绘图事件回调函数。
IMPL_PFB_ON_DRAW(__pfb_draw_handler)
{
...
}
这个函数似乎有两个传入参数:
ARM_2D_PARAM(pTarget);
ARM_2D_PARAM(ptTile);
这段代码为其中一个参数ptTile建立了一个画布,名叫__top_container:
arm_2d_canvas(ptTile, __top_container) {
...
}
代码在画布的中央居中了一个100*100的区域:
arm_2d_align_centre(__top_container, 100, 100) {
...
}
代码通过函数draw_round_corner_box() 绘制了一个圆角矩形区域。
draw_round_corner_box( ptTile,
&__centre_region,
GLCD_COLOR_BLACK,
64,
bIsNewFrame);
其中,__centre_region从名字来看似乎代表了居中的区域,猜测与arm_2d_align_centre有关,GLCD_COLOR_BLACK应该就是圆角矩形的颜色(黑色)。其它参数意义不明,先放一边。
函数busy_wheel2_show()应该是用来绘制类似“死亡小圈圈”的载入效果的。
总结来说,这个绘图函数试图在一个画布的中央绘制一个圆角矩形,然后在圆角矩形上绘制一个载入动画。实际上,上述代码对应的显示效果如下:
不能说如出一辙,至少也是完全一样。
那么?既然脚本语言本身已经能“清晰”的表达所要实现的功能,那么所谓的“可读性”差又体现在哪里呢?