版权申明:本文为博主窗户(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>。
若是有向图没有环,则该组合电路没有反馈。
那么有没有有反馈的电路呢?举一个例子以下:
四条边<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为序偶准备了三个函数:cons,car,cdr。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都对应着这样的一个结构,若是是一个门(只限于与、或、非)的输出,那么右边就是这样的一个列表,列表第一个表元指向门的类型(用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))
与或非门的实现,好比与门
实际上就是先造出列表来表示门和各个输入信号,而后再操做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函数就是为了产生对象,所谓对象就是构造了一个环境,其中q、history是对象的属性,而存款、取款、查余额、查历史则是对象的方法。全部的处理都在对象的内部,不会影响到全局环境。
咱们测试一下,
(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可能面临着两次计算。
这些问题如何解决呢?固然,这已经上升到算法问题了,脱离了本章的主题,这里并再也不给出答案,留给有兴趣的读者本身去考虑。