TD4 4位DIY CPU指南

Hacker News Top 工具

摘要

关于构建和理解来自阿里巴巴的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

Hacker News Top

本文详细介绍了基于《如何构建 CPU》一书及开源 PCB 设计,亲自动手组装并测试 TD4 4 位 CPU 的过程。

PC Engine CPU

Hacker News Top

关于PC Engine (TurboGrafx-16) CPU HuC6280的详细技术概述,这是一款基于65C02的快速8位处理器,涵盖其架构、时钟速度以及与NES和SNES CPU的差异。