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:

basePrj/
├── CMakeLists.txt
├── libs
│   └── hello
│       ├── CMakeLists.txt
│       ├── inc
│       │   └── hello.h
│       └── src
│           └── hello.c
└── src
    └── main.c

And here is the top level CMake file:

cmake_minimum_required(VERSION 3.11.1)

project(basePrj)

add_subdirectory(libs/hello)

set(SOURCES src/main.c)

add_executable(${PROJECT_NAME} ${SOURCES})
target_link_libraries(${PROJECT_NAME} hello)

 

To build and run the project, run these commands:

$ cd basePrj
$ mkdir build
$ cd build
$ cmake ..
$ make
$ ./basePrj
Hello World!

 

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:

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:

set(CMAKE_EXE_LINKER_FLAGS "--entry ResetISR -Wl,-T\"../tm4c123gh6pm.lds\"")

 

Bringing all of these CMake commands results in this top level CMakeLists.txt.

cmake_minimum_required(VERSION 3.11.1)

project(targetPrj)

set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_CROSSCOMPILING 1)

set(GCC_PATH
/tri/ti/css901/ccs/tools/compiler/gcc-arm-none-eabi-7-2017-q4-major/bin)

set(CMAKE_C_COMPILER ${GCC_PATH}/arm-none-eabi-gcc CACHE PATH "" FORCE)

set(SOURCES src/main.c src/tm4c123gh6pm_startup_ccs_gcc.c)

set(CMAKE_C_FLAGS "-mcpu=cortex-m4 -march=armv7e-m -mthumb -mfloat-abi=hard
-mfpu=fpv4-sp-d16 -DPART_TM4C123GH6PM -ffunction-sections -fdata-sections -Wall
-specs=\"nosys.specs\"")

set(CMAKE_C_FLAGS_RELEASE "-Os")
set(CMAKE_C_FLAGS_DEBUG "-Og -g -gdwarf-3 -gstrict-dwarf")
set(CMAKE_EXE_LINKER_FLAGS "--entry ResetISR -Wl,-T\"../tm4c123gh6pm.lds\"")

add_subdirectory(libs/hello)

add_executable(${PROJECT_NAME}.elf ${SOURCES}) 
target_link_libraries(${PROJECT_NAME}.elf hello)

 

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.

set(CMAKE_C_OBJCOPY ${GCC_PATH}/arm-none-eabi-objcopy CACHE PATH "" FORCE)

add_custom_target(${PROJECT_NAME}.bin ALL DEPENDS ${PROJECT_NAME}.elf)
add_custom_command(TARGET ${PROJECT_NAME}.bin
    COMMAND ${CMAKE_C_OBJCOPY} ARGS -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin)

 

Here's some notes on the added 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.

add_custom_target(flash DEPENDS ${PROJECT_NAME}.bin)
add_custom_command(TARGET flash
    USES_TERMINAL
    COMMAND JLinkExe -CommanderScript ../flash.jlink)

 

Here are some notes on the added command:

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.

Debugging

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 JLinkGDBServer and arm-none-eabi-gdb. These tools are require top be run in two separate terminals.

Here are the steps to debug our target:

These steps can be captured in a custom CMake command using xterm to run both tools in their own terminals:

set(CMAKE_C_GDB ${GCC_PATH}/arm-none-eabi-gdb CACHE PATH "" FORCE)

add_custom_target(gdb DEPENDS ${PROJECT_NAME}.elf)
add_custom_command(TARGET gdb
    COMMAND xterm -e JLinkGDBServer -if JTAG -device TM4C123GH6PM &
    COMMAND xterm -e ${CMAKE_C_GDB} ${PROJECT_NAME}.elf -x ../gdbInit &)

 

Note: The -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.

Now running 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.

Visit Our Site
Exit mobile version