Generating PWM with PIC Microcontroller – MPLAB XC8

PWM - Pulse Width Modulation

Generating PWM with PIC Microcontroller – MPLAB XC8

Pulse Width Modulation (PWM) is the one of the simple and most commonly used technique to produce analog voltages from digital signals. It has a wide variety of applications such as Digital to Analog Converter (DAC), DC Motor Speed Control, Sine Wave Inverters, Brightness control etc.

PWM signals are ON – OFF signals (HIGH or LOW) (hence the name Pulse) whose HIGH or ON duration is changed (hence Width Modulation) in accordance with our requirements. The fraction of time period for which the signal is ON to the total time period is termed as Duty Cycle.

PWM waves can be easily generated using CCP modules available with most of the PIC Microcontrollers. CCP stands for Capture / Compare / PWM, which means that it can be used for Capture or Compare or PWM operations. To program this module with MPLAB XC8 compiler we need to learn its working. Here for demonstration we are using PIC 16F877A microcontroller.

CCP – Capture / Compare / PWM Module

Microchip’s PIC 16F877A microcontroller has two CCP modules, named as CCP1 and CCP2. Each CCP module comprises of two 8 bit registers which can be operate as :


  • 16 bit Capture Register
  • 16 bit Compare Register
  • PWM Master / Slave Duty Cycle register

This tutorial deals only with PWM operation of CCP module. Using this we can generate PWM output having resolution up to 10 bit. Output of CCP modules are multiplexed with RC1 & RC2 of PORTC, hence TRIS<1> and TRIS<2> must be cleared to make these pins output.

Working

The simplified block diagram of PWM mode of CCP module is shown below.

PWM Simplified Block Diagram - PIC 16F877A

PWM Simplified Block Diagram – PIC 16F877A

Timer 2 module provides the time base for the PWM operation of both CCP modules. The value of the TMR2 register increases from zero to maximum value as per the timer 2 input clock. The input clock is determined by the microcontroller clock frequency (Fosc) and timer 2 prescaler value.

Timer 2 Block Diagram - PIC 16F877A

Timer 2 Block Diagram – PIC 16F877A

  • Prescaler Input Clock : Fosc / 4
  • Timer 2 Input Clock : (Fosc / 4) / Prescaler Value = Fosc / (4*Prescaler)

Time period of the generated PWM waves are determined by the value of PR2 Register. The comparator compares the values of PR2 and TMR2 registers. When these values become equal, the output pulse from the comparator sets the PWM output to HIGH. It also resets the Timer 2 value to zero.



The PWM duty cycle is determined by the value in the CCPR1L and CCP1CON<5:4> registers, which can be written any time. This value is stored to CCPR1H and a 2 bit internal latch when there is a match between PR2 and Timer 2. This avoids the possibility of glitches in the PWM output due to changing of duty cycle.

PWM Output

PWM Output

The PWM output become LOW when there is a match between Timer 2 value and Duty Cycle (CCPR1L and CCP1CON<5:4>).

Note 1 : The value of Duty Cycle should be less than Time Period (PR2) for the proper generation of PWM signals.

Note 2 : It is not possible to use different PWM frequencies for both CCP modules, because they use Timer 2 for their operation.



PWM Resolution

PWM Resolution of CCP modules can be calculated using the following equation.

PWM Resolution

Configuration

  1. Set the PWM Period by writing to PR2 register. The relation between PR2 value and time period is shown below.PWM Period - PIC Microcontroller
  2. Then set the PWM Duty Cycle by writing to CCPR1L and CCP1CON<5:4> registers. The relation between register value and duty cycle is shown below.PWM Duty Cycle
  3. Configure the CCP pins as Output by writing to TRIS<2> and TRIS<1> bits.
  4. Set the Timer 2 (TMR2) prescale value and enable it by writing to T2CON register.
  5. Finally, configure the CCP module for PWM operation by writing to CCP control registers.

Circuit Diagram

Generating PWM with PIC Microcontroller - Circuit Diagram

Generating PWM with PIC Microcontroller – Circuit Diagram

This circuit can be used to demonstrate the working of PWM. An LED is connected to the output of each CCP module through a series resistor to limit the current. Push Button Switches are used to adjust the PWM duty of both channels. We can see that the light intensity of LED increases as the PWM duty increases.

MPLAB XC8 Program

#define _XTAL_FREQ 20000000
#define TMR2PRESCALE 4

#include <xc.h>

// BEGIN CONFIG
#pragma config FOSC = HS   // Oscillator Selection bits (HS oscillator)
#pragma config WDTE = OFF  // Watchdog Timer Enable bit (WDT enabled)
#pragma config PWRTE = OFF // Power-up Timer Enable bit (PWRT disabled)
#pragma config BOREN = ON  // Brown-out Reset Enable bit (BOR enabled)
#pragma config LVP = OFF   // Low-Voltage (Single-Supply) In-Circuit Serial Programming Enable bit (RB3 is digital I/O, HV on MCLR must be used for programming)
#pragma config CPD = OFF   // Data EEPROM Memory Code Protection bit (Data EEPROM code protection off)
#pragma config WRT = OFF   // Flash Program Memory Write Enable bits (Write protection off; all program memory may be written to by EECON control)
#pragma config CP = OFF   // Flash Program Memory Code Protection bit (Code protection off)
//END CONFIG

long freq;

int PWM_Max_Duty()
{
  return(_XTAL_FREQ/(freq*TMR2PRESCALE);
}

PWM1_Init(long fre)
{
  PR2 = (_XTAL_FREQ/(fre*4*TMR2PRESCALE)) - 1;
  freq = fre;
}

PWM2_Init(long fre)
{
  PR2 = (_XTAL_FREQ/(fre*4*TMR2PRESCALE)) - 1;
  freq = fre;
}

PWM1_Duty(unsigned int duty)
{
  if(duty<1024)
  {
    duty = ((float)duty/1023)*PWM_Max_Duty();
    CCP1X = duty & 2;
    CCP1Y = duty & 1;
    CCPR1L = duty>>2;
  }
}

PWM2_Duty(unsigned int duty)
{
  if(duty<1024)
  {
    duty = ((float)duty/1023)*PWM_Max_Duty();
    CCP2X = duty & 2;
    CCP2Y = duty & 1;
    CCPR2L = duty>>2;
  }
}

PWM1_Start()
{
  CCP1M3 = 1;
  CCP1M2 = 1;
  #if TMR2PRESCALAR == 1
    T2CKPS0 = 0;
    T2CKPS1 = 0;
  #elif TMR2PRESCALAR == 4
    T2CKPS0 = 1;
    T2CKPS1 = 0;
  #elif TMR2PRESCALAR == 16
    T2CKPS0 = 1;
    T2CKPS1 = 1;
  #endif
  TMR2ON = 1;
  TRISC2 = 0;
}

PWM1_Stop()
{
  CCP1M3 = 0;
  CCP1M2 = 0;
}

PWM2_Start()
{
  CCP2M3 = 1;
  CCP2M2 = 1;
  #if TMR2PRESCALE == 1
    T2CKPS0 = 0;
    T2CKPS1 = 0;
  #elif TMR2PRESCALE == 4
    T2CKPS0 = 1;
    T2CKPS1 = 0;
  #elif TMR2PRESCALE == 16
    T2CKPS0 = 1;
    T2CKPS1 = 1;
  #endif
    TMR2ON = 1;
    TRISC1 = 0;
}

PWM2_Stop()
{
  CCP2M3 = 0;
  CCP2M2 = 0;
}

void main()
{
  unsigned int i=0,j=0;
  PWM1_Init(5000);
  PWM2_Init(5000);
  TRISD = 0xFF;
  TRISB = 0;
  PWM1_Duty(0);
  PWM2_Duty(0);
  PWM1_Start();
  PWM2_Start();
  do
  {
    if(RD0 == 0 && i<1000)
      i=i+10;
    if(RD1 == 0 && i>0)
      i=i-10;
    if(RD2 == 0 && j<1000)
      j=j+10;
    if(RD3 == 0 && j>0)
      j=j-10;
    PWM1_Duty(i);
    PWM2_Duty(j);

    __delay_ms(50);
  }while(1);
}

  • PWM1_Init(frequency) & PWM2_Init(frequency) will initialize the CCP1 and CCP2 modules respectively in the PWM mode with the specified frequency. You can’t have different frequencies for CCP modules as both of them uses Timer 2 for their operation.
  • PWM1_Duty(duty) & PWM2_Duty(duty) will set the duty cycle of the generated PWM signals. The parameter duty can be any value ranging from 0 to 1023 corresponding to 0% to 100% duty cycle.
  • PWM1_Start() & PWM2_Start() are used to initialize / start the PWM signal generation.
  • PWM1_Stop() & PWM2_Stop() are used to stop the PWM signal generation.

Download Here

You can download MPLAB XC8 project files here.

Video

Share this post

  • ali

    hi im a beginner in pic . and i want to send data with spi between tw0 pic16f877a ,please who can help me?

  • void main()
    {
    CMCON = 0x07; // To turn off comparators
    ADCON1 =0x06; //Turne off adc
    TRISB = 0x00; // Sets all pins in PORTB as output
    PORTB = 0b00000001; // Set RB0 to high 00000001
    TRISA = 0x00; // Sets all pins in PORTB as output
    PORTA = 0b00000000; // Set RB0 to high 00000001

    do // To set infinite loop
    {
    Delay_ms(100); // 300 mili seconds delay
    PORTA = PORTA<= 0b10000000) //To reset to 00000001
    { //when the count becomes 10000000
    Delay_ms(100);
    PORTA = 0b00000000
    PORTB = 0b00000001
    }
    if(PORTB >= 0b00000001) //If RB7 =1
    {
    Delay_ms(100);
    PORTB = PORTB<= 0b10000000) //To reset to 00000001
    {
    Delay_ms(100); //
    PORTB =0b00000000; //RB low
    PORTA =0b00000001;
    }
    }while(1); // To set infinite loop
    }

    plz edit and send to correct C file

  • Please use our forums ( https://electrosome.com/forums/ ) for asking doubts outside the scope of above tutorial.

  • Tony

    Hello, please i need you to explain the fomula PR2 = (_XTAL_FREQ/(fre*4*TMR2PRESCALE)) – 1; in the function PWM1_Init( long fre). From the PIC16f877 data sheet the formula for the pwm width= (PR2+1)*4*XTAL FREQ*TMR2PRESCALE. If you solve for PR2 what you get is different from the formula PR2 = (_XTAL_FREQ/(fre*4*TMR2PRESCALE)) – 1;
    Please can you explain why?

  • Datasheet formula is PW = (PR2 + 1)*4*Tosc*TMR2PS

  • Rahim Bestknight

    there is probleme with the PWM2 it don’t work

  • It is 100% working fine. I checked in both proteus and hardware. I took a video also and will upload soon.

  • Aymen Fezai

    is there any way to get 250Hz pulse from PIC?

  • Muhammad Ilyas

    Ligo George..can u upload a video of it..
    ur help will highly be appreciated.thanks

  • Muhammad Ilyas

    there is an error in TMR2PRESCAL…it is highlighted in the mplab x ide .
    what could be the problem?

  • Yes, you can generate it using CCP module but you need to use a lower clock frequency for pic. I hope 4MHz will work.

  • Uploaded, please check it.

  • Don’t you defined the TMR2PRESCALE value using #define ?

  • Omotoso Omoniyi

    Nice Tutorial George, please how do I make this work for a PIC 16F690 micro controller with just one CCP module. What will be the difference? Thanks

  • Just compare both datasheets and make necessary changes.

  • Rahul Singh

    Hi. Is there any way to save the last state to eeprom when we switch off the power. Both pwm signals go to zero when power fails. thanks

  • Yes, it is there. You can save the status to the PIC internal eeprom. Sorry we don’t have any XC8 tutorials for it now.
    You can make use of following tutorial now.
    https://electrosome.com/internal-eeprom-pic-microcontroller/

  • Manasa Ramprasad

    CCP1M3 = 1;
    CCP1M2 = 1;
    HI, CAN YOU TELL ME WHAT THIS COMMANDS INDICATES ?

  • Fabien

    Hi, it’s a good website. Tutorials are well explained, very usefull. What do you think to set the contrast of an LCD screen (with HD44780 controller) by using the PWM module of a PIC, and push buttons to change the PWM duty cycle ?

  • Please see the definition of CCPxCON register in the datasheet. You can see that we need to set those bit to make CCP module to work as PWM generator.

  • Thanks for the feedback.

    Yes, we may do like that. But I think it is not a good option for HD44780 LCDs. So pic microcontrollers have built in LCD drivers. With that we can easily interface LCD glass. In that case it is really useful.

  • Hi,
    Thanks for the feedback.

    Yes you may do like that. But I don’t prefer it for LCD controller by HD44780. Some pic microcontroller have inbuilt LCD driver module with which we can drive LCD glass. In that case we can easily change the contrast by changing driving voltage.

  • Shonda Jackson

    Do you have a reference or link to any info about code that I would like to change the PWM freq dynamically – i am using mikroc w 8MHz crystal on a PIC18F26K22 chip and PWM works fine at the higher frequenices, but it does not allow me to set frequencies less than 500 Hz.

    For example, utilizing PWM1_Init(487) – I try to compile and I get an error “Argument is out of range”

    I do have success at lower freq if i try a Short Delay Routine (SDR) such as :
    while(RB0_bit == 1)
    {
    RC2_bit = 1;
    Delay_us(1019);
    RC2_bit = 0;
    Delay_us(1019);
    }

    My problem can be summed up this way
    1. I am able to run the PWM from 500 Hz to 1000 Hz fine by itself.
    2. I am able to run the SDR from 5 Hz to 500 Hz fine by itself
    3. When I try to combine PWM and SDR – the PWM range (500 – 1000 – the PWM works fine but it seems that even though use the command PWM1_STOP() – it seems that the PWM process does not release control of the pin so that I can manipulate the signal utilizing the SDR for frequencies less than 500 Hz.

    I have tried USING ONLY using the SDR for the entire range of 5 Hz to 1000 Hz utilizing a switch statement, but I get a compilation error (There is not enough ROM space) as well when I try to compile.

    switch (freq)
    {
    case 184: while(RB0_bit == 1) { RC2_bit = 1; Delay_us(1019); RC2_bit = 0; Delay_us(1019); } break;
    case 183: while(RB0_bit == 1) { RC2_bit = 1; Delay_us(1024); RC2_bit = 0; Delay_us(1025); } break;
    case 182: while(RB0_bit == 1) { RC2_bit = 1; Delay_us(1030); RC2_bit = 0; Delay_us(1030); } break;
    case 181: while(RB0_bit == 1) { RC2_bit = 1; Delay_us(1035); RC2_bit = 0; Delay_us(1036); } break;
    case 180: while(RB0_bit == 1) { RC2_bit = 1; Delay_us(1041); RC2_bit = 0; Delay_us(1042); } break;
    // etc …
    // etc …
    case 10: while(RB0_bit == 1) { RC2_bit = 1; Delay_us(18750); RC2_bit = 0; Delay_us(18750); } break;
    case 9: while(RB0_bit == 1) { RC2_bit = 1; Delay_us(20833); RC2_bit = 0; Delay_us(20833); } break;
    case 8: while(RB0_bit == 1) { RC2_bit = 1; Delay_us(23437); RC2_bit = 0; Delay_us(23438); } break;
    case 7: while(RB0_bit == 1) { RC2_bit = 1; Delay_us(26785); RC2_bit = 0; Delay_us(26786); } break;
    case 6: while(RB0_bit == 1) { RC2_bit = 1; Delay_us(31250); RC2_bit = 0; Delay_us(31250); } break;
    case 5: while(RB0_bit == 1) { RC2_bit = 1; Delay_us(37500); RC2_bit = 0; Delay_us(37500); } break;
    }

  • Hi,
    PWM module is not intended for LOW frequencies. You may try coding with normail GPIO pins for that.