Scheme實現數字電路仿真(1)——組合電路

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

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

  做者:窗戶

  QQ/微信:6679072

  E-mail:6679072@qq.com

  EDA是個很大的話題,本系列只針對其中一小部分,數字電路的仿真,敘述一點概念性的東西,並不會過於深刻,這方面的內容實則是無底洞。本系列並非真的要作EDA,按照SICP裏的相關內容,採用Lisp的方言Scheme。再者,Lisp並非只有函數式一種編程範式,真正作EDA,仿真的核心部分爲了運行效率能夠採用C/C++編寫,編程的思路也能夠借鑑。html

  

  門級電路算法

 

  學過數字電路,咱們都知道與、或、非三個門。雖然從實際上真實電路的角度來講,與非門、或非路通常比起與、或門更爲簡單,但通常狀況下咱們可能更喜歡從與、或、非提及。sql

  與、或、非這三個門級的邏輯符號以下:編程

  與或非門符號

 

   與門的真值表以下:微信

輸入1 輸入2 輸出

   或門的真值表以下:數據結構

輸入1 輸入2 輸出

   非門的真值表以下:函數式編程

輸入 輸出

  除此以外還有異或門、同或門比較經常使用,符號以下:函數

  異或門和同或門符號

 

 

  異或門的真值表以下:測試

輸入1 輸入2 輸出

  同或門的真值表以下:優化

輸入1 輸入2 輸出

 

 

   組合電路

 

  將以上的門級電路連在一塊兒,獲得組合電路。前提是,組合電路沒有反饋。

  解釋一下反饋的意思,

  若是將組合電路當作一個有向圖,有向圖的頂點爲各組短接在一塊兒的導線,邊爲每一個門級上的輸入到輸出。

  好比

  組合電路

   在以上定義下,上面電路圖所對應的有向圖有7個頂點,a,b,c,d,e,f,g,邊爲<a,e>,<b,f>,<c,f>,<e,g>,<f,g>,<e,d>,<g,d>。

  若是有向圖沒有環,則該組合電路沒有反饋。

  

  那麼有沒有有反饋的電路呢?舉一個例子以下:  

  RS觸發器

  四條邊<R,Q>,<P,Q>,<Q,P>,<S,P>中<P,Q>和<Q,P>組成了一個環,這就是反饋,產生了時序方面的東西,就不是組合電路了。實際上,這是一個RS觸發器。

   

   組合電路的描述

 

  以上的電路圖固然描述了電路,只是,處於仿真的須要,咱們須要更爲精確而簡潔的信息。

  咱們能夠把上述電路圖中的頂點提出來,稱爲wire。

  組合電路

  好比對於verilog,咱們能夠用如下來門級描述(實際上verilog能夠有幾種看起來徹底不同的RTL描述方式):

wire a;
wire b;
wire c;
wire d;
wire e;
wire f;
wire g;
not u1(e, a);
or u2(f, b, c);
xor u3(g, e, f);
and u4(d, e, g);

 

  以上顯然不符合Scheme的S-表達式,咱們採用define,用定義變量的手段定義各個wire:

  (define a (make-wire))

  (define b (make-wire))

  (define c (make-wire))

  (define d (make-wire))

  (define e (make-wire))

  (define f (make-wire))

  (define g (make-wire))

 

  make-wire是函數,而各個wire用變量來表示。

  用門將各個wire連起來,

  (not-gate e a)

  (or-gate f b c)

  (xor-gate g e f)

  (and-gate d e g)

 

  not-gate、or-gate、xor-gate、and-gate都是用函數來表示門級,甚至於,咱們能夠經過與、或、非三個門來定義其中用異或門。

  

  上面就是用與、或、非門實現的異或門,verilog實現以下:

module xor_gate(
        output z,
        input x,
        input y);
wire nx;
wire ny;
wire p;
wire q;

not u1(nx, x);
not u2(ny, y);
and u3(p, nx, y);
and u4(q, x, ny);
or u5(z, p, q);
endmodule

 

  Scheme仿真也同樣能夠引入模塊建構能力,按照上面Scheme的描述,不難寫出xor-gate的Scheme函數實現應該以下:

   (define (xor-gate z x y)

     (let ((nx (make-wire))

        (ny (make-wire))

        (p (make-wire))

        (q (make-wire)))

            (begin (not-gate nx x)

              (not-gate ny y)

         (and-gate p nx y)

         (and-gate q x ny)

         (or-gate z p q))))

 

  仿真

 

  組合電路的仿真在於給定每一個輸入的信號,而後獲得輸出的信號,仿真比較簡單,

  爲了達到這個目的,咱們能夠定義一個set-signal函數,用於給wire設置信號,高低電平咱們通常用一、0表示。

  組合電路

  好比,咱們將a、b、c設爲0、一、0,

  (set-signal a 0)

  (set-signal b 1)

  (set-signal c 0)

 

  再給個仿真函數sim用於推理出信號的值,不須要返回值,但邏輯上是作了信號的推理。

  好比對於咱們的須要來講,咱們最終是爲了觀察信號g,那麼咱們能夠執行

  (simulate g)

  

  最後,咱們能夠再經過get-signal來獲取想要觀察的信號,

  (get-signal g)

  

  對於這個電路,以及上述的輸入信號,(get-signal g)會返回0。

 

  實現

 

  以上的仿真中,全部的wire都是變量,而且構建電路時使用函數。若是是純函數則不會影響全局的環境,只有改變了變量這樣的反作用發生,上面構建電路方法纔是有效的。上述跡象代表,此時使用的絕對不是函數式編程。表示wire的變量顯然承載了整個電路的全部信息,而且隨時能夠經過門電路函數讓任意兩個wire變量產生聯繫。咱們能夠經過序偶來實現這一切。

  全部的Lisp裏,最經常使用的手法固然是使用序偶(pair)來表示一切(其實Lisp也就是List Processing,list也是一種序偶),序偶也是數學裏很基本的概念,用來表示有序的一對數據,所謂有序,意思就是序偶中的兩個數據分先後,這和兩個數據組成的集合不一樣。Scheme爲序偶準備了三個函數:conscarcdr。cons用於生成一個序偶,car用於取序偶的第一個數據,cdr用於取序偶的第二個。

> (define s (cons 1 2))
> s
(1 . 2)
> (car s)
1
> (cdr s)
2

  Lisp裏的pair,像'(1 . 2)這樣一個pair是如下這樣的結構

  

 

   這兩個箭頭表明的是,序偶裏先後兩個存的是值的引用,而不是值。這一點很是重要,利用這個性質能夠構造不少的數據結構,好比最簡單的列表(或者也能夠叫鏈表)。

  好比列表 '(1 2 3)其實是'(1 . (2 . (3 . ()))),也就是以下圖這樣的結構

  

   

  既然pair裏存的是引用,Scheme早在最先的標準中就規定了set-car!set-cdr!用於修改pair中所存儲的兩個引用,以此實現各類複雜的數據結構。咱們使用set!彷佛作到,好比能夠這樣寫,

  (define (my-set-car! v x) (set! v (cons (car v) x)))

  (define (my-set-cdr! v x) (set! v (cons x (cdr v))))

  可是set-car!和set-cdr!實現的顆粒能夠更加的細,上述的my-set-car!和my-set-cdr!須要從新構建序偶,會破壞數據結構。

 

  而後,咱們能夠考慮如何表示電路的數據結構了。

  咱們能夠考慮用一個pair來表示wire,這個pair的第一個對象用來表明邏輯值,第二個對象用來表明wire的鏈接關係。

  

 

  而原來電路

   組合電路

  能夠用如下這樣的數據結構來表示:

  單個wire

 

   每一個wire都對應着這樣的一個結構,若是是一個門(只限於與、或、非)的輸出,那麼右邊就是這樣的一個列表,列表第一個表元指向門的類型(用symbol表示),後面的表元指向各個輸入的wire;而若是這wire是整個電路的輸入信號,右邊則指向空列'()。

  因而整個組合電路的數據結構就對應於上述定義下的一個圖(圖比較複雜,略)。

   用個相對簡單的電路來表示一下整個數據結構:

  帶一個與門和一個或門的電路

   其數據結構以下:

  圖狀組合電路數據結構

   當咱們用make-wire創建一個wire的時候,其邏輯值未定,wire也未與任何門相連,因而咱們可讓這個pair的第一個元給個默認邏輯值0,第二個元指向空列,即

  (define (make-wire) (cons 0 '()))

   注意,後面是(cons 0 '()),而不能是已經構造好的'(0),這樣,每次返回的纔是不一樣的pair,這一點是必須的,並且是可能出錯的地方。

 

  set-signal和get-signal這兩個函數用於設置、獲取wire的信號,顯然就是對pair的第一個元素進行操做,因而很簡單就能夠實現

  (define (set-signal x v) (set-car! x v))

  (define (get-signal x) (car x))

 

  與或非門的實現,好比與門

  單個wire

  實際上就是先造出列表來表示門和各個輸入信號,而後再操做pair的第二個元素指向這個列表。

  對於非門只會有一個輸入信號,

  (define (not-gate x y) (set-cdr! x (list 'not y)))

  而對於與、或門,會有多個輸入信號(可能不僅兩個),因而咱們用可變參數的寫法了。

  (define (and-gate x . input-list) (set-cdr! x (cons 'and input-list)))

  (define (or-gate x . input-list) (set-cdr! x (cons 'or input-list)))

  注意這裏,input-list是輸入信號列表,原本就是列表,因此只須要用cons把'and或者'or接在前面便可造出須要的完整列表了。

 

  其實,經過這麼一個圖,咱們也很容易看出信號仿真很明顯就是一個遞歸。

  圖狀組合電路數據結構

  計算一個wire的邏輯值,則看它的第二個元是否是空表:

  若是是,則表明這個wire確定是整個電路的輸入信號,沒有其餘門的依賴,因此不用計算;

  而若是不是,則必定是某個門的輸出,因而先計算出每一個輸入的信號,再最後計算值。

  用代碼來寫就是以下了:

(define (sim s)
 (if (null? (cdr s));第二個元指向的是否是一個空表
  (do-nothing);若是是空表,則不須要計算了
  (begin
   (for-each sim (cddr s));挨個去計算每個輸入信號
   (case (cadr s);看一下是什麼門
    ((not);非門
     (if (zero? (caaddr s))
      (set-car! s 1)
      (set-car! s 0)))
    ((and);與門
     (set-car! s (cal-and (cddr s))))
    ((or);非門
     (set-car! s (cal-or (cddr s))))
    (else (do-nothing))))))

 

  cal-and和cal-or也很容易用遞歸實現,而不是用fold,由於如下效率比起fold每一個都算還較高一些:

(define (cal-and wires)
 (cond
  ((null? wires) 1)
  ((zero? (caar wires)) 0)
  (else (cal-and (cdr wires)))))
(define (cal-or wires)
 (cond
  ((null? wires) 0)
  ((eq? 1 (caar wires)) 1)
  (else (cal-or (cdr wires)))))

  而do-nothing函數,Chez上能夠用void。

 

  變量做用域問題 

 

  咱們上面用到的都是全局變量,不少時候咱們或許不想污染全局環境。因而咱們能夠採用面向對象的方式,不少語言均可以直接在語言層次上支持。Lisp做爲彈性十足的語言,有多種方式來支持面向對象。

  問題簡單化一點,咱們就設想一個銀行卡的簡單系統,支持存錢、取錢、查看錢、查看歷史四個操做,爲了簡單起見,咱們不去管利息,取錢也能夠任意取,不用擔憂透支。

(define (make-card q history)
 (lambda s
  (case (car s)
   ((存款)
    (begin (set! q (+ q (cadr s)))
     (set! history (cons (list q '存款 (cadr s)) history))
     q))
   ((取款)
    (begin (set! q (- q (cadr s)))
     (set! history (cons (list q '取款 (cadr s)) history))
     q))
   ((查餘額) q)
   ((查歷史) (reverse history))
   ((else) '()))))

  

   以上就是Scheme自然支持一種方式的面向對象,make-card函數就是爲了產生對象,所謂對象就是構造了一個環境,其中qhistory是對象的屬性,而存款取款查餘額查歷史則是對象的方法。全部的處理都在對象的內部,不會影響到全局環境。

  咱們測試一下,

(define id-1 (make-card 0 '()));產生一個對象
(id-1 '存款 1000)
(id-1 '存款 2000)
(id-1 '取款 500)
(id-1 '存款 3000)
(display (format "餘額: ~a" (id-1 '查餘額)))
(newline)
(display "歷史:")
(newline)

;查看全部的歷史
(for-each
 (lambda (x) (display (format "~a~a 餘額: ~a" (cadr x) (caddr x) (car x)))(newline))
 (id-1 '查歷史))

 

  運行一下,結果以下:

餘額: 5500
歷史:
存款1000 餘額: 1000
存款2000 餘額: 3000
取款500 餘額: 2500
存款3000 餘額: 5500

 

  這樣的思路徹底能夠用來改造上述的仿真。

  

  其餘問題

 

  然而,咱們可能仍是會去想,

  (for-each sim (cddr s))

  面對一個門,算出它每個輸入,是否是應該如此。其實顯然不須要如此,上面這兩個cal-and和cal-or函數之因此不用fold就已是優化了。

  然而,任何狀況下,整個電路里全部的wire都被計算了,實際上,不少狀況可能不須要計算這麼多。

  好比

  

 

  根本不須要計算下面非門的輸出信號,就能夠知道最終信號是1。

   

 

   另外,還有信號重複計算問題,好比

  組合電路

  其中e可能面臨着兩次計算。

  這些問題如何解決呢?固然,這已經上升到算法問題了,脫離了本章的主題,這裏並再也不給出答案,留給有興趣的讀者本身去考慮。

相關文章
相關標籤/搜索