In the previous post, we took our first major step by writing “Hello World!” directly to the screen of the WT32-SC01 Plus using the LovyanGFX library. This proved that our hardware and basic communication are working. Now, let’s level up and start building real graphical interfaces.

In this tutorial, we will introduce two powerful tools that will transform the way we create visual projects: the LVGL library and the SquareLine Studio design tool. Our goal will be to recreate the “Hello World!” message, but this time, the interface will be designed in a visual software and then integrated into our Arduino code.

The Tools of the Trade: LVGL and SquareLine Studio

  • LVGL (Light and Versatile Graphics Library): It’s an extremely popular open-source library for creating graphical interfaces on embedded systems. It offers a rich set of “widgets” (buttons, sliders, charts, etc.) and handles all the heavy lifting of rendering and events.
  • SquareLine Studio: It’s a visual design software that allows you to drag and drop elements to create complex interfaces. While it is a commercial tool, it offers a free license for personal and hobbyist use with some limitations (like the number of screens and widgets), which is perfect for our projects. In the end, it exports a complete C project that we can directly integrate into our code.

Step 1: Preparing the Environment for LVGL

In addition to LovyanGFX and the ESP32 board package we’ve already installed, we now need to add the LVGL library.

  1. In the Arduino IDE, go to Tools > Manage Libraries….
  2. Search for lvgl and install version 8.3.11. It’s important to use this version to ensure compatibility with the files exported by SquareLine Studio.
  3. Create the lv_conf.h file: This is a crucial step. a. Find the lv_conf_template.h file in the LVGL library folder. b. Copy it to your root libraries folder (e.g., Documents/Arduino/libraries). c. Rename the copy to lv_conf.h. d. Open the file and ensure the #define LV_COLOR_DEPTH line is set to 16.

Step 2: Creating the Interface in SquareLine Studio

  1. Open SquareLine Studio and click “Create” for a new project.
  2. In the template selection screen, click on the “Arduino” tab.
  3. Select the “Arduino with TFT_eSPI” template. It is very important to choose this one, as it generates the correct file structure for the Arduino IDE, even though we are using LovyanGFX in our final code.
  4. In the “Project Settings” tab, set the screen resolution to 480x320 and click “Create”.
  5. Drag a “Label” widget onto the screen.
  6. In the “Inspector” panel on the right, change the label’s text to “Hello World!” and adjust the font and alignment as desired.
  7. Before exporting, go to the File > Project Settings menu. In the window that opens, in the “FILE EXPORT” section, configure the “Project Export Root” and “UI Files Export Path” fields to a folder of your choice (for example, you can create an “exports” folder inside your SquareLine project). This prevents errors during export.
  8. Go to Export > Export UI Files to generate the project files.

Step 3: The Integration Code

Important:

  • Place all files exported by SquareLine Studio (which will be in the folder you configured in step 7 of Step 2) in the same folder as your .ino sketch.
  • In the ui.c file generated by SquareLine, find the TEST LVGL SETTINGS block and comment out the LV_COLOR_DEPTH check, leaving it like this:
    ///////////////////// TEST LVGL SETTINGS ////////////////////
    // #if LV_COLOR_DEPTH != 32
    //     #error "LV_COLOR_DEPTH should be 32bit to match SquareLine Studio's settings"
    // #endif
    #if LV_COLOR_16_SWAP !=0
        #error "LV_COLOR_16_SWAP should be 0 to match SquareLine Studio's settings"
    #endif
    

Now, let’s get to the code that ties everything together. It uses the same LovyanGFX base from the previous example but adds all the initialization and the loop needed for LVGL to work.

/*
 * EXAMPLE 2: HELLO WORLD WITH SQUARELINE STUDIO AND LOVYANGFX
 * * This sketch demonstrates how to integrate a simple graphical interface,
 * created in SquareLine Studio, with the LovyanGFX library.
 */

#define LGFX_USE_V1 // Define that we are using Version 1 of LovyanGFX

#include <LovyanGFX.hpp>
#include <lvgl.h>
#include "ui.h" // The main header for your graphical interface generated by SquareLine

// ======================================================================================
// === 1. HARDWARE CONFIGURATION (LOVYANGFX) ==========================================
// ======================================================================================
// This class describes the screen hardware for the LovyanGFX library.
class LGFX : public lgfx::LGFX_Device
{
  lgfx::Panel_ST7796  _panel_instance;
  lgfx::Bus_Parallel8 _bus_instance;
  lgfx::Light_PWM     _light_instance;

public:
  LGFX(void)
  {
    { 
      auto cfg = _bus_instance.config();
      cfg.port = 0; cfg.freq_write = 20000000;
      cfg.pin_wr = 47; cfg.pin_rd = -1; cfg.pin_rs = 0;
      cfg.pin_d0 = 9; cfg.pin_d1 = 46; cfg.pin_d2 = 3; cfg.pin_d3 = 8;
      cfg.pin_d4 = 18; cfg.pin_d5 = 17; cfg.pin_d6 = 16; cfg.pin_d7 = 15;
      _bus_instance.config(cfg);
      _panel_instance.setBus(&_bus_instance);
    }
    { 
      auto cfg = _panel_instance.config();
      cfg.pin_cs = -1; cfg.pin_rst = 4; cfg.pin_busy = -1;
      cfg.memory_width = 320; cfg.memory_height = 480;
      cfg.panel_width = 320; cfg.panel_height = 480;
      cfg.offset_x = 0; cfg.offset_y = 0;
      cfg.invert = true; cfg.rgb_order = false;
      _panel_instance.config(cfg);
    }
    { 
      auto cfg = _light_instance.config();
      cfg.pin_bl = 45; cfg.invert = false; cfg.freq = 44100; cfg.pwm_channel = 7;
      _light_instance.config(cfg);
      _panel_instance.setLight(&_light_instance);
    }
    setPanel(&_panel_instance);
  }
};

// --- Instances and Buffers ---
LGFX gfx; 
static const uint16_t screenWidth  = 480;
static const uint16_t screenHeight = 320;
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf1[screenWidth * screenHeight / 10];

// --- Interface Functions (Bridge LVGL <-> LovyanGFX) ---
void my_disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
    uint32_t w = (area->x2 - area->x1 + 1);
    uint32_t h = (area->y2 - area->y1 + 1);
    gfx.startWrite();
    gfx.setAddrWindow(area->x1, area->y1, w, h);
    gfx.pushPixels((uint16_t *)color_p, w * h, true);
    gfx.endWrite();
    lv_disp_flush_ready(disp_drv);
}

// --- Setup and Loop ---
void setup() {
    Serial.begin(115200);
    Serial.println("Starting Example 2: SquareLine Studio with LovyanGFX");
    gfx.begin();
    gfx.setRotation(1);
    gfx.setBrightness(255);
    lv_init();
    lv_disp_draw_buf_init(&draw_buf, buf1, NULL, screenWidth * screenHeight / 10);
    static lv_disp_drv_t disp_drv;
    lv_disp_drv_init(&disp_drv);
    disp_drv.hor_res = screenWidth;
    disp_drv.ver_res = screenHeight;
    disp_drv.flush_cb = my_disp_flush;
    disp_drv.draw_buf = &draw_buf;
    lv_disp_drv_register(&disp_drv);
    ui_init(); // Loads the interface from SquareLine
    Serial.println("Setup finished. The SquareLine interface should now appear.");
}

void loop() {
    lv_timer_handler();
    lv_tick_inc(5);
    delay(5);
}

Understanding the New Code

  • Includes: We now have #include <lvgl.h> for the graphics library and #include "ui.h" to load all the interface files generated by SquareLine Studio.
  • LVGL Buffers: The draw_buf and buf1 variables are memory areas that LVGL uses to render the interface before sending it to the screen.
  • my_disp_flush: This is the essential “bridge” function. LVGL calls it when it has something new to draw, and our code uses LovyanGFX to actually put the pixels on the screen.
  • setup(): The initialization now includes lv_init() and the registration of our display driver with LVGL. The most important call is ui_init(), which executes the code generated by SquareLine to create the widgets.
  • loop(): The loop is now the heart of LVGL. lv_timer_handler() processes all interface tasks. The lv_tick_inc(5) function tells LVGL that time is passing. Although it has no visible effect in this static example, it’s crucial for future animations and events, so we’ve included it as a best practice. The value 5 should always match the delay(5) to keep LVGL’s internal clock synchronized with real-time.

Conclusion

If everything went well, you should now see on your screen the same interface you designed in SquareLine Studio! You’ve just taken a giant leap: from drawing text with code to using a professional UI design tool.

In the next post, we will enable touch and make our first interactive interface.

See you then!