Skip to main content
ATmega168 Pulse Width Modulation - PWM

ATmega168A Pulse Width Modulation – PWM

Dimming an incandescent bulb is easy. Simply adjust the current down using a potentiometer and you are done. Dimming an LED is another story entirely. When you reduce current through an LED there are unintended consequences like color shifts and dropouts. A better way is to use Pulse Width Modulation (PWM).

With PWM the LED is turned on and off many times per second. By adjusting the percentage of on time vs off time, the brightness of the LED can be controlled. The diagram below shows 4 PWM wave forms with varying duty cycles. The 90% duty cycle example would produce the greatest brightness whilst the 10% duty cycle will produce the least. The selected period should be sufficiently short so that a flicker isn’t noticeable.

Duty Cycles

LED dimming is not the only application of PWM. PWM is often used for controlling motors, regulating DC power, Digital to analogue conversion and much more.

Source Code Example 1

The example below produces a PWM waveform with a 20% duty cycle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <avr/io.h>
#include <util/delay.h> 
 
#define LED_PORT    PORTC
#define LED_PIN     0
#define LED_off()  LED_PORT&=~_BV(LED_PIN)
#define LED_on() LED_PORT|=_BV(LED_PIN)
 
int main (void)
{
	DDRC  = 0b10000000;
	while (1)
	{
		LED_on();
		_delay_ms(2);
		LED_off();
		_delay_ms(8);
	}
 
	return(0);
}

For most embedded applications, controlling PWM this way is not practical. A better approach would be to use the inbuilt PWM functionality on the ATmega168A microcontroller.

PWM output lines on ATmega168A

The ATmega168A microcontroller has 3 timers which in turn can control 6 PWM lines. Back in 2010 we did a tutorial on ATmega168 Timer Interrupts. If you are not familiar with timers, it would be useful to read this tutorial first.

ATmega168 PWM pinout

On each timer, the ATmega168A has 4 modes that produce a PWM wave form. These are selected by setting the appropriate WGMx2, WGMx1 and WGMx0 bits in TCCRxA and TCCRxB.

Mode WGMx2 WGMx1 WGMx0 Description Top
1 0 0 1 PWM, Phase Correct 0xFF
3 0 1 1 Fast PWM 0xFF
5 1 0 1 PWM, Phase Correct OCRxA
7 1 1 1 Fast PWM OCRxA

Each of the 6 PWM output lines can be set into 1 of 4 output modes. These are controlled by the COMxA1, COMxA0, COMxB1 and COMxB0 bits in the TCCRxA registers.

COMxx1 COMxx0 Description
0 0 Normal port operation, no PWM.
0 1 For timer mode 3, Normal port operation, no PWM
For timer mode 7, toggle output on compare match
1 0 Non Inverting Mode
1 1 Inverting Mode

This tutorial will focus on timer mode 3 and the non inverting port mode. To use these modes we need to:

  • Set the I/O lines to output mode by setting bits in the DDRx registers (see Introduction to I/O Registers)
  • Select mode 3 by setting WGMx1 and WGMx0 bits in TCCRxA
  • Select desired output port mode by setting the COMxA1 and COMxB1 bits in TCCRxA
  • Select the duty cycle for each port by setting the OCRxA and OCRxB registers (These are split into high and low registers on the 16 bit timer)

Fun with RGB LEDs

In the next example we’ll be using an RGB LED and use PWM to mix the colors together. These LEDs are really 3 different LEDs in a single epoxy package. They have a common cathode and 3 anode pins as shown below.

ATmega168 PWM pinout

Because we’ll be mixing the colors together, we sanded the LED lightly to help diffuse the light.

RGB LED lightly sanded

The circuit for this example is pretty simple with the RGB LED connected to OC0A, OC0B and OC2A. (PD6, PD5 and PB3). The RGB LED has different forward voltages for each color. We plugged these values into the LED Resistor Calculator and picked the nearest sizes we had on hand.

RGB Color mixer Circuit

Source Code Example 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#include <avr/io.h>
#include <util/delay.h> 
#include <math.h>
 
typedef struct 
{
    float red;          //0 to 1
    float green;        //0 to 1
    float blue;         //0 to 1
} SColorRGB;
 
typedef struct
{
    float hue;          //0 to 360
    float saturation;   //0 to 1
    float brightness;   //0 to 1
}  SColorHSB;
 
int main (void)
{
    DDRB  = 0b00001000;   // PB3 output
    DDRD  = 0b01100000;   // PD5 and PD6 outputs
 
    SColorHSB hsb;
    SColorRGB rgb;
    hsb.hue = 0;
    hsb.saturation = 1;
    hsb.brightness = .5;
 
    TCCR0A = _BV(COM0A1) | _BV(COM0B1) | _BV(WGM01) | _BV(WGM00);      // Non inverting mode on OC0A and OC0B, Mode = Mode 3 FAST PWM
    TCCR0B = _BV(CS00);                                                // No prescaling
 
    TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);      // Non inverting mode on OC2A, Mode = Mode 3 FAST PWM
    TCCR2B = _BV(CS20);                                                // No prescaling
 
    while (1)
    {
        rgbFromHSB(hsb,&rgb);
 
        OCR0A = (unsigned char)(255*rgb.red);
        OCR0B = (unsigned char)(185*rgb.green);
        OCR2A = (unsigned char)(150*rgb.blue);
 
        //Uncomment and adjust the values below to adjust white balance, then transfer the value above
        //OCR0A=255;
        //OCR0B=185;
        //OCR1A=150;
 
        _delay_ms(10);
        if (hsb.hue==359)
            hsb.hue=0;
        else
            hsb.hue++;
    }
 
    return(0);
}
 
void rgbFromHSB(SColorHSB __hsb,SColorRGB * __rgb)
{
	if (__hsb.saturation==0) 
	{
		__rgb->red = __hsb.brightness;
		__rgb->green = __hsb.brightness;
		__rgb->blue = __hsb.brightness;
	}
	else
	{
		float max = __hsb.brightness;
        float dif = __hsb.brightness * __hsb.saturation;
        float min = __hsb.brightness - dif;
 
        if (__hsb.hue < 60)
        {
            __rgb->red = max;
            __rgb->green = __hsb.hue * dif / 60 + min;
            __rgb->blue = min;
        }
        else if (__hsb.hue < 120)
        {
            __rgb->red = -(__hsb.hue - 120) * dif / 60 + min;
            __rgb->green = max;
            __rgb->blue = min;
        }
        else if (__hsb.hue < 180)
        {
            __rgb->red = min;
            __rgb->green = max;
            __rgb->blue = (__hsb.hue - 120) * dif / 60 + min;
        }
        else if (__hsb.hue < 240)
        {
            __rgb->red = min;
            __rgb->green = -(__hsb.hue - 240) * dif / 60 + min;
            __rgb->blue = max;
        }
        else if (__hsb.hue < 300)
        {
            __rgb->red = (__hsb.hue - 240) * dif / 60 + min;
            __rgb->green = min;
            __rgb->blue = max;
        }
        else if (__hsb.hue <= 360)
        {
            __rgb->red = max;
            __rgb->green = min;
            __rgb->blue = -(__hsb.hue - 360) * dif / 60 + min;
        }
        else
        {
            __rgb->red = 0;
            __rgb->green = 0;
            __rgb->blue  = 0;
        }
	}
}

This example cycles through all color hues: Red fading to orange then fading to yellow and so on. To achieve this we’ll use a HSB (Hue, Saturation, Brightness) color model, which is then converted to RGB values.

Although we are pumping roughly the same current for each color, some colors have greater intensity and perceived brightness than others. On lines 40-42 we multiply RGB values by a scaling factor then assign them to the corresponding Output Compare (OCRxA/B) registers. The scaling factor adjusts for the different intensities in order to preserve white balance.

Example 3 using Timer Interrupts

What do you do if you need more than 6 PWM lines? There are many solutions to this problem. Martin Ederveen, one of our customers sent us a solution that supports 3 lines, but can easily be modified for more. This solution uses timer interrupts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>
#include <avr/delay.h>
#include <stdio.h>
 
#define _ms(n) (17*n)
 
void wait(unsigned int a) //basic wait
{ 
volatile unsigned int b,c;
for(b=0;b!= a; b++)for(c=0;c!= 50;c++);
return;
}
 
int const bitsize=16;
unsigned int one_pwm[16];
unsigned int two_pwm[16];
unsigned int three_pwm[16];
unsigned int p1=1; // Set value of point to rightmost zero in array
unsigned int p2=1;
unsigned int p3=1;
 
void init_pwm () // Initialize pwm arrays
{
 unsigned int tel=0;
 for (tel=1;tel<bitsize;tel++)
 {
  one_pwm[tel]=0;
  two_pwm[tel]=0;
  three_pwm[tel]=0;
 }
 one_pwm[0]=1; // Set value of rightmost bit in array
 two_pwm[0]=1;
 three_pwm[0]=1;
 p1=1; // Set value of point to rightmost zero in array
 p2=1;
 p3=1;
 return;
}
 
 
unsigned char port_write=0b00000000;
unsigned int bitje=0;
 
ISR(SIG_OUTPUT_COMPARE1A)
{
 if (one_pwm[bitje]==1)
  { port_write |= (1<<0);}
 else 
  {port_write &= ~(1<<0);}
 if (two_pwm[bitje]==1)
  { port_write |= (1<<1);}
 else 
  {port_write &= ~(1<<1);}
 if (three_pwm[bitje]==1)
  { port_write |= (1<<2);}
 else 
  {port_write &= ~(1<<2);}
 
 bitje++;
 if (bitje>=bitsize) {bitje=0;}
 PORTB=port_write;
 return;
}
 
void ioinit (void)
{
   DDRC  = 0b11000111; //1 = output, 0 = input
   PORTC = 0b00111000; //Enable pin 5, 4 and 3 internal pullup
   DDRB  = 0b11111111; //1= output, 0 = input
   PORTB = 0b00000111; //Set PB pins 0, 1 and 2 to high
}
 
 
#define BTUP() (bit_is_clear(PINC,4))
#define BTDOWN() (bit_is_clear(PINC,5))
#define SWITCH() (bit_is_clear(PINC,3))
 
int main(void)
{
TIMSK1 = _BV(OCIE1A); //Enable Interrupt Timer/Counter 1,Output Compare A
TCCR1B = _BV(CS11) | _BV(WGM12); //Clock/8, 0.000008 secs/tick, Mode=CTC
OCR1A = 16; // SIG_COMPARE1A triggered every 0.000008*16 Seconds
 
init_pwm();
sei();
ioinit();
//lcd_init();
 
unsigned int maxdelay=1;
unsigned int thisport=1;
 
while (1)
{
 if (SWITCH()) // Button for other color control has been pushed
  {
   thisport++;
   if (thisport>3) {thisport=1;} // Note: only 3 ports in use
   _delay_ms(maxdelay*4);
  }
 
 if (BTUP())  // Button for pwm increase has been pushed
  {
   switch (thisport) 
   {
    case 1: if (p1<bitsize) 
	        {
			 one_pwm[p1]=1;
			 p1++;
			}
			break;
	case 2: if (p2<bitsize)
	        {
	         two_pwm[p2]=1;
			 p2++;
			}
			break;
	case 3: if (p3<bitsize)
	        {
			 three_pwm[p3]=1;
			 p3++;
			}
			break;
	default: break;
   }
  _delay_ms(maxdelay);
  }
 if (BTDOWN())  // Button for pwm decrease has been pushed
  {
   switch (thisport)
   {
    case 1: if (p1>0)
	        {
              p1--;
			 one_pwm[p1]=0;
			}
 
	break;
	case 2: if (p2>0)
	        {
			 p2--;
			 two_pwm[p2]=0;
			}
			break;
	case 3: if (p3>0)
	        {
			 p3--;
			 three_pwm[p3]=0;
			}
			break;
	default: break;
   }
  _delay_ms(maxdelay);
  }
 //char buffer[16];
 //sprintf(buffer, "%2d %2d %2d  P%1d",p1,p2,p3,thisport);
 //lcd_goto(0);
 //lcd_puts(buffer);
 _delay_ms(maxdelay);
}
}

Downloads

More Information

Related News

Analogue to Digital Conversion Interrupts on an ATmega168

Analogue to Digital Conversion Interrupts on an ATmega168A

Back in February, we wrote a post on Analogue to Digital Conversion. Many people mentioned...

External Interrupts on an ATmega168

This tutorial will teach you how to use external and pin change interrupts on an...

hd44780 Character LCD Displays – Part 2

Introduction This tutorial continues from Character LCD Displays - Part 1. In this part we...

6 Comments

  1. Shelby

    Please correct your BLUE wire (blue LED lead). You have it going to ground.

    1. protostackadmin Author

      Thanks for spotting that one. All fixed now.

  2. kiranvarma-npeducations

    I connected every thing as you described, i added one LCD. When i pressed the button the LCD value is double incremented. What was the problem?

    1. Muhammad Shafaqat Ali

      3 example not working

  3. Kenny Murphy

    Hello there, I’m wondering if you could help me out? Is it possible to connect ATmega168p to a PIR sensor for an air compressor to actuate inflatable components?

  4. Julian

    Firstly thanks for writing this great tutorial I found it very helpful.

    I know this post is a bit old but I have a question. When I setup fast PWM using my timers the PWM seems to work and I can display and cycle various colours.

    But when I assign OCR0A or OCR0B a value of 0 (zero) the LED is still very dimly lit.

    Any idea if I am doing something wrong?

Leave a reply

Your email address will not be published. Required fields are marked *

Shopping Cart