Jump to Navigation

Software Pulse Width Modulation (PWM) with an Arduino

Pulse Width Modulation (PWM) is a great way of controlling lights, motors and servos. By cunning use of PWM, it's possible to control the amount of power a device receives, or the voltage of a power supply, or the angle of the rotor of a servo. PWM is so useful that there's a great deal of support for it in various products. The Arduino, for example (which uses an Atmel atmega chip) has some pins that perform PWM, and make it incredibly easy to use with some simple software.

There are some excellent articles on the Arduino website about PWM. The basic tutorial is a start, but Secrets of PWM is a more complete reference. Of course, the documentation for analogWrite() is also useful. We won't try to replicate all that here, not least because those methods are the way that 99% of Arduino applications need and will probably use.

There is one other way to perform robust PWM: custom software. The reasons to do this are somewhat niche, because of all the other methods available. However, if for some reason those methods aren't suitable, then custom software might be a way forward.

As the 'bit banging' approach isn't very useful, we're going to use an interrupt and a counter. This is much like how Fast PWM works in hardware, although it's implemented purely in software. The method is pretty simple: Start with an output LOW. Every time there's an interrupt, increment a counter. When the counter reaches a trigger value, set the output HIGH. When the counter reaches another value, reset it to zero and set the output LOW again. This method allows us to configure the period and width of the pulse. Here's some code:

#include <avr/interrupt.h>
#include <avr/io.h>

// Which pin will we waggle?
// Just to show off, we'll pick a pin that doesn't do PWM in hardware ;-)
#define PWM_PIN 7

// TICK_SIZE is the time between interrupts. This is essentially the resolution
// of the PWM we're able to create. This is also an average value, because other
// interrupts will affect the frequency of ours.
#define PWM_TICK_SIZE 50 // uS

// PWM_PERIOD is the period of the PWM we want to have
#define PWM_PERIOD   20000 // uS (20ms)
// This calculation is used to determine the 'reset' number for the counter
#define PWM_TICK_PERIOD (int) (PWM_PERIOD / PWM_TICK_SIZE)

// Interrupt variables have to be defined as 'volatile' so that the CPU always
// fetches them from RAM and doesn't get an incorrect value from cache
volatile unsigned int timer2_counter = 0;
volatile unsigned int timer2_trigger_low = 150;

// Timer2 overflow interrupt vector handler
// This is called every time timer 2 overflows (around every 50ms)
// This routine needs to be as small and short as possible so that we don't
// impact the rest of the Arduino by executing lots of code in the background
ISR(TIMER2_OVF_vect) {
  timer2_counter++;
  if(timer2_counter == timer2_trigger_low) {
    digitalWrite(PWM_PIN, LOW);
  } else if(timer2_counter >= PWM_TICK_PERIOD) {
    timer2_counter = 0;
    digitalWrite(PWM_PIN, HIGH);
  }
  // Reset the timer2 counter so that it can count and overflow once again
  TCNT2 = 0;
}

// Setup what we need. We set the PWM pin as an output, and enable the
// interrupt on timer2's overflow (ie. call the function above)
void setup() {
  pinMode(PWM_PIN, OUTPUT);
  TIMSK2 = 1<<TOIE2;  // Timer 2 overflow interrupt enable
  TCNT2 = 0;
}

// Loop - we actually don't need to do anything here - the PWM will happen
// in the background. For demonstration purposes, we change timer2_trigger_low
// a bit so that it's possible to see the difference in PWM width
void loop() {
  timer2_trigger_low = 100;
  delay(2000);
  timer2_trigger_low = 200;
  delay(2000);
  timer2_trigger_low = 300;
  delay(2000);
  timer2_trigger_low = 400;
  delay(2000);
  timer2_trigger_low = 300;
  delay(5000);
  timer2_trigger_low = 200;
  delay(2000);
}

The #defines at the top of the code may need some explaining. The TICK_SIZE is actually defined by the time it takes for timer2 to overflow. It's possible to configure this counter and it's interrupts, although for simplicity we don't do this and just leave it at it's default. This gives us a 50uS resolution, which is quite a high resolution. For controlling something like a small servo, it's possible we don't need this fine resolution, so could reconfigure the timer to overflow more slowly.

Whatever the timer settings, it's possible to find the resolution by testing. Using delay(1000) and Serial.println(timer2_counter) it's easy to see how much the counter increments in one second, and so it's easy to calculate the TICK_SIZE from that.

PWM_PERIOD is also configurable. It's maximum size is defined by the time it takes for timer2_counter to overflow, which at 50uS TICK_SIZE is about 8 seconds. For most applications a PWM_PERIOD of a few milliseconds is all that's required, so it's possible that timer2_counter could be am unsigned char, rather than an unsigned int, if memory is tight.

Lastly, as it says in the code comments, Interrupt Service Routines (ISRs) need to be small. While the ISR is running, the loop() code is not, so having long running ISRs can affect the normal running of programs.

This software PWM method means we could extend PWM to every pin on the chip, although it may take some clever programming to make each pin do different things(!). In truth though, hardware PWM is almost always preferable to the software version, so unless there's some reason not to use hardware, the software alternative probably isn't advisable.

Update: This article talks in more detail about very similar concepts.



Main menu 2

Dr. Radut Consulting