The SystemDefinition is a required configuration object that describes all I2C devices in your system, their registers, commands, and data formats. It serves as the bridge between your high-level application code and the low-level I2C hardware details.
A temperature sensor register that returns a 16-bit signed value:
from instro.i2c.types import DataFormatRegisterDef( alias="TEMPERATURE", # Human-readable name register=0x05, # Register address default_value=0x0000, # Power-on reset value format=DataFormat(transfer_bits=16, signed=True), # 16-bit signed data endianness="big", # "little" or "big" for multi-byte data # No fields - read/write the entire register value)
Bit fields allow you to access specific bits within a register without manually masking and shifting. Each field can span multiple bits if needed.FieldDef Parameters:
name: Field identifier
lsb: Least significant bit position (0-indexed from the right)
width_bits: Number of bits in the field (default: 1)
Example with Multi-Bit Fields:
from instro.i2c.types import FieldDeffields = { "enable": FieldDef(name="enable", lsb=0, width_bits=1), # Single bit (Bit 0) "mode": FieldDef(name="mode", lsb=1, width_bits=2), # Two bits (Bits [2:1]) "rate": FieldDef(name="rate", lsb=3, width_bits=4), # Four bits (Bits [6:3])}
When reading or writing fields, I2CInterface automatically handles the masking and bit shifting:
# Read just a specific LED fieldled3_state = i2c.read("gpio_expander", "LED_OUTPUT_STATE", field="led3")# Write to a specific LED (read-modify-write operation)i2c.write("gpio_expander", "LED_OUTPUT_STATE", 1, field="led0") # Turn on LED 0# Read the entire register (all LEDs at once)all_leds = i2c.read("gpio_expander", "LED_OUTPUT_STATE")
from instro.i2c.types import DataFormat, LinearScalingformat = DataFormat( transfer_bits=16, # Total bits transferred over I2C data_width_bits=12, # Logical data bits (if different from transfer_bits) data_lsb=4, # Starting bit position of logical data signed=True, # Whether data is signed scaling=LinearScaling(gain=0.1, offset=-40.0), # Scaling function units="°C" # Physical units)
from instro.i2c.types import LinearScaling# Temperature sensor: 0.1°C per count, -40°C offset# Raw value 500 → (500 * 0.1) - 40 = 10.0°Cscaling = LinearScaling(gain=0.1, offset=-40.0)
from instro.i2c.types import CustomScaling# ADC with voltage divider: V = (ADC_value / 4095) * 5.0 * 7.2scaling = CustomScaling( to_physical_fn=lambda x: x / 4095 * 5.0 * 7.2, to_raw_fn=lambda v: int(v / 7.2 / 5.0 * 4095))
CustomScaling Parameters:
to_physical_fn: Function to convert raw integer → physical float
to_raw_fn: Optional function to convert physical float → raw integer (required for write operations)
Inverse Transformation Required for WritesIf you plan to write values to registers that use custom scaling, you must provide to_raw_fn. Otherwise, NotImplementedError will be raised when attempting to convert physical values back to raw.
Use descriptive names: Device and register names should be clear and match your hardware documentation
Define all registers: Include all registers you’ll access, even if not all fields are defined
Use field definitions: For registers with multiple settings, define fields to simplify read-modify-write operations
Set default values: Define default_value for registers to enable reset_reg() functionality
Use scaling functions: Define scaling for all sensor data to work in physical units
Validate addresses: Ensure I2C addresses are 7-bit (0x00-0x7F) and don’t conflict
Document units: Always specify units in DataFormat for clarity
SystemDefinition ReusabilityYou can reuse a SystemDefinition across multiple I2CInterface instances if they share the same I2C bus configuration. Create the definition once and pass it to multiple instruments.