Skip to content

Development Guide

This guide covers everything you need to know to build, modify, and debug the Multiflexmeter 3.7.0 firmware.

  1. PlatformIO Core or IDE

  2. AVR Programmer Software

    • AVRDude (usually included with PlatformIO)
    • Drivers for your ISP programmer
  3. Git (for version control and LMIC submodule)

  4. Serial Terminal (for debugging)

    • screen, minicom (Linux/Mac)
    • PuTTY, TeraTerm (Windows)
    • OR VS Code Serial Monitor extension
  • AVR ISP programmer (USBasp, AVRISP mkII, or Arduino as ISP)
  • 6-pin ISP cable
  • FTDI USB-to-Serial adapter (for debugging)
  • Multiflexmeter 3.7.0 hardware
  1. Download 3.7.0 from the GitHub Releases page.
  2. Extract the archive to a working directory.

The project uses MCCI Arduino-LMIC as a Git submodule:

Terminal window
git submodule update --init

This will clone the LMIC library into lib/arduino-lmic/.

  1. Open VS Code
  2. File → Open Folder → Select Multiflexmeter-3.7.0
  3. PlatformIO will auto-detect the project
  4. Select environment: mfm_v3_m1284p
Terminal window
cd Multiflexmeter-3.7.0
pio project init
Multiflexmeter-3.7.0/
├── platformio.ini # Build configuration
├── boards/ # Custom board definitions
│ └── mfm_v3_m1284p.json
├── include/ # Header files
│ ├── board.h # Board abstraction
│ ├── config.h # Compile-time configuration
│ ├── main.h # Main application header
│ ├── rom_conf.h # EEPROM configuration
│ ├── sensors.h # Sensor interface
│ ├── smbus.h # SMBus/I²C driver
│ ├── wdt.h # Watchdog timer
│ ├── errors.h # Error definitions
│ ├── debug.h # Debug macros
│ └── board_config/ # Board-specific configs
│ └── mfm_v3_m1284p.h
├── src/ # Source files
│ ├── main.cpp # Main application logic
│ ├── rom_conf.cpp # EEPROM management
│ ├── sensors.cpp # Sensor drivers
│ ├── smbus.cpp # SMBus implementation
│ ├── wdt.cpp # Watchdog implementation
│ └── boards/ # Board-specific implementations
│ └── mfm_v3_m1284p.cpp
└── lib/ # Libraries
├── arduino-lmic/ # LoRaWAN stack (submodule)
└── MedianFilter/ # Median filtering library

The platformio.ini file defines the build environment:

[platformio]
boards_dir = boards
[env]
platform = atmelavr
framework = arduino
build_flags =
-DARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS
-DDISABLE_BEACONS
-DDISABLE_PING
-DLMIC_PRINTF_TO=Serial
-DLMIC_DEBUG_LEVEL=2
-DFW_VERSION_MAJOR=0
-DFW_VERSION_MINOR=0
-DFW_VERSION_PATCH=0
-DFW_VERSION_PROTO=1
[env:mfm_v3_m1284p]
board = mfm_v3_m1284p
board_fuses.lfuse = 0xFF
board_fuses.hfuse = 0xD1
board_fuses.efuse = 0xFF
Terminal window
pio run -e mfm_v3_m1284p
Terminal window
pio run -e mfm_v3_m1284p -t clean
pio run -e mfm_v3_m1284p
Terminal window
pio run -e mfm_v3_m1284p -t upload
Terminal window
pio run -e mfm_v3_m1284p -t fuses

Successful build will generate:

.pio/build/mfm_v3_m1284p/
├── firmware.elf # ELF file with debug symbols
├── firmware.hex # Flash image
└── firmware.eep # EEPROM image (if any)

Enable debug output in include/config.h:

// Whether to output debug information to the serial output
#define DEBUG
// Whether to print the firmware build date/time on power on
#define PRINT_BUILD_DATE_TIME

When enabled:

  • Debug messages printed to UART at 115200 baud
  • Build timestamp printed on startup
  • Detailed LoRaWAN event logging

Minimum Data Rate (SF) in include/config.h:

// The lowest DR (thus highest SF) the device will join and TX at
#define MIN_LORA_DR 0 // DR0 = SF12 (slowest, longest range)

Adjust timing in src/main.cpp:

#define MEASUREMENT_SEND_DELAY_AFTER_PERFORM_S 10 // Sensor measurement delay
#define MEASUREMENT_DELAY_AFTER_PING_S 45 // Delay after version ping

Hard limits in include/config.h:

#define MIN_INTERVAL 20 // Minimum 20 seconds
#define MAX_INTERVAL 4270 // Maximum ~71 minutes

Versions are set as build flags in platformio.ini:

build_flags =
-DFW_VERSION_MAJOR=0
-DFW_VERSION_MINOR=0
-DFW_VERSION_PATCH=0
-DFW_VERSION_PROTO=1 # 0=dev, 1=release

Version format (16-bit):

Bit 15: Proto (0=dev, 1=release)
Bits 14-10: Major version (0-31)
Bits 9-5: Minor version (0-31)
Bits 4-0: Patch version (0-31)

Example: Version 1.3.7 (release) = 0b1_00001_00011_00111 = 0x8467

Configure your programmer in platformio.ini:

upload_protocol = usbasp # or atmelice_isp, arduino, etc.
upload_port = usb
upload_flags =
-e # Erase chip
-B0.25 # Set SCK period

Then upload:

Terminal window
pio run -e mfm_v3_m1284p -t upload
Terminal window
avrdude -c usbasp -p m1284p \
-U flash:w:.pio/build/mfm_v3_m1284p/firmware.hex:i

If you prefer traditional Makefile workflow:

Terminal window
make
make flash

Create eeprom_config.bin with your credentials:

import struct
MAGIC = b"MFM\x00"
HW_VERSION = (0, 3) # MSB, LSB
APP_EUI = bytes.fromhex("7766554433221100") # Little-endian
DEV_EUI = bytes.fromhex("0011223344556677") # Little-endian
APP_KEY = bytes.fromhex("0123456789ABCDEF0123456789ABCDEF") # Big-endian
INTERVAL = 900 # 15 minutes in seconds
TTN_FAIR_USE = 1
eeprom_data = struct.pack(
"<4s2B8s8s16sHB",
MAGIC, HW_VERSION[0], HW_VERSION[1],
APP_EUI, DEV_EUI, APP_KEY,
INTERVAL, TTN_FAIR_USE
)
with open("eeprom_config.bin", "wb") as f:
f.write(eeprom_data)
Terminal window
avrdude -c usbasp -p m1284p -U eeprom:w:eeprom_config.bin:r
Terminal window
avrdude -c usbasp -p m1284p -U eeprom:r:eeprom_read.bin:r
hexdump -C eeprom_read.bin | head -n 3

Expected output:

00000000 4d 46 4d 00 00 03 00 11 22 33 44 55 66 77 77 66 |MFM..... "3DUfwwf|
00000010 55 44 33 22 11 00 01 23 45 67 89 ab cd ef 01 23 |UD3"...#Eg.....#|
00000020 45 67 89 ab cd ef 84 03 01 |Eg.......|
  1. Connect FTDI Adapter

    • TX (FTDI) → RX (MFM)
    • RX (FTDI) → TX (MFM)
    • GND → GND
    • Do NOT connect VCC (device should be powered separately)
  2. Open Serial Monitor

    Terminal window
    screen /dev/ttyUSB0 115200
    # or
    pio device monitor -b 115200
  3. Debug Output Example

    Build at: Oct 09 2025 14:30:00
    [00000000] EV_JOINING
    [00005234] EV_JOINED
    [00005250] job_pingVersion
    [00010500] EV_TXCOMPLETE
    [00055234] job_performMeasurements
    [00065234] job_fetchAndSend
    [00070123] EV_TXCOMPLETE
    Measurement scheduled: 900000

Use debug macros in your code:

#include "debug.h"
void myFunction() {
_debugTime(); // Print timestamp
_debug(F("Hello World\n")); // Print string
_debug(variableValue); // Print variable
_debug(F("\n"));
_debugFlush(); // Flush serial buffer
}

If device resets unexpectedly:

  1. Check if watchdog is triggering
  2. Add watchdog resets in long loops:
    #include "wdt.h"
    while (condition) {
    wdt_reset(); // Reset watchdog
    // ... long operation ...
    }

See the firmware architecture documentation for sensor integration details.

Quick overview:

  1. Define sensor commands in include/sensors.h
  2. Implement I²C communication in src/sensors.cpp
  3. Update measurement reading logic in src/main.cpp

Runtime (via downlink):

Downlink: 0x10 <MSB> <LSB>
Example: 0x10 07 08 (1800 seconds = 30 minutes)

Compile-time (default): Modify EEPROM configuration before flashing.

  1. Credentials: Update EEPROM
  2. ADR Mode: Modify src/main.cpp:
    LMIC_setAdrMode(1); // 1=enabled, 0=disabled
  3. Data Rate: Set minimum DR in include/config.h

Add new commands in src/main.cpp:

#define DL_CMD_MY_CUSTOM 0x12
void processDownlink(uint8_t cmd, uint8_t *args, uint8_t len) {
switch (cmd) {
case DL_CMD_MY_CUSTOM:
// Handle custom command
_debug(F("Custom command received\n"));
break;
// ... existing cases ...
}
}

PlatformIO supports unit testing:

Terminal window
pio test

Create tests in test/ directory.

  1. Flash firmware with DEBUG enabled
  2. Monitor serial output
  3. Verify join procedure
  4. Send downlink commands
  5. Check measurement transmission
  1. Deploy with DEBUG disabled
  2. Monitor via TTN console
  3. Check battery voltage over time
  4. Verify measurement intervals
  5. Test range and coverage
  • Disable DEBUG in production
  • Use F() macro for strings (store in flash):
    _debug(F("String in flash\n")); // Good
    _debug("String in RAM\n"); // Wastes RAM
  • Enable Link Time Optimization (LTO) in platformio.ini:
    build_flags = -flto
  • Use const and PROGMEM for constants
  • Minimize global variables
  • Use stack allocation when possible
  • Reduce measurement frequency
  • Use sleep modes (already implemented)
  • Disable unnecessary peripherals
  • Optimize sensor reading time
Terminal window
fatal error: lmic.h: No such file or directory

Solution:

Terminal window
git submodule update --init
Terminal window
avrdude: verification error

Solution:

  • Check programmer connection
  • Try slower SCK speed: -B10
  • Verify programmer works with simple blink sketch
Terminal window
avrdude: error: programm enable: target doesn't answer

Solutions:

  • Check ISP cable orientation
  • Ensure device is powered
  • Try different programmer
  • Check fuses aren’t set incorrectly
Terminal window
region `text' overflowed by XXX bytes

Solutions:

  • Disable DEBUG
  • Remove unused code
  • Enable LTO
  • Optimize data structures