FPGA開発日記

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

【供養】2019年 CQ出版インターフェースでのRISC-V特集時にボツとなった原稿を無料公開します

2019年 にCQ出版さんのインターフェースで、RISC-V特集時に原稿を書かせていただいていました。

msyksphinz.hatenablog.com

そのときにには7本の原稿を提出していましたが、実は雑誌に掲載されていたのは5本でした。 残りの2本は紙面の関係上掲載は見送りとなっていました。 いろいろ議論したうえで、そのうちの1本は将来的にも使う事は無いということで、CQ出版側にも許可をもらって無料公開を決めました。

公開することにしたのは、SiFive社の販売していたHiFive Unleashedを使ってみた記事です。

msyksphinz.github.io

残念ながら、HiFive Unleashedは販売が終了(在庫をすべて売り切った)ため、今後再版される予定はないそうです。 したがって、今後この情報が活用される可能性は少ないということで「では無償公開で」と供養をすることにしました。

やはり、雑誌に掲載する記事って水物なので、タイムリーに掲載できないと厳しいですよね。 それが雑誌のメリットであり、最新の情報をスピーディーに掲載できるところが読者にとって魅力的です。

まあ今回の場合は割とざっくりとした原稿依頼から始まって、目次の組み立てから構成までほとんど思い通りに書かせてもらったので、余ってしまう原稿が発生してしまうのは仕方がないことですね。 今後ともよろしくお願いいたします。

f:id:msyksphinz:20191024012004p:plain:w200

Diplomacyを使ってOCPバスを作成する(7. Bundleの改善)

これまでDiplomacyを使ってOCPバスを作ってきたが、まだ改善したいところがある。まずは

  • OCPバスなのにコマンドチャネルにValid信号がある。
  • 各種コマンドをサポートしていきたい。

特にValid信号は大きな問題だ。OCPはCommand ⇔ Ready信号のハンドシェークなので、Valid信号は不要だ。これを何とかして削除したい。

DiplomacyのBundleがValid/Readyを持っている理由

ではそもそもなぜDiplomacyのバスはValid/Readyを使うようになっているのかというと、Bundleの基本がDecoupledIOになっているからだ。DecoupledIOはValid信号、Ready信号、データという構成になっており、これがTileLink / AMBAのベースになっている。

  • chisel-hw/rocketchip/src/main/scala/tilelink/Bundles.scala
class TLBundle(val params: TLBundleParameters) extends Record
{
...
  private val optA = Some                (Decoupled(new TLBundleA(params)))
  private val optB = params.hasBCE.option(Decoupled(new TLBundleB(params)).flip)
  private val optC = params.hasBCE.option(Decoupled(new TLBundleC(params)))
  private val optD = Some                (Decoupled(new TLBundleD(params)).flip)
  private val optE = params.hasBCE.option(Decoupled(new TLBundleE(params)))

  def a: DecoupledIO[TLBundleA] = optA.getOrElse(Wire(Decoupled(new TLBundleA(params))))
  def b: DecoupledIO[TLBundleB] = optB.getOrElse(Wire(Decoupled(new TLBundleB(params))))
  def c: DecoupledIO[TLBundleC] = optC.getOrElse(Wire(Decoupled(new TLBundleC(params))))
  def d: DecoupledIO[TLBundleD] = optD.getOrElse(Wire(Decoupled(new TLBundleD(params))))
  def e: DecoupledIO[TLBundleE] = optE.getOrElse(Wire(Decoupled(new TLBundleE(params))))

上記の通り、TileLinkのバスはすべてDecoupledIOがベースとなっている。従って、通信はすべてValid / Readyのハンド―シェークとなる。

  • rocketchip/src/main/scala/amba/axi4/Bundles.scala
class AXI4Bundle(params: AXI4BundleParameters) extends AXI4BundleBase(params)
{
  val aw = Irrevocable(new AXI4BundleAW(params))
  val w  = Irrevocable(new AXI4BundleW (params))
  val b  = Irrevocable(new AXI4BundleB (params)).flip
  val ar = Irrevocable(new AXI4BundleAR(params))
  val r  = Irrevocable(new AXI4BundleR (params)).flip
...

IrrevocalbleはReadyValidIOをベースとしているので、これも結局はReady / Validのハンドシェークだ。

一方で、APBはベースとなるBundleを使わずに、それぞれのビットを分解して定義していた。

  • rocketchip/src/main/scala/amba/apb/Bundles.scala
// Signal directions are from the master's point-of-view
class APBBundle(params: APBBundleParameters) extends APBBundleBase(params)
{
  // Flow control signals from the master
  val psel      = Bool(OUTPUT)
  val penable   = Bool(OUTPUT)

  // Payload signals
  val pwrite    = Bool(OUTPUT)
  val paddr     = UInt(OUTPUT, width = params.addrBits)
  val pprot     = UInt(OUTPUT, width = params.protBits)
  val pwdata    = UInt(OUTPUT, width = params.dataBits)
  val pstrb     = UInt(OUTPUT, width = params.dataBits/8)
  val pauser    = if (params.userBits > 0) Some(UInt(OUTPUT, width = params.userBits)) else None

  val pready    = Bool(INPUT)
  val pslverr   = Bool(INPUT)
  val prdata    = UInt(INPUT, width = params.dataBits)
...

そこで、OCPのBundleも新しく自分でベースのBundleを定義することにした。

  • chisel-hw/src/main/scala/ocp/Bundles.scala
class OCPBundle(val params: OCPBundleParameters) extends Record
{
  // Emulate a Bundle with elements abcde or ad depending on params.hasBCE

  private val optCmd  = Some (CmdReady(new OCPBundleCmd (params)))
  private val optData = Some (Decoupled(new OCPBundleData(params)))
  private val optResp = Some (Flipped(Decoupled(new OCPBundleResp(params))))

  def cmd : CmdReadyIO [OCPBundleCmd ] = optCmd. getOrElse(Wire(CmdReady (new OCPBundleCmd(params))))
  def data: DecoupledIO[OCPBundleData] = optData.getOrElse(Wire(Decoupled(new OCPBundleData(params))))
  def resp: DecoupledIO[OCPBundleResp] = optResp.getOrElse(Wire(Decoupled(new OCPBundleResp(params))))
...

DataチャネルとRespチャネルはそのままDecoupledIOを使用しているが、CommandチャネルはCmdReadyIOという新しいIOバンドルを作成した。

f:id:msyksphinz:20200208133913p:plain:w600
  • chisel-hw/src/main/scala/ocp/CmdReadyIO.scala
class CmdReadyIO[+T <: Data](gen: T) extends Bundle
{
  ...
  val ready = Input(Bool())
  val mcmd  = Output(UInt(3.W))
  val bits  = Output(genType)
}
object CmdReadyIO {
  implicit class AddMethodsToReadyValid[T<:Data](target: CmdReadyIO[T]) {
    /** Indicates if IO is both ready and valid
     */
    def fire(): Bool = target.ready && (target.mcmd =/= 0.U(3.W))
  }
}

上記のように、コマンドを示すOuput(UInt(3.W))と、Ready信号、そしてデータを示すbitsを定義した。

さて、これだけでは終わらない。次にOCPバッファの改造をおこなう。OCPバッファにはBufferParamsというパラメータが使われているが、実はこのパラメータの内部にDecoupledIOを使用するQueueの実装が隠れている。

  • rocketchip/src/main/scala/diplomacy/Parameters.scala
case class BufferParams(depth: Int, flow: Boolean, pipe: Boolean)
{
  require (depth >= 0, "Buffer depth must be >= 0")
  def isDefined = depth > 0
  def latency = if (isDefined && !flow) 1 else 0

  def apply[T <: Data](x: DecoupledIO[T]) =
    if (isDefined) Queue(x, depth, flow=flow, pipe=pipe)
    else x
  ...

上記のQueueの実装はDecoupledIOで実装されている。これではちょっと厳しいので、ついでにQueueも改造してCmdQueueを定義してしまおう。

  • chisel-hw/src/main/scala/ocp/CmdReadyIO.scala
case class BufferCmdParams(depth: Int, flow: Boolean, pipe: Boolean)
{
  require (depth >= 0, "Buffer depth must be >= 0")
  def isDefined = depth > 0
  def latency = if (isDefined && !flow) 1 else 0

  def apply[T <: Data](x: CmdReadyIO[T]) =
    if (isDefined) CmdQueue(x, depth, flow=flow, pipe=pipe)
    else x

  override def toString() = "BufferCmdParams:%d%s%s".format(depth, if (flow) "F" else "", if (pipe) "P" else "")
}


object BufferCmdParams
{
  implicit def apply(depth: Int): BufferCmdParams = BufferCmdParams(depth, false, false)

  val default = BufferCmdParams(2)
  val none    = BufferCmdParams(0)
  val flow    = BufferCmdParams(1, true, false)
  val pipe    = BufferCmdParams(1, false, true)
}

CmdQueueは通常のキューを、インタフェースをCmdReadyIOに変えただけである。

ここまでで、OCPの新しいBundleを使う準備が整った。

RustでRISC-V命令セットシミュレータを作ろう (11. 命令デコーダの実装)

f:id:msyksphinz:20190224185310p:plain:w400

Rust製自作命令セットシミュレータ、命令デコーダの実装を少しずつ改造している。

与えられた機械語から、命令をデコードして意味を解析する。 RISC-Vの機械語は非常に単純なので、命令デコーダの設計は簡単だ。 単純にcase文を並べていくだけで完成する。ここではRustのOption構文を使ってデコーダを作る。

命令が正常にデコードできる場合には何も問題ないのだが、不定命令をデコードしてしまった場合、「該当する命令が無い」ということをCPUに対して伝える必要がある。 すべての命令はenumで管理しているのだが、「どの命令でもない」ということを示すためにenumの最後にSENTINELを追加して、デコードに失敗したことを示していた。

  • C++での命令を示すenumの実装 (swimmer_riscv/src/inst_list_riscv.hpp)
enum class InstId_t : uint32_t {
    INST_ID_LUI = 0,
    INST_ID_AUIPC = 1,
    INST_ID_JAL = 2,
    INST_ID_JALR = 3,
...
    INST_ID_SENTINEL_MAX = 313    // デコードに失敗するとこの値が返される。
};
  • C++での命令デコーダの実装 (swimmer_riscv/src/inst_decoder_riscv.cpp)
// デコーダの戻り値はInstId_t型。もしデコードに失敗すると`INST_ID_SENTINEL_MAXが返される。
InstId_t RiscvDec::DecodeInst (InstWord_t inst) {
  InstWord_t field_LD = ExtractLDField (inst);
  switch (field_LD) {
    case 0x03 : 
    // Remaining Instruction is 269
    // lui        r[11:7],h[31:12]
...    
  • C++での命令デコーダの呼び出し部 (swimmer_riscv/src/riscv_pe_thread.cpp)
ExecStatus RiscvPeThread::StepExec (bool is_resume_break)
{
...
  InstId_t inst_idx = m_ptr_riscv_dec->DecodeInst (inst_hex);
...
  // デコードに失敗すると、例外にジャンプする。
  if (inst_idx == InstId_t::INST_ID_SENTINEL_MAX) {
    std::stringstream err_str;
    uint32_t bit_length  = m_bit_mode == RiscvBitMode_t::Bit32 ? 8 : 16;
    err_str << "<Error: instruction is not decoded. ["
...

一方Rustでは、想定する動きと異なる動作をキャッチするためにOption型を使うことができる。 Option型では通常値を保持しているのだが、想定しない動作や例外的な動作の場合に「何も値を持っていない」ということを示すために使用される。 これを使って、Rustの自作シミュレータではデコーダを以下のように作り変えた。

  • Rustでの命令デコーダ実装 (swimmer_rust/src/riscv64_insts.rs)
   // 命令デコーダ。通常はRiscvInstIdに準ずる値が返されるが、デコードに失敗した場合は"None"が返される。
   fn decode_inst(&mut self, inst: InstT) -> Option<RiscvInstId> {
        let opcode = inst & 0x7f;
...
  • Rustでの命令デコーダの呼び出し側 (swimmer_rust/src/main.rs)
        match riscv64_core.decode_inst(inst_data) {
            None => panic!("<Error: Unknown instruction : inst={:08x}>\n", inst_data),
            Some(inst_decode) => {
                riscv64_core.execute_inst(inst_decode, inst_data as InstT, count)
            },
        }

Option型は以下の2種類の値を取る。

  • Some(Data) : Option型に何らかの値が入っていることを示している。Dataが実際のデータだ。
  • None : Option型に値が何も入っていない。

デコード関数decode_inst()結果がOption型Option<RiscvInstId>で返されるので、その結果をmatchで判定しているわけだ。 何らかのデータが入っていればデコード成功、そうでなければデコード失敗としてエラーを出力する。(本当は命令実行例外にジャンプしなければなりませんが、実装をサボっている。。。)

デコーダの実装は、単純なmatch分を並べているだけで至って標準的なデコーダだ。

  • swimmer_rust/src/riscv64_insts.rs
    fn decode_inst(&mut self, inst: InstT) -> Option<RiscvInstId> {
        return match opcode {
            0x0f => match funct3 {
                0b000 => Some(RiscvInstId::FENCE),
                0b001 => Some(RiscvInstId::FENCEI),
                _ => None,
            },
            0x1b => match funct3 {
                0b000 => Some(RiscvInstId::ADDIW),
...

デコードに成功すると、いよいよ命令実行に入る。

命令実行ステージ

デコード結果に基づいて命令実行を行いる。実際これも大して工夫することなく実装することができた。例えば、ADDI命令の実装をする場合、

    fn execute_inst(&mut self, dec_inst: RiscvInstId, inst: InstT, step: u32) {
..
        match dec_inst {
            RiscvInstId::ADDI => {
                let rs1_data = self.read_reg(rs1);
                let imm_data = Self::extract_ifield(inst);
                let reg_data: Xlen64T = rs1_data.wrapping_add(imm_data);
                self.write_reg(rd, reg_data);
            }
...

レジスタからデータの読み出し、即値フィールドの切り出し、加算してレジスタへの書き込み、と非常に単純だ。 一点加算演算子 + ではなく.wrapping_add関数を使っているのは、テストケースでは64ビット同士の値がオーバフローしてしまうケースも含まれているからだ。 通常の+演算子を使うと、オーバフローが発生するとシミュレータが例外動作で終了してしまう。

RustでRISC-V命令セットシミュレータを作ろう (10. Rustでテストケースを作成する)

f:id:msyksphinz:20190224185310p:plain:w400

Cargoのテスト環境を使ってリグレッションテストを作る

Rustで作った命令セットシミュレータについて、テストの方法を考え直した。 現在はMakefileで管理をしているが、Cargoの機能を使ってテスト環境を構築した方が良さそうだ。

そこでRustのmain()を直接呼び出してテストをするのではなく、1枚ラッパーをかませることで、テストではラッパーを呼び出すことでmain()による引数解析の部分をスキップする。

f:id:msyksphinz:20200204214740p:plain
Cargoを使用したリグレッション環境
  • swimmer_rust/src/main.rs
fn main() -> Result<(), Box<std::error::Error>> {
    // 引数の解析がここで行われるので、バイナリファイルの指定などがmain()を呼び出すだけでは行えない。
    let args = parse_args();
...
    let ret = swimmer_rust::swimmer_rust_exec(args.bin_file[0].clone());

swimmer_rust_exec()は引数解析などが終わった状態で、バイナリファイルのみを渡す形に変換されているのでテストプラットフォームからはこの関数を呼び出すだけでよい。

  • swimmer_rust/tests/lib.rs
extern crate swimmer_rust;
#[test]
fn rv64ui_p_add () {
    assert_eq!(
        swimmer_rust::swimmer_rust_exec("riscv-tests/isa/rv64ui-p-add.bin".to_string()),
        1);
}

これをひたすら並べていく。

  • swimmer_rust/tests/lib.rs
extern crate swimmer_rust;

#[test]fn rv64ui_p_add    () { assert_eq!(swimmer_rust::swimmer_rust_exec("riscv-tests/isa/rv64ui-p-add.bin"     .to_string()), 1); }
#[test]fn rv64ui_p_addi   () { assert_eq!(swimmer_rust::swimmer_rust_exec("riscv-tests/isa/rv64ui-p-addi.bin"    .to_string()), 1); }
#[test]fn rv64ui_p_addiw  () { assert_eq!(swimmer_rust::swimmer_rust_exec("riscv-tests/isa/rv64ui-p-addiw.bin"   .to_string()), 1); }
#[test]fn rv64ui_p_addw   () { assert_eq!(swimmer_rust::swimmer_rust_exec("riscv-tests/isa/rv64ui-p-addw.bin"    .to_string()), 1); }
...

cargo testで、これらの定義したテストをすべて実行することができる。

$ cargo test
     Running target/debug/deps/lib-8b291594c607df5d

running 128 tests
test rv64ui_p_auipc ... ok
test rv64ui_p_andi ... ok
test rv64ui_p_addiw ... ok
...
test rv64um_v_remw ... ok

test result: ok. 128 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

RustでRISC-V命令セットシミュレータを作ろう (9. 命令のディスアセンブル機能を実装する)

f:id:msyksphinz:20190224185310p:plain:w400

Rustを使ったRISC-Vシミュレータの調整を行っている。デバッグ・トレース機能の調整を行った。

もうひとつの機能は、命令の機械語からニーモニックを出力する機能だ。 実はこの機能は必須の機能ではない。 spike-dasmというriscv-toolsを使えば、逆アセンブルの情報は命令の機械語から出力することができる。

$ echo "DASM(6ae6ba23)" | spike-dasm
sd      a4, 1716(a3)

これでも良いのだが、せっかくなのでシミュレータ自体にも逆アセンブルの機能を持たせておきたい。 これを実現するために、各命令のニーモニックの情報と、オペランドに関する情報をインプットした。

まずはC++で実装したRISC-Vシミュレータの情報をベースにして、命令のニーモニックオペランドの数、それぞれのオペランドの種類と機械語のどのフィールドから抽出すべきかの情報を格納する。

  • src/riscv_inst_mnemonic.rs
pub fn get_riscv_inst_mnemonic(dec_inst: RiscvInstId) -> String {
    match dec_inst {
...
        RiscvInstId::ADDI        => format!("addi       @,@,@"),
...

ニーモニックに関する情報だ。 命令IDに対して命令オペコードの情報、そしていくつのオペランドを使用するかについて記述している。 @の部分がオペランドになり、これは命令トレースを出力する段階で置き換える。

次に、オペランドの情報だが以下のような構造体を作っている。

  • src/riscv_inst_operand.rs
pub struct OperandInfo {
    pub m_size: u32,                       // オペランド情報のサイズ
    pub m_type_lst: [OperandType; 256],    // オペランドの種類
    pub m_msb_lst: [u32; 256],             // オペランド情報を切り出す命令のMSB
    pub m_lsb_lst: [u32; 256],             // オペランド情報を切り出す命令のLSB
    pub m_connect: [bool; 256],            // 次の情報と結合するか?
}
impl Tracer {
    pub fn format_operand(&mut self)
    {
...
        {
            let mut inst_operand = OperandInfo::new();

            // InstId_t::INST_ID_ADDI
            inst_operand.m_size = 3;
            inst_operand.m_type_lst[0] = OperandType::TypeXReg;
            inst_operand.m_msb_lst[0] = 11;
            inst_operand.m_lsb_lst[0] = 7;
            inst_operand.m_connect[0] = false;
            // ["r[11:7]", "r[19:15]", "h[31:20]"]
            inst_operand.m_type_lst[1] = OperandType::TypeXReg;
            inst_operand.m_msb_lst[1] = 19;
            inst_operand.m_lsb_lst[1] = 15;
            inst_operand.m_connect[1] = false;
            // ["r[11:7]", "r[19:15]", "h[31:20]"]
            inst_operand.m_type_lst[2] = OperandType::TypeHex;
            inst_operand.m_msb_lst[2] = 31;
            inst_operand.m_lsb_lst[2] = 20;
            inst_operand.m_connect[2] = false;
            // ["r[11:7]", "r[19:15]", "h[31:20]"]

            self.m_inst_operand_map.insert(RiscvInstId::ADDI, inst_operand);
        }

これらの情報は命令のディスアセンブル時に使用する。 上記のaddi @,@,@から、@に到達するとオペランドデータベースから当該命令のオペランド情報を取得し、文字列に変換して@の代わりに出力する。

        match self.m_dec_inst {
            Some(id) => {
                let inst_str = get_riscv_inst_mnemonic(id);
                let operand_info = self.m_inst_operand_map.get(&id);
                match operand_info {
                    Some(operand_info) => {
                        let mut at_index = 0;
                        let mut consume_idx = 0;
                        for c in inst_str.chars() {
                            if c == '@' {
                                let msb = operand_info.m_msb_lst[at_index];
                                let lsb = operand_info.m_lsb_lst[at_index];

                                let mask = (1 << (msb - lsb + 1)) - 1;
                                let opr_val = (self.m_inst_hex >> lsb) & mask;

                                match operand_info.m_type_lst[at_index] {
                                    OperandType::TypeXReg    => { print!("x{:02}", opr_val); consume_idx = consume_idx + 3; },
...
                                    _ => panic!("Unknown operand type {:?}", operand_info.m_type_lst[at_index] as u32),
                                }
                                at_index = at_index + 1;
                            } else {

これにより、命令トレース中にシミュレータ自身が逆アセンブルした情報を出力することができる。

ffc28293:addi       x05,x05,0xffc

最終的に、以下のようなトレースデータを出力することができるようになった。

        17:M:Bare:800028dc:00001697:auipc      x13,0x00001        :x13<=00000000800038dc
        18:M:Bare:800028e0:72468693:addi       x13,x13,0x724      :x13=>00000000800038dc x13<=0000000080004000
        19:M:Bare:800028e4:00002717:auipc      x14,0x00002        :x14<=00000000800048e4
        20:M:Bare:800028e8:71c70713:addi       x14,x14,0x71c      :x14=>00000000800048e4 x14<=0000000080005000
        21:M:Bare:800028ec:00c6d693:srli       x13,x13,0x0c       :x13=>0000000080004000 x13<=0000000000080004
        22:M:Bare:800028f0:00c75713:srli       x14,x14,0x0c       :x14=>0000000080005000 x14<=0000000000080005
        23:M:Bare:800028f4:00003797:auipc      x15,0x00003        :x15<=00000000800058f4
        24:M:Bare:800028f8:70c78793:addi       x15,x15,0x70c      :x15=>00000000800058f4 x15<=0000000080006000
        25:M:Bare:800028fc:00a69693:slli       x13,x13,0x0a       :x13=>0000000000080004 x13<=0000000020001000

RustでRISC-V命令セットシミュレータを作ろう (8. デバッグ・トレース機能の整理)

f:id:msyksphinz:20190224185310p:plain:w400

Rustを使ったRISC-Vシミュレータの調整を行っている。デバッグ・トレース機能の調整を行った。

ISSを使ってテストプログラムをデバッグする場合、いくつかの情報を出力してプログラムの動作を監視する必要がある。例えば、ISSからは以下の情報が出てくると役に立ちそうだ。

  • 実行中のプログラムカウンタ
  • 実行する命令の機械語情報
  • 実行する命令のアセンブリ
  • 実行する命令の動作
    • レジスタの読み込み・書き込み情報
    • メモリアクセス情報

これらの情報を標準出力に出して、デバッグに役立てていく。

トレースクラスの構成

まずはCPUを表現するstructに、トレースを格納するためのstructを追加する。

  • src/riscv64_core.rs
pub struct Riscv64Env {
    // m_bitmode: RiscvBitMode,
    pub m_pc: AddrT,
    m_previous_pc: AddrT,
    m_regs: [Xlen64T; 32],
...

    pub m_trace: Tracer,
...

Tracer構造体は、命令実行後の各種情報を格納するための構造体だ。

  • src/riscv_tracer.rs
pub struct Tracer {
    pub m_priv: PrivMode,          // 現在のマシン実行モード
    pub m_vmmode: VMMode,          // 現在の仮想アドレスモード
    pub m_executed_pc: AddrT,      // 命令フェッチアドレス(仮想アドレス)
    pub m_executed_phypc: AddrT,   // 命令フェッチアドレス(物理アドレス)
    pub m_inst_hex: InstT,         // 実行命令の機械語
    pub m_dec_inst: Option<RiscvInstId>, // デコード後の命令
    pub m_step: u32,               // 命令の実行ステップ数
    pub m_trace_info: Vec<TraceInfo>,  // 命令の動作トレース

    pub m_inst_operand_map: HashMap<RiscvInstId, OperandInfo>, // 命令トレースの情報を格納するHash
}

この中で注目しなければならないのはm_trace_infoメンバ変数だ。 これはTraceInfoという、命令の動作トレースを保持する情報を格納している。 このメンバ変数はベクトルであり複数の情報を格納することができる。

pub struct TraceInfo {
    pub m_trace_type: TraceType,
    pub m_trace_size: u32,
    pub m_trace_addr: AddrT,
    pub m_trace_value: Xlen64T,
    pub m_trace_memresult: MemResult,
}
f:id:msyksphinz:20200202220333p:plain:w400
RISC-Vシミュレータ。トレース機能の構成。

命令の動作の度に、この動作をトレースに追加していく。

  • src/riscv64_insts.rs
            RiscvInstId::ADDI => {
                let rs1_data = self.read_reg(rs1);
                let imm_data = Self::extract_ifield(inst);
                let reg_data: Xlen64T = rs1_data.wrapping_add(imm_data);
                self.write_reg(rd, reg_data);
            }
  • src/riscv64_core.rs
    fn read_reg(&mut self, reg_addr: RegAddrT) -> Xlen64T {
...
        let mut read_reg_trace = TraceInfo::new();
        read_reg_trace.m_trace_type = TraceType::XRegRead;
        read_reg_trace.m_trace_addr = reg_addr as AddrT;
        read_reg_trace.m_trace_value = ret_val;
        read_reg_trace.m_trace_memresult = MemResult::NoExcept;
        // トレース構造体に情報をpushする。
        self.m_trace.m_trace_info.push(read_reg_trace);
...
    }
    fn write_reg(&mut self, reg_addr: RegAddrT, data: Xlen64T) {
        if reg_addr != 0 {
            let mut write_reg_trace = TraceInfo::new();

            write_reg_trace.m_trace_type = TraceType::XRegWrite;
            write_reg_trace.m_trace_addr = reg_addr as AddrT;
            write_reg_trace.m_trace_value = data;
            write_reg_trace.m_trace_memresult = MemResult::NoExcept;
            // トレース構造体に情報をpushする。
            self.m_trace.m_trace_info.push(write_reg_trace);
        }
   }

命令の実行後に、このトレース情報を出力していく。 下記の例ではトレースの種類がレジスタ書き込みの場合、[整数レジスタアドレス]<=[書き込みデータ]のトレースデータを出力している。

  • src/riscv_tracer.rs
impl RiscvTracer for Tracer {
    ...
    fn print_trace(&mut self) {
        for trace_idx in 0..self.m_trace_info.len() {
            match self.m_trace_info[trace_idx].m_trace_type {
                TraceType::XRegWrite => {
                    print!(
                        "x{:02}<={:016x} ",
                        self.m_trace_info[trace_idx].m_trace_addr,
                        self.m_trace_info[trace_idx].m_trace_value
                    );
                }
             ...
x14=>00000000200000cf x13=>0000000080005944 (80005ff8)<=200000cf
--------------------- --------------------- --------------------
レジスタ読み込みトレース  レジスタ書き込みトレース  メモリ書き込みトレース

Diplomacyを使ってOCPバスを作成する(6. Delayerの実装)

Diplomacyを使ってOCPバスを作成している。OCP SRAMの動作を確認しつつ、OCPのDelayerを実装しようと思う。DelayerというのはバスのReady信号を遅らせて、信号を遅らせてバスの問題を検出するための機能だ。

TileLinkのDelayerはすでに実装されている。これはLFSRを使ってReady信号を遅らせる機構となっている。

  • chisel-hw/src/main/scala/ocp/Delayer.scala
    def feed[T <: Data](sink: DecoupledIO[T], source: DecoupledIO[T], noise: T) {
      val allow = UInt((q * 65535.0).toInt) <= LFSRNoiseMaker(16, source.valid)
      sink.valid := source.valid && allow
      source.ready := sink.ready && allow
      sink.bits := source.bits
      when (!sink.valid) { sink.bits := noise }
    }

このfeed関数を使ってOCPの3つのチャネルを接続する。Valid信号が立ち上がっていないときは、Noise信号を挿入してバスをわざと乱すことでデバッグを行う。

    (node.in zip node.out) foreach { case ((in, _), (out, _)) =>
      val cmd_noise = Wire(in.cmd.bits)
      cmd_noise.mcmd    := LFSRNoiseMaker(3)
      cmd_noise.tagId   := LFSRNoiseMaker(cmd_noise.params.tagidBits)
      cmd_noise.address := LFSRNoiseMaker(cmd_noise.params.addressBits)

      val data_noise = Wire(in.data.bits)
      data_noise.tagId  := LFSRNoiseMaker(data_noise.params.tagidBits)
      data_noise.data   := LFSRNoiseMaker(data_noise.params.dataBits)

      val resp_noise = Wire(out.resp.bits)
      resp_noise.tagId := LFSRNoiseMaker(resp_noise.params.tagidBits)
      resp_noise.data  := LFSRNoiseMaker(resp_noise.params.dataBits)

      feed(out.cmd,  in.cmd,   cmd_noise)
      feed(out.data, in.data,  data_noise)
      feed(in.resp,  out.resp, resp_noise)
    }

これを使ってテストコードを作ってみよう。これまで通りMasterとSlaveの間にDelayerを挿入して、シミュレーションを実行してみる。

  val pusher = LazyModule(new OCPPatternPusher("pat1", Seq(
    new WriteReqPattern(0x100, 0x2),
    new WriteDataPattern(0x012345678L),
    new WriteReqPattern(0x104, 0x2),
    new WriteDataPattern(0x0abcdef01L),
    new WriteReqPattern(0x108, 0x2),
    new WriteDataPattern(0x0deadbeefL),
    new WriteReqPattern(0x10c, 0x2),
    new WriteDataPattern(0x087654321L),
    new ReadExpectPattern(0x100, 0x2, 0x012345678L),
    new ReadExpectPattern(0x104, 0x2, 0x0abcdef01L),
    new ReadExpectPattern(0x108, 0x2, 0x0deadbeefL),
    new ReadExpectPattern(0x10c, 0x2, 0x087654321L)
  )))
  val ram = LazyModule(new OCPRAM(AddressSet(0x0, 0x3ff)))

  ram.node := OCPDelayer(0.01) := pusher.node
f:id:msyksphinz:20200205001425p:plain:w300
Delayerを挿入したOCP Diplomacy回路図

シミュレーションを実行して波形を表示してみた。上手く動作しているようだ。

make tilelink CONFIG=OCPUnitDelayerTestConfig
./tilelink
Started UnitTest OCPUnitDelayerTest
Count = 1000
Resp Fired : same as expected. 12345678
Resp Fired : same as expected. abcdef01
Count = 2000
Resp Fired : same as expected. deadbeef
Resp Fired : same as expected. 87654321
f:id:msyksphinz:20200205001502p:plain
Delayerを挿入したOCP回路 シミュレーション結果