Skip to content

Waveshare ESP32-S3-Touch-ePaper-1.54

Device Type:misc
Electrical Standard:global
Board:esp32
Difficulty:Disassembly required (3/5)

Product Description

ESP32-S3 (PICO-1-N8R8, 8MB flash + 8MB PSRAM) AIoT board with a 1.54” 200x200 e-paper display (monochrome on the -EN/base variant, 4-color BWRY on the ...1.54G variant), an ES8311 audio codec driving an onboard analog microphone and speaker, a SHTC3 temperature/humidity sensor, a PCF85063 RTC, a microSD slot, and lithium battery charging/monitoring. The Touch variant additionally has a capacitive touch panel.

This guide focuses on using the board as a fully local Home Assistant Voice Assistant satellite (wake word + STT + TTS) while keeping the SHTC3 sensor. The e-paper display is intentionally not covered in the config below — a full refresh takes ~15-20s, so it cannot show live listening/thinking/replying states and competes with the audio pipeline for SPI/CPU. See the Waveshare ESPHome climate-display writeup for a separate display-only firmware for this board family.

Important: the pins below are specific to this board. Every visually similar Waveshare ES8311 board (S3-Audio-Board, S3-RLCD-4.2, S3-Box, Korvo, …) uses different I2S pins for its codec. Copying a sibling board’s audio config compiles fine but leaves the mic/speaker producing no usable audio — this is the single biggest time sink with this board family.

Product Images

Waveshare ESP32-S3-Touch-ePaper-1.54 Front

Waveshare ESP32-S3-Touch-ePaper-1.54 Back Labels

GPIO Pinout

GPIOFunction
GPIO0BOOT button (active low)
GPIO4Battery voltage ADC (200K/200K divider)
GPIO6e-paper panel power (inverted: LOW = on)
GPIO8e-paper BUSY (HIGH = busy, LOW = idle; NOT inverted)
GPIO9e-paper RESET
GPIO10e-paper DC
GPIO11e-paper CS
GPIO12SPI CLK (e-paper)
GPIO13SPI MOSI (e-paper)
GPIO14I2S MCLK
GPIO15I2S BCLK
GPIO16I2S DIN (microphone, ES8311 ADC)
GPIO17Battery power latch (must be HIGH on battery)
GPIO18PWR button (active low, also deep-sleep wake)
GPIO38I2S LRCLK / WS
GPIO42Audio power rail (PA_EN, active LOW)
GPIO45I2S DOUT (speaker, ES8311 DAC), strapping pin
GPIO46Speaker amplifier enable (PA_CTRL, NS4150B), strapping pin
GPIO47I2C SDA (shared: ES8311 @0x18, SHTC3 @0x70, RTC @0x51)
GPIO48I2C SCL

Pin map verified against Waveshare’s own official ESP-IDF firmware (Example/ESP-IDF_5.5.1/07_Audio_Test/components/codec_board/board_cfg.txt and main/user_config.h in the waveshareteam/ESP32-S3-ePaper-1.54G repo, Apache-2.0) and against the board schematic PDF. GPIO45/46 are ESP32-S3 strapping pins; ESPHome will warn about this at compile time, it is expected and harmless on this board.

Basic Configuration

Includes Wi-Fi, the shared I2C bus, the ES8311 mic/speaker audio path, the SHTC3 sensor, and the boot button. No api:/ota: yet — add your own (or see the Voice Assistant config below, which includes them).

substitutions:
name: "waveshare-touch-epaper-154"
friendly_name: "Waveshare Touch ePaper 1.54"
esphome:
name: "${name}"
friendly_name: "${friendly_name}"
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
flash_size: 8MB
psram:
mode: octal
speed: 80MHz
logger:
hardware_uart: USB_SERIAL_JTAG # this board's only USB is native USB-JTAG
# Add your own wifi: block with your credentials.
# Shared by the ES8311 codec (0x18), SHTC3 sensor (0x70), and PCF85063 RTC (0x51).
i2c:
sda: GPIO47
scl: GPIO48
scan: true
id: bus_a
# Single shared I2S bus. The ES8311 does both DAC (speaker) and ADC
# (microphone) -- there is no separate ADC chip on this board, unlike the
# RLCD-4.2 / S3-Audio-Board which pair ES8311 with an ES7210.
i2s_audio:
- id: i2s_shared
i2s_lrclk_pin: GPIO38
i2s_bclk_pin: GPIO15
i2s_mclk_pin: GPIO14
audio_dac:
- platform: es8311
id: es8311_dac
address: 0x18
i2c_id: bus_a
bits_per_sample: 16bit
sample_rate: 16000
# DO NOT set use_microphone: true on this board. It switches the ES8311's
# ADC input from analog (this board's mic) to PDM digital (a mic this
# board does not have), giving total silence with no error anywhere.
# The analog mic path is already enabled by default.
use_microphone: false
mic_gain: 24dB # 30dB+ clipped into garbled STT in testing; 24dB was accurate.
speaker:
- platform: i2s_audio
id: i2s_audio_speaker
i2s_audio_id: i2s_shared
i2s_dout_pin: GPIO45
dac_type: external
audio_dac: es8311_dac
sample_rate: 16000
bits_per_sample: 16bit
microphone:
- platform: i2s_audio
id: va_mic
i2s_audio_id: i2s_shared
i2s_din_pin: GPIO16
adc_type: external
pdm: false
sample_rate: 16000
bits_per_sample: 16bit
# Audio power rail (GPIO42, active LOW) and the battery latch (GPIO17).
#
# These MUST be `power_supply:`, not `switch: platform: gpio`. ESPHome setup
# priorities (esphome/core/component.h):
# POWER = 1200 <- power_supply with enable_on_boot on an internal pin
# BUS = 1000 <- i2c
# IO = 900
# HARDWARE = 800 <- switch: platform: gpio
# A gpio switch is therefore set up AFTER i2c, so restore_mode: ALWAYS_ON does
# NOT get the rail up before the bus scan. On a warm/OTA reboot you never notice,
# because the rails are still powered from the previous run. On a true cold boot
# the unpowered ES8311 holds SCL low and the whole bus dies:
# "Recovery failed: SCL is held LOW during clock pulse cycle"
# "Found no devices" (plus an ~11s stall in i2c setup)
# power_supply with enable_on_boot: true reports setup_priority::POWER, the tier
# the core documents as "must be on before buses like i2c can work".
power_supply:
- id: audio_power
pin:
number: GPIO42
inverted: true # PA_EN P-FET: driving LOW turns the rail ON
enable_on_boot: true
keep_on_time: 0s
- id: vbat_power
pin: GPIO17 # battery power latch; harmless on USB power
enable_on_boot: true
keep_on_time: 0s
- platform: gpio
name: "Speaker Amp"
pin: GPIO46
restore_mode: RESTORE_DEFAULT_ON
sensor:
- platform: shtcx
address: 0x70
i2c_id: bus_a
update_interval: 60s
temperature:
name: "Temperature"
humidity:
name: "Humidity"
binary_sensor:
- platform: gpio
name: "Boot Button"
pin:
number: GPIO0
inverted: true
mode: INPUT_PULLUP

Key points, all reverse-engineered from ESPHome’s own source (esphome/components/es8311/es8311.cpp, esphome/components/i2s_audio/i2s_audio.h) after several rounds of plausible-looking-but-wrong config:

  • use_microphone: false on the es8311 audio_dac — do not set this true. The ES8311 component’s own docstring says it “configures the codec’s ADC to use PDM microphone input instead of analog,” which reads like “turn the mic on.” In the actual source:

    uint8_t reg14 = 0x1A; // analog MIC enabled + max PGA gain
    if (this->use_mic_) reg14 |= BIT(6); // switches ADC to PDM DIGITAL mic

    This board’s mic is analog, wired into the ES8311’s PGA — the analog path is already the default (0x1A). use_microphone: true points the ADC at a PDM digital mic this board doesn’t have: total silence, no error, no warning anywhere. Wake word never fires, STT gets pure silence, and raising mic gain changes nothing (because there is no signal to gain).

  • mic_gain: 24dB — 30dB and above clipped into garbled STT results in testing; 24dB gave accurate transcriptions.

  • The GPIO42/GPIO17 rails use power_supply:, NOT switch: platform: gpio. This is easy to get wrong, and the failure only shows up on a cold boot. ESPHome’s setup priorities (esphome/core/component.h) are:

    POWER = 1200 // "must be on before buses like i2c can work"
    BUS = 1000 // i2c
    IO = 900
    HARDWARE = 800 // switch: platform: gpio

    A gpio switch is set up at 800 — after i2c: at 1000 — so restore_mode: ALWAYS_ON does not guarantee the ES8311’s rail is up before the bus scan. You will not notice on warm or OTA reboots, because the rails are still powered from the previous run. On a true cold boot the unpowered codec holds SCL low and the entire bus fails:

    [E][i2c.idf]: Recovery failed: SCL is held LOW during clock pulse cycle
    [C][component]: Setup i2c took 11206ms
    [C][i2c.idf]: Found no devices

    (no ES8311, no SHTC3, no RTC — and that ~11 s stall can itself cause a failed boot and an OTA rollback). power_supply: with enable_on_boot: true reports setup_priority::POWER on an internal pin, which genuinely precedes i2c:. With it, a cold boot enumerates 0x18 (ES8311), 0x38 (touch controller), 0x51 (PCF85063 RTC) and 0x70 (SHTC3).

Voice Assistant Configuration

Extends the basic configuration above into a full local Voice Assistant satellite: on-device wake word, STT/TTS through Home Assistant’s Assist pipeline, and spoken responses through the onboard speaker. Requires a Home Assistant instance with an Assist pipeline configured — a local STT engine such as Wyoming Whisper works well here and keeps everything off-cloud.

substitutions:
name: "waveshare-touch-epaper-154"
friendly_name: "Waveshare Touch ePaper 1.54"
esphome:
name: "${name}"
friendly_name: "${friendly_name}"
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
flash_size: 8MB
psram:
mode: octal
speed: 80MHz
logger:
hardware_uart: USB_SERIAL_JTAG # this board's only USB is native USB-JTAG
# Add your own wifi: block with your credentials.
api:
ota:
- platform: esphome
# Shared by the ES8311 codec (0x18), SHTC3 sensor (0x70), and PCF85063 RTC (0x51).
i2c:
sda: GPIO47
scl: GPIO48
scan: true
id: bus_a
# Single shared I2S bus. The ES8311 does both DAC (speaker) and ADC
# (microphone) -- there is no separate ADC chip on this board, unlike the
# RLCD-4.2 / S3-Audio-Board which pair ES8311 with an ES7210.
i2s_audio:
- id: i2s_shared
i2s_lrclk_pin: GPIO38
i2s_bclk_pin: GPIO15
i2s_mclk_pin: GPIO14
audio_dac:
- platform: es8311
id: es8311_dac
address: 0x18
i2c_id: bus_a
bits_per_sample: 16bit
sample_rate: 16000
# DO NOT set use_microphone: true on this board. It switches the ES8311's
# ADC input from analog (this board's mic) to PDM digital (a mic this
# board does not have), giving total silence with no error anywhere.
# The analog mic path is already enabled by default.
use_microphone: false
mic_gain: 24dB # 30dB+ clipped into garbled STT in testing; 24dB was accurate.
speaker:
- platform: i2s_audio
id: i2s_audio_speaker
i2s_audio_id: i2s_shared
i2s_dout_pin: GPIO45
dac_type: external
audio_dac: es8311_dac
sample_rate: 16000
bits_per_sample: 16bit
microphone:
- platform: i2s_audio
id: va_mic
i2s_audio_id: i2s_shared
i2s_din_pin: GPIO16
adc_type: external
pdm: false
sample_rate: 16000
bits_per_sample: 16bit
# Audio power rail (GPIO42, active LOW) and the battery latch (GPIO17).
#
# These MUST be `power_supply:`, not `switch: platform: gpio`. ESPHome setup
# priorities (esphome/core/component.h):
# POWER = 1200 <- power_supply with enable_on_boot on an internal pin
# BUS = 1000 <- i2c
# IO = 900
# HARDWARE = 800 <- switch: platform: gpio
# A gpio switch is therefore set up AFTER i2c, so restore_mode: ALWAYS_ON does
# NOT get the rail up before the bus scan. On a warm/OTA reboot you never notice,
# because the rails are still powered from the previous run. On a true cold boot
# the unpowered ES8311 holds SCL low and the whole bus dies:
# "Recovery failed: SCL is held LOW during clock pulse cycle"
# "Found no devices" (plus an ~11s stall in i2c setup)
# power_supply with enable_on_boot: true reports setup_priority::POWER, the tier
# the core documents as "must be on before buses like i2c can work".
power_supply:
- id: audio_power
pin:
number: GPIO42
inverted: true # PA_EN P-FET: driving LOW turns the rail ON
enable_on_boot: true
keep_on_time: 0s
- id: vbat_power
pin: GPIO17 # battery power latch; harmless on USB power
enable_on_boot: true
keep_on_time: 0s
media_player:
- platform: speaker
id: spk_player
name: None
announcement_pipeline:
speaker: i2s_audio_speaker
format: FLAC
sample_rate: 16000
num_channels: 1
media_pipeline:
speaker: i2s_audio_speaker
format: FLAC
sample_rate: 16000
num_channels: 1
# micro_wake_word OWNS the microphone. Hand the shared I2S bus over with
# micro_wake_word.stop / .start ONLY. Do not add microphone.capture /
# microphone.stop_capture actions anywhere in this config: an explicit
# capture call creates a SECOND, independent claim on the mic that
# micro_wake_word.stop cannot release, which pins the bus open and makes
# every later playback attempt fail forever with "Parent bus is busy"
# (not the harmless one-off retry -- an infinite retry loop). This is
# easy to reproduce: a single announcement works, then the first full
# voice pipeline run adds the extra claim and all subsequent playback
# deadlocks.
on_play:
- micro_wake_word.stop:
on_announcement:
- micro_wake_word.stop:
on_idle:
- script.execute: restart_mww
micro_wake_word:
id: mww
microphone:
microphone: va_mic
channels: 0
gain_factor: 4 # default of 1 leaves the model too quiet to ever hit its cutoff
models:
- model: hey_jarvis
on_wake_word_detected:
- voice_assistant.start:
voice_assistant:
id: va
microphone: va_mic
media_player: spk_player
use_wake_word: false # micro_wake_word handles the wake word instead
noise_suppression_level: 2
auto_gain: 31dBFS
volume_multiplier: 2.0
on_client_connected:
- micro_wake_word.start:
on_client_disconnected:
- micro_wake_word.stop:
on_error:
- micro_wake_word.start:
# on_end fires BEFORE the TTS response actually plays -- it must NOT touch
# the mic/bus here. media_player's on_idle (above) is the sole place that
# reacquires the bus, once the speaker has actually finished.
script:
- id: restart_mww
then:
- wait_until:
not:
media_player.is_announcing:
- wait_until:
not:
speaker.is_playing:
id: i2s_audio_speaker
- delay: 100ms
- micro_wake_word.start:
sensor:
- platform: shtcx
address: 0x70
i2c_id: bus_a
update_interval: 60s
temperature:
name: "Temperature"
humidity:
name: "Humidity"
binary_sensor:
- platform: gpio
name: "Boot Button"
pin:
number: GPIO0
inverted: true
mode: INPUT_PULLUP

Key points:

  • gain_factor: 4 on micro_wake_word’s microphone source. The default of 1 leaves the wake-word model too quiet to ever cross its probability cutoff (0.97 by default) — the wake word never fires with the default gain even though the mic itself is working fine.

  • The mic/speaker handover is done with micro_wake_word.stop / .start ONLY, driven from media_player’s on_play / on_announcement / on_idle hooks — never from voice_assistant’s own hooks, and never with explicit microphone.capture / microphone.stop_capture actions. ESPHome’s i2s_audio bus guards itself with a single mutex (esphome/components/i2s_audio/i2s_audio.h):

    bool try_lock() { return this->lock_.try_lock(); }
    // speaker: if (!this->parent_->try_lock()) { ESP_LOGE(TAG, "Parent bus is busy"); }

    There is no full-duplex mic+speaker on stock ESPHome for a single ES8311/one I2S port — exactly one consumer holds the bus at a time. micro_wake_word captures continuously and already owns the microphone, so:

    • voice_assistant’s own on_tts_stream_start/on_tts_stream_end hooks require speaker: and are rejected at config-validation time when using media_player: instead (which you need for HA-initiated playback, not just pipeline TTS) — use media_player’s hooks instead.
    • voice_assistant’s on_end fires before the response actually plays, so it must not touch the mic/bus; only media_player’s on_idle (via script.restart_mww, which explicitly waits for media_player.is_announcing and speaker.is_playing to clear) should reacquire it.
    • Adding an explicit microphone.capture / microphone.stop_capture anywhere creates a second, independent claim on the mic that micro_wake_word.stop cannot release. The mic then keeps the bus mutex forever and the speaker retries “Parent bus is busy” in an infinite loop — not the harmless one-off retry you may see during a normal handover. This is easy to miss: a single announcement can work fine, and it only deadlocks after the first full voice-pipeline run.
  • Volume is not linear. media_player.volume_set maps onto the ES8311’s REG32 (0.5dB per step), where 191/255 = 0dB:

    • 0.45 → −38dB (inaudible)
    • 0.75 → 0dB (good starting point)
    • 1.00 → +32dB (loud and distorted on a small speaker)

Notes

  • The touch panel (on the Touch variant) is not yet covered in this guide.
  • The e-paper display is deliberately omitted from this voice-focused config; see the intro above.
  • Every board in this family (S3-Audio-Board, S3-RLCD-4.2, S3-Box, Korvo, this board, …) wires its ES8311 to different GPIOs. If you have a visually similar Waveshare board that isn’t this one, don’t reuse these pin numbers — check that board’s own schematic/example firmware first.