Русский
Русский
English
Статистика
Реклама

Modbus

ESP32 LVGL и круглый дисплей

25.03.2021 16:19:55 | Автор: admin

В прошлом году, после выхода видео про дисплей GC9A01 на канале "Электроника в объективе", я решил, что обязательно должен что-то на нем собрать, да еще и с использованием графической библиотеки LVGL. Заказал 2 таких дисплея, один на отладочной плате, второй отдельно только дисплей со шлейфом.

Так как я не очень люблю когда все соединено на проводах или макетных платах, (постоянно что-то отваливается), и хотелось какое-то законченное устройство в корпусе, решил поискать подходящий вариант и уже под него делать плату, ничего из стандартных корпусов для РЭА найти не удалось. Думал уже делать в виде съемной конструкции для своей отладочной платы на ESP32, но потом нашел подходящий вариант, (из под консилера :) ), он идеально подходил под мою задумку, был круглый открывающийся и имел прозрачное окно в размер дисплея. решено было делать плату под него.

Основные требования по устройству были: чип ESP32 так же для перспективы заложил драйвер RS-485 и слот для microSD карточки, так же хотелось иметь возможность питать все от usb и в перспективе иметь возможность подать внешнее питание. Так как разъем все равно ставить, хотелось сразу иметь возможность и программировать esp32 без дополнительных подключений. Но количество свободного места оказалось крайне мало, ставить CP2102 было дороговато, а CH340 с ее габаритами и кварцем не хотелось. Случайно увидел что есть компактная микросхема CH340N SOIC-8, преобразователь USB-UART с минимальной обвязкой в корпусе SOP-8. Ее и решено было использовать.

Так как на одной плате все не помещалось, разделил на две, верхняя ESP32 и дисплей, снизу питание, microSD слот, драйвер RS485 и USB-UART преобразователь. Для связи между нижней и верхней платой решил использовать гибкий плоский шлейф FFC и коннекторы FPC с шагом 0.5. Готовых шлейфов под размер и количество линий в наличии не было, решил делать из широкого и длинного шлейфа, думал что не получится разделить, но оказалось что это возможно. Для удешевления заказа объединил две платы в одну, с последующим удалением перемычки между ними, такая компоновка позволяет использовать верхнюю плату и под другие проекты с выводом информации на дисплей.

Общий вид печатной платы

Прикинув все размеры компонентов, решил уменьшить толщину платы до минимально возможного размера без увеличения стоимости, заказал с толщиной 0,6мм с зеленой маской. При такой толщине их можно резать чуть ли не ножницами, разделение на 2 платы заняло пару минут. Как оказалось не обошлось и без "косяков", перепутал выводы micro-USB разъема, разместил зеркально, пришлось резать дорожки и переделывать, после залил лаком. Так же вывод Reset дисплея соединил не с портом общего назначения, а с сигналом разрешения работы EN, в принципе на работе это не отразилось, но нет возможности сброса экрана из программы. Вся остальная схема запустилась без проблем, прошивается через micro-USB, через этот же разъем можно подключаться по Modbus например используя Modbus poll/mbslave, задействовал линии rx и tx которые идут на драйвер ADM3485 и они же используются при программировании.

Для создания проекта использовал ESP-IDF и порт LVGL. Данный пример содержит множество готовых драйверов дисплеев и примеры большинства виджетов библиотеки LVGL. Так как дисплей не имеет тачскрина, то мне были интересны примеры вывода графики в виде различных манометров и других, заточенных под круглые дисплеи, виджетов. Пример из библиотеки мне не очень понравился, хотелось использовать что-то более реалистичное, так как сам дисплей позволяет выводить довольно качественную картинку, решил использовать готовые подложки, их необходимо подготовить под разрешение 240x240, в принципе можно использовать любые подходящие, единственное перед конвертацией в графическом редакторе необходимо убрать стрелки, так как стрелка будет управляться отдельно из программы. Варианты стрелок так же можно подготовить в разном стиле и цвете, для разных подложек. Стрелка должна быть симметрична, так как из программы она будет вращаться относительно центра дисплея. Вся подготовленная под размер экрана графика конвертируется через сервис imageconverter в .с файл, который затем подключается в программе. Самый простой пример для манометра содержит два объекта, сама подложка она статическая и стрелка- изменяющийся объект.

img0 = lv_img_create(lv_scr_act(), NULL);//фон манометраlv_img_set_src(img0, &man5);lv_obj_align(img0, NULL, LV_ALIGN_CENTER, 0, 0);//стрелка, угол 60lv_img_set_angle(s6, 600);

Задание для стрелки выдается в отдельной задаче вызываемой с требуемым периодом:

static void update_time(void *arg){//    get_time(); //получить значение часов, минут, секундcur_time_s++;//    lv_img_set_angle(  lvHour, cur_time_h*30*10);//    lv_img_set_angle(  lvMinute, cur_time_m*6*10);    lv_img_set_angle(  lvSecond, cur_time_s*6*10);//задание угла для секундной стрелки}

Минимальный шаг задания 0,1, окружность разделена на 3600 частей. В зависимости от выбранной подложки можно рассчитать min и max задание в пределах шкалы. Остается преобразовать полученное значение от датчика в угол задания. Для демонстрации часов происходит управление тремя стрелками, достаточно поднять на ESP32 NTP сервер и получить текущее значение часов. минут, секунд (в моем примере NTP сервис не запущен) Для того, чтобы каждый раз не корректировать файлы стрелок под выбранный циферблат их можно масштабировать:

//удлинить минутную стрелкуlv_img_set_zoom(lvMinute, 340);

В примере продемонстрирован минимальный набор для запуска на дисплее, в самой библиотеке представлены примеры всех виджетов, большинство из которых имеет смысл на дисплеях с тачскрином. Ранее я проверил данную библиотеку на 7" дисплее с сенсорным тач контроллером.

Из недостатков можно отметить "тормознутость" на больших диагоналях из-за невозможности выделить полноценный буфер под полное разрешение экрана, особенно это проявляется при обновлении всего экрана, когда происходит скроллинг или открытие другого таба. Для простых кнопочных интерфейсов эта проблема не так актуальна, так как обновляется только область кнопки. Для представленного дисплея с разрешением 240x240 проблема не так заметна, но при использовании нескольких подложек сильно увеличивается размер прошивки. Пока вижу вариант загрузки графики с SD карты или использования ESP32 с памятью psram и размещением в ней графики или буфера. На моих платах интерфейс spi дисплея и sd карты разведены на одних и тех же пинах и, насколько я знаю, пока не удалось заставить их работать в тандеме, есть отдельные упоминания о работе но я пока не проверял. Если у кого-то есть работающий пример напишите в комментариях. Так же я пока не пробовал использовать память psram для размещения буфера дисплея, без этого памяти остается не так много. В чипе который используется ее нет, но она есть в чипе ESP32-WROVER на второй плате к которой можно подключить дисплей от Adafruit 4,3" , Waveshare 7" и данный круглый через переходник со шлейфом.

Общий вид отладочной платы

Дальше планирую проверить подключение по Modbus и можно использовать как небольшую панель для чтения параметров из внешних устройств. Так же на самой плате возле выводов модуля сделаны полигоны для возможности подключения датчиков, например ds18b20 или влажности/давления, внутри их размещать смысла нет, так как при закрытом корпусе там довольно жарко, но можно вывести наружу через отверстие. Так же можно организовать mqtt и получать данные с удаленного датчика. Наконец, есть еще bluetooth который я даже не включал, но думаю и там есть множество применений. Далее видео получившегося устройства с демонстрацией разных вариантов экранов.

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

Схему не привожу, так как ее у меня нет, плата разводилась "на лету" за чашкой чая вечером, обвязку для микросхем можно взять в pdf, esp32 так же позволяет переназначать все пины используемые в проекте. Для запуска примеров достаточно взять ESP32-DevKitC и плату с экраном GC9A01. Пины используемые в моем проекте: MOSI-12 CLK-14 CS-23 DC-22. SPI лучше использовать на пинах по умолчанию, тогда его можно будет запускать на "максималках", у меня первая плата была разведена не так и эту я оставил для совместимости проектов.

Перечень основных элементов:

  • ESP-WROOM-32

  • ADM3485EARZ-REEL7

  • CH340N

  • AMS1117

  • FPC SC 0.5MM 14P CB

  • 12PIN SPI TFT LCD GC9A01

Мой демо проект на github в папке components/image конвертированные файлы подложек для экранов.

Тут более свежие версии библиотеки с примерами.

Подробнее..

О том, как мы температуру в ЦОД мерили

21.04.2021 14:19:32 | Автор: admin

Если у вас большой и серьезный ЦОД, то параметрия температурных режимов не является проблемой. Существуют проверенные решения, например, программируемые контроллеры TAC Xenta, которые работают через LonWorks. Именно так мы собираем данные в московском ЦОД Datahouse. Но непосвящённому смертному весьма непросто собрать правильные показатели из этой связки и выводить их в мониторинг в нужном виде. К тому же решение промышленное и достаточно дорогостоящее. Поэтому при строительстве новой гермозоны
в Екатеринбурге мы решили поэкспериментировать и внедрить альтернативное решение по измерению температуры в холодных и горячих коридорах.

Ничто не предвещало беды

Так как множество систем в этом ЦОДе завязано на открытом коммуникационном протоколе Modbus мы решили заказать температурные датчики, работающие поэтой шине, и собирать данные с дальнейшей интерпретацией в интерфейсе мониторинга. Недорогие датчики быстро нашлись на известном китайском сайте и были заказаны партиями в количестве 20 и 40 штук.

Первая партия из 20 штук пришла достаточно оперативно, но при детальном рассмотрении стало понятно, что датчики незначительно отличаются корпусами. Важно ли это, как выяснилось да.

Из первой партии завелось 15 датчиков. Так как острой потребности в остальных не было, пока работали с ними. К моменту прихода второй партии выявили, что часть уже установленных в шину датчиков имеет поведение новогодней елки: показывают некорректные данные, отдают checksum error или отваливаются по таймауту.

Анализ второй партии показал, что в ней процент брака в разы выше.
В итоге из 60 датчиков мы получили всего 8 стабильных с правильными показаниями.

Вот так это выглядело:

Самое простое решение отказаться от этой затеи или заказать кучу датчиков и выбрать
из них не бракованные, но это не наш метод.

Не ищем легких путей

Вскрыли, посмотрели: микросхемы идентичны. Значит дело в прошивке.

Кстати, обратите внимание на заводское крепление кабеля оно было лишь на половине устройств. На остальных пришлось заливать кабель из термопистолета получилось прочней
и надежней.

Пока разбирались с работоспособностью датчиков поняли, как без особых сложностей обнаружить кривую версию прошивки. Если кроме Modbus работают обычные текстовые команды READ, PARAM, AUTO, STOP значит прошивка хорошая. В мертвых прошивках текст не отдается.

Решили взять прошивку с этих живых 8 датчиков, купили программатор Nu-Link,
но хитрые китайцы заблокировали чтение прошивки. То есть перезалить можно, а считать - нельзя. Запросы правильных прошивок у поставщика потерпели фиаско:
Я продаван, я не разработчик.

И тут я психанул, схватил крепкие напитки и закрылся у себя.
Через пару дней вышел с прошивками и программой.

За основу был взят Keil, пакет С51, позволяющий работать с 8-битными MCU.

В начале я научил читать сенсор SHT 20 (который собственно и снимает температурные данные), потом научил передавать эти данные по Modbus. В виду того, что этот MCU ни что иное как Nuvoton N76E003AT20, то вся база знаний, видимо, сосредоточена в руках наши китайских друзей.

В итоге i2c и Modbus сделал быстро, а вот с таймерами пришлось повозиться. Чтобы не было коллизий в шине, добавил возможность смены SLAVE_ID без выключения датчика в Китайской версии прошивки после смены адреса его нужно было обязательно выключать, что не очень удобно.

Начали шить новой прошивкой, из семи прошитых получили семь успешных и стабильных устройств. Так понемногу реанимировали все датчики, присвоили им номера и подключили витой парой в шину.

Так стало:

Если говорить про результаты измерений, то нормальные показания появляются только
в помещении с явной циркуляцией и движением воздуха. Без продува датчик показывает 30С. Это объясняется тем, что внутри установлен стабилизатор напряжения, который преобразует 24В в 3.3В, переводя разность в тепло.

Относительно влажности, пока можно утверждать, что требуется дополнительная калибровка, но способа ее произвести мы пока не придумали. Теме не менее, прямое воздействие, например, дыханием увеличивает показания влажности. В любом случае, через пару регистров это можно сделать по месту установки.

Несмотря на возникшие осложнения, данное решение имеет два очевидных преимущества: стоимость и гибкость. Датчики можно установить в любом удобном месте, точечно или объединить в гирлянды. Можно измерять как общую температуру, так и частные показания отдельных приборов и устройств. И самое важное, что все это отлично работает по Modbus.

Программа выложена на GitHub кому интересно, можно забрать и поиграться.
Стоимость датчика всего 300 , правда, нужен программатор.

Подробнее..

Сетевой интерфейс для программируемого реле с поддержкой Telegram Bot и HomeKit

07.05.2021 14:19:42 | Автор: admin

Как я реализовал удаленное управление и мониторинг, для программируемого реле ПР200, используя разные сервисы (Telegram Bot, HomeKit) и протоколы (Modbus RTU, Modbus TCP, mqtt) и ESP32.

Год назад была статья про управление освещением на ПР, где я на примере показал наиболее часто используемые алгоритмы. Пример демонстрировал работу с клавишными выключателями, подключенными к дискретным входам. В эпоху, когда даже у чайника есть облачный сервис или другой режим удаленного доступа, было бы странно не развить данное направление и для программируемого реле.

Долгое время я использовал сетевой интерфейс на основе модуля esp8266, на сегодня более перспективным вижу использование модуля esp32, долгое время я не рассматривал его из-за размеров, но впоследствии, проработав разные варианты, удалось не только вместить все на одной плате, но и сделать решение более универсальным и удобным.

Первая версия платы на основе ESP32

Во второй версии добавлен usb порт, что позволяет загрузить прошивку без использования дополнительных проводов и преобразователей. Базовая реализация изначально задумывалась с возможностью обновления прошивки по воздуху через ОТА, а для экспериментов проще использовать microUSB.

В обновленном варианте добавил ещё и кнопки сброса и загрузки при прошивке, а так же добавил поддержку модулей ESP32-WROVER с PSRAM, это позволит использовать больше памяти и расширит возможности.

В общем, структура взаимодействия сетевой платы с программируемым реле основана на протоколе modbus rtu, а с внешним миром варианты могут быть самые разнообразные от bluetooth до TelegramBot.

TelegramBot

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

Для использования бота достаточно получить токен и ввести его в поле ввода и сохранить, значение сохраняется в энергонезависимой памяти, после перезагрузки бот запустится автоматически.

Для универсальности взаимодействие бота с алгоритмом в приборе, использован режим чтения/записи сетевых регистров Modbus а разных форматах представления:

/R- целое 16 битное значение

/I-целое число занимающее 2 регистра

/F- число в формате float тоже 2 регистра.

После символа адрес в диапазоне 512-576, эти регистры можно читать и записывать, формат для записи /Xzzz=nnnn, для чтения достаточно отправить номер регистра в требуемом формате.

Для представления состояний регистра в битовых полях, можно отправить адрес в формате /Bzzz, ответ будет в виде 16 значения в булевом формате.

Apple HomeKit

Следующим этапом был сервис Apple HomeKit и приложение дом, как раз для управления освещением и другими точечными нагрузками он подходит лучше всего, я начал с 16 каналов, по количеству бит в регистре модбас.

После выхода обзора по такому применению,

многие задавали вопрос о раздельной привязке выключателей по разным комнатам. Сейчас это решено, после добавления аксессуара в системе появляется 16 независимых выключателей.

Для передачи в алгоритм реле используется один сетевой регистр, а уже в самой логике бит каждого выключателя может иметь разное назначение, от прямого включения выходного реле, до использования в качестве разрешающих сигналов для запуска других алгоритмов, аналог битовых полей назначаемых с экрана. В перспективе можно реализовать и другие сервисы HomeKit.

Для быстрого добавления платы в приложение Дом, на web страницу добавлен QR код, второй вариант ввести код настройки, индивидуальный для каждой платы. Постарался упростить и минимизировать все настройки для быстрого старта.

Так же протестировал mqtt, идея задания топиков взята из версии платы для esp8266. Проверил поддержку датчиков 1-wire ds18b20, для их подключения к плате предусмотрены посадочные места под разъем, и сигнальные линии с резисторами, такой-же использовался в плате prsd на esp8266.

4 пина, два из которых +3.3v и gnd, позволяют задействовать 2 порта в качестве интерфейса 1-wire или i2c. I2C позволяет подключать всякую экзотику, которую практически невозможно состыковать в базовой поставке прибора. Например, датчик влажности/давления с I2C или RFID ридер.

Для быстрого просмотра значений регистров используется протокол Modbus TCP, запустив Modbus Poll на ПК или Virtuino/Kascada и другие приложения на Android, можно быстро организовать доступ и управление устройством с помощью телефона или планшета.

Остальные настройки WEB интерфейса представлены ниже:

WEB настройки

Для смены прошивки платы, когда она уже установлена в устройство, предусмотрен режим обновления по воздуху (OTA), для этого достаточно выбрать bin файл, после загрузки прошивки устройство перезагрузится и запустится обновленная версия. Так же можно перезагрузить плату в ручном режиме через web кнопку.

При первом старте, когда устройство не имеет настроек точки доступа и пароля и не может подключиться к сети wi-fi, плата включает режим точки доступа для подключения и ввода ssid и pass, после сохранения значений и перезагрузки если подключение к сети успешно, точка доступа выключается. Если токен Telegram bot введен, то после подключения и выхода в интернет, узнать IP адрес платы можно введя команду. Через бот можно получить и другую информацию.

Основные моменты по работе представлены в видео.

На данный момент прошивка находится в режиме доработки, демонстрационные версии будут доступны позже.

Для экономии ресурсов ESP32 в перспективе думаю разделить на версии с поддержкой того или иного функционала, памяти в esp32 хоть и больше в сравнении с esp8266, но одновременная работа всех сервисов может снизить ресурсы до критического уровня.

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

В меню бота есть возможность смотреть время бесперебойной работы в секундах, текущий и минимальный объем доступной Heap памяти, используя один и тот же токен для TelegramBot, можно читать параметры сразу с нескольких устройств, аналогично и на запись параметров. Т.е. любая команда, отправленная с телефона боту, отправляется всем устройствам с токеном, это происходит не всегда, иногда отвечает только часть устройств, и приходится делать запросы повторно. Сейчас все они не имеют привязки к имени, думаю сделать уникальные имена для отличия при запросах/ответах.

Используя несложный сетевой интерфейс с чипом ESP32, можно значительно расширить функционал программируемого реле ПР200 и в перспективе ПР103, куда можно установить сетевой интерфейс, другие модели ПР100/ПР102 потребуют внешний драйвер RS-485 для подключения снаружи, так как сетевые интерфейсы в них не съемные.

Наличие программатора на борту платы, позволяет создать собственные алгоритмы с быстрой загрузкой в устройство, необходимо лишь обеспечить обмен по протоколу Modbus на стандартных выводах UART esp32.

Подробнее..

Портирование ModBus Slave RTUASCII на IAR AVR v3

27.11.2020 16:12:57 | Автор: admin


Я уже десять лет не писал под AVR А вдруг разучился?! Для проверки я решил портировать библиотеку ModBus Slave RTU/ASCII без смс и регистрации на платформу IAR AVR, а также, по просьбам читателей, показать демку подключения к панели оператора Weintek.


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


Спаян шнурок для программатора AVReal.


В хламе найдена макетная плата с ATMega48. Фотография макетной платы на на первом рисунке.

Поехали!


Для портирования библиотеки ModBus Slave RTU/ASCII без смс и регистрации необходимо написать интерфейсы системного таймера и последовательного порта. У автора нездоровая привычка, писать низкоуровневый ввод/вывод для AVR на ассемблере. В нашем случае, я не буду отказывать себе в своих привычках.
Заголовочный файл systimer.h
#ifndef __SYSTIMER_H#define __SYSTIMER_H#ifdef __SYSTIMER_ASM#define CLKSysTimer (8000000/64)#else#include "main.h"//Инициализацияvoid InitSysClock(void);//время от запуска в милисекундахunsigned long Clock(void);#endif#endif


Файл systimer.asm
#define __SYSTIMER_ASM#include <iom48.h>#include "systimer.h"MODULE __systimerCOMMON INTVECORG TIMER0_COMPA_vect  rjmp tim0_compRSEG CODEtim0_comp:  in r10,SREG  inc r11  add r12,r11  dec r11  adc r13,r11  adc r14,r11  adc r15,r11  out SREG,r10  retiPUBLIC InitSysClockInitSysClock:  cli  clr r11  clr r12  clr r13  clr r14  clr r15  push r16  ldi r16,(0<<COM0A1)|(0<<COM0A0)|(0<<COM0B1)|(0<<COM0B0)|(1<<WGM01)|(0<<WGM00)  out TCCR0A,r16  ldi r16,(0<<FOC0A)|(0<<FOC0B)|(0<<WGM02)|(3<<CS00)  out TCCR0B,r16  ldi r16,(CLKSysTimer/1000)  out OCR0A,r16  ldi r16,(0<<OCIE0B)|(1<<OCIE0A)|(0<<TOIE0)  sts TIMSK0,r16  pop r16  retiPUBLIC ClockClock:  cli  movw r16,r12  movw r18,r14   retiENDMODEND


В качестве системного таймера используется TIMER0. В прерывании по совпадению таймера (COMPA), происходящем каждую милисекунду, инкрементируется четырехбайтная переменная находящаяся в регистрах r12-r15. Эти регистры не поддерживают работу с константами, поэтому для инкремента приходится использовать регистр r11. Регистр r10 используется для сохранения регистра состояния процессора. Перечисленные регистры зарезервированы в настройках компилятора.
Значение переменной r12-r15, через атомарную операцию считывается функцией Clock(), необходимой для работы библиотеки ModBus Slave RTU/ASCII.
Частота прерываний таймера определяется константой CLKSysTimer в заголовочном файле. Значение константы отношение тактовой частоты процессора к пределителю таймера.

Интерфейс последовательного порта.
Заголовочный файл uart.h
#ifndef __UART_H#define __UART_H#ifdef __UART_ASM#define CLK_Uart (8000000)#define UartSpeed (19200)#define FIFORX (32)#define FIFOTX (64)#else#include "main.h"void UartInit(void);unsigned short Inkey16Uart(void);void PutUart(unsigned char a);#endif#endif


Файл uart.asm
#define __UART_ASM#include <iom48.h>#include "uart.h"MODULE __uartrxtxRSEG NEAR_Zrxfifo: //Буфер FIFO  DS FIFORXrxHead://голова, пишем на голову  DS 1 rxTail://хвост, читаем с хвоста  DS 1 txfifo://Буфер FIFO  DS FIFOTXtxHead://голова, пишем на голову  DS 1 txTail://хвост, читаем с хвоста и в UART  DS 1 COMMON INTVECORG USART_RX_vect  rjmp uart_rxORG USART_TX_vect  rjmp uart_tx  RSEG CODE//void UartInit(void);PUBLIC UartInitUartInit:  sbi PORTD,0  cli  push r16  //обнуление указателей  clr r16  sts rxHead,r16  sts rxTail,r16  sts txHead,r16  sts txTail,r16  //Скорость передачи  ldi r16,LOW((CLK_Uart/8+UartSpeed/2)/UartSpeed-1)  sts UBRR0L,r16  ldi r16,HIGH((CLK_Uart/8+UartSpeed/2)/UartSpeed-1)  sts UBRR0H,r16  //Enable receiver and transmitter, разрешение прерываний  ldi r16,(1<<RXEN0)|(1<<TXEN0)|(1<<RXCIE0)|(1<<TXCIE0)  sts UCSR0B,r16  //Set frame format: 8data, 1stop bit, Parity No  ldi r16, (0<<UMSEL00)|(0<<UPM00)|(0<<USBS0)|(3<<UCSZ00)  sts UCSR0C,r16  //сброс флагов прерываний UART  lds r16,UCSR0A  ori r16,(1<<TXC0)|(1<<U2X0)   sts UCSR0A,r16  lds r16,UDR0  pop r16  reti//Обработчик прерывания по приемуuart_rx:  push r16  in r16,SREG  push r16  push XL  push XH//UART->FIFO    lds r16,rxHead  ldi XL,LOW(rxfifo)  ldi XH,HIGH(rxfifo)  add XL,r16  adc XH,r16  sub XH,r16  inc r16  andi r16,(FIFORX-1)  sts rxHead,r16  lds r16,UDR0  st X,r16  pop XH  pop XL      pop r16  out SREG,r16  pop r16  reti//unsigned short Inkey16Uart(void);//Если нет данных возвращает 0х0000, иначе возвращает 0х01ХХPUBLIC Inkey16UartInkey16Uart:  lds R17,rxHead  lds r16,rxTail  cp r16,r17  breq Inkey16Uart1  //читаем данные из FIFO    push XL  push XH  ldi XL,LOW(rxfifo)  ldi XH,HIGH(rxfifo)  add XL,r16  adc XH,r16  sub XH,r16    inc r16  andi r16,(FIFORX-1)  sts rxTail,r16    ld r16,X  pop XH  pop XL  ldi r17,1  retInkey16Uart1:  clr r16  clr r17  ret//обработчик прерывания по передачеuart_tx:  push r16  in r16,SREG  push r16  push r17  //проверяем наличие данных в буфере  lds r17,txHead  lds r16,txTail  cp r16,r17  brne uart_tx2    rjmp uart_tx_enduart_tx2://если данные есть - передаем    push XL  push XH  ldi XL,LOW(txfifo)  ldi XH,HIGH(txfifo)  add XL,r16  adc XH,r16  sub XH,r16    inc r16  andi r16,(FIFOTX-1)  sts txTail,r16    ld r16,X  sts UDR0,r16  pop XH  pop XLuart_tx_end:  pop r17  pop r16  out SREG,r16  pop r16  reti  //void PutUart(char a);PUBLIC PutUartPutUart:  push XL  push XH//проверяем наличие данных в буфере  lds XH,txHead  lds XL,txTail  cp XH,XL  brne PutUart1//проверякм регистр передачи  lds XL,UCSR0A  sbrs XL,UDRE0  rjmp PutUart1  sts UDR0,r16  pop XH  pop XL  retPutUart1://положить в txfifo[]    push r16  mov r16,XH  ldi XL,LOW(txfifo)  ldi XH,HIGH(txfifo)  add XL,r16  adc XH,r16  sub XH,r16  inc r16  andi r16,(FIFOTX-1)  sts txHead,r16  pop r16  st X,r16  pop XH  pop XL      retENDMODEND


Интерфейс последовательного порта реализован по классической схеме. Как на прием, так и на передачу реализован тип данных очередь на кольцевом буфере. Размер буфера приема и передачи определяется константами FIFORX, FIFOTX соответственно. В целях экономии вычислительных ресурсов процессора, размер буферов приема и передачи должен быть кратен 2^N (2,4,8,16,32...), но не больше 256.
Скорость приема/передачи последовательного порта определяется константой UartSpeed. При тактовой частоте микроконтроллера (определяется константой CLK_Uart) 8МГц, то есть, при использовании внутреннего RC-генератора нет возможности использовать высокие скорости передачи.

При попытке скомпилировать файл библиотеки modbus.c, IAR заругался страшными словами. Компилятор IAR AVR не умеет много чего из стандарта С99. Он также не умеет, при использовании модификатора const, размещать объекты в памяти программ, для этого служит специальный модификатор __flash. Пришлось потратить несколько минут для приведения кода в соответствие требованиям компилятора.
В файле библиотеки modbus.h необходимо определить макросы вызова функций последовательного интерфейса и системного таймера.
//Системный таймер, инкрементируется каждую милисекунду#define ModBusSysTimer Clock()//Запись байта в поток последовательного порта - void ModBusPUT(unsigned char A)#define ModBusPUT(A) PutUart(A) //Чтение байта из потока последовательного порта, - unsigned short ModBusGET(void)//Если нет данных возвращает 0х0000, иначе возвращает 0х01ХХ#define ModBusGET() Inkey16Uart()

А также определить количество дискретных входов/выходов, регистров для чтения и регистров для чтения/записи. На этом портирование библиотеки можно считать законченным.

Демка


Для демонстрации возможностей библиотеки ModBus Slave RTU/ASCII подключим наше устройство к панели оператора Weintek. В микроконтроллере организованы часы, значения часов, минут секунд выводятся в регистры Modbus, код содержится в файле ModBus2Prg.c:
void Prg2ModBusOutReg(void)  {//заполнение регистров 4Х регистры для чтения/записи  ModBusOutReg[0]=Seconds;  ModBusOutReg[1]=Minutes;  ModBusOutReg[2]=Hours;   return;  }void Prg2ModBusInReg(void)  {//заполнение регистов 3Х регистры для чтения  ModBusInReg[0]=Seconds;  ModBusInReg[1]=Minutes;  ModBusInReg[2]=Hours;    return;  }

Через регистры чтения/записи можно произвести установку часов:
void ModBus2PrgOutReg(void)  {//чтение регистров 4Х регистры для чтения/записи  Seconds=ModBusOutReg[0];  Minutes=ModBusOutReg[1];  Hours=ModBusOutReg[2];   return;  }

Дискретные входы/выходы Modbus подключены к портам вывода микроконтроллера. Так же к дискретному входу 4 подключен счетчик полусекунд:
void Prg2ModBusOutBit(void)  {//заполнение регистров дискретных выходов  ModBusOutBit[0].bit0=PORTC_Bit1;  ModBusOutBit[0].bit1=PORTC_Bit2;  ModBusOutBit[0].bit2=PORTC_Bit3;  ModBusOutBit[0].bit3=PORTC_Bit4;  return;  }void Prg2ModBusInBit(void)  {//заполнение регистров дискретных входов  ModBusInBit[0].bit0=PORTC_Bit1;  ModBusInBit[0].bit1=PORTC_Bit2;  ModBusInBit[0].bit2=PORTC_Bit3;  ModBusInBit[0].bit3=PORTC_Bit4;  ModBusInBit[0].bit4=PoluSeconds;  return;  }

Через дискретные выходы можно управлять состоянием портов микроконтроллера:
void ModBus2PrgOutBit(void)  {//чтение регистров дискретных выходов  PORTC_Bit1=ModBusOutBit[0].bit0;  PORTC_Bit2=ModBusOutBit[0].bit1;  PORTC_Bit3=ModBusOutBit[0].bit2;  PORTC_Bit4=ModBusOutBit[0].bit3;  return;  }

Программное обеспечение панели разрабатывается в среде EasyBuilder Pro v5.

Подключаем микроконтроллер через USB преобразователь к компьютеру, указываем в настройках проекта панели протокол обмена Modbud RTU и настройки COM-порта через который произошло подключение. Запускаем онлайн симуляцию панели.

Код демки с использованием библиотеки ModBus Slave RTU/ASCII со всеми опциями, после компиляции IAR AVR v3 с оптимизацией по скорости оказался на удивление компактным. Он занимает 3024 байта памяти программ, 398 байт памяти данных. Искренне надеюсь, что библиотека ModBus Slave RTU/ASCII найдет широкое применение для разработки Modbus устройств на маломощных микроконтроллерах.

Проект на GitHub
Видео демки
Подробнее..

Кому в микроконтроллере жить хорошо?

04.12.2020 00:04:03 | Автор: admin

В каком году рассчитывай, в какой земле угадывай, задачился вопросами. Насколько ARM быстрее AVR? Какая разновидность протокола Modbus более быстрая? ASCII или RTU?

Под быстротой, в данном случае, будем понимать количество машинных циклов процессора необходимых для исполнения всех действий протокола.
Исследование быстродействия будем проводить на, широко известной в узких кругах, библиотеке ModBus Slave RTU/ASCII, портированной на микроконтроллеры ATMega48 и STM32L052. Вывод информации будем осуществлять по протоколу Modbus в эмулятор панели Weintek. Тестирование будем производить на демонстрационном примере. Помимо результатов теста на панель выводятся состояния регистров Modbus: дискретных входов, дискретных выходов, регистров для чтения и регистров для чтения/записи. Также средствами панели производится подсчет количества ошибок обмена данными с микроконтроллером. Внешний вид тестового окна приведен на рисунке.



Оценку быстродействия будем проводить измеряя время выполнения функции обработки сообщений протокола. Измерение времени выполнения будем проводить рабоче-крестьянским способом. Перед запуском функции обнуляем аппаратный таймер, частота счета которого равна тактовой частоте микроконтроллера, после выполнения функции считываем значения таймера и проводим обработку результатов измерений. Вычисляем минимальное, максимальное и среднее значение времени выполнения функции обработки сообщений протокола Modbus.
while(1)    {    TIM6->CNT=0;    ModBusRTU();    //ModBusASCII();    tcurent=TIM6->CNT;    if(tcurent<tmin)tmin=tcurent;    if(tcurent>tmax)tmax=tcurent;    avg32=avg32-(avg32>>16)+tcurent;    tavg=avg32>>alfa;    ...

Результаты исследований, сведены в таблицу. Исследование проводились при различных опциях библиотеки:
  • ModBusUseTableCRC Использовать расчет CRC по таблице;
  • ModBusUseErrMes Использовать сообщения о логических ошибках протокола;

А также при различных стратегиях оптимизации компилятора.
Библиотека ModBus Slave RTU/ASCII поддерживает, в некоторых случаях, важную функцию пауза между получением запроса от Modbus Master и ответом Modbus Slave. Исследование проводились при значениях паузы 2 милисекунды и 0 (то есть без паузы), эти значения указаны в графе таблицы Пауза П/П. В графе Размер указан размер модуля, который включает в себя обе функции обработки сообщений Modbus (ModBusRTU(), ModBusASCII()).



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

Глубоко задумавшись над результатами исследований, можно сделать следующие выводы:
  1. AVR не такой уж медленный!!! В среднем он в полтора раза медленнее ARM при той же тактовой частоте. А в случае оптимизации по размеру (например варианты 13 и 15) практически приближается к ARM.
  2. Протокол ASCII, по сравнению c RTU, не только более медленный по скорости передачи, но и занимает гораздо больше ресурсов микроконтроллера.
  3. Использование сообщений о логических ошибках протокола, никак не влияет на быстродействие.
  4. Табличный метод вычисления CRC позволяет более чем в полтора раза снизить использование вычислительных ресурсов микроконтроллера.
  5. Использование паузы между приемом запроса и передачей ответа, позволяет не только избежать конфликтов на шине RS-485, но и уменьшить блокирующие действие функции обработки сообщений протокола Modbus.


Какие выводы еще можно сделать?

Проект на GitHub


Скачать одним файлом

Подробнее..

Управление наружным освещением

24.03.2021 00:15:52 | Автор: admin

Однажды передо мной встала задача модернизации системы управления наружным освещением: художественная подсветка фасадов, рекламные вывески, уличные столбы, подсветка адресных табличек и т. п. Необходимость модернизации была вызвана тем, что работа осветительных установок требовала частого участия человека. Для того чтобы управление работало как положено, приходилось вручную корректировать время включения и отключения.

Модернизация предполагала максимальное использование существующей инженерной и IT инфраструктуры. Требовалось воздержаться от монтажа нового или замены старого оборудования, свести к минимуму перечень используемого ПО. Принципы, применяемые в системе управления, должны быть универсальными и пригодными для повторного использования. Должна быть обеспечена возможность легкого расширения системы. Осветительные установки, вводимые в эксплуатацию, могут быть смонтированы в разных районах города, в разных регионах или странах.

Разбираемся с инфраструктурой

На практике мне встречалось несколько подходов к управлению наружным освещением: регламентированное включение и отключение силами оперативного персонала, применение реле времени, сумеречных датчиков, фотореле и астрономических реле.

Использование астрономических реле является, наверное, одним из самых интересных решений. Их работа основана на определении времени восходов и закатов в определенном географическом положении. Во время заката освещение включается, во время восхода - отключается. Как правило, при настройке таких реле имеется возможность установки временных корректировок для ускорения или замедления действия реле. Внесение таких корректировок позволяет включить (отключить) освещение на несколько минут позже заката или раньше рассвета.

По похожей схеме работает и модернизируемая нами система. В роли астрономического реле выступает свободно программируемый логический контроллер (ПЛК). По линии RS485 ПЛК управляет проприетарными модулями ввода-вывода, которые установлены в разных зданиях. Управление и настройка системы осуществляется с использованием SCADA и OPC-сервера, установленного на одной из машин в сети Ethernet, к которой подключен ПЛК. Все используемое ПО является проприетарным.

Выявленные недостатки и их причины

В ходе эксплуатации осветительных установок стало понятно, что система управления требует постоянного внимания персонала. По большому счету все сводилось к тому, что становилось очевидно: подсветка зданий включается слишком поздно, а отключается слишком рано. Эта проблема решалась корректировкой коэффициентов в системе диспетчеризации. Путем ручного подбора коэффициентов удавалось достигнуть времени включения и отключения, соответствующего городскому графику. Коэффициенты оставались неизменными до следующей подобной ситуации. А повторялись такие ситуации довольно часто.

Было сделано предположение, что причина проблемы кроется в проекте, загруженном в ПЛК. Ознакомиться с исходниками проекта не представлялось возможным. Из текстового описания проекта стало понятно, что для определения времени включения/отключения освещения используется функциональный блок, доступный в проприетарной среде программирования. Используя триальную версию этой среды, удалось получить доступ к справке и более подробному описанию этого блока. Это внесло некоторую ясность: функциональный блок высчитывает время, когда угол (высота) Солнца над горизонтом для заданного географического положения станет равен 0. Коэффициенты корректируют этот угол. Например, при коэффициенте "-6" будет высчитано время, когда Солнце окажется ниже горизонта на 6. Но в ходе проведенных экспериментов сложилось мнение, что функциональный блок производит расчеты не совсем так, как это предполагается. Дальнейшие работы в этом направлении были прекращены ввиду отсутствия универсальности такой реализации.

Начинаем модернизацию

При рассмотрении существующих вариантов, я склонялся к варианту управления по расписанию. Не секрет, что существуют общедоступные графики отключения наружного освещения. Для Москвы и Санкт-Петербурга, например - МОССВЕТ и ЛЕНСВЕТ. Обладая этой информацией несложно написать скрипт, который следит за временем и по графику управляет освещением. Тем более, что в сети Ethernet, к которой подключен ПЛК, имеется Linux-машина.

Для того чтобы такой скрипт мог управлять удаленными модулями ввода-вывода, потребовалось заново создать проект для ПЛК. По-сути ПЛК стал выступать в роли шлюза, что предоставило нам возможность управлять освещением по открытому протоколу Modbus TCP.

Для работы с Modbus будем использовать утилиту modpoll. Скачаем и распакуем ее на нашей Linux машине:

$ wget https://www.modbusdriver.com/downloads/modpoll.tgz$ tar xzf modpoll.tgz$ sudo cp modpoll/linux_x86-64/modpoll /usr/local/bin/

Теперь управлять освещением будем следующим образом:

#Включить освещение$ modpoll -m tcp -r 2 -t 0 -a 1 -p 502 192.168.0.227 1 1 1 1 1 1 1 1 #Отключить освещение$ modpoll -m tcp -r 2 -t 0 -a 1 -p 502 192.168.0.227 0 0 0 0 0 0 0 0 

Дело осталось за малым - вызывать эти команды по заранее сформированному расписанию. Как было упомянуто выше, подобные графики доступны и мы можем ими воспользоваться. Но появилось желание найти более универсальный подход, поэтому давайте вспомним о таком явлении, как "сумерки".

Сумерки

В сутках существуют периоды, называемые сумерки. Это время перед восходом Солнца и после заката, когда небо частично освещено рассеянным солнечным светом. Выделяют три вида сумерек: гражданские, навигационные и астрономические. Гражданские сумерки определяются как период, когда угол нахождения Солнца под горизонтом составляет от 050 до 6, навигационные сумерки от 6 до 12, а астрономические сумерки от 12 до 18.

Наибольший интерес для нас представляют навигационные сумерки. Именно в этот период освещение становится ближе к ночному, чем к вечернему, поэтому улицы городов нуждаются в искусственном освещении. Проще говоря, наружное освещение включается с началом навигационных сумерек (Солнце опускается ниже -6 ) и отключается с их окончанием (Солнце поднимается выше -6 ). Конечно стоит понимать, что в зависимости от погодных условий и в условиях городской застройки, включение может потребоваться раньше, чем Солнце опустится ниже -6 за горизонт.

Еще немного об астрономических реле.

Когда мы обсуждали работу астрономических реле, то упоминали временные корректировки, ускоряющие или замедляющие действие реле. Такой подход с задержкой времени имеет недостатки. Как мы выяснили, освещенность на улицах в основном определяется углом Солнца относительно горизонта. В одном и том же географическом положении скорость, с которой Солнце опускается за горизонт, изменяется в течении года. Или, иначе говоря, в зависимости от времени года изменяется продолжительность сумерек и навигационные сумерки могут наступать позже или раньше. Из-за этого временные задержки в астрономических реле требуют периодических изменений.

Если управлять освещением, опираясь именно на фактическое положение Солнца, то такая проблема отсутствует.

Подробный и крайне наглядный рассказ о движении Солнца был найден на Youtube - Как солнце ходит по небу / How the sun moves across the sky (by daybit).

Положение Солнца и наружное освещение

Итак, для решения нашей задачи мы будем синхронизировать работу наружного освещения с положением Солнца. При наступлении навигационных сумерек - включение освещения, в момент начала гражданских - отключение. Так как в нашем распоряжении имеется Linux машина и соответственно Perl, то для расчета положения Солнца воспользуемся им. Загрузим необходимый нам модуль:

$ sudo cpan install Astro::Coord::ECI

Создадим скрипт get_sun_elevation.pl, вычисляющий угол Солнца относительно горизонта.

#!/usr/bin/perl# Вычисление высоты Солнца над горизонтом в градусах в текущий момент# get_sun_elevation.pl 55.7558 37.6173 127# 55.7558 - широта в градусах# 37.6173 - долгота в градусах# 127 - высота над уровнем моря в метрахuse Astro::Coord::ECI::Sun;use Astro::Coord::ECI::Utils qw{:all};my ($lat, $lon, $elev) = (deg2rad($ARGV[0]), deg2rad($ARGV[1]), $ARGV[2]/1000);my $time = time ();my $loc = Astro::Coord::ECI->geodetic ($lat, $lon, $elev);my $sun = Astro::Coord::ECI::Sun->universal ($time);my ($azimuth, $elevation, $range) = $loc->azel ($sun);print rad2deg ($elevation), "\n";

Скрипт moscow_lights_ctrl.sh будет сравнивать заданное положение Солнца и его текущее положение в Москве. Если Солнце окажется ниже заданного угла, то отправим команду на включение, иначе - команду на отключение освещения:

#!/bin/sh[ -z "$1" ] && angle=-6 || angle=$1sun_angle=`./sun_pos.pl 55.751244 37.618423 124`if [ $(echo "$sun_angle >= $angle" |bc -l) -eq "0" ]; then  modpoll -m tcp -r 2 -t 0 -a 1 -p 502 192.168.0.227 1 1 1 1 1 1 1 1  exit 0fimodpoll -m tcp -r 2 -t 0 -a 1 -p 502 192.168.0.227 0 0 0 0 0 0 0 0

Опытным путем было определено, что на модернизируемом объекте потребность в наружном освещении возникает, когда Солнце опускается ниже -1.5. К слову, также было замечено, что городское освещение включается примерно в это же время.

С помощью cron будем выполнять moscow_lights_ctrl.sh каждую минуту:

# Если Солнце ниже 1.5 градусов - включение освещения, иначе - отключение* * * * * root /path/to/moscow_lights_ctrl.sh -1.5

Нам ничего не мешает создавать такие скрипты для любого географического положения. А когда возникнет необходимость расширения системы, мы сможем применить любое оборудование. Лично я склоняюсь к использованию модулей ввода-вывода, поддерживающих протокол Modbus TCP.

По большому счету все поставленные цели достигнуты. Модернизацию можно считать успешно завершенной.

ZABBIX

В ходе работ появились некоторые планы на ближайшее будущее, а именно настройка мониторинга работы оборудования. Возможности такого мониторинга сильно зависят от степени готовности самих инженерных систем. Например, мы можем следить за положением силового контактора и контролировать включение освещения. Или получать значение силы тока и тем самым определять, сколько ламп вышло из строя и т.д. К сожалению, на текущий момент к полноценному мониторингу модернизируемая система не готова. Тем не менее, для задела на будущее было решено использовать уже существующую на предприятии систему мониторинга - ZABBIX.

Все принципы работы остаются неизменными. Мы лишь перенесем всю описанную выше логику управления в ZABBIX.

Шаблон для ZABBIX

Создадим шаблон astro_outdoor_lighting для Zabbix со следующими макросами:

  • {$CIVIL_DEGREES} - Окончание и начало гражданских сумерек в градусах. Включение и отключение наружного освещения,

  • {$ELEV} - Высота над уровнем моря в метрах,

  • {$LAT} - Широта в градусах,

  • {$LON} - Долгота в градусах.

Элементы данных

Шаблон содержит только один элемент данных - elevation. Этот элемент следит за положением солнца в заданном географическом положении.

Чтобы получать текущее положение Солнца, элемент осуществляет внешнюю проверку через ранее созданный скрипт get_sun_elevation.pl.

/usr/lib/zabbix/externalscripts/get_sun_elevation.pl
#!/usr/bin/perl# Вычисление высоты Солнца над горизонтом в градусах в текущий момент# get_sun_elevation.pl 55.7558 37.6173 127# 55.7558 - широта в градусах# 37.6173 - долгота в градусах# 127 - высота над уровнем моря в метрахuse Astro::Coord::ECI::Sun;use Astro::Coord::ECI::Utils qw{:all};my ($lat, $lon, $elev) = (deg2rad($ARGV[0]), deg2rad($ARGV[1]), $ARGV[2]/1000);my $time = time ();my $loc = Astro::Coord::ECI->geodetic ($lat, $lon, $elev);my $sun = Astro::Coord::ECI::Sun->universal ($time);my ($azimuth, $elevation, $range) = $loc->azel ($sun);print rad2deg ($elevation), "\n";

Подробности настройки внешних проверок в ZABBIX смотрите в документации.

Триггеры

Единственный триггер civil_twilight_dawn срабатывает по окончании гражданских сумерек, т. е. в момент, когда возникает необходимость в работе наружного освещения.

Шаблон созданного макроса доступен на github.

Добавляем узел сети

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

Скрипты и действия ZABBIX

В разделе [Администрирование]->[Скрипты] создадим глобальные скрипты с говорящими названиями facade light off и facade light on.

Когда триггер civil_twilight_dawn переходит в состояние "Проблема", нам необходимо включить наружное освещение, т.е. выполнить скрипт facade light on. После восстановления триггера освещение необходимо отключить, для чего потребуется вызвать скрипт facade light off. Поэтому в разделе [Настройки]->[Действия] мы создадим действие facade light, реализующее необходимое нам поведение системы.

Подобным же образом добавляем скрипты и действия для каждого узла сети.

На этом настройку ZABBIX сервера для управления установками наружного освещения можно считать завершенной.

Заключение

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

Как мне видится, даже в простейшем варианте (работа по cron) такой подход перекрывает потребности большинства подобных типовых задач в крупных коммерческих и административных зданиях. В плане экономической конкурентоспособности он тоже выглядит вполне достойно. Когда на объекте требуется более одного астрономического реле, реле времени и т. п., то их стоимость может быть вполне ощутимой. Кроме этого, чаще всего эти элементы управления разнесены по разным частям зданий и добиться их синхронной работы - не такая простая задача, как это может показаться.

Вариант с применением системы мониторинга может решать еще более интересные задачи. Кроме управления, он дает возможность контролировать работу оборудования и оповещать персонал при выявлении неисправностей. В качестве примера можно привести синхронное включение рекламных вывесок на фасаде торгового центра. А в случае неисправности какой-либо вывески оперативный персонал получит сообщение.

Конечно, все вышесказанное актуально при наличии какой-никакой IT инфраструктуры. Но, как правило, она имеется.

На этом все. Спасибо за внимание!

Подробнее..

Как я мониторил РИП-12 от Bolid

28.04.2021 02:24:00 | Автор: admin

Резервированные источники питания используются повсеместно. Они обеспечивают бесперебойным электропитанием приборы охранной и пожарной сигнализации, оборудование систем контроля доступа и другие системы. На нашем предприятии в качестве таких источников, как правило, используются приборы от ЗАО НВП Болид. У некоторых из них, как, например, у РИП-12-6/80M3-R-RS, имеется интерфейс RS485, что позволяет включать их в систему мониторинга.

Средства мониторинга

Мы используем Zabbix 5.2. Получать данные от РИП будем по протоколу Modbus RTU over TCP. Поддержка этого протокола реализована в Zabbix с помощью загружаемого модуля libzbxmodbus. Также в процессе мониторинга принимают участие преобразователь протокола C2000-ПП (вер. 1,32) в режиме Master и преобразователь последовательных интерфейсов (RS485 в Ethernet).

Объекты мониторинга

Для начала определимся, что конкретно мы сможем контролировать. Из документации к РИП-12-6/80M3-R-RS и С2000-ПП выяснилось, что рассчитывать мы можем на получение состояния семи зон (ШС) и числовых значений тока и напряжения. В ходе экспериментов мне удалось воспроизвести следующие состояния ШС:

ШС 0 Состояние прибора

149

Взлом корпуса прибора

Корпус РИП открыт

152

Восстановление корпуса прибора

Корпус РИП закрыт

250

Потеряна связь с прибором

Потеряна связь с прибором

ШС 1 Выходное напряжение

193

Подключение выходного напряжения

РИП подключил выходное напряжение при появлении напряжения в сети

192

Отключение выходного напряжения

РИП отключил выходное напряжение при отсутствии напряжения в сети и разряде батареи

199

Восстановление питания

Напряжение питания прибора пришло в норму

250

Потеряна связь с прибором

Потеряна связь с прибором

ШС 2 Ток нагрузки

194

Перегрузка источника питания

Выходной ток РИП более 7,5 А

195

Перегрузка источника питания устранена

Выходной ток РИП менее 7,5 А

250

Потеряна связь с прибором

Потеряна связь с прибором

ШС 3 и ШС 4 Напряжение на батарее 1 и 2 соответственно

200

Восстановление батареи

Напряжение батареи выше 10 В, заряд батареи возможен

202

Неисправность батареи

Напряжение на батарее ниже 7 В или не подключена

211

Батарея разряжена

Напряжение на батарее ниже 11 В при отсутствии сетевого напряжения

250

Потеряна связь с прибором

Потеряна связь с прибором

ШС 5 Степень заряда батарей

196

Неисправность зарядного устройства

ЗУ не обеспечивает напряжение и ток для заряда батареи в заданных пределах

197

Восстановление зарядного устройства

ЗУ обеспечивает напряжение и ток для заряда батареи в заданных пределах

250

Потеряна связь с прибором

Потеряна связь с прибором

ШС 6 Напряжение сети

1

Восстановление сети 220

Сетевое напряжение питания < 150 В или > 250 В

2

Авария сети 220 В

Сетевое напряжение питания в пределах 150250 В

250

Потеряна связь с прибором

Потеряна связь с прибором

Крайне вероятно, что мной была получена только часть из всех возможных состояний. Например, имеются догадки, что ШС 3 и 4 должны также иметь состояние [204] Необходимо обслуживание, а ШС 0 - состояние [203] Сброс прибора и другие. К сожалению, чтение документации ситуацию не прояснило. В связи с этим нам необходимо следить и реагировать на появление событий, которые мы не предусмотрели.

Конфигурирование устройств

Не будем долго останавливаться на процессе конфигурирования приборов, только коротко рассмотрим перечень необходимых действий. Настройка устройств Болид осуществляется при помощи утилиты UProg и имеет следующий порядок:

  1. Назначение сетевых адресов всем устройствам (РИП и С2000-ПП),

  2. Конфигурирование интерфейса интеграции С2000-ПП (Modbus RTU),

  3. Добавление ШС, описанных выше, в таблицу зон С2000-ПП. Крайне важно, чтобы, во-первых, были добавлены все ШС, а во-вторых, ШС должны следовать друг за другом в порядке возрастания.

UProg. Конфигурация С2000-ППUProg. Конфигурация С2000-ПП

При заполнении таблицы зон следует помнить следующее:

  • адрес прибора - сетевой адрес РИП, в нашем случае 126,

  • номер ШС - номер ШС от 0 до 6,

  • тип зоны - тип ШС, для ШС 0 назначаем тип зоны "3 - состояние прибора", для всех остальных - "8-РИП напряжение / ток".

Создаем шаблоны Zabbix

Напомню, что Zabbix с модулем libzbxmodbus выступает в роли Modbus-мастера. Из-за особенностей получения данных от C2000-ПП, о которых речь пойдет в процессе создания шаблонов, мы будем рассматривать два подхода к мониторингу.

  • мониторинг состояния ШС.

  • мониторинг как состояния, так и числовых параметров РИП.

Мониторинг состояния ШС

Итак, создадим шаблон RIP 12 mod 56 RIP 12 6 80 M3 R RS. Шаблон имеет один элемент данных с именем Request и типом "Простая проверка". Ключом элемента является функция: modbus_read[{$MODBUS_PORT},{$MODBUS_SLAVE},{$STATUS_REG},3,7*uint16] . В параметрах функции используются значения макросов, которые позволяют составить корректный modbus запрос к C2000-ПП.

  • {MODBUS_PORT} - тип используемого протокола (enc - Modbus RTU over TCP), адрес и порт преобразователя последовательных интерфейсов.

  • {MODBUS_SLAVE} - Modbus UID С2000-ПП (настраивается в UProg на вкладке прибор).

  • {STATUS_REG} - адрес регистра в котором расположен ШС 0 интересующего нас РИПа. Получить данный адрес можно следующим образом: "Номер зоны в таблице зон С2000-ПП" + 40000 - 1. В нашем примере это: 450+40000-1 = 40449.

Основная задача элемента Request - запросить у С2000-ПП значение всех семи ШС контролируемого РИП и предоставить их в формате JSON. Результирующий JSON содержит объекты, ключами которых являются адресы регистров С2000-ПП, а значениями - содержимое этих регистров:

{  "40449":39115,  "40450":51195,  "40451":50171,  "40452":51963,  "40453":51451,  "40454":50683,  "40455":763}

Зависимые элементы данных

Элемент данных Request имеет 7 зависимых элементов. Основная задача этих элементов - распарсить JSON и получить состояние каждого ШС индивидуально. Вот эти элементы:

  • Status - состояние прибора (ШС 0),

  • Uout - выходное напряжение (ШС 1),

  • Iout - ток нагрузки (ШС 2),

  • Ubat1 - напряжение АКБ1 (ШС 3),

  • Ubat2 - напряжение АКБ2 (ШС 4),

  • Capacity - степень заряда АКБ (ШС 5),

  • Uin - напряжение сети (ШС 6).

Предобработка зависимых элементов данных

Чтобы получить состояние ШС 0 (Status), нам достаточно два шага предобработки. На первом шаге мы воспользуемся стандартным функционалом JSONPath, а затем разделим полученное значение на 256, тем самым получим код состояния.

К сожалению, мне не удалось использовать математические операции в параметрах JSONPath. Поэтому для оставшихся элементов данных пришлось использовать javascritpt-предобработку. Например, для элемента данных Iout (ШС 2) javascript-предобработка выглядит так:

function (value){    var reg = parseInt({$STATUS_REG})+2;    var data = JSON.parse(value);    return data[reg];}

Триггеры

После добавления триггеров создание шаблона можно считать завершенным. Перечень созданных триггеров:

  1. Взлом корпуса прибора,

  2. Перегрузка источника питания,

  3. Отключение выходного напряжения,

  4. Неисправность батареи АКБ1,

  5. Неисправность батареи АКБ2,

  6. АКБ1 разряжен,

  7. АКБ2 разряжен,

  8. Авария сети 220 В,

  9. Потеряна связь с прибором,

  10. Неизвестное состояние Status,

  11. Неизвестное состояние Iout,

  12. Неизвестное состояние Uout,

  13. Неизвестное состояние АКБ1,

  14. Неизвестное состояние АКБ2,

  15. Неизвестное состояние Capacity,

  16. Неизвестное состояние Uin,

  17. Превышено время отсутствия по MODBUS.

Демонстрация и импорт RIP 12 mod 56 RIP 12 6 80 M3 R RS

Шаблон RIP 12 mod 56 RIP 12 6 80 M3 R RS в картинкахШаблон RIP 12 mod 56 RIP 12 6 80 M3 R RS в картинкахПример создания узла сетиПример создания узла сети

Ссылки для импорта: Шаблон RIP 12 mod 56 RIP 12 6 80 M3 R RS, Преобразование значений RIP 12 mod 56 RIP 12 6 80 M3 R RS.

Мониторинг состояния и числовых параметров

Мониторинг числовых параметров имеет свои особенности. Все дело в том, что для получения числового значения нам необходимо сделать два modbus-запроса к С2000-ПП. Первый запрос устанавливает зону для запроса тока или напряжения, второй - непосредственное получение значения. В таком случае мы не имеем возможности использовать функционал libzbxmodbus, т.к. попросту не cможем гарантировать правильную очередность запросов.

Первое и самое простое, что приходит на ум, - это создать скрипт, который сделает получение числового параметра атомарной операцией и воспользоваться возможностями внешних проверок Zabbix. Но такой скрипт тоже не позволит использовать загружаемый модуль libzbxmodbus по причине невозможности организации монопольного доступа к разделяемому ресурсу, в нашем случае к преобразователю последовательных интерфейсов.

В связи с вышесказанным, для этих целей было решено отказаться от использования libzbxmodbus и написать скрипт, который сможет предоставлять и числовые параметры РИП и состояния его ШС.

Пишем shell скрипт для внешней проверки

Для того, чтобы синхронизировать доступ к преобразователю последовательных интерфейсов, воспользуемся утилитой flock. Работу с Modbus будем осуществлять при помощи modpoll. В /usr/lib/zabbix/externalscripts создадим скрипт rip_12_mod_56.sh

#!/bin/bash# rip_12_mod_56.sh# $1 - protocol://host:port# $2 - Modbus UID# $3 - Status register# $4 - Offset (0 - 6)# Example of requesting statuses:       ./rip_12_mod_56.sh enc://127.0.0.1:4001 1 40000# Example value request:                ./rip_12_mod_56.sh enc://127.0.0.1:4001 1 40000 3(($# < 3)) && { printf '%s\n' "You have given little data. Command exited with non-zero"; exit 1; }lockfile=$(echo "$1" | awk -F "://" '{print $2}')setzone(){        modpoll -m $1 -a $4 -r 46181 -0 -1 -c 1 -p $3 $2 $5> /dev/null 2>&1    (($? != 0)) && { printf '%s\n' "Command exited with non-zero"; exit 1; }    sleep 0.15}getvalue (){        value=$(modpoll -m $1 -a $4 -r 46328 -0 -1 -c 1 -t 4:hex -p $3 $2 |grep ]: |awk '{print $2}')        printf "%d" $value}getstatus (){        status=$(modpoll -m $1 -a $4 -r $5 -1 -c 7 -t 4:hex -p $3 $2 | grep ]: | awk -F "0x" 'BEGIN { printf"["} NR!=7{printf "\""$2"\","} NR==7 {printf "\""$2"\""} END { printf "]"}')    echo "{ \"status\": $status }"}(        flock -e 200        protocol=$(echo $1 | awk -F "://" '{print $1}');        host=$(echo $1 | awk -F "://" '{print $2}' | awk -F ":" '{print $1}')        port=$(echo $1 | awk -F "://" '{print $2}' | awk -F ":" '{print $2}')        register=$(($3+1))        if (($# >= 4)); then                zone=$(($register+$4-40000))                setzone $protocol $host $port $2 $zone                echo $(getvalue $protocol $host $port $2)                sleep 0.15                exit 0        fi        echo $(getstatus $protocol $host $port $2 $register)        sleep 0.15;) 200> /tmp/$lockfile

Подробности настройки внешних проверок в Zabbix уточняйте в документации.

Создаем RIP 12 mod 56 RIP 12 6 80 M3 R RS EXTENDED

Для получения информации о состоянии ШС шаблон содержит элемент данных Request с типом "Внешняя проверка". Ключом элемента является скрипт: rip_12_mod_56.sh[{$MODBUS_PORT}, {$MODBUS_SLAVE}, {$STATUS_REG}]. Как и в шаблоне RIP 12 mod 56 RIP 12 6 80 M3 R RS, задача элемента Request - сформировать JSON с состояниями всех ШС.

Возвращаемый JSON оптимизирован для использования функционала JSONPath. Для упрощения скрипта значения возвращаются в шестнадцатеричной форме:

{  "status": ["98CB","C7FB","C3FB","CAFB","C8FB","C5FB","02FB"]}

Состояния ШС. Снова зависимые элементы данных.

Как и в предыдущем шаблоне, элемент данных Request имеет 7 зависимых элементов. Задача этих элементов тоже неизменна - распарсить JSON и получить состояние каждого ШС.

Получаем числовые значения

Для получения числовых значений создадим 5 элементов данных с типом "Внешняя проверка".

  • Uout_value - значение выходного напряжения, В.

  • Iout_value - значение выходного тока, А.

  • Ubat1_value - значение напряжения на батарее 1, В.

  • Ubat2_value - значение напряжения на батарее 2, В.

  • Uin_value -значение напряжения сети, В.

Ключом этих элементов является скрипт: rip_12_mod_56.sh[{$MODBUS_PORT}, {$MODBUS_SLAVE}, {$STATUS_REG}, <НОМЕР ШС>].

Триггеры

Перечень триггеров не отличается от триггеров, созданных в шаблоне RIP 12 mod 56 RIP 12 6 80 M3 R RS.

Демонстрация и импорт RIP 12 mod 56 RIP 12 6 80 M3 R RS EXTENDED

Шаблон RIP 12 mod 56 RIP 12 6 80 M3 R RS EXTENDED в картинкахШаблон RIP 12 mod 56 RIP 12 6 80 M3 R RS EXTENDED в картинкахПоследние значения RIP 12 mod 56 RIP 12 6 80 M3 R RS EXTENDEDПоследние значения RIP 12 mod 56 RIP 12 6 80 M3 R RS EXTENDED

Ссылки для импорта: Шаблон RIP 12 mod 56 RIP 12 6 80 M3 R RS EXTENDED, rip_12_mod_56.sh.

Вместо заключения

В своем мониторинге мы используем шаблон RIP 12 mod 56 RIP 12 6 80 M3 R RS. По-большому счету причина такого решения одна - расширяемость системы. Использование загружаемого модуля позволяет включать в одну линию приборы разных типов и модификаций, организовать их мониторинг стандартными средствами. Кроме этого, большой потребности в получении числовых значений у нас пока не возникало.

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

Спасибо за внимание!

Подробнее..

Добавляем modbus в Embox RTOS и используем на STM32 и не только

17.03.2021 18:23:31 | Автор: admin
image
Нас часто спрашивают, чем Embox отличается от других ОС для микроконтроллеров, например, FreeRTOS? Сравнивать проекты между собой, конечно, правильно. Но параметры, по которым порой предлагают сравнение, лично меня повергают в легкое недоумение. Например, сколько нужно памяти для работы Embox? А какое время переключения между задачами? А в Embox поддерживается modbus? В данной статье на примере вопроса про modbus мы хотим показать, что отличием Embox является другой подход к процессу разработки.

Давайте разработаем устройство, в составе которого будет работать в том числе modbus server. Наше устройство будет простым. Ведь оно предназначено только для демонстрации modbus, Данное устройство будет позволять управлять светодиодами по протоколу Modbus. Для связи с устройством будем использовать ethernet соединение.

Modbus открытый коммуникационный протокол. Широко применяется в промышленности для организации связи между электронными устройствами. Может использоваться для передачи данных через последовательные линии связи RS-485, RS-422, RS-232 и сети TCP/IP (Modbus TCP).

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

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

Наш проект будем вести в отдельном репозитории. При желании все можно скачать и воспроизвести самостоятельно.

Разработка прототипа на Linux


Начнем с разработки прототипа на хосте. Для того чтобы можно было использовать libmodbus в качестве библиотеки его нужно скачать, сконфигурировать и собрать.
Для этих целей я набросал Makefile

libmodbus-$(LIBMODBUS_VER).tar.gz:    wget http://libmodbus.org/releases/libmodbus-$(LIBMODBUS_VER).tar.gz$(BUILD_BASE)/libmodbus/lib/pkgconfig/libmodbus.pc : libmodbus-$(LIBMODBUS_VER).tar.gz    tar -xf libmodbus-$(LIBMODBUS_VER).tar.gz    cd libmodbus-$(LIBMODBUS_VER); \    ./configure --prefix=$(BUILD_BASE)/libmodbus --enable-static --disable-shared; \    make install; cd ..;


Собственно из параметров конфигурации мы используем только prefix чтобы собрать библиотеку локально. А поскольку мы хотим использовать библиотеку не только на хосте, соберем ее статическую версию.

Теперь нам нужен modbus сервер. В проекте libmodbus есть примеры, давайте на основе какого-нибудь простого сервера сделаем свою реализацию.

    ctx = modbus_new_tcp(ip, port);    header_len = modbus_get_header_length(ctx);    query = malloc(MODBUS_TCP_MAX_ADU_LENGTH);    modbus_set_debug(ctx, TRUE);    mb_mapping = mb_mapping_wrapper_new();    if (mb_mapping == NULL) {        fprintf(stderr, "Failed to allocate the mapping: %s\n",                modbus_strerror(errno));        modbus_free(ctx);        return -1;    }    listen_socket = modbus_tcp_listen(ctx, 1);    for (;;) {        client_socket = modbus_tcp_accept(ctx, &listen_socket);        if (-1 == client_socket) {            break;        }        for (;;) {            int query_len;            query_len = modbus_receive(ctx, query);            if (-1 == query_len) {                /* Connection closed by the client or error */                break;            }            if (query[header_len - 1] != MODBUS_TCP_SLAVE) {                continue;            }            mb_mapping_getstates(mb_mapping);            if (-1 == modbus_reply(ctx, query, query_len, mb_mapping)) {                break;            }            leddrv_updatestates(mb_mapping->tab_bits);        }        close(client_socket);    }    printf("exiting: %s\n", modbus_strerror(errno));    close(listen_socket);    mb_mapping_wrapper_free(mb_mapping);    free(query);    modbus_free(ctx);


Здесь все стандартно. Пара мест, которые представляют интерес, это функции mb_mapping_getstates и leddrv_updatestates. Это как раз функционал, который и реализует наше устройство.

static modbus_mapping_t *mb_mapping_wrapper_new(void) {    modbus_mapping_t *mb_mapping;    mb_mapping = modbus_mapping_new(LEDDRV_LED_N, 0, 0, 0);    return mb_mapping;}static void mb_mapping_wrapper_free(modbus_mapping_t *mb_mapping) {    modbus_mapping_free(mb_mapping);}static void mb_mapping_getstates(modbus_mapping_t *mb_mapping) {    int i;    leddrv_getstates(mb_mapping->tab_bits);    for (i = 0; i < mb_mapping->nb_bits; i++) {        mb_mapping->tab_bits[i] = mb_mapping->tab_bits[i] ? ON : OFF;    }}


Таким образом, нам нужны leddrv_updatestates, которая задает состояние светодиодов, и leddrv_getstates, которая получает состояние светодиодов.
static unsigned char leddrv_leds_state[LEDDRV_LED_N];int leddrv_init(void) {    static int inited = 0;    if (inited) {        return 0;    }    inited = 1;    leddrv_ll_init();    leddrv_load_state(leddrv_leds_state);    leddrv_ll_update(leddrv_leds_state);    return 0;}...int leddrv_getstates(unsigned char leds_state[LEDDRV_LED_N]) {    memcpy(leds_state, leddrv_leds_state, sizeof(leddrv_leds_state));    return 0;}int leddrv_updatestates(unsigned char new_leds_state[LEDDRV_LED_N]) {    memcpy(leddrv_leds_state, new_leds_state, sizeof(leddrv_leds_state));    leddrv_ll_update(leddrv_leds_state);    return 0;}


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

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

void leddrv_ll_update(unsigned char leds_state[LEDDRV_LED_N]) {    int i;    int idx;    char buff[LEDDRV_LED_N * 2];        for (i = 0; i < LEDDRV_LED_N; i++) {        char state = !!leds_state[i];        fprintf(stderr, "led(%03d)=%d\n", i, state);        buff[i * 2] = state + '0';        buff[i * 2 + 1] = ',';    }    idx = open(LED_FILE_NAME, O_RDWR);    if (idx < 0) {        return;    }    write(idx, buff, (LEDDRV_LED_N * 2) - 1);    close(idx);}...void leddrv_load_state(unsigned char leds_state[LEDDRV_LED_N]) {    int i;    int idx;    char buff[LEDDRV_LED_N * 2];    idx = open(LED_FILE_NAME, O_RDWR);    if (idx < 0) {        return;    }    read(idx, buff, (LEDDRV_LED_N * 2));    close(idx);        for (i = 0; i < LEDDRV_LED_N; i++) {        leds_state[i] = buff[i * 2] - '0';    }}


Нам нужно указать файл где будет сохранено начальное состояние светодиодов. Формат файла простой. Через запятую перечисляются состояние светодиодов, 1 светодиод включен, а 0 -выключен. В нашем устройстве 80 светодиодов, точнее 40 пар светодиодов. Давайте предположим, что по умолчанию четные светодиоды будут выключены а нечетные включены. Содержимое файла

0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1


Запускаем сервер
./led-serverled(000)=0led(001)=1...led(078)=0led(079)=1


Теперь нам нужен клиент для управления нашим устройством. Его тоже очень просто разрабатываем на базе примера из libmodbus

ctx = modbus_new_tcp(ip, port);    if (ctx == NULL) {        fprintf(stderr, "Unable to allocate libmodbus context\n");        return -1;    }    modbus_set_debug(ctx, TRUE);    modbus_set_error_recovery(ctx,            MODBUS_ERROR_RECOVERY_LINK |            MODBUS_ERROR_RECOVERY_PROTOCOL);    if (modbus_connect(ctx) == -1) {        fprintf(stderr, "Connection failed: %s\n",                modbus_strerror(errno));        modbus_free(ctx);        return -1;    }    if (1 == modbus_write_bit(ctx, bit_n, bit_value)) {        printf("OK\n");    } else {        printf("FAILED\n");    }    /* Close the connection */    modbus_close(ctx);    modbus_free(ctx);


Запускаем клиент. Установим 78 светодиод, который по умолчанию выключен

./led-client set 78Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4E][FF][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><FF><00>OK


На сервере увидим
...led(076)=0led(077)=1led(078)=1led(079)=1Waiting for an indication...ERROR Connection reset by peer: read


То есть светодиод установлен. Давайте выключим его.
./led-client clr 78Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4E][00][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><00><00>OK


На сервере увидим сообщение об изменении
...led(076)=0led(077)=1led(078)=0led(079)=1Waiting for an indication...ERROR Connection reset by peer: read


Запустим http сервер. О разработке веб-сайтов мы рассказывали в статье. К тому же веб-сайт нам нужен только для более удобной демонстрации работы modbus. Поэтому не буду сильно вдаваться в подробности. Сразу приведу cgi скрипт

#!/bin/bashecho -ne "HTTP/1.1 200 OK\r\n"echo -ne "Content-Type: application/json\r\n"echo -ne "Connection: close\r\n"echo -ne "\r\n"if [ $REQUEST_METHOD = "GET" ]; then    echo "Query: $QUERY_STRING" >&2    case "$QUERY_STRING" in        "c=led_driver&a1=serialize_states")            echo [ $(cat ../emulate/conf/leds.txt) ]            ;;        "c=led_driver&a1=serialize_errors")            echo [ $(printf "0, %.0s" {1..79}) 1 ]            ;;        "c=led_names&a1=serialize")            echo '[ "one", "two", "WWWWWWWWWWWWWWWW", "W W W W W W W W " ]'            ;;    esacelif [ $REQUEST_METHOD = "POST" ]; then    read -n $CONTENT_LENGTH POST_DATA    echo "Posted: $POST_DATA" >&2fi


И напомню что запустить можно с помощью любого http сервера с поддержкой CGI. Мы используем встроенный в python сервер. Запускаем следующей командой
python3 -m http.server --cgi -d .


Откроем наш сайт в браузере


Установим 78 светодиод с помощью клиента
./led-client -a 127.0.0.1 set 78Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4E][FF][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><FF><00>OK


сбросим 79 светодиод
./led-client -a 127.0.0.1 clr 79Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4F][00][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4F><00><00>OK


На сайте увидим разницу


Собственно все, на Linux наша библиотека прекрасно работает.

Адаптация к Embox и запуск на эмуляторе


Библиотека libmodbus


Теперь нам нужно перенести код в Embox. начнем с самого проекта libmodbus.
Все просто. Нам нужно описание модуля (Mybuild)
package third_party.lib@Build(script="$(EXTERNAL_MAKE)")@BuildArtifactPath(cppflags="-I$(ROOT_DIR)/build/extbld/third_party/lib/libmodbus/install/include/modbus")module libmodbus {    @AddPrefix("^BUILD/extbld/^MOD_PATH/install/lib")    source "libmodbus.a"    @NoRuntime depends embox.compat.posix.util.nanosleep}


Мы с помощью аннотации Build(script="$(EXTERNAL_MAKE)") указываем что используем Makefile для работы с внешними проектами.

С помощью аннотации BuildArtifactPath добавляем пути для поиска заголовочных файлов к тем модулям, которые будут зависеть от этой библиотеки.

И говорим что нам нужна библиотека source libmodbus.a

PKG_NAME := libmodbusPKG_VER  := 3.1.6PKG_SOURCES := http://libmodbus.org/releases/$(PKG_NAME)-$(PKG_VER).tar.gzPKG_MD5     := 15c84c1f7fb49502b3efaaa668cfd25ePKG_PATCHES := accept4_disable.patchinclude $(EXTBLD_LIB)libmodbus_cflags = -UHAVE_ACCEPT4$(CONFIGURE) :    export EMBOX_GCC_LINK=full; \    cd $(PKG_SOURCE_DIR) && ( \        CC=$(EMBOX_GCC) ./configure --host=$(AUTOCONF_TARGET_TRIPLET) \        prefix=$(PKG_INSTALL_DIR) \        CFLAGS=$(libmodbus_cflags) \    )    touch $@$(BUILD) :    cd $(PKG_SOURCE_DIR) && ( \        $(MAKE) install MAKEFLAGS='$(EMBOX_IMPORTED_MAKEFLAGS)'; \    )    touch $@


Makefile для сборки тоже простой и очевидный. Единственное, отмечу что используем внутренний компилятор ($(EMBOX_GCC) ) Embox и в качестве платформы (--host) передаем ту, которая задана в Embox ($(AUTOCONF_TARGET_TRIPLET)).

Подключаем проект к Embox


Напомню, что для удобства разработки мы создали отдельный репозиторий. Для того чтобы подключить его к Embox достаточно указать Embox где лежит внешний проект.
Делается это с помощью команды
make ext_conf EXT_PROJECT_PATH=<path to project> 

в корне Embox. Например,
 make ext_conf EXT_PROJECT_PATH=~/git/embox_project_modbus_iocontrol


modbus-server


Исходный код modbus сервера не требует изменений. То есть мы используем тот же код, который разработали на хосте. Нам нужно добавить Mybuild
package iocontrol.modbus.cmd@AutoCmd@Build(script="true")@BuildDepends(third_party.lib.libmodbus)@Cmd(name="modbus_server")module modbus_server {    source "modbus_server.c"    @NoRuntime depends third_party.lib.libmodbus}


В котором с помощью аннотаций мы укажем что это у нас команда, а также что она зависит от библиотеки libmodbus.

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

Нам также нужно собрать нашу систему вместе с modbus сервером
Добавляем наши модули в mods.conf
    include iocontrol.modbus.http_admin    include iocontrol.modbus.cmd.flash_settings    include iocontrol.modbus.cmd.led_names    include third_party.lib.libmodbus    include iocontrol.modbus.cmd.modbus_server    include iocontrol.modbus.cmd.led_driver    include embox.service.cgi_cmd_wrapper(cmds_check=true, allowed_cmds="led_driver led_names flash_settings")    include iocontrol.modbus.lib.libleddrv_ll_stub


А наш файл leds.txt со статусами светодиодов кладем в корневую файловую систему. Но так как нам нужен изменяемый файл, давайте добавим RAM disk и скопируем наш файл на этот диск. Содержимое system_start.inc
"export PWD=/","export HOME=/","netmanager","service telnetd","service httpd http_admin","ntpdate 0.europe.pool.ntp.org","mkdir -v /conf","mount -t ramfs /dev/static_ramdisk /conf","cp leds.txt /conf/leds.txt","led_driver init","service modbus_server","tish",


Этого достаточно запустим Embox на qemu
./scripts/qemu/auto_qemu

modbus и httpd сервера запускаются автоматически при старте. Установим такие же значения с помощью modbus клиента, только указав адрес нашего QEMU (10.0.2.16)
./led-client -a 10.0.2.16 set 78Connecting to 10.0.2.16:1502[00][01][00][00][00][06][FF][05][00][4E][FF][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><FF><00>OK


и соответственно
./led-client -a 10.0.2.16 clr 79Connecting to 10.0.2.16:1502[00][01][00][00][00][06][FF][05][00][4F][00][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4F><00><00>


Откроем браузер


Как и ожидалось все тоже самое. Мы можем управлять устройством через modbus протокол уже на Embox.

Запуск на микроконтроллере


Для запуска на микроконтроллере будем использовать STM32F4-discovery. На вышеприведенных скриншотах страниц браузера, видно что используется 80 ног вывода, объединенные в пары, и еще можно заметить что у этих пар есть другие свойства, например можно задать имя, или пара может подсвечиваться. На самом деле, код был взят из реального проекта и из него для упрощения были убраны лишние части. 80 выходных пинов было получено с помощью дополнительных микросхем сдвиговых регистров.

Но на плате STM32F4-discovery всего 4 светодиода. Было бы удобно задавать количество светодиодов, чтобы не модифицировать исходный код В Embox есть механизм позволяющий параметризировать модули. Нужно в описании модуля (Mybuild) добавить опцию
package iocontrol.modbus.libstatic module libleddrv {    option number leds_quantity = 80...}


И можно будет использовать в коде
#ifdef __EMBOX__#include <framework/mod/options.h>#include <module/iocontrol/modbus/lib/libleddrv.h>#define LEDDRV_LED_N OPTION_MODULE_GET(iocontrol__modbus__lib__libleddrv,NUMBER,leds_quantity)#else#define LEDDRV_LED_N 80#endif


При этом менять этот параметр можно будет указав его в файле mods.conf
    include  iocontrol.modbus.lib.libleddrv(leds_quantity=4)


если параметр не указывается, то используется тот который задан в модуле по умолчанию, то есть 80.

Нам нужно еще управлять реальными линиями вывода. Код следующий
struct leddrv_pin_desc {    int gpio; /**< port */    int pin; /**< pin mask */};static const struct leddrv_pin_desc leds[] = {    #include <leds_config.inc>};void leddrv_ll_init(void) {    int i;    for (i = 0; i < LEDDRV_LED_N; i++) {        gpio_setup_mode(leds[i].gpio, leds[i].pin, GPIO_MODE_OUTPUT);    }}void leddrv_ll_update(unsigned char leds_state[LEDDRV_LED_N]) {    int i;    for (i = 0; i < LEDDRV_LED_N; i++) {        gpio_set(leds[i].gpio, leds[i].pin,                leds_state[i] ? GPIO_PIN_HIGH : GPIO_PIN_LOW);    }}


В файле mods.conf нам нужна конфигурация для нашей платы. К ней добавляем наши модули
    include iocontrol.modbus.http_admin    include iocontrol.modbus.cmd.flash_settings    include iocontrol.modbus.cmd.led_names    include third_party.lib.libmodbus    include iocontrol.modbus.cmd.modbus_server    include iocontrol.modbus.cmd.led_driver    include embox.service.cgi_cmd_wrapper(cmds_check=true, allowed_cmds="led_driver led_names flash_settings")    include iocontrol.modbus.lib.libleddrv(leds_quantity=4)    include iocontrol.modbus.lib.libleddrv_ll_stm32_f4_demo


По сути дела, те же модули как и для ARM QEMU, за исключением конечно драйвера.

Собираем, прошиваем, запускаем. И с помощью того же modbus клиента управляем светодиодами. Нужно только поставить правильный адрес, и не забыть что у нас всего 4 светодиода на плате.

Работу на плате stm32f4-discovery можно увидеть на этом коротком видео


Выводы


На этом простом примере мы постарались показать, в чем же основное отличие Embox от других ОС для микроконтроллеров. В том числе которые имеют POSIX совместимость. Ведь мы по сути дела, взяли готовый модуль, разработали бизнес логику на Linux используя при этом несколько приложений. И запустили все это на нашей целевой платформе. Тем самым существенно упростив и ускорив саму разработку.

Да, конечно, приложение демонстрационное и не сложное. Сам протокол modbus так же можно было бы реализовать самостоятельно. Но в этом случае нам нужно было бы разбираться в протоколе modbus. А наш подход позволяет сосредоточится каждому специалисту на своей части. Ну и конечно, большинство проблем решаются на хосте, что гораздо более удобно чем разрабатывать напрямую на плате.
Подробнее..

Быстрый прототип IIoT-решения на Raspberry PI и Yandex IoT

17.12.2020 10:10:32 | Автор: admin

В этой серии статей я расскажу как самостоятельно собрать полнофункциональный прототип промышленного IIoT-шлюза на базе Raspberry PI.

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

Однако в качестве быстрого и дешевого решения на этапе проверки гипотез (в момент когда вам только предстоит определиться какие данные каким способом снимать и как их потом хранить и использовать) такое решение вполне имеет право на существование.

В конце концов, с программной точки зрения, большинство современных промышленных IoT-шлюзов - не что иное, как обычные одноплатные ПК со специфической ОС (чаще всего на базе Linux) и набором предустановленного ПО.

В общем те, кто готов к подобным экспериментам на производстве, либо просто интересуется IIoT и хочет поэкспериментировать с технологиями для собственного развития - вэлкам под кат!


Постановка задачи

Для начала давайте определим, какие функции мы хотим получить на выходе.

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

То есть в первую очередь - сбор телеметрии с различных датчиков, отправка её на сервер (например в облако) для централизованной обработки и накопление истории. Дальнейшая судьба собранной статистики зависит от конкретной решаемой задачи и вашей фантазии.

Также неплохо было бы иметь возможность мониторить поступающие данные в реальном времени (хотя бы чтобы понять идут они или нет) и накапливать локальный архив timeseries на шлюзе (например, если нет соединения с интернетом).

В первой части статьи мы реализуем основные функции:

  • сбор телеметрии с промышленных датчиков по протоколу Modbus;

  • передачу данных в облако;

  • локальный мониторинг в реальном времени;

Во второй - разберемся с тем, как можно накапливать и обрабатывать телеметрию в облаке;

А в третьей - доработаем проект дополнительными фичами:

  • локальным хранилищем телеметрии;

  • устройством для прямого считывания аналогового или цифрового сигнала;

Общая архитектура

Наш небольшой прототип будет иметь все обязательные компоненты IIoT-решения:

  1. Устройство считывания показаний датчиков (можно использовать промышленный контроллер, умные датчики, или собрать свой вариант на базе любого arduino-совместимого контроллера);

  2. IIoT-шлюз, в качестве которого будет выступать Raspberry PI;

  3. Облачный сервис, который принимает данные по протоколу MQTT, сохраняет их в Managed DB и производит дальнейшую обработку - эту часть развернем на платформе Yandex.Cloud

Основные компоненты решенияОсновные компоненты решения

Настраиваем шлюз

Начнем с центрального узла нашего небольшого проекта, то есть малинки.

В качестве ядра системы на стороне шлюза удобно использовать Node-RED. Это простой и удобный инструмент Low-code разработки, который позволяет сократить время создания IoT (и не только) решений. Если по какой-то неведомой причине вы им ещё не пользуетесь - обязательно почитайте про него тут и тут!

Одно из главных преимуществ Node-RED - наличие огромного количества расширений. В том числе, специальных кубиков для работы с modbus, serial и всевозможными базами данных. Там же есть и конструктор легких дашбордов для real-time мониторинга (всё это нам понадобится).

1) Устанавливаем и настраиваем Node-RED:

Вообще Node-RED есть в официальном репозитории Raspberry и его можно поставить просто через apt-get. Но разработчики рекомендуют использовать специально подготовленный ими скрипт, который сразу ставит ещё и npm и настраивает node-RED для запуска в качестве сервиса.

Заходим на малинку и запускаем скрипт:

$ bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)

Дожидаемся завершения установки (она может занять несколько минут). Когда установка завершена, можно сразу настроить автозапуск при старте ОС:

$ sudo systemctl enable nodered.service

Если хотите сразу озаботиться некоторой безопасностью, можно настроить вход в web-интерфейс по паролю вот по этой инструкции.

Spoiler

Файл settings.js скорее всего будет находиться в папке: /home/pi/.node-red

Теперь всё готово к запуску Nede-RED:

$ node-red-start

Если всё установилось и запустилось успешно, web-интерфейс Node-RED будет доступен в локальной сети по адресу: [IP малинки]:1880

Но настройка на этом ещё не закончена - по умолчанию у нас есть только базовые предустановленные компоненты, а для задуманного понадобится несколько дополнительных, которые надо еще поставить.

Заходим в web-интерфейс, идем в настройки палитры: [Меню в правом верхнем углу] ->[Pallet manager]:

Меню настроек палитрыМеню настроек палитры

Переходим во вкладку Install, находим и устанавливаем следующие пакеты:

node-red-contrib-modbus - пакет для работы по протоколу Modbus

node-red-dashboard - дашборды для мониторинга в реальном времени

postgrestor - простой компонент для выполнения запросов к PostgreSQL

node-red-node-serialport - компонент для работы с serial (этот компонент может быть уже установлен вместе с базовыми)

Вот теперь Node-RED настроен, можно приступать к разработке!

2) Реализуем считывание данных по Modbus:

Modbus - открытый протокол, разработанный ещё в 1979 году для использования в контроллерах MODICON (бренд, между прочим, жив до сих пор и ныне принадлежит Schneider Electric).

Сейчас modbus является де-факто стандартом для промышленных сетей и поддерживается подавляющим большинством контроллеров и умных датчиков. Для интересующихся, вот тут есть хорошая обзорная статья по теме.

Я же не буду подробно останавливаться на его описании. Упомяну только что протокол имеет 3 основные модификации:

  • две для использования с сетевыми интерфейсами RS-232/422/485 (Modbus ASCII и Modbus RTU)

  • и одну для обмена по TCP/IP (Modbus TCP)

Это важно, так как с одной стороны влияет на то, как Raspberry будет физически подключаться к устройствам (в первом случае понадобится переходник COM/RS-USB), а с другой - от этого зависят настройки считывания данных.

И так, подключаем девайс в соответствующее гнездо малины, создаем поток, добавляем в него кубик modbus-read и заходим в его настройки:

Для начала надо создать подключение к Modbus-серверу. Нажимаем Создать подключение и вводим параметры устройства, с которого хотим получать телеметрию:

Для варианта с RS (Serial) необходимо указать адрес порта, к которому подключено устройство, тип протокола (RTU или ASCII) и поддерживаемую скорость обмена (baud rate):

Для TCP указываем IP-адрес устройства (стоит убедиться, что для eth0 на малинке настроена статика в той же подсети и устройство успешно пингуется) и номер порта (обычно используется порт 502):

Теперь настраиваем сам кубик. Тут важны 4 параметра:

FC (код функции) - Для считывания цифровых данных - 02, для аналоговых - 04.

Address - адрес первого регистра для считывания. Адреса регистров можно посмотреть в спецификации устройства, обычно смысловые регистры идут в начале, т.е. Начинаются непосредственно с 0.

Quantity - количество регистров для считывания. Этот параметр зависит от количества передаваемых устройством параметров - сколько передается сигналов столько и будет регистров. Эту информацию также можно найти в спецификации или даташите.

Poll Rate - частота опроса. Задает частоту, с которой поток будет получать данные от устройства.

В данном примере настроено получение сигналов восьми аналоговых датчиков, начиная с регистра 0000, раз в 5 секунд:

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

var out_msg = []var values = []var values_formated = []values = msg.payload;var time = (new Date()).toISOString()var n = 0;for(var v in values){    values_formated.push({out:"A"+n.toString(), val:values[v]});    n = n+1;}out_msg.push({payload:{datetime:time, val:values_formated}});return out_msg;

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

Нажимаем кнопку Deploy в правом верхнем углу. Если подключение настроено правильно, под нодой подключения к modbus появится статус active, а во вкладке debug начнут выводиться отформатированные сообщения:

3) Настраиваем мониторинг:

Теперь настроим дашбордик для мониторинга сигналов из web-интерфейса Node-RED. Для этого используем кубик chart.

Функционал chart позволяет отображать несколько сигналов на одном графике, но подаваться они должны по отдельности - каждый в своем сообщении со своим топиком. Поэтому массив, приходящий от устройства по modbus надо разделить на отдельные сигналы. Для этого воспользуемся ещё одной функцией:

var values = [];var values_formated = [];var topic_nm = "";values = msg.payload;var n = 0;for(var v in values) // для каждого сигнала формируем свое сообщение{    topic_nm = "A"+n.toString(); // формируем название сигнала    values_formated.push( // подготовленные сообщения складываем в общий массив:        {topic:topic_nm, // название сигнала - в топик         payload:values[v]}); // значение сигнала - в payload    n = n+1;}return {payload:values_formated};

Функция в Nod-RED может иметь больше одного выхода (количество выходов настраивается в нижней части окна свойств):

Но в данном случае мы можем не знать заранее сколько сигналов (и соответственно выходов) будет. Поэтому положим всё в один массив и вернем его целиком:

...return {payload:values_formated};

А дальше воспользуемся нодой split для разделения сообщений и нодой "change" для их форматирования.

split - разделит массив на отдельные payload-ы, а в change мы положим топик и payload каждого сообщения на его законное место:

На выходе получаем поток отдельных сообщений в правильном формате, готовый для подачи в чарт. В результате получается вот такой flow:

Деплоим его и переходим по адресу: http://[IP малинки]:183/ui

И видим график сигналов в реальном времени:

Вот и всё, сбор и локальное отображение телеметрии настроено!

Настройка брокера MQTT в облаке

Теперь можно приступить к настройке серверной части решения. Для этого воспользуемся облачным сервисом Yandex, называемым Yandex IoT Core.

MQTT - ещё один де факто стандартный протокол для IoT-проектов. Вот тут есть хороший обзор самого протокола, поэтому подробно на нем останавливаться не буду, но немного расскажу как устроен MQTT-брокер от Yandex:

В Yandex IoT Core есть два основных типа объектов - реестры и устройства.

Реестры с одной стороны группируют устройства (в одном реестре может быть одновременно несколько устройств) а с другой - являются как бы второй стороной обмена сообщениями.

Каждый реестр имеет доступ к телеметрии своих устройств и может отправлять им команды. При этом у каждого реестра и каждого устройства есть свой набор топиков. Каждый реестр может читать и отправлять сообщения в свои топики, и в топики любого своего устройства. Аналогично каждое устройство может писать и читать свои топики и топики своего реестра (но не другого устройства, даже если оно "живет" в том же реестре).

Организация топиков Yandex IoT CoreОрганизация топиков Yandex IoT Core

При этом сами топики реестров и устройств никак между собой не связаны - сообщение приходит именно в тот топик, в который отправлено и в других недоступно.

Для дальнейших действий потребуется учетная запись Yandex.Cloud. Если у вас такой еще нет, ее можно достаточно быстро завести. В процессе необходимо ввести данные карты но (в отличии от некоторых других облачных сервисов) деньги с неё списываться не будут пока вы явно не переключитесь на платный аккаунт. После регистрации Yandex предоставляет небольшой грант в 4К рублей, которого на приведенные тут эксперименты хватит с лихвой.

Подключаем и настраиваем сервис:

Для начала генерируем сертификат, который будет использоваться для чтения данных из реестра. Вообще говоря, сервис позволяет идентификацию по логину и паролю как для устройств, так и для реестров, но поскольку предполагается использовать реальные данные с оборудования, лучше озаботиться идентификацией по SSL.

Сгенерировать пару сертификатов можно при помощи утилиты OpenSSL вот такой командой:

$ openssl req -x509 \--newkey rsa:4096 \--keyout key_reg.pem \ # имя сертификата закрытого ключа--out crt_reg.pem \   # имя сертификата открытого ключа--nodes \--days 365 \--subj '/CN=localhost'

Теперь заходим в консоль Yandex.Cloud, выбираем IoT Core и создаем новый реестр:

Открытую часть ключа, сгенерированного на предыдущем шаге (crt_reg.pem), загружаем в настройки реестра. Этот сертификаты будут использоваться для считывания телеметрии из брокера внешними сервисами и отправки команд устройствам:

Нажимаем "Создать" и попадаем в настройки свежесозданного реестра. Теперь надо зарегистрировать в нем малинку в качестве устройства.

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

Заходим в Устройства и создаем новое:

Аналогично с созданием реестра загружаем сертификат из пары, созданной на малинке и нажимаем "Добавить".

На этом настройка брокера завершена, всё готово для приема сообщений.

ID устройств и реестров можно посмотреть на вкладке "Обзор". Они нужны для задания адресов топиков:

Топик устройства: $devices/<ID устройства>/events

Топик реестра: $registries/<ID реестра>/events

Также у каждого устройства и реестра есть перманентный топик. Основное отличие перманентных топиков состоит в том, что в них всегда сохраняется последнее сообщение. То есть при подключении консьюмер получит последнее отправленное в него сообщение, даже если не был в сети в момент его отправки. Адреса перманентных топиков похожи на обычные, но заканчиваются на state а не events:

Перманентный топик устройства: $devices/<ID устройства>/state

Перманентный топик реестра: $registries/<ID реестра>/state

Подробнее о топиках Yandex IoT Core можно почитать вот тут.

Настройка отправки сообщений по MQTT

Возвращаемся к проекту Node-RED.

Кубик для отправки сообщений по MQTT в Node-RED уже предустановлен. Добавляем его в поток после функции "add_time_stamp"и настраиваем:

Указываем адрес топика, в который собираемся писать, например топик реестра. Уровень сервиса (QoS) ставим 0 - для наших задач отслеживание доставки не требуется:

Заходим в настройки сервера. Тут настраиваем подключение к брокеру:

Сервер: mqtt.cloud.yandex.net

Порт: 8883

Ставим галочку Enable secure (SSL/TLS) connection и заходим в настройки TLS:

Указываем пути до файлов ключей, сгенерированных на этапе настройки IoT Core:

Сохраняем настройки и деплоим проект. В итоге flow выглядит вот так:

А телеметрия успешно отправляется в облако!

Проверить это можно с помощью утилиты командной строки yc, подписавшись на указанный в настройках топик:

$ yc iot mqtt subscribe \--cert registry-cert.pem \ # файл сертификата реестра--key registry-key.pem \  # файл ключа реестра--topic '$devices/<ID устройства>/events' \--qos 1

Либо собрав отдельный поток Nod-RED с чтением из MQTT с помощью нода "mqtt in" (интереснее, если он будет работать на другом устройстве, но и та же самая малинка тоже подойдет):

Он настраивается аналогично "mqtt out", но обратите внимание, что для чтения надо создать отдельное подключение со своей конфигурацией TLS, в которую надо загрузить уже сертификаты реестра. Подключение от имени устройства свои же сообщения читать не будет. А вот топик должен быть именно тот, в который происходит запись.

Результат

И так, минимальный функционал шлюза реализован - телеметрия собирается, передается в облако и доступна в реальном времени из любой точки планеты. Можно, например, развернуть Node-RED на вашем локальном ПК и сделать на нем небольшой дашбордик, отражающий собираемые показатели.

А в качестве бонуса уже есть небольшой локальный монитор сигналов, доступный в локальной сети малинки.

В следующей части начнем сохранять получаемую телеметрию и посмотрим что ещё с ней можно делать в облаке.

Пример потоков Node-RED, описываемых в статье, можно скачать тут.

Подробнее..

ModBus Slave RTUASCII без смс и регистрации

03.11.2020 20:15:07 | Автор: admin
image

В открытом доступе присутствует множество библиотек для реализации ModBus Slave устройства, но они зачастую, содержат избыточный функционал, сложны в освоении и содержат грубые ошибки. В данной статье рассматривается библиотека, по скромному мнению автора, лишенная этих недостатков.

Программное обеспечение библиотеки поставляется в виде открытого исходного кода на языке Си.

modbus.h
//////////////////////////////////////////////////////////////////////    Асинхронная обработка сообщение ModBus v2   ////    Автор - Iван                                                      /////////////////////////////////////////////////////////////////////#ifndef __MODBUS_H#define __MODBUS_H#include "main.h"/////////////////////////////////////////////////////////////////////////////////Настройки МодБас//Данные, регистры Модбас #define ModBusUseGlobal (0) //Использовать глобальные входы/выходы, входные/выходные регистры//Функции протокола Модбас#define ModBusUseFunc1  (0) //Использовать функцию 1  - чтение статуса Coils (дискретных выходных битов)#define ModBusUseFunc2  (0) //Использовать функцию 2  - чтение статуса дискретных входов#define ModBusUseFunc3  (1) //Использовать функцию 3  - чтение значения выходных регистров#define ModBusUseFunc4  (0) //Использовать функцию 4  - чтение значения входных регистров#define ModBusUseFunc5  (0) //Использовать функцию 5  - запись выходного бита#define ModBusUseFunc6  (1) //Использовать функцию 6  - запись выходного регистра#define ModBusUseFunc15 (0) //Использовать функцию 15 - запись нескольких выходных битов#define ModBusUseFunc16 (1) //Использовать функцию 16 - запись нескольких выходных регистров//Адрес устройства#define ModBusID (1) //Адрес на шине МодБас#define ModBusID_FF (255) //Адрес на шине МодБас, на который отвечает всегда//Таймауты#define ModBusMaxPause (5)//Пауза между символами, для определения начала пакета [mS], #define ModBusMaxPauseResp (2) //Пауза между запросом Мастера и ответом Слайва [mS]//Длинна пакетов#define ModBusMaxPaketRX (96)//Максимальный размер принимаемого пакета <127//Дискретные входы#define ModBusMaxInBit (0) //Количество дискретных входов #define ModBusMaxInBitTX (8) //Максимальное количество дискретных входов при передаче пакета #define ModBusMaxInByte ((ModBusMaxInBit+7)/8) //Количество дискретных входов в байтах//Дискретные выходы#define ModBusMaxOutBit (0) //Количество дискретных выходов#define ModBusMaxOutByte ((ModBusMaxOutBit+7)/8) //Количество дискретных выходов в байтах#define ModBusMaxOutBitTX (8) //Максимальное количество дискретных выходов в передаваемом пакете #define ModBusMaxOutBitRX (8) //Максимальное количество дискретных выходов доступное для груповой установки//Регистры доступные для чтения#define ModBusMaxInReg (0) //Количество входных регистров (регистры только для чтения)#define ModBusMaxInRegTX (24) //Максимальное количество входных регистров в передаваемом пакете //Регистры доступные для чтения-записи#define ModBusMaxOutReg (48) //Количество выходных регистров#define ModBusMaxOutRegTX (32)//Максимальное количество выходных регистров в передаваемом пакете #define ModBusMaxOutRegRX (32)//Максимальное количество выходных регистров  доступное для груповой установки//////////////////////////////////////////////////////////////////////////////////Опорные функции, связь с системой//Системный таймер, инкрементируется каждую миллисекунду#define ModBusSysTimer TimingDelay//Запись байта в поток последовательного порта - void ModBusPUT(unsigned char A)#define ModBusPUT(A) PutFifo0(A) //Чтение байта из потока последовательного порта, - unsigned short ModBusGET(void)//Если нет данных возвращает 0х0000, иначе возвращает 0х01ХХ#define ModBusGET()  Inkey16Fifo0() //////////////////////////////////////////////////////////////////////////////////Инициализация void ModBusIni(void);//Функция обработки Сообщений модбас RTU//Работает совместно с системным таймером //Использует макросы ModbusPUT(A) ModbusGET()void ModBusRTU(void);//Функция обработки Сообщений модбас ASCII//Работает совместно с системным таймером //Использует макросы ModbusPUT(A) ModbusGET()void ModBusASCII(void);//Заполнение регистров Модбас//перенос данных из программных переменных в регистры МодБасvoid Prg2ModBusOutBit(void);void Prg2ModBusInBit(void);void Prg2ModBusOutReg(void);void Prg2ModBusInReg(void);//Считывание регистров Модбас//перенос данных из регистров МодБас в программные переменныеvoid ModBus2PrgOutBit(void);void ModBus2PrgOutReg(void);#pragma pack(push,1)//Тип данных для работы с дискретными входами/выходамиtypedef union  {  unsigned char byte;  struct    {    unsigned char bit0:1;    unsigned char bit1:1;    unsigned char bit2:1;    unsigned char bit3:1;    unsigned char bit4:1;    unsigned char bit5:1;    unsigned char bit6:1;    unsigned char bit7:1;    };  }  ModBusBit_t;#pragma pack(pop)  #ifdef __MODBUS2PRG_C#if ModBusMaxInBit!=0ModBusBit_t ModBusInBit[ModBusMaxInByte]; //массив дискретных входов#endif#if ModBusMaxOutBit!=0ModBusBit_t ModBusOutBit[ModBusMaxOutByte]; //массив дискретных выходов#endif#if ModBusMaxInReg!=0unsigned short ModBusInReg[ModBusMaxInReg]; //массив входных регистров#endif#if ModBusMaxOutReg!=0unsigned short ModBusOutReg[ModBusMaxOutReg]; //массив выходных регистров#endif#else #if ModBusUseGlobal!=0 || defined(__MODBUS_C)#if ModBusMaxInBit!=0extern ModBusBit_t ModBusInBit[ModBusMaxInByte]; //массив дискретных входов#endif#if ModBusMaxOutBit!=0extern ModBusBit_t ModBusOutBit[ModBusMaxOutByte]; //массив дискретных выходов#endif#if ModBusMaxInReg!=0extern unsigned short ModBusInReg[ModBusMaxInReg]; //массив входных регистров#endif#if ModBusMaxOutReg!=0extern unsigned short ModBusOutReg[ModBusMaxOutReg]; //массив выходных регистров#endif#endif//#if ModBusUseGlobal!=0#endif//#ifdef __MODBUS2PRG_C#endif//#ifndef __MODBUS_H


modbus.c
#define __MODBUS_C#include "modbus.h"static unsigned char PaketRX[ModBusMaxPaketRX];//массив для сохранения пакетаstatic unsigned char UkPaket;//указатель в массиве, текущий принятый символ static unsigned long TimModbus; //время приема пакета по системному таймеруstatic unsigned short CRCmodbus;//текущий CRCstatic unsigned char Sost;//состояние 0/1 прием/передача//Инициализация void ModBusIni(void)  {  TimModbus=ModBusSysTimer;//запомнить таймер  UkPaket=0;//сбросить указатель пакета  CRCmodbus=0xFFFF; //установить начальное значение CRC  //Инициализация регистров МодБас#if ModBusMaxOutBit!=0  Prg2ModBusOutBit();#endif  #if ModBusMaxInBit!=0    Prg2ModBusInBit();#endif  #if ModBusMaxOutReg!=0    Prg2ModBusOutReg();#endif  #if ModBusMaxInReg!=0  Prg2ModBusInReg();#endif    return;  }//Функция вычисления CRCstatic inline unsigned short CRCfunc(unsigned short inCRC, unsigned char in)  {  inCRC=inCRC^in;  for(int j=0;j<8;j++){if(inCRC&1){inCRC=(inCRC>>1)^0xA001U;}else {inCRC=inCRC>>1;}}  return inCRC;  }//Функция обработки Сообщений модбасvoid ModBusRTU(void)  {  if(Sost==0)    {//Состояние прием    while(!0)      {//Цикл приема символов      unsigned short Tmp=ModBusGET(); //читаем символ из входного потока      if(Tmp==0) return; //если нет данных - возврат       //символ принят      Tmp=Tmp&0xFF;//отбрасываем признак приема байта      //Проверка временного интервала между символами      if((ModBusSysTimer-TimModbus)>ModBusMaxPause)        {//превышен таймаут, начинаем прием нового пакета        PaketRX[0]=Tmp;//сохранить принятый символ в буфер приема        UkPaket=1;//установить указатель пакета        TimModbus=ModBusSysTimer;//сбросить таймер        //вычисление CRC        CRCmodbus=CRCfunc(0xFFFF,Tmp);        continue;//повторный запрос символа        }      else        {//таймаут не превышен, принимаем уже начатый пакет        TimModbus=ModBusSysTimer;//сбросить таймер        PaketRX[UkPaket]=Tmp;//сохранить принятый символ        UkPaket++;//инкремент указателя пакета        if(UkPaket==ModBusMaxPaketRX)//проверяем на длину пакета          {//буфер пакета переполнился          UkPaket=0;//сбросить указатель пакета          CRCmodbus=0xFFFF; //установить начальное значение CRC          return;//ошибка, повторный запрос символа требуется          }        //вычисление CRC        CRCmodbus=CRCfunc(CRCmodbus,Tmp);        }      //Если принято мало данных      if(UkPaket<8) continue; //повторный запрос символа      //проверка на принятие пакета      if(CRCmodbus==0)         {//проверка на длинные пакеты        if(PaketRX[1]==15 || PaketRX[1]==16)          {//если длинные команды (15,16) , проверяем "Счетчик байт"          if((PaketRX[6]+9)!=UkPaket) continue;          }        break; //Ура! Пакет принят!!!        }      }    //////////////////////////////////////////////////////////////////////////////    //                         Ура! Пакет принят!!!    /////////////////////////////////////////////////////////////////////////////    UkPaket=0;//сбросить указатель пакета        //проверка адреса    if((PaketRX[0]!=ModBusID)&&(PaketRX[0]!=ModBusID_FF))      {//Не наш адрес      CRCmodbus=0xFFFF; //установить начальное значение CRC      return;//повторный запрос не требуется      }              //переходим в состояние передача ответа    Sost=!0;#if ModBusMaxPauseResp!=0      return;//повторный запрос не требуется#endif     }    /////////////////////////////////////////////////////////////////////////////   if(Sost!=0 #if ModBusMaxPauseResp!=0          && (ModBusSysTimer-TimModbus)>=ModBusMaxPauseResp#endif          )    {//Состояние передача ответа    Sost=0;    /////////////////////////////////////////////////////////////////////////////        //                       обработка команд                                  //    /////////////////////////////////////////////////////////////////////////////    //Код функции 01 - чтение статуса Coils (дискретных выходных битов).     /*Сообщение-запрос содержит адрес начального бита и количество битов для чтения.     Биты нумеруются начиная с 0.     В сообщении-ответе каждое значение переменной передается одним битом,    то есть в одном байте пакуется статус 8 битов переменных.     Если количество их не кратно восьми, остальные биты в байте заполняются нулями.     Счетчик вмещает количество байт в поле данных.    01 Чтение статуса выходов           ОПИСАНИЕ           Читает статуса ON/OFF дискретных выходов в подчиненном.           ЗАПРОС           Запрос содержит адрес начального выхода и количество выходов для чтения.           Выхода адресуются начиная с нуля: выхода 1-16 адресуются как 0-15.          Ниже приведен пример запроса на чтение выходов 20-56 с подчиненного устройства 17.           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция011          Начальный адрес Hi002          Начальный адрес Lo133          Количество Hi004          Количество Lo255          Контрольная сумма (CRC или LRC)--          ОТВЕТ           Статус выходов в ответном сообщении передается как один выход на бит.          Если возвращаемое количество выходов не кратно восьми, то оставшиеся биты в последнем байте сообщения будут установлены в 0.           Счетчик байт содержит количество байт передаваемых в поле данных.           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция011          Счетчик байт052          Данные(Выхода 27-20)CD3          Данные(Выхода 35-28)6B4          Данные(Выхода 43-36)B25          Данные(Выхода 51-44)0E6          Данные(Выхода 56-52)1B7          Контрольная сумма (CRC или LRC)--    */#if ModBusUseFunc1!=0           if(PaketRX[1]==0x01)      {      //вычисление адреса запрашиваемых бит      unsigned short AdresBit=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //вычисление количества запрашиваемых бит      unsigned short KolvoBit=((((unsigned short)PaketRX[4])<<8)|(PaketRX[5]));      //если неправильный адрес и количество      if((AdresBit+KolvoBit)>(ModBusMaxOutBit) || KolvoBit>ModBusMaxOutBitTX || KolvoBit==0)        {//неправильный адрес и количество        CRCmodbus=0xFFFF; //установить начальное значение CRC        return;//повторный запрос не требуется        }      Prg2ModBusOutBit();//Заполнение регистров Модбас (GlobalDate->ModBus)      //формирование пакета ответа      //адрес      ModBusPUT(PaketRX[0]);      CRCmodbus=CRCfunc(0xFFFF,PaketRX[0]);      //код команды          ModBusPUT(1);      CRCmodbus=CRCfunc(CRCmodbus,1);      //количества полных байт      ModBusPUT((KolvoBit+7)>>3);      CRCmodbus=CRCfunc(CRCmodbus,((KolvoBit+7)>>3));      //копирование битов в пакет ответа      unsigned char TxByte=0;//текущий байт      unsigned char Bit=AdresBit&7;//указатель бит в ModBusOutBit[]      AdresBit=AdresBit>>3;//указатель байт ModBusOutBit[]      //копирование из регистра ModBusOutBit[] в пакет      int i=0;      while(!0)        {        if((ModBusOutBit[AdresBit].byte)&(1<<Bit))          {          TxByte=TxByte|(1<<(i&7));          }        //инкрементруем указатели         Bit++;        if(Bit==8){Bit=0;AdresBit++;}        i++;        if((i&7)==0)          {          ModBusPUT(TxByte);          CRCmodbus=CRCfunc(CRCmodbus,TxByte);          TxByte=0;          if(i==KolvoBit) break; else continue;          }        if(i==KolvoBit)           {          ModBusPUT(TxByte);          CRCmodbus=CRCfunc(CRCmodbus,TxByte);          break;          }        }      ModBusPUT(CRCmodbus);      ModBusPUT(CRCmodbus>>8);      //конец      CRCmodbus=0xFFFF; //установить начальное значение CRC      return;//повторный запрос не требуется       }#endif        /////////////////////////////////////////////////////////////////////////////    //Код функции 2 - чтение статуса дискретных входов    /*02 Read Input Status           ОПИСАНИЕ           Чтение ON/OFF состояния дискретных входов (ссылка 1Х) в пдчиненном.           ЗАПРОС           Запрос содержит номер начального входа и количество входов для чтения. Входа адресуются начиная с 0.          Ниже приведен пример запроса на чтение входов 10197-10218 с подчиненного устройства 17.                   Запрос           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция021          Начальный адрес ст.002          Начальный адрес мл.C43          Кол-во входов ст.004          Кол-во входов мл.165          Контрольная сумма--          ОТВЕТ           Статус входов в ответном сообщении передается как один выход на бит.          Если возвращаемое количество входов не кратно восьми, то оставшиеся биты в последнем байте сообщения будут установлены в 0.           Счетчик байт содержит количество байт передаваемых в поле данных.           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция011          Счетчик байт032          Данные(Входы 10204-10197)AC3          Данные(Входы 10212-10205)DB4          Данные(Входы 10218-10213)355          Контрольная сумма (CRC или LRC)--      */#if ModBusUseFunc2!=0         if(PaketRX[1]==0x02)      {      //вычисление адреса запрашиваемых бит      unsigned short AdresBit=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //вычисление количества запрашиваемых бит      unsigned short KolvoBit=((((unsigned short)PaketRX[4])<<8)|(PaketRX[5]));      //если неправильный адрес и количество      if((AdresBit+KolvoBit)>(ModBusMaxInBit) || KolvoBit>ModBusMaxInBitTX || KolvoBit==0)        {//неправильный адрес и количество        CRCmodbus=0xFFFF; //установить начальное значение CRC        return;//повторный запрос не требуется        }      Prg2ModBusInBit();//Заполнение регистров Модбас (GlobalDate->ModBus)      //формирование пакета ответа      //адрес      ModBusPUT(PaketRX[0]);      CRCmodbus=CRCfunc(0xFFFF,PaketRX[0]);      //код команды          ModBusPUT(2);      CRCmodbus=CRCfunc(CRCmodbus,2);      //количества полных байт      ModBusPUT((KolvoBit+7)>>3);      CRCmodbus=CRCfunc(CRCmodbus,((KolvoBit+7)>>3));      //копирование битов в пакет ответа      unsigned char TxByte=0;//текущий байт      unsigned char Bit=AdresBit&7;//указатель бит       AdresBit=AdresBit>>3;//указатель байт       //копирование из регистра ModBusInBit[] в пакет      int i=0;      while(!0)        {        if((ModBusInBit[AdresBit].byte)&(1<<Bit))          {//устанавливаем бит в пакете          TxByte=TxByte|(1<<(i&7));          }        //инкрементруем указатели         Bit++;        if(Bit==8){Bit=0;AdresBit++;}        i++;        if((i&7)==0)          {          ModBusPUT(TxByte);          CRCmodbus=CRCfunc(CRCmodbus,TxByte);          TxByte=0;          if(i==KolvoBit) break; else continue;          }        if(i==KolvoBit)          {          ModBusPUT(TxByte);          CRCmodbus=CRCfunc(CRCmodbus,TxByte);          break;          }        }      ModBusPUT(CRCmodbus);      ModBusPUT(CRCmodbus>>8);      //конец      CRCmodbus=0xFFFF; //установить начальное значение CRC      return;//повторный запрос не требуется      }#endif        /////////////////////////////////////////////////////////////////////////////    //Код функции 03 - чтение значения выходных/внутренних регистров.     /*Сообщение-запрос содержит адрес начального исходного/внутреннего регистра (двухбайтовое слово),     и количество регистров для чтения. Регистры нумеруются начиная с 0.    03 Read Holding Registers           ОПИСАНИЕ           Чтение двоичного содержания регистров (ссылка 4Х) в подчиненном.           ЗАПРОС           Сообщение запроса специфицирует начальный регистр и количество регистров для чтения.           Регистры адресуются начина с 0: регистры 1-16 адресуются как 0-15.          Ниже приведен пример чтения регистров 40108-40110 с подчиненного устройства 17.           Запрос           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция031          Начальный адрес ст.002          Начальный адрес мл.6B3          Кол-во регистров ст.004          Кол-во регистров мл.035          Контрольная сумма--          ОТВЕТ           Данные регистров в ответе передаются как два бйта на регистр.           Для каждого регистра, первый байт содержит старшие биты второй байт содержит младшие биты.          За одно обращение может считываться 125 регистров для контроллеров 984-Х8Х (984-685 и т.д.),           и 32 регистра для других контроллеров. Ответ дается когда все данные укомплектованы.          Это пример ответа на запрос представленный выше:           Ответ           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция031          Счетчик байт062          Данные (регистр 40108) ст.023          Данные (регистр 40108) мл.2B4          Данные (регистр 40109) ст.005          Данные (регистр 40109) мл.006          Данные (регистр 40110) ст.007          Данные (регистр 40110) мл.648          Контрольная сумма--    */#if ModBusUseFunc3!=0          if(PaketRX[1]==0x03)      {      //вычисление адреса запрашиваемых слов      unsigned short AdresWord=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //вычисление адреса количества запрашиваемых слов      unsigned short KolvoWord=((((unsigned short)PaketRX[4])<<8)|(PaketRX[5]));       //если неправильный адрес и количество      if(((AdresWord+KolvoWord)>ModBusMaxOutReg) || (KolvoWord>ModBusMaxOutRegTX))        {//тады конец        CRCmodbus=0xFFFF;//установить начальное значение CRC        return;//Ошибка, повторный запрос не требуется        }      Prg2ModBusOutReg();//Заполнение регистров Модбас (GlobalDate->ModBus)      //формирование пакета ответа      //адрес      ModBusPUT(PaketRX[0]);      CRCmodbus=CRCfunc(0xFFFF,PaketRX[0]);      //код команды          ModBusPUT(3);      CRCmodbus=CRCfunc(CRCmodbus,3);      //количества полных байт      ModBusPUT(KolvoWord<<1);      CRCmodbus=CRCfunc(CRCmodbus,(KolvoWord<<1));      //Копирование из регистра ModBusOutReg[] в пакет ответа      for(int i=0;i<KolvoWord;i++)        {        ModBusPUT(ModBusOutReg[AdresWord+i]>>8);        CRCmodbus=CRCfunc(CRCmodbus,(ModBusOutReg[AdresWord+i]>>8));        ModBusPUT(ModBusOutReg[AdresWord+i]>>0);        CRCmodbus=CRCfunc(CRCmodbus,(ModBusOutReg[AdresWord+i]>>0));        }      ModBusPUT(CRCmodbus);      ModBusPUT(CRCmodbus>>8);      //конец      CRCmodbus=0xFFFF; //установить начальное значение CRC      return;//повторный запрос не требуется      }#endif         /////////////////////////////////////////////////////////////////////////////    //Код функции 04 - чтение значения входных регистров    /*04 Read Input Registers           СОДЕРЖАНИЕ           Чтение двоичного содержания входных регистров (ссылка 3Х) в подчиненном.           ЗАПРОС           Запрос содержит номер начального регистра и количество регистров для чтения.          Ниже приведен пример запроса для чтения регистра 30009 с подчиненного устройства 17.           Запрос           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция031          Начальный адрес ст.002          Начальный адрес мл.6B3          Кол-во регистров ст.004          Кол-во регистров мл.035          Контрольная сумма--             ОТВЕТ           Данные регистров в ответе передаются как два бйта на регистр.           Для каждого регистра, первый байт содержит старшие биты второй байт содержит младшие биты.          За одно обращение может считываться 125 регистров для контроллеров 984-Х8Х (984-685 и т.д.),           и 32 регистра для других контроллеров. Ответ дается когда все данные укомплектованы.          Это пример ответа на запрос представленный выше:           Ответ           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция031          Счетчик байт022          Данные (регистр 30009) ст.003          Данные (регистр 30009) мл.2A4          Контрольная сумма--      */#if ModBusUseFunc4!=0         if(PaketRX[1]==0x04)      {      //вычисление адреса запрашиваемых слов      unsigned short AdresWord=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //вычисление адреса количества запрашиваемых слов      unsigned short KolvoWord=((((unsigned short)PaketRX[4])<<8)|(PaketRX[5]));       //если неправильный адрес и количество      if(((AdresWord+KolvoWord)>ModBusMaxInReg) || (KolvoWord>ModBusMaxInRegTX))        {//тады конец        CRCmodbus=0xFFFF;//установить начальное значение CRC        return;//Ошибка, повторный запрос не требуется        }      Prg2ModBusInReg();//Заполнение регистров Модбас (GlobalDate->ModBus)      //формирование пакета ответа      //адрес      ModBusPUT(PaketRX[0]);      CRCmodbus=CRCfunc(0xFFFF,(PaketRX[0]));      //код команды          ModBusPUT(4);      CRCmodbus=CRCfunc(CRCmodbus,4);      //количества полных байт      ModBusPUT(KolvoWord<<1);      CRCmodbus=CRCfunc(CRCmodbus,(KolvoWord<<1));      //Копирование из регистра ModBusInReg[] в пакет ответа      for(int i=0;i<KolvoWord;i++)        {        ModBusPUT(ModBusInReg[AdresWord+i]>>8);        CRCmodbus=CRCfunc(CRCmodbus,(ModBusInReg[AdresWord+i]>>8));        ModBusPUT(ModBusInReg[AdresWord+i]>>0);        CRCmodbus=CRCfunc(CRCmodbus,(ModBusInReg[AdresWord+i]>>0));        }      ModBusPUT(CRCmodbus);      ModBusPUT(CRCmodbus>>8);      //конец      CRCmodbus=0xFFFF; //установить начальное значение CRC      return;//повторный запрос не требуется      }#endif          /////////////////////////////////////////////////////////////////////////////    //Код функции 05 - запись выходного/внутреннего бита    /*05 Force Single Coil           ОПИСАНИЕ           Установка единичного выхода (ссылка 0Х) в ON или OFF.           При широковещательной передаче функция устанавливает все выходы с данным адресом во всех подчиненных контроллерах.           ЗАМЕЧАНИЕ Функция может пересекаться с установкой защиты                          памяти и установкой недоступности выходов.           ЗАПРОС           Запрос содержит номер выхода для установки. Выходы адресуются начиная с 0. Выход 1 адресуется как 0.          Состояние, в которое необходимо установить выход (ON/OFF) описывается в поле данных.           Величина FF00 Hex - ON. Величина 0000 - OFF. Любое другое число неверно и не влияет на выход.          В приведенном ниже примере устанавливается выход 173 в состояние ON в подчиненном устройстве 17.           Запрос           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция051          Адрес выхода мл.002          Адрес выхода ст.AC3          Данные ст.FF4          Данные мл.005          Контрольная сумма--             ОТВЕТ           Нормальный ответ повторяет запрос.           Ответ           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция051          Адрес выхода мл.002          Адрес выхода ст.AC3          Данные ст.FF4          Данные мл.005          Контрольная сумма--      */#if ModBusUseFunc5!=0         if(PaketRX[1]==0x05)      {      //вычисление адреса записываемого выхода      unsigned short AdresBit=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //проверка на допустимый адрес        if(AdresBit>=ModBusMaxOutBit)        {//если неправильный адрес        CRCmodbus=0xFFFF; //установить начальное значение CRC        return;//Ошибка, повторный запрос не требуется        }      //установка сброс бита      switch (((((unsigned short)PaketRX[4])<<8)|(PaketRX[5])))        {        case 0xFF00:        //установка бита        ModBusOutBit[(AdresBit>>3)].byte|=(1<<(AdresBit&7));        break;        case 0x0000:        //сброс бита        ModBusOutBit[(AdresBit>>3)].byte&=(~(1<<(AdresBit&7)));        break;        default:          {//конец          CRCmodbus=0xFFFF; //установить начальное значение CRC          return;//Ошибка, повторный запрос не требуется          }         }      //Ответ      for(int i=0;i<8;i++) ModBusPUT(PaketRX[i]);//запускаем передачу пакета ответа      ModBus2PrgOutBit();//Считывание регистров Модбас (ModBus->GlobalDate)      //конец      CRCmodbus=0xFFFF; //установить начальное значение CRC      return;//повторный запрос не требуется       }#endif         /////////////////////////////////////////////////////////////////////////////    //Код функции 06 - запись выходного/внутреннего регистра.     /*Функция аналогична 05, но оперирует с регистрами (словами).     В запросе указывается номер выходного/внутреннего регистра и его значение.     06 Preset Single Register           ОПИСАНИЕ. Записывает величину в единичный регистр (ссылка 4Х).          При щироковезательной передаче на всех подчиненных устройствах устанавливается один и тот же регистр.           ЗАМЕЧАНИЕ           Функция может пересекаться с установленной защитой памяти.           ЗАПРОС           Запрос содержит ссылку на регистр, который необходимо установить. Регистры адресуются с 0.          Величина, в которую необходимо установить регистр передается в поле данных.           Контроллеры M84 и 484 используют 10-ти битную величину, старшие шесть бит заполняются 0.           Все другие контроллерыиспользуют 16 бит.          В приведенном ниже примере в регистр 40002 записывается величина 0003 Hex в подчиненном устройстве 17.           Запрос           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция061          Адрес регистра мл.002          Адрес регистра ст.013          Данные ст.004          Данные мл.035          Контрольная сумма--             ОТВЕТ           Нормальный ответ повторяет запрос.           Ответ           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция061          Адрес регистра мл.002          Адрес регистра ст.013          Данные ст.004          Данные мл.035          Контрольная сумма--      */#if ModBusUseFunc6!=0        if(PaketRX[1]==0x06)      {      //вычисление адреса записываемого выхода      unsigned short AdresWord=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //проверка на допустимый адрес        if(AdresWord>=(ModBusMaxOutReg))        {//если неправильный адрес        CRCmodbus=0xFFFF; //установить начальное значение CRC        return;//Ошибка, повторный запрос не требуется        }      //запись слова      ModBusOutReg[AdresWord]=(((((unsigned short)PaketRX[4])<<8)|(PaketRX[5])));      //Ответ      for(int i=0;i<8;i++) ModBusPUT(PaketRX[i]);//запускаем передачу пакета ответа      ModBus2PrgOutReg();//Считывание регистров Модбас (ModBus->GlobalDate)      //конец      CRCmodbus=0xFFFF; //установить начальное значение CRC      return;//повторный запрос не требуется      }#endif         /////////////////////////////////////////////////////////////////////////////    //Код функции 0x0F - запись нескольких выходных/внутренних битов.     /*В запросе указывается начальный адрес бита, количество бит для записи, счетчик байтов и непосредственно значения.     15 (0F Hex) Force Multiple Coils           ОПИСАНИЕ           Устанавливает каждый выход (ссылка 0Х) последовательности выходов в одно из состояний ON или OFF.           При широковещательной передаче функция устанавливает подобные выходы на всех подчиненных.           ЗАМЕЧАНИЕ Функция может пересекаться с установкой защиты памяти и установкой недоступности выходов.           ЗАПРОС           Запрос специфицирует выходы для установки. Выходы адресуются начиная с 0.          Ниже показан пример запроса на установку последовательности выходов начиная с 20 (адресуется как 19)           в подчиненном устройстве 17.          Поле данных запроса содержит 2 байта: CD 01 Hex (1100 1101 0000 0001 двоичное).           Соответствие битов и выходов представлено ниже:           Бит:    1  1  0  0  1  1  0  10  0  0  0  0  0   0  1           Выход: 27 26 25 24 23 22 21 20-  -  -  -  -  -  29 28           Запрос           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция0F1          Адрес выхода ст.002          Адрес выхода мл.133          Кол-во выходов ст.004          Кол-во выходов мл.0A5          Счетчик байт026          Данные для установки (Выходы 27-20)CD7          Данные для установки (Выходы 29-28) 018          Контрольная сумма--9             ОТВЕТ           Нормальный ответ возвращает адрес подчиненного, код функции, начальный адрес, и количество установленных выходов.          Это пример ответа на представленный выше запрос.           Ответ           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция0F1          Адрес выхода ст.002          Адрес выхода мл.133          Кол-во выходов ст.004          Кол-во выходов мл.0A5          Контрольная сумма--    */#if ModBusUseFunc15!=0        if(PaketRX[1]==0x0F)      {      //вычисление адреса записываемых бит      unsigned short AdresBit=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //вычисление количества записываемых бит      unsigned short KolvoBit=(((((unsigned short)PaketRX[4])<<8)|(PaketRX[5])));      //если неправильный адрес и количество      if(((AdresBit+KolvoBit)>ModBusMaxOutBit) || (KolvoBit>ModBusMaxOutBitRX))        {//тады конец        CRCmodbus=0xFFFF; //установить начальное значение CRC        return;//Ошибка, повторный запрос не требуется        }      //установка битов      unsigned char Bit=(AdresBit&7);//указатель бит в ModBusOutBit[]      AdresBit=AdresBit>>3;//указатель байт ModBusOutBit[]      //цикл по битам      for(int i=0;i<KolvoBit;i++)        {        if(PaketRX[7+(i>>3)]&(1<<(i&7)))//если текущий бит PaketRX равен 1          {//устанавливаем бит в ModBusOutBit[]          ModBusOutBit[AdresBit].byte=(ModBusOutBit[AdresBit].byte)|((unsigned char)(1<<Bit));          }        else          {//сбрасываем бит ModBusOutBit[]          ModBusOutBit[AdresBit].byte=(ModBusOutBit[AdresBit].byte)&((unsigned char)(~(1<<Bit)));          }        //инкрементруем указатели         Bit++;if(Bit==8){Bit=0;AdresBit++;}        }                 //вычисляем CRC пакета передачи и передаем      CRCmodbus=0xFFFF;      for(int i=0;i<6;i++)        {        ModBusPUT(PaketRX[i]);        CRCmodbus=CRCfunc(CRCmodbus,(PaketRX[i]));        }      ModBusPUT(CRCmodbus);      ModBusPUT(CRCmodbus>>8);                ModBus2PrgOutBit();//Считывание регистров Модбас (ModBus->GlobalDate)            //конец      CRCmodbus=0xFFFF; //установить начальное значение CRC      return;//повторный запрос не требуется      }#endif         //Код функции 0x10 запись нескольких выходных/внутренних регистров.    /*16 (10 Hex) Preset Multiple Regs           ОПИСАНИЕ           Запись данных в последовательность регистров (ссылка 4Х).           При широковещательной передаче, функция устанавливает подобные регистры во всех подчиненных устройствах.           ЗАМЕЧАНИЕ           Функция может пересекаться с установленной защитой памяти.           ЗАПРОС           Запрос специфицирует регистры для записи. Регистры адресуются начиная с 0.          Данные для записи в регистры содержатся в поле данных запроса.           Контроллеры M84 и 484 используют 10-битовую величину, со старшими шестью битами установленными в 0.           Все остальные контроллеры используют 16 бит.          Ниже приведен пример запроса на установку двух регистров начиная с 40002 в 00 0A и 01 02 Hex,           в подчиненном устройстве 17:           Запрос           Имя поляПример                                                                  (Hex)           Адрес подчиненного110          Функция101          Начальный адрес002          Начальный адрес013          Кол-во регистров ст.004          Кол-во регистров мл.025          Счетчик байт046          Данные ст.007          Данные мл.0A8          Данные ст.019          Данные мл.0210          Контрольная сумма--             ОТВЕТ           Нормальный ответ содержит адрес подчиненного, код функции, начальный адрес, и количество регистров.     */#if ModBusUseFunc16!=0         if(PaketRX[1]==0x10)      {      //вычисление адреса записываемых слов      unsigned short b=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //вычисление количества записываемых слов      unsigned short c=(((((unsigned short)PaketRX[4])<<8)|(PaketRX[5])));      //если неправильный адрес и количество      if(((b+c)>ModBusMaxOutReg) || c>ModBusMaxOutRegRX || c==0)        {//тады конец        CRCmodbus=0xFFFF;//установить начальное значение CRC        return;//Ошибка, повторный запрос не требуется        }      //Копирование из пакета в регистр ModBusOutReg[]      for(int i=0;i<c;i++)        {        ModBusOutReg[b+i]=(((unsigned short)PaketRX[7+(i<<1)])<<8)|(PaketRX[8+(i<<1)]);        }      //вычисляем CRC пакета передачи и передаем      CRCmodbus=0xFFFF;      for(int i=0;i<6;i++)        {        ModBusPUT(PaketRX[i]);        CRCmodbus=CRCfunc(CRCmodbus,(PaketRX[i]));        }      ModBusPUT(CRCmodbus);      ModBusPUT(CRCmodbus>>8);      ModBus2PrgOutReg();//Считывание регистров Модбас (ModBus->GlobalDate)      //конец      CRCmodbus=0xFFFF; //установить начальное значение CRC      return;//повторный запрос не требуется       }#endif             /////////////////////////////////////////////////////////////////////////////    //полный конец    CRCmodbus=0xFFFF; //установить начальное значение CRC    return;////Ошибка, нераспознана команда, повторный запрос не требуется    }  return;//повторный запрос не требуется   }//Функция конвертация шеснадцатиричных символов в числоstatic inline unsigned char Hex2Dig(unsigned char h)  {  if((h>='0')&&(h<='9')) return (h -'0');  if((h>='A')&&(h<='F')) return (h -'A'+10);  return 0;  }static unsigned char LRCmodbus;//тукущий LRCstatic unsigned char Simvol0;//предидущий принятвй символ#define ASCII_CR (0x0D)//возврат каретки #define ASCII_LF (0x0A)//перевод строкиstatic const unsigned char BCD[]="0123456789ABCDEF";//строка для конвертации числа в символ//Функция обработки Сообщений модбас ASCIIvoid ModBusASCII(void)  {  if(Sost==0)    {//Состояние прием    while(!0)      {//Цикл приема символов      unsigned short Tmp=ModBusGET(); //читаем символ из входного потока      if(Tmp==0) return; //если нет данных повторный запрос не требуется       //Символ принят      Tmp=Tmp&0xFF;//отбрасываем признак приема байта      //проверка на начало пакета      if(Tmp==':')        {//начало пакета        LRCmodbus=0;//обнуляем LRC        UkPaket=0;//указатель в массиве, текущий принятый символ        continue;//запускаем повторный запрос символа        }             //проверка на алфавит сообщения      if(!(           ((Tmp>='0')&&(Tmp<='9'))||           ((Tmp>='A')&&(Tmp<='F'))||           (Tmp==ASCII_CR)||           (Tmp==ASCII_LF)           ))         {        return;//Ошибка, повторный запрос не требуется        }              //сохраняем принятый символ      if((UkPaket&1)==0)        {//указатель принятых данных четный 0,2,4,6...        Simvol0=Tmp; //сохраняем первый символ пакета        UkPaket++; //икреметируем указатель пакета        continue;//запускаем повторный запрос         }      else         {//указатель принятых данных нечетный 1,3,5,7...        if(Tmp!=ASCII_LF)          {//не достигнут конец          PaketRX[UkPaket>>1]=(Hex2Dig(Simvol0)<<4)|(Hex2Dig(Tmp));//сохраняем байт пакета           LRCmodbus=LRCmodbus-PaketRX[UkPaket>>1];//считаем LRC          UkPaket++;//икреметируем указатель пакета          if(UkPaket>(ModBusMaxPaketRX<<1))//проверка на переполнение            {//Буфер приема переполнился            UkPaket=0;//сбросить указатель пакета            return;//ошибка, повторный запрос не требуется            }          }        else break;        }      }              //Проверка LCR    if(LRCmodbus!=0) return;//Ошибка, повторный запрос не требуется        //Провекка адреса    if((PaketRX[0]!=ModBusID)&&(PaketRX[0]!=ModBusID_FF))      {//Не наш адрес      return;//повторный запрос не требуется      }          //преходим в состояние передача    Sost=!0;    TimModbus=ModBusSysTimer;//запомнить таймер#if ModBusMaxPauseResp!=0      return;//повторный запрос не требуется#endif      }      /////////////////////////////////////////////////////////////////////////////   if(Sost!=0 #if ModBusMaxPauseResp!=0          && (ModBusSysTimer-TimModbus)>=ModBusMaxPauseResp#endif          )    {//Состояние передача ответа    Sost=0;    /////////////////////////////////////////////////////////////////////////////        //                       обработка команд                                  //    /////////////////////////////////////////////////////////////////////////////#if ModBusUseFunc1!=0         //01 Чтение статуса выходов     if(PaketRX[1]==0x01)      {      //вычисление адреса запрашиваемых бит      unsigned short AdresBit=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //вычисление количества запрашиваемых бит      unsigned short KolvoBit=((((unsigned short)PaketRX[4])<<8)|(PaketRX[5]));      //если неправильный адрес и количество      if((AdresBit+KolvoBit)>(ModBusMaxOutBit) || KolvoBit>ModBusMaxOutBitTX || KolvoBit==0)        {//конец        return;//Ошибка, повторный запрос не требуется        }      Prg2ModBusOutBit();//Заполнение регистров Модбас (GlobalDate->ModBus)      //формирование пакета ответа      ModBusPUT(':');      //адрес      ModBusPUT(BCD[PaketRX[0]>>4]);//Передаем старший       ModBusPUT(BCD[PaketRX[0]&0x0F]);//передаем младший      LRCmodbus=0-PaketRX[0];//считаем LRC      //код команды          ModBusPUT(BCD[1>>4]);//Передаем старший       ModBusPUT(BCD[1&0x0F]);//передаем младший      LRCmodbus=LRCmodbus-1;//считаем LRC      //количества полных байт      ModBusPUT(BCD[((KolvoBit+7)>>3)>>4]);//Передаем старший       ModBusPUT(BCD[((KolvoBit+7)>>3)&0x0F]);//передаем младший      LRCmodbus=LRCmodbus-((KolvoBit+7)>>3);//считаем LRC      //копирование битов в пакет ответа      unsigned char TxByte=0;//текущий байт      unsigned char Bit=AdresBit&7;//указатель бит в ModBusOutBit[]      AdresBit=AdresBit>>3;//указатель байт ModBusOutBit[]      //копирование из регистра ModBusOutBit[] в пакет      int i=0;      while(!0)        {        if((ModBusOutBit[AdresBit].byte)&(1<<Bit))//если текущий бит ModBusOutBit[] равен 1          {//устанавливаем бит в пакете          TxByte=TxByte|(1<<(i&7));          }        //инкрементруем указатели         Bit++;        if(Bit==8){Bit=0;AdresBit++;}        i++;        if((i&7)==0)          {          ModBusPUT(BCD[TxByte>>4]);//Передаем старший           ModBusPUT(BCD[TxByte&0x0F]);//передаем младший          LRCmodbus=LRCmodbus-TxByte;//считаем LRC          TxByte=0;          if(i==KolvoBit) break; else continue;          }        if(i==KolvoBit)           {          ModBusPUT(BCD[TxByte>>4]);//Передаем старший           ModBusPUT(BCD[TxByte&0x0F]);//передаем младший          LRCmodbus=LRCmodbus-TxByte;//считаем LRC          break;          }        }      ModBusPUT(BCD[LRCmodbus>>4]);      ModBusPUT(BCD[LRCmodbus&0x0F]);      ModBusPUT(ASCII_CR);      ModBusPUT(ASCII_LF);      //конец      return;//повторный запрос не требуется      }#endif#if ModBusUseFunc2!=0         //02 Read Input Status     if(PaketRX[1]==0x02)      {      //вычисление адреса запрашиваемых бит      unsigned short AdresBit=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //вычисление количества запрашиваемых бит      unsigned short KolvoBit=((((unsigned short)PaketRX[4])<<8)|(PaketRX[5]));      //если неправильный адрес и количество      if((AdresBit+KolvoBit)>(ModBusMaxInBit) || KolvoBit>ModBusMaxInBitTX || KolvoBit==0)        {//конец        return;//Ошибка, повторный запрос не требуется        }      Prg2ModBusInBit();//Заполнение регистров Модбас (GlobalDate->ModBus)      //формирование пакета ответа      ModBusPUT(':');      //адрес      ModBusPUT(BCD[PaketRX[0]>>4]);//Передаем старший       ModBusPUT(BCD[PaketRX[0]&0x0F]);//передаем младший      LRCmodbus=0-PaketRX[0];//считаем LRC      //код команды          ModBusPUT(BCD[2>>4]);//Передаем старший       ModBusPUT(BCD[2&0x0F]);//передаем младший      LRCmodbus=LRCmodbus-2;//считаем LRC      //количества полных байт      ModBusPUT(BCD[((KolvoBit+7)>>3)>>4]);//Передаем старший       ModBusPUT(BCD[((KolvoBit+7)>>3)&0x0F]);//передаем младший      LRCmodbus=LRCmodbus-((KolvoBit+7)>>3);//считаем LRC      //копирование битов в пакет ответа      unsigned char TxByte=0;//текущий байт      unsigned char Bit=AdresBit&7;//указатель бит в ModBusOutBit[]      AdresBit=AdresBit>>3;//указатель байт ModBusOutBit[]      //копирование из регистра ModBusOutBit[] в пакет      int i=0;      while(!0)        {        if((ModBusInBit[AdresBit].byte)&(1<<Bit))//если текущий бит ModBusOutBit[] равен 1          {//устанавливаем бит в пакете          TxByte=TxByte|(1<<(i&7));          }        //инкрементруем указатели         Bit++;        if(Bit==8){Bit=0;AdresBit++;}        i++;        if((i&7)==0)          {          ModBusPUT(BCD[TxByte>>4]);//Передаем старший           ModBusPUT(BCD[TxByte&0x0F]);//передаем младший          LRCmodbus=LRCmodbus-TxByte;//считаем LRC          TxByte=0;          if(i==KolvoBit) break; else continue;          }        if(i==KolvoBit)           {          ModBusPUT(BCD[TxByte>>4]);//Передаем старший           ModBusPUT(BCD[TxByte&0x0F]);//передаем младший          LRCmodbus=LRCmodbus-TxByte;//считаем LRC          break;          }        }      ModBusPUT(BCD[LRCmodbus>>4]);      ModBusPUT(BCD[LRCmodbus&0x0F]);      ModBusPUT(ASCII_CR);      ModBusPUT(ASCII_LF);      //конец      return;//повторный запрос не требуется      }#endif#if ModBusUseFunc3!=0         //03 Read Holding Registers     if(PaketRX[1]==0x03)      {      //вычисление адреса запрашиваемых слов      unsigned short AdresWord=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //вычисление адреса количества запрашиваемых слов      unsigned short KolvoWord=((((unsigned short)PaketRX[4])<<8)|(PaketRX[5]));       //если неправильный адрес и количество      if(((AdresWord+KolvoWord)>ModBusMaxOutReg) || KolvoWord>ModBusMaxOutRegTX)        {//тады конец        return;//Ошибка, повторный запрос не требуется        }      Prg2ModBusOutReg();//Заполнение регистров Модбас (GlobalDate->ModBus)      //формирование пакета ответа      ModBusPUT(':');      //адрес      ModBusPUT(BCD[PaketRX[0]>>4]);//Передаем старший       ModBusPUT(BCD[PaketRX[0]&0x0F]);//передаем младший      LRCmodbus=0-PaketRX[0];//считаем LRC      //код команды      ModBusPUT(BCD[3>>4]);//Передаем старший       ModBusPUT(BCD[3&0x0F]);//передаем младший      LRCmodbus=LRCmodbus-3;//считаем LRC      //количества полных байт      ModBusPUT(BCD[(KolvoWord<<1)>>4]);//Передаем старший       ModBusPUT(BCD[(KolvoWord<<1)&0x0F]);//передаем младший      LRCmodbus=LRCmodbus-(KolvoWord<<1);//считаем LRC      //Копирование из регистра ModBusOutReg[] в пакет ответа      for(int i=0;i<KolvoWord;i++)        {        ModBusPUT(BCD[((ModBusOutReg[AdresWord+i])>>8)>>4]);//Передаем старший         ModBusPUT(BCD[((ModBusOutReg[AdresWord+i])>>8)&0x0F]);//передаем младший        LRCmodbus=LRCmodbus-((ModBusOutReg[AdresWord+i])>>8);//считаем LRC        ModBusPUT(BCD[(((ModBusOutReg[AdresWord+i])>>0)>>4)&0x0F]);//Передаем старший         ModBusPUT(BCD[(((ModBusOutReg[AdresWord+i])>>0)>>0)&0x0F]);//передаем младший        LRCmodbus=LRCmodbus-((ModBusOutReg[AdresWord+i])>>0);//считаем LRC        }      ModBusPUT(BCD[LRCmodbus>>4]);      ModBusPUT(BCD[LRCmodbus&0x0F]);      ModBusPUT(ASCII_CR);      ModBusPUT(ASCII_LF);      //конец      return;//повторный запрос не требуется      }#endif#if ModBusUseFunc4!=0         //04 Read Input Registers     if(PaketRX[1]==0x04)      {      //вычисление адреса запрашиваемых слов      unsigned short AdresWord=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //вычисление адреса количества запрашиваемых слов      unsigned short KolvoWord=((((unsigned short)PaketRX[4])<<8)|(PaketRX[5]));       //если неправильный адрес и количество      if(((AdresWord+KolvoWord)>ModBusMaxOutReg) || KolvoWord>ModBusMaxOutRegTX)        {//тады конец        return;//Ошибка, повторный запрос не требуется        }      Prg2ModBusInReg();//Заполнение регистров Модбас (GlobalDate->ModBus)      //формирование пакета ответа      ModBusPUT(':');      //адрес      ModBusPUT(BCD[PaketRX[0]>>4]);//Передаем старший       ModBusPUT(BCD[PaketRX[0]&0x0F]);//передаем младший      LRCmodbus=0-PaketRX[0];//считаем LRC      //код команды      ModBusPUT(BCD[4>>4]);//Передаем старший       ModBusPUT(BCD[4&0x0F]);//передаем младший      LRCmodbus=LRCmodbus-4;//считаем LRC      //количества полных байт      ModBusPUT(BCD[(KolvoWord<<1)>>4]);//Передаем старший       ModBusPUT(BCD[(KolvoWord<<1)&0x0F]);//передаем младший      LRCmodbus=LRCmodbus-(KolvoWord<<1);//считаем LRC      //Копирование из регистра ModBusOutReg[] в пакет ответа      for(int i=0;i<KolvoWord;i++)        {        ModBusPUT(BCD[((ModBusInReg[AdresWord+i])>>8)>>4]);//Передаем старший         ModBusPUT(BCD[((ModBusInReg[AdresWord+i])>>8)&0x0F]);//передаем младший        LRCmodbus=LRCmodbus-((ModBusInReg[AdresWord+i])>>8);//считаем LRC        ModBusPUT(BCD[(((ModBusInReg[AdresWord+i])>>0)>>4)&0x0F]);//Передаем старший         ModBusPUT(BCD[(((ModBusInReg[AdresWord+i])>>0)>>0)&0x0F]);//передаем младший        LRCmodbus=LRCmodbus-((ModBusInReg[AdresWord+i])>>0);//считаем LRC        }      ModBusPUT(BCD[LRCmodbus>>4]);      ModBusPUT(BCD[LRCmodbus&0x0F]);      ModBusPUT(ASCII_CR);      ModBusPUT(ASCII_LF);      //конец      return;//повторный запрос не требуется      }#endif#if ModBusUseFunc5!=0         //05 Force Single Coil     if(PaketRX[1]==0x05)      {      //вычисление адреса записываемого выхода      unsigned short AdresBit=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //проверка на допустимый адрес        if(AdresBit>=ModBusMaxOutBit)//если неправильный адрес        {//тады конец        return;//Ошибка, повторный запрос не требуется        }      //установка сброс бита      switch (((((unsigned short)PaketRX[4])<<8)|(PaketRX[5])))        {        case 0xFF00:        //установка бита        ModBusOutBit[(AdresBit>>3)].byte|=(1<<(AdresBit&7));        break;        case 0x0000:        //сброс бита        ModBusOutBit[(AdresBit>>3)].byte&=(~(1<<(AdresBit&7)));        break;        default:          { //конец          return;//Ошибка, повторный запрос не требуется          }         }                    //Ответ      ModBusPUT(':');      for(int i=0;i<7;i++)        {        ModBusPUT(BCD[PaketRX[i]>>4]);//Передаем старший         ModBusPUT(BCD[PaketRX[i]&0x0F]);//передаем младший        }      ModBusPUT(ASCII_CR);      ModBusPUT(ASCII_LF);               ModBus2PrgOutBit();//Считывание регистров Модбас (ModBus->GlobalDate)            //конец      return;//повторный запрос не требуется       }#endif#if ModBusUseFunc6!=0         //06 Preset Single Register     if(PaketRX[1]==0x06)      {      //вычисление адреса записываемого выхода      unsigned short AdresWord=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));            //проверка на допустимый адрес        if(AdresWord>=(ModBusMaxOutReg))//если неправильный адрес        {//тады конец        return;//Ошибка, повторный запрос не требуется        }      //запись слова      ModBusOutReg[AdresWord]=(((((unsigned short)PaketRX[4])<<8)|(PaketRX[5])));            //Ответ      ModBusPUT(':');      for(int i=0;i<7;i++)        {        ModBusPUT(BCD[PaketRX[i]>>4]);//Передаем старший         ModBusPUT(BCD[PaketRX[i]&0x0F]);//передаем младший        }      ModBusPUT(ASCII_CR);      ModBusPUT(ASCII_LF);            ModBus2PrgOutReg();//Считывание регистров Модбас (ModBus->GlobalDate)              //конец      return;//повторный запрос не требуется      }#endif#if ModBusUseFunc15!=0          //15 (0F Hex) Force Multiple Coils     if(PaketRX[1]==0x0F)      {      //вычисление адреса записываемых бит      unsigned short AdresBit=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //вычисление количества записываемых бит      unsigned short KolvoBit=(((((unsigned short)PaketRX[4])<<8)|(PaketRX[5])));      //если неправильный адрес и количество      if(((AdresBit+KolvoBit)>ModBusMaxOutBit) || (KolvoBit>ModBusMaxOutBitRX))        {//тады конец        return;//Ошибка, повторный запрос не требуется        }      //установка битов      unsigned char Bit=(AdresBit&7);//указатель бит в ModBusOutBit[]      AdresBit=AdresBit>>3;//указатель байт ModBusOutBit[]      //цикл по битам      for(int i=0;i<KolvoBit;i++)        {        if(PaketRX[7+(i>>3)]&(1<<(i&7)))//если текущий бит PaketRX равен 1          {//устанавливаем бит в ModBusOutBit[]          ModBusOutBit[AdresBit].byte=(ModBusOutBit[AdresBit].byte)|((unsigned char)(1<<Bit));          }        else          {//сбрасываем бит ModBusOutBit[]          ModBusOutBit[AdresBit].byte=(ModBusOutBit[AdresBit].byte)&((unsigned char)(~(1<<Bit)));          }        //инкрементруем указатели         Bit++;if(Bit==8){Bit=0;AdresBit++;}        }                       //вычисляем LRC пакета передачи и передаем      LRCmodbus=0;      ModBusPUT(':');      for(int i=0;i<6;i++)        {        ModBusPUT(BCD[PaketRX[i]>>4]);//Передаем старший         ModBusPUT(BCD[PaketRX[i]&0x0F]);//передаем младший        LRCmodbus=LRCmodbus-PaketRX[i];//считаем LRC        }      ModBusPUT(BCD[LRCmodbus>>4]);      ModBusPUT(BCD[LRCmodbus&0x0F]);      ModBusPUT(ASCII_CR);      ModBusPUT(ASCII_LF);            ModBus2PrgOutBit();//Считывание регистров Модбас (ModBus->GlobalDate)            //конец      return;//повторный запрос не требуется      }#endif#if ModBusUseFunc16!=0            //16 (10 Hex) Preset Multiple Regs     if(PaketRX[1]==0x10)      {      //вычисление адреса записываемых слов      unsigned short b=(((((unsigned short)PaketRX[2])<<8)|(PaketRX[3])));      //вычисление количества записываемых слов      unsigned short c=(((((unsigned short)PaketRX[4])<<8)|(PaketRX[5])));            //если неправильный адрес и количество      if(((b+c)>ModBusMaxOutReg) || c>ModBusMaxOutRegRX)        {        //тады конец        return;//Ошибка, повторный запрос не требуется        }      //Копирование из пакета в регистр ModBusOutReg[]      for(int i=0;i<c;i++)        {        ModBusOutReg[b+i]=(((unsigned short)PaketRX[7+(i<<1)])<<8)|(PaketRX[8+(i<<1)]);        }            //вычисляем LRC пакета передачи и передаем      LRCmodbus=0;      ModBusPUT(':');      for(int i=0;i<6;i++)        {        ModBusPUT(BCD[PaketRX[i]>>4]);//Передаем старший         ModBusPUT(BCD[PaketRX[i]&0x0F]);//передаем младший        LRCmodbus=LRCmodbus-PaketRX[i];//считаем LRC        }      ModBusPUT(BCD[LRCmodbus>>4]);      ModBusPUT(BCD[LRCmodbus&0x0F]);      ModBusPUT(ASCII_CR);      ModBusPUT(ASCII_LF);            ModBus2PrgOutReg();//Считывание регистров Модбас (ModBus->GlobalDate)            //конец      return;//повторный запрос не требуется       }#endif        }   //конец  return;//Ошибка, нераспознана команда, повторный запрос не требуется  }


ModBus2Prg.c
#define __MODBUS2PRG_C#include "modbus.h"//Заполнение регистров Модбас//перенос данных из программных переменных в регистры МодБасvoid Prg2ModBusOutBit(void)  {//заполнение регистров дискретных выходов    return;  }void Prg2ModBusInBit(void)  {//заполнение регистров дискретных входов  //ModBusInBit[0].bit0=1;    return;  }void Prg2ModBusOutReg(void)  {//заполнение регистров 4Х регистры для чтения/записи    return;  }void Prg2ModBusInReg(void)  {//заполнение регистов 3Х регистры для чтения    return;  }//Считывание регистров Модбас//Перенос данных из регистров МодБас в программные переменные void ModBus2PrgOutBit(void)  {//чтение регистров дискретных выходов    return;  }void ModBus2PrgOutReg(void)  {//чтение регистров 4Х регистры для чтения/записи    return;  }


В файле modbus.h содержатся требуемые объявления, опции компиляции и настроечные константы. Кратко опишем основные опции и настроечные параметры.
ModBusUseFunc1 ModBusUseFunc15 опция компиляции, определяющая использование функций протокола ModBus. Практические реализации устройств ModBus работают с ограниченным набором функций протокола, наиболее часто, функции 3,6 и 16. Нет необходимости включать в проект лишний код.
ModBusID, ModBusID_FF Адреса на шине ModBus. Данный реализация протокола поддерживает два адреса. Это может быть удобно для ввода в эксплуатацию устройств, адрес ModBusID является настраиваемым адресом устройства, а адрес ModBusID_FF адресом для индивидуальной настройки устройства.
ModBusMaxPause Пауза между символами, для определения начала пакета, задается в квантах ModBusSysTimer. Как правило квант ModBusSysTimer равен 1мС. Для большинства приложений соблюдение таймаутов описанных в стандарте протокола просто невозможно. Например, ModBus Master работающий на Win-машине никогда не сможет обеспечить требуемые протоколом таймауты. Поэтому задавать квант времени менее 1мС можно считать нецелесообразным. Практические наблюдения показывают, что величина ModBusMaxPause должна быть порядка 5-10мС.
ModBusMaxPauseResp Пауза между запросом Master и ответом Slave. Многие ModBus Master устройства имеют задержку переключения с передачи на прием, эту задержку можно скомпенсировать этой константой.
ModBusMaxInBit, ModBusMaxOutBit, ModBusMaxInReg, ModBusMaxOutReg Количество дискретных входов, выходов, регистров для чтения, регистров для чтения/записи. В программе резервируется память под регистры ModBus. Если определенный тип регистров не используется значение необходимо указать равное нулю.
ModBusMaxInBitTX, ModBusMaxOutBitTX, ModBusMaxInRegTX, ModBusMaxOutRegTX Максимальное количество дискретных входов, выходов, регистров для чтения, регистров для чтения/записи выходных регистров в передаваемом пакете. Эта настройка должна совпадать с соответствующей настройкой ModBus Master.

Для портирования библиотеки на любую платформу необходимо указать через макросы три функций.
ModBusSysTimer Системный таймер, переменная, инкрементирующаяся каждую миллисекунду в отдельном потоке выполнения. В качестве этой переменной может выступать uwTick, из библиотеки HAL STM32, или стандартная функция языка Си clock().
void ModBusPUT(unsigned char A) Запись байта в последовательный поток.
unsigned short ModBusGET(void) Чтение байта из последовательного потока. Если в последовательном потоке нет данных, то функция возвращает 0, если данные есть, то возвращаемое значение старший байт 0х01, младший байт прочитанные данные.

Для использования библиотеки необходимо заполнить тело функций Prg2ModBusOutBit(), Prg2ModBusInBit(), Prg2ModBusOutReg(), Prg2ModBusInReg(), отвечающие за копирование переменных пользователя в регистры ModBus. Так же, необходимо заполнить тело функций ModBus2PrgOutBit(), ModBus2PrgOutReg(), отвечающие за копирование регистров ModBus в переменные пользователя. В теле этих функций можно выполнить некоторые действия связанные с изменением регистров, например, осуществить проверку на допустимые значения.
Например:
void Prg2ModBusOutReg(void)  {//заполнение регистров, 4Х регистры для чтения/записи  ModBusOutReg[0]=A;  ModBusOutReg[1]=B;  ModBusOutReg[2]=C;  return;  }void ModBus2PrgOutReg(void)  { //чтение регистров 4Х, регистры для чтения/записи  if(ModBusOutReg[0] < MaxA) A= ModBusOutReg[0];  B=ModBusOutReg[1];  C=ModBusOutReg[2];  return;  }

Допускается не заполнять тело указанных функций, а работать с регистрами напрямую, при этом надо использовать опцию ModBusUseGlobal.
Для инициализации ModBus устройства необходимо вызвать функцию ModBusIni(). Функции ModBusRTU() или ModBusASCII() обеспечивающей работу устройства по протоколам RTU и ASCII соответственно. Их необходимо вызывать в главном цикле программы:
ModBusIni();while(!0)  {  if(ModBusTip==RTU) ModBusRTU(); else ModBusASCII();  }

Не стоит забывать, что перед инициализацией и вызовом функции, обеспечивающей работу ModBus устройства, необходимо позаботится об инициализации последовательного потока (UART). Программные решения, связанные с организацией поточного ввода/вывода, зависят от аппаратной платформы, и их рассмотрение выходит за рамки данной статьи.

Данная библиотека была протестирована с OPC сервером Kepware, панелями SIMATIC и Wientek, также другими ModBus Masterами, во множестве устройств на микроконтроллерах семейства PIC и STM32, и показала свою 142% работоспособность. Простота портирования данной библиотеки позволит легко адаптировать ее под другие типы 8-16-32разрядных микроконтроллеров.
Подробнее..

ModBus Slave RTUASCII без смс и регистрации. Версия 3

09.11.2020 14:21:24 | Автор: admin
image

Ранее на Хабре была опубликована статья ModBus Slave RTU/ASCII без смс и регистрации, посвященная реализации ModBus Slave RTU/ASCII устройств. В комментариях к статье было высказано множество замечаний, в том числе и несколько весьма дельных. В данной публикации приведена новая версия ModBus Slave RTU/ASCII с учетом этих замечаний.

Новые версии файлов:
modbus.c
modbus.h
ModBus2Prg.c

Настоечные константы полностью аналогичны предыдущей версии. Основные отличии от предыдущей версии:
  • Добавлен расчет CRC по таблице. Включается опцией ModBusUseTableCRC. Расчет CRC таблице не только более эффективен по скорости, но и гораздо компактнее по размеру, при условии использовании высокой оптимизации компилятора по скорости. При оптимизации компилятора по размеру, целесообразней не использовать табличный метод.
  • Введена обработка логических ошибок протокола Modbus. Включается опцией ModBusUseErrMes. Поддерживаются сообщения об ошибках ILLEGAL_FUNCTION, ILLEGAL_DATA_ADDRESS, ILLEGAL_DATA_VALUE, согласно спецификации протокола V1.1b3.
  • Добавлена функция протокола 22-запись регистра по маске. Включается опцией ModBusUseFunc22. Многие Modbus Master устройства опционально поддерживают эту функцию, ее использование позволяет оптимизировать трафик при использовании регистров чтения/записи (4Х) как битовых переменных.
  • Проведена оптимизация кода, исключены дублирующие действия, уменьшено использование статических переменных и т.п.
  • Исправлены ошибки в комментариях.


Мои коллеги, зная что я занимаюсь разработкой промышленных датчиков, просят меня поделится исходными кодами Modbus интерфейса. Я решил делится кодом в автоматическом режиме. Надеюсь данная библиотека будет полезна инженерам разрабатывающим ModBus устройства.
Подробнее..

Сервер Modbus TCP для Simatic S7-1200 S7-1500

06.01.2021 08:23:22 | Автор: admin

Первая спецификация протокола Modbus была опубликова в 1979 году. Протокол предназначен для опроса подчиненных устройств по принципу запрос-ответ. Modbus RTU (Remote Terminal Unit) работает по последовательному интерфейсу передачи данных (RS-232, RS-485, RS-422). Сегодня речь пойдет о немного измененном протоколе, Modbus TCP, работающий на прикладном уровне стека протоколов TCP/IP.

Для начала посмотрим, как настраивается (программируется, если быть точнее) серверная часть. Modbus TCP Server аналог Modbus RTU Slave, то есть, является подчиненным устройством. Это важно, не путайте. Сервер лишь отвечает на запросы, но не генерирует их.

В данном примере применяется CPU S7-1516 с версией прошивки 2.6. Серия S7-1200 программируется аналогично.

Для начала разместим в OB1 экземпляр функционального блока MB_SERVER (Instructions Communications Others MODBUS TCP).

Далее необходимо сделать три вещи. Во-первых, подать что-нибудь на вход MB_HOLD_REG. Этот входной пин экземпляра ФБ должен содержать область памяти, которая выделяется на регистры хранения (holding registers).

Небольшое отступление. В версиях библиотек Modbus TCP до 5.0 переменные дискретные входы (Discrete inputs), т.е. те двоичные переменные, которые можно только читать это непосредственно все BOOL'евые переменные из области процесса %I. Coils, катушки дискретные переменные, которые можно и читать, и записывать, это область %Q. Input Registers, входные регистры это слова данных из области %I, точнее %IW. Грубо говоря, все дискретные переменные протокола Modbus и аналоговые входа являются переменными областей памяти I или Q. Это дает возможность читать непосредственно значение входов, а так же записывать значения дискретных выходов напрямую. С моей точки зрения нелогично отдавать возможность управлять дискретными выходами какой-нибудь сторонней системе, пусть даже и теоритическую, поскольку в зависимости от построения прикладного ПО контроллера, на эти выхода будет приходить правильное с точки зрения системы значение. Для того, чтобы ограничить клиентам Modbus TCP возможность прямого обращения к выходам, в экземпляре функционального блока есть несколько переменных.

Нас интересуют все переменные, которые начинаются на IB и QB. Указав в качестве значений QB_Count, QB_Read_Count и IB_Count нули, вместо значения по умолчанию 65535, мы запрещаем полностью чтение/запись входов/выходов напрямую.

Для чтения/записи регистров хранения, в свою очередь, необходимо отдельно вручную задать область данных. Мой личный опыт показывает, что наиболее удобный способ это структура, объявленная в глобальном блоке данных со стандартным (а не оптимизированным) доступом. Я сейчас продемонстрирую, как надо, а под конец данной заметки мы пройдемся по граблям и посмотрим типичные ошибки, которые возникнут, если заполнить данное поле неправильно.

В версии библиотеки, начиная с 5.0 (требуется прошивка 2.5 для S7-1500 и 4.2 для S7-1200) можно иначе переназначать входные дискреты, катушки и прочие переменные модбас. Например завести все в битовые переменные глобального блока данных. Необходимо дополнительная конфигурация, которая описана в пункте Access to data areas in DBs instead of direct access to MODBUS addresses as of version V5.0 встроенной справки.

Итак, добавляем глобальный блок данных с неоптимизированным доступом в картинках.

Нажать Add new block

Выбрать Data block и дать ему осмысленное имя, далее нажать ОК

Вызвать свойства свежедобавленного блока данных

Найти в атрибутах блока галочку Оптимизированный доступ, снять ее, подтвердить снятие и нажать ОК

Открыть в редакторе блок данных и создать в нем отдельную структуру

Заполнить поля этой структуры. Имеет смысл сразу в коментариях делать пометки о номере(адресе) регистра хранения. Откомпилировать блок данных.

Подать созданную структуру на входной пин MB_HOLD_REG

Во-вторых, требуется создать и заполнить структуру типа TCON_IP_v4 или TCON_Configured. Данная структура содержит некоторые подробности для коммуникации контроллера. Лично я предпочитаю первый способ, он мне кажется более аскетичным, а кроме того он не требует загрузки Hardware, в отличии от второго. В связи с тем, что структура относится к настроечной части протокола Modbus, ее можно разместить в уже созданном блоке данных (хотя, никто не запрещает объявить ее, где угодно).

Добавление структуры типа TCON_IP_v4

Поле InterfaceID заполним чуть позже, а сейчас пройдемся по остальным полям.

ID внутренний идентификатор соединения. Допустимые значения от 1 до 4096. Каждое соединение (экземпляр блока MBSERVER, хотя на самом деле все немного сложнее). должно иметь свой уникальный идентификатор. Ставлю равным 1.

ConnectionType тип соединения. По умолчанию стоит 11 (0B в шестнадцатиричной системе): TCP. Его и оставляем.

ActiveEstablished оставляем false, в данном случае сервер не является инициатором связи, инициатором связи являютя клиенты.

RemoteAddress если оставить нули, то к серверу сможет подключиться любой клиент. Если задать удаленный IP-адрес конкретно, то к серверу может обратиться только один явно заданный клиент. Оставляем нули.

RemotePort оставляю ноль, не органичиваю и номер порта со стороны клиента

LocalPort номер TCP порта, по которому будет отвечать сервер. В соответствии со старой-доброй традицией (и RFC) протокол Modbus TCP работает на порту 502 (а игра Doom по порту 666, но это совсем другая история). Порт 502 я указываю явно.

В итоге получаем следующее:

Осталось задать лишь ID интерфейса. Это присвоение я делаю в программное коде, разместив network с присвоением (MOVE) до вызова блока Modbus. Идентификаторы интерфейсов уже созданы в Step 7 автоматически, необходимо лишь найти нужную переменную. В моем случае Modbus будет работать на интерфейсе X1. Его я и нахожу в списке переменных, выпадающем автоматически.

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

Можно так же просто указать значение 64 для переменной "ModbusData".CONNECT_Struct.InterfaceId

Далее подаем на вход CONNECT заполненную структуру и получаем следующую программу:

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

Компилируем программу, загружаем ее в контроллер и выходим Online:

Статус 7002 не означает ошибку, он говорит о том, что соединиение устанавливается. Обязательно почитайте описание возможных значений поля STATUS, пригодится. Перед тем, как начать читать/записывать данные при помощи стороннего Modbus-клиента, дадим переменным ненулевые значения (разумеется, мои любимые число зверя и три топора).

В качестве Modbus-клиента можно использовать любой проверенный софт. Главное правильно сформировать запрос со стороны клиента. В нашем случае объявлено всего 5 регистров хранения, и если запросить 10, то сервер Modbus вернет ошибку, и будет прав. Второй немаловажный момент не забывайте про порядок байт в слове: если little endian отображать, как big endian, или наоборот, то вместо разумных чисел на экране будет ерунда. На данном скриншоте клиент настроен на опрос 5 регистров хранения, представление данных, как float, настроено переворачивание байт в словах:

Чуть выше я говорил, что дискретные выхода контроллера (точнее, биты области %Q) это и есть койлы с точки зрения протокола Modbus, и что при настройках по умолчанию клиент получит возможность как читать сигналы напрямую, так и записывать их. Давайте в этом убедимся. Для начала на модуле дискретных выходов я объявляю переменную, для дальнейшего удобства:

Нулевой бит восьмого байта выходной области. Номер 64, если считать с нуля (8 * 8 + 0 = 64). Задам в контроллере значение истина и прочитаю в Modbus-клиенте:

Вижу значение истина (читаю один койл с начальным смещением 64). Изменю это значение на ложь со стороны modbus:

Значение, разумеется, так же изменилось и в Step 7, и в контроллере, и на выходе модуля (это одно и то же):

Как я уже говорил, нехорошо давать прямой доступ к управлению всяким сторонним системам. Для формирования выходных сигналов есть внутренняя логика программы, продуманные и прописанные алгоритмы управления, защит и блокировок. Поэтому снизим до нуля количество доступных к записи койлов (байт выходного простраства, если точнее):

После этого изменения в блоке данных (прямо в online, без перезаливки и перезагрузки контроллера) клиент протокола modbus в ответ на требование записи катушки показал табличку Illegal data address, а именно такое сообщение и вернул сервер. Дополнительную информацию читаем в справке: Restriction of read access to process images as of version V5.0.

Теперь давайте посмотрим, что происходит при некорректном назначении области памяти от регистров хранения. В первую очередь достаем встроенную справку Step 7 и читаем:

В качестве регистров хранения применять глобальный блок данных с оптимизированным доступом или битовую область. И вот тут очень интересно. Потому что справка в части MBHOLDREG parameter выглядит следующим образом:

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

Эксперимент 1. Регистры хранения это структура в блоке данных с оптимизированным доступом (почти, как сделано в этом примере). В этом случае получаем ошибку 8187 : The MBHOLD_REG parameter has an invalid pointer. Data area is too small.

Эксперимент 2. Массив переменных типа WORD, объявленный в оптимизированном блоке данных. Работает, со стороны клиента переменные меняются, ошибок нет.

Эксперимент 3. Меркерная область. Работает, с клиента удалось внести значения, ошибок нет.

С моей точки зрения, в документации недостаточно ясно. Должно быть написано используйте блок данных со стандартным доступом или битовую (меркерную) память, а не оптимизированным доступом. В случае оптимизированного доступа вполне подойдут массивы слов. И с моей точки зрения самым удобным способом является способ, описанный в изначальном примере. Эксперимент 2 в принципе тоже работоспособен (и тому есть объяснение), но с моей точки зрения неудобен для работы.

В следующий раз мы займемся клиентом Modbus TCP.

Подробнее..

Клиент Modbus TCP для Simatic S7-1200 S7-1500

07.01.2021 14:19:53 | Автор: admin

Продолжаем тему программирования протокола Modbus TCP на контроллерах Simatic S7-1500. В прошлый раз речь шла о серверной части, сегодня опишем клиентскую. Клиент Modbus TCP это узел, который генерирует запросы к серверу, т.е. запрашивает данные и передает уставки/команды. В терминологии Modbus RTU это мастер, ведущее устройство. В отличии от RTU, в протоколе TCP может быть несколько мастеров (правильно клиентов).

Программировать клиентскую сторону сложнее. Если для сервера достаточно одного вызова функционального блока и одного блока данных, то с клиентом не все так просто. Во-первых, клиент может обмениваться данными с несколькими серверами, что на практике и происходит. Во-вторых, к одному серверу может быть (и бывает) больше одного запроса прочитать входные регистры, прочитать регистры хранения, записать команды в выходные койлы, и все это с одним абонентским устройством. В-третьих, не забываем про остроконечный и тупоконечный порядок байт в словах разных аппаратных платформ, при несовпадении байты необходимо переворачивать самостоятельно.

По этой причине имеет смысл программировать клиента на языке SCL (ST в терминологии МЭК 61131-3) и завернуть всю обработку в функциональный блок. Для большей реалистичности в данном примере контроллер будет общаться с двумя серверами Modbus TCP с несколькими запросами к каждому.

В первую очередь создадим функциональный блок ModbusClient на языке SCL и добавим вызов его экземпляра в OB1.

Далее в области STAT переменных функционального блока необходимо прописать две структуры TCON_IP_v4. Зачем две? Затем, что у нас два соединения с двумя разными серверами. Фактически у нас два разных соединения (connection) и каждое необходимо описать. Как я говорил ранее, возможно применить и конфигурируемые соединения, но в данном примере они не используются.

Объявлено две структуры для связи с двумя серверами

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

Первое поле, InterfaceId. Идентификатор интерфейса (или сетевой карты) нашего контроллера. Клиент Modbus работает на интерфейсе 1 контроллера, смотрим его ID в конфигурации устройства.

Его ID равен 64. Обращаю внимание, что нужен идентификатор именно интерфейса, а не его портов.

Следующее поле структуры, ID. Это идентификатор соединения. Не путать с идентификатором интерфейса. Не путать с номером модбас-устройства. Это некий внутренний логический номер коннекшена, который программист назначает самостоятельно в диапазоне от 1 до 4096. У каждого коннекшена должен быть свой уникальный идентификатор. Ответственность за корректное присвоение целиком на ваших плечах. Назначаем ID = 1 и едем дальше.

Далее идет тип соединения, ConnectionType TCP или UDP. По умолчанию значение этого поля 0x0B в hex или 11 в dec. Оставляем по умолчанию, TCP.

Флаг ActiveEstablished. Выставляем его в истину. В случае клиента именно наша сторона должна инициировать соединение.

RemoteAddress. Тут пишем IP-адрес нашего первого сервера. Пусть будет 192.168.43.100.

RemotePort. Номер порта, по которому сервер Modbus TCP будет отвечать на наши запросы. По умолчанию все сервера этого протокола должны слушать порт 502.

LocalPort. Оставляем равным нулю.

В итоге, описание соединения с первым сервером выглядит следующим образом.

Описание соединения с первым сервером

Вторая структура заполняется аналогично. Разумеется, пишем другой ID и другой IP адрес. В итоге получаем.

Сделаем первые робкие шаги и попробуем прочитать один регистр хранения с одного сервера. Для начала надо перетащить ФБ MB_CLIENT из библиотеки в программу.

После перетаскивания появится диалогое окно о создании экземпляра. Выбираем мульти-экземпляр и немного корректируем имя.

Выбираем мультиэкзепляр

Промежуточный итог

Приведем вызов в человеческий вид

Кратко пройдемся по параметрам этого вызова. Подробное описание в нашей горяч0 любимой документации, которую мало кто, почему-то, читает.

REQ активирует выполнение опроса. Пока REQ = TRUE, клиент проводит чтение данных с сервера или запись данных на сервер.

DISCONNECT разорвать соединение

MB_MODE режим работы клиента. В совокупности со входом MB_DATA_ADDR оказывает влияние на используемую функций Modbus TCP. Возможные значения описаны в документации. Для чтения одного или нескольких регистров хранения значение MODE должно быть равно 0.

MB_DATA_ADDR указывает адрес в адресном пространстве протокола Modbus TCP. Значение нашего примера 40001 первый регистр хранения

MB_DATA_LEN количество читаемых или записываемых величин. В нашем случае единица. В итоге все три указанных выше параметра означают читать один регистр хранения начиная с адреса 40001

MB_DATA_PTR переменная или структура данных, куда мы записываем прочитанное значение. Переменная может быть в любом блоке данных, я объявил локальную статическую переменную SingleHR типа INT, размер которой равен 2 байтам и совпадает с размером одного регистра хранения Modbus. При несовпадении размерности читаемой области данных с локальным хранилищем вызов функционального блока завершится ошибкой.

CONNECT уже созданная нами структура типа TCON_IP_V4

Остается только запустить на ноутбуке сервер Modbus, скомпилировать и загрузить программу контроллера, и не получить ничего. Сервер не отвечает. Ответов нет. Вообще ничего нет. Ничего. По буквам Николай, Илья, Харитон ( Остапа понесло ). Для того, чтобы уточнить ошибку, необходимо доработать программу следующим образом.

Дело в том, что флаги успешного (DONE) или неуспешного (ERROR) вызова блока живут всего один цикл сканирования программы. Естественно, невозможно заметить настолько быстрое изменение. Поэтому по флагу ошибки я копирую статус вызова в отдельную переменную. А по флагу успешного выполнения обнуляю статус.

Кроме того, добавлены флаги управления запросом и соединением.

Немного прокоментирую ошибку, которая возникла у меня. В моем случае код ошибки был 80C6. В описании на блок MB_CLIENT этой ошибки нет, поэтому я вбил код ошибки в поиск справочной системы и нашел ссылку на функцию TCON (так же при неоднозначных ошибках можно искать и среди TSEND, TRECEIVE и прочих похожих блоках). Описание: The connection partner cannot be reached (network error). Ошибки сети. Ответ был очень прост и заключался в том, что программа-иммитатор Modbus не была прописана в разрешениях встроенного в Windows Firewall. Точнее, разрешение на ее работу было настроено только на частные сети, а интерфейс программатора был назначен в качестве публичной сети. Это лишний раз подчеркивает, что техника виновата в последнюю очередь, а чаще всего ошибку надо искать в радиусе закругления рук и в соответствии этого радиуса ГОСТам. После изменения настроек брэндмауэра ОС обмен заработал.

Усложним теперь задачу самую малость, и попробуем считать с сервера одну вещественную переменную. Одна вещественная переменная (REAL) это 4 байта. Или 2 регистра. Итого, в вызове я увеличил количество читаемых регистров до 2, и изменил указатель на прочитанные данные. Этот указатель все еще весьма прост это внутренняя статическая переменная типа REAL (дальше будет интереснее).

Хотелось бы обратить внимание на одну важную деталь. Если прогружать измененное прикладное ПО на горячую, с переинициализацией переменных нашего функционального блока ModbusClient в то время, когда контроллер ведет опрос сервера Modbus, то обмен может прекратиться, и на выходе блока будет стоять статус 80A3. Связано это, разумеется, со вмешательством во внутренние структуры обмена (из-за переинициализации всего блока). В моем случае это приводило к полной невозможности коммуникаций до рестарта контроллера переключателем старт/стоп. Я намеренно изменил сейчас функциональный блок (добавил еще одну переменную), чтобы продемонстрировать эту ошибку:

После стоп/старта контроллера и поднятия флага Srv1Req обмен успешно возобновляется. Чтобы не допустить такого зависания обмена (как ни крути, это частный случай) необходимо поднимать флаг Srv1Disconnect, проводить изменения в переменных функционального блока (имеются в виду именно переменные, т.е. интерфейсная часть блока, а не сам программный код), выполнять загрузку с переинициализацией, а потом вручную возобновлять обмен. Помните же, что флаги REQ и DISCONNECT у нас подключены к переменным, и этими переменными можно управлять, как вручную, так и посредством программного кода.

Пока мы не продвинулись дальше, хочется продемонстрировать описанный выше случай с разноконечностью (little-endian и big-endian) данных. В моем примере сервер modbus держит в двух регистрах хранения вещественную переменную со значением 0.666. Наш клиент Modbus вместо этого числа читает 1.663175E+38, что сильно отличается от нужного (увы, вне зависимости от того, покупаем мы или продаем). Связано это, конечно же, с порядком байт в словах и порядком самых слов в двойном слове. Пробуем разрешить ситуацию. Доработаем программный код следующим образом.

Инструкция SWAP меняет порядок байт (конкретно тут в двойном слове). Но в данном случае она не помогает, на выходе (в переменной Data.Test) все еще находится неправильное значение. Скорее всего, это означает, что сервер отдает регистры в неправильном порядке, а байты в правильном, то есть информация перемешалась. Радует, что байта всего 4 и составить их в нужном порядке дело техники.

Итак, снова модифицируем запрос. Складываем результат чтения двух регистров в массив из 4 байт и объявляем еще один массив из 4 байт, которым и будем манипулировать.

Функция Deserialize складывает массив из байт в какое-либо конкретное значение, в данном случае в вещественную переменную. Потребовалась всего одна итерация, чтобы получить корректное значение переменной с сервера Modbus.

Перед тем, как вернуться к штатной рутинной работе, необходимо рассказать про одну очень частую ошибку, возникающую при обмене по протоколу Modbus TCP, а именно указание или неуказание адреса (номера) подчиненного устройства (сервера). В протоколе Modbus RTU все слейвы имеют свой уникальный адрес в сети. Мастер, формируя запрос, указывает адрес слейва, однобайтовое поле в заголовке пакета. Unit ID, Device ID, адрес неважно, как называется, смысл один. В протоколе Modbus TCP адресом абонентского устройства является его IP-адрес. Тем не менее, поле Device ID в заголовке сохранилось. И это часто вносит путаницу, непонимание и ошибки. Дело в том, что в соответствии со спецификациями обычный сервер Modbus TCP должен игнорировать поле ID в запросе к нему. Unit ID учитывается лишь для устройств, преобразующих Modbus RTU в Modbus TCP (гейты, шлюзы, конвертеры протоколов и так далее). На практике же многие сервера Modbus проверяют и однобайтовый адрес Unit ID. При несовпадении своего адреса и адреса в запросе, сервер в этом случае чаще всего не отправляют никакую ответную телеграмму, и клиент возвращает ошибку опроса. Если на практике вы столкнетесь с таким странным поведением, то откройте экземпляр функционалного блока клиента Modbus и поищите в его статических переменных байтовую величину MB_Unit_ID. Это и есть адрес подчиненного устройства, т.е. сервера Modbus. По умолчанию его значение равно 0xFF или 255. Нормальные сервера его игнорируют, достаточно уже самого факта установления соединения по протоколу TCP/IP. Если же попался ненормальный, то поставьте тут вручную Unit ID вашего устройства. Связь должна установиться.

Кроме того, в статических переменных есть ряд полезных настроек, таких, как таймауты, количество повторных попыток запроса и т.д. Изучить эти переменные можно во встроенной справке.

Закончив описание подводных камней, вернемя к изначальной задаче. Будем считывать с первого сервера 3 вещественных переменных (6 регистров), начиная с 40001, и записывать одну (начальный адрес 40011). Предполагаем, что порядок слов и байт правильный. Шесть регистров (шесть слов данных) и три вещественных переменных. Можно, конечно, просто в лоб читать информацию в локальный массив байт, а потом средствами дополнительной обработки представлять их в виде трех вещественных величин (тем же Deserialize, например), но не стоит создавать себе лишнюю работу. Гораздо удобнее будет сразу разложить читаемую информацию в собственную структуру. В блоке данных Data я создаю структуру, состоящую из трех полей типа REAL.

Обращаю внимание, что блок данных Data должен быть стандартным или неоптимизированным, в противном случае вы будете получать ошибку опроса, к примеру 818B.

Разумеется, содержание этой структуры полностью зависит от того, в каком порядке и в какой форме отдаются данные от сервера.

Программа обретает следующий вид, и данные успешно читаются.

0.5, 0.7, 0.33 (с) ВИА Несчастный случай

Теперь необходимо добавить еще один запрос на запись одной вещественной уставки (двух регистров). Необходимо понимать, что для обмена с одним сервером по одному соединению у нас выделен один экземпляр функционального программного блока коммуникаций. И если выполнять второй запрос, не дожидаясь результатов первого не будет работать ни один. Необходимо работать последовательно вначале выполнить один запрос, а после его завершения формировать следующий. Для этого достаточно добавить в наш программный блок переменную номер шага (или номер запроса, если вам так удобнее).

В итоге получаем вот такую программу.

Переменная, на основании который выбирается опрос, называется у меня Server1Query (будет еще Server2Query). Выбор запроса выполнется в операторе CASE. Номер запроса меняется на следующий лишь в случае успешного или неуспешего выполнения текущего опроса.

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

Со вторым сервером у нас настроено отдельное соединение, есть отдельный экземпляр функционального блока modbus. Его мы можем вызывать одновременно с коммуникациями первого сервера, поэтому в общей программе есть два оператора CASE, которые работают независимо друг от друга.

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

Тем не менее, описание работы с протоколом Modbus TCP на этом я заканчиваю. В следующий раз посмотрим на программирование протокола Modbus RTU.

Подробнее..

Программирование Modbus RTU Master на примере Simatic S7-1200 и ПЧ Sinamics V20

08.01.2021 22:18:59 | Автор: admin

Давно хотел рассказать про тонкости программирования обмена по протоколу Modbus RTU в случае, когда контроллер (в нашем случае S7-1214) выступает RTU Master'ом. Недавно меня попросили помочь с обменом между ПЛК и частотным преобразователем Sinamics V20, ну и почему бы не написать заодно заметку, постаравшись приблизить решение задачи к боевым условиям.

Собственно говоря, сами немцы эту тему давно осветили:

SINAMICS V: Speed Control of a V20 with S7-1200 (TIA Portal) via USS protocol/MODBUS RTU with HMI

https://support.industry.siemens.com/cs/ru/ru/view/63696870/en

Смотрите этот пример, он сделан очень толково, с визуализацией,диалогами и квестамии возможностью расширить прикладную программу до опроса множества ПЧ V20 по нескольким интерфейсам (S7-1200 позволяет установить в свою корзину до 4 портов RS-485/422). Пример сделан очень хорошо и очень педантично. Вопросов коммуникаций по протоколу Modbus TCP я уже касался ранее, они есть на хабре.

Поэтому некоторые нюансы я повторно объяснять не буду, просто сразу напишу, как стоит делать правильно и удобно с моей точки зрения конкретно в случае опроса V20. Первоначальная настройка преобразователя частоты описана в документации, в том числе и в сопутствующей документации к вышеуказанному примеру. Вынесем лишь важные для нас пункты в качестве вводных.

Адрес подчиненного устройства модбас в сети: 1

Параметры связи: 9600 8-Even-1

Регистры хранения подчиненного устройства для чтения:

40110 ZSW Слово состояния

40111 HIW Текущая скорость

Регистры хранения для записи:

40100 STW Слово управления

40101 HSW Задание скорости

Параметр частотника Telegram off time (ms) P2014[0] рекомендую оставить по умолчанию, равным в 2000 мс (2 секунды), хоть пример и рекомендует снизить эту величину до 130 мс. Конкретно к протоколу Modbus это замечание не относится, разумеется, просто у меня при таймауте в 130 мс, ПЧ терял связь и выдавал ошибку Fault 72.

С частотником разобрались. Теперь о моей конфигурации ПЛК. Это S7-1214 с коммуникационным модулем 1241 под RS-485/422:

Среда программирования Step 7 V15.1 Update 4, версия прошивки CPU 4.3.

Итак, приступим. Для опроса подчиненных устройств с контроллера Simatic нам необходимо применить два функциональных блока: Modbus_Comm_Load (единовременно, только для конфигурации коммуникационного процессора) и Modbus_Master (циклически для чтения и/или записи регистров/катушек). Поэтому в программе экземпляр FB Modbus_Comm_Load у нас будет встречаться только один раз, а экземпляр Modbus_Master несколько раз, но с разными входными параметрами, в зависимости от адреса подчиненного устройства, типа читаемых данных и их количества, а так же направления передачи данных (чтение или запись). Обращаю ваше внимание, что для одного коммуникационного процессора (а их в системе может быть очень много) у вас не может быть больше одного экземпляра каждого блока данных.

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

Объявляем следующие входные переменные

Init (Bool) инициализация коммуникационного процессора, ее необходимо выполнить один раз перед началом обмена

PORT (PORT) аппаратный идентификтор коммуникационного процессора

BAUD (UDINT) скорость обмена по порту

STOP_BITS (USINT) количество стоповых бит кадра

PARITY (USINT) четность, где 0 нет четности, 1 odd, нечет, 2 even, чет

В статической области переменных так же прописываем переменную с именем Step и типом UInt, она отвечает за номер опроса или шаг работы алгоритма

Так же в статической области объявляем экземпляры ФБ для работы по протоколу Modbus RTU

Строки программы, отвечающие за инициализацию обмена.

По флагу инициализации выставляем номер шага 1. Следующие строчки очень важны для работы

#instModbusCommLoad.MODE := 4; //для линии RS-485 должна быть 4!

#instModbusCommLoad.STOPBITS := #STOP_BITS;

Тут мы задаем значения статических переменных экземпляра ФБ Modbus_Comm_Load, которые отвечают за физику передачи. Не понимаю, почему немцы поместили эти важные конфигурационные параметры в статическую область, а не в область входов. Дело в том, что они (переменные) все описаны во встроенной справке. Беда лишь в том, что большинстволенивых жопновичков до этого пункта справку не читает, а потом тратят несколько часов, а то и дней, пока не найдут ответ. А справка-то, вот она:

Переменная MODE отвечает за режим, в котором будет работать коммуникационный процессор. Как видно из справки, для RS-485 надо явно выставить 4. Значение по умолчанию 0, от этого большинство ошибок у программистов.

STOP_BITS количество стоповых бит.

Далее следует вызов блока настройки коммуникационного интерфейса Modbus_Comm_Load. Про параметр PORT (аппаратный идентификатор) будет рассказано чуть ниже. Параметры BAUD и PARITY скорость и четность приходят на вход внешнего блока данных, куда мы и завернули весь обмен. А вот параметр MB_DB интересен. На этот вход надо подать структуру типа P2P_MB_BASE, которая находится в области статических переменных экземпляра функционального блока Modbus_Master. Этот экземпляр в нашем большом функциональном блоке уже объявлен, привожу скриншот:

Следующая часть: функциональный блок приступает к циклическому обмену.

Я сразу заворачиваю обмен в CASE, чтобы не переписывать код в дальнейшем, но пока мы ограничимся чтением слова состояния и скорости ПЧ, т.е. прочитаем два регистра хранения.

Давайте посмотрим на вызов блока Modbus Master повнимательнее:

#instModbusMaster(REQ := TRUE,MB_ADDR := 1,MODE := 0,DATA_ADDR := 40110,DATA_LEN := 2,DATA_PTR := #ZSWHIW);

Входной параметр REQ включить опрос. Пока на входе TRUE, он выполняется, если FALSE не выполняется. Нет необходимости подавать положительный фронт на этот вход самостоятельно (в отличии от работы Modbus RTU в системах S7-300/S7-400), поэтому я просто даю TRUE константой

MB_ADDR адрес подчиненного устройства Modbus RTU. В моем случае адрес частотника = 1.

MODE направление передачи данных, 0 чтение, 1 запись

DATA_ADDR адрес интересуемых нас данных. В моем случае необходимо прочитать два регистра хранения (поэтому первая цифра4), начиная со 110го. В протоколе Modbus (что RTU, что TCP) очень часто возникает путаница в понятиях адрес и номер. И очень часто производитель оборудования эту путаницу добавляет в свою систему. Вот смотрите. Мы должны прочитать 2 регистра, начиная с адреса 40110. Для чтения регистров хранения в протоколе Modbus используется функция с номером 3. Именно 3 будет передаваться в телеграмме Modbus. А в качестве адреса в телеграмме будет передаваться не 40110, а 109. Связано это с тем, что код функции уже содержит описание области данных. А в самой телеграмме мы передаем не адрес, а номер требуемого регистра или катушки. И эта нумерация идет не с единицы, а с нуля. Сейчас я работаю именно с адресами и режимом (чтении или запись), поэтому мне достаточно указать то, что я нашел в документации. Если же в вашем устройстве будет указано входной регистр номер 0 содержит текущий статус устройства, то вам на вход DATA_ADDR необходимо будет подать 30001. Так же имейте в виду, что из-за частой путаницы с номерами и адресами, иногда эта адресация съезжает на единицу, поэтому не бойтесь экспериментировать. Если вместо полезных данных по запросу 16ого регистра вам прилетает полная чехарда, не имеющая ничего общего с документацией, прочитайте 15ый регистр. Не помогло? Опрашивайте 17ый. Более подробно с материалом необходимо ознакомиться опять же во встроенной справке.

DATA_LEN количество читаемых регистров, их 2

DATA_PTR указатель на область памяти, куда необходимо положить результат чтения регистров. Собственно, это те данные, которые мы прочитали и необходимо подсказать функциональному блоку, куда эти данные надо записать. С моей точки зрения самый удобный способ в этом случае это объявить в области STAT неименованную структуру должного размера. Поля структуры мы объявляем, в зависимости от самих читаемых данных, ведь это могут быть и наборы бит, и вещественные числа (расположенные в двух соседних регистрах). И нам еще повезет, если порядок байт в слове и слов в двойных словах контроллера и подчиненного устройства совпадут, иначе нам еще потребуется осуществить сдвиги байт/слов.

В данном случае я счел уместным объявить структуру из двух слов и скормить ее на вход FB:

, где

ZSW слово состояния (так оно называется в документации на ПЧ)

HIW скорость вращения двигателя

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

В случае успешного чтения необходимо полученные сырые данные как-то разобрать или переложить в другую область, и перейти к следующему опросу (у нас пока только один опрос, так что мы остаемся на шаге 1). При ошибке чтения данных минимальный разумный ход выставить где-нибудь флаг недостоверности и перейти к другому опросу.

Пока оставляем прием данных без обработки, компилируем и грузим программу, смотрим на результат. Кстати, обращаю еще внимание на один факт. Поскольку мы работаем, завернув системные вызовы в свой функциональный блок, то любое изменение своего ФБ с последующей загрузкой ПЛК, будет нарушать обмен в связи с переинициализацией экземпляра нашего ФБ. Например, будет уходить в ноль значение шага обмена. Да и внутренние статические переменные коммуникационных вызовов тоже пострадают. Самый простой способ стоп и старт контроллера. В боевом проекте это опасно, поэтому там на вход Init я бы подал еще одну переменную и поднимал ее самостоятельно после изменений в коммуникациях. Пока же боремся с остановом обмена простым стоп-стартом ПЛК.

Добавляем вызов нашего функционального блока в OB1 и грузим CPU

Переменная FirstScan имеет значение истина при первом цикле выполнения программы OB. Она назначается операционной системой ПЛК автоматически, ее применение настраивается в свойствах CPU.

Port. Это значение смотрим в проекте Step 7, аппаратная конфигурация:

Остальные параметры касаются скорости, четности и количества стоповых бит. Загружаем контроллер и смотрим, что нам приходит в ответ на единственный циклический запрос, открыв экземпляр нашего ФБ

В слове состояния что-то есть, скорость равна нулю. Открываем документацию и смотрим состав слова состояния ZSW:

Low enabled в примечаниях означает инверсию. К примеру, бит 15, перегрузка частотника, возникает, когда этот бит равен 0, а в нормальном состоянии приходит значение 1. Посмотрим на это слово состояния в watch table и посмотрим, какие его биты выставлены, а какие нет, оценим общее состояние ПЧ:

Тут нам везет, порядок байт в словах совпадают. Если вкратце, то видно, что ПЧ не готов, не включен, и сейчас активен сигнал аварии (fault, бит 3).

Далее я попытался разложить слово состояния в биты состояния, заменив WORD на структуру из бит, но что-то явно пошло не так.

Если посмотреть внимательно, то в таком представлении нулевой и первый байты явно не на своих местах. В общем, вопрос порядка следования информации в зависимости от того или иного представления он всегда важный и требует вдумчивости. Получил на этом этапе облом, я решаю вернуться к хранению внутри нашего ФБ только сырых данных, а удобочитаемый для человека формат представления информации перенести куда-нибудь во внешний глобальный блок. Добавляю в проект блок данных DataV20:

После чего задумываюсь, убираю из имен переменных окончание Inv и дописываю функциональный блок:

Теперь в глобалном блоке данных у нас находятся статусные биты преобразователя частоты без какой-либо инверсии:

Думаю, что сразу в блок данных надо вписать переменную типа Real, которая будет содержать текущие обороты двигателя. Текущие обороты приходят от ПЧ в виде определенного численного кода, и мы вольны трактовать этот код, как нам удобнее. Допустим, что хочу трактовать этот код, как Герцы, поданные на двигатель.

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

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

Знаете, мне, откровенно говоря, лень писать все эти переменные в глобальном интерфейсном блоке данных. В моей практике управление простыми технологическими процессами с применением преобразователей частоты ограничивалось командами включить, квитировать аварию и дать задание скорости. Поэтому, пойдем на поводу моей лени и в блоке данных V20Data пропишем всего лишь бит включить, бит квитировать и задание частоты в формате Real.

Изменю алгоритм на шаге 1, при успешном или неуспешном завершении опроса сделаю переход на шаг 2.

Добавим еще локальную структуру ФБ, которая содержит слово управления и слово задания скорости:

Дорабатываю программу обмена. Не забываем, что при изменении переменных функционального блока, после загрузки изменений в ПЛК происходит его переинициализация, посему надо выполнять стоп/старт CPU.

Параметры функционального блока модбас в данном случае отличаются от первого вызова. Разумеется, у нас тут другой адрес регистра. А так же отличается режим (MODE), он равен 1, так как в данном случае данные не читаются с частотника, а записываются в него. Разумеется, указатель на область данных так же другой.

Обратите внимание, что некоторые биты слова управления я принудительно выставляю в истину, другие в ложь. Всего два бита управления (включить и квитировать) доступны для внешней программы. Необходимое значение бит управления я вычитал в документации примера. Разумеется, это указано и в документации на сам преобразователь частоты. Изучая исходный пример, я обратил внимание, что если частотнику отдавать пустое (все биты выставлены в ноль) слово управления, то это подчиненное устройство модбас возвращает ошибку Invalid data. Однако, в этом примере я пробовал слать полностью пустое слово управления, и V20 принимал его. Однако, некоторые биты управления, все равно, должны быть установлены. К примеру, если снять бит Control by PLC, то запускаться ПЧ не будет. RTFM, как говорится!

Теперь пора перейти к регистру, который отвечает за задание скорости (ну и сразу же к регистру, который отображает текущую скорость). Из исходного примера я понял, что этот регистр меняет свое значение в пределах от 0 до 16384. Это же мельком нашел и в документации. Пока не будем делать никаких переводов величин, и зададим ПЧ максимальную скорость жестко прямо в программном коде.

Откроем наш блок данных DataV20 и выставим команду пуск:

V20 запустился и работает, судя по индикации своего экранчика, на максимальной скорости, т.е. на 50 Гц. Давайте посмотрим еще сырые данные его скорости, которые приходит по modbus.

Значит, пришло время доработать шаг 1 обмена (перевести коды скорости в герцы), ну и шаг 2 в части обратного преобразования, герцы в численное значение задания скорости. Математика самая простая, без проверок на достоверность и выход за диапазон, хотя все это не помешает.

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

Даем задание 25 Гц, даем пуск и наблюдаем за появлением сигнала Running и текущей скоростью.

Все регистры, которые можно считать с V20, описаны в документе по ссылке

https://support.industry.siemens.com/cs/attachments/109768394/V20opinstr0419en-US.pdf?download=true

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

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

В принципе, мотор маленький, крутится без нагрузки, поэтому значения похожи на достоверные. Тем не менее, задача стоит в демонстрации считывания данных, поэтому будем считать наличие хоть каких-либо цифры за огромный технологический прорыв. Итак, вы уже заметили, что я добавил читаемые параметры в блок данных DataV20. Дополнительно был доработан функциональный блок коммуникаций:

Читаются (mode = 0) четыре регистра хранения по адресу 40025. Результат помещается во внутренний статический массив [0..4] of WORD. Далее эти слова переводятся в формат Real и помещаются во внешний блок данных в результате несложных преобразований.

Ну, и напоследок остается проанализировать качество связи. Ведь не зря же на каждом шаге после выполнения ФБ Modbus_Master смотрю его флаги DONE или Error (кстати, эти флаги имеют значение истина только на протяжении одного вызова после успешного или неуспешного выполнения запросы, в остальное время ложь). Для этого я объявил массив из булевых переменных

Массив размерностью три, по количеству запросов Modbus. Соответственно, если на шине будет 10 частотников, по три запроса к каждому, то размерность этого массива, как и количество шагов алгоритма, будет равно 30. Ну, и в конце каждого опроса, при анализе флагов, наконец, прописываем присвоение флагам значения.

Будем считать, что частотник стабильно обменивается информацией с ПЛК, когда все три запроса к нему выполнены успешно. Поэтому самая последняя строчка нашего функционального блока будет такой (предварительно добавим булевую переменную Connected в блоке данных DataV20):

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru