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.
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.
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).
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.
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.
Please download and install the following tools that we’re going to need:
- CMake. Use version 3.14 or newer. Make sure to add it to your PATH variable.
- GNU Arm Embedded Toolchain.
- J-Link Software and Documentation Pack.
- nRF Command Line Tools.
- Ninja (optional). You only need to install Ninja if you’re running on Windows since you won’t have the popular make available by default, of course.
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.
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
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
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
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.
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
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!
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 )
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:
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.
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.
This is the path where you have extracted the nRF5 SDK.
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
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
Here is the path to the SDK configuration file (sdk_config.h), in our case we’ve copied it to the config folder.
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.
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, type
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.
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.
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.