Playing with analog-to-digital converter on Arduino Due

Today I’m going to present some of more advanced capabilities of ADC built in ATSAM3X8E – the heart of Arduino Due.

I like the Arduino platform. It makes using complex microcontrollers much simpler and faster. Lets take for example the analog-to-digital converter. To configure it even on Atmega328 (Arduino Uno/Duemilanove) you must understand and set correct values in 4 registers. And it can be much more in complex device, like 14 in ATSAM3X8E (Arduino Due)!
In Arduino, for no matter which processor, all you need to do is:

val = analogRead(A0);

It’s simple and useful. But there are situations, where you need to use more potential of your chip. Arduino allows you to do so – after all it’s just C++ with some additions.
In my project on Arduino Due I need to sample voltage continuously and as fast as possible. Lets try it the simplest way:

int input = A0;
int led = 13;
int val;
void setup()
{
  pinMode(input,INPUT);
  pinMode(led,OUTPUT);
}
void loop()
{
  digitalWrite(led,HIGH);
  val = analogRead(input);
  digitalWrite(led,LOW);
}

Anything the program does is reading ADC and toggling the led line, so I can measure how fast it happens. So lets compile program and look at the oscilloscope:
Timing of ADC - the simple way

There is nice square ~100kHz wave. So the sampling frequency is about 100kS/s. Not bad, but according to datasheet it can be much higher (1Ms/s). And I’m not doing anything else, just reading ADC and wasting all the processor’s time. There is also another problem, which is clearly visible when looking on a wave with oscilloscope with persistence:
ADC timing - animation

Yes, time between samples is not always even. It wiggles a microsecond or less from time to time. It is due to other tasks that Arduino are performing in background, like counting the time. That’s bad. Sampling frequency should be constant. To unleash full potential of this chip, different approach is needed.

Good news is that Arduino let’s you use almost all the capabilities of microcontroller by using low level C/C++ programming. Bad news – this is quite hard, especially with complex ARM processors. Processors peripherals’ documentation is large (about 100 pages in datasheet and two application notes for ADC alone). And it’s barely enough to understand all the details. Another useful info are examples available in Atmel Studio and the Arduino libraries’ source (\%arduino%\hardware\arduino\sam\cores\arduino).

Using ADC, just like any peripheral, is done by setting appropriate values to related registers. It can be done directly in program, but then it’s quite susceptible to errors. 14 registers, 32 bits in each of them – it’s 448 places to make a mistake. But fortunately, Atmel provides some libraries to make the task easier. And they are bundled in Arduino (\%arduino%\hardware\arduino\sam\system\libsam\). With them, instead of writing to registers, you call corresponding functions. They are handling all the bit masking, shifting and similar stuff. So lets look at the next version of program:

int input = A0;
int led = 13;
int val;

void setup()
{
  pinMode(input,INPUT);
  pinMode(led,OUTPUT);
  // Setup all registers
  pmc_enable_periph_clk(ID_ADC); // To use peripheral, we must enable clock distributon to it
  adc_init(ADC, SystemCoreClock, ADC_FREQ_MAX, ADC_STARTUP_FAST); // initialize, set maximum posibble speed
  adc_disable_interrupt(ADC, 0xFFFFFFFF);
  adc_set_resolution(ADC, ADC_12_BITS);
  adc_configure_power_save(ADC, 0, 0); // Disable sleep
  adc_configure_timing(ADC, 0, ADC_SETTLING_TIME_3, 1); // Set timings - standard values
  adc_set_bias_current(ADC, 1); // Bias current - maximum performance over current consumption
  adc_stop_sequencer(ADC); // not using it
  adc_disable_tag(ADC); // it has to do with sequencer, not using it
  adc_disable_ts(ADC); // deisable temperature sensor
  adc_disable_channel_differential_input(ADC, ADC_CHANNEL_7);
  adc_configure_trigger(ADC, ADC_TRIG_SW, 1); // triggering from software, freerunning mode
  adc_disable_all_channel(ADC);
  adc_enable_channel(ADC, ADC_CHANNEL_7); // just one channel enabled
  adc_start(ADC);
}

void loop() 
{
  while(1)
  {
    PIO_Set(PIOB,PIO_PB27B_TIOB0);
    while ((adc_get_status(ADC) & ADC_ISR_DRDY) != ADC_ISR_DRDY)
      {}; //Wait for end of conversion
    PIO_Clear(PIOB,PIO_PB27B_TIOB0);
    val = adc_get_latest_value(ADC); // Read ADC
  }
}

This program handles the ADC using Atmel SAM libraries. It initializes all ADC registers first. It’s not really necessary – most of these parameters are set to their default values. But it’s better to be sure – Arduino startup code plays with these registers too, and no one can be sure what will be there in next release.
In this example, I’ve set ADC to freerunning mode. In this mode, conversion has to be started just once, and ADC is running by itself after that. Every time conversion is complete, we have information in status register and next conversion starts immediately. So ADC is running in maximum possible speed and main program must only read converted values when they’re ready.

As you may notice, in this example I used ADC_CHANNEL_7 input. And if you think, it’s just A0 pin… you are just as wrong, as me! I wasted about an hour trying to find out why I read other value than I set at input. And then I checked Arduino schematic. Yes, analog pins A0..A7 are connected to analog inputs 0..7, but in reversed order! A0 is ADC channel 7, A1 – 6 and so on. Arduino, damn you!

To make this example faster, I exchanged Arduino’s slow digitalWrite function to faster direct register operations – PIO_Set and PIO_clear. I also used a while(1) loop inside the loop() function. It prevents some operations that Arduino are doing between loop() calls. It’s bad idea to do so – some Arduino functionality is lost, buts it’s only for this test. So lets check the timing now:

It’s much better. It achieves 666.6kS/s sample rate. Why not 1MS/s?
Because processor clock (84MHz) cannot be simply divided down to 20MHz ADC clock required for 1MS/s. Real ADC clock is 14MHz. It could be changed by changing processor’s main clock settings, but I don’t want to do that. It could disturb some of Arduino libraries. I can live with a little lower sample rate.

Sample rate is now OK, but processor still can’t do much except reading ADC values. If any part of program is taking more than 3us, ADC samples are lost. So, here comes great peripheral unseen in low end microcontrollers like AVR – DMA (Direct memory access). It does simple thing – reads value from some place (in this case – ADC, when conversion is complete) and transfers it to other place (in this case – memory). And all this is done independently of processor. Nice! So let’s see another program:

int input = A0;
int led = 13;
int pwm=3;
int val;
#define BUFFER_SIZE  1000
uint16_t buf[BUFFER_SIZE];

void setup()
{
  pinMode(input,INPUT);
  pinMode(led,OUTPUT);
  pinMode(pwm,OUTPUT);
  // Setup all registers
  pmc_enable_periph_clk (ID_ADC);
  adc_init (ADC, SystemCoreClock, ADC_FREQ_MIN, ADC_STARTUP_FAST);
  adc_disable_interrupt (ADC, 0xFFFFFFFF);
  adc_set_resolution (ADC, ADC_10_BITS);
  adc_configure_power_save (ADC, ADC_MR_SLEEP_NORMAL, ADC_MR_FWUP_OFF);
  adc_configure_timing (ADC, 1, ADC_SETTLING_TIME_3, 1);
  adc_set_bias_current (ADC, 1);
  adc_disable_tag (ADC);
  adc_disable_ts (ADC);
  adc_stop_sequencer (ADC);
  adc_disable_channel_differential_input (ADC, ADC_CHANNEL_7);
  adc_disable_all_channel (ADC);
  adc_enable_channel (ADC, ADC_CHANNEL_7);
  adc_configure_trigger (ADC, ADC_TRIG_SW, 1);
  Serial1.begin(115200);
  adc_start( ADC );
}

void loop() 
{
  static uint8_t pwmCnt=10;
  analogWrite(pwm,pwmCnt);
  pwmCnt+=30;
  // configure Peripheral DMA
  PDC_ADC->PERIPH_RPR = (uint32_t) buf; // address of buffer
  PDC_ADC->PERIPH_RCR = BUFFER_SIZE; 
  PDC_ADC->PERIPH_PTCR = PERIPH_PTCR_RXTEN; // enable receive
  digitalWrite(led,HIGH);
  while((adc_get_status(ADC) & ADC_ISR_ENDRX) == 0)
  {};
  digitalWrite(led,LOW);
  for (int i=0; i<BUFFER_SIZE; i++)
  {
    plot(buf[i]);
  }
  delay(500);
}

void plot(uint16_t data1)
{
  uint16_t pktSize = sizeof(uint16_t);
  uint16_t buffer[pktSize*3];
  buffer[0] = 0xCDAB;             //SimPlot packet header. Indicates start of data packet
  buffer[1] = pktSize;      //Size of data in bytes. Does not include the header and size fields
  buffer[2] = data1;
  pktSize += 2*sizeof(uint16_t); //Header bytes + size field bytes + data
  Serial1.write((uint8_t * )buffer, pktSize);
}

In this program, ADC is configured basically the same. The main difference is the use of PDC. PDC is Peripheral DMA Controller – one of two DMAs available in ATSAM3X8E. That DMA can be used with ADC.
So, once PDC is configured and enabled, processor can do other tasks. Once DMA transfer is complete (ADC_ISR_ENDRX), all the data is available in the memory buffer. PDC can also automatically transfer data to second buffer when the transfer is complete. This ensure no data loss, but this feature is not used here. All the program does is waiting for data and sending it by serial port. I used Serial1, one of 4 Due’s serial ports. It’s nice because it doesn’t interfere with programming port.

In this example, I decided to have anything interesting to convert. The simplest way is to use PWM outputs available at Arduino Due. To make it more analog and interesting, I connected PWM output to ADC input by simple RC circuit:

RC circuit

RC circuit

After 1000 points are gathered, they are transferred to PC. It’s transferred within special packets. They’re recognized by useful tool – SimPlot. It’s little nice program that can plot data from serial port:

Samples from ADC displayed in SimPlot

Samples from ADC displayed in SimPlot

So, that’s the end of this post. Time to build something cool using this Arduino ;)

You may also like...