Pull to refresh

Comments 24

Приятно видеть код без HAL :), только небольшое замечание:
MB_USART->CR1 = 0;
MB_USART->CR1 |= (USART_CR1_TE | USART_CR1_RE | USART_CR1_RTOIE);


Зачем тут чтение из CR1? Вы же его обнулили только что, можно просто присваивать.
Ну это я так, просто на глаза попалось :)
Блин, ну почему так! С спецификации что написано? Конец пакета — это пауза не менее 3,5 символов. В процессе приема пакета возникла пауза больше 1,5 символов — пакет должен быть проигнорирован. Это корневые требования. Без их реализации категорически нельзя пускать решение в реальный мир. Ну и где они в этой реализации? Как, к слову, и обработка ошибок кадрирования.

А все остальное… Скажите, а за что вы так не любите структуры? Вам самому не страшно от конструкций типа *((uint16_t*)&frame[len — 2])? И да, применительно к ModBus на ARM. Есть замечательная коллекция инструкций REV (к слову, накрытая CMSIS и приведенная к удобным макросам) позволяющая эффективно крутить последовательность байт на лету.
Тайминги надо выдерживать, солидарен.
Интересно, заметил что не один раз уже прочёл спецификацию на протокол Modbus в текущей версии.

В ней есть Очень много чего интересного, о чем многие не подозревают.
Не только «Modbus Serial Line Protocol and Implementation Guide V1.02»
но и «MODBUS Security Protocol» само по себе — интересное чтиво, а уж сколько всего интересного таит в себе «MODBUS Protocol Specification» и «MODBUS MESSAGING ON TCP/IP IMPLEMENTATION GUIDE V1.0b ». Как говориться "… О сколько нам открытий чудных. Готовят просвещенья дух..." (с) А.С.Пушкин
Ну, скажем так. «Modbus Serial Line Protocol and Implementation Guide» и «MODBUS Protocol Specification» были настольными книгами когда писал реализацию. Вообще, довольно странная шинка. Пожалуй, я готов многое списать ей за ее почтенный возраст и… "… строгость законов компенсируется не обязательностью их исполнения (с)". В частности для целей заказчика я довольно вольно обошелся с 0x14/0x15 Read/Write File Record. А еще веселее когда на шине два и больше мастера. Спецификация не запрещает такой расклад… Моя реализация была универсальной. В том смысле что я обладал адресом на шине и предоставлял регистры/входы/файлы, но при этом легко мог спросить соседа по необходимости. Справедливости для мало где используется хоть что-то умнее нескольких регистров, предоставляемых ведомым. И именно такая реализация чаще всего востребована. Вс всяком случае мне обычно встречается именно такое.

И беда с этой шиной всегда одна — на столе работает, «в поле» нет. Чаще всего первопричина в таймингах и методике разрешения коллизий. Многие производители, как и автор этой статьи, спецификацию читают очень выборочно. В итоге появляется некоторый «зоопарк», в который надо вклиниться своим изделием. Отчасти по этой причине все чаще приходиться наблюдать рядок конвертеров TCP/RTU с последующим подключением точка-точка. Это медленнее и дороже, но работает сразу после сборки чаще.
И беда с этой шиной всегда одна — на столе работает, «в поле» нет.

Про 485 Не совсем согласен.
Почему? Вот работаю сейчас в компании выпускающей ОПС. При этом в, пожалуй, самой большой по пожарной сигнализации. Ну то-есть вот 70% всех систем пожарной сигнализации (и какое то количество охранных) так или иначе на этом заводе сделаны. Все так или иначе завязаны на 485-й интерфейс.
Это Очень много. Это прямо Огромные цифры по оборудованию, даже в месяц.
Диспетчеризации 27/7. Вполне себе в основном стабильно может работать.
(А самом деле и автоматизации, ибо даже клапана дымоудаления в высотках многоквартирных и станции пожарные — тоже Автоматизация. а их Очень Много.)

Тут скорее вопрос к реализации протоколов на шине, и шириной трактовки протокола,
которая в многих протоколах сужена, с определенными целями.
А так то Modbus вполне себе современен, и TSL вроде даже может в нём быть.
P.S. смешал тёплое с зеленым, но смысл в том, что 485 вполне себе в больших объёмах применяют и в достаточно надежных применениях.
Я так скажу — скорее вопрос областей применения и сертификации (тестирования) итоговых изделий. Пожарка — это круто. Цена ошибки — жизни. Надеюсь что сертификация этого оборудования выполняется очень и очень строго. Муха не пролетит.
Промышленное оборудование — цена ошибки убыток. Сертификация проще, но спрос на стабильность шкалящий. Должно работать годами без сбоев. Ибо ремонт — это простой, а значит убытки.
А есть транспорт. При чем не транспорт-транспорт, а сервис. Типа табло прибытия или бегущей строки с остановками. Поломалось — и гори оно…
Вам повезло. В дикой природе вы сталкиваетесь в основном с первым. Я — в основном с третьим. Ваши реализации, которые «в поле» работали нестабильно были зарезаны на корню еще на выходе из лаборатории. А то, с чем я имею дело… Увы, но подход «х?? к, х?? к — и в продакшн (с)» просто повсеместен. И, если совсем честно, пусть так и остается. Не дай бог в РЕАЛЬНО ответственных сферах будет такой бардак.
Что до ModBus — так подавляющее большинство реализаций плавать хотела на правила нарезки пакетов в RTU. Даже libMODBUS (по крайней мере некоторое время назад — сейчас не знаю) резала их по формату, а не по таймингам. В итоге как только интенсивность обмена превышает некоторый предел все разваливается. Следующая засада — игнор ответов. Типа я послал выставить значения регистров и в домике. Получили их, не получили — мне пофиг. Ну и шлю, разумеется когда хочу, а не когда можно. Эта статья как раз такие вещи содержит. А посмотрите на первый же комментарий: «в закладки». А значит рано или поздно или конкретно эта, или на нее похожая реализация встретится в дикой природе.

Извините за некропостинг, но хотел бы задать несколько философский вопрос. Регистры содержат управляющие значения, т.е. запись в регистр должна вызывать изменение поведения. Как это делается наиболее фэншуйно? Я вижу 3 варианта:

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

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

  3. Примерно как 2: существует копия регистров, которая "на самом деле", и таск через определенные интервалы или по извещению сравнивает публичную и теневую таблицы регистров и применяет диффы.

Как поступать правильнее?

Мне кажется тут сильно зависит от контекста.

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

Если вообще философски мне кажется- то скажем запись в регистр, допустим с номером 7, положим какого то числа DA, допустим функцией 6 - совсем не означает что к регистру с номером 5 можно вообще обратиться за чтением функцией 3,

И совсем не означает что можно применить групповое чтение или запись, включающее этот регистр.

Это в свою очередь означает, что один адрес на чтение и запись может использоваться вообще с разным смыслом.

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

как пример из реальности - запрашивается по модбасу в преобразователя протоколов (есть такой в опс-е у меня) состояние зоны извещателя (датчика) -записывается номер датчика из конфигурации, допустим 18. Модбас устройство, при очередном цикле опроса мастером Опс-ной системе (а это другой круг интерфейса и данных) - передаёт тому, другому мастеру - " алё, чего там с датчиком"?. он, этот мастер, когда придёт его время с его приоритетом - спросит конкретное устройство - " чо там, как , хоть параметр скинь?" а устройство к примеру со своей 1-wire подобной шиной, которую целиком опрашивает в зависимости от количества датчиков и их типов и вообще запроса статуса - либо сразу из таблицы, либо если идёт дотошно спрашивает - до 5-и секунд. и потом обратно.

То-есть реальный ответ может придти и через секунду и через 10.

И конечно откидываться ошибками 06(обработка команды) и 15(данные пока не получены) можно, но это же ещё у нас к примеру разделено на два регистра, в один выставляешь что надо, в другом читаешь.

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

ну это я так вижу, есть более каноничные видения, на самом деле.

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

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

именно так.

Мне кажется не зачем ограничивать одним регистром.

Опять же можно даже выделить отдельный регистр Модбас для счётчика команд.

Более того, можно такой счётчик повесить на каждый регистр, распределив каким то логичным образом адресное пространство.

Регистров можно сделать много, и всё зависит от потребности.

Мне кажется не стоит впадать в крайность, когда одним регистром и на чтение и на запись делается вообще всё - можно прийти к другому примеру

)не помню название датчика газа) - у него на чтение и на запись был один 4-х байтный регистр.

первый Бит означал сработку-несработку с момента последнего сброса сработок, естественно тот же бит на запись сбрасывал,

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

Спасибо, тогда для конфигурации сделаю две таблицы с разным смещением, одна на запись (отдачу команд), другая на чтение (статус). Тогда можно будет в комбинации freertos-freemodbus в колбеке писать в очередь "изменение такого-то регистра" (в фримодбас колбеки исполняются в контексте прерывания), а в отдельной таске ртоса разбирать очередь и применять изменения.

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

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

Мне из спортивного интереса, а как обратиться с помощью структур к последним двум байтам frame при невозможности вычисления len во время компиляции?
А так, здесь это не нужно, можно считать crc всего frame, при корректной crc — результат будет 0.
На счет таймингов Вы правы, их действительно два: 3,5 и 1,5 символа. Таймаут в 1,5 символа, все же не отменяет выдержку таймаута в 3,5. Стандарт предлагает, делать проверку CRC и адреса после таймаута 1,5 и отбрасывать или обрабатывать полностью только после таймаута 3,5. Можно будет внести правки в реализацию.
Вам самому не страшно от конструкций типа *((uint16_t*)&frame[len — 2])?

Не страшно, если только этим не пренебрегать =) Но не очень понимаю, как тут «натянуть» структуру на фрейм переменной длины. Если поделитесь, то буду только рад.
Не вопрос. Начну с конца. Ниже правильно подсказали — целостность пакета проверяется путем вычисления его CRC. Если пакет живой, то будет ноль. Иначе пакет с ошибкой. Все. На этом про CRC мы забываем. Теперь о переменной длине. Смотрите, ModBus предполагает, что пакет всегда начинается со SLA, затем всегда идет Function. Это заголовок. Наложив эту структуру на пакет мы уже можем сказать нам ли он, и поддерживаем ли мы эту функцию. За заголовком идет наполнение. Оно специфично для каждой конкретной функции. Но! Теперь мы уже знаем функцию, а значит можем наложить на буфер структуру, соответствующую заданной функции. Уже лучше, но как же быть с переменным числом регистров? А просто…
typedef struct {
  uint8_t       addr;
  uint8_t       function;
} modbus_rtu_header;

typedef struct queryPresetMultipleegisters {
  modbus_rtu_header     header;
  uint16_t              regStart;
  uint16_t              regNumbers;
  uint8_t               ByteCount;      /* ByteCount = query.RegNumbers * 2 */
  uint16_t              raw[];          /* raw[query.regNumbers] - CRC */
} qPresetMultipleRegisters;


И вуаля… q->raw[0], q->raw[[1]… q->raw[q->regNumbers] — все наше. И даже дальше, но мы ж дальше не полезем ;-) При упаковке все то же самое в обратном порядке. CRC заполняется последним. Магия языка С (наложении структуры на массив) собственной персоной. Настоятельно рекомендую. Создать переменную типа qPresetMultipleRegisters не получится. Компилятор совершенно резонно заметит, что не знает сколько памяти под нее выделять. А вот массив байт привести к типу этой переменной и потом с ней работать — это запросто.

P.S.
Про тайминги. 3,5 — межпакетный, 1,5 межсимвольный. Если межсимвольный превышен (но межпакетный не достигнут), то пакет считается неживым и подлежит утилизации.
Да действительно можно, спасибо, возможно добавлю для своих команд наложение структур.
Создать переменную типа qPresetMultipleRegisters не получится. Компилятор совершенно резонно заметит, что не знает сколько памяти под нее выделять.

Отчего же не создаст, еще как создаст и даже ругаться не будет. Ведь
uint16_t raw[]; 
эквиалентно
uint16_t* raw;
. А размер указателя на uint16_t извествен.
Про тайминги. 3,5 — межпакетный, 1,5 межсимвольный. Если межсимвольный превышен (но межпакетный не достигнут), то пакет считается неживым и подлежит утилизации.

Да подлежит, но только после того, как будет выждан интервал 3.5 тоже. Как может быть достигнут межпакетный интервал в (3.5) без достижения межсимвольного (1.5) ?)
image
Как может быть достигнут межпакетный интервал в (3.5) без достижения межсимвольного (1.5) ?)


Абсолютно верно! Но именно по этой причине стоит проверять тот факт, что между отсечками в 3,5 символа была только одна отсечка в 1,5 символа. На самом деле все еще интереснее. 1,5 и 3,5 в спецификации это он конца до начала. Начало не всякий контроллер фиксировать умеет (и STM32 вроде как раз не умеет прерывание по START-биту делать). Потому фиксация межу концами. А значит не 1,5 и 3,5 а 2,5 и 4,5. А вот при таком раскладе наложения уже не получается. Потому ровно один.

Отчего же не создаст, еще как создаст и даже ругаться не будет.


Попробуйте. Но уверяю вас,

struct {
    char data[];
} abc;

struct {
   char *data;
} bbc;


С точки зрения компилятора совершенно разные структуры. Размер второй он знает точно. А на счет размера первой у него нет никаких мыслей. И это абсолютно правильно.
q->raw[q->regNumbers]
Выглядит словно выход за пределы массива. Хотя, там будет 1-й байт CRC, по идее.
Так и есть. С небольшой поправкой — не первый байт, а сама CRC. У raw[] тип uint16_t. В комментах выше об этом написано. Впрочем — еще раз повторюсь — это уже не важно. Если мы взяли пакет на обработку, то CRC в нем уже проверено.

И еще — вы абсолютно правы — ножик острый. И не выход за пределы массива целиком и полностью ответственность автора. А не компилятора, или еще каких-то средств контроля (которые не бесплатны в части производительности и расхода памяти). Привет низкоуровневые языки. Но именно за то их и любят.
На самом деле в Хорошей реализации может быть даже существенно больше.
У меня сейчас в «опекунстве» в том числе интересный прибор — С2000-ПП — преобразование из событийного протокола (охранно-пожарного) в Modbus. И в нём реализованы тоже интересные механизмы — запрос буфера событий (до 256 событий в кольцевом буфере) 6-ю регистрами к примеру.
Проблемы с такими устройствами у пользователей существенная — не всегда ПЛК или опросчик (OPC сервер или программа) может полностью удовлетворить условию обработки данных и ошибок. Нередко опрос пытаются свести к простому перебору регистров в неизвестном последовательном порядке.
Год назад видел датчик газа — у него один регистр. первый бит отвечает за флаг сработки, второй бит за флаг настройки, дальше из 8-и бит надо собрать число которое будет уставкой, следующие биты уже не помню, но в целом документацию читать было достаточно весело.

Эхх, я даже недавно статью сюда на эту тему написал в песочницу, но ещё не изучил форматирование статей тут и, естественно не оформил.
Очень интересно. Буду ждать статью.
Когда я ковырял С2000-ПП, то кажется не все регистры отзывались на функции единичного/множественного чтения…

Год назад видел датчик газа… — Этих российских датчиков с проприетарными протоколами столько, что пора делать фестивали протоколов и присуждать премии по номинациям =/.
Более того, для получения некоторой информации от С2000-ПП
нужно сначала записать значение в один регистр, потом подождать и прочитать значении в другом регистре.
Ну или с событиями — сначала прочитать один регистр — самого последнего события, потом другой — первого события, в зависимости от того, что там получилось — записать в третий регистр номер события который хочется прочитать, потом читать от 4-го регистра некоторое количество байт. количество сильно зависит от того, что в храниться в предыдущем регистре события.

вполне себе modbus. у я как нибудь оформлю это в статью. ну с третьего раза в песочницу принять могут. наверно)
Sign up to leave a comment.

Articles