数电课程模型机设计: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)也可以。


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