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_size | returns the transaction size |
SIM_transaction_initiator | returns the transaction initiator |
| API Function | Description |
|---|---|
ATOM_<type> | atom constructor |
ATOM_get_transaction_<type> | retrieves atom of type <type> (returns 0/NULL if the atom is absent) |
ATOM_transaction_<type> | retrieves pointer to atom of type <type> (returns NULL if the atom is absent) |
SIM_register_python_atom_type | registers 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 | primary function to issue transactions to map_target_t endpoint |
SIM_issue_read_transaction | C helper function to issue read transactions |
SIM_issue_write_transaction | C helper function to issue write transactions |
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_le(t, dev->reg_a);
else
dev->reg_a = SIM_get_transaction_value_le(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 38.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_le(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] = { 0 }; // 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(write=True, size=8, 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 uint8
issue_synchronous_1_byte_read(my_device_t *dev, uint64 addr)
{
// create a 1-byte read transaction
uint8 val = -1;
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) {
// USER-TODO: handle error condition
}
return val;
}
The following example issues a 4-byte read 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);
}
The transaction and atoms are allocated on the heap to ensure that the transaction remains valid until its completion. Additionally, to support asynchronous completion the transaction is required to have a completion atom. It holds a reference to a completion callback function. The completion callback is invoked when the transaction is completed. 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(write=True, size=4, value_le=0x12345678)
space.iface.transaction.issue(t1, 0x1000)
# Example 2: creating and issuing a synchronous 2-byte inquiry read
t2 = simics.transaction_t(read=True, size=2, 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(read=True, size=4, completion=completion)
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()
{
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. For CMake it is enough to list the header file in the SOURCES arguments when calling simics_add_module in the CMakeLists.txt file. One can also add the header to the PYWRAP argument for the same function.
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(write=True, size=8, device_address=0x7)
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. Transaction chaining also makes it possible to override atom values from a previous transaction in the list. Whether it is OK to override a particular atom depends on its type. For example, overriding of the flags atom is not allowed (even though Simics doesn't check this at the moment).
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.
To support asynchronous completion, all transactions chained together should have a non-NULL completion atom. See, for example, sample code from Transaction Chaining Example C section where a new transaction that is chained with the original one provides a completion atom.
The following sample code defines an appender class. The appender class defines the transaction_translator interface which is used to append the device_address atom to incoming transactions and forward 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 8-byte read transaction:
def completion(obj, t, ex):
print("Completion of original transaction")
return ex
t = simics.transaction_t(size=8, completion=completion)
# 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
The following sample code chains the incoming transaction prev and adds the device_address atom and forwards it to a map_target. If the transaction is deferred the transaction is copied to the heap. Function SIM_replace_transaction is then called to tell Simics to use the heap allocated transaction instead. When the transaction completes the complete function is called and frees the heap allocated transaction and its atoms.
#include <simics/device-api.h>
#include <simics/devs/translator.h>
#include <simics/model-iface/transaction.h>
#include <simics/arch/arm.h>
/* The sample_transaction_t structure is used to collocate transaction_t and
atoms that are appended to the original transaction. Having transaction_t
and atoms in the same structure allows to allocate a transaction and its
atoms by calling malloc only once. */
typedef struct {
transaction_t t; /* NB: 't' field should come first as the code below
frees the allocated sample_transaction_t by passing
the address of 't' field. */
atom_t atoms[4];
} sample_transaction_t;
typedef struct {
conf_object_t obj;
/* Target for translations */
conf_object_t *target;
/* Map target for translations */
map_target_t *map_target;
VECT(transaction_t *) deferred_transactions;
} sample_t;
static exception_type_t
empty_complete(conf_object_t *obj, transaction_t *t, exception_type_t ex)
{
/* There is nothing to do in this function. This empty function is needed
since transactions that support asynchronous completion are required
to have a non-NULL completion atom. */
return ex;
}
static exception_type_t
complete(conf_object_t *obj, transaction_t *t, exception_type_t ex)
{
sample_t *sample = (sample_t *)obj;
VREMOVE_FIRST_MATCH(sample->deferred_transactions, t);
MM_FREE(t);
return ex;
}
static exception_type_t
translate(
conf_object_t *obj,
uint64 addr,
access_t access,
transaction_t *prev,
exception_type_t (*callback)(
translation_t txl, transaction_t *t, cbdata_call_t cbdata),
cbdata_register_t cbdata)
{
sample_t *sample = (sample_t *)obj;
atom_t atoms[] = {
ATOM_arm_nsaid(0x8086),
ATOM_owner(obj),
ATOM_completion(empty_complete),
ATOM_LIST_END
};
transaction_t t = { .atoms = atoms, .prev = prev };
transaction_t *t_addr = &t;
translation_t txl = { .target = sample->map_target };
exception_type_t exc = callback(txl, t_addr, cbdata);
if (exc == Sim_PE_Deferred) {
/* 't' was deferred and will be completed later. We need to move it
from stack to dynamic memory. NB: we allocate 't' dynamically only
after it was deferred to avoid using dynamic memory - most of the
transactions are not deferred, and using dynamic memory is rather
slow. */
sample_transaction_t* st = MM_ZALLOC(1, sample_transaction_t);
st->atoms[0] = ATOM_arm_nsaid(0x8086);
st->atoms[1] = ATOM_owner(obj);
st->atoms[2] = ATOM_completion(complete);
st->atoms[3] = ATOM_LIST_END;
st->t.atoms = st->atoms;
st->t.prev = prev;
SIM_replace_transaction(t_addr, &st->t);
VADD(sample->deferred_transactions, &st->t);
t_addr = &st->t;
}
return SIM_monitor_chained_transaction(t_addr, exc);
}
Registration of the transaction_translator interface is done by the below code:
static const transaction_translator_interface_t t_txl_iface = {
.translate = translate
};
SIM_register_interface(
class, TRANSACTION_TRANSLATOR_INTERFACE, &t_txl_iface);
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:
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 an in-memory snapshots is restored, then all uncompleted transactions are first canceled 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 obtained 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] = { 0 }; // 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 transaction 't' is completed at this point.
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.