用HTML+JavaScript实现4位移位寄存器可视化模拟器

发布时间:2026/10/18 3:33:16
用HTML+JavaScript实现4位移位寄存器可视化模拟器 这个项目是我在整理数字逻辑与计算机组成辅导材料时做的系列小实验编号正好是38号——一个用纯HTML和JavaScript实现的4位移位寄存器可视化模拟器。选这个题目是因为移位寄存器几乎是每一位学数字电路的人都绕不过去的第一道坎四个触发器串在一起一根时钟线统一指挥一根数据线持续供给……原理图大家都见过可真要问数据到底是怎么一步一步从输入端挪到输出端的不少人还是得盯着时序图发呆半天。与其反复讲不如直接做一个能点、能看、能复位、能连续运行的交互页面。这篇就把我做这个模拟器的完整思路、关键代码和踩过的坑一起记录下来适合正在带数字电路课的同学也适合想用前端把计算机组成原理可视化的开发者。1. 拆解核心4位移位寄存器到底在移什么1.1 从D触发器的记忆特性说起移位寄存器不是凭空冒出来的它的基本单元是D触发器。D触发器是一个最基础的数字存储电路它有一个数据输入端D、一个时钟输入端CLK和一个输出端Q。工作规则非常单一只要时钟出现有效边沿通常约定是上升沿也就是信号从低到高的那一刻输出Q就变成D当下的电平其余任何时间不管D怎么变化Q都保持之前的值不动。你可以把它想成一个只在哨声响起时才会睁开眼看一眼号码牌的哨兵。哨声不响前面人怎么摆手它都不管哨声一响它立刻记录面前号码牌上的数字然后继续盯着。放在电路里这个盯着不动的能力就是记忆功能也是所有时序逻辑的基础。组合逻辑没有记忆而时序逻辑的状态正是靠这些触发器一个个撑起来的。很多人刚开始模拟时序电路时容易把触发器当成随叫随到的传输通道这是最大的误解。D触发器的输入变化不会立刻出现在输出它只会在时钟边沿那一刻被采样。做可视化工具时这个特性必须第一时间体现在界面上数据输入D即便切换到1只要没按下CLKQ0就不该亮。这个细节如果不做对整个演示就失去了教学意义。1.2 四个触发器如何串成传送带4位移位寄存器本质上是把4个D触发器首尾串联共享同一根时钟信号然后把每级触发器的输出接到下一级的输入。具体连法是Q0的输出接到Q1的D端Q1的输出接到Q2的D端Q2的输出接到Q3的D端最前面的Q0的D端则作为整个寄存器的串行数据输入口。这样接好之后每来一个时钟上升沿所有触发器同时动作但每一个动作的规则都一样输出跟随自己D端此刻的输入。于是的结果是Q0拿到外部新数据Q1拿到上一拍Q0的状态Q2拿到上一拍Q1的状态Q3拿到上一拍Q2的状态。整组数据就像一支队伍在往前挪动一个位置队伍最前面的新成员从D进入排在最后的人则从Q3端被挤出去。用排队来类比就非常好懂四个人站一排哨声一响每个人都往自己前方看并记住前一个人的位置然后前移一位队伍前面冒出空位新来的人外部D输入补上队伍末尾的人被腾挪出去。整个过程只需要一次哨声也就是一个时钟脉冲四个人同时换位而不是一个人先动、后面人再跟着动——这一点在做模拟时极其关键后面代码部分会专门提到。1.3 串入并出怎么把串行数据变成并行数据为什么这个东西叫移位寄存器而不是四个触发器因为它真正厉害的地方在于数据格式的转换能力。比如我们要把1、0、1、1这四个比特依次送进电路假设寄存器初始状态为0000第一个时钟D1寄存器变成0001第二个时钟D0同时原来的1被推到Q1寄存器变成0010第三个时钟D1状态变成0101第四个时钟D1最终状态变成1011。这时4位并行输出Q3、Q2、Q1、Q0上就是完整的1011。这个过程叫串行输入、并行输出简称串入并出。在现实世界里这种能力到处都是传感器一次只能吐出一根线上的数据寄存器把若干位攒齐后一次性交给后面的并行总线反过来也有并行转串行的用法这块先不展开。理解了串入并出再说4位就清晰了——它一次能存住4个比特的历史输入并且这是一个有顺序的记忆而不是抽象的4个零散位。顺序的语义靠的就是数据在触发器之间逐拍传递。2. 可视化方案把时序图改造成可点击的交互页面2.1 界面上必须出现的元素做这类教学小工具界面设计的第一原则不是好看而是和一个完整时序图的信息对得上。至少要有这么几个东西一个数据输入开关D用来模拟外部送来的0或1一个时钟按钮CLK点击一次代表送进一个上升沿四个状态指示灯分别对应Q0到Q3一个复位按钮把寄存器清回0000此外还要有一个节拍计数器和二进制/十进制读数显示。节拍计数器看着不起眼但对验证移了几位非常有用。指示灯的顺序建议Q0在左、Q3在右靠近输入端这样符合信号从左往右流动的阅读习惯。每条灯旁边配上触发器编号灯亮用红色加发光效果灯灭用灰色颜色差别要足够大因为在课堂投影上演示时色差不够会被环境光吃掉。按钮的反馈也不能含糊CLK按钮按下时最好有明显的位置反馈让操作者感觉到确实触发了一步。在布局上我用了上下三段式最上面是寄存器主体区四盏灯一字排开输入D放在最左侧旁边标注串行输入中间是读数区显示Q3Q2Q1Q0的二进制值和十进制值最下面是操作按钮区按钮高度做大一点方便在触屏设备上也可以直接点击。这个排版在手机上实测也够用一个页面就能讲完原理不需要任何额外软件。2.2 为什么选手动点击时钟作为主操作自动播放看似炫酷但我把手动产生脉冲做成了主操作用户每点一次CLK寄存器走一拍。这样每个时钟沿前后屏幕上的变化是可控的操作者可以在这一个节拍内看清楚输入D是多少四个灯怎么联动读数怎么变化。尤其讲数据移位流程时最忌讳时序一闪而过学生还没反应过来状态已经变了两次。手动模式的好处就是慢镜头。界面右侧我专门放了一块当前步数文本每点一次CLK就自增。演示串行输入的时候我会让学生这样练设定D1点CLK记下Q0亮D切换成0点CLK看到1被推到Q1D再切换成1点CLK……连续四次之后让他们不看屏幕先在纸上写出四拍后的寄存器值然后再对照页面验证。这个玩法在课堂上反馈非常好比对着教材画时序图有用得多。有人会问那为什么不直接放一个真实时钟信号连续跳因为真实时钟在页面上很难看到沿。连续跳的时候人眼只能看到灯在乱闪无法追踪单个脉冲的传播过程。所以自动演示我放在了第二优先级用于在学生对过程有直觉后把速度调起来连续观察整体动态这部分下一小节再说。2.3 自动演示与速率调节的设计自动演示的核心价值是建立动态系统的整体感觉。我加了一个频率滑块范围从每秒1个脉冲到每秒10个脉冲默认5。滑块对应的其实是setInterval的间隔毫秒数逻辑很简单但交互上有个细节改滑块时不能直接把定时器拆了重建否则每拖动一次就重置计数体验很别扭。我的做法是先用一个变量保存当前频率只在定时器重新创建时才把新频率应用进去。启动自动演示后CLK按钮仍然可以使用。这里有个节奏问题如果定时器刚好要在下一秒触发用户又在手动点击那么两拍之间的间隔会被压缩。但JavaScript本身是单线程事件队列不存在真正的并发冲突连续两个产生脉冲调用只会按顺序执行结果仍然是对的。不过从教学体验上说自动演示过程中再叠手动点击容易让操作者搞不清当前是哪拍所以我做了一个折中自动运行时把手动CLK按钮置灰暂停自动后再恢复。这个互斥的思路在代码部分会具体体现。自动演示的初始状态处理也要想清楚开始自动前最好先复位一次否则寄存器里残留上次实验的数据观察结论就乱了。我遇到的另一个小麻烦是暂停之后节拍计数器没有重置导致学生记录第几拍时对不上。后来我改成复位按钮同时清零节拍和停止自动演示暂停自动只停时钟、不清数据。这样状态机就很干净复位等于全清暂停等于冻结恢复等于续跑。3. 代码实现结构、样式与控制逻辑3.1 HTML结构控制区、显示区、自动演示区先把页面的骨架写出来。我用一个主容器包住三块区域最外侧限制最大宽度避免在投影上拉得太宽。寄存器主体部分用一组div每个触发器包含灯和编号布局上特意把输入D做成一个独立的灯不和四个寄存器灯混在一排而是稍微向左隔开这样外部输入和内部状态的空间关系就清楚了。div classwrapper div classsection div classinput-side div classled idled-input/div span串行输入 D/span /div div classreg-side div classled led-reg idled-q0spanQ0/span/div div classled led-reg idled-q1spanQ1/span/div div classled led-reg idled-q2spanQ2/span/div div classled led-reg idled-q3spanQ3/span/div /div /div div classsection readout spanQ3Q2Q1Q0 b idbin-out0000/b/span span十进制 b iddec-out0/b/span span已走节拍 b idstep-count0/b/span /div div classsection controls button idbtn-dataD 输入 0/button button idbtn-clkCLK 手动脉冲/button button idbtn-reset复位/button label自动频率 input typerange idrate-slider min1 max10 value5/label button idbtn-auto开始自动演示/button /div /div这个结构之所以把D输入放最左是因为阅读习惯从左往右数据流的起点必须在视觉起点。而控制区放最后是因为课堂演示时手指经常点按钮放在下方不容易挡住上面的状态灯。按钮的id命名我统一带了前缀btn-对应按钮led-对应灯后面接语义这样写JS时找元素非常省事。3.2 CSS样式让LED状态一眼可辨LED是这套界面的视觉核心敲定了它的样式整个页面气质也就定了。我用了圆形的div加border-radius变成发光圆点灯灭时是深灰色灯亮时换成红色并加上box-shadow产生光晕。四盏灯之间用flex布局拉开间距保持对称。为了让灯的亮灭变化更自然我加了一个0.15秒过渡切换时不会突然闪投影上也看得舒服。.led { width: 48px; height: 48px; border-radius: 50%; background: #3a3a3a; border: 2px solid #222; display: inline-flex; align-items: center; justify-content: center; font-size: 14px; color: #999; transition: background 0.15s, box-shadow 0.15s; } .led.on { background: #e53935; color: #fff; box-shadow: 0 0 14px rgba(229, 57, 53, 0.8); }这里有个小知识点box-shadow的透明度不能太高太高在浅色投影布上会糊成一片也不能太低太低就没有发光感。我调成0.8配合红色主色实测在正常环境光下足够醒目。灯的尺寸倒不一定要这么大但考虑触屏手指点击场景按钮做大的同时灯也做大一些整体观感才协调。另一个容易被忽略的地方是灯内文字对比度灯灭时文字用浅灰灯亮时文字用白色保证标签始终能读出来。3.3 JavaScript核心用数组模拟时钟上升沿到核心逻辑了。寄存器状态我用一个长度为4的数组reg保存数组下标0对应Q01对应Q12对应Q23对应Q3另外用变量dInput保存当前D输入用stepCount保存节拍数。点击CLK按钮时调用clockPulse()这才是整个模拟器的心脏。let reg [0, 0, 0, 0]; let dInput 0; let stepCount 0; function clockPulse() { const next [ dInput, reg[0], reg[1], reg[2] ]; reg next; stepCount; render(); }每次脉冲新的Q0接收外部输入新的Q1、Q2、Q3分别接收旧Q0、旧Q1、旧Q2。所有赋值都基于旧状态一次性算出这一步是绝对不可妥协的。如果图省事写成依次赋值先改reg[0]再改reg[1]取reg[0]的新值那就变成了第一个人先动下一个再跟着学完全违背同步时序的含义演示结果也会在第二个脉冲开始出现错误。这一点我在下面常见问题里又单独提了一次因为它太容易踩坑。render()函数负责把reg状态画到界面上包括灯的颜色、二进制读数、十进制读数和节拍数。二进制读数在显示前要reverse因为reg的下标从Q0到Q3但读数的书写习惯是高位在左、低位在右也就是Q3Q2Q1Q0的顺序。数组默认join出来是Q0Q1Q2Q3直接当成二进制串会和十进制换算对不上这是新手实现时容易忽略的隐藏坑。function render() { [q0, q1, q2, q3].forEach((id, i) { document.getElementById(led- id).classList.toggle(on, reg[i] 1); }); document.getElementById(led-input).classList.toggle(on, dInput 1); const binText [...reg].reverse().join(); document.getElementById(bin-out).textContent binText; document.getElementById(dec-out).textContent parseInt(binText, 2); document.getElementById(step-count).textContent stepCount; }3.4 事件绑定与自动演示的定时器管理事件绑定的部分很直接D按钮点击后切换dInput同时更新按钮文本和输入灯CLK按钮调用clockPulse复位按钮把reg清全0、dInput清0、stepCount归零并停止自动演示。这里最值得讲的是自动演示的定时器管理。我管定时器叫autoTimer用一个变量是否为null来标记是否在运行。let autoTimer null; function toggleAuto() { if (autoTimer) { clearInterval(autoTimer); autoTimer null; document.getElementById(btn-auto).textContent 开始自动演示; document.getElementById(btn-clk).disabled false; } else { const hz Number(document.getElementById(rate-slider).value); autoTimer setInterval(clockPulse, Math.round(1000 / hz)); document.getElementById(btn-auto).textContent 暂停自动演示; document.getElementById(btn-clk).disabled true; } }注意我每次创建定时器前都把旧的clear掉并且用autoTimer是否为null来判断状态。这个模式在JS里非常可靠比用布尔变量单独标记更不容易出明明还在跑却显示暂停的错乱。自动运行时手动CLK按钮被禁用课堂演示时就不会出现我点一下它也跳一下的混乱体验。复位按钮在自动运行中的行为要特别小心我的处理是先clearInterval再清状态否则定时器回调会在清空后的一瞬间又跑一拍导致页面显示的数据很不稳定。实测中我还遇到过切换滑块频率时新定时器尚未创建、旧的还在执行回调造成一瞬间的快慢不均后来统一用每次开关只创建或销毁一个定时器的方式解决效果稳定。4. 测试思路与边界情况处理4.1 一组可以直接照抄的手动测试序列写完代码不能直接拿去用要先跑一遍确定性测试。我建议大家都用同样的套路验证先复位确保全0然后把D设为1点CLK预期Q0亮、Q3Q2Q1Q00001再把D设为0点CLK预期Q0灭、Q1由0变1因为刚才的1被推到了Q1整体变成0010再设D为1点CLK预期Q0亮、Q1保持0、Q2变成1整体为0101最后再设D为1点CLK预期变为1011。为什么选1011这个序列它包含连续两个1的重复输入能检验虽然D连续两次为1但Q3和Q2在不同节拍应该不同如果模拟器出现信号透传错误这个序列最容易暴露。整个测试只要四拍拍完一对照代码的正确性基本就能确认。节拍计数器在每一拍后也要跟着加1最后显示为4如果数值不对说明render的调用次序有问题。节拍D输入动作预期 Q3Q2Q1Q011CLK000120CLK001031CLK010141CLK10114.2 自动演示、复位、切换频率的组合场景自动演示看起来只是定时调用clockPulse但它和别的按钮组合后会出现一些需要注意的交叉场景。第一种情况自动运行中点击复位。正确的行为应该是自动停止、状态清零、节拍归零。如果复位函数里忘了clearInterval就会出现复位了还在自动跳的灵异现象。第二种情况自动运行中拖动频率滑块。滑块本身只是个数字不会立即生效必须等下一次开关自动演示时新建定时器才生效。为了反馈更直观我在滑块旁边放了一个当前频率xHz的小字拖动时实时更新提示。第三种情况相对隐蔽暂停自动演示后stepCount没有重置继续开始后节拍从暂停前的数字继续跳。我保留了这种设计因为它能让学生看到暂停了多久以及总共走了多少拍。但如果你的教学场景要求每轮演示都从0计数那就在开始自动演示的逻辑里加一行stepCount 0。两种行为都有道理我选择保留续跑是因为可以借此讲解寄存器状态与操作次数是两回事。4.3 边界情况初始状态和全零或全一输入边界情况里最常被忽略的是初始状态。页面加载时寄存器是全0如果不复位直接开始点CLK数据会从全零基础上往寄存器里填充这是合法的但讲解时容易让学生误以为寄存器默认装的是某段串行历史。所以我在页面加载完成后主动调用一次render()让读数区明确显示0000而不是等用户第一次操作才刷新。另一个边界是D长期保持0或长期保持1D保持0四个脉冲后寄存器还是0000这时节拍走了4步但状态看起来一样学生容易误以为没工作。D保持1则是全1汇聚经过四拍寄存器变成1111。为了帮助理解我在读数区额外显示了十进制值全0是0全1是15引导观察不同输入序列产生不同的数值而不会因为状态灯没变化就以为程序坏了。5. 常见问题与调试实录5.1 问题第一次点CLK灯却没有反应这是最常见的灵异现象十有八九是D输入还是0。寄存器接收0时Q0本来就是灭的表面看起来没变化容易被人当成程序没写对。我在演示时提醒同学先点D按钮看按钮文本确保显示D 输入 1再点CLKQ0就亮了。真要说代码层面的话那就是忘了在render里同步输入灯或者classList.toggle第二个参数传反了。排查方法很简单在render里临时输出console.log(dInput, reg)对照控制台看一眼就知道状态有没有跟上。另一个类似的问题是在复位后直接按了自动演示如果D为0四个脉冲的动画几乎看不出灯的变化节奏对了但视觉上像卡死。所以我在自动演示开始前加了提示建议先把D设为1或者输入一段有变化的序列演示效果会直观得多。5.2 问题二进制显示方向和面板顺序对不上界面上灯从左到右是Q0到Q3但标准二进制书写是高位在左、低位在右也就是Q3在左。换句话说数组reg按Q0到Q3存join出来是Q0开头的字符串比如reg[1,0,1,1]时会显示1011但这并不是常规写法。按习惯Q3Q2Q1Q0应该写作1101。两者方向相反十进制数也会完全不一样。我的做法是定下规则状态数组一律按下标0到3存Q0到Q3LED也按这个顺序排列只有在生成二进制显示时才执行一次reverse。这样内部逻辑和视觉保持一致最终读数格式受到统一控制改起来不会两头乱。调试时最有效的办法是给每条LED下面标注Q0、Q1、Q2、Q3然后手动跑一个已知序列比如四拍后状态Q31、Q21、Q10、Q01页面二进制显示1101。如果页面显示1011那就是reverse加错了位置如果显示1101但灯的位置和标签对不齐通常是灯的顺序和数组下标映射写反了。5.3 问题自动演示和手动CLK按钮互相干扰我在自动运行时禁用了手动CLK按钮这个方法虽然简单却挡掉了很多麻烦。最开始一版我允许两个入口同时触发clockPulse测试中发现双击速度稍快时前一拍的render还没让按钮文字刷新新一拍又进来了虽然结果不错但步数显示会跳跃课堂上容易让人误解成两次操作对应两拍。如果不想禁用按钮另一个解法是把所有产生脉冲的入口统一到同一个名为pulse()的函数在函数内部记log方便追踪一次演示总共发出多少次脉冲。还有一种情况是在定时器回调里调用了clockPulse但clockPulse内部又去读了一下自动状态的开关导致每次都误判为需要停止表现就是自动演示只跳了一拍就停了。排查这类问题先看console是否有报错再看定时器ID是否被意外clear。我的经验是不要把业务状态判断塞进定时器回调定时器回调只负责一件事产生一拍脉冲是否停止由控制按钮决定这样代码职责就清晰了。5.4 避坑总结速查表现象常见原因对策点CLK后灯不变D输入为0或render未同步输入灯先确认D设为1检查render中的toggle参数读数方向反了数组顺序与二进制书写顺序不一致显示前对数组做一次reverse连续点CLK状态错乱循环内逐个赋值导致后级读到新值一次性从旧状态计算next数组自动演示停不下来复位时未clearInterval复位函数里先清定时器再清状态暂停后步数对不上自动关闭时未重置stepCount按教学需求选择续跑或清零这张表我在每次做完类似的模拟器都会更新一版后来做多位加法器、模计数器的时候也直接沿用这套排查思路。其实这些坑大多不是算法难度而是对同步采样理解不够或者状态管理不统一造成的写代码之前把状态模型想清楚后面的调试时间能省掉一大半。最后再分享一个个人习惯这类HTML小工具做出来之后我总会先拿一串固定序列手工推演一遍再和页面逐拍对照确认无误后才拿到课堂上用。虽然代码不长但它承担的是教学演示任务状态只要错一拍学生建立起来的心智模型就得推倒重来。用数组模拟触发器阵列是一个很上手的办法后来我把同样的思路扩展到了模8计数器、四位加法器等系列小实验原理都是那一句话先读旧状态再写新状态一拍一次绝不透传。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询