Addressing SAM D21 ADC

I am trying to squeeze more precision out of the ADC on my Seeeduino LoRaWAN board which is based on the Arduino Zero but I'm having trouble with the parameters. I have downloaded ATMEL's AT07627: ASF Manual (SAM D21) and have looked at examples of how to address the ADC but seem to be missing something in the process and am having trouble understanding which parameters to set.

For background, I want to read a sensor every 15 minutes, so speed of the readings isn't an issue. For development, I'm using a capacitance soil moisture sensor, but my main issue is in reading a dendrometer, which needs more precision. For the dendrometer, accuracy isn't as much of an issue as precision, as long as the accuracy doesn't drift during the day.

Here is my test code. I get a compiler error 'struct Adc' has no member named 'adc_resolution' on the line addressing the ADC (and have the same error with attempts to set other parameters). I get the same error if I try to set the board to a Zero instead of Seeeduino LoRaWAN.

/*Code to read a Teros 10 capacitance soil moisture sensor, output reading to Serial
 * All LoRaWAN code removed for debugging
 * Written for Seeeduino so the serial port to the computer is SerialUSB
 * The current requirement of the sensor is 12 mA at 3VDC so can power from digital pin
 */

// millis timer global------------------------------------------------------------------

#include <millisDelay.h>	  //handy timing library
                            //https://www.forward.com.au/pfod/ArduinoProgramming/TimingDelaysInArduino.html
  millisDelay SensorDelay;  // the delay object

  // battery of Seeeduino LoRaWAN
  const short pin_battery_status  = A5;
  const short pin_battery_voltage = A4;   //note that this will read the USB voltage if plugged into computer
  boolean arStatus;               //not very useful
  int arVoltage;
  float bvoltage;                 

	const int Apin = A3;            //analog pin connected to sensor +
	const int ReadingTime = 20;     //time to read from Apin in ms 10 is minimum  
  int SensorBits = 0;             //variable to store sensor reading from 12 bit A/D converter
	double SensorMV = 0;		        //store sensor reading converted to mV	

// Setup readings global-----------------------------------------------------------

	const unsigned long DelayTime = 1000*60; 	// have const here so it is easy to find and change (in mS)

// end global----------------------------------------------------------------------

void setup() {
	// put your setup code here, to run once:

  pinMode(Apin, INPUT);  
  pinMode(LED_BUILTIN, OUTPUT);   	//During testing have the LED on when taking a reading
  SerialUSB.begin(19200);
  while (!SerialUSB) { 
    ;   // wait for serial port to connect
  }
  //analogReadResolution(12);         //will do this through direct addressing
	//analogReference(AR_INTERNAL);    	//2.23V SAMD Boards e.g. Zero/Seeeduino only has error

  //Read sensor once as throwaway value but print it out
  SerialUSB.println("Read in setup");
	digitalWrite(LED_BUILTIN, HIGH);
  delay(10);
	SensorBits = analogRead(Apin);
	digitalWrite(LED_BUILTIN, LOW);
	SerialUSB.print("SensorBits = ");
	SerialUSB.println(SensorBits);
	SensorMV = SensorBits/4096.0*3.3;	//Not sure if this should be 4095. Assumes 3.3 mV analog reference
	SerialUSB.print("SensorMV = ");
	SerialUSB.println(SensorMV);

  //Read sensor again after setting ADC for testing as throwaway value but print it out
  SerialUSB.println("Set ADC using direct addressing");
  configure_adc();
    
  digitalWrite(LED_BUILTIN, HIGH);
  delay(10);
  SensorBits = analogRead(Apin);
  digitalWrite(LED_BUILTIN, LOW);
  SerialUSB.print("SensorBits = ");
  SerialUSB.println(SensorBits);
  SensorMV = SensorBits/4096.0*3.3; //Not sure if this should be 4095. Assumes 3.3 mV analog reference
  SerialUSB.print("SensorMV = ");
  SerialUSB.println(SensorMV);

	SensorDelay.start(DelayTime);     //Start millis delay timer for sensor reading intervals

}     //end setup

void loop() {
	// put your main code here, to run repeatedly:
	if (SensorDelay.justFinished()) {		// repeat delay and take reading
		SensorDelay.repeat(); // repeat
		SerialUSB.print("millis() = ");
    SerialUSB.println(millis());
		ReadTeros10();
//    SendPayload();
	}
}     //end loop

void configure_adc(void){
  /* This routine uses commands that send instructions directly to the SAM-D21 chip
   *  so it is not portable to other systems
   *  they are taken from the AT07627: ASF Manual (SAM D21)
   *  not sure which commands are needed so trial and error
   */
    
  ADC->adc_resolution = ADC_RESOLUTION_16BIT;
} //end configure_adc

void ReadTeros10() {
	digitalWrite(LED_BUILTIN, HIGH);
  delay(10);
	SensorBits = analogRead(Apin);
	digitalWrite(LED_BUILTIN, LOW);
	SerialUSB.print("SensorBits = ");
	SerialUSB.println(SensorBits);
	SensorMV = SensorBits/4096.0*3.3;	//Not sure if this should be 4095. Assumes 3.3 mV analog reference
	SerialUSB.print("SensorMV = ");
	SerialUSB.println(SensorMV);
} //end ReadTeros10

Hi haresfur,

To increase the ADC's resolution to 16-bits it's necessary to oversample by accumulating 256 samples (4*n samples, where n = 4 extra bits) and decimation with 4 automatic shifts to the right. This requires the SAMPLECTRL and ADJRES bitfields in the ADC's AVGCTRL register to be set to ADC_AVGCTRL_SAMPLENUM_256 and 0 respectively:

ADC->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_256 | ADC_AVGCTRL_ADJRES(0);

This is in addition to setting the resolution to 16-bits in the CTRLB register, to activate averaging mode:

ADC->CTRLB.reg |= ADC_CTRLB_RESSEL_16BIT;         // Set ADC resolution to 16 bits

It might also be beneficial to speed up the sampling time by setting the SAMPLECTRL register to 0 as well, (but does depend on the source resistance seen by your analog input pin: Arduino Zero ADC Sample Time Too Long · Issue #327 · arduino/ArduinoCore-samd · GitHub):

ADC->SAMPCTRL.reg = 0x00;

Thank you. I could get your statements to compile so obviously I'm missing something about how to send commands to the ADC. Can you point me to where to find information on how to do this or where the values are defined? Clearly they aren't the same as what is in the ASF manual.

Hi haresfur,

The ASF (Advanced Software Framework) is a hardware abstraction layer for Microchip's (formally Atmel's) microcontrollers. However, it's possible to alternatively access the microcontroller's registers directly using the CMSIS (Cortex Microcontroller Software Interface Standard) definitions and use them in conjuction with the SAMD21's datasheet. Whether to use ASF or direct register manipulation really a matter of to personal preference.

The register definintions for the SAMD21 are stored (on my Windows machine at least) at the following location:

C:\Users\Computer\AppData\Local\Arduino15\packages\arduino\tools\CMSIS-Atmel\1.2.0\CMSIS\Device\ATMEL\samd21\include...

In this diectory there are three further sub-folders: "component", "instance" and "pio".

The "component" directory has set of header files (for example "adc.h") that contain the register structures and bitfield definitions for each peripheral. Using these structures and definitions it's possible to access either the entire register:

ADC->CTRLA.reg |= ADC_CTRLA_ENABLE;      // Enable the ADC

or the registers bits/bitfield indvidually:

ADC->CTRLA.bit.ENABLE = 0x01;      // Enable the ADC

The "instance" directory contains the register definitions for each peripheral instance, for example SERCOM0, SERCOM1, etc... These provide an alternative, but less commonly used notation:

REG_ADC_CTRLA |= ADC_CTRLA_ENABLE;      // Enable the ADC

Th "pio" directory provides the GPIO definitions for each variant of microcontroller.

I normally just use a editor such as Notepad++, to browse and cut 'n' paste the register definitions into an Arduino sketch.

Here's an example of using register definitions, in order to initialise the ADC and adapt it to my specific requirements:

ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV512 |    // Divide Clock ADC GCLK by 512 (48MHz/512 = 93.7kHz)
                 ADC_CTRLB_RESSEL_12BIT;         // Set ADC resolution to 12 bits
while(ADC->STATUS.bit.SYNCBUSY);                 // Wait for synchronization
ADC->SAMPCTRL.reg = 0x00;                        // Set max Sampling Time Length to half divided ADC clock pulse (5.33us)
ADC->INPUTCTRL.bit.MUXPOS = 0x0;                 // Set the analog input to A0
while(ADC->STATUS.bit.SYNCBUSY);                 // Wait for synchronization
ADC->CTRLA.bit.ENABLE = 0x01;                    // Enable the ADC
while(ADC->STATUS.bit.SYNCBUSY);                 // Wait for synchronization

I don’t believe that the arduino core normally uses ASF for anything, so trying to use SOME asf functions might not be practical. Refer to the datasheet instead, and manipulate the ADC registers directly. (This isn’t even much harder than using asf (which should make the asf authors ashamed!)

Thank you for the replies. I could use some clarification, though.

Do I need to do the analog input pin mapping and ADC reading through the registers or can I use the normal Arduino pinMode and analogRead commands?

I get strange results trying to read the sensor inputs with analogRead. The readings don't change if I set the resolution to 10, 12, or 16 bit eg:

ADC->CTRLB.reg |= ADC_CTRLB_RESSEL_16BIT;

but is different with:

ADC->CTRLB.reg |= ADC_CTRLB_RESSEL_8BIT;

Maybe I'm missing something but does the datasheet give the default values of the registers after a reset? I'm having trouble figuring out which ones I need to specify and what I can leave as-is.

Thanks.

Hi haresfur,

I've just tested the ADC with 16-bit resolution on my SAMD21 and it's working fine, returning values higher than 12-bit resolution, in other words higher than 4095:

8437
8434
8441
8434
8440

Have you also added the line?:

ADC->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_256 | ADC_AVGCTRL_ADJRES(0);

To get 16-bit resolution it's necessary to accumulate 256 samples without result adjust (ADJRES). The ADC generates a 20-bit resolution output that's automatically shifted to the right 4 times, to prevent overflow of its 16-bit RESULT register.

Hi haresfur,

Looking at the Arduino SAMD21 core code, it appears as though the analog resolution is intrinsic to the analogRead() function.

Here's an example that reads the analog input A0 with 16-bit resolution without analogRead():

// Set-up the ADC to read analog input on A0 with 16-bit resolution
void setup() {
  Serial.begin(115200);                            // Set-up the native serial port at 115200 bit/s
  while(!SerialUSB);
  ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV512 |    // Divide Clock ADC GCLK by 512 (48MHz/512 = 93.7kHz)
                   ADC_CTRLB_RESSEL_16BIT;         // Set ADC resolution to 16 bits
  while(ADC->STATUS.bit.SYNCBUSY);                 // Wait for synchronization
  ADC->SAMPCTRL.reg = 0x00;                        // Set max Sampling Time Length to half divided ADC clock pulse (5.33us)
  ADC->INPUTCTRL.bit.MUXPOS = 0x0;                 // Set the analog input to A0
  while(ADC->STATUS.bit.SYNCBUSY);                 // Wait for synchronization
  ADC->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_256 |   // Accumulate 256 samples for 16-bit resolution
                     ADC_AVGCTRL_ADJRES(0);        // Zero result adjust
  ADC->CTRLA.bit.ENABLE = 0x01;                    // Enable the ADC
  while(ADC->STATUS.bit.SYNCBUSY);                 // Wait for synchronization
}

void loop() {
  ADC->SWTRIG.bit.START = 1;                       // Initiate a software trigger to start an ADC conversion
  while(ADC->STATUS.bit.SYNCBUSY);                 // Wait for write synchronization
  while(!ADC->INTFLAG.bit.RESRDY);                 // Wait for the conversion to complete
  ADC->INTFLAG.bit.RESRDY = 1;                     // Clear the result ready (RESRDY) interrupt flag
  while(ADC->STATUS.bit.SYNCBUSY);                 // Wait for read synchronization
  int result = ADC->RESULT.reg;                    // Read the ADC result
  SerialUSB.println(result);                       // Output the result
  delay(500);                                      // Wait for 500ms
}
2 Likes

Thank you so much. MartinL, your code runs and appears to be giving 16 bit resolution. I'm not sure I will ever figure out the datasheet, but will plug away. I need to play with it to read multiple pins.