Skip to content

Bring Micras v1 up in a real maze, from the first search to a fast run with the fan - #60

Open
GabrielCosme wants to merge 67 commits into
refactor/restructurefrom
feature/bringup
Open

GabrielCosme wants to merge 67 commits into
refactor/restructurefrom
feature/bringup

Conversation

@GabrielCosme

@GabrielCosme GabrielCosme commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Micras v1 solving a real 4x4 maze: the robot is measured, calibrated and tuned on the maze it runs in, and the simulator models the same robot and maze.

On the robot

The maze is the home maze's boards rearranged into sim/mazes/race4x4.txt (start (3,0) facing north, goal (2,3)):

Run Time Simulated
Search from a blank map, to the goal 7.8 s 7.6 s
Return to the start 5.9 s 5.6 s
Fast run with the fan 1.70 s 1.71 s
Fast run with the fan and the racing line 1.40 s 1.40 s

Both fast runs held their path within 11 mm sideways.

Talking to the robot

  • Link from a terminal. tools/micras_link.py talks to the robot over Bluetooth. It reads, writes and streams variables, and sends commands: explore, solve, stop, resume, save, and the calibration procedures. A stream survives a dropped link and writes each sample as it arrives.
  • Faults and resets. A crash is 25 m/s^2 for 5 ms, and the fault is streamed. The robot publishes why and where it last reset; a hard fault, or an aborted save, leaves its status in the trace. A stopped robot waits 30 s before the watchdog.
  • Calibrations survive a reboot. The configuration in git comes first.

Measured and calibrated

  • Drive.
    • Each wheel is driven from the bridge wired to it, and the encoders count up when the wheels roll forward.
    • The drive model takes the torque constant and the breakaway that a polarity sweep measured.
    • The track is 47.2 mm, from turns in place against the gyroscope.
  • Robot model. The robot model comes from the hardware as built: wheels, outline, mass, skids, and the fan's 150 g of downforce with its skirt. The tires hold 0.54, measured by tilting the robot on the maze floor.
  • Wall sensors.
    • Each sensor is read with its own emitter alone, at 1 kHz.
    • The crosstalk is measured from the link.
    • The references are fitted to the robot in its black PLA caps, including the robot pulled back from a wall.
    • The home maze's semi-gloss walls are modeled with the Minnaert exponent of 1.83 they measure.
    • The references belong to the walls they were calibrated on. Each maze configuration scales them by what its walls send back against those, so the classic mazes' matte walls, which send the diagonal sensors 1.7 times as much light at 45 degrees, read as the robot's calibration expects.
  • Timing. The loop runs at 10 kHz, and the inertial measurement unit is read at 7.8 MHz.

Tuned

  • Search. It runs at 0.3 m/s with a third of the traction.
  • Fast runs.
    • The turns keep 20 mm from the walls.
    • A fast run asks for 0.6 of the traction without the fan, up to 1 m/s.
    • With the fan running at half of the battery, a run asks for 0.5, which keeps 16 mm from the walls in the race maze even if the fan makes only 1 N on tires that hold 0.42.
  • Simulator grip. The simulated tires hold 0.46, which is how the fast runs on the robot slip.

Build

  • Maze configuration. The firmware is built for the maze it runs in (MICRAS_MAZE), and the test programs get that maze's configuration too.
  • Turn designs. The turns of two bends are designed whenever the firmware is built.

Before merging

Merge the stack below first: this is stacked on #59, and it goes with Team-Micras/micras-lib#3 and Team-Micras/micras-simulation#2. The simulation baseline is recorded with these changes: the checked exploration reaches its goal 0.16 s sooner, with the same pose error.

Both encoder timers counted down for a wheel rolling forward. Capturing input 1 on its falling edge
reverses the direction the quadrature decoder counts in, without touching the sensors' DIR bit.
The designs depend on the robot model, the wall thickness and the margins, so a committed header went
stale with any of them and failed the build's clearance check. The designer now runs as part of the
build, into a generated header: the simulation builds it with its own targets, and the robot build
through a host project of its own, since the designer is a host program.
Each test now starts from a probe a variable monitor can write as well as from the button, and
publishes the faults of the fan, the diagnostics and counts per turn of the encoders, and the mean,
minimum and maximum current of each step of a sweep of wheel commands.
Each emitter is lit alone in turn, then all of them and none, and the mean intensity and dark
reading of every receiver is kept per mode, which shows the light each sensor takes from the
others, the ambient floor and which receiver answers which emitter.
The link could start a run but not stop one, and the error state could only be left by switching
the robot off. STOP now ends any procedure that moves in the error state with the STOPPED fault, and
any wait back in idle; RESUME leaves the error state unless the initialization failed. MAINTAIN starts
the procedure its argument names, whatever the switches say, and two procedures exist only for it: a
check of the sensors, which keeps them on with the robot still until STOP, and a check of the
polarity, which drives each wheel each way at two commands in turn.

What these checks are read with is published too: the state, a bit per failed check of the
initialization, the wheel angles, the motor command applied, the raw intensity of each wall sensor
and whether its reading is valid or blind.
A client of the link's protocol in plain Python, over the native Bluetooth bridge or the simulator's
monitor bridge: it lists the schema, reads, writes and sends commands, and streams a group to a CSV
with the robot's sequence numbers and timestamps, so a lost sample shows.
…rrives

A radio link that drops for a moment lost every sample of the stream, since the file was only
written at its end. Each sample now goes to the file as it arrives, and a dropped link is opened
again and its group defined again until the stream's time runs out; a clock that went back shows
that the robot restarted in between.
The size, start, goal, cell size and wall thickness of the maze were written into the constants and
the robot model, so another maze meant editing what the simulation and its baselines run too. Each
maze is now a directory of config/mazes with its maze_config.hpp, which MICRAS_MAZE puts on the
include path the way the board directory is, and the classic one stays the default. A goal of any
number of cells is accepted, each of them checked to be inside the maze.
Start at (3, 0) open to the north, a single goal cell at (2, 3) and walls 15.1 mm thick, built with
MICRAS_MAZE=home. A drawing of a maze with that start and goal and a scenario that explores and
solves it with the fan off rehearse it in the simulation.
The robot runs without its fan for now and weighs 78 g on a scale, and its tires measure 22.5 mm
across, 45 mm apart. The firmware's model and the simulation's robot take the same values, the
simulation's center of mass and inertia less the fan, so the two still agree. The search of the
classic maze reaches the goal 0.9 s later than with the fan's mass, and the baseline is recorded
again.
…onnection

The sequence restarts with the group, and a sample that arrived while the group was being defined
again was compared with the last one of the dropped link, which counted a wrap of the sequence as
sixty five thousand lost samples.
The receivers read light with no wall in sight, so how much each emitter puts into each receiver has
to be measured on the robot. MAINTAIN 5 lights every emitter off, then each one alone, then all, a
second each, and publishes the mode lit, so a stream of the intensities splits into the matrix of
emitter to receiver light.
…rity check run through it

The inertial measurement unit went silent for 5 ms as soon as the motors were driven on a stand,
which stopped the check of the polarity before a wheel turned. The counts of how its transfers end
and the longest run of iterations without a new sample since a procedure started are published,
and the polarity check, which runs on a stand, now stops only on STOP, so it shows what the sensors
do while the motors run. The pool holds up to 128 variables.
….8 MHz

The loop read the sensor once per 125 us, at the rate the sensor samples on its own clock, so the two
drifted in and out of phase over tens of milliseconds. The sensor holds its output registers while a
burst is read, a sample that falls inside one is lost, and the angular rate stopped for up to 122 ms
at a time on the robot, which the fault check took for a lost sensor. At 10 kHz the reads beat
against the 8 kHz samples at 2 kHz, and SPI3 at 7.8 MHz holds the registers for 18 us instead of 35:
the longest run without a new sample is now two iterations. The simulation follows the period with
its timestep and the bus with its prescaler.

The estimate of the pose now starts in the start cell, where the robot is placed, rather than at the
corner of the maze, which the first iteration of a run could still show before the run reset it.
On the v1 board the bridge on TIM1 drives the left wheel and the one on TIM3 the right, and the
left motor turns its wheel backward when its first output is driven: the check of the polarity
moved the right encoder for a left command and the left encoder backward for a right one. The
locomotion now follows the wiring, and the simulation measures the current of each bridge the
way the board does, sensor 0 on the right wheel.
…it first

Every calibration was lost at the next boot unless it was typed into the configuration and
flashed. The references of the wall sensors, their offsets, measured by the new MAINTAIN 6 with the
robot held up in open air, and the scale of the gyroscope are now saved to the flash with the maze
when their procedure ends, and used at boot. Each keeps the value of the configuration it replaced,
and is used only while the configuration still holds it: a value written into the configuration
and flashed always wins, so the repository stays what the robot runs on. A result whose spread
shows the robot moved is not kept. The link tool reads the record and writes it into target.hpp
and robot.hpp, to commit.

The check of the polarity now sweeps each wheel each way from 10 % to 50 %, which shows where it
breaks away and how its speed grows with the command.
…asured

On the stand the free left wheel gains 38.7 rad/s per volt, which bounds its torque constant at
5.0 mN m/A against the 6.27 the datasheet's no-load speed gave, and it breaks away at 13 % of the
supply in both directions, the friction of the drive together with the part of each pulse the
bridge loses at 100 kHz. Both go into the robot model, and the torque constant into the simulation's
drive too. The simulated bridge loses nothing, so its breakaway stays at the motor's friction, and
the feed-forward overdrives it at low speed: the search of the classic maze still runs clean, and
the baseline is recorded again.
With a wall ahead, the 45 degree emitter of each side put 14 to 34 % of a front receiver's light
into it, and as much as the receiver's own with walls at the sides, which the dark scan of the
paired emitters subtracted. TIM4 now relights its emitters one at a time from a table its update
DMA request loads on DMA2 stream 0, with no interrupt: an emitter is lit for 75 us on each side of
its end of a 200 us half of the count, and a cycle of six ends, one per emitter and two dark, gives
a reading every 1.2 ms, 833 Hz. A half of 200 us is two periods of the control loop, which the
simulation steps at. The check of the crosstalk publishes the light of each emitter in every
receiver from the same cycle, and the simulation schedules its emitters from the firmware's table.
The search of the classic maze runs clean, and the baseline is recorded again.
The emitters' table now repeats every two frames of one end per emitter and one dark, and each
frame is a reading, every millisecond. The simulation times its scans by the emitter timer's half
period instead of by whole ticks of the loop, and reads the firmware's buffer a frame at a time;
the baseline is recorded again.
…tion

Each emitter's current disturbs the supply its receiver shares, which reads the receiver's own lit
scan a little below its dark one with no light reaching it: an offset is measured down to -0.05 of
the full scale, used at boot in the same range, and kept when its standard deviation is within the
noise floor, which the spread, a ratio to a mean near zero, could not tell.
…t 30 s

The watchdog reset the robot in the middle of saving to the flash, which normally takes 0.9 s.
The boot now publishes the reset flags and the last mark the program left, which the save sets at
each of its steps, and the watchdog window of a stopped robot is close to the 32 s the watchdog
allows, so that a slow erase and a hang can be told apart. Offsets are kept when the deviation of
their readings is within 0.005 of the full scale, which a flickering lamp stays within.
…race

The next boot publishes where the program was when it reset; a hard fault or an abort now says so
there instead of keeping the last mark of whatever the program was doing.
…ake effect

Saving to the flash reset the robot one save in a few: the window the save asked the watchdog for
did not take effect before the refresh, and expired during the erase. Twenty saves in a row and the
calibrations that save now pass.
…lator its gains

The calibration in a corridor of the home maze, with the sensors fired one at a time and no caps,
gives 0.4519 and 0.4157 for the diagonal sensors. The front sensors reach their ceiling facing a
wall from the center of a cell, so their references stay the placeholders. The simulator's
diagonal gains are calibrated again to read those values at the same pose, and its receivers stop
at the 3.08 V the front ones stop at on the robot. The calibration tool steps at the loop period of
the firmware.
… the full scale

On the robot the front receivers stop at 93 to 94 % of the full scale and the diagonal ones at 88
to 90 %, so a reading at 85 % or more, its dark part included, is taken as saturated. The wall
flags publish it in the four bits after the blind ones. In the simulator, whose diagonal sensors
now saturate as the robot's do, the exploration keeps the pose within 7 mm, against 17 mm when
saturated readings were taken as ranges.
Without the caps the front receivers reach their ceiling up to about 110 mm from a wall, so their
references are fitted to the readings from 110 to 200 mm, along the model of the receiver, and
are what they would read at the center of a cell with no ceiling: 4.390 and 3.616. The
simulator's front gains read the same.
A saturated receiver reads the same at any nearer distance, so a reference measured at its
ceiling would place every wall too far.
…'s perpendicular

Turning the robot by hand in a cell, its sensors read within a few millimeters up to 55 deg of
incidence and 20 to 60 mm short at 60 to 80 deg. On the fast run's diagonals such readings moved the
pose by 45 mm at the same place on every run, whatever the speed, and the robot steered into a wall.
… downforce it makes without the skirt

The fan study puts 14 N and a winding past 140 deg C at full speed. Without the skirt, half of the battery makes 0.4 to 0.95 N by its model; runs are planned on 0.5 N, and with the fan ask for 0.65 of the traction, 0.7 with boost. The simulated tires hold 0.46, which is how the fast runs on the robot slip.
The skirt seals the board's underside, which centers the suction 7.8 mm ahead of the axle. With the fan running, a run asks for 0.6 of the traction and a boosted one for 0.65, which keep 5 and 9 mm from the walls in the home maze with the fan making only 1 N.
In the race maze 0.6 hits a wall when the fan makes 1 N on tires that hold 0.42, and slid the robot into one on its first run with the fan. 0.5 keeps 16 mm in that case.
…ainst the walls they were calibrated on

The references are the robot's, calibrated against the home maze's semi-gloss boards. The matte walls of the classic mazes send the diagonal sensors 1.7 times as much light at 45 degrees, so without the scale the search in them corrects its pose against readings that put every wall too close.
…e's, and drop a battery check left commented out
The robot model, the wall sensors in their caps, the search at 0.3 m/s and the references scaled to the classic mazes' matte walls reach the goal of the checked exploration 0.16 s sooner, with the same pose error.
… the path and the sighting range of the wall votes
…a half turn, kept in RAM for the link to read page by page, and take references written from the link
…there at boot, each board's wall references scaled for its own sensors
…h running turbo when no fan is mounted, and let the crash threshold grow with the fan's downforce
…de at 47 degrees, and let only the diagonals' near walls vote while the search turns slowly
…eading the gyroscope followed, since the fan's torque turned the robot off the start's heading
…g line planned on all of it hit the first post of the contest maze
… and 2.5 m/s, since 0.45 lost the line in the contest maze's quick turns
…and fan ones for less speed, for the contest's official runs

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant