3-channel pseudo-13-bit PWM on one low cost ATmega? - Page 1
Forum section: electronics / projects

author: Fagear
author_rank: Regular Contributor
timestamp: August 31, 2013, 09:46:46 pm
content: I was playing around with ATmega88 and trying to control RGB LED light strip with it. I've quickly noticed that 8-bit PWM is not enough for light control: when PWM value is close to zero there are definetely visible steps in brightness as well as the lowest brightness (PWM value 1) is not enough dark.I had to switch to more precise PWM, but in ATmega88 (and all cheap AVRs) there are only one 16-bit timer but I needed three PWM outputs at least. So I've scratched my head for some time and had came with solution.I'm using 16-bit timer for timing.So how it works: firmware converts linear one-byte brightness values into integers (non-linear), sorts them, puts them into buffer for PWM processor. Then "Match A" interrupt saves the buffered values, turns all non-zero channels on and sets "Match B" interrupt to nearest (lowest) value. Then three (in general case, when all brightnesses are different and not zero) "Match B" interrupts occur one by one, on each one one of channels turns off and OCR1B value sets to next nearest value.This description seems to be confusing, but this is how it works.Yes, my algorithm have some limitations: due to not-hardware type of PWM and very long interrupts procedures values must differ more than 20 (in 0...8192 scale). Because of software-processing time lag. This is handled in not time-critical section: if integer value are too close to each other or to zero - they will be equalized or zeroed (and "equal" flags will be set). But it affects final RGB output only on very low brightness.Code:More detailed description of my algorithm:1) First I have three one-byte (0...255) numbers: brightness values for RGB channels in some order, let's say "R (CH-1), G (CH-2), B (CH-3)".2) I convert these one-byte (char) variables with table in flash memory (it is not loaded into RAM) into two-byte (integer) values. Conversion is not linear because of eye response. I choosed 13-bit value limit, this is good enough (8192 top value). Relation between values is in attachment "byte2pwm.png". This is not time-critical part and it runs in main program cycle.3) Then I sort variables by value and set some flags if needed: "1 and 2 are equal" and "2 and 3 are equal" (this is for faster interrupt processing later).4) Also I have to store order of channels to not mix them up. For example: R=125, G=0, B=230. Let's say after conversion values are: R=1048, G=0, B=6000. All of them are different, so no "equal" flags are set. After sorting values we have: LOW=0, MIDDLE=1048, HIGH=6000. Now we have to store order of sorted values. And this what it is: LOW=CH-2, MIDDLE=CH-1, HIGH=CH-3.5) Now this is time critical part. I turn off interrupts and store values into global buffer for PWM processor: three integer values of brightness, three "order" chars and one char with "equal" flags. Then I turn interrupts on again. This is needed because interrupt can occur while not all of these values are stored into buffered variables, but interrupt will save all of them for internal processing while not all of them had been updated.All these run in main program cycle.Now, interrupt part.Timer is set to "Clear on compare" with OCR1A=0x2000 (8192dec) as TOP value. OCR1B initially set to 0xFFFF. AVR core runs at 16 MHz and timer clock is clk/8=2 MHz.I use two "Timer1 Compare Match" interrupts.Match A occurs on PWM TOP value and this event turns on all PWM channels (if PWM value is not 0). Match B handles event of counting for next brightness level, switching according channel off and switching to another one OCR value.What "Match A" handler does:1) Stores all buffered on step 5 (above) variables into internal ones needed for PWM processing.2) Checks one by one all brightness levels (from HIGH to LOW) and if they are not zero - set according "turn on" bit in mask and store it brightness level for "Match B" interrupt. Also it stores PWM stage according to last non-zero channel. While going from HIGH to LOW it overwrites previous values exept of mask.3) Puts minimum found (but not zero) brightness value into OCR1B and switches PWM channels on by calculated mask.What "Match B" handler does:1) Resets channels "turn-off" mask.For stage 1 (timer counted to LOW brightness):2.1) Set bit for LOW channel in mask.2.2.a) If "1 and 2 are equal" bit is set - set bit for MIDDLE channel in mask (so they can switch off together).2.2.a.a) If "2 and 3 are equal" bit is set - set bit for HIGH channel in mask (so all channels can switch off together). Work is done, waiting for "Match A" interrupt.2.2.a.b) "2 and 3 are equal" bit is not set - set OCR1B to HIGH value and switch to stage #3. Routine is finished.2.2.b) "1 and 2 are equal" bit is not set - set OCR1B to MIDDLE value and switch to stage #2. Routine is finished.For stage 2 (timer counted to MIDDLE brightness):3.1) Set bit for MIDDLE channel in mask.3.2.a) If "2 and 3 are equal" bit is set - set bit for HIGH channel in mask (so 2 and 3 can switch off together). Work is done, waiting for "Match A" interrupt.3.2.b) "2 and 3 are equal" bit is not set - set OCR1B to HIGH value and switch to stage #3. Routine is finished.For stage 3 (timer counted to HIGH brightness):4) Set bit for HIGH channel in mask.5) Apply mask to PWM output.16-bit timer allows me to get more precise low-brightness values and lower minimum brightness (I use only 13 bits because time lag with software processing can't get me even more precise low-brightness steps and lower brightness).13-bit PWM gets me more natural and soft colors and I can use wider range of them. For example, orange color on RGB strip with 8-bit PWM is pretty difficult task. You have RED on 255, you set GREEN to 1 and it is already yellow-like (also RED is often less bright than GREEN on these light strips). If you want to adjust brightness of this color - you are trapped (one channel is on maximum, other - on minimum).I have implemented this algorithm, sticked it together with microphone, low-pass filter and yet another non-linear table conversions, and got an music RGB controller:?? RGB?,? (10)(wide range brightness changing)?? RGB?,? ((different brightness orange color + white flash)I don't know, if there are other similar algorithms. But what I got from my head gives me 13-bit 3-channel PWM out of cheap AVR MCU with very little limitations.

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: August 31, 2013, 09:59:34 pm
content: 2 methods for increasing PWM resolution. :1) Do a 'coarse' PWM pulse to whatever resolution you can, let's say this is 32uSThen do a second 'fine' pulse of 0-31uS.This gives 13 bits at 1uS resolution. I've done 8 channels of 13-bit PWM on a PIC16F818 like this2) dither.On each PWM cycle, you conditionally increase the pulse length by one to get a fine adjustment. This gives a small ripple which is not noticeable.e.g. lengthen every other pulse for a 'half' step, every 4th pulse for a quarter step etc.There is a neat algorithm for this, using a phase accumulator :acc, coarse, fine are 8 bit values, and hardware PWM is 8 bitOn each PWM cycle :acc=acc+fineif carry clear, PWM = coarse else PWM=coarse+1For smooth dimming of a single colour at low levels you need a minimum of 12 bits. You can use a simple gamma correction function to get a decent greyscale from 8-bit intensity values, by squaring the 8 bit valuee.g. for 12 bit PWM, PWM=(i*i)/16

author: jaxbird
author_rank: Frequent Contributor
timestamp: August 31, 2013, 10:22:18 pm
content: I often find the dither method very useful to increase resolution, especially if you have some kind of capacitor/low pass filter on the output.

author: Fagear
author_rank: Regular Contributor
timestamp: November 02, 2013, 05:02:45 pm
content: I have switched to BAM (Binary Angle Modulation) or BCM (Binary Code Modulation). Now using MIBAM technique (mirrored BAM to eliminate flicker on some transitions). My old code just have no contest to it.My MIBAM variant is much smaller in case of code memory and CPU time. It is way more stable, have no restrictions on minimal brightness and all channels' brightness.Now I'm using 10-bit MIBAM with some optimizations and it works very well.Using only one interrupt (16-bit timer overflow), simple interrupt routine, double buffering values.Here is code:Code:// Maxim Krukov aka Fagear (2013)// Code used on ATmega168 (ATmega88/ATmega328)#define IO_LED_PORTPORTD// Output port for LEDs.#define IO_LED_DIRDDRD#define IO_LED1(1<<7)// Channel G.#define IO_LED2(1<<6)// Channel B.#define IO_LED3(1<<5)// Channel R.#define BAM_DATA_VALID(1<<0)// BAM values updated successfully.#define BAM_MAX_STAGE16// BAM last stage number.#define BAM_STAGE_COUNTBAM_MAX_STAGE+1// BAM stage count.#define COLOR_R0#define COLOR_G1#define COLOR_B2register volatile unsigned char uc_bam_stage asm("r2");volatile unsigned char uc_bam_data_flags=0;volatile unsigned intui_bam1_comp=0,// BAM channel 1 value for BAM processor (interrupts).ui_bam2_comp=0,// BAM channel 2 value for BAM processor (interrupts).ui_bam3_comp=0;// BAM channel 3 value for BAM processor (interrupts).unsigned intui_bam_bit=0,// BAM bit numer in input value.ui_bam1_in=0,// BAM channel 1 buffered value from BAM_data_convert();ui_bam2_in=0,// BAM channel 2 buffered value from BAM_data_convert();ui_bam3_in=0;// BAM channel 3 buffered value from BAM_data_convert();// Lookup table for BAM stage -> bit mask (1<<bit).const unsigned char tbl_bam_bitmask = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 3, 4, 5, 6, 7, 8, 9};// Lookup table for BAM stage -> period time.const unsigned int tbl_bam_len = {5120, 7680, 8960, 9600, 9920, 10080, 10160, 10240, 10280, 10300, 10380, 10540, 10860, 11500, 12780, 15340, 20460};// Lookup table for BYTE -> BAM (brightness correction).const unsigned int tbl_byte_to_bam PROGMEM ={0,0,0,1,1,1,1,1,1,2,2,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,6,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,14,14,15,16,16,17,18,19,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,38,39,40,41,43,44,46,47,48,50,51,53,55,56,58,59,61,63,65,66,68,70,72,74,76,78,80,82,84,86,88,90,92,95,97,99,102,104,107,109,112,114,117,119,122,125,127,130,133,136,139,142,145,148,151,154,157,160,163,167,170,173,177,180,184,187,191,194,198,202,205,209,213,217,221,225,229,233,237,241,245,250,254,258,263,267,272,276,281,286,290,295,300,305,310,315,320,325,330,335,340,345,351,356,361,367,373,378,384,389,395,401,407,413,419,425,431,437,443,450,456,462,469,475,482,488,495,502,509,516,522,529,536,544,551,558,565,573,580,587,595,603,610,618,626,634,642,650,658,666,674,682,690,699,707,716,724,733,742,750,759,768,777,786,795,805,814,823,833,842,852,861,871,881,890,900,910,920,930,941,951,961,972,982,993,1003,1014,1023};// Timer1 Compare Match B Handler.// Software 3-channel BAM processing.ISR(TIMER1_COMPB_vect){ui_bam_bit=(1<<tbl_bam_bitmask);if((ui_bam1_comp&ui_bam_bit)!=0)IO_LED_PORT|=IO_LED1;elseIO_LED_PORT&=~IO_LED1;if((ui_bam2_comp&ui_bam_bit)!=0)IO_LED_PORT|=IO_LED2;elseIO_LED_PORT&=~IO_LED2;if((ui_bam3_comp&ui_bam_bit)!=0)IO_LED_PORT|=IO_LED3;elseIO_LED_PORT&=~IO_LED3;// Set next time trap.OCR1B=tbl_bam_len;// Go to next stage.uc_bam_stage++;if(uc_bam_stage>BAM_MAX_STAGE){uc_bam_stage=0;// Check data validity.if((uc_bam_data_flags&BAM_DATA_VALID)!=0){// Pre-load new color values.ui_bam1_comp=ui_bam1_in;ui_bam2_comp=ui_bam2_in;ui_bam3_comp=ui_bam3_in;}}}//-------------------------------------- Startup initialization.inline void reset_init(void){// Hardware init.// Setup LED pins.IO_LED_DIR|=IO_LED1|IO_LED2|IO_LED3;// Start Timer1 (10-bit MIBAM timing): clk/8, clear on compare match (TOP=OCR1A).TCCR1B=(1<<CS11)|(1<<WGM12);// Set largest time offset as TOP.OCR1A=tbl_bam_len+1;// Set default OCR1B (prepare for bit0).OCR1B=tbl_bam_len;// Enable Timer1 overflow interrupt.TIMSK1=(1<<OCIE1B);}//-------------------------------------- Convert values for BAM.void BAM_data_convert(void){// Clear flag.uc_bam_data_flags&=~BAM_DATA_VALID;// Convert 8-bit values to 10-bit values (0...1023).ui_bam1_in=pgm_read_word_near(tbl_byte_to_bam+uca_transit);ui_bam2_in=pgm_read_word_near(tbl_byte_to_bam+uca_transit);ui_bam3_in=pgm_read_word_near(tbl_byte_to_bam+uca_transit);// Set flag.uc_bam_data_flags|=BAM_DATA_VALID;}Input brightness values:uca_transit,uca_transit,uca_transit- unsigned char (0...255).BAM_data_convert()must be called after each update ofuca_transitor simply in cycle frommain().This algorithm is universal and can be easily converted in 8-bit, 9-bit, 10-bit or whatever variant of BAM or MIBAM by changing defineBAM_MAX_STAGEand arraystbl_bam_bitmask[],tbl_bam_len[]. Refresh rate and accuracy are also controlled by these values and clock of MCU.I have an excel file with some scripts to recalc

author: David_AVD
author_rank: Super Contributor
timestamp: November 02, 2013, 09:11:04 pm
content: Quote from: mikeselectricstuff on August 31, 2013, 09:59:34 pmOne of the Christmas light manufacturers (Light-O-Rama) uses a similar trick to get 7-bit resolution from the 5-bit pixel drive ICs (LPD6803) in their "Cosmic Color Ribbon" products. Of course in their case they are diddling with the pixel data stream, not PWM periods.

author: DINKIN
author_rank: Unknown
timestamp: August 03, 2017, 10:41:08 am
content: Quote from: David_AVD on November 02, 2013, 09:11:04 pmI always wondered the difference between an Light-O-Rama system and a traditional DMX controller??

author: David_AVD
author_rank: Super Contributor
timestamp: August 03, 2017, 11:51:13 am
content: All but the oldest LOR controllers will accept DMX as well as the LOR serial protocol.

author: albert22
author_rank: Regular Contributor
timestamp: August 03, 2017, 08:22:02 pm
content: QuoteI dont understand how do you manage to do the 2nd pwm (1uS) and combine it with the 1st.Could you explain this method a little more ?Thanks

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: August 03, 2017, 08:57:12 pm
content: Quote from: albert22 on August 03, 2017, 08:22:02 pmYou do those one channel at a time with a simple delay loop and/or jump over variable number of NOPs. The small gap on the other channels while doing this means you never get quite 100% but this isn't a big deal

author: georges80
author_rank: Frequent Contributor
timestamp: August 03, 2017, 09:06:40 pm
content: A love the AVR family of uC. BUT... I've started using the STM STM32F030C6T6 device and it offers so much more capability for less money. The ONLY aspect I miss is the onboard eeprom, but a 20c external sot part takes care of that if needed (if flash emulation is not good enough - i.e. eeprom variables that change too often).Microchip has at least reduced the cost of a bunch of the AVR parts, but they are still relatively expensive versus a lot alternatives that are out there now.The STM32F030C6T6 (as an example) provides a LOT more timers and timer capabilities with multiple channels and 16 bit resolution etc.Anyhow, not trying to start a uC war, but sometimes a fresh look on what is out there makes you realize you have been fighting limitations and higher cost for no reason.And yes, dithering schemes can be made to work depending on the lower frequency 'ripple' you can deal with (based on free cycles available to your code).cheers,george.

author: albert22
author_rank: Regular Contributor
timestamp: August 05, 2017, 07:36:03 pm
content: Quote from: mikeselectricstuff on August 03, 2017, 08:57:12 pmThanks a lot !!.I got the coarse and fine pwm part but I still trying to figure out how to do several of this loops at the same time without disrupting each other timings. It is being a nice puzzle to solve, now that I know that is possible. I guess that the key is not reaching 100% as you say. I will try 2 or 4 pwm on a 16f88.BTW I like your videos, always waiting for a new one, good work.Regards

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: August 05, 2017, 08:35:17 pm
content: Quote from: albert22 on August 05, 2017, 07:36:03 pmYou do the coarse by calculating the PWM for each channel in turn then updating all ports at once. This dictates your coarse step time. In assembler you can get this down to a few instructions per channel using compare and rotate to shift the carry from each compare result.However PWM is hard to scale - binary code modulation is a lot easier, especially if you have a hardware peripheral that can generate exact pulse lengths, and a global enable for all LEDs. e.g. for n outputs with x bits of resollutiont=1for bit = 0 to x-1Turn on all outputs that have a "1" in bit <bit>.Pulse the global enable on for time tt=t*2next bit

author: Fungus
author_rank: Super Contributor
timestamp: August 06, 2017, 12:21:04 pm
content: Quote from: albert22 on August 05, 2017, 07:36:03 pmIt might be easier to ditch the hardware PWM and do the whole thing in software. It's not difficult to count up to 65536 and change a pin state when you go past a value.Do any other processing in between the PWM pulses. You'll find you can do quite a lot of work without affecting the LED display.(in fact a bit of extra processing actuallyhelpswhen the problem's a big step between PWM '0' and PWM '1'. With enough extra processing you can do 12 or even 8-bit PWM instead of 16-bit.)

author: danadak
author_rank: Super Contributor
timestamp: August 07, 2017, 12:00:20 am
content: Here is an example of 16 16 bit HW PWMs on a chip, PSOC. Plus I threwinto design some VDACs and DSP filter, also all on same chip.https://www.eevblog.com/forum/microcontrollers/best-way-to-add-external-dacs/msg1207330/#msg1207330Regards, Dana.