Using an ATtiny2313/4313 as SPI master
If you're building smaller projects, the ATtiny2313 chip is a nice one, as it still comes with a real UART - something the other tiny chips don't have.
Now what if you want to talk to SPI slaves, such as LED drivers or ordinary shift registers? Of course you can use simple bit-banging, but this won't give you the maximum possible speed.
As you may know the ATtiny chips come with an USI module, which can be made to work as an SPI (master/slave) device, or even a I²C device as well. The latter is a bit tricky though for beginners. There's an article + library on the playground as well.
Recommended include files:
#include <avr/io.h>
#include <inttypes.h>
#include <avr/interrupt.h>
#include <util/delay.h>
Setup for the DO,DI,USICLK pins:
/*
* DON'T use the MOSI/MISO pins. They're for ISP programming only!
* Read the datasheet.
*/
DDRB |= _BV(PB4); // as output (latch)
DDRB |= _BV(PB6); // as output (DO) - data out
DDRB |= _BV(PB7); // as output (USISCK) - clock
DDRB &= ~_BV(PB5); // as input (DI) - data in
PORTB |= _BV(PB5); // pullup on (DI)
SPI send function:
uint8_t spi_transfer(uint8_t data) {
USIDR = data;
USISR = _BV(USIOIF); // clear flag
while ( (USISR & _BV(USIOIF)) == 0 ) {
USICR = (1<<USIWM0)|(1<<USICS1)|(1<<USICLK)|(1<<USITC);
}
return USIDR;
}
Example - sending bytes to a LED driver / shift register:
#define F_CPU 8000000UL
#include <avr/io.h>
#include <inttypes.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#define __LATCH_LOW PORTB &= ~(1 << PB4)
#define __LATCH_HIGH PORTB |= (1 << PB4)
void setup(void);
void loop(void);
uint8_t spi_transfer(uint8_t data);
int main(void);
void setup(void) {
DDRB |= _BV(PB0); // set LED pin as output
PORTB |= _BV(PB0); // turn the LED on
// USI stuff
DDRB |= _BV(PB4); // as output (latch)
DDRB |= _BV(PB6); // as output (DO)
DDRB |= _BV(PB7); // as output (USISCK)
DDRB &= ~_BV(PB5); // as input (DI)
PORTB |= _BV(PB5); // pullup on (DI)
}
void loop(void) {
__LATCH_LOW;
spi_transfer(0x01); // channel 1 active (red)
__LATCH_HIGH;
_delay_ms(500);
PORTB ^= _BV(PB0); // toggle LED
__LATCH_LOW;
spi_transfer(0x02); // channel 2 active (green)
__LATCH_HIGH;
_delay_ms(500);
PORTB ^= _BV(PB0); // toggle LED
__LATCH_LOW;
spi_transfer(0x04); // channel 3 active (blue)
__LATCH_HIGH;
_delay_ms(500);
PORTB ^= _BV(PB0); // toggle LED
__LATCH_LOW;
spi_transfer(0x07); // channels 1,2,3 active (white)
__LATCH_HIGH;
_delay_ms(500);
PORTB ^= _BV(PB0); // toggle LED
__LATCH_LOW;
spi_transfer(0x00); // all outputs off
__LATCH_HIGH;
_delay_ms(500);
PORTB ^= _BV(PB0); // toggle LED
}
int main(void) {
setup();
for(;;) {
loop();
}
};
/*
Functions dealing with hardware specific jobs / settings
*/
uint8_t spi_transfer(uint8_t data) {
USIDR = data;
USISR = _BV(USIOIF); // clear flag
while ( (USISR & _BV(USIOIF)) == 0 ) {
USICR = (1<<USIWM0)|(1<<USICS1)|(1<<USICLK)|(1<<USITC);
}
return USIDR;
}
In this example there's an LED + current limiting resistor connected to PB0 and GND. PB0 is physical pin #10 on the chip.
Fuse settings:
Chips fresh from the factory have the 'CLKDIV8' fuse enabled. If you plan to use the chip running at 8MHz (internal RC oscillator or external resonator), this fuse must be adapted.
I recommend this fuse calculator + reading the datasheet.
http://www.engbedded.com/fusecalc
For an external 8MHz ceramic resonator and brownout reset at 2.7V, the fuse settings can be set with:
- avrdude -c usbtiny -p attiny2313 -U lfuse:w:0xDC:m -U hfuse:w:0xDB:m
Make sure these settings match your project by verifying them using the fuse calculator!
Compilation & upload:
- avr-gcc -Os -g -fno-exceptions -ffunction-sections -fdata-sections -mmcu=attiny2313 ./code.c -o code.elf
- avr-objcopy -O ihex code.elf code.hex
- avr-objdump -D -S -s code.elf
- avr-size code.elf
- avrdude -c usbtiny -p attiny2313 -e -U flash:w:code.elf:i