FPGA開発日記

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

Vivado-HLSを使って高位合成でCPUを作ってみる(1. メモリのRead/Writeのモデルを作成)

Vivado-HLSの勉強をしてみたいのだけれども、何か題材をもってデザインを作ってみる方が良いと思う。

そこで、簡単なモデルでも良いのでCPUを作ってみることにした。 最終的にはRISC-Vを実行させてみたいのだけれども、まずは簡単なモデルから動かすということで、実験してみる。 Vivado-HLSはC言語だから、テストベンチなども作りやすいはずだ。

f:id:msyksphinz:20181224132702p:plain

こういった、Vivado-HLSを使ったデザイン開発には多くのメリットがあると思う。 CPUなどの性能をも取られる分野でも、

  • 性能をあまり求められないマイクロコントローラの分野でアジャイル開発の1手法として使える。
  • 機能拡張の行いやすさでメリットがある。
  • ベースとなる記述がC言語なので、いろんな場所へ応用が利きやすい。
    • 最終的に、C言語で記述された命令セットシミュレータからそのもまVerilogが生成される、というのが理想だと思う。

という訳で、簡単なモデルを作ってみた。

github.com

CPUなので、命令バスとデータバスを用意している。

void cpu_hls (const uint32_t inst_mem[1024], uint32_t data_mem[1024])

内部にCPUモデルを持っており、レジスタなどの状態を含んでいる。

...
class rv32 {
  uint32_t reg32[32];

public:
  inst_rv32_t decode_inst (uint32_t inst);

さらに、いくつかの補助関数をクラスのメンバ関数として挿入した。

  inst_rv32_t decode_inst (uint32_t inst);

  uint8_t get_rs1_addr (uint32_t inst) {
    return ((inst >> 15) & 0x1f);
  }
  uint8_t get_rs2_addr (uint32_t inst) {
    return ((inst >> 20) & 0x1f);
  }
  uint8_t get_rd_addr (uint32_t inst) {
    return ((inst >> 7) & 0x1f);
  }

  uint32_t read_reg(uint8_t addr) {
    return reg32[addr];
  }

  void write_reg(uint8_t addr, uint32_t data) {
    reg32[addr] = data;
  }

デコーダは命令を解析して、必要なOpcodeを生成するだけだ。とりあえず、ADD, LOAD, STOREだけ。

inst_rv32_t rv32::decode_inst (uint32_t inst)
{
  switch(inst & 0x3f) {
    case 0x33 : return ADD;
    case 0x03 : return LW;
    case 0x23 : return SW;
    case 0xff : return NOP;   //
    default   : return NOP;
  }
}

デコード結果に基づいて命令を実行するループを定義する。

  do {
    inst = inst_mem[addr];
    dec_inst = u_rv32.decode_inst(inst);

    uint32_t reg_data;

    uint8_t rs1 = u_rv32.get_rs1_addr (inst);
    uint8_t rs2 = u_rv32.get_rs2_addr (inst);
    uint8_t rd  = u_rv32.get_rd_addr  (inst);

    switch (dec_inst) {
      case LW  : {
        uint32_t addr = rs1 + ((inst >> 20) & 0xfff);
        reg_data = mem_access(LOAD, rs1, addr, data_mem);
        u_rv32.write_reg(rd, reg_data);
        break;
      }
      case ADD : {
        reg_data = u_rv32.read_reg(rs1) + u_rv32.read_reg(rs2);
        u_rv32.write_reg(rd, reg_data);
        break;
      }
...

まずはテストをしなければならないのだが、これだけではテストパタンも流せないので、とりあえずはVivado-HLSによるVerilogの生成を行ってみる。

directives.tclとして以下を定義した。つまり、命令バスとデータバスは1つのバスとして扱うことにする。

set_directive_interface -mode s_axilite -bundle slv0 "cpu_hls"
set_directive_interface -mode s_axilite -bundle slv0 "cpu_hls" inst_mem
set_directive_interface -mode s_axilite -bundle slv0 "cpu_hls" data_mem

これでVivado-HLSでVerilogファイルを生成してみる。

  • cpu_hls/normal/syn/verilog/cpu_hls.v
module cpu_hls (
        ap_clk,
        ap_rst_n,
        s_axi_slv0_AWVALID,
        s_axi_slv0_AWREADY,
        s_axi_slv0_AWADDR,
        s_axi_slv0_WVALID,
        s_axi_slv0_WREADY,
        s_axi_slv0_WDATA,
        s_axi_slv0_WSTRB,
        s_axi_slv0_ARVALID,
        s_axi_slv0_ARREADY,
        s_axi_slv0_ARADDR,
        s_axi_slv0_RVALID,
        s_axi_slv0_RREADY,
        s_axi_slv0_RDATA,
        s_axi_slv0_RRESP,
        s_axi_slv0_BVALID,
        s_axi_slv0_BREADY,
        s_axi_slv0_BRESP,
        interrupt
);
...
cpu_hls_slv0_s_axi #(
    .C_S_AXI_ADDR_WIDTH( C_S_AXI_SLV0_ADDR_WIDTH ),
    .C_S_AXI_DATA_WIDTH( C_S_AXI_SLV0_DATA_WIDTH ))
cpu_hls_slv0_s_axi_U(
    .AWVALID(s_axi_slv0_AWVALID),
    .AWREADY(s_axi_slv0_AWREADY),
    .AWADDR(s_axi_slv0_AWADDR),
    .WVALID(s_axi_slv0_WVALID),
    .WREADY(s_axi_slv0_WREADY),
...

ちゃんとバスが1つにまとめられている。