AVR SPI C Snippets
From OpenAVR
Contents |
[edit] What is SPI?
A general explanation is available at SPI article on Wikipedia.
The best source for AVR specific informations is the datasheet of the microcontroller that you are using.
[edit] Alternatives
SPI communication requires at least three signal wires, MOSI, MISO and SCK, plus an optional select line for each attached device. If that is too much for your application, you may consider TWI (I2C) as an alternative, which needs two signal wires only. However, SPI is less complicated and generally faster (often ~4 MHz, opposed to 400 kHz on I2C), specially when large amounts of data need to be transfered in both directions (for example Microchip ENC28J60 Ethernet chip).
[edit] Using the Runtime Library
AvrLibC does not provide generic routines for SPI. However, it does provide a number of definitions, which help to keep our code portable among different AVR targets. To make use of it, we need to include the related header file.
#include <avr/io.h>[edit] SPI Initialization
[edit] Initializing the Master
At least two bits need to be set in the SPI control register, the SPI enable bit and the master mode bit.
SPCR = _BV(SPE) | _BV(MSTR);
Unlike with some other peripherals, the output mode of the SCK and the MOSI port lines is not automatically enabled. We must explicitly set the data direction register. On the ATmega2561, SCK is at PB1 and MOSI is at PB2.
DDRB |= _BV(1) | _BV(2);
When running master mode, a low level at the slave select pin SS will force the device into slave mode. This may cause trouble, if the pin is configured for input and not connected (floating). In this case, enabling the internal pull-up resistor will be a good idea. For the ATmega2561, Pin SS is at PB0.
/* Check whether PB0 is an input. */ if (bit_is_clear(DDRB, 0)) { /* If yes, activate the pull-up. */ PORTB |= _BV(0); }
Doing the same for the MISO pin avoids receiving random data from a floating input. If activating the pull-up, we will always get 0xFF on a broken connection. MISO is at PB3 for the ATmega2561.
PORTB |= _BV(3);
Upon start, we should also make sure, that all status flags are cleared. This is done by reading the status and the data register.
unsigned char tmp; tmp = SPSR; tmp = SPDR;
When putting all together, the final initialization may look like this:
void SpiMasterInit(void) { unsigned char tmp; /* Check whether PB0 is an input. */ if (bit_is_clear(DDRB, 0)) { /* If yes, activate the pull-up. */ PORTB |= _BV(0); } /* Switch SCK and MOSI pins to output mode. */ DDRB |= _BV(1) | _BV(2); /* Enable MISO pull-up. */ PORTB |= _BV(3); /* Activate the SPI hardware. */ SPCR = _BV(SPE) | _BV(MSTR); /* Clear status flags. */ tmp = SPSR; tmp = SPDR; }
[edit] Initializing the Slave
This is quite similar to the master initialization, except that the master mode bit must not be set.
SPCR = _BV(SPE);
And, of course, the I/O direction changes. MISO (PB3 on the ATmega2561) has to be an output, all remaining lines are automatically set to input mode.
DDRB |= _BV(3);
Here's the final initialization routine:
void SpiSlaveInit(void) { unsigned char tmp; /* Switch MISO pin to output mode. */ DDRB |= _BV(3); /* Enable MOSI pull-up. */ PORTB |= _BV(2); /* Activate the SPI hardware. */ SPCR = _BV(SPE); /* Clear status register flags. */ tmp = SPSR; tmp = SPDR; }
[edit] SPI Configurations
Two items are configurable on the AVR platform, SPI mode and transfer speed.
[edit] SPI Mode
[edit] SPI Speed
[edit] Polling Mode Data Transfer
Data transmission starts as soon as a data byte is written to the SPI data register.
SPDR = tx_data;
We can use the SPI status register to check, whether the transmission has finished. Remember, SPI concurrently transmits and receives data. We can use the SPI status register to check, whether our data byte has been transmitted and a new data byte has been received.
loop_until_bit_is_set(SPSR, SPIF);
The data byte received is available in the SPI data register. Reading it will also clear the SPIF bit in the status register.
rx_data = SPDR;
Combining these statements results in a useful function, which accepts a byte to transmit and returns the byte received:
unsigned char SpiByte(unsigned char data) { SPDR = data; loop_until_bit_is_set(SPSR, SPIF); return SPDR; }
Simple, isn't it. If we want to transmit only without receiving anything, then we can simply discard the return value. The following routine will send a NUL terminated C string.
void SpiSendString(const char *str) { while(*str) { SpiByte((unsigned char)(*str)); str++; } }
Note three things here: First we use a type cast to convert each char to an unsigned char. Otherwise the compiler will emit a warning. Nevertheless, we intentionally use a signed char pointer for the string, because this is C standard. The second thing to note is, that we increment the pointer by using an extra statement. We could have written
SpiByte((unsigned char)(*str++));
as well and it would work as expected. However, it is generally critical to do so in a parameter list. We know here, that SpiByte() is a routine, not a preprocessor macro. But this information is not always available. Or it may be changed later to become a macro for some reason. If in that case the pointer will be referenced in the macro expansion more than once, it may get incremented more than once as well. You can imagine, that such kind of bugs are very difficult to find later.
The third thing to note is the const keyword. It specifies, that the string contents will not be modified by our routine. This helps the compiler to optimize the code.
Back to SPI. What, if we just want to receive data without sending anything? In this case we simply send dummy bytes. Any value may be used, typically 0x00 or 0xFF.
The following routine receives a specified number of bytes:
void SpiRead(void *buffer, int len) { unsigned char *bp = (unsigned char *)buffer; while(len--) { *bp++ = SpiByte(0xFF); } }
The routine may look more complicated than necessary. However, using a void pointer as a parameter clearly indicates, that any pointer type may be used by the caller.
[edit] Interrupt Driven Data Transfer
The statement used for polling the SPI status register
loop_until_bit_is_set(SPSR, SPIF);
may execute some thousands or more loops, which is a waste of CPU power. When using interrupts, the CPU can do other tasks while the data is concurrently transfered in the background.
A possible interrupt service routine may look like this:
volatile size_t icnt; volatile unsigned char * volatile iptr; ISR(SPI_STC_vect) { /* Put received byte in buffer. */ *iptr++ = SPDR; /* Decrement byte counter. */ icnt--; /* If the counter is not 0... */ if (icnt) { /* ...send the next byte. */ SPDR = *iptr; } else { /* ...otherwise disable interrupt mode. */ SPCR &= ~_BV(SPIE); } }
The following routine can be used to start interrupt controlled SPI transfer:
void StartTransfer(unsigned char *data, size_t len) { /* Set interrupt pointer and counter. */ iptr = data; icnt = len; /* Activate interrupt mode and trigger the transfer. */ SPCR |= _BV(SPIE); SPDR = *iptr; }
As the interrupt service automatically disables the interrupt mode after all bytes had been transfered, we can query the interrupt enable bit to check when the transfer had been done. The following function returns 0 in this case.
int TransferIsBusy(void) { return SPCR & _BV(SPIE); }
Here is a simple application routine, which writes a page to an AT45 series DataFlash:
void PageWrite(unsigned int pa, unsigned int ba, unsigned char *buf, size_t len) { unsigned char cmd[8]; /* Kommando zum direkten Schreiben, inkl. Löschen. */ cmd[0] = 0x82; cmd[1] = (unsigned char)(pa >> 7); cmd[2] = (unsigned char)((pa << 1) | (ba >> 7)); cmd[3] = (unsigned char)ba; StartTransfer(cmd, 8); while(TransferIsBusy()); /* Daten senden. */ StartTransfer(buf, len); }
Parameter pa contains the address of the page, ba contains the byte address within the page, buf points to the data to be written and len specifies the number of bytes to write. The routine will return while the data is transfered in the background.
[edit] SPI Software Emulation
If additional interfaces are required or if the CPU doesn't provide any SPI hardware, you can emulate the interface by software, using ordinary port pins.
[edit] External Links
AVR151: Setup and use of the SPI
Atmel application note, which describes how to setup and use the Serial Peripheral Interface of the AVR microcontrollers.
spi.c File Reference
SPI source code of the Procyon AVRlib.

