ADC - Analog-to-Digital Conversion

Analog-to-Digital Conversion is yet another nice feature you can get with a PIC. It's basically used to convert a voltage as an analog source (continuous) into a digital number (discrete).

ADC with water...

To better understand ADC, imagine you have some water going out of a pipe, and you'd like to know how many water it goes outside. One approach would be to collect all the water in a bucket, and then measure what you've collected. But what if water flow never ends ? And, more important, what if water flow isn't constant and you want to measure the flow in real-time ?

The answer is ADC. With ADC, you're going to extract samples of water. For instance, you're going to put a little glass for 1 second under the pipe, every ten seconds. Doing the math, you'll be able to know the mean rate of flow.

The faster you'll collect water, the more accurate the rate will be. That is, if you're able to collect 10 glasses of water each second, you'll have a better overview of the rate of water than if you collect 1 glass each ten seconds. This is the process of making a continous flow a discrete, finite value. And this is about resolution, one important property of ADC (and this is also about clock speed...). The higher the resolution, the more accurate the results.

Now, what if the water flow is so high that your glass gets filled before the end of the sample time ? You could use a bigger glass, but let's assume you can't (scenario need...). This means you can't measure any water flow, this one has to be scaled according to your glass. On the contrary, the water flow may be so low samples you extract may not be relevant related to the glass size (only few drops). Fortunately, you can use a smaller glass (yes, scenario need) to scale down your sample. That is about voltage reference, another important property.

Leaving our glass of water, many PICs provide several ADC channels: pins that can do this process, measuring voltage as input. In order to use this peripheral, you'll first have to configure how many ADC channels you want. Then you'll need to specify the resolution, usually using 8 bits (0 to 255), 10 bits (0 to 1024) or even 12 bits (0 to 4096). Finally, you'll have to setup voltage references depending on the voltage spread you plan to measure.

ADC with jallib...

As usual, Microchip PICs offers a wide choice configuring ADC:
  • Not all PICs have ADC module (...)
  • Analog pins are dispatched differently amongst PICs, still for user's sake, they have to be automatically configured as input. We thus need to know, for each PIC, where analog pins are...
  • Some PICs have their analog pins dependent from each other, and some are independent (more on this later)
  • Clock configuration can be different
  • As previously stated, some PICs have 8-bits low resolution ADC module, some have 10-bits high resolution ADC module1
  • Some PICs can have two external voltage references (VRef+ and VRef-), only one voltage reference (Vref+ or Vref-) and some can't handle external voltage references at all
  • (and probably other differences I can't remember :)...

Luckily most of these differences are transparent to users...

Dependent and independent analog pins

OK, let's write some code ! But before this, you have to understand one very important point: some PICs have their analog pins dependent from each other, some PICs have their analog pins independent from each other. "What is this suppose to mean ?" I can hear...

Let's consider two famous PICs: 16F877 and 16F88. 16F877 datasheet explains how to configure the number of analog pins, and vref, setting PCFG bits:

Figure 1. 16F877 ADC channels are controlled by PCFG bits

Want 6 analog pins, no Vref ? Then PCFG bits must be set to 0b1001. What will then be the analog pins ? RA0, RA1, RA2, RA3, RA5 and RE0. "What if I want 7 analog pins, no Vref ?" You can't because you'll get a Vref pin, no choice. "What if I want 2 analog pins being RE1 and RE2 ?" You can't, because there's no such combination. So, for this PIC, analog pins are dependent from each other, driven by a combination. In this case, you'll have to specify:
  • the number of ADC channels you want,
  • and amongst them, the number of Vref channels

Now, let's consider 16F88. In this case, there's no such table:

Figure 2. 16F88 ADC channels are controlled by ANS bits

Mmmh... OK, there are ANS bits, one for each analog pins. Setting an ANS bit to 1 sets the corresponding pin to analog. This means I can set whatever pin I want to be analog. "I can have 3 analog pins, configured on RA0, RA4 and RB6. Freedom !"

Analog pins are independent from each other in this case, you can do what you want. As a consequence, since it's not driven by a combination, you won't be able to specify the number of ADC channels here. Instead, you'll use set_analog_pin() procedure, and if needed, the reverse set_digital_pin() procedure. These procedures takes a analog pin number as argument. Say analog pin AN5 is on pin RB6. To turn this pin as analog, you just have to write set_analog_pin(5), because this is about analog pin AN5, and not RB6.

Remember: as a consequence, these procedures don't exist when analog pins are dependent as in our first case.
CAUTION:
it's not because there are PCFG bits that PICs have dependent analog pins. Some have PCFG bits which act exactly the same as ANS bits (like some of recent 18F)
Tip: how to know if your PIC has dependent or independent pins ? First have a look at its datasheet, if you can a table like the one for 16F877, there are dependent. Also, if you configure a PIC with dependent pins as if it was one with independent pins (and vice-versa), you'll get an error. Finally, if you get an error like: "Unable to configure ADC channels. Configuration is supposed to be done using ANS bits but it seems there's no ANS bits for this PIC. Maybe your PIC isn't supported, please report !", or the like, well, this is not a normal situation, so as stated, please report !

Once configured, using ADC is easy. You'll find adc_read() and adc_read_low_res() functions, for respectively read ADC in high and low resolution. Because low resolution is coded on 8-bits, adc_read() returns a byte as the result. adc_read_low_res() returns a word.

Example with 16F877, dependent analog pins

The following examples briefly explains how to setup ADC module when analog pins are dependent from each other, using PIC 16F877.

The following diagram is here to help knowing where analog pins (blue) are and where Vref pins (red) are:

Figure 3. Analog pins and Vref pins on 16F877

Example 1: 16F877, with only one analog pin, no external voltage reference

-- beginning is about configuring the chip
-- this is the same for all examples for about 18F877
include 16f877
-- setup clock running @20MHz
pragma target OSC HS
pragma target clock 20_000_000
-- no watchdog
pragma target WDT  disabled
pragma target LVP  disabled
enable_digital_io()
include delay

-- ok, now setup serial, we'll use this
-- to get ADC measures
const serial_hw_baudrate = 19_200
include serial_hardware
serial_hw_init()


-- ok, now let's configure ADC
-- we want to measure using low resolution
-- (that's our choice, we could use high resolution as well)
const bit ADC_HIGH_RESOLUTION = false
-- we said we want 1 analog channel...
const byte ADC_NCHANNEL = 1
-- and no external voltage reference
const byte ADC_NVREF = ADC_NO_EXT_VREF
-- now we can include the library
-- note it's now named "adc", not "adc_hardware" anymore
include adc
-- and run the initialization step
adc_init()


-- will periodically send those chars
var byte measure
forever loop
   -- get ADC result, on channel 0
   -- this means we're currently reading on pin RA0/AN0 !
   measure = adc_read_low_res(0)
   -- send it back through serial
   serial_hw_write(measure)

   -- and sleep a litte to prevent flooding serial...
   delay_1ms(200)
end loop

Example 2: 16F877, with 5 analog pins, 1 external voltage reference, that is, Vref+

This is almost the same as before, except we now want 5 (analog pins) + 1 (Vref) = 6 ADC channels (yes, I consider Vref+ pin as an ADC channel).

The beginning is the same, here's just the part about ADC configuration and readings:

const bit ADC_HIGH_RESOLUTION = false
-- our 6 ADC channel
const byte ADC_NCHANNEL = 6
-- and one Vref pin
const byte ADC_NVREF = ADC_VREF_POS
-- the two parameters could be read as:
-- "I want 6 ADC channels, amongst which 1 will be
-- reserved for Vref, and the 5 remaining ones will be
-- analog pins"
include adc
adc_init()

-- will periodically send those chars
var byte measure
forever loop
   -- get ADC result, on channel 0
   -- this means we're currently reading on pin RA0/AN0 !
   measure = adc_read_low_res(0)
   -- send it back through serial
   serial_hw_write(measure)

   -- same for pin RA1/AN1
   measure = adc_read_low_res(1)
   serial_hw_write(measure)

   -- same for pin RA2/AN2
   measure = adc_read_low_res(2)
   serial_hw_write(measure)

   -- pin RA3/AN3 can't be read, since it's Vref+

   -- same for pin RA5/AN4
   -- 4 is from from "AN4" !
   measure = adc_read_low_res(4)
   serial_hw_write(measure)

   -- same for pin RE10/AN5
   measure = adc_read_low_res(5)
   serial_hw_write(measure)

   -- and sleep a litte to prevent flooding serial...
   delay_1ms(200)
end loop 

Example with 16F88, independent analog pins

The following example is about setting up ADC module with PIC 16F88, where analog pins are independent from each other.

The following diagram is here to help knowing where analog pins (blue) are and where Vref pins (red) are:

Figure 4. Analog pins and Vref pins on 16F88

Example 1: 16F88, analog pins on RA0/AN0, RA4/AN4 and RB6/AN5. No external voltage reference.

-- beginning is about configuring the chip
include 16f88
-- We'll use internal oscillator. It work @ 8MHz
pragma target CLOCK     8_000_000
pragma target OSC       INTOSC_NOCLKOUT
OSCCON_IRCF = 0b_111
pragma target WDT       disabled
enable_digital_io()

-- ok, now setup serial, we'll use this
-- to get ADC measures
const serial_hw_baudrate = 19_200
include serial_hardware
serial_hw_init()

-- now configure ADC
const bit ADC_HIGH_RESOLUTION = false
const byte ADC_NVREF = ADC_NO_EXT_VREF
-- we can't specify a number of ADC channel here,
-- or we'll get an error !
include adc
adc_init()
-- now we declare the pin we want as analog !
set_analog_pin(0)  -- RA0/AN0
set_analog_pin(4)  -- RA4/AN4
set_analog_pin(5)  -- RB6/AN5

-- reading is then the same
var byte measure
forever loop

   measure = adc_read_low_res(0)
   serial_hw_write(measure)

   measure = adc_read_low_res(4)
   serial_hw_write(measure)

   measure = adc_read_low_res(5)
   serial_hw_write(measure)

end loop

Whether you would want to turn RB6/AN5 into a digital pin again, you'd just call:

set_digital_pin(5)
1 and some have 12-bits, those aren't currently handled by jallib ADC libraries, That's a restriction.