Firmware Architecture
Software architecture and code organization for the Multiflexmeter 3.7.0 firmware.
Architecture Overview
Section titled “Architecture Overview”The firmware follows an event-driven architecture using the Arduino-LMIC library’s job scheduler:
┌──────────────────────────────────────────┐│ Application Layer ││ (main.cpp - Event Handlers) │└────────┬─────────────────────────┬───────┘ │ │┌────────▼─────────┐ ┌──────────▼────────┐│ Sensor Layer │ │ LoRaWAN Layer ││ (sensors.cpp) │ │ (LMIC) │└────────┬─────────┘ └──────────┬────────┘ │ │┌────────▼─────────┐ ┌──────────▼────────┐│ SMBus Layer │ │ Radio Layer ││ (smbus.cpp) │ │ (RFM95) │└──────────────────┘ └───────────────────┘ │ │┌────────▼─────────────────────────▼────────┐│ Hardware Abstraction Layer ││ (board_config, board.cpp) │└───────────────────────────────────────────┘Core Components
Section titled “Core Components”1. Main Application (main.cpp)
Section titled “1. Main Application (main.cpp)”Event-driven main loop handling:
void setup() { board_setup(); // Initialize hardware sensors_init(); // Initialize sensor interface os_init(); // Initialize LMIC LMIC_reset(); // Reset LoRaWAN stack
if (!conf_load()) { // Load EEPROM config os_setCallback(&main_job, FUNC_ADDR(job_error)); return; }
LMIC_startJoining(); // Begin OTAA join}
void loop() { os_runloop_once(); // LMIC job scheduler (only this!)}Key Functions:
onEvent()- LMIC event handler for join/TX/RX eventsjob_performMeasurements()- Trigger sensor measurementjob_fetchAndSend()- Read sensor data and transmitjob_pingVersion()- Send version information after joinjob_reset()- Device reset handlerscheduleNextMeasurement()- Calculate and schedule next cycleprocessDownlink()- Handle received commandsgetTransmissionTime()- Enforce duty cycle compliance
Job Workflow:
- Join Phase:
EV_JOINED→job_pingVersion() - Version Phase: After 45s (
MEASUREMENT_DELAY_AFTER_PING_S) →job_performMeasurements() - Measurement Phase: After 10s (
MEASUREMENT_SEND_DELAY_AFTER_PERFORM_S) →job_fetchAndSend() - Transmission Phase: After TX →
scheduleNextMeasurement() - Repeat: Back to step 3 (with configurable interval)
2. Sensor Interface (sensors.cpp, smbus.cpp)
Section titled “2. Sensor Interface (sensors.cpp, smbus.cpp)”I2C Hardware Architecture
Section titled “I2C Hardware Architecture”The sensor communication is built on a master-slave I2C architecture:
ATmega1284P (I2C Master)│├── SDA Pin (PC1/Pin 17) ──┐├── SCL Pin (PC0/Pin 16) ──┤│ │└── I2C Bus ───────────────┘ │ └── 0x36: Sensor Board (I2C Slave) ├── JSN-SR04T (Ultrasonic) └── DS18B20 (Temperature)Hardware Abstraction Layer
Section titled “Hardware Abstraction Layer”SMBus Interface (smbus.cpp):
error_t smbus_init(void); // Initialize I2C peripheralerror_t smbus_sendByte(uint8_t addr, uint8_t cmd); // Send commanderror_t smbus_blockRead(uint8_t addr, uint8_t cmd, // Read data block uint8_t *buf, uint8_t *len);Application Interface
Section titled “Application Interface”Sensor API (sensors.cpp):
#define SENSOR_BOARD_ADDR 0x36#define CMD_PERFORM 0x10 // Trigger measurement#define CMD_READ 0x11 // Read results
error_t sensors_init(void);error_t sensors_performMeasurement(void);error_t sensors_readMeasurement(uint8_t *buf, uint8_t *length);I2C Transaction Flow
Section titled “I2C Transaction Flow”1. Trigger Measurement:
sensors_performMeasurement()└── smbus_sendByte(0x36, 0x10) └── I2C: [START][0x36][0x10][STOP]2. Read Measurement Data:
sensors_readMeasurement()└── smbus_blockRead(0x36, 0x11, buf, &len) └── I2C: [START][0x36][0x11][RESTART][0x36|READ][LENGTH][DATA...][STOP]Communication Protocol
Section titled “Communication Protocol”- Send
CMD_PERFORM(0x10) to sensor address 0x36 - Wait exactly 10 seconds (
MEASUREMENT_SEND_DELAY_AFTER_PERFORM_S) - Send
CMD_READ(0x11) to retrieve measurement data - Receive variable-length response (1-32 bytes)
- Transmit data via LoRaWAN on port 1
Legacy SMBus Functions
Section titled “Legacy SMBus Functions”For compatibility, additional SMBus functions are available:
error_t smbus_init(void); // Set SCL to 80kHzerror_t smbus_sendByte(uint8_t addr, uint8_t byte); // Send commanderror_t smbus_blockRead(uint8_t addr, uint8_t cmd, uint8_t *rx_buf, uint8_t *rx_length); // Read blockerror_t smbus_blockWrite(uint8_t addr, uint8_t cmd, uint8_t *tx_buf, uint8_t tx_length); // Write block3. Configuration Management (rom_conf.cpp, config.h)
Section titled “3. Configuration Management (rom_conf.cpp, config.h)”EEPROM Structure:
struct __attribute__((packed)) rom_conf_t { uint8_t MAGIC[4]; // "MFM\0" signature struct { uint8_t MSB; // Hardware version MSB uint8_t LSB; // Hardware version LSB } HW_VERSION; uint8_t APP_EUI[8]; // Application EUI uint8_t DEV_EUI[8]; // Device EUI uint8_t APP_KEY[16]; // Application Key (128-bit) uint16_t MEASUREMENT_INTERVAL; // Interval in seconds uint8_t USE_TTN_FAIR_USE_POLICY; // Fair use enforcement flag};// Total: 41 bytesConfiguration Functions:
bool conf_load(void); // Load from EEPROM, validate MAGICvoid conf_save(void); // Save to EEPROMvoid conf_getDevEui(uint8_t *buf); // Get Device EUIvoid conf_getAppEui(uint8_t *buf); // Get Application EUIvoid conf_getAppKey(uint8_t *buf); // Get Application Keyuint16_t conf_getMeasurementInterval(); // Get interval (bounds checked)void conf_setMeasurementInterval(uint16_t interval); // Set intervalVersion Handling:
- Firmware Version: From compile-time defines (
FW_VERSION_PROTO= 1,FW_VERSION_MAJOR= 0,FW_VERSION_MINOR= 0,FW_VERSION_PATCH= 0) - Hardware Version: From EEPROM
HW_VERSIONfield (2 bytes: MSB, LSB) - Encoding: 16-bit format
[proto:1][major:5][minor:5][patch:5]
Error Handling:
typedef enum { ERR_NONE, // No error ERR_SMBUS_SLAVE_NACK, // Slave did not acknowledge ERR_SMBUS_ARB_LOST, // Bus arbitration lost ERR_SMBUS_NO_ALERT, // No alert pending ERR_SMBUS_ERR, // General SMBus error} error_t;Configuration Functions:
conf_load()- Load config from EEPROMconf_save()- Save config to EEPROMconf_getMeasurementInterval()- Get measurement interval with bounds checkingconf_setMeasurementInterval()- Set measurement intervalconf_getAppEui(),conf_getDevEui(),conf_getAppKey()- LoRaWAN credentialsconf_getFirmwareVersion(),conf_getHardwareVersion()- Version infoversionToUint16()- Convert version struct to uint16
Compile-Time Configuration (config.h):
#define MIN_INTERVAL 20 // Minimum interval (seconds)#define MAX_INTERVAL 4270 // Maximum interval (seconds)#define SENSOR_ADDRESS 0x36 // I²C address4. Watchdog Timer (wdt.cpp)
Section titled “4. Watchdog Timer (wdt.cpp)”Custom watchdog implementation for device reset functionality:
void mcu_reset(void); // Force MCU reset via watchdog- Uses AVR watchdog timer directly
- 15ms timeout for reset
- Used by downlink reset command (0xDEAD)
5. Board Support (boards/)
Section titled “5. Board Support (boards/)”Board-specific implementations for hardware variants:
// mfm_v3_m1284p.cpp / mfm_v3.cppvoid board_setup(void); // Initialize board-specific settingsPin Definitions (include/board_config/):
mfm_v3_m1284p.h- ATmega1284P pin mappingsmfm_v3.h- ATmega328P pin mappings (legacy)
Board Selection via PlatformIO build flags:
-DBOARD_MFM_V3_M1284Pfor current boards-DBOARD_MFM_V3for legacy boards
Event Flow
Section titled “Event Flow”Power-On Sequence
Section titled “Power-On Sequence”graph TD
A[Power On] --> B[setup]
B --> C[Initialize Hardware]
C --> D[Load EEPROM Config]
D --> E{Config Valid?}
E -->|No| F[Blink LED Error]
E -->|Yes| G[Initialize LMIC]
G --> H[Start OTAA Join]
H --> I[loop]
Measurement Cycle
Section titled “Measurement Cycle”graph TD
A[Timer Expires] --> B[job_performMeasurements]
B --> C[sensors_performMeasurement]
C --> D[Send CMD_PERFORM to 0x36]
D --> E[Wait 10 seconds]
E --> F[job_fetchAndSend]
F --> G[sensors_readMeasurement]
G --> H[Send CMD_READ to 0x36]
H --> I[Read variable sensor data]
I --> J[LMIC_setTxData2 on FPort 1]
J --> K[scheduleNextMeasurement]
Downlink Handling
Section titled “Downlink Handling”graph TD
A[Receive Downlink] --> B[EV_TXCOMPLETE Event]
B --> C{LMIC.dataLen > 0?}
C -->|Yes| D[processDownlink]
D --> E{Command Byte}
E -->|0x10| F[DL_CMD_INTERVAL]
E -->|0x11| G[DL_CMD_MODULE]
E -->|0xDE| H[DL_CMD_REJOIN]
F --> I[conf_setMeasurementInterval + conf_save]
G --> J[smbus_blockWrite to sensor]
H --> K{Second byte = 0xAD?}
K -->|Yes| L[Schedule mcu_reset in 5s]
C -->|No| M[Continue Normal Operation]
Downlink Commands
Section titled “Downlink Commands”The firmware supports three downlink commands processed by processDownlink():
1. Device Reset (DL_CMD_REJOIN = 0xDE)
Section titled “1. Device Reset (DL_CMD_REJOIN = 0xDE)”Format: 0xDE 0xAD
- Purpose: Force device reset and rejoin
- Security: Requires exact second byte
0xAD(forms0xDEAD) - Action: Schedules
job_reset()after 5 seconds - Implementation:
mcu_reset()via watchdog timer
2. Measurement Interval (DL_CMD_INTERVAL = 0x10)
Section titled “2. Measurement Interval (DL_CMD_INTERVAL = 0x10)”Format: 0x10 <MSB> <LSB>
- Purpose: Change measurement interval
- Range: 20-4270 seconds (enforced by bounds checking)
- Action: Updates
conf_setMeasurementInterval()and saves to EEPROM - Side Effect: Cancels current measurement job and reschedules
3. Module Command (DL_CMD_MODULE = 0x11)
Section titled “3. Module Command (DL_CMD_MODULE = 0x11)”Format: 0x11 <ADDRESS> <COMMAND> [ARGS...]
- Purpose: Send SMBus command to external sensor
- Address: Sensor I²C address (typically 0x36)
- Command: Sensor-specific command byte
- Args: Optional command arguments (variable length)
- Implementation:
smbus_blockWrite(address, command, args, length)
Example Commands:
0xDE 0xAD → Reset device0x10 0x00 0x3C → Set interval to 60 seconds0x11 0x36 0x20 0x01 → Send command 0x20 with arg 0x01 to sensor 0x36Memory Layout
Section titled “Memory Layout”Flash (128KB)
Section titled “Flash (128KB)”- Bootloader: 512 bytes
- Application: ~50-60KB (depends on features)
- LMIC Library: ~30KB
- Arduino Core: ~20KB
- Free: ~20-30KB
SRAM (16KB)
Section titled “SRAM (16KB)”- Stack: ~2KB
- Heap: ~8KB
- LMIC Buffers: ~4KB
- Global Variables: ~2KB
EEPROM (4KB)
Section titled “EEPROM (4KB)”- Configuration: 41 bytes
- Free: 4055 bytes (available for extensions)
Design Patterns
Section titled “Design Patterns”1. Event-Driven Architecture
Section titled “1. Event-Driven Architecture”- Uses LMIC job scheduler
- Non-blocking operations
- Callback-based event handling
2. Hardware Abstraction
Section titled “2. Hardware Abstraction”- Board-specific code in
boards/directory - Conditional compilation for variants
- Easy to port to new hardware
3. Configuration Management
Section titled “3. Configuration Management”- Persistent storage in EEPROM
- Runtime validation
- Default fallback values
4. Power Management
Section titled “4. Power Management”- LMIC-based power control: Uses
os_runloop_once()for efficient sleep/wake cycles automatically - Custom reset functionality: Custom
wdt.cppfor controlled device resets (not sleep) - No external sleep libraries: Power management is handled entirely by LMIC library
- Job-based scheduling: All timing and power states managed by LMIC job scheduler
- Low-power operation: Event-driven design minimizes active time
Build System Integration
Section titled “Build System Integration”Conditional Compilation
Section titled “Conditional Compilation”#if BOARD == BOARD_MFM_V3_M1284P // ATmega1284P-specific code#endif
#ifdef DEBUG // Debug logging#endifOptimization Flags
Section titled “Optimization Flags”From platformio.ini:
build_flags = -Os # Optimize for size -ffunction-sections # Dead code elimination -fdata-sections -flto # Link-time optimizationExtending the Firmware
Section titled “Extending the Firmware”Adding New Sensor Types
Section titled “Adding New Sensor Types”- Define sensor commands in
sensors.h - Implement sensor driver in
sensors.cpp - Add sensor selection in
config.h - Update measurement loop in
main.cpp
Adding New Downlink Commands
Section titled “Adding New Downlink Commands”- Define command code in
main.cpp - Implement handler in
onEvent()→EV_TXCOMPLETE - Update payload decoder in TTN
- Document in protocol specification
Adding New Board Variants
Section titled “Adding New Board Variants”- Create new board config in
include/board_config/ - Create board implementation in
src/boards/ - Add board definition to
platformio.ini - Update
board.hwith new board ID
Next Steps
Section titled “Next Steps”- Development Guide - Build and modify firmware
- API Reference - Function documentation
- Build System - PlatformIO configuration