The PPM protocol for encoding Remote Control channel values is now a legacy. Still, it is widely accepted by different hardware and when tinkering with Arduino, remote control, and working on own accessories for flight controllers, PPM is still a valid option.

A few years ago I presented a code that allows generating PPM stream using Arduino and AVR hardware. That solution is very hardware-specific and works only with ATMega microcontrollers.

During my work of ESP32 DiyMotionController I stumbled on a problem: if ESP32 is not compatible with AVR when timers are a concern, how to generate a PPM stream on ESP32?

PPM signal on an oscilloscope

This is why I reserved one evening, powered up my oscilloscope (Rigol DS1054Z), and created the code you can see below.

    #define PPM_FRAME_LENGTH 22500
    #define PPM_PULSE_LENGTH 300
    #define PPM_CHANNELS 8
    #define DEFAULT_CHANNEL_VALUE 1500

    #define OUTPUT_PIN 14

    uint16_t channelValue[16] = {1500};

    hw_timer_t * timer = NULL;

    enum ppmState_e {

    int getRcChannel_wrapper(uint8_t channel)
        if (channel >= 0 && channel < 16)
            return channelValue[channel];
            return DEFAULT_CHANNEL_VALUE;

    void IRAM_ATTR onPpmTimer() {

        static uint8_t ppmState = PPM_STATE_IDLE;
        static uint8_t ppmChannel = 0;
        static uint8_t ppmOutput = LOW;
        static int usedFrameLength = 0;
        int currentChannelValue;


        if (ppmState == PPM_STATE_IDLE) {
            ppmState = PPM_STATE_PULSE;
            ppmChannel = 0;
            usedFrameLength = 0;

        if (ppmState == PPM_STATE_PULSE) {
            ppmOutput = HIGH;
            usedFrameLength += PPM_PULSE_LENGTH;
            ppmState = PPM_STATE_FILL;

            timerAlarmWrite(timer, PPM_PULSE_LENGTH, true);
        } else if (ppmState == PPM_STATE_FILL) {
            ppmOutput = LOW;
            currentChannelValue = getRcChannel_wrapper(ppmChannel);

            ppmState = PPM_STATE_PULSE;

            if (ppmChannel > PPM_CHANNELS) {
                ppmChannel = 0;
                timerAlarmWrite(timer, PPM_FRAME_LENGTH - usedFrameLength, true);
                usedFrameLength = 0;
            } else {
                usedFrameLength += currentChannelValue - PPM_PULSE_LENGTH;
                timerAlarmWrite(timer, currentChannelValue - PPM_PULSE_LENGTH, true);
        digitalWrite(OUTPUT_PIN, ppmOutput);

    void setup()
        pinMode(OUTPUT_PIN, OUTPUT);
        timer = timerBegin(0, 80, true);
        timerAttachInterrupt(timer, &onPpmTimer, true);
        timerAlarmWrite(timer, 12000, true);

    void loop()
        Here you can modify the content of channelValue array and it will be automatically
        picked up by the code and outputted as PPM stream. For example:
        channelValue[0] = 1750;
        channelValue[1] = 1350;

the code uses one of ESP32 Timers/Alarm to generate PPM in the background. Logic happens inside of onPpmTimer handler function. Code rescheduled the timer alarm to trigger according to RC channel values. The given example allows to encode 8 channels in a PPM stream. It's fully asynchronous from the main loop.