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

Just getting started with Tasks and Events: PWM => SAADC

Coming from other embedded systems, I'm impressed with the flexibility and power of the Task/Event framework of the nRF52832. But with great flexibility comes great confusion! :)

I'd like to do something along these lines, written here in pidgin C:

for (int i=0; i<12; i++) {
    gpio_set(PIN_A);
    gpio_clr(PIN_B);
    pause_briefly();
    dma_buf[2*i] = saadc_read();
    gpio_clr(PIN_A);
    gpio_set(PIN_B);
    pause_briefly();
    dma_buf[2*i+1] = saadc_read();
}

My best guess as to translating this into Tasks and Events:

  • Configure PWM to drive PIN_A inverted and PIN_B non-inverted.
  • Set PWM LOOP.CNT to 24
  • Use ??? to trigger an SAADC TASKS_SAMPLE [*]
  • Use DMA to write SAADC samples into RAM
  • Use PWM's EVENTS_LOOPSDONE to trigger interrupt in user code

[*] I think I can use a second PWM channel to toggle its output shortly after setting PIN_A and PIN_B and use that to generate an Event. This event give PIN_A and PIN_B a chance to settle before triggering the SAADC read. As long as I can use any of the channel outputs to generate an event I should be okay.

So the questions:

Is this more or less the right approach? What are some good examples to use as starting points? Any gotchas I should be aware of?

My environment:

  • nRF54 dev board
  • nRF5_SDK_14.2.0
  • SEGGER Embedded Studio Release 3.30
  • macOS High Sierra 10.31.1
  • Hi

    You can indeed do a lot of fun stuff with the task/event system, but finding the best way to do things is not always easy ;)

    I don't think the PWM module is the way to go here, unless you need to change the duty cycle of the two pins dynamically. Instead I would propose a method using a TIMER and the GPIOTE, together with the SAADC and the PPI.

    The TIMER modules have multiple compare registers, that can be used to trigger activities at different times through the PPI controller.

    I wrote a small example to show how you can have the timer toggle the pins and sample the ADC at different times:

    #define GPIOTE_CH_A 0
    #define GPIOTE_CH_B 1
    
    #define PIN_A       10
    #define PIN_B       11
    
    #define PPI_CH_A    0
    #define PPI_CH_B    1
    #define PPI_CH_C    2
    
    #define DELAY_IO_FLIP_US    10
    #define DELAY_ADC_SAMPLE_US 10
    
    static void timer_init(void)
    {
        // Set up the timer for a 1MHz rate, configure 2 different delays, and have it auto clear on CC[1]
        ADC_TIMER->PRESCALER = 4;
        ADC_TIMER->CC[0] = DELAY_IO_FLIP_US;
        ADC_TIMER->CC[1] = DELAY_IO_FLIP_US + DELAY_ADC_SAMPLE_US;
        ADC_TIMER->SHORTS = TIMER_SHORTS_COMPARE1_CLEAR_Msk;
        
        // Set up pin A in toggle mode, inital value low
        NRF_GPIOTE->CONFIG[GPIOTE_CH_A] = GPIOTE_CONFIG_MODE_Task       << GPIOTE_CONFIG_MODE_Pos | 
                                          GPIOTE_CONFIG_OUTINIT_Low     << GPIOTE_CONFIG_OUTINIT_Pos |
                                          GPIOTE_CONFIG_POLARITY_Toggle << GPIOTE_CONFIG_POLARITY_Pos |
                                          PIN_A                         << GPIOTE_CONFIG_PSEL_Pos;
        
        // Set up pin B in toggle mode, initial value high
        NRF_GPIOTE->CONFIG[GPIOTE_CH_B] = GPIOTE_CONFIG_MODE_Task       << GPIOTE_CONFIG_MODE_Pos | 
                                          GPIOTE_CONFIG_OUTINIT_High    << GPIOTE_CONFIG_OUTINIT_Pos |
                                          GPIOTE_CONFIG_POLARITY_Toggle << GPIOTE_CONFIG_POLARITY_Pos |
                                          PIN_B                         << GPIOTE_CONFIG_PSEL_Pos;
        
        // Have CC[0] in the timer toggle both pin A and pin B (using the FORK feature to control two tasks)
        NRF_PPI->CH[PPI_CH_A].EEP   = (uint32_t)&ADC_TIMER->EVENTS_COMPARE[0];
        NRF_PPI->CH[PPI_CH_A].TEP   = (uint32_t)&NRF_GPIOTE->TASKS_OUT[GPIOTE_CH_A];
        NRF_PPI->FORK[PPI_CH_A].TEP = (uint32_t)&NRF_GPIOTE->TASKS_OUT[GPIOTE_CH_B];
        NRF_PPI->CHENSET = (1 << PPI_CH_A);
        
        // Have CC[1] trigger an ADC sample
        NRF_PPI->CH[PPI_CH_B].EEP   = (uint32_t)&ADC_TIMER->EVENTS_COMPARE[1];
        NRF_PPI->CH[PPI_CH_B].TEP   = (uint32_t)&NRF_SAADC->TASKS_SAMPLE;
        NRF_PPI->CHENSET = (1 << PPI_CH_B);
        
        // Stop the timer once the ADC buffer is full
        NRF_PPI->CH[PPI_CH_C].EEP   = (uint32_t)&NRF_SAADC->EVENTS_END;
        NRF_PPI->CH[PPI_CH_C].TEP   = (uint32_t)&ADC_TIMER->TASKS_STOP;
        NRF_PPI->CHENSET = (1 << PPI_CH_C);      
    }
    

    I haven't included the ADC configuration, but essentially the length of the configured buffer will determine how long the total operation will take. When the END event in the SAADC module occurs the timer will be stopped, and you can use the same event to trigger an interrupt.

    To start the operation simply activate the START tasks of the timer and ADC:

    static void adc_read()
    {
        NRF_SAADC->TASKS_START = 1;
        ADC_TIMER->TASKS_START = 1;
    }
    

    Please be aware that the code compiles, but is not tested ;)

    Best regards
    Torbørn

  • This is brilliant, and exactly the kind of answer I needed because it jump-starts my understanding of the PPI system and prevents me from going down the wrong path. Thank you very much for the prompt and detailed response!

  • I am happy to help :)

    The best of luck with your project!

  • Thank you Torborn;  why can't the SDK provide examples like this instead of the overcomplicated macro hell it does; 

Related