Building State Machines with Test‑Driven Development in Embedded C
State machines are foundational to many embedded systems, yet their development can be complex. This article outlines proven strategies for building state‑machine (SM) software using Test‑Driven Development (TDD), with a focus on C implementations suitable for resource‑constrained environments.
At its core, an SM model consists of states, transitions, and actions. States represent conditions, transitions define the path from one state to another—usually triggered by an event—and actions capture the behavior executed during entry, exit, or while transitioning.
In UML state machines, actions can be attached to state entry/exit, the transition itself, or internal reactions. All state‑machine formalisms assume a Run‑To‑Completion (RTC) execution model: each event is fully processed—including exit actions, transition actions, and entry actions—before the next event is handled.
Before diving into TDD strategies, it is essential to understand why TDD matters. TDD is an incremental development approach that mandates writing a failing unit test before any production code. Tests are small, automated, and serve as the specification for the desired behavior. The TDD cycle—derived from James Grenning’s “Test‑Driven Development for Embedded C”—includes:
- Write a small, failing test.
- Run all tests; the new test should fail.
- Write the minimal code to pass the test.
- Run all tests again to confirm the new test passes.
- Refactor to eliminate duplication and improve clarity.
Consider the SM diagram in Figure 1. It begins in StateA and, upon receiving the Alpha event, transitions to StateB, executing xStateA(), effect(), and nStateB() in sequence. How do we validate that this behavior is correct?
Figure 1. Basic state machine (Source: VortexMakes)
The most straightforward testing approach involves verifying the SM’s transition table. This creates one test case per state, stimulating the SM with relevant events and asserting the resulting state and executed actions. Complex actions warrant dedicated tests. (See the article “Testing state machines using unit test” for an in‑depth discussion.)
Each test follows the four‑phase xUnit pattern:
- Setup – establish preconditions (e.g., current state, event, expected target state, and action sequence).
- Exercise – trigger the SM with the event.
- Verify – assert the outcomes match expectations.
- Cleanup – reset the SM to its initial state (optional).
While this strategy works well for simple SMs, more complex behavior—such as a chain of transitions where an effect is only observable after several steps—requires functional scenario testing. Instead of isolated state checks, we test a complete sequence that exercises the necessary states, events, and actions.
Figure 2 illustrates this with a DoWhile SM that mimics a traditional do‑while loop. The machine initializes with default values via x = 0 and output = 0, sets the loop counter with x++ or x = (x > 0) ? x-: x, and iterates with i++ until the guard i == x fails, after which it outputs output = i.
Figure 2. The DoWhile state machine (Source: VortexMakes)
Prior to coding, we create a test list derived from the specification. For DoWhile, the initial list includes:
- All data set to defaults upon initialization.
- Incrementing the X attribute.
- Decrementing the X attribute.
- Executing a single‑iteration loop.
- Executing multiple iterations.
- Handling a non‑iterating scenario.
- Checking out‑of‑bounds conditions.
We employ Ceedling as the build system and Unity as the lightweight test harness for the C code. The SM is defined in DoWhile.h and DoWhile.c, and the test suite resides in test_DoWhile.c.
Below is a concise excerpt illustrating the “single‑iteration” test case.
Code Listing 1: Single iteration test (Source: VortexMakes)
The test initializes the SM, sets the iteration count to zero, dispatches the Up event to increment it, triggers the Start event, and finally sends Alpha to execute one loop iteration. Assertions verify that the out attribute equals the number of iterations and that the SM remains in StateC.
Extending the same test demonstrates multiple iterations (see Code Listing 2). A separate test (Listing 3) confirms that the SM performs no iterations when Alpha is received without a prior Up event.
Code Listing 2: Multiple iteration test (Source: VortexMakes)
Code Listing 3: Non‑iteration test (Source: VortexMakes)
The implementation details are not the focus of this article, but excerpts from DoWhile.c and DoWhile.h illustrate a clean switch‑case approach to state handling.
Code Listing 4: Fragment of DoWhile implementation (Source: VortexMakes)
Code Listing 5: Fragment of DoWhile specification (Source: VortexMakes)
In summary, TDD provides two complementary strategies for state‑machine development: (1) state‑by‑state transition testing, and (2) functional scenario testing that covers multiple states and actions. These methods are language‑agnostic, tool‑agnostic, and particularly well suited to embedded systems where state‑based logic dominates.
Because C remains the lingua franca of embedded software, pairing it with Ceedling and Unity yields a robust TDD workflow. Adopting these practices leads to software that is more flexible, maintainable, and reusable than conventional approaches.
All code and models are available in the GitHub repository https://github.com/leanfrancucci/sm-tdd.git.
Embedded
- Designing Finite State Machines: From Concept to Implementation
- Eclipse IoT: A Unified, Open-Source Package for Rapid IoT Development
- FPGA Hardware Resources for High‑Performance Embedded Systems
- Embedded FPGA Design: A Complete Development Process
- Common CNC Machine Issues and Expert Solutions
- Lightburn Laser Software Now Bundled With Mantech Laser Machines
- C++ Polymorphism Explained: Practical Examples & Key Concepts
- Advancing RAPM with PtFS: Accelerating Composite Manufacturing
- AI-Driven Test Automation: Revolutionizing QA for Modern ERP Systems
- Accurate Battery Testing: How to Use a Test Meter Effectively