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(); }

18 comments:

  1. This article is enlightening, thanks for posting. I had trouble getting it compiled on Arduino 0018 (regLvlOut undeclared in this scope), but I added:
    "typedef unsigned char uchar_t;" to the top of the loop function and all was well.

    ReplyDelete
  2. how do you change the frequency(say to 30Hz) at which the pwm is running?

    ReplyDelete
  3. You call "loop()" 30 times a second to get 30Hz. So you would write something like:
    while(1)
    {
    pwm.loop();
    delay(1000/30);
    }

    Note that if you don't want to be calling loop() all the time, its easy enough to hook it into the AVR's timer interrupt. I can point you to an example if you need one.

    ReplyDelete
  4. what if you wanted 75kHz for example?
    I don't think that delay(1000/75000) = delay(1/75) would work?

    ReplyDelete
  5. Take a look at the "delayMicroseconds()" function in the Arduino library. Also, you could always use one of the CPUs timers and have the handler call pwm.loop(). Doing it this way uses up one of the timers though, which means you can't use some of the hardware PWM.

    But I'm not sure if this code will go that fast. You'll have to check. Take a look at the micros() function. It returns an unsigned long int that is the # of microseconds since CPU power up. Be aware that it wraps once an hour or so...

    ReplyDelete
  6. This looks super useful, but for some reason when I copy/pasted the code into a header file, the compiler says it has issues with the constructor declaration (which subsequently breaks everything else in the code). Any ideas on what I'm doing wrong? Could you post it as a zipped file to make sure I'm getting the code right? I'm using 0018...

    ReplyDelete
  7. Sure, I just recompiled it successfully in the IDE, zipped it up and added it as a link...

    ReplyDelete
  8. .pde file compiled fine, but the software doesn't like it as an include. I modified the code a little, then moved it around as needed to create an actual library. Download at http://www.zshare.net/download/76792810f1ef8083/ . Works like a charm! Thanks so much!

    ReplyDelete
  9. .pde file compiled fine, but the software doesn't like it as an include. I modified the code a little, then moved it around as needed to create an actual library. Download at http://www.zshare.net/download/76792810f1ef8083/ . Works like a charm! Thanks so much!

    ReplyDelete
  10. Yes, I threw in the setup() and loop()
    functions so it works as a sketch, not a library for you (you can't have both simultaneously). But if you remove those fns, and then move all the implementations to a .c file and the class def to a .h then you can "libraryify" it!

    ReplyDelete
  11. i would like to know how do i use pwm to control the speed of a dc motor? Is there any eg that i can refer to?

    ReplyDelete
  12. There's nothing to it! DC motors change speed based on the voltage applied to the motor. PWM techniques flip the voltage between 0 and 5v so fast that it "looks" like a stable voltage to a "slow" device like a motor (remember a motor is an inductor, which in fact is commonly used to smooth out voltage spikes). The "perceived" voltage is somewhere between 0 and 5v, and its exact value depends on the ratio of "off" (0volts) vs "on" (5v) time.

    So you can just hook the output to the motor, IF your motor does not draw much current (Arduino limit is 30mA I think). If you motor draws more current than the Arduino IO can handle, you need to use a transistor to amplify the allowable current.

    And if you are attaching a "naked" DC motor -- not from a toy -- do not forget to use a diode across the motor leads. It must be installed backwards, so current can flow thru the diode from the - lead BACK to the + lead.

    Do you understand these issues or should I do a tutorial?

    ReplyDelete
  13. I will try one more time.
    I need a sketch to control the speed of a 3phase ac motor using PWM and a simple pot.

    This is for an electric car using standard 3phase motor.
    I can work out the interfacing but not the sketch..... HELP.
    Thanks John

    ReplyDelete
  14. Can you ask specific questions? Do you have any programming experience?

    ReplyDelete
  15. Is there some way you could use this code/library to add an extra PWM pin to an attiny85 which is being programmed through an arduino via the ISP method? Regardless, great work for the arduino and making it possible to have extra PWM pins! :)

    ReplyDelete
  16. Yes, this code does not really use the Arduino "Wired" libraries so you should be ok there (just remove the loop() and setup() on the bottom). But the main thing is that it uses the mega 168 registers, PORTD PORTB, DDRD and DDRB. These registers control the state and type of the pins. So you'll have to fine the attiny85 equivalents.

    ReplyDelete
  17. A modified Sketch that actually works with HS 422 servo, Gertboard and Raspberry Pi:
    http://www.linuxcircle.com/?p=640

    ReplyDelete