Scheme實現數字電路仿真(2)——原語

  版權申明:本文爲博主窗戶(Colin Cai)原創,歡迎轉帖。如要轉貼,必須註明原文網址

  http://www.cnblogs.com/Colin-Cai/p/12045295.html 

  做者:窗戶

  QQ/微信:6679072

  E-mail:6679072@qq.com

  上一章給出了組合電路的仿真實現,這一章開始思考時序電路的仿真實現。但時序電路遠比組合電路複雜的多,咱們先從組成電路的每一個元件提及。在程序實現層次,咱們能夠考慮給每一個基礎元件一個自定義描述方式,稱爲原語。html

 

  Verilog原語算法

 

  Verilog提供了元件原語建模的方式,說白了,就是用一個表格來體現全部狀況下的輸出。Verilog的原語只容許有一個輸出。sql

  好比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保持不變。這稱之爲電平觸發。用波形圖能夠描述其特性:app

  D鎖存器波形

  用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保持不變。這種觸發稱之爲沿觸發。波形圖能夠用如下描述其特性:

  D觸發器波形

  用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。波形圖以下:

  帶復位D觸發器波形

  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 '(() . ()))則是一個原語的實例封裝,裏面包含着狀態,能夠用來在仿真中反覆迭代。

相關文章
相關標籤/搜索