About TLM
I should probably start by saying I didn't really get much out of the first things I read about TLM (might be either that I didn't really read it through, or that it is too abstract a concept to understand without seeing some examples of). TLM stands for Transaction Level Modelling, and Wikipedia defines it as "a high-level approach to modeling digital systems where details of communication among modules are separated from the details of the implementation of functional units or of the communication architecture". The definition is rather vague which is part of made me confused about this, but actually this loose definition is what makes it powerful and interesting.Let's take an example to get a more solid grip on it: main memory connected to a processor, no caches, no complicated memory hierarchies. A Von Neumann type processor has a rather simple expectation from the main memory it's connected to: it wants to be able to write to the memory at a given address, and similarly read from the memory at a given address. If you wanted to create a model for a memory that satisfies these requirements, you could do it in many ways on many levels. You could, for example, write a piece of C code that fills in/reads out elements of an array with read/write function calls, or a slightly more complicated version with some kind of wait() function to emulate the delays occurring while accessing the memory. Or you could write a VHDL/Verilog description at the RTL (register transfer level). Or go down a level to build this memory transistor by transistor. Alternatively, the processor and the memory may be communication through some kind of bus instead of having a direct connection, and this connection itself can be subject to different levels of modelling.
All of these levels of modelling have their advantages/disadvantages, serving some kind of specific purpose. There is, however, one thing that does not change: the interface to the memory is essentially the same in all these levels of modelling, whereas the gory (or not so gory) details of the actual implementation change according to model. This is essentially what TLM dictates: define the interface, what kind of data is passed back and forth. It's quite alike the object oriented programming concept of interfaces in this respect.
There's of course more than TLM than this, the OSCI (Open SystemC Initiative) has standardized the approach and offer a set of well-defined building blocks for constructing TLM interfaces. It may sound scary, but especially TLM 1.0 is quite simple to understand: it defines interfaces for either uni- or bi-directional communication for any given data type (via C++ templating), either blocking or unblocking.
If that muddled things down instead of clearing them up, the next bit should help more: a real TLM example, from ArchC!
The ArchC TLM port
ArchC normally offers a pretty complete package for playing around with processors, but there's a great deal of flexibility possible when you want to start customizing parts of that package, and this is where TLM comes into play. Starting from ArchC 2.0 (or something close to that :)) a TLM interface is offered for connecting "memory" to ArchC-generated cores. This is how:AC_ARCH(mips1)
{
ac_tlm_port DM:16M; // declare a TLM port that can address 16M
ac_regbank RB:32;
ac_reg npc;
ac_reg hi, lo;
ac_tlm_intr_port inta;
ac_wordsize 32;
ARCH_CTOR(mips1) {
ac_isa("mips1_isa.ac");
set_endian("big");
};
};
Easy! So once we have the TLM port, what do we do with it? What does the ArchC TLM interface look like? On the "internal" side (=the side which the processor core itself talks to) the functions inheritsed from ac_inout_if is used, just like regular memory. But it's the "external" side that concerns us: what does the TLM port do to "talk" to the outside world? It's derived from sc_port
typedef tlm_transport_if
Now that's a SystemC TLM interface: the template uses ac_tlm_req for sending out requests to the external world and ac_tlm_rsp for getting responses from the external world. Those types are:
/// ArchC TLM request packet.
struct ac_tlm_req {
ac_tlm_req_type type; // READ, WRITE, LOCK, UNLOCK...
int dev_id;
uint32_t addr;
uint32_t data;
};
/// ArchC TLM response packet.
struct ac_tlm_rsp {
ac_tlm_rsp_status status; // ERROR, SUCCESS
ac_tlm_req_type req_type; // same as in request
uint32_t data;
};
And that's it! That's what ArchC uses when it wants to do something with a piece of external memory. The TLM base interface itself (tlm_transport_if) needs a function called transport that carries the request and returns the response:
ac_tlm_rsp transport(const ac_tlm_req & req);
Having seen these, it shouldn't be difficult to model a simple memory that can be connected to the ArchC TLM port:
class ArchCTLMMemory : public ac_tlm_transport_if {
public:
ArchCTLMMemory(uint32_t size_bytes) {memory = new char[size_bytes];};
~ArchCTLMMemory() {delete memory;};
ac_tlm_rsp transport(const ac_tlm_req & req)
{
ac_tlm_rsp response;
response.status = SUCCESS;
response.req_type = req.type;
if(req.type == READ)
response.data = *((uint32_t *) memory[req.addr]);
else if(req.type == WRITE)
*((uint32_t *) memory[req.addr]) = req.data;
return response;
};
protected:
uint8_t *memory;
};
While it's by no means a complete example, it should be enough to illustrate the simplicity of making an ArchC-interfaceable memory element. And the real power is of course the TLM interface dictates nothing about the memory implementation - you're free to include delays, assertions, stat counters, or routing this memory request to some other component (which is the case for the tile-based system I'm building).