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