This project demonstrates how precision manual PWM can compensate for the non-linearity of a human's perception of light intensity, creating multiple visually appealing light patterns, all within 1kB of program space. The application was a retrofit of an Ikea light fixture for use as a Christmas tree star, and the user controls were fitted into a professional-looking package made with common tools and a shoestring budget.

Non-Linearity of Human Perception

A human's perception of light intensity is non-linear. As an example, the perceived difference between 1% and 2% duty cycle lights is much greater than between 99% and 100%. The result when increasing standard PWM values for lighting is the light will ramp up to almost full intensity very quickly and subsequently only gradually increase in intensity.

To compensate for this, an algorithm is required that increments the intensity value with small steps on the low end and larger steps on the high end--an exponential algorithm. One obvious solution would be iteratively using the following:

Seeing as the target device (ATtiny2313A--I had on hand and am familiar with) doesn't have a multiply instruction and that a goal was to keep the program space usage to 1kB, this obvious solution would not meet the project requirements. Instead, a less program space intensive algorithm was devised and used. Counting through consecutive values while multiplying by two (a left bit-shift) every 16 counts results in an approximation of an exponential function with values like this:

0b0000010000000000 <-- Bit shift; restart count with leading '1'.
0b0000100000000000 <-- Bit shift; restart count with leading '1'.

Manual PWM

For a larger dynamic range of light intensity and smoother brightening and dimming, 16-bit PWM was chosen. As the ATtiny2313A has only a single 16-bit timer with only two PWM channels, a manual PWM scheme had to be employed to modulate the three color channels simultaneously.

The 16-bit overflow interrupt is used to mark the beginning of major frames--a major frame lasts 2^16 MCU cycles. The major frames are split into four equally-timed minor frames. Tasks are split between these minor frames so as to prevent calculations and color switching from interfering with each other and causing glitches.

Minor frame actions:

The calculations for on and off switching are performed in the overflow interrupt routine for the 1st minor frame. Timer compare A interrupt routine is used to mark the beginning of the 2nd-4th minor frames where channels are turned on. Timer compare B interrupt routine is used to turn off the colors at the precisely calculated times during the 2nd-4th minor frames. The timers to turn channels on and off in each minor frame are set during the previous minor frame. The masks used to turn on and turn off the colors are loaded into a non-indexed variable at the end of the execution of the previous minor frame and used immediately when beginning the routine. In this way, the accuracy of the color intensity is maximized.

Light Patterns

The CRM-114B has 8 light pattern modes that are controlled by the switch integrated into the rotary encoder. Pushing the knob cycles through the pattern modes.

In addition to the pattern modes, the user can select seven different speeds for each of the patterns by rotating the encoder.

Miscellaneous Program Space Savings

Much information is available online for saving code space on an AVR. In addition to the more obvious ways, the CRM-114B employs the following software and hardware code space saving techniques:

// Only works if NUM_MODES is a power of two.
current_mode &= (NUM_MODES - 1);
  Outputs       +-\/-+        Inputs
          PA2  1|    |20  VCC
          PD0  2|    |19  PB7
          PD1  3|    |18  PB6
          PA1  4|    |17  PB5
          PA0  5|    |16  PB4
  Red LED PD2  6|    |15  PB3
Green LED PD3  7|    |14  PB2 Switch button
 Blue LED PD4  8|    |13  PB1 Switch DT
          PD5  9|    |12  PB0 Switch CLK
          GND 10|    |11  PD6
// Less efficient
uint8_t delay = 1;
// ...
if (delay >= 1)
delay >>= 1;

// vs

// More efficient
uint8_t delay;
// ...
delay >>= 1;
if (delay == 0)
delay = 1;
// Less efficient
uint16_t intensities[NUM_LEDS];
// ...
if ((intensities[channel << 1] >> 14) > standard_period)

// vs

// More efficient -- saves 16 bytes
uint8_t bytes[NUM_LEDS * 2];
uint16_t value[NUM_LEDS];
} intensities;
// ...
// Only need to manipulate the high byte
if ((intensities.bytes[(channel << 1) + 1] >> 6) > standard_period)

Additional program space saving techniques that were note used:

// This wouldn't be needed if the pushbutton's pull-up resistor was
//  installed.


The CRM-114B is a fine addition to our Christmas tree this year and was an enjoyable project to design and build. It should top our Christmas tree for many years to come. Additionally, in the process of putting it together the first few in a set of shareable templates for control panels was released.

The final program used 1000 bytes of program space when built with avr-gcc 4.9.2 using the -Os optimization flag. No EEPROM space was used.

~/proj/crm114b/src $ avr-size --mcu=attiny2313 crm114b.elf 
   text	   data	    bss	    dec	    hex	filename
   1000	      0	     35	   1035	    40b	crm114b.elf

It should be noted that using a larger device (2 kB FLASH capacity) made development of the software easier. By using the larger device, the program features could be implemented before working on reducing the occupied program space. Then after the program space was reduced below the 1 kB limit, additional features and refinements could be made. This was an iterative process that would've likely been a lot less interesting and productive without such freedom.


For more details about the software design, see the verbose comments embedded in the code itself:

The project repo can be found at:

The control panel template repo can be found at: