Perf-V FPGA板的资源可以实现RISC-V CPU核,即用FPGA中的硬逻辑搭出来一个CPU. 这个“搭积木”的过程就是FPGA综合工具干的活了——是将CPU逻辑的描述(源代码)翻译成硬件(逻辑门、寄存器等)的过程。用硬件描述语言(HDL)实现的CPU并且其源代码是开放的,就可称作开源CPU. 开源的RISC-V CPU已经有不少了,可以作为学习和实验RISC-V的基础。
E203是蜂鸟E200系列RISC-V IP核中的一款开源软核,用Verilog语言编写。E203作者写了两本关于RISC-V的书对E203的设计和应用进行了较为详细的介绍,一本偏重硬件,一本侧重软件。想深入了解RISC-V的朋友可以找来读。
Perf-V官方提供的技术资料里面包含了蜂鸟E203开源SOC的FPGA demo工程,是把Xilinx Vivado的工程目录整个打包了。
不过几十MB的压缩包中,几乎都是Vivado生成的文件,源代码只占一点点。要学习E203源代码,可以从里面找,也可以从github单独下载。
E203 SOC不仅仅是一个CPU,它包含了E203 RISC-V核之外的总线、内存块、外设功能模块等,相当于一个完整的MCU.
在正式到板子上玩 RISC-V 之前,有必要对 E203 SOC 的结构进行了解。
下面是我从 github 上抓下来的 E203 RTL代码树,一共有几个子目录,源码总共不到3MB.
rtl
├─core
│ config.v
│ e203_biu.v
│ e203_clkgate.v
│ e203_clk_ctrl.v
│ e203_core.v
│ e203_cpu.v
│ e203_cpu_top.v
│ e203_defines.v
│ e203_dtcm_ctrl.v
│ e203_dtcm_ram.v
│ e203_extend_csr.v
│ e203_exu.v
│ e203_exu_alu.v
│ e203_exu_alu_bjp.v
│ e203_exu_alu_csrctrl.v
│ e203_exu_alu_dpath.v
│ e203_exu_alu_lsuagu.v
│ e203_exu_alu_muldiv.v
│ e203_exu_alu_rglr.v
│ e203_exu_branchslv.v
│ e203_exu_commit.v
│ e203_exu_csr.v
│ e203_exu_decode.v
│ e203_exu_disp.v
│ e203_exu_excp.v
│ e203_exu_longpwbck.v
│ e203_exu_oitf.v
│ e203_exu_regfile.v
│ e203_exu_wbck.v
│ e203_ifu.v
│ e203_ifu_ifetch.v
│ e203_ifu_ift2icb.v
│ e203_ifu_litebpu.v
│ e203_ifu_minidec.v
│ e203_irq_sync.v
│ e203_itcm_ctrl.v
│ e203_itcm_ram.v
│ e203_lsu.v
│ e203_lsu_ctrl.v
│ e203_reset_ctrl.v
│ e203_srams.v
│
├─debug
│ sirv_debug_csr.v
│ sirv_debug_module.v
│ sirv_debug_ram.v
│ sirv_debug_rom.v
│ sirv_jtag_dtm.v
│
├─fab
│ sirv_icb1to16_bus.v
│ sirv_icb1to2_bus.v
│ sirv_icb1to8_bus.v
│
├─general
│ sirv_1cyc_sram_ctrl.v
│ sirv_gnrl_bufs.v
│ sirv_gnrl_dffs.v
│ sirv_gnrl_icbs.v
│ sirv_gnrl_ram.v
│ sirv_gnrl_xchecker.v
│ sirv_sim_ram.v
│ sirv_sram_icb_ctrl.v
│
├─mems
│ sirv_mrom.v
│ sirv_mrom_top.v
│
├─perips
│ i2c_master_bit_ctrl.v
│ i2c_master_byte_ctrl.v
│ i2c_master_defines.v
│ i2c_master_top.v
│ sirv_aon.v
│ sirv_aon_lclkgen_regs.v
│ sirv_aon_porrst.v
│ sirv_aon_top.v
│ sirv_aon_wrapper.v
│ sirv_AsyncResetReg.v
│ sirv_AsyncResetRegVec.v
│ sirv_AsyncResetRegVec_1.v
│ sirv_AsyncResetRegVec_129.v
│ sirv_AsyncResetRegVec_36.v
│ sirv_AsyncResetRegVec_67.v
│ sirv_clint.v
│ sirv_clint_top.v
│ sirv_DeglitchShiftRegister.v
│ sirv_expl_apb_slv.v
│ sirv_expl_axi_slv.v
│ sirv_flash_qspi.v
│ sirv_flash_qspi_top.v
│ sirv_gpio.v
│ sirv_gpio_top.v
│ sirv_hclkgen_regs.v
│ sirv_jtaggpioport.v
│ sirv_LevelGateway.v
│ sirv_otp_top.v
│ sirv_plic_man.v
│ sirv_plic_top.v
│ sirv_pmu.v
│ sirv_pmu_core.v
│ sirv_pwm16.v
│ sirv_pwm16_core.v
│ sirv_pwm16_top.v
│ sirv_pwm8.v
│ sirv_pwm8_core.v
│ sirv_pwm8_top.v
│ sirv_pwmgpioport.v
│ sirv_qspi_1cs.v
│ sirv_qspi_1cs_top.v
│ sirv_qspi_4cs.v
│ sirv_qspi_4cs_top.v
│ sirv_qspi_arbiter.v
│ sirv_qspi_fifo.v
│ sirv_qspi_media.v
│ sirv_qspi_media_1.v
│ sirv_qspi_media_2.v
│ sirv_qspi_physical.v
│ sirv_qspi_physical_1.v
│ sirv_qspi_physical_2.v
│ sirv_queue.v
│ sirv_queue_1.v
│ sirv_repeater_6.v
│ sirv_ResetCatchAndSync.v
│ sirv_ResetCatchAndSync_2.v
│ sirv_rtc.v
│ sirv_spigpioport.v
│ sirv_spigpioport_1.v
│ sirv_spigpioport_2.v
│ sirv_spi_flashmap.v
│ sirv_tlfragmenter_qspi_1.v
│ sirv_tlwidthwidget_qspi.v
│ sirv_tl_repeater_5.v
│ sirv_uart.v
│ sirv_uartgpioport.v
│ sirv_uartrx.v
│ sirv_uarttx.v
│ sirv_uart_top.v
│ sirv_wdog.v
│
├─soc
│ e203_soc_top.v
│
└─subsys
e203_subsys_clint.v
e203_subsys_gfcm.v
e203_subsys_hclkgen.v
e203_subsys_hclkgen_rstsync.v
e203_subsys_main.v
e203_subsys_mems.v
e203_subsys_perips.v
e203_subsys_plic.v
e203_subsys_pll.v
e203_subsys_pllclkdiv.v
e203_subsys_top.v
文档中给出的层次结构描述是这样的:
和这套RTL代码一起提供的还有很多测试例,可以对E203 CPU进行仿真测试。
在tb目录下有一个 tb_top.v 的文件,就是测试用的顶层模块。在PC机上,用Verilog语言的仿真工具,差不多就可以做仿真实验了。在文档中,作者提到使用的是iverilog这个免费的仿真软件(全称 Icarus Verilog) ,正好我也用这个,就直接调用 iverilog 处理源代码。
不过一开始就遇到错误了:包含文件 e200_defines.v 找不到。原来 rtl/core 里面文件名是 e203_defines.v 不一样。改了下,然后居然宏定义缺失。仔细一看,是 tb_top.v 里面到处使用的是 e200 和 E200 字样,而 rtl 代码树中都没有出现,遂都分别改为 e203 和 E203 才可以。
因为我不想逐个添加 verilog 文件,就用了 iverilog 的 +libdir 命令指定子目录。这样还有一些模块找不到的错误,是因为引用的 verilog 模块名没有对应到 verilog 文件名(一个文件可以包含多个模块定义),故还需要单独指定一些文件。调整好以后,写成的文件如下:
+incdir+rtl/core
+incdir+rtl/perips
+define+DISABLE_SV_ASSERTION=1
rtl/general/sirv_1cyc_sram_ctrl.v
rtl/general/sirv_gnrl_icbs.v
rtl/general/sirv_sim_ram.v
rtl/general/sirv_gnrl_bufs.v
rtl/general/sirv_gnrl_ram.v
rtl/general/sirv_sram_icb_ctrl.v
rtl/general/sirv_gnrl_dffs.v
rtl/general/sirv_gnrl_xchecker.v
rtl/perips/sirv_spi_flashmap.v
+libdir+rtl/core
+libdir+rtl/mems
+libdir+rtl/debug
+libdir+rtl/fab
+libdir+rtl/perips
+libdir+rtl/soc
+libdir+rtl/subsys
tb/tb_top.v
将这个文件命名为 sim_e203.f 再用 iverilog -f sim_e203.f 即可编译。
如果成功了,执行 vvp a.out 进行仿真。
这时候 vvp 报错了,是 tb_top.v 中这里有问题
initial begin
$value$plusargs("DUMPWAVE=%d",dumpwave);
if(dumpwave != 0)begin
// To add your waveform generation function
end
end
我暂时也不用 dump ,先给把这段注释屏蔽掉不管。然后再来一次,出现仿真运行的信息了:
E:\Perf-V\e203>vvp a.out
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
ERROR: tb/tb_top.v:257: $readmemh: Unable to open .verilog for reading.
ITCM 0x00: xxxxxxxxxxxxxxxx
ITCM 0x01: xxxxxxxxxxxxxxxx
ITCM 0x02: xxxxxxxxxxxxxxxx
ITCM 0x03: xxxxxxxxxxxxxxxx
ITCM 0x04: xxxxxxxxxxxxxxxx
ITCM 0x05: xxxxxxxxxxxxxxxx
ITCM 0x06: xxxxxxxxxxxxxxxx
ITCM 0x07: xxxxxxxxxxxxxxxx
ITCM 0x16: xxxxxxxxxxxxxxxx
ITCM 0x20: xxxxxxxxxxxxxxxx
看来是需要加载的内存数据文件缺少了,不能正确初始化。根据提示找到代码中读取文件的位置,便知道了需要提供 TESTCASE 宏指定一个数据文件。
在 github 上 e200_opensource/riscv-tools/riscv-tests/isa/generated/ 下有许多编译过的文件,以 .verilog 结尾,可以直接拿来测试。这个目录下的测试例是测试指令集的。比如我使用 rv32ui-p-add.verilog 这个文件,就执行:
E:\Perf-V\e203>vvp a.out +TESTCASE=rv32ui-p-add
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
TESTCASE= rv32ui-p-add
ITCM 0x00: 340510730001aa0d
ITCM 0x01: ff85051300002517
ITCM 0x02: 01f5222301e52023
ITCM 0x03: 040f416334202f73
ITCM 0x04: 4fa507ff02634fa1
ITCM 0x05: 0c634fad05ff0f63
ITCM 0x06: 0bff05634f8505ff
ITCM 0x07: 4f9d0dff00634f95
ITCM 0x16: 2f03f52505130000
ITCM 0x20: 2f8300052f03f065
然后等待仿真结果。
但是等了很久都没有出来结果。电脑的CPU也一直占用着…… Ctrl-C中止了它,仿真时间还是在0,有奇怪的问题了。
不解,我就到网上搜搜看有没有人遇到同样问题的。还真有人提到E203仿真的坑,就点开看看。据说是 iverilog 版本10不行,需要版本12. 而我用的 win32 port 版本正好是v10.0 ,而且更高版本的已经没有现成的win32 port了。
那么远程登陆到单位的Linux工作站上去试试吧。iverilog 肯定是要自己编译的,好在源代码并不大,从 github 上很快拖下来了。准备编译,需要 gperf 但该机上没有,现装,可以用。make 过程中, g++ 报错了,估计是GCC版本不够高的原因。我看了下,出错的代码是
buf << (file_? file_ : "") << ":" << lineno_;
那么就修改成 if-else 语句来用吧。net_scope.cc 文件改了两处,编译通过。最后 iverilog 编译完成了。
再执行 iverilog 和 vvp ,不到一分钟,仿真运行结果出来了:
用仿真耗时和"Total cycle_count"表达的仿真过程主时钟周期数进行折算,相当于 E203 SOC 的仿真运行频率只有 800Hz 左右.
这样的仿真虽然耗时,但因为可以查看每个信号的状态,适合查错用。上面我试的仿真内容是验证 add 指令,其测试代码(RISC-V指令)是用汇编语言写的,而且用了宏定义,故不熟悉指令架构就难以明白其意图。例如,这是汇编文件片段:
#-------------------------------------------------------------
# Arithmetic tests
#-------------------------------------------------------------
TEST_RR_OP( 2, add, 0x00000000, 0x00000000, 0x00000000 );
TEST_RR_OP( 3, add, 0x00000002, 0x00000001, 0x00000001 );
TEST_RR_OP( 4, add, 0x0000000a, 0x00000003, 0x00000007 );
TEST_RR_OP( 5, add, 0xffffffffffff8000, 0x0000000000000000, 0xffffffffffff8000 );
TEST_RR_OP( 6, add, 0xffffffff80000000, 0xffffffff80000000, 0x00000000 );
TEST_RR_OP( 7, add, 0xffffffff7fff8000, 0xffffffff80000000, 0xffffffffffff8000 );
TEST_RR_OP( 8, add, 0x0000000000007fff, 0x0000000000000000, 0x0000000000007fff );
TEST_RR_OP( 9, add, 0x000000007fffffff, 0x000000007fffffff, 0x0000000000000000 );
TEST_RR_OP( 10, add, 0x0000000080007ffe, 0x000000007fffffff, 0x0000000000007fff );
TEST_RR_OP( 11, add, 0xffffffff80007fff, 0xffffffff80000000, 0x0000000000007fff );
TEST_RR_OP( 12, add, 0x000000007fff7fff, 0x000000007fffffff, 0xffffffffffff8000 );
TEST_RR_OP( 13, add, 0xffffffffffffffff, 0x0000000000000000, 0xffffffffffffffff );
TEST_RR_OP( 14, add, 0x0000000000000000, 0xffffffffffffffff, 0x0000000000000001 );
TEST_RR_OP( 15, add, 0xfffffffffffffffe, 0xffffffffffffffff, 0xffffffffffffffff );
generated 目录下生成的机器码包括仿真用的 verilog 文件(相当于单片机烧写用的 .hex 文件的作用),编译得到的 .elf 文件,以及反汇编 ELF 文件的结果 *.dump.
看一下 .dump 文件的内容:很长,不能贴全在这里了
Disassembly of section .text.init:
80000000 <_start>:
80000000: aa0d j 80000132 <reset_vector>
80000002: 0001 nop
80000004 <trap_vector>:
80000004: 34051073 csrw mscratch,a0
80000008: 00002517 auipc a0,0x2
8000000c: ff850513 addi a0,a0,-8 # 80002000 <test_trap_data>
80000010: 01e52023 sw t5,0(a0)
80000014: 01f52223 sw t6,4(a0)
80000018: 34202f73 csrr t5,mcause
8000001c: 040f4163 bltz t5,8000005e <other_interrupts>
80000020: 4fa1 li t6,8
80000022: 07ff0263 beq t5,t6,80000086 <write_tohost>
80000026: 4fa5 li t6,9
80000028: 05ff0f63 beq t5,t6,80000086 <write_tohost>
8000002c: 4fad li t6,11
8000002e: 05ff0c63 beq t5,t6,80000086 <write_tohost>
80000032: 4f85 li t6,1
80000034: 0bff0563 beq t5,t6,800000de <ifetch_error_handler>
80000038: 4f95 li t6,5
8000003a: 0dff0063 beq t5,t6,800000fa <load_error_handler>
8000003e: 4f9d li t6,7
80000040: 0dff0b63 beq t5,t6,80000116 <store_error_handler>
80000044: 80000f17 auipc t5,0x80000
80000048: fbcf0f13 addi t5,t5,-68 # 0 <_start-0x80000000>
8000004c: 000f0363 beqz t5,80000052 <trap_vector+0x4e>
80000050: 8f02 jr t5
80000052: 34202f73 csrr t5,mcause
80000056: 000f5363 bgez t5,8000005c <handle_exception>
8000005a: a009 j 8000005c <handle_exception>
8000005c <handle_exception>:
8000005c: a01d j 80000082 <other_interrupts+0x24>
8000005e <other_interrupts>:
8000005e: 80000fb7 lui t6,0x80000
80000062: 003f8f93 addi t6,t6,3 # 80000003 <_end+0xffffdff3>
80000066: 05ff0463 beq t5,t6,800000ae <sft_irq_handler>
8000006a: 80000fb7 lui t6,0x80000
8000006e: 007f8f93 addi t6,t6,7 # 80000007 <_end+0xffffdff7>
80000072: 05ff0a63 beq t5,t6,800000c6 <tmr_irq_handler>
80000076: 80000fb7 lui t6,0x80000
8000007a: 00bf8f93 addi t6,t6,11 # 8000000b <_end+0xffffdffb>
8000007e: 01ff0c63 beq t5,t6,80000096 <ext_irq_handler>
80000082: 5391e193 ori gp,gp,1337
80000086 <write_tohost>:
80000086: 4521 li a0,8
80000088: 30052073 csrs mstatus,a0
8000008c: 00001f17 auipc t5,0x1
80000090: f63f2a23 sw gp,-140(t5) # 80001000 <tohost>
80000094: bfcd j 80000086 <write_tohost>
80000096 <ext_irq_handler>:
80000096: 00002517 auipc a0,0x2
8000009a: f6a50513 addi a0,a0,-150 # 80002000 <test_trap_data>
8000009e: 00052f03 lw t5,0(a0)
800000a2: 00452f83 lw t6,4(a0)
800000a6: 34002573 csrr a0,mscratch
800000aa: 30200073 mret
800000ae <sft_irq_handler>:
800000ae: 00002517 auipc a0,0x2
800000b2: f5250513 addi a0,a0,-174 # 80002000 <test_trap_data>
800000b6: 00052f03 lw t5,0(a0)
800000ba: 00452f83 lw t6,4(a0)
800000be: 34002573 csrr a0,mscratch
800000c2: 30200073 mret
......
80000132 <reset_vector>:
80000132: 00000f13 li t5,0
80000136: 00000f93 li t6,0
8000013a: f1402573 csrr a0,mhartid
8000013e: e101 bnez a0,8000013e <reset_vector+0xc>
80000140: 4181 li gp,0
80000142: 00000297 auipc t0,0x0
80000146: ec228293 addi t0,t0,-318 # 80000004 <trap_vector>
8000014a: 4521 li a0,8
8000014c: 30052073 csrs mstatus,a0
80000150: fff00513 li a0,-1
80000154: 30452073 csrs mie,a0
80000158: 30529073 csrw mtvec,t0
8000015c <post_mtvec>:
8000015c: 80000297 auipc t0,0x80000
80000160: ea428293 addi t0,t0,-348 # 0 <_start-0x80000000>
80000164: 00028e63 beqz t0,80000180 <post_mtvec+0x24>
80000168: 10529073 csrw stvec,t0
8000016c: 0000b2b7 lui t0,0xb
80000170: 10928293 addi t0,t0,265 # b109 <_start-0x7fff4ef7>
80000174: 30229073 csrw medeleg,t0
80000178: 30202373 csrr t1,medeleg
8000017c: ee6290e3 bne t0,t1,8000005c <handle_exception>
80000180: 30005073 csrwi mstatus,0
80000184: 08000513 li a0,128
80000188: 30052073 csrs mstatus,a0
8000018c: 4501 li a0,0
8000018e: bfc51073 csrw 0xbfc,a0
80000192: 0000100f fence.i
80000196: 000012b7 lui t0,0x1
8000019a: a0028293 addi t0,t0,-1536 # a00 <_start-0x7ffff600>
8000019e <waitloop1>:
8000019e: 12fd addi t0,t0,-1
800001a0: fe029fe3 bnez t0,8000019e <waitloop1>
800001a4: 100083b7 lui t2,0x10008
800001a8: 00838393 addi t2,t2,8 # 10008008 <_start-0x6fff7ff8>
800001ac: 0003a283 lw t0,0(t2)
800001b0: 00040337 lui t1,0x40
800001b4: fff34313 not t1,t1
800001b8: 0062f2b3 and t0,t0,t1
800001bc: 0053a023 sw t0,0(t2)
800001c0: 40000293 li t0,1024
800001c4 <waitloop2>:
800001c4: 12fd addi t0,t0,-1
800001c6: 0003ae03 lw t3,0(t2)
800001ca: fe029de3 bnez t0,800001c4 <waitloop2>
800001ce: 0003a283 lw t0,0(t2)
800001d2: 00040337 lui t1,0x40
800001d6: 0062e2b3 or t0,t0,t1
800001da: 0053a023 sw t0,0(t2)
......
80000262 <test_2>:
80000262: 4081 li ra,0
80000264: 4101 li sp,0
80000266: 00208f33 add t5,ra,sp
8000026a: 4e81 li t4,0
8000026c: 4189 li gp,2
8000026e: 37df1d63 bne t5,t4,800005e8 <fail>
80000272 <test_3>:
80000272: 4085 li ra,1
80000274: 4105 li sp,1
80000276: 00208f33 add t5,ra,sp
8000027a: 4e89 li t4,2
8000027c: 418d li gp,3
8000027e: 37df1563 bne t5,t4,800005e8 <fail>
80000282 <test_4>:
80000282: 408d li ra,3
80000284: 411d li sp,7
80000286: 00208f33 add t5,ra,sp
8000028a: 4ea9 li t4,10
8000028c: 4191 li gp,4
8000028e: 35df1d63 bne t5,t4,800005e8 <fail>
......
我尚不熟悉RISC-V的指令集,因此大部分都看不懂。
这段代码是直接放到 E203 SOC 的 ITCM 中的,因此一启动就执行了。它包含了中断向量的处理,是一个完整的程序,只是并不像我们做MCU程序要操作片上设备以实现功能。
想查看仿真的细节,用verilog系统的dump功能就可以。在 tb_top.v 中加这两行,将 e_203_cpu 这个模块以及其下级的信号导出成vcd文件:
$dumpfile("e203test.vcd");
$dumpvars(0, u_e203_soc_top.u_e203_subsys_top.u_e203_subsys_main.u_e203_cpu_top.u_e203_cpu);
再重新做仿真,就会每次都生成 e203test.vcd 这个文件(几百MB,是纯文本文件)。然后可以用 gtkwave 这个软件来看波形了:
这样可以观看 CPU 是如何取指令,如何执行指令等等的每一步状态变化,可以结合源代码阅读,帮助理解E203 RISC-V CPU的设计。
既然 E203 SOC 整体都被仿真了,要写程序控制 GPIO、UART 也是可以做到,并且从仿真可以观察到的。只是,那样就适合用 FPGA 在电路中验证。
本帖最后由 cruelfox 于 2021-6-12 22:36 编辑