简介山东大学计算机组成原理课设——简单模型机设计微程序实现是一份面向计算机组成原理课程的FPGA工程参考包适用于需要完成模型机设计或进阶理解微程序控制器的本科生。资源基于Quartus II 13.0开发重点覆盖指令译码、数据通路、运算器、时序生成与微程序控制逻辑等环节展示了如何在FPGA上以微程序方式实现一台简单模型机。压缩包共152个文件大小约2.18MB。文件类型以bdf/bsf原理图、tdf/vhd硬件描述源码为主并包含rpt综合报告、mif存储器初始化文件、vwf仿真波形和jpg实验截图等可辅助复现工程、查看微指令序列与调试仿真过程。目前已有3669人学习下载。通过这套工程可获取模型机各模块连接关系、微程序控制核心实现及完整仿真流程有助于理解计算机硬件工作原理也能为课程设计报告撰写与答辩提供素材。实验存在一定工具版本依赖建议使用相同的Quartus II版本打开工程。1. 微程序模型机课设到底在逼你做哪件事某高校的计算机组成原理课设常常要求做一台“简单模型机微程序实现”指令系统自己定数据通路自己连控制器不用硬布线而用微程序最终能在仿真环境里把一条条指令跑起来看到寄存器、内存、控制信号按预期变化。说白了这门课设不是让你背“微程序控制”的定义而是逼你亲手把“控制器到底是怎么一条条发控制信号的”这件事从黑匣子变成透明盒子。你做完之后敢拍着胸脯说一句“CPU 那条取指-译码-执行的路我真的走通了”。这篇笔记就按我实际做模拟项目X时的路线来写从指令系统和数据通路怎么定到微指令格式和控制信号怎么编码再到 Verilog 里的具体实现最后把我翻过车的地方集中泼给你。适合正在做课设、又被“微程序”三个字卡住的人——新手能跟着搭起第一版熟手可以直接跳到避坑章对着自己的仿真波形自查。2. 设计一台模型机第一步先定指令集再定数据通路2.1 指令系统设计先圈定指令种类后面写微程序才不失控做微程序模型机最忌讳一上来就画数据通路。硬件结构是由指令集反推出来的每条指令需要哪些源操作数、要做什么运算、结果往哪写决定了你需要哪些寄存器、哪些总线、哪些控制点。我一般会先把指令集压缩在 8 条左右比如传送类LDA取数到 A、STA存 A 到内存运算类ADDA 加内存值、SUBA 减内存值、ANDA 与内存值跳转类JMP无条件跳转、JZ结果为 0 跳转停机类HLT每类挑一两条代表就够了。课设评分看的是“微程序实现过程完整”不是指令数量。指令多了微程序 ROM 条目翻倍排错成本跟着涨。确定指令集后还要给每条指令定编码格式。常见做法是单字长格式高 4 位为操作码 OP低 4 位为地址字段 ADDR或者直接给出内存单元号。这样模型机的寻址空间就是 16 个单元教学上够用了。操作码编码我习惯用独热码风格而不是二进制顺序编码比如 LDA0001、STA0010、ADD0011这样后续译码逻辑写起来直观也便于查错代价只是操作码空间浪费而模型机根本不在乎这点浪费。这些设计决策必须写进课设文档的“指令系统设计”章节。很多同学在这章只贴一张指令表却不解释“为什么 8 条就够”“为什么用独热码”答辩时很容易被问住。你在文档里写清楚“每条指令需要几个微周期、为什么需要这么多”比罗列一堆指令更能体现你真的理解了微程序。2.2 数据通路设计公共总线加寄存器的经典结构模型机数据通路我用的是教科书里最常见的“单总线结构 通用寄存器组”它和微程序控制天然匹配每个微周期内控制信号决定“哪个寄存器输出到总线、算逻单元ALU做什么运算、结果写入哪个寄存器”。总线是公共的所以同一时刻只能有一个源往总线上送数这是后续所有控制信号设计的根本约束。我的模型机里寄存器规划如下寄存器宽度作用备注PC4 位程序计数器指向下一条指令地址AR4 位地址寄存器作为内存地址输入IR8 位指令寄存器存放当前指令A4 位累加器ALU 输入端与结果寄存器B4 位暂存寄存器ALU 另一输入端DR4 位数据寄存器内存读出的数据暂存对应这条通路控制信号分成两组数据通路控制信号源选择、写入使能、运算模式和内存控制信号读/写、地址来源选择。后面设计微指令时就是把这些控制信号按位排到一个长字段里。A 同学踩过的一个典型坑是寄存器数据来源和目标写使能信号连在一起设计导致“A 寄存器输出”和“A 寄存器写入”永远同时有效数据在总线里被反复写同一个寄存器仿真结果看起来像没反应。控制信号必须独立成位写入使能是写入使能输出使能是输出使能两者各管各的。数据通路画好之后把每条信号列成一张表标注“该信号有效的微周期”这表就是微指令编码的原始依据。有个好习惯先按流程图把信号表写出来再回头调整数据通路。我第一版就是反过来画的结果反复改了五六次连接才让所有微操作都能找到通路。2.3 微指令格式选型垂直型还是水平型这道选择题决定你后面调试的工作量微指令的编排是微程序控制器设计的核心决策主要分两类水平型微指令每个控制位对应一个控制信号一条微指令里可以同时发起多个并行微操作。执行速度快但是指令字长且难读。垂直型微指令类似普通机器指令每条微指令完成一个简单操作需要多个微指令才能完成一个机器周期。字长短但对硬件译码电路要求高且微程序更长。课设模型机强烈建议选水平型。原因不是性能而是可读性你仿真时看到微指令里某一位为 1就能知道内存读使能在那拍被拉高了出错时能直接用波形的每一位去对照控制信号表。垂直型的译码封装会把问题藏起来调试时你对着“微指令 opcode 是 3 到底是什么操作”发呆极其痛苦。水平型微指令字段设计我采用的格式如下操作控制字段16 位每位对应一个控制信号独热使能、判别字段2 位用于表示是否需要根据状态条件进行分支、下地址字段6 位指向下一条微指令地址。加总 24 位。判别字段的编码规则00 表示顺序执行01 表示按 IR 操作码译码分支10 表示按进位标志 C 分支11 表示按零标志 Z 分支。这个设计能覆盖取指、执行、跳转判断三类最常见的流程。3. 把机器指令翻译成微程序取指公共段与执行段的分工3.1 取指周期是公共微程序所有指令共用同一段开头微程序和普通程序一样要有公共入口。无论执行哪条机器指令取指周期做的事情都一样因此这段微程序放到公共微程序段所有指令共用。以我设计的模型机为例取指公共段需要三条微指令第一条把 PC 地址送到 AR第二条启动内存读并将读出的指令写入 IR第三条 PC 自增并回到公共入口。写成微程序伪码如下所示M_ADDR: AR - PC // 微地址 00PC 送地址寄存器 M_READ: IR - M[AR] // 微地址 01读内存送指令寄存器 M_INC: PC - PC 1; 转移到取指结束按操作码分支 // 微地址 02这段话里藏着整个微程序控制器最关键的一个决策点第三条微指令“按操作码分支”不是直接进入执行周期而是需要一个翻译环节。因为我们的操作码是 4 位而微程序 ROM 里每个指令执行段有独立入口这个入口地址由“操作码 执行段基地址”拼接而成。我在这里把执行段基础映射关系放到一张表里微程序编写时不必背地址只按逻辑顺序排队即可机器指令操作码执行段入口微地址LDA00010x10STA00100x14ADD00110x18SUB01000x1CAND01010x20JMP01100x24JZ01110x28HLT10000x2C3.2 一条 ADD 指令的执行段每个微周期做什么要能写出流程图我们拆解一条 ADD A, M[ADDR] 指令它从内存指定单元取数与累加器 A 相加结果存回 A。如果用文字描述这个过程需要好几句但用微程序写法就是四个明确的微周期。我把微程序表给出并附上每个微周期里各控制信号的状态解释// ADD 指令执行段微程序 AD_FETCH: AR - IR[ADDR] // 把指令低 4 位内存地址送 AR AD_READ: DR - M[AR] // 读内存数据进数据寄存器 DR AD_ADD: A - A DR // ALU 做加法结果写回 A AD_DONE: 根据 Z/C 标志决定后续无分支则回公共取指很多同学写到这里会把第三条微指令写成 A - A B却忘记先把内存数据放进 B 寄存器。这就是数据通路的约束在设计微程序时被发现单总线上ALU 两个输入必须同时可获取。我们只有一条总线所以必须提前把其中一个操作数稳定住——于是就有了 B 寄存器作为“锁定”第二个操作数的暂存器。正确顺序是DR 先锁存数据下个微周期把 DR 输出到总线同时 ALU 另一输入固定成 A 寄存器这就需要 B 暂存否则第二个数到达ALU时第一个数已经被总线上的新数据覆盖了。ADD 的执行段里第四个微周期通常不需要进行 Z/C 更新后的分支而是直接跳回取指公共段。但 JZ 这样的条件跳转指令就需要用到 Z 标志。JZ 的执行段微程序设计方案是第一条微指令根据零标志 Z 是否有效决定下一条微地址是跳转目标修改还是顺序返回。3.3 微程序编址方式顺序执行加一级分支映射就够微程序控制器的下一条微地址怎么来我们用的最简单的方案是微程序计数器μPC自动加一作为顺序下地址遇到分支则由“判别字段”决定是否加载新的微地址。也就是说我们不需要复杂的微地址发生器硬件只需要// 下地址逻辑伪代码 if (判别字段 按操作码分支) begin 下地址 {执行段基地址, IR[7:4]}; // 拼接出执行段入口 end else if (判别字段 按Z标志分支) begin 下地址 (Z 1) ? 跳转地址 : 顺序下地址; end else begin 下地址 当前微地址 1; end这方案的优点在课设文档里可以说得很漂亮微地址的形成逻辑完全规则化没有复杂的 PLA 译码。缺点也很明显每条执行段都有固定长度区域空间浪费大。不过 8 条指令每条分配 4 个微地址空间一共 32 个微地址ROM 才多大浪费得起。写微程序时我建议你把每条指令的执行段都用表格列出来手工算好每个微地址的偏移量再核对评测功能是否要求写出微程序控制器原理图。有的课设要求用到“组合逻辑译码法”而微程序实现用的“存储逻辑法”。这两者的对比是答辩常报问题组合逻辑控制器由门电路产生控制信号修改指令需要重新布线微程序控制器控制信号存放在 ROM 里改微程序就能改指令灵活很多代价是多一拍 ROM 访问时间。4. Verilog 实现微程序控制器从 ROM 表到时钟驱动4.1 微程序 ROM 用什么写case 语句就是你的只读存储器模型机同步仿真用 Verilog 实现我会把微程序控制器设计成两个核心模块微程序 ROM 模块和微地址生成模块。ROM 模块不需要调用 IP 核Verilog 的 case 语句配合寄存器的组合逻辑查找就是最直观的“微程序存储”。下面给出一个简化版的微程序 ROM 示例用 24 位微指令作为输出module micro_rom( input wire [5:0] micro_addr, // 6 位微地址覆盖 64 个条目 output reg [23:0] micro_code // 24 位微指令控制字 ); // 字段声明仅示意实际以你的控制信号表为准 // micro_code[23:20] : μOP 控制译码 // micro_code[19:8] : 数据通路控制信号 // micro_code[7:6] : 判别字段 P // micro_code[5:0] : 下地址字段 NA always (*) begin case (micro_addr) 6d0: micro_code 24b0001_000000000001_00_000001; // AR-PC 6d1: micro_code 24b0010_100000001000_00_000010; // IR-M[AR] 6d2: micro_code 24b0000_000000100000_01_000000; // PC自增按操作码分支 // 此处省略其余微指令 default: micro_code 24b0; endcase end endmodule这段代码里的下地址字段需要你手工维护非常容易数错位。我的习惯是把微指令按竖排写成二进制文本每 4 位一组分组显示这样肉眼校验字段边界时省力得多。同时你应该在使用说明文档中写明每个控制位的缩写名例如第 8 位是“DR 写入使能”这样波形检查时能在仿真软件里按位查看。参数上还有两个设置值得注意微地址宽度取 6 位最大支持 64 条微指令控制字宽度 24 位包含两大字段。如果你的课设要求有算术逻辑运算模式选择字段会多出 4 到 6 位控制字宽度相应扩展到 28 或 30 位这属于正常调整。4.2 微地址生成模块把分支条件接进下一拍微地址有了微程序 ROM还需要微地址生成模块来驱动它。我写的微地址生成逻辑分两级描述第一级组合逻辑生成“拟用下地址”第二级在时钟沿锁定。下面给出简化代码module micro_seq ( input wire clk, input wire rst_n, input wire [3:0] opcode, // 指令寄存器高四位 input wire zero_flag, // Z 标志 output reg [5:0] micro_addr ); wire [5:0] next_addr; always (*) begin case (判别字段) 2b00: next_addr micro_addr 1b1; // 顺序执行 2b01: next_addr {2b01, opcode}; // 按操作码跳转基地址 0x10 2b10: next_addr zero_flag ? 6d37 : micro_addr 1b1; // 条件跳转 default: next_addr 6d0; endcase end always (posedge clk or negedge rst_n) begin if (!rst_n) micro_addr 6d0; else micro_addr next_addr; end endmodule注意里面“按操作码跳转”用的是拼接{2b01, opcode}。这意味着执行段入口地址必须与操作码对齐才能成立设计微程序段时务必让入口地址低四位等于操作码数值。如果入口地址不满足这个规律你就会需要一个地址映射查找表实现复杂度立刻上来了。所以最初规划微程序存储布局时就要故意把 LDA 的入口放在 0x10STA 放在 0x14ADD 放在 0x18这正是前面那张映射表的意义。零标志 Z 的更新也应遵循一个教训Z 标志必须在加法或减法微周期的下一个时钟沿更新不能和 ALU 输出同一拍就送进微地址生成逻辑。因为微地址生成逻辑和 ALU 结果更新如果抢同一拍会形成组合环路仿真结果要么时序脏乱要么整条微程序死循环。建议单独用一个寄存器存放 Z 标志在运算完成后一拍再更新这个“慢一拍”的时序设计是稳定的关键。4.3 数据通路和内存模型的 Verilog 骨架让微程序跑起来的最小仿真环境上面两个模块只是控制器还缺数据通路和内存模型才能整体仿真。我用一个总体模块把两者组装起来module model_cpu ( input wire clk, input wire rst_n, output wire [7:0] debug_bus // 观察用总线 ); // 寄存器声明 reg [3:0] PC, AR, A, B, DR; reg [3:0] IR; // 微指令信号抽线 wire [23:0] micro_code; // 译出关键控制信号 wire ld_PC micro_code[0]; // PC 装载使能 wire inc_PC micro_code[1]; // PC 加一使能 // 其余信号照此展开 // 内存模型16 个 4 位单元 reg [3:0] mem [0:15]; // 数据通路 always (posedge clk) begin if (ld_AR) AR PC; // 时序逻辑逐条仿照微指令定义书写 end endmodule这个模块最重要的设计原则是每个控制信号在微指令中的位号必须和数据通路模块一一对应不能有重名错位。我见过一个最耗时的问题内存读写使能用同一位号却分属不同方向导致一条 STA 指令把内存写成了全 1。排查了一天最后发现是微指令编码表里“读”和“写”位共享了同一个编号。血泪经验控制字位号设计成“读使能”和“写使能”分开绝不合并成一个读/写方向位。内存模型的写法上用 reg 数组加同步读写的常见做法是读操作在时钟沿后立即输出写操作在同一个沿写入。你如果不加初始数据仿真时内存全为不定态 X指令根本跑不起来。建议在 initial 块里给内存单元初始化一段指令序列最小程序可以是一条 LDA 加一条 STA 收尾保证取指后能进入执行段。5. 模型机调试避坑5 个我反复踩进同一个坑里的问题5.1 现象一微程序永远卡在取指段执行段入口进不去仿真波形里看到微地址一直在 0、1、2 三个值中循环永远跳不到 0x10 之后。原因几乎都是判别字段或操作码拼接错位。微地址生成模块的按操作码分支逻辑里如果拼接用的 opcode 位数和下地址位宽不一致比如 opcode 是 4 位而高基地址也是 4 位拼接结果变成 8 位却赋给了 6 位信号被截断后地址直接变成随机值。解决方法是打印微地址值比对每个执行段的入口是否等于预先规划表。排查时写一段 testbench让 opcode 依次等于 LDA 到 HLT观察微地址是否依次跳到 0x10、0x14、0x18……一直到 0x2C。如果跳转目标偏移了 2那十有八九是基地址统一加了微地址偏移量却没用常数直接改成 {2b01, opcode} 并用 testbench 确认即可。5.2 现象二加法结果一直少 1或者算出来的数完全不对ALU 功能没问题但 A 寄存器结果少 1或者直接是内存里的原始数据。排查时发现 ADD 执行段 AD_READ 和 AD_ADD 之间少了一个微周期DR 读出的数据没有先送到 B 寄存器ADD 微周期里 B 保持的还是上一个旧值。这就是我在前面强调的“B 寄存器锁定操作数”的典型翻车现场。某同学问我说 B 寄存器是否多余我直接让他去掉后仿真加法结果自然错乱。解决方法是增加一个微周期AD_STB 微周期让 DR - B再在 AD_ADD 周期里 B 保持稳定ALU 才能得到正确的两个输入。同时请检查微程序 ROM 里 AD_STB 对应条目的下地址字段不要因为多插一条微指令而导致后续地址全偏。5.3 现象三STA 存数据时内存单元写进去的全是 1 或者不定态STA 指令执行段的微操作顺序应该是AR - IR[ADDR]A - DRDR - M[AR]。但我第一次写时把“A - DR”和“DR - M[AR]”放在同一个微周期里总线上的新数据覆盖了 DR 旧值内存写进去的其实是总线上的残留瞬时值最终表现为乱值这不是真全 1 也不是特定的脏数。解决问题的方法有两个一是把 STA 的执行段拆成两个微周期让 DR 先锁存 A再让 DR 输出到内存数据线二是给内存写数据端加一级寄存器缓存。前者更符合单总线结构的特点。在设计文档里要特别说明 STA 不能用一条微指令完成因为单总线不允许“寄存器输出到总线并同时写入另一个寄存器”和“把总线上的值写成内存”在同拍完成。5.4 现象四条件跳转 JZ 指令执行后程序要么永远跳转要么永远不会跳JZ 的微程序是周期 1 根据 Z 标志判断是否跳转周期 2 修改 PC。问题在于很多人的微地址生成逻辑里判断 Z 标志用的还是跳转前的老值没有滞后一拍导致 ALU 刚算出一个非零结果Z 标志还在变微地址已经做出了错误的跳转决定——所以“永远不跳”倒罢了“永远跳”这种幻觉就是 Z 标志和微地址逻辑抢时序抢出来的。正确做法是把 Z 标志在运算结果寄存器更新后的下一个时钟沿才更新到标志寄存器或者参考 4.2 节里 micro_seq 组合逻辑和时序逻辑分离的做法确保微地址分支判断总是使用上一拍已经稳定的标志位。另外测试时注意JZ 指令判断的“零”是运算结果零而不是指令本身地址字段为零别拿 IR 的立即数去比较。5.5 现象五仿真波形上有毛刺某个控制信号莫名出现极窄脉冲这个问题非常玄学但原因很机械组合逻辑输出总线竞争两条信号同时驱动同一条线仿真器会在极小时间间隔里看到多次切换。比如内存读使能由微指令控制字某位直接引出同时内存地址选择逻辑里也有一路输出接到读使能上这两个组合逻辑叠加控制信号就出现了毛刺。解决步骤是先给控制信号都加一级寄存器输出把组合逻辑变成时序输出同时检查微指令控制字位号有没有重复赋值的编码失误。实践中在顶层模块例化处拉一条 glitch_free 信号线替换原控制信号再跑同一仿真完全消除毛刺后才是真的修好。组合逻辑毛刺在仿真里偶尔不影响最终结果但会导致内存读写提前一拍执行属于最隐蔽的坑。6. 一个最值的调试技巧把微地址、微指令、控制信号同时拉进同一条波形讲到验证再补你最后一刀仿真波形不要只盯 PC 和 A 寄存器要把微地址、微指令完整控制字、IR、Z 标志全部拉进同一画面。具体操作是在 testbench 中把顶层模块的内部信号用层次引用暴露出来并接入观察总线或者直接把微地址作为输出端口引到仿真顶层。这样每条机器指令从取指到执行的每个微周期五六个信号在同一根时间轴上对齐错一拍瞬间可见。这招的验证效果非常直观你选单步执行一条 ADD预期看到微地址顺序是 0-1-2-18进入执行-19-20-21-22-0A 寄存器在 20 的下一拍更新。哪一步的微地址不符合预期问题就锁定在哪个字段。我自己调试模拟项目X时有超过一半的问题都是靠这种总线观察法直接看出来的而不是靠断断续续打印寄存器值去猜。进阶方向你有兴趣的话可以在现有模型机上扩展乘法指令把乘加循环展开成两三条微指令的循环用计数寄存器控制循环次数。这个改动不大但对微指令循环结构、Z/C 标志复用、下地址回跳三条知识点的考察比新增十条简单指令都扎实。答辩时如果被问“微程序和硬布线的性能差异”你可以用自己模型机里每条指令多了一个 ROM 访问周期来回答既有数据支撑又能体现出“从实现出发理解概念”的深度。这也是我做课设后经常给学弟学妹唠叨的一句话先做出一个错误百出但能跑的版本再在跑起来的基础上对照波形调比闭门连写三天后直接上线更现实——毕竟那三天里你连自己错在哪都不知道只能事后花更多时间重构希望这篇笔记能帮你少绕这些远路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?