Firmware Architecture
Firmware Architecture
Section titled “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)”High-Level Sensor API:
error_t sensors_init(void); // Initialize SMBus interfaceerror_t sensors_performMeasurement(void); // Send CMD_PERFORM to 0x36error_t sensors_readMeasurement(uint8_t *buf, uint8_t *length); // Read data
Low-Level SMBus Protocol:
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 block
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
3. 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 bytes
Configuration 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 interval
Version Handling:
- Firmware Version: From compile-time defines (
FW_VERSION_MAJOR
= 0,FW_VERSION_MINOR
= 0,FW_VERSION_PATCH
= 0) - Hardware Version: From EEPROM
HW_VERSION
field - Encoding: 16-bit format
[proto:1][major:5][minor:5][patch:5]
uint8_t LSB; // Hardware version LSB HW_VERSION; // Hardware version (2 bytes) uint8_t APP_EUI[8]; // Application EUI uint8_t DEV_EUI[8]; // Device EUI uint8_t APP_KEY[16]; // Application Key uint16_t MEASUREMENT_INTERVAL; // Measurement interval (seconds) uint8_t USE_TTN_FAIR_USE_POLICY; // Fair Use Policy compliance
**Error Handling:**```cpptypedef 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;
uint16_t MEASUREMENT_INTERVAL; // Measurement interval (seconds)uint8_t USE_TTN_FAIR_USE_POLICY; // Fair Use Policy compliance
};
**Configuration Functions:**- `conf_load()` - Load config from EEPROM- `conf_save()` - Save config to EEPROM- `conf_getMeasurementInterval()` - Get measurement interval with bounds checking- `conf_setMeasurementInterval()` - Set measurement interval- `conf_getAppEui()`, `conf_getDevEui()`, `conf_getAppKey()` - LoRaWAN credentials- `conf_getFirmwareVersion()`, `conf_getHardwareVersion()` - Version info- `versionToUint16()` - Convert version struct to uint16
**Compile-Time Configuration** (`config.h`):```cpp#define MIN_INTERVAL 20 // Minimum interval (seconds)#define MAX_INTERVAL 4270 // Maximum interval (seconds)#define SENSOR_ADDRESS 0x36 // I²C address
4. 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 settings
Pin 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_M1284P
for current boards-DBOARD_MFM_V3
for 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 0x36
Memory 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.cpp
for 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#endif
Optimization Flags
Section titled “Optimization Flags”From platformio.ini
:
build_flags = -Os # Optimize for size -ffunction-sections # Dead code elimination -fdata-sections -flto # Link-time optimization
Extending 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.h
with new board ID
Next Steps
Section titled “Next Steps”- Development Guide - Build and modify firmware
- API Reference - Function documentation
- Build System - PlatformIO configuration