FriendlyWire.com


beginner-friendly electronics tutorials and projects


Discover the joy of understanding electronics!

Understanding analog to digital converters

December 8, 2019 tutorial

When working with microcontrollers we sometimes forget that the real world does not just consist of 1's and 0's. It is what we call analog, and in this article we will learn on how to convert analog values from the real world (like the position of a potentiometer) into a digital number consisting of 1's and 0's.

Analog vs. digital

In the real world we are used to an infinite variety of colors, shapes, sounds, volumes, and much more. On a computer, however, everything has to be represented in terms of logical bits and bytes. Computers use the binary system and therefore everything continuous that exists in the real world has to be mapped to something discrete.

Care for an example? Look at the above sunset. If you have ever seen one in real life, you can imagine the perfect gradient from orange to yellow. But on a computer we only have a finite number of colors. We also only have a finite number of pixels. So if you compare the left “analog” situation to the right “digital” situation, you see that it has been pixelized, or, in other words, discretized.

I am quite aware that this example is not very accurate. The picture above already is on a computer (your computer or phone that you are reading this text from). But I hope you get the idea.

Here is a less pretty and poetic, but more tangible example. Imagine you are building a circuit that is supposed to measure a voltage. It should not just measure ON or OFF, but also the magnitude of a signal. This is what an analog to digital converter (or ADC for short) does. It takes a continuous, real world quantity (like a voltage) and converts into a binary number.

Imagine you want to measure how a signal changes over time:

The solid, green line is the continuous signal that can take many values. The ADC converts that quantity into a discrete number (written at the top in the green font). The white bars visualize that number. You see that the numbers have steps and are not continuous, unlike the green line. You also see that the white bars have a duration. This is very characteristic for ADCs:

  • The resolution of an analog to digital converter tells you how close it can look. In the above diagram we see an example of 8 bit, meaning that the resulting digital number can be from 0 to 255. Read more about bits and bytes here.
  • The reference is the maximum value the signal can take. In the above, the maximum is 255 because the ADC is an 8 bit ADC. If it was a 10 bit ADC, that number would be 1024, and so on. You get it :) In a real world application we typically have to set the reference in form of a voltage. If it is set to, say, 5V, then a signal of 5V will be converted into 255, and a signal of 2.5V will be converted into 127. I hope that makes sense :)
  • The conversion time is visualized by the width of the white bars. It takes the ADC some time to convert a signal into a number, and further below we will understand in more detail why this is so. In general the conversion time and the resolution are inversely proportional. If you need a fast result it will be rather inaccurate, but if you need a super accurate result than the conversion time will be slow.

Basic principle of ADCs

Okay, so now that we have some basic idea of what analog to digital converters do, let us understand next how they do it. Here is a simple example sketch:

Okay, there is a lot going on in this schematic. Click here if you want to learn more about reading schematics. Let's go through the main components before we explain how everything works.

  • The signal is connected at the jumper called U (because here the signal is a voltage).
  • The reference is connected to the jumper REF, and it is also a voltage.
  • C is a capacitor, and R is a resistor.
  • The switch S, when turned left, charges the capacitor, and when the switch is turned to the right, the capacitor is discharged via the resistor R.
  • The integrated circuit IC is a comparator. It has two inputs called + and -. The output of the comparator is 1 when + is larger than -, and 0 when + is less than -. That makes sense, right? It compares the two inputs!
  • And the thing on the very right is a timing circuit that I visualized as a simple clock.

So how does it work? We can divide the process into three stages:

  1. The switch is in the left position, and the capacitor C is charged to the current value of the signal at the U jumper. This is also called acquisition.
  2. When the switch is turned to the right, the capacitor is discharged. Also, the clock is turned on and starts counting. I won't go into the details here, but when you discharge a capacitor C over a resistor R, the voltage cuts in half roughly every 0.7RC seconds. So the capacitor C is being discharged.
  3. When the voltage stored in the capacitor reaches the reference voltage, the comparator kicks in. The signal at + is no longer greater than that at -! This means that the output of the comparator turns ON and stops the timer. This process is called conversion. Now that we have the time it took to reduce the signal from U to REF, we can calculate U if we know the values of REF as well as R and C. The formula is written in the diagram above.

And that's it! Granted, I described it a bit schematically, but in principle that is exactly how it works. And now we can also understand the role played by the resolution and conversion time: the better the accuracy of the reference voltage, and the higher the resolution of the timing circuit, the more accurate the reading will be. Also, the larger the capacitor C, the higher the conversion time. But it is also a trade off: if the capacitor is very large, the acquisition time becomes longer, but the accuracy of the timing circuit does not have to be very good either :)

Using the ADC module of a PIC microcontroller

Okay, let's look at a concrete example! You see, many PIC microcontrollers come with a built-in ADC module. Here we will look at the PIC16F883 that we already know from the binary clock project. Only now we will use it to read out the position of a potentiometer:

Depending on the position of the yellow marker, this potentiometer will have a value of 0-4.7kΩ, and our goal is to convert that position into a number between 0 and 255. All we have to do is connect the middle pin to the ADC input of our PIC16F883. Here you can see the pinout of the PIC16F883:

All ADC inputs are highlighted in orange, and, as you can see, there are many of them! The PIC16F883 only has one built-in ADC module, however, and we have to control with our software (see below) which analog input is connected to the ADC module.

Our little test circuit is very simple: we will convert the position of the potentiometer into a number from 0-1023 (because the PIC16F883's ADC module has a resolution of 10 bit) and then we will use pulse-width modulation to adjust the brightness of an LED accordingly. The PIC16F883's PWM module also has a resolution of 10 bit, so we can just take the 10 bit ADC conversion result and write it into the duty-cycle register of the PWM module.

Here is the quite simple schematic:

Now let's take a look at the important bits of the source code (see the full code in the appendix, and download the main.c file as well as the compiled .hex-file in the resources box).

We configure the PIC16F883 to run on its 4MHz internal crystal so that we can save external components. Then we initialize the ADC module as follows:

	// ADC configuration
    
    // ADC sampling time per bit is 32 * T_osc
	ADCS0 = 0;
	ADCS1 = 1;

	// PORT RA0 is an analog input
	TRISA0 = 1;
    ANS0 = 1;

     // select channel AN0
    CHS0 = 0;
    CHS1 = 0;
    CHS2 = 0;
    CHS3 = 0;
    
	// turn ADC ON
	ADON = 1;
  • In lines 32-34 we set the conversion time for the ADC module, and we set it to the lowest possible speed because this circuit does not have to be very fast, so we can instead aim for accuracy. (Strictly speaking we also don't need accuracy since we are just adjusting the brightness of an LED, but I am just writing what we are doing so that you can decide for yourself what you prefer.)
  • Lines 36-38 set the AN0 pin (pin RA0) to the analog input mode. It is very important to set the ANS0 bit to 1, because otherwise the ADC features are disabled.
  • Then, in lines 40-44, we select channel AN0 to be the active analog input channel. If we wanted to read out instead, say, AN1, we would have to change those lines. Check out page 105 in the PIC16F883 datasheet to learn how.
  • Finally, in line 47, we can turn the ADC module on at last. The reference voltage of the ADC module, if not specified otherwise, is set to +5V automatically. This means that one full revolution of the potentiometer will give the result of 1023.

Then we set up the PWM module, which is identical to what we learned in the PWM article, so I won't repeat it here. And then comes the main loop:

    // main loop
    while (1) {
        
        // read ADC value
        GO = 1; while (GO);
        
        // set the 10-bit duty cycle value
        CCPR1L = ADRESH;
        CCP1X = (ADRESL >> 6) & 1;
        CCP1Y = ADRESL >> 7; 
    
        // wait a bit
        __delay_ms(1);
        
    }
  • This main loop gets repeated over and over and over.
  • In line 69 the magic happens: we tell the ADC to start a conversion! This is very simple, all we have to do is set the GO to 1. After that we wait in a loop until the GO bit is zero again, which happens after a successful conversion.
  • This while (GO); command is OK for this little example, but it is not recommended for bigger programs: it effectively pauses the entire program until the conversion is completed, and thereby wastes potentially useful computation time and performance. In a better program we should use interrupts, as we already learned in the article on how to create a 1Hz signal with a PIC controller, but I will leave this topic for another time.
  • Okay, so after that the conversion result is stored in the 8-bit variables ADRESH and ADRESL. We need two of them because the result is a number between 0-1023, which does not fit into one byte (that only goes from 0-255). In lines 71-74 we take those values and insert them into the duty-cycle registers of the PWM module (which, this time, are three variables, because the 10-bit duty-cycle value does not fit into a single byte).
  • You might wonder about lines 73-74, why so complicated? This is because the bit 1 is stored in bit no. 7 of ADRESL, and bit 0 is stored in bit no. 6 of ADRESL. In the datasheet this is called the “alignment” of the result, see also page 102, where we find the following picture:

  • By default the PIC16F883 follows the convention in the first line. It is quite common to deal with some bit gymnastics on 8-bit microcontrollers, and if you want to learn more about it, check out our article on how to read binary numbers.
  • And then, finally, in line 77 I put a small break of 1 millisecond so that we can save some power and let the controller rest for a bit in this non-time critical application.

And that's it! Wasn't that easy? Sure, dimming an LED with the turn of a button might sound a bit boring, but remember: you understand everything about it! Isn't that great?

Final thoughts?

And here we are! We have understood how we can convert analog signals from the real world into a sequence of 1's and 0's that can easily be digested by microcontrollers. This little tutorial may be a bit boring on its own, granted, but just imagine the possibilities of what we can do with this new knowledge:

  • We can have new and interesting input devices with potentiometers.
  • We can measure voltages, resistances, brightness, volumes, currents, and all other kinds of analog signals and let our PIC microcontroller react to it!
  • Everything that has not a clear 1 or 0 as a value has just become accessible to us!

Thanks for reading this tutorial, and I hope you have found it useful. Please, as always, reach out on social media if something was not clear enough, or if you want to learn more!

Appendix: The full source code

Here you can find the full source code. The code (as well as the .hex file) are also available for download in the resources box.

/*
 * File:   main.c
 * Author: boos
 *
 * Created on December 8, 2019, 9:26 PM
 */

// CONFIG1
#pragma config FOSC = INTRC_NOCLKOUT// Oscillator Selection bits (INTOSCIO oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled and can be enabled by SWDTEN bit of the WDTCON register)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = ON       // RE3/MCLR pin function select bit (RE3/MCLR pin function is MCLR)
#pragma config CP = OFF         // Code Protection bit (Program memory code protection is disabled)
#pragma config CPD = OFF        // Data Code Protection bit (Data memory code protection is disabled)
#pragma config BOREN = OFF      // Brown Out Reset Selection bits (BOR disabled)
#pragma config IESO = OFF       // Internal External Switchover bit (Internal/External Switchover mode is disabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enabled bit (Fail-Safe Clock Monitor is disabled)
#pragma config LVP = OFF        // Low Voltage Programming Enable bit (RB3 pin has digital I/O, HV on MCLR must be used for programming)

// CONFIG2
#pragma config BOR4V = BOR40V   // Brown-out Reset Selection bit (Brown-out Reset set to 4.0V)
#pragma config WRT = OFF        // Flash Program Memory Self Write Enable bits (Write protection off)

#include 

#define _XTAL_FREQ 4000000

void main(void) {
    
    // ADC configuration
    
    // ADC sampling time per bit is 32 * T_osc
	ADCS0 = 0;
	ADCS1 = 1;

	// PORT RA0 is an analog input
	TRISA0 = 1;
    ANS0 = 1;

     // select channel AN0
    CHS0 = 0;
    CHS1 = 0;
    CHS2 = 0;
    CHS3 = 0;
    
	// turn ADC ON
	ADON = 1;
    
    // PWM settings
    
    // PORT RC2 is an output
    TRISC2 = 0;
    
    // start PWM module
    CCP1CON = 0b1100;
    
    // set upper limit of TIMER2 (sets the PWM frequency)
    PR2 = 0xff;

    // set the prescaler of TIMER2 to 1:1 (bits no. 0 and 1)
    // (00 = 1:1, 01 = 1:4, 1x = 1:16)
    // and activate TIMER2 (bit no. 2)
    T2CON = 0b100;
    
    // main loop
    while (1) {
        
        // read ADC value
        GO = 1; while (GO);
        
        // set the 10-bit duty cycle value
        CCPR1L = ADRESH;
        CCP1X = (ADRESL >> 6) & 1;
        CCP1Y = ADRESL >> 7; 
    
        // wait a bit
        __delay_ms(1);
        
    }
    
    return;
    
}

About FriendlyWire

Beginner-friendly electronics tutorials and projects. Discover the joy of electronics! Keep reading.

Let's build a community

How did you get interested in electronics? What do you want to learn? Connect and share your story!

Tag Cloud

  • tutorial
  • analog signals
  • analog to digital converter
  • ADC
  • conversion time
  • reference
  • resolution
  • beginner-friendly
  • breadboard
  • schematics
  • microcontroller