数电课程模型机设计:SM & 指令寄存器 & 状态寄存器 & 指令计数器 & 通用寄存器组 & RAM(Verilog 实现)

以下代码虽经本人检查,但并未经过验收,仅供参考,保留随时更改甚至推倒重来的可能。

前几个部分使用Verilog实现,还是比较简单的。最重要的是posedge和negedge的使用。代码中部分由GitHub Copilot生成。

AM使用Block Diagram,借助Quartus自带Megafunction实现。

SM

看懂表格就能做,非常简单。

只在时钟下降沿作用,其他时候不作用,所以使用negedge clk来捕获时钟信号下降沿。

CLKEN功能
下降沿 1SM<=SM取反
下降沿0SM不变
verilog
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module SM(clk,EN,z);
    input clk,EN;
    output z;

    reg z;

    always @(negedge clk) begin
        if(EN) z <= ~z;
    end

endmodule //SM

IR / 指令寄存器

CLKIR_LD功能
下降沿1d写入ir

应该没什么好讲的吧。

verilog
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module IR(clk,ir_ld,d,ir);
    input clk,ir_ld;
    input[7:0] d;
    output[7:0] ir;

    reg[7:0] ir;
    
    always @(negedge clk) begin
        if(ir_ld) begin
            ir <= d;
        end
    end
endmodule

PSW / 状态寄存器

此程序目前存在一个问题:第一个下降沿,若cf_en或zf_en为0,c或z会变成未知值。目前把它们都设置为1,为数据做一个初始化,来规避这个问题。

CLK控制信号 功能
下降沿 $cf_en=1$cf写入c
下降沿$zf_en=1$zf写入z

这里讲一个之前一直没注意的事情。本程序可以使用两个always块。组合逻辑模块,处理cf和zf的使能;时序逻辑模块,处理下降沿更新。组合逻辑的结果由寄存器负责暂存。当然,也可以使用三个,一个处理cf,一个处理zf,一个侦测时钟信号,但生成的RTL视图实际上是一样的。

如果将cf_en,zf_en等不带沿的条件和negedge clk写到一个always条件内,Quartus会提示 10122 错误:在一个always块中,双沿的检测(就是普通的always @(n))和单上升 / 下降沿是不能共存的。可以参考 logic - Single and double-edge expressions (Verilog) - Stack Overflow

需要注意,之前所讲的 “输入必须都放在always条件里”,只适用于“在这些输入值改变的时候输出值需要立刻改变” 的时候,即同时侦测信号的上升沿和下降沿,这样输入值一旦改变,就可以及时的改变输出值。但时序电路不需要,如PSW只需要在时钟下降沿的时候根据两个使能信号的值更新输出就行了。所以输入和使能都没有必要放到always里,只需要在if侦测就行了。

下面是仅使用单cf_en的结果。

verilog
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
module PSW(clk,cf_en,zf_en,cf,zf,c,z);
    input clk,cf_en,zf_en,cf,zf;
    output c,z;
    reg c,z;
    reg cTemp,zTemp;

    always @(negedge clk) begin
        if(cf_en) begin
            c<=cf;
        end
        if(zf_en) begin
            z<=zf;
        end
    end
endmodule //PSW

PC / 指令计数器

仍然是看表格就能做。

这个 “a向入c” 的意思是将a的数据导入c。

CLKIN PCLD PC功能
下降沿 10$c[7..0]$ 中数据自加1
下降沿01$a[7..0]$ 向入 $c[7..0]$
verilog
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
module PC(ld,inc,clk,a,c);
    input ld,inc,clk;
    input[7:0] a;
    output[7:0] c;

    reg[7:0] c;

    always @(negedge clk) begin
        if (ld) begin
            c <= a;
        end else begin
            if (inc) begin
                c <= c + 1;
            end
        end
    end
endmodule //PC

REG / 通用寄存器组

不只有表里看起来这么简单。

任何时候都是能够读取的,只有写受下降沿限制。因为写的时候只由RWBA指定寄存器地址,所以RAA并没有什么关系。

读取只是把当前的值忠实读出来而已,如果有写入那就读出来新的值。

明明是寄存器,但表中甚至没有描述寄存器部分…… 也不难,定义三个八位的reg变量,就是寄存器的核心部分了,也就是读写的对象。

操作 CLKWE$RAA[1..0]$$RWBA[1..0]$ 功能
00或01或1000或01或10根据 $RAA[1..0]$ 的值从A,B,C中选择一个寄存器的值由S口输出
根据 $RWBA[1..0]$ 的值从A,B,C中选择一个寄存器的值由D口输出
下降沿 0XX00或01或10 控制信号WE为0,根据 $RWBA[1..0]$ 的值, 在时钟下降沿将外部输入写入A,B,C三个寄存器中的某个寄存器。

寄存器是由负责读取的组合逻辑电路和负责写入的时序逻辑电路合并而成的。其读的状态不受写的状态影响,使用阻塞赋值;写则使用非阻塞赋值。

对于RAA和RWBA的输入,00代表A,01代表B,10代表C,都好理解,但如果输入11,仍然要输出C,而不是输出0或者高阻态之类的。可以直接写A、B、default。

verilog
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
module Register(WE,clk,RA,WA,i,S,D);
    input WE,clk;
    input [1:0] RA,WA;
    input [7:0] i;

    output[7:0] S,D;

    reg [7:0] S,D;
    reg [7:0] a,b,c;

    always @(RA) begin
        case(RA)
            2'b00: S=a;
            2'b01: S=b;
            2'b10: S=c;
            dmodule Register(WE,clk,RA,WA,i,S,D);
    input WE,clk;
    input [1:0] RA,WA;
    input [7:0] i;

    output[7:0] S,D;

    reg [7:0] S,D;
    reg [7:0] a=8'b00110110;
    reg [7:0] b=8'b10001011;
    reg [7:0] c=8'b11001110;

    always @(RA) begin
        case(RA)
            2'b00: S=a;
            2'b01: S=b;
            default: S=c;
        endcase
    end

    always @(WA) begin
        case(WA)
            2'b00: D=a;
            2'b01: D=b;
            default: D=c;
        endcase
    end

    always @(negedge clk) begin
        if(WE==0) begin
            case(WA)
                2'b00: a<=i;
                2'b01: b<=i;
                2'b10: c<=i;
            endcase
        end
    end

endmodule //Register

RAM

RAM包含一个QPF文件和一个MIF文件。QPF文件用于描述电路,而MIF文件用于定义RAM初始的内容。所谓初始值,就是在你没有向某个地址的内存块输入值的情况下,它默认输出的值。

新建一个Memory Initialization File,使用默认的256*8即可。里面随便填点东西,可以右键选择Custom Fill Cells搞点花样,比如使用梯度为1的Increment,可以将每个单元格赋给与它地址相等的值。

RAM 初始化

再新建一个Block Diagram File,这么画。注意三态门出来的线必须是总线(粗细可辨)。

RAM 电路图

实际上它是和指导书上有一定区别的。如果有多余的引脚,无需搭理。

双击表格可以修改参数,改成一样的就行。LPM_FILE填入刚刚MIF的相对路径,也就是说如果目录相同可以使用 “./xxx.mif” 表示,一定要带双引号。绝对路径(如D:/test/test.mif或 / root/test/test.mif)也可以。


本文代码所用波形文件 在这里 可以下载,建议仅用于参考波形。