Этот материал для тех, у кого возникает проблема неуверенного приёма цифрового сигнала из линии связи или не слишком надёжного хранилища, когда данные приходят с некоторым количеством ошибок. Конечно, проблема проблеме рознь и в первую очередь следует озаботиться улучшением программных и схемных решений, для исключения источников помех и уменьшения их влияния. Но нередки ситуации, когда все средства исчерпаны, а данные всё равно приходят с ошибками.
Простейший вариант – добавить к данным надёжную контрольную сумму и, в случае обнаружения ошибки, перезапросить отправку пакета, но это не всегда возможно. Решением здесь может стать использование для передачи данных т.н. корректирующих (помехоустойчивых) кодов, специально предназначенных для обнаружения и исправления ошибок передачи (англ. 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, то сообщение будет полностью восстановлено. Если же их окажется больше, то Вы по, крайней мере, об этом узнаете (не примете неудачно раскодированное сообщение за правильное). Последнее утверждение имеет одно исключение, см. раздел «Где лежат грабли» ниже.
Библиотека содержит две базовые функции – одну для кодирования и одну для декодирования (кто бы мог подумать!). Кроме того, там есть ещё две функции и три макроса, которые являются обёрткой (синтаксическим сахаром) для базовых и призваны предельно упростить использование библиотеки в простых случаях.
Базовые функции библиотеки:
2 | RS_RESULTS ReedSolomonEncoding::rsEncode( void *buf, const uint8_t E, const uint8_t bufSize); |
5 | int 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. Это лучше бы всегда проверять.
Ну, теперь мы готовы запустить первый пример. Чтобы не усложнять, здесь опущены проверки на возвращаемые значения. В примерах же, прилагаемых к библиотеке всё делается как надо.
07 | #include "ReedSolomon.h" |
09 | #define printVar(x) do { Serial.print(#x "="); Serial.println(x); } while (false) |
12 | 0, 0, 0, 0, 0, 0, 0, 0, |
18 | Serial .println( "Go on!" ); |
22 | ReedSolomonEncoding::rsEncode(buffer, 4, 13); |
33 | const int errorsCorrected = ReedSolomonEncoding::rsDecode(buffer, 4, 13); |
38 | printVar(errorsCorrected); |
Надеюсь, этот пример полностью самодокументирован и в дополнительных комментариях не нуждается.
Мне очень не нравится строка №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
05 | #include "ReedSolomon.h" |
07 | #define printVar(x) do { Serial.print(#x "="); Serial.println(x); } while (false) |
10 | typedef uint8_t TFiveBytesArray[5]; |
13 | DECLARE_PAYLOAD(buffer, TFiveBytesArray, 4); |
17 | Serial .println( "Go on!" ); |
21 | buffer.payload[0] = 132; |
22 | buffer.payload[1] = 11; |
23 | buffer.payload[2] = 101; |
24 | buffer.payload[3] = 255; |
25 | buffer.payload[4] = 48; |
34 | buffer.payload[1] = 123; |
35 | buffer.payload[3] = 19; |
36 | ((uint8_t *)(&buffer))[0]++; |
37 | ((uint8_t *)(&buffer))[5]++; |
41 | const int errorsCorrected = rsDecode(buffer); |
46 | printVar(errorsCorrected); |
47 | printVar(buffer.payload[0]); |
48 | printVar(buffer.payload[1]); |
49 | printVar(buffer.payload[2]); |
50 | printVar(buffer.payload[3]); |
51 | printVar(buffer.payload[4]); |
а теперь с использованием макроса DECLARE_PAYLOAD_INIT
05 | #include "ReedSolomon.h" |
07 | #define printVar(x) do { Serial.print(#x "="); Serial.println(x); } while (false) |
10 | typedef uint8_t TFiveBytesArray[5]; |
13 | DECLARE_PAYLOAD_INIT(buffer, TFiveBytesArray, 4) |
14 | { 132, 11, 101, 255, 48 } |
15 | END_OF_DECLARE_PAYLOAD_INIT |
19 | Serial .println( "Go on!" ); |
28 | buffer.payload[1] = 123; |
29 | buffer.payload[3] = 19; |
30 | ((uint8_t *)(&buffer))[0]++; |
31 | ((uint8_t *)(&buffer))[5]++; |
35 | const int errorsCorrected = rsDecode(buffer); |
40 | printVar(errorsCorrected); |
41 | printVar(buffer.payload[0]); |
42 | printVar(buffer.payload[1]); |
43 | printVar(buffer.payload[2]); |
44 | printVar(buffer.payload[3]); |
45 | printVar(buffer.payload[4]); |
Проще ведь, чем было с базовыми функциями?
Где лежат грабли
Выше было сказано, что метод достаточно безопасен. Если ошибок при передаче окажется не больше, чем Е, то всё восстановится, а если больше, то функция декодирования вернёт 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. Передача русского текста с использованием только базовых функций
Чистых Вам линий!
23 + 19х2 = 61 байт
это почти 3 сообщения вместо одного. Это видимо три вложенные копии сообщения в одном, а потёртые данные восстановятся из других копий. Это особенно важно когда телеграмма идёт с орбиты Нептуна, так как нельзя быстро передать потерянное сообщение ещё раз.
23 + 19х2 = 61 байт
это почти 3 сообщения вместо одного.
это абсолютно нормальное соотношение для протокола с защитой от ошибок.
23 + 19х2 = 61 байт
это почти 3 сообщения вместо одного.
это абсолютно нормальное соотношение для протокола с защитой от ошибок.
на текущий момент не нормальное, для низкоуровневых сигналов Нобелевский лауреат Joseph Taylor придумал FT-8...
Особенности FT8:
То-есть, декодируется сигнал ниже уровня шума на 20 децибел
у меня нет ссылки на неё, использую готовую программу, попробую поискать, на хабре об FT-8
ЗЫ там же и ссылка оказалась на библиотеку
Особенности FT8:
То-есть, декодируется сигнал ниже уровня шума на 20 децибел
Чет не то. То что я прочитал про этот протокол: 77бит + 14бит CRC дальше с избыточностью получается 174бита.
Итого - на 77бит данных 174бита передаваемой информации.
Причем на том же хабре, но чуть ниже.
Не, столько ресурсов на декодирование - это и я умею. Вы давайте такую, чтобы нанка потянула.
Не, столько ресурсов на декодирование - это и я умею. Вы давайте такую, чтобы нанка потянула.
других нету или я плохо искал )))
ЗЫ почитал, да FEC LDPC это жесть
1
FEC код работает в двух режимах, так называетмые
short
FECFRAME и normal FECFRAME. Отличие в длине выхода. Кодовый блок LDPC для короткого фрейма 16200 байт, для длинного 64800. Для передачи «полезной» информации (видео, радио) используют нормальные фреймы. Для служебной информацией, для которой совсем критично время, используют короткие фреймы.
2
Но и это ещё не всё: для короткого фрейма существует 10 режимов с различными скоростями для БЧХ и LDPC кодирования, для нормального 11 вариантов. Итого 21 вариант FEC кода! Кому интересно подробнее — см Table 5a и Table 5b в [2].
Этот материал для тех, у кого возникает проблема неуверенного приёма цифрового сигнала
А библиотечка интересная. Доберусь до nRF24 - протестирую на максимальной дальности.
Этот материал для тех, у кого возникает проблема неуверенного приёма цифрового сигнала
А библиотечка интересная. Доберусь до nRF24 - протестирую на максимальной дальности.
да я и так тебе скажу, 1 ватт 400 км
да я и так тебе скажу, 1 ватт 400 км
2,4ГГц и 400км. Чего-то сомнительно. Дай бог 1км
23 + 19х2 = 61 байт
это почти 3 сообщения вместо одного.
это абсолютно нормальное соотношение для протокола с защитой от ошибок.
на текущий момент не нормальное, для низкоуровневых сигналов Нобелевский лауреат Joseph Taylor придумал FT-8...
Особенности FT8:
То-есть, декодируется сигнал ниже уровня шума на 20 децибел
2. Не нравится - выложите свою библиотеку. Которая лучше. Только - в своей теме. И Вам скажут спасибо. А придираться к тому, что пишущая машинка не печет блинов - по меньшей мере не умно.
кстати, FT8 - это аналог преобразования фурье.
23 + 19х2 = 61 байт
это почти 3 сообщения вместо одного.
Ну, я видел 6 ошибок и взял коэффициент Мэрфи равным Пи, отсюда 19. Взял бы поменьше, было бы меньше. Меня в данной задаче это вполне устраивает.
Это видимо три вложенные копии сообщения в одном, а потёртые данные восстановятся из других копий.
Да, Господь с Вами. Если бы это было так, то достаточно было бы трёх ошибок в "правильных" местах, чтобы всё свалилось. Но оно же не валится не только от трёх, в плоть до 19-ти независимо от того в какие места они попали. Тут гораздо более интересная математика.
нельзя быстро передать потерянное сообщение ещё раз.
Если конкретно у меня и сейчас, то да - тот самый случай. Связь односторонняя. Передать-то ещё раз можно, а вот запросить такой повтор - никак.
да я и так тебе скажу, 1 ватт 400 км
2,4ГГц и 400км. Чего-то сомнительно. Дай бог 1км
более 35000 Км и 12 ватт не сомнительно? )))
Да, Господь с Вами. Если бы это было так, то достаточно было бы трёх ошибок в "правильных" местах, чтобы всё свалилось. Но оно же не валится не только от трёх, в плоть до 19-ти независимо от того в какие места они попали. Тут гораздо более интересная математика.
Как-то сомнительно про 19 независимых ошибок, может неполное восстановление? Вы тестировали на 19 ошибок случайной выборкой(в том числе входных значений)? Но конечно всё зависит от эффективности алгоритма сжатия, можно упаковать и больше 3-х.
Это вот о чем сейчас было?
Также Вам никто не мешает потестировать самому. Но ...
Лучше предварительно ознакомиться с математикой процесса, чтобы сразу отпали сомнения про "неполное" и "случайное". Там такого не бывает. Если ошибок не более наперёд заданного числа, то сообщение восстанавливается полностью - никаких исключений (разумеется, если не считать возможных багов программиста - но это уже про качество программы, а не метода)
да, я уже проверил, действительно работает, до 20 ошибок, сообщение 23, пакет 61. Спасибо было интересно.
Интересно, а в регистровой памяти на один байт добавлялся всего один бит, видимо алгоритм был другой?
Интересно, а в регистровой памяти на один байт добавлялся всего один бит, видимо алгоритм был другой?
Это вообще о чем? О бите четности?
(Особенно, учитывая, что "регистровая память" - устаревший термин, современный аналог - кеш.)
Интересно, а в регистровой памяти на один байт добавлялся всего один бит, видимо алгоритм был другой?
Это вообще о чем? О бите четности?
(Особенно, учитывая, что "регистровая память" - устаревший термин, современный аналог - кеш.)
под регистровой RAM подразумевается память с ECC (простонародное название придумал не я)
Господа - почитайте подпрограмму!
Реально интересное решение, хоть и небезспорное. Но компактное. И логарифм в прогмеме. Не проверял, сколько кушает, но по поверхностной оценке - почти ничего. А в передаче данных - там 2-4 раза больше. Кто их считает (особенно коллизии)?
почитайте подпрограмму!
Вы не поверите, но я читал :-)
Да поверю, потому что:
Все животные равны, но некоторые животные более равны, чем другие :)