Vivado-HLSで高位合成からCPUを作ってみたい。 高位合成でCPUを作るというのはネタとしてはイマイチだけれども、高位合成がどういうものか、目標をもって試してみるには良い題材だと思うので試してみている。
前回はとりあえずメモリにアクセスしてデータをフェッチしたり書き込んだりするモデルを作った。 次は、本格的にCPUっぽいものを作っていく。
目標としてはRISC-VのISSに近い、C++の実装からVivado-HLSを通じて高位合成でハードウェアを生成することだ。 C++でISSを作った場合、プロセッサのモデルをクラス化し、そこからいろんなメソッドを追加するような構成になる(と思う)。
そこで、まずは一般的なCPUのクラスを作成し、そこにレジスタ、メモリアクセスのメソッド、システムレジスタアクセス用のサブクラスを追加していく。
class rv32_cpu { XLEN_t m_reg32[32]; rv32_csr m_rv32_csr; uint32_t *m_data_mem; public: rv32_cpu (uint32_t *data_mem); ...
m_rv32_csr
はCSRレジスタをまとめたクラスだ。m_reg32[32]
汎用レジスタ群となる。
まず、命令をフェッチしてデコードする。
do {
inst = inst_mem[addr];
dec_inst = u_rv32_cpu.decode_inst(inst);
デコード論理はこんな感じ。非常にしょぼい。
inst_rv32_t rv32_cpu::decode_inst (uint32_t inst) { switch(inst & 0x3f) { case 0x33 : return ADD; case 0x03 : return LW; case 0x23 : return SW; case 0xff : return NOP; // case 0x73 : switch ((inst >> 12) & 0x07) { case 0b001 : return CSRRW ; case 0b010 : return CSRRS ; case 0b011 : return CSRRC ;
次に、オペランドを取り出す。
RegAddr_t rs1 = u_rv32_cpu.get_rs1_addr (inst); RegAddr_t rs2 = u_rv32_cpu.get_rs2_addr (inst); RegAddr_t rd = u_rv32_cpu.get_rd_addr (inst); uint16_t csr_addr = (inst >> 16) & 0x0ffff;
次にデコード結果に基づき実行を行う。メモリアクセスについては、mem_access
関数に集約し、data_mem
配列にアクセスすることで実現している。
switch (dec_inst) { case CSRRW : { reg_data = u_rv32_cpu.csrrw (csr_addr, rs1); u_rv32_cpu.write_reg(rd, reg_data); break; } case CSRRS : { reg_data = u_rv32_cpu.csrrs (csr_addr, rs1); ... case LW : { uint32_t addr = rs1 + ((inst >> 20) & 0xfff); reg_data = u_rv32_cpu.mem_access(LOAD, rs1, addr, data_mem); 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); ...
という訳で、これらのCPUのモデルをクラス化した。
これでVivado-HLSを実行してみると、なるほど、ちゃんと合成が終了したようだ。 C++のクラスも扱えるし、ポインタも行ける。 Vivado-HLSはすごいなあ。
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, ...