When I first built the Skully prototype I didn’t really know what I was doing. I didn’t even put pull-up resistors on the I2C bus, being unsure what they were for. Somehow it still worked.

I drove the LEDs with 18 concurrent software PWM channels, one for each LED.

It looked fantastic in person but I couldn’t help but notice how difficult it was to take a photograph or video of it that looked decent. I think I ended up with a poor framerate due to the large number of PWM duty cycles I needed to maintain.

I eventually learned about something called Binary Angle Modulation, also called Binary Code Modulation. It sounded like a better way but I found it difficult to understand, references to it were sparse and a bit hard to follow, and I put off putting in the work to fully understand it, but I’m glad I invested the time in figuring it out.

The Problem We’re Trying to Solve

On a basic level, I want to programmatically control the individual brightness level of an array of LEDs.

The way this is typically done is with something called PWM (Pulse Width Modulation): Essentially you turn an LED on and off at a frequency much faster than the human eye can perceive. If the LED is flipped on and off 50% of the time your eye will see an “average” brightness of 50% (*).

There are a lot of reasons why this method is used which I explored in another post.

(*In yet another post I explain why the brightness curve is not linear, but that’s another story.)

Software PWM’s limitations

Before diving into BAM, let’s understand why traditional software PWM becomes problematic as you scale up the number of LEDs:

// Traditional software PWM - must be called frequently in your main loop
void updateLeds() {
  static uint8_t pwmCounter = 0;

  // For each LED, check if it should be on at this PWM step
  for (uint8_t i = 0; i < NUM_LEDS; i++) {
    if (ledBrightness[i] > pwmCounter) {
      digitalWrite(ledPins[i], HIGH);
    } else {
      digitalWrite(ledPins[i], LOW);
    }
  }

  // Increment counter and wrap around at 255
  pwmCounter = (pwmCounter + 1) % 256;
}

This approach has several critical drawbacks:

  1. Per-LED Processing: Each LED requires its own if statement and digitalWrite() call
  2. Linear Time Complexity: The processing time scales linearly with the number of LEDs (O(n))
  3. Inefficient Pin Manipulation: In this example I’ve used the digitalWrite() helper function which is easier to understand but not designed for maximum efficiency, involving multiple operations per call
  4. Constant Attention: The main loop must call this function frequently for smooth dimming

These problems grow worse the more LEDs you add.

Hardware PWM

It’s less of a problem if you’re using hardware PWM where the microcontroller ties a hardware timer to the output state of a pin without using the main clock, but most microcontrollers only support hardware PWM output on only a subset of their pins, and it of course uses up timers that you could use for something else.

The Tradeoff between Driving Frames and Computing Frames

Assuming you’re using a simple microcontroller with only a single core, it has to balance two competing tasks:

  1. Calculating the next frame (and the rest of your application logic)
  2. Actually switch LEDs on and off

Every clock cycle spent on one task is one that can’t be used for the other. As your LED count grows, traditional software PWM quickly consumes more of your processing power just for brightness control.

How does BAM help?

Part 1: Binary-coded Time Slices

BAM takes a fundamentally different approach to the animation cycle than PWM. Instead of dividing time into equal slices, it uses binary-weighted time slices corresponding to each bit in the brightness value.

Here’s how it works:

  1. For an 8-bit brightness value, you create 8 time slices with durations proportional to 2^0, 2^1, 2^2, etc.
  2. For each bit position in the brightness value, if that bit is set to 1, the LED is on during the corresponding time slice
  3. The perceived brightness is the sum of all activated time slices

This means that instead of checking 256 different brightness levels for each LED, you only need to check 8 bit positions!

Example

Let’s look at an example. Say we have an LED that we want to set to a brightness level of 173 (binary: 10101101).

With PWM, we’d keep the LED on for 173 time units out of 256 total time units. We count up to 255, toggle the LED at 173, and let the variable overflow to zero and toggle it again.

With BAM, we look at each bit position:

  • Bit 0 (least significant) is 1, so LED is ON for 2^0 = 1 time unit
  • Bit 1 is 0, so LED is OFF for 2^1 = 2 time units
  • Bit 2 is 1, so LED is ON for 2^2 = 4 time units
  • Bit 3 is 1, so LED is ON for 2^3 = 8 time units
  • Bit 4 is 0, so LED is OFF for 2^4 = 16 time units
  • Bit 5 is 1, so LED is ON for 2^5 = 32 time units
  • Bit 6 is 0, so LED is OFF for 2^6 = 64 time units
  • Bit 7 (most significant) is 1, so LED is ON for 2^7 = 128 time units

The total ON time is still 173 units (1+4+8+32+128), matching our desired brightness.

So the LED is still on the same amount of time the same amount of time it would be in PWM.

Part 2: Parallel writes

As an example of why slicing time into binary-weighted slices is advantageous, let’s look at direct port manipulation.

What is Direct Port Manipulation?

Microcontrollers typically define their GPIO pins into groups called “ports” (PORTB, PORTC, etc.). Typically each port controls up to 8 pins, represented by a single byte (the “register”), with each bit in the byte corresponding to the state of one pin. So you can set all pins on a port simultaneously by writing a single byte value to the port register:

// Instead of these 8 slow digitalWrite() calls:
digitalWrite(8, HIGH);  // Pin "8" is bit 0 of PORTB
digitalWrite(9, LOW);   // Pin "9" is bit 1 of PORTB
digitalWrite(10, HIGH); // And so on...

// You can set all PORTB pins at once with a single fast operation:
PORTB = 0b00010101;  // Sets port pins 0, 2, 4 HIGH and the rest LOW

This is dramatically faster than calling digitalWrite() for each pin.

With direct port manipulation, we can update all LEDs at once for a given bit position, by writing to the entire port register at once.

Combining Both Parts

Combining both parts of BAM (updating whole ports at once, encoding brightness as binary time slices) we can drive LEDs with the fewest operations possible.

With BAM, we can update all LEDs at once for a given bit position:

# Pseudocode
function UpdateLEDsForCurrentBit(bitPosition):
    # Get the ON/OFF state for all LEDs at this bit position
    ledStates = ledStatesForBitPosition[bitPosition]

    # Update all LEDs on this port in a single operation
    LEDPort = ledStates

    # Move to next bit position (cycle through 0-7)
    bitPosition = (bitPosition + 1) % 8

    # Set timer for next update (doubling duration each time)
    nextTimerDuration = 2**bitPosition

With this approach, you can switch LEDs with just a single instruction per port, with an independent brightness per LED, regardless of how many LEDs are connected to each port.

Interactive Demo

PWM

This demonstration shows how PWM works. To adjust the perceived brightness, within the frequency we simply move the point where we toggle the LED. You can see why it’s called Pulse Width Modulation.

BAM

In contrast, this demo shows how BAM works for a single LED.

You can adjust the brightness values and see how the binary bit patterns translate to the resulting brightness through the binary-weighted time slices.

LED Driving Scenarios Where BAM is Ideal

Scenario 1: Direct LED Control from Microcontroller Pins

When you’re driving LEDs directly from microcontroller pins, BAM allows you to control more LEDs with less overhead than software PWM.

The key advantage here is that adding more LEDs to an already-used port requires zero additional processing time to drive it. Each port update is a single instruction, whether you’re controlling 1 LED or 8 LEDs on that port.

Scenario 2: Driving LEDs with Shift Registers

When using simple shift registers like the 74HC595 to drive LEDs, with PWM you’d need to send complete updates to your shift registers 256 times per PWM cycle. With BAM, you only need 8 updates.

void updateShiftRegisters(uint8_t bitPosition) {
  // Prepare data for this bit position across all LEDs
  for (uint8_t reg = 0; reg < NUM_SHIFT_REGISTERS; reg++) {
    shiftData[reg] = 0;
    for (uint8_t led = 0; led < 8; led++) {
      if (ledBrightness[reg * 8 + led] & (1 << bitPosition)) {
        bitSet(shiftData[reg], led);
      }
    }
  }

  // Send data to shift registers (just once per bit position)
  sendToShiftRegisters(shiftData, NUM_SHIFT_REGISTERS);
}

This approach drastically reduces the communication overhead when driving LED arrays through external driver chips that don’t support PWM themselves.

Side Benefits

If you are driving a bunch of LEDs with PWM, assuming you’re using the same frequency, their on cycles line up at the beginning of the period, so initially all of the LEDs will be on at the same time. Under BAM the on pulses are distributed throughout the cycle.

This has a number of side benefits.

  • Camera shutter interference: BAM’s non-uniform timing pattern is less likely to synchronize with camera frame rates, possibly resulting in better-looking photos and videos.
  • More consistent power draw: If all of your LEDs are pulsed on all at once, it briefly puts more load on the power supply and can cause voltage drops.
  • EMI: The varying pulse lengths create less concentrated electromagnetic noise.

Implementation Details

To implement BAM in software/firmware, you’ll need a few components:

  1. Timer Interrupt: Use a hardware timer to trigger updates at precise intervals
  2. Pre-computed States: Calculate and store the LED on/off states for each bit position
  3. Parallel Writes: Update the on status all of the LEDs at once
    • If driving LEDs off of GPIO pins, use direct port manipulation to rewrite whole port registers at a time
    • If using external driver chips this merely means we need to write less often using a variable timing cycle

Code Example

Here’s a simplified implementation sketch using direct port manipulation:

// bitSlicePatterns stores the LED state for each port at each bit position
// It's a 2D array: first dimension is the port (0=PORTB, 1=PORTC, etc.)
// second dimension is the bit position (0-7)
// Each value represents which LEDs should be ON for that bit position
uint8_t bitSlicePatterns[3][8] = {0};  // For PORTB, PORTC, PORTD

// Pre-compute bit patterns for each bit position
void calculateBitPatterns() {
  // For each LED in our system
  for (uint8_t led = 0; led < NUM_LEDS; led++) {
    uint8_t port = ledPorts[led];  // Which port this LED is on (0=PORTB, etc.)
    uint8_t pin = ledPins[led];    // Which pin on that port

    // For each bit in the brightness value (0-7)
    for (uint8_t bit = 0; bit < 8; bit++) {
      // Check if this bit is set in the brightness value
      if (ledBrightness[led] & (1 << bit)) {
        // If this bit is set, mark this LED as ON for this bit position
        // by setting the corresponding bit in our pattern
        bitSlicePatterns[port][bit] |= (1 << pin);
      }
    }
  }
}

// Timer interrupt service routine - called automatically by the timer hardware
ISR(TIMER2_COMPA_vect) {
  static uint8_t bitpos = 0;  // Current bit position (0-7)

  // Update all ports at once using our pre-computed patterns
  // The & with port masks ensures we only affect pins that are used for LEDs
  PORTB = (PORTB & ~PORTB_MASK) | (bitSlicePatterns[0][bitpos] & PORTB_MASK);
  PORTC = (PORTC & ~PORTC_MASK) | (bitSlicePatterns[1][bitpos] & PORTC_MASK);
  PORTD = (PORTD & ~PORTD_MASK) | (bitSlicePatterns[2][bitpos] & PORTD_MASK);

  // Move to next bit position, wrapping around from 7 back to 0
  bitpos = (bitpos + 1) & 0x07;

  // Here we have the timer function modify its own period
  // Set timer for next bit slice (doubling duration each time)
  // OCR2A is the Output Compare Register that controls when the next interrupt happens
  OCR2A = 1 << bitpos;  // Same as 2^bitpos

  // When we wrap back to bit position 0, reset the timer to its minimum value
  if (bitpos == 0) {
    OCR2A = 1;
  }
}

The trick is the timer interrupt service routine actually modifies its own period with each execution, doubling it each time and then resetting once it reaches the max. This creates the binary-weighted time slices we need for BAM, with each successive bit position getting twice the duration of the previous one.

I wrote a post with a more complete example.

What does the Name Mean?

  • Binary Code Modulation: This refers to encoding the on/off cycle as bits rather than a quantity.
  • Bit Angle Modulation: This terminlogy comes from viewing the timing sequence in a circular representation. If you imagine the full dimming cycle as a 360° circle:
    • The most significant bit (MSB) might control 180° of the circle (half the cycle)
    • The second bit controls 90°
    • The third bit controls 45°
    • etc.

I’ve seen some references say that BCM clusters all of the on periods at the beginning whereas BAM spreads them out across the cycle, but that doesn’t make much sense to me, because in that case BCM wouldn’t be any different than PWM.

I prefer the name Bit Angle Modulation because it sounds cooler.

Conclusion

Bit Angle Modulation is significantly more scalable than PWM as the number of LEDs grows; it allows us to switch LEDs using the fewest operations possible, though it takes a little bit more time to understand how it works. It not only looks and scales better but it gives me more clock cycles to compute more complicated animations.