1bit CPU writeUP
About
其实也不是啥新的东西了… 好早之前就做好了, 只是一直没来得及写一些解释性的文字. 恰好最近稍微有点空, 来小小写个说明.
项目地址在: li-yiyang/one-bit-cpu (github) 上. 懒得看解析的, 可以直接看代码. 以及项目的最初来源为 nato64/1bit-CPU.
原始逻辑门电路的分析
还请直接看电路图
(原始图片在 nato64 repo 中)
这里把图片抽象一下, 大概如下图所示:
其中:
- Registers: 74HC74 芯片 为一个双 D 触发器的一个逻辑芯片, 在这里用作一个一位寄存器的功能.
- Memory: 通过两个 2 位编码开关来实现选择.
这里通过 74HC14 芯片 (6 个施密特反相触发器, 作用类似于二值化模拟电平并反相输出, 可以简单看作是非门) 以及二极管实现了选择哪一个编码开关的功能: 若 PC 为 HIGH, 则 SW1 相当于是悬空而 SW2 生效.
- ALU: 通过 74HC00 芯片 (与非门) 实现的 XOR (异或门).
- 多路选择器: 通过 74HC153 芯片 (4 输入选择器) 实现对结果的选择.
两个结果一个给了 Reg A (Y1) 另一个给了 PC (Y2).
- Display: 通过几个 LED 显示 Reg A, PC, Clock 的情况.
复刻的尝试
正如上面所分析的一样, 这里把整个 CPU 做一个分割:
- Clock: 时钟部分
- Register: 寄存器, 通过时钟上升沿进行数据的写
- ALU: 包含多路选择器的输出和逻辑计算部分
- Program: 连接外部的编码器
- Display: 连接显示的 LED
// Clock
wire clk;
slower_clk #(.SLOW_RATE(20_000_000)) slower_clk (.clk(clock), .slow_clk(clk));
// Register
wire reg_out, pc_out, a, p;
register reg_a (.clk(clk), .set_p(1), .data(reg_out), .reset(reset), .data_reg(a));
register pc (.clk(clk), .set_p(1), .data(pc_out), .reset(reset), .data_reg(p));
// ALU
wire [1:0] code;
alu alu (.reg_in(a), .pc_in(p), .code(code), .reg_out(reg_out), .pc_out(pc_out));
// Program
assign code = p ?
memory[3:2] :
memory[1:0];
// Display
assign clk_led = clk;
assign reg_led = a;
assign pc_led = p;
(具体请参考: one-bit-CPU/cpu.v 中的代码)
不难发现每一部分的功能都非常容易实现, (所以我觉得作为一个 FPGA 的练手项目简直非常适合).
Clock
这里用计数器实现将板子 (Nano Tang 9K) 自身的时钟 (27MHz) 变成一个慢时钟.
方法就是计数, 若达到一定值, 则翻转输出
always @(posedge clk)
if (counter == SLOW_RATE) begin
counter <= 0;
slow_clk <= ~slow_clk;
end else
counter <= counter + 1;
(具体请参考: one-bit-CPU/slower-clock.v 中的代码)
ALU
在 Verilog 里面写这种就感觉轻松好多了呢… 基本上只需要规定好控制部分和参数部分即可, 比如:
// code[1] is the cmd, code[0] is the arg
assign reg_out = code[1] == 1'b0 ?
reg_in ^ code[0] : // xor
reg_in; // reg
assign pc_out = code[1] == 1'b1 ?
code[1] & code[0] : // jmp
~pc_in; // pc++
(具体请参考: one-bit-CPU/alu.v 中的代码)
注: 这里我就想要吐槽一下我之前的代码定义了…
不是一般会把命令放在前面, 但是为什么会反过来呢.
啊, 这里思考了一下, 发现自己以前还是很聪明的 (并没有).
这里应该是 Big-Endian 和 Little-Endian 的区别.
在这里, 1'b10
的 [1]
指向的是 1
(即从右往左).
注: 这里还要吐槽一下… 诶, 写代码最好不要太过依赖复制了… 因为我发现旧的代码因为复制的原因导致了一个命令的错误 (one-bit-CPU@02b73ce).
Register
寄存器大概长这样:
// At every clock, if set_p, set data_out as data
always @(posedge clk)
if (reset == 1'b0)
data_reg <= 1'b0; // reset data_reg
else if (set_p == 1'b1)
data_reg <= data; // set data_reg
(具体请参考: one-bit-CPU/register.v 中的代码)
这里有一个小小的坑, 因为我用的开发板上的按钮的电平是按下为低 (?), 所以一开始仿真测试的时候搞反了…
Coding for 1bit CPU
虽然可以支持的程序大小只有 2 条指令, 但是又不是不能用. 你甚至可以通过历遍程序的方式来实现对程序的所有的编程.
Assembler 和 Disassembler
直接看 one-bit-CPU/alu.v 中的代码, 可以发现命令就两种:
XOR <a>
: 为0*
的形式JMP <l>
: 为1*
的形式
于是很容易就可以对代码 (* * * *)
的形式写一个简单的 disassembler:
(defun disassembler (code)
"1 bit cpu disassmbler"
(when code
(destructuring-bind (cmd arg . rest) code
(cons (ecase cmd
(0 (list :xor arg))
(1 (list :jmp arg)))
(disassembler rest)))))
同理也有 assembler:
(defun assembler (code1 &optional (code2 '(:jmp 0)))
"1 bit cpu assembler"
(flet ((:> (code)
(destructuring-bind (cmd arg) code
(list (ecase cmd (:xor 0) (:jmp 1)) arg))))
(append (:> code1) (:> code2))))
代码上的分析…
嗯, 虽然我觉着手动分析有点麻烦, 但是毕竟代码还是简单嘛…
(:jmp 0) xxx
和 xxx (:jmp 1)
原地 TP
比如说代码类似于: (:jmp 0) xxx
这样的程序. 相当于是一直原地 TP,
此时 PC
保持 0
不变, REG A
保持不变.
而 xxx (:jmp 1)
相当于是做了一条指令后进行原地 TP.
(:jmp 1) xxx
和 xxx (:jmp 0)
单指令有效
类似于 (:jmp 1) xxx
和 xxx (:jmp 0)
这样的程序,
相当于直接浪费了一个代码槽, 只有一个 XOR 的功能.
剩下的?
大概就是 XOR 吧, 剩下的直接看仿真吧…
仿真测试
诶, 仿真…
真是让人伤心和头秃, 一开始以为自己以前的代码写错了, 结果最后发现是自己仿真的测试条件搞错了…
诶.
(具体代码请看: one-bit-CPU/cpu_test.v)
仿真的结果如下 (大家就图一乐吧, 还没有写比较用的检测代码, 但是太晚了先去准备睡觉了. )
(仿真时的参数: STEP = 10, LENGTH = 16
)
后记
大概, 就酱, 去吃饭了.
又, 加一张在板子上跑的结果:
如您所见, GIF 里面显示的是一个代码对应为 (1 1 0 1)
,
即 ((:jmp 1) (:xor 1))
的程序, 等价于 ((:xor 1))
,
图中可见 PC (三盏灯中最下方的一个) 常亮, REG A (中间) 不亮.