Firmware for a Seeed XIAO ESP32-C6 driving a C4001 24GHz radar, with a web UI for live data and calibration. - Auto-detects whether the C4001 is on I2C (0x2A/0x2B) or UART, so the board's mode switch position doesn't matter. Serves the probe log over HTTP when nothing is found. - Single-page UI inlined into flash (no CDN, so it works in AP mode): presence state, target count, range, speed, energy, rolling 60s chart, and mode-aware calibration controls. - Joins WiFi from a gitignored include/secrets.h, falls back to a "xiao-mmwave" AP, registers mmwave.local. - Poll rate adapts to the interface: UART at 9600 costs ~60ms per register round-trip, so polling it at the I2C rate starved the HTTP server. Two hardware gotchas documented in the README: - The sensor UART must not use D6/D7. Those are GPIO16/17, the C6's hard-wired U0TXD/U0RXD. Pointing Serial1 at them deadlocks the UART driver and trips the interrupt watchdog, boot-looping the board. UART1 has no fixed pins, so it routes through the GPIO matrix to D10/D9. - DFRobot's own wiring table pairs the sensor's RX with the MCU's RX, and their library swaps the rxpin/txpin parameter names between the header and the .cpp. Verified on ESP32-C6FH4 rev v0.1, 4MB flash: builds at 57.6% flash / 11.9% RAM, flashes over the native USB Serial/JTAG port, and reads a live C4001 on I2C @ 0x2A. Co-Authored-By: Claude <noreply@anthropic.com>
177 lines
6.8 KiB
Markdown
177 lines
6.8 KiB
Markdown
# 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`).
|