Using Zephyr SMP with Multiple MCUs

The Simple Management Protocol (SMP) built into Zephyr is designed to send data to and from a microcontroller running Zephyr RTOS. The most visible purpose is when using the MCUmgr command line tool to access data on a chip from a computer. In fact, we use this approach with the certificate provisioning sample in the Golioth Firmware SDK to store certificates in the filesystem. But its also possible to use SMP to communicate between two microcontrollers.

In today’s post we’ll explore what options are available with SMP. This includes the basics of using the SMP Client, some of the features that are built into the SMP Server, and options you have for adding your own customization to the server.

Overview of SMP Protocol and Transport

SMP is a protocol based around one server and one client. It has built-in support for OS, Image (firmware, etc.), Statistics, Settings, File, Shell, Enumeration, and Zephyr management groups. Out of the box, the most useful to us has been the file system group and the image group.

The SMP Protocol depends on a separate SMP Transport Specification. Available transports target Bluetooth Low Energy (BLE), serial, and UDP. While details vary between transports, generally speaking this layer automatically handles splitting larger data into packets, transferring, and verifying the integrity on the other side. It is possible for SMP to coexist on the same UART that is used for the Zephyr Shell. If you need a different transport, you have the option to write your own.

Diagram showing the structure of an SMP frame. This includes differently colored blocks to represent data set during allocation, by the user, and by the subsystem.The SMP Protocol centers around the concept of Management Groups. Each management group has its own set of commands whose inputs and outputs are all generally the same. A request may include a CBOR encoded packet containing information necessary to perform the command. A response includes a CBOR encoded packet with error information (return code) and desired data returned from the server. Golioth regularly uses CBOR encoded data to reduce overhead and usage.

The limiting factor for SMP is the Network Buffer used to send commands and to receive data responses. This is configured using the CONFIG_MCUMGR_TRANSPORT_NETBUF_SIZE symbol (which defaults to 384 bytes). The setting determines the largest chunk of data (minus an 8-byte header) that may be transferred to or from the server using SMP.

Don’t Forego the Command Line Tools

One of the most useful parts of SMP is the ability to access the protocol between a computer and a microcontroller.

Let’s look at the example of the SMP Server example included in the Zephyr tree. While the bulk of this sample revolves around firmware update, the stat group is also included. Here is a demonstration of accessing stats using a USB-to-Serial cable:

➜ mcumgr stat list --conntype serial --connstring "dev=/dev/ttyUSB0,baud=115200"
stat groups:
    smp_svr_stats

➜ mcumgr stat smp_svr_stats --conntype serial --connstring "dev=/dev/ttyUSB0,baud=115200"
stat group: smp_svr_stats
       152 ticks

You can see that the first command lists the names of available stats on this device. The second queries the stat by name, returning the uptime in seconds. This is handy to validate your hardware setup, and there are some tools available like the python smpclient that we have in mind for automated testing of SMP-based applications.

Microcontroller-Based SMP Client

At the time of writing, all SMP Zephyr documentation focuses on the SMP Server. However, you can look at a few of the built-in management groups to see how the SMP client works in embedded systems. At its most basic:

  1. Allocate a net_buf that includes the SMP header for your request.
  2. (Optionally) add data to your request in CBOR format.
  3. Send the command.
  4. Handle the response in a callback.

Using the same stats example from above, here’s what sending a request looks like.

struct net_buf *stat_nb = smp_client_buf_allocation(&smp_client,
                                                    MGMT_GROUP_ID_STAT,
                                                    STAT_MGMT_ID_SHOW,
                                                    MGMT_OP_READ,
                                                    SMP_MCUMGR_VERSION_2);

if (!stat_nb)
{
    LOG_ERR("Failed to allocate net_buf");
    return;
}

zcbor_state_t zse[CONFIG_MCUMGR_SMP_CBOR_MAX_DECODING_LEVELS];

zcbor_new_encode_state(zse,
                       ARRAY_SIZE(zse),
                       stat_nb->data + stat_nb->len,
                       net_buf_tailroom(stat_nb),
                       0);

bool ok = zcbor_map_start_encode(zse, 1) && zcbor_tstr_put_lit(zse, "name")
    && zcbor_tstr_put_lit(zse, "smp_svr_stats") && zcbor_map_end_encode(zse, 2);

if (!ok)
{
    LOG_ERR("Failed to encode CBOR");
    smp_packet_free(stat_nb);
    return;
}

stat_nb->len = zse->payload - stat_nb->data;

int err = smp_client_send_cmd(&smp_client, stat_nb, on_stat, NULL, 1);
if (err)
{
    smp_packet_free(stat_nb);
    LOG_ERR("SMP Error: %d", err);
    return;
}

/* smp_clien_send_cmd() was successful, net_buf will automatically be freed after callback */

There are important things to consider in this request. There is a header generated during the allocation step, so your CBOR encoded data must begin after that header. When a command is successfully sent, the net_buf will automatically be freed by the callback, but if the command fails (no callback will be called) you need to free the memory.

Handling data in the callback is pretty simple.

int on_stat(struct net_buf *nb, void *user_data)
{
    if (!nb)
    {
        /* "Lack of response is only allowed when there is no SMP service or device is
         * non-responsive." */
        LOG_ERR("SMP STAT_SHOW command timed out");
        return -ENODATA;
    }

    if ((nb->len == 2) && (nb->data[0] == 0xBF) && (nb->data[1] == 0xFF))
    {
        /* "Note that in the case of a successful command, an empty map will be returned" */
        LOG_INF("Successfully updated state for LED%d", led_num);
        return 0;
    }

    LOG_HEXDUMP_DBG(nb->data, nb->len, "on_stat");

    return 0;
}

This example does not decode the CBOR returned by the server (and in this example, remember ‘server’ will be another microcontroller). However, it does illustrate important aspects of determining the status of the response:

  1. If the net_buf is NULL, the request timed out
  2. If the CBOR map is empty ( payload is [0xBF, 0xFF] ) the operation was successful. This will never be the case with this particular command as data is always returned, but it’s here for illustration.
  3. If the management group returns an error, it will be encoded using rc as the key and a custom error code from the management group as the value.

In our case, the server returns the following data (the raw CBOR is shown first, followed by the decoded values):

bf 64 6e 61 6d 65 6d 73  6d 70 5f 73 76 72 5f 73
74 61 74 73 66 66 69 65  6c 64 73 bf 65 74 69 63
6b 73 0d ff ff                                  

{_ "name": "smp_svr_stats", "fields": {_ "ticks": 13}}

I used the excellent cbor.me online tool to perform the decoding using the output from LOG_HEXDUMP_DBG().

Writing Custom Management Groups

A complete guide on custom management groups is a post in itself. But this is one area in which the Zephyr documentation shines. The best resource for standing up your own custom groups is the MCUmgr handlers page.

As a high-level overview, Zephyr reserves all management group numbers from 64 up to 255 for application and module use. Each group may register up to 256 commands, each with callbacks for a separate read and write operation. So you write your callbacks, put those functions into an API-like struct, and then register the group using that struct. All is explained on the page linked above.

Golioth and SMP

One need we often hear from our customers is the ability to push Over-the-Air updates from Golioth to downstream devices. Consider a system that has one controller that handles internet connectivity, with several devices connected locally (Bluetooth, canbus, modbus, i2c, spi, uart, etc). Earlier last year we demonstrated a Multi-MCU firmware update. It performs a firmware update of an nrf52840 by passing the firmware from Golioth, through an nrf9160 (cellular) over the UART transport using SMP.

It’s easy to see that Golioth makes firmware updates for internet-connected devices a snap. But combine Golioth Cohorts, our improved OTA event log, and interconnectivity tools like SMP, and you end up with a powerful OTA scheme for all of the controllers in a complex system.

Mike Szczys
Mike Szczys
Mike is a Firmware Engineer at Golioth. His deep love of microcontrollers began in the early 2000s, growing from the desire to make more of the BEAM robotics he was building. During his 12 years at Hackaday (eight of them as Editor in Chief), he had a front-row seat for the growth of the industry, and was active in developing a number of custom electronic conference badges. When he's not reading data sheets he's busy as an orchestra musician in Madison, Wisconsin.

Post Comments

No comments yet! Start the discussion at forum.golioth.io

More from this author

Related posts

spot_img

Latest posts

Golioth Design Partners: IoT Solutions for Constrained Devices | 2025 Partner Network

We regularly refer Golioth users to our trusted Design Partners to help design, test, and deploy hardware out into the world. The Golioth Design Partner program has been going for more than 2 years and continues growing. In 2025, we reached 20 listed partners, with others in the wings.

Adding Golioth Example Code to Your ESP-IDF Project

In our previous blog post, we demonstrated how to add the Golioth Firmware SDK to an ESP-IDF project. As you start integrating Golioth into...

Tracking Our CEO at CES

We used Golioth Location to build an application that keeps up with Golioth CEO Jonathan Beri at CES 2025.

Want to stay up to date with the latest news?

We would love to hear from you! Please fill in your details and we will stay in touch. It's that simple!