GPIOTE trigger of SPIM read using EasyDMA with manual CS

I’m using NRF Connect SDK 3.2.4 with the nRF54L15-DK. I’ve successfully used the nrfx_gppi_conn_alloc() and  nrfx_gppi_conn_enable() APIs to connect a timer to a GPIOTE output. That output is on P1.12 and it drives ADC conversion control to an ADC IC. The ADC in turn drives the DRDY line to the nRF54L15 P1.11. I now want to have a GPIOTE input on pin 1.11 trigger a SPIM read using manual chip select control. As a first step I’m just trying to toggle the SPIM_CS_PIN (task) using the DRDY_PIN_TOGGLE event. I realize that I will likely have to add a timer to meet the CS timing requirements of the ADC IC, but first things first. I ‘ve spent too much time on this and think I’m doing everything right. Can someone point out what I’m missing. Thank you.

prj.conf

CONFIG_NCS_SAMPLES_DEFAULTS=y
CONFIG_ESB=y
CONFIG_DK_LIBRARY=y
CONFIG_CLOCK_CONTROL=y
CONFIG_ESB_MAX_PAYLOAD_LENGTH=252
#CONFIG_ESB_TX_FIFO_SIZE=10
#CONFIG_ESB_RX_FIFO_SIZE=10
CONFIG_CRC=y
CONFIG_ESB_FAST_CHANNEL_SWITCHING=y
CONFIG_ESB_FAST_SWITCHING=y
#CONFIG_ESB_NEVER_DISABLE_TX=y
CONFIG_FPU=y
CONFIG_CBPRINTF_FP_SUPPORT=y
#CONFIG_SPI=y
CONFIG_TIMING_FUNCTIONS=y
CONFIG_ENTROPY_GENERATOR=y

CONFIG_SPI=n
CONFIG_NRFX_SPIM=y

CONFIG_GPIO=y

# Disable LFXO (external 32kHz crystal)
CONFIG_CLOCK_CONTROL_NRF_K32SRC_XTAL=n

# Enable LFRC (internal RC oscillator)
CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y

# Select RC Accuracy (500ppm is typical for internal, can be 250ppm or 500ppm)
CONFIG_CLOCK_CONTROL_NRF_K32SRC_500PPM=y

# Enable calibration to improve accuracy (highly recommended)
CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC_CALIBRATION=y

# Set LFRC frequency
CONFIG_CLOCK_CONTROL_NRF_K32SRC_FREQUENCY=32768

CONFIG_NRFX_TIMER=y

CONFIG_NRFX_GPPI=y

# CONFIG_SPI_ASYNC=y

device tree overlay

&uicr {
	nfct-pins-as-gpios;
};

/ {
	gpi {
		compatible = "gpio-keys";

		drdy: drdy {
			gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>;
			label = "ADS9324 DRDY";
		};
	};

	gpo {
		compatible = "gpio-leds";

		reset: reset {
			gpios = <&gpio2 8 GPIO_ACTIVE_LOW>;
			label = "ADS9324 RESET";
		};

		convst: convst {
			gpios = <&gpio1 12 GPIO_ACTIVE_LOW>;
			label = "ADS9324 CONVST";
		};
	};

	aliases {
		reset = &reset;
		convst = &convst;
		drdy = &drdy;
	};
};

&nfct {
Relevant code snippet
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
void drdy_init(nrfx_gppi_handle_t* gppi_h) {

	int err;
	
	uint8_t gppi_channel;
	
	uint8_t in_channel;

	uint8_t out_channel;
	
	// ---------------------------------------
	// INIT GPIOTE
	// ---------------------------------------	
	//IRQ_DIRECT_CONNECT(GPIOTE20_0_IRQn, IRQ_PRIO_LOWEST, gpiote_20_direct_isr, 0);
    //IRQ_DIRECT_CONNECT(GPIOTE20_1_IRQn, IRQ_PRIO_LOWEST, gpiote_20_direct_isr, 0);
  
	if(!nrfx_gpiote_init_check(&gpiote_inst))
	{		
		//err = nrfx_gpiote_init(&gpiote_inst, IRQ_PRIO_LOWEST);
		err = nrfx_gpiote_init(&gpiote_inst, NRFX_GPIOTE_DEFAULT_CONFIG_IRQ_PRIORITY);		
		if (err != 0) {
			LOG_ERR("GPIOTE init fail: %x", err);	
		}
		LOG_INF("GPIOTE status: %s", nrfx_gpiote_init_check(&gpiote_inst) ? "initialized" : "not initialized");
	}

   //irq_enable(GPIOTE20_1_IRQn);

	// ---------------------------------------
	// DRDY Input
	// ---------------------------------------	
	err = nrfx_gpiote_channel_alloc(&gpiote_inst, &in_channel);
	if (err != 0) {
		LOG_ERR("Channel alloc fail: %x", err);	
	}

	static const nrf_gpio_pin_pull_t pull_config = NRF_GPIO_PIN_PULLDOWN;

	nrfx_gpiote_trigger_config_t trigger_config = {
		.trigger = NRFX_GPIOTE_TRIGGER_LOTOHI,
		.p_in_channel = &in_channel
  	};

	// static const nrfx_gpiote_handler_config_t handler_config = {
	// 	.handler = gpiote_evt_handler,
    // 	.p_context = NULL
	// };

  	nrfx_gpiote_input_pin_config_t gpiote_in_cfg = {
		.p_pull_config = &pull_config,
    	.p_trigger_config = &trigger_config,
    	.p_handler_config = NULL
  	};

	// We want drdy input from ADC to trigger a SPI read 
  	err = nrfx_gpiote_input_configure(&gpiote_inst, DRDY_PIN, &gpiote_in_cfg);
	if (err != 0) {
		LOG_ERR("GPIOTE configure fail: %x", err);	
	}	
	
	nrfx_gpiote_trigger_enable(&gpiote_inst, DRDY_PIN, false);	

	// ---------------------------------------
	// SPIM CS Output
	// ---------------------------------------	
	err = nrfx_gpiote_channel_alloc(&gpiote_inst, &out_channel);
	if (err != 0) {
		LOG_ERR("Channel alloc fail: %x", err);	
	}
	
	nrfx_gpiote_output_config_t gpiote_out_cfg = {
		.drive = NRF_GPIO_PIN_S0S1,
		.input_connect = NRF_GPIO_PIN_INPUT_DISCONNECT,
		.pull = NRF_GPIO_PIN_NOPULL,
	};

	nrfx_gpiote_task_config_t gpiote_task_cfg = {
		.task_ch = out_channel,
		.polarity = GPIOTE_CONFIG_POLARITY_Toggle,
		.init_val = NRF_GPIOTE_INITIAL_VALUE_HIGH,
	};	

	err = nrfx_gpiote_output_configure(&gpiote_inst, SPIM_CS_PIN, &gpiote_out_cfg, &gpiote_task_cfg);
	if (err != 0) {
		LOG_ERR("GPIOTE configure fail: %x", err);	
	}	
	
	// // // ---------------------------------------
	// // // INIT SPI
	// // // ---------------------------------------
	// nrfx_spim_xfer_desc_t adc_xfer_desc;	

	// adc_xfer_desc.p_tx_buffer  = NULL;
	// adc_xfer_desc.tx_length    = 0;
	// adc_xfer_desc.p_rx_buffer  = adc_buff;
	// adc_xfer_desc.rx_length    = 16;	
    
    // uint32_t flags = NRFX_SPIM_FLAG_HOLD_XFER    |
    //                  NRFX_SPIM_FLAG_REPEATED_XFER|
    //                  NRFX_SPIM_FLAG_RX_POSTINC   |
    //                  NRFX_SPIM_FLAG_NO_XFER_EVT_HANDLER;
    
    // err = nrfx_spim_xfer(&spi_inst, &adc_xfer_desc, flags);
	// if (err != 0) {		
	// 	LOG_ERR("SPIM transfer failed: %i", err);
	// 	return err;
	// }

	// // ---------------------------------------
	// // 3. CONNECT GPIOTE w/ SPIM via GPPI
	// // ---------------------------------------
	// err = nrfx_gppi_conn_alloc(    
    // 	nrfx_gpiote_in_event_address_get(&gpiote_inst, DRDY_PIN), 
	// 	nrfx_spim_start_task_address_get(&spi_inst), gppi_h);		
		
    // if (err != 0) {
	// 	LOG_ERR("GPPI channel allocation fail: %x", err);	
	// }	
	
	err = nrfx_gppi_conn_alloc( 
		nrfx_gpiote_in_event_address_get(&gpiote_inst, DRDY_PIN),
		nrfx_gpiote_set_task_address_get(&gpiote_inst, SPIM_CS_PIN), gppi_h);		
		
    if (err != 0) {
		LOG_ERR("GPPI channel allocation fail: %x", err);	
	}

	// err = nrfx_gppi_conn_alloc( 
	// 	nrfx_spim_end_event_address_get(&spi_inst),
	// 	nrfx_gpiote_set_task_address_get(&gpiote_inst, SPIM_CS_PIN), gppi_h);				

    // if (err != 0) {
	// 	LOG_ERR("GPPI channel allocation fail: %x", err);	
	// }
	
	nrfx_gpiote_trigger_enable(&gpiote_inst, DRDY_PIN, false);	
	nrfx_gpiote_out_task_enable(&gpiote_inst, SPIM_CS_PIN);
	nrfx_gppi_conn_enable(*gppi_h);


}
Parents
  • Hie Jason,

    Thanks a lot for your patience. 

    Your GPPI implementation looks solid to me. nrfx_gppi_conn_alloc() and nrfx_gppi_conn_enable() are the correct calls here they handle configuring the DPPI channel and managing the PPI bridge automatically if the endpoints cross power domains. I don't suspect the connection logic itself. You should probably check if the few initialization step have been implemented correctly 

    1. GPIOTE Task/Event Enables:

      CS Pin: Once you configure the CS pin as an output using nrfx_gpiote_output_configure(), make sure to call nrfx_gpiote_out_task_enable() on the CS pin. Without this, the OUT task won't drive the pin via PPI, leading to the common issue of the hardware getting stuck in a half-configured state where the channel triggers but CS pin ignores it completely and never changes state. 

      DRDY Pin: The IN event won't generate until you execute nrfx_gpiote_trigger_enable(inst, DRDY_PIN, false).Setting the last parameter (int_enable) to false configures the hardware to generate the event locally without routing an interrupt to the CPU. This allows the PPI path to run entirely in hardware without invoking any software handlers or interrupting core execution.

      Also,please check the parameter order in   the nrfx_gppi_conn_alloc() function signature, the parameter order strictly expects the triggering event address first, followed by the target task address second. Passing them in reverse order won't trigger a compiler error since both are standard integer addresses, but it will map the signal routes backward, preventing the PPI channel from triggering.

    2. SPIM CS Line Ownership:

      The next thing I would check is whether the SPIM is still holding the CS pin. With a manual CS you have to set ss_pin = NRF_SPIM_PIN_NOT_CONNECTED, otherwise the driver keeps driving the pin and ends up fighting the GPIOTE task. Same goes for the spim node if they are using pinctrl. 

      • . I realize that I will likely have to add a timer to meet the CS timing requirements of the ADC IC,

      Since the Timer output (P1.12) and DRDY (P1.11) both sit on the GPIOTE20 instance, using GPIOTE20 for the CS pin as well keeps the whole event-task loop inside a single hardware domain and will keep  your resource allocation clean.

    Please get back to us if you have more questions.

    Best Regards

    Pallavi

  • Thanks for the response Pallavi. If you look at the drdy_init() routine in the relevant code snippet above you will note that I’ve performed each of your recommendations.

     GPIOTE Task/Event Enables: 

    CS Pin: Once you configure the CS pin as an output using nrfx_gpiote_output_configure(), make sure to call nrfx_gpiote_out_task_enable() on the CS pin. (see line 139).

     DRDY Pin: The IN event won't generate until you execute nrfx_gpiote_trigger_enable(inst, DRDY_PIN, false). (see line 63)

    Also, please check the parameter order in  the nrfx_gppi_conn_alloc() function signature, the parameter order strictly expects the triggering event address first, followed by the target task address second. Passing them in reverse order won't trigger a compiler error since both are standard integer addresses, but it will map the signal routes backward, preventing the PPI channel from triggering. (see line 122).

     SPIM CS Line Ownership: 

    The next thing I would check is whether the SPIM is still holding the CS pin. With a manual CS you have to set ss_pin = NRF_SPIM_PIN_NOT_CONNECTED, otherwise the driver keeps driving the pin and ends up fighting the GPIOTE task. (see below).

    	nrfx_spim_config_t spim_config = NRFX_SPIM_DEFAULT_CONFIG(SPIM_SCK_PIN,
                                                                  SPIM_MOSI_PIN,
                                                                  SPIM_MISO_PIN,
                                                                  NRF_SPIM_PIN_NOT_CONNECTED);
    
    	spim_config.frequency = 8000000;
    
    	err = nrfx_spim_init(&spi_inst, &spim_config, NULL, NULL);
        if (err) {
            return 0;
        }	
    
    	uint32_t flags = 0;	
    	uint16_t reg_data = 0;
    
    	nrf_gpio_cfg_output(SPIM_CS_PIN);

     

    I would like to also note that I have a convst_init() routine that successfully uses the high-level gppi routines to connect a timer to a GPIOTE output. See below.

     

    // ----------------------------------------------------------------------------
    // ----------------------------------------------------------------------------	
    void convst_init(nrfx_gppi_handle_t* gppi_h) {
    
    	int err;	
    	
    	uint8_t out_channel;		
    	
    	// ---------------------------------------
    	// INIT GPIOTE
    	// ---------------------------------------	
    	if(!nrfx_gpiote_init_check(&gpiote_inst))
    	{
    		err = nrfx_gpiote_init(&gpiote_inst, NRFX_GPIOTE_DEFAULT_CONFIG_IRQ_PRIORITY);
    		if (err != 0) {
    			LOG_ERR("GPIOTE init fail: %x", err);	
    		}
    		LOG_INF("GPIOTE status: %s", nrfx_gpiote_init_check(&gpiote_inst) ? "initialized" : "not initialized");
    	}
    
    	// ---------------------------------------
    	// CONVST Output
    	// ---------------------------------------	
    	err = nrfx_gpiote_channel_alloc(&gpiote_inst, &out_channel);
    	if (err != 0) {
    		LOG_ERR("Channel alloc fail: %x", err);	
    	}
    	
    	nrfx_gpiote_output_config_t gpiote_out_cfg = {
    		.drive = NRF_GPIO_PIN_S0S1,
    		.input_connect = NRF_GPIO_PIN_INPUT_DISCONNECT,
    		.pull = NRF_GPIO_PIN_NOPULL,
    	};
    
    	nrfx_gpiote_task_config_t gpiote_task_cfg = {
    		.task_ch = out_channel,
    		.polarity = NRF_GPIOTE_POLARITY_TOGGLE,
    		.init_val = NRF_GPIOTE_INITIAL_VALUE_HIGH,
    	};
    
    	err = nrfx_gpiote_output_configure(&gpiote_inst, CONVST_PIN, &gpiote_out_cfg, &gpiote_task_cfg);
    	if (err != 0) {
    		LOG_ERR("GPIOTE configure fail: %x", err);	
    	}
    
    	nrfx_gpiote_out_task_enable(&gpiote_inst, CONVST_PIN);
    	
    	// ---------------------------------------
    	// INIT TIMER
    	// ---------------------------------------
    	nrfx_timer_config_t timer_cfg = {
        	.frequency = NRFX_MHZ_TO_HZ(1), 
        	.mode = NRF_TIMER_MODE_TIMER,
        	.bit_width = NRF_TIMER_BIT_WIDTH_16,
        	.interrupt_priority = NRFX_TIMER_DEFAULT_CONFIG_IRQ_PRIORITY,
    	};    
    	
       	err = nrfx_timer_init(&timer_inst, &timer_cfg, NULL);
         if (err != 0) {
    		LOG_ERR("TIMER init fail: %x", err);	
    	}
    
        nrfx_timer_clear(&timer_inst);   
    
        nrfx_timer_extended_compare(&timer_inst, NRF_TIMER_CC_CHANNEL1, 4, NRF_TIMER_SHORT_COMPARE1_CLEAR_MASK, false);	
    
    	// ---------------------------------------
    	// 3. CONNECT GPIOTE w/ TIMER via GPPI
    	// ---------------------------------------
    	err = nrfx_gppi_conn_alloc(
            nrfx_timer_compare_event_address_get(&timer_inst, NRF_TIMER_CC_CHANNEL1),
            nrfx_gpiote_out_task_address_get(&gpiote_inst, CONVST_PIN), gppi_h);
        if (err != 0) {
    		LOG_ERR("GPPI channel allocation fail: %x", err);	
    	}	
    
        nrfx_timer_enable(&timer_inst);		  
    
    	nrfx_gppi_conn_enable(*gppi_h);	
    }

     

    Below is how I am calling each routine. I'm assuming I need two separate gppi handles since these are standalone and separate event/task and peripherals.

    Any additional thoughts on what I could be missing?

    nrfx_gppi_handle_t drdy_gppi_h, convst_gppi_h;		
    	
    	// timer triggers conversion start (convst) to ADC 
    	convst_init(&convst_gppi_h);	
    
    	// drdy from ADC triggers SPI read of 15 samples (240 bytes)
    	drdy_init(&drdy_gppi_h);		

Reply
  • Thanks for the response Pallavi. If you look at the drdy_init() routine in the relevant code snippet above you will note that I’ve performed each of your recommendations.

     GPIOTE Task/Event Enables: 

    CS Pin: Once you configure the CS pin as an output using nrfx_gpiote_output_configure(), make sure to call nrfx_gpiote_out_task_enable() on the CS pin. (see line 139).

     DRDY Pin: The IN event won't generate until you execute nrfx_gpiote_trigger_enable(inst, DRDY_PIN, false). (see line 63)

    Also, please check the parameter order in  the nrfx_gppi_conn_alloc() function signature, the parameter order strictly expects the triggering event address first, followed by the target task address second. Passing them in reverse order won't trigger a compiler error since both are standard integer addresses, but it will map the signal routes backward, preventing the PPI channel from triggering. (see line 122).

     SPIM CS Line Ownership: 

    The next thing I would check is whether the SPIM is still holding the CS pin. With a manual CS you have to set ss_pin = NRF_SPIM_PIN_NOT_CONNECTED, otherwise the driver keeps driving the pin and ends up fighting the GPIOTE task. (see below).

    	nrfx_spim_config_t spim_config = NRFX_SPIM_DEFAULT_CONFIG(SPIM_SCK_PIN,
                                                                  SPIM_MOSI_PIN,
                                                                  SPIM_MISO_PIN,
                                                                  NRF_SPIM_PIN_NOT_CONNECTED);
    
    	spim_config.frequency = 8000000;
    
    	err = nrfx_spim_init(&spi_inst, &spim_config, NULL, NULL);
        if (err) {
            return 0;
        }	
    
    	uint32_t flags = 0;	
    	uint16_t reg_data = 0;
    
    	nrf_gpio_cfg_output(SPIM_CS_PIN);

     

    I would like to also note that I have a convst_init() routine that successfully uses the high-level gppi routines to connect a timer to a GPIOTE output. See below.

     

    // ----------------------------------------------------------------------------
    // ----------------------------------------------------------------------------	
    void convst_init(nrfx_gppi_handle_t* gppi_h) {
    
    	int err;	
    	
    	uint8_t out_channel;		
    	
    	// ---------------------------------------
    	// INIT GPIOTE
    	// ---------------------------------------	
    	if(!nrfx_gpiote_init_check(&gpiote_inst))
    	{
    		err = nrfx_gpiote_init(&gpiote_inst, NRFX_GPIOTE_DEFAULT_CONFIG_IRQ_PRIORITY);
    		if (err != 0) {
    			LOG_ERR("GPIOTE init fail: %x", err);	
    		}
    		LOG_INF("GPIOTE status: %s", nrfx_gpiote_init_check(&gpiote_inst) ? "initialized" : "not initialized");
    	}
    
    	// ---------------------------------------
    	// CONVST Output
    	// ---------------------------------------	
    	err = nrfx_gpiote_channel_alloc(&gpiote_inst, &out_channel);
    	if (err != 0) {
    		LOG_ERR("Channel alloc fail: %x", err);	
    	}
    	
    	nrfx_gpiote_output_config_t gpiote_out_cfg = {
    		.drive = NRF_GPIO_PIN_S0S1,
    		.input_connect = NRF_GPIO_PIN_INPUT_DISCONNECT,
    		.pull = NRF_GPIO_PIN_NOPULL,
    	};
    
    	nrfx_gpiote_task_config_t gpiote_task_cfg = {
    		.task_ch = out_channel,
    		.polarity = NRF_GPIOTE_POLARITY_TOGGLE,
    		.init_val = NRF_GPIOTE_INITIAL_VALUE_HIGH,
    	};
    
    	err = nrfx_gpiote_output_configure(&gpiote_inst, CONVST_PIN, &gpiote_out_cfg, &gpiote_task_cfg);
    	if (err != 0) {
    		LOG_ERR("GPIOTE configure fail: %x", err);	
    	}
    
    	nrfx_gpiote_out_task_enable(&gpiote_inst, CONVST_PIN);
    	
    	// ---------------------------------------
    	// INIT TIMER
    	// ---------------------------------------
    	nrfx_timer_config_t timer_cfg = {
        	.frequency = NRFX_MHZ_TO_HZ(1), 
        	.mode = NRF_TIMER_MODE_TIMER,
        	.bit_width = NRF_TIMER_BIT_WIDTH_16,
        	.interrupt_priority = NRFX_TIMER_DEFAULT_CONFIG_IRQ_PRIORITY,
    	};    
    	
       	err = nrfx_timer_init(&timer_inst, &timer_cfg, NULL);
         if (err != 0) {
    		LOG_ERR("TIMER init fail: %x", err);	
    	}
    
        nrfx_timer_clear(&timer_inst);   
    
        nrfx_timer_extended_compare(&timer_inst, NRF_TIMER_CC_CHANNEL1, 4, NRF_TIMER_SHORT_COMPARE1_CLEAR_MASK, false);	
    
    	// ---------------------------------------
    	// 3. CONNECT GPIOTE w/ TIMER via GPPI
    	// ---------------------------------------
    	err = nrfx_gppi_conn_alloc(
            nrfx_timer_compare_event_address_get(&timer_inst, NRF_TIMER_CC_CHANNEL1),
            nrfx_gpiote_out_task_address_get(&gpiote_inst, CONVST_PIN), gppi_h);
        if (err != 0) {
    		LOG_ERR("GPPI channel allocation fail: %x", err);	
    	}	
    
        nrfx_timer_enable(&timer_inst);		  
    
    	nrfx_gppi_conn_enable(*gppi_h);	
    }

     

    Below is how I am calling each routine. I'm assuming I need two separate gppi handles since these are standalone and separate event/task and peripherals.

    Any additional thoughts on what I could be missing?

    nrfx_gppi_handle_t drdy_gppi_h, convst_gppi_h;		
    	
    	// timer triggers conversion start (convst) to ADC 
    	convst_init(&convst_gppi_h);	
    
    	// drdy from ADC triggers SPI read of 15 samples (240 bytes)
    	drdy_init(&drdy_gppi_h);		

Children
  • Hie, 

    1. If this is a continuous acquisition, add NRFX_SPIM_FLAG_REPEATED_XFER together with NRFX_SPIM_FLAG_NO_XFER_EVT_HANDLER, so each DRDY re-runs the same held transfer without you re-arming it every time. Without that you get exactly one read and then silence
    2. if the CS is still the manual GPIOTE one, the same DRDY edge should both pull CS low and start the SPIM, which stays a single handle, you just attach the second task with nrfx_gppi_ep_attach()

    Regards

    Pallavi

Related