Initial porting

SDK files

The starting point for the port is the SDK library, currently in the nordic folder. The first part is to convert the SDK files and defines into something CMake-compatible. This includes splitting out the main.c-file from the pure SDK-related files.

As the Nordic SDK is already a blob of SDK_SRC and INC_FOLDERS, these lists of files can be converted into CMake lists using the set()-function. Note that CMake uses curly-brackets instead of parenthesis when referencing variables (in this case $(SDK_ROOT)). We’ll define ${SDK_ROOT} later in our top-level CMakeLists.txt.

Converting the sources/includes into variables should now look something like this:

set(SRC_FILES
    ${SDK_ROOT}/modules/nrfx/mdk/gcc_startup_nrf52840.S
    ${SDK_ROOT}/components/libraries/log/src/nrf_log_backend_rtt.c
    ...
)

Note that the first file is an assembly file. This should be first to make sure it’s compiled and placed first in the resulting object-file for the SDK.

Next we’ll declare the CMake library object with the add_library()-function: add_library(${LIB_NAME} OBJECT ${SRC_FILES})

Note that the library is declared as a OBJECT library. The CMake documentation has more info on what it does, but in general the effect is that it doesn’t compile the specified files into a library, but makes them available for other libraries (and executables) to reference. This prevents unreferenced functions from being optimised away. (More details on this in Static vs Object libraries)

After the sources, the header files are linked in with target_include_directories(${LIB_NAME} PUBLIC ${INC_FOLDERS}). The header files are set as PUBLIC to make them available to any modules that require them.

SDK libraries and defines

The Makefile also contains define and linked libraries, in addition to the source files and includes.

These can be set in CMake with the following functions:

  • target_compile_definitions() for defines
  • target_link_options() for linker flags
  • target_link_libraries() for linked libraries via flags (e.g. -lc for libc)

The Makefile in the Blinky example isn’t the only Makefile used during compilation. The Makefile also references a Makefile.common for general-purpose functionality. This file contains an option (PASS_LINKER_INPUT_VIA_FILE) for combining the linked libraries in a .out-file. This is on by default, and setting it to 0 results in an identical binary file at the end of compilation and linking, so I don’t think it does anything. I also haven’t found anything on Google regarding dumping the linked libraries to a file, so until further notice we can ignore that option.

The defines and linker flags should look something like this once converted to CMake:

target_compile_definitions(${LIB_NAME} PUBLIC
    APP_TIMER_V2
    APP_TIMER_V2_RTC1_ENABLED
    BOARD_PCA10056
    CONFIG_GPIO_AS_PINRESET
    FLOAT_ABI_HARD
    NRF52840_XXAA
    NRF_SD_BLE_API_VERSION=7
    S140
    SOFTDEVICE_PRESENT
    __HEAP_SIZE=8192
    __STACK_SIZE=8192
)

target_compile_options(${LIB_NAME} PUBLIC
    -O3
    -g3
    -mcpu=cortex-m4
    -mthumb -mabi=aapcs
    -Wall -Werror -Wno-array-bounds
    -mfloat-abi=hard -mfpu=fpv4-sp-d16
    # keep every function in a separate section, this allows linker to discard unused ones
    -ffunction-sections -fdata-sections -fno-strict-aliasing
    -fno-builtin -fshort-enums
)

target_link_options(${LIB_NAME} PUBLIC
    -O3
    -g3
    -mthumb -mabi=aapcs
    -L${SDK_ROOT}/modules/nrfx/mdk
    -T${CMAKE_CURRENT_SOURCE_DIR}/ld/ble_app_blinky_gcc_nrf52.ld
    -mcpu=cortex-m4
    -mfloat-abi=hard -mfpu=fpv4-sp-d16
    # let linker dump unused sections
    -Wl,--gc-sections
    # use newlib in nano version
    --specs=nano.specs
    -Wl,-z,max-page-size=4096 # For correct flash page alignment
)

target_link_libraries(${LIB_NAME} PUBLIC -lc -lnosys -lm)

Now that the SDK has been split out, we can move to the application in main.c.

Note: max-page-size linker flag

On versions 2.38 and 2.39 of binutils, which is the source for ld amongst other tools used for compiling the image, the max-page-size linker-flag is set to a page size of 64K for ARM32 targets. This affects the nRF52 binary as it will set the flash page size used for linked application to 64K, resulting in a bloated binary as all new, page-aligned flash sections will be aligned to 64K instead of 4K, which is the correct flash page size for the nRF52.

On versions 2.40 and onwards, and on preceding versions (?), the flag is defaulted to 4096.

For this blinky example, the alignment doesn’t affect the size; however, for larger projects, this will bloat the binary.

Defining the application

The application CMakeLists.txt structure is similar to that of our nordic library, with a name-variable and a target definition.

set(APP_NAME blinky)

add_subdirectory(nordic)

add_executable(${APP_NAME} main.c)
target_link_libraries(${APP_NAME} PRIVATE nordic)

As this is going to be an executable file for our nRF52840DK, it can be defined as such with add_executable(). This instructs CMake to compile an executable application (in this case a .bin-file) and link in the specified libraries. In theory, the massive SOURCES blob and headers could be compiled in one executable target like the SDK Makefile does.

CMake encourages creating an executable and linking in any specified libraries. Doing this, the resulting CMakeLists.txt for our main application will look something like this:

In this case the libraries are linked privately with our application. Not that it makes a lot of difference as this is our main target, but in theory it prevents future executables from using the linked libraries during building.

Now that our executable file is compiled, it’s time to move up to the final CMakeLists.txt in our project root.

Project root

Using the main CMakeLists.txt from the previous blog post, the project will start compiling but will generate some interesting errors. It can be seen as an encouraging sign for our port to CMake that it compiles at all, given the intricasies of the SDK example’s Makefile, but the devil’s in the details (or in some cases not even there).

First, we get several compilers for missing symbols, the first (and most alarming) of which is:

Missing Reset_Handler symbol

The error message I get is:

warning: cannot find entry symbol Reset_Handler; defaulting to 00027000

The assembly file is included in the SRC_FILES-variable in our nordic library, so why doesn’t it compile? The reason lies in CMake and the project function. In addition to specifying the project name, it’s also required (and encouraged) to specify the language(s) used in the project. In addition, the CMake/project version is encouraged, as it can be used in submodules to manage dependencies or alternate versions of files.

As our project features C and assembly files, we have to specify this in the project variable. As CMake also tracks file changes, it will automatically reconfigure the project so that we don’t have to delete the build-folder.

project(
    nrf5_sdk 
    VERSION 1.0
    LANGUAGES C ASM
)

This ensures that CMake also compiles the assembly file. Re-running CMake and Make results in the error going away! There are still some warnings left, though.

Library warnings

With the arm-none-eabi-gcc version I have installed, 12.2.1, I get a bunch of warnings along the lines of:

warning: _close is not implemented and will always fail from libc_nano.a

This is nothing to worry about, as the calls are all OS-related calls stripped away in the nano libc-implementation used in the compilation.

Size comparisons

Now that the CMake version is compiling without any serious errors, a quick way to verify that the CMake version of the blinky library is the same as the Makefile one from the SDK is to compare the size of the binaries.

Comparing the binary files shows that the Makefile-specified binary is 28092 bytes, while the CMake binary is 29084 bytes.

Where have the 8 bytes gone?

Comparing the .map-files shows a different order of function symbols and data. This is a result of the CMake vs Make approach, where the Makefile jams every file into one massive object when compiling, while the CMake version specifies a library for the SDK and an executable for the _main.c_file. This means that the main.c-file will be compiled first, then the library will be linked to it. In the Makefile, everything is in one blob, so the main.c-file is placed after the preceding files in the SDK sources. This can be verified by placing the main.c-file into the nordic library in the CMake version, and declaring that as an executable. This results in the same order of the functions and data sections, and gives the same file size. The order of these function shouldn’t have anything to say, really. I’ve not been able to detect any differences in the flashed applications.

This doesn’t explain where the 8 byte have gone, though. Going back to the .map-files and inspecting them step by step, one difference shows up when comparing the files closely: The fill-sections.

The fill-sections differ because the order of the functions differ. This means that the compiler is able to place the functions in different 32-bit sections of the flash memory. This is illustrated in the following image:

Missing observer functions

The Makefile has a 14 byte fill after the data rodata.drv_rtc_init.str1.4 in drv_rtc_init.c.o, while the CMake version has a 2 byte fill. The CMake version has a 4 byte larger fill in .text.app_timer_init, which results in the total difference of 8 bytes seen earlier.

Based on this the comparison of the .map-files, the following observations can be drawn:

  • Different order of main.c and SDK files in CMake affects order of functions compiled binary.
    • The ISR vector, _stack_init, _mainCRTStartup, and _start symbols are all in the same location
    • Difference seems to be order of handlers. In CMake, the handlers from main.c are first, and in the Makefile the SDK ones are.
  • Interestingly, the order from main.c fits better in flash in the CMake version and saves 8 bytes.
    • This will probably even out in larger projects.

Testing

Flashing the nRF52840 DK with the compiled binary gives the expected behaviour: A blinking LED and connectable BLE device. Success!

This is all for now, and should be enough to get started with using the nRF5 SDK with CMake.

Addendum: Static vs Object libraries

Defining the nordic library as a STATIC library instead of an OBJECT results in the compield .out-file being 29468 bytes. That’s 1312 bytes less than the Makefile-version, nrf52840_xxaa.out.

Inspecting the .map-files provides some clues to where the bytes have gone:

Missing observer functions

The file to the left is nrf52840_xxaa.map, and right is blinky.out.map The picture shows some missing observer-functions provided by the linker script. These are optimised out of the CMake version as there’s seemingly no reference to it in the code. The one in question here, sdh_stack_observers0 in nrf_sdh_soc.c.o, is referenced by the softdevice.

The problems is that when declaring the library as a STATIC library (the default type), the library is compiled independently, so from CMake’s perspective, there’s a whole bunch of unreferenced symbols it can optimise away. These symbols are usually referenced by other files, such as main.c, so they’re incorrectly optimised away.

The missing symbols will manifest itself as potentially difficult and rare bugs, such as missing interrupts in the event handlers. Simply changing the library type to OBJECT fixes this.