Saturday, January 23, 2021

Binary Code Modulation (BCM) aka Bit Angle Modulation (BAM) library for AVR

Binary Code Modulation (BCM) was invented by Artistic License and is a form of PWM but not really. The main advantage over software PWM is the low CPU usage regardless of how many channels it controls.

This library provides a fast implementation of Binary Code Modulation useful for controlling RGB leds and dimming multiple leds for creating animations like led cubes and includes an array for logarithmic brightness. A complete cycle takes 8 timer interrupts and each interrupt takes only 4us on a 8MHz CPU.

Bit Code Modulation (BCM) aka Bit Angle Modulation (BAM) library for RGB led dimming - 8-bit

Contents


How Binary Code Modulation (BCM) works and how it differs from PWM

To dim a led with PMW is simple. If you want the led to be half as bright you turn the led on for 50% of the cycle and 50% for the other half. Or 20% on and 80% off for an even dimmer led.

 

PWM example

Bit Angle Modulation uses the weight of each bit in a binary number. For example in one byte there are 8 bits with numbers from 0 to 7. Bit 0 is called the Least Significant Bit (LSB) and its weight is 1. Next bit 1 has a weight of 2, bit 2 has a weight of 4, then 8, 16, 32, 64 and 128. Bit 7 is called the Most Significant Bit (MSB) because it has the highest weight - 128.

8-bit binary weight

With 8-bit BCM resolution there are 256 levels of brightness because 2^8 = 256 from 0 to 255. Number "0" means 0% duty cycle, 255 represents 100% duty cycle and 128 is 50% duty cycle (256 * 0.5).

To convert from a duty cycle percentage to a BCM number multiply 256 with the percentage. E.g:

30% duty cycle: 256 * 0.3 = 77

To convert from a BCM number to a percentage divide the number by 256. E.g:

77 / 256 = 0.3 (30%) duty cycle

Example of dimming a led with the Binary Code Modulation method with 30% duty cycle

77 in binary is 0b01001101 and represents 30% duty cycle with the BCM method. The microcontroller is clocked at 8MHz and a timer is set in CTC mode with 256 prescaler. The led refresh rate will be 122Hz and the timer interrupt will trigger 8 times in one cycle, 1 time for each binary bit. One cycle is 1 / 122Hz = 8.2ms. The interrupt takes only 4us to run the led dimming code regardless of how many leds there are. Hope you can already see the benefits of BCM and how less CPU time it takes to control 8, 16 or even 32 leds. I will be talking about how to calculate all this in the "how to use the library" section.

Example of 77 in binary

One cycle is made of 256 ticks. 

Bit 0 has a 1 and the weight is 1 so the led will be on for 1 tick

Bit 1 has a 0 and the weight is 2 - the led will be off for 2 ticks

Bit 2 has a 1 and the weight is 4 - the led will be on for 4 ticks

Bit 3 has a 1 and the weight is 8 - the led will be on for 8 ticks

Bit 4 has a 0 and the weight is 16 - the led will be off for 16 ticks

Bit 5 has a 0 and the weight is 32 - the led will be off for 32 ticks

Bit 6 has a 1 and the weight is 64 - the led will be on for 64 ticks

Bit 7 has a 0 and the weight is 128 - the led will be off for 128 ticks

Adding ticks: 1 + 4 + 8 + 64 = 77. Here is a snapshot taken from PulseView with how the BCM pulses look like for number 77 or 30% duty cycle.

Binary Code Modulation pulses in PulseView

On channel 1 is the dimmed led and on channel 2 is the start and end of the timer interrupt for each of the 8 bits. Zooming in the interrupt was measured and it takes around 4us. Notice how the on/off time increases with each bit.

BCM applications

BCM/BAM differs fundamental from Standard Edge-Aligned PWM. Because BCM turns pins ON and OFF at irregular binary interval it behaves poorly with certain inductive/capacitive loads.

Works well with: 

  • Multiplexed LED Displays & Matrices: Excellent application; BCM dramatically lowers CPU overhead compared to standard software PWM when driving multi-channel LED drivers.
  • Heating Elements (Resistive loads): Thermal mass has high inertia, so thermal systems process high frequency BCM pulses without issue.

Does not work well with:

  • DC Motors / Inductive Loads: BCM's non-uniform switching produces aggressive current spikes due to back-EMF. Inductive coils suffer high acoustic noise (audible whining) due to the fractional harmonics of the bit-duration timer. Standard fixed-frequency PWM is mandatory for smooth motor torque.
  • Piezo Transducers / Buzzers: Piezo elements require consistent fundamental frequencies to output clear acoustic tones. BCM creates hash noise across broadband frequencies instead of pure tones.
  • Power Converters (Buck/Boost): Switch-mode supplies rely on predictable ripple currents defined by fixed frequency continuous conduction. BCM will distort inductor flux and ruin voltage regulation.
 

Dimming leds using logarithmic brightness

If you ever increased the brightness of a led using PWM or BCM you might have noticed that at first the led brightness increases rapidly and then the light remains bright for a longer time without changing much in brightness. This is due to the way the eyes perceive the differences in light intensities. At lower light levels we can distinguish between small differences in light intensities but as the light becomes brighter we distinguish less and less between changes in intensity. The eyes and also hearing, work in a logarithmic fashion.

For this reason I have included with this library an array for each bit mode that includes logarithmic levels for leds. It should also work with PWM not only with BCM. The generated array values are based on the CIE 1931 formula which models standard human visual perception of luminance.

CIE 1931 formula

Where L* is perceived lightness and Y is relative luminance output.

Linear vs logarithmic brightness
The above formula generates the red logarithmic curve. The led brightness increases slower then ramps up faster but for the eyes the increase in brightness in linear. The spreadsheet used to generate the graph can be downloaded below.

The blinking problem of the Bit Angle Modulation

There is no issue by using random duty cycles but during fade-in and fade-out there will be a blink during the transition between 128 to 127 or 127 to 128.

 

The change in pulse positions creates an ON period equal to 255, just for 1 period. Then it's fine again. Going from 127 to 128 it's a bright blink. From 128 to 127 it's a dim blink, because they line up the other way (combined period = 0 dutycycle).

To prevent this glitch showing when driving a led, choose a lower prescaler to increase the BCM frequency which will lower the glitch time.

API

Library configuration

Which timer to use: timer 0, 1, 2 for class 0 devices and TCA0, TCB0 for class 1 UPDI devices.

#define BCM_TIMER               BCM_TIMER2

Selecting the prescaler for the timer

#define BCM_PRESCALER           128

The lower the prescaler the faster the timer interrupt and thus the higher refresh rate for the leds. A higher refresh rate prevents led flickering. The CPU frequency must be high enough especially if you have other interrupts. Check the following spreadsheet and select the prescaler depending on your CPU frequency. The ISR BIT 0 TRIGGER TIME is the lowest time the interrupt triggers for bit 0. So the ISR must finish the code faster than this time. You can download the spreadsheet down below and try other prescalers.

Prescaler vs CPU frequency for Binary Code Modulation (BCM)

The formula for calculating the leds refresh rate for Binary Code Modulation is 

F_CPU / prescaler / 256

Pin and ports

#define BCM_PORTA_MASK          (_BV(1) | _BV(2) | _BV(3))
#define BCM_PORTB_MASK          0
#define BCM_PORTC_MASK          0
#define BCM_PORTD_MASK          0
#define BCM_PORTE_MASK          0

Defines on what pins the BCM channels are located. Port A is an example where pins 1, 2 and 3 are used. This is a bitmask used by the library to mask out the pins that are not used. For efficiency use pins on the same port.

How many bits to use

#define NUMBER_OF_BITS		BITS_8

The more bits the higher the resolution because the number of steps increases but also the time for one cycle increases and if the prescaler is not low enough you will create a nice stroboscope. Lowering the prescaler will make the ISR trigger faster and this also can lead to flickering because there is less time for the CPU. Use the spreadsheet below to calculate a good ratio between F_CPU, prescaler and number of bits. Other interrupts could introduce flickering as well if they take too long to complete.

Maximum number of channels 

#define MAX_CHANNELS            CH_LIMIT_8

Use logarithmic array

#define USE_LOGARITHMIC_ARRAY   0

Set this to 1 if you want to use the logarithmic gamma correction array. The array is stored in flash memory using the PROGMEM attribute so it won't occupy the RAM. There are 2 arrays and only one will be included depending on the number of bits specified (8 or 10).

Initializing the library

void BCM_init(void)

Sets up the selected timer in CTC mode and enables global interrupts.

Stopping the interrupt

void BCM_stop(void)

Disables the timer interrupt.

Encoding the Binary Code Modulation duty cycle

void BCM_encode(const bcm_duty_t duty_cycle[], char port_letter)

Uses a global array for each port. The interrupt uses this encoding to control the pins.

duty_cycle

The duty cycle for each channel. Values should be in the same order as the pins in macros such as BCM_PORTA_MASK.

port_letter

Channels port. Must be a char like: 'B', 'C', 'D'.

Converting linear duty cycle to logarithmic duty cycle

uint16_t BCM_linearToLog(uint16_t duty_cycle)

To convert the duty cycle from linear to logarithmic using the array, pass the linear duty cycle to this function and use the returned value for the BCM_encode().

Frame end flag

The BCM_CYCLE_END is a flag that indicates to the main loop that a new encoding can be made with a new value. Because the main loop is faster than the ISR we don't want the encode function to run pointless multiple times without the BCM completing at least 1 frame. The main loop must clear this flag.

Example of breathing led effect using Bit Angle Modulation (BAM):

#include <util/delay.h>
#include "bcm.h"

int main(void) {
    bcm_duty_t led[1] = {0};
    int8_t dir = 1;

    BCM_init();

    while (1) {
        if (BCM_CYCLE_END) {
            BCM_CYCLE_END = false;

            // Breathing LED
            led[0] += dir;

            if (led[0] == 255) {
                dir = -1;
            } else if (led[0] == 0) {
                dir = 1;
            }

            // Send array values to BCM encoder for PORTC
            BCM_encode(led, 'C');
        }
    }

    return 0;
}
 

Links

v2.0

bcm.h

bcm.c
Other links

Libreoffice spreadsheet for prescaler selection based on CPU frequency

Libreoffice spreadsheet for logarithmic brightness
Changelog
v2.0 09-08-2026:
- Simplified API.
- Changed logarithmic array to use CIE 1931 formula.
- Added support for UPDI devices.

No comments:

Post a Comment