mattandClaude b9162db707 XIAO ESP32-C6 + DFRobot C4001 mmWave presence sensor
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>
2026-08-14 10:05:27 -07:00

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

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

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).

S
Description
mmWaveESP testing
Readme
79 KiB
0 Stars 1 Watchers 0 Forks
Languages
C++ 92.1%
C 7.9%