SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
Variant
mtb-mpy and mtb-only

The radar task and its DSP chain are CM55 template source in both packages. The three sensors.radar* calls are thin IPC clients over that task and exist on mtb-mpy only; on mtb-only you read the same state through the extern variables and the two diagnostic functions, which is what the on-screen page does on both variants.

Learning goal

After this chapter you can get a presence flag, an energy figure and a first-peak range out of the BGT60TR13C, set a detection threshold and recapture a clutter baseline, and — when the numbers freeze, which is the failure this sensor actually has — read the two diagnostic triads that tell you which of three things stopped: the SPI link, the frame sequencer, or the task itself.

Note
Zero callers. sensors.radar_range() and sensors.radar_config() have no call site anywhere in the shipped tree, and neither does sensors.radar() inside this package. Every Python example in this chapter is authored. The C side has a shipped caller — the radar task runs from CM55 boot on both variants — so the sequence below is read from running code, not invented.

The real firmware sequence

The radar lives entirely on CM55. tesaiot_radar_task() (radar_task.h:96) brings up SPI, configures the BGT60TR13C, and then polls: read a frame, compute energy, update the presence flag. Three volatile globals carry the result to anyone who wants it — tesaiot_radar_presence_detected (:72), tesaiot_radar_current_energy (:77) and tesaiot_radar_initialized (:82). No lock: three word reads.

The DSP chain is a second consumer of the same frames. radar_dsp.c/h runs a 128-sample pipeline — RADAR_DSP_N is 128 (radar_dsp.h:29) — over radar_dsp_process() (:48), keeps a range snapshot fetched by radar_dsp_snapshot() (:59), and takes a detection threshold through radar_dsp_set_threshold_x10() (:52). The compiled-in default is RADAR_DSP_THRESHOLD_DB 6.0 (radar_dsp.h:32).

MicroPython is a client, not a driver. All three Python calls are IPC round-trips into that task, with the same retry and timeout budget: 20 send retries 100 µs apart, then a 500 ms wait for the response (modsensors.c:301-303). sensors.radar() sends IPC_CMD_RADAR_STATUS (modsensors.c:306); radar_range() and radar_config() share one helper, radar_dsp_ipc_roundtrip() (modsensors.c:367), and therefore share one shared-memory buffer pair — deliberately, because the shared region is full on some kits. Both failure modes raise OSError: "radar IPC send failed" when the pipe will not take the message, "radar IPC timeout" when CM55 does not answer inside 500 ms.

radar_range() has a fourth failure mode that is not an IPC failure. If the response comes back with initialized false it raises OSError("radar dsp not running") (modsensors.c:417-419) — the link is fine, the DSP chain simply is not up.

Threshold semantics, including the special value. sensors.radar_config(threshold_db) (modsensors.c:444):

  • 0.0 means re-capture the clutter baseline. It sends the config command with value 0, and the recapture takes roughly 160 ms. Clear moving targets out of the scene first, or you bake them into the baseline.
  • Anything else must be in 0.1 .. 60.0 dB above the baseline, or it raises ValueError("threshold 0.1..60.0 dB (0=recal)"). The value is sent as tenths of a dB.

There is no way to read the current threshold back.

Edge AI drains the same chirps. Under BENTO_HAS_EDGE_AI=1, radar_ai_frame_next() (radar_task.h:147) hands the model every 128-sample chirp in order through an 8-deep ring. The header records why the ring exists: a newest-only slot dropped every chirp but the last of each processing burst, so 88/s of the radar's 200/s reached the model and wrecked the inter-chirp Doppler pattern the network classifies on. It is a single-reader API — the CM55 ai_engine feed — and you should not add a second reader.

Step by step

Step 1 — Presence and energy

Example (authored — no shipped call site)
import sensors, time
for _ in range(20):
r = sensors.radar()
print(r['initialized'], r['presence'], "%.3f" % r['energy'])
time.sleep_ms(500)
What you should observe
initialized True once the CM55 task has finished bring-up. presence flips True when someone moves in front of the board and False a moment after they stop. energy is a float that rises with motion — it is a signal level for display and debugging, not a calibrated unit, and the header says so (radar_task.h:75-77). If initialized is False and stays False, the sensor never came up on CM55 and no Python call will change that.

On screen, the radar page reflects the same two globals, so the page and the REPL cannot disagree.

Step 2 — Range to the first peak

Example (authored — no shipped call site)
import sensors, time
while True:
d = sensors.radar_range()
if d['target']:
print("%.2f m peak %.1f dB res %.3f m seq %d"
% (d['distance_m'], d['peak_db'], d['resolution_m'], d['seq']))
else:
print("no target")
time.sleep_ms(300)
What you should observe
Five keys: distance_m, peak_db, resolution_m, target, seq. Walk towards the board and distance_m falls. distance_m == 0.0 means no target above the threshold — it does not mean "zero metres", and target is the boolean that says so plainly. resolution_m is the bin width, so a distance is only meaningful to about that precision. seq increments per DSP frame: if it stops advancing while the calls keep returning, the DSP chain has stalled and you are reading a stale snapshot — go to Step 4.

An OSError("radar dsp not running") here means initialized was false in the response: the chain is not up, which is different from "no target".

Step 3 — Set a threshold, then recapture the baseline

Example (authored — no shipped call site)
import sensors, time
sensors.radar_config(12.0) # 12 dB above the clutter baseline
time.sleep(1)
print(sensors.radar_range())
# Clear the scene of moving targets, then:
sensors.radar_config(0.0) # re-capture the baseline, ~160 ms
time.sleep_ms(300)
print(sensors.radar_range())
What you should observe
Both calls return None. A higher threshold makes target go False for weak reflections — a wall stops being a target before a person does. After a baseline recapture with the scene clear, static clutter drops out and a person walking in registers cleanly. Recapture with someone standing in front of the board and you have taught the sensor that they are furniture; the fix is to recapture again with the scene empty.

radar_config(0.05) or radar_config(61) raises ValueError. Zero is the one value outside 0.1..60.0 that is legal, and it means something else entirely.

Step 4 — When the numbers freeze (both variants)

This is the diagnostic step, and it is C on both variants — there is no Python binding for the two stats functions.

Example (authored — no shipped call site)
#include "radar_task.h"
uint32_t tries = 0, fails = 0; int32_t last_rc = 0;
uint32_t loops = 0, frames = 0, phase = 0;
tesaiot_radar_recover_stats(&tries, &fails, &last_rc);
tesaiot_radar_loop_stats(&loops, &frames, &phase);
What you should observe
Both are three volatile word reads with no lock and are safe from another task; any argument may be NULL. Read them twice a second apart and compare — the header spells out the whole decision table (radar_task.h:98-129):
Reading Meaning
tries climbing, fails 0 the frame sequencer stalls and the watchdog is healing it once a second — real, and self-correcting
tries and fails climbing together the SPI link is down; restarting the sequencer cannot work and never will
tries flat while frames frozen the radar task itself is not running
loops frozen with phase on an SPI value stuck in an unbounded spin inside the vendor platform layer, waiting on a flag only the SCB interrupt clears; nothing downstream can recover it
loops climbing, frames frozen the loop is fine and the sensor has stopped delivering

"Energy freezes after a minute or two" is the UI symptom of the first three rows, and these two calls are what separate them. Guessing between them from the screen is not possible.

Traps

Warning
distance_m == 0.0 is "no target", not "zero metres". Test target instead.
radar_config(0.0) is not "threshold zero". It is a baseline recapture, and it takes about 160 ms. Clear the scene first.
A frozen seq with successful calls means a stale snapshot. The IPC round trip succeeding proves the link, not the sensor.
radar_range() and radar_config() share one shared-memory buffer pair (modsensors.c:364-367). They are called from the MicroPython task only. Do not add a third caller on another task.
Do not add a second reader to radar_ai_frame_next(). It is lock-free on the strength of being single-reader (radar_task.h:139-142); a second cursor corrupts the first.
The three radar calls exist only when BSP_HAS_RADAR=1. They are inside #if BSP_HAS_RADAR in the registration table (modsensors.c:1359-1363); on a board without it, sensors.radar is an AttributeError, not an OSError.

Variant box

mtb-mpy mtb-only
Presence / energy sensors.radar() over IPC read tesaiot_radar_presence_detected and tesaiot_radar_current_energy directly on CM55
Range sensors.radar_range() radar_dsp_snapshot() (radar_dsp.h:59)
Threshold sensors.radar_config(db) radar_dsp_set_threshold_x10() (radar_dsp.h:52)
Diagnostics none from Python tesaiot_radar_recover_stats(), tesaiot_radar_loop_stats() — Step 4
Edge AI feed not reachable from Python radar_ai_frame_next(), single reader
Observable the radar page, plus the dicts the radar page only