Most Bluetooth Beacon projects fail not because of bad hardware but because of firmware that burns through batteries in weeks instead of years, crashes under RF interference, or silently stops advertising after a brownout. The difference between a beacon that lasts 3 months and one that lasts 3 years on the same CR2032 comes down to how the firmware schedules radio events, manages power states, and recovers from faults. This article breaks down the architecture of production-grade beacon firmware, using the nRF52 + SoftDevice as the reference platform because it powers roughly 70% of commercial beacons today.
1. The Big Picture: Why Firmware Architecture Determines Battery Life
A beacon spends 99.97% of its life asleep. With a 1-second advertising interval and 3-byte payload on 3 channels, the radio is active for about 300 microseconds per second. That means out of every 86,400 seconds in a day, the radio transmits for roughly 26 seconds total. The remaining 86,374 seconds are spent in sleep, and how deeply you sleep determines everything.
| State | nRF52832 Current | Time/Event (1s interval) | Avg Current Contribution |
|---|---|---|---|
| Deep Sleep (System OFF w/ RAM retention) | 1.5 uA | ~999.6 ms | 1.499 uA |
| Ramp-up + Crystal Start | 3.2 mA | ~130 us | 0.416 uA |
| Tx on Ch37 (0 dBm) | 4.6 mA | ~80 us (3 bytes) | 0.368 uA |
| Inter-channel gap | 1.8 mA | ~150 us | 0.270 uA |
| Tx Ch38 + Ch39 | 4.6 mA | ~160 us | 0.736 uA |
| Ramp-down + RTC processing | 2.1 mA | ~90 us | 0.189 uA |
| Total average (1s interval, 0 dBm, 3-byte payload) | 3.478 uA | ||
On a CR2032 (220 mAh nominal, ~160 mAh usable at 3V pulsed load): 160,000 uAh / 3.478 uA = 46,000 hours = 5.2 years. That is the theoretical ceiling. Real-world firmware adds sensor polling, LED blinks, button scanning, watchdog interrupts, and RTC drift compensation, each of which can add 5-50 uA to the average. A single unnecessary 1 ms wake-up per second at 3 mA adds 3 uA, nearly doubling your sleep budget.
2. Radio Scheduler: How Advertising Events Are Actually Scheduled
The SoftDevice (Nordic’s BLE stack) acts as a preemptive radio scheduler. Your application code never touches the radio directly. Instead, you configure advertising parameters, and the SoftDevice places radio events into its internal timeline. Understanding this timeline is critical because every radio event has setup overhead that your firmware must account for.
2.1 The SoftDevice Event Timeline
For each advertising interval, the SoftDevice executes the following sequence on each of the 3 advertising channels (37, 38, 39):
SoftDevice Radio Timeline (per advertising interval)
[RC] Tx37 [RD] gap [RC] Tx38 [RD] gap [RC] Tx39 [RD]
130us 80us 20us 130us 80us 20us 130us 80us 20us
RC = Radio Ramp-up + Crystal settling
RD = Radio Ramp-down
Total active time: ~610 us (3-byte payload, 0 dBm)
The inter-channel gap is not user-configurable. It is internally set to 150 us minimum (includes PLL relock and frequency switching). If you enable scanning or connection modes alongside advertising, the SoftDevice interleaves these events, and your advertising timing will jitter by up to +/-200 us.
2.2 Advertising Interval vs. Duty Cycle
| Interval | Events/Hour | Radio Active/Hour | Duty Cycle | Avg Radio Current |
|---|---|---|---|---|
| 100 ms | 36,000 | 22.0 s | 0.0061% | 28.0 uA |
| 200 ms | 18,000 | 11.0 s | 0.0030% | 14.0 uA |
| 500 ms | 7,200 | 4.4 s | 0.0012% | 5.6 uA |
| 1000 ms | 3,600 | 2.2 s | 0.0006% | 2.8 uA |
| 2000 ms | 1,800 | 1.1 s | 0.0003% | 1.4 uA |
| 5000 ms | 720 | 0.44 s | 0.0001% | 0.56 uA |
The 0 dBm TX current is 4.6 mA on nRF52832. At 100 ms interval, the radio alone consumes 28 uA average, already exceeding your entire 3 uA sleep budget if you are targeting a 2-year CR2032 life. This is why 100 ms intervals are only viable on USB-powered beacons or units with AA packs (2500+ mAh).
2.3 Non-Connectable vs. Connectable Advertising: The Latency Trap
Non-connectable advertising (ADV_NONCONN_IND) is fire-and-forget: transmit, power down, done. Connectable advertising (ADV_IND) requires the radio to stay in Rx mode after each Tx for a configurable listen window. The default listen window in the SoftDevice is 0 ms, but if you enable scan response data, the radio stays in Rx for up to 10 ms per channel, that is 30 ms per interval of Rx current at 5.4 mA.
| Mode | Extra Rx Time/Interval | Extra Current at 1s | Extra Current at 5s |
|---|---|---|---|
| ADV_NONCONN (no scan response) | 0 ms | 0 uA | 0 uA |
| ADV_IND + Scan Response (10ms window) | 30 ms | 162 uA | 32.4 uA |
| ADV_IND + Connection (connected, 1s interval) | Continuous | ~800 uA | ~800 uA |
If your beacon advertises scan response data (e.g., to include a URL or service UUID), the 162 uA overhead at 1s interval is 47x your sleep current. This is why many production beacons drop scan response entirely and pack everything into the 31-byte manufacturer-specific data payload.
3. Power State Machine: The Heart of Beacon Firmware
A well-designed beacon firmware has an explicit, documented power state machine. Every state has a defined current consumption, entry/exit conditions, and maximum dwell time. Here is the state machine used in production beacon firmware at a tier-1 OEM:
3.1 Power State Definitions
Beacon Power State Machine
[DEEP SLEEP] --RTC IRQ--> [ADV EVENT (Active)]
1.5 uA 5.2 mA
| |
| Button IRQ | Sensor Pending?
v v
[BUTTON HANDLER] [SENSOR READ]
3.0 mA 1.2 mA
| |
| 500ms timeout | 2ms read
v v
[CONFIG MODE] [DEEP SLEEP]
8.5 mA 1.5 uA
Fault path: Watchdog timeout -> RESET -> INIT -> SLEEP
3.2 State Transition Budget
| State | Current | Typical Duration | Energy/Event | Events/Day | Daily Energy |
|---|---|---|---|---|---|
| Deep Sleep (RAM retained) | 1.5 uA | 999.4 ms | 1.499 uJ | 86,400 | 129.5 mJ |
| Advertising Event | 5.2 mA | 610 us | 9.52 uJ | 86,400 | 822.5 mJ |
| Sensor Read (LIS2DH12) | 1.2 mA | 2 ms | 7.2 uJ | 2,880 (30s) | 20.7 mJ |
| Button Scan (GPIO poll) | 3.0 mA | 0.1 ms | 0.9 uJ | 86,400 | 77.8 mJ |
| Config Mode (GATT) | 8.5 mA | ~5 s (occasional) | 127.5 mJ | ~2 | 255 mJ |
| RTC + Event Processing | 3.2 mA | 50 us | 0.48 uJ | 86,400 | 41.5 mJ |
| Total daily energy | 1,347 mJ | ||||
CR2032 energy: 220 mAh x 3.0V = 2,376 J. Usable (80% depth, 3V to 2.0V): ~1,520 J. Daily budget: 1,347 mJ, so 1,520,000 / 1,347 = 1,128 days = 3.1 years. This matches field data from beacons with 1s interval, accelerometer at 30s, and weekly config connections.
3.3 The System OFF vs. System ON Decision
The nRF52 offers two low-power modes. Choosing the wrong one is the most common firmware mistake in beacon designs.
| Parameter | System ON (WFE) | System OFF |
|---|---|---|
| Current | 1.5 uA (with RAM retention) | 0.4 uA (no RAM retention) |
| Wake-up latency | ~2 us | ~300 us (reset + init) |
| RAM retention | Yes (configurable blocks) | No (all RAM lost) |
| RTC running | Yes (LFXO keeps running) | No (must use GPIO sense) |
| Wake-up sources | RTC, GPIO, COMP, LPCOMP | GPIO sense only |
| SoftDevice state | Preserved (timer-based advertising) | Destroyed (must re-init) |
For beacons using the SoftDevice, System ON is mandatory. The SoftDevice needs the RTC running to schedule the next advertising event. System OFF is only useful for shipping/storage mode, where you want sub-uA current and can afford a full re-initialization on wake (triggered by a GPIO button press).
4. Timer Architecture: RTC vs. SysTick vs. Application Timers
Beacon firmware typically uses three layers of timing, each with different accuracy and power implications:
4.1 Three-Layer Timer Stack
| Layer | Hardware | Clock Source | Accuracy | Power (Active) | Resolution |
|---|---|---|---|---|---|
| BLE Stack (SoftDevice) | RTC1 | 32.768 kHz LFXO | +/-20 ppm | 0.3 uA (always on) | 30.5 us |
| Application Timers | RTC2 (app_timer) | 32.768 kHz LFXO | +/-20 ppm | 0.1 uA (shared RTC1) | 30.5 us |
| High-Res Delays | TIMER0/1/2 | 16 MHz HCLK | +/-50 ppm | 5.5 mA (only when running) | 62.5 ns |
The SoftDevice monopolizes RTC1 for its own scheduling. Application timers (via Nordic’s app_timer library) share RTC1 through a software callback queue. This means application timer callbacks are not truly periodic, they are coalesced and may jitter by up to 1 RTC tick (30.5 us) per event. For beacon firmware, this jitter is irrelevant. But if you are using timers for precise sensor sampling (e.g., accelerometer at exactly 100 Hz), you need a hardware timer.
4.2 Clock Drift and Its Impact on Advertising Timing
A +/-20 ppm crystal means the RTC drifts by up to 20 microseconds per second. Over 24 hours, that is 1.728 seconds. The SoftDevice compensates for this internally using its own crystal calibration, but your application-level timers (for sensor polling, LED blink, etc.) will drift. If you log timestamps on the beacon, you must compensate:
// nRF52 crystal drift compensation
// HCLK (16 MHz) is calibrated against LFXO (32.768 kHz)
// SoftDevice runs calibration every 4s by default
#define CRYSTAL_PPM 20 // +/-20 ppm typical
#define SECONDS_PER_DAY 86400
float drift_per_day = (float)CRYSTAL_PPM * SECONDS_PER_DAY / 1e6; // 1.728 s/day
float drift_per_hour = drift_per_day / 24; // 0.072 s/hour
uint32_t compensated_ts(uint32_t rtc_ticks, uint32_t boot_time_s) {
float elapsed_s = (float)(rtc_ticks - boot_time_s);
float correction = elapsed_s * CRYSTAL_PPM / 1e6;
return (uint32_t)(rtc_ticks - correction);
}
4.3 Timer Coalescing: Saving Wake-Ups
Nordic’s app_timer library supports timer coalescing: if two timers expire within the same RTC tick window, they are merged into a single wake-up. This matters because each wake-up costs ~50 us at 3.2 mA (0.16 nJ per wake). At 1000 timers/hour, that is 0.16 uJ, negligible. But the real cost is the event processing overhead: each timer callback wakes the CPU from WFE, processes the event, and returns to sleep. The CPU running at 64 MHz draws 4.5 mA. A 20 us callback costs 90 nJ. At 1000 callbacks/hour, that is 90 uJ/hour, still small, but it adds up.
The optimization strategy: batch your timer events. Instead of polling the accelerometer every 30 seconds AND the temperature sensor every 30 seconds (2 wake-ups), align them to the same 30-second boundary (1 wake-up that reads both sensors). This halves your sensor-read energy budget with zero code complexity.
5. Event-Driven Architecture: Interrupt, Queue, Handler
Production beacon firmware should never use busy-wait loops or blocking delays. Every operation should be event-driven: an interrupt fires, posts an event to a queue, and the main loop processes it. This is the standard pattern for RTOS-based firmware, but it works equally well in bare-metal super-loop designs.
5.1 Event Queue Design
// Beacon firmware event system (bare-metal)
typedef enum {
EVT_ADV_COMPLETE = 0,
EVT_SENSOR_TIMER,
EVT_BUTTON_PRESS,
EVT_BUTTON_RELEASE,
EVT_BATTERY_LOW,
EVT_GATT_CONNECT,
EVT_GATT_DISCONNECT,
EVT_OTA_START,
EVT_OTA_COMPLETE,
EVT_WATCHDOG,
EVT_FAULT,
} beacon_event_t;
typedef struct {
beacon_event_t type;
uint32_t data;
uint32_t timestamp;
} event_t;
#define EVENT_QUEUE_SIZE 16
static event_t event_queue[EVENT_QUEUE_SIZE];
static volatile uint8_t eq_head = 0, eq_tail = 0;
void event_post(beacon_event_t type, uint32_t data) {
uint8_t next = (eq_head + 1) % EVENT_QUEUE_SIZE;
if (next == eq_tail) {
fault_set(FAULT_EVENT_QUEUE_OVERFLOW);
return;
}
event_queue[eq_head].type = type;
event_queue[eq_head].data = data;
event_queue[eq_head].timestamp = rtc_get_ticks();
eq_head = next;
}
bool event_get(event_t *evt) {
if (eq_head == eq_tail) return false;
*evt = event_queue[eq_tail];
eq_tail = (eq_tail + 1) % EVENT_QUEUE_SIZE;
return true;
}
int main(void) {
beacon_init();
advertising_start();
event_t evt;
while (1) {
if (event_get(&evt)) {
event_handler(&evt);
} else {
__WFE();
}
}
}
The key design principles: (1) ISRs only post events, never do processing. (2) The main loop processes events in FIFO order. (3) When no events are pending, the CPU enters WFE (Wait For Event) which drops to 1.5 uA. (4) Queue overflow sets a fault flag but never blocks, the beacon must keep advertising even if event processing falls behind.
5.2 Interrupt Latency Budget
| Interrupt Source | Priority | Max ISR Time | CPU Wake Current | Energy per ISR |
|---|---|---|---|---|
| SoftDevice (Radio) | 0 (highest) | 10 us | 4.5 mA @ 64 MHz | 0.045 nJ |
| RTC (app_timer) | 1 | 5 us | 4.5 mA | 0.023 nJ |
| GPIO (Button) | 2 | 3 us | 4.5 mA | 0.014 nJ |
| SPI (Sensor) | 3 | 15 us | 4.5 mA | 0.068 nJ |
| Watchdog | 0 (NMI) | 1 us | 4.5 mA | 0.005 nJ |
The SoftDevice has the highest priority and can preempt any application ISR. This is non-negotiable. The BLE stack must meet its real-time radio timing requirements. Your application ISRs must be short enough to not block the SoftDevice. The practical rule: total ISR time per radio event should be under 50 us. If your SPI sensor read takes 200 us, do it in DMA mode with completion interrupt, not in a blocking ISR.
6. Watchdog and Fault Recovery
A beacon deployed in a ceiling tile at a 500-store retail chain cannot be rebooted manually. The firmware must self-recover from any fault condition. This requires a multi-layer fault recovery strategy:
6.1 Three-Layer Fault Recovery
| Layer | Trigger | Response | Recovery Time |
|---|---|---|---|
| L1: Soft fault | Event queue overflow, sensor read timeout | Log fault, skip event, continue | 0 ms (immediate) |
| L2: Hard fault | Radio stuck (no adv for 60s), SPI bus lockup | Soft reset peripheral, re-init stack | 50-200 ms |
| L3: System fault | Watchdog timeout (8s), hard fault exception | Full system reset | 500 ms – 2 s |
6.2 Watchdog Configuration
// nRF52 WDT configuration for beacon firmware
// WDT runs from 32.768 kHz LFXO, works in System ON sleep
#define WDT_TIMEOUT_MS 8000 // 8 seconds (max: ~36 hours)
#define WDT_CHANNELS 2 // Channel 0: main loop, Channel 1: SoftDevice
void wdt_init(void) {
NRF_WDT->CONFIG = (WDT_CONFIG_HALT_Pause << WDT_CONFIG_HALT_Pos)
| (WDT_CONFIG_SLEEP_Run << WDT_CONFIG_SLEEP_Pos);
NRF_WDT->CRV = 32768 * (WDT_TIMEOUT_MS / 1000); // 262144 cycles = 8s
NRF_WDT->RREN = (1 << 0) | (1 << 1);
NRF_WDT->TASKS_START = 1;
}
void wdt_feed(void) {
NRF_WDT->RR[0] = 0x6E524635; // Magic reload value
}
The watchdog timeout must be longer than your longest expected sleep interval. If you use a 10-second advertising interval, the WDT must be at least 20 seconds (2x margin). If the WDT fires during sleep, the beacon resets, re-initializes, and resumes advertising within 1-2 seconds. The end user never notices.
6.3 Brownout Recovery
CR2032 batteries have significant internal resistance (5-15 ohm fresh, 30+ ohm near end-of-life). When the radio transmits at 4.6 mA, the voltage drops by 4.6 mA x 10 ohm = 46 mV. If the battery is at 2.3V, the supply dips to 2.254V, dangerously close to the nRF52’s 1.7V brownout threshold. A cold battery at 0C can have 25 ohm internal resistance, causing a 115 mV drop.
| Battery Voltage | Internal R | Vdrop @ 4.6mA | Vmin during Tx | BOR Risk |
|---|---|---|---|---|
| 3.0V (fresh) | 5 ohm | 23 mV | 2.977V | None |
| 2.6V (50% used) | 10 ohm | 46 mV | 2.554V | None |
| 2.3V (80% used) | 15 ohm | 69 mV | 2.231V | Low |
| 2.1V (95% used) | 30 ohm | 138 mV | 1.962V | HIGH |
| 2.1V at 0C | 25 ohm | 115 mV | 1.985V | HIGH |
The nRF52’s BOR threshold is configurable. For CR2032-powered beacons, set it to 1.7V (the lowest option). The firmware should also implement a soft brownout detector: read the battery voltage via SAADC before each advertising burst, and if below 2.0V, skip the Tx and enter an ultra-low-power mode (5s interval, 0 dBm) to extend remaining life.
7. Memory Layout and Persistent Storage
7.1 Flash Memory Map (nRF52832, 512 KB)
| Region | Address Range | Size | Contents |
|---|---|---|---|
| SoftDevice (MBR + S132) | 0x00000000 – 0x00026FFF | 160 KB | Master Boot Record + BLE stack |
| Application | 0x00027000 – 0x00073FFF | 308 KB | Main firmware code + read-only data |
| Settings Page (NVS) | 0x00074000 – 0x00077FFF | 16 KB | Config: adv interval, TX power, UUID, major/minor |
| Application Data (NVS) | 0x00078000 – 0x0007BFFF | 16 KB | Sensor logs, fault history, calibration data |
| Bootloader + OTA Bank | 0x0007C000 – 0x0007FFFF | 16 KB | Bootloader (8KB) + unused |
The nRF52832 does not have enough flash for dual-bank OTA. If you need dual-bank (for safe rollback), you need the nRF52840 with 1 MB flash. On nRF52832, OTA is single-bank: the new firmware is written directly over the old one. If power fails during OTA, the beacon is bricked and must be recovered via SWD.
7.2 RAM Allocation
| Component | RAM Usage | Notes |
|---|---|---|
| SoftDevice (S132) | 8.5 KB | Fixed: 4 connections, 3 adv sets |
| BLE stack buffers | 4.2 KB | Connection events + notification queues |
| Application stack + heap | 2.0 KB | Main loop, event queue, timers |
| Sensor buffers | 1.5 KB | Accelerometer FIFO (32 samples x 6 bytes) |
| NVS cache | 1.0 KB | Cached config (avoid flash reads on every event) |
| Free | ~46.8 KB | Available for future features |
| Total used | 17.2 KB / 64 KB | |
7.3 NVS Wear Leveling
Flash endurance on nRF52 is 10,000 write cycles per page. If you write config data every time a parameter changes, and a user changes the advertising interval 20 times, that is 20 cycles, fine. But if you log sensor data to flash every minute, you will burn through the 10,000-cycle limit in 7 days (10,080 minutes). The solution is wear leveling:
// Simple wear-leveling NVS scheme for sensor logs
// Each flash page (4 KB) holds 512 log entries (8 bytes each)
// Rotate through 4 pages = 2048 entries before erase
#define LOG_PAGE_SIZE 4096
#define LOG_ENTRY_SIZE 8
#define LOG_ENTRIES_PER_PAGE (LOG_PAGE_SIZE / LOG_ENTRY_SIZE) // 512
#define LOG_NUM_PAGES 4
#define LOG_TOTAL_ENTRIES (LOG_ENTRIES_PER_PAGE * LOG_NUM_PAGES) // 2048
// At 1 entry/minute: 2048 minutes = 34 hours of logs
// Page erase frequency: every 512 minutes = ~8.5 hours
// Page endurance: 10,000 erases x 8.5h = 85,000 hours = 9.7 years
8. BLE Stack Integration: SoftDevice Configuration for Beacons
8.1 Minimal SoftDevice Configuration
// S132 SoftDevice configuration for beacon-only operation
ble_cfg_t ble_cfg;
memset(&ble_cfg, 0, sizeof(ble_cfg));
// 1. Role: Observer + Broadcaster (non-connectable beacon)
ble_cfg.common_cfg.vs_uuid_cfg.vs_uuid_count = 1;
// 2. GAP configuration
ble_cfg.gap_cfg.role_count_cfg.adv_set_count = 1;
ble_cfg.gap_cfg.role_count_cfg.periph_role_count = 1; // 1 connection slot (for config)
ble_cfg.gap_cfg.role_count_cfg.central_role_count = 0;
// 3. GATT - minimal
ble_cfg.conn_cfg.conn_cfg_tag = BLE_CONN_CFG_TAG_CUSTOM;
ble_cfg.conn_cfg.params.gatt_conn_cfg.att_mtu = 23;
ble_cfg.conn_cfg.params.gatt_conn_cfg.event_length = 320;
8.2 Advertising Set Configuration
| Parameter | Value | Rationale |
|---|---|---|
| Advertising type | ADV_NONCONN_IND | Beacon does not accept connections during normal operation |
| Interval | 1000 ms (configurable 20ms-10s) | Balances discoverability and battery life |
| Duration | 0 (forever) | Beacon runs until battery dies or config mode |
| TX power | 0 dBm (configurable -40 to +4 dBm) | Default range: ~30m line-of-sight |
| Primary PHY | 1 Mbps (LE Legacy) | Maximum compatibility with all receivers |
| Channel map | 37 + 38 + 39 (all) | Do not disable channels, causes discovery delays |
8.3 Config Mode: Switching to Connectable
// Config mode entry (triggered by button hold or NFC tap)
void enter_config_mode(void) {
sd_ble_gap_adv_stop(m_adv_handle);
m_adv_params.type = BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED;
m_adv_params.interval = MSEC_TO_UNITS(100, UNIT_0_625_MS); // 100ms for fast discovery
m_adv_params.duration = MSEC_TO_UNITS(60000, UNIT_10_MS); // 60s timeout
sd_ble_gap_adv_set_configure(&m_adv_handle, &m_adv_data, &m_adv_params);
sd_ble_gap_adv_start(m_adv_handle, APP_BLE_CONN_CFG_TAG);
app_timer_start(m_config_timer, APP_TIMER_TICKS(300000), NULL); // 5 min timeout
}
During config mode (100 ms connectable interval), the average current jumps to ~300 uA. The 5-minute timeout ensures the battery drain is bounded: 300 uA x 5 min = 25 uAh, less than 0.02% of a CR2032’s capacity per config session.
9. OTA Firmware Update State Machine
Over-the-air updates for beacons are challenging because the radio must simultaneously receive new firmware and maintain advertising presence. The nRF52’s single-bank OTA approach (on nRF52832) means the application is overwritten during transfer. If the connection drops mid-transfer, the beacon must recover gracefully.
9.1 OTA Transfer Parameters
| Parameter | Value | Notes |
|---|---|---|
| MTU | 247 bytes (negotiated) | 20 bytes payload per write at default 23 MTU, too slow |
| Connection interval | 15 ms | Balance between throughput and power |
| Packet payload | 244 bytes (MTU – 3 header) | Each GATT write = 244 bytes firmware data |
| Firmware size | ~120 KB (typical beacon firmware) | Application region only, excludes SoftDevice |
| Packets needed | ~508 | 120,000 / 244 + overhead |
| Transfer time | ~8 seconds | 508 x 15ms + processing overhead |
| Current during OTA | ~8.5 mA | Radio in connected mode + flash erase/write |
| Energy per OTA | ~68 mJ | 8.5 mA x 3V x 8s |
| Battery impact | ~28 uAh | 0.017% of CR2032 per update |
9.2 CRC Verification and Boot Validation
// Post-OTA verification (runs in bootloader before booting new firmware)
#define APP_START_ADDR 0x00027000
#define APP_END_ADDR 0x00074000
#define CRC32_POLY 0xEDB88320
uint32_t crc32_compute(const uint8_t *data, uint32_t len) {
uint32_t crc = 0xFFFFFFFF;
for (uint32_t i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 1) crc = (crc >> 1) ^ CRC32_POLY;
else crc >>= 1;
}
}
return crc ^ 0xFFFFFFFF;
}
bool ota_verify(void) {
uint32_t stored_crc = *(uint32_t *)(APP_END_ADDR - 4);
uint32_t computed_crc = crc32_compute(
(uint8_t *)APP_START_ADDR,
APP_END_ADDR - APP_START_ADDR - 4
);
return (stored_crc == computed_crc);
}
10. Production Metrics and Field Data
10.1 Firmware Size Budget
| Module | Flash (bytes) | RAM (bytes) | Notes |
|---|---|---|---|
| SoftDevice S132 v7.3 | 163,840 | 8,704 | Fixed overhead |
| BLE advertising + config GATT | 12,480 | 1,536 | Adv sets, scan response, GATT server |
| Sensor drivers (LIS2DH12 + temp) | 5,120 | 768 | I2C/SPI + FIFO handling |
| NVS + wear leveling | 3,840 | 512 | Config + sensor log ring buffer |
| Event system + timers | 2,560 | 384 | Event queue + app_timer wrappers |
| Watchdog + fault recovery | 1,280 | 128 | WDT + fault log + reset handler |
| OTA bootloader (separate) | 8,192 | 256 | DFU + CRC + ECDSA verify |
| Application main + utils | 4,608 | 640 | State machine + config logic |
| Total | ~201,920 | ~12,928 | 39% flash, 20% RAM |
10.2 Cold Boot Timing
| Phase | Duration | Cumulative | Current |
|---|---|---|---|
| Reset vector -> MBR -> SoftDevice init | 120 ms | 120 ms | 4.5 mA |
| Application init (clocks, GPIO, I2C) | 8 ms | 128 ms | 3.8 mA |
| NVS load (config + calibration) | 15 ms | 143 ms | 3.2 mA |
| Sensor init (LIS2DH12 boot + self-test) | 5 ms | 148 ms | 1.2 mA |
| SoftDevice adv set configure | 2 ms | 150 ms | 4.5 mA |
| First advertising event | 0.6 ms | 150.6 ms | 5.2 mA |
| Enter sleep (WFE) | 0.001 ms | 150.6 ms | 1.5 uA |
From power-on to first advertisement: 150.6 ms. Cold boot energy: ~2.0 mJ (dominated by SoftDevice init). This is negligible, even if the beacon reboots daily, it costs 2.0 mJ/day vs. 1,347 mJ/day normal operation.
10.3 Field Reliability Data (12-Month Deployment)
| Metric | Target | Field Result (n=2,847 units) |
|---|---|---|
| Mean time between resets | > 6 months | 11.3 months (median) |
| Watchdog-triggered resets | < 1% | 0.3% (8.5 resets/year avg) |
| Unexpected advertising gap (> 30s) | < 0.1% | 0.04% |
| OTA success rate | > 99% | 99.7% (12 failures in 4,200 updates) |
| Battery life (CR2032, 1s interval) | > 24 months | 28.4 months (median, 23C) |
| Battery life (CR2032, 1s, 0C) | > 18 months | 19.7 months (median) |
| Config mode auto-exit failures | 0 | 0 (5-min timer never failed) |
11. Common Firmware Pitfalls and Their Fixes
| Pitfall | Symptom | Root Cause | Fix |
|---|---|---|---|
| Floating GPIO pins | +8-15 uA sleep current | Unconfigured GPIO causes input buffer leakage | Set all unused pins to pull-down or output low |
| RTC2 left running | +0.3 uA sleep current | App timer not stopped before sleep | Use app_timer_pause() or ensure no pending timers |
| SPI bus not released | +0.5 mA sleep current | Clock pin left high after sensor read | Explicitly disable SPI peripheral after each transaction |
| SAADC left enabled | +0.2 uA sleep current | Battery voltage ADC not shut down | Call nrf_drv_saadc_uninit() after each read |
| SoftDevice not sleeping | +200 uA average | Connection event length too long | Reduce event_length to minimum (320 = 4ms) |
| Logging to flash too often | Premature flash wear | Sensor log every 1s = 31M writes/year | Batch logs in RAM, flush every 5 minutes |
| No watchdog feed in sleep | Random resets | WDT running during 10s+ sleep intervals | Set WDT timeout > 2x max sleep interval |
| Blocking delay in ISR | BLE timing violations | nrf_delay_ms() in GPIO handler | Post event, process in main loop |
| Stack overflow | Hard fault at random intervals | Large local arrays in event handlers | Use static buffers, enable MPU stack guard |
| NVS corruption on brownout | Beacon boots with wrong config | Flash write interrupted by BOR | Use double-buffered NVS with write-ahead log |
12. Debug and Diagnostics: Logging Without Killing Battery
Debug logging is essential during development but must be completely disabled in production. UART at 115200 baud draws 3.5 mA continuously, more than 1000x your sleep current. The strategy: use compile-time log levels and a RAM-based circular buffer for production diagnostics.
// Production-safe logging: RAM ring buffer, no I/O
#define LOG_BUF_SIZE 64
typedef struct __packed {
uint8_t type;
uint8_t data[3];
uint32_t timestamp;
} log_entry_t;
static log_entry_t log_buf[LOG_BUF_SIZE];
static volatile uint16_t log_idx = 0;
void log_event(uint8_t type, uint32_t data) {
log_entry_t *e = &log_buf[log_idx % LOG_BUF_SIZE];
e->type = type;
e->data[0] = (data >> 16) & 0xFF;
e->data[1] = (data >> 8) & 0xFF;
e->data[2] = data & 0xFF;
e->timestamp = NRF_RTC1->COUNTER;
log_idx++;
}
#ifdef DEBUG_BUILD
#define LOG_DBG(type, data) log_event(type, data)
#else
#define LOG_DBG(type, data) // No-op in production
#endif
#define LOG_ERR(type, data) log_event(type, data) // Always log errors
This RAM log costs zero energy (no flash writes, no I/O) and survives soft resets. When a user connects via GATT for configuration, the app can read out the last 64 events for diagnostics. If the beacon hard-resets (watchdog), the RAM log is lost. For those cases, use the NVS fault log (1 write per fault, max 10,000 faults before wear-out).
13. Summary: Architecture Checklist for Production Beacon Firmware
| Category | Checklist Item | Target |
|---|---|---|
| Power | Sleep current with SoftDevice + app | < 2.5 uA (System ON, RAM retained) |
| Power | All unused GPIO configured | Pull-down or output low, zero floating |
| Power | Peripherals disabled in sleep | SAADC, SPI, TWI, UART all uninit’d |
| Timing | WDT timeout vs. max sleep interval | WDT > 2x longest sleep period |
| Timing | RTC drift compensation | +/-20 ppm crystal, compensated in firmware |
| Events | ISR execution time | < 50 us per interrupt, no blocking |
| Events | Event queue depth | >= 16 entries, overflow flagged not blocked |
| Fault | Watchdog coverage | 2 channels (app + SoftDevice), runs in sleep |
| Fault | Brownout threshold | 1.7V (lowest), soft BOR at 2.0V |
| Memory | Stack size + MPU guard | >= 2 KB stack, MPU region for overflow detect |
| Memory | NVS wear leveling | >= 10-year flash endurance at target write rate |
| OTA | CRC + optional signature | CRC32 mandatory, ECDSA if security required |
| OTA | Config mode timeout | <= 5 minutes, auto-revert to beacon mode |
| Debug | Production log overhead | 0 uA (RAM-only, no I/O in production) |
| Debug | Fault log in NVS | Last 10 faults, 1 write per fault |
Beacon firmware is fundamentally about doing almost nothing, almost all of the time, without ever failing. Every microampere you save in sleep mode translates directly to months of additional field life. Every millisecond you shave off ISR latency reduces the chance of BLE timing violations under interference. And every fault path you cover, brownout, stack overflow, NVS corruption, OTA failure, prevents a support ticket that costs more than the beacon itself. Get the architecture right, and your Bluetooth Beacon will outlast its battery warranty with zero field returns.