Памятка по декодированию PWM и PPM сигнала
7 августа 2017
Типичная радиоаппаратура работает как-то так. Есть передатчик (собственно, сама аппа) и приемник. Приемник с передатчиком общаются по какому-то своему протоколу, часто закрытому. Приемник декодирует этот протокол и передает положение ручек на аппаратуре дальше, например, полетному контроллеру (ПК) квадрокоптера. ПК и приемник общаются по своему протоколу, который должны понимать оба. В этом месте большой популярностью пользуются PWM и PPM. Есть и другие варианты, в частности, SBUS, DSM2 и DSX, но в рамках данной статьи мы рассмотрим только PWM и PPM.
PWM (Pulse Width Modulation), он же ШИМ, всем хорошо знаком. Раз в 20 мс по проводу передается положительный импульс, длительность которого определяет положение ручки. Длительность импульса около 1000 микросекунд соответствует одному крайнему положению, а 2000 микросекунд — другому крайнему положению. На практике присутствует некоторая ошибка измерения, из-за чего, в частности, полетный контроллер и аппаратура требуют калибровки. PWM прост и понятен, но плох тем, что требует отдельного провода на каждый канал (то есть, каждую ручку аппаратуры). Сейчас многие аппаратуры имеют 8 и более каналов, а значит при использовании PWM приходится использовать много проводов.
PPM (Pulse Position Modulation) — это такая попытка запихнуть много PWM сигналов в один провод, ну или, по крайней мере, мне лично нравится о нем так думать. PPM всегда передает короткие импульсы. Паузы между фронтами импульсов соответствуют длительности одного PWM сигнала. Сигналы передаются последовательно, сначала значение на первом канале, затем на втором, и так далее. Соответствие между PPM и PWM сигналами хорошо отражает следующая осциллограмма, записанная на Rigol DS1054Z:
Осциллограмма снята с приемника RadioLink R8EF, имеющего режим, при котором на два его пина подаются сигналы PPM и SBUS, а на остальные пины — PWM сигнал для каналов с 3 по 8. Поэтому PWM сигналы для каналов 1 и 2 не показаны. Тут можно видеть несколько интересных особенностей. Во-первых, данный конкретный приемник использует для логической единицы 3.3 В, хотя и питается от 5 В. Во-вторых, в нем PPM сигнал инвертирован по сравнению с тем, как его обычно рисуют на картинках (например, в этом треде). Как ни странно, все написанное выше про фронты импульсов при этом остается верным. В частности, на картинке курсорами выделены фронты, соответствующие PWM сигналу на 3 канале. При этом PPM сигнал оказывается смещенным относительно PWM сигналов, но вообще-то приемник и не обязан был их как-то синхронизировать.
PPM работает хорошо до тех пор, пока вы не пытаетесь засунуть в него больше 8 каналов. Если каналов больше, то либо сигналы начинают обновляться с задержкой больше 20 мс (как в случае с PWM), либо приходится «сжимать» сигналы, теряя их точность, либо использовать дополнительные провода. Я не видел, чтобы по PPM передавали больше 8 каналов.
Хорошо, теперь допустим, что я хочу управлять своим роботом при помощи радиоаппаратуры. Вот соответствующий код для PWM:
#include <PinChangeInt.h>
#define NCHANNELS 8
#define CH1_PIN 5 // CH2_PIN = CH1_PIN + 1, etc
volatile int pwm_value[NCHANNELS] = { 0 };
volatile int prev_time[NCHANNELS] = { 0 };
void rising();
void falling() {
uint8_t pin = PCintPort::arduinoPin;
PCintPort::attachInterrupt(pin, &rising, RISING);
pwm_value[pin - CH1_PIN] = micros() - prev_time[pin - CH1_PIN];
}
void rising()
{
uint8_t pin = PCintPort::arduinoPin;
PCintPort::attachInterrupt(pin, &falling, FALLING);
prev_time[pin-CH1_PIN] = micros();
}
void setup() {
for(uint8_t i = 0; i < NCHANNELS; i++) {
pinMode(CH1_PIN + i, INPUT_PULLUP);
PCintPort::attachInterrupt(CH1_PIN + i, &rising, RISING);
}
Serial.begin(9600);
}
void loop() {
for(uint8_t i = 0; i < NCHANNELS; i++)
Serial.println("ch" + String(i+1) + " = " + String(pwm_value[i]));
Serial.println("----------------");
delay(1000);
}
А это — код для PPM:
#include <PinChangeInt.h>
#define PPM_PIN 6
#define MAX_CHANNELS 12
volatile int pwm_value[MAX_CHANNELS] = { 0 };
volatile int prev_time = 0;
volatile int curr_channel = 0;
volatile bool overflow = false;
void rising()
{
int tstamp = micros();
/* overflow should never acutally happen, but who knows... */
if(curr_channel < MAX_CHANNELS) {
pwm_value[curr_channel] = tstamp - prev_time;
if(pwm_value[curr_channel] > 2100) { /* it's actually a sync */
pwm_value[curr_channel] = 0;
curr_channel = 0;
} else
curr_channel++;
} else
overflow = true;
prev_time = tstamp;
}
void setup() {
pinMode(PPM_PIN, INPUT_PULLUP);
PCintPort::attachInterrupt(PPM_PIN, &rising, RISING);
Serial.begin(9600);
}
void loop() {
for(uint8_t i = 0; i < MAX_CHANNELS; i++)
Serial.println("ch" + String(i+1) + " = " + String(pwm_value[i]));
if(overflow)
Serial.println("OVERFLOW!");
Serial.println("----------------");
delay(1000);
}
Код был проверен на радиоаппаратуре RadioLink T8FB и приемнике к ней R8EF. На следующем фото приемник, переведенный в режим PPM, подключен к Arduino Nano с залитой в нее прошивкой для PPM:
Пример отладочного вывода по UART:
ch2 = 1500
ch3 = 1508
ch4 = 1496
ch5 = 2000
ch6 = 996
ch7 = 1996
ch8 = 2000
...
Назначение использованной выше библиотеки PinChangeInt должно быть понятно по коду. Она позволяет вешать прерывания на передний и задний фронты (нарастание и спад) сигнала на заданных пинах. Подробности об этой библиотеке можно найти здесь и здесь. Полная версия кода прошивок для декодирования PWM и PPM сигналов доступна на GitHub.
Вооруженные полученными здесь знаниями, мы можем не только управлять Arduino при помощи радиоаппаратуры, но и, например, паять перекодировщики PWM в/из PPM (если не хочется платить за готовые на AliExpress), или даже изготавливать собственные радиоаппаратуры на базе какого-нибудь NRF24L01 или иного радиомодуля.
Метки: AVR, Электроника.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.