Рассказ о том, как мы прикрутили к Modbus быстрое сканирование шины, события и разрешение коллизий адресов. Из статьи вы узнаете, зачем это нам было нужно, с какими проблемами мы столкнулись в процессе и что у нас в итоге получилось.  Периферийные устройства Wiren Board подключаются к шине RS-485 и работают по протоколу Modbus RTU — это старый промышленный протокол передачи данных, поддержка которого есть почти во всех контроллерах или программном обеспечении верхнего уровня. Но в Modbus RTU есть недостатки, которые вытекают из его архитектуры: проблема коллизий адресов — если на шине два устройства с одинаковым адресом, то надо физически отключить одно из них от шины и поменять второму адрес на свободный; долгое сканирование шины — надо перебирать все 247 адресов на всех возможных параметрах соединения; долгий период опроса одного регистра в устройстве — если на шине много устройств и в каждом по паре сотен регистров, то в зависимости от скорости и расположения регистров, пауза между опросами одного и того же регистра может достигать нескольких секунд. Это в конечном счёте приводит к медленной реакции системы. Первые две проблемы проявлялись только на этапе пусконаладки и не влияли на конечного пользователя, а вот долгий период опроса сказывался на скорости реакции системы. Поэтому интеграторам приходилось тонко настраивать систему: повышать скорость опроса, отключать опрос ненужных регистров, менять приоритеты опроса в нашем драйвере, разбивать одну шину на несколько. Но на что нужно быстро реагировать в системах диспетчеризации и автоматизации инженерных систем? В основном, на действия пользователя. Например, нажал пользователь на выключатель и свет сразу включился. Здесь очень важно максимальное время между действием и реакцией на него — если система обычно реагирует за 10 мс, но иногда за 10 с, то пользователь это заметит и начнет жаловаться. А раз есть понятные проблемы — можно попытаться их решить. Первое, что пришло в голову — сменить интерфейс и протокол устройств, например, делать устройства с CAN-шиной. Но этот вариант нам не подходил: Мы хорошо умеем делать устройства с Modbus RTU и RS-485 — под это заточены наши процессы в разработке и производстве. Наши клиенты часто используют устройства Wiren Board со своими контроллерами и поддержка Modbus RTU есть почти везде. К нашему контроллеру часто подключают устройства сторонних производителей, и выбор устройств с RS-485 и Modbus RTU кратно шире, чем с тем же CAN. Наверное, можно было одновременно выпускать две линейки устройств с разными протоколами под разные сегменты рынка, но это очень дорого в разработке, производстве и поддержке. Поэтому нам было важно сохранить наш основной протокол Modbus RTU, а значит единственное решение — разработка расширения протокола, используя заложенные разработчиками протокола Modbus возможности. Итак, требования к решению: программное, без модификации схемотехники — пользователи недавно выпущенных устройств смогут получить доступ к расширению через обновление прошивки; совместимое с классическим Modbus RTU — наши устройства должны оставаться Modbus RTU устройствами и уметь работать на одной шине с Modbus-устройствами других вендоров, а также с контроллерами, которые о расширении ничего не знают; простое — это позволит без проблем поддержать его в стороннем софте верхнего уровня, что важно партнёрам, которые используют наше железо и свой софт; открытое — мы не любим закрытые протоколы, которые привязывают пользователей к производителю железа. В статье мы будем разбирать расширение протокола на примерах и иллюстрировать картинками с логического анализатора. Если вы хотите своими глазами увидеть, что происходит внутри, вот список используемых в статье устройств и софта: Компьютер с Linux с преобразователем WB-USB485, можно взять другой преобразователь или просто контроллер Wiren Board. А также утилита wb-modbus-scanner, она же является референсной реализацией мастера для работы с расширением. Собранная версия для компьютера с Linux лежит в корне репозитория. Модуль счётных входов WB-MCM8 и реле WB-MR6C v.2. Важно, чтобы на модулях была последняя прошивка с поддержкой событий. Если это не так — обновите её описанным в документации способом. Логический анализатор, например, клон Saleae Logic Analyzer. Программа PulseView под Linux или Saleae Logic под WIndows. Надо включить декодер протокола UART. Настройки семплирования: 10М / 2 МГц. Команды подавались сразу после запуска захвата — это позволило целиком записывать запрос мастера, что не удавалось с установленным триггером на спад уровня.  Мы подключались к входу трансивера RS-485 одного из слейв-устройств на шине.  Прежде чем разбираться с расширением, вспомним, как работает классический Modbus RTU. Полное описание протокола читайте в нашей Вики или официальной документации. Modbus — это протокол, который служит для обмена данными между устройствами автоматизации.  В устройствах Wiren Board данные передаются по последовательным линиям связи RS-485 с использованием протокола Modbus RTU, который использует полудуплексный режим передачи данных и работает по принципу «клиент-сервер» или «мастер-слейв»: Мастер — это клиент, контроллер; Слейв — это сервер, модуль ввода-вывода или просто «устройство». По спецификации протокола каждый слейв на шине имеет свой уникальный адрес от 1 до 247, адрес 0 используется для широковещательной передачи команд. Есть ещё зарезервированные адреса с 248 по 255, которые в протоколе не используются. Мастер периодически по очереди опрашивает слейвы, которые ему отвечают. Мастер не имеет адреса, а передача сообщений от слейва без запроса мастера в протоколе не предусмотрена. Если при выполнении команды возникла ошибка, то слейв возвращает её код. Если слейв не ответил — мастер выжидает установленный таймаут и переходит к опросу следующего устройства. Обмен данными в Modbus RTU происходит через регистры, всего их четыре вида. Для каждого вида есть свои функции чтения/записи.  Тип Размер Функции Чтение Запись Coils — регистры флагов 1 бит 0x01 0x05 — одного 0x0F — нескольких Discrete Inputs — дискретные входы 1 бит 0x02 — Holding Registers — регистры хранения 16-битное слово 0x03 0x06 — одного 0x10 — нескольких Input Registers — регистры ввода 16-битное слово 0x04 — В запросе мастера содержится информация: адрес устройства на шине; код функции, которую необходимо выполнить; адрес первого регистра, количество нужных регистров и контрольная сумма. В ответе слейва содержится информация: адрес устройства; код функции; количество передаваемых байт; сами данные и контрольная сумма (CRC). Например, запросим у устройства Wiren Board с адресом 20 его modbus-адрес, хранящийся в holding-регистре 128: 0x14 — адрес устройства в HEX, 20 в DEC; 0x03 — прочитать один holding-регистр; 0x00 0x80 — адрес первого регистра в HEX, 128 в DEC; 0x00 0x01 — запрашиваем один регистр; 0x87 0x27 — контрольная сумма. Ответ слейва: 0x14 — адрес устройства в HEX, 20 в DEC; 0x03 —функция, которую выполнили; 0x02 — количество передаваемых байт; 0x00 0x14 — данные в регистре, 20 в DEC; 0xB5 0x88 — контрольная сумма.  Как мы уже видели выше, в Modbus есть концепция функций. Функций много, 15 из них используется, а остальные зарезервированы за производителями железа. Поэтому производители могут добавить отсутствующую в стандартном Modbus функциональность, просто используя заложенные в протоколе возможности. Мы взяли свободную функцию 0x46, с помощью которой реализовали возможности нашего расширения. Для отправки широковещательных команд мы используем зарезервированный адрес 0xFD (253). Контрольная сумма рассчитывается точно так же, как в обычном Modbus RTU. Важный момент — если ПЛК или SCADA-система ничего не знают о Быстром Modbus, они будут общаться с новыми «быстрыми» устройствами в обычном режиме, с функциями записать/прочитать регистры. То же справедливо и для контроллеров Wiren Board — если устройства-слейвы не поддерживают Быстрый Modbus, они будут опрашиваться «по старинке». Быстрый Modbus — это не отдельный режим или новый протокол — это просто расширение стандартного протокола Modbus RTU, которое добавляет новые возможности. Если функции доступны — ими можно пользоваться, если нет — всё будет работать как обычно. Со стороны слейвов расширение требует соблюдение таймингов, а от мастера не требуется ничего, кроме поддержки используемых в расширении команд и формата пакетов. Расширение «Быстрый Modbus» открыто и документировано, вы можете использовать его в своих устройствах и программном обеспечении. Для разработчиков устройств мы предлагаем платный фреймворк, реализующий протокольную часть классического Modbus RTU с нашим расширением и поддержкой обновления прошивок по шине RS-485. Но это не обязательно, вы можете поддержать у себя наше расширение самостоятельно. Далее мы рассмотрим, ради чего всё это делалось: мгновенный поиск устройств на шине, быстрая доставка изменений регистров от слейва к мастеру и разрешение коллизий адресов на шине. Арбитраж — это способ слейвам самим решить, кто сейчас будет отвечать на широковещательный запрос мастера, а также понять, есть ли ещё желающие ответить. Обычно это работает за счет того, что несколько устройств во время передачи разных состояний могут оценить, кто и что передавал. Мы вдохновились концепцией арбитража в CAN и сделали свой вариант по RS-485 с учётом особенностей шины. И в RS-485 и в CAN для передачи данных в шины используются специальные микросхемы — трансиверы (передатчики), но устроены они по-разному. В CAN есть две линии (CANH и CANL), которые растягиваются двумя транзисторами: CANH может быть подтянута к питанию через транзистор Q1, а CANL — прижата к GND через транзистор Q2.  Когда на шине вообще ничего не происходит, то транзисторы закрыты и потенциалы между линиями выравниваются через терминирующие резисторы.  Приёмники детектируют такое состояние как рецессивное. В момент передачи доминантного состояния оба транзистора открываются и линия CANH подтягивается к питанию, а линия CANL прижимается к GND. По разности потенциалов между линиями, приёмники понимают, что на шине доминантное состояние. Как видно из рисунка выше, если соединить два передатчика CAN вместе и одновременно передать разные состояния, то на шине будет установлено доминантное состояние. И тот, кто передает рецессивное, узнает об этом. Получается, что мы можем одновременно разными передатчиками выставлять на шине разные состояния. Сам арбитраж в CAN устроен так: устройства передают в шину побитно свой идентификатор, выставляя на шине то рецессивное, то доминантное состояние. Устройство, выставившее в шину доминантное состояние, арбитраж выигрывает, а рецессивное — проигрывает. Атомарной единицей арбитража здесь является бит. В RS-485 тоже есть две линии (A и B), но передатчик имеет отдельную пару транзисторов, чтобы подтянуть линии A и B к питанию (Q1 и Q2) и отдельную пару для прижима к GND (Q3 и Q4) — это нужно, чтобы иметь возможность явно передавать в линию логический ноль и единицу, что повышает помехоустойчивость.  Когда на шине вообще ничего не происходит, то есть передатчик выключен и все четыре транзистора внутри него закрыты —  линии растягиваются резисторами failsafe bias. Такое состояние приёмники детектируют как логическую единицу. В момент передачи включается передатчик и транзисторы подтягивают одну из линий к питанию, а другую прижимают к GND — состояния транзисторов зависят от того, что мы передаём: логическая единица — Q1 подтягивает B к питанию, а Q4 прижимает A к GND; логический ноль — Q2 подтягивает A к питанию, а Q3 прижимает B к GND. Если следовать идеям CAN-шины, напрашивается закономерный способ слушать эхо во время передачи в надежде, что какое-то из состояний на линии RS-485 окажется доминантным — однако этого не произойдёт.  На картинке выше два передатчика подключены к общей шине RS-485 и передают одновременно разные состояния: в первом передатчике открыты транзисторы Q1a и Q4a — на шину передаётся логическая единица; во втором передатчике открыты транзисторы Q2b и Q3b — на шину передаётся логический ноль. Как мы видим, ток из одного передатчика перетекает в другой. Поэтому на короткой линии состояние определит более сильный передатчик (или как повезет), а на длинной — потенциал распределится по шине, и приёмники каждого из устройств будут слышать те состояния, которые передают их же передатчики.  Это значит, что мы не можем использовать состояния 0 и 1 как доминантные и рецессивные при включённых передатчиках. Поэтому нам не оставалось другого выхода, как принять молчание за рецессивное состояние, а передачу одного из состояний — за доминантное. В процессе исследований мы столкнулись ещё с одной проблемой, которая нам не давала скопировать принцип арбитража CAN с атомарностью в один бит — Linux в мастере. Хотя он и не участвует в арбитраже, но после запроса ждёт ответа от слейвов. И если принимаемые посылки не будут иметь формат USART фрейма то приемник в Linux будет детектировать ERROR FRAME, а это усложнит работу с шиной. Поэтому мы сделали передачу одного байта в качестве доминантного состояния — это позволило нам без проблем обрабатывать эти состояния в мастере с Linux и избавило слейвы от необходимости слушать, что передают в шину другие. Доминантное состояние у нас передаётся значением 0xFF с использованием обычной передачи USART и аппаратным управлением драйвера шины. 0xFF — это максимальное число, которое можно передать в одном байте и передаётся оно импульсом шириной 1 бит во время передачи старт-бита. Остальное время посылка передается с состоянием 1 (IDLE ) на шине. Это оказалось удобно, так как если возникнет какая-то рассинхронизация передатчиков слейвов, она будет небольшая. Рецессивное состояние — это молчание в течение арбитражного окна, то есть передатчик выключен. Этим мы обходим аппаратное ограничение шины RS-485, в которой невозможно одновременно передавать разные состояния.  Арбитражное окно — это интервал времени, которое требуется для приёма 12 бит (одного UART-фрейма с двумя стоп-битами и битом чётности) плюс запас времени на обработку прерывания в микроконтроллере слейва. Запас времени на прерывание ограничен снизу 50 мкс и кратен времени передачи 1 бита.  За одно арбитражное окно передаётся один бит идентификатора: 0 — доминантным состоянием, а 1 — рецессивным.  Если устройство должно передавать доминантное состояние, то по началу арбитражного окна оно отправляет в шину 0xFF. Если на шине уже идет передача — устройство молчит, чтобы не передавать посылку, которая рассинхронизировалась. В этом арбитражном окне такое устройство проиграть не может. Чужая передача обнаруживается с помощью флага BUS BUSY, который есть в аппаратном блоке USART и выставляется, если на шине обнаружен чужой стартовый бит. Если же устройство должно передать рецессивное состояние — оно молчит в течение всего арбитражного окна и слушает шину. Если из шины за время арбитражного окна был принят байт — другое устройство передало доминантное состояние и арбитраж проигран. Во время арбитража устройство-слейв передаёт по одному биту друг за другом арбитражное слово, которое состоит из приоритета и уникального идентификатора. Приоритет сообщения — это 4 бита: 0 (0b0000) — наивысший, 15 (0b1111) — низший.   Уникальный идентификатор зависит от команды, отправленной мастером: Сканирование — (28-битное число) младшие 28 бит уникального серийного номера, что позволяет нам не обращать внимание на коллизии modbus-адресов. Чтобы устройства с поддержкой расширения от разных производителей смогли работать на одной шине, мы раздаём производителям блоки серийных номеров, а для DIY выделили отдельный диапазон — он указан в документации на Гитхабе. Опрос событий —modbus-адрес (8-битное число). Устройства на шине уже настроены, коллизии адресов отсутствуют, нет смысла тратить время на арбитраж по серийным номерам. В итоге арбитражное слово имеет длину 12 бит (4+8) при событиях или 32 бита (4+28) при сканировании.  Биты идентификатора передаются по порядку, начиная с MSB (most significant bit, самый старший бит в слове) и заканчивая LSB (least significant bit, самый младший бит в слове). Передача битов арбитражного слова ведётся по одному в течение арбитражного окна. К моменту завершения передачи арбитражного слова остаётся только одно устройство — победитель арбитража. Ноль передаётся доминантным состоянием, а единица рецессивным — это значит, что арбитраж всегда выигрывают устройства, у которых значение арбитражного слова меньше. Так как 4 бита приоритета идут в начале, то более приоритетные сообщения выигрывают. Если приоритет одинаковый, то арбитраж выигрывает устройство с наименьшим идентификатором. Если вы сейчас возьмёте серийный номер любого нашего устройства и переведёте в двоичную систему счисления, то у вас получится 32-битное число. Но как уже сказали, мы используем только младшие 28 бит серийника, так куда деваются старшие?  Старшие четыре бита серийного номера мы просто отбрасываем, чтобы вместе с битами приоритета уложиться в 32-битное арбитражное слово: 28+4 = 32. У нас всегда в старших четырёх битах серийного номера единицы, поэтому мы ничего от такого подхода не теряем.  Чтобы понять природу выбранных таймингов, вспомним, как выглядит посылка UART.  Каждый бит данных, а также стартовый и стоповый биты имеют одинаковую длительность, которая равна 1 с  / baudrate. Для формирования арбитражных окон мы используем отдельный аппаратный таймер, который настраивается таким образом, чтобы он инкрементировался с той же скоростью, с которой один бит передаётся по UART. Поэтому все временные интервалы у нас кратны времени передачи одного бита. Ожидание начала арбитража — это время между запросом мастера и первым арбитражным окном. Сверху ограничено временем передачи 3.5 символов по стандарту Modbus — это нужно, чтобы успеть на мастере выключить передатчик и включить приёмник для получения ответов от устройств на шине. Снизу ограничено 800 мкс и кратно времени передачи целого бита. Нижняя граница подобрана экспериментально с разумным запасом и нужна для того, чтобы устройства успели обработать запрос и понять, надо ли им участвовать в арбитраже и с каким приоритетом. Длительность арбитражного окна равна времени, необходимого для передачи одного фрейма в 12 бит (8 бит данные, 1 бит чётности, 2 стоп-бита и 1 старт-бит) плюс 50 мкс, округлённые до времени передачи целого бита — это время, необходимое устройствам обработать доминантное состояние на шине и решить, играют ли они дальше арбитраж. Таймаут на приём ответа — это время, которое ждёт мастер, чтобы понять, что на шине нет устройств, поддерживающих расширение Быстрый Modbus, и начать опрос классическим поллингом. Рассчитывается так: ожидание начала арбитража + длительность арбитражного окна * количество бит в арбитраже. Например, рассчитаем таймаут ожидания ответа на скорости 115200 бит/с: Время передачи 1 бита на заданной скорости =  (1с/115200 бит/с) * 1000000 = 8.681 мкс Ожидание начала арбитража = 3.5 * ( 1 старт + 8 данные + 1 четность + 2 стоп ) = 42 бита * 8.681 мкс = 364.602 мкс Получилось меньше требуемых 800 мкс, а ждать надо не менее 800 мкс, по этому таймер настраивается на ROUNDUP(800 / 8.681) = 93 бита. Время составит 93 * 8.681 = 807.333 мкс Длительность арбитражного окна = (12 бит * 8.681 мкс) + ROUNDUP(50 мкс / 8.681 мкс) * 8.681 мкс = 104.172 + 68.681 = 156.258 мкс Количество бит в арбитраже при сканировании 32, а в событиях 12. Получаем таймаут на приём ответа, который выжидает мастер: Сканирование: 807.333 мкс + 156.258 мкс * 32 = 5800.256 мкс ≈ 5.8 мс События: 807.333 мкс + 156.258 мкс * 12 = 2675.096 мкс ≈ 2.7 мс  Классическое сканирование шины в Modbus — это последовательный перебор всех адресов со всеми настройками подключения, что занимает больше 15 минут: 247 адресов, 8 скоростей, 3 варианта чётности и 2 варианта стоповых битов. А если на шине есть устройства с одинаковыми адресами, то отличить одно от другого становится невозможным. С помощью Быстрого Modbus перебор всех возможных комбинаций скорости, чётности и стоповых битов на одной шине происходит всего за ~3.5 секунды. Нам не надо перебирать все 247 адресов на шине и ждать таймаут на каждый запрос — этим мы экономим время. В общем случае в наших устройствах это происходит так: Мастер отправляет в шину широковещательную команду «Начать сканирование». Слейвы проводят между собой арбитраж — победитель передаёт данные о себе: серийный номер и modbus-адрес. Мастер обращается к слейву по серийному номеру и вычитывает из него модель. После вычитки модели мастер отправляет в шину команду «Продолжить сканирование». Слейвы снова проводят арбитраж, победитель арбитража отправляет данные о себе, а мастер вычитывает из него команду. Мастер снова отправляет в шину команду «Продолжить сканирование». И так продолжается до тех пор, пока на шине не останется неотсканированных устройств. Когда это случится — от устройства с наименьшим идентификатором придёт сообщение «Конец сканирования». Если на команду «Начать сканирование» никто не ответил, мастер выжидает таймаут и переключает настройки связи. Таймаут маленький, перебирать все адреса не нужно, поэтому перебор настроек происходит очень быстро. Для сканирования предусмотрены функции: 0x01 — начать сканирование, отправляет мастер; 0x03 — ответ на сканирование, отправляет один из слейвов; 0x02 — продолжить сканирование, отправляет мастер; 0x04 — конец сканирования, отправляет один из слейвов.  Возьмём два устройства и подключим их к шине, у наших серийные номера: WB-MCM8 — 4265607340 (dec) = 0xFE4000AC WB-MR6C v.2 — 4275217318 (dec) = 0xFED2A3A6 Просканируем шину с помощью утилиты wb-modbus-scanner: При выполнении команды мы включили отладку параметром -D, чтобы было видно отправляемые и принимаемые байты, которые мы разберём ниже. Мастер отправляет в шину команду «Начать сканирование», которая фактически звучит: «Есть кто?». Приняв эту команду, все устройства-слейвы на шине считают себя неотсканированными. Неотсканированные устройства будут пытаться отправлять ответ с одинаковым приоритетом (0b0110), поэтому в этот раз арбитраж выиграет устройство с наименьшим серийным номером. 0xFD — широковещательный адрес; 0x46 — команда работы с расширенными функциями; 0x01 — субкоманда начала сканирования; 0x13 0x90 — контрольная сумма.  Слейвы проводят между собой арбитраж, который выигрывает неотсканированное устройство с меньшим серийным номером. Здесь важно, что устройство указывает в ответе свой серийный номер, то есть в случае коллизии modbus-адресов на шине мы можем это увидеть и сменить modbus-адрес, обратившись к устройству по серийному номеру. 0xFD — широковещательный адрес; 0x46 — команда работы с расширенными функциями; 0x03 — субкоманда ответа на сканирование; 0xFE 0x40 0x00 0xAC — серийный номер устройства (big endian); 0x14 — modbus-адрес устройства; 0xE8 0x3A — контрольная сумма.  После того, как мастер получит ответ от выигравшего арбитраж слейва, мы можем отправить ему дополнительный запрос, например вычитать модель устройства. Здесь мы обращаемся к устройству по серийному номеру, используем стандартную команду «читать регистр», но упаковываем запрос в специальный пакет с функцией эмуляции стандартной команды 0x08. 0xFD — широковещательный адрес; 0x46 — команда работы с расширенными функциями; 0x08 — субкоманда эмуляции стандартных запросов; 0xFE 0x40 0x00 0xAC — серийный номер устройства к которому обращаемся (big endian); 0x03 — обычный запрос: стандартный код функции работы с регистрами; 0x00 0xC8 0x00 0x14 — тело запроса: прочитать 20 регистров (0x14) начиная с 200 (0xC8); 0x91 0xBA — контрольная сумма.  Устройство, получившее адресованный ему запрос, отвечает пакетом с функцией 0x09 и вкладывает в тело стандартный ответ на Modbus-команду. 0xFD — широковещательный адрес; 0x46 — команда работы с расширенными функциями; 0x09 — субкоманда эмуляции ответа на стандартный запрос; 0xFE 0x40 0x00 0xAC — серийный номер устройства к которому обращаемся (big endian); 0x03 — обычный ответ: стандартный код функции работы с регистрами; 0x28  0x00  0x57  0x00  0x42  0x00  0x4D  0x00  0x43  0x00  0x4D  0x00  0x38  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00 — тело ответа; 0xCE 0x86 — контрольная сумма.  Итак, мастер получил ответ от выигравшего арбитраж устройства и считал из него дополнительную информацию. Далее мастер отправляет команду «Продолжить сканирование», которая звучит как «Есть ещё кто?».  0xFD — широковещательный адрес; 0x46 — команда работы с расширенными функциями; 0x02 — субкоманда продолжения сканирования; 0x53 0x91 — контрольная сумма.  Устройства снова разыгрывают арбитраж, который побеждает неотсканированное устройство с наименьшим серийным номером. Победитель арбитража отвечает «Я тут», а мастер вычитывает из него модель. Этот цикл «Продолжить сканирование» → «Арбитраж» → «Ответ устройства» → «Запрос модели» → «Ответ устройства» повторяется до тех пор, пока все устройства на шине не будут отсканированы. Важный момент — на каждый запрос мастера «Продолжить сканирование» в арбитраже участвуют все устройства на шине, но сообщения у неотсканированных устройств с высоким приоритетом (0b0110), а у отсканированных — с низким (0b1111). Это значит, что пока на шине есть ещё неотсканированные устройства, их ответы всегда выигрывают арбитраж: ведь приоритет их сообщений был выше.  Но как только на шине будут отсканированы все устройства, на очередную команду «Продолжить сканирование» арбитраж выиграет сообщение от устройства, которое уже было отсканировано. Этот арбитраж выиграет устройство с наименьшим серийным номером (по оставшимся битам арбитражного слова). Но неважно, что за устройство выиграло этот арбитраж: все отсканированные устройства пытались отправить одно и то же сообщение «Конец сканирования». Получив такой ответ, мастер может быть уверен, что ни одного неотсканированного устройства на шине не осталось. Такой подход избавляет мастер выжидать таймаут. 0xFD — широковещательный адрес; 0x46 — команда работы с расширенными функциями; 0x04 — субкоманда завершения сканирования; 0xD3 0x93 — контрольная сумма.  Расширение Быстрый Modbus позволяет быстро опрашивать события, возникшие в устройствах без поочередного опроса каждого из них. Здесь, в отличие от сканирования, мы работаем по modbus-адресам устройств, поэтому на шине не должно быть коллизий адресов, как и при работе классического Modbus. В общем случае происходит это так: Мастер отправляет в шину широковещательную команду «Есть у кого что?». Слейвы проводят между собой арбитраж по приоритету и адресу — победитель передаёт пакет с имеющимися у него событиями. Мастер принимает пакет с событиями и отправляет в шину пакет с командой «Есть у кого что?». Если событий ни у кого не осталось, в арбитраже выигрывает устройство с наименьшим modbus-адресом и сообщает мастеру, что событий на шине больше нет. Мастер снова отправляет в шину команду «Есть у кого что?» и всё повторяется снова: арбитраж, ответ устройств с событиями.  Для работы с событиями предусмотрены функции: 0x10 — запрос мастером событий у слейвов; 0x11 — передача событий от слейва к мастеру; 0x12 — ответ слейва, если событий на шине нет; 0x18 — настройка событий мастером в слейве. Есть 4 типа событий, аналогичные типам регистров и пятый системный: 1 — coil; 2 — discrete; 3 — holding; 4 — input; 15 — системное событие. По умолчанию в устройствах Wiren Board все события, кроме события с низким приоритетом «Я перезагрузился», отключены. Включение происходит изменением приоритетов, которые влияют на те 4 бита приоритета сообщения в момент арбитража: 0 — событие не активно; 1 — событие с низким приоритетом; 2 — событие с высоким приоритетом. Приоритеты задаются для каждого события: если у устройства сработает приоритетное событие, оно выйдет на арбитраж с меньшим значением битов приоритета и выиграет арбитраж первым. Если таких устройств окажется несколько, они будут выигрывать арбитраж по очереди, начиная с наименьшего modbus-адреса. Подробнее о процедуре арбитража читайте в разделе «Арбитраж». Также у мастера есть возможность запрашивать события у устройств, начиная с определённого modbus-адреса. Это инструмент защиты от случаев, когда какое-то устройство с маленьким адресом спамит в шину событиями, не давая другим устройствам выиграть арбитраж. Чтобы гарантировать доставку событий от устройства к мастеру, предусмотрено подтверждение событий. В пакете с событиями есть поле флага, который может быть 0 или 1. Слейвы в каждом пакете с событиями инвертируют этот флаг относительно предыдущего, отправленного ими же. Таким образом, не существует двух пакетов подряд от одного устройства с одинаковым значением флага. Флаги от разных устройств между собой не связаны. Когда мастер принял пакет с событиями, он отправляет в шину очередной широковещательный запрос событий и вкладывает в специальное место modbus-адрес устройства, которому нужно подтвердить событие и флаг из полученного пакета с событиями, которые он подтверждает. Если подтверждать нечего — мастер в пакете запроса событий в поле для modbus-адреса устройства просто указывает 0. Слейв, который в запросе событий увидел свой modbus-адрес и верный флаг, сбрасывает отправленные события и выходит на арбитраж с новыми. Если флаг пришёл неверный — он добавляет новые события к уже отправленным, выходит на арбитраж и после победы отправляет новый пакет с тем же флагом. При опросе событий мастер руководствуется стандартной формулой времени ожидания, в которой ожидается 12 арбитражных окон: передача 4 бит приоритета и 8 бит modbus-адреса. Включим отправку события изменения счётчика коротких нажатий восьмого входа устройства WB-MCM8 с адресом 20: Параметры команды: -D — выводить отладку, то есть байтики; -b 115200 — скорость шины; -i 20 — modbus-адрес устройства; -r471 — 471 регистр устройства; -t 4 — тип регистра, Input; -c 1 — включить событие с приоритетом 1. Если сейчас снять с устройства питание и подать снова, то событие будет отключено, — надо включить его заново, если оно нам нужно. В ПО контроллера Wiren Board включением событий занимается драйвер wb-mqtt-serial, который включает нужные события автоматически при получении от устройства события «Я перезагрузился».  При настройке событий нужно учитывать, что не все регистры устройств Wiren Board поддерживают события — это сделано, чтобы избежать спама неважными событиями, например изменением напряжения питания. Номера регистров, для которых доступны события, описаны в документации на наши устройства. У драйвера в контроллере Wiren Board есть шаблон устройства с указаниями, какие события нужно включить, поэтому пользователю ничего настраивать не нужно. Конечно, при разработке своих устройств с использованием расширения Быстрый Modbus вы можете реализовать своё поведение. Например, сохранять настройку событий при перезагрузке и как-то усреднять значения, которые постоянно изменяются, чтобы не было спама. Никаких ограничений на уровне расширения протокола нет. Команда мастера звучит так: «Устройство с адресом 20, включи событие для регистра 471 с приоритетом 1». 0x14 — адрес устройства; 0x46 — команда управления разрешением передачи событий; 0x18 — субкоманда, настройка событий; 0x05 — длина списка настроек равна 5 байтам; 0x04 — тип регистра; 0x01 0xD7 0x01 — адрес регистра (big endian);  0x01 — количество регистров подряд ; 0x01 — приоритет события; 0x69 0xEA — контрольная сумма. В одном запросе можно включить события сразу нескольких регистров устройства, тогда команда будет звучать так: «Устройство с адресом N, включи события в X регистрах, начиная с регистра Y и для каждого установи такие-то приоритеты». Примеры смотрите на Гитхабе в разделе Функция настройки отправки событий.  Ответ устройства на команду изменения настроек событий звучит так: «Я устройство с адресом 20, включил события с такими-то приоритетами». 0x14 — адрес устройства. 0x46 — команда управления разрешением передачи событий. 0x18 — субкоманда управления разрешением передачи событий изменения значения регистра. 0x01 — длина списка значений настройки. 0x01 — флаги разрешения отправки событий, мы настраиваем один регистр, у нас флаг 1 — включено событие в одном регистре. 0x41 0x1C — контрольная сумма. В ответе опущены поля «тип регистра», «адрес регистра» и «количество регистров подряд», а флаги разрешений (приоритеты), упаковываются в битовые маски.  Аналогичным образом включим событие на состояние первого выхода второго устройства — реле WB-MR6C v.2 с адресом 241, регистр 0 тип Coil, приоритет 2: Параметры: -D — выводить отладку, то есть байтики; -b 115200 — скорость шины; -i 241 — modbus-адрес устройства; -r0 — 0 регистр устройства; -t 1 — тип регистра, Coil; -c 2 — включить событие с приоритетом 2. Структуру пакетов настройки событий мы разбирали выше, повторяться не будем. После настройки сгенерируем события в устройствах: замкнём и разомкнём 8-й вход WB-MCM8 — это увеличит счётчик срабатывания входа; включим канал 1 реле WB-MR6C v.2, для этого замкнём вход 1 этого реле. Сегодня в устройствах Wiren Board нет очереди событий, то есть устройство отдаёт мастеру сам факт события и текущее состояние регистра (payload), изменение которого вызвало событие. Поэтому для фиксации быстротекущих изменений, например нажатий на выключатель, мы используем счётчики. Всего у нас на шине два устройства с событиями, поэтому чтобы вычитать все события и получить финальное «Событий больше нет», нам потребуется три запроса и три раунда арбитража. Конечно, в реальной жизни мастер отправляет запросы постоянно, устройства постоянно играют арбитраж и кто-то из них отправляет свои события или сообщение, что событий нет. Но для того, чтобы понять суть происходящего, мы условно разделим череду запросов и ответов на раунды. Воспользуемся нашей утилитой и запросим события у устройств на шине. Так как подтверждать нам нечего, в флаге подтверждения и адреса устройства выставляем 0 (-e 0): Параметры команды: -D — выводить отладку, то есть байтики; -b 115200 — скорость шины; -e — подтвердить события с флагом 0, а если надо подтвердить события с флагом 1 — используется -E. После параметра следует адрес устройства, которому надо подтверждать события. У нас таких нет, поэтому указываем 0. 0xFD — широковещательный адрес; 0x46 — команда работы с расширенными функциями; 0x10 — субкоманда запрос событий от устройств; 0x00 — минимальный modbus-адрес устройства которое выйдет на арбитраж; 0xFF — максимальная длина поля данных с событиями в пакете, которую ожидает мастер, по стандарту длина всего пакета не должна превышать 256 байт; 0x00 — адрес устройства, от которого был получен предыдущий пакет с событиями. Нужно для подтверждения доставки, у нас подтверждать нечего, поэтому здесь ноль; 0x00  — флаг предыдущего полученного пакета для подтверждения приёма; 0xC8 0x9A — контрольная сумма.  После того, как мастер запросил события, устройства играют между собой арбитраж с учётом битов приоритета. В первом раунде у нас арбитраж выиграло реле WB-MR6C с адресом 241, а WB-MCM8 с адресом 20 проиграло. Это произошло из-за того, что у реле мы настроили событие с высоким приоритетом, и оно успело произойти до запроса событий мастером. То есть реле выиграло арбитраж по битам приоритета, а не по серийному номеру. 0xF1 — 241, адрес устройства; 0x46 — команда работы с расширенными функциями; 0x11 — субкоманда передачи событий от устройства; 0x00 — флаг этого пакета для подтверждения получения; 0x02 — количество не сброшенных событий; 0x09 — длина поля данных всех событий в байтах до контрольной суммы; событие 1 — 0x01 0x01 0x00 0x00 0x01; событие 2 — 0x00 0x0F 0x00 0x00; 0x10 0x64 — контрольная сумма. В одном ответе содержатся все события, произошедшие в устройстве и не подтверждённые мастером.  Информация о первом событии (изменение состояния выхода) содержит 0x01 0x01 0x00 0x00 0x01: 0x01 — длина дополнительных данных события (payload); 0x01 — 1, тип события — coil; 0x00 0x00 — идентификатор события или номер регистра (big endian); 0x01 — дополнительные данные (payload) (формат little endian).  То есть содержимое регистра 0 с типом 1. Информация  втором событии (Я перезагрузился) содержит 0x00 0x0F 0x00 0x00: 0x00 — длина дополнительных данных события. 0x0F — 15,  тип события — системное. 0x00 0x00 — идентификатор события или номер регистра (big endian). дополнительных данных нет. Событие «Я перезагрузился» возникло сразу после подачи питания и мы его не подтверждали — поэтому оно пришло вместе с событием, которые мы сгенерировали сами — включён выход 1. После обработки ответа слейва, мастер снова отправляет в шину запрос, но дополнительно указывает, что устройству с адресом 241 надо подтвердить события с флагом 0 (-e 241): 0xFD — широковещательный адрес; 0x46 — команда работы с расширенными функциями; 0x10 — субкоманда запроса событий от устройств; 0x00 — минимальный modbus-адрес устройства которое выйдет на арбитраж; (0xFF — максимальная длина поля данных с событиями в пакете, которую ожидает мастер, по стандарту длина всего пакета не должна превышать 256 байт; 0xF1 — 241, адрес устройства, от которого был получен предыдущий пакет с событиями. Нужно для подтверждения доставки событий; 0x00  — флаг предыдущего полученного пакета для подтверждения; 0x8D 0x0A — контрольная сумма.  Устройство с адресом 241 молча подтверждает у себя отправленные события и в следующем пакете с событиями уже выставит флаг подтверждения 1. Также оно выходит на арбитраж с другими устройствами с сообщением об отсутствии событий, которое отправляется с самым низким приоритетом (4 бита приоритета равные 0b1111). Во втором раунде побеждает уже устройство WB-MCM8 с адресом 20,  но тоже из-за битов приоритета. 0x14 — 20, адрес устройства; 0x46 — команда работы с расширенными функциями; 0x11 — субкоманда передачи событий от устройства; 0x00 — флаг этого пакета для подтверждения получения; 0x02 — количество не сброшенных событий; 0x0A — 10, длина поля данных всех событий в байтах до контрольной суммы; событие 1 — 0x02 0x04 0x01 0xD7 0x01 0x00 0x00; событие 2 — 0x00 0x0F 0x00 0x00; 0x7A 0xDA — контрольная сумма. В одном ответе содержатся все события, произошедшие в устройстве и не подтверждённые мастером.  Информация о первом событии (изменился счётчик входа) содержит 0x02 0x04 0x01 0xD7 0x01 0x00: 0x02 — длина дополнительных данных события (payload); 0x04 — 4, тип события — input; 0x01 0xD7 — 471, идентификатор события или номер регистра (big endian); 0x01 0x00 — дополнительные данные (payload) (little endian). То есть содержимое input-регистра 471. Информация о втором событии (Я перезагрузился) содержит 0x00 0x0F 0x00 0x00: 0x00 — длина дополнительных данных события; 0x0F — 15, тип события — системное; 0x00 0x00 — идентификатор события или номер регистра (big endian); дополнительных данных нет. Событие «Я перезагрузился» возникло сразу после подачи питания и мы его не подтверждали — поэтому оно пришло вместе с событием, которые мы сгенерировали сами — изменился счётчик входа 1. После обработки ответа от устройства, мастер снова отправляет в шину запрос, но дополнительно указывает, что устройству с адресом 20 надо подтвердить события с флагом 0: 0xFD — широковещательный адрес; 0x46 — команда работы с расширенными функциями; 0x10 — субкоманда запроса событий от устройств; 0x00 — минимальный modbus-адрес устройства которое выйдет на арбитраж; 0xFF — максимальная длина поля данных с событиями в пакете, которую ожидает мастер, по стандарту длина всего пакета не должна превышать 256 байт; 0x14 — 20, адрес устройства, от которого был получен предыдущий пакет с событиями. Нужно для подтверждения доставки событий; 0x00  — флаг предыдущего полученного пакета для подтверждения; 0xC7 0x9A — контрольная сумма.  Устройство с адресом 20 молча подтверждает у себя отправленные события и в следующем пакете с событиями уже выставит флаг подтверждения 1. Также оно выходит на арбитраж с другими устройствами с сообщением об отсутствии событий. 0xFD — широковещательный адрес; 0x46 — команда работы с расширенными функциями; 0x12 — субкоманда передачи сообщения об отсутствии событий; 0x52 0x5D — контрольная сумма.  После получения сообщение об отсутствии событий, мастер снова посылает запрос новых событий, но без подтверждения. И так по кругу. Наш драйвер wb-mqtt-serial отправляет запросы событий каждые 50 мс, что позволяет организовать практически мгновенную доставку событий. Кроме сканирования шины и работы с событиями, расширение Быстрый Modbus позволяет обратиться к устройству по серийному номеру и записать значение в регистр. Эту возможность можно использовать для разрешения коллизий modbus-адресов на шине — просто назначаете свободный modbus-адрес одному из устройств с одинаковыми адресами и готово: 0xFD — широковещательный адрес; 0x46 — команда работы с расширенными функциями; 0x08 — субкоманда эмуляции стандартных запросов; 0xFE 0x40 0x00 0xAC — серийный номер устройства к которому обращаемся (big endian); 0x06 — обычный запрос: стандартный код функции записи в регистр; 0x00 0x80 0x00 0xC8 — тело запроса: записать в регистр 128 (0x00 0x80) число 200 (0x00 0xC8); (2 байта) 0xDC 0x35 — контрольная сумма.  0xFD — широковещательный адрес; 0x46 — команда работы с расширенными функциями; 0x09 — субкоманда эмуляции ответа на стандартный запрос; 0xFE 0x40 0x00 0xAC — обычный PDU: серийный номер устройства к которому обращаемся (big endian); 0x06 — обычный ответ: стандартный код функции записи в регистр;  0x00 0x80 0x00 0xC8 — тело ответа: записал число 200 в регистр 128; 0x8D 0xF0 — контрольная сумма.  Таким образом, с помощью функции эмуляции стандартных запросов 0x08 мы можем читать и писать любые регистры устройства, обращаясь к нему по серийному номеру. Так как расширение базируется на классическом Modbus, а арбитраж между устройствами происходит только после запроса мастера, то ничто не мешает мастеру работать на одной шине с «быстрыми» и «медленными» устройствами. Мастер просто чередует запросы классического Modbus с широковещательными запросами Быстрого Modbus. Устройства, которые не поддерживают расширение, просто игнорируют незнакомый широковещательный адрес и команды. Для иллюстрации мы запустили опрос устройств через наш драйвер wb-mqtt-serial, который умеет одновременно опрашивать устройства обычным поллингом и по событиями. Драйвер каждые 50 мс отправляет в шину запрос событий, читает их, а остальное время работает с устройствами обычным поллингом. Если от устройства пришло событие «Я перезагрузился», драйвер отправляет ему пакет с настройками событий. На очереди внедрение всех возможностей расширения в софт контроллера, в частности, автоматическое добавление устройств на шине в конфигурацию драйвера, установка оптимальных настроек связи и раздача адресов для разрешения коллизий. Ещё мы подумываем расширить возможности самих периферийных устройств, например позволить им обмениваться сообщениями между собой без контроллера, — это позволит строить по-настоящему распределённые системы автоматизации поверх классической и надёжной шины RS-485. Техническая возможность есть, в железе мы уже пробовали. Также будем помогать партнёрам внедрять это расширение в свои устройства и программное обеспечение верхнего уровня с открытым исходным кодом. В статье мы рассказали о своём расширении «Быстрый Modbus», о причинах его создания, основных принципах и решаемых с его помощью проблемах. Теперь наши устройства можно моментально найти на шине, разрешить коллизии modbus-адресов и забыть про задержки при передаче изменений в регистрах в инсталляциях с большим количеством устройств.  Нам удалось добиться хорошего распределения времени реакции на события. Вот пример нашей гистограммы. Мы подключили много реле к одному контроллеру на одну шину, скрипт через GPIO дёргал входы реле и измерял задержку от отправки импульса до получения обновления в MQTT. Результаты занесли в таблицу и построили гистограмму. Расширение реализовано на программном уровне с применением заложенных в оригинальный протокол возможностей, поэтому мы добились полной совместимости наших устройств с протоколом Modbus. Если мастер, опрашивающий устройства ничего не знает про наше расширение — он будет работать с ними в рамках классического Modbus. Быстрый Modbus — открытое и хорошо документированное расширение протокола Modbus RTU, поэтому вы можете использовать его в своих устройствах и программном обеспечении.  Поделитесь в комментариях, а как бы вы решили стоящие перед нами задачи с учётом ограничений, которые у нас были?  Если вы разработчик, приходите к нам работать, мы ищем программиста и инженера-электронщика — вакансии на hh.ru. Много интересных задач, отлаженные процессы разработки, дружный коллектив и возможность делать устройства, которыми пользуются десятки компаний и тысячи частных пользователей. Есть удалёнка или офисы в Москве и Астане (Казахстан).   Техписатель