简介:这份文华学院《计算机组成原理》课程设计文档,面向正在完成Proteus模型计算机设计与实现作业的本科生,提供一份可对照参考的个人作业范例。压缩包仅含1个doc文件,约2.79MB,内容为完整课程论文,涵盖需求与总体设计、模型机逻辑图、运算器与存储器电路及控制信号、整机层次化设计和软件设计。文中整理了74LS181运算器、6116存储器等模块的信号名称与有效电平,并记录时序分析、指令数据流向、微指令与微代码设计、编程调试过程及应得结果与实际结果对比,便于读者理解控制信号归纳、脉冲信号与电平信号区分、微程序控制器连接等关键环节。已有537人学习,适合需要搭建Proteus模型机、梳理课程设计报告结构或查漏补缺的计算机相关专业学生参考,但应结合自身方案独立完成。
1. 一台只会做加法的计算机,反而最考验你对时序的理解
在 Proteus 里把 6116 的 /WE 和 /CS 顺序接反,数据就是写不进去,灯不亮、数码管不动,你根本分不清是片子坏了、网线断了,还是电平给错了。计算机组成原理这门课最容易卡住的地方就在这里:教材上真值表会背、功能表会填,可真要把 74LS181、74LS273、6116 拼成一台能跑程序的机器,中间隔着几百根连线和一堆脉冲、电平的区别。这份课程设计做的就是这样一件事:用 Proteus 8.9/8.11 搭一台 8 位单总线模型计算机,自己定义指令、自己排微代码、自己拨开关对时序,最后跑通一段 IN/ADD/OUT 的程序,58H 加 58H 得到 D0H。它适合两类人:正被课设卡住、需要一份能对照着连线的同学,以及想把“CPU 到底怎么动起来”这件事从抽象补成具体画面的自学者。
2. 整机层次化拆分:五张子图、8 位单总线与端口连线
2.1 为什么先拆层次再连线
直接在顶层画一张包含全部器件的整机图,几乎是自找麻烦。这份设计走的是层次化路子:主电路只保留按钮开关、显示灯、时序部分、指令寄存器 IR1(74LS273)以及各子电路的方框,把运算器、寄存器组、指令计数器、存储器、控制器这五块各自做成一张子图(Proteus 里的子电路),顶层靠端口把信号接进去。好处很实在:调运算器的时候只看运算器那张图,噪声少、连线少,出错范围也小;等五个子图各自能动了,再往顶层一拼,问题基本只剩端口连错或者控制信号漏接。
单总线结构是这套设计的骨架。整机只有一组 8 位数据总线 D[0..7] 和一组 8 位地址总线 AB[0..7],任何两个部件之间传数据都必须“一个放、一个收”,同一时刻总线上只能有一个源。这条约束决定了后面所有微指令的写法:你要把 R0 送进 ALU,就得先把 R0 打开到总线、再把 ALU 暂存器的写允许打开,中间不能有任何第二个源抢总线,否则仿真里看到的就是一片 X 或者全 0。很多人第一次调不通,不是器件坏,是总线打架。
我曾经把“先拆子图”当成形式主义,觉得顶层一把画完更省事,结果一个 8 位加法算错,排查了整整两天,最后发现是子图里一根 /EDR2 没引出来。从那以后我坚持一个原则:顶层只允许出现端口和方框,任何器件都下沉到子图里。
2.2 五张子电路和它们的端口清单
端口命名最好一次定死,后面写微指令、拨开关全靠它。这份设计的端口整理出来大概是下面这样,我做了一点归类,方便你连线和对照:
| 子电路 | 主要端口信号 | 说明 |
|---|---|---|
| 运算器单元 | D2CK、D1CK、/EDR1、/EDR2、S3、S2、S1、S0、CN、M、/ALU-O、D[0..7] | 两片 74LS181 组 8 位 ALU,DR1/DR2 为两个暂存器 |
| 寄存器组单元 | SB、SA、/WR、RCK、/RD、D[0..7] | SA/SB 选读写寄存器,RCK 为写脉冲 |
| 指令计数器单元 | CLR、PCK、PC+1、ELP、PC-O、IR2CK、EIR2、IR2-O、D[0..7]、AB[0..7] | 两片 74LS193 级联成 8 位 PC,244 做三态输出,273 做 IR2 |
| 存储器单元 | /WM、EMCK、/RM、/CS、D[0..7]、AB[0..7] | 6116 静态 RAM,读写分离控制 |
| 控制器单元 | CLR、SETW、SETR、DT2、T4、MD[0..25]、MB[0..5] | 微指令寄存器输出控制字段与下地址 |
这张表看着朴素,实际是整个项目的“接口契约”。子图内部你怎么画都行,但端口名字一旦定成 /EDR1,顶层、微指令表、调试记录里就必须都叫 /EDR1,不要一会儿写 EDR1、一会儿写 DR1EN。血泪经验:名字不统一,最后对微代码的时候你会怀疑人生。
2.3 把所有控制信号归纳成一张表
题目要求“归纳此模型计算机的所有控制信号”,这一步其实是整个设计的分水岭。信号归纳清楚了,微指令字段就有了;归纳不清楚,后面全是补丁。按功能分,这批信号大致是四类:
第一类是数据源选择,比如 SW-BUS(把开关数据送上总线)、/ALU-O(ALU 结果送上总线)、PC-O(PC 送地址总线)、IR2-O(指令寄存器送总线)。第二类是数据目的写允许,比如 /EDR1、/EDR2、/WR、EIR2、ELP、MCS。第三类是功能选择,比如 S3~S0、M、CN 决定 74LS181 做什么运算,SA/SB 决定读写哪个通用寄存器。第四类是时序与脉冲,比如 D1CK、D2CK、RCK、PCK、EMCK,这些是上升沿有效的打脉冲。
这四类里,前两类在微指令里表现为电平位,第三类也是电平位,第四类是真正意义上的“脉冲”。区分这两者的意义在于:电平可以一直保持,你慢慢拨开关都没事;脉冲必须靠时序电路自动产生,因为人手拨开关根本拨不出一个足够窄的上升沿。这也是为什么这份设计必须有时序部分——T2、T4 两相时序,一个负责出脉冲、一个负责切换电平。
3. 运算器与存储器子电路:74LS181 和 6116 的控制信号怎么给
3.1 两片 74LS181 拼 8 位,S3~S0、M、CN 怎么配
74LS181 是 4 位 ALU,做 8 位就得两片级联:低 4 位那片的进位输出接到高 4 位那片的进位输入,两片的 S3~S0 和 M 并联,由同一组控制位驱动。这样一条微指令就能同时决定 8 位运算的功能。
功能选择的关键是这几位:S3~S0 选运算类型(这片子的选择输入是低有效,所以微指令里写 0 才表示“选中”),M 决定算术还是逻辑(高电平走逻辑运算,低电平走算术运算),CN 表示进位输入。常见组合我整理成下面这张表,注意极性,实际以你手上那份数据手册的功能表为准,仿真模型偶尔也会有出入:
| 运算 | S3 S2 S1 S0 | M | CN | 说明 |
|---|---|---|---|---|
| A 加 B | 1 0 0 1 | 0 | 1 | 课设里的 ADD 走这一组 |
| A 减 B | 0 1 1 0 | 0 | 1 | 结果带借位补偿,需要再看进位标志 |
| A 与 B | 1 0 1 1 | 1 | x | 逻辑运算时 CN 无关 |
| A 或 B | 1 1 1 0 | 1 | x | 同上 |
| A 异或 B | 0 1 1 0 | 1 | x | 逻辑异或 |
| A 取反 | 0 0 0 0 | 1 | x | 单目运算,B 端数据无所谓 |
这张表的价值在于:把 ADD 换成 SUB、AND、OR、XOR、COM,硬件一根线都不用动,只改微指令里那 6 个位。这也是这份课设天然留出的扩展口,最后一章会展开。
写微代码时我习惯用宏把控制位定义出来,避免直接写一串 0 和 1:
/* 微指令控制字段按位定义,位号与设计中的 MD 字段顺序一致 */ #define MD_SWBUS (1u << 25) /* 开关数据上总线 */ #define MD_EDR1 (1u << 24) /* 打开 DR1 写允许(电平) */ #define MD_EDR2 (1u << 23) /* 打开 DR2 写允许(电平) */ #define MD_M (1u << 22) /* ALU 方式:1 逻辑,0 算术 */ #define MD_S3 (1u << 21) /* ALU 功能选择,低有效 */ #define MD_ALUO (1u << 16) /* ALU 结果送总线,低有效 */ #define MD_MW (1u << 4) /* 存储器写允许,低有效 */ #define MD_MCS (1u << 3) /* 存储器片选,低有效 */ /* ADD R0,R1 的执行微指令:算术加法,结果送上总线,写回 R0 */ #define UCODE_ADD_EXEC (MD_S3 | MD_ALUO) /* 其余位按寄存器选择补齐 */这段代码不是拿来编译运行的,它的作用是让微指令表变得可读。用宏之后,一条微指令写出来是“打开了哪些源、打开了哪些写允许”,而不是0b0110...。参数上要特别注意有效电平:/ALU-O、/EDR1、/WR 这些都是低有效,宏里如果直接按“置 1 表示打开”写,最后填到 ROM 里就得整体取反,这一步非常容易翻车,建议宏名和实际位值严格对应仿真里的电平,宁可多写一行注释。
3.2 D1CK、D2CK 与 /EDR1、/EDR2 的配合
运算器这一块最容易被误解的组合就是“写允许 + 打脉冲”。/EDR1、/EDR2 是电平原语,它只表示“我允许数据进入 DR1 或 DR2”;真正把数据锁进去的是 D1CK、D2CK 这两个上升沿。只给电平不给脉冲,暂存器里永远是上一次的值,你在仿真里会看到 ALU 输出一直是老数据,怎么改 S3~S0 都不对。
所以 IN R0,#DATA1 这条指令要拆成两条微指令:第一条把开关数据送上总线(SW-BUS 有效)、打开 DR1 写允许(/EDR1 低)、同时选通目标寄存器,然后在 T2 相给出 D1CK 脉冲,数据落进暂存器;第二条把写允许关掉。原稿的调试步骤里专门有“再给 T2 一个脉冲关闭寄存器写端”这一步,就是在补第二拍。很多人觉得这一步多余,其实不然:写允许一直开着,后面任何一次总线上的数据抖动都会灌进寄存器,尤其是你手拨开关的时候,抖动是常态。
寄存器组那边同理,/WR 是写允许,RCK 是写脉冲,两个都得给。/RD 是读允许,只给电平就行,因为读是把寄存器内容打到总线上,不需要锁存。这种“写要脉冲、读只要电平”的不对称,是单总线结构里最典型的规律。
3.3 存储器:/CS、/WM、/RM、EMCK 的读写时序
6116 是 2K×8 的静态 RAM,11 根地址线,而这份设计里地址总线只引出了 AB[0..7] 八根,等于只用了 256 字节的空间。对课设来说够用,但心里要有数:这是个简化,不是片子只有这么大。
存储器相关信号四个:/CS 片选、/WM 写允许、/RM 读允许、EMCK 写入脉冲。写一个字节的正确顺序是:先把地址稳定在 AB[0..7] 上,再拉低 /CS 选中片子,然后拉低 /WM 并给出 EMCK 上升沿,数据在上升沿被锁进去,最后依次释放 /WM、/CS。顺序错了就写不进去,或者写进去一个错值。读的时候更简单:地址稳定、/CS 低、/RM 低,数据就出现在 D[0..7] 上,全程不需要脉冲。
这里有个细节值得单独说:/RM 和 /WM 绝对不能同时有效。仿真里两个都拉低,6116 模型的行为是不确定的,有的模型输出高阻,有的模型直接把总线拉成某个值,表现出来就是“程序跑到访存就乱”。所以微指令里这两位要互斥,写的时候读关掉,读的时候写关掉。我在微代码表里给每条访存指令都单独留了一条“关闭写/关闭读”的微指令,就是这个原因。
3.4 脉冲信号和电平信号:一张表分清
题目明确要求区分脉冲和电平,这不是文字游戏,它直接决定了你的调试方式。电平信号你可以用面板上的开关慢慢拨,拨完停下来看灯;脉冲信号必须由时序电路给,你手拨出来的“脉冲”宽度可能是几百毫秒,也可能带着抖动,逻辑上不可靠。
| 信号 | 类型 | 有效条件 | 调试方式 |
|---|---|---|---|
| SW-BUS、/ALU-O、PC-O、IR2-O | 电平 | 低有效(选通类) | 拨开关保持即可 |
| /EDR1、/EDR2、/WR、EIR2、ELP | 电平 | 低有效(写允许) | 拨开关保持 |
| S3~S0、M、CN、SA、SB | 电平 | 见功能表 | 拨开关保持 |
| /CS、/WM、/RM | 电平 | 低有效 | 拨开关保持,注意互斥 |
| D1CK、D2CK、RCK、PCK、EMCK | 脉冲 | 上升沿 | 由 T2 时序自动产生 |
| PC+1、CLR | 脉冲/电平 | 上升沿 / 高有效 | PC+1 走时序,CLR 手动 |
把这张表贴在屏幕边上,调试的时候少走很多弯路。凡是“拨了开关没反应”的,先看这个信号在表里是电平还是脉冲;如果是脉冲,那拨开关本来就不该有反应。
4. 微程序控制器:从汇编指令到微指令码点
4.1 微指令格式:26 位控制字段加 6 位下地址
控制器这块的核心是一张微指令表。这份设计的微指令字长 32 位,高 26 位是控制字段 MD[0..25],低 6 位是下地址字段 MB[0..5],所以微程序最多能放 64 条微指令,足够跑通课设里的几条指令。控制字段的排布大致是:MD25 起是 SW-BUS、EDR1、EDR2、M、S3、S2、S1、S0、CN、ALU-O,再往后是 SB、SA、WR、读寄存器、PC+1、ELP、PC-O、EIR2、IR2-O、MW、MCS、EIR1、P 等。不同版本的表格位号可能差一两位,动手前先拿自己的表对一遍,不要照抄别人的位号。
下地址字段的作用是“这条微指令执行完,下一条去哪儿”。它有两种典型写法:顺序执行时直接写下一条的地址,遇到分支(比如指令译码后跳转到对应指令的微程序入口)时由指令操作码拼出地址。课设规模不大,一般用后者:IR 里的操作码经译码后作为微地址的高几位,低位由微程序计数器给,这样一条机器指令就能定位到自己那段微程序。
微指令的存储介质在 Proteus 里通常用 EEPROM 模型(比如 2816、28C16 这类),把 32 位拆成 4 个字节按地址烧进去。原稿里 CM1~CM4 用DB伪指令一行写 8 位,一条微指令正好四行,这个写法不是随便定的,它就是 ROM 的内容格式。
4.2 逐条拆:IN R0,#DATA1 为什么要两条微指令
拿最简单的输入指令开刀。IN R0,#DATA1干的事是:把开关上的 8 位数据读进 R0。拆成微操作是:
- 开关数据送上总线(SW-BUS 有效),选通 R0(SA/SB 指向 R0),打开寄存器写允许(/WR 低);
- 打开 ALU 暂存器 DR1 的写允许(/EDR1 低),准备后续运算;
- T2 相给 D1CK 或 RCK 脉冲,把总线数据锁进目标;
- 关掉写允许,释放总线。
所以它至少占两条微指令:一条“开写 + 打脉冲”,一条“关写”。原稿的调试步骤第 1、2 步就是这两条:“给 T2 一个脉冲信号并且给 T4 脉冲,将输入的数据打入寄存器 R0 中”“给 T2 一个脉冲关闭寄存器写端”。IN R1,#DATA2完全一样,只是把 SA/SB 换成 R1。
这里有个容易忽略的点:第一条微指令里的 SW-BUS 是电平,必须在脉冲来之前就稳定。如果你让 SW-BUS 和脉冲在同一拍产生,仿真里可能采到上一个值。稳妥的做法是让电平在 T4 相切换,脉冲在 T2 相产生,两相错开。
4.3 ADD R0,R1 的数据流向与微代码
加法是这份设计的重头戏,也是微指令条数最多的一条。数据流向是:R0 和 R1 的内容分别送进 ALU 的两个暂存器 DR1、DR2,ALU 做加法,结果经总线写回 R0。原稿记录的过程拆得很细:
| 步骤 | 动作 | 关键控制信号 |
|---|---|---|
| 1 | 关总线,选 R0,打开 DR1 写 | SW-BUS 无效,SB/SA 选 R0,/EDR1 有效 |
| 2 | 关 DR1 写 | /EDR1 无效 |
| 3 | 选 R1,打开 DR2 写 | SB/SA 选 R1,/EDR2 有效 |
| 4 | 关 DR2 写 | /EDR2 无效 |
| 5 | ALU 执行加法,结果上总线,写回 R0 | S3~S0 = 1001,M = 0,CN = 1,/ALU-O 有效,/WR 有效 |
| 6 | 关寄存器写 | /WR 无效 |
对应到汇编层面,程序长这样:
; 两数相加并输出,DATA1/DATA2 由开关给出 IN R0, #DATA1 ; 读第一个操作数到 R0 IN R1, #DATA2 ; 读第二个操作数到 R1 ADD R0, R1 ; R0 = R0 + R1,结果写回 R0 OUT R0 ; 把 R0 内容送显示灯 HLT ; 停机这段程序的参数说明很短:DATA1、DATA2 是开关输入的立即数,R0、R1 是通用寄存器,ADD 走的是 74LS181 的算术加法模式。注释里我特意标了每个操作数从哪来、到哪去,因为调试的时候你盯着灯看,唯一能判断对错的就是“这一步总线上应该是谁”。
第二条程序用到了存储器:
IN R0, #DATA1 ; 读第一个数 STA (01H), R0 ; 存入存储单元 01H IN R0, #DATA2 ; 读第二个数 MOV R1, (01H) ; 从 01H 取回第一个数 ADD R0, R1 ; 相加 OUT R0 ; 输出结果这条程序多了 STA 和 MOV,等于多验证了存储器的写和读两条通路,是整机联调时最有价值的一段测试。跑通它,说明数据总线、地址总线、存储器读写时序、微地址跳转都是对的。
4.4 微代码数组的落地
把微指令表写成数组,便于对照和生成 ROM 内容。原稿里的写法是一条微指令四行DB,我这里用 C 数组表达同样的结构:
/* 一条微指令 32 位:高 26 位控制字段,低 6 位下地址 */ typedef unsigned int u32; const u32 ucode_add[] = { /* 取 R0 到 DR1:选 R0、开 DR1 写,下地址指向下一条 */ (MD_SWBUS_OFF | MD_EDR1 | SA_R0 | SB_R0 | NEXT(0x08)), /* 关 DR1 写 */ (MD_SWBUS_OFF | SA_R0 | SB_R0 | NEXT(0x09)), /* 取 R1 到 DR2 */ (MD_EDR2 | SA_R1 | SB_R1 | NEXT(0x0A)), /* 关 DR2 写 */ (SA_R1 | SB_R1 | NEXT(0x0B)), /* ALU 加法,结果上总线并写回 R0 */ (MD_S3 | MD_ALUO | MD_WR | SA_R0 | SB_R0 | NEXT(0x0C)), /* 关写允许,微程序结束 */ (SA_R0 | SB_R0 | NEXT(0x00)), };代码里的NEXT()就是 MB 下地址字段,SA_R0、SB_R0是寄存器选择位的组合,MD_*是上一章定义的字段宏。参数上要盯住两件事:一是每条微指令的下地址必须指向正确的下一条,中间漏一条整段就跑飞;二是最后一条的下地址要么回到取指入口,要么指向停机循环,不能随手写 0 了事——0 通常就是取指入口,等于让程序从头再跑一遍,表现出来就是“结果对了但灯一直闪”。
5. 整机联调与常见问题排查
5.1 时序分配:T2 管打,T4 管选
这份设计只用了两相时序:T2 和 T4。我的理解是,T4 负责切换电平类控制信号(选通、写允许、ALU 功能位),T2 负责产生打入脉冲(D1CK、D2CK、RCK、PCK、EMCK)。两相错开的好处是:电平先稳定,脉冲后到达,采样不会踩在跳变沿上。
节拍安排上,一条微指令占一个或两个时钟周期。简单指令(比如关写允许)一个周期就够;访存和 ALU 运算这类需要“先建立后打入”的,就给两个周期。原稿的调试记录里反复出现“给 T4 脉冲打开……再给 T2 脉冲打入……”,就是这套约定的直接体现。
真正上整机之后,T2、T4 由时序电路自动产生,你只需要按“启动”按钮。手动拨开关的阶段是把时序拆开、一步一步验证,两者不能混着来。
5.2 用开关模拟控制信号的十三步操作
手动调试阶段,把时序电路断开,用拨动开关逐个给控制信号,按原稿记录的顺序走一遍 ADD:
- 给 T2 脉冲、T4 脉冲,把开关数据打入 R0;
- 给 T2 脉冲,关闭寄存器写端;
- 给 T2、T4 脉冲,把第二个数据打入 R1;
- 给 T2 脉冲,关闭寄存器写端;
- 给 T2 脉冲,关闭总线(SW-BUS 无效);
- 给 T4 脉冲,打开寄存器读,默认先读 R0;
- 给 T4 脉冲打开运算器写,给 T2 脉冲把 R0 打入 ALU 暂存器 DR1;
- 给 T4 脉冲,关闭运算器写端;
- 给 T4 脉冲打开运算器写、改选 R1,给 T2 脉冲把 R1 打入 DR2;
- 给 T4 脉冲,关闭运算器写和寄存器读;
- 给 T4 脉冲,打开 ALU 输出,读出运算结果;
- 给 T4 脉冲打开寄存器写,给 T2 脉冲把结果写回 R0;
- 给 T4 脉冲关闭寄存器写,再打开显示,读 R0 内容。
这一串操作看着啰嗦,但它把“一条指令在硬件上到底发生了几件事”摊开给你看。做完一遍,你对微指令表的理解会完全不一样。结果对上了:58H 加 58H,显示 D0H,说明运算器和总线通路没问题。
5.3 常见问题排查:五条踩坑记录
现象一:数据打不进寄存器,灯一直不亮。原因:只给了 /EDR1 或 /WR 这样的写允许电平,没给 D1CK、RCK 脉冲。允许是“门开着”,脉冲才是“人进去”。 解决:用示波器或者 Proteus 的逻辑探针看脉冲脚有没有上升沿;确认 T2 相确实产生了脉冲,而不是被 T4 的电平盖住。
现象二:加法结果总是差 1,或者全 F。原因:CN 的极性搞反了。原设计里 CN 记为高电平表示有进位输入,而 74LS181 本身的进位输入是低有效,中间如果没加反相,就等于一直在借位。 解决:按你手上片子的功能表确认极性,在子图里加一级反相,或者直接在微指令里把这一位取反。这个坑很典型,改完记得把减法也重测一遍。
现象三:写进存储器的数读出来不对。原因:/WM 和 /RM 同时有效,或者地址还没稳定就给了 EMCK 上升沿。 解决:写时序严格按“地址 → /CS → /WM → EMCK 上升沿 → 释放”走,读写允许互斥。仿真里可以把地址总线加个探针,确认上升沿那一刻地址是对的。
现象四:程序跑飞,PC 乱跳。原因:PC+1 和 ELP 同时有效。ELP 是让 PC 装入新值,PC+1 是让计数器加一,两个一起来,装进去的值下一秒就被加了 1。 解决:这两位在微指令里必须互斥,跳转指令用 ELP,顺序执行用 PC+1,别图省事一起给。
现象五:微代码烧进去,机器不执行。原因:字节序或位序反了。Proteus 的 ROM 模型按字节寻址,一条 32 位微指令占四个地址,你把高字节放低地址还是低字节放低地址,结果完全不同。 解决:固定一种约定写进注释,用脚本统一生成 hex,别手工一行行敲。手工敲过一次 64 条微指令之后,我就再也没手敲过。
6. 二次开发:把加法机扩成一台能算加减与逻辑的机器
6.1 改指令集:只动 S3~S0 和 M 位
这套设计最舒服的地方在于,扩展运算能力几乎不用动硬件。ADD、SUB、AND、OR、XOR、COM 的差别全在 74LS181 的 6 个功能位上,你在微指令表里给每种运算各开一段微程序,译码器把操作码映射到不同的微地址入口,硬件一根线都不用改。原稿在 ADD 后面特意注了(SUB、AND、OR、XOR、COM),其实已经把这个扩展口留出来了。
具体做法:先扩充操作码字段,比如原来 4 位操作码只用了 3 个编码,现在把 SUB=0100、AND=0101、OR=0110、XOR=0111、COM=1000 填进去;再在微指令表里复制 ADD 那段六条微指令,只改第 5 条的 S3~S0 和 M;最后把译码器的输出接到微地址高位。验证方法很简单:拿同一组数,先算 ADD 记下结果,再算 SUB,两个结果应该满足 A+B 与 A-B 的关系;AND、OR、XOR 用 0FH 和 0F0H 这种边界值试,能一眼看出对不对。
6.2 微代码自动化:一个几十行的脚本
手工维护微代码表是二次开发最大的阻力。我一般会写个小脚本,把“字段名 + 位宽”的描述转成 32 位十六进制,再输出成 Proteus 能直接加载的 hex:
# 把微指令字段拼成 32 位字,并按字节输出 hex FIELDS = [("SWBUS", 1), ("EDR1", 1), ("EDR2", 1), ("M", 1), ("S3", 1), ("S2", 1), ("S1", 1), ("S0", 1), ("CN", 1), ("ALUO", 1)] def pack(bits: dict, next_addr: int) -> int: word = 0 # 按字段顺序从高位往低位拼,缺省补 0 for name, width in FIELDS: word = (word << width) | (bits.get(name, 0) & ((1 << width) - 1)) word = (word << 6) | (next_addr & 0x3F) # 低 6 位是下地址 return word def to_hex(words): out = [] for w in words: # 一条微指令 4 字节,高位在前,与 ROM 地址递增方向一致 out.append(":".join(f"{(w >> s) & 0xFF:02X}" for s in (24, 16, 8, 0))) return out ucode = [pack({"EDR1": 1, "S3": 1}, 0x08), pack({"ALUO": 1}, 0x00)] print("\n".join(to_hex(ucode)))脚本的输入是字段名到 0/1 的映射,输出是每行四字节的十六进制。参数上要注意两点:FIELDS的顺序必须和微指令表的位序严格一致,错一位整张表就废了;下地址只占 6 位,超过 63 的地址会被截断,微程序写太长就会绕回开头,表现出来是“跑着跑着又从头开始”。这类问题在仿真里极难看出来,用脚本生成至少能保证格式一致。
我现在的习惯是:微指令表永远只维护字段描述,不维护二进制。每次改指令集,改完描述跑一次脚本,生成的 hex 直接拖进 Proteus 的 ROM 模型,再对着显示灯核一遍结果。从那以后我每次搭总线型电路,都先把所有控制信号列成一张“脉冲/电平”两列表,再动手连线——省下来的时间远比列表本身多。希望帮到你。
本文还有配套的精品资源,点击获取