Firmware field guide · concurrency and synchronization
Name every actor. Protect the invariant. Prove bounded progress.
Concurrency becomes manageable when every shared object has an owner, every handoff has a publication rule, and every wait has a bound. Use one single-core Cortex-M4 system to compare mutexes, queues, notifications, atomics, priority inheritance, and explicit buffer ownership.
Separate atomic access, memory ordering, ownership, and lifetime
Choose an RTOS primitive by meaning, not by habit
Calculate blocking and buffer capacity under worst-case timing
Boundary: this guide begins with named task, ISR, DMA, and peripheral actors, but it does not reteach NVIC priority, masking, handler design, or DMA-completion service. Use the interrupts and DMA guide for those mechanics. Here we own task/task synchronization, C memory ordering, priority inversion, deadlock, and buffer lifetime after handoff.
01Actors
Context · owner · invariant · lifetime
Start with the shared state, not the lock.
A task, ISR, DMA engine, and peripheral can all influence one result even on a single-core MCU. List who can read, who can write, when ownership changes, and what must remain true before choosing any synchronization mechanism.
Execution model
Interleaving is enough to create a race
Two tasks do not need to execute simultaneously. Preemption between a multi-step write and a read can expose a mixed configuration. DMA can operate in parallel with the core, and a peripheral can change status independently.
Draw the legal preemption points and hardware ownership intervals before calling a sequence “atomic.”
Design model
Give each object one publication contract
Name the writer, readers, lifetime, reuse signal, and invalid state. Prefer single-writer or single-owner designs. A queue copy or command message often removes shared writable state entirely.
The invariant belongs in code structure and tests, not only in a comment beside a mutex.
Failure policy
Define overload before it happens
Every bounded queue or buffer pool eventually fills under a long enough consumer blackout. Decide whether to block, drop newest, drop oldest, overwrite with a sequence gap, shed optional work, or enter a fault state.
“Should never happen” is not a backpressure policy.
Interactive lab 1 · interleaving explorer
Can a reader observe half of a configuration update?
Task A changes gain and sample rate. Task B reads both fields to program hardware. Compare an unsafe shared struct with a mutex, queue copy, and single-owner command design.
1Old config
2Write gain
3Reader runs
4Write rate
5Commit
Step 1 of 5 · old configurationPublished pair is { gain=2, rate=10 kHz }
Both tasks agree on one complete old configuration.
InvariantHardware must consume gain and rate from the same configuration revision
Current resultSAFE SO FAR · no update has begun
Checkpoint · 2 easy, 2 medium, 1 hard
Can you name the owner before the primitive?
Easy What is an invariant?
An invariant is a property that must remain true at every externally visible point, such as “gain and rate belong to the same revision” or “a DMA buffer has exactly one owner.” It tells you what synchronization must protect.
What this tests: reasoning from correctness rather than APIs.
Easy Can a race occur on a single-core processor?
Yes. Preemption can interleave two multi-step operations, and DMA or peripherals can act independently of the core. Simultaneous instruction execution is not required for one actor to observe another actor’s partial state.
What this tests: interleaving versus physical parallelism.
Medium Two tasks read a configuration, but only one task ever writes it. Is synchronization unnecessary?
No. Single writer removes write/write conflict but not read/write publication. Readers still need a complete revision and a lifetime rule. Use a mutex, immutable copy with synchronized pointer/index publication, queue copy, or single-owner command path.
What this tests: single-writer benefits and remaining reader contract.
Medium Why can a pointer queue be less safe than a value-copy queue?
The pointer transfers an address, not necessarily ownership or lifetime. The producer may reuse stack storage or mutate the object while the consumer reads it. A value-copy queue gives the queue its own snapshot; a pointer queue needs explicit immutable lifetime and return/reuse rules.
What this tests: aliasing, lifetime, and ownership transfer.
Hard Design the ownership of a control configuration used by acquisition and telemetry tasks.
Give one control task exclusive mutable ownership. Other tasks send bounded commands to it. After validation, the owner publishes an immutable revision by queue copy or release/acquire index into a stable double buffer. Acquisition applies revisions only at a documented boundary; telemetry reports the committed revision number.
Define queue-full behavior, timeout rollback, object lifetime, and boot defaults. Prove no actor observes a partially validated configuration.
What this tests: complete actor map, ownership transfer, overload, and proof.
02Publication
Atomicity · ordering · visibility · reuse
An atomic flag does not automatically publish its payload.
Atomicity prevents one object access from tearing. Publication additionally requires an ordering relationship so a consumer that observes “ready” also observes the payload writes that happened before it. The payload must remain stable until reuse is acknowledged.
Not synchronization
volatile is not a thread contract
volatile is used for observable accesses such as MMIO and some implementation-defined environments. It does not make a compound operation atomic, remove a C data race, or create release/acquire ordering between tasks.
Atomic object
Choose order for the actual guarantee
Relaxed atomics provide atomic modification order for the atomic object but no payload ordering. A release store can publish prior writes to an acquire load that reads from the matching release sequence.
Do not assume an atomic type is lock-free. Verify atomic_is_lock_free, compiler documentation, and the target port where latency or ISR legality depends on it.
Lifetime
Publication needs a reuse protocol
Even correct release/acquire code fails if the producer overwrites the payload while the consumer still uses it. Add acknowledgment, generation counters, immutable allocation, double buffering, or a queue whose ownership rules cover reuse.
Interactive lab 2 · publication litmus
When does “ready” make the payload visible?
Compare plain, volatile, relaxed atomic, and release/acquire flag code. The readout states only the guarantee provided by the C abstract machine.
Language guaranteeIf the acquire reads the released value, the prior payload write happens before the consumer reads it
This publishes one stable payload revision. It does not by itself prevent the producer from reusing the payload too early.
Allowed conclusionORDERED PUBLICATION · add an explicit acknowledgment or immutable lifetime before reuse
Target checkVerify the chosen atomic type is implemented with acceptable bounded behavior on the compiler and Cortex-M4 target
Checkpoint · 2 easy, 2 medium, 1 hard
Can you distinguish atomic access from publication?
Easy What does atomicity guarantee for one object?
An atomic access participates in the atomic object’s modification order and is not observed as a torn mixture of two writes. It does not automatically group adjacent objects into one transaction or publish unrelated payload writes.
What this tests: scope of an atomic operation.
Easy Does a volatile ready flag publish an ordinary payload?
No. volatile makes specified accesses observable to the C implementation, but it does not create an inter-thread happens-before relationship for the payload. Use a publication design whose release/acquire operation covers a stable payload, or transfer ownership through an RTOS primitive with a documented memory contract.
What this tests: volatile limits and payload publication.
Medium When does a release store synchronize with an acquire load?
When the acquire load observes the value written by the release store or an applicable release sequence. Then writes sequenced before the release happen before reads sequenced after the acquire. Merely using acquire somewhere does not synchronize unrelated values.
What this tests: the reads-from relationship, not keyword matching.
Medium When is memory_order_relaxed useful?
It is useful when you need atomicity and per-object modification order but no cross-object publication, such as an independent diagnostic count. It is not sufficient for a ready flag that must make ordinary payload writes visible.
What this tests: matching memory order to the invariant.
Hard Publish a multi-field snapshot without holding a mutex while readers process it.
Use two stable buffers. The single writer fills the inactive buffer, validates it, then release-stores its index or generation. A reader acquire-loads the index and uses only that immutable revision. Add a reader acknowledgment, reference count, epoch, or bounded copy so the writer cannot reuse the buffer while a reader still holds it.
If multiple writers exist, serialize them first. Verify atomic lock freedom only if the design’s latency depends on it, and add DMA/cache maintenance if the selected target requires it.
What this tests: coherent publication plus lifetime and reuse.
03Primitives
Ownership · signaling · transfer · exclusion
Choose the primitive whose meaning matches the contract.
Mutexes express task-owned exclusion. Semaphores and notifications signal availability. Queues transfer bounded messages. Event bits summarize conditions. Critical sections suppress scheduling or interrupts for a tightly bounded operation. Similar APIs do not make these mechanisms interchangeable.
Owned exclusion
Mutex
The task that takes a mutex owns it and must release it. FreeRTOS mutexes add priority inheritance, making them appropriate for short task/task resource protection. They are not an ISR signaling primitive.
Signal or count
Semaphore or notification
A binary semaphore represents an event; a counting semaphore represents available units or accumulated events. A direct task notification is compact and fast when the signal has one receiving task and its value semantics fit.
Transfer
Queue
A queue copies fixed-size messages into bounded storage and makes overload visible. It is often the cleanest way to move a command or immutable snapshot to a single owner.
Decision matrix · FreeRTOS semantics
Ask what crosses the boundary.
Port APIs and ISR-safe variants are target-specific. The table captures intent, not a substitute for the official API contract.
Need
Default choice
Ownership rule
Blocking rule
Common mistake
Protect task-owned bus transaction
Mutex
Taker releases
Bound wait and hold time
Giving from a different task or ISR
Wake one worker for an event
Task notification or binary semaphore
No resource ownership
Receiver may block
Treating repeated events as an unbounded count
Track N identical resources
Counting semaphore
Count represents available units
Take blocks only by policy
Count drifting from real resources
Transfer a command snapshot
Queue by value
Queue owns copied bytes
Define full/empty timeouts
Blocking forever while holding another lock
Wait for several level conditions
Event group
Bits represent documented conditions
Define clear-on-exit behavior
Using one bit as a lossless event counter
Protect a few instructions from preemption
Critical section
Current context owns a bounded region
Never block inside
Wrapping slow I/O or unknown library code
Checkpoint · 2 easy, 2 medium, 1 hard
Can you select by semantics?
Easy What key property distinguishes a mutex from a binary semaphore?
A mutex has ownership: the task that locks it must unlock it, and the RTOS can associate priority inheritance with that owner. A binary semaphore represents a signal and may be given by a different permitted context.
What this tests: exclusion ownership versus signaling.
Easy When is a queue preferable to a shared global plus a notification?
When the consumer needs a stable command or snapshot and bounded backlog matters. A value-copy queue combines transfer, lifetime, ordering, and full/empty policy instead of leaving those contracts around a mutable global.
What this tests: message transfer and explicit capacity.
Medium Can an event bit count five identical arrivals?
No. A bit represents a level such as “work exists”; setting it repeatedly while already set does not preserve multiplicity. Use a counting semaphore, queue entries, notification count, or explicit bounded counter when every arrival matters.
What this tests: level state versus event count.
Medium Why is blocking on a queue while holding an unrelated mutex dangerous?
The task can retain the mutex for the entire queue wait, inflating blocking and possibly forming a wait-for cycle if the queue consumer needs that mutex. Release unrelated ownership before a blocking call or redesign the transaction.
What this tests: compositional blocking and deadlock risk.
Hard Select primitives for a UART command service with many clients and one hardware owner.
Give one service task exclusive hardware ownership. Clients send bounded command records through a queue and include a completion token or response queue when needed. The service serializes transactions and signals completion without clients taking a hardware mutex. Define queue-full, client timeout, cancellation, and late-response disposal.
The ISR-to-service wakeup remains in the interrupts guide; task/task command ownership belongs here. Avoid holding client locks while waiting for a response.
What this tests: single-owner architecture, bounded messaging, and cross-guide boundaries.
04Blocking
H · M · L · critical section · inheritance
A high-priority task can wait behind medium-priority work.
Priority inversion begins when a low-priority task owns a resource needed by a high-priority task. An unrelated medium-priority task can then delay the owner. Priority inheritance reduces this specific interference, but it does not make long critical sections, nested locks, or deadlock safe.
Blocking term
Bound the owner’s remaining work
When H requests the mutex, its direct blocking depends on the lower-priority owner’s remaining critical section plus protocol and scheduling effects. Average hold time is not the deadline argument.
Protocol
Inheritance boosts the owner temporarily
When H blocks, L inherits H’s priority so M cannot preempt L. Once L releases the relevant mutex, the RTOS restores priority according to its implementation rules.
Study the actual FreeRTOS version and nested-mutex limitations.
Limit
Inheritance does not solve cycles
If tasks acquire A then B and B then A, no priority boost can create progress. Enforce one global lock order, avoid blocking calls while holding unrelated locks, and design rollback for timeouts.
Interactive lab 3 · priority inversion simulator
Follow L, H, and M on one exact timeline.
L enters a 3 ms mutex-protected section at t=0. H wakes and blocks at 1 ms. M wakes at 1.2 ms with 8 ms of work. Assume zero scheduler overhead and no other releases.
t=0L locks
t=1.0H blocks
t=1.2M wakes
releaseL unlocks
nextH runs
Step 1 of 5 · t=0L starts its 3 ms critical section at base priority L
No higher-priority task is runnable yet.
High-task blockingNot started; H has not requested the mutex
Protocol conclusionWithout inheritance, later medium work can delay the low-priority owner
Checkpoint · 2 easy, 2 medium, 1 hard
Can you calculate blocking instead of naming the pattern?
Easy What is priority inversion?
A higher-priority task is prevented from progressing because a lower-priority task owns a required resource. Unrelated intermediate-priority work can extend the delay if the owner is preempted.
What this tests: resource-induced blocking versus ordinary scheduling.
Easy What does priority inheritance change?
It temporarily raises the priority of the resource owner when a higher-priority task blocks on its mutex. This lets the owner finish and release without interference from tasks below the inherited priority.
What this tests: owner boosting, not waiter boosting.
Medium In the lab, how long does H wait without inheritance?
H blocks at 1 ms. L executes until 1.2 ms, leaving 1.8 ms of its section. M then runs for 8 ms until 9.2 ms; L finishes at 11.0 ms. H waits 11.0 - 1.0 = 10.0 ms under the stated zero-overhead assumptions.
What this tests: an exact preemptive timeline.
Medium How long does H wait with inheritance in the same lab?
At 1 ms, L inherits H’s priority and continues. M cannot preempt at 1.2 ms. L completes its 3 ms section at t=3 ms, so H waits 3 - 1 = 2 ms, excluding stated-zero protocol overhead.
What this tests: the bound inheritance improves and the work it cannot remove.
Hard A task holds two mutexes while calling a slow driver. Why is “FreeRTOS has inheritance” not a schedulability proof?
The driver duration may be unbounded, and nested mutex behavior can delay priority disinheritance depending on the kernel version. Other tasks may form lock-order cycles. Measure worst-case hold time, split slow I/O from protected state, establish a global lock order, and analyze each blocking chain against deadlines.
Use the official FreeRTOS implementation contract for the exact version. Inheritance reduces one class of interference; it does not replace whole-system blocking analysis.
What this tests: protocol limits, nesting, and schedulability evidence.
05Buffers
Ownership state · capacity · backpressure · reuse
A completed buffer is not automatically free.
After hardware completes a transfer, software must publish a stable buffer, process it, and explicitly return it before reuse. Capacity converts a maximum consumer blackout into a concrete number of simultaneously owned buffers.
Lifecycle
Make ownership a checked state machine
Use FREE → DMA_OWNED → READY → PROCESSING → FREE. Only the owner may mutate payload or metadata. Illegal transitions indicate double return, overwrite, or stale handles.
Capacity
Budget blackout, not average throughput
ControlNode completes 128 samples at 20 ksample/s every 128 / 20,000 = 6.4 ms. With no backlog at blackout start, total buffers needed are one active DMA buffer plus the number that can complete while the consumer is unavailable.
Policy
State what happens when full
A control loop might stop acquisition and fault; telemetry might drop oldest with a sequence gap; a logger might shed lower-value records. The decision depends on product semantics, not the queue API.
Interactive lab 4 · buffer ownership and capacity
Will this pool survive the consumer blackout?
Choose the worst-case processing blackout and total buffer count. The lab calculates completed capacity and identifies overwrite or margin.
Consumer blackout
Total pool size
FREEDMA_OWNEDREADYPROCESSINGFREE
Capacity calculationceil(18 / 6.4) = 3 completed buffers; plus 1 active DMA buffer = 4 total
Pool verdictPASS WITH NO MARGIN · 4 available, 4 required
The pool survives exactly the assumed blackout. Add margin for scheduling jitter, handoff overhead, and a blackout that begins just before completion.
Overflow policyIf no FREE buffer exists, stop acquisition and record a capacity fault; never overwrite DMA_OWNED or PROCESSING storage
Cross-guide boundary: how the DMA-complete event is configured, acknowledged, prioritized, and handed out of interrupt context belongs to the interrupts guide. This chapter begins once ownership of a completed buffer is being published to software.
Checkpoint · 2 easy, 2 medium, 1 hard
Can you prove ownership and capacity?
Easy Who may write a DMA_OWNED buffer?
Only the DMA engine according to the programmed transfer contract. Software may inspect control state required by the driver but must not reuse or mutate the payload until completion and visibility requirements transfer ownership.
What this tests: exclusive ownership during hardware production.
Easy Why include a generation number in a buffer handle?
It distinguishes the current ownership cycle from a stale reference to the same physical slot. Returning a handle with an old generation can be rejected instead of freeing a buffer that has already been reassigned.
What this tests: stale references and ABA-like reuse.
Medium How many total buffers does an 18 ms blackout require in this lab?
Each block takes 6.4 ms. ceil(18 / 6.4) = 3 buffers can complete while the consumer is unavailable. Add one active DMA buffer, giving four total before margin under the stated phase assumption.
What this tests: rates, ceiling, and simultaneous ownership.
Medium Is four buffers a robust production choice for that 18 ms blackout?
It is only the exact lab minimum. Scheduling jitter, completion-to-queue delay, blackout phase, processing release time, and measurement uncertainty can consume it. Add justified margin or tighten the blackout bound and prove it.
What this tests: mathematical minimum versus engineered capacity.
Hard Define a loss policy for acquisition data that controls a safety function and also feeds optional telemetry.
Separate the safety consumer from optional telemetry ownership. Reserve capacity or priority for the safety path, and never let telemetry retain the only processing buffer. If the safety path cannot obtain a complete sequence within its bound, stop or degrade the controlled function and record a fault. Telemetry may drop oldest or decimate with explicit sequence gaps.
Measure high-water marks, blackout duration, drops, and illegal transitions. Do not overwrite a buffer still owned by DMA or safety processing.
What this tests: product semantics, partitioned capacity, and observable overload.
06Liveness
Lock order · wait graph · timeout rollback · proof
Correct data is not enough if the system cannot progress.
Safety properties say bad states never occur. Liveness says required work eventually completes. A complete firmware design proves bounded queues, finite critical sections, acyclic lock acquisition, timeout rollback, and recovery when a dependency stops responding.
Deadlock
Make the wait-for graph acyclic
Enforce one global lock order, such as configuration before storage before transport. Detect violations in review and debug builds. Prefer message ownership when two subsystems would otherwise call each other while locked.
Timeout
Rollback every partial acquisition
A timeout is not complete until the task releases already-held resources, cancels or marks late work, restores state, and returns an unambiguous error. Otherwise retries leak tokens or corrupt ownership.
Proof
Measure the longest path
Instrument queue high-water marks, mutex hold times, blocking time, missed deadlines, buffer generations, timeout reasons, and recovery count. Verify worst-case combinations, not only one primitive in isolation.
Synthesis capstone · complete runtime contract
ControlNode concurrency proof sheet
Every row identifies the state owner, transfer primitive, capacity or blocking bound, overload action, and evidence. Replace lab values with measured product limits.
Flow
Owner
Transfer
Bound
Failure action
Proof
Configuration commands
Control task
Queue by value
8 commands
Reject newest with explicit busy response
Queue high-water and revision trace
Published configuration
Immutable revision
Release/acquire index
2 buffers
Delay reuse until acknowledgment
Generation and reader-ack assertions
ADC blocks
DMA → acquisition task
Ownership state plus queue
5 buffers
Stop acquisition on no FREE buffer
Illegal-transition and high-water counters
Shared storage service
Storage task
Request/response queues
25 ms request deadline
Cancel token and discard late response
End-to-end latency histogram
Short I²C register sequence
Calling task with mutex
Owned exclusion
350 µs hold
Release, reset bus owner state, report timeout
Maximum hold and waiter-block time
Fault notification
Safety task
Dedicated notification
One pending level plus cause record
Enter safe state on missed service bound
Cause sequence and response timestamp
Global lock orderConfiguration → storage metadata → transport; no reverse acquisition is permitted
Blocking ruleNo task blocks on a queue, notification, or slow peripheral while holding an unrelated mutex
Whole-system invariantEach resource has one owner, every transfer has bounded storage and lifetime, and each timeout either completes or rolls state back
Checkpoint · 2 easy, 2 medium, 1 hard
Can you prove the runtime eventually moves?
Easy What is deadlock?
Deadlock is a state in which actors wait on a cycle of resources or conditions that none can satisfy. With mutual exclusion, hold-and-wait, no forced release, and circular wait, progress stops even if CPU time remains available.
What this tests: liveness failure versus CPU overload.
Easy How does a global lock order prevent a common deadlock?
If every task acquires locks only in one increasing order, a circular wait among those locks cannot form. The order must include every acquisition path, callbacks, error paths, and helper functions.
What this tests: a structural deadlock-prevention argument.
Medium A queue send times out after a task has reserved a buffer. What must happen?
The task must return or otherwise resolve the reserved buffer exactly once, update generation/state consistently, and report that the message was not transferred. A retry cannot reuse the same ownership token ambiguously.
What this tests: timeout rollback and resource conservation.
Medium Why is a timeout alone not proof of liveness?
A timeout bounds one wait, but the caller may immediately retry forever, leak a resource, or leave the callee processing a request whose response becomes stale. Prove the complete state transition after timeout, including cancellation, cleanup, retry budget, and recovery.
What this tests: local bounds versus system progress.
Hard Review the capstone for a failure where storage stalls, telemetry backs up, and acquisition continues.
Storage requests must time out with cancellation and no held unrelated mutex. Telemetry must apply its bounded drop or shedding policy instead of retaining acquisition buffers. Acquisition capacity remains reserved for the safety path; if its no-FREE bound is reached, it stops or faults according to policy. The control task remains responsive because it owns configuration through a separate bounded queue.
Test the combined fault with instrumentation for queue high-water, ownership transitions, hold times, late responses, drops, and final recovery. The system passes only if no buffer is overwritten, the safety bound holds, and normal service resumes or enters a defined degraded state.
What this tests: interacting bounds, partitioned ownership, and recovery proof.
Primary references
Verify the language, architecture, RTOS, compiler, and target together.