Skip to content

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.

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:

  1. To avoid undefined and unspecified behavior. Most rules forbid constructs whose meaning depends on the compiler, or that are easy to get wrong.
  2. 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.
  3. To make code reviewable. A reviewer who knows that every switch has a default, 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 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.

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:

CategoryMeaningDeviation allowed?
MandatoryMust always be followedNo
RequiredMust be followed unless a formal deviation is recordedYes, with documented justification
AdvisoryRecommended; may be disapplied by the projectYes, with less formality

A project can also promote advisory rules to required, or required to mandatory, in its own compliance plan.

These are the MISRA C:2012 guidelines that most often appear in real code review, with the reason behind each:

Guideline (category)What it saysWhy
Dir 4.12 (required), Rule 21.3 (required)No dynamic memory allocation; do not use malloc, calloc, realloc, freeFragmentation, nondeterministic timing, failure at runtime
Rule 17.2 (required)No recursion, direct or indirectStack depth cannot be bounded statically
Rule 15.5 (advisory)A function should have a single point of exit at the endEasier 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 blockAllows 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 typeCatches promotion and sign conversion bugs
Rule 14.4 (required)Controlling expressions of if, while, and so on must be essentially BooleanNo if (count), write if (count != 0u)
Rule 16.4 (required)Every switch has a default labelUnexpected values are handled explicitly
Rule 16.3 (required)Every non-empty switch clause ends with an unconditional breakNo accidental fallthrough
Rule 18.4 (advisory)Do not use +, -, +=, -= on pointers; use array indexingIndexing is easier to bounds-check
Rule 19.2 (advisory)The union keyword should not be usedType punning and overlay ambiguity
Dir 4.10 (required)Header files must be protected against multiple inclusionInclude guards
Rule 20.5 (advisory)#undef should not be usedA 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 setUninitialized reads are UB
Rule 7.2 (required)Unsigned integer constants need a u suffix1u << 31 instead of the UB 1 << 31
Rule 11.4 (advisory)Avoid conversions between pointers and integersNeeded 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?”.

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.

DEV-007: Rule 11.4 for memory-mapped registers
Rule: MISRA C:2012 Rule 11.4 (advisory)
Scope: Files under platform/include/ that define peripheral base macros
Justification: 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-10

A 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:

ToolTypeNotes
Cppcheck with the MISRA addonOpen sourceGood for learning and catching many rules; needs the rule texts supplied separately for messages; incomplete coverage
PC-lint PlusCommercialLong-established, configurable, wide rule coverage
Coverity, Polyspace, Helix QAC, Parasoft C/C++test, LDRACommercialFull MISRA coverage, reports for certification, some with tool qualification kits
Compiler warnings (-Wconversion, -Wsign-conversion)FreeOverlap with parts of the essential type rules; not a MISRA checker
Terminal window
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.

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 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.

StandardDomainIntegrity levels (lowest to highest)
IEC 61508Generic electrical/electronic/programmable safety systems; the parent of many sector standardsSIL 1 to SIL 4
ISO 26262Road vehiclesQM (no safety requirement), ASIL A to ASIL D
IEC 62304Medical device softwareSoftware safety class A, B, C
DO-178CAirborne softwareDAL E (no effect) to DAL A (catastrophic)
EN 50128 / EN 50716Railway softwareSIL 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.

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.

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.

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.

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.

Never trust a single input blindly. Check it against physics and against other inputs:

temp_check.c
#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.

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:

safe_var.h
#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;
}
#endif

Any 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.

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:

ram_test.c
#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.

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:

flash_crc.c
#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.

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.

cfm.c
#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 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.

Every detected fault needs a defined reaction. A table makes the design reviewable:

FaultDetected byDetection timeReactionSafe state reached within
Temperature sensors disagreetemp_plausible() every 100 ms100 msHeater off, alarm, log110 ms
Temperature out of rangeRange check100 msHeater off, latch until service110 ms
RAM test failure at bootMarch C-At bootRemain in bootloader, outputs off, error LEDBefore outputs enabled
Flash CRC mismatchBoot and background CRCBoot / 30 sRefuse to start / safe state and resetImmediate / 30 s
Control flow signature wrongEnd of control cycle10 msSafe state, stop feeding watchdog10 ms
Task missed its liveness deadlineWatchdog supervisor500 msWatchdog reset; outputs default off in reset500 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 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.

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:

.clang-format
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 100
BreakBeforeBraces: Linux
AllowShortIfStatementsOnASingleLine: Never
AllowShortFunctionsOnASingleLine: Empty
PointerAlignment: Right
AlignConsecutiveMacros: true
SortIncludes: false

SortIncludes: 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.

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

  1. No undefined behavior: signed overflow, shifts by width or more, out-of-bounds access, uninitialized reads.
  2. Every implicit conversion that narrows or changes signedness is made explicit, and the reviewer has checked the range.
  3. Unsigned constants have a u suffix; bit masks use 1u << n.
  4. Size arithmetic cannot wrap (subtract from the known-larger value instead of adding).
  5. Every switch on an enum handles all values and has a default.

Memory and bounds

  1. Every buffer write is bounded by the buffer’s size, checked before the write.
  2. Every index or length derived from external input is validated.
  3. No dynamic allocation after initialization; no recursion.
  4. Stack usage of new call chains is known (-fstack-usage) and within the task’s budget.
  5. Strings are always terminated; snprintf return values are checked for truncation.

Errors and resources

  1. Every return value is checked or explicitly documented as ignorable.
  2. Every error path releases exactly what was acquired, in reverse order.
  3. Errors are propagated to a layer that can act, not logged and dropped.
  4. assert is used only for programmer errors, never for external input.

Concurrency and timing

  1. Every variable shared between ISRs and tasks is identified, and its access is atomic or protected.
  2. Critical sections are short and bounded; interrupts are restored to their previous state, not blindly enabled.
  3. No blocking calls in ISRs; only ISR-safe APIs are used there.
  4. Every hardware wait has a timeout; time comparisons are wraparound-safe.
  5. New work fits the task’s time budget; deadlines and priorities are still met.

Hardware

  1. Register addresses, bit positions, and access types are checked against the reference manual, with a section reference in a comment.
  2. Read-modify-write sequences on registers shared with ISRs are protected or replaced with set/reset registers.
  3. Peripheral clocks are enabled before access; status flags are cleared the way the manual specifies (write 1, read sequence, and so on).
  4. The relevant errata sheet has been checked for every peripheral touched.

Process

  1. New behaviour has tests (unit, and HIL where hardware is involved); bug fixes have a regression test.
  2. Static analysis and MISRA checks pass with no new violations, or each new deviation is documented and approved.
  • 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 - a after checking a <= 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.
  1. Run cppcheck --addon=misra on 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).
  2. 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.
  3. 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 backgrounds 0x55555555 and 0x33333333 and inject a fault between two bits of the same word that the original version misses.
  4. Write the post-build Python script that computes the STM32 hardware CRC variant (polynomial 0x04C11DB7, initial 0xFFFFFFFF, no reflection, no final XOR, little-endian 32-bit words) over a .bin file and patches the result into a reserved word. Verify it on the board with flash_crc_ok(), then flip one bit in the binary and confirm detection.
  5. 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.

Question 1:Under the MISRA essential type model, which line is a violation if a is uint8_t and w is uint16_t?

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?

Question 3:Why is the March C- routine's base pointer declared volatile uint32_t *?

Question 4:A safety product's assertion handler is disabled with NDEBUG in release builds to save flash. What is the core problem?

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?

  • 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.