Use the Analog to Digital Converter (ADC) to create a crude voltmeterDr Nathan Scott & Dr Hiroyuki Kagawa · July 2002 You should now have an AVR set up on a breadboard. In this section we will use the ADC to read external voltages.
|
[Dr Kagawa, Please insert Japanese here] |

|
Potentiometers are cheap and can be quite accurate. A potentiometer can be used as an input control (a knob to turn), or it can measure an angle in a mechanism. For example a potentiometer can tell you the angle of one robot link with respect to another. As you can see from Figure 1(c), it is also easy to connect a potentiometer to the AVR. This is a good way to get started: we can quickly get some kind of measurement this way. As usual there are various levels of sophistication here and I will explain some of them after we have something simple running. First of all - some limits. The AVR is a delicate electronic device and it can only measure voltages in a certain range. Voltages outside that range could damage the AVR. |
[Dr Kagawa, Please insert Japanese here] |

Figure 2 Acceptable range for analog input to the AVR
|
To protect the AVR we must use a circuit like the one in Figure 3: |
[Dr Kagawa, Please insert Japanese here] |

Figure 3: How to protect the AVR from unacceptable analog input
|
The diodes only conduct current in the direction of the arrow. If the input is between AREF and AGND, the input signal can pass through to the AVR unchanged. However if the input signal goes too high, the top diode will conduct and short the input to the AREF terminal. If the input signal goes negative, the lower diode will conduct and short the input to the AGND terminal. Well - that is the simple version of what happens! Real diodes do not quite work this way. They will not become conductive until the voltage across them is 0.2V or greater. This means that the input is only trimmed off when it goes a little bit outside the acceptable range: |
[Dr Kagawa, Please insert Japanese here] |

Figure 4: Diode protection and ADC conversion result.
|
The data sheet for the AVR says that ADC input must be kept strictly in the range AGND to AREF. However I have found that the AVR seems to survive voltages in the range (AGND - 0.2) to (AREF + 0.2). Sometimes we have to work a little outside the rules, however we take a risk in doing so. Always use a diode circuit like Figure 3 on your ADC inputs (except if you are absolutely in control of the input voltage, for example see the circuit of Figure 1(c) - the AVR cannot possibly see any voltage outside GND..VCC so no diodes are needed). I once foolishly left the diode protection off of a data logger I was building and burned out the ADC on an AVR. I do not even know how the damage happened, it could have been just a stray static discharge. |
[Dr Kagawa, Please insert Japanese here] |
|
|
|

Figure 6: The ADC can sample the voltage on any of the pins of PORTA.
|
ADMUX must be set before each conversion is requested. This will become clear in the code example below. |
|
|
[Dr Kagawa, Please insert Japanese here] |

Figure 5: The ADC result is spread across two bytes called ADCH and ADCL.
|
We have been programming the AVR using a very convenient high-level language (C) and it is easy to forget that the variables with names like ADCH are actually hardware registers. Occasionally we must remember this fact in order to avoid software errors. When we use the ADC, the result can become available at any time (just as an interrupt could happen at any time). Therefore some caution is needed because if our program happens to be reading ADCH, and ADCL is updated by hardware at the same moment, the combination ADCH-ADCL could be read as a mis-matched pair. ADCH could be from one conversion and ADCL could be from another! This might not sound like a big problem but in a robotic application it could lead to control system problems (glitches) that would be hard to trace. The designers of the AVR thought about this problem and implemented a hardware system to eliminate it. In our software we can use ADCH and ADCL as usual provided we follow one simple rule: we must ALWAYS read the value of ADCL first. This is illustrated in Figure 6 below. As always, if you wish to know why, consult the data sheet for the AVR.
|
[Dr Kagawa, Please insert Japanese here] |
int GetADCSample(char channel)
{
char c0, c1;
int result;
ADMUX = channel; // set ADC channel
ADCSR |= BIT(ADSC); // request a conversion
ADCSR |= BIT(ADEN); // enable ADC
// now wait for the flag to go down
while (ADCSR & BIT(ADSC))
; // do nothing, wait for flag that tells us that the conversion is
// done
// read the result, note that we read the LOW byte first
c0 = ADCL;
c1 = ADCH;
result = c0;
result |= c1 << 8;
// disable the ADC
ADCSR &= ~BIT(ADEN);
return result;
}
|
|
Procedure GetADCSample() illustrates a basic sequence of steps for getting an ADC conversion. It's not the only way to do it, but it is a good starting example:
Note that some processor time is wasted in a "polling" loop (the while loop). During this time the program can't really do other things although interrupt handlers still work of course. The conversion will only take 25 microseconds or so and it may be acceptable for the "main thread" to delay for that period, this is a design decision. The following code example is rather long but I hope you will now be able to digest it. It uses several systems that you have learned about in previous tutorials:
You should set up a potentiometer as in Figure 1 so that you can control the voltage on PORTA, PIN0. Protection diodes are not needed in this case because the potentiometer will ensure that the ADC voltage remains in the safe range. |
[Dr Kagawa, Please insert Japanese here] |
/*
Code for a crash course in AVR programming
Example "adc_demo.c"
Dr Nathan Scott, July 2002
Minimum hardware:
1) A 7-segment LED driven off PORTC by 7447 driver. Sending
PORTC = 3 should show the number 3 on the 7-segment LED.
2) A 50kohm potentiometer should be used to control the voltage at
PORTA, PIN0 (ADC0).
3) A MAX232 should be set up for RS232 communication.
Recommended extra hardware:
4) A motor with H-bridge driver should be set up as for the PWM tutorial,
this motor will be controlled by both keyboard and the potentiometer!
*/
#include "adc.h"
#include "delay.h"
#include "uart.h"
// define names for important numbers.
// It is easier to work with the names.
#define kTimerStartCount 0x3E // this is used to control the frequency
// of the overflow interrupt. The counter counts UP from this start value
// and the overflow happens when it gets to 0xFF
// global variable, the number of ticks since the program started
int gTicks;
// how long is a tick? You have control and you can set it to be almost any
// value. The length of a tick in microseconds is approximately
// (prescale constant) * (0xFF - kTimerStartCount)/XTAL_MHZ
char gVoltmeter;
void prompt(char p)
{
putch('\r'); putch('\n'); putch(p);
}
void SetMotorSpeed(char speed)
{
char c;
OCR2 = speed;
// display speed setting on the LED
c = (10 * speed) / 0xFF;
if (c > 9) c = 9; // problem with a rounding error
PORTC = c;
}
// tell the compiler that this is a special block of code that will handle
// the interrupt message "Timer 0 has overflowed"
#pragma interrupt_handler IT_TIMER0_OVF0_interrupt:iv_TIMER0_OVF
void IT_TIMER0_OVF0_interrupt()
{
char buf[6];
int sample;
// prime the TMR0 register with the start count again
TCNT0 = kTimerStartCount;
gTicks++;
if ((gTicks % 100 == 0) && gVoltmeter)
{
sample = GetADCSample(0);
ADCSampleToStr(sample, buf);
// set motor speed
SetMotorSpeed(sample >> 2); // take most significant 8 bits only
putstr(buf);
putstr("\b\b\b\b\b"); // 5 backspace characters, put the cursor at
// the start of the number display so it can over-write
}
}
void Idle()
// This is the code which is called while we are waiting for a character
// to arrive from the terminal host.
{
}
void ObeyCommand(char c)
{
char i;
if ('0' <= c && c <= '9')
{
SetMotorSpeed((0xFF * (c - '0')) / 9);
}
switch (c)
{
case '?': // about
putstr("AVR ready");
// flash the LED a few times
for (i = 0; i < 3; i++)
{
PORTC |= BIT(LED_pin);
DelayMs(20);
PORTC &= ~BIT(LED_pin);
DelayMs(20);
}
break;
case 'v': // enter voltmeter mode
gVoltmeter = 1;
break;
} // end switch
}
void main()
{
char c = 0;
gVoltmeter = 0;
// enable TMR0 overflow interrupt
TIMSK |= BIT(TOIE0);
// we may as well start the global tick count from zero.
// Note that declared variables could contain NOISE until you set them!
gTicks = 0; // configure timer/counter 0 to give us the sample heartbeat.
// Several prescaler options are available to get a wide range of count
// speeds.
TCCR0 |= TMR_prescale_8; // divide main oscillator frequency by 8
// see environment.h for a list of the options
TCNT0 = kTimerStartCount; // this only affects the first heartbeat
// set up PWM using timer 2
// Set up PD7 as an output, this is the pin used by the PWM
DDRD |= BIT(PD7);
// COM21 and COM20 determine whether this is normal PWM
// or inverted. We will select "normal"
TCCR2 |= BIT(COM21) | BIT(PWM2);
TCCR2 |= TMR_prescale_8; // the prescale rate must be set, the
// default value is "stopped"!
// OCR2 = 0x00; // start value for PWM fraction
// set up hardware UART for serial communications. This is where we
// specify the baud rate. See uart.h for more info.
InitUART(UBRR38400);
// set up control over PORTC so we can use it to flash the LED
PORTC = 0; // put a definite start value into the register for PORTC
// - it's best not to leave this to chance
DDRC = 0xFF; // all outputs, drive a single 7-seg display via 7447
SetMotorSpeed(0x80); // half-way, should be approximately
// "off" if a very simple H-bridge driver is used
// initialisation is done, it is time to "flip the switch" by
// setting the Global Interrupt Enable flag
SEI();
// show a welcome string on the terminal
ObeyCommand('?'); // show "about" string
while (1) // endless loop
{
if (!gVoltmeter) prompt('>');
c = ReceiveByte(); // calls Idle() while nothing is happening
if (gVoltmeter) prompt('>'); // otherwise the display can be messy.
// any received byte disables voltmeter (live display) mode
gVoltmeter = 0;
putch(c); // echo (send back to the terminal)
putch(' '); // separate response from echoed command
ObeyCommand(c);
}
}
|
|
What does the program do? Compile and download, and find out! The program has two main modes of operation. When it starts it is in "terminal" mode and you can send commands to it. For example you can type '7' into the terminal window and this will
The other mode of operation is called "voltmeter" mode. To enter this mode, type 'v' into the terminal window. In "voltmeter" mode the TMR0 interrupt rapidly samples PORTA, PIN0, and
The technique of "live" display on the terminal is useful because it allows us to implement complex, dynamic displays without filling the window. In this program it is achieved using the backspace character ('\b' in the C language). The carriage return ('\r') used by itself, i.e. without the newline ('\n') character, can also be used to overwrite a line of text. The program leaves voltmeter mode if any fresh command is given. |
[Dr Kagawa, Please insert Japanese here] |
|
|
AVR input impedanceWhen I first started using the AVR I saw an amazing figure in the data sheet: the input impedance for a DIGITAL INPUT pin is listed as "100M |
|
| So we have to now consider what kind of signal we are really measuring. If it is from a low-impedance source, we can connect it to the AVR with (e.g.) a 500 However if we have a less powerful voltage source, it may be necessary to put an op-amp buffer between it and the AVR. If you want to learn how to do that, refer to Prof. Hoenig's "How to Build & Use Electronic Circuits Without Frustration, Panic, Mountains of Money or an Engineering Degree". A truly inspiring book. I am gradually putting it online but the best parts are not there yet. |