栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Python

Python 简单硬件仿真

Python 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Python 简单硬件仿真

文章目录
  • 说明
  • 实现
    • 实现 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

对照异或的真值表:

aba ^ b
000
011
101
110

说明这个 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 上的代码 中有其它测试。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1001586.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号