Передача данных с коррекцией ошибок

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

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

Простейший вариант – добавить к данным надёжную контрольную сумму и, в случае обнаружения ошибки, перезапросить отправку пакета, но это не всегда возможно. Решением здесь может стать использование для передачи данных т.н. корректирующих (помехоустойчивых) кодов, специально предназначенных для обнаружения и исправления ошибок передачи (англ. ECC — «error-correcting code», например, если Вы видели маркировку ECC на платах памяти, то это именно оно и есть).

Я не буду здесь останавливаться на лежащей за этими кодами математике. Она очень красива, но это тема отдельной большой статьи, а скорее - серии статей. Те, кому интересны математические подробности и соответствующая теория, могут почитать фундаментальную книгу Питерсона и Уэлдона «Коды, исправляющие ошибки». Изд. «Мир», 1976, которая доступна в сети, или же начать со статьи на Хабре.

Здесь же я опишу небольшую библиотеку, реализующую кодировку Рида-Соломона – одного из наиболее распространенных корректирующих кодов.

Почему именно кодировка Рида-Соломона? Сам я не считаю себя особо компетентным в этих вопросах, поэтому просто положился на мнение специалистов, которые выбрали именно эту кодировку для передачи данных с аппарата «Вояджер» на расстояние в двадцать миллиардов километров, для корректировки ошибок в CD-ROM и очень много для чего ещё.

Если нужно передавать данные с большими скоростями, то лучше использовать аппаратные кодировщики/декодировщики (например, IPC4507, CS3110/12, SAA7207H и ещё много, поищите Reed-Solomon encoder decoder IC datasheet), но это не самые дешёвые микросхемы, да и использовать их на скоростях передачи в десятки килобод – из пушки по воробьям, они, по большей части, на мегабоды заточены.

Суть применения метода Рида-Соломона

Чтобы воспользоваться кодировкой Рида-Соломона, необходимо заранее оценить количество ошибок, которое может возникнуть на Вашей линии связи (обозначим его через E) при передаче сообщения длинны N. Тогда, если при передаче в сообщение вкрадутся E или меньше ошибок, то Вы сможете полностью, гарантированно восстановить исходное сообщение. При этом передаваемое сообщение будет содержать P=N-2xE байтов полезной информации и 2хЕ служебных байтов.

Как оценить величину E? Да как сможете. Могу рассказать, как я это делал при решении задачи, которая и привела меня к созданию этой библиотеки.

Мне нужно было передавать пакеты размером в P=23 байта. Я выбрал «окно» размером в 64 байта (оставил запас на «служебную информацию) и запустил «бесконечную» передачу (заведомо зная что именно я передаю). На принимающей стороне фиксировал ошибки. Тест длился целую неделю. В результате я узнал, что за неделю непрерывной работы, максимальное количество ошибок на 64 байта сообщения составило 6. Т.е. никакие подряд идущие 64 байта не содержали более шести ошибок. Поскольку коэффициент Мэрфи обычно принимают равным π, я выбрал E равным 19 (6xπ) и, думаю, что этого хватит. Таким образом, я передаю пакеты в 23 + 19х2 = 61 байт и, если количество ошибок на пакет не превысит 19, у меня всё будет в порядке – я восстановлю исходное сообщение.

Итак, когда известны допустимое количество ошибок (E) и размер полезных данных (P), необходимо закодировать сообщение (получится пакет размера P+2хE) и передать его. А на принимающей стороне раскодировать. Если при этом ошибок оказалось не больше, чем E, то сообщение будет полностью восстановлено. Если же их окажется больше, то Вы по, крайней мере, об этом узнаете (не примете неудачно раскодированное сообщение за правильное). Последнее утверждение имеет одно исключение, см. раздел «Где лежат грабли» ниже.

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

Базовые функции библиотеки:

1// Кодирование
2RS_RESULTS ReedSolomonEncoding::rsEncode(void *buf, const uint8_t E, const uint8_t bufSize);
3 
4// Декодирование
5int ReedSolomonEncoding::rsDecode(void *buf, const uint8_t E, const uint8_t bufSize);

Параметры у них одинаковые:

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

Е - допустимое количество ошибок

bufSize - размер буфера (целиком, включая и полезную, и служебную информацию).

Возвращаемые значения:

rsEncode - возвращает одно из следующих значений:

RSE_NO_ERROR - всё в порядке

RSE_NOT_ENOUGH_MEMORY - не хватило памяти для выполнения операции (см. ниже)

RSE_BUFFER_TOO_SMALL - буфер слишком мал (меньше 3-х байтов)

RSE_LARGE_AMOUNT_OF_ERRORS -величина Е слишком велика для указанного размера буфера

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

RSE_TOO_MANY_ERRORS - пакет содержит более, чем E ошибок

RSE_NOT_ENOUGH_MEMORY - не хватило памяти для выполнения операции (см. ниже)

RSE_BUFFER_TOO_SMALL - буфер слишком мал (меньше 3-х байтов)

RSE_LARGE_AMOUNT_OF_ERRORS -величина Е слишком велика для указанного размера буфера

Для работы функциям нужна дополнительная память которую они динамически запрашивают (а перед выходом освобождают). Размер запрашиваемой зависит от Е и равен: для rsEncode - 7+4хЕ, а для rsDecode - 20+14хЕ байтов. Если память выделить не удалось, функции вернут RSE_NOT_ENOUGH_MEMORY. Это лучше бы всегда проверять.

Ну, теперь мы готовы запустить первый пример. Чтобы не усложнять, здесь опущены проверки на возвращаемые значения. В примерах же, прилагаемых к библиотеке всё делается как надо.

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

Мне очень не нравится строка №12. Непонятно, зачем служебная информация располагается перед полезной, а не после (и какой идиот так сделал!). Если бы она была после, то строку №12 можно было бы просто выбросить, чтобы мозг не выносила. Но, уж, как сделано, так сделано, переделывать лень.

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

Синтаксический сахар:

который состоит из трёх макросов для формирования правильного буфера и двух функций «обёрток» для наших базовых функций.

Макрос DECLARE_PAYLOAD(name, type, errors)

Создание буфера. Первый параметр – имя переменной, которую нужно создать (собственно имя будущего буфера). Второй параметр – тип данных полезного сообщения. И третий параметр – выбранная величина E.

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

Тип может быть почти любым. Не допускаются виртуальные методы и наличие неумолчательного конструктора.

Макрос создаст переменную с именем, которое задано первым параметром у которой будет поле payload. Это поле будет иметь тип, указанный вторым параметром, и именно через него Вы можете заполнять пакет полезной информацией (и вытаскивать информацию после декодирования на принимающей стороне).

Заметьте, что данный макрос не позволяет инициализировать полезные данные при объявлении. Их можно задать только во время выполнения через поле payload.

Макрос DECLARE_PAYLOAD_INIT(name, type, errors)

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

Сразу же после данных для инициализации, следует вызвать макрос END_OF_DECLARE_PAYLOAD_INIT, который и завершит описание. У макроса END_OF_DECLARE_PAYLOAD_INIT параметров нет.

Поскольку данные здесь инициализируется, используемый тип имеет право иметь неумолчательный конструктор. Виртуальные методы по-прежнему запрещены.

Обёрточные функции называются rsEncode и rsEncode (вызываются БЕЗ указания пространства имён). У обеих единственный параметр – переменная объявленная одним из макросов DECLARE_PAYLOAD или DECLARE_PAYLOAD_INIT. Возвращают они те же самые значения, что и соответствующие базовые функции, описанные выше.

Чтобы показать, насколько эти макросы и обёрточные функции упрощают жизнь, запишем вышеразобранный пример с использованием макроса DECLARE_PAYLOAD

а теперь с использованием макроса DECLARE_PAYLOAD_INIT

Проще ведь, чем было с базовыми функциями?

Где лежат грабли

Выше было сказано, что метод достаточно безопасен. Если ошибок при передаче окажется не больше, чем Е, то всё восстановится, а если больше, то функция декодирования вернёт RSE_TOO_MANY_ERRORS, т.е. ошибка не проскочит незамеченной.

Встаёт вопрос, а может ли получиться так, что ошибки (в количестве, большем, чем Е) совпадут таким образом, что сообщение правильно декодируется, но с перевранными данными? Ответ - теоретически возможно, но вероятность этого такая же, как того, что мой кот Матвеич «натопчет» на клавиатуре точную копию этого поста. В принципе, конечно, возможно.

Но грабли таки есть! Дело в том, что если полезная часть пакеты полностью состоит из нулей, то и служебная будет содержать только нули. И тут уж возможны варианты! Например, если Вы на принимающей стороне объявите буфер глобально (он будет заполнен нулями), а приём у Вас по каким-то причинам сорвётся - вообще ничего не примете, то такой заполненный нулями буфер успешно декодируется, покажет ноль ошибок и радостно выдаст Вам эти нули в качестве результата. Чтобы этого не случилось, следите, чтобы разумное сообщение никогда не состояло только из нулей. Например, добавьте к нему один байтик и всегда пихайте туда единицу. Тогда на принимающей стороне, Вы, проверив эту единицу, всегда сможете отличить реально пришедшие нули от ошибочных.

Ну, собственно и всё.

Библиотека живёт на гитверзе. К ней прилагаются семь примеров:

1. Передача числа типа long с использованием DECLARE_PAYLOAD_INIT
2. Передача числа типа long с использованием DECLARE_PAYLOAD
3. Передача массива из 10 элементов типа int с использованием DECLARE_PAYLOAD_INIT
4. Передача массива из 10 элементов типа int с использованием DECLARE_PAYLOAD
5. Передача сложной (с блэкджеком и девочками) структуры с использованием DECLARE_PAYLOAD_INIT
6, Передача сложной (с блэкджеком и девочками) структуры с использованием DECLARE_PAYLOAD
7. Передача русского текста с использованием только базовых функций

Чистых Вам линий!

ddr2
Offline
Зарегистрирован: 27.12.2020

23 + 19х2 = 61 байт

это почти 3 сообщения вместо одного. Это видимо три вложенные копии сообщения в одном, а потёртые данные восстановятся из других копий. Это особенно важно когда телеграмма идёт с орбиты Нептуна, так как нельзя быстро передать потерянное сообщение ещё раз. 

b707
Offline
Зарегистрирован: 26.05.2017

ddr2 пишет:

23 + 19х2 = 61 байт

это почти 3 сообщения вместо одного.

это абсолютно нормальное соотношение для протокола с защитой от ошибок.

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

b707 пишет:

ddr2 пишет:

23 + 19х2 = 61 байт

это почти 3 сообщения вместо одного.

это абсолютно нормальное соотношение для протокола с защитой от ошибок.

на текущий момент не нормальное, для низкоуровневых сигналов Нобелевский лауреат Joseph Taylor придумал FT-8...

Особенности FT8:
 

  • Длина сообщения 15с, сообщения передаются в фиксированные тайм-слоты, что облегчает декодирование.
  • Длина сообщения 77 бит + 12-бит CRC.
  • Коррекция ошибок FEC LDPC(174,87).
  • Частотная модуляция 8-FSK, расстояние между тонами 6.25Гц.
  • Ширина полосы 50Гц. При такой узкой полосе может одновременно работать и декодироваться множество станций.
  • Порог декодирования -20дБ.
  • Опциональная возможность автоматической работы, автоматической отправки ответов и пр.

 

То-есть, декодируется сигнал ниже уровня шума на 20 децибел

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

ua6em пишет:
придумал FT-8...
Выкладывайте библиотеку. Потестим :-)

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

ЕвгенийП пишет:

ua6em пишет:
придумал FT-8...
Выкладывайте библиотеку. Потестим :-)

у меня нет ссылки на неё, использую готовую программу, попробую поискать, на хабре об FT-8
ЗЫ там же и ссылка оказалась на библиотеку

 

mykaida
mykaida аватар
Offline
Зарегистрирован: 12.07.2018

ua6em пишет:

Особенности FT8:

  • Длина сообщения 15с, сообщения передаются в фиксированные тайм-слоты, что облегчает декодирование.
  • Длина сообщения 77 бит + 12-бит CRC.
  • Коррекция ошибок FEC LDPC(174,87).
  • Частотная модуляция 8-FSK, расстояние между тонами 6.25Гц.
  • Ширина полосы 50Гц. При такой узкой полосе может одновременно работать и декодироваться множество станций.
  • Порог декодирования -20дБ.
  • Опциональная возможность автоматической работы, автоматической отправки ответов и пр.

 

То-есть, декодируется сигнал ниже уровня шума на 20 децибел

 

Чет не то. То что я прочитал про этот протокол: 77бит + 14бит CRC дальше с избыточностью получается 174бита.

Итого - на 77бит данных 174бита передаваемой информации.

Причем на том же хабре, но чуть ниже.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Не, столько ресурсов на декодирование - это и я умею. Вы давайте такую, чтобы нанка потянула.

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

ЕвгенийП пишет:

Не, столько ресурсов на декодирование - это и я умею. Вы давайте такую, чтобы нанка потянула.

других нету  или я плохо искал )))

ЗЫ почитал, да FEC LDPC это жесть
 

1FEC код работает в двух режимах, так называетмые short FECFRAME и normal FECFRAME. Отличие в длине выхода. Кодовый блок LDPC для короткого фрейма 16200 байт, для длинного 64800. Для передачи «полезной» информации (видео, радио) используют нормальные фреймы. Для служебной информацией, для которой совсем критично время, используют короткие фреймы.
2Но и это ещё не всё: для короткого фрейма существует 10 режимов с различными скоростями для БЧХ и LDPC кодирования, для нормального 11 вариантов. Итого 21 вариант FEC кода! Кому интересно подробнее — см Table 5a и Table 5b в [2].

 

mykaida
mykaida аватар
Offline
Зарегистрирован: 12.07.2018

ЕвгенийП пишет:

Этот материал для тех, у кого возникает проблема неуверенного приёма цифрового сигнала 

А библиотечка интересная. Доберусь до nRF24 - протестирую на максимальной дальности.

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

mykaida пишет:

ЕвгенийП пишет:

Этот материал для тех, у кого возникает проблема неуверенного приёма цифрового сигнала 

А библиотечка интересная. Доберусь до nRF24 - протестирую на максимальной дальности.

да я и так тебе скажу, 1 ватт 400 км

mykaida
mykaida аватар
Offline
Зарегистрирован: 12.07.2018

ua6em пишет:

да я и так тебе скажу, 1 ватт 400 км

2,4ГГц и 400км. Чего-то сомнительно. Дай бог 1км

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

ua6em пишет:

b707 пишет:

ddr2 пишет:

23 + 19х2 = 61 байт

это почти 3 сообщения вместо одного.

это абсолютно нормальное соотношение для протокола с защитой от ошибок.

на текущий момент не нормальное, для низкоуровневых сигналов Нобелевский лауреат Joseph Taylor придумал FT-8...

Особенности FT8:
 

  • Длина сообщения 15с, сообщения передаются в фиксированные тайм-слоты, что облегчает декодирование.
  • Длина сообщения 77 бит + 12-бит CRC.
  • Коррекция ошибок FEC LDPC(174,87).
  • Частотная модуляция 8-FSK, расстояние между тонами 6.25Гц.
  • Ширина полосы 50Гц. При такой узкой полосе может одновременно работать и декодироваться множество станций.
  • Порог декодирования -20дБ.
  • Опциональная возможность автоматической работы, автоматической отправки ответов и пр.

 

То-есть, декодируется сигнал ниже уровня шума на 20 децибел

 

1. Какие низкоуровневые сигналы? Какие тайм-слоты? В каких единицах измеряется длина сообщения? Какая частотная модуляция? Какая ширина полосы? Какие децибелы? Вы вообще о чем? У Вас поток байтов на входе и поток байтов на выходе, остальное несущественно.

2. Не нравится - выложите свою библиотеку. Которая лучше. Только - в своей теме. И Вам скажут спасибо. А придираться к тому, что пишущая машинка не печет блинов - по меньшей мере не умно.

ddr2
Offline
Зарегистрирован: 27.12.2020

кстати, FT8 - это аналог преобразования фурье.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

ddr2 пишет:

23 + 19х2 = 61 байт

это почти 3 сообщения вместо одного.  

Ну, я видел 6 ошибок и взял коэффициент Мэрфи равным Пи, отсюда 19. Взял бы поменьше, было бы меньше. Меня в данной задаче это вполне устраивает.

ddr2 пишет:

Это видимо три вложенные копии сообщения в одном, а потёртые данные восстановятся из других копий. 

Да, Господь с Вами. Если бы это было так, то достаточно было бы трёх ошибок в "правильных" местах, чтобы всё свалилось. Но оно же не валится не только от трёх, в плоть до 19-ти независимо от того в какие места они попали. Тут гораздо более интересная математика.

ddr2 пишет:

нельзя быстро передать потерянное сообщение ещё раз. 

Если конкретно у меня и сейчас, то да - тот самый случай. Связь односторонняя. Передать-то ещё раз можно, а вот запросить такой повтор - никак.

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

mykaida пишет:

ua6em пишет:

да я и так тебе скажу, 1 ватт 400 км

2,4ГГц и 400км. Чего-то сомнительно. Дай бог 1км

более 35000 Км и 12 ватт не сомнительно? )))

ddr2
Offline
Зарегистрирован: 27.12.2020

ЕвгенийП пишет:
ddr2 пишет:
 Это видимо три вложенные копии сообщения в одном, а потёртые данные восстановятся из других копий. 

Да, Господь с Вами. Если бы это было так, то достаточно было бы трёх ошибок в "правильных" местах, чтобы всё свалилось. Но оно же не валится не только от трёх, в плоть до 19-ти независимо от того в какие места они попали. Тут гораздо более интересная математика.

Как-то сомнительно про 19 независимых ошибок, может неполное восстановление? Вы тестировали на 19 ошибок случайной выборкой(в том числе входных значений)? Но конечно всё зависит от эффективности алгоритма сжатия, можно упаковать и больше 3-х.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

ddr2 пишет:

Как-то сомнительно про 19 независимых ошибок, может неполное восстановление? Вы тестировали на 19 ошибок случайной выборкой(в том числе входных значений)? Но конечно всё зависит от эффективности алгоритма сжатия, можно упаковать и больше 3-х.

Это вот о чем сейчас было?

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

ddr2 пишет:
Как-то сомнительно про 19 независимых ошибок, может неполное восстановление?
Этот метод либо восстанавливает полностью, либо никак - математика у него такая. Если ошибок не более Е, то полностью.

ddr2 пишет:
Вы тестировали на 19 ошибок случайной выборкой(в том числе входных значений)?
В примерах к библиотеке и на 30 ошибок тесты есть.

Также Вам никто не мешает потестировать самому. Но ...

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

ddr2
Offline
Зарегистрирован: 27.12.2020

да, я уже проверил, действительно работает, до 20 ошибок, сообщение 23, пакет 61. Спасибо было интересно.  

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

Интересно, а в регистровой памяти на один байт добавлялся всего один бит, видимо алгоритм был другой?

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

ua6em пишет:

Интересно, а в регистровой памяти на один байт добавлялся всего один бит, видимо алгоритм был другой?

Это вообще о чем? О бите четности?

(Особенно, учитывая, что "регистровая память" - устаревший термин, современный аналог - кеш.)

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

andriano пишет:

ua6em пишет:

Интересно, а в регистровой памяти на один байт добавлялся всего один бит, видимо алгоритм был другой?

Это вообще о чем? О бите четности?

(Особенно, учитывая, что "регистровая память" - устаревший термин, современный аналог - кеш.)

под регистровой RAM подразумевается  память с ECC (простонародное название придумал не я)

mykaida
mykaida аватар
Offline
Зарегистрирован: 12.07.2018

Господа - почитайте подпрограмму!

Реально интересное решение, хоть и небезспорное. Но компактное. И логарифм в прогмеме. Не проверял, сколько кушает, но по поверхностной оценке - почти ничего. А в передаче данных - там 2-4 раза больше. Кто их считает (особенно коллизии)?

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

mykaida пишет:

почитайте подпрограмму!

Вы не поверите, но я читал :-)

mykaida
mykaida аватар
Offline
Зарегистрирован: 12.07.2018

ЕвгенийП пишет:
Вы не поверите, но я читал :-)

Да поверю, потому что:

Все животные равны, но некоторые животные более равны, чем другие :)