FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages

RustのパーサジェネレータLALRPOPに入門する(2. 電卓を作る)

パーサジェネレータの続き。

電卓を作る

lalrpop.github.io

次はパーサを使って電卓を作ってみる。電卓は四則演算を行うことができ、*, /の優先度が+, -よりも高い。calculator3.lalrpopは以下のようになる。

  • calculator3.lalrpop
use std::str::FromStr;

grammar;

pub Expr: i32 = {
    <l:Expr> "+" <r:Factor> => l + r,
    <l:Expr> "-" <r:Factor> => l - r,
    Factor,
};

Factor: i32 = {
    <l:Factor> "*" <r:Term> => l * r,
    <l:Factor> "/" <r:Term> => l / r,
    Term,
};

Term: i32 = {
    Num,
    "(" <Expr> ")",
};

Num: i32 = {
    r"[0-9]+" => i32::from_str(<>).unwrap(),
};

ここでは

  • Expr
  • Factor
  • Term
  • Num

の4つを定義している。それぞれ型はi32であり、即時計算を行っている。

  • rust/calculator3/src/main.rs
#[test]
fn calculator3() {
    let v = calculator3::ExprParser::new().parse("((22 * 44) + 66)").unwrap();
    assert!(v == 1034);
}

ASTを作る

次は四則演算を作るASTを作り上げる。文法自体はRustのマクロを使って実装されているので、RustのEnumなどを使ってASTを作り上げることができる。

  • rust/calculator4/src/ast.rs
pub enum Expr {
    Number(i32),
    Op(Box<Expr>, Opcode, Box<Expr>),
}

#[derive(Copy, Clone)]
pub enum Opcode {
    Mul,
    Div,
    Add,
    Sub,
}
  • rust/calculator4/src/calculator4.lalrpop
use std::str::FromStr;
use crate::ast::{Expr, Opcode}; // (0)

grammar;

pub Expr: Box<Expr> = { // (1)
    Expr ExprOp Factor => Box::new(Expr::Op(<>)), // (2)
    Factor,
};

ExprOp: Opcode = { // (3)
    "+" => Opcode::Add,
    "-" => Opcode::Sub,
};

Factor: Box<Expr> = {
    Factor FactorOp Term => Box::new(Expr::Op(<>)),
    Term,
};

FactorOp: Opcode = {
    "*" => Opcode::Mul,
    "/" => Opcode::Div,
};

Term: Box<Expr> = {
    Num => Box::new(Expr::Number(<>)), // (4)
    "(" <Expr> ")"
};

Num: i32 = {
    r"[0-9]+" => i32::from_str(<>).unwrap()
};
  • rust/calculator4/src/main.rs
#[test]
fn calculator4() {
    let expr = calculator4::ExprParser::new()
        .parse("22 * 44 + 66")
        .unwrap();
    assert_eq!(&format!("{:?}", expr), "((22 * 44) + 66)");
}

上記のExprParserは最終的に文字列を出力している。これを扱うのはast.rsに書かれたfmt(&self, fmt: &mut Formatter)を使っている。

  • rust/calculator4/src/ast.rs
impl Debug for Expr {
    fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> {
        use self::Expr::*;
        match *self {
            Number(n) => write!(fmt, "{:?}", n),
            Op(ref l, op, ref r) => write!(fmt, "({:?} {:?} {:?})", l, op, r),
        }
    }
}


impl Debug for Opcode {
    fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> {
        use self::Opcode::*;
        match *self {
            Mul => write!(fmt, "*"),
            Div => write!(fmt, "/"),
            Add => write!(fmt, "+"),
            Sub => write!(fmt, "-"),
        }
    }
}

RustのパーサジェネレータLALRPOPに入門する(1. チュートリアルを読む)

パーサジェネレータについて調べていると、以下のようなGitHubリポジトリを見つけた。

LALRPOPという、Rust向けのパーサジェネレータらしい。結構☆が付いているのでかなり使い物になりそうな気がする。試してみよう。

github.com

チュートリアルは以下のページから参照できる。

lalrpop.github.io

基本的な考え方

LALRPOPでは、パーサの核になる記述をxxx.lalrpopというファイルに格納する。これはふたを開けてみればRustのソースコードそのもので、例えば以下のようなものを記述する。

  • calculator1.lalrpop
use std::str::FromStr;

grammar;

pub Term: i32 = {
    <n:Num> => n,
    "(" <t:Term> ")" => t,
};

Num: i32 = <s:r"[0-9]+"> => i32::from_str(s).unwrap();

次に、プロジェクト内のCargo.tomlを編集しよう。そのままチュートリアルに則って以下のような記述に変える。

[package]
name = "calculator"
version = "0.1.0"
authors = ["msyksphinz <msyksphinz.dev@gmail.com>"]
edition = "2018"

[build-dependencies]
lalrpop = "0.17.2"

[dependencies]
lalrpop-util = "0.17.2"
regex = "0.2.1"

さらにbuild.rsにも以下を追加することで準備完了だ。

extern crate lalrpop;

fn main() {
    lalrpop::process_root().unwrap();
}

LALRPOP文法の読み方

それではさっそく上記のcalculator1.lalrpopの読み方を勉強する。上記のcalculator1.lalrpopは2つの文法を定義している。

  • Term : i32を返す。Numもしくは(Term)となる。
  • Num : i32を返す。10進数の数値を示す。

LALRPOPの文法では、<変数名:文法名>とすることでその変数名を後で参照することができるようになっている。

この文法は簡単なのですぐに理解できたが、例えば以下のような文法を受け入れることができる。

  • 22
  • (22)
  • ((22))
  • (((((22)))))

テストする

さて、main.rsに以下のテストを追加して実行してみる。

#[macro_use] extern crate lalrpop_util;

lalrpop_mod!(pub calculator1); // synthesized by LALRPOP

#[test]
fn calculator1() {
    assert!(calculator1::TermParser::new().parse("22").is_ok());
    assert!(calculator1::TermParser::new().parse("(22)").is_ok());
    assert!(calculator1::TermParser::new().parse("((((22))))").is_ok());
    assert!(calculator1::TermParser::new().parse("((22)").is_err());
}

TermParserというのは、Term文法から始まるParserであることを示す。つまりNum文法をテストしたい場合はNumParserとすれば良い。

$ cargo test
    Finished dev [unoptimized + debuginfo] target(s) in 2.02s
     Running target/debug/deps/calculator-06b0a7843f952790

running 1 test
test calculator1 ... ok

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

RustでRISC-V命令セットシミュレータを作ろう (13. RV32仮想アドレスモードの実装)

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

Rustで作る自作命令セットシミュレータの続き。RV32のテストは動くようになってきたので、次はRV32の仮想アドレスモードの対応について考える。 これも昔作ったC++RISC-Vシミュレータですでに対応しているので簡単なのだが、いくつかRV32とRV64で切り替えなければならない点がある。

まずはRV32とRV64で動作する仮想アドレスモードが違うということ。RV32ではMBare(物理アドレスモード)とSv32が使用できるあが、RV64ではMBareとSv39, Sv48, Sv57, Sv64が使用できる(Sv57, Sv64はまだ仕様としては未定義)。

f:id:msyksphinz:20200216121758p:plain
RISC-Vの仮想アドレスモード・RV32とRV64でサポートする仮想アドレスモードの違い

さらにSATPシステムレジスタの定義も少し異なる。これもRV64とRV32で区別しなければならない。

f:id:msyksphinz:20200216121909p:plain
SATPレジスタのRV32とRV64での仕様の違い。ビットフィールドが異なる。

したがって、まずはページテーブルウォークの際のSATP参照の実装を変更する必要がある。

  • swimmer_rust/src/riscv_mmu.rs
        let pte_base: Addr64T = match self.m_xlen {
            32 => Self::extract_bit_field(satp, 21, 0) as Addr64T,
            64 => Self::extract_bit_field(satp, 43, 0) as Addr64T,
            _ => panic!("Internal Error: XLEN should either 32 or 64"),
        };
  • swimmer_rust/src/riscv_core.rs
    fn get_vm_mode(&mut self) -> VMMode {
        let satp_val = self.m_csr.csrrs(CsrAddr::Satp, 0) as Xlen64T; // SATP
        let mode = match self.m_xlen {
            32 => Self::extract_bit_field(satp_val, 31, 31),
            64 => Self::extract_bit_field(satp_val, 63, 60),
            _ => panic!("Internal Error: XLEN should either 32 or 64"),
        };

XLENの値に応じて変更を行う。ビットフィールドの定義がRV32とRV64で異なっているためだ。

それ以外の所でケアしなければならないことはあまりない。Sv32, Sv39でビットフィールドの定義を変えつつ、実装を追加する。

  • swimmer_rust/src/riscv_mmu.rs
        if self.get_vm_mode() == VMMode::Sv39
            && (priv_mode == PrivMode::Supervisor || priv_mode == PrivMode::User)
        {
            let ppn_idx: Vec<u8> = vec![12, 21, 30];
            let pte_len: Vec<u8> = vec![9, 9, 26];
            let pte_idx: Vec<u8> = vec![10, 19, 28];
            let vpn_len: Vec<u8> = vec![9, 9, 9];
            let vpn_idx: Vec<u8> = vec![12, 21, 30];
            let pagesize: u32 = 4096; // num::pow(2, 12);
            let ptesize: u32 = 8;

            return self.walk_page_table(
                vaddr, acc_type, 3, ppn_idx, pte_len, pte_idx, vpn_len, vpn_idx, pagesize, ptesize,
            );
        } else if self.get_vm_mode() == VMMode::Sv32
            && (priv_mode == PrivMode::Supervisor || priv_mode == PrivMode::User)
        {
            let ppn_idx: Vec<u8> = vec![12, 22];
            let pte_len: Vec<u8> = vec![10, 12];
            let pte_idx: Vec<u8> = vec![10, 20];
            let vpn_len: Vec<u8> = vec![10, 10];
            let vpn_idx: Vec<u8> = vec![12, 22];
            let pagesize: u32 = 4096; // num::pow(2, 12);
            let ptesize: u32 = 4;

            return self.walk_page_table(
                vaddr, acc_type, 2, ppn_idx, pte_len, pte_idx, vpn_len, vpn_idx, pagesize, ptesize,
            );
        } else {
            return Ok(vaddr);
        }

動作確認を行う。

cargo run -- --arch rv32 riscv-tests/isa/rv32ui-v-add.bin | tee rv32ui-v-add.log
     20650:S:Sv32:ffffffffffc02234:0046a783:lw         x15,0x004(x13)     :x13=>ffffffffffc01000 (ffffffffffc01004)=>00000000 x15<=0000000000000000
     20651:S:Sv32:ffffffffffc02238:00050813:addi       x16,x10,0x000      :x10=>0000000000000001 x16<=0000000000000001
     20652:S:Sv32:ffffffffffc0223c:41f55893:srai       x17,x10,0x1f       :x10=>0000000000000001 x17<=0000000000000000
     20653:S:Sv32:ffffffffffc02240:00f76733:or         x14,x14,x15        :x14=>0000000000000000 x15=>0000000000000000 x14<=0000000000000000
     20654:S:Sv32:ffffffffffc02244:02070663:beq        x14,x00,0x2c        :x14=>0000000000000000 x00=>0000000000000000
     20655:S:Sv32:ffffffffffc02270:0106a023:sw         x16,0x0000000(x13) :x16=>0000000000000001 x13=>ffffffffffc01000 (ffffffffffc01000)<=00000001
PASS : riscv-tests/isa/rv32ui-v-add.bin

Passできたようだ。リグレッションテストで確認していこう。

RustでRISC-V命令セットシミュレータを作ろう (12. RV32機能の実装)

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

Rustで自作命令セットシミュレータを作っているが、RV64のテストパタンはある程度Passできるようになっている。

RV32についてだが、これまではRV32とRV64を別のクラスとして実装していた。しかしどうもこれでは冗長なコードがあってたまらないので、統一してRV64のクラスのみでRV32もサポートすることにした。

RV32をサポートするために、まずはRV64のRISC-Vコアを管理しているstructにXLENのパラメータを追加する。

  • swimmer_rust/src/riscv_core.rs
pub struct Riscv64Env {
    pub m_xlen: i32,
...
    pub fn new(xlen: i32) -> Riscv64Env {
        Riscv64Env {
            m_xlen: xlen,
            m_pc: DRAM_BASE as Addr64T,
...

これに応じて、各命令の実装を変えていく。例えば、ADD命令やSUB命令ならば演算の最後に符号によるXLENビット長への丸め込みを行っておく。

  • swimmer_rust/src/riscv_insts.rs
            RiscvInstId::ADD => {
                let rs1_data = self.read_reg(rs1);
                let rs2_data = self.read_reg(rs2);
                let reg_data: Xlen64T = self.sext_xlen(rs1_data.wrapping_add(rs2_data));
                self.write_reg(rd, reg_data);
            }
            RiscvInstId::SUB => {
                let rs1_data = self.read_reg(rs1);
                let rs2_data = self.read_reg(rs2);
                let reg_data: Xlen64T = self.sext_xlen(rs1_data.wrapping_sub(rs2_data));
                self.write_reg(rd, reg_data);
            }
...

このsext_xlen()だが、Spikeの実装をかなりパクってしまった。

  • swimmer_rust/src/riscv_core.rs
    pub fn sext_xlen(&mut self, hex: Xlen64T) -> Xlen64T {
        return (hex << (64-self.m_xlen)) >> (64-self.m_xlen)
    }

    pub fn uext_xlen(&mut self, hex: Xlen64T) -> UXlen64T {
        return ((hex as UXlen64T) << (64-self.m_xlen)) >> (64-self.m_xlen)
    }

とりあえずテストを動かしてみる。まずはPhysical Addressモードの場合でテストを行う。

cargo run -- --arch rv32 riscv-tests/isa/rv32ui-p-add.bin | tee rv32ui-p-add.log
       466:M:Bare:ffffffff8000000c:03ff0a63:beq        x30,x31,0x34        :x30=>000000000000000b x31=>0000000000000008
       467:M:Bare:ffffffff80000010:00900f93:addi       x31,x00,0x009      :x00=>0000000000000000 x31<=0000000000000009
       468:M:Bare:ffffffff80000014:03ff0663:beq        x30,x31,0x2c        :x30=>000000000000000b x31=>0000000000000009
       469:M:Bare:ffffffff80000018:00b00f93:addi       x31,x00,0x00b      :x00=>0000000000000000 x31<=000000000000000b
       470:M:Bare:ffffffff8000001c:03ff0263:beq        x30,x31,0x24        :x30=>000000000000000b x31=>000000000000000b
       471:M:Bare:ffffffff80000040:00001f17:auipc      x30,0x00001        :x30<=ffffffff80001040
       472:M:Bare:ffffffff80000044:fc3f2023:sw         x03,0x0000fc0(x30) :x03=>0000000000000001 x30=>ffffffff80001040 (ffffffff80001000)<=00000001
PASS : riscv-tests/isa/rv32ui-p-add.bin

動作自体は問題ないようだ。あとはリグレッションテストで確認していく。

Diplomacyを使ってOCPバスを作成する (9. Xbarの作成)

Diplomacyを使ってOCPバスを作成するプロジェクト、次はクロスバーを作成してみよう。クロスバーは2つ以上のスレーブノードを接続してバスをスプリットする。

  val ram0 = LazyModule(new OCPRAM(AddressSet(0x000, 0x3ff)))
  val ram1 = LazyModule(new OCPRAM(AddressSet(0x400, 0x3ff)))
  val xbar = LazyModule(new OCPXbar())

  xbar.node := OCPDelayer(0.01) := pusher.node
  ram0.node := xbar.node
  ram1.node := xbar.node

上記のように、クロスバーの実装を代入演算子の左側に置くことでソースノードを指定する。一方でクロスバーの実装を代入演算子の右側に置くことでスレーブノードを指定している。このスレーブノードはいくつでも指定することができる。

この実装はどのように実現されているのだろうか?中身を見てみる。

  • chisel-hw/src/main/scala/ocp/Xbar.scala
class OCPXbar(policy: OCPArbiter.Policy = OCPArbiter.roundRobin)(implicit p: Parameters) extends LazyModule
{
  val node = OCPNexusNode(
    clientFn  = { seq =>
...
    }
    managerFn = { seq =>
...
    }

ノードの種類としてはネクサスノードを使用している。ネクサスノードの解説はこうだ。

https://chipyard.readthedocs.io/en/latest/TileLink-Diplomacy-Reference/NodeTypes.html#nexus-node

The nexus node is similar to the adapter node in that it has a different output interface than input interface. But it can also have a different number of inputs than it does outputs. This node type is mainly used by the TLXbar widget, which provides a TileLink crossbar generator. You will also likely not need to define this node type manually, but its invocation is as follows.

  • chisel-hw/src/main/scala/ocp/Nodes.scala
case class OCPNexusNode(
  clientFn:        Seq[OCPClientPortParameters]  => OCPClientPortParameters,
  managerFn:       Seq[OCPManagerPortParameters] => OCPManagerPortParameters)(
  implicit valName: ValName)
  extends NexusNode(OCPImp)(clientFn, managerFn) with OCPFormatNode

ノードの接続を行うのは以下の実装だ。複数の入力を1つのベクトルに代入する。

  • chisel-hw/src/main/scala/ocp/Xbar.scala
for (i <- 0 until in.size) {
  val r = inputIdRanges(i)

  if (connectAIO(i).exists(x=>x)) {
    in(i).cmd <> io_in(i).cmd
    in(i).cmd.bits.tagId := io_in(i).cmd.bits.tagId | UInt(r.start)
  } else {
    in(i).cmd.mcmd := UInt(0)
    io_in(i).cmd.ready := Bool(true)
  }

  if (connectCIO(i).exists(x=>x)) {
    in(i).data <> io_in(i).data
    in(i).data.bits.tagId := io_in(i).data.bits.tagId | UInt(r.start)
  } else {
    in(i).data.valid := Bool(false)
    io_in(i).data.ready := Bool(true)
  }

  if (connectDIO(i).exists(x=>x)) {
    in(i).resp <> io_in(i).resp
  } else {
    in(i).resp.valid := Bool(false)
    io_in(i).resp.ready := Bool(true)
  }
}

ノードの出力先は以下のようにしている。

  • chisel-hw/src/main/scala/ocp/Xbar.scala
val out = Wire(Vec(io_out.size, OCPBundle(wide_bundle)))
for (o <- 0 until out.size) {
  val r = outputIdRanges(o)

  if (connectAOI(o).exists(x=>x)) {
    io_out(o).cmd <> out(o).cmd
  } else {
    out(o).cmd.ready := Bool(true)
    io_out(o).cmd.mcmd := UInt(0)
  }

  if (connectCOI(o).exists(x=>x)) {
    io_out(o).data <> out(o).data
  } else {
    out(o).data.ready := Bool(true)
    io_out(o).data.valid := Bool(false)
  }

  if (connectDOI(o).exists(x=>x)) {
    out(o).resp <> io_out(o).resp
    out(o).resp.bits.tagId := io_out(o).resp.bits.tagId | UInt(r.start)
  } else {
    out(o).resp.valid := Bool(false)
    io_out(o).resp.ready := Bool(true)
  }
}

OCPアービタの接続を行う。

  • chisel-hw/src/main/scala/ocp/Xbar.scala
// Arbitrate amongst the sources
for (o <- 0 until out.size) {
  OCPArbiter(policy, out(o).cmd,  filter(beatsCmdI  zip portsCmdOI(o),  connectAOI(o)):_*)
  OCPArbiter(policy, out(o).data, filter(beatsDataI zip portsDataOI(o), connectCOI(o)):_*)
}

for (i <- 0 until in.size) {
  OCPArbiter(policy, in(i).resp, filter(beatsRespO zip portsRespIO(i), connectDIO(i)):_*)
}

Coqをベースとしたハードウェア表現および生成プラットフォーム"Kami"を試す

MITのプログラム言語を研究しているグループの検証プラットフォーム「Kami」の論文を読んでいる。

KamiとはCoqをベースとしたDSLで、ハードウェアを表現するための言語およびそのプラットフォーム。KamiはCoqのハードウェア記述から検証、ハードウェア生成までを行うことを目的とする。

論文は以下に掲載されている。30ページもあり長い。

とりあえず動かしてみる。以下のリポジトリを使おう。

github.com

# Revision : ed1d6cd
$ git clone git@github.com:sifive/Kami.git

ツールチェインを用意する。まずはCoqの最新バージョンをインストールしよう。KamiはCoqの比較的最新バージョンが必要となる。

$ cd /tmp
$ curl -L https://github.com/coq/coq/archive/V8.11.0.tar.gz | tar xz
$ cd coq-8.11.0
$ ./configure
$ make -j$(nproc)
$ sudo make install
$ coqtop --version
The Coq Proof Assistant, version 8.11.0 (February 2020)
compiled on Feb 7 2020 2:49:57 with OCaml 4.05.0

次にライブラリをインストールする。以下のCoqの追加ライブラリが必要だ。

$ git clone git@github.com:mit-plv/bbv.git
$ cd bbv
$ make
$ git clone git@github.com:tchajed/coq-record-update.git
$ cd coq-record-update
$ make

最終的に以下のようなディレクトリ構造になっていればOK。

.
|-- Kami
|-- bbv
`-- coq-record-update

さて、とりあえずMakeを実行してみよう。

$ make -j8
coq_makefile -f _CoqProject ./All.v ./AllDefn.v ./AllNotations.v ./Compiler/Compiler.v ./Compiler/CompilerDoubleWrites.v ./Compiler/CompilerProps.v ./Compiler/CompilerSimple.v ./Compiler/CompilerSimpleProps.v ./Compiler/CompilerSimpleSem.v ./Compiler/Rtl.v ./Compiler/Test.v ./Compiler/UnverifiedIncompleteCompiler.v ./Extraction.v ./Guard.v ./Lib/EclecticLib.v ./Lib/Fold.v ./Lib/HexNotation.v ./Lib/HexNotationWord.v ./Lib/VectorFacts.v ./Lib/Word.v ./Lib/WordProperties.v ./LibStruct.v ./Notations.v ./NotationsTest.v ./Notations_rewrites.v ./PPlusProperties.v ./PProperties.v ./Properties.v ./Simulator/CoqSim/Eval.v ./Simulator/CoqSim/HaskellTypes.v ./Simulator/CoqSim/Misc.v ./Simulator/CoqSim/RegisterFile.v ./Simulator/CoqSim/SimTypes.v ./Simulator/CoqSim/Simulator.v ./Simulator/CoqSim/TransparentProofs.v ./Simulator/NativeTest.v ./StateMonad.v ./Syntax.v ./SyntaxDoubleWrites.v ./Tactics.v ./Tutorial/ExtractEx.v ./Tutorial/GallinaActionEx.v ./Tutorial/PhoasEx.v ./Tutorial/SyntaxEx.v ./Tutorial/TacticsEx.v ./Utila.v ./WfMod_Helper.v -o Makefile.coq.all
Warning: no common logical root
Warning: in such case INSTALLDEFAULTROOT must be defined
Warning: the install-doc target is going to install files
Warning: in orphan_Kami_bbv_RecordUpdate
make -f Makefile.coq.all
make[1]: Entering directory '/home/msyksphinz/work/riscv/kami/Kami'
COQDEP VFILES
COQC Lib/Fold.v
COQC Lib/EclecticLib.v
COQC Lib/VectorFacts.v
COQC StateMonad.v
COQC Lib/HexNotation.v
COQC Lib/Word.v
COQC Tutorial/PhoasEx.v
...
implEq cannot be defined because the projections counterImpl, isSending were
not defined. [cannot-define-projection,records]
File "./Tutorial/TacticsEx.v", line 46, characters 2-641:
Warning:
specEq cannot be defined because the projections isSending, counterImpl,
counterImpl were not defined. [cannot-define-projection,records]
COQC Tutorial/ExtractEx.v
COQC Tutorial/GallinaActionEx.v
COQC Tutorial/PhoasEx.v
     = "let xO := (true || false) in let xSO := true in (xO && xSO)"
     : string
COQC Tutorial/SyntaxEx.v
COQC WfMod_Helper.v

とりあえずエラー無くコンパイルが完了した。内部を解析していこう。

Diplomacyを使ってOCPバスを作成する (8. PatternPusherの改良)

前回はDiplomacyを使ったOCPバスの作成で、Bundleを作成してValid/Readyの信号をCmd/Readyに変換した。次はこれに基づいてテスト環境を構築する。テスト環境ではまずはPatternPusherを使ってコマンドを挿入する。まずはPatternPusherを改造しなければ始まらない。

PatternPusherでは、これまでValid信号の判定に使っていた信号を、Cmdを使う方式に切り替える。

  • chisel-hw/src/main/scala/ocp/PatternPusher.scala
val (plegal, pcmds,   pbits) = pattern.map(_.bits  (edgeOut).getOrElse(edgeOut.NopCmd ())).unzip3
val (wlegal, wvalids, wbits) = pattern.map(_.wrdata(edgeOut).getOrElse(edgeOut.NopData())).unzip3

cmd.mcmd   := Mux(pcmds(step) =/= OCPMessages.NopCmd && io.run && !flight && ready && !end, pcmds(step), OCPMessages.NopCmd)
cmd.bits   := pbits(step)

pcmd(step)の作り方だが、パタンのビットフィールドから構築している。コマンドは以下の4種類を用意した。

  • WriteReqPattern : 書き込み要求。アドレスとデータサイズを指定する。データは含まれない。Cmdチャネルを使用する。
case class WriteReqPattern(address: BigInt, size: Int) extends Pattern
{
  def bits (edge: OCPEdgeOut) = Some(edge.Write(0.U, address.U))
  def wrdata (edge: OCPEdgeOut) = None
}
  • WriteDataPattern : 書き込みデータ要求。書き込みデータを指定する。Dataチャネルを使用する。
case class WriteDataPattern(data: BigInt) extends Pattern
{
  override def address = 0
  override def size = 0
  def bits (edge: OCPEdgeOut) = None
  def wrdata (edge: OCPEdgeOut) = Some(edge.WriteData(0.U, data.U))
}
  • ReadPattern : 読み込み要求。コマンドチャネルを指定する。
case class ReadPattern(address: BigInt, size: Int) extends Pattern
{
  def bits(edge: OCPEdgeOut) = Some(edge.Read(0.U, address.U))
  def wrdata (edge: OCPEdgeOut) = None
}
  • ReadExpectPattern : 読み込み要求。コマンドチャネルとレスポンスチャネルを使用する。データの比較を行う。
case class ReadExpectPattern(address: BigInt, size: Int, data: BigInt) extends Pattern
{
  def bits(edge: OCPEdgeOut) = Some(edge.Read(0.U, address.U))
  def wrdata (edge: OCPEdgeOut) = None
  override def dataIn = Some(data)
}

このedge.Write, edge.WriteData, edge.Readの作り方だが、これはEdges.scalaで記述している。

  • chisel-hw/src/main/scala/ocp/Edges.scala
class OCPEdgeOut(
  client:  OCPClientPortParameters,
  manager: OCPManagerPortParameters,
  params:  Parameters,
  sourceInfo: SourceInfo)
  extends OCPEdge(client, manager, params, sourceInfo)
{
  // Nop
  def NopCmd(): (Bool, UInt, OCPBundleCmd) = {
    val cmd = Wire(new OCPBundleCmd(bundle))
    cmd.tagId   := 0.U
    cmd.address := 0.U
    (true.B, OCPMessages.NopCmd, cmd)
  }

  // Accesses
  def Read(fromTagId: UInt, toAddress: UInt): (Bool, UInt, OCPBundleCmd) = {
    val cmd = Wire(new OCPBundleCmd(bundle))
    cmd.tagId  := fromTagId
    cmd.address := toAddress
    (true.B, OCPMessages.Read, cmd)
  }

  def Write(fromTagId: UInt, toAddress: UInt): (Bool, UInt, OCPBundleCmd) = {
    val cmd = Wire(new OCPBundleCmd(bundle))
    cmd.tagId   := fromTagId
    cmd.address := toAddress
    (true.B, OCPMessages.Write, cmd)
  }

  def NopData(): (Bool, Bool, OCPBundleData) = {
    val data = Wire(new OCPBundleData(bundle))
    data.tagId := 0.U
    data.data  := 0.U
    (true.B, false.B, data)
  }

  def WriteData(fromTagId: UInt, data: UInt): (Bool, Bool, OCPBundleData) = {
    val d = Wire(new OCPBundleData(bundle))
    d.tagId := fromTagId
    d.data  := data
    (true.B, true.B, d)
  }
}

コマンドチャネルを使用するメソッドの戻り値は、

  • Bool : 命令が有効か
  • UInt : コマンドを使用するか
  • OCPBundleCmd : コマンド発行に使用するビット群

データチャネルを使用するメソッドの戻り値は、

  • Bool : 命令が有効か
  • Bool : そのチャネルを本当に使用するか
  • OCPBundleData : データチャネルに使用するビット群

これに基づいてSRAMと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

実行結果は、ただしく動作しているようだ。

./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:20200208141659p:plain
図. OCPでコマンドを発行し、SRAMから読みだした様子。