MyHDL中文手册(七)—— 高层次建模

介绍

要用MyHDL编写可综合的模型,您应该坚持使用RTL建模中展示的RTL模板。然而,MyHDL中的建模功能要强大得多。从概念上讲,MyHDL是一个用于硬件系统的通用事件驱动建模和仿真的库。
为什么要在比RTL更高的抽象级别上建模?原因有很多。例如,您可以使用MyHDL来验证体系结构特性,例如系统吞吐量、延迟和缓冲区大小。您还可以为依赖于特定技术的功能内核编写高级模型,而不需综合。最后但并非最不重要的一点是,您可以使用MyHDL编写验证系统模型或可综合电路的测试平台。
本章探讨了使用MyHDL进行高级建模的一些选项。

总线函数过程建模

总线函数过程是对在物理接口上实现某种抽象事务所需的低级操作的可重用封装。总线函数过程通常用于灵活的验证环境。总线函数表示它仿佛实现了类似某种协议总线的功能,可以随时被测试环境调用。“过程”强调调用需要花费一段时间。

同样,MyHDL使用生成器函数来支持总线函数过程。在MyHDL中,实例和总线函数过程调用之间的区别来自于生成器函数的使用方式。

作为一个例子,我们将设计一个简化的UART发射机的总线功能程序。我们假设8个数据位、无奇偶校验位和一个停止位,并添加PRINT语句以遵循仿真行为:

T_9600 = int(1e9 / 9600)

def rs232_tx(tx, data, duration=T_9600):

    """ Simple rs232 transmitter procedure.

    tx -- serial output data
    data -- input data byte to be transmitted
    duration -- transmit bit duration

    """

    print "-- Transmitting %s --" % hex(data)
    print "TX: start bit"
    tx.next = 0
    yield delay(duration)

    for i in range(8):
        print "TX: %s" % data[i]
        tx.next = data[i]
        yield delay(duration)

    print "TX: stop bit"
    tx.next = 1
    yield delay(duration)

这看起来与前面几节中的生成器函数完全相同。当我们以新的方式使用它时,它就变成了一个总线函数过程。假设在一个测试平台中,我们想要生成一些要传输的数据字节。可以按以下方式进行建模:

testvals = (0xc5, 0x3a, 0x4b)

def stimulus():
    tx = Signal(1)
    for val in testvals:
        txData = intbv(val)
        yield rs232_tx(tx, txData)

我们使用总线函数调用作为yield语句中的子句。这将引入第四种形式的yield语句:使用生成器作为子句。虽然这是一种比前面的情况更动态的用法,但其含义实际上非常相似:在该时刻,外部生成器应该等待内部生成器的完成。在这种情况下,当rs232_tx(tx,txData)生成器返回时,外部生成器将恢复。
仿真此过程时,我们会得到:

-- Transmitting 0xc5 --
TX: start bit
TX: 1
TX: 0
TX: 1
TX: 0
TX: 0
TX: 0
TX: 1
TX: 1
TX: stop bit
-- Transmitting 0x3a --
TX: start bit
TX: 0
TX: 1
TX: 0
TX: 1
...

我们继续设计相应的UART接收器总线函数过程。这将使我们能够进一步介绍MyHDL的功能及其对yield语句的使用。

到目前为止,yield语句只有一个子句。但是,它们也可以有多个子句。在这种情况下,只要满足其中一个子句指定的等待条件,生成器就会恢复。这与Verilog和VHDL中灵敏度列表的功能相对应。

例如,假设我们要设计一个带超时的UART接收过程。我们可以在等待开始位时指定超时条件,如以下生成器函数所示:

def rs232_rx(rx, data, duration=T_9600, timeout=MAX_TIMEOUT):

    """ Simple rs232 receiver procedure.

    rx -- serial input data
    data -- data received
    duration -- receive bit duration

    """

    # wait on start bit until timeout
    yield rx.negedge, delay(timeout)
    if rx == 1:
        raise StopSimulation, "RX time out error"

    # sample in the middle of the bit duration
    yield delay(duration // 2)
    print "RX: start bit"

    for i in range(8):
        yield delay(duration)
        print "RX: %s" % rx
        data[i] = rx

    yield delay(duration)
    print "RX: stop bit"
    print "-- Received %s --" % hex(data)

如果触发超时条件,则接收位rx仍为1。在这种情况下,我们提出一个异常来停止仿真。StopSimulation异常是在MyHDL中为此目的预定义的。在另一种情况下,我们将采样点定位在位持续时间的中间,并对接收到的数据位进行采样。

当一个yield语句有多个子句时,它们可以是作为合法单个子句支持的任何类型,包括生成器。例如,我们可以通过将发射器和接收器生成器放在一起来相互验证,如下所示:

def test():
    tx = Signal(1)
    rx = tx
    rxData = intbv(0)
    for val in testvals:
        txData = intbv(val)
        yield rs232_rx(rx, rxData), rs232_tx(tx, txData)

两个forked生成器将同时运行,一旦其中一个生成器完成(在这种情况下,它将是发送器),就会恢复外部的生成器。仿真输出显示UART过程如何步调一致地运行

-- Transmitting 0xc5 --
TX: start bit
RX: start bit
TX: 1
RX: 1
TX: 0
RX: 0
TX: 1
RX: 1
TX: 0
RX: 0
TX: 0
RX: 0
TX: 0
RX: 0
TX: 1
RX: 1
TX: 1
RX: 1
TX: stop bit
RX: stop bit
-- Received 0xc5 --
-- Transmitting 0x3a --
TX: start bit
RX: start bit
TX: 0
RX: 0
...

为了完整起见,我们使用将rx与tx信号断开连接(断开环回)的测试平台来验证超时行为,并为接收过程指定一个小一点的超时:

def testTimeout():
    tx = Signal(1)
    rx = Signal(1)
    rxData = intbv(0)
    for val in testvals:
        txData = intbv(val)
        yield rs232_rx(rx, rxData, timeout=4*T_9600-1), rs232_tx(tx, txData)

仿真这次会停止,在几个传输周期后出现超时异常:

-- Transmitting 0xc5 --
TX: start bit
TX: 1
TX: 0
TX: 1
StopSimulation: RX time out error

回想一下,只要有一个forked生成器返回,原始生成器就会恢复。在之前的情况下,这是很好的,因为发射机和接收机运行的步调一致。但是,可能需要仅在所有分叉生成器完成后才恢复调用者。例如,假设我们想要表征发射机和接收机设计对比特持续时间差异的稳健性。我们可以按如下方式调整我们的测试平台,以更快的速度运行发射机:

T_10200 = int(1e9 / 10200)

def testNoJoin():
    tx = Signal(1)
    rx = tx
    rxData = intbv(0)
    for val in testvals:
        txData = intbv(val)
        yield rs232_rx(rx, rxData), rs232_tx(tx, txData, duration=T_10200)

仿真此操作将显示新字节的传输是如何在接收到前一个字节之前开始的,这可能会导致额外的传输错误:

-- Transmitting 0xc5 --
TX: start bit
RX: start bit
...
TX: 1
RX: 1
TX: 1
TX: stop bit
RX: 1
-- Transmitting 0x3a --
TX: start bit
RX: stop bit
-- Received 0xc5 --
RX: start bit
TX: 0

更有可能的是,我们希望逐个字节地描述设计,并在传输每个字节之前对齐两个生成器。在MyHDL中,这是通过join函数完成的。通过在yield语句中将子句组合在一起,我们创建了一个仅当其所有子句参数都已触发时才触发的新子句。例如,我们可以调整测试工作台,如下所示:

def testJoin():
    tx = Signal(1)
    rx = tx
    rxData = intbv(0)
    for val in testvals:
        txData = intbv(val)
        yield join(rs232_rx(rx, rxData), rs232_tx(tx, txData, duration=T_10200))

现在,新字节的传输仅在完整收到前一个字节后才开始:

-- Transmitting 0xc5 --
TX: start bit
RX: start bit
...
TX: 1
RX: 1
TX: 1
TX: stop bit
RX: 1
RX: stop bit
-- Received 0xc5 --
-- Transmitting 0x3a --
TX: start bit
RX: start bit
TX: 0
RX: 0

使用内置类型对内存进行建模

Python具有强大的内置数据类型,可用于对硬件内存进行建模。仅仅需要给某些数据类型操作包装特定的接口。

例如,字典可以方便地对稀疏内存结构进行建模(在其他语言中,此数据类型称为关联数组或哈希表)。稀疏存储器是指在特定的应用程序或仿真中只使用一小部分地址的存储器。与静态分配可能较大的完整地址空间不同,动态分配所需的存储空间会是更好的做法。这正是字典所提供的。以下是稀疏内存模型的示例:

def sparseMemory(dout, din, addr, we, en, clk):

    """ Sparse memory model based on a dictionary.

    Ports:
    dout -- data out
    din -- data in
    addr -- address bus
    we -- write enable: write if 1, read otherwise
    en -- interface enable: enabled if 1
    clk -- clock input

    """

    memory = {}

    @always(clk.posedge)
    def access():
        if en:
            if we:
                memory[addr.val] = din.val
            else:
                dout.next = memory[addr.val]

    return access

注意我们如何使用din信号的val属性,因为我们不想存储信号对象本身,而是存储它的当前值。同样,我们使用addr信号的val属性作为字典键。

在许多情况下,当没有歧义时,MyHDL代码会自动使用信号的当前值:例如,在表达式中使用信号时。但是,在其他情况下,例如在本例中,您必须显式地引用该值:例如,当信号用作字典键时,或者当它不在表达式中使用时。一个选项是使用val属性,如本例所示。另一种可能性是使用int()或bool()函数将信号类型转换为整数或布尔值。这些函数对于intbv对象也很有用。

作为第二个示例,我们将演示如何使用python列表对同步FIFO进行建模:

	def fifo(dout, din, re, we, empty, full, clk, maxFilling=sys.maxint):

    """ Synchronous fifo model based on a list.

    Ports:
    dout -- data out
    din -- data in
    re -- read enable
    we -- write enable
    empty -- empty indication flag
    full -- full indication flag
    clk -- clock input

    Optional parameter:
    maxFilling -- maximum fifo filling, "infinite" by default

    """

    memory = []

    @always(clk.posedge)
    def access():
        if we:
            memory.insert(0, din.val)
        if re:
            dout.next = memory.pop()
        filling = len(memory)
        empty.next = (filling == 0)
        full.next = (filling == maxFilling)

    return access

同样,该模型仅仅是一个围绕列表上的一些操作的MyHDL接口:insert以插入条目,pop以检索条目,以及len以获取Python对象的大小。

使用异常建模错误

在上一节中,我们使用Python数据类型进行建模。如果这样的类型使用不当,Python的运行时错误系统将发挥作用。例如,如果我们访问了以前未初始化的parseMemory模型中的地址,我们将得到类似于以下内容的追踪(为了清晰起见,省略了一些行):

Traceback (most recent call last):
...
  File "sparseMemory.py", line 31, in access
    dout.next = memory[addr.val]
KeyError: Signal(51)

同样,如果FIFO是空的,并且我们尝试从它读取,我们会得到:

Traceback (most recent call last):
...
  File "fifo.py", line 34, in fifo
    dout.next = memory.pop()
IndexError: pop from empty list

与其定义这些低级错误,不如在功能级别定义错误。在Python中,这通常是通过定义一个自定义Error错误异常,即通过对标准Exception异常类扩展子类来完成。当出现错误情况时,将显式引发此异常。

例如,我们可以按如下方式更改sparseMemory函数(为了简洁起见省略doc字符串):

class Error(Exception):
    pass

def sparseMemory2(dout, din, addr, we, en, clk):

    memory = {}

    @always(clk.posedge)
    def access():
        if en:
            if we:
                memory[addr.val] = din.val
            else:
                try:
                    dout.next = memory[addr.val]
                except KeyError:
                    raise Error, "Uninitialized address %s" % hex(addr)

    return access

这是通过捕获低级数据类型异常,并引发带有适当错误消息的自定义异常来实现的。如果在具有相同名称的模块中定义了sparseMemory函数,则将报告访问错误,如下所示:

Traceback (most recent call last):
...
  File "sparseMemory.py", line 61, in access
    raise Error, "Uninitialized address %s" % hex(addr)
Error: Uninitialized address 0x33

同样,FIFO函数可以进行如下调整,以报告下溢和上溢错误:

class Error(Exception):
    pass


def fifo2(dout, din, re, we, empty, full, clk, maxFilling=sys.maxint):

    memory = []

    @always(clk.posedge)
    def access():
        if we:
            memory.insert(0, din.val)
        if re:
            try:
                dout.next = memory.pop()
            except IndexError:
                raise Error, "Underflow -- Read from empty fifo"
        filling = len(memory)
        empty.next = (filling == 0)
        full.next = (filling == maxFilling)
        if filling > maxFilling:
            raise Error, "Overflow -- Max filling %s exceeded" % maxFilling

    return access

在这种情况下,通过捕获列表数据类型上的低级异常,可以像以前一样检测下溢错误。另一方面,通过定期检查列表的长度来检测溢出错误。

面向对象建模。

前面几节中的模型在内部使用了高级内置数据类型。然而,他们有一个传统的RTL风格的界面。与这样的模块的通信是通过在实例化期间附加到它的信号来完成的。

一种更高级的方法是将硬件块建模为对象。与对象的通信是通过方法调用完成的。方法封装对象执行的特定任务的所有详细信息。由于对象具有方法接口而不是RTL风格的硬件接口,因此这是一种更高级的方法。

例如,我们将设计一个同步队列对象。这样的对象可以由生产者填充,并由消费者独立读取。当队列为空时,使用者应该等待,直到有一个项目可用为止。队列可以建模为具有put(item)和get方法的对象,如下所示:

from myhdl import *

def trigger(event):
    event.next = not event

class queue:
    def __init__(self):
       self.l = []
       self.sync = Signal(0)
       self.item = None
    def put(self,item):
       # non time-consuming method
       self.l.append(item)
       trigger(self.sync)
    def get(self):
       # time-consuming method
       if not self.l:
          yield self.sync
       self.item = self.l.pop(0)

queue对象构造函数初始化内部列表以保存项目,并初始化同步sync信号以同步方法之间的操作。每当将一项放入队列中时,信号就会被触发。当get方法看到列表为空时,它首先等待触发器。get是一种生成方法,因为它可能会消耗时间。由于在MyHDL中使用yield语句进行时间控制,因此该方法不能“产生”该项。相反,它使其在该项实例变量中可用。

为了测试队列操作,我们将在测试工作台中对生产者和消费者进行建模。由于等待的使用者不应该阻塞整个系统,所以它应该在并发的“线程”中运行。和在MyHDL中一样,并发是由Python生成器建模的。因此,生产者和消费者将独立运行,我们将通过一些打印语句监视它们的操作:

q = queue()

def Producer(q):
    yield delay(120)
    for i in range(5):
        print "%s: PUT item %s" % (now(), i)
        q.put(i)
        yield delay(max(5, 45 - 10*i))

def Consumer(q):
    yield delay(100)
    while 1:
        print "%s: TRY to get item" % now()
        yield q.get()
        print "%s: GOT item %s" % (now(), q.item)
        yield delay(30)

def main():
    P = Producer(q)
    C = Consumer(q)
    return P, C

sim = Simulation(main())
sim.run()

请注意,生成器方法get是在consumer函数的yield语句中调用的。新的生成器将从消费者手中接管,直到完成为止。运行此测试工作台将生成以下输出:

% python queue.py
100: TRY to get item
120: PUT item 0
120: GOT item 0
150: TRY to get item
165: PUT item 1
165: GOT item 1
195: TRY to get item
200: PUT item 2
200: GOT item 2
225: PUT item 3
230: TRY to get item
230: GOT item 3
240: PUT item 4
260: TRY to get item
260: GOT item 4
290: TRY to get item
StopSimulation: No more events

猜你喜欢

转载自blog.csdn.net/zt5169/article/details/84569515