engineering

August 18, 2020   |   13min read

nRF5 CMake Tutorial—Building nRF52 Projects Made Easy

In this tutorial, I’m going to introduce you to our open-source solution for creating, building and maintaining nRF52 projects with CMake—the nRF5 CMake. If you have developed firmware for nRF52 chips before, you may be surprised how easy it is to set up a project without using an IDE. If you are a beginner, the nRF5 CMake is yet another viable option to get you started with nRF52 development and make the start of this journey much easier. By the end of this tutorial, you should be able to kick off your new nRF52 project with nRF5 CMake (or migrate an existing one), effortlessly pull the libraries you need to it and build it for multiple nRF52 chips with no trouble.

Introduction

As you may know, the nRF52 is the Nordic Semiconductor’s series of System on Chips (SoCs) that is widely used in BLE-enabled IoT devices available on the market today. During my career as an embedded software developer, I grew quite fond of these chips, mostly because of good documentation, an extensive SDK, and active support from both Nordic Semiconductor and the community.

However, similarly to other embedded processors and SoCs I’ve encountered, setting up a development environment for them from scratch is usually not an easy task. Fortunately, in the case of nRF52, IDEs like Keil uVision, IAR Embedded Workbench or Segger Embedded Studio are officially supported, and they will take care of this for you.

However for me, a code editor, a build system and CMake make up the core of the best development environment for any embedded or native project. Don’t get me wrong, IDEs are great, but the flexibility this setup offers is unbeatable for me. With CMake at the center, you can easily add custom steps to your build, integrate different tools and organize the project the way you want. The fact that it is also cross-platform is an important benefit.

The problem

So what’s the fuss all about, you might ask. Just install the ARM toolchain, CMake and VS Code, download the nRF5 SDK and get started, right? Well, not quite.

First of all, you have to take care of setting up compiler flags, linker script, startup file, preprocessor definitions and get them all correct in order to build a proper binary for the nRF52 chip. These parameters are usually specific to a particular nRF52 chip like nRF52810. If you decide to migrate your project to a different SoC like nRF52840, you need to go through this configuration again.

The other thing is that in an nRF52 firmware project, you’re probably going to use some of the libraries, drivers and modules available in the nRF5 SDK. Naturally, more advanced libraries in the SDK are made using the simpler ones, which may make pulling them into your project manually difficult. I remember trying to add a specific library to my project only to find that I also need to identify and add a dozen other libraries (its direct and indirect dependencies).

Enter the nRF5 CMake

Having to deal with the problems I mentioned, my colleagues and I concluded that we need a CMake-based solution for quickly setting up nRF52 firmware projects that is complete and flexible enough so that we can use it in our commercial work. That’s how the nRF5 CMake was born. It is basically a set of CMake scripts and utility scripts written in Python and Bash but for most cases you’re only going to need the CMake scripts.

The nRF5 CMake will take care of most of the configuration that is specific to your target nRF52 chip (compile flags, preprocessor definitions, startup file etc.) while still allowing you to customize it to a large extent. We also took an effort to ensure that in most cases a single CMakeLists.txt file (a script that describes your project in CMake) is sufficient to build your project for multiple nRF52 chips. That way, you can easily migrate to another nRF52 chip if you wish.

Finally, when it comes to pulling the libraries into your project from the nRF5 SDK, we already support a large number of them and also track their dependencies. By doing that, whenever you specify a library, our script automatically adds its dependencies to your project. This way, you only need to specify the libraries that you use directly.

Embedded IoT chip

nRF5 CMake tutorial

Now, I’m going to show you how to create a simple project and build it using the nRF5 CMake. Generally, when using the nRF5 CMake, you (as the user) are supposed to provide the following things:

  • Application source code
  • SDK configuration file (holds the configuration of the libraries that you are going to use)
  • Linker script (defines the memory layout of the output binary)
  • CMakeLists.txt (defines what you want CMake to build)

Since writing code for the nRF52 is not actually the subject I want to focus on, we are going to use the source code, SDK configuration file and the linker script from one of the official examples from the nRF5 SDK, the BSP Example. Our job here will be limited to writing the CMakeLists.txt script. If you have an nRF52-DK board (with the nRF52832 SoC) or an nRF52840-DK board, you will be able to load the built binary into it to see if it works. You can finish this tutorial on any major operating system—Windows, Linux or macOS.

Required tools and the SDK

Please download and install the following tools that we’re going to need:

Once you have these tools installed in your system, download the newest version of the nRF5 SDK that is supported by the nRF5 CMake. At the time of writing this tutorial, versions 15.3.0 and 16.0.0 are supported, but we will definitely be adding support for newer SDK versions as they are released. Finally, extract the SDK under an easy-accessible path.

The project structure

We need to set up a directory structure for the project. Create a new folder, let’s call it nrf5_example_bsp. Within it, create the config subfolder. As I mentioned, we’re going to use an already written source code for one of the nRF5 SDK examples. Go to the path where you’ve extracted the SDK and navigate to examples/peripheral/bsp directory. Copy the main.c source file to the main directory of your project. From the directory of the original BSP Example you should also see 2 subfolders: pca10040 and pca10056. These folder names are the symbols precisely identifying the board: nRF52-DK (with nRF52832 SoC) and nRF52840-DK, respectively. Go to the folder corresponding to the board you want to build this example for. From there, copy the following files into the nrf5_example_bsp/config directory:

  • blank/armgcc/bsp_gcc_nrf52.ld
  • blank/config/sdk_config.h

The first of these files is a linker script defining the memory layout of the output binary. The second one is the SDK configuration file that holds the configuration of the libraries that are used in this example.

In the end, your project directory should look like this:

nrf5_example_bsp
├── config
│   ├── bsp_gcc_nrf52.ld
│   └── sdk_config.h
└── main.c

Adding nRF5 CMake to the project

Go to the nRF5 CMake GitHub page and download it as a ZIP archive. Extract it and copy its cmake directory to the nrf5_example_bsp project folder. It should now look like this:

nrf5_example_bsp
├── cmake
│   ├── nrf5.cmake
│   └── ...
├── config
│   ├── bsp_gcc_nrf52.ld
│   └── sdk_config.h
└── main.c

You can also add nRF5 CMake as a Git submodule so that you can easily keep it up-to-date. However, we won’t do it this time.

Writing CMakeLists.txt

Create a CMakeLists.txt file in the main project folder. Start by specifying a minimum required CMake version and by declaring the project:

cmake_minimum_required(VERSION 3.14)
project(nrf5_example_bsp LANGUAGES C ASM)

Next, include the nrf5.cmake module so that we can use the functionality of nRF5 CMake. Just before that, you need to tell CMake where to look for this module by adding the cmake folder to the CMAKE_MODULE_PATH variable:

list(APPEND CMAKE_MODULE_PATH "cmake")
include("nrf5")

Once you’ve done that, create an executable CMake target which represents the output binary and make it an nRF5 target by calling the nrf5_target() function from nRF5 CMake:

add_exectable(nrf5_example_bsp "main.c")
nrf5_target(nrf5_example_bsp)

The BSP Example that we are trying to build uses the following libraries from the nRF5 SDK:

  • Application error for error-handling
  • Board Support Package (BSP) for handling board hardware like buttons and LEDs
  • Logger for printing log entries on the UART peripheral so you can observe what is going on in the application as it is running

You need to add them to your project by writing:

nrf5_target_link_libraries(nrf5_example_bsp PRIVATE
  nrf5_app_error
  nrf5_log
  nrf5_log_backend_serial
  nrf5_log_backend_uart
  nrf5_log_default_backends
  nrf5_bsp
)

Notice the use of the nrf5_target_link_libraries() function. It resembles the standard CMake target_link_libraries() clause however, it additionally links the dependencies of the libraries you specify. This is the reason why we can keep the list of libraries so short. To give you a perspective, just the nrf5_log library depends on several other libraries that we, fortunately, do not have to specify due to dependency tracking in nRF5 CMake. Yay!

The final CMakeLists.txt:

cmake_minimum_required(VERSION 3.14)
project(nrf5_example_bsp LANGUAGES C ASM)

list(APPEND CMAKE_MODULE_PATH "cmake")
include("nrf5")

add_executable(nrf5_example_bsp "main.c")
nrf5_target(nrf5_example_bsp)
nrf5_target_link_libraries(nrf5_example_bsp PRIVATE
  nrf5_app_error
  nrf5_log
  nrf5_log_backend_serial
  nrf5_log_backend_uart
  nrf5_log_default_backends
  nrf5_bsp
)

Building the project

As you’re probably aware, CMake does not actually build anything. Instead, it generates input files for build systems (like make or Ninja) which actually do that job. To generate those files, we need to call CMake while passing some required parameters. Writing everything in a single command line might not be very convenient so I recommend that you create a Shell or Batch script in the main project directory. If you’re using Shell, it may look like this:

cmake                                                     \
   -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake       \
   -DTOOLCHAIN_PREFIX=~/tools/gcc-arm-none-eabi           \
   -DNRF5_SDK_PATH=~/tools/nRF5SDK160098a08e2             \
   -DNRF5_BOARD=pca10040                                  \
   -DNRF5_SOFTDEVICE_VARIANT=blank                        \
   -DNRF5_SDKCONFIG_PATH=config/pca10040                  \
   -DNRF5_LINKER_SCRIPT=config/pca10040/bsp_gcc_nrf52.ld  \
   -G "Unix Makefiles"                                    \
   -B build                                               \
   -S .

Let me explain what is going on here by going through these CMake options one by one:

  • -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake

By default, CMake uses the toolchain (compiler, linker etc.) that is native to the operating system that you run CMake on. That’s not what we want since we need a binary for the ARM Cortex-M embedded processor. By passing a so-called toolchain file you specify the right compiler, assembler, linker, etc. and a proper set of flags for them. We provide a toolchain file for the GNU Arm Embedded toolchain (the one we need) as part of nRF5 CMake.

  • -DTOOLCHAIN_PREFIX=~/tools/gcc-arm-none-eabi-9-2019-q4-major-win32

Here you tell CMake where to find the toolchain indicated by the toolchain file on your computer. Pass the path to where you’ve installed the GNU Arm Embedded Toolchain.

  • -DNRF5_SDK_PATH=~/tools/nRF5SDK160098a08e2

This is the path where you have extracted the nRF5 SDK.

  • -DNRF5_BOARD=pca10040

Here you specify the board you want to build the project for. In my case, it is the nRF52-DK board. If you have the nRF52840 board, please specify pca10056 instead.

  • -DNRF5_SOFTDEVICE_VARIANT=blank

SoftDevice is the name of Nordic’s BLE/ANT stack which comes in many variants e.g. S112, S,132, S140. You need to tell CMake which SoftDevice (if any) you’re going to use in your project. In our case, we don’t use BLE or ANT at all so you need to specify blank here.

  • -DNRF5_SDKCONFIG_PATH=config

Here is the path to the SDK configuration file (sdk_config.h), in our case we’ve copied it to the config folder.

  • -DNRF5_LINKER_SCRIPT=config/bsp_gcc_nrf52.ld

By using this option, we specify the linker script.

  • -G "Unix Makefiles"

The -G option tells CMake which build system we’re going to use. If you use Ninja, specify Ninja instead.

  • -B build

Here we pass the directory where we want CMake to generate the build system input files. It will be created if it doesn’t exist.

  • -S . Finally, we point CMake to the location where we have the CMakeLists.txt we’ve just written.

Open up a console and run the script in order to call CMake. The output should look more or less like this:

-- The C compiler identification is GNU 9.2.1
-- The ASM compiler identification is GNU
-- Found assembler: D:/tools/gcc-arm-none-eabi-9-2019-q4-major-win32/bin/arm-none-eabi-gcc.exe
-- Check for working C compiler: D:/tools/gcc-arm-none-eabi-9-2019-q4-major-win32/bin/arm-none-eabi-gcc.exe
-- Check for working C compiler: D:/tools/gcc-arm-none-eabi-9-2019-q4-major-win32/bin/arm-none-eabi-gcc.exe - works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- nRF SDK version not explicitly provided, using deduced one: 16.0.0
-- Using nRF5 DK board: pca10040
-- Using nRF5 target: nrf52832_xxaa
-- Using SoftDevice variant: blank
-- Using linker script: D:/workspace/nrf5-bsp/config/pca10040/bsp_gcc_nrf52.ld
-- Using sdk_config.h include path: D:/workspace/nrf5-bsp/config/pca10040
-- Using nrfjprog utility available from PATH
-- Configuring done
-- Generating done
-- Build files have been written to: D:/workspace/nrf5-bsp/build

Navigate to the build directory where you should have the files for your build system. Depending on which build system you chose, typemake or ninja. Assuming everything went alright, you should see a little summary at the end:

[43/43] Linking C executable nrf5_example_bsp
   text    data     bss     dec     hex filename
  30824     288    2208   33320    8228 nrf5_example_bsp

Congratulations! You’ve just built the binary for an nRF52 SoC with nRF5 CMake.

Programming the board

Connect your development board (nRF52-DK or nRF52840-DK) to your PC via USB. From the system console, navigate to the build directory and type make flash or ninja flash depending on your build system. Using the nrfjprog utility, the binary should get loaded into your board. You can now test if it’s working correctly. Try pressing Button 1 or Button 2 on the board. You should notice different LED blinking patterns each time you press a button. You can also open a Virtual COM port provided by the board and observe the log entries printed by the application running on the nRF52 SoC.

Summary

Hopefully, you’ve learned from this tutorial how to create and build a simple nRF52 project using the nRF52 CMake. With a bit of luck, I hope you find it a nice alternative to IDEs when it comes to nRF52 development. There’s still a lot to learn, though. We didn’t even touch subjects like consistency checks, continuous integration, and CMake scripts generation, which are all already implemented in nRF5 CMake. If you’re interested, be sure to visit the nRF5 CMake Wiki on Github. You can also find additional information in README.md files located in the repository.

Borys Jeleński

Software Engineer

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!