Put text on the screen.
That’s the whole assignment.
Waveshare ships a BSP that powers the panel, starts LVGL, and hands you a canvas.
Hello world is eight calls:
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();
The board dies in minutes
The code above works.
Then, one to four minutes in, it doesn’t.
The screen goes dark — sometimes the whole board with it — and nothing in the log says why.
Two bugs.
Neither is in your eight calls.
The PMU starves the board
The AXP2101 limits USB input to 500 mA by default.
This board runs an ESP32-S3, lights an AMOLED, and charges a lithium battery on that budget — and when the charger ramps up, the PMU cuts power to everything.
It doesn’t look like a power failure.
The screen holds its last frame and the whole thing reads as a software hang.
It isn’t.
AMOLED memory keeps the image after death.
Two register writes fix it: raise input to 900 mA, cap charging at 300 mA.
pmu_init() in the source.
The panel reset floats
The wiki says SH8601 panel, FT3168 touch.
Ours boots co5300 and CST816S.
Waveshare revised the hardware and left the docs behind — current units are V2.
On V2, panel reset hides behind a TCA9554 I/O expander the BSP never drives.
A floating reset usually comes up, the demo runs, and everything looks fine.
Then the panel drops dark at random — while every esp_lcd call keeps returning ESP_OK.
Pulse reset the way the factory firmware does, before display init.
panel_reset_release() in the source; waveshareteam issue #12.
With both fixes: a 15-minute soak, no flicker.
Flash it
Download day-01-hello-world.bin.
It’s the bootloader, partition table, and app merged into one image — one command, no offsets.
Find your port: ls /dev/cu.usbmodem* on macOS, ls /dev/ttyACM* on Linux.
uvx esptool --chip esp32s3 --port /dev/cu.usbmodem1101 \
write-flash 0x0 day-01-hello-world.bin
The screen says hello.
Building from source instead?
ESP-IDF v5.5+, then idf.py -p PORT flash monitor from firmware/.
Dependencies fetch on first build.
What we learned
printf is one line because someone built the plumbing before you arrived.
A screen has no plumbing.
Eight calls is what naive costs on a display.
Defaults are decisions someone else made, for a board they never met.
500 mA starved this one.
Docs describe the board they remember.
The boot log names the board you have.
Trust the log.
Hardware fails politely.
Every call returned ESP_OK while the panel sat in reset.
Success codes measure the conversation, not the picture.
Firmware source
firmware/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esptember-day-01-hello-world)
firmware/main/CMakeLists.txt
idf_component_register(SRCS "main.c")
firmware/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.
}
firmware/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