Vengineerの戯言

人生は短いけど、長いです。人生を楽しみましょう!

GoogleのCFU Playgroundの cfu を作るには?

@Vengineerの戯言 : Twitter SystemVerilogの世界へようこそすべては、SystemC v0.9公開から始まった 

はじめに

今回は、CFU をどのように 記述すればいいかを見ていきます。基本的には、Python コードで書き、そのPythonコードを自動的に Verilog HDL コードに変換している感じのようです。

Cfuクラス

CFU Playground では、プロジェクトディレクトリにある cfu.py ファイルから cfu.v ファイルを生成します。

例えば、proj/proj_template ディレクトリの cfu.py ファイルは、次のようになっています。

cfu.py

def make_cfu():
    return Cfu({
        # Add instructions here...
        0: TemplateInstruction(),
    })

この cfu.py の make_fuc メソッドの部分で Cfu クラスに、このファイルで定義した TemplateInstruction メソッドを機能番号 0 に 登録しています。 Cfu クラスは、CFU-Playground/python/nmigen_cfu/cfu.py で次のように定義しています。

init メソッドで、引数で渡した instructions を self.instructions に代入します。最大8個の instructions を登録できます。 その後、Verilog HDL コードになったときの入力および出力信号を定義しています。

    def __init__(self, instructions):
        self.instructions = instructions
        for i in range(8):
            if i not in self.instructions:
                self.instructions[i] = _FallbackInstruction()
        self.cmd_valid = Signal(name='cmd_valid')
        self.cmd_ready = Signal(name='cmd_ready')
        self.cmd_function_id = Signal(
            10, name='cmd_payload_function_id')
        self.cmd_in0 = Signal(32, name='cmd_payload_inputs_0')
        self.cmd_in1 = Signal(32, name='cmd_payload_inputs_1')
        self.rsp_valid = Signal(name='rsp_valid')
        self.rsp_ready = Signal(name='rsp_ready')
        self.rsp_ok = Signal(name='rsp_payload_response_ok')
        self.rsp_out = Signal(32, name='rsp_payload_outputs_0')
        self.reset = Signal(name='reset')
        self.ports = [
            self.cmd_valid,
            self.cmd_ready,
            self.cmd_function_id,
            self.cmd_in0,
            self.cmd_in1,
            self.rsp_valid,
            self.rsp_ready,
            self.rsp_ok,
            self.rsp_out,
            self.reset
        ]

elab メソッドでは、Verilog HDLの入出力のタイミングを生成しています。登録した各instructionを並列に実行して、入力信号 (cmd_function_id) の下位3ビットを機能番号として、この機能番号にて切り替えをやっている感じです。

    def elab(self, m):
        # break out the functionid
        funct3 = Signal(3)
        funct7 = Signal(7)
        m.d.comb += [
            funct3.eq(self.cmd_function_id[:3]),
            funct7.eq(self.cmd_function_id[-7:]),
        ]
        stored_function_id = Signal(3)
        current_function_id = Signal(3)
        current_function_done = Signal()
        stored_output = Signal(32)

        # Response is always OK.
        m.d.comb += self.rsp_ok.eq(1)

        instruction_outputs = Array(Signal(32) for _ in range(8))
        instruction_dones = Array(Signal() for _ in range(8))
        instruction_starts = Array(Signal() for _ in range(8))
        for (i, instruction) in self.instructions.items():
            m.d.comb += instruction_outputs[i].eq(instruction.output)
            m.d.comb += instruction_dones[i].eq(instruction.done)
            m.d.comb += instruction.start.eq(instruction_starts[i])

        def check_instruction_done():
            with m.If(current_function_done):
                m.d.comb += self.rsp_valid.eq(1)
                m.d.comb += self.rsp_out.eq(
                    instruction_outputs[current_function_id])
                with m.If(self.rsp_ready):
                    m.next = "WAIT_CMD"
                with m.Else():
                    m.d.sync += stored_output.eq(
                        instruction_outputs[current_function_id])
                    m.next = "WAIT_TRANSFER"
            with m.Else():
                m.next = "WAIT_INSTRUCTION"

        with m.FSM():
            with m.State("WAIT_CMD"):
                # We're waiting for a command from the CPU.
                m.d.comb += current_function_id.eq(funct3)
                m.d.comb += current_function_done.eq(
                    instruction_dones[current_function_id])
                m.d.comb += self.cmd_ready.eq(1)
                with m.If(self.cmd_valid):
                    m.d.sync += stored_function_id.eq(
                        self.cmd_function_id[:3])
                    m.d.comb += instruction_starts[current_function_id].eq(1)
                    check_instruction_done()
            with m.State("WAIT_INSTRUCTION"):
                # An instruction is executing on the CFU. We're waiting until it
                # completes.
                m.d.comb += current_function_id.eq(stored_function_id)
                m.d.comb += current_function_done.eq(
                    instruction_dones[current_function_id])
                check_instruction_done()
            with m.State("WAIT_TRANSFER"):
                # Instruction has completed, but the CPU isn't ready to receive
                # the result.
                m.d.comb += self.rsp_valid.eq(1)
                m.d.comb += self.rsp_out.eq(stored_output)
                with m.If(self.rsp_ready):
                    m.next = "WAIT_CMD"

        for (i, instruction) in self.instructions.items():
            m.d.comb += [
                instruction.in0.eq(self.cmd_in0),
                instruction.in1.eq(self.cmd_in1),
                instruction.funct7.eq(funct7),
            ]
            m.submodules[f"fn{i}"] = instruction

機能 (Instruction) の定義

各 Instruction の機能は、プロジェクトディレクトリにある cfu.py のInstructionBase クラスを継承したクラスの elab メソッドの部分になります。 proj/proj_template ディレクトリの cfu.py では、次のようにTemplateInstruction クラスの elab メソッドになります。

start が true の時は、in0 + in1 => output になり、done 信号を 1 にする。start が false の時は、done 信号を 0 にする、という感じです。

class TemplateInstruction(InstructionBase):
    """Template instruction
    """
    def elab(self, m):
        with m.If(self.start):
            m.d.sync += self.output.eq(self.in0 + self.in1)
            m.d.sync += self.done.eq(1)
        with m.Else():
            m.d.sync += self.done.eq(0)

Cfu クラスには、8個のInstructionを設定できると説明しましたが、設定しなかった機能番号には、次のような FallbackInstruction クラス が設定されます。 in0 => output になり、done 信号が常に 1 になっています。

FallbackInstructionクラス

class FallbackInstruction(InstructionBase):
    """Executed by CFU when no instruction explicitly defined for a given function id.
    This does nothing useful, but it does ensure that the CFU does not hang on an unknown functionid.
    """

    def elab(self, m):
        m.d.comb += self.output.eq(self.in0)
        m.d.comb += self.done.eq(1)

Pythonコードから生成されるVerilog HDLコード

cfu.v は、次のように、Cfu モジュールとして定義されています。登録した Instruction は、fn0からfn7 モジュールとして定義されています。Cfu モジュールの中で、fn0からfn7 の切り替えを機能番号 (cmd_payload_function_id) にて切り替えを行っています。

/* Generated by Yosys 0.9 (git sha1 1979e0b) */

(* \nmigen.hierarchy  = "Cfu" *)
(* top =  1  *)
(* generator = "nMigen" *)
module Cfu(cmd_ready, cmd_payload_function_id, cmd_payload_inputs_0, cmd_payload_inputs_1, rsp_valid, rsp_ready, rsp_payload_response_ok, rsp_payload_outputs_0, reset, clk, rst,
cmd_valid);

TemplateInstructionクラスに対応するVerilog HDLコード(cfu.v)は、次のようになっています。

(* \nmigen.hierarchy  = "Cfu.fn0" *)
(* generator = "nMigen" *)
module fn0(done, start, in0, in1, rst, clk, \output );
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/proj/proj_template/cfu.py:27" *)
  wire [32:0] \$1 ;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/proj/proj_template/cfu.py:27" *)
  wire [32:0] \$2 ;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/third_party/python/nmigen/nmigen/hdl/ir.py:524" *)
  input clk;
  (* init = 1'h0 *)
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:51" *)
  output done;
  reg done = 1'h0;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:51" *)
  reg \done$next ;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:46" *)
  input [31:0] in0;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:52" *)
  reg [31:0] in0s;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:47" *)
  input [31:0] in1;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:53" *)
  reg [31:0] in1s;
  (* init = 32'd0 *)
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:49" *)
  output [31:0] \output ;
  reg [31:0] \output  = 32'd0;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:49" *)
  reg [31:0] \output$next ;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/third_party/python/nmigen/nmigen/hdl/ir.py:524" *)
  input rst;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:50" *)
  input start;
  assign \$2  = in0 + (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/proj/proj_template/cfu.py:27" *) in1;
  always @(posedge clk)
      done <= \done$next ;
  always @(posedge clk)
      \output  <= \output$next ;
  always @* begin
    \output$next  = \output ;
    (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/proj/proj_template/cfu.py:26" *)
    casez (start)
      /* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/proj/proj_template/cfu.py:26" */
      1'h1:
          \output$next  = \$1 [31:0];
    endcase
    (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/third_party/python/nmigen/nmigen/hdl/xfrm.py:519" *)
    casez (rst)
         \output$next  = 32'd0;
    endcase
  end
  always @* begin
    \done$next  = done;
    (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/proj/proj_template/cfu.py:26" *)
    casez (start)
      /* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/proj/proj_template/cfu.py:26" */
      1'h1:
          \done$next  = 1'h1;
      /* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/proj/proj_template/cfu.py:29" */
      default:
          \done$next  = 1'h0;
    endcase
    (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/third_party/python/nmigen/nmigen/hdl/xfrm.py:519" *)
    casez (rst)
      1'h1:
          \done$next  = 1'h0;
    endcase
  end
  always @* begin
    in0s = 32'd0;
    in0s = in0;
  end
  always @* begin
    in1s = 32'd0;
    in1s = in1;
  end
  assign \$1  = \$2 ;
endmodule

一方、FallInstruction クラスは、fn1 (fn2からfn7も同じ感じ)のように生成されています。

(* \nmigen.hierarchy  = "Cfu.fn1" *)
(* generator = "nMigen" *)
module fn1(done, in0, in1, \output );
  (* init = 1'h0 *)
  reg \$verilog_initial_trigger  = 1'h0;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:51" *)
  output done;
  reg done;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:46" *)
  input [31:0] in0;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:52" *)
  reg [31:0] in0s;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:47" *)
  input [31:0] in1;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:53" *)
  reg [31:0] in1s;
  (* src = "/mnt/c/Users/haray/home/verilator/CFU-Playground/python/nmigen_cfu/cfu.py:49" *)
  output [31:0] \output ;
  reg [31:0] \output ;
  always @* begin
    \output  = 32'd0;
    \output  = in0;
  end
  always @* begin
    done = 1'h0;
    done = 1'h1;
    \$verilog_initial_trigger  = \$verilog_initial_trigger ;
  end
  always @* begin
    in0s = 32'd0;
    in0s = in0;
  end
  always @* begin
    in1s = 32'd0;
    in1s = in1;
  end
endmodule

おわりに

プロジェクトディレクトリにて、cfu.py という Python コード内で InstructionBase クラスを継承して、命令を定義すれば、あとは自動的に Verilog HDLコード (cfu.v) に変換され、全体に組み込まれるという仕組みです。

追加する部分だけを作ればいいというのはいいですね。