Migration notes for nRF Connect SDK v2.0.0

In version 2.0.0, the nRF Connect SDK introduced a number of major changes into its codebase. If your application was built using nRF Connect SDK v1.x.x, make sure to complete the actions listed in the following sections to migrate your application to nRF Connect SDK v2.x.x.

Toolchain change

nRF Connect SDK no longer uses the GNU Arm Embedded Toolchain by default. The current default toolchain is the Zephyr SDK instead, which now supports all three main operating systems (Microsoft Windows, macOS, and GNU/Linux).

Required action:

If you do not use the Toolchain Manager, install the Zephyr SDK by following the steps in Install a Toolchain section. The updated Toolchain Manager already contains it.

Note that if you use the Toolchain Manager, it will now install the toolchain in a separate directory (for example toolchains/v2.0.0/), next to the main nRF Connect SDK directory.

Default IDE change

Visual Studio Code extension IDE has replaced SEGGER Embedded Studio as the default supported IDE for working with the nRF Connect SDK.

Required action:

If you are building the application or sample using SEGGER Embedded Studio or on the command line and want to migrate to Visual Studio Code extension IDE, follow the instructions in the migrating from other IDEs to VS Code documentation.

Pin control transition

Zephyr v3.x.x introduces new state-based pin control API (pinctrl). For more details, see Pin Control in Zephyr documentation.

Required action:

The Pin control user guide provides information on how to migrate your application to use the new pin control mechanism.

Default secure firmware solution change

Trusted Firmware M (TF-M) replaces the Secure Partition Manager (SPM) for secure image firmware. TF-M is now enabled by default for most nRF9160 and nRF5340 applications and samples.

Required action:

To start using TF-M instead of SPM in your application, see the information in Migrating from Secure Partition Manager to Trusted Firmware-M.

Switch from TinyCBOR to zcbor

With Zephyr v3.x.x, TinyCBOR becomes deprecated and is replaced by zcbor. If you are using TinyCBOR to encode data into CBOR format in your application, follow this guide to migrate your code.


If you have extended MCUmgr with additional custom commands, then your code will not compile anymore as the commands will receive pointers to variables that no longer exist or that have changed their types during TinyCBOR to zcbor transition.

General migration

The TinyCBOR to zcbor migration consists of two tasks:

  1. zcbor state initialization

  2. TinyCBOR to zcbor decoding/encoding API replacement

The first task involves context initialization, while the second task is almost a function-to-function conversion, with a few exceptions in how parameters are passed and what return values are returned from API calls.

zcbor state initialization

You must first initialize the decoding and encoding state of zcbor.

Decoding state initialization

Decoding with TinyCBOR requires setting up a reader with a buffer by calling cbor_buf_reader_init(), and attaching it to a parser state with cbor_parser_init(). zcbor decoding is initialized with a call to zcbor_new_decode_state() that directly ties the provided buffer to decoding state. For better understanding of how zcbor states work, refer to the zcbor documentation.

The following is an example of simple zcbor state initialization for decoding:

#define N   20              /* Max expected encoded elements */
zcbor_state_t zs[2];
uint8_t buffer[SOME_SIZE];
/* Read some data to the buffer */
zcbor_new_decode_state(zs, ARRAY_SIZE(zs), buffer, ARRAY_SIZE(buffer), N);
/* Decode data from the buffer with zs state */

In this example:

  • The N parameter represents a possible number of encoded fields. It does not have to be accurate, but it must at least have a value corresponding to the number of expected fields. In case the top element is a list or a map, then this will be 1. MCUmgr is an example of such a case, as there is always a top-level, single map required.

  • zs is the zcbor state that consists of two states, which is a minimal practical state count. These states will allow to work on flat CBOR encoding where no list or map is used to envelope variables at the top level of CBOR stream encoding. Two states are used because one will be the current state and the other can be used as a constant state where an encoding error is stored with some other internal variables. In case you plan to have embedded lists or maps, the number of states for decoding must be increased by adding a number of levels to the base 2. For example, in case of three levels of map or list encoding, the number of states must be set to 5.

  • The buffer parameter can be any plain buffer and decoding requires the buffer size.

At this point, the zcbor state is initialized. After initializing the zcbor state, the zcbor decoding API calls must be given a pointer to the first zcbor state. This can be zs or &zs[0], as in the example.


Currently, there is no API provided for zcbor to get information on how much of an input buffer has been consumed by decoding. The only way to find this out is to substitute the buffer pointer, given at the zcbor state initialization, from the state object element payload_mut, for example zs->payload_mut - buffer.

Encoding state initialization

Encoding with TinyCBOR is similar to decoding and requires two API calls: one to set up a buffer writer (cbor_buf_writer_init()) and one to attach the writer to the encoding state (cbor_encoder_init()). zcbor encoding is initialized with a single call to zcbor_new_encode_state() that directly ties the provided buffer to an encoding state. See the following example of simple zcbor state initialization for encoding:

zcbor_state_t zs[2];
uint8_t buffer[SOME_SIZE];
zcbor_new_encode_state(zs, ARRAY_SIZE(zs), buffer, ARRAY_SIZE(buffer), 0);
/* Encode data to the buffer with zs state */

In this example:

  • zs is the zcbor state that consists of two states, which is enough when zcbor is in non-canonical configuration (see CONFIG_ZCBOR_CANONICAL). In the non-canonical configuration, the number of states would need to be increased by a maximum level of expected lists or arrays enveloping each other (or both).

  • The buffer parameter where encoded data will be placed, with size that limits it.

  • 0 is the final parameter that represents the number of items encoded into the buffer.

At this point, the zcbor state is initialized and zcbor decoding API calls that follow it must be given a pointer to the first zcbor state. This can be zs or &zs[0], as in the example.


Currently, there is no API provided for zcbor to get information on how much of an output buffer has been used by encoded data. The only way to find this out is to substitute the buffer pointer, given at a zcbor state initialization, from the state object element payload_mut, for example zs->payload_mut - buffer.

TinyCBOR to zcbor decoding/encoding API replacement

There are three major differences between TinyCBOR and zcbor APIs:

  • TinyCBOR calls return error codes while zcbor returns a boolean value (true is success).

  • zcbor does not provide type or value peek functions.

  • All of zcbor functions advance buffer on success.

zbcor stores error codes with zs and, when configured, can automatically fail consecutive calls when decoding or encoding fails. Otherwise, the calls are quite similar: functions from both APIs take context or state as one parameter and value or pointer for value and optional buffer size, when required, as the other parameters.

The following list maps TinyCBOR functions to their zcbor equivalents:




zcbor_map_start_decode() zcbor_list_start_decode()


zcbor_map_end_decode() zcbor_list_end_decode()












zcbor_float64_decode() zcbor_double_decode()


No direct replacement, the zcbor_bstr_decode() or zcbor_tstr_decode() functions will retrieve both the value pointer and length in a single call.


No direct replacement, the zcbor_tstr_decode() function will retrieve both the value pointer and length in a single call.


No direct replacement, the zcbor_bstr_decode() function will retrieve both the value pointer and length in a single call.






zcbor_map_end_encode() zcbor_list_end_encode()


zcbor_int32_put() zcbor_int64_put()


zcbor_uint32_put() zcbor_uint64_put()




No equivalent




zcbor_float64_put() zcbor_double_put()


zcbor_tstr_encode() zcbor_tstr_encode_ptr() zcbor_tstr_put_lit()




zcbor_bstr_encode() zcbor_bstr_encode_ptr()





There is no direct replacement for the cbor_value_is_null() and cbor_value_is_undefined() functions in zcbor. To check whether such value is expected and where it is expected, use the following code:

bool ok;
/* Make sure that not in error state first */
if (zs->constant_state->error != 0) {
    /* Already in error */
ok = zcbor_nil_decode(zs, NULL);
if (!ok && zs->constant_state->error == ZCBOR_ERR_WRONG_TYPE) {
    /* Process non-NULL element */
} else if (!ok) {
    /* Other error */
/* Successfully decoded NULL element in CBOR stream */

MCUmgr commands and groups migration

In contrast to General migration, except for tuning some Kconfig configuration constants, zcbor setup is not needed for the MCUmgr commands and groups, because the MCUmgr library takes care of the task. The only exception is tuning some Kconfig configuration constants.

Most of the function replacement task is covered by the General migration, with exception to usage of cborattr, which is covered in Replacing cbor_read_object() with zcbor_map_decode_bulk().

The other important issue is decoding or encoding context access, which is described in Decoding and encoding context.


The encoding context provided by the command processing handler function as a parameter has the top level map already created. The map will be closed on a successful return from the handler, so handlers need not create the top level map or close it. The decoding context, in contrary to the encoding, is set before the top map and handlers are responsible for opening of the top-level map as a part of command decoding or processing code.

Decoding and encoding context

Whenever TinyCBOR or zcbor is used, command processing functions are given a pointer to an object of type struct mgmt_ctxt. TinyCBOR uses the encoder element of this object for encoding functions and it, a TinyCBOR CborValue type iterator, for decoding functions.

zcbor uses zcbor_state_t type objects for decoding and encoding states. These state objects have indirectly replaced TinyCBOR’s it iterator and encoder, respectively, within struct mgmt_ctxt. They are embedded within the struct cbor_nb_reader type object for decoding context, and in the struct cbor_nb_writer type object for encoding context.

The struct cbor_nb_reader and struct cbor_nb_writer objects tie zcbor_state_t with net_buf type buffers that hold data for decoding or will hold encoded data.

The substitution, in code, of references to decoder and encoder objects, as accessed through struct mgmt_ctxt, is shown by the following table:









Replacing cbor_read_object() with zcbor_map_decode_bulk()


In scenarios where decoding of keys is not required or it is required for only a single key, the procedure described in this section can be greatly simplified as a single key can be obtained by using zcbor_tst_decode() that is looped until the key is found.

cborattr was a private MCUmgr utility that was used within the command processing code to process CBOR list contents in bulk and is now replaced with zcbor_bulk(). These utility APIs are represented by a single function: cbor_read_object() in case of TinyCBOR, and zcbor_map_decode_bulk() in case of zcbor.

The advantage of zcbor_map_decode_bulk() over cbor_read_object() is the simplification of structures that define the mapping of decoding functions to keys in a CBOR map.

The following example demonstrates the transition from cbor_read_object() to zcbor_map_decode_bulk():

int image;
uint8_t img_data[SOME_DATA_LEN];
size_t data_len;
const struct cbor_attr_t off_attr[] = {
    [0] = {
        .attribute = "image",
        .type = CborAttrUnsignedIntegerType,
        .addr.uinteger = &req.image,
        .nodefault = true
    [1] = {
        .attribute = "data",
        .type = CborAttrByteStringType,
        .addr.bytestring.data = img_data,
        .addr.bytestring.len = &data_len,
        .len = sizeof(img_data)
rc = cbor_read_object(&ctxt->it, off_attr);

The off_attr provides specification of data encoded in CBOR format that is decoded by the call to cbor_read_object(). The specification is a list of structures that specify the following elements:

  • An expected list key (.attribute element)

  • A type of key (.type)

  • A pointer to the buffer for the key (usually subelement of the .addr element)

  • A few other attributes

The cbor_read_object() function takes the specification and attempts to get all the described fields into the specified designated variables, using the TinyCBOR decoding context. After a successful call to cbor_read_object(), the variables are set and ready for further processing, and the buffer is shifted beyond the list. This also includes copying binary and string buffers to the specified locations.

The context is accessed by ctxt->it in the given example. It is provided by the MCUmgr library, so it is already initialized and has the buffer attached.

The following code sample is the zcbor conversion of the given code example:

int image;
struct zcbor_string zst;
struct zcbor_map_decode_key_val image_upload_decode[] = {
        ZCBOR_MAP_DECODE_KEY_VAL(image, zcbor_int32_decode, &image),
        ZCBOR_MAP_DECODE_KEY_VAL(data, zcbor_bstr_decode, &zst),
ok = zcbor_map_decode_bulk(zsd, image_upload_decode,
                           ARRAY_SIZE(image_upload_decode), &decoded) == 0;

The list specification is significantly simplified as it consists of an array of ZCBOR_MAP_DECODE_KEY_VAL defined mappings, each of which is provided with the following parameters:

  • Name of a key (without quotation marks)

  • zcbor decoding function directly from API (or user-provided type equivalent)

  • Pointer to a variable where the extracted value will be stored

The difference here is that the decoding of binary and string data does not involve copying of the data to a specified buffer. Instead, as described in General migration, the decoding function is paired with a struct zcbor_string type variable where decoding will store position and length of the data, within the provided CBOR buffer. It is then up to the user to copy the values to a specified location. This mechanism improves the RAM and CPU usage because the need for an intermediate buffer is often reduced.

To process a CBOR buffer and decode it according to the defined mapping, zcbor_map_decode_bulk() takes the following parameters:

  • zcbor decoding context (zsd in the above example)

  • Mapping specification and size of the mapping

  • Pointer to a variable for number of successfully decoded map elements


Currently, there is no method provided to determine which keys have not been found in a decoded map. It is up to the user to verify whether keys that are optional have appeared in the decoded string. This can be done, for example, by checking if a destination variable value has changed from the initial value.

New Zephyr logging service - logging v2

Zephyr v3.x.x uses a new logging service (logging v2) by default. The legacy version of logging is still supported but it is deprecated and will be removed after Zephyr v3.1.0. For details about the Zephyr logging service, see Logging. Version 2 supports the same set of features with a number of extensions, however, the logging backend API is different. All backends in the tree support version 2 API but if you are using an out-of-tree backend, it must be adapted to use the new logging system.

Required action:

Logging v1 uses the following three functions that must be replaced:

/* DEPRECATED! Functions used for logging v1. */
void (*put)(const struct log_backend *const backend,
         struct log_msg *msg);
void (*put_sync_string)(const struct log_backend *const backend,
         struct log_msg_ids src_level, uint32_t timestamp,
         const char *fmt, va_list ap);
void (*put_sync_hexdump)(const struct log_backend *const backend,
         struct log_msg_ids src_level, uint32_t timestamp,
         const char *metadata, const uint8_t *data, uint32_t len);

Replace these functions with the following function used by logging v2:

/* Logging v2 function. */
void (*process)(const struct log_backend *const backend,
        union log_msg2_generic *msg);

Zephyr v3.x.x namespace change

All Zephyr public headers have been moved to include/zephyr, meaning they must be prefixed with <zephyr/...> when included. Because this change can potentially break many applications or libraries, CONFIG_LEGACY_INCLUDE_PATH is provided to allow using the old include path. This option is now enabled by default to allow a smooth transition. In order to facilitate the migration to the new include prefix, a script to automate the process is also provided in scripts/utils/migrate_includes.py (in Zephyr).

Changes in PWM API

Zephyr v3.x.x introduces changes in the PWM API that require modifying the board definitions. Old board definitions will cause a compilation error, and calling the old API functions will result in warnings stating that these functions are deprecated.

Required action:
  • pwms properties in devicetree nodes must be extended with two more cells (with period and flags) and now they need to specify PWM channels, not pin numbers.

  • Calls to the deprecated pwm_pin_set_cycles function must be replaced with calls to the pwm_set_cycles() function.

  • Calls to the deprecated pwm_pin_set_usec and pwm_pin_set_nsec functions must be replaced with calls to the pwm_set() function with the period and pulse values wrapped in the PWM_USEC macro or the PWM_NSEC macro, respectively.

Note that the pwm_set() and pwm_set_cycles() functions take a PWM channel as a parameter, not a pin number as the deprecated functions did. Also, the flags parameter is now supported, so either the PWM_POLARITY_INVERTED or PWM_POLARITY_NORMAL flag must be provided in each call.

Wherever possible, it is recommended to use the newly introduced PWM_DT_SPEC_GET macro (or another suitable one from its family) to obtain PWM information from devicetree, and then use the pwm_set_dt() or pwm_set_pulse_dt() function instead of pwm_set().

For example, for PWM channels defined as follows:

pwm0_default: pwm0_default {
    group1 {
        psels = <NRF_PSEL(PWM_OUT0, 0, 11)>;
    group2 {
        psels = <NRF_PSEL(PWM_OUT3, 1, 5)>;

pwm0_sleep: pwm0_sleep {
    group1 {
        psels = <NRF_PSEL(PWM_OUT0, 0, 11)>,
                <NRF_PSEL(PWM_OUT3, 1, 5)>;

&pwm0 {
    status = "okay";
    pinctrl-0 = <&pwm0_default>;
    pinctrl-1 = <&pwm0_sleep>;
    pinctrl-names = "default", "sleep";

You must update the PWM LED definitions that use those channels:

/* old definitions that will no longer work */
pwm_led0: pwm_led_0 {
    pwms = <&pwm0 11>;
pwm_led1: pwm_led_1 {
    pwms = <&pwm0 37>;

The above PWM LED definitions must be updated in the following way:

/* updated definitions */
pwm_led0: pwm_led_0 {
    pwms = <&pwm0 0 PWM_MSEC(20) PWM_POLARITY_INVERTED>;
pwm_led1: pwm_led_1 {
    pwms = <&pwm0 3 PWM_MSEC(20) PWM_POLARITY_NORMAL>;


The period lengths, set here arbitrarily to commonly used value of 20 ms, are provided as default ones. They can be overridden in the actual PWM API calls if needed.

Then, you can use the definitions in PWM API calls in the following way:

#define PWM_LED0_NODE DT_NODELABEL(pwm_led0)
#define PWM_LED1_NODE DT_NODELABEL(pwm_led1)
static const struct pwm_dt_spec led0_spec = PWM_DT_SPEC_GET(PWM_LED0_NODE);
static const struct pwm_dt_spec led1_spec = PWM_DT_SPEC_GET(PWM_LED1_NODE);
/* ... */
/* Use 10 ms period for LED0 to override the default 20 ms from devicetree. */
ret = pwm_set_dt(&led0_spec, PWM_MSEC(10), PWM_USEC(pulse_us));
/* ... */
ret = pwm_set_pulse_dt(&led1_spec, PWM_USEC(pulse_us));