Tuesday, March 11, 2014

Theory: shift registers, and the test jig code explained!

Ahh... Shift registers. The heart of most LED cubes.

So, what is a shift register and what does it do? Basically it's a serial input, parallel output IC; you send a stream of ones and zeroes to it on one pin and this makes a bunch of output pins go high or low. Usually you have two more pins: a clock pin and a latch pin. The clock pin is used to tell the shift register when to read a new bit on its input pin. The latch pin is used to tell it when to make the read bits appear on the output pins.

In my cube I use the 74HC595 shift register which is an 8 bit shift register. It simply means that it has 8 outputs. You can find these for almost a dime a dozen on eBay. If you need the smaller SMD type, search for 74HC595D.
The 595 (as I'll refer to it from now on) has two internal registers: the shift register and the storage register. We'll start at the beginning - the input pin - and work our way down to the output pins.

When you want to send data to the 595 you start by setting the DS pin (Serial Data Input) high or low, depending on whether you want to write a one or zero. Then you make the SHCP pin (Shift Register Clock Input) go from low to high to low again. The shift register will update whenever that pin goes from low to high. It will store whatever value is at DS and put it in bit 0 of the shift register. All the other bits of the register are shifted to the next one. So bit 1 will get the old value of bit 0, bit 2 the old value of bit 1, etc etc. The cool thing about this is that the old value of bit 7 will appear at a pin called Q7S. If you connect this pin to the DS pin of a second 595, and also connect the SHCP pins of both 595s, then bit 7 of the first 595 will be shifted to bit 0 of the second 595. You can chain a whole bunch of registers like this, effectively forming one large shift register.

Great, now that you've shifted 8 bits into the 595, you'll want to have them appear at the output pins. To do this you pull the STCP (Storage Register Clock Input, otherwise known as Latch) pin from low to high to low again. Whenever this pin goes from low to high all values from the shift register will be copied to the storage register.
This storage register is connected to the output pins, so any value in it will appear on the output pins. But only if the OE (Output Enable) pin is pulled low. If OE is high all output pins will be low.

One thing that's important to know is that when both the STCP and SHCP pins go high simultaneously, the shift will take place after the contents of the shift register have been copied to the storage register. This means that when you have STCP and SHCP connected to eachother, and you shift in a bit, that this bit will not appear on the output directly (since the content of the shift register has been copied to the output register before the new bit was shifted in).

Let's have another look at my pillar testing code. For this I have connected STCP and SHCP to the same pin on the controller (B4) and DS to B3. OE has been connected to ground.


1:  /*  
2:   * TinyPillarTester.c  
3:   *  
4:   * Created: 6-3-2014 21:48:11  
5:   * Author: Daniel  
6:   */   
7:    
8:    
9:  #include <avr/io.h>  
10:  #include <avr/interrupt.h>  
11:    
12:  int main(void)  
13:  {  
14:      PORTB = 7;  
15:      DDRB = _BV(DDB0) | _BV(DDB1) | _BV(DDB2) | _BV(DDB3) | _BV(DDB4);  
16:    
17:      TCNT0 = 0;  
18:      TIMSK0 = (1 << OCIE0A); // interrupt on compare match  
19:      TCCR0A = _BV(WGM01);  
20:      TCCR0B = _BV(CS01); // F_CPU / 8  
21:      OCR0A = 250;  
22:      asm("sei");  
23:    while(1)  
24:    {  
25:        // do nothing!  
26:        asm("sleep");  
27:    }  
28:  }  
29:    
30:  uint8_t cathodeCounter = 4;  
31:  uint8_t anodeCounter = 0;  
32:    
33:  ISR(TIM0_COMPA_vect, ISR_NAKED)  
34:  {  
35:      uint8_t sreg = SREG;  
36:      static uint8_t counter = 0;  
37:      ++counter;  
38:      if ( counter >= 75 )  
39:      {  
40:          counter = 0;  
41:          ++anodeCounter;  
42:          if ( anodeCounter == 8 )  
43:          {  
44:              anodeCounter = 0;  
45:              PORTB |= _BV(3);  
46:          }  
47:          else if ( anodeCounter == 1 )  
48:          {  
49:              PORTB |= cathodeCounter;  
50:              cathodeCounter <<= 1;  
51:              if ( cathodeCounter >= 8 )  
52:              {  
53:                  cathodeCounter = 1;  
54:              }  
55:              PORTB &= ~cathodeCounter;  
56:              PORTB &= ~_BV(3);  
57:          }  
58:          PORTB |= _BV(4);  
59:          PORTB &= ~_BV(4);  
60:      }  
61:      SREG = sreg;  
62:      asm("reti");  
63:  }  
64: 

Let's go through this line by line.

12:  int main(void)  
13:  {  
14:      PORTB = 7;  
15:      DDRB = _BV(DDB0) | _BV(DDB1) | _BV(DDB2) | _BV(DDB3) | _BV(DDB4);  
Line 14: Pins B0,B1 and B2 are set high, since they are connected to the cathodes of the LEDs this will make the LEDs go off. B3 (DS) and B4 (SHCP, STCP) are set low.
Line 15: All pins are configured as output pins (except B5, which is the reset pin)

17:      TCNT0 = 0;  
18:      TIMSK0 = (1 << OCIE0A); // interrupt on compare match  
19:      TCCR0A = _BV(WGM01);  
20:      TCCR0B = _BV(CS01); // F_CPU / 8  
21:      OCR0A = 250;  
Line 17-21: Timer initialization, it's set to generate an interrupt on compare match. The value to match is 250. The counter will reset when the match occurs (line 19) and its speed is 1/8th of the cpu speed (line 20).

22:      asm("sei");  
23:    while(1)  
24:    {  
25:        // do nothing!  
26:        asm("sleep");  
27:    }  
Line 22: Inline assembler, the "sei" command will enable interrupts
Line 23 - 27: Do nothing. The "sleep" command puts the cpu in idle state until there's an interrupt. This also reduces power consumption from 3 mA to 0.5 mA.

33:  ISR(TIM0_COMPA_vect, ISR_NAKED)  
34:  {  
35:      uint8_t sreg = SREG;  
Line 33: Start of interrupt handler for timer0 compare match. ISR_NAKED means the compiler won't put any code at the start and end specific to handling interrupts. One of the things you need do do is store the SREG register and restore it at the end, as you can see on line 35
Let's skip some stuff

61:      SREG = sreg;  
62:      asm("reti");  
63:  }  
64: 
Line 61: Restore SREG, not really necessary since we don't have any code in our main loop. But hey, it's good practice.
Line 62: Return from interrupt. Required special command if you exit an interrupt handler.

36:      static uint8_t counter = 0;  
37:      ++counter;  
38:      if ( counter >= 75 )  
39:      {  
40:          counter = 0;  
Line 36-40: The timer is configured to generate 600 interrupts per second, I only need 8, so this slows it down a bit :)

41:          ++anodeCounter;  
42:          if ( anodeCounter == 8 )  
43:          {  
44:              anodeCounter = 0;  
45:              PORTB |= _BV(3);  
46:          }  
Line 41: A counter to run through all the anodes. When it reaches 8, I reset it to 0. I also set B3 (DS) high (line 45).
47:          else if ( anodeCounter == 1 )  
48:          {
56:              PORTB &= ~_BV(3);  
57:          }  
Line 47: On the first interrupt (or the first one after the anode counter has been reset) I set B3 low again (line 56). It'll stay that way until the counter reaches 8 again. So it's high for 1 update, and low for 7.

58:          PORTB |= _BV(4);  
59:          PORTB &= ~_BV(4);  
Line 58: Set B4 (SHCP and STCP) high, this will cause the contents of the shift register to be copied to the storage register, and then shift a new bit from DS into the shift register.
Line 59: Set B4 low again.

And that's pretty much it. This clocks one bit to the shift register every 8th of a second, one one and seven zeroes. So one pin will be high and the others low, and the high pin will shift to the next every 8th of a second.

Now there's just a small piece of code left:
49:              PORTB |= cathodeCounter;  
50:              cathodeCounter <<= 1;  
51:              if ( cathodeCounter >= 8 )  
52:              {  
53:                  cathodeCounter = 1;  
54:              }  
55:              PORTB &= ~cathodeCounter;  
Line 49-55: This handles switching between red green and blue (cathodes). The cathodeCounter variable is a bit mask, only one bit will be set, which corresponds to one of the output pins B0, B1 or B2. I first apply this mask to the pins, which makes the current active pin go high (line 49) turning the LED off. Then I shift the variable left one bit to move to the next pin (line 50). If it goes past the last pin (line 51) I reset it back to the first pin (line 53). Finally I negate the mask and apply it, setting the new active pin low (line 55) turning the LED on.

The reason this piece of code sits inside the anodeCounter == 1 block is because the actual output of the 595 lags one clock cycle behind the content of the shift register. If I had put this code in the anodeCounter == 8 block then the jig would switch colors while the last LED was on. Since we don't want that, we have to wait one more cycle.

That's it! I hope this gives a better understanding of the 595 and the test jig :)

No comments:

Post a Comment