计算机组成 - Pre 反思

一、写在前面的话

首先,这道题目我并没有过,一共是应该六个左右的测试点,我最好的一次提交是还剩下 2 个测试点没有过,但是那次提交已经是属于脱离了规范,纯粹为了测试而写的,不是规范风格的代码。我在签退以后,虽然在群里面同学的帮助下(结尾有鸣谢名单),意识到自己代码存在的问题,但是新写好的代码也是没有经过评测机检验的。所以尽管我很想对大家负责,但是确实是客观条件不允许,我的代码要是运行出来 bug 了,请大家不要打我。

所以这篇文章,大家就看个乐呵就行了,要是真从其中咂吧出啥滋味来,那是诸位的功德,要是没有,也只是我的水平太低,与诸位无关。

我在文章里面只贴了一个 tb 模块的原码,主要是为了让大家看的明白一些,没有 cscore 的原码,跪求助教大大不要删帖。谢谢助教大大。


二、测试模块的写法

2.1 总论

这个东西也是我最近才接触,也没有做过大型电路的调试,所以可能现在很多方法和规范,到了大型电路的时候,或者现在,就很显得不那么优雅,所以也欢迎同学在评论区,介绍自己喜欢的调试方法,我会选择优雅的方法补充到我的文章中,并在鸣谢名单中加上名字的。

2.2 ISim 特性介绍

第一、如果想要让一个变量输出在 ISim 的观测变量栏中,只需要把它在 tb 模块中按照声明变量的方式声明就可以了。比如这样。

image-20211014141731307

尽管 test 和这道题目没啥关系,但是在模拟的时候,我们依然能看见他。因为没有与电路建立任何关系,所以它的值是高阻态。

image-20211014141954854

换句话说,所有在 tb 模块中声明的变量,都会被视为待观测变量

第二、对于观测量,是可以调整输出形式的,默认形式是二进制,但是这道题的 in 是串行字符串,所以修改为 ASCII 格式方便调试。

image-20211014142522601

第三、对于一位二进制,波形图是那种阶梯图,但是对于多位数,波形图就是条带图,就是说光看波形图,不看文字的话,是不知道它的值多少了,只能判断它在某个节点是否发生变化。以上图为例,clk 就是阶梯图,但是 status 就是条带图。

第四、ISim 只能同时运行唯一一个窗口,如果两次打开,会报错,所以要把第一个先关掉,再打开第二个。有的时候还是不行,显示 “previous” 的报错信息,这时只需在 tb 模块任意空白处加一个空格,再次运行即可。

2.3 与被测模块建立联系

ISE 主动提供这种服务,如果是先写电路模块,然后在生成测试模块,ISE 会自动为用户写好这种联系的代码,大概就是这样。

image-20211014143251581

这段代码的意思是,ISE 声明了跟你编写的待测试模块同名的变量(输入是寄存器类型,输出是线型),注意这里的重名易造成混淆。最后这段代码是说,把在 tb 模块上声明的变量连接到我们的待测试端口,这样才有意义(不然就会像之前的那个 test 变量一样,是高阻态)。

有的时候,我们需要知道模块的中间变量是怎样变化的,经两位助教哥哥的指正,我又学习了更加快捷的方法,只需要如下操作:

首先,需要现在 ISim 界面勾选 Instance and Processes 和 Obejects 两个选项。

image-20211014185747336

然后电机 Instance and Process 中的 tb -> uut (不用点开)。这时在 Object 中就会显示里面的变量,有 I(input),O(output),V(variable),C(constant)四种类型。我们选择 variable 类型的中间变量,将其拖拽或者 Ctrl + W 就可以加到右面栏里了。像这样:

image-20211014190329371

这时,有可能你的跟我一样,是会不显示的,所以就需要按这几个按钮:

image-20211014190612514

先按 1.00us 向左数第三个,再按 1.00us 向左数第一个。就可以显示了。(这个感觉很神奇,可能是个坑,要是有空一定更新。)

2.4 生成测试时序规范

我个人是习惯三个模块,第一个模块负责赋初值,然后完成复位操作(岔一句,因为 initial 是没办法综合,所以为了让写好的模块有初始值,必须要求模块有写复位操作,这样才可以借助 tb 来完成模块的赋初值操作),代码如下:

initial begin
clk = 1;
reset = 1;
in = 0;
#2
reset = 0;
end

这个模块完成了 tb 模块的赋初值和复位操作(就是被测试模块的赋初值)。一会一起解释延时的数值确定问题。

第二个模块用于产生时钟周期,代码如下:

always #5 clk = ~clk;

这是一个以 10 为周期的是时钟。

第三个模块用于生成测试数据,同样间隔也是 10。

initial begin
#7
in = "C";
#10 in = "S";
#10 in = "C";
#10 in = "O";
#10 in = "R";
#10 in = "E";
#10 in = " ";
#10 in = "c";
#10 in = "s";
#10 in = "c";
#10 in = "s";
#10 in = "c";
#10 in = "o";
#10 in = "r";
#10 in = "e";
#10 in = " ";
#10 in = "c";
#10 in = "s";
#10 in = "c";
#10 in = "o";
#10 in = "r";
#10 in = "e";
#10 in = "A";
#10 in = "c";
#10 in = "s";
#10 in = "c";
#10 in = "o";
#10 in = "r";
#10 in = "e";
end

最后解释一下时间问题,第一点就是 in 的变化周期应当与时钟周期相同,这点是寄存器的要求,只要变化周期小于时钟周期,就会有数据没有被录入(很好理解)。第二点是复位信号,复位信号最好除了复位,不进行任何干扰,所以尽量让他在半个时钟周期内结束,所以延迟时间是 1,2,3,4 都是可以的。第三点是 in 的边沿必须与时钟边沿不同,这是为了减少无谓的竞争,与时钟边沿相同的变化会造成很多的不方便(后面会有一个例子)所以第一个时钟延时是 7 而不是 10,是为了避免边沿重复

下面放一个完整 tb 代码,这个代码应该可以辅助调试,我列举了多种情况,要是过了,应该(不负责)评测机也能过。(我知道讨论区不能放原码,但是这个只是 tb 的原码,求求助教大大手下留情,不要删帖,后面不会有 cscore 的完整代码,只会有代码片段)。

module tb;
// Inputs
reg clk;
reg reset;
reg [7:0] in;

// Outputs
wire [7:0] out;

// Instantiate the Unit Under Test (UUT)
cscore uut (
.clk(clk),
.reset(reset),
.in(in),
.out(out)
);

initial begin
clk = 1;
reset = 1;
in = 0;
#2
reset = 0;
end

always #5 clk = ~clk;

initial begin
#7
in = "C";
#10 in = "S";
#10 in = "C";
#10 in = "O";
#10 in = "R";
#10 in = "E";
#10 in = " ";
#10 in = "c";
#10 in = "s";
#10 in = "c";
#10 in = "s";
#10 in = "c";
#10 in = "o";
#10 in = "r";
#10 in = "e";
#10 in = " ";
#10 in = "c";
#10 in = "s";
#10 in = "c";
#10 in = "o";
#10 in = "r";
#10 in = "e";
#10 in = "A";
#10 in = "c";
#10 in = "s";
#10 in = "c";
#10 in = "o";
#10 in = "r";
#10 in = "e";
end
endmodule

三、cscore 模块相关

3.1 题目思路

主要就是 cscore 中如果状态机进入到了读取完第二个 c 的状态时,如果再读 s 的时候,需要回到的状态是读取完 s 的状态,而不是不需要考虑,同时还有一个关键点就是,发生这个事情以后,还需要把当前计数器(我的代码里是 cnt 和 nextCnt)清零,因为这个算读到异常字符了,需要重新计数,我就是只考虑了第一点,没考虑第二点,等发现的时候,已经来不及了。

此外,需要强调的是,这篇题解的思路是按照一种工业开发自动机(好中二的名字)的规范写的,这个风格我会在下面介绍,这种风格在教材中也有介绍,所以可能与诸位的方法有所差异,我本人也是上机前才接触,所以用的不是很熟练,导致最后改代码没有改完,其中也是有很多细节需要重新熟悉,我会在下面介绍。

3.2 自动机的个数与状态数

现在我们大多的思考方式,还是搭建一个自动机,来表示所有我们需要的状态,但是其实,有的时候会有过于复杂的情况,导致一个自动机不够的情况,但是我们并没有意识到我们不自觉地使用了多个自动机。

以教程中的 cpu 指令序列检测题为例,我的代码第一次就是大概 60 几个状态,里面有一个状态对应的是读入了第一个十进制数,当然还有读入了第二个十进制数这个状态等一系列。这样做的好处就是我不再需要十进制计数器十六进制计数器了。我们可以说,这样才是纯的单一状态机的应用。

但是我们看 csdn 的代码,会发现他应用了两个寄存器,就把状态减少到了 14 个,其实这就是多状态机的应用,寄存器可以看做一个简单的状态机,以 status 和 char 作为输入,保存并输出一个数,应用多状态机,可以有效减少状态的量。

如果是双状态机系统,状态机 A 表示的状态共 n 个,状态机 B 表示的状态共 m 个。那么系统总共可以表示的状态就有 mn 个,是很方便的方法。

在我自己习惯的代码规范中(前面这几个字说明了这个规范的不靠谱性质),所有的寄存器都是状态机,所有的状态机都可以用自动机规范(下一章介绍)写法来书写,所以大家看个乐呵就好了。

3.3 自动机规范

首先,这种规范把所有的自动机都完全的拆成了三个部分,状态转移部分(属于组合电路)、状态记忆部分(属于时序电路)、输出部分(属于组合电路)。这三个模块用三个 always 块来实现,下面的例子是我的当前计数器自动状态机,风格如下:

always @(*) begin
if(status == s0)
nextCnt = 0;
else if(status == s3 && nextStatus == s2)
nextCnt = 0;
else if(status == s6)
nextCnt = cnt + 1;
else
nextCnt = cnt;
end

第一个描述了状态转移电路,可以看到,赋值的对象是 nextCnt,输入是 cnt 和 status(闭环),是一个类似于组合电路的输出。有两种清零情况,一种是初始状态,一个是从 csc 返回 cs 的状态,只有在 s6 下会加 1,不然就保存原值。

always @(posedge clk, posedge reset) begin//同步复位写法:always @(posedge clk) begin
if(reset)
cnt <= 0;
else
cnt <= nextCnt;
end

第二个描述了状态记忆部分,实现了两个功能,一个是异步复位,同步复位只需要改第一行。另一个是状态更新,因为是时序逻辑,所以是非阻塞赋值。

assign tmp1 = cnt;

第三个也可以用 always-comb 实现,但是因为 cnt 不需要输出,所以就用这个简单代替一下,领会精神。(其实最完整的是放 status 状态机,但是怕被制裁)。

3.4 本题的状态机

我最优雅的一篇代码是用了 3 个状态机,分别是 status 状态机(用于分析字符串),cnt 状态机(用于记录当前数目),max 状态机(用于记录最大数目)。


四、一些细节

4.1 输出延后一个时钟周期

最后我的输出语句是:

assign out = nextMax;

这个看着很奇怪,因为在我的逻辑里,他本来应该是:

assign out = max;

但是写成第二个以后,会导致输出延后一个时钟周期,像这样:

image-20211014163041824

本来应该在黄线处 out 发生变化,但是到了黄线以后的一个周期,才发生变化,输出整整拖延了一个周期。

这是为什么呢?我将其称为非阻塞赋值左式的计算比所有语句都慢,当然更严谨的是用层次化队列的方式解释,这里提供一篇博客,写的十分详尽:

Verilog 之非阻塞赋值(三)—— 赋值延后一个周期_隔壁 old 余的博客 - CSDN 博客

里面有如下解释:

​ 如果条件判断式中含有非阻塞赋值的 reg 变量(无论是本模块的还是其他外部模块的),那么在该条件分支下的非阻塞赋值语句会延后一个周期完成(相对条件判断式中的变量的赋值完成,延后一个周期赋值)

在 cnt 的赋值中,需要 nextCnt,nextCnt 的值需要非阻塞变量 status 的判断,所以间接地,非阻塞变量 cnt 的赋值需要非阻塞变量 status 的判断,所以 cnt 就晚了一个周期,相似地,max 也因此晚了一个周期。

但是不是啥大事情,因为哪怕是多个状态机串联,最后也是晚一个周期。对于只有一个状态机的情况,我们就输出状态机本身就可以了,输出 nextStatus 不太好(4.2 有写),对于多状态机,只要用 next 就好了,就可以把输出归为正常。

4.2 拆分问题

正如前所言,工业开发自动机把状态机拆成了,状态转移部分(属于组合电路)、状态记忆部分(属于时序电路),所以状态转移部分可能变化会很快(会让人摸不着头脑,比如下面这种情况)。

image-20211014161552470

nextStatus 被在一个周期中总是在时钟上升沿跳到 0 去一段时间,虽然不影响功能吧,但是看着很奇怪。经过 seeeagull 的谆谆教诲,我明白,因为 status 在上升沿发生改变,而此时 in 并没有改变,因为 nextStatus 是组合逻辑的输出,所以会立即改变为 s0(一般都是,本质是跳入 else 了)。

但是正如 4.1 所述因为是组合逻辑且是连续赋值,所以并不会因为这里的异常造成真正的 bug。是完全安全的写法。


五、写在后面的话

我能写出这篇文章,多亏了好友的多多支援,下面进行鸣谢(按真实姓名字典序):

  • 奶凶,解决灵感枯竭问题

  • 常浩轩助教哥哥,解决修改帖子问题和 ISim 中间变量查看

  • 郭鸿宇,提出了 cscs 计数器归零

  • seeeagull,解决了拆分问题

  • 叶颜函,解释了郭鸿宇的话

  • 叶焯仁助教哥哥,解决 ISim 中间变量查看和波形显示问题

  • 不知道叫啥名字,坐我对面眼睛狭长的小姐姐,解决我肝不动的问题

    给大家安利一下我的博客,请大家帮忙看一看,提一提热度,白嫖也没关系。多谢。(毕竟要是没有这个点的话,答辩就只能说我身体很好,立定跳跳两米七,一千米三分十几了)

    (1 条消息) living_frontier 的博客_CSDN 博客 - 领域博主