简介:这份教师版实验指导手册面向高校计算机专业《计算机组成原理》《计算机体系结构》等课程的实验教学,围绕龙芯 LS-CPU-EXB-002 教学实验平台展开,帮助教师组织从基础运算部件到完整 CPU 的递进式实验。压缩包内为 1 个 PDF 文件,大小约 6.09MB,内容按实验一到实验七编排,覆盖定点加法、定点乘法、寄存器堆、ALU、存储器、单周期与多周期 CPU,并延伸至静态 5 级流水线 CPU 课程设计及系统优化拓展。手册还附有 MIPS 指令集与 CP0 寄存器说明,便于对照硬件实现理解指令执行流程;目录层次清晰,实验目的、设备、任务与设计步骤相对完整,适合课堂讲授、实验布置和学生自主验证。目前已有 681 人学习,可作为相关课程实验教学与 CPU 设计入门的参考材料。
1. 第一次听说要做一台CPU,多数人第一反应是“我肯定做不出来”
很多人学完数字逻辑、学完一门硬件描述语言,成绩单也不难看,可一听到“CPU 设计与体系结构实验”这几个字,心里立刻发怵:芯片是工厂里流水线上做出来的东西,我一个刚入门的新手,在实验箱上拨弄几下就能做出来?这份《CPU 设计与体系结构实验指导手册》要拆掉的正是这道心理门槛。它不跟你谈大道理,而是把你按在实验台前,从一个 32 位加法器起步,一路走到寄存器堆、ALU、存储器、单周期 CPU、多周期 CPU,最后是静态五级流水线。适合刚学完计算机组成原理、还没真正碰过板子的零基础新手。读完这一篇,你至少知道第一周该干什么、第一个例子怎么跑通。
2. 先看清这台实验箱:FPGA、Verilog 和一块能点的屏
2.1 为什么这门课非要你亲手做一台 CPU
上组成原理课时,取指、译码、执行、访存、写回这几个词你能背得滚瓜烂熟,卷面也能拿分。可一旦问你“取指那一拍,地址是从哪个寄存器出来的,走哪条线进了存储器”,脑子里就是一片空白。这不是你笨,是因为课本上的 CPU 是一张静态框图,而真实 CPU 是一堆随时钟一起跳动的信号,中间隔着一条必须自己走一遍的沟。
这份手册对应的实验系统,一开始就把定位写清楚了:面向数字逻辑、计算机组成原理、计算机体系结构这几门课的实验教学,覆盖验证型、综合型、创新型三个层次。翻译成人话就是,它不指望你第一节课就设计出商用处理器,但要求你从“照着验证一个加法器”开始,逐步做到“自己搭出一个能跑指令的 CPU”。这个递进关系很重要,因为 CPU 设计最怕的就是跳步。你连一个 32 位加法器的进位都没搞清楚,直接去看流水线的冒险处理,那基本等于看天书。
为什么用 FPGA 来做这件事?说白了就一条:它能反复烧写。你在纸上画错一根线,擦掉重画就行;在真实芯片上设计错一根线,那就是几百万的损失。FPGA 相当于给你一颗可以随便改的芯片,今天写成加法器,明天擦掉写成 CPU,错了重来没有额外成本,这是新手最需要的那颗“后悔药”。
我见过不少同学第一次进实验室,看到板子上密密麻麻的排针就开始紧张,生怕碰坏了。其实这类教学板的接口都是按“学生天天插拔”设计的,你真正要小心的只有两件事:断电再插拔排线,以及别把电源适配器插到信号口上。剩下的,放心大胆地折腾。
2.2 实验箱里到底装了什么,每样东西干什么用
打开箱子,你能看到的配套资源是这几样:FPGA 实验板一块、厂商下载线一根、串口线一根(含 USB 转串口的小模块)、电源线加适配器一套。数量不多,但每一样都对应流程里的一个环节,缺一个都走不完。
下载线负责把电脑上综合出来的电路配置文件送进板子上的 FPGA 芯片,这是“你写的代码”变成“板子上真实电路”的唯一通道。串口线通常用于和板子通信、看打印信息或者做上位机交互,前几个实验用得少,但别扔,后面调试会用上。电源适配器不用多说,注意电压和接口方向就行。
真正让这套平台和普通开发板拉开差距的,是板子上那块 LCD 触摸屏,以及配套的硬件驱动设计。手册里专门强调了一句:这块屏不需要处理器核参与就能完成显示和输入,而且对外提供了简单清晰的调用接口。这句话的分量,等你自己动手就明白了。
设想一下,你要验证一个 32 位加法器。用最土的办法,输入两个 32 位数需要 64 个拨码开关,输出结果需要 32 个 LED 灯。教学板上根本给不了这么多 IO 口。所以传统做法要么把位宽砍到 4 位、8 位,做个缩水版意思意思;要么搞一套非常绕的按键输入逻辑,按半天才输进去一个数。有了触摸屏,你可以直接在屏幕上点数字,结果也直接显示在屏幕上——输入输出这一层被彻底解决了,你只需要专心对付加法器本身。
配套材料里,这块屏被封装成了一个黑盒网表文件,你在工程里调用它就行,看不到内部实现。有人会觉得这是“留一手”,其实这是教学上的刻意安排。屏幕驱动涉及时序、刷新、坐标换算一大堆和 CPU 设计完全无关的细节,新手要是钻进去,两周都出不来,主线全废了。把它当黑匣子用,是这个阶段最聪明的选择。
2.3 Verilog 不是“C 语言换个皮”,这一点必须先扭过来
几乎所有新手在这里翻过车。写惯了 C 的人,看到if、else、for就觉得亲切,然后写出这样的代码:先算 a 加 b,再把结果乘 c,最后判断。写完一仿真,结果全错。
原因在于,Verilog 描述的不是“先做什么、后做什么”,而是“这些东西之间怎么连、在时钟边沿怎么变”。C 语言里每句话是按顺序执行的;Verilog 里你写的每一条assign都是一根实实在在的导线,它们同时存在、同时生效。你写assign c = a + b;,不是“执行了一次加法”,而是“造了一个加法器,它的输出永远等于 a 加 b”。
这不是抠字眼。等你做到单周期 CPU 那一步,几十条控制信号在同一个时钟沿一起翻转,任何一点顺序思维都会让你彻底看不懂波形。所以从第一天写加法器开始,就应该养成一个习惯:写每一行代码前,先在心里问一句“这一行对应的是哪根线、哪个器件”。
还有一点,硬件描述语言里“仿真对”和“上板对”是两件事。仿真是在电脑里算逻辑,没有物理延迟;上板是电流在硅片里跑,有延迟、有竞争、有毛刺。前几个实验你还感觉不到这个差别,等做到多周期 CPU、流水线,这就是最常见的翻车点。这一点在第 5 章还会专门说。
3. 从第一个例子开始:32 位加法器的每一步怎么落地
3.1 建工程:那几个下拉框到底在选什么
第一个实验就是“数据运算:定点加法”,手册把它定位成软硬件平台的入门实验。目标很明确:熟悉实验箱、熟悉软件工具、理解加法器原理、把 Verilog 用起来,为后面设计 CPU 打地基。
打开集成开发环境,新建工程,一路点下去,会碰到几个关键选择。
第一处是工程类型,选 RTL 工程,并且勾上“暂不指定源文件”。为什么先不加文件?因为你要先让工具知道“这个工程是给哪颗芯片做设计”,它才能给你匹配正确的资源库。顺序反了会很麻烦。
第二处是器件选择,这是最容易随手点错的地方。你需要先在筛选器里选定板子上那颗芯片所属的系列和封装,再从列表里挑出实验室指定的具体型号。选错的后果不是报错,而是更气人的“综合能过、下载不进去”或者“引脚全红”。因为工具以为你在给另一颗芯片布局,引脚自然对不上。
提示:器件型号别凭记忆填,直接看实验室贴在板子上或者写在实验指导里的那串字符,一个字母一个数字地核对。
工程建好之后,工程管理区会显示模块之间的层次关系。这个层次视图后面会一直用到:谁是顶层、谁被谁调用、哪个模块还缺文件,一眼就能看出来。养成隔一会儿看一眼这个视图的习惯,比事后翻日志快得多。
3.2 第一个模块:加法器主体只有一行有效代码
新建或添加一个源文件,后缀是.v。这就是你第一个 Verilog 模块,内容如下。
`timescale 1ns / 1ps // 32 位定点加法器:两个操作数 + 一个低位进位 module adder( input [31:0] operand1, // 加数 1 input [31:0] operand2, // 加数 2 input cin, // 来自低位的进位 output [31:0] result, // 32 位加法结果 output cout // 向高位的进位 ); // 花括号是拼接:把 cout 和 result 接成 33 位一起赋值 assign {cout, result} = operand1 + operand2 + cin; endmodule先说端口。operand1、operand2都是 32 位宽,[31:0]就是从第 31 位到第 0 位。cin是一位输入,代表低位传上来的进位。输出这边result32 位,cout一位。
关键在那一行赋值。左边{cout, result}用花括号把两个信号拼成一个 33 位的整体,右边三个数相加,结果也是 33 位。加法一旦产生第 33 位,刚好落在cout上,剩下 32 位落在result上。如果不这么拼,只写assign result = operand1 + operand2 + cin;,那进位就被丢掉了,高位的溢出你永远看不到。
这里还有一个值得说的选择。手册里明确提到,加法器可以自己用全加器一级一级搭出来,也可以直接用加号让工具去调用元件库里的加法器——后者在面积和时序上通常还更优。这不是偷懒,是工程上的常规做法:能复用成熟单元就不要手搓。当然,如果老师要求你必须自己搭全加器链,那就老老实实搭,因为那个过程能让你真正理解进位是怎么一级一级传上去的。
写代码的时候注意保持端口名和后面外围模块、约束文件里用的名字一致。名字对不上,工具不会告诉你“你打错字了”,它只会告诉你“这个端口找不到”,然后你自己去翻几十行代码。
3.3 testbench:不给激励的仿真等于没做
代码写完不能直接上板,中间必须过仿真这一关。仿真需要一个测试平台文件,也就是 testbench。它的作用很朴素:给被测模块喂输入,让它算,然后你看波形对不对。
`timescale 1ns / 1ps module testbench; reg [31:0] operand1; // 输入用 reg,因为要在过程块里赋值 reg [31:0] operand2; reg cin; wire [31:0] result; // 输出用 wire,因为由被测模块驱动 wire cout; // 例化被测模块,端口按名字连接 adder uut ( .operand1(operand1), .operand2(operand2), .cin(cin), .result(result), .cout(cout) ); initial begin operand1 = 0; operand2 = 0; cin = 0; #100; // 先等 100ns,让全局复位走完 end // 每 10ns 换一组随机激励 always #10 operand1 = $random; always #10 operand2 = $random; always #10 cin = {$random} % 2; // 拼花括号得到非负数,再取余得到 0 或 1 endmodule几个点要说清楚。第一,输入端口声明成reg,输出声明成wire,这是规矩:你要在initial或always里给它赋值,就得是reg;被别的模块驱动的,就得是wire。写反了编译就过不去。
第二,#10是延时 10 个时间单位。文件头写了1ns/1ps,所以#10就是 10 纳秒。always #10的意思是“每隔 10 纳秒干一次”,于是三个信号每 10 纳秒刷新一次。
第三,$random产生的是带符号的随机数,直接拿来取余可能得到负数。外面套一层花括号{$random}就变成非负数,再% 2就稳定得到 0 或 1,正好当进位用。这种小坑不写出来,新手能卡一下午。
加进工程之后,记得把这个测试文件设为顶层,否则工具不知道该仿真谁。跑起来之后会弹出波形窗口。默认显示的是二进制,32 位数一长串 0 和 1,眼睛根本没法看。在信号名上右键,找到进制设置,改成十六进制,世界立刻清爽。
然后就是验算。比如波形上抓到一组:b897be71加42f24186,低位进位为 0,结果应该是fb89fff7;把进位改成 1,结果就变成fb89fff8。自己拿计算器按一遍,对上了,说明这个加法器是活的。随机激励跑一段时间都没出错,基本可以认为功能稳定。
注意:仿真通过只代表逻辑对,不代表能上板。这两件事后面会分得很开。
3.4 上板:约束文件把代码和引脚接起来
上板这一步,新手最容易糊里糊涂地“点过去了”,其实逻辑很简单:你写的顶层模块有一堆端口,板子上的芯片有一堆引脚,约束文件就是那张“谁接谁”的对照表。文件后缀是.xdc,内容大致长这样。
# 时钟和几个手动输入 set_property PACKAGE_PIN AC19 [get_ports clk] set_property PACKAGE_PIN A3 [get_ports led_cout] set_property PACKAGE_PIN Y3 [get_ports resetn] set_property PACKAGE_PIN AC21 [get_ports input_sel] # 电平标准,写错这一行引脚会报错 set_property IOSTANDARD LVCMOS33 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports led_cout] set_property IOSTANDARD LVCMOS33 [get_ports resetn] set_property IOSTANDARD LVCMOS33 [get_ports input_sel]每两行是一组:第一行说“这个端口焊在芯片的哪个物理引脚上”,第二行说“这个引脚用几伏的电平标准”。引脚编号必须查板子的原理图或实验室给的模板,千万别自己编;电平标准基本都是LVCMOS33,漏写一个,工具在布局布线阶段就会给你脸色看。
触摸屏那一堆信号(复位、片选、读写、数据线、背光、触摸中断等)同样要在约束文件里一一绑定。好消息是这部分实验室一般会给现成的模板,你只要保证自己顶层模块里用到的端口名和模板里的名字对得上就行。
约束齐了,就依次做综合、布局布线、生成配置文件、下载。这一串按钮点下去要花几分钟,第一次跑会觉得很慢,后面就习惯了。下载完成后,屏幕上能点数字、能看到加法结果,这个实验就算通了。
3.5 跑通之后回头看:这一步到底练了什么
很多人做完实验一只记得“我做了个加法器”,觉得太简单。其实这个实验真正的价值在流程:建工程、写模块、写测试、跑仿真、绑引脚、上板验证,这六步是后面所有实验的骨架。实验二到实验七,换的只是中间那个模块的内容,外围流程一模一样。
手册里还有一个细节值得留意:加法器是作为一个独立模块存在的,外面专门套了一个展示模块去调用它,展示模块里再去调用屏幕模块。这种“功能模块”和“展示模块”分开的做法,是从第一天就要建立的意识。等到你做 CPU 的时候,一个顶层下面挂十几个子模块是常态,如果一开始就把所有东西糊在一个文件里,后面改一处崩一片。
4. 七个实验加两个课程设计,顺序为什么不能乱
4.1 实验二到实验五:一块砖一块砖地攒零件
手册列出的实验顺序是:定点加法、定点乘法、寄存器堆、ALU 模块、存储器、单周期 CPU、多周期 CPU,然后是静态五级流水线课程设计和优化拓展。这个顺序不是按难度随手排的,而是按“CPU 由哪些部件组成”来排的。
加法器是最小的运算单元,也是你熟悉工具链的载体。乘法看起来难,本质上还是加法和移位:被乘数不断左移,根据乘数当前位决定加不加,最后累加出结果。做完乘法,你对“一个运算要拆成多少个时钟周期”会有第一次直观感受。
寄存器堆是 CPU 里存东西的地方,一堆 32 位寄存器排在一起,配两个读端口一个写端口,再加写使能。它的难点不在逻辑复杂,而在“什么时候能读、什么时候能写、写进去的东西下一拍能不能马上读到”这类时序规矩。这个实验做扎实了,后面写回阶段的数据通路你就不会晕。
ALU 是把加法器、逻辑运算、移位、比较这些功能打包到一块,前面加一个选择信号决定这一拍干什么。它是 CPU 的心脏,也是你第一次接触“同一个硬件按不同控制信号做不同事”的设计思路。
存储器实验通常把指令存储器和数据存储器分开做,涉及地址译码、读写使能、读写时序。到这里,CPU 的五大件基本凑齐了:运算的、存数据的、存指令的、控制流向的,再加一个时钟。
为什么不能跳?因为单周期 CPU 就是把这些零件用线连起来。你零件没做熟,连的时候根本不知道哪根线该从哪出来。
4.2 单周期到多周期:多出来的时钟周期花在哪
单周期 CPU 的思路特别直白:一条指令的所有动作,在一个时钟周期内全部做完。取指、译码、执行、访存、写回,全部压在一拍里。
这么做的好处是控制逻辑简单,一条指令对应一套固定的控制信号,几乎不用状态机。坏处也很明显:时钟周期的长度必须按最慢的那条指令来定。访存那条指令要等存储器返回数据,慢;加法指令压根不访存,却要陪着一起等。整台机器的频率被最慢的指令拖住了。
多周期 CPU 就是把一条指令拆成几步,每步一个时钟周期。取指一拍、译码一拍、执行一拍、访存一拍、写回一拍。好处有三个:时钟周期可以按最慢的那一步来定,比按最慢的整条指令定要短;硬件可以复用,比如存储器和 ALU 在不同周期干不同的事;控制信号从“一堆组合逻辑”变成“一个状态机”,什么状态发什么信号,逻辑清楚很多。
代价是 CPI 变大了,一条指令要好几拍。但每拍更短,总体算下来往往还是划算。
这个从单周期到多周期的转换,是很多人真正开窍的时刻。你会第一次意识到,CPU 设计的核心不是“能不能算对”,而是“在正确的时间把正确的数据送到正确的地方”。这个意识一旦建立,流水线就不难理解了。
4.3 课程设计:五级流水线到底在“流水”什么
静态五级流水线把一条指令切成五段:取指、译码、执行、访存、写回。关键在于,这五段不是串行等着的,而是像流水线一样同时开工——第一条指令在写回的时候,第二条在访存,第三条在执行,第四条在译码,第五条在取指。
同一时刻,芯片里有五条指令在不同阶段各干各的。这就是吞吐量提升的来源:虽然一条指令还是要走五拍,但每一拍都有一条指令完成,理想情况下每个时钟周期都能完成一条。
听着很美,做起来有三个绕不过去的麻烦。数据冒险:后一条指令要用前一条刚算出来的结果,可那个结果还没写回寄存器。控制冒险:分支指令跳不跳还没定,后面已经取进来的指令可能是错的。结构冒险:两条指令同时想用同一个硬件,比如都想访存。
应对办法也是三板斧:前推(把还没写回的结果直接送给需要的下一级)、停顿(插气泡,等一等)、分支预测(先猜一个方向往下走,猜错了再清空重来)。这三个词你现在看着陌生,等做到那一步,手册和课本会反复提。
课程设计拓展部分叫“优化 CPU 系统”,就是在能跑的基础上继续压性能:指令调度、资源分配这类话题。这一步不着急,先把五级流水线跑通再说。
5. 避坑与排查:新手最容易翻车的五个地方
5.1 波形窗口一片空白,或者全是 X
现象:仿真跑起来了,波形窗口倒是弹出来了,可信号要么是一条直线,要么全是红色的 X,改什么都一样。
原因通常有三个。一是测试文件没设成顶层,工具仿真的是那个空壳的顶层模块,自然啥都没有。二是initial块里只赋了初值,没给后续激励,信号一直停在 0。三是往波形窗口加了新信号却没重新载入仿真,屏幕上还是旧数据。
解决办法很机械:先看工程管理区里测试文件前面有没有顶层的标记,没有就右键设成顶层;再看initial块里#100之后有没有always在持续给激励;最后记住,仿真跑起来之后再加信号,必须重新载入一次才能看到新数据。这三步按顺序走一遍,九成的空白波形都能解决。
5.2 综合能过,下载进板子却毫无反应
现象:工具一路绿灯,配置文件也生成了,下载提示成功,可板子上该亮的灯不亮、屏幕上什么都不显示。
原因八成出在约束文件和顶层选择上。顶层模块选错了,工具就按错误的端口列表去绑引脚,你真正想用的那些端口根本没接出去。约束文件里的端口名和代码里的不一致,那一行绑定会被忽略,而工具默认不会因为少绑一个端口就中断流程。电平标准漏写,某些引脚会直接工作异常。
排查顺序建议是:先在工程里确认顶层模块名字,再打开约束文件,逐行核对你绑的端口名和顶层模块声明的端口名是否完全一致(大小写、下划线都算),然后检查每个端口是不是既有引脚绑定又有电平标准。这三项都对着,再去看板子上的复位键是不是按对了方向。
5.3 加法结果总是差一
现象:仿真时结果和手算对不上,仔细一看,永远比预期少 1,或者进位标志永远不亮。
原因基本是进位那条线断了。要么cin端口在例化时没接,悬空了;要么接是接了,但一直给 0;要么输出那边只写了result没把cout拼进去,进位算出来了却没地方去。
对应地解决:回到例化那一行,检查.cin(cin)有没有漏;回到测试文件,检查cin是不是真的在随机变化;回到加法器本体,确认左边是{cout, result}而不是单独的result。这三个地方挨个看一遍,这类问题一分钟就能定位。
5.4 仿真全对,上板就错
现象:波形上怎么看怎么对,下载到板子上结果就是乱的,或者偶尔对一次,重开又错了。
原因是仿真和上板根本不是一回事。仿真是理想环境,没有物理延迟;上板的时候信号在硅片里传播要时间,时钟到达不同触发器的时间也有先后。你的组合逻辑级数太深、时钟频率给太高,就会在板子上出问题。另一个常见原因是复位没做干净,上电瞬间寄存器里是随机的,你以为初值是 0。
解决办法:先确认复位信号真的接到了所有需要复位的寄存器上,而且复位持续时间够长;再把时钟频率降下来试试,如果降频就对了,说明是时序问题,得回去看关键路径。工具里有时序报告,别怕看,重点找那条“不满足”的路径,它通常会直接告诉你是哪一段逻辑太长。
5.5 代码抄下来了,一到下一个实验就崩
现象:第一个实验照着敲完,跑通了,感觉良好。第二个实验一上来,完全不知道从哪下手,只能继续找现成代码。
原因是你抄的是结果,没抄过程。第一个实验真正的产出不是你那个加法器,而是“建工程、写模块、写测试、看波形、绑引脚、上板”这一整套肌肉记忆。跳过任何一环,下一个实验就得从头再摸索。
比较靠谱的做法是,每做完一个实验,合上手册,自己从头再走一遍流程,中间卡在哪就说明哪块没吃透。特别是波形那一步,别只看“对了”,要能指着某一段说清楚这里为什么是高电平。这种自查很枯燥,但它是把“我跑通过”变成“我会做”的唯一路径。
6. 下一步学什么:把手册当路线图,而不是当答案册
七个实验加两个课程设计,看着多,其实可以分成三个阶段来安排节奏。
| 阶段 | 手上要完成的事 | 过关的标志 |
|---|---|---|
| 熟悉工具链 | 定点加法、定点乘法 | 能不看手册独立走完建工程到上板的全流程 |
| 攒齐零件 | 寄存器堆、ALU、存储器 | 每个模块单独仿真通过,且能说清端口和时序 |
| 拼成整机 | 单周期、多周期、五级流水线 | 能跑通一小段指令序列,并能解释每条控制信号的来源 |
第一阶段别贪快。我见过有人两天刷完前两个实验,觉得自己掌握了,结果做寄存器堆的时候连“写使能什么时候有效”都说不清楚。前两个实验的意义就是让你把工具用熟,把仿真和上板的差别摸出来,慢慢来反而快。
第二阶段要开始画图。做每个零件之前,先在纸上把端口列出来:输入有哪些、输出有哪些、位宽多少、什么时候有效。这张图不是交作业用的,是给你自己看的。图画不清楚,代码一定写不清楚。
第三阶段是最容易放弃的地方。单周期 CPU 的连线会多到让你怀疑人生,几十根线在顶层模块里穿来穿去。这时候别想着一次写完,按数据通路一段一段来:先把取指通路连起来,仿真看指令能不能取出来;再加译码;再打通执行。每通一段就验证一次,比全写完再调试省十倍时间。
最后一个建议,也是我自己的血泪经验:每做完一个实验,把当时的波形截图和一个简短的说明存到一个文件夹里。等你做流水线卡住的时候,回头翻这些记录,经常能发现自己当初是怎么解决某个时序问题的。这个习惯我从第一次上板出错之后就一直保持着,每次开新工程之前都强制翻一遍旧记录。
CPU 设计这门课,难点从来不在某一个知识点,而在“把一堆各自都对的小东西连成一个整体还对”。这份手册的价值,就是把这个过程切成了你能一口一口吃下去的小块。按顺序走完,你会发现所谓的“做一台 CPU”,不过是把七个熟悉的小模块,用一堆名字很唬人的线连起来而已。
希望帮到你。
本文还有配套的精品资源,点击获取