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...

22 Responses

  1. Frenky says:

    A while ago I made some ADC speed tests on Arduino Due:
    http://frenki.net/2013/10/fast-analogread-with-arduino-due/

    Might come handy…

  2. Jonathan says:

    Should probably fix the “&gt” and “&amp”s to avoid confusing people.

  3. chris says:

    Hi, and I tried to compile this for a due, and wonder if there is a #include file to define all the constants and variables…
    I get something like this
    thanks
    chris

    In function ‘void myADCsetup()’:
    WebServer.ino:1438:26: error: ‘ID_ADC’ was not declared in this scope
    WebServer.ino:1438:32: error: ‘pmc_enable_periph_clk’ was not declared in this scope
    WebServer.ino:1440:18: error: ‘SystemCoreClock’ was not declared in this scope
    WebServer.ino:1440:64: error: ‘ADC_STARTUP_FAST’ was not declared in this scope
    WebServer.ino:1440:80: error: ‘adc_init’ was not declared in this scope

    • piotr says:

      Are you using Arduino program? In my case (version 1.6.5) no additional include is needed, it’s done in the lower layer by Arduino. You can try including (it’s in the hardware\arduino\sam\system\libsam).

  4. chris says:

    Hi, and thanks for the reply, I’m using the Arduino IDE on a Mac, version 1.6.3

  5. chris says:

    and under hardware/arduino there is no sam nor system nor libsam folder :-(

  6. chris says:

    a libsam folder was included in my 1.6.1 download, and I moved it to my Library folder, and added

    #include

    to the source code

    • piotr says:

      Is it working for you now? I think the best way will be to install the newest Arduino and use boards manager to make sure that Arduino Due support is installed.

  7. Hieu Tran says:

    I use your method to convert analog voltage value. I want to display real value to serial port.
    I added some lines to convert raw data to real data, but cannot work. It can display raw value only.
    Could you help me why for a newbie like me? Thank you very much

    • piotr says:

      Please post your code, I will try to help.

      • Hieu Tran says:

        This is my code.
        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);// To use peripheral, we must enable clock distributon to it
        adc_init (ADC, SystemCoreClock, ADC_FREQ_MIN, 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, ADC_MR_SLEEP_NORMAL, ADC_MR_FWUP_OFF);// Disable sleep
        adc_configure_timing (ADC, 1, ADC_SETTLING_TIME_3, 1);// Set timings – standard value
        adc_set_bias_current (ADC, 1);// Bias current – maximum performance over current consumption
        adc_disable_tag (ADC);// it has to do with sequencer, not using it
        adc_disable_ts (ADC);// deisable temperature sensor
        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);// just one channel enabled
        adc_configure_trigger (ADC, ADC_TRIG_SW, 1);// triggering from software, freerunning mode
        adc_start( ADC );
        PDC_ADC->PERIPH_RPR = (uint32_t) buf; // address of DMA buffer
        PDC_ADC->PERIPH_RCR = BUFFER_SIZE;
        PDC_ADC->PERIPH_PTCR = PERIPH_PTCR_RXTEN; // enable receive
        Serial.begin(115200);
        }

        void loop()
        {
        uint8_t pwmCnt=10;
        float data1=0;
        float raw=0;

        analogWrite(pwm,pwmCnt);
        pwmCnt+=30;
        // configure Peripheral DMA

        digitalWrite(led,HIGH);
        while((adc_get_status(ADC) & ADC_ISR_ENDRX) == 0)
        {}; //Wait for end of conversion (EOC) & DMA transfer completes
        digitalWrite(led,LOW);

        for (int i=0; i<BUFFER_SIZE; i++)
        {

        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
        Serial.write((uint8_t * )buffer, pktSize);
        Serial.read(pktSize);
        Serial.println(buf[i]);
        raw = buffer;
        raw = map(adc_get_latest_value(ADC),16,4095,0,4095);
        data1 = raw*(3.300/4095.0);
        Serial.println(data1);
        }
        }

        • piotr says:

          If you want to simply print voltage value to serial, you can just use:
          Serial.println(data1*(3.300/4095.0));
          or
          Serial.println(buf[i]*(3.300/4095.0));
          instead of my plot function.
          If you want to use simplot – I think it cannot display float data, only integers.

          • Hieu Tran says:

            Thank you very much for your precious time. However, it cannot update values when I adjust potentiometer. What’s wrong with my loop
            void loop()
            {
            uint8_t pwmCnt=10;
            float data1=0;
            float raw=0;

            analogWrite(pwm,pwmCnt);
            pwmCnt+=30;

            digitalWrite(led,HIGH);
            while((adc_get_status(ADC) & ADC_ISR_ENDRX) == 0)
            {}; //Wait for end of conversion (EOC) & DMA transfer completes
            digitalWrite(led,LOW);
            // configure Peripheral DMA
            PDC_ADC->PERIPH_RPR = (uint32_t) buf; // address of DMA buffer
            PDC_ADC->PERIPH_RCR = BUFFER_SIZE;
            PDC_ADC->PERIPH_PTCR = PERIPH_PTCR_RXTEN; // enable receive

            for (int i=0; i<BUFFER_SIZE; i++)
            {
            Serial.println(buf[i]*(3.300/4095.0));
            delay(500);
            }
            }

  8. Hieu Tran says:

    Hi Prior,
    It works now. Since delay(500) in “for loop” is my mistake. BTW, could I email you (your email?) to discuss for more comfortable? Thanks again.

  9. Hello, great work, any way to modify the libsam_sam3x8_gcc_rel.a/.txt files or variant.cpp/.h files to include an equivalent to analogReference(INTERNAL1V1); ??

  10. Augusto says:

    Hi! I’m very new to Arduino world. How can I change that code to get values from two inputs instead of one?

    • piotr says:

      Hi. Are you sure the simplest method of using analogRead function is not enough for you? When diving deeper into configuration of ADC and other peripherals, things go much more complex.

      • Augusto says:

        Hi! I didn’t saw this and I didn’t get any notification before, sorry! I’m trying to get two signals from an ultrasound generator working around 20kHz in each each channel. I would like to achieve the higher frequency that Arduino Due can give me for each channel, but I don’t know how. Can you help me a little bit?

        I’m looking to transfer the data to the PC to do digital signal processing with Python, in first place

  11. Jonah says:

    note “serial1.write()” is only for Meta. For due it should be “serial.write()” without the “1”.

  1. November 20, 2015

    […] For more detail: Playing with analog-to-digital converter on Arduino Due […]

  2. February 25, 2017

    […] For More Details: Playing with analog-to-digital converter on Arduino Due […]

Leave a Reply

Your email address will not be published. Required fields are marked *