Embedded and Hardware Testing Questions
Testing software that runs on or against hardware and embedded systems. Covers hardware simulation and mock interfaces, resilience and reliability in constrained environments, field diagnostics, and preventing embedded-specific failure modes. Includes validating firmware/hardware interactions where direct observation is limited.
Implement a lock-free single-producer single-consumer circular buffer in C suitable for an ISR producer and a thread consumer. Provide the function prototypes and indicate which operations must be atomic or volatile. Constraints: buffer size is power of two, no malloc, no mutexes, and minimal interrupt disabling.
Sample Answer
Approach (brief)
Use a power-of-two sized ring with head (write index) updated by the ISR producer and tail (read index) updated by the thread consumer. For SPSC single-word indices on typical MCUs, plain aligned uint32_t reads/writes are atomic; mark them volatile to prevent compiler reordering. No mutexes or malloc; minimal interrupt disabling is not required for correctness in true SPSC if single-word atomicity holds.
Function prototypes
void rb_init(uint8_t *buf, size_t size_pow2);
bool rb_push_from_isr(const uint8_t *data, size_t len); // called in ISR
size_t rb_pop(uint8_t *out, size_t maxlen); // called in thread
bool rb_is_empty(void);
bool rb_is_full(void);
Which variables must be volatile/atomic
- volatile uint32_t head; // written by ISR, read by consumer
- volatile uint32_t tail; // written by consumer, read by ISR
- buffer array (normal)
If C11 atomics are available, use atomic_uint_fast32_t for head/tail.
Implementation (core operations)
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
static uint8_t *rb_buf;
static size_t rb_mask; // size - 1 (power of two)
static volatile uint32_t rb_head; // producer (ISR) index
static volatile uint32_t rb_tail; // consumer index
void rb_init(uint8_t *buf, size_t size_pow2) {
rb_buf = buf;
rb_mask = size_pow2 - 1;
rb_head = rb_tail = 0;
}
bool rb_push_from_isr(const uint8_t *data, size_t len) {
uint32_t head = rb_head; // read volatile once
uint32_t tail = rb_tail; // read volatile once
uint32_t free = (tail + (rb_mask + 1) - head - 1) & rb_mask;
if (len > free) return false;
for (size_t i = 0; i < len; ++i) {
rb_buf[(head + i) & rb_mask] = data[i];
}
/* Ensure buffer writes complete before publishing head.
On most compilers a compiler barrier suffices: */
__asm__ volatile ("" ::: "memory");
rb_head = (head + len) & rb_mask;
return true;
}
size_t rb_pop(uint8_t *out, size_t maxlen) {
uint32_t head = rb_head; // producer index (volatile read)
uint32_t tail = rb_tail;
uint32_t available = (head - tail) & rb_mask;
size_t toread = (maxlen < available) ? maxlen : available;
for (size_t i = 0; i < toread; ++i) {
out[i] = rb_buf[(tail + i) & rb_mask];
}
__asm__ volatile ("" ::: "memory");
rb_tail = (tail + toread) & rb_mask;
return toread;
}
bool rb_is_empty(void) { return rb_head == rb_tail; }
bool rb_is_full(void) { return ((rb_head + 1) & rb_mask) == rb_tail; }
Key reasoning and safety
- SPSC guarantees no concurrent writers to same index: ISR updates head only; consumer updates tail only.
- Volatile prevents compiler reordering; the memory barrier ensures data is visible before head/tail publish.
- If target CPU has non-atomic word writes or multiple cores, use C11 atomics or disable interrupts briefly when publishing indices.
- No dynamic memory; buffer provided by caller sized to power-of-two.
Edge cases
- Ensure buffer pointer alignment and that size is power of two.
- If ISR can preempt during multi-byte push, ensure push is done in ISR context atomically; else push single byte per ISR or disable interrupts briefly during multi-byte publish.
Implement in C an exponential backoff that persists its retry count across a reboot using a small 32-byte wear-friendly flash slot. Describe how to minimize wear and ensure the retry counter is consistent after power loss (no partial-write corruption leading to wrong count).
Sample Answer
Approach (brief)
Use an append-only circular log inside the 32‑byte flash slot. Each entry is written once (no in-place overwrite) until the slot is full, then erase once. Make each entry self-validating so a power-loss mid-write never yields a bogus counter: include the counter, a CRC8, and write a final 1‑byte “valid” marker as the last byte of the entry — write the marker last (or flip from 0xFF to a non‑FF), so incomplete entries are ignored on boot.
- Entry size: 4 bytes (uint16_t counter, uint8_t crc8, uint8_t marker) → 8 entries in 32 bytes.
- Wear minimization: append-only writes, erase only when all entries used.
- On boot: scan entries from start, accept the last fully valid entry (crc and marker ok) as current counter.
Code (illustrative, hardware flash primitives abstracted):
#include <stdint.h>
#include <stdbool.h>
#define SLOT_ADDR 0x10000 // example flash base
#define SLOT_SIZE 32
#define ENTRY_SIZE 4
#define ENTRIES (SLOT_SIZE/ENTRY_SIZE)
#define MARKER_VALID 0x7E
// Hardware primitives (implement for target)
int flash_read(uint32_t addr, void *buf, size_t len);
int flash_program(uint32_t addr, const void *buf, size_t len); // can only clear bits 1->0
int flash_erase_slot(uint32_t addr, size_t len); // erases to 0xFF
static uint8_t crc8(const uint8_t *p, size_t n) {
uint8_t crc = 0xFF;
for (size_t i=0;i<n;i++){
uint8_t d = p[i];
for (int b=0;b<8;b++){
crc ^= d;
d >>= 1;
if (crc & 1) crc = (crc >> 1) ^ 0x8C;
else crc >>= 1;
}
}
return crc;
}
typedef struct { uint16_t cnt; uint8_t crc; uint8_t mark; } entry_t;
static int read_last_valid(entry_t *out) {
entry_t e;
int last = -1;
for (int i=0;i<ENTRIES;i++){
flash_read(SLOT_ADDR + i*ENTRY_SIZE, &e, ENTRY_SIZE);
if (e.mark != MARKER_VALID) break; // marker written last so stop at first invalid
uint8_t c = crc8((uint8_t*)&e, sizeof(e)-2); // crc over cnt only
if (c != e.crc) break;
last = i;
}
if (last >= 0) {
flash_read(SLOT_ADDR + last*ENTRY_SIZE, out, ENTRY_SIZE);
return 0;
}
return -1;
}
int persist_increment(void) {
entry_t e;
int rc = read_last_valid(&e);
uint16_t next = (rc==0) ? (e.cnt + 1) : 1;
// find next free slot
int idx = (rc>=0) ? (( (SLOT_SIZE/ENTRY_SIZE) > 0 ? ( ( ( ( ( (rc)+1 ) ) ) ) ) : 0 )) : 0;
// simple loop to find first with mark != MARKER_VALID
entry_t probe;
for (idx=0; idx<ENTRIES; idx++){
flash_read(SLOT_ADDR + idx*ENTRY_SIZE, &probe, ENTRY_SIZE);
if (probe.mark != MARKER_VALID) break;
}
if (idx==ENTRIES) {
// slot full -> erase whole slot
flash_erase_slot(SLOT_ADDR, SLOT_SIZE);
idx = 0;
}
entry_t newe = { .cnt = next, .crc = 0xFF, .mark = 0xFF };
newe.crc = crc8((uint8_t*)&newe, sizeof(newe)-2);
// program cnt+crc first, then program marker as last byte to commit
flash_program(SLOT_ADDR + idx*ENTRY_SIZE, &newe, ENTRY_SIZE-1);
flash_program(SLOT_ADDR + idx*ENTRY_SIZE + ENTRY_SIZE-1, &newe.mark, 1);
return 0;
}
Why this is safe and wear‑friendly
- Atomicity: marker written last acts as commit flag; partial writes leave marker == 0xFF and entry ignored.
- Integrity: CRC detects corrupted writes even if marker flipped incorrectly.
- Wear: only one byte-program per entry commit (plus previous bytes), slot erased infrequently (after ENTRIES writes).
- Power-loss consistency: on boot the scan picks the last fully committed valid entry; partial writes never advance counter.
Edge cases & improvements
- Use ECC or 16‑bit CRC for stronger protection if needed.
- Protect against interrupted erase by using two slots (swap) if erase reliability is a concern.
- If counters must be monotonic across many increments, increase slot size or compress (store deltas) to reduce erases.
Compare the trade-offs between using ECC memory vs running two independent redundant systems with voting (e.g., dual-mcu with majority voting) for a safety-critical embedded product. Consider cost, power, weight, detection/correction ability, complexity, and impact on MTBF and certification.
Sample Answer
Situation & summary
For a safety-critical embedded product you’re choosing between ECC memory and redundant independent MCUs with voting. Both improve reliability but address different failure modes; the right choice often combines them.
Detection / correction
- ECC memory: detects/corrects single-bit errors (e.g., SEC-DED); low latency correction in-line; cannot protect against CPU logic faults, software bugs, or multi-bit failures beyond its capability.
- Dual/Triple MCU + voting: detects divergent behavior and can mask a faulty node (TMR masks single failure with majority voting). Protects CPU, peripherals, software execution divergence, and stuck-at faults if votes run frequently.
Cost / power / weight
- ECC: minimal BOM, low power/weight, minor die or controller cost. Good for constrained systems.
- Redundant MCUs: significant increase in BOM, PCB area, connectors; higher power and weight; complexity in enclosure/mechanical design.
Complexity
- ECC: simple to enable if supported by hardware; low SW changes.
- Redundant systems: complex synchronization, deterministic voting, failover logic, watchdogs, state replication, and testing. Higher SW verification burden.
MTBF & certification
- ECC improves MTBF for memory-related faults; easy to argue in safety cases.
- Redundancy with voting can dramatically increase system-level fault tolerance and lower residual failure probability but introduces common-cause and design-common-mode risk that must be mitigated (diverse implementations, independent power domains, independent software stacks). Certification (DO-178C/ISO 26262) requires thorough hazard analysis, fault injection, and independence evidence — redundant architectures often demand more rigorous justification.
Recommendation
Use ECC for memory protection as a low-cost baseline. For high-integrity requirements, combine ECC with at least TMR/dual-redundant architecture with diverse mitigation and careful independence to meet certification targets.
Design a resilient data-sync protocol and data structures so a constrained sensor node can upload telemetry to the cloud with exactly-once semantics despite intermittent connectivity, power loss, and possible duplicate uploads from retries. Describe message identifiers, acknowledgement behavior, and recovery after interrupted transfers.
Sample Answer
Situation & goals
Design a tiny, flash-backed sync protocol providing exactly-once upload from a constrained sensor node to cloud despite intermittent connectivity, power loss, and retries.
Protocol overview
- Use a persistent journal on flash storing entries until cloud-side commit is acknowledged.
- Each logical telemetry item gets a 128-bit Id: <device_id (48)> | <epoch_counter (32)> | <seq (32)> | <crc32 (16)>. Epoch increments on reboot/bootloader detect or explicit boot epoch.
- Transport-level messages: UploadPacket { upload_id (UUID), first_seq, last_seq, payloads[] } where each payload pairs (seq, data).
- Server maintains an idempotency store keyed by (device_id, epoch_counter, seq).
Ack behavior
- Server returns CumulativeAck { upload_id, acked_up_to_seq } once it has durably stored all <= acked_up_to_seq.
- Server also returns CommitToken when data is committed to long-term store.
- Node treats CumulativeAck as ground truth to GC journal entries <= acked_up_to_seq.
- Retransmissions reuse same upload_id and same payloads for overlapping seq ranges.
Recovery after interruptions
- On reboot or power loss, bootloader restores journal index and epoch_counter.
- Node resumes by re-sending the oldest non-acked seq..seq+window-1 in new UploadPacket (same seq numbers).
- Server deduplicates via (device_id, seq) with per-entry crc and accepts once; duplicates return success with same ack.
Resource & wear considerations
- Keep journal circular with configurable retention; store only metadata in RAM. Write-on-append with power-safe flash writes (atomic sector commit + tombstone).
- Use small sliding window (e.g., 4-8) to limit RAM and packet size.
Example C struct (embedded-friendly)
typedef struct {
uint64_t device_id;
uint32_t epoch;
uint32_t seq;
uint16_t crc16;
} TelemetryId;
typedef struct {
uint8_t upload_id[16];
uint32_t first_seq;
uint32_t last_seq;
// payloads follow
} UploadPacket;
Why this gives exactly-once
- Server-side idempotency prevents double-apply of same (device, seq).
- Node only deletes journal entries after durable cumulative ack.
- Replays use identical identifiers so server recognizes duplicates and responds idempotently.
Edge cases
- Epoch rollover: server rejects seq from older epoch if newer seq exists — node must bump epoch on persistent state reset and server must accept epoched seqs.
- Partial packet accepted: server only ack contiguous sequences; node resumes from ack+1.
This design balances minimal node state, flash-backed journaling, small windowed sends, and server-side idempotency to deliver exactly-once semantics under intermittent power and connectivity.
Explain brownout detection and how it interacts with watchdog timers and RTC-driven tasks in a battery-powered embedded device. Provide a strategy for handling brownout events to avoid flash corruption and ensure safe shutdown or state preservation.
Sample Answer
Brief definition & interaction
Brownout detection (BOD/BOR) monitors supply voltage and triggers an interrupt or reset when Vcc falls below a threshold. On many MCUs it can generate either a non-maskable reset or a pre-reset interrupt; configure based on hardware. Watchdog timers (WDT) continue during low-voltage unless explicitly halted—if BOD causes a reset, the WDT ensures the system doesn't hang during recovery. RTC-driven wakeups are useful to resume periodic tasks after power returns because RTC often runs from a backup domain.
Risks
- Flash/EEPROM writes during undervoltage can corrupt contents.
- Peripheral state can be inconsistent if power fails mid-transaction.
Strategy to handle brownout safely
- Configure BOD threshold above the minimum voltage required for safe flash writes and peripheral operation.
- Prefer BOD-as-interrupt (if available) to receive a warning before forced reset; set warning margin = safe write voltage + hysteresis.
- In BOD ISR:
- Immediately stop noncritical ISRs and disable new flash/EEPROM/SD transactions.
- Stop DMA and put peripherals into safe states (flush FIFOs).
- Persist critical state atomically to safe storage:
- Use backup SRAM/RTC backup registers or small capacitor-backed RAM if available.
- If writing flash is unavoidable, use journaling: write a small consistent checkpoint and CRC, then mark valid only after completion.
- Kick/disable WDT appropriately: allow WDT to fire to ensure a clean reset if ISR cannot finish.
- Use a small hold-up capacitor or supercapacitor to provide enough energy for the ISR + one checkpoint write.
- On reboot:
- Validate checkpoint with CRC; resume or roll back incomplete operations.
- Ensure bootloader checks for corrupted flash sections and recovers.
Best practices
- Make flash writes idempotent and quick; avoid long erase operations in normal runtime.
- Test brownout scenarios: threshold sweep, timing margins, and storage integrity.
- Document power budget for safe shutdown path and tune thresholds accordingly.
This approach avoids flash corruption, preserves essential state, and leverages RTC wakeups for orderly recovery.
Unlock Full Question Bank
Get access to all 39 Embedded and Hardware Testing interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.