Scheme實現數位電路模擬(2)——原語
版權申明:本文為博主窗戶(Colin Cai)原創,歡迎轉帖。如要轉貼,必須註明原文網址 http://www.cnblogs.com/Colin-Cai/p/12045295.html 作者:窗戶 QQ/微信:6679072 E-mail:[email protected]
上一章給出了組合電路的模擬實現,這一章開始思考時序電路的模擬實現。但時序電路遠比組合電路複雜的多,我們先從組成電路的每個元件說起。在程式實現層次,我們可以考慮給每個基礎元件一個自定義描述方式,稱為原語。
Verilog原語
Verilog提供了元件原語建模的方式,說白了,就是用一個表格來體現所有情況下的輸出。Verilog的原語只允許有一個輸出。
比如and門,用Verilog原語來描述如下
primitive myand(out,in1,in2); output out; input in1,in2; table // in1 in2 out 0 ? : 0; 1 0 : 0; 1 1 : 1; endtable endprimitive
Verilog原語中不能使用高阻(因為除了三態門產生高阻輸出之外,這的確與真實電路不符,而Verilog並無VHDL那般抽象),不能表示三態門。
對於時序電路,Verilog也一樣可以支援。所謂時序電路,意味著電路的輸出不僅僅與當前電路的輸入有關,還與電路之前的狀態有關,所謂電路之前的狀態也就是電路之前的輸出。
我們來考慮這樣一個時序元件,稱之為D鎖存器,有兩個輸入en和in,一個輸出out。當en為0時,out和in保持一致;當en為1時,out保持不變。這稱之為電平觸發。用波形圖可以描述其特性:
用verilog描述可以如下:
module dlatch(out, en, in); output out; input en, in; reg out; always@(in) if(!en) out <= in; endmodule
電平觸發的D鎖存器可以用原語描述如下:
primitive dlatch(out, en, in); output out; input en, in; reg out; table //en in : out : next out 0 0 : ? : 0; 0 1 : ? : 1; 1 ? : ? : -; endtable endprimitive
狀態表的最後一行next out位置的 - 符號代表狀態保持。
再來一個我們數字設計時最常用的元件D觸發器,它有兩個輸入訊號clk和in,有一個輸出訊號out。當clk從0變到1的瞬間(稱之為上升沿),out被賦予in的值,其他時候out保持不變。這種觸發稱之為沿觸發。波形圖可以用以下描述其特性:
用Verilog描述如下:
module dff(out, clk, in); output out; input clk, in; reg out; always@(posedge clk) out <= in; endmodule
而用Verilog原語描述則如下:
primitive dff(out, clk, in); output out; input clk, in; reg out; table // clk in : out : next out (01) 0 : ? : 0; (01) 1 : ? : 1; endtable endprimitive
原語沒有寫的部分都是保持。換句話說,之前D鎖存器的原語實現table的最後一行保持是可以不寫的。
前面的D鎖存器是電平觸發,D觸發器是沿觸發。實際上原語也可以同時支援兩種觸發。比如存在非同步復位的D觸發器,多了個觸發的rst訊號,在rst為1的時候,out會被賦予0。波形圖如下:
Verilog描述可以如下:
module dff(out, rst, clk, in); output out; input rst, clk, in; reg out; always@(posedge rst or posedge clk) if(rst) out <= 1'b0; else out <= in; endmodule
用原語描述則為:
primitive dff(out, rst, clk, in); output out; input rst, clk, in; reg out; table // rst clk in : out : next out 0 (01) 0 : ? : 0; 0 (01) 1 : ? : 1; 1 ? ? : ? : 0; endtable endprimitive
以上的原語中就同時包含電平觸發和沿觸發。
Scheme建模下的原語
Verilog原語用表來表示,實際上是用表來代表一個函式關係,於是我們要做的,是試著用一個函式來代表基本元件的原語描述。
比如與門,我們是不是可以用以下函式來描述:
(define (myand in1 in2) (if (and (= in1 1) (= in2 1)) 1 0))
上述函式方便的表示一個組合邏輯,甚至上述可以延伸到表示任意多輸入的一個與門,描述如下
(define (myand . in) (if (member 0 in) 0 1))
可是上述的描述並未方便的引入時序的概念,最終在模擬的時候無法區分組合邏輯和時序邏輯。從而上述的函式來代表原語描述是失敗的,需要再修改一下。
於是我們描述函式的引數列表裡不僅有當前各輸入訊號,還得有當前輸出訊號,考慮到沿觸發器件,還得加入沿的資訊。於是我們可以定義原語是這樣的一個函式:帶有三個引數,第一個引數是輸入訊號值的列表,第二個引數是當前輸出訊號值,第三個引數代表沿觸發的訊號,簡單起見,就用沿觸發的訊號在輸入訊號列表中的序號來表示,如果不是沿觸發則此處傳入-1;函式返回即將輸出的訊號值。
那麼我們的任意多輸入的與門,描述如下
(define (myand input current-output edge-) (if (member 0 input) 0 1))
那麼D鎖存器的原語描述如下
(define (dlatch input current-output edge) (let ((en (car input)) (in (cadr input))) (if (= en 1) current-output in)))
上面的let顯示了輸入列表是[en, in];
D觸發器的原語描述如下,輸入列表為[clk, in]
(define (dff (let ((clk (car input)) (in (cadr input))) (if (and (= edge 0) (= clk 1)) in current-output)))
對於之前帶非同步復位的D觸發器,作為一個既有電平觸發又有沿觸發的例子
(define (dff-with-rst input current-output edge) (let ((rst (car input))(clk (cadr input)) (in (caddr input))) (cond ((= rst 1) 0) ((and (= edge 0) (= clk 1)) in) (else current-output))))
進一步修改原語
之前的設計已經完備,但未必方便。比如可能一些邏輯可程式設計器件的程式設計粒度不會細到門級。Verilog的原語裡,只有一個輸出,我們可以考慮這裡原語的輸出可以有多個。
在此我們考慮一位全加器,也就是三個單bit的數相加,得到兩位輸出的組合電路,輸出訊號既然可能不止一個,原語函式的輸出當然是一個列表,第二個引數current-output當然也是列表。
(define (add input current-output edge) (let ((a (car input))(b (cadr input)) (c (caddr input))) (let* ((sum (+ a b c)) (cout (if (>= sum 2) 1 0)) (s (if (= cout 0) sum (- sum 2)))) (list cout s))))
最後,我們考慮,原語可以為每一個訊號可以加一個位寬。
在這裡,我們來考慮做一個四位計數器,有一個非同步復位(rst),有一個時鐘(clk),一個4位的輸出(out),每當clk上升沿,輸出都會加1,注意如果當前輸出如果是1111,下一個輸出將會是0000,描述如下
(define (counter input current-output edge) (define (add1-list lst) (cond ((null? lst) '()) ((= (car lst) 0) (cons 1 (cdr lst))) (else (cons 0 (add1-list (cdr lst)))))) (let ((rst (car input)) (clk (cadr input))) (cond ((= rst 1) '((0 0 0 0))) ((and (= edge 1) (= clk 1)) (list (add1-list (car current-output)))) (else current-output))))
用0/1的list有一些不方便的地方,我們可以用數來代替,也可以考慮數和list一起支援,那麼我們在處理的時候可能需要判斷一下傳入的是數還是list,Scheme裡提供了兩個函式來判斷,一個是list?用來判斷是不是list,一個是number?用來判斷是不是數。在上面定義的基礎上加上對於數的支援也很容易。
迭代
以上雖然用函式來定義了原語,但是從函式卻沒有定義任何表示原語訊號介面的東西,不看原語定義無法知道原語怎麼使用,並且在模擬的時候,上述原語本身並不提供各個訊號當前的值。
本來會在後面的章節提到解決方案,在此也給個方案。
我們可以用閉包解決這個問題,閉包中包含著輸入、輸出訊號的資訊。Scheme的閉包可以有多種方式,可以採用上一章中區域性作用域變數的方法(這種方法並不是所有的語言都支援,比如Python則只能用class建立類了),另一種方式則是用不變量了,也就是純函數語言程式設計方式。本章就來說說第二種方式,雖然在我之前的其他文章中說到的閉包主要是採取這種方式。
我們先看一個簡單的例子,我們希望有這樣的需求:
定義一個變數x
(define x (make-sum 0))
(set! x (x 1))
(set! x (x 2))
(set! x (x 3))
(x)得到6
這樣,每次x都是一個閉包,現在要看如何定義make-sum。
我們先這樣定義:
(define (make-sum n) (lambda (m) (make-sum (+ n m))))
但是,我們馬上發現,我們要求的值變的不可提取,閉包返回的這個函式,不僅僅可以帶一個引數用來再度返回閉包,還應該可以不帶引數,以支援上面(x)這樣的提取。
上面的實現需要一點修改,需要判斷一下引數個數:
(define (make-sum n) (lambda s (if (null? s) n (make-sum (+ n (car s))))))
測試一下,OK了,最後得到了6,說明make-sum是可行的。
然後,我們可以抽象加法這個符號,繼續做運算元f-step。
(define (f-step step n) (lambda s (if (null? s) n (f-step step (step n (car s))))))
這樣,make-sum可以由上述運算元定義而得
(define make-sum (lambda (n) (f-step + n))
定義f-step運算元有什麼好處呢?實際上,它是為迭代的每一步動作進行建模。
於是我們可以用f-step為零件,構建所有的迭代。
比如對於輾轉相除法(歐幾里得演算法)求最大公約數,描述如下
(define (gcd a b) (if (zero? b) a (gcd b (remainder a b))))
如果要用f-step,則首先要把迭代的內容表示成一個物件,可以用cons對來對gcd的兩個引數a,b打包。
f-step的第二個引數是一個函式,我們稱之為step,step函式有兩個引數,一個是用於迭代的資料,在這裡就是這個cons對,而第二個引數可以看成是外界激勵,這裡是不需要的,傳任意值即可。
我們清楚輾轉相除法的這一步,應該描述如下
(define (step pair none) (cons (cdr pair) (remainder (car pair) (cdr pair))))
反覆的迭代,其終止條件是判斷pair的第二個成員是否為0,如果是0則返回pair的第一個成員,否則繼續迭代
(define (continue-gcd-do f) (let ((x (f))) (if (zero? (cdr x)) (car x) (continue-gcd-do (f '())))))
於是,我們的gcd就被重新設計了
(define (gcd a b) (continue-gcd-do (f-step step (cons a b))))
雖然看起來的確比最開始的實現複雜了不少,但是可以實現統一的設計,以便更復雜的情況下的應用。
反柯里化
f-step還可以用來設計fold-left運算元,我們回憶一下fold-left
(fold-left cons 'a '(b c d))
得到
'(((a . b) . c) . d)
我們可以看成是一個迭代,
最開始是'a
然後通過函式cons和'b,得到
'(a . b)
然後再通過函式cons和'c,得到
'((a . b) . c)
最後再通過函式cons和'd,得到
'(((a . b) . c) . d)
顯然,我們可以使用f-step,定義以下
(define func (f-step cons 'a))
那麼
(((func 'b) 'c) 'd)
則是最後的結果。
但這樣似乎不太好用,假如我們有這麼一個函式,暫且稱為F
((F func) 'b 'c 'd)
也就是
(apply (F func) '(b c d))
那麼就容易實現了。
F這個過程正好和我之前的文章《map的實現和柯里化(Curring)》裡的柯里化過程相反,稱之為反柯里化,重新給個合適的名字叫uncurry
(define (uncurry f) (lambda s (if (null? s) (f) (apply (uncurry (f (car s))) (cdr s)))))
於是fold-left就可以如下實現
(define (my-fold-left f init lst) (apply (uncurry (f-step f init)) lst))
封裝
繞了一圈,似乎與主題有點遠了。一個原語所表示的電路,實際上也是隨著外界輸入,在不斷的變化輸出,也可以用f-step運算元來模擬。
電路的狀態包含了電路的輸出,同時也包含著電路的輸入,因為需要判斷沿變化,當然我們只需要關注沿觸發的訊號就行了,其他輸入訊號不需要在狀態裡。
我們就以之前的帶復位的D觸發器為例,我們重新給出它的原語描述,並按第三節裡修改之後的來,
(define (dff-with-rst input current-output edge) (let ((rst (caar input))(clk (caadr input)) (in (caaddr input))) (cond ((= rst 1) '((0))) ((and (= edge 0) (= clk 1)) (list (list in))) (else current-output))))
我們的初始狀態可以設定為
'((z) . (z)),
之所以用z來表示,而不是0/1,在於初始的時候,我們認為都是一種渾沌的狀態,當然,也可以設為用0/1,這完全可以按模擬意願來。
前面第一個'(z)表示所有可以帶來沿觸發的訊號列表,這裡可以帶來沿觸發的是第二個訊號clk,序號從0開始算為1,而輸出訊號初始也先設定為'(z)
於是狀態轉換函式則為
(define step
(lambda (stat input)
(cons (cadr input) (dff-with-rst input (cdr stat) (if (eq? (caar stat) (caadr input)) -1 1)))))
於是
(f-step step '(() . ()))則是一個原語的例項封裝,裡面包含著狀態,可以用來在模擬中反覆迭