Efficient software PWM with bit-angle modulation

It is a few months now since I last had a play with pulse-width modulation (PWM). At the time I was concerned by the amount of processing power used by the simplistic approach I had implemented, so I abandoned the exercise.

Luckily, there are more efficient ways to do software PWM. One of the most popular is "bit-angle modulation" (BAM), also known as "binary code modulation" (BCM). The idea is deceptively simple, and is based on the premise that since the whole point of PWM is to switch an output on for a certain proportion of time, and off for a certain proportion of time, the system does not really need to check every single clock cycle just to see if any of the outputs should change. As long as the end result has the right proportions, the specific times at which the output is switched on and off are immaterial.

Following this thinking, we can get to a point where we can group some proportions together, allowing the system to sleep for longer periods, and thus use less processing power. For example, consider a system with 256 levels of PWM range. This can be represented as eight bits of data. We would expect value 255 (0xFF) to be fully on, and 0 to be fully off. We would also expect 128 (0x80) to be on for half the time, 64 (0x40) to be on for a quarter of the time and so on.

Any output with a PWM value of 128 or greater will be on for at least half the time, and any output with a value of less than 128 will be off for at least half the time. All outputs will be one or the other, which implies that by grouping outputs on this bit of the data we can allow the system to set the appropriate outputs on or off, then sleep for a whole half cycle! The process works similarly for the other bits of the PWM level.

To demonstrate this in action, I have written some C code to pretend that it is driving a PWM output but write the output to the console instead.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#define BITS 4
#define STEP 1000

#define N_CHANNELS 16
#define N_CYCLES 5

extern int usleep(int usecs);

int levels[N_CHANNELS];
int nchannels = 0;
int state[N_CHANNELS];
char history[N_CHANNELS][(1 << BITS) * N_CYCLES + 1];
int hi;

int setup(int argc, char**argv, int* channels) {
  int ret = 0;
  for (ret = 0; ret < argc-1; ++ret) {
    channels[ret] = atoi(argv[ret+1]);
  }

  printf("running %d cycles of %d channels\n", N_CYCLES, ret);
  return ret;
}

void switch_on(int channel) {
  state[channel] = 1;
}

void switch_off(int channel) {
  state[channel] = 0;
}

void wait(int ticks) {
  int i;
  int channel;
  for (i = 0; i < ticks; ++i) {
    for (channel = 0; channel < nchannels; ++channel) {
      history[channel][hi] = state[channel] ? '^' : '_';
    }
    usleep(STEP);
    ++hi;
  }
}

void emit() {
  int bit;
  int power;
  int channel;

  for (bit = 0; bit < BITS; ++bit) {
    power = 1 << bit;
    for (channel = 0; channel < nchannels; ++channel) {
      if ((levels[channel] & power) > 0) {
        switch_on(channel);
      } else {
        switch_off(channel);
      }
    }
    wait(power);
  }
}

void describe() {
  int channel;
  for (channel = 0; channel < nchannels; ++channel) {
    history[channel][hi] = 0;
    puts(history[channel]);
  }
  putchar('\n');
}

int main(int argc, char**argv) {
  int cycle;

  nchannels = setup(argc, argv, levels);

  hi = 0;
  for (cycle = 0; cycle < N_CYCLES; ++cycle) {
    emit();
  }
  describe();
}

This code, together with a makefile so you can build it, can be found at the Raspberry Alpha Omega "pwm" project on github.

The example code runs five example cycles of as many PWM pins as you like. However, for simplicity they are limited to levels in the range 0-15. When you run the demo just give a sequence of numbers between 0 and 15, and it will show a graph of the output levels in glorious text-o-vision. For example, entering ./console-demo 0 1 2 3 4 5 6 7 8 9 10 11 12 13 13 15 gives the following output:

___________________________________________________________________________
^______________^______________^______________^______________^______________
_^^_____________^^_____________^^_____________^^_____________^^____________
^^^____________^^^____________^^^____________^^^____________^^^____________
___^^^^___________^^^^___________^^^^___________^^^^___________^^^^________
^__^^^^________^__^^^^________^__^^^^________^__^^^^________^__^^^^________
_^^^^^^_________^^^^^^_________^^^^^^_________^^^^^^_________^^^^^^________
^^^^^^^________^^^^^^^________^^^^^^^________^^^^^^^________^^^^^^^________
_______^^^^^^^^_______^^^^^^^^_______^^^^^^^^_______^^^^^^^^_______^^^^^^^^
^______^^^^^^^^^______^^^^^^^^^______^^^^^^^^^______^^^^^^^^^______^^^^^^^^
_^^____^^^^^^^^_^^____^^^^^^^^_^^____^^^^^^^^_^^____^^^^^^^^_^^____^^^^^^^^
^^^____^^^^^^^^^^^____^^^^^^^^^^^____^^^^^^^^^^^____^^^^^^^^^^^____^^^^^^^^
___^^^^^^^^^^^^___^^^^^^^^^^^^___^^^^^^^^^^^^___^^^^^^^^^^^^___^^^^^^^^^^^^
^__^^^^^^^^^^^^^__^^^^^^^^^^^^^__^^^^^^^^^^^^^__^^^^^^^^^^^^^__^^^^^^^^^^^^
^__^^^^^^^^^^^^^__^^^^^^^^^^^^^__^^^^^^^^^^^^^__^^^^^^^^^^^^^__^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

(The ^ characters show when the signal is "on".)

As I hope you can see, the number of ^ characters per cycle does indeed add up to the specified PWM level. What may seem odd is that they are not all adjacent, as was the case for the "obvious but inefficient" approach. If all we are concerned about is the proportion of time spent on or off per cycle, though, this is unimportant!

Next time I hope to connect this code to some actual GPIO pins and drive some real PWM.

One Comment

  1. Pingback: Using bit-angle PWM to drive GPIO pins | Raspberry Alpha Omega

Leave a Reply

Your email address will not be published. Required fields are marked *