Board is now selected by `default_envs` in platformio.ini rather than being hardcoded. Both boards stay supported and both targets build. - src/board_pins.h holds one block per board plus an ledSet() abstraction, so main.cpp has no board-specific code. The C6's active-low LED and the S3's WS2812 RGB go through the same call, and the C6 antenna-switch lines are behind #if HAS_RF_SWITCH. - YD-ESP32-S3 env targets esp32-s3-devkitc1-n16r8 (verified against the actual part: ESP32-S3 rev v0.2, 16MB quad flash, 8MB octal PSRAM, CH343 bridge). Serial is UART0 -> CH343, not USB CDC. Fixes for hangs that left the board alive on USB but dead on the network: - The vendor library's UART path has loops with no timeout: sensorStop() is while(1) that only exits on a "sensorStop" reply, and the UART branch of getStatus() is while(len == 0). Calling either with no sensor attached hung setup() permanently. UART is now probed with a raw 1.5s read on Serial1, and the library only gets the port once bytes actually arrive. The I2C path is bounded, so prefer I2C. - Floating SDA/SCL can stall Wire.endTransmission() forever. Check both lines read high with pull-ups before touching Wire. - Serial.setTxTimeoutMs(0) is a trap: HWCDC::write seeds a uint32_t from it and does a bare tries--, so 0 underflows to 4.29e9 and spins for ~49 days. Use 10, and only on USB CDC boards. Diagnostics, so "did it crash?" is answerable without a serial cable: - Boot count in RTC memory, esp_reset_reason(), free/min heap and WiFi drop count exposed via /api/live and shown as header pills in the UI. - WiFi auto-reconnect, and retry of station mode from the AP fallback. Without it a single dropout left the board unreachable forever, which looks exactly like a crash. Co-Authored-By: Claude <noreply@anthropic.com>
224 lines
8.9 KiB
Markdown
224 lines
8.9 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, USB-C end at the 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
|
||
```
|
||
|
||
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`).
|