MyHDL:一种基于 Python 的硬件描述语言

作者:Jan Decaluwe

数字硬件设计通常使用一种专门的语言来完成,这种语言被称为硬件描述语言 (HDL)。这种方法是基于硬件设计具有独特需求的理念。主流的 HDL 是 Verilog 和 VHDL。

MyHDL 项目通过使使用 Python 这种高级通用语言进行硬件设计成为可能,从而挑战了传统观念。这种方法让硬件设计师能够受益于一种设计良好、广泛使用的语言以及其背后的开源模式。

概念

HDL 包含某些反映硬件本质的概念。最典型的概念是用于大规模并发的模型。HDL 描述由大量微小的线程组成,这些线程彼此紧密通信。这种设计要求采用尽可能轻量级的线程处理方法。HDL 描述在称为模拟器的专用运行时环境中执行。

在设计 MyHDL 时,我采用了极简主义的方法,这与 Python 的精神以及总体上的好主意是一致的。因此,MyHDL 的重要组成部分是 Python 的使用模型。另一部分是由一个名为 myhdl 的 Python 包组成,其中包含实现 HDL 概念的对象。以下 Python 代码导入了一些我们稍后将要使用的 MyHDL 对象

from myhdl import Signal, Simulation, delay, now

MyHDL 使用生成器函数来模拟并发,这是 Python 最近引入的特性(请参阅在线资源)。它们类似于经典函数,只是它们具有非致命的返回能力。当调用生成器函数时,它返回一个生成器,这是感兴趣的对象。生成器是可恢复的,并在调用之间保持状态,使其可用作超轻量级线程。

以下示例是一个模拟时钟生成器的生成器函数

    def clkgen(clk):
        """ Clock generator.
        clk -- clock signal
        """
        while 1:
            yield delay(10)
            clk.next = not clk

这个函数看起来类似于 Python 中的经典函数。请注意,函数代码以 while 1 循环开始;这是使生成器永远保持活动的惯用方法。经典函数和生成器函数之间的本质区别在于 yield 语句。它的行为类似于 return 语句,只是生成器在 yield 后仍然处于活动状态,并且可以从该点恢复。此外,yield 语句返回一个延迟对象。在 MyHDL 中,此机制用于将控制信息传递给模拟器。在这种情况下,模拟器被告知应在延迟十个时间单位后恢复生成器。

参数 clk 表示时钟信号。在 MyHDL 中,信号用于生成器之间的通信。信号的概念继承自 VHDL。信号是一个具有两个值的对象:一个只读的当前值和一个可以通过将其赋值给 .next 属性来修改的下一个值。在示例中,通过将其下一个值设置为其当前值的反值来切换时钟信号。

为了模拟时钟生成器,我们首先创建一个时钟信号

clk = Signal(bool(0))

信号 clk 的初始值为布尔零。现在,我们可以通过调用生成器函数来创建一个时钟生成器

clkgen_inst = clkgen(clk)

为了进行最基本的有用模拟,让我们创建另一个生成器,用于监视和打印时钟信号随时间的变化

def monitor():
    print "time: clk"
    while 1:
        print "%4d: %s" % (now(), int(clk))
        yield clk

yield clk 语句显示了生成器如何等待信号值的变化。

在 MyHDL 中,模拟器是使用 Simulation 对象构造函数创建的,该构造函数接受任意数量的生成器作为参数

sim = Simulation(clkGen_inst, monitor())

要运行模拟器,我们调用其 run 方法

sim.run(50)

这将运行模拟 50 个时间单位。输出如下

$ python clkgen.py
time: clk
   0: 0
  10: 1
  20: 0
  30: 1
  40: 0
  50: 1

此时,我们可以描述模拟器的工作原理。模拟算法的灵感来自 VHDL,VHDL 是一种比 Verilog 稍逊一筹的 HDL,但却是更好的效仿范例。模拟器使用事件驱动算法协调所有生成器的并行执行。生成器 yield 的对象指定了它希望等待的事件,然后才能进行下一次调用。

假设在给定的模拟步骤中,由于它们正在等待的事件已经发生,因此某些生成器变为活动状态。在第一个模拟阶段,所有活动的生成器都使用当前信号值运行,并赋值给下一个值。在第二个阶段,当前信号值将使用下一个值进行更新。由于信号值更改,一些生成器再次变为活动状态,并且模拟周期重复。这种机制保证了确定性,因为活动生成器的运行顺序与模型的行为无关。

真实设计示例

在介绍了这些概念之后,我们现在准备好使用 MyHDL 解决一个真实的设计示例。我选择了一个串行外围接口 (SPI) 从设备硬件模块。SPI 是一种流行的同步串行控制接口,最初由摩托罗拉设计。

单个 SPI 主设备可以控制多个从设备。有三个常用的 I/O 端口:mosi,主设备输出,从设备输入串行线;miso,主设备输入,从设备输出串行线;以及 sclk,由主设备驱动的串行时钟。此外,每个从设备都存在一个从设备选择线 ss_n。SPI 通信始终在两个方向上同时发生。通常,触发数据更改的有效时钟边沿是可配置的。在此示例中,我们使用上升沿。

清单 1 中显示了 SPI 从设备的 MyHDL 代码。名为 SPISlave 的经典 Python 函数用于模拟硬件模块。该函数具有所有接口信号作为其参数,并且它返回两个生成器。此代码说明了如何在 MyHDL 中模拟层次结构:更高级别的函数调用更低级别的函数,并将返回的生成器包含在其自身的返回值中。

清单 1. SPI 从设备的 MyHDL 模型

from myhdl import Signal, posedge, negedge, intbv

ACTIVE_n, INACTIVE_n = bool(0), bool(1)
IDLE, TRANSFER = bool(0), bool(1)

def toggle(sig):
    sig.next = not sig

def SPISlave(miso, mosi, sclk, ss_n,
             txdata, txrdy, rxdata, rxrdy,
             rst_n, n=8):
    """ SPI Slave model.

    miso -- master in, slave out serial output
    mosi -- master out, slave in serial input
    sclk -- shift clock input
    ss_n -- active low slave select input
    txdata -- n-bit input with data to be transmitted
    txrdy -- toggles when new txdata can be accepted
    rxdata -- n-bit output with data received
    rxrdy -- toggles when new rxdata is available
    rst_n -- active low reset input
    n -- data width parameter

    """

    cnt = Signal(intbv(0, min=0, max=n))

    def RX():
        sreg = intbv(0)[n:]
        while 1:
            yield negedge(sclk)
            if ss_n == ACTIVE_n:
                sreg[n:1] = sreg[n-1:]
                sreg[0] = mosi
                if cnt == n-1:
                    rxdata.next = sreg
                    toggle(rxrdy)

    def TX():
        sreg = intbv(0)[n:]
        state = IDLE
        while 1:
           yield posedge(sclk), negedge(rst_n)
            if rst_n == ACTIVE_n:
                state = IDLE
                cnt.next = 0
            else:
                if state == IDLE:
                    if ss_n == ACTIVE_n:
                        sreg[:] = txdata
                        toggle(txrdy)
                        state = TRANSFER
                        cnt.next = 0
                else: # TRANSFER
                    sreg[n:1] = sreg[n-1:]
                    if cnt == n-2:
                        state = IDLE
                    cnt.next = (cnt + 1) % n
                miso.next = sreg[n-1]

    return RX(), TX()

模块接口包含一些附加信号和参数。txdata 是要传输的输入数据字,当可以接受新字时,txrdy 会切换。类似地,rxdata 包含接收到的数据字,当接收到新字时,rxrdy 会切换。最后,有一个复位输入 rst_n 和一个定义数据字宽度的参数 n。

在 SPI 从设备模块内部,我们创建一个信号 cnt 来跟踪串行周期数。它使用 intbv 对象作为其初始值。intbv 是一个面向硬件的类,它的工作方式类似于具有位级功能的整数。Python 的索引和切片接口可用于访问各个位和切片。此外,intbv 对象可以具有最小值和最大值。

RX 生成器函数描述了接收行为。每当从设备选择线 ss_n 为低电平时,mosi 输入都会移入移位寄存器 sreg。yield negedge(sclk) 语句指示该操作发生在下降时钟沿。在最后一个串行周期中,移位寄存器被传输到 rxdata 输出,并且 rxrdy 切换。

TX 生成器函数稍微复杂一些,因为它需要一个小型的状态机来控制协议。在这种情况下,yield 语句指定了两个事件,这意味着生成器在首先发生的事件上恢复。当复位输入为低电平时,cnt 和 state 会被复位。在另一种情况下,该操作取决于状态。在 IDLE 状态下,我们等待选择线变为低电平,然后再接受用于传输的数据字并进入 TRANSFER 状态。在 TRANSFER 状态下,移位寄存器串行移出。状态机维护正确的串行周期计数,并在最后一次移位时返回到 IDLE 状态。

验证

SPI 从设备模块的建模级别非常接近实际实现。这是介绍 MyHDL 概念的好方法。但是,为此目的使用 MyHDL 并不能比传统的 HDL 提供很多优势。相反,MyHDL 的真正价值在于它使整个 Python 可供硬件设计师使用。Python 的表达能力、灵活性和广泛的库提供了超出传统 HDL 范围的可能性。

Python 式功能非常理想的一个领域是验证。与软件一样,在硬件设计中,验证是困难的部分。人们普遍认为传统的 HDL 无法胜任这项任务。因此,又出现了一种语言类型,即硬件验证语言 (HVL)。MyHDL 再次依靠 Python 来挑战这一趋势。

要设置硬件验证环境,我们首先创建一个测试平台。这是一个硬件模块,它实例化被测设计 (DUT) 以及数据生成器和检查器。清单 2 显示了 SPI 从设备模块的测试平台。它实例化了 SPI 从设备模块以及控制所有接口引脚的 SPI 测试器模块。为了能够使用多个 SPI 测试器模块来验证设计的各个方面,SPI 测试器模块是测试平台的参数。

清单 2. SPI 从设备模块的测试平台

import unittest
from random import randrange

from myhdl import Signal, intbv, traceSignals

from SPISlave import SPISlave, ACTIVE_n, INACTIVE_n

def TestBench(SPITester, n):

    miso = Signal(bool(0))
    mosi = Signal(bool(0))
    sclk = Signal(bool(0))
    ss_n = Signal(INACTIVE_n)
    txrdy = Signal(bool(0))
    rxrdy = Signal(bool(0))
    rst_n = Signal(INACTIVE_n)
    txdata = Signal(intbv(0)[n:])
    rxdata = Signal(intbv(0)[n:])

    SPISlave_inst = traceSignals(SPISlave,
        miso, mosi, sclk, ss_n,
        txdata, txrdy, rxdata, rxrdy, rst_n, n=n)

    SPITester_inst = SPITester(
        miso, mosi, sclk, ss_n,
        txdata, txrdy, rxdata, rxrdy, rst_n, n=n)

    return SPISlave_inst, SPITester_inst

对于测试本身,我们使用单元测试框架。单元测试是极限编程 (XP) 的基石,极限编程是一种现代软件开发方法论,它是常识和全新想法的有趣混合体。真正的 XP 方法是首先开发测试,然后再进行实现。XP 是一种有用的方法论,但硬件设计界几乎忽略了它的教训。借助 MyHDL,Python 的单元测试框架 unittest 可以用于测试驱动的硬件开发。

清单 3 显示了 SPI 从设备模块的测试代码。测试在 unittest.TestCase 类的子类中定义。每个方法名称都带有前缀 test,对应于一个实际的测试,但也可以编写其他方法来支持测试。典型的测试套件由多个测试和测试用例组成,但我们描述单个测试以演示该想法。

清单 3. 通过 SPI 接收数据的测试用例

import unittest
from random import randrange

from myhdl import Simulation, join, delay, \
                  intbv, downrange

from SPISlave import SPISlave, ACTIVE_n, INACTIVE_n
from SPISlaveTestBench import TestBench

n = 8
NR_TESTS = 100

class TestSPISlave(unittest.TestCase):

    def RXTester(self, miso, mosi, sclk, ss_n,
                 txdata, txrdy, rxdata, rxrdy,
                 rst_n, n):

        def stimulus(data):
            yield delay(50)
            ss_n.next = ACTIVE_n
            yield delay(10)
            for i in downrange(n):
                sclk.next = 1
                mosi.next = data[i]
                yield delay(10)
                sclk.next = 0
                yield delay(10)
            ss_n.next = INACTIVE_n

        def check(data):
            yield rxrdy
            self.assertEqual(rxdata, data)

        for i in range(NR_TESTS):
            data = intbv(randrange(2**n))
            yield join(stimulus(data), check(data))

    def testRX(self):
        """ Test RX path of SPI Slave """
        sim = Simulation(TestBench(self.RXTester, n))
        sim.run(quiet=1)

if __name__ == '__main__':
    unittest.main()

RXTester 方法是一个生成器函数,专为 SPI 从设备接收路径的基本测试而设计。它包含一个本地生成器函数 stimulus,该函数作为主设备在 SPI 总线上发送数据字。另一个本地生成器函数 check 检查从设备是否正确接收到数据字。完整的测试由多次随机数据字传输组成。对于每个数据字,我们创建一个 stimulus 和一个 check 生成器。为了等待它们的完成,MyHDL 允许我们将它们放在 yield 语句中。为了正确同步,我们希望仅在两个生成器都完成时才继续。此功能由 join 函数完成。

当我们运行测试程序时,输出指示哪些测试在哪个点失败。当一切正常运行时,我们的小示例的输出如下

$ python test_SPISlave.py -v
Test RX path of SPI Slave ... ok
------------------------------------------------
Ran 1 test in 0.559s
波形查看

MyHDL 支持波形查看,这是一种可视化硬件行为的流行方式。在清单 2 中,SPI 从设备模块的实例化包装在对函数 traceSignals 的调用中。作为副作用,信号更改在模拟期间以标准格式转储到文件中。图 1 显示了 gtkwave(一种开源波形查看器)呈现的波形示例。

MyHDL: a Python-Based Hardware Description Language

图 1. 使用 gtkwave,您可以可视化测试套件运行时的所有信号。

与其他 HDL 的链接

MyHDL 是一种实用的解决方案,与其他 HDL 链接。MyHDL 支持与其他具有操作系统接口的 HDL 模拟器进行协同仿真。必须为每个模拟器实现一个桥接器。这已经为开源 Verilog 模拟器 Icarus 和 cver 完成。

此外,MyHDL 代码的面向实现的子集可以自动转换为 Verilog。这是通过一个名为 toVerilog 的函数完成的,它的使用方式与前面描述的 traceSignals 函数相同。生成的代码可以在标准设计流程中使用,例如,自动将其综合为实现。

结语

著名的 Python 大师 Tim Peters 用一句看似矛盾的话解释了他对 Python 的热爱:“Python 代码很容易扔掉。” 本着同样的精神,MyHDL 旨在成为试验新想法的首选硬件设计工具。

本文资源: /article/7749

Jan Decaluwe 曾担任 ASIC 设计工程师和企业家 18 年。目前,他是一名电子设计和自动化顾问。可以通过 jan@jandecaluwe.com 与他联系。

加载 Disqus 评论