ATmega168 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.

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

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.

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

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

Downloads

More Information