ESP32 bring-up (S3, C5, and C6)
openaliro/openaliro
Hardware

ESP32 bring-up (S3, C5, and C6)

One page, match-the-table. The pin map's source of truth is ports/esp32/components/woz_uwb/port/board_pins.h; if you change it there, change it here.

1. Wire the radio

DWM3000EVB shield

Power the EVB from the ESP32 board's 3V3 pin, not 5 V — the DW3000 is a 3.3 V part. Share a common ground and USB-power the board during bring-up.

DWM3000EVB pinSignalESP32-S3ESP32-C5ESP32-C6
D13SCLKGPIO12GPIO8GPIO6
D11MOSIGPIO11GPIO9GPIO7
D12MISOGPIO13GPIO23GPIO2
D10CSGPIO10GPIO10GPIO10
D8IRQGPIO5GPIO5GPIO3
D7RSTnGPIO4GPIO4GPIO1
D9WAKEUPGPIO6GPIO6GPIO0
3V3power3V33V33V3
GNDgroundGNDGNDGND
D1SPI-POLGND (mode-0 strap)samesame
D0SPI-PHAGND (mode-0 strap)samesame

Mode-0 strap: the DW3000 SPI must run CPOL=0/CPHA=0. Tie D0 and D1 to GND unless your EVB revision already fixes the mode on the shield — check the EVB manual before soldering.

GPIO 4, 5, 6, and 10-13 are clear of the S3's octal PSRAM pins. SPI2 routes through the GPIO matrix, so the bus can be remapped in board_pins.h if your board does not break these pins out.

Why the C5 data pins differ: on the C5 the S3's GPIO11/12 are the UART0 console and GPIO13 is USB-Serial-JTAG; GPIO8/9/23 also avoid the strapping pins (2/7/25/27/28, plus 3/26 per the DevKitC-1 guide) and the GPIO27 RGB LED. The C5 build targets the 4 MB flash floor of the WROOM-1 family (partitions_4mb.csv); if your module has more, raise CONFIG_ESPTOOLPY_FLASHSIZE in sdkconfig.defaults.esp32c5.

Why the C6 pins differ: GPIO6/7/2 are SPI2's direct IO_MUX SCLK/MOSI/MISO pins. GPIO10/1/3/0 keep the control signals away from the C6 strapping pins (GPIO4/5/8/9/15), native USB (GPIO12/13), and UART0 (GPIO16/17). The C6 reader and Matter configs target the official ESP32-C6-DevKitC-1's 8 MB flash.

Sources: ESP32-C6 SPI2 pins and ESP32-C6-DevKitC-1 pin/flash guide.

BU04 module in direct-SPI mode

The same C6 firmware can drive the DW radio inside a BU04 directly. The onboard STM32F103 shares the DW SPI and control nets, so connect BU04 pad 2 (ST_NRST) to GND before powering either board and keep it low for the entire session. Do not release ST_NRST while the ESP32 is driving those nets; otherwise both MCUs can drive the bus.

BU04 module padSignalESP32-C6-DevKitC-1
13 SPI_CLKSCLKGPIO6
14 SPI_MOSIMOSIGPIO7
15 SPI_MISOMISOGPIO2
16 SPI_CSNCSGPIO10
17 IRQIRQGPIO3
12 DW_RSTNRSTnGPIO1
11 DW_WAKEUPWAKEUPGPIO0
20 GPIO5/SPIPOLSPI mode strapGND
21 GPIO6/SPIPHASPI mode strapGND
1 or 10groundGND
23 VDD1 and 24/34 3V3power domainsregulated 3.3 V
2 ST_NRSThold onboard STM32 in resetGND for the entire session

The BU04 specification recommends a 3.3 V supply capable of at least 500 mA. Do not use 5 V on these module power pins. DW_RSTN is open-drain: the ESP driver only pulls it low and releases it; never add an external pull-up to a different voltage.

Source: Ai-Thinker BU04 specification.

This first image still selects the existing DW3000-family driver profile. On boot, capture the raw DEV_ID line. IDs ending in ...02/...12 use the current profile; ...04/...14 require switching the component to its in-tree DW3720 profile before ranging.

Check the EVB power-select jumper

Do this before anything else. Correct wiring is not enough if the EVB's own power-select jumper picks the wrong source: SPI then fails silently, with no valid device ID and a responder that never listens, and it looks exactly like a software fault. This cost days of debugging once. Check the jumper first.

2. Build and flash

$cd ports/esp32/apps/reader
$make set-target TARGET=esp32c6
$make build
$make flash
$make monitor

make set-target runs idf.py set-target once per checkout. ESP-IDF is expected at ~/esp/esp-idf; override with IDF_EXPORT=. The port is auto-detected and SEGGER/J-Link ports are refused; make ports lists what is attached and how each is classified.

3. What good output looks like

The bench app brings the radio up, binds a canned URSK, and starts the CCC DS-TWR responder:

I (xxx) woz_esp32: app_responder_start() = 0 (DW3000 up, responder listening)

  • = 0 — SPI, DW3000, and the CCC init path all came up. The engine is talking to the

chip. With no peer present there are no range lines, which is expected.

  • = <nonzero> (FAILED -- check wiring/SPI) — the DW3000 did not answer. In order:

recheck the power-select jumper, then CS/SCLK/MOSI/MISO, then the mode-0 strap, then drop to slow-only by setting WOZ_DW3000_SPI_FAST_HZ to 2000000 in board_pins.h.

4. Prove a real range

Ranging needs a peer to drive the DS-TWR exchange: an Aliro-capable iPhone with a key provisioned for this reader, or a second DW3000 board acting as initiator. With a peer, range: NN cm lines appear and status reports a trusted range.

For the full approach-unlock path — commissioning, a key in the phone's wallet, and the Wallet unlock animation — use the Matter app in ports/esp32/apps/matter-lock instead. This bench app has no Matter layer, so nothing provisions a real credential into a phone for it.

No antenna calibration was needed on this hardware. If distances come out negative or absurd, read docs/esp32-gotchas.md §6.4 before reaching for a calibration constant — it was a timestamp-pairing bug, not a physical offset.

ESP32-S3 is hardware-validated with the DWM3000EVB. ESP32-C6 is hardware-validated with the BU04 in direct-SPI mode and ST_NRST held low. ESP32-C5 has build/release support but no recorded hardware validation.