ESP32 IDF驱动ST7789+移植LVGL(v8.4)

一. 前期准备

1. LVGL源码下载,选择v8.4版本https://github.com/lvgl/lvgl

2. LVGL源码精简

保留examples文件夹、src文件夹、lv_conf_template.h和 lvgl.h,其他删除

image

3. ESP32驱动屏幕

以ESP32C6为例,屏幕驱动相关代码,需要根据自己的屏幕进行调整

需要引用组件:esp_lcd

LCD.h文件

#define LCD_PIN_NUM_SCLK           7
#define LCD_PIN_NUM_MOSI           6
#define LCD_PIN_NUM_MISO           5
#define LCD_PIN_NUM_LCD_CS         14
#define LCD_PIN_NUM_LCD_DC         15
#define LCD_PIN_NUM_LCD_RST        21
#define LCD_PIN_NUM_BK_LIGHT       22

#define LCD_HOR_RES              172
#define LCD_VER_RES              320

#define LCD_OFFSET_X 34
#define LCD_OFFSET_Y 0


#define LEDC_HS_TIMER          LEDC_TIMER_0
#define LEDC_LS_MODE           LEDC_LOW_SPEED_MODE
#define LEDC_HS_CH0_GPIO       EXAMPLE_PIN_NUM_BK_LIGHT
#define LEDC_HS_CH0_CHANNEL    LEDC_CHANNEL_0
#define LEDC_TEST_DUTY         (4000)
#define LEDC_ResolutionRatio   LEDC_TIMER_13_BIT
#define LEDC_MAX_Duty          ((1 << LEDC_ResolutionRatio) - 1)


extern volatile uint8_t refresh_done_flag;
extern esp_lcd_panel_handle_t panel_handle;

void LCD_Init(void);
void LCD_Flush(uint16_t color);
void Backlight_Init(void);
void Backlight_Light(uint8_t Light);

LCD.c文件

esp_lcd_panel_io_handle_t io_handle = NULL;
esp_lcd_panel_handle_t panel_handle = NULL;
static ledc_channel_config_t ledc_channel;

volatile uint8_t refresh_done_flag;

static bool notify_lcd_flush_ready(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx)
{   
    refresh_done_flag = 1;
    return false;
}

uint16_t lcd_buff[LCD_HOR_RES * 10];
void LCD_Flush(uint16_t color)
{
    uint16_t color_tmp = ((color & 0x00FF) << 8) | ((color & 0xFF00) >> 8); /* 需要转换一下颜色值 */

    for (uint32_t i = 0; i < sizeof(lcd_buff) / sizeof(lcd_buff[0]); i++)
        lcd_buff[i] = color_tmp;

    refresh_done_flag = 0;

    for (uint16_t y = 0; y < LCD_VER_RES; y += 10)
        esp_lcd_panel_draw_bitmap(panel_handle, 0 + LCD_OFFSET_X, y + LCD_OFFSET_Y, LCD_HOR_RES + LCD_OFFSET_X, y + 10 + LCD_OFFSET_Y, lcd_buff);

    do
    {
        /* 等待内部缓存刷新完成 */
        vTaskDelay(10);
    } while (refresh_done_flag != 1);
}

void LCD_Init(void)
{
    spi_bus_config_t buscfg = {
        .sclk_io_num = LCD_PIN_NUM_SCLK,                                 /* 时钟引脚 */
        .mosi_io_num = LCD_PIN_NUM_MOSI,                                 /* 主机输出从机输入引脚 */
        .miso_io_num = LCD_PIN_NUM_MISO,                                 /* 主机输入从机输出引脚 */
        .quadwp_io_num = -1,                                             /* 用于Quad模式的WP引脚,未使用时设置为-1 */
        .quadhd_io_num = -1,                                             /* 用于Quad模式的HD引脚,未使用时设置为-1 */
        .max_transfer_sz = LCD_HOR_RES * LCD_VER_RES * sizeof(uint16_t), /* 最大传输大小(整屏(RGB565格式)) */
    };
    /* 初始化SPI总线 */
    ESP_ERROR_CHECK(spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO));

    esp_lcd_panel_io_handle_t io_handle = NULL; /* LCD IO设备句柄 */
    /* spi配置 */
    esp_lcd_panel_io_spi_config_t io_config = {
        .dc_gpio_num = LCD_PIN_NUM_LCD_DC, /* DC IO */
        .cs_gpio_num = LCD_PIN_NUM_LCD_CS, /* CS IO */
        .pclk_hz = 60 * 1000 * 1000,       /* PCLK为60MHz */
        .lcd_cmd_bits = 8,                 /* 命令位宽 */
        .lcd_param_bits = 8,               /* LCD参数位宽 */
        .spi_mode = 0,                     /* SPI模式 */
        .trans_queue_depth = 10,           /* 传输队列 */
    };
    /* 将LCD设备挂载至SPI总线上 */
    ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, &io_handle));

    /* LCD设备配置 */
    esp_lcd_panel_dev_config_t panel_config = {
        .reset_gpio_num = LCD_PIN_NUM_LCD_RST,        /* RTS IO */
        .rgb_ele_order = COLOR_RGB_ELEMENT_ORDER_RGB, /* 颜色格式 */
        .bits_per_pixel = 16,                         /* 颜色深度 */
        .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,    /* 大端顺序 */
    };

    /* 为ST7789创建LCD面板句柄,并指定SPI IO设备句柄 */
    ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));
    /* 复位LCD */
    ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
    /* 反显 */
    ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true));
    /* 初始化LCD句柄 */
    ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
    // 镜像
    // ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_handle, true, false));
    /* 打开屏幕 */
    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));
    // 传输完成回调
    const esp_lcd_panel_io_callbacks_t cbs = {
        .on_color_trans_done = notify_lcd_flush_ready,
    };
    /* 注册屏幕刷新完成回调函数 */
    ESP_ERROR_CHECK(esp_lcd_panel_io_register_event_callbacks(io_handle, &cbs, NULL));
}

// 背光初始化
void Backlight_Init(void)
{
    gpio_config_t bk_gpio_config = {
        .mode = GPIO_MODE_OUTPUT,
        .pin_bit_mask = 1ULL << LCD_PIN_NUM_BK_LIGHT};
    ESP_ERROR_CHECK(gpio_config(&bk_gpio_config));

    // 配置LEDC
    ledc_timer_config_t ledc_timer = {
        .duty_resolution = LEDC_TIMER_13_BIT,
        .freq_hz = 5000,
        .speed_mode = LEDC_LS_MODE,
        .timer_num = LEDC_HS_TIMER,
        .clk_cfg = LEDC_AUTO_CLK};
    ledc_timer_config(&ledc_timer);

    ledc_channel.channel = LEDC_HS_CH0_CHANNEL;
    ledc_channel.duty = 0;
    ledc_channel.gpio_num = LCD_PIN_NUM_BK_LIGHT;
    ledc_channel.speed_mode = LEDC_LS_MODE;
    ledc_channel.timer_sel = LEDC_HS_TIMER;
    ledc_channel_config(&ledc_channel);
    ledc_fade_func_install(0);
}

void Backlight_Light(uint8_t Light)
{
    static uint8_t last_ligtht = 0xff;
    if (last_ligtht == Light)
        return;
    last_ligtht = Light;
    if (Light > 100)
        Light = 100;
    uint16_t Duty = LEDC_MAX_Duty - (81 * (100 - Light));
    if (Light == 0)
        Duty = 0;
    // 设置PWM占空比
    ledc_set_duty(ledc_channel.speed_mode, ledc_channel.channel, Duty);
    ledc_update_duty(ledc_channel.speed_mode, ledc_channel.channel);
}

主函数中调用

void app_main(void)
{
    LCD_Init();
    Backlight_Init();
    Backlight_Light(100);
  
    LCD_Flush(0xffff);
    while (1)
    {
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

实际效果

image

二. 开始移植

1. 创建LVGL组件

命名为创建一个组件,命名为LVGL

image

并添加新建2个文件夹,分别命名为lvgl和lv_port。完成后如下

image

2. 添加LVGL代码文件

把上面精简的源文件添加到lvgl文件夹中

image

在路径lvgl->examples->porting下有移植相关的接口文件,复制到lv_port文件夹,并重命名删除_template字段

image image

3. 修改lv_port_disp代码

lv_port_disp.h打开宏开关

image

lv_port_disp.c打开宏开关

image

配置屏幕大小

image

配置缓存

简单移植,直接用第一种缓存模式,把其它的都注释掉

image

添加屏幕初始化函数

把上面的屏幕初始化添加到这里。

image

添加刷屏函数

/*Flush the content of the internal buffer the specific area on the display
 *You can use DMA or any hardware acceleration to do this operation in the background but
 *'lv_disp_flush_ready()' has to be called when finished.*/
static void disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p)
{
    if (disp_flush_enabled)
    {
        /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/

        // int32_t x;
        // int32_t y;
        // for(y = area->y1; y <= area->y2; y++) {
        //     for(x = area->x1; x <= area->x2; x++) {
        //         /*Put a pixel to the display. For example:*/
        //         /*put_px(x, y, *color_p)*/
        //         color_p++;
        //     }
        // }

        int offsetx1 = area->x1;
        int offsetx2 = area->x2;
        int offsety1 = area->y1;
        int offsety2 = area->y2;
        // copy a buffer's content to a specific area of the display
        esp_lcd_panel_draw_bitmap(panel_handle, offsetx1 + LCD_OFFSET_X, offsety1 + LCD_OFFSET_Y, offsetx2 + LCD_OFFSET_X + 1, offsety2 + LCD_OFFSET_Y + 1, color_p);

        // 阻塞等待传输完成
        refresh_done_flag = 0;
        while (refresh_done_flag != 1);
    }

    /*IMPORTANT!!!
     *Inform the graphics library that you are ready with the flushing*/
    lv_disp_flush_ready(disp_drv);
}

4. 修改lv_port_indev代码

lv_port_indev.h打开宏开关

image

lv_port_indev.c打开宏开关

image

暂时没用上任何输入,因此在初始化中把全部输入设备注册代码注释

void lv_port_indev_init(void)
{
    /**
     * Here you will find example implementation of input devices supported by LittelvGL:
     *  - Touchpad
     *  - Mouse (with cursor support)
     *  - Keypad (supports GUI usage only with key)
     *  - Encoder (supports GUI usage only with: left, right, push)
     *  - Button (external buttons to press points on the screen)
     *
     *  The `..._read()` function are only examples.
     *  You should shape them according to your hardware
     */

    static lv_indev_drv_t indev_drv;

    // /*------------------
    //  * Touchpad
    //  * -----------------*/

    // /*Initialize your touchpad if you have*/
    // touchpad_init();

    // /*Register a touchpad input device*/
    // lv_indev_drv_init(&indev_drv);
    // indev_drv.type = LV_INDEV_TYPE_POINTER;
    // indev_drv.read_cb = touchpad_read;
    // indev_touchpad = lv_indev_drv_register(&indev_drv);

    // /*------------------
    //  * Mouse
    //  * -----------------*/

    // /*Initialize your mouse if you have*/
    // mouse_init();

    // /*Register a mouse input device*/
    // lv_indev_drv_init(&indev_drv);
    // indev_drv.type = LV_INDEV_TYPE_POINTER;
    // indev_drv.read_cb = mouse_read;
    // indev_mouse = lv_indev_drv_register(&indev_drv);

    // /*Set cursor. For simplicity set a HOME symbol now.*/
    // lv_obj_t * mouse_cursor = lv_img_create(lv_scr_act());
    // lv_img_set_src(mouse_cursor, LV_SYMBOL_HOME);
    // lv_indev_set_cursor(indev_mouse, mouse_cursor);

    // /*------------------
    //  * Keypad
    //  * -----------------*/

    // /*Initialize your keypad or keyboard if you have*/
    // keypad_init();

    // /*Register a keypad input device*/
    // lv_indev_drv_init(&indev_drv);
    // indev_drv.type = LV_INDEV_TYPE_KEYPAD;
    // indev_drv.read_cb = keypad_read;
    // indev_keypad = lv_indev_drv_register(&indev_drv);

    // /*Later you should create group(s) with `lv_group_t * group = lv_group_create()`,
    //  *add objects to the group with `lv_group_add_obj(group, obj)`
    //  *and assign this input device to group to navigate in it:
    //  *`lv_indev_set_group(indev_keypad, group);`*/

    // /*------------------
    //  * Encoder
    //  * -----------------*/

    // /*Initialize your encoder if you have*/
    // encoder_init();

    // /*Register a encoder input device*/
    // lv_indev_drv_init(&indev_drv);
    // indev_drv.type = LV_INDEV_TYPE_ENCODER;
    // indev_drv.read_cb = encoder_read;
    // indev_encoder = lv_indev_drv_register(&indev_drv);

    // /*Later you should create group(s) with `lv_group_t * group = lv_group_create()`,
    //  *add objects to the group with `lv_group_add_obj(group, obj)`
    //  *and assign this input device to group to navigate in it:
    //  *`lv_indev_set_group(indev_encoder, group);`*/

    // /*------------------
    //  * Button
    //  * -----------------*/

    // /*Initialize your button if you have*/
    // button_init();

    // /*Register a button input device*/
    // lv_indev_drv_init(&indev_drv);
    // indev_drv.type = LV_INDEV_TYPE_BUTTON;
    // indev_drv.read_cb = button_read;
    // indev_button = lv_indev_drv_register(&indev_drv);

    // /*Assign buttons to points on the screen*/
    // static const lv_point_t btn_points[2] = {
    //     {10, 10},   /*Button 0 -> x:10; y:10*/
    //     {40, 100},  /*Button 1 -> x:40; y:100*/
    // };
    // lv_indev_set_button_points(indev_button, btn_points);
}

5. 配置lv_conf.h

将lv_conf_template.h文件复制到lvgl->src文件夹,并重命名为lv_conf.h

image

打开宏开关

image

根据所用的屏幕参数配置颜色深度和颜色格式

image

检查IDF的menuconfig的LVGL配置。如果有,则找到LVGL configuration,取消勾选“Uncheck this to use custom lv_conf.h”项,确保 lv_conf.h生效

6. 编写CMakeLists文件

由于lvgl源码中有很多层文件夹,所以要对lvgl的文件夹进行递归查找,否则编译的时候可能会因为找不到某个.c或.h文件而报错

# idf_component_register(SRCS "LVGL.c"
#                     INCLUDE_DIRS "include")

# 用户配置

# .c递归目录:自动查找所有子目录的.c文件
set(C_RECURSIVE_DIRS
    lvgl
)

# .c非递归目录:只包含直接.c文件,不查找子目录
set(C_NON_RECURSIVE_DIRS
    .
    lv_port
)

# .h递归目录:自动查找所有子目录的.h文件
set(H_RECURSIVE_DIRS
    lvgl
)

# .h非递归目录:只包含直接.h文件,不查找子目录
set(H_NON_RECURSIVE_DIRS
    .
    lv_port
)

# 设置排除编译的文件(单个文件,可选)
set(exclude_srcs
    # aaa.c
    # XX/bbb.c
)

# 设置依赖库
set(requires
    driver
    esp_common
    esp_timer
    USER_DRIVER  #我这里新建了个组件存放上面LCD驱动的代码,因此要添加引用
)

# 自动处理逻辑
# 初始化变量
set(ALL_SOURCES "")
set(INCLUDE_DIRS "")

# 1. 处理.c递归目录
foreach(dir ${C_RECURSIVE_DIRS})
    file(GLOB_RECURSE sources "${dir}/*.c")
    list(APPEND ALL_SOURCES ${sources})
endforeach()

# 2. 处理.c非递归目录
foreach(dir ${C_NON_RECURSIVE_DIRS})
    file(GLOB sources "${dir}/*.c")
    list(APPEND ALL_SOURCES ${sources})
endforeach()

# 3. 处理.h递归目录(收集头文件目录)
foreach(dir ${H_RECURSIVE_DIRS})
    # 递归查找所有.h文件
    file(GLOB_RECURSE headers "${dir}/*.h")
    
    # 提取头文件所在目录
    foreach(header ${headers})
        get_filename_component(hdir ${header} DIRECTORY)
        list(APPEND INCLUDE_DIRS ${hdir})
    endforeach()
    
    # 添加基目录(即使没有找到.h文件)
    list(APPEND INCLUDE_DIRS ${dir})
endforeach()

# 4. 处理.h非递归目录(收集头文件目录)
foreach(dir ${H_NON_RECURSIVE_DIRS})
    # 只查找直接.h文件(不递归)
    file(GLOB headers "${dir}/*.h")
    
    # 提取头文件所在目录
    foreach(header ${headers})
        get_filename_component(hdir ${header} DIRECTORY)
        list(APPEND INCLUDE_DIRS ${hdir})
    endforeach()
    
    # 添加基目录(即使没有找到.h文件)
    list(APPEND INCLUDE_DIRS ${dir})
endforeach()

# 5. 清理和去重
if(ALL_SOURCES)
    list(REMOVE_DUPLICATES ALL_SOURCES)
endif()

if(INCLUDE_DIRS)
    list(REMOVE_DUPLICATES INCLUDE_DIRS)
endif()

# 6. 调试输出
# message("源文件总数: ${#ALL_SOURCES}")
# foreach(src ${ALL_SOURCES})
#     message("   ${src}")
# endforeach()
# 
# message("头文件目录: ${INCLUDE_DIRS}")
# foreach(inc ${INCLUDE_DIRS})
#     message("   ${inc}")
# endforeach()

# 注册组件
idf_component_register(
    SRCS ${ALL_SOURCES}
    INCLUDE_DIRS ${INCLUDE_DIRS}
    EXCLUDE_SRCS ${exclude_srcs}
    REQUIRES ${requires}
)

# 编译选项
# -O3: 最高优化级别,最大化性能
# -ffast-math: 加速浮点运算(如果项目中使用了浮点数)
# -Wno-error=format: 不把格式字符串警告当作错误
# -Wno-format: 不显示格式字符串警告
component_compile_options(-O3 -ffast-math -Wno-error=format -Wno-format)

至此,移植已经基本完成了。

三. 使用测试

demo配置

打开移植好的lv_conf.h文件,找到LV_USE_DEMO_MUSIC,打开该宏定义

image

在开启LV_USE_DEMO_MUSIC的同时,找到字体开启处,开启下面3种字体

image

使用测试

1. 使用定时器定时调用LVGL时基单元

2. 初始化屏幕

3. 初始化lvgl

4. 循环调用lvgl处理函数

static esp_timer_handle_t lvgl_tick_timer = NULL;

// 定时回调函数,每 1ms 触发
static void lv_tick_task(void *arg)
{
    lv_tick_inc(1);
}

// 初始化 LVGL Tick 定时器
void lvgl_tick_timer_init(void)
{
    const esp_timer_create_args_t timer_args = {
        .callback = &lv_tick_task,
        .arg = NULL,
        .dispatch_method = ESP_TIMER_TASK,
        .name = NULL};

    esp_timer_create(&timer_args, &lvgl_tick_timer);
    esp_timer_start_periodic(lvgl_tick_timer, 1000); // 1ms 触发
}

void app_main(void)
{
    LCD_Init();
    Backlight_Init();
    Backlight_Light(100);

    lvgl_tick_timer_init();
    lv_init();
    lv_port_disp_init();
    lv_port_indev_init();

    /********************* Demo *********************/
    // lv_demo_widgets();
    // lv_demo_keypad_encoder();
    // lv_demo_benchmark();
    // lv_demo_stress();
    // lv_demo_music();

    while (1)
    {
        lv_task_handler();
        // Backlight_Light(100);

        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

实际效果

image

五. 源码

其他的细节自行查阅源码:https://gitee.com/renmuqiao/esp32_-lvgl_-test

四. Q&A

可能有人问x2和y2坐标为什么要加1?有请Deepseek

image

不想阻塞等待传输完成,如何更改?

1. 修改lv_port_disp_init函数,把disp_drv变量改为全局变量,并在lv_port_disp.h中声明

2. 修改LCD驱动函数

image

image

3. 修改lv_port_disp.c中的disp_flush函数

image

posted @ 2025-12-11 17:25  乔木仁  阅读(10)  评论(0)    收藏  举报