如何制造一个CPU(1.2):VHDL单元测试框架VUnit,以及利用Github的持续集成功能

1 VUnit

我现在觉得像之前那样测试实在太慢了,于是便打算参照软件工程中学到的思想,来对每个模块进行单元测试。于是我找到了Vunit单元测试框架,感觉挺好用的。这里是项目文档,我觉得它的文档写得并不是很好,但参见代码里自带的例子可以学得很快。

1.1 VUnit测试方法举例

1.1.1 代码结构

以现在的PC模块为例:

/**
IF模块
Encoding: UTF-8 
**/

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_SIGNED.ALL;

use WORK.INCLUDE.ALL;

entity PC is
    Port ( rst:                             in  STD_LOGIC;                                              -- Reset
           clk:                             in  STD_LOGIC;                                              -- Clock
           pause_i :                        in  STD_LOGIC_VECTOR(CTRL_PAUSE_LEN-1 downto 0);            -- input pause info from PAUSE_CTRL
           branch_i:                        in  STD_LOGIC;                                              -- input 是否跳转 from ID
           branch_target_addr_i:            in  STD_LOGIC_VECTOR(ADDR_LEN-1 downto 0);                  -- input 跳转目的地址 from ID
           new_pc_i :                       in  STD_LOGIC_VECTOR(ADDR_LEN-1 downto 0);
           new_pc_en_i:                     in  STD_LOGIC;
           en_o:                            out STD_LOGIC;                                              -- output RAM读指令使能 to MMU
           pc_o:                            out STD_LOGIC_VECTOR(ADDR_LEN-1 downto 0));                 -- output RAM读指令地址 to MMU
end PC;

architecture Behavioral of PC is
begin
    process (clk)
        variable is_start: STD_LOGIC;
    begin
        if rising_edge(clk) then
            if rst = RST_ENABLE then
                en_o <= CHIP_DISABLE;
                pc_o <= x"00000000";
                is_start := '1';
            else
                en_o <= CHIP_ENABLE;
                if pause_i(PC_PAUSE_INDEX) = PAUSE_NOT then
                    if branch_i = BRANCH then
                        pc_o <= branch_target_addr_i;
                    elsif new_pc_en_i = '1' then
                        pc_o <= new_pc_i;
                    else
                        if is_start = '0' then
                            pc_o <= pc_o + x"00000004";  -- IEEE.STD_LOGIC_SIGNED library
                        else
                            pc_o <= x"80000000";
                            is_start := '0';
                        end if;
                    end if;
                end if;
                -- 暂停时输出不变
            end if;
        end if;
    end process;

end Behavioral;

针对这个模块,我写了这样一个单元测试:测试Reset的功能(tb_PC_rst.vhd)

library vunit_lib;
context vunit_lib.vunit_context;

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_SIGNED.ALL;

use WORK.INCLUDE.ALL;

entity tb_PC_rst is
  generic (runner_cfg : string);
end entity;

architecture tb of tb_PC_rst is

component PC
    Port ( rst:                             in  STD_LOGIC;                                              -- Reset
           clk:                             in  STD_LOGIC;                                              -- Clock
           pause_i :                        in  STD_LOGIC_VECTOR(CTRL_PAUSE_LEN-1 downto 0);            -- input pause info from PAUSE_CTRL
           branch_i:                        in  STD_LOGIC;                                              -- input 是否跳转 from ID
           branch_target_addr_i:            in  STD_LOGIC_VECTOR(ADDR_LEN-1 downto 0);                  -- input 跳转目的地址 from ID
           new_pc_i :                       in  STD_LOGIC_VECTOR(ADDR_LEN-1 downto 0);
           new_pc_en_i:                     in  STD_LOGIC;
           en_o:                            out STD_LOGIC;                                              -- output RAM读指令使能 to MMU
           pc_o:                            out STD_LOGIC_VECTOR(ADDR_LEN-1 downto 0));                 -- output RAM读指令地址 to MMU
end component;

signal rst: STD_LOGIC;
signal clk: STD_LOGIC;
signal pause: STD_LOGIC_VECTOR(CTRL_PAUSE_LEN-1 downto 0);
signal branch: STD_LOGIC;
signal branch_target_addr: STD_LOGIC_VECTOR(ADDR_LEN-1 downto 0);
signal new_pc: STD_LOGIC_VECTOR(ADDR_LEN-1 downto 0);
signal new_pc_en: STD_LOGIC;
signal en: STD_LOGIC;
signal pc_o: STD_LOGIC_VECTOR(ADDR_LEN-1 downto 0);

begin

    PC_0: PC port map(
        rst => rst, clk => clk, pause_i => pause,
        branch_i => branch, branch_target_addr_i => branch_target_addr, 
        new_pc_i => new_pc, new_pc_en_i => new_pc_en,
        en_o => en, pc_o => pc_o);

    process
    begin
        clk <= '0';
        wait for 10 ns;
        clk <= '1';
        wait for 10 ns;
    end process;

    test_runner : process
    begin
        test_runner_setup(runner, runner_cfg);

        wait until clk = '0';
        log(to_string(clk));
        rst <= '1';
        branch <= '0';
        branch_target_addr <= x"00000000";
        wait until clk = '1';
        wait for 5 ns;
        log("clk = " & to_string(clk));
        log("en = " & to_string(en));
        log("pc_o =" & to_string(pc_o));
        check(en = '0', "Expect en_o=0 when rst is set", failure);
        check(pc_o = "00000000000000000000000000000000", "Expect pc_o=x80000000 when rst is set", failure);

        test_runner_cleanup(runner);
    end process test_runner;
end architecture;

然后在同目录下建立run.py文件:

import os
from vunit import VUnit
 
test_root = os.path.abspath('..') + "/srcs"
cur_root = os.path.abspath('.')
 
ui = VUnit.from_argv()
lib = ui.add_library("lib")
lib.add_source_files(test_root + "/PC.vhd")
lib.add_source_files(test_root + "/include.vhd")
lib.add_source_files(cur_root + "/*.vhd")
ui.main()

其中,文件目录结构是这样的:

  • base_dir
    • srcs
      • PC.vhd
      • include.vhd
      • 其他项目文件
    • unit_test
      • run.py
      • tb_PC_rst.vhd

1.1.2 编译及输出

base_dir/unit_test目录下运行:

python run.py -v lib.tb_PC_rst*

打印出如下结果(忽略了编译输出):

Running test: lib.tb_pc_rst.all
Running 1 tests

Starting lib.tb_pc_rst.all
Output file: vunit_out\test_output\lib.tb_pc_rst.all_a0102e501ec34d01518a2fa79a66b9e2ad66433d\output.txt
# vsim -modelsimini C:/Users/lenovo/Documents/GitHub/NewThinpadProject/unit_test/vunit_out/modelsim/modelsim.ini -wlf C:/Users/lenovo/Documents/GitHub/NewThinpadProject/unit_test/vunit_out/test_output/lib.tb_pc_rst.all_a0102e501ec34d01518a2fa79a66b9e2ad66433d/modelsim/vsim.wlf -quiet -t ps -onfinish stop lib.tb_pc_rst(tb) -L vunit_lib -L lib -g/tb_pc_rst/runner_cfg="active python runner : true,enabled_test_cases : ,output path : C::/Users/lenovo/Documents/GitHub/NewThinpadProject/unit_test/vunit_out/test_output/lib.tb_pc_rst.all_a0102e501ec34d01518a2fa79a66b9e2ad66433d/,tb path : C::/Users/lenovo/Documents/GitHub/NewThinpadProject/unit_test/"
# Start time: 15:23:34 on Feb 03,2018
# ** Warning: Design size of 12121 statements exceeds ModelSim-Intel FPGA Starter Edition recommended capacity.
# Expect performance to be adversely affected.
# 0
# clk = 1
# en = 0
# pc_o =00000000000000000000000000000000
# Break in Subprogram vunit_stop at C:/Users/lenovo/AppData/Local/Programs/Python/Python35/lib/site-packages/vunit/vhdl/core/src/stop_body_2008.vhd line 10
# Stopped at C:/Users/lenovo/AppData/Local/Programs/Python/Python35/lib/site-packages/vunit/vhdl/core/src/stop_body_2008.vhd line 10
pass (P=1 S=0 F=0 T=1) lib.tb_pc_rst.all (4.2 seconds)

==== Summary =============================
pass lib.tb_pc_rst.all (4.2 seconds)
==========================================
pass 1 of 1
==========================================
Total time was 4.2 seconds
Elapsed time was 4.6 seconds
==========================================
All passed!

全部测试通过。

更复杂的写法请参见VUnit的文档。

2 Github持续集成

2.1 添加Build Status

添加Build Status使用的是Travis-CI

2.1.1 注册

首先进入Travis-CI的官网,然后点击右上角的“Sign in with Github”按钮,将Github账号授权给travis-ci。
travis-ci-index.png

然后在Profile页面打开要进行持续集成的项目,对我来说是zhanghuimeng/NewThinpadProject。然后在项目根目录下添加.travis.yml,Travis-CI会按照.travis.yml里的内容进行构建。之后提交.travis.yml到 Github,就会自动触发持续集成,你可以到travis-ci-status中查看结果。

2.1.2 填写.travis.yaml

然后我发现Travis不支持VHDL,只好采用一些比较曲折的方法(如果能成功的话)。网上也找到了一些方法:

  • Develop the directory structure and testing infrastructure for CoreLib:这是VHDL的官方repo之一,里面给出了一个先安装GHDL再用VUnit进行测试的方法,我采用的就是这种方法,几乎直接抄他的.travis.yml就可以了。这种方法的问题是,如果需要用到Vivado的IP核,就需要把IP核单独编译出来之类的。在Travis CI提供的虚拟机上直接安装Vivado几乎是不可行的,因为有20多G,肯定会超出一个小时。
  • Vivado-CI:这个方法简单粗暴,只需在本机或者其他的什么安装了Vivado的主机上安装服务器端程序来帮助进行编译和仿真就可以了。我没有尝试,因为当前的电脑上没有安装Vivado。
  • Dockerizing GHDL as a step towards the libre and portable VHDL design framework:这个看起来十分复杂,似乎用到了DockerHub,没有看懂具体是怎么实现的。

2.1.3 得到徽章

现在就可以把徽章填在README.md上了!当然,通过图片链接贴在别的地方也可以,比如下面。

NewThinpadProject.svg?branch=simulation

2.2 添加Coverage徽章

2.2.1 开启账号

2.2.2 崩溃

我在网上找了很久,只找到用ghdl加上lcov的flag再编译,然后再用gcov或lcov的处理工具对输出结果进行处理的方法,但这种方法要求用的GHDL后端是用GCC/GMAT编译的。之前Travis CI上的后端是用LLVM编译的,但反正现在它也崩了。我手里只有一个mcode编译的版本,而且旧得不行。(之前一直是用免费版的ModelSim仿真的)至于在Windows环境上用GCC编译GHDl,作者表示:

Under investigation on how to build that beast.

另一种方法是用VUnit自带的Coverage工具,但它只能用于ModelSim(其他仿真器的Coverage正在开发中),而且因为这是一项实验功能,我也跑不出来Coverage。

现在最大的问题是,即使我revert到之前能运行的版本,Travis CI的虚拟机中的ruby也崩了,真是不明所以。(所以我也真好意思把build failing和coverage unknown的徽章贴在上面……)

2.2.3 得到徽章

3 参考文献


新增一则回应

除非特别注明,本页内容采用以下授权方式: Creative Commons Attribution-ShareAlike 3.0 License