Playing with PICs : 4. Dimming LEDs with PWM

Getting Started with PICs

Getting Started with PICs

At the end of the last post I had managed to cobble something together that replicates the demo code that the PIC on the PICkit 2 demo board comes with complete with chasing LEDs, direction changing and speed control. Having done that it occurred to me that one thing I’d really like to try out is to see if I could get the LEDs to dim using the PIC’s onboard PWM control capabilities. I had a quick check of the data sheet and luckily enough, three of the four PWM outputs happen to be multiplexed with port D GPIO which means the demo board LEDs can be driven from them. Coincidence? Dunno but it made me happy anyway!

Okay so let’s back up a here and get a little perspective on this whole thing by tackling two fairly obvious questions that might pop up if you’re fairly new to this stuff. Firstly, what’s PWM and secondly why do you need it to dim LEDs?

Starting with the second question first, LED brightness can be controlled using variable voltages much like a normal tungsten bulb but in digital circuits signals are either on or off so in order to dim a LED in a digital circuit we need to find another way. This is where PWM (Pulse Width Modulation) come a long and gives us a big wet, sloppy kiss on the lips.

The way this works is basically to say that if you rapidly switch something on and off, say for example an electric motor, it’s a bit like running it at a lower voltage and how fast the motor will go depends on the amount of time it is switched on when compared to the amount of time it’s switched off. The terminology can be seen in the diagram below which comes from the PIC data sheet.

PWM Terminology

PWM Terminology

The total length of time for an on / off cycle is called the ‘period’ and this can also be expressed as a frequency which is 1/period. The amount of time that the circuit is switched on for during the period is called the ‘duty cycle’ and this is where the work gets done, motors get turned and LEDs light up. The stuff on there about timer 2 should become clearer later on.

So how is all this implemented in a PIC16F887 then? Well the PWM functionality comes as part of the ‘Capture Compare PWM’ (CCP) modules of which there are two in this particular PIC. These are cunningly named CCP1 and CCP2 and the one I chose to use was CCP1 because it’s the one that connects to the LEDs.

Configuring the PWM peripheral consists of setting the period, configuring how the duty cycle will be implemented (e.g. active high, active low and on which pins), setting the duty cycle and configuring the timer that’ll keep the whole thing going.

Easy peasy right? Err sort of!

Okay so the next thing to do was to fire up MPLAB and create a new project. After creating the project I grabbed the code from the previous PIC post as a starting point and deleted the button code. The ADC code I kept because I wanted to use it to control the LED dimming. I also deleted out the code that did the chasing so I ended up with an empty while loop. While I was in there I refactored the button initialisation code into a new function to get it out of the way. Basically all that was left in my main function was the oscillator configuration, a call to initialise the variable resistor ADC and an empty while loop.

Once I did that I used a little trick that I use a lot when I have a long and involved procedure or algorithm I need to work through and that is to add in all the comments first and add the code afterwards. The reason I sometimes do this is twofold. Firstly it means I can sketch out everything that needs to happen in comments before I get bogged down in how to actually get it to happen in that language on that platform which means I don’t forget anything. Secondly it allows me to tackle the code in any order I want to so i can start out by filling in the easy stuff and work through the complex stuff later on.

The data sheet helps us out a lot if we take that approach because it documents the exact initialisation procedure in chapter 11.5.7 Setup for PWM Operation. Hurrah! I copied the text out of the data sheet, pasted it into the code editor and converted each step into a comment. Now I just needed to fill in the blanks.

The first step is to temporarily set the pins we’re outputting to as inputs. The PWM output can go to four pins called P1A, P1B, P1C and P1D which are multiplexed with RC2, RD5, RD6 and RD7 respectively. Incidentally, the reason I elected to use CCP1 is because I needed to get output to the pins with LEDs attached and that can be done by using a trick called ‘Pulse Steering Mode’. Anyhow, as I had pins on both port B and port C and I’m not using them for anything else, I simply set them all to input.

    // Disable the PWM pin (CCPx) output drivers as an input by setting
    // the associated TRIS bit.
    TRISC = 0xff;  // blat the lot, we're not using them
    TRISD = 0xff;  // for anything else

The next job is to set the period. This is a bit of an awkward process because the period has dependencies not only on the period register (PR2) but also on the oscillator frequency and any pre-scaling on timer 2 that may or may not be set up. There is a full discussion including equations for calculating everything you need in the data sheet in section 11.5.1, 11.5.2 and 11.5.3. There are also a few examples in table 11-4 which I used as a starting point for subsequent tweakery.

    // Set the PWM period by loading the PR2 register.
    PR2 = 0x40; // this combined with a TMR2 prescale of 16 gives us
                // a PWM frequency of just under 2kHz

The value 0x40 is decimal 64 which is a quarter of 255, the maximum value for a single byte such as the one returned by my variable resistor reading function. If you check out equation 11-3 you’ll see that to get a fully on signal you need to supply a value for the duty cycle that is four times the value in the PR2 register. I ran through the calculations to see what my period frequency would end up as and it came out at just under 2KHz which seemed pretty good to me.

The next thing to do is to configure the CCP module so that it knows what kind of output you’re after. As I wanted to use pulse steering mode to direct the same output to RD5-7 I needed to use single output mode and that’s configured with the P1M bits in the CCP1CON register. I also set the extended CCP mode bits to tell that I wanted all output to be active high and finally set the pulse steering mode register PSTRCON to send output to the three LEDs.

    // Configure the CCP module for the PWM mode by loading the CCPxCON
    // register with the appropriate values.
    CCP1CONbits.P1M = 0b00;     // Single output mode
    CCP1CONbits.CCP1M = 0b1100; // ECCP Mode PWM P1A-D active high
    PSTRCON = 0b00011110;       // set up pulse steering mode on port D pins

Next I loaded up the duty cycle which is a 10bit value so it’s stored across a couple of registers. The least significant bits are held in the DC1B bits of the CCP1CON register if you’re using CCP1 and the most significant bits are written to CCPR1L.

    // Set the PWM duty cycle by loading the CCPRxL register and DCxB
    // bits of the CCPxCON register.
    CCP1CONbits.DC1B = 0x80 & 0b11;
    CCPR1L = 0x80 >> 2; // 0x80 is approx 50% duty cycle... I think!

In the code above I took an initial duty cycle value 0x80, masked off the two LSBs and wrote them to DC1B. I then shifted the same initial duty cycle value, shifted right by two places (they’re written to the other register) and wrote the result to the CCPR1L register.

The next job is to configure timer 2 which is going to drive the whole thing. The timer speed depends on the oscillator frequency and you can vary it by using pre-scalers and post-scalers. As mentioned above I used a pre-scaler value of 16 which is configured by setting the T2CKPS bits in the T2CON register to 0b10.

    // Configure and start Timer2:
    //  • Clear the TMR2IF interrupt flag bit of the PIR1 register.
    PIR1bits.TMR2IF = 0;

    //  • Set the Timer2 prescale value by loading the T2CKPS bits of the
    //    T2CON register.
    T2CONbits.T2CKPS = 0b10; // 16

    //  • Enable Timer2 by setting the TMR2ON bit of the T2CON register.
    T2CONbits.TMR2ON = 1;

Once all that is done the timer is switched on but we need to wait until a new PWM cycle has started before carrying on.

    // Enable PWM output after a new PWM cycle has started:
    //  • Wait until Timer2 overflows(TMR2IF bit of the PIR1 register is set).
    while(PIR1bits.TMR2IF == 0){}

Finally we let the output flow out of the pins.

    //  • Enable the CCPxp in output driver by clearing the associated
    //    TRIS bit.
    TRISD = 0;

One last thing I did was to set the LED adjacent to the dimmed ones to on so that I could compare brightness levels and get some idea of how well it was working.

    // switch on one of the adjacent LEDs for brightness comparison
    PORTD = 0b00010000;

Then of course you have your empty, infinite while loop.

If you run the code at this point the LEDs on RD5-7 display but are clearly dimmer than the one that is fully on which is great, it means the thing actually works. The next step though is to get that variable resistor into the mix so we can use that to set the amount of dimming.

Well we already have the code to get a byte value from the variable resistor and we already have the code to set the duty cycle. All we have to do is combine it together.

    char duty = 0;  // the duty cycle value derived from the VR value

    // loop forever
    while(1)
    {
        // read an 8-bit value from the VR
        duty = readVR();

        // set the new duty cycle
        CCP1CONbits.DC1B = duty & 0b11; // LSB
        CCPR1L = duty >> 2;             // MSB
    }

I then built the code, connected my PICKit 2 and demo board and gave it a spin. Sure enough it all seemed to work just fine. Turning the variable resistor allows a full range of dimming control from completely off to completely on.

#include <pic.h>
#include <pic16f887.h>

// PIC16F887 Configuration Bit Settings

// CONFIG1
#pragma config FOSC = INTRC_NOCLKOUT// Oscillator Selection bits (INTOSCIO oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled and can be enabled by SWDTEN bit of the WDTCON register)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = ON       // RE3/MCLR pin function select bit (RE3/MCLR pin function is MCLR)
#pragma config CP = OFF         // Code Protection bit (Program memory code protection is disabled)
#pragma config CPD = OFF        // Data Code Protection bit (Data memory code protection is disabled)
#pragma config BOREN = ON       // Brown Out Reset Selection bits (BOR enabled)
#pragma config IESO = ON        // Internal External Switchover bit (Internal/External Switchover mode is enabled)
#pragma config FCMEN = ON       // Fail-Safe Clock Monitor Enabled bit (Fail-Safe Clock Monitor is enabled)
#pragma config LVP = OFF        // Low Voltage Programming Enable bit (RB3 pin has digital I/O, HV on MCLR must be used for programming)

// CONFIG2
#pragma config BOR4V = BOR40V   // Brown-out Reset Selection bit (Brown-out Reset set to 4.0V)
#pragma config WRT = OFF        // Flash Program Memory Self Write Enable bits (Write protection off)

#define _XTAL_FREQ 8000000

void initVR()
{
    // configure RA0 as analog AN0
    ADCON1bits.ADFM = 0; // left justified result
    ADCON1bits.VCFG0 = 0; // use VDD as reference voltage
    ADCON1bits.VCFG1 = 0; // use VCC as reference gnd

    TRISA = 1;
    ANSELbits.ANS0 = 1;

    ADCON0bits.ADCS = 0b10; // clock / 32
    ADCON0bits.CHS = 0;     // AN0 selected
    ADCON0bits.ADON = 1;    // turn on the ad converter
}

char readVR()
{
    ADCON0bits.GO = 1;
    while(ADCON0bits.GO == 1){}
    return ADRESH;
}

void main()
{
    // set up the oscillator to run at 8MHz
    OSCCONbits.IRFC = 0b111;

    // init the ADC to read the VR
    initVR();

    // Disable the PWM pin (CCPx) output drivers as an input by setting
    // the associated TRIS bit.
    TRISC = 0xff;  // blat the lot, we're not using them
    TRISD = 0xff;  // for anything else

    // Set the PWM period by loading the PR2 register.
    PR2 = 0x40; // this combined with a TMR2 prescale of 16 gives us
                // a PWM frequency of just under 2kHz

    // Configure the CCP module for the PWM mode by loading the CCPxCON
    // register with the appropriate values.
    CCP1CONbits.P1M = 0b00;     // Single output mode
    CCP1CONbits.CCP1M = 0b1100; // ECCP Mode PWM P1A-D active high
    PSTRCON = 0b00011110;       // set up pulse steering mode on port D pins

    // Set the PWM duty cycle by loading the CCPRxL register and DCxB
    // bits of the CCPxCON register.
    CCP1CONbits.DC1B = 0x80 & 0b11;
    CCPR1L = 0x80 >> 2; // 0x80 is approx 50% duty cycle... I think!

    // Configure and start Timer2:
    //  • Clear the TMR2IF interrupt flag bit of the PIR1 register.
    PIR1bits.TMR2IF = 0;

    //  • Set the Timer2 prescale value by loading the T2CKPS bits of the
    //    T2CON register.
    T2CONbits.T2CKPS = 0b10; // 16

    //  • Enable Timer2 by setting the TMR2ON bit of the T2CON register.
    T2CONbits.TMR2ON = 1;

    // Enable PWM output after a new PWM cycle has started:
    //  • Wait until Timer2 overflows(TMR2IF bit of the PIR1 register is set).
    while(PIR1bits.TMR2IF == 0){}

    //  • Enable the CCPxp in output driver by clearing the associated
    //    TRIS bit.
    TRISD = 0;

    // switch on one of the adjacent LEDs for brightness comparison
    PORTD = 0b00010000;

    char duty = 0;  // the duty cycle value derived from the VR value

    // loop forever
    while(1)
    {
        // read an 8-bit value from the VR
        duty = readVR();

        // set the new duty cycle
        CCP1CONbits.DC1B = duty & 0b11; // LSB
        CCPR1L = duty >> 2;             // MSB
    }
}

The full, final code is above if you fancy picking through it (no pun intended). I hope there’s something vaguely useful in there. As always questions, comments and suggestions are more than welcome otherwise feel free just to say hi.

Advertisements
2 comments
  1. Heather said:

    I was wondering how to implement this code in Real PIc Simulator? Can you help me with that?

    • Hi Heather, thanks for stopping by the blog.

      I’ve not used Real Pic Simulator but I did have a quick look at the documentation and it does at least appear to support the right PIC.

      If I get the chance I’ll download it and have a crack. I have got quite a lot in at the mo though so it might not be for a while!

      Cheers
      Jon

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: