I2C Communication

Basic Operation

Inter-Integrated Circuit (I2C) is also referred to as Two-Wire Interface (TWI).

In I2C communication there is a master device and one or more slave devices. The microcontroller can be configured as either a master or slave device.

The clock (SCL) signal is primarily controlled by the master. The data transfer happens on the data (SDA) line. The master communicates to each slave using a unique 7-bit address.

The following I2C specifications can be used:

I2C specifications.

Only the master device sends the "START" and "STOP" signals. "READ" is used by input channels, "WRITE" by output channels, and data can be used by both input and output channels. The "ACK" and "NACK" signals are sent by the device receiving the data and are read by the device sending the data.

The specifications for the I2C devices that the microcontroller needs to communicate with can be given in the "I2C" option as (StringTypeLink["I2C", paclet:ref/device/I2C])->{(Subscript[name, 1])->{(Subscript[spec, 11]),(Subscript[spec, 12]),…},(Subscript[name, 2])->{(Subscript[spec, 21]),(Subscript[spec, 22]),…},…}. The (Subscript[name, i]) of the I2C device can take the following values:

Possible names of I2C devices connected to the microcontroller.

For slave I2C devices, a "ClockFrequency" suboption can be given along with its specifications.

The microcontroller's I2C channels:

I2C channels in master and slave modes.

In an input channel, the suboption "Conversion" specifies how the data coming in needs to be converted and assigned to the input.

In an output channel, the suboption "Conversion" specifies how the output needs to be converted and sent out.

If the microcontroller is operating in slave mode, its I2C address must be specified in the second argument that has all the microcontroller details as (PackageLink[MicrocontrollerEmbedCode, paclet:MicrocontrollerKit/ref/MicrocontrollerEmbedCode, MicrocontrollerKit Package Symbol])[sys,<|…,"I2CAddress"->"addr"…|>,…].

Example

We are going to use an ATmega168 as a master, an Arduino Uno as the slave, and a slave MPU-6050 temperature sensor. The master receives the reading from slave MPU-6050 and transmits it to the slave Arduino Uno. The slave Uno then sends the data over serial which can be read by the device framework.

The I2C connections, along with a voltage regulator for the ATmega168, are shown below.

The master needs to know the addresses of the slave devices to communicate with them. From the datasheet of the MPU-6050 we know that its address is 0x68. We can choose any other valid address for the slave Uno.

The slave addresses.

mpuAddr="0x68";
unoAddr="0x10";

The master first initializes the MPU-6050. Then at every sampling instant it read the two bytes it sends.

The specification for the input from the MPU6050 sensor.

mpuSpecs=mpuAddr->{{"START","0x41","START","READ","ACK","READ","STOP"},"Initialization"->{"START","0x6B","0x0","STOP"}};

The master passes the two bytes from one slave to another.

The input-output system of the master ATmega168 is an identity matrix.

at168Sys=(StateSpaceModelFull[1, 0, 0, 1, 0.5`, , ])

The master then needs to write these bytes to the slave Uno, whose clock frequency is 16 MHz.

The specification for the output to the Uno slave.

unoSpecs=unoAddr-> {{"START","WRITE","WRITE","STOP"},"ClockFrequency"->16 (10^6)};

Pin 27 is the SDA pin on the master ATmega168 through which data is received and transmitted. The input and output byte conversions are identical because the bytes are just passed through.

Set up the input and output channel specifications.

at168IO[addr_]:=Table["27"-><|"Type"-> "I2C","SlaveAddress"->addr, "Conversion"->fn|>,{fn,{#1&,#2&}}]

Load the package.

Needs["MicrocontrollerKit`"]

Embed to the master.

MicrocontrollerEmbedCode[at168Sys,
<|"Target"->"ATmega168A_28PDIP","Inputs"->at168IO["0x68"],"Outputs"->at168IO["0x10"]|>,<|"ExternalProgrammer"->"usbasp"|>,
<|"I2C"->{mpuSpecs,unoSpecs}|>]

The master will transmit the raw bytes from the slave MPU-6050 to the slave Uno and the slave Uno will perform the conversion computations.

We need to set up the slave Uno to read the two bytes from the master.

The specification for the input from the Uno master.

masterSpecs="Master"->{"READ","ACK","READ"};

The datasheet of the MPU-6050 specifies how to convert the raw bytes to an actual temperature value. The raw bytes are converted to a 16-bit value and then the converted to the actual value.

The conversion for the input received by the Uno slave.

Needs["SymbolicC`"]

fn=COperator[BitOr,{COperator[BitShiftLeft,{#1,8}],#2}]&;

The systems model to convert it to degrees C.

unoSys=NonlinearStateSpaceModel[{{},36.53+u/340},{},u,SamplingPeriod->0.5]

Set up the slave to receive the I2C data, do the conversion, and transmit it over the serial port. We must also specify the slave I2C address, so it can respond when called upon by the master.

Embed code to the slave Uno.

ℳ=MicrocontrollerEmbedCode[unoSys,<|"Target"->"ArduinoUno","Inputs"->{"A4"-><|"Type"->"I2C", "Conversion"->fn|>},"Outputs"->{"1"->"Serial"},"I2CAddress"->unoAddr|>,"/dev/tty.usbmodem14111",<|"I2C"->masterSpecs|>]

Finally, the device framework can be setup to receive the incoming serial data from the slave.

Open a connection to the Uno slave.

dv=DeviceOpen["Serial","/dev/tty.usbmodem14111"]

A function to parse the received data.

parseData[{ℳ["Serial"]["StartByte"],val0__}]:=ToExpression[FromCharacterCode[{val0}]]
parseData[___]:=Sequence[]

Set up a task to read the data.

y={};
task=SessionSubmit@ScheduledTask[If[DeviceExecute[dv,"SerialReadyQ"],y=Join[y,{parseData@DeviceReadBuffer[dv,"ReadTerminator"->ℳ["Serial"]["EndByte"]]}];
DeviceExecute[dv,"ReadFlush"];],0.5];

Remove the task and close the device after some time.

TaskRemove[task];
DeviceClose[dv];

The plot of the received data.

ListLinePlot[y,PlotRange->All]