• cpu设计和实现(取指)


    【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】

            cpu设计的本质是数字电路的设计。要是没有verilog、vhdl这些语言,那么剩下来使用的方法基本只有卡诺图这一种了。在数字电路中,有两种基本的电路,一种是逻辑电路,一种是时序电路。两者的区别在于,逻辑电路随着输入而变化,而时序电路只有在时钟边沿触发的时候才能变化,其他时刻都会保持这一电路。

            听上去或许有一点拗口,我们可以通过一个示例代码来进行说明。假设有这么一个文件test.v,

    1. module test(clk, rst, in, out_a, out_b);
    2. input wire clk;
    3. input wire rst;
    4. input wire in;
    5. output wire out_a;
    6. output reg out_b;
    7. assign out_a = in;
    8. always@(posedge clk or posedge rst) begin
    9. if(rst)
    10. out_b <= 1'b0;
    11. else
    12. out_b <= in;
    13. end
    14. endmodule

            从上面的代码可以看出来,有5个信号,3个是输入信号,2个是输出信号。输入信号中,一个时钟clk,一个复位rst,一个输入信号in。输出信号中,一个是组合逻辑输出out_a,一个时序逻辑输出out_b。对于时序逻辑out_b,可以看到在复位发生的时候,信号输出为0,其他时刻只有在时钟上升沿的时候,电路才会把信号in传递给out_b。

            为了测试test.v,我们需要编译一个激励模块test_tb.v,

    1. `timescale 1ns/1ps
    2. module test_tb();
    3. reg clock;
    4. reg rst;
    5. reg in;
    6. wire out_a;
    7. wire out_b;
    8. initial begin
    9. clock = 1'b0;
    10. forever #5 clock = ~clock;
    11. end
    12. initial begin
    13. rst = 1'b0;
    14. #1 rst = 1'b1;
    15. #11 rst = 1'b0;
    16. #1000 $stop;
    17. end
    18. initial begin
    19. in = 1'b1;
    20. #17 in = 1'b0;
    21. #19 in = 1'b1;
    22. #21 in = 1'b0;
    23. end
    24. test test0(
    25. .clk(clock),
    26. .rst(rst),
    27. .in(in),
    28. .out_a(out_a),
    29. .out_b(out_b)
    30. );
    31. initial
    32. begin
    33. $dumpfile("hello.vcd");
    34. $dumpvars(0, test_tb);
    35. end
    36. endmodule

            在激励模块中,我们定义了clk、rst、in,下面就观察out_a和out_b是如何变化的了。借助于iverilog和gtkwave工具,结果我们可以分析下,

            从上面的信号不难看出,out_a和in的信号是时刻保持一致的。而对于信号out_b而言,在复位这一段时间,一直保持在0的状态。等到复位结束之后,在第一个clk上升沿到来的时候,此时in处在1的状态,那么out_b翻转为1。接着,in又继续调整为0,但是此刻out_b具有记忆功能,并且只在clk上升沿到来的时候处理信号,所以out_b继续保持为1,直到下一个clk时刻发现in已经是0的情况下,才会跟随in调整为0。

            上面这段代码只是一个插曲。这段代码保存在github上,https://github.com/feixiaoxing/design_mips_cpu/tree/master/exercise

            回归正题,今天讨论的主要是取指这个部分。顾名思义,那就应该有两个部分,一个是pc地址的生成,一个是rom指令的读取。本次代码主要参考了《自己动手写cpu》这本书,有兴趣的朋友可以找来看看。

            首先看一下pc_reg.v,

    1. module pc_reg(
    2. input wire clk,
    3. input wire rst,
    4. output reg[5:0] pc,
    5. output reg ce
    6. );
    7. always @(posedge clk)
    8. begin
    9. if(rst == 1'b1) begin
    10. ce <= 1'b0;
    11. end else begin
    12. ce <= 1'b1;
    13. end
    14. end
    15. always @(posedge clk)
    16. begin
    17. if(ce == 1'b0) begin
    18. pc <= 6'h00;
    19. end else begin
    20. pc <= pc + 1'b1;
    21. end
    22. end
    23. endmodule

             代码中主要有pc和ce两个寄存器。pc当然是指令地址的意思,而ce则是chip enable的意思。从代码上看,pc只有ce不是0的时候,才会开始自增。而ce变成1,需要等到rst结束之后的第一个clk上升沿才会变成1,因此pc也会顺延一个clk,才会开始地址自增。

    1. module rom (
    2. input wire ce,
    3. input wire[5:0] addr,
    4. output reg[31:0] inst
    5. );
    6. reg[31:0] rom[63:0];
    7. initial $readmemh ( "rom.data", rom );
    8. always@(*)
    9. if(ce == 1'b0) begin
    10. inst <= 32'h0;
    11. end else begin
    12. inst <= rom[addr];
    13. end
    14. endmodule

            取指令这部分代码是组合逻辑的代码,这从always(*)可以看的出来。需要稍微注意的是其中initial的部分,在真实的asic芯片中,也会存在把一部分代码固化在chip上的情况。当然这里使用了readmemh读取指令文件,主要还是为了测试的方便。指令文件rom.data就是文本文件,

    1. 00000000
    2. 01010101
    3. 02020202
    4. 03030303
    5. 04040404
    6. 05050505

            准备好了pc_reg.v和rom.v之后,接下来就应该将二者组合在一起了,生成inst_fetch.v,

    1. module inst_fetch(
    2. input wire clk,
    3. input wire rst,
    4. output wire[31:0] inst_o
    5. );
    6. wire[5:0] pc;
    7. wire rom_ce;
    8. pc_reg pc_reg0(
    9. .clk(clk),
    10. .rst(rst),
    11. .pc(pc),
    12. .ce(rom_ce)
    13. );
    14. rom rom0(
    15. .ce(rom_ce),
    16. .addr(pc),
    17. .inst(inst_o)
    18. );
    19. endmodule

            这部分的代码也不复杂,最主要的工作就是创建pc和rom_ce连线,这样可以将两个module示例串联在一起。当然,为了测试还需要编写一个激励模inst_fetch_tb.v,

    1. `timescale 1ns/1ps
    2. module inst_fetch_tb;
    3. reg clock;
    4. reg rst;
    5. wire[31:0] inst;
    6. initial begin
    7. clock = 1'b0;
    8. forever #10 clock = ~clock;
    9. end
    10. initial begin
    11. rst = 1'b1;
    12. #195 rst = 1'b0;
    13. #1000 $stop;
    14. end
    15. initial
    16. begin
    17. $dumpfile("hello.vcd");
    18. $dumpvars(0, inst_fetch_tb);
    19. end
    20. inst_fetch inst_fetch0(
    21. .clk(clock),
    22. .rst(rst),
    23. .inst_o(inst)
    24. );
    25. endmodule

            激励模块中只要正常给出clk和rst即可,主要就是看输出的inst中,有没有和我们之前期望的一样,可以在inst寄存器当中出现rom.data保存的那些指令数据。如果有,则代表我们的设计是正确的。反之,则代表verilog代码还是有问题的。所有的这些文件都准备妥当之后,就可以用iverilog和gtkwave仿真测试了,

            从信号仿真来看,整个设计还是符合要求的。首先rst一直处于复位的时候,ce为0。等rst结束,ce在第一个clk上升沿的时候调整为1。ce调整为1后,pc在下一个clk上升沿的时候开始自增。此外,由于rom.v是一个组合逻辑,所以pc发生改变之后,inst立马就发生了变化。所以从一开始inst是0x00000000之后,随着pc的改变,指令也开始一个一个读进来了。

            大家可以自己试试,代码链接地址在这,

     https://github.com/feixiaoxing/design_mips_cpu/tree/master/rtl/day02

  • 相关阅读:
    Python 入门基础
    小程序(开发必备常识)1
    uni-app 实现自定义按 A~Z 排序的通讯录(字母索引导航)
    【JAVAEE框架】Spring 项目构建流程
    ARM与单片机有啥区别?
    GitHub神坛变动,10W字Spring Cloud Alibaba笔记,30W星标登顶第一
    《架构师说》第五期,DPU 正在掀起数据中心变革!
    数据结构——八叉树
    Session实现登录(springboot项目)
    【西瓜书】5.神经网络
  • 原文地址:https://blog.csdn.net/feixiaoxing/article/details/127914989