State machines: At the C-level

Those of you who have read my blog for a while have seen my posts on the tool Stateflow by The MathWorks, such as the Tips for Readability and the Stateflow Best Practices document on which much of the MathWorks documentation is based. Today I wanted to do something different, dive into State machines at a basic level through a C code example

Starting Simple

We will start with a simple 3 State example, A, B & C, where A is the “Default State”, and there are valid transitions between all 3 states. Further we will treat the State machine function as a “Stateless” function, meaning that all data will be passed into the function.

First, the simplest representation of this in C for clarity of the example

The Statemachine function evaluates the held “current state” of the machine and, following an if / else if structure, flows down to the currently active state.

Once the active state is reached it first checks to see if this is the first time in the state (stateA.isFirst). If so, it performs “entry actions” and sets the entry time (in case there are temporal actions)

Next, it performs any actions associated with the active state, which could be a calculation or calling a sub-function.

Once all these actions are complete, the function checks for exits. If an exit condition exists, it updates the current state and last state variables.

The First Efficiency

The first optimisation is to think about “what is active most of the time. In the current example, we have only three top-level states, so a low number of evaluations are performed. But if at each level we had 10 states, then ordering the states in terms of the percentage time they are active makes sense.

E.g. if in our case

  • A : Active 20%
  • B : Active 70%
  • C : Active 10%

Then the If / Else If tree should be ordered B, A, C.

NOTE: The order of evaluation of the exit conditions should be set based on the precident of the exit, e.g. if two exits could be true at the same time the statemachine designer needs to determine which ‘goes first’

The Data and Children

In this first example we have just one level of states, no child states; this can be easily extended following our pattern.

First the data:

In our current implimentation a simple structure is used: For now we will expand this data structure with a small modification, in the next section we will consider memory optimizations.

Initial

The issue with this approach is fairly clear, while the pattern is easy to follow, you could end up with multiple “twoChild”, “threeChild”,…”nChild” structures following this pattern, further it does not support unique ‘local data’ constructs.

Updated

Child Patterns

The pattern used in the initial code is now extended to the child states

Cyclomatic complexity: Testing the Machine

A quick look a the code above shows how quickly the number of independent branches grow, leading to a high cyclomatic complexity score.

The solution is to limit the depth of the children: As a general rule of thumb, IF logic more then 3 levels deep quickly becomes difficult to maintain. This is done either by re-factoring the state logic into multiple machines, or creating sub-state machine functions that can be indepdendently validated.

Data Efficency versus Clarity

The final topic is on data efficency versus clarity; there are two issues with the current implimentation of the state structure

  1. Inactive data: Within any state structure only one “state and children” will be active at any time. This means that the data associated with all of the other states is not active and could be colapsed
  2. Data structure alignment: The “easy to read” version of the array structure can result in alignment issues, e.g. when you are switch between different sized variables

Solution 1: “Common Data”

There are 4 types of data in use

  1. Current State: This can be changed to a single array of size N, where N is the max depth of the state chart. This requires book keeping to know the “depth” of the child.
  2. Last State: Like current state this can be changed to a single array size N
  3. First time in: This can be changed to a uint32 and then using bit logic on the state number to determine if it is the first time in or not
  4. Local data: The maximum number of floats, doubles, integers used by the state machine can be determined and then a single instance is created.

With the exception of local data there is no impact on the clarity of the code. Because of this thelocal data should be fully commented.

Solution 2: Alignment:

When you follow the approach of #1 you can quickly perform the alignment tasks on the structure.

Safety is an Ecosystem: History Rhymes

The programming language RUST is in the news recently, with articles that are recursive(1)

  • RUST: We have learned lessons from C++. We address the fundamental memory thread safety issues!
    Back 15 years
  • C++: We have learned lessons from C. We have improved memory cleanup and dangling pointer issues!
    Back 30 years
  • C: We have learned lessons from FORTRAN. We have a cleaner, more efficient syntax with better memory management
    Back 45 years
  • FORTRAN: Dang, assembly is hard and not portable, let’s use a human-readable language
Languages: Beyond Embedded

The basic narrative is accurate, but it misses a larger point in software development. It takes an ecosystem to adopt languages.

Ecosystem building blocks

For a language to be adopted, there are 5 basic building blocks that need to be present and qualified(2)

  1. Standardized language syntax: The syntax of the language needs to be formalized and stable(3). All downstream tools are dependent on this first part.
  2. Qualified compilers: Compilers exist that are verified to translate the language into a ‘correct’ hardware implementation. Large-scale adoption requires support for multiple silicon targets
  3. Coding standards: In C / C++ this often means the MISRA guidelines. It represents the distilled ‘best practices for safety.
  4. Supporting tools: A collection of tools such as style guide checkers (for 3), code coverage tools, debuggers, testing infrastructure (e.g. xUnit…), OS integration…(4)

The Apex Predator: Experienced Developers

No matter how good the language or the tool chain is, until a critical mass of experienced developers exists to support development, the language will not take off. This is one of the reasons why new languages often start off with smaller projects that require smaller teams.

Languages as Invasive Species

The software development environment is a crowded field with multiple languages competing for use. Even within the embedded domain, where only a subset of modern languages are used, the competition is fierce. RUST has some good features, but only time will tell if it thrives or becomes a footnote in the history of embedded languages(5)

Footnotes

  1. As a side note: safety systems should avoid recursion
  2. The cost of qualifying tools is significant! Until a language builds up a critical “mass” of real-world use, companies and vendors will not invest in developing the tools.
  3. This is a frozen version of the language that companies can depend on not changing for on the order of ~3 to 5 years. Further, there is an expectation that future iterations of the language will be backward compatible.
  4. A ‘full’ set of supporting tools doesn’t need to be present, but until they are, it is difficult for companies to adopt a new language.
  5. The initial list of languages above is a small subset of what has been part of the embedded landscape. Dropping things such as ADA, OCCAM, COBALT, and many more.

Why not MVP your Process Adoption?

The core concept of the Minimum Viable Product (MVP) is to pilot a quick, low-investment proof-of-concept product to validate the behavior/needs/functionality with the end users. The work is intended to be iterative and potentially thrown away at the end of the evaluation cycle. The MVP is intended for ‘new’ processes or products

The best MVPs start with the mindset of

I know enough to create an initial design, but I don’t know enough to invest resources into the final product. I need to learn

Why We Need Processes: (Human)

Product development requires processes to ensure quality and consistency. Not all products require the same set of processes or rigor within a process. Misalignment of the required processes or rigor can result in low-quality products (missing processes, rigor too low) or wasted engineering effort (unneeded processes, rigor too high).

Both types of misalignment led to discontent among the developers. They can see problems even when they don’t understand the root cause.

Why People Follow Processes

People will follow processes if 4 conditions hold

  1. The process does not significantly slow down their ‘core’ work
  2. They see a near-term impact of following the process
  3. There is consistent support from management for the process
  4. The sum total of process work is seen as low.

The final point, ‘seen as low,’ is critical. If new processes are added too quickly, then the total volume of process work is seen as high. By having a ramp-up to processes, they become part of the background work and can be mastered before new processes are introduced.

One Size does not fit all

The pitfall of people working in a process/systems engineering role is the belief that what worked before will work now. (This is a pitfall in every field.) In many, if not most, cases, this will be true, but care still needs to be taken in ‘assigning’ processes. Which brings us to the thesis of this post.

Processes are “New”: The case for MVP Processes Adoption

Thinking about the objectives of an MVP and the resistance to Processes, it becomes clear that, with modifications, Process Adoption should be treated as an MVP

  • Incremental rollout:
    • New processes are tested out at a small scale to determine if they fit the needs.
    • Users of the process give feedback, validating the process and giving them buy-in
    • Prevents “process dump” burnout
  • Validation loop:
    • Bugs and issues in the process are discovered early, reducing re-work costs
  • Low-cost:
    • Developing, deploying, and documenting a process takes resources; by identifying issues early, sunk costs can be reduced

End Note: Why We Need Processes: (Legal)

There is a second reason we need processes: legal liability. In the case of an issue with the product, showing that the company developed it following industry best practices insulates them from repercussions. This is the truth, but this will never be what motivates developers to adopt processes. When we emphasize this as the reason to adopt, we get reluctant adoption.