# XIAO ESP32-C6 + DFRobot C4001 mmWave presence sensor Live-data and calibration rig for a C4001 24 GHz radar on a Seeed XIAO ESP32-C6. The board hosts a web page showing presence, distance, velocity and signal energy in real time, with sliders for every sensor parameter. ## Verified on this machine | Thing | Value | |---|---| | Chip | ESP32-C6FH4 (QFN32) rev v0.1, single core + LP core, 160 MHz | | Flash | 4 MB embedded (mfr 0x20, dev 0x4016) | | Radios | WiFi 6, BT 5 LE, IEEE 802.15.4 | | MAC | `e4:b3:23:b4:7b:c4` | | Port | `/dev/ttyACM0` (native USB Serial/JTAG, VID `303a` PID `1001`) | | Toolchain | PlatformIO 6.1.19, espressif32 `2026.02.30`, Arduino core 3.3.7 | | Build | succeeds, 57.6% flash / 11.9% RAM | Flashing was confirmed working: esptool connected, uploaded its stub loader, ran it, and read the flash chip ID back. > **Arduino core version matters.** ESP32-C6 needs Arduino-ESP32 **3.x**. The > installed platform is the pioarduino fork (`2026.02.30`), which ships 3.3.7 and > includes the `XIAO_ESP32C6` variant. The old registry `espressif32 @ 6.x` has no > C6 support — don't downgrade. ## Pin diagram The XIAO's pads are unlabelled on the top side. Hold the board **component side up with the USB-C connector at the top** — pin 1 of each column is nearest the USB connector. ``` ┌───┐ USB-C ┌───┐ ┌─────┴───┴───────┴───┴─────┐ D0 ────┤ o o ├──── VBUS (5 V from USB) GPIO0 │ │ D1 ────┤ o o ├──── GND GPIO1 │ │ D2 ────┤ o seeed studio o ├──── 3V3 (3.3 V out) GPIO2 │ XIAO-ESP32-C6 │ D3 ────┤ o o ├──── D10 GPIO18 GPIO21 │ │ D4 ────┤ o o ├──── D9 GPIO20 GPIO22 │ │ D5 ────┤ o o ├──── D8 GPIO19 GPIO23 │ │ D6 ────┤ o [u.FL] [ant] o ├──── D7 GPIO17 GPIO16 └───────────────────────────┘ ``` Full mapping, from Seeed's official pinout sheet cross-checked against the Arduino variant header: | Silkscreen | GPIO | Alt function | |---|---|---| | D0 | 0 | ADC1 / A0, LP_GPIO0 | | D1 | 1 | ADC1 / A1, LP_GPIO1 | | D2 | 2 | ADC1 / A2, LP_GPIO2 | | D3 | 21 | — | | D4 | 22 | **I2C SDA** | | D5 | 23 | **I2C SCL** | | D6 | 16 | **U0 TXD** | | D7 | 17 | **U0 RXD** | | D8 | 19 | SPI SCK | | D9 | 20 | SPI MISO | | D10 | 18 | SPI MOSI | Not on the headers but used by the firmware: **GPIO15** user LED (active LOW), **GPIO3** RF switch enable (LOW = on), **GPIO14** antenna select (LOW = on-board ceramic, HIGH = external u.FL). ## Wiring the C4001 Power the sensor from **3V3, not 5V.** The C4001 accepts 3.3–5 V, but at 5 V its `OUT` pin can swing to 5 V and the C6's GPIOs are not 5 V tolerant. Running the sensor at 3.3 V makes every logic level match and removes the question entirely. The firmware auto-detects the interface at boot, so wire whichever your board's mode switch is set to. **UART (works on every C4001 SKU):** | C4001 | XIAO | Note | |---|---|---| | VIN | 3V3 | | | GND | GND | | | TX | **D9** (GPIO20) | sensor transmit → board receive | | RX | **D10** (GPIO18) | sensor receive ← board transmit | | OUT | D2 (GPIO2) | optional | **I2C (if your board has an I2C/UART switch set to I2C):** | C4001 | XIAO | |---|---| | VIN | 3V3 | | GND | GND | | SCL | **D5** (GPIO23) | | SDA | **D4** (GPIO22) | | OUT | D2 (GPIO2), optional | `OUT` is the sensor's own thresholded presence output. It is not required — the firmware reads everything over the data link — but wiring it lets you compare the sensor's internal decision against the raw numbers, which is genuinely useful while calibrating. > **Don't use D6/D7 for the sensor UART.** They are GPIO16/GPIO17, this chip's > hard-wired `U0TXD`/`U0RXD`. UART0 already owns them, so pointing `Serial1` at > those pins deadlocks the UART driver and trips the interrupt watchdog — the > board boot-loops before it ever reaches WiFi. Found this the hard way; the > UART pins above route UART1 through the GPIO matrix to pins with no default > peripheral function. > **Heads up on TX/RX.** DFRobot's own example table pairs the sensor's RX with > the ESP32's RX, which is wrong. Their library also swaps the `rxpin`/`txpin` > parameter names between the header and the `.cpp`. The table above is the > electrically correct wiring: **sensor TX → board RX**, sensor RX → board TX. ## Setup ```bash cp include/secrets.h.example include/secrets.h $EDITOR include/secrets.h # your WiFi; the file is gitignored ``` Skipping this is fine — the board then goes straight to AP mode. ## Build, flash, monitor ```bash pio run # build pio run -t upload # flash /dev/ttyACM0 pio device monitor # 115200 baud ``` If upload fails to auto-reset, hold **B** (boot), tap **R** (reset), release **B**, then upload. ## Using it On boot the firmware joins your WiFi; if that fails within 15 s it starts an access point: - **SSID** `xiao-mmwave` · **password** `mmwave1234` - Web UI at the IP printed on serial, or `http://mmwave.local/` The page shows a presence indicator, live target count / range / speed / energy, a rolling 60-second chart, and a calibration panel that changes with the active sensor mode. Serial output at boot tells you which interface was detected and prints the full probe log if nothing was found. ### The two sensor modes **Presence mode** (`eExitMode`) reports occupancy only. Parameters: min/max/trigger range, trigger and keep sensitivity (0–9), trigger delay (×0.01 s), keep timeout (×0.5 s). **Speed mode** (`eSpeedMode`) reports target count, distance, velocity and signal energy. Parameters: min/max range and a detection threshold (×0.1). Calibrate in speed mode, because it shows you the numbers behind the decision. Then switch to presence mode for normal operation. The in-page guide walks through it. Settings apply immediately but are volatile — **Save to sensor flash** to persist them. ## Layout ``` platformio.ini board, framework, C4001 library from git include/secrets.h.example template for WiFi creds (real one is gitignored) src/main.cpp detection, polling, WiFi, HTTP API src/web_ui.h the single-page UI, inlined into flash ``` HTTP API: `GET /api/live`, `GET /api/config`, `POST /api/config`, `POST /api/action` (`start`/`stop`/`save`/`reset`/`factory`).