Saturday, February 26, 2011

PWM on Any Digital Pin on Arduino

I recently got an Arduino to play around with. It's been a few years since I've done some electronics hacking, and I wanted to get back into the hobby. There are tons of low cost embedded microcontrollers to play with these days, compared to when I was back in college, so I decided to give the Arduino a try. My first experiment was to emit PWM signals on any of the digital output pins.

My real goal was to learn how to use the hardware interrupt timers in general in the Arduino environment. I knew I would have to do this to implement my impending hardware project to build a 16x8 LED matrix light board. Fortunately doing that is pretty much like any microcontroller, so it's not too difficult to figure out from the documentation. Even more fortunately, Wim Leers abstracted out all the necessary logic to figure out the current clock speed and timer implementations of various Arduino platforms, making it child's play to set up an interrupt timer routine. His library co-opts timer 2 and is called FlexiTimer2.

Pulse-Width Modulation is simply manipulating the high and low value durations of a fixed period digital waveform to simulate an analog signal level. Well, maybe it's not that simple, but you can check out the Wikipedia page to get a more technically correct explanation. In any case, the Arduino bootstrap environment provides PWM output on specific pins through a call to analogWrite. While some ATmega pins support hardware PWM, on others it is done in software using timers. My sketch does this as well:
1:   // AnyPWM by Nick Borko  
2:   // This work is licensed under a Creative Commons  
3:   // Attribution-ShareAlike 3.0 Unported License  
4:    
5:   // Manually do PWM using FlexiTimer2  
6:   // (http://www.arduino.cc/playground/Main/FlexiTimer2)  
7:   #include <FlexiTimer2.h>  
8:    
9:   // LED to pulse (non-PWM pin)  
10:  #define LED 13  
11:    
12:  // Period of the PWM wave (and therefore the number of levels)  
13:  #define PERIOD 256  
14:    
15:  namespace AnyPWM {  
16:   extern volatile byte pinLevel[12];  
17:   extern boolean state[12];  
18:   extern byte timer[12];  
19:   void pulse();  
20:   void analogWrite(byte pin, byte level);  
21:   void init();  
22:  }  
23:    
24:  // Variables to keep track of the pin states  
25:  volatile byte AnyPWM::pinLevel[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };  
26:  boolean AnyPWM::state[12] = { LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW };  
27:  byte AnyPWM::timer[12] = { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 };  
28:    
29:  // Set a digital out pin to a specific level  
30:  void AnyPWM::analogWrite(byte pin, byte level) {  
31:   if (pin > 1 && pin < 14 && level >= 0 && level < PERIOD) {  
32:    pin -= 2;  
33:    AnyPWM::pinLevel[pin] = level;  
34:    if (level == 0) {  
35:     // reset the PWM state  
36:     AnyPWM::state[pin] = LOW;  
37:     AnyPWM::timer[pin] = 255;  
38:     digitalWrite(pin + 2, LOW);  
39:    }  
40:   }  
41:  }  
42:    
43:  // Initialize the timer routine; must be called before calling  
44:  // AnyPWM::analogWrite!  
45:  void AnyPWM::init() {  
46:   // (PERIOD * 48) Hertz seems to be a high enough frequency to produce  
47:   // a steady PWM signal on all 12 output pins  
48:   FlexiTimer2::set(1, 1.0/(PERIOD * 48), AnyPWM::pulse);  
49:   FlexiTimer2::start();  
50:  }  
51:    
52:  // Routine to emit the PWM on the pins  
53:  void AnyPWM::pulse() {  
54:   for(int i = 0; i < 12; i += 1) {  
55:    if (AnyPWM::pinLevel[i]) {  
56:     if (AnyPWM::timer[i] == 0) {  
57:      AnyPWM::timer[i] = (AnyPWM::state[i] = !AnyPWM::state[i]) ? AnyPWM::pinLevel[i] : (byte)PERIOD - AnyPWM::pinLevel[i];  
58:     } else {  
59:      digitalWrite(i + 2, AnyPWM::state[i]);  
60:     }  
61:     AnyPWM::timer[i] -= 1;  
62:    }  
63:   }  
64:  }  
65:    
66:  void setup() {  
67:   AnyPWM::init();    // initialize the PWM timer  
68:   pinMode(LED, OUTPUT); // declare LED pin to be an output  
69:  }  
70:    
71:  byte brightness = 0;  // how bright the LED is  
72:  byte fadeAmount = 5;  // how many points to fade the LED by  
73:    
74:  void loop() {  
75:   // set the brightness of the LED:  
76:   AnyPWM::analogWrite(LED, brightness);  
77:    
78:   // change the brightness for next time through the loop:  
79:   brightness = brightness + fadeAmount;  
80:    
81:   // reverse the direction of the fading at the ends of the fade:   
82:   if (brightness == 0 || brightness == 255) {  
83:    fadeAmount = -fadeAmount;  
84:   }  
85:   // wait for 30 milliseconds to see the dimming effect  
86:   delay(30);  
87:  }  
Download code

Now, a LED attached to pin 13 (or the surface mounted LED on some Arduino boards), a non-PWM pin, will pulse, just like the Fade sketch in the Examples (you'll note the loop code is nearly identical).

There are some downsides to this approach. First, the code is 942 bytes longer than using the stock analogWrite call on a PWM pin. Second, since all pins share the same timer, the more pins you write to, the slower the execution. This may be an acceptable trade off, and the timing issue may be mitigated by decreasing the duty cycle (PERIOD) while keeping the timer frequency the same (set in AnyPWM:init via FlexiTimer2::set).

This same approach could be used for other applications than PWM. For example, you could control an RGB LED matrix to provide up to 24-bit color, assuming the processor was fast enough; instead of controlling multiple output pins, you could instead be controlling the individual red, green and blue values for each LED through a serial driver. Or, 16 levels of brightness on a single color LED array (if PERIOD is set to 16), which I am thinking about doing for my LED project.

UPDATE: Don't use this code! Instead, read my follow-up and download the code!

No comments:

Post a Comment