Compared to SPI, I2C (a.k.a Two-Wire or sometimes just refered to it's Arduino name, Wire) is even simpler - it's only 2 pins, SCL and SDA. I2C ports are a huge pain to emulate or bitbang due to the somewhat convoluted interface with multiple exceptions (repeated start! clock stretching!) and the bidirectional SDA line.

For that reason, being able to create new Wire's is awesome

How Wire 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 "Wire" SERCOM in Wire.cpp

TwoWire Wire(&PERIPH_WIRE, PIN_WIRE_SDA, PIN_WIRE_SCL);

void WIRE_IT_HANDLER(void) {
  Wire.onService();
}

The macros are see in variants.h

#define PIN_WIRE_SDA         (20u)
#define PIN_WIRE_SCL         (21u)
#define PERIPH_WIRE          sercom3
#define WIRE_IT_HANDLER      SERCOM3_Handler

You can see here that Wire is of type TwoWire and if you expand the macros:

      TwoWire Wire(&sercom3, 20, 21);
    

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

PA22	D20 / SDA	SERCOM3.0	SERCOM5.0
PA23	D21 / SCL	SERCOM3.1	SERCOM5.1

You can see that the first argument (&sercom3) does in fact match our observation that I2C is on SERCOM3. The second and third arguments match with the SDA & SCL pins.

Unlike Serial or SPI, there are no passed in arguments for the MUXing. I2C, rather, has 'fixed' pads:

Pad 0 will always be SDA and pad 1 will always be SCL

Both the SAMD21 and SAMD51 have the same restrictions for SDA/SCL pads

OK so let's make a new I2C SERCOM already

Now we're ready, lets try creating a new SERCOM and testing it out

Let's make a Wire device on SERCOM 1

Since we have to use pads 0 & 1, lets check the mux table:

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

SDA will be on SERCOM1.0 D11 and SCL will be on SERCOM1.1 D13

The definition for the new Wire will look like this:

TwoWire Wire(&sercom1, 11, 13);

Lets test it out!

#include <Wire.h>

TwoWire myWire(&sercom1, 11, 13);

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

uint8_t i=0;
void loop() {
  Serial.println(i);
  myWire.beginTransmission(0x55); // start transmission to device 
  myWire.write(i); // send a byte
  myWire.endTransmission(); // end transmission, actually sending
}

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

const PinDescription g_APinDescription[]=
...
  // 11
  { 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]
  // 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_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

Note that I've also put in code to control an MCP4725 by hand, since its a very simple I2C device (write 2 bytes to 0x40 to set an analog voltage out)

#include <Wire.h>
#include "wiring_private.h" // pinPeripheral() function

TwoWire myWire(&sercom1, 11, 13);

#define MCP4725_CMD_WRITEDAC            (0x40)
#define MCP4725_ADDR                    (0x62)
void setup() {
  Serial.begin(115200);
  myWire.begin();
  
  // Assign pins 13 & 11 to SERCOM functionality
  pinPeripheral(11, PIO_SERCOM);
  pinPeripheral(13, PIO_SERCOM);
}

uint8_t i=0;
void loop() {
  Serial.println(i);
  myWire.beginTransmission(MCP4725_ADDR); // start transmission to device 
  myWire.write(MCP4725_CMD_WRITEDAC);
  myWire.write(i++);
  myWire.write((uint8_t)0x0);  // bottom four bits are 0x0
  myWire.endTransmission(); // end transmission, actually sending
}
2-10K pullup resistors are required on SDA and SCL, both go to 3.3V! You can use your oscilloscope to see the data traces

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

We will have to have SDA on D4 (SERCOM2.0), SCL on D3 (SERCOM2.1)

#include <Wire.h>
#include "wiring_private.h" // pinPeripheral() function

TwoWire myWire(&sercom2, 4, 3);

#define MCP4725_CMD_WRITEDAC            (0x40)
#define MCP4725_ADDR                    (0x62)
void setup() {
  Serial.begin(115200);
  myWire.begin();
  
  // Assign pins 4 & 3 to SERCOM functionality
  pinPeripheral(4, PIO_SERCOM_ALT);
  pinPeripheral(3, PIO_SERCOM_ALT);
}

uint8_t i=0;
void loop() {
  Serial.println(i);
  myWire.beginTransmission(MCP4725_ADDR); // start transmission to device 
  myWire.write(MCP4725_CMD_WRITEDAC);
  myWire.write(i++);
  myWire.write((uint8_t)0x0);  // bottom four bits are 0x0
  myWire.endTransmission(); // end transmission, actually sending
}

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 Mar 08, 2024.

This page (Creating a new Wire) was last updated on Mar 08, 2024.

Text editor powered by tinymce.