说明
这篇文章使用 Python 的语法实现一个简单的硬件仿真器。只能通过实例化模块构建电路,Verilog 等 HDL 中的高级语法都用不了。
Nand 是与非门,只需使用它,任何逻辑门都可以搭建。所以,我们决定这个硬件仿真器中,与非门是最基础的元件,其它模块通过实例化它构建电路。
目标:实现这样的语法:
from hardware_sim_simple import * # 要实现的模块
class Xor(Module): # 每个模块都要继承自 Module
inputs = ('a', 1), ('b', 1)
outputs = ('out', 1),
def build(self):
# 输入
a = self.a
b = self.b
# 输出
out = self.out
# 内部导线
wire0 = Wire()
wire1 = Wire()
wire2 = Wire()
# 构造
Nand(a=a, b=b, out=wire0) # Nand 是基础门,无需搭建
Nand(a=a, b=wire0, out=wire1)
Nand(a=b, b=wire0, out=wire2)
Nand(a=wire1, b=wire2, out=out)
这段代码定义了一个异或门的结构。它的输入由 inputs
类变量指出:输入有两个,一个是 a,位宽是 1;另一个是 b,位宽也是 1。输出由 outputs
指出(为了使它成为一个元组,需要在末尾有一个逗号。如果在 Module.__init_subclass__
中写了类型转换的代码,则末尾的逗号不需要,但这里没有):输出有一个,是 out,位宽是 1。
build
方法用于构建电路,每个模块初始化时会调用它。
self.a
,self.b
,self.out
用于获取该模块的输入 a、 b 和输出 out,将它们赋值给同名变量可以节省打字时间。Wire()
用于生成一条导线,这里生成了 wire0
- wire2
,用于承载内部数据。“构造”部分是模块的核心部分(之前的只是固定搭配),描述了电路内部的连接。实例化了 4 个与非门。实例化的方式是 <模块名>(<端口 1>=<导线 1>, <端口 2>=<导线 2>, ..., <端口 n>=<导线 n>
的关键字传参方式。以第一个与非门为例,例化它的语句是:
Nand(a=a, b=b, out=wire0)
这表示,实例化一个与非门,把它的 a 输入连到导线 a(异或门的一个输入),b 输入连到 b(异或门的另一个输入),out 输出连到导线 wire0(内部导线)。这条语句的电路图是:
+-------+
a --+ | nand |
+--+a |
| out+------- wire0
+--+b |
b --+ | |
+-------+
测试这个模块,使用这样的方法:
def testbench():
def next_and_show():
next_step(test) # 使数据传递一步
print(out.read()) # out.read() 读取导线 out 上的信息
a = Wire()
b = Wire()
out = Wire()
test = Xor(a=a, b=b, out=out) # 要测试的模块
values = '01'
for a_ in values:
for b_ in values:
a.write(a_) # 把 a_ 写入导线 a
b.write(b_)
next_and_show() # 因为这个 Xor 有三级门延迟,所以需要三个
next_and_show()
next_and_show()
print(f'end: a={
a_}, b={
b_}, out={
out.read()}')
if __name__ == '__main__':
testbench()
与多数事件驱动的 HDL 不同,这个需要手动驱动。这样优点是可以更清楚地了解模块的门延迟,缺点显而易见。
实现
下面,探究如何实现。应该实现 Module、 Nand(继承自 Module)、 Wire 三个类和 next_step() 函数。
实现 Wire 类
我们首先实现 Wire 类。这个类应该实现的方法已经基本明了了,但是还应该有一个方法,在多位总线上使用。这个方法就是索引和切片(均为 __getitem__
),用于这样的代码:
Xor(a=a[0], b=b[0], out=out[0])
# 假如 Xor16 是输入输出位宽都为 16 位的异或门
Xor(a=a[0:16], b=b[16:32], out=out)
# 注意和 Verilog 不同,低位在前,高位在后,使用 Python 的切片表示法
所以,这个方法的返回值也应该是导线。向这根子导线写入时,父导线的值也会被改变。
我们还需要一个 width
方法,方便计算导线宽度。