Developing with ZBOSS for Zigbee
Programming principles

The ZBOSS stack provides the following services for the Zigbee application developer:

  • Zigbee Cluster Library (ZCL) definition and processing,
  • Commissioning according to the BDB profile,
  • Zigbee Device Object (ZDO) services,
  • Internal Zigbee stack data structures,
  • Lower level application framework: multitasking and memory management.

Application structure

The SDK provides header files and libraries for C programming language. This means that applications must be implemented using C or C++.

The typical application source must:

  • Include SDK header or headers.
  • Declare global ZCL data: attribute lists, clusters, endpoints, ZCL context.
  • Register ZCL endpoints and ZCL device context.
  • Optionally, set up defaults for the commissioning: network role, stack parameters (64-bit long address, security key, maximum number of children, etc.).
  • Start ZBOSS threads with zigbee_enable.
  • Implement zboss_signal_handler() to handle the commissioning events.
  • Implement ZCL device callback.
  • Implement ZCL-specific cluster command handler.

Zigbee type definitions

The stack uses a set of platform-independent type definitions such as:

  • common C types (char, integer, void, etc.),
  • specific types for core and Zigbee functionalities.

All ZBOSS type names start with the prefix "zb_".

Zigbee stack header files

The application must include the header file zboss_api.h, and do so before any other ZBOSS header files. This file contains most of the required routines to start the application.

Zigbee stack multitasking (scheduler)

The stack uses a cooperative multitasking model. The main component that provides multitasking capabilities is the scheduler.

Callbacks

There is no "task" abstraction in the scheduler; it uses "callback" instead. The callback has to match one of the following types:

Running a task in the scheduler simply executes a callback function.

Each callback function can schedule one or more callback functions for execution through the scheduler. The scheduled callbacks are stored in the internal scheduler queue. This queue is handled in the scheduler main loop: the callbacks are executed one-by-one in the FIFO order. If there is currently no callback to execute, the scheduler can put the device into sleep mode (if so configured).

Callbacks are scheduled for execution with:

A common practice is to specify a buffer ID as the first (or as the single) parameter for the callback. Scheduling a callback does not block the currently running function, as it is a fast non-blocking operation. More than one callback can be scheduled in succession. If there is no other callback in the queue, the scheduled callback is executed immediately after the current callback is finished.

Timer alarms

The timer functionality uses the scheduler infrastructure and is implemented with alarms. An alarm is a callback that is scheduled for execution with a defined time delay. The time delay is specified during the scheduling of the alarm and guarantees that the alarm callback function is called not earlier than specified by the delay parameter.

Note
Precise timing is not guaranteed: the alarm callback can be called later than the defined time delay.

There are API primitives provided to work with the alarms:

  • ZB_SCHEDULE_APP_ALARM is used to schedule a callback with a given parameter and a defined timeout. (Similar to callbacks, alarms are executed in the main scheduler loop.)
  • ZB_SCHEDULE_APP_ALARM_CANCEL is used to cancel a previously scheduled alarm.

Scheduler API

The following table lists the Scheduler API macros:

Macro Parameters Return value Description
ZB_SCHEDULE_APP_CALLBACK(func, param) zb_callback_t func – function to execute.
zb_uint8_t param – callback parameter – usually a buffer ID.
RET_OK or RET_OVERFLOW. Schedule a callback function with one parameter for execution.
ZB_SCHEDULE_APP_CALLBACK2(func, param, user_param) zb_callback_t func – function to execute.
zb_uint8_t param – callback parameter.
zb_uint16_t user_param – user parameter.
RET_OK or RET_OVERFLOW. Schedule a callback function with two parameters for execution.
ZB_SCHEDULE_APP_ALARM(func, param, timeout_bi) zb_callback_t func – function to execute.
zb_uint8_t param – callback parameter.
zb_time_t timeout_bi – timeout, in beacon intervals.
RET_OK or RET_OVERFLOW. Schedule an alarm for execution in the main scheduler loop: a callback with one parameter to be executed after a delay defined by the timeout value.
ZB_SCHEDULE_APP_ALARM_CANCEL(func, param) zb_callback_t func – scheduled function to cancel.
zb_uint8_t param – callback parameter.
RET_OK or RET_OVERFLOW. Cancel previously scheduled alarm.
ZB_SCHEDULE_GET_ALARM_TIME(func, param, timeout_bi) zb_callback_t func – function to execute.
zb_uint8_t param – callback parameter
zb_time_t timeout_bi – time left for an execution, in beacon intervals.
RET_OK or RET_NOT_FOUND. Check whether an alarm was scheduled and return time left for the callback execution.
Scheduler API calls
Note
Except for the scheduler API, the Zigbee stack API is not thread-safe. This means that calls to the Zigbee API must be invoked through scheduler callbacks or from the same thread that runs the scheduler loop. In NCS, the scheduler API is overloaded by functions that make it interrupt- and thread-safe. These functions can be found in nrf/subsys/zigbee/osif/zb_nrf_platform.h.

The following examples demonstrate the usage of Zigbee multitasking with two functions executing each other in an infinite loop. The first function executes immediately. The second function executes after a 10-second delay.

Zigbee stack memory management subsystem

The Zigbee stack uses static memory allocation for its internal data structures (the memory is not allocated dynamically at runtime). The memory management system provides predictable memory usage and minimizes data copying at runtime when possible. The internal stack data structures are usually invisible to the application.

The main data structure visible for the application is the memory buffer, which transfers packets and parameters between the application and stack layers.

The stack uses a predefined pool of memory buffers of a fixed size and has a number of default presets to define the size of the memory buffer pool. You can configure the pool by using one of the following header files:

  • zb_mem_config_min.h sets the minimal memory buffer pool of 20 buffers.
  • zb_mem_config_med.h sets the medium memory buffer pool of 32 buffers.
  • zb_mem_config_max.h sets the maximum memory buffer pool of 48 buffers.

A memory buffer consists of a header and a data buffer. The memory buffer size equals 128 bytes + buffer tail with a set of parameters. This is enough to hold a single Zigbee packet or an APS fragment. When the APS fragmentation is required on rare occasions, the ZBOSS stack uses extended buffers.

Note
The APS fragmentation is an optional feature of ZBOSS.

The application or the stack function can allocate or release a single memory buffer from the pool on demand. Memory buffer allocation is a blocking operation: ZBOSS stack calls the user's callback when the buffer becomes available.

The application must use zb_buf_get_out_delayed() or zb_buf_get_in_delayed() calls to allocate a new buffer. When a buffer becomes available, the delayed buffer allocation makes a callback call through the scheduler.

Logically, the memory buffers are divided into two types:

  • RX buffers for incoming packets;
  • TX buffers for outgoing packets.

This separation prevents allocating all memory buffers to only one type of buffer. For example, if all buffers are only TX buffers and all are in use waiting for an ACK packet, no ACK packet will be received because no RX buffer is available. To prevent this situation, the stack stops allocating TX buffers when half of the buffer pool is allocated to TX buffers. The same rule applies to allocating RX buffers.

Most of the stack functions require some parameters and some data (usually Zigbee packet or its fragment). Both parameters and data are passed to the API in a single memory buffer. Call parameters are stored in the tail of the buffer, while the data section is stored at the head of the buffer. Both the caller and the called functions must be aware of the parameter types.

For setting parameters and getting them from the buffer, ZB_BUF_GET_PARAM() is used.

The following buffer operations are available in a Zigbee application:

  • Allocate and deallocate buffers.
  • Allocate or deallocate space inside a buffer.
  • Add data at the head or the tail of a payload.
  • Read payload data (get data start, get data length, get buffer parameters).

To reserve a space in the memory buffer, use the following methods:

The following table lists the available memory management API macros.

Macro Parameters Return value Description
zb_buf_begin(bufid) zb_bufid_t bufid – memory buffer ID. void *data – data begin. Get a pointer to the first allocated data element in the data buffer.
zb_buf_len(bufid) zb_bufid_t bufid – memory buffer ID. zb_uint_t length – data length. Get the allocated data size.
zb_buf_initial_alloc(bufid, size) zb_bufid_t bufid – memory buffer ID.
zb_uint8_t size – size to allocate.
void *data – data begin. Allocate a buffer in the memory buffer. If possible, the buffer is allocated starting with an offset in the data buffer. The existing buffer content is lost.
zb_buf_alloc_left(bufid, size) zb_bufid_t bufid – memory buffer ID.
zb_uint8_t size – size to allocate.
void *data – data begin. Extend the currently allocated buffer to the left. Commonly used for allocating a buffer for a packet header.
zb_buf_alloc_right(bufid, size) zb_bufid_t bufid – memory buffer ID.
zb_uint8_t size – size to allocate.
void *data – data begin. Extend the currently allocated buffer to the right. Commonly used for allocating a buffer for a packet tail.
zb_buf_cut_left(bufid, size) zb_bufid_t bufid – memory buffer ID.
zb_uint8_t size – size to cut.
void *data – data begin. Cut a buffer beginning. This macro is normally used for cutting a packet header.
zb_buf_cut_right(bufid, size) zb_bufid_t bufid – memory buffer ID.
zb_uint8_t size – size to cut.
None. Cut a buffer ending. This macro is usually used for cutting a packet tail.
ZB_BUF_GET_PARAM(bufid, type) zb_bufid_t bufid – memory buffer ID.
type – requested parameter type.
type *ptr – pointer to the parameters. Get a pointer to the structured parameters. The parameters are stored at the tail of the data buffer.
zb_buf_reuse(bufid) zb_bufid_t bufid – memory buffer ID. None. Get the specified IN or OUT buffer, clear all the data, and prepare the buffer for further usage. The buffer does not change its type: if it was an IN buffer, it remains an IN buffer after the procedure.
zb_buf_free(bufid) zb_bufid_t bufid – memory buffer ID. None. Release a memory buffer.
zb_buf_get() None. zb_bufid_t bufid – memory buffer ID Get an IN buffer from the buffer pool. If no buffer is available, return NULL. If possible, use the zb_buf_get_in_delayed(callback) instead.
zb_buf_get_in_delayed(callback) zb_callback_t callback – pointer to a callback function. RET_OK or error code. Get an IN buffer from the buffer pool; call the specified callback when the buffer is available.
If the buffer is available, schedule the callback for execution immediately. Otherwise, wait until a buffer is available and then execute the callback.
zb_buf_get_out_delayed(callback) zb_callback_t callback – pointer to a callback function. RET_OK or error code. Get an OUT buffer from the buffer pool; call the specified callback when the buffer is available.
If the buffer is available, schedule the callback for execution immediately. Otherwise, wait until a buffer is available and then execute the callback.
zb_buf_get_out_delayed_ext(callback, arg, max_size) zb_callback2_t callback – pointer to a callback function.
zb_uint16_t arg – to be passed to callback.
zb_uint_t max_size – required maximum buffer payload size
RET_OK or error code Get an OUT buffer from the buffer pool. The behavior is the same as with zb_buf_get_out_delayed(), but a user parameter is passed to the callback as the second argument alongside the allocated buffer. Call this macro to use extended buffers.
zb_buf_get_in_delayed_ext(callback, arg, max_size) zb_callback_t callback – pointer to a callback function.
zb_uint16_t arg – to be passed to callback.
zb_uint_t max_size – required maximum buffer payload size
RET_OK or error code. Get an IN buffer from the buffer pool; call the specified callback when the buffer is available.
If the buffer is available, schedule the callback for execution immediately. Otherwise, wait until a buffer is available and then execute the callback. Call this macro to use extended buffers.
Memory management API calls

Time subsystem

The time subsystem is based on the hardware timer. It uses the special type zb_time_t, defined in the stack API. The time value is stored in ticks. Each tick is equal to one "beacon interval" (15.36 ms). The size of the zb_time_t type depends on the platform. The overflow is handled by the Time API.

The Time API provides the ability to:

  • Get the current value of the timer.
  • Compare two timestamps with "more or equal" operator (>=).
  • Add or subtract timestamps.
  • Convert beacon intervals to milliseconds and vice versa.

The following table lists the available time API macros.

Macro Parameters Return value Description
ZB_TIMER_GET() None. zb_time_t time Get current timer value in beacon intervals.
ZB_TIME_SUBTRACT(a, b) zb_time_t a – time value.
zb_time_t b – time value to subtract.
zb_time_t time – subtraction result. Subtract time value: a - b.
ZB_TIME_ADD(a, b) zb_time_t a – time value.
zb_time_t b – time value.
zb_ret_t time – addition result. Add time values: a + b.
ZB_TIME_GE(a, b) zb_callback_t func – first time value to compare.
zb_uint8_t param – second time value to compare.
ZB_TRUE if ab, ZB_FALSE otherwise. Compare time value a and value b, check if ab.
ZB_TIME_ONE_SECOND None. zb_time_t time – one second timeout. Constant: one second in beacon intervals.
ZB_MILLISECONDS_TO_BEACON_INTERVAL(ms) zb_uint16_t ms – number of milliseconds to convert. zb_time_t time – timeout in ms. Convert time from milliseconds to beacon intervals.
Time API calls

Debugging

The Zigbee stack provides API for tracing capabilities for customer support purposes. The stack outputs a hex-encoded binary trace through the Logger module. The binary trace log cannot be decoded with the provided SDK. However, the Nordic support team can use it to find stack-level issues .

You can enable or disable tracing for a particular subsystem, and define the level of tracing. In addition, the stack is able to trace Zigbee input and output traffic (in/out) on the MAC level.

The trace mask enables or disables trace messages for the whole subsystem:

Macro Value Description
TRACE_SUBSYSTEM_APP 0x0800 Application.
TRACE_SUBSYSTEM_ZDO 0x0040 ZDO subsystem.
TRACE_SUBSYSTEM_ZCL 0x0010 APS subsystem.
TRACE_SUBSYSTEM_NWK 0x0008 NWK subsystem.
Trace masks

Tracing is controlled by the trace level and the trace mask. The trace level defines the importance of the message from 1 to 4: 1 is the most important, 4 is the least important. Setting the trace level to 1 blocks all the messages except those with the highest priority. If an application sets the trace level to 4, all trace messages are allowed to print.

The provided tracing subsystem is disabled by default. Use the following tracing API to manage tracing.

Macro Parameters Return value Description
ZB_SET_TRACE_LEVEL(level) zb_uint8_t level – trace level. None. Set trace level.
ZB_SET_TRACE_MASK(mask) zb_uint8_t mask – trace mask. None. Set trace mask.
ZB_SET_TRACE_ON() None. None. Enable trace.
ZB_SET_TRACE_OFF() None. None. Disable trace.
ZB_SET_TRAF_DUMP_ON() None. None. Enable dump.
ZB_SET_TRAF_DUMP_OFF() None. None. Disable dump.
Trace API calls

Power saving feature

The ZBOSS stack informs the application about how long its scheduler is empty by using the signal handler's ZB_COMMON_SIGNAL_CAN_SLEEP.

The mechanism starts with the scheduler that tracks the callback queue (see Zigbee stack multitasking (scheduler)). If there is no immediate callback for the execution and there is no outgoing packet on the MAC level, a special signal ZB_COMMON_SIGNAL_CAN_SLEEP is sent to the application. This signal is sent when the ZBOSS scheduler is empty for some time, and it is sent regardless of the device role. It has a sleep time parameter that specifies the allowed sleep duration in milliseconds.

Upon receiving the signal, the application checks its internal state and decides whether to enter the sleep mode or not. To switch to the sleep mode, the application must call the zb_sleep_now() function.

After calling zb_sleep_now(), the stack prepares its peripherals for sleep, and informs the application that it is ready to go to sleep with a call to zb_osif_sleep(). The sleep duration value is based on the time remaining before the first alarm is called, and it is longer for the sleepy end device.

The application can use zb_sleep_set_threshold() to set the minimum sleep duration value, which reduces the number of the triggered ZB_COMMON_SIGNAL_CAN_SLEEP signals. If the sleep duration is lower than the specified threshold, the signal is not triggered. By default, the value of the sleep duration threshold ZB_SCHED_SLEEP_THRESHOLD_MS is set to 20 ms. The upper limit for this value is 86400000 ms (one day).

APIs
The following table provides an overview of the power saving sleep API.
Macro Parameters Return value Description
zb_ret_t zb_sleep_set_threshold(zb_uint32_t threshold_ms) threshold_ms -– sleep threshold value in milliseconds. RET_OK – If the interval is successfully set.
RET_ERROR – Otherwise.
Set threshold value.
By default, set to ZB_SCHED_SLEEP_THRESHOLD_MS (20 ms).
zb_uint32_t zb_get_sleep_threshold() None. Currently used threshold value.
zb_sleep_now() None. None. Immediately put the device into sleep mode. This call is permitted only in signal handler when application receives the ZB_COMMON_SIGNAL_CAN_SLEEP signal.
Power saving sleep API calls

Configuring sleepy behavior for end devices

To enable the sleepy behavior on an end device, make sure your application calls the zb_set_rx_on_when_idle() function with the ZB_FALSE parameter, but before starting the ZBOSS stack. This function specifies whether the device should have the radio disabled between the polls to parent.

As a result, during the preparation of peripherals for sleep, the stack will additionally turn off Sleepy End device radio and its own timer.


End device keep alive

All end devices (including RxOnWhenIdle=TRUE) that have received an End Device Timeout Response Command with a status SUCCESS can periodically send keep alive messages to their router parent to ensure they remain in the router’s neighbor table.

You can configure the end device timeout period with zb_set_ed_timeout. The default timeout that is set in NWK_ED_DEVICE_TIMEOUT_DEFAULT equals 256 minutes. If the parent does not receive any keep alive messages from the child during this period, it deletes the child from the neighbor table.

The period for sending the keep alive to the router parent must be determined by the manufacturer of the device and is not specified by the standard. It is recommended to set this period in such a way that the end device sends by default 3 keep alive messages during the End Device Timeout period. The application can use zb_set_keepalive_timeout to configure how often the keep alive message is sent.

The keep alive method that the end device uses depends on the support of the router parent. It can be MAC data poll keep alive (which is the most common option), or End Device Timeout Request keep alive.

If the router parent supports the MAC data poll keep alive, the keep alive configuration may conflict with the Long Poll configuration. This happens when the application changes the Long Poll interval using the Poll Control Cluster API or using zb_zdo_pim_set_long_poll_interval. It is recommended to set the Long Poll interval to a value smaller than ((End Device Timeout period) / 3) to avoid unexpected device aging.

If the router parent supports the End Device Timeout Request keep alive, then the device sends an End Device Timeout Request command as a unicast and waits for an End Device Timeout Response from its parent, according to the configured Keep Alive Timeout.


Data polling mechanism

The polling intervals can be configured by the Poll control ZCL cluster commands or by zb_zdo_pim_set_long_poll_interval.

There are two standard polling presets:

The device always uses the long poll preset, unless there is an ongoing communication that requires a higher packet exchange rate.

Turbo poll

The turbo poll is a ZBOSS extension that allows to temporary increase the poll rate when ZED is awaiting an answer from the parent. This is useful for actions when you know either the exact number of packets to be received or the time period when these packets are expected to be received.

The extension implements an adaptive algorithm that lowers the poll interval to the configured value and raises it gradually up to the long poll interval when the parent is not responding. The turbo poll can be enabled and disabled by zb_zdo_pim_permit_turbo_poll.

When the turbo poll is enabled, it starts automatically for the specified number of packets when one of the following conditions is met:

  • ZED sends a command that should be acknowledged on the APS layer.
  • TCLK transmission error occurred.
  • ZDO or ZCL command expects a response.

In each of these cases, the turbo poll uses the adaptive algorithm until the expected number of packets is received.

The application can start the turbo poll for the specified number of packets by calling zb_zdo_pim_start_turbo_poll_packets.

The turbo poll can also be forced for a certain period of time by calling zb_zdo_pim_start_turbo_poll_continuous, and stopped by calling zb_zdo_pim_turbo_poll_continuous_leave when the application expects multiple packets during this period. In this case, the turbo poll uses the adaptive algorithm until the timeout expiration. For instance, the application may enable the turbo poll during the initial configuration of Sleepy End Device to speed up the communication process.


Production configuration

Zigbee stack includes a production configuration block feature. This block is useful for per-device customization and is supposed to be written at device production time. The size of the production block must not exceed 128 bytes.

The production configuration block:

  • Is created dynamically during the build process. You can check the partition addresses in the build/zephyr/include/generated/pm_config.h file. To place the production config partition at a specific address, use static configuration. For more information, see Partition Manager in the nRF Connect SDK documentation.
  • Is not directly visible to the application. Zigbee stack reads it during initialization and sets its internal parameters.
  • Must be written to the flash before launching the application.

The block is loaded at the very start of zboss_main_loop_iteration(), so it rewrites all the corresponding data from NVRAM.

The production configuration block has a system part and an optional application-specific part:

Data that can be placed into the system part of the production configuration block includes:

  • 802.15.4 channel mask
  • Device IEEE address
  • Definition of the maximum TX power per channel per page
  • Install code of the device

Moreover, the data specific to your application can be placed in the application part of the production configuration block.

Note
- The size of the application block of production configuration is limited to 74 bytes for first version of production config. It may change across versions because newer versions of the production configuration may include additional data.
zb_uint8_t
unsigned char zb_uint8_t
Project-local 1-byte unsigned int type.
Definition: zb_types.h:153
zb_ret_t
zb_int32_t zb_ret_t
Return type for ZB functions returning execution status.
Definition: zb_errors.h:33
zb_get_app_signal
zb_zdo_app_signal_type_t zb_get_app_signal(zb_uint8_t param, zb_zdo_app_signal_hdr_t **sg_p)
Unpack application signal buffer in zboss_signal_handler()
ZB_GET_APP_SIGNAL_STATUS
#define ZB_GET_APP_SIGNAL_STATUS(param)
Get status from the application signal.
Definition: zboss_api_zdo.h:1251
ZB_ZDO_SIGNAL_DEFAULT_START
#define ZB_ZDO_SIGNAL_DEFAULT_START
Definition: zboss_api_zdo.h:130
ZB_BDB_SIGNAL_DEVICE_FIRST_START
#define ZB_BDB_SIGNAL_DEVICE_FIRST_START
Definition: zboss_api_zdo.h:214
zb_uint32_t
unsigned int zb_uint32_t
Project-local 4-byte unsigned int type.
Definition: zb_types.h:201
ZB_SCHEDULE_APP_CALLBACK
#define ZB_SCHEDULE_APP_CALLBACK(func, param)
Definition: zboss_api_core.h:319
ZB_SCHEDULE_APP_ALARM
#define ZB_SCHEDULE_APP_ALARM(func, param, timeout_bi)
Definition: zboss_api_core.h:370
ZB_ZDO_SIGNAL_PRODUCTION_CONFIG_READY
#define ZB_ZDO_SIGNAL_PRODUCTION_CONFIG_READY
Definition: zboss_api_zdo.h:503
ZB_INIT
#define ZB_INIT(trace_comment)
Definition: zboss_api.h:462
zb_zdo_app_signal_hdr_t
struct zb_zdo_app_signal_hdr_s zb_zdo_app_signal_hdr_t
zboss_start
zb_ret_t zboss_start(void)
ZBOSS start function.
RET_OK
#define RET_OK
Error codes for non-void stack functions. In general, function can return OK, BLOCKED or some error....
Definition: zb_errors.h:78
ZB_ZDO_SIGNAL_SKIP_STARTUP
#define ZB_ZDO_SIGNAL_SKIP_STARTUP
Definition: zboss_api_zdo.h:149
ZB_TIME_ONE_SECOND
#define ZB_TIME_ONE_SECOND
Definition: zboss_api_core.h:172
zb_zdo_app_signal_hdr_s
Definition: zboss_api_zdo.h:1201
zboss_main_loop_iteration
void zboss_main_loop_iteration(void)
zboss_signal_handler
void zboss_signal_handler(zb_uint8_t param)
zb_buf_free
#define zb_buf_free(buf)
Free packet buffer and put it into free list.
Definition: zboss_api_buf.h:333
zb_buf_len
#define zb_buf_len(buf)
Definition: zboss_api_buf.h:361
ZB_ZDO_SIGNAL_GET_PARAMS
#define ZB_ZDO_SIGNAL_GET_PARAMS(sg_p, type)
Definition: zboss_api_zdo.h:876
zb_bufid_t
zb_uint8_t zb_bufid_t
Definition: zboss_api_buf.h:172
ZB_BDB_SIGNAL_DEVICE_REBOOT
#define ZB_BDB_SIGNAL_DEVICE_REBOOT
Definition: zboss_api_zdo.h:233