Skip to content

Каналы и CRSF

Каналы и протокол CRSF

Цель занятия: обучающиеся назначают и переназначают каналы на пульте RadioMaster Boxer, читают значения каналов в коде и находят расхождение между ожидаемым и фактическим назначением по дампу.

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

Что такое канал

Пульт передаёт 16 чисел, много раз в секунду. Каждое число — положение одного органа управления: стика, тумблера, потенциометра. Эти числа и есть каналы.

Пульт не знает, что означают эти числа. Он просто отправляет: «канал 3 = 1700, канал 5 = 1900». Смысл им придаёт приёмная сторона — прошивка аппарата.

КаналОбычное назначениеОрган на Boxer
CH1Крен (roll, aileron)Правый стик, влево-вправо
CH2Тангаж (pitch, elevator)Правый стик, вперёд-назад
CH3Газ (throttle)Левый стик, вверх-вниз
CH4Рыскание (yaw, rudder)Левый стик, влево-вправо
CH5ARM — разрешение моторовТумблер SA
CH6–CH16Режимы, подвес, доп. функцииSB…SH, потенциометры S1, S2

Диапазон значения канала — 1000…2000, в микросекундах (наследие старых серво-сигналов). 1000 — минимум, 1500 — центр, 2000 — максимум.

CH5 и ARM — вопрос безопасности, а не удобства. Отдельный тумблер, без которого моторы не крутятся, обязателен всегда. Он позволяет мгновенно обесточить моторы одним движением, не ища стик газа. В коде это проверка одного порога — а на практике это разница между испугом и травмой.

Цепочка от пульта до кода

Пульт формирует значения каналов

Прошивка пульта (EdgeTX на Boxer) берёт положения стиков, применяет к ним кривые и микшеры и раскладывает результат по 16 каналам.

Передатчик отправляет радиопакет

Модуль ExpressLRS упаковывает каналы и отправляет по радио на 2,4 ГГц сотни раз в секунду.

Приёмник принимает и выдаёт в UART

Приёмник ELRS на борту распаковывает радиопакет и выдаёт его по проводу (UART) в формате CRSF на скорости 420000 бод.

Контроллер разбирает CRSF

Наш код читает байты из UART, находит начало пакета, проверяет целостность и распаковывает 16 чисел. Дальше они идут в микшер.

Устройство пакета CRSF

Пакет с каналами выглядит так:

┌──────┬──────┬──────┬────────────────────────────┬──────┐
│ 0xC8 │ LEN  │ 0x16 │  22 байта данных каналов   │ CRC8 │
└──────┴──────┴──────┴────────────────────────────┴──────┘
  синх   длина   тип          полезная нагрузка      контроль
ПолеЗначениеСмысл
Синхробайт0xC8Признак начала пакета
Длинаобычно 24Сколько байт идёт дальше (тип + данные + CRC)
Тип0x16«Это пакет с каналами» (RC Channels Packed)
Данные22 байта16 каналов, упакованных по 11 бит
CRC81 байтКонтрольная сумма, полином 0xD5

Почему 22 байта, а не 32

16 каналов по 11 бит = 176 бит = ровно 22 байта. Одиннадцати бит хватает на 2048 градаций — этого более чем достаточно для положения стика, а экономия эфира позволяет отправлять пакеты чаще. В радиосвязи каждый байт стоит времени в эфире, а частота обновления напрямую влияет на отзывчивость управления.

Плата за экономию: значения каналов не выровнены по границам байт. Канал может начаться в середине одного байта и закончиться в другом.

байт 0        байт 1        байт 2        байт 3
┌────────┐   ┌────────┐   ┌────────┐   ┌────────┐
│ CH1 ... │   │..CH1 CH2│   │ CH2 CH3│   │ CH3 ...│
└────────┘   └────────┘   └────────┘   └────────┘
    8 бит       3+5 бит      6+2 бит      ...

Отсюда — сдвиги и маски при распаковке.

Разбор кода: распаковка каналов

Первые три канала из рабочей прошивки ERLS_Rover:

_channels[0] = ((uint32_t)payload[0] | ((uint32_t)payload[1] << 8)) & 0x7FF;

_channels[1] = (((uint32_t)payload[1] >> 3) | ((uint32_t)payload[2] << 5)) & 0x7FF;

_channels[2] = (((uint32_t)payload[2] >> 6) | ((uint32_t)payload[3] << 2)
                                            | ((uint32_t)payload[4] << 10)) & 0x7FF;

Разобрать вслух — это и есть занятие:

  • & 0x7FF — маска на 11 младших бит (0x7FF = 2047 = 11 единиц). Отрезает всё лишнее, что попало в переменную сверху.
  • CH1 лежит в байте 0 целиком и в трёх младших битах байта 1. Сдвиг << 8 ставит байт 1 выше байта 0.
  • CH2 начинается с бита 3 байта 1: >> 3 выбрасывает биты, уже отданные CH1.
  • CH3 собирается из трёх байт: остаток байта 2, весь байт 3 и два бита байта 4. Именно поэтому там три слагаемых.
Вопрос на понимание. Почему приведение к uint32_t обязательно? Без него payload[4] << 10 вычисляется в 8-битном типе, старшие биты уезжают за границу и теряются — канал 3 начинает врать в верхней части диапазона. Это классическая ошибка переполнения при битовых сдвигах, и она ловится только на краях диапазона: в центре стика всё выглядит правильно.

Приведение к привычному диапазону

Сырое 11-битное значение приходит в диапазоне 172…1811 (так устроен CRSF). Переводим в привычные микросекунды:

_channels[i] = map(_channels[i], 172, 1811, 1000, 2000);
_channels[i] = constrain(_channels[i], 1000, 2000);

constrain нужен, потому что map не ограничивает результат: при значении чуть за границей диапазона (а такое бывает при точной подстройке концов на пульте) на выходе получится 2003 или 998, и дальше это уедет в микшер.

Конечный автомат приёма

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

    stateDiagram-v2
    [*] --> WAIT_SYNC
    WAIT_SYNC --> WAIT_LEN: байт == 0xC8
    WAIT_SYNC --> WAIT_SYNC: любой другой байт
    WAIT_LEN --> WAIT_DATA: длина в пределах 2..62
    WAIT_LEN --> WAIT_SYNC: длина неправдоподобна
    WAIT_DATA --> WAIT_DATA: копим байты
    WAIT_DATA --> WAIT_SYNC: пакет собран (CRC проверен)
  
case WAIT_SYNC:
    if (b == 0xC8) { bytesRead = 0; parserState = WAIT_LEN; }
    break;
case WAIT_LEN:
    if (b >= 2 && b <= 62) { payloadLen = b; bytesRead = 0; parserState = WAIT_DATA; }
    else { parserState = WAIT_SYNC; }   // мусор — ищем синхробайт заново
    break;

Проверка b >= 2 && b <= 62 — защита от мусора: байт 0xC8 может случайно встретиться внутри данных, и тогда автомат «зацепится» не за то место. Неверная длина это обнаружит, и поиск начнётся заново.

Зачем проверять CRC

if (calculate_crc8(crsfBuffer, payloadLen - 1) == crsfBuffer[payloadLen - 1]) {
    parseCrsfChannels(&crsfBuffer[1]);
    lastPacketTime = millis();
}

Радиоканал — среда с помехами. Пакет может прийти искажённым, и без проверки искажённое значение попадёт прямо в моторы. Контрольная сумма — это фильтр между помехой и винтами. Пакет с неверным CRC просто отбрасывается: лучше пропустить одно обновление (следующее придёт через пару миллисекунд), чем исполнить случайную команду.

Failsafe: что делать, когда связи нет

if (millis() - lastPacketTime > 500 && linkUp) {
    linkUp = false;
    _channels[0] = 1500;  // крен — центр
    _channels[1] = 1500;  // тангаж — центр
    _channels[2] = 1000;  // ГАЗ — МИНИМУМ
    _channels[3] = 1500;  // рыскание — центр
    for (int i = 4; i < 16; i++) _channels[i] = 1500;
}

Логика: пакеты приходят сотни раз в секунду, поэтому полсекунды тишины — уже авария. Все управляющие каналы уходят в центр, а газ — в минимум.

Почему газ именно в 1000, а не в 1500. Центр для газа означал бы половину мощности — аппарат без связи продолжил бы лететь. Для наземной техники это «уехал», для летающей — «улетел неизвестно куда на половине тяги». Все каналы в центр, газ в минимум — это не стилистика, а единственный безопасный выбор.

Для летающего аппарата failsafe устроен сложнее: мгновенное снятие газа на высоте означает падение. Там применяют возврат домой или контролируемое снижение — см. радиолинию и планирование маршрута.

Практика: назначение каналов

Включить дамп каналов

В прошивке включаем вывод всех 16 каналов в консоль. В ERLS_Rover это #define CHANNEL_DEBUG_DUMP 1.

Найти каждый орган управления

Двигаем по одному органу и смотрим, какое число меняется. Записываем таблицу: «стик газа → CH3», «тумблер SA → CH5». Так составляется фактическая карта каналов — не та, что «должна быть», а та, что есть.

Сверить с ожиданием

Сравниваем полученную карту с тем, что ожидает код. Расхождение — источник почти всех «аппарат не слушается» на этом этапе.

Переназначить и проверить снова

Меняем назначение на пульте (в EdgeTX — раздел микшеров) и повторяем дамп. Убеждаемся, что контракт выполняется. Устройство пульта и порядок настройки — в пульте: EdgeTX и Boxer.

Стенд без железа. В проекте ERLS_Rover есть crsf_visualizer.py — Python-стенд, повторяющий декодер прошивки построчно. Он позволяет провести это занятие, даже если аппарат разобран: приёмник подключается к компьютеру, и каналы видно на экране. Хороший способ занять группу, когда аппаратов меньше, чем обучающихся.

Диагностика

СимптомПричинаПроверка
В консоли пустоНет питания приёмника, перепутаны RX/TX, не та скоростьПроверить 420000 бод, поменять RX/TX местами
Числа хаотично скачутНе проверяется CRC или неверная скорость UARTВключить проверку CRC, сверить скорость
Все каналы 1500, газ 1000Сработал failsafe — связи нетПроверить привязку и питание приёмника
Один канал врёт на краяхПереполнение при сдвигеПроверить приведение к uint32_t
Реагирует не тот органРасхождение карты каналовДамп и сверка назначения
Значения не доходят до 1000/2000Не настроены концы на пультеПодстроить endpoints
Газ и крен «поменялись местами»Расхождение AETR/TAERПорядок каналов

Приёмка занятия

  • Объясняет, зачем нужны синхробайт, длина и CRC, и что произойдёт без каждого.
  • По дампу составляет карту каналов и находит подставленное расхождение.
  • Объясняет, почему в failsafe газ уходит в минимум, а не в центр.
  • Отвечает, почему CH3 распаковывается из трёх байт, а CH1 — из двух.