A Home Assistant that forces HTTPS on 8123 with a certificate that does not cover the LAN IP makes every announcement/TTS fetch fail certificate verification, and the device holds in STREAMING_MICROPHONE with no "Starting STT by VAD", identical in the log to a too-tight vad threshold. Add the log signature and the HA-side fix to the gotchas list. Reported in issue #3. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
11 KiB
name, description
| name | description |
|---|---|
| waveshare-esp32-s3-audio | Reference for building/editing ESPHome configs on the Waveshare ESP32-S3-AUDIO-Board (ESP32-S3R8 smart-speaker devkit: ES8311 codec + NS4150B amp, ES7210 dual-mic ADC, TCA9555 I/O expander, 7x WS2812 ring, PCF85063 RTC, DVP camera and SPI LCD connectors). Use whenever working on this board (or the base/core.yaml in this repo): correct pinout, why the DAC must be forced I2S master, the mute/gain traps, the EXIO map, strapping pins, and which "official" sources are wrong. |
Waveshare ESP32-S3-AUDIO-Board: ESPHome working notes
Facts below are from Waveshare's schematic v1.1 and their own demo source (Arduino + ESP-IDF), cross-checked with a working ESPHome config. Where sources conflict, that is stated. Don't paper over it.
Full detail and citations: docs/HARDWARE.md in this repo.
Board
- ESP32-S3R8 (bare chip), 240 MHz, 8 MB octal PSRAM, 16 MB flash.
- ES8311 mono codec (DAC) into NS4150B Class-D amp into speaker (JST header).
- ES7210 4-ch ADC with 2 physical mics (CH1/CH2). CH3 = AEC loopback.
- TCA9555 I/O expander @ 0x20: amp enable + 3 buttons (+ LCD/cam/SD lines).
- 7x WS2812B ring on GPIO38, driven directly over RMT, not via expander.
- PCF85063 RTC @ 0x51. DVP camera + SPI/QSPI LCD connectors. USB-C. Li-ion header.
- Wi-Fi 2.4 GHz + BT 5 LE. ESP32-S3 has no Bluetooth Classic, so no A2DP.
ESPHome target: board: esp32-s3-devkitc-1, variant: esp32s3, flash_size: 16MB,
framework: esp-idf, psram: {mode: octal, speed: 80MHz}.
Pinout (authoritative)
I2S (ONE shared bus): MCLK=12 BCLK/SCLK=13 LRCK/WS=14 DIN=15 (mic) DOUT=16 (spk)
I2C (one bus): SDA=11 SCL=10 100 kHz confirmed working
LED ring WS2812: DATA=38 (7 LEDs, RGB order, but verify: see gotchas)
BOOT button: GPIO0 (active low). RESET = hardware CHIP_PU, not readable.
SD (1-bit SDMMC): CLK=40 CMD=42 D0=41 CS=EXIO3 (D1/D2 = NC)
LCD (use WIKI table): CS=3 SCK=4 BL=5 SDA3=6 DC=7 MISO=8 MOSI=9 RST=EXIO0
Camera DVP: D0=2 D1=17 D2=18 D3=39 D4=45 D5=46 D6=47 D7=48
VSYNC=21 HREF=1 PCLK/XCLK muxed 44/43 or 19/20 PWDN=EXIO5
USB: D-=19 D+=20 UART0: TX=43 RX=44
GPIO33-37: UNUSABLE, taken by octal PSRAM (flash uses 26-32)
I2C addresses: ES8311 0x18, ES7210 0x40, TCA9555 0x20, PCF85063 0x51.
TCA9555 EXIO map
0: LCD_RST 4: unknown 8: PA_EN (amp enable, ACTIVE HIGH)
1: TP_RST 5: CAM_PWDN (AL) 9: Key1 (active low, 10k HW pull-up)
2: TP_INT 6: Camera_SEL * 10: Key2 (active low)
3: SD_CS 7: USB/cam mux * 11: Key3 (active low)
12-15: expansion header
* EXIO6/EXIO7: schematic and wiki/demo disagree on which does the mux.
NEVER drive either. The wrong one kills USB and forces manual download mode.
The TCA9555 has no reset pin (schematic pin 1 is INT#).
The one thing that defines this board: shared I2S clocks
ES8311 and ES7210 sit on the same BCLK (13) / LRCK (14). Only one device may
drive them, and ESPHome cannot run a single i2s_audio bus full-duplex: a
microphone and a speaker on one 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 that works on stock ESPHome (no patched es8311):
- Two i2s_audio buses (two I2S ports) over the shared pins. The mic bus is the master and 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 - which is what a slave speaker (and the ES8311
DAC) need. Making the mic the master also gives it a correct-rate stream; a
codec-mastered clock (the old
force_masterroute) fed the mic garbage and killed wake word. - Pin the mic to 16-bit. As master it sets the frame slot width, and the i2s_audio default is 32-bit; a 32-bit frame against the 16-bit ES8311/speaker doubles the bit clock they expect and playback comes out as noise.
i2s_audio:
- id: i2s_input # mic bus = master (drives the shared clock)
i2s_mclk_pin: GPIO12
i2s_bclk_pin: { number: GPIO13, allow_other_uses: true }
i2s_lrclk_pin: { number: GPIO14, allow_other_uses: true }
- id: i2s_output # speaker bus = slave
i2s_bclk_pin: { number: GPIO13, allow_other_uses: true }
i2s_lrclk_pin: { number: GPIO14, allow_other_uses: true }
audio_dac: { platform: es8311, id: es8311_dac } # stock
audio_adc: { platform: es7210, id: adc_mic } # stock
microphone:
- platform: i2s_audio
i2s_audio_id: i2s_input # default i2s_mode: primary -> master
bits_per_sample: 16bit
speaker:
- platform: i2s_audio
i2s_audio_id: i2s_output
i2s_mode: secondary # slave to the mic's clock
Do not make the ES8311 the master via a force_master-style patch: a
codec-mastered clock feeds the ESP mic a wrong-rate stream and kills the wake
word. The ESP-mastered two-bus layout needs no patched component.
Gotchas that cost real time
microphone.muteis the correct mute. It makes the Microphone hand every consumer a zero-filled buffer (set_mute_stateinmicrophone.h), so the wake word hears real silence and the stream never restarts. Do not "mute" by setting ES7210 gain to 0, because 0 dB is unity gain, not silence. There are alsomicrophone.unmuteand themicrophone.is_mutedcondition.- ES7210 gain caps at 37.5 dB, not 42.
set_mic_gain()doesclamp<float>(gain, MIN, MAX)and the register steps are 3 dB up to 33 dB, then 34.5/36/37.5. A slider promising more than 37.5 is lying to the user. - Template switch triggers fire during
setup(), atsetup_priority HARDWARE - 2, i.e. before the mic/mWW components exist, wheneverrestore_modeisn'tDISABLED(TemplateSwitch::setup()callsturn_on()/turn_off(), which fires the trigger). If a switch'son_turn_on/turn_on_actiontouches audio components, guard it with aninit_in_progress-style flag and apply the real state fromon_boot(priority -100). The same applies to a templateselectwithrestore_value: true: it replays the saved option duringsetup()and fireson_valuebefore the light/RMT and voice_assistant exist. Anon_valuethat repaints the ring (control_leds) then paints an effect on an uninitialised strip and crash-loops the board into safe mode. Guard theon_valuewith the sameinit_in_progresscheck. channels:onvoice_assistant/micro_wake_wordis NOT a channel count. If you wrap the mic (microphone: { microphone: id, channels: N }) it is aMicrophoneSourceandchannelsis a list of channel indices (cv.ensure_list(cv.int_range(0, 7)), default0), not a count. This firmware just passes the mic directly (microphone: i2s_mics) and lets it default, so the wrapper isn't used - simplest, and Assist won't take a stereo source anyway.- Hardware AEC is not reachable from stock ESPHome here. The demo packs
4x16-bit ADC channels into 2x32-bit I2S slots and unpacks in software; ESPHome
doesn't. Use
noise_suppression_level/auto_gaininstead. Practical fallout: the mic hears the device's own speaker loudly, so a "stop" wake word to interrupt a reply does not work on this board (detected too weakly and late). (The demo's declared slot order"RMNM"also doesn't reconcile with the schematic wiring - unresolved.) - Cold-boot: mic + LEDs sometimes don't come up until a reset. Reported on
the HA forum in a single post with zero replies, with no published root cause
or fix. The TCA9555 direction registers defaulting to
0xFF(all inputs) would leave PA_EN undriven, which ESPHome'stca9555+ aRESTORE_DEFAULT_ONGPIO switch on EXIO8 addresses. But that does not explain the LED symptom (the ring is on GPIO38/RMT, not the expander), and the reporter says replaying registers didn't help. Don't claim this is solved. - Strapping: GPIO45 (CAM_D4) and GPIO46 (CAM_D5) must be LOW at boot. A camera left plugged into J3 sits on both and can stop the board booting, even on a voice-only build. GPIO3 (LCD_CS) is also a strap, so don't add strong pulls.
- Waveshare's demo source contains stale copy-paste from other boards. Proven:
its
bsp_board.hLCD pins contradict the wiki's LCD table, and itsBAT_ADC_PIN 8contradicts the schematic's GPIO1 (GPIO8 is LCD_MISO here). Prefer the wiki + schematic over the demo for pin tables; prefer the demo for behaviour (which EXIO gets driven, init order). - The HA forum thread swaps I2C: it says SDA=10/SCL=11. It's SDA=11, SCL=10.
- RGB vs GRB: the demo says RGB and its own trailing comment says GRB, while
WS2812B is conventionally GRB. Two sources favour
rgb_order: RGB, but confirm with a pure-red test before trusting either. - Idle-amp hiss at boot. The amp (PA_EN on EXIO8) enabled at boot amplifies
the undriven DAC line as a faint hiss until the first playback (after which the
i2s speaker,
timeout: never, holds the line at clean silence). Fix by gating the amp:restore_mode: ALWAYS_OFF, then turn it on from the media_playeron_statewhen playback starts and leave it on. Do not try to fix this by playing a boot sound through the media player - a standalone boot announcement leavesmedia_player.is_announcingstuck true, andon_wake_word_detectedthen only ever stops that phantom announcement instead of starting Assist (wake word detected, nothing happens). - HTTPS-only Home Assistant stalls the pipeline. If HA serves port 8123 over
HTTPS with a certificate that does not cover the LAN address the device reaches
it by, every announcement/TTS fetch fails certificate verification and the
device holds in
STREAMING_MICROPHONEwith noStarting STT by VADline. In the log this is identical to a too-tightvadthreshold; the tell isesp-x509-crt-bundle: Failed to verify certificate/mbedtls_ssl_handshake returned -0x3000/Connection failed, sock < 0. The fix is HA-side, not firmware: set the HA Internal URL tohttp://<ip>:8123, or terminate TLS at a reverse proxy and keep HA's ownhttp:server plain. - Battery monitoring is effectively unavailable: the divider needs a 0 Ω resistor soldered (depopulated by default) and enabling it kills the camera. Ratio 3.0. Pin is GPIO1 per schematic, not GPIO8, which is stale demo code.
Validating without flashing
ESPHome is the real validator, but scripts/validate.py in this repo catches
YAML syntax, unresolved ${substitutions} and duplicate component ids offline.
Note when writing such tooling: an id: under a dotted key (script.execute,
light.turn_on, mixer_speaker.apply_ducking) is a reference, not a
declaration. Component declarations never sit under a dotted key.
scripts/esplog.py streams device logs over the native API (reads the API key
out of secrets.yaml), which beats the dashboard's log view for boot-time races.