TU/e 5EID0 · Group 10 · 2026
Venus rover
Run-on-boot firmware for a rover that hunts colored cubes, maps terrain, and shares every find with its partner.
The task
For the 5EID0 “Venus Project” (Group 10, 2026) a PYNQ-Z2 rover has to hunt colored cubes, map terrain, and coordinate with a partner robot over one MQTT link — UART0 to an ESP32 to the broker. The firmware is C, runs from boot, and I also wrote the Python mission-control tool.
Two rovers start back to back at the origin of an arena bounded by black tape, with cubes of two sizes, 3 cm and 6 cm, and obstacles we called mountains. The job is to find all six cubes, report each with its position, size, color and the local temperature, and stop. No operator and no keyboard: the firmware starts with no arguments and talks only over MQTT, so the robot has to be sure of what it has seen before it acts on it.
The report’s non-functional requirements set the tempo: the distance array had to refresh the obstacle logic at ≥ 10 Hz, a cliff or boundary had to stop forward motion within 200 ms, and the map had to update within 1 s of a packet. The wireless link is an ESP32-C3-WROOM-02 connectivity board on UART, speaking MQTT to the broker; only wireless communication was allowed during the mission (Fig. 2).
Sensors and odometry
A vertical stack of three VL53L0X time-of-flight sensors sits on a shared I²C bus: the bottom beam for 3 cm cubes, the middle for 6 cm cubes, the top for obstacles — “mountains” (Fig. 3). Two TCRT5000 IR sensors watch for the black-tape boundary, a TCS3200 reads color, and an NTC thermistor reads temperature. Two stepper-driven wheels give differential drive, with dead-reckoned odometry from the commanded steps — no encoders: 1600 steps/rev, Ø75 mm wheels, 120 mm track.
Every VL53L0X ships at the same I²C address, 0x29, so the bus is sorted out at boot: the firmware holds the middle and bottom sensors in shutdown through their XSHUT pins, finds the always-on top sensor and moves it to 0x68, then wakes the other two one at a time and re-addresses them to 0x69 and 0x6A.
The XSHUT bring-up was verified with an I²C bus scanner after each boot phase: one device at 0x29, then 0x68 and 0x29, then 0x69 joins, ending at a stable 0x68 / 0x69 / 0x6A. Settling delays below 50 ms caused boot-timing collisions, which is why the firmware waits 100 ms. Disconnecting AR4 or AR5 to simulate a dead sensor made the driver log a non-fatal timeout and carry on without locking the bus.
The NTC divider — 10 kΩ, 3.3 V, β = 4050 K — first read wrong because the ADC’s input impedance loaded the divider; a unity-gain op-amp buffer between divider and ADC fixed it. The TCS3200 is read as a frequency: S0/S1 at 20 % scaling, calibrated against white and black, 20 static readings per cube, classified by Euclidean distance to five color profiles. It is accurate at the 15–20 mm working distance and degrades with range. Fig. 4 shows how the sensors, the control loop and the link fit together, with the interfaces as they are actually wired.
The autopilot
Four reactive states: SCAN, turning in 5° steps, 72 per revolution, looking for the nearest unknown cube on the beams; APPROACH, driving toward a remembered cube and steering around a mountain if one is closer; EVALUATE, reading the color, registering and reporting the cube, then backing off 5 cm; and ROAM, turning a random direction and driving 40 cm when a full sweep finds nothing. A boundary-evade interrupt is checked at the top of every tick and pre-empts all four (Fig. 1).
The approach switches to an open-loop final drive inside 10 cm, because the time-of-flight sensors cannot read below about 6 cm, and gives up after 30 ticks rather than pushing forever. When the IR sensors see tape, the evade reverses, turns about 180° with ±30° of jitter, drops any approach in progress, and re-scans.
What survives a dropout
Time-of-flight reads drop out, so a cube is confirmed by a 6-read burst vote: at least two reads within 5 cm. Color is averaged over 20 reads, and a cube is registered on detection strength, so a weak color read never discards a real find. Finds are deduplicated within 12 cm. The thresholds were tuned against on-bench ToF readings, and the README says so, so the next person can re-measure rather than guess.
Both robots share one world frame — same origin, back-to-back headings — so a cube position from either robot is comparable with no transform at run time. Every new find goes out as a JSON frame over UART0 to the ESP32, which relays it to the broker; the partner drains its UART each tick and folds any find with a known color into its own registry. When the sixth cube is known, by either robot, both stop.
Mission control
The Python mission-control tool is an MQTT relay between the two robots, a JSON message logger, and a live matplotlib map of both robots’ paths, headings and every cube, drawn to scale — 3 cm against 6 cm.
Six revisions
The firmware went through six revisions. The third is the structural one: the sensor code, the ToF driver and the MQTT framing that had lived in separate files were consolidated into a single main.c of about 2,600 lines that links against only libpynq and libm, so deployment is one gcc command and a copy.