With the io_memory interface now considered legacy in Simics 7,
all new modelling should implement the transaction interface.
Migrating existing code to transaction interface from io_memory
is also strongly recommended. With this in mind, the challenge to port existing platforms to
entirely use transactions can be significant. Therefore Simics
supports automatic internal conversion from io_memory to transaction
and vice versa to allow incremental migration from io_memory to transaction.
To make this work, memory accesses shall not use direct interface calls for memory accesses but shall instead
use function SIM_issue_transaction.
If your model has to forward a generic_transaction_t and cannot construct the transaction_t itself,
then function VT_map_target_access can be used but is considered an exception and the modeller
shall construct a transaction_t whenever possible.
An important concept with the new transaction framework is the type map_target_t.
Simics objects, i.e. conf_object_t, that implement any of the memory/IO related interfaces can be
converted to a map_target_t through function SIM_new_map_target. A map_target_t must
be freed at end of use by calling function SIM_free_map_target.
See API Reference Manual Chapter 3.2 Device API Data Types for detailed information about
map_target_t.
The device example_dev contains two connects: memory and target_dev, implementing interfaces
memory_space and io_memory. It contains two methods write_to_dev32 and
read_memory32. These methods create a generic_transaction_t to do a 32-bit io_memory
write and read. The question now is how to port device example_dev
to the new transaction framework.
dml 1.4;
device example_dev;
import "utility.dml";
connect memory {
interface memory_space;
}
connect target_dev {
interface io_memory;
}
method write_to_dev32(uint64 addr, uint32 value) {
local bytes_t buf;
buf.data = cast(&value, uint8*);
buf.len = sizeof(value);
local generic_transaction_t mop = SIM_make_mem_op_write(addr,
buf,
false,
dev.obj);
local map_info_t map_info;
dev.target_dev.io_memory.operation(&mop, map_info);
}
method read_memory32(uint64 addr) -> (uint32) {
local uint32 value;
local buffer_t buf;
buf.data = cast(&value, uint8*);
buf.len = sizeof(value);
local generic_transaction_t mop = SIM_make_mem_op_read(addr,
buf,
false,
dev.obj);
dev.memory.memory_space.access(&mop);
return value;
}
The code below ports the device to use transactions and map targets.
The model is now ported to the new transaction framework and is at the same time
Memory/IO interface agnostic. Making the model possible to be used in legacy
platforms where the connected target_dev implements
io_memory and in new platforms the connected target_dev can be using transactions.
The target_dev object can still use io_memory but can now be ported to transactions at a later point
without breaking its dependency to device example_dev.
This way, incremental migration of a platform away from
io_memory to transactions is possible.
dml 1.4;
device example_dev;
import "utility.dml";
method validate_map_target(conf_object_t *obj) -> (bool) {
local map_target_t *tmp = SIM_new_map_target(obj, NULL, NULL);
if (!tmp) {
local exception_type_t _exc = SIM_clear_exception();
SIM_attribute_error(SIM_last_error());
return false;
}
SIM_free_map_target(tmp);
return true;
}
connect memory {
session map_target_t *map_target;
method validate(conf_object_t *obj) -> (bool) {
return validate_map_target(obj);
}
method set(conf_object_t *obj) {
SIM_free_map_target(this.map_target);
default(obj);
this.map_target = obj ? SIM_new_map_target(obj, NULL, NULL) : NULL;
}
}
connect target_dev {
session map_target_t *map_target;
method validate(conf_object_t *obj) -> (bool) {
return validate_map_target(obj);
}
method set(conf_object_t *obj) {
SIM_free_map_target(this.map_target);
default(obj);
this.map_target = obj ? SIM_new_map_target(obj, NULL, NULL) : NULL;
}
}
method write_to_dev32(uint64 addr, uint32 value) throws {
if (!target_dev.map_target)
throw;
local uint8 buf[4];
local atom_t atoms[5] = {
ATOM_data(buf),
ATOM_size(4),
ATOM_flags(Sim_Transaction_Write),
ATOM_initiator(dev.obj),
ATOM_list_end(0)
};
local transaction_t t;
t.atoms = atoms;
SIM_set_transaction_value_le(&t, value);
if (SIM_issue_transaction(target_dev.map_target, &t, addr) != Sim_PE_No_Exception)
throw;
}
method read_memory32(uint64 addr) -> (uint32) throws {
if (!memory.map_target)
throw;
local uint8 val[4];
local atom_t atoms[4] = {
ATOM_data(val),
ATOM_size(4),
ATOM_initiator(dev.obj),
ATOM_list_end(0)
};
local transaction_t t;
t.atoms = atoms;
if (SIM_issue_transaction(memory.map_target, &t, addr) != Sim_PE_No_Exception)
throw;
return SIM_get_transaction_value_le(&t);
}
In DML there is a template map_target that can be applied to any connect that
implements any of the Memory/IO interfaces defined in the bullet list above.
The code can be rewritten very compactly because of the helper functions
provided by the map_target template.
See Device Modeling Language 1.4 Reference Manual chapter Standard Templates
for a detailed description of the map_target template.
dml 1.4;
device example_dev;
import "utility.dml";
connect memory is map_target;
connect target_dev is map_target;
method write_to_dev32(uint64 addr, uint32 value) throws {
target_dev.write(addr, 4, value);
}
method read_memory32(uint64 addr) -> (uint32) throws {
return memory.read(addr, 4);
}
We have now covered how to remove dependency on external devices implementing
io_memory interfaces by interacting with them through a map_target.
Another scenario is when the device itself implements an io_memory type
of interface and you cannot port that until the devices communicating with it
transitions to map_target usage. In cases where the device forwards the memop
to another device a partial migration is still possible.
dml 1.4;
device example_dev2;
import "utility.dml";
connect target_dev1 {
interface io_memory;
}
connect target_dev2 {
interface io_memory;
}
port demux is signal_port;
implement io_memory {
method operation(generic_transaction_t *mem_op,
map_info_t map_info) -> (exception_type_t) {
if (demux.signal.high)
return target_dev1.io_memory.operation(mem_op, map_info);
else
return target_dev2.io_memory.operation(mem_op, map_info);
}
}
Doing a partial migration of this code where we keep our own implementation of
io_memory but remove the dependency of target_dev1 and target_dev2
to implement io_memory would require us to either convert generic_transaction_t
to a transaction_t and issue a SIM_issue_transaction or
to use the convenient help function VT_map_target_access which allows the user
to use a generic_transaction_t together with a map_target_t.
In the code below we utlize the VT_map_target_access function since it is
a lot more convenient in this scenario.
dml 1.4;
device example_dev2;
import "utility.dml";
connect target_dev1 is map_target;
connect target_dev2 is map_target;
port demux is signal_port;
implement io_memory {
method operation(generic_transaction_t *mem_op,
map_info_t map_info) -> (exception_type_t) {
SIM_set_mem_op_physical_address(mem_op,
SIM_get_mem_op_physical_address(mem_op) - map_info.base + map_info.start);
if (demux.signal.high)
return VT_map_target_access(target_dev1.map_target, mem_op);
else
return VT_map_target_access(target_dev2.map_target, mem_op);
}
}
When a full migration can be done, the code should be rewritten as:
dml 1.4;
device example_dev2;
import "utility.dml";
connect target_dev1 is map_target;
connect target_dev2 is map_target;
port demux is signal_port {
implement signal {
method signal_raise() {
default();
SIM_translation_changed(dev.obj);
}
method signal_lower() {
default();
SIM_translation_changed(dev.obj);
}
}
}
implement translator {
method translate(uint64 addr, access_t access,
const map_target_t *default_target) -> (translation_t)
local translation_t t;
if (demux.signal.high)
t.target = target_dev1.map_target;
else
t.target = target_dev2.map_target;
return t;
}
With generic_transaction_t custom meta data could be
added to IO Memory transactions through user_data.
The example device below showcases setting user_data
to a generic_transaction_t through function
SIM_set_mem_op_user_data.
dml 1.4;
device example_user_data_set;
typedef struct {
int id;
} example_message_t;
connect endpoint {
interface io_memory;
}
bank regs {
register msg_id size 4 @ 0x0 "Message id";
register msg_send size 8 @ 0x8 "Send message" {
is write;
method write(uint64 value) {
if (endpoint.io_memory.val) {
local bytes_t buf;
buf.data = cast(&value, uint8*);
buf.len = sizeof(value);
local generic_transaction_t mop = SIM_make_mem_op_write(0,
buf,
false,
dev.obj);
local example_message_t msg = { .id = msg_id.val, ... };
SIM_set_mem_op_user_data(&mop, &msg);
local map_info_t map_info;
endpoint.io_memory.operation(&mop, map_info);
}
}
}
}
The example device below showcases retrieving the meta data
from a generic_transaction_t through function
SIM_get_mem_op_user_data.
dml 1.4;
device example_user_data_read;
typedef struct {
int id;
} example_message_t;
implement io_memory {
method operation(generic_transaction_t *mem_op,
map_info_t map_info) -> (exception_type_t) {
local example_message_t* msg = SIM_get_mem_op_user_data(mem_op);
if (msg != NULL)
log info, 1: "Received message #%d", msg->id;
return Sim_PE_No_Exception;
}
}
With generic_transaction_t a void pointer is passed between the
initiator and the target making the data type unsafe. With transactions meta data can
be shared through atoms which are type safe.
The target shall declare its own custom atoms
for the meta data. A declaration consists of a type and
methods to create, retrieve and set atom values. The target can check if a specific atom
is part of the transaction making the meta data exchange type safe.
Custom atoms are defined in a Simics Interface Module similar to custom interfaces.
The below file example-atom.h declares an atom with the same signature
as in the previous example.
#ifndef MESSAGE_ATOM_H
#define MESSAGE_ATOM_H
#include <simics/device-api.h>
#if defined(__cplusplus)
extern "C" {
#endif
typedef struct {
int id;
} example_message_t;
// Allow creation from Python, if required
SIM_PY_ALLOCATABLE(example_message_t);
#define ATOM_TYPE_example_message example_message_t *
SIM_CUSTOM_ATOM(example_message);
#if defined(__cplusplus)
}
#endif
#endif /* MESSAGE_ATOM_H */
File example-atom.c creates an interface module which
at runtime registers the example_message atom for runtime usage.
#include "example-atom.h"
void
init_local(void)
{
ATOM_register_example_message();
}
File example-atom.dml adds DML wrappers
for the example_message atom data type and its accessors.
Devices written in DML shall import this file.
dml 1.4;
header %{
#include "example-atom.h"
%}
extern typedef struct {
int id;
} example_message_t;
// Create atom
extern atom_t ATOM_example_message(const example_message_t *msg);
// Get atom
extern const example_message_t* ATOM_get_transaction_example_message(const transaction_t *t);
The code below is class example_user_data_set ported to instead
use an atom for sending the message.
dml 1.4;
device example_user_data_set;
import "utility.dml";
import "example-atom.dml";
connect endpoint is map_target;
bank regs {
register msg_id size 4 @ 0x0 "Message id";
register msg_send size 8 @ 0x8 "Send message" {
is write;
method write(uint64 value) {
local buffer_t buf;
buf.data = cast(&value, uint8*);
buf.len = sizeof(value);
local example_message_t msg = { .id = msg_id.val, ... };
local atom_t atoms[6] = {
ATOM_initiator(dev.obj),
ATOM_data(buf.data),
ATOM_size(buf.len),
ATOM_flags(Sim_Transaction_Write),
ATOM_example_message(&msg),
ATOM_list_end(0)
};
local transaction_t t;
t.atoms = atoms;
endpoint.issue(&t, 0);
}
}
}
The code below is class example_user_data_read ported to instead
use an atom for the message.
dml 1.4;
device example_user_data_read;
import "example-atom.dml";
implement transaction {
method issue(transaction_t *t, uint64 addr) -> (exception_type_t) {
local example_message_t* msg = ATOM_get_transaction_example_message(t);
if (msg != NULL)
log info, 1: "Received message #%d", msg->id;
return Sim_PE_No_Exception;
}
}
For interfaces that used the generic_transaction_t structure
a memory space would split transactions into individual transactions according
to the align-size setting of the memory space map entry.
For transaction_t based interfaces this is no longer the case.
The memory space will still apply byte swapping according to the byte swap and
align-size settings, but it will no longer split the transactions.
There are some rare cases where a device might rely on the fact that transactions arrive only in chunks that are equal to or smaller than the align-size setting of its mapping. In such a case, the splitting needs to be handled by the device code or a transaction splitter needs to be inserted between the memory space and the device expecting the split transactions.
Device code handling of splitting is case specific and depends on the language
used. In DML or C++, one can address oversized transactions in the function
transaction_access of a bank, while in C one might do that
directly in the implementation of the issue function of the
transaction interface.
The easiest and a language agnostic way is inserting a transaction splitter.
An example of such a splitter is the class transaction_splitter
shipped with Simics. The source code can be found in
src/devices/transaction-splitter. Assume that originally you had a
case like shown below in Python.
# - mem refers to a memory space object # - dev refers to an io-memory based device with a bank called "regs" # map regs at offset 0x30, with length 0x100 and an align-size of 2 mem.map.append([0x30, dev.bank.regs, 0, 0, 0x100, None, 0, 2])
If that device was migrated from IO Memory to transaction but you still want to ensure that all transactions carry at most 2 bytes, you could insert the transaction splitter as shown below.
# - mem refers to a memory space object
# - dev refers to an io-memory based device with a bank called "regs"
# create a splitter, point it to the actual bank and configure it to split
# into two bytes
splitter = SIM_create_object('transaction_splitter',
'splitter',
target = dev.bank.regs,
split_size = 2)
# only change in the mapping is that it now points to the splitter object
mem.map.append([0x30, splitter, 0, 0, 0x100, None, 0, 2])