Start PWMs synchronous

Hello there,

I have configured the 4 PWM peripherals to generate a certain waveform. I need to start all 4 pwm peripherals simultaneous.

I thought of achieving this through usage of the PPI + EGU.
EGU channel 0 event triggers EGU channel 1 and channel 2 tasks.

EGU channel 1 event triggers PWM0/1 SEQSTART tasks.

EGU channel 2 event triggers PWM2/3 SEQSTART tasks.

This is written in the code below:

static void setup_ppi(nrf_ppi_channel_t *channel, uint32_t event, uint32_t task1, uint32_t task2)
{
    nrfx_err_t err = nrfx_ppi_channel_alloc(channel);
    assert(err == NRFX_SUCCESS);
    err = nrfx_ppi_channel_assign(*channel, event, task1);
    assert(err == NRFX_SUCCESS);
    err = nrfx_ppi_channel_fork_assign(*channel, task2);
    assert(err == NRFX_SUCCESS);
    err = nrfx_ppi_channel_enable(*channel);
    assert(err == NRFX_SUCCESS);
}

static void start_pwms(const nrfx_pwm_t *pwms)
{
    nrf_ppi_channel_t channel[3];

    setup_ppi(&channel[0], nrf_egu_event_address_get(NRF_EGU0, NRF_EGU_EVENT_TRIGGERED0),
              nrf_egu_task_address_get(NRF_EGU0, NRF_EGU_TASK_TRIGGER1),
              nrf_egu_task_address_get(NRF_EGU0, NRF_EGU_TASK_TRIGGER2));
    setup_ppi(&channel[1], nrf_egu_event_address_get(NRF_EGU0, NRF_EGU_EVENT_TRIGGERED1),
              nrfx_pwm_task_address_get(&pwms[0], NRF_PWM_TASK_SEQSTART1),
              nrfx_pwm_task_address_get(&pwms[1], NRF_PWM_TASK_SEQSTART1));
    setup_ppi(&channel[2], nrf_egu_event_address_get(NRF_EGU0, NRF_EGU_EVENT_TRIGGERED2),
              nrfx_pwm_task_address_get(&pwms[2], NRF_PWM_TASK_SEQSTART1),
              nrfx_pwm_task_address_get(&pwms[3], NRF_PWM_TASK_SEQSTART1));

    nrf_egu_task_trigger(NRF_EGU0, NRF_EGU_TASK_TRIGGER0);

    for (size_t k = 0; k < ARRAY_SIZE(channel); k++) {
        nrfx_err_t err = nrfx_ppi_channel_free(channel[k]);
        assert(NRFX_SUCCESS == err);
    }
}

The following 4 signals should rise at the same time, however they are 1.85 us out of sync.



Triggering the PWM SEQSTART tasks manually has the same result:

    nrf_pwm_task_trigger(NRF_PWM0, NRF_PWM_TASK_SEQSTART1);
    nrf_pwm_task_trigger(NRF_PWM1, NRF_PWM_TASK_SEQSTART1);
    nrf_pwm_task_trigger(NRF_PWM2, NRF_PWM_TASK_SEQSTART1);
    nrf_pwm_task_trigger(NRF_PWM3, NRF_PWM_TASK_SEQSTART1);

Strangely enough, if I reverse the order in which I start the PWMS:

    nrf_pwm_task_trigger(NRF_PWM3, NRF_PWM_TASK_SEQSTART1);
    nrf_pwm_task_trigger(NRF_PWM2, NRF_PWM_TASK_SEQSTART1);
    nrf_pwm_task_trigger(NRF_PWM1, NRF_PWM_TASK_SEQSTART1);
    nrf_pwm_task_trigger(NRF_PWM0, NRF_PWM_TASK_SEQSTART1);

The PWM signals are still generated in order PWM0->PWM3



Can anyone explain this? Is there no way to start the PWMs at the same time?

I am using NCS V2.5.0.

  • Hi!

    Do you have different duty cycles on these 4 signals? It could be that they started at the same time, but what you see is just a offset due to different duty cycles?

    If this is not the case, then please provide more info about how you have configured the PWM sequences. A example that reproduces the issue would be helpful.

  • I have configured the same duty cycle (0xFFFF).
    I have included a snippet below. The actual code makes a more elaborate pattern, but I removed that for simplicity.

    #include "pwm_controller.h"
    
    #include <zephyr/kernel.h>
    #include <assert.h>
    
    #include <nrfx_pwm.h>
    #include <nrfx_ppi.h>
    #include <nrfx_egu.h>
    
    #define NUM_AVAIL_PWMS 4
    #define NRFX_PWM_PIN_NOT_USED (0xff)
    
    typedef struct {
        nrf_pwm_values_wave_form_t pwm_values[NUM_AVAIL_PWMS][STIMENGINE_PATTERN_STEPS_MAX];
        struct k_event event_done;
    } pwm_controller_data_t;
    
    typedef struct {
        nrfx_pwm_t pwm_instance[NUM_AVAIL_PWMS];
    } pwm_controller_cfg_t;
    
    static pwm_controller_data_t pwm_controller_data;
    static pwm_controller_cfg_t pwm_controller_config = {
        .pwm_instance = { NRFX_PWM_INSTANCE(0), NRFX_PWM_INSTANCE(1), NRFX_PWM_INSTANCE(2), NRFX_PWM_INSTANCE(3), },
    };
    
    void pwm_controller_init(void)
    {
        IRQ_DIRECT_CONNECT(PWM0_IRQn, 0, nrfx_pwm_0_irq_handler, 0);
        IRQ_DIRECT_CONNECT(PWM1_IRQn, 0, nrfx_pwm_1_irq_handler, 0);
        IRQ_DIRECT_CONNECT(PWM2_IRQn, 0, nrfx_pwm_2_irq_handler, 0);
        IRQ_DIRECT_CONNECT(PWM3_IRQn, 0, nrfx_pwm_3_irq_handler, 0);
    }
    
    static uint16_t bool_to_pwm(bool high)
    {
        return high ? 0xFFFF : 0x7FFF;
    }
    
    static void generate_pattern(pwm_controller_data_t *data)
    {
        const bool pattern[4][3] = {
            { true, false, false },
            { false, true, false },
            { true, false, true },
            { false, false, false },
        };
        const uint16_t timing_us[4] = { 10, 50, 10, 5 };
        for (int step = 0; step < 4; step++) {
            for (int pwm = 0; pwm < NUM_AVAIL_PWMS; pwm++) {
                data->pwm_values[pwm][step].channel_0 = bool_to_pwm(pattern[step][0]);
                data->pwm_values[pwm][step].channel_1 = bool_to_pwm(pattern[step][1]);
                data->pwm_values[pwm][step].channel_2 = bool_to_pwm(pattern[step][2]);
                data->pwm_values[pwm][step].counter_top = timing_us[step];
            }
        };
    }
    
    static void pwm_handler(nrfx_pwm_evt_type_t event_type, void *ctx)
    {
        if (event_type != NRFX_PWM_EVT_FINISHED) {
            return;
        }
        const nrfx_pwm_t *pwm = (const nrfx_pwm_t *) ctx;
        k_event_set(&(pwm_controller_data.event_done), (1 << pwm->instance_id));
    }
    
    static void prepare_pwms(const nrfx_pwm_t *pwm, nrf_pwm_values_wave_form_t *wave, const uint8_t *pins,
                             size_t wave_form_len)
    {
        const nrfx_pwm_config_t pwm_config = {
                .output_pins = {
                    pins[0],
                    pins[1],
                    pins[2],
                    NRFX_PWM_PIN_NOT_USED,
                },
                .pin_inverted = {false, false, false, false},
                .irq_priority = 4,
                .base_clock = NRF_PWM_CLK_1MHz,
                .count_mode = NRF_PWM_MODE_UP,
                .top_value = 0,
                .load_mode = NRF_PWM_LOAD_WAVE_FORM,
                .step_mode = NRF_PWM_STEP_AUTO,
                .skip_gpio_cfg = false,
                .skip_psel_cfg = false,
            };
        const nrfx_err_t err = nrfx_pwm_init(pwm, &pwm_config, pwm_handler, (void *) pwm);
        assert(NRFX_SUCCESS == err);
        const uint32_t flags = NRFX_PWM_FLAG_START_VIA_TASK | NRFX_PWM_FLAG_STOP;
        const nrf_pwm_sequence_t seq = {
            .values.p_wave_form = wave,
            .length = 4 * wave_form_len,
            .repeats = 0,
            .end_delay = 0,
        };
        nrfx_pwm_simple_playback(pwm, &seq, 1, flags);
    }
    
    static void setup_ppi(nrf_ppi_channel_t *channel, uint32_t event, uint32_t task1, uint32_t task2)
    {
        nrfx_err_t err = nrfx_ppi_channel_alloc(channel);
        assert(err == NRFX_SUCCESS);
        err = nrfx_ppi_channel_assign(*channel, event, task2);
        assert(err == NRFX_SUCCESS);
        err = nrfx_ppi_channel_fork_assign(*channel, task1);
        assert(err == NRFX_SUCCESS);
        err = nrfx_ppi_channel_enable(*channel);
        assert(err == NRFX_SUCCESS);
    }
    
    static void start_pwms(const nrfx_pwm_t *pwms)
    {
        nrf_ppi_channel_t channel[3];
    
        setup_ppi(&channel[0], nrf_egu_event_address_get(NRF_EGU0, NRF_EGU_EVENT_TRIGGERED0),
                  nrf_egu_task_address_get(NRF_EGU0, NRF_EGU_TASK_TRIGGER1),
                  nrf_egu_task_address_get(NRF_EGU0, NRF_EGU_TASK_TRIGGER2));
        setup_ppi(&channel[1], nrf_egu_event_address_get(NRF_EGU0, NRF_EGU_EVENT_TRIGGERED1),
                  nrfx_pwm_task_address_get(&pwms[0], NRF_PWM_TASK_SEQSTART1),
                  nrfx_pwm_task_address_get(&pwms[1], NRF_PWM_TASK_SEQSTART1));
        setup_ppi(&channel[2], nrf_egu_event_address_get(NRF_EGU0, NRF_EGU_EVENT_TRIGGERED2),
                  nrfx_pwm_task_address_get(&pwms[2], NRF_PWM_TASK_SEQSTART1),
                  nrfx_pwm_task_address_get(&pwms[3], NRF_PWM_TASK_SEQSTART1));
    
        nrf_egu_task_trigger(NRF_EGU0, NRF_EGU_TASK_TRIGGER0);
    
        for (size_t k = 0; k < ARRAY_SIZE(channel); k++) {
            nrfx_err_t err = nrfx_ppi_channel_free(channel[k]);
            assert(NRFX_SUCCESS == err);
        }
    }
    
    void pwm_controller_generate(void)
    
    {
        const pwm_controller_cfg_t *p_cfg = &pwm_controller_config;
        pwm_controller_data_t *p_data = &pwm_controller_data;
    
        generate_pattern(p_data);
    
        const uint8_t pins[12] = {
            13, 19, 25, 10, 5 + 32, 3 + 32, 9 + 32, 11, 1 + 32, 4 + 32, 28, 0xff,
        };
    
        for (int pwm = 0; pwm < NUM_AVAIL_PWMS; pwm++) {
            prepare_pwms(&p_cfg->pwm_instance[pwm], p_data->pwm_values[pwm], &pins[3 * pwm], 4);
        }
    
        start_pwms(p_cfg->pwm_instance);
    
        uint32_t events = k_event_wait(&(p_data->event_done), 0xF, true, K_FOREVER);
        if (events == 0) {
            printk("timeout\n");
        }
        for (size_t k = 0; k < NUM_AVAIL_PWMS; k++) {
            nrfx_pwm_uninit(&(p_cfg->pwm_instance[k]));
        }
    }
    

  • We discussed synchronising multiple PWMs a while back, and the use of EGU and PPI allows this; here is a long thread, towards the end is some code which may help: pwm-interrupts

  • "Essentially you can use two PPI channels to connect one of the EGU events to all three PWM start tasks, and then you just have to activate the corresponding EGU task to have all three PWM modules start in perfect sync. "

    That is what I am doing.

    It also seems they were not able to synchronize the PWMs after all. They ended up adding dead time to compensate.

  • Maybe it wasn't clear, but here's a bare-bones example of starting all 4 PWMs with zero (0nSec) delay between the outputs. I just tested it, seems to do what you are looking for:

    static void EnableExtClockPWM(NRF_PWM_Type * const pPWM, const uint32_t PWM_Pin);
    
    // PWM synch 4 PWM channels
    void SyncPWMs(void)
    {
       EnableExtClockPWM(NRF_PWM0, 17);
       EnableExtClockPWM(NRF_PWM1, 18);
       EnableExtClockPWM(NRF_PWM2, 19);
       EnableExtClockPWM(NRF_PWM3, 20);
       NRF_PPI->CH[0].EEP = (uint32_t)&NRF_EGU0->EVENTS_TRIGGERED[0];
       NRF_PPI->CH[0].TEP = (uint32_t)&NRF_PWM0->TASKS_SEQSTART[0];
       NRF_PPI->FORK[0].TEP = (uint32_t)&NRF_PWM1->TASKS_SEQSTART[0];
       NRF_PPI->CH[1].EEP = (uint32_t)&NRF_EGU0->EVENTS_TRIGGERED[0];
       NRF_PPI->CH[1].TEP = (uint32_t)&NRF_PWM2->TASKS_SEQSTART[0];
       NRF_PPI->FORK[1].TEP = (uint32_t)&NRF_PWM3->TASKS_SEQSTART[0];
       NRF_PPI->CHEN = (PPI_CHEN_CH0_Enabled << PPI_CHEN_CH0_Pos) | (PPI_CHEN_CH1_Enabled << PPI_CHEN_CH1_Pos);
       NRF_EGU0->TASKS_TRIGGER[0] = 1;
    }

    This is the PWM code, simple clock output on all 4 PWMs with no phase offset:

    #define PWM_CH0_DUTY 8                      // Using 50% duty cycle
    #define PWM_CH1_DUTY PWM_CH0_DUTY | (1<<15) // 50% duty cycle, polarity bit set so inverse of CH0
    static uint16_t pwm_seq[] = {PWM_CH0_DUTY, PWM_CH1_DUTY};
    
    static void EnableExtClockPWM(NRF_PWM_Type * const pPWM, const uint32_t PWM_Pin)
    {
       // Set pins to output ports first, high drive to get sharpest edges, driving low
       nrf_gpio_pin_clear(PWM_Pin);
       // Configuration      Direction                Input                          Pullup               Drive Level        Sense Level
       // =================  =======================  =============================  ===================  =================  =====================
       nrf_gpio_cfg(PWM_Pin, NRF_GPIO_PIN_DIR_OUTPUT, NRF_GPIO_PIN_INPUT_DISCONNECT, NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE);
    
       pPWM->PSEL.OUT[0] = (PWM_Pin  << PWM_PSEL_OUT_PIN_Pos) | (PWM_PSEL_OUT_CONNECT_Connected << PWM_PSEL_OUT_CONNECT_Pos);
       pPWM->ENABLE      = (PWM_ENABLE_ENABLE_Enabled << PWM_ENABLE_ENABLE_Pos);
       pPWM->MODE        = (PWM_MODE_UPDOWN_Up << PWM_MODE_UPDOWN_Pos);
       pPWM->PRESCALER   = (PWM_PRESCALER_PRESCALER_DIV_2 << PWM_PRESCALER_PRESCALER_Pos);
       pPWM->COUNTERTOP  = ((PWM_CH0_DUTY*2) << PWM_COUNTERTOP_COUNTERTOP_Pos);
       pPWM->LOOP        = 10;
       pPWM->DECODER     = (PWM_DECODER_LOAD_Grouped << PWM_DECODER_LOAD_Pos) | (PWM_DECODER_MODE_RefreshCount << PWM_DECODER_MODE_Pos);
       pPWM->SEQ[0].PTR  = ((uint32_t)(pwm_seq) << PWM_SEQ_PTR_PTR_Pos);
       pPWM->SEQ[0].CNT  = ((sizeof(pwm_seq) / sizeof(uint16_t)) << PWM_SEQ_CNT_CNT_Pos);
       pPWM->SEQ[0].REFRESH  = 0;
       pPWM->SEQ[0].ENDDELAY = 0;
    }
    

Related