计算机组成实验 P0 到 P2 回顾
在写下这篇博客的时候,CO 课程已经结束了。在此回忆并记录一下计组实验过程中那些难忘的经历。
P0
P0 要求我们掌握 logisim 的基本用法,并且要会在 logisim 中实现摩尔型状态机和米莉型状态机。
关于 logisim 的基本部件,我在学习的时候主要参考查阅了以下博客:
这几篇博客对基本部件的基本功能叙述基本齐全,在学习 logisim 的时候给了我很大帮助。
在此补充一下关于 ROM 和 RAM 部件的使用方法:
ROM
ROM 是只读存储器。在我们的课程中主要用于 P3 中指令存储器的设计。
侧边栏功能介绍:
- Address Bit Width:这是地址位数,决定 ROM 的容量。
- Data Bit Width:这是数据的位数。
- Contents:点击这里来编辑 ROM 中的内容。
编辑 RAM 中的内容需要打开文本文件。点击 Contents 后页面如下所示:
点击 Open 可以选择载入 txt 中的内容。点击 Save 可以将 ROM 中的内容保存在本地文件中。
logisim 对于载入 ROM 的文本有格式要求:首行必须是 v2.0 raw
,并且以十六进制格式导入数据。
比如我上面那个 Contents 示例就是将以下内容导入到 ROM 之后的结果。
1 | v2.0 raw |
RAM
RAM 是随机存储存储器。其 Address Bit Width 和 Data Bit Width 的意义和 ROM 相同。Data Interface 可以改变 RAM 的存取端口设置。
在 P3 中,一般采用 Separate load and store ports 设置(分离的加载和存储端口)。
此外我们可以右键 RAM 模块,来编辑或清空 RAM 模块的内容。编辑 RAM 模块内容的方法和 ROM 相同。
状态机
要好好理解摩尔型状态机和米莉型状态机的区别,明确实现方法。
摩尔型状态机
一言以蔽之,输出只取决于当前状态,次态由当前状态和输入共同决定,在时钟上升沿来临时更改状态。
米莉型状态机
与摩尔型状态机不同的地方在于输出由当前状态和输入共同决定。这意味着,当输入改变时,即使时钟上升沿没有到来,输出也应该随着输入的改变而做出相应的变化。
同步复位与异步复位
异步复位的做法十分简单,直接将复位信号连接到 register 的 reset 端口即可。
同步复位则稍微复杂一些。给出一个往届的方法:
状态转移
设计状态机的过程中最重要的一点是设计好状态转移的过程。 logisim 中可以根据真值表自动生成电路。这个方法在计组的教程中讲的已经十分详细了。在我们设计的状态机的状态位数和输入位数之和不太多的情况下,可以利用这一功能快速生成状态转移模块。
具体做法如下:
因为按照真值表生成的电路的输入输出端口只能是一位的,故我们要将状态转移模块的输入拆分开,输出合并起来。上图中 register 中存储现态, now 代表现态输出,in 代表输入,next 代表次态。这个电路中的 MUX 起到了同步复位的作用。
其中状态转移模块内部就是由 logisim 根据真值表自动生成的电路。我们不需要关心内部具体是什么样子的,只需要保证我们的转移逻辑和拆分、合并信号时连线没有错误就可以。
编辑模块外观
logisim 默认的模块外观总是比较丑陋的…我们可以点击这个摁钮来编辑模块的外观:
我们可以编辑模块的形状、端口位置、添加文字说明等等。通过编辑模块的外观我们可以大大美化电路,这在 P3 中尤为明显(或许不是十分的重要)。
合理运用 Tunnel
Tunnel 是尤为常用的一个部件。合理使用 Tunnel 能大大简化我们的布线,让模块整体更加美观。但是不建议滥用 Tunnel。滥用的缺点是让电路更难看懂(因为不能直观的看到连线情况)。
一个 logisim 的 bug
我在 P0 上机的时候遇到了一个 logisim 的 bug:所有的连线全部是蓝色 的。遇到这种情况首先要检查 Simulate 设置中的 Simulation Enable 选项,这个选项应该是打开的;然后在保存后直接重启 logisim 就可以恢复啦!
P1
在 P1 中我们要掌握 Verilog 的基本语法,学会组合逻辑和时序逻辑,并且要会在 Verilog 中写状态机。
常数的写法
verilog 中常数的写法是 <常数位数>'<类型><值>
。教程里说的很详细,这里不再多说。
主要是想提醒大家在写代码的时候每一个数字都要遵循 <常数位数>'<类型><值>
的形式,不要没头没尾的写一个数字上去。一般而言,写 verilog 程序中我们接触二进制、十六进制数字的次数要比十进制数字的次数多得多。
阻塞赋值与非阻塞赋值
在初学 Verilog 时要区分阻塞赋值与非阻塞赋值。
阻塞赋值
阻塞赋值使用 =
。阻塞赋值可以理解为物理上的直接连线。当右边的值(驱动量)发生变化时,左侧值将立刻发生变化。建议只在组合逻辑中使用阻塞赋值。
阻塞赋值一般是赋值给 wire 型变量的。但是视情况也可以赋值给 reg 型变量和 integer 型变量。
非阻塞赋值
非阻塞赋值使用 <=
。非阻塞赋值会在一个块结束后统一赋值。比如下列代码:
1 | always@(posedge clk)begin |
以上代码的结果是在每一个时钟上升沿到来时交换 a 和 b 的值。非阻塞赋值建议只在 always 块内给 reg 型变量赋值使用(时序逻辑)。
组合逻辑
组合逻辑有两种写法,一种是使用 wire 和阻塞赋值(下称连线),一种是使用 wire 和 always@(*)
块内使用非阻塞赋值。我个人比较喜欢使用第一种写法。在进行条件判断时,要嵌套三目运算符 ? :
。
使用第一种写法时注意 wire 型变量只能连线一次。此外可以在定义 wire 型变量时直接连线。
1 | wire judge = input & 1'b1; |
上述代码的功能:当使能信号 en 为 0 时,ans 保持为 0,当使能信号为 1 且输入为奇数时, ans 为 1,否则为 2。
时序逻辑
时序逻辑使用 always 块和非阻塞赋值。建议一个模块只使用一个 always 块,并且在一个 always 内要保证每个时钟上升沿来临时对使用到的每个 reg 变量都有且仅有一次赋值。
在时序逻辑中可以使用 if-else 语句和 switch 语句进行条件判断。不再多说。
同步复位与异步复位
同步复位的写法:
1 | always@(posedge clk)begin |
异步复位的写法:
1 | always@(posedge clk or posedge reset)begin |
这两种复位方法都要掌握,在上机时一般都会考到。
状态机
在通过 P0 之后相信对两种状态机已经足够了解了。在 verilog 中实现两种状态机并不难。合理地将组合逻辑和时序逻辑组合使用便可以搭建两种状态机。在写状态机的时候最重要的还是设计状态和状态转移逻辑,编写代码只需要足够的细心即可。
在编写时序逻辑的时候要注意每一个 if 块都要有 else 作为结尾,在 else 内部编写 default 逻辑。
P2
在 P2 中,我们要掌握 Mars 的用法。Mars 的用法在教程里教的很详细,不再多说。这里主要是提几点建议和要注意的地方。
宏
写 Mars 程序时要合理地使用宏(.micro
)来简化代码。诸如读取数字、输出数字、数据的压栈、弹栈等都可以使用宏来编写。这里给两个压栈和弹栈的例子:
1 | # 数据压栈 |
递归
写递归的时候要注意数据的压栈和弹栈。在进入函数的时候压栈,在函数结束的时候弹栈。
一般使用 $a0-$a3 来进行函数的传参,使用 $v0-$v1 来接受函数的返回值。
Mars 文档
在 Mars 中 F1 键可以调出 Mars 帮助文档。在写代码的时候可以提供莫大帮助。在上机前建议弄懂字符串的读写,我这届有许多同学因为不了解字符串的读写而在 P2 上机的时候翻车。
上机
P2 上机写 Mars 代码个人感触最重要的还是细心。在 P2 中哪怕写错一个寄存器的名字也会寄。细心编写的代码能减少 $80$% 的 bug。我在上机时遇到的 bug 基本全都是因为粗心导致的。
在上机前可以提前在机房电脑上做一些准备,诸如调整机房里 Mars 的字体设置等;也可以背下来一些宏,提前敲进去。
一般而言 P2 的上机都是翻译 C 语言代码。如果他没有给出 C 代码,那就自己写一份 C 代码再翻译成 Mars 代码,能减少 bug 数量和 dbug 难度。在翻译代码的时候合理分配那几个寄存器的使用,个人翻译本身感觉难度不大。
结语
大致回忆了一下 P0 到 P2 的知识点和坑点。这几个 P 都是在为后面搭 CPU 打基础,因此难度不是很大。
欢迎在评论区讨论交流。