简介:这份华中科技大学计算机学院课程实验报告围绕Logisim平台上的16位自动运算器展开,面向数字电路、计算机组成原理课程的学习者与实验备考学生,帮助其完成从ALU设计到自动累加电路搭建的完整实践。报告共1个doc文件,压缩包约341KB,正文按实验目的、环境、内容、方案设计、过程调试、总结心得与参考文献组织,重点记录16位运算器的加减乘除、逻辑与移位运算、状态字判断,以及由RAM、寄存器、计数器构成的时钟驱动累加电路:自动累加RAM 0—15单元并将中间结果回存16—31单元,关键节点以十进制探测显示,结果用十六进制数码管输出。报告还附有运算器功能表、自动运算电路封装思路与故障调试分析,便于对照复现和排查时序、控制信号问题。资源已有4049人学习,可作为课程实验参考、电路复现与排错对照材料。
1. 需求先立住:一个 16 位运算器为什么要配一条自动累加通路
做 logisim 自动运算器这个题目,前面多半很顺,卡住基本都在 RAM 那几根引脚上。16 位运算器本身不新鲜,加减乘除、与或非异或、四种移位,平台上现成的运算部件拖出来把位宽设成 16 就能跑;真正让人熬到后半夜的是“自动”这两个字——电路要在时钟驱动下自己把 RAM 0~15 号单元加完,每加一次就把中间结果写回 16~31 号单元,全程不许人插手。这套方案围绕这个交付目标拆成三块:一块能独立测试的 16 位运算器、一条读地址与写地址错开的数据通路、一套到点自己停下来的时钟闸门。适合正在做计算机组成原理课程设计、需要交一份能演示能答辩的报告的人,也适合手上有一堆零散电路、想理出一条完整数据通路的人。
2. 总体架构设计:运算器、地址通路与时钟闸门的三段拆分
2.1 从功能表倒推模块边界
很多人拿到题目第一反应是打开 Logisim 就开始拖线,拖到一半发现运算器和 RAM 缠在一起,改一个地方要拆半个电路。我的习惯是先不动手,把功能表抄一遍。题目给的功能表其实是一份已经写好的需求清单:MF 从 0000 到 1011 一共十二种运算,加法、减法、乘法、除法、逻辑与、逻辑或、逻辑非、逻辑异或、逻辑左移、逻辑右移、算术左移、算术右移。
这十二种按“结果是怎么来的”分组,边界立刻就出来了。算术与、或、非、异或是纯组合逻辑,一个门就出结果;加减乘除走运算部件,还额外产生进位和溢出;四种移位走的是位线重排,跟数值运算没有半点关系。三组结果最后汇到一个多路选择器上,由 MF 决定谁出线。运算器封装好之后对外只有 A、B 两个数据口、一组 MF 选择线、一个结果口、一组状态位,内部怎么实现主电路完全不用管。
这一步想清楚,后面所有调试都能拆开做。运算器单独能测,自动累加电路单独能测,出问题时先判断是运算器算错还是通路接错,不用面对一坨线团靠猜。我一般会把功能表直接画在图纸空白处,每接完一组就对照着勾掉一行,比闷头连完再统一试要省时间得多。
2.2 数据通路、地址通路、控制通路的三段切分
自动运算电路里最容易被忽略的是“地址也走一条通路”。多数人只盯着数据怎么从 RAM 出来、怎么算、怎么回去,地址从哪来往往临时拉一根线过去,结果读到第几个单元、写到第几个单元全靠运气。
我一般把整张图横着切三刀,从上到下依次是数据通路、地址通路、控制通路。数据通路是:RAM 的数据输出进运算器 A 口,运算器结果一方面回送到 RAM 的数据输入,另一方面引到数码管。地址通路是:计数器给出读地址,同一个计数值加一个常数偏移后给出写地址,两条地址线分别接到 RAM 的地址脚。控制通路是:主时钟、半周期时钟、加载使能、存储使能、自停信号、显示选择、总清零,全部集中到图的一侧,不散落在数据线中间。
这么切的好处是接线时不会出现“这根线到底算数据还是算控制”的犹豫。Logisim 里隧道标签长得都一样,一旦把地址线和数据线混在一个区域,颜色再多也分不清。我给自己的规矩是:数据线统一用绿色系,地址线用蓝色系,控制线用红色系,右边一栏专门放所有控制信号的产生电路,需要往左边引的一律走隧道。
2.3 位宽、时钟与显示约定
位宽错一位,整个电路就是玄学。Logisim 对位宽不一致的连接会给红叉,但通过隧道和分离器绕一圈之后红叉就没了,错误被藏起来,跑起来数值就是不对。开工前把下面这张表定死,后面每个模块照着填,能省掉大量“明明逻辑对但数不对”的排查。
| 信号 | 位宽 | 显示进制 | 说明 |
|---|---|---|---|
| A、B | 16 | 十六进制 | 运算器两个操作数 |
| MF | 4 | 二进制 | 功能选择,实际用到 0000~1011 |
| F | 16 | 十六进制 | 运算结果,四个数码管显示 |
| 读地址 | 5 | 十进制 | 计数器输出,0~15 有效 |
| 写地址 | 5 | 十进制 | 读地址加偏移,16~31 有效 |
| RAM 数据 | 16 | 十进制 | 探测器观察中间结果 |
| 关键探测点 | 16 | 十进制 | 主电路最上面一行统一引出 |
显示规范在题目里其实写得很明确:主电路最上面一行把关键点用探测加隧道的方式引出来,用十进制显示便于检查,运算器结果用十六进制数码管。这一条别嫌麻烦,它决定了答辩时你能不能在三秒内指出“累加到第 7 个单元时结果是 112”。十进制看累加过程直观,十六进制看最终结果的位模式直观,两种进制各管一段,别混用。
3. 16 位运算器实现:十二种运算的并行组织与状态字生成
3.1 功能表编码:四位 MF 怎么落到多路选择器
最省事的实现不是“按 MF 顺序依次执行”,而是把十二种运算全部并行算出来,再用一个十六选一的选择器挑。Logisim 是组合逻辑仿真,没有指令周期的概念,十二个运算部件同时工作只是多占一点画布,不会像真实处理器那样有功耗和延迟压力。这个思路的好处是控制逻辑极简:MF 直接当选择端,中间不需要任何状态机。
多路选择器的数据位宽一定要设成 16,数据端的个数按 2 的幂选 16 个,把十二种结果接进去,剩下四个空位接地。空位接地不是可有可无的细节,Logisim 的选择器在未接数据端时会给出不定态,不定态一旦流到数码管上就是一片红色,排查起来很费劲。
// 运算器行为级参考模型,用于和 Logisim 电路对拍 module alu16( input [15:0] a, b, input [3:0] mf, output reg [15:0] f, output reg c, v, z // 进位、溢出、零标志 ); reg [16:0] wide; // 借一位宽度捕捉加法进位 always @(*) begin c = 1'b0; v = 1'b0; case (mf) 4'b0000: begin wide = a + b; f = wide[15:0]; c = wide[16]; end 4'b0001: begin wide = a - b; f = wide[15:0]; c = wide[16]; end 4'b0010: begin wide = a * b; f = wide[15:0]; c = |wide[31:16]; end 4'b0011: begin f = a / b; c = (a % b) != 0; end // 余数非零即有溢出 4'b0100: f = a & b; 4'b0101: f = a | b; 4'b0110: f = ~a; 4'b0111: f = a ^ b; 4'b1000: f = a << 1; // 逻辑左移 4'b1001: f = a >> 1; // 逻辑右移,高位补 0 4'b1010: f = a <<< 1; // 算术左移,与逻辑左移同 4'b1011: f = $signed(a) >>> 1; // 算术右移,补符号位 default: f = 16'h0000; endcase z = (f == 16'h0000); // 有符号溢出:两操作数同号而结果异号 if (mf == 4'b0000 || mf == 4'b0001) v = (a[15] == b[15]) && (f[15] != a[15]); end endmodule这段参考模型不是让你抄进 Logisim,它的用处是“对拍”:电路跑出来的结果和模型跑出来的结果逐条比对,十二条功能全过了再封装。里面几个细节值得对照着看。加法用 17 位宽去接,第 16 位就是进位,这比单独再搭一个进位链稳。乘法判断溢出看高 16 位有没有非零,除法判断溢出看余数是不是非零,这正是题目里说的“乘法有进位或除法存在余数时与零比较”。算术右移必须用带符号的右移,也就是高位补原符号位,用逻辑右移去凑会在负数上翻车。
3.2 移位运算:用分离器错位拼接,别指望现成部件
四种移位里真正需要自己搭线的是三种右移,左移反而好办。逻辑左移是把 16 根线整体往高位方向挪一位,最低位补常数 0,最高位被挤出去丢掉——在真实处理器里被挤出去的那一位就是移位进位,可以顺手接到进位标志上。算术左移和逻辑左移在位模式上完全一样,题目里也把这两条并列写着,功能表里区分它们更多是为了语义完整。
右移的做法是把分离器设成 16 位输入、上下两段输出,中间错开一位重新拼。逻辑右移把最高位补 0,算术右移把最高位补原数的第 15 位。这个“补原符号位”的动作必须用一个隧道从输入端口直接引过来,不能在输出端接反馈线,否则会形成组合环,Logisim 会直接报“检测到振荡”。
分离器的位选择要一格一格点,很容易点错。我的做法是搭完先用一组固定的测试值过一遍,比如输入8001,看逻辑右移是不是4000、算术右移是不是C000。这两个值一对,符号位补得对不对、错位错得准不准,立刻就暴露了。这一步花五分钟,能省掉后面自动累加跑完发现结果整体偏一位、却不知道是运算器还是通路的几小时。
3.3 程序状态字:进位、溢出、零的三路判定与切换
状态字是运算器里最容易被糊弄过去的部分。结果算对了,标志位随便接一根线,测试时看着像对的,一换运算类型就露馅。原因是不同运算的“进位”和“溢出”含义根本不是一回事。
加法的进位来自加法器的进位输出,这最直接。减法的进位其实是借位,把减法器的借位输出直接当进位用,语义上是“够减为 0、不够减为 1”,写报告时要注明是借位而不是进位,否则答辩被问一句就卡住。乘法的溢出看高位部分有没有非零,除法的溢出看余数有没有非零,这两个判据合起来就是一个“与零比较”的比较器加一个多路选择器——题目里说的就是这个做法:把乘法的进位项和除法的余数项一起送进比较器,与零比完再按 MF 选出来。
有符号溢出则是另一套判据:两个操作数符号相同而结果符号不同,就是溢出。用两个异或门加一个与门就能拼出来,不需要比较器。零标志最简单,16 位结果按位或非,全零输出 1。三路判据做好之后再套一层多路选择器,MF 走到哪一组就放哪一组的状态字出去。
这一块我建议在封装前单独做一张测试表,把典型值列出来跑一遍:正数加正数出负数看溢出位、FFFF加0001看进位和零标志、负数右移看符号位是否保留。表格填满,状态字这块就没什么可翻车的了。
3.4 封装:引脚命名、位宽与调试引出
运算器封装的引脚清单要一次定死,因为自动运算电路要按这个接口接线,改一次封装等于主电路重接一遍。我一般留这些:A、B 两个 16 位输入,MF 一个 4 位输入,F 一个 16 位输出,C、V、Z 三个 1 位输出。顺序按“数据进、控制进、数据出、状态出”排,引脚标签直接写英文缩写,别用中文,Logisim 的标签在缩放后会糊成一片。
封装时有个小技巧:把内部的关键节点用隧道引到封装的外层,这些隧道在子电路里看得见、在封装后的符号上不占用引脚。这样自动运算电路里如果怀疑运算器有问题,双击进去就能直接看到输入输出,不用一层层剥。题目里提到用探测和隧道结合引出的做法,在封装这一层同样适用。
4. 自动运算电路实现:双计数器、RAM 双时钟与到点自停
4.1 双计数器方案:读地址与写地址的 +16 偏移
累加要求写得很清楚:0~15 号单元的数据依次累加,第 i 次累加的中间结果写回第 16+i 号单元。读地址是 0、1、2……15,写地址是 16、17、18……31,两条地址线永远差 16。
最直白的做法是两个计数器受同一个时钟触发,第一个计数器输出直接当读地址,第二个计数器输出加一个常数当写地址。这里有个容易被追问的细节:报告里写的偏移是 15,而实际需要的偏移是 16。差的那一位在加法器的进位输入端——把常数设成 15、同时把进位输入 Cin 置成 1,等效就是加 16。这个接法在 Logisim 里很常见,因为它的加法器常数可以填任意值,而进位输入脚经常被空着;空着是 0,接上常量 1 就多进一位。答辩时被问到“为什么是 15”,能答出这一层,比含糊地说“差不多”强得多。
也用一个计数器 0~31、再把高低位拆开来产生两条地址的做法,同样能跑通。但它的问题是两个地址没法独立清零,调试时想看“只读不写”的状态就做不到了。双计数器多一个元件,换来的可观测性值这个成本。
4.2 RAM 引脚接法:加载与存储分离配半周期时钟
RAM 的数据接口在 Logisim 里有三种模式:一个数据脚既读又写、分离的数据输入输出脚、分离的加载与存储脚。第一种最省线,但读写共用一根数据线,回存的时候输出还挂在线上,容易打架;第二种把 din 和 dout 分开,读出来的数据不会和要写的数据混;第三种在此基础上再把加载使能和存储使能也分开,可以精确控制“这一拍只读”“这一拍只写”。
题目里选的是第三种,这是有道理的:自动累加的过程是“读第 i 个、算、写第 16+i 个”,读和写在同一拍里发生,如果只用一个时钟去触发,就会出现加载和存储抢同一拍的情况,表现出来就是只加载不存储,或者存进去的不是累加结果。解法是给 RAM 单独配一个时钟,周期是主时钟的一半,让加载和存储在半个主周期内各自刷新一次。
具体接法上,主时钟接两个计数器,RAM 的时钟脚接那个高频时钟。加载使能和存储使能分别由主时钟的两相派生:主时钟为低的那半拍让加载有效,RAM 把当前地址的内容送到数据输出;主时钟为高的那半拍让存储有效,RAM 在边沿上把数据输入端的内容写进写地址。这样一拍之内先读后写,读到的永远是上一次存进去的累加值,不会读到正在写的这一笔。
4.3 到点自停:比较器加或门的时钟闸门
计数器累加到 16 就必须停下来,否则会继续往后读,把 16~31 号单元里刚存的中间结果又当成新数据加一遍,最终结果全错。题目给的方案是用比较器:计数器输出和常数 16 比较,相等时输出 1,这个 1 和时钟一起送进或门,或门的输出当时钟用。
或门的语义要想清楚:只要有一路是 1,输出就恒为 1,时钟就被“焊死”在高电平上,再没有下降沿,计数器和寄存器都不再动作,电路停住。这个做法比用与门优雅,与门是把时钟掐断在 0,某些模块对持续低电平的处理和持续高电平不一样;或门是把时钟钉在 1,效果更确定。
一开始用一堆逻辑门去凑“计到 16”的判据是很常见的弯路,因为 16 是二进制10000,用与门或非门拼要五根线全对,少接一根就变成计到 8 就停或者根本不停。比较器一个元件搞定,还自带大于小于的输出,将来想改成“累加到 20 停”只改一个常数。
自停信号还有一个隐患:清零的瞬间计数器回到 0,比较器输出从 1 跳回 0,或门重新放出时钟,电路又自己跑起来。如果封装上要留一个清零按钮,这一点必须处理,通常的做法是把自停信号用一个触发器锁存住,清零时一起复位。
4.4 结果回存与显示切换:什么时候只显示最终值
运行过程中数码管上数字一直在跳,跳的是当前累加值。跳到第 16 拍停下时,数码管上停住的应该是最终结果。如果直接把累加值接到数码管,停下来那一刻显示的确实也是最终值,看起来没问题。但一旦有人中途按了清零、或者想观察中途某个时刻的值,就会分不清“这是过程值还是终值”。
题目的做法是加一个二路选择器:自停信号作为选择端,没停的时候数码管显示实时累加值,停下来之后切到锁存的最终结果。这样终值一旦产生就被固定住,中途再怎么折腾时钟显示也不会变。四个十六进制数码管拼成一个 16 位显示,高位在前低位在后,别接反——接反了数值看着像对但其实每位都错位,最容易在答辩时被一眼看穿。
对应的 RAM 内容检查也要做:跑完之后打开 RAM 看 16~31 号单元,第 16 号应该是第一个数和第二个数之和,第 17 号是前三个数之和,依次类推。这个“前缀和”的规律是验证回存正确性最有力的证据,比看数码管终值可靠得多。
4.5 封装与整体清零:把调试口留在上层
电路跑通之后封装成一个符号,对外只留时钟输入和清零按钮,数码管作为输出显示。所有寄存器和计数器的复位脚在内部连在一起,接同一个清零信号,按下按钮全部回到初值。这一步不难,容易出问题的是“封装完就看不见内部了”。
我的处理是把探测点分成两类。一类是必须暴露的,比如累加中间结果、读地址、写地址,用输出引脚引到封装外层;另一类只在调试时看,用隧道标签留在子电路内部,双击进去就能看到。题目里提到的“活用探测器和输入引脚”,说的就是这个思路——电路没有真正的数据输入,所有输入都来自 RAM 初始值和常数,那就靠探测器和输出引脚把内部状态“引”出来看。
封装完先别急着演示,退到最外层按一次清零,看数码管是不是归零、计数器是不是回到 0、自停信号是不是复位。这三样都对了,再导入数据跑一遍完整流程。
5. 避坑与排查:五处最容易翻车的地方
5.1 只加载不存储,或者存进去的不是累加结果
现象是跑完一遍打开 RAM,0~15 号单元的数据还在,16~31 号单元要么全是初始值,要么存的是原始数据而不是前缀和。
原因基本都在 RAM 的使能和时钟上。加载和存储共用一个时钟边沿时,读动作和写动作挤在同一拍,写进去的可能是还没来得及运算的旧值,也可能是运算器输出的中间态。另一种情况是存储使能根本没接,RAM 一直处于只读状态,看起来“运行正常”,其实一笔都没写进去。
解决办法是把加载和存储分开控制,用两相时钟或者一个高频时钟配合两路使能,保证一拍之内先完成读、后完成写。查的时候不要看数码管,直接打开 RAM 看内容,16 号单元是不是等于 0 号加 1 号,这一条对上了,写通路就是通的。
5.2 累加值整体错位一拍
现象是最终结果的数值看着“眼熟但不对”,往往差第一个数或者多算了一遍最后一个数,RAM 里 16~31 号单元的内容整体往后挪了一位。
原因是计数器、运算器和 RAM 三者的动作不在同一拍上。计数器加一之后,RAM 的地址变了,但运算器用的还是上一个地址读出来的数据;或者反过来,运算器算完了,写地址已经先加过一了。这种错拍在组合逻辑仿真里不会报错,只会安安静静地算错。
解决办法是画一张时序表,把“第 n 拍:读地址 = n,运算器输入 = 第 n 个数据,写地址 = 16+n,写入值 = 前 n 项的累加和”逐拍写清楚,然后对着表里的地址去查 RAM。错拍问题只要把表列出来,通常一眼就能看出是哪一级的时钟接反了。
5.3 溢出标志在乘除运算时乱跳
现象是加减法时溢出位看着正常,一切到乘法或除法,明明结果没超范围,溢出位却是 1。
原因是所有运算共用了一套溢出判据。加减法的有符号溢出判据是“同号相加得异号”,乘法除法的溢出判据是“高位非零”或“余数非零”,两套判据的物理意义完全不同,硬接在一起必然打架。
解决办法是每种判据单独生成,再用 MF 驱动的多路选择器切换。乘法把高 16 位或起来,除法把余数与零比较,加减法用符号位异或,三路并排接进选择器。封装前拿几组边界值过一遍:7FFF乘 2、FFFF除以 2、8000加8000,三组结果的状态位都对上,才算过关。
5.4 封装之后发现没有可操作的输入
现象是子电路里跑得好好的,封装成符号放到主电路上,发现除了时钟没别的能按,想看某个中间值也没地方看。
原因是设计时把一切都当成内部自动流程,没有预留观测接口。自动运算电路确实不需要数据输入,但“不需要输入”不等于“不需要观测”。
解决办法是封装前把关键节点列一张清单,逐个决定是引成输出引脚还是留成内部隧道。累加中间结果、读地址、写地址、自停信号,这四个至少要有两个能在外层看到。另外把清零做成一个显式的按钮引脚,演示时按一下就能重跑,比关掉重开仿真体面得多。
5.5 探测器读数和数码管对不上
现象是探测器显示累加值到了 500 多,数码管上却是另一个数,两边差得很离谱。
原因是进制和位宽没对齐。探测器默认可能是二进制或者位数不够,16 位数只显示低 8 位,高位被截掉;数码管如果是四个四位的十六进制管,读数规则和十进制完全不是一回事。题目要求关键点用十进制、结果用十六进制,这个分工必须严格执行,混着看必然误判。
解决办法是给每个探测器显式设定位宽和进制,全部对齐到 16 位十进制;数码管固定四管十六进制,从高位到低位依次接 F 的第 15~12、11~8、7~4、3~0 位。设置完拿一个已知值试一次,比如常数1234十六进制,看数码管是不是显示1234,探测器是不是显示 4660。
6. 验收与答辩:用一张观测表证明电路真的跑通了
验收那天最怕的不是电路有 bug,是电路跑对了却说不清楚。我带过的几份课程设计里,凡是能顺利过答辩的,都在演示前准备了一张观测表——不是事后补的报告表格,是跑仿真时一边看一边填的现场记录。
表怎么列,取决于你要证明什么。自动累加电路要证明三件事:数据读对了、累加算对了、结果存对了。所以表里至少要有这几列:时钟拍数、读地址、读出的数据、累加中间结果、写地址、写入值。
| 拍数 | 读地址 | 读出数据 | 累加中间结果 | 写地址 | 写入值 |
|---|---|---|---|---|---|
| 0 | 0 | 3 | 3 | 16 | 3 |
| 1 | 1 | 5 | 8 | 17 | 8 |
| 2 | 2 | 7 | 15 | 18 | 15 |
| 3 | 3 | 9 | 24 | 19 | 24 |
| … | … | … | … | … | … |
| 15 | 15 | 33 | 288 | 31 | 288 |
这张表填满之后有三个用处。第一,填表过程中任何一拍对不上,立刻能定位到是读错、算错还是写错,不用等跑完再看终值。第二,答辩时老师问“你怎么知道写回去的是中间结果而不是原始数据”,把表一推,第 17 号单元的值等于前两项之和,这就叫证据。第三,报告里的测试结果与分析章节可以直接用这张表展开,比贴几张看不清的截图有说服力。
演示流程我一般排成四步,每步不超过一分钟。第一步,清零,展示所有计数器归零、数码管全零、RAM 的 16~31 号单元是初值。第二步,单步走三拍,一边走一边指探测器,说明读地址在涨、累加值在涨、写地址在涨。第三步,放开时钟连续跑,让计数器自己涨到 16 然后停住,重点指出“停住了”——自停逻辑是评分点,一定要让老师看到时钟被闸住的那一下。第四步,打开 RAM 看内容,指着 16~31 号单元说明前缀和规律。
被问到最多的三个问题,提前想好答案。问“为什么 RAM 要用独立的半周期时钟”,答加载和存储在同一拍会互相干扰,用两相把读写分开,读到的永远是上一次写进去的值。问“为什么写地址偏移写的是 15”,答加法器常数填 15、进位输入置 1,等效加 16。问“溢出怎么判的”,答加减法看符号位、乘除法看高位和余数,两套判据用 MF 选择器切换。
有一个坑我踩过:把所有测试数据都设成好算的数,比如全是 1、全是 2,结果电路里某一位接错了也能凑出正确答案,验收时换个数据当场露馅。从那以后我每次准备测试数据都强制走一遍“边界值加随机值”的组合,至少包含一个负数、一个最大值、一个会让结果溢出的值,跑通了才敢去演示。希望这套拆法和这张表能帮到你,把那份报告交得踏实一点。
本文还有配套的精品资源,点击获取