Download as pdf or txt
Download as pdf or txt
You are on page 1of 34

© Gooligum Electronics 2012 www.gooligum.com.

au

Introduction to PIC Programming


Programming Mid-Range PICs in C

by David Meiklejohn, Gooligum Electronics

Lesson 11: CCP, part 1 - Capture and Compare

Mid-range assembler lesson 17 introduced the mid-range Enhanced Capture/Compare/PWM (ECCP)


module1, beginning with its capture and compare modes. We saw that these modes can be used (in capture
mode) to accurately time external signals or (in compare mode) to automatically schedule an event, such as
toggling a pin or initiating an analog-to-digital conversion.
This lesson revisits that material, showing how to use C to control and access the ECCP module’s capture
and compare modes, re-implementing the examples using Microchip’s XC8 compiler2 (running in “Free
mode”).
We’ll look at the pulse-width modulation (PWM) mode in the next lesson.
In summary, this lesson covers:
 Introduction to the ECCP module and its capture and compare modes
 Using capture mode to measure signal period and pulse width
 Using compare mode to trigger accurately-timed external events (pin changes)
 Using compare mode to initiate regular analog-to-digital conversions

PIC16F684 ECCP Module


The PIC16F684 includes a single ECCP module3.
It is controlled by the CCP1CON register:

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


CCP1CON P1M1 P1M0 DC1B1 DC1B0 CCP1M3 CCP1M2 CCP1M1 CCP1M0

Bits 4 to 7 are only used in PWM mode, and will be described in the next lesson.
The CCP1M<3:0> bits select the operating mode.

1
some older mid-range PICs include a standard Capture/Compare/PWM (CCP) module, which does not provide all the
features of the enhanced version available on the PIC16F684
2
Available as a free download from www.microchip.com.
3
many larger PICs include multiple ECCP and/or CCP modules

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 1
© Gooligum Electronics 2012 www.gooligum.com.au

Clearing these mode select bits turns off and resets the ECCP module.
We’ll look at the bit settings relevant to the capture and compare modes, below.

Capture Mode
Capture mode allows us to measure the duration of an external signal on the CCP1 pin4.
A capture event is a defined change, or some number of changes, in the signal on CCP1.
When this occurs, the current value of TMR1 is copied into a pair of registers: CCPR1H and CCPR1L,
forming the upper and lower 8 bits of the 16-bit captured timer value.
An interrupt request flag, CCP1IF (in the PIR1 register) is also set, and an interrupt will be triggered if the
CCP1IE enable bit (in the PIE1 register) is set, and peripheral interrupts are enabled.

The available capture events, and the


corresponding mode selection bit settings, are CCP1M<3:0> mode capture event
shown in the table on the right. 0000 off
0100 capture every falling edge
If TMR1 is incrementing at a known rate, this 0101 capture every rising edge
allows us to measure the time between events,
by subtracting the captured timer values. 0110 capture every 4th rising edge
Hopefully some examples will make this 0111 capture every 16th rising edge
clearer!

Example 1: Period measurement using capture mode


To show how capture mode can be used to measure the period of a digital signal, we’ll use the circuit
(similar to those used in the Timer1 gate control examples in lesson 9) shown below:

4
shared with RC5 on the PIC16F684

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 2
© Gooligum Electronics 2012 www.gooligum.com.au

Once again, the 555 timer generates a train of digital pulses (square waves), with the potentiometer adjusting
the frequency between approximately 150 and 10000 Hz (depending on component values, which will vary
due to tolerances), generating active-high pulses ranging from less than 100 µs to more than 3000 µs.
This variable-frequency oscillator is included on the Gooligum training board, with the frequency controlled
by trimpot RP1. If you have the Gooligum board, you can implement this circuit by:
 placing shunts (six of them) across every position in jumper block JP4, connecting segments A-D, F
and G to pins RA0-1 and RC1-4
 placing a single shunt in position 2 (“RA/RB2”) of JP5, connecting segment E to pin RA2
 placing a shunt across pins 1 and 2 (“GND”) of JP6, connecting digit 1 to ground
 placing a shunt in position 1 (“CCP1”) of JP26, connecting the variable frequency digital output to
the CCP1 pin.
All other shunts should be removed.
If you are using the Microchip Low Pin Count Demo Board, you will need to build the 555-based oscillator
circuit separately and connect it to the 14-pin header on the demo board (RC5/CCP1 is available on pin 4 of
the header, while power and ground are pins 13 and 14).

We will display the measured period as a single hexadecimal digit on the 7-segment LED display – after
appropriate scaling, of course!

To measure the period of a signal, we need to record the time at the start of the signal (call it t1), and at the
end of the signal (call it t2). The period (call it T) is then the difference between the two: T = t2 – t1.
Our signal is a square wave, so the start of the signal is the rising edge of each pulse, and the end of the
signal is the rising edge of the next pulse (the end of one period is the start of the next).
Or, you could say that the period is the time between successive falling edges. With a simple square wave, it
doesn’t matter if we measure the time between rising or falling edges – we’ll get the same period, either way.
So, to measure the signal’s period, we need to record (or capture) the time of every rising or falling edge.
That’s a good task for the ECCP module, in capture mode!

To measure time, we need a timer, incrementing at a steady, known rate.


Since the ECCP module’s capture mode uses Timer1, we don’t have any choices here; we have to configure
Timer1 to provide the time base.
In this example, we’ll configure Timer1 in timer mode, where the timing is derived from the processor clock.
We’re not aiming for high accuracy, so the internal RC oscillator is good enough to use as the time base.

It is easiest to calculate the period if the timer can run for the whole period without overflowing.
Given that Timer1 is a 16-bit timer, it will overflow after 65,536 increments, and ideally the period should be
less than this.
For the best time resolution, we should run Timer1 as quickly as possible, while keeping the period to less
than 65,536 increments.
Our signal has a minimum frequency of around 150 Hz, so the maximum period will be around 7 ms.

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 3
© Gooligum Electronics 2012 www.gooligum.com.au

If we’re using the internal RC oscillator, the fastest processor clock we can select is 8 MHz, and the
instruction clock will run at 2 MHz (one quarter of the processor clock).
This means that, if we use the internal RC oscillator, the fastest that we can clock Timer1 (selecting a 1:1
prescaler) is 2 MHz – incrementing TMR1 every 0.5 µs.
Timer1 will then overflow every 65,536 × 0.5 µs = 32.768 ms, so we can measure periods up to 32.767 ms.
That’s more than we need, while running the timer as quickly as possible, so we should configure Timer1 as
a timer, using the (2 MHz) instruction clock, derived from the internal RC oscillator running at 8 MHz.

So first, we configure the device to use the internal RC oscillator, as usual:


/***** CONFIGURATION *****/
// ext reset, no code or data protect, no brownout detect
#pragma config MCLRE = ON, CP = OFF, CPD = OFF, BOREN = OFF
// no watchdog, power-up timer enabled, int oscillator with I/O
#pragma config WDTE = OFF, PWRTE = ON, FOSC = INTOSCIO
// no failsafe clock monitor, two-speed start-up disabled
#pragma config FCMEN = OFF, IESO = OFF

We then select the 8 MHz clock (see midrange assembler lesson 10):
// configure oscillator
OSCCONbits.IOSCF = 0b111; // internal oscillator = 8 MHz

And then configure Timer1 in timer mode, with no prescaler (see lesson 9):
// configure Timer1
T1CONbits.TMR1GE = 0; // gate disabled
T1CONbits.T1OSCEN = 0; // LP oscillator disabled
T1CONbits.TMR1CS = 0; // internal clock
T1CONbits.T1CKPS = 0b00; // prescale = 1
T1CONbits.TMR1ON = 1; // enable timer
// -> increment TMR1 every 0.5 us

Now we can configure the ECCP module to capture every falling or rising edge, with:
CCP1CONbits.CCP1M = 0b0100; // capture every falling edge

or:
CCP1CONbits.CCP1M = 0b0101; // capture every rising edge

With the ECCP module and timer configured and running, we can process each capture event.
In pseudo code, the main loop will look like:
repeat
wait for a capture event
period = current capture time – previous (saved) capture time
scale and display period
save current capture time (for next period calculation)
forever

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 4
© Gooligum Electronics 2012 www.gooligum.com.au

The first step, waiting for a capture event (such as a falling or rising edge on CCP1) is easy – simply poll the
CCP1IF flag:
while (!PIR1bits.CCP1IF) // wait for CCP1 interrupt flag to go high
;
PIR1bits.CCP1IF = 0; // clear flag for next event

Note that the flag has to be cleared after the polling loop, ready to be set by the next capture event.

At this point, the CCPR1 registers hold the value of TMR1 when the event occurred.
We already have the “current capture time” – it is simply the current value of CCPR1.
But it’s clear that we will need to save this current value somewhere, so that it can become the “previous
capture time”, when the process the next event. This means that we will need an unsigned 16-bit variable to
hold our “saved” (or “previous”) capture time:
uint16_t ccpr1_s = 0; // saved value of CCPR1 (previous capture)

It’s being initialised to zero to ensure that the first period calculation will be valid.

Now we need to calculate and display the period.


The signal’s period is equal to the current capture time (held in the CCPR1 registers) minus the previous
capture time (saved in ccpr1_s).
XC8 makes the CCPR1 registers available as an unsigned 16-bit variable, ‘CCPR1’.
So, if we declare an unsigned 16-bit variable, ‘period’:
uint16_t ccpr1_s = 0; // signal period

the period calculation is simply:


period = CCPR1 – ccpr1_s; // period = current capture - saved

You may think that this is too simple – what if ccpr1_s is greater than CCPR1? Wouldn’t the result of the
subtraction then be negative? And how can we have a negative period?
If the calculation was done with signed integers, you would be right. However, these variables are all
unsigned 16-bit integers, and because of the way that fixed-length unsigned integer arithmetic operates, the
subtraction always “just works” and we will get the correct result. If you want to see how this is possible,
see mid-range assembler lesson 17, where this is explained in detail.

Having calculated the signal’s period, we need to display it as a single hex digit, which means that we must
scale it to the range 0 to 15 (four bits).
Our maximum period will be around 7 ms. At 0.5 µs per clock, that’s a maximum of 14,000 clock periods.
To avoid complicated (and slow) arithmetic, we should, if possible, scale using divisions or multiplications
which the compiler can implement efficiently as simple binary operations, such as shifts. So, ideally, to
scale the period for display, we should divide it by a power of two.
14,000 is close to 16,384 = 214.
This means that we can consider the period to be a 14-bit quantity. So, to convert it to a 4-bit quantity for
display, we divide it by 210 = 1024.
Since we have a single digit display, to be safe, we should also mask off (clear) the upper nybble (4 bits)
before trying to display the result, to ensure that we never try to display a value greater than ‘F’ (15).

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 5
© Gooligum Electronics 2012 www.gooligum.com.au

So we have:
set7seg(period/1024 & 0x0f); // display scaled period

(using the 7-segment display function developed in lesson 7)

Thus, to calculate the period, scale and display it, we have:


period = CCPR1 – ccpr1_s; // period = current capture - saved
set7seg(period/1024 & 0x0f); // display scaled period

In this example, the only thing we’re doing with the period is displaying it, so we don’t need to keep the
period for any purpose, and we can collapse this down to a single expression within the function call:
// calculate and display scaled period
set7seg((CCPR1 - ccpr1_s)/1024 & 0x0f); // period = capture - saved

Finally, we need to save the current capture value for next time:
ccpr1_s = CCPR1;

Complete program
This is how it all fits together (using falling edges):
/************************************************************************
* Description: Lesson 11, example 1a *
* *
* Demonstrates use of CCP capture mode *
* to measure the period of a digital signal on CCP1, *
* scaled and displayed as a single hex digit *
* *
* Period (in 0.5 us) between falling edges on CCP1 is captured *
* Result is divided by 1024 and displayed in hex on a single-digit *
* 7-segment LED display. *
* Time base is internal RC oscillator at 8 MHz. *
* *
*************************************************************************
* Pin assignments: *
* RA0-2, RC1-4 = 7-segment display bus (common cathode) *
* CCP1 = signal to measure period of (8 ms max) *
* *
************************************************************************/

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

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


// ext reset, no code or data protect, no brownout detect
#pragma config MCLRE = ON, CP = OFF, CPD = OFF, BOREN = OFF
// no watchdog, power-up timer enabled, int oscillator with I/O
#pragma config WDTE = OFF, PWRTE = ON, FOSC = INTOSCIO
// no failsafe clock monitor, two-speed start-up disabled
#pragma config FCMEN = OFF, IESO = OFF

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


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

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 6
© Gooligum Electronics 2012 www.gooligum.com.au

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


void main()
{
uint16_t ccpr1_s = 0; // saved value of CCPR1 (previous capture)

/*** Initialisation ***/

// configure ports
PORTA = 0; // start with PORTA and PORTC clear
PORTC = 0; // (all LED segments off)
TRISA = 0; // configure PORTA and PORTC as all outputs
TRISC = 1<<5; // except RC5 (CCP1 input)
ANSEL = 0; // no analog inputs

// configure oscillator
OSCCONbits.IOSCF = 0b111; // internal oscillator = 8 MHz

// configure Timer1
T1CONbits.TMR1GE = 0; // gate disabled
T1CONbits.T1OSCEN = 0; // LP oscillator disabled
T1CONbits.TMR1CS = 0; // internal clock
T1CONbits.T1CKPS = 0b00; // prescale = 1
T1CONbits.TMR1ON = 1; // enable timer
// -> increment TMR1 every 0.5 us

// configure ECCP module


CCP1CONbits.CCP1M = 0b0100; // capture every falling edge

/*** Main loop ***/


for (;;)
{
// Measure period of pulses on CCP1 input

// wait for capture event


while (!PIR1bits.CCP1IF) // wait for CCP1 interrupt flag to go high
;
PIR1bits.CCP1IF = 0; // clear flag for next event

// calculate and display scaled period


set7seg((CCPR1 - ccpr1_s)/1024 & 0x0f); // period = capture - saved

// save current capture time for next period


ccpr1_s = CCPR1;
}
}

/***** 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] = {
// RA2:0 = EFG
0b000110, // 0
0b000000, // 1
0b000101, // 2
0b000001, // 3
0b000011, // 4

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 7
© Gooligum Electronics 2012 www.gooligum.com.au

0b000011, // 5
0b000111, // 6
0b000000, // 7
0b000111, // 8
0b000011, // 9
0b000111, // A
0b000111, // b
0b000110, // C
0b000101, // d
0b000111, // E
0b000111 // 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
};

// lookup pattern bits and write to port registers


PORTA = pat7segA[digit];
PORTC = pat7segC[digit];
}

Example 2: Pulse width measurement using capture mode


Capture mode can also be used to measure the width of a pulse, which we can demonstrate using the circuit
from the last example.

Given that the pulse width is the time between each rising edge and the next falling edge, we need to capture
the rising and falling edges. Then, for each pulse, we subtract the rising edge time from the falling edge
time, to get the pulse width.
This means that, instead of configuring the ECCP module once, in our initialisation code, we need to
configure it at the start of the main loop, to capture rising edges, and then wait for the rising edge at the start
of the pulse:
// wait for rising edge (pulse start)
CCP1CONbits.CCP1M = 0b0101; // configure CCP to capture rising edges
while (!PIR1bits.CCP1IF) // wait for CCP1 interrupt flag to go high
;
PIR1bits.CCP1IF = 0; // clear flag for next event

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 8
© Gooligum Electronics 2012 www.gooligum.com.au

If we now save the captured time, we have a record of when the pulse started:
// save capture value at pulse start
ccpr1_s = CCPR1;

So far, this is the same as the period measurement technique. The difference with pulse width measurement
is that we now reconfigure the ECCP module to capture falling edges, and wait for the end of the pulse:
// wait for falling edge (pulse end)
CCP1CONbits.CCP1M = 0b0100; // configure CCP to capture falling edges
while (!PIR1bits.CCP1IF) // wait for CCP1 interrupt flag to go high
;
PIR1bits.CCP1IF = 0; // clear flag for next event

We can now subtract the two captured times, to give the width:
width = CCPR1 – ccpr1_s; // width = pulse end - pulse start
// = current capture – saved

This is the same as before, except that we’ve named the variable ‘width’, instead of ‘period’.
It holds the number of 0.5 µs periods (assuming that Timer1 has been configured to increment every 0.5 µs,
as before) between the pulse’s rising and falling edges.
We have to scale this value for display as a single hex digit.
Although we could divide by 1024, as we did for the period measurement, we can get better resolution by
dividing by only 512 – because although the signal’s period could be up to 8 ms, its pulse width should be no
more than 4 ms, with the component values shown.
So to scale and display the pulse width, we have:
set7seg(width/512 & 0x0f); // display scaled pulse width

Again, we can do without the ‘width’ variable, collapsing these two statements down to one:
// calculate and display scaled width
set7seg((CCPR1 - ccpr1_s)/512 & 0x0f); // width = pulse end - start
// = capture - saved

Finally, we can repeat the process – going back to wait for the next rising edge.
Our main loop is therefore:
/*** Main loop ***/
for (;;)
{
// Measure with of pulses on CCP1 input

// wait for rising edge (pulse start)


CCP1CONbits.CCP1M = 0b0101; // configure CCP to capture rising edges
while (!PIR1bits.CCP1IF) // wait for CCP1 interrupt flag to go high
;
PIR1bits.CCP1IF = 0; // clear flag for next event

// save capture value at pulse start


ccpr1_s = CCPR1;

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 9
© Gooligum Electronics 2012 www.gooligum.com.au

// wait for falling edge (pulse end)


CCP1CONbits.CCP1M = 0b0100; // configure CCP to capture falling edges
while (!PIR1bits.CCP1IF) // wait for CCP1 interrupt flag to go high
;
PIR1bits.CCP1IF = 0; // clear flag for next event

// calculate and display scaled width


set7seg((CCPR1 - ccpr1_s)/512 & 0x0f); // width = pulse end - start
// = capture - saved
}

The code is otherwise the same as it was in the first example, so there is no need to repeat it here.

As an exercise, you may wish to calculate the signal’s period, as well as its pulse width, by combining these
first two examples. The period calculation would be done immediately after each rising edge is captured,
before the falling edge is captured. By adding display multiplexing (see lesson 7), you could output the
period and pulse width on separate digits. And if you want a real challenge, you could divide the pulse width
by the period, to get the duty cycle, and display that using a couple of 7-segment digits.

It’s interesting to compare the capture mode approach to the one we took in lesson 9, where we used Timer1
gate control for pulse width measurement. Both methods use Timer1, and have the same time resolution
(dependent on how quickly Timer1 is incremented).
The Timer1 gate control method is simpler in some ways (the example code is only 60 lines long, compared
with 65 lines for the equivalent capture mode example) – so why would we choose to use capture mode?
Perhaps the most significant reason is that capture events can trigger interrupts, as we’ll see in the next
example, while the gate control method relies on polling – which is not always appropriate.

Example 3: Period measurement using capture mode interrupts


Instead of polling the CCP interrupt flag, you may prefer to use an interrupt handler to process each capture
event – especially if your code needs to be doing other things “at the same time”.
In this example, we’ll continue to use the circuit from the first two examples, to show how CCP interrupts
can be used to measure the period of a square wave.

If enabled, the CCP interrupt is triggered on every capture event. The interrupt handler would then calculate
the signal’s period, in much the same way as we did in example 1. The ISR would typically store the period
in a variable, which would be processed and/or displayed by another routine, perhaps within the main loop or
in another interrupt handler – similar to what we did to display the ADC output in the ADC interrupt
example in lesson 8.

However, you probably don’t want the interrupt handler to run too often, or else you won’t have enough time
between interrupts to do much else.
With the oscillator running at up to 10 kHz, the CCP interrupt could be run as often as every 100 µs – if it is
triggered on every pulse.
Luckily, the ECCP module provides a way to limit how often the CCP interrupt is triggered. Instead of
capturing every falling or rising edge, as we did in example 1, we can capture every 4th or 16th rising edge.

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 10
© Gooligum Electronics 2012 www.gooligum.com.au

We can configure the ECCP module to capture every 16th rising edge, with:
CCP1CONbits.CCP1M = 0b0111; // capture every 16th rising edge

With the oscillator running at 10 kHz, the CCP interrupt will be triggered every 16 × 100 µs = 1.6 ms, which
is much more reasonable.
Another advantage of capturing every 16th rising edge is that we’re effectively measuring the average period
of a series of 16 pulses – which is useful if there is some jitter in the signal which we’d like to smooth out,
without having to implement an averaging filter, like the one in lesson 8.
However, if our oscillator runs at its minimum 150 Hz, giving a period of around 7 ms, the interrupt would
be triggered every 16 × 7 ms = 112 ms. If we continue to run the device at 8 MHz (0.5 µs per instruction
clock), that’s a period between capture events of up to 224,000 instruction cycles.
Our 16-bit timer, TMR1, can only count up to 65,535, so, if we don’t make any other changes, the timer will
overflow between capture events. We could run the PIC at a lower clock rate, but that defeats our purpose of
being able to do as much processing as possible between interrupts.
The solution is simple – use a 1:4 prescaler with Timer1:
// configure Timer1
T1CONbits.TMR1GE = 0; // gate disabled
T1CONbits.T1OSCEN = 0; // LP oscillator disabled
T1CONbits.TMR1CS = 0; // internal clock
T1CONbits.T1CKPS = 0b10; // prescale = 4
T1CONbits.TMR1ON = 1; // enable timer
// -> increment TMR1 every 2 us

With TMR1 incrementing every 2 µs, it will overflow every ~131 ms – long enough to measure our signal’s
period, even at the slowest oscillator speed.

Previously, when measuring a single pulse, our “full scale” of ~8 ms was 16,383 (214-1) × 0.5 µs counts.
Now that we’re capturing every 16th rising edge, and counting every 2 µs, our full scale of ~131 ms will be
65,535 (216-1) counts.
To convert this to a 4-bit quantity, for display as a single hex digit, we need to divide it by 212 = 4096.
So, to calculate, scale and display the period, we now have:
// calculate and display scaled period
set7seg((CCPR1 - ccpr1_s)/4096); // period = current capture - saved

If you make these changes to the code from example 1, the resulting program should work the same way as
before, displaying a ‘0’ when (if you are using the Gooligum training board) trimpot RP1 is turned all the
way clockwise, and ‘d’ (or maybe ‘c’ or ‘E’, depending on component values) when it is at the other
extreme.

But of course, we want to implement this using interrupts!


First, we need to enable the CCP interrupt:
PIE1bits.CCP1IE = 1; // enable CCP1 interrupt

and of course peripheral and global interrupts, as usual:


INTCONbits.PEIE = 1; // enable peripheral interrupts
ei(); // enable global interrupts

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 11
© Gooligum Electronics 2012 www.gooligum.com.au

Our interrupt handler should begin, as always, by clearing the CCP interrupt flag:
//*** Service CCP1 interrupt
//
// Triggered on capture event,
// every 16th rising edge on CCP1 input
//
// Measures average period of pulses on CCP1 input
//
// (only CCP1 interrupts are enabled)
//
PIR1bits.CCP1IF = 0; // clear interrupt flag

Then, within the interrupt handler, we can calculate the signal’s period:
// calculate period
period = CCPR1 - ccpr1_s; // = current capture - saved

As we’ve mentioned before, interrupt handlers should ideally be kept short, performing their specific task,
but no more than necessary. In this case, the CCP interrupt handler records the period between each capture
event, but that’s all it needs to do. Scaling and displaying the period can be done elsewhere, such as in the
main loop (as we’ll do in this example) and/or another interrupt handler.
That means that, because the ‘period’ variable is used to pass date (the signal’s period…) between
functions, it has to be defined as a global variable, before the main() function:
/***** GLOBAL VARIABLES *****/
uint16_t period; // signal period (raw)

On the other hand, the variable used to store the previous capture value, ccpr1_s, is only used within the
interrupt handler, so should be defined at the start of the interrupt service routine:
void interrupt isr(void)
{
static uint16_t ccpr1_s = 0; // saved value of CCPR1 (previous capture)

Since it has to be preserved between interrupts (the whole point of this variable is that, each time the CCP
interrupt runs, ccpr1_s holds the value that was in CCPR1, from the previous time the CCP interrupt ran),
it must be defined as ‘static’.
And, to ensure that the period calculation is correct the first time that the CCP interrupt handler runs, it is
initialised to zero.

Finally, save the current capture value, in CCPR1, for the next time the CCP interrupt is triggered:
// save current capture value for next period
ccpr1_s = CCPR1;

The main loop only has to display the period, which we could do by:
for (;;)
{
// display scaled period
set7seg(period/4096);
}

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 12
© Gooligum Electronics 2012 www.gooligum.com.au

However, we do need to be careful here.


The period variable is updated by the interrupt service routine, which could run at any time.
The ‘period’ variable holds a 16-bit value and, as we’ve discussed before, the mid-range PICs can only
operate on eight bits at a time. This means that it is possible that the ISR will run, updating the value in
‘period’, in the middle of the ‘period/4096’ calculation.
Whether or not that might be a problem will depend on the compiler’s implementation of that expression. In
fact, with XC8 v1.11, running in “Free mode”, it’s not a problem. But there is no guarantee that a future
version of the compiler, or a different compiler (if you later port the code) will perform the calculation in the
same way. The danger is that, if ‘period/4096’ doesn’t evaluate correctly, a value greater than 15 might
be passed to the set7seg() function, which would then attempt to lookup a value beyond the end of the
pattern arrays, and the program would probably crash.
To avoid this potential problem in mid-range assembler lesson 17, we introduced another variable, ‘digit’,
which holds the value of the digit to be displayed. We can a similar approach here.
First, ‘digit’ is defined as a local variable within main(), since it’s not accessed in any other part of the
program:
void main()
{
uint8_t digit; // digit to be displayed

We can disable interrupts while calculating the digit to display (the scaled period), by wrapping the
expression within a pair of ‘di()’ and ‘ei()’ statements:
// calculate scaled period
di(); // (disable interrupts while accessing period)
digit = period/4096;
ei();

We can now be sure that ‘period’ won’t change during this calculation.
As we’ve noted before, any such sections of code, where interrupts are disabled, should be kept as short as
possible, to give any interrupts a chance to run as soon as possible after the event that triggered them.
Finally, we can safely display the result:
// display scaled period
set7seg(digit);

Complete program
Here is how all these fragments fit together:
/************************************************************************
* Description: Lesson 11, example 3b *
* *
* Demonstrates use of CCP capture mode interrupts *
* to measure the period of a digital signal on CCP1, *
* scaled and displayed as a single hex digit *
* *
* Period between every 16th rising edge on CCP1 is captured *
* using CCP1 interrupt handler *
* Result is scaled and displayed in hex on a single-digit *
* 7-segment LED display. *
* Time base is internal RC oscillator at 8 MHz. *
* *

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 13
© Gooligum Electronics 2012 www.gooligum.com.au

*************************************************************************
* *
* Pin assignments: *
* RA0-2, RC1-4 = 7-segment display bus (common cathode) *
* CCP1 = signal to measure period of (8 ms max) *
* *
************************************************************************/

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

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


// ext reset, no code or data protect, no brownout detect
#pragma config MCLRE = ON, CP = OFF, CPD = OFF, BOREN = OFF
// no watchdog, power-up timer enabled, int oscillator with I/O
#pragma config WDTE = OFF, PWRTE = ON, FOSC = INTOSCIO
// no failsafe clock monitor, two-speed start-up disabled
#pragma config FCMEN = OFF, IESO = OFF

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


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

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


uint16_t period; // signal period (raw)

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


void main()
{
uint8_t digit; // digit to be displayed

/*** Initialisation ***/

// configure ports
PORTA = 0; // start with PORTA and PORTC clear
PORTC = 0; // (all LED segments off)
TRISA = 0; // configure PORTA and PORTC as all outputs
TRISC = 1<<5; // except RC5 (CCP1 input)
ANSEL = 0; // no analog inputs

// configure oscillator
OSCCONbits.IOSCF = 0b111; // internal oscillator = 8 MHz

// configure Timer1
T1CONbits.TMR1GE = 0; // gate disabled
T1CONbits.T1OSCEN = 0; // LP oscillator disabled
T1CONbits.TMR1CS = 0; // internal clock
T1CONbits.T1CKPS = 0b10; // prescale = 4
T1CONbits.TMR1ON = 1; // enable timer
// -> increment TMR1 every 2 us

// configure ECCP module


CCP1CONbits.CCP1M = 0b0111; // capture every 16th rising edge
PIE1bits.CCP1IE = 1; // enable CCP1 interrupt

// enable interrupts
INTCONbits.PEIE = 1; // enable peripheral interrupts
ei(); // enable global interrupts

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 14
© Gooligum Electronics 2012 www.gooligum.com.au

/*** Main loop ***/


for (;;)
{
// calculate scaled period
di(); // (disable interrupts while accessing period)
digit = period/4096;
ei();

// display scaled period


set7seg(digit);
}
}

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


void interrupt isr(void)
{
static uint16_t ccpr1_s = 0; // saved value of CCPR1 (previous
capture)

//*** Service CCP1 interrupt


//
// Triggered on capture event,
// every 16th rising edge on CCP1 input
//
// Measures average period of pulses on CCP1 input
//
// (only CCP1 interrupts are enabled)
//
PIR1bits.CCP1IF = 0; // clear interrupt flag

// calculate period
period = CCPR1 - ccpr1_s; // = current capture - saved

// save current capture value for next period


ccpr1_s = CCPR1;
}

/***** 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] = {
// RA2:0 = EFG
0b000110, // 0
0b000000, // 1
0b000101, // 2
0b000001, // 3
0b000011, // 4
0b000011, // 5
0b000111, // 6
0b000000, // 7
0b000111, // 8
0b000011, // 9
0b000111, // A
0b000111, // b
0b000110, // C
0b000101, // d

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 15
© Gooligum Electronics 2012 www.gooligum.com.au

0b000111, // E
0b000111 // 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
};

// lookup pattern bits and write to port registers


PORTA = pat7segA[digit];
PORTC = pat7segC[digit];
}

Compare Mode
Compare mode automatically performs an action, such as changing the state of the CCP1 output pin,
whenever TMR1 matches the contents of the CCPR1 registers. If TMR1 is incrementing at a known rate,
this allows us to schedule a particular action to be performed at a predetermined time.
The interrupt request flag, CCP1IF (in the PIR1 register) is also set, and an interrupt will be triggered if the
CCP1IE enable bit (in the PIE1 register) is set, and peripheral interrupts are enabled.
Compare mode is often used to generate precisely-timed pulses, such as those used in some serial modulation
formats. It is also used to schedule periodic interrupts, and to automatically sample the ADC at a steady rate,
as we shall see.

The available compare actions, and their CCP1M<3:0> mode action


corresponding mode selection bit settings, are
shown in the table on the right. 0000 off
0010 compare toggle CCP1 output
1000 compare set CCP1 output
Hopefully some more examples will make 1001 compare clear CCP1 output
this clearer!
1010 compare none (interrupt only)
1011 compare clear TMR1,
initiate ADC if enabled

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 16
© Gooligum Electronics 2012 www.gooligum.com.au

Example 4: Using compare mode to flash an LED


Although compare mode is most commonly used to generate short pulses, such as those used in serial
communications, we can make the compare mode actions visible (literally!) by, yet again, flashing an LED.

To make the CCP1 output visible, we can connect an LED to


the CCP1 pin (shared with RC5), as shown on the right.

Unfortunately, neither the Gooligum training board, nor the


Microchip Low Pin Count Demo Board, come with an LED
connected to RC5. That’s not really a problem though – it’s
easy to connect one of the on-board LEDs to RC5, via the
expansion headers.
If you have the Gooligum board, close jumper JP19 to enable
the LED on RC3, and place a wire link between pins 5
(‘RC5’) and 7 (‘RC3’) of the 16-pin expansion header.
If you are using the Microchip board, you can do the same
thing by adding a wire link between pins 4 (‘RC5’) and 6
(‘RC3’) of the 14-pin expansion header.
With this link in place, the LED labelled ‘RC3’ will light
whenever the CCP1 output goes high5.

To flash the LED at 1 Hz, with a 50% duty cycle (as we’ve done numerous times, going back to lesson 1),
we need to toggle the CCP1 output every 500 ms.
Therefore, we should select ECCP mode ‘0010’: “Compare mode, toggle output on match”.
We then start Timer1 counting at some known, steady rate, and load a value into the CCPR1 registers such
that 500 ms will have passed when TMR1 reaches that value.

So, Timer1 has to be able to count for at least 500 ms without overflowing.
In lesson 9 we saw that, in timer mode, with a 4 MHz processor clock and a 1:8 prescaler, Timer1 will
overflow every 524 ms. That’s more than the 500 ms we need, so we can configure the PIC to use the
internal RC oscillator, running at the default 4 MHz, and initialise Timer1 with:
// initialise Timer1
TMR1 = 0; // clear timer
T1CONbits.TMR1GE = 0; // gate disabled
T1CONbits.T1OSCEN = 0; // LP oscillator disabled
T1CONbits.TMR1CS = 0; // internal clock
T1CONbits.T1CKPS = 0b11; // prescale = 8
T1CONbits.TMR1ON = 1; // enable timer
// -> increment TMR1 every 8 us

TMR1 is being cleared here, to make it easy to load the appropriate initial value into CCPR1, so that a
match will occur after 500 ms:
CCPR1 = 500000/8; // initial compare time = 0.5 s /8 us/count

5
Assuming that the RC3 output is not enabled. We’ll leave it configured as an input – effectively disconnected, or “tri-
stated” – in these examples.

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 17
© Gooligum Electronics 2012 www.gooligum.com.au

We can then configure the ECCP module to toggle the CCP1 pin, when TMR1 reaches the value stored in
the CCPR1 registers:
CCP1CONbits.CCP1M = 0b0010; // compare mode, toggle CCP1 on match

Note that, for CCP1 to be used as an output, the pin it shares (RC5) must also be configured as an output:
TRISC = ~(1<<5); // configure PORTC as all inputs
// except RC5 (CCP1 output)

Following a power-on reset, the CCP1 output is initially low.


Therefore, after completing the previous initialisation instructions, the LED will be initially off. Meanwhile,
TMR1 will be counting, incrementing every 8 µs. After 500 ms, TMR1 will reach 62,500 (= 500,000 ÷ 8),
which is the value stored in CCPR1, and the CCP1 output will toggle – changing to high.
Thus, if we do nothing else, the LED will turn on, automatically, after 500 ms.

Since we want the LED to continually flash, we need to wait for CCP1 to toggle, then add the appropriate
value to CCPR1 (such that CCP1 will toggle again, after another 500 ms), and then repeat.
CCP1 will toggle when a CCP match occurs, and we can detect by clearing the CCP1IF flag, and then
waiting until it goes high:
// wait for CCP match
PIR1bits.CCP1IF = 0; // clear CCP1 interrupt flag
while (!PIR1bits.CCP1IF) // wait flag to go high
;

At this point, we know that the CCP1 output has toggled.


We can now add another 500 ms to the value in CCPR1:
// add 0.5 sec to last compare time
CCPR1 += 500000/8; // add 0.5 sec / 8 us/count

TMR1 will continue to increment (we don’t care about TMR1 overflows), and when it reaches this new
compare value, CCP1 (and our LED) will toggle again.
We then wait for this match to happen, add another 500 ms, wait again, add 500 ms, and so on.

Our main loop is then simply:


for (;;)
{
// Toggle CCP1 output every 0.5 sec

// wait for CCP match


PIR1bits.CCP1IF = 0; // clear CCP1 interrupt flag
while (!PIR1bits.CCP1IF) // wait flag to go high
;

// add 0.5 sec to last compare time


CCPR1 += 500000/8; // add 0.5 sec / 8 us/count
}

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 18
© Gooligum Electronics 2012 www.gooligum.com.au

Suppose, though, that instead of simply toggling the output at a steady rate, flashing the LED with a 50%
duty cycle, we wanted to flash the LED asymmetrically, with (say) a 20% duty cycle, as we did in lesson 1?
We would need to change our approach – clearing the CCP1 output (turning off the LED), waiting 800 ms,
setting the CCP1 output (lighting the LED), then waiting another 200 ms, and then repeating.
We could do that with the “toggle” action, as above, but it would be hard to keep track of what we were
doing. It’s easier to understand if we explicitly use the “set” and “clear” actions to turn the LED on and off.
This means that, instead of configuring the ECCP module once, in the initialisation routine, we need to re-
configure it, to alternately set and clear CCP1, within the main loop.

But first, we have to address a small problem. Our LED has to stay off for 800 ms, which means that TMR1
has to be able to count for at least 800 ms without overflowing. But, as mentioned above, the maximum
period we can configure for TMR1 is 524 ms – if we use the default 4 MHz processor clock.
In this case, the solution is easy – select a slower processor clock!
The internal oscillator can be configured to clock the PIC at 2 MHz, instead of 4 MHz, by:
// configure oscillator
OSCCONbits.IOSCF = 0b101; // internal oscillator = 2 MHz

If we then configure Timer1 with a 1:8 prescaler, as before, TMR1 will increment every 16 µs and will
overflow every 1049 ms, which is long enough.

Recall that, following a power-on reset, the CCP1 output will initially be low.
That means that, when we start the main loop, we should assume that CCP1 is low, and the LED is off.
We want the LED to stay off for 800 ms, and then turn on, so we should configure the ECCP module to set
the CCP1 output (lighting the LED), 800 ms from now:
// add 0.8 sec to last compare time
CCPR1 += 800000/16; // add 0.8 sec / 16 us/count

// configure ECCP to set CCP1 after 0.8 sec


CCP1CONbits.CCP1M = 0b1000; // compare mode, set CCP1 on match

There is nothing to do now but wait for the match to happen, which we can do by monitoring the CCP1IF
flag, as we did before:
// wait for CCP match
PIR1bits.CCP1IF = 0; // clear CCP1 interrupt flag
while (!PIR1bits.CCP1IF) // wait for flag to go high
;

At this point, we know that the CCP1 output has just gone high.
We want the LED to stay on for 200 ms, so we should re-configure the ECCP module to clear CCP1
(turning off the LED), after another 200 ms:
// add 0.2 sec to last compare time
CCPR1 += 200000/16; // add 0.2 sec / 16 us/count

// configure ECCP to clear CCP1 after 0.2 sec


CCP1CONbits.CCP1M = 0b1001; // compare mode, clear CCP1 on match

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 19
© Gooligum Electronics 2012 www.gooligum.com.au

Now we have to wait once more for TMR1 to match CCPR1:


// wait for CCP match
PIR1bits.CCP1IF = 0; // clear CCP1 interrupt flag
while (!PIR1bits.CCP1IF) // wait for flag to go high
;

When the CCP1IF flag goes high, we know that the CCP action (clearing CCP1) has occurred.
The LED has been turned off, we’re back to where we started, and we can restart the main loop.

Once we’re into the main loop, accurate pulse timing is maintained by adding fixed quantities to the compare
value stored in CCPR1, as TMR1 continues to steadily count. The timing of each transition is relative to the
last – the LED turns off 200 ms after it turns on, and we don’t care about the actual values are, at the start of
the loop. As long as we add the values corresponding to 200 ms and 800 ms, the correct timing will be
maintained, each time we go around the loop.
However, we need to start somewhere. If we want the LED to stay off for a full 800 ms, the first time the
main loop is entered, we need to clear TMR1 and CCPR1 before the main loop starts. But – and this is the
important point for understanding compare mode – having cleared CCPR1 once, we never have to load a
value into it again. It’s all done by adding, because the timing of each pulse is always relative.

Complete program
Here is how these fragments fit together, to form the CCP version of the “flash an LED with 20% duty
cycle” program:
/************************************************************************
* *
* Description: Lesson 11, example 4b *
* *
* Demonstrates use of CCP compare mode *
* to generate high and low pulses on the CCP1 pin *
* *
* Flashes an LED on CCP1 at 1 Hz, with 20% duty cycle *
* *
*************************************************************************
* *
* Pin assignments: *
* CCP1 = indicator LED *
* *
************************************************************************/

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

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


// ext reset, no code or data protect, no brownout detect
#pragma config MCLRE = ON, CP = OFF, CPD = OFF, BOREN = OFF
// no watchdog, power-up timer enabled, int oscillator with I/O
#pragma config WDTE = OFF, PWRTE = ON, FOSC = INTOSCIO
// no failsafe clock monitor, two-speed start-up disabled
#pragma config FCMEN = OFF, IESO = OFF

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

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 20
© Gooligum Electronics 2012 www.gooligum.com.au

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

// configure ports
TRISC = ~(1<<5); // configure PORTC as all inputs
// except RC5 (CCP1 output)

// configure oscillator
OSCCONbits.IOSCF = 0b101; // internal oscillator = 2 MHz

// initialise Timer1
TMR1 = 0; // clear timer
T1CONbits.TMR1GE = 0; // gate disabled
T1CONbits.T1OSCEN = 0; // LP oscillator disabled
T1CONbits.TMR1CS = 0; // internal clock
T1CONbits.T1CKPS = 0b11; // prescale = 8
T1CONbits.TMR1ON = 1; // enable timer
// -> increment TMR1 every 16 us

// initialise ECCP module


CCPR1 = 0; // initial compare time = 0
// (CCP initially off, CCP1 output low)

/*** Main loop ***/


for (;;)
{
// Output low for 0.8 sec

// add 0.8 sec to last compare time


CCPR1 += 800000/16; // add 0.8 sec / 16 us/count

// configure ECCP to set CCP1 after 0.8 sec


CCP1CONbits.CCP1M = 0b1000; // compare mode, set CCP1 on match

// wait for CCP match


PIR1bits.CCP1IF = 0; // clear CCP1 interrupt flag
while (!PIR1bits.CCP1IF) // wait for flag to go high
;

// Output high for 0.2 sec

// add 0.2 sec to last compare time


CCPR1 += 200000/16; // add 0.2 sec / 16 us/count

// configure ECCP to clear CCP1 after 0.2 sec


CCP1CONbits.CCP1M = 0b1001; // compare mode, clear CCP1 on match

// wait for CCP match


PIR1bits.CCP1IF = 0; // clear CCP1 interrupt flag
while (!PIR1bits.CCP1IF) // wait for flag to go high
;
}
}

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 21
© Gooligum Electronics 2012 www.gooligum.com.au

Example 5: Compare mode interrupts


Compare mode can also be used to generate precise intervals,
without having to affect the CCP1 output pin.
We can illustrate that by using CCP interrupts to flash an
LED connected to RC3 (instead of CCP1), as shown in the
circuit on the right, at exactly 1 Hz.
If you have the Gooligum training board, you can reconfigure
it for this example by removing the wire link from the 16-pin
expansion header, while leaving jumper JP19 closed,
enabling the LED on RC3.

In lesson 3 we used Timer0 interrupts to flash an LED at


exactly 1 Hz (given an accurate 4 MHz processor clock), by
adding an offset to TMR0 within the ISR.
We also saw, in lesson 9, that it’s messy to try to take the
same approach with Timer16. It was mentioned that there are
more elegant ways to achieve precise, arbitrary interrupt
timing with Timer1.

One way to do this is to use the CCP module’s compare mode to generate a periodic interrupt.
If we select CCP mode ‘1010’, the CCP1IF flag will be set when TMR1 matches CCPR1, but no other
action will occur. However, CCP1IF is an interrupt flag. This means that, in CCP mode ‘1010’, a CCP
interrupt will be triggered when TMR1 matches CCPR1, if CCP interrupts are enabled.
This is also true in the other compare modes, such as those used in example 4, above. The difference with
mode ‘1010’ is that no other action occurs. A CCP interrupt will be triggered (if enabled), but the CCP1
output will not be affected.

We’ll flash the LED with a simple 50% duty cycle, as we did in the timer interrupt examples in lessons 3 and
9. Our code will be quite similar to those examples, except that we’re now using a CCP interrupt. That is,
the ISR will toggle the LED output every 500 ms, via a shadow register. The main loop will be then have
nothing to do except continually copy the shadow register to the port, making the changes visible.

The ISR will be triggered when TMR1 matches CCPR1, and we want this to occur every 500 ms.
We’ve seen that Timer1 can be configured to count for up to 524 ms without overflowing, given a 1:8
prescaler and 4 MHz processor clock, so 500 ms is ok.

To make the code more maintainable, we’ll define this toggle period as a constant:
#define FlashMS 500 // LED flash toggle time in milliseconds
// (max 524 ms)

6
when adding an offset to a 16-bit timer that is actively counting, the timer may overflow during the two-part add
operation, and this possibility has to be handled carefully, leading to increased code complexity

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 22
© Gooligum Electronics 2012 www.gooligum.com.au

Since TMR1 will be incrementing every 8 µs, we can generate each new 500 ms interval by adding
FlashMS*1000/8 to CCPR1, in much the same way as in example 4.
Instead of repeating this expression multiple times, we can make the code a little easier to read by defining it
as a constant7:
#define IncCCPR FlashMS*1000/8 // Amount to incr CCPR1 by to generate
// FlashMS delay (assuming 8 us/tick)

Timer1 and the CCP module are configured as before, except that we will now use CCP mode ‘1010’:
// initialise Timer1
TMR1 = 0; // clear timer
T1CONbits.TMR1GE = 0; // gate disabled
T1CONbits.T1OSCEN = 0; // LP oscillator disabled
T1CONbits.TMR1CS = 0; // internal clock
T1CONbits.T1CKPS = 0b11; // prescale = 8
T1CONbits.TMR1ON = 1; // enable timer
// -> increment TMR1 every 8 us

// initialise ECCP module


CCPR1 = IncCCPR; // load initial compare time
CCP1CONbits.CCP1M = 0b0010; // compare mode, interrupt only

We also need to enable the CCP interrupt:


PIE1bits.CCP1IE = 1; // enable CCP1 interrupt

and the peripheral and global interrupts:


INTCONbits.PEIE = 1; // enable peripheral interrupts
ei(); // enable global interrupts

Note that, since we cleared TMR1 and loaded CCPR1 with the value that TMR1 will reach in 500 ms, the
first interrupt will occur in 500 ms.

Within the interrupt service routine, we begin by clearing the interrupt flag, as usual:
PIR1bits.CCP1IF = 0; // clear interrupt flag

If we then add 500 ms to the value in CCPR1, the next interrupt will be triggered in another 500 ms:
// add offset to CCPR1 for next match
CCPR1 += IncCCPR;

It’s important to note that this timing will be exact. It’s not like the example in lesson 9, where we added an
offset to Timer1 to generate an approximate interval. It doesn’t matter that TMR1 continues to increment
while we do this addition. By adding a value representing 500 ms to CCPR1, we can be sure that the next
interrupt will be triggered exactly 500 ms after this one.

We can then toggle the LED, using a shadow register, as we’ve done before:
sF_LED = !sF_LED;

7
whether this is really easier to read is, of course, a question of personal preference…

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 23
© Gooligum Electronics 2012 www.gooligum.com.au

And finally, in the main loop, copy this shadow register to PORTC:
for (;;)
{
// copy shadow register (updated by ISR) to port
PORTC = sPORTC.RC;
}

Complete program
Here is how all of these fragments fit together, in a similar way to our earlier interrupt-based LED flashers:
/************************************************************************
* Description: Lesson 11, example 5 *
* *
* Demonstrates use of CCP compare mode interrupts *
* *
* Flashes an LED at 1 Hz, with 50% duty cycle *
* *
*************************************************************************
* Pin assignments: *
* RC3 = flashing LED *
* *
************************************************************************/

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

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


// ext reset, no code or data protect, no brownout detect
#pragma config MCLRE = ON, CP = OFF, CPD = OFF, BOREN = OFF
// no watchdog, power-up timer enabled, int 4 MHz oscillator with I/O
#pragma config WDTE = OFF, PWRTE = ON, FOSC = INTOSCIO
// no failsafe clock monitor, two-speed start-up disabled
#pragma config FCMEN = OFF, IESO = OFF

// Pin assignments
#define sF_LED sPORTC.RC3 // flashing LED (shadow)

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


#define FlashMS 500 // LED flash toggle time in milliseconds
// (max 524 ms)
#define IncCCPR FlashMS*1000/8 // Amount to incr CCPR1 by to generate
// FlashMS delay (assuming 8 us/tick)

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


volatile union { // shadow copy of PORTC
uint8_t RC;
struct {
unsigned RC0 : 1;
unsigned RC1 : 1;
unsigned RC2 : 1;
unsigned RC3 : 1;
unsigned RC4 : 1;
unsigned RC5 : 1;
};
} sPORTC;

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 24
© Gooligum Electronics 2012 www.gooligum.com.au

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


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

// configure ports
PORTC = 0; // start with PORTC clear (LED off)
sPORTC.RC = 0; // and update shadow
TRISC = ~(1<<3); // configure RC3 (only) as an output

// initialise Timer1
TMR1 = 0; // clear timer
T1CONbits.TMR1GE = 0; // gate disabled
T1CONbits.T1OSCEN = 0; // LP oscillator disabled
T1CONbits.TMR1CS = 0; // internal clock
T1CONbits.T1CKPS = 0b11; // prescale = 8
T1CONbits.TMR1ON = 1; // enable timer
// -> increment TMR1 every 8 us

// initialise ECCP module


CCPR1 = IncCCPR; // load initial compare time
CCP1CONbits.CCP1M = 0b0010; // compare mode, interrupt only
PIE1bits.CCP1IE = 1; // enable CCP1 interrupt

// enable interrupts
INTCONbits.PEIE = 1; // enable peripheral interrupts
ei(); // enable global interrupts

/*** Main loop ***/


for (;;)
{
// copy shadow register (updated by ISR) to port
PORTC = sPORTC.RC;
}
}

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


void interrupt isr(void)
{
//*** Service CCP1 interrupt
//
// Triggered when TMR1 matches CCPR1
// (every 500 ms)
//
// Flashes LED at 1 Hz by toggling on each interrupt
//
// (only CCP1 interrupts are enabled)
//
PIR1bits.CCP1IF = 0; // clear interrupt flag

// add offset to CCPR1 for next match


CCPR1 += IncCCPR;

// toggle LED (using shadow register)


sF_LED = !sF_LED;
}

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 25
© Gooligum Electronics 2012 www.gooligum.com.au

Example 6: Using CCPR1 as a period register for Timer1


If you’re not using the analog-to-digital converter (ADC), the CCP module’s “special event trigger” mode,
‘1011’, provides a simpler way generate periodic interrupts with precise, arbitrary timing.
This “special event trigger” mode is like the “interrupt only” mode used in the last example, in that it sets the
CCP1IF flag, and triggers an interrupt (if CCP interrupts are enabled), when TMR1 matches CCPR1.
However, in addition to triggering an interrupt, special event trigger mode also resets (clears) Timer1 and
starts an analog-to-digital conversion if ADC is enabled.
This means that, as long as ADC is not enabled, the special event trigger mode has the useful property of
automatically clearing TMR1, as soon as TMR1 reaches the value specified in CCPR1.
Thus, CCPR1 acts as a 16-bit period register for Timer1.
This is similar to the Timer2 period register, PR2, described in lesson 10. An important difference,
however, is that PR2 holds Timer2’s maximum count (equal to the period minus one), while, in special
event trigger mode, CCPR1 holds Timer1’s period.
For example, if you wanted Timer2 to reset every 100 counts, you would load the value 99 into PR2. But if
you wanted Timer1 to reset every 1000 counts, in special event trigger mode, you would load the value 1000
into CCPR1.

To illustrate this, we can re-implement the previous example, using special event trigger mode.
Most of the code is exactly the same, so won’t be repeated here.
We will continue to use a CCP interrupt, triggered every 500 ms, to toggle RC3 via a shadow register.
And we will still define the toggle period as a constant:
#define FlashMS 500 // LED flash toggle time in milliseconds
// (max 524 ms)

Timer1 will increment every 8 µs, as before.


The period between interrupts is the same as the Timer1 period: we want Timer1 to be reset every 500 ms.
So, the number of times that TMR1 should increment, before being cleared, is given by:
Timer1 period = 500 ms × 1000 µs/ms ÷ 8 µs/tick = 62,500 ticks
Again, we can define this expression as a constant:
#define T1Period FlashMS*1000/8 // number of Timer1 counts to generate
// FlashMS delay (assuming 8 us/tick)

This value, being the Timer1 period, is the value we need to load into CCPR1.

Initialising the CCP module, selecting mode ‘1011’ this time, then becomes:
// initialise ECCP module
CCPR1 = T1Period; // load compare vslue (= TMR1 period)
CCP1CONbits.CCP1M = 0b1011; // special trigger mode (reset TMR1 on match)
PIE1bits.CCP1IE = 1; // enable CCP1 interrupt

The rest of the initialisation, and the main loop, is the same as in the last example.
However, now that TMR1 is being automatically cleared whenever it matches CCPR1, we no longer have
to adjust CCPR1 in the interrupt service routine.

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 26
© Gooligum Electronics 2012 www.gooligum.com.au

The CCP interrupt handler becomes simply:


void interrupt isr(void)
{
//*** Service CCP1 interrupt
//
// Triggered when TMR1 matches CCPR1
// (every 500 ms)
//
// Flashes LED at 1 Hz by toggling on each interrupt
//
// (only CCP1 interrupts are enabled)
//
PIR1bits.CCP1IF = 0; // clear interrupt flag

// toggle flashng LED (using shadow register)


sF_LED = !sF_LED;
}

And that’s all.


Other than these change, the program is exactly the same as in the last example.

Example 7: Periodic analog-to-digital conversions


We saw in lesson 8 that timer interrupts can be used to periodically sample an analog input. The example in
that lesson used a Timer0 interrupt to initiate each analog-to-digital conversion.
That approach makes sense when you already have a timer interrupt running for some other purpose (such as
display multiplexing) at some rate that is also appropriate for sampling your analog inputs. However, it
won’t always be appropriate to tie analog-to-digital conversions to a timer interrupt in that way.
An alternative, and convenient, way to periodically sample an analog input is to use the CCP module’s
“special event trigger” mode. As we saw in the last example, special event trigger mode resets Timer1 and
starts an analog-to-digital conversion if ADC is enabled, whenever TMR1 matches CCPR1.
This means that, if the ADC module is enabled, the special event trigger mode has the useful property of
automatically triggering periodic AC conversions, with the period specified by the 16-bit value in CCPR1.
To demonstrate how to do this, we’ll use a 2-digit version of the circuit from lesson 8 , as shown below.

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 27
© Gooligum Electronics 2012 www.gooligum.com.au

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

Having only two digits instead of three reduces the program complexity a little, making it easier to see how
the CCP module’s special event trigger mode is used to drive the ADC module.
We can adapt the code from the ADC interrupt example in lesson 8, with a Timer0 interrupt used to
multiplex the display.

The Timer0 interrupt handler runs every 2 ms, and displays the current contents of two variables, ‘ dig1’ and
‘dig2’. Note that that is the only thing the Timer0 handler is responsible for – unlike in the ADC interrupt
example in lesson 8, it does not trigger any analog-to-digital conversions. Its only job is display the current
contents of those two digit variables.
Instead, we configure the CCP module, through its special event trigger mode, to automatically trigger the
analog-to-digital conversions:
// initialise ECCP module
CCPR1 = ADCPeriod; // compare vslue = ADC sample period)
CCP1CONbits.CCP1M = 0b1011; // special trigger mode
// -> sample ADC and reset TMR1 on match

Note that we’re loading CCPR1 with the sample period, which, for better maintainability, has been defined
as a constant:
#define ADCPeriod 10000 // ADC sample period in microseconds
// (max 65535)

This assumes that Timer1 has been configured to increment every 1 µs, which we can do by:
// configure Timer1
T1CONbits.TMR1GE = 0; // gate disabled
T1CONbits.T1OSCEN = 0; // LP oscillator disabled
T1CONbits.TMR1CS = 0; // internal clock
T1CONbits.T1CKPS = 0b00; // prescale = 1
T1CONbits.TMR1ON = 1; // enable timer
// -> increment TMR1 every 1 us

With TMR1 being incremented every 1 µs, the maximum sample period will be 65.5 ms. You can extend it
to 524 ms by selecting a 1:8 prescaler, or if you need a longer sample period that that, you could run the PIC
at a slower clock rate, as we did in example 4.

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 28
© Gooligum Electronics 2012 www.gooligum.com.au

Since we are only displaying two digits of the ADC result, it is easiest if we configure the ADC with the
result left-justified, with the upper 8 bits in ADRESH:
// configure ADC
ADCON1bits.ADCS = 0b001; // Tad = 8*Tosc = 2.0 us (with Fosc = 4 MHz)
ADCON0bits.ADFM = 0; // MSB of result in ADRESH<7>
ADCON0bits.VCFG = 0; // voltage reference is Vdd
ADCON0bits.CHS = 0b010; // select channel AN2
ADCON0bits.ADON = 1; // turn ADC on

We also need to enable the ADC and Timer0 interrupts:


INTCONbits.T0IE = 1; // enable Timer0 interrupt
PIE1bits.ADIE = 1; // enable ADC interrupt
INTCONbits.PEIE = 1; // enable peripheral interrupts
ei(); // enable global interrupts

Note again that we do not have to enable the CCP or Timer1 interrupts.
The analog-to-digital conversions are triggered automatically when TMR1, after counting for the sample
period (10 ms in this example), matches CCPR1. TMR1 is then automatically reset and the count repeats.
No CCP or Timer1 interrupts are involved. It all “just happens”, behind the scenes.

When the result of the analog-to-digital conversion is ready, the ADC interrupt will be triggered. The ADC
interrupt handler then has to extract the digits from the result, for the Timer0 interrupt handler to display:
// *** Service ADC interrupt
//
// Conversion is initiated by CCP special event trigger,
// every ADCPeriod microseconds
//
PIR1bits.ADIF = 0; // clear interrupt flag

// copy ADC result to display variables


// (to be displayed by Timer0 handler)
dig1 = ADRESH >> 4; // get digit 1 from high nybble of ADRESH
dig2 = ADRESH & 0x0F; // get digit 2 from low nybble of ADRESH

With the ADC interrupt handler extracting the ADC result, and the Timer0 interrupt handler displaying it,
there is nothing for the main loop to do:
/*** Main loop ***/
for (;;)
; // do nothing

Complete program
Here is how the program, based on the ADC interrupt example from lesson 8, comes together:
/************************************************************************
* *
* Description: Lesson 11, example 7 *
* *
* Demonstrates use of CCP compare mode special event trigger *
* to schedule periodic ADC measurements *
* *

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 29
© Gooligum Electronics 2012 www.gooligum.com.au

* Displays ADC output in hexadecimal on 7-segment LED displays *


* *
* Regularly samples analog input, using CCP special event trigger, *
* displaying result as 2 x hex 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 = digit 1 enable (active high) *
* RA5 = digit 2 (ones) enable *
* *
************************************************************************/

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

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


// ext reset, no code or data protect, no brownout detect
#pragma config MCLRE = ON, CP = OFF, CPD = OFF, BOREN = OFF
// no watchdog, power-up timer enabled, int 4 MHz oscillator with I/O
#pragma config WDTE = OFF, PWRTE = ON, FOSC = INTOSCIO
// no failsafe clock monitor, two-speed start-up disabled
#pragma config FCMEN = OFF, IESO = OFF

// Pin assignments
#define sDIG1_EN sPORTC.RC5 // digit 1 (most significant) enable (shadow)
#define sDIG2_EN sPORTA.RA5 // digit 2 (least significant) enable

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


#define ADCPeriod 10000 // ADC sample period in microseconds
// (max 65535)

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


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

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


volatile union { // shadow copy of PORTA
uint8_t RA;
struct {
unsigned RA0 : 1;
unsigned RA1 : 1;
unsigned RA2 : 1;
unsigned RA3 : 1;
unsigned RA4 : 1;
unsigned RA5 : 1;
};
} sPORTA;

volatile union { // shadow copy of PORTC


uint8_t RC;
struct {
unsigned RC0 : 1;
unsigned RC1 : 1;
unsigned RC2 : 1;
unsigned RC3 : 1;

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 30
© Gooligum Electronics 2012 www.gooligum.com.au

unsigned RC4 : 1;
unsigned RC5 : 1;
};
} sPORTC;

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


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

// configure ports
TRISC = 0; // configure PORTA and PORTC as all outputs
TRISA = 1<<2; // except RA2/AN2
ANSEL = 1<<2; // make AN2 (only) analog
CMCON0bits.CM = 7; // disable comparators (mode 7)

// configure Timer0
OPTION_REGbits.T0CS = 0; // select timer mode
OPTION_REGbits.PSA = 0; // assign prescaler to Timer0
OPTION_REGbits.PS = 0b010; // prescale = 8
// -> increment every 8 us
// -> TMR0 overflows every 2.048 ms
INTCONbits.T0IE = 1; // enable Timer0 interrupt

// configure Timer1
T1CONbits.TMR1GE = 0; // gate disabled
T1CONbits.T1OSCEN = 0; // LP oscillator disabled
T1CONbits.TMR1CS = 0; // internal clock
T1CONbits.T1CKPS = 0b00; // prescale = 1
T1CONbits.TMR1ON = 1; // enable timer
// -> increment TMR1 every 1 us

// configure ADC
ADCON1bits.ADCS = 0b001; // Tad = 8*Tosc = 2.0 us (with Fosc = 4 MHz)
ADCON0bits.ADFM = 0; // MSB of result in ADRESH<7>
ADCON0bits.VCFG = 0; // voltage reference is Vdd
ADCON0bits.CHS = 0b010; // select channel AN2
ADCON0bits.ADON = 1; // turn ADC on
PIE1bits.ADIE = 1; // enable ADC interrupt

// initialise ECCP module


CCPR1 = ADCPeriod; // compare vslue = ADC sample period)
CCP1CONbits.CCP1M = 0b1011; // special trigger mode
// -> sample ADC and reset TMR1 on match
PIE1bits.CCP1IE = 1; // enable CCP1 interrupt

// enable interrupts
INTCONbits.PEIE = 1; // enable peripheral interrupts
ei(); // enable global interrupts

/*** Main loop ***/


for (;;)
; // do nothing
}

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


void interrupt isr(void)
{

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 31
© Gooligum Electronics 2012 www.gooligum.com.au

static uint8_t mpx_cnt = 0; // multiplex counter


// current ADC result (in hex):
static uint8_t dig1 = 0; // digit 1 (most significant)
static uint8_t dig2 = 0; // digit 2 (least significant)

// Service all triggered interrupt sources

if (INTCONbits.T0IF)
{
// *** Service Timer0 interrupt
//
// TMR0 overflows every 2.048 ms
//
// Displays current ADC result (in hex) on 7-segment displays
//
INTCONbits.T0IF = 0; // clear interrupt flag

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


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

case 1:
// display digit 2
set7seg(dig2); // output digit 2
sDIG2_EN = 1; // enable digit 2 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;

// copy shadow regs to ports


PORTA = sPORTA.RA;
PORTC = sPORTC.RC;
}

if (PIR1bits.ADIF)
{
// *** Service ADC interrupt
//
// Conversion is initiated by CCP special event trigger,
// every ADCPeriod microseconds
//
PIR1bits.ADIF = 0; // clear interrupt flag

// copy ADC result to display variables


// (to be displayed by Timer0 handler)
dig1 = ADRESH >> 4; // get digit 1 from high nybble of ADRESH
dig2 = ADRESH & 0x0F; // get digit 2 from low nybble of ADRESH
}
}

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 32
© Gooligum Electronics 2012 www.gooligum.com.au

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

/***** Display digit on 7-segment display (shadow) *****/


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
0b011010, // 5
0b011010, // 6
0b010110, // 7
0b011110, // 8
0b011110, // 9
0b010110, // A
0b011000, // b
0b001010, // C
0b011100, // d
0b001010, // E
0b000010 // F
};

// lookup pattern bits and write to shadow registers


sPORTA.RA = pat7segA[digit];
sPORTC.RC = pat7segC[digit];
}

Summary
This has been a long lesson, but as we’ve seen, the ECCP module’s capture and compare modes are useful in
a number of situations where accurate timing is required, from measuring short input signal periods and
pulse widths, to driving precisely-timed output changes, and in generating regular interrupts and analog-to-
digital conversions with an arbitrary period.

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 33
© Gooligum Electronics 2012 www.gooligum.com.au

We’ve also seen that XC8 makes it quite straightforward to configure and use the capture and compare
modes – certainly much more so than with assembly language, because of the ease with which we can
operate on 16-bit values, such as the CCPR1 and TMR1 registers, using C.

You may not need to use the capture and compare modes every day, but when you do, you’ll be glad to have
them available.
However, the CCP module’s pulse-width modulation (PWM) modes are arguably more commonly used,
providing (in conjunction with filtering) a form of analog output, especially useful for applications such as
light dimming and motor control, as we’ll see in the next lesson.

Mid-Range PIC C, Lesson 11: CCP, part 1 – Capture and Compare Page 34

You might also like