DEV Community

prodbymarcu
prodbymarcu

Posted on

I built a handheld mmWave presence radar, and the first bug was a board that never ran my code

One small device noticing one thing. That's the demo genre I keep falling for, and this time it's an M5Stack Cardputer-Adv with an LD2410 mmWave sensor hanging off its EXT header. The firmware turns the sensor's UART stream into a radar scope on the handheld's 240 by 135 screen: a sweep line rotating through the cone the sensor watches, roughly 60 degrees, a contact sitting at a radius equal to the measured distance, a trail fading behind it as the target moves, and a NO PRESENCE state when the room is empty. No app, no phone in between. The device itself knows someone is approaching.

Writing the firmware was the easy part. The hard part was getting a single byte back out of the board.

What the sensor streams

The LD2410 reports presence and distance about every 100 milliseconds at 256000 baud, one target at a time, split into a moving reading and a stationary reading. Frames come wrapped in a fixed envelope: four header bytes up front, F4 F3 F2 F1, four tail bytes at the back, F8 F7 F6 F5, a length and a type in between. The frame I care about carries the basic target state, one byte for the state (nobody, moving, stationary, both), two bytes of moving distance, a moving energy byte, then the same trio again for the stationary side.

Parsing that is a small state machine over the byte stream. Slide until the four header bytes line up, accumulate until the four tail bytes close the frame, decode, repeat. On top of it, a render loop runs at 10 frames a second, the sweep advances 9 degrees per frame, about 40 sweeps a minute, and the contact plots at measured distance against the sensor's 6 meter range, mapped onto the scope radius. Above the scope sits the distance in centimeters, MOVING or STATIC, and two energy bars. And one small flag, ld:OK, that lights when the sensor has sent frames in the last second. That flag ended up being the most important pixel on the screen.

Zero bytes, and where I was looking

First flash, first open of the serial monitor, and the board had zero bytes to say. No garbage bytes, no half frames, nothing.

I reviewed the parser the way you do, baud rate, frame layout, pins. Zero bytes gives you nothing to debug against, though, so I flashed the dumbest possible sketch, a heartbeat that prints one line every two seconds and does nothing else. It streamed zero bytes too. A sketch that can't fail in software, equally silent.

That killed the parser theory. Zero bytes meant the app had never started, and the reasons were mechanical, all of them outside the code.

My build config had ARDUINO_USB_MODE=1, the JTAG option. This card talks serial over native CDC, and the JTAG build left it unresponsive to stdio. One line, ARDUINO_USB_MODE=0, and it's the first line I check in any new ESP32 build config now.

After an upload, esptool and pio leave the board sitting in its bootloader, and the app does not auto-run out of it. Zero bytes is exactly what a board in that state looks like, and the only fix is a press of the device's power button.

Then the one that stung. When I finally pulled the chip info, the board on my bench came back as a plain ESP32-S3 DevKitC, 8MB PSRAM, 16MB of flash. Not a Cardputer. The firmware was written for a handheld with a display, a keyboard, and an M5 library, and the thing on my desk has none of those. I had assumed the one serial device plugged into my Mac was the Cardputer I left there. Wrong board, and no amount of re-reading code was going to put a display on a devkit.

Porting down to what actually exists

So I ported the project to the hardware that was in front of me. devkit_radar/devkit_radar.ino drops the M5 library and the display and keeps the part that matters, the same LD2410 frame parser on GPIO15 and GPIO16, printed straight to serial. A line every two seconds, plus a line any time presence flips or the distance moves more than 5 centimeters, shaped like this:

[TARGET 87cm MOV ld:OK e:0.42]
Enter fullscreen mode Exit fullscreen mode

That line is the piece I'd keep from this stretch, because it makes silence readable. Three states, three different fixes. No bytes at all means the board is parked in the bootloader, press the reset button. Lines arriving with the flag at ld:-- mean the board is running and the sensor is quiet, a wiring problem, and the classic one is swapped TX and RX, which reads as distance stuck at zero. And ld:OK on a TARGET line with a real centimeter value means the chain is alive, and walking toward the sensor should close the number.

The same mirror went into the Cardputer firmware, so both builds talk the same protocol over USB. The radar firmware went onto the board three times, hash verified on every pass, and the devkit flashed through arduino-cli with the USB mode pinned to CDC.

What shipped

The Cardputer side is done in software. Scope, sweep, trails, three color themes on the 0 key, a HUD toggle on the period key, ESC to blank the screen, the serial mirror, the heartbeat. devkit_radar is the headless twin for the bench board. The wiring is written down as a table, EXT pin 13 to the sensor's TX, pin 14 to its RX, 5V and ground, UART straight through since the LD2410 logic runs at 3.3V while the sensor itself takes 5V on VCC.

What's still open is physical, and I won't pretend otherwise. The app needs a power button press after flashing before it runs, the sensor has never been wired, and the walk test hasn't happened yet. The next session is a reset press and four jumper wires, then watch the serial line. If the distance closes, the blip closes on the scope. If it doesn't, the line tells me which link is lying within one boot cycle, and on hardware that counts as a good day.

Top comments (0)