Skip to content

Example: Bus and Device Creation

Example code and setups

This example demonstrates

  • how to create a bus and device interface on the Protocol Exerciser.
  • how to interact with the controller and fetch information from the target device.

Here we use I3C as an example, but the same principles apply to other bus types.

Create a new file called run_i3c_example.py and copy the following code into it:

run_i3c_example.py
import logging

from aqpxlib import AqProtocolExerciser
from aqpxlib.i3c import (
    ACUTE_DEFAULT_PID,
    PxI3CBus,
    PxI3CBaseController,
    PxI3C32BitRegisterTarget,
)
from aqpxlib.exceptions import PxAPIError

logger = logging.getLogger(__name__)

def exec_i3c_example():
    """Basic demonstration of how to interact with Acute's Protocol Exerciser.

    Steps will be performed in the following order:

    1. Create and open a connection to Acute's Protocol Exerciser.
    2. Setting proper configuration of voltage levels and pull-up resistors on the bus.
    3. Create an I3C Bus Handle Interface.
    4. Attach an I3C controller and a target device to the bus.
    5. Initialize the I3C bus to perform Dynamic Address Assignment procedure.
    6. Perform Write / Read data to the target device.
    7. Detach the devices and close the connection to the Protocol Exerciser.
    """

    with AqProtocolExerciser.connect(port=60600) as px_session:
        logger.info("Created an Exerciser session")

        # Configure an I3C Bus on SCL pin 0 and SDA pin 1
        i3c_bus = PxI3CBus(px_session, scl=0, sda=1)
        logger.info(f"Created {i3c_bus}")

        # Setting the voltage level to 3.3V and pull-up resistance to 1.5kΩ
        i3c_bus.voltage = 3300
        i3c_bus.pullup_resistance = 1500

        # Create a Controller device
        i3c_controller = PxI3CBaseController(px_session, name="i3c_controller")
        try:
            i3c_controller.attach_to_bus(i3c_bus)
        except PxAPIError:
            logger.warning("Multiple controller is not supported, using the existing controller")
            i3c_controller = i3c_bus.get_current_controller()

        logger.info(f"Added a I3C controller with name {i3c_controller.name}")

        # Create a Target Device
        bcr = 0x00  # Bus Characteristic Register
        dcr = 0x00  # Device Characteristic Register
        pid = ACUTE_DEFAULT_PID | (0x01 << 32) | 0 # Provisioned ID

        i3c_target = PxI3C32BitRegisterTarget(
            px_session,
            name="i3c_target",
            static_addr=0x12,
            sub_address_type=PxI3C32BitRegisterTarget.SubAddressType.NONE,
            bcr=bcr,
            dcr=dcr,
            pid=pid,
        )
        i3c_target.attach_to_bus(i3c_bus)
        logger.info(f"Added a I3C 32-bit register target with name {i3c_target.name}")

        # Initialize the I3C bus, which follows the procedure defined in the I3C specification
        i3c_controller.init_bus()
        logger.info("I3C bus initialized")

        # Fetch the target's state to get the target's assigned dynamic address
        assigned_address = i3c_target.assigned_address
        logger.info(f"Target assigned dynamic address: {assigned_address}")

        # Perform Write / Read data to the target device
        is_ack = i3c_controller.private_write(assigned_address, [0xDE, 0xAD, 0xBE, 0xEF])
        logger.info(f"I3C target" + (" acknowledged the write" if is_ack else " did not acknowledge the write"))

        data = i3c_controller.private_read(assigned_address, 4)
        logger.info(f"I3C target read data: {[f'{byte:02x}' for byte in data]}")

        # Detach the devices from the bus
        i3c_target.detach_from_bus()
        i3c_controller.detach_from_bus()
        logger.info(f"Detached the controller and target from the bus")


if __name__ == "__main__":
    logging.basicConfig(format='[%(levelname)s] %(message)s')
    logger.setLevel(logging.INFO)
    exec_i3c_example()

Run the code by executing the following command in the terminal:

$ python run_i3c_example.py

The output should look like the following:

[INFO] Created an Exerciser session
[INFO] Created I3C Bus (SCL: 0, SDA: 1)
[INFO] Added a I3C controller with name i3c_controller
[INFO] Added a I3C 32-bit register target with name i3c_target
[INFO] I3C bus initialized
[INFO] Target assigned dynamic address: 0x08
[INFO] I3C target acknowledged the write
[INFO] I3C target read data: ['0xde', '0xad', '0xbe', '0xef']
[INFO] Detached the controller and target from the bus

Now we will walk through the code step by step and explain each part.

Create a session

First, we need to create a session to connect to the Protocol Exerciser. We defined with statement to ensure the session is closed automatically when the block is exited.

with AqProtocolExerciser.connect(port=60600) as px_session:
    # session `px_session` is opened
    # do something here
    ...

# session `px_session` is closed

If you want to use AqProtocolExerciser outside of a with block, you will need to disconnect the connection manually.

Create a bus handle

Next, we need to configure a bus handle on the Protocol Exerciser. In this example, we will create an I3C bus. We can configure the hardware parameters of the bus. This must depend on the hardware configuration you are using. In this example, we set our I3C bus to 3.3V with an 1.5kΩ pull-up resistors.

# Configure an I3C Bus on SCL pin 0 and SDA pin 1
i3c_bus = PxI3CBus(px_session, scl=0, sda=1)

A typical bus handle shall include its channel configuration, which we specify by the scl and sda parameters for the I3C bus.

During the construction of the PxI3CBus object, it automatically sends a request to configure the bus on the Protocol Exerciser.

If successfully configured, the pin definitions on the Protocol Exerciser will be updated to the following:

PIN 0 PIN 1 PIN 2 PIN 3 PIN 4 PIN 5 PIN 6 PIN 7
I3C SCL I3C SDA (null) (null) (null) (null) (null) (null)

It is also possible to configure the bus with different pin definitions by passing the scl and sda parameters to the PxI3CBus constructor.

Bus Handle Pin Confliction

You are allowed to create multiple bus handles, but you must ensure that the pin definitions are not conflicting with each other. Otherwise, there will be some bus handles that are not created successfully.

Create devices

Next, we need to create devices and attach them to the bus. In this example, we will create a controller and a target device.

Here is the snippet of the code from the example above, which creates PxI3CBaseController and PxI3C32BitRegisterTarget devices:

# Create a Controller device
i3c_controller = PxI3CBaseController(px_session, name="i3c_controller")
try:
    i3c_controller.attach_to_bus(i3c_bus)
except PxAPIError as e:
    i3c_controller = i3c_bus.get_current_controller()

# Create a Target Device
bcr = 0x00  # Bus Characteristic Register
dcr = 0x00  # Device Characteristic Register
pid = ACUTE_DEFAULT_PID | (0x01 << 32) | 0 # Provisioned ID

i3c_target = PxI3C32BitRegisterTarget(
    px_session,
    name="i3c_target",
    static_addr=0x12,
    sub_address_type=PxI3C32BitRegisterTarget.SubAddressType.NONE,
    bcr=bcr,
    dcr=dcr,
    pid=pid,
)
i3c_target.attach_to_bus(i3c_bus)

Perform Operations

Now our devices are all ready to be used. We can perform operations on the target device by using the controller.

Specifically for I3C, we need to initialize the bus which handles address assignment procedure.

# Initialize the I3C bus, which follows the procedure defined in the I3C specification
i3c_controller.init_bus()
logger.info("I3C bus initialized")

Once this is done, all devices on I3C bus should be usable and are able to fetch their dynamic addresses if assigned by the controller.

# Fetch the target's assigned dynamic address
assigned_address = i3c_target.assigned_address
logger.info(f"Target assigned dynamic address: {assigned_address}")

Now we attempt to write data to the target device and read the data back.

# Perform Write / Read data to the target device
is_ack = i3c_controller.private_write(assigned_address, [0xDE, 0xAD, 0xBE, 0xEF])
logger.info(f"I3C target" + (" acknowledged the write" if is_ack else " did not acknowledge the write"))

data = i3c_controller.private_read(assigned_address, 4)
logger.info(f"I3C target read data: {[f'{byte:02x}' for byte in data]}")