Detection previously reported only "no ACK at 0x2a/0x2b", which cannot tell a sensor at an unexpected address from one that is not wired at all. - Sweep the full 1..126 address range and log any responders. - Before enabling the internal pull-ups, sample SDA/SCL as plain inputs. A powered C4001 breakout holds them high through its own pull-ups, so an all-low reading means nothing powered is on those pins -- a wiring answer rather than a protocol one. This immediately identified a miswire that the ACK-only log had left ambiguous. - Drop the \r\n from the raw UART probe. DFRobot_C4001_UART::writeReg sends the bare command string with no terminator, so the probe should match it. README had the YD-ESP32-S3 header order right but the orientation wrong: it said USB-C at the top when the module is at the top and both USB-C sockets are at the bottom. Read upside down that walks the sensor into the PSRAM pins. Also note the separate plain LED on GPIO38 vs the RGB on GPIO48. Verified on hardware: C4001 detected at I2C 0x2a, init ok, streaming range, velocity and energy in speed mode. Co-Authored-By: Claude <noreply@anthropic.com>
234 lines
9.5 KiB
Markdown
234 lines
9.5 KiB
Markdown
# DFRobot C4001 mmWave presence sensor on ESP32
|
||
|
||
Live-data and calibration rig for a C4001 24 GHz radar. The board hosts a web
|
||
page showing presence, distance, velocity and signal energy in real time, with
|
||
sliders for every sensor parameter.
|
||
|
||
Two boards are supported. Both are kept working; nothing is deleted when you
|
||
switch.
|
||
|
||
## Choosing a board
|
||
|
||
Change one line in `platformio.ini`:
|
||
|
||
```ini
|
||
[platformio]
|
||
default_envs = yd_esp32_s3 ; or: xiao_esp32c6
|
||
```
|
||
|
||
Or build one without editing the file:
|
||
|
||
```bash
|
||
pio run -e xiao_esp32c6 -t upload
|
||
```
|
||
|
||
Pin maps live in [src/board_pins.h](src/board_pins.h), one block per board.
|
||
To add a board: add an `[env:...]` with a new `-DBOARD_xxx` flag, then a
|
||
matching block in that header. Nothing else in the firmware is board-specific.
|
||
|
||
| | YD-ESP32-S3 | XIAO ESP32-C6 |
|
||
|---|---|---|
|
||
| env | `yd_esp32_s3` | `xiao_esp32c6` |
|
||
| MCU | ESP32-S3-WROOM-1 N16R8, rev v0.2 | ESP32-C6FH4 rev v0.1 |
|
||
| Flash / PSRAM | 16 MB quad / 8 MB octal | 4 MB / none |
|
||
| MAC | `24:58:7c:cd:9a:d8` | `e4:b3:23:b4:7b:c4` |
|
||
| Serial | UART0 → CH343 (`1a86:55d3`), COM port | native USB Serial/JTAG (`303a:1001`) |
|
||
| Free heap | ~301 KB | ~250 KB |
|
||
|
||
Both verified: builds clean, flashes, boots, joins WiFi, serves the UI.
|
||
|
||
## 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 neither ESP32's GPIOs are 5 V tolerant.
|
||
|
||
**Prefer I2C.** See the vendor-library warning below — the UART path in
|
||
DFRobot's driver has loops with no timeout. I2C is bounded and safe.
|
||
|
||
The firmware auto-detects which bus the sensor is on, so wire whichever your
|
||
board's mode switch is set to.
|
||
|
||
### YD-ESP32-S3
|
||
|
||
The YD board has GPIO numbers printed on the silkscreen, so read the board
|
||
itself — the header order below is only for orientation.
|
||
|
||
| C4001 | Pin | Header |
|
||
|---|---|---|
|
||
| VIN | 3V3 | P1, top |
|
||
| GND | GND | P1 bottom, or either GND on P2 |
|
||
| SCL | **GPIO9** | P1 |
|
||
| SDA | **GPIO8** | P1 |
|
||
| TX (UART) | **GPIO18** | P1 |
|
||
| RX (UART) | **GPIO17** | P1 |
|
||
| OUT | GPIO16 | P1, optional |
|
||
|
||
Header order. **Orientation matters: hold the board with the ESP32-S3-WROOM-1
|
||
module and its antenna at the TOP, and both USB-C sockets at the BOTTOM.**
|
||
Pin 1 of each column is the end nearest the module, furthest from the USB
|
||
sockets. (Getting this upside down is an easy way to wire the sensor into the
|
||
PSRAM pins.)
|
||
|
||
```
|
||
[ ESP32-S3-WROOM-1 module + antenna ] <- TOP
|
||
P1 (left) P2 (right)
|
||
1 3V3 1 GND
|
||
2 3V3 2 GPIO43 U0TXD -> CH343
|
||
3 RST 3 GPIO44 U0RXD <- CH343
|
||
4 GPIO4 4 GPIO1
|
||
5 GPIO5 5 GPIO2
|
||
6 GPIO6 6 GPIO42
|
||
7 GPIO7 7 GPIO41
|
||
8 GPIO15 8 GPIO40
|
||
9 GPIO16 <- C4001 OUT 9 GPIO39
|
||
10 GPIO17 -> C4001 RX 10 GPIO38
|
||
11 GPIO18 <- C4001 TX 11 GPIO37 ** PSRAM **
|
||
12 GPIO8 <- C4001 SDA 12 GPIO36 ** PSRAM **
|
||
13 GPIO3 13 GPIO35 ** PSRAM **
|
||
14 GPIO46 strapping 14 GPIO0 BOOT button
|
||
15 GPIO9 <- C4001 SCL 15 GPIO45 strapping
|
||
16 GPIO10 16 GPIO48 RGB WS2812
|
||
17 GPIO11 17 GPIO47
|
||
18 GPIO12 18 GPIO21
|
||
19 GPIO13 19 GPIO20 USB D+
|
||
20 GPIO14 20 GPIO19 USB D-
|
||
21 5V 21 GND
|
||
22 GND 22 GND
|
||
[ USB-C: COM (CH343) | USB ] <- BOTTOM
|
||
```
|
||
|
||
Reference pinout: https://mischianti.org/vcc-gnd-studio-yd-esp32-s3-devkitc-1-clone-high-resolution-pinout-and-specs/
|
||
Note that board also has a plain built-in LED on **GPIO38**, separate from the
|
||
RGB WS2812 on GPIO48 that this firmware drives.
|
||
|
||
Do not use for I/O: **GPIO35/36/37** (octal PSRAM — the R8 in N16R8),
|
||
**GPIO43/44** (UART0 to the CH343), **GPIO19/20** (native USB), GPIO0/45/46
|
||
(strapping), GPIO48 (RGB LED), GPIO26–32 (flash, not broken out).
|
||
|
||
The board has **two USB-C sockets**. Use the one marked **COM** (CH343) — that's
|
||
what `default_envs = yd_esp32_s3` assumes, and it puts `Serial` on a real UART
|
||
instead of USB CDC.
|
||
|
||
### XIAO ESP32-C6
|
||
|
||
The XIAO's pads are unlabelled. Component side up, USB-C at the top; pin 1 of
|
||
each column is nearest the connector.
|
||
|
||
```
|
||
┌───┐ USB-C ┌───┐
|
||
┌─────┴───┴───────┴───┴─────┐
|
||
D0 ────┤ o o ├──── VBUS (5 V)
|
||
GPIO0 │ │
|
||
D1 ────┤ o o ├──── GND
|
||
GPIO1 │ │
|
||
D2 ────┤ o seeed studio o ├──── 3V3
|
||
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 └───────────────────────────┘
|
||
```
|
||
|
||
| C4001 | XIAO pin |
|
||
|---|---|
|
||
| VIN | 3V3 |
|
||
| GND | GND |
|
||
| SCL | **D5** (GPIO23) |
|
||
| SDA | **D4** (GPIO22) |
|
||
| TX (UART) | **D9** (GPIO20) |
|
||
| RX (UART) | **D10** (GPIO18) |
|
||
| OUT | D2 (GPIO2), optional |
|
||
|
||
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). Without the RF switch lines, WiFi range is centimetres.
|
||
|
||
## 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
|
||
(`xiao-mmwave` / `mmwave1234`).
|
||
|
||
```bash
|
||
pio run # build the default env
|
||
pio run -t upload # flash
|
||
pio device monitor # 115200
|
||
```
|
||
|
||
Web UI at the IP printed on serial, or `http://mmwave.local/`.
|
||
|
||
## Traps found the hard way
|
||
|
||
**The vendor library's UART path can hang forever.** In
|
||
`DFRobot_C4001.cpp`, `sensorStop()` is `while(1){...}` that only exits on a
|
||
`"sensorStop"` reply, and the UART branch of `getStatus()` is
|
||
`while(len == 0){...}` with no timeout. Calling either with no sensor attached
|
||
hangs `setup()` permanently and the board never reaches WiFi. This firmware
|
||
therefore probes UART with a raw 1.5 s read on `Serial1` and only hands the port
|
||
to the library once bytes actually arrive. **The I2C path is bounded and safe** —
|
||
prefer I2C.
|
||
|
||
**Never point the sensor UART at the console pins.** GPIO43/44 on the S3 and
|
||
GPIO16/17 (D6/D7) on the C6 are the chips' hard-wired `U0TXD`/`U0RXD`. Attaching
|
||
a second UART to them deadlocks the UART driver and trips the interrupt
|
||
watchdog — the board boot-loops before reaching WiFi.
|
||
|
||
**Floating I2C lines can stall the peripheral.** With nothing wired, SDA/SCL sit
|
||
low and `Wire.endTransmission()` may never return. The firmware checks both
|
||
lines read high with pull-ups before touching `Wire`.
|
||
|
||
**`Serial.setTxTimeoutMs(0)` is a trap on USB CDC boards.** In `HWCDC::write`,
|
||
`tries` is a `uint32_t` seeded from that value and the no-progress path does a
|
||
bare `tries--`, so 0 underflows to 4.29 billion and spins for ~49 days. Use a
|
||
small non-zero value. Only relevant when `Serial` is USB CDC (the C6 env).
|
||
|
||
**DFRobot's wiring table is wrong.** It pairs the sensor's RX with the MCU's RX,
|
||
and the library swaps the `rxpin`/`txpin` parameter names between the header and
|
||
the `.cpp`. Correct is sensor TX → board RX.
|
||
|
||
**Watch power on bus-powered boards.** A 24 GHz radar plus WiFi TX bursts on one
|
||
USB port is marginal. Symptoms are not a crash: no panic, no backtrace, the USB
|
||
device simply disconnects and re-enumerates. If that happens, add a 100–470 µF
|
||
bulk capacitor across the sensor's VIN/GND and use a direct port rather than a
|
||
hub.
|
||
|
||
## Using it
|
||
|
||
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. Header pills report board, link type, uptime, boot count, last
|
||
reset reason, WiFi drops and free heap — enough to answer "did it crash?"
|
||
without a serial cable.
|
||
|
||
**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 — it shows the numbers behind the decision — then switch
|
||
to presence mode for normal use. Settings apply immediately but are volatile;
|
||
**Save to sensor flash** to persist them.
|
||
|
||
## Layout
|
||
|
||
```
|
||
platformio.ini board envs; default_envs selects one
|
||
src/board_pins.h per-board pin map + LED abstraction
|
||
src/main.cpp detection, polling, WiFi, HTTP API
|
||
src/web_ui.h single-page UI, inlined into flash
|
||
include/secrets.h.example WiFi template (real one is gitignored)
|
||
```
|
||
|
||
HTTP API: `GET /api/live`, `GET /api/config`, `POST /api/config`,
|
||
`POST /api/action` (`start`/`stop`/`save`/`reset`/`factory`).
|