WOLFRAM SYSTEM MODELER

I2C

Library for creating your own I2C drivers

Package Contents

Package Constants (2)

initTransaction

Value: Transaction(0, false)

Type: Transaction

initState

Value: State(Stage.Enumeration, 1, 1)

Type: State

Information

The ModelPlug I2C library allows communicating with any device supporting the I2C protocol. Writing drivers using the I2C library requires following a specific procedure that will be described in this guide. The first part will describe a template that can be used to communicate with any I2C device and the second will focus on a specific example.

In general, communicating with an I2C device involves two stages: configuring the device (Setup stage) and continuously reading the device (Loop stage). For example, the BMP180 temperature sensor requires reading the calibration coefficients of the device, then continuosly reading a specific register to get the measured temperature.

The ModelPlug I2C library organizes the flow of data in a series of Transactions. The Transactions can be classified with two characteristics: direction (Read or Write) and stage (Setup or Loop).

The following example will describe how the sensor LSM6DS3 (accelerometer and gyroscope) is used.

First you need to add a ModelPlug.Internal.Interfaces.PinConnector component by dragging it to your model. This connector is used to establish the communication with the Arduino board.


model Sensor
ModelPlug.Internal.Interfaces.PinConnector board;
end Sensor;

Next you need to define a set of auxiliary variables used by the ModelPlug I2C library to keep track of the transactions and also to describe the device.


model Sensor
ModelPlug.Internal.Interfaces.PinConnector board;
import I2C = ModelPlug.Sensors.I2C;
I2C.Device device(address = 107, id = board.id) "Device information";
I2C.State state "Used to keep track of the transactions";
I2C.Transaction t[5] "Vector of transactions, one per each read/write operation we are performing";
Boolean done "Flag to signal when all transactions are performed";
end Sensor;

The component "device" is used to keep the I2C device information like the address and the board identifier that the sensor is connected to. The board identifier is provided by the "board" connector. The component "state" is used by the I2C library functions to keep track of the steps that are being performed. The transactions vector "t[5]" is a vector that holds information for every read and write operation that is performed. Note that the size of the vector needs to be adjusted for each specific case. Finally, the variable "done" is set to true every time that all transactions have been performed.

Once you have all the declarations, you need to register the I2C device with the corresponding board. This is done in the algorithm section.

model Sensor
ModelPlug.Internal.Interfaces.PinConnector board;
import I2C = ModelPlug.Sensors.I2C;
I2C.Device device(address = 107, id = board.id) "Device information";
I2C.State state "Used to keep track of the transactions";
I2C.Transaction t[5] "Vector of transactions, one per each read/write operation we are performing";
Boolean done "Flag to signal when all transactions are performed";
algorithm
// Triggered only once when the Arduino board is ready
when board.board_ready then
E.configI2C(0, board.id) "Enables I2C comunication";
E.registerI2CDevice(device.address, board.id) "Registers a device with the given address attached to the board with the provided id";
end when;
end Sensor;

The variable "board.board_ready" is set to true when the Arduino board is ready to start receiving any operation. Registering the board requires two steps: enabling the I2C communications and sending the address of the attached device.

After the board has been registered, you can start performing transactions at each sample point. Two variables are used to control the communication: "board.i2c_ready" and "board.sample". The first one, "board.i2c_ready" becomes true when the Arduino board is ready to start sending and receiving I2C commands. The second, "board.sample" is triggered every time the Arduino synchronizes by sending and receiving information. These two variables are used by the when statement where all transactions occur.


model Sensor
ModelPlug.Internal.Interfaces.PinConnector board;
import I2C = ModelPlug.Sensors.I2C;
I2C.Device device(address = 107, id = board.id) "Device information";
I2C.State state "Used to keep track of the transactions";
I2C.Transaction t[5] "Vector of transactions, one per each read/write operation we are performing";
Boolean done "Flag to signal when all transactions are performed";
algorithm
// Triggered only once when the Arduino board is ready
when board.board_ready then
E.configI2C(0, board.id) "Enables I2C comunication";
E.registerI2CDevice(device.address, board.id) "Registers a device with the given address attached to the board with the provided id";
end when;
// Triggered every time the Arduino is ready to receive and send I2C information
when board.i2c_ready and board.sample then
end when;
end Sensor;

Up to this point, the code presented applies to any I2C device that you want to communicate with. Now you are going to start writing the specific transactions needed by the LSM6DS3 sensor.

Before creating a driver for a new sensor, it is necessary to know the steps to configure it and read it. The LSM6DS3 is an accelerometer and gyroscope sensor and the datasheet can be found here. In this case, the most basic way of using it will be described. Enabling some of the advanced features requires reading the datasheet in detail and knowing the functions of each register.

Using the LSM6DS3 in basic mode requires the following steps:

  • Enabling the accelerometer by writing the value 0x10 to the register 0x10 (16 in decimal). This is performed only once during the Setup stage.
  • Reading the x, y and z acceleration values from registers 0x29 (41), 0x2B (43) and 0x2D (45). This is performed continuously during the Loop stage.
  • Convert the read values to gravity units (g).

Performing these operations requires 5 transactions: 1 for enabling the accelerometer, 3 for reading the registers and 1 to end all transactions. First, send the Setup stage transactions.


when board.i2c_ready and board.sample then
// Setup
(state, t[1]) := I2C.writeByte(device, state, t[1], I2C.Stage.Setup, 16, 16);
end when;

In this case, the function "I2C.writeByte" is used to write a single value to the register 16 (0x10). This function takes as arguments the "device", the current "state", the transaction "t[1]", the stage "I2C.Stage.Setup", the register number 16 and the value, which is also 16. Transactions for the stage "I2C.Stage.Setup" are performed one time only.

Once you have configured the sensor, you can start reading the accelerometer values in the Loop stage.


when board.i2c_ready and board.sample then
// Setup
(state, t[1]) := I2C.writeByte(device, state, t[1], I2C.Stage.Setup, 16, 16);
// Loop
(state, t[2], iX) := I2C.readByte(device, state, t[2], I2C.Stage.Loop, 41, iX);
(state, t[3], iY) := I2C.readByte(device, state, t[3], I2C.Stage.Loop, 43, iY);
(state, t[4], iZ) := I2C.readByte(device, state, t[4], I2C.Stage.Loop, 45, iZ);
end when;

The arguments to the "I2C.readByte" function are the "device", the "state", the transaction, the stage (in this case "I2C.Stage.Loop") and the register number (41, 43 and 45). The last argument is the value of the variable that will store the read value. It is possible to see that when reading the acceleration for the X axis, you store it in the variable iX and you provide the same variable as argument.

Once you have performed all read and write operations you need to end all transactions. This is done by using the function "I2C.endTransactions". You can see in the following piece of code that the function "I2C.endTransactions" resturs a Boolean value "done", which becomes true when all reads and writes are completed and you are ready to repeat all the Loop transacions. You can use this flag to trigger the computation of the actual acceleration values X, Y and Z. To calculate the final values, you use the utility functions provided in ModelPlug.Internal.Utilities to convert 2 bytes into 16-bit signed integers. The library ModelPlug.Internal.Utilities contains a few useful functions to perform conversions and also to perform common bitwise operations.


when board.i2c_ready and board.sample then
// Setup
(state, t[1]) := I2C.writeByte(device, state, t[1], I2C.Stage.Setup, 16, 16);
// Loop
(state, t[2], iX) := I2C.readByte(device, state, t[2], I2C.Stage.Loop, 41, iX);
(state, t[3], iY) := I2C.readByte(device, state, t[3], I2C.Stage.Loop, 43, iY);
(state, t[4], iZ) := I2C.readByte(device, state, t[4], I2C.Stage.Loop, 45, iZ);
// End
(state, t[5], done) := I2C.endTransactions(state, t[5]);
if done then
X := ModelPlug.Internal.Utilities.signed16(iX, 0) * 0.061 / 1000;
Y := ModelPlug.Internal.Utilities.signed16(iY, 0) * 0.061 / 1000;
Z := ModelPlug.Internal.Utilities.signed16(iZ, 0) * 0.061 / 1000;
end if;
end when;

The final code looks as follows. Note that the 'initial equation' section has been included to provide initial values to all the variables and eliminate warnings.


model Sensor
import E = ModelPlug.Internal.ExternalFunctions;
import I2C = ModelPlug.Sensors.I2C;
Internal.Interfaces.PinConnector board;
discrete Modelica.Blocks.Interfaces.RealOutput X;
discrete Modelica.Blocks.Interfaces.RealOutput Y;
discrete Modelica.Blocks.Interfaces.RealOutput Z;
protected
I2C.Device device(address = 107, id = board.id);
I2C.State state;
I2C.Transaction t[5];
Boolean done;
Integer iX, iY, iZ;
initial equation
t = fill(I2C.initTransaction, size(t, 1));
X = 0;
Y = 0;
Z = 0;
iX = 0;
iY = 0;
iZ = 0;
state = I2C.initState;
done = false;
algorithm
// Triggered once when the Arduino board is ready
when board.board_ready then
E.configI2C(0, board.id);
E.registerI2CDevice(device.address, board.id);
end when;
// Triggered every sample once the I2C bus is ready
when board.i2c_ready and board.sample then
// Setup
(state, t[1]) := I2C.writeByte(device, state, t[1], I2C.Stage.Setup, 16, 16);
// Loop
(state, t[2], iX) := I2C.readByte(device, state, t[2], I2C.Stage.Loop, 41, iX);
(state, t[3], iY) := I2C.readByte(device, state, t[3], I2C.Stage.Loop, 43, iY);
(state, t[4], iZ) := I2C.readByte(device, state, t[4], I2C.Stage.Loop, 45, iZ);
(state, t[5], done) := I2C.endTransactions(state, t[5]);
if done then
X := ModelPlug.Internal.Utilities.signed16(iX, 0) * 0.061 / 1000;
Y := ModelPlug.Internal.Utilities.signed16(iY, 0) * 0.061 / 1000;
Z := ModelPlug.Internal.Utilities.signed16(iZ, 0) * 0.061 / 1000;
end if;
end when;
end Sensor;

Wolfram Language

In[1]:=
SystemModel["ModelPlug.Internal.I2C"]
Out[1]:=