A beginner-focused, reproducible workflow for testing embedded C, built around the NXP FRDM-MCXA153. The goal is to move the reader from a standard of "it works" (it compiled and the LED blinked) to a standard of "it is verified": not just to show which buttons to press in Unity, Ceedling and CMock, but to change how students reason about software correctness in embedded systems, where the hardware is slow to reach, hard to observe and easy to blame.
The guide is written for undergraduates who know C and pointers, have seen memory-mapped I/O, and have never written a test harness or a mock. Every command is reproducible on Ubuntu Bash, and everything the chapters show (compiler errors, test output, register values, flash logs) comes from an actual run.
Concretely, it teaches how to separate a decision from a hardware action, how to put a seam between your code and the vendor SDK, how to mock that seam so a driver can be unit-tested on a laptop with no board attached, and how to carry the same source file, once written, onto real hardware, verifying it twice without changing it once (dual-targeting).
| Chapter | Title | What it covers |
|---|---|---|
| 1 | Why We Test | What "it works" means, the V-model, module design, and why a sloppy design or a sloppy test both fail silently |
| 2 | Unit Testing with Unity & Ceedling | Installing the toolchain and building a real Unity/Ceedling harness that catches a planted bug |
| 3 | Mocking and Hardware Abstraction | The hardware wall, the seam, CMock, and unit-testing a driver that never touches a register on the host |
| 4 | Dual-Targeting: Build and Flash | The real HAL implementations, connecting the board, and flashing host-tested code to run for real |
| 5 | Code the Hardware Calls | An interrupt handler tested on the host: an ISR is a function, time is a parameter, and where this guide's testing deliberately stops |
docs/ the five chapters above, plus docs/asset/ (screenshots Chapter 2 embeds)
examples/<name>/ one Ceedling project per example, shipped and ready to run
scripts/*.sh five scripts; see below
| Project | Chapter | Host test result |
|---|---|---|
examples/overtemp_alarm |
2 | passes: 13 |
examples/led_driver |
3 §3.2, the wall | fails on purpose; has EXPECTED_TO_FAIL. Before Chapter 4 it stops on PERI_ADC.h: No such file or directory. After Chapter 4, source scripts/target_env.sh and uncomment the SDK include lines in its project.yml to reproduce Obstacle 1's #error "Unknown Arm architecture profile" |
examples/led_driver/experiments |
3 §3.2 Obstacles 2 and 3 | not a Ceedling project; built directly with gcc |
examples/led_driver_seam |
3 §3.4-§3.6, 4 §4.1, 4 §4.4 | passes: 3 |
examples/alarm_update |
3 §3.7 | passes: 2 |
examples/button |
5 | passes: 7 |
scripts/run_tests.sh runs ceedling test:all in every project folder (every folder with a
project.yml) and skips any folder that has an EXPECTED_TO_FAIL marker, led_driver being
the only one. The other four scripts belong to Chapter 4's board workflow:
setup_target_toolchain.sh (one-time target toolchain setup), connect_board.sh (udev rule
and probe check, native Ubuntu and WSL2), create_target_project.sh (exports a freestanding
target project from the SDK), and target_env.sh (sourced, not run, to set the Chapter 4
shell environment).
| Item | Needed for | Notes |
|---|---|---|
| A laptop or PC (x86_64) running Ubuntu 24.04 | every chapter | Native Ubuntu or WSL2 Ubuntu 24.04 on Windows; all commands are Bash. Chapters 1, 2, 3 and 5 need nothing else: the host tests run on the laptop alone |
| NXP FRDM-MCXA153 board | Chapter 4 | MCXA153 MCU (Arm Cortex-M33). The on-board MCU-Link debug probe (port J15) does the flashing and debugging, so no separate probe is needed |
| USB-C cable that carries data | Chapter 4 | Connects J15 to the laptop. A charge-only cable will not enumerate |
On WSL2 only: usbipd-win on the Windows side |
Chapter 4 §4.3 | Hands the USB port over to WSL2; scripts/connect_board.sh prints the exact commands |
Host testing (Chapter 2 §2.2 installs these, in this order):
| Tool | Role | How it is installed |
|---|---|---|
build-essential (gcc, make) |
compiles and runs the tests on the host | sudo apt install -y build-essential |
Ruby 3.x with ruby-dev |
Ceedling runs on it | sudo apt install -y ruby ruby-dev |
| Ceedling (bundles Unity and CMock) | build/test manager; Unity is the test framework, CMock the mock generator | sudo gem install ceedling. The guide's transcripts were made with Ceedling 1.1.8, Unity 2.7.2 and CMock 2.7.2 |
Target build and flash (Chapter 4 §4.2 and scripts/):
| Tool | Role | How it is installed |
|---|---|---|
| MCUXpresso Installer | NXP's installer for everything below | Downloaded from NXP; tick MCUXpresso SDK Developer and LinkServer |
Arm GNU Toolchain (arm-none-eabi-gcc, arm-none-eabi-gdb) |
cross-compiles for the Cortex-M33 and debugs it | via the Installer, under ~/.mcuxpressotools |
west, cmake, ninja, git, Python |
fetches and builds the SDK-based target project | via the Installer (west lives in its Python venv) |
| LinkServer | talks to the MCU-Link probe: flash, gdbserver | via the Installer, under /usr/local/LinkServer_* |
MCUXpresso SDK, v26.06.00-LTS, board frdmmcxa153 |
vendor drivers, startup code and CMake files | scripts/setup_target_toolchain.sh fetches it with west into ~/mcuxsdk |
scripts/target_env.sh (sourced, not run) puts all of the target tools on PATH and sets
ARMGCC_DIR for a Chapter 4 shell.
Install Ceedling as described in Chapter 2 §2.2, then, for any example:
cd examples/<name>
ceedling test:allor run every project at once from the repository root:
scripts/run_tests.shBuilding and flashing the target requires the physical FRDM-MCXA153 board and the SDK workspace set up in Chapter 4; there is no host-only substitute for that part.
Commands in the chapters build projects under ~/embedded-testing/exercises/<name>, because
you are building them as you follow along. The finished version of each one is shipped in
this repository under examples/<name>, so you can compare against it or run a chapter's
tests without typing the chapter first.
Verified against schematic SPF-90829_A1 and the MCXA153 reference manual:
- RGB LED, common-anode, active-low: red P3_12, green P3_13, blue P3_0, all on GPIO3.
- GPIO3 base
0x40105000; PSOR (set)0x44, PCOR (clear)0x48, PDDR (direction)0x54. - SW3 (user button) is P1_7 on GPIO1, active-low, pulled up. Chapter 4 reads it as a level, Chapter 5 as an edge.
Board documentation is NXP's UM12012, linked from the FRDM-MCXA153 product page: https://www.nxp.com/webapp/Download?colCode=UM12012.
It is a guide to one workflow, host-first unit testing with a thin hardware abstraction, taken far enough that a reader can apply it to their own driver, not a survey of embedded testing.
In scope: the test hierarchy from Chapter 1 and why "it compiled and the LED blinked" is not verification; host unit tests with Unity and Ceedling; separating decisions (pure logic, tested directly) from actions (thin hardware calls, mocked only when the call itself needs checking); hardware abstraction and the seam; CMock; dual-targeting, with the same source file built for the host and for the board; interrupt wiring tested on the host, with the ISR reduced to a shell.
Out of scope: RTOS and race conditions, memory-leak analysis, frameworks other than Unity/CMock/Ceedling, DMA, NVIC arbitration, Docker-based reproducibility, TDD theory beyond working fluency, and emulators (QEMU/Renode get a callout in §4.9 and no more).
- Grenning, James W. Test-Driven Development for Embedded C. Pragmatic Bookshelf, 2011.
- Feathers, Michael. Working Effectively with Legacy Code. Prentice Hall, 2004. (Coined the term "seam", which Grenning applied to embedded C.)
MIT. See LICENSE.