Skip to main content

Working with Arduino

This chapter contains the following sections. Please read as needed:

Arduino Getting Started

New to Arduino ESP32 development and looking for a quick start? We have prepared a comprehensive Getting Started Tutorial for you.

Note: This tutorial uses the ESP32-S3-Zero as a reference example, and all hardware code is based on its pinout. Before you start, we recommend checking the pinout of your development board to ensure the pin configuration is correct.

Setting Up Development Environment

1. Installing and Configuring Arduino IDE

Please refer to the tutorial Installing and Configuring Arduino IDE to download and install the Arduino IDE and add ESP32 support.

Board NameBoard Installation RequirementVersion Requirement
esp32 by Espressif Systems"Install Offline" / "Install Online"3.0.5

2. Installing Libraries

To run the example, you need to install the corresponding library. The example code uses the GFX Library for Arduino library to drive the ST7789V2 display and the Arduino_DriveBus library to drive the CST816T touch controller.

You can click this link to download the example package for the ESP32-S3-Touch-LCD-1.69 development board. The Arduino\libraries directory within this package contains all the necessary library files required for this tutorial.

Library/File NameDescriptionVersionInstallation Method
Arduino_DriveBusCST816, CST816 touch controller driver libraryv1.0.1Install manually
GFX Library for ArduinoST7789 display driver graphics libraryv1.4.9Install via library manager or manually
SensorLibPCF85063, QMI8658 sensor driver libraryv0.1.6Install via library manager or manually
lvglLVGL graphics libraryv8.4.0Install via library manager or manually
Mylibrary/pin_config.hBoard pin macro definition-Install manually
lv_conf.hLVGL configuration file-Install manually
Version Compatibility Notes

There are strong dependencies between versions of LVGL and its driver libraries. For example, a driver written for LVGL v8 may not be compatible with LVGL v9. To ensure that the examples can be reproduced reliably, it is recommended to use the specific versions listed in the table above. Mixing different versions of libraries may lead to compilation failures or runtime errors.

Installation Steps:

  1. Unzip the downloaded example package.

  2. Copy all folders (Arduino_DriveBus, GFX_Library_for_Arduino, etc.) in the Arduino\libraries directory to the Arduino library folder.

    info

    The path to the Arduino libraries folder is typically: c:\Users\<username>\Documents\Arduino\libraries.

    You can also locate it in the Arduino IDE by going to File > Preferences and checking the "Sketchbook location". The libraries folder is the libraries subfolder within this path.

  3. For other installation methods, please refer to: Arduino Library Management Tutorial.

3. Other Tips

  1. The ESP32-S3-Touch-LCD-1.69 can be directly selected in the Arduino IDE.

    Select ESP32-S3-Touch-LCD-1.69 in Arduino IDE
  2. The ESP32-S3-Touch-LCD-1.69 uses the ESP32-S3 native USB interface, not UART-to-USB. For serial communication:

    • The printf() function can be used directly;

    • To use the Serial.println() function, additional configuration is required: Enable the "USB CDC On Boot" option in the IDE's Tools menu, or declare an HWCDC object in your code to handle USB serial communication.

      note

      As shown in the figure, set "USB CDC On Boot" in the "Tools" option of the Arduino IDE

      Enable USBCDC

Example

The Arduino examples are located in the Arduino/examples directory of the example package.

ExampleBasic DescriptionDependency Library
01_HelloWorldDemonstrates basic graphics library functions, can also be used to test the basic performance of the display and random text display effectsGFX_Library_for_Arduino
02_Drawing_boardDemonstrates basic graphics library functions, can also be used to test the basic performance of the display and random text display effectsGFX_Library_for_Arduino, Arduino DriveBus
03_GFX_AsciiTablePrints ASCII characters in rows and columns on the screen according to the screen sizeGFX_Library_for_Arduino
04_GFX_ESPWiFiAnalyzerDraws Wi-Fi band signal strength on the ST7789 displayGFX_Library_for_Arduino
05_GFX_ClockA simple ST7789 clock example implementing a clock with simple tick marks and time managementGFX_Library_for_Arduino
06_GFX_PCF85063_simpleTimeDisplays the current timeSensorLib, GFX_Library_for_Arduino
07_LVGL_Measuring_voltageMeasures voltage using a voltage divider on a reserved pin, reads analog value from GPIO1 and calculates battery voltage using the divider formulaLVGL
08_LVGL_PCF85063_simpleTimeDisplays current time on the ST7789 display using the PCF85063 RTC module under LVGLLVGL, SensorLib
09_LVGL_Keys_BeeMulti‑function button usageLVGL
10_LVGL_QMI8658_uiUses LVGL for graphical display and communicates with the QMI8658 IMU to obtain accelerometer and gyroscope dataLVGL, SensorLib
11_LVGL_ArduinoLVGL exampleLVGL, Arduino DriveBus

01_HelloWorld

This example shows how to use the ST7789 display with the Arduino GFX library and the Arduino DriveBus library to implement dynamic text display. Specifically, it displays the text "Hello World!" at a fixed position on the display, then in a loop it displays the same text at random positions, colors, and sizes every second. This code can also be used to test the basic performance of the display.

Example 1 illustration

Code

01_HelloWorld.ino
#include <Arduino.h>
#include "Arduino_GFX_Library.h"
#include "pin_config.h"
#include <Wire.h>
#include "HWCDC.h"

HWCDC USBSerial;

Arduino_DataBus *bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCK, LCD_MOSI);

Arduino_GFX *gfx = new Arduino_ST7789(bus, LCD_RST /* RST */,
0 /* rotation */, true /* IPS */, LCD_WIDTH, LCD_HEIGHT, 0, 20, 0, 0);

void setup(void) {
USBSerial.begin(115200);
// USBSerial.setDebugOutput(true);
// while(!USBSerial);
USBSerial.println("Arduino_GFX Hello World example");

// Init Display
if (!gfx->begin()) {
USBSerial.println("gfx->begin() failed!");
}
gfx->fillScreen(BLACK);

pinMode(LCD_BL, OUTPUT);
digitalWrite(LCD_BL, HIGH);

gfx->setCursor(10, 10);
gfx->setTextColor(RED);
gfx->println("Hello World!");

delay(5000); // 5 seconds
}

void loop() {
gfx->setCursor(random(gfx->width()), random(gfx->height()));
gfx->setTextColor(random(0xffff), random(0xffff));
gfx->setTextSize(random(6) /* x scale */, random(6) /* y scale */, random(2) /* pixel_margin */);
gfx->println("Hello World!");

delay(1000); // 1 second
}

Code Analysis

  • Display initialization:

    if (!gfx->begin()) {
    USBSerial.println("gfx->begin() failed!");
    }
  • Clear the screen and display text:

    gfx->fillScreen(BLACK);
    gfx->setCursor(10, 10);
    gfx->setTextColor(RED);
    gfx->println("Hello World!");
  • Animated display:

    gfx->setCursor(random(gfx->width()), random(gfx->height()));
    gfx->setTextColor(random(0xffff), random(0xffff));
    gfx->setTextSize(random(6), random(6), random(2));
    gfx->println("Hello World!");

02_Drawing_board

This example demonstrates how to use the CST816T touch controller and the Arduino GFX library to implement touch interaction on an ESP32 development board. The code initializes the touch controller and configures the display. In the loop, the program reads touch coordinates and draws a dot at the corresponding position on the screen when touch input is detected. This example can be used to test the basic response of the touch screen and provides a foundation for touch‑interactive applications. When the program starts, it also shows a loading effect on the display from dark to full brightness, giving the user a dynamic visual experience.

Example 2 illustration

Code

02_Drawing_board.ino
#include <Wire.h>
#include <Arduino.h>
#include "pin_config.h"
#include "Arduino_GFX_Library.h"
#include "Arduino_DriveBus_Library.h"
#include "HWCDC.h"
HWCDC USBSerial;

Arduino_DataBus *bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCK, LCD_MOSI);

Arduino_GFX *gfx = new Arduino_ST7789(bus, LCD_RST /* RST */,
0 /* rotation */, true /* IPS */, LCD_WIDTH, LCD_HEIGHT, 0, 20, 0, 0);

std::shared_ptr<Arduino_IIC_DriveBus> IIC_Bus =
std::make_shared<Arduino_HWIIC>(IIC_SDA, IIC_SCL, &Wire);

void Arduino_IIC_Touch_Interrupt(void);

std::unique_ptr<Arduino_IIC> CST816T(new Arduino_CST816x(IIC_Bus, CST816T_DEVICE_ADDRESS,
TP_RST, TP_INT, Arduino_IIC_Touch_Interrupt));

void Arduino_IIC_Touch_Interrupt(void) {
CST816T->IIC_Interrupt_Flag = true;
}

void setup() {
USBSerial.begin(115200);
Wire.begin(IIC_SDA, IIC_SCL);

while (CST816T->begin() == false) {
USBSerial.println("CST816T initialization fail");
delay(2000);
}
USBSerial.println("CST816T initialization successfully");

CST816T->IIC_Write_Device_State(CST816T->Arduino_IIC_Touch::Device::TOUCH_DEVICE_INTERRUPT_MODE,
CST816T->Arduino_IIC_Touch::Device_Mode::TOUCH_DEVICE_INTERRUPT_PERIODIC);

gfx->begin();
pinMode(LCD_BL, OUTPUT);
digitalWrite(LCD_BL, HIGH);
gfx->fillScreen(WHITE);

for (int i = 0; i <= 255; i++) //0-255
{
gfx->Display_Brightness(i);
gfx->setCursor(20, 100);
gfx->setTextColor(BLUE);
gfx->setTextSize(2);
gfx->println("Loading board");
delay(3);
}
delay(500);
gfx->fillScreen(WHITE);
}

void loop() {
int32_t touchX = CST816T->IIC_Read_Device_Value(CST816T->Arduino_IIC_Touch::Value_Information::TOUCH_COORDINATE_X);
int32_t touchY = CST816T->IIC_Read_Device_Value(CST816T->Arduino_IIC_Touch::Value_Information::TOUCH_COORDINATE_Y);

USBSerial.printf("Touch X:%d Y:%d\n", touchX, touchY);
if (touchX > 20 && touchY > 20) {
gfx->fillCircle(touchX, touchY, 3, BLUE);
}
}

Code Analysis

  • Display initialization and brightness fade animation:

    gfx->begin();
    gfx->fillScreen(WHITE);
    for(int i = 0;i <= 255;i++){
    gfx->Display_Brightness(i);
    gfx->setCursor(30, 150);
    gfx->setTextColor(BLUE);
    gfx->setTextSize(4);
    gfx->println("Loading board");
    delay(3);
    }

03_GFX_AsciiTable

This example shows how to create an ASCII table display effect using the ST7789 display and the Arduino GFX library. Specifically, after initializing the display, the code displays ASCII character indices on the screen according to predefined colors and positions. First, the string "Arduino_GFX AsciiTable example" is printed to the serial monitor via USBSerial. Then indices are displayed in two colors (green and blue) at fixed positions, followed by the full ASCII character table displayed in white on a black background.

Example 3 illustration

Code

03_GFX_AsciiTable.ino
#include <Arduino.h>
#include "Arduino_GFX_Library.h"
#include "pin_config.h"
#include <Wire.h>
#include "HWCDC.h"

HWCDC USBSerial;

Arduino_DataBus *bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCK, LCD_MOSI);

Arduino_GFX *gfx = new Arduino_ST7789(bus, LCD_RST /* RST */,
0 /* rotation */, true /* IPS */, LCD_WIDTH, LCD_HEIGHT, 0, 20, 0, 0);

void setup(void) {
USBSerial.begin(115200);
// USBSerial.setDebugOutput(true);
// while(!USBSerial);
USBSerial.println("Arduino_GFX AsciiTable example");

int numCols = LCD_WIDTH / 8;
int numRows = LCD_HEIGHT / 10;

#ifdef GFX_EXTRA_PRE_INIT
GFX_EXTRA_PRE_INIT();
#endif

// Init Display
if (!gfx->begin()) {
USBSerial.println("gfx->begin() failed!");
}
gfx->fillScreen(BLACK);

pinMode(LCD_BL, OUTPUT);
digitalWrite(LCD_BL, HIGH);

gfx->setTextColor(GREEN);
for (int x = 0; x < numRows; x++) {
gfx->setCursor(10 + x * 8, 2);
gfx->print(x, 16);
}
gfx->setTextColor(BLUE);
for (int y = 0; y < numCols; y++) {
gfx->setCursor(2, 12 + y * 10);
gfx->print(y, 16);
}

char c = 0;
for (int y = 0; y < numRows; y++) {
for (int x = 0; x < numCols; x++) {
gfx->drawChar(10 + x * 8, 12 + y * 10, c++, WHITE, BLACK);
}
}

delay(5000); // 5 seconds
}

void loop() {
}

Code Analysis

  • Initialize the display:

    if (!gfx->begin()) {
    USBSerial.println("gfx->begin() failed!");
    }
  • Calculate rows and columns and label them:

    Based on the display dimensions, it calculates the number of columns and rows that can be displayed. Then, using two loops with different text colors, it prints row and column numbers on the display, making it easy to determine character positions when drawing the ASCII table later.

    int numCols = LCD_WIDTH / 8;
    int numRows = LCD_HEIGHT / 10;

    // Label the line number
    gfx->setTextColor(GREEN);
    for (int x = 0; x < numRows; x++) {
    gfx->setCursor(10 + x * 8, 2);
    gfx->print(x, 16);
    }

    // Label the column number
    gfx->setTextColor(BLUE);
    for (int y = 0; y < numCols; y++) {
    gfx->setCursor(2, 12 + y * 10);
    gfx->print(y, 16);
    }
  • Draws the ASCII character table:

    char c = 0;
    for (int y = 0; y < numRows; y++) {
    for (int x = 0; x < numCols; x++) {
    gfx->drawChar(10 + x * 8, 12 + y * 10, c++, WHITE, BLACK);
    }
    }

04_GFX_ESPWiFiAnalyzer

This example demonstrates drawing Wi-Fi band signal strength on the ST7789 display, implementing the functionality of a Wi-Fi analyzer.

Example 4 illustration

Code

04_GFX_ESPWiFiAnalyzer.ino
#include <Arduino.h>
#include "Arduino_GFX_Library.h"
#include "pin_config.h"
#include <Wire.h>
#include "HWCDC.h"

HWCDC USBSerial;

Arduino_DataBus *bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCK, LCD_MOSI);

Arduino_GFX *gfx = new Arduino_ST7789(bus, LCD_RST /* RST */,
0 /* rotation */, true /* IPS */, LCD_WIDTH, LCD_HEIGHT, 0, 20, 0, 0);

#if defined(ESP32)
#include "WiFi.h"
#else
#include "ESP8266WiFi.h"
#define log_i(format, ...) Serial.printf(format, ##__VA_ARGS__)
#endif

int16_t w, h, text_size, banner_height, graph_baseline, graph_height, channel_width, signal_width;

// RSSI RANGE
#define RSSI_CEILING -40
#define RSSI_FLOOR -100

// Channel color mapping from channel 1 to 14
uint16_t channel_color[] = {
RED, ORANGE, YELLOW, GREEN, CYAN, BLUE, MAGENTA,
RED, ORANGE, YELLOW, GREEN, CYAN, BLUE, MAGENTA
};

uint8_t scan_count = 0;

void setup() {
Serial.begin(115200);
// Serial.setDebugOutput(true);
// while(!Serial);
Serial.println("Arduino_GFX ESP WiFi Analyzer example");

// Set Wi-Fi to station mode and disconnect from an AP if it was previously connected
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);

#ifdef GFX_EXTRA_PRE_INIT
GFX_EXTRA_PRE_INIT();
#endif

#if defined(LCD_PWR_PIN)
pinMode(LCD_PWR_PIN, OUTPUT); // sets the pin as output
digitalWrite(LCD_PWR_PIN, HIGH); // power on
#endif

// Init Display
if (!gfx->begin()) {
Serial.println("gfx->begin() failed!");
}
w = gfx->width();
h = gfx->height();
text_size = (h < 200) ? 1 : 2;
banner_height = text_size * 3 * 4;
graph_baseline = h - 20; // minus 2 text lines
graph_height = graph_baseline - banner_height - 30; // minus 3 text lines
channel_width = w / 17;
signal_width = channel_width * 2;
pinMode(LCD_BL, OUTPUT);
digitalWrite(LCD_BL, HIGH);

// init banner
gfx->setTextSize(text_size);
gfx->fillScreen(BLACK);
gfx->setTextColor(RED);
gfx->setCursor(0, 0);
gfx->print("ESP");
gfx->setTextColor(WHITE);
gfx->print(" WiFi Analyzer");
}

bool matchBssidPrefix(uint8_t *a, uint8_t *b) {
for (uint8_t i = 0; i < 5; i++) { // only compare first 5 bytes
if (a[i] != b[i]) {
return false;
}
}
return true;
}

void loop() {
uint8_t ap_count_list[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int32_t noise_list[] = {RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR};
int32_t peak_list[] = {RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR, RSSI_FLOOR};
int16_t peak_id_list[] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
int32_t channel;
int16_t idx;
int32_t rssi;
uint8_t *bssid;
String ssid;
uint16_t color;
int16_t height, offset, text_width;

// WiFi.scanNetworks will return the number of networks found
#if defined(ESP32)
int n = WiFi.scanNetworks(false /* async */, true /* show_hidden */, true /* passive */, 500 /* max_ms_per_chan */);
#else
int n = WiFi.scanNetworks(false /* async */, true /* show_hidden */);
#endif

// clear old graph
gfx->fillRect(0, banner_height, w, h - banner_height, BLACK);
gfx->setTextSize(1);

if (n == 0) {
gfx->setTextColor(WHITE);
gfx->setCursor(0, banner_height);
gfx->println("no networks found");
}else{
for (int i = 0; i < n; i++) {
channel = WiFi.channel(i);
idx = channel - 1;
rssi = WiFi.RSSI(i);
bssid = WiFi.BSSID(i);

// channel peak stat
if (peak_list[idx] < rssi) {
peak_list[idx] = rssi;
peak_id_list[idx] = i;
}

// check signal come from same AP
bool duplicate_SSID = false;
for (int j = 0; j < i; j++) {
if ((WiFi.channel(j) == channel) && matchBssidPrefix(WiFi.BSSID(j), bssid)) {
duplicate_SSID = true;
break;
}
}

if (!duplicate_SSID) {
ap_count_list[idx]++;

// noise stat
int32_t noise = rssi - RSSI_FLOOR;
noise *= noise;
if (channel > 4) {
noise_list[idx - 4] += noise;
}
if (channel > 3) {
noise_list[idx - 3] += noise;
}
if (channel > 2) {
noise_list[idx - 2] += noise;
}
if (channel > 1) {
noise_list[idx - 1] += noise;
}
noise_list[idx] += noise;
if (channel < 14) {
noise_list[idx + 1] += noise;
}
if (channel < 13) {
noise_list[idx + 2] += noise;
}
if (channel < 12) {
noise_list[idx + 3] += noise;
}
if (channel < 11) {
noise_list[idx + 4] += noise;
}
}
}

// plot found WiFi info
for (int i = 0; i < n; i++) {
channel = WiFi.channel(i);
idx = channel - 1;
rssi = WiFi.RSSI(i);
color = channel_color[idx];
height = constrain(map(rssi, RSSI_FLOOR, RSSI_CEILING, 1, graph_height), 1, graph_height);
offset = (channel + 1) * channel_width;

// trim rssi with RSSI_FLOOR
if (rssi < RSSI_FLOOR) {
rssi = RSSI_FLOOR;
}

// plot chart
// gfx->drawLine(offset, graph_baseline - height, offset - signal_width, graph_baseline + 1, color);
// gfx->drawLine(offset, graph_baseline - height, offset + signal_width, graph_baseline + 1, color);
gfx->startWrite();
gfx->writeEllipseHelper(offset, graph_baseline + 1, signal_width, height, 0b0011, color);
gfx->endWrite();

if (i == peak_id_list[idx]) {
// Print SSID, signal strengh and if not encrypted
String ssid = WiFi.SSID(i);
if (ssid.length() == 0) {
ssid = WiFi.BSSIDstr(i);
}
text_width = (ssid.length() + 6) * 6;
if (text_width > w) {
offset = 0;
}else{
offset -= signal_width;
if ((offset + text_width) > w) {
offset = w - text_width;
}
}
gfx->setTextColor(color);
gfx->setCursor(offset, graph_baseline - 10 - height);
gfx->print(ssid);
gfx->print('(');
gfx->print(rssi);
gfx->print(')');
#if defined(ESP32)
if (WiFi.encryptionType(i) == WIFI_AUTH_OPEN)
#else
if (WiFi.encryptionType(i) == ENC_TYPE_NONE)
#endif
{
gfx->print('*');
}
}
}
}

// print WiFi stat
gfx->setTextColor(WHITE);
gfx->setCursor(0, banner_height);
gfx->print(n);
gfx->print(" networks found, lesser noise channels: ");
bool listed_first_channel = false;
int32_t min_noise = noise_list[0]; // init with channel 1 value
for (channel = 2; channel <= 11; channel++) // channels 12-14 may not available
{
idx = channel - 1;
log_i("min_noise: %d, noise_list[%d]: %d", min_noise, idx, noise_list[idx]);
if (noise_list[idx] < min_noise) {
min_noise = noise_list[idx];
}
}

for (channel = 1; channel <= 11; channel++) // channels 12-14 may not available
{
idx = channel - 1;
// check channel with min noise
if (noise_list[idx] == min_noise) {
if (!listed_first_channel) {
listed_first_channel = true;
}else{
gfx->print(", ");
}
gfx->print(channel);
}
}

// draw graph base axle
gfx->drawFastHLine(0, graph_baseline, gfx->width(), WHITE);
for (channel = 1; channel <= 14; channel++) {
idx = channel - 1;
offset = (channel + 1) * channel_width;
gfx->setTextColor(channel_color[idx]);
gfx->setCursor(offset - ((channel < 10) ? 3 : 6), graph_baseline + 2);
gfx->print(channel);
if (ap_count_list[idx] > 0) {
gfx->setCursor(offset - ((ap_count_list[idx] < 10) ? 9 : 12), graph_baseline + 8 + 2);
gfx->print('{');
gfx->print(ap_count_list[idx]);
gfx->print('}');
}
}

// Wait a bit before scanning again
// delay(SCAN_INTERVAL);

#if defined(SCAN_COUNT_SLEEP)
// POWER SAVING
if (++scan_count >= SCAN_COUNT_SLEEP) {
#if defined(LCD_PWR_PIN)
pinMode(LCD_PWR_PIN, INPUT); // disable pin
#endif

#if defined(GFX_BL)
pinMode(GFX_BL, INPUT); // disable pin
#endif

#if defined(ESP32)
esp_sleep_enable_ext0_wakeup(GPIO_NUM_36, LOW);
esp_deep_sleep_start();
#else
ESP.deepSleep(0);
#endif
}
#endif // defined(SCAN_COUNT_SLEEP)
}

Code Analysis

  • setup():

    Initializes serial communication; Sets Wi-Fi to station mode and disconnects; Initializes the display, gets screen dimensions and calculates various drawing parameters; Sets the screen background to black and draws the title bar.

  • loop():

    Scans Wi-Fi networks and retrieves network information, including channel, RSSI, BSSID, and SSID; Counts the number of networks per channel, noise levels, and peak signal strengths; Clears the old graph and draws a new one based on the scan results, including signal strength ellipses and network information text; Prints the number of networks found and the channels with the least noise; Draws the graph baseline and channel numbers; Enters low‑power mode based on conditions.

05_GFX_Clock

This example demonstrates a simple ST7789 clock example, implementing a clock with simple tick marks and time management.

Example 5 illustration

Code

05_GFX_Clock.ino
#include <Arduino.h>
#include "Arduino_GFX_Library.h"
#include "pin_config.h"
#include <Wire.h>
#include "HWCDC.h"

HWCDC USBSerial;
// SensorPCF85063 rtc;

Arduino_DataBus *bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCK, LCD_MOSI);

Arduino_GFX *gfx = new Arduino_ST7789(bus, LCD_RST /* RST */,
0 /* rotation */, true /* IPS */, LCD_WIDTH, LCD_HEIGHT, 0, 20, 0, 0);

#define BACKGROUND BLACK
#define MARK_COLOR WHITE
#define SUBMARK_COLOR DARKGREY // LIGHTGREY
#define HOUR_COLOR WHITE
#define MINUTE_COLOR BLUE // LIGHTGREY
#define SECOND_COLOR RED

#define SIXTIETH 0.016666667
#define TWELFTH 0.08333333
#define SIXTIETH_RADIAN 0.10471976
#define TWELFTH_RADIAN 0.52359878
#define RIGHT_ANGLE_RADIAN 1.5707963

static uint8_t conv2d(const char *p)
{
uint8_t v = 0;
return (10 * (*p - '0')) + (*++p - '0');
}

static int16_t w, h, center;
static int16_t hHandLen, mHandLen, sHandLen, markLen;
static float sdeg, mdeg, hdeg;
static int16_t osx = 0, osy = 0, omx = 0, omy = 0, ohx = 0, ohy = 0; // Saved H, M, S x & y coords
static int16_t nsx, nsy, nmx, nmy, nhx, nhy; // H, M, S x & y coords
static int16_t xMin, yMin, xMax, yMax; // redraw range
static int16_t hh, mm, ss;
static unsigned long targetTime; // next action time

static int16_t *cached_points;
static uint16_t cached_points_idx = 0;
static int16_t *last_cached_point;

void setup(void)
{
USBSerial.begin(115200);
USBSerial.println("Arduino_GFX Clock example");

// Init Display
if (!gfx->begin())
{
USBSerial.println("gfx->begin() failed!");
}
gfx->fillScreen(BACKGROUND);

pinMode(LCD_BL, OUTPUT);
digitalWrite(LCD_BL, HIGH);

// init LCD constant
w = gfx->width();
h = gfx->height();
if (w < h)
{
center = w / 2;
}
else
{
center = h / 2;
}
hHandLen = center * 3 / 8;
mHandLen = center * 2 / 3;
sHandLen = center * 5 / 6;
markLen = sHandLen / 6;
cached_points = (int16_t *)malloc((hHandLen + 1 + mHandLen + 1 + sHandLen + 1) * 2 * 2);

// Draw 60 clock marks
draw_round_clock_mark(
// draw_square_clock_mark(
center - markLen, center,
center - (markLen * 2 / 3), center,
center - (markLen / 2), center);

hh = conv2d(__TIME__);
mm = conv2d(__TIME__ + 3);
ss = conv2d(__TIME__ + 6);

targetTime = ((millis() / 1000) + 1) * 1000;
}

void loop()
{
unsigned long cur_millis = millis();
if (cur_millis >= targetTime)
{
targetTime += 1000;
ss++; // Advance second
if (ss == 60)
{
ss = 0;
mm++; // Advance minute
if (mm > 59)
{
mm = 0;
hh++; // Advance hour
if (hh > 23)
{
hh = 0;
}
}
}
}

// Pre-compute hand degrees, x & y coords for a fast screen update
sdeg = SIXTIETH_RADIAN * ((0.001 * (cur_millis % 1000)) + ss); // 0-59 (includes millis)
nsx = cos(sdeg - RIGHT_ANGLE_RADIAN) * sHandLen + center;
nsy = sin(sdeg - RIGHT_ANGLE_RADIAN) * sHandLen + center;
if ((nsx != osx) || (nsy != osy))
{
mdeg = (SIXTIETH * sdeg) + (SIXTIETH_RADIAN * mm); // 0-59 (includes seconds)
hdeg = (TWELFTH * mdeg) + (TWELFTH_RADIAN * hh); // 0-11 (includes minutes)
mdeg -= RIGHT_ANGLE_RADIAN;
hdeg -= RIGHT_ANGLE_RADIAN;
nmx = cos(mdeg) * mHandLen + center;
nmy = sin(mdeg) * mHandLen + center;
nhx = cos(hdeg) * hHandLen + center;
nhy = sin(hdeg) * hHandLen + center;

// redraw hands
redraw_hands_cached_draw_and_erase();

ohx = nhx;
ohy = nhy;
omx = nmx;
omy = nmy;
osx = nsx;
osy = nsy;

delay(1);
}
}

void draw_round_clock_mark(int16_t innerR1, int16_t outerR1, int16_t innerR2, int16_t outerR2, int16_t innerR3, int16_t outerR3)
{
float x, y;
int16_t x0, x1, y0, y1, innerR, outerR;
uint16_t c;

for (uint8_t i = 0; i < 60; i++)
{
if ((i % 15) == 0)
{
innerR = innerR1;
outerR = outerR1;
c = MARK_COLOR;
}
else if ((i % 5) == 0)
{
innerR = innerR2;
outerR = outerR2;
c = MARK_COLOR;
}
else
{
innerR = innerR3;
outerR = outerR3;
c = SUBMARK_COLOR;
}

mdeg = (SIXTIETH_RADIAN * i) - RIGHT_ANGLE_RADIAN;
x = cos(mdeg);
y = sin(mdeg);
x0 = x * outerR + center;
y0 = y * outerR + center;
x1 = x * innerR + center;
y1 = y * innerR + center;

gfx->drawLine(x0, y0, x1, y1, c);
}
}

void draw_square_clock_mark(int16_t innerR1, int16_t outerR1, int16_t innerR2, int16_t outerR2, int16_t innerR3, int16_t outerR3)
{
float x, y;
int16_t x0, x1, y0, y1, innerR, outerR;
uint16_t c;

for (uint8_t i = 0; i < 60; i++)
{
if ((i % 15) == 0)
{
innerR = innerR1;
outerR = outerR1;
c = MARK_COLOR;
}
else if ((i % 5) == 0)
{
innerR = innerR2;
outerR = outerR2;
c = MARK_COLOR;
}
else
{
innerR = innerR3;
outerR = outerR3;
c = SUBMARK_COLOR;
}

if ((i >= 53) || (i < 8))
{
x = tan(SIXTIETH_RADIAN * i);
x0 = center + (x * outerR);
y0 = center + (1 - outerR);
x1 = center + (x * innerR);
y1 = center + (1 - innerR);
}
else if (i < 23)
{
y = tan((SIXTIETH_RADIAN * i) - RIGHT_ANGLE_RADIAN);
x0 = center + (outerR);
y0 = center + (y * outerR);
x1 = center + (innerR);
y1 = center + (y * innerR);
}
else if (i < 38)
{
x = tan(SIXTIETH_RADIAN * i);
x0 = center - (x * outerR);
y0 = center + (outerR);
x1 = center - (x * innerR);
y1 = center + (innerR);
}
else if (i < 53)
{
y = tan((SIXTIETH_RADIAN * i) - RIGHT_ANGLE_RADIAN);
x0 = center + (1 - outerR);
y0 = center - (y * outerR);
x1 = center + (1 - innerR);
y1 = center - (y * innerR);
}
gfx->drawLine(x0, y0, x1, y1, c);
}
}

void redraw_hands_cached_draw_and_erase()
{
gfx->startWrite();
draw_and_erase_cached_line(center, center, nsx, nsy, SECOND_COLOR, cached_points, sHandLen + 1, false, false);
draw_and_erase_cached_line(center, center, nhx, nhy, HOUR_COLOR, cached_points + ((sHandLen + 1) * 2), hHandLen + 1, true, false);
draw_and_erase_cached_line(center, center, nmx, nmy, MINUTE_COLOR, cached_points + ((sHandLen + 1 + hHandLen + 1) * 2), mHandLen + 1, true, true);
gfx->endWrite();
}

void draw_and_erase_cached_line(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t color, int16_t *cache, int16_t cache_len, bool cross_check_second, bool cross_check_hour)
{
#if defined(ESP8266)
yield();
#endif
bool steep = _diff(y1, y0) > _diff(x1, x0);
if (steep)
{
_swap_int16_t(x0, y0);
_swap_int16_t(x1, y1);
}

int16_t dx, dy;
dx = _diff(x1, x0);
dy = _diff(y1, y0);

int16_t err = dx / 2;
int8_t xstep = (x0 < x1) ? 1 : -1;
int8_t ystep = (y0 < y1) ? 1 : -1;
x1 += xstep;
int16_t x, y, ox, oy;
for (uint16_t i = 0; i <= dx; i++)
{
if (steep)
{
x = y0;
y = x0;
}
else
{
x = x0;
y = y0;
}
ox = *(cache + (i * 2));
oy = *(cache + (i * 2) + 1);
if ((x == ox) && (y == oy))
{
if (cross_check_second || cross_check_hour)
{
write_cache_pixel(x, y, color, cross_check_second, cross_check_hour);
}
}
else
{
write_cache_pixel(x, y, color, cross_check_second, cross_check_hour);
if ((ox > 0) || (oy > 0))
{
write_cache_pixel(ox, oy, BACKGROUND, cross_check_second, cross_check_hour);
}
*(cache + (i * 2)) = x;
*(cache + (i * 2) + 1) = y;
}
if (err < dy)
{
y0 += ystep;
err += dx;
}
err -= dy;
x0 += xstep;
}
for (uint16_t i = dx + 1; i < cache_len; i++)
{
ox = *(cache + (i * 2));
oy = *(cache + (i * 2) + 1);
if ((ox > 0) || (oy > 0))
{
write_cache_pixel(ox, oy, BACKGROUND, cross_check_second, cross_check_hour);
}
*(cache + (i * 2)) = 0;
*(cache + (i * 2) + 1) = 0;
}
}

void write_cache_pixel(int16_t x, int16_t y, int16_t color, bool cross_check_second, bool cross_check_hour)
{
int16_t *cache = cached_points;
if (cross_check_second)
{
for (uint16_t i = 0; i <= sHandLen; i++)
{
if ((x == *(cache++)) && (y == *(cache)))
{
return;
}
cache++;
}
}
if (cross_check_hour)
{
cache = cached_points + ((sHandLen + 1) * 2);
for (uint16_t i = 0; i <= hHandLen; i++)
{
if ((x == *(cache++)) && (y == *(cache)))
{
return;
}
cache++;
}
}
gfx->writePixel(x, y, color);
}

Code Analysis

  • Drawing hour, minute, and second hands:

    void redraw_hands_cached_draw_and_erase() {
    gfx->startWrite();
    draw_and_erase_cached_line(center, center, nsx, nsy, SECOND_COLOR, cached_points, sHandLen + 1, false, false);
    draw_and_erase_cached_line(center, center, nhx, nhy, HOUR_COLOR, cached_points + ((sHandLen + 1) * 2), hHandLen + 1, true, false);
    draw_and_erase_cached_line(center, center, nmx, nmy, MINUTE_COLOR, cached_points + ((sHandLen + 1 + hHandLen + 1) * 2), mHandLen + 1, true, true);
    gfx->endWrite();
    }

06_GFX_PCF85063_simpleTime

This example demonstrates using the PCF85063 RTC module to display the current time on the ST7789 display. It retrieves the time every second and updates the display only when the time changes.

Example 6 illustration

Code

06_GFX_PCF85063_simpleTime.ino
#include <Arduino.h>
#include "Arduino_GFX_Library.h"
#include "pin_config.h"
#include "SensorPCF85063.hpp"
#include <Wire.h>

#include "HWCDC.h"
HWCDC USBSerial;

SensorPCF85063 rtc;
uint32_t lastMillis;
char previousTimeString[20] = "";
Arduino_DataBus *bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCK, LCD_MOSI);

Arduino_GFX *gfx = new Arduino_ST7789(bus, LCD_RST /* RST */,
0 /* rotation */, true /* IPS */, LCD_WIDTH, LCD_HEIGHT, 0, 20, 0, 0);

int16_t getCenteredX(const char *text, uint8_t textSize) {
int16_t textWidth = strlen(text) * 3 * textSize; // 6 pixels per character in default size
return (LCD_WIDTH - textWidth) / 2;
}

void setup() {
USBSerial.begin(115200);
if (!rtc.begin(Wire, PCF85063_SLAVE_ADDRESS, IIC_SDA, IIC_SCL)) {
USBSerial.println("Failed to find PCF8563 - check your wiring!");
while (1) {
delay(1000);
}
}

uint16_t year = 2024;
uint8_t month = 9;
uint8_t day = 24;
uint8_t hour = 11;
uint8_t minute = 24;
uint8_t second = 30;

rtc.setDateTime(year, month, day, hour, minute, second);
gfx->begin();
gfx->fillScreen(WHITE);
pinMode(LCD_BL, OUTPUT);
digitalWrite(LCD_BL, HIGH);
}

void loop() {

if (millis() - lastMillis > 1000) {
lastMillis = millis();

RTC_DateTime datetime = rtc.getDateTime();

// Format the current time as a string
char timeString[20];
sprintf(timeString, "%04d-%02d-%02d %02d:%02d:%02d",
datetime.year, datetime.month, datetime.day,
datetime.hour, datetime.minute, datetime.second);

// Only update the time if it has changed
if (strcmp(timeString, previousTimeString) != 0) {
// Clear the previous time area by filling a small rectangle
gfx->fillRect(0, 150, LCD_WIDTH, 50, WHITE); // Clear the area for the time
gfx->setTextColor(BLACK);
gfx->setTextSize(3,3,0);

int16_t timeX = getCenteredX(timeString, 3);
gfx->setCursor(timeX, 150); // Adjust Y-coordinate as needed
gfx->println(timeString); // Display the new time

// Save the current time as the previous time
strcpy(previousTimeString, timeString);
}
}
}

Code Analysis

  • loop():
    • First, get the current time. If the current time is different from the previously displayed time, do the following:
    • Clear the area where the previous time was displayed by filling a rectangle, so that overlapping does not occur when updating the time.
    • Set the text color to black and the text size to 3.
    • Call getCenteredX to calculate the X coordinate for centering the current time string on the screen.
    • Set the cursor position and print the current time string, updating the time display.
    • Copy the current time string to previousTimeString for the next comparison.

07_LVGL_Measuring_voltage

This example demonstrates how to use the LVGL library and the Arduino GFX library to implement display text updates and voltage measurement. The code initializes the ST7789 display, creates a label at the center of the screen, periodically reads the actual voltage value from the voltage divider, and updates the label to show the latest voltage information.

Example 7 illustration

Code

07_LVGL_Measuring_voltage.ino
#include <lvgl.h>
#include "Arduino_GFX_Library.h"
#include "lv_conf.h"
#include "demos/lv_demos.h"
#include "pin_config.h"

/*Using LVGL with Arduino requires some extra steps:
*Be sure to read the docs here: https://docs.lvgl.io/master/get-started/platforms/arduino.html */

#define EXAMPLE_LVGL_TICK_PERIOD_MS 2

/* Change to your screen resolution */
static const uint16_t screenWidth = 240;
static const uint16_t screenHeight = 280;

static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[screenWidth * screenHeight / 10];

const int voltageDividerPin = 1; // GPIO1 pin
float vRef = 3.3; // Power supply voltage of ESP32-S3 (unit: volts)
float R1 = 200000.0; // Resistance value of the first resistor (unit: ohms)
float R2 = 100000.0; // Resistance value of the second resistor (unit: ohms)

lv_obj_t *label; // Global label object

Arduino_DataBus *bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCK, LCD_MOSI);

Arduino_GFX *gfx = new Arduino_ST7789(bus, LCD_RST /* RST */,
0 /* rotation */, true /* IPS */, LCD_WIDTH, LCD_HEIGHT, 0, 20, 0, 0);

#if LV_USE_LOG != 0
/* Serial debugging */
void my_print(const char *buf) {
Serial.printf(buf);
Serial.flush();
}
#endif

/* Display flushing */
void my_disp_flush(lv_disp_drv_t *disp, 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);

#if (LV_COLOR_16_SWAP != 0)
gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
#else
gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
#endif

lv_disp_flush_ready(disp);
}

void example_increase_lvgl_tick(void *arg) {
/* Tell LVGL how many milliseconds has elapsed */
lv_tick_inc(EXAMPLE_LVGL_TICK_PERIOD_MS);
}

static uint8_t count = 0;
void example_increase_reboot(void *arg) {
count++;
if (count == 30) {
esp_restart();
}
}

void setup() {
Serial.begin(115200); /* prepare for possible serial debug */
pinMode(voltageDividerPin, INPUT);

String LVGL_Arduino = "Hello Arduino! ";
LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();

Serial.println(LVGL_Arduino);
Serial.println("I am LVGL_Arduino");

lv_init();

#if LV_USE_LOG != 0
lv_log_register_print_cb(my_print); /* register print function for debugging */
#endif

gfx->begin();
pinMode(LCD_BL, OUTPUT);
digitalWrite(LCD_BL, HIGH);

lv_disp_draw_buf_init(&draw_buf, buf, NULL, screenWidth * screenHeight / 10);

/*Initialize the display*/
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
/*Change the following line to your display resolution*/
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);

const esp_timer_create_args_t lvgl_tick_timer_args = {
.callback = &example_increase_lvgl_tick,
.name = "lvgl_tick"
};

const esp_timer_create_args_t reboot_timer_args = {
.callback = &example_increase_reboot,
.name = "reboot"
};

esp_timer_handle_t lvgl_tick_timer = NULL;
esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer);
esp_timer_start_periodic(lvgl_tick_timer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000);

// esp_timer_handle_t reboot_timer = NULL;
// esp_timer_create(&reboot_timer_args, &reboot_timer);
// esp_timer_start_periodic(reboot_timer, 2000 * 1000);

/* Create label */
label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Initializing...");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

Serial.println("Setup done");
}

void loop() {
lv_timer_handler(); /* let the GUI do its work */
delay(5);

// Read ADC value
int adcValue = analogRead(voltageDividerPin);

// Convert to voltage
float voltage = (float)adcValue * (vRef / 4095.0);

// Apply the voltage divider formula to calculate the actual voltage
float actualVoltage = voltage * ((R1 + R2) / R2);

// Print the actual voltage
Serial.print("Actual Voltage: ");
Serial.print(actualVoltage);
Serial.println(" V");

// Update label content
String voltageStr = "Actual Voltage: " + String(actualVoltage) + " V";
lv_label_set_text(label, voltageStr.c_str());
}

Code Analysis

  • my_print(): Used for LVGL log output; if LVGL logging is enabled, this function prints log messages to the serial port.
  • my_disp_flush(): Responsible for flushing the LVGL drawing buffer contents to the display.
  • example_increase_lvgl_tick(): Timer callback function to inform LVGL of the passage of time.
  • example_increase_reboot(): Another timer callback function that counts and may trigger a system restart after a certain number of calls.

08_LVGL_PCF85063_simpleTime

This example demonstrates using the PCF85063 RTC module under LVGL to display the current time on the ST7789 display. It retrieves the time every second and updates the display only when the time changes.

Example 8 illustration

Code

08_LVGL_PCF85063_simpleTime.ino
#include <lvgl.h>
#include "Arduino_GFX_Library.h"
#include "pin_config.h"
#include "lv_conf.h"
#include <Wire.h>
#include <SPI.h>
#include <Arduino.h>
#include "SensorPCF85063.hpp"
#include "HWCDC.h"

HWCDC USBSerial;
#define EXAMPLE_LVGL_TICK_PERIOD_MS 2

static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[LCD_WIDTH * LCD_HEIGHT / 10];

lv_obj_t *label; // Global label object
SensorPCF85063 rtc;
uint32_t lastMillis;

Arduino_DataBus *bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCK, LCD_MOSI);

Arduino_GFX *gfx = new Arduino_ST7789(bus, LCD_RST /* RST */,
0 /* rotation */, true /* IPS */, LCD_WIDTH, LCD_HEIGHT, 0, 20, 0, 0);
#if LV_USE_LOG != 0
/* Serial debugging */
void my_print(const char *buf) {
USBSerial.printf(buf);
USBSerial.flush();
}
#endif

/* Display flushing */
void my_disp_flush(lv_disp_drv_t *disp, 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);

#if (LV_COLOR_16_SWAP != 0)
gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
#else
gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
#endif

lv_disp_flush_ready(disp);
}

void example_increase_lvgl_tick(void *arg) {
/* Tell LVGL how many milliseconds has elapsed */
lv_tick_inc(EXAMPLE_LVGL_TICK_PERIOD_MS);
}

static uint8_t count = 0;
void example_increase_reboot(void *arg) {
count++;
if (count == 30) {
esp_restart();
}
}

void setup() {
USBSerial.begin(115200); /* prepare for possible serial debug */
if (!rtc.begin(Wire, PCF85063_SLAVE_ADDRESS, IIC_SDA, IIC_SCL)) {
USBSerial.println("Failed to find PCF8563 - check your wiring!");
while (1) {
delay(1000);
}
}

uint16_t year = 2024;
uint8_t month = 9;
uint8_t day = 24;
uint8_t hour = 11;
uint8_t minute = 9;
uint8_t second = 41;

rtc.setDateTime(year, month, day, hour, minute, second);

gfx->begin();
pinMode(LCD_BL, OUTPUT);
digitalWrite(LCD_BL, HIGH);

String LVGL_Arduino = "Hello Arduino! ";
LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();

USBSerial.println(LVGL_Arduino);
USBSerial.println("I am LVGL_Arduino");

lv_init();

#if LV_USE_LOG != 0
lv_log_register_print_cb(my_print); /* register print function for debugging */
#endif

lv_disp_draw_buf_init(&draw_buf, buf, NULL, LCD_WIDTH * LCD_HEIGHT / 10);

/*Initialize the display*/
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
/*Change the following line to your display resolution*/
disp_drv.hor_res = LCD_WIDTH;
disp_drv.ver_res = LCD_HEIGHT;
disp_drv.flush_cb = my_disp_flush;
disp_drv.draw_buf = &draw_buf;
lv_disp_drv_register(&disp_drv);

/*Initialize the (dummy) input device driver*/
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
lv_indev_drv_register(&indev_drv);

// lv_obj_t *label = lv_label_create(lv_scr_act());
// lv_label_set_text(label, "Hello Ardino and LVGL!");
// lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

const esp_timer_create_args_t lvgl_tick_timer_args = {
.callback = &example_increase_lvgl_tick,
.name = "lvgl_tick"
};

const esp_timer_create_args_t reboot_timer_args = {
.callback = &example_increase_reboot,
.name = "reboot"
};

esp_timer_handle_t lvgl_tick_timer = NULL;
esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer);
esp_timer_start_periodic(lvgl_tick_timer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000);

label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Initializing...");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
}

void loop() {
lv_timer_handler(); /* let the GUI do its work */
delay(5);

if (millis() - lastMillis > 1000) {
lastMillis = millis();
RTC_DateTime datetime = rtc.getDateTime();
USBSerial.printf(" Year :");
USBSerial.print(datetime.year);
USBSerial.printf(" Month:");
USBSerial.print(datetime.month);
USBSerial.printf(" Day :");
USBSerial.print(datetime.day);
USBSerial.printf(" Hour:");
USBSerial.print(datetime.hour);
USBSerial.printf(" Minute:");
USBSerial.print(datetime.minute);
USBSerial.printf(" Sec :");
USBSerial.println(datetime.second);

char buf[32];
snprintf(buf, sizeof(buf), "%02d:%02d:%02d\n%02d-%02d-%04d",
datetime.hour, datetime.minute, datetime.second,
datetime.day, datetime.month, datetime.year);

// Update label with current time
lv_label_set_text(label, buf);
lv_obj_set_style_text_font(label, &lv_font_montserrat_40, LV_PART_MAIN);
}
delay(20);
}

Code Analysis

  • setup():

    • Initializes serial communication at 115200 baud for possible serial debugging;
    • Attempts to connect to the PCF85063 real-time clock chip; if it fails, enters an infinite loop;
    • Sets the initial RTC time to 2024-09-24 11:09:41;
    • Initializes the display and sets screen brightness;
    • Initializes LVGL and registers a log output function (if logging is enabled);
    • Configures the LVGL display driver and drawing buffer, and initializes a dummy input device driver;
    • Creates a timer to periodically trigger LVGL tick updates;
    • Creates a label and sets its initial text to "Initializing...".
  • loop():

    • Calls lv_timer_handler to let LVGL handle GUI tasks;
    • Every second, checks whether the time has been updated; if so, retrieves the current time from the RTC, outputs it via serial, formats it as a string, updates the label's text, and sets the label's font to lv_font_montserrat_40.

09_LVGL_Keys_Bee

This example demonstrates how to use the LVGL library in combination with Arduino and ESP32 to implement a button response system with a simple GUI interaction. The code allows the user to detect single-click, double-click, and long-press events via a button and displays corresponding output on the connected LCD screen.

Specific functionalities include: displaying the words "Single Click", "Double Click", and "Long Press" on the screen using trigger actions, and enabling the buzzer on a long press. The program refreshes the screen using LVGL's drawing functions and improves button recognition accuracy with a simple debouncing mechanism. Additionally, an auto‑restart mechanism is provided for device maintenance during long‑run operation.

Example 9 illustration

Code

09_LVGL_Keys_Bee.ino
#error "If the key fails, change USE_NEW_PIN_CONFIG to 1 to use the new pin configuration. Comment this line as you run the code"

#include <lvgl.h>
#include "Arduino_GFX_Library.h"
#include "lv_conf.h"
#include "demos/lv_demos.h"
#include "pin_config.h"
#include <Wire.h>
#include "HWCDC.h"

HWCDC USBSerial;
/*To use the built-in examples and demos of LVGL uncomment the includes below respectively.
*You also need to copy `lvgl/examples` to `lvgl/src/examples`. Similarly for the demos `lvgl/demos` to `lvgl/src/demos`.
Note that the `lv_examples` library is for LVGL v7 and you shouldn't install it for this version (since LVGL v8)
as the examples and demos are now part of the main LVGL library. */

#define EXAMPLE_LVGL_TICK_PERIOD_MS 2
#define USE_NEW_PIN_CONFIG 0

#if (USE_NEW_PIN_CONFIG)
// New pin configuration
const int inputPin = 40; // Define GPIO40 as input
const int outputPin = 41; // Define GPIO41 as output
const int beePin = 42; // Define GPIO42 for buzzer
#else
// Existing pin configuration
const int inputPin = 36; // Define GPIO36 as input
const int outputPin = 35; // Define GPIO35 as output
const int beePin = 33; // Define GPIO33 for buzzer
#endif

bool buttonState = false;
bool lastButtonState = false;
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 50;
unsigned long lastClickTime = 0;
unsigned long clickInterval = 500; // Double-click interval
unsigned long longPressDuration = 1000; // Long press duration

bool longPressDetected = false;
bool doubleClickDetected = false;
bool clickDetected = false;

/* Change to your screen resolution */
static const uint16_t screenWidth = 240;
static const uint16_t screenHeight = 280;

static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[screenWidth * screenHeight / 10];

lv_obj_t *label; // Global label object

Arduino_DataBus *bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCK, LCD_MOSI);

Arduino_GFX *gfx = new Arduino_ST7789(bus, LCD_RST /* RST */,
0 /* rotation */, true /* IPS */, LCD_WIDTH, LCD_HEIGHT, 0, 20, 0, 0);

#if LV_USE_LOG != 0
/* USBSerial debugging */
void my_print(const char *buf) {
USBSerial.printf(buf);
USBSerial.flush();
}
#endif

/* Display flushing */
void my_disp_flush(lv_disp_drv_t *disp, 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);

#if (LV_COLOR_16_SWAP != 0)
gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
#else
gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
#endif

lv_disp_flush_ready(disp);
}

void example_increase_lvgl_tick(void *arg) {
/* Tell LVGL how many milliseconds has elapsed */
lv_tick_inc(EXAMPLE_LVGL_TICK_PERIOD_MS);
}

static uint8_t count = 0;
void example_increase_reboot(void *arg) {
count++;
if (count == 30) {
esp_restart();
}
}

void setup() {
pinMode(inputPin, INPUT);
pinMode(outputPin, OUTPUT);
pinMode(beePin, OUTPUT);
digitalWrite(outputPin, HIGH); // Initialize output pin to HIGH

gfx->begin();
pinMode(LCD_BL, OUTPUT);
digitalWrite(LCD_BL, HIGH);

String LVGL_Arduino = "Hello Arduino! ";
LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();

USBSerial.println(LVGL_Arduino);
USBSerial.println("I am LVGL_Arduino");

lv_init();

#if LV_USE_LOG != 0
lv_log_register_print_cb(my_print); /* register print function for debugging */
#endif

lv_disp_draw_buf_init(&draw_buf, buf, NULL, screenWidth * screenHeight / 10);

/*Initialize the display*/
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
/*Change the following line to your display resolution*/
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);

const esp_timer_create_args_t lvgl_tick_timer_args = {
.callback = &example_increase_lvgl_tick,
.name = "lvgl_tick"
};

const esp_timer_create_args_t reboot_timer_args = {
.callback = &example_increase_reboot,
.name = "reboot"
};

esp_timer_handle_t lvgl_tick_timer = NULL;
esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer);
esp_timer_start_periodic(lvgl_tick_timer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000);

// esp_timer_handle_t reboot_timer = NULL;
// esp_timer_create(&reboot_timer_args, &reboot_timer);
// esp_timer_start_periodic(reboot_timer, 2000 * 1000);

label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Initializing...");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

USBSerial.println("Setup done");
}

void loop() {
lv_timer_handler(); /* let the GUI do its work */
delay(5);

int reading = digitalRead(inputPin);
// Debounce processing
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
if (reading != buttonState) {
buttonState = reading;

if (buttonState == LOW) {
// Operation when button is pressed
unsigned long now = millis();
if (now - lastClickTime < clickInterval && !longPressDetected) {
lv_label_set_text(label, "Double Click");
// Double click detected
printf("Double Click\n");
doubleClickDetected = true;
}else{
// Single click detected
if (!longPressDetected && !doubleClickDetected) {
lv_label_set_text(label, "Single Click");
printf("Single Click\n");
clickDetected = true;
}
}
lastClickTime = now;

delay(100); // Used to eliminate button noise
}else{
// Operation when button is released
if (longPressDetected) {
lv_label_set_text(label, "Long Press Released");

// Set GPIO35 output to low level after long pressing the button and releasing it
printf("Long Press Released\n");
noTone(beePin);
digitalWrite(outputPin, LOW); // Set GPIO35 output to low level
longPressDetected = false; // Reset long press detection flag
}
clickDetected = false;
doubleClickDetected = false;
}
}
}

// Check if the button is in long press state
if (buttonState == LOW && (millis() - lastDebounceTime >= longPressDuration)) {
lv_label_set_text(label, "Long Press");
// Long press button
printf("Long Press\n");
tone(beePin, 2000);
longPressDetected = true; // Set long press detection flag
clickDetected = false; // Reset single click detection flag
doubleClickDetected = false; // Reset double click detection flag
}
lastButtonState = reading;
}

Code Analysis

  • loop():
    • Calls lv_timer_handler to let LVGL handle GUI tasks;
    • Reads the state of the input pin and performs debouncing;
    • Based on the button state (pressed or released) and timing, determines whether a single‑click, double‑click, or long‑press event occurred, and updates the label text on the display accordingly;
    • For a long press, activates the buzzer; when released, stops the buzzer and sets a specific output pin to low.

10_LVGL_QMI8658_ui

This example demonstrates using LVGL for graphical display and communicating with the QMI8658 IMU to obtain accelerometer and gyroscope data.

Example 10 illustration

Code

10_LVGL_QMI8658_ui.ino
#include <lvgl.h>
#include "Arduino_GFX_Library.h"
#include "pin_config.h"
#include "lv_conf.h"
#include <Arduino.h>
#include <Wire.h>
#include "SensorQMI8658.hpp"
#include "HWCDC.h"

HWCDC USBSerial;

#define EXAMPLE_LVGL_TICK_PERIOD_MS 2

static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[LCD_WIDTH * LCD_HEIGHT / 10];

SensorQMI8658 qmi;

IMUdata acc;
IMUdata gyr;

lv_obj_t *label; // Global label object
lv_obj_t *chart; // Global chart object
lv_chart_series_t *acc_series_x; // Acceleration X series
lv_chart_series_t *acc_series_y; // Acceleration Y series
lv_chart_series_t *acc_series_z; // Acceleration Z series

Arduino_DataBus *bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCK, LCD_MOSI);

Arduino_GFX *gfx = new Arduino_ST7789(bus, LCD_RST /* RST */,
0 /* rotation */, true /* IPS */, LCD_WIDTH, LCD_HEIGHT, 0, 20, 0, 0);

#if LV_USE_LOG != 0
/* Serial debugging */
void my_print(const char *buf) {
USBSerial.printf(buf);
USBSerial.flush();
}
#endif

/* Display flushing */
void my_disp_flush(lv_disp_drv_t *disp, 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);

#if (LV_COLOR_16_SWAP != 0)
gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
#else
gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
#endif

lv_disp_flush_ready(disp);
}

void example_increase_lvgl_tick(void *arg) {
/* Tell LVGL how many milliseconds has elapsed */
lv_tick_inc(EXAMPLE_LVGL_TICK_PERIOD_MS);
}

static uint8_t count = 0;
void example_increase_reboot(void *arg) {
count++;
if (count == 30) {
esp_restart();
}
}

void setup() {
USBSerial.begin(115200); /* prepare for possible serial debug */

// pinMode(LCD_EN, OUTPUT);
// digitalWrite(LCD_EN, HIGH);

gfx->begin();
pinMode(LCD_BL, OUTPUT);
pinMode(38, OUTPUT);
digitalWrite(LCD_BL, HIGH);
digitalWrite(38, HIGH);
String LVGL_Arduino = "Hello Arduino! ";
LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();

USBSerial.println(LVGL_Arduino);
USBSerial.println("I am LVGL_Arduino");

lv_init();

#if LV_USE_LOG != 0
lv_log_register_print_cb(my_print); /* register print function for debugging */
#endif

lv_disp_draw_buf_init(&draw_buf, buf, NULL, LCD_WIDTH * LCD_HEIGHT / 10);

/*Initialize the display*/
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
/*Change the following line to your display resolution*/
disp_drv.hor_res = LCD_WIDTH;
disp_drv.ver_res = LCD_HEIGHT;
disp_drv.flush_cb = my_disp_flush;
disp_drv.draw_buf = &draw_buf;
lv_disp_drv_register(&disp_drv);

/*Initialize the (dummy) input device driver*/
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
lv_indev_drv_register(&indev_drv);

lv_obj_t *label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Hello Ardino and LVGL!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

const esp_timer_create_args_t lvgl_tick_timer_args = {
.callback = &example_increase_lvgl_tick,
.name = "lvgl_tick"
};

const esp_timer_create_args_t reboot_timer_args = {
.callback = &example_increase_reboot,
.name = "reboot"
};

esp_timer_handle_t lvgl_tick_timer = NULL;
esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer);
esp_timer_start_periodic(lvgl_tick_timer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000);

label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Initializing...");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

/* Create chart */
chart = lv_chart_create(lv_scr_act());
lv_obj_set_size(chart, 240, 280);
lv_obj_align(chart, LV_ALIGN_CENTER, 0, 0);
lv_chart_set_type(chart, LV_CHART_TYPE_LINE); /* Set the type to line */
lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, -3, 3); /* Set the range of y axis */
lv_chart_set_point_count(chart, 20); /* Set the number of data points */
acc_series_x = lv_chart_add_series(chart, lv_palette_main(LV_PALETTE_RED), LV_CHART_AXIS_PRIMARY_Y);
acc_series_y = lv_chart_add_series(chart, lv_palette_main(LV_PALETTE_GREEN), LV_CHART_AXIS_PRIMARY_Y);
acc_series_z = lv_chart_add_series(chart, lv_palette_main(LV_PALETTE_BLUE), LV_CHART_AXIS_PRIMARY_Y);

USBSerial.println("Setup done");

if (!qmi.begin(Wire, QMI8658_L_SLAVE_ADDRESS, IIC_SDA, IIC_SCL)) {

while (1) {
delay(1000);
}
}

/* Get chip id*/
USBSerial.println(qmi.getChipID());

qmi.configAccelerometer(
SensorQMI8658::ACC_RANGE_4G,
SensorQMI8658::ACC_ODR_1000Hz,
SensorQMI8658::LPF_MODE_0,
true);

qmi.configGyroscope(
SensorQMI8658::GYR_RANGE_64DPS,
SensorQMI8658::GYR_ODR_896_8Hz,
SensorQMI8658::LPF_MODE_3,
true);

qmi.enableGyroscope();
qmi.enableAccelerometer();

qmi.dumpCtrlRegister();

USBSerial.println("Read data now...");
}

void loop() {
lv_timer_handler(); /* let the GUI do its work */
delay(5);

if (qmi.getDataReady()) {
if (qmi.getAccelerometer(acc.x, acc.y, acc.z)) {
USBSerial.print("{ACCEL: ");
USBSerial.print(acc.x);
USBSerial.print(",");
USBSerial.print(acc.y);
USBSerial.print(",");
USBSerial.print(acc.z);
USBSerial.println("}");

// Update chart with new accelerometer data
lv_chart_set_next_value(chart, acc_series_x, acc.x);
lv_chart_set_next_value(chart, acc_series_y, acc.y);
lv_chart_set_next_value(chart, acc_series_z, acc.z);
}

if (qmi.getGyroscope(gyr.x, gyr.y, gyr.z)) {
USBSerial.print("{GYRO: ");
USBSerial.print(gyr.x);
USBSerial.print(",");
USBSerial.print(gyr.y);
USBSerial.print(",");
USBSerial.print(gyr.z);
USBSerial.println("}");
}
}
delay(20); // Increase the frequency of data polling
}

Code Analysis

  • my_disp_flush():

    • This function is the refresh funtion of LVGL display driver. It is responsible for refreshing the LVGL drawing buffer content to the display screen;
    • Depending on the color format setting, it calls the appropriate gfx object function to draw the bitmap to the specified area.
    • Finally, it notifies LVGL that the display refreshing is complete.
  • loop():

    • Calls lv_timer_handler to let LVGL handle GUI tasks;
    • Checks whether new data is ready from the qmi (QMI8658 sensor object). If so, attempts to retrieve accelerometer data and gyroscope data and outputs them via serial;
    • Simultaneously, updates the LVGL chart with the accelerometer data to display real‑time changes in acceleration on the three axes;
    • Uses delay(20) to increase the data polling frequency, ensuring timely retrieval of sensor data and display updates.

11_LVGL_Arduino

This example demonstrates the LVGL Widgets demo, achieving a frame rate of 20–30 FPS in dynamic states.

Example 11 illustration Example 11 illustration 2