Compiler Workbench & Testing
Compile real C for an embedded target and inspect what the compiler produced, then the discipline around it: where to draw the host-testable boundary, reading the generated assembly, undefined behaviour and the sanitizers, the warnings worth enabling, and measuring size and stack.
Testing, Debugging, Static Analysis, and Safety Evidence
Firmware testing is difficult because important failures depend on architecture, timing, interrupts, peripherals, power loss, and limited observability. The solution is not to push every test onto hardware. Separate portable decision logic from target adapters, run fast exhaustive and randomized tests on the host, use sanitizers and fuzzers where the environment supports them, verify register transactions against fakes, and reserve target testing for instruction, timing, electrical, cache, interrupt, and integration behavior. Safety-oriented development adds traceability: each requirement has a design response, a verification method, evidence, and an owner for unresolved assumptions.
How it is built
- Strict compiler diagnostics catch suspicious conversions, missing prototypes, shadowing, format mismatches, unreachable cases, and undefined constructs early. Warnings are a policy, not proof; flags vary by compiler and must be curated so developers do not normalize noise.
- Static analysis explores paths without executing the program. It can find null dereference, range, lifetime, uninitialized-state, dataflow, and rule violations, but results depend on models and annotations. Findings need triage, documented disposition, and regression tracking.
- Unit tests isolate pure functions and modules. Table tests cover known partitions and boundaries; property tests generate values and assert invariants; model-based tests compare operation sequences against a simpler reference; fuzzers mutate byte streams and call sequences to discover parser states humans did not enumerate.
- Host sanitizers instrument memory and arithmetic behavior such as out-of-bounds access, use-after-free, alignment, and many undefined operations. They do not model MCU peripheral semantics, interrupt timing, or every target ABI, so clean host runs complement rather than replace target evidence.
- Hardware tests verify startup, MMIO, interrupt entry, DMA/cache behavior, power states, reset cause, flash geometry, stack high-water, deadlines, and electrical signals. SWO, ETM, logic analyzers, GPIO timing pins, counters, and structured event logs make invisible ordering observable.
- Fault injection forces the states production systems eventually encounter: allocation exhaustion, full queues, corrupt lengths, bus faults, delayed completion, stuck peripherals, brownout, torn flash writes, watchdog expiry, and reset at every persistence step. Recovery is tested as a first-class feature.
- Coding standards such as MISRA C and CERT C turn known hazards into enforceable rules. Compliance is not automatic correctness; deviations must be justified against the system, toolchain, and safety case, and generated or vendor code needs an explicit boundary.
Design procedure
- Translate each requirement into a proof table: claim, responsible module, assumptions, verification method, expected evidence, and failure response. Include resource and timing requirements, not only functional output.
- Build host-testable cores with injected dependencies. Replace MMIO, time, randomness, flash, and transport with fakes that record call order, return programmed faults, and expose internal telemetry without changing production decisions.
- Compile at multiple optimization levels under strict warnings and at least one additional compiler when practical. Run static analysis and host sanitizers in automated builds; treat new warnings and unreviewed findings as failures.
- Design tests from partitions and boundaries: empty, minimum, maximum, exact fit, one beyond, wrap, invalid enum, null where permitted or forbidden, repeated call, reset state, and each documented status. Add properties and model sequences for stateful structures.
- Fuzz every untrusted parser with arbitrary lengths, contents, and chunk boundaries. Seed with valid frames and known corruptions, preserve minimizing crash inputs, and assert bounded execution and no partial publication as well as no crash.
- Measure target-only properties with release optimization and realistic interrupt load. Report distribution and worst observed timing separately from a defensible upper bound; record stack high-water, map usage, queue high-water, reset cause, and dropped-work counters.
- Inject faults at every ownership and persistence transition. Automate power-cut simulation for flash updates, delay consumers until queues fill, force peripheral timeouts, and confirm the system enters a documented degraded or safe state with diagnostic evidence.
Key terms
- property test
- Generated inputs checked against an invariant that should hold for all values in the stated domain.
- model-based test
- Random or exhaustive operation sequences compared with a simpler trusted reference model.
- fuzzing
- Automated generation and mutation of inputs to explore parser and state-machine behavior, guided by coverage or outcomes.
- sanitizer
- Compiler instrumentation that detects classes of invalid memory, arithmetic, or concurrency behavior during execution.
- traceability
- The maintained relationship from requirement through design and implementation to verification evidence.
- fault injection
- Deliberately forcing failures and timing conditions to verify detection, containment, recovery, and diagnostics.
Worked example
/* Property: a successful encode/decode round trip preserves value.
* Failure at any destination capacity leaves writer length unchanged. */
for (uint32_t value = 0U; value <= UINT16_MAX; ++value) {
uint8_t storage[2] = { 0xA5U, 0xA5U };
byte_writer_t w = { storage, sizeof storage, 0U };
assert(writer_put_u16_be(&w, (uint16_t)value));
assert(w.length == 2U);
assert(read_u16_be(storage) == (uint16_t)value);
}
for (size_t capacity = 0U; capacity < 2U; ++capacity) {
uint8_t storage[2] = { 0xA5U, 0xA5U };
byte_writer_t w = { storage, capacity, 0U };
assert(!writer_put_u16_be(&w, 0x1234U));
assert(w.length == 0U);
assert(storage[0] == 0xA5U && storage[1] == 0xA5U);
}Common pitfalls
Embedded C Compiler Workbench
A compiler workbench makes the translation pipeline inspectable. The source editor holds one C translation unit. A selected cross-compiler parses and optimizes it for a declared architecture, emits machine instructions and creates a relocatable ELF object. Object inspection then reveals emitted sections and symbols. A separate host build can execute pure logic and test vectors in a sandbox. These are complementary forms of evidence: local declaration analysis explains likely storage, the object proves what one compiler emitted for one file, the linked map proves final placement across the whole image, and target measurement proves runtime behavior on hardware.
How it is built
- The source boundary is explicit. Editing, declaration classification and simple structure layout happen locally in the browser. A remote compile occurs only after the learner presses Compile or Run. The workbench warns that the public compiler service receives the submitted source and may cache compilation results, so secrets and confidential firmware do not belong in the editor.
- Target selection is a contract rather than a cosmetic label. Cortex-M4 uses Armv7E-M Thumb instructions, Cortex-M33 uses the Armv8-M Main profile, and RV32IM uses a 32-bit RISC-V integer ABI with multiply and divide instructions. The compiler identifier and architecture flags travel together so the assembly listing can be traced back to the exact target choice.
- Optimization changes the program representation while preserving allowed C behavior. -O0 usually keeps source structure visible and spills more state. -O2 performs inlining, constant propagation, dead-code removal and loop transformations. -Os applies size-oriented choices. Comparing these builds exposes undefined behavior assumptions, volatile access requirements and the difference between a source statement and the instructions that survive optimization.
- Strict diagnostics are part of the build contract. The workbench requests C11 mode with -Wall, -Wextra and -Wconversion, captures tagged error or warning locations, removes terminal color escapes and reports the exact effective compiler arguments. A warning-free build is useful evidence, but it cannot prove bounds, concurrency, protocol correctness or hardware behavior.
- The assembly view correlates emitted instructions with source line numbers when the compiler supplies that mapping. Addresses and opcode bytes reveal instruction width; mnemonics reveal loads, stores, branches, widening operations and calls. Directives and relocation records are preserved because an object file is not yet a final address image.
- The object map comes from the emitted ELF object rather than a guessed source model. readelf section headers supply byte counts for .text, .rodata, .data and .bss-style sections. nm supplies symbol type letters and exact object-level sizes. Debug information and relocation metadata are intentionally excluded from the allocatable total shown to the learner.
- The local memory model serves a different purpose. It identifies recognized global, static, automatic, const, array, structure and allocation declarations; predicts their likely flash, data, BSS, stack or heap category; and visualizes alignment holes and tail padding. It remains a teaching estimate because macros, compiler extensions, register spills, inlining, calling conventions and linker rules can change the real result.
- Host-side execution compiles the same source for an x86-64 sandbox and runs main. This is valuable for pure functions, parsers, fixed-point helpers, state machines and boundary tests. It is not target execution: integer widths can differ, endianness can differ, and MMIO, interrupts, DMA, caches, timing and startup behavior cannot be validated by the host run.
- The final firmware memory map is a linker artifact. It combines all translation units, startup objects and selected libraries under one linker script; resolves relocations; removes or retains sections; applies alignment; places load and run addresses; and can introduce veneers, tables or padding. An object-section total therefore answers what this file emitted, not whether the complete board image fits flash and SRAM.
- A disciplined workflow uses the views in order. First predict what the compiler should do. Compile and explain the important instructions. Compare object sections with the local declaration model. Inspect the largest symbols. Repair every diagnostic. Run deterministic host tests. Finally reproduce the build in the product toolchain, inspect its link map and stack-usage files, flash the target and collect timing, high-water and fault evidence.
Design procedure
- Choose the example nearest the intended task or paste a small self-contained C11 translation unit. Remove credentials, proprietary algorithms and secret constants before using the remote compiler.
- Select the exact embedded architecture and the optimization level used for the question being asked. Start with -O0 when learning source correspondence, then compare -O2 and -Os because production behavior is defined by the release build.
- Predict likely sections, structure padding and the key instructions before pressing Compile. Record the hypothesis so the tool produces feedback rather than passive output.
- Compile the target. If it fails, use tagged diagnostics and their source locations first. Do not interpret stale assembly from an earlier source revision as current evidence.
- Trace source-correlated assembly. Identify argument registers, loop control, loads and stores, sign or zero extension, multiply-accumulate instructions, branches and function return. Explain why each memory access remains after optimization.
- Open Object map and compare exact ELF section sizes with the local declaration categories. Differences should trigger a concrete question about optimization, unused objects, const placement, alignment or compiler-generated data.
- Inspect symbols to find which functions and objects dominate the translation unit. Remember that local and discarded symbols can vary with optimization and that library cost appears only after linking.
- Add a main-based test harness with boundary vectors and explicit nonzero failure status, then run host tests. Treat a zero exit status as one layer of evidence and preserve target-only cases for hardware verification.
- Rebuild the same source in the real firmware project. Archive compiler version, flags, linker script, map file, size report, stack-usage data and target measurements with the release evidence.
Key terms
- translation unit
- One preprocessed C source file presented to the compiler, including the headers it expands.
- cross-compiler
- A compiler running on one system but generating instructions and ABI objects for another target.
- relocatable object
- An ELF file containing sections, symbols and unresolved relocation records before final linking.
- source correlation
- Compiler metadata connecting an emitted assembly line to the source line that contributed to it.
- section
- A named group of code or data with shared placement and permission characteristics, such as .text or .bss.
- symbol
- A named function or object recorded with binding, type, section-relative address and often a byte size.
- host test
- Execution of target-independent logic on the development architecture; fast and useful but not a hardware test.
Worked example
The default dot-product program contains read-only coefficients, initialized writable state, a zero-initialized buffer, automatic arrays and an integer multiply-accumulate loop. Compiling for Cortex-M reveals Thumb instructions and exact object sections; the symbol view exposes function and object sizes; the local analyzer explains the likely storage of every recognized declaration; and main returns failure if the host-executed dot product differs from 60.Common pitfalls
What a test can prove about firmware
Most embedded logic can be tested on a host at full speed with no hardware at all, provided it is separated from the registers it eventually drives. That separation is the whole technique: pure functions that transform values are testable, and code that reads a register is not. The practical question is not how to test firmware but where to put the boundary.
How it is built
- Pure logic - protocol parsing, state machines, filters, unit conversion - depends on no hardware and runs identically on a host.
- Hardware access should be a thin layer that reads and writes registers and contains no decisions.
- With that split, the interesting code is host-testable and the untestable part is small enough to review by eye.
- A fake implementation of the hardware layer lets integration paths be exercised without a board.
- On-target tests remain necessary for timing, peripheral behaviour and anything the datasheet describes rather than your code.
Design procedure
- Extract the decision from the access: read the register, pass the value to a pure function, write the result back.
- Test the pure function exhaustively, including the boundary values that are awkward to produce on hardware.
- Keep the hardware layer free of branches, so there is little to get wrong where you cannot test it.
- Run the host tests on every commit; they are fast enough that there is no reason not to.
- Reserve on-target testing for what only hardware can answer, and write down which properties those are.
Key terms
- Hardware abstraction
- A thin layer containing register access and no decisions.
- Pure function
- Output determined by input, no side effects. Trivially testable.
- Host test
- A test compiled and run on the development machine, with no target involved.
- Fake
- A working stand-in for hardware, used to exercise integration paths off-target.
- Test boundary
- Where testable logic ends and untestable access begins. A design decision.
Worked example
The same feature, before and after the boundary is drawn:
// UNTESTABLE - decision and access are one
void update_led(void) {
uint32_t adc = ADC->DR;
if (adc > 2048) GPIOA->BSRR = LED;
else GPIOA->BSRR = LED << 16;
}
// TESTABLE - the decision is a function of a value
bool led_should_be_on(uint16_t adc, uint16_t threshold) {
return adc > threshold;
}
void update_led(void) {
bool on = led_should_be_on(ADC->DR, 2048);
GPIOA->BSRR = on ? LED : (LED << 16);
}
The second version can be tested at every boundary in microseconds:
assert(!led_should_be_on(2047, 2048));
assert(!led_should_be_on(2048, 2048)); // the off-by-one
assert( led_should_be_on(2049, 2048));
assert( led_should_be_on(0xFFFF, 0)); // saturation
And update_led is now four lines with no branch worth testing.Common pitfalls
Reading the compiler's output
The compiler will show you exactly what it generated, and doing so settles questions that reasoning cannot. Whether an access survived optimisation, whether a loop reloads a value, whether a bit operation became a read-modify-write, whether a function was inlined - all of these are visible in about thirty instructions, and none of them are reliably answerable from the source.
How it is built
- -S emits assembly rather than an object file, and -fverbose-asm annotates it with the source expressions.
- objdump -d disassembles an object or executable, showing addresses and encodings.
- A volatile access appears as a load or store that cannot be moved or removed; an elided one is simply absent.
- nm lists a translation unit's defined and required symbols, which answers most link errors directly.
- size reports the section totals, which is how a code-size change is measured rather than estimated.
Design procedure
- Compile the single file with -S -O2 and read the function; it is usually short enough to read in full.
- For a poll loop, check whether the load is above or below the label the branch targets - that alone answers whether it reloads.
- Count stores against the writes in the source; a mismatch is dead-store elimination.
- Compare -O0 and -O2 output for the same function when behaviour differs between builds.
- Use size before and after a change rather than guessing whether it helped.
Key terms
- -S
- Compile to assembly rather than to an object file.
- objdump -d
- Disassemble, with addresses and encodings alongside mnemonics.
- nm
- List symbols: what a file defines and what it needs.
- size
- Report .text, .data and .bss totals. How a size change is measured.
- Loop label
- The branch target. A load above it happens once; below it, every iteration.
Worked example
The one-glance diagnostic for a missing volatile:
while ((STATUS & READY) == 0) { }
plain pointer volatile pointer
LDR r1, [r0] .Lloop:
TST r1, #1 LDR r1, [r0] <- inside
BNE .Ldone TST r1, #1
.Lloop: BEQ .Lloop
B .Lloop <- an .Ldone:
.Ldone: infinite
loop
Left: the load is ABOVE the label, so it happens once. The body
is a branch to itself - the compiler has correctly compiled an
infinite loop from code that says "wait".
And measuring a size change rather than estimating it:
$ arm-none-eabi-size firmware.elf
text data bss dec
24316 108 8192 32616
Run it before and after. The number is not an opinion.Common pitfalls
Undefined behaviour, and the sanitizers that find it
Undefined behaviour is not a runtime error - it is a licence the compiler holds to assume the situation never occurs, and to optimise accordingly. That is why undefined behaviour so often shows up as deleted code rather than as a crash, and why the sanitizers matter: they turn the assumption into a report at the moment it is violated.
How it is built
- Signed overflow, out-of-bounds access, misaligned access, shifting too far and using an indeterminate value are all undefined.
- The compiler may assume none of them happen, which lets it delete checks that would only matter if they did.
- UBSan instruments the build to trap on undefined operations, reporting the file and line.
- ASan detects out-of-bounds and use-after-free on a host build; it does not run on a typical microcontroller.
- Host tests are where the sanitizers are usable, which is another reason to keep the logic host-testable.
Design procedure
- Run the host test suite under -fsanitize=undefined,address as part of the normal build.
- Treat every sanitizer report as a defect rather than a false positive, since it is reporting a real violation.
- Enable -Wall -Wextra -Wconversion and fix the warnings, which catches a large fraction before the sanitizer is needed.
- Compile the target build with -fno-strict-aliasing only if you know you are violating aliasing, and prefer fixing it.
- Check signed overflow explicitly where it can occur, since a post-hoc test may be optimised away.
Key terms
- Undefined behaviour
- The standard imposes no requirement; the compiler may assume it does not occur.
- UBSan
- The undefined-behaviour sanitizer, trapping at the point of violation.
- ASan
- The address sanitizer, detecting out-of-bounds and lifetime errors on a host build.
- Strict aliasing
- The rule that two pointers of incompatible types do not point at the same object.
- Deleted check
- A test the compiler removed because it could only matter if undefined behaviour occurred.
Worked example
The check that is legally deleted, which is the whole reason this matters:
int add(int a, int b) {
int sum = a + b;
if (sum < a) return -1; // intended overflow check
return sum;
}
Signed overflow is undefined, so the compiler reasons: if no
overflow occurred then a + b >= a whenever b >= 0, and overflow
cannot occur because it is undefined. The comparison is provably
false and the branch is removed. At -O2 there is no check.
Unsigned wrapping is defined, so this one survives:
unsigned sum = a + b;
if (sum < a) return -1; // a real wraparound test
And the correct signed form checks BEFORE:
if (b > 0 && a > INT_MAX - b) return -1;
if (b < 0 && a < INT_MIN - b) return -1;
UBSan reports the original at the moment the addition overflows, with the file
and line, which is considerably faster than discovering it from the listing.Common pitfalls
Static analysis and the warnings worth enabling
Compiler warnings are the cheapest static analysis available and most projects run with a fraction of them enabled. A handful in particular catch defect classes that are otherwise found by debugging: implicit conversions that lose data, comparisons between signed and unsigned, and switch statements that do not handle every enumerator.
How it is built
- -Wall and -Wextra are the baseline and are not exhaustive despite the name.
- -Wconversion reports every implicit narrowing, which is where truncation bugs live.
- -Wsign-conversion reports signed/unsigned mixing, which is where comparison bugs live.
- -Wswitch, included in -Wall, reports an enum switch missing an enumerator - but only where there is no default clause.
- -Werror makes warnings fail the build, which is what stops them accumulating into noise nobody reads.
Design procedure
- Enable -Wall -Wextra -Wconversion -Wsign-conversion and fix what appears, a file at a time if necessary.
- Add -Werror once the existing warnings are cleared, so new ones cannot accumulate.
- Omit the default clause from switches over an enum, so adding a state produces a warning at every switch.
- Suppress a specific warning at a specific site with a comment explaining why, rather than disabling it globally.
- Run a separate static analyser periodically for the interprocedural checks the compiler does not do.
Key terms
- -Wconversion
- Warns on implicit narrowing. Noisy initially and finds real truncation.
- -Wsign-conversion
- Warns on signed/unsigned mixing, the source of many bounds bugs.
- -Wswitch
- Warns on an enum switch missing a case. Silenced by a default clause.
- -Werror
- Warnings become errors, so they cannot be ignored into irrelevance.
- Targeted suppression
- Disabling one warning at one site with a reason, rather than globally.
Worked example
What each of the three catches, in one line apiece:
-Wconversion
uint8_t x = some_int; // silently truncates
warning: conversion from 'int' to 'uint8_t' may change value
-Wsign-conversion
for (int i = 0; i < len; i++) // len is size_t
warning: comparison of integer expressions of different
signedness
-> a negative i becomes an enormous unsigned value and the
loop runs past the end
-Wswitch
switch (mode) { // no default clause
case IDLE: break;
case RUN: break;
}
warning: enumeration value 'FAULT' not handled in switch
And why the default clause is worth omitting:
Adding a fourth mode produces a warning at every switch that does
not handle it. With `default: break;` present, it compiles
silently and the new mode does nothing at runtime.Common pitfalls
Measuring size and stack, rather than estimating them
Flash and RAM are fixed, and both are consumed by things that do not appear in any single place in the source. Code size is the sum of everything the linker kept; stack usage is the deepest call path plus interrupt nesting. Both are measurable with tools that ship with the toolchain, and both are routinely estimated instead - which is how a project discovers at integration that it does not fit.
How it is built
- size reports .text, .data and .bss; flash usage is text plus data, and RAM is data plus bss plus the stack and heap.
- The map file lists every symbol with its address and size, which is how the largest contributors are found.
- -fstack-usage emits a .su file per translation unit giving each function's frame size.
- Worst-case stack is the deepest call path's frames summed, plus the deepest interrupt nesting on top.
- Filling the stack with a pattern at startup and inspecting it later gives the actual high-water mark.
Design procedure
- Record size output on every build and watch the trend rather than the absolute number.
- Read the map file when the image grows unexpectedly; the cause is usually one library pulled in by one call.
- Generate stack usage with -fstack-usage and compute the worst path rather than assuming it.
- Fill the stack at startup and check the high-water mark after a realistic run, including error paths.
- Add an MPU guard region below the stack so an overflow faults rather than silently corrupting .bss.
Key terms
- size
- The toolchain utility reporting section totals.
- Map file
- The linker's record of every symbol's address and size.
- .su file
- Per-function stack frame sizes, emitted by -fstack-usage.
- High-water mark
- The deepest the stack actually reached, found from a fill pattern.
- Stack guard
- An MPU region that faults on overflow instead of allowing corruption.
Worked example
Where the flash actually went, which the map file answers directly:
$ arm-none-eabi-size firmware.elf
text data bss dec
52104 216 12288 64608
flash = text + data = 52,320 bytes
RAM = data + bss = 12,504, plus stack and heap
Grep the map for the largest symbols and one line usually
explains a jump:
_printf_float 6284 <- one %f in one debug line
And the stack, computed rather than guessed:
$ cat *.su
main.c:42:5:process_frame 256 static
parse.c:18:9:decode_message 128 static
crc.c:7:12:crc32 16 static
worst path 256 + 128 + 16 = 400
deepest ISR nesting = 192
margin = reserve at least 25%
-> allocate 768 bytes minimum
The fill-pattern check confirms it against a real run, which is the part that
catches the path nobody thought about.Common pitfalls
Assertions, defaults, and failing usefully
Firmware cannot print a stack trace and stop. What it can do is decide, in advance, what each kind of failure means and what should happen - and the decision is worth making explicitly, because the default of continuing with corrupted state is almost never the right one and is what happens when nobody chooses.
How it is built
- An assertion documents an invariant the code relies on, and in a debug build reports it immediately.
- assert compiles to nothing when NDEBUG is defined, so anything with a side effect inside one disappears in release.
- A release build usually needs a different response: log and reset, enter a safe state, or halt - not silently continue.
- A watchdog turns a hang into a reset, which is a recovery only if the reset cause is recorded.
- Recording the fault registers and the reset cause is what makes a field failure diagnosable at all.
Design procedure
- Assert on invariants your own code guarantees, and validate rather than assert on anything from outside.
- Never put an expression with a side effect inside an assertion, since the release build removes it.
- Define a release-build failure policy explicitly, and make it the same everywhere.
- Record the reset cause and the fault registers in non-volatile storage so a field reset is explainable.
- Test the failure paths deliberately, since they are the least-exercised code in the system and run when everything else has gone wrong.
Key terms
- Invariant
- Something that must always hold. What an assertion documents.
- NDEBUG
- The macro that compiles assertions out. Standard in release builds.
- Safe state
- The defined condition to enter on failure: outputs off, motors stopped.
- Reset cause
- The register recording why the part reset. Distinguishes a watchdog from a brown-out.
- Fault registers
- Cortex-M registers recording why a fault occurred, valid inside the handler.
Worked example
The side effect that vanishes in release:
assert(init_peripheral() == OK); // WRONG
With NDEBUG defined, assert expands to nothing and the whole
expression is removed - the peripheral is never initialised. The
release build differs from the debug build in a way nothing in
the source suggests.
Status s = init_peripheral(); // correct
assert(s == OK);
if (s != OK) enter_safe_state();
And a fault handler that leaves something behind:
void HardFault_Handler(void) {
uint32_t *sp = (uint32_t *)__get_MSP();
fault_record.cfsr = SCB->CFSR; // why
fault_record.pc = sp[6]; // where
fault_record.lr = sp[5]; // who called
fault_record.magic = FAULT_MAGIC; // survives the reset
NVIC_SystemReset();
}
On the next boot, check the magic and log the record. A field
unit that resets once a week becomes an address in the map file
rather than an unreproducible report.Common pitfalls
More in Embedded C
- Interrupts, Rings & ConcurrencyThe handler is a second thread you did not declare. What is and is not atomic on a single core, the lock-free ring buffer and the conditions it requires, DMA and cache coherency, low-power modes and their wake sources, priority inversion, and debugging a race you cannot reproduce.
- Volatile RegistersMaster volatile keyword usage for memory-mapped registers in embedded C. Interactive simulator shows compiler optimization effects on register reads.
- User-Defined TypesDeclare your own types in embedded C: struct, union, enum, typedef, bitfields, designated initialisers, flexible array members and opaque handles.
- Embedded C + DSA 0 → 100One self-sufficient course connecting beginner C, Embedded C, hardware-facing APIs, bounded data structures, Embedded DSA practice, compiled code, diagnostics and production capstones.
- Bits, Fields & Fixed PointRegister fields and the mask conventions that silently disagree, Gray code and where one-bit-at-a-time matters, fixed-point arithmetic and the intermediate width a multiply needs, the undefined-behaviour traps in ordinary bit idioms, wire-format packing, and what each checksum detects.
- Types / PromotionUnderstand integer promotion and type conversion in embedded C. Interactive lab demonstrates implicit and explicit casting with signed/unsigned types.
- Functions & ContractsDesign production C functions and modules with explicit preconditions, postconditions, ownership, status codes, opaque types, reentrancy, HAL seams and failure-state behavior.
- Arrays, Strings & BuffersArrays decay and the length does not travel; strings are a convention, not a type; and every length in a received packet is data rather than fact. Spans, the three string copies, framing and resynchronisation, serialisation, and parsing untrusted input safely.
- Object Layout & StorageWhere an object lives and what it costs: storage duration and linkage, the sections a declaration lands in, struct layout and the padding that makes a struct larger than its members, allocation without a heap, and integrity checks over stored data.
- C Basics & the Translation UnitFrom source text to a linked image: declarations against definitions, the translation unit the compiler actually sees, the preprocessor and what a macro can and cannot do, and the four build stages with the error vocabulary each one produces.
- FSM / DispatchDesign finite state machines with dispatch tables in embedded C. Interactive lab demonstrates state transitions and event handling for firmware.
- Embedded CA complete source-to-silicon workbench: C semantics, arrays and APIs, compiler and startup, MMIO, interrupts, DMA and caches, bounded systems, testing, safety evidence and production defense.