analogRead alternatives (Part1)

Part 1, 2

Long story short: I never met wizards before Valerio knocked on quai-lab‘s door. And we all committed to help him to ‘augment’ the experience of his magic puppets. However, it’s long road from puppets to bytes and bits. But Valerio is not this sort of person who gives up easily and this post is a quite exhaustive answer to a question that I recently rose. Enjoy !

The analogRead is a very popular function among the basic standard functions from the Arduino programming environment. In just one command and one parameter it allows data acquisition from any from the 6 (or more) analog ports from an Arduino board. Fair enough for most applications. However, some applications may require some tweaks or fine tuning that analogRead cannot stand. This post describes what is behind the scene of analog to digital conversion on Arduino UNOs. Using the nuts and bolts from this post, you will be able to tailor your own analog readings using lower resolution and lower latency. These tunings may be very useful in applications where reading accuracy is not issue while reading time is an issue: e.g. reading a potentiometer position within a loop.

Achieving the ultimate performances has a price, and the price to pay is few lines of code. I decided to split the ADC reading operation in two distinctive parts: ADC initialization and ADC reading. Initializing the ADC is requires careful programming of 3 registers:  ADCSRA,  ADCSRB and ADMUX. Next are the few, almost self explanatory, lines of code that you may insert in your ADC initialization routine:

uint8_t InitADC(uint8_t channel, float reference, uint8_t resolution, uint8_t prescaler)
{
 /* Reset register contents */
 ADCSRA = 0; /* Consequently clears ADEN */
 ADCSRB = 0; 
 ADMUX = 0;
 /* Set voltage reference */
 if (reference == REF_VCC) {
 ADMUX |= (1 << REFS0); /* DEFAULT 5 V */
 } else if (reference == REF_INTERNAL) {
 ADMUX |= (1 << REFS1) | (1 << REFS0); /* REF_INTERNAL 1.1 V */
 }
 _reference = reference;
 _resolution = resolution;
 if (_resolution == RES_LOW) {
 /* left align ADC value to 8 bits from ADCH register */
 ADMUX |= (1 << ADLAR);
 }
 /* Set channel */ 
 ADMUX |= channel;
 /* Set pre-scaler */
 if (prescaler < 2) {
 prescaler = 2;
 } else if(prescaler > 7) {
 prescaler = 7; 
 }
 _prescaler = prescaler;
 ADCSRA |= _prescaler;
 /* Enable ADC */
 ADCSRA |= (1 << ADEN);
 /* Start first conversion */
 ADCSRA |= (1 << ADSC); 
 /* Wait for conversion */
 while (ADCSRA & (1 << ADSC));
}

Firstly, we will reset the content of the 3 registers by writing ‘0x00’s in each of them. Doing so we disable the ADC by clearing the ADEN bit. Then we will set ADMUX content, starting with the reference voltage setting. As most of you know, you may use Vcc (so as to say 5V) or a builtin reference voltage (1.1V) or read the voltage at the Vref input. As this post is intended for people with intermediate knowledge, I skipped the Vref option as it requires a good understanding of ADC in order to prevent irrecoverable mistakes. However it is easy from the table below to adapt the code to your needs:

adc_02

The reference setting is recorded in a global variable (_reference) as it will be needed in future code. In the same register, we will set the bits alignement. Let’s keep it simple. If you require a 0 to 256 range of ADC counts, use this option which I gave the constant name ‘RES_LOW’ for resolution low. Conversly, if you need a 0 to 1024 range of ADC counts, use the ‘RES_HIGH’ value in the resolution parameter. Once again, we will need this parameter for future code so it is recorded in the ‘_resolution’ global variable. Ultimately, we want to decide from which analog port we want to acquire data, that we do through the simplistic ADMUX |= channel; command.

A little bit more sophisticated is the prescaler setting within the ADCSRA register. Prescaling means applying a dividing factor to a reference clock, presently the CPU clock which runs at 16 MHz on an Arduino UNO.

adc_01

As it takes 13 ADC clock cycles per conversion in one shot mode, taking into account the 16 MHz CPU clock, it takes between 1.625 µs up to 104 µs to perform an ADC conversion depending on the prescaler value (from 1 to 7, which turns to a division factor ranging from 2 to 128 as per the above table). This highlights the fact that under the 16 MHz conditions, the choice of a division factor of 2 leads to a sampling frequency of 600+ kHz, which is far beyond the ADC potential. I experimentally determined that for a CPU clock rate of 16 MHz, the lowest permitted prescaler is 2 leading to a division factor of 4 and an ADC sampling rate of 300+ kHz.

You may wonder why we would not use the lowest prescaler as a default value. The reason is that the longer the sampling time from the ADC, the better the signal to noise ratio. In other words, the longer the time spent acquiring signal, the least the influence of noise, and ultimately the better the precision of measurements. So that if we need high precision measurements, we may want to use prescalers up to 7, to the cost of longer sampling times.

Then we want to enabled the ADC and launch a first conversion. In this way, all later conversion will all last 13 ADC clock cycles, while the first ‘blank’ conversion will require 25 ADC clock cycles.

Acquiring data is much simpler in comparison

inline uint16_t ADCRead(void)
{
	uint16_t result;
	/* Start conversion */
	ADCSRA |= (1 << ADSC); 
	/* Wait for conversion */
	while (ADCSRA & (1 << ADSC));
	/* Read digital value */
	if (_resolution == RES_LOW) {
		result = ADCH;
	} else {
		result = ADCL | (ADCH << 8);
	}
	/* Returned value */
	return(result);
}

Writing ADSC bit to one starts the one shot ADC conversion and then we wait until this bit is set back to ‘0’, meaning that the ADC conversion is fully completed. Then we read the ADCx registers according to the previously selected data format (left aligned or not). Just keep in mind that LSBs must be read priori to MSBs. Additionally, we may want to oversample data in order to improve the signal to noise ratio. Here is a complementary function which runs the ADCRead function a predetermined number of times:

uint16_t ADCRead(uint8_t samples)
{
	uint32_t result = 0;
	for (uint8_t i = 0; i < samples; i++) {
		result += ADCRead();
	}
	result /= samples;
	return(result);
}

Next post on same subject

Leave a Reply

You must be logged in to post a comment.