nRF5 SDK for Thread and Zigbee v3.0.0
Programming principles

Table of Contents

The nRF5 SDK for Thread and Zigbee provides the following services for the Zigbee application developer:

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:

Zigbee type definitions

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

The framework includes a set of platform-independent type definitions such as common C types (char, integer, void etc.), and specific types for core and Zigbee functionality.

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

Zigbee stack header files

The application must include the header file zboss_api.h, and include it 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 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 reference 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:

Scheduler API

The following table lists the Scheduler API macros:

Macro Parameters Return value Description
ZB_SCHEDULE_CALLBACK(func, param) zb_callback_t func – function to execute.
zb_uint8_t param – callback parameter – usually a reference to a memory buffer.
RET_OK or error code. Schedule a callback function with one parameter for execution.
ZB_SCHEDULE_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 error code. Schedule a callback function with two parameters for execution.
ZB_SCHEDULE_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 error code. 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_ALARM_CANCEL(func, param) zb_callback_t func – scheduled function to cancel.
zb_uint8_t param – callback parameter.
RET_OK or error code. Cancel previously scheduled alarm.
ZB_SCHEDULE_ALARM_CANCEL_AND_GET_BUF(func, param, p_param) zb_callback_t func – scheduled function to cancel.
zb_uint8_t param – callback parameter.
zb_uint8_t p_param – returned parameter.
RET_OK or error code.Cancel previously scheduled alarm and return a parameter defined while scheduling. This API is used to prevent buffer leak.
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 error code. 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.

The following example demonstrates 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.

void func1(zb_uint8_t param)
{
ZB_SCHEDULE_CALLBACK(func2, param);
}
void func2(zb_uint8_t param)
{
ZB_SCHEDULE_ALARM (func1, param, 10 * ZB_TIME_ONE_SECOND));
}
void main()
{
func1(1);
ZBOSS_INIT();
while(1)
{
}
}

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. 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. A memory buffer consists of a header and a data buffer. The memory buffer size is enough to hold a single Zigbee packet or an APS fragment. When the APS fragmentation is required on rare occasions, the stack uses memory buffer chaining.

Note
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 kernel calls the user’s callback when the buffer becomes available.

The application must use ZB_GET_OUT_BUF_DELAYED() or ZB_GET_IN_BUF_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: in and out buffers, for incoming and outgoing packets respectively. This separation prevents allocating all the 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 for 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.

There are two methods for setting parameters and getting them from the buffer, respectively:

A recommended practice is to use buffer references instead of zb_buf_t* buffer pointers. The buffer reference is a single byte variable that holds the buffer ID.

The stack provides two helpers to get either the buffer by its reference or the reference of a particular buffer, respectively:

The following buffer operations are available in a Zigbee application:

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_FROM_REF(ref) zb_uint8_t ref – memory buffer reference. zb_buf_t *buf – memory buffer. Get a pointer to the memory buffer using its reference.
ZB_REF_FROM_BUF(buf) zb_buf_t *buf – memory buffer. zb_uint8_t ref – buffer reference. Get a reference (index) to the memory buffer.
ZB_BUF_BEGIN(buf) zb_buf_t *buf – memory buffer. zb_uint8_t *data – data begin. Get a pointer to the first allocated data element in the data buffer.
ZB_BUF_LEN(buf) zb_buf_t *buf – memory buffer. zb_uint8_t length – data length. Get the allocated data size.
ZB_BUF_INITIAL_ALLOC(buf, size, ptr) zb_buf_t *buf – memory buffer.
zb_uint8_t size – size to allocate.
zb_void_t *ptr – (out) pointer to the allocated data buffer.
None. 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(buf, size, ptr) zb_buf_t *buf – memory buffer.
zb_uint8_t size – size to allocate.
zb_void_t *ptr – (out) pointer to the allocated buffer.
None. Extend the currently allocated buffer to the left. Commonly used for allocating a buffer for a packet header.
ZB_BUF_ALLOC_RIGHT(buf, size, ptr) zb_buf_t *buf – memory buffer.
zb_uint8_t size – size to allocate.
zb_void_t *ptr – (out) pointer to the allocated buffer.
None. Extend the currently allocated buffer to the right. Commonly used for allocating a buffer for a packet tail.
ZB_BUF_CUT_LEFT(buf, size, ptr) zb_buf_t *buf – memory buffer.
zb_uint8_t size – size to cut.
zb_void_t *ptr – (out) pointer to new buffer beginning.
None. Cut a buffer beginning. It is normally used for cutting a packet header.
ZB_BUF_CUT_RIGHT(buf, size) zb_buf_t *buf – memory buffer.
zb_uint8_t size – size to cut.
None. Cut a buffer ending. It is usually used for cutting a packet tail.
ZB_GET_BUF_PARAM(buf, type) zb_buf_t *buf – memory buffer.
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_SET_BUF_PARAM_PTR(buf, param, type) zb_buf_t *buf – memory buffer.
zb_void_t *param – pointer to the user data.
type – parameter type.
None.Store the structured parameters. The parameters are stored at the tail of the data buffer.
ZB_BUF_REUSE(buf) zb_buf_t *buf – memory buffer. None. Reuse a buffer, discard data, re-initialize internals of the memory buffer.
ZB_FREE_BUF(buf) zb_buf_t *buf – memory buffer. None. Release a memory buffer.
ZB_GET_IN_BUF() None. zb_buf_t *buf – memory buffer. Get an IN buffer from the buffer pool. If no buffer is available, return NULL.
ZB_GET_OUT_BUF() None. zb_buf_t *buf – memory buffer. Get an OUT buffer from the buffer pool. If no buffer is available, return NULL.
ZB_GET_IN_BUF_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 execute the callback then.
ZB_GET_OUT_BUF_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 execute the callback then.
ZB_GET_OUT_BUF_DELAYED2(callback, user_param) zb_callback2_t callback – pointer to a callback function.
zb_uint16_t user_param – to be passed to callback.
RET_OK or error code Get OUT buffer from the buffer pool. The behavior is the same with ZB_GET_OUT_BUF_DELAYED(), but a user parameter is passed to the callback as the second argument alongside the allocated buffer.
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:

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 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, but it is a very powerful tool for finding stack-level issues by Nordic's support team.

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/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 (1-4) and the trace mask. The trace level defines the importance of the message: 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.

To use the trace libraries, you must link your project with them. They are located in the <InstallFolder>\external\zboss\lib\debug folder.

It is also recommended to set te following values in the sdk_config.h file if you want to gather the traces:

Configuration option Value
NRF_LOG_DEFAULT_LEVEL 4
NRF_LOG_BUFSIZE 65536
NRF_LOG_MSGPOOL_ELEMENT_COUNT 32
NRF_LOG_MSGPOOL_ELEMENT_SIZE 40
NRF_LOG_BACKEND_RTT_TX_RETRY_CNT 100
SEGGER_RTT_CONFIG_BUFFER_SIZE_UP 32768

The provided tracing subsystem is disabled by default. Use the following tracing API to manage tracing: enable and disable subsystems, set trace level, etc.

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 optimization

By default, the Zigbee examples are power optimized. This means that in the time of inactivity the CPU enters the low power mode. The Sleepy End devices use the signal feature to know when to sleep (see Power saving for ZED), while the other devices enter the low power mode whenever the stack decides it has nothing more to do. The weak function zb_void_t zb_osif_go_idle(zb_void_t) is defined to check if the logger has anything to process. The CPU goes idle if nothing is left to process.

If some other checks are to be performed (or the behavior must be changed in any way), you can redefine this function in the application. Check the example in the Light Bulb device (see Zigbee Light Control example).

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 it in such a way that the end device sends 3 keep alive messages during the End Device Timeout period and it is set so by default.

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 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 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 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 long poll unless there is an ongoing communication requiring higher packet exchange rate.

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

It 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. Turbo poll can be enabled/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:

In this case, the Turbo poll uses the adaptive algorithm until the expected number of packets is received. The application can start 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 in order 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 and never changed after that.

The production configuration block:

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

The Zigbee stack applies the system part internally, and the application can handle its specific part by itself:

if (status != RET_OK)
{
NRF_LOG_WARNING("Production config is not present or invalid");
}
else
{
zb_uint32_t app_data_length = ZB_BUF_LEN(ZB_BUF_FROM_REF(param)) - sizeof(zb_zdo_app_signal_hdr_t);
if (app_data_length != 0)
{
NRF_LOG_INFO("Application configuration data received; length: %d", app_data_length);
example_application_config_t * ex_cfg = ZB_ZDO_SIGNAL_GET_PARAMS(p_sg_p, example_application_config_t);
process_example_application_config(ex_cfg);
}
NRF_LOG_INFO("Applied Production config");
}
break;

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

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

Note
After the production configuration is applied, do not call functions that would overwrite this data (zb_set_long_address() and so on).
An alternative way to change the long address for the final product is to modify the zb_osif_get_ieee_eui64(ieee_addr) function. When used during development, the function uses a random EUI64 address from the FICR registry. Modify it to use your own OUI.

You can generate the production configuration hexfile with the nrfutil utility, which takes the production configuration yaml text file and generates the binary in Intel hex format out of it.

Example of yaml input

channel_mask: 0x00100000
install_code: 83FED3407A939723A5C639B26916D505
extended_address: AABBCCDDEEFF0011
tx_power: 9
app_data: 01ABCD

Apart from the system part parameters, the production configuration includes the application configuration app_data made of three bytes: 0x01, 0xAB, 0xCD.


Documentation feedback | Developer Zone | Subscribe | Updated