Playing with PICs : 5. Smooth LED Chasing with PWM

PWM LEDS Breadboard

PWM LEDS Breadboard

In last few posts I looked at ways to chase a few LEDs and dim them using the PWM functionality of the PIC16F887 which is mounted on the PICkit2 demo board. I decided that I wanted to go a bit further with this though and to combine the two things together and see if I could get some smoother chase action going by using PWM to pulse the LEDs on and off rather than switch them on and off.

I had to go ditch the demo board to do this though as the LEDs on the demo board aren’t really connected to suitable inputs, unless of course you implement PWM in code using GPIO which is possible. I happen to have a PIC18F26K22 knocking about in a 28 pin DIP package, some breadboard and a bunch of other bits and bobs so I decided to build my own demo board for this project.

The first thing I did was to build a basic circuit that would allow me to power, program and run the PIC. I then checked the PIC data sheet to see where the CCP (capture, compare, PWM) pins are and added in a few LEDs along with current limiting resistors. Finally I dug out a 10K variable resistor and wired that up to one of the analog inputs of the PIC. The circuit diagram is shown below.

PWM LEDs Schematic

PWM LEDs Schematic

As the PIC18F26K22 is not supported by the PICkit 2 I also had to get hold of a PICkit 3 programmer. It’s time I just bought the programmer on its own rather than buy one with a demo board. After all, I’m creating my own board now right?

Next I had to figure out exactly what it was that I wanted the thing to do. I decided that I’d keep the chasing LEDs but use PWM to dim the brightness using a set of brightness values derived from a sine wave. Each of the three LEDs would start from a different point in the sine wave so that the first is at 0 degrees, the second at 45 degrees and the last one at 90 degrees. I decided to implement this using a look up table of byte values. The variable resistor, I decided, would be used to control speed.

Having moved to a new PIC, I had to generate a new set of config values. Once again I used the tool built into the MPLAB IDE to do this. I set it to use the internal oscillator and I switched LVP off as requested by the PICKit 3 programmer. Generating the code yielded the following which I added into the main.c code file.

// PIC18F26K22 Configuration Bit Settings

// CONFIG1H
#pragma config FOSC = INTIO67   // Oscillator Selection bits (Internal oscillator block)
#pragma config PLLCFG = OFF     // 4X PLL Enable (Oscillator used directly)
#pragma config PRICLKEN = ON    // Primary clock enable bit (Primary clock enabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable bit (Fail-Safe Clock Monitor disabled)
#pragma config IESO = OFF       // Internal/External Oscillator Switchover bit (Oscillator Switchover mode disabled)

// CONFIG2L
#pragma config PWRTEN = OFF     // Power-up Timer Enable bit (Power up timer disabled)
#pragma config BOREN = SBORDIS  // Brown-out Reset Enable bits (Brown-out Reset enabled in hardware only (SBOREN is disabled))
#pragma config BORV = 190       // Brown Out Reset Voltage bits (VBOR set to 1.90 V nominal)

// CONFIG2H
#pragma config WDTEN = ON       // Watchdog Timer Enable bits (WDT is always enabled. SWDTEN bit has no effect)
#pragma config WDTPS = 32768    // Watchdog Timer Postscale Select bits (1:32768)

// CONFIG3H
#pragma config CCP2MX = PORTC1  // CCP2 MUX bit (CCP2 input/output is multiplexed with RC1)
#pragma config PBADEN = ON      // PORTB A/D Enable bit (PORTB pins are configured as analog input channels on Reset)
#pragma config CCP3MX = PORTB5  // P3A/CCP3 Mux bit (P3A/CCP3 input/output is multiplexed with RB5)
#pragma config HFOFST = ON      // HFINTOSC Fast Start-up (HFINTOSC output and ready status are not delayed by the oscillator stable status)
#pragma config T3CMX = PORTC0   // Timer3 Clock input mux bit (T3CKI is on RC0)
#pragma config P2BMX = PORTB5   // ECCP2 B output mux bit (P2B is on RB5)
#pragma config MCLRE = EXTMCLR  // MCLR Pin Enable bit (MCLR pin enabled, RE3 input pin disabled)

// CONFIG4L
#pragma config STVREN = ON      // Stack Full/Underflow Reset Enable bit (Stack full/underflow will cause Reset)
#pragma config LVP = OFF        // Single-Supply ICSP Enable bit (Single-Supply ICSP disabled)
#pragma config XINST = OFF      // Extended Instruction Set Enable bit (Instruction set extension and Indexed Addressing mode disabled (Legacy mode))

// CONFIG5L
#pragma config CP0 = OFF        // Code Protection Block 0 (Block 0 (000800-003FFFh) not code-protected)
#pragma config CP1 = OFF        // Code Protection Block 1 (Block 1 (004000-007FFFh) not code-protected)
#pragma config CP2 = OFF        // Code Protection Block 2 (Block 2 (008000-00BFFFh) not code-protected)
#pragma config CP3 = OFF        // Code Protection Block 3 (Block 3 (00C000-00FFFFh) not code-protected)

// CONFIG5H
#pragma config CPB = OFF        // Boot Block Code Protection bit (Boot block (000000-0007FFh) not code-protected)
#pragma config CPD = OFF        // Data EEPROM Code Protection bit (Data EEPROM not code-protected)

// CONFIG6L
#pragma config WRT0 = OFF       // Write Protection Block 0 (Block 0 (000800-003FFFh) not write-protected)
#pragma config WRT1 = OFF       // Write Protection Block 1 (Block 1 (004000-007FFFh) not write-protected)
#pragma config WRT2 = OFF       // Write Protection Block 2 (Block 2 (008000-00BFFFh) not write-protected)
#pragma config WRT3 = OFF       // Write Protection Block 3 (Block 3 (00C000-00FFFFh) not write-protected)

// CONFIG6H
#pragma config WRTC = OFF       // Configuration Register Write Protection bit (Configuration registers (300000-3000FFh) not write-protected)
#pragma config WRTB = OFF       // Boot Block Write Protection bit (Boot Block (000000-0007FFh) not write-protected)
#pragma config WRTD = OFF       // Data EEPROM Write Protection bit (Data EEPROM not write-protected)

// CONFIG7L
#pragma config EBTR0 = OFF      // Table Read Protection Block 0 (Block 0 (000800-003FFFh) not protected from table reads executed in other blocks)
#pragma config EBTR1 = OFF      // Table Read Protection Block 1 (Block 1 (004000-007FFFh) not protected from table reads executed in other blocks)
#pragma config EBTR2 = OFF      // Table Read Protection Block 2 (Block 2 (008000-00BFFFh) not protected from table reads executed in other blocks)
#pragma config EBTR3 = OFF      // Table Read Protection Block 3 (Block 3 (00C000-00FFFFh) not protected from table reads executed in other blocks)

// CONFIG7H
#pragma config EBTRB = OFF      // Boot Block Table Read Protection bit (Boot Block (000000-0007FFh) not protected from table reads executed in other blocks)

I also set the clock frequency by setting the IRCF bits in the OSCCON register. This is the first statement in my main() function.

/*
 * The main function.
 */
void main()
{
    // set up the internal oscillator
    OSCCONbits.IRCF = 0b111;  // 16MHz

    while(1)
    {
    }
}

Knowing I was going to need a delay function later on, I defined _XTAL_FREQ at 16MHz and created a millisecond delay function based on the __delay_ms macro.

#define _XTAL_FREQ 16000000

/*
 * Implements a delay of a number of milliseconds.
 */
void delay(int ms)
{
    while(ms-- > 0)
    {
        __delay_ms(1);
    }
}

The approach I took to generating the sine table of brightness values was to calculate the values using a spreadsheet. I arbitrarily decided on 100 values and created a list of angles in 3.6 degree steps. In the column next to it I generate raw sine values for each angle which I multiplied by 128 and added 127. This gave me a range of values between 0 and 255 and I transferred those into an array of values in the program.

/*
 * Defines a list of values that are derived from a sine wave that
 * can be used to smoothly pulse the PWM duty cycle and the LEDs.
 */
const char brightness_values[] = {
    128, 136, 144, 152, 160, 168, 175, 182, 190, 197,
    203, 210, 216, 221, 227, 232, 236, 240, 244, 247,
    250, 252, 254, 255, 255, 255, 255, 255, 254, 252,
    250, 247, 244, 240, 236, 232, 227, 221, 216, 210,
    203, 197, 190, 182, 175, 168, 160, 152, 144, 136,
    128, 120, 112, 104,  96,  88,  81,  74,  66,  59,
     53,  46,  40,  35,  29,  24,  20,  16,  12,   9,
      6,   4,   2,   1,   0,   0,   0,   1,   2,   4,
      6,   9,  12,  16,  20,  24,  29,  35,  40,  46,
     53,  59,  66,  74,  81,  88,  96, 104, 112, 120
};

#define BRIGHTNESS_VALUES_COUNT 100

These values are going to act as my duty cycles to control the LED brightness.

Once I had done that I added in a method to initialise the PIC PWM on the three CCP modules I wanted to use to drive the LEDs. The configuration on the PIC18 is unsurprisingly slightly different to that of the PIC16 I was using before so, having used the previous trick of turning the steps in the data sheet into comments, I went back to the data sheet and worked through each step to make sure everything was properly configured.

I wanted to make sure my resolution was still 8 bits to match my brightness data so having selected a TMRx divider of 8 and a PRx value of 64, I added a new sheet to my ‘useful calculations’ spreadsheet to do the math and check it.

PWM Calculator

PWM Calculator

It turns out that the resolution is 8 bits with those settings and I have a period frequency of 7.7KHz which is great. The final code for the initPWM() method is below.

/*
 * Initialises PWM on the three PWM generators.
 */
void initPWM()
{
    // 1. Disable the CCPx pin output driver by setting the associated TRIS bit.
    TRISC = 0b00000110; // RC1 and RC2
    TRISBbits.RB5 = 1;

    // 2. Select the 8-bit TimerX resource, (Timer2, Timer4 or Timer6) to be
    //    used for PWM generation by setting the CxTSEL bits in the
    //    CCPTMRSx register.
    CCPTMRS0bits.C1TSEL = 0; // user timer 2

    // 3. Load the PRx register for the selected TimerX with the PWM period value.
    PR2 = 0x40; // decimal 64 period

    // 4. Configure the CCP module for the PWM mode by loading the CCPxCON
    //    register with the appropriate values.
    CCP1CONbits.CCP1M = 0b1100;
    CCP2CONbits.CCP2M = 0b1100;
    CCP3CONbits.CCP3M = 0b1100;

    // 5. Load the CCPRxL register and the DCxB bits of the CCPxCON register,
    //    with the PWM duty cycle value.
    CCPR1L = 128 >> 2;
    CCPR2L = 128 >> 2;
    CCPR3L = 128 >> 2;

    CCP1CONbits.DC1B = 128 & 0b00000011; // 128 is 50% duty cycle
    CCP2CONbits.DC2B = 128 & 0b00000011; // 128 is 50% duty cycle
    CCP3CONbits.DC3B = 128 & 0b00000011; // 128 is 50% duty cycle

    // 6. Configure and start the 8-bit TimerX resource:
    //    - Clear the TMRxIF interrupt flag bit of the
    //      PIR2 or PIR4 register. See Note 1 below.
    PIR1bits.TMR2IF = 0;

    //    - Configure the TxCKPS bits of the TxCON
    //      register with the Timer prescale value.
    T2CONbits.T2CKPS = 0b11; // prescale 8

    //    - Enable the Timer by setting the TMRxON
    //      bit of the TxCON register.
    T2CONbits.TMR2ON = 1;

    // 7. Enable PWM output pin:
    //    - Wait until the Timer overflows and the TMRxIF bit of the PIR2 or PIR4
    //      register is set. See Note 1 below.
    while(PIR1bits.TMR2IF == 0){}

    //    - Enable the CCPx pin output driver by clearing the associated TRIS bit.
    TRISC = 0;
    TRISBbits.RB5 = 0;
}

This method can then be called from the main() function prior to the while loop to initialise PWM across the three outputs. I also added a separate method to allow the duty cycles for each output to be set individually.

/*
 * Sets the PWM values for each of the three PWM controllers.
 */
void setPWM(char value1, char value2, char value3)
{
    CCPR1L = value1 >> 2;
    CCP1CONbits.DC1B = value1 & 0b00000011;

    CCPR2L = value2 >> 2;
    CCP2CONbits.DC2B = value2 & 0b00000011;

    CCPR3L = value3 >> 2;
    CCP3CONbits.DC3B = value3 & 0b00000011;
}

All this method does is replace the PWM duty cycle value in each of the CCPRxL and CCPxCON registers. The bit shifting and masking ensures that the LSBs end up in the DCxB bits of the CCPxCON register while the MSBs are written to the CCPRxL registers.

The next task was to add in the code to configure the ADC to read the data from the variable resistor. Again, I took the initialisation procedure from the data sheet and turned it into a set of comments that I could then take my time filling in. There are a couple of things that needed some thought. Firstly the ADC clock needed to be divided by 16 to bring it into the range of speeds that the ADC can cope with. This is all detailed in the data sheet in table 17-1 so there’s no mysterious voodoo in any of that. The other thing that needs to be calculated is the acquisition delay length. The ADC uses an sample and hold circuit based on an RC network and the acquisition time is the amount of time it’ll take to charge the capacitor before performing the conversion. Obviously you need to give it enough time to do that job and the way to do that is to set a period in TAD units in the ACQT bits of the ADCON2 register. Equation 17-1 is provided in the data sheet to help calculate the minimum TAD required. Again, I popped it into my ever growing spreadsheet of useful calculations and selected 12 as my value. This leaves plenty of time for signal capture in this application.

/*
 * Initialises the variable resistor ADC configuration.
 */
void initVR()
{
    // 1. Configure Port:
    //  - Disable pin output driver(See TRIS register)
    TRISAbits.RA0 = 1;

    //  - Configure pin as analog
    ANSELAbits.ANSA0 = 1;

    // 2. Configure the ADC module:
    //  - Select ADC conversion clock
    ADCON2bits.ADCS = 0b101; // Fosc/16

    //  - Configure voltage reference
    ADCON1bits.PVCFG = 0; // +ref is VDD
    ADCON1bits.NVCFG = 0; // -ref is VSS

    //  - Select ADC input channel
    ADCON0bits.CHS = 0; // connect AN0 to the ADC

    //  - Select result format
    ADCON2bits.ADFM = 0; // left justified

    //  - Select acquisition delay
    ADCON2bits.ACQT = 0b101; // 12 TAD

    //  - Turn on ADC module
    ADCON0bits.ADON = 1;
}

As with the previous programs I set the result alignment to be left aligned so that I could return a single byte value that worked across the whole of the potentiometer range. There’s plenty of resolution there for something this simple. Reference voltages again are VDD and VSS (GND) as that matches the potentiometer circuit and the analog input is configured to be taken from AN0 which is multiplexed with RA0.

I also added in a function to read and return the potentiometer value. This simply starts a conversion and waits for it to complete by set the GO bit in ADCON0 and waiting for it to clear. We could use interrupts for this which would be a neater solution but this will do for now. Finally the result is returned.

/*
 * Reads the value of the variable resistor into a single byte.
 */
char readVR()
{
    // start a conversion
    ADCON0bits.GO = 1;

    // wait for it to complete
    while(ADCON0bits.GO == 1){}

    // return the top 8 bits of the value
    return ADRESH;
}

Almost there now, the last thing to do was to update the main function to call of these methods and light up the LEDs.

/*
 * The main function.
 */
void main()
{
    // set up the internal oscillator
    OSCCONbits.IRCF = 0b111;  // 16MHz

    // init the PWM
    initPWM();

    // init the VR
    initVR();

    // brightness counter
    char val1 = 0;
    char val2 = 0;
    char val3 = 0;

    while(1)
    {
        // create a set of values at 45 degrees to each other in the table
        val1 = (val1 + 1) % 100;
        val2 = (val1 + 33) % 100;
        val3 = (val1 + 66) % 100;

        // set the PWM values
        setPWM(brightness_values[val1],
               brightness_values[val2],
               brightness_values[val3]);

        // create a delay proportional to the VR value
        delay((readVR() / 10) + 2);
    }
}

It’s all very straightforward. I created three values to hold individual indexes into the brightness values for each LED. They start at different points in the cycle and increment each time through the loop. The modulus operator resets the index back to the start of the look up table if they wrap around. Meanwhile the delay is set by the potentiometer reading from the AD conversion so that the speed of the chasing can be set. The delay value is manipulated a bit because I wanted to limit it from 2ms to roughly 30ms. Those values were determined through experimentation to see what felt about right.

The complete main.c code is included below.

#include <htc.h>
#include <pic18f26k22.h>


// PIC18F26K22 Configuration Bit Settings

// CONFIG1H
#pragma config FOSC = INTIO67   // Oscillator Selection bits (Internal oscillator block)
#pragma config PLLCFG = OFF     // 4X PLL Enable (Oscillator used directly)
#pragma config PRICLKEN = ON    // Primary clock enable bit (Primary clock enabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable bit (Fail-Safe Clock Monitor disabled)
#pragma config IESO = OFF       // Internal/External Oscillator Switchover bit (Oscillator Switchover mode disabled)

// CONFIG2L
#pragma config PWRTEN = OFF     // Power-up Timer Enable bit (Power up timer disabled)
#pragma config BOREN = SBORDIS  // Brown-out Reset Enable bits (Brown-out Reset enabled in hardware only (SBOREN is disabled))
#pragma config BORV = 190       // Brown Out Reset Voltage bits (VBOR set to 1.90 V nominal)

// CONFIG2H
#pragma config WDTEN = ON       // Watchdog Timer Enable bits (WDT is always enabled. SWDTEN bit has no effect)
#pragma config WDTPS = 32768    // Watchdog Timer Postscale Select bits (1:32768)

// CONFIG3H
#pragma config CCP2MX = PORTC1  // CCP2 MUX bit (CCP2 input/output is multiplexed with RC1)
#pragma config PBADEN = ON      // PORTB A/D Enable bit (PORTB pins are configured as analog input channels on Reset)
#pragma config CCP3MX = PORTB5  // P3A/CCP3 Mux bit (P3A/CCP3 input/output is multiplexed with RB5)
#pragma config HFOFST = ON      // HFINTOSC Fast Start-up (HFINTOSC output and ready status are not delayed by the oscillator stable status)
#pragma config T3CMX = PORTC0   // Timer3 Clock input mux bit (T3CKI is on RC0)
#pragma config P2BMX = PORTB5   // ECCP2 B output mux bit (P2B is on RB5)
#pragma config MCLRE = EXTMCLR  // MCLR Pin Enable bit (MCLR pin enabled, RE3 input pin disabled)

// CONFIG4L
#pragma config STVREN = ON      // Stack Full/Underflow Reset Enable bit (Stack full/underflow will cause Reset)
#pragma config LVP = OFF        // Single-Supply ICSP Enable bit (Single-Supply ICSP disabled)
#pragma config XINST = OFF      // Extended Instruction Set Enable bit (Instruction set extension and Indexed Addressing mode disabled (Legacy mode))

// CONFIG5L
#pragma config CP0 = OFF        // Code Protection Block 0 (Block 0 (000800-003FFFh) not code-protected)
#pragma config CP1 = OFF        // Code Protection Block 1 (Block 1 (004000-007FFFh) not code-protected)
#pragma config CP2 = OFF        // Code Protection Block 2 (Block 2 (008000-00BFFFh) not code-protected)
#pragma config CP3 = OFF        // Code Protection Block 3 (Block 3 (00C000-00FFFFh) not code-protected)

// CONFIG5H
#pragma config CPB = OFF        // Boot Block Code Protection bit (Boot block (000000-0007FFh) not code-protected)
#pragma config CPD = OFF        // Data EEPROM Code Protection bit (Data EEPROM not code-protected)

// CONFIG6L
#pragma config WRT0 = OFF       // Write Protection Block 0 (Block 0 (000800-003FFFh) not write-protected)
#pragma config WRT1 = OFF       // Write Protection Block 1 (Block 1 (004000-007FFFh) not write-protected)
#pragma config WRT2 = OFF       // Write Protection Block 2 (Block 2 (008000-00BFFFh) not write-protected)
#pragma config WRT3 = OFF       // Write Protection Block 3 (Block 3 (00C000-00FFFFh) not write-protected)

// CONFIG6H
#pragma config WRTC = OFF       // Configuration Register Write Protection bit (Configuration registers (300000-3000FFh) not write-protected)
#pragma config WRTB = OFF       // Boot Block Write Protection bit (Boot Block (000000-0007FFh) not write-protected)
#pragma config WRTD = OFF       // Data EEPROM Write Protection bit (Data EEPROM not write-protected)

// CONFIG7L
#pragma config EBTR0 = OFF      // Table Read Protection Block 0 (Block 0 (000800-003FFFh) not protected from table reads executed in other blocks)
#pragma config EBTR1 = OFF      // Table Read Protection Block 1 (Block 1 (004000-007FFFh) not protected from table reads executed in other blocks)
#pragma config EBTR2 = OFF      // Table Read Protection Block 2 (Block 2 (008000-00BFFFh) not protected from table reads executed in other blocks)
#pragma config EBTR3 = OFF      // Table Read Protection Block 3 (Block 3 (00C000-00FFFFh) not protected from table reads executed in other blocks)

// CONFIG7H
#pragma config EBTRB = OFF      // Boot Block Table Read Protection bit (Boot Block (000000-0007FFh) not protected from table reads executed in other blocks)

#define _XTAL_FREQ 16000000

/*
 * Defines a list of values that are derived from a sine wave that
 * can be used to smoothly pulse the PWM duty cycle and the LEDs.
 */
const char brightness_values[] = {
    128, 136, 144, 152, 160, 168, 175, 182, 190, 197,
    203, 210, 216, 221, 227, 232, 236, 240, 244, 247,
    250, 252, 254, 255, 255, 255, 255, 255, 254, 252,
    250, 247, 244, 240, 236, 232, 227, 221, 216, 210,
    203, 197, 190, 182, 175, 168, 160, 152, 144, 136,
    128, 120, 112, 104,  96,  88,  81,  74,  66,  59,
     53,  46,  40,  35,  29,  24,  20,  16,  12,   9,
      6,   4,   2,   1,   0,   0,   0,   1,   2,   4,
      6,   9,  12,  16,  20,  24,  29,  35,  40,  46,
     53,  59,  66,  74,  81,  88,  96, 104, 112, 120
};

#define BRIGHTNESS_VALUES_COUNT 100

/*
 * Implements a delay of a number of milliseconds.
 */
void delay(int ms)
{
    while(ms-- > 0)
    {
        __delay_ms(1);
    }
}

/*
 * Initialises the variable resistor ADC configuration.
 */
void initVR()
{
    // 1. Configure Port:
    //  - Disable pin output driver(See TRIS register)
    TRISAbits.RA0 = 1;

    //  - Configure pin as analog
    ANSELAbits.ANSA0 = 1;

    // 2. Configure the ADC module:
    //  - Select ADC conversion clock
    ADCON2bits.ADCS = 0b101; // Fosc/16

    //  - Configure voltage reference
    ADCON1bits.PVCFG = 0; // +ref is VDD
    ADCON1bits.NVCFG = 0; // -ref is VSS

    //  - Select ADC input channel
    ADCON0bits.CHS = 0; // connect AN0 to the ADC

    //  - Select result format
    ADCON2bits.ADFM = 0; // left justified

    //  - Select acquisition delay
    ADCON2bits.ACQT = 0b101; // 12 TAD

    //  - Turn on ADC module
    ADCON0bits.ADON = 1;
}

/*
 * Reads the value of the variable resistor into a single byte.
 */
char readVR()
{
    // start a conversion
    ADCON0bits.GO = 1;

    // wait for it to complete
    while(ADCON0bits.GO == 1)
    {
        delay(1);
    }

    // return the top 8 bits of the value
    return ADRESH;
}

/*
 * Initialises PWM on the three PWM generators.
 */
void initPWM()
{
    // 1. Disable the CCPx pin output driver by setting the associated TRIS bit.
    TRISC = 0b00000110; // RC1 and RC2
    TRISBbits.RB5 = 1;

    // 2. Select the 8-bit TimerX resource, (Timer2, Timer4 or Timer6) to be
    //    used for PWM generation by setting the CxTSEL bits in the
    //    CCPTMRSx register.
    CCPTMRS0bits.C1TSEL = 0; // user timer 2

    // 3. Load the PRx register for the selected TimerX with the PWM period value.
    PR2 = 0x40; // decimal 64 period

    // 4. Configure the CCP module for the PWM mode by loading the CCPxCON
    //    register with the appropriate values.
    CCP1CONbits.CCP1M = 0b1100;
    CCP2CONbits.CCP2M = 0b1100;
    CCP3CONbits.CCP3M = 0b1100;

    // 5. Load the CCPRxL register and the DCxB bits of the CCPxCON register,
    //    with the PWM duty cycle value.
    CCPR1L = 128 >> 2;
    CCPR2L = 128 >> 2;
    CCPR3L = 128 >> 2;

    CCP1CONbits.DC1B = 128 & 0b00000011; // 128 is 50% duty cycle
    CCP2CONbits.DC2B = 128 & 0b00000011; // 128 is 50% duty cycle
    CCP3CONbits.DC3B = 128 & 0b00000011; // 128 is 50% duty cycle

    // 6. Configure and start the 8-bit TimerX resource:
    //    - Clear the TMRxIF interrupt flag bit of the
    //      PIR2 or PIR4 register. See Note 1 below.
    PIR1bits.TMR2IF = 0;

    //    - Configure the TxCKPS bits of the TxCON
    //      register with the Timer prescale value.
    T2CONbits.T2CKPS = 0b11; // prescale 8

    //    - Enable the Timer by setting the TMRxON
    //      bit of the TxCON register.
    T2CONbits.TMR2ON = 1;

    // 7. Enable PWM output pin:
    //    - Wait until the Timer overflows and the TMRxIF bit of the PIR2 or PIR4
    //      register is set. See Note 1 below.
    while(PIR1bits.TMR2IF == 0){}

    //    - Enable the CCPx pin output driver by clearing the associated TRIS bit.
    TRISC = 0;
    TRISBbits.RB5 = 0;
}

/*
 * Sets the PWM values for each of the three PWM controllers.
 */
void setPWM(char value1, char value2, char value3)
{
    CCPR1L = value1 >> 2;
    CCP1CONbits.DC1B = value1 & 0b00000011;

    CCPR2L = value2 >> 2;
    CCP2CONbits.DC2B = value2 & 0b00000011;

    CCPR3L = value3 >> 2;
    CCP3CONbits.DC3B = value3 & 0b00000011;
}

/*
 * The main function.
 */
void main()
{
    // set up the internal oscillator
    OSCCONbits.IRCF = 0b111;  // 16MHz

    // init the PWM
    initPWM();

    // init the VR
    initVR();

    // brightness counter
    char val1 = 0;
    char val2 = 0;
    char val3 = 0;

    while(1)
    {
        // create a set of values at 45- to each other in the table
        val1 = (val1 + 1) % 100;
        val2 = (val1 + 33) % 100;
        val3 = (val1 + 66) % 100;

        // set the PWM values
        setPWM(brightness_values[val1],
               brightness_values[val2],
               brightness_values[val3]);

        // create a delay proportional to the VR value
        delay((readVR() / 10) + 2);
    }
}

So there it is, sinusoidally pulsed, PWM implemented, variable speed LED chasing. At some point I might look at driving a string of LEDs this way. Perhaps it could be a project for later in the year. In the meantime though I’m going to continue to explore some of the other features of the PIC18F26K22 and see what fun can be had.

Once again, please feel free to contact me with any comments or suggestions. I’m always happy to hear from you!

Advertisements
4 comments
  1. jsrichards2 said:

    Hi Jon, Great post that has been a lot of help in using PWM with the PIC18F26K22. Not sure about your prescaler value though for T2CONbits.T2CKPS. The prescale options for Timer2 are on page 177 of the data sheet. T2CONbits.T2CKPS = 0b11 gives 1:16 prescale (3.85 kHz) not 1:8?

    • Hi, thanks for the feedback it’s hugely appreciated. I shall check it out forthwith 🙂

      Jon

  2. Kermit Lohry said:

    Hi Jon, I have been trying to use Timer4 for pwm on CCP2, which seems possible from the data sheet (set C2TSEL[1:0] in CCPTMRS0 to 0b01), but it still uses Timer2. Any insight into this?
    Also I’ve that you have to subtract 1 from the PRx value to get the correct freq/period since it counts from 0 then after the match resets the timer.

    • Looks like I made a couple for mistakes in the Timer config! I’ll sort them out and update the article. Thanks for pointing it out, hugely appreciated.

      Wrt to your question, I’m not really sure what the problem might be with that. If I get the chance I’ll have a play and see what happens.

      Thanks again for the feedback, it’s really appreciated.

      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: