DEV Community

standarddeviant
standarddeviant

Posted on • Edited on

Firmware Architecture with StateSmith

StateSmith is a tool that can transform UML into Code+Diagrams. Besides UML, other valid inputs are yED diagrams and draw.io diagrams.

Importantly StateSmith...

  • is well tested
  • has rich features
  • has multi-language support
  • is open source
  • is light-weight
  • is easy to use
  • has simple integration with VSCode for visualization
  • produces auto-generated code that is easily readable

I'm an embedded engineer with almost 20 years of experience. I have encountered many buggy state machines written in C. Sadly, I have written some buggy state machines in C. I started using StateSmith on an important project written in C on a microcontroller.

I call StateSmith a "career changer" because now:

I will never write another complex state machine by hand.

Paraphrasing Adam, the creator of StateSmith, the value in tools like this is that once you create the UML diagram, you get:

  1. source code that does exactly what you specified
  2. a visual diagram of the state machine behavior

The visual diagram is extremely helpful when debugging an issue or changing behavior of the code.

This lets us:

Fix the code by fixing the picture.

StateSmith doesn't remove the need for care and consideration when designing system behavior, but it enables a much more rapid debug cycle by providing source code and a diagram that are effectively guaranteed to match each other.

If all you need is a single state machine, then dive into the StateSmith docs and you should be all set.

However...

Multiple state machines that interact with each other is necessary to handle the complexity of many projects. I wanted a simple and robust way to test overall system behavior with Hardware-In-Loop testing and realized I wanted the ability to "inject" events into any state machine and get a "report" of each issued event and state change for all involved state machines.

The test software only has to concern itself with syntax for two things:

  • injection of events
  • reporting of events and state changes

For simplicity, I use a single message type that contains three fields of information:

  1. event-or-state-change
  2. subsystem ID, where each state machine has its own integer ID
  3. subsystem value, an integer ID to represent individual events or state changes

Fields 1 and 2 are just static, manually created enumerations. Field 3 is nicely handled by StateSmith infrastructure per SomeSm_StateID_... and SomeSm_EventID_... for a single state machine named SomeSm

The C version of this data type is

typedef struct {
  uint8_t mtype;      // event=0 or state=1
  uint8_t subsys_id;  // manually created enum
  uint8_t subsys_val; // event IDs or state IDs generated via StateSmith
} inj_rep_t;
Enter fullscreen mode Exit fullscreen mode

Per the type name, this single data type can be "injected into" or "reported from" any state machine.

Whether a message is "injected" or "reported" is determined by the text syntax over a text-based serial port (like UART) or implied per function calls in the firmware.

Another important idea I stumbled upon is using a "content-based message router" that receives all reported events and state changes, and then potentially injects new events based on reported events and state changes.

This enables modular and flexible logic for the overall system behavior. It also opens the door for flexible Hardware-In-Loop testing!

A diagram of injected and reported events for HIL testing is shown below:

HIL Capable Firmware Architecture with StateSmith

Importantly, I'm using Zephyr RTOS and chose to use zbus, a pub/sub implementation built into Zephyr RTOS. My usage of zbus is very simple; it just takes all reported events and routes a copy to the content-based message router and (optionally) a copy to the UART based reporting feature. This second copy to the "reporter" is extremely useful for HIL testig to verify the system is in a certain state or that certain events have happened.

With this approach:

  • No state machine has to know about any other state machines
    • But HAL-based query functions may enable simpler state machines
  • All "integration logic" is in one place, the content-based router
    • This accelerates development and changing overall system behavior
  • A text message of inj A B C is virtually the same as an actual hardware event
    • This helps enable all kinds of automated HIL testing!
  • A "reporter" script that ingests all the rep X Y Z messages is very easy to write and maintain

After a bit of googling and asking, I think it's accurate to say this approach is

the "Actor Model" leveraging a "Content-Based Message Router"

This approach is probably not appropriate for many situations. However, for a product with multiple different types of hardware that need to interact with each other, it seems to be a very efficient way to manage the necessary complexity of different hardware interactions. Good examples of different hardware it can integrate in a firmware project are:

  • Buttons
  • Lights
  • Displays
  • Speakers
  • Motors
  • Wireless Radio Connections, i.e. Bluetooth
  • Battery charger chips or Power-Management ICs (PMICs)
  • and more!

Tying It Altogether (Update)

In the original post, I left out some key implementation considerations regarding "injection" and "reporting" since those are pieces of project infrastructure that are custom and outside StateSmith.

From the above data-flow diagram, the key pieces technically outside StateSmith are

  • INJ, a C function to inject events into state machines
  • REP, a C function to report events and new states

INJ

The key requirements are INJ are

  1. Create a custom enumeration of state machines as mentioned above
  2. Write a function with a switch statement that looks something like:
#include <stdint.h>

#include "FirstSm.h"
#include "SecondSm.h"
#include "ThirdSm.h"
#include "EtcSm.h"

enum {
  SM_ID_FirstSm,
  SM_ID_SecondSm,
  SM_ID_ThirdSm,
  SM_ID_EtcSm
};

// NOTE: allocate global objects
FirstSm first;
SecondSm second;
ThirdSm third;
EtcSm etc;

// NOTE: INJ blindly assumes that the state machines have been initialized elsewhere
void INJ(uint8_t state_machine_id, uint8_t event_id) {
  switch (state_machine_id) {

  case SM_ID_FirstSm:
    FirstSm_dispatch_event(&first, event_id);
    break;

  case SM_ID_SecondSm:
    FirstSm_dispatch_event(&second, event_id);
    break;

  case SM_ID_ThirdSm:
    FirstSm_dispatch_event(&third, event_id);
    break;

  case SM_ID_EtcSm:
    FirstSm_dispatch_event(&etc, event_id);
    break;

  default:
    LOG_WRN("INJ received unknown state_machine_id = %d", state_machine_id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now any part of the code, especially the router function can call INJ(SM_ID_FirstSm, FirstSm_EventId_BeFirst);

REP

The core REP function is simpler than INJ, but setting up the "router" and "reporter" takes a bit of code to set up a thread and the zbus channel since I'm using Zephyr RTOS.


#include <zephyr/kernel.h>
#include <zephyr/zbus/zbus.h>

void thread_fn_router(void *ptr1, void *ptr2, void *ptr3);
K_THREAD_DEFINE(thread_id_router, 4096, 
  thread_fn_router, NULL, NULL, NULL, 11, 0, 0);



ZBUS_MSG_SUBSCRIBER_DEFINE(router);
ZBUS_LISTENER_DEFINE_WITH_ENABLE(reporter, fn_reporter, false);
ZBUS_CHAN_DEFINE(report_chan,                      /* Name */
                 inj_rep_t,                        /* Message type */
                 NULL,                             /* Validator */
                 NULL,                             /* User data */
                 ZBUS_OBSERVERS(router, reporter), /* observers */
                 ZBUS_MSG_INIT(.mtype = 0, .subsys_id = 0, .subsys_val = 0));

void fn_reporter(const struct zbus_channel *chan) {
  // static uint32_t count;
  const inj_rep_t *ir;
  if (&report_chan == chan) {
    ir = zbus_chan_const_msg(chan); // Use this

    // NOTE:
    // optionally print to reported message, ir, to UART to orchestrate
    // hardware-in-loop testing
  }
}

void thread_fn_router(void *ptr1, void *ptr2, void *ptr3) {
  static uint32_t tic, toc;
  static inj_rep_t rep = {0};
  int rc;

  ARG_UNUSED(ptr1);
  ARG_UNUSED(ptr2);
  ARG_UNUSED(ptr3);

  const struct zbus_channel *which_chan;

  // struct acc_msg acc = {0};

  while (!zbus_sub_wait_msg(&router, &which_chan, &rep, K_FOREVER)) {
    if (&s_report_chan != which_chan) {
      continue;
    }

    // NOTE: router_proc_rep function handles per-project-desires of how to
    // relay/route messages into follow-on events i.e. when the system "wakes
    // up", we send a message to "blink the led with the wake-up pattern"
    router_proc_rep(rep);
  }
}

int REP(inj_rep_t ir) {
  int rc = 0;

  if (ir.subsys_val == 0) {
    // WARN: don't report 'do events'
    return 0;
  }

  rc = zbus_chan_pub(&s_report_chan, &ir, K_NO_WAIT);
  if (0 != rc) {
    // handle publish error
  }
  return rc;
}
Enter fullscreen mode Exit fullscreen mode

I'm omitting an example for router_proc_rep, but the approach is similar to the inject function:

  1. make a switch case that handles each state machine separately
  2. handle events and state-changes in the "message router" to potentially produce new events in state machines

One last piece of the puzzle...

The arrow where all the state machines effectively call REP is not included in "vanilla StateSmith" at this time. Using StateSmith has two general paths:

  1. The Easy Way
  2. The .csx Way, more complex and more capable

I chose the The Easy Way, which has no hooks to call a function on events or state changes. So...

sd to the rescue!

sd is a modern alternative to sed

The main function of sd is to "find and replace" content in files. I use it in a script to carefully find and replace all occurrences where an event_id is dispatched or the state_id variable changes in a StateSmith output source file. This is mildly sketchy, but it does work.

To do this, I wrote a simple PowerShell script to iterate over a defined list of state machine source files in my project, that looks similar to

# This script will perform (2) sd operations per state-machine source file 

# WARN: THIS SCRIPT MODIFIES THE CONTENTS OF ALL AUTO GENERATED STATE MACHINE C SOURCE FILES

# WARN: this is NOT idempotent!!!!!

# INFO: The (2) operations per file effectively inject zbus-based reporting for
# 1. all dispatched events via XYZ_event_dispatch(...)
# 2. all state changes via sm->state_id = XYZ;

$smList = "FirstSm", "SecondSm", "ThirdSm", "EtcSm"
foreach ($smName in $smList)
{
  Write-Host "Adding report hooks to: $smName.c"

  # operation 1-of-2 : (1) event-dispatch
  $find_str = 'void (.*)(_dispatch_event)\((.+), (.+)\)(\s*)\{'
  $rep_str_1 = 'void $1$2($3, $4)$5{\n    REP((inj_rep_t)\n    {.mtype=MTYPE_EVENT,'
  $rep_str_2 = ".subsys_id=SM_ID_$smName, .subsys_val=event_id});"
  $rep_str = "$rep_str_1$rep_str_2"

  sd -f m `
    $find_str `
    $rep_str `
    "sm/$smName.c"

  # operation 2-of-2 : (2) state change
  $find_str = '\n(\s*)sm->state_id =(.*)\;'
  $rep_str_1 = '\n$1 sm->state_id =$2; \n$1 REP((inj_rep_t) \n$1 '
  $rep_str_2 = "{.mtype=MTYPE_STATE, .subsys_id=SM_ID_$smName, "
  $rep_str_3 = '.subsys_val=$2});'
  $rep_str = "$rep_str_1$rep_str_2$rep_str_3"

  sd -f m `
    $find_str `
    $rep_str `
    "sm/$smName.c"
}
Enter fullscreen mode Exit fullscreen mode

My understanding is that I could add these "reporting hooks" in a cleaner way with .csx files.

It's a workable solution for now, but in the future I plan to remove the requirement for a "very careful regex operation" from my build process.

I've been rather pleased with what I've been able to achieve with this approach in about 6 months of development time. Please feel free to leave suggestions, critiques, and alternative approaches in the comments.

Top comments (1)

Collapse
 
adam_fraserkruck_7bd95c6 profile image
Adam Fraser-Kruck

"Fix the code by fixing the picture." well said! I'm gonna steal that :)