Arduino Due Unpredictable Analog Input Behavior

Hi!

I've got a problem I've isolated as best I can to something "quirky" in the Arduino Due ADC that I can't quite figure out.

I've got a project where I need to rapidly sample ADC channels 0, 1, 4, and 5.

I have found that on the Due, I can get away with sequential ADC reads without an intolerable amount of noise, so the below loop is what I'm using to collect 4000 sequential samples at full resolution and format them into an array to send to my computer over the serial port interleaved.

uint8_t analog[16000];
for(int i=0; i<4000; i++) {
      unsigned int analogvalue4 = analogRead(A0);
      unsigned int analogvalue5 = analogRead(A1);
      analog[4*i] = (analogvalue4 >> 8) & 0xFF;
      analog[4*i+1] = analogvalue4 & 0xFF;
      analog[4*i+2] = (analogvalue5 >> 8) & 0xFF;
      analog[4*i+3] = analogvalue5 & 0xFF;
      delayMicroseconds(ADCdelay);
    }
SerialUSB.write(analog, 16000);

The results from this close match my scope, even with ADCdelay at just 2 microseconds. Some popping, but I can filter that out in postprocessing and since I'm measuring phase I'd rather have more accurate timing than less noise.

However, this code breaks everything for reasons that escape me.

uint8_t analog[16000];
analogRead(A4);
delay(1);
for(int i=0; i<4000; i++) {
      unsigned int analogvalue4 = analogRead(A0);
      unsigned int analogvalue5 = analogRead(A1);
      analog[4*i] = (analogvalue4 >> 8) & 0xFF;
      analog[4*i+1] = analogvalue4 & 0xFF;
      analog[4*i+2] = (analogvalue5 >> 8) & 0xFF;
      analog[4*i+3] = analogvalue5 & 0xFF;
      delayMicroseconds(ADCdelay);
    }
SerialUSB.write(analog, 16000);

Even with a full millisecond of delay between doing a single shot measurement on A4, for some reason now A1 is giving me results that look like they are either A4 of a signal strongly influenced by A4.

Anyone have any ideas? I tried to find a way to... disconnect A4 or something, but didn't find any obvious way to do that. I tried setting the channel to an output (which I'm not sure is feasible anyway since the sensor is also a voltage source), but even that didn't help.

Ultimately I want to do something like this:

    uint8_t analog[32000];
    for(int i=0; i<4000; i++) {
      unsigned int analogvalue0 = analogRead(A0);
      unsigned int analogvalue1 = analogRead(A1);
      analog[8*i] = (analogvalue0 >> 8) & 0xFF;
      analog[8*i+1] = analogvalue0 & 0xFF;
      analog[8*i+2] = (analogvalue1 >> 8) & 0xFF;
      analog[8*i+3] = analogvalue1 & 0xFF;
      delayMicroseconds(ADCdelay);
    }
    delay(1);
    for(int i=0; i<4000; i++) {
      unsigned int analogvalue4 = analogRead(A4);
      unsigned int analogvalue5 = analogRead(A5);
      analog[8*i+4] = (analogvalue4 >> 8) & 0xFF;
      analog[8*i+5] = analogvalue4 & 0xFF;
      analog[8*i+6] = (analogvalue5 >> 8) & 0xFF;
      analog[8*i+7] = analogvalue5 & 0xFF;
      delayMicroseconds(ADCdelay);
    }
    SerialUSB.write(analog, 32000);

In order to measure the relative phase on A0/A1 first and then A4/A5 after a delay for letting things settle (although that shouldn't really be necessary), but obviously that's completely out of the question if just doing one read on A4 breaks A0 and A1... very odd.

I would not do this:
unsigned int analogvalue4 = analogRead(A0);
unsigned int analogvalue5 = analogRead(A1);

You are assigning a default value with a parameter that is returned from a function. I would recommend:

unsigned int analogvalue4 ;
unsigned int analogvalue5 ;
analogvalue4 = analogRead(A0);
analogvalue5 = analogRead(A1);

Give it a try.

What do you think the delay is doing for you?

you seem to be doing a lot of unnecessary multiplication of index variables, why not let the for loop increment by four or eight?

What is the impedance of your input sensors?

I was just doing it quick to validate that the ADC works properly before trying to optimize. It will probably end up different since this won't have very good timing accuracy, but since I couldn't get to the point where the four ADCs were simultaneously sampling I didn't bother going further. I thought this would be the easy part.

The delay shouldn't do anything, but with such strange behavior I thought it best to isolate the two parts in timing. Originally it was just measuring all four channels one after another in the main for loop, but that didn't work so I started pulling things apart to isolate the simplest instruction that would break it. The delay just proved that the read on A4 caused a persistent change to the behavior of the ADC, as opposed to some multiplexer-switching based settling issue.

With regard to defining the variable based on a function... forgive me, but I don't understand why that would be an issue. Doesn't the compiler take care of that? In any case, I have tried double reading the ADC already:

unsigned int analogvalue4 = analogRead(A0);
analogvalue4 = analogRead(A0);

and it didn't change anything. My hope with that was that reading once would switch the multiplexer and then the second read would be more stable, but this issue seems to stem from something else.

The impedance is 50 ohms for all four channels (connected to an opamp output through resistor).

I wonder if there is some bug in the Due ADC library. I might try rewriting it without using AnalogRead() and instead using the underlying registers, but that will take a little while to work through the data sheet.

The impedance is 50 ohms for all four channels (connected to an opamp output through resistor).

You sure of that it sounds very low for something coming out of an op amp.

With regard to defining the variable based on a function... forgive me, but I don't understand why that would be an issue. Doesn't the compiler take care of that?

It is not your issue but I thought as you were worried about timing it might be relevant. A compiler can only do so much optimization and there is way too much there for the compiler to make any sensible guess as to how to cut the processing down.

I would initially suspect some sort of latch up in the internal multiplexers, have you checked your grounds and have you made sure that the voltage your op amps deliver are not greater than the 3V3 supply voltage of the chip or less than zero volts.

Ah, of course. Yes, I am going to set up the system to do timer based sampling to improve accuracy by using a polled flag at even intervals (need to halt the other code running during this step anyway). This code was good enough for now, and simpler to explain. Once I've at least got qualitatively right behavior, I'll look into the timing details.

Right now, reading A4 a single time causes a persistent error on A1 but not A0. A0 still shows the same signal as before, but A1 starts showing crazy stuff. Since they're measured at the same timing, I don't think it's something like a delay causing the signal to stretch. The data collected also doesn't match my oscilloscope, so I am confident it's something in the Arduino as opposed to some oscillation in the sensor.

The opamp is the LT1638. You're right that it has a higher impedance, at most 1k, closer to 100 ohms at the frequency I'm testing at (plus the 50 ohm resistor).

The possibility of latching up the internal multiplexer sounds like it might describe the behavior I'm seeing. I don't think it should be able to drive the voltage that high, but I have some pads that I can put a zener diode on just in case.

Oh, except that A0 continues to sample properly while only A1 is messed up. I'll check anyway, but if the multiplexer were locking up, you would think it would show A4 data for both A0 and A1 instead of just A1. It's clearly still switching between two different channels, it just seems like maybe the wrong ones...

Okay, I did a bunch of testing to characterize the bug. The results are interesting, but still unclear what could be going on.

I first modified the code with the little changes suggested, just in case.

unsigned int analogvalue4, analogvalue5;
uint8_t analog[16000];
// analogRead(4);
delay(1);
for(int i=0; i<16000; i=i+4) {
  analogvalue4 = analogRead(0);
  analogvalue5 = analogRead(1);
  analog[i] = (analogvalue4 >> 8) & 0xFF;
  analog[i+1] = analogvalue4 & 0xFF;
  analog[i+2] = (analogvalue5 >> 8) & 0xFF;
  analog[i+3] = analogvalue5 & 0xFF;
  delayMicroseconds(ADCdelay);
}
SerialUSB.write(analog, 16000);

The result of reading and parsing this data in LabVIEW is the following:

which matches my scope.

If I modify this to do a single A4 read, channel A1 now reads the wrong data.

unsigned int analogvalue4, analogvalue5;
uint8_t analog[16000];
analogRead(4);
delay(1);
for(int i=0; i<16000; i=i+4) {
  analogvalue4 = analogRead(0);
  analogvalue5 = analogRead(1);
  analog[i] = (analogvalue4 >> 8) & 0xFF;
  analog[i+1] = analogvalue4 & 0xFF;
  analog[i+2] = (analogvalue5 >> 8) & 0xFF;
  analog[i+3] = analogvalue5 & 0xFF;
  delayMicroseconds(ADCdelay);
}
SerialUSB.write(analog, 16000);

gives

Okay, so I then tried switching the read order of A0 and A1 inside the for loop like this.

unsigned int analogvalue4, analogvalue5;
uint8_t analog[16000];
//analogRead(4);
delay(1);
for(int i=0; i<16000; i=i+4) {
  analogvalue4 = analogRead(1);
  analogvalue5 = analogRead(0);
  analog[i] = (analogvalue4 >> 8) & 0xFF;
  analog[i+1] = analogvalue4 & 0xFF;
  analog[i+2] = (analogvalue5 >> 8) & 0xFF;
  analog[i+3] = analogvalue5 & 0xFF;
  delayMicroseconds(ADCdelay);
}
SerialUSB.write(analog, 16000);

but this gave new bizarre behavior, as shown.

This looks good, except that switching which ADC input went to which set of data didn't change the data in the packet! The red channel is supposed to be the second input channel, so it should have flipped the colors of the two plots!

So apparently, the loop reading pairs of values is totally ignoring the actual settings I give it and reads A0 first followed by A1 even when I tell it to do it in the other order!

Then I put back the order of A0/A1 in reading, but read A5 first instead of A4. A5 is the same frequency as A4 but a lower voltage amplitude.

unsigned int analogvalue4, analogvalue5;
uint8_t analog[16000];
analogRead(5);
delay(1);
for(int i=0; i<16000; i=i+4) {
  analogvalue4 = analogRead(0);
  analogvalue5 = analogRead(1);
  analog[i] = (analogvalue4 >> 8) & 0xFF;
  analog[i+1] = analogvalue4 & 0xFF;
  analog[i+2] = (analogvalue5 >> 8) & 0xFF;
  analog[i+3] = analogvalue5 & 0xFF;
  delayMicroseconds(ADCdelay);
}
SerialUSB.write(analog, 16000);

and doing this gave me what appears to be A5 in place of A1 in the data.

so now the problem looks like it isn't anything special about A4 either, but rather something again that has to do with data read order.

In case the problem had to do with reading from A4/A5 while sampling A0/A1 (not sure why this would be an issue), I flipped it around and sampled the pair A4/A5 without touching A0/A1.

unsigned int analogvalue4, analogvalue5;
uint8_t analog[16000];
//analogRead(5);
delay(1);
for(int i=0; i<16000; i=i+4) {
  analogvalue4 = analogRead(4);
  analogvalue5 = analogRead(5);
  analog[i] = (analogvalue4 >> 8) & 0xFF;
  analog[i+1] = analogvalue4 & 0xFF;
  analog[i+2] = (analogvalue5 >> 8) & 0xFF;
  analog[i+3] = analogvalue5 & 0xFF;
  delayMicroseconds(ADCdelay);
}
SerialUSB.write(analog, 16000);

which gave me apparently good looking data for channels A4 and A5.

Predictably, a single read of A0 now messes up the A4/A5 pair.

unsigned int analogvalue4, analogvalue5;
uint8_t analog[16000];
analogRead(0);
delay(1);
for(int i=0; i<16000; i=i+4) {
  analogvalue4 = analogRead(4);
  analogvalue5 = analogRead(5);
  analog[i] = (analogvalue4 >> 8) & 0xFF;
  analog[i+1] = analogvalue4 & 0xFF;
  analog[i+2] = (analogvalue5 >> 8) & 0xFF;
  analog[i+3] = analogvalue5 & 0xFF;
  delayMicroseconds(ADCdelay);
}
SerialUSB.write(analog, 16000);

with A0 now apparently replacing A4.

Finally, I tried reading A5 followed by A4 followed by the pair reading of A0/A1 to see if it had to do with the most recent channel sampled or if it was something else.

unsigned int analogvalue4, analogvalue5;
uint8_t analog[16000];
analogRead(5);
analogRead(4)
delay(1);
for(int i=0; i<16000; i=i+4) {
  analogvalue4 = analogRead(0);
  analogvalue5 = analogRead(1);
  analog[i] = (analogvalue4 >> 8) & 0xFF;
  analog[i+1] = analogvalue4 & 0xFF;
  analog[i+2] = (analogvalue5 >> 8) & 0xFF;
  analog[i+3] = analogvalue5 & 0xFF;
  delayMicroseconds(ADCdelay);
}
SerialUSB.write(analog, 16000);

which gave

showing that A5 is still replacing A0! This is not remotely what I would expect, so I still clearly am understanding the bug wrong. It's not even the most recent channel sampled, measuring A5, then A4, and then the pair A0/A1 does the same error as if I didn't measure A4 in between.

I also tried with the A1/A4 pair. I won't continue to post the code and results, but A1/A4 looked good as a pair. Adding a single sample of A0 however replaced A1 with A0 data.

This is all incredibly strange to me. The ADC is clearly collecting real data, it matches at least one of the input signals in each case, matches my oscilloscope readout (but not the right channel), and it even has glitches that I would expect from the multiplexer. It appears to be doing analog reads based only on the order of requests, instead of on the argument to AnalogRead(). But the first time AnalogRead is called it seems to set a channel.

I don't even know where to start here. I doubt it's electrical since the scope isn't showing anything strange and there is some configuration in each case with which the data is correct. Really I feel like this level of characterization of the bug means there's almost nothing it could be other than the library or hardware.

That's an excerpt from the analogRead implementation for the SAM:

	static uint32_t latestSelectedChannel = -1;
	switch ( g_APinDescription[ulPin].ulAnalogChannel )
	{
		// Handling ADC 12 bits channels
		case ADC0 :
		case ADC1 :
		case ADC2 :
		case ADC3 :
		case ADC4 :
		case ADC5 :
		case ADC6 :
		case ADC7 :
		case ADC8 :
		case ADC9 :
		case ADC10 :
		case ADC11 :

			// Enable the corresponding channel
			if (ulChannel != latestSelectedChannel) {
			  adc_enable_channel( ADC, ulChannel );
			  latestSelectedChannel = ulChannel;
			}

It looks like there's no change in the MUX configuration if the stored channel is the same as the one of the last call. If you're calling that from interrupt context, it might be possible that these stuff gets disordered somehow. I would try to remove this optimization and call adc_enable_channel() in each call. The look if your results change.

A few things to check:
There may have been recent changes to ADC in the latest IDE version or nightly build.
There are problems and workarounds for ADC discussed in section 49.2.4 (page 1454) of the SAM3X datasheet.
A DAC solution from another thread:

Hmm, that looks promising. I'm not quite sure what's going on in this section of code. It looks like the switch is based on g_APinDescription[ulPin].ulAnalogChannel, but then it's doing the mux update based only on ulChannel. Is that defined outside of the excerpt, or is this a typo in the library?

Either way, I tried a few things to see if it would work, but they're giving me type issues. I did this:

unsigned int analogvalue4, analogvalue5;
uint8_t analog[16000];
//    analogRead(4);
delay(1);
for(int i=0; i<16000; i=i+4) {
  adc_enable_channel(ADC, ADC0);
  analogvalue4 = analogRead(0);
  analogvalue5 = analogRead(1);
  analog[i] = (analogvalue4 >> 8) & 0xFF;
  analog[i+1] = analogvalue4 & 0xFF;
  analog[i+2] = (analogvalue5 >> 8) & 0xFF;
  analog[i+3] = analogvalue5 & 0xFF;
  delayMicroseconds(ADCdelay);
}
SerialUSB.write(analog, 16000);

but it gives the error

StimEvaluationCommands.ino: In function 'void evaluatecommand(String)':
StimEvaluationCommands:29: error: cannot convert '_EAnalogChannel' to 'adc_channel_num_t' for argument '2' to 'void adc_enable_channel(Adc*, adc_channel_num_t)'

so apparently the type of the second argument is something called "adc_channel_num_t" but I am not sure what that is. It looks like maybe a numref, but I don't see what the correct way to do this is.

Okay. Thanks everyone for all the help.

I downloaded the 1.5.6-r2 version of the Arduino IDE and it now works like I would expect. Based on a conversation in another thread Due analog read · Issue #1740 · arduino/Arduino · GitHub it looks like this was a bug where adc_disable_channel(ADC, ulChannel); was not properly called when channels were switched.

Man, that was a lot of time down the drain for a library bug. Ugh.

But working now! Thanks again for helping me pin it down!