FPGA開発日記

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

Vivado-HLSを使って高位合成でCPUを作ってみる(2. 標準的な命令を実装する)

Vivado-HLSを使って簡単なRISC-V CPUを作ってみている。

まずは性能を度外視して動くものを作る。 次に、性能を向上させるための様々な試行を実施する。 まずは、標準的なテストパタンをPassさせるために命令を追加していこう。

一般的なCPUと同じで、まずは命令のデコード、デコードに応じて命令の動作の記述、そしてプログラムカウンタの更新と、これらの処理をすべてC言語で記述し、Vivado-HLSで高位合成させてみる。

まず、テストベンチを作成した。これはrv32ui-p-simpleテストパタンを逆アセンブリし、32bit単位でhexを並べてメモリに格納しておく。 これで、テストパタン完成だ。

riscv64-unknown-elf-objcopy -O binary rv32ui-p-simple rv32ui-p-simple.bin
hexdump -v -e ' 1/4 "%08x " "\n"' rv32ui-p-simple.bin > rv32ui-p-simple.hex
  • rv32ui-p-simple.hex
04c0006f
34202f73
00800f93
03ff0a63
00900f93
03ff0663
00b00f93
03ff0263
...

これを高位合成CPUのテスト環境で動作させるために、test_cpu_hls.cpp(cpu_hlsのテストパタン環境)でファイルとしてロードする。このあたり、C言語の関数がそのまま使えるので便利だ。

  • test_cpu_hls.cpp
int main ()
{
  uint32_t memory[2048];

  FILE *fp;
  if ((fp = fopen ("test.hex", "r")) == NULL) {
    perror ("test.hex");
    exit (1);
  }

  int idx = 0;
  while (fscanf(fp, "%08x", &memory[idx]) != EOF) {
    fprintf (stdout, "memory[%d] <= %08x\n", idx, memory[idx]);
    idx ++;
  }
...

そして、これをデコードして命令を実行してく。 cpu_hls内に、以下の命令のデコーダとデータパスを実装した。

  • rv32_cpu.hpp
typedef enum {
  CSRRW , CSRRS , CSRRC ,
  CSRRWI, CSRRSI, CSRRCI,
  LUI, AUIPC,
  ADDI, SLTI, SLTIU, XORI, ORI, ANDI, SLLI, SRLI, SRAI,
  ADD, SUB, SLL ,SLT, SLTU, XOR, SRL, SRA, OR, AND,
  LW,  SW,
  JAL, JALR,
  BEQ, BNE, BLT, BGE, BLTU, BGEU,
  NOP,
  WFI
} inst_rv32_t;

これらに対して、以下のように命令のデータパスを追加していく。

...
      case ORI : {
        reg_data = u_rv32_cpu.read_reg(rs1) | u_rv32_cpu.ExtractIField (inst);
        u_rv32_cpu.write_reg(rd, reg_data);
        break;
      }
      case ANDI : {
        reg_data = u_rv32_cpu.read_reg(rs1) & u_rv32_cpu.ExtractIField (inst);
        u_rv32_cpu.write_reg(rd, reg_data);
        break;
      }

      case ADD : {
        reg_data = u_rv32_cpu.read_reg(rs1) + u_rv32_cpu.read_reg(rs2);
        u_rv32_cpu.write_reg(rd, reg_data);
        break;
      }
      case SUB : {
        reg_data = u_rv32_cpu.read_reg(rs1) - u_rv32_cpu.read_reg(rs2);
        u_rv32_cpu.write_reg(rd, reg_data);
        break;
      }
...

かなり冗長な記述をしているが、これで回路規模がどのようになるのかは検証していない。 もう少し最適化の余地がありそうな気がする。

そして、さらに迷ったのが、高位合成デザイン内でfprintf()などのデバッガを仕込んでいる場合、これを合成時に除去するためのdefineは、__SYNTHESIS__というものを使うらしい。 FPGAの部屋を調べると書いてあった。ありがたい。

marsee101.blog19.fc2.com

  • cpu_hls.cpp
#ifndef __SYNTHESIS__
  FILE *cpu_log;
  if ((cpu_log = fopen("cpu.log", "w")) == NULL) {
    perror ("cpu.log");
  }
#endif  // __SYNTHESIS__

#ifndef __SYNTHESIS__
    fprintf(cpu_log, "[%08x] : %08x DASM(%08x)\n", pc, inst, inst);
#endif // __SYNTHESIS__

fprintf()でログを出力し、その内容を解析した。

spike-dasm < cpu.log

結果は以下のようになった。fenceはまだ実装していないのでそこで停止してしまったが、概ね動作しているような気がする。ちゃんとレジスタ書き込み値も出力した方が良いな。

[00000000] : 04c0006f j       pc + 0x4c
[0000004c] : f1402573 csrr    a0, mhartid
[00000050] : 00051063 bnez    a0, pc + 0
[00000054] : 00000297 auipc   t0, 0x0
[00000058] : 01028293 addi    t0, t0, 16
[0000005c] : 30529073 csrw    mtvec, t0
[00000060] : 18005073 csrwi   satp, 0
[00000064] : 00000297 auipc   t0, 0x0
[00000068] : 02028293 addi    t0, t0, 32
[0000006c] : 30529073 csrw    mtvec, t0
[00000070] : 800002b7 lui     t0, 0x80000
[00000074] : fff28293 addi    t0, t0, -1
[00000078] : 3b029073 csrw    pmpaddr0, t0
[0000007c] : 01f00293 li      t0, 31
[00000080] : 3a029073 csrw    pmpcfg0, t0
[00000084] : 00000297 auipc   t0, 0x0
[00000088] : 01828293 addi    t0, t0, 24
[0000008c] : 30529073 csrw    mtvec, t0
[00000090] : 30205073 csrwi   medeleg, 0
[00000094] : 30305073 csrwi   mideleg, 0
[00000098] : 30405073 csrwi   mie, 0
[0000009c] : 00000193 li      gp, 0
[000000a0] : 00000297 auipc   t0, 0x0
[000000a4] : f6428293 addi    t0, t0, -156
[000000a8] : 30529073 csrw    mtvec, t0
[000000ac] : 00100513 li      a0, 1
[000000b0] : 01f51513 slli    a0, a0, 31
[000000b4] : 00054863 bltz    a0, pc + 16
[000000b8] : 0ff0000f fence
f:id:msyksphinz:20190215011547p:plain
Vivado-HLSのC言語モードでCPU_HLSをシミュレーションした。