自定义平台移植

本文档提供从零开始将 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(禁用),仅在需要时启用,不引入额外开销。

说明

EGUI_CONFIG_PLATFORM_CUSTOM_MALLOC

启用后,malloc/free 字段加入 ops;可替换为 RTOS heap 或内存池

EGUI_CONFIG_PLATFORM_CUSTOM_PRINTF

启用后,vlog/vsprintf 字段加入 ops;可重定向日志输出到 UART、RTT 等

EGUI_CONFIG_PLATFORM_CUSTOM_MEMORY_OP

启用后,memset_fast/memcpy_fast 字段加入 ops;可接入 DMA 或 SRAM 优化实现

当宏为 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

常见问题排查

屏幕无显示

  1. 检查 LCD 供电和背光

  2. 检查 SPI 连线(MOSI、SCK、CS、DC、RST)

  3. 用逻辑分析仪确认 SPI 有数据输出

  4. 检查 LCD 初始化命令序列是否正确

屏幕花屏

  1. EGUI_CONFIG_COLOR_16_SWAP:SPI 8-bit 模式下通常需要设为 1;如果不同屏幕需求不同,优先改为 setup.render_config->color_16_swap

  2. 检查 draw_area 中的坐标计算

  3. 优先确认 PFB 宽高选成了屏幕宽高的整数约数

画面撕裂

  1. 启用帧同步(TE 信号)

  2. 或使用双缓冲 + DMA 异步传输

帧率低

  1. 增大 PFB 尺寸减少 draw_area 调用次数

  2. 提高 SPI 时钟频率

  3. 启用 DMA 异步传输

  4. 启用双缓冲让 CPU 和 DMA 并行

动画不流畅

  1. 检查 get_tick_ms 返回值是否单调递增

  2. 确认 SysTick 中断正常工作

  3. 检查是否有其他中断长时间占用 CPU

RAM 不足

  1. 减小 PFB 尺寸

  2. 使用 Page union 模式共享控件内存

  3. 关闭不需要的功能模块

  4. 将大资源放到外部存储

编译错误

  1. 确认 app_egui_config.h 在 include 路径中

  2. 确认 build.mk 正确包含了源文件和头文件路径

  3. 检查工具链版本兼容性(需要 C99 支持)