Using the DPPI on the nRF5340 to enable the ADC and GPIO toggling based on a hardware timer

Hi,

I need to enable the ADC read and GPIO pin toggling to happen simultaneously at high precision, without using the MCU resources, approximately 15 us interval. After searching through this forum, I realized the only way to achieve this precision is to use DPPI in the nrfx drivers directly, instead of using the Zephyr drivers.

I successfully enabled the timer1 that triggers the ADC read configured in the DPPI, and another set of code to use the same timer that toggles the GPIO pin also configured in the DPPI but I cannot set them together. Here are snippets of my code for configuring DPPI:

/* Includes ------------------------------------------------------------------*/
#include <errno.h>
#include <logging/log.h>

#include <nrfx.h>
#include <nrfx_saadc.h>
#include <nrfx_timer.h>
#include <nrfx_gpiote.h>
#include <helpers/nrfx_gppi.h>

#include <nrfx_dppi.h>

#define SAMPLES_IN_BUFFER 	4
#define SAMPLE_RATE 		16000000//1000000UL

#define IRQ_PRIO_LOWEST 	0

#define GPIOTE_INTERRUPT_PRIORITY 1
#define GPIOTE_MCLK_SYNC_PIN_OUT 0
#define DIVIDER_COUNT 20

#define PIN 3

// READ1 channel
#define AIN5_READ1						1
#define AIN4_VCM_CHANNEL_INPUT 			NRF_SAADC_INPUT_AIN4
#define AIN5_READ1_MCU_CHANNEL_INPUT 	NRF_SAADC_INPUT_AIN5

LOG_MODULE_REGISTER(nrfx_sample, LOG_LEVEL_INF);

static void saadc_evt_handler(nrfx_saadc_evt_t const * p_event);
static void timer1_evt_handler(nrf_timer_event_t event_type, void * p_context);
static void timer_init(void);
static void saadc_init(void);

int16_t saadc_buf1[SAMPLES_IN_BUFFER];

nrfx_timer_t timer1 = NRFX_TIMER_INSTANCE(1);

void main(void)
{
	printk("nrfx_saadc sample on %s\n", CONFIG_BOARD);

	nrfx_err_t err;

	/* Connect SAADC IRQ to nrfx_saadc_irq_handler */
	IRQ_CONNECT(SAADC_IRQn, IRQ_PRIO_LOWEST, nrfx_isr, nrfx_saadc_irq_handler, 0);

	timer_init();
	saadc_init();
	// gpiote_init();

	/* Allocate a (D)PPI channel. */
	uint8_t dppi_channel_1;
	err = nrfx_dppi_channel_alloc(&dppi_channel_1);
	if (err != NRFX_SUCCESS) {
		LOG_ERR("(D)PPI channel 1 allocation error: %08x", err);
		return;
	}

	/* Configure endpoints of the channel so that the TIMER1 CAPTURE0
	 * event is connected with the SAADC SAMPLE task. This means that each time
	 * TIMER1 reaches it's set compare value, the SAADC will sample all 
	 * enabled channel once.
	 */
	nrfx_gppi_channel_endpoints_setup(dppi_channel_1,
		nrfx_timer_event_address_get(&timer1, NRF_TIMER_EVENT_COMPARE0),
		nrf_saadc_task_address_get(NRF_SAADC, NRF_SAADC_TASK_SAMPLE));

	/* Enable (D)PPI channel. */
	err = nrfx_dppi_channel_enable(dppi_channel_1);
	if (err != NRFX_SUCCESS) {
		LOG_ERR("Failed to enable (D)PPI channel 1, error: %08x", err);
		return;
	}

	LOG_INF("(D)PPI configured, leaving main()");

	nrfx_timer_enable(&timer1);
	printk("TIMER1 started\n");
}

static void saadc_evt_handler(nrfx_saadc_evt_t const * p_event)
{
	nrfx_err_t err = NRFX_SUCCESS;

	if(p_event->type == NRFX_SAADC_EVT_DONE)
	{		
		printk("SAADC sampled: \nREAD1\tREAD2\tREAD3\tREAD4\n");
		for(uint8_t i = 0; i < p_event->data.done.size; i++)
		{
			printk("%hi\t", p_event->data.done.p_buffer[i]);
		}
		printk("\n");
	}
	else if(p_event->type == NRFX_SAADC_EVT_CALIBRATEDONE)
	{
		printk("SAADC calibrated.\n");
	}
	else if(p_event->type == NRFX_SAADC_EVT_BUF_REQ)
	{
		printk("SAADC buffer requested\n");

		err = nrfx_saadc_buffer_set(saadc_buf1, SAMPLES_IN_BUFFER);
		if(err != NRFX_SUCCESS)
		{
			LOG_ERR("Error! Could not set buffer1: %d\n", err);
		}
	}
	else if(p_event->type == NRFX_SAADC_EVT_FINISHED)
	{
		printk("SAADC finished sampling\n\n");
	}
}

static void timer1_evt_handler(nrf_timer_event_t event_type, void * p_context)
{
	// Nothing to do here, the timer's IRQ is not enabled
}

static void timer_init(void)
{
	nrfx_err_t err = NRFX_SUCCESS;
	nrfx_timer_config_t timer_cfg = NRFX_TIMER_DEFAULT_CONFIG;
	timer_cfg.bit_width = NRF_TIMER_BIT_WIDTH_32;

	err = nrfx_timer_init(&timer1, &timer_cfg, timer1_evt_handler);
	if(err != NRFX_SUCCESS)
	{
		LOG_ERR("Error! Could not initialize TIMER1: %d\n", err);
	}

	nrfx_timer_extended_compare(&timer1, NRF_TIMER_CC_CHANNEL0, nrfx_timer_us_to_ticks(&timer1, 1000000), NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);
}

static void saadc_init(void)
{
	nrfx_err_t err = NRFX_SUCCESS;

	nrfx_saadc_channel_t saadc_channel_read1 = NRFX_SAADC_DEFAULT_CHANNEL_DIFFERENTIAL(AIN5_READ1_MCU_CHANNEL_INPUT, AIN4_VCM_CHANNEL_INPUT, AIN5_READ1);
	nrfx_saadc_adv_config_t saadc_adv_cfg = NRFX_SAADC_DEFAULT_ADV_CONFIG;
	saadc_adv_cfg.start_on_end = true;
	
	err = nrfx_saadc_init(IRQ_PRIO_LOWEST);
	if(err != NRFX_SUCCESS)
	{
		LOG_ERR("Error! Could not initialize SAADC: %d\n", err);
	}

	err = nrfx_saadc_offset_calibrate(NULL);
	if(err != NRFX_SUCCESS)
	{
		LOG_ERR("Error! Could not calibrate offset: %d\n", err);
	}
	
	err = nrfx_saadc_channel_config(&saadc_channel_read1);
	if(err != NRFX_SUCCESS)
	{
		LOG_ERR("Error! Could not configure SAADC channel 1: %d\n", err);
	}

	err = nrfx_saadc_advanced_mode_set((1 << AIN5_READ1), NRF_SAADC_RESOLUTION_10BIT, &saadc_adv_cfg, saadc_evt_handler);
	if(err != NRFX_SUCCESS)
	{
		LOG_ERR("Error! Could not set advanced SAADC mode: %d\n", err);
	}

	err = nrfx_saadc_buffer_set(saadc_buf1, SAMPLES_IN_BUFFER);
	if(err != NRFX_SUCCESS)
	{
		LOG_ERR("Error! Could not set buffer1: %d\n", err);
	}

	err = nrfx_saadc_mode_trigger();
	if(err != NRFX_SUCCESS)
	{
		LOG_ERR("Error! Could trigger mode: %d\n", err);
	}	
}

void gpiote_init(void)
{
    LOG_INF("nrfx_gpiote sample on %s", CONFIG_BOARD);

    nrfx_err_t err;

    err = nrfx_gpiote_init(GPIOTE_INTERRUPT_PRIORITY);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_gpiote_init error: %08x", err);
        return;
    }

    nrfx_gpiote_out_config_t const out_config = NRFX_GPIOTE_CONFIG_OUT_TASK_TOGGLE(false);

    /* Initialize output pin. SET task will turn the LED on,
     * CLR will turn it off and OUT will toggle it.
     */
    err = nrfx_gpiote_out_init(PIN, &out_config);
    if (err != NRFX_SUCCESS )
    {
        LOG_ERR("nrfx_gpiote_out_init error: %08x", err);
    }

    nrfx_timer_config_t timer_cfg = NRFX_TIMER_DEFAULT_CONFIG;
    timer_cfg.mode = NRF_TIMER_MODE_TIMER;
    timer_cfg.bit_width = NRF_TIMER_BIT_WIDTH_32;
    err = nrfx_timer_init(&timer1, &timer_cfg, timer1_evt_handler);

    nrfx_timer_extended_compare(&timer1, NRF_TIMER_CC_CHANNEL0, DIVIDER_COUNT/2, NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);

    uint8_t channel;
    err = nrfx_dppi_channel_alloc(&channel);
    nrfx_gpiote_pin_t gpiote_mclk_sync_pin_out = PIN;

    nrfx_gppi_channel_endpoints_setup(channel, nrfx_timer_event_address_get(&timer1, NRF_TIMER_EVENT_COMPARE0), nrfx_gpiote_out_task_addr_get(gpiote_mclk_sync_pin_out));
    nrfx_gppi_channels_enable(BIT(channel));
    nrfx_gpiote_out_task_enable(PIN);
    nrfx_timer_enable(&timer1);
}

In the API reference, DPPI does not allow one task or event to associate with more than one channel. In my case, ideally, I would like to toggle the GPIO pin based on the timer1 and also triggers the ADC read every time the GPIO toggles, at the rising edge and falling edge.

Can you suggest a method to seamlessly enable the ADC read and GPIO pin toggling with the timer1 configured in DPPI? Maybe there is a way to set a GPIO pin to the PWM based on the timer1 while triggering the ADC read in DPPI?

Thanks!

  • Hi,

    When you alloc a dppi channel, then you can use this channel to connect severval publishers and subcribers, e.g. in this case you want to set the allocated channel to the TIMER->PUBLISH_COMPARE[] register:
    https://infocenter.nordicsemi.com/topic/ps_nrf5340/timer.html#register.PUBLISH_COMPARE

    And then for each peripheral that you want to do something based on this event, then you need to set the subscribe, e.g. in your case set the came dppi channel to SAADC->SUBSCRIBE_START and GPIOTE->SUBSCRIBE_OUT[]:
    https://infocenter.nordicsemi.com/topic/ps_nrf5340/saadc.html#register.SUBSCRIBE_START 
    https://infocenter.nordicsemi.com/topic/ps_nrf5340/gpiote.html#register.SUBSCRIBE_OUT 

    Best regards,
    Kenneth

  • Hello Kenneth,

    Thanks for the info. Do you have any SDK examples available for subscribing to those DPPI channels? It seems like the nrfx drivers do not offer many examples in the SDK...

    Ted

  • I can find various code that show this to some degree, for instance ppi_init() from esb.c

    nrfx_dppi_channel_alloc(&ppi_ch_radio_ready_timer_start);
    nrfx_dppi_channel_alloc(&ppi_ch_radio_address_timer_stop);
    nrfx_dppi_channel_alloc(&ppi_ch_timer_compare0_radio_disable);
    nrfx_dppi_channel_alloc(&ppi_ch_timer_compare1_radio_txen);
    
    NRF_RADIO->PUBLISH_READY          = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | ppi_ch_radio_ready_timer_start;
    ESB_SYS_TIMER->SUBSCRIBE_START    = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | ppi_ch_radio_ready_timer_start;
    // here you can add several subscribe if you like.
    NRF_RADIO->PUBLISH_ADDRESS        = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | ppi_ch_radio_address_timer_stop;
    ESB_SYS_TIMER->SUBSCRIBE_SHUTDOWN = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | ppi_ch_radio_address_timer_stop;
    // here you can add several subscribe if you like.
    ESB_SYS_TIMER->PUBLISH_COMPARE[0] = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | ppi_ch_timer_compare0_radio_disable;
    NRF_RADIO->SUBSCRIBE_DISABLE      = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | ppi_ch_timer_compare0_radio_disable;
    // here you can add several subscribe if you like.
    ESB_SYS_TIMER->PUBLISH_COMPARE[1] = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | ppi_ch_timer_compare1_radio_txen;
    NRF_RADIO->SUBSCRIBE_TXEN         = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | ppi_ch_timer_compare1_radio_txen;
    // here you can add several subscribe if you like.
    
    static uint32_t ppi_all_channels_mask;
    
    ppi_all_channels_mask = (1 << ppi_ch_radio_ready_timer_start) | (1 << ppi_ch_radio_address_timer_stop) |
    						(1 << ppi_ch_timer_compare0_radio_disable) | (1 << ppi_ch_timer_compare1_radio_txen);
    							
    nrfx_gppi_channels_enable(ppi_all_channels_mask);			

  • So here is what I did but I still cannot get the ADC read working... The GPIO pin toggling only works. Am I missing something here?

    	/* Allocate a (D)PPI channel. */
    	uint8_t dppi_channel_0, dppi_channel_1;
    	err = nrfx_dppi_channel_alloc(&dppi_channel_0);
    	if (err != NRFX_SUCCESS) {
    		LOG_ERR("(D)PPI channel 0 allocation error: %08x", err);
    		return;
    	}
    	err = nrfx_dppi_channel_alloc(&dppi_channel_1);
    	if (err != NRFX_SUCCESS) {
    		LOG_ERR("(D)PPI channel 1 allocation error: %08x", err);
    		return;
    	}
    	
    	NRF_TIMER1->PUBLISH_COMPARE[0] = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | dppi_channel_0;
    	NRF_SAADC->SUBSCRIBE_START = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | dppi_channel_0;
    
    	NRF_TIMER1->PUBLISH_COMPARE[0] = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | dppi_channel_1;
    	NRF_GPIOTE->SUBSCRIBE_OUT[1] = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | dppi_channel_1;
    
    	static uint32_t dppi_all_channels_mask;
    
    	dppi_all_channels_mask = (1 << dppi_channel_0) | (1 << dppi_channel_1);
    
    	nrfx_gppi_channels_enable(dppi_all_channels_mask);	
    
    	LOG_INF("DPPI configured");
    
    	nrfx_gpiote_out_task_enable(PIN);
    
    	LOG_INF("nrfx_gpiote initialized");
    
    	nrfx_timer_enable(&timer1);
    	printk("TIMER1 started\n");

  • Try something like this:

    	/* Allocate a (D)PPI channel. */
    	uint8_t dppi_channel_0;
    	err = nrfx_dppi_channel_alloc(&dppi_channel_0);
    	if (err != NRFX_SUCCESS) {
    		LOG_ERR("(D)PPI channel 0 allocation error: %08x", err);
    		return;
    	}
    
    	NRF_TIMER1->PUBLISH_COMPARE[0] = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | dppi_channel_0;
    	NRF_SAADC->SUBSCRIBE_START = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | dppi_channel_0;
    	NRF_GPIOTE->SUBSCRIBE_OUT[1] = DPPIC_SUBSCRIBE_CHG_EN_EN_Msk | dppi_channel_0;
    
    	static uint32_t dppi_all_channels_mask;
    
    	dppi_all_channels_mask = (1 << dppi_channel_0);
    
    	nrfx_gppi_channels_enable(dppi_all_channels_mask);	
    
    	LOG_INF("DPPI configured");
    
    	nrfx_gpiote_out_task_enable(PIN);
    
    	LOG_INF("nrfx_gpiote initialized");
    
    	nrfx_timer_enable(&timer1);
    	printk("TIMER1 started\n");

Related