Skip to main content

I2CInterface

I2CInterface is a hardware abstraction layer (HAL) that provides a unified interface for I2C communication across multiple adapter vendors. The key benefit is vendor-independent code: create your SystemDefinition once, and the same code works with different I2C adapters.

Supported Vendors

  • Total Phase - Aardvark I2C/SPI Host Adapter
If your adapter vendor is not listed, custom driver development is available to add support for your hardware.

Key Concepts

System Definition

I2CInterface’s architecture utilizes a SystemDefinition to provide a low-code, human-readable way to interact with I2C devices on the bus. This design reduces the need for magic numbers, manual bitwise operations, and scattered hardware knowledge throughout your test code. The SystemDefinition serves as a single source of truth about your I2C bus configuration, centralizing:
  • Device addresses and names
  • Register maps and bit field definitions
  • Data formats and scaling functions
  • Command definitions for command-based devices
Instead of writing code like:
You write code like:
See the System Definition page for detailed information on creating and configuring your SystemDefinition.
Create your SystemDefinition in a separate file and import it into your test code. This cleanly separates hardware configuration (typically done by a hardware/firmware engineer) from the test and automation logic (usually the test engineer’s domain), leading to more maintainable and collaborative code.

Lifecycle Pattern

The typical I2CInterface workflow follows this pattern:
  1. Create a SystemDefinition - Define all I2C devices, registers, and commands (see System Definition)
  2. Construct I2CInterface(name, driver=..., system_definition=...) - Compose a vendor driver (e.g. Aardvark) and pass it directly
  3. open() - Establish connection to the I2C adapter hardware
  4. start() - Begins a periodic daemon in the background. (Optional)
  5. Configure and communicate - Access registers, fields, or send commands to devices
  6. stop() - End background daemon (if started)
  7. close() - Disconnect from hardware
Custom Background Daemon
  • To define your own background daemon, call define_background_daemon().
  • To add a method to the background daemon stack, call add_background_daemon_function().
See Two ways to get data for more information regarding background fetching of measurements.
Important Note about PublishersData is published as a direct result of an instrument method being called.For example, when you call read(), this not only returns a register value but also causes all attached Publishers to publish the response automatically.Therefore the background daemon, when calling these instrument methods, is publishing data in the background as well!

Device Types

I2CInterface’s System Definition supports two types of I2C devices:
  • Register-based devices - Devices with register maps (e.g., GPIO expanders, sensors with registers)
  • Command-based devices - Devices that respond to command bytes (e.g., ADCs that accept selection commands)
Both device types are configured in the SystemDefinition with human-readable names, allowing you to access devices without remembering raw I2C addresses.
You can read and write directly to the I2C bus using write_raw() and read_raw(), which bypasses the benefits provided by the SystemDefinition architecture. This is useful for using I2C devices that I2CInterface doesn’t yet provide lower-code interactions with, allowing you to move forward regardless.

Creating an I2CInterface Instance

Construct I2CInterface directly. The caller picks a vendor driver and passes it in. There is no factory method or vendor enum.

Parameters

  • name: A name for this I2C instance. Used as a prefix for channel names when using a publisher.
  • driver: An I2CDriverBase implementation (e.g. Aardvark(serial_number=...))
  • system_definition: Complete SystemDefinition object describing all I2C devices (required)
  • publishers: Optional list of publishers to attach
  • **kwargs: Additional keyword arguments become default tags when using a publisher that supports tags (like NominalCorePublisher).
SystemDefinition RequiredI2CInterface requires a SystemDefinition parameter. You cannot create a I2CInterface instance without first defining your I2C devices. See the System Definition page for details.

Examples

All measurement methods return Measurement objects. This is common amongst all Instrument objects. All examples below import from a shared system_definition.py file. This follows the recommended practice of separating hardware configuration from test logic.

System Definition File

First, create a system_definition.py file that defines your I2C bus configuration:

Basic Register Read/Write

Field-Level Register Access

For registers with bit fields, you can read and write individual fields:

Command-Based Device Query

For command-based devices (like ADCs):

Raw I2C Operations

For advanced use cases, you can bypass the system definition and use raw I2C operations:
Raw vs System Definition Methods
  • System definition methods (read(), write(), query()): Use device names and register aliases, handle data format conversion automatically
  • Raw methods (read_raw(), write_raw(), etc.): Direct I2C address and byte-level operations, no format conversion
Use system definition methods for most applications. Use raw methods only when you need direct control over I2C transactions.

Register Reset

Reset a register to its default value as defined in the system definition:
This is equivalent to writing the default_value specified in the RegisterDef.

Published channels

Every read/write produces a channel keyed under {name}.{descriptor}, where {name} is the constructor argument and {descriptor} is built from the device names you defined in your SystemDefinition. {peripheral}, {register_alias}, {field}, and {batch_command} are the names you assigned in your SystemDefinition. If you depend on the pre-v1.0 underscore-separator form (e.g. {name}_{peripheral}_{register} with a trailing _cmd), pass legacy_naming=True to the constructor.

Method Reference


Driver Development

This section is for developers implementing I2CInterface support for I2C adapter vendors that are not supported out of the box.

Overview

Driver developers implement the I2CDriverBase abstract interface to add support for new I2C adapter vendors. The driver is responsible for translating I2CInterface’s vendor-independent API calls into vendor-specific hardware operations, ensuring users get consistent behavior regardless of the underlying hardware.

Driver Responsibilities

An I2C driver must:
  1. Hardware connection lifecycle: Implement open() and close() for establishing and terminating hardware connections
  2. Basic I2C operations: Implement read(), write(), and write_read() for fundamental I2C transactions
  3. Hardware configuration: Implement set_bitrate(), set_pullups(), and set_power_enable() for adapter configuration
  4. Resource management: Properly manage hardware resources and thread safety

I2CDriverBase Interface

All I2C drivers must subclass I2CDriverBase and implement these abstract methods:

Required Methods

Driver Composition

Concrete I2C drivers own their transport SDK. I2CInterface calls driver.open() / driver.close() and forwards I2C transactions to the driver. The driver does not need a back-reference to the instrument.

Implementation Example: Total Phase Driver

Here’s a reference implementation for part of the Total Phase Aardvark driver:

Using Custom Drivers

For custom drivers, construct I2CInterface directly and pass your driver instance:

Summary

Driver development requires careful mapping of vendor-specific operations to the unified I2CDriverBase interface. Focus on:
  • Implementing all I2CDriverBase abstract methods
  • Handling 7-bit I2C addresses correctly (most vendors expect this)
  • Properly managing hardware resources (open/close)
  • Supporting both single and combined write-read operations
  • Testing with actual hardware to ensure commands work as expected