This content was presented June 13th, 2020
Introduction
Welcome to firmware 105, the last (for now) firmware tutorial! By now you should have a good understanding on how our microcontrollers interact with the hardware, slave ICs, and each other.
In this lesson we’ll be covering:
FSMs
Event queues
Project Architecture / patterns
Recap
Here’s a quick rundown of the last lesson:
We talk to ICs using the SPI and I2C communication protocols
We have a car-wide CAN bus to communicate between controllers
We use code generation (in a separate repo) to facilitate using CAN
Finite State Machines
A Finite State Machine (FSM) is a very useful concept that helps define system behaviour in many different circumstances. These are not necessarily a programming concept! You can think of an FSM as a never-ending flowchart. You’ll generally learn about these in your digital circuits/logic class (e.g. SYDE 192, MTE 262).
Here’s a quick example of how an FSM can define the behaviour of a coffee machine:
The “states” in this FSM are IDLE
, COIN_INSERTED
, and OPTION_SELECTED
. The arrows are what we call “transitions”. The nice thing about FSMs is that they can be succinctly described in code: if state is this then do thing, else if state is that then do other thing, else if etc. The ‘code’ to describe the above machine would be as follows:
The is_blablabla()
functions are just pseudocode, but hopefully the point is made: an FSM can be described with a bunch of if/else statements. We could also call this a ‘table’:
State | Transitions |
IDLE | Coin inserted → COIN_INSERTED |
COIN_INSERTED | Option selected → OPTION_SELECTED |
OPTION_SELECTED | Coffee dispensed → IDLE |
Although we could just go and write those if/else statements, we implement a library to make our code a bit more descriptive. Instructions for use can be found in libraries/ms-common/inc/fsm.h: https://github.com/uw-midsun/firmware_xiv/blob/master/libraries/ms-common/inc/fsm.h
An important note is that to make FSMs more useful, we allow conditional state transitions (called guarded transitions) as well as functions to be called when a state is transitioned to (called output functions).
Steps for using the library are as follows:
Declare states with
FSM_DECLARE_STATE(state_name)
Define the transition table with
FSM_STATE_TRANSITION(state) { FSM_ADD_TRANSITION(event, state) }
Initialize state output functions with
fsm_state_init(state, output_function)
Initialize the fsm with
fsm_init(&fsm, name, &starting_state, void *context)
.
Please read through the header file for more details on usage / parameters.
An excellent example is MCI, or motor controller interface. We track between three states: OFF, DRIVE, and REVERSE. Here’s the implementation of the FSM:
Step 1:
Step 2:
Step 3:
A quick note about guarded transitions: the guard function must return a boolean and can optionally take in some context as a parameter. It is evaluated upon a transition being triggered, and if it evaluates to false the transition will not happen, otherwise the fsm will transition. Output functions are called (with optional context) after the guard function is called.
Let’s talk about how we actually trigger transitions now.
Event Queues
You may have noticed in the previous code examples values like MCI_DRIVE_FSM_EVENT_OFF_STATE
: These are what we call events. Projects generally contain a global event queue used to communicate between modules. This is interacted with in two ways: modules call
And modules expose a method to process events, e.g.
Process event functions are usually called in the main while loop. You may recall that we call ‘can_process_event’ there as well: this is because our CAN library is also implemented through an event queue, where each incoming CAN message is raised as an event, to be processed one at a time. Events have priority, which can be one of
The event with the highest priority is processed first, otherwise in chronological order.
For more information see libraries/ms-common/inc/event_queue.h: https://github.com/uw-midsun/firmware_xiv/blob/master/libraries/ms-common/inc/event_queue.h but normally all you need to know is event_raise()
and *_process_event
.
The Event
structure has two properties: an id
and data
. The id should be an enum value defined in a header somewhere in your project, while the data is a uint16_t
that you can use however you’d like.
Events in FSMs
FSMs expose a function called fsm_process_event()
:
Generally you wouldn’t call this directly from main, but rather with a module specific process_event()
function. This way you can use multiple FSMs in your project without them conflicting. You declare events per project, and use them to trigger FSM state transitions. E.g.
Note that events shouldn’t conflict with each other, so that process_event functions don’t trigger on incorrect events. Here, you can see the MCI_DRIVE_FSM_EVENT…
events that are used to transition the drive fsm. They’re raised via CAN messages from center console, like so:
The MciDriveFsmEvent s_drive_output_fsm_map[]
is what’s called a lookup table. You check the index of the EE_DRIVE_OUTPUT…
to find the corresponding MCI_DRIVE_FSM_EVENT…
.
Aside: Exported Enums
To talk between projects, sometimes it’s useful to have the same definitions for some things, e.g. center console telling MCI what drive state to be in. For this, we use a header file called Exported Enums (EE).
https://github.com/uw-midsun/firmware_xiv/blob/master/libraries/ms-helper/inc/exported_enums.h
Here, we just define a bunch of ENUMS to use for values for miscellaneous things. However, their indices always start at 0 per enum, so we need to use the lookup table to translate it into the right event for transitioning the FSM. If you’re adding CAN messages or functionality that shares values across projects, you should add to exported enums.
Summary
All in all, using FSMs and events looks like this
Define your events
Define your FSM (states, transitions, and outputs)
Define your
_process_event()
functionInitialize your FSM
Call your
*_process_event()
function
Congratulations, you now have a working FSM!
Project Architecture / Patterns
When it comes to solving a problem with code, there are infinitely many solutions. However, most of the time we fall into a solution that we call a “Pattern”. For example, the solution to repeating a few lines of code a lot would be to add a function that runs those lines of code. Most of this stuff is super complicated, so we’ll just stick to our overarching basic pattern:
Event Driven Architecture
In a project with lots of modules, you can end up with code from one module wanting to access functionality in other modules. This can get messy really quickly, with code diagrams that look like this:
This is not good. If you want to change a function, you have to change it in lots of different places. Instead, by using events, you can make it look like this:
This is much easier to work with, and can be done using event queues. Here, the only function you need to make public is your *_process_event()
function. Each module raises some event, then main()
calls each module's process_event()
function on that event, performing the necessary actions. In practice it can be more complicated, but in general we want to minimize module interdependence as much as possible. This also makes modules much easier to work on in parallel, so many people can work on the same project at the same time.
Conclusion
That’s all, folks! You’re now finished the firmware tutorials, just one last bit of homework and you’ll have learned as much as there is to learn from completing tutorials. As a recap, here’s everything you’ve learned since firmware 101:
What firmware is
What an overview of our firmware system looks like
How we’re able to work remotely
What our process for writing firmware is
How we run code on hardware
How we write code
How we write tests and validate code
How we use GitHub and Jira to collaborate
How we use GPIO, PWM, ADC, and interrupt libraries
How we read schematics and datasheets
How we use drivers
What SPI and I2C are and how we use them
What CAN is and how we use it to talk between controllers
How we generate code to facilitate using CAN
How we use FSMs to describe program behaviour
How we use event queues to talk between modules and FSMs
How we organize projects using events
Pat yourself on the back. This is a lot of ground covered. Some of it will definitely be useful in your courses as well, so this isn’t all for nothing; that’s a promise. There is no next tutorial, but if there are additional topics you think would be useful to have a tutorial about, tell your lead! We could always write a firmware 106 or 201.
Homework
To get some practice using FSMs and event queues, mock up the interaction of the centre console sending drive state commands to Motor Controller Interface (MCI). Some background: our MCI board listens on system CAN for throttle messages, then sends commands on a separate CAN bus to our motor controllers, which drive the actual motors.
Set up a
centre_console_mock
project that periodically (every 5 seconds) sends a command to change state.Set up a
mci_mock
project that implements a state machine that can transition freely between the statesOFF
,DRIVE
, andREVERSE
. Don’t worry about guards.Periodically (every second) print the current state of MCI (this mimics how we need to periodically send a command to the actual motor controller).
Run the projects on two different terminals and screenshot the changing output
Hints:
Use the exported
enum
for drive state to control themci_mock
stateThe FSM should be implemented in a file other than main, and should expose an
init()
andprocess_event()
function through the headerReference projects/mci/src/drive_fsm.c for information on how to implement the FSM