Saturday, February 26, 2011

PWM on the AVR ATTiny2313

Pulse width modulation (PWM) can be used for a wide variety of things: controlling motor speed, synthesizing sounds, dimming LEDs by varying amounts, even amplifying signals.  In a nutshell, you take a train of pulses and vary the duty cycle (percentage of time on vs off) to control the output of your target device, which could be a spinning rotor or a speaker cone.   Usually some filter effect comes into play, whether it be a tuned L-C circuit placed in front of the speaker or the rotational inertia of a motor.  The great thing about PWM is that it is usually very power-efficient.

Lately, I've been playing with the PWM channels on the ATTiny2313 AVR microcontroller.  PWM on the AVR looks pretty daunting when you first start looking at the data sheets, but it's actually pretty straightforward.  The best thing about the PWM channels on the AVR is that they are implemented completely in hardware, meaning that the PWM doesn't need any software to run, not even an interrupt service routine.  All you have to do is set a few registers and it's generating a signal!  It can get more complicated than that if you want take advantage of the number of options available, butyou can keep things pretty simple if you want.

PWM is based on the AVR hardware timer.  The timer is a counter that is automatically incremented by the microprocessor clock through a configurable divider.  The timer can be configured to send an interrupt request every time it overflows, making it a way to run code on a periodic basis.  But besides that, the PWM hardware also allows the timer to send an output signal via one of the AVR's output pins.  So if we can figure out how to reset that output after some amount of time, the AVR can send a pulse on a periodic basis.  This is where PWM comes in...  We can configure the AVR to reset the signal at value of the timer before it overflows.   This value will determine the duty cycle of our signal.

For example, if our timer uses an 8-bit coutner, it will continuously count from 0 to 255.  Every time it hits 255 it will reset to 0 and start over, sending an interrupt request if we choose.  At that point it can also raise an output to a logic high state.  We can also set a PWM register so that when the counter reaches some intermediate number, lets say 128, it resets the output to a logic low.   So as the timer continuously counts, the output will toggle up and down producing a square wave.  In this example square wave will have a 50% duty cylcle (128 is 50% of 256)...  If we set the PWM control register to 64, the duty cycle will be 25%.

Those are the basics of how PWM works on the AVR microcontroller...  There are a couple of really good tutorials at the AVRFreaks web site that cover PWM in depth; here's one for example.  (You'll need to get a free account to get to a lot of the good information on the AVRFreaks web site.)

For a demo, I set up an ATTiny2313 with its OC0A output driving a N-channel MOSFET to switch a load off and on.  OC0A is a PWM output based on the 8-bit timer 0 of the ATTiny2313.  In this case, I'm driving a 30 mA meter movement I bought at a hamfest last fall.  In series with the meter movement and MOSFET is a 1 kΩ  trimmer pot that I set so that the meter reads full scale when the OC0A output has a 100% duty cycle (constant logic high.)  The meter will read a value proportional to the duty cycle of the OC0A PWM output.  So when the OCRA0 register is set to 128, the meter will read 15 mA.


To make the demo interesting, I used Timer 0 to update the PWM duty cycle every second.   The default clock configuration on the ATTiny2313 causes timer 0 overflow interrupts 3906 times a second - way more often than I want to update the PWM duty cycle.  To slow things down, I use the timer overflow interrupt service routine to increment a clock scaling counter and watch that counter in the main loop of the program.  When that counter hits 3906, I update the PWM duty cycle and clear the counter.  As for the actual duty cycles I display on the meter, I start at 0 and increment the duty cycle 1.6% each time.  After 60 seconds, the duty cycle reaches 100%, the meter reaches full scale, and  I set the duty cycle back to zero.   My sample code is over here in GitHub.  Below is a video of the demo setup.