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).
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.
Luckily most of these differences are transparent to users...
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:
Now, let's consider 16F88. In this case, there's no such table:
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.
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.
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:
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
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:
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)
Attachment | Size |
---|---|
![]() | 90.6 KB |
![]() | 40.05 KB |
![]() | 61.86 KB |
![]() | 54.68 KB |