Optimize Your Workflow By Moving Embedded Projects to CMake
As software developers, we get very comfortable with our workflows. These workflows can be personal preferences or developed from different project requirements. Changes to these workflows can be frustrating and end up distracting from the project at hand.
Most microcontroller ecosystems utilize their own IDE and project files in order to build projects. This can be a convenient way to start with a microcontroller, but often introduces unwanted workflow changes. Unfamiliar text editors, obfuscated build processes, and unknown compilers are some common trade-offs when it comes to using a vendor's IDE and build system. Open-source build tools can replace the embedded target's IDE allowing a developer to gain a more robust, familiar, and repeatable text based build system across a team.
CMake is well-suited for cross-platform embedded projects because it allows for custom commands designed specifically for your embedded target while maintaining its native build tool management method.
The microcontroller I will be targeting in these examples is the TI TM4C123G, which uses an ARM Cortex-M4F. Results may vary across microcontroller manufacturers but the same ideas should apply with some tinkering.
All example projects can be found at https://github.com/dornerworks/embeddedCmakeExamples.
Basic CMake Project
Before jumping straight into the embedded projects it would be useful to briefly cover some CMake basics. Included in the projects repo is
basePrj. This is nothing more than a hello world library and main file built with CMake. This is the project that we are going to build on for our embedded target.
Here is the project file layout:
And here is the top level CMake file:
To build and run the project, run these commands:
Embedded CMake Project
Each microcontroller will have different required supporting files needed to execute code on target. Different targets will also require specific compilers and flags. I have found that the easiest way to find all of these requirements is to create a test project within the microcontroller's IDE.
Back to our example, we can create a new project in TI's Code Composer Studio and select our desired gcc-based compiler. This project generates
tm4c123gh6pm_startup_ccs_gcc.c which contains required startup code for the microcontroller and
tm4c123gh6pm.lds which is a linker script for gcc. These files will be used later by CMake. Now we need to build the project in debug and release modes noting the build and linking output.
Here we have the IDE debug mode output of the test program.
Now we can use this output to make our CMake commands:
- First we need to tell CMake that we are cross compiling for ARM. That is done with these commands:
- The blue section shows where and what compiler the IDE was using. This information can be stored in variables shown in these commands:
GCC_PATHwill need to change per system so a relative path with a sourceable script may work better for a multiuser code base.
- The green sections show the flags needed to compile code for the target. This is captured in this command:
- The orange section shows the flags specific to debug or release builds. This output was from a debug build (the release only had a
-0s). This information is stored in these commands:
- The yellow section in the compiling step shows Makefile dependency rules which are unneeded because CMake will handle it behind the scenes.
- The grey section in the linker command shows the linker options which point to the linker script we already grabbed from the IDE project. This information is captured in this variable:
All of the build and linking output from the IDE has now been converted into CMake commands. However the ELF file these commands produce results in the embedded target beginning execution at garbage addresses when using a debugger. This is because sometimes the IDE makes assumptions about how debuggers or other tools will be used with the resulting build output. In this example, we need to specify an entry point at
ResetISR for our linker so the target knows where to begin execution. This is where you may need to do some tinkering with you're target's build system. Here is our updated linker variable:
Bringing all of these CMake commands results in this top level CMakeLists.txt.
With these changes we can now build and link an ELF file for our embedded target.
Objcopy with CMake
Before we can flash our microcontroller we will need to use objcopy to convert our ELF into a binary file. We could do that as a manual step after building but why not bake it into a CMake command? The following lines automatically create a binary file after building the ELF.
Here's some notes on the added command:
- The objcopy tool location is stored in
DEPENDSspecifies the dependency of the target to the ELF file.
ALLspecifies that this target is to be run automatically with
- The custom command points to the target and specifies the command.
Now, each build automatically creates a binary file for our microcontroller.
Flash with CMake
Another useful CMake trick is to make a flash command to automatically flash our target for us. This step will largely rely on your board's desired flashing method. Luckily my setup uses JTAG for flashing and debugging. I am using a SEGGER JLink so I can use the
JLinkExe command and a JLink commander script to flash the board. My commander script,
flash.jlink, will need to be modified to work with other microcontrollers using JLink.
Here are some notes on the added command:
- The custom target again added a dependency, but does not run automatically for
- The custom command specifies
USES_TERMINALso we can see the output from the
Now, after having built the binary, running
make flash will automatically flash the target.
Note: At this point in the example you won't see any output from the board because there are no communication protocols setup in this simple example. Debugging will show that the code is executing.
Our last custom CMake command for our embedded target will automatically launch a debugging session on our target. Before jumping straight into CMake commands it is a good idea to understand how to debug manually. Debugging our target is again done with the SEGGER JLink, this time using
arm-none-eabi-gdb. These tools are require top be run in two separate terminals.
Here are the steps to debug our target:
- Create a debug build directory and run CMake in its debug configuration.
- In the first terminal run the JLink GDB server
- In the second terminal run GDB from the same
GCC_PATHfrom the CMakeLists.txt, targeting the ELF file.
- In GDB connect to the target, reset the microcontroller and load the ELF.
- Continue debugging like normal from here
These steps can be captured in a custom CMake command using
xterm to run both tools in their own terminals:
-x ../gdbInit option was added to GDB to automatically run the
target remote :2331,
mon reset, and
load commands on boot. The
mon reset and
load commands can be run again from inside GDB to reset the target.
make gdb will open a debugging session connected to our target.
Hopefully this blog has given you some ideas on how CMake can be used to transition embedded projects away from a target's IDE to support your team's desired workflows. DornerWorks is focused on working alongside our customers to maintain their desired workflows and quality standards. Whatever tools our customers use or how they use them, our flexibility and knowledge of embedded systems helps them efficiently create and improve their projects throughout the entire project life cycle without needing to abandon tried and true development methods.