[SAML21J18B] How to keep ADC results in correct sequence?

Go To Last Post
2 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Hello all,

 

I am working with the built-in ADC on the SAM L21, and I am having trouble keeping the read values in the correct sequence. I am using START, but I have tried it without, and am still having issues. With the code supplied below, I'm only getting about 1 misplaced value per second, but it seems like any additional code throws the timing off, and the errors just cascade.

 

Currently, in the ADC complete callback, I am storing the value of ADC -> SEQSTATUS.reg in an extern variable current_pwm_channel, and then the main loop then uses this variable to select which variable to place the current ADC value in, but I'm not sure if that is the best way, considering the issues I'm having.

 

I have also tried not using the START-supplied functions for the ADC, in case they were taking too long to be called, and wrote my own, but the issues still persisted. Any programming advice or tricks would be greatly appreciated. The code is in no way a final form, just basic code I've been using to test the ADC.

 

The ADC is attached to a voltage divider, where the first value should be ~766mV, and the second value should be ~378mV.

 

main.C

#include <atmel_start.h>
#include <stdio.h>
#include "user_hal.h"

#define VALUE_0 0x80
#define VALUE_1 0x01


uint8_t adc0_value[2];

uint16_t output_voltage;
uint16_t output_voltage2;
int16_t error_voltage;
uint16_t current_pwm_channel = 0;
uint32_t error_count;


int main(void)
{
	/* Initializes MCU, drivers and middleware */

	atmel_start_init();
	spi_m_sync_enable(&SPI_INSTANCE);
	gfx_mono_init();
	pwm_set_parameters(&PWM_0, 480, 240);
	pwm_enable(&PWM_0);
	delay_ms(5);
	WDT_init();
	timer0_init();
	ADC_init();
	delay_ms(100); //gives the internal reference time to stabilize
	adc_async_start_conversion(&ADC_0);
	
	
	uint32_t cycle_count = 0;
	
	while (1) {
			if (adc_async_read_channel(&ADC_0, 0, adc0_value, 2))
			{
				if(current_pwm_channel == VALUE_0) {
					output_voltage = adc0_value[0] | adc0_value[1] << 8;
					output_voltage = (output_voltage*1000)/4096; //converts ADC value to mV
				}
				else if (current_pwm_channel == VALUE_1)
				{
					output_voltage2 = adc0_value[0] | adc0_value[1] << 8;
					output_voltage2 = (output_voltage2*1000)/4096;
					adc_async_start_conversion(&ADC_0);   //converts ADC value to mV
				}
				
				if (output_voltage*1000/output_voltage2 <= 1000)
				{
					error_count++;
				}
				
			}
			
			

	}
}

user_hal.c

#include <atmel_start.h>
#include "user_hal.h"
#include <stdio.h>


extern uint16_t output_voltage;
extern uint16_t output_voltage2;
extern uint16_t current_pwm_channel;
uint32_t conversion_count;
extern uint32_t error_count;



static struct timer_task TIMER_0_screen_task;

static void ADC_cb(const struct adc_async_descriptor *const descr, const uint8_t channel)
{
	//gpio_toggle_pin_level(DGI_GPIO3);
	current_pwm_channel = ADC->SEQSTATUS.reg;	//used to figure out which channel was just read
	conversion_count++;
	
}

static void ADC_error_cb(const struct adc_async_descriptor *const descr, const uint8_t channel)
{
	//__asm("BKPT #0");
}

void print_integer(uint32_t i, int x, int y) {
	char int_string[0];
	sprintf(int_string, "%04ld", i);
	gfx_mono_text_draw_string(&MONOCHROME_TEXT_0_desc, (uint8_t*)int_string, x, y, &basic_6x7);

}

static void TIMER_0_screen_cb(const struct timer_task *const timer_task)
{
	gpio_set_pin_level(DGI_GPIO2, true);
	print_integer(output_voltage, 5, 5);
	print_integer(output_voltage2, 40, 5);
	print_integer(output_voltage*1000/output_voltage2, 80, 5);
	print_integer(error_count, 5, 20);
	gpio_set_pin_level(DGI_GPIO2, false);
	error_count = 0;
	conversion_count = 0;					//used to count conversions per second
	wdt_feed(&WDT_0);
	
}

void ADC_init() {
	adc_async_register_callback(&ADC_0, 0, ADC_ASYNC_CONVERT_CB, ADC_cb);
	adc_async_register_callback(&ADC_0, 0, ADC_ASYNC_ERROR_CB, ADC_error_cb);
	adc_async_enable_channel(&ADC_0, 0);

	
}

void timer0_init() {
	TIMER_0_screen_task.interval = 100;
	TIMER_0_screen_task.cb       = TIMER_0_screen_cb;
	TIMER_0_screen_task.mode     = TIMER_TASK_REPEAT;

	timer_add_task(&TIMER_0, &TIMER_0_screen_task);
	timer_start(&TIMER_0);
}

void WDT_init() {
		uint32_t clk_rate;
		uint16_t timeout_period;

		clk_rate       = 1000;
		timeout_period = 4096;
		wdt_set_timeout_period(&WDT_0, clk_rate, timeout_period);
		wdt_enable(&WDT_0);
}

 

Attachment(s): 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The problem is both the main application and the interrupts are accessing non-atomic data at the same time.  When the interrupt occurs at the wrong time it changes the value of current_pwm_channel without main realizing it.  The ADC value is no longer in sync.  Or worse, the interrupt occurs at exactly the wrong time when the application is reading the prior value, corrupting the result (half old result, half new result, or even garbage results)

 

There are a few solutions:

1.  Disable interrupts.  Do this for a very short period of time, copy the values of interest, reenable interrupts, then check the copied values

2.  Use a semaphore/mutex to control access to the shared data.  Google it.  And yes it can work without an operating system using an atomic variable type like "int"

3.  Use a ring buffer where the interrupt puts data into the buffer and the main application pulls data out.  Be sure to protect the common ring buffer data structures with disabled interrupts