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

SAADC Calibration Lock Up

I am trying to implement a periodic calibration routine that is transparent to upper layers of my application.

I have the SAADC set up to continuously sample one channel (oversample X4) and I am dual buffering into two 512-byte buffers. I call calibration every 5 seconds using a Ticker provided by mbed.

My calibration routine, executed in the interrupt context, used to look like this:

void calibrate_saadc(void) { 

nrf_drv_saadc_abort();
while(nrf_drv_saadc_calibrate_offset() != NRF_SUCCESS);

}

What I found was that it would hang on the while() loop since the ADC would stay busy... not sure why.

So then I tried changing it to trigger calibration in the SAADC driver callback (which should get executed when nrf_drv_saadc_abort() is called, it will report a smaller buffer than expected). If the buffer was smaller than expected (meaning it was aborted for calibration), I then execute the while(nrf_drv_saadc_calibrate_offset() != NRF_SUCCESS) line...

However, this line never ends up getting called and my application just halts since there are no more SAADC events to drive execution.

Could someone explain the events the SAADC driver/HAL should see for calibration to be carried out properly?

EDIT: I have viewed the examples in the github repository found here: https://github.com/NordicPlayground/nRF52-ADC-examples/blob/master/saadc_low_power/main.c

They flag the calibration event and execute it in the thread context. I am writing a low level driver that doesn't currently have a convenient way to defer processing to a thread with the current structure. How is the nrf_drv_saadc_calibrate_offset() function supposed to be used?

Parents
  • What priority is the Ticker running at? If it runs at a higher or equal priority to the SAADC, the SAADC IRQ handler will not be able to run to pull the driver out of BUSY state.

    Also make sure you do not init the buffers again in the SAADC handler after a abort call. This will put the SAADC driver back in BUSY state. You can set a flag in the ticker handler when you call abort and check this flag before setting up buffers.

  • I checked the priority, it appears mbed instantiates Timer/Tickers with the lowest interrupt priority of 7. I changed the interrupt priority of the SAADC IRQ handler to 6 and then to 1 and still it gets stuck when calibrating.

    Here is my driver's interrupt handler:

    static void microphone_in_irq_handler(nrf_drv_saadc_evt_t const* event)
    {
    	// Get the microphonein_t instance
    	microphonein_t* instance = (microphonein_t*) m_instance;
    
    	/*
    	 * For some reason, Nordic FW engineers called this EVT_DONE
    	 * This is triggered when a sample buffer is filled
    	 * While Nordic HW engineers called this EVT_END while EVT_DONE
    	 * means only one conversion is done...
    	 */
    	if (event->type == NRF_DRV_SAADC_EVT_DONE)
    	{
    		/*
    		 * If the SAADC is aborted in the middle of a sample buffer, it will
    		 * still signal this handler. So we need to check to make sure the whole
    		 * buffer has been filled
    		 */
    		if(event->data.done.size == instance->buffer_len || instance->calibrating)
    		{
    			/*
    			 * Normal operation - buffer has been completely filled
    			 *
    			 * When a buffer is filled, we queue up that same buffer to the SAADC EasyDMA
    			 * Since the buffer address is double-buffered, this will take effect
    			 * after the next buffer is full
    			 *
    			 */
    			nrf_drv_saadc_buffer_convert(event->data.done.p_buffer,
    					instance->buffer_len);
    
    			// Notify the application
    			if(m_irq_handler)
    				m_irq_handler(m_id, (uint16_t*) event->data.done.p_buffer);
    
    		}
    		else
    		{
    			if(instance->running)
    			{
    				if(instance->calibrating)
    				{
    					/*
    					 * Sampling was aborted due to calibration,
    					 * retain samples that were already done and restart
    					 * sampling after calibration where we left off.
    					 *
    					 * This will result in a <1ms gap in samples.
    					 */
    					instance->current_buffer = (uint16_t*) event->data.done.p_buffer;
    					instance->offset = event->data.done.size;
    				}
    				else
    				{
    					/*
    					 * Buffer size was shorter due to restarting
    					 * a previously-aborted sampling. We must make sure
    					 * to report the appropriate buffer to the application
    					 * in this case
    					 */
    
    					// Queue up the full size buffer now from the beginning
    					nrf_drv_saadc_buffer_convert(
    							(nrf_saadc_value_t*) instance->current_buffer,
    							instance->buffer_len);
    
    					// Notify the application
    					if(m_irq_handler)
    						m_irq_handler(m_id, instance->current_buffer);
    				}
    			}
    
    			// In this case, sampling was aborted by calling microphonein_stop()
    		}
    
    	}
    	else if(event->type == NRF_DRV_SAADC_EVT_CALIBRATEDONE)
    	{
    		instance->calibrating = 0;
    
    		// Calibration requires ongoing sampling to be aborted
    		// So we must restart from where we left off and queue up the other buffer
    		nrf_drv_saadc_buffer_convert(
    				(nrf_saadc_value_t*) (instance->current_buffer + instance->offset),
    				(instance->buffer_len - instance->offset));
    
    		// Get the appropriate next buffer
    		nrf_saadc_value_t* next_buffer = (nrf_saadc_value_t*) instance->buffer_1;
    		if(instance->current_buffer == instance->buffer_2)
    			next_buffer = (nrf_saadc_value_t*) instance->buffer_2;
    
    		nrf_drv_saadc_buffer_convert((nrf_saadc_value_t*) next_buffer,
    				instance->buffer_len);
    
    		// Trigger sampling
    		nrf_drv_saadc_sample();
    	}
    }

    And here is the calibration routine called every 5 seconds by the mbed Ticker driver (operating at interrupt priority 7):

    void microphonein_calibrate(microphonein_t* obj)
    {
    	/*
    	 * See:
    	 * https://github.com/NordicPlayground/nRF52-ADC-examples/tree/b4fbd256c886e07a515a8570a2f3d8c01cc1e913/saadc_low_power#about-this-project
    	 * For more information about calibration
    	 *
    	 * "In order to trigger the offset calibration task,
    	 * the SAADC driver needs to be in IDLE mode. This is
    	 * achieved by calling the abort task, which will abort
    	 * all ongoing conversions. A flag is set to trigger the
    	 * offset calibration task in the main context, when the
    	 * driver have entered IDLE state. When calibration is
    	 * done, the SAADC throws a "calibration done" event.
    	 * When calibration is done, both buffers need to be setup
    	 * for conversion again to keep double-buffering, since they
    	 * were removed by the abort task. The table below shows how
    	 * long it typically takes to calibrate the SAADC for different
    	 * acquisition time settings:"
    	 *
    	 * |----------|----------------|-------------------------------|
    	 * | Acq time | Length of Cal 0| Start cal until SAADC ready	|
    	 * |----------|----------------|-------------------------------|
    	 *	|  3 us    |     102 us		 |            118 us					|
    	 *	|  10 us	  | 	  279 us 	 | 			  314 us					|
    	 *	|  40 us	  | 	  988 us 	 | 			  1152 us				|
    	 * |----------|----------------|-------------------------------|
    	 */
    	obj->calibrating = 1;
    	nrf_drv_saadc_abort();
    	
    	/** Execution hangs here - the driver status never goes to IDLE */
    	while(nrf_drv_saadc_calibrate_offset() != NRF_SUCCESS);
    }

    I have static structures for storing driver state information (calibrating, running, etc).

  • It appears the nrf_drv_saadc_abort() function times out after attempting to trigger NRF_SAADC_TASK_STOP:

    void nrf_drv_saadc_abort(void)
    {
        if (nrf_drv_saadc_is_busy())
        {
            nrf_saadc_event_clear(NRF_SAADC_EVENT_STOPPED);
            nrf_saadc_task_trigger(NRF_SAADC_TASK_STOP);
    
            if (m_cb.adc_state == NRF_SAADC_STATE_CALIBRATION)
            {
                m_cb.adc_state = NRF_SAADC_STATE_IDLE;
            }
            else
            {
                // Wait for ADC being stopped.
                uint32_t timeout = HW_TIMEOUT;
    
                while ((m_cb.adc_state != NRF_SAADC_STATE_IDLE) && (timeout > 0))
                {
                    --timeout;
                }
                
                // ERROR - THIS IS TIMING OUT FOR SOME REASON!
                
                ASSERT(timeout > 0);
            }
    
            m_cb.p_buffer           = 0;
            m_cb.p_secondary_buffer = 0;
            NRF_LOG_INFO("Conversion aborted.");
        }
    }

    EDIT:

    The SAADC driver IRQ handler relies on the NRF_SAADC_EVENT_STOPPED interrupt happening to complete the nrf_drv_saadc_abort() process (setting the internal state back to IDLE).

    However, it appears that the driver never actually enables this interrupt (I checked the SAADC INTEN register, 0x40007300 and it was 0x00000002).

    So the driver is relying on an interrupt that it never enabled to complete processing the abort request. This seems like a bug.

    I'm currently working with nRF SDK version 14.2 (the version mbed is on).

    EDIT AGAIN:

    Considering the event flow, it seems that my above assumption that the driver needs to enable the STOPPED_EVENT interrupt is incorrect. The interrupt should be executed when the STOPPED event also causes the SAADC END event.

    Any suggestions for how to properly trigger and resume from calibration with the SAADC using interrupts?

Reply
  • It appears the nrf_drv_saadc_abort() function times out after attempting to trigger NRF_SAADC_TASK_STOP:

    void nrf_drv_saadc_abort(void)
    {
        if (nrf_drv_saadc_is_busy())
        {
            nrf_saadc_event_clear(NRF_SAADC_EVENT_STOPPED);
            nrf_saadc_task_trigger(NRF_SAADC_TASK_STOP);
    
            if (m_cb.adc_state == NRF_SAADC_STATE_CALIBRATION)
            {
                m_cb.adc_state = NRF_SAADC_STATE_IDLE;
            }
            else
            {
                // Wait for ADC being stopped.
                uint32_t timeout = HW_TIMEOUT;
    
                while ((m_cb.adc_state != NRF_SAADC_STATE_IDLE) && (timeout > 0))
                {
                    --timeout;
                }
                
                // ERROR - THIS IS TIMING OUT FOR SOME REASON!
                
                ASSERT(timeout > 0);
            }
    
            m_cb.p_buffer           = 0;
            m_cb.p_secondary_buffer = 0;
            NRF_LOG_INFO("Conversion aborted.");
        }
    }

    EDIT:

    The SAADC driver IRQ handler relies on the NRF_SAADC_EVENT_STOPPED interrupt happening to complete the nrf_drv_saadc_abort() process (setting the internal state back to IDLE).

    However, it appears that the driver never actually enables this interrupt (I checked the SAADC INTEN register, 0x40007300 and it was 0x00000002).

    So the driver is relying on an interrupt that it never enabled to complete processing the abort request. This seems like a bug.

    I'm currently working with nRF SDK version 14.2 (the version mbed is on).

    EDIT AGAIN:

    Considering the event flow, it seems that my above assumption that the driver needs to enable the STOPPED_EVENT interrupt is incorrect. The interrupt should be executed when the STOPPED event also causes the SAADC END event.

    Any suggestions for how to properly trigger and resume from calibration with the SAADC using interrupts?

Children
No Data
Related