【ESP32】立创实战派S3使用LovyanGFX+lvgl9

夕凪マナ 发布于 2025-05-13 98 次阅读


1. 创建项目

这次使用的板子是嘉立创的实战派S3,上面搭载了一块2寸的240*320分辨率彩屏,驱动芯片为st7789,触摸IC为ft6336。

在嘉立创给的例程中使用的LVGL版本是8.3.11,我打算尝试移植一下LVGL9.2.2。

使用ESP-IDF提供的hello_world例程,首先添加LVGL9.2.2,利用乐鑫的组件管理器自动添加LVGL组件

idf.py add-dependency "lvgl/lvgl^9.2.2"

从git上在把LovyanGFX clone下来,放在components文件夹下。

cd project_name/components
git clone https://github.com/lovyan03/LovyanGFX.git

LovyanGFX是用c++编写的图形库,所以顺手也把ESP-IDF的程序环境迁移到c++下,ESP-IDF是支持c++编程的。修改主函数文件名后缀为cpp,在app_main(void)前添加extern "C"即可

2. 编写程序

虽然LovyanGFX已经封装了多种屏幕驱动芯片的类,其中也包括立创实战派用到的st7789,不过实战派S3的屏幕的CS引脚是接在PCA9557这个IO扩展芯片上的,所以不能直接使用LovyanGFX封装的类。

2.1 PCA9557驱动

因此首先来完成PCA9557的驱动,这个IO扩展芯片使用I2C通信,直接参考嘉立创提供的驱动即可

// esp32_s3_szp.h
#include <string.h>
#include "math.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_check.h"
#include "driver/i2c.h"
#include "driver/spi_master.h"
#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#ifdef __cplusplus
extern "C" {
#endif

#define BSP_I2C_SDA           (GPIO_NUM_1)   // SDA引脚
#define BSP_I2C_SCL           (GPIO_NUM_2)   // SCL引脚

#define BSP_I2C_NUM           (0)            // I2C外设
#define BSP_I2C_FREQ_HZ       100000         // 100kHz

esp_err_t bsp_i2c_init(void);   // 初始化I2C接口

#define PCA9557_INPUT_PORT              0x00
#define PCA9557_OUTPUT_PORT             0x01
#define PCA9557_POLARITY_INVERSION_PORT 0x02
#define PCA9557_CONFIGURATION_PORT      0x03

#define LCD_CS_GPIO                 BIT(0)    // PCA9557_GPIO_NUM_1
#define PA_EN_GPIO                  BIT(1)    // PCA9557_GPIO_NUM_2
#define DVP_PWDN_GPIO               BIT(2)    // PCA9557_GPIO_NUM_3

#define PCA9557_SENSOR_ADDR             0x19        /*!< Slave address of the MPU9250 sensor */

#define SET_BITS(_m, _s, _v)  ((_v) ? (_m)|((_s)) : (_m)&~((_s)))

void pca9557_init(void);

void lcd_cs(uint8_t level);

void pa_en(uint8_t level);

void dvp_pwdn(uint8_t level);

#ifdef __cplusplus
}
#endif

因为我们主程序是用的c++语言,所以不要忘记使用extern "C"把c语言的.h文件包裹起来。

// esp32_s3_szp.c
#include <stdio.h>
#include "esp32_s3_szp.h"

static const char *TAG = "esp32_s3_szp";

esp_err_t bsp_i2c_init(void)
{
    i2c_config_t i2c_conf = {
        .mode = I2C_MODE_MASTER,
        .sda_io_num = BSP_I2C_SDA,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_io_num = BSP_I2C_SCL,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master.clk_speed = BSP_I2C_FREQ_HZ
    };
    i2c_param_config(BSP_I2C_NUM, &i2c_conf);

    return i2c_driver_install(BSP_I2C_NUM, i2c_conf.mode, 0, 0, 0);
}

// 读取PCA9557寄存器的值
esp_err_t pca9557_register_read(uint8_t reg_addr, uint8_t *data, size_t len)
{
    return i2c_master_write_read_device(BSP_I2C_NUM, PCA9557_SENSOR_ADDR,  &reg_addr, 1, data, len, 1000 / portTICK_PERIOD_MS);
}

// 给PCA9557的寄存器写值
esp_err_t pca9557_register_write_byte(uint8_t reg_addr, uint8_t data)
{
    uint8_t write_buf[2] = {reg_addr, data};

    return i2c_master_write_to_device(BSP_I2C_NUM, PCA9557_SENSOR_ADDR, write_buf, sizeof(write_buf), 1000 / portTICK_PERIOD_MS);
}

// 初始化PCA9557 IO扩展芯片
void pca9557_init(void)
{
    // 写入控制引脚默认值 DVP_PWDN=1  PA_EN = 0  LCD_CS = 1
    pca9557_register_write_byte(PCA9557_OUTPUT_PORT, 0x05);  
    // 把PCA9557芯片的IO1 IO1 IO2设置为输出 其它引脚保持默认的输入
    pca9557_register_write_byte(PCA9557_CONFIGURATION_PORT, 0xf8); 
}

// 设置PCA9557芯片的某个IO引脚输出高低电平
esp_err_t pca9557_set_output_state(uint8_t gpio_bit, uint8_t level)
{
    uint8_t data;
    esp_err_t res = ESP_FAIL;

    pca9557_register_read(PCA9557_OUTPUT_PORT, &data, 1);
    res = pca9557_register_write_byte(PCA9557_OUTPUT_PORT, SET_BITS(data, gpio_bit, level));

    return res;
}

// 控制 PCA9557_LCD_CS 引脚输出高低电平 参数0输出低电平 参数1输出高电平 
void lcd_cs(uint8_t level)
{
    pca9557_set_output_state(LCD_CS_GPIO, level);
}

// 控制 PCA9557_PA_EN 引脚输出高低电平 参数0输出低电平 参数1输出高电平 
void pa_en(uint8_t level)
{
    pca9557_set_output_state(PA_EN_GPIO, level);
}

// 控制 PCA9557_DVP_PWDN 引脚输出高低电平 参数0输出低电平 参数1输出高电平 
void dvp_pwdn(uint8_t level)
{
    pca9557_set_output_state(DVP_PWDN_GPIO, level);
}

我们主要用到的就是lcd_cs这个函数来控制屏幕的CS引脚。

2.2 重写CS引脚操作方法

完成了PCA9557的驱动,之后就是重写LovyanGFX的st7789类,在源码里可以看到提供了CS引脚和RST引脚控制的四个虚函数,可以自己定义引脚操作。

创建一个新的panel类继承Panel_ST7789即可。

// panel_st7789_override.hpp
#pragma once
#include <LovyanGFX.hpp>
#include "esp32_s3_szp.h"

namespace lgfx {
    class Panel_ST7789_Override : public lgfx::Panel_ST7789 {
    protected:
        void init_cs(void) override {
            lcd_cs(1);
        }
        void cs_control(bool level) override {
            if(level) {
                lcd_cs(1);
            } else {
                lcd_cs(0);
            }
        }
    };
}

之前用过微雪的一块ESP32-C6开发板,上面的1.47寸屏幕微雪提供的资料说是st7789驱动芯片,但是使用st7789的驱动却一直显示有问题,折腾了不少时间。看微雪提供的示例代码才发现它上面的st7789初始化方式不一样,之后也是照着LovyanGFX的panel重写了一份才成功把屏幕点亮,现在想来也是可以通过继承然后直接重写getInitCommands(uint8_t listno)这个方法来实现。

之后根据LovyanGFX的示例修改配置

// hal_display.hpp
#pragma once
#define LGFX_USE_V1
#define LGFX_AUTODETECT

#include "panel_st7789_override.hpp"

/// 独自の設定を行うクラスを、LGFX_Deviceから派生して作成します。
class LGFX_Vanilla : public lgfx::LGFX_Device {
    // 接続するパネルの型にあったインスタンスを用意します。
    lgfx::Panel_ST7789_Override     _panel_instance;
//    lgfx::Panel_ST7789 _panel_instance;
    // パネルを接続するバスの種類にあったインスタンスを用意します。
    lgfx::Bus_SPI      _bus_instance;
    lgfx::Light_PWM    _light_instance;
    lgfx::Touch_FT5x06 _touch_instance;

public:
    // コンストラクタを作成し、ここで各種設定を行います。
    // クラス名を変更した場合はコンストラクタも同じ名前を指定してください。
    LGFX_Vanilla()
    {
        { // バス制御の設定を行います。
            auto cfg = _bus_instance.config();    // バス設定用の構造体を取得します。

            // SPIバスの設定
            cfg.spi_host = SPI3_HOST;     // 使用するSPIを選択  ESP32-S2,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST
            // ※ ESP-IDFバージョンアップに伴い、VSPI_HOST , HSPI_HOSTの記述は非推奨になるため、エラーが出る場合は代わりにSPI2_HOST , SPI3_HOSTを使用してください。
            cfg.spi_mode = 3;             // SPI通信モードを設定 (0 ~ 3)
            //   cfg.freq_write = 1*1000*1000;    // 送信時のSPIクロック (最大80MHz, 80MHzを整数で割った値に丸められます)
            //   cfg.freq_write = 10*1000*1000;
            cfg.freq_write = 80*1000*1000;
            cfg.freq_read  = 16000000;    // 受信時のSPIクロック
            cfg.spi_3wire  = false;        // 受信をMOSIピンで行う場合はtrueを設定
            cfg.use_lock   = true;        // トランザクションロックを使用する場合はtrueを設定
            cfg.dma_channel = SPI_DMA_CH_AUTO; // 使用するDMAチャンネルを設定 (0=DMA不使用 / 1=1ch / 2=ch / SPI_DMA_CH_AUTO=自動設 定)
            cfg.dma_channel = 0;

            cfg.pin_sclk    = 41;
            cfg.pin_mosi    = 40;
            cfg.pin_miso    = -1;
            cfg.pin_dc      = 39;

            _bus_instance.config(cfg);    // 設定値をバスに反映します。
            _panel_instance.setBus(&_bus_instance);      // バスをパネルにセットします。
        }

        { // 表示パネル制御の設定を行います。
            auto cfg = _panel_instance.config();    // 表示パネル設定用の構造体を取得します。

            cfg.pin_cs           =    -1;  // CSが接続されているピン番号   (-1 = disable)
            cfg.pin_rst          =    -1;  // RSTが接続されているピン番号  (-1 = disable)
            cfg.pin_busy         =    -1;  // BUSYが接続されているピン番号 (-1 = disable)

            // ※ 以下の設定値はパネル毎に一般的な初期値が設定されていますので、不明な項目はコメントアウトして試してみてください。

            cfg.panel_width      =   240;  // 実際に表示可能な幅
            cfg.panel_height     =   320;  // 実際に表示可能な高さ

            cfg.offset_x         =     0;  // パネルのX方向オフセット量
            cfg.offset_y         =     0;  // パネルのY方向オフセット量
            cfg.offset_rotation  =     1;  // 回転方向の値のオフセット 0~7 (4~7は上下反転)
            cfg.dummy_read_pixel =     8;  // ピクセル読出し前のダミーリードのビット数
            cfg.dummy_read_bits  =     1;  // ピクセル以外のデータ読出し前のダミーリードのビット数
            cfg.readable         =  false;  // データ読出しが可能な場合 trueに設定
            cfg.invert           = true;  // パネルの明暗が反転してしまう場合 trueに設定
            cfg.rgb_order        = false;  // パネルの赤と青が入れ替わってしまう場合 trueに設定
            cfg.dlen_16bit       = false;  // 16bitパラレルやSPIでデータ長を16bit単位で送信するパネルの場合 trueに設定
            cfg.bus_shared       =  false;  // SDカードとバスを共有している場合 trueに設定(drawJpgFile等でバス制御を行います)

            _panel_instance.config(cfg);
        }

        { // バックライト制御の設定を行います。(必要なければ削除)
            auto cfg = _light_instance.config();    // バックライト設定用の構造体を取得します。

            cfg.pin_bl = 42;              // バックライトが接続されているピン番号
            cfg.invert = false;           // バックライトの輝度を反転させる場合 true
            cfg.freq   = 5000;           // バックライトのPWM周波数
            cfg.pwm_channel = 0;          // 使用するPWMのチャンネル番号

            _light_instance.config(cfg);
            _panel_instance.setLight(&_light_instance);  // バックライトをパネルにセットします。
        }

        { // タッチスクリーン制御の設定を行います。(必要なければ削除)
            auto cfg = _touch_instance.config();

            cfg.x_min      = 0;    // タッチスクリーンから得られる最小のX値(生の値)
            cfg.x_max      = 239;  // タッチスクリーンから得られる最大のX値(生の値)
            cfg.y_min      = 0;    // タッチスクリーンから得られる最小のY値(生の値)
            cfg.y_max      = 319;  // タッチスクリーンから得られる最大のY値(生の値)
            cfg.pin_int    = -1;   // INTが接続されているピン番号
            cfg.bus_shared = false; // 画面と共通のバスを使用している場合 trueを設定
            cfg.offset_rotation = 0;// 表示とタッチの向きのが一致しない場合の調整 0~7の値で設定

// I2C接続の場合
            cfg.i2c_port = 0;      // 使用するI2Cを選択 (0 or 1)
            cfg.i2c_addr = 0x38;   // I2Cデバイスアドレス番号
            cfg.pin_sda  = 1;     // SDAが接続されているピン番号
            cfg.pin_scl  = 2;     // SCLが接続されているピン番号
            cfg.freq = 400000;     // I2Cクロックを設定

            _touch_instance.config(cfg);
            _panel_instance.setTouch(&_touch_instance);  // タッチスクリーンをパネルにセットします。
        }
//*/

        setPanel(&_panel_instance); // 使用するパネルをセットします。
    }
};

立创实战派S3屏幕相关的引脚连接如下:

引脚 对应IO
spi_dc 39
spi_mosi 40
spi_sclk 41
back_light 42
tp_sda 1
tp_scl 2

其余引脚配置为-1即可,背光亮度通过pwm调节,触摸IC的ft6336类也已经提供,直接使用即可。

2.3 初始化LVGL

之后就是对LVGL进行初始化了。

// hal_lvgl.hpp
#pragma once

#include "esp_log.h"
#include "lvgl.h"
#include "display/hal_display.hpp"

namespace LVGL {

    static constexpr const char *TAG = "LVGL";
    static constexpr uint32_t LVGL_TICK_PERIOD_MS = 2;
    static constexpr uint32_t width = 240;
    static constexpr uint32_t height = 320;

    static lv_color_t *buf1 = nullptr;
    static lv_color_t *buf2 = nullptr;

    static lgfx::LGFX_Device *_disp = nullptr;

    class LVGL {
    private:
        static void _disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
            if(_disp->getStartCount() == 0) {
                _disp->endWrite();
            }
            _disp->pushImage(area->x1, area->y1, area->x2 - area->x1 + 1, area->y2 - area->y1 + 1, (lgfx::rgb565_t *)px_map);

            lv_display_flush_ready(disp);
        }

        static void _increase_lvgl_tick(void *args) {
            lv_tick_inc(LVGL_TICK_PERIOD_MS);
        }

        static inline void _lv_port_disp_init() {
            buf1 = static_cast<lv_color_t*>(heap_caps_malloc(width * height * sizeof(lv_color_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
            buf2 = static_cast<lv_color_t*>(heap_caps_malloc(width * height * sizeof(lv_color_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));;
            if (!buf1 || !buf2) {
                ESP_LOGE(TAG, "LVGL draw buffer allocation failed (PSRAM)");
                return;
            }
            ESP_LOGI(TAG, "buf1 allocated at: %p", buf1);
            ESP_LOGI(TAG, "buf2 allocated at: %p", buf2);

            // 创建显示对象
            lv_display_t *disp = lv_display_create(height, width);

            // 设置显示缓冲区
            ESP_LOGI(TAG, "设置显示缓冲区");
            lv_display_set_buffers(
                    disp,
                    buf1,
                    buf2,
                    sizeof(lv_color_t) * width * height,
                    LV_DISPLAY_RENDER_MODE_FULL
            );

            lv_display_set_flush_cb(disp, _disp_flush);

            // 开启 LVGL 系统时钟定时器
            ESP_LOGI(TAG, "开启LVGL系统时钟定时器");
            const esp_timer_create_args_t lvgl_tick_timer_args = {
                    .callback = _increase_lvgl_tick,
                    .name = "lvgl_tick"
            };
            esp_timer_handle_t lvgl_tick_timer = nullptr;
            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));
        }

        static inline void _lv_port_indev_init() {
            lv_indev_t *indev = lv_indev_create();
            lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
            lv_indev_set_read_cb(indev, [](lv_indev_t *indev, lv_indev_data_t *data) {
                if (!_disp) return;

                lgfx::touch_point_t tp;
                if (_disp->getTouch(&tp)) {
                    data->point.x = tp.x;
                    data->point.y = tp.y;
                    data->state = LV_INDEV_STATE_PRESSED;
                } else {
                    data->state = LV_INDEV_STATE_RELEASED;
                }
            });
        }

    public:
        inline void init(lgfx::LGFX_Device *disp) {
            ESP_LOGI(TAG, "Initializing LVGL 9.2.2...");
            _disp = disp;

            lv_init();
            _lv_port_disp_init();
            _lv_port_indev_init();

            ESP_LOGI(TAG, "LVGL initialized.");
        }

        static inline void update() {
            lv_timer_handler();
        }
    };
}

上面的buffer是开在PSRAM上的,所以需要在menuconfig里使能PSRAM,具体路径:

menuconfig->Component config->ESP PSRAM->(勾选Support for external, SPI-connected RAM)->SPI RAM config

要修改的有Mode修改为Octal Mode PSRAM,Allow .bss segment placed in external memory。否则在编译的时候会报错。另外也记得把主任务的栈大小调大一些,或者为lvgl单开一个任务。

写到这里,移植工作基本就已经完成了,后续就是在主程序里依次初始化i2c,pca9557,屏幕和LVGL了。

简单尝试了一下lv_demo_music,显示和触摸都是正常的。

此作者没有提供个人介绍
最后更新于 2025-05-13