引言 经过对OpenRISC经过近一年的分析和研究,我们了解了计算机系统结构设计的主要概念、重要技术和基本思想。我认为现在有必要练习。
本节将设计一个简单的部分cpu,包括ISA模块,模块划分,RTL实现,编写asm汇编程序,用modelsim模拟和使用quartusII的综合。
1.计算器和计算机 我认为,在EDVAC以前的计算机可以被视为计算器。
原因是冯诺依曼对EDVAC在分析过程中,提出了二进制计算和添加存储部件,在此之前,计算机没有存储功能,例如,我们需要计算(1 2)&(3 4)如果使用计算器,其操作步骤如下:
a,先用计算器计算1 结果3,然后人脑自己记住这个数字。
b,用计算器计算3 4的结果7,人脑也自己记住这个数。
c,最后,用计算器计算3&7的结果3。
若采用计算机,其操作过程如下:
首先,我们需要写一个程序,假设程序放在程序存储器的地址0x55、56、57、58四个地址分别放置在数据存储器中。
程序执行如下:
a,将data_mem的0x放55个数据r1。
b,将data_mem的0x数据放在56处r2。
c,执行add r2,r1,结果放在r2里面。
d,将r写入2的内容data_mem的0x60这个地址。
e,将data_mem的0x数据放在57处r3。
f,将data_mem的0x数据放在58处r4。
g,执行add r4,r3,结果放在r4里面。
h,将r4内容写入data_mem的0x61这个地址。
i,将data_mem的0x放60个数据r5。
j,将data_mem的0x数据放在61处r6。
k,执行and r6,r5,结果放在r6里面。
l,将r内容写入6data_mem的0x62这个地址,最终得到计算结果。
我们可以看到,如果用计算机计算,只需要三个步骤,但如果用计算机,需要12个步骤。使用计算机的效率低吗?今天计算机的蓬勃发展使得答案不言而喻。
原因是只要编写程序,计算机的整个计算过程就不需要人为干预。
我认为这就是计算机发展的根本原因,即计算机的出现对人们来说是一个巨大的解放。只要我们以某种方式写程序,然后交给计算机,计算机就会自动完成任务,我们的手就可以做其他事情!
2,架构设计 1>整体设计 通过上面的例子,我们可以体验到计算机的好处。让我们设计一个cpu,来完成1 2的计算。
关于计算机系统结构,我们之前说过足够多的内容。以下几点仅供说明:
a,我们采用harvard结构,即指令和数据的总线是独立的。
b,流水线,我们暂时不采用流水设计,但最终,我给出了五级流水时的数据通路设计框架。
c,至于指令集,由于是学习目的,我们只实现基本的访问和存储指令、操作指令和分支指令。操作不支持乘除和浮点。详见附录。每个指令为16-bit。
d,为我们设计cpu进行仿真和验证,我们需要设计一个简单的soc才行,这个soc只包括指令存储器,cpu内核,数据存储器。
e,core内总线为8-bit。有一个问题,core外是8-bit,但分支指令的目的地地址是11-bit,所以如果超过8-bit,会有问题,还没有解决。
下面是soc整体架构图:让我们给他取个名字,叫 tiny_soc,小cpu简单地称她为tiny_core。
2>模块划分 cpu core结构如下:
整个cpu core由数据通道、控制通道和调试单元组成。
其中数据通路包括:
PC生成模块:genpc
运算模块:alu,在alu前面是多选一个操作数mux。
寄存器堆:regfile模块
还有栈:stack。
控制数据通路模块ctrl_path控制模块,解码控制通道的指令,并产生相应的控制信号。
由于调试单元只是学习目的,调试单元是最简化的,只输出当前的PC值和当前指令内容两个信息。
3.模块划分和界面定义 在体架构设计完成后,需要进一步细化。此时,需要定义具体的模块名称和模块功能。一旦确定了功能,就可以确定具体的模块接口信号。
如果模块功能太大,我们需要将它们分成更小的模块,即top-down设计方法。关于设计方法(top-down,bottom-up),很多资料都有介绍,这里就不赘述了。
一个完整的项目,不同于理论研究,需要处理很多实现细节。下面,我们来介绍一下更重要的部分:
1>genpc模块 需要考虑三点:上电复位PC默认值是多少?执行正常指令时PC如何变化?遇到分支指令时?PC如何变化?
关于上电默认值,我们可以通过一个define设置句子,方便用户以后修改。
关于正常指令的指令,PC是加1、加2还是加4取决于指令存储器的访问方式。我们的指令存储器在每个地址放一个指令,每个指令放两个字节(16-bit),所以我们只需要PC加1就够了。
对于分支指令,我们直接将解码的跳转地址赋予控制通路PC即可。
genpc模块C语言伪代码如下:
genpc module pseudo code if(rst) { pc= boot_addr; } else { if(branch) { pc = branch_target; } else { pc = pc 1; } }
2>alu模块 alu大家都很熟悉模块,是执行单元部件,负责操作指令。
该模块的输入信号是由控制通路解码的操作数和操作码,输出信号是操作结果。
需要注意的是,该模块可以完全组合逻辑电路。
3>rf模块 register file在物理上,模块是一个模块block ram。
从逻辑上讲,该模块对软件程序员透明,寄存器堆和指令集是软件和硬件之间的交互接口。
4>stack stack(栈)存放在处理分支指令时PC例如,当我们处理子程序调用时,我们需要首先调用当前值PC 1压栈,遇到子程序返回指令时使用。
栈的特点是LIFO(last in first out),这一点与heap(堆)不同。
5>ctrl_path模块 负责控制通道genpc解码模块产生的地址处的指令,并产生相应的操作数、操作代码和控制模型。这部分信号稍多。
6>tiny_soc 为了测试这个cpu我们需要建立一个最小的系统,包括指令只读取存储器insn_rom机器代码存储在模块中。
由于是harvard结构也需要数据存储器ram相当于内存的模块。
当然,如果你想插其他插件,I/O外设,我们只需要定义它的地址空间,需要注意的是I/O外设的地址空间无法与RAM重叠,各个I/O外设不能重叠。
RAM和I/O外设之间可以通过一个arbiter与cpu core实现数据交互。
当然,如果有不止一个地方存储指令,也需要指令arbiter。
4,RTL实现 在完成模块划分、界面定义、仔细分析和考虑模块间时序后,如果没有问题,我们可以编码。
编码,需要注意的是,编码必须标准化,信号命名,代码注释,尽量小心。这里直接给出RTL代码(verilog HDL)
按自上而下顺序给出:
1>tiny_soc顶层模块:soc_top
/* * * file name : soc_top.v * author : Rill * date : 2013-08-11 * */ `timescale 1ns / 1ps module soc_top ( input clk, input rst ); wire read_e; wire write_e; wire [7:0] port_addr; wire [7:0] core_in; wire [7:0] core_out; wire [15:0] instruction; wire [10:0] inst_addr; wire [15:0] debug_insn; wire [10:0] debug_pc; insn_rom insn_rom ( .clk (lk), .rst (rst), .address (inst_addr), .instruction (instruction) ); core core ( .clk (clk), .rst (rst), .read_e (read_e), .write_e (write_e), .port_addr (port_addr), .data_in (core_in), .data_out (core_out), .inst_addr (inst_addr), .instruction (instruction), .debug_pc (debug_pc), .debug_insn (debug_insn) ); ram ram ( .clk (clk), .rst (rst), .wr (write_e), .rd (read_e), .addr (port_addr), .din (core_out), .dout(core_in) ); endmodule
2>指令存储器:insn_rom
/* * * file name : insn_rom.v * author : Rill * date : 2013-08-11 * */ module insn_rom ( input clk, input rst, input [10:0] address, output reg [15:0] instruction ); //(* RAM_STYLE="BLOCK" *) reg [15:0] rom [2047:0]; always @(posedge clk) begin if(rst) begin rom[0] <= 16'h5801;//0: jmp start rom[1] <= 16'h1101;//1:start mov r1,1 rom[2] <= 16'h1202;//2: mov r2,2 rom[3] <= 16'h3220;//3: add r2,r1 rom[4] <= 16'h2237;//4: str r2,55 rom[5] <= 16'h5806;//5: jmp end rom[6] <= 16'h5806;//6:end jmp end*/ end else begin instruction <= rom[address]; end end endmodule
3>数据存储器:ram
* * * file name : ram.v * author : Rill * date : 2013-08-11 * */ module ram( input clk, input rst, input [7:0] din, input [7:0] addr, output reg [7:0] dout, input wr, input rd ); (* RAM_STYLE="DISTRIBUTED" *) reg [7:0] ram [255:0]; always @(posedge clk) begin if(rst) begin dout <= 8'b0; ram[0] = 0; ram[1] = 1; ram[2] = 2; ram[32] = 32; ram[64] = 64; end else begin if (wr) ram[addr] <= din; else if(rd) dout <= ram[addr]; end end endmodule
4>CPU核心:core
/* * * file name : core.v * author : Rill * date : 2013-08-11 * */ module core ( input clk, input rst, output [7:0] port_addr, output read_e, output write_e, input [7:0] data_in, output [7:0] data_out, output [10:0] inst_addr, input [15:0] instruction, output [10:0] debug_pc,//debug i/f output [15:0] debug_insn ); wire z,c; wire insel; wire we; wire [2:0] raa; wire [2:0] rab; wire [2:0] wa; wire [2:0] opalu; wire [2:0] sh; wire selpc; wire ldpc; wire ldflag; wire [10:0] ninst_addr; wire selk; wire [7:0] KTE; wire [10:0] stack_addr; wire wr_en, rd_en; wire [7:0] imm; wire selimm; control_path control_path ( .clk (clk), .rst (rst), .instruction (instruction), .z (z), .c (c), .port_addr (port_addr), .write_e (write_e), .read_e (read_e), .insel (insel), .we (we), .raa (raa), .rab (rab), .wa (wa), .opalu (opalu), .sh (sh), .selpc (selpc), .ldpc (ldpc), .ldflag (ldflag), .naddress (ninst_addr), .selk (selk), .KTE (KTE), .stack_addr (stack_addr), .wr_en (wr_en), .rd_en (rd_en), .imm (imm), .selimm (selimm) ); data_path data_path_i ( .clk (clk), .rst (rst), .data_in (data_in), .insel (insel), .we (we), .raa (raa), .rab (rab), .wa (wa), .opalu (opalu), .sh (sh), .selpc (selpc), .selk (selk), .ldpc (ldpc), .ldflag (ldflag), .wr_en (wr_en), .rd_en (rd_en), .ninst_addr (ninst_addr), .kte (KTE), .imm (imm), .selimm (selimm), .data_out (data_out), .inst_addr (inst_addr), .stack_addr (stack_addr), .z (z), .c (c) ); debug debug ( .pc_in (inst_addr), .insn_in (instruction), .pc (debug_pc), .insn (debug_insn) ); endmodule
5>调试单元:debug
/* * * file name : debug.v * author : Rill * date : 2013-08-11 * */ module debug ( input [10:0] pc_in, input [15:0] insn_in, output [10:0] pc, output [15:0] insn ); assign pc = pc_in; assign insn = insn_in; endmodule
6>控制通路:control_path
/* * * file name : control_path.v * author : Rill * date : 2013-08-11 * */ module control_path ( input clk, input rst, input [15:0] instruction, input z, input c, output reg [7:0] port_addr, output reg write_e, output reg read_e, output reg insel, output reg we, output reg [2:0] raa, output reg [2:0] rab, output reg [2:0] wa, output reg [2:0] opalu, output reg [2:0] sh, output reg selpc, output reg ldpc, output reg ldflag, output reg [10:0] naddress, output reg selk, output reg [7:0] KTE, input [10:0] stack_addr, output reg wr_en, rd_en, output reg [7:0] imm, output reg selimm ); parameter fetch= 5'd0; parameter decode= 5'd1; parameter ldi= 5'd2; parameter ldm= 5'd3; parameter stm= 5'd4; parameter cmp= 5'd5; parameter add= 5'd6; parameter sub= 5'd7; parameter andi= 5'd8; parameter oor= 5'd9; parameter xori= 5'd10; parameter jmp= 5'd11; parameter jpz= 5'd12; parameter jnz= 5'd13; parameter jpc= 5'd14; parameter jnc= 5'd15; parameter csr= 5'd16; parameter ret= 5'd17; parameter adi= 5'd18; parameter csz= 5'd19; parameter cnz= 5'd20; parameter csc= 5'd21; parameter cnc= 5'd22; parameter sl0= 5'd23; parameter sl1= 5'd24; parameter sr0= 5'd25; parameter sr1= 5'd26; parameter rrl= 5'd27; parameter rrr= 5'd28; parameter noti= 5'd29; parameter nop= 5'd30; wire [4:0] opcode; reg [4:0] state; assign opcode=instruction[15:11]; always@(posedge clk or posedge rst) begin if (rst) begin state<=decode; end else begin case (state) fetch: begin state<=decode; end decode: begin if(opcode >=ldi && opcode <=nop) state <= opcode;//state just is the opcode now else state <= nop; end default: state<=fetch; endcase end end always@(*) begin port_addr<=0; write_e<=0; read_e<=0; insel<=0; we<=0; raa<=0; rab<=0; wa<=0; opalu<=4; sh<=4; selpc<=0; ldpc<=1; ldflag<=0; naddress<=0; selk<=0; KTE<=0; wr_en<=0; rd_en<=0; imm<=0; selimm<=0; case (state) fetch: begin ldpc<=0; end decode: begin ldpc<=0; if (opcode==stm) begin raa<=instruction[10:8]; port_addr<=instruction[7:0]; end else if (opcode==ldm) begin wa<=instruction[10:8]; port_addr<=instruction[7:0]; end else if (opcode==ret) begin rd_en<=1; end end ldi: begin selk<=1; KTE<=instruction[7:0]; we<=1; wa<=instruction[10:8]; end ldm: begin wa<=instruction[10:8]; we<=1; read_e<=1; port_addr<=instruction[7:0]; end stm: begin raa<=instruction[10:8]; write_e<=1; port_addr<=instruction[7:0]; end cmp: begin ldflag<=1; raa<=instruction[10:8]; rab<=instruction[7:5]; opalu<=6; end add: begin raa<=instruction[10:8]; rab<=instruction[7:5]; wa<=instruction[10:8]; insel<=1; opalu<=5; we<=1; end sub: begin raa<=instruction[10:8]; rab<=instruction[7:5]; wa<=instruction[10:8]; insel<=1; opalu<=6; we<=1; end andi: begin raa<=instruction[10:8]; rab<=instruction[7:5]; wa<=instruction[10:8]; insel<=1; opalu<=1; we<=1; end oor: begin raa<=instruction[10:8]; rab<=instruction[7:5]; wa<=instruction[10:8]; insel<=1; opalu<=3; we<=1; end xori: begin raa<=instruction[10:8]; rab<=instruction[7:5]; wa<=instruction[10:8]; insel<=1; opalu<=2; we<=1; end jmp: begin naddress<=instruction[10:0]; selpc<=1; ldpc<=1; end jpz: if (z) begin naddress<=instruction[10:0]; selpc<=1; ldpc<=1; end jnz: if (!z) begin naddress<=instruction[10:0]; selpc<=1; ldpc<=1; end jpc: if (c) begin naddress<=instruction[10:0]; selpc<=1; ldpc<=1; end jnc: if (!c) begin naddress<=instruction[10:0]; selpc<=1; ldpc<=1; end csr: begin naddress<=instruction[10:0]; selpc<=1; ldpc<=1; wr_en<=1; end ret: begin naddress<=stack_addr; selpc<=1; ldpc<=1; end adi: begin raa<=instruction[10:8]; wa<=instruction[10:8]; imm<=instruction[7:0]; selimm<=1; insel<=1; opalu<=5; we<=1; end csz: if (z) begin naddress<=instruction[10:0]; selpc<=1; ldpc<=1; wr_en<=1; end cnz: if (!z) begin naddress<=instruction[10:0]; selpc<=1; ldpc<=1; wr_en<=1; end csc: if (c) begin naddress<=instruction[10:0]; selpc<=1; ldpc<=1; wr_en<=1; end cnc: if (!c) begin naddress<=instruction[10:0]; selpc<=1; ldpc<=1; wr_en<=1; end sl0: begin raa<=instruction[10:8]; wa<=instruction[10:8]; insel<=1; sh<=0; we<=1; end sl1: begin raa<=instruction[10:8]; wa<=instruction[10:8]; insel<=1; sh<=5; we<=1; end sr0: begin raa<=instruction[10:8]; wa<=instruction[10:8]; insel<=1; sh<=2; we<=1; end sr1: begin raa<=instruction[10:8]; wa<=instruction[10:8]; insel<=1; sh<=6; we<=1; end rrl: begin raa<=instruction[10:8]; wa<=instruction[10:8]; insel<=1; sh<=1; we<=1; end rrr: begin raa<=instruction[10:8]; wa<=instruction[10:8]; insel<=1; sh<=3; we<=1; end noti: begin raa<=instruction[10:8]; wa<=instruction[10:8]; insel<=1; opalu<=0; we<=1; end nop: begin opalu<=4; end endcase end endmodule
7>数据通路:data_path
/* * * file name : data_path.v * author : Rill * date : 2013-08-11 * */ module data_path ( input clk, input rst, input [7:0] data_in, input insel, input we, input [2:0] raa, input [2:0] rab, input [2:0] wa, input [2:0] opalu, input [2:0] sh, input selpc, input selk, input ldpc, input ldflag, input wr_en, rd_en, input [10:0] ninst_addr, input [7:0] kte, input [7:0] imm, input selimm, output [7:0] data_out, output [10:0] inst_addr, output [10:0] stack_addr, output z,c ); wire [7:0] regmux, muximm; wire [7:0] portA, portB; wire [7:0] shiftout; assign data_out=shiftout; genpc genpc ( .clk (clk), .rst (rst), .ldpc (ldpc), .selpc (selpc), .ninst_addr (ninst_addr), .inst_addr (inst_addr) ); alu_mux alu_mux ( .selimm (selimm), .imm (imm), .portB (portB), .muximm (muximm) ); alu alu ( .a (portA), .b (muximm), .opalu (opalu), .ldflag (ldflag), .zero (z), .carry (c), .sh (sh), .dshift (shiftout) ); stack stack ( .clk (clk), .rst (rst), .wr_en (wr_en), .rd_en (rd_en), .din (inst_addr), .dout (stack_addr) ); regfile_mux regfile_mux ( .insel (insel), .selk (selk), .shiftout (shiftout), .kte (kte), .data_in (data_in), .regmux (regmux) ); regfile regfile ( .datain (regmux), .clk (clk), .we (we), .wa (wa), .raa (raa), .rab (rab), .porta (portA), .portb (portB) ); endmodule
8>程序计算器:genpc
/* * * file name : genpc.v * author : Rill * date : 2013-08-11 * */ `define boot_addr 0 //boot address after power on module genpc ( input clk, input rst, input ldpc, input selpc, input [10:0] ninst_addr, output [10:0] inst_addr ); reg [10:0] pc; assign inst_addr=pc; always@(posedge clk or posedge rst) begin if (rst) pc <=`boot_addr; else if (ldpc) if(selpc) pc<=ninst_addr; else pc<=pc+1; end endmodule
9>运算单元:alu ,alu_mux
/* * * file name : alu_mux.v * author : Rill * date : 2013-08-11 * */ module alu_mux ( input selimm, input [7:0] imm, input [7:0] portB, output [7:0] muximm ); assign muximm = selimm? imm : portB;//result : imm if ldi insn,portb if ldm insn endmodule
/* * * file name : alu.v * author : Rill * date : 2013-08-11 * */ module alu ( input [7:0] a, input [7:0] b, input [2:0] opalu, input ldflag, output zero, output carry, input [2:0] sh, output reg [7:0] dshift ); reg [7:0] resu; assign zero=ldflag?(resu==0):1'b0; assign carry=ldflag?(a<b):1'b0; always@(*) case (opalu) 0: resu <= ~a; 1: resu <= a & b; 2: resu <= a ^ b; 3: resu <= a | b; 4: resu <= a; 5: resu <= a + b; 6: resu <= a - b; default: resu <= a + 1; endcase always@* case (sh) 0: dshift <= {resu[6:0], 1'b0}; 1: dshift <= {resu[6:0], resu[7]}; 2: dshift <= {1'b0, resu[7:1]}; 3: dshift <= {resu[0], resu[7:1]}; 4: dshift <= resu; 5: dshift <= {resu[6:0], 1'b1}; 6: dshift <= {1'b1, resu[7:1]}; default: dshift <= resu; endcase endmodule
10>寄存器堆:regfile,regfile_mux
/* * * file name : regfile_mux.v * author : Rill * date : 2013-08-11 * */ module regfile_mux ( input insel, input selk, input [7:0] shiftout, input [7:0] kte, input [7:0] data_in, output [7:0] regmux ); wire [7:0] muxkte; assign regmux=insel? shiftout : muxkte; assign muxkte=selk? kte : data_in; endmodule
/* * * file name : regfile.v * author : Rill * date : 2013-08-11 * */ module regfile( input [7:0] datain, input clk, we, input [2:0] wa, input [2:0] raa, input [2:0] rab, output [7:0] porta, output [7:0] portb ); reg [7:0] mem [7:0];//r0 ~r255 always@(posedge clk) begin mem[0]<=0;//r0 always is 0 if(we) mem[wa]<=datain; end assign porta=mem[raa]; assign portb=mem[rab]; endmodule
11>栈:stack
/* * * file name : stack.v * author : Rill * date : 2013-08-11 * */ module stack( input clk, input rst, input wr_en, input rd_en, input [10:0] din, output [10:0] dout ); (* RAM_STYLE="DISTRIBUTED" *) reg [3:0] addr; reg [10:0] ram [15:0]; assign dout = ram[addr] +1; always@(posedge clk) begin if (rst) addr<=0; else begin if (wr_en==0 && rd_en==1) //leer if (addr>0) addr<=addr-1; if (wr_en==1 && rd_en==0) //guardar if (addr<15) addr<=addr+1; end end always @(posedge clk) begin if (wr_en) ram[addr] <= din; end endmodule
5,modelsim仿真 1>编写testbench 要进行仿真,需要编写对应的testbench,由于咱们这个cpu很简单,所以测试激励也很简单,代码如下:
/* * * file name : tiny_soc_tb.v * atthor : Rill * date : 2013-08-11 * */ `timescale 1ns / 1ps module tiny_soc_tb; reg clk; reg rst; always #5 clk = ~clk; initial begin #0 clk = 0; rst = 0; #15 rst = 1; #10 rst = 0; #1000 $stop; end soc_top soc_top ( .clk (clk), .rst (rst) ); endmodule
2>编写汇编代码及手动汇编 当然还要编写其汇编代码,如下:
然后我们要手动汇编成机器码,指令集都是自己定义的,所以是没有现成的compiler,只能手动汇编了,还好手动汇编要比手动反汇编轻松多了(之前,我们手动反汇编过OpenRISC的启动代码)。
汇编完成后,我们将机器码放到指令存储器里,如下,共七条指令。
3>仿真结果 完成上面的工作之后,我们就可以用仿真工具进行仿真了,下面是我用modelsim仿真的结果。
从波形可以清晰的看出七条指令的执行过程。在运算完成后拉高write_e信号并将1+2的运算结果3写到了ram地址是55(0x37)的地方