FPGA開発日記

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

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

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

www.gem5.org

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

msyksphinz.hatenablog.com


リクエスト受信の実装

recvTimingReqの実装はより複雑である。SimpleMemobjがリクエストを受信できるかどうかをチェックする必要がある。SimpleMemobjは非常にシンプルなブロック構造をしている; 1つのリクエストのみ同時に受け付けることができる。従って、あるリクエストを処理中に別のリクエストが到達すると、2番目のリクエストはブロックされる。

実装を単純にするために、CPUSidePortはポートインタフェースにすべてのフローコントロール情報を格納する。したがって、CPUSidePortに別のメンバ変数としてneedRetryを追加する必要がある。この変数はboolean変数であり、SimpleMemobjがフリーになった場合にリトライが必要であることを意味する。そして、SimpleMemobjはリクエストがブロックされ、将来的にリトライする実行する必要があることを意味する。

bool
SimpleMemobj::CPUSidePort::recvTimingReq(PacketPtr pkt)
{
    if (!owner->handleRequest(pkt)) {
        needRetry = true;
        return false;
    } else {
        return true;
    }
}

SimpleMemobjへのリクエストを処理するために、まずSimpleMemobjが他のリクエストへの応答を待って既にブロックされているかどうかをチェックする。ブロックされている場合、falseを返し、呼び出されたマスタポートに今リクエストを 受け付けられないことを通知する。そうでなければ、ポートをブロックされたものとしてマークし、パケットをメモリポートから送信する。このために、SimpleMemobj実装からフロー制御を隠すために、MemSidePortオブジェクトにヘルパー関数を定義することができる。memPortが全てのフロー制御を処理し、リクエストの消費に成功したので、常にhandleRequestからtrueを返すと仮定する。

bool
SimpleMemobj::handleRequest(PacketPtr pkt)
{
    if (blocked) {
        return false;
    }
    DPRINTF(SimpleMemobj, "Got request for addr %#x\n", pkt->getAddr());
    blocked = true;
    memPort.sendPacket(pkt);
    return true;
}

次に、MemSidePortsendPacket関数を実装する必要がある。この関数は、相手のスレーブ・ポートがリクエストを受け付けられない場合のフロー制御を行う。そのために、MemSidePortに、パケットがブロックされた場合に備えてパケットを保存するためのメンバを追加する必要がある。レシーバーがリクエスト(またはレスポンス)を受信できない場合にパケットを保存するのは、送信側の責任である。 送信に失敗した場合、このオブジェクトはパケットをblockedPacketメンバ関数に格納し、後で(recvReqRetryを受信したときに)パケットを送信できるようにする。この関数には、バグがないことを確認するための防御コードも含まれており、blockedPacket変数を間違って上書きしようとすることはない。

void
SimpleMemobj::MemSidePort::sendPacket(PacketPtr pkt)
{
    panic_if(blockedPacket != nullptr, "Should never try to send if blocked!");
    if (!sendTimingReq(pkt)) {
        blockedPacket = pkt;
    }
}

次に、パケットを再送するコードを実装する必要がある。この関数では、上で書いたsendPacket関数を呼び出して、パケットの再送を試みる。

void
SimpleMemobj::MemSidePort::recvReqRetry()
{
    assert(blockedPacket != nullptr);

    PacketPtr pkt = blockedPacket;
    blockedPacket = nullptr;

    sendPacket(pkt);
}

レスポンス受信の実装

応答のコードパスは受信のコードパスに似ている。MemSidePortが応答を取得すると、SimpleMemobjを通して適切なCPUSidePortに応答を転送する。

bool
SimpleMemobj::MemSidePort::recvTimingResp(PacketPtr pkt)
{
    return owner->handleResponse(pkt);
}

SimpleMemobjでは、まず、オブジェクトがブロッキングされているため、応答を受信すると常にブロックされる。パケットをCPU側に送り返す前に、オブジェクトがもはやブロックされていないことをマークする必要がある。これは、sendTimingRespを呼び出す前に行わなければならない。そうしないと、マスター・ポートが応答を受信してから別のリクエストを送信するまでの間に1つのコールチェーンを持っている可能性があるため、無限ループにはまる可能性がある。

SimpleMemobjのブロックを解除した後、パケットが命令パケットかデータパケットかをチェックし、適切なポートに送り返す。最後に、オブジェクトのブロックが解除されたので、CPU側のポートに、失敗したリクエストの再試行が可能になったことを通知する必要があるかもしれない。

bool
SimpleMemobj::handleResponse(PacketPtr pkt)
{
    assert(blocked);
    DPRINTF(SimpleMemobj, "Got response for addr %#x\n", pkt->getAddr());

    blocked = false;

    // Simply forward to the memory port
    if (pkt->req->isInstFetch()) {
        instPort.sendPacket(pkt);
    } else {
        dataPort.sendPacket(pkt);
    }

    instPort.trySendRetry();
    dataPort.trySendRetry();

    return true;
}

MemSidePortにパケット送信用の便利関数を実装したのと同様に、CPUSidePortsendPacket関数を実装して、CPU側にレスポンスを送信することができる。この関数はsendTimingRespを呼び出し、その結果、相手マスター・ポートのrecvTimingRespが呼び出される。この呼び出しが失敗し、ピアポートが現在ブロックされている場合、後で送信するためにパケットを保存する。

void
SimpleMemobj::CPUSidePort::sendPacket(PacketPtr pkt)
{
    panic_if(blockedPacket != nullptr, "Should never try to send if blocked!");

    if (!sendTimingResp(pkt)) {
        blockedPacket = pkt;
    }
}

このブロックされたパケットは、後でrecvRespRetryを受信したときに送信する。この関数は、上記のrecvReqRetryとまったく同じで、単にパケットを再送しようとするだけである。

void
SimpleMemobj::CPUSidePort::recvRespRetry()
{
    assert(blockedPacket != nullptr);

    PacketPtr pkt = blockedPacket;
    blockedPacket = nullptr;

    sendPacket(pkt);
}

最後に、CPUSidePort用の追加関数trySendRetryを実装する必要がある。trySendRetrySimpleMemobjが新しいリクエストでブロックされたときにrecvTimingReqでマークしたリトライが必要かどうかをチェックする。そして、リトライが必要な場合、この関数はsendRetryReqを呼び出し、次に相手マスターポート(この場合はCPU)のrecvReqRetryを呼び出す。

void
SimpleMemobj::CPUSidePort::trySendRetry()
{
    if (needRetry && blockedPacket == nullptr) {
        needRetry = false;
        DPRINTF(SimpleMemobj, "Sending retry req for %d\n", id);
        sendRetryReq();
    }
}

この関数に加えて、ファイルを完成させるためにSimpleMemobjcreate関数を追加する。

SimpleMemobj*
SimpleMemobjParams::create()
{
    return new SimpleMemobj(this);
}

以下の図は、CPUSidePortMemSidePortSimpleMemobj の関係を示している。この図は、ピアポートがSimpleMemobjの実装とどのように相互作用するかを示している。太字の関数はそれぞれ実装しなければならないもので、太字でない関数はピアポートへのポートインターフェースである。色は、オブジェクトを通るAPIパスの1つ(例えば、リクエストの受信やメモリ範囲の更新)をハイライトしている。

この単純なメモリー・オブジェクトでは、パケットはCPU側からメモリー側に転送されるだけだ。しかし、handleRequesthandleResponseを変更することで、次の章のキャッシュのようなリッチな機能を持つオブジェクトを作ることができる。