LVGL GUI界面移植到ST7789圆形屏幕实践

你有没有遇到过这种情况:手头一块挺漂亮的240×240圆形屏,接上MCU后却发现四角发黑、UI跑得卡顿、边缘还乱画?😅
别急——这几乎是每个玩过 ST7789 + 圆形TFT 的开发者都踩过的坑。而当你想用LVGL做出炫酷动画时,问题就更多了:刷新慢、内存爆、渲染越界……简直让人怀疑人生。

但好消息是,只要搞懂底层驱动逻辑和LVGL的“脾气”,这些问题都能优雅解决!✨
本文不讲空话,直接带你从零搞定 LVGL 在 ST7789 驱动的 240×240 圆形屏上的完整移植方案 ,重点突破裁剪优化、DMA加速、性能调优三大难点,让你的小屏幕也能跑出丝滑UI!


ST7789不只是个“显卡”——理解它的脾气才能驾驭它

先别急着写 lv_init() ,咱们得先摸清这块屏的底细。🧠

ST7789听起来像个简单的SPI显示屏控制器,但它其实是个“有个性”的家伙。比如:

  • 它内部自带GRAM(图形内存),不需要外挂显存,对MCU很友好;
  • 支持RGB565格式,16位色足够应付大多数GUI场景;
  • 能通过MADCTL寄存器灵活翻转坐标系——这点在圆形屏里特别关键!

但注意⚠️:虽然屏幕标称240×240,可实际可视区域是一个直径约216~220像素的圆!四个角是物理遮挡的“黑角”。如果不管不顾地往全屏绘图,不仅浪费CPU和带宽,还会导致LVGL误判脏区域,越刷越卡。

所以第一步不是初始化LVGL,而是让ST7789乖乖听话👇

初始化要稳,更要“懂屏”

void st7789_init(void) {
    gpio_set_direction(DC_PIN, GPIO_MODE_OUTPUT);
    gpio_set_direction(RST_PIN, GPIO_MODE_OUTPUT);
    spi_bus_initialize(HSPI_HOST, &buscfg, SPI_DMA_CH_AUTO);

    spi_device_interface_config_t devcfg = {
        .command_bits = 8,
        .address_bits = 0,
        .mode = 0,
        .clock_speed_hz = 60 * 1000 * 1000,  // 尽量拉高,ESP32能扛住
        .spics_io_num = CS_PIN,
    };
    spi_device_handle_t spi;
    spi_bus_add_device(HSPI_HOST, &devcfg, &spi);

    // 复位序列
    digitalWrite(RST_PIN, 0);
    delay_ms(10);
    digitalWrite(RST_PIN, 1);
    delay_ms(120);

    st7789_send_cmd(spi, 0x36); // Memory Access Control
    st7789_send_data(spi, 0xC0); // 关键!横屏+上下翻转,适配圆形布局

    st7789_send_cmd(spi, 0x3A); // Pixel Format
    st7789_send_data(spi, 0x05); // RGB565

    // 后续配置省略...伽马、电源、帧率等按数据手册设置即可
    st7789_send_cmd(spi, 0x11); // Sleep Out
    delay_ms(120);
    st7789_send_cmd(spi, 0x29); // Display ON
}

📌 划重点
- 0x36 寄存器设为 0xC0 是为了将显示方向调整为适合圆形屏使用的模式(通常是旋转90°并翻转),确保原点在左上角且Y轴向下;
- SPI速率尽量开到 40~60MHz ,否则刷屏会像PPT翻页一样慢;
- 初始化顺序不能乱,尤其是 Sleep Out → 延时 → Display ON 三部曲,少了哪步都可能黑屏!


让LVGL和ST7789“握手成功”——flush_cb才是灵魂所在

LVGL本身不管硬件,它只负责计算该画什么。真正把像素送上屏幕的,是你写的 flush_cb 回调函数。这才是整个GUI流畅与否的核心枢纽!💥

flush_cb怎么写才高效?

static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[LVGL_BUFFER_SIZE]; // 缓冲区大小建议为 240x10 ~ 240x30

void lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) {
    int32_t w = (area->x2 - area->x1 + 1);
    int32_t h = (area->y2 - area->y1 + 1);

    // Step 1: 设置GRAM地址窗口(Column & Row)
    uint8_t cmd, data[4];

    cmd = 0x2A; // Column Address Set
    data[0] = (area->x1 >> 8) & 0xFF;
    data[1] = area->x1 & 0xFF;
    data[2] = (area->x2 >> 8) & 0xFF;
    data[3] = area->x2 & 0xFF;
    st7789_send_cmd_data(spi_handle, cmd, data, 4);

    cmd = 0x2B; // Page Address Set
    data[0] = (area->y1 >> 8) & 0xFF;
    data[1] = area->y1 & 0xFF;
    data[2] = (area->y2 >> 8) & 0xFF;
    data[3] = area->y2 & 0xFF;
    st7789_send_cmd_data(spi_handle, cmd, data, 4);

    // Step 2: 开始写入像素数据
    cmd = 0x2C; // Memory Write
    st7789_send_cmd(spi_handle, cmd);

    // 使用DMA异步发送,千万别阻塞主线程!
    st7789_send_colors_dma(spi_handle, (uint16_t*)color_map, w * h);

    // 注意:lv_disp_flush_ready() 必须在DMA传输完成后调用!
    // 所以你应该在SPI DMA中断服务函数里触发它
}

🔧 关键技巧提示
- 每次刷新前必须设置GRAM窗口( 0x2A , 0x2B ),否则数据会写错位置;
- 推荐使用 半双工SPI + DMA 发送颜色数据,避免CPU占用过高;
- lv_disp_flush_ready(drv) 绝对不能放在 flush_cb 末尾直接调用!必须等DMA完成后再通知LVGL,否则会导致画面撕裂或崩溃。

💡小贴士:如果你用的是ESP-IDF或STM32 HAL,可以用 spi_transfer_end_event() 或DMA中断回调来安全调用 lv_disp_flush_ready()


圆形屏的灵魂拷问:如何不让LVGL“瞎画”?

这才是本文最实用的部分!👏

默认情况下,LVGL认为你的屏幕是完整的矩形,它会在 (0,0) (239,239) 之间任意绘制。但在圆形屏上,四个角落根本看不见,却还在拼命渲染——白白消耗资源不说,还可能导致触摸响应错位。

怎么办?答案就是: 告诉LVGL“哪里不能画”

利用 rounder_cb 实现动态行级裁剪

LVGL提供了一个叫 rounder_cb 的回调函数,专门用于修正待刷新区域的边界。我们可以在其中实现“圆形裁剪”。

void rounder_cb(struct _lv_disp_drv_t *disp_drv, lv_area_t *area) {
    static const int32_t cx = 120, cy = 120; // 圆心
    static const int32_t r = 118;             // 半径(留2px边距防溢出)

    lv_coord_t x1 = area->x1;
    lv_coord_t y1 = area->y1;
    lv_coord_t x2 = area->x2;
    lv_coord_t y2 = area->y2;

    bool clipped = false;

    // 对每一行做水平裁剪
    for (lv_coord_t y = y1; y <= y2; y++) {
        lv_coord_t dy = y - cy;
        if (dy >= r || dy <= -r) continue; // 超出垂直范围

        lv_coord_t dx = (lv_coord_t)sqrtf((float)(r*r - dy*dy));
        lv_coord_t row_x1 = cx - dx;
        lv_coord_t row_x2 = cx + dx;

        if (x1 < row_x1) { x1 = row_x1; clipped = true; }
        if (x2 > row_x2) { x2 = row_x2; clipped = true; }

        if (x1 > x2) {
            // 整行都在圆外 → 直接跳过
            continue;
        }
    }

    if (clipped || (x1 <= x2)) {
        area->x1 = x1;
        area->x2 = x2;
    } else {
        // 完全不可见 → 标记为空区域
        lv_area_set(area, 0, 0, -1, -1);
    }
}

🎯 这段代码的妙处在于:
- 它不是粗暴地把整个区域砍掉,而是逐行计算有效区间;
- 只保留圆内的部分,自动忽略无效角落;
- 最终减少约 30%~40% 的无效渲染量,显著提升帧率!

记得注册这个回调哦:

disp_drv.rounder_cb = rounder_cb;

性能优化实战清单 🛠️(亲测有效)

光理论不够,下面这些是我调试多款产品总结出来的“保命清单”:

优化项 推荐做法
缓冲区大小 至少 240x10=2400 个像素;双缓冲更稳(需16KB RAM)
DMA传输 必须启用!否则SPI吞吐跟不上LVGL刷新节奏
部分刷新 默认开启,避免全屏重绘;复杂UI下性能提升明显
动画帧率 控制在20~30fps,太高反而卡顿
字体选择 优先使用 lv_font_montserrat_14 这类紧凑字体,避免加载大尺寸位图
抗锯齿 如非必要关闭 LVGL_USE_GPU_SDL_RENDER 类功能,节省算力
定时器精度 确保每5ms调用一次 lv_timer_handler() ,可用RTOS任务或硬件定时器

📌 特别提醒:
某些开发板(如ESP32-WROVER)虽然有PSRAM,但LVGL默认不会使用它作为绘图缓冲区!你需要手动配置:

// 示例:使用外部PSRAM分配缓冲区
lv_color_t *buf1 = heap_caps_malloc(LVGL_BUFFER_SIZE * sizeof(lv_color_t), MALLOC_CAP_SPIRAM);
lv_color_t *buf2 = heap_caps_malloc(LVGL_BUFFER_SIZE * sizeof(lv_color_t), MALLOC_CAP_SPIRAM);
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, LVGL_BUFFER_SIZE);

这样才能真正释放内存压力!


UI设计也要“圆”来配合 😎

硬件和驱动搞定了,UI也得跟上审美和技术节奏。

设计建议:

  • 控件居中布局 :按钮、图标尽量集中在中心区域,避免靠近边缘变形或被裁;
  • 环形菜单更自然 :利用 lv_arc 或自定义控件做旋转式导航,契合圆形主题;
  • 字体清晰优先 :小字号下推荐无衬线字体,避免笔画粘连;
  • 触摸映射校准 :若搭配XPT2046等电阻屏,需将触摸坐标映射到圆形有效区;
  • 背光控制节能 :通过PWM调节亮度,在待机时降低功耗;

🎨 拓展玩法:
- 加入渐变背景、圆形进度条、模拟表盘等视觉元素;
- 配合传感器数据实时更新图表(如心率、步数);
- 用 lv_anim_timeline 打造流畅转场动画,用户体验直接起飞🚀


最后说点掏心窝的话 ❤️

LVGL + ST7789 这套组合拳,看似简单,实则处处是坑。但从另一个角度看,这也正是嵌入式GUI的魅力所在:你要懂硬件时序、要会调内存、还要兼顾美学与性能。

但一旦你把它调通了——看着那个小小的圆形屏上,按钮滑动如丝般顺滑,动画流转自如,那种成就感,真的无可替代。🌈

这套方案我已经用在智能手环、WiFi配网指示器、迷你音乐播放器等多个项目中,稳定性杠杠的。无论是STM32F4、GD32还是ESP32平台,只需改几根引脚定义就能快速移植,非常适合产品原型开发。

未来还可以继续升级:
- 接入GT911电容触摸,实现多点交互;
- 集成FreeType动态加载TTF字体,支持中文显示;
- 结合LittleFS保存用户配置和历史记录;
- 甚至上RTOS做多任务调度,让GUI与通信互不干扰。

技术没有终点,只有不断迭代的过程。而每一次成功的移植,都是通往更好产品的一步。


“最好的GUI,不是最炫的,而是让用户感觉不到它的存在。”
—— 致每一位默默打磨细节的嵌入式工程师 💪🔧

现在,去点亮你的那块圆形屏吧!🌟

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐