Part 4 · Professional Firmware32 min read
Safety, Standards, and Coding Guidelines
When firmware controls an insulin pump, a car’s brakes, a factory robot, or an aircraft’s fuel system, “it passed the tests” is not enough. Regulators and customers want evidence that the code was written to rules that avoid whole classes of bugs, that hazards were analysed, that every requirement was verified, and that the device detects its own failures and goes to a safe state. Even outside regulated industries, the same rules make code more portable, more reviewable, and less likely to contain undefined behavior. This lesson covers the coding guidelines you will meet (MISRA C and CERT C), what the functional safety standards ask of a firmware team, the defensive mechanisms safety firmware actually contains, and a code review checklist you can adopt tomorrow.
Why coding guidelines exist
Section titled “Why coding guidelines exist”C gives you enormous freedom, and much of that freedom is a trap. Undefined Behavior, volatile, and What the Optimizer Assumes showed how signed overflow, out-of-bounds access, and uninitialized reads let the compiler produce code that does something other than what you wrote. Coding guidelines exist for three reasons:
- To avoid undefined and unspecified behavior. Most rules forbid constructs whose meaning depends on the compiler, or that are easy to get wrong.
- To make code portable. Rules pin down implementation-defined behavior so the code behaves the same on GCC, IAR, and Keil, on Cortex-M0 and Cortex-M7.
- To make code reviewable. A reviewer who knows that every
switchhas adefault, every conversion is explicit, and no function recurses can review faster and miss less.
Guidelines are not a substitute for design, testing, or thinking. A fully MISRA-compliant program can still compute the wrong answer. What they remove is the category of bugs where the code does not mean what it appears to mean.
MISRA C
Section titled “MISRA C”MISRA C is the most widely used coding guideline for embedded C, originally from the automotive industry and now required or expected in automotive, medical, industrial, railway, and aerospace work. The edition you will most often see referenced is MISRA C:2012, with amendments adding rules for security and for C11/C18 features; MISRA C:2023 consolidates the 2012 edition and its amendments into one document. Projects name the edition they comply with, and newer editions continue to appear, so check which one your customer or standard requires.
MISRA C is a paid document; buy it if your project claims compliance. What follows is a summary of how it works and the rules you will hit most often, not a substitute for the text.
Categories
Section titled “Categories”Each guideline is a rule (checkable from the source code alone) or a directive (needs design information or process to check). Each has a category:
| Category | Meaning | Deviation allowed? |
|---|---|---|
| Mandatory | Must always be followed | No |
| Required | Must be followed unless a formal deviation is recorded | Yes, with documented justification |
| Advisory | Recommended; may be disapplied by the project | Yes, with less formality |
A project can also promote advisory rules to required, or required to mandatory, in its own compliance plan.
The rules you will hit daily
Section titled “The rules you will hit daily”These are the MISRA C:2012 guidelines that most often appear in real code review, with the reason behind each:
| Guideline (category) | What it says | Why |
|---|---|---|
| Dir 4.12 (required), Rule 21.3 (required) | No dynamic memory allocation; do not use malloc, calloc, realloc, free | Fragmentation, nondeterministic timing, failure at runtime |
| Rule 17.2 (required) | No recursion, direct or indirect | Stack depth cannot be bounded statically |
| Rule 15.5 (advisory) | A function should have a single point of exit at the end | Easier to verify cleanup; widely debated and often disapplied |
| Rules 15.1 (advisory), 15.2, 15.3 (required) | goto should not be used; if used, only jump forward to a label in the same or an enclosing block | Allows the goto-cleanup pattern while forbidding spaghetti |
| Rules 10.1 to 10.8 (required/advisory) | The essential type model: no implicit conversions between essential type categories (Boolean, character, signed, unsigned, enum, floating), no assigning to a narrower type | Catches promotion and sign conversion bugs |
| Rule 14.4 (required) | Controlling expressions of if, while, and so on must be essentially Boolean | No if (count), write if (count != 0u) |
| Rule 16.4 (required) | Every switch has a default label | Unexpected values are handled explicitly |
| Rule 16.3 (required) | Every non-empty switch clause ends with an unconditional break | No accidental fallthrough |
| Rule 18.4 (advisory) | Do not use +, -, +=, -= on pointers; use array indexing | Indexing is easier to bounds-check |
| Rule 19.2 (advisory) | The union keyword should not be used | Type punning and overlay ambiguity |
| Dir 4.10 (required) | Header files must be protected against multiple inclusion | Include guards |
| Rule 20.5 (advisory) | #undef should not be used | A macro’s meaning should not change mid-file |
| Rule 17.7 (required) | The return value of a non-void function must be used (or explicitly cast to void) | Unchecked errors |
| Rule 9.1 (mandatory) | Do not read an automatic variable before it has been set | Uninitialized reads are UB |
| Rule 7.2 (required) | Unsigned integer constants need a u suffix | 1u << 31 instead of the UB 1 << 31 |
| Rule 11.4 (advisory) | Avoid conversions between pointers and integers | Needed for register access, so it is usually deviated |
The essential type model is the part that changes how you write code the most. MISRA classifies every expression by what it represents rather than by C’s promoted type, and forbids mixing categories implicitly:
#include <stdbool.h>#include <stdint.h>
uint8_t a, b;uint16_t wide;int32_t s;bool flag;
void examples(void){ wide = a; /* compliant: unsigned to wider unsigned */ a = wide; /* violation: narrowing assignment */ a = (uint8_t)wide; /* compliant: explicit, reviewer sees it */ s = a; /* violation: unsigned to signed category */ if (a) { } /* violation: not essentially Boolean */ if (a != 0u) { } /* compliant */ flag = (s > 0); /* compliant: comparison is essentially Boolean */}Each violation here compiles without a warning under -Wall. Each is a place where a value can change silently. The model forces the conversion to be visible, so a reviewer can ask “can wide exceed 255 here?”.
Deviations
Section titled “Deviations”Real firmware cannot follow every required rule. Writing a register means converting an integer to a pointer (Rule 11.4, plus the pointer cast rules in 11.x). Some third-party code is not compliant. The MISRA process handles this with deviations: a documented, reviewed, approved record that a specific rule is not followed in a specific place or class of places, with the reason and the measures taken to make it safe.
Rule: MISRA C:2012 Rule 11.4 (advisory)Scope: Files under platform/include/ that define peripheral base macrosJustification: Peripheral registers exist at fixed addresses defined by RM0090; the only way to access them in C is a cast from an integer constant to a pointer to a volatile-qualified struct.Safeguards: Addresses taken only from the vendor SVD-derived header; _Static_assert on every register offset; no arithmetic on the resulting pointers.Approved: Software safety lead, 2026-02-10A deviation record names the rule, the scope, the reason, and the safeguards. MISRA Compliance:2020 describes the surrounding documents: a guideline enforcement plan (which tool checks which rule), deviation records, and a guideline compliance summary listing every rule and its status. A blanket “we ignore Rule 10.x” is not a deviation; it is non-compliance.
Nobody checks MISRA by hand. Static analysers do it, with varying coverage:
| Tool | Type | Notes |
|---|---|---|
| Cppcheck with the MISRA addon | Open source | Good for learning and catching many rules; needs the rule texts supplied separately for messages; incomplete coverage |
| PC-lint Plus | Commercial | Long-established, configurable, wide rule coverage |
| Coverity, Polyspace, Helix QAC, Parasoft C/C++test, LDRA | Commercial | Full MISRA coverage, reports for certification, some with tool qualification kits |
Compiler warnings (-Wconversion, -Wsign-conversion) | Free | Overlap with parts of the essential type rules; not a MISRA checker |
cppcheck --addon=misra --enable=style --std=c11 -Iinclude src/Run the checker in CI on every change and fail on new violations. Introducing MISRA into an existing codebase produces thousands of findings at once; baseline the existing ones, fix new code strictly, and pay down the baseline module by module.
CERT C
Section titled “CERT C”The SEI CERT C Coding Standard comes from the security world. Where MISRA restricts the language to a safer subset, CERT C is a catalogue of specific ways C programs fail, each with noncompliant and compliant examples. It is free to read. Five rules that every firmware engineer should know by name:
INT30-C. Ensure that unsigned integer operations do not wrap. Unsigned wrap is defined behavior, which is exactly why it is dangerous in size calculations:
#include <stddef.h>#include <stdint.h>#include <string.h>
/* Noncompliant: hdr_len + payload_len can wrap to a small number */int build_bad(uint8_t *out, size_t cap, size_t hdr_len, size_t payload_len){ (void)out; if (hdr_len + payload_len > cap) { return -1; } /* ... copies hdr_len + payload_len bytes ... */ return 0;}
/* Compliant: check each term without forming a sum that can wrap */int build_good(uint8_t *out, size_t cap, size_t hdr_len, size_t payload_len){ (void)out; if (hdr_len > cap || payload_len > cap - hdr_len) { return -1; } /* ... */ return 0;}If an attacker controls payload_len and sends SIZE_MAX, the sum in build_bad wraps to hdr_len - 1, passes the check, and the copy overruns the buffer. build_good subtracts from a value known to be larger, which cannot wrap.
INT32-C. Ensure that operations on signed integers do not result in overflow. Signed overflow is undefined behavior, and the optimizer exploits it. Check before the operation (if (a > INT32_MAX - b)) or use the GCC builtins __builtin_add_overflow() and __builtin_mul_overflow(), which compute the result and report overflow.
ARR30-C. Do not form or use out-of-bounds pointers or array subscripts. Every index from outside the function is validated against the array size before use, including indexes computed from protocol fields.
STR31-C. Guarantee that storage for strings has sufficient space for character data and the null terminator. The classic off-by-one: a buffer of n bytes holds n - 1 characters. Use snprintf and check its return value for truncation, as Arrays, Strings, and Tables showed.
EXP33-C. Do not read uninitialized memory. On an MCU, a local variable read before it is written contains whatever the previous function left on the stack: often a plausible-looking value, so the bug appears and disappears with unrelated code changes. Initialize at declaration, and let -Wmaybe-uninitialized and static analysis find the paths you missed.
MISRA and CERT overlap heavily and are complementary: MISRA is stricter about language subset, CERT is more focused on security consequences. Many teams apply MISRA for structure and use CERT rules as review criteria for parsers and anything handling external input.
Functional safety standards
Section titled “Functional safety standards”Functional safety is freedom from unacceptable risk of harm caused by a system malfunctioning. The standards define how to develop systems whose failures could hurt people, and they apply to software as much as hardware.
| Standard | Domain | Integrity levels (lowest to highest) |
|---|---|---|
| IEC 61508 | Generic electrical/electronic/programmable safety systems; the parent of many sector standards | SIL 1 to SIL 4 |
| ISO 26262 | Road vehicles | QM (no safety requirement), ASIL A to ASIL D |
| IEC 62304 | Medical device software | Software safety class A, B, C |
| DO-178C | Airborne software | DAL E (no effect) to DAL A (catastrophic) |
| EN 50128 / EN 50716 | Railway software | SIL 0 to SIL 4 |
The integrity level comes from analysis of how bad a failure would be and how likely and controllable it is. Higher levels demand more rigorous methods, more verification, and more independence.
What they require from a firmware team
Section titled “What they require from a firmware team”The standards differ in vocabulary but ask for the same things:
- A defined process. A documented development lifecycle, plans for development, verification, configuration management, and quality assurance, and evidence that you followed them.
- Requirements. Software safety requirements derived from system safety requirements, each uniquely identified, unambiguous, and verifiable.
- Traceability. Every requirement traces down to design and code and to the tests that verify it; every piece of code traces up to a requirement. Code with no requirement is either unneeded or a missing requirement.
- Verification. Reviews of requirements, design, and code; unit, integration, and system testing; structural coverage targets that rise with the integrity level (statement, branch, then MC/DC for the highest levels of DO-178C and recommended for ASIL D).
- Tool qualification. Evidence that tools whose failure could introduce or hide errors (compilers, static analysers, test tools, code generators) are fit for purpose. ISO 26262 classifies tools by tool confidence level; DO-178C uses the companion document DO-330.
- Independence. At higher levels, verification must be done by people other than the author, sometimes by a separate team or organization.
- Coding standards. A defined language subset, which in practice means MISRA C for C projects.
Safety concept and hazard analysis
Section titled “Safety concept and hazard analysis”Before anyone writes safety code, the system team performs a hazard analysis: identify what the system could do that harms someone (a heater stays on, a motor moves unexpectedly, a dose is wrong), estimate severity and likelihood, and derive safety goals such as “the heater shall not exceed 80 °C”. Techniques include HAZOP, FMEA (failure modes and effects analysis, bottom-up from component failures), and FTA (fault tree analysis, top-down from a hazard). The safety concept then allocates each safety goal to mechanisms: what detects the dangerous condition, how fast, and what the system does about it. Firmware typically owns many of those mechanisms, which is where the next section comes in.
Defensive design
Section titled “Defensive design”Safety firmware assumes things will go wrong (hardware faults, cosmic-ray bit flips, software bugs that escaped testing) and contains mechanisms to detect them and react. These mechanisms are worth knowing even for unregulated products, because many are cheap.
Safe states
Section titled “Safe states”A safe state is a condition in which the system cannot cause harm: heater off, motor de-energised and braked, valve closed, outputs in their failsafe level. Define it for every product during design, make sure hardware supports it without software (an output that defaults to off when the MCU is in reset), and make every serious detected fault lead there. “Reset and hope” is not a safe state if the reset takes 200 ms and the motor keeps running.
Plausibility checks on inputs
Section titled “Plausibility checks on inputs”Never trust a single input blindly. Check it against physics and against other inputs:
#include <stdbool.h>#include <stdint.h>
#define T_MIN_CENTI (-4000)#define T_MAX_CENTI 12500#define T_MAX_STEP_CENTI 200 /* 2 degC per 100 ms sample is impossible here */#define T_MAX_DIFF_CENTI 300 /* two sensors on the same plate */
typedef enum { TEMP_OK, TEMP_RANGE, TEMP_RATE, TEMP_DISAGREE } temp_status_t;
temp_status_t temp_plausible(int32_t a, int32_t b, int32_t prev_a){ if (a < T_MIN_CENTI || a > T_MAX_CENTI || b < T_MIN_CENTI || b > T_MAX_CENTI) { return TEMP_RANGE; /* open or shorted sensor */ } int32_t step = a - prev_a; if (step > T_MAX_STEP_CENTI || step < -T_MAX_STEP_CENTI) { return TEMP_RATE; /* physically impossible change */ } int32_t diff = a - b; if (diff > T_MAX_DIFF_CENTI || diff < -T_MAX_DIFF_CENTI) { return TEMP_DISAGREE; /* one channel has failed */ } return TEMP_OK;}Range checks catch open and shorted sensors, which typically read as the ADC’s minimum or maximum. Rate checks catch glitches and intermittent connections. Comparing two independent sensors (redundancy) catches a sensor that has drifted to a plausible but wrong value, which no single-channel check can detect.
Redundant storage of critical variables
Section titled “Redundant storage of critical variables”A bit flip in RAM (from radiation, electrical noise, or a stray pointer write) can silently change a safety-critical value. Storing the value together with its bitwise complement makes corruption detectable:
#ifndef SAFE_VAR_H#define SAFE_VAR_H
#include <stdbool.h>#include <stdint.h>
typedef struct { uint32_t value; uint32_t inverse;} safe_u32_t;
static inline void safe_set(safe_u32_t *v, uint32_t x){ v->value = x; v->inverse = ~x;}
static inline bool safe_get(const safe_u32_t *v, uint32_t *out){ if ((v->value ^ v->inverse) != 0xFFFFFFFFu) { return false; /* corrupted: caller enters safe state */ } *out = v->value; return true;}
#endifAny single-bit error, and any stray write that changes one word but not the other, makes value ^ inverse differ from all ones. Use it for a handful of truly critical values (setpoints, mode flags, limits), not for everything; for bulk data, use a CRC.
Memory tests
Section titled “Memory tests”Safety standards ask for detection of RAM and flash faults. For RAM, the standard technique is a March test: a sequence of writes and reads that walks up and down memory in a pattern designed to detect stuck-at faults, transition faults, and coupling between cells. March C- is the common choice:
#include <stdbool.h>#include <stddef.h>#include <stdint.h>
/* March C-: (w0); up(r0,w1); up(r1,w0); down(r0,w1); down(r1,w0); (r0) * Destructive: the region's contents are lost. */bool ram_march_c_minus(volatile uint32_t *base, size_t words){ const uint32_t p0 = 0x00000000u; const uint32_t p1 = 0xFFFFFFFFu; size_t i;
for (i = 0; i < words; i++) { base[i] = p0; } for (i = 0; i < words; i++) { if (base[i] != p0) { return false; } base[i] = p1; } for (i = 0; i < words; i++) { if (base[i] != p1) { return false; } base[i] = p0; } for (i = words; i-- > 0u; ) { if (base[i] != p0) { return false; } base[i] = p1; } for (i = words; i-- > 0u; ) { if (base[i] != p1) { return false; } base[i] = p0; } for (i = 0; i < words; i++) { if (base[i] != p0) { return false; } } return true;}Each element reads the value the previous element left and writes its complement, ascending or descending, so a cell whose write disturbs a neighbour is caught when the neighbour is read in the next pass. The pointer is volatile so the compiler cannot conclude that a value just written must read back unchanged and delete the checks. The loop for (i = words; i-- > 0u; ) counts down safely with an unsigned index.
Two practical constraints. A destructive test cannot run on RAM the program is using, so the startup test runs before .data and .bss are initialized, from code that uses only registers (often written in assembly for exactly this reason), or it tests RAM in blocks with the contents saved and restored with interrupts disabled. And using only all-zeros and all-ones words does not detect coupling between bits within the same word; certified implementations add more data backgrounds (such as 0x55555555 and 0x33333333). Vendors including ST provide certified self-test libraries for this, and using one is usually cheaper than certifying your own.
CRC over flash at boot
Section titled “CRC over flash at boot”Flash cells can lose charge or be disturbed. Computing a CRC over the application image at boot, and periodically in the background, detects corruption before it causes a wrong computation. A post-build step computes the CRC over the image and stores it at a known location (the image header from Bootloaders and Firmware Update is the natural place). The STM32F407’s CRC peripheral (base 0x40023000, clock enable bit 12 of RCC_AHB1ENR) computes CRC-32 with the polynomial 0x04C11DB7 over 32-bit words:
#include <stdbool.h>#include <stdint.h>
#define RCC_AHB1ENR (*(volatile uint32_t *)0x40023830u)#define CRC_DR (*(volatile uint32_t *)0x40023000u)#define CRC_CR (*(volatile uint32_t *)0x40023008u)
extern const uint32_t __app_start[]; /* from the linker script */extern const uint32_t __app_crc_end[]; /* end of the CRC-covered range */extern const uint32_t __app_crc_value; /* written by the post-build tool */
bool flash_crc_ok(void){ RCC_AHB1ENR |= (1u << 12); /* CRCEN */ (void)RCC_AHB1ENR; CRC_CR = 1u; /* RESET: DR = 0xFFFFFFFF */
for (const uint32_t *p = __app_start; p < __app_crc_end; p++) { CRC_DR = *p; } return CRC_DR == __app_crc_value;}The hardware variant is CRC-32 with initial value 0xFFFFFFFF, no bit reflection, and no final XOR, fed one 32-bit word at a time. That is not the same as the zlib CRC-32 that most host tools compute by default, so the post-build script must implement the same variant over the same word order, or every boot will report corruption. Covering 448 KB this way takes a few milliseconds. For a background check, process a few kilobytes per idle cycle so it never threatens a deadline.
Control-flow monitoring
Section titled “Control-flow monitoring”A fault that corrupts the program counter, or a software bug that skips a step, can make the main loop run but skip a safety check. Control-flow monitoring detects this: each step of a cycle adds a unique signature to an accumulator, and the end of the cycle checks that the total matches the expected value.
#include <stdint.h>
#define CFM_READ_INPUTS 0x1001u#define CFM_CHECK_LIMITS 0x2002u#define CFM_COMPUTE 0x4004u#define CFM_WRITE_OUTPUTS 0x8008u#define CFM_EXPECTED (CFM_READ_INPUTS + CFM_CHECK_LIMITS + CFM_COMPUTE + CFM_WRITE_OUTPUTS)
static uint32_t cfm_acc;
#define CFM_MARK(sig) (cfm_acc += (sig))
void enter_safe_state(void);void read_inputs(void);void check_limits(void);void compute(void);void write_outputs(void);
void control_cycle(void){ cfm_acc = 0; read_inputs(); CFM_MARK(CFM_READ_INPUTS); check_limits(); CFM_MARK(CFM_CHECK_LIMITS); compute(); CFM_MARK(CFM_COMPUTE); write_outputs(); CFM_MARK(CFM_WRITE_OUTPUTS);
if (cfm_acc != CFM_EXPECTED) { enter_safe_state(); }}This simple version detects a skipped step. Detecting a wrong order needs an order-dependent signature, such as a CRC or shift-and-XOR over the step IDs. In real systems the marks are placed inside the called functions, and the final check is tied to the watchdog: the watchdog is fed only when the cycle completes with the correct signature, so a corrupted control flow also stops the feeding. That is the liveness-linked watchdog strategy from Watchdogs, Faults, and Recovering From Them, extended from “every task ran” to “every task ran the right steps”.
Stack and heap monitors
Section titled “Stack and heap monitors”Stack overflow is the classic silent killer. Safety firmware detects it with stack painting and periodic high-water checks, a guard word or MPU guard region below each stack, and on newer cores the stack limit registers, all covered in Memory Protection, Stack Safety, and Robustness. Heap monitoring is simple in safety firmware, because there is usually no heap.
Error reaction tables
Section titled “Error reaction tables”Every detected fault needs a defined reaction. A table makes the design reviewable:
| Fault | Detected by | Detection time | Reaction | Safe state reached within |
|---|---|---|---|---|
| Temperature sensors disagree | temp_plausible() every 100 ms | 100 ms | Heater off, alarm, log | 110 ms |
| Temperature out of range | Range check | 100 ms | Heater off, latch until service | 110 ms |
| RAM test failure at boot | March C- | At boot | Remain in bootloader, outputs off, error LED | Before outputs enabled |
| Flash CRC mismatch | Boot and background CRC | Boot / 30 s | Refuse to start / safe state and reset | Immediate / 30 s |
| Control flow signature wrong | End of control cycle | 10 ms | Safe state, stop feeding watchdog | 10 ms |
| Task missed its liveness deadline | Watchdog supervisor | 500 ms | Watchdog reset; outputs default off in reset | 500 ms + reset |
The last column must be less than the fault tolerance time that the hazard analysis allows (how long the dangerous condition can persist before harm occurs). If it is not, the design changes.
assert policy in safety firmware
Section titled “assert policy in safety firmware”assert is controversial in safety work, because a failed assertion is itself a reaction, and it must be a deliberate one:
- During development, an assertion failure halts at a breakpoint (
__asm volatile ("bkpt #0")) with full context for the debugger. - In production, an assertion failure is treated as a detected fault: record the location in the crash record, drive outputs to the safe state, and then reset or stay halted, according to the error reaction table.
- Never compile assertions out of safety firmware with
NDEBUG. The checks are part of the safety mechanisms, and the code you verified must be the code you ship. - Never assert on external input. Malformed messages and out-of-range sensor values are expected events with defined reactions, not programmer errors.
Code style
Section titled “Code style”Style matters less than consistency, and consistency is only achieved by tools. Choose a style, encode it in a clang-format configuration, and run it in CI and in a pre-commit hook:
BasedOnStyle: LLVMIndentWidth: 4ColumnLimit: 100BreakBeforeBraces: LinuxAllowShortIfStatementsOnASingleLine: NeverAllowShortFunctionsOnASingleLine: EmptyPointerAlignment: RightAlignConsecutiveMacros: trueSortIncludes: falseSortIncludes: false is deliberate in embedded code, where include order sometimes matters for vendor headers. AllowShortIfStatementsOnASingleLine: Never keeps every if body on its own line with braces, which MISRA also requires.
Naming conventions to adopt and write down: module_verb_noun() for functions (uart_write_bytes), MODULE_CONSTANT for macros and enum constants, _t suffix for typedefs, g_ prefix for the rare global, s_ or no prefix for file-static state, and units in names where they matter (timeout_ms, temp_centi, voltage_mv). A unit suffix is the cheapest bug prevention there is.
Every file starts with a short header:
/* SPDX-License-Identifier: LicenseRef-Proprietary * Copyright (c) 2026 Example Devices Ltd. * * temp_check.c - plausibility checks for the dual heater-plate sensors. * Implements SSR-HEAT-012, SSR-HEAT-013. */The SPDX line makes license scanning automatic (important for SBOMs), and the requirement IDs give traceability from code to requirements.
A 25-point firmware code review checklist
Section titled “A 25-point firmware code review checklist”Use this as a pull request template. Not every item applies to every change; the reviewer ticks the ones that do.
Correctness and C language
- No undefined behavior: signed overflow, shifts by width or more, out-of-bounds access, uninitialized reads.
- Every implicit conversion that narrows or changes signedness is made explicit, and the reviewer has checked the range.
- Unsigned constants have a
usuffix; bit masks use1u << n. - Size arithmetic cannot wrap (subtract from the known-larger value instead of adding).
- Every
switchon an enum handles all values and has adefault.
Memory and bounds
- Every buffer write is bounded by the buffer’s size, checked before the write.
- Every index or length derived from external input is validated.
- No dynamic allocation after initialization; no recursion.
- Stack usage of new call chains is known (
-fstack-usage) and within the task’s budget. - Strings are always terminated;
snprintfreturn values are checked for truncation.
Errors and resources
- Every return value is checked or explicitly documented as ignorable.
- Every error path releases exactly what was acquired, in reverse order.
- Errors are propagated to a layer that can act, not logged and dropped.
assertis used only for programmer errors, never for external input.
Concurrency and timing
- Every variable shared between ISRs and tasks is identified, and its access is atomic or protected.
- Critical sections are short and bounded; interrupts are restored to their previous state, not blindly enabled.
- No blocking calls in ISRs; only ISR-safe APIs are used there.
- Every hardware wait has a timeout; time comparisons are wraparound-safe.
- New work fits the task’s time budget; deadlines and priorities are still met.
Hardware
- Register addresses, bit positions, and access types are checked against the reference manual, with a section reference in a comment.
- Read-modify-write sequences on registers shared with ISRs are protected or replaced with set/reset registers.
- Peripheral clocks are enabled before access; status flags are cleared the way the manual specifies (write 1, read sequence, and so on).
- The relevant errata sheet has been checked for every peripheral touched.
Process
- New behaviour has tests (unit, and HIL where hardware is involved); bug fixes have a regression test.
- Static analysis and MISRA checks pass with no new violations, or each new deviation is documented and approved.
Common mistakes
Section titled “Common mistakes”- Treating MISRA compliance as proof of correctness. Fix: guidelines remove a class of bugs; design reviews and testing remove the rest.
- Blanket suppression of a rule across the codebase. Fix: formal deviations with specific scope, justification, and safeguards.
- Introducing a MISRA checker to legacy code and trying to fix all findings at once. Fix: baseline existing findings, enforce strictly on new code, pay down by module.
- Size checks written as
a + b > cap. Fix:b > cap - aafter checkinga <= cap. - Compiling out assertions in release builds of safety firmware. Fix: assertions stay, with a defined production reaction.
- RAM tests run from C on the RAM the C code is using. Fix: run destructive tests before C runtime init, or test in saved-and-restored blocks.
- Flash CRC computed with a different CRC-32 variant by the post-build tool. Fix: match polynomial, initial value, reflection, final XOR, and word order exactly.
- A safe state that depends on software continuing to run. Fix: hardware defaults (outputs off in reset) plus a watchdog that resets into that state.
Exercises
Section titled “Exercises”- Run
cppcheck --addon=misraon a module you wrote in an earlier part. Categorize the findings by rule, fix all the essential-type findings, and write one formal deviation record for a rule you decide not to follow (register access is the usual candidate). - Write the noncompliant and compliant versions of a function that reads a length-prefixed record from a buffer, and write a host test that passes a length of
SIZE_MAX - 2. Run the noncompliant version under AddressSanitizer and record the report; confirm the compliant version rejects the input. - Implement
ram_march_c_minus()on the host against a simulated memory with an injected fault: a cell stuck at 1, then a cell whose write also flips its neighbour. Confirm both are detected. Then extend the test with backgrounds0x55555555and0x33333333and inject a fault between two bits of the same word that the original version misses. - Write the post-build Python script that computes the STM32 hardware CRC variant (polynomial
0x04C11DB7, initial0xFFFFFFFF, no reflection, no final XOR, little-endian 32-bit words) over a.binfile and patches the result into a reserved word. Verify it on the board withflash_crc_ok(), then flip one bit in the binary and confirm detection. - For a heater controller with two temperature sensors, a relay, and a watchdog, write a hazard list (at least four hazards), an error reaction table with detection and reaction times, and the plausibility check function. State the fault tolerance time you assumed for each hazard.
Check yourself
Section titled “Check yourself”Question 1:Under the MISRA essential type model, which line is a violation if a is uint8_t and w is uint16_t?
Assigning a wider essential type to a narrower object implicitly is forbidden. The explicit cast is compliant because it makes the narrowing visible for review.
Question 2:A parser checks if (hdr + len > cap) return -1; where all three are size_t and len comes from the packet. What can an attacker do?
Unsigned arithmetic wraps by definition (CERT INT30-C). Rewriting the check as len > cap - hdr after confirming hdr <= cap cannot wrap.
Question 3:Why is the March C- routine's base pointer declared volatile uint32_t *?
Without volatile, the optimizer knows what it just stored and can fold every comparison to true, deleting the test entirely.
Question 4:A safety product's assertion handler is disabled with NDEBUG in release builds to save flash. What is the core problem?
In safety firmware assertions are detection mechanisms with a defined reaction. Removing them changes behaviour after verification and leaves faults undetected.
Question 5:The error reaction table shows a fault reaching the safe state in 600 ms, but the hazard analysis gives a fault tolerance time of 250 ms. What must happen?
The safe state must be reached within the fault tolerance time or harm can occur. Faster detection, a faster reaction path, or a hardware mechanism is required.
Key takeaways
Section titled “Key takeaways”- Coding guidelines remove undefined, implementation-defined, and hard-to-review constructs; they do not prove the code correct.
- MISRA C has mandatory, required, and advisory guidelines; required ones may be deviated only with a documented, approved record.
- The essential type model, no dynamic memory, no recursion, Boolean conditions, and complete switches are the MISRA rules that shape daily code.
- CERT C INT30, INT32, ARR30, STR31, and EXP33 cover the bugs most often exploited in firmware parsers.
- Functional safety standards demand process, requirements, traceability, verification with coverage, tool qualification, and independence, scaled by integrity level.
- Defensive firmware defines safe states and detects faults with plausibility checks, redundancy, RAM and flash tests, control-flow monitoring, and liveness-linked watchdogs.
- Every detected fault has a documented reaction that reaches the safe state within the fault tolerance time.
Next: Embedded Linux and Where Bare Metal Ends, where the device grows an MMU, a kernel, and a filesystem.