spacer LEDactus - Generation 1

In this generation I will add a degree of intensity control to the LEDs. This is accomplished by varying the duty cycle of the LEDs but is far from perfect. This is partially a result of the non-linear perceived brightness of a pulsed LED but also due to natural variations in the brightness of the LEDs I use. Nonetheless, the effect is reasonably pleasant.


PWM Control of LED Brightness

The idea of turning LEDs (and other devices such as motors) on and off rapidly to control their intensity (or speed) is called pulse-width-modulation (PWM). The LED is usually switched on and off at a fixed but arbitrary frequency. The time the LED is turned on versus the period of the cycle is called the duty-cycle. A duty-cycle of 0 means the LED is always off. A duty-cycle of 100 means the LED is always on. And a duty-cycle of 25 means the LED is on 25% of the time and off for 75%.

The perceived brightness of a pulsed LED is much brighter than one expect based on the on-time of the LED. For the cheap LEDs I purchased, an LED that is on half the time (50% duty cycle) appears about 70% the brightness of a permanently lit LED.

The plot below shows, based on my eyeballs, the rough duty cycle required to achieve a level of brightness. I've chosen 127 to represent a value of full brightness (I wanted to reserve the 8th bit for a future project).

Duty Cycle versus Perceived Brightness

Because of this non-linear behaviour we'll need to devise a method to translate desired brightness to the appropriate duty cycle.

Intensity Duty-Cycle
120 89
112 77
104 66
96 56
88 47
80 39
72 31
64 24
56 18
48 13
40 9
32 5
24 3
16 1
8 1
0 0

Flicker

Finally a word about flicker. The current code writes each LED somewhere in the neighbourhood of 7K times per second. Because we want to be able to a use a duty cycle as low as 1/128 this update rate gets divided by 128 yielding a 54Hz update rate for an intensity modulated LED. This rate of 54Hz creates a mild but perceptible flicker. If you need to get rid of this, or want to expand the dynamic range of intensities you can display, you'll need to increase the clock speed of the PIC18F1320 or improve the efficiency of the code.

Currently the PIC chip is running at 8MHz. This can be increased to 20MHz using an external ceramic resonator and a few low value capacitors. Using a crystal and invoking HSpll mode, you can do 40MHz.

Code

void display21(unsigned long pattern){
   static unsigned long lastPattern = 0xffffffff;
   static unsigned short portB_pat[3];
   unsigned short i,j;
   unsigned short *pPortB_pat;

   // to increase performance, the PORTB patterns are cached since they
   // involve the most calculation.  This yields about a 3x increase.

   pPortB_pat = portB_pat;
   if (lastPattern != pattern) {
      lastPattern = pattern;
      for (i=0; i<3; i++) {
         *pPortB_pat = pattern & 0b1111111;
         pattern >>= 7;
         for (j=0; j<=i; j++){
            //roll the pattern left 1
            *pPortB_pat = (((*pPortB_pat) & 0b10000000) > 0) | ((*pPortB_pat) << 1);
         }
         pPortB_pat++;
     }
  }
  for (i=0; i<3; i++) {
     TRISB = 0b11111111;
     PORTB = *pPortB_pat;
     TRISB = ~(*pPortB_pat | (1 << i));
     // adding a delay after turning on the LED results in a considerably
     // brighter display.  Makes sense.
     Delay_us(16);
pPortB_pat++; } TRISB = 0b11111111; } unsigned long filterGray21(unsigned short *gray,unsigned short intensityLevel) { unsigned long pattern = 0; short i; // generates a bit pattern that turns on all LEDs whose intensity is // greater than the reference intensityLevel for (i=20; i>=0; i--) { pattern <<= 1; if (gray[i] >= intensityLevel) { pattern++; } } return pattern; } void displayGray21(unsigned short *gray){ unsigned short dutyCycleLookup[] = {0,1,2,3,5,9,13,18,24,31,39,47,56,66,77,89}; unsigned long pattern; unsigned short dutyCycle; unsigned short index; unsigned short i; index = 0; dutyCycle = 0; // the basic loop displays 128 patterns before repeating. By displaying // different patterns during this loop we can achieve a duty cycle // from 0.78% up to 100% in steps of 0.78%. for (i=0; i<128; i++) { if (i==dutyCycle) { // only update the pattern when the duty cycle changes pattern = filterGray21(gray,index << 3); index++; dutyCycle = dutyCycleLookup[index]; } display21(pattern); } } void main(){ unsigned short pattern[] = {127,112,96,80,64,48,32,16,8, 4,0,127,112,96,80,64,48,32,16,8,0}; unsigned short i; unsigned short swap; OSCCON = 0x70; //set the internal OSC to 8MHz (specific to 18F1320) INTCON = 0x00; //disable all interrupts ADCON1 = 0b01111111; //turn off analog inputs AN0 - AN6 // display a 'gray scale' pattern swap = pattern[0]; do { for (i=0; i<5; i++){ displayGray21(pattern); } // rotate the pattern swap = pattern[0]; for (i=0;i<20;i++) { pattern[i] = pattern[i+1]; } pattern[20] = swap; } while(1); } //@DRI

This code reuses the routines from generation-0 but with a few improvements to the display21( ) routine. Most time in this routine is spent creating the PORTB patterns. This involves numerous bitwise rolls. Being used in the innermost loop, this routine deserves some attention. I decided to cache the PORTB patterns whenever the bit pattern changed. This routine is usually called many times in a row without changing the pattern and caching the PORTB patterns speeds things up by about a factor of 3.

The new routine displayGray21( ) is in charge of the PWM of the LEDs. It does this by displaying a sequence of 128 binary patterns using display21( ). During the sequence, it gradually turns off bits controlling dimmer LEDs while leaving on those that control brighter LEDs. The points at which the bits are turned off are controlled by the dutyCycleLookup[ ] table. The entries in the table tell at what point in the 128 pattern cycle the next change in LED pattern should occur. The intensity an LED must have to remain on at the point in the sequence is calculated as the index into the table times 8.

Each time a the dutyCycleLookup[ ] table specifies that a pattern change should occur, the filterGray21( ) is called. This routine creates a binary display pattern based on those LED whose intensity is greater than the reference intensity level.

Next Section: LEDactus - Light Sensing