HomeAbout UsContact Us

Effective Technical Writing for Embedded Engineers

By Jithin Tom
July 31, 2026
4 min read
Effective Technical Writing for Embedded Engineers

Table Of Contents

01
The Documentation Hierarchy
02
Architecture Decision Records (ADRs)
03
Register-Level Documentation Template
04
Hardware-Software Interface (HSI) Contracts
05
Decision Logs in Code
06
Technical Writing for Safety-Critical Systems (ISO 26262 & IEC 61508)
07
Documentation That Engineers & Auditors Actually Read
08
Common Anti-Patterns to Avoid
09
Tooling That Automates Documentation
10
Summary
11
Related Reading
12
References
13
Frequently Asked Questions

Technical writing is not a soft skill — it is a force multiplier for embedded and safety-critical engineering teams. Every hour spent writing clear documentation saves ten hours of debugging, onboarding, regulatory auditing, and rework. Yet many embedded engineers treat documentation as an afterthought, producing brief README files that explain how to compile code but not how the system works, why decisions were made, or how it behaves under failure modes.

This article covers practical patterns for writing embedded and safety-critical technical documentation that engineers and auditors actually read, trust, and verify.

The Documentation Hierarchy

Embedded projects require four distinct layers of documentation, each serving a specific audience, abstraction level, and verification purpose:

+-------------------------------------------------------------------+
| DOCUMENTATION LAYERS |
+-------------------------------------------------------------------+
| |
| L1 SYSTEM & SAFETY SPECIFICATION <- Architect, Safety Engineer |
| - System & safety requirements (ISO 26262 / IEC 61508) |
| - Interfaces, protocols, safety goals, ASIL/SIL targets |
| - Bidirectional requirements traceability & V&V criteria |
| |
| L2 ARCHITECTURE DECISION LOG <- Tech Lead, Systems Lead |
| - Why we chose MCU / RTOS / memory map / safety mechanisms |
| - Freedom From Interference (FFI) & MPU partitioning |
| - Trade-offs considered, alternatives rejected |
| |
| L3 MODULE DESIGN & HSI DOCS <- Implementer, Verifier |
| - Register maps, state machines, ISR latency budgets |
| - Hardware-Software Interface (HSI) contracts & protocols |
| - Unit & Hardware-in-the-Loop (HIL) test strategies |
| |
| L4 CODE-LEVEL & SAFETY COMMENTS <- Maintainer, Auditor |
| - Explains 'why' behind register bits & timing constraints |
| - Silicon errata workarounds tagged with silicon revision |
| - MISRA compliance deviations & cycle-budget annotations |
| |
+-------------------------------------------------------------------+

Most teams only produce L4 (scattered comments) and call it done. The missing upper layers are where integration bugs, race conditions, and compliance failures hide.

Architecture Decision Records (ADRs)

An Architecture Decision Record (ADR) captures a significant technical choice with its context, rationale, and consequences. Keep them lightweight — one Markdown file per decision, stored in docs/adr/ with sequential numbering.

In safety-critical systems, ADRs serve as evidence for why specific safety mechanisms, memory protection layouts, or synchronization primitives were selected over alternatives.

# ADR-0042: Use FreeRTOS Stream Buffers for UART DMA Reception
## Status
Accepted
## Context
We need high-throughput UART reception (up to 4 Mbps) on STM32H743VI with minimal CPU overhead. Options considered:
1. Interrupt-per-byte with ring buffer → CPU saturation at high baud rates.
2. DMA + circular buffer + idle-line detection → Complex ISR handling, fragile timing window.
3. FreeRTOS Stream Buffers + DMA → Direct task notification, optimized thread-safe IPC.
## Decision
Use Option 3. Stream buffers accept variable-length writes from ISR context via `xStreamBufferSendFromISR()` and notify the consumer task directly without mutex overhead.
## Consequences
- Requires FreeRTOS V10.0.0+ (Stream Buffer API introduction).
- Data is passed by light copy into stream buffer memory (not zero-copy; for zero-copy DMA, direct task notification with double-buffered pointer swapping is required).
- DMA HT (Half-Transfer) and TC (Transfer-Complete) interrupts feed stream buffer.
- Consumer task blocks on `xStreamBufferReceive()` with a configurable timeout.
- Memory allocation: 4 KB stream buffer + 2 KB DMA buffer per UART interface.
## References
- FreeRTOS Stream Buffer API: https://www.freertos.org/RTOS-stream-buffers.html
- STMicroelectronics STM32H7 DMA + UART Application Note: AN4882

Rule: Every architectural decision affecting hardware interfaces, memory layout, RTOS configuration, fault tolerance, or third-party integration gets an ADR.

Register-Level Documentation Template

When writing device drivers or register-level interface code, document each register configuration with a clear table or structured block comment. The following C example demonstrates STM32H7 I2C timing configuration for 400 kHz Fast Mode:

/* I2C_TIMINGR register configuration for 400 kHz Fast Mode
* MCU Variant: STM32H743VI, PCLK1 = 120 MHz, I2C Clock (I2CCLK) = 120 MHz
* Datasheet: ST Reference Manual RM0433 Rev 8, Section 53.4.9
* Silicon Errata: ES0392 Rev 7, Section 2.14.1 (I2C analog filter delay) */
typedef struct {
uint32_t presc; // PRESC[3:0] - Clock prescaler: t_PRESC = (PRESC + 1) * t_I2CCLK
uint32_t scldel; // SCLDEL[3:0] - Data setup time: t_SCLDEL = (SCLDEL + 1) * t_PRESC (t_SU_DAT)
uint32_t sdadel; // SDADEL[3:0] - Data hold time: t_SDADEL = SDADEL * t_PRESC (t_HD_DAT)
uint32_t sclh; // SCLH[7:0] - SCL high period: t_SCLH = (SCLH + 1) * t_PRESC
uint32_t scll; // SCLL[7:0] - SCL low period: t_SCLL = (SCLL + 1) * t_PRESC
} I2C_Timing_t;
/* Prescaler: PRESC = 1 -> f_PRESC = 120 MHz / (1 + 1) = 60 MHz (t_PRESC = 16.67 ns)
* Setup time (SCLDEL = 4): (4 + 1) * 16.67 ns = 83.33 ns (+ analog filter delay = ~110 ns, t_SU_DAT min 100 ns)
* Hold time (SDADEL = 2): 2 * 16.67 ns = 33.33 ns (t_HD_DAT min 0 ns)
* SCL High (SCLH = 53): (53 + 1) * 16.67 ns = 900 ns (0.9 us, t_HIGH min 0.6 us)
* SCL Low (SCLL = 95): (95 + 1) * 16.67 ns = 1600 ns (1.6 us, t_LOW min 1.3 us)
* Total SCL period: (54 + 96) * 16.67 ns = 150 * 16.67 ns = 2.50 us -> Exact 400.0 kHz */
const I2C_Timing_t I2C_TIMING_400KHZ = {
.presc = 0x1, // 60 MHz prescaled clock (t_PRESC = 16.67 ns)
.scldel = 0x4, // 5 cycles = 83.3 ns setup time (t_SU_DAT)
.sdadel = 0x2, // 2 cycles = 33.3 ns hold time (t_HD_DAT)
.sclh = 0x35, // 54 cycles high = 900 ns (0.9 us)
.scll = 0x5F, // 96 cycles low = 1600 ns (1.6 us)
};
/* Computed 32-bit register value conforming to STM32 I2C_TIMINGR bit layout:
* Bits [31:28] = PRESC, Bits [23:20] = SCLDEL, Bits [19:16] = SDADEL,
* Bits [15:8] = SCLH, Bits [7:0] = SCLL */
#define I2C_TIMINGR_VALUE (((uint32_t)(I2C_TIMING_400KHZ.presc) << 28) | \
((uint32_t)(I2C_TIMING_400KHZ.scldel) << 20) | \
((uint32_t)(I2C_TIMING_400KHZ.sdadel) << 16) | \
((uint32_t)(I2C_TIMING_400KHZ.sclh) << 8) | \
((uint32_t)(I2C_TIMING_400KHZ.scll) << 0))

Key Elements of High-Quality Register Documentation:

  • Datasheet section reference — Allows independent verification by peer reviewers and safety auditors.
  • Errata reference — Explains non-obvious constraints resulting from silicon bugs.
  • Explicit bit field mapping — Disambiguates setup time (SCLDEL) vs hold time (SDADEL).
  • Mathematical proof in comments — Shows intermediate cycle calculations and total frequency matching the specification.
  • MISRA-compliant type casting — Prevents implicit integer conversion warnings in safety-critical builds.

Hardware-Software Interface (HSI) Contracts

The most expensive bugs in embedded development occur at the hardware-software boundary. In ISO 26262 and DO-178C workflows, the Hardware-Software Interface (HSI) specification is a mandatory work product.

# HW-SW Contract: ADC1 + DMAMUX1/DMA2 + FreeRTOS Task (STM32H7)
## Signal Chain
ADC1 (16-bit) → DMAMUX1 → DMA2 Stream 0 (circular, half/transfer complete IRQ) → FreeRTOS Task (Priority 6)
## Timing Requirements (ADC clock = 36 MHz)
| Parameter | Symbol | Min | Typ | Max | Unit | Notes |
|--------------------|----------|------|------|------|------|------------------------------------------|
| Sample rate | f_s | - | 100 | - | kSPS | 100 kSPS per channel (2 channels total) |
| Conversion time | t_conv | - | 0.36 | 0.50 | µs | 16-bit: 4.5 sampling + 8.5 conv = 13 cyc |
| DMA transfer time | t_dma | - | 0.8 | 1.0 | µs | 16-bit × 2 ch, 16-beat burst |
| Task wake latency | t_wake | - | 2.1 | 3.5 | µs | FreeRTOS context switch + D-cache inv |
| End-to-end latency | t_e2e | - | 3.3 | 5.0 | µs | Sample completion to task start |
## Memory & Cache Buffer Contract
- DMA buffer: 2 × 1024 `uint16_t` (ping-pong), aligned to 32 bytes (`__attribute__((aligned(32)))`) for Cortex-M7 D-cache line compatibility.
- Half-Transfer (HT) IRQ → Notify task to process first half; call `SCB_InvalidateDCache_by_Addr()` prior to buffer read.
- Transfer-Complete (TC) IRQ → Notify task to process second half; call `SCB_InvalidateDCache_by_Addr()` prior to buffer read.
- Processing deadline: Task MUST complete processing each half within 5.12 ms (200 kSPS total throughput = 5.12 ms filling time per 1024-sample half).
## Error Handling & Diagnostic Coverage
| Condition | Detection Mechanism | Action / Safe State |
|------------------------|---------------------|------------------------------------------|
| DMA transfer error | DMA TEIF flag | Assert, log event, re-initialize DMA |
| ADC overrun | ADC OVR flag | Flag diagnostic fault, increment counter |
| Task overrun (missed) | Sequence counter gap| Discard stale frame, enter safe state |
## Power & Initialization Sequencing
1. Enable ADC clock in RCC.
2. Perform ADC self-calibration (single-ended & differential modes).
3. Configure DMAMUX1 routing and DMA2 Stream 0 (circular mode, HT/TC IRQs enabled).
4. Create consumer FreeRTOS task.
5. Enable DMA stream and start ADC conversions (timer-triggered).
## Verification Checklist
- [ ] Monitor ADC EOC, DMA IRQ, and task toggle pins on logic analyzer.
- [ ] Verify zero sample loss at 100 kSPS/channel over 24-hour continuous run.
- [ ] Inject DMA transfer error → Verify diagnostic interrupt fires and logs correctly.
- [ ] Inject ADC overrun → Verify error counter increments and safe state logic executes.

Rule: Never rely on verbal agreements for hardware-software boundaries. Document them as formal contracts before writing driver or application logic.

Decision Logs in Code

For localized design choices that do not require a full ADR but would confuse future maintainers, use a standardized decision comment block:

/* DECISION: Use LPTIM1 for system tick instead of SysTick
* REASON: SysTick halts in System Stop mode; LPTIM1 continues running off LSE (32.768 kHz) in D1/D2/D3 System Stop modes.
* TRADE-OFF: LPTIM1 tick resolution is 30.5 µs (32.768 kHz) vs SysTick 2.08 ns (480 MHz).
* ALTERNATIVES: SysTick + RTC wake-up timer (rejected due to higher wake latency and power state transitions).
* REFERENCE: ST RM0433 Sec 41 (LPTIM), FreeRTOS tickless idle porting guide
* AUTHOR: jtom, 2026-03-15
*/

Format standard: DECISION, REASON, TRADE-OFF, ALTERNATIVES, REFERENCE, AUTHOR, DATE. This structure is easily greppable and actionable during code reviews.

Technical Writing for Safety-Critical Systems (ISO 26262 & IEC 61508)

For engineers developing systems subject to functional safety standards (ISO 26262 for automotive, IEC 61508 for industrial, DO-178C for aerospace, IEC 62304 for medical), documentation is an essential part of the safety case.

1. Bidirectional Requirements Traceability

Safety standards require full traceability from top-level safety goals down to individual lines of code and test cases:

System Safety Requirement (SSR-102)
└── Software Safety Requirement (SWR-204)
└── Software Architecture Component (ARCH-CAN-01)
└── Module Design & Source Code (can_driver.c:L142)
└── Unit Test Case (test_can_rx_timeout())

In technical documentation and code comments, reference requirement identifiers explicitly:

/* Implements SWR-204 [ASIL-C]: Verify CAN bus-off recovery timer
* Safety Goal: SG-03 (Prevent unannounced loss of CAN communication) */
void CAN_CheckBusOffRecovery(void) {
// ...
}

2. Documenting Safety Mechanisms & Diagnostic Coverage

When writing safety-critical design docs, specify:

  • Diagnostic Coverage (DC): High (≥99%), Medium (≥90%), or Low (≥60%).
  • Fault Detection Time Interval (FDTI): Maximum allowable time between fault occurrence and detection.
  • Fault Reaction Time Interval (FRTI): Maximum time to transition the system to a defined safe state.

3. Documenting Coding Standard Deviations

When code must deviate from standards like MISRA C:2012 / MISRA C:2023, document the deviation inline with justification:

/* MISRA C:2012 Rule 11.4 Deviation
* Justification: Hardware memory-mapped register access requires casting integer address to volatile pointer.
* Verification: Address verified against ST RM0433 memory map Section 2.3. */
#define I2C1_BASE_ADDR ((I2C_TypeDef *) 0x40005400U)

Documentation That Engineers & Auditors Actually Read

1. Maintain a “Quick Start” for Hardware Bring-Up

# Quick Start: STM32H743VI + FreeRTOS + LwIP
## Hardware Required
- Custom Board Rev B3 or later (Rev B2 has PHY reset bug; see ERR-007)
- ST-LINK/V3 or J-Link OB debugger
- 12V / 2A DC supply, Ethernet cable
## First Boot Procedure (5 minutes)
1. Connect ST-LINK debugger and apply 12V power.
2. Launch OpenOCD server:
`openocd -f interface/stlink.cfg -f target/stm32h7x.cfg`
3. In a separate terminal, launch GDB:
`arm-none-eabi-gdb build/firmware.elf`
4. Connect GDB to OpenOCD session:
`(gdb) target extended-remote :3333`
`(gdb) load`
`(gdb) monitor reset halt`
`(gdb) continue`
## Verification Steps
- Green LED (PD13) blinks at 1 Hz → FreeRTOS system tick is running.
- Blue LED (PD15) solid ON → LwIP stack initialized, DHCP IP acquired.
- Host ping test: `ping <board-ip>` → Ethernet communication verified.
## Known Hardware & Silicon Issues
| Issue | Workaround | Target Fix |
|--------------------------------------|----------------------------------------------|-----------------|
| PHY reset fails on cold power-up | Cycle 12V main power supply | Board Rev B4 |
| ADC calibration hangs if VDDA < 2.7V | Verify power rail before calling calibration | Firmware v2.1.0 |

2. Maintain a “Known Issues” Log Per Module

## Known Issues: I2C Driver (i2c_stm32h7.c)
| ID | Description | Workaround | Status |
|--------|-----------------------------------------------------------|--------------------------------------------------------------|-----------------------|
| I2C-01 | Clock stretching timeout at 400 kHz with legacy slave ICs | Reduce clock to 100 kHz or implement bit-bang fallback | Open (Hardware limit) |
| I2C-02 | Bus busy flag stuck after arbitration loss | Execute software reset of I2C peripheral + GPIO clock toggle | Fixed (v1.3.0) |
| I2C-03 | DMA TX complete flag sets before final byte is on wire | Add 2-byte dummy buffer padding for DMA transfers | Verified (v1.4.0) |

3. Document Executable Test Procedures

## Integration Test: UART DMA + FreeRTOS Stream Buffer
### Setup
- Target Board: STM32H743VI Custom Board Rev B3
- PC Interface: USB-to-UART Adapter (FT232H), 4 Mbps, 8N1
- Test Script: `scripts/uart_stress.py`
### Procedure
1. Flash `firmware_uart_dma_test.elf` to target board.
2. Run test script on host PC:
`python3 scripts/uart_stress.py --port /dev/ttyUSB0 --baud 4000000 --duration 60`
3. Monitor performance metrics and logic analyzer output.
### Pass Criteria
| Metric | Target Constraint | Measured Result |
|-----------------|-------------------|-----------------|
| Throughput | ≥ 3.8 Mbps | 3.92 Mbps |
| CPU Utilization | ≤ 15% | 11.4% |
| Buffer Overruns | 0 in 60 seconds | 0 |
| CRC Error Rate | 0 | 0 |

Common Anti-Patterns to Avoid

Anti-PatternWhy It FailsBetter Approach
“See datasheet” commentsReference manuals exceed 2000 pages; context is lostCite exact section, register name, and bit-field
Documenting “what”, not “why”Source code already shows what happensDocument timing constraints, silicon errata, and trade-offs
README-only documentationREADME explains build commands, not architectureHierarchy: System Spec → ADR → HSI Contract → Code Comments
Outdated commentsOutdated documentation causes severe debugging delaysEnforce comment reviews in PRs; remove invalid comments
Omitting hardware contextSoftware maintainers lack hardware schematicsInclude pin maps, memory layouts, and power-up sequences
Missing safety traceabilityAudits fail due to unlinked requirementsTag code and design blocks with requirement IDs (e.g., SWR-101)

Tooling That Automates Documentation

  • Doxygen + Graphviz — Auto-generate call graphs, data structures, and peripheral interaction diagrams from code annotations.
  • Markdown Lint (markdownlint-cli) — Enforce heading hierarchy, line lengths, and consistent formatting in CI/CD pipelines.
  • Vale — Prose linting engine enforcing consistent technical style and terminology.
  • Mermaid.js — Render sequence diagrams, state machines, and timing flows natively in Markdown.
  • Pre-Commit Hooks — Automatically validate Markdown formatting, link integrity, and requirement tagging prior to git commit.

Summary

Effective technical writing in embedded and safety-critical engineering is not about writing long documents — it is about precision at the hardware-software boundary.

The four-layer hierarchy (System Spec → ADR → Module Design / HSI → Code Comments) ensures that developers, architects, and compliance auditors find exact information at the right level of abstraction. Register tables with explicit formulas prevent bit-manipulation bugs. HSI contracts with timing and cache rules eliminate integration failures. ADRs and inline decision logs preserve design rationale for future maintainers.

Start small:

  1. Add an ADR for your next major technical choice.
  2. Add a structured register table with datasheet citations to your next driver module.
  3. Establish a Hardware-Software Interface contract for your next peripheral integration.

Treat technical writing as an essential engineering discipline, and it will compound in value across the entire lifecycle of your embedded system.

References

  1. International Organization for Standardization. “ISO 26262-6:2018 Road vehicles — Functional safety — Part 6: Software level development.” ISO, 2018.
  2. International Electrotechnical Commission. “IEC 61508-3:2010 Functional safety of electrical/electronic/programmable electronic safety-related systems - Part 3: Software requirements.” IEC, 2010.
  3. Diomidis Spinellis. “Effective Debugging: 66 Specific Ways to Debug Software and Systems.” Addison-Wesley, 2016. ISBN 978-0134394956.
  4. Michael Nygard. “Documenting Architecture Decisions.” Cutter IT Journal, Vol. 24, No. 2, 2011.
  5. FreeRTOS Kernel API Reference — Stream Buffers. Amazon Web Services. https://freertos.org/Documentation/02-Kernel/02-Kernel-features/04-Stream-and-message-buffers/01-RTOS-stream-and-message-buffers
  6. STMicroelectronics. “STM32H742, STM32H743/753, STM32H750 Value Line Reference Manual.” RM0433 Rev 8. https://www.st.com/resource/en/reference_manual/rm0433-stm32h742-stm32h743753-and-stm32h750-value-line-advanced-armbased-32bit-mcus-stmicroelectronics.pdf
  7. Karl Wiegers & Joy Beatty. “Software Requirements.” 3rd ed. Microsoft Press, 2013. ISBN 978-0735679665.
  8. NASA Software Engineering Handbook. NASA-HDBK-2203. Section 5.02: Interface Design Description. https://swehb.nasa.gov/spaces/SWEHBVD/pages/102695656/5.02+-+IDD+-+Interface-Design-Description

Frequently Asked Questions

Why is technical writing important for embedded engineers?

Embedded engineers produce firmware that runs on hardware others must integrate, debug, audit, and maintain. Clear documentation reduces integration time, prevents catastrophic field failures, satisfies safety-critical compliance (e.g., ISO 26262, IEC 61508, DO-178C), and enables seamless knowledge transfer when team members leave.

What distinguishes good embedded documentation from bad?

Good embedded documentation explains the 'why' behind decisions (register choices, timing constraints, memory layout, safety mechanisms), includes working code examples with exact bit-field formulas, and specifies hardware dependencies (MCU variant, peripheral version, errata). Bad documentation only describes 'what' the code does without context.

How should I document register-level code?

Use a structured table or comment mapping each register bit-field to its datasheet formula, setup/hold constraints, and exact calculated values. Include datasheet section references, errata workarounds, and show both the raw hex value and symbolic bitwise construction so readers can verify and modify.

What is the most common documentation gap in embedded projects?

Hardware-Software Interface (HSI) contracts — timing budgets, pin mappings, power sequencing, cache maintenance rules, and bootloader handoff protocols that must be formally agreed upon before hardware or software implementation begins.

How do I write documentation that survives team turnover and safety audits?

Establish a four-layer documentation hierarchy (System/Safety Spec → ADR → Module Design → Code Comments), maintain bidirectional requirements traceability, maintain a 'Known Issues' log with errata workarounds, and record architectural trade-offs in Architecture Decision Records.

Tags

technical-writingdocumentationcommunicationcareerembedded-systemssafety-critical

Share


Previous Article
FreeRTOS SMP: Multi-Core Task Affinity & Load Balancing
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Embedded Firmware Testing Strategies for Safety-Critical Systems
Embedded Firmware Testing Strategies for Safety-Critical Systems
July 25, 2026
3 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media