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.
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 name | Atom type | Description |
flags | transaction_flags_t | see description below |
data | uint8 * | see description below |
size | uint32 | transaction size |
initiator | conf_object_t * | initiator object |
owner | conf_object_t * | object passed to completion function |
completion | transaction_completion_t | completion function |
fill_value | uint8 | value for each byte in the transaction |
user_data | lang_void * | obsolete atom |
memop | generic_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:
Flag | Meaning |
Sim_Transaction_Fetch | instruction fetch |
Sim_Transaction_Write | write operation |
Sim_Transaction_Inquiry | inquiry operation |
Sim_Transaction_Control | control 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:
SIM_get_transaction_bytes
, SIM_get_transaction_bytes_offs
, SIM_get_transaction_value_be
, SIM_get_transaction_value_le
are available to get the data from a transaction (when servicing write transactions);
SIM_set_transaction_bytes
, SIM_set_transaction_bytes_offs
, SIM_set_transaction_value_be
, SIM_set_transaction_value_le
, SIM_set_transaction_bytes_constant
are available in order to write data to a transaction (when servicing read transactions).
Additional transaction flags may be defined in the future.
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.
Various API functions exist to retrieve information about a transaction:
API Function | Description |
SIM_transaction_is_read | returns true for loads |
SIM_transaction_is_write | returns true for stores |
SIM_transaction_is_fetch | returns true for instruction fetches |
SIM_transaction_is_inquiry | returns true for inquiry transactions |
SIM_transaction_flags | returns the value of the flags atom |
SIM_transaction_initiator | returns the transaction initiator |
API Function | Description |
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_type | register custom Python atom type |
API Function | Description |
SIM_set_transaction_bytes | set buffer contents |
SIM_set_transaction_bytes_offs | set some buffer bytes |
SIM_set_transaction_value_le | encode value using little endian byte order |
SIM_set_transaction_value_be | encode value using big endian byte order |
SIM_set_transaction_bytes_constant | set all transaction bytes to a given value |
SIM_get_transaction_bytes | retrieve buffer contents |
SIM_get_transaction_bytes_offs | retrieve some buffer bytes |
SIM_get_transaction_value_le | interpret buffer as a little endian encoded integer |
SIM_get_transaction_value_be | interpret buffer as a big endian encoded integer |
API function | Description |
SIM_defer_transaction | defer transaction for later completion |
SIM_defer_owned_transaction | defer transaction for later completion |
using a supplied transaction | |
SIM_complete_transaction | complete a deferred transaction |
SIM_monitor_transaction | monitor transaction for |
asynchronous completion | |
SIM_monitor_chained_transaction | monitor chained transaction |
for asynchronous completion | |
SIM_transaction_wait | wait for transaction completion |
API function | Description |
SIM_get_transaction_id | retrieve transaction ID for checkpointing |
SIM_reconnect_transaction | relink transaction at checkpoint restore |
API function | Description |
SIM_issue_transaction | issue transaction to map_target_t endpoint |
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 Code | Meaning |
Sim_PE_No_Exception | success |
Sim_PE_IO_Not_Taken | access where nothing is mapped |
Sim_PE_IO_Error | target abort, mostly applicable to PCI devices |
Sim_PE_Inquiry_Unhandled | inquiry access not supported |
Sim_PE_Stall_CPU | abort current instruction and reissue it |
Sim_PE_Deferred | transaction will be completed asynchronously |
Sim_PE_Async_Required | synchronous 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.
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.
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.
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)
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
}
}
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.
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:
Attribute | Description |
read | transaction is a read operation |
write | transaction is a write operation |
fetch | transaction is an instruction fetch |
inquiry | transaction is an inquiry operation |
size | transaction size |
flags | SIM_Transaction_xxx flags |
initiator | initiator object |
owner | object passed to completion function |
data | contents as a byte string |
fill_value | value for each byte in the transaction |
value_le | contents as a little endian integer |
value_be | contents as a big endian integer |
completion | completion function |
memop | legacy generic_transaction_t |
prev | parent 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.
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)
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.
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.
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
Since asynchronously issued transactions are not always completed immediately, they need to be checkpointable. Checkpointing is performed as follows:
SIM_get_transaction_id
function.At checkpoint restore, the following should be done:
SIM_reconnect_transaction
.SIM_defer_transaction
with a NULL
transaction argument. The recreated transaction together with the checkpointed id is passed to SIM_reconnect_transaction
.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.
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.
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:
generic_transaction_t
transaction has a transaction
field which points to the original transactiontransaction_t
transaction has a memop
atom with a pointer to the original transaction.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
.
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.