Files
mmWaveESP/README.md
T
mattandClaude 9c1c3fe1e8 Add YD-ESP32-S3 support, boot diagnostics, and hang guards
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>
2026-08-14 11:55:03 -07:00

224 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`).