September 03 / 2026 · Day 03

A Static Graphic

Turn a real photograph into pixels the board can display.

ESP-IDF v5.5 + Waveshare BSP (LVGL 9.5)Build verified
On this page
Illustration of the Waveshare Touch AMOLED 1.8 development kit

Used in this project

Waveshare Touch AMOLED 1.8

ESP32-S3-Touch-AMOLED-1.8 V2

1.8″ AMOLED · 368 × 448 · ESP32-S3

Run this project

Firmware for ESP32-S3-Touch-AMOLED-1.8 V2

Download firmware ↓
Or flash from your browser

Connect the ESP32-S3-Touch-AMOLED-1.8 V2 with a USB data cable. Use a desktop Chromium browser with Web Serial support. Installation replaces the firmware on the connected board.

This browser does not support Web Serial. Use the download and terminal instructions below.

The result

Turn a real photograph into pixels the board can display. The firmware displays Michael Chan’s saved GitHub portrait at 240 × 240. Each day is a standalone firmware image; you can start here without flashing earlier days.

What you need

  • Board: Waveshare ESP32-S3-Touch-AMOLED-1.8 V2: ESP32-S3, CO5300 panel, CST816-family touch, 16 MB flash, 8 MB PSRAM.
  • Connection: a USB data cable and a computer with access to the serial port.
  • For the download: uv supplies the uvx command below. ESP-IDF is not needed.
  • For source builds: ESP-IDF v5.5 with its environment activated. Dependencies are declared in the firmware project.

The display is 368 × 448 pixels. Flashing replaces the firmware currently on the board.

Run it

Download day-03-static-graphic.bin and open a terminal in the download directory. This is a merged image containing the bootloader, partition table, and application with its image assets.

Find your serial port:

# macOS
ls /dev/cu.usbmodem*
# Linux
ls /dev/ttyACM*

On Windows, use the board’s COM port from Device Manager. Replace PORT with your port, then flash the image at 0x0:

uvx esptool --chip esp32s3 --port PORT \
  write-flash 0x0 day-03-static-graphic.bin

Close any serial monitor using that port before flashing. When flashing completes, the board restarts into this lesson.

Check the result

  • The grayscale GitHub portrait appears upright, centered, and surrounded by black.
  • Compare it with the saved original; check that the crop and tones look correct.
  • Leave the image displayed to check for flicker or loss of power.
  • In the serial monitor, expect READY day03 followed by periodic day03 alive messages.

Heartbeats confirm that the firmware is running. Inspect the screen to confirm the image.

Recorded evidence · September 4, 2026: The portrait firmware builds successfully. Its pixels match the first carousel image; this portrait revision has not been separately flashed.

How it works

This is the UI excerpt from the firmware source. The full program first initializes power, releases the V2 panel reset, starts the display, and sets brightness.

bsp_display_lock(0);
lv_obj_set_style_bg_color(lv_screen_active(), lv_color_black(), 0);
lv_obj_t *image = lv_image_create(lv_screen_active());
lv_image_set_src(image, &esptember_graphic);
lv_obj_center(image);
bsp_display_unlock();

The saved portraits and their source attribution live in the repository. The converted assets are included; building does not need to fetch a current avatar from the internet. With Python 3 and FFmpeg installed, regenerate the media lessons from the repository root:

python3 scripts/make-media.py

This regenerates all media assets, including later lessons. Commit the source image and conversion changes together if you replace a portrait.

The descriptor uses RGB565, width 240, height 240, stride 480, and data size 115,200 bytes. The component embeds avatar.rgb565 with EMBED_FILES.

Build and change it

Clone the repository once:

git clone https://github.com/chantastic/esptember.git
cd esptember

With the ESP-IDF environment activated, run from the repository root:

cd days/day-03-static-graphic/firmware
idf.py build
idf.py size
idf.py -p PORT flash monitor

Replace PORT with the board’s serial port. Exit the monitor with Ctrl+]. To create the single downloadable image, run idf.py merge-bin in the same firmware directory. The output is build/merged-binary.bin.

Keep pmu_init() and panel_reset_release() before display startup when changing the UI.

If it goes wrong

SymptomCheck / fix
Image has wrong colors or geometryMatch little-endian RGB565 data to LV_COLOR_FORMAT_RGB565, 240 × 240 dimensions, and 480-byte stride.
Embedded asset cannot be foundKeep EMBED_FILES "avatar.rgb565" in the main component’s CMake configuration.
Display goes dark after startupPreserve the board’s PMU and panel-reset initialization.

Take it with you

A JPEG’s file size is not its display budget. This portrait becomes 115,200 bytes of RGB565 pixels before it reaches the board.

Read the build story for the investigation and lessons behind this version.

The implementation

Firmware source

Read the files here or open the project on GitHub ↗.

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esptember-day-03-static-graphic)
main/CMakeLists.txt
idf_component_register(SRCS "main.c" EMBED_FILES "avatar.rgb565")
main/idf_component.yml
dependencies:
  waveshare/esp32_s3_touch_amoled_1_8: "==2.0.3"
  espressif/esp_io_expander_tca9554: "*"
  lvgl/lvgl: "==9.5.0"
main/main.c
// ESPtember Day 03 — A Static Graphic
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c_master.h"
#include "bsp/esp-bsp.h"
#include "lvgl.h"
#include "esp_log.h"
#include "esp_heap_caps.h"
#include "esp_timer.h"

// --- AXP2101 power management ------------------------------------------
// The BSP doesn't configure the board's AXP2101 PMU at all. Its default
// VBUS input limit is 500 mA — once battery charging ramps up, the
// ESP32-S3 + AMOLED + charger exceed that and the PMU cuts system power
// (screen freezes/blanks, USB drops, ~1–4 min in). Raise the input
// limit and cap the charge current so the budget always fits.

#define AXP2101_ADDR 0x34

static i2c_master_dev_handle_t pmu;

static uint8_t pmu_read(uint8_t reg)
{
    uint8_t val = 0;
    i2c_master_transmit_receive(pmu, &reg, 1, &val, 1, 100);
    return val;
}

static void pmu_write(uint8_t reg, uint8_t val)
{
    uint8_t buf[2] = {reg, val};
    i2c_master_transmit(pmu, buf, 2, 100);
}

static void pmu_init(void)
{
    bsp_i2c_init();
    i2c_device_config_t cfg = {
        .device_address = AXP2101_ADDR,
        .scl_speed_hz = 100000,
    };
    ESP_ERROR_CHECK(
        i2c_master_bus_add_device(bsp_i2c_get_handle(), &cfg, &pmu));
    // 0x16 bits[2:0]: VBUS input current limit — 2 = 900 mA
    pmu_write(0x16, (pmu_read(0x16) & 0xF8) | 0x02);
    // 0x62 bits[4:0]: battery charge current — 9 = 300 mA
    pmu_write(0x62, (pmu_read(0x62) & 0xE0) | 0x09);
}
// --- V2 panel reset --------------------------------------------------
// On V2 boards (CO5300 panel, CST816-family touch) the panel's reset
// line sits behind a TCA9554 I/O expander that neither the BSP nor the
// examples ever drive. Left floating, the panel may come up — and then
// randomly drop dark minutes later while every esp_lcd call still
// returns ESP_OK. Pulse reset like the factory firmware does, then
// hold it high. Must run BEFORE bsp_display_start().
// https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-1.8/issues/12

#include "esp_io_expander_tca9554.h"

static void panel_reset_release(void)
{
    esp_io_expander_handle_t expander = NULL;
    ESP_ERROR_CHECK(esp_io_expander_new_i2c_tca9554(
        bsp_i2c_get_handle(), ESP_IO_EXPANDER_I2C_TCA9554_ADDRESS_000,
        &expander));
    const uint32_t reset_pins =
        IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1 | IO_EXPANDER_PIN_NUM_2;
    ESP_ERROR_CHECK(
        esp_io_expander_set_dir(expander, reset_pins, IO_EXPANDER_OUTPUT));
    ESP_ERROR_CHECK(esp_io_expander_set_level(expander, reset_pins, 1));
    vTaskDelay(pdMS_TO_TICKS(100));
    ESP_ERROR_CHECK(esp_io_expander_set_level(expander, reset_pins, 0));
    vTaskDelay(pdMS_TO_TICKS(300));
    ESP_ERROR_CHECK(esp_io_expander_set_level(expander, reset_pins, 1));
}

static void board_start(void)
{
    pmu_init();
    panel_reset_release();
    if (!bsp_display_start()) abort();
    ESP_ERROR_CHECK(bsp_display_backlight_on());
}

static void heartbeat(const char *lesson)
{
    while (1) {
        ESP_LOGI("media", "%s alive: uptime=%llds heap=%u psram=%u", lesson,
                 esp_timer_get_time() / 1000000,
                 (unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
                 (unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
        vTaskDelay(pdMS_TO_TICKS(10000));
    }
}

extern const uint8_t avatar_start[] asm("_binary_avatar_rgb565_start");
static const lv_image_dsc_t esptember_graphic = {
    .header = { .magic = LV_IMAGE_HEADER_MAGIC, .cf = LV_COLOR_FORMAT_RGB565,
                .w = 240, .h = 240, .stride = 480 },
    .data_size = 240 * 240 * 2,
    .data = avatar_start,
};

void app_main(void)
{
    board_start();
    bsp_display_lock(0);
    lv_obj_set_style_bg_color(lv_screen_active(), lv_color_black(), 0);
    lv_obj_t *image = lv_image_create(lv_screen_active());
    lv_image_set_src(image, &esptember_graphic);
    lv_obj_center(image);
    bsp_display_unlock();
    ESP_LOGI("media", "READY day03: chantastic GitHub portrait; 240x240 RGB565; 115200 bytes");
    heartbeat("day03");
}
partitions.csv
# Name, Type, SubType, Offset, Size, Flags
nvs,data,nvs,0x9000,0x6000,
phy_init,data,phy,0xf000,0x1000,
factory,app,factory,0x10000,0x300000,
sdkconfig.defaults
# ESPtember Day 03
CONFIG_IDF_TARGET="esp32s3"

# Board: Waveshare ESP32-S3-Touch-AMOLED-1.8 (16MB quad flash, 8MB octal PSRAM)
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=y

# Console over the board's USB-C port (USB-Serial/JTAG peripheral)
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y

# Optimize for size
CONFIG_COMPILER_OPTIMIZATION_SIZE=y


# LVGL dark theme: white text, black background — zero styling code,
# and on an AMOLED, black pixels are simply off.
CONFIG_LV_THEME_DEFAULT_DARK=y

CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"