APRS. AFSK модулятор из Flipper Zero

Есть такой интересный протокол передачи данных — APRS. Про него в интернете уже много рассказывалось, здесь не будет углубленного теоретического материала. В этой статье будет описано как создать свой собственный «карманный» модулятор AFSK. В последующих статьях будут инструкции по выходу в эфир и по созданию простого демодулятора, который позволит принять пакеты APRS и отобразить информацию на дисплее прямо на улице. Всё будет реализовано для Flipper Zero. Если у Вас еще нет этого гаджета, то не расстраивайтесь и попробуйте всё на великой и ужасной Arduino. Передавать информацию на расстояние «своими руками» очень интересно.

Мы не станем строить «велосипед», только наставим «костылей»

В интернете есть вообще всё. И реализация AFSK модуляторов тоже существует. Только под FZ (Flipper Zero) пока еще нет. Этот гаджет хорош по сравнению с моей любимой Arduino Nano, например, тем, что представляет собой законченное устройство в корпусе с дисплеем и кнопками. Поэтому будут взяты исходные коды с одной из открытых лицензий и доработаны с учетом особенностей FZ. Здесь хочется обратиться к более опытным читателям, которые хорошо разбираются в FreeRTOS, ведь именно она «крутится» на FZ. Оригинальные исходные коды предназначаются для Arduino, где нет многозадачности, а при разработке под FZ я столкнулся с выходом из важных функций формирования сигнала в прерывания. Решение было найдено, но, уверен, что можно реализовать изящнее. Пожалуйста, расскажите в комментариях как сделать правильно.

«Если кто-то захочет разработать приложение, я в деле» [Глути. // Рик и Морти. — 4 сезон. — 2 серия.]

Помимо реализации модулятора AFSK и протокола AX.25 потребуется разобраться с созданием приложений под Flipper Zero. Все про что здесь написано создано для версии 0.83.1 https://github.com/flipperdevices/flipperzero-firmware/releases/tag/0.83.1.

рис. 1. Скриншот версии прошивки девайса из эксперимента

рис. 1. Скриншот версии прошивки девайса из эксперимента

Здесь тоже подробно останавливаться не станем, потому что всё уже неплохо описано в других статьях. Приведу примеры и дам ссылки, которыми сам воспользовался. (Большое спасибо авторам этих статей за их проделанную для нас работу!):

  1. https://amperka.ru/blogs/projects/flipper-zero-app-programming-tutorial

  2. https://yakovlev.me/hello-flipper-zero/

В статье по второй ссылке есть хороший пример реализации срабатывания по таймеру. Сначала я использовал furi_delay_ms(tx_delay);, но находясь на паузе между передачами выйти из приложения не получится. Это не очень удобно, особенно когда паузы по три минуты. Поэтому таймеры просто необходимы. Таким образом шаблон с выделением/освобождением памяти из первой статьи был улучшен использованием таймера из второй статьи. (Как мне показалось, вторая статья будет попроще для понимания, особенно читателям, которые пока что не выходили за пределы Arduino).

Хороший шаблон с выделением/освобождением памяти для будущих приложений

https://github.com/NSV47/good_template

Просто скачайте, разархивируйте и положите каталог /good_template в каталог flipperzero-firmware/applications_user. Далее команды как в статьях ./fbt fap_good_template и ./fbt fap_deploy и приложение уже на девайсе.

Все говорят о том что самое сложное это иконка и это действительно правда. В репозитории можно взять мою иконку APRS.

Собрали, перенесли на девайс. Вот демонстрация:

О переносе проекта с одной отладочной платы на другой девайс

Когда я узнал про APRS и захотел повторить успех, то начал шарить по интернету и наткнулся на проект, который мне показался очень интересным. Вот этот проект: https://github.com/handiko/Arduino-APRS. Давайте возьмем наш хороший шаблон, перенесем в него функции из репозитория пользователя Github с ником Handiko и будем ему бесконечно благодарны за его большую проделанную работу. Лучше пойдем с тестовых прошивок, чтобы я смог на деле указать на особенность FreeRTOS.

Single tone test 1200 hz

В бесконечном цикле вызывается set_nada(_1200);, которая вызывает set_nada_1200(void);. Во второй функции выключается и выключается один из выходов. Для FZ использование GPIO описано в первой статье и тогда наша функция set_nada(_1200); приобретает вид:

void set_nada_1200(SingleToneTest1200HzApp* app)
{
    app->output_value = true;
    furi_hal_gpio_write(app->output_pin, app->output_value);
    furi_delay_us(tc1200);
    app->output_value = false;
    furi_hal_gpio_write(app->output_pin, app->output_value);
    furi_delay_us(tc1200);
}

По сути тоже самое что у Handiko, но под реалии FZ. Жаль только что это работает не так как нам хотелось бы:

К выходу подключен самый простой зуммер, мы слышим его щелчки на фоне. Вместо звука 1200 Гц, частота около 1 Гц. Это как раз те особенности использования FreeRTOS. Получается бесконечный цикл в приложении FZ немного не такой, как в Arduino. Перепишем функцию void set_nada_1200(SingleToneTest1200HzApp* app). И вызывать ее следует не в бесконечном цикле, а до него:

void set_nada_1200(SingleToneTest1200HzApp* app)
{
    uint32_t value = 5000;
    while(value--){
        app->output_value = true;
        furi_hal_gpio_write(app->output_pin, app->output_value);
        furi_delay_us(tc1200);
        app->output_value = false;
        furi_hal_gpio_write(app->output_pin, app->output_value);
        furi_delay_us(tc1200);
    }
}

Результат:

Снова проблема, на этот раз не такая очевидная, и на ее решение у меня ушло пару вечеров после работы. Слышны какие-то искажения сигнала, их быть не должно. На осциллограмме эта аномалия видна очень хорошо:

рис. 2. Осциллограмма аномалии в сигнале

рис. 2. Осциллограмма аномалии в сигнале

Если оставить как есть и продолжить переносить код, то визуально всё будет выглядеть, так как надо. Интерпретировать осциллограмму модулированного сообщения очень сложно, ну зуммер это вообще игрушка. Разобраться с этой проблемой необходимо именно на этом этапе. Это теперь как раз те особенности FreeRTOS, про которые упомянуто в самом начале, и которые надо решить более изящно. При поиске решения мне попалась вот эта статья https://habr.com/ru/articles/129445/. Спасибо доброму человеку, который ее написал. Необходимо использовать критические секции, причем приостановка работы планировщика никак не помогла, помогло именно использование критической секции, построенной на макросах. Ну в статье и написано, что приостановка планировщика защищает только от других тасков, а мы сваливаемся в прерывание. Теперь функция принимает вид:

void set_nada_1200(SingleToneTest1200HzDraftApp* app)
{
    uint32_t value = 5000;
    taskENTER_CRITICAL();
      while(value--){
          app->output_value = true;
          furi_hal_gpio_write(app->output_pin, app->output_value);
          furi_delay_us(tc1200);
          app->output_value = false;
          furi_hal_gpio_write(app->output_pin, app->output_value);
          furi_delay_us(tc1200);
      }
    taskEXIT_CRITICAL();
}

Теперь и на слух всё правильно и на осциллограмме порядок. Только одно но. Пока будет выполняться код в критической секции ничего другого сделать не получится, например, выйти из приложения, нажав кнопку назад. В нашем случае, конечно, пакеты не такие-то уж и большие, можно подождать, но душа инженера требует идеала, а семья внимания, поэтому вопрос к более опытным читателям: «Как избавиться от этого «костыля»?»

Ссылка на исходники Single tone test 1200 hz на всякий случай (обратите внимание в этом коде еще не реализовано обращение к таймеру, но он тут и не нужен):

https://github.com/NSV47/Single_Tone_Test_1200_Hz_draft

У Handiko в репозитории Arduino_APRS есть еще ряд тестовых прошивок. Но разбирать я из не буду, потому что не столкнулся с какими-то серьезными проблемами типа необходимости использования критических секций. Прокомментирую только пару моментов.

Еще до того как я стал использовать критические секции, я не уделил должного внимания аномалии на осциллограмме (ну или на слух с зуммера) и реализовал сразу тест APRS_Random_String. К моему удивлению этот сигнал достаточно неплохо декодировался компьютером (как это можно сделать я опишу дальше). Это меня сбило с толку дальше, когда я реализовал APRS_Hello_World, который декодировался примерно в одном случае из 20. При этом я грешил на переменную «baud_adj», потому что думал что у меня что-то не так с длительностью импульса.

Потом я нашел решение и использовал критические секции, но к моему удивлению декодировать не получалось. Я думал, что из-за того не умею правильно использовать критические секции, при переходе из функции в функцию происходит прерывание. Пытался сравнить оригинальные осциллограммы снятые с Arduino с сигналом с FZ. А один вечер потратил на то, чтобы написать все в одной функции (выглядит страшно, там больше 3000 строк, но мне очень сильно хотелось чтобы это работало).

рис. 3. Пример ненормального программирования

рис. 3. Пример ненормального программирования

А ошибка была просто в моей невнимательности. В исходнике APRS_Random_String в реализации void send_string_len(const char *in_string, int len) в функцию send_char_NRZI(in_string[j + len], HIGH) передается индекс j+len, а вот в исходнике APRS_Hello_world в реализации той же самой функции строчка передается с индексом j. Поэтому сдирая чужой код будьте внимательны:)

APRS_Hello_world

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

https://github.com/NSV47/APRS_hello_world_clean

Декодер AFSK

Для проверки работоспособности приложения необходимо декодировать сообщение. Для этого можно использовать AFSK1200 Decoder. Чтобы завести сигнал на звуковую карту ноутбука я воспользовался вот таким разъемом:

рис. 4. Распиновка Jack 3.5 для соединения Flipper Zero с компьютером

рис. 4. Распиновка Jack 3.5 для соединения Flipper Zero с компьютером

Необходимо будет выбрать в окошке INPUT источник сигнала и нажать Start Decoder:

рис. 5. Пример декодированных сообщений

рис. 5. Пример декодированных сообщений

Есть и другие программы, которыми можно декодировать такие сигналы, например, UISS (инструкция https://r4uab.ru/aprs-iss/). С помощью этой программы у меня получалось даже лучше принимать реальные сигналы APRS. Или GNU Radio (инструкция https://github.com/handiko/gr-APRS). Я пользовался в UBUNTU GNU RADIO и этой инструкцией.

Итоги

Теперь у нас получилось создать модулятор AFSK своими руками и разобраться с тем как устроены приложения Flipper Zero. Мы даже соединили Flipper Zero с компьютером. Но чтобы по-настоящему раскрыть протокол передачи данных APRS нам потребуется передатчик. У меня, например, пока что нет лицензии радиолюбителя и радиопозывного, поэтому в одной из следующих статей мы выйдем в эфир в PMR диапазоне (446–446.200 МГц), где лицензия не требуется. Мощность работы передатчика также составит разрешенные 500 мВт. И тогда посмотрим насколько далеко получится передавать сообщения.

Несмотря на некоторый успех, я все-таки прошу более опытных читателей дать ответы на возникшие вопросы:

  1. Как правильно защититься от особенностей работы FreeRTOS, чтобы важный код не мешался с прерываниями?

  2. Можно ли использовать частоты APRS (144.800 МГц) для, например, общения с другими радиолюбителями (конечно же имея все необходимые разрешения, лицензии, позывные и т. п.) наподобие чата в WhatsApp (ну это например)?

  3. Как, всё-таки, сейчас получить лицензию радиолюбителя и позывной? Может быть, кто-то недавно получал? (Потому что я вроде вычитал что сейчас даже и ходить никуда не надо, все можно сделать прямо из дома удаленно).

  4. Что будет если выходить в эфир без лицензии и позывного, например с помощью той же самой Baofeng UV-5R? А если осторожно с мощностью 1 Вт? (Мне просто интересно)

  5. Что будет, если работать в PMR диапазоне (446–446.200 МГц), где лицензия не требуется, но с мощностью 1 Вт?

Спасибо, что дочитали. Пишите, если что вдруг будет не получаться — разберемся вместе.

С. Н.

© Habrahabr.ru