Menu Close

RISC-V EXU模块和CPU运行(1)rv32i_core模块

当完成整个RISC-V 内核模块后, 需要ITCM 模块 读取软件编写代码的机器码, 按照取指, 译码,执行,访存,写回等逻辑来执行当前的机器码, 这5个部分并不是每次都会发送,有时也会提前返回,比如 addi, 没有访存外部存储器。当前版本是RISC-V V2.01版, 使用的是状态机逻辑, 在后续的版本中, 我们会修改为流水线方式。

相关参考文章:

RISC-V 教学教案

RISC-V 地址空间

%title插图%num

代码如下:

module fii_rv32i_core 
# (
parameter [31:0] TMR_BASEADDR = 32'h0200_0000,
parameter [31:0] PLIC_BASEADDR = 32'h0c00_0000,
parameter [31:0] CPU_BASEADDR = 32'h8000_0000,
parameter [31:0] MEM_BASEADDR = 32'h9000_0000,
parameter [31:0] UART_BASEADDR = 32'he000_0000,
parameter [31:0] GPIO_BASEADDR = 32'hf000_0000
)
(
input sys_clk,

//====================================
input [ 31: 0 ] i_GPIO_dina,
output [ 31: 0 ] o_GPIO_douta,
output [ 31: 0 ] o_GPIO_ta,

input [ 31: 0 ] i_GPIO_dinb,
output [ 31: 0 ] o_GPIO_doutb,
output [ 31: 0 ] o_GPIO_tb,

input [ 31: 0 ] i_GPIO_dinc,
output [ 31: 0 ] o_GPIO_doutc,
output [ 31: 0 ] o_GPIO_tc,

input [ 31: 0 ] i_GPIO_dind,
output [ 31: 0 ] o_GPIO_doutd,
output [ 31: 0 ] o_GPIO_td,

output txd_start,
output [7:0] txd_data,
input txd_done,

//====================================
output [31:0] o_sft_int_v,
output [31:0] o_timer_l,
output [31:0] o_timer_h,

input [31:0] i_timer_l,
input [31:0] i_timer_h,

output [31:0] o_tcmp_l,
output [31:0] o_tcmp_h,

output [1:0] o_timer_valid,
output [31:0] o_tm_ctrl,


input [31:0] code_addr,
input [31:0] code_din,
input code_wea,
//====================================
input i_ext_irq,
input i_sft_irq,
input i_tmr_irq,

output o_meie,
output o_msie,
output o_mtie,
output o_glb_irq,
//====================================
input i_cpu_reset,
input rst_n
);

//===============================================================================
localparam [ 2: 0 ] IDLE = 8'd0,
I_FCH = 8'd1,
I_EXE = 8'd2,
I_LS = 8'd3,
I_WB = 8'd4,
I_RO1 = 8'd5,
I_RO2 = 8'd6,
I_RO3 = 8'd7;
wire ls_need;

// load program data
(* mark_debug = "yes" *)wire o_CPU_cs;
wire [ 31: 0 ] o_CPU_PC;
//===============================================================================
(* mark_debug = "yes" *) reg [ 2: 0 ] instr_st = 0;
reg [ 2: 0 ] instr_st_nxt = 0;
always@( posedge sys_clk )
if (( rst_n == 1'b0 ) | i_cpu_reset ) instr_st <= IDLE;
else instr_st <= instr_st_nxt;

always @ ( * )
begin
case ( instr_st )
IDLE: // 0
begin
if(!i_cpu_reset)
instr_st_nxt = I_FCH;
else
instr_st_nxt = IDLE;
end
I_FCH: // 1
begin
if(i_cpu_reset) instr_st_nxt = IDLE;
else instr_st_nxt = I_EXE;
end
I_EXE: // 2
begin
if ( ls_need & o_CPU_cs)
instr_st_nxt = I_RO1;
else
instr_st_nxt = I_WB;
end
I_WB: // 4
begin
instr_st_nxt = I_FCH;
end
I_RO1: // 5
begin
instr_st_nxt = I_RO2;
end
I_RO2: // 6
begin
instr_st_nxt = I_FCH;
end
I_RO3: // 7
begin
instr_st_nxt = I_FCH;
end
default : instr_st_nxt = IDLE;
endcase
end
//===============================================================================
(* mark_debug = "yes" *)reg irq_tch_r = 0;
always @ (posedge sys_clk or negedge rst_n)
if(~rst_n) irq_tch_r <= 0;
else if(instr_st == I_FCH) irq_tch_r <= (i_ext_irq | i_sft_irq | i_tmr_irq);


(* mark_debug = "yes" *)wire w_irq_src = o_CPU_cs ? 1'b0 : irq_tch_r;
(* mark_debug = "yes" *)wire jump_irq_pc = w_irq_src & o_glb_irq;
wire w_exp_src = 0;
wire irq_exp = w_irq_src | w_exp_src ;

wire mret;
wire [31:0] mepc;
wire [31:0] w_irq_pc;
(* mark_debug = "yes" *) reg [ 31: 0 ] i_fch_PC = 0;
reg [ 31: 0 ] res_PC = 0;
wire [31:0] w_exe_PC;

always @( posedge sys_clk or negedge rst_n )
if ( !rst_n ) i_fch_PC <= CPU_BASEADDR;
else
begin
if ( instr_st == IDLE ) //0
i_fch_PC <= CPU_BASEADDR;
else if ( instr_st == I_EXE ) //2
begin
if( mret )
i_fch_PC <= mepc;
else if (jump_irq_pc)
i_fch_PC <= w_irq_pc;
else if( o_CPU_cs )
begin
i_fch_PC <= o_CPU_PC;
res_PC <= w_exe_PC;
end
else i_fch_PC <= w_exe_PC;
end
else if ( instr_st == I_RO1 )
begin
i_fch_PC <= res_PC;
end
end

//===============================================================================
(* mark_debug = "yes" *) reg EXE_vld = 0;
always @( posedge sys_clk or negedge rst_n )
if ( !rst_n ) EXE_vld <= 0;
else
begin
if ( instr_st == I_FCH ) //1
EXE_vld <= 1'b1;
else
EXE_vld <= 1'b0;
end


wire [ 31: 0 ] instr;

(* mark_debug = "yes" *)reg [ 31: 0 ] exe_instr = 0;
always @( posedge sys_clk or negedge rst_n )
if ( !rst_n ) exe_instr <= 0;
else if ( instr_st == I_FCH )
exe_instr <= instr;

(* mark_debug = "true" *)reg i_CPU_load_vld = 0;
(* mark_debug = "true" *)reg [ 31: 0 ] i_CPU_load_data = 0;
always @( posedge sys_clk or negedge rst_n )
if ( !rst_n )
begin
i_CPU_load_data <= 0;
i_CPU_load_vld <= 0;
end
else if ( instr_st == I_RO2 )
begin
i_CPU_load_data <= instr;
i_CPU_load_vld <= 1'b1;
end
else i_CPU_load_vld <= 1'b0;

//===============================================================================

(* mark_debug = "yes" *) wire [ 31: 0 ] instr_PC = i_fch_PC;

rv32I_exu #
(
.TMR_BASEADDR ( TMR_BASEADDR ),
.PLIC_BASEADDR ( PLIC_BASEADDR ),
.CPU_BASEADDR ( CPU_BASEADDR ),
.MEM_BASEADDR ( MEM_BASEADDR ),
.UART_BASEADDR ( UART_BASEADDR ),
.GPIO_BASEADDR ( GPIO_BASEADDR )
)
rv32I_exu_inst
(
.sys_clk ( sys_clk ),

.i_ir ( exe_instr ), // The instruction register
.i_PC ( instr_PC ), // The PC register along with
.i_EXE_vld ( EXE_vld ),

.i_CPU_load_vld ( i_CPU_load_vld),
.i_CPU_load_data ( i_CPU_load_data ),

.o_ls_need ( ls_need ),

.o_exe_PC ( w_exe_PC ),

// load program data
.o_CPU_cs ( o_CPU_cs ),
.o_CPU_PC ( o_CPU_PC ),

.i_ext_irq ( i_ext_irq ),
.i_sft_irq ( i_sft_irq ),
.i_tmr_irq ( i_tmr_irq ),

.o_meie ( o_meie ),
.o_msie ( o_msie ),
.o_mtie ( o_mtie ),
.o_glb_irq ( o_glb_irq ),

.i_irq_src ( w_irq_src ),
.i_exp_src ( w_exp_src ),

.o_mret ( mret ),
.o_irq_pc ( w_irq_pc ),
.o_mepc ( mepc ),

//===============================================================================
.i_GPIO_dina ( i_GPIO_dina ),
.o_GPIO_douta ( o_GPIO_douta ),
.o_GPIO_ta ( o_GPIO_ta ),

.i_GPIO_dinb ( i_GPIO_dinb ),
.o_GPIO_doutb ( o_GPIO_doutb ),
.o_GPIO_tb ( o_GPIO_tb ),

.i_GPIO_dinc ( i_GPIO_dinc ),
.o_GPIO_doutc ( o_GPIO_doutc ),
.o_GPIO_tc ( o_GPIO_tc ),

.i_GPIO_dind ( i_GPIO_dind ),
.o_GPIO_doutd ( o_GPIO_doutd ),
.o_GPIO_td ( o_GPIO_td ),

.txd_start ( txd_start ),
.txd_data ( txd_data ),
.txd_done ( txd_done ),

.o_sft_int_v ( o_sft_int_v ),
.i_timer_l ( i_timer_l ),
.i_timer_h ( i_timer_h ),

.o_timer_l ( o_timer_l ),
.o_timer_h ( o_timer_h ),

.o_tcmp_l ( o_tcmp_l ),
.o_tcmp_h ( o_tcmp_h ),

.o_timer_valid ( o_timer_valid ),
.o_tm_ctrl ( o_tm_ctrl),

.i_cpu_reset ( i_cpu_reset),
.rst_n ( rst_n )
);
//===============================================================================
wire instr_ena = ( instr_PC[ 31: 16 ] == CPU_BASEADDR[31:16] ) ? 1'b1 : 1'b0;

TDP_RAM_INSTR program_inst
(
.clka ( sys_clk ),
.ena ( 1'b1 ),
.wea ( code_wea ),
.addra ( code_addr[31:2] ),
.dina ( code_din ),
.douta ( ),

.clkb ( sys_clk ),
.enb ( instr_ena ),
.web ( 1'b0 ),
.addrb ( instr_PC[ 31: 2 ] ), //8K 32bits, 32K byte
.dinb ( 32'b0 ),
.doutb ( instr )
);
//===============================================================================


endmodule

模块介绍

执行状态机, 使用两段式状态机。 

reg [ 2: 0 ] instr_st = 0;
reg [ 2: 0 ] instr_st_nxt = 0;
always@( posedge sys_clk )
if (( rst_n == 1’b0 ) | i_cpu_reset ) instr_st <= IDLE;
else instr_st <= instr_st_nxt;

always @ ( * )
begin
    case ( instr_st )
    IDLE:  // 0,状态机0 , 这是一个初始状态,用来使用uart 下载 汇编语言的机器码,平时状态机不使用这个状态。
    begin
        if(!i_cpu_reset)
            instr_st_nxt = I_FCH;
       else
            instr_st_nxt = IDLE;
    end
    I_FCH: // 1, 状态机1, 取指状态, 在这个状态下, pc 被赋值一个新的地址。
    begin
        if(i_cpu_reset) instr_st_nxt = IDLE;
        else instr_st_nxt = I_EXE;
    end
    I_EXE: // 2, 状态机2, 执行状态, 译码, 执行都在这个状态下。
    begin
        if ( ls_need & o_CPU_cs) // 如果是LOAD/STORE 指令,普林斯顿架构操作, 将地址指针跳转的相应的区域, 读取数据,之后再还原当前的pc 值。
            instr_st_nxt = I_RO1;
       else // 哈佛架构下,从数据区(DTCM ,外设) 访存相应的数据
           instr_st_nxt = I_WB;
    end
    I_WB: // 4, 状态机4, 负责 写回数据到32个通用寄存器
    begin
        instr_st_nxt = I_FCH;
    end
    I_RO1: // 5, 状态机5, 读双端口需要2个时钟周期,才能得到数据,这是一个等待周期。
    begin
        instr_st_nxt = I_RO2;
    end
    I_RO2: // 6,状态机6, LOAD/STORE 普林斯顿架构下的相应数据
    begin
        instr_st_nxt = I_FCH;
    end
    I_RO3: // 7, 状态机7 , 没有使用。
    begin
        instr_st_nxt = I_FCH;
    end
    default : instr_st_nxt = IDLE;
    endcase
end

 

reg irq_fch_r = 0;
always @ (posedge sys_clk or negedge rst_n)
if(~rst_n) irq_fch_r <= 0;
else if(instr_st == I_FCH) irq_fch_r <= (i_ext_irq | i_sft_irq | i_tmr_irq);

在取指状态下锁存 各种中断状态,包括外部中断,软件中断,timer 中断。

wire w_irq_src = o_CPU_cs ? 1’b0 : irq_fch_r;

wire jump_irq_pc = w_irq_src & o_glb_irq;

如果上一条指令为普林斯顿架构,需要让这条指令执行完毕, 同时在CSR 寄存器中设置的全局中断enable (o_glb_irq) , 这时产生的jump_irq_pc 有效。

 

wire mret;
wire [31:0] mepc;
wire [31:0] w_irq_pc;
reg [ 31: 0 ] i_fch_PC = 0;
reg [ 31: 0 ] res_PC = 0;
wire [31:0] w_exe_PC;

always @( posedge sys_clk or negedge rst_n )
if ( !rst_n ) i_fch_PC <= CPU_BASEADDR;
else
begin
    if ( instr_st == IDLE ) //0, 在使用uart 下载代码时, 当前pc 固定在起始位置。
        i_fch_PC <= CPU_BASEADDR;
    else if ( instr_st == I_EXE ) //2, 
    begin
        if( mret )  //如果当前指令时中断返回,mret, 将中断前的pc 还原给 当前pc
            i_fch_PC <= mepc;
        else if (jump_irq_pc) // 当中断发生时,将中断入口地址赋值给当前pc
            i_fch_PC <= w_irq_pc;
        else if( o_CPU_cs )  // 如果是普林斯顿架构load, 将存储数据的地址赋值给当前pc ,注意,这时读取的数据不是指令,而是load 地址下要读取的数据。
        begin
            i_fch_PC <= o_CPU_PC; 
            res_PC <= w_exe_PC; // 记录当前的pc值, load 结束后, 会返回。
        end
        else i_fch_PC <= w_exe_PC; // 正常的pc 指令, 要么 pc = pc + 4;要么相应的跳转指令(JAL,JALR, BRANCH) 等 所指向的地址。
    end
    else if ( instr_st == I_RO1 ) // 当普林斯顿架构下load 完成 , 返回当前指令运行的pc。
    begin
        i_fch_PC <= res_PC;
    end
end

 

reg EXE_vld = 0;
always @( posedge sys_clk or negedge rst_n )
if ( !rst_n ) EXE_vld <= 0;
else
begin
    if ( instr_st == I_FCH ) //1
        EXE_vld <= 1’b1;
    else
        EXE_vld <= 1’b0;
end

EXE_vld 和instr_st == EXE 指令同步,用来指示当前状态为 执行周期

wire [ 31: 0 ] instr;

reg [ 31: 0 ] exe_instr = 0;
always @( posedge sys_clk or negedge rst_n )
if ( !rst_n ) exe_instr <= 0;
else if ( instr_st == I_FCH )
    exe_instr <= instr;

同时,将需要执行的指令给译码,执行模块。

reg i_CPU_load_vld = 0;
reg [ 31: 0 ] i_CPU_load_data = 0;
always @( posedge sys_clk or negedge rst_n )
if ( !rst_n )
begin
    i_CPU_load_data <= 0;
    i_CPU_load_vld <= 0;
end
else if ( instr_st == I_RO2 )
begin
    i_CPU_load_data <= instr;
    i_CPU_load_vld <= 1’b1;
end
else i_CPU_load_vld <= 1’b0;

在普林斯顿架构下,将load 指令读到的数据 送给 lsu 模块 。

 

wire instr_ena = ( instr_PC[ 31: 16 ] == CPU_BASEADDR[31:16] ) ? 1’b1 : 1’b0;

TDP_RAM_INSTR program_inst
(
    .clka     ( sys_clk ),
    .ena      ( 1’b1 ),
    .wea     ( code_wea ),
    .addra  ( code_addr[31:2] ),
    .dina    ( code_din ),
    .douta  ( ),

    .clkb     ( sys_clk ),
    .enb      ( instr_ena ),
    .web     ( 1’b0 ),
    .addrb  ( instr_PC[ 31: 2 ] ), //8K 32bits, 32K byte
    .dinb    ( 32’b0 ),
    .doutb  ( instr )
);

参考文章:RISC-V 地址空间

%title插图%num

instr_ena 选择的时ITCM 地址空间。

这个双端口为ITCM :

  • 端口A :用户可以通过uart 口,将软件的汇编机器码下载到 ITCM; 
  • 端口B: instr_ena 是选中itcm, instr_PC[31:2]  是当前指令PC。instr 读取ITCM 的指令(如果是普林斯顿架构,也可以读取到的是数据)。
Posted in FPGA, FPGA, IC, RISC-V, RISC-V IPcore设计, RISC-V 教案, 教材与教案, 文章

发表评论

相关链接