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:
... 0b0000001000000000 0b0000001000100000 0b0000001001000000 0b0000001001100000 ... 0b0000001111100000 0b0000010000000000 <-- Bit shift; restart count with leading '1'. 0b0000010001000000 0b0000010010000000 0b0000010011000000 ... 0b0000011111000000 0b0000100000000000 <-- Bit shift; restart count with leading '1'. 0b0000100010000000 0b0000100100000000 0b0000100110000000 ...
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:
- 1st minor frame: Most calculations performed, appropriate colors turned on, and setup for on/off switching of 2nd minor frame.
- 2nd minor frame: Appropriate colors switched, red turned off at precise time, and setup for on/off switching of 3rd minor frame.
- 3rd minor frame: Appropriate colors switched, green turned off at precise time, and setup for on/off switching of 4th minor frame.
- 4th minor frame: Appropriate colors switched and blue turned off at precise time.
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.
- Random color approach mode: A pseudo-random color is chosen and each executed frame brings the displayed color closer to the chosen color. When a color channel's color reaches the chosen color, the process repeats.
- Random color flip-through mode: Random colors are periodically displayed.
- Primary color grow mode: At initialization of this mode, the brightness of all three color channels are set equidistant to each other. All colors' brightnesses are continuously increased. When their values overflow, the brightness is returned to off before repeating the sequence again.
- Secondary color flip-through mode: Continuously flips through all primary, secondary, and white and off colors.
- Manual red, green, and blue modes: These modes allow manual control of each of the color channels by rotating the knob. Only one color channel intensity is controlled at a time, while the other channels' intensities stay that same. Using this mode one can set the CRM-114B to display any constant color within the devices color map.
- Twinkle mode: A mostly white color is displayed, changing such that it appears to be twinkling.
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:
- To generate psuedo-random values for colors, sequential bytes of program space were read. While this would fail all but the simplest of randomness tests, it is sufficient for setting colors. The period of the psuedo-random number generator "algorithm" is 1024--the number of bytes of program space used by the algorithm. As it is used by 3 different color channels and since 1024 is not divisible by 3, the period of this PRNG is essentially 3072. Further, seat-of-the-pants viewing over several minutes confirms that there's no perceptible pattern or weightings.
- While using smaller data types is near the top of the list of tips for saving program space, it turns out that using a 16-bit variable for the PRNG address saves 10 bytes of program space over using an 8-bit variable. This is due to it being used as an address and the extra manipulation the compiler would perform for the 8-bit variable.
- The number of modes, 8, saved additional program space as the roll-over algorithm was simplified.
// Only works if NUM_MODES is a power of two. current_mode &= (NUM_MODES - 1);
- More generally, powers of two were used anywhere a multiply or division operation was required. Designing the system such that multiplications and divisions can be done by powers of two reduces the value manipulations to simple bit shifts. This technique was used in several portions of the CRM-114B SW design.
- By rearranging code so that references to the same variable are close to each other, the compiler can often take advantage of a variable already being loaded in a register, eliminating the program space usage for loading these variables into a register. This technique was tested by trial and error in multiple places that seemed likely to reduce program space and used to reduce used program space by dozens of bytes.
- All inputs were connected to one port. All outputs were connected to another single port. This allowed a single read/write instruction for all inputs/outpus at a time. Additionally, the output channels were connected to consecutive pins (i.e. "PD2", not "6") allowing the use of a loop and an index to maniuplate them.
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 +----+
- Initializing variable values is best practice and recommended, but on smaller-scale software projects you're more likely to be able to get away with not doing so. With the AVR, one can rely on the variables to be initialized to zero upon power application, while initializing them to non-zero values requires additional program space. When designing code there will be occasions where using run-time variable manipulation logic in lieu of variable initialization can save program space. This was used in the CRM-114B code by redesigning the logic that manipulates and tests the speed (variable is "delay") that can't be zero without breaking the logic. The following also takes advantage of AVR-GCC's reserved "zero register"--a register that is set to zero.
// Less efficient
uint8_t delay = 1;
// ...
if (delay >= 1)
{
delay >>= 1;
}
// vs
// More efficient
uint8_t delay;
// ...
delay >>= 1;
if (delay == 0)
{
delay = 1;
}
- 8-bit devices' operations on 16-bit variables are more expensive than operations on an 8-bit variable. There are times where only one byte of a 16-bit variable needs to be accessed. By using unions one can do that. 20 bytes of program space were saved with this technique.
// Less efficient
uint16_t intensities[NUM_LEDS];
// ...
if ((intensities[channel << 1] >> 14) > standard_period)
// vs
// More efficient -- saves 16 bytes
union
{
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)
- "volatile" tells the compiler that a variable might be changed out of order--for instance, by an interrupt service routine. The compiler will not optimize the way it is accessed when this keyword modifies a variables declaration. One place you'd generally want to use this is for variables that are written to in an ISR and read outside of an ISR. Experimenting with this, it was found that a couple of variables that experience would suggest applying the volatile modifier to, in fact didn't need it. This technique saved additional program space.
- This last not-so-obvious technique for saving program space relies on the magic of compiler optimization. By trial and error, it was found in multiple places that repositioning certain lines of code relative to each other (where the logic allows) can save significant program space. This is similar to the "rearranging code" technique discussed earlier, but different in that the changes attempted didn't seem likely to reduce program space. One particular use of this technique in which a single line of C code was moved above the previous line of code saved 78 bytes of program space!
Additional program space saving techniques that were note used:
- If the outputs were reconnected to use PD0..PD2 instead of PD3..PD5 an additional 8 bytes of program space could have been saved.
- The rotary encoder module includes pull-up resistors for the encoder signals. It also includes solder pads for a pull-up resistor for the pushbutton, but it does not include the pull-up resistor itself. The addition of this pull-up resistor would remove the necessity to enable the MCU's internal pull-up resistor and saved 4 additional bytes of program space.
// This wouldn't be needed if the pushbutton's pull-up resistor was
// installed.
SWITCH_PORT = (1 << SWITCH_BUTTON);
- The interrupt vector table is a table in a fixed location of program space that defines the addresses of the interrupt service routines. It is possible to use contiguous unused portion of this table for other parts of the program. In this particular case, an additional 20 bytes of program space should be able to be saved.
Results
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.
Links
For more details about the software design, see the verbose comments embedded in the code itself: https://github.com/oelgern/crm114b/blob/master/src/crm114b.c
The project repo can be found at: https://github.com/oelgern/crm114b
The control panel template repo can be found at: https://github.com/oelgern/box_underlays