在写下这篇博客的时候,CO 课程已经结束了。在此回忆并记录一下计组实验过程中那些难忘的经历。

P0

P0 要求我们掌握 logisim 的基本用法,并且要会在 logisim 中实现摩尔型状态机和米莉型状态机。

关于 logisim 的基本部件,我在学习的时候主要参考查阅了以下博客:

Logisim元件用法详解一:Wiring 线

Logisim元件用法详解二:Gates 门

Logisim元件用法详解四:Arithmetic 运算器

Logisim元件用法详解三:Plexers 复用器

Logisim元件用法详解五:Memory 储存库

这几篇博客对基本部件的基本功能叙述基本齐全,在学习 logisim 的时候给了我很大帮助。

在此补充一下关于 ROMRAM 部件的使用方法:

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
2
3
4
v2.0 raw
3c1fff10
37ff00ff
0c000c00

RAM

RAM 是随机存储存储器。其 Address Bit WidthData Bit Width 的意义和 ROM 相同。Data Interface 可以改变 RAM 的存取端口设置。

P3 中,一般采用 Separate load and store ports 设置(分离的加载和存储端口)。

此外我们可以右键 RAM 模块,来编辑或清空 RAM 模块的内容。编辑 RAM 模块内容的方法和 ROM 相同。

状态机

要好好理解摩尔型状态机和米莉型状态机的区别,明确实现方法。

摩尔型状态机

一言以蔽之,输出只取决于当前状态,次态由当前状态和输入共同决定,在时钟上升沿来临时更改状态。

米莉型状态机

与摩尔型状态机不同的地方在于输出由当前状态和输入共同决定。这意味着,当输入改变时,即使时钟上升沿没有到来,输出也应该随着输入的改变而做出相应的变化。

同步复位与异步复位

异步复位的做法十分简单,直接将复位信号连接到 registerreset 端口即可。

同步复位则稍微复杂一些。给出一个往届的方法:

状态转移

设计状态机的过程中最重要的一点是设计好状态转移的过程。 logisim 中可以根据真值表自动生成电路。这个方法在计组的教程中讲的已经十分详细了。在我们设计的状态机的状态位数和输入位数之和不太多的情况下,可以利用这一功能快速生成状态转移模块。

具体做法如下:

因为按照真值表生成的电路的输入输出端口只能是一位的,故我们要将状态转移模块的输入拆分开,输出合并起来。上图中 register 中存储现态, now 代表现态输出,in 代表输入,next 代表次态。这个电路中的 MUX 起到了同步复位的作用。

其中状态转移模块内部就是由 logisim 根据真值表自动生成的电路。我们不需要关心内部具体是什么样子的,只需要保证我们的转移逻辑和拆分、合并信号时连线没有错误就可以。

编辑模块外观

logisim 默认的模块外观总是比较丑陋的…我们可以点击这个摁钮来编辑模块的外观:

我们可以编辑模块的形状、端口位置、添加文字说明等等。通过编辑模块的外观我们可以大大美化电路,这在 P3 中尤为明显(或许不是十分的重要)。

合理运用 Tunnel

Tunnel 是尤为常用的一个部件。合理使用 Tunnel 能大大简化我们的布线,让模块整体更加美观。但是不建议滥用 Tunnel。滥用的缺点是让电路更难看懂(因为不能直观的看到连线情况)。

一个 logisimbug

我在 P0 上机的时候遇到了一个 logisimbug:所有的连线全部是蓝色 的。遇到这种情况首先要检查 Simulate 设置中的 Simulation Enable 选项,这个选项应该是打开的;然后在保存后直接重启 logisim 就可以恢复啦!

P1

P1 中我们要掌握 Verilog 的基本语法,学会组合逻辑和时序逻辑,并且要会在 Verilog 中写状态机。

常数的写法

verilog 中常数的写法是 <常数位数>'<类型><值>。教程里说的很详细,这里不再多说。

主要是想提醒大家在写代码的时候每一个数字都要遵循 <常数位数>'<类型><值> 的形式,不要没头没尾的写一个数字上去。一般而言,写 verilog 程序中我们接触二进制、十六进制数字的次数要比十进制数字的次数多得多。

阻塞赋值与非阻塞赋值

在初学 Verilog 时要区分阻塞赋值与非阻塞赋值。

阻塞赋值

阻塞赋值使用 =。阻塞赋值可以理解为物理上的直接连线。当右边的值(驱动量)发生变化时,左侧值将立刻发生变化。建议只在组合逻辑中使用阻塞赋值。

阻塞赋值一般是赋值给 wire 型变量的。但是视情况也可以赋值给 reg 型变量和 integer 型变量。

非阻塞赋值

非阻塞赋值使用 <=。非阻塞赋值会在一个块结束后统一赋值。比如下列代码:

1
2
3
4
always@(posedge clk)begin
b <= a;
a <= b;
end

以上代码的结果是在每一个时钟上升沿到来时交换 ab 的值。非阻塞赋值建议只在 always 块内给 reg 型变量赋值使用(时序逻辑)。

组合逻辑

组合逻辑有两种写法,一种是使用 wire 和阻塞赋值(下称连线),一种是使用 wirealways@(*) 块内使用非阻塞赋值。我个人比较喜欢使用第一种写法。在进行条件判断时,要嵌套三目运算符 ? :

使用第一种写法时注意 wire 型变量只能连线一次。此外可以在定义 wire 型变量时直接连线。

1
2
3
wire judge = input & 1'b1;
wire [1:0]ans = en == 1'b0 ? 2'b00 :
judge == 1'b1 ? 2'b01 : 2'b10;

上述代码的功能:当使能信号 en0 时,ans 保持为 0,当使能信号为 1 且输入为奇数时, ans1,否则为 2

时序逻辑

时序逻辑使用 always 块和非阻塞赋值。建议一个模块只使用一个 always 块,并且在一个 always 内要保证每个时钟上升沿来临时对使用到的每个 reg 变量都有且仅有一次赋值。

在时序逻辑中可以使用 if-else 语句和 switch 语句进行条件判断。不再多说。

同步复位与异步复位

同步复位的写法:

1
2
3
4
5
6
7
8
always@(posedge clk)begin
if(reset == 1'b1)begin
//进行复位操作
end
else begin
//进行其他操作
end
end

异步复位的写法:

1
2
3
4
5
6
7
8
always@(posedge clk or posedge reset)begin
if(reset == 1'b1)begin
//进行复位操作
end
else begin
//进行其他操作
end
end

这两种复位方法都要掌握,在上机时一般都会考到。

状态机

在通过 P0 之后相信对两种状态机已经足够了解了。在 verilog 中实现两种状态机并不难。合理地将组合逻辑和时序逻辑组合使用便可以搭建两种状态机。在写状态机的时候最重要的还是设计状态和状态转移逻辑,编写代码只需要足够的细心即可。

在编写时序逻辑的时候要注意每一个 if 块都要有 else 作为结尾,在 else 内部编写 default 逻辑。

P2

P2 中,我们要掌握 Mars 的用法。Mars 的用法在教程里教的很详细,不再多说。这里主要是提几点建议和要注意的地方。

Mars 程序时要合理地使用宏(.micro)来简化代码。诸如读取数字、输出数字、数据的压栈、弹栈等都可以使用宏来编写。这里给两个压栈和弹栈的例子:

1
2
3
4
5
6
7
8
9
10
11
# 数据压栈
.macro stackPush(%num)
sw %num 0($sp)
add $sp $sp -4
.end_macro

# 数据弹栈
.macro stackPop(%num)
add $sp $sp 4
lw %num 0($sp)
.end_macro

递归

写递归的时候要注意数据的压栈和弹栈。在进入函数的时候压栈,在函数结束的时候弹栈。

一般使用 $a0-$a3 来进行函数的传参,使用 $v0-$v1 来接受函数的返回值。

Mars 文档

MarsF1 键可以调出 Mars 帮助文档。在写代码的时候可以提供莫大帮助。在上机前建议弄懂字符串的读写,我这届有许多同学因为不了解字符串的读写而在 P2 上机的时候翻车。

上机

P2 上机写 Mars 代码个人感触最重要的还是细心。在 P2 中哪怕写错一个寄存器的名字也会寄。细心编写的代码能减少 $80$% 的 bug。我在上机时遇到的 bug 基本全都是因为粗心导致的。

在上机前可以提前在机房电脑上做一些准备,诸如调整机房里 Mars 的字体设置等;也可以背下来一些宏,提前敲进去。

一般而言 P2 的上机都是翻译 C 语言代码。如果他没有给出 C 代码,那就自己写一份 C 代码再翻译成 Mars 代码,能减少 bug 数量和 dbug 难度。在翻译代码的时候合理分配那几个寄存器的使用,个人翻译本身感觉难度不大。

结语

大致回忆了一下 P0P2 的知识点和坑点。这几个 P 都是在为后面搭 CPU 打基础,因此难度不是很大。

欢迎在评论区讨论交流。