代码中理解CPU结构及工作原理

时间:2022-06-27 16:43:22

一、前言

  从研究生开始到工作半年,陆续在接触MCU SOC这些以CPU为核心的控制器,但由于专业的原因一直对CPU的内部结构和工作原理一知半解。今天从一篇博客中打破一直以来的盲区。特此声明,本文设计思想及代码均源于如下博文,这里仅用于自己学习记录,以及分享心得之用。

简易CPU的设计和实现_阡飞陌-CSDN博客
https://blog.csdn.net/weixin_36077867/article/details/82286612

二、简易CPU结构与工作原理概述

用下原文中的结构图:

代码中理解CPU结构及工作原理

  CPU核心模块包括控制器、程序计数器(PC)、存储器(memory)、译码器和算术逻辑单元(ALU)。控制器负责指挥调度各个模块正常工作:PC每到达一个数阶段内,均会进行取指令->译码->执行指令。取指令从memory中取出PC值指向地址的数据,之后数据传入译码器翻译为具体操作目的,最后根据这一目标来让ALU完成算数和逻辑运算,并将运算结果保存到memory指定地址。memory的内容就是在我们之前玩单片机时用IDE将C/C++等高级语言转化成的比特流,里边包括了代码指令、临时变量及所有需要保存的数据数值。

三、设计代码与仿真分析

  以下代码仅是对转载博客中进行了少许改动,并无实质变化。

 `timescale 1ns / 1ps

 // Description:
// program counter module PC
#(parameter ADDR_WIDTH = )
(
input clock,
input reset,
input en,
output reg [ADDR_WIDTH-:] pc
); wire [ADDR_WIDTH-:] pc_next; always@(posedge clock or posedge reset)begin
if(reset)
pc <= ;
else if(en)
pc <= pc_next;
end assign pc_next = pc + ; endmodule

PC.v

 `timescale 1ns / 1ps

 // Description:
// memory used for storing instructions, temporary variables, and initialization data
//STA,store A to
//LDA, load A from module memory
#(
parameter ADDR_WIDTH = ,
parameter DATA_WIDTH =
)
(
input clock,
input reset,
input wr_en,
input rd_en,
input [ADDR_WIDTH-:] addr,
input [DATA_WIDTH-:] din,
output reg [DATA_WIDTH-:] dout
); reg [DATA_WIDTH-:] mem [:-]; always@(posedge clock,posedge reset)begin
if(reset)begin
mem [] <= 'b000_01011; //LDA 01011
mem [] <= 'b010_01100; //ADD 01100
mem [] <= 'b001_01101; //STA 01101
mem [] <= 'b000_01011; //LDA 01011
mem [] <= 'b100_01100; //AND 01100
mem [] <= 'b001_01110; //STA 01110
mem [] <= 'b000_01011; //LDA 01011
mem [] <= 'b011_01100; //SUB 01100
mem [] <= 'b001_01111; //STA 01111
mem [] <= 'b10100000; //HLT
mem [] <= 'b00000000;
mem [] <= 'b10010101;
mem [] <= 'b01100101;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
mem [] <= 'b00000000;
end
else begin
if(wr_en)
mem[addr] <= din;
else if(rd_en)
dout <= mem[addr];
end
end
endmodule

memory.v

`timescale 1ns / 1ps

// Description:
// instruction decoder module idec
#(
parameter DATA_WIDTH = ,
parameter ADDR_WIDTH =
)
(
input clock,
input reset,
input en,
input [DATA_WIDTH-:] instruction,//from memory
output reg [DATA_WIDTH-ADDR_WIDTH-:] opcode,
output reg [ADDR_WIDTH-:] addr
); always@(posedge clock,posedge reset)begin
if(reset)begin
opcode <= ;
addr <= ;
end
else if(en)begin
opcode <= instruction[DATA_WIDTH- -:];
addr <= instruction[ADDR_WIDTH-:];
end
end endmodule

idec.v

 `timescale 1ns / 1ps

 // Description:
// arithmetic logic unit module alu
#(parameter OP_WIDTH = )
(
input clock,
input reset, input en,
input add_en,//加法运算使能
input sub_en,
input and_en,
input pass_en,
input [OP_WIDTH-:] din, output n,//负标志
output z,//0标志
output reg c,//输出进位标志
output v,//输出溢出标志
output reg [OP_WIDTH-:] a//累加器输出寄存器 dout ); assign n = (c == ) ? : ; //负数标志,如果进位标志为1,则n=1
assign z = (a == 'd0) ? 1: 0 ; //0标志,如果累加器为0,z=1
assign v = ((a>**(OP_WIDTH-)-) || (a<-**(OP_WIDTH-)) ? : ); //溢出标志 补码取值范围:-2^(n-1)~~~~~2^(n-1)-1 n=8 always @(posedge clock or posedge reset)begin
if (reset) begin
a <= ; //复位累加器清0,
c <= ;
end
else begin
if(en) begin
if(add_en)
{c,a} <= a + din;
else if(sub_en)
{c,a} <= a - din;
else if(and_en)
a <= a & din;
else if(pass_en)
a <= din;
end
end
end endmodule

alu.v

 `timescale 1ns / 1ps

 module control#(
parameter DATA_WIDTH = ,
parameter ADDR_WIDTH =
)
(
input clock,
input reset,
input [DATA_WIDTH-ADDR_WIDTH-:] opcode,//来自解码器解码后指令 output reg [-:] s,//使能信号
output reg addr_sel,//程序或数据地址选通
output reg [-:] instrs ); parameter [DATA_WIDTH-ADDR_WIDTH-:] LDA = 'b000,
STA = 'b001,
ADD = 'b010,
SUB = 'b011,
AND = 'b100; reg [-:] cnt;
wire add_cnt,end_cnt; always@(posedge clock, posedge reset)begin
if(reset)
cnt <= ;
else if(add_cnt)begin
if(end_cnt)
cnt <= ;
else
cnt <= cnt + ;
end
end assign add_cnt = ;
assign end_cnt = add_cnt && cnt == -; always@(*)begin
case(cnt)
:begin//取指令
s = 'b100_000;
addr_sel = ;
instrs = ;
end
:begin//解码
s = 'b010_000;
addr_sel = ;
end
:begin//read from the memory
addr_sel = ;
if(
(opcode == LDA) ||
(opcode == ADD) ||
(opcode == SUB) ||
(opcode == AND)
)
s = 'b001_000;
else
s = 'b000_000;
end
:begin//ALU operations
s = 'b000_100;
addr_sel = ;
case(opcode)
LDA:instrs = 'b0001;
ADD:instrs = 'b1000;
SUB:instrs = 'b0100;
AND:instrs = 'b0010;
STA:instrs = 'b0000;
default:instrs = 'b0000;
endcase
end
:begin//write to the memory
addr_sel = ;
if(opcode == STA)
s = 'b000_010;
else
s = 'b000_000;
end
:begin// PC
s = 'b000_001;
addr_sel = ;
end
default:begin
s = 'b000_000;
addr_sel = ;
instrs = ;
end
endcase
end endmodule

control.v

 `timescale 1ns / 1ps

 module cpu_top
(
input clock,
input reset, output n,//负标志
output z,//0标志
output c,//输出进位标志
output v//输出溢出标志
); parameter DATA_WIDTH = ,
ADDR_WIDTH = ; wire [-:] s;
wire [ADDR_WIDTH-:] addr_mem,addr_idec,addr_pc;
wire addr_sel;
wire [DATA_WIDTH-:] dout_mem,din_mem;
wire [DATA_WIDTH-ADDR_WIDTH-:] opcode;
wire [-:] alu_oper; assign addr_mem = addr_sel == ? addr_idec: addr_pc; control#(
.DATA_WIDTH (DATA_WIDTH),
.ADDR_WIDTH (ADDR_WIDTH)
)
controlor
(
.clock (clock),
.reset (reset),
.opcode (opcode),//来自解码器解码后指令
.s (s),//使能信号
.addr_sel (addr_sel),//程序或数据地址选通
.instrs (alu_oper) ); PC
#(.ADDR_WIDTH (ADDR_WIDTH))
pointer_counter
(
.clock (clock),
.reset (reset),
.en (s[]),
.pc (addr_pc)//code address
); memory
#(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH (DATA_WIDTH)
)
memory
(
.clock (clock),
.reset (reset),
.wr_en (s[]),
.rd_en (s[] | s[]),
.addr (addr_mem),
.din (din_mem),
.dout (dout_mem)
); idec
#(
.DATA_WIDTH (DATA_WIDTH),
.ADDR_WIDTH (ADDR_WIDTH)
)
instr_decoder
(
.clock (clock),
.reset (reset),
.en (s[]),
.instruction(dout_mem),//from memory .opcode (opcode),
.addr (addr_idec)//data address
); alu
#(.OP_WIDTH(DATA_WIDTH))
alu
(
.clock (clock),
.reset (reset),
.en (s[]),
.add_en (alu_oper[]),//加法运算使能
.sub_en (alu_oper[]),
.and_en (alu_oper[]),
.pass_en (alu_oper[]),
.din (dout_mem),
.n (n),//负标志
.z (z),//0标志
.c (c),//输出进位标志
.v (v),//输出溢出标志
.a (din_mem)//累加器输出寄存器 dout ); endmodule

cpu_top.v

  现在仿真观察逻辑是否按照预期工作。这里使用Questasim工具,该工具的Windows/Linux版本都很容易下载到,而且对SV UVM支持程度高,是芯片自学的首选。只写了个简单的testbench来toggle clock和reset。

`timescale 1ns/1ps;

module tb_top;

    parameter T = ;

    logic clock;
logic reset;
logic n,z,c,v; initial begin:clock_toggle
clock = ;
forever begin
#(T/2.0);
clock = ~clock;
end
end initial begin
reset = ;
#;
reset = ;
#T;
reset = ;
#;
$stop;
end cpu_top DUT
(
.clock (clock),
.reset (reset),
.n (n),//负标志
.z (z),//0标志
.c (c),//输出进位标志
.v (v)//输出溢出标志
); endmodule

testbench.sv

代码中理解CPU结构及工作原理

  PC不断从0计数到5.每个计数周期内,各个模块的使能信号s也在交替拉高,指示当前进行不同的操作步骤。我们以第三个周期为例:

代码中理解CPU结构及工作原理

  s5:读取memory的'h1地址数据'b010_01100

  s4:得到8'h4c,解析出当前操作码是高三位3'h2(ADD),操作地址是第五位5'h0c

  s3:读取5'h0c地址内的数据'b0110_0101 即8'h65

  s2:调用ALU,将上次计算结果与当前读取memory中数据相加给din_mem。'h95+'h65='hfa

  s1:由于操作码不包括写入,当前时钟不操作

  s0:PC加1,为下一个指令周期做准备

  这个“CPU”真的简单到几乎不能做任何事情,但其对于初步接触的人还是很有帮助的。现代CPU指令集非常庞大,还包括一些寄存器、总线单元等专用硬件逻辑,所以要学的还有很多。从应用角度来讲,在更上一个层次掌握MCU的结构及原理更加重要。