用复古电脑程序 Toy CPU 学习低级编程
我写了一个名为 “Toy CPU” 的教育性复古计算机程序，以便我的学生能够学习机器语言。
编程 Altair 低级语言
我写了一个名为 “Toy CPU” 的教育性复古计算机程序，以便我的学生能够学习机器语言。
![](/data/attachment/album/202301/20/190137rkynyeltzemltutf.jpg)



> 
> 我写了一个名为 “Toy CPU” 的教育性复古计算机程序，以便我的学生能够学习机器语言。
> 
> 
> 


我兼职教授大学课程，包括一个对所有专业开放的一般计算机主题的课程。这是一门入门课程，向学生讲授技术是如何运作的，以消除围绕计算的神秘感。


虽然不是计算机科学课程，但这门课的一个部分涉及计算机编程。我通常用非常抽象的术语谈论编程，所以不会让听众听不懂。但是今年，我想让我的学生以 “老派” 的方式做一些需要 “动手” 的编程。同时，我又想保持简单，以便让每个人都能跟上。


我喜欢将我的课程结构化，以显示你是如何从 “那里” 到 “这里” 的。理想情况下，我会让我的学生学习如何编写一个简单的程序。然后，我将从这里开始，展示现代编程是如何让开发人员创建更复杂的程序的。我决定尝试一种非常规的方法 —— 教学生学习终极的低级别编程语言：机器语言。


### 机器语言编程


早期的个人电脑如 Apple II（1977 年）、TRS-80（1977 年）和 IBM PC（1981 年）让用户用键盘输入程序，并在屏幕上显示结果。但计算机并不总是带有屏幕和键盘。


Altair 8800 和 IMSAI 8080（均为 1975 年制造）要求用户使用面板上的 “开关和灯” 输入程序。你可以用机器语言输入指令，使用一组开关，机器会点亮 LED 灯以代表每个二进制指令的 1 和 0。


![Altair 8800 计算机的图片](/data/attachment/album/202301/20/190030j8zo5ood9hgdkrgx.png)


对这些早期机器进行编程，需要了解被称为 “<ruby> 操作码 <rt>  opcode </rt></ruby>” （操作代码的简称）的机器语言指令，以执行基本操作，如将两个数字相加或将一个值存储到计算机的存储器中。我想向我的学生展示程序员是如何通过开关和灯，手工输入一系列指令和内存地址的。


然而，在这门课上，使用实际的 Altair 8800 就有点太复杂了。我需要一些简单的、任何初级水平的学生都能掌握的东西。理想情况下，我希望能找到一个简单的 “业余” 复古计算机，其工作原理与 Altair 8800 相似，但我无法找到一个价格低于 100 美元的合适的 “类似 Altair” 的设备。我找到了几个 “Altair” 软件模拟器，但它们忠实地再现了 Altair 8800 的操作码，这对我的需求来说太过沉重。


我决定编写我自己的 “教育” 复古计算机。我称它为 “Toy CPU”。你可以在我的 [GitHub 代码库](https://github.com/freedosproject/toycpu) 上找到它，包括几个可以运行的版本。第一版是一个实验性的原型，运行在 [FreeDOS](https://opensource.com/downloads/guide-using-freedos) 上。第二版是一个更新的原型，在 Linux 上用 [ncurses](https://opensource.com/article/21/8/ncurses-linux) 运行。版本 3 是一个 FreeDOS 程序，在图形模式下运行。


### Toy CPU 的编程


Toy CPU 是一个非常简单的复古计算机。它只有 256 字节的内存和一个最小化的指令集，其目的是在复制 “开关和灯” 编程模式的同时保持简单化。它的界面模仿 Altair 8800，有一系列 8 个 LED 灯，分别代表计数器（程序的 “行号”）、指令、累积器（用于临时数据的内部存储器）和状态。


当你启动 Toy CPU 时，它通过清除内存来模拟 “启动”。当它启动时，它也会在屏幕右下方的状态灯中显示 “INI”（初始化）。“PWR”（电源）灯亮表示 Toy CPU 已被打开。


![Toy CPU 的启动屏幕](/data/attachment/album/202301/20/190030xivcexgvy412lblc.png)


当 Toy CPU 准备好让你进入一个程序时，它通过状态灯指示 “INP”（“输入”模式），并让你从程序的计数器 0 开始。Toy CPU 的程序总是从计数器 0 开始。


在 “输入” 模式下，用上下方向键显示不同的程序计数器，按回车键编辑当前计数器上的指令。当你进入 “编辑” 模式时，Toy CPU 的状态灯上会显示 “EDT”（“编辑” 模式）。


![Toy CPU 编辑屏幕](/data/attachment/album/202301/20/190031sdjikuiydcu2dvcd.png)


Toy CPU 有一张速查表，被 “贴” 在显示屏的前面。它列出了 Toy CPU 可以处理的不同操作码。


* `00000000`（`STOP`）：停止程序执行。
* `00000001`（`RIGHT`）：将累加器中的位向右移动一个位置。值 `00000010` 变成 `00000001`，`00000001` 变成 `00000000`。
* `00000010`（`LEFT`）：将累加器中的位向左移动一个位置。值 `01000000` 变成 `10000000`，`10000000` 变成 `00000000`。
* `00001111`（`NOT`）：对累加器进行二进制非操作。例如，值 `10001000` 变成 `01110111`。
* `00010001`（`AND`）：对累加器用存储在某一地址的值进行二进制与操作。该地址被存储在下一个计数器中。
* `00010010`（`OR`）：对累积器用存储在某一地址的值进行二进制或运算。
* `00010011`（`XOR`）：对累加器用存储在某一地址的值进行二进制异或运算。
* `00010100`（`LOAD`）：将一个地址的值加载（复制）到累加器中。
* `00010101`（`STORE`）： 存储（复制）累加器中的值到一个地址。
* `00010110`（`ADD`）：将存储在某一地址的数值加入到累加器中。
* `00010111`（`SUB`）：从累积器中减去储存在某一地址的数值。
* `00011000`（`GOTO`）：转到（跳到）一个计数器地址。
* `00011001`（`IFZERO`）：如果累加器为零，转到（跳到）一个计数器地址。
* `10000000`（`NOP`）：空操作，可以安全地忽略。


当处于 “编辑” 模式时，使用左右方向键选择操作码中的一个位，然后按空格键在关闭（0）和开启（1）之间翻转数值。当你完成编辑后，按回车键回到 “输入” 模式。


![Toy CPU 输入模式屏幕](/data/attachment/album/202301/20/190031d5a5aal5cc5yfzca.png)


### 一个示例程序


我想通过输入一个简短的程序来探索 Toy CPU，将两个数值相加，并将结果存储在 Toy CPU 的内存中。实际上，这执行的是算术运算 `A+B=C`。要创建这个程序，你只需要几个操作码：


* `00010100`（`LOAD`）
* `00010110`（`ADD`）
* `00010101`（`STORE`）
* `00000000`（`STOP`）


`LOAD`、`ADD` 和 `STORE` 指令需要一个内存地址，这个地址总是在下一个计数器的位置。例如，程序的前两条指令是:



```
计数器 0：00010100
计数器 1：某个内存地址，第一个值 A 存放在那里

```

计数器 0 中的指令是 `LOAD` 操作，计数器 1 中的值是你存储某个值的内存地址。这两条指令一起将内存中的数值复制到 Toy CPU 的累加器中，在那里你可以对该数值进行操作。


将一个数字 `A` 装入累加器后，你需要将数值 `B` 加到它上面。你可以用这两条指令来做：



```
计数器 2：00010110
计数器 3：存储第二个值 B 的内存地址

```

假设你把值 `1`（`A`）装入累加器，然后把值 `3`（`B`）加到它上面。现在累加器的值是 `4`。现在你需要用这两条指令把数值 `4` 复制到另一个内存地址（`C`）：



```
计数器 4：00010101
计数器 5：一个内存地址（C），我们可以在那里保存新的值

```

把这两个值加在一起后，现在可以用这条指令结束程序：



```
计数器 6: 00000000

```

计数器 6 之后的任何指令都可以供程序作为存储内存使用。这意味着你可以用计数器 7 的内存来储存值 `A`，计数器 8 的内存来储存值 `B` ，计数器 9 的内存来储存值 `C`。你需要将这些分别输入到 Toy CPU 中：



```
计数器 7：00000001（1）
计数器 8：00000011（3）
计数器 9：00000000（0，以后会被覆盖）

```

在弄清了所有指令和 `A`、`B` 和 `C` 的内存位置后，现在可以将完整的程序输入到 Toy CPU 中。这个程序将数值 1 和 3 相加，得到 4：



```
计数器 0：00010100
计数器 1：00000111（7）
计数器 2：00010110
计数器 3：00001000（8）
计数器 4：00010101
计数器 5：00001001（9）
计数器 6：00000000
计数器 7：00000001（1）
计数器 8：00000011（3）
计数器 9：00000000（0，以后会被覆盖）

```

要运行程序，在 “输入” 模式下按下 `R` 键。Toy CPU 将在状态灯中显示 “RUN”（“运行” 模式），并从计数器 0 开始执行你的程序。


Toy CPU 有一个明显的延迟，所以你可以看到它执行程序中的每一步。随着程序的进行，你应该看到计数器从 `00000000`（0）移动到 `00000110`（6）。在计数器 1 之后，程序从内存位置 7 加载数值 `1`，累积器更新为 `00000001`（1）。在计数器 3 之后，程序将加数值 `3`，并更新累加器显示 `00000100`（4）。累加器将保持这种状态，直到程序在计数器 5 之后将数值存入内存位置 9，然后在计数器 6 结束。


![在运行模式下的 Toy CPU](/data/attachment/album/202301/20/190031xappbq1444vv3av2.png)


### 探索机器语言编程


你可以使用 Toy CPU 来创建其他程序，并进一步探索机器语言编程。通过用机器语言编写这些程序来测试你的创造力。


### 一个在累积器上闪灯的程序


你能点亮累加器上的右四位，然后是左四位，然后是所有的位吗？你可以用两种方法之一来写这个程序。


一种直接的方法是，从不同的内存地址加载三个数值，像这样：



```
计数器 0：LOAD
计数器 1：“右边”
计数器 2：LOAD
计数器 3：“左边”
计数器 4：LOAD
计数器 5：“所有”
计数器 6：STOP
计数器 7：00001111（“右边”）
计数器 8：11110000（“左边”）
计数器 9：11111111（“全部”）

```

写这个程序的另一种方法是尝试使用 `NOT` 和 `OR` 二进制操作。这样可以得到一个更小的程序：



```
计数器 0：LOAD
计数器 1：“右边”
计数器 2：NOT
计数器 3：OR
计数器 4：“右边”
计数器 5：STOP
计数器 6：00001111（“右边”）

```

### 从一个数字开始倒数


你可以把 Toy CPU 作为一个倒数计时器。这个程序行使 `IFZERO` 测试，只有当累加器为零时，程序才会跳转到一个新的计数器：



```
计数器 0：LOAD
计数器 1：“初始值”
计数器 2：IFZERO（这也是倒计时的“开始”）
计数器 3：“结束”
计数器 4：SUB
计数器 5：“1”
计数器 6：GOTO
计数器 7：“开始”
计数器 8：STOP
计数器 9：00000111（“初始值”）
计数器 10：00000001（“1”）

```

Toy CPU 是学习机器语言的一个好方法。我在入门课程中使用了 Toy CPU，学生们说他们发现写第一个程序很困难，但写下一个程序就容易多了。学生们还表示，用这种方式编写程序其实很有趣，他们学到了很多关于计算机实际工作的知识。Toy CPU 既具有教育性，也很有趣味性！




---


via: <https://opensource.com/article/23/1/learn-machine-language-retro-computer>


作者：[Jim Hall](https://opensource.com/users/jim-hall) 选题：[lkxed](https://github.com/lkxed) 译者：[wxy](https://github.com/wxy) 校对：[wxy](https://github.com/wxy)


本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出

