Зима в северных широтах — время, когда у FPV пилота появляется время, чтобы отдохнуть от гонок и постоянных поломок, взять в руки паяльник, и смастерить что-нибудь полезное для своего хобби.

Раз уж на улице летать холодно, то будем тренировать навыки пилотирования на симуляторах. Для этого нужно подключить свою радиоаппаратуру к компьютеру через специальный адаптер, который преобразует PPM сигнал с пульта в сигналы USB-джойстика, понятные компу. Такие адаптеры, конечно не редкость и стоят копейки в китайских магазинах. Однако доставки заказа ждать долго, да и будет ли он работать так как мы ожидали? Например у меня есть вот такой:

Он по каким-то, еще не постигнутым мною причинам, наотрез отказывается адекватно калиброваться в симуляторе FPV Freerider, хотя прекрасно работает в Phoenix RC и Aerofly RC 7. А FPV Freerider вполне неплохо передает физику акро-полета на гоночном коптере, да к тому же имеет бесплатный демо-режим.

Решено — делаем адаптер сами!

Немного матчасти:

Большинство мало-мальски серьезных RC-аппаратур имеют разъем, куда выводят управляющие сигналы в PPM (Pulse Position Modulation) формате. PPM-cигнал представляет собой последовательность коротких импульсов, промежуток между которыми определяет управляющее значение каждого из каналов радиоаппаратуры.
Суть PPM отлично передает картинка:


Чтобы декодировать PPM нужно точно замерять интервалы времени между последовательными импульсами (при этом не имеет значения между какими фронтами: передними или задними, ведь длительность самих импульсов всегда одинакова).

Реализация:

Почерпнув вдохновения из статьи AlexeyStn о создании PPM-to-USB адаптера на базе STM32F3Discovery, но имея в распоряжении только Arduino Pro Micro (Leonardo) с аппаратной поддержкой USB, начнем несложный путь к своему адаптеру.

На гитхабе можно найти несколько похожих проектов, и некоторые даже не требуют аппаратного USB в контроллере. Однако большинство из них нужно серьезно допиливать напильником, чтобы получить нечто рабочее. Годным проектом оказался rc-leonardo-joy, который начал работать почти сразу после заливки скетча, но в нем тут же обнаружились некоторые недостатки: все показания джойстика были не очень стабильными — маркер курсора в панели управления все время плясал вокруг установленной точки. Не могу сказать, что это существенно влияло на управляемость в симуляторе, но мы ведь хотим тренироваться на хорошей аппаратуре!

Что ж, лезем в код и видим: вычисление ширины импульса PPM сделано через обработку внешнего прерывания и замер интервалов между вызовами функции micros():

void rxInt(void) {
  uint16_t now,diff;
  static uint16_t last = 0;
  static uint8_t chan = 0;

  now = micros();
  sei();
  diff = now - last;
  last = now;
  if(diff>3000) chan = 0;
  else {
    if(900<diff && diff<2200 && chan<RC_CHANS ) {
      rcValue[chan] = adjust(diff,chan);
    }
    chan++;
  }
}

Читаем о функции micros() в документации Arduino:

Returns the number of microseconds since the Arduino board began running the current program. This number will overflow (go back to zero), after approximately 70 minutes. On 16 MHz Arduino boards (e.g. Duemilanove and Nano), this function has a resolution of four microseconds (i.e. the value returned is always a multiple of four). On 8 MHz Arduino boards (e.g. the LilyPad), this function has a resolution of eight microseconds.

То есть мало того, что функция не отличается особой точностью и всегда возвращает значения кратные 4 мкс, так она еще и переполнится через 70 минут, выдав нам какое-нибудь кривое значение замеренного интервала. Не годится. Лучше будет использовать таймер и его прерывания по захвату внешних сигналов.

Смотрим дальше: большая часть данных о положении джойстика искусственно ограничены одним байтом (0-255):
// Convert a value in the range 1000-2000 to 0-255
byte stickValue(int rcVal) {
  return map( constrain(rcVal - 1000, 0, 1000), 0, 1000, 0, 255);
}

Хм, хотелось бы поточнее. Но для этого придется перепилить HID дескриптор и поправить все сопутствующие типы данных.

Сказано — сделано!
Форкаем репозиторий, переписываем код на использование таймера для отсчета PPM интервалов:
void initTimer(void) { 
	// Input Capture setup
	// ICNC1: =0 Disable Input Capture Noise Canceler to prevent delay in reading
	// ICES1: =1 for trigger on rising edge
	// CS11: =1 set prescaler to 1/8 system clock (F_CPU)
	TCCR1A = 0;
	TCCR1B = (0<<ICNC1) | (1<<ICES1) | (1<<CS11);
	TCCR1C = 0;

	// Interrupt setup
	// ICIE1: Input capture 
	TIFR1 = (1<<ICF1); // clear pending
	TIMSK1 = (1<<ICIE1); // and enable
}
...

ISR(TIMER1_CAPT_vect) {
	union twoBytes {
		uint16_t word;
		uint8_t  byte[2];
	} timeValue;

	uint16_t now, diff;
	static uint16_t last = 0;
	static uint8_t chan = 0;
	
	timeValue.byte[0] = ICR1L;    // grab captured timer value (low byte)
	timeValue.byte[1] = ICR1H;    // grab captured timer value (high byte)

	now = timeValue.word;
	diff = now - last;
	last = now;

	//all numbers are microseconds multiplied by TIMER_COUNT_DIVIDER (as prescaler is set to 1/8 of 16 MHz)
	if(diff > (NEWFRAME_PULSE_WIDTH * TIMER_COUNT_DIVIDER)) {
		chan = 0;  // new data frame detected, start again
	}
	else {
		if(diff > (MIN_PULSE_WIDTH * TIMER_COUNT_DIVIDER - THRESHOLD) 
			&& diff < (MAX_PULSE_WIDTH * TIMER_COUNT_DIVIDER + THRESHOLD) 
			&& chan < RC_CHANNELS_COUNT) 
		{
			rcValue[chan] = adjust(diff, chan); //store detected value
		}
		chan++; //no value detected within expected range, move to next channel
	}
}

Увеличиваем интервалы отклонения стиков до 0-1000 в HID descriptor:
	// Joystick
	0x05, 0x01,			// USAGE_PAGE (Generic Desktop)
	0x09, 0x04,			// USAGE (Joystick)
	0xa1, 0x01,			// COLLECTION (Application)
	0x85, JOYSTICK_REPORT_ID,	//   REPORT_ID (3)
	...
	0xA1, 0x00,		    //   COLLECTION (Physical)
	0x09, 0x30,		    //     USAGE (x)
	0x09, 0x31,		    //     USAGE (y)
	0x09, 0x33,		    //     USAGE (rx)
	0x09, 0x34,		    //     USAGE (ry)
	0x15, 0x00,		    //	   LOGICAL_MINIMUM (0)
	0x26, 0xE8, 0x03,	    //     LOGICAL_MAXIMUM (1000)
	0x75, 0x10,	  	    //	   REPORT_SIZE (16)
	0x95, 0x04,		    //     REPORT_COUNT (4)
	0x81, 0x02,		    //     INPUT (Data,Var,Abs)
	0xc0,			    //   END_COLLECTION

	0xc0				// END_COLLECTION

Попутно меняем uint8_t на uint16_t везде где передаются значения отклонений стиков.
Убираем лишний код, добавляем десяток #define и получается неплохой скетч, заточенный на работу в качестве адаптера PPM-USB.

Результат работы доступен в гитхабе: github.com/voroshkov/Leonardo-USB-RC-Adapter

Настройки скетча:

Имеет смысл убрать дефайн для футабы, если у вас другая аппаратура:
#define FUTABA

и по-необходимости подстроить значения микросекунд в параметрах, если ваша аппаратура выдает другие тайминги:
#define STICK_HALFWAY 500
#define STICK_CENTER 1500
#define THRESHOLD 100


Компиляция:

Чтобы скомпилировать и залить скетч, нужно подменить библиотеки работы с USB в самой среде Arduino, предварительно сделав бэкапы.
Для этого идем в недра Arduino по пути ...\Arduino\hardware\arduino\cores\arduino\, бэкапим usbapi.h и hid.cpp, после чего перезаписываем их одноименными файлами из папки ArduinoLibs репозитория. Далее открываем скетч, подключаем Arduino Leonardo и заливаем.

Подключение:

Все до безобразия просто: с одной стороны вставляем USB, c другой – паяем два провода (на Digital Pin 4 и землю) и втыкаем соответственно в PPM и землю разъема передатчика. Получается как-то так:

В Windows 7 устройство распознается как составное (клавиатура, мышь, джойстик) с именем Arduino Leonardo.

Отдельного упоминания заслуживает разъем в аппаратуре. Где-то это обычный аудио-джек, а где-то (как в моей Futaba 7C) – нечто похитрее:


Для сборки различных «папа»-разъемов я уже давно успешно использую горячий клей. Для этого кладем на «маму» бумагу, или полиэтилен, протыкаем эту подложку пинами, чтобы они воткнулись в разъем с другой стороны, а затем постепенно заливаем между пинами клей, попутно придавая ему форму мокрыми пальцами. Провода, конечно, нужно припаять заранее, чтобы не расплавить застывший клей при пайке. Получается не всегда эстетично, но весьма функционально:

(Здесь в разъеме 4 штырька нужны для однозначного позиционирования, рабочих только два.)

Вот и все. Качаем симулятор, подключаем аппаратуру и тренируем навыки пилотирования, попивая горячий чай перед камином темными зимними вечерами.

P.S.

Что делать если нет Arduino Leonardo, но есть вот такая Minimum Development Board на STM32F103C8T6?

Не волнуйтесь, все путем. Для вас, а также для собственного развития, я портировал уже упомянутый проект Алексея Станкевича.
Исходники и скомпилированные бинарники для заливки в этот контроллер можно найти здесь: github.com/voroshkov/STM32-RC-USB-Adapter.

На все вопросы с удовольствием отвечу в комментах.

Приятных полетов!

Комментарии (1)


  1. pokryshkin
    08.01.2016 20:37

    (Здесь в разъеме 4 штырька нужны для однозначного позиционирования, рабочих только два.)

    на самом деле есть смысл коротить еще 2 контакта, кажется 2 правых в нижнем ряду (если смотреть на рисунок из статьи), от этого аппа включается, но не питает высокочастотный модуль.