Getting Started with TLM-2.0 (Tutorial 2 - Response Status, DMI, and Debug Transport)

www.doulos.com

2つ目のチュートリアルでは汎用ペイロードの返答ステータス、デバッグメモリ、デバッグ転送インタフェースを見ていく。

The Response Status of the Generic Payload

Example 1ではターゲットがトランザクションを実行できなかった場合、単純にSystemCレポートハンドラーである SC_REPORT_ERROR()を呼び出して諦めた。
これは許容できるが、より構造的なエラーハンドリングの方法がある。すなわち、汎用ペイロードの返答ステータスである。
返答ステータスはトランザクションオブジェクトの一部であり、トランザクション完了時にイニシエータによってチェックできる。

返答ステータスのデフォルト値は TLM_INCOMPLETE_RESPONSE であり、トランザクションがターゲットに到達していないかターゲットによってトランザクションが実行されていないことを表している。
トランザクションが成功した場合、ターゲットは返答ステータスとして TLM_OK_RESPONSE を設定する。
もしトランザクションが失敗した場合、ターゲットは定義済みエラーから選ぶことができる。

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)) {
    trans.set_response_status( tlm::TLM_ADDRESS_ERROR_RESPONSE );
    return;
  }

アドレスエラーはアドレスが範囲外であったり、アドレス値が原因でトランザクションが失敗した場合に使用される。

  if (byt != 0) {
    trans.set_response_status( tlm::TLM_BYTE_ENABLE_ERROR_RESPONSE );
    return;
  }

バイトイネーブルエラーはバイトイネーブル値によってターゲットでエラーが生じた場合やターゲットがバイトイネーブルを全くサポートしていない場合に使用される。

  if (len > 4 || wid < len) {
    trans.set_response_status( tlm::TLM_BURST_ERROR_RESPONSE );
    return;
  }

バーストエラーはデータ長あるいはストリーミング幅が原因でターゲットでエラーが生じた場合や、ターゲットがバースト転送をまったく対応しない場合に使用される。

5つ目のエラー TLM_GENERIC_ERROR_RESPONSE はトランザクション中に生じたいかなるエラーにも使用できる。

イニシエータがトランザクション完了後に返答エラーをチェックするのに役立つメソッドを汎用ペイロードは提供する。

socket->b_transport( *trans, delay );

if ( trans->is_response_error() )
{
  char txt[100];
  sprintf(txt, "Error from b_transport, response status = %s",
          trans->get_response_string().c_str());
  SC_REPORT_ERROR("TLM-2", txt);
}

is_response_ok、is_response_error を使うと明示的に値をチェックするのを避けることができ便利である。
get_response_string は返答を文字列で返す。エラーメッセージを出力する際に便利である。

Using the Direct Memory Interface

Direct Memory Interface (DMI) を使う目的は、イニシエータに直接ターゲット内のメモリ領域のポインタを与えることでシミュレーションをスピードアップすることである。したがって、トランスポートインタフェース経由のread/writeトランザクションはバイパスされる。
DMIはforward/backwardインタフェースの両方を使う。fowardパスはイニシエータソケットからターゲットソケットへの関数呼び出しのことであり、backwardパスはその逆方向である。forward DMIインタフェースではイニシエータがDirect Memory Pointerをターゲットに要求し、backward DMIインタフェースではターゲットが以前イニシエータに与えたDMI pointerを無効化する。

新しいDMIポインタを取得するメソッドから見ていく。このメソッドは get_direct_mem_ptr で、イニシエータから foward pathで呼び出され、ターゲットで実装される。メモリ(ターゲット)は simple_target_socket を使うので、b_transportと同様にターゲットはソケットに実装を登録しなければならない。そうでなければ、シンプルソケットは何も実行しないデフォルト実装を割り当てる。

socket.register_get_direct_mem_ptr(this, &Memory::get_direct_mem_ptr);

実装は以下のとおりである。

virtual bool get_direct_mem_ptr(tlm::tlm_generic_payload& trans,
                                tlm::tlm_dmi& dmi_data)
{
  dmi_data.allow_read_write();

イニシエータは特定アドレス・特定アクセスモード(R/W/RW)のDMIポインタを要求する。
ターゲットは要求を許可するかどうかを決定する。この例ではターゲットはどんなリクエストに対しても許可する。
ターゲットは DMI データオブジェクトに対して与えるアクセスの詳細を設定する必要がある。

  dmi_data.set_dmi_ptr( reinterpret_cast( &mem[0] ) );
  dmi_data.set_start_address( 0 );
  dmi_data.set_end_address( SIZE*4-1 );
  dmi_data.set_read_latency( LATENCY );
  dmi_data.set_write_latency( LATENCY );

  return true;
}

dmi_ptr は実際のダイレクトメモリポインターである。これは要求アドレスと必ずしも対応する必要がない。なぜならば、ターゲットはどのDMI領域を与えるかは自由だからである。ターゲットが許可する領域が大きいことは望ましくない。start_address と end_address はターゲットから見たDMI領域の境界を表し、例題ではメモリの全域を設定している。
read_latency と write_latency はメモリアクセスのタイミングパラメータの見積もりであり、モデルのタイミング制度に応じてイニシエータ側で使用されたり無視されたりする。

get_direct_mem_ptr は DMIポインタを供給できる場合は true を返し、そうでなければ false を返す。

ターゲットにはまだもう一つ考えられる改善がある。ターゲットは汎用ペイロードのDMIヒント属性を使ってDMIをサポートできるかどうかをイニシエータに伝達することができる。これによりイニシエータのシミュレーション速度を向上できる。なぜならば、get_direct_mem_ptr を繰り返し呼び出して fail が返り続けることは無駄だからである。したがって、我々の例では b_transport メソッドは以下のようにDMI ヒントを設定している。

trans.set_dmi_allowed(true);

ここからはイニシエータがDMIヒントをどのように使うかを見ていく。トランザクション完了後、イニシエータは汎用ペイロード内のDMIヒントをチェックする。もし、ヒントが設定されている場合、イニシエータはターゲットにDMIポインタを要求する。

tlm::tlm_generic_payload* trans = new tlm::tlm_generic_payload;
...
socket->b_transport( *trans, delay  );

if ( trans->is_dmi_allowed() )
{
  dmi_ptr_valid = socket->get_direct_mem_ptr( *trans, dmi_data );
}

イニシエータは b_transport を呼び出し、DMIヒントをチェックし、get_direct_mem_ptr を呼び出して dmi_ptr_valid フラグを設定し、DMIポインタが有効であることを示す。
イニシエータは全く同じトランザクションオブジェクトを転送にもDMIにも使用することに留意すること。これにより、シミュレーション効率が改善する。
その後、イニシエータはDMIポインタを使えるようになり、トランスポートインタフェースをバイパスできる。

if (dmi_ptr_valid)
{
  if ( cmd == tlm::TLM_READ_COMMAND )
  {
    assert( dmi_data.is_read_allowed() );
    memcpy(&data, dmi_data.get_dmi_ptr() + i, 4);
    wait( dmi_data.get_read_latency() );
  }
  else if ( cmd == tlm::TLM_WRITE_COMMAND )
  {
    assert( dmi_data.is_write_allowed() );
    memcpy(dmi_data.get_dmi_ptr() + i, &data, 4);
    wait( dmi_data.get_write_latency() );
  }
}
else
{
  ...
  socket->b_transport(*trans, delay );
  ...
  if ( trans->is_dmi_allowed() )
    ...
}

上のコードでは DMI read_latency と write_latency を使っていることに留意すること。
イニシエータが DMI を使っているとき、dmi_data オブジェクト経由でレイテンシ情報を受け取る。

これで forward DMI interfaceに関する説明を終える。ここからはbackward interfaceについて述べる。
イニシエータは invalidate_direct_mem_ptr を実装する必要がある。
この関数はターゲットからの要求に応じて既存のポインタを消去し、また、イニシエータソケットに登録される必要がある。

socket.register_invalidate_direct_mem_ptr(
       this, &Initiator::invalidate_direct_mem_ptr);
...

virtual void invalidate_direct_mem_ptr(sc_dt::uint64 start_range,
                                       sc_dt::uint64 end_range)
{
  dmi_ptr_valid = false;
}

この例ではイニシエータはダイレクトメモリ領域の境界を無視し、単純に DMI ポインタを無効化している。

Using the Debug Transport Interface

デバッグ転送インタフェースの目的はイニシエータが副作用なしに・シミュレーション時間経過なしにターゲットのメモリを読み書きできるようにすることである。
DMIとデバッグ転送インタフェースは似ているが、その意図は全く異なる。
DMIは通常のトランザクションのシミュレーション速度を改善するためのものであり、デバッグ転送インタフェースはデバッグ用途である。

ここではイニシエータからターゲットへのfoward pathのデバッグ転送インタフェースを使う。
ターゲットは transport_dbg を実装する必要があり、シンプルターゲットソケットの場合ソケットにメソッドを登録する必要がある。
そうしない場合、DMIと同様、シンプルソケットは何もしないデフォルト実装を供給する。

socket.register_transport_dbg(this, &Memory::transport_dbg);
...

virtual unsigned int transport_dbg(tlm::tlm_generic_payload& trans)
{
  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 int num_bytes = (len < SIZE - adr) ? len : SIZE - adr;

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

  return num_bytes;
}

デバッグ転送インタフェースはこれまでと同様 tlm_generic_payload を使う。
転送インタフェースと比較して、デバッグ転送インタフェースは制限された属性セットを使う:コマンド、アドレス、データポインタ、データ長。
上の例のように、transport_dbg メソッドは与えらえたデータ長のすべてを読み書きする必要はなく、可能な限りの長さを読み書きし、その長さを返す。
このメソッドの実装は意図的に単純にしている。トランザクション内のポインタを使ってデータコピーする以外のことをしてはならない。そうでないとデバッグ転送の目的を達成できない。

最後に、イニシエータが transport_dbg を呼び出してメモリの内容をダンプする例を見る。

trans->set_address(0);
trans->set_read();
trans->set_data_length(128);

unsigned char* data = new unsigned char[128];
trans->set_data_ptr(data);

unsigned int n_bytes = socket->transport_dbg( *trans );

for (unsigned int i = 0; i < n_bytes; i += 4)
{
  cout << "mem[" << i << "] = "
       << *(reinterpret_cast<unsigned int*>( &data[i] )) << endl;
}