Verification projects that use both SystemVerilog and SystemC make use of TLM transactions to exchange data between the two realms. The communication between SV and SC requires a serialization operation on the source side and a deserialization on the destination side, the last being sensitive to coding errors (i.e. definition misalignment). Serialization is required since both sides use TLM Generic Payload (GP) as a common ground, which in turn uses a byte array to transport data.
One simple way to check if the definitions are aligned is by sending a transaction with a TLM read command, unpack it and send it back to SV and comparing with the original transaction:
Before jumping to an example, the following assumptions will be made:
- UVM-ML is used to connect SV to SC
- A blocking port is connected, SV being the initiator
Let’s consider a simple transaction with three fields:
class simple_transaction;
byte m_type;
int unsigned m_len; // number of data frames
int m_data[$];
endclass
First, one must create an “uvm_tlm_generic_payload” that holds one of these transactions. In order to serialize the transaction into TLM GP, one can use SystemVerilog’s streaming operators:
function uvm_tlm_generic_payload trans2gp(simple_transaction tr);
// Create a new GP object
uvm_tlm_generic_payload gp = new();
// Pack transaction
byte unsigned data[] = {>>{tr.m_type,tr.m_len,tr.m_data}};
// Put the serialized transaction in the GP
gp.set_data(data);
gp.set_data_length(data.size());
gp.set_command(UVM_TLM_READ_COMMAND);
[...]
return gp;
endfunction
Once the GP is available it can be sent to SystemC, where it will be unpacked into a similar class:
class simple_transaction {
public:
unsigned char m_type;
uint32_t m_len;
std::vector<int> m_data;
};
For packing/unpacking in C++, there are multiple options: string streams, overwriting the stream operators for the transaction or even “memcpy”. This won’t be shown here since it’s not relevant, but you can find the whole example on AMIQ’s GitHub
void b_transport(tlm_generic_payload& gp, sc_time& dt) {
[...]
// Unpack gp into transaction class
simple_transaction t = array2transaction(gp.get_data_ptr(), gp.get_data_length());
[...]
After extracting all the fields, the same transaction will be repacked as a GP (by doing the same operations in reverse order) and sent back to SV::
void b_transport(tlm_generic_payload& gp, sc_time& dt) {
[...]
// Overwrite previous data
unsigned char* data = gp.get_data_ptr();
unsigned int len = gp.get_data_len();
transaction2array(t, data, len);
}
Back in SV, the new GP containing the read command response (the same transaction packed from SC) is unpacked into a different transaction object and compared with what was initially sent:
{>>{recv.m_type, recv.m_len, recv.m_data}} = gp.m_data;
if (sent.compare(recv)) begin
`uvm_info("PASS", "Got same data", UVM_LOW);
end else begin
`uvm_error("FAIL", "Got different data on same transaction");
end
For the full example check out AMIQ’s GitHub.
The UVM do_pack / do_unpack functions can also be used to pack data in SV automatically.
If using UVM-ML with Cadence, there is also the “mltypemap” utility which can automatically generate corresponding object declarations along with packers and unpackers in both SC and SV.
References
How to Pack Data Using the SystemVerilog Streaming Operators (>>, <<)
How to Unpack Data Using the SystemVerilog Streaming Operators (>>, <<)