Final Part: This is Part 20 — the concluding article of the 20-part CMSIS Mastery Series. The skills from Parts 1–19 (bare-metal registers through RTOS, DSP, security, testing, and architecture) are the raw material. This part is the professional layer that wraps them: automated, repeatable, verifiable firmware delivery.
1
Overview & ARM Cortex-M Ecosystem
CMSIS layers, Cortex-M families, memory map, toolchains
2
CMSIS-Core: Registers, NVIC & SysTick
core_cmX.h, register access, interrupt controller, SysTick timer
3
Startup Code, Linker Scripts & Vector Table
Reset handler, BSS init, scatter files, boot process
4
CMSIS-RTOS2: Threads, Mutexes & Semaphores
Thread management, synchronization primitives, scheduling
5
CMSIS-RTOS2: Message Queues & Event Flags
Inter-thread comms, ISR-to-thread, real-time design patterns
6
CMSIS-DSP: Filters, FFT & Math Functions
FIR/IIR filters, FFT, SIMD optimizations
7
CMSIS-Driver: UART, SPI & I2C
Driver abstraction layer, callbacks, DMA integration
8
CMSIS-Pack & Software Components
Pack files, device support, dependency management
9
Debugging with CMSIS-DAP & CoreSight
SWD/JTAG, HardFault analysis, ITM tracing
10
Portable Firmware: Multi-Vendor Projects
HAL vs CMSIS, cross-platform BSPs, reusable libraries
11
Interrupts, Concurrency & Real-Time Constraints
Interrupt latency, critical sections, lock-free programming
12
Memory Management in Embedded Systems
Static vs dynamic, heap fragmentation, memory pools
13
Low Power & Energy Optimization
Sleep modes, clock gating, tickless RTOS, power profiling
14
DMA & High-Performance Data Handling
DMA basics, peripheral transfers, zero-copy techniques
15
Security: ARMv8-M & TrustZone
Secure/non-secure worlds, secure boot, firmware protection
16
Bootloaders & Firmware Updates
OTA updates, dual-bank flash, fail-safe strategies
17
Testing & Validation
Unity/Ceedling unit tests, HIL testing, integration testing
18
Performance Optimization
Compiler flags, inline assembly, cache (M7/M33), profiling
19
Embedded Software Architecture
Layered design, event-driven, state machines, component-based
20
Tooling & Workflow (Professional Level)
CI/CD for embedded, MISRA, static analysis, Doxygen
You Are Here
CI/CD for Embedded Firmware
A continuous integration pipeline for embedded firmware must solve a problem that server-side CI does not face: the production artifact runs on different hardware than the machine running the pipeline. The solution is a two-compiler strategy: arm-none-eabi-gcc builds the firmware binary (proving it compiles for the real target), while host GCC compiles and runs unit tests (fast, no hardware required).
The pipeline below is a complete, production-quality GitHub Actions workflow. It goes beyond a simple build check: static analysis, coverage gating, documentation generation, and release artifact creation are all automated. Every merge to main produces a signed, versioned, fully-documented firmware binary.
| Stage |
Gate (Fail CI?) |
Parallelism |
Typical Duration |
| Lint / Format |
Yes — clang-format violations |
Parallel with build |
30 s |
| Cross Build |
Yes — compile error |
Matrix: Debug + Release |
2–5 min |
| Unit Tests |
Yes — any test failure |
Parallel with static analysis |
1–3 min |
| Coverage |
Yes — below threshold |
Sequential after unit tests |
30 s |
| Static Analysis |
Yes — ERROR severity |
Parallel with unit tests |
1–2 min |
| HIL Tests |
Yes — on release branch |
Sequential (hardware queue) |
10–30 min |
| Release |
N/A — artefact upload |
Sequential after all gates pass |
1 min |
# .github/workflows/firmware-ci-cd.yml
# Complete professional embedded CI/CD pipeline
name: Embedded Firmware CI/CD
on:
push:
branches: [main, develop, 'release/**']
pull_request:
branches: [main, develop]
release:
types: [created]
env:
ARM_GCC_VERSION: "13.3.rel1"
COVERAGE_THRESHOLD: "80"
jobs:
# ── Job 1: Build firmware for target ──────────────────────────────────
build:
name: Cross-Compile (${{ matrix.build_type }})
runs-on: ubuntu-latest
strategy:
matrix:
build_type: [Debug, Release]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install arm-none-eabi toolchain
run: |
sudo apt-get update -qq
sudo apt-get install -y cmake ninja-build
wget -q "https://developer.arm.com/-/media/Files/downloads/gnu/$ARM_GCC_VERSION/binrel/\
arm-gnu-toolchain-$ARM_GCC_VERSION-x86_64-arm-none-eabi.tar.xz"
tar xf arm-gnu-toolchain-*.tar.xz -C /opt
echo "/opt/arm-gnu-toolchain-$ARM_GCC_VERSION-x86_64-arm-none-eabi/bin" >> $GITHUB_PATH
- name: Configure CMake
run: |
cmake -B build/${{ matrix.build_type }} \
-G Ninja \
-DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake \
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
- name: Build
run: cmake --build build/${{ matrix.build_type }} --parallel
- name: Check firmware size
run: |
arm-none-eabi-size build/${{ matrix.build_type }}/firmware.elf
# Fail if text section > 512 KB
TEXT=$(arm-none-eabi-size build/${{ matrix.build_type }}/firmware.elf | awk 'NR==2{print $1}')
[ "$TEXT" -le 524288 ] || (echo "Firmware too large: $TEXT bytes" && exit 1)
- name: Upload firmware artifact
uses: actions/upload-artifact@v4
with:
name: firmware-${{ matrix.build_type }}-${{ github.sha }}
path: |
build/${{ matrix.build_type }}/firmware.elf
build/${{ matrix.build_type }}/firmware.bin
build/${{ matrix.build_type }}/firmware.map
# ── Job 2: Unit tests + coverage ─────────────────────────────────────
test:
name: Unit Tests + Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install test tools
run: |
sudo apt-get update -qq
sudo apt-get install -y gcc ruby gcovr
gem install ceedling
- name: Run Unity tests
run: ceedling test:all
- name: Generate coverage
run: ceedling gcov:all utils:gcov
- name: Enforce coverage threshold
run: |
BRANCH=$(gcovr --json-summary-pretty --output - | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(int(d['branch_percent']))")
echo "Branch coverage: $BRANCH%"
[ "$BRANCH" -ge "$COVERAGE_THRESHOLD" ] || \
(echo "Coverage $BRANCH% below threshold $COVERAGE_THRESHOLD%" && exit 1)
- name: Upload coverage report
uses: codecov/codecov-action@v4
with:
files: build/test/artifacts/gcov/GcovCoverageResults.xml
# ── Job 3: Static analysis ───────────────────────────────────────────
static-analysis:
name: Static Analysis (cppcheck + MISRA)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cppcheck
run: sudo apt-get install -y cppcheck
- name: Run cppcheck with MISRA addon
run: |
cppcheck \
--enable=all \
--error-exitcode=1 \
--suppress=missingIncludeSystem \
--inline-suppr \
-I src/ \
-I CMSIS/Core/Include/ \
--addon=misra \
src/
continue-on-error: false
# ── Job 4: Documentation ─────────────────────────────────────────────
docs:
name: Generate Doxygen Docs
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
needs: [build, test, static-analysis]
steps:
- uses: actions/checkout@v4
- name: Install Doxygen
run: sudo apt-get install -y doxygen graphviz
- name: Generate API docs
run: doxygen docs/Doxyfile
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: docs/html
Static Analysis with cppcheck
Static analysis tools examine source code without executing it, finding bugs that testing may miss: null pointer dereferences, buffer overflows, use-after-free, uninitialized variables, and coding standard violations. For embedded firmware, they are especially valuable because hardware bugs are expensive to diagnose and reproduce.
cppcheck is the leading open-source C static analyser — free, fast, low false-positive rate, and integrates with GitHub Actions, VS Code, and CLion. The MISRA addon extends it to report MISRA-C:2012 rule violations. Commercial tools (PC-lint Plus, Polyspace, Coverity) have significantly lower false positive rates and better MISRA coverage, but cppcheck is the right starting point for most projects.
# Run cppcheck with MISRA-C:2012 addon
# Install: sudo apt-get install cppcheck
# Basic static analysis (all error types, fail on any error)
cppcheck \
--enable=all \
--error-exitcode=1 \
--suppress=missingIncludeSystem \
--inline-suppr \
-I src/ \
-I CMSIS/Core/Include/ \
-I CMSIS/Device/ST/STM32F4xx/Include/ \
--std=c11 \
src/
# With MISRA-C:2012 addon (requires misra.py in cppcheck addons)
cppcheck \
--addon=misra \
--suppress=misra-c2012-2.3 \ # Suppress: unused type declaration
--suppress=misra-c2012-2.4 \ # Suppress: unused tag declaration
--enable=style,performance,portability \
src/
# Generate XML report for CI integration
cppcheck \
--enable=all \
--xml \
--xml-version=2 \
src/ 2> cppcheck-report.xml
# Convert XML to HTML for human review
cppcheck-htmlreport \
--file=cppcheck-report.xml \
--report-dir=cppcheck-html \
--source-dir=src/
| Tool |
MISRA Support |
False Positive Rate |
CI Integration |
Cost |
| cppcheck |
Partial (addon) |
Low–Medium |
Excellent (open source) |
Free |
| PC-lint Plus |
Full MISRA-C 2004/2012/2023 |
Very Low |
Good (CLI) |
Commercial |
| Polyspace Bug Finder |
Full + CERT-C |
Very Low |
Good (Jenkins plugin) |
Commercial (MathWorks) |
| Coverity |
MISRA + custom checkers |
Very Low |
Excellent (GitHub/GitLab) |
Commercial (Synopsys) |
| CodeSonar |
Full MISRA + safety standards |
Extremely Low |
Good |
Commercial (CodeSecure) |
MISRA-C Compliance
MISRA-C (Motor Industry Software Reliability Association) is the embedded C coding standard used in safety-critical systems — automotive (ISO 26262), aerospace (DO-178C), medical (IEC 62304), and industrial (IEC 61508). MISRA-C:2012 has 143 rules: 10 mandatory, 111 required, 22 advisory.
For firmware not in a formally safety-critical domain, MISRA compliance still provides value as a discipline: it bans the most dangerous C constructs (undefined behaviour, implicit conversions, goto, recursion, dynamic allocation). Think of it as a coding standard that forces you to write C the way experienced embedded engineers already do.
Pragmatic MISRA Approach: Full MISRA compliance on a new project is achievable. Retrofitting MISRA onto legacy firmware produces thousands of findings. For legacy code, prioritise: (1) all mandatory rules, (2) required rules flagged as safety-relevant by your safety analysis, (3) use //cppcheck-suppress or equivalent with justified deviations for the rest. Document every deviation.
Code Coverage Gating in CI
Part 17 covered GCOV and gcovr for generating coverage reports. Here we focus on using coverage as a CI gate — blocking merges if coverage drops below the project threshold. This prevents the common pattern where developers add new code without writing tests, gradually eroding coverage over time.
The CMake CTest integration allows running unit tests as part of the CMake build system — making them accessible to any CI system via a standard interface, not just Ceedling.
# CMakeLists.txt — CTest integration for host-side unit tests
cmake_minimum_required(VERSION 3.20)
project(firmware_tests C)
# Enable CTest
enable_testing()
# Host test executable (GCC, not arm-none-eabi)
add_executable(test_ring_buffer
test/test_ring_buffer.c
src/ring_buffer.c
vendor/unity/unity.c
)
target_include_directories(test_ring_buffer PRIVATE
src/
vendor/unity/
test/support/
)
target_compile_options(test_ring_buffer PRIVATE
--coverage
-O0
-DUNIT_TESTING
)
target_link_options(test_ring_buffer PRIVATE --coverage)
# Register with CTest
add_test(NAME ring_buffer_tests COMMAND test_ring_buffer)
set_tests_properties(ring_buffer_tests PROPERTIES
PASS_REGULAR_EXPRESSION "OK"
FAIL_REGULAR_EXPRESSION "FAIL"
)
# Run via: cmake --build . && ctest --output-on-failure
# Coverage: gcovr --root .. --html --html-details -o coverage.html
Doxygen API Documentation
Doxygen generates HTML, PDF, and LaTeX API documentation from structured comments in C source files. CMSIS itself is documented with Doxygen — the CMSIS-Core API reference you read online is generated from the same core_cm4.h comments. Following the CMSIS documentation style means your team's firmware documentation integrates seamlessly with the CMSIS reference.
/**
* @file ring_buffer.h
* @brief Lock-free ring buffer for embedded firmware.
*
* Provides a statically-allocated, ISR-safe ring buffer for byte data.
* Suitable for UART receive buffers, audio sample FIFOs, and sensor data queues.
*
* @note Push is ISR-safe when only one ISR writes and the application reads.
* For multiple-writer scenarios use a mutex or disable interrupts.
*
* @author Wasil Zafar
* @date 2026-03-31
* @version 1.2.0
*/
/**
* @brief Ring buffer instance structure.
*
* Initialise with ring_buffer_init() before use.
* Do not access members directly — use the API functions.
*/
typedef struct {
uint8_t *storage; /**< Pointer to backing storage array */
uint32_t capacity; /**< Total capacity in bytes */
uint32_t head; /**< Write index (modified by producer) */
uint32_t tail; /**< Read index (modified by consumer) */
} ring_buffer_t;
/**
* @brief Initialise a ring buffer.
*
* Associates the ring buffer instance with a caller-provided backing array.
* The backing array must remain valid for the lifetime of the ring buffer.
*
* @param[out] rb Pointer to uninitialised ring_buffer_t instance.
* @param[in] storage Pointer to caller-allocated backing array.
* @param[in] capacity Size of backing array in bytes.
*
* @retval RING_BUFFER_OK Initialisation successful.
* @retval RING_BUFFER_ERROR Invalid arguments (NULL pointer or zero capacity).
*
* @code
* static uint8_t buf[64];
* ring_buffer_t rb;
* ring_buffer_init(&rb, buf, sizeof(buf));
* @endcode
*/
int32_t ring_buffer_init(ring_buffer_t *rb, uint8_t *storage, uint32_t capacity);
# Doxyfile configuration for embedded C project (key settings)
# Generate with: doxygen -g Doxyfile, then edit
PROJECT_NAME = "My Firmware API"
PROJECT_NUMBER = "v1.2.0"
PROJECT_BRIEF = "CMSIS-based embedded firmware for SmartSensor v2"
OUTPUT_DIRECTORY = docs/
# Source files
INPUT = src/ include/
FILE_PATTERNS = *.c *.h
RECURSIVE = YES
EXTRACT_ALL = YES
EXTRACT_STATIC = YES
# CMSIS-style: extract all groups and brief descriptions
JAVADOC_AUTOBRIEF = YES
QT_AUTOBRIEF = YES
MULTILINE_CPP_IS_BRIEF = YES
# Output formats
GENERATE_HTML = YES
GENERATE_LATEX = NO # Set YES for PDF generation
HTML_OUTPUT = html/
HTML_COLORSTYLE = DARK
HTML_TIMESTAMP = YES
# Call/caller graphs (requires Graphviz)
HAVE_DOT = YES
CALL_GRAPH = YES
CALLER_GRAPH = YES
DOT_IMAGE_FORMAT = svg
# Build: doxygen Doxyfile
# Open: xdg-open docs/html/index.html
Semantic Versioning & Release Automation
Semantic versioning (MAJOR.MINOR.PATCH) gives firmware versions meaning: MAJOR increments on breaking API changes, MINOR on new backwards-compatible features, PATCH on bug fixes. For embedded firmware, version numbers are embedded in the binary image header — any firmware update mechanism can read the version from flash without executing the firmware.
/**
* Firmware version embedded in the image header.
* Placed in a named section so the bootloader and OTA manager can read it
* at a fixed offset without executing the application.
*
* CMakeLists.txt sets VERSION_MAJOR/MINOR/PATCH via:
* project(firmware VERSION 1.4.2)
* target_compile_definitions(firmware.elf PRIVATE
* FW_VERSION_MAJOR=${PROJECT_VERSION_MAJOR}
* FW_VERSION_MINOR=${PROJECT_VERSION_MINOR}
* FW_VERSION_PATCH=${PROJECT_VERSION_PATCH}
* )
*/
#include
/* Firmware image header — placed at fixed offset (e.g., 0x200 from flash start) */
typedef struct __attribute__((packed)) {
uint32_t magic; /* 0xDEADC0DE — identifies valid firmware image */
uint8_t version_major; /* Semantic version MAJOR */
uint8_t version_minor; /* Semantic version MINOR */
uint8_t version_patch; /* Semantic version PATCH */
uint8_t reserved;
uint32_t build_timestamp; /* Unix timestamp at build time */
uint32_t image_size; /* Size of application image in bytes */
uint32_t crc32; /* CRC32 of the image for integrity verification */
char git_hash[8]; /* First 8 chars of git commit hash */
} fw_image_header_t;
/* Static const — placed in read-only flash by linker */
__attribute__((section(".fw_header"), used))
static const fw_image_header_t g_fw_header = {
.magic = 0xDEADC0DEU,
.version_major = FW_VERSION_MAJOR,
.version_minor = FW_VERSION_MINOR,
.version_patch = FW_VERSION_PATCH,
.build_timestamp = 0U, /* Filled by post-build script */
.image_size = 0U, /* Filled by post-build script */
.crc32 = 0U, /* Filled by post-build script */
.git_hash = GIT_HASH_SHORT,
};
Exercises
Exercise 1
Intermediate
Complete GitHub Actions CI Pipeline from Scratch
Create a GitHub repository for an embedded project (use any of the code examples from this series). Build a complete .github/workflows/ci.yml that: (1) installs arm-none-eabi-gcc and builds the firmware, (2) installs Ceedling and runs unit tests, (3) measures branch coverage with gcovr and fails if below 70%, (4) runs cppcheck and fails on ERROR findings. Verify the pipeline triggers on every push and pull request. Add a status badge to the repository README.
GitHub Actions
Ceedling
gcovr
CI Gates
Exercise 2
Intermediate
Fix All cppcheck High-Severity Findings
Take the ring buffer implementation from Part 17. Intentionally introduce 5 classic C bugs: a null pointer dereference, an array out-of-bounds access, an uninitialized variable, an integer overflow on array index, and a missing break in a switch statement. Run cppcheck with --enable=all. Confirm it detects all 5 bugs. Fix each finding. Verify cppcheck exits with 0 after all fixes. Document what each fix was and why cppcheck caught it.
cppcheck
Static Analysis
Bug Detection
Exercise 3
Advanced
Configure Doxygen for Full API Documentation
Add Doxygen /** */ comment blocks to all public functions in the ring buffer, UART logger, and sensor modules from earlier parts. Configure a Doxyfile with: call graphs enabled (requires Graphviz), dark HTML theme, EXTRACT_ALL = YES, and module grouping using @defgroup / @ingroup. Generate HTML documentation. Add a GitHub Actions step that builds Doxygen on every push to main and deploys it to GitHub Pages. Verify the deployed API docs are accessible via the repository's Pages URL.
Doxygen
API Documentation
GitHub Pages
Graphviz
CI/CD Pipeline Planner
Use this tool to plan and document your embedded CI/CD pipeline — platform selection, static analysis tools, coding standards, coverage thresholds, documentation strategy, and deployment targets. Download as Word, Excel, PDF, or PPTX for team agreement or quality management documentation.
Conclusion — The Complete CMSIS Toolkit
In this final part we have completed the professional embedded development workflow:
- A complete GitHub Actions CI/CD pipeline — dual-compiler strategy (arm-none-eabi for firmware, host GCC for unit tests), parallel jobs, coverage gates, static analysis gates, and automated release artefact creation.
- cppcheck with MISRA addon — open-source static analysis that catches null dereferences, buffer overflows, and MISRA-C rule violations before code reaches review.
- MISRA-C:2012 — the embedded coding standard that bans the most dangerous C constructs; applied pragmatically with documented deviations rather than zero-tolerance all-or-nothing.
- CMake CTest integration — standardised unit test execution accessible to any CI system without Ceedling dependency.
- Doxygen with CMSIS-style comment conventions — API documentation that follows the same format as the CMSIS reference itself, deployed automatically to GitHub Pages.
- Semantic versioning in the firmware image header — version numbers embedded in flash, readable by the bootloader and OTA system without executing application code.
Congratulations — CMSIS Mastery Complete!
You have completed all 20 parts of the CMSIS Mastery Series. You now have the skills to build professional-grade embedded firmware from bare-metal register access through RTOS, DSP, security, and CI/CD — entirely with CMSIS standards. Go build something real.
Related Articles in This Series
Part 17: Testing & Validation
Unity/Ceedling unit tests, CMock peripheral mocking, GCOV code coverage, and HIL testing — the testing foundation this CI/CD pipeline automates.
Read Article
Part 19: Embedded Software Architecture
Layered architecture, event-driven FSMs, component-based design, and publish-subscribe patterns — the code structure that makes automated quality checks effective.
Read Article
Part 1: Overview & ARM Cortex-M Ecosystem
Return to the beginning — the CMSIS layers, Cortex-M families, memory architecture, and toolchain setup that started this journey.
Read Article