从0到1构建计算机(4/12)--时序逻辑芯片:时序门、寄存器、RAM、计数器

上一篇我们实现了一系列组合逻辑芯片和CPU的运算大脑:ALU,但是组合逻辑芯片是一个实时系统,只要输入值改变了输出值也马上跟着改变,上一秒辛苦计算出来的值,下一秒就消失不见了,因此我们需要一个具备"记忆功能"的原件,用于存储数据。这就是我们本篇要介绍的:时序逻辑芯片。

反馈电路、时钟系统、D触发器

组合逻辑芯片之所以无法记住上一次计算的值,是因为电流信息的流向是单向的,它总是向前流动,导致我们无法两次踏入同一条河流。要解决这个问题也很简单,我们引一条导线让数据回流,形成一个环路即可(即形成反馈)。下图就是用OR门加上一路环路构成的一位存储器,但他的缺点是只能记忆一次1的输入,后续无论输入如何改变,输出总是为1。如果想要实现功能更强大的记忆芯片就需要更复杂的电路设计,但仍然是基于门的输出再重新作为输入的原理。

提到"记忆"就不能不涉及到"时间",计算机中的时间是基于震荡时钟系统的,震荡时钟提供连续的交变信号(高低电平)从而形成离散的时间周期。时钟周期是计算机系统中各元器件能整齐划一,协调同步的工作的基础。当一个时钟周期到来时,计算机的各个元器件必须抓紧在本次周期内完成自己的任务,然后停下来安静等待下一个周期的来临(这也是为什么时钟频率通常被当做衡量计算机性能的重要指标,因为频率越高,单位时间内计算机完成的工作步骤就越多)。例如只有当时钟上升沿到来时,各时序芯片才会瞬间改变状态,其余时间保持状态锁定。如果把CPU、内存、输入输出设备比作乐团中各个乐器的乐手,那么时钟系统就是计算机乐团的乐队指挥。

在搭建hack的时候,我们一共用到了两个基本芯片(即不用自己实现的芯片),一个是上一篇用于搭建组合逻辑芯片系统的Nand,另外一个就是本片用于搭建时序逻辑芯片的DFF(data Flip-Flop:D触发器)。DFF的结构由R-S锁存器派生而来。(DFF的实现相对复杂,如果对锁存器和DFF感兴趣,可以参考《编码的奥秘》Charles Petzold)。
边沿触发的D型触发器

DFF包含1bit的输入和1bit的输出,另外DFF有一个时钟输入(图中的小三角代表时钟输入),DFF根据数据输入位和时钟输入位的输入实现了out(t)=in(t-1),即DFF简单的将前一个时间周期的输入值作为当前时间周期的输出值。这是DFF具备记忆功能的表现,也是我们后续实现16bit寄存器,16K、64K、256K等RAM的基础。

1位寄存器和16位寄存器

有了DFF的记忆功能,我们接下来先实现最简单1bit寄存器和16bit寄存器。1bit寄存器的接口包括:in输入管脚,负责传输输入数据;load管脚,load管脚是控制位,只有当load为1的时候,才会存储in的输入数据;out管脚输出当前存储的值。接口如下所示:

1bit芯片很好实现,主要是实现load位的控制功能即可,根据芯片描述可以把芯片设计为:load为1时选择输入in的数据,load为0时选择输入上一次寄存器保存的数据。这种选择功能正好是上一篇我们实现的Multiplexor芯片(后面在实现RAM的时候,Multiplexor在地址选择中的作用体现得更加明显)。1bit寄存器内部实现如下:

然后我们用硬件描述语言(HDL)实现:

/**
 * 1-bit register:
 * If load[t] == 1 then out[t+1] = in[t]
 *                 else out does not change (out[t+1] = out[t])
 */

CHIP Bit {
    IN in, load;
    OUT out;

    PARTS:
    Mux(a=outBack, b=in, sel=load, out=dffIn);
    DFF(in=dffIn, out=out, out=outBack);
}

有1bit寄存器,16bit寄存器可以看做是16个1bit寄存器的组合:

HDL实现:16bit寄存器

/**
 * 16-bit register:
 * If load[t] == 1 then out[t+1] = in[t]
 * else out does not change
 */

CHIP Register {
    IN in[16], load;
    OUT out[16];

    PARTS:
    Mux(a=outBack0, b=in[0], sel=load, out=dffIn0);
    DFF(in=dffIn0, out=out[0], out=outBack0);

    Mux(a=outBack1, b=in[1], sel=load, out=dffIn1);
    DFF(in=dffIn1, out=out[1], out=outBack1);

    Mux(a=outBack2, b=in[2], sel=load, out=dffIn2);
    DFF(in=dffIn2, out=out[2], out=outBack2);

    Mux(a=outBack3, b=in[3], sel=load, out=dffIn3);
    DFF(in=dffIn3, out=out[3], out=outBack3);

    Mux(a=outBack4, b=in[4], sel=load, out=dffIn4);
    DFF(in=dffIn4, out=out[4], out=outBack4);

    Mux(a=outBack5, b=in[5], sel=load, out=dffIn5);
    DFF(in=dffIn5, out=out[5], out=outBack5);

    // ...6到15
}

RAM

接着我们来实现计算机中另一个非常重要的组成部分:RAM。刚才看到,16bit寄存器可以由16个1bit的寄存器组合而成,同理一个mKB的RAM,可以由n个16位寄存器组合而成,16位寄存器就是RAM中的基本存储单元,寄存器的宽度(此处是16位)就是RAM"字"(WORD)的宽度。我们都知道,需要通过地址来访问RAM,RAM中每个寄存器都绑定到一个唯一地址。下图是RAM的接口设计,RAM有三种输入:数据输入,地址输入和加载位。地址指定了当前周期RAM中哪一个寄存器被访问,当进行读操作时(load=0),RAM立即输出被选中的寄存器的值,当进行写操作时(load=1),被选中的寄存器被赋予新的值,从下个周期开始RAM将开始发出新的值。输入和输出的数据宽度就是RAM"字"的宽度。

接下来我们要实现RAM8,RAM64,RAM512,RAM4K,RAM16K几种内存容量的RAM。RAMn中的n即代表RAM中有n个寄存器。例如RAM16K中有16K个16位寄存器,那么他的内存容量就是16K*(16/2)byte=32Kbyte。想要实现一个RAMn芯片,我们需要在RAMn芯片中集成n个寄存器,外加一些列控制逻辑(处理load)和地址选择逻辑,那么涉及到选择逻辑自然少不了Multiplexor和DMultiplexor。那么下面我们以RAM8为例讲一下我自己的实现方式:

  • RAM8中有8个寄存器
  • 8个寄存器有8个地址,所以我们的地址位需要2^n=8, n=3位
  • DMux用于解析写入时的地址,用3位sel分离出需要写入的那一路,再和load做与操作后作为寄存器的load位输入,最终通过load位来决定哪一个寄存器被写入In。
  • Mux用于解析读取时的地址,用3位sel在8个寄存器中选择想要读取的那一路数据

HDL实现:RAM8

/**
 * Memory of 8 registers, each 16 bit-wide. Out holds the value
 * stored at the memory location specified by address. If load==1, then 
 * the in value is loaded into the memory location specified by address 
 * (the loaded value will be emitted to out from the next time step onward).
 */

CHIP RAM8 {
    IN in[16], load, address[3];
    OUT out[16];

    PARTS:
    DMux8Way(in=true, sel=address, a=a, b=b, c=c, d=d, e=e, f=f, g=g, h=h);

    And(a=a, b=load, out=load0);
    And(a=b, b=load, out=load1);
    And(a=c, b=load, out=load2);
    And(a=d, b=load, out=load3);
    And(a=e, b=load, out=load4);
    And(a=f, b=load, out=load5);
    And(a=g, b=load, out=load6);
    And(a=h, b=load, out=load7);

    Register(in=in, load=load0, out=R0);
    Register(in=in, load=load1, out=R1);
    Register(in=in, load=load2, out=R2);
    Register(in=in, load=load3, out=R3);
    Register(in=in, load=load4, out=R4);
    Register(in=in, load=load5, out=R5);
    Register(in=in, load=load6, out=R6);
    Register(in=in, load=load7, out=R7);

    Mux8Way16(a=R0, b=R1, c=R2, d=R3, e=R4, f=R5, g=R6, h=R7, sel=address, out=out);
}

有了RAM8,我们就可以利用8个RAM8的组合实现RAM64,以此类推实现后面容量更大的RAM。

  • 实现RAM64的技巧是对地址位的拆分,RAM64需要6个地址位,把6个地址为拆分为3+3
  • 和实现RAM8的时候类似,需要引入一组Mux和DMux,这组选择芯片使用6位地址中的高三位,通过高3位把地址初步划分为8个地址段,每个地址段的容量即RAM8中寄存器的数量
  • 每个RAM8使用6位地址中的低三位,然后从中定位到目标地址寄存器

HDL实现:RAM64

/**
 * Memory of 64 registers, each 16 bit-wide. Out holds the value
 * stored at the memory location specified by address. If load==1, then 
 * the in value is loaded into the memory location specified by address 
 * (the loaded value will be emitted to out from the next time step onward).
 */

CHIP RAM64 {
    IN in[16], load, address[6];
    OUT out[16];

    PARTS:
    DMux8Way(in=true, sel[0]=address[3], sel[1]=address[4], sel[2]=address[5], a=a, b=b, c=c, d=d, e=e, f=f, g=g, h=h);

    And(a=a, b=load, out=load0);
    And(a=b, b=load, out=load1);
    And(a=c, b=load, out=load2);
    And(a=d, b=load, out=load3);
    And(a=e, b=load, out=load4);
    And(a=f, b=load, out=load5);
    And(a=g, b=load, out=load6);
    And(a=h, b=load, out=load7);

    RAM8(in=in, load=load0, address[0]=address[0], address[1]=address[1], address[2]=address[2], out=R0);
    RAM8(in=in, load=load1, address[0]=address[0], address[1]=address[1], address[2]=address[2], out=R1);
    RAM8(in=in, load=load2, address[0]=address[0], address[1]=address[1], address[2]=address[2], out=R2);
    RAM8(in=in, load=load3, address[0]=address[0], address[1]=address[1], address[2]=address[2], out=R3);
    RAM8(in=in, load=load4, address[0]=address[0], address[1]=address[1], address[2]=address[2], out=R4);
    RAM8(in=in, load=load5, address[0]=address[0], address[1]=address[1], address[2]=address[2], out=R5);
    RAM8(in=in, load=load6, address[0]=address[0], address[1]=address[1], address[2]=address[2], out=R6);
    RAM8(in=in, load=load7, address[0]=address[0], address[1]=address[1], address[2]=address[2], out=R7);

    Mux8Way16(a=R0, b=R1, c=R2, d=R3, e=R4, f=R5, g=R6, h=R7, sel[0]=address[3], sel[1]=address[4], sel[2]=address[5], out=out);
}

HDL实现:RAM4K

/**
 * Memory of 4K registers, each 16 bit-wide. Out holds the value
 * stored at the memory location specified by address. If load==1, then 
 * the in value is loaded into the memory location specified by address 
 * (the loaded value will be emitted to out from the next time step onward).
 */

CHIP RAM4K {
    IN in[16], load, address[12];
    OUT out[16];

    PARTS:
    DMux8Way(in=true, sel[0..2]=address[9..11], a=a, b=b, c=c, d=d, e=e, f=f, g=g, h=h);

    And(a=a, b=load, out=load0);
    And(a=b, b=load, out=load1);
    And(a=c, b=load, out=load2);
    And(a=d, b=load, out=load3);
    And(a=e, b=load, out=load4);
    And(a=f, b=load, out=load5);
    And(a=g, b=load, out=load6);
    And(a=h, b=load, out=load7);

    RAM512(in=in, load=load0, address[0..8]=address[0..8], out=R0);
    RAM512(in=in, load=load1, address[0..8]=address[0..8], out=R1);
    RAM512(in=in, load=load2, address[0..8]=address[0..8], out=R2);
    RAM512(in=in, load=load3, address[0..8]=address[0..8], out=R3);
    RAM512(in=in, load=load4, address[0..8]=address[0..8], out=R4);
    RAM512(in=in, load=load5, address[0..8]=address[0..8], out=R5);
    RAM512(in=in, load=load6, address[0..8]=address[0..8], out=R6);
    RAM512(in=in, load=load7, address[0..8]=address[0..8], out=R7);

    Mux8Way16(a=R0, b=R1, c=R2, d=R3, e=R4, f=R5, g=R6, h=R7, sel[0..2]=address[9..11], out=out);
}

至此,我们了解了RAM中内存访问的方式。在RAM中的存储单元可以通过解析地址信号来进行随机的访问,而不会受限于存储单元的位置,访问任何位置的存储单元的速度都是相等的,例如访问内存中一个数组中任意下标的数据的速度都是相等的。这也是Random Access Memory概念的由来。

计数器

计数器也是CPU中一个重要的原件,主要用来存储将要执行指令的内存地址。在大多数情况下,计数器每个周期简单的进行+1操作,这时计算机能够获取下一条地址,这和CPU顺序执行指令的模式相符,同时CPU也支持直接跳转到编号为n的指令去执行,因此计数器要支持将其值设置为n的能力。下面我们来看一下计数器的接口设计:计数器芯片有2个附加控制位reset和inc,当inc=1时,计数器在每个时钟周期自加1,其输出值为out(t)=out(t-1)+1;如果想要将计数器置为0,就将reset置为1。如果想要将计数器设置为某个值,则将load置为1,并将数值从in输入。

下图是我对计数器的实现逻辑

  • reset,inc,load用一个Or8way输出,只要其中有一个为1,那么register的load为就为1,register的值会被更新
  • 3个Mux串联用于选择自增1、in的输入值、重置0这三种操作

HDL实现:计数器

/**
 * A 16-bit counter with load and reset control bits.
 * if      (reset[t] == 1) out[t+1] = 0
 * else if (load[t] == 1)  out[t+1] = in[t]
 * else if (inc[t] == 1)   out[t+1] = out[t] + 1  (integer addition)
 * else                    out[t+1] = out[t]
 */

CHIP PC {
    IN in[16],load,inc,reset;
    OUT out[16];

    PARTS:
    Or8Way(in[0]=inc, in[1]=reset, in[2]=load, in[3..7]=false, out=ReLoad);

    Mux16(a=outBack, b=outPlus, sel=inc, out=loadIN);
    Mux16(a=loadIN, b=in, sel=load, out=resetIN);
    Mux16(a=resetIN, b=false, sel=reset, out=ReIn);

    Register(in=ReIn, load=ReLoad, out=out, out=outBack);
    Inc16(in=outBack, out=outPlus);
}

我们以下图为例,看一下计数器是如何在时钟系统下变化数值的。

总结

至此,我们实现了hack CPU所需的所有芯片。组合逻辑芯片主要负责算数运算、逻辑运算、选择逻辑,并且通过适当的组合可以用来实现指令解析和某些功能控制;时序芯片主要负责存储数据。下一篇我们会用这些芯片搭建一个简单但实用的CPU。

  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值