开发板 【正点原子】DNESP32S3开发板
在这里插入图片描述
在正点原子 DNESP32S3 开发板上移植 LVGL 图形库。

idf_component.yml

dependencies:
  idf: ">=4.4"
  lvgl/lvgl: 9.2.0

lvgl_port.c

#include "lvgl_port.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sys/lock.h"
#include "sys/param.h"   // 修复:提供 MAX/MIN 宏
#include "unistd.h"      // 修复:提供 usleep
#include "driver/spi_master.h"
#include "lcd_driver.h"
#include "assert.h"

static const char *TAG = "lvgl_port";

// LVGL 线程安全互斥锁
static _lock_t lvgl_api_lock;
// 显示设备句柄(内部使用)
static lv_display_t *s_display = NULL;

// 刷新完成回调:通知 LVGL 刷新结束
static bool lvgl_port_flush_ready_cb(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx)
{
    lv_display_t *disp = (lv_display_t *)user_ctx;
    lv_display_flush_ready(disp);
    return false;
}

// LVGL 刷新回调:将渲染好的帧缓冲推送到 LCD 面板
static void lvgl_port_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map)
{
    esp_lcd_panel_handle_t panel_handle = lv_display_get_user_data(disp);
    int offsetx1 = area->x1;
    int offsetx2 = area->x2;
    int offsety1 = area->y1;
    int offsety2 = area->y2;

    // SPI LCD 为大端,需交换 RGB565 字节序
    lv_draw_sw_rgb565_swap(px_map, (offsetx2 + 1 - offsetx1) * (offsety2 + 1 - offsety1));
    esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, px_map);
}

// LVGL 节拍递增回调
static void lvgl_port_tick_inc_cb(void *arg)
{
    lv_tick_inc(LVGL_TICK_PERIOD_MS);
}

// LVGL 主任务:处理定时器、事件、渲染
static void lvgl_port_task(void *arg)
{
    ESP_LOGI(TAG, "Starting LVGL task");
    uint32_t time_till_next_ms = 0;
    uint32_t min_delay_ms = 1000 / CONFIG_FREERTOS_HZ;

    while (1)
    {
        _lock_acquire(&lvgl_api_lock);
        time_till_next_ms = lv_timer_handler();
        _lock_release(&lvgl_api_lock);

        // 防止任务看门狗超时
        time_till_next_ms = MAX(time_till_next_ms, min_delay_ms);
        // 防止显示未就绪时空转过快
        time_till_next_ms = MIN(time_till_next_ms, LVGL_TASK_MAX_DELAY_MS);
        usleep(1000 * time_till_next_ms);
    }
}

lv_display_t *lvgl_port_init(esp_lcd_panel_handle_t panel_handle, esp_lcd_panel_io_handle_t io_handle)
{
    // 1. 初始化 LVGL 核心
    lv_init();

    // 2. 创建显示设备
    lv_display_t *display = lv_display_create(LCD_H_RES, LCD_V_RES);
    s_display = display;

    // 3. 分配 DMA 绘制缓冲(双缓冲,局部渲染模式)
    size_t draw_buffer_sz = LCD_H_RES * LVGL_DRAW_BUF_LINES * sizeof(lv_color16_t);
    void *buf1 = spi_bus_dma_memory_alloc(LCD_HOST, draw_buffer_sz, 0);
    assert(buf1);
    void *buf2 = spi_bus_dma_memory_alloc(LCD_HOST, draw_buffer_sz, 0);
    assert(buf2);
    lv_display_set_buffers(display, buf1, buf2, draw_buffer_sz, LV_DISPLAY_RENDER_MODE_PARTIAL);

    // 4. 绑定面板句柄与刷新回调
    lv_display_set_user_data(display, panel_handle);
    lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565);
    lv_display_set_flush_cb(display, lvgl_port_flush_cb);

    // 5. 注册 SPI 传输完成回调(修复:直接使用传入的 io_handle)
    const esp_lcd_panel_io_callbacks_t cbs = {
        .on_color_trans_done = lvgl_port_flush_ready_cb,
    };
    esp_lcd_panel_io_register_event_callbacks(io_handle, &cbs, display);

    return display;
}

void lvgl_port_start(void)
{
    // 1. 安装 LVGL 节拍定时器
    ESP_LOGI(TAG, "Install LVGL tick timer");
    const esp_timer_create_args_t lvgl_tick_timer_args = {
        .callback = &lvgl_port_tick_inc_cb,
        .name = "lvgl_tick"
    };
    esp_timer_handle_t lvgl_tick_timer = NULL;
    ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer));
    ESP_ERROR_CHECK(esp_timer_start_periodic(lvgl_tick_timer, LVGL_TICK_PERIOD_MS * 1000));

    // 2. 创建 LVGL 处理任务
    ESP_LOGI(TAG, "Create LVGL task");
    xTaskCreate(lvgl_port_task, "LVGL", LVGL_TASK_STACK_SIZE, NULL, LVGL_TASK_PRIORITY, NULL);
}

void lvgl_port_lock(void)
{
    _lock_acquire(&lvgl_api_lock);
}

void lvgl_port_unlock(void)
{
    _lock_release(&lvgl_api_lock);
}

main.c

/*
 * SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: CC0-1.0
 */
#include <stdio.h>
#include "esp_log.h"
#include "lcd_driver.h"
#include "lvgl_port.h"
#include "lvgl_ui.h"

static const char *TAG = "main";
i2c_obj_t i2c0_master;
void app_main(void)
{

    i2c0_master = iic_init(I2C_NUM_0);  /* 初始化IIC0 */
    xl9555_init(i2c0_master);           /* IO扩展芯片初始化 */


    // 1. 初始化 LCD 硬件驱动,同时获取面板句柄和IO句柄
    esp_lcd_panel_handle_t panel_handle = NULL;
    esp_lcd_panel_io_handle_t io_handle = NULL;
    ESP_ERROR_CHECK(lcd_driver_init(&panel_handle, &io_handle));
    ESP_LOGI(TAG, "LCD driver initialized");

    // 2. 初始化 LVGL 移植层(传入面板句柄和IO句柄)
    lv_display_t *display = lvgl_port_init(panel_handle, io_handle);
    ESP_LOGI(TAG, "LVGL port initialized");

    // 3. 启动 LVGL 任务与节拍
    lvgl_port_start();

    // 4. 初始化 UI 界面(加锁保护线程安全)
    ESP_LOGI(TAG, "Initialize LVGL UI interface");
    lvgl_port_lock();
    lvgl_ui_init(display);
    lvgl_port_unlock();
}

总结

本文详细展示了在 ESP32-S3 平台上将 LVGL 图形库移植到 SPI 接口 LCD 屏幕的完整过程。核心工作集中在 lvgl_port.c 文件中,它作为 LVGL 与底层硬件的桥梁,主要实现了以下几个关键功能:

  1. 显示驱动对接:通过 lvgl_port_flush_cb 回调函数,将 LVGL 渲染好的帧缓冲区数据(RGB565格式)交换字节序后,通过 esp_lcd_panel_draw_bitmap 推送到 LCD 面板。
  2. 任务与定时器管理:创建了独立的 lvgl_port_task 任务来周期性地调用 lv_timer_handler(),处理 LVGL 的内部定时器、事件和渲染逻辑。同时,通过 ESP 硬件定时器为 LVGL 提供精确的节拍 (lv_tick_inc)。
  3. 线程安全与同步:使用 _lock_t 互斥锁 (lvgl_api_lock) 保护 LVGL API 的调用,确保在多任务环境下的数据安全。并通过 SPI 传输完成回调 (lvgl_port_flush_ready_cb) 通知 LVGL 一帧数据刷新完成。
  4. 内存管理:使用 spi_bus_dma_memory_alloc 为 LVGL 分配 DMA 可访问的双绘图缓冲区,设置为局部渲染模式以优化性能。

main.c 中,应用程序的启动流程清晰明了:

  • 初始化 I2C 和 IO 扩展芯片。
  • 初始化 LCD 硬件驱动,获取面板和 IO 句柄。
  • 调用 lvgl_port_init 初始化 LVGL 并绑定显示设备。
  • 调用 lvgl_port_start 启动 LVGL 任务和节拍定时器。
  • 在锁保护下初始化用户界面。

idf_component.yml 则简洁地定义了项目对 ESP-IDF (>=4.4) 和 LVGL (9.2.0) 的依赖关系。

更多推荐