My friend, J, has this 7 segment RGB display from Adafruit. She plans to make each segment cycle through the colors and make it wearable. So, while she's getting ready for that, I thought I'd see if I could make it cycle through colors. She has 4 AtTiny85's so I thought I would use one and a Lattice M4A5 as a shift register to control the 21 cathodes for the common anode display.
The AtTiny code uses a virtual color wheel. It's very simple. I divide the circle into 240 "degrees" or steps. Each color is just a triangle centered at 240*(0/3), 240*(1/3), and 240*(2/3). I.e. 0, 80, and 160. Each color occupies half the circle. Each segment has its own byte that contains its angle on the color wheel.
A decomposition function converts a color into RGB components by locating the height of each triangle if the angle is on the triangle. It scales it so that the peak is 255 (ish). It returns an array of 3 bytes of 0-255.
The next step is to use a sawtooth to compare to the RGB components to decide if each LED should be off or on. If one saw tooth were used, then the LEDs would all be on at the same time and that would cause on uneven drain on the power. So, eadh segment gets its own sawtooth, each one offset by an amount to make them scattered over the entire period of the sawtooth wave. This should even out the LED load quite a bit.
Once the LED states are determined, they are output. There is a consideration though with the forward voltage of the LEDs. They depend on which color of LED is being turned on. It's about 3.4 for green and blue, but 1.7 for red. That means that if I try to turn on a blue and red LED at the same time, the current will run only through the blue LED. So I have to turn on each color separately. So I mask out each color and output the red, then green, then blue, then turn all colors off. If the resistor is sufficiently small, there should be enough power to light up the LED with a narrow pulse.
The CPLD is simply a shift register. It has a SCK, DI, CS, and RESET. When CS goes low, SCK's rising edge shifts DI into the shift registers. CS's rising edge strobes the shift register to the outputs.
Some back of the napkin calculations showed me that I'd probably want a resistor to resist the current at a level of about 30 ohms. But when I started with a 100 ohm resistor, it lit up wonderfully. I increased it to 200 ohms and the colors were just as nice.
The toolchain I used for the AtTiny was the avr-gcc chain. It comes with a makefile that needs only be modified slightly. I did have to run the makefile in cygwin to build otherwise, I suspect, it uses MS make instead of gnuMake. The upload to the AtTiny uses avrdude with the avrisp setting which can talk to an Arduino with the avr programmer sketch loaded. The error messages aren't terribly helpful, but I eventually got it programming in circuit. This requires that I leave the RESET line of the AtTiny free to be RESET. I have the AtTiny lines going to little sockets that take wires from the Arduino. While I had to use cygwin's make to build, I found that "make program" in cygwin failed in its attempt to open COM3 for avrdude. So I just copied the avrdude command line and ran it in an MS-DOS command prompt.
Here is a schematic.
Here is the AtTiny85 code:
1 // This module is designed for the ATTiny85 and is designed to run a SPI shift register
2 // that has 21 pins going out to LEDs.
3 // The driver for the CPLD should assert RESET, CS, and toggle the SCK pin.
4 // SCK rising edge clocks SDA when CS is low. Shift register is written to outputs on rising edge
5 // of CS
6
7 // This code assigns a 0-239 value to each segment. This is an angle on a color wheel where
8 // each color (RGB) occupies one half the circle peaking in the center and each one third
9 // of the circle
10
11 // The segments are slowly rotated through the color wheel.
12
13 // Color is made by decomposing the color wheel angle into a 0-255 value for each color where
14 // 0 is off, and 255 is full on. This value is used in PWM control of the LEDs
15
16 // The sawtooth for PWM comparison for each LED is offset so that the load is balanced out
17 // across the PWM carrier period.
18
19 #include <stdio.h>
20 #include <stdint.h>
21 #ifndef TEST
22 #include <avr/io.h>
23 #include <util/delay_basic.h> // delays without floats
24 #else
25 #define _delay_loop_2(x)
26 #endif
27
28 // Port assignments
29 // PB0: MOSI (RESET#==0) SDA out
30 // PB1: MISO (RESET#==0)
31 // PB2: SCK (RESET#==0) SCK out
32 // PB3:
33 // PB4: CS# out, pulled up externally
34 // PB5: RESET# in
35
36 // PB5: unprogram RSTDISBL fuse,
37 // DDRB |= _BV(PB4) | _BV(PB0) | _BV(PB2); // CS out
38 // USICR = USIWM=10, USICS=00, USICLK strobe
39 // USIDR data msb out first
40
41 // angles of each of the 7 segments on a 240/div circle
42 uint8_t angles [ 7 ];
43
44 // counter for saw-tooth
45 uint8_t counter ;
46
47 /* Defaults all correct for this application
48 FUSES =
49 {
50 .low = LFUSE_DEFAULT,
51 .high = (HFUSE_DEFAULT),
52 .extended = EFUSE_DEFAULT,
53 };
54 */
55
56 // gets the distance between from and angle
57 // In many cases this is just the absolute value of the difference
58 // 'from' and 'angle' are 0-239.
59 // Returns 0-119
60 uint8_t minDistanceFrom ( uint8_t from , uint8_t angle )
61 {
62 uint8_t low ;
63 uint8_t high ;
64 uint8_t distance ;
65
66 // Get the direction in the positive direction
67 if ( from < angle )
68 {
69 low = from ;
70 high = angle ;
71 }
72 else
73 {
74 low = angle ;
75 high = from ;
76 }
77 distance = high - low ;
78
79 if ( distance > 120 )
80 {
81 // distance > half way, so it is closer to go ccw
82 // but we have to skip the gap from 240-255
83 distance = 240 - ( distance );
84 }
85 return distance ;
86 }
87
88 // Scales fromPeak from 0-60 to 255-0-ish
89 // returns 0 -> 255
90 // fromPeak: 60 -> 0 is linear and 60 or greater is 0
91 uint8_t scale ( uint8_t fromPeak )
92 {
93 uint8_t scaled = 0 ;
94 if ( fromPeak < 60 )
95 {
96 scaled = 255 - (( fromPeak << 2 ) + ( fromPeak >> 2 ));
97 }
98 return scaled ;
99 }
100
101 // This function takes an "angle" on the color wheel (0-255) and
102 // converts it to three colors that make up that color.
103 // It is a first order approximation.
104 // Each color is a triangle occupying one half of the circle
105 // and each is shifted 120 degrees.
106 // Red is centered on 0 and goes from 270 to 90
107 // Blue is centered on 120 and goes from 30 to 210
108 // Green is centered on 240 and goes from 150 to 330
109 //
110 // These values are scaled to 240 in a circle. i.e.
111 // Red is centered on 0 and goes from 180 to 60
112 // Blue is centered on 80 and goes from 20 to 140
113 // Green is centered on 160 and goes from 100 to 220
114 #define TOTAL_CIRCLE 240
115 #define RED_CENTER 0
116 #define BLUE_CENTER 80
117 #define GREEN_CENTER 160
118 #define HALF_PART 60
119
120 #define RED_START (RED_CENTER + 240 - HALF_PART)
121 #define RED_END (RED_CENTER + HALF_PART)
122 #define BLUE_START (BLUE_CENTER - HALF_PART)
123 #define BLUE_END (BLUE_CENTER + HALF_PART)
124 #define GREEN_START (GREEN_CENTER - HALF_PART)
125 #define GREEN_END (GREEN_CENTER + HALF_PART)
126
127 // anngle is 0-239
128 void decomposeColor ( uint8_t angle , uint8_t * pRGB )
129 {
130 uint8_t * p = pRGB ;
131 * p = scale ( minDistanceFrom ( angle , RED_CENTER ));
132 p ++ ;
133 * p = scale ( minDistanceFrom ( angle , GREEN_CENTER ));
134 p ++ ;
135 * p = scale ( minDistanceFrom ( angle , BLUE_CENTER ));
136 p ++ ;
137 }
138
139 #ifndef TEST
140 void shiftOut ( uint32_t bits )
141 {
142 uint8_t * byteArray = ( uint8_t * ) & bits ;
143 const uint8_t SCK_UP = 0x11 ; // 3 wire mode, TOGGLE SCK
144 const uint8_t SCK_DN = 0x13 ; // 3 wire mode, TOGGLE SCK, shift USIDR
145 const uint8_t TOSS = 0x12 ; // 3 wire mode, shift USIDR
146
147 PORTB &= ~ _BV ( PB2 ); // SCK low
148 PORTB &= ~ _BV ( PB4 ); // CS down
149
150 USIDR = byteArray [ 2 ]; // delay
151 USIDR = byteArray [ 2 ];
152
153 USICR = TOSS ; // toss bits 23, 22, 21
154 USICR = TOSS ;
155 USICR = TOSS ;
156 USICR = SCK_UP ; USICR = SCK_DN ;
157
158 USICR = SCK_UP ; USICR = SCK_DN ;
159 USICR = SCK_UP ; USICR = SCK_DN ;
160 USICR = SCK_UP ; USICR = SCK_DN ;
161 USICR = SCK_UP ; USICR = SCK_DN ;
162
163 USIDR = byteArray [ 1 ];
164
165 USICR = SCK_UP ; USICR = SCK_DN ;
166 USICR = SCK_UP ; USICR = SCK_DN ;
167 USICR = SCK_UP ; USICR = SCK_DN ;
168 USICR = SCK_UP ; USICR = SCK_DN ;
169
170 USICR = SCK_UP ; USICR = SCK_DN ;
171 USICR = SCK_UP ; USICR = SCK_DN ;
172 USICR = SCK_UP ; USICR = SCK_DN ;
173 USICR = SCK_UP ; USICR = SCK_DN ;
174
175 USIDR = byteArray [ 0 ];
176
177 USICR = SCK_UP ; USICR = SCK_DN ;
178 USICR = SCK_UP ; USICR = SCK_DN ;
179 USICR = SCK_UP ; USICR = SCK_DN ;
180 USICR = SCK_UP ; USICR = SCK_DN ;
181
182 USICR = SCK_UP ; USICR = SCK_DN ;
183 USICR = SCK_UP ; USICR = SCK_DN ;
184 USICR = SCK_UP ; USICR = SCK_DN ;
185 USICR = SCK_UP ; USICR = SCK_DN ;
186
187 PORTB &= ~ _BV ( PB4 ); // CS still down
188 PORTB |= _BV ( PB4 ); // CS up
189
190 USIDR = 0 ;
191 }
192 #else
193 uint8_t pwm [ 21 ];
194 uint8_t testCount ;
195 void shiftOut ( uint32_t bits )
196 {
197 uint8_t seg ;
198 uint8_t i ;
199 char color [] = { 'R' , 'B' , 'G' };
200 for ( seg = 0 ; seg < 7 ; seg ++ )
201 {
202 for ( i = 0 ; i < 3 ; i ++ )
203 {
204 uint8_t bit = ( seg * 3 + i );
205 if ( bits & ( 1 << bit ))
206 {
207 //printf("%c", color[i]);
208 pwm [ bit ] ++ ;
209 }
210 else
211 {
212 //printf(" ");
213 }
214 }
215 //printf(" ");
216 }
217 //printf("\r");
218 testCount ++ ;
219 if (( testCount & 0xff ) == 0 )
220 {
221 //printf("\n");
222 for ( seg = 0 ; seg < 7 ; seg ++ )
223 {
224 for ( i = 0 ; i < 3 ; i ++ )
225 {
226 uint8_t bit = ( seg * 3 + i );
227 printf ( "%02x" , pwm [ bit ]);
228 pwm [ bit ] = 0 ;
229 }
230 printf ( " " );
231 }
232 printf ( " \r\n " );
233 }
234 }
235 #endif
236
237 // The following scale functions convert 0-255 to the PWM value that is compared to 0-255 sawtooth.
238 // Red LEDs have a 1.7V forward voltage so at 32ohms, a pulse is 100mA which is allowed with a
239 // duty cycle of 1/10. So 255 corresponds to about 25
240 // Blue LEDs, have a3.4 forward voltage, so at 32 ohms, a pulse is 50mA which slighlty over the
241 // rated 100% duty cycle current of 40mA. so 255 corresponds to about 192
242
243 uint8_t redScale ( uint8_t raw )
244 {
245 return raw >> 3 ;
246 }
247
248 uint8_t greenScale ( uint8_t raw )
249 {
250 return raw >> 1 ;
251 }
252
253 uint8_t blueScale ( uint8_t raw )
254 {
255 return raw ;
256 }
257
258 #define RED_BIT_MASK 0x049249UL
259 #define GREEN_BIT_MASK 0x092492UL
260 #define BLUE_BIT_MASK 0x124924UL
261 uint32_t remappedRedMask ;
262 uint32_t remappedGreenMask ;
263 uint32_t remappedBlueMask ;
264
265
266 // remaps the color map RGB * 1-7 to pins
267 uint32_t remap ( uint32_t colormap )
268 {
269 /*
270 static const uint8_t pinByIndex[21] = {
271 17, 18, 19,
272 20, 21, 24,
273 25, 26, 27,
274 28, 29, 30,
275 31, 36, 37,
276 38, 39, 40,
277 41, 42, 43
278 };
279 static const uint8_t ledPinByIndex[21] = {
280 23, 22, 21,
281 25, 16, 24,
282 6, 11, 5,
283 4, 3, 2,
284 10, 1, 9,
285 14, 13, 18,
286 20, 15, 19
287 };
288 */
289
290 // Reindex converts seg.RGB into a bit to be shifted into
291 // the shift register
292 static const uint32_t reindex [ 21 ] = {
293 // red green blue
294 ( 1UL << 18 ), ( 1UL << 17 ), ( 1UL << 16 ), // seg a
295 ( 1UL << 20 ), ( 1UL << 12 ), ( 1UL << 19 ), // seg b
296 ( 1UL << 5 ), ( 1UL << 8 ), ( 1UL << 4 ), // seg c
297 ( 1UL << 3 ), ( 1UL << 2 ), ( 1UL << 1 ), // seg d
298 ( 1UL << 7 ), ( 1UL << 0 ), ( 1UL << 6 ), // seg e
299 ( 1UL << 10 ), ( 1UL << 9 ), ( 1UL << 13 ), // seg f
300 ( 1UL << 15 ), ( 1UL << 11 ), ( 1UL << 14 ) // seg g
301
302 //(1UL << 18), (1UL << 16), (1UL << 17), // seg a
303 //(1UL << 20), (1UL << 19), (1UL << 12), // seg b
304 //(1UL << 5), (1UL << 4), (1UL << 8), // seg c
305 //(1UL << 3), (1UL << 1), (1UL << 2), // seg d
306 //(1UL << 7), (1UL << 6), (1UL << 0), // seg e
307 //(1UL << 10), (1UL << 13), (1UL << 9), // seg f
308 //(1UL << 15), (1UL << 14), (1UL << 11) // seg g
309 };
310
311 //return colormap;
312
313 uint32_t remap = 0 ;
314 int i ;
315 for ( i = 0 ; i < 21 ; i ++ )
316 {
317 if ( colormap & 1 )
318 {
319 remap |= reindex [ i ];
320 }
321 colormap >>= 1 ;
322 }
323 return remap ;
324 }
325
326 uint8_t trim ( uint8_t raw )
327 {
328 return raw ;
329 }
330
331 // tick controls the colors. There is a -15 counter.
332 // When the color value / 16 is less than the counter, then the LED
333 // is off.
334 void tick ( uint32_t ticks )
335 {
336 uint8_t seg ;
337 uint8_t count ;
338 uint8_t decomposition [ 3 ];
339 uint8_t decomp ;
340 uint32_t bits = 0 ;
341 uint16_t i ;
342
343 count = trim ( counter );
344 counter ++ ;
345
346 // Saw tooth 0 - 256
347 for ( i = 0 ; i < 256 ; i += 20 )
348 {
349 count = i ;
350 for ( seg = 0 ; seg < 7 ; seg ++ )
351 {
352 decomposeColor ( angles [ seg ], decomposition );
353
354 decomp = redScale ( decomposition [ 0 ]);
355 decomp = trim ( decomp );
356 bits >>= 1 ;
357 if ( decomp > count )
358 {
359 bits |= 0x100000UL ;
360 }
361 // increment after each bit. This will allow the LEDs to
362 // load more evenly, rather than focus around the low
363 // end of 'count'
364 count = trim ( count + 11 );
365
366 decomp = greenScale ( decomposition [ 1 ]);
367 decomp = trim ( decomp );
368 bits >>= 1 ;
369 if ( decomp > count )
370 {
371 bits |= 0x100000UL ;
372 }
373 // increment after each bit. This will allow the LEDs to
374 // load more evenly, rather than focus around the low
375 // end of 'count'
376 count = trim ( count + 11 );
377
378 decomp = blueScale ( decomposition [ 2 ]);
379 decomp = trim ( decomp );
380 bits >>= 1 ;
381 if ( decomp > count )
382 {
383 bits |= 0x100000UL ;
384 }
385 // increment after each bit. This will allow the LEDs to
386 // load more evenly, rather than focus around the low
387 // end of 'count'
388 count = trim ( count + 11 );
389
390 }
391
392
393 // TODO delay should be proportional to the number of LEDs. Add bit counter
394
395 // output each part so that LED FW voltages are matched.
396 #ifndef TEST
397 // remap for more convenient layout.
398 bits = remap ( bits );
399
400 //shiftOut(0xe7e7e);
401
402 shiftOut ( bits & remappedRedMask );
403 //_delay_loop_2(20);
404 shiftOut ( bits & remappedGreenMask );
405 //_delay_loop_2(20);
406 shiftOut ( bits & remappedBlueMask );
407 //_delay_loop_2(20);
408 shiftOut ( 0 );
409 #else
410 shiftOut ( bits );
411 #endif
412 }
413 // slowly rotate the segment color wheels
414 // every time ticks 4.096 seconds
415 const uint32_t tickMask = 0x1f ;
416 //const uint32_t tickMask = 0x00ff;
417
418 if (( ticks & tickMask ) == 0 )
419 {
420 for ( seg = 0 ; seg < 7 ; seg ++ )
421 {
422 angles [ seg ] += 3 ;
423 if ( angles [ seg ] >= 240 )
424 {
425 angles [ seg ] = 0 ;
426 }
427 }
428 }
429 }
430 // initialize angles on color wheel for each segment. Evenly
431 // distribute across wheel
432 // RRRRRR------
433 // ----BBBBBB--
434 // GG------GGGG
435 void init ( void )
436 {
437 uint8_t seg ;
438
439 #ifndef TEST
440 // Set CS, SCK, and SDA to output
441 PORTB = _BV ( PB1 ) | _BV ( PB2 ) | _BV ( PB4 );
442 DDRB = _BV ( PB1 ) | _BV ( PB2 ) | _BV ( PB4 );
443 #endif
444
445 shiftOut ( 0 );
446
447 for ( seg = 0 ; seg < 7 ; seg ++ )
448 {
449 // * 32. i.e. 0, 32, 64, 96, 128, 160, 192
450 angles [ seg ] = seg << 5 ;
451 }
452 counter = 0 ;
453
454 remappedRedMask = remap ( RED_BIT_MASK );
455 remappedGreenMask = remap ( GREEN_BIT_MASK );
456 remappedBlueMask = remap ( BLUE_BIT_MASK );
457
458 }
459
460
461 #ifndef TEST
462 int main ( void )
463 {
464 // system setup: pin direction/config, ticks
465 uint32_t ticks = 0 ;
466
467 init ();
468
469 while ( 1 )
470 {
471 tick ( ticks ++ );
472 }
473
474
475 return 0 ;
476 }
477 #else
478
479 void decompTest ()
480 {
481 int i ;
482 uint8_t rgb [ 3 ];
483 for ( i = 0 ; i < 256 ; i ++ )
484 {
485 decomposeColor (( uint8_t ) i , rgb );
486 printf ( "%d [%d]: %02d %02d %02d \r\n " , i , ( i / 2 ) * 3 , rgb [ 0 ], rgb [ 1 ], rgb [ 2 ]);
487 }
488 }
489 void main ()
490 {
491 uint32_t ticks = 0 ;
492 init ();
493 //decompTest();
494
495 while ( 1 )
496 {
497 tick ( ticks ++ );
498 }
499
500 }
501 #endif
Here is the Verilog for the M4A5-32/64 (32/32 would probably be fine for this simple shift register):
1 // shiftReg.v
2 //
3 // This verilog file describes a shift register with a strobe.
4 //
5
6 module ShiftRegister21 (
7 input pin_nRESET ,
8 input pin_nCS ,
9 input pin_SDA ,
10 input pin_SCK ,
11 output [ 20 : 0 ] pins_shift
12 );
13 reg [ 20 : 0 ] shift = 21 'b000000000000000000000 ;
14 reg [ 20 : 0 ] shiftOut = 21 'b000000000000000000000 ;
15
16 // output as long as RESET# is not asserted
17 assign pins_shift [ 20 : 0 ] = ( pin_nRESET == 1 'b1 ) ? ~ shiftOut [ 20 : 0 ] : 21 ' bzzzzzzzzzzzzzzzzzzzzz ;
18 //assign pins_shift[0] = (pin_nRESET & shiftOut[0]) ? 1'b0 : 1'bz;
19 //assign pins_shift[1] = (pin_nRESET & shiftOut[1]) ? 1'b0 : 1'bz;
20 //assign pins_shift[2] = (pin_nRESET & shiftOut[2]) ? 1'b0 : 1'bz;
21 //assign pins_shift[3] = (pin_nRESET & shiftOut[3]) ? 1'b0 : 1'bz;
22 //assign pins_shift[4] = (pin_nRESET & shiftOut[4]) ? 1'b0 : 1'bz;
23 //assign pins_shift[5] = (pin_nRESET & shiftOut[5]) ? 1'b0 : 1'bz;
24 //assign pins_shift[6] = (pin_nRESET & shiftOut[6]) ? 1'b0 : 1'bz;
25 //assign pins_shift[7] = (pin_nRESET & shiftOut[7]) ? 1'b0 : 1'bz;
26 //assign pins_shift[8] = (pin_nRESET & shiftOut[8]) ? 1'b0 : 1'bz;
27 //assign pins_shift[9] = (pin_nRESET & shiftOut[9]) ? 1'b0 : 1'bz;
28 //assign pins_shift[10] = (pin_nRESET & shiftOut[10]) ? 1'b0 : 1'bz;
29 //assign pins_shift[11] = (pin_nRESET & shiftOut[11]) ? 1'b0 : 1'bz;
30 //assign pins_shift[12] = (pin_nRESET & shiftOut[12]) ? 1'b0 : 1'bz;
31 //assign pins_shift[13] = (pin_nRESET & shiftOut[13]) ? 1'b0 : 1'bz;
32 //assign pins_shift[14] = (pin_nRESET & shiftOut[14]) ? 1'b0 : 1'bz;
33 //assign pins_shift[15] = (pin_nRESET & shiftOut[15]) ? 1'b0 : 1'bz;
34 //assign pins_shift[16] = (pin_nRESET & shiftOut[16]) ? 1'b0 : 1'bz;
35 //assign pins_shift[17] = (pin_nRESET & shiftOut[17]) ? 1'b0 : 1'bz;
36 //assign pins_shift[18] = (pin_nRESET & shiftOut[18]) ? 1'b0 : 1'bz;
37 //assign pins_shift[19] = (pin_nRESET & shiftOut[19]) ? 1'b0 : 1'bz;
38 //assign pins_shift[20] = (pin_nRESET & shiftOut[20]) ? 1'b0 : 1'bz;
39
40 // strobe shift register to output registers on CS# rising edge
41 always @( posedge pin_nCS ) begin
42 shiftOut [ 20 : 0 ] <= shift [ 20 : 0 ];
43 end
44
45 // shift SDA into shift register on rising edge of SCK when CS# is low
46 // or set whole shift register to 0 on RESET# low
47 always @( posedge pin_SCK ) begin
48 if ( pin_nRESET == 1 'b0 ) begin
49 shift [ 20 : 0 ] <= 21 'b111111111111111111111 ;
50 end
51 else begin
52 if ( pin_nCS == 1 'b0 ) begin
53 shift [ 20 : 0 ] <= { shift [ 19 : 0 ], pin_SDA };
54 end
55 end
56 end
57
58 endmodule
I tried to take a video, but the pulsing of LEDs works on the human eye differently than on video. It would look better on video if each LED had its own resistor and was grounded by devices able to sink enough current (30-40mA continuous). The way I did it takes far fewer components.
I am at a stage where I could, if I knew how, make a small PCB so that I could use smaller surface mount parts. The display itself is fairly large--the digit is about an inch high, so a 2 sided board could fit all the components in the same surface area under the display and behind it.