telos I2C equipment and Python

telos I2C products such as telos Tracii XL 2.0 or the telos I2C Negative Tester are outstanding tools for the development of I2C related products and offer a wide range of functions that are useful if not even mandatory for the analysis of the behaviour of I2C bus systems. All these features can be intuitively applied using the GUI application I2C Studio.

However, I2C bus systems are usually not an isolated element in a product and in certain cases developers might want to control the I2C development tools using a script language rather than using a GUI application. Scripts allow certain test szenarios to be repeatedly performed and may also allow the integration of other development devices like CAN tracers or logic analysers in a complex test. Such scripts may also automatically check the results of the programmed operations.

During the past years, Python became a common language for a wide range of applications. So, the wish to control a Tracii XL 2.0, a Connii or a I2C Negative Tester by a Python script has become obvious. Since telos provides .NET-libraries to access its I2C products, the easiest way to integrate I2C accesses into existing python scripts is by using IronPython as Python interpreter. IronPython can execute scripts that are compliant to Python 2.7 and has been extended to support the access to .NET libraries, also. This article shall provide a brief introduction into the usage of IronPython in order to access telos I2C products via Python scripts.

IronPython

IronPython is an open source implementation of Python based and for the .NET environment.IronPython is provided by the .NET Foundation. It is open source and licensed under the Apache License, Version 2. A windows installer for the version 2.7.9 can be downloaded at GitHub. After the download, the windows installer has to be executed. It guides through the installation process.

When the installation is finished, IronPython could already be used. However, the PATH variable still needs a manual update to provide convenient access to the interpreter. Once, the PATH is properly set, the interpreter can be started by typing “ipy” into the command line of a console window.

Preparation for I2C Access

In order to access the telos I2C Products that are connected to your PC’s USB port, the python interpreter needs access to the .NET libraries provided by telos and that are part of the distributions of I2C Studio. The easiest way is to copy them into the project directory where the Python scripts will be started from, later. The libraries can be found in the subdirectory “lib/dotnet/x86” or “lib/dotnet/x64” of your I2C Studio installation. The files, which have to be copied, are i2capi_dotnet_net40.xml and i2capi_dotnet_net40.dll.

The first Script: Board Enumeration

Now, everything is in place to write and run a first script. The first example shows one way to perform a simple board enumeration. It shall print the serial numbers of all connected devices to the console. The successful execution is also a strong indication that the software has been properly set up.

The complete script will look like that:
import clr
clr.AddReference("i2capi_dotnet_net40")
from telos.I2cApi.DotNet import Board
device = Board()
board_list = device.ListOfBoards
for board in board_list:
print(str(board.BoardType) + ": " + hex(int(board.SerialNumber)))
device.Dispose()

If the script has been typed into the file “board_iteration.py”, it can be executed by typing “ipy board_iteration.py”. As a result, one or more lines should be printed to the console. An example result might look like that:
C:\Users\Sandbox\Documents\tmp>ipy board_iteration.py
DUMMY: 0xa900
TRACII_XL2: 0x10ef
TRACII_XL2: 0x6cc
C:\Users\Sandbox\Documents\tmp>

The script conists of a number of parts that shall be expained, here:

Code Explanation
import clr This line imports the ‘clr’ module, which is needed to load .NET assemblies into the Python environment.
clr.AddReference("i2capi_dotnet_net40") The I2CAPI is loaded into the Python environment.
from telos.I2cApi.DotNet import Board The class Board is imported. So, the class can be referenced by its short name (“Board”) rather than by its complete name (“telos.I2cApi.DotNet.Board”).
device = Board() A board instance is created. This instance is not jet bound to a certain hardware. However, it already provides access to the list of boards.
board_list = device.ListOfBoards The list if I2C devices is assigned to ‘board_list’.

for board in board_list:
print(str(board.BoardType) + ": " + hex(int(board.SerialNumber)))
Here, the script enumerates the found devices on the console. Actually, there is much more information available than the type and the serial number, but for this example, it should be sufficient. A complete reference of the classes and their members can be found in the I2C.NET API help file, which is part of the I2C Studio installation.
device.Dispose() Finally, an open board should always be disposed. Otherwise, the execution of a script that worked well till the end, will crash in the very last moment.

The first I2C transfer issued by a Script

Important: In this article, you’ll find scripts that perform I2C transmission to and from slaves. The exchanged data is randomly chosen with no specific slave device in mind. The focus is on the transmission itself and not on the way, the devices at the bus would respond to it. However, you, as the user, have to take care that the operations don’t cause any damage. It is recommended to use a bus with sample devices (e.g. memory chips) as targets for these examples.

The next script will use a board to receive an I2C message (RX) from a slave. The script will request 5 bytes from a slave at address 0x50. First, the complete code:
import clr
clr.AddReference("i2capi_dotnet_net40")
from telos.I2cApi.DotNet import Board
from telos.I2cApi.DotNet import I2cAddress
from telos.I2cApi.DotNet import MasterMessageRx

address = I2cAddress(0x50, False)
message_rx = MasterMessageRx(address, 5)

operational_device = None
device = Board()
board_list = device.ListOfBoards
for board in board_list:
if str(board.BoardType) == "DUMMY":
continue
if not board.Master:
continue
operational_device = Board()
operational_device.Open(board)
break
print("Opening " + str(board.BoardType) + ": " + hex(int(board.SerialNumber)))
device.Dispose()

if (operational_device == None):
print("No device matches the requirements.")
exit()

try:
operational_device.Master.TransferData(message_rx)
finally:
operational_device.Dispose()
operational_device = None

print(message_rx.Data)

The following lines and blocks are new:


from telos.I2cApi.DotNet import I2cAddress
from telos.I2cApi.DotNet import MasterMessageRx
In this script, we use new types that have to be imported. For details, check the documentation of the I2C.NET API.
address = I2cAddress(0x50, False) We create an object that represents an I2C address.
message_rx = MasterMessageRx(address, 5) A RX-message is defined that targets the slave at the given address and requests 5 bytes.
operational_device = None The Board enumeration shall result in a board object that is connected to a physical hardware. After a successful enumeration, that Board object shall be assigned to ‘operational_device’.

if str(board.BoardType) == "DUMMY":
continue
if not board.Master:
continue
We neither want to use a “DUMMY” device nor do we want to use a device that cannot perform operations as I2C master. Such devices are skipped.

operational_device = Board()
operational_device.Open(board)
Once, we have found a hardware that matches our expectations, we create a Board object and assign it to that hardware by opening it.
operational_device.Master.TransferData(message_rx) The heart of the script: The hardware is called to perform the transmission.
print(message_rx.Data) Finally, the received data is printed to the console.

Separate Module for Board Management

In order to keep the overview in future scripts, we put the code for opening and disposure of boards in a separate module and call it i2c_boards.py. The script also tries to open a board that can be used as a tracer and a board that can be used as a negative tester:

import clr
clr.AddReference("i2capi_dotnet_net40")
from telos.I2cApi.DotNet import Board
global i2c_master_device
global i2c_negative_tester_device
global i2c_tracer_device
i2c_master_device = None
i2c_negative_tester_device = None
i2c_tracer_device = None
device = Board()
board_list = device.ListOfBoards
for board in board_list:
if str(board.BoardType) == "DUMMY":
continue
if board.Master and i2c_master_device == None:
i2c_master_device = Board()
i2c_master_device.Open(board)
print("Master: " + hex(int(board.SerialNumber)))
if board.Tracer and i2c_tracer_device == None:
i2c_tracer_device = Board()
i2c_tracer_device.Open(board)
print("Tracer: " + hex(int(board.SerialNumber)))
if board.NegativeTester and i2c_negative_tester_device == None:
i2c_negative_tester_device = Board()
i2c_negative_tester_device.Open(board)
print("Negative Tester: " + hex(int(board.SerialNumber)))
device.Dispose()
if i2c_master_device == None:
print("Master: ---")
if i2c_tracer_device == None:
print("Tracer: ---")
if i2c_negative_tester_device == None:
print("Negative Tester: ---")
def disposeBoards():
if not (i2c_master_device == None):
i2c_master_device.Dispose()
if not (i2c_negative_tester_device == None):
i2c_negative_tester_device.Dispose()
if not (i2c_tracer_device == None):
i2c_tracer_device.Dispose()

A script that shall perform an I2C operation, simply has to import the corresponding board variable (in the example below i2c_master_device) and finally has to call the function disposeBoards().

from ipy_board import i2c_master_device
from ipy_board import disposeBoards
from telos.I2cApi.DotNet import I2cAddress
from telos.I2cApi.DotNet import MasterMessageRx
address = I2cAddress(0x50, False)
message_rx = MasterMessageRx(address, 5)
try:
i2c_master_device.Master.TransferData(message_rx)
finally:
disposeBoards()
print(message_rx.Data)

Exception Handling

In the last scripts, the transmission itself was surrounded by a try-finally block. The reason is, that attempts to perform I2C bus operations can cause exceptions when the I2C bus isn’t properly set up. Two very common reasons for such exceptions are a missing or insufficient power supply of the I2C bus or a missing slave device for the desired address. By placing the dispose-operation in the finally block, the script is stopped in such cases but doesn’t crash.

Typed Arrays

The line
print(message_rx.Data)
prints something like that to the console:
List[Byte]([0, 0, 0, 0, 0])
This is obviously not a typical Python-style array. In order to access the array in Python-style, the array has to be converted. That can be done by
array = list(message_rx.Data)
In some places, typed arrays are required as parameters for methods. E.g., when TX messages have to be defined. The code below shows how a classic python array (message) can be converted into a .NET-like array of bytes (byte_list) which can serve as a parameter for the constructor of a TX message.
message = [1, 2, 3, 4]
byte_list = Array[Byte](message)
message_tx = MasterMessageTx(address, byte_list)

In order to send a TX message with the content [1, 2, 3, 4] to the address 0x70, we can use a script like this:
from ipy_board import i2c_master_device
from ipy_board import disposeBoards
from telos.I2cApi.DotNet import I2cAddress
from telos.I2cApi.DotNet import MasterMessageTx
from System import Array
from System import Byte

data = [1, 2, 3, 4]

typed_array = Array[Byte](data)

address = I2cAddress(0x70, False)
message_tx = MasterMessageTx(address, typed_array)

try:
i2c_master_device.Master.TransferData(message_tx)
finally:
disposeBoards()

Tracing the I2C Bus Traffic

telos Tracii XL 2.0

Tracing the traffic on the bus is, certainly, also possible. The script below shows a simple example.
from ipy_board import i2c_tracer_device
from ipy_board import disposeBoards
from telos.I2cApi.DotNet import I2cAddress
from telos.I2cApi.DotNet import TracerMessageList

trace = TracerMessageList()
try:
i2c_tracer_device.Tracer.TracerMsgList = trace
i2c_tracer_device.Tracer.EnableAllAddresses()
i2c_tracer_device.Tracer.Enabled = True
cnt = 0
while cnt < 50:
i2c_tracer_device.WaitForData(1000000)
cnt = cnt + i2c_tracer_device.Tracer.ReceiveMessages()
print("Received Messages: " + str(cnt) + ". Waiting for " + str (50 - cnt) + " messages.")
i2c_tracer_device.Tracer.Enabled = False
finally:
disposeBoards()
for x in trace:
print("Nachricht: " + str(x.Timestamp) + ": " + str(x.Type) + " " +  str(x.Data.Data) + " " +  str(x.Data.Confirmation))

When the tracer is started, it waits for 50 messages. If 50 messages (or more) have been received, the tracer is stopped and the results from the trace are being printed.

Combining Master and Tracer

Of course, it is possible to apply a combination of features of a Tracii XL (2.0) device in a single script. E.g., it is possible to induce I2C transfers via the master and trace that traffic by the tracer feature of the same device. A reason for such a setup might be that the timing of a transmission is of higher interest than the content that has been transmitted.
An example script is shown below. Once, the tracer has been activated, 4 RX messages are induced. After that, the script waits until a certain number of trace messages has been received. After the reception, the result is printed.

from ipy_board import i2c_master_device
from ipy_board import i2c_tracer_device
from ipy_board import disposeBoards
from telos.I2cApi.DotNet import I2cAddress
from telos.I2cApi.DotNet import TracerMessageList
from telos.I2cApi.DotNet import MasterMessageRx

address = I2cAddress(0x50, False)
message_rx = MasterMessageRx(address, 5)
trace = TracerMessageList()
try:
i2c_tracer_device.Tracer.TracerMsgList = trace
i2c_tracer_device.Tracer.EnableAllAddresses()
i2c_tracer_device.Tracer.Enabled = True
i2c_master_device.Master.TransferData(message_rx)
i2c_master_device.Master.TransferData(message_rx)
i2c_master_device.Master.TransferData(message_rx)
i2c_master_device.Master.TransferData(message_rx)
cnt = 0
while cnt < 30:
i2c_tracer_device.WaitForData(1000000)
cnt = cnt + i2c_tracer_device.Tracer.ReceiveMessages()
print("Received Messages: " + str(cnt) + ". Waiting for " + str (50 - cnt) + " messages.")
i2c_tracer_device.Tracer.Enabled = False
finally:
disposeBoards()
for x in trace:
print("Nachricht: " + str(x.Timestamp) + ": " + str(x.Type) + " " +  str(x.Data.Data) + " " +  str(x.Data.Confirmation))