Loading

tinyriscv——测试仿真分析

tinyriscv测试通过iverilog和gtkwave实现,对其在指令集测试时如何判断test pass比较好奇,故分析一下这方面的代码。

一,外设模块与地址

tinyriscv挂了六个外设,rom从0x0000_0000开始,ram从0x1000_0000开始,而外设的地址在总线中定义,rib总线的主从模块接口如下:

module rib(

    input wire clk,
    input wire rst,
......
  // master 0 interface
    input wire[`MemAddrBus] m0_addr_i,     // 主设备0读、写地址
    input wire[`MemBus] m0_data_i,         // 主设备0写数据
    output reg[`MemBus] m0_data_o,         // 主设备0读取到的数据
    input wire m0_req_i,                   // 主设备0访问请求标志
    input wire m0_we_i,                    // 主设备0写标志
 // slave 0 interface
    output reg[`MemAddrBus] s0_addr_o,     // 从设备0读、写地址
    output reg[`MemBus] s0_data_o,         // 从设备0写数据
    input wire[`MemBus] s0_data_i,         // 从设备0读取到的数据
    output reg s0_we_o,                    // 从设备0写标志
......
endmodule

在soc_top.v中,例化各个模块时将输入输出接入总线:

//这段例化把内核模块接入总线的master模块,注意pc取指也当作一个主模块来访问总线以获得data,
tinyriscv u_tinyriscv(
        .clk(clk),
        .rst(rst),
        .rib_ex_addr_o(m0_addr_i),
        .rib_ex_data_i(m0_data_o),
        .rib_ex_data_o(m0_data_i),
        .rib_ex_req_o(m0_req_i),
        .rib_ex_we_o(m0_we_i),

        .rib_pc_addr_o(m1_addr_i),
        .rib_pc_data_i(m1_data_o),

        .jtag_reg_addr_i(jtag_reg_addr_o),
        .jtag_reg_data_i(jtag_reg_data_o),
        .jtag_reg_we_i(jtag_reg_we_o),
        .jtag_reg_data_o(jtag_reg_data_i),

        .rib_hold_flag_i(rib_hold_flag_o),
        .jtag_halt_flag_i(jtag_halt_req_o),
        .jtag_reset_flag_i(jtag_reset_req_o),

        .int_i(int_flag)
    );

从设备接入总线:

// gpio模块例化
    gpio gpio_0(
        .clk(clk),
        .rst(rst),
        .we_i(s4_we_o),
        .addr_i(s4_addr_o),
        .data_i(s4_data_o),
        .data_o(s4_data_i),
        .io_pin_i(io_in),
        .reg_ctrl(gpio_ctrl),
        .reg_data(gpio_data)
    );

而设备地址在rib.v中设置:

 // 访问地址的最高4位决定要访问的是哪一个从设备
    // 因此最多支持16个从设备
    parameter [3:0]slave_0 = 4'b0000;
    parameter [3:0]slave_1 = 4'b0001;
    parameter [3:0]slave_2 = 4'b0010;
    parameter [3:0]slave_3 = 4'b0011;
    parameter [3:0]slave_4 = 4'b0100;
    parameter [3:0]slave_5 = 4'b0101;

即对应的这个设备图的情况:

 

二,sim

python .\compliance_test.py ..\..\tests\riscv-compliance\build_generated\rv32i\I-ADD-01.elf.bin inst.data 该脚本将可执行bin转换为二进制的inst.data,然后执行iverilog的编译和执行工作。

lowRISC/riscv-compliance: TEMPORARY FORK of the riscv-compliance repository (github.com) 测试框架下其通过比对输出与标准的区别,从而判断仿真是否正确

compliance_test.py脚本分析

测试文件是:tinyriscv/compliance_test.py at master · liangkangnan/tinyriscv (github.com)

主要流程如下:

  • 1.将给定的二进制文件(.bin)转换为内存文件(.mem)
  • 2.编译 Verilog 文件,包括模拟器和被测模块
  • 3.使用模拟器运行被测模块,将输出结果保存到文件中
  • 4.将输出结果与预期输出结果进行比较,如果一致则输出 "PASS",否则输出 "FAIL"

在测试流程中,转化的hex文件名称是inst.data

 # 1.将bin文件转成mem文件
    cmd = r'python ../../tools/BinToMem_CLI.py' + ' ' + sys.argv[1] + ' ' + sys.argv[2]
    f = os.popen(cmd)
    f.close()

通过iverilog编译为可执行文件out.vvp(类似verilator),然后执行testbench

    # 2.编译rtl文件
    cmd = r'python ../compile_rtl.py' + r' ../..'
    f = os.popen(cmd)
    f.close()

    # 3.运行
    logfile = open('run.log', 'w')
    vvp_cmd = [r'vvp']
    vvp_cmd.append(r'out.vvp')
    process = subprocess.Popen(vvp_cmd, stdout=logfile, stderr=logfile)
    process.wait(timeout=5)
    logfile.close()

最后和结果进行比较:

  # 4.比较结果
    ref_file = get_reference_file(sys.argv[1])
    if (ref_file != None):
        # 如果文件大小不一致,直接报fail
        if (os.path.getsize('signature.output') != os.path.getsize(ref_file)):
            print('!!! FAIL, size != !!!')
            return
        f1 = open('signature.output')
        f2 = open(ref_file)
        f1_lines = f1.readlines()
        i = 0
        # 逐行比较
        for line in f2.readlines():
            # 只要有一行内容不一样就报fail
            if (f1_lines[i] != line):
                print('!!! FAIL, content != !!!')
                f1.close()
                f2.close()
                return
            i = i + 1
        f1.close()
        f2.close()
        print('### PASS ###')
    else:
        print('No ref file found, please check result by yourself.')

if __name__ == '__main__':
    sys.exit(main())

三,进一步分析

问题:testbench中是如何定义起始pc的地址的(默认为0地址),如何确定ex_end_flag,begin_signature,end_signature设置为ram[4],ram[2],ram[3]呢?

//testbench 关键片段
//...
  wire[31:0] ex_end_flag = tinyriscv_soc_top_0.u_ram._ram[4];
  wire[31:0] begin_signature=tinyriscv_soc_top_0.u_ram._ram[2];
  wire[31:0] end_signature = tinyriscv_soc_top_0.u_ram._ram[3];
//...
 wait(ex_end_flag == 32'h1);  // wait sim end

        fd = $fopen(`OUTPUT);   // OUTPUT的值在命令行里定义
        for (r = begin_signature; r < end_signature; r = r + 4) begin
            $fdisplay(fd, "%x", tinyriscv_soc_top_0.u_rom._rom[r[31:2]]);
        end
        $fclose(fd);
...

为了搞明白这个问题,我们需要看看riscv isa是如何进行测试的

1,riscv-compliance

是一个用于测试RISC-V CPU实现是否符合RISC-V规范的测试套件。这个测试套件由RISC-V International维护,包含了一系列的测试程序,可以检测CPU的指令集、异常处理、内存访问、CSR访问、指令流水线等方面是否符合规范,并生成测试报告。使用帮助文档是:riscv-compliance/doc at master · lowRISC/riscv-compliance (github.com)

项目采用的是旧版本的测试套件,新的测试是:riscv-non-isa/riscv-arch-test (github.com)

需要注意的是这种一致性测试不能替代处理器验证,因为其只是验证处理器是否符合架构规范,但不关注细节,例如,指令操作数的所有可能值或可能寄存器的所有组合。

 This is the RISC-V assembler code that is executed on the processor and that provides results in a defined memory area (the signature)

2.5. The test signature

The test signature is defined as reference data written into memory during the execution of the test. It should record values and results of the operation of the Test. It is expected that an implementation, at the end of a test, dumps the signature in to a file such that only 4-bytes are written per line, starting with the most-significant byte on the left.

2.6. The test reference signature

The test reference signature is the test signature saved from an execution run of the RISC‑V golden model. This is currently from a RISC-V ISS, but the intention is that the RISC-V Formal Model from the RISCV.org Formal Working Group will be used when it is complete, functional, and available.   //spike也可以实现

仓库提供了一些典型的target:比如spike等模拟器,那么如何添加一个新的target/测试逻辑是什么呢?

(1)重新定义riscv-compliance/riscv-target/xxxtarget/中的ISA/src/compliance_test.h 和 binary_coding/src/compliance_test.h

其中定义了一些宏,当使用 #define 定义一个宏时,实际上是在代码中创建了一个类似于函数的东西,可以将其视为一种代码替换机制。格式基本为

#define MACRO_NAME(parameter_list) replacement_text

其中,MACRO_NAME 是宏的名称,parameter_list 是可选的参数列表,replacement_text 是用来替换宏的文本。

需要定义RV_COMPLIANCE_HALT、RV_COMPLIANCE_CODE_BEGIN、RV_COMPLIANCE_DATA_BEGIN、RV_COMPLIANCE_DATA_END

(2)compliance_io.h

  • RVTEST_IO_INIT:初始化输入输出功能。
  • RVTEST_IO_WRITE_STR(_SP, _STR):将字符串 _STR 写入目标环境的输出流,并指定堆栈指针 _SP
  • RVTEST_IO_CHECK():检查目标环境的输出流是否符合预期。
  • RVTEST_IO_ASSERT_GPR_EQ(_SP, _R, _I):检查目标环境中通用寄存器 _R 的值是否等于 _I
  • RVTEST_IO_ASSERT_SFPR_EQ(_F, _R, _I):检查目标环境中单精度浮点寄存器 _F 的值是否等于 _I
  • RVTEST_IO_ASSERT_DFPR_EQ(_D, _R, _I):检查目标环境中双精度浮点寄存器 _D 的值是否等于 _I

 

(2)看看tinyriscv是如何适配的,主要是修改了compilance_test.h,定义了signature的起始和终止地址

//头文件保护措施,防止重复包含头文件
#ifndef _COMPLIANCE_TEST_H
#define _COMPLIANCE_TEST_H

//来自riscv-env
#include "riscv_test.h"

//-----------------------------------------------------------------------
// RV Compliance Macros
//-----------------------------------------------------------------------

#define TESTUTIL_BASE 0x10000000
#define TESTUTIL_ADDR_HALT (TESTUTIL_BASE + 0x10)
#define TESTUTIL_ADDR_BEGIN_SIGNATURE (TESTUTIL_BASE + 0x8)
#define TESTUTIL_ADDR_END_SIGNATURE (TESTUTIL_BASE + 0xc)

//在本例中,TESTUTIL_BASE 的值为 0x10000000, //TESTUTIL_ADDR_HALT 的值为 0x10000010, 
//TESTUTIL_ADDR_BEGIN_SIGNATURE 的值为 0x10000008, 
//TESTUTIL_ADDR_END_SIGNATURE 的值为 0x1000000c。

#define RV_COMPLIANCE_HALT                                                    \
        /* tell simulation about location of begin_signature */               \
        la t0, begin_signature;                                               \
        li t1, TESTUTIL_ADDR_BEGIN_SIGNATURE;                                 \
        sw t0, 0(t1);                                                         \
        /* tell simulation about location of end_signature */                 \
        la t0, end_signature;                                                 \
        li t1, TESTUTIL_ADDR_END_SIGNATURE;                                   \
        sw t0, 0(t1);                                                         \
        /* dump signature and terminate simulation */                         \
        li t0, 1;                                                             \
        li t1, TESTUTIL_ADDR_HALT;                                            \
        sw t0, 0(t1);                                                         \
        RVTEST_PASS                                                           \

#define RV_COMPLIANCE_RV32M                                                   \
        RVTEST_RV32M                                                          \

#define RV_COMPLIANCE_CODE_BEGIN                                              \
        RVTEST_CODE_BEGIN                                                     \

#define RV_COMPLIANCE_CODE_END                                                \
        RVTEST_CODE_END                                                       \

#define RV_COMPLIANCE_DATA_BEGIN                                              \
        RVTEST_DATA_BEGIN                                                     \

#define RV_COMPLIANCE_DATA_END                                                \
        RVTEST_DATA_END                                                       \

#endif

可以看到 TESTUTIL_ADDR_BEGIN_SIGNATURE 的值为 0x10000008, ESTUTIL_ADDR_END_SIGNATURE 的值为 0x1000000c。

按字寻址,每个字是32位,则0x10000008是第二个字,对应ram[2],0x1000000c对应ram[3]。

 

posted @ 2023-04-14 17:48  Haowen_Zhao  阅读(735)  评论(0编辑  收藏  举报