现代数字硬件设计流程中的许多方面都可以看作是一种特殊的软件开发。从这个角度来看,软件设计技术的进步很自然也能应用于硬件设计。
一种值得注意的软件设计方法是极限编程(XP)。这是一套引人入胜的技术和指导方针,似乎常常与传统智慧背道而驰。在其他情况下,XP似乎只是强调常识,它并不总是符合通常的做法。例如,如果我们想拥有良好的软件开发所需的新鲜思维,XP强调正常工作周的重要性。
提出一个关于极限编程的教程不是我的意图,也没有资格。相反,在本节中,我将强调一个我认为与硬件设计非常相关的XP概念:单元测试的重要性和方法。
单元测试是极限编程的基石之一。其他XP概念,如代码的集体所有权和持续改进,只有通过单元测试才能实现。此外,XP强调编写单元测试应该是自动化的,应该测试每个类中的所有内容,并且应该始终完美地运行。
我认为这些概念直接适用于硬件设计。此外,单元测试是管仿真时间的一种方法。例如,在不经常发生的事件上运行非常缓慢的状态机可能无法在系统级别进行验证,即使在最快的仿真器上也是如此。另一方面,即使在速度最慢的仿真器上,在单元测试中也很容易对其进行详尽的验证。
显然,单元测试具有令人信服的优势。另一方面,如果我们需要测试所有的东西,我们必须编写大量的单元测试。因此,创建、管理和运行它们应该是简单而愉快的。因此,XP强调需要一个支持这些任务的单元测试框架。在本章中,我们将探讨如何使用标准Python库中的unittest模块为硬件设计创建单元测试。
在本节中,我们将非正式地探讨单元测试技术在硬件设计中的应用。我们将通过一个(小的)示例:测试二进制到格雷编码器(如位索引bit indexing一节中所介绍的)来实现这一点。
我们首先定义需求。对于格雷编码器,我们希望输出符合格雷码的特性。让我们将代码定义为一个码字列表,其中一个码字是位字符串。n阶码有2*n个码字。
众所周知,格雷码的特点是:
格雷码中的连续码字应该在一位中有所不同。
这就够了吗?不完全是:例如,假设一个实现返回每个二进制输入的LSB。这将符合要求,但显然不是我们想要的。此外,我们不希望格雷码字的位宽度超过二进制码字的位宽。
n阶格雷码中的每个码字必须在同一阶二进制码中精确出现一次。
把要求写下来,我们就可以继续了。
XP世界中一个引人入胜的指导方针是首先编写单元测试。也就是说,在实现某项内容之前,首先编写将对其进行验证的测试。这似乎违背了我们的自然倾向,也违背了我们的一般做法。许多工程师喜欢先实现,然后再考虑验证。
但是如果你考虑一下,首先处理验证是很有意义的。验证只涉及到需求–所以您的想法还没有被实现的细节弄得杂乱无章。单元测试是对需求的可执行描述,因此可以更好地理解它们,并且非常清楚需要做什么。因此,执行工作应该更加顺利。也许最重要的是,当您完成实现时,测试是可用的,并且任何人都可以随时运行该测试来验证更改。
Python有一个标准的unittest模块,可以方便地编写、管理和运行单元测试。使用unittest,通过创建从unittest.TestCase继承的类来编写测试用例。单个测试是由该类的方法创建的:所有test开头的方法名称都被视为测试用例的测试。
我们将为Gray代码属性定义一个测试用例,然后为每个需求编写一个测试。测试用例类的概要如下:
import unittest
class TestGrayCodeProperties(unittest.TestCase):
def testSingleBitChange(self):
"""Check that only one bit changes in successive codewords."""
....
def testUniqueCodeWords(self):
"""Check that all codewords occur exactly once."""
....
每种方法都将是一个测试单个需求的小型测试平台。为了编写测试,我们不需要实现格雷编码器,但我们需要设计的接口。我们可以通过一个假设的实现来指定这一点,如下所示:
from myhdl import block
@block
def bin2gray(B, G):
# DUMMY PLACEHOLDER
""" Gray encoder.
B -- binary input
G -- Gray encoded output
"""
pass
对于第一个需求,我们将测试所有连续的输入数字,并对每一个输入的当前输出和前一个输出进行比较,我们将检查差异是否正好是一个位。对于第二个需求,我们将测试所有输入数字,并将结果放在一个列表中。这一要求意味着如果我们对结果列表进行排序,我们应该得到一个数字范围。对于这两种要求,我们将测试所有格雷码,直到某一阶最大宽度max_Width。测试代码如下所示:
import unittest
from myhdl import Simulation, Signal, delay, intbv, bin
from bin2gray import bin2gray
MAX_WIDTH = 11
class TestGrayCodeProperties(unittest.TestCase):
def testSingleBitChange(self):
"""Check that only one bit changes in successive codewords."""
def test(B, G):
w = len(B)
G_Z = Signal(intbv(0)[w:])
B.next = intbv(0)
yield delay(10)
for i in range(1, 2**w):
G_Z.next = G
B.next = intbv(i)
yield delay(10)
diffcode = bin(G ^ G_Z)
self.assertEqual(diffcode.count('1'), 1)
self.runTests(test)
def testUniqueCodeWords(self):
"""Check that all codewords occur exactly once."""
def test(B, G):
w = len(B)
actual = []
for i in range(2**w):
B.next = intbv(i)
yield delay(10)
actual.append(int(G))
actual.sort()
expected = list(range(2**w))
self.assertEqual(actual, expected)
self.runTests(test)
def runTests(self, test):
"""Helper method to run the actual tests."""
for w in range(1, MAX_WIDTH):
B = Signal(intbv(0)[w:])
G = Signal(intbv(0)[w:])
dut = bin2gray(B, G)
check = test(B, G)
sim = Simulation(dut, check)
sim.run(quiet=1)
if __name__ == '__main__':
unittest.main(verbosity=2)
请注意实际的检查是如何由一个由unittest.TestCase类定义的self.assertEqual方法执行的。此外,我们还在一个单独的方法runTest中分离出所有Gray码运行测试的因素。
写好测试后,我们从实现开始。为了便于说明,我们将特意编写一些不正确的实现,以了解测试的行为。
运行使用unittest框架定义的测试的最简单方法是在测试模块的末尾调用它的main方法:
unittest.main()
让我们使用前面显示的假Gray编码器运行测试:
% python test_gray_properties.py
testSingleBitChange (__main__.TestGrayCodeProperties)
Check that only one bit changes in successive codewords. ... ERROR
testUniqueCodeWords (__main__.TestGrayCodeProperties)
Check that all codewords occur exactly once. ... ERROR
不出所料,这完全失败了。让我们尝试一个不正确的实现,将输入的LSB放在输出中:
from myhdl import block, always_comb
@block
def bin2gray(B, G):
# INCORRECT IMPLEMENTATION
""" Gray encoder.
B -- binary input
G -- Gray encoded output
"""
@always_comb
def logic():
G.next = B[0]
return logic
运行测试会产生:
python test_gray_properties.py
testSingleBitChange (__main__.TestGrayCodeProperties)
Check that only one bit changes in successive codewords. ... ok
testUniqueCodeWords (__main__.TestGrayCodeProperties)
Check that all codewords occur exactly once. ... FAIL
======================================================================
FAIL: testUniqueCodeWords (__main__.TestGrayCodeProperties)
Check that all codewords occur exactly once.
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_gray_properties.py", line 42, in testUniqueCodeWords
self.runTests(test)
File "test_gray_properties.py", line 53, in runTests
sim.run(quiet=1)
File "/home/jand/dev/myhdl/myhdl/_Simulation.py", line 154, in run
waiter.next(waiters, actives, exc)
File "/home/jand/dev/myhdl/myhdl/_Waiter.py", line 127, in next
clause = next(self.generator)
File "test_gray_properties.py", line 40, in test
self.assertEqual(actual, expected)
AssertionError: Lists differ: [0, 0, 1, 1] != [0, 1, 2, 3]
First differing element 1:
0
1
- [0, 0, 1, 1]
+ [0, 1, 2, 3]
----------------------------------------------------------------------
Ran 2 tests in 0.083s
FAILED (failures=1)
现在,测试像预期的那样通过了第一个需求,但是没有通过第二个需求。在测试反馈之后,显示了一个完整的回溯,可以帮助调试测试输出。
最后,我们使用正确的实现:
from myhdl import block, always_comb
@block
def bin2gray(B, G):
""" Gray encoder.
B -- binary input
G -- Gray encoded output
"""
@always_comb
def logic():
G.next = (B>>1) ^ B
return logic
这次通过了。
在这里插入代码片$ python test_gray_properties.py
testSingleBitChange (__main__.TestGrayCodeProperties)
Check that only one bit changes in successive codewords. ... ok
testUniqueCodeWords (__main__.TestGrayCodeProperties)
Check that all codewords occur exactly once. ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.476s
OK
在上一节中,我们集中讨论了格雷码的一般要求。可以在不指定实际代码的情况下指定这些代码。很容易看出,有几个代码满足这些要求。在良好的XP风格中,我们只测试了需求,仅此而已。
可能需要更多的控制。例如,该要求可能是对特定代码的要求,而不是对一般属性的遵从性。作为一个例子,我们将展示如何测试原始的格雷码,这是一个满足上一节要求的特定实例。在这个特定的例子中,这个测试实际上比前一个测试更容易。
我们将原始的n阶格雷码表示为Ln。一些例子:
L1 = ['0', '1']
L2 = ['00', '01', '11', '10']
L3 = ['000', '001', '011', '010', '110', '111', '101', 100']
可以通过递归算法指定这些代码,如下所示:
L1=[‘0’,‘1’]。
LN+1可以从Ln中得到如下结果。创建一个新代码Ln0,方法是在Ln的所有代码字前面加上“0”。创建另一个新代码LN1,方法是在Ln的所有代码字前面加上“1”,然后反转它们的顺序。Ln+1是Ln0和LN1的级联。
Python以其优雅的算法描述而闻名,这是一个很好的例子。我们可以用Python编写算法,如下所示:
def nextLn(Ln):
""" Return Gray code Ln+1, given Ln. """
Ln0 = ['0' + codeword for codeword in Ln]
Ln1 = ['1' + codeword for codeword in Ln]
Ln1.reverse()
return Ln0 + Ln1
代码[‘0’+用于.的代码字]叫做列表推导式。它是描述由for循环中的短计算构建的列表的一种简洁方法。
现在的要求是输出代码与预期的代码Ln匹配。我们使用nextLn函数来计算预期的结果。新的测试用例代码如下所示:
import unittest
from myhdl import Simulation, Signal, delay, intbv, bin
from bin2gray import bin2gray
from next_gray_code import nextLn
MAX_WIDTH = 11
class TestOriginalGrayCode(unittest.TestCase):
def testOriginalGrayCode(self):
"""Check that the code is an original Gray code."""
Rn = []
def stimulus(B, G, n):
for i in range(2**n):
B.next = intbv(i)
yield delay(10)
Rn.append(bin(G, width=n))
Ln = ['0', '1'] # n == 1
for w in range(2, MAX_WIDTH):
Ln = nextLn(Ln)
del Rn[:]
B = Signal(intbv(0)[w:])
G = Signal(intbv(0)[w:])
dut = bin2gray(B, G)
stim = stimulus(B, G, w)
sim = Simulation(dut, stim)
sim.run(quiet=1)
self.assertEqual(Ln, Rn)
if __name__ == '__main__':
unittest.main(verbosity=2)
实际上,我们的实现显然是一个原始的格雷码:
$ python test_gray_original.py
testOriginalGrayCode (__main__.TestOriginalGrayCode)
Check that the code is an original Gray code. ... ok
----------------------------------------------------------------------
Ran 1 test in 0.269s
OK