3.3.4 数字电路的模拟器
设计一个复杂的数字系统,例如计算机,是一个重要的工程活动。
数字系统由相互连接的简单的组件组成的。尽管这些单独的组件的
行为是简单的,它们的网络能有非常复杂的行为。待选的电路的设计
的计算机模拟是数字系统工程师的一个重要的工具。在这一部分中,我们
设计一个系统来执行数字逻辑的模拟。这个系统作为一类程序的特征
叫做事件驱动模拟。它的事件触发器触发稍后发生的事件,这事件再触发
更多的事件,等等。
我们的电路的计算模型由这样的一些对象组成。这些对象对应于由电路组成的
最基础的组件。有电线传输着电信号。一个数字的信号仅有两个可能的值之一,0和1。
也有多个数字功能盒子的类型。它连接有输入信号的电线,和其它输出信号的电线。
这样的功能盒子计算它们的输入信号,产生输出信号。根据功能盒子的类型不同,输出信号被延迟一段时间。
例如一个反转器是一个原生的功能盒子。它反转输入信号。
如果进入反转器的一个输入信号改为0,
那么一个反转器的延迟时间之后,反转器将把输出信号改为1。
如果进入反转器的一个输入信号改为1,
那么一个反转器的延迟时间之后,反转器将把输出信号改为0。
我们在图3.24中画了一个反转器的符号,一个与门也显示在图中。
与门是一个原生的功能盒子,它有两个输入和一个输出。
它让输出信号等于输入信号的逻辑与的结果值。也就是说,如果输入的信号都是1,
那么一个与门延迟时间之后,与门强制它的输出信号为1;否则输出为0。
一个或门是一个与之相似的两个输入的原生的功能盒子,它让输出信号
等于输入信号的逻辑或的结果值。也就是说,输出将是1,
如果至少有一个输入信号是1;否则输出是0。
图3.24 在数字逻辑模拟器中的原生的功能
我们能连接原生的功能在一起来组装起更复杂的功能。为了完成这个任务,
我们把一些功能盒子的输出连接到其它功能盒子的输入。例如,在图3.25中显示的
半加法器电路由一个或门,两个与门,一个反转器组成。它接受两个输入信号A和B,
并且它有两个输出信号S和C。S将成为1,当A和B仅仅有一个是1时,C将为1当A和B都为1时。
从图中我们能看到,由于延迟的加入,输出可能产生于不同的时机。在数字电路的设计中,
许多困难之处都源于这个事实。
图3.25 一个半加法器的电路
我们现在将构建一个程序为我们要研究的数字逻辑电路进行建模。
这个程序将组装计算对象来对线路进行建模,它将传递信号。
功能盒子由程序来建模,这个程序来保证信号之间的正确的关系。
我们的模拟的基本的元素是一个程序make-wire,它组装了线路。例如,
我们能组装6个线路,如下:
(define a (make-wire))
(define b (make-wire))
(define c (make-wire))
(define d (make-wire))
(define e (make-wire))
(define s (make-wire))
通过调用程序来组装那种类型的盒子,我们把一个功能盒子附加到一些线路的集合中。
组装子程序的实际参数是被附加到盒子上的线路。例如,给定的情况是我们能组装与门,
或门和反转器,我们能连接线路,形成如图3.25所示的半加法器:
(or-gate a b d)
ok
(and-gate a b c)
ok
(inverter c e)
ok
(and-gate d e s)
ok
然而,更好的是通过定义一个程序half-adder来组装这个电路的方式我们能
够对这个操作进行命名,给这个半加法器附加上四条外部的线路:
(define (half-adder a b s c)
(let ((d (make-wire)) (e (make-wire)))
(or-gate a b d)
(and-gate a b c)
(inverter c e)
(and-gate d e s) 'ok
)
)
做出这个定义的好处是在创建更复杂的电路时,我们能使用这个半加法器作为它的构建
模块。在图3.26中,例如, 显示了一个全加法器由两个半加法器和一个或门组成。
我们能组装一个全加法器如下:
(define (full-adder a b c-in sum c-out)
(let ((s (make-wire)) (c1 (make-wire)) (c2 (make-wire)))
(half-adder b c-in s c1)
(half-adder a s sum c2)
(or-gate c1 c2 c-out) 'ok)
)
已经定义了全加法器作为一个程序,我们现在能看到它作为一个构建
模块来创建更加复杂的电路。(例如,见练习3.30)
图3.26 一个全加法器的电路
在本质上,我们的模拟吕提供给我们组装一个电路的语言的工具。如果我们采用了
语言上的通用的角度,也就是我们在1.1部分中Lisp的研究的方法,我们能说
原生的功能盒子形成了语言的原生的元素,线路盒子在一起提供了一个组合的方法,
指定线路的模式作为程序,是一种抽象的方法。
* 原生的功能的盒子
原生的功能盒子实现了这种力量,即在一个线路上的信号的改变能影响了其它的线路上的
信号们。为了构建功能盒子,我们看到了在线路上的如下的操作:
(get-signal <wire>)
返回线路上的信号的当前值。
(set-signal! <wire> <new value>)
把线路上的信号的值修改为新值。
(add-action! <wire> <procedure of no arguments>)
断言无论何时在线路上的信号有改变,特定的程序应该能运行。这样的程序是
驱动器,它通过在线路上的信号值的改变来和其它的线路进行通信。
此外,我们将利用一个程序after-delay,来得到一个时间的延迟和一个被运行的程序
并且执行一个给定的延迟后的给定的程序。
使用这些程序,我们能定义原生的数学逻辑函数。为了连接一个输入到输出
通过一个反转器,我们能使用add-action!,来关联一个输入的线路和一个程序
这个程序在无论何时在线路上的输入信号有改变时,就将运行起来。程序计算输入
信号的逻辑非,然后,在一个反转器的时间延迟后,设置输出信号到这个新的值:
(define (inverter input output)
(define (invert-input)
(let ((new-value (logical-not (get-signal input))))
(after-delay inverter-delay
(lambda ()
(set-signal! output new-value))))
)
(add-action! input invert-input)
'ok
)
(define (logical-not s)
(cond ((= s 0) 1)
((= s 1) 0)
(else (error "Invalid signal" s))
)
)
一个与门是有一点更复杂了。如果门中的任何一个输入有改变的话,动作程序必须被运行起来。
它计算输入的线路上的信号的值的逻辑与(使用程序与非门的程序相似)并且在一个与门延迟之后,
在输出线路上发生一个设置成新值的改变。
(define (and-gate a1 a2 output)
(define (and-action-procedure)
(let ((new-value (logical-and (get-signal al) (get-signal a2))))
(after-delay and-gate-delay (lambda () (set-signal! output new-value))))
)
(add-action! a1 and-action-procedure)
(add-action! a2 and-action-procedure)
'ok
)
练习3.28
设计一个或门作为一个原生的功能盒子。你的或门组装子应该与与门的相似。
练习3.29
组装一个或门的另一个方式是作为一个复合的数字逻辑设备,从与门和反转器来构建。
定义一个或门程序来完成这个任务。在使用了与门延迟和反转器的延迟后,或门的延迟时间是什么?
练习3.30
图3.27显示了一个连锁进位的加法器由若干个全加法器串联而成。这是为了两个有n位的二进制数
相加的并行加法器中的最简单的形式了。输入A1,A2,A3....An 和B1,B2,B3,,,Bn是被相加的两个二进制数。
电路生成了S1,S2,S3....Sn它们是和的n位,c是相加后的进位。写一个程序ripple-carry-adder
生成了这个电路。这个程序以三个列表为实际参数。这三个列表的元素有Ak,Bk,Sk.还有线路C。
连锁进位的加法器的主要的缺点是需要等待进位信号的参与。以与门,或门,反转器的延迟来表达延迟,
为了从一个有N位的连锁进位的加法器中得到完整的输出,需要的延迟是多少?
图3.27 一个有n位的数的连锁进位的加法器
* 表示线路
在我们的模拟中,一个线路将是一个计算对象带有两个局部状态变量:一个信号值(初始时为0)
和当信号修改值时能被运行的动作程序的一个集合。我们实现了线路,使用消息传递方式,作为
一个局部程序的集合放在一起,以分发程序来选择合适的局部操作,正如,我们在3.1.1部分中的
简单银行账户对象时,我们所做的那样。
(define (make-wire)
(let ((signal-value 0) (action-procedures '()))
(define (set-my-signal! new-value)
(if (not (= signal-value new-value))
(begin (set! signal-value new-value)
(call-each action-procedures)) 'done)
)
(define (accept-action-procedure! proc)
(set! action-procedures (cons proc action-procedures))
(proc))
(define (dispatch m)
(cond ((eq? m 'get-signal) signal-value)
((eq? m 'set-signal!) set-my-signal!)
((eq? m 'add-action!) accept-action-procedure!)
(else (error "Unknown operation ---WIRE" m)))
)
dispatch
)
)
内部程序set-my-signal!测试是否新信号值改变了线路上的信号。如果是,它使用如下的程序
call-each,来运行动作程序中的每一个,这个程序调用没有参数的程序的列表中的每一个项:
(define (call-each procedures)
(if (null? procedures)
'done
(begin ((car procedures))
(call-each (cdr procedures)))
)
)
内部程序accept-action-procedure!添加给定的程序要运行的程序的列表中,
并且然后运行一次这个新的程序。(见练习3.31)
有内部的分发程序如指定的情况安装,我们能提供如下的程序从线路上读取内部的操作。
(define (get-signal wire) (wire 'get-signal))
(define (set-signal! wire new-value) ((wire 'set-signal!) new-value))
(define (add-action! wire action-procedure) ((wire 'add-action!) action-procedure))
线路有随时间改变的信号和可能被附加到设备上,是典型的可变对象。
我们以带有局部状态变量的程序来为它们建模,这些局部状态变量可
以用赋值语句修改。当一个新的线路创建后,一个新的状态变量的集合被分配
(在make-wire使用let表达式)和一个新的分发程序被组装和返回,用新的状态变量
捕获了环境。
线路被多个连接它的设备共享。因此,与一个设备的交互产生的改变
将影响到所有的与这个线路相联的设备。当连接被建立后,通过调用
提供给它的动作程序,线路与它的邻居沟通信号的变化。
* 议事日程
完成模拟器仅需要的是after-delay.这里的想法是我们维护一个数据结构
叫做议事日程,它包括一个要做的事情的时刻表。为了议事日程定义了如下的操作:
(make-agenda)
返回一个新的空的议事日程
(empty-agenda? <agenda>)
如果指定的议事日程是空的,返回真。
(first-agenda-item <agenda>)
返回议事日程中的第一项
(remove-first-agenda-item! <agenda>)
删除议事日程中的第一项
(add-to-agenda! <time> <action> <agenda>)
修改议事日程,通过添加给定的动作程序,让它在指定的时间执行。
(current-time <agenda>)
返回当前的模拟时间
我们所使用的议事日程是由the-agenda提供的。程序after-delay
添加新的元素到the-agenda:
(define (after-delay delay action)
(add-to-agenda! (+ delay (current-time the-agenda))
action
the-agenda))
程序propagate驱动着模拟过程,它在the-agenda上操作,按顺序执行议事日程中的
每一个程序。总之,随着模拟过程的运行,新的项被添加到议事日程中,并且
propagate将继续模拟过程,只要议事日程中有项存在。
(define (propagate)
(if (empty-agenda? the-agenda)
'done
(let ((first-item (first-agenda-item the-agenda)))
(first-item)
(remove-first-agenda-item! the-agenda)
(propagate))))
* 一个样品的模拟
如下的程序在一个线路上设置了一个探针,它显示模拟器上的动作。
探针告诉线路,无论何时线路上的信号值有了改变,线路应该打印
的新的信号值,以及当前的时间和线路的名称:
(define (probe name wire)
(add-action! wire
(lambda ()
(newline)
(display name)
(display " ")
(display (current-time the-agenda))
(display " New-value = ")
(display (get-signal wire))
)
)
)
我们开始于初始化议事日程,并且指定原生功能盒子的延迟:
(define the-agenda (make-agenda))
(define inverter-delay 2)
(define and-gate-delay 3)
(define or-gate-delay 5)
现在我们定义四个线路,在它们的两个中设置探针:
(define input-1 (make-wire))
(define input-2 (make-wire))
(define sum (make-wire))
(define carry (make-wire))
(probe 'sum sum)
sum 0 New-value = 0
(probe 'carry carry)
carry 0 New-value = 0
现在我们在一个半加法器的电路中连接线路(如图3.25),设置信号
在input-1上的为1,运行这个模拟:
(half-adder input-1 input-2 sum carry)
ok
(set-signal! input-1 1)
done
(propagate)
sum 8 New-value = 1
done
在时间8时,sum这个信号变成了1。我们现在从模拟开始过了8个时间单位。
在这个点上,我们能设置input-2为1,并且允许这个值进入模拟中。
(set-signal! input-2 1)
done
(propagate)
carry 11 New-value = 1
sum 16 New-value = 0
done
在时间11时,进位这个信号变成了1,并且sum在时间16时变成了0。
练习3.31
内部程序accept-action-procedure!定义在make-wire程序中,它指定了
当一个新的动作程序被加入到线路中时,程序要立即运行。解释为什么
这个初始化是必要的。特别是,跟踪半加法器的例子在上边这段中,说
如果我们定义了accept-action-procedure!v如下的话,系统如何返回
不同于之前的情况?
(define (accept-action-procedure! proc)
(set! action-procedures (cons proc action-procedures))
)
* 实现议事日程
最后,我们给出议事日程这个数据结构的细节,它包括了被预计为未来
执行的程序。
议事日程由时间片段组成。任何一个时间片段是一个由一个数(时间)
和一个队列(见练习3.32)组成的数对。这个队列包括了程序,这些程序
被制定了时刻表在那些时间片段中被运行。
(define (make-time-segment time queue)
(cons time queue)
)
(define (segment-time s) (car s))
(define (segment-queue s) (cdr s))
我们将使用3.3.2部分中的描述的队列操作来操作时间片段的队列。
议事日程本身是一个一维的时间片段的表格。它不同于在3.3.3部分中
描述的表格。片段将被以时间的升序进行排序。除此,我们存储,当前
的时间(例如,最后的动作被执行的时间)在议事日程的头部。一个新的
组装了的议事日程没有时间片段并且和一个为0值是当前时间:
(define (make-agenda) (list 0))
(define (current-time agenda) (car agenda))
(defne (set-current-time! agenda time) (set-car! agenda time))
(define (segments agenda) (cdr agenda))
(define (set-segments! agenda segments) (set-cdr! agenda time))
(define (first-segment agenda) (car (segments agenda)))
(define (rest-segment agenda) (cdr (segments agenda)))
如果没有时间片段,一个议事日程是空的:
(define (empty-agenda? agenda)
(null? (segments agenda)))
为了添加一个动作到议事日程中,我们首先检查议事日程是否是空的。如果是
我们就为动作创建一个时间片段,并且把它安装到议事日程中。否则,我们扫描
议事日程,检查每个片段的时间。如果我们发现一个我们约定的时间的片段,
我们能把动作添加到相关联的队列中。如果我们到达了比我们的约定时间更晚的时间时,
我们插入一个时间片段到它之前的议事日程之中。如果我们到达了议事日程的结尾,
我们必须创建一个新的时间片段在结尾处。
(define (add-to-agenda! action agenda)
(define (belongs-before? segments)
(or (null? segments) (< time (segment-time (car segments))))
)
(define (make-new-time-segment time action)
(let ((q (make-queue)))
(insert-queue! q action)
(make-time-segment time q))
)
(define (add-to-segments! segments)
(if (= (segment-time (car segments)) time)
(insert-queue! (segment-queue (car segments)) action)
(let ((rest (cdr segments)))
(if (belongs-before? rest)
(set-cdr! segments (cons (make-new-time-segment time action)
(cdr segments)))
(add-to-segments! rest))))
)
(let ((segments (segments agenda)))
(if (belongs-before? segments)
(set-segments! agenda (cons (make-new-time-segment time action) segments))
(add-to-segments! segments)
))
)
程序从议事日程中移除第一项,就是在第一个时间片段中,删除队列头部的项。如果这个删除让
时间片段为空,我们把这从片段的列表中移除:
(define (remove-first-agenda-item! agenda)
(let ((q (segment-queue (first-segment agenda))))
(delete-queue! q)
(if (empty-queue? q)
(set-segments! agenda (rest-segments agenda))))
)
在队列的头部找到的第一个议事日程的项,在第一个时间片段。当我们抽取一个项时,
我们也要更新当前的时间:
(define (first-agenda-item agenda)
(if (empty-agenda? agenda)
(error "Agenda is empty --First agenda item" )
(let ((first-seg (first-segment agenda)))
(set-current-time! agenda (segment-time first-seg))
(front-queue (segment-queue first-seg))
)
)
)
练习3.32
在议事日程中的任何一个时间片段的期间被运行的程序都保存在一个队列中。
因此,任何一个片段的程序都被顺序地调用,以它们被加入到议事日程中的顺序
(先进先出)。解释为什么必须使用这种顺序。特别是跟踪一个与门,在一个
相同的时间片段内,它的输入从0,1改变为1,0,如果我们存储一个片段的程序
以一个普通的列表,说出行为如何区别?添加与删除程序仅在头部进行(后进,先出)