自定义平台移植¶
本文档提供从零开始将 EmbeddedGUI 移植到新硬件平台的完整步骤。
移植步骤清单¶
第一步:创建移植目录¶
复制空模板作为起点:
cp -r porting/stm32g0 porting/your_platform
目录结构:
porting/your_platform/
├── Porting/
│ ├── egui_port_mcu.c # Display + Platform Driver 注册
│ └── port_main.c # 主循环入口
├── app_egui_config.h # 配置覆盖(放在 example 目录下)
└── build.mk # 构建模块定义
第二步:配置屏幕参数¶
在你的示例目录下创建 app_egui_config.h:
#ifndef _APP_EGUI_CONFIG_H_
#define _APP_EGUI_CONFIG_H_
#ifdef __cplusplus
extern "C" {
#endif
// 屏幕尺寸
#define EGUI_CONFIG_SCREEN_WIDTH 240
#define EGUI_CONFIG_SCREEN_HEIGHT 320
// 色深
#define EGUI_CONFIG_COLOR_DEPTH 16 // RGB565
// PFB 大小(建议优先选为屏幕尺寸的整数约数)
#define EGUI_CONFIG_PFB_WIDTH 30 // 240 / 8
#define EGUI_CONFIG_PFB_HEIGHT 40 // 320 / 8
// RGB565 字节交换默认值(SPI 8-bit 接口常用)
#define EGUI_CONFIG_COLOR_16_SWAP 0
#ifdef __cplusplus
}
#endif
#endif
第三步:实现 Display Driver¶
在 egui_port_mcu.c 中实现显示驱动:
#include "egui.h"
// --- Display Driver ---
static void my_display_init(egui_core_t *core)
{
EGUI_UNUSED(core);
// 初始化 LCD 硬件(SPI、GPIO、LCD 控制器)
lcd_hw_init();
}
static void my_display_draw_area(egui_core_t *core, int16_t x, int16_t y, int16_t w, int16_t h, const egui_color_int_t *data)
{
EGUI_UNUSED(core);
lcd_set_window(x, y, x + w - 1, y + h - 1);
lcd_write_data((const uint8_t *)data, w * h * sizeof(egui_color_int_t));
}
static void my_display_flush(egui_core_t *core)
{
EGUI_UNUSED(core);
// 如果 LCD 需要手动触发刷新,在此处理
// 大多数 SPI LCD 不需要,留空即可
}
static const egui_display_driver_ops_t my_display_ops = {
.init = my_display_init,
.draw_area = my_display_draw_area,
.wait_draw_complete = NULL, // draw_area 是同步阻塞的,无需等待
.flush = my_display_flush,
.set_brightness = NULL,
.set_power = NULL,
.set_rotation = NULL,
.fill_rect = NULL,
.blit = NULL,
.blend = NULL,
.wait_vsync = NULL,
};
static egui_display_driver_t my_display = {
.ops = &my_display_ops,
.physical_width = EGUI_CONFIG_SCREEN_WIDTH,
.physical_height = EGUI_CONFIG_SCREEN_HEIGHT,
.rotation = EGUI_DISPLAY_ROTATION_0,
.brightness = 255,
.power_on = 1,
};
第四步:实现 Platform Driver¶
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
static void my_assert_handler(const char *file, int line)
{
printf("ASSERT: %s:%d\n", file, line);
while (1) { }
}
static uint32_t my_get_tick_ms(void)
{
return HAL_GetTick(); // replace with your platform API
}
static void my_delay(uint32_t ms)
{
HAL_Delay(ms);
}
static egui_base_t my_interrupt_disable(void)
{
__disable_irq();
return 0;
}
static void my_interrupt_enable(egui_base_t level)
{
(void)level;
__enable_irq();
}
// Enable EGUI_CONFIG_PLATFORM_CUSTOM_PRINTF=1 in build flags to route
// log output and sprintf through the platform ops.
#if EGUI_CONFIG_PLATFORM_CUSTOM_PRINTF
static void my_vlog(const char *format, va_list args)
{
vprintf(format, args); // or output via UART
}
static void my_vsprintf(char *str, const char *format, va_list args)
{
vsprintf(str, format, args);
}
#endif
// Enable EGUI_CONFIG_PLATFORM_CUSTOM_MEMORY_OP=1 to route all internal
// memset/memcpy calls through the ops, e.g. via DMA or SRAM-optimized routine.
#if EGUI_CONFIG_PLATFORM_CUSTOM_MEMORY_OP
static void my_memset_fast(void *s, int c, int n)
{
memset(s, c, n); // replace with DMA or platform-optimized implementation
}
static void my_memcpy_fast(void *dst, const void *src, int n)
{
memcpy(dst, src, n); // replace with DMA or platform-optimized implementation
}
#endif
static const egui_platform_ops_t my_platform_ops = {
.assert_handler = my_assert_handler,
.delay = my_delay,
.get_tick_ms = my_get_tick_ms,
.interrupt_disable = my_interrupt_disable,
.interrupt_enable = my_interrupt_enable,
.load_external_resource = NULL,
.mutex_create = NULL,
.mutex_lock = NULL,
.mutex_unlock = NULL,
.mutex_destroy = NULL,
.timer_start = NULL,
.timer_stop = NULL,
.watchdog_feed = NULL,
// Enable EGUI_CONFIG_PLATFORM_CUSTOM_PRINTF=1 to route log/sprintf through ops.
#if EGUI_CONFIG_PLATFORM_CUSTOM_PRINTF
.vlog = my_vlog,
.vsprintf = my_vsprintf,
#endif
// Enable EGUI_CONFIG_PLATFORM_CUSTOM_MALLOC=1 to route heap through ops.
#if EGUI_CONFIG_PLATFORM_CUSTOM_MALLOC
.malloc = NULL,
.free = NULL,
#endif
// Enable EGUI_CONFIG_PLATFORM_CUSTOM_MEMORY_OP=1 to route memset/memcpy through ops.
#if EGUI_CONFIG_PLATFORM_CUSTOM_MEMORY_OP
.memset_fast = my_memset_fast,
.memcpy_fast = my_memcpy_fast,
#endif
};
static egui_platform_t my_platform = {
.ops = &my_platform_ops,
};
第五步:注册驱动和主循环¶
void egui_port_init(egui_core_t *core)
{
egui_platform_register(core, &my_platform);
}
static egui_color_int_t egui_pfb[EGUI_CONFIG_PFB_WIDTH * EGUI_CONFIG_PFB_HEIGHT];
void port_main(void)
{
egui_core_t core;
egui_color_int_t *pfb_bufs[1] = { egui_pfb };
egui_display_setup_t setup;
egui_port_init(&core);
setup.screen_width = EGUI_CONFIG_SCREEN_WIDTH;
setup.screen_height = EGUI_CONFIG_SCREEN_HEIGHT;
setup.pfb_width = EGUI_CONFIG_PFB_WIDTH;
setup.pfb_height = EGUI_CONFIG_PFB_HEIGHT;
setup.pfb_buffers = pfb_bufs;
setup.pfb_buffer_count = 1;
setup.display_driver = &my_display;
setup.render_config = NULL;
setup.touch_register = NULL; // 有触摸时填 egui_port_register_touch_driver
setup.uicode_init = uicode_disp0_init;
setup.display_id = 0;
egui_setup_display(&core, &setup);
while (1)
{
egui_polling_work(&core);
}
}
第六步:配置构建系统¶
build.mk:
EGUI_CODE_SRC += porting/your_platform/Porting/egui_port_mcu.c
EGUI_CODE_SRC += porting/your_platform/Porting/port_main.c
EGUI_CODE_INCLUDE += -Iporting/your_platform
# Optional platform override macros (default 0, no overhead when disabled):
# EGUI_CONFIG_PLATFORM_CUSTOM_PRINTF=1 Route vlog/vsprintf through ops
# EGUI_CONFIG_PLATFORM_CUSTOM_MALLOC=1 Route malloc/free through ops
# EGUI_CONFIG_PLATFORM_CUSTOM_MEMORY_OP=1 Route memset/memcpy through ops
EGUI_CFLAGS += -DEGUI_CONFIG_PLATFORM_CUSTOM_PRINTF=1
# EGUI_CFLAGS += -DEGUI_CONFIG_PLATFORM_CUSTOM_MALLOC=1
# EGUI_CFLAGS += -DEGUI_CONFIG_PLATFORM_CUSTOM_MEMORY_OP=1
平台扩展宏说明¶
EmbeddedGUI 提供三个独立的平台扩展宏,控制是否通过 egui_platform_ops_t 注册特定回调。
所有宏默认为 0(禁用),仅在需要时启用,不引入额外开销。
宏 |
说明 |
|---|---|
|
启用后, |
|
启用后, |
|
启用后, |
当宏为 0 时,对应 ops 字段不存在(编译期消除),porting 层无需注册。
当宏为 1 时,所有内部 memset/memcpy 调用都会经过 egui_api_memset/egui_api_memcpy 并转发到注册的回调;若回调为 NULL,则回退到标准库。
调试检查点¶
移植过程中,建议按以下顺序逐步验证,每一步确认正确后再进入下一步。
检查点 1:纯色填充¶
验证 draw_area 基本工作。在 uicode_disp0_init 中不创建任何控件,框架会用背景色(黑色)清屏。
预期结果:屏幕显示纯黑色。
如果屏幕花屏或无显示:
检查 SPI 时序和 GPIO 配置
检查 LCD 初始化命令序列
检查
EGUI_CONFIG_COLOR_16_SWAP这个默认值是否需要设为 1;多屏或异构屏更推荐通过setup.render_config->color_16_swap按屏覆盖用示波器或逻辑分析仪检查 SPI 信号
检查点 2:矩形绘制¶
创建一个简单的彩色视图:
static egui_view_t test_view;
void uicode_disp0_init(egui_core_t *core)
{
egui_view_init(EGUI_VIEW_OF(&test_view), core);
egui_view_set_position(EGUI_VIEW_OF(&test_view), 50, 50);
egui_view_set_size(EGUI_VIEW_OF(&test_view), 100, 100);
egui_view_set_bg_color(EGUI_VIEW_OF(&test_view), EGUI_COLOR_RED, EGUI_ALPHA_100);
egui_core_add_user_root_view(core, EGUI_VIEW_OF(&test_view));
}
预期结果:屏幕上显示一个红色矩形。
如果矩形位置或大小不对:
检查
draw_area中的坐标映射检查
EGUI_CONFIG_SCREEN_WIDTH/HEIGHT是否与实际屏幕匹配检查 LCD 的 CASET/RASET 命令参数
检查点 3:文本渲染¶
添加一个 Label 控件:
static egui_view_label_t label;
EGUI_VIEW_LABEL_PARAMS_INIT(label_params, 10, 10, 200, 30,
"Hello EGUI!", NULL, EGUI_COLOR_WHITE, EGUI_ALPHA_100);
void uicode_disp0_init(egui_core_t *core)
{
egui_view_label_init_with_params(EGUI_VIEW_OF(&label), core, &label_params);
egui_core_add_user_root_view(EGUI_VIEW_OF(&label));
}
预期结果:屏幕上显示白色文本 “Hello EGUI!”。
如果文本不显示或乱码:
确认字体资源已正确生成和链接
检查
app_egui_resource_generate.h中的字体声明
检查点 4:触摸输入¶
如果有触摸屏,添加一个按钮测试触摸响应:
static egui_view_button_t button;
void on_button_click(egui_view_t *self)
{
EGUI_LOG_INF("Button clicked!\n");
}
void uicode_disp0_init(egui_core_t *core)
{
egui_view_button_init(EGUI_VIEW_OF(&button), core);
egui_view_set_position(EGUI_VIEW_OF(&button), 50, 50);
egui_view_set_size(EGUI_VIEW_OF(&button), 120, 40);
egui_view_set_on_click_listener(EGUI_VIEW_OF(&button), on_button_click);
egui_core_add_user_root_view(core, EGUI_VIEW_OF(&button));
}
预期结果:点击按钮区域时,串口输出 “Button clicked!”。
如果触摸无响应:
检查触摸 IC 的 I2C 通信
检查触摸坐标是否与屏幕坐标系一致
确认
EGUI_CONFIG_FUNCTION_SUPPORT_TOUCH为 1
常见问题排查¶
屏幕无显示¶
检查 LCD 供电和背光
检查 SPI 连线(MOSI、SCK、CS、DC、RST)
用逻辑分析仪确认 SPI 有数据输出
检查 LCD 初始化命令序列是否正确
屏幕花屏¶
EGUI_CONFIG_COLOR_16_SWAP:SPI 8-bit 模式下通常需要设为 1;如果不同屏幕需求不同,优先改为setup.render_config->color_16_swap检查
draw_area中的坐标计算优先确认 PFB 宽高选成了屏幕宽高的整数约数
画面撕裂¶
启用帧同步(TE 信号)
或使用双缓冲 + DMA 异步传输
帧率低¶
增大 PFB 尺寸减少
draw_area调用次数提高 SPI 时钟频率
启用 DMA 异步传输
启用双缓冲让 CPU 和 DMA 并行
动画不流畅¶
检查
get_tick_ms返回值是否单调递增确认 SysTick 中断正常工作
检查是否有其他中断长时间占用 CPU
RAM 不足¶
减小 PFB 尺寸
使用 Page union 模式共享控件内存
关闭不需要的功能模块
将大资源放到外部存储
编译错误¶
确认
app_egui_config.h在 include 路径中确认
build.mk正确包含了源文件和头文件路径检查工具链版本兼容性(需要 C99 支持)