rocketnumbernine

Andrew's Project Blog: Hardware, Software, Stuff I find Interesting

This is a follow up to Using SPI on an AVR (1) and Using SPI on an AVR (2) articles which illustrated using an AVR Microcontroller to communicate with some simple SPI Slave devices. This entry shows two AVR Micro controllers communicating with each other through SPI - one acting as Master and one as Slave. An interrupt handler is used in the slave to notify it when data is being received. One device is an at90usb162 on a Teensy Board running AVR GCC code, the other an Arduino (with ATmega328) although the code should run on most AVR microcontrollers and Arduino versions that are SPI capable.

AVR SPI Slave Mode

When a device is operating as the SPI slave device it receives data under control of the master:

  1. The slave devices slave select (SS) pin is brought low by the master.
  2. The master oscillates the SCK clock as it places bits onto the MOSI line and reads from the MISO line.
  3. It's the master's responsibility to use the SPI mode the slave is expecting (whether data is written and read on the rising or falling edges of the clock) and to make sure the clock rate is not too high for the slave.

The AVR's built in SPI hardware takes care of receiving and processing the SPI data and placing the received data a byte at a time onto the SPI data register buffer, the program simply needs to consume the read bytes from the buffer fast enough.

There are two ways for the program to ascertain when data is ready, it can either poll the SPI status register to see when a byte has been received (SPSR = (1<<SPIF)) - the same send_spi() helper function that was developed in a previous article can be used. Alternatively the SPI system can be configured to generate an interrupt when a byte has been received and is ready to read from the buffer. This is achieved by setting the SPIE bit in the SPI control register (SPCR = (1<<SPIE)), I encapsulated this as a parameter to the spi setup helper function developed earlier.

setup_spi(SPI_MODE_1, SPI_MSB, SPI_INTERRUPT, SPI_SLAVE);

Example

To reduce the amount of code to be written, each program (the AVR GCC and Arduino) acts as both master and slave, and either part can be replaced with the other (ie. two Arduinos can be used etc.). The circuit is shown below, apart from the SPI connections each device has an LED and a push button connected. spi-mqasterslave

When the push button on one device is pressed, the LED on the other flashes. This is achieved using the following logic:

  1. The device is put into SPI slave mode so that an interrupt is raised when data is received on the SPI bus.
  2. When the button is pressed the program puts the AVR into SPI master mode and sends the slave a message (a number of bytes) then goes back into slave mode.
  3. The program's SPI interrupt handler simply stores the bytes received in a buffer until a terminating null byte (0x00) is received, it then parses and processes the message.
  4. Currently a single message type is supported - FLASH_LED (0x00) which is followed by a byte containing the number of times the slave should flash its LED - so to make the slave flash its LED 5 times the master would send 3 bytes - 0x01, 0x05, 0x00.

With the SPI system in slave mode and with interrupts enabled the interupt service handler for SPISTCVect will be called when a byte has been received. All the handler implementation does (below) is save the byte received from SPI in a buffer (incoming) and checks if the byte received is null or we've filled the buffer, if so the parse_message() is called to process the data received. As stated previously the interrupt handler mu st return fast enough to handle the next received byte. Whilst the handler is running interrupts will be disabled and if it takes too long data will be lost.

1
2
3
4
5
6
7
8
ISR(SPI_STC_vect)
{
  incoming[received++] = received_from_spi(0x00);
  if (received >= BUFSIZE || incoming[received-1] == 0x00) {
    parse_message();
    received = 0;
  }
}

The receivedfromspi() function simply reads the byte read from the SPI pipeline and sets the value of the next byte to be sent - note the SPDR buffer is double buffered so the value can be written before being read.

1
2
3
4
5
unsigned char received_from_spi(unsigned char data)
{
  SPDR = data;
  return SPDR;
}
Using an interrupt handler to receive and process the SPI data allows the AVR to perform other tasks, or sleep, until all data is ready.

Sending a SPI message on button press

We also use an interrupt to trigger sending of a message when a button is pressed in the SPI master. We configure the AVR to trigger an interrupt when a pin goes low due to the button being pressed. The Arduino has a helper function to set this up:

attachInterrupt(0, button_pressed, FALLING);
This will result in the function "buttonpressed" being called when external interrupt 0 (Connected to Arduino pin D2) goes from high to low (FALLING). In native AVR GCC this takes a little more effort - we set the relevant bits in the EICRB register to receive an interrupt on a falling signal (ISCO1 and ISC00), enable external interrupts in EIMSK register, and then enable interrupts (sei()). See the External Interrupt Channel of the relevant AVR datasheet for details of the registers.
1
2
3
EICRB = (1<<ISC01) | (0<<ISC00);
 EIMSK |= (1<<INT0);
 sei();
The above configuration results in the handler named ISR(INT0
vect) being called when the INT0 pin goes from high to low, both this and the Arduino's buttonpressed() just call the sendmessage function to transmit a message. This attempts to put the device into SPI master mode, checks that it was successful (If its select pin isn't low) and if so selects the other device, sends 3 bytes, then deselects the other device and goes back into slave mode:
1
2
3
4
5
6
7
8
9
10
void send_message()
{
  setup_spi(SPI_MODE_1, SPI_MSB, SPI_NO_INTERRUPT, SPI_MSTR_CLK8);
  if (SPCR & (1<<MSTR)) { // if we are still in master mode
    SELECT_OTHER; // tell other device to flash LED twice
    send_spi(FLASH_LED_COMMAND); send_spi(0x02); send_spi(0x00);
    DESELECT_OTHER;
  }
  setup_spi(SPI_MODE_1, SPI_MSB, SPI_INTERRUPT, SPI_SLAVE);
}

The AVR spec says the SPI clock frequency in the slave device shouldn't exceed the MCU frequency/4 - I've found frequency/8 is safer.

We've seen that the slave device just stores byte received until it receives the terminating null byte, it then calls parsemessage(). This checks the value of the first byte in the buffer and parses the remaining bytes accordingly. The current implementation just supports the single FLASHLED_COMMAND which reads the next byte and flashes an LED that number of times. If we receive an unexpected message the LED is flashed repeatedly:

1
2
3
4
5
6
7
8
9
10
void parse_message()
{
 switch(incoming[0]) {
 case FLASH_LED_COMMAND:
   flash_led(incoming[1]);
   break;
 default:
   flash_led(20);
 }
}

The complete programs are shown below and are also included as examples in the SPI helper library zip below AVR GCC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <avr/interrupt.h>
#include <util/delay.h>
#include <spi.h>

#define FLASH_LED_COMMAND 0x01
#define OTHER_SELECT_PIN PB6
#define SELECT_OTHER PORTB &= ~(1<<OTHER_SELECT_PIN)
#define DESELECT_OTHER PORTB |= (1<<OTHER_SELECT_PIN)

#define BUFSIZE 20
volatile unsigned char incoming[BUFSIZE];
volatile short int received=0;


// flash led that's connected to pin PD7
void flash_led(int count)
{
 DDRD |= (1<<PD7);
 for (int i=0; i<count*2; i++) {
   PORTD ^= (1<<PD7);
   _delay_ms(75);
 }
}

// send a SPI message to the other device - 3 bytes then go back into
// slave mode
void send_message()
{
 setup_spi(SPI_MODE_1, SPI_MSB, SPI_NO_INTERRUPT, SPI_MSTR_CLK8);
 if (SPCR & (1<<MSTR)) { // if we are still in master mode
   SELECT_OTHER; // tell other device to flash LED twice
   send_spi(FLASH_LED_COMMAND); send_spi(0x02); send_spi(0x00);
   DESELECT_OTHER;
 }
 setup_spi(SPI_MODE_1, SPI_MSB, SPI_INTERRUPT, SPI_SLAVE);
}

// called when the button pushed and pin INT0 goes from 1 to 0
ISR(INT0_vect)
{
 send_message();
 _delay_ms(500); // 'debounce'
}

// parse the data received from the other device
// currently just knows about the FLASH_LED_COMMAND
void parse_message()
{
 switch(incoming[0]) {
 case FLASH_LED_COMMAND:
   flash_led(incoming[1]);
   break;
 default:
   flash_led(20);
 }
}

// called by the SPI system when there is data ready.
// Just store the incoming data in a buffer, when we receive a
// terminating byte (0x00) call parse_message to process the data received
ISR(SPI_STC_vect)
{
 incoming[received++] = received_from_spi(0x00);
 if (received >= BUFSIZE || incoming[received-1] == 0x00) {
   parse_message();
   received = 0;
 }
}

int main(void)
{
 // make sure other device is unselected (pin is HIGH) and setup spi
 DESELECT_OTHER;
 DDRB |= (1<<OTHER_SELECT_PIN);
 setup_spi(SPI_MODE_1, SPI_MSB, SPI_INTERRUPT, SPI_SLAVE);

 // raise interrupt when the button is pushed and INT0 pin goes
 // from 1 to 0 (pin PD0 at AT90usbXXX, pin PD2 on ATmegaXXX,
 // arduino pin 2)). The code in ISR(INT0_vect) above will be called
 EICRB = (1<<ISC01) | (0<<ISC00);
 EIMSK |= (1<<INT0);
 sei();

 // flash LED at start to indicate were ready
 flash_led(1);

 while (1); // do nothing
}

Arduino:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <spi.h>
#include <util/delay.h>

#define BUTTON_PIN 2
#define LED_PIN 3
#define FLASH_LED_COMMAND 0x01

#define OTHER_SELECT_PIN 7
#define SELECT_OTHER digitalWrite(OTHER_SELECT_PIN, LOW);
#define DESELECT_OTHER digitalWrite(OTHER_SELECT_PIN, HIGH);

#define BUFSIZE 20
volatile unsigned char incoming[BUFSIZE];
volatile short int received=0;

// flash led that's connected to pin PD7
void flash_led(int count)
{
 pinMode(LED_PIN, OUTPUT);
 for (int i=0; i<count*2; i++) {
   digitalWrite(LED_PIN, i%2==0);
   _delay_ms(75);
 }
}

// send a SPI message to the other device - 3 bytes then go back into
// slave mode
void send_message()
{
 setup_spi(SPI_MODE_1, SPI_MSB, SPI_NO_INTERRUPT, SPI_MSTR_CLK8);
 if (SPCR & (1<<MSTR)) { // if we are still in master mode
   SELECT_OTHER; // tell other device to flash LED twice
   send_spi(FLASH_LED_COMMAND); send_spi(0x02); send_spi(0x00);
   DESELECT_OTHER;
 }
 setup_spi(SPI_MODE_1, SPI_MSB, SPI_INTERRUPT, SPI_SLAVE);
}


// called when button pushed and pin INT0 goes from 1 to 0
void button_pressed()
{
 send_message();
 _delay_ms(500);  // 'debounce'
}

// parse the data received from the other device
// currently just knows about the FLASH_LED_COMMAND
void parse_message()
{
 switch(incoming[0]) {
 case FLASH_LED_COMMAND:
   flash_led(incoming[1]);
   break;
 default:
   flash_led(20);
 }
}

// called by the SPI system when there is data ready.
// Just store the incoming data in a buffer, when we receive a
// terminating byte (0x00) call parse_message to process the data received
ISR(SPI_STC_vect)
{
 incoming[received++] = received_from_spi(0x00);
 if (received == BUFSIZE || incoming[received-1] == 0x00) {
     parse_message();
     received = 0;
  }
}

void setup()
{
 // make sure other device is unselected (pin is HIGH) and setup spi
 DESELECT_OTHER;
 pinMode(OTHER_SELECT_PIN, OUTPUT);
 setup_spi(SPI_MODE_1, SPI_MSB, SPI_INTERRUPT, SPI_SLAVE);

 // raise interrupt when INT0 pin falls (arduino pin 2))
 // the function button_pressed will be called
 attachInterrupt(0, button_pressed, FALLING);

 // flash LED at start to indicate were ready
 flash_led(1);
}


void loop()
{
}

References

It looks like your browser doesn't support javascript so comments won't work

Tags/Categories: arduino, AVR, SPI, at90usb162, atmega328, interrupts, master/slave, howto