Transitioning from Bare Metal: Nordic SDK Build System

Transitioning from Bare Metal: Nordic SDK Build System

Intro

As embedded systems complexity grows, firmware development needs more and more tools and build systems to handle it. One common reason to switch from bare metal to a more complex RTOS is when adding more communications, such as bluetooth, to an embedded system. Nordic is a very popular creator of bluetooth chips, and they support their hardware with their nordic nRF connect SDK, a collection of tools and the Zephyr RTOS all packaged up and included in custom VSCode extensions. Nordic supports devs working on their hardware with online courses, such as their nRF Connect SDK Fundamentals course, that guides you in installing all the necessary tools to build and flash a dev kit from Nordic. 

However, a lot of the intro level courses want to get to developing a project, and completely gloss over how things are built and flashed. They have you install a collection of mystery tools, then give you magic strings to use. The moment something doesn’t go according to the tutorial, you’re stuck searching the internet for forum posts. Developing a basic model of how the code is actually built and flashed before diving into the projects themselves gives a strong foundation for using and troubleshooting projects. As a bonus, it also prevents the common “mistrust of complexity” problem when trying to shift bare metal engineers to more complex systems. 

This post will go through the tools used to build a Nordic blinky project from the Fundamentals course. It will then parse out the logs from the build system and connect them to the tools we talked about.

Book a Call with Dojo Five Embedded Experts

What will we need to install?

The Nordic SDK contains tools to build your application for whichever supported hardware you’re using, plus the source code to build an RTOS-based project. Nordic uses Zephyr as the RTOS of choice, so it also includes the Zephyr SDK. The Zephyr SDK contains everything needed to build Zephyr projects, as well as any libraries that are needed by either the Zephyr RTOS itself, or the application written by the developer.

Review tooling from the bottom-up

The above summary has a lot of layers and components, so lets start at someplace a bare metal engineer recognizes, and work up the layers. 

The Compiler and Linker

The classic ARM GCC is used for compiling and linking the source code into an ELF or hex file that you will flash onto a device. The actual binaries will be part of the Zephyr SDK, but the versioning is managed by the Nordic SDK and VSCode extensions. Here’s an example filepath to the gcc that I used to compile code for the nRF5340 dev kit:

/opt/nordic/ncs/toolchains/580e4ef81c/opt/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc

Which shows the binary is inside the nordic toolchain folder and the Zephyr SDK folder. 

Ninja

If you’re familiar with Make, Ninja is a replacement for it developed by Evan Martin back in 2012. Like Make, Ninja requires a script (called a build.ninja for Ninja) that, when run, will go through all .c/.h files to be compiled/linked and build the final hex. This will include the source files written by you, the header file for the given hardware, and the header files and source code for the RTOS functionality needed by the project. How are those last two items found or created? Well they are generated ultimately by…

CMake

CMake is used to add a level of abstraction to the build process, since CMake can generate multiple different build scripts from the same initial CMake files, as well as being OS-agnostic. Cmake supports Ninja, Make, and other build systems like GreenHills and Xcode. However, Zephyr only supports using Ninja or Make, and the Nordic SDK defaults to generating Ninja scripts. So when run, CMake will generate Ninja scripts that use a particular compiler/assembler/linker needed for your hardware.

Since CMake is being used for a Zephyr project, it also will be used to call Python scripts that generate the header files for the given hardware, and the header files and source code for the planned RTOS functionality. Once generated, these are used by the generated Ninja script when compiling the project together. Two Zephyr configuration files, the DeviceTree and Kconfig files (explained more below), are used by the Python scripts to generate the header/source files for the particular hardware/dev kit you want to flash. 

To give more visibility into what CMake is doing, it can even generate a list of the actual commands in a compile_commands.json file. This is set by default in the Nordic SDK, so when building projects, you can verify CMake is creating the commands you think it should. 

For more info: CMAKE_EXPORT_COMPILE_COMMANDS

Zephyr West build

The Zephyr west tool is a “meta-tool” that has a lot of functionality in it, but the main focus for this post is the west build function, which will run CMake and build the code. If no build folder exists, west build will run CMake to generate the folder and all configuration files needed. If there already exists a build folder, it will incrementally recompile with CMake.

Zephyr Configurations – An Aside

There’s two configuration files listed in the above build process that you won’t be familiar with if you haven’t used Zephyr or worked on the Linux kernel: DeviceTree and KConfig

The DeviceTree is a file with specific syntax used to describe the hardware (GPIO, buttons, the SoC, etc.). The build system uses it to generate the header file representing the hardware that more bare metal firmware engineers would be familiar with. It also configures the default boot-time configuration of the hardware. It can have a base configuration file provided by Nordic for Nordic chips, with “overlays” that modify the described hardware, to support custom hardware. 

The Kconfig is borrowed from the Linux kernel, originally. This configuration file controls which software functionality from the kernel will be compiled in the final elf/hex file. An example would be software support for the watchdog, or a floating point unit. Like the DeviceTree, the Kconfig can also have a base configuration with modifications done per project.

Zephyr has more info on them both, and if you’re confused about the differences between the two: DeviceTree vs Kconfig 

Installing the tools

Now that we know what we’re supposed to be using, let’s get a blinky project running on your dev kit, following the Nordic Developer Academy fundamentals course. The first thing the course does is have you install extensions.

The Extensions

Below is the list of extensions Nordic currently recommends to install for VSCode, which can be installed all at once with their nRF Connect for VS Code Extension Pack. The descriptions below are partly pulled from their overview of what’s in the extension:

  • nRF Connect for VS Code
    • This has an interface to the build system and nRF Connect SDK. It also provides an interface to manage nRF Connect SDK versions and toolchains, and sets an active SDK/toolchain version.
    • The nRF Connect SDK installs these: Zephyr SDK, cmake, dtc, git, gperf, ninja, python3 and west.
    • The Zephyr SDK includes the toolchains for multiple supported architectures.
  • nRF DeviceTree
    • Provides DeviceTree language support and the DeviceTree Visual Editor.
    • It’s a nice GUI for editing DeviceTree files, but is not strictly necessary for building and compiling.
  • nRF Kconfig
    • Provides Kconfig language support.
    • Note: This extension does not appear to work on MacOS Sequoia 15.1
  • nRF Terminal
    • Provides a builtin serial and RTT terminal to VSCode. 
    • Mainly useful if you do not already have a favorite serial terminal.
  • C/C++ from Microsoft
    • Adds language support for C/C++, including features such as IntelliSense.
    • If you already use VSCode for C/C++ coding, this is likely already installed.
  • CMake
    • CMake language support
    • Same as the C/C++ extension above, may already be installed
  • GNU Linker Map Files
    • Linker map files support

Once the above extensions are installed, the course walks you through downloading a particular toolchain and Nordic SDK version, creating an application based on the blinky sample, and configuring the hardware you want to build to, so the correct DeviceTree and Kconfig files are used. Finally, it has you actually build the application, and use the VSCode extension to flash your hardware. In my case, I was using the nRF5340 dev kit, which had a built in J-Link to support flashing the chip, so flashing the hardware didn’t require me to dig out a programmer from my bins.

When I followed the steps from dev academy, I successfully got blinky flashed to my dev kit, but started getting endless errors from finder about how the J-Link was not ejected properly. 

Successful flash!

Angry JLink message:

There are two different ways to solve this. 

First solution

I first did some hunting online and came across this forum post that implied the issue was coming from the J-Link firmware on the dev kit being too old. With this and other vague hints online, I decided to update the J-Link programmer on the dev kit.

I downloaded the J-Link config program from SEGGER.  Once installed, I opened up the J-Link config app v8.10f and powered on the dev kit. From there, I could see the J-Link programmer on my dev kit listed as connected via USB.

Right clicking on the appeared J-Link and then “update firmware”, I got the J-Link firmware that was compiled oct 8 2024.

Once this finished updating, the error went away! 

Now, to then flash using VScode and the updated J-Link programmer, you must reset the device, otherwise you’ll get the following error:

[error] [  JLink] - Communication timed out: Requested 4 bytes, received -1 bytes !

[error] [ Client] - Encountered error -105: Command select_coprocessor executed for 128 milliseconds with result -105

ERROR: Failed when selecting coprocessor APPLICATION

[error] [ Worker] - An unknown error.

ERROR: The JLinkARM DLL timed out while communicating to the J-Link probe.

ERROR: If the error condition persists, run the same command again with

ERROR: argument -- log, contact Nordic Semiconductor and provide the generated

ERROR: log.log file to them.

NOTE: For additional output, try running again with logging enabled (--log).

NOTE: Any generated log error messages will be displayed.

FATAL ERROR: command exited with status 35:

Second Solution

Talking with coworkers, I learned Zephyr actually suggests a different solution: disabling the Mass Storage device.

Removing the Mass Storage device functionality frees up a USB Endpoint on the J-link programmer that supports 512 bytes, which prevents data corruption or drops if you use the USB CDC ACM Serial Port with packets larger than 64 bytes.

If you already installed the J-Link Software and Documentation pack from the first solution, you can just go to your terminal, enter JLinkExe, and then at the prompt do MSDDisable to disable the Mass Storage device functionality.

Example Logs from Building and Flashing

Since we’ve now built and flashed the blinky application, we can review the logs to see how the tools we learned about are actually called. Here are some example logs generated when building and flashing the nRF5340 devkit. 

Note: If, instead of using the VSCode extension, you run these commands on the command line, you can include -vvv flag after west to get extra debug information

Build Configuration Logs

Here’s the start of the log example when generating the initial build configuration for a project named fund_less5_exer1. This was triggered by adding a build config for an nRF5340 dev kit in VSCode and unchecking the option to both build the configurations and build the application.

Executing task: nRF Connect: Generate config nrf5340dk_nrf5340_cpuapp_ns for fund_less5_exer1

Building fund_less5_exer1

west build --build-dir /Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1/build /Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1 --pristine --board nrf5340dk_nrf5340_cpuapp_ns --cmake-only -- -DNCS_TOOLCHAIN_VERSION=NONE

The west build command above includes the needed paths, which board, and doing the configuration only.

-- west build: generating a build system

Loading Zephyr default modules (Zephyr base).

-- Application: /Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1

-- CMake version: 3.21.0

-- Found Python3: /opt/nordic/ncs/toolchains/580e4ef81c/opt/python@3.9/bin/python3.9 (found suitable version "3.9.6", minimum required is "3.8") found components: Interpreter

-- Cache files will be written to: /Users/yourprofile/Library/Caches/zephyr

-- Zephyr version: 3.5.99 (/opt/nordic/ncs/v2.6.1/zephyr)

-- Found west (found suitable version "1.2.0", minimum required is "0.14.0")

-- Board: nrf5340dk_nrf5340_cpuapp_ns

-- Found host-tools: zephyr 0.16.5 (/opt/nordic/ncs/toolchains/580e4ef81c/opt/zephyr-sdk)

-- Found toolchain: zephyr 0.16.5 (/opt/nordic/ncs/toolchains/580e4ef81c/opt/zephyr-sdk)

-- Found Dtc: /opt/nordic/ncs/toolchains/580e4ef81c/bin/dtc (found suitable version "1.6.1", minimum required is "1.4.6")

Here the logs show the west build is collecting the tools needed: CMake, python for the Python scripts, and Zephyr SDK and a related toolchain.

-- Found BOARD.dts: /opt/nordic/ncs/v2.6.1/zephyr/boards/arm/nrf5340dk_nrf5340/nrf5340dk_nrf5340_cpuapp_ns.dts

-- Generated zephyr.dts: /Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1/build/zephyr/zephyr.dts

-- Generated devicetree_generated.h: /Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1/build/zephyr/include/generated/devicetree_generated.h

-- Including generated dts.cmake file: /Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1/build/zephyr/dts.cmake

This section of the log gets the default DeviceTree for the given hardware processes it, and generates a .h file for the DeviceTree

Parsing /opt/nordic/ncs/v2.6.1/zephyr/Kconfig

Loaded configuration '/opt/nordic/ncs/v2.6.1/zephyr/boards/arm/nrf5340dk_nrf5340/nrf5340dk_nrf5340_cpuapp_ns_defconfig'

Merged configuration '/Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1/prj.conf'

Configuration saved to '/Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1/build/zephyr/.config'

Kconfig header saved to '/Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1/build/zephyr/include/generated/autoconf.h'

Here it’s finding the default Kconfig, adding any custom configs I added in the application project, and generating a header file for it.

-- Found GnuLd: /opt/nordic/ncs/toolchains/580e4ef81c/opt/zephyr-sdk/arm-zephyr-eabi/bin/../lib/gcc/arm-zephyr-eabi/12.2.0/../../../../arm-zephyr-eabi/bin/ld.bfd (found version "2.38")

-- The C compiler identification is GNU 12.2.0

-- The CXX compiler identification is GNU 12.2.0

-- The ASM compiler identification is GNU

-- Found assembler: /opt/nordic/ncs/toolchains/580e4ef81c/opt/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc

-- Using ccache: /opt/nordic/ncs/toolchains/580e4ef81c/bin/ccache

Dropping partition 'nonsecure_storage' since it is empty.

-- Configuring done

-- Generating done

-- Build files have been written to: /Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1/build

And lastly for the initial configuration, here’s the log of the chosen toolchain the code will be built with. Remember, this first log list is the build configuration process, it hasn’t actually generated the final files to flash to the hardware. That’s done in this next section. At this point, you can look in the /build folder and see the generated build configuration files. 

Building Logs

Here’s the logs to actually build the hex files. The default logging levels feel pretty high-level, but again, you can use the -vvv flag on the command line to get more log output, if desired.

Building fund_less5_exer1

west build --build-dir /Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1/build /Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1

Here’s the main west call.

[1/219] Preparing syscall dependency handling

[4/219] Generating include/generated/version.h

-- Zephyr version: 3.5.99 (/opt/nordic/ncs/v2.6.1/zephyr), build: v3.5.99-ncs1-1

Zephyr determines used syscalls, and shows which version it’s using

[9/219] Generating ../../tfm/CMakeCache.txt

CMake Warning at cmake/version.cmake:22 (message):

  Actual TF-M version is not available from Git repository.  Settled to

  v2.0.0

Trusted Firmware M is outside the scope of this post. Because I’m building to a cortex-m33 chip on my nRF5340, it has support for a Secure Processing Environment (SPE). Trusted Firmware M implements the SPE for Armv8-M and Armv8.1-M architecture, so it’s automatically included and built as needed.

Call Stack (most recent call first):

  CMakeLists.txt:22 (include)

-- Found Git: /opt/nordic/ncs/toolchains/580e4ef81c/bin/git (found version "2.37.3")

-- The C compiler identification is GNU 12.2.0

-- The CXX compiler identification is GNU 12.2.0

-- The ASM compiler identification is GNU

-- Found assembler: /opt/nordic/ncs/toolchains/580e4ef81c/opt/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc

-- Found Python3: /opt/nordic/ncs/toolchains/580e4ef81c/opt/python@3.9/bin/python3.9 (found version "3.9.6") found components: Interpreter

CMake Deprecation Warning at /opt/nordic/ncs/v2.6.1/zephyr/cmake/modules/FindDeprecated.cmake:121 (message):

  'PYTHON_PREFER' variable is deprecated.  Please use Python3_EXECUTABLE

  instead.

Call Stack (most recent call first):

  /opt/nordic/ncs/v2.6.1/zephyr/cmake/modules/python.cmake:16 (find_package)

  /opt/nordic/ncs/v2.6.1/zephyr/cmake/modules/user_cache.cmake:30 (include)

  /opt/nordic/ncs/v2.6.1/zephyr/cmake/modules/extensions.cmake:5 (include)

  /opt/nordic/ncs/v2.6.1/nrf/subsys/nrf_security/tfm/CMakeLists.txt:38 (include)

-- Found Python3: /opt/nordic/ncs/toolchains/580e4ef81c/opt/python@3.9/bin/python3.9 (found suitable version "3.9.6", minimum required is "3.8") found components: Interpreter

-- Cache files will be written to: /Users/yourprofile/Library/Caches/zephyr

-- Configuring done

-- Generating done

The logs above give the versioning info again

CMake Warning:

  Manually-specified variables were not used by the project:

CRYPTO_RNG_MODULE_ENABLED

MBEDTLS_PSA_CRYPTO_USER_CONFIG_FILE

PYTHON_PREFER

-- Build files have been written to: /Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1/build/tfm

[163/167] Linking C executable bin/tfm_s.axf

Memory region     Used Size  Region Size  %age Used

        FLASH:   32104 B    32 KB 97.97%

          RAM:   10416 B    32 KB 31.79%

[17/219] Performing install step for 'tfm'

-- Install configuration: "MinSizeRel"

----- Installing platform NS -----

[217/219] Linking C executable zephyr/zephyr.elf

Memory region     Used Size  Region Size  %age Used

        FLASH:   25456 B   992 KB  2.51%

          RAM:    4896 B   416 KB  1.15%

     IDT_LIST:      0 GB    32 KB  0.00%

[219/219] Generating zephyr/merged.hex

And lastly, here it shows the merged.hex file that was generated (as well as the tfm generation, which again, is outside the scope of this post).

Flashing Logs

We have the merged.hex file generated from the build above, so let’s flash the dev kit and see if that’s the file used! Here’s the logs when hitting the “Flash” button in VSCode.

Building fund_less5_exer1

west build --build-dir /Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1/build /Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1

[0/16] Performing build step for 'tfm'

ninja: no work to do.

[2/3] Performing install step for 'tfm'

-- Install configuration: "MinSizeRel"

----- Installing platform NS -----

[3/3] Completed 'tfm'

The west build command is called again, but notice it can tell the project was just recently built, and doesn’t try to do anything.

*  Executing task: nRF Connect: Flash: fund_less5_exer1/build (active)

Flashing build to 1050029006

west flash -d /Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1/build --skip-rebuild --dev-id 1050029006

-- west flash: using runner nrfjprog

-- runners.nrfjprog: reset after flashing requested

Here’s the actual flash command! West is used for flashing as well, like mentioned earlier, it’s a “meta-tool” that has a lot of functionality, and flashing is one of them. The west command also has the dev-id, which is the SN of the J-Link programmer on my devkit.

-- runners.nrfjprog: Flashing file: 

/Users/yourprofile/nordic/nordic-zephyr/fund_less5_exer1/build/zephyr/merged.hex

[ #################### ]   1.895s | Erase file - Done erasing                                                      

[ #################### ]   0.433s | Program file - Done programming                                                

[ #################### ]   0.335s | Verify file - Done verifying                                                  

Applying pin reset.

-- runners.nrfjprog: Board with serial number 1050029006 flashed successfully.

And there’s the merged.hex file getting flashed to the device, followed by toggling the reset pin.

Success!

Next Steps

This post focused solely on the build system at a high level, connecting the dots from the compiler up to build a better mental model of the Nordic Connect SDK ecosystem. The next steps would be working through the nRF dev academy courses to continue to get more in-depth with the actual Zephyr and Nordic application code. Or continue on your own, using the Nordic Connect SDK documentation and Zephyr API info. I’ve included links in the references section below to help move forward. I hope this gives you more context and more confidence to work with Nordic hardware.

Do Embedded Firmware Right with Dojo Five on Your Team

If you’d like to work with a knowledgeable team of engineers just like me on your next embedded project, you can book a call with us to get the conversation started.

Contact Dojo Five >

References

Discover why Dojo Five EmbedOps is the embedded enterprise choice for build tool and test management.

Sign up to receive a free account to the EmbedOps platform and start building with confidence..

  • Connect a repo
  • Use Dev Containers with your Continuous Integration (CI) provider
  • Analyze memory usage
  • Integrate and visualize static analysis results
  • Perform Hardware-in-the-Loop (HIL) tests
  • Install the Command Line Interface for a developer-friendly experience

Subscribe to our Monthly Newsletter

Subscribe to our monthly newsletter for development insights delivered straight to your inbox.

Interested in learning more?

Best-in-class embedded firmware content, resources and best practices

Laptop with some code on screen

I want to write my first embedded program. Where do I start?

The boom in the Internet of Things (IoT) commercial devices and hobbyist platforms like the Raspberry Pi and Arduino have created a lot of options, offering inexpensive platforms with easy to use development tools for creating embedded projects. You have a lot of options to choose from. An embedded development platform is typically a microcontroller chip mounted on a circuit board designed to show off its features. There are typically two types out there: there are inexpensive versions, sometimes called

Read More »
Medical device monitoring vitals

IEC-62304 Medical Device Software – Software Life Cycle Processes Primer – Part 1

IEC-62304 Software Lifecycle requires a lot of self-reflection to scrutinize and document your development processes. There is an endless pursuit of perfection when it comes to heavily regulated industries. How can you guarantee something will have zero defects? That’s a pretty hefty task. The regulatory approach for the medical device industry is process control. The concept essentially states that if you document how every step must be completed, and provide checks to show every step has been completed properly, you

Read More »
Operating room filled with medical devices

IEC-62304 Medical Device Software – Software Life Cycle Processes Primer – Part II

Part I provides some background to IEC-62304. Part II provides a slightly more in-depth look at some of the specifics. The IEC 62304 Medical Device Software – Software Lifecycle Processes looks into your development processes for creating and maintaining your software. The standard is available for purchase here. So what activities does the standard look at? Here are some of the major topics. For any given topic, there will be a lot more specifics. This will look at a few

Read More »