vunit-py 0.0.2
Generating testbench written in python for VUnit
Requires Python: >=3.6

# 背景
这个项目起源于个人的硬件项目。

在项目开发过程之中，缺乏一个好的（面向软件从业人员的）单元测试工具非常让人头疼。即使 [VUnit](https://github.com/VUnit/vunit) 提供了一个很好的测试框架，然而用 verilog 写单元测试这件事本身就很令人头疼。那么有没有一个测试框架可以用软件编程语言来描述硬件接口呢？我并没有找到，于是我在 VUnit 基础上，用 python 搭了一个简单的测试框架，提供了最简单的时序和逻辑描述和测试。

# 使用方法
首先安装 [VUnit](https://github.com/VUnit/vunit)，常用方法是运行
```bash
> pip install --user vunit_hdl
```
或参照 VUnit 项目说明。

其次参(co)见(py) [example/tests/adder.py](https://github.com/wwy9/vunit-py/blob/master/example/tests/adder.py)，里面有详细使用方法的实例。说实话看代码比看下面的字要快。

## 创建一个测试用例
一个测试用例包括以下几个方面：模块、事件时钟和信号、测试。
```python
from tests import Test
t = Test("需要测试的模块名",
         "测试用例名",
         "生成文件路径，建议使用 tests/__autogen__，需要事先创建好",
         in_ports=["输入端口名，宽度默认为 1", ("输入端口名", 输入端口宽度)],
         out_ports=["输出端口名，宽度默认为 1", ("输出端口名", 输出端口宽度)],
         parameters={"参数名": 参数值})
t.addEventClock("事件时钟名", [事件间隔], 偏移)
t["输入端口名"] ** "事件时钟名" // 初始值 << [值序列]
t["输出端口名"] ** "事件时钟名" >> [期望值序列]
Test.run([t], ["测试模块源文件所在文件夹"])
```

## 定义事件时钟
一个事件时钟定义为一系列事件的循环。给定这一系列事件的时间间隔，以及整个时钟的偏移，就唯一确定了这些事件的发生时间。由于仿真器的限制，所有时间为负或者为零的事件均视为不发生。

事件时钟示例如下：

| [间隔], 偏移        | 0   | 1   | 2   | 3   | 4   | 5   | 6   | 7   | 8   | 9   | 10  | 11  | 12  | 13  | 14  | 15  |
| ------------------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| [1], 0              |     | x   | x   | x   | x   | x   | x   | x   | x   | x   | x   | x   | x   | x   | x   | x   |
| [1], 3              |     |     |     |     | x   | x   | x   | x   | x   | x   | x   | x   | x   | x   | x   | x   |
| [1, 2], 0           |     | x   |     | x   | x   |     | x   | x   |     | x   | x   |     | x   | x   |     | x   |
| [1, 2], 4           |     |     |     |     |     | x   |     | x   | x   |     | x   | x   |     | x   | x   |
| [1, 2], -4          | x   |     | x   | x   |     | x   | x   |     | x   | x   |     | x   | x   |     | x   | x   |
| [2, 3, 2, 10000], 0 |     |     | x   |     |     | x   |     | x   |     |     |     |     |     |     |     |

## 定义输入信号
一个输入端口必须依附于一个事件时钟，当事件发生时，输入值发生变化。例如如下定义
```python
t["in"] ** "ec" // 0 << [2, 3, 5]
```
意味着输入端口 `in` 的初始值为 0，并且事件时钟 `ec` 的第 0、1、2 次事件发生时，输入信号分别变为 2、3、5。其中 `t["in"]` 返回一个端口，`端口 ** “事件时钟名”` 将输入端口依附于时钟；`端口 // 初始值` 给定端口初始值；`端口 << [序列]` 定义端口输入。

> 注意: 由于以上操作实际上是 python 操作符的重载，因此操作符的优先顺序没有发生变化，同样为 `**` > `//` > `<<`，因此类似 `t["in"] // 0 ** "ec"` 的代码是错误的。由于 `<<` 的优先级比 `+` 低，因此 `t["in"] << [1] + [2]` 和 `t["in"] << [1] * 2` 是安全的，但是由于 `//` 的优先级和 `*` 一致，`t["in"] // [1] + [2]` 和 `t["in"] // [1] * 2` 是错误的。

> 注意：一个端口只可以依附于一个事件时钟，并且只能有一个初始值，因此 `t["in"] ** "ec0" ** "ec1"` 会使端口 `in` 依附于 `ec1`，以最后出现的事件时钟为准；`t["in"] // 0 // 1` 会使端口 `in` 的初始值设置为 1，以最后出现的值为准。如果端口已有输入信号序列，则不能设置（改变）初始值。

> 注意：输入信号是可扩展的，因此 `t["in"] << [0, 1] << [2, 3]` 会使端口 `in` 的输入信号序列变为 `[0 1 2 3]`。

## 定义输出信号
输出端口与输入端口类似，不同的是输出端口不能定义初始值，并且信号序列用大于号定义。输出端口的信号序列用于检查模块输出。例如如下定义
```python
t["out"] ** "ec" >> [2, 3, 5]
```
意味着在事件时钟 `ec` 的第 0、1、2 次事件发生时，检查输出信号是否分别为 2、3、5。如果给定的输出信号序列包含 `x`，则对应事件跳过检查（亦即 `x` 可以对应任意值）。

## 信号序列表示方法
信号序列可以被简单的表示为数组。数组的每个元素可以是整数或者字符串。例如 `[1, 2, 3]`、`["01", "xz"]` 或者 `[1, "xz"]`。如果元素为整数，则需要非负且位宽小于端口宽度；如果元素为字符串，则字符串长度和端口宽度需要一致，并且只能包含 `01xXzZ` 6 种字符。

信号序列也可以被表示为字典。字典的键为事件次数，值可以是整数或者字符串，与数组表示方法里元素的要求一致。例如 `{2: 2, 5: "xz"}`。使用字典表示方法时，信号序列会被填充至字典中最大的事件次数，但是输入和输出端口对应填充值不一致：
- 输出端口：所有位填充 `x`
- 输入端口
  - 如果之前某一时刻有定义：填充为最近的有定义的时刻对应的值
  - 如果之前没有定义（输入序列为空）
    - 如果有初始值：填充为初始值
    - 如果没有初始值：所有位填充 `x`
字典表示方法尤其适用于输出端口，当只需要检测某一时刻的值时，使用字典比使用数组并且手写 `x` 要方便很多。

某些可序列化的信号序列可以被表示为字节。例子包括大部分的单 bit 串行接口。使用字节表示方法时，端口宽度需要为 2 的幂次，字节从前到后从高到低依次展开。例如 `b"\xab"` 对于一个宽度为 1 的端口等效为 `["1", "0", "1", "0", "1", "0", "1", "1"]`；对于一个宽度为 4 的端口等效为 `["1010", "1011"]`。使用字节表示方法时，总位数一定是 8 的倍数，并且无法表示 `x` 和 `z`。

字节表示方法同样可以用于数组中，但是总位数需要和端口宽度完全一致。

## 信号与 python 类型的转换
为了方便用 python 进行逻辑建模，所有的信号序列都是 `List[List[Value]]` 类型。在使用 python 建模时，可以直接操作 `Value` 枚举类型，或者转换为数组表达形式，例如
```python
input = [Value.valuesToInteger(i) for i in t["in"]._input]
output = [Value.valuesToString(o) for o in t["out"]._output]
```
其中 `Value.valuesToString()` 将一个信号转换为字符串；`Value.valuesToInteger()` 将一个仅包含 0 和 1 的信号转换为整数。

# 自问自答
0. 为什么要搞这么一个东西？仿真器不好用吗？

一部分原因是我在 Mac 下用 VSCode 连到 Windows 虚拟机下写代码，所以一般不会真的去打开一个仿真器。再说 python 可以完成一部分计算任务（比如序列化），不需要靠人工去写测试向量和肉眼观察结果。

另外一部分原因是自动化测试脚本是没法使用仿真器界面的，只能靠命令行。

1. 为什么默认 SystemVerilog 而不是 verilog？

因为 VUnit 用到了 SystemVerilog。当然，如果代码是 verilog，那么只需要在 tests.py 里面找到
```verilog
  `include "{module}.sv"
```
这一行，把 `sv` 改成 `v` 即可。

2. 为什么是 python？

因为 VUnit 是用 python 实现的。再说了，python 它不香么？

3. 为什么只有接口测试？内部信号怎么办？

因为我不会 `¯\_(ツ)_/¯`。当然了，感谢 VUnit 的强大功能，使用 `--gui` 参数就可以打开默认的仿真器界面查看波形了。