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.
Contents
- How Binary Code Modulation (BCM) works
- BCM applications
- Dimming leds using logarithmic brightness
- The blinking problem of the Bit Angle Modulation
- API
- Library configuration
- Initializing the library
- Stopping the interrupt
- Encoding the Binary Code Modulation duty cycle
- Converting linear duty cycle to logarithmic duty cycle
- Frame end flag
- Links
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.
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.
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.
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.
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.
Where L* is perceived lightness and Y is relative luminance output. 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.
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_8The 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