speeding up analogread() at the Arduino Zero

Hello,

i use the Zero and was looking for faster Analog reading times. I found articles for the Due (link), but nothing really for the Zero. Since i'm not verry µC experienced, it would take me a lot time to do the transfer.

But, with google i found a thread in a japanese forum, and by including their code, i come from 436µs to 21µs analog reading time. Maybe this is some use for someone:

void AdcBooster()
{
  ADC->CTRLA.bit.ENABLE = 0;                     // Disable ADC
  while( ADC->STATUS.bit.SYNCBUSY == 1 );        // Wait for synchronization
  ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV64 |   // Divide Clock by 64.
                   ADC_CTRLB_RESSEL_12BIT;       // Result on 12 bits
  ADC->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_1 |   // 1 sample
                     ADC_AVGCTRL_ADJRES(0x00ul); // Adjusting result by 0
  ADC->SAMPCTRL.reg = 0x00;                      // Sampling Time Length = 0
  ADC->CTRLA.bit.ENABLE = 1;                     // Enable ADC
  while( ADC->STATUS.bit.SYNCBUSY == 1 );        // Wait for synchronization
} // AdcBooster


int analogPin = 1;

unsigned long start_times[100];
unsigned long stop_times[100];
unsigned long values[100];


void setup() {
   AdcBooster();                              //uncomment for reading time without 'boost' 
  
  Serial.begin(9600);
  pinMode(analogPin, INPUT);
}

void loop() {
    analogReadResolution(12);                 //analog resolution to 12bit 
    unsigned int i;

    for(i=0;i<100;i++) {
    start_times[i] = micros();
    values[i] = analogRead(analogPin);
    stop_times[i] = micros();
    }

    // print out the results
    Serial.println("\n\n--- Results ---"); 
    for(i=0;i<100;i++) {
    Serial.print(values[i]);
    Serial.print(" elapse = ");
    Serial.print(stop_times[i] - start_times[i]);
    Serial.print(" us\n");
    }
    
    }

The datasheet of the Atmel SAM D21E mentions readingspeed of up to 350ks, so there might still be room for some more optimisations to reach the ~3µs, like making it run in Free Running mode?

I found some more code that could be used as an example here.

1 Like

Hi,

You can use the ADC with DMA to fill a buffer with ADC samples. With this method, it takes around 2us to measure one sample. Depending of you needs, this could be useful.

Check this thread for more information : ZERO's ADC with DMA - Arduino Zero - Arduino Forum

// adcdma
//  analog A1
//   could use DAC to provide input voltage   A0
//   http://www.atmel.com/Images/Atmel-42258-ASF-Manual-SAM-D21_AP-Note_AT07627.pdf pg 73

#define ADCPIN A1
#define HWORDS 1024
uint16_t adcbuf[HWORDS];     

typedef struct {
    uint16_t btctrl;
    uint16_t btcnt;
    uint32_t srcaddr;
    uint32_t dstaddr;
    uint32_t descaddr;
} dmacdescriptor ;
volatile dmacdescriptor wrb[12] __attribute__ ((aligned (16)));
dmacdescriptor descriptor_section[12] __attribute__ ((aligned (16)));
dmacdescriptor descriptor __attribute__ ((aligned (16)));


static uint32_t chnl = 0;  // DMA channel
volatile uint32_t dmadone;

void DMAC_Handler() {
    // interrupts DMAC_CHINTENCLR_TERR DMAC_CHINTENCLR_TCMPL DMAC_CHINTENCLR_SUSP
    uint8_t active_channel;

    // disable irqs ?
    __disable_irq();
    active_channel =  DMAC->INTPEND.reg & DMAC_INTPEND_ID_Msk; // get channel number
    DMAC->CHID.reg = DMAC_CHID_ID(active_channel);
    dmadone = DMAC->CHINTFLAG.reg;
    DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_TCMPL; // clear
    DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_TERR;
    DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_SUSP;
    __enable_irq();
}


void dma_init() {
    // probably on by default
    PM->AHBMASK.reg |= PM_AHBMASK_DMAC ;
    PM->APBBMASK.reg |= PM_APBBMASK_DMAC ;
    NVIC_EnableIRQ( DMAC_IRQn ) ;

    DMAC->BASEADDR.reg = (uint32_t)descriptor_section;
    DMAC->WRBADDR.reg = (uint32_t)wrb;
    DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf);
}

void adc_dma(void *rxdata,  size_t hwords) {
    uint32_t temp_CHCTRLB_reg;

    DMAC->CHID.reg = DMAC_CHID_ID(chnl);
    DMAC->CHCTRLA.reg &= ~DMAC_CHCTRLA_ENABLE;
    DMAC->CHCTRLA.reg = DMAC_CHCTRLA_SWRST;
    DMAC->SWTRIGCTRL.reg &= (uint32_t)(~(1 << chnl));
    temp_CHCTRLB_reg = DMAC_CHCTRLB_LVL(0) |
      DMAC_CHCTRLB_TRIGSRC(ADC_DMAC_ID_RESRDY) | DMAC_CHCTRLB_TRIGACT_BEAT;
    DMAC->CHCTRLB.reg = temp_CHCTRLB_reg;
    DMAC->CHINTENSET.reg = DMAC_CHINTENSET_MASK ; // enable all 3 interrupts
    dmadone = 0;
    descriptor.descaddr = 0;
    descriptor.srcaddr = (uint32_t) &ADC->RESULT.reg;
    descriptor.btcnt =  hwords;
    descriptor.dstaddr = (uint32_t)rxdata + hwords*2;   // end address
    descriptor.btctrl =  DMAC_BTCTRL_BEATSIZE_HWORD | DMAC_BTCTRL_DSTINC | DMAC_BTCTRL_VALID;
    memcpy(&descriptor_section[chnl],&descriptor, sizeof(dmacdescriptor));

    // start channel
    DMAC->CHID.reg = DMAC_CHID_ID(chnl);
    DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;
}

static __inline__ void ADCsync() __attribute__((always_inline, unused));
static void   ADCsync() {
  while (ADC->STATUS.bit.SYNCBUSY == 1); //Just wait till the ADC is free
}


void adc_init(){
  analogRead(ADCPIN);  // do some pin init  pinPeripheral()
  ADC->CTRLA.bit.ENABLE = 0x00;             // Disable ADC
  ADCsync();
  //ADC->REFCTRL.bit.REFSEL = ADC_REFCTRL_REFSEL_INTVCC0_Val; //  2.2297 V Supply VDDANA
  //ADC->INPUTCTRL.bit.GAIN = ADC_INPUTCTRL_GAIN_1X_Val;      // Gain select as 1X
  ADC->INPUTCTRL.bit.GAIN = ADC_INPUTCTRL_GAIN_DIV2_Val;  // default
  ADC->REFCTRL.bit.REFSEL = ADC_REFCTRL_REFSEL_INTVCC1_Val;
  ADCsync();    //  ref 31.6.16
  ADC->INPUTCTRL.bit.MUXPOS = g_APinDescription[ADCPIN].ulADCChannelNumber;
  ADCsync();
  ADC->AVGCTRL.reg = 0x00 ;       //no averaging
  ADC->SAMPCTRL.reg = 0x00;  ; //sample length in 1/2 CLK_ADC cycles
  ADCsync();
  ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV16 | ADC_CTRLB_FREERUN | ADC_CTRLB_RESSEL_10BIT;
  ADCsync();
  ADC->CTRLA.bit.ENABLE = 0x01;
  ADCsync();
}


void setup(){
	Serial.begin(9600);
	analogWriteResolution(10);
	analogWrite(A0,64);   // test with DAC 
	adc_init();
	dma_init();
}

void loop() {
	uint32_t t;

	t = micros();
	adc_dma(adcbuf,HWORDS);
	while(!dmadone);  // await DMA done isr
	t = micros() - t;
	Serial.print(t);  Serial.print(" us   ");
	Serial.println(adcbuf[0]);
	delay(2000);
}
1 Like

I have made a function analogReadFast() in the Albert library.

GitHub: GitHub - avandalen/Albert-Arduino-library

The analogReadFast function is much faster than the original analogRead: 19us instead of 435us. For backward compatibility with AVR based boards, the resolution is 10bits. Although the SAMD21 can handle higher resolutions, this is not implemented.

Several code lines makes no sense so I blocked them out, the speed is the same.
Can anyone tell me if this is okay?

#if defined(__arm__) 
int inline analogReadFast(byte ADCpin, byte prescalerBits) // inline library functions must be in header
{ // ADC->CTRLA.bit.ENABLE = 0;                     // Disable ADC, makes no sense?
  // while( ADC->STATUS.bit.SYNCBUSY == 1 );        // Wait for synchronization, makes no sense?
  ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV64 |      // Divide Clock by 64.
                   ADC_CTRLB_RESSEL_10BIT; 
  // ADC->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_1 |   // 1 sample, makes no sense?   
  //                    ADC_AVGCTRL_ADJRES(0x00ul); // Adjusting result by 0
  ADC->SAMPCTRL.reg = 0x00;                         // Sampling Time Length = 0

  // ADC->CTRLA.bit.ENABLE = 1;                     // Enable ADC, makes no sense?
  // while( ADC->STATUS.bit.SYNCBUSY == 1 );        // Wait for synchronization, makes no sense?  
  return analogRead(ADCpin);
}
#else
int inline analogReadFast(byte ADCpin, byte prescalerBits) // inline library functions must be in header
{ byte ADCSRAoriginal = ADCSRA; 
  ADCSRA = (ADCSRA & B11111000) | prescalerBits; 
  int adc = analogRead(ADCpin);  
  ADCSRA = ADCSRAoriginal;
  return adc;
}
#endif

NEW LIBRARY
Fast analogRead 10/12 bit ADC for the Arduino Zero and Uno
See here:

http://www.avdweb.nl/arduino/libraries/fast-10-bit-adc.html

So wait; how is that faster even though it calls the normal analogRead()? Just by changing the prescaler on the clock?
I was thinking that it should't be necessary to disable/renable the adc for each reading, though that makes it harder to co-exist with the existing code.

Hi westfw,

The main issue with the ADC's current configuration on the Arduino Zero is not only the ADC's prescaler, but also the SAMPEN bitfield in the ADC's SAMPCTRL register.

In the Arduino Zero core file "wiring.c" the SAMPEN is set to the maximum of 63 (0x3F), this adds an extra 63 half ADC clock cycles to the sample time.

The sample time can be adjusted in this way to account for a higher source resistance, the calculations for the minimum sample time are provided in the Electrical Characteristics at the back of the SAMD21 datasheet.

In my opinion, setting SAMPEN to 63 is extremely conservative. Even with the SAMPEN bitfield set to 0, the source resistance can be up to 165kOhms and is unlikely to trouble the majority of Arduino Zero users, while also offering a four fold decrease in ADC conversion time from the current 430us to about 90us.

It's then a matter of how low to set the ADC's prescaler? At 128 the prescaler reduces the conversion time down to around 28us with a maximum source resistance of 38kOhms.

In my code, I simply add a couple of lines to my sketch's setup() function to set SAMPEN to 0, as well as adjusting the ADC's prescaler and resolution:

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)

I've recently raised the issue SAMD core developers on Github: Arduino Zero ADC Sample Time Too Long · Issue #327 · arduino/ArduinoCore-samd · GitHub.

Hi Avandelen, I like that analogreadfast() library, many thanks for that. I am interested in the same issue for an Arduino Due. Do you happen to know whether it also works for the Due?