FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Gem5のチュートリアル "Learning Gem5"をやってみる(13. メモリシステムへのSimpleMemobjの作成)

gem5を構成するSimObjectを追加するためのチュートリアルをやってみる。

www.gem5.org

  • gem5記事一覧インデックス

msyksphinz.hatenablog.com


単純なメモリ・オブジェクトの例

このセクションでは、単純なメモリ・オブジェクトを作る。最初は、単純にCPU側(単純なCPU)からメモリー側(単純なメモリーバス)にリクエストを通すだけである。下の図を見てほしい。メモリ・バスにリクエストを送るためのマスター・ポートが1つと、CPUの命令キャッシュ・ポートとデータ・キャッシュ・ポート用のCPU側ポートが2つある。次の章simplecache-chapterでは、このオブジェクトをキャッシュにするためのロジックを追加する。

SimObjectを宣言する

SimObjectを宣言する hello-simobject-chapterで単純なSimObjectを作成したときと同じように、最初のステップはSimObject Pythonファイルを作成することである。この単純なメモリオブジェクトをSimpleMemobjと呼び、SimObject Pythonファイルをsrc/learning_gem5/simple_memobjに作成する。

from m5.params import *
from m5.proxy import *
from m5.SimObject import SimObject

class SimpleMemobj(SimObject):
    type = 'SimpleMemobj'
    cxx_header = "learning_gem5/part2/simple_memobj.hh"

    inst_port = SlavePort("CPU side port, receives requests")
    data_port = SlavePort("CPU side port, receives requests")
    mem_side = MasterPort("Memory side port, sends requests")

このオブジェクトはSimObjectを継承している。SimObjectクラスには、C++の実装で定義しなければならない純粋仮想関数getPortがある。

このオブジェクトのパラメータは3つのポートである。CPUが命令ポートとデータ・ポートに接続するための2つのポートと、メモリー・バスに接続するためのポートだ。これらのポートにはデフォルト値はなく、簡単な説明しかない。

これらのポートの名前を覚えておくことが重要である。SimpleMemobjを実装し、getPort関数を定義する際に、これらの名前を明示的に使用する。

もちろん、SimObject Pythonファイルを宣言するSConscriptファイルも新しいディレクトリに作成する必要がある。

SimpleMemobjクラスを定義する

SimpleMemobjのヘッダファイルを作成する。

#include "mem/port.hh"
#include "params/SimpleMemobj.hh"
#include "sim/sim_object.hh"

class SimpleMemobj : public SimObject
{
  private:

  public:

    /** constructor
     */
    SimpleMemobj(SimpleMemobjParams *params);
};

slaveポート型を定義する

次に、2種類のポート(CPU側ポートとメモリー側ポート)のクラスを定義する必要がある。他のオブジェクトがこれらのクラスを使うことはないので、SimpleMemobjクラスの中でこれらのクラスを宣言する。

まず、スレーブポート(CPU側ポート)から始めよう。SlavePortクラスを継承する。以下は、SlavePortクラスのすべての純粋仮想関数をオーバーライドするために必要なコードである。

class CPUSidePort : public SlavePort
{
  private:
    SimpleMemobj *owner;

  public:
    CPUSidePort(const std::string& name, SimpleMemobj *owner) :
        SlavePort(name, owner), owner(owner)
    { }

    AddrRangeList getAddrRanges() const override;

  protected:
    Tick recvAtomic(PacketPtr pkt) override { panic("recvAtomic unimpl."); }
    void recvFunctional(PacketPtr pkt) override;
    bool recvTimingReq(PacketPtr pkt) override;
    void recvRespRetry() override;
};

このオブジェクトでは、5つの関数を定義する必要がある。

このオブジェクトはまた、1つのメンバ変数を持つので、そのオブジェクトの関数を呼び出すことができる。

masterポート型を定義する

次に、マスター・ポート・タイプを定義する必要がある。これは、CPU側からのリクエストをメモリー・システムの残りの部分に転送するメモリー側のポートになる。

class MemSidePort : public MasterPort
{
  private:
    SimpleMemobj *owner;

  public:
    MemSidePort(const std::string& name, SimpleMemobj *owner) :
        MasterPort(name, owner), owner(owner)
    { }

  protected:
    bool recvTimingResp(PacketPtr pkt) override;
    void recvReqRetry() override;
    void recvRangeChange() override;
};

このクラスには、必ずオーバライドしなければならない純粋仮想関数が3つ用意されている。

SimObjectインタフェースを定義する

ここまでで、2つの新しい型CPUSidePortMemSidePortを定義し、SimpleMemobjで使用する3つのポートを宣言できるようになった。SimPbjectクラスの純粋仮想関数についても定義する必要があり、getPortである。この関数はポートを通じてメモリオブジェクトを接続る初期化フェーズで実行される。

class SimpleMemobj : public SimObject
{
  private:

    <CPUSidePort declaration>
    <MemSidePort declaration>

    CPUSidePort instPort;
    CPUSidePort dataPort;

    MemSidePort memPort;

  public:
    SimpleMemobj(SimpleMemobjParams *params);

    Port &getPort(const std::string &if_name,
                  PortID idx=InvalidPortID) override;
};

基本的なSimObject関数の実装

SimpleMemobjのコンストラクタでは、単純にSimObjectコンストラクタが呼ばれるだけである。また、すべてのポートを初期化する必要がある。それぞれのポートのコンストラクタは2つの引数を持つ:ポートの名前と、その所持者のポインタであり、これらはヘッダファイルに定義したものを使用する。名前はどのような文字列でも良いが、利便性のためにPythonのSimObjectファイルの名前と同一にする。また、blocked変数をfalseとする。

#include "learning_gem5/part2/simple_memobj.hh"
#include "debug/SimpleMemobj.hh"

SimpleMemobj::SimpleMemobj(SimpleMemobjParams *params) :
    SimObject(params),
    instPort(params->name + ".inst_port", this),
    dataPort(params->name + ".data_port", this),
    memPort(params->name + ".mem_side", this), blocked(false)
{
}

getPortを実装するために、if_nameを比較してmem_sideという名前であるかをチェックする。もしそうであれば、memPortオブジェクトを返す。名前がinst_portであれば、instPortを返し、data_portであればdataPortを返す。そうでなければ、新たに上位のgetPort()を返すことになる。

Port &
SimpleMemobj::getPort(const std::string &if_name, PortID idx)
{
    panic_if(idx != InvalidPortID, "This object doesn't support vector ports");

    // This is the name from the Python SimObject declaration (SimpleMemobj.py)
    if (if_name == "mem_side") {
        return memPort;
    } else if (if_name == "inst_port") {
        return instPort;
    } else if (if_name == "data_port") {
        return dataPort;
    } else {
        // pass it along to our super class
        return SimObject::getPort(if_name, idx);
    }
}

slaveポートとmasterポート関数の実装

slaveとmasterポートの実装はシンプルである。ほとんどの場合、ポートのか縫うは情報をメインメモリオブジェクト(SimpleMemobj)にフォワードするだけである。

2つのシンプルな関数から始める:getAddrRanges()とrecvFunctional()は、単純にSimpleMemobjのものを呼び出すだけである

AddrRangeList
SimpleMemobj::CPUSidePort::getAddrRanges() const
{
    return owner->getAddrRanges();
}

void
SimpleMemobj::CPUSidePort::recvFunctional(PacketPtr pkt)
{
    return owner->handleFunctional(pkt);
}

これらの関数の実装はSimpleMemobjのものと同一である。これらの実装は単純にリクエストをメモリ側に転送するだけである。DPRINTF関数を使用して、どのように関数が呼ばれているのかをトラックすることも可能である。

void
SimpleMemobj::handleFunctional(PacketPtr pkt)
{
    memPort.sendFunctional(pkt);
}

AddrRangeList
SimpleMemobj::getAddrRanges() const
{
    DPRINTF(SimpleMemobj, "Sending new ranges\n");
    return memPort.getAddrRanges();
}

MemSidePortと同様に、recvRangeChangeを実装する必要があり、SimpleMemobjを通じてスレーブポートに転送を行う必要がある。

void
SimpleMemobj::MemSidePort::recvRangeChange()
{
    owner->sendRangeChange();
}
void
SimpleMemobj::sendRangeChange()
{
    instPort.sendRangeChange();
    dataPort.sendRangeChange();
}