
Предполагается, что читатель уже имеет начальные знания языка C, что-то знает о Zigbee, чипе cc2530, методах его прошивания и использования, а также знаком с такими проектами, как zigbee2mqtt. Если нет — подготовьтесь или сходите почитать на https://myzigbee.ru и https://www.zigbee2mqtt.io/
Статья написана сперва подробно, но постепенно ускоряется и уже не останавливается на деталях, а описывает готовый код прошивки. Если кому-то не интересны рассуждения, то просто открывайте исходники прошивки и читайте их.
Исходный код готовой прошивки
Код и подход к разработке не претендует на идеальность. “Я не волшебник, я только учусь.”
Цель
Основная цель — разобраться, как писать прошивки под Z-Stack, давно хотел. Поэтому решил реализовать альтернативную прошивку под готовое оборудование (в качестве примера выбрано реле Sonoff BASICZBR3) и добавить возможность подключения популярного датчика температуры ds18b20.
Дополнительно хотел показат�� начинающим Zigbee-разработчикам пример разработки прошивки под чип TI cc2530 на Z-Stack.
1. Подготовка
Для начала разработки нужно скачать и установить Z-Stack 3.0.2 — это SDK для разработки прошивок с примерами и документацией.
Также нужно скачать и установить IAR Embeded Workbench for 8051 — это среда разработки с возможностью компиляции под чипы TI cc2530. Бесплатный период использования — 1 месяц (но ищущий найдет решение).
Для разработки и отладки я использую CCDebugger — он позволяет не только прошивать чипы cc2531/cc2530, но и выполнять отладку приложения в среде IAR.

Для упрощения экспериментов, макетирование и отладку я делаю на devboard и соответствующем модуле cc2530:

2. Создание нового приложения
Создаем новый проект на база GenericApp. Это пример базового приложения на Z-Stack. Располагается оно в папке Z-Stack 3.0.2\Projects\zstack\HomeAutomation\GenericApp.
Копируем рядом и переименовываем, например, в DIYRuZRT (так назовем приложение для нашего устройства).
Внутри папки CC2530DB есть файлы:
- GenericApp.ewd — настройки проекта для C-SPY
- GenericApp.ewp — файл проекта
- GenericApp.eww — рабочая область Workspace
Переименовываем файлы в DIYRuZRT.eww и DIYRuZRT.ewp.
Внутри всех файлов (в том числе и в папке Source) также меняем все упоминания GenericApp на DIYRuZRT.
Теперь открываем проект DIYRuZRT.ewp в IAR. Выбираем конфигурацию RouterEB и выполняем Rebuild All.

В папке CC2530DB создастся папка RouterEB, а внутри, в папке EXE, появится файл DIYRuZRT.d51 — этот файл удобен для прошивки и отладки из IAR.
Но если нам надо прошить прошивку через SmartRF Flash Programmer, то сделаем небольшие изменения. Для этого в настройках проекта в разделе Link на вкладке Output поменяем настройки Output file и Format:

После этого в папке EXE будет создаваться файл прошивки DIYRuZRT.hex удобный для прошивания из других инструментов и другими способами.
Но после заливки этой прошивки устройство не подключается к сети. Что ж, будем разбираться.
3. Немного терминологии
В терминологии Zigbee есть следующие понятия:
- Endpoint (эндпоинт) — точка описания конечного устройства. Обычно в простых устройствах один эндпоинт. В многофункциональных устройствах их может быть несколько, также как в устройствах с разными профилями взаимодействия (один профиль — один эндпоинт).
- Cluster (кластер) — набор атрибутов и команд, относящихся к единому функционалу (вкл/выкл, регулирование освещения, температурные измерения и т.п.). Кластер указывает на возможности, реализуемые эндпоинтом. В одном эндпоинте можно реализовать несколько разных кластеров, но не одинаковых.
- Attribute (атрибут) — характеристика кластера, значение которого можно прочитать или записать. В кластере может быть множество атрибутов.
- Command (команда) — управляющее сообщение, которое может обработать кластер. У команды могут быть параметры. Это реализуется функцией, которая выполняется при при получении команды и параметров.
Виды кластеров, атрибутов, команд стандартизованы в Zigbee Cluster Library. Но производители могут применять собственные кластеры, со своими атрибутами и командами.
Некоторые горе-производители наплевательски относятся к стандартам и делают что-то около стандарта. Потом под них приходится подстраиваться.
В терминологии Z-Stack тоже есть свои понятия, например:
- OSAL (Operating System Abstraction Layer) — уровень абстракции Операционной системы. Здесь оперируют задачами (tasks), сообщениями (messages), событиями (events), таймерами (timers) и другими объектами.
- HAL (Hardware Abstraction Layer) — уровень абстракции оборудования. Здесь оперируют кнопками (keys), светодиодами (leds), прерываниями (Interrupt) и т.п.
Аппаратный уровень обеспечивает изоляцию программного кода и оборудования, которым он управляет. Операционный уровень предоставляет механизмы построения и взаимодействия между элементами приложения.
Использование этого всего вас ждет ниже и в принципе при разработке прошивок.
4. Что же у нас внутри базового приложения?
Код приложения расположен в папке Source:
- OSAL_DIYRuZRT.c — основной файл инициализирующий приложение
- zcl_DIYRuZRT.h — заголовочный файл
- zcl_DIYRuZRT.c — файл реализации функций
- zcl_DIYRuZRT_data.c — файл констант, переменных и структур
OSAL_DIYRuZRT.c — основной файл, в котором заполняе��ся массив обработчиков задач (task) pTaskEventHandlerFn tasksArr и реализуется функция их инициализации osalInitTasks.
Все остальные файлы нужны для реализации этих инициализаторов и обработчиков.
Список обработчиков задач pTaskEventHandlerFn tasksArr заполняется ссылками на функции. Часть задач подключаются/отключаются соответствующими директивами компиляции.
Посмотреть и настроить директивы компиляции можно в опциях компилятора Defined symbols:

const pTaskEventHandlerFn tasksArr[] = { macEventLoop, nwk_event_loop, #if !defined (DISABLE_GREENPOWER_BASIC_PROXY) && (ZG_BUILD_RTR_TYPE) gp_event_loop, #endif Hal_ProcessEvent, #if defined( MT_TASK ) MT_ProcessEvent, #endif APS_event_loop, #if defined ( ZIGBEE_FRAGMENTATION ) APSF_ProcessEvent, #endif ZDApp_event_loop, #if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT ) ZDNwkMgr_event_loop, #endif //Added to include TouchLink functionality #if defined ( INTER_PAN ) StubAPS_ProcessEvent, #endif // Added to include TouchLink initiator functionality #if defined ( BDB_TL_INITIATOR ) touchLinkInitiator_event_loop, #endif // Added to include TouchLink target functionality #if defined ( BDB_TL_TARGET ) touchLinkTarget_event_loop, #endif zcl_event_loop, bdb_event_loop, zclDIYRuZRT_event_loop };
osalInitTasks — стартовая функция приложения, которая регистрирует задачи, реализуемые приложением.
Регистрация задач выполняется по порядку, и каждая задача получает свой собственный номер. Важно соблюсти тот же порядок, что и в массиве tasksArr, т.к. обработчики вызываются в соответствие с номером задачи.
void osalInitTasks( void ) { uint8 taskID = 0; tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt); osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt)); macTaskInit( taskID++ ); nwk_init( taskID++ ); #if !defined (DISABLE_GREENPOWER_BASIC_PROXY) && (ZG_BUILD_RTR_TYPE) gp_Init( taskID++ ); #endif Hal_Init( taskID++ ); #if defined( MT_TASK ) MT_TaskInit( taskID++ ); #endif APS_Init( taskID++ ); #if defined ( ZIGBEE_FRAGMENTATION ) APSF_Init( taskID++ ); #endif ZDApp_Init( taskID++ ); #if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT ) ZDNwkMgr_Init( taskID++ ); #endif // Added to include TouchLink functionality #if defined ( INTER_PAN ) StubAPS_Init( taskID++ ); #endif // Added to include TouchLink initiator functionality #if defined( BDB_TL_INITIATOR ) touchLinkInitiator_Init( taskID++ ); #endif // Added to include TouchLink target functionality #if defined ( BDB_TL_TARGET ) touchLinkTarget_Init( taskID++ ); #endif zcl_Init( taskID++ ); bdb_Init( taskID++ ); zclDIYRuZRT_Init( taskID ); }
Наше приложение зарегистрировало функцию обработчик zclDIYRuZRT_event_loop и функцию инициализации zclDIYRuZRT_Init. Они добавлены последними по списку.
Это две основных функций нашего приложения. Реализация этих функций находится в файле zcl_DIYRuZRT.c.
zclDIYRuZRT_Init — функция регистрации задачи.
DIYRuZRT_ENDPOINT — номер эндпоинта, реализуемого нашим приложением.
Последовательно выполняются шаги регистрации, описывающие наше приложение:
- bdb_RegisterSimpleDescriptor — регистрация описания нашего приложения. Описание представлено переменной SimpleDescriptionFormat_t zclDIYRuZRT_SimpleDesc — структура описывает один эндпоинт, его профиль, характеристики, входящие и исходящие кластеры. Заполнение структур данных находится в файле OSAL_DIYRuZRT_data.c
- zclGeneral_RegisterCmdCallbacks — регистрация таблицы обработчиков команд эндпоинта zclGeneral_AppCallbacks_t zclDIYRuZRT_CmdCallbacks — это структура, где для каждой команды надо указать обработчик.
- zcl_registerAttrList — регистрация атрибутов эндпоинта zclAttrRec_t zclDIYRuZRT_Attrs — массив атрибутов, описывающих каждый зарегистрированный выше кластер.
- zcl_registerForMsg — регистрация получения управляющих сообщений.
- RegisterForKeys — подписываем нашу задачу на получение событий нажатия кнопок.
/********************************************************************* * SIMPLE DESCRIPTOR */ // This is the Cluster ID List and should be filled with Application // specific cluster IDs. const cId_t zclDIYRuZRT_InClusterList[] = { ZCL_CLUSTER_ID_GEN_BASIC, ZCL_CLUSTER_ID_GEN_IDENTIFY, // DIYRuZRT_TODO: Add application specific Input Clusters Here. // See zcl.h for Cluster ID definitions }; #define ZCLDIYRuZRT_MAX_INCLUSTERS (sizeof(zclDIYRuZRT_InClusterList) / sizeof(zclDIYRuZRT_InClusterList[0])) const cId_t zclDIYRuZRT_OutClusterList[] = { ZCL_CLUSTER_ID_GEN_BASIC, // DIYRuZRT_TODO: Add application specific Output Clusters Here. // See zcl.h for Cluster ID definitions }; #define ZCLDIYRuZRT_MAX_OUTCLUSTERS (sizeof(zclDIYRuZRT_OutClusterList) / sizeof(zclDIYRuZRT_OutClusterList[0])) SimpleDescriptionFormat_t zclDIYRuZRT_SimpleDesc = { DIYRuZRT_ENDPOINT, // int Endpoint; ZCL_HA_PROFILE_ID, // uint16 AppProfId; // DIYRuZRT_TODO: Replace ZCL_HA_DEVICEID_ON_OFF_LIGHT with application specific device ID ZCL_HA_DEVICEID_ON_OFF_LIGHT, // uint16 AppDeviceId; DIYRuZRT_DEVICE_VERSION, // int AppDevVer:4; DIYRuZRT_FLAGS, // int AppFlags:4; ZCLDIYRuZRT_MAX_INCLUSTERS, // byte AppNumInClusters; (cId_t *)zclDIYRuZRT_InClusterList, // byte *pAppInClusterList; ZCLDIYRuZRT_MAX_OUTCLUSTERS, // byte AppNumInClusters; (cId_t *)zclDIYRuZRT_OutClusterList // byte *pAppInClusterList; };
zclDIYRuZRT_event_loop — функция обработчиков событий нашего приложения.
Сперва в цикле обрабатываются системные события:
- ZCL_INCOMING_MSG — команды управления устройством, обрабатываются в zclDIYRuZRT_ProcessIncomingMsg.
- KEY_CHANGE — события нажатия кнопок, обрабатываются в zclDIYRuZRT_HandleKeys.
- ZDO_STATE_CHANGE — события изменения состояния сети.
if ( events & SYS_EVENT_MSG ) { while ( (MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( zclDIYRuZRT_TaskID )) ) { switch ( MSGpkt->hdr.event ) { case ZCL_INCOMING_MSG: // Incoming ZCL Foundation command/response messages zclDIYRuZRT_ProcessIncomingMsg( (zclIncomingMsg_t *)MSGpkt ); break; case KEY_CHANGE: zclDIYRuZRT_HandleKeys( ((keyChange_t *)MSGpkt)->state, ((keyChange_t *)MSGpkt)->keys ); break; case ZDO_STATE_CHANGE: zclDIYRuZRT_NwkState = (devStates_t)(MSGpkt->hdr.status); // now on the network if ( (zclDIYRuZRT_NwkState == DEV_ZB_COORD) || (zclDIYRuZRT_NwkState == DEV_ROUTER) || (zclDIYRuZRT_NwkState == DEV_END_DEVICE) ) { giGenAppScreenMode = GENERIC_MAINMODE; zclDIYRuZRT_LcdDisplayUpdate(); } break; default: break; } // Release the memory osal_msg_deallocate( (uint8 *)MSGpkt ); }
Далее — обработка специального события DIYRuZRT_EVT_1, которое переключает состояние светодиода HAL_LED_2 и запускает таймер на 500м с таким же событием. Тем самым запускается мигание светодиода HAL_LED_2.
if ( events & DIYRuZRT_EVT_1 ) { // toggle LED 2 state, start another timer for 500ms HalLedSet ( HAL_LED_2, HAL_LED_MODE_TOGGLE ); osal_start_timerEx( zclDIYRuZRT_TaskID, DIYRuZRT_EVT_1, 500 ); return ( events ^ DIYRuZRT_EVT_1 ); }
Дело в том, что при старте прошивки возникает событие HAL_KEY_SW_1 и именно в нем происходит инициализация таймера и события DIYRuZRT_EVT_1. И если нажать на кнопку S2, то мигание остановится (у меня светодиод остается включенным). Повторное нажатие снова запустит мигание.
5. HAL: светодиоды и кнопки
«Погодите, какой светодиод и кнопки?», — спросите вы. Изначально, все примеры в Z-stack ориентированы на различного рода отладочные платы серии SmartRF05 EB:

У меня немного другая плата для отладки и модуль с чипом.
На плате есть 2 кнопки (+ ресет) и 3 светодиода (+ индикатор питания). Вот один из них (D2) мигает при корректной работе прошивки.
Прозвонив контак��ы, определяем соответствие пинов, диодов и кнопок:
- D1 — P10
- D2 — P11
- D3 — P14
- S2 — P20
- S1 — P01
Так вот, HAL — это Hardware Abstraction Layer, способ абстрагироваться от реализации оборудования. В коде приложения используются макросы и функции, которые работают с абстракциями типа Кнопка 1 или Светодиод 2, а конкретное соответствие абстракций и оборудования задается отдельно.
Разберемся что за HAL_LED_2 и как понять, на какой пин он подвешен.
Поиском находим файл hal_led.h, где описаны эти константы и функция HalLedSet, куда передается номер светодиода и режим. Внутри вызывается функция HalLedOnOff для включения и выключения светодиода, которая в свою очередь выполняет либо HAL_TURN_ON_LED2 либо HAL_TURN_OFF_LED2.
HAL_TURN_ON_LED2 и HAL_TURN_OFF_LED2 — это макросы, описанные в hal_board_cfg.h. В зависимости от конфигурации оборудования макросы меняются.
В моём случае:
#define HAL_TURN_OFF_LED2() st( LED2_SBIT = LED2_POLARITY (0); ) #define HAL_TURN_ON_LED2() st( LED2_SBIT = LED2_POLARITY (1); )
Чуть выше в файле описаны соответствия LED2_SBIT и LED2_POLARITY:
/* 2 - Red */ #define LED2_BV BV(1) #define LED2_SBIT P1_1 #define LED2_DDR P1DIR #define LED2_POLARITY ACTIVE_HIGH
Это означает, что светодиод 2 у нас располагается на пине P1_1 и его уровень включения — высокий. Но, судя по коду, светодиод должен был погаснуть при нажатии на кнопку, а у нас он остается гореть. Если в этом файле hal_board_cfg.h поменяем:
#define LED2_POLARITY ACTIVE_HIGH
на
#define LED2_POLARITY ACTIVE_LOW
то теперь светодиод гаснет при нажатии на кнопку S2, как и должно быть по логике.
Чтобы не менять общие файлы, не относящиеся к нашему приложению, лучше сделать иначе:
- создадим копию файла hal_board_cfg.h (из папки Z-Stack 3.0.2\Components\hal\target\CC2530EB\) в нашу папку Source и назовём его например hal_board_cfg_DIYRuZRT.h
- сделаем так, что наша копия файла подключалась самая первая (тем самым исключив подключение общего файла). Создадим в нашей папке Source файл preinclude.h и запишем туда строку:
#include "hal_board_cfg_DIYRuZRT.h"
- укажем подключение этого файла самым первым — в настройках проекта:
$PROJ_DIR$\..\Source\preinclude.h

Теперь можем менять параметры оборудования в нашем файле hal_board_cfg_DIYRuZRT.h и в файле preinclude.h без необходимости править общие файлы.
В этот же файл preinclude.h я перенес директивы компилятора и удалил их в Options компилятора:
#define SECURE 1 #define TC_LINKKEY_JOIN #define NV_INIT #define NV_RESTORE #define xZTOOL_P1 #define xMT_TASK #define xMT_APP_FUNC #define xMT_SYS_FUNC #define xMT_ZDO_FUNC #define xMT_ZDO_MGMT #define xMT_APP_CNF_FUNC #define LEGACY_LCD_DEBUG #define LCD_SUPPORTED DEBUG #define MULTICAST_ENABLED FALSE #define ZCL_READ #define ZCL_WRITE #define ZCL_BASIC #define ZCL_IDENTIFY #define ZCL_SCENES #define ZCL_GROUPS
В том же файле hal_board_cfg_DIYRuZRT.h находим описание кнопки S1 и Joystick Center Press:
/* S1 */ #define PUSH1_BV BV(1) #define PUSH1_SBIT P0_1 /* Joystick Center Press */ #define PUSH2_BV BV(0) #define PUSH2_SBIT P2_0 #define PUSH2_POLARITY ACTIVE_HIGH
Это соответствует пинам кнопок на плате.
Посмотрим на инициализацию оборудования — макрос HAL_BOARD_INIT в этом же файле. По-умолчанию включается директива HAL_BOARD_CC2530EB_REV17, поэтому смотрим соответствующий вариант макроса.
/* ----------- Board Initialization ---------- */ #if defined (HAL_BOARD_CC2530EB_REV17) && !defined (HAL_PA_LNA) && \ !defined (HAL_PA_LNA_CC2590) && !defined (HAL_PA_LNA_SE2431L) && \ !defined (HAL_PA_LNA_CC2592) #define HAL_BOARD_INIT() \ { \ uint16 i; \ \ SLEEPCMD &= ~OSC_PD; /* turn on 16MHz RC and 32MHz XOSC */ \ while (!(SLEEPSTA & XOSC_STB)); /* wait for 32MHz XOSC stable */ \ asm("NOP"); /* chip bug workaround */ \ for (i=0; i<504; i++) asm("NOP"); /* Require 63us delay for all revs */ \ CLKCONCMD = (CLKCONCMD_32MHZ | OSC_32KHZ); /* Select 32MHz XOSC and the source for 32K clock */ \ while (CLKCONSTA != (CLKCONCMD_32MHZ | OSC_32KHZ)); /* Wait for the change to be effective */ \ SLEEPCMD |= OSC_PD; /* turn off 16MHz RC */ \ \ /* Turn on cache prefetch mode */ \ PREFETCH_ENABLE(); \ \ HAL_TURN_OFF_LED1(); \ LED1_DDR |= LED1_BV; \ HAL_TURN_OFF_LED2(); \ LED2_DDR |= LED2_BV; \ HAL_TURN_OFF_LED3(); \ LED3_DDR |= LED3_BV; \ HAL_TURN_OFF_LED4(); \ LED4_SET_DIR(); \ \ /* configure tristates */ \ P0INP |= PUSH2_BV; \ }
Именно в этом макросе происходит инициализация режимов и регистров процессора.
Вместо LED2_DDR и других будет подставлен P1DIR — это регистр порта P1 , отвечающий за режим работы пинов (вход или выход). Соответственно LED2_BV — это установка в значения 1 в бит соответствующего пина (в нашем случае в 1й бит, что соответствует пину P1_1):

Регистры и режимы процессора описаны в документации
«cc253x User's Guide»
Но нигде не видно, как настраиваются кнопки. Кнопки обрабатываются аналогично, но в другом файле — hal_key.c. В нем определены параметры работы кнопок и функции HalKeyInit, HalKeyConfig, HalKeyRead, HalKeyPoll. Эти функции отвечают за инициализацию подсистемы работы с кнопками и считывания значений.
По-умолчанию обработка кнопок выполняется по таймеру, каждые 100мс. Пин P2_0 для текущей конфигурации назначен на джойстик и его текущее состояние считывается как нажатие — поэтому запускается таймер мигания светодиодом.
6. Настраиваем устройство под себя
Поменяем в файле zcl_DIYRuZRT.h:
- DIYRuZRT_ENDPOINT на 1
в файле OSAL_DIYRuZRT_data.c:
- DIYRuZRT_DEVICE_VERSION на 1
- zclDIYRuZRT_ManufacturerName на { 6, 'D','I','Y','R','u','Z' }
- zclDIYRuZRT_ModelId на { 9, 'D','I','Y','R','u','Z','_','R','T' }
- zclDIYRuZRT_DateCode на { 8, '2','0','2','0','0','4','0','5' }
Для того, чтобы устройство могло подключаться к сети на любом канале (по умолчанию только на 11, указан в директиве DEFAULT_CHANLIST в файле Tools\f8wConfig.cfg), надо указать эту возможность в файле preinclude.h изменив значение директивы.
Еще добавим директиву компиляции DISABLE_GREENPOWER_BASIC_PROXY, чтобы для нашего устройства не создавался эндпоинт GREENPOWER.
Также отключим ненужную нам поддержку LCD экрана.
//#define LCD_SUPPORTED DEBUG #define DISABLE_GREENPOWER_BASIC_PROXY #define DEFAULT_CHANLIST 0x07FFF800 // ALL Channels
Чтобы наше устройство автоматически пыталось подключиться к сети, добавим в код функции zclDIYRuZRT_Init запуск подключения к сети.
bdb_StartCommissioning(BDB_COMMISSIONING_MODE_NWK_STEERING | BDB_COMMISSIONING_MODE_FINDING_BINDING);
После этого выполняем Build, заливаем прошивку в чип и запускаем спаривание на координаторе. Я проверяю работу Zigbee-сети в ioBroker.zigbee, вот так выглядит новое подключенное устройство:

Отлично, получилось подключить устройство!
7. Усложняем работу устройства
Теперь попробуем немного адаптировать функционал:
- Процесс подключения устройства к сети сделаем по долгому нажатию на кнопку.
- Если устройство уже было в сети, то долгое нажатие выводит его из сети.
- Короткое нажатие — переключает состояние светодиода.
- Состояние светодиода должно сохраняться при запуске устройства после пропадания питания.
Для настройки собственной обработки кнопок я создал функцию DIYRuZRT_HalKeyInit по аналогии с подобной в модуле hal_key.c, но исключительно для своего набора кнопок.
// Инициализация работы кнопок (входов) void DIYRuZRT_HalKeyInit( void ) { /* Сбрасываем сохраняемое состояние кнопок в 0 */ halKeySavedKeys = 0; PUSH1_SEL &= ~(PUSH1_BV); /* Выставляем функцию пина - GPIO */ PUSH1_DIR &= ~(PUSH1_BV); /* Выставляем режим пина - Вход */ PUSH1_ICTL &= ~(PUSH1_ICTLBIT); /* Не генерируем прерывания на пине */ PUSH1_IEN &= ~(PUSH1_IENBIT); /* Очищаем признак включения прерываний */ PUSH2_SEL &= ~(PUSH2_BV); /* Set pin function to GPIO */ PUSH2_DIR &= ~(PUSH2_BV); /* Set pin direction to Input */ PUSH2_ICTL &= ~(PUSH2_ICTLBIT); /* don't generate interrupt */ PUSH2_IEN &= ~(PUSH2_IENBIT); /* Clear interrupt enable bit */ }
Вызов этой функции добавил в макрос HAL_BOARD_INIT файла hal_board_cfg_DIYRuZRT.h. Чтобы не было конфликта — отключим встроенный hal_key в том же файле hal_board_cfg_DIYRuZRT.h:
#define HAL_KEY FALSE
Т.к. отключен стандартный обработчик считывания кнопок, то будем это делать сами.
В функции инициализации zclDIYRuZRT_Init запустим таймер на считывание состояний кнопок. Таймер будет генерировать наше событие HAL_KEY_EVENT.
osal_start_reload_timer( zclDIYRuZRT_TaskID, HAL_KEY_EVENT, 100);
А в цикле обработки событий обработаем событие HAL_KEY_EVENT вызвав функцию DIYRuZRT_HalKeyPoll:
// Считывание кнопок void DIYRuZRT_HalKeyPoll (void) { uint8 keys = 0; // нажата кнопка 1 ? if (HAL_PUSH_BUTTON1()) { keys |= HAL_KEY_SW_1; } // нажата кнопка 2 ? if (HAL_PUSH_BUTTON2()) { keys |= HAL_KEY_SW_2; } if (keys == halKeySavedKeys) { // Выход - нет изменений return; } // Сохраним текущее состояние кнопок для сравнения в след. раз halKeySavedKeys = keys; // Вызовем генерацию события изменений кнопок OnBoard_SendKeys(keys, HAL_KEY_STATE_NORMAL); }
Сохранение состояния кнопок в переменной halKeySavedKeys позволяет нам определять момент изменения — нажатия и отжатия кнопок.
При нажатии на кнопку запустим таймер на 5 секунд. Если этот таймер сработает, то сформируется событие DIYRuZRT_EVT_LONG. Если кнопку отпускают, то таймер сбрасывается. В любом случае, если нажимают кнопку — переключаем состояние светодиода.
// Обработчик нажатий клавиш static void zclDIYRuZRT_HandleKeys( byte shift, byte keys ) { if ( keys & HAL_KEY_SW_1 ) { // Запускаем таймер для определения долгого нажатия - 5 сек osal_start_timerEx(zclDIYRuZRT_TaskID, DIYRuZRT_EVT_LONG, 5000); // Переключаем реле updateRelay(RELAY_STATE == 0); } else { // Останавливаем таймер ожидания долгого нажатия osal_stop_timerEx(zclDIYRuZRT_TaskID, DIYRuZRT_EVT_LONG); } }
Теперь при обработке события долгого нажатия обращаем внимание на текущее состояние сети через атрибут структуры bdbAttributes.bdbNodeIsOnANetwork
// событие DIYRuZRT_EVT_LONG if ( events & DIYRuZRT_EVT_LONG ) { // Проверяем текущее состояние устройства // В сети или не в сети? if ( bdbAttributes.bdbNodeIsOnANetwork ) { // покидаем сеть zclDIYRuZRT_LeaveNetwork(); } else { // инициируем вход в сеть bdb_StartCommissioning( BDB_COMMISSIONING_MODE_NWK_FORMATION | BDB_COMMISSIONING_MODE_NWK_STEERING | BDB_COMMISSIONING_MODE_FINDING_BINDING | BDB_COMMISSIONING_MODE_INITIATOR_TL ); // будем мигать, пока не подключимся osal_start_timerEx(zclDIYRuZRT_TaskID, DIYRuZRT_EVT_BLINK, 500); } return ( events ^ DIYRuZRT_EVT_LONG ); }
Идем далее. Состояние светодиода сохраним в переменной, значение которой будем сохранять в NV-памяти. При старте устройства будем считывать значение из памяти в переменную.
// инициализируем NVM для хранения RELAY STATE if ( SUCCESS == osal_nv_item_init( NV_DIYRuZRT_RELAY_STATE_ID, 1, &RELAY_STATE ) ) { // читаем значение RELAY STATE из памяти osal_nv_read( NV_DIYRuZRT_RELAY_STATE_ID, 0, 1, &RELAY_STATE ); } // применяем состояние реле applyRelay();
// Изменение состояния реле void updateRelay ( bool value ) { if (value) { RELAY_STATE = 1; } else { RELAY_STATE = 0; } // сохраняем состояние реле osal_nv_write(NV_DIYRuZRT_RELAY_STATE_ID, 0, 1, &RELAY_STATE); // Отображаем новое состояние applyRelay(); } // Применение состояние реле void applyRelay ( void ) { // если выключено if (RELAY_STATE == 0) { // то гасим светодиод 1 HalLedSet ( HAL_LED_1, HAL_LED_MODE_OFF ); } else { // иначе включаем светодиод 1 HalLedSet ( HAL_LED_1, HAL_LED_MODE_ON ); } }
8. Теперь разберемся с Zigbee
С аппаратной частью пока разобрались — кнопкой управляем светодиодом. Теперь реализуем это же через Zigbee.
Для управления реле нам достаточно использовать наш единственный эндпоинт и реализовать кластер GenOnOff. Прочитаем спецификацию Zigbee Cluster Library для кластера GenOnOff:


Достаточно реализовать атрибут OnOff и команды On, Off, Toggle.
Для начала добавим директиву в preinclude.h:
#define ZCL_ON_OFF
В описание наших атрибутов zclDIYRuZRT_Attrs добавляем новые атрибуты кластера:
// *** Атрибуты On/Off кластера *** { ZCL_CLUSTER_ID_GEN_ON_OFF, { // состояние ATTRID_ON_OFF, ZCL_DATATYPE_BOOLEAN, ACCESS_CONTROL_READ, (void *)&RELAY_STATE } }, { ZCL_CLUSTER_ID_GEN_ON_OFF, { // версия On/Off кластера ATTRID_CLUSTER_REVISION, ZCL_DATATYPE_UINT16, ACCESS_CONTROL_READ | ACCESS_CLIENT, (void *)&zclDIYRuZRT_clusterRevision_all } },
Также добавим кластер в список поддерживаемых входящих кластеров эндпоинта zclDIYRuZRT_InClusterList.
Для реализации команд управления добавим обработчик в таблицу zclDIYRuZRT_CmdCallbacks.
/********************************************************************* * Таблица обработчиков основных ZCL команд */ static zclGeneral_AppCallbacks_t zclDIYRuZRT_CmdCallbacks = { zclDIYRuZRT_BasicResetCB, // Basic Cluster Reset command NULL, // Identify Trigger Effect command zclDIYRuZRT_OnOffCB, // On/Off cluster commands NULL, // On/Off cluster enhanced command Off with Effect NULL, // On/Off cluster enhanced command On with Recall Global Scene NULL, // On/Off cluster enhanced command On with Timed Off #ifdef ZCL_LEVEL_CTRL NULL, // Level Control Move to Level command NULL, // Level Control Move command NULL, // Level Control Step command NULL, // Level Control Stop command #endif
И реализуем его:
// Обработчик команд кластера OnOff static void zclDIYRuZRT_OnOffCB(uint8 cmd) { // запомним адрес, откуда пришла команда // чтобы отправить обратно отчет afIncomingMSGPacket_t *pPtr = zcl_getRawAFMsg(); zclDIYRuZRT_DstAddr.addr.shortAddr = pPtr->srcAddr.addr.shortAddr; // Включить if (cmd == COMMAND_ON) { updateRelay(TRUE); } // Выключить else if (cmd == COMMAND_OFF) { updateRelay(FALSE); } // Переключить else if (cmd == COMMAND_TOGGLE) { updateRelay(RELAY_STATE == 0); } }
Отлично, теперь реле можно переключать командами.


Но этого мало. Теперь мы должны также информировать координатор о текущем состоянии светодиода, если мы переключаем его кнопкой.
Опять же, добавим директиву:
#define ZCL_REPORTING_DEVICE
Теперь создадим функцию zclDIYRuZRT_ReportOnOff , отправляющую сообщение о состоянии. Будем вызывать ее при переключении светодиода и при старте устройства.
// Информирование о состоянии реле void zclDIYRuZRT_ReportOnOff(void) { const uint8 NUM_ATTRIBUTES = 1; zclReportCmd_t *pReportCmd; pReportCmd = osal_mem_alloc(sizeof(zclReportCmd_t) + (NUM_ATTRIBUTES * sizeof(zclReport_t))); if (pReportCmd != NULL) { pReportCmd->numAttr = NUM_ATTRIBUTES; pReportCmd->attrList[0].attrID = ATTRID_ON_OFF; pReportCmd->attrList[0].dataType = ZCL_DATATYPE_BOOLEAN; pReportCmd->attrList[0].attrData = (void *)(&RELAY_STATE); zclDIYRuZRT_DstAddr.addrMode = (afAddrMode_t)Addr16Bit; zclDIYRuZRT_DstAddr.addr.shortAddr = 0; zclDIYRuZRT_DstAddr.endPoint = 1; zcl_SendReportCmd(DIYRuZRT_ENDPOINT, &zclDIYRuZRT_DstAddr, ZCL_CLUSTER_ID_GEN_ON_OFF, pReportCmd, ZCL_FRAME_CLIENT_SERVER_DIR, false, SeqNum++); } osal_mem_free(pReportCmd); }
Теперь в логах видим сообщения об изменении состояния светодиода.
9. Подключаем датчик температуры ds18b20
Подключается датчик на любой свободный пин (в моем случае поставил P2_1).
Добавляем в приложение код опроса датчика. Опрашивать будем регулярно — раз в минуту.
Сразу при опросе будет оповещать координатор сети о текущем значении.
Прочитаем спецификации ZCL по отправке данных с датчиков температуры. Нам нужен кластер
Temperature Measurement

Видим что нужно реализовать 3 атрибута, один из которых представляет значение температуры умноженное на 100.
Здесь атрибуты добавляем по аналогии с кластером GenOnOff. Информировать координатор будет по событию DIYRuZRT_REPORTING_EVT, которое запланируем при старте раз в минуту. В обработчике события будем вызывать zclDIYRuZRT_ReportTemp, которая будет считывать температуру датчика и отправлять сообщение.
// Информирование о температуре void zclDIYRuZRT_ReportTemp( void ) { // читаем температуру zclDIYRuZRT_MeasuredValue = readTemperature(); const uint8 NUM_ATTRIBUTES = 1; zclReportCmd_t *pReportCmd; pReportCmd = osal_mem_alloc(sizeof(zclReportCmd_t) + (NUM_ATTRIBUTES * sizeof(zclReport_t))); if (pReportCmd != NULL) { pReportCmd->numAttr = NUM_ATTRIBUTES; pReportCmd->attrList[0].attrID = ATTRID_MS_TEMPERATURE_MEASURED_VALUE; pReportCmd->attrList[0].dataType = ZCL_DATATYPE_INT16; pReportCmd->attrList[0].attrData = (void *)(&zclDIYRuZRT_MeasuredValue); zclDIYRuZRT_DstAddr.addrMode = (afAddrMode_t)Addr16Bit; zclDIYRuZRT_DstAddr.addr.shortAddr = 0; zclDIYRuZRT_DstAddr.endPoint = 1; zcl_SendReportCmd(DIYRuZRT_ENDPOINT, &zclDIYRuZRT_DstAddr, ZCL_CLUSTER_ID_MS_TEMPERATURE_MEASUREMENT, pReportCmd, ZCL_FRAME_CLIENT_SERVER_DIR, false, SeqNum++); } osal_mem_free(pReportCmd); }
10. Заливаем прошивку в устройство
Для смены devboard на Sonoff BASICZBR3 необходимо скорректировать соответствие пинов светодиодов и кнопки.

Переделаем светодиод 1 на пин P0_7, чтобы управлять реле. Включение осуществляется высоким уровнем ACTIVE_HIGH. Кнопку S1 перевешиваем на пин P1_3, а информационный светодиод 2 на P1_0. Датчик температуры оставляем на пине P2_1. Все эти изменения делаем в файле hal_board_cfg_DIYRuZRT.h. Для выбора конфигурации сделаем отдельную директиву HAL_SONOFF. Если она задана, то будут использоваться настройки для Sonoff BASICZBR3, иначе для devboard.
#ifdef HAL_SONOFF /* 1 - P0_7 Реле */ #define LED1_BV BV(7) #define LED1_SBIT P0_7 #define LED1_DDR P0DIR #define LED1_POLARITY ACTIVE_HIGH /* 2 - P1_0 Синий */ #define LED2_BV BV(0) #define LED2_SBIT P1_0 #define LED2_DDR P1DIR #define LED2_POLARITY ACTIVE_LOW #else /* 1 - P1_0 Зеленый */ #define LED1_BV BV(0) #define LED1_SBIT P1_0 #define LED1_DDR P1DIR #define LED1_POLARITY ACTIVE_LOW /* 2 - P1_1 Красный */ #define LED2_BV BV(1) #define LED2_SBIT P1_1 #define LED2_DDR P1DIR #define LED2_POLARITY ACTIVE_LOW #endif
Еще один важный параметр пришлось поправить — наличие «часового» кварца, т.к. на плате Sonoff BASICZBR3 он не распаян:
//#define HAL_CLOCK_CRYSTAL #define OSC32K_CRYSTAL_INSTALLED FALSE
Без этих опций прошивка не стартует (точнее не всегда).
После это собираем прошивку и подключаемся для прошивки.
ВНИМАНИЕ!!! Отключите реле Sonoff BASICZBR3 от сети переменного тока перед любыми действиями с подключением и прошивкой!
Соединяем Sonoff BASICZBR3 проводками с CCDebugger, стираем чип и прошиваем нашу прошивку.

11. Заводим устройство в zigbee2mqtt и ioBroker.Zigbee
Хоть устройство у нас и появилось в списке подключенных устройств и мы можем управлять им через отправку команд, но надо сделать это более корректно — с картинками и состояниями.
Чтобы завести новое устройство в ioBroker.Zigbee, нужно выполнить 2 шага:
- Добавить описание устройства в пакет zigbee-herdsman-converters. Выполнение этого шага будет достаточно для того, чтобы заставить работать устройство в проекте zigbee2mqtt.
- Добавить описание устройства в ioBroker.Zigbee
Все изменения можно сперва выполнить в локальных файлах, а затем сделать PR в соответствующие репозитории.
Находим расположение пакета zigbee-herdsman-converters в установленном ioBroker (или zigbee2mqtt). Внутри пакета находим файл devices.js https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/devices.js
В этом файле хранятся описания всех устройств, с которыми умеет работать ioBroker.zigbee и zigbee2mqtt. Находим в нем блок описаний устройств DIYRuZ (после 2300 строки). Добавляем в этот блок описание нового устройства:
{ zigbeeModel: ['DIYRuZ_RT'], model: 'DIYRuZ_RT', vendor: 'DIYRuZ', description: '', supports: 'on/off, temperature', fromZigbee: [fz.on_off, fz.temperature], toZigbee: [tz.on_off], },
В атрибут fromZigbee мы указываем конвертеры, которые будут обрабатывать сообщения, приходящие от устройства. Наши два сообщения стандартизованы. Конвертер fz.on_off обрабатывает сообщение о включении/выключении, а fz.temperature — данные о температуре. В коде этих конвертеров (располагаются в файле converters/fromZigbee.js) видно, как обрабатываются входящие сообщения и что температура делится на 100.
on_off: { cluster: 'genOnOff', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { if (msg.data.hasOwnProperty('onOff')) { const property = getProperty('state', msg, model); return {[property]: msg.data['onOff'] === 1 ? 'ON' : 'OFF'}; } }, },
temperature: { cluster: 'msTemperatureMeasurement', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const temperature = parseFloat(msg.data['measuredValue']) / 100.0; return {temperature: calibrateAndPrecisionRoundOptions(temperature, options, 'temperature')}; }, },
В атрибут toZigbee указываем конвертеры, которые будут обрабатывать наши команды устройству. В нашем случае это конвертер tz.on_off для переключения реле.
Всё, в «конвертеры» добавили. Кто пользуется zigbee2mqtt — можете уже пользоваться.
А пользователи ioBroker еще добавляют описание устройства в файл ioBroker.zigbee\lib\devices.js
{ vendor: 'DIYRuZ', models: ['DIYRuZ_RT'], icon: 'img/DIYRuZ.png', states: [ states.state, states.temperature, ], },
Здесь достаточно указать точно такую же модель, файл с картинкой и перечень состояний. В нашем случае состояния также стандартные: state для состояния реле, temperature для отображения значений температуры.

12. Что дальше?
К сожалению, я не смог разобраться со всеми аспектами и возможностями, которые предоставляет Z-Stack 3.0. Скорее всего я даже не корректно реализовал какой-то функционал или для его реализации можно было использовать какие-то встроенные механизмы.
Поэтому, приведенное решение можно улучшать и развивать. Вот некоторые направления:
- Я не смог быстро найти решения по возможности подключения дочерних устройств через реле. Другие устройства-роутеры могут выполнять команду “permit_join” и подключать новые устройства через себя, без необходимости подносить новое устройство к координатору. Устройство представляется роутером, корректно отображается на карте сети, но выполнять команду “permit_join”, отказывается. Точнее, команду выполняет, но устройства не подключаются через него.
- Также, не реализовал корректный reporting. Это способ настройки уведомления о состоянии, когда можно командой configReport указать перечень атрибутов для отправки и частоту уведомления.
- Работа с группами.
- Разобраться с прерываниями и реализовать опрос кнопки через прерывания.
Ну а для следующих устройств нужно разбираться с режимами питания и созданием “спящих” устройств на батарейках.
Как всегда, помимо комментариев, приглашаю обсуждать это и другие устройства в Телеграм-чатике по Zigbee.
Хочу выразить благодарность за поддержку и помощь в разработке своим коллегам по Телеграм-чату и Zigbee сообществу:
Ссылки
Основная документация включена в SDK Z-Stack 3.0.2 и устанавливается вместе с ним. Но часть ссылок приведу тут:
- OSAL и HAL
- TI ZIgbee Wiki
- Create New Application For SmartRF05 + CC2530 SWRA231 Version 1.1
- Z-Stack Home Sample Application User's Guide SWRU354 Version 1.3
- Z-Stack Sample Applications SWRA201 Version 1.6
- Z-Stack Developer's Guide SWRA176 Version 1.12
- Z-Stack 3.0 Developer's Guide Version 1.14
- Z-Stack API SWRA195 Version 1.10
- TI CC253x User's Guide
- https://github.com/formtapez/ZigUP
- Zigbee Cluster Library rev 6
- Исходные тексты прошивки DIYRuZ_RT
