day 01 Hello World

toolchain: ESP-IDF v5.5 + Waveshare BSP (LVGL) · download firmware · source on GitHub

Flash from your browser (experimental)

This flashes over Web Serial — Chromium-based desktop browsers only. It only works with the Waveshare ESP32-S3-Touch-AMOLED-1.8; other boards may misbehave or be damaged. Flashing firmware is inherently risky and overwrites whatever is on the device. Proceed at your own risk — I take no responsibility for damage to your device.

Hello world, on the screen where it belongs: Hello, ESPtember! on the Waveshare ESP32-S3-Touch-AMOLED-1.8’s display — the most naive way possible. One label, default everything, then we’re done. (Making it big and pretty is a later lesson.)

Every remaining line is load-bearing. bsp_display_start() hides the real work (AXP2101 power, SH8601 panel init over QSPI, LVGL and its render task). The lock/unlock pair exists because that render task is already running — LVGL isn’t thread-safe. And bsp_display_backlight_on() looks removable (init already sets brightness to 100%) but isn’t: the brightness command sent during init doesn’t stick, so without this line the panel stays dark. We checked.

The display is an SH8601 AMOLED driven over QSPI, powered through an AXP2101 PMU — none of which you have to touch, because Waveshare ships a board support package that brings the panel up and hands you LVGL:

void app_main(void)
{
    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);
    lv_obj_t *label = lv_label_create(lv_screen_active());
    lv_label_set_text(label, "Hello, ESPtember!");
    lv_obj_center(label);
    bsp_display_unlock();

    // app_main can simply return — the BSP's LVGL task keeps the
    // screen alive without us.
}

The whole flashable image is ~570KB — roughly 220KB of bootloader + hello world, and the rest is the price of the display stack (panel driver, PMU, LVGL).

Flash it (prebuilt binary)

You need esptool and the board connected over USB-C.

  1. Download day-01-hello-world.bin
  2. Find your serial port (macOS: ls /dev/cu.usbmodem*, Linux: ls /dev/ttyACM*)
  3. Flash:
# with uv (no install)
uvx esptool --chip esp32s3 --port /dev/cu.usbmodem1101 \
  write-flash 0x0 day-01-hello-world.bin

# or with pip-installed esptool
esptool --chip esp32s3 --port /dev/cu.usbmodem1101 \
  write-flash 0x0 day-01-hello-world.bin
  1. The screen says hello.

Build from source

Requires ESP-IDF v5.5+. The BSP dependency is fetched automatically from the component registry on first build.

cd firmware
idf.py set-target esp32s3
idf.py -p /dev/cu.usbmodem1101 flash monitor

(exit the monitor with ctrl-])

What’s in the image

offsetfilewhat
0x0bootloader.binsecond-stage bootloader
0x8000partition-table.binwhere the app lives in flash
0x10000apphello world + display stack (BSP + LVGL)

The downloadable .bin is all three merged into one image flashed at offset 0x0 — that’s why the flash command is a single line.

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. The board support package brings up
// the display (SH8601 AMOLED over QSPI, AXP2101 PMU) and hands us
// LVGL; we put one label on it and we're done.
#include "bsp/esp-bsp.h"
#include "lvgl.h"

void app_main(void)
{
    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);
    lv_obj_t *label = lv_label_create(lv_screen_active());
    lv_label_set_text(label, "Hello, ESPtember!");
    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