Monday, February 28, 2011

AnyPWM Revisited

In my original post, I presented an Arduino sketch that could output PWM on any digital pin. It occurred to me spontaneously that the code was horribly inefficient, both in size and execution. So I've refactored the code a bit, and now it appears to work at least as well as the built in PWM functions.

The first thing I realized was that the algorithm I was using to track the signals was horribly inefficient; there was way more code and operations than were necessary, and I was tracking too much information. The other thing I realized was that the pulses were not in sync; that might not be too important for most applications, but there might be an application where timing is more critical.

Here's the code:
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:   void pulse();  
18:   void analogWrite(byte pin, byte level);  
19:   void init();  
20:  }  
21:    
22:  // Variables to keep track of the pin states  
23:  volatile byte AnyPWM::pinLevel[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };  
24:    
25:  // Set a digital out pin to a specific level  
26:  void AnyPWM::analogWrite(byte pin, byte level) {  
27:   if (pin > 1 && pin < 14 && level >= 0 && level < PERIOD) {  
28:    pin -= 2;  
29:    AnyPWM::pinLevel[pin] = level;  
30:    if (level == 0) {  
31:     digitalWrite(pin + 2, LOW);  
32:    }  
33:   }  
34:  }  
35:    
36:  // Initialize the timer routine; must be called before calling  
37:  // AnyPWM::analogWrite!  
38:  void AnyPWM::init() {  
39:   // (PERIOD * 64) Hertz seems to be a high enough frequency to produce  
40:   // a steady PWM signal on all 12 output pins  
41:   FlexiTimer2::set(1, 1.0/(PERIOD * 64), AnyPWM::pulse);  
42:   FlexiTimer2::start();  
43:  }  
44:    
45:  // Routine to emit the PWM on the pins  
46:  void AnyPWM::pulse() {  
47:   static int counter = 0;  
48:   for(int i = 0; i < 12; i += 1) {  
49:    if (AnyPWM::pinLevel[i]) {  
50:     digitalWrite(i + 2, AnyPWM::pinLevel[i] > counter);  
51:    }  
52:   }  
53:   counter = ++counter > PERIOD ? 0 : counter;  
54:  }  
55:    
56:  void setup() {  
57:   AnyPWM::init();    // initialize the PWM timer  
58:   pinMode(LED, OUTPUT); // declare LED pin to be an output  
59:  }  
60:    
61:  byte brightness = 0;  // how bright the LED is  
62:  byte fadeAmount = 5;  // how many points to fade the LED by  
63:    
64:  void loop() {  
65:   // set the brightness of the LED:  
66:   AnyPWM::analogWrite(LED, brightness);  
67:    
68:   // change the brightness for next time through the loop:  
69:   brightness = brightness + fadeAmount;  
70:    
71:   // reverse the direction of the fading at the ends of the fade:   
72:   if (brightness == 0 || brightness == 255) {  
73:    fadeAmount = -fadeAmount;  
74:   }  
75:   // wait for 30 milliseconds to see the dimming effect  
76:   delay(30);  
77:  }  
Download code

The first thing to notice is that I've gotten rid of 2 arrays: state and timer. These are completely unnecessary, and it saves 24 bytes right off the top. Second, I've upped my multiplier to calculate the timer frequency to 64, which makes for smoother levels when more pins are being pulsed. Finally, a lot of code in the pulse() function has been replaced with a simple comparison against a single rolling counter, making the CPU time spent in the routine much shorter.

The entire code savings is about 80 bytes from the original, which makes this sketch come in at 2114 bytes, compared to the 1252 bytes of the compiled Fade sketch (862 byte difference). Additionally, the performance is much closer to the built in analogWrite() functionality. I don't know the exact timing, but the difference is fairly imperceptible to a human, even when pulsing 6 pins (the maximum number of PWM pins on an ATmegaXX8 Arduino).

No comments:

Post a Comment