这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » 国产MCU » GD32F503V-EVAL上跑通Zephyr+LVGL的移植实录

共1条 1/1 1 跳转至

GD32F503V-EVAL上跑通Zephyr+LVGL的移植实录

高工
2026-07-04 21:12:36     打赏

摘要:把 Zephyr RTOS 和 LVGL 一起移植到兆易创新 GD32F503V-EVAL 评估板,驱动 ILI9320 240×320 电阻屏,最终跑起 lv_demo_widgets() 演示,并支持 XPT2046 触摸。本文把从点亮 LED 到点亮 GUI 的完整路径、关键坑点、代码组织方式整理出来,供同样想在国产 Cortex-M33 上跑 Zephyr + LVGL 的朋友参考。

关键字:GD32F503、Zephyr、LVGL、ILI9320、EXMC、MIPI-DBI、8080-16bit、XPT2046


一、硬件与软件环境

1.1 硬件平台

项目说明



MCUGD32F503VGT6,Cortex-M33,最高 252 MHz,512 KB Flash,128 KB SRAM
开发板GD32F503V-EVAL
LCDILI9320,240×320,8080 16-bit 并行接口,接在 EXMC 的 NE0/Bank0
触摸屏XPT2046 电阻屏控制器,软件 SPI(bit-bang)
调试器J-Link + Ozone / GDB
LEDPC7 / PC8 / PC9 三颗流水灯
按键PA0(WAKEUP)、PA1(USER)、PC13(TAMPER)

1.2 软件版本

项目版本



Zephyr4.4.0
Zephyr SDK1.0.1
HALGigaDevice SPL gd32f50x V1.0.4
LVGLZephyr modules 内置版本(与 v4.4.0 配套)
构建工具west / CMake / Ninja

二、先看一下最终效果

【GD32F503eval Zephyr LVGL展示】 https://www.bilibili.com/video/BV1Z9MP6cET5/?share_source=copy_web&vd_source=46ffd4c7b1be3ec2b63adcc32d97b26b

资源占用(Release + -Os 大致结果):

Flash: 约 277 KB
RAM:   约 106 KB(含 LVGL 约 38 KB 局部缓冲 + 48 KB 堆 + Zephyr 运行栈)

对于 512 KB Flash / 128 KB SRAM 的 GD32F503 来说,LVGL 完整 demo 刚刚能塞进 SRAM,后续如果要加更多业务逻辑,建议把 LV_Z_VDB_SIZE 调小或改用单缓冲。


三、整体架构:分层而不是一口吞

整个移植我把它拆成 5 个阶段,每个阶段只验证一件事,这样出问题容易定位。

┌─────────────────────────────────────────────┐
│  Phase E: XPT2046 触摸 → lv_indev_t          │
├─────────────────────────────────────────────┤
│  Phase D: LVGL + display_driver_api          │
│           (drivers/display/display_ili9320.c)│
├─────────────────────────────────────────────┤
│  Phase C: LCD panel vtable + app painter     │
│           (src/panel/panel_*.c)              │
├─────────────────────────────────────────────┤
│  Phase B: MIPI-DBI 控制器 + EXMC 底层        │
│           (drivers/mipi_dbi/mipi_dbi_gd32_exmc.c)
├─────────────────────────────────────────────┤
│  Phase A: Zephyr blinky + GPIO 驱动跑通      │
│           (修正 AHB1 基址、GPIO 双寄存器等)   │
└─────────────────────────────────────────────┘

这个分层思路是这次移植能走通的关键:先让 Zephyr 内核在 GD32F503 上跑起来,再让 LCD 控制器硬件通,再让面板协议通,再让 Zephyr display API 通,最后才接 LVGL。很多人一上来就同时调显示屏和 GUI,两个子系统都在报错,很难判断 bug 在哪一层。


四、让 Zephyr 在 GD32F503 上跑起来

这个阶段的目标只是把 samples/basic/blinky 跑起来,让三颗 LED 流水闪。但 GD32F50x 在 Zephyr 里的支持当时并不完整,遇到了 7 个坑。最核心的 3 个写在这里:

4.1 AHB1 总线基址错误导致 BusFault

Zephyr dts 里把 AHB1 基址写成了 0x40010000,而 GigaDevice SPL 头文件 gd32f50x.h 里定义的是 0x40018000。RCU 等挂在 AHB1 上的外设地址因此全部错 0x8000,一打开 GPIO 时钟就 BusFault。

经验:SoC vendor 的 CMSIS 头文件是外设地址的权威来源,Devicetree 注释里手算的式子要回头核对 vendor 头。

4.2 GPIO 双寄存器兼容层把 PC8/PC9 配成输入

GD32F4xx 的 GPIO 是 CTL0(pin 0-7)和 CTL1(pin 8-15)两个寄存器;GD32F50x 是单个 32-bit GPIO_CTL 管 16 个 pin。如果 driver 按 F4xx 的逻辑写 pin -= 8,PC8 的 mode 会被写到 PC0 的位上,PC8/PC9 永远是输入模式。

经验:同一个 vendor 的不同系列,GPIO 寄存器布局可能完全不一样。F50x 需要走 GD32_HAS_AF_PINMUX 分支,用单寄存器 RMW。

4.3 宏冲突与编译警告

gd32f50x.h 里的 BIT(x) 没有 #ifndef 保护,和 Zephyr sys/util_macro.h 冲突;SPL 里某些函数局部变量没初始化触发 -Wmaybe-uninitialized。这些都是 vendor HAL 进入 Zephyr 时的常见适配问题。

经验:vendor HAL 里 BIT、REG32、BITS 这类基础设施宏进入 Zephyr 时必须加 #ifndef 保护;对确实不会被 Zephyr 调到的 SPL 函数,可以选择性初始化局部变量消警告。


五、LCD 控制器与面板框架化

5.1 8080 并行屏用 MIPI-DBI 框架驱动

GD32F503 的 EXMC(外部存储器控制器)可以配成 NOR/PSRAM 模式来驱动 8080 接口 LCD。我实现了一个 Zephyr mipi_dbi 控制器:mipi_dbi_gd32_exmc.c。

它的核心是把 LCD 映射成两个 AHB 地址:

  • 0x60000000:命令口(RS/DC=0)

  • 0x61000000:数据口(RS/DC=1)

通过 EXMC 的地址线某一位来区分命令/数据,这样 mipi_dbi_command_write() 和 mipi_dbi_write_display() 最终都变成一次 AHB 写,不需要额外的 GPIO 翻转。

5.2 面板 vtable 抽象

在 app 层先做了 struct lcd_panel vtable:

struct lcd_panel {
    enum lcd_panel_id id;
    const char *name;
    uint16_t width, height;
    int (*init)(const struct device *, struct mipi_dbi_config *);
    int (*set_window)(const struct device *, struct mipi_dbi_config *,
                      uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1);
};

当时支持了 ILI9320 / ILI9341 / ILI9486 / ST7789V 四种面板。这个抽象不是最终形态,但它让我们在 app 层就能验证:

  1. EXMC 时序是否正确;

  2. 面板初始化序列是否正确;

  3. set_window 和像素填充方向是否正确。

5.3 这一阶段最隐蔽的两个坑

坑 1:8080-16bit 的字节序合同

mipi_dbi_gd32_exmc.c 的 command_write 实现里要求 caller 把每个 16-bit 参数按大端字节序打包进 data_buf,driver 内部用 sys_get_be16 解包后再写总线。原因是:

  • 在 LE Cortex-M 上,一个 uint16_t val = 0x0173 内存里是 [0x73, 0x01];

  • 如果直接把 &val 传给 driver,sys_get_be16 会把它读成 0x7301;

  • 8080-16bit 面板读到的就是 0x7301,显示使能位等关键 bit 全错。

正确做法:caller 用 sys_cpu_to_be16() 把每个参数先 BE-pack,再传 buffer。

uint16_t be = sys_cpu_to_be16(val);
mipi_dbi_command_write(dev, cfg, cmd, (const uint8_t *)&be, 2);

坑 2:面板方向与 fill_buf 顺序

ILI9320 的 R0x03 控制 GRAM 地址计数器方向。vendor 给的值是 0x1020(H-decrement / V-decrement),这意味着面板会把 fill_buf[0] 放到窗口最右列。如果 app 层 lcd_char() 还是按 MSB→fill_buf[0] 的方式填充,字符就会左右镜像。

经验:面板方向位(ID/SS/GS/MADCTL)和 host 的 buffer 布局是一个整体约定。要么改面板方向让 host 按自然顺序写,要么 host 按面板方向倒着写,两个里只能选一个,都做就是 double-flip。


六、把 LVGL 接到标准 display API 上

6.1 为什么必须写 kernel display driver

Zephyr 的 LVGL module 在 modules/lvgl/include/lvgl_zephyr.h 里有硬检查:

#if DT_ZEPHYR_DISPLAYS_COUNT == 0
#error Could not find "zephyr,display" chosen property...

也就是说,没有注册 display_driver_api 的 kernel 设备,LVGL module 直接编译报错。想在 app 层自己注册 lv_display_t + 自定义 flush_cb 这条路走不通。

所以我新增了一个 kernel-tree driver:drivers/display/display_ili9320.c。

6.2 新增的文件

文件作用



zephyr/drivers/display/display_ili9320.c实现 display_driver_api:init / write / blanking / get_capabilities
zephyr/drivers/display/Kconfig.ili9320CONFIG_ILI9320 开关
zephyr/dts/bindings/display/ilitek,ili9320.yamlDevicetree binding
boards/gd/gd32f503v_eval/gd32f503v_eval.dts加 ili9320 子节点 + chosen { zephyr,display = &ili9320; }
app/prj.confCONFIG_LVGL=y 等

6.3 ILI9320 初始化序列的处理

vendor SPL 里的 lcd_init() 有 37 个寄存器。我把这个序列原样搬进 display_ili9320.c,并遵循 BE-pack 合同:

static int ili9320_transmit(const struct device *dev, uint8_t cmd, uint16_t val)
{
    const uint16_t be = sys_cpu_to_be16(val);
    return mipi_dbi_command_write(cfg->mipi_dev, &data->dbi_config,
                                  cmd, (const uint8_t *)&be, sizeof(be));
}

唯一不是 vendor-verbatim 的是 R0x03:vendor 是 0x1020,我改成 0x1030。为什么?

因为 LVGL 的 buffer 是自然 LTR-TTB(从左到右、从上到下)布局,而 0x1020 让 GRAM 计数器 H-decrement / V-decrement。配合 R0x01 的 SS 位和 R0x60 的 GS 位,三组翻转最终要合成一个"净 LTR-TTB"。实验下来 0x1030(H+/V+)配合 vendor 的 R0x01=0x0100、R0x60=0x2700,正好是 LVGL 想要的净方向。

经验:vendor 序列里的方向位不是孤立看的,要把它和 LVGL buffer 顺序、面板物理安装方向一起当成一个系统来调。

6.4 真正隐蔽的 off-by-one

ili9320_set_window() 里,窗口寄存器 R0x50~R0x53 是闭区间。我的 caller 已经按"inclusive end"传 (x + width - 1, y + height - 1),但 set_window 内部又减了一次 1,导致每个 band 都少写 1 行。 symptoms 是:屏幕底部出现黑色横条,字符整体往右下方斜。

经验:面板 driver 的 set_window 协议必须明确是 half-open [x0, x1) 还是 inclusive [x0, x1]。ILI9320 的硬件寄存器是 inclusive,所以 caller 传 inclusive end 时,driver 内部不要再减。

6.5 不要 driver-side 反转

看到镜像时,第一反应是在 driver 里加 per-row pixel reversal。结果加了之后还是镜像——因为 vendor 的 SS/GS/ID 已经把方向翻好了,driver 再翻一次就是 double-flip。最终做法是:driver 里不做任何像素顺序反转,直接 pass-through LVGL buffer。

经验:方向问题要一层一层隔离。先确认 EXMC 没有交换字节,再确认 MIPI-DBI 合同正确,再确认 panel 方向位组合,最后再考虑 driver 是否要做补偿。越底层越不应该做"智能"反转。


七、XPT2046 触摸接入 LVGL

7.1 软件 SPI bit-bang

XPT2046 接的是:

  • SCK  = PC10

  • MOSI = PC12

  • MISO = PC11

  • CS   = PE4

  • PEN  = PE5

没有走 Zephyr SPI controller,而是用一个软件 SPI bit-bang 驱动。原因有两个:

  1. 这个触摸屏在评估板上和 LCD 共用一组 GPIO,Zephyr 的 SPI driver 可能没有为这个特定接线做 overlay;

  2. 数据率很低(几十 Hz 采样),bit-bang 完全够用,代码更可控。

实现上把 vendor SPL 的 touch_panel.c 里的时序原样搬过来,只是把 GPIO 操作换成 Zephyr gpio_pin_set / gpio_pin_get。

7.2 注册为 LVGL 输入设备

static void touch_indev_read_cb(lv_indev_t *indev, lv_indev_data_t *data)
{
    uint16_t x = 0U, y = 0U;
    if (touch_xpt2046_read(&x, &y) != 0) {
        /* EVAL 上触摸读数方向与 LCD 差 180°,双轴镜像校准 */
        x = TOUCH_LCD_W - 1U - x;
        y = TOUCH_LCD_H - 1U - y;
        last_touch.point.x = x;
        last_touch.point.y = y;
        last_touch.state = LV_INDEV_STATE_PRESSED;
    } else {
        last_touch.state = LV_INDEV_STATE_RELEASED;
    }
    *data = last_touch;
}

注意这里有两个坐标系:

  • XPT2046 读出来的 AD 值经过 vendor 标定后,原点在面板某个角;

  • LCD 的 LTR-TTB 原点在左上角。

因为评估板上的触摸屏和 LCD 是反向安装的,所以我在 read_cb 里做了双轴镜像。如果你的板子安装方向不同,这两行可以去掉。


八、关键代码组织(app 目录)

app/gd32f503_blink/
├── CMakeLists.txt              # app 构建 + LVGL demos 源文件
├── prj.conf                    # Zephyr / LVGL / 显示 / 触摸 Kconfig
├── src/
│   ├── main.c                  # LED、按键、LCD display 初始化、LVGL demo
│   ├── font_8x16.h             # 自带 ASCII 字模(Phase C app painter 用)
│   ├── panel/                  # Phase C 的 panel vtable(保留未引用)
│   │   ├── panel.h
│   │   ├── panel_ili9320.c
│   │   ├── panel_ili9341.c
│   │   ├── panel_ili9486.c
│   │   ├── panel_st7789v.c
│   │   ├── panel_internal.c
│   │   └── panel_internal.h
│   ├── touch_xpt2046.c         # XPT2046 软件 SPI 驱动
│   └── touch_xpt2046.h
└── docs/
    ├── gd32f503_zephyr_port.md     # Phase A 详细记录
    ├── phase_c_lcd_panel_bugfixes.md
    ├── phase_d_lvgl_bugfixes.md
    └── lvgl_port_experience.md     # 本文

Kernel tree 里改动的文件(在 zephyr 工作区):

zephyr/
├── drivers/mipi_dbi/mipi_dbi_gd32_exmc.c   # EXMC 8080 控制器
├── drivers/display/display_ili9320.c       # ILI9320 display driver
├── drivers/display/Kconfig.ili9320
├── drivers/display/Kconfig
├── drivers/display/CMakeLists.txt
├── dts/bindings/display/ilitek,ili9320.yaml
├── dts/arm/gd/gd32f50x/gd32f50x.dtsi       # 修正 AHB1 基址
├── drivers/gpio/gpio_gd32.c                # 配合 F50x 单寄存器
└── soc/gd/gd32/gd32f50x/Kconfig            # select GD32_HAS_AF_PINMUX

九、prj.conf 关键项说明

# 显示控制器 + MIPI-DBI
CONFIG_MIPI_DBI=y
CONFIG_MIPI_DBI_GD32_EXMC=y
CONFIG_MIPI_DBI_GD32_EXMC_MEM_BARRIER=y
CONFIG_DISPLAY=y

# LVGL 主体
CONFIG_LVGL=y
CONFIG_LV_Z_AUTO_INIT=y
CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y
CONFIG_LV_Z_LVGL_MUTEX=y

# 内存
CONFIG_LV_Z_MEM_POOL_SYS_HEAP=y
CONFIG_LV_Z_MEM_POOL_SIZE=49152      # 48 KB 堆,给 widgets + styles
CONFIG_LV_Z_VDB_SIZE=18              # 渲染缓冲,约占 38 KB
CONFIG_LV_Z_LVGL_WORKQUEUE_STACK_SIZE=16384
CONFIG_MAIN_STACK_SIZE=8192

# demo
CONFIG_LV_USE_DEMO_WIDGETS=y

LV_Z_VDB_SIZE=18 是 Zephyr LVGL module 里以"1/10 屏幕"为单位的缓冲大小,18 对应约 60% 屏幕面积,实际 RAM 占用要看具体实现。这个值可以根据 SRAM 余量调整。


十、移植 checklist(可直接照做)

如果要在 GD32F50x 或其他国产 Cortex-M 上跑 Zephyr + LVGL,建议按这个顺序:

  1. 先让 Zephyr 最小系统跑起来:blinky 能闪,串口能输出,再往下做。

  2. 核对 AHB/APB 基址:用 vendor CMSIS 头文件和 DTS 对一遍,错一个地址就是 BusFault。

  3. 核对 GPIO 寄存器布局:不同系列差异很大,单寄存器/双寄存器、AFIO/SYSCFG 都要确认。

  4. 让 EXMC / FSMC / 8080 控制器先通:可以用 vendor SPL 的初始化代码做参考,但用 Zephyr driver 框架封装。

  5. 先点亮屏幕,再跑 LVGL:在 app 层做简单的 rect fill / 字符显示,确认方向、颜色、字节序都对。

  6. 写 kernel display driver:LVGL module 强制要求 zephyr,display chosen 节点。

  7. 严格遵循 MIPI-DBI 字节序合同:command 参数 BE-pack,framebuffer LE-pass-through。

  8. set_window 协议要明确 inclusive / half-open:ILI9320 是 inclusive,不要重复减 1。

  9. 方向位要系统调:R0x03 / R0x01 / R0x60 / MADCTL 这些方向位和 LVGL buffer 顺序一起看,不要 driver 里再加反转。

  10. 触摸最后接:先确认 LCD 显示正确,再接入 XPT2046,避免显示和触摸两个变量同时调试。


十一、几点心得

11.1 为什么分层做很重要

如果同时调 LCD 硬件、面板协议、LVGL、触摸,出了问题根本没法定位。分阶段后,每一阶段的验收标准都很清楚:

  • Phase A:LED 流水闪;

  • Phase B:能写单个像素到屏上;

  • Phase C:能显示字符和色条;

  • Phase D:lv_demo_widgets() 正常显示;

  • Phase E:触摸能滑动切页。

每一阶段不通过,绝不进入下一阶段。

11.2 vendor SPL 的价值和陷阱

vendor SPL 的初始化序列、时序、标定参数是宝藏,但直接复制要小心:

  • 宝藏:LCD init 序列、EXMC timing、XPT2046 校准值,这些不用自己推导;

  • 陷阱:SPL 是阻塞式、裸机思维,和 Zephyr 的设备模型、多线程、中断模型不兼容。可以借鉴时序和寄存器值,但不能原样搬进 app。

11.3 Zephyr 设备模型不是负担

一开始我也想过绕过 display_driver_api,直接在 app 层操作 mipi_dbi。但 LVGL module 的硬检查让我必须写 kernel driver。回过头看,这个约束反而让架构更清晰:

  • 硬件细节封装在 driver 里;

  • app 只调用标准 display_write();

  • 以后换 ILI9341 / ST7789V,只要换个 driver,app 不用改。

11.4 调试 8080 屏的几件利器

  • Ozone Watch:直接看 RCU.APB2EN、GPIOx.CTL、EXMC.SNCTL/SNTCFG,比 printk 快;

  • 反汇编 + objdump:BusFault 时 PC 指向的指令反汇编出来,能迅速定位到寄存器读写;

  • 全屏 flood:用纯色先刷一遍,确认底层通不通;

  • 字符测试:色条对了不代表方向对了,字符最容易暴露镜像/错位问题。


结语

GD32F503 + Zephyr + LVGL 这条路径走下来,最大的感受是:国产 MCU 跑现代 RTOS + GUI 是可行的,但 vendor HAL 和 Zephyr 设备模型之间的缝隙需要一层一层填补。不要指望一次性把所有功能堆起来点亮,分阶段、每层有明确验收、遇到方向/字节序/off-by-one 问题时用最小测试用例隔离,效率会高很多。

希望这篇记录对同样想在这条路上走的同学有帮助。如果有问题欢迎在楼下讨论。





关键词: Zephyr     GD32F503V-EVAL     LVG    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]