- 说明
- 实现
- 实现 Wire 类
- 实现 Module 类
- 实现 Nand 类和 next_step() 函数
- 测试
- 其它
这篇文章使用 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 方法,方便计算导线宽度。
把所有方法聚合起来,得到 Wire 类的实现:
class Wire:
name: str | None # 该导线的名字(可以为 None)
_value: str # 该导线上的值(四值逻辑),字符串长度就是导线长度,初始是不定态 x
def __init__(self, name: str = None, width: int = 1):
self.name = name
self._value = 'x' * width
def read(self) -> str:
return self._value
def write(self, data: int | str):
data = str(data)
length = len(data)
width = len(self._value)
if length < width:
data = '0' * (width - length) + data
self._value = data
def width(self):
return len(self._value)
def __getitem__(self, item: int | slice) -> 'Wire':
if isinstance(item, int):
pos = slice(item, item + 1)
elif item.step is not None:
raise ValueError(f"{item} 的步长只能为 1")
else:
start, stop = item.start, item.stop
if start is None:
start = 0
if stop is None:
stop = len(self._value) + 1
pos = slice(start, stop)
return _SubWire(self, pos)
class _SubWire(Wire):
parent: Wire # 父导线
pos: slice # 在父导线中的位置
def __init__(self, parent: Wire, pos: slice):
start, stop = pos.start, pos.stop
super().__init__(f'{parent.name}[{start}: {stop}]', stop - start)
width = len(parent._value)
self.pos = slice(width - stop, width - start) # 因为低位在右,所以要倒着数
self.parent = parent
def write(self, data: int | str):
super().write(data)
parent = self.parent
parent_value = list(parent._value)
parent_value[self.pos] = list(data)
parent._value = ''.join(parent_value)
def read(self) -> str:
return self.parent._value[self.pos]
实现 Module 类
接下来实现 Module 类。
为了实现 next_step() 函数,需要有 flush 方法。它使这个模块中的信号传递一步。它应该递归地调用该模块中所有子模块的 flush 方法。所以 Module 类需要 sub_modules 实例属性。导线的值也要延迟一步再写入,因为模块是通过 Wire.write() 函数写入的数据。因为不同子模块刷新是串行的,所以之前的 Wire.write() 方法就会使刷新时各模块的输入不是同一时刻的。可以这样修改:
all_wires: list['Wire'] = [] # next_step() 时刷新
class Wire:
_new_value: str # 新值,调用 flush() 时会刷新
def __init__(self, name: str = None, width: int = 1):
self.name = name
self._value = 'x' * width
self._new_value = 'x' * width # 改动的代码
all_wires.append(self)
def write(self, data: int | str):
data = str(data)
length = len(data)
width = len(self._value)
if length < width:
data = '0' * (width - length) + data
self._new_value = data # 改动的代码
def flush(self): # 新的方法
self._value = self._new_value
class _SubWire(Wire):
def write(self, data: int | str):
super().write(data)
parent = self.parent
parent_value = list(parent._new_value) # 改动的代码
parent_value[self.pos] = list(data)
parent.write(''.join(parent_value)) # 改动的代码
__init__ 方法应接收任意个关键字参数,并按名字赋值给实例属性。同时要检测位宽,不匹配时报错。没有连接的输入应接上一个永远输出高阻态 z 的导线。之后,调用 build 函数。调用结束后,把 self 追加进父模块的 sub_modules 属性。
Module 类的实现是这样的:
class Module:
__slots__ = 'ports', 'sub_modules'
inputs: port_t
outputs: port_t
ports: dict[str, 'Wire']
sub_modules: list['Module']
def __init__(self, **kwargs: 'Wire'):
global now_module
raw_now = now_module
if now_module is not None:
now_module.sub_modules.append(self)
now_module = self
self.sub_modules = []
ports = self.ports = {}
inputs = self.inputs
outputs = self.outputs
input_names = {tup[0] for tup in inputs}
name2width = {tup[0]: tup[1] for tup in (inputs + outputs)}
names = name2width.keys()
for key, value in kwargs.items():
if key in names:
real, expect = value.width(), name2width[key]
if real != expect:
raise ValueError(f"{key} 对应的值的位宽({real})和期待的位宽({expect})不匹配")
ports[key] = value
else:
raise ValueError(f"{type(self).__name__} 没有名为 {key} 的端口")
for name in input_names - set(kwargs.keys()):
new = Wire(f'name} port>')
new.write('z')
ports[name] = new
self.build()
now_module = raw_now
def flush(self):
for m in self.sub_modules:
m.flush()
# 刷新输出,使输出可以立即变化
ports = self.ports
for tup in self.outputs:
output = tup[0]
if output in ports.keys():
ports[output].flush()
def build(self):
raise NotImplementedError
def __getattr__(self, item: str):
ports = self.ports
if item in ports.keys():
return ports[item]
raise ValueError(f"模块 {type(self).__name__} 中没有名为 {item} 的端口")
实现 Nand 类和 next_step() 函数
有了 Module 类,Nand 类也可简单地写出:
class Nand(Module):
inputs = ('a', 1), ('b', 1)
outputs = ('out', 1),
def build(self):
pass
def flush(self):
a, b = self.a.read(), self.b.read()
write = self.out.write
match a, b:
case ('0', _) | (_, '0'):
write('1')
case ('1', 'x') | ('x', '1'):
write('x')
case ('x', 'x') | ('x', 'z') | ('z', 'x'):
write('x') # TODO: 是否要像 logisim 那样输出 E?
case ('z', 'z'):
write('z')
case ('1', '1') | ('1', 'z') | ('z', '1'):
write('0')
case _:
raise ValueError(f"未知 a, b 值:{a, b}")
next_step 函数很简单:
def next_step(m: Module):
for wire in all_wires:
wire.flush()
m.flush()
测试
运行开头的测试,看看输出:
x 0 0 end: a=0, b=0, out=0 0 1 1 end: a=0, b=1, out=1 1 1 1 end: a=1, b=0, out=1 1 1 0 end: a=1, b=1, out=0
对照异或的真值表:
| a | b | a ^ b |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
说明这个 Xor 的搭建无误。
其它还记得我们将 Wire 类实现成了多位的导线吗?可以试一试。比如,这是一个 16 位的异或门和它的测试:
class Xor16(Module):
inputs = ('a', 16), ('b', 16)
outputs = ('out', 16),
def build(self):
a = self.a
b = self.b
out = self.out
for i in range(16):
Xor(a=a[i], b=b[i], out=out[i])
def testbench():
def next_and_show():
next_step(test)
print(out.read())
def to_binary(value: int):
res = ''
while value:
res = str(value & 1) + res
value >>= 1
return res
a = Wire('a', 16)
b = Wire('b', 16)
out = Wire('out', 16)
test = Xor16(a=a, b=b, out=out)
test_values = (0b100, 0b1000), (0x1234, 0x1111), (0x1101, 0xffff)
for a_, b_ in test_values:
a_ = to_binary(a_)
b_ = to_binary(b_)
a.write(a_)
b.write(b_)
next_and_show()
next_and_show()
next_and_show()
print(f"end: a=16'b{a.read()}, b=16'b{b.read()}, out=16'b{out.read()}")
这里使用了循环来生成一位异或门。程序不能感觉到使用了循环,只觉得和放 16 个定义是一样的。
运行测试的输出是:
xxxxxxxxxxxxxxxx 000000000000xx00 0000000000001100 end: a=16'b0000000000000100, b=16'b0000000000001000, out=16'b0000000000001100 0000000000001100 0001001100110101 0000001100100101 end: a=16'b0001001000110100, b=16'b0001000100010001, out=16'b0000001100100101 0000001100100101 1110111111101111 1110111011111110 end: a=16'b0001000100000001, b=16'b1111111111111111, out=16'b1110111011111110
在 Github 上的代码 中有其它测试。



