The result
Put your first words on an AMOLED. Keep them there.
The firmware displays “Hello, ESPtember!” in the center of a black screen.
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-01-hello-world.bin and open a terminal in the download directory.
This is a merged image containing the bootloader, partition table, and application.
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-01-hello-world.bin
Close any serial monitor using that port before flashing.
When flashing completes, the board restarts into this lesson.
Check the result
- “Hello, ESPtember!” appears in the center on a black background.
- The label remains visible and USB stays connected during a 15-minute run.
- No flicker, unexpected freeze, or power loss occurs.
A successful flash or ESP_OK response does not establish that the panel is drawing.
Recorded evidence · September 4, 2026: The original lesson records a 15-minute hardware soak with both board fixes applied and no flicker.
How it works
This is the UI and startup excerpt from the firmware source.
The two board initialization functions must run before display startup.
pmu_init();
panel_reset_release();
bsp_display_start();
// AMOLEDs have no backlight — this sends the panel its brightness
// command. Required: the one sent during init doesn't stick.
bsp_display_backlight_on();
bsp_display_lock(0);
// Not styling — physics: the dark theme's background is dark grey,
// which keeps every AMOLED pixel lit. True black (#000000) turns
// them off.
lv_obj_set_style_bg_color(lv_screen_active(), lv_color_black(), 0);
lv_obj_t *label = lv_label_create(lv_screen_active());
lv_label_set_text(label, "Hello, ESPtember!");
// Not styling — survival: at the default position (top-left, 0,0)
// the label hides under the panel's rounded corner entirely.
lv_obj_center(label);
bsp_display_unlock();
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-01-hello-world/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
| Symptom | Check / fix |
|---|
| Board freezes or disappears from USB after a few minutes | Keep pmu_init() before display startup. This project sets the AXP2101 USB input limit to 900 mA and charging to 300 mA. |
| Panel turns black while display calls return success | Keep panel_reset_release() before bsp_display_start(). V2 panel reset uses the TCA9554 expander. |
| Label is missing near the top-left | Center it so it clears the panel’s rounded corner. |
These settings belong to this V2 board and its power setup.
Take it with you
Board startup is part of the program.
The PMU and reset fixes keep this V2 panel alive; an API success code cannot prove that the screen is visible.
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-01-hello-world)
main/CMakeLists.txt
idf_component_register(SRCS "main.c")
main/idf_component.yml
dependencies:
waveshare/esp32_s3_touch_amoled_1_8: "^2.0.3"
espressif/esp_io_expander_tca9554: "*"
main/main.c
// ESPtember Day 01 — Hello World
// Print text on the Waveshare ESP32-S3-Touch-AMOLED-1.8's screen, the
// most naive way possible — plus the two pieces of board bring-up the
// BSP is missing (PMU power budget, V2 panel reset) without which the
// board dies minutes in. This is a V2 board: CO5300 panel driver,
// CST816-family touch, AMOLED over QSPI, AXP2101 PMU.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c_master.h"
#include "bsp/esp-bsp.h"
#include "lvgl.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, ®, 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));
}
// ------------------------------------------------------------------------
void app_main(void)
{
pmu_init();
panel_reset_release();
bsp_display_start();
// AMOLEDs have no backlight — this sends the panel its brightness
// command. Required: the one sent during init doesn't stick.
bsp_display_backlight_on();
bsp_display_lock(0);
// Not styling — physics: the dark theme's background is dark grey,
// which keeps every AMOLED pixel lit. True black (#000000) turns
// them off.
lv_obj_set_style_bg_color(lv_screen_active(), lv_color_black(), 0);
lv_obj_t *label = lv_label_create(lv_screen_active());
lv_label_set_text(label, "Hello, ESPtember!");
// Not styling — survival: at the default position (top-left, 0,0)
// the label hides under the panel's rounded corner entirely.
lv_obj_center(label);
bsp_display_unlock();
// app_main can simply return — the BSP's LVGL task keeps the
// screen alive without us.
}
sdkconfig.defaults
# ESPtember Day 01
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