№2 / 2018 / статья 7

Модули Mbee DualBand на контроллерах CC1350 в системах учета ресурсов

Александр Калачев (г. Барнаул)

Современные счетчики учета ресурсов с импульсным выходом объединяются в систему сбора и учета данных с помощью беспроводного канала. Но дополнить эту функцию возможностью считывания пользователем текущих значений можно с помощью беспроводного контроллера CC1350 производства Texas Instruments, двухдиапазонных беспроводных модулей MBee-DUAL-3.3 производства российской компании СМК и подробно описанного в данной статье программного проекта.

В основном в системах удаленного сбора показаний счетчиков потребления ресурсов при организации беспроводного канала применяются частоты субгигагерцевого диапазона. Это обусловлено меньшим влиянием препятствий и большей дальностью связи по сравнению с диапазоном 2,4 ГГц, а также меньшей на данный момент плотностью беспроводных систем. К тому же, использование субгигагерцевого диапазона несколько повышает защищенность системы сбора данных от взлома или подмены данных – как минимум, злоумышленнику необходимы будут специализированные устройства для перехвата трафика, а не имеющиеся практически в каждом устройстве Wi-Fi- и Bluetooth-адаптеры.

Простому же пользователю хотелось бы знать текущее потребление воды, тепла или электричества, причем, желательно – не заглядывая в укромные уголки помещения, где, как правило, устанавливаются счетчики. Как раз для решения данной проблемы могут пригодиться технологии Wi-Fi или Bluetooth. Но установка дополнительного устройства в счетчик не всегда возможна.

Применение двухдиапазонного беспроводного контроллера семейства CC1350 [1] производства Texas Instruments позволяет комплексно решить проблему – и организовать служебный канал, и предоставить информацию о работе прибора и текущем уровне потребления по удобному для пользователя интерфейсу. Например, передача показаний идет на частоте 868 МГц, а передача пользовательской информации – по BLE [2].

Сейчас многие приборы учета (особенно счетчики воды) имеют импульсные выходы. Один из путей автоматизации таких приборов – батарейный импульсный адаптер с радиоканалом для передачи информации в центральную систему сбора данных. Для потребителя этот же импульсный адаптер может передавать информацию о расходе воды в виде широковещательных BLE-сообщений.

Рассмотрим макетный вариант подобного импульсного адаптера для счетчиков воды. В составе макета – схема сопряжения с импульсным выходом, беспроводные модули, имитатор счетчика.

Подготовка к старту

В самом общем виде входящие элементы системы учета с дружественным пользовательским интерфейсом и связи между ними представлены на рисунке 1.

Рис. 1. Схематичное представление задачи

Рис. 1. Схематичное представление задачи

В качестве беспроводных модулей будут использованы модули MBee-DUAL-3.3-UFL-SOLDER-1350-UFL [3] производства компании «Системы, модули и компоненты», как отвечающие следующим критериям:

  • небольшие габариты;
  • наличие на плате модуля всей необходимой обвязки – радиотракта, цепей питания, кварцевых резонаторов;
  • достаточное количество доступных линий ввода-вывода;
  • небольшая стоимость.

Внешний вид модуля и распиновка его выводов представлена на рисунке 2.

Рис. 2. Внешний вид и распиновка модуля MBee-DUAL-3.3-UFL-SOLDER-1350-UFL

Рис. 2. Внешний вид и распиновка модуля MBee-DUAL-3.3-UFL-SOLDER-1350-UFL

В качестве основы программного проекта будет использован пример от Texas Instruments – Wireless Sensor Network Dual Mode, который входит в набор программного обеспечения SIMPLELINK-CC13X0-SDK. Примененные средства разработки – также производства TI:

  • Code Composter Studio для основной части проекта (версия CCS 7.3 и выше);
  • Sensor Controller Studio для работы с импульсным входом и фоновых задач (версия SC5.0 и выше);
  • программатор-отладчик XDS110.

Для макета проекта понадобятся два модуля – один из них будет выполнять роль концентратора (центрального узла сети сбора данных), второй – роль конечного узла, подсчитывающего импульсы со счетчика, передающего их концентратору и транслирующего результаты подсчета в широковещательных BLE-пакетах. Импортируем в рабочее пространство CCS проекты rfWsnNodeBleAdv и rfWsnConcentrator. При установке CCS с настройками «по умолчанию» проекты находятся в папке C:\ti\simplelink_cc13x0_sdk_1_50_00_08\examples\rtos\CC1350_LAUNCHXL\easylink\ (рисунок 3).

Рис. 3. Расположение проектов rfWsnNodeBleAdv и rfWsnConcentrator в структуре SDK simplelink_cc13x0_sdk_1_50_00_08

Рис. 3. Расположение проектов rfWsnNodeBleAdv и rfWsnConcentrator в структуре SDK simplelink_cc13x0_sdk_1_50_00_08

Обычно импорт и компиляция проектов проходят без особых затруднений.

Схема подключения модулей MBee-DUAL-3.3 к плате CC-DEVPACK-DEBUG (программатор XDS-110) представлена на рисунке 4. В качестве программатора можно также использовать плату LAUNCHXL-CC2650.

Рис. 4. Схема подключения модулей MBee-DUAL-3.3 к программатору XDS-110

Рис. 4. Схема подключения модулей MBee-DUAL-3.3 к программатору XDS-110

Пример rfWsnNodeBleAdv

В исходном примере rfWsnNodeBleAdv демонстрируется работа узла беспроводной сети, периодически посылающего пакеты центральному узлу (концентратору) в диапазоне 868 МГц и параллельно генерирующего широковещательные пакеты BLE-advertisiment. Примеры с беспроводным узлом и концентратором примечательны тем, что практически не задействована дополнительная периферия, за исключением нескольких выводов, к которым может подключаться опциональный внешний LCD-экран (430BOOST-SHARP96). Наличие этого LCD обеспечивает большую наглядность, но и его отсутствие не мешает основной функциональности проектов. Кроме того, вместо LCD-дисплея можно организовать вывод информации через UART на внешнюю программу-терминал.

В работе программы задействовано несколько задач, включая работу контроллера датчиков (SC). Для передачи данных концентратору используется простой протокол EasyLink API [5]. Запуск задач производится в файле rfWsnNode.c, там же инициализируется необходимая периферия (внешние выводы и дисплей):


int main(void)
{
  /* Инициализация внешних выводов и модуля дисплея (если подключен) */
  Board_initGeneral();
  Display_init();
  /* Инициализация задач работы с беспроводным каналом и опроса датчика */
  NodeRadioTask_init();
  NodeTask_init();
  /* Запуск операционной системы и задач */
  BIOS_start();
return (0);
}

В rfWsnNodeBleAdv задействованы несколько выводов:

  • Board_PIN_LED0 – для светодиода индикации, переключается после каждой операции отправки пакета;
  • Board_PIN_LED1 – мигает три раза, если на отправленный координатору пакет не было получено подтверждение, и один раз – если получено;
  • Board_ADCCHANNEL_A0 – аналоговый вход для подключения датчика освещенности/фотодиода, используется SC;
  • Board_PIN_BUTTON0 – вход для кнопки переключения режимов отправки пакетов, раз в одну секунду или раз в пять секунд.
  • Board_DIO30_SWPWR CC1350_LAUNCHXL_DIO30_SWPWR – включается при активном режиме работы радиотракта (на момент передачи/приема пакета данных).

Задача NodeTask (определена в одноименном файле NodeTask.c) ожидает прихода события от SCE, по его приходу переключает состояние вывода Board_PIN_LED1 и передает полученное значение АЦП задаче NodeRadioTask.

Основные функции NodeTask – инициализация периферии, запуск задачи на SC, ожидание и обработка событий:


static void nodeTaskFunction(UArg arg0, UArg arg1)
{
 /* Инициализация дисплея */
 Display_Params params;
 Display_Params_init(&params);
 params.lineClearMode = DISPLAY_CLEAR_BOTH;
 hDisplayLcd = Display_open(Display_Type_LCD, &params);
 hDisplaySerial = Display_open(Display_Type_UART, &params);
 /* Проверка наличия дисплея – проверяется два типа дисплея – UART и I²C */
 if (hDisplaySerial)
  {
  Display_printf(hDisplaySerial, 0, 0, "Waiting for SCE ADC reading...");
  }
 if (hDisplayLcd)
  {
  Display_printf(hDisplayLcd, 0, 0, "Waiting for ADC...");
  }
 /* Инициализация выводов индикации (для светодиодов) */
 ledPinHandle = PIN_open(&ledPinState, pinTable);
 if (!ledPinHandle)
  {
  System_abort("Error initializing board 3.3V domain pins\n");
  }
 /* Запуск задачи на SCE с периодом срабатывания 1 раз в секунду */
 SceAdc_init(0x00010000, NODE_ADCTASK_REPORTINTERVAL_FAST, NODE_ADCTASK_CHANGE_MASK);
 SceAdc_registerAdcCallback(adcCallback);
 SceAdc_start();
 /* Установка таймаута для режима быстрого опроса */
 Clock_setTimeout(fastReportTimeoutClockHandle,
 NODE_ADCTASK_REPORTINTERVAL_FAST_DURIATION_MS * 1000 / Clock_tickPeriod);
 /* Запуск режима быстрого опроса */
 Clock_start(fastReportTimeoutClockHandle);
 buttonPinHandle = PIN_open(&buttonPinState, buttonPinTable);
 if (!buttonPinHandle)
  {
  System_abort("Error initializing button pins\n");
  }
 /* Настройка кнопки */
 if (PIN_registerIntCb(buttonPinHandle, &buttonCallback) != 0)
  {
  System_abort("Error registering button callback function");
  }
 /* Цикл ожидания и обработки событий */
 while (1)
  {
  /* Wait for event */
  uint32_t events = Event_pend(nodeEventHandle, 0, NODE_EVENT_ALL, BIOS_WAIT_FOREVER);
  /* Если получено новое значение АЦП */
  if (events & NODE_EVENT_NEW_ADC_VALUE)
   {
   /* переключаем состояние светодиода */
   PIN_setOutputValue(ledPinHandle, NODE_ACTIVITY_LED,!PIN_getOutputValue(NODE_ACTIVITY_LED));
   /* Передача значения концентратору */
   NodeRadioTask_sendAdcData(latestAdcValue);
   /* Update display */
   updateLcd();
   }
 /* If new ADC value, send this data */
 if (events & NODE_EVENT_UPDATE_LCD)
  {
  /* update display */
  updateLcd();
  }
 }
}

NodeRadioTask, в свою очередь, получив данные, отправляет пакет по радиоканалу посредством протокола EasyLink и после передачи пакета ожидает подтверждения (пакета ACK от координатора). Если подтверждение не получено – попытка передачи пакета повторяется еще три раза.

Основная функция – nodeRadioTaskFunction – производит настройку параметров протокола 868 МГц – EasyLink и входит в цикл обработки сообщений:


static void nodeRadioTaskFunction(UArg arg0, UArg arg1)
{
 #ifdef FEATURE_BLE_ADV
 BleAdv_Params_t bleAdv_Params;
 /* Set mulitclient mode for EasyLink */
 EasyLink_setCtrl(EasyLink_Ctrl_MultiClient_Mode, 1);
 #endif
 //Настройка параметров протокола EasyLink
 EasyLink_Params easyLink_Params;
 EasyLink_Params_init(&easyLink_Params);
 #ifdef FEATURE_BLE_ADV
 easyLink_Params.pClientEventCb = &rfSwitchCallback;
 easyLink_Params.ui32ModType = RADIO_EASYLINK_MODULATION;
 easyLink_Params.nClientEventMask = RF_ClientEventSwitchClientEntered;
 /* Инициализация EasyLink */
 if(EasyLink_init_multimode(&easyLink_Params) != EasyLink_Status_Success)
  {
  System_abort("EasyLink_init failed");
  }
 #else
 if (EasyLink_init(RADIO_EASYLINK_MODULATION) != EasyLink_Status_Success)
  {
  System_abort("EasyLink_init failed");
  }
 #endif
 /* Генерация сетевого адреса для узла: используется функция генерации случайных чисел True Random Number Generator */
 Power_setDependency(PowerCC26XX_PERIPH_TRNG);
 TRNGEnable();
 do
 {
 while (!(TRNGStatusGet() & TRNG_NUMBER_READY))
  {
  //Wait for random number generator
  }
  nodeAddress = (uint8_t)TRNGNumberGet(TRNG_LOW_WORD);
 }
 while (nodeAddress == RADIO_CONCENTRATOR_ADDRESS);
 TRNGDisable();
 Power_releaseDependency(PowerCC26XX_PERIPH_TRNG);
 /* Set the filter to the generated random address */
 if (EasyLink_enableRxAddrFilter(&nodeAddress, 1, 1) != EasyLink_Status_Success)
 {
 System_abort("EasyLink_enableRxAddrFilter failed");
 }
 /* Настройка параметров пакета для передачи координатору */
 dmSensorPacket.header.sourceAddress = nodeAddress;
 dmSensorPacket.header.packetType = RADIO_PACKET_TYPE_DM_SENSOR_PACKET;
 /* Initialise previous Tick count used to calculate uptime for the TLM beacon */
 prevTicks = Clock_getTicks();
 #ifdef FEATURE_BLE_ADV
 /* Инициализация модуля Simple Beacon с параметрами по умолчанию */
 BleAdv_Params_init(&bleAdv_Params);
 bleAdv_Params.pfnPostEvtProxyCB = bleAdv_eventProxyCB;
 bleAdv_Params.pfnUpdateTlmCB = bleAdv_updateTlmCB;
 bleAdv_Params.pfnUpdateMsButtonCB = bleAdv_updateMsButtonCB;
 bleAdv_Params.pfnAdvStatsCB = NodeTask_advStatsCB;
 BleAdv_init(&bleAdv_Params);
 /* Установка формата ADV-пакета */
 BleAdv_setAdvertiserType(BleAdv_AdertiserUid);
 #endif
 /* Цикл обработки сообщений */
 while (1)
 {
 /* Wait for an event */
 uint32_t events = Event_pend(radioOperationEventHandle, 0, RADIO_EVENT_ALL, BIOS_WAIT_FOREVER);
 //Разбор типа события и реагирование на него
 /*
 Если необходимо передать пакет с новыми данными АЦП, он предварительно формируется:
 - подсчитывается время от прихода предыдущего пакета;
 - считывается текущий уровень напряжения;
 - считывается принятое значение АЦП;
 - считывается текущее состояние кнопки.
 После формирования пакета он передается координатору.
 */
 if (events & RADIO_EVENT_SEND_ADC_DATA)
  {
  uint32_t currentTicks;
  currentTicks = Clock_getTicks();
  //Подсчет времени с момента последнего чтения данных
  if (currentTicks > prevTicks)
  {
  dmSensorPacket.time100MiliSec += ((currentTicks – prevTicks) * Clock_tickPeriod) / 100000;
  }
 else
 {
 dmSensorPacket.time100MiliSec += ((prevTicks – currentTicks) * Clock_tickPeriod) / 100000;
 }
 prevTicks = currentTicks;
 //Формирование пакета на передачу
 dmSensorPacket.batt = AONBatMonBatteryVoltageGet();
 dmSensorPacket.adcValue = adcData;
 dmSensorPacket.button = !PIN_getInputValue(Board_PIN_BUTTON0);
 sendDmPacket(dmSensorPacket, NODERADIO_MAX_RETRIES, NORERADIO_ACK_TIMEOUT_TIME_MS);
 }
 /* Случай прихода пакета подтверждения (ACK) от концентратора */
 if (events & RADIO_EVENT_DATA_ACK_RECEIVED)
  {
  returnRadioOperationStatus(NodeRadioStatus_Success);
  }
  /* Срабатывание таймаута по ACK */
 if (events & RADIO_EVENT_ACK_TIMEOUT)
  {
  /* Достигнуто максимальное количество попыток передачи пакета */
  if (currentRadioOperation.retriesDone < currentRadioOperation.maxNumberOfRetries)
   {
   resendPacket();
   }
 else
  {
  /* Else return send fail */
  Event_post(radioOperationEventHandle, RADIO_EVENT_SEND_FAIL);
  }
 }
 /* Ошибка передачи пакета */
 if (events & RADIO_EVENT_SEND_FAIL)
 {
 returnRadioOperationStatus(NodeRadioStatus_Failed);
 }
 /* Обработка события BLE */
 #ifdef FEATURE_BLE_ADV
 if (events & NODE_EVENT_UBLE)
  {
  uble_processMsg();
  }
 #endif
  }
}

Формат сформированного пакета можно посмотреть в функции sendDmPacket():


static void sendDmPacket(struct DualModeSensorPacket sensorPacket, uint8_t maxNumberOfRetries, uint32_t ackTimeoutMs)
{
 /* Установка адреса получателя, в данном случае – концентратора */
 currentRadioOperation.easyLinkTxPacket.dstAddr[0] = RADIO_CONCENTRATOR_ADDRESS;
 /* Формирование полезной нагрузки пакета – данные АЦП, время с последней передачи и прочее */
 currentRadioOperation.easyLinkTxPacket.payload[0] = dmSensorPacket.header.sourceAddress;
 currentRadioOperation.easyLinkTxPacket.payload[1] = dmSensorPacket.header.packetType;
 currentRadioOperation.easyLinkTxPacket.payload[2] = (dmSensorPacket.adcValue & 0xFF00) >> 8;
 currentRadioOperation.easyLinkTxPacket.payload[3] = (dmSensorPacket.adcValue & 0xFF);
 currentRadioOperation.easyLinkTxPacket.payload[4] = (dmSensorPacket.batt & 0xFF00) >> 8;
 currentRadioOperation.easyLinkTxPacket.payload[5] = (dmSensorPacket.batt & 0xFF);
 currentRadioOperation.easyLinkTxPacket.payload[6] = (dmSensorPacket.time100MiliSec & 0xFF000000) >> 24;
 currentRadioOperation.easyLinkTxPacket.payload[7] = (dmSensorPacket.time100MiliSec & 0x00FF0000) >> 16;
 currentRadioOperation.easyLinkTxPacket.payload[8] = (dmSensorPacket.time100MiliSec & 0xFF00) >> 8;
 currentRadioOperation.easyLinkTxPacket.payload[9] = (dmSensorPacket.time100MiliSec & 0xFF);
 currentRadioOperation.easyLinkTxPacket.payload[10] = dmSensorPacket.button;
 /* Подсчет длины пакета данных */
 currentRadioOperation.easyLinkTxPacket.len = sizeof(struct DualModeSensorPacket);
 /* Установка числа попыток повтора передачи: по умолчанию – 3 и длительности таймаута */
 currentRadioOperation.maxNumberOfRetries = maxNumberOfRetries;
 currentRadioOperation.ackTimeoutMs = ackTimeoutMs;
 currentRadioOperation.retriesDone = 0;
 EasyLink_setCtrl(EasyLink_Ctrl_AsyncRx_TimeOut, EasyLink_ms_To_RadioTime(ackTimeoutMs));
 /* Собственно, передача пакета */
 if (EasyLink_transmit(&currentRadioOperation.easyLinkTxPacket) != EasyLink_Status_Success)
  {
  System_abort("EasyLink_transmit failed");
  }
 #if defined(Board_DIO30_SWPWR)
 /* this was a blocking call, so Tx is now complete. Turn off the RF switch power */
 PIN_setOutputValue(blePinHandle, Board_DIO30_SWPWR, 0);
 #endif
 /* Переход в режим приема */
 if (EasyLink_receiveAsync(rxDoneCallback, 0) != EasyLink_Status_Success)
  {
  System_abort("EasyLink_receiveAsync failed");
  }
}

В примере предлагается три формата широковещательных Adv-пакетов протокола BLE (определяются в файле BleAdv.c):

  • проприетарный;
  • Eddystone UID;
  • Eddystone URL.

Eddystone является открытым (по лицензии Apache 2.0) форматом сообщений для Bluetooth Low Energy (BLE) маячков от Google [4]. На данный момент он поддерживает следующие виды пакетов (advertisement packet):

  • Eddystone-UID – 16-байтовый идентификатор, который состоит из 10-байт namespaceId и 6-байт instanceId;
  • Eddystone-URL – транслирует URL. Любой длинный URL можно сократить с помощью Google URL Shortener или любого другого сервиса сокращения ссылок, чтобы он поместился в ограниченный 18 байтами Advertisment packet. После декодирования URL может быть использован любым клиентом с доступом к Интернету. Eddystone-URL позволяет легко обнаруживать и взаимодействовать с окружающими BLE-устройствами и сервисами;
  • Eddystone-TLM – телеметрия, которой доступны такие данные как напряжение аккумуляторной батареи, температура устройства, количество отправленных пакетов с момента включения и время с момента включения;
  • Eddystone-eTLM – зашифрованная версия кадра телеметрии (в данном примере не рассматривается).

Формат adv-пакетов различных типов и их вид в программе BLE-сканер, представленный на рисунках 5, 6, 7, задается в теле функции nodeRadioTaskFunction:

Рис. 5. Структура adv-пакета формата Eddystone UID

Рис. 5. Структура adv-пакета формата Eddystone UID


/* Установка формата ADV-пакета */
 BleAdv_setAdvertiserType(BleAdv_AdertiserUid);

Возможные параметры функции – BleAdv_AdertiserUid, BleAdv_AdertiserUrl, BleAdv_AdertiserMs.

Формат Eddystone UID содержит заголовок, определяющий сервисы Eddystone UUID, первые десять байт хеш-функции SHA-1 от адреса www.ti.com/product/cc1350 и в качестве идентификатора – шесть младших байт IEEE-адреса устройства.

Рис. 6. Структура adv-пакета формата Eddystone URL

Рис. 6. Структура adv-пакета формата Eddystone URL

В случае adv-пакетов формата URL в программе-сканере смартфона отображается веб-адрес и, более того, предлагается осуществить переход по указанному адресу. В пакете сам адрес хранится в сокращенной форме – без префикса и окончания.

Рис. 7. Структура adv-пакета «свободного» формата – определяемого производителем

Рис. 7. Структура adv-пакета «свободного» формата – определяемого производителем

Формат, определяемый производителем, в данном примере – самый короткий и содержит только одно поле.

Пример rfWsnConcentrator

rfWsnConcentrator – пример беспроводного центрального узла, принимающего пакеты от соседних узлов (диапазон 868 МГц). Концентратор принимает пакеты от периферийных сенсоров. Также как и rfWsnNodeBleAdv, данный пример состоит из нескольких задач, и узел в диапазоне 868 МГц работает с протоколом EasyLink.

Из внешних выводов микросхемы задействован только один – Board_PIN_LED0. Подключенный к этому пину светодиод мигает, когда по радиоканалу приходит пакет данных.

Задача ConcentratorRadioTask работает непосредственно с протоколом передачи данных – настраивает протокол EasyLink и использует его API для приема пакетов. По приему пакета отсылается подтверждение получения (ACK) и пакет пересылается задаче ConсentratorTask.

ConсentratorTask отображает содержимое пакета на ЖК-дисплее (если он подключен) и сигнализирует о приеме миганием светодиода, подключенного к выводу Board_PIN_LED0.

Приятной особенностью является то, что если в качестве дисплея настроен UART (по умолчанию в примере это так и есть), то есть возможность принимать и наблюдать сообщения, посылаемые на дисплей через функцию Display_printf(), подключившись к UART модуля. Сделать это на ПК можно посредством любого преобразователя USB-UART. Настройки выводов для UART-интерфейса у CC1350 для данного проекта следующие (файл CC1350_LAUNCHXL.h):


/* UART Board */
#define CC1350_LAUNCHXL_UART_RX    IOID_2     /* RXD */
#define CC1350_LAUNCHXL_UART_TX    IOID_3     /* TXD */

После включения концентратора он выдает на UART строку:


Waiting for nodes...

Эта же строка появляется в случае, если приходит поврежденный пакет или пакет был принят с ошибкой. При отладке приложения в условиях рабочего места такая ситуация – весьма частая, так как модули концентратора и сенсорного узла могут оказаться слишком близко друг от друга, а это – не самый лучший вариант для частот менее гигагерца.

В случае удачно принятого пакета в терминале появятся строки следующего вида:


.[2J .[0;0HNodes Value SW  RSSI
0x07  0932  0  -103

В первой строке – бинарное содержимое пакета и строка-комментарий, во второй – данные: адрес узла, от которого был получен пакет, полученное значение АЦП от сенсорного узла, состояние кнопки удаленного узла и уровень сигнала.

Центральным файлом проекта является rfWsnConcentrator.c, основная функция производит общие настройки и запускает задачи:


static void concentratorTaskFunction(UArg arg0, UArg arg1)
{
 /* Инициализация дисплея */
 Display_Params params;
 Display_Params_init(&params);
 params.lineClearMode = DISPLAY_CLEAR_BOTH;
 hDisplayLcd = Display_open(Display_Type_LCD, &params);
 hDisplaySerial = Display_open(Display_Type_UART, &params);
 if (hDisplaySerial)
  {
  Display_printf(hDisplaySerial, 0, 0, "Waiting for nodes...");
  }
 if (hDisplayLcd)
  {
  Display_printf(hDisplayLcd, 0, 0, "Waiting for nodes...");
  }
  /* Регистрация call-back-функции для работы с радиопротоколом */
  ConcentratorRadioTask_registerPacketReceivedCallback(packetReceivedCallback);
  /* Цикл ожидания и обработки сообщений */
 while(1)
  {
  /* Ожидание сообщения */
  uint32_t events = Event_pend(concentratorEventHandle, 0, CONCENTRATOR_EVENT_ALL, BIOS_WAIT_FOREVER);
  // Разбор типа сообщения
  /* Получено новое значение АЦП */
 if(events & CONCENTRATOR_EVENT_NEW_ADC_SENSOR_VALUE) {
  /* Смотрим, получены ли данные от уже известного узла */
 if(isKnownNodeAddress(latestActiveAdcSensorNode.address)) {
  updateNode(&latestActiveAdcSensorNode);
  }
 else
  {
  /* Если узел новый - добавляем его к списку активных подключенных узлов */
  addNewNode(&latestActiveAdcSensorNode);
  }
  /* Обновляем экранчик */
  updateLcd();
  }
 }
}

Call-back-функция packetReceivedCallback производит разбор пакета и сохраняет полученные в нем данные во внутренние переменные.

Работа с радиопротоколом осуществляется функцией concentratorRadioTaskFunction:


static void concentratorRadioTaskFunction(UArg arg0, UArg arg1)
{
 /* Инициализация протокола EasyLink */
 if(EasyLink_init(RADIO_EASYLINK_MODULATION) != EasyLink_Status_Success)
 {
 System_abort("EasyLink_init failed");
 }
 /* Установка адреса концентратора */
 concentratorAddress = RADIO_CONCENTRATOR_ADDRESS;
 EasyLink_enableRxAddrFilter(&concentratorAddress, 1, 1);
 /* Настройка пакетов подтверждений – Ack */
 ackPacket.header.sourceAddress = concentratorAddress;
 ackPacket.header.packetType = RADIO_PACKET_TYPE_ACK_PACKET;
 /* Переход в режим приема */
 if(EasyLink_receiveAsync(rxDoneCallback, 0) != EasyLink_Status_Success)
  {
  System_abort("EasyLink_receiveAsync failed");
  }
 /* Основной цикл ожидания и обработки сообщений */
 while (1)
  {
  uint32_t events = Event_pend(radioOperationEventHandle, 0, RADIO_EVENT_ALL, BIOS_WAIT_FOREVER);
  /* Получение пакета данных */
 if(events & RADIO_EVENT_VALID_PACKET_RECEIVED)
  {
  /* Отправка подтверждения – ACK */
  sendAck(latestRxPacket.header.sourceAddress);
  /* Вызов callback-функции на прием пакета */
  notifyPacketReceived(&latestRxPacket);
  /* Переход в режим приема */
 if(EasyLink_receiveAsync(rxDoneCallback, 0) != EasyLink_Status_Success)
  {
  System_abort("EasyLink_receiveAsync failed");
  }
 /* Переключение состояния светодиода */
 PIN_setOutputValue(ledPinHandle, CONCENTRATOR_ACTIVITY_LED,
 !PIN_getOutputValue(CONCENTRATOR_ACTIVITY_LED));
 }
 /* Если полученный пакет поврежден */
 if(events & RADIO_EVENT_INVALID_PACKET_RECEIVED)
  {
  /* Переход в режим приема */
 if(EasyLink_receiveAsync(rxDoneCallback, 0) != EasyLink_Status_Success)
   {
   System_abort("EasyLink_receiveAsync failed");
   }
  }
 }
}

Sensor Controller Task

Задача для SC состоит всего из двух секций – Initialization Code и Execution Code.

В первой секции настраивается требуемый канал АЦП:


// Select ADC input (A2 / DIO25)
 adcSelectGpioInput(2);
// Schedule the first execution
 fwScheduleTask(1);

В Execution Code запускается одиночный опрос выбранного канала АЦП и, если считанное значение отличается от предыдущего:


// Разрешение работы АЦП
 adcEnableSync(ADC_REF_FIXED, ADC_SAMPLE_TIME_2P7_US, ADC_TRIGGER_MANUAL);
// Считывание результата преобразования
 S16 adcValue;
 adcGenManualTrigger();
 adcReadFifo(adcValue);
 output.adcValue = adcValue;
// Остановка АЦП
 adcDisable();
// Оповещение основного ядра в случае отличающегося значения АЦП от предыдущего
 U16 adcMaskedBits = adcValue & cfg.changeMask;
 if (adcMaskedBits != state.oldAdcMaskedBits)
  {
  fwGenAlertInterrupt();
  state.samplesSinceLastReport = 0;
  }
 else
  {
  state.samplesSinceLastReport = state.samplesSinceLastReport + 1;
  }
//Alert driver if minimum report interval has expired
 if(cfg.minReportInterval != 0)
  {
  if(state.samplesSinceLastReport >= cfg.minReportInterval)
   {
   fwGenAlertInterrupt();
   state.samplesSinceLastReport = 0;
   }
  }
// Запоминаем значение для сравнения с последующим
 state.oldAdcMaskedBits = adcValue & cfg.changeMask;
// Разрешение запуска на следующей итерации
 fwScheduleTask(1);

Полученное значение adcValue будет передано call-back-функции adcCallBack задачи NodeTask. adcCallBack, в свою очередь, сохранит значение в переменной и инициирует сообщение NODE_EVENT_NEW_ADC_VALUE, которое будет обработано:


static void adcCallback(uint16_t adcValue)
{
 /* Save latest value */
 latestAdcValue = adcValue;
 /* Post event */
 Event_post(nodeEventHandle, NODE_EVENT_NEW_ADC_VALUE);
}

Непосредственно за получение данных от SC отвечает call-back-функция taskAlertCallback, определенная в файле SceAdc.c:


static void taskAlertCallback(void)
{
 /* Очистка флага ALERT */
 scifClearAlertIntSource();
 /* Отрабатываются только периодические события */
 if (scifGetAlertEvents() & (1 < < SCIF_ADC_SAMPLE_TASK_ID)) { /* Получаем выходную структуру данных SCE "output" */ SCIF_ADC_SAMPLE_OUTPUT_T* pOutput = scifGetTaskStruct(SCIF_ADC_SAMPLE_TASK_ID, SCIF_STRUCT_OUTPUT); /* Передаем значение АЦП через call-back-процедуру */ if (adcCallback) { adcCallback(pOutput->adcValue);
   }
  }
  /* Подтверждаем обработку события ALERT */
  scifAckAlertEvents();
}

Подключение MBee-DUAL-3.3 для примеров rfWsnNodeBleAdv и rfWsnConcentrator

Проекты настроены на отладочные платы LAUNCHXL-CC1350, но это не доставляет особых хлопот, так как изначально в платах данной серии максимально освобождены линии ввода-вывода и отсутствуют периферийные устройства. Соответствие выводов CC1350_LAUNCHXL и выводов модулей MBee-DUAL-3.3 для примеров rfWsnNodeBleAdv и rfWsnConcentrator представлено на рисунке 8. К выводам UART узла-концентратора можно подключить преобразователь USB-UART и наблюдать сообщения, предназначенные UART-дисплею в программе-терминале.

Рис. 8. Соответствие выводов CC1350_LAUNCHXL и выводов модулей MBee-DUAL-3.3 для примеров rfWsnNodeBleAdv и rfWsnConcentrator

Рис. 8. Соответствие выводов CC1350_LAUNCHXL и выводов модулей MBee-DUAL-3.3 для примеров rfWsnNodeBleAdv и rfWsnConcentrator

Макет беспроводного адаптера для счетчика с импульсным выходом

Для функционирования макета адаптера необходимо следующее – организовать считывание импульсов, в идеале – обеспечить некоторый объем памяти для хранения импульсов на случай перебоев с питанием. Симуляции считывания импульсов можно производить, используя геркон (наиболее близкий к реальному случаю вариант) или простую нормально разомкнутую кнопку. В качестве энергонезависимой памяти можно применить или дешевую EEPROM серии 24Fxx, или достаточно популярную среди любителей микросхему часов реального времени DS1307 с отдельным автономным источником питания.

Макет беспроводного адаптера для счетчика с импульсным выходом на базе MBee-DUAL-3.3 с часами реального времени представлен на рисунке 9. Помимо вспомогательной функции хранения показаний, DS1307 позволяет вести отсчет времени, что может быть использовано для будущих расширений.

Рис. 9. Макет беспроводного адаптера для счетчика с импульсным выходом на базе MBee-DUAL-3.3

Рис. 9. Макет беспроводного адаптера для счетчика с импульсным выходом на базе MBee-DUAL-3.3

Светодиоды индикации оставлены в первоначальном виде, аналоговый вход из исходного кода оригинального проекта TI не будет задействован. В проект добавляется работа с шиной I²C, и одна из линий контроллера будет использована в качестве счетного входа. Задача подсчета импульсов и работы с часами реального времени будет возложена на SC. Для этого придется переработать программу для контроллера датчиков и настройки проекта. В исходный вариант проекта добавляется поддержка (рисунок 10):

Рис. 10. Настройки проекта ADC Sample для счетчика

Рис. 10. Настройки проекта ADC Sample для счетчика

  • входных линий;
  • шины I²C;
  • генерации события по изменению состояния вывода.

Распределение выводов для макета следующее: линия DIO3 (номер в домене AUX – 12) – для подсчета импульсов, DIO4 и DIO5 – шина I²C, линии SCL и SDA – соответственно (рисунок 11).

Рис. 11. Настройки использования выводов SC

Рис. 11. Настройки использования выводов SC

На данный момент в проекте для SC появляется четыре секции кода – Initialization, Execution, Event Handler и Termination. В конфигурационную структуру данных добавляется следующее:

  • переменная rtcset (будет доступна как cfg.rtcset)
    • ее значение, равное 0х02, будет приводить к сбросу текущего значения счетных импульсов, в том числе – и в ОЗУ часов реального времени;
    • значение 0х01 – установка значения времени и даты «по умолчанию».

В выходную структуру данных добавляем переменные:

  • pulse – для подсчета импульсов (будет доступна как cfg.pulse);
  • переменные sec, min, hour, day, month, year – для чтения показаний при работе с часами реального времени.

Программный проект для SC, секция Initialization Code

В секции Initialization Code настраивается срабатывание события по фронту импульса на линии DIO3 (для данной функции используется нумерация выводов AUX Mapping, и DIO3 соответствует номер 12) – функция evhSetupGpioTrigger():


// Настраиваем срабатывание события по изменению состояния
// evhSetupGpioTrigger(0,12,0,EVH_GPIO_TRIG_ON_EDGE);
// Если конфигурационная переменная равна 0х02 - сбрасываем счетчик в памяти
 U16 th;
 U16 tl;
 if(cfg.rtcset==0x2)
  {
  i2cStart();
  i2cTx(I2C_OP_WRITE | 0xD0);
  i2cTx(0x8); //
  i2cTx(0x00); //
  i2cStop();
  i2cStart();
  i2cTx(I2C_OP_WRITE | 0xD0);
  i2cTx(0x9); //
  i2cTx(0x00); //
  i2cStop();
  }
// Считываем значение счетчика из памяти DS1307
 i2cStart();
 i2cTx(I2C_OP_WRITE | 0xD0);
 i2cTx(0x08);
 i2cRepeatedStart();
 i2cTx(I2C_OP_READ | 0xD0);
 i2cRxAck(tl);
 i2cRxNack(th);
 output.pulse=(th< <8)|tl;
 i2cStop();
 U16 template;
// Если значение переменной rtcset=0х01 -
// Устанавливаются значения времени по умолчанию
 if(cfg.rtcset==0x1)
  {
  i2cStart();
  i2cTx(I2C_OP_WRITE | 0xD0);
  i2cTx(0x0); //seconds
  i2cTx(0x00); //seconds
  i2cStop();
  i2cStart();
  i2cTx(I2C_OP_WRITE | 0xD0);
  i2cTx(0x1); //minutes
  i2cTx(0x36); //minutes
  i2cStop();
  i2cStart();
  i2cTx(I2C_OP_WRITE | 0xD0);
  i2cTx(0x02); //hours
  i2cTx(0x20); //hours
  i2cStop();
  i2cStart();
  i2cTx(I2C_OP_WRITE | 0xD0);
  i2cTx(0x04); //days
  i2cTx(0x10); //days
  i2cStop();
  i2cStart();
  i2cTx(I2C_OP_WRITE | 0xD0);
  i2cTx(0x05); //month
  i2cTx(0x11); //
  i2cStop();
  i2cStart();
  i2cTx(I2C_OP_WRITE | 0xA2);
  i2cTx(0x06); //year
  i2cTx(0x17); //year
  i2cStop();
  // Сбрасываем флаг настройки часов
  cfg.rtcset=0;
  }
// Считываем текущее время
 i2cStart();
 i2cTx(I2C_OP_WRITE | 0xD0);
 i2cTx(0x00); //send register address
// Read the result
 i2cRepeatedStart();
 i2cTx(I2C_OP_READ | 0xD0);
 i2cRxAck(template); //seconds
 output.sec=template&0x7F;
 i2cRxAck(template); //minutes
 output.min=template&0x7F;
 i2cRxAck(template); //hours
 output.hour=template&0x3F;
 i2cRxAck(template); //null reading
 i2cRxAck(template); //day
 output.day=template&0x7F;
 i2cRxAck(template); //month
 output.month=template&0x1F;
 i2cRxNack(template); //year
 output.year=template&0x7F;
 i2cStop();
//
 fwScheduleTask(1);

Секция Event Handler Code срабатывает по фронту на выводе DIO3, увеличивает значение счетчика, сохраняет его в памяти DS1307 и сигнализирует основному ядру о готовности данных (в качестве бонуса считываются также текущие дата и время, при желании они могут быть учтены далее в основной прикладной задаче):


U16 template;
// По фронту считываем текущее время
 i2cStart();
 i2cTx(I2C_OP_WRITE | 0xD0);
 i2cTx(0x00);
 i2cRepeatedStart();
 i2cTx(I2C_OP_READ | 0xD0);
 i2cRxAck(template); //seconds
 output.sec=template&0x7F;
 i2cRxAck(template); //minutes
 output.min=template&0x7F;
 i2cRxAck(template); //hours
 output.hour=template&0x3F;
 i2cRxAck(template); //day
 output.day=template&0x7F;
 i2cRxAck(template); //null reading
 i2cRxAck(template); //month
 output.month=template&0x1F;
 i2cRxNack(template); //year
 output.year=template&0x7F;
 i2cStop();
// Увеличиваем значение счетчика
 output.pulse+=1;
// Сохраняем значение счетчика в ОЗУ DS1307
// Младший байт
 template=output.pulse;
 template=template&0xFF;
 i2cStart();
 i2cTx(I2C_OP_WRITE | 0xD0);
 i2cTx(0x8);
 i2cTx(template);
 i2cStop();
// Старший байт
 template=output.pulse;
 template=template>>8;
 template=template&0xFF;
 i2cStart();
 i2cTx(I2C_OP_WRITE | 0xD0);
 i2cTx(0x9);
 i2cTx(template);
 i2cStop();
// Повторно настраиваем срабатывание события по изменению состояния, так как
// evhSetupGpioTrigger() – функция одноразового срабатывания
 evhSetupGpioTrigger(0,12,0,EVH_GPIO_TRIG_ON_EDGE);
 fwGenAlertInterrupt();

В Termination Code запрещается срабатывание триггеров evhCancelTrigger(0).

Теперь SC при перепаде уровня будет передавать основному ядру количество подсчитанных на данный момент импульсов.

Модификация проекта rfWsnNodeBleAdv

Для начала модифицируется call-back-функция taskAlertCallback(void) в файле SceAdc.c – она будет считывать значение переменной SC pulse (output.pulse) и передавать его функции adcCallback:


static void taskAlertCallback(void)
{
 /* Clear the ALERT interrupt source */
 scifClearAlertIntSource();
 SCIF_ADC_SAMPLE_OUTPUT_T* pOutput = scifGetTaskStruct(SCIF_ADC_SAMPLE_TASK_ID, SCIF_STRUCT_OUTPUT);
 if (adcCallback)
  {
  /* Ничего лишнего, просто считываем текущее значение счетчика импульсов */
  adcCallback(pOutput->pulse);
  }
 /* Acknowledge the alert event */
 scifAckAlertEvents();
}

Следующие изменения коснутся файла BleAdv.c, в котором описываются форматы широковещательных пакетов проекта. Показания счетчика будут отображаться в пакетах формата BleAdv_AdertiserMs, как в наиболее универсальных. Ниже представлена структура пакета. Первые три байта – общие флаги, далее поле «данных, определяемых производителем» для удобства поиска устройства среди многих – поле имени. Многие привычные флаги-константы в uBle не определены, поэтому прописываются непосредственно цифрами или доопределяются отдельно в заголовочных файлах:


/* propreitory advertisement packet */
static uint8_t propAdvertisement[] =
{
 // initial
 0x02, // длина поля 1
 0x01, // флаги
 0x02, // флаг «LE General Discoverable Mode»
 0x06, // длина поля 2
 0xff, // флаг «Manufacturer Specific Data»
 0xff, // [5] поля данных 5-9
 0xff, // [6]
 0xff, // [7]
 0xff, // [8]
 0xff, // [9]
 0x0D, // длина поля 3
 0x09, // флаг поля-имени «GAP_ADTYPE_LOCAL_NAME_COMPLETE»
 // собственно, далее следуют символы имени
 'm', 'b', 'e', 'e', 'd', 'u', 'a', 'l', '3', '.', '3',
};
//BTN state

Для динамического изменения adv-пакета достаточно будет изменять данные массива propAdvertisement[]. Делать это можно посредством функции BleAdv_updateAdv(), которую определим в том же файле BleAdv.c. Например, в данном виде BleAdv_updateAdv() принимает 16-битное число и побайтно записывает его в 5 и 6 позицию в adv-пакете в поле «Manufacturer Specific Data»:


BleAdv_Status BleAdv_updateAdv(uint16_t newData)
{
 BleAdv_Status status = BleAdv_Status_Config_Error;
 propAdvertisement[6]=newData&0xFF;
 propAdvertisement[5]=(newData>>8)&0xFF;
 status = BleAdv_Status_Success;
 return status;
}

При желании, и если нет специальных требований по безопасности, можно перевести показания в символьный вид и транслировать их как часть имени устройства, например:


BleAdv_Status BleAdv_updateAdv(uint16_t newData)
{
 uint16_t temp;
 BleAdv_Status status = BleAdv_Status_Config_Error;
 // Транслируем данные в «сыром виде»
 propAdvertisement[6]=newData&0xFF;
 propAdvertisement[5]=(newData>>8)&0xFF;
 temp= newData;
 // Оставляем “mbee” для «видности», после этого приводим значение счетчика
 propAdvertisement[16]=’ ’; // пробел для читаемости
 propAdvertisement[21]=temp%10 + 0x30; temp=temp/10;
 propAdvertisement[20]=temp%10 + 0x30; temp=temp/10;
 propAdvertisement[19]=temp%10 + 0x30; temp=temp/10;
 propAdvertisement[18]=temp%10 + 0x30; temp=temp/10;
 propAdvertisement[17]=temp%10 + 0x30;
 status = BleAdv_Status_Success;
 return status;
}

Чтобы adv-пакет изменялся по мере прихода счетного импульса, немного модифицируется call-back-функция adcCallback() в файле NodeTask.c:


static void adcCallback(uint16_t adcValue)
{
 /* Save latest value */
 latestAdcValue = adcValue;
 // Модифицируем массив propAdvertisement[17]
 BleAdv_updateAdv(adcValue);
 // Заставляем модуль выдавать обновленный пакет в эфир
 BleAdv_setAdvertiserType(BleAdv_AdertiserMs);
 /* Post event */
 Event_post(nodeEventHandle, NODE_EVENT_NEW_ADC_VALUE);
}

Результаты тестирования кода представлены на рисунке 12: а) непосредственно после включения макета отображается только имя; б) то же, с подробным выводом содержимого пакета в НЕХ-виде; в) вид пакета после получения данных счетчика, в имени устройства: также отображается количество подсчитанных импульсов – в данном случае “mbee 01022”.

Рис. 12. Отображение adv-пакетов программой BLE-сканера

Рис. 12. Отображение adv-пакетов программой BLE-сканера

Модификации кода для узла-концентратора не требуется – с его стороны функциональность и интерфейс не поменялись.

Представленный пример кода для макета импульсного адаптера является, конечно, довольно простым, но может послужить основой для дальнейшего развития и наращивания функциональности.

Заключение

Система-на-кристалле СС1350 – отличный кандидат для устройств, требующих, с одной стороны, передачи данных на большие расстояния в диапазоне 433/868 МГц, с другой – предоставляющий простой и привычный интерфейс через смартфон.

Для быстрого вывода продукта на рынок, особенно когда речь идет о производстве относительно небольших партий устройств, использование готовых радиомодулей существенно упрощает процесс разработки. Модули MBee-DUAL-3.3 в этом плане – прекрасное решение. В них присутствует вся необходимая обвязка, включая радиотракт (причем и для 2,4 ГГц, и для 868 МГц), кварцевые резонаторы, конденсаторы и цепь сброса. Форм-фактор модулей и исполнение выводов удобны и для ручной, и для автоматической пайки.

Также для скачивания прилагается программный проект.

Литература

  1. SimpleLink™ Ultra-Low Power Dual Band Wireless Microcontroller // http://www.ti.com/product/CC1350;
  2. Джина Копли, Джонас Олссон, Свейн Ветти IoT: как совместить передачу данных на большое расстояние и подключение смартфона? // https://www.compel.ru/lib/ne/2017/2/7-iot-kak-sovmestit-peredachu-dannyih-na-bolshoe-rasstoyanie-i-podklyuchenie-smartfona;
  3. Документация на серию MBee-DUAL // https://www.compel.ru/pdf-items/_smk/ps/mbee-dual/da38964c2cf20c907f36c40ee1354944;
  4. Specification for Eddystone, an open beacon format from Google // https://github.com/google/eddystone;
  5. Олег Пушкарев. Превращение контроллера в сетевой процессор: Как управлять CC1310 с помощью АТ-команд // https://www.compel.ru/lib/ne/2016/3/.

Наши информационные каналы

О компании Texas Instruments

В середине 2001 г. компании Texas Instruments и КОМПЭЛ заключили официальное дистрибьюторское соглашение, которое явилось результатом длительной и успешной работы КОМПЭЛ в качестве официального дистрибьютора фирмы Burr-Brown. (Как известно, Burr-Brown вошла в состав TI так же, как и компании Unitrode, Power Trend и Klixon). С этого времени компания КОМПЭЛ получила доступ к поставке всей номенклатуры производимых компанией TI компонентов, ...читать далее

Наличие на складах
Наименование Наличие Цена
CC1350F128RGZT (TI) 3 766 5.0082 $ 315.00 руб. от 250 шт
CC1350F128RGZR (TI) 3 156 11.61 $ 730.14 руб. от 100 шт
MBee-DUAL-3.3-UFL-PLS12-1350-UFL (СМК) 0
MBee-DUAL-3.3-SMA-PLS10-1310 (СМК) 0
LAUNCHXL-CC1350-4 (TI) 492 39.23 $ 2467 руб. от 2 шт
LAUNCHXL-CC1350US (TI) 151 55.72 $ 3505 руб. от 1 шт
24FC128-I/ST (MCRCH) 16 257 0.3654 $ 22.99 руб. от 500 шт
24FC128-I/SN (MCRCH) 36 676 0.3857 $ 24.26 руб. от 1 000 шт
DS1307Z+T&R (MAX) 43 142 0.4884 $ 30.72 руб. от 1 024 шт
DS1307ZN+T&R (MAX) 33 922 0.5733 $ 36.06 руб. от 873 шт