We already saw in the two previous parts:
– Part 1: Pi and Arduino (SAMD21) using SPI 
– Part 2: Pi and Arduino using Python

This is the last part for now: making a bi-directional connection between the Arduino (SAMD21) and the Pi.

While protocols like RS-232 have both a send (TX) and a receive (RX), other Serial protocols like i2c and SPI work a little different. For i2c / Wire, the standard Arduino libraries give us already very good examples of being a master reader, master writer, slave reader and slave writer so that we can manage to develop our bi-directional communication in our programs. No such luck for SPI. 

But as it turns out, our example code already supports bi-directional communication. Every time a byte is written from the master, a byte is also read from the slave.

Slave (SAMD21)

Our sample code based on MartinL and Scotty454 has a line that is crucial for this: “SERCOM1->SPI.DATA.reg” the byte stored in this register is read by the master just after the current byte was written.

In the updated example for Arduino / SAMD21 we have added a buffer and a counter: the buffer contains the bytes that will be sent back to the master. We implemented it as a circular buffer for now (after the last character was sent, it will start over again).

/**
 * J.A. Korten August 6, 2017
 * Bi-directional extension added March 12, 2018
 * 
 * Modified based on code by MartinL, Scotty454
 */
 
 #include <SPI.h>

const int slaveAPin = 10; // SS (PA18 / D10 on SAMD21)
const int numChars = 6;
int testCharacters[numChars] = {0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x3b}; // "Hello;"
volatile int indexChars = 0;

void setup() {

  Serial.begin(115200);
  pinMode(slaveAPin, INPUT_PULLUP);
  attachInterrupt(10, SERCOM1_Handler, FALLING);
  spiSlave_init();
}

void loop() {
  // put your main code here, to run repeatedly:
  asm ("nop");
}

void SERCOM1_Handler()
{
  noInterrupts();
  
  uint8_t data = 0;
  data = (uint8_t)SERCOM1->SPI.DATA.reg;
  uint8_t interrupts = SERCOM1->SPI.INTFLAG.reg; //Read SPI interrupt register

  if (interrupts & (1 << 3))
  {
    SERCOM1->SPI.INTFLAG.bit.SSL = 1; //clear slave select interrupt
  }
  if (interrupts & (1 << 2))
  {
    data = SERCOM1->SPI.DATA.reg; //Read data register
    SERCOM1->SPI.INTFLAG.bit.RXC = 1; //clear receive complete interrupt
  }
  if (interrupts & (1 << 1))
  {
    SERCOM1->SPI.INTFLAG.bit.TXC = 1; //clear receive complete interrupt
  }

  if (interrupts & (1 << 0))
  {
    byte currentChar = testCharacters[indexChars];
    indexChars++;
    if (indexChars >= numChars) {
      indexChars = 0;
    }
    SERCOM1->SPI.DATA.reg = currentChar; // 0x41; // was AA (0x41 -> DEC 65, Char: A) 
  }
  char _data = data;
  Serial.print(_data); // print received data
  
  interrupts();
}

void spiSlave_init()
{

  PORT->Group[PORTA].PINCFG[16].bit.PMUXEN = 0x1; //Enable Peripheral Multiplexing for SERCOM1 SPI PA18 Arduino PIN10
  PORT->Group[PORTA].PMUX[8].bit.PMUXE = 0x2; //SERCOM 1 is selected for peripherial use of this pad
  PORT->Group[PORTA].PINCFG[17].bit.PMUXEN = 0x1; //Enable Peripheral Multiplexing for SERCOM1 SPI PA18 Arduino PIN10
  PORT->Group[PORTA].PMUX[8].bit.PMUXO = 0x2; //SERCOM 1 is selected for peripherial use of this pad
  PORT->Group[PORTA].PINCFG[18].bit.PMUXEN = 0x1; //Enable Peripheral Multiplexing for SERCOM1 SPI PA18 Arduino PIN10
  PORT->Group[PORTA].PMUX[9].bit.PMUXE = 0x2; //SERCOM 1 is selected for peripherial use of this pad
  PORT->Group[PORTA].PINCFG[19].bit.PMUXEN = 0x1; //Enable Peripheral Multiplexing for SERCOM1 SPI PA18 Arduino PIN10
  PORT->Group[PORTA].PMUX[9].bit.PMUXO = 0x2; //SERCOM 1 is selected for peripherial use of this pad

  //Disable SPI 1
  SERCOM1->SPI.CTRLA.bit.ENABLE = 0;
  while (SERCOM1->SPI.SYNCBUSY.bit.ENABLE);

  //Reset SPI 1
  SERCOM1->SPI.CTRLA.bit.SWRST = 1;
  while (SERCOM1->SPI.CTRLA.bit.SWRST || SERCOM1->SPI.SYNCBUSY.bit.SWRST);

  //Setting up NVIC
  NVIC_EnableIRQ(SERCOM1_IRQn);
  NVIC_SetPriority(SERCOM1_IRQn, 2);

  //Setting Generic Clock Controller!!!!
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_ID(GCM_SERCOM1_CORE) | //Generic Clock 0
                      GCLK_CLKCTRL_GEN_GCLK0 | // Generic Clock Generator 0 is the source
                      GCLK_CLKCTRL_CLKEN; // Enable Generic Clock Generator

  while (GCLK->STATUS.reg & GCLK_STATUS_SYNCBUSY); //Wait for synchronisation


  //Set up SPI Control A Register
  SERCOM1->SPI.CTRLA.bit.DORD = 0; //MSB first
  SERCOM1->SPI.CTRLA.bit.CPOL = 0; //SCK is low when idle, leading edge is rising edge
  SERCOM1->SPI.CTRLA.bit.CPHA = 0; //data sampled on leading sck edge and changed on a trailing sck edge
  SERCOM1->SPI.CTRLA.bit.FORM = 0x0; //Frame format = SPI
  SERCOM1->SPI.CTRLA.bit.DIPO = 0; //DATA PAD0 MOSI is used as input (slave mode)
  SERCOM1->SPI.CTRLA.bit.DOPO = 0x2; //DATA PAD3 MISO is used as output
  SERCOM1->SPI.CTRLA.bit.MODE = 0x2; //SPI in Slave mode
  SERCOM1->SPI.CTRLA.bit.IBON = 0x1; //Buffer Overflow notification
  SERCOM1->SPI.CTRLA.bit.RUNSTDBY = 1; //wake on receiver complete

  //Set up SPI control B register
  //SERCOM1->SPI.CTRLB.bit.RXEN = 0x1; //Enable Receiver
  SERCOM1->SPI.CTRLB.bit.SSDE = 0x1; //Slave Selecte Detection Enabled
  SERCOM1->SPI.CTRLB.bit.CHSIZE = 0; //character size 8 Bit
  //SERCOM1->SPI.CTRLB.bit.PLOADEN = 0x1; //Enable Preload Data Register
  //while (SERCOM1->SPI.SYNCBUSY.bit.CTRLB);

  //Set up SPI interrupts
  SERCOM1->SPI.INTENSET.bit.SSL = 0x1; //Enable Slave Select low interrupt
  SERCOM1->SPI.INTENSET.bit.RXC = 0x1; //Receive complete interrupt
  SERCOM1->SPI.INTENSET.bit.TXC = 0x1; //Receive complete interrupt
  SERCOM1->SPI.INTENSET.bit.ERROR = 0x1; //Receive complete interrupt
  SERCOM1->SPI.INTENSET.bit.DRE = 0x1; //Data Register Empty interrupt
  //init SPI CLK
  //SERCOM1->SPI.BAUD.reg = SERCOM_FREQ_REF / (2*4000000u)-1;
  //Enable SPI
  SERCOM1->SPI.CTRLA.bit.ENABLE = 1;
  while (SERCOM1->SPI.SYNCBUSY.bit.ENABLE);
  SERCOM1->SPI.CTRLB.bit.RXEN = 0x1; //Enable Receiver, this is done here due to errate issue
  while (SERCOM1->SPI.SYNCBUSY.bit.CTRLB); //wait until receiver is enabled

}

For the Raspberry Pi side the proof of concept code is really simple:

"""
  Proof of concept to show how to make a bi-directional connection
  between a Raspberry Pi and (in our case) a SAMD21-based Arduino
  board.

  J.A. Korten 
  March 12, 2018

  spiBiPOC.py

"""

#!/usr/bin/python

import spidev
import time

spi = spidev.SpiDev()
spi.open(0, 0)
spi.max_speed_hz = 40000

# write and read bytes 
def updateSPIMessage():
    result = spi.xfer([0x41, 0x62, 0x3B, 0xA]) #some example bytes (Ab;\n)
    # from result with bytes to String
    myStr = "".join(map(chr, result))
    print(myStr)  #show what we received!

try:
    # Keep updating messages
    while True:
       updateSPIMessage()
       time.sleep(0.5)

except KeyboardInterrupt:
       spi.close()

And this yields as terminal output:

That is not bad at all for what I need! Now I want to create proper libraries and a data protocol for it, but so far so good! Hope you also learned something from it.

For further reading on the technical aspects of SPI see Sparkfun.com