MODERN C++ IN EMBEDDED DEVELOPMENT

C – the only right tool for the job

It is a common opinion amongst embedded developers that the only right tool for the job is C.

Being an embedded developer you spend a lot of time reading device datasheets. Datasheets describe peripherals in terms of registers that are memory-mapped. You configure peripherals by tweaking bits in registers which boils down to writing a value in a certain memory address. 

C is designed to provide low-level access to memory which makes it a perfect tool for clock configuration, peripheral setup, driver implementation, and all tasks related to memory-mapped peripherals and devices. This 40-year-old language is close to embedded developers as it provides them with full control of memory. 

C with classes

In its early days, C++ was considered by many as C with classes. The standard library and containers that utilize dynamic memory allocation didn’t help its adoption among the embedded developers. Working on extremely memory-restricted devices made dynamic memory allocation a no-go. Rigorous safety standards and guidelines are also prohibiting it. 

Abstractions allow us to think about problems in different ways. Instead of thinking about arrays, we can think about strings that are easier to comprehend. C++ provides you with a string class which is a part of the standard library. It allows you to easily concatenate strings, search for substrings inside a string, compare two strings, etc. The downside is that this class internally is using dynamic memory allocation. Due to the reasons mentioned earlier, the string class is usually not accepted by embedded developers.

Back in the early days, compiler support for different architectures was also a major concern for embedded developers. Nowadays that’s different, as most modern compilers provide support for recent C++ standard versions.

C++ 11

The position of C++ in the embedded world started to change for the better with the arrival of the C++11 standard. C++11 introduced the keyword auto which made the language feel more modern. It allows you to easily declare a variable, without declaring its type, in cases where you are assigning a value to the variable that is provided by some function. 

A standard library container that guarantees static allocation std::array was also introduced in C++11. It provides users with an “at” method that takes care of array boundaries, a common problem that needs to be taken care of by developers in C manually. And that’s not the only reason why you would use it over a C array. Range-based for loop was also introduced in C++11. Looping over all members of the array now becomes way easier, and easier to read.

#define N 20
int buffer[N];

for(int i = 0; i < N; i ++) {
	printf(“%d “, buffer[i]);
}

The above snippet of C code can be translated into the following C++11 code: 

std::array<int, 20> buffer;

for(auto& element : buffer) {
	printf(“%d “, element);
}

Lambdas

Another major feature C++11 brought is lambdas and std::function, a general-purpose polymorphic function wrapper that allows you to store a lambda object.

Using lambdas and algorithms makes your intention more concise and readable. Let us copy all integers from array_a to array_b, but only if they are smaller than some value. One of the possible C implementations is below.

int w_idx = 0;
for(int i = 0; i < sizeof(array_a); i++) {
	if(array_a[i] < 10) {
		array_b[w_idx++] = array_a[i];
	}
}

In C++ we could write it as:

auto less_than_10 =  [](const int& x) {
	return x < 10;
};
std::copy_if(array_a.begin(), array_a.end(), array_b.begin(), less_than_10);

You can write short lambdas close to the code where they are passed as arguments in other functions so that the intent is easier to understand. 

constexpr

constexpr specifier allows you to compute values at compile time. This is a great asset for a developer working on resource-limited embedded systems. Instead of calculating values on your microcontroller and storing the function that does the calculation in flash, you can do it in compile time.

constexpr float compute_charging_current(float battery_capacity) {
	return battery_capacity / 10;
}
constexpr charging_current = compute_charging_current(750);

This is a very simplified example, and lots of people coming from a C background are probably rolling their eyes and saying that this can be done with a simple macro. It’s true. But keep in mind that constexpr functions can be a lot more complex. So do macros, but the constexpr functions are a lot easier to read. 

Templates

Templates have been for a long time in C++. One of the most common usage they bring is compile-time or static polymorphism. They were originally designed to replace primitive macros, and from the beginning, they evolved into one of the most powerful features of C++.

When working on embedded systems where you need to support different architectures and hardware configurations templates are a great asset for code configuration.

Imagine a BLE service that reads data from IMU sensors and sends it over BLE to a client device. This service will also be responsible for some complex processing of data received by sensors before sending the results to the client. We can place the business logic of this software module in templated ImuService class that will depend on the sensors being used in hardware configuration and the actual SoC. 

template <typename S, typename Hw>
class ImuService {
	private:
		S &sensor_;
		Hw &hw_;
	public:
		ImuService(const S& sensor, const Hw& hw): sensor_(sensor), hw_(hw) {
		}
}; 

This approach allows us to instantiate ImuService class with different sensor modules that will comply with the usage of private member sensor_ in the class. The other template parameter Hw will provide the class with hardware-dependent functions. Possible usage of the above class:

MPU6050 imu_sensor;
STM32WB hw; 
ImuService<MPU6050, STM32WB> imu_service(imu_sensor, hw);

Since the C++17 compiler will handle the template parameter deduction, so the last line can be written as: 

ImuService imu_service(imu_sensor, hw);

The other advantage of this approach is testability as it’s quite easy to provide the ImuService with mocked classes for sensor and hardware classes. 

C++ 17

C++17 brought in structured bindings that “bind the specified names to subobjects or elements of the initializer”. This is yet another sugar syntax feature that makes language feel a bit modern and allows you to do things like:

int a[2] = {1,2};
auto& [xr, yr] = a; // xr refers to a[0], yr refers to a[1]

struct S {
    mutable int x1 : 2;
    volatile double y1;
};
S f() { return S{1, 2.3}; }
const auto [x, y] = f();  // x is an int lvalue identifying the 2-bit bit field
                              // y is a const volatile double lvalue

if constexpr was also introduced in the C++17 and it makes metaprogramming in C++ easier, but this topic requires a separate blog post. 

Is C++ better than C for embedded development? 

“C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off”, said Bjarne Stroustrup, a Danish scientist who invented C++. Here is his explanation of the statement he made: 

“What people tend to miss, is that what I said there about C++ is to a varying extent true for all powerful languages. As you protect people from simple dangers, they get themselves into new and less obvious problems. Someone who avoids the simple problems may simply be heading for a not-so-simple one. One problem with very supporting and protective environments is that the hard problems may be discovered too late or be too hard to remedy once discovered. Also, a rare problem is harder to find than a frequent one because you don’t suspect it.”

Transitioning from C to C++ can be very intimidating and often undermined by false assumptions about C++. The purpose of this blog post was to introduce embedded C developers with C++ features I found to be useful. My transition was cautious, and I was including C++ features in my codebase as I was discovering them. I decided to put in my toolbox what seemed and later on showed to be useful for my projects. 

C++ is a huge language, with lots of features that I am still discovering. Not all apply to embedded development, but those that are do make the steep learning curve worthwhile.

Amar Mahmutbegović

Amar Mahmutbegović

Head of Engineering

Embedded developer with a proven history in the development of BLE embedded firmware in C and C++. Co-founder of Semblie.

Semblie is a hardware and software development company based in Europe. We believe that great products emerge from ideas that solve real-world problems.