Skip to content

搭建一个简单的 UT 环境

Verilua 的启动速度很快,并且运行时足够轻量,能够用来搭建 Unit Test(UT)环境,测试一些中小规模的硬件模块。

在本节中,我们将介绍如何基于 Verilua 搭建一个简单的 UT 环境,所有的代码可以在这里找到。

1. 编写 UT 环境代码

我们可以编写一个简单的 UT 环境代码,它将包含一些实际验证中较为常用的函数,具体可以看下面的代码片段:

env.lua
local clock = dut.clock:chdl()
local reset = dut.reset:chdl()

local function posedge(...)
    clock:posedge(...)
end

local function negedge(...)
    clock:negedge(...)
end

local function dut_reset(reset_cycles)
    reset:set_imm(1)
    clock:posedge(reset_cycles or 10)
    reset:set_imm(0)
end

local function expect_happen_until(limit_cycles, func)
    assert(type(limit_cycles) == "number")
    assert(type(func) == "function")
    local ok = clock:posedge_until(limit_cycles, func)
    assert(ok)
end

local function expect_not_happen_until(limit_cycles, func)
    assert(type(limit_cycles) == "number")
    assert(type(func) == "function")
    local ok = clock:posedge_until(limit_cycles, func)
    assert(not ok)
end

local test_case_count = 0

local function TEST_SUCCESS()
    print("total_test_cases: <" .. test_case_count .. ">\n")
    print(">>>TEST_SUCCESS!<<<")

    local ANSI_GREEN = "\27[32m"
    local ANSI_RESET = "\27[0m"

    print(ANSI_GREEN .. [[
  _____         _____ _____ 
 |  __ \ /\    / ____/ ____|
 | |__) /  \  | (___| (___  
 |  ___/ /\ \  \___ \\___ \ 
 | |  / ____ \ ____) |___) |
 |_| /_/    \_\_____/_____/ 
]] .. ANSI_RESET)

    io.flush()
    sim.finish()
end

-- 
-- Test case management
-- 
local function register_test_case(case_name)
    assert(type(case_name) == "string")

    return function(func_table)
        assert(type(func_table) == "table")
        assert(#func_table == 1)

        assert(type(func_table[1]) == "function")
        local func = func_table[1]

        local new_env = {
            print = function(...) print("|", ...) end,
            printf = function(...) io.write("|\t" .. string.format(...)) end,
        }

        setmetatable(new_env, { __index = _G })
        setfenv(func, new_env)

        return function (...)
            print(string.format([[
-----------------------------------------------------------------
| [%d] start test case ==> %s
-----------------------------------------------------------------]], test_case_count, case_name))

            -- Execute the test case
            func(...)

            print(string.format([[
-----------------------------------------------------------------
| [%d] end test case ==> %s
-----------------------------------------------------------------]], test_case_count, case_name))

            test_case_count = test_case_count + 1
        end
    end
end

return {
    posedge = posedge,
    negedge = negedge,
    dut_reset = dut_reset,
    expect_happen_until = expect_happen_until,
    expect_not_happen_until = expect_not_happen_until,
    register_test_case = register_test_case,
    TEST_SUCCESS = TEST_SUCCESS,
}

上述代码片段提供了如下的函数:

  • env.posedge(...) / env.negedge(...)

    对全局的 clock 这个 chdlposedge 函数进行封装,其使用方式和 <chdl>:posedge(...) / <chdl>:negedge(...) 是一样的。

  • env.dut_reset(reset_cycles)

    对 DUT 进行复位,其中会对 reset 信号进行赋值,可以通过 reset_cycles 来指定复位的周期。

  • env.expect_happen_until(limit_cycles, func)

    检查 funclimit_cycles 周期内是否发生,如果发生则立即返回,否则会触发 assert 错误,这在具体编写验证代码的时候比较常用,用来检查特定信号是否在预期时间内发生。

  • env.expect_not_happen_until(limit_cycles, func)

    env.expect_happen_until(limit_cycles, func) 作用相反。

  • env.TEST_SUCCESS()

    用来打印一个显眼的信息到 Terminal 上,表示测试已经成功结束。

  • env.register_test_case(case_name)

    注册一个测试用例,其中 case_name 是测试用例的名称,返回一个被注册的测试用例函数。使用示例如下:

    local env = require "env"
    
    local some_test_case = env.register_test_case "name of the test case" {
        -- Test case body
        function ()
            -- Do something
        end
    }
    
    fork {
        function ()
            env.dut_reset()
    
            -- Execute the test case
            some_test_case()
    
            env.TEST_SUCCESS()
            sim.finish()
        end
    }
    

通过上面这个简单的 env.lua 模块,就能为 UT 测试创建一个简易的验证环境。

2. 编写 UT 测试主体

接下来需要编写 UT 的具体业务代码(一个 lua 文件),这里同样以一个 Counter 模块为例:

Counter.v
module Counter(
    input  wire clock,
    input  wire reset,
    input  wire incr,
    output wire [7:0] value
);

reg [7:0] value_reg;

always@(posedge clock) begin
    if (reset) 
        value_reg <= 8'd0;
    else if (incr == 1'b1) begin
        value_reg <= value_reg + 1'b1;
    end 
end

assign value = value_reg;

endmodule

那么上述设计的 UT 业务代码可以写成这样:

test_counter.lua
local env = require "env"

local test_value_incr = env.register_test_case "test value incr" {
    function ()
        env.dut_reset()

        env.posedge()
            dut.incr:set(1)

        env.posedge()
            dut.value:expect(0)

        env.posedge()
            dut.value:expect(1)
            dut.incr:set(0)

        env.posedge()
            dut.value:expect(2)

        env.posedge()
            dut.value:expect(2)
    end
}

local test_value_no_incr = env.register_test_case "test value no incr" {
    function ()
        env.dut_reset()

        env.posedge()
            dut.incr:set(0)

        env.expect_not_happen_until(1000, function ()
            return dut.value:is_not(0)
        end)
    end
}

local test_value_overflow = env.register_test_case "test value overflow" {
    function ()
        env.dut_reset()

        env.posedge()
            dut.incr:set(1)

        env.expect_happen_until(300, function()
            return dut.value:get() == 255
        end)
    end
}

fork {
    function ()
        env.dut_reset()

        test_value_incr()
        test_value_no_incr()
        test_value_overflow()

        env.TEST_SUCCESS()
        sim.finish()
    end
}

这里我们写了三个测试用例:(1)test value incr,(2)test value no incr,(3)test value overflow。并在 fork 中启动了这三个测试用例。

3. 编写 xmake.lua

对于 HVL 场景,我们都需要编写一个 xmake.lua 文件来管理整个工程。

xmake.lua
1
2
3
4
5
6
7
8
9
target("test_counter")
    add_rules("verilua")
    add_toolchains("@verilator")

    add_files("env.lua")
    add_files("Counter.v")

    set_values("cfg.lua_main", "./test_counter.lua")
    set_values("cfg.top", "Counter")

4. 执行测试

执行下面的命令即可编译并进行测试,这里如果 RTL 代码没有修改则只需要编译一次,修改 Lua 代码并不需要重新编译。

xmake build -P . test_counter

xmake run -P . test_counter

如果所有的测试用例都测试成功,那么就会打印一个成功的提示信息,并调用 sim.finish() 来结束仿真。命令行打印的信息如下所示:

Terminal
-----------------------------------------------------------------
| [0] start test case ==> test value incr
-----------------------------------------------------------------
-----------------------------------------------------------------
| [0] end test case ==> test value incr
-----------------------------------------------------------------
-----------------------------------------------------------------
| [1] start test case ==> test value no incr
-----------------------------------------------------------------
-----------------------------------------------------------------
| [1] end test case ==> test value no incr
-----------------------------------------------------------------
-----------------------------------------------------------------
| [2] start test case ==> test value overflow
-----------------------------------------------------------------
-----------------------------------------------------------------
| [2] end test case ==> test value overflow
-----------------------------------------------------------------
total_test_cases: <3>

>>>TEST_SUCCESS!<<<
  _____         _____ _____ 
 |  __ \ /\    / ____/ ____|
 | |__) /  \  | (___| (___  
 |  ___/ /\ \  \___ \\___ \ 
 | |  / ____ \ ____) |___) |
 |_| /_/    \_\_____/_____/