MCU Serial Protocol#
Todo
This documentation was drafted from code analysis and needs verification against the MCU firmware.
Overview#
The host computer communicates with a microcontroller (MCU) over a serial link to program an ICE40 FPGA and retrieve fitness-evaluation data. The MCU acts as a bridge: the host sends a command byte indicating the type of measurement it wants, and the MCU drives the FPGA and returns either a stream of ADC waveform samples or a pulse count.
This protocol is implemented in Microcontroller
(src/Hardware/Microcontroller.py) and conforms to the
Hardware protocol defined in
BitstreamEvolutionProtocols.py.
Configuration#
Connection parameters are bundled in the
MicrocontrollerConfig dataclass:
Field |
Type |
Description |
|---|---|---|
|
|
Device path to the serial port (e.g. |
|
|
Baud rate for the serial connection. |
|
|
Timeout in seconds for serial read operations. |
Connection Setup#
On construction, the Microcontroller class opens a persistent
serial.Serial connection using the values from
MicrocontrollerConfig:
self.__serial = Serial(
config.usb_path,
config.serial_baud,
timeout=config.read_timeout,
)
self.__serial.dtr = False
Setting DTR low is critical – it prevents the MCU from resetting when the serial port is opened, which is the default behaviour on many Arduino-compatible boards.
DataRequest Types#
The type of measurement requested is encoded as a
DataRequest enum (defined in
BitstreamEvolutionProtocols.py):
DataRequest.WAVEFORMRequests an ADC waveform capture. Dispatches to
measure_signal().DataRequest.OSCILLATIONSRequests one or more pulse-count readings. Dispatches to
measure_pulses(), which callsmeasure_pulses_once()repeatedly for the number of samples specified in theMeasurementobject.
Before either measurement type, the circuit is compiled for the FPGA via
ckt.compile(fpga_data) using the IceStorm toolchain (icepack /
iceprog).
Waveform Measurement Protocol#
Serial command: b'2'
Todo
Verify waveform protocol against the MCU firmware. The exact sample count, sample rate, and ADC resolution should be confirmed with the firmware source.
Sequence:
The host flushes both input and output serial buffers.
The host sends
b'2'to initiate ADC capture.The host polls
read_until()for theSTART\ndelimiter, re-sendingb'2'on each iteration until it arrives or the read timeout is exceeded.After
START\n, the host reads lines one at a time. Each line contains a single integer ADC sample (samples are approximately 10 microseconds apart). Approximately 500 samples are expected.The stream ends when the host receives
FINISHED\n.If
read_timeoutis exceeded at any stage, reading is aborted and whatever samples have been collected so far are returned.
Host --> MCU: b'2'
MCU --> Host: START\n
MCU --> Host: <sample_0>\n
MCU --> Host: <sample_1>\n
...
MCU --> Host: <sample_N>\n
MCU --> Host: FINISHED\n
Return value: list[int] – the parsed integer ADC samples, excluding
the START and FINISHED sentinel lines.
Pulse Count Measurement Protocol#
Serial command: b'1'
Todo
Verify pulse count protocol against the MCU firmware. The pulse-counting window duration and any firmware-side filtering should be confirmed with the firmware source.
Sequence:
The host flushes both input and output serial buffers.
The host sends
b'1'to initiate pulse counting.The host calls
read_until()in a loop, looking for a line that:Is not empty (
b""),Does not contain
:,START,FINISH, or a space.
This filtering is intended to skip MCU debug or status lines and isolate the numeric pulse count.
When a valid line is found, carriage-return and newline bytes are stripped and the result is parsed as an integer.
Retry logic: If the read timeout is exceeded, the host retries up to 5 attempts before giving up.
Sentinel / error values:
Value |
Meaning |
|---|---|
|
Read timed out after exhausting all retry attempts. |
|
The buffer was empty after the read loop completed (should never occur under normal conditions). |
|
The received bytes could not be parsed as an integer
( |
Multi-sample collection: measure_pulses(samples) calls
measure_pulses_once() the requested number of times and concatenates the
results into a single list[int].
Host --> MCU: b'1'
MCU --> Host: <pulse_count>\n
Error Handling#
The request_measurement() method wraps each measurement call in a
try / except block. Results are stored on the Measurement object
using the returns library:
Success path:
measurement.result = Success(data)Failure path:
measurement.result = Failure(exception)
Before any measurement is taken, the Measurement object is initialized with
Failure(MeasurementNotTaken(...)) so that unconsumed measurements are never
mistaken for successful reads.
Known Issues / TODOs#
The following issues are noted directly in the source code:
Todo
Optimize serial read/write in measure_signal — the waveform reading loop
could read all available bytes at once rather than line-by-line.
Todo
Improve regex/filtering in measure_pulses_once — the current chain of
byte-string containment checks should be replaced with a proper parser that
handles transmission-loss artefacts (dropped or extra spaces, shifted colons).
Todo
Add elif branches in request_measurement() to handle future
DataRequest types beyond WAVEFORM and OSCILLATIONS.