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. Such 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, an easy way to integrate I2C accesses into existing python scripts is by using packages like python.NET.

This article provides examples that can serve as a starting points for individual solutions that use telos I2C products via Python scripts. The first section shows, how to use the master capabilties via a script. The Tracer is used in the second script and in the final example a Negative Tester is used in combination with a Tracer.

Step by Step: A simple Master Script

The first example demonstrates, how I2C-master-messages can be sent and received using a Python script.

Preparation

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 following examples are based on the assumption that these libraries are located in the same folder as the script itself. So, the files i2capi_dotnet_net40.xml and i2capi_dotnet_net40.dll should be copied from the subdirectory “lib/dotnet/x86” or “lib/dotnet/x64” of your I2C Studio installation to the script directory.

Imports from Python- and .NET-World

In order to be able to use elements from the .NET world in a python script, the module clr must be imported, first:

import clr

That clr module provides a function to add a reference to a .NET-DLL. By calling that function, the I2C.NET API becomes available to the Python script:

clr.AddReference("i2capi_dotnet_net40")

Built-in .NET types are available now and some of them have to be imported for the following script:

from System import Enum
from System import Array
from System import Byte
from System import UInt32
from System import UInt16

Beside the built-in types, elements of the I2C.NET API have to be imported, also:

from telos.I2cApi.DotNet import Board
from telos.I2cApi.DotNet import BoardType
from telos.I2cApi.DotNet import I2cAddress
from telos.I2cApi.DotNet import Master
from telos.I2cApi.DotNet import MasterMessageTx
from telos.I2cApi.DotNet import MasterMessageRx
from telos.I2cApi.DotNet import MasterMessageList

Selecting the I2C Adapter

Before a script can perform any I2C-transfers, it has to connect to a telos I2C adapter. Therefore, a Board object has to be created.

board_master = Board()

Once, the Board object has been created, it has to be bound to a certain I2C-adapter by calling the object’s Open()-method. There are several variants of that method available and in this example, we focus on the Open()-method that requires a HardwareInfo-object as parameter.

For each I2C adapter that is connected to the PC, such a HardwareInfo-object is available. A complete list is provided by the Board object:

hardware_list = board_master.ListOfBoards

Even though the name HardwareInfo suggest something else, there is always one HardwareInfo object available which describes the DUMMY device, which is only a software simulation of an I2C adapter. For that reason, the ListOfBoards property usually provides two or more entries if one or more I2C adapters are connected to the computer. So, it is necessary that the script choses the desired board. In Python, filters provide an elegant way to reduce the number of available elements to those which are actually useful.

pysical_hw_list = filter(lambda brd: not Enum.GetName(BoardType, brd.BoardType) == "DUMMY", hardware_list)

As the script will work only on boards that can operate as I2C-masters, the list should be further reduced by removing non-master boards:

master_hw_list = filter(lambda brd: brd.Master, pysical_hw_list)

Now, the ‘list’ should contain exactly a single entry. That’s the HardwareInfo object which is going to be used to establish the connection:

board_master.Open(master_hw_list[0])

From now on it is possible to control the I2C-Adapter via the python script. For example, the I2C power supply can be set, if desired. The following line instructs the I2C-Adapter to apply 3,3 V:

board_master.I2cVccSupply = 3300

If the I2C-Bus must not be powered from the I2C-Adapter, the value should be set to 0, instead:

board_master.I2cVccSupply = 0

Transfering I2C Messages

Its time to prepare the actual I2C messages that shall be transferred. The first example shows a TX-message that sends the bytes 0x01, 0x02, 0x03 to the slave with the 7-bit-address 0x52. The slave-address is represented by a dedicated object:

i2c_address = I2cAddress(UInt16(0x52), False)

Its important not to apply the literal ‘0x52’ directly but to convert it explicitly into a UInt16-Object. When the CLR system looks for a matching constructor, it requires an exactly match and the python literal ‘0x52’ isn’t a UInt16 type. The actual pitfall is, that the system will use the default constructor if it doesn’t find a better one. So Python wouldn’t complain when the literal ‘0x52’ is directly used in the constructor. Instead, it would ignore that parameter and call the default constructor of I2cAddress. The result would be an Addressobject with the 7-bit-address 0x50 instead of 0x52 as the default constructor uses 0x50.

The data that shall be transmitted to the I2C-slave must be available in the correct .NET-type, too. To provide it correctly, the Python-style list of values must be converted into a .NET array:

data = Array[Byte]([1, 2, 3])

Finally, the master message object can be created and sent. Here, we use the board’s master-property, which provides the TransferData command.

message_tx = MasterMessageTx(i2c_address, data)
board_master.Master.TransferData(message_tx)

In order to receive data form an I2C-slave, a MasterMessageRx-Object has to be created. In order to receive 12 bytes from the slave, a corresponding object can be created in the following way:

message_rx = MasterMessageRx(i2c_address, UInt32(12))

Then, it can be transfer can be performed and after the successful transfer, the result can be evaluated.

board_master.Master.TransferData(message_rx)
print(message_rx.Data)

Final Cleanup

Before the script ends, the Board object’s Dispose()-Method should be called. Otherwise, the script would end with an exception complining about the missing Dispose()-call.

board_master.Dispose()

Step by Step: Tracing the I2C Bus Traffic

telos Tracii XL 2.0

Tracing the traffic on the bus is, certainly, also possible, provided that an Adapter with tracer capabilities is connected to the PC.

The first steps (importing necessary packages, and adding references) are similar to the example above except for certain packages that are dedicated to tracer functionality only.

from time import sleep
clr.AddReference("i2capi_dotnet_net40")
from telos.I2cApi.DotNet import Board
from telos.I2cApi.DotNet import BoardType
from telos.I2cApi.DotNet import TracerMessageList
from telos.I2cApi.DotNet import TracerMessageType
from telos.I2cApi.DotNet import TracerMessageDataConfirmation
When we select the desired board, we have to look for a board with the capability to trace not to operate as a master. Therefore, the applied filter differs slightly:
board_tracer = Board()
list_of_boards = board_tracer.ListOfBoards
pysical_devices = filter(lambda hardware: not Enum.GetName(BoardType, hardware.BoardType) == "DUMMY", list_of_boards)
tracer_devices = list(filter(lambda hardware: hardware.Tracer, pysical_devices))
hw_tracer = tracer_devices[0]
board_tracer.Open(hw_tracer)
For this example, the tracer is instructed to forward all traced messages without filtering:
board_tracer.Tracer.EnableAllAddresses()
Before the tracer is finally started (currently, its still inactive), a container must be provided where the traced data should be stored.
trace = TracerMessageList()
board_tracer.Tracer.TracerMsgList = trace
Once, the tracer is properly set up, it can be activated:
board_tracer.Tracer.Enabled = True
Now, its time to wait for incoming tracedata.
Note: In the context of the tracer, a message is a small action on the bus. This can be a start-condition, a stop-condition or a transmitted data-byte including its acknowledge. So, an I2C-message always consits of three or more tracer messages.
This is done in a loop which checks the number of received messages and exits as soon as messages are available:
message_count = board_tracer.Tracer.ReceiveMessages()
while message_count == 0:
  sleep(0.1)
  message_count = board_tracer.Tracer.ReceiveMessages()
Now, the reference to the current message container is kept and a new container is assigned to the tracer.
tmp = trace
trace = TracerMessageList()
board_tracer.Tracer.TracerMsgList = trace
The TracerMessageList tmp isn’t filled any longer and its content can be processed (e.g. printed to the console).
for tracer_sample in tmp:
   str_ts = str(tracer_sample.Timestamp)
   str_type = Enum.GetName(TracerMessageType, tracer_sample.Type)
   str_data = '{:02x}'.format(tracer_sample.Data.Data)
   print(str_ts + " " + str_type + " " + str_data)

Step by Step: Using the Negative Tester

This example shows how the Negative Tester can be instructed to generate test patterns while a tracer samples the generated traffic on the bus.

Imports

Similar to the previous examples, this one starts with some imports:
import sys
from time import sleep

import clr

clr.AddReference("i2capi_dotnet_net40")

from System import Enum
from System import Array
from System import Byte
from System import UInt32
from System import UInt16
from System import Boolean

from telos.I2cApi.DotNet import Board
from telos.I2cApi.DotNet import BoardType
from telos.I2cApi.DotNet import NegativeTester
from telos.I2cApi.DotNet import NegativeTesterMaster
from telos.I2cApi.DotNet import TracerMessageList
from telos.I2cApi.DotNet import TracerMessageType
from telos.I2cApi.DotNet import TracerMessageDataConfirmation
from telos.I2cApi.DotNet import I2cAddress

Preparing the Testsequence

In the next step, the testsequence is going to be defined. In this example, the sequence shall consist of a number of TX messages that are sent to a slave. All messages shall contain the same payload: 10 bytes with increasing values from 0 to 9. Next, the objects representing the addresses and the payloads are created:
i2c_address_1 = I2cAddress(UInt16(0x50), False)

message_data_raw = list(map(lambda value: value & 0x00FF, range(10)))
message_data_array = Array[Byte](message_data_raw)
Now, everything is prepared to create the master testsequence. Such a sequence is contained by a dedicated NegativeTesterMaster object:
nt_sequence = NegativeTesterMaster()
It is often desired to repeat a certain testsequence for a number of times. This repetition can be accomplished by setting a loop counter. The follwing line will cause the Negative Tester to perform the sequence 5 times:
nt_sequence.LoopCount = 5
In this example, a given message should be repeatedly sent using different bit rates. The class NegativeTesterMaster provides a property Bitrate that defines the bitrate which shall be applied to the message that follows.
A TX-message is added to the sequence by calling the method SendTransmitter(). So, the folling instructions tell the Negative Tester to send three messages with the same payload to the same slave but at different bitrates (100kHz, 400kHz and 300kHz):
nt_sequence.Bitrate = UInt32(100000)
nt_sequence.SendTransmitter (i2c_address_1, message_data_array)

nt_sequence.Bitrate = UInt32(400000)
nt_sequence.SendTransmitter (i2c_address_1, message_data_array)

nt_sequence.Bitrate = UInt32(300000)
nt_sequence.SendTransmitter (i2c_address_1, message_data_array)

Connecting to the I2C boards

Since the Negative Tester is only responsible for the generation of testsequences, a tracer is also required in order to sample the results. So, the following script needs to access to boards.
board_tracer = Board()
board_negative_tester = Board()
pysical_devices = list(filter(lambda hardware: not Enum.GetName(BoardType, hardware.BoardType) == "DUMMY", board_tracer.ListOfBoards))
negative_testers = list(filter(lambda hardware: hardware.NegativeTester, pysical_devices))
tracers = list(filter(lambda hardware: hardware.Tracer, pysical_devices))
board_tracer.Open(tracers[0])
board_negative_tester.Open(negative_testers[0])

Running the Test

This example can be executed with a Negative Tester directly connected to a Tracer. In that case, the software slave should be enabled via I2C Studio. In such a setup, either the Negative Tester or the Tracer has to provide the power supply for the I2C bus. The example choses the Negative Tester as source:
board_tracer.I2cVccSupply = 0
board_negative_tester.I2cVccSupply = 3300
Its time to start the Tracer:
trace = TracerMessageList()
board_tracer.Tracer.TracerMsgList = trace
board_tracer.Tracer.EnableAllAddresses()
board_tracer.Tracer.Enabled = True
When the Tracer is running (and waiting for traffic), the Negative Tester should be started and the script shall wait until it has performed the defined sequences:
negative_tester.StartMaster(nt_sequence.GetMessages())
while negative_tester.IsRunning:
	sleep(1)
At this point of time, the messages have been transferred and the traced messages have to be captured:
nt_sequence_total = 0
message_count = board_tracer.Tracer.ReceiveMessages()
nt_sequence_total = nt_sequence_total + message_count
while message_count > 0:
	message_count = board_tracer.Tracer.ReceiveMessages()
	nt_sequence_total = nt_sequence_total + message_count

Evaluating the Results

As soon as the tracer message list contains the results, its time to evaluate them. In this simple example, the script only prints the messages to the console:
start_timestamp = trace[0].Timestamp
for message in trace:
    line = str(message.Timestamp - start_timestamp).rjust(15)
    line += ": "
    enum_name_message_type = Enum.GetName(TracerMessageType, message.Type)
    line += enum_name_message_type.replace("MSG_FRAME_","").ljust(8)
    
    if "MSG_FRAME_DATA" == Enum.GetName(TracerMessageType, message.Type):
        enum_name_confirmation_type = Enum.GetName(TracerMessageDataConfirmation, message.Data.Confirmation)
        line += enum_name_confirmation_type.replace("DATA_CONFIRMATION_", "").ljust(6)
        line += '0x{:02x}'.format(message.Data.Data).upper().rjust(3)
    print(line)

Finally doing Cleanup

As a very last step, the two boards have to be disposed:
board_tracer.Dispose()
board_negative_tester.Dispose()