36 Extension Classes 38 Checkpoint Compatibility
Model Builder User's Guide  /  VI Simics API  / 

37 Transactions

Simics 6 introduces a new API for memory transactions, based on the transaction_t data type. The new transaction is more flexible and supports more features than the old generic_transaction_t, but both types of transactions can be used concurrently in a configuration to make it easier to migrate to the new transaction.

37.1 Transaction Atoms

A transaction is basically a list with properties, where each property is called an "atom". Below is a list with the most commonly used transaction atoms with a brief description. More information about different atoms is provided in subsequent sections.

Atom nameAtom typeDescription
flagstransaction_flags_tsee description below
datauint8 *see description below
sizeuint32transaction size
initiatorconf_object_t *initiator object
ownerconf_object_t *object passed to completion function
completiontransaction_completion_tcompletion function
fill_valueuint8value for each byte in the transaction
user_datalang_void *obsolete atom
memopgeneric_transaction_t *pointer to obsolete generic_transaction_t

The flags atom defines whether the transaction is a read, write or fetch and whether it is an inquiry transaction. It is a combination (bitmap) of the following flags:

FlagMeaning
Sim_Transaction_Fetchinstruction fetch
Sim_Transaction_Writewrite operation
Sim_Transaction_Inquiryinquiry operation
Sim_Transaction_Controlcontrol operation (e.g. cache line fetch)

When neither Sim_Transaction_Fetch nor Sim_Transaction_Write is set the transaction is a read transaction.

The data atom holds a pointer either to data that should be written (for write transactions) or to a location where data should be read to (for read transactions). Please note that endpoints servicing transactions should not use the data atom directly but instead use data access functions:

Additional transaction flags may be defined in the future.

37.2 Transaction Datatype

The transaction_t type itself is defined as follows:

    typedef struct transaction {
        atom_t *atoms;
        struct transaction *prev;

        ...internal fields...
    } transaction_t;

The only fields that may be used are the atoms field and the prev field. There are also some internal fields that must be initialized to zero, but they should never be referred to by name. The prev field is either NULL or points to a parent transaction. That is, transactions form a linked list, and this mechanism is utilized to append additional atoms to an existing transaction. This is discussed in more details in subsequent sections.

Transaction atoms should be accessed by using the available accessors and not by accessing the atoms pointer directly.

37.3 Transaction API Overview

Various API functions exist to retrieve information about a transaction:

API FunctionDescription
SIM_transaction_is_readreturns true for loads
SIM_transaction_is_writereturns true for stores
SIM_transaction_is_fetchreturns true for instruction fetches
SIM_transaction_is_inquiryreturns true for inquiry transactions
SIM_transaction_flagsreturns the value of the flags atom
SIM_transaction_initiatorreturns the transaction initiator
API FunctionDescription
ATOM_<type>atom constructor
ATOM_get_transaction_<type>retrieve atom of type <type>
ATOM_transaction_<type>retrieve pointer to atom of type <type>
SIM_register_python_atom_typeregister custom Python atom type
API FunctionDescription
SIM_set_transaction_bytesset buffer contents
SIM_set_transaction_bytes_offsset some buffer bytes
SIM_set_transaction_value_leencode value using little endian byte order
SIM_set_transaction_value_beencode value using big endian byte order
SIM_set_transaction_bytes_constantset all transaction bytes to a given value
SIM_get_transaction_bytesretrieve buffer contents
SIM_get_transaction_bytes_offsretrieve some buffer bytes
SIM_get_transaction_value_leinterpret buffer as a little endian encoded integer
SIM_get_transaction_value_beinterpret buffer as a big endian encoded integer
API functionDescription
SIM_defer_transactiondefer transaction for later completion
SIM_defer_owned_transactiondefer transaction for later completion
using a supplied transaction
SIM_complete_transactioncomplete a deferred transaction
SIM_monitor_transactionmonitor transaction for
asynchronous completion
SIM_monitor_chained_transactionmonitor chained transaction
for asynchronous completion
SIM_transaction_waitwait for transaction completion
API functionDescription
SIM_get_transaction_idretrieve transaction ID for checkpointing
SIM_reconnect_transactionrelink transaction at checkpoint restore
API functionDescription
SIM_issue_transactionissue transaction to map_target_t endpoint

37.4 Transaction Interface

Devices mapped into a memory space implement the transaction interface in order to receive transactions. The transaction interface looks as follows:

typedef struct transaction_interface {
    exception_type_t (*issue)(conf_object_t *NOTNULL obj,
                              transaction_t *NOTNULL t,
                              uint64 addr);
} transaction_interface_t;

The issue method is called when a transaction t is issued to the device. The addr parameter is an offset into the mapped device. If the transaction is handled successfully then Sim_PE_No_Exception should be returned. Below is a list with common return codes:

Return CodeMeaning
Sim_PE_No_Exceptionsuccess
Sim_PE_IO_Not_Takenaccess where nothing is mapped
Sim_PE_IO_Errortarget abort, mostly applicable to PCI devices
Sim_PE_Inquiry_Unhandledinquiry access not supported
Sim_PE_Stall_CPUabort current instruction and reissue it
Sim_PE_Deferredtransaction will be completed asynchronously
Sim_PE_Async_Requiredsynchronous operation is not supported

The following sections discuss how the interface is used for synchronous and asynchronous transactions.

When a device is accessed through a memory space, then addr is given by the expression (memory_space_addr - map.base) + map.start, where memory_space_addr is the address at which the memory space was accessed.

37.5 Synchronous Completion

Completing a transaction synchronously is simple. The issue method of the transaction interface just performs the requested operation and returns Sim_PE_No_Exception, or alternatively, returns some appropriate error code. A simple example in C is given below:

static exception_type_t
issue_method(conf_object_t *obj, transaction_t *t, uint64 addr)
{
    my_device_t *dev = (my_device_t *) obj;
    unsigned size = SIM_transaction_size(t);

    if (addr == REG_A_OFFSET && size == 4) {
        if (SIM_transaction_is_read(t))
            SIM_set_transaction_value_be(t, dev->reg_a);
        else
            dev->reg_a = SIM_get_transaction_value_be(t);
        return Sim_PE_No_Exception;
    } else {
        // One can handle more cases. We just return an exception.
        return Sim_PE_IO_Not_Taken;
    }
}

For synchronous operation, the transaction interface is quite similar to the old io_memory interface.

37.6 Asynchronous Completion

Transactions can be completed asynchronously, provided that the initiator supports it. The following example shows how this is done:

static exception_type_t
issue_method_that_defers_transaction(
        conf_object_t *obj, transaction_t *t, uint64 addr)
{
    my_device_t *dev = (my_device_t *) obj;

    transaction_t *t_def = SIM_defer_transaction(obj, t);
    if (!t_def)
        return Sim_PE_Async_Required;

    dev->t_def = t_def;
    return Sim_PE_Deferred;
}

The main points to note are that SIM_defer_transaction is used to obtain a new transaction pointer, t_def, which remains valid after the return from the issue function, and that the return value must be Sim_PE_Deferred to signify asynchronous completion. Calling SIM_defer_transaction also makes Simics aware of the uncompleted transaction. Uncompleted, deferred, transactions can be listed with the list-transactions command.

If the originator of the issued transaction does not support asynchronous completion (see 37.9), then SIM_defer_transaction will return NULL. In this case, the device should handle the transaction synchronously or return Sim_PE_Async_Required if this is not feasible.

The deferred transaction carries the same information as the original transaction. Once the device is ready with the requested operation, the deferred transaction is completed by calling SIM_complete_transaction. This is illustrated in the following example, which completes a deferred read transaction.

 // first we write the data to the transaction
    SIM_set_transaction_value_be(dev->t_def, reg_value);

    // then report that the transaction was completed
    SIM_complete_transaction(dev->t_def, Sim_PE_No_Exception);

    dev->t_def = NULL;  // nullify t_def to avoid a dangling pointer

The call to SIM_complete_transaction releases the deferred transaction, and it must not be accessed after this call.

As a special case, completing a deferred transaction from within the issue method itself is allowed. In this case, the return value from issue should still be Sim_PE_Deferred.

The transaction pointer passed as an argument to issue must never be kept around after the interface method has returned. Instead, SIM_defer_transaction should be used to obtain a pointer which remains valid until the transaction has been completed.

37.7 Creating Transactions

Below is an example how a 8-byte write transaction can be constructed in C:

uint8 buf[8];  // USER-TODO: fill buf with the actual data to write
atom_t atoms[] = {
        ATOM_flags(Sim_Transaction_Write),
        ATOM_data(buf),
        ATOM_size(sizeof buf),
        ATOM_LIST_END
};
transaction_t t = { atoms };

The atom list must always be terminated by the ATOM_LIST_END marker.

The same example in Python is even simpler:

from simics import transaction_t

t = transaction_t(size = 8, write = True, value_le = 0x11223344)

37.8 Issuing a Synchronous Transaction

Issuing a transaction synchronously is done by just calling the issue method of the transaction interface or using SIM_issue_transaction with a map_tgt handle representing the destination.

static void
issue_synchronous_1_byte_read(my_device_t *dev, uint64 addr)
{
    // create a 1-byte read transaction
    uint8 val;
    atom_t atoms[] = {
        ATOM_flags(0),  // zero flags value denotes a read transaction
        ATOM_data(&val),
        ATOM_size(sizeof val),
        ATOM_initiator(&dev->obj),
        ATOM_LIST_END
    };
    transaction_t t = { atoms };

    // issue the transaction @ addr
    exception_type_t ex = trans_iface->issue(dst_obj, &t, addr);
    if (ex != Sim_PE_No_Exception) {
        // handle error condition
    }
}

37.9 Issuing an Asynchronous Transaction

The following example issues a 4-byte read asynchronously. The transaction and atoms are allocated on the heap to ensure that the transaction remains valid until completion. The presence of the completion atom with a non-NULL value signifies that the transaction can be completed asynchronously.

typedef struct {
    transaction_t t;
    atom_t atoms[6];
    uint8 buf[4];
} my_trans_t;

static exception_type_t
completion(conf_object_t *obj, transaction_t *t, exception_type_t ex)
{
    my_device_t *dev = (my_device_t *) obj;

    // read out the read result
    uint32 value = SIM_get_transaction_value_le(t);

    // "process" the value here
    dev->reg_a = value;

    // free transaction
    my_trans_t *my_t = (my_trans_t *) t;
    MM_FREE(my_t);
    return ex;
}

static void
issue_asynchronous_read(my_device_t *dev, uint64 addr)
{
    my_trans_t *m = MM_MALLOC(1, my_trans_t);
    *m = (my_trans_t){
        .t = { m->atoms },
        .atoms = {
            ATOM_flags(0),  // zero flags value denotes a read transaction
            ATOM_size(sizeof m->buf),
            ATOM_data(m->buf),
            ATOM_initiator(&dev->obj),
            ATOM_completion(completion),
            ATOM_LIST_END,
        },
    };
    exception_type_t ex = trans_iface->issue(dst_obj, &m->t, addr);
    SIM_monitor_transaction(&m->t, ex);
}

When the transaction is completed, then the completion callback is invoked. The return value from the completion function should normally be the exception code received as an argument.

The completion callback will never be invoked before the call to SIM_monitor_transaction is done. If the transaction has been completed synchronously, then the return value from issue is a code other than Sim_PE_Deferred, and then SIM_monitor_transaction invokes the callback. If the transaction is deferred, then SIM_monitor_transaction marks it as being monitored for completion and returns immediately.

Omitting the call to SIM_monitor_transaction results in the transaction never being completed.

The object argument to the completion function is obtained from either an owner atom or from an initiator atom. The former takes precedence if both are present. The difference between owner and initiator is primarily that the later defines the initiator of the request, and this object is used for instance when handling direct memory permissions. The owner object is only used as an argument to the completion callback.

37.10 Python Support

37.10.1 Accessing Transaction Atoms

The transaction_t type is available in Python and has attributes that in most cases make it unnecessary to use accessors like SIM_transaction_is_write. The following attributes are available:

AttributeDescription
readtransaction is a read operation
writetransaction is a write operation
fetchtransaction is an instruction fetch
inquirytransaction is an inquiry operation
sizetransaction size
flagsSIM_Transaction_xxx flags
initiatorinitiator object
ownerobject passed to completion function
datacontents as a byte string
fill_valuevalue for each byte in the transaction
value_lecontents as a little endian integer
value_becontents as a big endian integer
completioncompletion function
memoplegacy generic_transaction_t
prevparent transaction
<atom-type>atom of type <atom-type>

The attributes above can be used both as arguments to the constructor and as attributes of the transaction_t object.

37.10.2 Sample Code to Create and Issue a Transaction

Below are some simple examples how transactions can be created and issued from Python:

import simics

def create_config():
    '''Creates a memory-space with a single ram object'''
    space = simics.pre_conf_object('space', 'memory-space')
    space.ram = simics.pre_conf_object('ram')
    space.ram.image = simics.pre_conf_object('image', size = 0x10000)
    space.ram(image = space.ram.image)
    space(map = [[0, space.ram, 0, 0, 0x10000]])
    simics.SIM_add_configuration([space], None)
    return simics.SIM_get_object(space.name)

space = create_config()

# Example 1: creating and issuing a synchronous 4-byte write
t1 = simics.transaction_t(size = 4, write = True, value_le = 0x12345678)
space.iface.transaction.issue(t1, 0x1000)

# Example 2: creating and issuing a synchronous 2-byte inquiry read
t2 = simics.transaction_t(size = 2, read = True, inquiry = True)
space.iface.transaction.issue(t2, 0x1000)
print("Synchronous read: %x" % t2.value_le)

# Example 3: creating and issuing an asynchronous 4-byte read
def completion(obj, t, ex):
    print("Asynchronous read: %x" % t.value_le)
    return ex

t3 = simics.transaction_t(size = 4, completion = completion, read = True)
ex = space.iface.transaction.issue(t3, 0x1000)
print("Monitoring for completion...")
simics.SIM_monitor_transaction(t3, ex)

37.11 Custom Atom Types

It is possible to define custom atoms. The following example (complete source code is distributed in the sample-transaction-atoms module) defines two atom types - device_address and complex_atom_t:

#ifndef SAMPLE_TRANSACTION_ATOMS_H
#define SAMPLE_TRANSACTION_ATOMS_H
#include <simics/device-api.h>

#if defined(__cplusplus)
extern "C" {
#endif

// Define the 'device_address' atom type
#define ATOM_TYPE_device_address uint64
SIM_CUSTOM_ATOM(device_address);

// Define the 'complex' atom type
typedef struct {
        uint64 address;
        uint32 attributes;
} complex_atom_t;

// Allow creation from Python, if required
SIM_PY_ALLOCATABLE(complex_atom_t);
#define ATOM_TYPE_complex complex_atom_t *
SIM_CUSTOM_ATOM(complex);

#if defined(__cplusplus)
}
#endif

#endif /* SAMPLE_TRANSACTION_ATOMS_H */

The types should also be registered from the module's init_local function:

#include "sample-transaction-atoms.h"

void
init_local(void)
{
        ATOM_register_device_address();
        ATOM_register_complex();

        // function_with_sample_code contains sample code showing how
        // to create transactions and access the new atoms we just defined.
        function_with_sample_code();
}

To get Python support for the new atom type, the header needs to be listed in the IFACE_FILES module's makefile variable.

Custom atom types can be used just like the pre-defined ones. Below is an example how the example atoms above can be used from Python:

from simics import (
    SIM_load_module,
    transaction_t,
)

# Load the module defining custom transaction atoms:
SIM_load_module('sample-transaction-atoms')
# Import the complex_atom_t type from the custom_transaction_atoms module:
from simmod.sample_transaction_atoms.sample_transaction_atoms import (
    complex_atom_t,
)


# Transaction with the device_address atom
t1 = transaction_t(device_address = 0x7, write = True, size = 8)
print(f"Device address: {t1.device_address:#x}")

# Transaction with the complex atom
t2 = transaction_t(
    complex = complex_atom_t(address = 0x10, attributes = 0x5))
print(f"complex.address: {t2.complex.address:#x}")
print(f"complex.attributes: {t2.complex.attributes:#x}")

From C, custom atoms are retrieved using type-safe accessors, e.g.

uint64 dev_address = ATOM_get_transaction_device_address(t);
complex_atom_t *comp = ATOM_get_transaction_complex(t);

If the atom does not exist, then 0 or NULL will be returned, depending on the defined type. If it is important to handle specially the case when an atom is not present at all, one can use the ATOM_transaction_<type> accessor function instead:

        const uint64 *dev_address = ATOM_transaction_device_address(&t);
        if (dev_address != NULL) {
                // atom is present, pointer is valid
                SIM_printf("Device address: %#llx\n", *dev_address);
        } else {
                // atom is not present
                SIM_printf("Device address atom is not present\n");
        }

ATOM_transaction_<type> accessor functions do not transfer data ownership: the pointer returned by the function may not be valid outside of the call chain.

37.12 Transaction Chaining

Two or more transactions can be chained together into a linked list with the help of the prev field in the transaction_t type. This is useful primarily to append atoms to an existing transaction. API functions that look for a specific atom examine the atom list of the last transaction first, then the atom list of its parent and so on until an atom of the correct kind has been found.

Simics does not consult the parent of a transaction when looking for a completion or owner atom. These atoms are always associated with a specific transaction.

37.12.1 Transaction Chaining Example

The following sample code defines an appender class that appends the device_address atom to incoming transactions and forwards them to another device:

import conf
import pyobj
import simics

# Load the module that defines the device_address atom.
# See section about custom atom types for more information.
simics.SIM_load_module('sample-transaction-atoms')

# Translator that appends the device_address atoms to transactions
class appender(pyobj.ConfObject):
    class transaction_translator(pyobj.Interface):
        def translate(self, addr, access, t, clbk, data):

            def completion(obj, t, ex):
                print("Completion of chained transaction")
                return ex

            self.t = simics.transaction_t(
                prev = t,
                device_address = 0x20,
                completion = completion)

            translation = simics.translation_t(
                target = self._up.target.map_target)

            ex = clbk(translation, self.t, data)
            return simics.SIM_monitor_chained_transaction(self.t, ex)

    class target(pyobj.Attribute):
        '''Target for accesses. It can be NIL. In that case accesses
        are terminated with the Sim_PE_IO_Not_Taken exception.'''
        attrattr = simics.Sim_Attr_Optional
        attrtype = "o|n"
        def _initialize(self):
            self.val = None
            self.map_target = None
        def getter(self):
            return self.val
        def setter(self, val):
            if self.map_target:
                simics.SIM_free_map_target(self.map_target)
            self.val = val
            self.map_target = (simics.SIM_new_map_target(val, None, None)
                               if val else None)

The appender class above supports asynchronous transactions, as indicated by the presence of the completion atom. If the completion atom is omitted, then the call to SIM_monitor_chained_transaction should be removed and the exception code returned directly.

The SIM_monitor_chained_transaction functions like SIM_monitor_transaction except that when the chained transaction is completed, its parent will also be completed using the exception code returned by the chained completion function.

Below is a sample code that creates a test configuration with an object of the appender class and issues a transaction:

# Endpoint device class
class mydev(pyobj.ConfObject):
    class transaction(pyobj.Interface):
        def issue(self, t, addr):
            print("address: %x, size: %x, device-address: %x" % (
                addr, t.size, t.device_address))
            return simics.Sim_PE_No_Exception

def create_test_configuration():
    mydev = simics.pre_conf_object('mydev', 'mydev')
    appender = simics.pre_conf_object('appender', 'appender', target = mydev)
    simics.SIM_add_configuration([mydev, appender], None)

def issue_transaction(destination, addr):
    # Create an asynchronous transaction:
    def completion(obj, t, ex):
        print("Completion of original transaction")
        return ex
    t = simics.transaction_t(completion = completion, size = 8)

    # Issue transaction:
    mt = simics.SIM_new_map_target(destination, None, None)
    ex = simics.SIM_issue_transaction(mt, t, addr)
    simics.SIM_monitor_transaction(t, ex)

    # In this simple example we just free 'mt'. In the real device model it is
    # beneficial to store it and use whenever transactions are to be issued:
    simics.SIM_free_map_target(mt)

create_test_configuration()
issue_transaction(conf.appender, 0x1000)

The following output is generated when the issue_transaction function is executed:

simics> @issue_transaction(conf.appender, 0x1000)
address: 1000, size: 8, device-address: 20
Completion of chained transaction
Completion of original transaction

37.13 Transaction Checkpointing

Since asynchronously issued transactions are not always completed immediately, they need to be checkpointable. Checkpointing is performed as follows:

At checkpoint restore, the following should be done:

The value returned by SIM_get_transaction_id should not be cached since it is not necessarily stable during execution. Moreover, checkpointing will fail with an error if the function is not called for each uncompleted transaction.

A device appending a chained transaction should follow the same checkpoint flow as a regular initiator. Only appended atoms should be checkpointed and restored. The prev pointer is restored automatically by the SIM_reconnect_transaction call.

37.14 Reverse Execution

When reverse execution restores an in-memory checkpoint, then all uncompleted transactions are first canceled with with the completion code Sim_PE_Cancelled. This means that all deferred transactions have been released when the attribute setters subsequently are called.

37.15 Legacy Support

Simics Core has a conversion layer that automatically converts generic_transaction_t transactions to transactions_t transactions, and vice versa. For instance, a memory operation issued to a memory space using an old interface will be converted to a transaction_t before it is issued to a device implementing the transaction interface. Whenever conversion occurs, the original transaction can be obtain as follows:

37.16 Transaction Wait

The API function SIM_transaction_wait can be used together with a NULL completion atom to issue a transaction which can be completed asynchronously, but is handled as a synchronous transaction by the initiator. An example in C is given below:

    uint8 buf[8];  // USER-TODO: fill buf with the actual data to write
    atom_t atoms[] = {
            ATOM_flags(Sim_Transaction_Write),
            ATOM_data(buf),
            ATOM_size(sizeof buf),
            ATOM_completion(NULL),
            ATOM_LIST_END
    };
    transaction_t t = { atoms };
    exception_type_t ex = trans_iface->issue(dst_obj, &t, addr);
    ex = SIM_transaction_wait(&t, ex);

The SIM_transaction_wait function blocks until the transaction has completed. What happens is that Simics switches to a different user-level thread which continues the execution, typically by advancing time without dispatching instructions.

If the context from which issue function is called does not support user-level thread switching, then the transaction will not support asynchronous completion. In other words, SIM_defer_transaction will return NULL in that case.

SIM_transaction_wait can cause issues for devices further up in the call stack since such devices might see additional accesses before blocking call returns, and such accesses might be unexpected. It is recommended that SIM_transaction_wait is used only in situations where it is known that this is not a problem. Native Simics 6 CPUs should typically support SIM_transaction_wait without issues.

Checkpointing is not supported while a transaction is being waited upon with SIM_transaction_wait.

37.17 CLI support for transactions

Simics provides wait-for-read, wait-for-write, wait-for-get, wait-for-set, <transaction>.wait-for-read, <transaction>.wait-for-write, <transaction>.wait-for-get, and <transaction>.wait-for-set commands which allow to issue transactions from a command line. The commands are available from script branches. Here is an example of a script branch which issues a read transaction and prints a returned value once the transaction is completed:

simics> script-branch "read transaction" {
    $val = (wait-for-read address = 0x1000 size = 4 -l)
    echo "Read value: %#x" % $val
}

If the transaction in the example above completes synchronously then the script branch doesn't wait and completes immediately.

The list-transactions command allows to see the list of the transactions which have not completed yet.

36 Extension Classes 38 Checkpoint Compatibility