Saturday, February 6, 2010

Arduino PWM on all pins.

What is PWM?
PWM (pulse width modulation) is the art of faking a particular voltage by rapidly moving between 2 other voltages. For example if you have a digital logic line that is 0 volts when LOW or 5 volts when HIGH, you can "trick" a multimeter into reading 1 volt by setting the line to low for 4/5s of the time and high for 1/5 of the time. The ratio of high to low is called the "duty cycle" and is specified in a percentage.

Why Use PWM?

You can trick a lot of other things too if you toggle the line tens to hundreds of times a second. For example if you connect a LED it will appear dimmer due to the same "persistence of vision" effect that makes movies appear smooth.

Another example is driving motors. Motors contain electromagnets which cause electrical inductance (in analogy "momentum"). If you put a diode across the motor's leads (backwards) then when the motor is turned off, the collapse of the magnetic field continues to drive current through the wire, and through the diode back to the other side of the motor. This current maintains the magnetic field! If there was no resistance this would go on forever (BTW this is how superconductors float above magnets). But since there is resistance, the circulating current slowly dies out.

But if you are doing PWM on the motor, a new influx of energy will re-energize the motor's coils resulting in a smoothly turning motor. But the speed of the motor will vary based on the duty cycle of the PWM. So you can use PWM to change the speed of a DC motor.

The Arduino has some hardware-based PWM. But only on certain pins. For things like motors, you do not need to PWM very fast so it can be done in software. So I have made a software PWM class for Arduino sketches that works on all pins.
The Four Line states in an AVR microcontroller

Another cool feature is that it will do PWM between any 2 line states. You may normally think of a digital line as having only 2 states (high or low) but the Arduino actually offers 4 states, high, low, high impedence, and pull up.

A lot of beginners think of the "low" state as "off" but really it means driven to 0 volts. So if you put a light between a "low" pin and 5v, the light will turn on!

On the other hand "high impedence" actually means "disconnected" or "floating" -- at least as close as you can get to that using solid state electronics. So in a high impedence state, the light described above would NOT turn on. But one issue with a line in the "high impedence" state is that its voltage is undefined and can be sensed as high or low and change between the 2 due to vagaries of electric fields on the board, etc.

"Pull up" refers to a connection to 5v through a large resistor. So it is "high impedence" in the sense that it cannot drive anything, and if driven by some external device it will follow that device. But if the line is otherwise floating, the connection to 5v thru the resistor will make the line show as high.

DOWNLOAD

The Code


#define PWM_NUM_PINS 16
#define PWM_MAX_DUTY 255
#define PWM_DEFAULT_FREQ (16000000.0/128.0) //2MHz for /8 prescale from 16MHz

//?? Software based PWM (Pulse width modulation) library for the ATMEGA168/328 (Arduino).
//
// This class implements PWM in software. PWM is a method whose purpose is to emulate an analog voltage by rapidly toggling a digital
// pin between the high and low states. By changing the duty cycle -- the percent of time the pin is "on" verses "off" the apparent voltage changes.
// This technique is only useful if the pin is controlling a device that averages voltages; for example:
// * Inductors (electromagnets, motors)
// * Capacitors
// * Human perception (via LEDs for example)
// This library does not work as efficiently as the hardware based PWM availabe in the ATMEGA, so that should be used if possible.
// However this library offers PWM on all pins, and also allows you to specify what the "on" and "off" pin states are. For example, you could choose the "off" state to be high impedence.
//
class SoftPWM
{
public:
typedef enum
{
DRIVEN_LOW = 2 | 0,
DRIVEN_HIGH = 2 | 1,
DRIVEN = 2,

FLOATING = 0,
PULL_UP = 1,
HIGH_IMPEDENCE = 0,

UNUSED = 4
} PinBehavior;
//?? Constructor
SoftPWM();

//?? Call this periodically to trigger a state change
void loop(void);

//?? Automatically call the loop() function periodically. Once you call this function YOU SHOULD NOT CALL LOOP()!
void startAutoLoop(int timer=2,int frequency=PWM_DEFAULT_FREQ);
//?? Stop automatic looping.
void stopAutoLoop(void);

//?? Duty Cycle. The fraction of time that the pin is HIGH is duty/255
uint8_t duty[PWM_NUM_PINS];

//?? Set what it means for a pin to be off or on.
// Typically you would want to PWM an output pin and toggle output voltage. This is what PWM normally means, and is the default.
// However, for other applications you may want to toggle the impedence and value simultaneously.
// For example, if the pin is sinking the base of a PNP transistor then you would switch from a high impedence (i.e. open circuit) state to a low output state (i.e. closed circuit, sinking current).
//
void enablePin(int pin, PinBehavior offMode=DRIVEN_LOW, PinBehavior onMode=DRIVEN_HIGH)
{
pinOnBehavior[pin] = onMode;
pinOffBehavior[pin] = offMode;
}
void disablePin(int pin) { pinOnBehavior[pin] = UNUSED; pinOffBehavior[pin] = UNUSED; }

//?? Set all duty cycles to 0
void zero(void)
{
for (int i=0;i != PWM_NUM_PINS;i++)
duty[i] = 0;
}

uint8_t pinOffBehavior[PWM_NUM_PINS];
uint8_t pinOnBehavior[PWM_NUM_PINS];
int bresenham[PWM_NUM_PINS];
};

SoftPWM::SoftPWM()
{
for (int c=0;c != PWM_NUM_PINS;c++)
{
bresenham[c] = 0;
pinOffBehavior[c] = UNUSED;
pinOnBehavior[c] = UNUSED;
}


}

void SoftPWM::loop(void)
{
boolean lvl;
unsigned char regLvlOut=0;
unsigned char regLvlIn;

unsigned char regDirOut=0;
unsigned char regDirIn;

for (int i=0;i != PWM_NUM_PINS;i++)
{
if (i==0) { regLvlIn = PORTD; regDirIn = DDRD; }
if (i==8) { regLvlIn = PORTB; regDirIn = DDRB; }

if (i==8)
{
DDRD = regDirOut;
PORTD = regLvlOut;
}

regLvlOut >>=1;
regDirOut >>=1;

if ((pinOnBehavior[i] & UNUSED) == UNUSED) // If its unused then keep the values the same
{
regDirOut |= (regDirIn&1);
regLvlOut |= (regLvlIn&1);
}
else
{
bresenham[i] += duty[i];
if (bresenham[i]>=PWM_MAX_DUTY)
{
bresenham[i] -= PWM_MAX_DUTY;
lvl = true;
}
else lvl = false;

if (lvl)
{
regLvlOut |= (pinOnBehavior[i]&1) ? 0x80:0;
regDirOut |= (pinOnBehavior[i]>>1) ? 0x80:0;
}
else
{
regLvlOut |= (pinOffBehavior[i]&1) ? 0x80:0;
regDirOut |= (pinOffBehavior[i]>>1) ? 0x80:0;
}

}

regLvlIn >>=1;
regDirIn >>=1;
}

if (1)
{
DDRB = regDirOut;
PORTB = regLvlOut;
}
}

typedef unsigned char byte;

int ledPin = 13;

void testSoftPWM()
{
SoftPWM pwm;
pwm.enablePin(ledPin, SoftPWM::DRIVEN_LOW, SoftPWM::DRIVEN_HIGH);
for (int i=0;i<25600;i++)
{
pwm.duty[ledPin] = i/100;
pwm.loop();
delay(1);
}
}

void setup() {}
void loop() { testSoftPWM(); }