Getting Started with TLM-2.0 (Tutorial 1 - Sockets, Generic Payload, Blocking Transport)

今日からはGetting Started with TLM-2.0を見ていく。

www.doulos.com

長いので重要なところだけ訳していく。

Modeling Concepts

TLMはプロセス間通信にフォーカスしている。
OSCI TLM-2.0では特に、オンチップのメモリマップドバスのモデリングにフォーカスしている。
ここでは、読者がSystemCのモジュール、ポート、プロセス、チャネル、インタフェース、イベントに詳しいことを想定する。
TLM-2.0では異なるモジュールのプロセス間通信をするために、portとexport経由でのインタフェースメソッドコールを使う。

Initiators, Targets, and Sockets

イニシエータとは、新しいトランザクションを開始するモジュールのことである(要するにmaster)。
ターゲットとはほかのモジュールによって開始したトランザクションに応答するモジュールのことである(要するにslave)。
トランザクションとはデータ構造であり(C++オブジェクト)、イニシエータ-ターゲット間を関数呼び出しによって渡される。
モジュールはイニシエータであると同時にターゲットにもなることができ、それはアービタールーター、バスなどのモデリングにおいて典型的である。

イニシエータとターゲット間でトランザクションを通すために、TLM-2.0ではソケットを使う。
イニシエータはイニシエータソケットを通じてトランザクションを送り、ターゲットはターゲットソケットを通じてトランザクションを受け取る。
トランザクションを変更せずに転送するモジュールはインターコネクトと呼ばれる。
インターコネクトはイニシエータソケットとターゲットソケットの両方を持つ。

SystemCのコードを見ていく。tlm.hをインクルードする必要がある。このケースではさらにtlm_utilsから2つのソケットに関するヘッダファイルをインクルードしている。

#define SC_INCLUDE_DYNAMIC_PROCESSES

#include "systemc"
using namespace sc_core;
using namespace sc_dt;
using namespace std;

#include "tlm.h"
#include "tlm_utils/simple_initiator_socket.h"
#include "tlm_utils/simple_target_socket.h"

OSCIシミュレータを使う場合、一部のTLM-2.0機能を使う(特にシンプルソケット)際に SC_INCLUDE_DYNAMIC_PROCESSES マクロを定義する必要がある。シンプルソケットではダイナミックプロセスを生成するためである。

イニシエータとターゲットモジュールを定義する。

struct Initiator: sc_module
{...};

struct Memory: sc_module
{...};

接続する。

SC_MODULE(Top)
{
  Initiator *initiator;
  Memory    *memory;

  SC_CTOR(Top)
  {
    initiator = new Initiator("initiator");
    memory    = new Memory   ("memory");

    initiator->socket.bind( memory->socket );
  }
};

トップレベルモジュールではイニシエータとメモリをインスタンシエーションする。イニシエータのイニシエータソケットをターゲットメモリのターゲットソケットにバインドする。ソケットは双方向通信に必要なすべてをカプセル化している。あるイニシエータソケットは常にあるターゲットソケットにバインドされる。イニシエータソケットはsc_portの派生クラスであり、ターゲットソケットはsc_exportの派生クラスである。バインド関数は一度の関数呼び出しでport-export接続を双方向に行う。これがソケットの便利な点である。

OSCIシミュレータで動かすにはsc_main関数が必要。

int sc_main(int argc, char* argv[])
{
  Top top("top");
  sc_start();
  return 0;
}

イニシエータとメモリモジュールではソケットが宣言されてコンストラクトを明示的に行う必要がある。

struct Initiator: sc_module
{
  tlm_utils::simple_initiator_socket<Initiator> socket;

  SC_CTOR(Initiator) : socket("socket")
  {
    ...
};

struct Memory: sc_module
{
  tlm_utils::simple_target_socket<Memory> socket;

  SC_CTOR(Memory) : socket("socket")
  {
    ...
};

すべてのTLM-2.0ライブラリはtlmもしくはtlm_utils名前空間に属する。
simple_initiator_socket<Initiator>、simple_target_socket<Memory>のテンプレート引数は親モジュールを指定する。
シンプルソケットは tlm_initiator_socket や tlm_target_socket の派生クラスであるが、ここではその詳細を知る必要はない。

この例ではイニシエータはブロッキング転送を使ってターゲットメモリと通信する。そのためには b_transport メソッドを実装する必要がある。
シンプルターゲットソケットを使う場合、以下のように、ターゲットでソケットに対してコールバックメソッドを登録する。

socket.register_b_transport(this, &Memory::b_transport);

ターゲットがすべきことは b_transport メソッドを実装することだけであり、以下で述べる。

補注:ここまでのコードをまとめると以下のような感じになる(コンパイル可能)。
newは令和の時代では使うのは微妙なので実体をTopに置いている。

#define SC_INCLUDE_DYNAMIC_PROCESSES

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>

using namespace sc_core;
using namespace sc_dt;
using namespace std;

SC_MODULE(Initiator) {
  tlm_utils::simple_initiator_socket<Initiator> socket;
  SC_CTOR(Initiator) : socket("socket") {
  }
};

SC_MODULE(Memory) {
  tlm_utils::simple_target_socket<Memory> socket;
  SC_CTOR(Memory) : socket("socket") {
    //socket.register_b_transport(this, &Memory::b_transport);
  }
};

SC_MODULE(Top) {
  Initiator initiator;
  Memory memory;
  SC_CTOR(Top) : initiator("initiator"), memory("memory") {
    initiator.socket.bind(memory.socket);
  }
};

int sc_main(int, char**) {
  Top top("top");
  sc_start();
  return 0;
}

The Generic Payload and Blocking Transport

ソケットクラスのデフォルト(=テンプレートクラス引数で特に指定しない場合)のトランザクションタイプとして、tlm_generic_payload が使われる。
汎用ペイロードはTLMの相互運用性を達成するという意味でTLM2.0標準のカギとなるものである。
汎用ペイロードは2つの密接に関連した目的がある。
1つ目の目的は、汎用目的のトランザクションタイプとして抽象メモリマップドバスのモデリングに使うことであり、特定のバスプロトコルの正確な詳細に関心がないときに既存のモデルとすぐさま接続できる。
2つ目の目的は、汎用ペイロードは様々な特定のプロトコルの詳細レベルをモデリングする際に基礎として(補注:基底クラスとしての意味か?)使用することができる。同じ汎用ペイロードタイプを基礎に作られた異なるプロトコル同士の橋渡しが比較的簡単にできる。

例題のイニシエータモジュールはスレッドプロセスを持ち汎用ペイロードトランザクションのストリームを生成する。

SC_CTOR(Initiator) : socket("socket")
{
  SC_THREAD(thread_process);
}

void thread_process()
{
  tlm::tlm_generic_payload* trans = new tlm::tlm_generic_payload;
  sc_time delay = sc_time(10, SC_NS);

  for (int i = 32; i < 96; i += 4)
  {
    ...
    socket->b_transport( *trans, delay );
    ...
  }
}

トランザクションは b_transport メソッドによりソケット経由で送られる。
トランザクションは参照渡しで渡され、返り値はない。
イニシエータはトランザクションのstorage破棄に責任がある。
b_transport 呼び出しではタイミングアノテーションも運ぶ。
タイミングアノテーションは現トランザクションが完了後、現シミュレーション時刻に加算される必要がある。
タイミングアノテーションはb_transport 呼び出し時もreturn時も有効である。

tlm::tlm_command cmd = static_cast(rand() % 2);
if (cmd == tlm::TLM_WRITE_COMMAND) data = 0xFF000000 | i;

trans->set_command( cmd );
trans->set_address( i );
trans->set_data_ptr( reinterpret_cast<unsigned char*>(&data) );
trans->set_data_length( 4 );
trans->set_streaming_width( 4 );
trans->set_byte_enable_ptr( 0 );
trans->set_dmi_allowed( false );
trans->set_response_status( tlm::TLM_INCOMPLETE_RESPONSE );

socket->b_transport( *trans, delay );

汎用ペイロードは標準的な属性を持つ。コマンド、アドレス、データ、バイトイネーブル、ストリーミング幅、返答ステータスである。
汎用ペイロードはDMIヒント・拡張もともに運ぶ。
それぞれの属性はデフォルト値を持つが、10のうち8つは明示的に設定することが推奨される。
なぜなら、トランザクションオブジェクトはプールから再利用されることが多いからである。

コマンドはread/writeの2種類がある。
アドレスはデータの読み込み・書き込みが行われる区間の最下位アドレスである。
データポインタはイニシエータ内のデータバッファを指し示し、データ長はデータのバイト数である。
実際のデータコピーはターゲット内で実行される。

ストリーミング幅は要するにバス幅のことである(超訳
ストリームでないトランザクションではストリーミング幅はデータ長と同じとなる。
デフォルト値は0だが、0はインタフェースメソッドコール経由で送られる場合許容されない。データポインタとデータ長も同様。

バイトイネーブルポインタは0であれば使用しないことを示す。バイトイネーブル長属性も存在するが、バイトイネーブルポインタが0の場合は無視されるためここでは設定しない。

set_dmi_allowed メソッドは DMI ヒントを設定する。これは常に falseを設定すべきである。
DMIヒントはターゲットにより設定されることもあり、Direct Memory Interfaceをサポートするかどうかを示す。

返答ステータスは常に TLM_INCOMPLETE_RESPONSE を設定すべきである。本当の返答ステータスはターゲットにより設定される。

ここで触れられていない10個目の属性は拡張機能の配列である。拡張機能については今後のチュートリアルで議論される。デフォルトではイニシエータでもターゲットでも無視される。

ブロッキング転送メソッドはターゲットメモリ内で実装される。まず、6属性が汎用ペイロードから取り出される。

virtual void b_transport( tlm::tlm_generic_payload& trans, sc_time& delay )
{
  tlm::tlm_command cmd = trans.get_command();
  sc_dt::uint64    adr = trans.get_address() / 4;
  unsigned char*   ptr = trans.get_data_ptr();
  unsigned int     len = trans.get_data_length();
  unsigned char*   byt = trans.get_byte_enable_ptr();
  unsigned int     wid = trans.get_streaming_width();

次に、ターゲットがサポートしていない機能をイニシエータが使おうとしてないかをチェックする。
ここではバイトイネーブル、ストリーミング幅、バースト長の確認をしている。
また、アドレスが範囲外かどうかもチェックする。
トランザクションが実行できない場合は、SystemCレポートハンドラーを呼んでエラー生成する。

  if (adr >= sc_dt::uint64(SIZE) || byt != 0 || len > 4 || wid < len)
    SC_REPORT_ERROR("TLM-2", 
                "Target does not support given generic payload transaction");

ターゲットはread/writeコマンドをデータコピーにより実装する。エンディアンに関しては汎用ペイロードはホストコンピュータと同じエンディアンを使う。
ターゲットメモリがホストのエンディアンと同じ限り、データコピーはmemcpyにより実現できる。

  if ( cmd == tlm::TLM_READ_COMMAND )
    memcpy(ptr, &mem[adr], len);
  else if ( cmd == tlm::TLM_WRITE_COMMAND )
    memcpy(&mem[adr], ptr, len);

最後にブロッキング転送は返答ステータスをトランザクションに設定し、トランザクションの完了成功を示す。
設定しない場合、トランザクションが未完了を示すことになる。

  trans.set_response_status( tlm::TLM_OK_RESPONSE );

Timing Annotation

ブロッキング転送メソッドでは機能のみモデリングし、時間についてはモデリングしないため、単にdelay引数は無視し、変更しないままイニシエータに返す。
b_transport 呼び出し後は、イニシエータは返答ステータスをチェックする。

if (trans->is_response_error() )
  SC_REPORT_ERROR("TLM-2", "Response error from b_transport");

ここで、イニシエータはアノテートされた時間を認識する必要がある。
このモデルは機能のみに関心があるため、delayを無限に累算し続けてもよい。
このアイディアでは、シミュレーションモデルは機能をフルスピードでシミュレーションし、消費時間は「内緒で」累算し続けることになる。
このコーディングスタイルはTLM-2.0標準ではLoosly-Timedと呼ばれる。
しかしながら、モデルが実際にするのはb_transport呼び出しで返ってきたdelayに従ってwaitすることである。
これはシミュレーション実行速度を遅くする。なぜならコンテキストスイッチングがトランザクション毎に発生するからである。
しかし、単純な例題ではこれで十分だし、シミュレーションログも解釈が容易である。

補注:全体のコードはここで参照できる。
https://github.com/marcelosousa/llvmvf/blob/master/tests/systemc/tlm/tlm2_getting_started_1.cpp

私が書き直したコードは以下。

#define SC_INCLUDE_DYNAMIC_PROCESSES

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>

using namespace sc_core;
using namespace sc_dt;
using namespace std;

SC_MODULE(Initiator) {
  tlm_utils::simple_initiator_socket<Initiator> socket;
  int data;
  SC_CTOR(Initiator) : socket("socket"), data(0) {
    SC_THREAD(thread_process);
  }
  void thread_process() {
    tlm::tlm_generic_payload trans;
    sc_time delay = sc_time(10, SC_NS);
    for (int i = 32; i < 96; i += 4) {
      tlm::tlm_command cmd = static_cast<tlm::tlm_command>(rand() % 2);
      if (cmd == tlm::TLM_WRITE_COMMAND) data = 0xFF000000 | i;
      trans.set_command(cmd);
      trans.set_address(i);
      trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
      trans.set_data_length(4);
      trans.set_streaming_width(4);
      trans.set_byte_enable_ptr(0);
      trans.set_dmi_allowed(false);
      trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);
      cout << sc_time_stamp() << ": [initiator] cmd=" << cmd << " addr=" << i;
      if (cmd) cout << " data=" << data;
      cout << endl;
      socket->b_transport(trans, delay);
      if (trans.is_response_error()) {
        SC_REPORT_ERROR("TLM-2", "Response error from b_transport");
      }
      wait(delay);
    }
  }
};

SC_MODULE(Memory) {
  tlm_utils::simple_target_socket<Memory> socket;
  static constexpr int SIZE = 256;
  int mem[SIZE];
  SC_CTOR(Memory) : socket("socket") {
    socket.register_b_transport(this, &Memory::b_transport);
  }
  virtual void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    tlm::tlm_command cmd = trans.get_command();
    sc_dt::uint64    adr = trans.get_address() / 4;
    unsigned char*   ptr = trans.get_data_ptr();
    unsigned int     len = trans.get_data_length();
    unsigned char*   byt = trans.get_byte_enable_ptr();
    unsigned int     wid = trans.get_streaming_width();
    if (adr >= sc_dt::uint64(SIZE) || byt != 0 || len > 4 || wid < len) {
      SC_REPORT_ERROR("TLM-2", "Target does not support given generic payload transaction");
    }
    if (cmd == tlm::TLM_READ_COMMAND) {
      memcpy(ptr, &mem[adr], len);
    } else if (cmd == tlm::TLM_WRITE_COMMAND) {
      memcpy(&mem[adr], ptr, len);
    }
    cout << sc_time_stamp() << ": [memory] cmd=" << cmd << " addr=" << adr * 4;
    if (cmd) cout << " data=" << *reinterpret_cast<int*>(ptr);
    cout << endl;
    trans.set_response_status(tlm::TLM_OK_RESPONSE);
  }
};

SC_MODULE(Top) {
  Initiator initiator;
  Memory memory;
  SC_CTOR(Top) : initiator("initiator"), memory("memory") {
    initiator.socket.bind(memory.socket);
  }
};

int sc_main(int, char**) {
  Top top("top");
  sc_start();
  return 0;
}

Interoperability and the Base Protocol

まとめると、TLM-2.0は標準的なAPIを使ったモデル間の相互運用性を確かにし、ユーティリティクラスによって生産性の向上と一貫したコーディングスタイルを提供するものである。相互運用性のキーは

  1. 標準のイニシエータソケット・ターゲットソケットを使う。
  2. 汎用ペイロードを使う。
  3. ベースプロトコルを使う。

このチュートリアルではまだベースプロトコルの詳細については触れられていないが、シンプルソケットと b_transport メソッドで暗に使用されている。ベースプロトコルは汎用ペイロードの使い方のルールを指定する。これはのちのチュートリアルで詳細を示す。

ブロッキング転送インタフェースは1回の関数呼び出しでトランザクションが完了するときに使う。
トランザクション要求がb_transport呼び出しで渡され、返答はb_transportからのreturn時に運ばれる。
この例では、一つのトランザクションオブジェクトが呼び出し毎に再利用される。トランザクションオブジェクトのstorageは最初にイニシエータ内ですべて割り当てられる。この方式で動くのは、同時に1つのトランザクションのみが渡されるためである。

この例題の別の特徴としては、ブロッキング転送メソッドがブロックしない点である。つまり、waitを呼び出さない点である。
b_transport内でwaitを呼ぶこともできた。しかしながら、原理的にはイニシエータ内の複数のスレッドから同じソケットを介してb_transportへの複数の並行した呼び出すこともでき、これはタイミングアノテーションと競合する可能性がある。
この状況はベースプロトコルのルールでは許容される。