
简介面向山东大学计算机组成原理课程设计的简单模型机设计项目包采用微程序控制方式实现适合正在完成课设、需要对照工程源码理解控制器与数据通路配合或入门Quartus II上FPGA逻辑设计的高年级本科生。压缩包共152个文件大小约2.18MB以Quartus II 13.0工程为主按功能分为原理图与符号文件.bdf/.bsf、硬件描述语言源码.tdf/.vhd、仿真测试文件.vwf/.do、综合报告.rpt/.summary、存储器初始化文件.mif以及目标配置生成文件.sof/.qpf等整体覆盖从电路设计、编译综合到仿真验证的完整流程。已有3669人学习下载。工程顶层lab3.bdf集成了ALU、3-8译码器、移位寄存器、多路选择器、数据选择与地址生成等模块可对照各子模块原理图和HDL源码查看多个仿真波形梳理指令译码、微指令序列与数据通路之间的时序关系。对于课设排错、方案比较和微程序控制器落地实践均有直接参考价值。1. 把课设拆成活部件微程序模型机到底在做什么某高校计算机组成原理课设里“简单模型机设计微程序实现”是出现频率最高的题目之一。拿到题目就写代码是最容易翻车的开局很多同学把重点放在“写一个能跑指令的程序”上结果被控制器、数据通路、控制信号一堆黑匣子卡住。这个课设真正要交付的是一件“能按指令干活的小CPU”而它的灵魂不是代码是一张叫“控制存储”的表。你只要能读懂这张表微指令、时序、仿真的问题会一下子变透明。本文按“先定指令集和数据通路再写微程序最后仿真验证”的顺序把它完整拆给你适合正在做类似课程设计、又不想只抄报告的读者。2. 先定指令集与数据通路两张决定微程序寿命的表格2.1 指令集设计六条指令就能撑起完整的流程很多课程设计要求做“简单模型机”但简单不等于少。我见过某组的报告写了二十几条指令微程序表画了三页最后连仿真都没跑完。原因是微程序控制器每多一条指令就要多一组“取指到执行的子程序”而每个子程序里每条微指令都要逐拍对齐数据通路。与其盲目扩充不如先用最核心的六条指令验证整个架构能跑通再按需加。我采用的指令格式是双字节定长格式第一字节是操作码第二字节是地址或立即数。数据宽度固定为8位地址宽度同样8位内存容量取256字节仿真时用128字节也足够。这样指令格式简单取指周期也能统一微程序无需为“这条指令有几字节”做额外判断。助记符机器码操作说明LDA addr0x20AC ← M[addr]更新零标志ZSTA addr0x30M[addr] ← ACADD addr0x40AC ← AC M[addr]更新零标志Z和进位标志CJZ addr0x10若Z1PC ← addr否则继续下一条指令JMP addr0x70PC ← addr无条件转移HLT0xF0停机暂停时钟选择这六条指令不是随意的。LDA和STA覆盖了“寄存器到内存、内存到寄存器”的两条必经路径ADD让ALU真正参与运算并暴露标志位问题JZ是最难做的条件转移能验证微程序里“按条件选择下址”的能力HLT则让整机有一个明确的终止状态。这组指令能覆盖单字节取指、双字节取指、读写内存、ALU运算、寄存器载入、条件分支、停机等几乎全部控制场景比堆二十条同类型指令有价值得多。地址取值也要说清楚在汇编助记符中写“LDA 0x30”实际存储在内存里的是两个字节第一个字节是0x20第二个字节是0x30。取指阶段会连续读两次内存第一次读操作码第二次读操作数地址之后PC已经指向下一条指令的首字节。这一点如果没想明白后面微程序里的PC增量时机一定会出错。2.2 数据通路与控制信号让每个寄存器都听命于控制字指令集定完后第二步就是把数据通路画出来。我习惯先画一张“寄存器与总线”的块图不需要画连线图只要把关键模块和传递方向写清楚PC8位程序计数器带增量功能输出到地址总线。AR8位地址寄存器保存当前要访问的内存地址。DR8位数据寄存器保存从内存读出的数据或者待写入内存的数据。IR8位指令寄存器保存当前指令的操作码用于产生操作码映射地址。AC8位累加器是ALU的主要输入和运算结果寄存器。FR标志寄存器至少包含Z和C两位。数据通路采用单总线结构所有寄存器输出经过三态门挂到数据总线上ALU输出也挂到总线上。这样控制逻辑只需要决定“哪路源把数据放到总线上哪个寄存器把总线数据锁存下来”。单总线结构简单但同一拍只能完成一次源到目的传递这也是为什么一条宏指令通常需要多个微指令拍才能完成。从实现角度讲单总线是课设里最容易梳理清楚、也最不容易在仿真中打架的方案。我定义的常用控制信号如下控制信号作用LDPC把总线上数据写入PCLDAR把总线上数据写入ARLDDR把总线上数据写入DRLDIR把总线上数据写入IRLDAC把总线上数据写入ACPC_INCPC加一MEM_RD读内存输出到总线上MEM_WR写内存数据来自总线ALU_ADDALU执行加法ALU_PASSALU直通即结果等于输入FLAG_EN运算结束后锁存Z/C标志HALT停止时钟控制信号不需要每个都单独占一条线实际可以合并。比如MEM_RD和MEM_WR合起来用两位状态编码ALU操作也只需要几位选择码。这里我使用“动作码”方式把一组互斥的微操作编码成一个字节再通过译码器展开成上述具体控制信号。这样微指令字短写微程序时也直观。把指令和微操作对应起来看就能发现规律LDA需要“从内存读地址→从地址读数据→写入AC”而ADD比LDA多一个“AC和DR相加”的步骤。ISA层面的指令最终都会被拆成一串控制信号序列编写微程序的工作量就集中在这个拆解过程。能把这个对应关系理清比直接上手改波形文件重要得多。3. 微程序控制器落地微指令格式、ROM生成与一条指令的执行过程3.1 微指令格式和定序方式微程序控制器的核心是控制存储ROM里面每一格保存一条微指令。每条微指令告诉硬件“这一拍执行什么微操作下一拍去哪”。我给这个模型机设计了16位微指令字分成三个字段字段位宽含义动作字段8位微操作编码译码后生成控制信号序控字段2位决定下一微地址来源下址字段6位直接给出下一微地址序控字段的四种取值是这套设计的关键00下址字段按给定地址走。01顺序加一等价于普通程序的顺序执行。02操作码映射把IR高四位拼到微地址高四位低四位清零跳到该指令对应的微程序入口。03条件分支根据FR中的Z标志选择走哪条路用于JZ指令。这里有一个课设里的常见设计为简化操作码映射我不让每个操作码对应一个独立页面而是让同组指令共用页面。六条指令的操作码分别是0x20、0x30、0x40、0x10、0x70、0xF0操作码的高四位分别是2、3、4、1、7、F。取指阶段结束后把高四位作为微地址高四位低四位清零正好分别跳到0x20、0x30、0x40、0x10、0x70、0xF0这六个入口。这就是“操作码映射定序”的直观效果不需要额外查表只靠一组选择器就能完成宏指令到微子程序的跳转。3.2 微程序清单六条指令的完整控制流先写公共取指段四条微指令完成“读操作码、读操作数、PC自增”三个动作微地址动作字段序控字段下址说明0x00PC→AR顺序0x01把PC指向的指令地址打入AR0x01M[AR]→IR, PC顺序0x02读操作码到IRPC指向后半字节0x02PC→AR顺序0x03AR指向操作数地址0x03M[AR]→DR, PC操作码映射0x00读操作数到DRPC指向下一条指令然后映射这里最容易被忽视的是0x01和0x03都对PC加了1。第一条指令经过取指后PC要越过两个字节所以必须做两次PC_INC否则下一次取指还是会读同一个地址仿真里就会出现“一条指令卡死”的现象。取指完成后根据指令操作码跳转到各自入口。六个执行子程序如下微地址动作字段序控字段下址对应指令0x20DR→AR顺序0x21LDA入口0x21M[AR]→DR顺序0x22读目标内存单元到DR0x22AC←DR, 更新Z下址0x00AC载入返回取指0x30DR→AR顺序0x31STA入口0x31M[AR]←AC下址0x00写内存返回取指0x40DR→AR顺序0x41ADD入口0x41M[AR]→DR顺序0x42读操作数到DR0x42AC←ALU_ADD(AC,DR), 更新Z/C下址0x00相加并写回返回取指0x10判断Z条件分支0x11JZ入口Z1跳到0x12否则0x110x11空操作下址0x00未跳转顺序执行下一条宏指令0x12DR→PC下址0x00跳转目标写入PC0x70DR→PC下址0x00JMP入口0xF0停机下址0xF0HLT入口保持停机状态对照微地址表能看出ADD这样看似简单的指令实际要经过取指4拍加执行3拍共7个微周期。如果某条指令执行结果不对第一步就应该在微程序表里逐条核对而不是对着仿真波形乱猜。微程序实现方式最大的优点就在这里每一拍对应的动作都是显式写出来的能直接在表上“人肉检票”。3.3 Python脚本生成控制存储ROM文件手工往仿真平台的ROM初始化文件里填十六进制数据非常容易错我通常写一个极简Python脚本做编码转换。脚本里用可读字符串描述微程序然后自动生成HEX文件方便反复修改。actions { PC_AR: 0x01, M_IR_PINC: 0x02, M_DR_PINC: 0x04, DR_AR: 0x05, M_DR: 0x06, AC_DR: 0x07, M_AC: 0x08, ALU_ADD: 0x09, DR_PC: 0x0A, HALT: 0x0B, NOP: 0x00, } seq_mode {SEQ: 0, ADDR: 1, MAP: 2, COND: 3} urom [] def add(addr, act, mode, nxt): if mode SEQ: nxt addr 1 word (actions[act] 8) | (seq_mode[mode] 6) | nxt urom.append((addr, word)) add(0x00, PC_AR, SEQ, 0) add(0x01, M_IR_PINC, SEQ, 0) add(0x02, PC_AR, SEQ, 0) add(0x03, M_DR_PINC, MAP, 0) add(0x20, DR_AR, SEQ, 0) add(0x21, M_DR, SEQ, 0) add(0x22, AC_DR, ADDR, 0) add(0x30, DR_AR, SEQ, 0) add(0x31, M_AC, ADDR, 0) add(0x40, DR_AR, SEQ, 0) add(0x41, M_DR, SEQ, 0) add(0x42, ALU_ADD, ADDR, 0) add(0x10, NOP, COND, 0x11) add(0x11, NOP, ADDR, 0) add(0x12, DR_PC, ADDR, 0) add(0x70, DR_PC, ADDR, 0) add(0xF0, HALT, ADDR, 0)这段脚本把微程序表转成16位字写入“urom.hex”。逻辑说明add函数接受地址、动作、序控模式和下一地址如果是顺序模式则下址自动取当前地址加1省去手写。word拼接时高8位是动作编码中间2位是序控模式低6位是下址。条件分支那条指令的nxt参数传0x11仿真器在Z等于1时改用0x12这属于硬件逻辑不在ROM数据里体现。参数上要留意MAP模式中nxt值无效硬件会直接用IR高四位生成地址所以填0即可。如果你在仿真平台里看到0x03地址后的指令不对首先查操作码高四位与ROM入口0x20、0x30、0x40这些值是否对得上。4. 常见问题排查五条把模型机课设救回来的实测记录4.1 现象PC始终读出同一地址所有指令循环执行同一段原因基本可以锁定在PC_INC信号没有真正作用到PC上。我在仿真里遇到过PC连线接到了PC的复位端而不是增量端导致取指阶段始终读第一条指令。另一个常见原因是两个字节指令只做了一次PC增量第二次读操作数时又回到当前地址。解决方式是先检查PC模块的增量逻辑时钟上升沿到来时若PC_INC有效PC应加1然后检查取指段0x01和0x03是否都产生了PC_INC信号。如果仿真时信号名相同但连到了两个不同实例上也会出现这种表现。4.2 现象操作码映射后直接跳到0x00执行子程序全部失效这是微程序定序中最容易踩的坑。现象是ADD指令执行时微地址从0x03变成0x00而不是0x40。原因是仿真平台里IR的高四位没有接入微地址生成器或者接入的是IR的低四位导致0x40这个入口被映射到0x00。解决方式是明确“操作码映射”是把IR的高四位放到微地址高四位低四位清0。如果你的微架构用了8位微地址也要保证IR在取指完成后一直稳定不变否则下一拍映射会用到已经更新的数据。4.3 现象JZ指令永远跳转或永远不跳转问题大概率出在标志位只更新了ALU输出却没有送到FR寄存器的输入。另一种情况更隐蔽执行ADD时FR更新和AC写入共用一个时钟沿那么执行子程序内如果继续用FR判断会读到旧标志。常见做法是让FLAG_EN在运算完成后的下一个微周期再生效比如在我的微程序表里ADD的0x41拍只读数据0x42拍才做加法和写标志标志数据在0x43拍之前必须保持稳定。解决方式是给FR加一个独立的锁存使能不要和AC的写入使能共用。4.4 现象STA写内存时地址和数据总有那么一次错位原因通常是AR和DR的更新时序没有错开。按我的微程序表STA执行时要先DR→AR然后再用AR去访问内存。如果DR→AR这一步没完成MEM_WR就生效那写的就是旧AR地址。解决方式是把“DR→AR”和“M[AR]←AC”拆成两个微周期中间至少隔一个时钟确保AR稳定后MEM_WR再有效。仿真波形上要能看出AR在写周期之前已经等于DR的值。4.5 现象ROM文件加载后前几行全为零指令执行完全无规律这通常不是设计问题是文件格式问题。很多HEX文件默认带地址偏移比如第一行地址是0x00但仿真平台按0x0000起始加载就会错位。解决方式是统一格式要么全部用相对地址要么加载时把“base address”设置为0x00。字节序也有讲究16位微指令字如果按大端写入文件而仿真平台按小端读动作和地址就会交换表现就是微程序疯狂乱跳。我习惯在文件开头固定写一行“00”再跟十六进制数据这样所有平台都能准确定位。5. 仿真整机跑起来从模块连接到单步出一个ADD5.1 在仿真平台里连接数据通路和控制总线搭数据通路时我建议分层操作。先放内存、PC、AR、DR、IR、AC、ALU这些核心模块不接控制器时用按钮手动给控制信号拍电验证每条通路是否可以手动传数据。等到手动模式下能手动完成“从内存读一个数到AC”再接过微程序控制器。关键连接点有四处PC输出到AR输入的路径内存数据线到IR和DR的路径DR到内存地址线或AR的路径ALU输出到AC输入的路径。其中PC到AR的连接最容易漏因为有的仿真版图里PC自带地址总线输出初学者会直接把PC接到内存地址端绕过AR。这样做不是不能用但会让微程序的PC→AR拍变成空操作整个架构的时序就乱套了。5.2 加载控制ROM和测试程序控制ROM用第三章脚本生成的urom.hex。加载时确认ROM位宽是16位或按“字”模式读取不要按字节读取。数据内存是8位宽直接加载我们的测试程序。测试程序放在低地址区数据放在0x30以后避免和指令重叠。一个最简单的四行程序足够完成验证内存地址机器码助记符0x100x20 0x30LDA 0x300x120x40 0x31ADD 0x310x140x30 0x32STA 0x320x160x70 0x10JMP 0x100x300x05数值50x310x03数值30x320x00结果预留这段程序从0x30读出5加上0x31里的3得到8写入0x32然后跳回0x10循环。数值都很小一眼就能看出结果对不对。5.3 单步时钟观察微地址序列仿真中不要直接启动连续时钟先把时钟改成手动单步模式。每跳一次时钟记录当前微地址。以ADD这条指令为例正常微地址序列应该是时钟拍微地址执行动作PC/AC/AR/DR变化10x00PC→ARAR0x1220x01M[AR]→IR, PCIR0x40, PC0x1330x02PC→ARAR0x1340x03M[AR]→DR, PCDR0x31, PC0x1450x40DR→ARAR0x3160x41M[AR]→DRDR0x0370x42AC←ALU_ADD, Z/C更新AC0x08, Z0, C080x00返回取指开始下一条指令进入STA这一步能一次性暴露前面几章讲的所有问题。如果拍数和微地址对不上不要改电路先回到微程序表确认程序流转是否合理。微程序控制器最大优势是“每拍做什么完全可预期”如果你发现某拍做了计划外的动作九成是上一条微指令的序控字段写错了。5.4 从单步到连续运行单步验证通过后把时钟切到连续模式跑100个周期左右检查0x32地址里的值是否稳定停在0x08。程序是无限循环因此内存会不断写入8属于正常现象。如果想更精确可以加一个周期计数器观察执行完一遍循环需要多少个时钟周期。我这边测得一轮循环约40拍左右具体数字取决于你使用的取指段和执行子程序结构。6. 最后一道验证习惯用微地址序列表给设计收尾课程设计交报告前我强烈建议补一张“宏指令→微地址序列”对照表。以ADD为例列出0x00、0x01、0x02、0x03、0x40、0x41、0x42再到0x00的完整路径。这个表不用做进报告但调试时它是判断电路是否正常工作的唯一标准。每次仿真卡住我都先拿着这个表一步步对微地址。如果微地址停在0x00不动说明取指段没跳走查MAP逻辑如果在0x41之后跳到0x00而不是0x42说明下址字段写错或序控字段把ALU操作提前终结了。另一个值得养成的习惯是给每个操作码单独写一条运行轨迹。JZ比JMP多一个条件分支我会专门构造一组“零标志成立”和“零标志不成立”的两个用例把期望的微地址轨迹分别写出来再在仿真平台里跑两遍。条件分支最怕“看起来差不多”因为Z1和Z0时微地址只差一格但电路行为完全不同。宁可花二十分钟把这些轨迹逐条验证也不要最后答辩时被老师随手一个测试向量问倒。最后再提一个容易被忽视的细节报告里的微程序表要和实际ROM文件保持完全一致。很多同学写完仿真就重新誊一遍表格结果两个版本对不上复盘时自己都看懵。我的习惯是直接用Python脚本生成的hex文件反推微程序表把脚本源文件贴在报告附录里这样从程序到电路全部能追溯。从那以后我每次做模型机课设都强制走一遍“指令集表→微程序表→ROM文件→单步轨迹记录”这条线。希望这部分经验能帮到你少踩几个我已经替你踩过的坑。本文还有配套的精品资源点击获取