A better way to write firmware code for the IoT

A better way to write firmware code for the IoT

Firmware development presents multiple challenges, due to the conditions that exist. Specifically, there are memory restrictions, processing capacities, the need for low power consumption, among others. The Internet of Things adds more difficulties to the list: battery autonomy, sensing, processing, telecommunications, intelligence at the edge, just to mention some.

On the other hand, software development has evolved considerably and the devices (servers, computers, mobile phones) have increasingly better features. This has made software developers not give importance to the previously exposed needs, worrying more about quite more important design characteristics (for the software development) such as modularity, scalability, portability, etc.

As I mentioned previously, for software development, these design patterns are extremely important, but for firmware development, they can be counterproductive. A balance must exist between the good use of design patterns and the particular challenges of hardware devices.

In recent years the maker community has evolved, becoming a whole movement. This community places importance on rapid development, which allows them to develop proofs of concept in development times that can be as short as a few hours. But this type of software development has a deficiency, which is the quality of the code. Specifically, in the case of firmware, the Arduino community is without a doubt the most dominant, but the code format it promotes is far from actual firmware development.

In this series of articles, I am going to explain how to write better firmware, with examples of specific problems. For that, we will first analyze the most used patterns in firmware development and then specifically delve into finite state machines.

Firmware source structure

A typical firmware source starts with the main function; this function initializes everything (peripherals, ports, variables, state machines, etc) and then goes to an infinite loop cycle where the application logic is done:

*main.c* file:

// Specific include directives:
// Example:
#include “includes.h”

int main(void) {
  // Initialization or Setup
  // Example:
  HAL_MCU_INIT();
  bool aliveLed = HAL_GPIO_INIT(PIN_0, OUTPUT);

  while (1) {
    // Application Code
    // Example:
    aliveLed = ! aliveLed;
    delay(1000);
  }
  return 0; // No error
}

Every firmware code uses this simple structure, but the programmer can use different software architectures and design patterns to make the software more reusable, human-readable, robust, or any kind of optimization that is needed.

Firmware architecture

Some years ago all the firmware was written in a single source file containing every different layer of abstraction. In those days the firmware was not reusable, the application logic and the access to the hardware (for example) were merged making it impossible to port the code to another microcontroller even of the same manufacturer; change the application to another board required rewriting the source. Nowadays the firmware is written using software architectures that allow to modularize different parts of the code and in this way reuse some part of the code. The simplest is the Two-layer Embedded software architecture:

+------------------------------------+                       
|            Application             |                       
+------------------------------------+                       
|              Drivers               |                       
+------------------------------------+                       
|              Hardware              |                       
+------------------------------------+    

Two-Layer Architecture

In this architecture Drivers for specific hardware are written (commonly by the microcontroller manufacturer). In this way, the developer can concentrate on the application logic. But if the application logic includes some specific protocols or another type of specific logic is better to have this in another layer (this is the case of Bluetooth and USB specifications). When this is needed a middleware layer can be included:

+------------------------------------+                       
|            Application             |                       
+------------------------------------+                       
|             Middleware             |                       
+------------------------------------+                       
|              Drivers               |                       
+------------------------------------+                       
|              Hardware              |                       
+------------------------------------+    

Three-Layer Architecture (Middleware Layer)

These architectures work for a microcontroller or a specific board, but if something in the board changes or another board is selected the firmware must be rewritten. This is why there is a need to have a specific board source. In this way, if a new board is selected the logic can be maintained.

+------------------------------------+                       
|            Application             |                       
+------------------------------------+                       
|             Middleware             |                       
+------------------------------------+                       
|    Board Support Package (BSP)     |                       
+------------------------------------+                       
|              Drivers               |                       
+------------------------------------+                       
|              Hardware              |                       
+------------------------------------+

Four-Layer Architecture (BSP Layer)

Each Hardware in the Four-Layer architecture can have its own way to be configured, initialized, utilized, and closed. For this reason, the developer must understand and appropriate the different ways drivers works. That is the reason why a new kind of API (Application programming interface) is been used these days. The idea is to formalize the way hardware is accessed, the Hardware Abstraction Layer is used to accomplish this.

+------------------------------------+                       
|            Application             |                       
+------------------------------------+                       
|             Middleware             |                       
+------------------------------------+                       
|    Board Support Package (BSP)     |                       
+------------------------------------+                       
|  Hardware Abstaction Layer (HAL)   |                       
+------------------------------------+                       
|              Drivers               |                       
+------------------------------------+                       
|              Hardware              |                       
+------------------------------------+

Five-Layer Architecture (HAL Layer)

HAL is a specific API to help the developer to access the hardware in a common formalized way. But the API principle can be used by the developer in the application logic code. That is why an API layer can be used. In this way, specific software design patterns can be used and the API layer can facilitate these patterns.

+------------------------------------+                       
|            Application             |                       
+------------------------------------+                       
|  App Programming Interface (API)   |                       
+------------------------------------+                       
|             Middleware             |                       
+------------------------------------+                       
|    Board Support Package (BSP)     |                       
+------------------------------------+                       
|  Hardware Abstaction Layer (HAL)   |                       
+------------------------------------+                       
|              Drivers               |                       
+------------------------------------+                       
|              Hardware              |                       
+------------------------------------+

Six-Layer Architecture (API Layer)

Nowadays most firmware source has a similar architecture to that of the Six-Layer. The microcontrollers and board manufacturers provide most of the layers (in most cases from Drivers to Middleware) through SDKs (Software development kits). If one wants to build a new board with specific hardware the Board Support Package must be written. Some manufactures can provide specific APIs for specific purposes, and some software development companies provide APIs to implement design patterns.

Firmware Design Patterns

In software development, some problems require specific solutions, if this solution can be generalized a pattern exists. In firmware, there are a lot of patterns and in this section, we are going to discuss the most important.
In the previous section, we discussed different layers of firmware nowadays has. Some of these layers are developed using the design patterns that we are going to talk about, for this article we are going to consider some examples, some patterns can be used simultaneously. We are going to consider a very simple example where we have a blinking led that changes its frequency according to two push buttons (+ and -). Also, we consider that we are using a HAL for the CPU and GPIOs and some delay API.

                        +---------------------------+                        
                        |                           |                        
                        |                           |                        
 + Button ---------------                           ----------  LED
                        |                           |                        
                        |          System           |                        
                        |                           |                        
                        |                           |                        
 - Button ---------------                           |                        
                        |                           |                        
                        +---------------------------+                        

Cyclic Executive pattern

This is the simplest pattern in firmware development; It consists of just call all the actions that must be done in a sequence way inside the infinite loop.

main.c file:

// Specific include directives:
#include “includes.h”

int main(void) {
  // Initialization or Setup
  uint16_t delayValue = 1000;
  bool aliveLed = HAL_GPIO_INIT(PIN_0, OUTPUT);
  
  HAL_MCU_INIT();
  HAL_GPIO_INIT(PIN_1, INPUT); // - button
  HAL_GPIO_INIT(PIN_2, INPUT); // + button

  while (1) {
    // Application Code
    aliveLed = ! aliveLed;
    if (HAL_GPIO_READ(PIN_1) == true) {
      delayValue = delayValue + 100;
    }
    if (HAL_GPIO_READ(PIN_2) == true) {
      delayValue = delayValue - 100;
    }
    delay(delayValue);
  }
  return 0; // No error
}

In this simple example, we have 4 actions inside the loop (Change LED state, read – button, read + button, and delay), each happens after the other. For simplicity, we can implement 3 functions before the main (Note: these 3 functions can (and must) be declared in a .h file and implemented in another .c file, but for simplicity, they are implemented in the same main.c file):

main.c file:

// Specific include directives:
#include “includes.h”

uint16_t delayValue = 1000;
bool aliveLed = HAL_GPIO_INIT(PIN_0, OUTPUT);

void blinkLed() {
  aliveLed = ! aliveLed;
}

void poolMinusButton() {
  if (HAL_GPIO_READ(PIN_1) == true) {
    delayValue = delayValue + 100;
  }
}

void poolPlusButton() {
  if (HAL_GPIO_READ(PIN_1) == true) {
    delayValue = delayValue + 100;
  }
}

int main(void) {
  // Initialization or Setup  
  HAL_MCU_INIT();
  HAL_GPIO_INIT(PIN_1, INPUT); // - button
  HAL_GPIO_INIT(PIN_2, INPUT); // + button

  while (1) {
    // Application Code
    blinkLed();
    poolMinusButton();
    poolPlusButton();
    delay(delayValue);
  }
  return 0; // No error
}

In this example, 4 things are happening inside the infinite loop, but each is affecting the other. For example, if the “minus” button is pressed multiple times, or the user just presses it for a long time the delayValue is going to increase and the delay is going to take more time, so if the user then presses the “plus” button he must wait for more time to this action to take place. This is one of the problems with this pattern, each polling action can affect the behavior of the others. That is why a better approach must be done in many cases.

Interrupt handled pattern

A better way to work with multiple tasks that must be handled is to work with interruptions. These interrupts can happen asynchronously and the system is going to respond to them doing wherever it must do.

main.c file:

// Specific include directives:
#include “includes.h”

uint16_t delayValue = 1000;
bool aliveLed = HAL_GPIO_INIT(PIN_0, OUTPUT);

void blinkLed() {
  aliveLed = ! aliveLed;
  START_TIMER_INTERVAL(delayValue, blinkLed); // Run the interval again with the delayValue
                                              // (maybe updated by the others ISRs)
}

void isrMinusButtonFunction() {
  delayValue = delayValue + 100;
}

void isrPlusButtonFunction() {
  delayValue = delayValue + 100;
}

int main(void) {
  // Initialization or Setup
  
  HAL_MCU_INIT();
  HAL_GPIO_INIT(PIN_1, INPUT_CHANGE); // - button
  HAL_GPIO_INIT(PIN_2, INPUT_CHANGE); // + button
  TIMERS_INIT();

  HAL_SET_ISR(PIN_1, isrMinusButtonFunction); //Set the callback function for PIN_1 change state
  HAL_SET_ISR(PIN_2, isrPlusButtonFunction);  //Set the callback function for PIN_2 change state
  START_TIMER_INTERVAL(delayValue, blinkLed); //Set the callback function for the blinking function
  
  while (1) {
    //Do nothing
  }
  return 0; // No error
}

In the example the infinite loop does nothing and everything is handled by interrupts. In this way the different tasks are independent. But the problem with this approach is that every task is fully independent. That can be a problem if one depends on the other. Another problem is that ISRs (Interrupt service routines) can not consume too much time so its source must be very simple (modify some variables, set some flag, etc). That is why is better to combine ISRs with events.

Event handled pattern

When an application requires to handle different events coming from different sources (ex. external hardware) sometimes those events must perform time-consuming tasks and their interrupt service routine is going to interrupt the microprocessor execution. As I already mentioned the ISRs must be small; so a better way to develop an application is to use events. Now the ISR is going to launch a specific event and those events are going to be handled in a cyclic executive way.

main.c file:

// Specific include directives:
#include “includes.h”

//Events declaration
#define BLINK_EVENT        1
#define MINUS_BUTTON_EVENT 2
#define PLUS_BUTTON_EVENT  3

//Varibles initialization
uint16_t delayValue = 1000;
bool aliveLed = HAL_GPIO_INIT(PIN_0, OUTPUT);
uint8_t currentEvent = 0;

void blinkLed() {
  currentEvent = BLINK_EVENT;
}

void isrMinusButtonFunction() {
  currentEvent = MINUS_BUTTON_EVENT;
}

void isrPlusButtonFunction() {
  currentEvent = PLUS_BUTTON_EVENT;
}

int main(void) {
  // Initialization or Setup
  
  HAL_MCU_INIT();
  HAL_GPIO_INIT(PIN_1, INPUT_CHANGE); // - button
  HAL_GPIO_INIT(PIN_2, INPUT_CHANGE); // + button
  TIMERS_INIT();

  HAL_SET_ISR(PIN_1, isrMinusButtonFunction); //Set the callback function for PIN_1 change state
  HAL_SET_ISR(PIN_2, isrPlusButtonFunction);  //Set the callback function for PIN_2 change state
  START_TIMER_INTERVAL(delayValue, blinkLed); //Set the callback function for the blinking function
  
  while (1) {
    switch (currentEvent) {
      case BLINK_EVENT:
        aliveLed = ! aliveLed;
        START_TIMER_INTERVAL(delayValue, blinkLed); // Run the interval again with the delayValue
                                                    // (maybe updated by the others ISRs)
        break;
      case MINUS_BUTTON_EVENT:
        delayValue = delayValue + 100;
        break;
      case PLUS_BUTTON_EVENT:
        delayValue = delayValue + 100;
        break;
    }
    currentEvent = 0;
  }
  return 0; // No error
}

There are some clarifications:

  • The events declaration must be done in a different .h file, but for simplicity, I defined it in the same code.
  • In a better implementation, each event handle case must call a specific handler function.
  • In the previous source if two events happen in the same cycle the currentEvent variable is going to be overwritten and only one event is going to be handled, that is why a better implementation must use “flagged” events (using binary defined values).

A better implementation of the previous source can be:

events.h file:

//Events declaration
#define NO_EVENTS          0b00000000
#define BLINK_EVENT        0b00000001
#define MINUS_BUTTON_EVENT 0b00000010
#define PLUS_BUTTON_EVENT  0b00000100

main.c file:

// Specific include directives:
#include “includes.h”
#include "events.h"

//Varibles initialization
uint16_t delayValue = 1000;
bool aliveLed = HAL_GPIO_INIT(PIN_0, OUTPUT);
uint8_t currentEvent = NO_EVENTS;

//ISR Functions
void blinkLed() {
  currentEvent = currentEvent | BLINK_EVENT;
}

void isrMinusButtonFunction() {
  currentEvent = currentEvent | MINUS_BUTTON_EVENT;
}

void isrPlusButtonFunction() {
  currentEvent = currentEvent | PLUS_BUTTON_EVENT;
}

//Callback functions
void callbackBlinkLedEvent() {
  aliveLed = ! aliveLed;
  START_TIMER_INTERVAL(delayValue, blinkLed); // Run the interval again with the delayValue
}

void callbackMinusButtonEvent() {
  delayValue = delayValue + 100;
}

void callbackPlusButtonEvent() {
  delayValue = delayValue - 100;
}

int main(void) {
  // Initialization or Setup
  
  HAL_MCU_INIT();
  HAL_GPIO_INIT(PIN_1, INPUT_CHANGE); // - button
  HAL_GPIO_INIT(PIN_2, INPUT_CHANGE); // + button
  TIMERS_INIT();

  HAL_SET_ISR(PIN_1, isrMinusButtonFunction); //Set the callback function for PIN_1 change state
  HAL_SET_ISR(PIN_2, isrPlusButtonFunction);  //Set the callback function for PIN_2 change state
  START_TIMER_INTERVAL(delayValue, blinkLed); //Set the callback function for the blinking function
  
  while (1) {
    if ((currentEvent & BLINK_EVENT) != 0) {
      callbackBlinkLedEvent();
    }
    if ((currentEvent & MINUS_BUTTON_EVENT) != 0) {
      callbackMinusButtonEvent();
    }
    if ((currentEvent & PLUS_BUTTON_EVENT) != 0) {
      callbackPlusButtonEvent();
    }
    currentEvent = NO_EVENTS;
  }
  return 0; // No error
}

Note: In this example, the ISR Functions and the Callback functions are in the main.c file but in a better source must have .h and .c files for declaration and implementation of each of these types of functions.

Finite State Machine Pattern

The previous pattern works fine for small systems that respond to events that are not related. But what happens when some events are not allowed to happen if the system is in some state (for example the device has not been initialized yet)? That’s the topic of the next article. Feel free to leave us your comments

4 Responses

  1. HectorCoags says:

    Earlier I thought differently, thanks for an explanation.

  2. Excellent post. I definitely appreciate this website. Thanks!

  3. Hello! I simply wish to give you a big thumbs up for the excellent info you have right here on this post. I will be coming back to your web site for more soon.

  4. Very nice blog post. I absolutely love this website. Keep writing!

Leave a Reply

Your email address will not be published. Required fields are marked *