Подключение аппаратуры удаленного управления FlySky (FS-i6 FS-iA6 FS-iA6B) к роботу на основе контроллера ESP32 (Arduino)
Удаленное управление роботом, актуальный вопрос, которого в то или иной степени касался каждый Конструктор роботов. Когда мой коллега, приобрел систему дистанционного управления FlySky, я отнесся к этому как к шагу назад, рассуждая, что мы и самостоятельно сможем делать подобные системы. Но, как показало время, создание качественных систем дистанционного управления непростая и недешевая задача. После того, как мы сделали и оттестировали один пульт на основе NRF24L01 и сконструировали несколько систем обмена информацией между роботами на основе данных модулей, я сделал вывод, что есть случаи, когда приобрести готовую систему дистанционного управления будет и дешевле и лучше/не хуже в эксплуатации.
Рисунок 1
В свое время также рассматривался вопрос удаленного управления по каналу Bluetooth («Мобильные роботы на базе Arduino»), это удобный и мало затратный способ, но имеет ряд ограничений:
- малая дальность (до 10 метров) что вполне достаточно для помещений, но затрудняет или делает не возможным управление роботами квадрокоптерами или гоночными болидами и пр.;
- низкая защита от помех, при зашумленности «эфира», возможны разрывы и «подтормаживания» соединений, что не страшно при связи с гарнитурой, но может привести робота к аварии;
- удобство - управление с экрана смартфона или планшета не возможно без визуального контроля, т.е. нужно периодически смотреть на экран, ведь рычаги управления не имеют тактильной обратной связи, они просто нарисованы на экране.
И, в связи с наличием перечисленных мнений и ограничений, попытаюсь рассказать о способах подключения к контроллеру Arduino (на примере ESP32) аппаратуры удаленного управления Flysky, на примере FS-i6 (рис.2).
Рисунок 2. Аппаратура удаленного управления FS-i6
Аппаратура состоит из передатчика (FS-i6) и приемника (FS-iA6 или FS-iA6B). Передатчик на заводской прошивке может передавать до 6 значений от рычажков управления, какие из 10 рычажков будут задействованы, настраивается из меню передатчика. Довольно просто установить альтернативную прошивку, которая дает возможность задействовать одновременно все рычажки управления (я ее установил c https://github.com/qba667/FlySkyI6/releases ), но суть не в этом, и останавливаться на замене прошивки я не буду.
Для управления роботом потребуется как приемник, так и передатчик. Отличия приемников FS-iA6 и FS-iA6B в наличии у FS-iA6B дополнительной шины (bus) о которой поговорим ниже.
Передатчик FS-i6 пока отложим в сторону (как синхронизировать/связать приемник и передатчик ищем в других документах) и подробнее рассмотрим приемники, начнем с FS-iA6, он изображен на следующем рисунке.
Рисунок 3
Питание приемника осуществляется через верхний ряд контактов B/VCC, ряды CH1-CH6 , это выходы, и если питание с приемника брать не нужно, то потребуется использовать только левый столбец контактов, на которых приемник генерирует ШИМ сигнал с шириной импульса соответствующей положению рычажков на передатчике. Замечу, что генерация ШИМ-сигналов приемником, это отсыл к моделям, которые не имеют собственного микроконтроллера, и двигатели которых управляются непосредственно с пульта. Т.е. пульт подключается напрямую к сервомашинкам самолета, которые регулируют положение руля, подкрылков и …, я не специалист в области летательных аппаратов.
Но наши модели являются роботами, они имеют собственные микроконтроллер, который и должен обслуживать сигналы дистанционного управления. Для этого их нужно расшифровать, а конкретно в случае ШИМ сигналов нужно измерить длительность импульса в такте. Измеряем длину импульса в каждом интересующем нас канале и используем по назначению.
Простейший участок кода, отвечающий за обработку входных ШИМ последовательностей, выглядит следующим образом:
«Все листинги приведены для использования с контроллером ESP32».
Рисунок 4
GPIO14 -> CH2
GPIO27 -> CH3
GPIO26 -> CH4
Приемник FS-iA6 и ESP32 от одного источника питания 5 вольт.
Рисунок 5
Применяя функцию pulseIn(), поочередно измеряем длину импульса, на каждом GPIO к которому подключен ШИМ канал от приемника.
pulseIn() может применяться с двумя или тремя параметрами: первый – номер GPIO, второй –тип ожидаемого сигнала , третий – максимальное время ожидания. pulseIn() измеряет длину в микросекундах импульса на GPIO.
У вышеприведенного кода есть один существенный минус –считывание всех сигналов производится последовательно и возможно в разных циклах, что приводит нас к расчету максимальной задержки, которая равна сумме времени ожидания каждого вызова pulseIn().
Давайте попробуем исследовать сигналы не по очереди, а параллельно!
ESP32 быстрый контроллер, его частота 240МГц, и сильно оптимизировать код под него не нужно, хоть это возможно.
Разработаем алгоритм параллельного опроса GPIO в цикле с дискрецией 5мкСек. Сканируем GPIO в цикле, отлавливаем сигналы и измеряем их длину. Основная программа приведена ниже, а функция сканирования расположена в файле intpilt.h , там также описание всех сопутствующих переменных и прочее.
Рисунок 6
Рисунок 7
Листинг 1. Модуль intpult.h
#define PWMPOWERMIN 950 // Минимальное состоянние PWM управления от приемника
#define PWMPOWERMAX 2100 // Максимальное состояние PWM управления от приемника
#define MAXTIME 21000
#define MAXTIMECANAL 2100
#define pinCanal1 13
#define pinCanal2 14
#define pinCanal3 27
#define pinCanal4 26
#define DELTA 5 //Через сколько микросекунд начинается
bool Canal1, Canal2, Canal3, Canal4; bool ScanC1, ScanC2, ScanC3, ScanC4; bool flagCanal1, flagCanal2, flagCanal3, flagCanal4;
bool C1Err, C2Err, C3Err, C4Err, C5Err, CSErr; uint16_t Canal1Timeoff, Canal2Timeoff, Canal3Timeoff, Canal4Timeoff;
uint16_t Canal1Time, Canal2Time, Canal3Time, Canal4Time;
// Перед запуском: // 1. Поднимаем ScanC1=true // 2. Canal1Time = 0; // 3. Canal1Timeoff =0;
// 4. Canal1 = true; //На случай, если начали считывание с 1, нужн ждать пока она кончится
// 5. flagCanal1 = false; //Начало нормального импульса
void setup_CHANEL()
{
pinMode(pinCanal1, INPUT);
pinMode(pinCanal2, INPUT);
pinMode(pinCanal3, INPUT);
pinMode(pinCanal4, INPUT);
}
void ScanCHANEL()
{
uint32_t LERRUA;
ScanC1 = true; ScanC2 = true; ScanC3 = true; ScanC4 = true; Canal1Time = 0; Canal2Time = 0; Canal3Time = 0; Canal4Time = 0;
Canal1Timeoff = 0; Canal2Timeoff = 0; Canal3Timeoff = 0; Canal4Timeoff = 0; Canal1 = true; Canal2 = true; Canal3 = true; Canal4 = true;
flagCanal1 = false; flagCanal2 = false; flagCanal3 = false; flagCanal4 = false; C1Err = false; C2Err = false; C3Err = false; C4Err = false;
unsigned long tt, tectime, end_time;
tectime = micros();
end_time = tectime + MAXTIME + DELTA;
while ((tectime < end_time) && (ScanC1 || ScanC2 || ScanC3 || ScanC4 ))
{
//====== C1
if (ScanC1) //Если поднято считывание с канала
{
if (flagCanal1) // Если поднят расчет длины импульса
{
Canal1 = digitalRead(pinCanal1);
if (Canal1) {
Canal1Time += DELTA;
if (Canal1Time > MAXTIMECANAL) {
C1Err = true;
ScanC1 = false;
}
}
else {
ScanC1 = false;
}
}
else //Если не поднят флаг расчет длины импульса
{
if (Canal1) //Если в канале 1, то мы в начале
{
Canal1 = digitalRead(pinCanal1);
}
else
{
Canal1 = digitalRead(pinCanal1);
if (Canal1)
{
Canal1Time = DELTA;
flagCanal1 = true;
}
else
{
Canal1Timeoff += DELTA; if (Canal1Timeoff > MAXTIME) ScanC1 = false; //Завершили
}
}
}
}
//============= C2
if (ScanC2) //Если поднято считывание с канала
{
if (flagCanal2) // Если поднят расчет длины импульса
{
Canal2 = digitalRead(pinCanal2);
if (Canal2) {
Canal2Time += DELTA;
if (Canal2Time > MAXTIMECANAL)
{
C2Err = true;
ScanC2 = false;
}
}
else
{
ScanC2 = false;
}
}
else //Если не поднят флаг расчет длины импульса
{
if (Canal2) //Если в канале 1, то мы в начале
{
Canal2 = digitalRead(pinCanal2);
}
else
{
Canal2 = digitalRead(pinCanal2);
if (Canal2)
{
Canal2Time = DELTA;
flagCanal2 = true;
}
else
{
Canal2Timeoff += DELTA; if (Canal2Timeoff > MAXTIME) ScanC2 = false; //Завершили
}
}
}
}
//===== C3
if (ScanC3) //Если поднято считывание с канала
{
if (flagCanal3) // Если поднят расчет длины импульса
{
Canal3 = digitalRead(pinCanal3);
if (Canal3) {
Canal3Time += DELTA;
if (Canal3Time > MAXTIMECANAL)
{
C3Err = true;
ScanC3 = false;
}
}
else
{
ScanC3 = false;
}
}
else //Если не поднят флаг расчет длины импульса
{
if (Canal3) //Если в канале 1, то мы в начале
{
Canal3 = digitalRead(pinCanal3);
}
else
{
Canal3 = digitalRead(pinCanal3);
if (Canal3)
{
Canal3Time = DELTA;
flagCanal3 = true;
}
else
{
Canal3Timeoff += DELTA; if (Canal3Timeoff > MAXTIME) ScanC3 = false; //Завершили
}
}
}
}
//===== C4
if (ScanC4) //Если поднято считывание с канала
{
if (flagCanal4) // Если поднят расчет длины импульса
{
Canal4 = digitalRead(pinCanal4);
if (Canal4) {
Canal4Time += DELTA;
if (Canal4Time > MAXTIMECANAL)
{
C4Err = true;
ScanC4 = false;
}
}
else
{
ScanC4 = false;
}
}
else //Если не поднят флаг расчет длины импульса
{
if (Canal4) //Если в канале 1, то мы в начале
{
Canal4 = digitalRead(pinCanal4);
}
else
{
Canal4 = digitalRead(pinCanal4);
if (Canal4)
{
Canal4Time = DELTA;
flagCanal4 = true;
}
else
{
Canal4Timeoff += DELTA; if (Canal4Timeoff > MAXTIME) ScanC4 = false; //Завершили
}
}
}
}
tt = micros();
while (tt < tectime) tt = micros();
tectime = tt + DELTA;
}
}
Теперь блок сканирования будет выполняться за время равное длине одного такта, 21 миллисекунд в нашем случае. Время уменьшено в 4 раза для 4х каналов, для 6 каналов, будет 6 раз соответственно.
Но, по-моему, все равно время велико!
Тратить 21 миллисекунду впустую тогда, когда наш контроллер мог бы производить полезные вычисления траектории робота или сканировать датчики, поэтому перейдем к рассмотрению приемника FS-iA6B.
Рисунок 8
И тут нам на помощь приходит одна замечательная библиотека flyskyIBus (https://github.com/aanon4/FlySkyIBus ).
Скачиваем, подключаем GPIO4 подключаем к iBus Servos контакт S, настраиваем под особенности ESP32 см.ниже:
А именно – добавляем адресные GPIO
Вот ка выглядит теперь основная программа:
Рисунок 9
Обрабатываем входной буфер порта ввода/вывода IBus.loop() , записываем данные о состоянии рычажков передатчика в переменные и всё. Соединение использует только один GPIO (вывод нами не используется), прием данных в буфер порта проходит на аппаратном уровне и может не задерживать выполнение программы.
Вот так, затратив только одну GPIO контроллера ESP32, мы подключили приемник, причем у меня он передает на контроллер 10 значений состояния рычажков управления, а может 14, где взять еще 4 рычажка?