Use the Analog to Digital Converter (ADC) to create a crude voltmeter

back to main tutorial page

Dr 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.


Analog and digital signals

An analog signal is a voltage which may have any value and can change with time. For example

  • mains electric voltage is AC and is typically 100-240 volts;
  • the output of of a thermocouple, strain gauge bridge or photodetector;
  • the position of a potentiometer set up as in Figure 1.

[Dr Kagawa, Please insert Japanese here]

Figure 1 Using a rotary potentiometer to generate an analog input voltage

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]


ADC electrical connections

You will remember from the first tutorial (hello world) that the AVR has some pins called AREF, AGND and AVCC. We connected these pins to VCC, GND and VCC respectively. The pins are actually

  • AREF = reference voltage for ADC conversions, this is the voltage that corresponds to the highest digital result of an ADC conversion (0x03FF).
  • AGND = ground level for the analog parts of the AVR.
  • AVCC = power supply for the analog parts of the AVR.

A reasonable "start" approach is to connect AVCC and AREF to VCC and AGND to GND. This will be acceptable while you are learning to use the ADC. Later on you may want better performance from the ADC and a more careful design of the power and GND lines will be needed. But - again - let's get something working and then go after higher performance.


ADC multiplex

The AVR actually has only one ADC converter inside it. A multiplex (switch) controls which of the pins of PORTA will be sampled:

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.


ADC bit resolution

A digital number always has a certain length which is measured in bits. For example a single byte has 8 bits, and can thus store 256 distinct numerical values. An 8 bit number is said to have 8 bit resolution. The cheapest and fastest ADC chips available are generally 8 bit. ADCs are available with much higher resolutions up to about 24 bits. However our purpose here is to introduce you to the ADC built into the AVR, and it has a resolution of 10 bits.

A 10 bit digital number can store 1024 distinct numerical values. The AVR ADC result, then, has a voltage resolution of about AREF/1024 or about 5mV. This is actually not quite true as the least significant bits of the 10 bit number tend to be quite noisy. I have found the useful resolution of the ADC to be somewhere between 8 and 9 bits.

A 10 bit number cannot be stored in just one byte. Each ADC result is thus reported as two bytes. However only the lowest 2 bits of the high byte are used:

[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.


Using the ADC (polling method)

The project for this tutorial is "adc.prj" and you might like to open it at this point. There are several source files in the project. One source file is "adc.c" which is like a small library of useful code for working with the ADC. Open adc.c and look at the procedure "GetADCSample(char channel)".

[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;
}
Figure 6: procedure GetADCSample()

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:

  1. Set the ADC channel i.e. which pin of PORTA we wish to measure;
  2. Request an ADC conversion (by setting a flag bit in ADCSR);
  3. Enable the ADC hardware;
  4. Wait until a flag in ADCST shows that the result is ready;
  5. Read the result, note we read ADCL first as explained above.
  6. Disable the ADC hardware if desired (this saves a few microwatts of power which may or may not matter in your particular application).

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);
  }
}
Figure 7 Source code in adc_demo.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

  1. Set the motor speed to 7 (slow clockwise?); and
  2. Show the number 7 on the LED.

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

  1. Sets the motor speed according to the sampled voltage;
  2. Shows the voltage on the terminal window as a "live" display;
  3. Shows the motor speed on the 7-segment LED display.

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]


Exercises

  1. At this stage in the course, you should be making up your own exercises. You should be asking yourself, "can I do THIS?", or "what would happen if I changed THAT?".
  2. For example, you could set yourself the goal of changing the program so that the potentiometer always changes the motor speed, whether or not the program is in "voltmeter" mode. In other words, voltmeter mode could become purely a display mode.
  3. If you are stuck and can't think of an exciting project to do at this point, how about adding features from the int0 tutorial to this program? Can you implement some "buttons" which the user can press instead of the keys on the terminal? For example, can you implement a "run/stop" button for the motor?
  4. Can you expand the program so that PORTA, PIN1 is also sampled, and the new value controls something different from PIN0? For example, can you make PIN0 control the LED readout, and PIN1 will control the motor?
  5. Advanced exercise: the ADC has its own interrupt and when this is enabled it is possible to respond to the event "an ADC sample is available". Modify the program so that the GetADCSample() procedure is not used. Instead the TMR0 interrupt should request an ADC sample and the ADC interrupt procedure should display the sample result and change the motor speed.

AVR input impedance

When 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". That is a very high resistance, much higher than you can even measure with an ordinary ohmmeter. But I wasn't reading the data sheet carefully - I didn't notice that this value was for DIGITAL type inputs only. It was NOT the impedance of the ADC inputs! For years I built circuits like the one in Figure 3 above, but with a 100k resistor, because I thought that the impedance of the AVR was essentially infinite, so the resistance value was not important. I reasoned that the AVR could never draw any current from the measured voltage, so there could never be any voltage drop on the input resistor, however large it was. But I was wrong and eventually noticed that my measurements were not always the best. The actual input impedance of the AVR is more like 1 to 5 k. That makes a big difference. If you have a high impedance signal source then the AVR might distort the measurement by drawing current from the source and causing an unwanted voltage drop.

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 resistor, and the measurements will be fine. An example is measuring a nice healthy new alkaline battery voltage. A fresh battery can supply quite a lot of current so it will have no problem supplying a few mA to the AVR's ADC.

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.


Dr Nathan Scott · nscott@mech.uwa.edu.au