Комментарии 45
Достаточно красивая имплементация, теперь предлагаю ответить на вопрос, что будет, если подряд встретятся несколько байтов 0x23, который выбран как стартовый байт. Для упрощения жизни, много лет назад придумали такую штуку, как ESC последовательности и byte stuffing
Очень рекомендую посмотреть имплементацию протокола wake и slip, как источник вдохновения.
Если отправить несколько стартовых байтов, то на стороне приемника ничего не произойдет, так как количество байт в пакете по умолчанию 8. Если отправить 8 стартовых байтов, то приемник попытается прочесть пакет, но не пройдет проверка CRC.
Например, сделайте началом пакета 0xFA+<байт с установленным старшим битом>, а все встретившиеся 0xFA заменяйте на 0xFA+0x05. И будет вам праздник. Можете даже в этом первом байте в семи оставшихся битах что-то полезное кодировать — код команды, длину пакеты или ещё что-то, а сочетания 0xFA+<байт с нулевым старшим битом> использовать для какой-то дополнительной сигнализации (конец пакета, например, чтобы длину не передавать) И всё — протокол гарантированно восстанавливается после сбоя на следующем же пакете.
Интересная идея. Возможно попробую так сделать в следующей версии
Заголовочный байт + контрольная сумма + кольцевой буфер так же гарантированно восстанавливают следующий же пакет.
Представьте все то же самое со своим решением. И я вас умоляю, с современными процессорами посчитать контрольную сумму аппаратно - вообще не вопрос.
У вас устройство может представлять из себя микроконтроллер с частотой 35 МГц и без всяких аппаратных подсчётов CRC. И чтобы что-то кто-то «аппаратно» посчитатл, то кто-то эти данные в этот блок расчёта должен передать, а потом дождаться результата. Ваш алгоритм плох абсолютно со всех сторон. Даже на «больших» процессорах это будет гемор и куча лишней работы.
Объясните мне вменяемо, почему я должен выбирать в качестве сигнатуры 0xFF, и откуда появится 255 байт (к чему бы, кстати?) именно сигнатурных байт, и тогда я готов выслушать все остальные претензии.
Кстати, судя по всему, вы вообще не в курсе, как считается контрольная сумма аппаратными средствами в современных контроллерах, даже тех, у которых всего-то 48МГц в максимуме.
И всё зачем? Чтобы не делать простейшего байт-стафинга, где ни данных от уже посчитанной контрольной суммы хранить не нужно, и время обработки пакета получается предсказуемое. Можно даже на лету пакет или какие-то его части разбирать, что сильно упрощает программу при использовании для приёма чередующихся DMA-буферов. Можно например по 32 байта буферами принимать и иметь только один буфер для корректно-принятого пакета.
Мой опыт подсказывает, что самый надежный и удобный способ определить конец/начало пакета - это пауза на линии. Тем более, что современные микроконтроллеры поддерживают детектирование и обработку паузы на аппаратном уровне.
В таком случае разве не снижается пропускная способность канала ?
Снижается и еще как.
Кстати, в UART предусмотрен специальный сигнал "break condition" который часто используют для того, что бы оповестить приемную сторону о начале новой последовательности данных или об окончании предидущей. Например, протокол DMX512.
А если внимательно посмотреть, то на на самом деле, так как ошибка стартового бита приводит к потере синхронизации, то синхронизация гарантированно восстанавливается на определённой последовательности байт в непрерывном потоке ;) Конкретная последовательность зависит от параметров обмена. Во многих случаях это просто одиночный байт 0xFF, который гарантированно восстаналивает синхронизацию на стартовом бите следующего байта.
А если копать ещё глубже, то есть байты, и сочетания байт, которые повышают вероятность корректного восстановления синхронизации (т.е.можно доказать что при случайной ошибке синхронизация восстановится, например, в 80% случаев, а в 20% придётся ждать паузы в обмене данными).
Паузы это плохо по той причине, что при быстром обмене их сложно реализовывать, а при обмене с PC, так, и вообще небольшие паузы нельзя гарантировать. Если возможен длительный непрерывный поток данных и нужно гарантированное время восстановления, то проще определить последовательность байт, которая гарантированно восстанавливает синхронизацию и слать эти байты в начале/конце пакета. И программы упрощаются, и гарантия восстановления, и средняя скорость обмена получается выше.
А еще мой опыт показывает, что разработка на самом пределе технических возможностей сущности - это bad practices.
Это очень плохое решение. Лучший способ — маркеры и байт-стафинг.
А контроллерах, измерение интервалов ломает работу с DMA, так как вы не знаете времени окончания приёма, например. Да, и вообще, лего сказать «таймеры». В сложной программе это выливается в лишние танцы с бубном, особенно на высоких скоростях.
У байт-стафинга вообще нет недостатков при обмене запрос-ответ, либо при выделенном мастере. А при полностью асинхронном обмене, без чётности, либо с чётной чётностью байт 0xFF восстанавливает битовую синхронизацию.
Вопрос — городить огород с задержками и таймерами, если есть более простые и гарантированно работающие решения?
А еще проще взять уже готовую библиотеку Modbus и не париться.
Modbus - транзакционный протокол: один запрос, один ответ. На протокол пакетной передачи не тянет, так как не позволяет пересылать запросы независимо от ответов. Помимо этого в Modbus-е конец передачи определяется паузой в передаче, что для интенсивного обмена совсем "не айс".
Но похоже, что для задачи решаемой автором статьи даже Modbus-а более чем позаглаза. :)
3,5 символа паузы на фоне общей скорости передачи линии связи практически не сказывается на производительности. А необходимость ответов на запросы может определятся логикой приложения (на широковещательные запросы ведомые устройства не отвечают).
К тому же Modbus позволяет расширять систему команд за счет пользовательских кодов функций, которые могут быть вообще любого формата.
Капитан Ясен Куй, ведь приемопередатчиков на линии может быть ого-го, а автор чпокается с двумя приемопередатчиками . Но если добавить в линию ещё кого нить то мега "протокол" автора превратится в генератор белого шума).
Когда в линии несколько абонентов (типичная ситуация для RS485), надо или разрабатывать протокол разрешения коллизий, или, что проще в реализации, переходить на опросный режим master-slave. Т.е. 1 узел всегда master, сеанс связи с любым другим узлом может инициировать только он. Все остальные - slave. Даже если им "есть что сказать", сидят и молчат в тряпочку пока их не спросят.
Либо используйте MODBUS ASCII, но это очень медленно.
А ещё обратите внимание, что в модбасе есть проблема с тем, чтоб различить запрос и ответ — MODBUS на полудуплексной линии (RS422) это боль и страдания.
По возможности избегайте использовать Mobdus.
Протоколы 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-м (сначала я все что на ПК работает писал, потом это разрослось и занимался главным образом разработкой микроядра, клиентов делал другой человек).
Не поверите, но именно под Win все это и работало. И действительно работало и все еще работает.
Правда, в полной версии ушли на архитектуру ПК - IP шлюзы по UDP, IP-шлюз - УСО по RS-485. Но это требования заказчика - им нужна была распределенная система чтобы из одной точки контролировать много объектов очень сильно разнесенных в пространстве.
"Размах" полной системы - порядка 20-ти IP шлюзов, на каждом от 5-ти до 30-ти УСО (зависиит от конфигурации каждого конкретного "куста"). Общее количество конечных устройств в системе порядка 1000 (точно затруднюсь зазвать цифру)
А лайтовая версия для локальных объектов по прежнему работает вся на 485. Там нет IP шлюзов, несколько УСО по 485 (через преобразователь RS-485-USB) подключаются непосредственно к ПК.
В лайтовой версии, конечно, поменьше - на то она и лайтовая. Там 2-3 УСО и устройств пара десятков всего.
Нормальная заметка, пусть реализация и хромает. Прочитал из любопытства. Всяко лучше рекламной пурги и/или машинных переводов, которые хабр заполонили. Автор, не отчаивайтесь!.. :) Главное анализировать свои действия и вовремя вносить правки. Ведь как известно: профессионал - этот тот же дилетант, но знающий где может ошибиться. Удачи в разработке!
ЗЫ: Для каких железок, вы это реализуете?
Добрый день как с вами связаться ?
Пакетный обмен данными по UART