Program description

What does it do?

The PWM program reads an analogue value from a potentiometer and displays the analogue value as a varying LED brightness using PWM (Pulse Width Modulation).

The PWM program also features a for loop and a nested set of subroutine calls.

New concepts

for statements are commonly used to create conditional loops.

for loops begin by setting the loop starting state, ending condition, and index, or the amount of change in each cycle. If all of these parameters are known ahead of time, for loops can be more compact than equivalent while loops.

			
for (count = 10; count != 0; count --)
	statement;
			

The above for loop is shorter, and functionally equivalent to the while loop, below:


count = 10;
while (count != 0)
{
	statement;
	count --;
}
			

PWM programming activity

The analogue program converted an analogue value into a digital number representing the input value. A-D conversion was made possible because of the analogue-to-digital converter hardware built into the microcontroller. Unfortunately, the PIC microcontroller does not contain equivalent D-A converter hardware, so it's not capable of producing a true analogue output from its digital data.

For many applications, a real analogue output may not be necessary, and a PWM (Pulse Width Modulated) digital output can be used instead. Pulse-width modulation rapidly turns an output on and off over time, producing an instantaneous average level at a potential between the off and on states of the output pin.

What you should know before starting

PWM waves

PWM waves usually have a constant time period and a variable duty cycle, or the ratio of on-time to off-time, as shown in the diagram:

PWMwaves

The first wave has an on-time that is 10% of the total wave period. Averaged over time, the energy contained in the on pulses is 10% of the maximum energy available had the output stayed on (shown by the red line). Likewise, the second and third waves show 50%, and 90% energy levels, respectively.

Now, imagine a device such as an LED connected to the PWM signal. At low pulse frequencies, say one pulse per second, the LED would blink on for a fraction of the second, and then turn off.

If the pulses repeat at a high enough frequency — turning on the LED hundreds, or thousands of times per second, for example — the individual pulses will no longer be visible to us. At high frequencies, your eye will see the LED producing an amount of light equivalent to the average energy level of the pulses being sent to it. In essence, we can get an LED which is just turning on and off to mimic an analogue output by: turning the LED on and off faster than the eye can register; and, varying the width of the LED on and off pulses.

PWM isn't just useful for LED control — PWM can modify sound waves for speakers, as well as drive motors at variable speeds.

Create the PWM program

PWM uses the same files as the Analogue project, with the following new PWM.c file:


/*==============================================================================
	Project: PWM
	Version: 3.1				Date: November 24, 2014
	Target: CHRP3				Processor: PIC16F886

	This program demonstrates PWM (pulse-width modulation) output to a single
	LED, 'for' loops, and nested function calls. The analogue input potential
	modulates the 'on-time' of the LED. Longer on-times produce brighter light.
==============================================================================*/

#include	"xc.h"				// XC compiler general include file

#include	"stdint.h"			// Include integer definitions
#include	"stdbool.h"			// Include Boolean (true/false) definitions

#include	"CHRP3.h"			// Include user-created constants and functions

/*==============================================================================
	A/D conversion function. Pass the channel to be converted using one of the
	definitions in user.h. Returns the most significant 8-bits of the result.
==============================================================================*/

unsigned char adConvert(unsigned char chan)
{
	ADON = 1;					// Turn A-D converter on
	ADCON0 = (ADCON0 & 0b11000011);	// Change the conversion channel by wiping
	ADCON0 = (ADCON0 | chan);		// CHS bits and ORing with selected channel
	NOP();						// Wait to allow the input to stabilize
	GO_DONE = 1;				// Start the conversion

	while(GO_DONE);				// Wait for the conversion to finish (GO_DONE=0)
	
	ADON = 0;					// Turn A-D converter off and
	return (ADRESH);			// return the upper 8-bits of the result
}

/*==============================================================================
 PWM function. Falling count comparison turns LED on when counter = brightness.
==============================================================================*/

void pwmLED10(unsigned char brightness)
{
	unsigned char counter;		// PWM period counter
	LED10 = 0;					// Turn LED10 off.

	for(counter = 255; counter != 0; counter --)	// Count down from 255 to 0
	{
		if (counter == brightness)	// Turn on the LED when counter = brightness
			LED10 = 1;
	}
}

/*==============================================================================
	Main program code
==============================================================================*/

int main(void)
{
    // Initialize I/O and peripherals
    initPorts();

    // Set starting conditions

    while(1)
    {
		pwmLED10(adConvert(ADVR1));		// Set LED10 brightness based on VR1.
    }									// CHRP3.h shows other possible inputs.
}

		

Download the program into the CHRP and verify its operation by turning the potentiometer. LED10 will brighten and dim corresponding to the position of the potentiometer.

How the program works

The PWM function

There are two common methods of generating PWM signals from an analogue value. The first method most people come up with usually looks something like the following pseudo-code:


	onTime = adConvert(ADVR1);
	LED10 = 1;
	delay(onTime);
	
	offTime = 255 - onTime;
	LED10 = 0;
	delay(offTime);
		

In this method, the PWM on-time is directly derived from the analogue result. The LED is turned on for a time duration proportional to the A-D value.

Next, it's time to calculate the off-time. To do so, the maximum time period needs to be known. Since the A-D conversion routine returns an 8-bit value, the maximum time period will be 255 — the same as the maximum A-D result. Therefore, if the analogue value is 255, the on-time will be 255, and the off-time will be 255 - 255 = 0. Likewise, if the on-time is 0, the off-time will be 255.

After calculating the off time, all that's left to finish the PWM pulse is to turn off the LED and wait for the duration of the off-time.

Despite this method seeming straightforward, it's a bit more code intensive that the solution we'll look at next, and it doesn't lend itself to producing multiple, simultaneous PWM outputs. For these reasons, we'll use a different method to make our PWM wave:


/*==============================================================================
 PWM function. Falling count comparison turns LED on when counter = brightness.
==============================================================================*/

void pwmLED10(unsigned char brightness)
{
	unsigned char counter;		// PWM period counter
	LED10 = 0;					// Turn LED10 off.

	for(counter = 255; counter != 0; counter --)	// Count down from 255 to 0
	{
		if (counter == brightness)	// Turn on the LED when counter = brightness
			LED10 = 1;
	}
}
		

The pwmLED10 function declaration accepts a brightness value, which will be an 8-bit character capable of representing 256 light levels. A counter variable is also needed to count the PWM delay periods, so the first line of the function is used to define it;

The first step in producing the PWM signal is to turn the LED off. Next, the for loop for(counter = 255; counter != 0; counter --) starts a count-down counter at 255. If the counter is equal to the brightness value, the LED will be turned on.

The first time through the loop, the LED will turn on if brightness is equal to 255, the counter value. If not, the counter decrements by one to 254, and is compared with the brightness value again. The counter keeps decrementing while it's not zero. The counter != 0 conditional check is known as a hard condition, and, in the microcontroller used in the CHRP, results in more efficient code generation than an equivalent counter > 0 soft condition.

As the counter counts down, the delay inherent in the countdown process becomes the source of the PWM time delay. There is no separate on-time and off-time delay. All of the delay happens within the countdown loop. At some point during the countdown, the counter value will match the PWM value and the LED will be turned on. The duration of the on-time will be the remainder of the full count, matching the PWM on-time to the original analogue conversion result.

Eventually, the counter will reach zero, and the PWM function will finish.

Local variables

The counter variable defined in the PWM function is a local variable – its contents are only available to the code within the PWM function. Defining a variable within the function makes it local to that function. Defining a variable outside the function makes it global, or available to all of the program code, including within all functions.

Nested function calls

The adConvert function is unchanged from the Analogue project, and is used to read the analogue position of the potentiometer. Whereas the Analogue program's A-D result is sent directly to the Port B LEDs, the A-D result in this program becomes the input for the pwmLED10 function:


    while(1)
    {
		pwmLED10(adConvert(ADVR1));		// Set LED10 brightness based on VR1.
    }									// User.h shows other possible inputs.
		

Starting from the innermost bracket, the ADVR1 constant is passed to the adConvert function. adConvert completes an A-D conversion and returns a value which gets passed to the pwmLED10 function. pwmLED10 completes one PWM cycle and returns nothing — other than a flash of light corresponding to the analogue value. Since this nested function is the only statement within the while loop, the next cycle starts immediately after the current cycle completes.

While nested function calls are a powerful concept, it understandably seems a bit strange the first time you see a function call containing a function call.

Test your knowledge

  1. Will any value be returned from the pwmLED10 function? How can you tell?
  2. Why is the for loop's counter != 0; condition check preferable to a counter > 0; decision?
  3. If the value 187 is passed to the pwmLED10 function, for how many cycles will LED10 be lit? For how many cycles will LED10 be off?
  4. What will the pwmLED10 function do when the value zero is passed to it? Will LED10 turn on at all?

Apply your skills

  1. Create a program that controls LED brightness through pushbutton presses. For example, press S3 to decrease the brightness and S5 to increase the brightness.
  2. Modify the above program to simultaneously control two or more LEDs at different brightnesses.