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

SAADC fails to sync with PWM via PPI

I'm looking to sync an ADC reading with the PWM output. I'm using PPI for this but fails to make it synchronise properly with the PWM.

I'm getting interrupts and sampled values, indicating something is sampled. But if I toggle a pin when starting the sampling and when its ended and then use an oscilloscope to compare with the PWM output, they are not synchronised.

Code for setting up SAADC and PPI:

// Configure SAADC and interrupts
NRF_SAADC->RESOLUTION = SAADC_RESOLUTION_VAL_12bit << SAADC_RESOLUTION_VAL_Pos;
NRF_SAADC->ENABLE = SAADC_ENABLE_ENABLE_Enabled << SAADC_ENABLE_ENABLE_Pos;
sd_nvic_ClearPendingIRQ(SAADC_IRQn);
sd_nvic_SetPriority(SAADC_IRQn, 7U);

// Configure and enable channels (only one shown)
NRF_SAADC->CH[0].PSELN = SAADC_CH_PSELN_PSELN_NC << SAADC_CH_PSELN_PSELN_Pos;
NRF_SAADC->CH[0].PSELP = 7U << SAADC_CH_PSELN_PSELN_Pos;

// Configure EasyDMA
NRF_SAADC->RESULT.PTR = (uint32_t)(buffer);
NRF_SAADC->RESULT.MAXCNT = 3U;

// Enable interrupts, configure PPI and start conversion
sd_nvic_ClearPendingIRQ(SAADC_IRQn);
sd_nvic_EnableIRQ(SAADC_IRQn);
NRF_SAADC->INTENSET = 0xFFFFFFFF;  // Ugly way of enabling all...
NRF_SAADC->TASKS_START = 1U;
NRF_PPI->CH[0].EEP = (uint32_t)(&(NRF_PWM0->EVENTS_SEQSTARTED[kIndicatorPwmSequence]));
NRF_PPI->CH[0].TEP = (uint32_t)(&NRF_SAADC->TASKS_SAMPLE);

// Pin toggle
NRF_P0->OUTSET = (1 << 3);

// Interrupt handling
void SAADC_IRQHandler(void) {
    if (NRF_SAADC->EVENTS_END) {
        NRF_P0->OUTCLR = (1 << 3);
        sd_nvic_DisableIRQ(SAADC_IRQn);
        NRF_SAADC->INTENCLR = 0xFFFFFFFF;  // Ugly, again...
        NRF_SAADC->EVENTS_END = 0U;
    }
}

Code for setting up PWM:

// Starting PWM
const uint32_t enable_mask = PWM_PSEL_OUT_CONNECT_Connected
                                 << PWM_PSEL_OUT_CONNECT_Pos;
NRF_PWM0->PSEL.OUT[RED] = enable_mask | (static_cast<uint32_t>(r_pin) << PWM_PSEL_OUT_PIN_Pos);
NRF_PWM0->PSEL.OUT[GREEN] = enable_mask | (static_cast<uint32_t>(g_pin) << PWM_PSEL_OUT_PIN_Pos);
NRF_PWM0->PSEL.OUT[BLUE] = enable_mask | (static_cast<uint32_t>(b_pin) << PWM_PSEL_OUT_PIN_Pos);
NRF_PWM0->ENABLE = PWM_ENABLE_ENABLE_Enabled << PWM_ENABLE_ENABLE_Pos;
NRF_PWM0->MODE = PWM_MODE_UPDOWN_UpAndDown << PWM_MODE_UPDOWN_Pos;
NRF_PWM0->PRESCALER = PWM_PRESCALER_PRESCALER_DIV_32 << PWM_PRESCALER_PRESCALER_Pos;
NRF_PWM0->COUNTERTOP = kDefaultCounterTop;
NRF_PWM0->LOOP = PWM_LOOP_CNT_Disabled << PWM_LOOP_CNT_Pos;
NRF_PWM0->DECODER = (PWM_DECODER_LOAD_Individual << PWM_DECODER_LOAD_Pos);
NRF_PWM0->SEQ[0].PTR = (uint32_t)(m_periods);
NRF_PWM0->SEQ[kIndicatorPwmSequence].CNT = 4U;
NRF_PWM0->SEQ[kIndicatorPwmSequence].REFRESH = 0U;
NRF_PWM0->SEQ[kIndicatorPwmSequence].ENDDELAY = 0U;
NRF_PWM0->TASKS_SEQSTART[0] = 1U;

// Changing period (example); done occassionally on demand
m_periods[0] = 300;
NRF_PWM0->TASKS_SEQSTART[0] = 1U;

I would expected when looking at the oscilloscope that pin 3 is high when the PWM goes high, but they seem to move independently. The time pin 3 is high is in the range of 1-5ms. Depending on how well the SAADC start synchronises with the PWM, I would have expected something much shorter? There seems to be some logic behind it, in that it can be quite stable at for example 1.5ms and then after 20 samples it jumps to for example 4.5ms.

The PWM is configured with a period of 4ms and I'm trying to sample the ADC every 500ms. The accuracy of the SAADC is not a major concern, we need to be in the 100mV range and we are fine.

I suspect I'm configuring the PPI triggering and/or interrupts wrong somehow?

  • Hello,

    What is your pin 3 connected to? Is it RED GREEN or BLUE pin of yours, or something else?

    To be honest, I am having a little trouble understanding exactly what you want to do. Are you trying to create a feedback loop to measure the average on the PWM pin, or are you using the PWM signal as a timer to trigger the SAADC readings?

    BR,

    Edvin

  • Hi,

    Pin 3 is not connected to anything. I'm just using it to verify that the ADC is synced with the PWM properly. I.e. I sample the PWM output and pin 3 output with the oscilloscope to see that the ADC sampling occurs when the PWM signal is high.

    Yeah, a little background info would probably be in place. But the short answer is: using the PWM signal as a timer to trigger the SAADC readings.

    The PWM is controlling an RGB LED (sinking it through transistors). To detect that the LED is working properly, we want to measure the voltage drop over the diodes, using the SAADC.

    For example, assume that the LED's are powered with 5V and have a voltage drop of 3V. A normally functioning diode, when on, would have a voltage of 5V-3V = 2V at its cathode. A diode that has short-circuited would have 5V and one that has open-circuited would have 0V (because we are sinking it to GND). If it is not on, the voltage would be >3.3V, even when it's working. (We are not really sure of this, but its a theory anyway)

    So, in order to reliably determine if the diode is working, we need to sync the SAADC reading with when the LED is ON, thus with the PWM.

  • Hello,

    I see. You set the pin in the saadc_evt_handler, right? And you try to trigger the SAADC from the PWM started event, correct?

    I have not used the SAADC on register level before, so I can't say without testing whether your SAADC setup is correct or not, but I guess the SAADC needs more samples before it will trigger the event. It fills up the entire buffer, which I guess is the one you have set to 3:

    NRF_SAADC->RESULT.MAXCNT = 3U;

    Hence, even if the EEP triggers:

    NRF_PPI->CH[0].EEP = (uint32_t)(&(NRF_PWM0->EVENTS_SEQSTARTED[kIndicatorPwmSequence]));

    And the TEP triggers:

    NRF_PPI->CH[0].TEP = (uint32_t)(&NRF_SAADC->TASKS_SAMPLE);

    It will not trigger the saadc_evt_handler; because the SAADC doesn't get a callback until it has finished reading n samples, where n is the number of elements in the buffer.

    Check this log from the saadc example in the SDK:

    <info> SAADC: Function: nrfx_saadc_init, error code: NRF_SUCCESS.
    <info> SAADC: Channel initialized: 0.
    <info> SAADC: Function: nrfx_saadc_channel_init, error code: NRF_SUCCESS.
    <info> SAADC: Function: nrfx_saadc_buffer_convert, buffer length: 5, active channels: 1.
    <info> SAADC: Function: nrfx_saadc_buffer_convert, error code: NRF_SUCCESS.
    <warning> SAADC: Function: nrfx_saadc_buffer_convert, error code: NRF_SUCCESS.
    <info> app: SAADC HAL simple example started.
    <debug> SAADC: Event: 
    <debug> SAADC: Event: NRF_SAADC_EVENT_END.
    <warning> SAADC: Function: nrfx_saadc_buffer_convert, error code: NRF_SUCCESS.
    <info> app: ADC event number: 0
    <info> app: 12
    <info> app: 11
    <info> app: 12
    <info> app: 9
    <info> app: 9
    

    Only the logs starting with <debug> are from the SAADC_IRQHandler, the rest is from main.c.

    I modified this function by adding the NRFX_LOG_DEBUG("Event:"); in the start of this function:

    void nrfx_saadc_irq_handler(void)
    {
        NRFX_LOG_DEBUG("Event: ");
        if (nrf_saadc_event_check(NRF_SAADC_EVENT_END))
        {
            nrf_saadc_event_clear(NRF_SAADC_EVENT_END);
            NRFX_LOG_DEBUG("Event: %s.", EVT_TO_STR(NRF_SAADC_EVENT_END));
        ...

    As you can see, the first event in the irq handler is the NRF_SAADC_EVENT_END, which is after the first 5 samples are sampled (there are 5 samples in the buffer in this example).

    You should check out this example. It sets up a timer, starts it, and uses PPI to trigger sample events from the timeout events. You can see in the example that the timeout handler is empty, so it is only used to trigger the samples via PPI.

    When doing PPI implementations like the one you have posted, it is a good idea to start with a simple check, such as starting with the PWM, but as an TEP, just toggle a pin. To set up an GPIOTE in ppi, you can try the following:

    void pin_init(uint32_t pinselect)
    {  
        NRF_GPIOTE->CONFIG[CHANNEL_NUMBER] = GPIOTE_CONFIG_MODE_Task << GPIOTE_CONFIG_MODE_Pos | 
                                             GPIOTE_CONFIG_POLARITY_Toggle << GPIOTE_CONFIG_POLARITY_Pos | 
                                             pinselect << GPIOTE_CONFIG_PSEL_Pos | 
                                             GPIOTE_CONFIG_OUTINIT_High << GPIOTE_CONFIG_OUTINIT_Pos;
    
        NRF_PPI->CH[PIN_PPI_CH_A].EEP = (uint32_t)(&(NRF_PWM0->EVENTS_SEQSTARTED[kIndicatorPwmSequence]));
        NRF_PPI->CH[PIN_PPI_CH_A].TEP = (uint32_t)&NRF_GPIOTE->TASKS_CLR[PWM0_GPIOTE_CH];
        
        NRF_PPI->CHENSET               = (1 << PIN_PPI_CH_A);
    }

    I suggest you check out the saadc example, which triggers the samples via ppi, and sets up all this using the SDK API, together with starting checking whether the PWM is set up correctly by toggling the pin on the PWM event end points (EEP).

    Best regards,

    Edvin

  • Thanks for the thorough reply.

    I set the pin when I want to start the ADC conversion, and then clear it in the event handler. The issue is not that I'm not getting samples or that the event handler is called. I get a DONE event.

    The issue is that the sequence doesn't sync with the PWM. What I want is that if I want to acquire a sample, the ADC shall wait until the beginning of the next PWM cycle. Assuming the the acquisition time is t_acq and the PWM period is T, I would expect an interrupt at T+t_acq, but using the oscilloscope, I can see that these are not at all related.

    So I'm thinking that I have misinterpreted when PWM events are generated. I basically want an interrupt every T seconds, regardless if I'm changing the PWM duty cycles or not.

    If this is not clear I can write a sample program that explains the use case better?

  • The point of the peripherals on the nRF is that they should do as much as possible without having to interrupt or wait for the CPU. Especially with the PWM driver. If you generate a PWM signal to control an LED or an electric motor, you certainly don't want to get an application interrupt every 2ms to toggle the pin. So after you start the PWM, it will not generate an event on every PWM period. 

    I still don't understand whether you generate the PWM on the nRF to trigger samples, or if you ultimately want it to trigger on a PWM signal that is generated externally.

     

    obbe said:
    I basically want an interrupt every T seconds

     Sounds like a timer, and not PWM is the way to go.

    BR,

    Edvin

Related