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 yourSystemDefinition once, and the same code works with different I2C adapters.
Supported Vendors
- Total Phase - Aardvark I2C/SPI Host Adapter
Key Concepts
System Definition
I2CInterface’s architecture utilizes aSystemDefinition 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
SystemDefinition.
Lifecycle Pattern
The typical I2CInterface workflow follows this pattern:- Create a
SystemDefinition- Define all I2C devices, registers, and commands (see System Definition) - Construct
I2CInterface(name, driver=..., system_definition=...)- Compose a vendor driver (e.g.Aardvark) and pass it directly open()- Establish connection to the I2C adapter hardwarestart()- Begins a periodic daemon in the background. (Optional)- Configure and communicate - Access registers, fields, or send commands to devices
stop()- End background daemon (if started)close()- Disconnect from hardware
- To define your own background daemon, call
define_background_daemon(). - To add a method to the background daemon stack, call
add_background_daemon_function().
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’sSystem 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)
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
ConstructI2CInterface 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: AnI2CDriverBaseimplementation (e.g.Aardvark(serial_number=...))system_definition: CompleteSystemDefinitionobject 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 (likeNominalCorePublisher).
Examples
All measurement methods returnMeasurement 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 asystem_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
Register Reset
Reset a register to its default value as defined in the system definition: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.
| Method | Descriptor | Type |
|---|---|---|
read(peripheral, register_alias) | {peripheral}.{register_alias} | telemetry |
read(peripheral, register_alias, field=...) | {peripheral}.{register_alias}.{field} | telemetry |
write(peripheral, register_alias, ...) | {peripheral}.{register_alias}.cmd | command |
write(peripheral, register_alias, field=..., ...) | {peripheral}.{register_alias}.{field}.cmd | command |
query(peripheral, batch_command) | {peripheral}.{batch_command} | telemetry |
{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
| Method | Purpose |
|---|---|
I2CInterface(name, driver, system_definition, publishers=None, legacy_naming=False, **kwargs) | Construct an I2CInterface instance composing a vendor driver |
open() | Establish connection to the I2C adapter |
close() | Disconnect from adapter and close all publishers |
read(peripheral, register_alias, field="", **kwargs) | Read a register or field from a register-based device |
write(peripheral, register_alias, value, field="", **kwargs) | Write to a register or field on a register-based device |
reset_reg(peripheral, register_alias, **kwargs) | Reset a register to its default value |
query(peripheral, batch_command, **kwargs) | Send command to a command-based device and read response |
read_raw(address, length, endianness) | Perform raw I2C read operation |
write_raw(address, data) | Perform raw I2C write operation |
write_read_raw(address, payload, length, endianness) | Write then read without stop condition |
write_then_read_raw(address, payload, length, endianness) | Write then read with stop condition between operations |
start() | Begin background telemetry daemon |
stop() | End background telemetry daemon |
add_publisher(publisher) | Attach a publisher for data routing |
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 theI2CDriverBase 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:- Hardware connection lifecycle: Implement
open()andclose()for establishing and terminating hardware connections - Basic I2C operations: Implement
read(),write(), andwrite_read()for fundamental I2C transactions - Hardware configuration: Implement
set_bitrate(),set_pullups(), andset_power_enable()for adapter configuration - Resource management: Properly manage hardware resources and thread safety
I2CDriverBase Interface
All I2C drivers must subclassI2CDriverBase 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, constructI2CInterface directly and pass your driver instance:
Summary
Driver development requires careful mapping of vendor-specific operations to the unifiedI2CDriverBase interface. Focus on:
- Implementing all
I2CDriverBaseabstract 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