Introduction: Getting More I/O Pins on ATTiny With Shift Registers

If you previously worked(or currently working) with small 8-bit microcontrollers, like ATTiny or PIC12, you've probably encountered a fundamental problem of not having enough GPIO pins for your needs or project requirements.

Upgrading to a larger MCU is only one of the options, but as usual there is an alternative. In this article I will explain how to use shift registers in some common situations in order to expand the I/O capacity of your microcontroller. As an example I will use an ATTiny13A and a 74HC595 shift register.

Step 1: ​First Date

Shift register is a semiconductor device which accepts serial input and produces parallel or serial output depending on how you use it. Also, as almost all semiconductor ICs, shift registers have gotten very cheap, so instead of spending relatively small sum of +$1.00 on a single different microcontroller you should get a handful of those 74HC595's at $0.12 apiece.

74HC595 is an 8-bit serial-in, serial or parallel-out shift register with output latch and 3-state output. In order to understand what it is let's look at the functional diagram of this device:

We feed serial input bit-by-bit through DS pin, while providing clock signal to SHCP pin in order for shift register to shift its contents. If we want to save the 8-bit input data, we have to toggle STCP pin to latch the contents onto the storage register. This gives us the ability to "hold" the old output while feeding 8 bits of the next output. Pin Q7S is used for cascading, which means if we have 2 shift registers and we connect Q7S output of the first one to the DS pin of the second one, we get a 16-bit shift register! And, as you should've noticed, we only need 3 pins to use its essential functions (DS, SHCP and STCP).

A pinout diagram for our shift register is shown above.

Most of the pins were described earlier, but there are 2 more left:

  • OE (Output Enable): is active low. When set to 1 it disables the output and sets pins Q0...Q7 to a high-impedance state.
  • MR(Master Reset): is also active low. When set to 1 it clears the contents of the shift register (not the latch).

We are not using either of these pins for this project, so OE must be connected to ground, while MR is connected to VCC, which will keep output pins in the on-state all the time and will prevent shift register from resetting. In order to clear the contents of the shift register we are just going to send it the value 0x00 just like regular data. To learn more about this device please read the 74HC595 datasheet.

Step 2: Example #1 [OUTPUT]

Code

We are going to read an 8-bit value from ADC3 (most significant bits) and then pass the corresponding output value to our shift register.

To be more specific I've made a table of values for I/O states:

ADC3 valueOutput valueSegments on
0..400xFF0
41..800xFE1
81..1200xFC2
121..1600xF83
161..2000xF04
201..2550xE05

NOTE: Because it is a common anode LED indicator we need to set the appropriate pin LOW whenever we want it to light up.

Now, let's write some code.

In short, we need to set up a Timer Overflow interrupt, which will read the current state of ADC3 and send the corresponding value to our shift register approximately every ~27ms.

SEND_BYTE subroutine sends an 8-bit stream of data and latches it in the shift register. This is a bit simplified function, but with some minor improvements can be used universally.

I wrote most numbers in binary form, so it will be easier (at least for me) to see which LEDs are enabled and which flags are set.

Step 3: Example #1: Code

The code is written in AVR Assembly. If you work with controllers like ATTiny - it is a must! It looks scary, but in reality it's much simpler and easier than C.

Another reason for using assembly language is that this code only needs 148 bytes of space after compilation, while Arduino IDE produces 644 bytes of binary output for the same code (haven't tested in AVR GCC), so if you want to expand functionality of your Tiny project, you have less than 400 bytes left to work with...

/*
 *	Shift register demo #1
 *	
 *	ATTiny13A Running @9.6MHz
 *	ADC running @150kHz
 *
 *	PIN ASSIGNMENT:
 *		PB0 - Shift Register Clock
 *		PB1 - Shift Register Serial Data
 *		PB2 - Shift Register Latch(Store)
 *		PB3 - ADC3 (Potentiometer input)
 *		PB4 - [NOT USED]
 *		PB5 - RESET
 */ 
.include "tn13Adef.inc"
.def	A = R16	; g.p. variable and/or function argument
.def	B = R17	; Used in SEND_BYTE and ADC_START as temporary storage
.def	LED = R18	; stores current LED output
.def	BCT = R19	; Bit counter for SEND_BYTE
.equ	SRCK = 0	; PB0 = Clock
.equ	SRDA = 1	; PB1 = Serial Data
.equ	SRLC = 2	; PB2 = Latch

/*	INTERRUPT VECTORS	*/
.org	0x0000
rjmp	RESET		; Reset interrupt
.org	0x0003
rjmp	TC0_OV		; Timer1 interrupt

/*
 *	START!!!
 */
RESET:
        /*	SETUP STACK	*/
	ldi	A, low(RAMEND)	; Set stack pointer
	out	SPL, A
	/*	SETUP PINS	*/
	ldi	A,0b0000_0111	; Set output pins PB0..PB2
	out	DDRB,A			
	/*	SETUP TIMER1	*/
	ldi	A,0b0000_0101   ; Set Timer Prescaler (1024)
	out	TCCR0B,A	; This will cause Timer Interrupt every ~27ms
	ldi	A,0b00000010	; Enable Timer0 Overflow Interrupt
	out	TIMSK0,A
	/*	SETUP ADC3	*/
	ldi	A,0
	out	ADCSRB,A	; Disable autotrigger(Free running)
	ldi	A,0b00001000	; Disable Digital Input on PB3(ADC3)
	out	DIDR0,A
	ldi	A,0b00000011
	out	ADMUX,A		; Source:ADC3, Align:RIGHT, Reference:VCC.
	ldi	A,0b10000110
	out	ADCSRA,A	; Enable ADC with prescale 1/64
	/*	RESET REGISTERS	*/
	ldi	A,0x00		; clear A
	ldi	LED,0xFF	; Set all LED's to OFF(1-off, 0-on)
	rcall	SEND_BYTE	; Clear display
	sei			; Enable interrupts

/*	Main loop	*/
MAIN:
	rjmp	MAIN

/*	
 *	Sends 8-bit data from LED register to Shift Register
 */
SEND_BYTE:
	ldi	BCT,0b1000_0000	; Set Bit counter
next_bit:
	mov	B,LED		; Move data byte to temp
	and	B,BCT		; Check bit
	breq	zero		; Set Data to 0
	sbi	PortB,SRDA	; Set Data to 1
	rjmp	shift		; shift
zero:
	cbi	PortB,SRDA
shift:
	sbi	PortB,SRCK	; CLK up
	nop
	cbi	PortB,SRCK	; CLK down
	clc			; Clear Carry flag
	ror	BCT		; Shift bit counter
	brne	next_bit	; Next iteration
	sbi	PortB,SRLC	; When done, Latch
	nop
	cbi	PortB,SRLC
	ret			; Done
/*	Start ADC conversion. Saves result to A	*/
ADC_START:
        sbi	ADCSRA,ADSC	; Start ADC conversion
adc_wait:
	sbic	ADCSRA,ADSC	; Check conversion status
	rjmp	adc_wait	; Skip jump if completed
	in	A,ADCL		; Get low bits
	in	B,ADCH		; Get high bits
	lsr     B		; Shift 2 bits to the right
	ror A			; through Carry
	lsr B
	ror A
	ret
/*	Timer 0 overflow interrupt	*/
TC0_OV:
	rcall	ADC_START	; start ADC0 Conversion
	/* Compare Input, Set output */ 
	cpi	A,0xC8		; A>=200?
	brlo	gt_160
	ldi	LED,0b11100000
	rjmp	sr_write
gt_160:				; A>=160?
	cpi	A,0xA0
	brlo	gt_120
	ldi	LED,0b11110000
	rjmp	sr_write
gt_120:				; A>=120?
	cpi	A,0x78
	brlo	gt_80
	ldi	LED,0b11111000
	rjmp	sr_write
gt_80:				; A>=80?
	cpi	A,0x50
	brlo	gt_40
	ldi	LED,0b11111100
	rjmp	sr_write
gt_40:				; A>=40?
	cpi	A,0x28
	brlo	lt_40
	ldi	LED,0b11111110
	rjmp	sr_write
lt_40:				; A<40
	ldi	LED,0b11111111
sr_write:
	rcall	SEND_BYTE	; Send byte to shift reg.
	reti			; return

Step 4: Example #2: [INPUT]

As strange as it sounds, handling multiple digital inputs with a shift register is almost the same as handling multiple outputs. Let's look at the circuit first, so I can explain how it works.

Diodes are added to protect the outputs of the shift register, since multiple HIGH inputs may cause a short circuit. PB3 is connected to the ground through a 10K resistor (logical 0 when no match found).

The general idea is to send a certain set of data bits to the shift register and if there is a bitwise match with the input - we will get HIGH signal on PB3. For example, we have an 8-bit input 0x91, which is 10010001 in binary.

We start with sending 0x01 to the shift register (0b00000001) and see if the first bit is 1. If we have a match (PB3 is HIGH), we perform OR operation of the input to the result. Next, we shift the test data 1 bit to the left, so we get 0x02 (0b00000010) and repeat the procedure to acquire the second bit, which gives no match and results in logical 0... and so on until we test all 8 bits.

Shift RegisterPB3Result
00000001100000001
00000010000000001
00000100000000001
00001000000000001
00010000100010001
00100000000010001
01000000000010001
10000000110010001

This technique allows to read reasonably large array of inputs at the cost of acquisition speed only. It does not require any additional pins, so it is a perfect solution for low-speed applications, like keypads, switchboards, or even low-speed digital sensors. The number of used pins can be further reduced, if we alternate Serial Data pin of the microcontroller between digital output and digital input(instead of PB3).

For our next example we will use almost identical circuit, but instead of digital inputs and transistors we will use simple tactile switches.

Step 5: Example #2: CODE

This code is much smaller and simpler than the previous example, because this time we are not using ADC. As you can see, SEND_BYTE subroutine is unchanged and does pretty much the same thing.

/*
 *	Shift register demo #2
 *	ATTiny13A Running @9.6MHz
 *
 *	PIN ASSIGNMENT:
 *		PB0 - Shift Register Clock
 *		PB1 - Shift Register Serial Data
 *		PB2 - Shift Register Latch(Store)
 *		PB3 - Digital input(bit match)
 *		PB4 - LED
 *		PB5 - RST
 *
 */ 

.include "tn13Adef.inc"

.def	A = R16		; g.p. variable and/or function argument
.def	B = R17		; Used in SEND_BYTE and ADC_START as temporary storage
.def	LED = R18	; stores current LED output
.def	BCT = R19	; Bit counter for SEND_BYTE
.def	TIM = R20	; Stores how many iterations of TOV0 have passed
.def	TMP = R21
.equ	SRCK = 0	; PB0 = Clock
.equ	SRDA = 1	; PB1 = Serial Data
.equ	SRLC = 2	; PB2 = Latch

/*	INTERRUPT VECTORS	*/
.org	0x0000		; Reset interrupt
rjmp	RESET
.org	0x0003
rjmp	TC0_OV		; Timer0 Overflow interrupt

/*
 *	START!!!
 */

RESET:
	/*	SETUP STACK	*/
	ldi	A, low(RAMEND)	; Set stack pointer
	out	SPL, A
	/*	SETUP PINS	*/
	ldi	A,0b0001_0111	; Set output pins PB0..PB2(CLK,DATA,LATCH)
	out	DDRB,A		; PB4 - LED output
	/*	SETUP TIMER0	*/
	ldi	A,0b0000_0101   ; Set Timer Prescaler 1/1024
	out	TCCR0B,A	; Interrupt every ~27ms
	ldi	A,0b00000010	; Enable Timer0 Overflow Interrupt
	out	TIMSK0,A	;
	/*	RESET REGISTERS	*/
	ldi	A,0x00		; clear A
	ldi	LED,0x10	; Default blink speed(~1Hz)
	sei			; Enable interrupts

/*	Main loop	*/
MAIN:
	ldi	A,1		; Set the first bit
	ldi	TMP,0		; Temporary storage for new LED delay
next:
	rcall	SEND_BYTE	; Send A to Shift Reg.
	sbic	PINB,3		; Check for match
	or	TMP,A		; Add it to result
	clc			; Clear carry
	rol	A		; Rotate A
	breq	check		; If A==0, check and start over
	rjmp	next		; else get next bit
check:				; Only assign non-zero values
	tst	TMP		; TMP==0?
	breq	MAIN		; skip
	mov	LED,TMP		; else assign new LED delay
	rjmp	MAIN

/*		
 *	Sends 8-bit data from A register to Shift Register
 *      (Same as in the prev. example)
 */
SEND_BYTE:
	ldi	BCT,0b1000_0000	; Set Bit counter
next_bit:
	mov	B,A		; Move data byte to temp
	and	B,BCT		; Check bit
	breq	zero		; Skip if 0
	sbi	PortB,SRDA	; Send Data
	rjmp	shift		; shift right
zero:
	cbi	PortB,SRDA
shift:
	sbi	PortB,SRCK	; CLK up
	nop
	cbi	PortB,SRCK	; CLK down
	clc			; Clear Carry flag
	ror	BCT		; Shift bit counter
	brne	next_bit	; Next iteration
	sbi	PortB,SRLC	; When done, Latch
	nop
	cbi	PortB,SRLC
	ret			; Done

/*	Timer 0 overflow interrupt	*/
TC0_OV:
	inc	TIM		; TIM++
	cp	TIM,LED		; TIM>LED?
	brlo	early		; too early
	push	TMP		; Save old TMP value
	in	TIM, PINB	; Read current port state
	ldi	TMP, 0x10	;
	eor	TIM,TMP		; Toggle PB4
	out	PORTB, TIM	; 
	ldi	TIM,0		; Reset counter
	pop	TMP		; Restore TMP
early:
	reti			; return

Step 6: Additional Resources

If you are new to AVR Assembly and you feel intimidated by cryptic opcodes and registers, but you think you are ready to conquer the world of microcontrollers, these are good places to start:

Comments

author
illumation_ made it!(author)2017-01-21

Not to be negative, but wouldn't it easier to charlieplex the display instead? You can drive 6 leds with 3 pins without any extra chip.

author
silentbogo made it!(author)2017-01-21

but with daisy-chained shift registers you can drive hundreds, or even thousands. It's just a concept, that I wanted to show a few years back.

It all started with a work project, which had to be cheap and include over 50 LED segments, 5 buttons and 4 relays. Spent a whole week learning assembly to make it work, but it paid off in a long run. The entire code with self-test routines fit in 1KB with some room to spare, and the PCB layout came out a lot easier, since every 6-8 segments were only connected to a 74HC595, while the rest was hanging on the same serial bus.

And there's also an aspect of power. You cannot drive more than a few LEDs at full brightness directly from a microcontroller.

author
mrandle made it!(author)2015-05-01

This is basically the only way to drive 7 segment displays without a chip that does it specifically. For a basic clock that's 28 IO pins! great job explaining this.

author
pmck made it!(author)2015-04-30

Not to be negative, but why not just use a larger micro if you need the io? adding a shift register will take up as much space as a single 328

author
diy_bloke made it!(author)2015-05-01

you could do that indeed, especially since an entire pro mini arduino clone is around 1.60 euro now, but if space is at premium and say an attiny10 costs 33 euro cnts and a 595 can be get for 7 cents..........

author
silentbogo made it!(author)2015-04-30

That's a good question. If you only need to operate with <20 IO pins and you have a capable microcontroller - it is cool.

When you need to handle 30-60 IO pins, even large microcontrollers are not able to provide enough...

There are also several other very important reasons:

1) The price difference between ATTiny and ATMega will get you more than a dozen of shift registers

2) It is much easier to create a PCB layout for an array of ICs, than dancing around a single MCU. All you need is to provide common CLOCK and LATCH signals and connect registers together through carry pin (but it all depends on application).

3) If you are working with LED arrays, then you should take in account current flow and power consumption. With a safe maximum of 100mA you can only drive about 10 LEDs off your ATMega328 (5mm at half power) without relying on additional components(transistors etc.) Each shift register has its own limit, but even the low-power rated ones can safely drive 8 abovementioned LED's at close to full brightness. There are also models with up to 700mA combined throughput.

4) This approach is very flexible and modular. It is widely used in LED tabloids, outdoor LED displays etc.

author
pmck made it!(author)2015-04-30

great points, makes a lot more sense to me now cheers

author
ArduinoGuido made it!(author)2015-04-30

Well done! I need to do this on an existing project.

Where did you buy that cool battery display?

author
silentbogo made it!(author)2015-04-30

Got it from here:

http://www.dx.com/p/pz-301-6-segment-battery-style...

I used to buy parts from these guys all the time, but now I just order parts locally from work (bulk quantities are much cheaper).

author
skepticaljay made it!(author)2015-04-29

This is exactly what I needed to complete my project.

author
silentbogo made it!(author)2015-04-29

Awesome! Please share it, when it's done )

author
diy_bloke made it!(author)2015-04-29

Very nice instructable. I have used a 164 Shift Register to add an LCD to an attiny and still had 3 pins of the attiny left :-)
I love shift registers

author
silentbogo made it!(author)2015-04-29

The last one I did was a puzzle box with ATTiny13A and 6 chained 74HC595 shift registers (an order from work). Only 4 pins were used on my microcontroller for both input and output: 34 LEDs and 5 buttons!

Those are very powerful and useful devices. At 100MHz shift-out speed you can use them even with fast dev. boards like Tiva C Launchpad or any other Cortex M3/M4 dev board without worrying about proper timings.

I'll try to post a video sometime soon.

author
simon.w.nordberg made it!(author)2015-04-02

This is great! Very useful!

author
tomatoskins made it!(author)2015-03-29

Awesome! I remember the days that I worked with Assembly. So much work, but it was fun to learn more about how these things work.

author
silentbogo made it!(author)2015-03-29

Thx!

That's what I like about assembler: it helps you to understand MCUs better and it makes more sense in that environment. I've only started re-learning electronics about a year ago, but unlike most people, I took my first steps by building digital logic blocks in Logisim and writing small simulated programs on paper with homebrew mini-compilers.

About This Instructable

14,804views

286favorites

License:

More by silentbogo:DIY Soldering StationDIY PCB lab for under $35.00Controlling Arduino with Gamepad
Add instructable to: