2

Work Environment and Workflow Optimization

The first step toward a successful software project is choosing the right tools. Embedded development requires a set of hardware and software instruments that make the developer’s life easier and may significantly improve productivity and cut down the total development time. This chapter provides a description of these tools and gives advice on how to use them to improve the workflow.

The first section gives us an overview of the workflow in native C programming, and gradually reveals the changes necessary to translate the model to an embedded development environment. The GCC toolchain, a set of development tools to build the embedded application, is introduced through the analysis of its components.

Finally, in the last two sections, strategies of interaction with the target are proposed, to provide mechanisms for the debugging and validation of the embedded software running on the platform.

The topics covered in this chapter are the following:

  • Workflow overview
  • Text editors versus integrated environments
  • The GCC toolchain
  • Interaction with the target
  • Validation

By the end of this chapter, you will have learned how to create an optimized workflow by following a few basic rules, keeping the focus on test preparation, and a smart approach to debugging.

Workflow overview

Writing software in C, as well as in every other compiled language, requires the code to be transformed into an executable format for a specific target to run it. C is portable across different architectures and execution environments. Programmers rely on a set of tools to compile, link, execute, and debug software to a specific target.

Building the firmware image of an embedded system relies on a similar set of tools, which can produce firmware images for specific targets, called a toolchain. This section gives an overview of the common sets of tools required to write software in C and produce programs that are directly executable on the machine that compiled them. The workflow must then be extended and adapted to integrate the toolchain components and produce executable code for the target platform.

The C compiler

The C compiler is a tool responsible for translating source code into machine code, which can be interpreted by a specific CPU. Each compiler can produce machine code for one environment only, as it translates the functions into machine-specific instructions, and it is configured to use the address model and the register layout of one specific architecture. The native compiler included in most GNU/Linux distributions is the GNU Compiler Collection, commonly known as GCC. The GCC is a free software compiler system distributed under the GNU general public license since 1987, and since then, it has been successfully used to build UNIX-like systems. The GCC included in the system can compile C code into applications and libraries capable of running on the same architecture as that of the machine running the compiler.

The GCC compiler takes source code files as input, with the .c extension, and produces object files, with .o extensions, containing the functions and the initial values of the variables, translated from the input source code into machine instructions. The compiler can be configured to perform additional optimization steps at the end of the compilation that are specific to the target platform and insert debug data to facilitate debugging at a later stage.

A minimalist command line used to compile a source file into an object using the host compiler only requires the -c option, instructing the GCC program to compile the sources into an object of the same name:

$ gcc -c hello.c

This statement will try to compile the C source contained in the hello.c file and transform it into machine-specific code that is stored in the newly created hello.o file.

Compiling code for a specific target platform requires a set of tools designed for that purpose. Architecture-specific compilers exist, which provide compilers creating machine instructions for a specific target, different from the building machine. The process of generating code for a different target is called cross compilation. The cross compiler runs on a development machine, the host, to produce machine-specific code that can execute on the target.

In the next section, a GCC-based toolchain is introduced as the tool to create the firmware for an embedded target. The syntax and the characteristics of the GCC compiler are described there.

The first step for building a program made of separate modules is to compile all the sources into object files so that the components needed by the system are grouped and organized together in the final step, consisting of linking together all the required symbols and arranging the memory areas to prepare the final executable, which is done by another dedicated component in the toolchain.

Linker

The linker is the tool that composes executable programs and resolves the dependencies among object files provided as input.

The default executable format that is produced by the linker is the Executable and Linkable Format (ELF). The ELF is the default standard format for programs, objects, shared libraries, and even GDB core dumps on many Unix and Unix-like systems. The format has been designed to store programs on disks and other media supports, so the host operating system can execute it by loading the instructions in RAM and allocating the space for the program data.

Executable files are divided into sections, which can be mapped to specific areas in memory needed by the program to execute. The ELF file starts with a header containing the pointer to the various sections within the file itself, which contains the program’s code and data.

The linker maps the content of the areas describing an executable program into sections conventionally starting with a . (dot). The minimum set of sections required to run the executable consists of the following:

  • .text: Contains the code of the program, accessed in read-only mode. It contains the executable instructions of the program. The functions compiled into object files are arranged by the linker in this section, and the program always executes instructions within this memory area.
  • .rodata: Contains the value of constants that cannot be altered at runtime. It is used by the compiler as the default section to store constants because it is not allowed to modify the stored values at runtime.
  • .data: Contains the values of all the initialized variables of the program that are accessible in the read/write mode at runtime. It is the section that contains all the variables (static or global) that have been initialized in the code. Before executing, this area is generally remapped to a writable location in RAM, and the content of the ELF is automatically copied during the initialization of the program, at runtime, before executing the main function.
  • .bss: This is a section reserved for uninitialized data, accessible in the read/write mode at runtime. It derives its name from an ancient assembly instruction of old microcode written for the IBM 704 in the 1950s. It was originally an acronym for Block Started by Symbol (BSS), used to reserve a fixed amount of uninitialized memory. In the ELF context, it contains all the uninitialized global and static symbols, which must be accessible in the read-write mode at runtime. Because there is no value assigned, the ELF file only describes the section in the header but does not provide any content for it. The initialization code should ensure that all the variables in this section are set to zero before the execution of the main() function.

When building native software on the host machine, much of the complexity of the linking step is hidden, but the linker is configured by default to arrange the compiled symbols into specific sections, which can be later used by the operating system to assign the corresponding segments in the process virtual address space when executing the program. It is possible to create a working executable for the host machine by simply invoking gcc, this time without the -c option, providing the list of the object files that must be linked together to produce the ELF file. The -o option is used to specify the output filename, which otherwise would default to a.out:

$ gcc -o helloworld hello.o world.o

This command will try to build the helloworld file, which is an ELF executable for the host system, using the symbols previously compiled into the two objects.

In an embedded system things change a bit, as booting a bare-metal application implies that the sections must be mapped to physical areas in memory at linking time. To instruct the linker to associate the sections to well-known physical addresses, a custom linker script file must be provided, describing the memory layout of the executable bare-metal application, and providing additional custom sections that may be required by the target system.

A more detailed explanation of the linking step is provided later, in the Linking the executable section.

Make: a build automation tool

Several open source tools are available to automate a build process, and a few of them are widely used in different development environments. Make is the standard UNIX tool to automate the steps required to create the required binary images from the sources, check the dependencies for each component, and execute the steps in the right order. Make is a standard POSIX tool, and it is part of many UNIX-like systems. In a GNU/Linux distribution, it is implemented as a standalone tool, which is part of the GNU project. From this point on, the GNU Make implementation is simply referred to as Make.

Make is designed to execute the default build by simply invoking the make command with no arguments from the command line, provided that a makefile is present in the working directory. A makefile is a special instruction file, containing rules and recipes to build all the files needed until the expected output files are generated. Open source alternatives offering similar solutions for build automation exist, such as CMake and SCons, but all the examples in this book are built using Make because it provides a simple and essential enough environment to control the build system, and it is the one standardized by POSIX.

Some integrated development environments use built-in mechanisms to coordinate the building steps or generate makefiles before invoking Make automatically when the user requests to build the output files. However, editing makefiles manually gives complete control over the intermediate steps to generate the final images, where the user can customize the recipes and rules used to generate the desired output files.

There is no specific version that needs to be installed to cross compile code for the Cortex-M target, but some extra parameters, such as the location of the toolchain binaries, or the specific flags needed by the compiler, need to be taken care of when writing targets and directives within the makefile.

One of the advantages of using a build process is that targets may have implicit dependencies from other intermediate components that are automatically resolved at compile time. If all the dependencies are correctly configured, a makefile ensures that the intermediate steps are executed only when needed, reducing the compile time of the whole project when only a few sources are altered or when single object files have been deleted.

Makefiles have a specific syntax to describe rules. Each rule begins with the target files expected as the output of the rule, a colon, and the list of prerequisites, which are the files necessary to execute the rule. A set of recipe items follow, each one describing the actions that Make will execute to create the desired target:

target: [prerequisites]
 recipe
 recipe
 ...

By default, Make will execute the first rule encountered while parsing the file if no rule name is specified from the command line. If any of the prerequisites are not available, Make automatically looks for a rule in the same makefile that can create the required file recursively until the chain of requirements is satisfied.

Makefiles can assign a custom string of text to internal variables while executing. Variable names can be assigned using the = operator and referred to by prefixing them with $. For example, the following assignment is used to put the name of two object files into the OBJS variable:

OBJS = hello.o world.o

A few important variables that are assigned automatically within the rules are the following:

Table 2.1 – Some automatic variables that can be used in makefile recipes

Table 2.1 – Some automatic variables that can be used in makefile recipes

These variables are handy to use within the recipe action lines. For example, the recipe to generate a helloworld ELF file from the two object files can be written as follows:

helloworld: $(OBJS)
 gcc -o $(@) $(^)

Some of the rules are implicitly defined by Make. For example, the rule to create the hello.o and world.o files from their respective source files can be omitted, as Make expects to be able to obtain each one of these object files in the most obvious way, which is by compiling the corresponding C source files with the same name if present. This means that this minimalist makefile is already able to compile the two objects from the sources and link them together using the default set of options for the host system.

The linking recipe can also be implicit if the executable has the same name as one of its prerequisite objects minus its .o extension. If the final ELF file is called hello, our makefile could simply become the following one-liner:

hello: world.o

This would automatically resolve the hello.o and world.o dependencies, and then link them together using an implicit linker recipe similar to the one we used in the explicit target.

Implicit rules use predefined variables, which are assigned automatically before the rules are executed, but can be modified within the makefile. For example, it is possible to change the default compiler by altering the CC variable. Here is a short list of the most important variables that may be used to alter implicit rules and recipes:

Table 2.2 – Implicit, predefined variables that specify the default toolchain and flags

Table 2.2 – Implicit, predefined variables that specify the default toolchain and flags

When linking a bare-metal application for embedded platforms, the makefile must be modified accordingly, and as shown later in this chapter, several flags are required to properly cross compile the sources and instruct the linker to use the desired memory layout to organize the memory sections. Moreover, additional steps are generally needed to manipulate the ELF file and translate it to a format that can be transferred to the target system. However, the syntax of the makefile is the same, and the simple rules shown here are not too different from those used to build the example. The default variables still need to be adjusted to modify the default behavior if implicit rules are used.

When all the dependencies are correctly configured in the makefile, Make ensures that the rules are only executed when the target is older than its dependencies, thus reducing the compile time of the whole project when only a few sources are altered or when single object files have been deleted.

Make is a very powerful tool, and its range of possibilities goes far beyond the few features used to generate the examples in this book. Mastering the automation process of builds may lead to optimized build processes. The syntax of makefiles includes useful features, such as conditionals, which can be used to produce different results by invoking a makefile using different targets or environment variables. For a better understanding of the capabilities of Make, please refer to the GNU Make manual available at https://www.gnu.org/software/make/manual.

Debugger

In the host environment, debugging an application that runs on top of the operating system is done by running a debugger tool, which can attach to an existing process or spawn a new one given an executable ELF file and its command-line arguments. The default debugging option provided by the GCC suite is called GDB, an acronym for the GNU Debugger. While GDB is a command-line tool, several frontends have been developed to provide better visualization of the state of the execution, and some integrated development environments provide built-in frontends for interacting with the debugger while tracing the single lines being executed.

Once again, the situation is slightly changed when the software to debug is running on a remote platform. A version of GDB, distributed with the toolchain and specific to the target platform, can be run on the development machine to connect to a remote debug session. A debug session on a remote target requires an intermediate tool that is configured to translate GDB commands into actual actions on the core CPU and the related hardware infrastructure to establish communication with the core.

Some embedded platforms provide hardware breakpoints, which are used to trigger system exceptions every time the selected instructions are executed.

Later in this chapter, we will see how a remote GDB session can be established with the target in order to interrupt its execution at the current point, proceed to step through the code, place breakpoints and watchpoints, and inspect and modify the values in memory.

A handful of GDB commands are introduced, giving a quick reference to some of the functionalities provided by the GDB command-line interface, which can be effectively used to debug embedded applications.

The debugger gives the best possible understanding of what the software is doing at runtime and facilitates the hunt for programming errors while directly looking at the effects of the execution on memory and CPU registers.

Embedded workflow

If compared to other domains, the embedded development life cycle includes some additional steps. The code must be cross compiled, the image manipulated then uploaded to a target, tests must be run, and possibly hardware tools are involved in the measurement and verification phases. The life cycle of native application software, when using compiled languages, looks like this diagram:

Figure 2.1 – A typical life cycle of application development

Figure 2.1 – A typical life cycle of application development

When writing software within the same architecture, tests and debugging can be performed right after compiling, and it is often easier to detect issues. This results in a shorter time for the typical loop. Moreover, if the application crashes because of a bug, the underlying operating system can produce a core dump, which can be analyzed using the debugger at a later time by restoring the content of the virtual memory and the context of the CPU registers right at the moment when the bug shows up.

On the other hand, intercepting fatal errors on an embedded target might be slightly more challenging because of the potential side effect of memory and register corruption in the absence of virtual addresses and memory segmentation provided by the operating systems in other contexts. Even if some targets can intercept abnormal situations by triggering diagnostic interrupts, such as the hard fault handler in Cortex-M, restoring the original context that generated the error is often impossible.

Furthermore, every time new software is generated, there are a few time-consuming steps to perform, such as the translation of the image to a specific format, and uploading the image to the target itself, which may take anywhere from a few seconds up to a minute, depending on the size of the image and the speed of the interface used to communicate with the target:

Figure 2.2 – The embedded development life cycle, including additional steps required by the environment

Figure 2.2 – The embedded development life cycle, including additional steps required by the environment

In some of the phases of the development, when multiple consecutive iterations may be required to finalize a feature implementation or detect a defect, the timing between compiling and testing the software has an impact on the efficiency of the whole life cycle. Specific tasks implemented in the software, which involve communication through serial or network interfaces, can only be verified with signal analysis or by observing the effect on the peripheral or the remote system involved. Analyzing the electrical effects on the embedded system requires some hardware setup and instrument configuration, which add more time to the equation.

Finally, developing a distributed embedded system composed of several devices running different software images may result in repeating the preceding iterations for each of these devices. Whenever possible, these steps should be eliminated by using the same image and different set configuration parameters on each device and by implementing parallel firmware upgrade mechanisms. Protocols such as JTAG support uploading the software image to multiple targets sharing the same bus, significantly cutting down the time required for the firmware upgrades, especially in those distributed systems with a larger number of devices involved.

No matter how complex the project is expected to be, it is, in general, worth spending as much time as needed to optimize the life cycle of the software development at the beginning in order to increase efficiency later on. No developer likes to switch the focus away from the actual coding steps for too long, and it might be frustrating to work in a suboptimal environment where stepping through the process requires too much time or human interaction.

An embedded project can be started from scratch using a text editor or by creating a new project in an integrated development environment.

Text editors versus integrated environments

While mostly a matter of developer preferences, the debate is still open in the embedded community between those who use a standalone text editor and those who prefer to have all the components of the toolchain integrated into one GUI.

Modern IDEs incorporate tools for the following tasks:

  • Managing the components of the project
  • Quickly accessing all the files for editing as well as extensions to upload the software on the board
  • Starting a debugging session with a single click

Microcontroller manufacturers often distribute their development kits along with IDEs that make it easy to access advanced features that are specific to the microcontroller, thanks to preconfigured setups and wizards facilitating the creation of new projects. Most IDEs include widgets to automatically generate the setup code for pin multiplexing for specific microcontrollers, starting from a graphical interface. Some of them even offer simulators and tools to predict runtime resource usage, such as dynamic memory and power consumption.

The majority of these tools are based on some customization of Eclipse, a popular open source desktop IDE, originally designed as a tool for Java software development, then later on very successful in many other fields thanks to the possibilities to extend and customize its interface.

There are downsides to the IDE approach too. IDEs, in general, do not embed the actual toolchain in the code. Rather, they provide a frontend interface to interact with a compiler, linker, debugger, and other tools. To do so, they have to store all the flags, configuration options, the paths of included files, and compile-time-defined symbols in a machine-readable configuration file. Some users find those options difficult to access by navigating through the multiple menus of the GUI. Other key components of the project, such as the linker script, may also be well hidden under the hood, in some cases even automatically generated by the IDE and difficult to read. For most IDE users, however, these downsides are compensated by the perks of developing with an integrated environment.

There is one last caveat that has to be considered, though. The project will sooner or later be built and tested automatically, as previously analyzed in the Make: a build automation tool section. Robots are, in general, terrible users of IDEs, while they can build and run any test, even interacting with real targets, using a command-line interface. A development team using an IDE for embedded development should always consider providing an option to build and test any software through a command-line alternate strategy.

While some complexity of the toolchain can be abstracted with a GUI, it is useful to understand the functions of the set of applications under the hood. The remaining sections of this chapter explore the GCC toolchain, the most popular cross-architecture set of compilers for many 32-bit microcontrollers.

The GCC toolchain

While in the case of the IDE, its complexity is abstracted through the user interface, a toolchain is a set of standalone software applications, each one serving a specific purpose.

GCC is one of the reference toolchains to build embedded systems due to its modular structure allowing backends for multiple architectures. Thanks to its open source model, and the flexibility in building tailored toolchains from it, GCC-based toolchains are among the most popular development tools in embedded systems.

Building software using a command-line-based toolchain has several advantages, including the possibility of automating the intermediate steps that would build all the modules up from the source code into the final image. This is particularly useful when it is required to program multiple devices in a row or to automate builds on a continuous integration server.

ARM distributes the GNU Arm Embedded Toolchain for all the most popular development hosts. Toolchains are prefixed with a triplet describing the target. In the case of the GNU Arm Embedded Toolchain, the prefix is arm-none-eabi, indicating that the cross compiler backend is configured to produce objects for ARM, with no specific support for an operating system API, and with the embedded ABI.

The cross compiler

The cross compiler distributed with a toolchain is a variant of GCC, with the backend configured to build object files that contain machine code for a specific architecture. The output of the compilation is one set of object files containing symbols that can only be interpreted by the specific target. Arm-none-eabi-gcc, the GCC variant provided by ARM to build software for microcontrollers, can compile C code into machine instructions and CPU optimizations for several different targets. Each architecture needs its own specific toolchain that will produce target-specific executables.

The GCC backend for the ARM architecture supports several machine-specific options to select the correct instruction set for the CPU and the machine-specific optimization parameters.

The following table lists some of the ARM-specific machine options available on the GCC backend as -m flags:

Table 2.3 – Architecture-specific compiler options for GCC ARM

Table 2.3 – Architecture-specific compiler options for GCC ARM

To compile code that is compatible with a generic ARM Cortex M4, the -mthumb and -mcpu=cortex-m4 options must be specified every time the compiler is invoked:

$ arm-none-eabi-gcc -c test.c -mthumb -mcpu=cortex-m4

The test.o file that is the result of this compile step is very different from the one that can be compiled, from the same source, using the gcc host. The difference can be better appreciated if, instead of the two object files, the intermediate assembly code is compared. The compiler is, in fact, capable of creating intermediate assembly code files instead of compiled and assembled objects when it is invoked with the -S option.

In a similar way to the host GCC compiler, there are different levels of possible optimization available to activate. In some cases, it makes sense to activate the size optimization to generate smaller object files. It is preferable, though, that the non-optimized image can fit the flash during the development to facilitate the debugging procedures, as optimized code flow is more difficult to follow when the compiler may change the order of the execution of the code and hide away the content of some variables. The optimization parameter can be provided at the command line to select the desired optimization level:

Table 2.4 – GCC levels of optimization

Table 2.4 – GCC levels of optimization

Another generic GCC command-line option that is often used while debugging and prototyping is the -g flag, which instructs the compiler to keep the debugging-related data in the final object in order to facilitate access to functions’ and variables’ readable handles while running within the debugger.

To inform the compiler that we are running a bare-metal application, the -ffreestanding command-line option is used. In GCC jargon, a freestanding environment is defined by the possible lack of a standard library in the linking step, and most importantly, this option alerts the compiler that it should not expect to use the main function as the entry point of the program or provide any preamble code before the beginning of the execution. This option is required when compiling code for the embedded platforms, as it enables the boot mechanism described in Chapter 4, The Boot-Up Procedure.

The GCC program supports many more command-line options than those quickly introduced here. For a more complete overview of the functionalities offered, please refer to the GNU GCC manual, available at https://gcc.gnu.org/onlinedocs/.

To integrate the cross compiling toolchain in the automated build using Make, a few changes are required in the makefile.

Assuming that the toolchain is correctly installed on the development host and reachable in its executing path, it is sufficient to change the default compiler command using the CC Make variable in the makefile:

CC=arm-none-eabi-gcc

The custom command-line options required to run the compile options may be exported through the CFLAGS variable:

CFLAGS=-mthumb -mcpu=cortex-m4 -ffreestanding

Using default makefile variables, such as CC and CFLAGS, enables implicit makefile rules, building object files from C sources with the same name, and a custom compiler configuration.

Compiling the compiler

Binary distributions of the GCC toolchain are available to download for several specific targets and host machines. To compile the code for the ARM Cortex-M microprocessors, the arm-none-eabi toolchain is made available for most GNU/Linux distributions. However, in some cases, it might be handy to build the toolchain entirely from the sources. This might be, for example, when the compiler for a certain target does not exist yet or is not shipped in binary format for our favorite development environment. This process is also useful to better understand the various components that are required to build the tools.

Crosstool-NG is an open source project that consists of a set of scripts aimed at automating the process of creating a toolchain. The tool retrieves the selected version of every component, then creates an archive of the toolchain that can be redistributed in binary form. This is normally not necessary, while sometimes useful when it is necessary to modify the sources for a specific component, such as, for example, the C libraries that are finally integrated into the toolchain. It is easy to create a new configuration in crosstool-NG, thanks to its configurator, based on the Linux menuconfig kernel. After installing crosstool-NG, the configurator can be invoked by using the following:

$ ct-ng menuconfig

Once the configuration has been created, the build process can be started. Since the operation requires retrieving all the components, patching them, and building the toolchain, it may take several minutes, depending on the speed of the host machine and the internet connection, to retrieve all the components. The build process can be started by issuing the following:

$ ct-ng build

Predefined configurations are available for compiling commonly used toolchains, mostly for targets running Linux. When compiling a toolchain for a Linux target, there are a few C libraries to choose from. In our case, since we want a bare-metal toolchain, newlib is the default choice. Several other libraries provide an implementation of a subset of the C standard library, such as uClibc and musl. The newlib library is a small cross-platform C library mostly designed for embedded systems with no operating system on board, and it is provided as the default in many GCC distributions, including the arm-none-eabi cross compiler distributed by ARM.

Linking the executable

Linking is the last step in the creation of the ELF file. The cross compiling GCC groups all the object files together and resolves the dependencies among symbols. By passing the -T filename option at the command line, the linker is asked to replace the default memory layout for the program with a custom script, contained in the filename.

The linker script is a file containing the description of the memory sections in the target, which need to be known in advance in order for the linker to place the symbols in the correct sections in flash, and instruct the software components about special locations in the memory mapping area that can be referenced in the code. The file is recognizable by its .ld extension, and it is written in a specific language. As a rule of thumb, all the symbols from every single compiled object are grouped in the sections of the final executable image.

The script can interact with the C code, exporting symbols defined within the script and following indications provided in the code using GCC-specific attributes associated with symbols. The __attribute__ keyword is provided by GCC to be put in front of the symbol definition to activate GCC-specific, non-standard attributes for each symbol.

Some GCC attributes can be used to communicate to the linker about the following:

  • Weak symbols, which can be overridden by symbols with the same name
  • Symbols to be stored in a specific section in the ELF file, defined in the linker script
  • Implicitly used symbols, which prevent the linker from discarding the symbol because it is referred to nowhere in the code

The weak attribute is used to define weak symbols, which can be overridden anywhere else in the code by another definition with the same name. Consider, for example, the following definition:

void __attribute__(weak) my_procedure(int x) {/* do nothing */}

In this case, the procedure is defined to do nothing, but it is possible to override it anywhere else in the code base by defining it again, using the same name, but this time without the weak attribute:

void my_procedure(int x) { y = x; }

The linker step ensures that the final executable contains exactly one copy of each defined symbol, which is the one without the attribute, if available. This mechanism introduces the possibility of having several different implementations of the same functionality within the code, which can be altered by including different object files in the linking phase. This is particularly useful when writing code that is portable to different targets while still maintaining the same abstractions.

Besides the default sections required in the ELF description, custom sections may be added to store specific symbols, such as functions and variables, at fixed memory addresses. This is useful when storing data at the beginning of a flash page, which might be uploaded to flash at a different time than the software itself. This is the case for target-specific settings in some cases.

Using the custom GCC section attribute when defining a symbol ensures that the symbol ends up at the desired position in the final image. Sections may have custom names as long as an entry exists in the linker to locate them. The section attribute can be added to a symbol definition as follows:

const uint8_t
 __attribute__((section(".keys")))
 private_key[KEY_SIZE] = {0};

In this example, the array is placed in the .keys section, which requires its own entry in the linker script as well.

It is considered good practice to have the linker discard the unused symbols in the final image, especially when using third-party libraries that are not completely utilized by the embedded application. This can be done in GCC using the linker garbage collector, activated via the -gc-sections command-line option. If this flag is provided, the sections that are unused in the code are automatically discarded, and the unused symbols will be kept out of the final image.

To prevent the linker from discarding symbols associated with a particular section, the used attribute marks the symbol as implicitly used by the program. Multiple attributes can be listed in the same declaration, separated by commas, as follows:

const uint8_t    __attribute__((used,section(".keys")))
   private_key[KEY_SIZE] = {0};

In this example, the attributes indicate both that the private_key array belongs to the .keys section and that it must not be discarded by the linker garbage collector because it is marked as used.

A simple linker script for an embedded target defines at least the two sections relative to RAM and FLASH mapping and exports some predefined symbols to instruct the assembler of the toolchain about the memory areas. A bare-metal system based on the GNU toolchain usually starts with a MEMORY section, describing the mapping of the two different areas in the system, such as the following:

MEMORY {
  FLASH(rx) : ORIGIN = 0x00000000, LENGTH=256k
  RAM(rwx) : ORIGIN = 0x20000000, LENGTH=64k
}

The preceding code snippet describes two memory areas used in the system. The first block is 256k mapped to FLASH, with the r and x flags indicating that the area is accessible for read and execute operations. This enforces the read-only attribute of the whole area and ensures that no variant sections are placed there. RAM, on the other hand, can be accessed in write mode directly, which means that variables are going to be placed in a section within that area. In this specific example, the target maps the FLASH mapping at the beginning of the address space, while the RAM is mapped starting at 512 MB. Each target has its address space mapping and flash/RAM size, which makes the linker script target-specific.

As mentioned earlier in this chapter, the .text and .rodata ELF sections can only be accessed for reading, so they can safely be stored in the FLASH area since they will not be modified while the target is running. On the other hand, both .data and .bss must be mapped in RAM to ensure that they are modifiable.

Additional custom sections can be added to the script where it is necessary to store additional sections at a specific location in memory. The linker script can also export symbols related to a specific position in memory or to the length of dynamically sized sections in memory, which can be referred to as external symbols and accessed in the C source code.

The second block of statements in the linker script is called SECTIONS and contains the allocation of the sections in specific positions of the defined memory areas. The . symbol, when associated with a variable in the script, represents the current position in the area, which is filled progressively from the lower addresses available.

Each section must specify the area where it has to be mapped. The following example, though still incomplete to run the binary executable, shows how the different sections can be deployed using the linker script. The .text and .rodata sections are mapped in the flash memory:

SECTIONS
{
    /* Text section (code and read-only data) */
    .text :
    {
        . = ALIGN(4);
        _start_text = .;
        *(.text*) /* code */
        . = ALIGN(4);
        _end_text = .;
        *(.rodata*) /* read only data */
        . = ALIGN(4);
        _end_rodata = .;
} > FLASH

The modifiable sections are mapped in RAM, with two special cases to notice here.

The AT keyword is used to indicate the load address to the linker, which is the area where the original values of the variables in .data are stored, while the actual addresses used in the execution are in a different memory region. More details about the load address and the virtual address for the .data section are explained in Chapter 4, The Boot-Up Procedure.

The NOLOAD attribute used for the .bss section ensures that no predefined values are stored in the ELF file for this section. Uninitialized global and static variables are mapped by the linker in the RAM area, which is allocated by the linker:

_stored_data = .;
.data: AT(__stored_data)
{
    . = ALIGN(4);
    _start_data = .;
    *(.data*)
    . = ALIGN(4);
    _start_data = .;
} > RAM
.bss (NOLOAD):
{
    . = ALIGN(4);
    _start_bss = .;
    *(.bss*)
    . = ALIGN(4);
    _end_bss = .;
} > RAM

The alternative way to force the linker to keep sections in the final executable, avoiding their removal due to the linker garbage collector, is the use of the KEEP instruction to mark sections. Please note that this is an alternative to the __attribute__((used)) mechanism explained earlier:

.keys :
{
    . = ALIGN(4);
    *(.keys*) = .;
    KEEP(*(.keys*));
} > FLASH

It is useful, and advisable in general, to have the linker create a .map file alongside the resultant binary. This is done by appending the -Map=filename option to the link step, such as in the following:

$ arm-none-eabi-ld -o image.elf object1.o object2.o 
-T linker_script.ld -Map=map_file.map

The map file contains the location and the description of all the symbols, grouped by sections. This is useful for looking for the specific location of symbols in the image, as well as for verifying that useful symbols are not accidentally discarded due to a misconfiguration.

Cross compiling toolchains provide standard C libraries for generic functionalities, such as string manipulation or standard type declarations. These are substantially a subset of the library calls available in the application space of an operating system, including standard input/output functions. The backend implementation of these functions is often left to the applications, so calling a function from the library that requires interaction with the hardware, such as printf, implies that a write function is implemented outside of the library, providing the final transfer to a device or peripheral.

The implementation of the backend write function determines which channel would act as the standard output for the embedded application. The linker is capable of resolving the dependencies towards standard library calls automatically, using the built-in newlib implementation. To exclude the standard C library symbols from the linking process, the -nostdlib option can be added to the options passed to GCC during the linking step.

Binary format conversion

Despite containing all the compiled symbols in binary format, an ELF file is prefixed with a header that contains a description of the content and pointers to the positions where the sections start within the file. All this extra information is not needed to run on an embedded target, so the ELF file produced by the linker has to be transformed into a plain binary file. A tool in the toolchain, called objcopy, converts images from one standard format to others, and what is generally done is a conversion of the ELF into a raw binary image without altering the symbols. To transform the image from ELF to binary format, invoke the following:

$ arm-none-eabi-objcopy -I elf -O binary image.elf image.bin

This creates a new file, called image.bin, from the symbols contained in the original ELF executable, which can be uploaded to the target.

Even if not suitable in general for direct upload on the target with third-party tools, it is possible to load the symbols through the debugger and upload them to the flash address. The original ELF file is also useful as the target of other diagnostic tools in the GNU toolchain, such as nm and readelf, which display the symbols in each module, with their type and relative address within the binary image. Furthermore, by using the objdump tool on the final image, or even on single object files, several details about the image can be retrieved, including the visualization of the entire assembly code, using the -d disassemble option:

arm-none-eabi-objdump -d image.elf

At this point, the toolchain has provided us with all the artifacts needed to run, debug, and analyze the compiled software on a target microcontroller. In order to transfer the image or start a debugging session, we need additional specific tools, described in the next section.

Interacting with the target

For development purposes, embedded platforms are usually accessed through a JTAG or an SWD interface. Through these communication channels, it is possible to upload the software onto the flash of the target and access the on-chip debug functionality. Several self-contained JTAG/SWD adapters on the market can be controlled through a USB from the host, while some development boards are equipped with an extra chip controlling the JTAG channel that connects to the host through a USB.

A powerful generic open source tool to access JTAG/SWD functionalities on the target is the Open On-Chip Debugger (OpenOCD). Once properly configured, it creates local sockets that can be used as a command console and for interaction with the debugger frontend. Some development boards are distributed with additional interfaces to communicate with the core CPU. For example, STMicroelectronics prototyping boards for Cortex-M are rarely shipped without a chip technology called ST-Link, which allows direct access to debug and flash manipulation functionalities. Thanks to its flexible backend, OpenOCD can communicate with these devices using different transport types and physical interfaces, including ST-Link and other protocols. Several different boards are supported and the configuration files can be found by OpenOCD.

When started, OpenOCD opens two local TCP server sockets on preconfigured ports, providing communication services with the target platform. One socket provides an interactive command console that can be accessed through Telnet, while the other is a GDB server used for remote debugging, as described in the next section.

OpenOCD is distributed along with two sets of configuration files that describe the target microcontroller and peripherals (in the target/ directory), and the debugging interface used to communicate to it via JTAG or SWD (in the interface/ directory). A third set of configuration files (in the board/ directory) contain configuration files for well-known systems, such as development boards equipped with an interface chip, which combines both interfaces and target settings by including the correct files.

In order to configure OpenOCD for the STM32F746-Discovery target, we can use the following openocd.cfg configuration file:

telnet_port 4444
gdb_port 3333
source [find board/stm32f7discovery.cfg]

The board-specific configuration file, which was imported from openocd.cfg, through the source directive, instructs OpenOCD to use the ST-Link interface to communicate with the target and sets all the CPU-specific options for the STM32F7 family of microcontrollers.

The two ports specified in the main configuration file, using the telnet_port and gdb_port directives, instruct OpenOCD to open two listening TCP sockets.

The first socket, often referred to as the monitor console, can be accessed by connecting to the local 4444 TCP port, using a Telnet client from the command line:

$ telnet localhost 4444
Open On-Chip Debugger
>

The sequence of OpenOCD directives to initialize, erase the flash, and transfer the image starts with the following:

> init
> halt
> flash probe 0

The execution is stopped at the beginning of the software image. After the probe command, the flash is initialized, and OpenOCD will print out some information, including the address mapped to write on the flash. The following information shows up with the STM32F746:

device id = 0x10016449
flash size = 1024kbytes
flash "stm32f2x" found at 0x08000000

The geometry of the flash can be retrieved using this command:

> flash info 0

Which, on STM32F746 shows as the following:

#0 : stm32f2x at 0x08000000, size 0x00100000, buswidth 0, chipwidth 0
# 0: 0x00000000 (0x8000 32kB) not protected
# 1: 0x00008000 (0x8000 32kB) not protected
# 2: 0x00010000 (0x8000 32kB) not protected
# 3: 0x00018000 (0x8000 32kB) not protected
# 4: 0x00020000 (0x20000 128kB) not protected
# 5: 0x00040000 (0x40000 256kB) not protected
# 6: 0x00080000 (0x40000 256kB) not protected
# 7: 0x000c0000 (0x40000 256kB) not protected
STM32F7[4|5]x - Rev: Z

This flash contains eight sectors. If the OpenOCD target supports it, the flash can be completely erased by issuing the following command from the console:

> flash erase_sector 0 0 7

Once the flash memory is erased, we can upload a software image to it, linked and converted to raw binary format using the flash write_image directive. As the raw binary format does not contain any information about its destination address in the mapped area, the starting address in the flash must be provided as the last argument, as follows:

> flash write_image /path/to/image.bin 0x08000000

These directives can be appended to the openocd.cfg file, or to different configuration files, in order to automate all the steps needed for a specific action, such as erasing the flash and uploading an updated image.

Some hardware manufacturers offer their own set of tools to interact with the devices. STMicroelectronics devices can be programmed using the ST-Link utilities, an open source project that includes a flash tool (st-flash), and a GDB server counterpart (st-util). Some platforms have built-in bootloaders that accept alternative formats or binary transfer procedures. A common example is Device Firmware Upgrade (DFU), which is a mechanism to deploy firmware on targets through a USB. The reference implementation on the host side is dfu-util, which is a free software tool.

Each tool, either generic or specific, tends to meet the same goal of communicating with the device and providing an interface for debugging the code, although often exposing a different interface toward the development tools.

Most IDEs provided by manufacturers to work with a specific family of microcontrollers incorporate in the IDE their own tools or third-party applications to access flash mapping and control the execution on the target. While, on one hand, they promise to hide the unnecessary complexity of the operation and provide firmware upload in one click, on the other, they generally do not provide a convenient interface for programming multiple targets at the same time, or at least efficiently, when it comes to production in large batches requiring to upload of the initial factory firmware.

Knowing the mechanisms and procedures from a command-line interface allows for understanding what is happening behind the scenes every time a new firmware is uploaded to the target, and anticipating the issues that would impact the life cycle during this phase.

The GDB session

Regardless of the programmer’s accuracy or the complexity of the project we are working on, most of the development time will be spent trying to understand what our software does, or most likely, what has gone wrong and why the software is not behaving as we would expect when the code was written for the first time. The debugger is the most powerful tool in our toolchain, allowing us to communicate directly with the CPU, place breakpoints, control the execution flow instruction by instruction, and check the values of CPU registers, local variables, and memory areas. Good knowledge of the debugger means less time spent trying to figure out what is going on, and a more effective hunt for bugs and defects.

The arm-none-eabi toolchain includes a GDB capable of interpreting the memory and the register layout of the remote target and can be accessed with the same interfaces as the host GDB, provided that its backend can communicate with the embedded platform, using OpenOCD or a similar host tool providing communication with the target through the GDB server protocol. As previously described, OpenOCD can be configured to provide a GDB server interface, which in the proposed configuration is on port 3333.

After starting arm-none-eabi-gdb, we may connect to the running tool using the GDB target command. Connecting to the GDB server while OpenOCD is running can be done using the target command:

> target remote localhost:3333

All GDB commands can be abbreviated, so the command often becomes the following:

> tar rem :3333

Once connected, the target would typically stop the execution, allowing GDB to retrieve the information about the instruction that is currently being executed, the stack trace, and the values of the CPU registers.

From this moment on, the debugger interface can be used normally to step through the code, place breakpoints and watchpoints, and inspect and alter CPU registers and writable memory areas at runtime.

GDB can be used entirely from its command-line interface, using shortcuts and commands to start and stop the execution, and access memory and registers.

The following reference table enumerates a few of the GDB commands available in a debug session and provides a quick explanation of their usage:

Table 2.5 – A few commonly used GDB commands
Table 2.5 – A few commonly used GDB commands

Table 2.5 – A few commonly used GDB commands

GDB is a very powerful and complete debugger, and the commands that have been shown in this section are a small portion of its actual potential. We advise you to discover the other features offered by GDB by going through its manual in order to find the set of commands that best fit your needs.

IDEs often offer a separate graphic mode to deal with debugging sessions, which is integrated with the editor and allows you to set breakpoints, watch variables, and explore the content of memory areas while the system is running in debug mode.

Validation

Debugging alone, or even simple output analysis is often not enough when verifying system behavior and identifying issues and unwanted effects in the code. Different approaches may be taken to validate the implementation of single components, as well as the behavior of the entire system under different conditions. While, in some cases, the results can be directly measurable from the host machine, in more specific contexts, it is often difficult to reproduce the exact scenario or to acquire the necessary information from the system output.

External tools may come in handy, especially in the analysis of communication interfaces and network devices in a more complex, distributed system. In other cases, single modules can be tested off-target using simulated or emulated environments to run smaller portions of the code base.

Different tests, validation strategies, and tools are considered in this section to provide solutions for any scenario.

Functional tests

Writing test cases before writing the code is generally considered an optimal practice in modern programming. Writing tests first not only speeds up the development phases but also improves the structure of the workflow. By setting clear and measurable goals from the beginning, it is harder to introduce conceptual defects in the design of the single components, and it also forces a clearer separation among the modules. More specifically, an embedded developer has less possibility to verify the correct behavior of the system through direct interaction; thus test-driven development (TDD) is the preferred approach for the verification of single components as well as the functional behavior of the entire system, as long as the expected results can be directly measurable from the host system.

However, it must be considered that testing often introduces dependencies on specific hardware, and sometimes the output of an embedded system can only be validated through specific hardware tools or in a very unique and peculiar usage scenario. In all these cases, the usual TDD paradigm is less applicable, and the project can instead benefit from a modular design to give the possibility to test as many components as possible in a synthetic environment, such as emulators or unit test platforms.

Writing tests often involves programming the host so that it can retrieve information about the running target while the embedded software is executing or alongside an ongoing debugging session while the target executes in between breakpoints. The target can be configured to provide immediate output through a communication interface, such as a UART-based serial port, which can, in turn, be parsed by the host. It is usually more convenient to write test tools on the host using a higher-level interpreted programming language to better organize the test cases and easily integrate the parsing of test results using regular expressions. Python, Perl, Ruby, and other languages with similar characteristics, are often a good fit for this purpose, also thanks to the availability of libraries and components designed for collecting and analyzing test results and interacting with continuous integration engines. A good organization of the test and verification infrastructure contributes more than everything else to the stability of the project because regressions can be detected at the right time only if all the existing tests are repeated at every modification. Constantly running all the test cases while the development is ongoing not only improves the efficiency of detecting undesired effects as early as possible but helps keep the development goals visible at all times by directly measuring the number of failures and makes the refactoring of components more affordable at any stage in the project lifetime.

Efficiency is the key because embedded programming is an iterative process with several steps being repeated over and over, and the approach required from the developers is much more predictive than reactive.

Hardware tools

If there is a tool that is absolutely indispensable in assisting embedded software developers, it is the logic analyzer. By scoping the input and output signals involving the microcontroller, it is possible to detect the electrical behavior of the signals, their timing, and even the digital encoding of the single bits in the interface protocols. Most logic analyzers can identify and decode sequences of symbols by sensing the voltage of the wires, which is often the most effective way to verify that protocols are correctly implemented and compliant with the contracts to communicate with peripherals and network endpoints. While historically available only as standalone dedicated computers, a logic analyzer is often available in other forms, such as electronic instruments that can be connected to the host machine using USB or Ethernet interfaces, and use PC-based software to capture and decode the signals. The result of this process is a complete discrete analysis of the signals involved, which are sampled at a constant rate and then visualized on a screen.

While a similar task can be performed by oscilloscopes, they are often more complex to configure than logic analyzers when dealing with discrete signals. Nevertheless, an oscilloscope is the best tool for the analysis of analog signals, such as analog audio and communication among radio transceivers. Depending on the task, it might be better to use one or the other, but in general, the biggest advantage of a logic analyzer is that it provides better insight into discrete signals. Mixed-signal logic analyzers are often a good compromise between the flexibility of an oscilloscope, with the simplicity and insights of discrete signal-logic analysis.

Oscilloscopes and logic analyzers are often used to capture the activity of signals in a specific time window, which might be challenging to synchronize with the running software. Instead of capturing those signals continuously, the beginning of the capture can be synchronized with a physical event, such as a digital signal changing its value for the first time or an analog signal crossing a predefined threshold. This is done by configuring the instrument to initiate the capture using a trigger, which guarantees that the information captured only contains a time slice that is interesting for the ongoing diagnostic.

Testing off-target

Another efficient way to speed up the development is by limiting the interaction, as much as possible, with the actual target. This is, of course, not always possible, especially when developing device drivers that need to be tested on actual hardware, but tools and methodologies to partially test the software directly on the development machine exist.

Portions of code that are not CPU-specific can be compiled for the host machine architecture and run directly, as long as their surroundings are properly abstracted to simulate the real environment. The software to test can be as small as a single function, and in this case, a unit test can be written specifically for the development architecture.

Unit tests are, in general, small applications that verify the behavior of a single component by feeding them with well-known input and verifying its output. Several tools are available on a Linux system to assist in writing unit tests. The check library provides an interface for defining unit tests by writing a few preprocessor macros. The result is small self-contained applications that can be run every time the code is changed, directly on the host machine. Those components of the system that the function under test depends on are abstracted using mocks. For example, the following code detects and discards a specific escape sequence, Esc + C, from the input from a serial line interface, reading from the serial line until the character is returned:

int serial_parser(char *buffer, uint32_t max_len)
{
  int pos = 0;
  while (pos < max_len) {
    buffer[pos] = read_from_serial();
    if (buffer[pos] == (char)0)
      break;
    if (buffer[pos] == ESC) {
       buffer[++pos] = read_from_serial();
       if (buffer[pos] == 'c')
         pos = pos - 1;
         continue;
    }
    pos++;
  }
  return pos;
}

A set of unit tests to verify this function using a check test suite may look like the following:

START_TEST(test_plain) {
  const char test0[] = "hello world!";
  char buffer[40];
  set_mock_buffer(test0);
  fail_if(serial_parser(buffer, 40) != strlen(test0));
  fail_if(strcmp(test0,buffer) != 0);
}
END_TEST

Each test case can be contained in its START_TEST()/END_TEST block and provide a different initial configuration:

START_TEST(test_escape) {
  const char test0[] = "hello world!";
  const char test1[] = "hello 33cworld!";
  char buffer[40];
  set_mock_buffer(test1);
  fail_if(serial_parser(buffer, 40) != strlen(test0));
  fail_if(strcmp(test0,buffer) != 0);
}
END_TEST
START_TEST(test_other) {
  const char test2[] = "hello 33dworld!";
  char buffer[40];
  set_mock_buffer(test2);
  fail_if(serial_parser(buffer, 40) != strlen(test2));
  fail_if(strcmp(test2,buffer) != 0);
}
END_TEST

This first test_plain test ensures that a string with no escape characters is parsed correctly. The second test ensures that the escape sequence is skipped, and the third one verifies that a similar escape string is left untouched by the output buffer.

Serial communication is simulated using a mock function that replaces the original serial_read functionality provided by the driver when running the code on the target. This is a simple mock that feeds the parser with a constant buffer that can be reinitialized using the set_serial_buffer helper function. The mock code looks like this:

static int serial_pos = 0;
static char serial_buffer[40];
char read_from_serial(void) {
  return serial_buffer[serial_pos++];
}
void set_mock_buffer(const char *buf)
{
  serial_pos = 0;
  strncpy(serial_buffer, buf, 20);
}

Unit tests are very useful to improve the quality of the code, but of course, achieving high code coverage consumes a large amount of time and resources in the economy of the project. Functional tests can also be run directly in the development environment by grouping functions into self-contained modules and implementing simulators that are slightly more complex than mocks for specific test cases. In the example of the serial parser, it would be possible to test the entire application logic on top of a different serial driver on the host machine, which is also able to simulate an entire conversation over the serial line, and interact with other components in the system, such as virtual terminals and other applications generating input sequences.

While covering a larger portion of the code within a single test case, the complexity of the simulated environment increases, and so does the amount of work required to reproduce the surroundings of the embedded system on the host machine. Nevertheless, it is good practice, especially when they could be used as verification tools throughout the whole development cycle and even integrated into the automated test process.

Sometimes, implementing a simulator allows for a much more complete set of tests, or it might be the only viable option. Think, for example, about those embedded systems using a GPS receiver for positioning: testing the application logic with negative latitude values would be impossible while sitting in the northern hemisphere, so writing a simulator that imitates the data coming from such a receiver is the quickest way to verify that our final device will not stop working across the equator.

Emulators

Another valid approach to running the code on the development machine, which is much less invasive for our code base and loosens the specific portability requirements, is emulating the whole platform on the host PC. An emulator is a computer program that can replicate the functionality of an entire system, including its core CPU, memory, and a set of peripherals. Some of the modern virtualization hypervisors for PCs are derived from QEMU, a free software emulator capable of virtualizing entire systems, even with a different architecture from those of the machine where it runs. Since it contains the full implementation of the instruction set of many different targets, QEMU can run the firmware image, which had been compiled for our target, in a process on top of the development machine’s operating system. One of the supported targets that can run ARM Cortex-M3 microcode is lm3s6965evb, an old Cortex-M-based microcontroller, no longer recommended for new designs by the manufacturer, that is fully supported by QEMU.

Once a binary image has been created using lm3s6965evb as a target, and properly converted to raw binary format using objcopy, a fully emulated system can be run by invoking QEMU as follows:

$ qemu-system-arm -M lm3s6965evb --kernel image.bin

The --kernel option instructs the emulator to run the image at startup, and while it might sound misnamed, it is called kernel because QEMU is widely used to emulate headless Linux systems on other synthetic targets. Similarly, a convenient debugging session can be started by using QEMU’s built-in GDB server through the -gdb option, which can also halt the system until our GDB client is connected to it:

$ qemu-system-arm -M lm3s6965evb --kernel image.bin -nographic -S -gdb tcp::3333

In the same way, as with the real target, we can connect arm-none-eabi-gdb to TCP port 3333 on localhost and start debugging the software image exactly as it was running on the actual platform.

The limit of the emulation approach is that QEMU can only be used to debug generic features that do not involve interaction with actual modern hardware. Nevertheless, running QEMU with a Cortex-M3 target can be a quick way to learn about generic Cortex-M features, such as memory management, system interrupt handling, and processor modes, because many features of the Cortex-M CPU are accurately emulated.

More accurate emulation of microcontroller systems can be achieved using Renode (https://renode.io). Renode is an open source, configurable emulator for many different microcontrollers and CPU-based embedded systems. The emulation includes peripherals, sensors, LEDs, and even wireless and wired interfaces to interconnect multiple emulated systems and the host network.

Renode is a desktop application with a command-line console. A single configuration file must be provided from the command-line invocation, with several platforms and development board configurations provided under the /scripts directory. This means that once installed, the emulator for the STM32F4 discovery board can be started by invoking the following command:

$ renode /opt/renode/scripts/single-node/stm32f4_discovery.resc

This command will load the demo firmware on an emulated STM32F4 target flash memory and redirect the I/O of one of the emulated UART serial ports to a console in a new window. To start the demo, type start in the Renode console.

The example script comes with a demo firmware image running Contiki OS. The firmware image is loaded by the script via the Renode command:

sysbus LoadELF $bin

Where $bin is a variable pointing to the path (or the URL) of the firmware ELF file to load on the emulated flash memory. This option, as well as the UART analyzer port and other specific commands to execute when the emulator is started, can be easily changed by customizing the script file.

Renode integrates a GDB server that can be spawned from the Renode console or the startup script before starting the emulation, for example, using the following:

machine StartGdbServer 3333

In this case, 3333 is the TCP port the GDB server will be listening to, as in the other cases with QEMU and a debugger on the physical target.

Unlike QEMU, which is a very generic emulator, Renode is a project created with the purpose of assisting embedded developers throughout the entire life cycle. The possibility to emulate different complete platforms, creating mocks for sensors on several architectures, including RISC-V, makes it a unique tool to automate testing on multiple targets quickly or test on systems even when the actual hardware is not available.

Last but not least, thanks to its own scripting language, Renode is perfectly integrated with test automation systems, where the execution of the emulated target can be started, stopped, and resumed, and the configuration of all devices and peripherals altered while the test is running.

The approaches proposed for the definition of test strategies take into account different scenarios. The idea has been to introduce a range of possible solutions for software validation, from lab equipment to tests off-target in simulated and emulated environments, for the developer to choose from in a specific scenario.

Summary

This chapter introduced the tools for working on the development of embedded systems. A hands-on approach has been proposed to get you up and running with the toolchain and the utilities required to communicate with the hardware platform. Using appropriate tools can make embedded development easier and shorten workflow iterations.

In the next chapter, we provide indications for workflow organization when working with larger teams. Based on real-life experiences, we propose solutions for splitting and organizing tasks, executing tests, iterating throughout the phases of design, and the definition and implementation of an embedded project.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset