Book Image

Embedded Programming with Modern C++ Cookbook

By : Igor Viarheichyk
Book Image

Embedded Programming with Modern C++ Cookbook

By: Igor Viarheichyk

Overview of this book

Developing applications for embedded systems may seem like a daunting task as developers face challenges related to limited memory, high power consumption, and maintaining real-time responses. This book is a collection of practical examples to explain how to develop applications for embedded boards and overcome the challenges that you may encounter while developing. The book will start with an introduction to embedded systems and how to set up the development environment. By teaching you to build your first embedded application, the book will help you progress from the basics to more complex concepts, such as debugging, logging, and profiling. Moving ahead, you will learn how to use specialized memory and custom allocators. From here, you will delve into recipes that will teach you how to work with the C++ memory model, atomic variables, and synchronization. The book will then take you through recipes on inter-process communication, data serialization, and timers. Finally, you will cover topics such as error handling and guidelines for real-time systems and safety-critical systems. By the end of this book, you will have become proficient in building robust and secure embedded applications with C++.
Table of Contents (17 chapters)

Working with different architectures

Developers of desktop applications usually pay little attention to the hardware architecture. First, they often use high-level programming languages that hide these complexities at the cost of some performance drop. Second, in most cases, their code runs on x86 architecture and they often take its features for granted. For example, they may assume that the size of int is 32 bits, which is not true in many cases.

Embedded developers deal with a much wider variety of architectures. Even if they do not write code in assembly language native to the target platform, they should be aware that all C and C++ fundamental types are architecture-dependent; the standard only guarantees that int is at least 16 bits. They should also know the traits of particular architectures, such as endianness and alignment, and take into account that operations with floating point or 64-bit numbers, which are relatively cheap on x86 architecture, may be much more expensive on other architectures.

Endianness

Endianness defines the order in which bytes that represent large numerical values are stored in memory.

There are two types of endianness:

  • Big-endian: The most significant byte is stored first. The 0x01020304 32-bit value is stored at the ptr address as follows:

    Offset in memory Value
    ptr 0x01
    ptr + 1 0x02
    ptr + 2 0x03
    ptr + 3 0x04

Examples of big-endian architectures are AVR32 and Motorola 68000.

  • Little-endian: The least significant byte is stored first. The 0x01020304 32-bit value is stored at the ptr address as follows:

    Offset in memory Value
    ptr 0x04
    ptr + 1 0x03
    ptr + 2 0x02
    ptr + 3 0x01

The x86 architecture is little-endian.

  • Bi-endian: Hardware supports switchable endianness. Some examples are PowerPC, ARMv3, and the preceding examples.

Endianness is particularly essential when exchanging data with other systems. If a developer sends the 0x01020304 32-bit integer as is, it may be read as 0x04030201 if the endianness of the receiver does not match the endianness of the sender. That is why data should be serialized.

This C++ snippet can be used to determine the endianness of a system:

#include <iostream>
int main() {
union {
uint32_t i;
uint8_t c[4];
} data;
data.i = 0x01020304;
if (data.c[0] == 0x01) {
std::cout << "Big-endian" << std::endl;
} else {
std::cout << "Little-endian" << std::endl;
}
}

Alignment

Processors don't read and write data in bytes but in memory words—chunks that match their data address size. 32-bit processors work with 32-bit words, 64-bit processors with 64-bit words, and so on.

Reads and writes are most efficient when words are aligned—the data address is a multiple of the word size. For example, for 32-bit architectures, the 0x00000004 address is aligned, while 0x00000005 is unaligned.

Compilers align data automatically to achieve the most efficient data access. When it comes to structures, the result may be surprising for developers who are not aware of alignment:

 struct {

uint8_t c;

uint32_t i;

} a = {1, 1};

std::cout << sizeof(a) << std::endl;

What is the output of the preceding code snippet? The size of uint8_t is 1 and the size of  uint32_t is 4. A developer may expect that the size of the structure is the sum of the individual sizes. However, the result highly depends on the target architecture.

For x86, the result is 8. Let's add one more uint8_t field before i:

struct {

uint8_t c;

uint8_t cc;

uint32_t i;

} a = {1, 1};

std::cout << sizeof(a) << std::endl;

The result is still 8! The compiler optimizes the placement of the data fields within a structure according to alignment rules by adding padding bytes. The rules are architecture-dependent and the result may be different for other architectures. As a result, structures cannot be exchanged directly between two different systems without serializationwhich will be explained in more depth in Chapter 8, Communication and Serialization.

Besides the CPU, access data alignment is also crucial for efficient memory mapping through hardware address translation mechanisms. Modern operating systems operate 4 KB memory blocks or pages to map a process virtual address space to physical memory. Aligning data structures on 4 KB boundaries can lead to performance gain.

Fixed-width integer types

C and C++ developers often forget that the size of fundamental data types, such as char, short, or int, is architecture-dependent. To make the code portable, embedded developers often use fixed-size integer types that explicitly specify the size of a data field.

The most commonly used data types are as follows:

Width Signed Unsigned
8-bit int8_t uint8_t
16-bit int16_t uint16_t
32-bit int32_t uint32_t

 

The pointer size also depends on the architecture. Developers often need to address elements of arrays and since arrays are internally represented as pointers, the offset representation depends on the pointer size. size_t is a special data type to represent the offset and data sizes in an architecture-independent way.