Pull to refresh

Comments 45

Достаточно красивая имплементация, теперь предлагаю ответить на вопрос, что будет, если подряд встретятся несколько байтов 0x23, который выбран как стартовый байт. Для упрощения жизни, много лет назад придумали такую штуку, как ESC последовательности и byte stuffing

Очень рекомендую посмотреть имплементацию протокола wake и slip, как источник вдохновения.

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

автор в очередной раз изобретает велосипед)

хотя это полезно для мозга и бесполезно практически)

А чего бы сразу byte-stuffing не сделать и не мучаться больше? У вас при плотном обмене неизвестное время восстановления получается, вплоть до бесконечного.
Например, сделайте началом пакета 0xFA+<байт с установленным старшим битом>, а все встретившиеся 0xFA заменяйте на 0xFA+0x05. И будет вам праздник. Можете даже в этом первом байте в семи оставшихся битах что-то полезное кодировать — код команды, длину пакеты или ещё что-то, а сочетания 0xFA+<байт с нулевым старшим битом> использовать для какой-то дополнительной сигнализации (конец пакета, например, чтобы длину не передавать) И всё — протокол гарантированно восстанавливается после сбоя на следующем же пакете.

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

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

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

много лишних телодвижений и крайне не эффективно. Представдье, что у вас заголовочный байт 0xFF, «битый» пакет с полем данных длинной 255 байт и в поле данных заполнено 0xff. Сколько раз вы посчитаете контрольную сумму до восстановления синхронизации и где будете искать новую работу если такое пойдёт в продакшн на контроллерах?

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

Вы случаем не Web-программист, чтобы такие заявления делать? Тогда посчитайте мне CRC на JS «аппаратно» — я посмеюсь.
У вас устройство может представлять из себя микроконтроллер с частотой 35 МГц и без всяких аппаратных подсчётов CRC. И чтобы что-то кто-то «аппаратно» посчитатл, то кто-то эти данные в этот блок расчёта должен передать, а потом дождаться результата. Ваш алгоритм плох абсолютно со всех сторон. Даже на «больших» процессорах это будет гемор и куча лишней работы.

Объясните мне вменяемо, почему я должен выбирать в качестве сигнатуры 0xFF, и откуда появится 255 байт (к чему бы, кстати?) именно сигнатурных байт, и тогда я готов выслушать все остальные претензии.

Кстати, судя по всему, вы вообще не в курсе, как считается контрольная сумма аппаратными средствами в современных контроллерах, даже тех, у которых всего-то 48МГц в максимуме.

255 сигнатурных байт, это пример. Если их будет 16, то всё равно, будет неприятно. Я очень дажде в курсе, но чтобы пересчитать контрольную сумму, вам всё равно нужно передать все данные блока. И даже DMA к CRC тут не сильно поможет при реальных последовательных интерфейсов. Т.е. при потере синхронизации, вы предлагаете на каждый сигнатурный байт анализировать и разбирать заголовок и считать CRC. Да ещё и хранить сырые принятые уже «обработанные» данные на случай потери синхронизации. Совершенно ужасный алгоритм для встраиваемых систем. У вас и память лишняя расходуется, и какие-то кольцевые буфера городить нужно (ну, или резервировать более двойного размер пакета под буфер приёма), и худшее время обработки пакета получается огромным относительно нормального. Т.е. если вы обычный пакет, например, обрабатываете 0.1 мс, то худший случай у вас запросто получается в сотни раз дольше. А резервировать время обработки пакета нужно исходя из худшего случая.
И всё зачем? Чтобы не делать простейшего байт-стафинга, где ни данных от уже посчитанной контрольной суммы хранить не нужно, и время обработки пакета получается предсказуемое. Можно даже на лету пакет или какие-то его части разбирать, что сильно упрощает программу при использовании для приёма чередующихся DMA-буферов. Можно например по 32 байта буферами принимать и иметь только один буфер для корректно-принятого пакета.

Мой опыт подсказывает, что самый надежный и удобный способ определить конец/начало пакета - это пауза на линии. Тем более, что современные микроконтроллеры поддерживают детектирование и обработку паузы на аппаратном уровне.

В таком случае разве не снижается пропускная способность канала ?

Снижается и еще как.

Кстати, в UART предусмотрен специальный сигнал "break condition" который часто используют для того, что бы оповестить приемную сторону о начале новой последовательности данных или об окончании предидущей. Например, протокол DMX512.

Преднамеренно вставляемая после окончания пакета пауза на линии при асинхронной передаче нужна в любом случае, для восстановления битовой синхронизации в случае ошибочного начала приёма пакета, вызванного шумом в случайной паузе.
Если у вас запрос-ответ или есть естественные паузы в обмене, то не нужна. Ну, и стартовый маркер лучше выбирать 0xF0 и правильно использовать контроль чётности.
В случае обмена запрос-ответ с несколькими ведомыми пауза не нужна перед ответом, но нужна перед запросом. Стартовый маркер 0xF0 не поможет аппаратному асинхронному приёмнику восстановить битовую синхронизацию в непрерывном потоке данных — лучше заменить стартовый маркер на паузу, если стоит задача максимально использовать пропускную способность канала. Естественные паузы в обмене обычно имеют случайную длительность, поэтому понадеявшись на них можно получить большое максимальное время доставки даже в малошумном канале.
Это кажется, что синхронизация не восстанавливается ;)
А если внимательно посмотреть, то на на самом деле, так как ошибка стартового бита приводит к потере синхронизации, то синхронизация гарантированно восстанавливается на определённой последовательности байт в непрерывном потоке ;) Конкретная последовательность зависит от параметров обмена. Во многих случаях это просто одиночный байт 0xFF, который гарантированно восстаналивает синхронизацию на стартовом бите следующего байта.
А если копать ещё глубже, то есть байты, и сочетания байт, которые повышают вероятность корректного восстановления синхронизации (т.е.можно доказать что при случайной ошибке синхронизация восстановится, например, в 80% случаев, а в 20% придётся ждать паузы в обмене данными).
Паузы это плохо по той причине, что при быстром обмене их сложно реализовывать, а при обмене с PC, так, и вообще небольшие паузы нельзя гарантировать. Если возможен длительный непрерывный поток данных и нужно гарантированное время восстановления, то проще определить последовательность байт, которая гарантированно восстанавливает синхронизацию и слать эти байты в начале/конце пакета. И программы упрощаются, и гарантия восстановления, и средняя скорость обмена получается выше.

А еще мой опыт показывает, что разработка на самом пределе технических возможностей сущности - это bad practices.

UFO just landed and posted this here
А как гарантировать небольшую паузу под Windows/Linux? Или пауза будет 100мс и пропускная способность максимум 10 пакетов в секунду на мегабитном канале?
Это очень плохое решение. Лучший способ — маркеры и байт-стафинг.
Так же, как и везде — использовать программируемый аппаратный таймер, или программно-аппаратный таймер на его основе в драйвере. Маркеры и байт-стафинг — хорошее решение для синхронного последовательного обмена, но не для асинхронного.
В каком драйвере? Вы будете под своё устройство драйвера под Windows/Linux писать? Ха-ха.
А контроллерах, измерение интервалов ломает работу с DMA, так как вы не знаете времени окончания приёма, например. Да, и вообще, лего сказать «таймеры». В сложной программе это выливается в лишние танцы с бубном, особенно на высоких скоростях.
У байт-стафинга вообще нет недостатков при обмене запрос-ответ, либо при выделенном мастере. А при полностью асинхронном обмене, без чётности, либо с чётной чётностью байт 0xFF восстанавливает битовую синхронизацию.
Вопрос — городить огород с задержками и таймерами, если есть более простые и гарантированно работающие решения?

А еще проще взять уже готовую библиотеку Modbus и не париться.

Modbus - транзакционный протокол: один запрос, один ответ. На протокол пакетной передачи не тянет, так как не позволяет пересылать запросы независимо от ответов. Помимо этого в Modbus-е конец передачи определяется паузой в передаче, что для интенсивного обмена совсем "не айс".

Но похоже, что для задачи решаемой автором статьи даже Modbus-а более чем позаглаза. :)

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

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

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

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

Modbus ужасен со всех сторон. Там конец пакета паузой передаётся. Это снижает пропускную способность, сильно усложняет логику работы (правильно выдерживать и измерять паузы на больших скоростях обмена) и очень плохо работает если один из абонентов это компьютер под Windows/Linux, где гарантия временных интервалов на асинхронных интерфейсах работает очень плохо.
Либо используйте MODBUS ASCII, но это очень медленно.
А ещё обратите внимание, что в модбасе есть проблема с тем, чтоб различить запрос и ответ — MODBUS на полудуплексной линии (RS422) это боль и страдания.
По возможности избегайте использовать Mobdus.
Modbus плох тем, что не универсален, и при этом его используют там, где нужен универсальный протокол. В 79-м году, когда он создавался, обновление ПО по последовательному интерфейсу не было обычной практикой, и о веб-интерфейсе для конфигурирования контроллеров никто не задумывался. Он неплох для своей узкой задачи но сейчас этого редко когда бывает достаточно. А чтобы выдерживать и измерять паузы просто нужна аппаратная поддержка в контроллере интерфейса.

Протоколы SLIP, PPP (HDLC) придумали 30 (если не 40) лет назад, они отработаны и опробированы в миллионах телекоммуникационных изделий, написаны и опубликованы сотни библиотек разной степени сложности. Зачем Вы изобретаете велосипед ? Тем более, что проблему Вы таки не решили, а поверхностно закопали (подложили мину себе и своим пользователям).

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

А Вы сами их применяли?
SLIP — не имеет контроля ошибок. Это чистый байт-стафинг и при этом далеко не лучший.
HDLC — мимо кассы, так как полагается на сигнализацию физического уровня и очень ресурсоёмкий при программной реализации.
PPP -108 служебных байт на пакет, из которых 4 имеют неизменной значение? И при этом он для синхронизации полагается на протоколы более низкого уровня. Чем оно тут поможет?
А Вы сами их применяли?

так речь о том «где можно подсмотреть идеи», а не о том, чтобы взять готовое.


SLIP — не имеет контроля ошибок. Это чистый байт-стафинг и при этом далеко не лучший.
HDLC — мимо кассы, так как полагается на сигнализацию физического уровня и очень ресурсоёмкий при программной реализации.
PPP -108 служебных байт на пакет, из которых 4 имеют неизменной значение? И при этом он для синхронизации полагается на протоколы более низкого уровня. Чем оно тут поможет?

статья про написанное вами была бы в 1000 раз полезнее этой статьи об очередном велосипеде.

Какой протокол тогда лучше использовать для связи STM8 и приложения?

сначала о том как не стоит делать по моему мнению:


  • разумеется, ничего такого тяжеловесного как hdlc и ppp использовать не стоит;
  • о протоколах, использующих задержку для определения старта передачи, были уже написаны вполне обоснованные критические комментарии;
  • иногда для маркировки начала передачи используют девятый бит (aka parity), но это опять же потенциальный источник проблем с переносимостью.

экранирование (byte stuffing) не нравится тем, что из сообщения фиксированной длины получается сообщение переменной.


Побайтного занесения данных;

я бы отказался от этого требования, слал по 7 бит на символ, восьмой бит же использовал как признак начала пакета. реализация очень простая, требуется только сдвиг и логическое «или».


вы не привели достаточно сведений: ожидаемый уровень ошибок, какие «повреждения» характерны для реальной линии, насколько нежелательна потеря пакетов; поэтому «лайтовый» вариант:
исходим из того, что в пакете 64 бита данных + минимум 8 бит crc (я очень люблю crc за его способность гарантированно обнаруживать некоторые виды ошибок), что даёт нам 72 бита нагрузки; мы договорились о 7 битах на символ — итого будем использовать 11 символов/77 бит.
5 бит остаются свободными, в них можно запихать какие-то магические числа, например, так:


1010dddd
0ddddddd
...
0ddddccc
110ccccc

(d — полезная нагрузка, c — crc8)


тогда первый байт всегда будет 0xA_, последний 0xC_ или 0xD_, а все остальные будут начинаться на цифру от 0 до 7. это достаточно удобно при отладке (сразу видны границы пакета), да и парсить просто.


код crc8 можно выбрать из тех, которые на сообщениях нужного размера гарантировано детектируют до 4 битовых ошибок (hd=4).


для относительно незашумлённых линий это будет неплохо работать.
не нужно никакой чёрной магии с таймингами при кодировании/раскодировании, а значит без проблем можно использовать usb-контроллеры или передавать по сети.
ну и простота реализации, конечно; самая сложная часть тут — кастомный crc8.


P. S. ещё встречал совет отправлять 0xFF перед посылкой, на первый взгляд выглядит разумно, если нет жёстких требований по утилизации канала, то я бы именно так и поступил (со стороны приёмника это никакой специальной обработки не требует).

А из каких соображений используется фиксированный размер буфера?

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

ПК -> RS232 -> Головной Контроллер (ГК) -> RS485 -> несколько Домовых Контроллеров (ДК), от каждого ДК -> RS485 -> несколько Устройств Сопряжения с Объектом (УСО).

А закончили уже распределенной системой:

Несколько интерфейсных клиентов (разного назначения) -> TCP -> Микроядро системы (вместо бывшего ГК) -> UDP -> несколько IP-шлюзов (вместо бывших ДК), от каждого шлюза -> RS485 -> несколько УСО

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

Структура датаграммы:

<Фиксированый заголовок <Общая длина датаграммы><Адрес (идентификатор) отправителя><Адрес (идентификатор) получателя><Идентификатор датаграммы (уникален для отправителя)><Ключ (команда)>><Блок данных переменной длины, содержимое определяется ключом (командой)><CRC16>

Чтение в три приема - сначала фиксированый заголовок, по нему понятно сколько данных надо прочитать. Дальше чтение данных и потом чтение CRC16.

Дальше - сравнение CRC16.

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

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

Естественно, все это обвешано таймаутами (не полный прием, отсутствие подтверждения и т.п.), алгоритмом перепосылок, переходом в состояние "адресат не отвечает" при установленном количестве неудачных перепосылок и т.п.

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

Начинали ту разработку мы в 93-м, ушел оттуда я в 17-м (сначала я все что на ПК работает писал, потом это разрослось и занимался главным образом разработкой микроядра, клиентов делал другой человек).

Реализуйте это под Windows, например, со всеми таймаутами :)

Не поверите, но именно под Win все это и работало. И действительно работало и все еще работает.

Правда, в полной версии ушли на архитектуру ПК - IP шлюзы по UDP, IP-шлюз - УСО по RS-485. Но это требования заказчика - им нужна была распределенная система чтобы из одной точки контролировать много объектов очень сильно разнесенных в пространстве.

"Размах" полной системы - порядка 20-ти IP шлюзов, на каждом от 5-ти до 30-ти УСО (зависиит от конфигурации каждого конкретного "куста"). Общее количество конечных устройств в системе порядка 1000 (точно затруднюсь зазвать цифру)

А лайтовая версия для локальных объектов по прежнему работает вся на 485. Там нет IP шлюзов, несколько УСО по 485 (через преобразователь RS-485-USB) подключаются непосредственно к ПК.

В лайтовой версии, конечно, поменьше - на то она и лайтовая. Там 2-3 УСО и устройств пара десятков всего.

Нормальная заметка, пусть реализация и хромает. Прочитал из любопытства. Всяко лучше рекламной пурги и/или машинных переводов, которые хабр заполонили. Автор, не отчаивайтесь!.. :) Главное анализировать свои действия и вовремя вносить правки. Ведь как известно: профессионал - этот тот же дилетант, но знающий где может ошибиться. Удачи в разработке!

ЗЫ: Для каких железок, вы это реализуете?

В основном для STM8

Понятно. Что создаете, если не секрет?

Девайс, который будет выступать переходником USB-NRF24L01 + приложение на QML

Sign up to leave a comment.

Articles