This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

How to synchronize PWM with external event

As for dimmer applications neccessary, a pwm signal with variable duty-cycle must be generated to drive a triac. But to be synchronized with the mains' zero crossing, the pwm cycle must be started by a GPIO event from a zero-crossing sensor (optocoupler). What would be the best approach to solve this problem? Has anybody done a similar thing before?

  • I've done exactly this and went through quite a few iterations to get something which worked reliably.

    First thing - if you want to use full half-cycles - using a triac with a zero cross turn-on circuit saves a lot of problems. You have plenty of time during one cycle to work out whether you want to turn on for the next one, you don't have to try and get the GPIO on event dead right. Doing this gives you very accurate power with the caveat that you are limited to a resolution of 2x mains frequency. I wish I'd started there because that's actually more than the resolution I needed for my application.

    Secondly, if you want to get an event trigger close to the actual zero cross you need a sensor which produces an on pulse fairly towards the start of the cycle and importantly produces a fast-rising one, else the GPIO line bounces. I use one PPI to turn off the GPIOTE as soon as the pulse is registered to stop any bouncing at all, it's turned on again during an interrupt service routine before the next one. For me, an FOD817 (C or D) with about 150k current limiting resistors on the mains side and a 100k pullup on the nRF side gives a good pulse with at least 48% duty cycle. That's for 240v and the resistors get warm! Increasing resistance too much made the edges of the pulse too wide, duty cycle fell closer to 40% and the bouncing was horrible.

    I found it best to work 1/2 a cycle ahead, setting up everything to be triggered by the next GPIOTE event using PPI during the interrupt service routine from the last one, then letting PPI actually do the job for me later. Obviously turning things off is easier, you just turn it off at any point and the triac will actually shut down at the next zero cross.

  • I tried it with pwm and while shifting the duty cycle from 1 to 99% to delay the triac firing in order to phase-cut the half-wave. The problem is that the 50Hz from the mains is not as stable as the pwm and I need to restart the pwm from time to time. As the pwm needs a complete cycle to restart, flickering appears at output. Could you share some code with me showing your solution in detail? I would appreciate this very much. I am using the VOL628A, 33K half watt at both 230V inputs to get the zero crossing, no warming so far. Could not see bounces at the GPIO.

  • Not sure you're going to hit perfection on this. I found the harder I tried to stay perfectly synched all the time, the worse the effects of a small error was. if I made the detector less-sensitive, I had issues, if I made it more sensitive, I had different issues. For me (I'm using it to control an oven) the oven going on and off put enough spike in the mains to break it.

    Where I ended up was deciding the mains was really pretty stable over even a fairly long period, so I ended up making the controlling timer run independently just on its own and then use another PPI off the detector to slowly (over the course of seconds) change the period of the main timer to keep it basically in sync I started trying to control the thing perfectly on every 1/2 cycle, but I don't need to (you do I think) and eventually moved to running full 1/2 cycles in trains to keep a running average of power ..

  • .. correct. Eg if I want 50% power I can run the triac for 32 on cycles and then 32 off cycles, it's an oven, it really doesn't care that much, if you're trying to get a flicker-free light, you do care.

    So the closest I got to 'perfect' control was separating the timer which controlled the triac from the one which measured the zero cross and slewing the former slowly as the latter changed. Slowly enough to filter out transients, fast enough to actually follow real changes. I suspect if I hooked that up to a light, as good as I ever got it, I'd see some flicker. Oh and I totally avoided anything under about 10% duty, it was too easy to be a little late with the off control signal and end up with full-on cycles randomly, it was a mess.

  • Finally I found a solution in heavy use of PPI. I don't use PWM any longer, but use a chain of 6 ppi channels. First one assigns the zero-crossing event with the start of a phase-shift timer, This one will be disabled, if the load should be switched off. The timer controls the delay from the zero-crossing event until the end of a half-wave. The second timer is triggered by the overrun of the first to define the pulse length of the triac firing pulse. I would appreciate any improvements of the following code.

    const nrf_drv_timer_t phaseshift_timer  = NRF_DRV_TIMER_INSTANCE(1);
    const nrf_drv_timer_t pulselength_timer = NRF_DRV_TIMER_INSTANCE(2);
    static nrf_ppi_channel_t                m_zerocrossing_channel;
    
    void zerocrossing_event_handler(nrf_drv_gpiote_pin_t pin, nrf_gpiote_polarity_t action) {}
    void trigger_event_handler(nrf_timer_event_t event_type, void* p_context){}
    
    /** @brief Function for timer initialization, which will be started by zero-crossing using PPI.
    */
    static void high_resolution_timer_init(void)
    {
        uint32_t err_code = NRF_SUCCESS;
        nrf_drv_timer_config_t timer_config;
        timer_config.frequency = NRF_TIMER_FREQ_1MHz;
        timer_config.bit_width = NRF_TIMER_BIT_WIDTH_16;
        timer_config.interrupt_priority = NRF_APP_PRIORITY_LOW;
        timer_config.mode = NRF_TIMER_MODE_TIMER;
        timer_config.p_context = NULL;
        err_code = nrf_drv_timer_init(&phaseshift_timer, &timer_config, trigger_event_handler);
        APP_ERROR_CHECK(err_code);
        // 10000 cycles at 1Mhz makes 0.01 sec - the duration of one half wave at 50Hz mains AC frequency
        nrf_drv_timer_extended_compare(&phaseshift_timer, NRF_TIMER_CC_CHANNEL0, 0x2710, NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);
        err_code = nrf_drv_timer_init(&pulselength_timer, &timer_config, trigger_event_handler);
        APP_ERROR_CHECK(err_code);
        // the length of the triac firing pulse is 0.001 sec
        nrf_drv_timer_extended_compare(&pulselength_timer, NRF_TIMER_CC_CHANNEL0, 0x03E8UL, NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);
    }
    
    /** @brief Function for initializing the PPI peripheral.
    */
    static void peripheral_init(void)
    {
        uint32_t err_code = NRF_SUCCESS;
        nrf_ppi_channel_t ppi_channel2;
        nrf_ppi_channel_t ppi_channel3;
        nrf_ppi_channel_t ppi_channel4;
        nrf_ppi_channel_t ppi_channel5;
        nrf_ppi_channel_t ppi_channel6;
        uint32_t zerocrossing_addr;
        uint32_t load_addr;
    
        // init the phase-shift and pulse-length timer
        high_resolution_timer_init();
        err_code = nrf_drv_ppi_init();
        APP_ERROR_CHECK(err_code);
    
        if (!nrf_drv_gpiote_is_init())
        {
            err_code = nrf_drv_gpiote_init();
            if(err_code != NRF_SUCCESS)
            {
                APP_ERROR_CHECK(err_code);
                return;
            }
        }
    
        // create the zero-crossing event
        nrf_drv_gpiote_in_config_t zero_config = GPIOTE_CONFIG_IN_SENSE_LOTOHI(true);
        zero_config.pull = NRF_GPIO_PIN_PULLUP;
        err_code = nrf_drv_gpiote_in_init(ZERO, &zero_config, zerocrossing_event_handler);
        APP_ERROR_CHECK(err_code);
        zerocrossing_addr = nrf_drv_gpiote_in_event_addr_get(ZERO);
        nrf_drv_gpiote_in_event_enable(ZERO, true);
        // assign zero-crossing event to start of phase-shift timer
        // start timer on zero-crossing event
        err_code = nrf_drv_ppi_channel_alloc(&m_zerocrossing_channel);
        APP_ERROR_CHECK(err_code);
        err_code = nrf_drv_ppi_channel_assign(m_zerocrossing_channel,
                                              zerocrossing_addr,
                                              nrf_drv_timer_task_address_get(&phaseshift_timer, NRF_TIMER_TASK_START));
        APP_ERROR_CHECK(err_code);
        // create the load control task
        nrf_drv_gpiote_out_config_t load_config = GPIOTE_CONFIG_OUT_TASK_TOGGLE(true);
        load_config.init_state = NRF_GPIOTE_INITIAL_VALUE_LOW;
        err_code = nrf_drv_gpiote_out_init(LOAD, &load_config);
        APP_ERROR_CHECK(err_code);
        load_addr = nrf_drv_gpiote_out_task_addr_get(LOAD);
        nrf_drv_gpiote_out_task_enable(LOAD);
        // assign the end of the phase-shift timer to the load control task
        // toggle load control if timer overruns
        err_code = nrf_drv_ppi_channel_alloc(&ppi_channel2);
        APP_ERROR_CHECK(err_code);
        err_code = nrf_drv_ppi_channel_assign(ppi_channel2,
                                              nrf_drv_timer_event_address_get(&phaseshift_timer, NRF_TIMER_EVENT_COMPARE0),
                                              load_addr);
        APP_ERROR_CHECK(err_code);
        // assign the end of the phase-shift timer to the start of the pulse-length timer
        // start pulse_length timer with the end of the phase-shift timer
        err_code = nrf_drv_ppi_channel_alloc(&ppi_channel3);
        APP_ERROR_CHECK(err_code);
        err_code = nrf_drv_ppi_channel_assign(ppi_channel3,
                                              nrf_drv_timer_event_address_get(&phaseshift_timer, NRF_TIMER_EVENT_COMPARE0),
                                              nrf_drv_timer_task_address_get(&pulselength_timer, NRF_TIMER_TASK_START));
        APP_ERROR_CHECK(err_code);
        // assign the end of the phase-shift timer to stop the phase-shift timer
        // end of phase-shift timer stops itself
        err_code = nrf_drv_ppi_channel_alloc(&ppi_channel4);
        APP_ERROR_CHECK(err_code);
        err_code = nrf_drv_ppi_channel_assign(ppi_channel4,
                                              nrf_drv_timer_event_address_get(&phaseshift_timer, NRF_TIMER_EVENT_COMPARE0),
                                              nrf_drv_timer_task_address_get(&phaseshift_timer, NRF_TIMER_TASK_STOP));
        APP_ERROR_CHECK(err_code);
        // assign end of pulse-length timer to the load control task
        // toggle load control with the end of pulse-length timer
        err_code = nrf_drv_ppi_channel_alloc(&ppi_channel5);
        APP_ERROR_CHECK(err_code);
        err_code = nrf_drv_ppi_channel_assign(ppi_channel5,
                                              nrf_drv_timer_event_address_get(&pulselength_timer, NRF_TIMER_EVENT_COMPARE0),
                                              load_addr);
        APP_ERROR_CHECK(err_code);
        // assign end of pulse-length timer to stop itself
        // end of pulse-length timer stops itself
        err_code = nrf_drv_ppi_channel_alloc(&ppi_channel6);
        APP_ERROR_CHECK(err_code);
        err_code = nrf_drv_ppi_channel_assign(ppi_channel6,
                                              nrf_drv_timer_event_address_get(&pulselength_timer, NRF_TIMER_EVENT_COMPARE0),
                                              nrf_drv_timer_task_address_get(&pulselength_timer, NRF_TIMER_TASK_STOP));
        APP_ERROR_CHECK(err_code);
        err_code = nrf_drv_ppi_channel_enable(ppi_channel2);
        APP_ERROR_CHECK(err_code);
        err_code = nrf_drv_ppi_channel_enable(ppi_channel3);
        APP_ERROR_CHECK(err_code);
        err_code = nrf_drv_ppi_channel_enable(ppi_channel4);
        APP_ERROR_CHECK(err_code);
        err_code = nrf_drv_ppi_channel_enable(ppi_channel5);
        APP_ERROR_CHECK(err_code);
        err_code = nrf_drv_ppi_channel_enable(ppi_channel6);
        APP_ERROR_CHECK(err_code);
    }
    
    void set_brightness(uint16_t brightness)
    {
        uint32_t err_code = NRF_SUCCESS;
        if(brightness > 0) {
            // enable zero-crossing event
            err_code = nrf_drv_ppi_channel_enable(m_zerocrossing_channel);
            APP_ERROR_CHECK(err_code);
            uint16_t counter;
            counter = 10000 - 100*brightness;
            // the min ticks is 4 - represents full cycle load
            nrf_drv_timer_extended_compare(&phaseshift_timer, NRF_TIMER_CC_CHANNEL0, counter+4, NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);
        }
        else
        {
            // disable zero-crossing event
            err_code = nrf_drv_ppi_channel_disable(m_zerocrossing_channel);
            APP_ERROR_CHECK(err_code);
        }
    }
    
Related