Motivation

The nRF52 is one of the most widely used BLE MCUs. It’s cheap, has good documentation and is readily available. Developing for it has been kinda messy, though, compared to other BLE MCU manufacturers such as Texas Instruments or Silicon Labs. The primary IDE for it is Segger Embedded Studio, which in it’s defence has pretty good integration with the SDK through configuration files. Segger Embedded Studio, though, is not everyone’s cup of tea, nor is the development environment setup required. I remember being annoyed by having to install four separate utilities when I just wanted to start developing: The ARM GCC toolchain, JLink, the Nordic nRF5 SDK and Segger Embedded Studio. Compared to the Texas Instruments CC-series of BLE MCUs, or even worse to Arduino, this setup seems unnecessarily complicated and is very error prone, especially for people getting started with embedded programming.

The base build system in nRF5 SDK is GNU Make. Effective and portable, but cryptic and difficult to change. The recommended development flow for the nRF5 SDK is to start with an example project and modify it as necessary for your desired application. As I’m not a huge fan of Makefiles, my preferred build system is CMake. Modern CMake can seem daunting, but once learned becomes very readable and flexible.

For this guide, I’ll be using the BLE Blinky Application. Later it’ll get extended with other peripherals such as flash-storage to highlight some of the problems caused by using CMake.

The project files are available on my GitHub.

Setup

The device used for this example is MacOS, so I’ll be installing the necessary toolchains from ARM and Nordic to be able to develop for and flash the Nordic nRF52840 DK development kit I have.

I assume CMake is already installed. I’m using version 3.24.2 from brew.

Installing the ARM GNU Toolchain

The ARM GNU Toolchain, formerly known as the GNU ARM Embedded Toolchain or “arm-none-eabi-gcc”, is required for compiling and linking programs for ARM-based embedded devices.

I’m grabbing the latest release: 12.2 from 2023-03-22 for Apple Silicon in .pkg-form. This will install the toolchain (by default) to /Applications/ArmGNUToolchain. For a custom path, download the tar-ball and extract it to your wanted location. Add the installed toolchain’s bin folder to your PATH. Verify with “arm-none-eabi-gcc –version”.

Installing nRF5 SDK and nrfjprog

Download and install the nRF5 SDK from Nordic Semiconductor. I’m installing version 17.1.0. The SDK also includes the S140 Softdevice which is compatible with the nRF52840 DK.

Extract the nRF5 SDK- and Softdevice folders to a temporary location.

To flash the development kit, nrfjprog is required. This is available in Nordics nRF Command Line Tools. Download and install the tools as instructed. The default install location is yet again /Applications/, though in this case /Applications/Nordic Semiconductor and /Applications/SEGGER. Add the Nordid Semiconductor/bin and SEGGER/JLink to your PATH.

Project structure

The easiest way I’ve found for using the nRF5 SDK in CMake is to include the whole SDK in the project folder. This ensures that everything related to building the project is contained in one location, as CMake will need to have access to all the SDK’s files when building. Alternatively, symbolic links can be used to link to a common SDK location. For most cases I’ll include the SDK as a git submodule and host it myself.

The project structure is based on Modern CMake with some alterations to make it a bit more suitable for embedded applications. I highly recommend reading the Modern CMake guide, as it highlights some of the benefits of using modern CMake for projects.

In general, the project structure is based on the structure from Modern CMake:

- project
  - .gitignore
  - README.md
  - LICENSE.md
  - CMakeLists.txt
  - apps
    - CMakeLists.txt
    - blinky
      - src
        - CMakeLists.txt
        - nordic
          - CMakeLists.txt
          - include
            - sdk_config.h
      - CMakeLists.txt
      - main.c
  - boards
    - CMakeLists.txt
    - nrf52840dk.h
  - cmake
    - generate_unittest.cmake
  - docs
    - public_doc.md
  - extern
    - CMakeLists.txt
    - unity
    - nrf5_sdk
  - include
    - CMakeLists.txt
    - fft
      - fft.h
      - fft_consts.h
  - scripts
    - helper.py
  - libs
    - CMakeLists.txt
    - fft
      - CMakeLists.txt
      - src
        - fft.c
      - include
        - fft_priv.h
      - tests
        - test_fft.c
  - tests
    - CMakeLists.txt
    - integration_test.c

Embedded applications tend to have multiple hardware targets (PCBs and/or MCUs) which is why there is a boards directory. In many cases, the different PCBs also have different applications that target share a lot of functionality. An argument can be made to split the applications into separate repositories, but then sharing the code between the applications becomes the challenge. Not sharing the code is also a viable alternative, as this will simplify the project structure and reduce the change of unintended side-effects in other applications when changing a shared file.

The main thing of note in the example structure is that the nRF5 SDK is in the extern folder, but that the sdk_config.h file is under the blinky application. This is located here as most applications have different SDK requirements. The nRF5 SDK is primarily configured using the sdk_config.h file, but it also uses defines for some parameters, which in this case is located in nordic/CMakeLists.txt.

Initial Structure

The initial Blinky example doesn’t need all of this structure yet. For now, we’ll make do with just having it compile with CMake by wrapping the existing Makefile.

The initial project structure should now look like this:

- project
  - .gitignore
  - CMakeLists.txt
  - apps
    - CMakeLists.txt
    - blinky
      - CMakeLists.txt
      - pca10059
        - s140
          - armgcc
            - _build
            - Makefile
            - ble_app_blinky_gcc_nrf52.ld
      - main.c
  - build
  - extern
    - nrf5_sdk

Configuring the project CMake

By default, CMake will use the system’s C compiler. This is initialised implicitly with the project() command in CMake. The compiler can be specified in several ways as described by CMake. Despite not being recommended, I find alternative 3. (using set()) to be the cleanest as it avoids extra arguments when calling cmake.

In addition to setting the compiler, project() also performs a quick test compilation to verify that the compiler works as expected. This won’t work with arm-none-eabi-gcc as it will generate a “The C compiler /Applications/ArmGNUToolchain/12.2.mpacbti-rel1/arm-none-eabi/bin/arm-none-eabi-gcc is not able to compile a simple test program” error. For now, the simplest methdo to avoid this is by setting the CMAKE_SYSTEM_NAME to “Generic” and CMAKE_TRY_COMPILE_TARGET_TYPE to “STATIC_LIBRARY”. This will make CMake skip the test compilation.

To avoid cluttering our project with CMake-related build files, the CMAKE_BINARY_DIR is set to “build”.

The resulting project-level CMakeLists.txt now looks like this:

cmake_minimum_required(VERSION 3.24)

# Set target compiler first to overwrite the default compiler
# that's set when _project()_ is called.
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)

# Required to prevent "The C Compiler is not able to compile a simple test program"
# error due to cross-compiling.
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

# Project name
project(nrf5_sdk)

set(CMAKE_BINARY_DIR build)

add_subdirectory(apps)

Configuring the SDK

The SDK has to be configured with the location of the ARM toolchain. This is configured in extern/nrf5_sdk/components/toolchain/gcc/Makefile.posix. Set the GNU details to match your ARM GNU Toolchain location and version.

GNU_INSTALL_ROOT ?= /Applications/ArmGNUToolchain/12.2.mpacbti-rel1/arm-none-eabi/bin/
GNU_VERSION ?= 12.2
GNU_PREFIX ?= arm-none-eabi

Configuring the example

The example Makefile needs to be modified to compile correctly. The two changes required are:

  1. Setting the SDK root folder.
  2. Set correct board
  3. Suppressing compilation errors detected in newer versions of GCC.

Change no. 1 is done by pointing to the relative SDK directory in the project folder. Following the structure from “Initial Structure”, the relative folder will be ../../../../../extern/nrf5_sdk.

Change no. 2 is required to get the correct pin mapping for LEDs. The default Blinky example uses pca10059 as the board file, while I’m using the pca10056. This is fixed by simply replacing the two occurances of pca10059.

Change no. 3 is required for sucessful compilations as the nRF5 SDK contains “-Werror=array-bounds” errors. These can be safely suppressed. This is done by appending -Wno-array-bounds to the CFlags.

Wrapping the Makefile in CMake

CMake provides mechanisms for wrapping other build systems. This is provided through the ExternalProject module.

In brief, the module allows for the different CMake steps to be translated into commands.

The wrapped Makefile looks like this:

cmake_minimum_required(VERSION 3.24)
# Compile Blinky using the example's existing Makefile
include(ExternalProject)
ExternalProject_Add(
    blinky
    SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}
    CONFIGURE_HANDLED_BY_BUILD true 
    BUILD_COMMAND
        make -C ${CMAKE_CURRENT_SOURCE_DIR}/pca10059/s140/armgcc
    INSTALL_COMMAND
        mv ${CMAKE_CURRENT_SOURCE_DIR}/pca10059/s140/armgcc/_build ${CMAKE_BINARY_DIR}
)

Note that the cmake_minimum_required parameter has been set. I had to do this to avoid erroring out when building the example.

Moving the compiled binary and hex files with mv isn’t the cleanest solution, but it works for now.

Building and verifying

To build the project, run cmake . -B build followed by make -C build. The compiled hex file and related files will be located in build/_build.

To flash the development kit, first clear the device with nrfjprog -f NRF52 --recover and nrfjprog -f NRF52 --eraseall. Then, flash the softdevice with nrfjprog -f NRF52 --program extern/nrf5_sdk/components/softdevice/s140/hex/s140_nrf52_7.2.0_softdevice.hex --verify --reset, followed by the compiled program: nrfjprog -f NRF52 --program build/_build/nrf52840_xxaa.hex --verify --reset.

LED1 on the development kit should light up and the device should be advertising on Bluetooth.

Next steps…

Now that the default project has been configured, the next steps are to convert the Makefile to CMake, add more libraries and application sources.