Firmware design patterns to ensure regulatory compliance without dropping critical telemetry.
LoRaWAN devices operate in unlicensed ISM bands, and with that freedom comes responsibility: duty cycle limits. In EU868, the standard mandates a 1% duty cycle per sub-band (ETSI EN 300.220). This means your device can transmit for at most 36 seconds per hour on any given channel.
For a firmware engineer, this is a hard real-time constraint — not a guideline.
Every transmission counts. Here’s what typical IoT telemetry looks like:
| Message Type | Payload | Air Time (SF12, 125 kHz) | % of Hourly Budget |
|---|---|---|---|
| Sensor reading | 12 bytes | ~1.5 s | 4.1% |
| ACK response | 0 bytes | ~0.5 s | 1.4% |
| OTA firmware fragment | 51 bytes | ~3.0 s | 8.3% |
| Join request | 23 bytes | ~2.3 s | 6.4% |
Transmit one join request + one sensor reading per hour and you’ve already consumed 10.5% of your budget — and you haven’t even acknowledged a downlink yet.
The solution is a central duty cycle manager that tracks consumed budget across all sub-bands:
typedef struct {
uint32_t frequency_hz;
uint8_t duty_pct; // e.g., 1 for 1%
uint32_t window_us; // regulatory window (1 hour)
int64_t budget_ns; // consumed this window
uint64_t window_start_ms;
} ChannelDuty;
typedef struct {
ChannelDuty channels[8];
uint8_t active_channel;
uint32_t pending_queue; // messages waiting for budget
} DutyCycleManager;
bool transmit_within_budget(DutyCycleManager *mgr,
uint32_t air_time_us) {
ChannelDuty *ch = &mgr->channels[mgr->active_channel];
uint64_t now = millis();
// Reset budget if window expired
if (now - ch->window_start_ms > 3_600_000) {
ch->budget_ns = 0;
ch->window_start_ms = now;
}
int64_t available = (int64_t)(ch->window_us / 100)
- ch->budget_ns;
if ((int64_t)air_time_us <= available) {
ch->budget_ns += air_time_us;
return true; // OK to transmit
}
return false; // Queue for next window
}
LoRaWAN’s ADR mechanism is your best tool for duty cycle efficiency. By reducing the spreading factor, you dramatically shrink air time:
| SF | Air Time (12 bytes, 125 kHz) | Rx sensitivity |
|---|---|---|
| SF12 | ~1,482 ms | -137 dBm |
| SF10 | ~371 ms | -132 dBm |
| SF7 | ~62 ms | -123 dBm |
Moving from SF12 to SF7 yields a 24× reduction in air time for the same payload — from 4.1% to 0.17% of your hourly budget.
Not all telemetry is equal. When budget is low, prioritize:
typedef enum {
PRIO_CRITICAL = 0, // Always send
PRIO_ACK = 1, // Send ASAP
PRIO_NORMAL = 2, // Send within budget
PRIO_DIAG = 3 // Opportunistic
} MsgPriority;
Efficient duty cycle management is a firmware engineering discipline. By tracking consumption per sub-band, leveraging ADR to minimize air time, and prioritizing messages by criticality, you can maintain regulatory compliance without sacrificing data quality.
The key takeaway: treat duty cycle like a memory budget — allocate, track, and optimize it from day one of your firmware design.