ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从逻辑门到CPU:手搓一台8位教学计算机的完整路线

从逻辑门到CPU:手搓一台8位教学计算机的完整路线 只用 AND、OR、NOT、NAND、XOR 这类基础逻辑门能不能搭出一台真正能执行程序的计算机答案是能而且这正是 CPU 设计最本质的路线。这个目标看起来很难是因为从一颗逻辑门到一台计算机之间缺少的并不是材料而是层次先要明白逻辑门能构成加法器这类运算部件再要明白如何用触发器构成存储部件最后还要解决运算、存储、控制信号如何在同一个时钟节拍里协同工作。下面就把这条链路拆开讲清楚并给出一条从仿真验证到准备上板的可执行路线。这篇内容适合正在学习数字电路、计算机组成原理或者想自己做一个教学 CPU 的开发者。读完以后你应该能回答三个问题逻辑门为什么能做出计算机一台最小计算机需要哪几个模块这些模块在仿真里跑通以后上到真实电路还要补哪些功课。1. 先拆解计算机到底由哪些“积木层”构成1.1 逻辑门是数字电路的最小功能单元逻辑门做的事情非常单一根据输入电平决定输出电平。输入和输出都只有两种状态通常用 0 和 1 表示。数字电路里最常见的门包括 AND、OR、NOT、NAND、NOR、XOR它们的功能可以用一张真值表概括。门类型输出为 1 的条件逻辑表达式AND所有输入都为 1Y A BOR至少一个输入为 1Y A | BNOT输入为 0Y ~ANAND不是“所有输入都为 1”Y ~(A B)NOR不是“至少一个输入为 1”Y ~(A | B)XOR两个输入不相等Y A ^ B这些门在物理上可以用晶体管实现也可以更底层地用继电器实现。真正关键的是只要能做出一个 NAND 门理论上就能组合出所有其他门。这是从逻辑门走向计算机的第一步也是最容易被人忽略的一步。用 NAND 门构造其他门并不复杂NOT A A NAND A A AND B NOT(A NAND B) (A NAND B) NAND (A NAND B) A OR B NOT A NAND NOT B (A NAND A) NAND (B NAND B) A XOR B (A NAND A NAND B) NAND (A NAND B NAND B)所以“只用基础逻辑门”这个前提并不苛刻。问题不是有没有足够的门而是如何用这些门组织出更高层的功能模块。注意NAND 是通用门但通用不代表高效。实际芯片会同时提供 AND、OR、XOR 等门是为了减少门级延迟和晶体管数量。教学实验里可以用 NAND 搭一切真实工程里应按需要选择门类型。1.2 从门到 CPU 的四次跳跃从逻辑门到计算机不能直接一步到位需要经过四个层次从逻辑门到运算部件。用 AND、XOR 等门构成半加器、全加器再扩展成多位加法器和 ALU。从组合逻辑到时序逻辑。用门构成锁存器、触发器和寄存器让电路具备“记住状态”的能力。从存储单元到可寻址内存。用触发器和地址译码器构成寄存器堆、RAM 和程序计数器。从数据通路到控制单元。把运算部件和存储部件通过总线连起来再用控制器协调每一步操作。这条路线就是整篇内容的技术主线。后面几节按这个顺序展开。理解这条主线以后再看任何 CPU 结构图都不会觉得它是黑盒因为每一层都由更底层的东西组成。2. 用逻辑门造“运算”从半加器到 ALU2.1 半加器与全加器CPU 最核心的运算单元是加法器。先看一位加法。两个一位二进制数 A 和 B 相加结果包括“和 S”与“进位 C”。这个电路叫半加器因为不考虑来自低位的进位。半加器真值表ABSC0000011010101101观察真值表可以发现S 就是 A 和 B 的 XOR 结果C 就是 A 和 B 的 AND 结果。所以半加器只需要两个门S A XOR B C A AND B一位加法器显然不够用。多位数相加时低位会产生进位高位必须把进位也计算进去。于是有了全加器它有三个输入A、B、以及来自低位的进位 Cin输出是 S 和 Cout。全加器的逻辑表达式S A XOR B XOR Cin Cout (A AND B) OR (Cin AND (A XOR B))用门电路实现时需要两个 XOR 门、两个 AND 门和一个 OR 门。全加器是所有算术运算的基础。2.2 从串行进位到超前进位把 8 个全加器按位串联低位的 Cout 接到高位的 Cin就得到一个 8 位加法器。这是最容易理解的结构叫串行进位加法器Ripple Carry Adder。它的问题也很明显最高位的计算结果必须等低位的进位一路传上来延迟会随位数线性增长。8 位还不明显32 位、64 位时进位链延迟就会成为性能瓶颈。解决思路是用更多逻辑门换取更短延迟。先算出每个位置的“生成信号 G”和“传播信号 P”G_i A_i AND B_i P_i A_i XOR B_i进位可以写成C_{i1} G_i OR (P_i AND C_i)把这个公式展开就能让各位进位不依赖前一位的逐级计算而是用同一组输入直接生成这就是超前进位加法器Carry Look-ahead Adder。代价是逻辑表达式会膨胀门数量增加布线复杂。实际 CPU 不会只用一种方案。学习阶段建议先实现串行进位因为它能让你看清进位的传递过程。真要做高性能模块再研究超前进位。2.3 减法运算和 ALU 控制有了加法器减法并不需要单独造一套电路。计算机普遍使用补码表示有符号数A - B 可以转换成A - B A (~B) 1所以 ALU 里只需要一组反相器和一个“加 1”控制信号。当控制信号为 1 时B 先取反再通过 Cin 加 1加法器就变成了减法器。一个最小 ALU 至少需要支持加、减、与、或、非等运算。用一个多路选择器根据操作码选择输出结果。下面是一个 8 位 ALU 的 Verilog 示例管脚和控制信号都用最简单的方式定义方便理解module alu8( input [7:0] a, input [7:0] b, input [2:0] op, output reg [7:0] y, output reg z ); always (*) begin case (op) 3b000: y a b; // add 3b001: y a - b; // sub 3b010: y a b; // and 3b011: y a | b; // or 3b100: y ~a; // not default: y 8h00; // 避免组合逻辑产生锁存器 endcase z (y 8h00); end endmodule这里要注意两个点。第一op就是 ALU 的控制信号它决定多路选择器选哪个运算结果。第二default分支不能省略。组合逻辑里如果漏掉某个分支综合工具可能推断出锁存器导致电路行为和你预期完全不同。行为级 Verilog 看起来比门级简洁很多但它综合之后仍然会被映射成逻辑门和触发器。学习阶段可以先用行为级描述验证数据通路再回到门级图里理解每一条连线。3. 用逻辑门造“记忆”锁存器、寄存器和计数器3.1 组合逻辑没有记忆能力前文提到的 AND、OR、XOR 等门都属于组合逻辑。组合逻辑的特点是没有状态输入变化输出立刻跟随变化。无论电路运行多久它都记不住之前发生过什么。要让电路记住数据必须引入反馈。把输出接回输入就形成了一种“保持”结构。最简单的记忆单元是锁存器。两个 NAND 门交叉连接可以得到 RS 锁存器一个输入负责置位一个输入负责复位两个输入都无效时输出保持原样。RS 锁存器虽然能保存 1 位数据但它的输入规则比较严格S 和 R 同时为有效电平时状态不确定。所以现代时序电路不会直接使用 RS 锁存器而是在它基础上增加“使能”控制和“边沿触发”能力。3.2 锁存器、触发器和寄存器先看门控 D 锁存器。它把数据端 D 送到存储结构由使能端 EN 控制是否采样。EN 为高电平时输出跟随 DEN 为低电平时输出保持。D 锁存器有一个缺陷EN 为高电平期间如果 D 电平变化输出也会跟着变这叫电平敏感。CPU 内部如果大量使用电平敏感锁存器很难保证多位数据在同一时刻被稳定采样。所以寄存器通常使用边沿触发的 D 触发器只在时钟上升沿或下降沿采样输入。用 Verilog 描述一个上升沿 D 触发器很简短module dff( input clk, input d, output reg q ); always (posedge clk) begin q d; end endmodule这里的是非阻塞赋值。时序逻辑中多个触发器在同一时钟沿更新时非阻塞赋值可以避免出现“读到的还是旧值”这类竞争问题。这是仿真和综合实践中非常重要的细节。把多个 D 触发器共用一个时钟就构成一个多位寄存器。8 个 D 触发器组成 8 位寄存器16 个组成 16 位寄存器。寄存器内部并不神秘它只是一排触发器共用同一根时钟信号。3.3 寄存器堆、RAM 和程序计数器有了寄存器还要解决如何选择“读写哪一个寄存器”。这需要地址译码器。例如两个地址信号可以选出 4 个寄存器中的一个3 个地址信号可以选出 8 个中的一个。寄存器堆内部就是“寄存器阵列 地址译码器 读使能/写使能逻辑”。RAM 的原理和寄存器堆类似但规模更大、单元结构更紧凑。教学 CPU 里程序放在指令存储器运行过程中的数据放在数据存储器。存储器一端接收地址一端接收写数据同时有读使能和写使能信号控制访问行为。程序计数器 PC 是一类特殊的寄存器。它保存当前指令的地址每取完一条指令后要加 1遇到跳转指令时又要被改成目标地址。所以 PC 本质上由三部分组成PC 寄存器 增量器加1电路 多路选择器选择PC1还是跳转地址这时候你会发现之前做的加法器和多路选择器都派上用场了。PC 的计算结果和跳转地址通过 MUX 选择再在下一个时钟沿写回 PC 寄存器。整个 CPU 的“自动运行”能力就来自这块简单的计数器电路。4. 数据通路与控制把运算和存储串成计算机4.1 最小数据通路设计一台最小教学 CPU 的完整数据通路至少需要这几类部件程序计数器 PC提供当前指令地址。指令存储器根据 PC 取出一条机器指令。指令寄存器 IR暂存当前指令。控制器根据指令操作码产生控制信号。寄存器堆或累加器保存操作数和中间结果。ALU执行算术和逻辑运算。数据存储器保存程序运行时的数据。它们之间的关系可以概括成一条取指-译码-执行循环取指阶段PC - 指令存储器 - IRPC PC 1 译码阶段控制器根据 IR 中的操作码生成所有控制信号 执行阶段寄存器堆读出数据 - ALU 计算 - 写回目标寄存器或数据存储器 跳转处理跳转指令把目标地址写回 PC这里把地址、数据、控制三类信号分开理解会轻松很多。地址信号决定从哪个位置取数数据信号是真正被运算的内容控制信号决定何时读、何时写、选哪一个操作数、做哪一种运算。4.2 控制器把指令翻译成控制信号控制器是整台计算机里“看起来最聪明”的部分拆开以后就是把指令操作码翻译成一组高低电平。一个简单控制器需要输出的控制信号至少包括下面这些控制信号作用有效电平或编码PCWrite是否允许 PC 在下一个时钟沿更新1 为允许IRWrite是否把取到的指令写入 IR1 为允许RegWrite是否允许寄存器堆写入1 为允许ALUSrc选择 ALU 第二操作数来自寄存器还是数据存储0/1 可选ALUOp选择 ALU 执行哪种运算编码对应 add/sub/and 等MemRead数据存储器读使能1 为允许MemWrite数据存储器写使能1 为允许例如执行ADD指令时控制器会让寄存器堆读使能有效让 ALU 操作码选择加法再让写回目标寄存器使能有效。执行STORE指令时ALU 计算结果不写回寄存器而是让数据存储器的写使能有效。实现控制器有两种常见风格。硬布线控制器用有限状态机直接产生控制信号速度快但每加一条指令都要修改逻辑。微码控制器把控制信号存在一段 ROM 里相当于用“微指令”解释“机器指令”优点是指令扩展方便缺点是额外多一层取微码的延迟。教学 CPU 适合先做硬布线因为它能直观反映每条指令对应哪个状态。4.3 时钟同步和时序约束数据通路本身只是一堆门和触发器真正把它们协调起来的是时钟。所有状态元件共用同一个时钟信号才能保证整个系统按统一节拍工作。时钟电路有两个重要参数建立时间和保持时间。建立时间指时钟沿到来前数据必须稳定的时间保持时间指时钟沿到来后数据必须继续维持的时间。如果门延迟太长导致数据到达触发器的时间晚于建立时间要求寄存器就会采到错误值。所以在真实电路里时钟频率不能随意提高必须满足最差路径的时序要求。注意不要用组合逻辑把时钟信号“切”出另一个时钟再送给触发器。例如用 AND 门把 clk 和某个使能信号组合会产生毛刺触发器的行为不可靠。推荐做法是所有触发器统一使用同一个时钟需要暂停更新时用使能信号控制写操作。5. 最小可运行示例8 位教学 CPU 的搭建路线5.1 先定义最小指令集开始搭 CPU 之前必须先定指令集。指令集就是硬件能直接识别的一组操作码编码。教学 CPU 不需要支持几十条指令4 条指令就能跑通一个完整的“取数-计算-存数”流程。助记符操作码机器码格式功能LDA0000 地址把内存对应地址的值装入 A 寄存器ADD1010 地址A A 内存对应地址的值STA0101 地址把 A 寄存器的值写入内存对应地址JMP1111 地址跳转到指定地址继续执行这里采用 8 位指令字高 2 位是操作码低 6 位是地址。所以最多支持 64 个字节的内存空间足够验证一个简单程序。下面是一段计算 1 2 并保存结果的汇编程序地址0: LDA 1 ; A Mem[1] 1 地址1: ADD 2 ; A A Mem[2] 3 地址2: STA 3 ; Mem[3] A 3 地址3: JMP 3 ; 跳回自身相当于停机 地址1的数据: 01 地址2的数据: 02 地址3的数据: 00对应的二进制机器码可以由你自己在汇编器里编码。学习阶段手动填 8 位二进制就可以重点不是汇编器而是让指令真正在数据通路上执行起来。5.2 模块清单与仿真环境建议从仿真开始因为仿真可以观察每一个内部信号。常用组合是 Icarus Verilog 做编译仿真GTKWave 看波形。安装完成后用下面命令编译并运行iverilog -o cpu_tb cpu.v tb.v vvp cpu_tb gtkwave cpu_tb.vcd仿真环境里先生成时钟和复位信号。测试文件里固定时钟周期为 20ns仿真时间至少跑到 1000ns才能观察到几条完整指令的执行过程。搭建顺序可以按模块一个个来先写 8 位加法器单独仿真。加上寄存器堆和 ALU仿真读写。加上 PC 和指令存储器仿真取指。最后接入控制器把指令流完整跑通。不要一开始就写一个 200 行的顶层模块。模块越多调试越难必须靠分段验证缩小问题范围。5.3 验证思路从“能启动”到“结果正确”很多新手把“仿真波形有信号”当作成功这是不够的。最少要验证三件事PC 按顺序递增取指阶段能读到正确指令。ALU 输出和预期一致写回寄存器的值正确。STA 指令执行后数据存储器的目标地址确实变成正确结果。对于上面那段计算 12 的程序终点检查非常简单仿真结束后内存地址 3 的值应该是 0x03。可以在 Testbench 里写一段监控逻辑在仿真结束前打印结果initial begin #500; if (mem[3] 8h03) $display(PASS: mem[3] %02h, mem[3]); else $display(FAIL: mem[3] %02h, expected 03, mem[3]); end这种“断言式验证”比人肉看波形更可靠。之后每增加一条指令就为它设计一个最小程序覆盖正常路径和边界路径。边界路径包括全 0、全 1、减法出现负数等。注意不要把“仿真波形有输出”当作验证通过。要检查输出值、内存值、寄存器状态以及控制信号是否在正确时刻有效。6. 从仿真到真机不同学习环境怎么选6.1 仿真、面包板与 FPGA 的差异同一个 8 位 CPU既可以用仿真器看波形也可以用面包板加 TTL 芯片搭出来还可以用 Verilog 写完后下载到 FPGA 里跑。它们各有特点。实现方式可视化程度调试能力硬件成本适合阶段Verilog 仿真器中等波形任意观察信号多无快速验证数据通路和控制器逻辑Logisim 等逻辑仿真工具高可逐门连线观察无理解门级结构、初学者入门面包板 TTL 芯片高需要示波器和万用表低到中验证单个模块比如加法器或计数器FPGA HDL低在线逻辑分析仪可看内部信号中到高接近真实设计的综合、时序、约束实践学习阶段不建议直接买大型 FPGA 开发板也不建议一上来就用电烙铁焊几百个晶体管。先用仿真器把逻辑跑通再用适合自己预算的方式做实物验证效率最高。6.2 仿真和真实电路的三点差异仿真器默认所有门延迟为 0信号翻转是瞬时的。真实电路完全不同至少要关注三点门延迟。信号经过每一级门都需要时间组合逻辑越深延迟越大。如果时钟周期小于最差路径延迟寄存器就会采到错误数据。毛刺。组合逻辑在输入切换瞬间可能出现短暂的错误电平如果此时正好有触发器采样就会把毛刺当成有效数据锁存进去。扇出和电源噪声。一个门的输出驱动太多输入时电平可能达不到标准阈值电源噪声也会导致时钟沿不稳定。所以当你把 CPU 从仿真挪到真实电路后通常需要降低时钟频率给信号留出足够的稳定时间。6.3 真实板卡上需要额外补的电路如果在 FPGA 上实现时钟要使用开发板自带的时钟引脚不要用低频 RC 振荡器产生时钟。复位信号要经过消抖和同步处理避免在上电瞬间出现不确定状态。如果在面包板上搭真实逻辑门还建议每个电源引脚附近加去耦电容减小电源噪声。空闲输入引脚不要悬空统一接到确定电平。关键信号用示波器观察上升沿质量。用低频率时钟逐步提高频率找到稳定边界。这些内容在仿真阶段不会遇到但恰恰是“能写仿真”和“能跑硅片”之间的差距。7. 常见问题和排错链路7.1 从仿真波形倒推问题手搓 CPU 最容易遇到的不是语法错误而是“波形看起来在跑但结果不对”。下面整理了几类常见故障模式。问题现象可能原因检查方式解决思路所有寄存器都不更新时钟没有产生或没有送到模块查看 clk 波形是否翻转检查 Testbench 中 clk 生成和模块端口连接数据总线出现大量 X多个模块同时输出到同一总线查看各输出使能信号增加三态门确保同时只有一个驱动源PC 不是顺序递增跳转逻辑误触发或复位未结束查看 PC 波形和控制器跳转信号检查 JMP 指令译码条件确认复位后 PC 初值加法结果明显不对数据位序接反或操作数来源选错打印 ALU 输入输出核对数据宽度、字节序和 ALUSrc 选择内存被莫名写入MemWrite 信号悬空或默认有效查看 MemWrite 默认电平控制器中所有未使用状态都要给出确定的控制信号仿真通过了但板卡不稳定组合逻辑延迟或电源问题降低时钟频率或观察信号毛刺换更快逻辑门、加去耦电容、使用更保守时钟这些故障的共同规律是问题往往不在某一个门而在“使能信号没有按预期配合”。所以调试时一定要同时观察控制信号和数据信号不能只盯住某一位输出。7.2 一套可复用的排错顺序出现异常时建议按下面顺序排查不要跳步。先检查时钟。时钟周期是否固定是否到达每个触发器。再检查复位。复位信号是否已经释放复位后电路有没有进入确定状态。检查 PC 和取指。PC 是否递增IR 是否写入了正确指令。检查控制信号。译码后的 PCWrite、RegWrite、MemWrite 是否只在正确状态有效。检查 ALU。两个输入是否正确运算结果是否符合真值表。检查写回。目标寄存器或数据存储器是否在正确时钟沿写入了结果。最后检查多条指令衔接。上一条指令的写回会不会影响下一条指令的读操作。这个顺序从“时间基准”开始到“功能模块”再到“整机衔接”能避免你在复杂波形里盲目猜问题。7.3 搭建前检查清单每次开始一个新模块以前先过一遍清单。它不需要很复杂但能挡住大部分低级错误。时钟周期已经确定Testbench 里已经生成 clk 信号。复位电平定义清楚是高有效还是低有效所有模块统一。每个模块的输入输出真值表已经写好。组合逻辑 always 块的所有分支和 default 值都写完整。所有未使用的控制信号都固定为确定电平。寄存器堆读操作和写操作不会发生在同一个时钟沿或已经做了时序分离。指令内存和数据内存的读写使能明确。每个模块先单独仿真通过再连入顶层。每增加一条指令就增加一个最小验证程序。上板前已经在真机上跑过完整的内存断言而不只是看过波形。8. 最佳实践与扩展方向8.1 手搓 CPU 的三条设计原则第一条原则是模块边界清晰。PC、IR、ALU、寄存器堆、控制状态机各占一个文件或一张图纸每个模块只暴露必要的端口。这样单个模块出问题不会牵连整份代码。第二条原则是先验证再集成。任何模块只要没有输出正确结果就不要接到顶层。许多项目死在“代码全部写完结果全错”根源就是没有做单模块测试。第三条原则是控制信号要显式复位。上电一瞬间寄存器内容未知控制信号也可能处于高阻状态。必须给控制器一个复位状态并规定每一个未使用状态的默认控制信号。8.2 下一步可以扩展的方向最小 8 位 CPU 能跑通之后可以往以下几个方向扩展把串行进位加法器换成超前进位加法器体会“用逻辑门换时间”。加入条件跳转指令比如 JZ结果为零则跳转让程序支持分支。把单周期执行改成多周期甚至加入简单流水线。用微码控制器替换硬布线控制器对比两种方式的设计自由度。加入中断请求和中断返回指令接近真实处理器。给 CPU 增加输入输出端口把它接到串口或显示模块上。扩展时不要一次加太多功能。建议一次只改一个维度要么加指令要么改时序要么改存储结构。这样出现问题时能迅速定位到变量。8.3 学习路径建议如果想继续深入可以参考经典书籍和公开课中“从 NAND 到应用”的路线例如一些课程会从一颗 NAND 门开始逐步做出一个能运行简单程序的计算机。这类课程的好处是每一步都建立在可运行实验上和“从 0 手搓 CPU”的思路一致。对新手来说最有价值的练习不是直接设计 32 位乱序执行处理器而是把一个 8 位 CPU 从头到尾完整做出来先画数据通路再写模块再仿真再上板。等你真正理解了时钟、控制信号和状态机这三样东西后面学任何架构都只是在这些基础上增加复杂度。最终你会发现所谓 CPU并不存在某个不可拆解的魔法模块。它能执行的每一条指令最终都落在逻辑门的电平变化上。理解到这一层以后再看到“CPU 体系结构”“计算机组成原理”这些术语时你看到的就不再是黑盒而是一层层可以定位、可以验证、可以修改的具体电路结构。
返回列表