Открыть список
Как стать автором
Обновить

ModBus Slave RTU/ASCII без смс и регистрации

Промышленное программированиеРазработка робототехникиПрограммирование микроконтроллеровРазработка для интернета вещейПроизводство и разработка электроники
Всего голосов 10: ↑8 и ↓2 +6
Просмотры4.4K
Комментарии 49

Комментарии 49

… и показала свою 142% работоспособность
Не стоит так — Вы же техническую статью пишите, а не протокол результатов голосования. Тем более для MODBUS, спецификации которого с тех пор, как он вышел за пределы контроллеров MODICON, стали трактоваться очень вольно. Сами же довольно вольно пишите о паузе, которая в спецификации MODBUS RTU оговорена строго в 3.5 символа, а на практике отнюдь не любой слейв откликается через такую задержку после чужой посылки, иногда и десятки миллисекунд приходится вводить. Так что я бы и 100% постеснялся писать.
Пожалуйста, не судите строго… это литературный прием, гипербола.
Да я это понял, но уж больно глаз резануло. Извините, что не сдержался.
И это если не иметь ввиду различные USB конверторы, у которых задержки вообще никак не нормируются.
Очень сильно режет глаза стиль форматирования кода.
Ни за что не буду использовать такие исходники, тем более, что сложностей с поддержкой Modbus никогда не возникает.

Прогоните через стилизатор.

1. Стиль кодирования реально вырвиглазый, извините. Советую применять clang-format, а бонусом хорошо бы вообще посмотреть в сторону MISRA C, раз уж вы пишите про микро/промышленные контроллеры.
2. Мешанина из русских и английских слов в именах тоже режет глаза. Лучше придите к чему-нибудь одному, да и в целом naming convention у вас, мягко говоря, странноватое, как по мне.
3. Огромные супер-функции на несколько экранов — это вообще не ок. Дробите. Об этом еще Макконнел и Дядя Боб много лет назад говорили :) Если ваш компилятор не совсем тупой, то он всё это дело без проблем заинлайнит.
4. «long», «int» — если вдруг когда-нибудь настанет время переносить код на другую платформу, будете горько плакать. Лучше сразу использовать int8_t, int16_t, int32_t и их uint-аналоги.
5. Для расчета CRC16 существует табличная реализация, обеспечивающая гораздо лучшую производительность.
6. Делиться кодом с миром лучше все-таки через github, а не огромными листингами прямо в статье :)

Много где смотрел, но так и не нашёл нормального объяснения int24. Может вы в комментарии напишите как это работает. А ещё лучше бы статью.

int24
А разве так бывает???
Понятно, что через #define или typedef можно сделать все, что угодно, интересен практический смысл.
Редко но бывает. Я дважды нарвался на проприетарный протокол. И там именно 24 бита на кодирование числа. Думаю те кто связан со звуком(программирование), это знают очень хорошо. Мне как раз помогла такая статья с стэковерфлоу для звука, но в плк. Пришлось городить свой велосипед.
UPD.
Самое интересное в том, что похоже эти наработки/разработки идут из военки.
Я сам сейчас настраиваю кодек для звука (микросхема CS43L22).
И, например, для I2S протокола независимо от формата кодирования, данные по DMA все равно передаются по 16 или 32 бита (даже при семпле 24 бита).
Поэтому и интересен практический смысл такого типа данных.
Могу рассказать свой пример. Я использовал тип uint24_t один раз в жизни. Делали очень простой контроллер для СКУД. Важна была цена, поэтому заложили очень простой микроконтроллер с минимальным объёмом памяти. При этом, чтобы обеспечить поддержку большего числа пользователей, для хранения кодов карт доступа использовали поле размеров в 24 бита. Карты были Em-Marine. В результате на 1000 пользователях экономия аж целый килобайт оказалась, но для контроллера это много :-)
Еще один пример UINT24 — это UEFI Firmware File System v2, где Intel не хватило одного байта в заголовке размером 24 байта, и чтобы не поплыло все выравнивание его решили откусить от размера файла, потому что 16 мегабайт (0xFFFFFF) должно хватить всем. В результате, понятно, не хватило, и пришлось выдумывать FFSv3, править все заголовки, обновлять все парсеры, и до сих пор еще встречаются прошивки, которые тома с v3 толком прожевать не могут и зависают в случайных местах.
Короче, если у вас там не прямо жесть-жесть, то экономить один байт, чтобы потом иметь геморрой и новый заголовок на восемь — это плохая негодная стратегия, не рекомендую.
Я ни разу в жизни не встречал int24, но вполне могу допустить его существование :)
Зачем — тут ответ простой, подозреваю что уже тут упомянутые какие-нибудь ЦАПы/АЦПы волне могут оперировать 24-битными семплами. Храня их «как есть» вместо расширения до 32 бит мы во-первых не делаем лишних движений при IO, а сразу фигачим блоки данных через DMA, а во-вторых можем на 25% оптимальнее использовать память, которой не всегда бывает много, не говоря уж о разных там аппаратных буферах и кэше процессора.

А вот вопрос «как это работает» уже интереснее. С регистрами проблем быть вроде как не должно, 24 бита отлично влезают в 32-битный регистр или в два по 16.
А вот я памятью все интереснее. Во-первых, процессоры обычно оперируют не единичными байтами, а машинными словами, поэтому за раз именно 24 бита из памяти вы не скопируете, придется копировать все 32. Но это, в принципе, не проблема, достаточно сделать сдвиг или битовую маску (в зависимости от того какая у нас endianness), тут ничего сложного.

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

Правда, если архитектура не возражает против такого доступа, то int24 вполне может существовать, а компилятор сам будет высчитывать правильные смещения и «подрезать» 32-битные инты до 24 бит, избавляя разработчика от лишней рутины.
А вот я памятью все интереснее. Во-первых, процессоры обычно оперируют не единичными байтами, а машинными словами, поэтому за раз именно 24 бита из памяти вы не скопируете, придется копировать все 32. Но это, в принципе, не проблема, достаточно сделать сдвиг или битовую маску (в зависимости от того какая у нас endianness), тут ничего сложного.
При чтении памяти понятно, но как быль при записи в память?
Читаем одно машинное слово того, что у же есть в памяти, накладываем битовую маску чтобы очистить то место куда мы поместим свои данные, при необходимости сдвигаем наше слово на сколько надо, далее логическое OR всего этого вместе, и всё, можно писать в память.
Да, хлопотно и много инструкций. Поэтому, подозреваю, что основные алгоритмы все равно работают именно с блоками из 32-битных слова, а int24 используется только для хранения и передачи.
Конечно, в отрыве от конкретной задачи это обсуждение бессмысленно, но Вам не кажется странной оптимизацией заменять один цикл записи в память на чтение-модификация-запись ради экономии 25% объема памяти за счет более плотной упаковки? Да, объем памяти — ограниченный ресурс, но ведь и быстродействие тоже.
Зависит от конкретной железки, если у нас есть быстрый процессор специально под нашу задачу, а вот памяти мало, или же надо слать несжатые данные по какому-нибудь интерфейсу с ограниченной пропуской способностью, или сторонний ЦАП/АЦП работает только именно с 24-битными семплами и не умеет ни во что другое…

Впрочем, я даже не удивлюсь, если у каких-нибудь DSP специально будут низкоуровневые команды для подобных операций.
Поэтому я сразу и оговорился про конкретную задачу, и речь шла именно о формате хранения данных в памяти, а не о вводе/выводе.
Ну, с «хранением» случаи тоже разные бывают. В embedded, в low-memory системах можно встретить такую штуку как zRam, в которую кладут swap. То есть по сути дела получается компрессия памяти. Казалось бы, ресурсы ЦП тоже ограничены (да еще и постоянные переключения контекста), но в итоге игра стоит свеч и подобное используется даже в сборках Android.
А урезание int32 до int24 — относительно очень быстрый и простой компрессор с гарантированным коэффициентом сжатия :)
Кстати, нашел еще упоминания, что некоторые AVR умеют в 24-разрядные адреса :)
Соответственно, как минимум нужен тип, чтобы их удобно было хранить, например, если вы решите запихать их в packed-структуру.
На счёт int8_t, int16_t, int32_t абсолютно верно. Всё таки заявляется, что код может использоваться и на встраиваемых системах. Тут чёткость должна быть.
А вот на счёт табличного расчёта CRC не соглашусь. Скорость в таких вещах не так важна. Тут же всё упирается в канал связи, а там обычно не более 115200 бит/сек.
Зато обычный метод расчёта сильно экономит память, а для микроконтроллеров это важно, её там не особо много.
Почему вы не формируете ответ с кодом ошибки, если мастер неверно запросил данные?

//если неправильный адрес и количество
if((AdresBit+KolvoBit)>(ModBusMaxOutBit) || KolvoBit>ModBusMaxOutBitTX || KolvoBit==0)
{
       //неправильный адрес и количество
        CRCmodbus=0xFFFF; //установить начальное значение CRC
        return;//повторный запрос не требуется
}


Ведь по спецификации Modbus должен быть ответ в таком случае, а ваш slave молчит как партизан. Поди догадайся, то ли сетевой номер не совпадает, то ли с командой что то не то.
При неправильном адресе slave не должен ничего отвечать. Ведь таких устройств на шине может быть много, для каждого из них чужой адрес будет «неправильным». В этом случае получится коллизия на шине.
Я так понимаю, речь идет про неправильный адрес регистра/ячейки, а не самого RTU.
А, тогда я неправильно понял. В этом случае да, слэйв обязан выдать ответ с ошибкой.
Вы неправильно понимаете.
Да, если адрес slave не совпадает с тем что запрашивает master надо молчать.
Но, если slave понимает что пакет адресуется ему и при этом он не может его обработать, он должен ответить коротким сообщением, в котором передаётся код ошибки.

Да, это верно. Спецификация Modbus подразумевает передачу слэйвом кода ошибки.
Моя практика показывает, что это не так уж и надо. А код раздувается.
В следующей версии сделаю как опцию.
Это надо, потому что это описано в спецификации, по которой производители программного обеспечения по всему миру пишут свой софт. Если вы ей не следуете, то не можете называть то что вы делаете modbus протоколом.
Как разработчик OPC сервера, использующего в своей работе Modbus в том числе, я часто сталкиваюсь с подобными поделками. Мне жалко конечного пользователя, который обычно остаётся с таким прибором один на один, стараемся вместе выкрутиться, приходится вносить изменения в собственное ПО, но к сожалению это не всегда получается. И всё только из за того что кто то решил
А, мне это не надо...

Вспомнил почему я не делал обработку ошибок!
Все дело в неоднозначности трактовок функций ошибок 2 (ILLEGAL_DATA_ADDRESS) и 3 (ILLEGAL_DATA_VALUE).
Каждый трактует их как хочет.
По большому счёту без разницы кто как трактует. Самое главное показать ошибку. Мастер от этого не сломается, зато человек сразу поймёт в каком месте проблема.
В натуре! сейчас проверил на Kepware,
задал мастеру блок чтения дискретных входов 16, а в слейву 8

что возвращаешь слайвом ILLEGAL_DATA_ADDRESS, что ILLEGAL_DATA_VALUE
Kepware пишет
Date Time Level User Name Source Event
05.11.2020 19:39:58 2 Default User Modbus Serial Bad address in block [000002 to 000012] on device 'c1.d1'

А где юнит-тесты? Уж для чего-чего, а для обработчиков коммуникационных протоколов тесты писать сам бог велел, особенно в embedded...

В качестве «калибра» использовал OPCсервер Kepware. Методику тестирования описывать долго. Но покрытие 100%!
Ну что тут скажешь. Все уже сказали до меня. Мне остается только сказать так — вернитесь к этой статье лет через пять. Не плохо. Но и не хорошо. ModBus точно не из тех протоколов, для которых надо (и стоит) из главного цикла запускать ModBusRTU/ASCII(). Так как ModBus (почти?) всегда не единственная функция устройства, то времена начинают плавать. А это не здорово. Да, я понимаю — большинство кода в сети написано так. Но попробуйте однажды написать код так, чтоб в главном цикле остался только сброс watchdog'а. Первый раз это всегда сложно. Но поверьте — результат того стоит и вам точно понравится.
ModBus точно не из тех протоколов, для которых надо (и стоит) из главного цикла запускать ModBusRTU/ASCII(). Так как ModBus (почти?) всегда не единственная функция устройства, то времена начинают плавать. А это не здорово.

Я тоже так когда-то думал… Но лет 15 как отпустило…
Хорошо. Значит у каждого свой путь.
Но 15 лет практики и такой код… Кто-то из нас куда-то не туда свернул…
Без обид. Абсолютно ничего личного.
Некоторые изучают стили форматирования, правила именования переменных итд. итп. А некоторые просто пишут.
Нельзя же утверждать что Достоевский не писатель, на основании почерка.
Нет конечно. Хотя, полагаю, графологи могут с этим не согласиться. Да и помимо подчерка есть построение предложений, обороты речи, манера использования слогов, созвучных описываемым явлениям. Впрочем, оставим это литераторам, лингвистам, и прочим специалистам-криминалистам. Я к ним «примазываться» не собираюсь. Не мое.

Мне понравились битовые поля в коде. Не часто их используют, хотя инструмент очень мощный. У меня даже нет особых претензий к длинным функциям. Я, конечно, побил бы. Но с другой стороны… Слегка ужать стек заinline'ив все руками — разве кто-то может запретить такую микрооптимизацию? Да, я вижу даже те места, которые написаны в угоду переносимости и выглядят не шибко оптимально. Хорошо, если это все сделано осознано и с пониманием.

Но знаете что настораживает? Несопоставимость задачи и решения. Уж больно несерьезная задача для серьезного разработчика. Без откровенно новаторских идей. Собственно отсюда и мои «лет через пять». Потому я позволю себе оставить эту рекомендацию. По себе знаю. Тот код, который писался 5 лет назад, сегодня был бы написан похоже… но немного иначе. А за тот, который успешно отработал для гарантийных срока по 8 лет откровенно стыдно. И это не смотря на то, что и 20 лет назад код писался на том же голом С.
А некоторые просто пишут.
Извините, что влезаю, но есть один старый анекдот, который заканчивается строкой «Чукча не читатель, чукча писатель...»
Вот он тут весьма актуален.
Стандарт кодирования (и в это понятие входят далеко не только конвенции наименования и оформления, нет), best practices используемого ЯП, продумывание архитектуры, юнит- и интеграционные тесты, линтеры, VCS и код-ревью — это все не глупые свистелки & перделки, это на деле очень полезные инструменты и проверенные тысячами людей рекомендации для улучшения жизни разработчика и написания надёжных (в первую очередь надежных!), переносимых, расширяемых программ с пригодным для чтения и переиспользования кодом и минимальным числом ошибок.
Да, если вы пишите какую-то наколенную поделку, которой кроме вас никто пользоваться не будет, то сойдет и «херак-херак и в продакшн». Но если вы являетесь профессионалом и разрабатываете за деньги ПО, которым будут пользоваться люди и с кодом которого возможно когда-нибудь будут работать другие программисты, стоит это делать всё-таки качественно и по уму, а не тяп-ляп. Этим, собственно, и отличается инженер-строитель от садовода, строящего сарай на участке…
стили форматирования
«Стиль кодирования» это не только «стиль форматирования». Я выше уже упомянул MISRA C — посмотрите, если еще не видели, подобный набор простых правил действительно хорошо помогает избегать целого ряда проблем и ошибок.
Ну а «стиль форматирования» так вообще изучать нечего, он при желании обычно автоматически применяется в IDE или в git pre-commit hooks :)
Но 15 лет практики и такой код… Кто-то из нас куда-то не туда свернул…
Увы, в ембеддед, в асутп и в околонауке такое встречается повсеместно — когда в сложнейшей железке за много $ код написан так, что ни за что бы не прошел code-review перед пушем в master-ветку даже в самой захудалой галере, не говоря уж о более приличных предприятиях.

Даже тут на Хабре об этом писали не раз и не два:
Ничего удивительного. По моим наблюдениям, многие «железячники» считают, что производство устройства — это искусство, подвластное избранным, а вот написать к нему код он сможет сам, так, на коленке. Это ж вообще мелочь. Получается работающий тихий ужас. Они очень обижаются, когда им на пальцах объясняют, почему их код дурно пахнет, потому что… ну… они ж железку сделали, че тут, программа какая-то»

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

А когда на это обращаешь внимание, так сразу начинаются или обиды, или сказки на тему «своей специфики».
Зато снобизма при упоминаниях десктопных- и веб- программистов через край.
Знаете, я сам железячник. И в низкоуровневые программисты пришел из схемотехников. И с улыбкой встречаю рассказы веб-программистов о сложностях очередного PHP/JAVA/нужное-подчеркнуть фреймворка. И да, для меня тоже главный критерий это бессбойная работа 24x7x365xМНОГО. Если устройство работает не стабильно, периодически сбоит или неадекватно реагирует на «шум на входе» — значит устройство не работает. Пожалуй, это наша «профессиональная деформация». Главное ее признать и не выпячивать лишний раз. Тем более, что строго по Михалкову «мамы всякие нужны, мамы всякое важны». Мир из одних только железячников был бы откровенно страшен.

Потому я уже сожалею что запустил эту ветку. Давайте закругляться. Абсолютно ничего личного.

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

Есть ещё одна, не сказать ошибка, скорее тонкость.
      if(CRCmodbus==0) 
        {//проверка на длинные пакеты
        if(PaketRX[1]==15 || PaketRX[1]==16)
          {//если длинные команды (15,16) , проверяем "Счетчик байт"
          if((PaketRX[6]+9)!=UkPaket) continue;
          }
        break; //Ура! Пакет принят!!!
        }


Вы окончание пакета определяете по совпадению контрольной суммы. Это не совсем корректно, на моей практике случалось, что у больших пакетов был такой набор данных что контрольная сумма совпадала в теле пакета, в результате поступали данные поступали некорректные.
По хорошему, необходимо анализировать служебную информацию, количество регистров, вычислять сколько должно быть байт в пакете, сличать что бы количество принятых байт было не меньше чем ожидается и только потом рассчитывать контрольную сумму.
Прошу прощения, оказывается есть такая проверка.
if((PaketRX[6]+9)!=UkPaket) continue;

Просто я дополнительно ещё и с количеством регистров на всякий случай сверяю.
дык это оно и есть.

там еще выше защита для коротких пакетов
if(UkPaket<8) continue;

Я ввожу дополнительную проверку ещё и потому, что в Modbus минимальный элемент имеет размер в 2 байта и запросто может оказаться так, что в результате ошибки мастер выдаст нечетное количество байт.

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