Arduino ADC channels programming approach

Please let me know that I want to display sensors reading, 6 sensors 3 for Voltage sensing and 3 for current sensing CT.

I recently write a code for reading ADC data, please let me know what is good approach for making code.

I mean, code with good sequence for controller, not hanging and not take extra memory.

Please make your valuable suggestions.

int sensorPinV1 = A0;    // select the input pin for the potentiometer
int sensorPinV2 = A1;    // select the input pin for the potentiometer
int sensorPinV3 = A2;    // select the input pin for the potentiometer
int sensorPinA1 = A3;    // select the input pin for the potentiometer
int sensorPinA2 = A4;    // select the input pin for the potentiomete
int sensorPinA3 = A5;    // select the input pin for the potentiomete

int sensorValueV1 = 0;  // variable to store the value coming from the sensor
int sensorValueV2 = 0;  // variable to store the value coming from the sensor
int sensorValueV3 = 0;  // variable to store the value coming from the sensor
int sensorValueA1 = 0;  // variable to store the value coming from the sensor
int sensorValueA2 = 0;  // variable to store the value coming from the sensor
int sensorValueA3 = 0;  // variable to store the value coming from the sensor



void setup() {
  // declare the ledPin as an OUTPUT:
  Serial.begin(9600);
 }

void loop() {
  // read the value from the sensor:
  sensorValueV1 = analogRead(sensorPinV1);
  delay(1);
  sensorValueV2 = analogRead(sensorPinV2);
  delay(1);
  sensorValueV3 = analogRead(sensorPinV3);
  delay(1);
  sensorValueA1 = analogRead(sensorPinA1);
  delay(1);
  sensorValueA2 = analogRead(sensorPinA2);
  delay(1);
  sensorValueA3 = analogRead(sensorPinA3);
  
  
  
  float V1 = sensorValueV1 * (5.0 / 1023.0);  
  Serial.print("V1 VALUE IS = ");
  Serial.println(V1);
  
  float V2 = sensorValueV2 * (5.0 / 1023.0);  
  Serial.print("V2 VALUE IS = ");
  Serial.println(V2);

  float V3 = sensorValueV3 * (5.0 / 1023.0);  
  Serial.print("V3 VALUE IS = ");
  Serial.println(V3);

  float A1 = sensorValueA1 * (5.0 / 1023.0);  
  Serial.print("A1 VALUE IS = ");
  Serial.println(A1);

  float A2 = sensorValueA2 * (5.0 / 1023.0);  
  Serial.print("A2 VALUE IS = ");
  Serial.println(A2);

  float A3 = sensorValueA3 * (5.0 / 1023.0);  
  Serial.print("A3 VALUE IS = ");
  Serial.println(A3);
  
  delay(3000); 
  
}
  • pins could be defined as const byte instead of int

  • don't leave stupid comments in the code  // declare the ledPin as an OUTPUT:

  • given you don't do anything with the variables you create but print, you could just as well do

  Serial.print("V1 VALUE IS = ");
  Serial.println(analogRead(sensorPinV1) * (5.0 / 1023.0));
  delay(1);

and same for the others.

  • delay(1);is a long time. double reading the analogPin should give you a fair read

  • don't us A1, A2, A3 for your variables. These are already in use by the IDE for analogPins...

A brief version of your codes (semi-tested in NANO) which occupies 3280/266 instead of 3804/302 space.

float sensorValueFloat[6] = {0};

void setup() 
{
  Serial.begin(9600);
}

void loop() 
{
  for(byte i=14, j=0; i<20; i++, j++)//A0 to A5 = 14 to 19
  {
    sensorValueFloat[j] = (float)analogRead(i)*(5.0/1023.0); 
    Serial.print("Sensor-"); Serial.print(j); Serial.print(" Value: ");
    Serial.print(sensorValueFloat[j], 2);
    Serial.println();
  }
  Serial.print("=========================");
  Serial.println();
  delay(3000);
}

GolamMostafa:
A brief version of your codes (semi-tested in NANO) which occupies 3280/266 instead of 3804/302 space.

if you want to have fun, (same output) you can go further in SRAM saving:

void setup()
{
  Serial.begin(112500);
}

void loop()
{
  for (char i = 14; i < 20; i++) {
    Serial.print(F("Sensor-")); 
    Serial.write(i - 14 + '0');   
    Serial.print(F(" Value: "));
    // analogRead(i); // you should really have this throw away line for voltage stabilization
    Serial.println((5.0 / 1023.0)*analogRead(i), 2);
  }

  for (char i = 0; i < 26; i++) Serial.write('-');
  Serial.write('\n');
  delay(3000);
}

→ 3286/200 bytes → you save 66 bytes of precious SRAM in exchange for 6 bytes of flash :slight_smile:

Note that for stability, you would need to read twice the analog input.

J-M-L:
→ 3286/200 bytes → you save 66 bytes of precious SRAM in exchange for 6 bytes of flash :slight_smile:

And if you want to go full bananas:

const byte sensorPins[] = {A0, A1, A2, A3, A4, A5};

uint16_t adcToMillivolt(uint16_t adcValue)
{
    uint32_t voltage = (uint32_t)adcValue * 5000;
    voltage = voltage / 512;
    // Check if rounding is necessary
    if((voltage & 0x01) == 1) {
        voltage += 2;
    }

    voltage = voltage / 2;
    return (uint16_t)voltage;
}

uint16_t millivoltToVolt(uint16_t millivolt)
{
    uint16_t volt = ((uint32_t)millivolt * 65) / 65536;
    return volt;
}

void printADCAsVoltage(uint16_t adcValue) 
{
    uint16_t millivolt = adcToMillivolt(adcValue);
    uint16_t volts = millivoltToVolt(millivolt);
    millivolt = millivolt - (volts * 1000);

    // print it
    Serial.print(volts);
    Serial.print(".");
    if(millivolt < 10) {
        Serial.print("0");
    }
    if(millivolt < 100) {
        Serial.print("0");
    }
    Serial.println(millivolt);
}

void setup() 
{
    Serial.begin(9600);
}

void loop()
{
    for(byte i = 0; i < sizeof(sensorPins); i++) {
        uint16_t adcValue = analogRead(sensorPins[i]);
        Serial.print(F("Sensor-"));
        Serial.print(i);
        Serial.print(F(" Value: "));
        printADCAsVoltage(adcValue);
    }

    for (char i = 0; i < 26; i++) {
        Serial.write('-');
    }
    Serial.write('\n');

    delay(3000);
}

same but rounded output, and 3 digits after the dot.
→ 2264/198 → you saved another 2 bytes of SRAM and 1022 bytes of flash.

J-M-L:
Note that for stability, you would need to read twice the analog input.

Why are you recommending so when the ‘int x = anoalogRead(A0);’ is the compact form of the following tasks; where, data is only read once complete conversion is done?

1. VREF and analog channel are selected;
2. ADC is started;
3. Keep checking (by polling/interrupt?) that the conversion is done;
4. Bring ADC data into user variable x.

GolamMostafa:
Why are you recommending so when the 'int x = anoalogRead(A0);'

It’s to ensure better accuracy Because of the way the ADC works on those ATMEGA:

There is only one ADC and the analog pins are multiplexed from PORT x into that unique ADC.

When you switch from one analog pin to another for reading voltage, say from pin A0 to pin A1, you actually tell the multiplexer to switch the electrical route coming into the ADC from pin A0 to A1.

ADC in AVR microcontrollers uses a technique known as successive approximation by comparing the input voltage with half of the reference voltage generated internally. The comparison continues by dividing the voltage further down and updating each bit in ADC register with a 1 if input voltage is higher or a 0 otherwise. This process repeats 10 times (as we have a 10 bit ADC) and generates resulting binary output.

So for this to work you can’t operate directly from the voltage at the pin (which can vary). You need first to acquire the input voltage and this is done with a “Sample and hold“ circuit which captures the pin voltage and freezes its value at a constant level for a specified minimum period of time. Freezing that value is accomplished by storing the electric charge of the input pin in a capacitor. And there is only one such sample and hold circuit, shared with all analog pins.

So to perform a proper analogRead() you need to ensure this capacitor has had the time to capture the relevant input voltage, and as for any capacitor to reach a certain value, there is a time requirement for this.

If you have important voltage variation between the old pin (A0) and new pin (A1) (say one is at 0V and the other is at 5V) and a high impedance on the pin you want to sample, when you switch at high speed, you are at risk of triggering the ADC successive approximations process on a locked charge that was not exactly settled on the input value.

Settling time issues can be reduced by lowering the throughput rate of the ADC to provide longer acquisition time, but an easy technique if you don’t want to mess around with registers and timing is to read once (that triggers the multiplexer route switch and starts feeding the “Sample and hold“ circuit), throw the value away, and read again. By then the capacitor will have adjusted and the second value you read will be “correct”.

You don’t need to do this if you don’t switch the pin you read from of course, but here we go quickly through pin A0 to A5, so there is a risk (depends on impedance of the sensor)

So cool reading (+1k).

One query: In ATmega328P MCU, does the 'Hold Capacitor' get discharged by some kind of shorting mechanism before the acquisition switches over to next analog channel?

Nick G has a good article on how the ADC works At high level and also a more detailed discussion here.

GolamMostafa:
One query: In ATmega328P MCU, does the ‘Hold Capacitor’ get discharged by some kind of shorting mechanism before the acquisition switches over to next analog channel?

I found another good reference here on how things work, see especially section 4.4 and 4.5

if you want to convince yourself, get a couple 100kΩ resistors and wire this way:
A0 <—> 100kΩ <----> GND
A1 <—> 100kΩ <----> 5V

and try the following code

// A0 <---> 100kΩ <----> GND
// A1 <---> 100kΩ <----> 5V

const uint8_t analogPins[] = {A0, A1};
const uint8_t nbPins = sizeof(analogPins) / sizeof(analogPins[0]);
volatile int  stableAnalog[nbPins]; // volatile to avoid compiler optimization

void setup() {
  Serial.begin(115200);
  for (uint8_t i = 0; i < sizeof(analogPins); i++) {
    stableAnalog[i] = analogRead(analogPins[i]);
    stableAnalog[i] = analogRead(analogPins[i]);
    Serial.print(F("Stable value of A")); Serial.print(i);
    Serial.print(F(" = ")); Serial.println(stableAnalog[i]);
  }
}

void loop()
{
  for (uint8_t i = 0; i < sizeof(analogPins); i++) {
    // analogRead(analogPins[i]);  // UNCOMMENT TO SEE BEHAVIOR CHANGE
    int a = analogRead(analogPins[i]);
    if ((a < stableAnalog[i] - 2) || (a > stableAnalog[i] + 2)) {
      Serial.print(F("A")); Serial.print(i);
      Serial.print(F(" is ")); Serial.println(a);
      Serial.print(F(" which differs from ")); Serial.println(stableAnalog[i]);
    }
  }
  delay(1000);
}

run it. stable values should be 0 and 1023 (min an max of the ADC if your arduino board is OK)
but you’ll see that the reads are not matching the stable initial values and you get warning messages.

Now, uncomment this line    // analogRead(analogPins[i]);  // UNCOMMENT TO SEE BEHAVIOR CHANGEand we perform now two analogRead() for the same pin and if you run the code again you’ll see no warning message anymore.

Thank you @J-M-L for quoting the references of goodies on ADC.

My query in Post#7 is to know if there exists in the ADC of ATmega328P a switch similar to S2 of the following figure (Fig-1), which drains the whole charge of the CSH capacitor before acquiring next sample of the input signal.


Figure-1:

The following figure (Fig-2) of the referred Nick Gammon post indicates that there is no such (switch S2 of Fig-1) shorting/discharging mechanism for the hold capacitor of the ADC Module of ATmega328P.
ADC_internals.png
Figure-2:

ADC_internals.png

Dear J-M-L , GolamMustafa and LightuC,

I am very thankful to you all.

Please let me know how to write C-code in tricky manner with direct approach, please refer

any article or book or datasheet or note?

Imranahmed:
Please let me know how to write C-code in tricky manner with direct approach, please refer any article or book or datasheet or note?

J-M-L:
if you want to have fun, [...]

Please, be attentive to the above comment of @J-M-L and write literate codes which you understand and other understand. Look at Post#4 --

LightuC:
And if you want to go full bananas:

@LightuC has totally avoided floating point calculation just to save 2 bytes memory space at the exchange by offering literate/illiterate/clumsy? codes.

Is it not more than a fun which @J-M-L have predicted in advance?

Follow simple rules to create simple program:

  1. Write line by line on a piece of paper using simple text how you want to solve your problem.
  2. Convert the text line of Step-1 into programming codes.
  3. Upload the skeytch and check that it works.
  4. Now, try to reduce the number of code lines of Step-3 using control structures like while-do, do-while, for(), if-else, functions, and nested structures.

GolamMostafa:
@LightuC has totally avoided floating point calculation just to save 2 bytes memory space at the exchange by offering literate/illiterate/clumsy? codes.

Actually, over 1000 bytes of program memory space and 2 bytes of ram.
Why do you think the code is "illiterate/clumsy"? Because it is longer? Because there are 3 functions?

The first function calls printADCValueAsVoltage, you can stop there, because this is all you need to know. It prints an adc value as a voltage.
If you want to go deeper, the next function calls adcToMillivolt and millivoltToVolt. Want to guess what they do? Well, you don't have to, because the names tell you everything.
If, after two weeks or so, you go back to the code I think it is a lot better to be greeted by a loop that "printsADCValuesAsVoltages", rather than a loop where you have to check the loop parameters and decode some number to character conversion. Furthermore, say you want to do this on an arduino that does not use pin 14 as A0. Or were pins A0-5 are not linear between 14-20. You would need to change the loop and look up the pin to number mapping, make sure that the numbers can be converted by the subtraction method and so on and so forth...

So no, I don't think the code of J-M-L is much fun, if you want to switch to a different arduino core, ever want to change the pin assignment, or even understand the code after n weeks of not looking at it. However, it introduces the very usefull F() macro.

GolamMostafa:
@LightuC has totally avoided floating point calculation just to save 2 bytes memory space at the exchange by offering literate/illiterate/clumsy? codes.

Is it not more than a fun which @J-M-L have predicted in advance?

His point was not about the 2 bytes of SRAM, but the 772 bytes you save in FLASH: 2514 bytes versus 3286 in my version

--> I actually find @LightuC solution pretty smart (and fun) and is a way to prove that not using floating point when you don't need to can result in a much smaller code!

EDIT: posted before I actually read @LightuC's answer

J-M-L:
→ I actually find @LightuC solution pretty smart (and fun) and is a way to prove that not using floating point when you don’t need to can result in a much smaller code!

Thanks! This was actually fun to write, however if this really is all the program should do, I’d probably also go with floats. Especially, my implementation isn’t as accurate, because of the conversion from the adc value to millivolts using integer math only.

But this approach might shed some light on another usefull concept: If you have some time critical calculation and are not sure how long it might take (divisions in particular), you can compute an estimation using integer and base 2 math for example. Then, you perform the exact computation, but if it takes too long (past its deadline), you return your estimation. This way you have a result, even if the computation did not complete in time.

LightuC:
Why do you think the code is "illiterate/clumsy"? Because it is longer? Because there are 3 functions?

The OP longed for a sketch that could use less memory space than his one; in response, I posted my sketch of Post#2 and then @J-M-L came up with the phrase 'to have fun' in Post#3. Was my Post#2 a fun or an effort or both?

I had another word (literate) with "illiterate/clumsy", as I believed that someone might find the embedded beauty of your integer math based sketch. That did not mean that I personally disliked it; but, the code lines were missing the necessary comments (why 65, 65536, /2, etc.) which had made it difficult (my perception) for the OP/learner to follow the program logic.

J-M-L:
--> I actually find @LightuC solution pretty smart (and fun) and is a way to prove that not using floating point when you don't need to can result in a much smaller code!

Balanced criticisms satisfy all the stakeholders.

@golam - the goal of the «fun» exercise was to reduce the memory footprint, not to provide the best documented code.

Your value judgment on the readability of the code is hence irrelevant I think - even if in general documenting code makes sense.

Also for anyone with a bit of experience the code structure with functions is quite self documenting and if you go to the bottom of it and have a bit of math background, it’s pretty easy to grasp the idea.

GolamMostafa:
The OP longed for a sketch that could use less memory space than his one; in response, I posted my sketch of Post#2 and then @J-M-L came up with the phrase ‘to have fun’ in Post#3. Was my Post#2 a fun or an effort or both?

As I said, if all the arduino would be doing is reading 6 voltages and sending them over serial, I’d go with your solution (except having another array for the pins).

GolamMostafa:
I had another word (literate) with “illiterate/clumsy”, as I believed that someone might find the embedded beauty of your integer math based sketch. That did not mean that I personally disliked it;

Indeed, you had. Sadly, english is not my native language so I couldn’t quite manage to put “literate”
into the context of the next sentences.

GolamMostafa:
but, the code lines were missing the necessary comments (why 65, 65536, /2, etc.) which had made it difficult (my perception) for the OP/learner to follow the program logic.

You are right. My first step was to not calculate a voltage from the ads value, but millivolts. So essentially, I multiplied the 5/1023 with 1000, resulting in the multiplier of 5000.
Then I thought, that 1023 is awfully close to 1024 and decided to wing it by using 1024 instead of 1023. Then, because of the truncation happening with integer math, I split the division by 1024 in a division of 512 and 2, to check if rounding the result up would be required.

As for the 65/65536: To get volts from millivolts you’ll have to divide the dynamic millivolt number by 1000.

x / 1000 is slow, because the compiler actually has to perform the division. On 8-bit architectures without a barrel shifter, shifting by 8,16,24,… (so on byte boundaries) is faster. I looked for a number, that would make the division fast, so naturally you’d try 256, 65536 and so on. 256 would not help, because it has to be divided by 1000, so I went with 65536 and expanded the equation to

x / 1000 * 65536 /65536

Swapping the divisors gives

x / 65536 * 65536 / 1000

Which is

x / 65536 * 65.536

As it is for display purposes I didn’t made the effort to find an inverse multiplicative for 0.536 and just used 65. Because of truncation, multiplying has to be done first, so:

(x *65) / 65536

To recover the millivolts, multiply the resulting voltage by 1000.

Thank you @LightuC with +1k for giving time to provide exhaustive comments in favor of the rationality of your mathematical expressions of Post#4 intended to acquire, remap, and display the 'input analog voltage' with 3-digit precision without undergoing into floating point math.

When I was asked by my fellow workers if there existed any other alternative, I presented the following codes (as a fun and pleasure) where the very first measurement was multiplied by 1000000 to avoid floating point math and to maintain the required fractional precision.

//unsigned long amplifiedVoltage = adcValue*1000000*(5000/1024)
    amplifiedVoltage = (adcValue*1000000)/1024*5000;//(1000000 / 1024) * 5000 * adcValue;
    Serial.println(amplifiedVoltage, DEC);
    j = 9;
    do
    {
      byte x = amplifiedVoltage % 10;
      amplifiedVoltage = amplifiedVoltage / 10;
      myVolt[j] = x;
      j--;
    }
    while (amplifiedVoltage != 0);
    Serial.print(myVolt[0]); Serial.print('.');
    Serial.print(myVolt[1]); Serial.print(myVolt[2]); Serial.print(myVolt[3]);
    Serial.println();