ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

程序计数器PC与ROM取指实验:Quartus II + ModelSim仿真完整指南

程序计数器PC与ROM取指实验:Quartus II + ModelSim仿真完整指南 开门见山地说如果你刚接触计算机组成原理又想亲手跑通一个“CPU怎么知道下一条指令在哪”的完整链路那程序计数器PC配上ROM取指这个实验绝对是最值得花一下午死磕的入门点。我当年第一次在Quartus II里把这个逻辑搭出来、再用ModelSim看到PC地址一个周期一个周期往上跳、ROM按地址吐出对应数据的时候才真正明白“存储程序”这四个字到底是什么意思。这篇文章就从设计PC、让PC驱动ROM地址开始把从原理图/Verilog编写到ModelSim仿真出波形的完整过程拆开揉碎全程基于Quartus II 13.0及以上版本的通用操作适合刚上完数字电路课、准备做EDA课程设计或者自学FPGA的读者。先说清楚这个模块是干什么的程序计数器PCProgram Counter本质上就是一个带使能、带复位的计数器它输出的二进制数就是ROMRead-Only Memory的地址线输入。每个时钟上升沿PC加1指向下一条要执行的指令地址ROM拿到这个地址后从对应存储单元里把指令数据送出来这就构成了CPU取指阶段最核心的“地址→指令”通路。下面我按自己的实操顺序来写而不是按教科书顺序。1. 程序计数器PC在CPU取指链路里的位置先别急着写代码很多同学第一次做这个实验会直接打开Quartus II开始画原理图或者敲Verilog结果做到一半才发现PC为什么需要使能、为什么需要复位、ROM地址线宽度到底该取几位这些问题全没想清楚。所以我还是建议动手前先花半小时把下面这个数据流在草稿纸上画一遍。PC的输出接ROM的地址总线这是本实验的核心连接。我们可以把ROM想象成一张带编号的格子纸每个格子有一个地址编号里面存了一条指令的二进制编码。CPU要执行程序就必须按顺序知道“现在该取哪个格子里的内容”这件事就是PC干的。PC初始值通常是0也就是从ROM的0号地址开始取第一条指令每取完一条PC自动加1下一拍指向1号地址、再下一拍指向2号地址如此反复。所以PC的硬件本质是“模2^n计数器”n就是地址线位数。如果你用4位地址PC就从0数到15正好覆盖16个ROM地址如果用8位地址就能覆盖256个地址。这里有一个容易忽略的知识点PC的更新节奏和指令执行的节奏是对齐的。在单周期处理器里每个时钟周期完成“取指→译码→执行”中的一段或全部PC在每个时钟上升沿加1。但设计里往往还会加一个使能信号常写作CE或PC_EN作用是让PC“暂停”。比如CPU遇到停机指令或者流水线阻塞就把使能拉低PC停止递增这样程序就不会继续乱跑了。我见过不少同学把PC做成普通计数器没加使能后面做到流水线实验时又回来改代码非常折腾。另外必须确定复位的极性。Quartus II实验板上常用的复位按键有高有效也有低有效取决于板子设计。本实验我用的是异步复位、低电平有效rst_n这也是Altera系板子和IP核里最常见的约定。异步复位的好处是复位信号一拉低PC立刻清零不需要等时钟沿这对上板调试时“按一下按键马上回到初始状态”非常友好。后面modelsim仿真里如果看到PC输出是红线x状态十有八九就是复位没做好或者没做复位。把这三个问题想清楚你在Quartus II里要建什么模块、模块端口有哪些就基本定下来了。我给出这个实验典型的端口定义clk系统时钟本实验仿真时可设为50MHz周期20nsrst_n异步复位信号低有效复位时PC清零ce计数使能高有效拉高时PC正常加1拉低时保持addr[3:0]PC输出的4位地址直接连到ROM的地址端口data[7:0]ROM读出的8位指令数据在仿真里可以直接观察它随地址变化这里地址线和数据线的宽度选择没有死规定只是为了让实验规模适中。你要是想模拟更复杂的指令集把地址做到8位、数据做到16位也行。重要的是把它们的关系在顶层框图里画清楚后面所有代码都是这个框图的翻译。2. Quartus II里的两种实现路线纯Verilog还是原理图混搭进了Quartus II之后第一个选择就是用原理图输入还是用硬件描述语言输入。我个人的看法是如果你只是想快速跑通本实验、重点验证PC取指的逻辑那就老老实实写Verilog然后把PC和ROM在顶层用Block Diagram连起来这两条路线可以同时存在因为Quartus II允许在同一个工程里混合使用原理图文件和Verilog文件。先讲纯Verilog路线。这种做法最简洁、最容易仿真也方便以后复用。我习惯建一个顶层文件top.v在里面例化PC模块和ROM模块PC的addr输出直接作为ROM的addr输入。初学者容易踩的坑有两个一是模块例化时端口名字没对上编译报错二是在ModelSim里仿真时一开始忘记加载顶层文件导致只看到PC在跑、ROM输出全是高阻z。所以顶层文件命名和你的工程名保持一致默认作为顶层实体例化端口用.名字(信号)的显式连接方式不要用位置连接可读性和出错率完全不一样。再说原理图混搭路线。Quartus II的原理图编辑器Block Diagram/Schematic File对于扩总线、放寄存器、看信号连接非常直观适合做课程设计报告里的截图。你可以新建一个.bdf文件从库里的primitives里拉出tff或者74161这类计数器元件搭一个PC再调用memoria里的ROM IP核用导线把它们连起来。这种方式的好处是“所见即所得”但修改起来远不如改几行代码方便尤其在改地址宽度、换复位极性这种需求上。我个人建议顶层用原理图展示底层PC和ROM都写成Verilog模块两者结合既好看又好改。后面第4节我会给出完整可跑的Verilog代码你大可直接抄。Quartus II新建工程有个细节要提前处理在“Family Device Settings”里选对FPGA型号。如果是纯仿真跑modelsim不打算上板那你选个Cyclone IV系列的任意型号就行比如EP4CE6E22C8这个型号在实验室里很常见如果只是教学演示甚至可以选Cyclone II。选错型号不影响仿真但影响引脚分配如果你后面要下载到板子上最好先问清楚你们实验室板子的具体型号。设置仿真工具的位置在Assignments - Settings - EDA Tool Settings - Simulation这里要选择ModelSim-Altera语言选Verilog HDL。这一步容易被忽略但漏掉的话后面Quartus II不会帮你自动生成ModelSim仿真需要的映射库。工程建好后还有个务必养成的习惯文件夹里不要出现中文名和空格。Quartus II对中文路径的支持一直很勉强ModelSim更是经常因为路径里有中文直接报“cant open simulation library”。当初我图省事把工程建在“桌面/计算机组成实验/实验2”下面结果编译是过了一进ModelSim就跑不起来最后把整个工程挪到D:\eda_lab\pc_rom才解决。这种问题不报错的时候什么都好一报错就是折腾半小时起步。3. PC模块和ROM模块的Verilog实现细节3.1 程序计数器PC的完整代码先看PC模块。这个模块麻雀虽小五脏俱全包含了同步置数、异步复位、计数使能三个基本要素。代码很简单但每个信号的意义要想清楚。module pc ( input wire clk, input wire rst_n, input wire ce, output reg [3:0] addr ); always (posedge clk or negedge rst_n) begin if (!rst_n) addr 4b0; // 异步复位清0 else if (ce) addr addr 1b1; // 使能有效时递增 else addr addr; // 使能无效时保持 end endmodule这段代码里有两个容易让新手摸不着头脑的点。第一为什么用reg类型、为什么放在always块里因为addr必须是一个“寄存器输出”它要记住上一个时钟周期的值只有在时钟沿到来才更新。第二为什么复位要在敏感列表里专门列出来因为这里写的是异步复位rst_n一旦变为低电平无论clk是什么状态addr立即变成0。如果你把敏感列表写成always (posedge clk)那就是同步复位复位信号必须等时钟上升沿才会生效。两种写法都能用但仿真时序不一样。我习惯用异步复位因为和FPGA内部触发器的全局复位端天然匹配而且我在仿真时习惯先拉低rst_n几个周期再释放这样PC从0开始计数的过程一目了然。这个PC模块只实现了“加1”操作。严格来说程序计数器还应该有“跳转”功能比如CPU执行到jmp指令时PC要更新为目标地址而不是加1。本实验先不加跳转专注把“顺序执行”这条路跑通。如果你学有余力可以给PC加一个load_en和load_data端口在使能逻辑里加一个分支为后面做跳转指令做准备这是一个很好的扩展方向。3.2 ROM的设计用Case语句仿真最简单用MIF文件最贴近工程ROM是整个实验的“数据提供者”。这里要先澄清一个概念在FPGA中真正的ROM是用FPGA内部的查找表LUT或者块RAMBlock RAM实现的不是像课本那样画一个带地址线和数据线的独立芯片。但逻辑行为完全一样给定地址输出该地址中预存的数据。在仿真和综合中ROM有两种常见的建模方式。第一种直接在Verilog里用case语句穷举地址和数据这种方式不改任何工程设置就能仿真最适合本实验快速验证PC和ROM的配合关系。缺点是ROM容量一大case写起来就要命。下面对应16个地址、每地址存1个8位数据module rom ( input wire [3:0] addr, output reg [7:0] data ); always (*) begin case (addr) 4h0: data 8h00; 4h1: data 8hA5; 4h2: data 8h3C; 4h3: data 8hFF; 4h4: data 8h10; 4h5: data 8h20; 4h6: data 8h30; 4h7: data 8h40; 4h8: data 8h50; 4h9: data 8h60; 4hA: data 8h70; 4hB: data 8h80; 4hC: data 8h90; 4hD: data 8hA0; 4hE: data 8hB0; 4hF: data 8hC0; default: data 8h00; endcase end endmodule注意这里data用了reg类型但其实它是组合逻辑。在Verilog里always (*)块内被赋值的信号必须声明为reg哪怕综合出的不是寄存器而是组合逻辑。很多初学者在这里会把data定义成wire结果编译报错属于典型的基础坑。还有一点default分支一定要写否则地址超出列举范围时data会锁存成之前的电平或者变成x这在仿真中会表现为一条难看的红线。第二种方式是用Quartus II自带的MegaWizard生成一个ROM IP核并用MIFMemory Initialization File文件预置内容。这种方式更接近真实工程因为ROM在FPGA里最终还是要靠BRAM实现而且仿真时能看到一个完整的初始化过程。MIF文件长这样DEPTH 16; WIDTH 8; ADDRESS_RADIX HEX; DATA_RADIX HEX; CONTENT BEGIN 0 : 00; 1 : A5; 2 : 3C; 3 : FF; 4 : 10; 5 : 20; 6 : 30; 7 : 40; 8 : 50; 9 : 60; A : 70; B : 80; C : 90; D : A0; E : B0; F : C0; END;MegaWizard生成rom核的时候会让你选择地址线和数据线宽度按4位地址、8位数据设置就行然后在“Memory Initialization”那一步指向你的.mif文件。这个IP核生成的顶层会带一个clock信号和clock enable信号和PC的配合稍微绕一点。因为同步RAM架构下地址变化后要等下一个时钟上升沿数据端口才会稳定输出也就是说ROM读数据比PC输出地址滞后一拍。这一点在波形分析上非常关键PC在0号地址时ROM输出的还是上一个地址的数据或者初始数据PC跳到1号地址后ROM输出才变成0号地址的内容。如果你用的是case写的组合ROM则没有这个问题地址一变数据立刻跟着变波形上看起来两者同拍变化。哪种更接近真实CPU其实同步读ROM更接近现代处理器设计但本实验做概念验证组合ROM更直观。两种方式在顶层连接上没有区别都是把pc.addr连到rom.addr。我现在做教学演示时通常先用case版让同学们看清原理再用MIF加IP核演示如何在软件中快速实例化一个带初始化内容的存储器两个坑都会踩一遍反而记忆更牢。3.3 顶层模块把PC和ROM焊在一起最后是顶层模块top.v它的作用就是实例化上面的两个块把PC输出的4位地址接到ROM的4位地址输入上同时把ROM输出的8位数据引到顶层输出方便仿真时观察。代码如下module top ( input wire clk, input wire rst_n, input wire ce, output wire [3:0] pc_addr, output wire [7:0] rom_data ); pc u_pc ( .clk (clk), .rst_n (rst_n), .ce (ce), .addr (pc_addr) ); rom u_rom ( .addr (pc_addr), .data (rom_data) ); endmodule这里有一个经常被问到的点pc_addr既是PC模块的输出又是ROM模块的输入在顶层里它应该定义为wire还是reg答案很明显因为PC的输出端口类型是output reg但它在顶层里作为内部连线存在顶层声明它时要用wire。模块的output reg表示“这个模块内部的信号是寄存器类型”并不影响它在上一层作为连线使用。类似的还有rom_data它在顶层里是output wire在rom模块内部是output reg。弄清楚这个你就不会在例化时报“cannot be driven by a register”或者“incompatible declaration”了。顶层文件是整个工程的“脸面”Quartus II会把和工程名同名的顶层模块作为最高层。如果你的工程名叫pc_rom那么顶层模块名最好也是pc_rom或者手动在Settings里指定顶层实体否则编译时软件会抱怨找不到顶层或者选错模块。我见过一个特别搞笑的错误工程名叫pc_rom但顶层模块名写成pc_rom_top结果Quartus把pc模块当成了顶层编译一通过进ModelSim仿真发现只有PC没有ROM折腾半天才发现是顶层实体设置的问题。至此一个可在Quartus II里完整编译、综合的PC产生ROM地址工程就成型了。下一步是仿真这也是本实验真正容易卡住的地方。4. ModelSim仿真testbench怎么写才能把PC递增过程看得清清楚楚仿真没跑通前面的设计就只能算完成了一半。本实验的仿真任务说白了就是验证两件事第一PC在时钟驱动和复位释放之后是否从0开始逐周期加1第二ROM能否根据PC给出的地址在对应输出端口上送出正确的数据。要验证这两点我们需要写一个testbench产生时钟和复位然后例化顶层模块让仿真时间跑几百纳秒最后在波形窗口里观察信号。4.1 testbench代码的骨架我给出的testbench如下这个写法是为本实验专门定制的最小可用版本timescale 1ns/1ps module tb_pc_rom; reg clk; reg rst_n; reg ce; wire [3:0] pc_addr; wire [7:0] rom_data; top u_top ( .clk (clk), .rst_n (rst_n), .ce (ce), .pc_addr (pc_addr), .rom_data (rom_data) ); initial begin clk 1b0; rst_n 1b0; ce 1b1; #25 rst_n 1b1; // 先保持复位25ns再释放 #500 $stop; end always #10 clk ~clk; // 20ns周期对应50MHz时钟 endmodule仿真时间设置为525ns足够观察PC从0跑到15之后再回到0的完整循环。你可能会问为什么复位只维持25ns因为PC是异步复位rst_n一拉高下一个时钟上升沿约第35ns处PC就立刻加1所以复位时间不用太长。这里我们故意让复位信号和时钟边沿错开不形成严格的建立/保持关系就是想在波形上观察复位后PC在第一个上升沿就跳到1的效果也顺便检验异步复位的“只要rst_n低不等时钟也能清零”特性。ce信号在初始块里被直接拉高表示PC从头到尾一直允许计数。如果后面你做了带跳转的PC可以把ce做成动态信号比如在第200ns拉低几个周期观察PC是否暂停这是不错的仿真扩展笔记。4.2 ModelSim的操作步骤踩过的坑都标在这里写完了testbench保存为tb_pc_rom.v然后打开ModelSim。如果你用的是Quartus II 13.0/13.1自带的ModelSim-Altera可以直接从Quartus里Tools - Run Simulation Tool - RTL Simulation打开它会自动加载当前工程的所有源文件。但独立版ModelSim操作起来更通用我们按下面的流程走新建Project命名随便比如pc_rom_sim存放路径最好和Quartus工程在同一级目录但不要放在同一个目录避免编译后的库冲突。把top.v、pc.v、rom.v、tb_pc_rom.v四个文件全部添加进工程。这里要注意如果你在Quartus里也把几个文件综合过一遍再拿到ModelSim单独编译顶层模块的路径和文件顺序不重要但文件一定要全部加进来漏一个都会导致编译报错。点击Compile All看Messages窗口有没有error和warning。编辑代码后重新编译旧模块会连带重新编译一般不会有问题。在Library窗口里找到work库展开它右键点击tb_pc_rom选择Simulate。注意模拟的顶层必须是testbench模块而不是top模块否则仿真里没有时钟信号驱动波形全是常量/红x。打开Wave窗口用“Add Wave”或者直接在Objects窗口把pc_addr、rom_data、clk、rst_n、ce拖进波形窗口。在命令行或者用菜单运行run -all或者run 700ns。如果只想跑一部分可以run 300ns。走完这六步你应该能看到和预期几乎一致的波形下面是关键观察点在0到25ns区间rst_n为低pc_addr应该是0rom_data根据组合ROM的case语句应该是0x00。第25ns释放复位后第一个时钟上升沿大约第35ns处pc_addr变为1同时rom_data变为0xA5。因为我们用的是组合ROM数据和地址同拍变化波形上是同一个沿同时变化。此后每个20ns周期pc_addr加1rom_data跟着变成地址对应的值。一直到pc_addr变为0xFrom_data为0xC0再下一个时钟周期pc_addr回绕到0rom_data回到0x00。这就验证了PC“模16计数”和ROM“按地址输出”两个行为都正确。4.3 仿真波形里最常出现的三种红线原因和解决仿真总是顺利才怪ModelSim里最劝退新手的现象就是波形窗口里一片红线或者高阻线。结合本实验我总结三种最常见的异常以及它们在本设计里的根源第一种pc_addr和rom_data全是红线x。这种情况几乎可以肯定是复位没生效。检查testbench里rst_n初始值是否为0是否在initial块里显式拉低检查PC的always块敏感列表里有没有把rst_n写进去。异步复位的代码里如果敏感列表漏写了negedge rst_nModelSim会把它当同步复位综合可testbench里rst_n和clk并不同步那么复位可能压根没被PC识别寄存器初始值就是x。修复方法很简单补齐敏感列表或者干脆给reg信号加初始值。第二种pc_addr规律变化但rom_data始终是6bz或者x。这是ROM模块没被正确编译或ROM内部没有default分支导致的。在组合ROM的case语句里如果地址范围没覆盖全比如只写了0到13漏了14和15那漏掉的地址对应的data就是x。另外如果rom_data是z高阻往往不是ROM代码本身的问题而是顶层例化时把rom_data声明成了wire但忘记让它从rom输出引出或者例化时端口名拼写错误。这时候逐个检查例化语句的端口连接比看波形快得多。第三种pc_addr从0跳到某一个大数比如跳到8或者跳到F并不是每个周期加1。这个现象通常不是PC代码错了而是ce信号有毛刺。仿真时ce被拉高理论上不会跳但如果ce在initial块中用了#号延迟反复赋值或者你给ce也分配了非整数时间片波形上可能看不出毛刺但寄存器采样值的时序已经乱了。解决办法在testbench里不要边延时边翻转ce让它保持唯一一次赋值或者用一个单独的always块按整数时钟周期翻转。这三种情况我在带学生实验时都见过前两种最普遍第三种是我自己在仿真一个多周期CPU时被坑过一把才发现的。要记住一个原则仿真波形显示的是离散的寄存器采样点有些毛刺和组合逻辑竞争并不会直接显示出来如果你发现PC的行为“莫名其妙地错”先查testbench的激励信号再看顶层连接最后才怀疑PC代码顺序不要反。5. 地址回绕与扩展实验从PC到完整取指模块仿真跑通后你已经完成了本实验的“规定动作”。如果还觉得不过瘾或者课程设计要求更高我强烈建议做下面这三个扩展之一它们都在本实验的思路上只增加少量代码但理解深度完全不同。5.1 把PC改成支持跳转的版本最简单的跳转是“载入并保持”增加一个load_en和load_data端口当load_en有效时PC在时钟沿把load_data的值赋给addr否则继续加1或保持不变。这样后面你设计指令系统时jmp指令译码后就可以把目标地址写到load_data上再拉一个周期的load_enPC就“跳”过去了。代码改动很小核心只在else分支里加一层else if (load_en) addr load_data; else if (ce) addr addr 1b1;注意跳转通常和使能的优先级load_en应该高于ce因为即便是暂停状态下遇到跳转指令也得先跳过去。这属于一个比较细的控制优先级设计体现了CPU控制信号的设计思想。5.2 把ROM换成RAM加上write_enable信号本实验用的是只读的ROM地址变化后数据直接输出非常简单。但真实CPU里数据和指令常常放在可读写存储器RAM里。把ROM换成RAM只需要在rom模块里增加一个写使能we和写数据din端口并用一个同步写逻辑always (posedge clk) begin if (we) mem[addr] din; else data mem[addr]; end这样你的顶层就不再是只能“取指”的只读通路而是具备“读写寄存器/存储器”能力的通路往后续的CPU设计就顺理成章了。值得注意的是这里RAM读出的数据滞后一拍波形上会有明显差异正好可以用来体验同步读和异步读的区别。5.3 将PC的地址从单周期加1改成流水线取指如果你的实验进度快已经在学习流水线那这个扩展非常有意思用两个寄存器分别存“当前PC”取指地址和“下一PC”让地址在fetch和decode两个阶段之间打一拍。这不一定要在本工程里实现但你可以尝试在testbench里额外加一条从rom_data译码出来的“跳转指令”信号反馈到PC的load_en上形成一个闭合的“取指→译码→跳转”通路。这个设计一旦调通你手里的就不再是一个单纯的PC驱动ROM而是一个迷你但完整的“取指跳转”单元。我个人觉得这是从“做实验”到“设计CPU核心”之间最有价值的一步过渡。5.4 在ModelSim里批量跑多组激励最后再分享一个提高仿真效率的小技巧。如果你改了好几组ROM数据想在ModelSim里连续仿真不同配置不需要反复改testbench。你可以用ModelSim的命令行do脚本把多个run和add wave命令写在.do文件里每次改完ROM源文件后重新编译再“do run_sim.do”ModelSim会自动执行全部仿真并刷新波形。这个习惯我从做验证工程师开始就保持到现在比每次手动点run高效得多。本实验虽然简单但提前把这个流程跑熟对后续做uart、vga、sdram这些大工程仿真会很有帮助。6. 结语前的一些进阶注意事项全流程走完后你会发现本实验并没有“特别难”的步骤真正难的是建立一种“从时序图反推实现”的思维。你现在应该能看着波形图说清楚“为什么在第35ns处pc_addr才从0变成1”也能解释“为什么组合ROM的数据随地址同拍变化而同步ROM要滞后一拍”。这些细节是无论写多少篇博客都替代不了的实战体会。在整个实验过程中我还想额外强调这几个容易被忽略但又很重要的细节它们也是我在多个实验室里指导学生时反复提醒的点第一Quartus II和ModelSim版本匹配问题。Altera系工具链里ModelSim-Altera通常跟随Quartus版本一起安装版本号要能对得上。如果你电脑上装的是Quartus 13.0配ModelSim-Altera 10.1d基本没问题。刻意去装最新版ModelSim反而可能因为license或者库文件不兼容出现“Error: Cant launch ModelSim”这类问题。我是建议直接用Quartus自带的开发套件里的ModelSim版本省去大量环境配置的时间。第二工程和路径命名一定要避免中文和空格。这点我说过一遍但值得再强调一次因为它太容易出问题又太容易被人忽略。宁可文件名难看了点也不要放中文路径否则大概率会在仿真库编译阶段出现莫名其妙的问题而问题定位过程会让你怀疑人生。第三仿真之前先在Quartus里做一次全编译。我见过不少同学在ModelSim里编译源文件时才发现语法错误但Quartus也有一套独立的语法检查机制。先在Quartus里Analysis Synthesis一遍能让大多数语法错误提前暴露而且还能生成一个网表文件方便你确认顶层模块没有命名冲突。做完这一步再进ModelSim仿真阶段的报错会少很多。第四理解testbench本身也是“设计”的一部分。仿真通过不代表电路正确只代表“在给定激励下的行为符合预期”。你给的时钟频率、复位时序、使能翻转时机都会影响仿真结果。本实验里复位释放时间和$stop时间都可以调试着把rst_n释放时间改为第50ns你会看到第一个PC加1发生在第55ns左右也就是异步复位释放后的下一个时钟沿这就更直观地验证了“PC更新需要时钟沿”这个基本事实。本实验做完后你手里已经有了一块非常坚固的积木一个带复位和使能的PC以及一个由它地址驱动的ROM。下一步无论是扩展成支持跳转的完整取指模块、还是继续往上搭寄存器堆和ALU这个基础模块都能原样复用。我给后续学习者的建议是别急着换实验板或者换工具链先把今天这个通路在波形图上彻底看明白再继续下一个实验这样整套计算机组成原理的实验做下来会非常顺。
返回列表