You are on page 1of 42

© Gooligum Electronics 2014 www.gooligum.com.

au

Introduction to PIC Programming


Programming Enhanced Mid-Range PICs in C

by David Meiklejohn, Gooligum Electronics

Lesson 11: Analog-to-Digital Conversion and Simple Filtering

We saw in lesson 8 that a comparator can be used to respond to an analog signal being above or below a
specific threshold. In other cases, the precise value of the input is important and you need to measure, or
digitise it, so that your code can process a digital representation of the signal’s value.
This lesson explains how to use the analog-to-digital converter (ADC) available on most enhanced mid-range
PICs to read analog inputs, converting them to digital values you can operate on. It then shows how a simple
moving-average filter can be implemented in C. The final example implements a simple light meter, with
the light level smoothed, scaled and shown as two decimal digits, using 7-segment LED displays.
In summary, this lesson covers:
 Using the ADC module to read analog inputs
 Using the fixed voltage reference with the ADC
 ADC operation in sleep mode
 ADC interrupts
 Hexadecimal output on 7-segment displays
 Calculating a moving average to implement a simple filter
with examples implemented using XC8 (running in “Free mode”).

Analog-to-Digital Converter
The analog-to-digital converter (ADC) on the 16F1824 allows us to measure analog input voltages to a
resolution of 10 bits, within a range defined by positive and negative reference voltages.
An input equal to the negative reference voltage (VSS1 or an external reference) will read as 0, while an input
equal to the positive reference voltage (VDD, an external reference, or an internal fixed reference)
corresponds to an ADC output of 1024 (= 210).
Eight analog input pins are available: AN0 to AN7, shared with digital pins RA0-2, RA4 and RC0-3.
However, since there is only one ADC module, only one input can be read (or converted) at once.

The analog-to-digital conversion is performed through a process of successive approximation, in which the
result is determined one bit at a time. This process is driven by a conversion clock, and the time to complete

1
usually VSS = 0 V

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 1
© Gooligum Electronics 2014 www.gooligum.com.au

each bit conversion is referred to as TAD. The complete 10-bit conversion requires 11 TAD periods (one to
start the conversion process, and then one for each bit of the result).
For example, if TAD = 2 µs, the full conversion will be completed in 11 × 2 µs = 22 µs.
The conversion clock is derived from either the processor clock (FOSC) or an internal RC oscillator (FRC)2.

The ADC conversion clock is selected by the ADCS<2:0> bits in the ADCON1 register:
Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
ADCON1 ADFM ADCS<2:0> – ADNREF ADPREF<1:0>

In the table on the right, FOSC is the processor ADCS<2:0> conversion clock TAD
clock speed, and TCY is the instruction cycle
period. 000 FOSC / 2 TCY / 2

For accurate conversions, the conversion clock 001 FOSC / 8 TCY × 2


must be selected such that TAD is at least 1 µs 010 FOSC / 32 TCY × 8
and no more than 9 µs.
011 FRC 1 µs – 6 µs
One way to achieve that would be to always
select the ADC’s internal RC clock, FRC. 100 FOSC / 4 TCY
That’s ok – it’s a safe option – but notice how 101 FOSC / 16 TCY × 4
variable FRC can be. If you select the internal 110 FOSC / 64 TCY × 16
RC clock, Tad could be anything up to 6 µs.
That means that the conversion could take up to 111 FRC 1 µs – 6 µs
66 µs (11 × 6 µs = 66 µs).
Normally, you want to complete the conversions as quickly as possible, so choosing a conversion clock
which makes Tad as small as possible, but not below 1 µs, is usually preferable. For this reason, the ADC’s
internal RC clock should only be used if you want to perform conversions while the device is in sleep mode,
when the main processor clock (FOSC) is turned off, as we’ll see later.

For most of the examples in this tutorial series, the processor has been clocked at 500 kHz, so for those
examples, FOSC = 500 kHz and TCY = 8 µs. In that case, the best choice is ADCS = 000, giving a
conversion clock of FOSC / 2 and TAD = TCY / 2 = 4 µs:
ADCON1bits.ADCS = 0b000; // Tad = 2*Tosc
// = 4 us (with Fosc = 500 kHz)

That is the closest we can come to the minimum of 1 µs, given a 500 kHz processor clock, but if the
processor was clocked at 32 MHz, you could select the FOSC / 32 option, for TAD = 1 µs.
But remember, if in doubt you can choose FRC (ADCS = x11). It won’t always give you the fastest
conversions, and conversion times won’t be consistent, but it will always work.
The result of the conversion is a 10-bit value, which is linearly and proportionally related to the analog input
being measured, with a maximum error of ±1 lsb (least significant bit) – an accuracy of better than 0.1 %.

2
This is separate from the internal RC oscillators (available as a processor clock source) described in lesson 7.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 2
© Gooligum Electronics 2014 www.gooligum.com.au

As mentioned, an ADC output of 0 corresponds to an input voltage equal to the negative reference, while an
input voltage equal to the positive reference corresponds to an ADC output of 1024 (= 210). However, the
maximum value that can be represented by 10 bits is 1023 (= 210-1), so the maximum input level that can be
measured, corresponding to the full-scale ADC output of 1023, is slightly (1/1024) less than the positive
reference voltage.
Since a 10-bit value won’t fit into a single 8-bit register, the result is stored in two registers: ADRESH
(holding the high part of the result) and ADRESL (the low part).
The XC8 compiler makes these registers available as 8-bit variables (‘ADRESH’ and ‘ADRESL’), as usual, and
also makes the 10-bit result available as a single 16-bit ‘ADRES’ variable.

There is more than one way to store a 10-bit value in two 8-bit registers, and the 16F1824’s ADC module
provides two ways to do it, selected by the ADFM bit.

If ADFM = 0, the result is left-justified, with the most significant eight bits of the result in ADRESH, and the
least significant two bits of the result in the upper two bits of ADRESL:
ADRESH ADRESL

R9 R8 R7 R6 R5 R4 R3 R2 R1 R0 0 0 0 0 0 0

The unused six bits in ADRESL read as ‘0’.


This format is useful when you are not concerned with the full 10-bit resolution; you can simply treat
ADRESH as holding an 8-bit result, and ignore the least significant two bits held in ADRESL.

If ADFM = 1, the result is right-justified, with the least significant eight bits of the result in ADRESL, and
the most significant two bits of the result in the lower two bits of ADRESH:
ADRESH ADRESL

0 0 0 0 0 0 R9 R8 R7 R6 R5 R4 R3 R2 R1 R0

The unused six bits in ADRESH read as ‘0’.


The right-justified format is useful when you want to perform calculations using the full 10-bit result, using
normal 16-bit arithmetic operations, as we will see in the next lesson.

The ADPREF bits select the positive voltage reference:

ADPREF<1:0> ADC Positive Voltage Reference The positive voltage reference can be taken
from VDD, the fixed voltage reference
00 VDD module (see lesson 11), or the VREF+ pin,
01 reserved (do not use) allowing you to reference the ADC to an
external voltage reference. The VREF+ pin
10 VREF+ pin (shared with RA1) is shared with RA1 on the 16F1824, so to
11 FVR (ADC output) use it as the ADC positive voltage reference
you must configure the pin as an analog
input.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 3
© Gooligum Electronics 2014 www.gooligum.com.au

The ADNREF bit selects the negative voltage reference:

ADNREF ADC Negative Voltage Reference Similarly, the negative voltage reference can
be taken from either VSS or an external
0 VSS (usually ground) reference connected to the VREF- pin, which
1 VREF- pin (shared with RA0) on the 16F1824 is shared with RA0, so to
use it with the ADC you must configure the
RA0 pin as an analog input.
An external voltage reference may be more accurate and stable than VDD, improving the accuracy of the
ADC result. Or it may be that the input you are measuring is, by nature, some fraction of another voltage,
and you need to read the fraction instead of the absolute value.
Note that, on the 16F1824, the difference between the positive and negative voltage references must be at
least 1.8 V for correct ADC operation.
This means that, if you select the FVR module as the positive voltage reference, it must be configured to
generate a reference voltage, supplied to the ADC, of either 2.048 V or 4.096 V – the 1.024 V output is too
low to use as the ADC reference.

The other features of the analog-to-digital converter are controlled by the ADCON0 register:

Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0

ADCON0 – CHS<4:0> GO/ DONE ADON

Having selected an appropriate ADC clock rate, voltage reference, and result format, you must select which
input channel to read, or sample, using the CHS bits3:

CHS<4:0> ADC channel Although most of the ADC channels


correspond to the analog input pins,
00000 AN0 pin (shared with RA0) AN0 to AN7, it is also possible to
00001 AN1 pin (shared with RA1) select the fixed voltage reference as an
ADC input. As we’ll see later, this is
00010 AN2 pin (shared with RA2) useful for (indirectly) measuring the
00011 AN3 pin (shared with RA4) supply voltage, VDD.
00100 AN4 pin (shared with RC0) Similarly, sampling the DAC output
(see lesson 9) allows us to (indirectly)
00101 AN5 pin (shared with RC1) measure some fraction (defined by the
00110 AN6 pin (shared with RC2) DAC output ratio) of the voltage on the
VREF+ pin.
00111 AN7 pin (shared with RC3)
Finally, as noted in lesson 9, the
11101 Temperature indicator 16F1824 also includes a temperature
11110 DAC output indicator module. Its output can be
sampled by the ADC, allowing the
11111 FVR (ADC output) device’s temperature to be determined;
see the data sheet for details.

3
CHS values not shown here do not correspond to a valid input channel. Many larger enhanced mid-range PICs have
more ADC input channels available, such as the 16F1828 where CHS = 01000 selects the AN8 pin, for example.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 4
© Gooligum Electronics 2014 www.gooligum.com.au

Before an input pin is selected as an input channel for the ADC, it should be configured as an analog input.
As we’ve seen before, all pins that can be configured as analog inputs will be configured as analog inputs at
power-on, and you must explicitly disable the analog configuration on a pin if you wish to use it for digital
I/O. This because, if a pin is configured as a digital input, it will draw excessive current if the input voltage
is not at a digital “high” or “low” level, i.e. somewhere in-between. Thus, the safe, low-current option is to
default to analog behaviour and to leave it up to the user program to enable digital inputs only on those pins
known to be digital.
Since the default is that all analog inputs are in analog mode at power-on, we don’t really have to select
which inputs will be analog – it’s more a case of turning off analog mode for any pins which you need to use
as digital inputs. It’s good practice to get into the habit of turning off analog mode on any pin you’re not
using as an analog input (whether for a comparator or ADC); it makes it easier to avoid problems later.
As explained in lessons 1 and 7, analog input mode for each analog-capable pin is selected via the analog
select registers:

Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0


ANSELA ANSA4 ANSA2 ANSA1 ANSA0
ANSELC ANSC3 ANSC2 ANSC1 ANSC0

Setting a bit in these registers places the corresponding analog input into analog mode.
To use any of these pins as digital inputs, they must first be deselected as analog inputs, by clearing the
corresponding analog select bit to ‘0’.

The ADON bit turns the ADC module on or off: ‘1’ to turn it on, ‘0’ to turn it off.
The ADC module is off (ADON = 0) by default, at power-on.

Note: To minimise power consumption, the ADC module should be turned off before entering sleep
mode – unless a conversion is being performed in sleep mode.

The ADC module includes a holding capacitor, CHOLD, which has to be allowed to charge to within ½ LSB
(i.e. 1/2048) of the input voltage, before the conversion commences. During the conversion process, this
capacitor is disconnected from the input (which may be changing) and is used as a fixed “copy” of the input
being sampled, during the successive approximation process.
Therefore, before starting the conversion, you must give this capacitor enough time to charge. This is
referred to as the acquisition or sampling time, TACQ.
The device data sheets include formulas you can use to calculate the minimum T ACQ – one of the main
variables is the source impedance of the input being sampled, although temperature also plays a role.
However, assuming that the source impedance is less than the recommended maximum of 10 kΩ, an
acquisition time of 10 µs is adequate for most enhanced mid-range PICs, including the 16F1824.

After delaying for the required acquisition time, the conversion is then initiated by setting the GO/ DONE bit
in ADCON0 to ‘1’.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 5
© Gooligum Electronics 2014 www.gooligum.com.au

Your code then needs to wait until the GO/ DONE bit has been cleared to ‘0’, which indicates that the
conversion is complete. You can then read the conversion result from the ADRESH and ADRESL registers.
You should copy the result before beginning the next conversion, so that it isn’t overwritten during the
conversion process.

An example will hopefully make these steps clearer.

Example 1: Binary Output


As a simple demonstration of how to use
the ADC, we can use a potentiometer to
provide a variable voltage to an analog
input, and four LEDs to show a 4-bit
binary representation of that value, using
the circuit shown on the right.
To build it using the Gooligum training
board, place a shunt across pins 1 and 2
(‘POT’) of JP24, connecting the 10 kΩ
pot (RP2) to AN0, and close JP16-19,
enabling the LEDs on RC0-3.
If you are using Microchip’s Low Pin
Count Demo Board, the onboard pot and
LEDs are already connected to AN0 and
RC0 – RC3. You only need to ensure
that jumpers JP1-5 are closed.

To make the display meaningful (i.e. a binary representation of the input voltage, corresponding to sixteen
input levels), the top four bits of the ADC result should be copied to the four LEDs.
The bottom six bits of the ADC result are thrown away; they are not significant when we only have four
output bits.

We need to configure RC0-3 as outputs, but it doesn’t hurt to make all of the PORTC pins outputs, by
clearing TRISC. We can leave all the PORTA pins configured as inputs (by default).
However, AN0 is being used as an analog input, so we need to set ANSELA<0>.
// configure ports
TRISC = 0; // PORTC is all outputs (PORTA all inputs)
ANSELAbits.ANSA0 = 1; // select analog mode for RA0
// -> RA0/AN0 is an analog input

We’ll configure the processor to use the internal RC oscillator, and because there is no real need for
additional processor speed in this example, it’s ok to use the default clock speed of 500 kHz – although, as
we’ve done before, it’s good practice to explicitly configure the RC oscillator, to make it clear that the
processor is to be clocked at 500 kHz:
// configure oscillator
OSCCONbits.SCS1 = 1; // select internal clock
OSCCONbits.IRCF = 0b0111; // internal oscillator = 500 kHz

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 6
© Gooligum Electronics 2014 www.gooligum.com.au

Given a 500 kHz processor clock, the best choice of ADC clock is FOSC/2, as explained earlier.
Since we want to use only the top four bits of the result, it is easier to work with them if they are all in the
same register, so it’s best to select the left-justified result format (with ADFM = 0); the top four bits of the
10-bit result will be held in the high nybble (top four bits) of ADRESH.
The potentiometer is connected between 0 V and VDD, so its output (the voltage on AN0) is a fraction of
VDD – it’s not derived from a fixed voltage. Therefore it makes sense, in this example, to configure the
ADC with VSS (0 V) and VDD as the negative and positive voltage references:
// configure ADC
ADCON1bits.ADCS = 0b000; // Tad = 2*Tosc = 4 us (with Fosc = 500 kHz)
ADCON1bits.ADFM = 0; // MSB of result in ADRESH<7>
ADCON1bits.ADNREF = 0; // Vref- is Vss
ADCON1bits.ADPREF = 0b00; // Vref+ is Vdd

We also need to select AN0 as the ADC input channel, and turn the ADC on, with:
ADCON0bits.CHS = 0b00000; // select channel AN0
ADCON0bits.ADON = 1; // turn ADC on

Before starting the conversion, we must wait the required minimum acquisition time, which can be done by:
__delay_us(10); // wait 10 us (acquisition time)

using the __delay_us() macro built into XC8.


.
To begin the conversion, set the GO/ DONE bit:
ADCON0bits.GO = 1; // start conversion

We then wait until the GO/ DONE bit is clear (something that can be done quite succinctly in C):
while (ADCON0bits.GO_nDONE) // wait until done
;

Note that, we’re using two symbols for the GO/ DONE bit, depending on the context: when setting the bit to
start the conversion, the bit-field is referred to as “GO”, but when using it as a flag to check whether the
conversion is complete, it is referred to as “GO_nDONE”. It would be clearer if XC8 provided an “nDONE”
symbol (something that had been available for the older mid-range PICs), but it is still possible to read this as
“set GO to start” and “wait while not done”, which is what the code is doing.
But if you prefer, you can always use the single bit-field name “GO_nDONE” to refer to the GO/ DONE bit in
both contexts.

The upper eight bits of the result of the conversion is available in ADRESH, accessible through the
‘ADRESH’ variable.

Finally we need to copy the upper four bits of the result to the lower four bits of LATC (where the LEDs are
connected).
This means shifting the result four bits to the right, which we can write as:
LATC = ADRESH >> 4; // copy high four bits of result
// to low nybble of output port (LEDs)

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 7
© Gooligum Electronics 2014 www.gooligum.com.au

Complete program
Here is how these code fragments fit together:
/************************************************************************
* *
* Description: Lesson 11, example 1 *
* *
* Demonstrates basic use of ADC *
* *
* Continuously samples analog input, copying value to 4 x LEDs *
* *
*************************************************************************
* *
* Pin assignments: *
* AN0 = voltage to be measured (e.g. pot output or LDR) *
* RC0-3 = output LEDs (RC3 is MSB) *
* *
************************************************************************/

#include <xc.h>

#define _XTAL_FREQ 500000 // oscillator frequency for _delay()

/***** CONFIGURATION *****/


// ext reset, internal oscillator (no clock out), 4xPLL off
#pragma config MCLRE = ON, FOSC = INTOSC, CLKOUTEN = OFF, PLLEN = OFF
// no watchdog timer, brownout resets enabled, low brownout voltage
#pragma config WDTE = OFF, BOREN = ON, BORV = LO
// no power-up timer, no failsafe clock monitor, two-speed start-up disabled
#pragma config PWRTE = OFF, FCMEN = OFF, IESO = OFF
// no code or data protect, no write protection
#pragma config CP = OFF, CPD = OFF, WRT = OFF
// stack resets on, high-voltage programming
#pragma config STVREN = ON, LVP = OFF

/***** MAIN PROGRAM *****/


void main()
{
/*** Initialisation ***/

// configure ports
TRISC = 0; // PORTC is all outputs (PORTA all inputs)
ANSELAbits.ANSA0 = 1; // select analog mode for RA0
// -> RA0/AN0 is an analog input

// configure oscillator
OSCCONbits.SCS1 = 1; // select internal clock
OSCCONbits.IRCF = 0b0111; // internal oscillator = 500 kHz

// configure ADC
ADCON1bits.ADCS = 0b000; // Tad = 2*Tosc = 4 us (with Fosc = 500 kHz)
ADCON1bits.ADFM = 0; // MSB of result in ADRESH<7>
ADCON1bits.ADNREF = 0; // Vref- is Vss
ADCON1bits.ADPREF = 0b00; // Vref+ is Vdd
ADCON0bits.CHS = 0b00000; // select channel AN0
ADCON0bits.ADON = 1; // turn ADC on

/*** Main loop ***/

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 8
© Gooligum Electronics 2014 www.gooligum.com.au

for (;;)
{
// sample analog input
__delay_us(10); // wait 10 us (acquisition time)
ADCON0bits.GO = 1; // start conversion
while (ADCON0bits.GO_nDONE) // wait until done
;

// display result on 4 x LEDs


LATC = ADRESH >> 4; // copy high four bits of result
// to low nybble of output port (LEDs)
}
}

ADC Operation in Sleep Mode


To save power, it is possible to place the PIC into sleep mode, immediately after initiating the conversion.
The device will wake when the conversion is complete; the result can then be accessed in ADRESL and
ADRESH as normal.
As with any other event able to wake an enhanced mid-range PIC from sleep mode, the corresponding
interrupt source must be enabled, which, for the ADC module, is done by setting the ADIE bit in the PIE1
register, and, because the ADC module is a peripheral, also setting the PEIE bit in INTCON.
If you only intend to wake the device from sleep, and do not wish to actually generate an interrupt when the
conversion completes (see the next section), you should ensure that the GIE bit in INTCON is clear, as
usual.
Before starting the conversion, you should clear the ADIF flag in the PIR1 register. This will be set when
the conversion is complete, waking the device from sleep mode. If you fail to clear ADIF, the PIC will wake
immediately, before the conversion is complete, and the conversion result will be incorrect.
Finally, as noted above, the ADC will only operate in sleep mode if the ADC’s internal oscillator, FRC, is
selected as the conversion clock source. This is because the processor clock is stopped while the device is in
sleep mode – and we need the ADC to continue to operate.

Example 2: Sleep Mode


To illustrate how to use the ADC module in sleep mode, we’ll modify the previous example, so that the
device enters sleep immediately after the conversion is started.
Recall that, to operate in sleep mode, the ADC’s internal oscillator must be selected as the conversion clock
source, so the ADC configuration statements become:
// configure ADC
ADCON1bits.ADCS = 0b011; // internal oscillator, Frc
ADCON1bits.ADFM = 0; // MSB of result in ADRESH<7>
ADCON1bits.ADNREF = 0; // Vref- is Vss
ADCON1bits.ADPREF = 0b00; // Vref+ is Vdd
ADCON0bits.CHS = 0b00000; // select channel AN0
ADCON0bits.ADON = 1; // turn ADC on

We also need to enable the ADC interrupt (but not global interrupts), so the device will wake from sleep
when the conversion is complete:
// enable ADC interrupt (for wake on completion)
PIE1bits.ADIE = 1; // enable ADC interrupt
INTCONbits.PEIE = 1; // and peripheral interrupts

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 9
© Gooligum Electronics 2014 www.gooligum.com.au

Within the main loop, we now need to clear the ADC interrupt flag (ADIF), accessible via the bit-field
‘PIR1bits.ADIF’, before initiating the conversion.
After the conversion has been done, the device can then be placed into sleep mode, using the SLEEP()
macro, instead of polling the GO/ DONE flag:
/*** Main loop ***/
for (;;)
{
// sample analog input
__delay_us(10); // wait 10 us (acquisition time)
PIR1bits.ADIF = 0; // clear ADC interrupt flag
ADCON0bits.GO = 1; // start conversion

// sleep until done


SLEEP();

// display result on 4 x LEDs


LATC = ADRESH >> 4; // copy high four bits of result
// to low nybble of output port (LEDs)
}

Of course, if we were serious about saving power, we’d turn off the LEDs before entering sleep mode. With
the LEDs left on, the power saved by using sleep mode is minimal.

ADC Interrupts
As mentioned in the section on sleep mode above, the ADC module can be configured to generate an
interrupt when the analog-to-digital conversion process is complete.
But this isn’t only useful for waking the PIC from sleep; ADC interrupts are an alternative to polling.
Polling the GO/ DONE flag is ok if your program has nothing else to do, but if reading analog inputs is only
one of a number of tasks, polling may be a waste of valuable instruction cycles.
For example, with a 20 MHz processor clock and the FOSC/32 conversion clock selected, TAD = 1.6 µs and
the conversion completes in 11 × 1.6 µs = 17.6 µs = 88 instruction cycles. That is, at higher processor clock
rates, there can be enough time to execute 88 instructions, or more, while the AD conversion completes.

Instead of doing nothing but poll the GO/ DONE flag, it is possible to spend at least some of that conversion
time doing something more useful by enabling the ADC interrupt, initiating the AD conversion, and then
going on with other tasks until the conversion is complete. The ADC interrupt will then be triggered, and
your interrupt service routine (ISR) can immediately read the result.

One possible approach is to have the ISR set a flag, which is polled within the main program loop. When the
main loop detects that the ISR has handled the ADC interrupt, a new conversion is initiated, after waiting the
required acquisition delay. But although this method features in a Microchip application note, it is really no
improvement over polling GO/ DONE directly; most of the processor time is still spent polling a flag.

If your program has to perform other tasks while also reading one or more analog inputs, it doesn’t really
make sense to initiate a new conversion as soon as the last one completes – that approach leaves no time to
do anything else. It is often more appropriate to perform the conversions at a steady rate – more slowly than

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 10
© Gooligum Electronics 2014 www.gooligum.com.au

the ADC module is actually capable of. An ideal way to do that is to use a timer-based interrupt (see lesson
5) to initiate the conversions, for example once every millisecond. Not only does this mean that other tasks
can be completed in between conversions, it also means that there is no need for any additional, explicit,
acquisition delay before initiating each conversion – because we know that, with the conversions spaced
apart, ample acquisition time has elapsed between each conversion.

If you are using a timer-based interrupt to initiate the conversions, it then makes sense to use an ADC
interrupt to process the conversion result. This avoids placing a polling loop within the ISR; something to be
avoided if at all possible. Interrupt service routines should be made as short and sharp as possible, so that
other interrupts, or conditions which the main loop is polling for, can be responded to quickly. Your
program will be more responsive overall, if you can minimise the time spent within ISRs.

And if a timer-based interrupt is already being used for display multiplexing (see lesson 10), it may make
sense to use this same time base (or some multiple of it) to initiate the AD conversions – especially if the
result of the conversion is being output on the multiplexed display; the sample rate can then be synchronised
with the display rate.

To illustrate this, we’ll use two examples.


We’ll start by using a multiplexed 7-segment LED display to show the value of an analog signal,
corresponding to the light level detected by a photocell, with the conversion being done within the main loop
and polling GO/ DONE in the “traditional” way.
Then we’ll implement the same thing, using an ADC interrupt instead of polling.

Example 3: Hexadecimal Output


A binary LED display, as in example 1, is not a very useful form of output. To create a more human-
readable output, we can modify the 7-segment LED circuit from lesson 10, as shown below:

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 11
© Gooligum Electronics 2014 www.gooligum.com.au

To implement this circuit using the Gooligum training board, place shunts:
 across every position (all six of them) of jumper block JP4, connecting segments A-D, F and G to
pins RA0-1 and RC1-4
 in position 1 (‘RA/RB4’) of JP5, connecting segment E to pin RA4
 across pins 2 and 3 (‘RC5’) of JP6, connecting digit 1 to the transistor controlled by RC5
 in jumpers JP8, JP9 and JP10, connecting pins RC5, RA5 and RC0 to their respective transistors
 in position 1 (‘AN2’) of JP25, connecting photocell PH2 to AN2.
All other shunts should be removed.
If you are using Microchip’s Low Pin Count Demo Board, you will need to supply your own display
modules, resistors, transistors and photocell, and connect them to the PIC via the 14-pin header on that
board.

We can adapt the multiplexed 7-segment display code from lesson 10 to display the hexadecimal value.

First, to drive the displays using RC0-RC5 and RA0, RA1, RA4 and RA5, we will configure every pin as a
digital output, except for AN2 which must be configured as an analog input:
// configure ports
TRISC = 0; // configure PORTA and PORTC as all outputs
TRISA = 1<<2; // except RA2/AN2
ANSELAbits.ANSA2 = 1; // select analog mode for RA2
// -> RA2/AN2 is an analog input

To ensure that there are enough processor cycles available to smoothly maintain the multiplexed display,
with plenty of overhead, we’ll run the internal RC oscillator at 8 MHz, as we did in lesson 10.

We also need to configure the ADC:


// configure ADC
ADCON1bits.ADCS = 0b101; // Tad = 16*Tosc = 2 us (with Fosc = 8 MHz)
ADCON1bits.ADFM = 1; // LSB of result in ADRESL<0>
ADCON1bits.ADNREF = 0; // Vref- is Vss
ADCON1bits.ADPREF = 0b00; // Vref+ is Vdd
ADCON0bits.CHS = 0b00010; // select channel AN2
ADCON0bits.ADON = 1; // turn ADC on

In this case it is easiest to access the 10-bit result, for display as three hexadecimal digits, if it is right-
justified (ADFM = 1). The highest hex digit of the result will appear as the lower two bits of ADRESH, and
the bottom two hex digits will be the lower eight bits of the result, in ADRESL.
Note that, with the processor now being clocked at 8 MHz, we need to use a higher conversion clock divider,
to ensure that TAD is at least 1 µs.
ADCS = 101 selects a conversion clock of FOSC/16, giving TAD = TOSC × 16 = 2 µs (with FOSC = 8 MHz),
which is well within spec.

The three-digit display can be maintained in the same way as was done in lesson 10, with Timer0 configured
to generate an interrupt every 2.048 ms, and the ISR displaying each digit in turn, using a “multiplex
counter” variable, ‘mpx_cnt’, to keep track of which digit to display.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 12
© Gooligum Electronics 2014 www.gooligum.com.au

A set of global variables are used to communicate with the ISR:


uint8_t hundreds = 0; // current ADC result (in hex): "hundreds"
uint8_t tens = 0; // "tens"
uint8_t ones = 0; // ones

(explicitly initialised to ensure that they hold defined values, within the expected 0 – 15 range, when the ISR,
which references them, first runs)
The main loop updates these display variables, which hold the three digits which the ISR then outputs to the
3 × 7-segment displays.
Note that since the value displayed is in hexadecimal, “hundreds” stores the number of 0x100s in the result,
not 100s, and “tens” stores 0x10s, not 10s...
The main loop then has the job of performing the analog-to-digital conversion:
// sample analog input
__delay_us(10); // wait 10 us (acquisition time)
ADCON0bits.GO = 1; // start conversion
while (ADCON0bits.GO_nDONE) // wait until done
;

and extracting the hexadecimal digits from the result into the display variables:
// copy result to variables for ISR to display
ones = ADRESL & 0x0F; // get ones digit from low nybble of ADRESL
tens = ADRESL >> 4; // get "tens" digit from high nybble of ADRESL
hundreds = ADRESH; // get "hundreds" digit from ADRESH

Note that there is no need to mask the result copied from ADRESH, because the unused upper six bits of
ADRESH are guaranteed to read a zero; in the right-justified result format, ADRESH will only ever contain
a number between 0 and 3.

The content of these variables is then displayed by the ISR, using code adapted from lesson 10:
// Display current ADC result (in hex) on 3 x 7-segment displays
// mpx_cnt determines current digit to display
//
switch (mpx_cnt)
{
case 0:
set7seg(ones); // output ones digit
ONES_EN = 1; // enable ones display
break;
case 1:
set7seg(tens); // output "tens" digit
TENS_EN = 1; // enable "tens" display
break;
case 2:
set7seg(hundreds); // output "hundreds" digit
HUNDREDS_EN = 1; // enable "hundreds" display
break;
}
// Increment mpx_cnt, to select next digit for next time
mpx_cnt++;
if (mpx_cnt == 3) // reset count if at end of digit sequence
mpx_cnt = 0;

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 13
© Gooligum Electronics 2014 www.gooligum.com.au

The ‘set7seg()’ function is based on that presented in lesson 10, extracting the pattern bits from a lookup
array (now extended to 16 entries) for each port and writing to the corresponding output latch register:
/***** Display digit on 7-segment display *****/
void set7seg(uint8_t digit)
{
// pattern table for 7 segment display on port A
const uint8_t pat7segA[16] = {
// RA4 = E, RA1:0 = FG
0b010010, // 0
[patterns 1-9 go here]
0b010011, // A
0b010011, // b
0b010010, // C
0b010001, // d
0b010011, // E
0b010011 // F
};

// pattern table for 7 segment display on port C


const uint8_t pat7segC[16] = {
// RC4:1 = CDBA
0b011110, // 0
[patterns 1-9 go here]
0b010110, // A
0b011000, // b
0b001010, // C
0b011100, // d
0b001010, // E
0b000010 // F
};

// disable displays
LATA = 0; // clear all digit enable lines on PORTA
LATC = 0; // and PORTC

// lookup and output digit pattern


LATA = pat7segA[digit];
LATC = pat7segC[digit];
}

Alternatively, you could use a single pattern array, instead of having a separate one for each port, and extract
the bits for each port from it, as was done in lesson 10:
/***** Display digit on 7-segment display *****/
void set7seg(uint8_t digit)
{
// pattern table for 7 segment display on ports A and C
const uint8_t pat7seg[16] = {
// RC4:1,RA4,RA1:0 = CDBAEFG
0b1111110, // 0
[patterns 1-9 go here]
0b1011111, // A
0b1100111, // b
0b0101110, // C
0b1110101, // d
0b0101111, // E
0b0001111 // F
};

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 14
© Gooligum Electronics 2014 www.gooligum.com.au

// disable displays
LATA = 0; // clear all digit enable lines on PORTA
LATC = 0; // and PORTC

// lookup pattern bits, extract, and write to port output latches


LATA = (pat7seg[digit] & 1<<2) << 2 | // RA4
(pat7seg[digit] & 0b00000011); // RA0-1
LATC = (pat7seg[digit] >> 2) & 0b011110; // RC1-4
}

Using a single pattern array is certainly shorter, but the more complex extraction process means that the C
compiler will generate longer code.
The code generated by the XC8 compiler (v1.32, running in ‘Free mode’) occupies 183 words when separate
pattern arrays are used, versus 193 words for the single, combined pattern array. If the lookup tables were
longer, it would become more memory efficient to combine them to save array storage memory – but in this
case, using separate arrays is still (a little) more efficient – and much easier to understand.

Another change you could make would be to have the ISR read the ADC result directly, instead of using
global display variables:
// Display current ADC result (in hex) on 3 x 7-segment displays
// mpx_cnt determines current digit to display
//
switch (mpx_cnt)
{
case 0:
set7seg(ADRESL & 0x0F); // output low nybble of ADRESL (ones)
ONES_EN = 1; // enable ones display
break;
case 1:
set7seg(ADRESL >> 4); // output high nybble of ADRESL ("tens")
TENS_EN = 1; // enable "tens" display
break;
case 2:
set7seg(ADRESH); // output ADRESH ("hundreds")
HUNDREDS_EN = 1; // enable "hundreds" display
break;
}

The main loop then does nothing more than continually perform analog-to-digital conversions:
/*** Main loop ***/
for (;;)
{
// sample analog input
__delay_us(10); // wait 10 us (acquisition time)
ADCON0bits.GO = 1; // start conversion
while (ADCON0bits.GO_nDONE) // wait until done
;
}

This is a valid approach (it works, and the code is shorter, and in some ways easier to understand), but goes
against the principle of keeping the ISR code as short as possible. Extracting the hex digits from the ADC
result does not involve a lot of processing; nevertheless, it is normally considered “better practice” to
minimise the amount of processing performed within an ISR, so that it finishes as quickly as possible.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 15
© Gooligum Electronics 2014 www.gooligum.com.au

Complete program
Here is the complete “ADC demo with hexadecimal output” program, using separate pattern arrays and
global display variables:
/************************************************************************
* Description: Lesson 11, example 3a *
* *
* Displays ADC output in hexadecimal on 7-segment LED displays *
* *
* Continuously samples analog input, *
* displaying result as 3 x hex digits on multiplexed 7-seg displays *
* (one pattern lookup array per port) *
* *
*************************************************************************
* *
* Pin assignments: *
* AN2 = voltage to be measured (e.g. pot or LDR) *
* RA0-1,RA4,RC1-4 = 7-segment display bus (common cathode) *
* RC5 = "hundreds" digit enable (active high) *
* RA5 = "tens" digit enable *
* RC0 = ones digit enable *
* *
************************************************************************/

#include <xc.h>
#include <stdint.h>

#define _XTAL_FREQ 8000000 // oscillator frequency for _delay()

/***** CONFIGURATION *****/


// ext reset, internal oscillator (no clock out), 4xPLL off
#pragma config MCLRE = ON, FOSC = INTOSC, CLKOUTEN = OFF, PLLEN = OFF
// no watchdog timer, brownout resets enabled, low brownout voltage
#pragma config WDTE = OFF, BOREN = ON, BORV = LO
// no power-up timer, no failsafe clock monitor, two-speed start-up disabled
#pragma config PWRTE = OFF, FCMEN = OFF, IESO = OFF
// no code or data protect, no write protection
#pragma config CP = OFF, CPD = OFF, WRT = OFF
// stack resets on, high-voltage programming
#pragma config STVREN = ON, LVP = OFF

// Pin assignments
#define HUNDREDS_EN LATCbits.LATC5 // "hundreds" digit enable (RC5)
#define TENS_EN LATAbits.LATA5 // "tens" digit enable (RA5)
#define ONES_EN LATCbits.LATC0 // ones digit enable (RC0)

/***** PROTOTYPES *****/


void set7seg(uint8_t digit); // display digit on 7-segment display

/***** GLOBAL VARIABLES *****/


uint8_t hundreds = 0; // current ADC result (in hex): "hundreds"
uint8_t tens = 0; // "tens"
uint8_t ones = 0; // ones

/***** MAIN PROGRAM *****/


void main()
{

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 16
© Gooligum Electronics 2014 www.gooligum.com.au

/*** Initialisation ***/

// configure ports
TRISC = 0; // configure PORTA and PORTC as all outputs
TRISA = 1<<2; // except RA2/AN2
ANSELAbits.ANSA2 = 1; // select analog mode for RA2
// -> RA2/AN2 is an analog input

// configure oscillator
OSCCONbits.SCS1 = 1; // select internal clock
OSCCONbits.IRCF = 0b1110; // internal oscillator = 8 MHz

// configure Timer0
OPTION_REGbits.TMR0CS = 0; // select timer mode
OPTION_REGbits.PSA = 0; // assign prescaler to Timer0
OPTION_REGbits.PS = 0b011; // prescale = 16
// -> increment TMR0 every 8 us
// -> TMR0 overflows every 2.048 ms

// configure ADC
ADCON1bits.ADCS = 0b101; // Tad = 16*Tosc = 2 us (with Fosc = 8 MHz)
ADCON1bits.ADFM = 1; // LSB of result in ADRESL<0>
ADCON1bits.ADNREF = 0; // Vref- is Vss
ADCON1bits.ADPREF = 0b00; // Vref+ is Vdd
ADCON0bits.CHS = 0b00010; // select channel AN2
ADCON0bits.ADON = 1; // turn ADC on

// enable interrupts
INTCONbits.TMR0IE = 1; // enable Timer0 interrupt
ei(); // enable global interrupts

/*** Main loop ***/


for (;;)
{
// sample analog input
__delay_us(10); // wait 10 us (acquisition time)
ADCON0bits.GO = 1; // start conversion
while (ADCON0bits.GO_nDONE) // wait until done
;

// copy result to variables for ISR to display


ones = ADRESL & 0x0F; // get ones digit from low nybble of ADRESL
tens = ADRESL >> 4; // get "tens" digit from high nybble of ADRESL
hundreds = ADRESH; // get "hundreds" digit from ADRESH
}
}

/***** INTERRUPT SERVICE ROUTINE *****/


void interrupt isr(void)
{
static uint8_t mpx_cnt = 0; // multiplex counter

// *** Service Timer0 interrupt


//
// TMR0 overflows every 2.048 ms
//
// Displays current ADC result (in hex) on 7-segment displays
//
// (only Timer0 interrupts are enabled)

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 17
© Gooligum Electronics 2014 www.gooligum.com.au

INTCONbits.TMR0IF = 0; // clear interrupt flag

// Display current ADC result (in hex) on 3 x 7-segment displays


// mpx_cnt determines current digit to display
//
switch (mpx_cnt)
{
case 0:
set7seg(ones); // output ones digit
ONES_EN = 1; // enable ones display
break;
case 1:
set7seg(tens); // output "tens" digit
TENS_EN = 1; // enable "tens" display
break;
case 2:
set7seg(hundreds); // output "hundreds" digit
HUNDREDS_EN = 1; // enable "hundreds" display
break;
}
// Increment mpx_cnt, to select next digit for next time
mpx_cnt++;
if (mpx_cnt == 3) // reset count if at end of digit sequence
mpx_cnt = 0;
}

/***** FUNCTIONS *****/

/***** Display digit on 7-segment display *****/


void set7seg(uint8_t digit)
{
// pattern table for 7 segment display on port A
const uint8_t pat7segA[16] = {
// RA4 = E, RA1:0 = FG
0b010010, // 0
0b000000, // 1
0b010001, // 2
0b000001, // 3
0b000011, // 4
0b000011, // 5
0b010011, // 6
0b000000, // 7
0b010011, // 8
0b000011, // 9
0b010011, // A
0b010011, // b
0b010010, // C
0b010001, // d
0b010011, // E
0b010011 // F
};

// pattern table for 7 segment display on port C


const uint8_t pat7segC[16] = {
// RC4:1 = CDBA
0b011110, // 0
0b010100, // 1
0b001110, // 2
0b011110, // 3
0b010100, // 4

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 18
© Gooligum Electronics 2014 www.gooligum.com.au

0b011010, // 5
0b011010, // 6
0b010110, // 7
0b011110, // 8
0b011110, // 9
0b010110, // A
0b011000, // b
0b001010, // C
0b011100, // d
0b001010, // E
0b000010 // F
};

// disable displays
LATA = 0; // clear all digit enable lines on PORTA
LATC = 0; // and PORTC

// lookup and output digit pattern


LATA = pat7segA[digit];
LATC = pat7segC[digit];
}

Example 4: ADC Interrupts


Now we’ll modify the last example, to use an ADC interrupt, instead of polling the GO/ DONE flag.
This is done by enabling ADC interrupts, and getting the timer interrupt to initiate the conversion. When the
conversion is complete, an interrupt is triggered, and the ADC interrupt handler copies the result into the
display variables, to be displayed by the timer interrupt, as before.

Most of the code is the same as in the last example.


To enable the ADC interrupt, we expand the interrupt initialisation code:
// enable interrupts
PIR1bits.ADIF = 0; // clear ADC interrupt flag
PIE1bits.ADIE = 1; // enable ADC
INTCONbits.TMR0IE = 1; // Timer0
INTCONbits.PEIE = 1; // peripheral
ei(); // and global interrupts

This is much the same as in the wake-up from sleep example, except that now, because global interrupts are
being enabled, as interrupt will actually be triggered as soon as the ADIF flag is set, which happens
whenever a conversion completes.
Note that, to avoid the ADC interrupt triggering prematurely, ADIF is cleared before the interrupts are
enabled4.

Next we need to make the Timer0 interrupt initiate the analog-to-digital conversions.

4
You may have noticed that we haven’t been clearing the Timer0 interrupt flag, TMR0IF, before enabling the Timer0
interrupt. This is because the Timer0 interrupt occurs asynchronously with the rest of the program; that is, it can
happen at any time. So it doesn’t really matter if it triggers prematurely. In fact, because the initial value of TMR0 is
undefined, the Timer0 interrupt could occur before your code is ready for it, even if you clear TMR0IF. If that’s a
concern, you should load TMR0 with a known value, and clear TMR0IF, before enabling the Timer0 interrupt.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 19
© Gooligum Electronics 2014 www.gooligum.com.au

Given that our display has three digits, with a different digit being displayed each time the Timer0 ISR runs,
the complete display is refreshed every third interrupt. Since the value being displayed is read from the
ADC, there is no point performing the conversions any faster than the display is being refreshed, i.e. every
third Timer0 interrupt.
So, instead of initiating the conversion every time the Timer0 ISR runs, it makes more sense to initiate it
after all three digits have been updated.
That means adding the code to begin the conversion, within the Timer0 ISR, just after the “hundreds” digit
has been displayed:
// display "hundreds" digit
set7seg(hundreds); // output "hundreds" digit
HUNDREDS_EN = 1; // enable "hundreds" display

// initiate next analog-to-digital conversion


ADCON0bits.GO = 1;
break;

Note that, now that the conversions are being initiated every 6 ms, there is no need to add a separate
acquisition time delay; 6 ms is more than long enough (>> 10 µs) for the ADC holding capacitor to charge
prior to each conversion.

Now that we have two interrupt sources, we need to add some code to the ISR, to execute the appropriate
interrupt handler, depending on which interrupt flag has been set. Note that, because more than one interrupt
flag may be set (more than one interrupt source may have triggered since the last time we entered the ISR),
you should test the interrupt flags in priority order.
In this case, we want the display to keep an even brightness, and a delay in reading the conversion result of a
couple of milliseconds won’t be noticeable, so we’ll give priority to the timer interrupt:
// Service all triggered interrupt sources

if (INTCONbits.TMR0IF)
{
// *** Service Timer0 interrupt
}

if (PIR1bits.ADIF)
{
// *** Service ADC interrupt
}

The ADC interrupt handler then consists of the code to read the conversion result, and write it into the
display variables. It is exactly the same code as in the polled example, above, except (as in any interrupt
handler) it begins by clearing the interrupt flag, to show that this interrupt has been processed:
// *** Service ADC interrupt
//
// Conversion is initiated by Timer0 interrupt, every 6 ms
//
PIR1bits.ADIF = 0; // clear interrupt flag

// copy ADC result to display variables


// (to be displayed by Timer0 handler)
ones = ADRESL & 0x0F; // get ones digit from low nybble of ADRESL
tens = ADRESL >> 4; // get "tens" digit from high nybble of ADRESL
hundreds = ADRESH; // get "hundreds" digit from ADRESH

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 20
© Gooligum Electronics 2014 www.gooligum.com.au

This leaves the main loop with nothing to do, which becomes simply:
/*** Main loop ***/
for (;;)
; // do nothing

And that, of course, is the point of using interrupts – all the display and regular input sampling activity runs
in the background, leaving the main loop to perform whichever “foreground” tasks remain, such as
responding to other inputs.

Complete interrupt service routine


Since most of the code is the same as in the previous example, with the changes detailed above, there is no
need to list the complete program here – but it’s worth seeing the new ISR:
/***** INTERRUPT SERVICE ROUTINE *****/
void interrupt isr(void)
{
static uint8_t mpx_cnt = 0; // multiplex counter

// Service all triggered interrupt sources

if (INTCONbits.TMR0IF)
{
// *** Service Timer0 interrupt
//
// TMR0 overflows every 2.048 ms
//
// Displays current ADC result (in hex) on 7-segment displays
//
// Initiates next analog conversion when display refresh is complete
// (every 6 ms)
//
INTCONbits.TMR0IF = 0; // clear interrupt flag

// Display current ADC result (in hex) on 3 x 7-segment displays


// mpx_cnt determines current digit to diplay
//
switch (mpx_cnt)
{
case 0:
// display ones digit
set7seg(ones); // output ones digit
ONES_EN = 1; // enable ones display
break;

case 1:
// display "tens" digit
set7seg(tens); // output "tens" digit
TENS_EN = 1; // enable "tens" display
break;

case 2:
// display "hundreds" digit
set7seg(hundreds); // output "hundreds" digit
HUNDREDS_EN = 1; // enable "hundreds" display

// initiate next analog-to-digital conversion


ADCON0bits.GO = 1;
break;
}

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 21
© Gooligum Electronics 2014 www.gooligum.com.au

// Increment mpx_cnt, to select next digit for next time


mpx_cnt++;
if (mpx_cnt == 3) // reset count if at end of digit sequence
mpx_cnt = 0;
}

if (PIR1bits.ADIF)
{
// *** Service ADC interrupt
//
// Conversion is initiated by Timer0 interrupt, every 6 ms
//
PIR1bits.ADIF = 0; // clear interrupt flag

// copy ADC result to display variables


// (to be displayed by Timer0 handler)
ones = ADRESL & 0x0F; // get ones digit from low nybble of ADRESL
tens = ADRESL >> 4; // get "tens" digit from high nybble of ADRESL
hundreds = ADRESH; // get "hundreds" digit from ADRESH
}
}

Example 5: Measuring Supply Voltage


As mentioned earlier, the fixed voltage reference (see lesson 9) can be sampled by the ADC. This can be
used to infer the supply voltage (actually VDD – VSS5, but to keep this simple we’ll assume VSS = 0 V).
Assuming that VDD = 5.0 V and VSS = 0 V, and that the FVR has been configured to output 2.048 V to the
ADC module, the FVR input channel should read as:
2.048 V ÷ 5.0 V × 1024 = 419
Now if VDD was to fall to, say, 3.5 V, the 0.6 V reference would then read as:
2.048 V ÷ 3.5 V × 1024 = 599
As VDD falls, sampling the fixed voltage reference gives a larger ADC result, since it remains constant as
VDD decreases.
So to check for the power supply voltage falling too low, the value returned by sampling the fixed voltage
reference can be compared with a threshold, allowing a warning to be displayed, or perhaps the device could
be shut down cleanly before power falls too low.

To illustrate this, we can adapt the circuit and program from example 3.
We will display the ADC reading corresponding to the fixed voltage reference as two hex digits, reducing
the number of digits to two because we don’t need more than 8-bit precision in this example.
We’ll also light a “low voltage warning” LED attached to RA2 (as shown on the next page) if VDD falls
below a threshold, which we’ll set at 3.5 V.

5
assuming that VDD and VSS have been selected as the ADC positive and negative voltage references

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 22
© Gooligum Electronics 2014 www.gooligum.com.au

If you are using the Gooligum training board, you should set it up as in the last example, but remove the
shunts from JP10 and JP25 (disconnecting the transistor from RC0 and the photocell from AN2) and close
JP13 (connecting the LED on RA2).

In summary, if we configure the ADC with VDD and VSS as the positive and negative references, we can
infer the value of VDD by sampling the FVR input, and then turn on the LED on RA2 when the FVR value
(as read by the ADC) goes above a threshold, to indicate a low power supply voltage.
To implement this, the code from example 3 can be reused with very little modification (to make the code
easier to follow, we won’t use the ADC interrupt from example 4). We only need to reduce the number of
displays from three to two, and add some code to configure the FVR and turn on the warning LED when the
value read on the FVR input channel is above the threshold.

The ADC can be configured similarly to the previous examples, but because we are now displaying only two
hex digits (the most significant eight bits of the result), it makes more sense to left-justify the result, using
ADRESH as our 8-bit result register, and ignoring the two LSBs in ADRESL.
To measure the fixed voltage reference output, we must select it as the ADC input channel, so our ADC
configuration becomes:
// configure ADC
ADCON1bits.ADCS = 0b101; // Tad = 16*Tosc = 2 us (with Fosc = 8 MHz)
ADCON1bits.ADFM = 0; // MSB of result in ADRESH<7>
ADCON1bits.ADNREF = 0; // Vref- is Vss
ADCON1bits.ADPREF = 0b00; // Vref+ is Vdd
ADCON0bits.CHS = 0b11111; // select FVR input channel
ADCON0bits.ADON = 1; // turn ADC on

Of course, we also need to configure the fixed voltage reference:


// configure fixed voltage reference
FVRCONbits.FVREN = 1; // FVR enabled
FVRCONbits.ADFVR = 0b10; // ADC output is 2x (= 2.048 V)
// -> output 2.048 V to ADC module

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 23
© Gooligum Electronics 2014 www.gooligum.com.au

Note that the FVR must be configured to output a voltage (to the ADC module) that is lower than the
threshold that we want to compare it to – a 4.096 V reference would not be useful if we need to detect the
supply voltage falling below 3.5 V, because the ADC cannot read input voltages that are higher than its
positive reference, and the FVR output cannot in any case exceed the supply voltage.
We’ve chosen a supply voltage threshold of 3.5 V in this example, so a 2.048 V reference voltage is
appropriate here. It would also be possible to use the 1.024 V reference level, but 2.048 V allows for more
accuracy because the corresponding ADC readings are higher – we’re using more of the ADC output range.

The only other change that we need to make to the initialisation code is in the port configuration – we no
longer have any external analog inputs, and RA2 is now a digital output, so we can simply configure every
pin as an output:
// configure ports
TRISA = 0; // configure PORTA and PORTC as all outputs
TRISC = 0;

Since there is no longer a “hundreds” digit, we can drop the ‘hundreds’ variable.

The display routine in the ISR is reduced to two digits, becoming:


// Display current ADC result (in hex) on 2 x 7-segment displays
// mpx_cnt determines current digit to display
//
switch (mpx_cnt)
{
case 0:
// display ones digit
set7seg(ones); // output ones digit
ONES_EN = 1; // enable ones display
break;

case 1:
// display "tens" digit
set7seg(tens); // output "tens" digit
TENS_EN = 1; // enable "tens" display
break;
}
// Increment mpx_cnt, to select next digit for next time
mpx_cnt++;
if (mpx_cnt == 2) // reset count if at end of digit sequence
mpx_cnt = 0;

With only two digits, this is a bit clunky. Instead of incrementing the ‘mpx_cnt’ variable and then resetting
it to 0 after it reaches 1, it would be simpler and neater to simply toggle a single bit. But since we’re
modifying existing code instead of writing new code from scratch, it’s easier to leave it like this, and we
know it works – even if it’s not elegant.
That’s a common experience when adapting old code to a new purpose – it might be “cleaner” to start again,
but it often makes sense to build on what’s tried and true.

The input is sampled within the main loop, in the same way as before, but we now need to add some code to
test for the low supply voltage condition (VDD < 3.5 V).

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 24
© Gooligum Electronics 2014 www.gooligum.com.au

To make the code easier to maintain, we can define the voltage threshold as a constant, so that it can be
easily changed later:
#define MINVDD 3.5 // minimum Vdd (Volts)

It is then tempting to write the low supply voltage test as:


// test for low Vdd
if (ADRESH > 2.048/MINVDD*256) // if measured 2.048 V > threshold
WARN = 1; // light warning LED

Writing it that way makes the code very clear, because the FVR’s output voltage is normally expressed as
2.048 V, not 2048 mV, and it is natural to express the minimum VDD as 3.5 V, instead of 3500 mV.
Note that the value 256 (= 28) is used in the threshold calculation, instead of 1024, because, with a 2-digit
display, we’re only be using eight bits of ADC resolution.

But there is a big problem with this – and it is a very easy mistake to make, when using C with small
microcontrollers. The compiler sees ‘2.048/MINVDD*256’ as a floating-point expression (which, of course,
it is), and implements the comparison as a floating-point operation. To do so, it links a number of floating-
point routines into the code, and generates code to convert ADRESH into floating-point form, passing it to a
floating-point comparison routine. This greatly increases the size of the generated code, blowing out to 388
words of program memory6. Compare that with example 3, which uses three 7-segment displays and lacks
only this ADC comparison routine, but requires only 183 words of program memory. You wouldn’t expect
that adding such a simple routine would more than double the size of the generated program! And normally
it wouldn’t; the only reason the generated code is so large is that floating-point routines have been
inadvertently, and unnecessarily, included into it.

Note: The inadvertent use of floating-point expressions in C programs can lead the C compiler to
unnecessarily link floating-point routines into the object code, significantly increasing the size of
the generated code.

There are a number of ways to overcome this problem, including the use of integer-only expressions, but
surely the simplest method, while maintaining clarity, is to explicitly cast the expression as an integer:
if (ADRESH > (int)(2.048/MINVDD*256)) // if measured 2.048 V > threshold
WARN = 1; // light warning LED

This simple change prevents the compiler from including floating-point code, reducing the size of the
generated code from 388 to only 176 words of program memory!

Finally, note that there’s a slight problem with this approach: whenever a digit is displayed by the ISR, the
LED on RA2 will be extinguished, since the lookup table for PORTA always returns a ‘0’ for bit 2. To
avoid that problem, the ‘set7seg’ function would need to be modified to include logical masking operations
so that RA2 is not overwritten. But it’s not really a significant problem; this “test for low VDD” code runs
much more often than the display update, so although the display code will continually extinguish the
warning LED, it will be lit again the next time that VDD is checked, when the main loop restarts. That is,
when the warning LED is ‘lit’, it will actually be flashing rapidly (being extinguished every 2 ms), but often
enough to make it visible.

6
Using XC8 v1.32 running in ‘Free mode’, with default options.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 25
© Gooligum Electronics 2014 www.gooligum.com.au

A useful “side effect” of this is that we don’t need to include any code to turn off the warning LED when the
supply voltage is above the threshold – the display routine will turn it off for us.

Complete program
Although the changes from example 3 are not extensive, the structure of the program has changed enough to
make it worth including the full listing here:
/************************************************************************
* *
* Description: Lesson 11, example 5b *
* *
* Demonstrates use of fixed reference with ADC to test supply voltage *
* *
* Continuously samples internal 2.048 V reference, *
* displaying result as 2 x hex digits on multiplexed 7-seg displays *
* Turns on warning LED if measurement > threshold *
* (using integer comparison) *
* *
*************************************************************************
* *
* Pin assignments: *
* RA0-1,RA4,RC1-4 = 7-segment display bus (common cathode) *
* RC5 = "tens" digit enable *
* RA5 = ones digit enable *
* RA2 = warning LED *
* *
************************************************************************/

#include <xc.h>
#include <stdint.h>

#define _XTAL_FREQ 8000000 // oscillator frequency for _delay()

/***** CONFIGURATION *****/


// ext reset, internal oscillator (no clock out), 4xPLL off
#pragma config MCLRE = ON, FOSC = INTOSC, CLKOUTEN = OFF, PLLEN = OFF
// no watchdog timer, brownout resets enabled, low brownout voltage
#pragma config WDTE = OFF, BOREN = ON, BORV = LO
// no power-up timer, no failsafe clock monitor, two-speed start-up disabled
#pragma config PWRTE = OFF, FCMEN = OFF, IESO = OFF
// no code or data protect, no write protection
#pragma config CP = OFF, CPD = OFF, WRT = OFF
// stack resets on, high-voltage programming
#pragma config STVREN = ON, LVP = OFF

// Pin assignments
#define TENS_EN LATCbits.LATC5 // "tens" digit enable (RC5)
#define ONES_EN LATAbits.LATA5 // ones digit enable (RA5)
#define WARN LATAbits.LATA2 // warning LED (RA2)

/***** CONSTANTS *****/


#define MINVDD 3.5 // minimum Vdd (Volts)

/***** PROTOTYPES *****/


void set7seg(uint8_t digit); // display digit on 7-segment display

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 26
© Gooligum Electronics 2014 www.gooligum.com.au

/***** GLOBAL VARIABLES *****/


uint8_t tens = 0; // current ADC result (in hex): "tens"
uint8_t ones = 0; // ones

/***** MAIN PROGRAM *****/


void main()
{
/*** Initialisation ***/

// configure ports
TRISA = 0; // configure PORTA and PORTC as all outputs
TRISC = 0;

// configure oscillator
OSCCONbits.SCS1 = 1; // select internal clock
OSCCONbits.IRCF = 0b1110; // internal oscillator = 8 MHz

// configure Timer0
OPTION_REGbits.TMR0CS = 0; // select timer mode
OPTION_REGbits.PSA = 0; // assign prescaler to Timer0
OPTION_REGbits.PS = 0b011; // prescale = 16
// -> increment TMR0 every 8 us
// -> TMR0 overflows every 2.048 ms

// configure fixed voltage reference


FVRCONbits.FVREN = 1; // FVR enabled
FVRCONbits.ADFVR = 0b10; // ADC output is 2x (= 2.048 V)
// -> output 2.048 V to ADC module

// configure ADC
ADCON1bits.ADCS = 0b101; // Tad = 16*Tosc = 2 us (with Fosc = 8 MHz)
ADCON1bits.ADFM = 0; // MSB of result in ADRESH<7>
ADCON1bits.ADNREF = 0; // Vref- is Vss
ADCON1bits.ADPREF = 0b00; // Vref+ is Vdd
ADCON0bits.CHS = 0b11111; // select FVR input channel
ADCON0bits.ADON = 1; // turn ADC on

// enable interrupts
INTCONbits.TMR0IE = 1; // enable Timer0 interrupt
ei(); // enable global interrupts

/*** Main loop ***/


for (;;)
{
// sample fixed voltage reference
__delay_us(10); // wait 10 us (acquisition time)
ADCON0bits.GO = 1; // start conversion
while (ADCON0bits.GO_nDONE) // wait until done
;

// test for low Vdd


if (ADRESH > (int)(2.048/MINVDD*256)) // if measured 2.048 V > threshold
WARN = 1; // light warning LED

// copy result to variables for ISR to display


ones = ADRESH & 0x0F; // get ones digit from low nybble of ADRESH
tens = ADRESH >> 4; // get "tens" digit from high nybble of ADRESH
}
}

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 27
© Gooligum Electronics 2014 www.gooligum.com.au

/***** INTERRUPT SERVICE ROUTINE *****/


void interrupt isr(void)
{
static uint8_t mpx_cnt = 0; // multiplex counter

// *** Service Timer0 interrupt


//
// TMR0 overflows every 2.048 ms
//
// Displays current ADC result (in hex) on 7-segment displays
//
// (only Timer0 interrupts are enabled)
//
INTCONbits.TMR0IF = 0; // clear interrupt flag

// Display current ADC result (in hex) on 2 x 7-segment displays


// mpx_cnt determines current digit to display
//
switch (mpx_cnt)
{
case 0:
// display ones digit
set7seg(ones); // output ones digit
ONES_EN = 1; // enable ones display
break;

case 1:
// display "tens" digit
set7seg(tens); // output "tens" digit
TENS_EN = 1; // enable "tens" display
break;
}
// Increment mpx_cnt, to select next digit for next time
mpx_cnt++;
if (mpx_cnt == 2) // reset count if at end of digit sequence
mpx_cnt = 0;
}

/***** FUNCTIONS *****/

/***** Display digit on 7-segment display *****/


void set7seg(uint8_t digit)
{
// pattern table for 7 segment display on port A
const uint8_t pat7segA[16] = {
// RA4 = E, RA1:0 = FG
0b010010, // 0
0b000000, // 1
0b010001, // 2
0b000001, // 3
0b000011, // 4
0b000011, // 5
0b010011, // 6
0b000000, // 7
0b010011, // 8
0b000011, // 9
0b010011, // A
0b010011, // b
0b010010, // C
0b010001, // d

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 28
© Gooligum Electronics 2014 www.gooligum.com.au

0b010011, // E
0b010011 // F
};

// pattern table for 7 segment display on port C


const uint8_t pat7segC[16] = {
// RC4:1 = CDBA
0b011110, // 0
0b010100, // 1
0b001110, // 2
0b011110, // 3
0b010100, // 4
0b011010, // 5
0b011010, // 6
0b010110, // 7
0b011110, // 8
0b011110, // 9
0b010110, // A
0b011000, // b
0b001010, // C
0b011100, // d
0b001010, // E
0b000010 // F
};

// disable displays
LATA = 0; // clear all digit enable lines on PORTA
LATC = 0; // and PORTC

// lookup and output digit pattern


LATA = pat7segA[digit];
LATC = pat7segC[digit];
}

To test this application, you need to be able to vary VDD.


If you are using a PICkit 3 to power your circuit, you can vary the voltage it supplies by configuring it in
MPLAB – if you are using MPLAB X, open the project properties window, select ‘PICkit 3’, and choose the
‘Power’ option. You can then select various voltage levels from a drop-down list.
Alternatively, you can use a variable power supply.
If you have the Gooligum training board, you can connect your power supply to VDD and ground via pins 15
(‘+V’) and 16 (‘GND’) on the 16-pin expansion header.

You can now start at around 5 V (being careful not exceed 5.5 V, if using a variable supply!) and then
decrease VDD a little at a time.
When you get down to 3.5 V or so, the display should read ‘95’ (hex for 149), and the warning LED should
light – but note that the PICkit 3 does not deliver a very accurate voltage, so if you are using a PICkit 3, you
may see different values at “3.5 V”.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 29
© Gooligum Electronics 2014 www.gooligum.com.au

Example 6: Light meter with decimal output


Most people would find it easier to read the output of the light meter presented above if the display was in
decimal, not hex, with a scale from 0 – 99 instead of 0 – 3FFh.
To demonstrate how to do this, we’ll use a 2-digit version of the circuit, as shown below.

To implement it using the Gooligum training board, place shunts:


 across every position (all six of them) of jumper block JP4, connecting segments A-D, F and G to
pins RA0-1 and RC1-4
 in position 1 (‘RA/RB4’) of JP5, connecting segment E to pin RA4
 across pins 2 and 3 (‘RC5’) of JP6, connecting digit 1 to the transistor controlled by RC5
 in jumpers JP8 and JP9, connecting pins RC5 and RA5 to their respective transistors
 in position 1 (‘AN2’) of JP25, connecting photocell PH2 to AN2.
All other shunts should be removed.

To scale the ADC output from 0 – 1023 to 0 – 99, we must multiply the 10-bit output by 100/1024, and
discard (round down) any remainder.
For example, an ADC output of 512 corresponds to 512×100/1024 = 50, while the full-scale output of 1023
corresponds to 1023×100/1024 = 99.90, which we would round down to 99.
But given that the output consists of only two digits, there is no need to use the full 10-bit ADC resolution.
Instead, we can treat the ADC output as an 8-bit number, by configuring the ADC to left-justify the result,
and using only the eight most-significant-bits in ADRESH (discarding the two LSBs in ADRESL);
This means that, to scale the result to 0 – 99, we can multiply the 8-bit ADC output by 100/256.

Most of the program code can be re-used from the hexadecimal example, above.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 30
© Gooligum Electronics 2014 www.gooligum.com.au

After sampling the analog input, we need to scale the ADC result to 0 – 99, and this scaled result is then
referenced twice; once for each digit. So it makes sense to store the scaled result in a variable, which we can
declare as:
uint8_t adc_dec; // scaled ADC output (0-99)

because this value will always be small enough (≤ 99) to represent using 8 bits.

After sampling the input, the ADC result is scaled as follows:


// scale result to 0-99
adc_dec = ADRESH * 100/256;

However, the XC8 compiler generates smaller code if this is written as:
adc_dec = (unsigned)ADRESH * 100/256;

That is, the 8-bit ADC result in ADRESH is cast as an unsigned integer.

C compilers usually promote smaller integral types (such as ‘char’) to type ‘int’ when they are included in
integer arithmetic calculations. In fact, this behaviour is required by the ANSI C standard.
However, C compilers will generally avoid integral promotion in situations where they can conclude that the
result will be the same if promotion doesn’t occur.
In this case, casting ADRESH as an unsigned integer allows the compiler to optimise its code generation,
because it can avoid promoting the ADC result to a signed integer and using signed multiplication and
division routines; unsigned arithmetic is simpler and therefore requires less code to implement.
Note though that you can’t simply assume that a particular change, like this, will make your code smaller – it
depends on the specific compiler and its optimisation settings. Sometimes you need to try a number of
combinations of type declarations and casting, if you want to generate the smallest possible code.

We then need to extract each digit of the scaled result for display. As we saw in lesson 10, this can be done
using the integer division (/) and modulus (%) operators:
// extract digits of scaled result for ISR to display
ones = (unsigned)adc_dec%10;
tens = (unsigned)adc_dec/10;

Again, the adc_dec variable has been cast as an unsigned integer in each expression, to optimise code
generation.

There is a subtle problem with this approach to updating the display variables.
These modulus and division operations take some time to run – around 250 µs each7. What if the interrupt,
which display the current value of these variables, runs after “ones” has been updated, but before “tens” is
updated? The display might then be inconsistent.
For example, suppose that the value being displayed changes from ‘39’ to ‘40’. The new “ones” value of
‘0’ will be calculated before the new “tens” value of ‘4’, so there is a chance that the display will briefly
show the incorrect value ‘30’ while the “tens” calculations complete.

7
with an 8 MHz clock and using the (unoptimised) generated by XC8 in ‘Free mode’

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 31
© Gooligum Electronics 2014 www.gooligum.com.au

That’s not really a problem in this example, especially when you consider that the display interrupt only
updates one digit at a time anyway (so momentary inconsistencies in the output have always been possible
with the multiplexing approach that we’ve been using), and any such brief display inconsistencies, or
“glitches” will not be apparent. But in some applications it may be important that the display variables (or
equivalent) always be consistent, and to avoid potential problems it’s best to get into “good habits” early,
even when not strictly necessary.

We can avoid this potential problem by temporarily disabling interrupts while the display variables are being
updated.
This can be done by placing these statements within a pair of directives (macros provided by XC8) to disable
and then re-enable interrupts:
// disable interrupts during display variable update
// (stops ISR attempting to display out-of-range intermediate results)
di();

// extract digits of scaled result for ISR to display


// (to be displayed by ISR)
ones = (unsigned)adc_dec%10;
tens = (unsigned)adc_dec/10;

// re-enable interrupts
ei();

However, if you do take this approach, you may find that, although the program works, the display
brightness is uneven. Given the significant time required for these calculations to complete, there is a chance
that, when TMR0 overflows, interrupts will be disabled, and the Timer0 interrupt will not run until these
calculations are complete. This means that one digit could be displayed for up to 2.5 ms, while the other is
displayed for only 1.5 ms. For a steady, even display, each digit must be displayed for the same amount of
time, and that will only happen if the timer interrupt is being run regularly – every 2 ms, or very close to it.
The answer is to perform the (slow) calculations while interrupts are enabled, extracting the decimal digits
from the scaled result into variables such as ‘adc_ones’ and ‘adc_tens’, and to then quickly copy the
extracted digits to separate variables, such as ‘dsp_ones’ and ‘dsp_tens’, for the ISR to display.
Although we don’t really need to disable interrupts while copying the extracted digits to the display
variables, it is good practice to do so, to ensure that the display variables are always completely consistent
when the display interrupt accesses them:
// extract digits of scaled result
adc_ones = (unsigned)adc_dec%10;
adc_tens = (unsigned)adc_dec/10;

// disable interrupts during display variable and port update


// (ensures consistent display)
di();

// copy digits to ISR display variables


dsp_ones = adc_ones;
dsp_tens = adc_tens;

// re-enable interrupts
ei();

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 32
© Gooligum Electronics 2014 www.gooligum.com.au

Main program listing


Here is the new main() function, showing where these additions fit in:
/***** MAIN PROGRAM *****/
void main()
{
uint8_t adc_dec; // scaled ADC output (0-99)
uint8_t adc_tens; // as extracted digits: tens
uint8_t adc_ones; // ones

/*** Initialisation ***/

// configure ports
TRISC = 0; // configure PORTA and PORTC as all outputs
TRISA = 1<<2; // except RA2/AN2
ANSELAbits.ANSA2 = 1; // select analog mode for RA2
// -> RA2/AN2 is an analog input

// configure oscillator
OSCCONbits.SCS1 = 1; // select internal clock
OSCCONbits.IRCF = 0b1110; // internal oscillator = 8 MHz

// configure Timer0
OPTION_REGbits.TMR0CS = 0; // select timer mode
OPTION_REGbits.PSA = 0; // assign prescaler to Timer0
OPTION_REGbits.PS = 0b011; // prescale = 16
// -> increment TMR0 every 8 us
// -> TMR0 overflows every 2.048 ms

// configure ADC
ADCON1bits.ADCS = 0b101; // Tad = 16*Tosc = 2 us (with Fosc = 8 MHz)
ADCON1bits.ADFM = 0; // MSB of result in ADRESH<7>
ADCON1bits.ADNREF = 0; // Vref- is Vss
ADCON1bits.ADPREF = 0b00; // Vref+ is Vdd
ADCON0bits.CHS = 0b00010; // select channel AN2
ADCON0bits.ADON = 1; // turn ADC on

// enable interrupts
INTCONbits.TMR0IE = 1; // enable Timer0 interrupt
ei(); // enable global interrupts

/*** Main loop ***/


for (;;)
{
// sample analog input
__delay_us(10); // wait 10 us (acquisition time)
ADCON0bits.GO = 1; // start conversion
while (ADCON0bits.GO_nDONE) // wait until done
;

// scale 8-bit ADC result to 0-99


adc_dec = (unsigned)ADRESH * 100/256;

// extract digits of scaled result


adc_ones = (unsigned)adc_dec%10;
adc_tens = (unsigned)adc_dec/10;

// disable interrupts during display variable and port update


// (ensures consistent display)

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 33
© Gooligum Electronics 2014 www.gooligum.com.au

di();

// copy digits to ISR display variables


dsp_ones = adc_ones;
dsp_tens = adc_tens;

// re-enable interrupts
ei();
}
}

Example 7: Light meter with smoothed decimal output


A problem with the decimal-output example above (and the previous hexadecimal-output example) is that
that the display can become unreadable in flickering light, such as that produced by fluorescent lamps.
These flicker at 50 or 60 Hz – too fast for the human eye to notice, but not too quickly for our simple light
meter, which displays a new light level 244 times per second.
One potential solution might be to reduce the sampling rate, to say one sample per second, so that the
changes become slow enough for a human to see. But that’s not ideal; the display would still jitter
significantly, since some samples would be taken when the illumination was high and others when it was
low.
Instead of using a single raw sample, it is often better to smooth the results by implementing a filter based on
a number of samples over time (a time series). Many filter algorithms exist, with various characteristics.
One that is particularly easy to implement is the simple moving average, also known as a box filter. This is
simply the mean value of the last N samples. It is important to average enough samples to produce a smooth
result, and to maintain a fast response time, a new average should be calculated every time a new sample is
read. For example, you could keep the last ten samples, and then to calculate the simple moving average by
adding all the sample values and then dividing by ten. Whenever a new sample is read, it is added to the list,
the oldest sample is discarded, and the calculation is repeated. In fact, it is not necessary to repeat all the
additions; it is only necessary to subtract the oldest value (the sample being discarded) and to add the new
sample value.
Sometimes it makes more sense to give additional weight to more recent samples, so that the moving average
more closely tracks the most recent input. A number of forms of weighting can be used, including arithmetic
and exponential, which require more calculation. But a simple moving average is sufficient here.

We’ve been using a timer interrupt running every 2 ms (approx), and if we use this interrupt to initiate the
conversions, we will need to average at least ten samples (10 × 2 ms = 20 ms) to smooth a 50 Hz cycle. A
longer window would be better; several times the cycle period would ensure that cyclic variations are
smoothed out. On the other hand, if the window is too long, the meter will not be responsive enough.
Fifty samples, spaced 2 ms apart, would give us a window of 50 × 2 ms = 100 ms. That seems to be a
reasonable choice – 0.1 sec isn’t too long to wait for the display to fully reflect a changed light level.

In the previous example, we discarded the lower two bits of the ADC’s 10-bit result, because we were only
displaying an integer from 0 to 99 and dealing with 8-bit numbers made the calculations easier. But in this
example, we’ll work with the full 10-bit results, maintaining more resolution than the display is able to show,
simply to demonstrate how it can be done.
This means that, if each10-bit sample is stored as a 16-bit value, we need to allocate two bytes for each
sample in the array, or buffer. For example, storing 50 samples would require 100 bytes of storage – which
shouldn’t be a problem, even after allowing for C compiler overheads.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 34
© Gooligum Electronics 2014 www.gooligum.com.au

Storing the samples and maintaining the running total should be done within the ADC interrupt handler,
because it handles the ADC results as they become available.
In theory, calculating the scaled average from the running total could be done in the main loop, outside the
ISR. That’s a valid option and would often be the best choice, because calculations take time and it’s usually
best to keep the ISR as short as possible, so that there is enough time to perform other tasks. But in this
example, there are no other interrupts or events for the program to respond to; it doesn’t matter if a lot of
time is spent within an ISR, performing calculations. Since it simplifies the code a little, we’ll perform the
scaling calculation within the ADC interrupt handler in this example.

Performing the averaging, scaling and digit extraction within the ADC interrupt handler means that the
running total, scaled average, and of course the sample buffer itself as well as the index used to reference the
current sample can be declared as local variables within the ISR:
// variables used by ADC interrupt to calculate moving average
static uint16_t smp_buf[NSAMPLES]; // sample buffer
// (cleared by startup code)
static uint8_t s = 0; // index into sample array
static uint32_t adc_sum = 0; // sum of samples in buffer
uint8_t adc_dec; // scaled average ADC output (0-99)

where ‘NSAMPLES’ is a symbol defined toward the start of the code, to make it easier to modify the number
of samples stored in the array:
#define NSAMPLES 50 // size of sample array

As NSAMPLES is increased, at some point the smp_buf[] array will become too big to fit into memory
alongside the other variables and the compiler’s working space, and you will see an error message like:
could not find space (... bytes) for variable ...

Using XC8 v1.32 in ‘Free mode’ with the PIC16F1824 and the code presented below, the maximum array
size is 106 samples, which, at 212 bytes, is about the maximum that could reasonably be expected.

The running total, adc_sum, is declared as an unsigned 32-bit integer because, given that the buffer may
need to store 100 or more samples and that each sample value can be up to 1023, the total could potentially
be 100,000 or more – too large to be represented by a 16-bit integer.
However, if you are prepared to limit the number of samples to no more than 64, you could declare adc_sum
as a 16-bit variable, because 64 × 1023 = 65,472, which is less than 216 – saving a couple of bytes of memory
and making the generated code a little smaller. You should then make it clear in a comment when defining
‘NSAMPLES’ that the maximum number of samples is 64.
But since we do have plenty of data and program memory available, we don’t need to worry about restricting
the number of samples and we can make adc_sum a 32-bit variable.

Note that the sample buffer, index and running total are declared as static, because they have to retain
their values from one interrupt to the next.

Note also that the index and running total are cleared to zero as part of the variable declaration. The sample
array also has to be cleared before it can be used, so that the running total is correct (if the running total is
initially zero, the array elements must initially sum to zero – easiest to ensure if they are all initially equal to
zero). But there is no need to include explicit code to clear the array.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 35
© Gooligum Electronics 2014 www.gooligum.com.au

By default, the XC8 compiler adds runtime code which, among other things, clears all uninitialised non-auto
(global or static) variables, including arrays.
If you are using MPLAB X, you will find this option within the “XC8 linker” category of the project
properties (File → Project Properties, or click on the Project Properties button on the left side of the project
dashboard), as shown below:

If the “Clear bss” runtime option is selected, the compiler-provided runtime code will clear all the global and
static variables.

Within the ADC interrupt handler, we must store the new sample and update the running total, as follows:
// store current ADC result and update running total
adc_sum -= smp_buf[s]; // subtract old sample from running total
smp_buf[s] = ADRES; // save new sample
adc_sum += smp_buf[s]; // and add it to running total

Note that we’ve used the 16-bit variable ‘ADRES’, which provides access the 10-bit ADC result held in the
ADRESH and ADRESL registers.

The above code assumes that the sample buffer index (‘s’) is pointing to the current sample. This will be
true when the program starts, because ‘s’ is initialised to zero, but having processed the current sample, we
need to increment ‘s’ to reference the next sample, ready for the next time the ADC handler runs:
// advance index to reference next sample
if (++s == NSAMPLES)
s = 0;

Equivalently, this could have been written as:


s++; // increment sample index
if (s == NSAMPLES) // if end of buffer is reached
s = 0; // reset index to start of buffer

The compiler will actually generate the same code in both cases, but the first form is shorter and just as
easily understood when you are familiar with C.

Next, we need to calculate the scaled average.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 36
© Gooligum Electronics 2014 www.gooligum.com.au

The average is equal to the running total divided by the number of samples in the buffer, and could be
calculated as:
adc_avg = adc_sum / NSAMPLES

This average value will be in the range 0 – 1023.


To scale it to the range 0 – 99, multiply it by 100 / 1024:
adc_dec = adc_avg * 100 / 1024

But there is no need to actually declare and use an intermediary variable such as ‘adc_avg’.
Instead, the averaging and scaling operations could be combined in a single expression, such as:
adc_dec = (adc_sum / NSAMPLES) * 100 / 1024;
or:
adc_dec = adc_sum * 100 / (1024 * NSAMPLES);

The second form is preferable, if we are striving to preserve as much resolution as possible, because if the
division is done first, only the integer part of the quotient is preserved and any remainder is lost, because we
are using integer arithmetic. Order of evaluation can be very important in integer arithmetic, and this can be
source of errors and confusion.
For example, suppose adc_sum = 103 and NSAMPLES = 10.
Then:
adc_dec = (adc_sum / NSAMPLES) * 100 / 1024
= (103 / 10) * 100 / 1024
= (10) * 100 / 1024
= 1000 / 1024
= 0

because 103/10 evaluates to 10 using integer arithmetic and 1000/1024 evaluates to 0 (the remainder
being thrown away in both cases).
But:
adc_dec = adc_sum * 100 / (1024 * NSAMPLES)
= 103 * 100 / (1024 * 10)
= 10300 / 10240
= 1

Using floating point (real number) arithmetic, the correct answer is 1.006. As you can see, the second
integer expression gives a more accurate result, because we’re not losing information when the division is
done.

But there is still a trap for the unwary:


By default XC8 will perform calculations using 16-bit ‘int’ types, the constant ‘1024’ is treated as an ‘int’
and the expression ‘1024*NSAMPLES’ is evaluated as a 16-bit integer constant.
That’s a problem if ‘NSAMPLES’ is greater than 31. If NSAMPLES = 50, then 1024*NSAMPLES = 51200,
which is too big to be expressed as a signed 16-bit integer.
The compiler won’t even warn you that there is a problem; XC8 (v1.32, running in ‘free mode’) silently
assigns the value ‘0’ to adc_dec if NSAMPLES > 31.
To avoid this, the constant ‘1024’ can be specified as a 32-bit quantity by appending an ‘L’ to it:
adc_dec = adc_sum * 100 / (1024L * NSAMPLES);

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 37
© Gooligum Electronics 2014 www.gooligum.com.au

The calculation will then be performed correctly, using 32-bit values throughout.
Similarly, if you had declared ‘adc_sum’ as a 16-bit integer (as discussed earlier), the compiler would by
default use 16-bit arithmetic to evaluate the expression “adc_sum*100”. To avoid that calculation
overflowing, you should cast ‘adc_sum’ as a 32-bit integer in this expression, or append an ‘L’ to the
constant ‘100’ to specify that it is a 32-bit quantity, to tell the compiler to use 32-bit arithmetic when
evaluating this expression.

Note that XC8 generates more efficient code to implement this expression if NSAMPLES is a power of two
(such as 32 or 64), because division can then be performed by a series of binary right-shifts.

We can then extract the decimal digits of the scaled average for display, as before:
// extract digits of scaled result for Timer0 handler to display
dsp_ones = (unsigned)adc_dec%10;
dsp_tens = (unsigned)adc_dec/10;

The rest of the program is essentially the same as in the ADC interrupt example, above.

Complete program
Here is the complete source code for the XC8 version of the “ADC demo with averaged decimal output”
program, showing where these code fragments fit in:
/************************************************************************
* Description: Lesson 11, example 7 *
* *
* Displays smoothed ADC output in decimal on 2x7-segment LED display *
* *
* Samples analog input every 2 ms, averages last N (e.g. 50) samples, *
* scales result to 0 - 99 and displays as 2 x decimal digits *
* on multiplexed 7-seg displays *
* *
*************************************************************************
* *
* Pin assignments: *
* AN2 = voltage to be measured (e.g. pot or LDR) *
* RA0-1,RA4,RC1-4 = 7-segment display bus (common cathode) *
* RC5 = tens digit enable (active high) *
* RA5 = ones digit enable *
* *
************************************************************************/

#include <xc.h>
#include <stdint.h>

#define _XTAL_FREQ 8000000 // oscillator frequency for _delay()

/***** CONFIGURATION *****/


// ext reset, internal oscillator (no clock out), 4xPLL off
#pragma config MCLRE = ON, FOSC = INTOSC, CLKOUTEN = OFF, PLLEN = OFF
// no watchdog timer, brownout resets enabled, low brownout voltage
#pragma config WDTE = OFF, BOREN = ON, BORV = LO
// no power-up timer, no failsafe clock monitor, two-speed start-up disabled
#pragma config PWRTE = OFF, FCMEN = OFF, IESO = OFF

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 38
© Gooligum Electronics 2014 www.gooligum.com.au

// no code or data protect, no write protection


#pragma config CP = OFF, CPD = OFF, WRT = OFF
// stack resets on, high-voltage programming
#pragma config STVREN = ON, LVP = OFF

// Pin assignments
#define TENS_EN LATCbits.LATC5 // "tens" digit enable (RC5)
#define ONES_EN LATAbits.LATA5 // ones digit enable (RA5)

/***** CONSTANTS *****/


#define NSAMPLES 50 // size of sample array

/***** PROTOTYPES *****/


void set7seg(uint8_t digit); // display digit on 7-segment display

/***** GLOBAL VARIABLES *****/


// current output in decimal (displayed by
ISR):
uint8_t dsp_tens = 0; // tens
uint8_t dsp_ones = 0; // ones

/***** MAIN PROGRAM *****/


void main()
{
/*** Initialisation ***/

// configure ports
TRISC = 0; // configure PORTA and PORTC as all outputs
TRISA = 1<<2; // except RA2/AN2
ANSELAbits.ANSA2 = 1; // select analog mode for RA2
// -> RA2/AN2 is an analog input

// configure oscillator
OSCCONbits.SCS1 = 1; // select internal clock
OSCCONbits.IRCF = 0b1110; // internal oscillator = 8 MHz

// configure Timer0
OPTION_REGbits.TMR0CS = 0; // select timer mode
OPTION_REGbits.PSA = 0; // assign prescaler to Timer0
OPTION_REGbits.PS = 0b011; // prescale = 16
// -> increment TMR0 every 8 us
// -> TMR0 overflows every 2.048 ms

// configure ADC
ADCON1bits.ADCS = 0b101; // Tad = 16*Tosc = 2 us (with Fosc = 8 MHz)
ADCON1bits.ADFM = 1; // LSB of result in ADRESL<0>
ADCON1bits.ADNREF = 0; // Vref- is Vss
ADCON1bits.ADPREF = 0b00; // Vref+ is Vdd
ADCON0bits.CHS = 0b00010; // select channel AN2
ADCON0bits.ADON = 1; // turn ADC on

// enable interrupts
PIR1bits.ADIF = 0; // clear ADC interrupt flag
PIE1bits.ADIE = 1; // enable ADC
INTCONbits.TMR0IE = 1; // Timer0
INTCONbits.PEIE = 1; // peripheral
ei(); // and global interrupts

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 39
© Gooligum Electronics 2014 www.gooligum.com.au

/*** Main loop ***/


for (;;)
; // do nothing
}

/***** INTERRUPT SERVICE ROUTINE *****/


void interrupt isr(void)
{
// variables used by timer0 interrupt
static uint8_t mpx_cnt = 0; // multiplex counter

// variables used by ADC interrupt to calculate moving average


static uint16_t smp_buf[NSAMPLES]; // sample buffer
// (cleared by startup code)
static uint8_t s = 0; // index into sample array
static uint32_t adc_sum = 0; // sum of samples in buffer
uint8_t adc_dec; // scaled average ADC output (0-99)

// Service all triggered interrupt sources

if (INTCONbits.TMR0IF)
{
// *** Service Timer0 interrupt
//
// TMR0 overflows every 2.048 ms
//
// Displays current averaged and scaled ADC result (in dec)
// on 7-segment displays
//
// Initiates next analog conversion
//
INTCONbits.TMR0IF = 0; // clear interrupt flag

// Display current averaged ADC result on 2 x 7-segment displays


// mpx_cnt determines current digit to display
//
switch (mpx_cnt)
{
case 0:
// display ones digit
set7seg(dsp_ones); // output ones digit
ONES_EN = 1; // enable ones display
break;

case 1:
// display tens digit
set7seg(dsp_tens); // output tens digit
TENS_EN = 1; // enable tens display
break;
}
// Increment mpx_cnt, to select next digit for next time
mpx_cnt++;
if (mpx_cnt == 2) // reset count if at end of digit sequence
mpx_cnt = 0;

// initiate next analog-to-digital conversion


ADCON0bits.GO = 1;
}

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 40
© Gooligum Electronics 2014 www.gooligum.com.au

if (PIR1bits.ADIF)
{
// *** Service ADC interrupt
//
// Conversion is initiated by Timer0 interrupt, every 2 ms
//
// Updates moving average with each ADC result
//
PIR1bits.ADIF = 0; // clear interrupt flag

// store current ADC result and update running total


adc_sum -= smp_buf[s]; // subtract old sample from running total
smp_buf[s] = ADRES; // save new sample
adc_sum += smp_buf[s]; // and add it to running total

// advance index to reference next sample


if (++s == NSAMPLES)
s = 0;

// scale running total to 0-99 for display


adc_dec = adc_sum * 100 / (1024L * NSAMPLES);

// extract digits of scaled result for Timer0 handler to display


dsp_ones = (unsigned)adc_dec%10;
dsp_tens = (unsigned)adc_dec/10;
}
}

/***** FUNCTIONS *****/

/***** Display digit on 7-segment display *****/


void set7seg(uint8_t digit)
{
// pattern table for 7 segment display on port A
const uint8_t pat7segA[10] = {
// RA4 = E, RA1:0 = FG
0b010010, // 0
0b000000, // 1
0b010001, // 2
0b000001, // 3
0b000011, // 4
0b000011, // 5
0b010011, // 6
0b000000, // 7
0b010011, // 8
0b000011 // 9
};

// pattern table for 7 segment display on port C


const uint8_t pat7segC[10] = {
// RC4:1 = CDBA
0b011110, // 0
0b010100, // 1
0b001110, // 2
0b011110, // 3
0b010100, // 4
0b011010, // 5
0b011010, // 6
0b010110, // 7
0b011110, // 8

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 41
© Gooligum Electronics 2014 www.gooligum.com.au

0b011110 // 9
};

// disable displays
LATA = 0; // clear all digit enable lines on PORTA
LATC = 0; // and PORTC

// lookup and output digit pattern


LATA = pat7segA[digit];
LATC = pat7segC[digit];
}

Conclusion
We’ve seen that it’s relatively simple to setup and use the ADC on the PIC16F1824, whether by polling or
through interrupts, and that it’s possible to save power by placing the device in sleep mode while the
conversions complete.
We’ve also seen that it’s possible to use the ADC in conjunction with the fixed voltage reference to infer the
PIC’s supply voltage.

We also saw that it is very important to be aware of the impact of variable and expression types on code
generation, and the need to use type casting appropriately, to allow the compiler to generate more efficient
code or indeed, as we saw in the last example, to produce correct results.
So although it is very easy to write arithmetic expressions in C, you have to be very careful when doing so.

Finally, we saw that it is easy to use XC8 to perform basic signal processing such as scaling and simple
filtering on even small enhanced mid-range devices such as the PIC16F1824.

We’ve covered a lot of material in this tutorial series, but the enhanced mid-range PIC architecture has
plenty more to offer.
Even for something as apparently simple as timers, we have only just scratched the surface, having only
described Timer0 so far.
In the next lesson, we’ll introduce a 16-bit timer: Timer1.

Enhanced Mid-Range PIC C, Lesson 11: Analog-to-Digital Conversion and Simple Filtering Page 42

You might also like