Saturday, January 23, 2021

Binary Code Modulation (BCM) aka Bit Angle Modulation (BAM) library for fading leds | ATmega328P

Binary Code Modulation (BCM) it's an amazing method for led dimming and was invented by Artistic Licence. It's like PWM but not really. The main advantage over PWM is the low CPU usage regardless of how many leds 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. The leds can be on different ports.

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

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 it's 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/of time increases with each bit.

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 algorithm used to generate the array can be used instead but it takes around 600us to calculate each value and also it need the math library which increases the code size much more than having an array. With the array method it takes 8us.

Searching online for a formula that will produce a nice logarithmic curve reveals many answers. The one that gave the desired result can be found here https://diarmuid.ie/blog/pwm-exponential-led-fading-on-arduino-or-other-platforms.

2^(LinearInputValue / R ) - 1

R = ((256 - 1) * Log10(2)) / Log10(256)

256 is the number of steps for 8-bit (2^8).

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. But that 1 period, is extremely visible. 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).

I tried finding a solution but haven't found one considering that each led could have a different duty cycle than the rest.

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

Setting up the library

Selecting which timer to use: Timer 0, 1 or 2

#define MICROS_TIMER 			MICROS_TIMER0

Selecting the prescaler for the timer

#define PRESCALER 			256

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

Setting the ports and pins for the leds

#define LEDS_ON_PORT_B			TRUE
#define LEDS_ON_PORT_C			FALSE
#define LEDS_ON_PORT_D			FALSE
#define LEDS_ON_PORT_E			FALSE

#define LEDS_ON_PINS_PORTB		(_BV(3) | _BV(4))
#define LEDS_ON_PINS_PORTC		0
#define LEDS_ON_PINS_PORTD		0
#define LEDS_ON_PINS_PORTE		0

LEDS_ON_PORT_x - on what ports the leds are located. Place FALSE if you don't want that port used by this library or TRUE if the leds are on that port

LEDS_ON_PINS_PORTx - to what pins are the leds connected for this particular port. This is a bitmask used by the library to mask out the pins that are not used by the leds. For example if there are two leds on port B on pins 3 and 4 write (_BV(3) | _BV(4)). The order doesn't matter. Put 0 on ports that are not used.

For efficiency it is preferred for the leds to be on the same port.

#define NUMBER_OF_BITS			BITS_8

How many bits to use. 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.

I have tested 9 bits on 8MHz CPU with a 256 or 64 prescaler with good results. Other interrupts could introduce flickering as well if they take too long to complete.

#define NUMBER_OF_LEDS			8

How many leds to be controlled.

#define USE_LOGARITHMIC_ARRAY		FALSE

Set this to TRUE if you want to use the logarithmic brightness array. The array is stored in flash memory using the PROGMEM attribute so it won't occupy the RAM. There are 3 arrays and only one will be included depending on the number of bits specified.

Initializing the library

BCM_init()

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

Encoding the Binary Code Modulation duty cycle

BCM_encode(dutyCycle[], portLetter, LEDPins[], nrOfLEDs)
Uses a global array with 8 elements - 1 for each bit - and each bit in the byte represents the state for each led. The ISR uses this encoding to control the leds.

dutyCycle[] - the duty cycle for each led from 0 to 255

portLetter - on what port are the leds located. Must be a char like so: 'B', 'C', 'D'.

LEDPins[] - the pin numbers for each led. Must be in the same order as the dutyCycle[]

nrOfLEDs - how many leds or the size for the two arrays

Converting linear duty cycle to logarithmic duty cycle

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

Binary Code Modulation timer interrupt

ISR(ISR_VECT){
	
	bitpos++;
	bitpos &= 7; // reset to 0 if > 7
	
	// Re-trigger after 1, 2, 4, 8, 16... cycles
	REG_OCR <<= 1;
	
	if(bitpos == 0){
		REG_OCR = 1;
		BCM_CYCLE_END = 1; // flag for main loop
	}
	
// Turn off then on only the led pins
#if LEDS_ON_PORT_B == TRUE
	PORTB = (PORTB & (~(LEDS_ON_PINS_PORTB))) | timesliceB[bitpos];
#endif

#if LEDS_ON_PORT_C == TRUE
	PORTC = (PORTC & (~(LEDS_ON_PINS_PORTC))) | timesliceC[bitpos];
#endif

#if LEDS_ON_PORT_D == TRUE
	PORTD = (PORTD & (~(LEDS_ON_PINS_PORTD))) | timesliceD[bitpos];
#endif

#if LEDS_ON_PORT_E == TRUE
	PORTE = (PORTE & (~(LEDS_ON_PINS_PORTE))) | timesliceE[bitpos];
#endif
}

This is the timer interrupt inside the library. Only the ports that you have selected will be included with the code. 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 cycle. The main loop must clear this flag.

Example 1 - controlling an RGB led using Bit Angle Modulation (BAM):

#include <avr/io.h>
#include "bcm.h"

#define NR_OF_LEDS_PORTC			1
#define NR_OF_LEDS_PORTE			2
#define RGB_MAX_COLOR_VALUE			255

enum RGB{
	RED,
	GREEN,
	BLUE,
	NUM_COLORS
};

int main(void){
	// RGB pin and ports
	uint16_t dutyCyclePORTC[NR_OF_LEDS_PORTC] = {255};       
	uint16_t dutyCyclePORTE[NR_OF_LEDS_PORTE] = {0, 0};  

	uint8_t const LEDPinsPORTC[NR_OF_LEDS_PORTC] = {4}; // pin PC4
	uint8_t const LEDPinsPORTE[NR_OF_LEDS_PORTE] = {3, 2}; // pins PE4, PE5
	
	// RGB related
	int16_t RGB_values[] = {255, 0, 0}; // Red, Green, Blue (must be int16_t or int)
	uint8_t RGB_fading_up_color = GREEN;
	uint8_t RGB_fading_down_color = RED;
	const uint8_t RGB_fade_step = 1;
	
	BCM_init();
	
	while(1){
		if(BCM_CYCLE_END){
			BCM_CYCLE_END = 0;
			
			dutyCyclePORTC[0] = RGB_values[0];
			dutyCyclePORTE[0] = RGB_values[1];
			dutyCyclePORTE[1] = RGB_values[2];
			
			BCM_encode(dutyCyclePORTC, 'C', LEDPinsPORTC, 1);
			BCM_encode(dutyCyclePORTE, 'E', LEDPinsPORTE, 2);
			
			RGB_values[RGB_fading_up_color] += RGB_fade_step;
			RGB_values[RGB_fading_down_color] -= RGB_fade_step;
			
			// Reached top of fading up color, change to the next one
			if(RGB_values[RGB_fading_up_color] > RGB_MAX_COLOR_VALUE){
				RGB_values[RGB_fading_up_color] = RGB_MAX_COLOR_VALUE;
				RGB_fading_up_color++;
				
				if(RGB_fading_up_color > BLUE) RGB_fading_up_color = RED;
			}
			
			// Reached bottom of fading down color, change to the next one
			if(RGB_values[RGB_fading_down_color] < 0){
				RGB_values[RGB_fading_down_color] = 0;
				RGB_fading_down_color++;
				
				if(RGB_fading_down_color > BLUE) RGB_fading_down_color = RED;
			}
			
			_delay_ms(50);
		}
	}
	
	return 0;
}


Download

Version 1.0

bcm-8bit.h

Spreadsheet for prescaler selection based on CPU frequency

Spreadsheet for logarithmic brightness (needs Libreoffice for generating the array code)

No comments:

Post a Comment