Debugging, Testing and Production Firmware
Hardware debugging, Firmware testing, Reliability, Security, Performance,...

Phase 5: Debugging, Testing and Production Firmware
Study notes for embedded firmware engineers. This phase is about turning code that "works on my desk" into firmware that is observable, tested, reliable, updatable, secure, efficient and maintainable once it ships in thousands of devices.
Table of Contents
- Topic 20: Debugging
- Topic 21: Hardware Debugging
- Topic 22: Firmware Testing
- Topic 23: Reliability
- Topic 24: Firmware Updates
- Topic 25: Security
- Topic 26: Performance Optimization
- Topic 27: Coding Standards
- Quick Revision Cheat Sheet
How the Topics Fit Together
Write code βββΊ Debug (20,21) βββΊ Test (22) βββΊ Harden (23) βββΊ Ship & Update (24)
β² β
β βΌ
Standards (27) βββββββ Optimize (26) βββββββββββ Secure (25) ββββββββββ
Debugging finds problems, testing stops them coming back, reliability keeps the device alive in the field, updates let you fix it remotely, security stops attackers abusing all of that, optimization makes it fit and run well, and coding standards keep the whole thing maintainable.
Topic 20: Debugging
Debugging means finding why the firmware behaves differently from what you intended. On a microcontroller you cannot just printf everything: timing is tight, memory is small and there is often no screen. So you use a debugger connected to the chip through a debug probe.
20.1 Breakpoints
A breakpoint stops the CPU at a chosen point so you can look around.
| Type | How it works | Notes |
|---|---|---|
| Hardware breakpoint | Uses comparator registers inside the CPU (e.g. the Cortex-M FPB unit). | Works in flash. Limited count (often 4 to 8). |
| Software breakpoint | The debugger replaces an instruction with a breakpoint instruction (BKPT). | Needs writable memory (RAM). Hard in flash. |
| Conditional breakpoint | Stops only when an expression is true, e.g. i == 100. | Slows execution because the debugger checks each hit. |
| Data breakpoint / watchpoint | Stops when a memory address is read or written. | Uses the DWT unit on Cortex-M. Great for finding who corrupts a variable. |
| Temporary breakpoint | Removed after the first hit. | Useful for "run to here". |
Tip: Since flash breakpoints are limited, remove ones you no longer need.
20.2 Watch Expressions
A watch expression is a variable or expression whose value the debugger shows and refreshes whenever the CPU halts.
- Watch simple variables:
motor_speed - Watch expressions:
buffer[idx] + 1 - Watch structs, arrays and pointers, and expand them to see members
- Watch peripheral registers:
GPIOA->ODR - Live watch (supported by some tools) updates while the CPU runs, without halting
Caveat: Variables the compiler optimized away show as <optimized out>. Mark them volatile, or lower the optimization level while debugging.
20.3 Stepping
| Command | What it does |
|---|---|
| Step Into | Executes one line and enters any function called on it. |
| Step Over | Executes one line and treats function calls as a single step. |
| Step Out | Runs until the current function returns. |
| Run to Cursor | Runs until a chosen line. |
| Instruction step | Steps one machine instruction at a time (in disassembly view). |
| Continue / Resume | Runs until the next breakpoint. |
Caveat: Stepping through code that depends on timing (communication, timers, motor control) can break it. A stopped CPU does not stop the outside world, so a UART may overflow or a motor may keep running. Use trace or logging for these cases.
20.4 Call Stack
The call stack shows the chain of functions that led to the current point.
main()
ββ sensor_task()
ββ read_adc()
ββ adc_wait_ready() βββ you are here
- Each entry is a stack frame holding the return address, saved registers and local variables.
- Clicking a frame lets you inspect that function's locals.
- Useful for answering "who called this with a bad value?"
- On Cortex-M, the stack grows downward and uses the MSP (main stack pointer) or PSP (process stack pointer, typically used by RTOS tasks).
- Stack overflow shows up as a corrupted or nonsense call stack, or as a fault.
20.5 Register Inspection
Registers come in two groups:
- CPU core registers: R0 to R12, SP (R13), LR (R14), PC (R15), xPSR. The PC tells you where the CPU is, and LR tells you where a function will return.
- Peripheral registers: GPIO, UART, SPI, timers, ADC and so on. The debugger reads them from an SVD file (System View Description) that names every register and bit field.
Why it matters: many bugs are "I thought I enabled the clock or configured the pin, but the register says otherwise." Reading the register tells you the truth.
Caveat: Reading some registers has side effects (for example, reading a UART data register may clear a flag). Be careful when auto-refresh is on.
20.6 Memory Inspection
A memory view shows raw bytes at any address.
Use it to:
- Check buffers, arrays and DMA targets
- Inspect the stack for corruption
- Look for stack canary patterns (e.g.
0xDEADBEEF) to measure stack usage - Verify flash contents and the vector table
- Spot memory-mapped peripherals at their addresses
Common patterns:
- Fill unused stack with a known value (like
0xA5) at startup. Later, the amount of that pattern still intact shows how much stack is unused (the high-water mark). - Look for unexpected
0xFF(erased flash) or0x00.
20.7 Fault Analysis
When the CPU does something illegal it triggers a fault exception. On Cortex-M the main ones are:
| Fault | Typical cause |
|---|---|
| HardFault | Catch-all. Escalated from other faults, or a bus error during exception handling. |
| MemManage fault | MPU violation, or access to forbidden memory. |
| BusFault | Invalid address, or access to a peripheral with no clock. |
| UsageFault | Undefined instruction, unaligned access, divide by zero (if enabled), bad EXC_RETURN. |
How to analyze a fault
- Break in the fault handler. Do not just loop forever there. Capture information first.
- Read the stacked registers. On exception entry, the CPU pushes R0-R3, R12, LR, PC and xPSR onto the stack. The stacked PC is the instruction that faulted.
- Read the fault status registers:
- CFSR (Configurable Fault Status Register): shows exactly which fault bit is set
- HFSR (HardFault Status Register)
- MMFAR / BFAR: the address that caused a memory or bus fault, if valid
- Map the PC to source code using the map file,
addr2line, or the disassembly view. - Reason about the cause: null pointer, stack overflow, misaligned access, wrong peripheral clock, corrupted function pointer, and so on.
A typical fault handler skeleton:
void HardFault_Handler(void) {
__asm volatile (
"tst lr, #4 \n" // which stack was in use?
"ite eq \n"
"mrseq r0, msp \n"
"mrsne r0, psp \n"
"b hard_fault_c \n"
);
}
void hard_fault_c(uint32_t *stack) {
volatile uint32_t pc = stack[6];
volatile uint32_t lr = stack[5];
volatile uint32_t cfsr = SCB->CFSR;
volatile uint32_t bfar = SCB->BFAR;
// log these to non-volatile memory, then reset or halt
(void)pc; (void)lr; (void)cfsr; (void)bfar;
for (;;) { }
}
20.8 General Debugging Strategy
- Reproduce the bug reliably.
- Isolate it (which module, which input, which condition).
- Form a hypothesis, then test it with one change at a time.
- Use the cheapest tool first (print/log), escalating to debugger, trace, then hardware tools.
- Fix, then add a test so it cannot return.
Topic 21: Hardware Debugging
Sometimes the bug is not in the code but in the hardware, the wiring or the signals. This topic covers the tools and interfaces for looking at the real world.
21.1 Debug Interfaces: JTAG and SWD
Both are how a debug probe (ST-Link, J-Link, CMSIS-DAP and similar) talks to the chip.
| Feature | JTAG | SWD (Serial Wire Debug) |
|---|---|---|
| Standard | IEEE 1149.1 | ARM-specific |
| Pins | TCK, TMS, TDI, TDO (+ optional nTRST) | SWDIO, SWCLK (+ optional SWO) |
| Pin count | 4 to 5 | 2 |
| Daisy-chaining several chips | Yes | Limited (multi-drop in newer versions) |
| Boundary scan (testing PCB connections) | Yes | No |
| Typical use | Complex boards, multi-chip, production test | Most Cortex-M designs |
Extra notes:
- SWO (Serial Wire Output) is a one-way pin that streams trace data such as
printf-style output (ITM) and profiling data, without halting the CPU. - Reset line (nRST) is usually wired too, so the debugger can reset the target and connect "under reset" when the firmware has locked the pins.
- Common connection problems: debug pins reused as GPIO by firmware, a chip in deep sleep, readout protection enabled, wrong voltage level, or a bad ground.
- Standard connectors: 10-pin Cortex Debug (1.27 mm), 20-pin ARM, and Tag-Connect for space-constrained boards.
21.2 Multimeter
The simplest tool, and often the first one to reach for.
- Voltage: check power rails (3.3 V, 5 V, battery), reference voltages and pull-ups.
- Continuity: check solder joints, broken traces and shorts.
- Resistance: verify resistors and pull-up/pull-down values (power off!).
- Current: check consumption (in series with the load) for sleep and active modes.
- Limitation: it shows average values only and is far too slow for fast signals.
21.3 Oscilloscope
Shows voltage versus time, letting you see the real analog shape of a signal.
Key parameters:
- Bandwidth: should be several times (commonly 5x) higher than the fastest signal you measure.
- Sample rate: how many samples per second.
- Probe types and grounding: 1x/10x probes, and keep the ground lead short to avoid ringing.
- Trigger: edge, pulse width, serial decode. This holds the display stable on the event of interest.
What you check with it:
- Power supply ripple and noise, and brown-out dips at startup or when a motor kicks in
- Clock quality and start-up time of crystals
- Rise/fall times, overshoot, ringing on fast digital lines
- PWM duty cycle and frequency
- Analog sensor signals before the ADC
- Pulse timing (e.g. toggle a GPIO at the start and end of a function and measure how long it takes)
21.4 Logic Analyzer
Records many digital channels (typically 8 to 32) at once and decodes them.
- Shows 1s and 0s over time on many pins at the same time
- Protocol decoders for I2C, SPI, UART, CAN, 1-Wire and more, showing addresses, data bytes, ACK/NACK and errors
- Ideal for "is my SPI sending the right bytes in the right order?" or "does the I2C device ACK?"
- Does not show analog quality (use a scope for that)
- Pick a sample rate at least 4 to 10 times the signal frequency
Scope vs logic analyzer:
| Oscilloscope | Logic Analyzer | |
|---|---|---|
| Channels | 2 to 4 | 8 to 32+ |
| Signal view | Analog detail | Digital 0/1 |
| Protocol decoding | Some models | Core feature |
| Best for | Signal integrity, power | Bus protocols, sequencing |
21.5 Serial Terminals
A serial terminal (PuTTY, Tera Term, minicom, screen, or a logging tool) connects over a USB-to-UART adapter to the firmware's UART.
- Used for debug logs, command-line interfaces and boot messages
- Settings must match on both sides: baud rate, data bits, parity, stop bits (e.g. 115200 8N1)
- Garbled characters nearly always mean a baud-rate or clock mismatch
- Remember to share GND between the board and the adapter, and match voltage levels (3.3 V vs 5 V)
- Logging over UART takes time. Heavy
printfcan change timing and hide or create bugs.
21.6 Signal Timing
Many embedded bugs are timing bugs. Things to measure:
- Setup and hold times for buses and chips (from the datasheet)
- Interrupt latency: time from the event to the first line of the ISR
- Function execution time (GPIO toggle trick, or the DWT cycle counter)
- Jitter: variation in period for periodic tasks
- Bus speed and clock stretching in I2C
- Baud-rate error in UART (keep it under about 2 to 3 percent)
- Debounce and glitch behavior on inputs
The GPIO toggle trick:
GPIO_SET(DEBUG_PIN); // start of section
do_work();
GPIO_CLEAR(DEBUG_PIN); // end of section
// Measure the pulse width on a scope or logic analyzer.
21.7 Which Tool When?
| Symptom | First tool |
|---|---|
| Board is dead | Multimeter (power rails), then scope |
| Random resets | Scope on the supply (brown-out), plus reset cause register |
| Sensor reads wrong values | Logic analyzer on the bus, scope on the analog line |
| Peripheral never responds | Logic analyzer (is the address ACKed?) |
| Code is slow or late | GPIO toggle plus scope, or profiling |
| Can't connect debugger | Check SWD wiring, power, reset, readout protection |
| Garbled serial output | Check baud rate and clock configuration |
Topic 22: Firmware Testing
Testing proves firmware behaves correctly and keeps behaving correctly as it changes.
22.1 The Test Pyramid for Firmware
/\
/ \ System / HIL tests (few, slow, real hardware)
/----\
/ \ Integration tests (some)
/--------\
/ \ Unit tests (many, fast, run on a PC)
/____________\
More tests at the bottom (cheap and fast), fewer at the top (expensive and realistic).
22.2 Unit Testing
Tests a single function or module in isolation.
- Usually runs on the host PC (not the target), so it is fast and can run automatically in CI
- Popular frameworks: Unity, CMock, Ceedling, CppUTest, GoogleTest
- Pattern: Arrange, Act, Assert
void test_checksum_of_known_data(void) {
uint8_t data[] = {0x01, 0x02, 0x03}; // Arrange
uint8_t result = calc_checksum(data, 3); // Act
TEST_ASSERT_EQUAL_UINT8(0x06, result); // Assert
}
Good unit-test targets: parsers, state machines, math, ring buffers, protocol encoders/decoders, and CRC code.
Design tips for testability:
- Keep hardware access in a thin layer (HAL or driver) separate from logic
- Pass dependencies in (function pointers or interfaces) rather than hard-coding register access
- Avoid hidden global state
Test edge cases: zero, maximum values, empty buffers, overflow, invalid input, and boundary conditions.
22.3 Test Stubs and Mocks
Used to replace real hardware or modules so the code under test can run anywhere.
| Term | Meaning | Example |
|---|---|---|
| Stub | A fake that returns fixed values. | adc_read() always returns 512. |
| Mock | A fake that also records and verifies calls (how many times, what arguments, what order). | Verify that uart_send() was called once with "OK". |
| Fake | A simplified working implementation. | An in-memory flash driver using a RAM array. |
| Spy | A wrapper that records calls but passes through to the real code. |
Why use them:
- Test error paths that are hard to produce on real hardware (flash write failure, sensor timeout)
- Make tests deterministic (no real time or randomness)
- Develop before hardware exists
Tools like CMock can auto-generate mocks from header files.
22.4 Integration Testing
Tests how modules work together (driver plus protocol layer plus application logic).
- Catches interface mismatches, wrong assumptions, ordering problems and shared-resource conflicts
- Can run on the host (with simulated hardware) or on the target
- Examples: sensor driver feeding a filter feeding a controller; bootloader handing control to the application; a communication stack with a real UART loopback
- Test timing-related behavior, interrupts, RTOS tasks and queues here
22.5 Hardware-in-the-Loop (HIL) Testing
The real firmware runs on the real target hardware, while a test rig simulates the outside world.
Test PC / Runner βββΊ Test Rig (DAQ, relays, DAC, CAN/UART/GPIO injectors)
β stimulates inputs, measures outputs
βΌ
Device Under Test (real MCU board)
- The rig can inject sensor signals, press buttons, drop power, send bus messages and measure outputs
- Tests real timing, peripherals, interrupts, power behavior and drivers
- Catches problems that host tests cannot, such as clock configuration, DMA behavior and electrical interactions
- Downsides: needs hardware, is slower, costs more and is harder to scale. Use it for the most important scenarios, and run it automatically (nightly or on release) via a CI runner.
22.6 Regression Testing
Re-running existing tests after every change to make sure old behavior did not break.
- Every fixed bug should get a test that reproduces it, so it never returns
- Automate with CI (Continuous Integration): each commit triggers build, static analysis, unit tests and, where possible, HIL tests
- Keep a golden-output or known-good baseline for comparisons
- Track code coverage (statement, branch and, for safety-critical work, MC/DC). Coverage shows what is untested, but high coverage does not prove good tests.
22.7 Other Useful Testing Ideas (within this topic)
- Boundary and negative testing: feed invalid data
- Fault injection: force errors (flash fail, bus error) to test recovery
- Stress and soak testing: run for long durations to find leaks, overflow and drift
- Test the failure paths such as watchdog, brown-out and update-interrupt behavior
Topic 23: Reliability
A product in the field must keep working, or fail safely and recover, even with noise, power glitches and software bugs.
23.1 Watchdog Recovery
A watchdog timer (WDT) is a hardware counter that resets the MCU if the firmware does not "kick" (refresh) it in time.
Normal: run β kick β run β kick β run β kick ...
Hang: run β (stuck!) ... timeout β RESET
Types:
- Independent watchdog (IWDG): has its own clock, so it keeps working even if the main clock fails. Cannot be stopped once started.
- Window watchdog (WWDG): must be kicked inside a time window. Kicking too early or too late causes a reset. This detects both hangs and runaway fast loops.
- External watchdog: a separate chip. It survives total MCU failure.
Best practices:
- Do not kick the watchdog from a timer interrupt because the interrupt may keep running while the main program is dead.
- Kick only when all critical tasks have proven they are alive (a "task heartbeat" or checkpoint scheme).
- Choose a timeout longer than the worst-case healthy loop, but short enough to recover quickly.
- After a watchdog reset, record the reason and take sensible action, not just blindly restart. Consider a safe state if resets repeat (a "reset loop").
- Pause or extend the watchdog carefully during long operations like flash erase, instead of disabling it.
23.2 Brown-Out Detection
A brown-out happens when supply voltage dips below the safe level but not to zero, which can cause unpredictable CPU, flash and peripheral behavior.
- Brown-out reset (BOR): hardware that holds the MCU in reset when VDD falls below a threshold, and releases it once the voltage is stable
- Programmable voltage detector (PVD): raises an interrupt before the voltage gets too low, giving firmware a few microseconds to save critical data or shut outputs safely
- Choose the BOR threshold to match the minimum operating voltage for the clock speed and flash
- Common causes: weak battery, motors starting, long cables, poor regulators and insufficient decoupling
- Dangerous scenario: a brown-out during a flash write, which can corrupt data. Mitigate with power-fail detection, hold-up capacitors and safe write schemes.
23.3 Reset Causes
Most MCUs have a reset status register telling you why the last reset happened.
| Reset cause | Meaning |
|---|---|
| Power-on reset (POR) | First power application |
| Brown-out reset (BOR) | Voltage dropped too low |
| Watchdog reset (IWDG/WWDG) | Firmware hung or missed a kick |
| Software reset | Firmware requested it (e.g. NVIC_SystemReset()) |
| External pin reset | Reset button or an external supervisor |
| Low-power reset | Wake or reset from low-power mode |
| Lockup / fault reset | CPU entered an unrecoverable state |
Good practice:
- Read the reset flag register early at boot
- Log it (for example to non-volatile memory or a retained RAM area)
- Clear the flags so the next reset is correctly identified
- Decide behavior based on the cause (e.g. treat a watchdog reset differently from a power-on reset, and count repeated resets)
23.4 Fault Handling
Plan what the system does when something goes wrong.
- Detect: error codes, range checks, CRC checks, timeouts and hardware fault exceptions
- Contain: stop the bad action (disable outputs, close valves, stop motors)
- Recover: retry, reinitialize a peripheral, restart a task or reset the system
- Report: log the error and, if possible, notify the user or host
- Fail-safe state: every output should have a defined safe default (e.g. heater OFF, motor stopped)
Strategies:
- Graceful degradation: keep partial function (e.g. use a backup sensor)
- Retry with limits and back-off: do not retry forever
- Redundancy: duplicate critical data or sensors, use checksums, or store two copies of critical config
- Fault handlers (HardFault and others) should capture state, log it and put the system in a safe state before resetting
23.5 Defensive Programming
Writing code that expects things to go wrong.
- Validate all inputs (function parameters, sensor values, received packets)
- Check return values of every function that can fail
- Use assertions for conditions that "should never happen" (and decide what they do in production)
- Initialize all variables. Avoid uninitialized memory.
- Guard against buffer overflows, array index errors and integer overflow
- Use timeouts on every wait loop (never
while(!flag);without an exit) - Use
volatilecorrectly for hardware registers and ISR-shared variables - Protect shared data (critical sections, atomic access, mutexes)
- Prefer static allocation over dynamic memory (
malloc) in production firmware - Use enumerations with a default case in
switchstatements, and handle impossible states - Add stack and heap guards, and an MPU where available
- Check and protect against power-loss during critical writes (write-then-verify, journaling, double-buffering)
// Defensive example
status_t read_sensor(uint16_t *out) {
if (out == NULL) return ERR_NULL_PTR; // validate input
uint32_t t0 = millis();
while (!sensor_ready()) {
if (millis() - t0 > SENSOR_TIMEOUT_MS) // never wait forever
return ERR_TIMEOUT;
}
uint16_t raw = sensor_raw();
if (raw > SENSOR_MAX) return ERR_RANGE; // sanity check
*out = raw;
return OK;
}
23.6 Error Logging
When a device fails in the field, logs are often the only evidence.
What to log:
- Error code and module
- Timestamp or uptime counter
- Reset cause and reset count
- Fault registers (PC, LR, CFSR) from crashes
- Firmware version and key state values
Where to log:
- Retained RAM (survives a soft reset)
- Flash/EEPROM (survives power loss, but has limited write cycles. Use wear-leveling and a circular log.)
- UART/debug port, or over the network to a server
Guidelines:
- Use severity levels (DEBUG, INFO, WARN, ERROR, FATAL)
- Keep logging lightweight and non-blocking so it does not disturb timing
- Use compact error codes instead of long strings to save space
- Rate-limit repeated messages
- Avoid logging sensitive data
Topic 24: Firmware Updates
Devices need bug fixes and new features after deployment. A safe update mechanism is essential, because a failed update can leave a device bricked (unusable).
24.1 Bootloaders
A bootloader is a small program that runs first after reset and decides what to do next: start the application, or load a new one.
Flash memory layout (example)
βββββββββββββββββββββββββ 0x0800_0000
β Bootloader β (small, rarely changed, often protected)
βββββββββββββββββββββββββ€
β Config / metadata β (version, flags, CRC)
βββββββββββββββββββββββββ€
β Application (slot A) β
βββββββββββββββββββββββββ€
β Application (slot B) β (optional: backup / new image)
βββββββββββββββββββββββββ
What a bootloader typically does:
- Initialize minimum hardware
- Check whether an update is requested or pending
- Validate the application (CRC, signature, version)
- Receive and write a new image if needed (UART, USB, CAN, I2C, SPI, Ethernet, wireless)
- Jump to the application: set the vector table (
SCB->VTOR), load the stack pointer from the app's vector table, and branch to its reset handler
Important details:
- The application must be linked to start at its own address, not at the flash base
- De-initialize peripherals and disable interrupts before jumping
- Protect the bootloader region from accidental overwrites (write protection)
24.2 In-Application Programming (IAP)
IAP means the running application itself reprograms flash (or a part of it), as opposed to ISP (in-system programming through a ROM bootloader or external programmer) or ICP (in-circuit programming through a debug interface).
- The application receives new firmware (over UART, network and so on), stores it, then writes it to flash
- Challenge: on many MCUs, you cannot execute code from a flash bank while erasing or writing the same bank. Solutions: run the flash routine from RAM, use dual-bank flash, or hand off to a bootloader.
- Writes must follow flash rules: erase before write, page or sector alignment, and a write-size granularity
- Flash has a limited number of erase cycles
24.3 OTA (Over-the-Air) Updates
Delivering firmware remotely with no physical access: Wi-Fi, Bluetooth LE, cellular, LoRa, Zigbee and others.
Typical flow:
Server ββ(new image + metadata)βββΊ Device downloads to secondary slot
β
βΌ
Verify (size, CRC/hash, signature, version)
β
βΌ
Mark pending β reboot
β
βΌ
Bootloader swaps/boots new image β app confirms "good"
Key considerations:
- Download in chunks and be able to resume if the connection drops
- Compression and delta (differential) updates reduce data size and time
- Verify before applying, and again after boot
- Check battery level and connectivity before starting
- Staged rollout: update a small group first, then widen it
- Protect with encryption and signatures (see Topic 25)
24.4 Update Layouts: Single, Dual-Bank and Swap
| Scheme | How it works | Pros | Cons |
|---|---|---|---|
| Single-slot (in place) | New image overwrites the old one | Least flash needed | Risky. Failure may brick the device. |
| Dual-slot (A/B) | New image goes into the inactive slot, then the bootloader switches to it | Safe. Old image stays valid. | Needs about 2x flash |
| Swap with scratch | Bootloader swaps images sector by sector (as in MCUboot) | Allows rollback with a small footprint | Slower boot after update, wears flash |
| Execute-in-place from a staging area | App runs from slot A while slot B receives updates | No downtime during download | Needs space and alignment care |
24.5 Firmware Versioning
A clear version scheme lets you manage what is running and what is allowed.
- Semantic versioning:
MAJOR.MINOR.PATCH(e.g.2.4.1)- MAJOR: incompatible changes
- MINOR: new backward-compatible features
- PATCH: bug fixes
- Include build number, git commit hash and build date
- Store the version in a fixed location in the image header so the bootloader can read it
- Report version to the server or host so you know what is deployed
- Also version data formats (config, storage layout, protocols) and handle migration from older formats
- Anti-rollback counters can stop downgrades to old, vulnerable versions (see Topic 25)
- Check hardware compatibility (hardware revision or model ID) before accepting an image
24.6 Rollback
Going back to the last known good version if the new one fails.
- Keep the previous image until the new one is confirmed
- The new firmware must confirm itself ("self-test OK") after boot, usually by setting a flag. If it does not confirm within a time or boot count, the bootloader reverts.
- Use a boot counter or trial-boot flag. If the new image crashes or the watchdog fires several times, rollback happens automatically.
- Decide when to confirm: after key subsystems (network, sensors, storage) have actually worked, not merely after boot.
- Balance rollback with security: rollback to a known-vulnerable version may need to be blocked by anti-rollback rules.
24.7 Interrupted-Update Recovery
What if power or connectivity is lost mid-update?
- Never erase the working image before the new one is fully received and verified (when space allows)
- Keep the bootloader minimal, stable and protected, so it can always start
- Use state flags/metadata (e.g. "update in progress", "image valid", "image pending") written with power-safe techniques (write the flag last, use redundant copies, use a CRC)
- On boot, the bootloader checks flags and CRC, and either resumes, retries or falls back to the old image
- Resumable downloads: remember how much has been received
- A recovery mode (e.g. USB DFU or UART bootloader entry via a pin or button) offers a last-resort way to reload firmware
- Test this on purpose: cut power at random points during update, many times
Topic 25: Security
Connected devices are targets. Firmware security aims to ensure only trusted code runs, secrets stay secret, and updates cannot be abused.
25.1 The Security Goals (CIA)
- Confidentiality: secrets and data cannot be read by outsiders
- Integrity: code and data cannot be modified without detection
- Availability: the device keeps working and cannot be easily disabled
- Plus authenticity: proof that something comes from who it claims
25.2 Cryptographic Basics
| Concept | Purpose | Examples |
|---|---|---|
| Hash function | Creates a fixed-size "fingerprint" of data. Any change gives a totally different hash. Used for integrity. | SHA-256 (avoid MD5 and SHA-1) |
| Symmetric encryption | One shared secret key encrypts and decrypts. Fast. | AES (modes: CBC, CTR, GCM) |
| Asymmetric (public-key) cryptography | Key pair: private key (secret) and public key (shared). Used for signatures and key exchange. | RSA, ECDSA, Ed25519, ECDH |
| MAC / HMAC | Hash plus a shared secret to prove integrity and authenticity | HMAC-SHA256, AES-CMAC |
| Digital signature | Private key signs a hash. Anyone with the public key can verify. | ECDSA, RSA-PSS |
| Authenticated encryption (AEAD) | Encrypts and authenticates together | AES-GCM, ChaCha20-Poly1305 |
| Random number generator (RNG/TRNG) | Unpredictable numbers for keys, nonces and IVs | Hardware TRNG |
Key rules:
- Do not invent your own cryptography. Use well-tested libraries (mbedTLS, wolfSSL, TinyCrypt, hardware crypto engines).
- CRC is not security. It detects accidental errors only, and attackers can easily forge it.
- Never reuse a nonce/IV with the same key in modes like GCM or CTR.
- Compare MACs and hashes in constant time to avoid timing attacks.
- Good randomness matters. A weak RNG breaks everything built on it.
25.3 Secure Boot
Ensures the device only runs authentic, unmodified firmware.
Chain of trust:
ROM bootloader (immutable, trusted root)
β verifies signature
βΌ
Stage-1 bootloader
β verifies signature
βΌ
Application firmware
How it works:
- Firmware image is signed at build time with a private key (kept safe offline or in an HSM)
- The device stores only the public key (or its hash) in protected storage, ideally immutable (ROM or OTP/fuses)
- At boot, the bootloader computes the image's hash and verifies the signature
- If invalid, it refuses to boot (or enters recovery)
Add-ons: anti-rollback version counters, image encryption, and measured boot (recording hashes of what ran).
25.4 Firmware Integrity
Detecting whether firmware has been corrupted or tampered with.
- At boot: hash and signature check (secure boot)
- At run time: periodic checks of code regions (CRC for accidental corruption, hashes or signatures for tampering)
- During transfer: verify the whole image before accepting it
- Use memory protection (MPU, write-protected flash, execute-never regions) so data cannot be run as code and code cannot be modified at run time
25.5 Key Storage
The weakest point of most systems is where the keys live.
Bad: keys hard-coded in plain text in source code, shared across all devices, or stored in readable external flash.
Better options:
- Hardware secure element (e.g. ATECC608, SE050) or TPM
- MCU secure storage / TrustZone (Cortex-M23/M33), with keys in a secure world that normal code cannot read
- OTP / eFuse regions, and flash with readout protection
- Hardware key store that lets you use a key (sign, decrypt) without ever exposing it
- Unique per-device keys (so breaking one device does not break the whole fleet), derived with a KDF from a root key plus device ID
- Plan for key rotation and revocation
- Wipe keys from RAM after use
25.6 Debug-Port Protection
An open debug port (JTAG/SWD) gives an attacker full control: read firmware, extract keys, modify code.
Protection levels (names vary by vendor, e.g. STM32 RDP levels):
- Open: full access (development only)
- Read-out protection: debugger cannot read flash or RAM
- Permanent lock: debug disabled forever (often irreversible)
- Authenticated debug: debug only opens after a challenge-response with a secret key (allows controlled field diagnostics)
Also consider:
- Disable or protect bootloader/DFU/UART entry in production
- Disable test and manufacturing modes
- Remember tradeoffs: locking debug makes field failure analysis harder. Plan for secure diagnostic access.
- Beware of physical attacks (fault injection, glitching, side channels) for high-security products
25.7 Secure Update Mechanisms
Updates are a prime attack path. Make them trustworthy:
- Sign every image. The device verifies the signature before installing.
- Encrypt images (optional) to protect intellectual property and confidentiality in transit.
- Use secure transport (TLS/DTLS) for OTA, with certificate validation.
- Anti-rollback: a monotonic version counter stops flashing old vulnerable versions.
- Verify before erase, and verify again after writing.
- Check device and hardware compatibility metadata to prevent wrong-image installs.
- Protect update credentials and endpoints, and authenticate the update server.
- Keep the bootloader (root of trust) immutable or tightly protected.
25.8 Other Good Habits
- Principle of least privilege (only grant what is needed)
- Remove debug logs, backdoors, default passwords and unused interfaces from production builds
- Validate all external input (buffer overflow protection, bounds checks)
- Enable compiler hardening: stack protection, fortify, no-execute memory
- Keep third-party libraries updated, since many vulnerabilities come from old dependencies
Topic 26: Performance Optimization
Embedded devices have limited speed, flash, RAM and battery. Optimization balances these four, and you can rarely maximize all of them at once.
Golden rule: Measure first, optimize second. Do not guess where the time or memory is going.
26.1 Execution Time
How fast code runs.
Measure:
- DWT cycle counter (
DWT->CYCCNT) on Cortex-M for precise cycle counts - Hardware timer read before and after
- GPIO toggle and an oscilloscope or logic analyzer
- Profiler or trace tools (SWO, ETM)
Improve:
- Choose better algorithms and data structures (the biggest win)
- Avoid heavy operations: division, floating-point on MCUs without an FPU,
printf,malloc - Use lookup tables to trade memory for speed
- Use fixed-point math instead of software floating-point
- Use DMA to move data without CPU involvement
- Use hardware peripherals (CRC, AES and DSP instructions) instead of software
- Use the right data sizes (native word size is often fastest) and avoid unaligned access
- Keep ISRs short. Defer work to the main loop or a task.
- Place hot code in fast memory (RAM, TCM or cache-friendly areas), and consider flash wait states
- Inline tiny functions, and unroll loops carefully where it helps
- Raise the clock frequency (but this costs power)
26.2 Code Size (Flash Usage)
Measure: look at the map file and tools like arm-none-eabi-size, nm, objdump, or bloat analyzers.
Reduce:
- Compile with
-Os(optimize for size) -ffunction-sections -fdata-sectionswith linker--gc-sectionsto drop unused code- Link-time optimization (LTO)
- Use a lightweight C library (newlib-nano) and avoid heavy
printfandscanf, or use compact formatted-output libraries - Avoid unnecessary floating-point and C++ features (exceptions, RTTI)
- Remove unused features, libraries and debug code
- Use Thumb/Thumb-2 instruction sets (compact)
- Compress strings or use error codes instead of long text
- Reuse common code, and avoid large macros that expand everywhere
26.3 RAM Usage
RAM is usually the scarcest resource.
Measure:
- Map file sections:
.data(initialized globals),.bss(zero-initialized globals), heap, stack - Stack high-water marking (fill stack with a pattern and see how much is used)
- RTOS tools showing per-task stack use
Reduce:
- Use
constfor read-only data so it stays in flash, not RAM - Choose the smallest data types that fit (
uint8_tinstead ofint), but watch alignment and speed - Avoid dynamic allocation (prevents fragmentation and unpredictable behavior)
- Reuse buffers (union or overlay for mutually exclusive uses)
- Reduce recursion and large local arrays on the stack
- Size each RTOS task stack based on measured need plus margin
- Use packed structures carefully (they save RAM but can slow or break unaligned access)
- Use ring buffers of appropriate size instead of oversized static ones
26.4 Power Consumption
Critical for battery-operated devices.
Where power goes: CPU active time, clock speed, voltage, peripherals left on, GPIO leakage, radios, and external components.
Roughly: dynamic power depends on frequency Γ voltageΒ².
Strategies:
- Sleep as much as possible. Use low-power modes (sleep, stop/deep sleep, standby/shutdown) and wake on interrupt, timer or event.
- Race to sleep: finish work quickly at a suitable clock, then sleep. This is often more efficient than running slowly.
- Event-driven design instead of polling. Use interrupts, and avoid busy-wait loops.
- Disable unused peripherals and their clocks, and set unused pins to a defined low-leakage state.
- Lower clock speed and voltage when full performance is not needed (dynamic voltage and frequency scaling, DVFS).
- Use DMA and peripheral-autonomous operation so the CPU can sleep.
- Use low-power timers (RTC/LPTIM) for wake-ups.
- Batch radio transmissions. Radio usage often dominates the power budget.
- Choose efficient components. Check pull-up/pull-down resistor leakage.
- Measure real current: use a multimeter for averages, or a current profiler or scope with a shunt resistor to capture short spikes. Average current = sum of (current Γ time) across all states. Battery life β capacity Γ· average current.
26.5 Compiler Optimization
Common GCC/Clang optimization levels:
| Flag | Meaning |
|---|---|
-O0 | No optimization. Best for debugging, but slow and large. |
-O1 | Basic optimization |
-O2 | Good balance. Most optimizations, no big size cost. |
-O3 | Aggressive speed (may increase code size) |
-Os | Optimize for size (common for MCUs) |
-Og | Optimize while keeping debugging experience reasonable |
-flto | Link-time optimization across files |
Things to know:
- Optimization can expose latent bugs: missing
volatile, undefined behavior, race conditions. If the code works at-O0and fails at-O2, suspect these. - The compiler may remove code it thinks is useless (e.g. an empty delay loop). Use
volatileor proper timers. - Use
volatilefor hardware registers and ISR-shared data. Use memory barriers where ordering matters. - Helpful attributes:
inline,__attribute__((section(...))),__attribute__((always_inline)),__attribute__((noinline)),__attribute__((used)) - Enable the FPU and correct float ABI, and choose the right
-mcpu,-mthumband-mfpuflags - Always re-test after changing optimization settings
26.6 Profiling
Finding where time (or energy) is actually spent.
| Method | How |
|---|---|
| Manual instrumentation | Timestamp or toggle a GPIO around code sections |
| Cycle counter (DWT) | Count cycles for a function |
| Sampling profiler | Periodically record the PC (via a timer interrupt or debugger) and build a histogram of hot spots |
| Trace (ETM / SWO / SystemView) | Records exact execution flow and timing without halting |
| RTOS stats | Task CPU usage, context switches, stack use |
| Power profiler | Correlate current draw with code activity |
Process:
- Set a target (e.g. "ISR under 20 Β΅s", "fits in 64 KB", "average current under 50 Β΅A")
- Measure the baseline
- Find the biggest bottleneck (the 80/20 rule)
- Change one thing and re-measure
- Stop when the target is met. Avoid unreadable "micro-optimizations" that gain little.
Trade-offs to remember: speed vs size vs RAM vs power vs readability. Optimize for what the product actually needs.
Topic 27: Coding Standards
Rules and habits that make firmware safe, consistent, readable and maintainable across a team and over many years.
27.1 MISRA C
MISRA C is a set of coding guidelines for C, created by the Motor Industry Software Reliability Association and widely used in automotive, medical, aerospace and industrial firmware.
Why: C is powerful but has many undefined, unspecified and implementation-defined behaviors. MISRA restricts the language to a safer subset.
Key points:
- Guidelines are classified as Mandatory, Required and Advisory
- Also split into Rules (checkable in source) and Directives (broader, process and design level)
- Versions include MISRA C:2004, C:2012 (with amendments) and later updates. MISRA C++ exists for C++.
- Typical rules cover:
- No dynamic memory allocation (
malloc/free) in safety-critical code - No recursion
- Use fixed-width types (
uint8_t,int32_t) - Every
switchhas adefault, and no accidental fall-through - Avoid implicit type conversions, and use explicit casts carefully
- No use of unspecified or undefined behavior
- Every function returns a value consistently, with return values checked
- Restrictions on pointer arithmetic,
goto, unions and the preprocessor - Controlled use of
volatile - No dead or unreachable code
- No dynamic memory allocation (
- Deviations are allowed, but must be formally documented with justification
- Compliance is checked with static analysis tools
27.2 Static Analysis
Analyzing source code without running it to find bugs, rule violations and risky patterns.
Finds: null pointer dereferences, buffer overflows, uninitialized variables, memory leaks, unreachable code, dead stores, integer overflow, MISRA violations, and concurrency problems.
Tools (examples): Cppcheck, clang-tidy / Clang Static Analyzer, PC-lint, Coverity, Polyspace, SonarQube, Parasoft, IAR C-STAT, GCC -fanalyzer.
Practices:
- Run it automatically in CI on every commit
- Treat new findings as build failures
- Triage false positives, and document suppressions with reasons
- Combine with other checks: unit tests, sanitizers (on host builds) and code review
27.3 Compiler Warnings
The compiler is a free first line of defense.
Recommended GCC/Clang flags:
-Wall -Wextra -Wpedantic
-Wconversion -Wsign-conversion
-Wshadow -Wundef -Wcast-align
-Wstrict-prototypes -Wmissing-prototypes
-Wformat=2 -Wnull-dereference
-Wdouble-promotion (helps on MCUs with single-precision FPUs)
-Werror (treat warnings as errors in CI)
Practices:
- Zero warnings policy. Fix the code instead of silencing warnings.
- If you must suppress, do it locally and with a comment
- Pick a language standard (
-std=c11, for example) and stick to it - Warnings can differ between optimization levels, so build at the level you ship
27.4 Code Reviews
A second pair of eyes catches bugs, spreads knowledge and keeps style consistent.
Good practices:
- Small, focused changes are easier to review
- Use a checklist: correctness, error handling, ISR safety, shared data, timing, resource use, naming, comments, tests, MISRA, and warnings
- Review design and intent, not just syntax
- Let automation (formatter, static analysis, CI) handle style, so humans focus on logic
- Be respectful and specific. Discuss the code, not the person.
- Require at least one approval before merging
- Check that tests and documentation are updated with the change
- Types: peer review, pair programming, formal inspection (for safety-critical work)
27.5 Documentation
Documentation preserves intent for your future self and others.
Levels:
- Code comments: explain why, not what. Avoid comments that merely repeat the code.
- Function headers: purpose, parameters, return values, side effects, preconditions, thread/ISR safety, and units (e.g. "milliseconds")
- Doxygen (or similar) to generate documentation from comments
- README / module docs: how to build, flash, test and debug
- Architecture and design docs: block diagrams, state machines, memory map, interface specs and timing diagrams
- Hardware dependencies: pin maps, clock tree, peripheral usage
- Changelog / release notes: what changed in each version
- Requirements and traceability: link requirements to code and tests (important in regulated industries)
Keep docs close to the code and updated together with it. Out-of-date documentation is worse than none.
/**
* @brief Reads the temperature from the sensor.
* @param out_celsius Pointer to store the result in 0.1 degC units.
* @retval OK on success, ERR_TIMEOUT if the sensor did not respond within
* SENSOR_TIMEOUT_MS, ERR_NULL_PTR if out_celsius is NULL.
* @note Not safe to call from an ISR (may block).
*/
status_t temp_read(int16_t *out_celsius);
27.6 Defensive Coding (as a Standard)
Coding standards usually require defensive practices (see also Topic 23.5):
- Validate inputs and check every return value
- Use fixed-width types and explicit sizes
- Use named constants and enums, not "magic numbers"
- Initialize everything, and limit variable scope (prefer
staticfor file-private items) - One purpose per function. Keep functions short, with limited complexity (cyclomatic complexity limits).
- Avoid unsafe functions (
gets,strcpy,sprintf) in favor of bounded versions - Use
constwherever data should not change - Handle all
enumcases and everyswitchbranch - Use assertions for impossible states
- Keep ISRs small and avoid blocking calls inside them
27.7 Other Consistency Practices
- Naming conventions (modules, functions, macros, constants)
- Formatting with an automatic formatter (e.g.
clang-format) so style debates disappear - Version control (Git) with clear commit messages and branching rules
- Consistent project structure and layering (application, middleware, HAL, drivers)
- Coding standard document agreed by the whole team (for example, MISRA C plus local rules)
Quick Revision Cheat Sheet
| # | Topic | Remember this |
|---|---|---|
| 20 | Debugging | Breakpoints, watches, stepping, call stack, registers and memory. For HardFaults, read the stacked PC and CFSR. |
| 21 | Hardware Debugging | SWD (2 wires) or JTAG (4+) for debugging. Multimeter for DC, scope for analog and timing, logic analyzer for digital buses, UART terminal for logs. |
| 22 | Testing | Unit (host, fast), integration (modules together), HIL (real hardware plus simulated world), stubs/mocks for isolation, regression to prevent old bugs returning. |
| 23 | Reliability | Watchdog for hangs, BOR/PVD for low voltage, log reset causes, handle faults safely, code defensively, keep error logs. |
| 24 | Updates | Bootloader validates and boots. IAP/OTA bring new images. Version carefully. Use A/B slots, rollback and power-loss-safe flags for recovery. |
| 25 | Security | Signed images, secure boot chain of trust, hashes for integrity, protected key storage, locked debug port, signed and anti-rollback updates. |
| 26 | Optimization | Measure first. Balance speed, flash, RAM and power. Use compiler flags and profiling. Sleep whenever possible. |
| 27 | Coding Standards | MISRA C, static analysis, zero compiler warnings, code reviews, good documentation and defensive coding. |
Final Checklist Before Shipping Firmware
- All compiler warnings fixed, and static analysis clean (or deviations documented)
- Unit, integration and regression tests pass. Key HIL scenarios pass.
- Watchdog, brown-out and fault handlers configured and tested
- Reset cause and error logs are recorded
- Bootloader, update, rollback and interrupted-update scenarios tested
- Secure boot enabled, images signed, keys protected, debug port locked
- Flash, RAM, stack and power budgets verified with margin
- Version number, changelog and documentation updated
- Code reviewed and approved
End of notes for Phase 5: Debugging, Testing and Production Firmware.
π§ Continue Learning
Related technical blueprints and practical guides in Firmware & Embedded Systems.

Embedded C Interview Preparation
Frequently tested concepts, Practical Embedded C programs

Microcontroller-Specific Embedded C
Embedded C is not completely platform-independent. You should learn the concepts above and then implement them on a real microcontroller

Advanced Firmware Development
ADC, DAC and Sensor Interfacing, DMA and Data Transfer, Embedded Software Architecture, RTOS β Real-Time Operating Systems, Embedded Build Process and Linker

Interrupts and Communication Protocols
Interrupts and Exception Handling, UART / USART Serial Communication, SPI β Serial Peripheral Interface, IΒ²C β Inter-Integrated Circuit, Other Communication Interfaces