资讯详情

资讯详情

16位ALU与自动累加:Logisim可运行工程及RAM时序避坑

简介面向数字电路与计算机组成原理课程学习者这份Logisim 16位自动运算器实验资料围绕ALU设计与数据通路搭建展开重点解决16位运算器封装、RAM读写时序控制及自动累加回存等实践问题。压缩包仅含1个doc文件大小约341KB以课程实验报告形式组织便于直接查阅与对照实验。报告完整覆盖实验目的、Logisim环境、16位运算器构建、自动运算电路设计与封装、过程调试、总结心得及参考文献其中运算器支持加减乘除、逻辑与或非异或、逻辑/算术移位及进位、溢出、零标志判断自动电路借助计数器寻址在时钟驱动下累加RAM 0—15号单元并将中间结果写回16—31号单元关键节点以十进制探测显示、结果用十六进制数码管呈现。已有4049人学习适合需要理解ALU功能表、溢出检测、存储器交互和时序调试的读者参考。1. 16 位运算器加自动累加一份能直接跑的 Logisim 工程翻到 logisim 自动运算器 这个题目的人多半手里已经攥着一份课设要求先做一个 16 位 ALU支持加减乘除、与或非异或、逻辑移位和算术右移再把 RAM 里 0–15 号单元的数依次累加中间结果写回 16–31 号单元全程由时钟推着走不许手动点。看着像两道题做起来是一道题——ALU 的溢出位判据写错累加到第三个数就开始飘RAM 的读写节拍没分开加载和存储会互相踩读数正常但写回去的是上一拍的值。这份资源给的就是一套已经跑通的 Logisim 工程16 位运算器、自动运算主电路、封装好的顶层模块外加探测器和隧道引出的十进制观测点。适合正在做组成原理课设、不想在时序上反复翻车的人也适合拿它当模板改数据通路的熟手。2. 16 位 ALU 怎么搭12 条操作码、三个状态位与移位实现2.1 运算部件选型内置算术组件还是自己搭全加器Logisim 的 Arithmetic 库里已经给了 Adder/Subtractor、Multiplier、Divider、Bit Shifter、Comparator 这几类组件课设要求写的是「利用现有运算部件构建」那就别逞能去用门电路拼 16 位超前进位加法器——那是另一门课的练法放到这里只会把调试时间全吃掉。真正要动手的地方在于位宽和输出截断所有算术组件的 Data Bits 统一设成 16两个 16 位数相加结果天然是 17 位多出来的那一位就是进位输出别把它丢了后面状态字要用。乘法器是最容易出岔子的一个。16 位乘 16 位完整积是 32 位多数发行版的乘法器组件允许你选输出位宽选 32 位就得自己截低 16 位当结果、把高 16 位是否为零送去判溢出选 16 位则等于直接丢弃高位溢出判断只能靠「高半部分非零」这个信息而你手上已经没有高半部分了。我一般会老老实实选 32 位输出接一个 Splitter 拆成高低两半低 16 位进结果总线高 16 位进一个「或归约」判断是否全零。除法同理Divider 输出商和余数两路余数不为零就说明除不尽无符号除法下这正是溢出的判据之一。减法直接用 Adder/Subtractor 组件把加减控制端接操作码的最低位就行不需要再单独搭一个减法器。这一步省下来的连线后面排查时能少看半小时。2.2 功能表编码4 位操作码怎么排到 12 路结果上原始工程用的是一张四位操作码表前半段 0000–0111 是算术与逻辑运算后半段 1000–1011 是四种移位。把它整理成一张能直接对着连线查的表比在脑子里记靠谱得多操作码功能结果来源备注0000F A B16 位加法器同时输出进位0001F A − B加法器减法模式借位即进位取反0010F A × B乘法器低 16 位高位判溢出0011F A ÷ B除法器商余数判溢出0100F A B与门阵列逐位0101F A | B或门阵列逐位0110F ¬A非门阵列B 忽略0111F A ⊕ B异或阵列逐位1000F A 逻辑左移分离器错位拼接低位补 01001F A 逻辑右移分离器错位拼接高位补 01010F A 算术左移同逻辑左移二者等价1011F A 算术右移错位拼接 符号位回填高位补 A1512 路结果要汇到一个输出端口上用多路选择器做。常见做法是一个 Select Bits 设 4、Data Bits 设 16 的 Multiplexer它天然有 16 路输入只接 12 路剩下的接地即可。想省组件也可以用两级先两个 8 选 1 分高低半区再一个 2 选 1 收口级联后选择端仍然是 4 位只是要自己把高位当区选、低位当路选。单级方案连线更乱但时序更短级联方案图面干净看你更怕哪一种。有一个细节值得单独说算术左移和逻辑左移在 16 位定长下结果完全一样所以表里 1010 和 1000 可以直接共用一路输出少一组分离器。原始工程把它们分列是为了让功能表看起来完整实际连线时把 1010 也接到同一路不影响任何测试结果。2.3 移位为什么用分离器拼位序标反就是灾难逻辑左移的标准做法是「错一位拼接」用一个 Fan Out 设 16、Bit Width In 设 16 的 Splitter 把输入拆成 16 根单线然后把这 16 根线接到第二个 Splitter 的分线端接法整体上移一位——原来的 bit0 接到 bit1bit1 接到 bit2一直到 bit14 接到 bit15新出现的 bit0 接常量 0合并端输出的就是左移一位的结果。逻辑右移把方向反过来bit15 空出来接常量 0。算术右移只改一处空出来的 bit15 不接常量 0而是接原输入的 bit15也就是符号位。这里最容易翻车的是 Splitter 的位序。它的分线端在图上是从上往下排的但哪一头是 bit0 取决于组件的 Facing 属性和你放置时的朝向翻个面顺序就倒过来了。位序标反的典型症状是输入 0x0001 左移期望 0x0002实际出来 0x8000。看到这个结果不用怀疑别的直接去检查两个 Splitter 的位序标注或者干脆把输入换成 0x8000 再试一次反着来一遍就能确认。2.4 三个状态位零、进位、溢出各有各的判据程序状态字里最常被混为一谈的是进位和溢出。零标志最简单16 位结果做一次或归约全零输出 1或者用 Comparator 把结果和常量 0 比。进位标志来自加法器的进位输出端减法时它是借位的反。溢出标志必须按有符号规则判两个操作数符号相同、结果符号与它们不同才叫溢出写成逻辑式就是 OF (A15 B15) (S15 ! A15)。拿无符号进位去当溢出用8 8 这种正数相加得 16 的情况会被误报成溢出而它其实只是无符号进位。乘除的溢出判断是另一套逻辑。乘法看高 16 位是否全零除法看余数是否为零两者都可以用一个 Comparator 把待判值和常量 0 比较输出接溢出位。注意这里的语义是「无符号溢出」和加减法的有符号溢出不是一回事报告里最好分开写别在一个标志位上混着表达两种含义否则后面查错时你根本分不清是哪种溢出触发的。3. 自动运算主电路32×16 RAM、双计数器与停机逻辑3.1 两个计数器读地址 0–15 与写地址 16–31 的偏移累加电路的骨架是「读一个、加一个、写一个」读地址和写地址必须同步推进。原始工程用两个计数器受同一个时钟触发第一个直接输出 0、1、2……作为读地址第二个在第一个的值上加一个常量作为写地址。这个常量取 16 还是 15取决于第一个计数器的起始值从 0 开始计数写地址就是读地址 16如果计数器配成从 1 开始偏移 15 才能对齐到 16。我一般统一成从 0 开始、偏移 16少一层心算。计数器本身的位宽是个隐蔽的坑。要计到 16 并在这个值上产生停止信号Data Bits 必须设 5因为 4 位最大只能表示 15计到 15 就回零了比较器永远等不到 16。这一点在原始工程的故障记录里没写但它是「计数器从 0 累加到 16 然后停止」这个需求能否成立的前提。把计数器位宽设 5、初始值 0比较器另一端接常量 16到点输出 1这条链路就通了。写地址那一路还要注意时序累加结果是在读出数据之后才算出来的如果写地址和读地址完全同拍推进写入的会是上一拍的累加值。解决办法要么给写地址计数器加一拍延迟用寄存器打一拍要么让写操作发生在读操作之后半个周期这就引出了下一节的半周期时钟。3.2 RAM 的加载与存储为什么要挂一个半周期时钟RAM 组件的数据位宽设 16地址位宽设 5正好 32 个单元。它的控制引脚有两个关键角色load 决定输出端是否把选中单元的数据送出来store 决定当前数据输入端是否写进选中单元。原始工程里踩的坑是「把主时钟直接接到 RAM上升沿更新数据」结果要么只加载不存储要么存进去的不是累加结果——因为同一个上升沿上读、算、写三件事挤在一起硬件只会按你连线的组合逻辑给出那一瞬间的值而这个值往往是旧值。解决办法是给 RAM 单独挂一个时钟频率设成主时钟的两倍。这样在一个主时钟周期内RAM 时钟会走完两个完整周期第一个沿附近 store 无效、load 有效数据被读出来送进运算器第二个沿附近 store 有效此时运算器的结果已经稳定写进去的就是当拍的累加值。频率翻倍在 Logisim 的时钟组件里就是改一个数字占空比不用管它固定是 50%。提示这种做法成立的前提是 RAM 的触发方式设成边沿触发Rising Edge 或 Falling Edge而不是电平触发。电平触发下 store 有效期间会持续写入两倍频也救不了你。如果不想用两个时钟另一种常见做法是用一个 D 触发器把 store 信号延迟半拍或者用主时钟的下降沿去驱动写地址寄存器。前者改动小后者图面更干净看你手上还剩多少耐心。3.3 停机逻辑比较器加或门别去断开时钟线计数到 16 要自动停原始工程用的是比较器输出 1再和时钟过一个或门把时钟线拉成常量高。这个思路是对的但新手常见的错误做法是「拿一个开关或三态门把时钟线断开」。在 Logisim 里悬空的线显示为蓝色或红色逻辑值是 E未知下游的边沿触发元件收到 E 之后的 Behavior 完全不可预期数码管直接显示 E 或者整条电路飘红。用或门拉高的好处是停机之后时钟线恒为 1寄存器、计数器和 RAM 都收不到新的上升沿状态被冻结显示保持最后结果。这里有个前提——下游所有时序元件都必须设成边沿触发。如果哪个寄存器是电平触发High Level恒 1 对它来说就是一直使能停机瞬间它会把当前输入反复锁进去看起来像「停了但数还在变」。我吃过这个亏后来每次加停机逻辑都会把所有时序元件的 Trigger 属性挨个过一遍。比较器的 Numeric Treatment 属性也要设成无符号输入是 5 位计数值和常量 16。如果误设成二进制补码16 会被解释成负数比较结果永远不成立电路会一直跑下去。3.4 探测与隧道把关键点拽到顶层用十进制看调试阶段最值钱的东西是观测点。主电路最上面一行应该把这几路信号引出来读地址、读出的数据、累加中间结果、写地址、状态字。做法是在需要观测的节点上放一个 Tunnel隧道给它起个名字再到顶层同一行放同名 Tunnel 接 Probe探测器Probe 的 Radix 属性设成 Unsigned Decimal显示的就是十进制。隧道的名字必须完全一致大小写敏感改一个字母就断链。运算器结果按要求用 16 进制数码管显示。一个 16 位数要拆成四位十六进制用 Splitter 把它分成四组 4 位每组接一个 Hex Digit Display四个数码管从左到右依次是 bit15–12、11–8、7–4、3–0。拆的时候注意别把高低位接反接反了读出来的数看着「像那么回事」实际上字节序全错这种错误靠肉眼比对累加值极难发现。探测器不要只放在最终结果上。累加过程中每一步的中间值都值得看一眼因为错误往往在第三步就出现了只是你到第十六步才发现结果不对。多放几个 Probe代价只是图面乱一点。4. 封装与联调从 RAM 映像导入到逐级对照验证4.1 封装引脚怎么留时钟、复位、结果、完成信号主电路调通之后把它做成子电路供顶层调用。封装的本质是给子电路定义端口时钟输入、复位输入、16 位结果输出、一位完成信号输出四个口就够。复位按键用 Button 或者输入引脚驱动把它接到所有计数器、累加寄存器的 Reset 端一根线串到底别漏掉任何一个时序元件——漏一个复位后那个元件还停在上次的状态你会看到「复位了但写地址从 16 开始」这种诡异现象。RAM 是封装里唯一没法靠复位清干净的部件。它没有「恢复到初始映像」的引脚Reset 端只能清掉地址计数器RAM 里的数据还留着上一轮的累加结果。所以在封装电路里做复位要么同时驱动 RAM 的 clear 引脚代价是 0–15 号单元的原始数据也一起没了得重新加载映像要么接受「复位只重置流程、不重置存储」这个设定每次重跑前手动重新加载一次映像。原始工程选的是后者报告里也写了「重新进行试验的时候可以通过这个按钮让自动运算电路回到初始状态」——严格说它回到的是流程初始态不是数据初始态这一点在答辩时容易被追问。封装完成后顶层只留时钟、复位按钮、四个十六进制数码管和一排十进制探测器。整个电路没有数据输入端口第一次看会觉得别扭习惯就好数据全在 RAM 里输入靠映像文件观测靠探测器和数码管。4.2 初始数据导入用脚本生成 RAM 映像文件RAM 的数据可以手工在组件里一个个填但 32 个单元填起来容易错位改一次数据就要重填一遍。更省事的做法是写一个十六进制映像文件右键 RAM 选加载映像一次到位。映像文件的格式通常是每行一个十六进制字行序就是地址序16 位数据要写满 4 位、不足补零。下面这段脚本生成一份可复现的测试数据前 16 个单元放随机数后 16 个单元清零同时打印出期望的累加和方便和数码管对照# gen_ram_image.py import random random.seed(20240501) # 固定种子保证每次生成同一组数据 vals [random.randint(0, 0xFFFF) for _ in range(16)] with open(ram_init.hex, w, encodingascii) as f: for v in vals: f.write(f{v:04X}\n) # 地址 0-15待累加的原始数据 for _ in range(16): f.write(0000\n) # 地址 16-31写回区先清零便于观察 expect sum(vals) 0xFFFF # 16 位定长累加和取低 16 位 carry sum(vals) 16 # 超出 16 位的部分用于核对进位标志 print(expect 0x%04X, carry_out %d % (expect, carry))脚本的关键点在最后两行。16 个 16 位数相加和很容易超过 0xFFFF这时数码管显示的是低 16 位而进位标志应该是 1。如果你只看数码管、不看进位位会误以为电路算错了。反过来如果进位位亮着但数码管的值和 expect 对不上那才是真的错了。种子固定下来的意义在于同一份映像反复加载每次跑出来的结果必须完全一致不一致就说明电路里有未初始化的状态或者时序竞争。映像加载完之后先别急着点时钟。把探测器挨个看一遍读地址是不是 0读数据是不是等于第 0 号单元的值累加寄存器是不是 0写地址是不是 16。这四个值任一不对先解决它再往下走。4.3 逐级对照法结果对不上时按这个顺序查自动运算电路出错最忌讳一上来就盯着最终结果看。正确的做法是从头到尾按数据流顺序逐级验证每一级确认无误再推进到下一级。下面这张表是我自己排错时的检查顺序从上往下走遇到第一个不符合的就停下来修观察点正常表现不符合时先查什么读地址计数器0→15 逐个递增到 16 停止计数器位宽是否 5、比较器常量是否 16读出数据与映像文件对应行一致load 引脚是否有效、地址线是否接反累加寄存器每拍等于上一拍加当前读数据加法器位宽、寄存器使能是否常开写地址从 16 开始与读地址同步递增偏移常量、写地址是否打了拍写回数据等于当拍的累加中间结果store 时序、RAM 时钟频率最终数码管等于脚本打印的 expect数码管位序是否接反原始工程的测试记录里有一处很典型的观察某个地址读出来的数据「不正常」但把上一次的正常读数据和上一次的累加结果相加正好等于这一次的累加结果。这说明加法本身没错错在读出环节——读到的是旧值或者错位值。这类现象靠逐级对照几分钟就能定位靠盯着最终结果猜可能要花一晚上。还有一点测试时把每一步的中间结果都记下来哪怕只是抄在纸上。累加电路的正确性是一条链任何一环断了后面的值全废只有把每环的实际值和期望值并排放才能看出是从哪一环开始分叉的。5. 避坑与排查五条翻车记录的现象、原因与解法5.1 时序与时钟类第一条计数器计到 15 就回零停机信号永远等不到。现象是电路跑满 16 拍也不停数码管一直在变。原因是计数器 Data Bits 设了 44 位最大 15到 15 之后自动回零比较器的常量 16 永远匹配不上。解决就是把 Data Bits 改成 5让它能表示 0–16回零由比较器输出的停机信号去控制而不是靠计数器自身溢出。第二条停机之后数码管还在跳。现象是时钟线已经拉高但累加寄存器的值仍在小幅变化。原因是某个时序元件的 Trigger 属性设成了 High Level电平触发恒 1 对它等于持续使能。解决是把所有寄存器、计数器、RAM 的触发方式统一改成 Rising Edge停机后再确认一遍每个元件的属性。第三条RAM 写回的数据总是滞后一拍。现象是 16 号单元里存的是第 0 次的累加值17 号存的是第 1 次的整体错位。原因是 store 和 load 挂在同一个时钟沿上写入用的是上一拍稳定下来的旧值。解决是给 RAM 单独挂两倍频时钟或者用触发器把 store 延迟半拍让写操作落在计算结果稳定之后。5.2 数据通路与位宽类第四条乘法结果看着对、溢出位却常亮。现象是随便乘两个小数溢出灯就亮。原因是用无符号进位当成了有符号溢出或者乘法器输出位宽选了 16 位、高位信息已经丢失判溢出的逻辑拿到的是残缺数据。解决是把乘法器输出设成 32 位高 16 位做或归约判溢出加减法的溢出单独用符号位规则判两套逻辑不要混用。第五条移位结果高低位颠倒。现象是输入 0x0001 左移得到 0x8000。原因是两个 Splitter 的位序标注方向相反拆的时候 bit0 在顶上拼的时候 bit0 跑到了底下。解决是把两个 Splitter 的 Facing 调成一致或者用 0x8000 做一次反向验证看移出来的位落在哪一端一次就能确认。5.3 显示、封装与仿真类第六条封装后复位按钮按下去没反应。现象是顶层点复位内部计数器纹丝不动。原因是 Reset 信号只连到了部分时序元件或者封装时内部隧道名和外部引脚名对不上导致断链。解决是把所有带 Reset 端的元件列一遍逐个确认连线隧道名大小写、拼写逐字核对。第七条数码管显示 E 或者整条电路飘红。现象是一上电就满屏 E。原因是某根线悬空常见于断开时钟线、漏接 Splitter 的空闲分线端、或者多路选择器没接的输入端留空。解决是给所有空闲输入端接常量 0时钟线不要断开而是用或门拉高多路选择器没用的那几路统一接地。6. 进阶改造把固定 16 次累加变成可配置的运算框架原版电路把「累加 16 个数」写死在了比较器的常量上改一次需求就要动一次连线。想让它变成能复用的框架最省事的一步是把常量 16 换成一个 5 位输入引脚接到比较器的一端这样累加长度就变成了运行时可配的参数接 8 就累加前 8 个单元接 16 就是原版行为。改动只有一根线但电路的性质从「一次性课设」变成了「可配置的数据通路 Demo」。再往上一层可以把运算器的操作码也引出来。原版在自动运算电路里把操作码固定成加法因为需求只要累加。把操作码做成输入端口之后同一套读地址、写地址、写回的骨架可以跑累加、累乘、逐位与甚至跑一个简单的滑动平均读两个单元、加、右移一位、写回。骨架不变只换运算和写回策略这才是这份工程真正的复用价值。验证方法上我习惯用脚本对着跑一遍验证项手工做法脚本做法累加和对着数码管读数生成映像时打印 expect逐位比对进位标志看状态字那一位用 sum 16 判断是否应亮中间结果抄探测器读数每拍把 Probe 值记下来和前缀和比边界数据手填 0xFFFF映像里塞几个 0xFFFF、0x0000 试极值边界数据这一栏最容易被跳过。全填随机数的映像跑通了不代表电路没问题——随机数很少触发进位链的最坏情况。往 0–15 号单元里塞几个 0xFFFF累加到第二个就该进位了这时候进位标志、溢出标志、写回值三者的表现才能把隐藏的位宽问题暴露出来。说个自己的教训。早些年做这类时序电路我总想着先把整条链路连完再一起仿真觉得分段调试太慢。结果每次出问题都要在几十个元件里大海捞针一晚上过去连错在哪一级都没定位到。后来改成每加一个模块就单独激励一次、确认输出再往下接看着慢实际上总时间少了一半。从那以后我每次搭带时钟的电路都强制先把读地址计数器单独跑一遍、再把读出数据单独验一遍确认这两级干净了才接运算器。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →