Utilities#

Some of the commonly used functionalities are handled by utilities, which can be adapted by the developer for an application.

AGI Module#

The AGI module is part of CC AGI. Please see Association Group Information CC.

Association Module#

The Association Module is a common module to CC Association and CC Multi Channel Association. See the doxygen output.

Interfacing Firmware Update Module “ota_util”#

ota_util is a part of Command Class Firmware Update Meta Data. See the Doxygen output for information about how to use this command class.

Peripheral Drivers#

Several peripheral drivers are available (EMDRV and EMLIB) and must be linked to the application before the hardware device in question can be accessed. EMDRV exist on top of the lower level EMLIB. Source code is also available for customization.

The Z-Wave 700 SDK is event-driven that requires an event-driven design to access the hardware device. Calls are already implemented for handling GPIOs (see section below). Refer to links below for further details.

Software Documentation (e.g., EMDRV and EMLIB)

https://siliconlabs.github.io/Gecko_SDK_Doc/efr32fg13/html/index.html

Technical Resource Search (Application Notes)

https://www.silabs.com/support/resources.ct-application-notes.ct-example-code.p-microcontrollers_32-bit-mcus?

32-bit Peripheral Examples (EMLIB)

https://github.com/SiliconLabs/peripheral_examples

GPIO#

For the Wireless Starter Kit Mainboard (BRD4001A) with a ZGM130S Radio Board (BRD4202A) and a Buttons and LEDs EXP Board (BRD8029A), the file board.c provides a high-level support for buttons and LEDs (Note: board.c does not currently make use of the EFR32 Flex Gecko board support packages).

The mapping of buttons and LEDs to actual GPIO ports and pins are defined in the two header files: extension_board_4001a.h and radio_board_zgm130s.h included with board.h.

The board should be initialized with Board_Init() from ApplicationInit().

ZW_APPLICATION_STATUS **ApplicationInit**(EResetReason_t eResetReason)
{
  :
  **Board\_Init**();
  :
}

To subscribe to button events from a specific button, simply call Board_EnableButton() from the application thread.

void **Board\_EnableButton**(button_id_t btn);

The application will then receive events of type BUTTON_EVENT (<btn_id>_DOWN, <btn_id>_UP, <btn_id>_SHORT_PRESS, <btn_id>_HOLD, <btn_id>_LONG_PRESS).

LEDs are controlled with the Board_SetLed() function that simply takes an LED_ON or LED_OFF value as parameter:

void **Board\_SetLed**(led_id_t led, led_state_t state);

GPIO Port Usage in Serial API#

The GPIO port usage in the serial API applications for EFR32ZG14 and ZGM130S respectively are as follows:

GPIO

EFR32ZG14 Usage

PA0

UART/VCOM TX

PA1

UART/VCOM RX

PB11

For future use

PB12

For future use

PB13

For future use

PB14

SAW Filter Select 1

PB15

SAW Filter Select 2

PC10

For future use

PC11

For future use

PD13

For future use

PD14

VCOM Enable

PD15

For future use

PF0

Serial Wire Debug Clock

PF1

Serial Wire Debug Data I/O

PF2

Serial Wire Debug Output

PF3

For future use

GPIO

ZGM130S Usage

PA0

UART0/VCOM TX

PA1

UART0/VCOM RX

PA2

UART0/VCOM CTS

PA3

UART0/VCOM RTS

PA4

For future use

PA5

VCOM Enable

PB11

For future use

PB12

For future use

PB13

For future use

PB14

SAW Filter Select 1

PB15

SAW Filter Select 2

PC6

For future use

PC7

For future use

PC8

For future use

PC9

For future use

PC10

For future use

PC11

For future use

PD9

For future use

PD10

For future use

PD11

For future use

PD12

For future use

PD13

For future use

PD14

For future use

PD15

For future use

PF0

Serial Wire Debug Clock

PF1

Serial Wire Debug Data I/O

PF2

Serial Wire Debug Output

PF3

For future use

PF4

For future use

PF5

For future use

PF6

For future use

PF7

For future use

UART Driver#

The UART driver uses the UART RX interrupt to wake up the application when it has collected enough relevant data. This is done as follows:

  1. Define a new application event, e.g., EAPPLICATIONEVENT_SERIALDATARX in the enum EApplicationEvent.

  2. Install an event handler (this is a callback) in static const EventDistributorEventHandler g_aEventHandlerTable.

    Trigger the event from the ISR
    Void USART0_RX_IRQHandler() {
    … Collect data …
    
      If(enough_data) {
        xTaskNotifyFromISR(g_AppTaskHandle,
          1\<\< EAPPLICATIONEVENT_SERIALDATARX,
          eSetBits,
          NULL);
      }
    }
  3. Enable the UART RX IRQ (for more information, read the standard EMLIB documentation such as [21]).

The event handler is called in the application thread context, which ensures thread safety in the application.

UART communication settings#

The UART interface of the serial API application uses the following settings:

Parameter

Value

Baud rate

115200 bits/s

Parity

No

Data bits

8

Stop bits

1

The least significant bit (LSB) of each byte MUST be transmitted first on the physical wire.

ADC Driver#

The Z-Wave Application Framework contains an API for measuring the supply voltage using the Analog-to-Digital Converter (ADC) of the ZGM130S.

Applications can use this API to get the current supply voltage level. The typical use case is for obtaining the battery status in battery operated devices.

The API consists of the following functions:

Function:

Description:

void ZAF_ADC_Enable(void)

Initialize and enables the ADC

void ZAF_ADC_Disable(void)

Disables the ADC

uint32_t ZAF_ADC_Measure_VSupply(void)

Returns the supply voltage in millivolts

API function prototypes are defined in the following header file, which much be included from the Application:

#include "ZAF_adc.h"

Typical usage example:

ZAF_ADC_Enable(); // Enable the ADC

VBATT_mV = ZAF_ADC_Measure_VSupply(); // Read the battery voltage

ZAF_ADC_Disable(); // All done - disable the ADC

Further usage examples can be found in the certified Z-Wave applications DoorLockKeyPad and SensorPIR.

Event Distributor#

The objectives of the Event Distributor are to receive events on several FIFO message queues, wake up the application task whenever a message is available, and call the event handler associated with each queue.

Event Loop#

A Z-Wave application task is basically an endless event loop calling EventDistributorDistribute().

ApplicationTask(SApplicationHandles* pAppHandles)
{
  /* Task initialization */
  :
  /* Endless event loop */
  for (;;)
  {
    EventDistributorDistribute(&g_EventDistributor, 10, 0);
  }
}

The events to the application task arrive on several queues. If none of the queues has messages to read, the application task will sleep for a number of milliseconds (10 ms in the example above). Whenever a message arrives on a queue, the application task will wake up and dispatch the message to one of the configured functions.

The first parameter of type SEventDistributor contains configuration data for the event distributor.

uint32_t **EventDistributorDistribute**(const SEventDistributor\* pThis,
                                        uint32_t iEventWait,
                                        uint32_t NotificationClearMask);

The SEventDistributor variable used with EventDistributorDistribute() must first be initialized with EventDistributorConfig():

EEventDistributorStatus EventDistributorConfig(
                            SEventDistributor* pThis,
                            uint8_t iEventHandlerTableSize,
                            const EventDistributorEventHandler* pEventHandlerTable,
                            void (*pNoEvent)(void)
);

The pEventHandlerTable is an array of EventDistributorEventHandler, which is simply a function pointer to the event handler functions.

typedef void (\***EventDistributorEventHandler**)(void);

Note: The parameter pNoEvent, if non-null, will be called every time EventDistributorDistribute() wakes up and finds that there are no messages to process.

Each message queue is assigned a notification bit number. The order of event handler functions in pEventHandlerTable must correspond to those bit numbers; e.g., when a message arrives on the queue with notification bit number 2, the event handler function at array index 2 in pEventHandlerTable will be called.

The bit numbers are conveniently defined with the EApplicationEvent enumeration:

typedef enum
{
  EAPPLICATIONEVENT_TIMER = 0,
  EAPPLICATIONEVENT_ZWRX,
  EAPPLICATIONEVENT_ZWCOMMANDSTATUS,
  EAPPLICATIONEVENT_APP
  } EApplicationEvent;

The event handler table to use with EventDistributorConfig() can then be defined like this:

static const EventDistributorEventHandler m_aEventHandlerTable[] =
{
  AppTimerNotificationHandler,
  EventHandlerZwRx,
  EventHandlerZwCommandStatus,
  EventHandlerApp
};

An implementation of AppTimerNotificationHandler() is provided by the framework, the other remaining functions must be implemented by the application developer.

Event Queues#

The queue creation (and notification bit assignment) for message queues receiving the Z-Wave packages and Z-Wave command results are specified in ApplicationInit() with ZW\_UserTask\_ApplicationRegisterTask(). Application timer events are not placed on a queue, but are sent to the application using the notification interface and must therefore be assigned a notification bit number by calling AppTimerInit():

ZW_APPLICATION_STATUS ApplicationInit(EResetReason_t eResetReason)
{
  :
  AppTimerInit(EAPPLICATIONEVENT\_TIMER, NULL);
  :
  ZW\_UserTask\_ApplicationRegisterTask(ApplicationTask,
                                           EAPPLICATIONEVENT\_ZWRX,
                                           EAPPLICATIONEVENT\_ZWCOMMANDSTATUS,                                  
                                           &ProtocolConfig);
  :
}

The application message queue must be created with the FreeRTOS function xQueueCreateStatic(). First, a storage space for the queue must be defined (in this example, we are allocating a space for 5 application events on the queue). After the queue has been created, it must be assigned the application notification bit with QueueNotifyingInit(). Finally, the ZAF\_EventHelper module must be configured to use the application queue:

static EVENT_APP eventQueueStorage[5];
static StaticQueue_t m_AppEventQueueObject;
static QueueHandle_t m_AppEventQueueHandle;
static SQueueNotifying m_AppEventNotifyingQueue;
static TaskHandle_t m_AppTaskHandle;

void ApplicationTask(SApplicationHandles* pAppHandles)
{
  m_AppTaskHandle = xTaskGetCurrentTaskHandle();
  :
  m_AppEventQueueHandle = xQueueCreateStatic(sizeof_array(eventQueueStorage),
                                             sizeof(eventQueueStorage[0]),
                                             (uint8_t*)eventQueueStorage,
                                             &m_AppEventQueueObject);

  QueueNotifyingInit(&m_AppEventNotifyingQueue,
                     m_AppEventQueueHandle,
                     m_AppTaskHandle,
                     EAPPLICATIONEVENT_APP);

  ZAF_EventHelperInit(&m_AppEventNotifyingQueue);
  :
}

The ZAF\_EventHelper module provides a simple way of placing events on the application event queue:

/* File: zaf_event_helper.h */
void ZAF_EventHelperInit(SQueueNotifying * pQueueNotifyingHandle);
bool ZAF_EventHelperEventEnqueue(const uint8_t event);
bool ZAF_EventHelperEventEnqueueFromISR(const uint8_t event);

Event Handler#

Each of the event handlers in the EventDistributorEventHandler table used when calling EventDistributorConfig() is simply called when one or more messages are available on its message queue. The event handler must receive and process all available messages from the queue. For example, the application event handler will typically be implemented as follows, where all events are simply forwarded to the application state manager function:

static void EventHandlerApp(void)
{
  uint8_t event;
  while (xQueueReceive(m_AppEventQueue, &event, 0) == pdTRUE)
  {
    AppStateManager((EVENT_APP)event);
  }
}

Job Event Queue#

Applications often have the need to temporarily queue up events that first should be processed when the active job is finished. For this reason, the ZAF\_JobHelper module provides its own job event queue that is not handled by the Event Distributor. The application must explicitly enqueue and dequeue events on the job queue as needed. First, the application must call ZAF\_JobHelperInit() to initialize the job event queue to hold JOB\_QUEUE\_BUFFER\_SIZE events. The ZAF\_JobHelperJobEnqueue() and ZAF\_JobHelperJobDequeue() can then be used to put and get events on the queue.

/* File: zaf_job_helper.h */
#define JOB_QUEUE_BUFFER_SIZE 3

void ZAF_JobHelperInit(void);
bool ZAF_JobHelperJobEnqueue(uint8_t event);
bool ZAF_JobHelperJobDequeue(uint8_t * pEvent);

Simple Event Handling#

In the simplest form, only the application event queue will be used, for example, to execute a single command when a user presses a button. The file board.c uses the ZAF\_EventHelper module to send button events to the application queue which will result in the EventHandlerApp() function activating AppStateManager() with the button event.

  1. In learn mode the user presses key01, which changes the state to STATE\_APP\_STARTUP:

    void AppStateManager(EVENT_APP event)
    {
      :
      switch(currentState) {
        :
        case STATE_APP_LEARN_MODE:
          :
          if ((BTN_EVENT_SHORT_PRESS(APP_BUTTON_LEARN_RESET) == (BUTTON_EVENT)event) ||
            (EVENT_SYSTEM_LEARNMODE_STOP == (EVENT_SYSTEM)event))
          {
            :
            ChangeState(STATE_APP_STARTUP);
            :
          }
          :
      }
    }

    When learn process is completed, EventHandlerZwCommandStatus will be triggered by protocol with status EZWAVECOMMANDSTATUS\_LEARN\_MODE\_STATUS, which will trigger LearnCompleted(), from where ZAF_EventHelperEventEnqueue((EVENT_APP) EVENT_SYSTEM_LEARNMODE_FINISHED) is called:

    static void LearnCompleted(void)
    {
      ..:
      ZAF_EventHelperEventEnqueue((EVENT_APP) EVENT_SYSTEM_LEARNMODE_FINISHED);
      Transport_OnLearnCompleted(bNodeID);
    }
  2. In state STATE\_APP\_STARTUP, the wanted functionality is executed and the state changes back to the idle state STATE\_APP\_IDLE.

:
case STATE_APP_STARTUP:
  if (EVENT_APP_INIT == event) {
    :
    ChangeState(STATE_APP_IDLE);
  }
  :

Multiple Event Jobs Handling#

It is possible to enqueue more jobs during the execution with the ZAF\_JobHelper module.

The example below illustrates how the job queue is used to expand a button press into multiple actions (send a Basic Set followed by send a Notification) to be performed in the state STATE\_APP\_TRANSMIT\_DATA.

case STATE_APP_IDLE:
  :
  if ((BTN_EVENT_DOWN(PIR_EVENT_BTN) == (BUTTON_EVENT)event) ||
      (BTN_EVENT_HOLD(PIR_EVENT_BTN) == (BUTTON_EVENT)event))
  {
    :
    ChangeState(STATE_APP_TRANSMIT_DATA);

    if (false == ZAF_EventHelperEventEnqueue(EVENT_APP_NEXT_EVENT_JOB))
    {
      DPRINT("\r\n** EVENT_APP_NEXT_EVENT_JOB fail\r\n");
    }
    /*Add event's on job-queue*/
    ZAF_JobHelperJobEnqueue(EVENT_APP_BASIC_START_JOB);
    ZAF_JobHelperJobEnqueue(EVENT_APP_NOTIFICATION_START_JOB);
    ZAF_JobHelperJobEnqueue(EVENT_APP_START_TIMER_EVENTJOB_STOP);
  }

In state STATE\_APP\_TRANSMIT\_DATA, shown next, the event EVENT\_APP\_NEXT\_EVENT\_JOB starts the job by fetching the first job event from the job event queue.

Each send-function is provided with a pointer to the call-back function ZCB\_JobStatus() that will be called by the framework when the transmission has completed. ZCB_JobStatus() will simply place a EVENT\_APP\_NEXT\_EVENT\_JOB event on the application event queue to trigger the processing of the next job event.

void **ZCB\_JobStatus**(TRANSMISSION_RESULT \* pTransmissionResult)
{
  if (TRANSMISSION_RESULT_FINISHED == pTransmissionResult-\>isFinished)
  {
    ZAF_EventHelperEventEnqueue(EVENT_APP_NEXT_EVENT_JOB);
  }
}

Power Manager#

The Z-Wave chip provides several low energy modes (EM). Each energy mode manages whether the CPU and its various peripherals are available. The energy modes range from EM0 Active to EM4 Shutoff. EM0 Active mode provides the highest number of features, enabling the CPU, Radio, and peripherals with the highest clock frequency. EM4 Shutoff Mode provides the lowest power state, allowing the chip to return to EM0 Active on a wake-up condition.

To achieve the longest possible battery life, it is essential that Z-Wave applications on battery powered devices reduce the time spent in the higher energy modes as much as possible.

The Z-Wave Power Manager owned by the Z-Wave protocol thread controls what energy mode the RTOS should go to when idle. It uses the concept of Power Locks where regions of code can lock the chip from entering a specific energy mode. The Power Manager keeps track of all power locks and ensures that the chip does not at any time enter a power mode lower than what is currently requested.

To interact with the Power Manager from an application, use the Power Management API in ZAF_PM_Wrapper.h. To use it, first initialize the interface with API\_IF\_init(), and then register a power lock with ZAF\_PM\_Register().

void API_IF_init(SApplicationHandles* pAppHandles);
void ZAF_PM_Register(SPowerLock_t* handle, pm_type_t type);

Use the power lock type pm_type_t to tell the Power Manager if the power lock should “protect” code that need access to the radio (PM_TYPE_RADIO: do not enter EM2/EM3/EM4) or just some peripherals (PM_TYPE_PERIPHERAL: do not enter EM3/EM4).

After the power lock has been registered, ZAF\_PM\_StayAwake() and ZAF\_PM\_Cancel() can be used to wrap the code that needs access to the radio or peripherals:

void ZAF_PM_StayAwake(SPowerLock_t\* handle, unsigned int msec);
void ZAF_PM_Cancel(SPowerLock_t\* handle);

If the msec parameter to ZAF_PM_StayAwake() is non-zero, the power lock will automatically be cancelled after the specified time (in that case, a call to ZAF_PM_Cancel() is not required).

static SPowerLock_t m_RadioPowerLock;

static void
ApplicationTask(SApplicationHandles\* pAppHandles)
{
  API\_IF\_init(pAppHandles);

  ZAF\_PM\_Register(&m_RadioPowerLock, PM_TYPE_RADIO);
  :
}

void SomeFunction(void)
{
  ZAF\_PM\_StayAwake(&m_RadioPowerLock, 0);
  :
  /* Do something where the radio module is required */
  :
  ZAF\_PM\_Cancel(&m_RadioPowerLock);
  :
}

Several callbacks from the protocol to the framework supported enabling execution of specific code as the last thing before entering sleep mode. Up to three callbacks are available. For details refer to function ZAF_PM_SetPowerDownCallback() in ZAF_PM_Wrapper.h.

The applications DoorLockKeyPad (Listening Sleeping End Device) and SensorPIR (Reporting Sleeping End Device) both use power locks. SensorPIR goes all the way to EM4 when idle, but since a Listening Sleeping End Device must wake up every 250 ms or 1000 ms, the DoorLockKeyPad application only sleeps in EM2 to achieve a fast wakeup time.

Application Timers#

The AppTimer module provides an application interface for software timers.

File: AppTimer.h

void **AppTimerInit**(uint8_t iTaskNotificationBitNumber, TaskHandle_t ReceiverTask);
void **AppTimerSetReceiverTask**(TaskHandle_t ReceiverTask);
bool **AppTimerRegister**(SSwTimer\* pTimer, bool bAutoReload, void(\*pCallback)(SSwTimer\* pTimer) );
void AppTimerNotificationHandler(void);

All software timers are essentially FreeRTOS timers running in the high priority FreeRTOS timer task. Any timer callbacks are normally executed in the context of the timer task. Using the AppTimer module, you can ensure that timer callback functions are executed in the context of the application task.

Note: If a timer expires while, e.g., a battery-operated device is deep sleeping in energy mode EM4 Shutoff, the device will wake up, but the timer callback function will not be executed, since the device will be going through a full startup initialization.

To start using application timers, you need a SSwTimer instance and you need to initialize the AppTimer module. That is usually done in ApplicationInit():

#include \<AppTimer.h\>

static SSwTimer myAppTimer;

ZW_APPLICATION_STATUS ApplicationInit(EResetReason_t eResetReason)
{
  AppTimerInit**(***EAPPLICATIONEVENT\_TIMER***, NULL);
}

See Event Loop for a description of the EAPPLICATIONEVENT\_TIMER notification bit number and the framework function AppTimerNotificationHandler().

In the ApplicationTask() function, you need to register the handle of the application task with the AppTimer module and also register all your SSwTimers (it is possible to register up to eight application timers):

void ApplicationTask(SApplicationHandles\* pAppHandles)
{
  m_AppTaskHandle = **xTaskGetCurrentTaskHandle**();

  AppTimerSetReceiverTask(m_AppTaskHandle);
  AppTimerRegister(&myAppTimer, false, ZCB_MyAppTimerCallback);
}

The timeout callback could be implemented like this:

void ZCB\_MyAppTimerCallback(SSwTimer \*pTimer)
{
  UNUSED(pTimer);
  ZAF_EventHelperEventEnqueue(EVENT_APP_MY_TIMER_TIMEOUT);

To start and stop an application timer, simply use the functions from the SwTimer module (all timeout values in milliseconds):

File: SwTimer.h

ESwTimerStatus TimerStart(SSwTimer\* pTimer, uint32_t iTimeout);
ESwTimerStatus TimerStartFromISR(SSwTimer\* pTimer, uint32_t iTimeout);
ESwTimerStatus TimerRestart(SSwTimer\* pTimer);
ESwTimerStatus TimerRestartFromISR(SSwTimer\* pTimer);
ESwTimerStatus TimerStop(SSwTimer\* pTimer);
ESwTimerStatus TimerStopFromISR(SSwTimer\* pTimer);
bool TimerIsActive(SSwTimer\* pTimer);

When one or more application timers expire, the framework function AppTimerNotificationHandler() is called, which in turn calls the timer callback function for each of the timers that have expired.

If an autoloading timer (i.e., restarts automatically) is configured with a very small timeout value, it is possible it can expire multiple times before AppTimerNotificationHandler() is called. In that case, the timeout events will not be queued; AppTimerNotificationHandler() will only be called a single time.

Be careful when using timers inside Interrupt Service Routines (ISR). As TimerStartISR uses xTimerChangePeriodFromISR (freeRTOS) https://www.freertos.org/FreeRTOS-timers-xTimerChangePeriodFromISR.html, it is not advisable to start more than ONE timer from ISR with TimerStartISR. Finally, use only the dedicated ISR functions in the ISR.