Generating PWM with PIC Microcontroller – MPLAB XC8
Contents
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.
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.
- 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.
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.
Configuration
- Set the PWM Period by writing to PR2 register. The relation between PR2 value and time period is shown below.
- 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.
- Configure the CCP pins as Output by writing to TRIS<2> and TRIS<1> bits.
- Set the Timer 2 (TMR2) prescale value and enable it by writing to T2CON register.
- Finally, configure the CCP module for PWM operation by writing to CCP control registers.
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.
Very good tutorial mate , thanks alot , i just have a question , is it possible to generate an inversed PWM signal ?
Very interesting, i like to see this program in c18 toolsuite, please help me!
An excellent tutorial on PWM
Beautifully done
can u explain this part of the code please? Where did you get the logic of AND operator and then right shifting?
PWM1_Duty(unsigned int duty)
{
if(duty>2;
}
}
Hi,does it possible to produce 100hz-200hz by using above method?nice tutorial….
Hi Ligo, could the push button switches be replaced with a potentiometer (one per PWM), i am having trouble with what the circuit/code adjustments would be to do this.
Great Tutorial, thanks for sharing!
Hi Ligo, I follow all your PIC projects using XC8 and have simulated many of them. This PWM project has code “#if”, “#elif” and “#endif”. Unfortunately my MPLAPXIDE and XC8 are giving errors for these instructions. Please help in resolving this. Many Thanks. Jogesh
Hi,
PWM module is not intended for LOW frequencies. You may try coding with normail GPIO pins for that.
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,
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.
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.
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.
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 ?
CCP1M3 = 1;
CCP1M2 = 1;
HI, CAN YOU TELL ME WHAT THIS COMMANDS INDICATES ?
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/
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
Just compare both datasheets and make necessary changes.
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
Don’t you defined the TMR2PRESCALE value using #define ?
Uploaded, please check it.
Yes, you can generate it using CCP module but you need to use a lower clock frequency for pic. I hope 4MHz will work.
there is an error in TMR2PRESCAL…it is highlighted in the mplab x ide .
what could be the problem?
Ligo George..can u upload a video of it..
ur help will highly be appreciated.thanks
is there any way to get 250Hz pulse from PIC?
It is 100% working fine. I checked in both proteus and hardware. I took a video also and will upload soon.
there is probleme with the PWM2 it don’t work
Datasheet formula is PW = (PR2 + 1)*4*Tosc*TMR2PS
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?
Please use our forums ( https://electrosome.com/forums/ ) for asking doubts outside the scope of above tutorial.
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
hi im a beginner in pic . and i want to send data with spi between tw0 pic16f877a ,please who can help me?