Firmware Architecture
Overview
The CICERONE AirLink firmware implements a non-blocking, timer-based architecture designed for continuous indoor air quality monitoring with remote telemonitoring capabilities. Built on the Arduino framework for the Nano 33 BLE Sense Rev2, it ensures reliable data collection and transmission.
Non-Blocking Timer System
The firmware uses a dual-timer architecture to achieve non-blocking operation:
5-Second Timer
Implementation: millis()-based
Purpose: Triggers sensor reading and data accumulation
Characteristics:
- Based on Arduino's
millis()function for microsecond precision - Non-blocking implementation prevents loop stalls
- Accuracy: ±10ms
Code Example:
unsigned long current_millis = millis();
if (current_millis - prev_millis >= 5000) {
prev_millis = current_millis;
alarma_5s = true; // Set flag for main loop
}
10-Minute Timer
Implementation: RTC-based
Purpose: Triggers data averaging and transmission
Characteristics:
- Uses DS3231M RTC for accurate timekeeping independent of microcontroller clock
- Maintains timing accuracy even during long-term deployment
- Accuracy: ±1 second
Benefits:
- Survives power cycles (RTC has battery backup)
- Independent of system clock drift
- Accurate over extended periods
Modular Component Structure
The firmware is organized into logical modules, each with dedicated header and implementation files:
| Module | Files | Purpose | Dependencies |
|---|---|---|---|
| Main | firmware.ino |
Entry point, setup, and main loop | All modules |
| Configuration | Configuracion.h |
Centralized settings (device ID, server, APN) | None |
| Debug | Debug.h |
Multi-level logging macros | None |
| Alarm System | Alarma.h/cpp |
Timer management and data averaging | Sensors, RTC |
| SEN54 API | SEN5X_API.h/cpp |
Particulate matter & VOC sensor interface | Wire (I2C) |
| T6793 API | T6793_API.h/cpp |
CO₂ sensor interface | T67XX driver |
| T67XX Driver | T67XX.h/cpp |
Low-level I2C driver for T6793 | Wire (I2C) |
| RTC | DS3231M.h/cpp |
Real-time clock interface | Wire (I2C) |
| NB-IoT | Transmision_NBIOT.h/cpp |
Network communication module | Serial1 (UART) |
| DateTime | Datetime_helper.h |
Date/time utilities and timezone support | None |
Module Interaction Diagram
graph TB
Main[firmware.ino] --> Config[Configuracion.h]
Main --> Debug[Debug.h]
Main --> Alarma[Alarma.h/cpp]
Main --> NbIoT[Transmision_NBIOT.h/cpp]
Alarma --> SEN5X[SEN5X_API.h/cpp]
Alarma --> T6793[T6793_API.h/cpp]
Alarma --> RTC[DS3231M.h/cpp]
T6793 --> T67XX[T67XX.h/cpp]
NbIoT --> DateTime[Datetime_helper.h]
SEN5X --> I2C[Wire Library]
T67XX --> I2C
RTC --> I2C
NbIoT --> UART[Serial1]
style Main fill:#e3f2fd
style Config fill:#fff3e0
style Alarma fill:#e8f5e9
style NbIoT fill:#f3e5f5
Data Flow Pipeline
The firmware follows a structured data flow from sensor reading to transmission:
graph TD
A[Sensors] -->|5s interval| B[Read Raw Data]
B --> C[Accumulate Samples]
C -->|120 samples| D[Calculate Average]
D --> E[Format JSON Packet]
E --> F{NB-IoT Enabled?}
F -->|Yes| G[HTTP POST]
F -->|No| H[Serial Output Only]
G --> I[Server]
H --> J[Local Monitor]
style A fill:#e1f5ff
style D fill:#fff4e1
style G fill:#e8f5e9
style I fill:#f3e5f5
Pipeline Stages
Stage 1: Sensor Reading (Every 5 seconds)
- Triggered by
alarma_5sflag - Reads all sensors via I2C
- Non-blocking implementation
Stage 2: Data Accumulation
- Sum values stored in accumulator variables
- Separate counters for different sensor groups
- Floating-point precision maintained
Stage 3: Averaging (Every 10 minutes)
- Arithmetic mean:
average = sum / count - Triggered by
alarma_10minflag - 120 samples (12 per minute × 10 minutes)
Stage 4: JSON Formatting
- Builds JSON object with all sensor data
- Includes timestamp from RTC
- Device ID from configuration
Stage 5: Transmission
- Conditional on
HABILITAR_NBIOTsetting - HTTP POST to configured server
- Retry mechanism on failure
Memory Management
The firmware employs careful memory management strategies to ensure reliable operation within the constraints of the microcontroller.
Static Allocation
Approach: Most variables are statically allocated
Benefits:
- Avoids heap fragmentation
- Predictable memory usage
- Faster execution (no malloc overhead)
Example:
// Static allocation in Alarma.cpp
float sum_sen5x_mc_2p5 = 0.0f;
float avg_sen5x_mc_2p5 = 0.0f;
uint16_t cont1 = 0;
Accumulator Variables
Purpose: Separate sum variables for each sensor parameter
Structure:
sum_*- Accumulation variables (reset every 10 minutes)avg_*- Average variables (calculated every 10 minutes)cont*- Sample counters
Memory Impact: ~50 bytes per sensor parameter
String Optimization
Strategy: Minimize dynamic string operations
Techniques:
- Use
F()macro to store strings in flash memory - Fixed-size character buffers for serial output
- Avoid String class concatenation in loops
Example:
// Good: String in flash
Serial.println(F("Sensor initialized"));
// Avoid: Dynamic string allocation
String msg = "Value: " + String(value); // Creates temporary objects
Buffer Management
Serial Communication: 256-byte buffer for debug output
NB-IoT Communication: Fixed buffers for AT commands and responses
JSON Generation: Pre-allocated buffer (configurable size)
Memory Usage Summary
| Category | Size | Notes |
|---|---|---|
| Code (Flash) | ~100-150 KB | Depends on enabled modules |
| Global Variables | ~5-10 KB | Sensor data, accumulators |
| Stack | ~2-4 KB | Function calls, local vars |
| Debug Buffers | ~256 bytes | When DEBUG_LEVEL > 0 |
| Available Flash | ~900 KB | Nano 33 BLE has 1MB flash |
| Available RAM | ~240 KB | Nano 33 BLE has 256KB RAM |
Memory Efficiency
The firmware uses less than 15% of available flash and less than 10% of available RAM, leaving plenty of room for future enhancements.
Conditional Compilation
The firmware uses preprocessor directives to include or exclude features at compile time.
NB-IoT Module
#if HABILITAR_NBIOT
#include "Transmision_NBIOT.h"
void loop() {
if (alarma_10min) {
nbiot_enviar(json);
}
}
#endif
When disabled (HABILITAR_NBIOT = 0):
- All NB-IoT code is removed from binary
- Reduces flash usage by ~30-40 KB
- Eliminates UART communication overhead
- Saves power (no cellular radio)
Debug System
#if DEBUG_LEVEL > 0
#define DEBUG_ERROR(format, ...) debug_printf("[ERROR]", format, ##__VA_ARGS__)
#else
#define DEBUG_ERROR(format, ...) // Expands to nothing
#endif
When disabled (DEBUG_LEVEL = 0):
- All debug code removed (zero overhead)
- Reduces flash usage by ~10-15 KB
- Eliminates string storage in flash
- No serial output
Benefits of Modular Architecture
Code Maintainability
- Separation of Concerns: Each module has a single responsibility
- Easy Testing: Modules can be tested independently
- Clear Interfaces: Well-defined function signatures
Extensibility
- Add Sensors: Create new API module following existing patterns
- New Communication: Replace NB-IoT with different protocol
- Custom Processing: Modify averaging algorithms without affecting sensors
Debugging
- Module-Level Debug: Enable/disable debug per module
- Isolated Issues: Easier to identify problematic components
- Clear Logging: Debug messages tagged with module name
Portability
- Hardware Abstraction: Sensor APIs abstract I2C details
- Platform Independence: Most code portable to other Arduino boards
- Configuration Centralized: Easy to adapt to different deployments
Related Documentation
- Program Flow - Detailed execution flow and state diagrams
- Sensor Interfaces - Sensor communication protocols
- Configuration System - Device configuration
- API Reference - Complete code documentation