DIY PWM to PPM Converter for 2.4GHz Receiver using Arduino

by Oscar

For radio receiver, there are a few output signal formats. The traditional and also most common type of RX signal is the PWM and basically PWM requires 1 cable per channel. PPM is now getting more and more popular, because it can handle all 8 channels in 1 signal wire.

Some of the links on this page are affiliate links. I receive a commission (at no extra cost to you) if you make a purchase after clicking on one of these affiliate links. This helps support the free content for the community on this website. Please read our Affiliate Link Policy for more information.

Find out more detail on radio receiver signal types.

You can buy a commercially ready PWM to PPM converter (which also does SBUS output as well): Amazon | Banggood | GetFPV

pwm-ppm-sbus-converter-receiver-rx

But for those who enjoy tinkering and DIY, here is a fun project for you.

 

Al Prettybong on Multicopter International Group shared with me how he made a PWM to PPM converter using an Arduino Pro Mini, and I thought I should share this with everyone.

Buy the Arduino from: Banggood | Amazon

Connection

The connection is really simple.

5V and GND on receiver is connected to “RAW” and GND pins on the Arduino board. Ch1 to Ch8 is connected D0 to D7 on the Arduino. (if you are using receiver that has fewer channels, you don’t have to worry about the rest of the pins on the Arduino)

For a more detail connection diagram, check out the top picture in this article.

pwm-ppm-converter-ardunio-rx-radio-receiver-back pwm-ppm-converter-ardunio-rx-radio-receiver-connection

He removed all the servo pins on this receiver, and soldered direct the arduino pwm to ppm converter. He said this has been running successfully for about 8 months, and i did the same conversion on an 8 channel receiver also has failsafe built in to the pwm ppm converter. Failsafe channel can be set up in the code.

There are 3 wires that will be connected to the FC, 5V, GND and PPM (Green).

And finally he put heatshrink over this unit, and now this cheap PWM RX has turned into a powerful PPM RX :) He put clear heatshrink in the middle so he could see the status LEDs.

pwm-ppm-converter-ardunio-rx-radio-receiver-finish

Upload Sketch on Arduino

I haven’t tested this code yet, but Al told me there is NO changes to the code, just copy and upload it to your Arduino and it will work.

Here is the main sketch.

// ------------------------------------------------------------------------------------------------------------------------------------------------------------ // 8 Channel PWM to 1 channel PPM converter for RC receivers, using Arduino // // // ..and has been hacked code to: // only support Atmel328 chips ( as found on Arduino Duemilanove or Arduino Uno ) chips // not support any "error" mode/s, just 8 PWM-IN channels TO one single PPM OUT // not support any LED indicators m just PWM-IN, and PPM-OUT // Integrated the one library that is used to the sketch, for easy user experience. // made it Arduino IDE compatible, so it uses standard bootloader and Serial uploader, like all realy Arduino/s. // make compile-time option to either "hold last good PPM value" or "hold default value/s" in case of // no actual input signal for each channel. see FAILHOLD and FAILCENTRE in .h file // . // ------------------------------------------------------------------------------------------------------------------------------------------------------------ // PREPROCESSOR DIRECTIVES // ------------------------------------------------------------------------------------------------------------------------------------------------------------ #include "Arduino.h" #include "ppm_encoder.h" #include <util/delay.h> #include <avr/io.h> #define ERROR_THRESHOLD 2 // Number of servo input errors before alerting #define ERROR_DETECTION_WINDOW 3000 * LOOP_TIMER_10MS // Detection window for error detection (default to 30s) #define ERROR_CONDITION_DELAY 500 * LOOP_TIMER_10MS // Servo error condition LED delay (LED blinking duration) #define PASSTHROUGH_MODE_ENABLED // Comment this line to remove CH8 radio passthrough mode support (hardware failsafe for Arduplane) #define PASSTHROUGH_CHANNEL 8 * 2 // Channel for passthrough mode selection #define PASSTHROUGH_CHANNEL_OFF_US ONE_US * 1600 - PPM_PRE_PULSE // Passthrough off threshold #define PASSTHROUGH_CHANNEL_ON_US ONE_US * 1800 - PPM_PRE_PULSE // Passthrough on threshold #define THROTTLE_CHANNEL 3 * 2 // Throttle Channel #define THROTTLE_CHANNEL_LED_TOGGLE_US ONE_US * 1200 - PPM_PRE_PULSE // Throttle Channel Led toggle threshold #define LED_LOW_BLINKING_RATE 125 * LOOP_TIMER_10MS // Led blink rate for low throttle position (half period) // Timers #define TIMER0_10MS 156 // Timer0 ticks for 10 ms duration #define TIMER1_10MS 20000 // Timer1 ticks for 10 ms duration #define TIMER2_100MS 1562 // Timer2 ticks for 100 ms duration #define LOOP_TIMER_10MS 10 // Loop timer ticks for 10 ms duration // LED Code #define SPACE_SHORT_DURATION 40 * LOOP_TIMER_10MS // Space after short symbol #define SPACE_LONG_DURATION 75 * LOOP_TIMER_10MS // Space after long symbol #define SYMBOL_SHORT_DURATION 20 * LOOP_TIMER_10MS // Short symbol duration #define SYMBOL_LONG_DURATION 100 * LOOP_TIMER_10MS // Long symbol duration #define INTER_CODE_DURATION 150 * LOOP_TIMER_10MS // Inter code duration #define INTER_CODE 0 // Symbols value for coding #define SHORT_SYMBOL 1 #define LONG_SYMBOL 2 #define SHORT_SPACE 3 #define LONG_SPACE 4 #define LOOP 5 // ------------------------------------------------------------------------------------------------------------------------------------------------------------ // PPM ENCODER INIT AND AUXILIARY TASKS // ------------------------------------------------------------------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------------------------------------------------------------------ // LOCAL VARIABLES // ------------------------------------------------------------------------------------------------------------------------------------------------------------ bool localinit = true; // We are inside init sequence bool mux_passthrough = false; // Mux passthrough mode status Flag : passthrough is off uint16_t led_acceleration; // Led acceleration based on throttle stick position bool servo_error_condition = false; // Servo signal error condition static uint16_t servo_error_detection_timer=0; // Servo error detection timer static uint16_t servo_error_condition_timer=0; // Servo error condition timer static uint16_t blink_led_timer = 0; // Blink led timer #ifdef PASSTHROUGH_MODE_ENABLED static uint8_t mux_timer = 0; // Mux timer static uint8_t mux_counter = 0; // Mux counter static int8_t mux_check = 0; static uint16_t mux_ppm = 500; #endif static uint16_t led_code_timer = 0; // Blink Code Timer static uint8_t led_code_symbol = 0; // Blink Code current symbol // ------------------------------------------------------------------------------------------------------------------------------------------------------------ // LOCAL FUNCTIONS // ------------------------------------------------------------------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------ // Led blinking (non blocking) function // ------------------------------------------------------------------------------ uint8_t blink_led ( uint16_t half_period ) // ( half_period max = 65 s ) { blink_led_timer++; if ( blink_led_timer < half_period ) // If half period has not been reached { return 0; // Exit timer function and return 0 } else // half period reached - LED Toggle { PPM_PORT ^= ( 1 << PB0 ); // Toggle status LED blink_led_timer = 0; // Blink led timer reset return 1; // half period reached - Exit timer function and return 1 } } // ------------------------------------------------------------------------------ // Led code (non blocking) function // ------------------------------------------------------------------------------ void blink_code_led ( uint8_t code ) { const uint8_t coding[2][14] = { // PPM_PASSTROUGH_MODE { INTER_CODE, LONG_SYMBOL, LONG_SPACE, SHORT_SYMBOL, SHORT_SPACE, SHORT_SYMBOL, LOOP }, // JETI_MODE { INTER_CODE, LONG_SYMBOL, LONG_SPACE, SHORT_SYMBOL, SHORT_SPACE, SHORT_SYMBOL, SHORT_SPACE, SHORT_SYMBOL,LOOP } }; led_code_timer++; switch ( coding [ code - 2 ] [ led_code_symbol ] ) { case INTER_CODE: if ( led_code_timer < ( INTER_CODE_DURATION ) ) return; else PPM_PORT |= ( 1 << PB0 ); // Enable status LED break; case LONG_SYMBOL: // Long symbol if ( led_code_timer < ( SYMBOL_LONG_DURATION ) ) return; else PPM_PORT &= ~( 1 << PB0 ); // Disable status LED break; case SHORT_SYMBOL: // Short symbol if ( led_code_timer < ( SYMBOL_SHORT_DURATION ) ) return; else PPM_PORT &= ~( 1 << PB0 ); // Disable status LED break; case SHORT_SPACE: // Short space if ( led_code_timer < ( SPACE_SHORT_DURATION ) ) return; else PPM_PORT |= ( 1 << PB0 ); // Enable status LED break; case LONG_SPACE: // Long space if ( led_code_timer < ( SPACE_LONG_DURATION ) ) return; else PPM_PORT |= ( 1 << PB0 ); // Enable status LED break; case LOOP: // Loop to code start led_code_symbol = 0; return; break; } led_code_timer = 0; // Code led timer reset led_code_symbol++; // Next symbol return; // LED code function return } // ------------------------------------------------------------------------------ // ppm reading helper - interrupt safe and non blocking function // ------------------------------------------------------------------------------ uint16_t ppm_read( uint8_t channel ) { uint16_t ppm_tmp = ppm[ channel ]; while( ppm_tmp != ppm[ channel ] ) ppm_tmp = ppm[ channel ]; return ppm_tmp; } // ------------------------------------------------------------------------------------------------------------------------------------------------------------ // INITIALISATION CODE // ------------------------------------------------------------------------------------------------------------------------------------------------------------ void setup() { // ------------------------------------------------------------------------------ // Reset Source checkings // ------------------------------------------------------------------------------ if (MCUSR & 1) // Power-on Reset { MCUSR=0; // Clear MCU Status register // custom code here } else if (MCUSR & 2) // External Reset { MCUSR=0; // Clear MCU Status register // custom code here } else if (MCUSR & 4) // Brown-Out Reset { MCUSR=0; // Clear MCU Status register brownout_reset=true; } else // Watchdog Reset { MCUSR=0; // Clear MCU Status register // custom code here } // ------------------------------------------------------------------------------ // Servo input and PPM generator init // ------------------------------------------------------------------------------ ppm_encoder_init(); // ------------------------------------------------------------------------------ // Outputs init // ------------------------------------------------------------------------------ PPM_DDR |= ( 1 << PB0 ); // Set LED pin (PB0) to output PPM_DDR |= ( 1 << PB1 ); // Set MUX pin (PB1) to output PPM_DDR |= ( 1 << PPM_OUTPUT_PIN ); // Set PPM pin (PPM_OUTPUT_PIN, OC1B) to output // ------------------------------------------------------------------------------ // Timer0 init (normal mode) used for LED control and custom code // ------------------------------------------------------------------------------ TCCR0A = 0x00; // Clock source: System Clock TCCR0B = 0x05; // Set 1024x prescaler - Clock value: 15.625 kHz - 16 ms max time TCNT0 = 0x00; OCR0A = 0x00; // OC0x outputs: Disconnected OCR0B = 0x00; TIMSK0 = 0x00; // Timer 1 interrupt disable // ------------------------------------------------------------------------------ // Enable global interrupt // ------------------------------------------------------------------------------ sei(); // Enable Global interrupt flag // ------------------------------------------------------------------------------ // Disable radio passthrough (mux chip A/B control) // ------------------------------------------------------------------------------ PPM_PORT |= ( 1 << PB1 ); // Set PIN B1 to disable Radio passthrough (mux) } void loop() { // ------------------------------------------------------------------------------------------------------------------------------------------------------------ // AUXILIARY TASKS // ------------------------------------------------------------------------------------------------------------------------------------------------------------ PWM_LOOP: // SERVO_PWM_MODE while( 1 ) { _delay_us (950); // Slow down while loop } // PWM Loop end } // main lopo function end 

And then create a new tab in your Arduino IDE and copy this, name it “ppm_encoder.h”, and copy the following code in the tab.

// ------------------------------------------------------------- #ifndef _PPM_ENCODER_H_ #define _PPM_ENCODER_H_ #include <avr/io.h> // ------------------------------------------------------------- #include <avr/interrupt.h> #include <avr/wdt.h> #include <util/delay.h> // ------------------------------------------------------------- // SERVO INPUT FILTERS // ------------------------------------------------------------- // Using both filters is not recommended and may reduce servo input resolution // #define _AVERAGE_FILTER_ // Average filter to smooth servo input capture jitter // #define _JITTER_FILTER_ // Cut filter to remove 0,5us servo input capture jitter // ------------------------------------------------------------- #ifndef F_CPU #define F_CPU 16000000UL #endif #ifndef true #define true 1 #endif #ifndef false #define false 0 #endif //#ifndef bool //#define bool boolean //#endif // 328 does not define PBX but defines an equivalent as PORTBX, comment these lines out if you already have a PB2 defined. #define PB2 PORTB2 #define PB1 PORTB1 #define PB0 PORTB0 // ------------------------------------------------------------- // SERVO INPUT MODE - !EXPERIMENTAL! // ------------------------------------------------------------- #define SERVO_PWM_MODE 1 // Normal 8 channel servo (pwm) input // Servo input mode (jumper (default), pwm, ppm, jeti or spektrum) volatile uint8_t servo_input_mode = SERVO_PWM_MODE; // ------------------------------------------------------------- // Number of Timer1 ticks in one microsecond #define ONE_US F_CPU / 8 / 1000 / 1000 // 400us PPM pre pulse #define PPM_PRE_PULSE ONE_US * 400 // ------------------------------------------------------------- // SERVO LIMIT VALUES // ------------------------------------------------------------- // Servo minimum position #define PPM_SERVO_MIN ONE_US * 900 - PPM_PRE_PULSE // Servo center position #define PPM_SERVO_CENTER ONE_US * 1500 - PPM_PRE_PULSE // Servo maximum position #define PPM_SERVO_MAX ONE_US * 2100 - PPM_PRE_PULSE // Throttle default at power on #define PPM_THROTTLE_DEFAULT ONE_US * 1100 - PPM_PRE_PULSE // Throttle during failsafe #define PPM_THROTTLE_FAILSAFE ONE_US * 900 - PPM_PRE_PULSE // CH5 power on values (mode selection channel) //#define PPM_CH5_MODE_4 ONE_US * 1555 - PPM_PRE_PULSE // ------------------------------------------------------------- // Number of servo input channels #define SERVO_CHANNELS 8 // PPM period 18.5ms - 26.5ms (54hz - 37Hz) #define PPM_PERIOD ONE_US * ( 22500 - ( 8 * 1500 ) ) // Size of ppm[..] data array ( servo channels * 2 + 2) #define PPM_ARRAY_MAX 18 // Data array for storing ppm (8 channels) pulse widths. volatile uint16_t ppm[ PPM_ARRAY_MAX ] = { PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 1 PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 2 PPM_PRE_PULSE, PPM_THROTTLE_DEFAULT, // Channel 3 (throttle) PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 4 PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 5 PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 6 PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 7 PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 8 PPM_PRE_PULSE, PPM_PERIOD }; // ------------------------------------------------------------- // SERVO FAILSAFE VALUES // ------------------------------------------------------------- const uint16_t failsafe_ppm[ PPM_ARRAY_MAX ] = { PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 1 PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 2 PPM_PRE_PULSE, PPM_THROTTLE_FAILSAFE, // Channel 3 (throttle) PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 4 PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 5 PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 6 PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 7 PPM_PRE_PULSE, PPM_SERVO_CENTER, // Channel 8 PPM_PRE_PULSE, PPM_PERIOD }; // ------------------------------------------------------------- // AVR parameters for ArduPilot MEGA v1.4 PPM Encoder (ATmega328P) #if defined (__AVR_ATmega328P__) || defined (__AVR_ATmega328__) #define SERVO_DDR DDRD #define SERVO_PORT PORTD #define SERVO_INPUT PIND // PCIE2 PC Interrupt enable 2 is for Arduino Pins (D0-D7), also called PORTD. #define SERVO_INT_VECTOR PCINT2_vect #define SERVO_INT_MASK PCMSK2 #define SERVO_INT_CLEAR_FLAG PCIF2 #define SERVO_INT_ENABLE PCIE2 #define SERVO_TIMER_CNT TCNT1 #define PPM_DDR DDRB #define PPM_PORT PORTB #define PPM_OUTPUT_PIN PB2 #define PPM_INT_VECTOR TIMER1_COMPB_vect #define PPM_COMPARE OCR1B #define PPM_COMPARE_FLAG COM1B0 #define PPM_COMPARE_ENABLE OCIE1B #else #error NO SUPPORTED DEVICE FOUND! ( ATmega328/p) #endif // Used to indicate invalid SERVO input signals //volatile uint8_t servo_input_errors = 0; // Used to indicate missing SERVO input signals volatile bool servo_input_missing = true; // Used to indicate if PPM generator is active volatile bool ppm_generator_active = false; // Used to indicate a brownout restart volatile bool brownout_reset = false; // ------------------------------------------------------------------------------ // PPM GENERATOR START - TOGGLE ON COMPARE INTERRUPT ENABLE // ------------------------------------------------------------------------------ // this starts OUTGOING PPM stream on PPM_PORT (PORTB, Arduino D8-D13) at PPM_OUTPUT_PIN (PB2, arduino pin D10) void ppm_start( void ) { // Prevent reenabling an already active PPM generator if( ppm_generator_active ) return; // Store interrupt status and register flags uint8_t SREG_tmp = SREG; // Stop interrupts cli(); // Make sure initial output state is low PPM_PORT &= ~(1 << PPM_OUTPUT_PIN); // Wait for output pin to settle //_delay_us( 1 ); // Set initial compare toggle to maximum (32ms) to give other parts of the system time to start SERVO_TIMER_CNT = 0; PPM_COMPARE = 0xFFFF; // Set toggle on compare output TCCR1A = (1 << PPM_COMPARE_FLAG); // Set TIMER1 8x prescaler TCCR1B = ( 1 << CS11 ); // Enable output compare interrupt TIMSK1 |= (1 << PPM_COMPARE_ENABLE); // Indicate that PPM generator is active ppm_generator_active = true; // Restore interrupt status and register flags SREG = SREG_tmp; } // ------------------------------------------------------------------------------ // PPM GENERATOR STOP - TOGGLE ON COMPARE INTERRUPT DISABLE // ------------------------------------------------------------------------------ void ppm_stop( void ) { // Store interrupt status and register flags uint8_t SREG_tmp = SREG; // Stop interrupts cli(); // Disable output compare interrupt TIMSK1 &= ~(1 << PPM_COMPARE_ENABLE); // Reset TIMER1 registers TCCR1A = 0; TCCR1B = 0; // Indicate that PPM generator is not active ppm_generator_active = false; // Restore interrupt status and register flags SREG = SREG_tmp; } // ------------------------------------------------------------------------------ // Watchdog Interrupt (interrupt only mode, no reset) // ------------------------------------------------------------------------------ ISR( WDT_vect ) // If watchdog is triggered then enable missing signal flag and copy power on or failsafe positions { // Use failsafe values if PPM generator is active or if chip has been reset from a brown-out if ( ppm_generator_active || brownout_reset ) { // Copy failsafe values to ppm[..] for ( uint8_t i = 0; i < PPM_ARRAY_MAX; i++ ) { ppm[ i ] = failsafe_ppm[ i ]; } } // Set missing receiver signal flag servo_input_missing = true; // Reset servo input error flag //servo_input_errors = 0; } // ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------ // SERVO/PPM INPUT - PIN CHANGE INTERRUPT, for any Arduino pin D0 -> D7 // ------------------------------------------------------------------------------ ISR( SERVO_INT_VECTOR ) { // Servo pulse start timing static uint16_t servo_start[ SERVO_CHANNELS ] = { 0, 0, 0, 0, 0, 0, 0, 0 }; // Missing throttle signal failsafe static uint8_t throttle_timeout = 0; // Servo input pin storage static uint8_t servo_pins_old = 0; // Used to store current servo input pins uint8_t servo_pins; // Read current servo pulse change time uint16_t servo_time = SERVO_TIMER_CNT; // ------------------------------------------------------------------------------ // SERVO PWM MODE // ------------------------------------------------------------------------------ CHECK_PINS_START: // Start of servo input check // Store current servo input pins servo_pins = SERVO_INPUT; // Calculate servo input pin change mask uint8_t servo_change = servo_pins ^ servo_pins_old; // Set initial servo pin and channel uint8_t servo_pin = 1; uint8_t servo_channel = 0; CHECK_PINS_LOOP: // Input servo pin check loop // Check for pin change on current servo channel if( servo_change & servo_pin ) { // if (( servo_pin == 1 ) && ( ppm_generator_active = false) ) ppm_start(); // if (( servo_pin == 8 ) && ( ppm_generator_active = true) ) ppm_stop(); // High (raising edge) if( servo_pins & servo_pin ) { servo_start[ servo_channel ] = servo_time; } else { // Get servo pulse width uint16_t servo_width = servo_time - servo_start[ servo_channel ] - PPM_PRE_PULSE; // Calculate servo channel position in ppm[..] uint8_t _ppm_channel = ( servo_channel << 1 ) + 1; // Check that servo pulse signal is valid before sending to ppm encoder if( servo_width > PPM_SERVO_MAX ) goto CHECK_PINS_ERROR; if( servo_width < PPM_SERVO_MIN ) goto CHECK_PINS_ERROR; goto CHECK_PINS_NOERROR; CHECK_PINS_ERROR: // on width input error, use defailt/failsave value, OR previous value // choose the error handling type here! #define FAILCENTRE 1 #ifdef FAILCENTRE servo_width = failsafe_ppm[ _ppm_channel ]; // failsafe defaults, most channels centred, throttle lowered. #endif #ifdef FAILHOLD servo_width = ppm[ _ppm_channel ]; // all channels hold their previous position! #endif CHECK_PINS_NOERROR: //Reset throttle failsafe timeout if( _ppm_channel == 5 ) throttle_timeout = 0; #ifdef _AVERAGE_FILTER_ // Average filter to smooth input jitter servo_width += ppm[ _ppm_channel ]; servo_width >>= 1; #endif #ifdef _JITTER_FILTER_ // 0.5us cut filter to remove input jitter int16_t ppm_tmp = ppm[ _ppm_channel ] - servo_width; if( ppm_tmp == 1 ) goto CHECK_PINS_NEXT; if( ppm_tmp == -1 ) goto CHECK_PINS_NEXT; #endif // Update ppm[..] ppm[ _ppm_channel ] = servo_width; } } CHECK_PINS_NEXT: // Select next servo pin servo_pin <<= 1; // Select next servo channel servo_channel++; // Check channel and process if needed if( servo_channel < SERVO_CHANNELS ) goto CHECK_PINS_LOOP; goto CHECK_PINS_DONE; // All servo input pins has now been processed CHECK_PINS_DONE: // Reset Watchdog Timer wdt_reset(); // Set servo input missing flag false to indicate that we have received servo input signals servo_input_missing = false; // Store current servo input pins for next check servo_pins_old = servo_pins; // Start PPM generator if not already running if( ppm_generator_active == false ) ppm_start(); // Throttle failsafe if( throttle_timeout++ >= 128 ) { // Reset throttle timeout throttle_timeout = 0; // Set throttle failsafe value ppm[ 5 ] = PPM_THROTTLE_FAILSAFE; } //Has servo input changed while processing pins, if so we need to re-check pins if( servo_pins != SERVO_INPUT ) goto CHECK_PINS_START; // Clear interrupt event from already processed pin changes PCIFR |= (1 << SERVO_INT_CLEAR_FLAG); } // ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------ // PPM OUTPUT - TIMER1 COMPARE INTERRUPT // ------------------------------------------------------------------------------ ISR( PPM_INT_VECTOR ) { // Current active ppm channel static uint8_t ppm_channel = PPM_ARRAY_MAX - 1; // Update timing for next compare toggle PPM_COMPARE += ppm[ ppm_channel ]; // Select the next ppm channel if( ++ppm_channel >= PPM_ARRAY_MAX ) { ppm_channel = 0; } } // ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------ // PPM READ - INTERRUPT SAFE PPM SERVO CHANNEL READ // ------------------------------------------------------------------------------ /* uint16_t ppm_read_channel( uint8_t channel ) { // Limit channel to valid value uint8_t _channel = channel; if( _channel == 0 ) _channel = 1; if( _channel > SERVO_CHANNELS ) _channel = SERVO_CHANNELS; // Calculate ppm[..] position uint8_t ppm_index = ( _channel << 1 ) + 1; // Read ppm[..] in a non blocking interrupt safe manner uint16_t ppm_tmp = ppm[ ppm_index ]; while( ppm_tmp != ppm[ ppm_index ] ) ppm_tmp = ppm[ ppm_index ]; // Return as normal servo value return ppm_tmp + PPM_PRE_PULSE; } */ // ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------ // PPM ENCODER INIT // ------------------------------------------------------------------------------ void ppm_encoder_init( void ) { // SERVO/PPM INPUT PINS // ------------------------------------------------------------------------------ // Set all servo input pins to inputs SERVO_DDR = 0; // Activate pullups on all input pins SERVO_PORT |= 0xFF; // SERVO/PPM INPUT - PIN CHANGE INTERRUPT // ------------------------------------------------------------------------------ if( servo_input_mode == SERVO_PWM_MODE ) { // Set servo input interrupt pin mask to all 8 servo input channels SERVO_INT_MASK = 0xFF; } // Enable servo input interrupt PCICR |= (1 << SERVO_INT_ENABLE); // PPM OUTPUT PIN // ------------------------------------------------------------------------------ // Set PPM pin to output PPM_DDR |= (1 << PPM_OUTPUT_PIN); // ------------------------------------------------------------------------------ // Enable watchdog interrupt mode // ------------------------------------------------------------------------------ // Disable watchdog wdt_disable(); // Reset watchdog timer wdt_reset(); // Start timed watchdog setup sequence WDTCSR |= (1<<WDCE) | (1<<WDE ); // Set 250 ms watchdog timeout and enable interrupt WDTCSR = (1<<WDIE) | (1<<WDP2); } // —————————————————————————— 

 

Leave a Comment

By using this form, you agree with the storage and handling of your data by this website. Note that all comments are held for moderation before appearing.

31 comments

Rajkumar 17th February 2023 - 1:24 pm

Please send pwm to PPM code

Reply
mootzeroni 18th April 2021 - 10:42 pm

Just wanted to say Hrvoje’s solution above worked first try. My servo breakout board has a couple different pins, but after two minutes of swapping attempts, all good. The “S2PW” converter board I bought on ebay was super jittery with the PPM conversion, but this Arduino Nano is smooth as silk.

Thank you.

Reply
aLDime 23rd October 2020 - 6:14 am

Grateful a lot for the work done!
Everything works great and I was able to connect the FS-i6 + iA6 to my computer for Liftoff training without any problems! I wish you success and good luck in everything!

Reply
Alexandr 11th April 2018 - 4:47 pm

Compilation error in line
// ———————————————— ——————————
ISR (SERVO_INT_VECTOR)
{
file ppm_encoder.h

Reply
Hrvoje 5th February 2018 - 6:56 am

Hi Oscar. It does not work for me.. I was using arduino micro though, but anyway it should make no difference. There is much better (and simpler) solution which i have tried and it works great on my flysky receiver using arduino nano (ATmega328p chip) so i will describe the process maybe it will help someone.

1. download .hex file from this location:
ardupilot.org/plane/docs/common-downloads_advanced_user_tools.html#arduppm-v2-3-16-atmega328p-firmware-for-apm1-x-copter-and-standalone-ppm-encoder

You should download:
ArduPPM_v2.3.16_ATMega328p_for_ArduCopter.hex_.zip for copter or
ArduPPM_v2.3.16_ATMega328p_for_ArduPlane.hex_.zip for plane.

Unzip the file

2. Download Xloader:
You can download Xloader from here: xloader.russemotto.com/
Unzip the file.

3. Connect Arduino and receiver assembly (wiring as described by Oscar’s blog, see picture on top of this page for wiring) to computer via USB

4. Run Xloader.exe and fill in:
.hex file location (browse to wherever it is saved on your computer)
Chose device from the list (in my case it is Nano ATmega328)
Chose COM port where you plugged in your arduino to computer USB (in my case COM6)
Baud rate should automatically change so you dont have to change it (in my case is 57600)

PRESS UPLOAD and you are done!

The .hex file will download straight to your arduino board and now you have PPM encoder.

Have fun, happy flying!

Reply
Srijal 13th January 2018 - 11:51 am

I converted my Turnigy9x from PWM to PPM successfully:
photos.app.goo.gl/veqGmXUtn2a3t0zr1

Make connections as shown in the picture. I used the ArduPPM encoder firmware which is actually for their PPM encoder (ardupilot.org/copter/docs/common-ppm-encoder-8-channel-standalone-encoder.html) but works well for the Arduino Pro Mini as well.

Download the .hex file for their firmware (ArduPPM for ArduCopter), and flash it using an USBasp programmer and programming software (like SinaProg). Connections are made for programming as shown here: samopal.pro/wp13_samopal/wp-content/uploads/561/cxema.jpg

You can contact me at srijal97_at_gmail.com for any clarifications.

Reply
sandy 30th December 2017 - 2:38 am

hi oscar

I have a new 6ch transmitter branded FS-CT6B from Flysky and 6ch FS-R6B receiver with only PWM output, the question is can your arduino 8ch program for 6ch receiver ?, or should I reduce the program line to fit my 6ch receiver ? , or your arduino program can already work on receivers under 8ch ?

Reply
Yayat 1st September 2017 - 10:40 am

Hi Oscar, can I use ppm encoder for racing quad?

Reply
Atafo plato 22nd September 2016 - 9:46 am

I have a question instead of connecting my arduino to a receiver , can’t I connect it to a PC and then download or copy the signal into my arduino , or use signal converter to to download signals from my PC

Reply
Graham Williams 27th June 2016 - 11:28 am

Hi Thanks for uploading this information
I am having a problem with a new build quad i am having
I have a walkera transmitter and a Walkera reciever RX702 which uses PPM
I have bought a sp pro F3 flight controller
Neither the controller or the reciever come with pin identification ,so i don’t know what pin goes were
I do know that i can connect from Batt on the Rx reciever .I dont know what the pins are colour wise from top to bottom
eg negative,posative ect
Would i connect these to uart 1 and if so again were would i wire each to
On my board it has + – Tx and RX

Reply
CHEN 15th April 2016 - 3:28 pm

How about the space this converter ? And how to confirm the PWM signal can precise convert to the PPM Ssignal? Just by the software encoding?

Reply
John 18th December 2015 - 2:54 pm

Compiles fine on mini pro, if you get from the original source.
github.com/davidbuzz/BuzzsArduinoCode/tree/master/buzz_8pwm_to_ppm328

PPM_encoder.h is missing a few lines at the end.
The last lines should look like this….

// ——————————————————————————
// Enable watchdog interrupt mode
// ——————————————————————————
// Disable watchdog
wdt_disable();
// Reset watchdog timer
wdt_reset();
// Start timed watchdog setup sequence
WDTCSR |= (1<<WDCE) | (1<<WDE );
// Set 250 ms watchdog timeout and enable interrupt
WDTCSR = (1<<WDIE) | (1<<WDP2);
}
// ——————————————————————————

#endif // _PPM_ENCODER_H_

Reply
Jimy Ramirez 22nd November 2016 - 9:06 pm

john
You did not have any problem with the original codes?

Reply
Theo 14th December 2015 - 8:56 am

Hi Oscar,

Anybody that got it working??????
I am trying with an Arduino Pro mini but i get errors all over the ppm_encoder.h
Any help is welcome.
Thanx

Reply
Theo 11th December 2015 - 9:05 pm

Hi Oskar.
So is the above code and info valid to be used?
I am thinking of using it to transform a pwm 2.4ghz Rx into a ppm one.

Reply
Theo 12th December 2015 - 8:03 pm

I tried already but i get constant errors.

Reply
Endra Dwi 7th November 2015 - 9:04 am

Hi Oscar,

I want to ask regarding the voltage of the RX.
When i check plug my arduino into the 5V supply, why the “RAW” pin only have 1,5V?
Is it normal or not?because LED on my RX still off, only the Arduino LED board that on when i plug into the supply.

Thank you.

Reply
Oscar 10th November 2015 - 10:35 am

raw pin is an input not an output, it takes unregulated voltage and convert into 5V for the board…

Reply
David Buzz 28th October 2015 - 1:01 am

The code in the article was ungraciously taken from my github repository without attribution:
( I wrote this version of the code, years ago, and based it off another code, which I provide the link to in my github repo.)

github.com/davidbuzz/BuzzsArduinoCode/tree/master/buzz_8pwm_to_ppm328

Reply
Oscar 28th October 2015 - 5:34 pm

sorry about that David, the post was written and shared with me by a guest writer. I will look into this and update your link in the post shortly.

Reply
V_kingo 21st September 2015 - 7:07 pm

Hi Oscar: I prefer to use this from
rcgroups.com/forums/showthread.php?t=1763334&page=3

#define PPMHI PORTD |= _BV(7);
#define PPMLO PORTD &= ~_BV(7);
//written by doughboy @RCGroups and @multiwii.com
//8 channel PPM sum using atmega328 @16mhz
//
//wiring
// +——- \/ ——+
// PC6 1| |28 PC5
// PD0 2| |27 PC4
// PD1 3| |26 PC3
// PD2 4| |25 PC2
// PD3 5| |24 PC1
// PD4 6| |23 PC0
// +5v-> VCC 7| |22 GND GND 8| |21 AREF
// XTAL-> PB6 9| Pro Mini |20 AVCC PB7 10| D13 |19 PB5 PD5 11| D5 D12 |18 PB4 PD6 12| D6 D11 |17 PB3 <-RX3
// PPMSUM<- PD7 13| D7 D10 |16 PB2 PB0 14| D8 D9 |15 PB1 <-RX1
// +——————+
//

void setup() {
PCICR |= (1<<PCIE0) | (1<<PCIE2); //Pin Change Interrupt 0 and 2
PCMSK0 = 0x3F; //interrupt on PB0-PB5
PCMSK2 |= (1<<PCINT21) | (1<<PCINT22); //PD5 – PD6
TCCR2A = 0;
TCCR2B |= (1<<CS22) | (1<<CS20); //div 128 prescaler
TIMSK2 |=(1<<OCIE2B);
DDRD |= _BV(7); //digital pin 7 output
}

void loop() {
}

ISR(PCINT0_vect) {
pwm_in();
}

ISR(PCINT2_vect) {
pwm_in();
}

ISR(TIMER2_COMPB_vect) {
PPMLO;
}

void pwm_in() {
PPMHI;
TCNT2=0;
OCR2B=38; //300us PPM pulse
}

Reply
Oscar 22nd September 2015 - 11:06 am

Thanks for the code recommendation :) i will have a look into that when have a chance!

Reply
Elias 18th September 2015 - 3:23 pm

Hi Oscar!

The “ppm_encoder.h” is very buggy. The posted version doesn’t even compile because of unmatching braces. Fixed that, but got on to more and more bugs.

Did anyone got this code to work?
It looks very promising!

Reply
Fayyaz 15th September 2015 - 10:28 pm

the “ppm_encoder.h” is missing the following;

the last two lines;
// Start timed watchdog setup sequence
WDTCSR |= (1<<WDCE) | (1

replace with below;

// Start timed watchdog setup sequence
WDTCSR |= (1<<WDCE) | (1<<WDE );
// Set 250 ms watchdog timeout and enable interrupt
WDTCSR = (1<<WDIE) | (1<<WDP2);
}
// ——————————————————————————

Reply
Oscar 16th September 2015 - 4:25 pm

thanks for the heads up! yea my blog didn’t like the < symbol and removed all the code after that... :) fixed now.

Reply
Fayyaz 15th September 2015 - 10:13 pm

the file “ppm_encoder.h” is not complete. can you please fix?

Reply
Christoph 10th September 2015 - 7:45 pm

Hi Oscar,

thanks for your tutorial, that’s exactly what I wanted to do. But the code you posted doesn’t work for me. I get the following compile error:
“Arduino: 1.6.4 (Linux), Platine: “Arduino Pro or Pro Mini, ATmega328 (5V, 16 MHz)”

In file included from /home/christoph/Downloads/arduino-latest/hardware/arduino/avr/cores/arduino/Arduino.h:30:0,
from ppm_enc.ino:21:
ppm_encoder.h: In function ‘void __vector_6()’:
ppm_encoder.h:264: error: expected unqualified-id before string constant
ISR( SERVO_INT_VECTOR )
^
In file included from ppm_enc.ino:22:0:
ppm_encoder.h:265: error: a function-definition is not allowed here before ‘{‘ token
{
^
ppm_enc:282: error: expected ‘}’ at end of input
expected unqualified-id before string constant”

I don’t have a Facebook account to ask AI what’s wrong here. Would it be possible for you to ask him what’s wrong here? Maybe I’m just missing a library?

Thanks!

Reply
Alexandr 11th April 2018 - 4:48 pm

Have you solved the problem?

Reply
Dj-Garfield 31st August 2015 - 6:06 pm

Hi Oscar :)
Realy good Article you made , I was working on a proto like this but the code source I had was a little “buggy”, I will try this one .
If I would add more Channels , is it possible ?
I see this in the code :

// Number of servo input channels
#define SERVO_CHANNELS 8

and

// Data array for storing ppm (8 channels) pulse widths.
volatile uint16_t ppm[ PPM_ARRAY_MAX ] =
{
PPM_PRE_PULSE,
PPM_SERVO_CENTER, // Channel 1
PPM_PRE_PULSE,
PPM_SERVO_CENTER, // Channel 2
PPM_PRE_PULSE,
PPM_THROTTLE_DEFAULT, // Channel 3 (throttle)
PPM_PRE_PULSE,
PPM_SERVO_CENTER, // Channel 4
PPM_PRE_PULSE,
PPM_SERVO_CENTER, // Channel 5
PPM_PRE_PULSE,
PPM_SERVO_CENTER, // Channel 6
PPM_PRE_PULSE,
PPM_SERVO_CENTER, // Channel 7
PPM_PRE_PULSE,
PPM_SERVO_CENTER, // Channel 8
PPM_PRE_PULSE,
PPM_PERIOD
};

I think that’s here , but … I don’t know if the time cycle of the whole code could be affected by this modification … I
Have to test this :)

Reply
Oscar 1st September 2015 - 11:26 am

Hey garfield… i think 8 ch is kind of the limit for PPM… it’s just how the signal is designed… if you need more than 8 ch going through 1 wire, you need to look into SBUS…

Reply
Dj-Garfield 1st September 2015 - 9:28 pm

Hy Oscar :)
Ok I will look at SBUS :)
As I see in the code , there is a limitation with the PPM Encoding , perhaps with an ST µcontroler in 32 bits would be faster and perhaps allows more than 8 channels ?
Ok , I will think about this when I will order a new Tx with Recievers :) ( SBUS ).
Thanx a lot Oscar :) Happy FLy :)

Reply