AVR basics: reading analogue input

It’s an increasingly digital world, but not all information comes packaged neatly in 1s and 0s. Sometimes you have to deal with analogue voltage levels using the microcontroller’s analogue to digital (ADC) converter.

Measuring analogue voltages is made easy in Arduino projects because the IDE comes with a handy analogRead() function. Providing your input voltage does not exceed what the board/microcontroller can take (and they can be very limited in this regard, in some cases going down to 1V or so – read the data sheet), then there’s little to worry about.

And yes, I’m using ‘analogue’ rather than ‘analog’ because I’m from England where English was invented. 😉

But often a whole Arduino board is overkill and you just want to use a simple microcontroller chip. This is the case with some ‘smart’ sensors I plan to use on my Sheldon robot. In brief, the idea is that each sensor will have its own microcontroller that will alert and communicate with another microcontroller or computer further up the hierarchy only if certain conditions are met.

Sharp infrared rangefinder

I decided, as a test run, to start with the classic infra-red rangefinder. I have two, both by Sharp – the GP2Y0A21YK0F which claims to work over distances of 10–80cm and the GP2Y0A02YK0F which is good for 20–150cm. Each sensor is powered by 5V and has an analogue output over the operating range of something like 0.4–3.2V (in the case of the GP2Y0A21YK0F, which is what I chose to use).

This voltage is roughly inversely proportional to distance, although the relationship isn’t quite linear. Calibrating these things to give an exact distance measurement would involve some jiggery-pokery. Luckily, that’s not what I want: I just want a configurable proximity sensor – one where I can set a threshold (or maybe two or three thresholds) that can be modified on the fly through commands from another microcontroller/computer.

Reading the signal

AVR ATTINY84A

But our task here is just to use an AVR microcontroller to read an analogue signal. The chip I chose for this task is not my usual, beloved ATMEGA328P, because that would have been over the top. Instead, I plumped for the ATTINY84A. It has enough GPIO for my needs (an analogue input, a digital output for an interrupt line to the superior node, and serial I/O for communication with the superior node (to send data and receive commands). The serial comms in this case will probably be I2C or SPI because the ATTINY84A lacks a serial UART.

That UARTlessness means that I also can’t output debugging info via the serial port, as we are wont to do in ArduinoLand. But no matter, I just used the debugWire capabilities of Atmel Studio and the AVR chip to check how things are going.

A lot of what I say here will apply to other AVR microcontrollers – just check the data sheet for any specific differences. And, of course, it also applies to the other models in the ATTINYx4A range – ATTINY24A and ATTINY44A.

I’m running the ATTINY84A with its internal 8MHz clock, but using the DIV8 clock divider, so it’s actually running at 1MHz – plenty fast enough for our purposes here. This becomes relevant later, as we’ll see.

Fun with registers

As always, everything is done via the cunning use of registers.

First up, let’s power up the ADC. By default, it’s not running. And Atmel warns that you should shut it down again before putting the microcontroller into power-saving sleep mode. We’re not going to be doing anything fancy like that, so we’ll just switch it on all the time. We control the ADC power with the PRADC bit (actually bit 7) of the Power Reduction Register (PRR). This is normally set to 1, so to power up the ADC we need to clear this bit:

PRR &= ~(1 << PRADC); // Clear ADC bit in power reduction register

Next up is the ADMUX register.

7 6 5 4 3 2 1 0
REFS1 REFS0 MUX5 MUX4 MUX3 MUX2 MUX1 MUX0

There are all kinds of things you can do with this, but the first is to consider is what you’re going to use as a reference voltage. There are essentially three choices:

  • Use Vcc – ie, your main power input for the chip. That’s what we’ll be doing here.
  • Use an external voltage source connected to the AREF pin (PA0) of the chip.
  • Use an internal 1.1V voltage reference.

That third one reminds us why it’s important to know the maximum voltage that your analogue input can stand. The reference is set with the REFS0 and REFS1 bits.

REFS1 REFS0 Voltage reference
0 0 Vcc
0 1 External (AREF)
1 0 Internal 1.1V

I want to use Vcc as the reference, just to make life simple). Arguably, for greater accuracy, I could use a resistor divider to create a 3.3V input to AREF and use that instead. I’ll think about that later, but right now, to get things going, Vcc will do just fine.

That means setting bits 6 and 7 of ADMUX both to 0. Actually, they default to that, so we don’t have to do anything, which is nice. But if you want to be emphatic, you could use:

ADMUX &= ~((1 << REFS0) | (1 << REFS1));

What about those other six bits (5 down to 0, or 5:0 in the lingo)? This is where I  point out that we’re only doing basic ADC stuff here. There are all kinds of clever things the ATTINY is capable of – such as all sorts of reference modes that are way beyond our scope here.

I just want what are called ‘single ended’ inputs – a single, simple input to a pin.

But what we do need to do is tell the ATTINY which pin we’re using for this ADC business. We’re only using one ADC input (the ATTINY88A has no fewer than 8 pins capable of analogue inputs, although not all at the same time), so we can simply tell it in the setup procedure. For this, we write a value for the bits MUX0, MUX1 and MUX2. A value (in binary) of 0b000 selects ADC0 (which is pin PA0). A value of 0b001 selects ADC1 (which is PA1). Can you see a pattern?

I’ve decided to use pin PA3 (aka ADC3), so I can select this with the value 3:

ADMUX |= 0b00000011;

As it happens, though, the standard AVR headers define the macro PA3 to have the value 3, so we could also use:

ADMUX |= PA3;

Setting the speed

There is also a small matter of how fast the ADC runs. To get the full 10-bit resolution (values of 0–1023), the ADC needs an input clock frequency of 50kHz to 200kHz. If you can make do with 8-bit resolution (values of 0–255), then you can have the clock higher (though no more than 1MHz) and take more readings per second.

You use two prescaler bits to set the frequency. And this is done in another important register, the ADC Control and Status Register A (ADCSRA):

7 6 5 4 3 2 1 0
ADEN ADSC ADATE ADIF ADIE ADPS2 ADPS1 ADPS0

The prescaler bits are bits 0–3 which set the ‘divide factor’ from a default of 2 to a maximum of 128. With the clock running at 1MHz on my chip, I went for a factor of 8, which means setting bits ADPS0 and ADPS1 both to 1, giving a clock input to the ADC of 125MHz.

ADCSRA |= ((1 << ADPS0) | (1 << ADPS1));

The last step in the setup is to enable the ADC, using the ADEN enable bit.

ADCSRA |= (1 << ADEN);

Now the ADC is all set. With the sensor powered up and the output line connected to PA3, let’s take some readings.

Taking readings

Converting an incoming voltage to a digital value is not instantaneous. There can be quite a few clock cycles involved. The approach, then, is to instruct the ADC to start a conversion and then keep checking to see if it’s done.

The conversion is triggered by writing a 1 to the ADSC bit of the ADCSRA register. This bit is automatically reset to 0 when the conversion is completed. At that point, the value is stored in two bytes, ADCL (the low byte) and ADCH (the high byte). So here’s a simple routine to read the ADC:

uint16_t val = 0;
ADCSRA |= (1 << ADSC); // Write bit to 1 to start conversion.
// It returns automatically to 0 when conversion is complete.
while((ADCSRA & (1 << ADSC))) {
    // just loop
}
// Analogue value should now be in ADCL (low byte) and ADCH (high byte).
val = ADCL; // read low byte first
val += (ADCH << 8); // then read high byte

Remember, though, that this value is inverted – objects further away will give a lower value, so it might be useful to invert this number:

val = 1023 – val;

Further work

There’s an awful lot more to ADCs, especially if you want accuracy and the ability to calibrate for specific measurements. But this should get you started.

For myself, I’ll probably add that resistor divider to create greater resolution. At the moment, with possible input values of 0.4–3.2V being compared to 5V, I’m really only using two-thirds of that 10-bit resolution. Comparing to 3.3V would pretty much use all of it.

» See all the AVR Basics posts »

Never miss a post

Enter your email address to subscribe to this blog and receive notifications of new posts by email.

2 thoughts on “AVR basics: reading analogue input

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.