Blog / Rhys Hill / December 10, 2019

Hardware projects bring with them their own unique set of problems. Let’s say, for example, you want to develop a state machine which progresses through a series of states each time you press a button and displays this state as a combination of two LEDs. You order yourself a new development board, a couple of LEDs, a button and a breadboard. The parts are scheduled to arrive sometime next week. Now what? Do you just wait? How can you approach writing this code if you don’t have the hardware to run it on yet? How do you test functions which interact with external hardware? Even once you do have the parts, how do you know if any potential problems are caused by your code or your hardware? In this post, we’ll look at solving these problems using external functions and event queues.

## External Functions

External functions allow us to replace the implementation of a function easily. For those of you who are familiar with interfaces, they work in a similar way to those, with a few caveats. Usually, overriding the functions of an interface is optional, with a default implementation being available as a fallback. For cases like ours when we’re using C, we must implement a new function. This is because a different build step is carried out depending on which implementation you are . using meaning that the default is not available. We also have a different set of dependencies tied to the environments in which the different implementations will run. For our example, we’re going to create a function which sets a gpio pin when running on hardware, but simply prints a log and sets a flag during testing. Obviously, a function which is trying to set a gpio pin won’t work in an environment without a gpio pin. We start by declaring the functions we want to make replaceable as external functions.

    #ifndef STATE_MACHINE
#define STATE_MACHINE

extern void firstState(void);
extern void secondState(void);
extern void thirdState(void);
extern void fourthState(void);

void triggerEvent(void);
void createDeviceState(void);

#endif

state_machine.h

We can then provide an implementation for these functions separate from the state machine itself. By not providing these implementations in state_machine.c, we ensure that they will be utilised by the state machine when our software is running on our hardware, but not during testing.

    #include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <driver/gpio.h>

void firstState(void) {
gpio_set_level(GPIO_NUM_27, 0);
gpio_set_level(GPIO_NUM_26, 0);
ESP_LOGI(TAG, "Off, Off");
}

void secondState(void) {
gpio_set_level(GPIO_NUM_27, 1);
gpio_set_level(GPIO_NUM_26, 0);
ESP_LOGI(TAG, "On, Off");
}

void thirdState(void) {
gpio_set_level(GPIO_NUM_27, 0);
gpio_set_level(GPIO_NUM_26, 1);
ESP_LOGI(TAG, "Off, On");
}

void fourthState(void) {
gpio_set_level(GPIO_NUM_27, 1);
gpio_set_level(GPIO_NUM_26, 1);
ESP_LOGI(TAG, "On, On");
}

states.c

Now, let’s build our basic state machine. For our example, this state machine will count button presses and then reset after 3 presses. Maybe not a particularly exciting use for a state machine, but it illustrates the advantage of externalising your hardware outputs.

    #include <assert.h>
#include "state_machine.h"

typedef struct DeviceState* DeviceStatePtr;
static DeviceStatePtr deviceState;

typedef void (*Action)(void);

static void setupFirstState(DeviceStatePtr deviceState);
static void setupSecondState(DeviceStatePtr deviceState);
static void setupThirdState(DeviceStatePtr deviceState);
static void setupFourthState(DeviceStatePtr deviceState);

struct DeviceState {
Action action;
void (*onButtonPress)(DeviceStatePtr);
};

static void setupFirstState(DeviceStatePtr deviceState) {
deviceState->action = firstState;
deviceState->onButtonPress = setupSecondState;
}

static void setupSecondState(DeviceStatePtr deviceState) {
deviceState->action = secondState;
deviceState->onButtonPress = setupThirdState;
}

static void setupThirdState(DeviceStatePtr deviceState) {
deviceState->action = thirdState;
deviceState->onButtonPress = setupFourthState;
}

static void setupFourthState(DeviceStatePtr deviceState) {
deviceState->action = fourthState;
deviceState->onButtonPress = setupFirstState;
}

state_machine.c

## Event Queues

Alright, we have our state transitions defined and the functions that set our LEDs are isolated so that we can replace them. That just leaves the trigger for these transitions, a button which doesn’t exist yet. If we add an event handler to drive the state machine we can generate events without the button and still test all of our transitions. This means that we need to abstract away the interrupt caused by button presses into an event queue which is processed by a handler. The function triggered by the interrupt should only add an event to the queue, not impact the state machine directly. This will also make testing straightforward since our tests can just add to this queue whenever they need to.

    #include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <esp_event_loop.h>
#include <driver/gpio.h>
#include "state_machine.h"

#define TAG "main.c"
#define ESP_INTR_FLAG_DEFAULT 0

static QueueHandle_t eventQueue;

ESP_LOGI(TAG, "State machine started");
createDeviceState();
int event = 1;

for (;;) {
triggerEvent();
}
}

}

void app_main() {
ESP_LOGI(TAG, "App main has started");
eventQueue = xQueueCreate(10, sizeof(int));
initialiseHardware(&eventQueue);
}

app_main.c

With our handler in place, we’ll need to add the trigger function to our state machine. This function simply calls the transitions we defined earlier based on the current state. We’ll also need a way to initialise the state machine so let’s add a function to create an instance of a device state which is set up as the first state.

    void triggerEvent(void) {
deviceState->onButtonPress();
deviceState->action();
}

void createDeviceState() {
deviceState = malloc(sizeof *deviceState);
assert(deviceState);
if (deviceState) {
setupFirstState(deviceState);
}
}

state_machine.c

Finally, we need to add our event generator, the hardware interrupt which fires off these events each time our button is pressed. As discussed earlier, the only thing that a button press will actually do is generate an event on our event queue—something we can do ourselves during testing. And of course, we’ll need two outputs set so that we can see our counter being incremented.

    #include <driver/gpio.h>

void IRAM_ATTR buttonHandler(void* arg) {
xQueueSendFromISR(eventQueue, NULL, NULL);
}

void initialiseHardware(xQueueHandle* events) {
eventQueue = *events;

gpio_config_t ioConfig;
ioConfig.intr_type = GPIO_PIN_INTR_DISABLE;
ioConfig.pin_bit_mask = 1 << 26 | 1 << 27;
ioConfig.mode = GPIO_MODE_OUTPUT;
ioConfig.pull_down_en = 0;
ioConfig.pull_up_en = 0;
gpio_config(&ioConfig);

ioConfig.intr_type = GPIO_PIN_INTR_POSEDGE;
ioConfig.mode = GPIO_MODE_INPUT;
ioConfig.pull_down_en = 1;
ioConfig.pull_up_en = 0;
gpio_config(&ioConfig);

gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
}