Firmware 105 Notes

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:

  1. FSMs

  2. Event queues

  3. 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 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: firmware_xiv/libraries/ms-common/inc/fsm.h at master · uw-midsun/firmware_xiv

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:

  1. Declare states with FSM_DECLARE_STATE(state_name)

  2. Define the transition table with FSM_STATE_TRANSITION(state) { FSM_ADD_TRANSITION(event, state) }

  3. Initialize state output functions with fsm_state_init(state, output_function)

  4. 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: firmware_xiv/libraries/ms-common/inc/event_queue.h at master · uw-midsun/firmware_xiv 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). 

firmware_xiv/libraries/ms-helper/inc/exported_enums.h at master · uw-midsun/firmware_xiv

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

  1. Define your events

  2. Define your FSM (states, transitions, and outputs)

  3. Define your _process_event() function

  4. Initialize your FSM

  5. 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:

  1. What firmware is

  2. What an overview of our firmware system looks like

  3. How we’re able to work remotely

  4. What our process for writing firmware is

  5. How we run code on hardware

  6. How we write code

  7. How we write tests and validate code

  8. How we use GitHub and Jira to collaborate

  9. How we use GPIO, PWM, ADC, and interrupt libraries

  10. How we read schematics and datasheets

  11. How we use drivers

  12. What SPI and I2C are and how we use them

  13. What CAN is and how we use it to talk between controllers

  14. How we generate code to facilitate using CAN

  15. How we use FSMs to describe program behaviour

  16. How we use event queues to talk between modules and FSMs

  17. 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.

  1. Set up a centre_console_mock project that periodically (every 5 seconds) sends a command to change state.

  2. Set up a mci_mock project that implements a state machine that can transition freely between the states OFF, DRIVE, and REVERSE. Don’t worry about guards.

  3. Periodically (every second) print the current state of MCI (this mimics how we need to periodically send a command to the actual motor controller).

  4. Run the projects on two different terminals and screenshot the changing output

Hints:

  • Use the exported enum for drive state to control the mci_mock state

  • The FSM should be implemented in a file other than main, and should expose an init() and process_event() function through the header

  • Reference projects/mci/src/drive_fsm.c for information on how to implement the FSM