Creating a new SPI

SPI is a high speed, 3-wire protocol that can be clocked at up to 12MHz on the ATSAMD21 and can also take advantage of DMA (that's for another tutorial)

Each SPI SERCOM has 3 pins - Master Clock line (SCK), Master Out/Slave In (MOSI) and Master In/Slave Out (MISO). In these examples we'll assume the ATSAMD21 is acting as the Master, since that's the by-far-most-common example. In this way the chip can control wifi modules, SD cards, DotStar LEDs, various sensors and actuators, etc.

So you might be wondering "Why does she keep bringing up that SPI uses only 3 pins?" It's cuz SERCOM's have 4 pads, but we only need three. There is one unused one that can be used as a normal GPIO (or whatever) pin

How SPI is Created Now

Luckily, Atmel & Arduino did a really great job with structuring SERCOMs so you can create and assign new ones. You can find the actual code that is used to create the "SPI" SERCOM in SPI.cpp

SPIClass SPI (&PERIPH_SPI,  PIN_SPI_MISO,  PIN_SPI_SCK,  PIN_SPI_MOSI,  PAD_SPI_TX,  PAD_SPI_RX);

The macros are see in variants.h

#define PIN_SPI_MISO         (22u)
#define PIN_SPI_MOSI         (23u)
#define PIN_SPI_SCK          (24u)
#define PERIPH_SPI           sercom4
#define PAD_SPI_TX           SPI_PAD_2_SCK_3
#define PAD_SPI_RX           SERCOM_RX_PAD_0

You can see here that SPI is of type SPIClass and if you expand the macros:

SPIClass SPI (&sercom4, 22, 24, 23, SPI_PAD_2_SCK_3, SERCOM_RX_PAD_0);

We can match this up with the pins we know are used for SPI:

PA12	D22 / MISO	SERCOM2.0	SERCOM4.0
PB10	D23 / MOSI			SERCOM4.2
PB11	D24 / SCK			SERCOM4.3

You can see that the first argument (&sercom4) does in fact match our observation that SPI is on SERCOM4. The second, third and fourth arguments match with the MISO/SCK/MOSI pins.

The last two args are what actually define the pin muxing. We can see these definitions in SERCOM.h

typedef enum
{
	SPI_PAD_0_SCK_1 = 0,
	SPI_PAD_2_SCK_3,
	SPI_PAD_3_SCK_1,
	SPI_PAD_0_SCK_3
} SercomSpiTXPad;

and

typedef enum
{
	SERCOM_RX_PAD_0 = 0,
	SERCOM_RX_PAD_1,
	SERCOM_RX_PAD_2,
	SERCOM_RX_PAD_3
} SercomRXPad;

The two together let us define the muxing. For example:

  • SPI_PAD_0_SCK_1 means MOSI is on SERCOMn.0 and SCK is on SERCOMn.1
  • SPI_PAD_2_SCK_3 means MOSI is on SERCOMn.2 and SCK is on SERCOMn.3
  • SPI_PAD_3_SCK_1 means MOSI is on SERCOMn.3 and SCK is on SERCOMn.1
  • SPI_PAD_0_SCK_3 means MOSI is on SERCOMn.0 and SCK is on SERCOMn.3

Then we can define which pin is for MISO

  • SERCOM_RX_PAD_0 means MISO on SERCOMn.0
  • SERCOM_RX_PAD_1 means MISO on SERCOMn.1
  • SERCOM_RX_PAD_2 means MISO on SERCOMn.2
  • SERCOM_RX_PAD_3 means MISO on SERCOMn.3

As you can tell, you can't quite have every possibility, for example you cannot have SCK on pad 0 or pad 2. And you cant have MOSI on pad 1. But you do have a lot of options!

OK so let's make a new SPI SERCOM already

ok ok so you are getting impatient! Lets try creating a new SERCOM and testing it out

Let's make a SPI device that 'mimics' the ATmega328 SPI pin numbering on Digital 11, 12, and 13:

  • SCK on Digital 13
  • MISO on Digital 12
  • MOSI on Digital 11

Looking at our SERCOMs from the Mux table, we should go for SERCOM1

Pin	Arduino 'Pin'	SERCOM		SERCOM alt
-----------------------------------------
PA18	D10		SERCOM1.2	SERCOM3.2
PA16	D11		SERCOM1.0	SERCOM3.0
PA19	D12		SERCOM1.3	SERCOM3.3
PA17	D13		SERCOM1.1	SERCOM3.1

To put SCK signal on D13 (SERCOM1.1) and MOSI on D11 (SERCOM1.0) we want to use SPI_PAD_0_SCK_1  for the TX mux. That leaves on MISO to do, on D12 (SERCOM1.3), which is SERCOM_RX_PAD_3

So something like this:

SPIClass SPI1 (&sercom1, 12, 13, 11, SPI_PAD_0_SCK_1, SERCOM_RX_PAD_3);

Lets test it out!

#include <SPI.h>

SPIClass mySPI (&sercom1, 12, 13, 11, SPI_PAD_0_SCK_1, SERCOM_RX_PAD_3);

void setup() {
  Serial.begin(115200);
  mySPI.begin();
}

uint8_t i=0;
void loop() {
  Serial.println(i);
  mySPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
  mySPI.transfer(i++);
  mySPI.endTransaction();
}

OK ... that wont work. Why? Good question! Check out those pin definitions in the variants.cpp file:

const PinDescription g_APinDescription[]=
...
  { PORTA, 16, PIO_TIMER, (PIN_ATTR_DIGITAL|PIN_ATTR_PWM|PIN_ATTR_TIMER), No_ADC_Channel, PWM2_CH0, TCC2_CH0, EXTERNAL_INT_0 }, // TCC2/WO[0]
  { PORTA, 19, PIO_TIMER_ALT, (PIN_ATTR_DIGITAL|PIN_ATTR_PWM|PIN_ATTR_TIMER_ALT), No_ADC_Channel, PWM0_CH3, TCC0_CH3, EXTERNAL_INT_3 }, // TCC0/WO[3]

  // 13 (LED)
  { PORTA, 17, PIO_PWM, (PIN_ATTR_DIGITAL|PIN_ATTR_PWM), No_ADC_Channel, PWM2_CH1, NOT_ON_TIMER, EXTERNAL_INT_1 }, // TCC2/WO[1]

The first argument is the port (e.g. PORTA or PORTB), the second is the port's pin number, e.g. PORTA, 16 -> PA16. The third argument 'sets' the type of pinmux we're going to use.

In this case, these are set to be used as PIO_TIMER, PIO_TIMER_ALT and PIO_PWM

We have to tell the Arduino core to change the MUX type before we reassign the SERCOM to this pin

You can do that by calling pinPeripheral(pinnumber, function) which is an internal function but hey we're being hardk0re

#include <SPI.h>
#include "wiring_private.h" // pinPeripheral() function
  
SPIClass mySPI (&sercom1, 12, 13, 11, SPI_PAD_0_SCK_1, SERCOM_RX_PAD_3);

void setup() {
  Serial.begin(115200);

  // do this first, for Reasons
  mySPI.begin();

  // Assign pins 11, 12, 13 to SERCOM functionality
  pinPeripheral(11, PIO_SERCOM);
  pinPeripheral(12, PIO_SERCOM);
  pinPeripheral(13, PIO_SERCOM);
}

uint8_t i=0;
void loop() {
  Serial.println(i);
  mySPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
  mySPI.transfer(i++);
  mySPI.endTransaction();
}

You can use your oscilloscope to see the data traces

Say you only want to transmit SPI data, you can leave out the pinPeripheral(12, PIO_SERCOM) line, and you can keep using #12 however you like

We can also try SERCOM2:

Pin	Arduino 'Pin'	SERCOM		SERCOM alt
-----------------------------------------
PA14	D2		SERCOM2.2	SERCOM4.2
PA09	D3		SERCOM0.1	SERCOM2.1
PA08	D4		SERCOM0.0	SERCOM2.0
PA15	D5		SERCOM2.3	SERCOM4.3

Lets put SCK on D5 (SERCOM2.3), MISO on D3 (SERCOM2.1) and MOSI on D4 (SERCOM2.0)

#include <SPI.h>
#include "wiring_private.h" // pinPeripheral() function
  
SPIClass mySPI (&sercom2, 3, 5, 4, SPI_PAD_0_SCK_3, SERCOM_RX_PAD_1);

void setup() {
  Serial.begin(115200);

  // do this first, for Reasons
  mySPI.begin();

  // Assign pins 3, 4, 5 to SERCOM & SERCOM_ALT
  pinPeripheral(3, PIO_SERCOM_ALT);
  pinPeripheral(4, PIO_SERCOM_ALT);
  pinPeripheral(5, PIO_SERCOM);
}

uint8_t i=0;
void loop() {
  Serial.println(i);
  mySPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
  mySPI.transfer(i++);
  mySPI.endTransaction();
}

Note that since pin 3 and 4 use a SERCOM alt mux, we have to pass in PIO_SERCOM_ALT rather than PIO_SERCOM when we call pinPeripheral()

This guide was first published on Jan 24, 2016. It was last updated on Nov 17, 2018. This page (Creating a new SPI) was last updated on Jul 14, 2017.