TD4 4位DIY CPU指南
摘要
关于构建和理解来自阿里巴巴的TD4 4位DIY CPU套件的详细指南,涵盖焊接、原理图和操作原理。
暂无内容
查看缓存全文
缓存时间: 2026/06/21 04:30
# TD4 四比特 DIY CPU 指南
来源:https://www.philipzucker.com/td4-4bit-cpu/
我在阿里巴巴(Aliexpress)上买了一个可爱的小四比特 CPU 套件(https://www.aliexpress.us/item/3256804578823017.html?gatewayAdapt=glo2usa4itemAdapt),型号为 TD4。它有两个寄存器、几个 LED 和 16 字节的程序 ROM。虽然功能非常有限,但很酷,能教给你很多计算机架构的原理。这个 CPU 的文档、原理图和图片都在这里:https://github.com/wuxx/TD4-4BIT-CPU。不过内容有点简略,我能想象学生可能会感到不知所措。所以我觉得写一些更详细的学习笔记可能会有帮助。
## 组装
我们花了两次焊接时间才把它焊好。这张图片
Alt 文字
加上原理图(https://github.com/wuxx/TD4-4BIT-CPU/blob/master/doc/cpu_td4.jpg)就足以指导我们完成,难度不算太大。
原理图
贴片二极管的方向让我们犹豫了一下,但我们用万用表的二极管测试功能搞清楚了。二极管**正面**的细小绿线朝向电路板底部。我印象中二极管**背面**的线朝向顶部,但现在已经焊上了,无法确认。
最让我们头疼的是焊接 USB 连接器。我建议在安装 IC 插座之前先焊接它,因为它会妨碍烙铁操作。USB 的中间引脚是未连接的,所以必要时可以直接用烙铁把它们连起来。USB 只用于供电。
哪个芯片放哪里可以通过查看原理图上的零件号和 IC 编号来确定。芯片上的缺口要与 PCB 板上的缺口对齐。
基本上第一次就成功了,除了 USB 上有一个间歇性的电源连接问题。焊接那些二极管真是累人。
## 工作原理
从高层次来看,它的工作原理如下:
Alt 文字
程序 ROM 是一排 16 个拨码开关(dip switches)。这是你可用的全部程序大小,所以要做很多事情有点棘手。每个拨码开关的引脚 5-8 是操作码,用于选择 `ADD`、`MOV`、`IN`、`OUT`、`JNC`、`JMP` 指令。这些位通过一些组合逻辑电路来控制:
1. 什么信号进入加法单元(指令的输入操作数)。
2. 哪个寄存器锁存加法单元的输出(指令的输出操作数)。
拨码开关的位 1-4 是指令的立即数,它始终被输入到加法器。数据选择器也可以选择一个硬接线为零的信号(接地)。例如,`MOV A,Im` 指令就用到了这个。这个 `MOV` 指令是通过将 `Im` 加到 `0` 上,然后将结果时钟输入到 `A` 来实现的。
立即数和命令位的顺序有点令人困惑。有时感觉它们是颠倒的。最低有效位是拨码开关上标注为低的那个。例如,值 3 在拨码开关的前 4 位上输入为 1100。
### 更深入一点
#### 地址译码器
地址译码器芯片 IC11 是一个解复用器,它接收 4 位信号,并将对应的 16 个输出信号中的一个驱动为*低电平*。当驱动为低电平时,如果该拨码组的开关已连接,电流可以通过该组的二极管流动。否则,上拉电阻 R21-R26 会将信号保持在高电平。IC12 本质上是一个用于 ROM 结果的缓冲反相电路。
#### 指令译码器
命令位由 IC8 和 IC10 中的分立组合逻辑芯片翻译成 `not_LOAD0,1,2,3` 和 `SEL_A`/`SEL_B` 信号。实际上,`not_LOAD0,1,2,3` 可以称为 `not_LOADA,B,Out,PC`,因为它们正是连接到这些端口。
| instruction | bit7-bit4 | bit3-bit0 | C | SEL_B | SEL_A | #LOAD0/A | #LOAD1/B | #LOAD2/IN | #LOAD3/PC |
|---|---|---|---|---|---|---|---|---|---|
| ADD A, Im | 0000 | Im | X | L | L | L | H | H | H |
| MOV A, B | 0001 | 0000 | X | L | H | L | H | H | H |
| IN A | 0010 | 0000 | X | H | L | L | H | H | H |
| MOV A, Im | 0011 | Im | X | H | H | L | H | H | H |
| MOV B, A | 0100 | 0000 | X | L | L | H | L | H | H |
| ADD B, Im | 0101 | Im | X | L | H | L | H | L | H |
| IN B | 0110 | 0000 | X | H | L | H | L | H | H |
| MOV B, Im | 0111 | Im | X | H | H | H | L | H | H |
| OUT B | 1001 | 0000 | X | L | L | H | H | L | H |
| OUT Im | 1011 | Im | X | H | H | H | H | L | H |
| JNC Im | 1110 | Im | L | H | H | H | H | H | L |
| JNC Im | 1110 | Im | H | X | X | H | H | H | H |
| JMP Im | 1111 | Im | X | H | H | H | H | H | L |
来源:https://raw.githubusercontent.com/wuxx/TD4-4BIT-CPU/master/doc/instruction.md
#### 数据选择器
`SEL_A` 和 `SEL_B` 连接到数据选择器芯片 IC6 和 IC7。它们从四个可能的选项中选择:
| A | B | In | Operand |
|---|---|---|---|
| 0 | 0 | A | |
| 0 | 1 | B | |
| 1 | 0 | In | |
| 1 | 1 | Zero | |
Zero 信号被硬接线到地。
#### 加法器
被选出的 4 位输出到加法器芯片 IC8,该芯片还接收来自所选 ROM 组的立即数位 1-4。一个附带说明:立即数位被比助记符看起来更多的指令读取。例如,`OUT B` 实际上仍然会加上立即数。这个功能可能有用,但也可能令人困惑。默认情况下,如果你不需要立即数,请将它们清零。
#### 寄存器
寄存器 A、B、Out、PC 都是计数器芯片。只有 PC 启用了计数功能。在时钟信号的边沿,如果 `not_LOADx` 信号为低电平,数据将从引脚 `A,B,C,D` 锁存到输出引脚 `QA,QB,QC,QD`。JMP 是通过将值移入 `PC` 寄存器来实现的,该寄存器的输出被反馈到 ROM 地址译码器。
#### 进位触发器
一个有趣的部分是进位电路,它有一个 D 触发器用于存储上一次操作的溢出进位位。这用于实现 `JNC`(“无进位则跳转”)功能。它的工作原理是将进位信号输入到指令译码器电路,并在有进位时禁用来自指令的 `LOADPC` 信号。注意,在这个版本的指令译码器原理图中似乎有一个错误。你能找到它吗?
#### 时钟
右下角是时钟电路。开关可以手动时钟和自动时钟之间切换,也可以改变时钟速度。时钟是一个 RC 控制的多谐振荡器。我还没有认真分析过它。
## 简单程序
我们运行了几个实验程序。首先,最好尝试一些非常简单的东西。
再次参考,这是指令集:
| instruction | bit7-bit4 | bit3-bit0 |
|---|---|---|
| ADD A, Im | 0000 | Im |
| MOV A, B | 0001 | 0000 |
| IN A | 0010 | 0000 |
| MOV A, Im | 0011 | Im |
| MOV B, A | 0100 | 0000 |
| ADD B, Im | 0101 | Im |
| IN B | 0110 | 0000 |
| MOV B, Im | 0111 | Im |
| OUT B | 1001 | 0000 |
| OUT Im | 1011 | Im |
| JNC Im | 1110 | Im |
| JMP Im | 1111 | Im |
### 输出到 LED
尝试让这个程序工作:
```
OUT 0101
```
查表可知 `OUT Im` 的操作码是 `1011`。这对应于第一个拨码开关设置为 `1010 1101`。第一部分是常数立即数,第二部分是该操作码的反向。
简单输出
### 简单闪烁
现在我们可以尝试一个带跳转的简单闪烁程序。我们输出两个不同的常数,然后 JMP 回到指令 0。
这转化为:
```
# Im | Com
#------|-----
OUT 0001 # 1000 | 1101
OUT 0010 # 0100 | 1101
JMP 0 # 0000 | 1111
```
简单闪烁
### 向上计数
现在我们可以尝试计数。
### 向下计数
一旦你意识到加上 15 等同于模 16 减去 1,就可以实现向下计数。这需要理解模运算。
### 先向上计数再向下计数
奇怪的是,这要棘手得多。我们在让这个简单程序工作时犯了很多错误。不过我们当时很累,这可以理解。但要记住更新跳转地址、按正确顺序放置内容、手动汇编,工作量很大。
我们犯的错误:
- 我们过早地喂了冰淇淋(?可能原文有误,或指某种操作)。
- 我们搞混了 JNC 和 JMP 的标签。
- 我们将 OUT 放在无法执行的位置(程序底部)。
- 我们将 OUT 移到 ADD 之后,破坏了 ADD A 1 的进位。
- 我们意识到应该把 ADD B 1, OUT B 看作一个整体单元——一个伪指令。
- 实际上,在检查电路之后,这两条指令可以压缩为 OUT B 1,因为 OUT 实际上也读取立即数。我们很幸运地在 MOV 和 OUT 中把立即数清零了。
记住程序计数器是从 0 开始索引的。
```
00: ADD B 1 1000 1010
01: OUT B xxxx 1001
02: MOV A B xxxx 1000 # 检查是否等于 15
03: ADD A 1 1000 0000 # 给 15 加 1 会产生进位
04: JNC 0 0000 0111 # 如果没有溢出则跳转到 0 (B != 15)
05: ADD B 15 1111 1010 # 通过加 15 向下减 1
06: OUT B xxxx 1001
07: MOV A B xxxx 1000 # 检查是否等于 0
08: ADD A 15 1111 0000 # 只有 0 加 15 不会溢出
09: JNC 0 0000 0111 # 如果没有溢出则跳转到上升段 (B = 0)
10: JMP 5 0110 1111 # 默认跳转到 5
```
另一个可能出错的版本。利用想法 `JC ~ JNC; JMP`,我们可以直接测试 B 的溢出而不使用 A 作为临时变量吗?不过我们会错过一些数字吗?循环的结尾很棘手。
```
00: ADD B 1
01: JNC 3
02: JMP 5
03: OUT B
04: JMP 0
05: MOV B 15
06: ADD B 15
07: JNC 0
08: OUT B
09: JMP 6
```
## Python 汇编器和模拟器
这是我和 Ben 快速写的一个小 Python 模拟器和汇编器。实际上主要是 Ben 写的。也许像这样使用正则表达式有点傻?也许也挺好?`match` 语句太棒了。我们应该回头改进它,但这只是开始。TD4 仓库中也有一个汇编器,但我还没试过。
```python
import re
from collections import namedtuple
from types import SimpleNamespace
instruction_regex = re.compile(r"(?P<cmd>ADD|OUT|MOV|JMP|JNC|IN) +(?P<arg1>A|B|\d{1,2})(?: +(?P<arg2>A|B|\d{1,2}))? *(?:#.*)?")
input = """\
ADD B 1
OUT B
MOV A B
ADD A 1
JNC 0
ADD B 15
OUT B
MOV A B
ADD A 15 # 这是一条注释
JNC 0
JMP 5\
"""
NULL_IM = "0000"
def im_to_str(im):
return f"{int(im):04b}"[::-1]
# 汇编器
# 我们在 ADD B 上犯了一个错误,没有从 ADD A 复制的东西更新
program = []
for line in input.split("\n"):
instruction = instruction_regex.fullmatch(line)
match instruction["cmd"], instruction["arg1"], instruction["arg2"]:
case "ADD", "A", im:
program.append(im_to_str(im)+"0000"[::-1])
case "MOV", "A", "B":
program.append(NULL_IM+"0001"[::-1])
case "IN", "A", None:
program.append(NULL_IM+"0010"[::-1])
case "MOV", "A", im:
program.append(im_to_str(im)+"0011"[::-1])
case "MOV", "B", "A":
program.append(NULL_IM+"0100"[::-1])
case "ADD", "B", im:
program.append(im_to_str(im)+"0101"[::-1])
case "IN", "B", None:
program.append(NULL_IM+"0110"[::-1])
case "MOV", "B", im:
program.append(im_to_str(im)+"0111"[::-1])
case "OUT", "B", None:
program.append(NULL_IM+"1001"[::-1])
case "OUT", im, None:
program.append(im_to_str(im)+"1011"[::-1])
case "JNC", im, None:
program.append(im_to_str(im)+"1110"[::-1])
case "JMP", im, None:
program.append(im_to_str(im)+"1111"[::-1])
case _:
raise Exception("不可识别的命令")
print(program)
# 模拟器
State = namedtuple("State", "a, b, out, pc, c, inp, program")
def build_starting_state(program):
return State(0,0,0,0,0,0,program)
starting_state = build_starting_state("""\
ADD B 1
OUT B
JMP 0\
""".split("\n"))
def step(state):
a, b, out, pc, c, inp, program = state
instruction = instruction_regex.fullmatch(program[pc])
match instruction["cmd"], instruction["arg1"], instruction["arg2"]:
case "ADD", "A", im:
im_int = int(im)
assert 0 <= im_int < 16
a_tmp = a+im_int
return State(a_tmp%16, b, out, (pc+1)%16, a_tmp>=16, inp, program)
case "MOV", "A", "B":
return State(b, b, out, (pc+1)%16, False, inp, program)
case "IN", "A", None:
return State(inp, b, out, (pc+1)%16, False, inp, program)
case "MOV", "A", im:
im_int = int(im)
assert 0 <= im_int < 16
return State(im_int, b, out, (pc+1)%16, False, inp, program)
case "MOV", "B", "A":
return State(a, a, out, (pc+1)%16, False, inp, program)
case "ADD", "B", im:
im_int = int(im)
assert 0 <= im_int < 16
b_tmp = b+im_int
return State(a, b_tmp%16, out, (pc+1)%16, b_tmp>=16, inp, program)
case "IN", "B", None:
return State(a, inp, out, (pc+1)%16, False, inp, program)
case "MOV", "B", im:
im_int = int(im)
assert 0 <= im_int < 16
return State(a, im_int, out, (pc+1)%16, False, inp, program)
case "OUT", "B", None:
print(b)
return State(a, b, b, (pc+1)%16, False, inp, program)
case "OUT", im, None:
im_int = int(im)
assert 0 <= im_int < 16
print(im_int)
return State(a, b, im_int, (pc+1)%16, False, inp, program)
case "JNC", im, None:
im_int = int(im)
assert 0 <= im_int < 16
return State(a, b, out, im_int if not c else (pc+1)%16, False, inp, program)
case "JMP", im, None:
im_int = int(im)
assert 0 <= im_int < 16
return State(a, b, out, im_int, False, inp, program)
case _:
raise Exception("不可识别的命令")
state = starting_state
print(state)
for _ in range(200):
state = step(state)
```
## 杂项
如果你觉得这个有趣,可能想看看另外两个相关项目:
几年前我们很喜欢上 Nand2Tetris 这门课(https://www.nand2tetris.org/)。在这门课中,你将学习如何从门电路开始设计一台计算机,直到一个小型编程语言。它是了解 TD4 背后原理的好参考。
另一个选择是 Ben Eater 的计算机(https://eater.net/8bit/)。套件要贵得多,CPU 更复杂/更强大,而且有更多资料。
似乎有一本关于这个东西的日文书,但我找不到翻译版。
查看原理图,发现没有任何 RAM 可言,所以这是一个非常受限的机器。A + B + OUT + PC 意味着总共有 16 位状态?可以轻松地对你想要的任何状态进行穷举检查。那么程序综合呢?也许这也挺好。
它如此受限,感觉更像是一个有限状态机。尝试对其进行模型检查?我能在 Python HDL、Verilog 或那个 GUI 工具中实现它吗?
一些进一步可以尝试的事情:
- 减法
- 相等性检查。有界模型检查
- 超级优化
Conor Mcbride 有没有做过关于 4 位 CPU 的视频?
外面真的有这样的 4 位 CPU 吗?https://en.wikipedia.org/wiki/4-bit_computing
能写出什么有趣程序?很难。
对每个芯片进行建模:
```verilog
module ALU()
endmodule
module ROM()
end
`def JMP 0
module Computron()
reg [3:0] A;
reg [3:0] B;
reg [3:0] OUT;
reg [3:0] PC;
endmodule
module HC153(in[4], out[16]); // 解复用器
endmodule;
```
74HC154 - 地址译码器 - 4 到 16 线译码器/解复用器。四个输入信号被翻译成 16 个互斥输出。例如,输入引脚为 LLHH 将使输出引脚 3 被拉为*低电平*。由于所有二极管的存在,只有当该组被拉低使能时,电流才会流过 ROM 开关。两个使能引脚被拉到地,因此输出始终使能。这些线进入这个译码器,选择哪个触发器被选中。
74HC540 是一个反相缓冲器。
https://github.com/wuxx/TD4-4BIT-CPU
https://en.wikipedia.org/wiki/Charlieplexing
所有这些二极管都是查理复用(Charlieplexed)的吗?不是。算了。
https://hackaday.io/project/8442-ttl-based-4-bit-cpu
https://hackaday.io/project/26215-td4-cpu
http://kamakurium.com/wp-content/uploads/2016/01/cpu_td4
http://visual6502.org/
http://www.4004.com/
Alt 文字
Alt 文字
Alt 文字
Alt 文字
Alt 文字
Alt 文字
Alt 文字
```python
# 这里我们尝试在电路级别进行模拟,但失败了。也许以后会回来搞定它。
# 这是错的。
def counter(a, b, c, d, not_load, not_reset, count_enable):
load = not not_load
reset = not not_reset
if reset:
return 0, 0, 0, 0
if load:
return a, b, c, d
inp = a + (b << 1) + (c << 2) + (d << 3)
inp += 1
inp %= 16
a = inp & 1
b = (inp >> 1) & 1
c = (inp >> 2) & 1
d = (inp >> 3) & 1
return a, b, c, d
assert(counter(1,1,1,1,0,1) == 1,1,1,1)
assert(counter(1,1,1,1,1,1) == 0,0,0,0)
assert(counter(1,1,1,1,1,0) == 0,0,0,0)
def multiplexer(c0, c1, c2, c3, a, b):
match a, b:
case False, False: return c0
case False, True: return c1
case True, False: return c2
case True, True: return c3
def flip_flop(d, not_clr):
clr = not not_clr
if clr:
return False, True
return d, not d
def adder(a0, a1, a2, a3, b0, b1, b2, b3):
a_int = a0 + (a1 << 1) + (a2 << 2) + (a3 << 3)
b_int = b0 + (b1 << 1) + (b2 << 2) + (b3 << 3)
s_int = a_int + b_int
s0 = s_int & 1
s1 = (s_int >> 1) & 1
s2 = (s_int >> 2) & 1
s3 = (s_int >> 3) & 1
carry_out = (s_int >> 4) & 1
return s0, s1, s2, s3, carry_out
# 我们发现其中一个图表有个错误。它显示
def command_decoder(d4, d5, d6, d7, not_c):
sel_a = d4 or d7
sel_b = d5
not_load0 = d6 or d7
not_load1 = (not d6) or d7
not_load2 = not ((not d6) and d7)
not_load3 = not ((not_c or d4) and d6 and d7)
return not_load0, not_load1, not_load2, not_load3, sel_a, sel_b
def rom():
...
def circuit_step(state):
a, b, out, pc, c, inp, program = state
...
```
相似文章
构建 TD4 4 位 CPU
本文详细介绍了基于《如何构建 CPU》一书及开源 PCB 设计,亲自动手组装并测试 TD4 4 位 CPU 的过程。
Show HN:我们作为大二电子工程学生构建了一个8位CPU
一个完全由分立逻辑门构建的8位哈佛架构CPU,使用Logisim-Evolution设计,提供开源文件和文档。由大二电子工程学生创建。
PC Engine CPU
关于PC Engine (TurboGrafx-16) CPU HuC6280的详细技术概述,这是一款基于65C02的快速8位处理器,涵盖其架构、时钟速度以及与NES和SNES CPU的差异。
关于Intel 8086处理器算术逻辑单元的笔记
详细的技术分析,探讨Intel 8086处理器算术逻辑单元(ALU)的控制电路,解释微码和控制信号如何协调执行28种不同操作。
386处理器寄存器的异常复杂电路
对英特尔386处理器寄存器电路的详细逆向工程分析,揭示了六种不同的定制电路和交织位存储。