Scheme实现数字电路仿真(3)——模块

2020-03-19 09:06:10 浏览数 (1)

  上一章介绍了数字电路的重要概念原语,可以用来做门级的元件。这一章里,我们在原语的基础上再引入模块的概念。

Verilog模块

  模块就是电路的具体描述了,当然上一章的原语也是用来描述电路,但一般原语是为了构造门级或者不可分割的元件级电路,而模块则是包含更广的需求,拿来设计更为复杂的电路。比如我们可以用Verilog模块来描述一段4位加法器。

代码语言:javascript复制
module add4(in1, in2, cin, out);
input [3:0]in1, in2;
input cin;
output [4:0]out;
assign out = in1   in2   cin;
endmodule

  以上是RTL(Register Transfer Level),不能直接反映电路的形状(虽然在不优化的情况下与粗粒度上的电路存在对应关系),如果用门级电路来描述就比较多,不过我们门级描述其实可以引入分层设计。以下体现分层设计的思想。

  先设计一个半加器(Half adder),也就是两个bits(姑且称为a、b)的输入,把两者看成1位二进制数,求和得到一个2位二进制输出(称低位为s,高位称为c)。于是很容易得到,用一个异或门得到低位输出s,用一个或门得到高位输出c。Verilog描述如下:

代码语言:javascript复制
module half_add(a, b, s, c);
input a, b;
output s, c;
xor u1(s, a, b);
and u2(c, a, b);
endmodule

  于是用两个半加器和一个或门级联就得到了一位的全加器,这应该是在学习数字电路的时候我们都会很熟悉的结果:

  用Verilog描述:

代码语言:javascript复制
module full_add(a, b, cin, out);
input a, b, cin;
output [1:0]out;

half_add u1(
        .a(a),
        .b(b),
        .s(s1),
        .c(s2)
);
half_add u2(
        .a(s1),
        .b(cin),
        .s(out[0]),
        .c(s3)
);
or u3(out[1], s2, s3);
endmodule

  最终,4个全加器级联成1个4位加法器:

代码语言:javascript复制
module add4(in1, in2, cin, out);
input [3:0]in1, in2;
input cin;
output [4:0]out;

wire c0, c1, c2;

full_add u1 (
        .a(in1[0]),
        .b(in2[0]),
        .cin(cin),
        .out({c0, out[0]})
);
full_add u2 (
        .a(in1[1]),
        .b(in2[1]),
        .cin(c0),
        .out({c1, out[1]})
);
full_add u1 (
        .a(in1[2]),
        .b(in2[2]),
        .cin(c1),
        .out({c2, out[2]})
);
full_add u1 (
        .a(in1[3]),
        .b(in2[3]),
        .cin(c2),
        .out(out[4:3])
);
endmodule

  我们在设计数字电路的时候,无论是用原始的原理图设计,还是使用HDL设计,一个大一点的设计一般都是如此级联或分层,某些时候可以借助软件的设计思想,比如可以提取公共的公共的功能,单独设计模块,然后在不同的地方例化。Verilog甚至有parameter这样的东西,使得相同的设计在不同的例化中成为不同位数的电路。

  很多结构化的模型里都会有图(graph)的概念,比如在流计算、神经网络,地图、网络中对于路由的计算等。

  比如上面这个电路,一共存在a、b、c、d、e、f、g七个在门之间传递信号的连接线,连接了一个非门、一个或门、一个异或门和一个与门。

  我们把这些门看成是图的点,而把两个点之间的连接看成是一个有向边,也就是一个连接线可能不止对应一条边,这样电路图就是一个有向图了。可是我们很快发现a、b、c、d只有一个点可以连,无法构成边,这显然不符合图论。但同时,我们意识到a、b、c、d正好是整个电路对外的输入/输出信号。于是为了图的完整,我们再为每个输入/输出造特殊的顶点类型,这类顶点只与具体输入/输出信号连接。这样,图就完整了。实际上,大多数EDA引入原理图输入的时候都会引入这样的一个标记,以下是QuatusII画的原理图

  于是我们得到之前要表示的电路的图中所有的边与顶点如下:

顶点:

A : input-pin(a)

  B : input-pin(b)

  C : input-pin(c)

  D : output-pin(d)

  E : not-gate([a],[e])

  F : or-gate([b,c],[f])

  G : xor-gate([e,f],[g])

  H : and-gate([e,g],[d])

  边:

  A->E

  B->F

  C->F

  E->H

  E->G

  F->G

  G->H

  H->D

  结合上一节所说,再次确定一下,一个模块所表示的图的顶点可能是input/output、原语或者别的模块。

  我们知道,时序电路里的基本元件,比如各种锁存器、触发器,是用各种组合电路反馈得到的。反馈对应于有向图有环。实际上,很多HDL是支持反馈的,比如verilog,完全可以成功仿真。但反馈是要靠不同的手段才可以推出其逻辑语意,并且实际中一般不会如此方式设计电路,所以暂时可以不支持反馈。

表示

  于是,我们模块中所需要的就是要去表示上节提到的图。这就涉及到有向图该如何表示,实际上我们有很多不同的方法来表示。

  还是以这个图为例,

  以下几个list可以描述图中所有的顶点,

  (input-pin a)

  (input-pin b)

  (input-pin c)

  (output-pin d)

  (not-gate (a) (e))

  (or-gate (b c) f)

  (xor-gate (e f) (g))

  (and-gate (e g) (d))

  以上只是用Lisp的括号来表示的list,实际上并不是十分严格。其实这些也携带了有向图的各个边的信息,于是如果以上8个顶点的list分别为s1~s8,那么(s1 s2 s3 s4 s5 s6 s7 s8)就是整个电路图了(虽然如此效率比较低一点,因为边不是直接存储的,需要搜索)。

接口

  类似于像第一章例子中构造异或这样的复杂门级那样,我们也可以模仿一下像以下这样定义本章例子电路模块,

代码语言:javascript复制
(define (newmodule input output edge)
 (let ((a (car input))
       (b (cadr input))
       (c (caddr input))
       (d (car output))
       (e (make-wire))
       (f (make-wire))
       (g (make-wire)))
  (make-primitive-instance not-gate (list a) (list e))
  (make-primitive-instance or-gate (list b c) (list f))
  (make-primitive-instance xor-gate (list e f) (list g))
  (make-primitive-instance and-gate (list e g) (list d))))

  这是希望和上一章的原语采用相同的语义。然而,和原语不一样的是,模块可以表示更复杂一些的电路:原语里的时序电路,所有的状态都在输出上;而更加复杂一些的电路,状态可能不止输出这些信号。

  比如以下verilog描述的模块

代码语言:javascript复制
module test
(
        reset,
        clk,
        en,
        in,
        out
);
input reset, clk, en, in;
output out;

reg [1:0]cnt;
assign out = cnt[1];

always@(posedge clk or posedge reset)
if(reset)
        cnt <= 2'b00;
else
        cnt <= cnt   2'b01;

endmodule

  其中的输出信号out并不代表着电路的所有状态,得再加上内部的cnt[0]在一起才是整体的状态(out是cnt[1])。

  于是,我们不得不去想,我们的module不大可能是和原语同一个接口了。我们回头再想一想,之前Scheme描述的原语实现的是无副作用的函数,也就是数学意义上的函数。而我们实际上可以引入副作用的方式来设计函数,我们可以把状态绑在module内部所有的wire上,这种方法第一章中提到过。

  那么,模块函数应该包含着建立上一节所提到的有向图结构以及建立相应每个wire的状态的信息。模块实例化则是函数返回一个带有副作用的闭包,参数edge是没有必要了,模块需要返回的最主要信息还是有向图结构信息,那么接口只需要删除掉edge,可以如下:

代码语言:javascript复制
(define (newmodule input output)
 (let ((a (car input))
       (b (cadr input))
       (c (caddr input))
       (d (car output))
       (e (make-wire))
       (f (make-wire))
       (g (make-wire)))
  (make-primitive-instance not-gate (list a) (list e))
  (make-primitive-instance or-gate (list b c) (list f))
  (make-primitive-instance xor-gate (list e f) (list g))
  (make-primitive-instance and-gate (list e g) (list d))))

  上面长的不太像数字电路设计,我们其实也可以考虑写成下面这样:

代码语言:javascript复制
(module newmodule
 (input a b c)
 (output d)
 (wire e f g)
 (
  (p not-gate (a) (e))
  (p or-gate (b c)(f))
  (p or-gate (e f) (g))
  (p and-gate (e g) (d))
 )
)

  这样的代码熟悉数字设计的朋友看起来会觉得比较熟悉,其中(p not-gate (a) (e))中最前面的p是用来区分原语和模块,如果填写字母m则代表模块,原因在于我这里原语和模块并没有统一,但如果统一了,则不需要这个标志了。

  包括Scheme在内的所有Lisp都有一种神奇的本领叫宏,让上述看起来面目全非的代码转换成之前要写的函数。

其他

  本章只是提到了一些思想,其实我们还有很多可能需要继续改造或者直接放弃的地方,以下列出几点:

  1.系列并没有给出inout,没有三态门。

  2.线与逻辑似乎并不好实现。

  3.原语和模块没有统一。

  4.只能做实现级的描述,无法做像verilog/VHDL那样的RTL。其实这里可以引入宏,来展开比较复杂表达式。

  5.将来为了仿真的方便,不考虑支持反馈,毕竟反馈在数字设计里用处不大。

0 人点赞