Osmeoisis 2022-09-06 15-33-19PIC - Mid - C - 12
Osmeoisis 2022-09-06 15-33-19PIC - Mid - C - 12
Osmeoisis 2022-09-06 15-33-19PIC - Mid - C - 12
au
Note that the DC motor control examples in this lesson require some components not supplied with the basic
Gooligum mid-range PIC training board.
They are detailed in Appendix A, and are available as a kit of parts from www.gooligum.com.au.
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
The PWM output mode is selected by the P1M<1:0> bits, as shown in the table below:
The pin(s) used in each PWM output mode must be configured as outputs by clearing their associated TRIS
bits – for example, on the 16F684 we need to clear TRISC<5> to enable the P1A (same pin as RC5) output.
In the single and half-bridge output modes, some pins, marked with ‘–’ in the table above, are not used as
PWM outputs and are therefore available for use as general I/O pins.
4
this is the only PWM output mode available on the (non-enhanced) CCP module
5
CCP1/P1A is shared with RC5 on the PIC16F684
6
P1B is shared with RC4 on the PIC16F684
7
P1C is shared with RC3 and P1D is shared with RC2 on the PIC16F684
Depending on the circuitry connected to the PWM output, the load or switch might be “activated” or “on”
when the PWM output is driven high, in which case we say that the output is active-high. Or, the load or
switch might be activated by a low PWM output, which we would refer to as active-low.
The CCP1M<1:0> bits are used to configure the PWM outputs as being active-high or active-low, as shown
in the table below:
Note that the PWM outputs are configured in pairs: P1A and P1C always have the same configuration, as do
P1B and P1D. If the combination you need isn’t available, you may need to reconfigure your circuit,
perhaps by adding an inverter before one or more of the load drivers or switches.
Period
Pulse Width
TMR2 = PR2+1
TMR2 = CCPR1L
TMR2 = 0
As you can see, it is a repeating series of digital pulses, with a specific period or frequency (usually fixed)
and pulse width (usually varying).
The timing of these pulses – their period and width – depends on Timer2 (TMR2).
As explained in more detail in mid-range assembler lesson 16, Timer2 can only be driven by the instruction
clock (period TCY), which is four times slower than the processor clock (period TOSC, frequency FOSC).
Timer2 can optionally be used with a prescaler, with a ratio of 1:4 or 1:16.
TMR2 increments until it matches the value stored in the PR2 register, and then resets to 0 on the next
increment cycle. This means that TMR2 has a period equal to PR2+1.
The PWM period is the same as the Timer2 period.
Hence:
PWM period = (PR2+1) × TCY × (Timer2 prescale ratio)
PWM frequency = FOSC / (4 × (PR2+1) × (Timer2 prescale ratio))
For example, if we are using a 4 MHz processor clock and we configure Timer2 with no prescaler and load
249 into PR2:
FOSC = 4 MHz
TOSC = 0.25 µs
TCY = 1 / (Fosc / 4) = 1 µs
prescale ratio = 1
PR2+1 = 249+1 = 250
PWM period = 250 × 1 µs × 1 = 250 µs
PWM frequency = 4 MHz / (4 × 250 × 1) = 4 MHz / 1000 = 4 kHz
8
duty cycle = pulse width ÷ period
9
this is equivalent to left-shifting the value in CCPR1L two times (multiplying it by four), then adding the DC1B bits
For example, if we configure the processor clock and Timer2 as above, and load 100 (decimal) into
CCPR1L and clear the DC1B bits:
CCPR1L:DC1B = 100 (decimal) concatenated with ‘00’ (binary) = 4 × 100 + 0 = 400
PWM pulse width = 400 × 0.25 µs × 1 = 100 µs
PWM duty cycle = 400 / (4 × 250) = 400 / 1000 = 40%
And if the PWM duty cycle is 40%, it means that the load or switch is “active” or “on” 40% of the time, on
average – meaning that 40% of the maximum available power is being delivered to the load.10
Note that when PR2 = 255, the maximum possible duty cycle is 1023 ÷ 1024 = 99.9%.
If you really do need to be able to generate a “100%” output, you need to use a PR2 value less than 255.
Finally, it’s worth noting that the CCPR1L register and DC1B bits are not used directly in the comparison
with TMR2 (and its two hidden less-significant bits). Instead, they are copied to the CCPR1H register
(which is read-only in PWM mode) and a hidden 2-bit latch at the end of each period, and these copied
values are used in the subsequent PWM period.
This is important because it means that you can update the CCPR1L register and DC1B bits at any time,
without worrying about creating output glitches.
This has been a lot of theory to start with! Some examples should help to explain it more clearly…
We’ll look at the various PWM output modes in turn, starting with the simplest and working our way up.
Single-output PWM
In single-output mode, the modulated signal is output on the CCP1/P1A pin.
Although the output is a “square wave”, as illustrated above, we don’t really want the fluctuations between
high and low to be apparent. The intent of PWM output is that these fluctuations are smoothed out and we
only see the average, which, as we’ve seen, is proportional to the duty cycle.
Often this averaging is done by the load, as we’ll see in the LED dimming and motor control examples, later.
However, in our first example we’ll use a simple RC filter to average the PWM output, so that we can see
clearly what’s going on.
10
This is of course the ideal case. In the real world, non-linear factors such as switching losses mean that the relation
between PWM duty cycle and average delivered power will not be linear – but it’s often close.
With the values shown here, with a PWM frequency of 4 kHz, f3dB = 15.9 Hz.
The AC ripple in the output is only 0.4% of the AC component of the original PWM signal, but our analog
output will be limited to frequencies below 16 Hz or so. That’s not much use if we wanted to generate audio,
but it is fine for applications where a slow response is ok.
If you have the Gooligum training board, you can use the solderless breadboard to connect the supplied 1 kΩ
resistor to CCP1/P1A (pin 5, ‘RC5’ on the 16-pin header) and the 1 µF capacitor to ground (pin 16,
‘GND’), as shown in the circuit diagram. No jumpers on the board need to be closed.
To start with, let’s generate a single, active-high PWM signal with a fixed frequency of 4 kHz and a fixed
duty cycle of 50%.
As usual, we’ll configure the PIC to use the internal RC oscillator, providing the default 4 MHz clock:
/***** 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
This means that the instruction clock, which drives Timer2, will run at 1 MHz, which is fast enough to
comfortably generate a 4 kHz PWM signal, as we’ll see.
A 4 kHz PWM signal has a period of 250 µs, so to generate a 4 kHz PWM signal, Timer2 must also have a
period of 250 µs.
If we use a 1:1 prescaler, the 1 MHz instruction clock will increment TMR2 every 1 µs. Hence, we need to
set PR2 to 249, so that TMR2 is reset after 249+1 = 250 counts:
// configure Timer2
T2CONbits.T2CKPS = 0b00; // prescale = 1
T2CONbits.TMR2ON = 1; // enable timer
// -> TMR2 increments every 1 us
PR2 = 249; // period = 250 x 1 us = 250 us
// -> PWM frequency = 4 kHz
To generate a 50% duty cycle, we must set the PWM pulse width to 50% of the period = 125 µs.
Recall that the pulse width is specified by CCPR1L:DC1B<1:0>, and that this 10-bit value is the number of
“quarter TMR2 increments” in the pulse width – it has four times the period’s resolution.
For example, if TMR2 increments every 1 µs, a “quarter increment” is 0.25 µs.
To set the pulse width to 125 µs, we need 125 ÷ 0.25 = 500 of these quarter increments.
This means that we must load CCPR1L:DC1B<1:0> with 500, so we have:
CCPR1L = 125 (= 500÷4)
DC1B =0 (= ‘00’ binary)
Perhaps an easier way to look at this is to say that CCPR1L is equal to the whole part of the pulse width
measured in “TMR2 increments”, and the DC1B bits hold any fractional remainder (0 in this case).
Either way, to configure the ECCP module and specify the pulse width, we have:
// configure ECCP module
CCP1CONbits.PM = 0b00; // select single output mode
// -> CCP1 active
CCP1CONbits.DCB = 0b00; // LSBs of PWM duty cycle = 00
CCP1CONbits.CCP1M = 0b1100; // select PWM mode: all active-high
// -> single output (CCP1) mode, active-high
CCPR1L = 125; // CCPR1L:DC1B<1:0> = 500 -> width = 125 us
// -> PWM duty cycle = 50%
Note that we’re also selecting PWM mode with all active-high outputs (CCP1M = ‘1100’) and single output
mode (P1M = ‘00’) here.
Note also that, although the PIC16F684 data sheet refers to the bitfields within the CCP1CON register as
‘P1M’ and ‘DC1B’, the “pic16f684.h” header file shipped with XC811 are defines them as ‘PM’ and
‘DCB’ within the ‘CCP1CONbits’ structure, as used in the code above. It’s important to always refer to the
11
as of version 1.12
header files shipped with your compiler to check the bit or bitfield symbol definitions, because they may
differ from the names used in the data sheet.
With Timer2 running and the PWM configured like this, a 4 kHz signal with 50% duty cycle will appear on
the CCP1 pin, as shown below:
The yellow
trace is the
PWM output
on CCP1, and
the green trace
is the filtered
output.
The output is
almost flat,
with the AC
ripple barely
visible, and is
around 50% of
the peak PWM
signal level.
With the PWM running “by itself”, the main loop has nothing to do:
for (;;)
{
// do nothing
;
}
Complete program
This is how it all fits together:
/************************************************************************
* *
* Description: Lesson 12, example 1a *
* *
* Demonstrates basic single-output PWM (fixed freq and duty cycle) *
* *
* PWM output is 4 kHz, active-high, 50% duty cycle *
* *
*************************************************************************
* *
* Pin assignments: *
* CCP1 = PWM output *
* *
************************************************************************/
#include <xc.h>
#include <stdint.h>
// configure ports
TRISC = ~(1<<5); // configure PORTC as all inputs
// except RC5 (CCP1 output)
// Setup PWM
// configure Timer2
T2CONbits.T2CKPS = 0b00; // prescale = 1
T2CONbits.TMR2ON = 1; // enable timer
// -> TMR2 increments every 1 us
PR2 = 249; // period = 250 x 1 us = 250 us
// -> PWM frequency = 4 kHz
// configure ECCP module
CCP1CONbits.PM = 0b00; // select single output mode
// -> CCP1 active
CCP1CONbits.DCB = 0b00; // LSBs of PWM duty cycle = 00
CCP1CONbits.CCP1M = 0b1100; // select PWM mode: all active-high
// -> single output (CCP1) mode, active-high
CCPR1L = 125; // CCPR1L:DC1B<1:0> = 500 -> width = 125 us
// -> PWM duty cycle = 50%
For example, for a 25% duty cycle, the pulse width will be 25% × 250 µs = 62.5 µs.
CCPR1L holds the whole number of TMR2 increments in the pulse width (62), and the DC1B bits represent
the fractional remainder (0.5) as a number of “quarter increments” (0.5 × 4 = 2 = ‘10’ binary).
The resulting
PWM signal
and filtered
output are
shown on the
right.
The output
now sits at
25% of the
peak PWM
signal level.
For a 75% duty cycle, the pulse width will be 75% × 250 µs = 187.5 µs.
The whole number of TMR2 increments (1 µs each) is 187, so CCPR1L = 187.
The 0.5 µs remainder is 2 × quarter increments (0.25 µs each), so DC1B<1:0> = 2 (= ‘10’ binary).
The ECCP configuration is then:
CCP1CONbits.PM = 0b00; // select single output mode
// -> CCP1 active
CCP1CONbits.DCB = 0b10; // LSBs of PWM duty cycle = 10
CCP1CONbits.CCP1M = 0b1100; // select PWM mode: all active-high
// -> single output (CCP1) mode, active-high
CCPR1L = 187; // CCPR1L:DC1B<1:0> = 750 -> width = 187.5 us
// -> PWM duty cycle = 75%
What about a 0% duty cycle, where there is no pulse at all (meaning that the load will be fully “off” or
“inactive”)?
That’s easy – there are no TMR2 increments, and no remainder, so CCPR1L = 0, DC1B<1:0> = 0, and we
have:
CCP1CONbits.PM = 0b00; // select single output mode
// -> CCP1 active
CCP1CONbits.DCB = 0b00; // LSBs of PWM duty cycle = 00
CCP1CONbits.CCP1M = 0b1100; // select PWM mode: all active-high
// -> single output (CCP1) mode, active-high
CCPR1L = 0; // CCPR1L:DC1B<1:0> = 0 -> width = 0 us
// -> PWM duty cycle = 0%
Of course, if you really wanted nothing more than a “0% duty cycle”, you’d just configure RC5 to output a
low, and you wouldn’t bother setting up the PWM at all. But this demonstrates that the PWM duty cycle can
be reduced all the way to 0%, if necessary.
Similarly, what if you needed a 100% duty cycle, where the output (and hence the load) is always active?
All you need to do is make the pulse width equal to12 the period.
In this case, the pulse width will be 100% × 250 µs = 250 µs.
The whole number of TMR2 increments (1 µs each) is 250, so CCPR1L = 250.
There is no fractional remainder, so DC1B<1:0> = 0 (= ‘00’ binary).
The ECCP configuration is then:
CCP1CONbits.PM = 0b00; // select single output mode
// -> CCP1 active
CCP1CONbits.DCB = 0b00; // LSBs of PWM duty cycle = 00
CCP1CONbits.CCP1M = 0b1100; // select PWM mode: all active-high
// -> single output (CCP1) mode, active-high
CCPR1L = 250; // CCPR1L:DC1B<1:0> = 1000 -> width = 250 us
// -> PWM duty cycle = 100%
Again, if your program did nothing more than output a “100% active” signal, you’d simply configure RC5 to
output a high, and you wouldn’t bother with PWM. But this shows that the PWM duty cycle can be
increased all the way to 100% – except, as noted earlier, in the case when PR2 = 255, in which case the
highest duty cycle you can achieve is 99.9%.
The table on the right shows the actual output Duty cycle Vout (actual) % max Vout
voltage measured for each duty cycle.
0% 0.000 V 0.0%
With a 5 V supply, the maximum output,
corresponding to a 100% cycle is 4.833 V. The 25% 1.206 V 25.0%
column on the far right shows the measured output 50% 2.414 V 49.9%
voltage as a percentage of this maximum.
75% 3.623 V 75.0%
As you can see, the actual output is within 0.1% of
the expected value for every duty cycle, 100% 4.833 V 100.0%
demonstrating that our DAC is really quite accurate!
12
or greater than, but having a pulse width longer than the period doesn’t make any sense…
The period is still 250 × TMR2 increments – it’s just that each increment is now 4 µs instead of 1 µs.
This means that, because the pulse width is specified in “TMR2 increments”, the duty cycle calculations are
exactly the same as before.
For example, to generate a 25% duty cycle, the ECCP configuration is:
CCP1CONbits.PM = 0b00; // select single output mode
// -> CCP1 active
CCP1CONbits.DCB = 0b10; // LSBs of PWM duty cycle = 10
CCP1CONbits.CCP1M = 0b1100; // select PWM mode: all active-high
// -> single output (CCP1) mode, active-high
CCPR1L = 62; // CCPR1L:DC1B<1:0> = 250 -> width = 250 us
// -> PWM duty cycle = 25%
We still have 250 × TMR2 increments and so the duty cycle calculations remain the same as before.
Note that, as we saw in mid-range assembler lesson 16, to select the 1:16 prescaler, we only need to set the
T2CKPS1 bit – there is no need to specify the whole T2CKPS bitfield.
And once again, for a 25% duty cycle, our ECCP configuration is the same as before:
CCP1CONbits.PM = 0b00; // select single output mode
// -> CCP1 active
CCP1CONbits.DCB = 0b10; // LSBs of PWM duty cycle = 10
CCP1CONbits.CCP1M = 0b1100; // select PWM mode: all active-high
// -> single output (CCP1) mode, active-high
CCPR1L = 62; // CCPR1L:DC1B<1:0> = 250 -> width = 250 us
// -> PWM duty cycle = 25%
The resultant 250 Hz PWM signal, with a duty cycle of 25%, is shown below:
The yellow
trace is the
PWM output
on CCP1, and
the green trace
is the filtered
output.
With this
much ripple in
the output, it is
now very
obvious that
our RC filter is
inadequate for
use with a 250
Hz signal – the
R and/or C
values should
be increased to
compensate for
the lower
frequency.
13
a topic for a future tutorial?
If you have the Gooligum training board, you can implement this circuit by placing a shunt across pins 1 and
2 (‘POT’) of JP2414, connecting the 10 kΩ pot (RP2) to AN0, and placing another shunt across pins 1 and 2
(‘GND’) of JP23, connecting the piezo speaker to ground.
Recall that the PWM frequency, and hence period, depends on the Timer2 period, which in turn depends on
the processor clock frequency, the Timer2 prescaler, and the value in the PR2 register. So, assuming that
the processor clock and Timer2 configuration remain the same, to the PWM frequency will change if the
value in PR2 changes.
Given a 4 MHz processor clock and the 1:16 Timer2 prescaler, the lowest PWM frequency (PR2 = 255) is
244 Hz, while the highest PWM frequency (PR2 = 0) is 62.5 kHz.
That’s not a great match for the range of human hearing, but most piezo speakers, such as the one on the
Gooligum training board, are ineffective at low frequencies, so the 244 Hz – 62.5 kHz range actually works
well in practice (you just won’t be able to hear the highest frequencies…).
We’re explicitly selecting the 1:1 postscaler (even though 1:1 is the default), because we’ll be using the
TMR2IF flag to wait for the start of a new PWM cycle – see below.
We don’t want to load PR2 with a fixed value – it has to vary (varying the PWM frequency) as the
potentiometer is adjusted.
The easiest way to do that is to use the ADC (see lesson 8) to read the analog voltage on AN0, and, in the
main loop, continually copy the most significant eight bits of the ADC result to PR2.
This means that we need to configure AN0 as an analog input:
TRISC = ~(1<<5); // configure PORTC as all inputs
// except RC5 (CCP1 output)
ANSEL = 1<<0; // make AN0 (only) analog
The ADC is then configured for use with a 4 MHz processor clock and with the most significant eight bits of
the 10-bit ADC result in the ADRESH register:
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 = 0b000; // select channel AN0
ADCON0bits.ADON = 1; // turn ADC on
Within the main loop we can then update the PWM period by simply copying the most significant eight bits
of the ADC result from ADRESH to PR2:
// set new PWM period
PR2 = ADRESH; // PWM period = high byte of ADC result + 1
14
or place the shunt across pins 2 and 3 (‘LDR1’) of JP24 to make a light-controlled musical instrument!
However, before updating the PWM period, we should wait for the end of the current PWM cycle, which
occurs when Timer2 overflows. Recall (from lesson 10) that, if the Timer2 postscale ratio is set to 1:1, the
TMR2IF interrupt flag will be set, every time Timer2 overflows.
So, to wait for the end of the current PWM cycle, we can use:
// wait for end of PWM period
PIR1bits.TMR2IF = 0; // clear Timer2 interrupt flag
while (!PIR1bits.TMR2IF) // then wait for it to go high
;
For a fixed duty cycle (for simplicity, let’s say 50%), we need to calculate a new pulse width whenever we
update the period.
Recall that the PWM period, measured in “TMR2 increments”, is given by PR2+1.
If we declare a variable to store the period:
uint16_t period; // PWM period (= PR2+1)
Note that the ‘period’ variable is declared as an unsigned 16-bit integer. We can’t use an 8-bit variable,
because PR2 is an 8-bit register: if PR2 = 255 and we add 1 to it, we want to get 255+1 = 256. If the result
had to be truncated to fit into an 8-bit variable, we’d get 255+1 = 0, which is not what we want!
To some extent C makes our lives easy by hiding complexities relating to variable and register size (in the
corresponding example in mid-range assembler lesson 18, we had to handle “PR2 = 255” as a special case,
to avoid this overflow problem). But you do need to be aware of these issues, and ensure that your variables
are large enough to store every possible value, to avoid potentially difficult-to-find bugs!
For a 50% duty cycle, the pulse width is always half the period.
Since CCPR1L holds the whole part of the pulse width, we have simply:
CCPR1L = period/2;
But what if the period doesn’t divide evenly by two? As a whole number of increments, to be represented
accurately, the pulse width would have to have a fractional part – and that’s what the DC1B bits are for.
We don’t need to worry about quarter-increments, so we can leave DC1B<0> = 0.
If the period is divides evenly by two, there will be no half-increment, so DC1B<1> = 0.
However, if the period is odd, the pulse width will need to include a half-increment, so DC1B<1> = 1.
That is, DC1B<1> should be set to the remainder after dividing the period by two, and for that we can use
the modulus operator:
CCP1CONbits.DC1B1 = period%2;
The C language makes this a lot simpler that the corresponding example in mid-range assembler lesson 18!
Complete program
This is how all these pieces fit together:
/************************************************************************
* *
* Description: Lesson 12, example 2 *
* *
* Demonstrates varying single-output PWM frequency (fixed duty cycle) *
* *
* Sounds piezo on CCP1 at 244 - 62500 Hz, *
* with frequency derived from an analog input *
* *
*************************************************************************
* *
* Pin assignments: *
* CCP1 = PWM output (e.g. piezo speaker) *
* AN0 = analog input (e.g. pot or LDR) *
* *
************************************************************************/
#include <xc.h>
#include <stdint.h>
// configure ports
TRISC = ~(1<<5); // configure PORTC as all inputs
// except RC5 (CCP1 output)
ANSEL = 1<<0; // make AN0 (only) analog
// 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 = 0b000; // select channel AN0
ADCON0bits.ADON = 1; // turn ADC on
// Setup PWM
// configure Timer2
T2CONbits.TOUTPS = 0; // postscale = 1:1
T2CONbits.T2CKPS1 = 1; // prescale = 16
T2CONbits.TMR2ON = 1; // enable timer
// -> TMR2 increments every 16 us
// configure ECCP module
CCP1CONbits.PM = 0b00; // select single output mode
// -> CCP1 active
When you try this program, you will find that the frequency produced by the piezo speaker (or whatever
you’re using for audio output) will vary as the voltage on AN0 varies, although any noise in the analog input
will result in the output tone not sounding “pure”, especially at higher frequencies. This could be reduced by
filtering (smoothing) the analog input, whether via hardware or software (see lesson 8).
For the highest PWM resolution, where the duty cycle can be adjusted in the smallest-possible increments,
we should make the value of PR2 as high as possible, which, because it’s an 8-bit register, is 255.
Keep in mind that, if you increase the PWM frequency by making the value in PR2 smaller, you will
decrease the PWM duty cycle resolution.
We want to demonstrate the full range of duty cycles, so we’ll make PR2 = 255.
15
As mentioned, if you were dimming a number of LEDs, you would normally arrange them in string, switched by a
series MOSFET, in a similar way to the motor control example, below.
16
or, if you place the shunt across pins 2 and 3 (‘LDR1’) of JP24, the LED will be controlled by photocell PH1
With our usual 4 MHz clock, and no Timer2 prescaler, TMR2 will increment every 1 µs:
// configure Timer2
T2CONbits.T2CKPS = 0b00; // prescale = 1
T2CONbits.TMR2ON = 1; // enable timer
// -> TMR2 increments every 1 us
With PR2 = 255 the TMR2 period is then 256 × 1 µs = 256 µs, and the PWM frequency is 3906 Hz17:
PR2 = 255; // period = 256 x 1 us = 256 us
// -> PWM frequency = 3906 Hz
This is more than fast enough to avoid the LED perceptibly flickering.
We don’t really need more than eight bits of PWM duty cycle resolution (can you notice a difference of less
than 0.4% in an LED’s brightness?), so we can ignore the DC1B bits, and leave them cleared:
// configure ECCP module
CCP1CONbits.PM = 0b00; // select single output mode
// -> CCP1 active
CCP1CONbits.DCB = 0b00; // LSBs of PWM duty cycle = 00
CCP1CONbits.CCP1M = 0b1100; // select PWM mode: all active-high
// -> single output (CCP1) mode, active-high
Since we only need eight bits of resolution, we only need the most significant eight bits of the ADC result, so
we can configure the ADC with:
// 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 = 0b000; // select channel AN0
ADCON0bits.ADON = 1; // turn ADC on
Whenever we sample AN0, the 8-bit ADC result will now appear in ADRESH.
To update the duty cycle, we can simply copy it to CCPR1L, within the main loop:
// set new PWM duty cycle
CCPR1L = ADRESH; // PWM duty cycle
// = high byte of ADC result / 256
Note that, if we did need the full 10-bit PWM resolution, we could extract the two least significant bits of the
ADC result from ADRESL, and copy them into the DC1B bits in CCP1CON.
But for this application there’s really no need to go to that extra effort18.
17
At power-on, the value in PR2 is 255 by default, so strictly-speaking there is no need to load 255 into PR2 like this.
But it’s good practice to explicitly do so, in case you want to change the PWM frequency later.
18
although, when programming in C, it’s fairly easy to do this – you might want to try it, as an exercise.
Complete program
Here is the complete source code, showing how these fragments fit together:
/************************************************************************
* *
* Description: Lesson 12, example 3 *
* *
* Demonstrates varying single-output PWM duty cycle *
* *
* Outputs PWM signal (~3906 Hz) on CCP1 *
* with duty cycle derived from an analog input *
* *
*************************************************************************
* *
* Pin assignments: *
* CCP1 = PWM output (e.g. LED or motor) *
* AN0 = analog input (e.g. pot or LDR) *
* *
************************************************************************/
#include <xc.h>
#include <stdint.h>
// configure ports
TRISC = ~(1<<5); // configure PORTC as all inputs
// except RC5 (CCP1 output)
ANSEL = 1<<0; // make AN0 (only) analog
// 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 = 0b000; // select channel AN0
ADCON0bits.ADON = 1; // turn ADC on
// Setup PWM
// configure Timer2
T2CONbits.T2CKPS = 0b00; // prescale = 1
T2CONbits.TMR2ON = 1; // enable timer
// -> TMR2 increments every 1 us
PR2 = 255; // period = 256 x 1 us = 256 us
// -> PWM frequency = 3906 Hz
// configure ECCP module
CCP1CONbits.PM = 0b00; // select single output mode
// -> CCP1 active
CCP1CONbits.DCB = 0b00; // LSBs of PWM duty cycle = 00
When you try this program, you may find that with the trimpot on the Gooligum training board fully counter-
clockwise, the LED still glows very faintly, appearing to flicker a little. This is due to noise in the analog
input, which, as in the previous example, could be reduced through hardware or software filtering – or
perhaps by modifying the “set new PWM duty cycle” routine so that, if the ADC result is below some
threshold, the duty cycle is set to zero.
It would be rare to find a motor that can operate with less than the 25 mA that one of the PIC16F684’s pins
can supply. Indeed, even the tiny motor supplied in the Gooligum training board motor control kit can draw
more than 150 mA. And although that motor will work fine with a 5 V supply, many DC motors are
designed for 12 V or 24 V operation. This means that driving a DC motor isn’t as simple as connecting it
directly to a PIC pin, as we were able to do with the LED in the example above. Instead, we need to use
some type of driver, or external switch.
A straightforward approach is to use a driver chip, such as the SN754410 (a pin-compatible, but improved,
version of the venerable L293D), as shown in the circuit below:
As we did in the last example, we can use the potentiometer connected to AN0 to control the PWM duty
cycle, which will in turn control the speed of the motor.
And as you can see, we don’t need any components other than a 754410 or L293D chip to drive the motor.
The “input” side of the 754410 is intended to connect directly to “5 V” digital logic, the input circuitry
having its own supply (VCC1), separated from the output circuits.
The motor can be driven directly by one of the outputs. The 754410 (like the L293D) includes internal
diodes on its outputs which clamp the voltage spikes generated by inductive loads (such as motors) when the
voltage across them changes suddenly, avoiding the need for an external flyback diode. That’s important
when using PWM, which by its nature involves sudden on/off voltage changes, to drive a motor.
The voltage delivered to the motor is derived from the “output” power supply connected to VCC2, which can
be up to 36 V. Although the same voltage is used for the input and output supplies in this example, you’d
typically use a separate supply on the output side, allowing for a higher motor voltage and, importantly,
isolating the output from the PIC’s power supply.
If you do use a single 5 V supply for the digital and output circuitry (as shown below), you must use an
external well-regulated power supply, connected via your training or development board or directly to the
breadboard. Do not use a PICkit 2 or PICkit 3 alone to power the whole circuit.
Adding a 1 µF ceramic bypass capacitor (supplied with your Gooligum training board) will help avoid high-
frequency noise feeding back from the motor to the power supply, potentially causing problems for the PIC,
which runs off the same supply.
However, to avoid problems it’s better to use a separate supply on the output side – even a 4.5 V battery
pack will do. It is then ok to use a PICkit 2 or PICkit 3 to power the logic circuits, including the PIC.
You can build this circuit with the Gooligum training board, using the SN754410 IC and motor supplied in
the motor control kit – connecting them to signals on the 16-pin header: CCP1/P1A on pin 5 (‘RC5’), +5 V
and ground on pins 15 (‘+V’) and 16 (‘GND’), using the solderless breadboard, as illustrated below.
You also need to place a shunt across pins 1 and 2 (‘POT’) of JP24, connecting the 10 kΩ pot (RP2) to AN0.
Note that the motor supplied in the motor control kit may be different to that shown, and that you may need
to solder some solid-core wire (supplied with the kit) to the motor to connect it to the breadboard, or use
crocodile clip leads.
Also note that a piece of foam has been pushed onto the motor spindle, to make it easier to see it spin.
An alternative approach is to use an N-channel logic level MOSFET switch, as in the circuit shown on the
next page.
If you do choose to use a single 5 V supply for the motor as well as the digital logic circuitry, as illustrated
here, note again that you must use an external well-regulated power supply, instead of using a PICkit 2 or
PICkit 3 to power the whole circuit.
It’s also possible to use a P-channel MOSFET as a “high-side” switch, between the motor and the positive
supply. As we’ll see in the full-bridge example later, that’s easy enough to do with a logic level MOSFET,
such as the NDP6020P supplied in the motor control kit – as long as the motor power supply is 5 V. If you
need to drive a higher-voltage motor, a high-side switch requires additional level-shifting circuitry to drive
the MOSFET – something which is not necessary with low-side N-channel MOSFETs. Additionally, P-
channel MOSFETs tend to be more expensive than equivalent N-channel devices. For these reasons, a low-
side switch, as in the circuit above, is more commonly used for an application like this.
Whether you use the SN754410 driver or the N-channel MOSFET, the method of PWM control is exactly
the same: a “high” on the CCP1/P1A pin will turn the motor on, a “low” turns it off, and increasing the
PWM duty cycle will deliver more power to the motor.
In fact, this is exactly the same as in the LED dimming example, above. A higher duty cycle means more
light from the LED, or more speed from the motor. Only the circuit is different.
So, whichever motor control circuit you use, if you try the program from example 3, you will find that the
motor speed varies from “stopped” to “full speed” as you turn the potentiometer.
However, despite the earlier comment that it’s common to use a PWM frequency of 4 kHz or higher to avoid
an audible whine from the motor, the motor supplied with the motor control kit for the Gooligum training
board may perform better with a lower drive frequency.
We can easily lower the PWM frequency by using the Timer2 prescaler.
Selecting the 1:16 prescaler increases the TMR2 period by 16 times, to 4096 µs:
// configure Timer2
T2CONbits.T2CKPS1 = 1; // prescale = 16
T2CONbits.TMR2ON = 1; // enable timer
// -> TMR2 increments every 16 us
PR2 = 255; // period = 256 x 16 us = 4096 us
// -> PWM frequency = 244 Hz
There is no need to list the rest of the program; it is the same as in example 3.
With this change, the PWM frequency is reduced to 244 Hz, and you may find that you now have better
control at low speeds (the motor stops spinning at a lower duty cycle than before).
Of course, every motor is different, and you may need to do some testing to find the optimal drive frequency
for your motor and control circuitry (e.g. switching losses will increase as the PWM frequency increases).
But there’s a risk. If both switches are ever “on” at the same time, a (potentially very high) shoot-through
current will flow directly from +V to ground.
This can occur inadvertently, if, when P1A and P1B are flipping between their active and inactive states,
one MOSFET takes longer to switch off than the other takes to switch on. For a brief time, during the
transition, both MOSFETs would be on, and a shoot-through current would flow.
To avoid this, the half-bridge output mode provides a “programmable dead-band delay” mode, where a delay
(called the dead-band) is introduced between one signal switching to inactive and the other becoming active,
giving the “on” switch enough time to fully turn off, before the other switch turns on.
This dead-band delay is set by the PDC<6:0> bits in the PWM1CON register.
However, as we will not be using this half-H driver arrangement in the examples in this section, we will not
make use of or further illustrate the programmable dead-band delay mode in this lesson.
If you do need to use this feature, please refer to the “programmable dead-band delay mode” section of the
data sheet.
19
The reason for the “half-H” name will become apparent when we look at the full-bridge example, later…
20
Each of the four driver circuits in a L293 (or compatible) can actively pull its output high or low, by sourcing or
sinking current, which is why the L293 is referred to as a “quad half-H” driver.
P1A
P1B
P1A – P1B
If a load is connected directly across the P1A and P1B pins, it will “see” the difference in voltage between
them: P1A – P1B, as shown.
As you can see, the total voltage swing between the two PWM phases is then twice as large as it is for a
single PWM output.
The code is the same as that used in example 1 to generate a 4 kHz PWM output, with 50% duty cycle21,
except that we need to configure both P1A and P1B as outputs:
// configure ports
TRISC = ~(1<<5|1<<4); // configure PORTC as all inputs
// except RC5 and RC4 (P1A, P1B outputs)
and of course we need to set the P1M bits to ‘10’ to select half-bridge output mode when configuring the
ECCP module:
// configure ECCP module
CCP1CONbits.PM = 0b10; // select half-bridge output mode
// -> P1A, P1B active
CCP1CONbits.DCB = 0b00; // LSBs of PWM duty cycle = 00
CCP1CONbits.CCP1M = 0b1100; // select PWM mode: all active-high
// -> half-bridge output mode,
// P1A, P1B active-high
21
You may want to experiment with different duty cycles, to hear how it affects the tone produced.
Assuming that the PWM outputs are both configured as active-high, during the active phase of the PWM
signal, P1A will be high and therefore the 754410’s ‘1Y’ output will be driven high. At the same time, P1B
will be low, driving the ‘2Y’ output low. Current will flow from 1Y to 2Y, causing the motor to attempt to
turn in the forward direction.
During the inactive PWM phase, P1A will be low and P1B will be high, driving the ‘1Y’ output low and
‘2Y’ high. Current will now flow in the reverse direction, from 2Y to 1Y, causing the motor to reverse.
If the duty cycle is 50%, the PWM output divides its time equally between the active and inactive phases,
and the current flow in the forward direction will be, on average, the same as that in the reverse direction. If
the PWM frequency was low enough, we’d see the motor jitter back and forth, attempting to alternately
move forward and reverse. But if the PWM frequency is high enough, a combination of the motor’s winding
inductance and mechanical inertia will effectively average this alternating forward and reverse current to
zero – the motor won’t move.
With a 100% duty cycle P1A is always high, P1B is always low and current only flows in the forward
direction, from 1Y to 2Y – and the motor will spin forward at maximum speed.
As the other extreme (0% duty cycle), P1A is always low and current only flows in the reverse direction,
from 2Y to 1Y – driving the motor in reverse, at maximum speed.
So we have:
0% duty cycle = full reverse
50% duty cycle = stopped
100% duty cycle = full forward
And because of the averaging effect, the motor speed varies smoothly between these points as the PWM duty
cycle is adjusted.
We can illustrate this with the circuit shown above, using the potentiometer connected to AN0 to control the
PWM duty cycle, as we did in examples 3 and 4.
If you have the Gooligum training board, you can build the circuit in the same way as in example 4,
connecting the SN754410 (or L293D) IC and motor supplied in the motor control kit to signals on the 16-pin
header: P1A on pin 5 (‘RC5’), P1B on pin 6 (‘RC4’), +5 V and ground on pins 15 (‘+V’) and 16 (‘GND’),
via the solderless breadboard. You also need to place a shunt across pins 1 and 2 (‘POT’) of JP24,
connecting the 10 kΩ pot (RP2) to AN0.
And again, ideally you should use a separate supply on the output side. However if you do choose to use a
single 5 V supply for both the output and logic circuitry, ensure that you use an external regulated 5 V power
supply, instead of using a PICkit 2 or PICkit 3 to power the whole circuit.
The program code is essentially the same as that in examples 3 and 4, with only a few differences.
As we did in the last example, we now need to configure both P1A and P1B as outputs:
// configure ports
TRISC = ~(1<<5|1<<4); // configure PORTC as all inputs
// except RC5 and RC4 (P1A, P1B outputs)
In example 4 we used a PWM frequency of only 244 Hz, because the motor supplied with the motor control
kit for the Gooligum training board may perform better with a lower drive frequency. However, when the
motor is driven in push/pull fashion, as in this example, that drive frequency may be too low – it might make
the motor vibrate. Better results may be obtained with a PWM frequency closer to 1 kHz.
And finally of course we need to set the P1M bits to ‘10’ to select half-bridge output mode when configuring
the ECCP module:
// configure ECCP module
CCP1CONbits.PM = 0b10; // select half-bridge output mode
// -> P1A, P1B active
CCP1CONbits.DCB = 0b00; // LSBs of PWM duty cycle = 00
CCP1CONbits.CCP1M = 0b1100; // select PWM mode: all active-high
// -> half-bridge output mode,
// P1A, P1B active-high
Complete program
Although the program is otherwise the same as that in example 3, it’s worth listing it again in full, so that
you can see where these few changes fit in:
/************************************************************************
* *
* Description: Lesson 12, example 6 *
* *
* Demonstrates varying half-bridge output PWM duty cycle *
* *
* Outputs complementary PWM signals (~977 Hz) on P1A and P1B *
* with duty cycle derived from an analog input *
* *
*************************************************************************
* *
* Pin assignments: *
* P1A = normal PWM output *
* P1B = complementary PWM output *
* AN0 = analog input (e.g. pot or LDR) *
* *
************************************************************************/
#include <xc.h>
#include <stdint.h>
// configure ports
TRISC = ~(1<<5|1<<4); // configure PORTC as all inputs
// except RC5 and RC4 (P1A, P1B outputs)
ANSEL = 1<<0; // make AN0 (only) analog
// 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 = 0b000; // select channel AN0
ADCON0bits.ADON = 1; // turn ADC on
// Setup PWM
// configure Timer2
T2CONbits.T2CKPS = 0b01; // prescale = 4
T2CONbits.TMR2ON = 1; // enable timer
// -> TMR2 increments every 4 us
PR2 = 255; // period = 256 x 4 us = 1024 us
// -> PWM frequency = 977 Hz
// configure ECCP module
CCP1CONbits.PM = 0b10; // select half-bridge output mode
// -> P1A, P1B active
CCP1CONbits.DCB = 0b00; // LSBs of PWM duty cycle = 00
CCP1CONbits.CCP1M = 0b1100; // select PWM mode: all active-high
// -> half-bridge output mode,
// P1A, P1B active-high
When driving a brushed DC motor, the H-bridge can be operated as a brake to rapidly stop the motor, by
switching on the two “lower” MOSFETs (connected P1B and P1D) while switching the others off.
The H-bridge is then equivalent to the (very!) simple circuit on the right.
The two sides of the motor are grounded, effectively shorting them.
Note that “brake mode” is not provided by the ECCP mode; to operate the H-bridge in this way, the ECCP
module should be disabled (by clearing the CCP1M bits) and the PWM outputs configured directly by
setting or clearing RC2, RC3, RC4 and RC5 pins as appropriate.
This is the same as we did in example 4, except that, instead of a single PWM output, now we’re using a full-
bridge.
The lower half of the H-bridge will be familiar from example 4. Logic-level N-channel MOSFETs, such as
the PSMN022-30PL devices shown here, are connected to P1B and P1D via 220 Ω resistors (used to limit
the gate in-rush current), while 10 kΩ resistors ensure that the MOSFETs remain off while the PWM outputs
are in their initial high-impedance state. If either the P1B or P1D output is set high, the corresponding
MOSFET is switched on. Thus, P1B and P1D should be configured as active-high.
The upper half of the H-bridge uses logic-level P-channel MOSFETs, such as the NDP6020P devices
supplied in the Gooligum motor control kit. It’s a solution that works well in this example, but it does mean
that the motor power supply voltage cannot be higher than the PIC’s supply voltage – otherwise, the PIC’s
PWM outputs would not be able to pull the MOSFET’s gates high enough to turn them off.
So, although you could in principal use a higher voltage on the motor side, allowing the use of a more
powerful motor, you can’t do that with this simple circuit. You’d need to add MOSFET drivers. That’s not
really a problem – using a higher motor drive voltage is such a common requirement that many appropriate
drivers are available, including entire H-bridges with logic-level inputs in a single package. We’ve used
discrete components here to more clearly illustrate the full-bridge modes (you can see the “H” clearly…), but
in a real design you may be more likely to deploy an integrated H-bridge.
With the P-channel MOSFETs connected as shown, if either the P1A or P1C output is driven low, the
corresponding MOSFET is switched on. Thus, P1A and P1C should be configured as active-low.
Once again the additional 1 µF ceramic bypass capacitor (supplied with the Gooligum training board) helps
to reduce high-frequency switching noise feeding back from the motor control circuit to the power supply.
You can build this circuit with the Gooligum training board, using the MOSFETs, resistors and motor
supplied in the motor control kit using the solderless breadboard, as illustrated below.
You will need to make
connections to the
following pins on the
16-pin header on the
Gooligum training
board:
P1A on pin 5 (‘RC5’),
P1B on pin 6 (‘RC4’),
P1C on pin 7 (‘RC3’),
P1D on pin 11 (‘RC2’),
+5V on pin 15 (‘+V’),
Ground on pin 16
(‘GND’).
You must also place a
shunt across pins 1 and
2 (‘POT’) of JP24,
connecting the 10 kΩ
pot (RP2) to AN0.
Most of the program code is exactly the same as that used in examples 3 and 4 – the only difference is that
we’re using a different PWM mode.
Since we’re now using the P1A, P1B, P1C and P1D pins for PWM, we have to configure them as outputs:
// configure ports
TRISC = 0b000011; // configure PORTC as all inputs
// except RC2-5 (P1A-D outputs)
As we did in example 4, we’ll use a PWM frequency of 244 Hz, because it’s appropriate for the motor
supplied with the Gooligum motor control kit:
// configure Timer2
T2CONbits.T2CKPS1 = 1; // prescale = 16
T2CONbits.TMR2ON = 1; // enable timer
// -> TMR2 increments every 16 us
PR2 = 255; // period = 256 x 16 us = 4096 us
// -> PWM frequency = 244 Hz
And of course we have to set the P1M bits to ‘01’ to select full-bridge output forward mode, and the
CCP1M bits to ‘1110’ to configure the P1A and P1C pins as active-low, and P1B and P1D as active-high:
// configure ECCP module
CCP1CONbits.PM = 0b01; // select full-bridge output forward mode
// -> PID modulated, P1A active,
// P1B, PIC inactive
CCP1CONbits.DCB = 0b00; // LSBs of PWM duty cycle = 00
CCP1CONbits.CCP1M = 0b1110; // select PWM mode: P1A, P1C active-low
// P1B, P1D active-high
// -> full-bridge output forward mode,
// P1D modulated (active-high)
// P1A active (low)
// P1B inactive (low)
// P1C inactive (high)
Alternatively, we could select full-bridge output reverse mode (setting the P1M bits to ‘11’) with:
// configure ECCP module
CCP1CONbits.PM = 0b11; // select full-bridge output reverse mode
// -> PID modulated, P1A active,
// P1B, PIC inactive
CCP1CONbits.DCB = 0b00; // LSBs of PWM duty cycle = 00
CCP1CONbits.CCP1M = 0b1110; // select PWM mode: P1A, P1C active-low
// P1B, P1D active-high
// -> full-bridge output reverse mode,
// P1B modulated (active-high)
// P1C active (low)
// P1A inactive (high)
// P1D inactive (low)
Since the rest of the code is exactly the same as in example 4, there is no need to list the whole program here.
22
and of course other types of motor
speed in the “forward” direction. When the pot is at the other extreme, the motor will spin at full speed in
the “reverse” direction. The motor speed will vary smoothly between these extremes as the pot is adjusted,
coming to a stop when the pot is half-way.
As we’ve seen, setting the P1M bits in the CCP1CON register to ‘01’ select full-bridge output forward
PWM mode, while setting the P1M bits to ‘11’ selects full-bridge output reverse.
Thus, setting P1M<0> selects full-bridge output mode, regardless of the direction.
The P1M<1> bit then controls the direction: clearing it selects forward mode, and setting it selects reverse.
Our motor direction will be controlled by the AN0 input, so we’ll need to select the direction in the main
loop, after reading AN0.
This means that, when we configure the ECCP module in the initialisation code, we don’t care (at this stage)
what the motor direction is; we only need to ensure that P1M<0> is set:
// configure ECCP module
CCP1CONbits.P1M0 = 1; // select full-bridge output mode
// -> PIA, P1B, P1C, P1D are PWM outputs
CCP1CONbits.DCB = 0b00; // LSBs of PWM duty cycle = 00
CCP1CONbits.CCP1M = 0b1110; // select PWM mode: P1A, P1C active-low
// P1B, P1D active-high
// -> full-bridge output mode, PWM outputs:
// P1A active-low
// P1B active-high
// P1C active-low
// P1D active-high
Note that, in the comments, we’re no longer describing specific PWM outputs as “modulated” or “active”, as
these roles will change depending on whether forward or reverse mode is selected.
We want the motor to spin forward if the pot is turned more than half-way clockwise. On the Gooligum
training board, this corresponds to a voltage on AN0 of more than VDD/2.
If we continue to configure the ADC with the most significant eight bits of the result in ADRESH, the value
in ADRESH will vary from 0 with the pot fully counter clockwise to 255 when is fully clockwise.
Mid-way, the ADC result will be 127 or 128 (we have to use whole numbers here…).
So we want:
Motor full speed reverse: ADRESH = 0
Motor stopped: ADRESH = 127 or 128
Motor full speed forward: ADRESH = 255
In mid-range assembler lesson 18, we noted that this meant that we could use the most significant bit of the
ADC result (ADRESH<7>) to indicate the motor direction:
If ADRESH<7> = 1, ADRESH ≥ 128 and the motor is either spinning forward or stopped.
Otherwise, it is either spinning in reverse or stopped.
However, in C it is more natural to compare the ADC result with a threshold value, instead of testing a single
bit, so we will use the construct:
// set motor direction and speed
if (ADRESH >= 128) // value of ADRESH determines motor direction
{
//*** forward mode
}
else
{
//*** reverse mode
It would seem tempting to ‘#define’ the threshold as a symbol (constant) at the start of the code, and relate
all the direction decisions and duty cycle calculations to this constant, so that you can easily change the
threshold later (you might decide that only the lower 1/3 of the potentiometer’s range should be for reverse,
and the upper 2/3 for forward mode), without having to modify any of this code. As we’ve seen before, that
type of abstraction can be a very good idea, making the program more maintainable, but we’ve also seen that
C compilers (including XC8) generate more efficient code when multiplying or dividing by powers of two.
Making the threshold arbitrary could lead (inadvertently) to some very inefficient calculations, potentially
using a lot of memory to implement them. On the other hand, making this code more general and flexible is
challenge that you may wish to take on!
In forward mode, we want ADRESH = 128 to correspond to stopped (PWM duty cycle = 0%) and
ADRESH = 255 to correspond to full speed (PWM duty cycle = 100%).
So, ideally, we would want:
duty cycle = (ADRESH – 128) ÷ 127
We previously set the duty cycle by copying ADRESH to CCPR1L, and with the DC1B bits in the
CCP1CON register clear, the PWM duty cycle is equal to CCPR1L ÷ 256.
Again, ideally, this means that we should calculate:
CCPR1L = (ADRESH – 128) × 256 ÷ 127
= (ADRESH – 128) × 2.01575
That’s an unnecessarily complex calculation for the compiler to implement, even if performed using fixed-
point arithmetic instead of floating point, when the value we are multiplying by is so close to 2.
We can get very close to the “correct” duty cycle with the much simpler:
// set new PWM duty cycle
CCPR1L = (ADRESH-128)*2; // PWM duty cycle =
// (high byte of ADC result - 128) / 128
If ADRESH = 128, (ADRESH – 128) × 2 = 0 and CCPR1L = 0 → PWM duty cycle = 0%.
If ADRESH = 255, (ADRESH – 128) × 2 = 254 and CCPR1L = 254 → PWM duty cycle = 99.2%.
That’s not quite 100%, but it’s close enough for the purposes of this example, so we’ll use this simpler duty
cycle calculation.
Having set the new duty cycle, we can simply clear the P1M<1> bit to select full-bridge forward mode:
// select forward direction
CCP1CONbits.P1M1 = 0; // select full-bridge output forward mode
// -> P1D modulated (active-high)
// P1A active (low)
// P1B inactive (low)
// P1C inactive (high)
When the direction control bit (P1M<1>) is changed, the ECCP module will wait until the end of the current
PWM period to effect the change.
The new direction and duty cycle will then be used for the following PWM period.
In reverse mode, we want ADRESH = 127 to correspond to stopped (PWM duty cycle = 0%) and ADRESH
= 0 to correspond to full speed (PWM duty cycle = 100%).
So ideally:
duty cycle = (127 – ADRESH) ÷ 127
and:
CCPR1L = (127 – ADRESH) × 256 ÷ 127
= (127 – ADRESH) × 2.01575
Again, that’s an unnecessarily complex calculation, when the value we are multiplying by is so close to 2.
We can get very close to the “correct” duty cycle with the much simpler:
// set new PWM duty cycle
CCPR1L = (127-ADRESH)*2; // PWM duty cycle =
// (127 - high byte of ADC result) / 128
If ADRESH = 127, (127 – ADRESH) × 2 = 0 and CCPR1L = 0 → PWM duty cycle = 0%.
If ADRESH = 0, (127 – ADRESH) × 2 = 254 and CCPR1L = 254 → PWM duty cycle = 99.2%.
Again, that’s not quite 100%, but it’s close enough for this example.
We can now go ahead and set the P1M<1> bit to select full-bridge reverse mode:
// select reverse direction
CCP1CONbits.P1M1 = 1; // select full-bridge output reverse mode
// -> P1B modulated (active-high)
// P1C active (low)
// P1A inactive (low)
// P1D inactive (high)
Complete program
Here is the full source code, showing how all these pieces fit together:
/************************************************************************
* *
* Description: Lesson 12, example 8 *
* *
* Demonstrates variable speed bidirectional brushed DC motor control, *
* using full-bridge PWM mode *
* *
* Outputs PWM signals (~244 Hz) on P1A-D in full-bridge mode *
* with direction snd duty cycle derived from an analog input *
* *
* If analog input is full-range potentiometer, motor will be: *
* full speed forward when pot fully at one extreme, *
* full speed reverse when pot fully at other extreme, *
* stopped when pot centred *
* *
* Implemented through this logic: *
* If 8-bit ADC result >= 128, *
* Select forward mode *
* PWM duty cycle = (result-128)/128 *
* Else *
* Select reverse mode *
* PWM duty cycle = (127-result)/128 *
* *
*************************************************************************
* *
* Pin assignments: *
* P1A = active-low PWM output *
* P1B = active-high PWM output *
* P1C = active-low PWM output *
* P1D = active-high PWM output *
* AN0 = analog input (e.g. pot or LDR) *
* *
************************************************************************/
#include <xc.h>
#include <stdint.h>
// configure ports
TRISC = 0b000011; // configure PORTC as all inputs
// except RC2-5 (P1A-D outputs)
ANSEL = 1<<0; // make AN0 (only) analog
// configure ADC
// Setup PWM
// configure Timer2
T2CONbits.T2CKPS1 = 1; // prescale = 16
T2CONbits.TMR2ON = 1; // enable timer
// -> TMR2 increments every 16 us
PR2 = 255; // period = 256 x 16 us = 4096 us
// -> PWM frequency = 244 Hz
// configure ECCP module
CCP1CONbits.P1M0 = 1; // select full-bridge output mode
// -> PIA, P1B, P1C, P1D are PWM outputs
CCP1CONbits.DCB = 0b00; // LSBs of PWM duty cycle = 00
CCP1CONbits.CCP1M = 0b1110; // select PWM mode: P1A, P1C active-low
// P1B, P1D active-high
// -> full-bridge output mode, PWM outputs:
// P1A active-low
// P1B active-high
// P1C active-low
// P1D active-high
Conclusion
This has been another very long lesson, especially for one that explores only a single feature of the ECCP
module: pulse-width modulation (PWM).
We’ve seen that PWM can be used to generate an output that, when averaged (whether by a simple RC filter,
the characteristics of a load such as a motor, or even the persistence of human vision), behaves as though it is
a finely-variable “analog” output. We also saw that such an output can be used to control the power
delivered to a load – such as the brightness of an LED or the speed of a motor.
And we saw that the ECCP module’s half-bridge and full-bridge PWM output modes are ideally suited,
when driving appropriate circuitry, to controlling both the speed and direction of brushless DC motors.
We also saw, especially in the last example, that using a C compiler such as XC8, instead of programming in
assembly language, makes it very straightforward to perform the arithmetic sometimes necessary to calculate
duty cycle values.
That brings us nearly to the end of the mid-range PIC architecture and assembly language lessons. We have
covered almost every significant feature of the PIC16F684 – except one: EEPROM data memory.
In our final lesson we’ll see how the 16F684’s EEPROM memory can be used to preserve data when the PIC
is powered off.
2 10 kΩ 5% 0.25W resistors