A MultiFlexMeter (MFM) sensor module. The hardware is a generic carrier: RS-485 + 1-Wire, with a software-selectable sensor supply rail (3.6 V / 5 V / 12 V). The firmware (RIOT-OS, STM32L010) is likewise mostly generic — it powers the rail, talks to the host over I2C, and runs a measurement FSM. The current application reads a LinkBlue dissolved-oxygen (DO) sensor over Modbus RTU.
The module is a slave to the MFM host: the host powers it, triggers a measurement over I2C, then reads back the result.
| Feature | Notes |
|---|---|
| Sensor bus | RS-485 (Modbus RTU) and 1-Wire |
| Sensor supply | Boost rail, selectable 3.6 V / 5 V / 12 V (FB divider) |
| Host link | I2C slave (MFM register protocol) |
| MCU | STM32L010 (mfm-module board) |
The rail voltage is a compile-time choice (boost_voltage in main.c),
currently 12 V for the DO sensor. KiCad sources live in hardware/; fab
outputs are generated separately and shipped as release assets.
Prerequisites: the arm-none-eabi GCC toolchain, plus the RIOT submodule and
an out-of-tree I2C-slave patch.
git submodule update --init --recursive
git apply --directory=RIOT 0001-i2c-slave.patch # I2C slave mode, not yet upstream
make # build
make flash # build + flash via SWDFW_VERSION is stamped from git describe --tags, so build from a tagged
commit for a meaningful version string.
Pass as make VAR=1. Defaults are production values.
| Flag | Default | Effect |
|---|---|---|
FAT |
0 |
1 = factory-acceptance loop: power rails, init Modbus + DO, read forever. Bench bring-up only. |
LOW_POWER |
1 |
0 = block all PM modes so the CPU busy-waits in idle, keeping the SWD link and breakpoints reliable for debugging. |
DEBUG_LED |
0 |
1 = blink LED1 with the FSM state and LED2 with the last error code. |
The host drives the module through the MFM I2C register protocol — firmware version, init, measurement start/status/data, and error. Register map and semantics: see the MultiFlexMeter host protocol spec.
A measurement cycle: host writes MEAS_START → firmware powers the rail,
validates VSensor, brings up Modbus, reads the DO sensor, publishes the payload
→ host reads MEAS_DATA. On a measurement failure the firmware returns to
IDLE and exposes an app error code (also blinked on LED2) in the error
register for the host to read. An invalid FSM state instead halts in ERROR;
the host recovers by power-cycling.
stateDiagram-v2
[*] --> SYSTEM_INIT: power on
SYSTEM_INIT --> IDLE: GPIO, I2C, ADC up
IDLE --> MFR_INIT: host: manufacturer init
MFR_INIT --> IDLE
IDLE --> MEASURING: host: measure
MEASURING --> IDLE: done or failure
ERROR --> ERROR: halt on invalid state (host power-cycles)
state MEASURING {
[*] --> SENSORS_INIT
SENSORS_INIT --> DO_CONF: rail OK, sensor probed
DO_CONF --> DO_READ: no-op (no DO config)
DO_READ --> MEASURE_FINISH
MEASURE_FINISH --> [*]
}
FAT=1 replaces this flow with a standalone read loop (APP_STATE_FAT) that
doesn't wait for an I2C command.
Measured at the MultiFlexMeter main board's supply, so the figures include the main board's own consumption, not just this module. Captured on a dev build (less power-efficient than production) with LoRa at SF7.
A measurement cycle's active phase lasts about 30 s and draws roughly 360 mC (11.7 mA average, 247 mA peak).
MEAS_DATA returns an 8-byte, packed, little-endian struct. Fields are copied
verbatim from the sensor's Modbus input registers — the firmware does no scaling
or validation, so each value is a raw fixed-point integer the consumer must
scale (and range-check) itself.
| Offset | Field | Type | Range | Scale | Unit |
|---|---|---|---|---|---|
| 0 | DO value | uint16 |
0–2000 | ×0.01 | mg/L |
| 2 | DO saturation | uint16 |
0–200 | ×0.1 | % |
| 4 | Electrode signal | uint16 |
0–1000 | raw | — |
| 6 | Temperature | int16 |
-100–1300 | ×0.1 | °C |
Apply the scale to recover the physical value — e.g. a DO field of 742 is
7.42 mg/L.
