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

SAADC scan mode of 3 channels --> buffer order swap

Hi,

I already read all the forum posts about this buffer order swap problem of the SAADC. Especially this one: https://devzone.nordicsemi.com/f/nordic-q-a/20291/offset-in-saadc-samples-with-easy-dma-and-ble/79053#79053

I tried to implement the PPI solution, but I am not able to bring this to work. I have still a swap in the order every second call of the callback function. Can anyone help me?

This is pretty much my firmware (without BLE stuff at the moment):

I'm using a compare event of the RTC to trigger the sample task in scan mode. Another compare event is used to enable the ppi channel again after disabling it after 10 samples per channel. The reason for that is that I want to sample at a high frequency but only 10 samples per second. The second PPI channel is used for the fix from the link above.

PPI:

void saadc_sampling_event_init(void)
{
    ret_code_t err_code;

    err_code = nrf_drv_ppi_init();
    APP_ERROR_CHECK(err_code);

    uint32_t rtc_compare_event_addr = nrf_drv_rtc_event_address_get(&rtc, NRF_RTC_EVENT_COMPARE_0);//nrf_drv_timer_compare_event_address_get(&m_timer, NRF_TIMER_CC_CHANNEL0);
    uint32_t saadc_sample_task_addr   = nrf_drv_saadc_sample_task_get();

    /* setup ppi channel so that timer compare event is triggering sample task in SAADC */
    err_code = nrf_drv_ppi_channel_alloc(&m_ppi_channel_1);
    APP_ERROR_CHECK(err_code);
	err_code = nrf_drv_ppi_channel_alloc(&m_ppi_channel_2);
    APP_ERROR_CHECK(err_code);

    err_code = nrf_drv_ppi_channel_assign(m_ppi_channel_1,
                                          rtc_compare_event_addr,
                                          saadc_sample_task_addr);
																					
    APP_ERROR_CHECK(err_code);
		
    err_code = nrf_drv_ppi_channel_assign(m_ppi_channel_2, nrf_saadc_event_address_get(NRF_SAADC_EVENT_END), nrf_saadc_task_address_get(NRF_SAADC_TASK_START));
	APP_ERROR_CHECK(err_code);
		
    err_code = nrf_drv_ppi_channel_enable(m_ppi_channel_2);
    APP_ERROR_CHECK(err_code);
}

SAADC:

#define SAMPLES_IN_BUFFER 10
#define ADC_CHANNEL_COUNT 3

void saadc_sampling_event_enable(void)
{
    ret_code_t err_code = nrf_drv_ppi_channel_enable(m_ppi_channel_1);

    APP_ERROR_CHECK(err_code);
}

void saadc_sampling_event_disable(void)
{
    ret_code_t err_code = nrf_drv_ppi_channel_disable(m_ppi_channel_1);

    APP_ERROR_CHECK(err_code);
}


void saadc_callback(nrf_drv_saadc_evt_t const * p_event)
{
		int value = 0;
		int i;
	
		//Event is triggered when buffer is full! Not when one conversion is done!
		////https://devzone.nordicsemi.com/f/nordic-q-a/14777/ble_app_proximity-saadc-channel-data#post-id-85497
    if (p_event->type == NRF_DRV_SAADC_EVT_DONE)
    {
        ret_code_t err_code;

        err_code = nrf_drv_saadc_buffer_convert(p_event->data.done.p_buffer, SAMPLES_IN_BUFFER*ADC_CHANNEL_COUNT); //Set buffer so the SAADC can write to it again. This is either "buffer 1" or "buffer 2"
        APP_ERROR_CHECK(err_code);
        
        NRF_LOG_INFO("ADC event number: %d", (int)m_adc_evt_counter);
				
        for (i = 0; i < SAMPLES_IN_BUFFER*ADC_CHANNEL_COUNT; i++)
        {
					NRF_LOG_INFO("%d", p_event->data.done.p_buffer[i]);
					switch(i%3)
					{
						case 0: battery_voltage_sum = battery_voltage_sum + p_event->data.done.p_buffer[i];
										break;
						case 1: thermistor_1_sum = thermistor_1_sum + p_event->data.done.p_buffer[i];
										break;
						case 2: thermistor_2_sum = thermistor_2_sum + p_event->data.done.p_buffer[i];
										break;	
					}
        }
					
				measDone = 1;
        m_adc_evt_counter++;
				
				//stop sampling until Compare 1 Event of RTC
				saadc_sampling_event_disable();
    }
}

void saadc_init(void)
{
		//Source for battery measurement: 
		//https://devzone.nordicsemi.com/nordic/nordic-blog/b/blog/posts/measuring-lithium-battery-voltage-with-nrf52
		//Sampling frequeny should be 1Hz-10Hz for high saadc input resistance value! Otherwise voltage devider will be affected by the input reistance!
	
		//Total time < Sum(CH[x].tACQ+tCONV), x=0..enabled channels (Source: Datasheet)
		//Total time < #Channels*(40us + 2us)
		//3 Channels: 126us --> max sampling rate = 1/126uS = 7.94kHz --> set RTC Compare Event (PPI) to this frequency
	
	  ret_code_t err_code;

		//Configure SAADC
		nrf_drv_saadc_config_t  config = NRF_DRV_SAADC_DEFAULT_CONFIG;
		config.resolution = NRF_SAADC_RESOLUTION_12BIT;  
    config.oversample = NRF_SAADC_OVERSAMPLE_DISABLED;  

		//Initialize SAADC
    err_code = nrf_drv_saadc_init(&config, saadc_callback);                       
    APP_ERROR_CHECK(err_code);

		//Configure SAADC channel 0
		nrf_saadc_channel_config_t channel_config = NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(NRF_SAADC_INPUT_AIN7);
		channel_config.gain = NRF_SAADC_GAIN1_4;
		channel_config.acq_time = SAADC_CH_CONFIG_TACQ_3us; //needed for maximum source resistance of 800kOhm (Datasheet) --> 40us. With additional C --> 3us
		//channel_config.burst = NRF_SAADC_BURST_ENABLED;
	                                        
		//Initialize SAADC channel 0
    err_code = nrf_drv_saadc_channel_init(0, &channel_config);
    APP_ERROR_CHECK(err_code);
	
	  //Configure SAADC channel 1
		channel_config.pin_p = NRF_SAADC_INPUT_AIN0;
	 	channel_config.reference = NRF_SAADC_REFERENCE_VDD4;
		channel_config.gain = NRF_SAADC_GAIN1_4;

		//Initialize SAADC channel 1
    err_code = nrf_drv_saadc_channel_init(1, &channel_config);
    APP_ERROR_CHECK(err_code);
		
		//Configure SAADC channel 2
		channel_config.pin_p = NRF_SAADC_INPUT_AIN1;

		//Initialize SAADC channel 2
    err_code = nrf_drv_saadc_channel_init(2, &channel_config);
    APP_ERROR_CHECK(err_code);

		//Set SAADC buffer 1. The SAADC will start to write to this buffer
    err_code = nrf_drv_saadc_buffer_convert(m_buffer_pool[0], SAMPLES_IN_BUFFER*ADC_CHANNEL_COUNT);
    APP_ERROR_CHECK(err_code);

		//Set SAADC buffer 2. The SAADC will write to this buffer when buffer 1 is full. This will give the applicaiton time to process data in buffer 1.
    err_code = nrf_drv_saadc_buffer_convert(m_buffer_pool[1], SAMPLES_IN_BUFFER*ADC_CHANNEL_COUNT);
    APP_ERROR_CHECK(err_code);
}

RTC:

static void rtc_handler(nrf_drv_rtc_int_type_t int_type)
{
		static int i = 2;
    if (int_type == NRF_DRV_RTC_INT_COMPARE0)
    {
        //nrf_gpio_pin_toggle(LED);
				//nrf_drv_rtc_counter_clear(&rtc);
				//reactivate compare irq! 
				nrf_drv_rtc_cc_set(&rtc,0,COMPARE_COUNTERTIME * i++,true);
    }
		
		else if (int_type == NRF_DRV_RTC_INT_COMPARE1)
    {
        nrf_gpio_pin_toggle(LED);
				nrf_drv_rtc_counter_clear(&rtc);
				//reactivate compare irq! 
				nrf_drv_rtc_cc_set(&rtc,0,COMPARE_COUNTERTIME * 1,true);
				nrf_drv_rtc_cc_set(&rtc,1,COMPARE_COUNTERTIME * 1000,true);
				i = 2;
				saadc_sampling_event_enable();
    }
		
    else if (int_type == NRF_DRV_RTC_INT_TICK)
    {
        rtcCounter++;
    }
		
}


static void lfclk_config(void)
{
    ret_code_t err_code = nrf_drv_clock_init();
    APP_ERROR_CHECK(err_code);

    nrf_drv_clock_lfclk_request(NULL);
}


static void rtc_config(void)
{
	uint32_t err_code;

	//Initialize RTC instance
	nrf_drv_rtc_config_t config = NRF_DRV_RTC_DEFAULT_CONFIG;
	config.prescaler = 32; // fRTC [kHz] = 32.768 / (PRESCALER + 1 ) = 1kHz
	//config.prescaler = 4095; // fRTC [kHz] = 32.768 / (PRESCALER + 1 ) --> 8Hz (minimum because prescsaler is 12bit)
	err_code = nrf_drv_rtc_init(&rtc, &config, rtc_handler);
	APP_ERROR_CHECK(err_code);

	//Enable tick event & interrupt
	nrf_drv_rtc_tick_enable(&rtc,true);

	//Set compare channel 0 to trigger interrupt after COMPARE_COUNTERTIME seconds
	err_code = nrf_drv_rtc_cc_set(&rtc,0,COMPARE_COUNTERTIME * 1,true); //1kHz
	APP_ERROR_CHECK(err_code);

	//Set compare channel 1 to trigger interrupt after COMPARE_COUNTERTIME seconds
	err_code = nrf_drv_rtc_cc_set(&rtc,1,COMPARE_COUNTERTIME * 1000,true); //1Hz
	APP_ERROR_CHECK(err_code);

	//Power on RTC instance
	nrf_drv_rtc_enable(&rtc);
}

void timer_handler(nrf_timer_event_t event_type, void * p_context)
{

}

Main:

int main(void)
{
	/* Configure board. */
    bsp_board_init(BSP_INIT_LEDS);
	bsp_board_leds_on();
	bsp_board_init(BSP_INIT_BUTTONS);

    APP_ERROR_CHECK(NRF_LOG_INIT(NULL));
    NRF_LOG_DEFAULT_BACKENDS_INIT();

	lfclk_config();
	rtc_config();
		
    saadc_init();
	saadc_sampling_event_init();
    saadc_sampling_event_enable();
		
	while(1)
    {
        if(measDone == 1)
        {
        ...

Thank you all and best regards! 
Manuel

  • You need to update the buffer pointer and maxcount registers immediately after a STARTED event. Right now it's done after the END event. 
    By updating the buffer registers after the START task has been triggered you will overwrite the previous buffer as new buffers will not be used until the next START task. 

    See the SAADC's EasyDMA chapter.


    "I'm using a compare event of the RTC to trigger the sample task in scan mode. Another compare event is used to enable the ppi channel again after disabling it after 10 samples per channel. The reason for that is that I want to sample at a high frequency but only 10 samples per second."
    Can you try to rephrase your description a bit, I did not quite catch why you're disabling the PPI to control the sampling, usually this is done with the MAXCNT register.

  • Thank you for your answer. But I do not fully understand it. How can I catch this STARTED event for calling nrf_drv_saadc_buffer_convert() then. Is there some samplecode for that?

    I have a hard time by understanding this PPI stuff like

    err_code = nrf_drv_ppi_channel_assign(m_ppi_channel_2, nrf_saadc_event_address_get(NRF_SAADC_EVENT_END), nrf_saadc_task_address_get(NRF_SAADC_TASK_START));

    Well, MAXCNT is to set the maximum sample count for the buffer before firing the NRF_DRV_SAADC_EVT_DONE event, right?

    What I want to implement is 1 measurement per second. 10 measurements per channel are made at 1kHz. After the DONE event, I switch off the PPI channel so no more samples will be made. After 1 second, I switch on the PPI channel again for 10 measurements. Summarized I want to have one measurement per seccond but the measurements are a average out of 10 samples which were sampled in a high frequency.

    Is it clearer now?

    Thank you and best regards,
    Manuel

    #edit:

    When I activate the STARTED interrupt with nrf_saadc_int_enable(NRF_SAADC_INT_STARTED), I end up in a NRF_ERROR_INVALID_STATE error while calling APP_ERROR_CHECK(err_code) after enabling my PPI channel 1 which is responsible for triggering the sample task.

  • Manuel55 said:
    Thank you for your answer. But I do not fully understand it. How can I catch this STARTED event for calling nrf_drv_saadc_buffer_convert() then. Is there some samplecode for that?

     No, I'm afraid there's no sample code that does that for you. You will need to modify the saadc driver's interrupt handler to handle this scenario. 
     

    Manuel55 said:
    I have a hard time by understanding this PPI stuff like

    The PPI uses the registers of the HW peripherals to connect their tasks and events.
    It monitors a given event register, and in the event that it changes state from 0x00 to 0x01(indicating that the event has happened) it will write a 0x01 to a given task register, thereby triggering the given task. 

    f.ex, the nRF52832'a SAADC EVENTS_END register has an address offset of 0x104, with an address base of 0x40007000 for the SAADC peripheral the EVENTS_END register has the address of 0x40007104, who's is what nrf_saadc_event_address_get(NRF_SAADC_EVENT_END) will return. 

    Likewise nrf_saadc_task_address_get(NRF_SAADC_TASK_START) will return 0x40007000, as the address offset of the TASKS_START register is 0x000. 

    This coupling between an event register and a task register is then assigned to a PPI channel. 
    An event or task register can be connected to any number of PPI channels. 

    Manuel55 said:
    Well, MAXCNT is to set the maximum sample count for the buffer before firing the NRF_DRV_SAADC_EVT_DONE event, right?

    The MAXCNT sets the number of samples that the SAADC will sample before it will fire the EVENTS_END, at which point all the samples have been transferred from the SAADC into RAM.

    MAXCNT along with PTR must be configured before triggering the next START task, otherwise, they will not be used. This is the core of your issue, that the MAXCNT and PTR register are not updated before the START task is triggered, or the start task is triggered after the SAADC has begun its next sampling cycle. 
    In both cases, you'll overwrite your previous buffers, and in the second case, you will suddenly change the buffer mid-sample-cycle, thereby creating out of order samples. 


    Manuel55 said:
    What I want to implement is 1 measurement per second. 10 measurements per channel are made at 1kHz. After the DONE event, I switch off the PPI channel so no more samples will be made. After 1 second, I switch on the PPI channel again for 10 measurements. Summarized I want to have one measurement per seccond but the measurements are a average out of 10 samples which were sampled in a high frequency.

    I interpret this as one set of measurements per second, 10 samples per channel per set, at 1kHz sample rate, with averaging of the sets of 10 samples.

    You don't need to disable the PPI channel, but that might work as intended. I would rather stop and clear the TIMER on the SAADC's end event, and then trigger the start event in order to start your sampling sequence. 

    Other thoughts:
    If you only enable one SAADC channel at a time you can use oversampling to average over the samples automatically at 1kHz. That way you will not have to do any averaging in SW. 

    You can do oversampling on multiple channels, but then you must enable BURST mode on all channels. From Oversampling:
    "H[n].CONFIG.BURST can be enabled to avoid setting SAMPLE task 2OVERSAMPLE times. With BURST = 1 the ADC will sample the input 2OVERSAMPLE times as fast as it can (actual timing: <(tACQ+tCONV)×2OVERSAMPLE). Thus, for the user it will just appear like the conversion took a bit longer time, but other than that, it is similar to one-shot mode. Scan mode can be combined with BURST=1, if burst is enabled on all channels."

  • Thank you haakonsh.

    Unfotunately I was not able to implement this without swaps in the buffer. I switched now to your suggested approach. SCAN mode of multiple channels with oversampling (256x). Burst mode is activated on every channel.

    I need a high accuracy of the thermistor measurements. Therefore I wanted to add a periodical offset calibration. Because a change in temperature will be very likely in my application.

    Therefore I added the following to my code:

    SAADC interrupt routine:

    void saadc_callback(nrf_drv_saadc_evt_t const * p_event)
    {
    	int value = 0;
    	int i;
    	ret_code_t err_code;
    
        if (p_event->type == NRF_DRV_SAADC_EVT_DONE)
        {
    		nrf_gpio_pin_toggle(LED);
    		
    		if((m_adc_evt_counter % SAADC_CALIBRATION_INTERVAL) == 0)       //Evaluate if offset calibration should be performed. Configure the SAADC_CALIBRATION_INTERVAL constant to change the calibration frequency
            {
                nrf_drv_saadc_abort();                                      // Abort all ongoing conversions. Calibration cannot be run if SAADC is busy
                m_saadc_calibrate = true;                                   // Set flag to trigger calibration in main context when SAADC is stopped
            }
            
            NRF_LOG_INFO("ADC event number: %d", (int)m_adc_evt_counter);
    				
    		for (i = 0; i < ADC_CHANNEL_COUNT; i++)
    		{
    			NRF_LOG_INFO("%d", p_event->data.done.p_buffer[i]);
    			switch(i%4)
    			{
    				case 0: battery_voltage_sum = p_event->data.done.p_buffer[i];
    								break;
    				case 1: thermistor_1_sum = p_event->data.done.p_buffer[i];
    								break;
    				case 2: thermistor_2_sum = p_event->data.done.p_buffer[i];
    								break;	
    				case 3: voltage_sum = p_event->data.done.p_buffer[i];
    								break;	
    			}
    		}
    		
    		if(m_saadc_calibrate == false)
            {
                err_code = nrf_drv_saadc_buffer_convert(p_event->data.done.p_buffer, ADC_CHANNEL_COUNT); //Set buffer so the SAADC can write to it again. This is either "buffer 1" or "buffer 2"
    			APP_ERROR_CHECK(err_code);
    			
    			measDone = 1;
            }
    				
    		m_adc_evt_counter++;
    		
    		nrf_gpio_pin_clear(THERMISTOR_VREF);
        }
    		
    	else if (p_event->type == NRF_DRV_SAADC_EVT_CALIBRATEDONE)
        {
            err_code = nrf_drv_saadc_buffer_convert(m_buffer_pool[0], ADC_CHANNEL_COUNT);             //Set buffer so the SAADC can write to it again. 
            APP_ERROR_CHECK(err_code);
            err_code = nrf_drv_saadc_buffer_convert(m_buffer_pool[1], ADC_CHANNEL_COUNT);             //Need to setup both buffers, as they were both removed with the call to nrf_drv_saadc_abort before calibration.
            APP_ERROR_CHECK(err_code);
            
            NRF_LOG_INFO("SAADC calibration complete !");                                              //Print on UART        
        }
    }

    main:

    while(1)
    {	
    	if(m_saadc_calibrate == true)
    	{
    		NRF_LOG_INFO("SAADC calibration starting...");    //Print on UART
    		while(nrf_drv_saadc_calibrate_offset() != NRF_SUCCESS); //Trigger calibration task
    		m_saadc_calibrate = false;
    	}
    
    	if(measDone == 1)
    	{
    	    ...
    	}

    I think I did everythink like described in this link:
    https://github.com/NordicPlayground/nRF52-ADC-examples/tree/master/saadc_low_power

    But as soon as I perform the first calibration, the buffer data is rubbish. I can't even recognize a pattern of buffer swap or something like this. 

    Could you please help me with this behaviour?

    Thank you and best regards,
    Manuel

  • You're getting calibration data in your samples.

    After you've triggered the calibrate offset task with the call to nrf_drv_saadc_calibrate_offset() you need to wait until it's complete:

    While(!NRF_SAADC->EVENTS_CALIBRATEDONE);

Related