cool dog

Embedding Sleek C++

Daniel Perez

One of the things you quickly come to appreciate of using a language like Python is how easy it is to make code that is elegant, descriptive and concise. The «pythonic» way of designing encourages this idea of abstracting and simplifying code, making it visually easy to quickly grasp the actual functionality being implemented, This obviously comes at a cost, as the code is generally neither as efficient nor as compact as a C/C++ version, without mentioning the other characteristics of being an interpreted language, lack of strong variable typing and depending on the garbage collector for memory control.

Inspired by posts like Migrating to Modern C++ or the KeyNote Meeting Embedded 2018 I’ll try to explore ways of making C++ code look cooler without impacting significantly in performance, reliability of time to perform each operation or memory consumption, all key requisites of programming for Embedded Systems.

Data Containers

Traversing a pythondictionary is as simple as:

sensorNodes = { 
               'pressure' : 'activated', 
               'temp' : '26', 
               'rpm' : '1500', 
               'volt' : '3.7'
               } 
                        
# Iterating over keys 
for node, state in sensorNodes: 
    print(node ":" state) 

Traditionally MCU’s are programmed in C, or basic implementations of C++, using the encapsulation obtained from classes and objects, but in general the code is much less appealing, requiring boilerplate to generate enumerated classes, initializing arrays with char* strings, generating an index to traverse the array and comparing with strncmp

The C++ Standard Template Library includes a series of classes, templates and functions that make a lot of the basic operations on data collections a lot easier. For example, the std::maphas a lot in common with Python dicts. The main problem with the STL is the verbose syntax:

std::map<std::string, std::string> sensorNodes;

sensorNodes["pressure"] = "activated";
sensorNodes["temp"] = "26";
sensorNodes["rpm"] = "1500";
sensorNodes["volt"] = "3.7";

// Iterating over keys
for (std::map<std::string, std::string>::iterator it = sensorNodes.begin(); it != sensorNodes.end(); it++){

    std::cout<<it->first<<":"<<it->second<<std::endl;
}

Not half as nice.

But with some of the improvements introduced from C++11 onwards, we can make this a lot easier on the eye:

std::map<std::string, std::string> sensorNodes = {
   {'pressure' : 'activated'}, 
   {'temp' : '26'}, 
   {'rpm' : '1500'}, 
   {'volt' : '3.7'}
};

// Iterating over keys 
for (auto pair : sensorNodes){
   std::cout<<it->first<<":"<<it->second<<std::endl;
}

In this snippet, the auto keyword tells the compiler to automatically infer the type of the variable from the code context (in this particular case, std::pair<std::string, std::string>. We also use a range-based loop to traverse the map.

One of the problems some people have with the STL is its use of dynamic memory, allocating bytes on the heap at runtime. This can cause issues such as having to handle errors due to lack of available space (microcontrollers generally have quite limited memory capacity) and the non deterministic nature of the process. To avoid them developers use traditional c arrays statically initialized with a fixed capacity at compile time. This approach has the side-effect of reserving the maximum amount of possibly necessary memory at all times.

With the STL this pitfall can be partially avoided, as some containers have a reserve(size_t size)method that can be run on program initialization, allocating the memory necessary for all structures before heap fragmentation can start to affect the system. If the initial size was correctly estimated, access to the positions/elements can be performed without additional memory allocation requirements.

Another useful container that simplifies access to array data is… std::array<class T, size n>. This is a very low level wrapper for a standard C array that allows things like passing an std::array as a value around without having to worry about pointers, references etc. For many situations, std::vector<class T> or std::deque<class T> are your friends, due to the abstraction and helper methods they provide.

Dealing with strings in C is another tedious task, although I could also argue that having to verbosely perform all the actual operations involved in simply creating a copy of a string or concatenating a couple makes one more aware of the underlying operations involved in the use of the C++ STL version, std::string. In this way we can be more aware of all the operations involved and, for example, evaluate replacing complex if...else structures using string comparisons, with an enumerated type that semantically defines the decision tree.

In any case, std::string does provide many useful abstractions, of which I’ll go for the ease of passing string values around without worrying about the underlying pointers and the existence of meaningful comparison operators:

std::string foo("foo");
std::string bar("bar");
std::string foo2("foo");

foo == foo2;     // true
foo == bar;      // false
bar < foo;       // true

This of course as opposed with a standard Cchar[], where the same operations would be comparing memory pointers and not obtain the expected results.

So, the STL using auto for exceedingly verbose types and some of the features available since C++11 like range-based loops can make you feel better when you have to look back at your code and try to decipher what it’s doing. That being said, I try to use them only when necessary and when it helps in the comprehension of the code. In other words, if your array has a very well specified scope and you don’t have to pass it to a function or sort it or any other more involved operations, just go with a standard c array.

Wrapper Objects & Callbacks

Many embedded applications are event driven: input from a peripheral, change of state of a GPIO or internal state change requires attention by means of the execution of a routine. In traditional C++ you could perform this operation with a pointer to a function:

// a typedef makes it a little easier to understand

typedef void (*event_handler_t)(int, std::string);


// define the callback function, with the signature typedef'ed above

void handle_event(int code, std::string value){
   std::cout<<"Event ["<<code<<"] = "<<value<<std::endl;
}

struct Event {
   int code;
   std::string value;
}

int main(){

    // saved variable as pointer to function
    event_handler_t handler = &handle_event;

    while(1){
       Event event;
       wait_on_event(&event);     // pseudo-function, waiting on queue
       handler(event.code, event.value);
    }
}

This has a couple of limitations:

  • The syntax is quite verbose: void (*event_handler_t)(int, std::string). The typedef mitigates this somewhat, but writing this after a while of not dealing with function pointers is error prone.
  • Maintaining a list of handlers is cumbersome, making the «handler» function a switch..case list or maintaining an array of event.code to function(event.value) assignments.

A possible workaround can be made with a template wrapper class, that uses a std::map to maintain internally the assignment between event codes and functions:

// *************** Event defined as before

typedef void (*event_handler_t)(Event*);


const static int EVENT_NONE = 0;
const static int EVENT_GPIO_INTERRUPT = 1;
// other event type definitions...

/**
 * @brief list of callbacks depending on the type of event
 * there can only be one callback function for every type of event
 *
 */
struct Event_Callbacks {
   private:
    int _code;

   public:
    Event_Callbacks() : _type(EVENT_NONE){};

    std::map<int, event_handler_t> _callbacks;

    // This function runs the callback assigned for the event code
    bool operator[](Event* event) {
        _code = event->code;
        auto found = _callbacks.find(_code);
        if (found != _callbacks.end()) {
            // if there was a callback associated with this type of event
            // second() returns the value of the iterator, a function
            ESP_LOGI(TAG, "Found callback TYPE %x", _code);

            found->second(event);
            _code = EVENT_NONE;
            return true;
        }
        ESP_LOGE(TAG, "No callback found for event type %x", _code);
        return false;
    }

    // This function is used for insertion of a callback into internal state
    Event_Callbacks& operator()(int code) {
        _code = code;
        return *this;
    }

    // This function performs the actual insertion of the callback function
    bool operator<<(event_handler_t action) {
        if (_code == EVENT_NONE)
            return false;
        _callbacks[_code] = action;
        _code = EVENT_NONE;
        return true;
    }
};

With this new data structure, the setup and usage of a callback list would be as follows:

void gpio_handler(Event* event){
   // code that runs the logic when a GPIO pin changes state, for example
}

void setup() {
    // During initialization, insert the function callback, chaining two 
    // functions from the Event_Callbacks class
    Event_Callbacks callbacks;
    callbacks(EVENT_GPIO_INTERRUPT)<< gpio_handler;
}

//...
// Same while loop from the main function:
void main() {
   while(1){
       Event event;
       wait_on_event(&event);     // pseudo-function, waiting on queue
       // runs the actual callback
       bool callback_exists = callbacks[event.code];
       // if the event.code == EVENT_GPIO_INTERRUPT, will run the 
       // 'gpio_handler' function
    }
}

At the repo https://gitlab.com/scrobotics/scr-hal-rtos/ similar helper classes have been generated for other FreeRTOS functions (tasks, callbacks and mutex locks). They have been used in quite a few of our projects as an ESP32 component, but can easily be used as a library in any platform.io project or adapted for use in any other firmware project based on FreeRTOS.


References:

Main image credits: Mel Elías on Unsplash

0 Comments

Deja una respuesta

More great articles

AI-based Pilgrim counter

Before going into the details of our latest project, let me put you into context. SC Robotics is based in…

Read Story

Booting Linux on an Olimex board. Part 1: The Bootloaders

In this series of posts I'm going to explore in detail how to boot Linux in an Olimex A20-OLinuXino-LIME2 board.…

Read Story

GPS Tracker with balenaOS

At SC Robotics we've recently started using the Balena suite in some of our projects. Balena provides many tools to…

Read Story
Arrow-up