Audit of the whole repo (globals, scripts, substitutions, ids, media files all verified used - no orphans). Applied the real findings: Removed / dead: - The set_led_color API action - control_leds always repaints with hardcoded colours, so anything it wrote was clobbered before display. The colour globals it fed are now restore_value: no (transient pass-throughs). - The debug: component (5s heap/loop logging, no entities consumed it). Fixed: - led_set_effect "solid" branch now clears the running effect (effect: None), so waking up while a timer pulses shows a steady violet instead of a pulsing one. - flash_ring (mic mute) returns to control_leds instead of going dark, so muting during a timer/VA no longer blanks the ring. - Dropped the inert restore_mode on the lambda-driven mic_mute_switch (documented why the on_client_connected logic owns the boot state instead). Docs: - README: status is now "working on hardware"; drop the removed "stop" word, set_led_color and the exposed daily-alarm/diag entities; add the boot chime. - CHANGELOG 0.2.0 expanded (stop removal, hidden alarm, boot chime, pulse fix, encryption drop, dead-switch removal); stale 0.1.0 "untested" note removed. - HARDWARE.md / SKILL.md: drop the stale `channel: left` mic wording, note the no-AEC barge-in limitation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
13 KiB
Waveshare ESP32-S3-AUDIO-Board: hardware reference
Pinout and gotchas for the ESP32-S3-AUDIO-Board, cross-checked against the sources below. Every value is attributed to its source. Several sources contradict each other, including Waveshare's own documentation against its own demo code; those conflicts are documented rather than resolved by assumption.
| Tier | Source |
|---|---|
| A | Schematic PDF v1.1, ground truth for wiring. Text-extracted, so net/pin association is occasionally ambiguous. |
| A | Official demo ZIP, Arduino + ESP-IDF. Ground truth for what firmware actually drives, but it contains proven stale copy-paste from other boards. |
| B | Waveshare wiki, pinout tables. (Returns HTTP 403 to some fetchers; a browser User-Agent gets through.) |
| B | An empirically-working community ESPHome config for this board, used as behavioural confirmation. |
Board identity
| Item | Value |
|---|---|
| MCU | ESP32-S3R8 (bare chip, not a module), dual-core LX7 @ 240 MHz |
| Flash | 16 MB, W25Q128JVSI |
| PSRAM | 8 MB, octal, in-package (the R8 suffix). Runs at 80 MHz. |
| Radio | 2.4 GHz Wi-Fi b/g/n, BT 5 LE. Ceramic antenna + IPEX (resistor reflow) |
| Power | MP1605GTF-Z buck to 3.314 V, max 2 A |
| Audio | ES8311 codec (DAC) + NS4150B mono Class-D amp; ES7210 4-ch ADC (mics) |
ESPHome target: board: esp32-s3-devkitc-1, variant: esp32s3, flash_size: 16MB,
framework esp-idf.
Audio: one shared I2S bus
Confirmed by three independent sources (wiki tables, the demo's bsp_board.h,
and the schematic nets). This is the most solid part of this document.
| Signal | GPIO | Direction |
|---|---|---|
| I2S MCLK | GPIO12 | ESP32 to both codecs |
| I2S BCLK / SCLK | GPIO13 | shared |
| I2S LRCK / WS | GPIO14 | shared |
| I2S DIN (ES7210 to ESP) | GPIO15 | in |
| I2S DOUT (ESP to ES8311) | GPIO16 | out |
The DAC and the ADC share one clock pair. The wiki's ES8311 and ES7210
tables list the same MCLK/SCLK/LRCK; the schematic lands one net set on both
chips; and the demo opens a single full-duplex I2S peripheral
(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle)).
That single shared clock pair is what shapes the whole audio setup, see Shared I2S clocks below.
I2C: one bus, GPIO10/11
| Item | Value |
|---|---|
| SDA | GPIO11 |
| SCL | GPIO10 |
| Speed | 100 kHz confirmed working |
The schematic uses different net names (ESP32_, EXIO_, RTC_, TP_,
TWI_ for the camera SCCB) but they all resolve to GPIO10/11. Schematic note:
"can only be used for I2C, cannot be used for other functions."
⚠️ The HA community thread on this board states "SDA=10, SCL=11", which is swapped versus every official source.
| Device | Address |
|---|---|
| ES8311 codec / DAC | 0x18 |
| ES7210 ADC / mics | 0x40 |
| TCA9555PWR I/O expander | 0x20 |
| PCF85063ATL RTC | 0x51 |
TCA9555 I/O expander (0x20)
There is no reset pin. The schematic's pin 1 is INT#; the part simply has
none.
| EXIO | Function | Used here |
|---|---|---|
| 0 | LCD_RST | - |
| 1 | TP_RST (touch reset) | - |
| 2 | TP_INT | - |
| 3 | SD_D3 / SD CS | - |
| 4 | unknown, no driver references it | - |
| 5 | CAM_PWDN (active low) | - |
| 6 | Camera_SEL (clock mux) | do not drive |
| 7 | USB/camera mux | do not drive |
| 8 | PA_EN, amplifier enable, ACTIVE HIGH | ✅ amp_enable |
| 9 | Key1, active low | ✅ volume down |
| 10 | Key2, active low | ✅ play/pause |
| 11 | Key3, active low | ✅ volume up |
| 12-15 | expansion header P1 | - |
Keys have 10 kΩ hardware pull-ups to 3V3, so they are active low and need no internal pull.
"The keys are on 12, not 9/10/11": settled, they are on 9/10/11
A working community config carries number: 9 # not '12' as schematics say!. The map
9/10/11 (keys) + 8 (PA_EN) is right, confirmed twice over:
- Waveshare's own driver code reads exactly those:
Button_Driver.cppcallsRead_EXIO(TCA9555_EXIO9/10/11)with#define BUTTON_ACTIVE_LEVEL 0, andAudio_ES8311.cpphasAudio_PA_EN()settingTCA9555_EXIO8true. - A working community config arrived at the same map empirically, independently.
Whether the schematic really implies 12 could not be confirmed (PDF text
extraction loses the geometry, and Key4/Key5 nets also appear near
Extend_IO12..15). It doesn't matter: code and practice agree.
⚠️ EXIO6 vs EXIO7: genuine unresolved conflict
The schematic carries explicit warnings naming EXIO7: driving it LOW
disables USB on GPIO19/20 and forces manual download mode before every flash;
driving it HIGH disables UART on GPIO43/44; EXIO7 defaults HIGH. But the
wiki and Camera_Driver.cpp attribute the identical function to EXIO6.
Off-by-one, unresolved. For a voice build: never drive either. Leaving both alone keeps USB flashing alive.
RGB LEDs
| Item | Value |
|---|---|
| GPIO | GPIO38, driven directly over RMT, not via the expander |
| Count | 7 (WS2812B-0807, U9-U14 + U19) |
| RGB order | RGB |
On RGB vs GRB: two sources say RGB (the official demo's
LED_STRIP_COLOR_COMPONENT_FMT_RGB, and a working community config). But WS2812B is conventionally GRB, and Waveshare's own line contradicts its own trailing comment (..._FMT_RGB, // The color order of the strip: GRB). Verify visually: set pure red and check it isn't green.
Microphones
Two physical mics. Per the schematic:
| ES7210 ch | Wired to |
|---|---|
| MIC1, MIC2 | the two real microphones |
| MIC3 | AEC loopback, the ES8311 speaker output, attenuated |
| MIC4 | nets exist, no source found, likely unused |
Corroboration: MICBIAS12 biases only channels 1 and 2, i.e. only those two
have actual capsules.
Unresolved: the demo declares its AFE input format as
"RMNM"(slot0 = reference, slot1 = mic, slot2 = null, slot3 = mic), which does not reconcile with a naive MIC1 to slot0 mapping. Possibly ES7210 slot packing isn't 1:1, possibly copy-paste from a Korvo BSP. Verify before relying on it.
Hardware AEC is not usable from stock ESPHome on this board. The demo packs
4x16-bit channels into 2x32-bit I2S slots and unpacks them in software; ESPHome
does not. This firmware takes a single mic channel and relies on ESPHome's
software noise_suppression_level / auto_gain instead. The practical fallout:
without AEC the mic hears the device's own speaker loudly, so barge-in features
(a "stop" wake word during a reply) don't work here.
Not used by this firmware
Kept for reference. This build is speaker + mics + ring + buttons only.
- BOOT button: GPIO0, active low. RESET: hardware
CHIP_PU, not readable. - SD (1-bit SDMMC): CLK GPIO40, CMD GPIO42, D0 GPIO41, CS EXIO3. D1/D2 = NC.
- LCD (18-pin FPC): CS GPIO3, SCK GPIO4, BL GPIO5, MOSI GPIO9, DC GPIO7,
MISO GPIO8, SDA3 GPIO6; RST EXIO0, TP_RST EXIO1, TP_INT EXIO2.
⚠️ Use the wiki table for the LCD, not the IDF demo's
bsp_board.h, which contradicts it (proven stale, see the battery note). - Camera (DVP, 24-pin FPC): D0 GPIO2, D1 GPIO17, D2 GPIO18, D3 GPIO39, D4 GPIO45, D5 GPIO46, D6 GPIO47, D7 GPIO48, VSYNC GPIO21, HREF GPIO1, PCLK/XCLK muxed GPIO44/43 or GPIO19/20, PWDN EXIO5.
- USB: D- GPIO19, D+ GPIO20. UART0: TX GPIO43, RX GPIO44.
- RTC PCF85063ATL @ 0x51 + 32.768 kHz crystal + backup battery header.
The
RTC_INTnet exists but where it terminates is unconfirmed. anrtc_int: 5seen in a community config collides with CAM_PWDN and is unused there.
⚠️ Battery ADC: conflicting sources, don't trust GPIO8
The schematic says GPIO1 (behind 0 Ω jumpers R53/R78, sharing with CAM_HREF);
the Arduino demo says BAT_ADC_PIN 8. The schematic is almost certainly right:
GPIO8 on this board is LCD_SDA1/MISO, GPIO8 is the battery ADC on other
Waveshare S3 boards (classic copy-paste), and the same demo's LCD pins are
provably wrong too.
Either way, the schematic notes battery measurement is not enabled by default. It needs a 0 Ω resistor soldered, and doing so makes the camera unusable. Divider ratio is 3.0 (R2 200 k / R11 100 k). Treat battery monitoring as unavailable.
Gotchas
Shared I2S clocks
ES8311 and ES7210 share BCLK/LRCK, so only one device may drive them. On top of
that, ESPHome cannot run a single i2s_audio bus full-duplex: a microphone
and a speaker on the same bus each call i2s_new_channel on the port, and the
second fails at runtime with Parent bus is busy (the speaker then crackles).
The layout this firmware uses, all on stock ESPHome components:
- Two
i2s_audiobuses (two I2S ports) over the shared GPIO13/14 (withallow_other_uses). The mic bus is the I2S master, the speaker bus is a slave reading its clock. - The mic is always capturing for the wake word, so as master it drives BCLK/LRCK/MCLK continuously - exactly what a slave speaker and the ES8311 need. It also gives the mic a correct-rate stream.
- The mic is pinned to 16-bit: as master it sets the frame slot width, and the i2s_audio default is 32-bit, which against the 16-bit DAC produces noise.
Making the ES8311 the master instead (a force_master-style patch, setting
the codec's MSC bit) also works, but it feeds the ESP mic a wrong-rate stream
that kills the wake word. The ESP-mastered two-bus layout above needs no patch.
See base/core.yaml for the annotated config.
⚠️ Cold-boot failure of the ES7210 / LEDs: no verified fix
There is a HA community thread reporting that the mic and LEDs don't come up on a cold boot, only after a reset. It is a single post with zero replies, a question rather than an answer, so no root cause or fix has been published for this board.
What its register dumps do establish:
| Device | Reg | Warm (works) | Cold (fails) |
|---|---|---|---|
| TCA9555 | 0x06 | 0x9C |
0xFF |
| ES7210 | 0x00 | 0x41 |
0x32 |
| ES7210 | 0x02 | 0xC1 |
0x02 |
TCA9555 regs 0x06/0x07 are the direction registers, power-on default
0xFF = all inputs. So the cold dump is just the un-configured power-on state,
and the warm one is what the previous firmware left behind, meaning PA_EN
(EXIO8) is never driven, so the amp stays off. Waveshare's own code fixes this
explicitly with TCA9555PWR_Init(0x0000) (all pins to output).
What that mechanism does not account for:
- The LED symptom. The ring is on GPIO38 over RMT, nowhere near the
expander, so an expander direction register cannot keep it dark. (The
reporter's "green LED" may be
LED1, the hardware charge indicator, which firmware cannot control.) - The ES7210 registers. The reporter states that replaying them did not help. Their other unsuccessful attempts were delays up to 1000 ms and bringing MCLK up before I2C.
What this firmware does (a mechanism-based mitigation, not a proven fix):
ESPHome's tca9555 writes the direction registers itself, and the amp_enable
GPIO switch with restore_mode: RESTORE_DEFAULT_ON drives PA_EN at boot. This
matches a community config that is reported working.
Amp idle hiss / turn-on pop
Waveshare's Audio_Init() orders it codec first, amp second
(es8311_codec_init() then Audio_PA_EN()), with a 50 ms delay after each
Set_EXIO. An amp left enabled at boot amplifies the undriven DAC line as a
faint hiss until the first playback. This firmware gates the amp instead:
amp_enable is ALWAYS_OFF at boot and the media_player on_state turns it on
when playback starts (then leaves it on, since the speaker holds the line at
clean silence via timeout: never).
Strapping pins
ESP32-S3 straps are GPIO0, GPIO3, GPIO45, GPIO46.
| Pin | Board use | Risk |
|---|---|---|
| GPIO0 | BOOT button | fine, intended |
| GPIO3 | LCD_CS | floating by default; don't add strong pulls |
| GPIO45 | CAM_D4 | ⚠️ must be LOW at boot (VDD_SPI = 3.3 V) |
| GPIO46 | CAM_D5 | ⚠️ must be LOW at boot |
⚠️ A camera plugged into J3 sits on two strapping pins. If the module drives them HIGH during reset the board may not boot, which is relevant even for a voice-only build if a camera is left connected.
GPIO33-37 do not exist for you
The octal PSRAM occupies GPIO33-37 (on top of flash on GPIO26-32). No Waveshare pinout table assigns them. Any config claiming to use them is wrong.
Unresolved
Documented rather than guessed at:
- Battery ADC pin: GPIO1 (schematic) vs GPIO8 (demo).
- EXIO6 vs EXIO7: official sources contradict each other.
- ES7210 slot order: wiring (CH3 = ref) vs demo's
"RMNM". - EXIO4: no known function.
- RTC_INT: net exists, terminus unknown.
- RGB vs GRB: verify with a pure-red test.
- I2C pull-ups: resistors visible, nets not resolvable from the PDF text.
- Cold-boot fix: none published.