在Clojure中模拟RAM

2020-11-14 09:54:02

“计算机都是由逻辑门组成的。”我们以前听过这句话。我们也有一种感觉,逻辑门是非常简单的机器,甚至类似于电灯开关。这就提出了一个问题:这种光开关究竟是如何组合在一起形成计算机的?“存储变量”或“调用函数”如何转换为逻辑门打开或关闭?

在回答这个问题的旅途中,我发现了J·克拉克·斯科特(J Clark Scott)的杰作“它是如何知道的?”他从与非门开始,带你踏上用它们建造计算机的旅程。

我非常喜欢他的书,所以我拿了他的RAM示意图,并用Clojure进行了模拟。在本文中,我将指导您完成这项工作:我们将模拟NAND门,并使用大约14,000个NAND门来构建256字节的RAM。

通过这次模拟,我有一种“啊哈”的感觉:看着14000台小机器嗡嗡作响,你会觉得无论是谁使用计算机,都是一个巫师。一个拥有数以百万计的机器仆人的巫师,每秒都在为他们做数十亿件小事。我希望它能给你同样的感觉。🙂。

这描述了与非门。与非门是一种有两条输入线的机器。如果两根输入线都有“高”电荷(表示为1),则输出电荷为“低”(表示为零)。输入电荷的任何其他组合,输出电荷都很高。

请注意,导线带有电荷,但我们选择解释电荷的含义。“高电荷量”表示1,“低电荷量”表示0。这台机器没有什么变化,这只是我们作为人类决定的事情(1)。

在左边你可以看到一张电路图。您可以将其理解为将电荷带入与非门的输入线a和b。与非门有一根导线c,携带输出电荷。对于我们将要绘制的所有电路图,您可以将其理解为电流从左到右或从上到下“流动”。

右边是一个“与非”门的“真”表。这只是一个奇特的名字,用来根据输入线总结NAND门可能达到的每一种状态。

现在,我们可以从比NAND门更低的位置开始,但这台机器非常简单。当两个输入接通时,构建一个可以关闭的东西不会那么难。不过你不必相信我的话,你可以搜索“用晶体管建造与非门”,当你确信的时候再回来。

首先,我们需要一些方法来表示我们赛道的状态。我们知道我们的RAM将完全由NAND门构建,所以让我们从其中一个中获得灵感:

以下是我们可以将其映射到Clojure中的数据结构的一种方法:

(def ex-state-v0{:Charge-Map{:A 1:B 1:C 0}:NAND-GATES[{:INS[:A:B]:OUT:C}]})。

我们可以使用关键字来表示我们的电线。我们还可以保存一张地图,告诉我们电线的电荷。最后,我们可以保存NAND门的列表,它告诉我们这些线是如何连接的。

就目前而言,这足以代表我们的赛道!让我们创建几个函数来帮助我们管理这个表示:

;更新状态V0;-(def Empty-State{:Charge-MAP{}:NAND-GATES[]})(Defn Charge[State wire](Get-in State[:Charge-Map wire]))(Defn Chards[State wire](MAP(部分电荷状态)wire))(Defn Set-Charge[State wire](Assoc-In State[:Charge-Map wire]Charge)(Defn wire-NAND-。GATE[STATE a b o](更新状态:NAND-GATES conj{:ins[a b]:out o})。

这些都是我们将与非门“连接”到电路中所需的基本工具。让我们在REPL中尝试一下:

(电荷(->;空状态(Set-Charge:A1)(Set-Charge:B 0))[:A:B]);=>;(1 0)(Wire-NAND-GATE空状态:A:B:C);=>;{:电荷映射{},:NAND-GATES[{:INS[:A:B],:OUT:C}]}。

好的!。我们现在可以“接线”一条线路。让我们通电通过它。

为了弄清楚如何将电流模拟到我们的电路中,让我们再次记住我们的图:

我们可以模拟这种情况的一种方式是把电想象成水:它从源头“流”到电线,并“触发”所有连接到这些电线上的设备。

对于这样的模型,如果在以下情况下“触发”一项收费,将会发生什么情况:

此后,A的电荷将转移到与其连接的所有与非门。在这种情况下,它将是我们上面的一个与非门。

然后,每个与非门将重新计算其电荷,如果电荷改变,则依次触发其输出线。在我们的例子中,这是c。

如果c连接到其他NAND门,这些门将被触发,该过程将继续。

现在,这是一个关于电是如何工作的非常天真的观点(2),但它足以让我们为RAM建模!

我们的与非输出函数接受两个输入电荷,并产生与非门将产生的输出电荷。

接下来,我们需要一个函数来查找连接到特定导线的所有与非门:

这将搜索电路中的所有NAND门,并找到连接到特定导线的那些门。

(声明Trigger-NAND-GATE)(Defn Trigger([STATE WIRE NEW-V](LET[OLD-Charge(Charge State Wire)State';(Set-Charge State Wire NEW-v)new-Charge(Charge State)](IF(=Old-Charge New-Charge)STATE';(Reduce(FN[Acc-State Out](Trigger-NAND-Gate Acc-State Out)STATE';(Dependent-NAND-GATES STATE&#)(Dependent-NAND-GATES STATE&#)。电线)。

剩下的就是实现NAND门在被触发时所做的事情:

(定义触发器与非门[STATE{:KEYS[INS OUT]}](LET[NEW-CHAGE(应用NAND-OUT(电荷状态INS))]](触发状态OUT NEW-CHAGE))。

这会计算与非门的新电荷,并用该电荷触发输出线。

最后一个辅助函数:让我们创建一些可以让我们“触发”多条线路的东西:

(Defn触发器-多个[状态导线电荷](Reduce(fn[acc-state[导线电荷]](触发acc状态导线电荷))状态(映射向量导线电荷)。

我们非常想做这件事,所以有这样的人在身边是很好的。

我们已经有了模拟通过NAND门的简单电荷所需的东西。让我们为此编写一个测试:

(Deftest-NAND-GATE(设[S1(->;EMPTY-STATE(Wire-NAND-GATE:A:B:O))(Trigger-My[:A:B][1 0]))S2(-&>S1(Trigger:B1))](Testing";Just a is on";(is(=';(1 0 1)(费用s1[:a:b:o])(测试";a和b都在";(is(=';(1 1 0)(费用s2[:a:b:o])。

如果我们取一个NAND门,并在两个输入端馈入相同的导线,会发生什么情况?

那么,输出将最终与其输入相反。当a为零时,c为1,当a为1时,c为0。砰的一声,那正好是一扇“非门”。这看起来是这样的:

要实现我们的NOT门,我们可以完全按照图中描述的那样做:将相同的导线馈送到NAND门的两个输入端:

(Deftest-NOT-GATE(设[S1(-&>;EMPTY-STATE(Wire-NOT-GATE:A:O))(Trigger:A0))S2(->;S1(Trigger:A1))](测试";a关闭";(is(=';(0 1)(费用S1[:A:O])(测试";a打开";(is(=';(1 0)(费用s2[:A:O])。

如果我们插入一个NAND的输出作为NOT门的输入,会怎么样?

它与NAND门相反:只有当a和b都是1时,d才会是1。这就是AND门:

(Defn Wire-and-Gate[State a b o](设[NAND-o:c](->;State(Wire-NAND-Gate a b NAND-o)(Wire-Not-Gate NAND-o)。

这会奏效的,…。差不多了。这里的棘手之处在于,在函数内部,我们有一个“中间”导线c,它连接与非门和非门。例如,如果我们制作了两个AND门,那么它们将共享同一根导线:C!

(def_u(atom{}))(Defn uniq-n[k](SWAP!_u UPDATE k(FN[i](Inc(Or I 0)(get@_u k)(Defn kw[&;args](-&>;&>args(MAP(fn[x])(if((某些-fn关键字?符号?)。X)(名称x)(应用字符串关键字))(Defn wire([n](let[i(uniq-n n)](if(>;i 1)(kw n";#";i)n)。

现在,如果我们用已经存在的名称创建一条线,它会在它的旁边添加一个漂亮的小“#2”。

(Defn Wire-and-Gate[State a b o](let[NAND-o(wire(kw a b:and-nAND-o))](->;state(wire-NAND-gate a b NAND-o)(Wire-NOT-GATE NAND-o)

(Deftest-and-gate(设[S1(->;Empty-State(Wire-and-Gate:A:B:O)(Trigger-My[:A:B][1 0])S2(->;S1(Trigger:B1))](Testing";Just a is on";(is(=';(1 0 0)(费用s1[:a:b:o])(测试";a和b";(is(=';(111)(费用s2[:a:b:o])。

现在是我们需要了解的最难也是最重要的赛道之一。让我们从描述我们的目标开始:

请注意这里的有趣之处。当s线为“1”时,i的值被转移到o。当它为“0”时,o的值不再受i的影响,o的电荷保持不变。

如果我们能做出这样的东西,那就意味着“o”上的电荷被储存起来了。因为它可以是1也可以是0,我们实际上可以“存储”1位数据。

这个电路的诀窍在于o和c连接在一起的方式。这种缠绕在一起的东西被称为“锁存”,因为一旦以某种方式设置了电荷,这些门就会找到一个平衡点,从而导致o被储存起来。相当酷!

这个电路相当复杂,有点难懂(3),但我们对模拟能力了如指掌!让我们尝试构建它并对其进行测试:

(要理解本电路中的变量,请按照教程中的图表进行操作";([STATE s I o](设[a(wire:a)b(wire:b)c(wire:c)](->;state(wire-NAND-gate i s a)(wire-NAND-gate a s b)(wire-NAND-gate a c)(wire-NAND-gate b o c)。

对于这样一台复杂的机器来说,它看起来真漂亮。现在是终极问题…。它会起作用吗?!

(Deftest-Memory-Bit(设[S1(->;Empty-State(Wire-Memory-bit:S:I:O)(Trigger-Memory[:I:S][1 0])S2(-&>S1(Trigger:S 1))S3(-&>S2(Trigger-Many[:S:I][0 0]))](Testing";打开I不会影响其余";(IS(=';(0 1 0)(费用S1[:S:I:O])(测试";启用SET传输I到O";(IS(=';(11 1)(费用S2[:S:I:O])(测试";禁用SET,消除对O";的进一步影响;(IS(=';(001)(费用S3[:S:I:O])。

现在我们有了位,我们可以用一根导线将8个内存位捆绑在一起。这将允许我们一起设置8位,这意味着我们可以“存储”8位数据…。这给了我们一个字节!(5)。这看起来是这样的:

请注意,在我们的图表中,“两条导线在一起”是书写8条导线的缩写。

(定义线字节[状态s INS OUTS](Reduce(fn[acc-state[i o]](Wire-Memory-bit acc-state s i o))状态(映射向量INS OUTS)。

不过,要测试这一点,我们需要一种方法来为导线“创建”一组名称。让我们编写几个帮助器函数来简化这一过程:

(定义名称[n r](mapv(fn[i](kw n";-";i))(范围r))(定义焊线(元件(部分mapv焊线)名称))。

(Deftest-byte(let[II(wire:I 8)os(wire:O 8)S1(->;Empty-State(wire-byte:S II os)(Trigger-Many II[1 1 0 0 0])(Trigger:S 0))S2(->;S1(Trigger:S 1))S3(->;S2(TRIGGER:S 0)(TRIGGER-MANY II[0 0 0 1])](测试";禁用设置,消除对O";(IS(=';(1 1 0 0 0)(费用S1 II)(IS(=';(0 0 0)(费用s1 os)(测试";设置s,因此os变为1 11";(is(=';(1 1 0 0 0)(费用s2 II)(is(=';(1 1 1 0 0 0)(费用s2 os)(测试";通过禁用S来冻结。请参见对i的进一步更改不会对o";(is(=';(0 0 0 1)(费用S3 II)(is(=';(1 1 1 0 0 0)(费用s2 os)。

请注意B1和B2之间的输出线是如何共享的。如果B1的电荷是“11110000”,而B2的电荷是“0001111”,那么输出线会发生什么情况?它将携带“1111111”的费用!假设我们想要确保只有一个字节将其输出电荷发送到输出线。我们怎么能这么做呢?

我们需要一台新机器。让我们来考虑一下,如果我们把一串AND门连接在一起会发生什么,就像这样:

现在,如果“e”线是“开”的,那么无论输入线是什么,输出线都会被充电。但是,如果“e”线是“OFF”,则输出为零。这台机器被称为“启动器”。如果我们把这些放在一起,我们就能控制送到输出线的电荷!

(Defn Wire-Enabler[State e Ins Out](Reduce(fn[acc-state[In Out]](线栅Acc-State e In Out)状态(贴图向量Ins Out))。

(Deftest-Enabler(让[II(wire:I 8)os(wire:O 8)S1(->;Empty-State(wire-Enabler:E II os)(Trigger-Many II[1 1 0 0 0])(Trigger:E 0))S2(Trigger S1:E 1)S3(Trigger S2:E 0)](Testing";is应该设置,但OS应该是0";(IS(=';(1 1 1 0 0 0)(费用S1II)(IS(=';(0 0 0)(费用S1 os)(测试";OS如果启用";((IS(=';(1 1 1 0 0 0)(费用S2 II))(IS(=';(1 1 0 0 0)(费用S2 OS)(测试";OS如果禁用应恢复";(IS(=';(1 1 1 0 0 0)(费用S3 II)(IS(=';(0 0 0)(收费S3 os)。

下面是我们如何“修复”B1和B2的问题:

如果我们在每个字节前面加上一个“启用码”,我们就可以控制发送到输出线的内容。如果我们想让B1充电,我们会“启用E1”,并确保禁用E2,反之亦然。

字节和启用码的这种组合非常常见,我们可以为此构建一台机器:

这叫收银机!寄存器让我们可以控制存储哪些字节,以及何时将这些字节公开为输出。

要设置它,我们需要做的就是将一个字节和一个启用码绑定在一起:

(Defn线寄存器[状态s e ins位输出](->;state(线字节s ins位)(线启用器e位输出)。

巴达宾,巴达博,应该行得通。这是一台非常重要的机器,所以让我们确保它能正常工作:

(Deftest-Register(设II(wire:I 8)bs(wire:B 8)os(wire:O 8)s1(->;空状态(线寄存器:S:E II bs os)(触发器多II[1 1 0 0 0])(触发器:S 0)(触发器:E 0)S2(触发器S1:S 1)S3(触发器S2:E 1)S4(->;S3(TRIGGER:S 0)(TRIGGER-MANY II[0 0 0 1])S5(TRIGGER S4:E 0)](测试应该设置,但是bs和os应该是0,b/c s&;e是0";(is(=';(1 1 0 0 0)(费用s1 II)(is(=';(0 0 0)(费用s1 bs)(is(=';(0 0 0)(费用s1 os)(测试&34;is&;bs应设置,因为s为ON。但操作系统应为0,b/c e为OFF";(is(=';(11 1 0 0 0)(费用S2 II)(is(=';(1 1 0 0 0)(费用S2 bs)(is(=';(1 1 0 0 0)(费用S2 bs)。(0 0 0)(充电s2 os)(测试&34;is&;bs应设置,因为s已打开。但操作系统应为0,b/c e为OFF";(is(=';(11 1 0 0 0)(费用S3 II)(is(=';(1 1 0 0 0)(费用S3 bs)(is(=';(1 1 0 0 0)(费用S3 bs))。(1110 000000)(费用S3os)(测试";is应该是新的v,但是bs和os应该是旧值";(is(=';(0000000001)(费用S4ii)(is(=';(0000000001)(费用S4ii)(。(1 1 1 0 0 0)(费用S4 bs))(IS(=';(1 1 0 0 0)(费用S4 os)(测试";OS应再次输出";(IS(=';(0 0 0 1)(费用S5 II)(IS(=';(0 0 0 1)(费用S5 II)(。(1 1 1 0 0 0)(费用S5 bs))(is(=';(0 0 0)(费用S5 os)。

好,让我们继续我们的实验,得出一个令人震惊的结果:如果我们将一组寄存器的输入和输出连接到相同的导线上会怎么样?

现在,请记住,s允许我们决定将什么“存储”到寄存器中,而“e”允许我们将寄存器字节的费用“传递”到输出。

在下面的场景中会发生什么。假设R1的字节包含“111”,所有的s和e线都是0。

“把R1的e充电到1”。现在R1将启用,母线将携带与R1相同的电荷

将R3充电为1,然后充电为0。这会将R3的值设置为流经母线的当前电荷。这恰好是R1的输出!

“Set R1&#s e to 0”(将R1的e设置为0),此时输入母线的电流将再次消失。

结果是什么呢?R1中的字节将被“复制”到R3。我们刚刚创造了一辆巴士。

要创建总线,我们只需将寄存器的输入和输出连接到相同的“总线”导线:

这只是一行代码,但是正确使用是非常重要的。毕竟,这就是让我们“复制”寄存器的东西。让我们看看它是否有效:

(Deftest-wire-bus(让[BW(WIRES:BW 8)R1-BITS(WIRES:R1 8)R2-BITS(WIRES:R2 8)R3-BITS(WIRES:R2 8)S1(->;空状态(有线总线BW:S1:E1 R1位)(有线总线BW:S2:E2 R2位)(有线总线BW:S3:E3 R3位)(触发器多个BW[1 1 0 0 0])(触发器:S1 0)(触发器:E1 0)S2(->;S1(触发:s1 1)(触发:s1 0)(触发多个带宽[0 0 0]))s3(->;s2(触发:e1 1)(触发:s3 1)(触发:s30)(触发:e1 0)](测试";仅总线应该有电荷";(IS(=';(1 1 1 0 0 0)(电荷S1BW))(IS(=';(0 0 0)(电荷S1 R1比特)(IS(=';(0 0 0)(电荷S1 R2比特)(IS(=';(0 0 0)(电荷S1 R2比特))。(0 0 0)(电荷S1R3比特)(测试";R1应具有电荷";(IS(=';(0 0 0)(电荷S2 BW)(IS(=';(1 1 1 0 0 0)(电荷S2 R1比特)(IS(=';(0 0 0)(电荷S2 R1比特)。(0 0 0(充电S2 R2位)(IS(=';(0 0 0(充电S2 R3位)(测试";将R1移动到R3&34;(IS(=';(0 0 0)(充电S3BW)(IS(=';(1 1 1 0 0 0)(电荷S3 R1位))(IS(=';(0 0 0)(电荷S2 R2位)(IS(=';(1 1 1 0 0 0)(电荷S3 R3位)。

请记住,母线现在可以从R1、R2或R3的输出端“接收”电荷。我们的套装炸药看起来是这样的:

想象一下,如果R1被启用,一根电线充电为1。如果R3被触发,会发生什么情况?

是时候发展我们的模式了。一种思考方式。

.