当完成整个RISC-V 内核模块后, 需要ITCM 模块 读取软件编写的代码的机器码, 按照取指, 译码,执行,访存,写回等逻辑来执行当前的机器码, 这5个部分并不是每次都会发送,有时也会提前返回,比如 addi, 没有访存外部存储器。在版本是RISC-V V3.00版和之后的版本中, 使用的是流水线方式来处理cpu 指令。我们采用的是3级流水线方式。
相关参考文章:
RISC-V教学教案
流水线框图:
图中蓝色方块都是D触发器锁存。 代表流水线的级别。
第一级流水线:
由于我们使用dual port memory 作为ITCM (指令存储器),所以每次给出地址之后, 需要等待一个时钟周期, 然后才能得到相应的指令数据。所以取指的过程需要2个时钟周期。 但这个部分依然属于第一级流水线范畴(取指)。如果使用reg 堆积出来一个ram, 将会节省一个时钟周期, 缺点是占用大量的fpga 系统资源。
第二级流水线:
处理整个的译码,执行阶段。包括decoder (译码), ALU(执行),branch (分支指令执行),CSR (寄存器相关的执行)
第三级流水线:
写回 32个通用寄存器(regfile)。如果是LOAD/STORE 相关的指令,处理外设相关的操作。这个部分会被连接到总线上,包括gpio ,timer, Uart, 以及之后risc-v cpu 扩展所需要的外设。
cpu 取指模块流水线:
`timescale 1ns / 1ps module fii_instr_rib # ( parameter [31:0] DBG_BASEADDR = 32'h0000_0800, 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] PWM_BASEADDR = 32'hf000_2000, parameter [31:0] GPIO_BASEADDR = 32'hf000_0000 ) ( input sys_clk, output reg [31:0] o_curr_pc, output [31:0] o_curr_instr, //=============================================================================== input i_pc_chg_flag, input i_ir_hold_flag, input [31:0] i_exe_next_PC, output o_CPU_load_vld, //=============================================================================== input i_ext_irq, input i_sft_irq, input i_tmr_irq, input o_glb_irq, // mstatus[3]; enable all Machine Mode interrupt input [31:0] i_dbg_entry_adr, // jtag debug entry address input i_dbg_entry_set, // jtag debug entry address enable ( one clock ) input [31:0] i_dpc_r, input [31:0] i_dbg_instr, input dret, input [31:0] i_mepc, input [31:0] i_vect_pc, // mtvec regitesr content output o_irq_src, output o_exp_src, input mret, input dbg_start, //=============================================================================== input itcm_wea, input [31:0] itcm_addra, input [31:0] itcm_dina, //=============================================================================== input [31:0] i_rib_saddr, input [31:0] i_rib_sdin, output [31:0] o_rib_sdout, input i_rib_svalid, output [1:0] o_rib_sready, input [3:0] i_rib_swe, input i_rib_srd, input i_rib_sop, //=============================================================================== // master bus signal input i_rib_mvalid, input [1:0] i_rib_mready, //=============================================================================== input i_cpu_reset, input rst_n ); //=============================================================================== // cpu master bus operation // long instruction : dec_instr should be set as 'INST_NOP, during long instruction. reg rib_delay_op = 0; always @ ( posedge sys_clk or negedge rst_n) if(!rst_n) rib_delay_op <= 0; //else if ( o_rib_mvalid & ~i_rib_mready[0] ) rib_delay_op <= 1; else if ( i_rib_mvalid ^ i_rib_mready[0] ) rib_delay_op <= 1; else rib_delay_op <= 0; wire bus_hsk_ok = i_rib_mvalid & i_rib_mready[0]; // long period load & store operation; normal operation long_ir_flag is 0 wire long_ir_flag = ( i_rib_mvalid & (~i_rib_mready[0]) ) | rib_delay_op; // long period instruction pos edge; normal operation long_ir_cs is 0 wire long_ir_cs = ( i_rib_mvalid & (~i_rib_mready[0]) ) & i_rib_sop; //=============================================================================== wire o_CPU_load_cs = ( i_rib_saddr[ 31: 16 ] == CPU_BASEADDR[ 31: 16 ] ) ? i_rib_sop : 1'b0; /* ls_r is used for 0x8000_0000 DATA load instruction, becasue 0x8000_0000 is dual port ram , it will be delay 2 clock (output data form dual port ram), the ls_r[1] is signal that o_cpu_load_cs delay 2 clock */ reg [1:0] ls_r = 0; always @( posedge sys_clk or negedge rst_n) if(!rst_n) ls_r <= 0; else ls_r <= {ls_r[0], o_CPU_load_cs}; assign o_CPU_load_vld = ls_r[1]; assign o_rib_sready = {1'b0, ls_r[1]}; //=============================================================================== wire [31:0] pc_instr; wire [31:0] i_CPU_load_data = pc_instr; wire [ 4: 0 ] data_sft = {i_rib_saddr[ 1: 0 ], 3'b000}; //i_D_PC[ 1: 0 ] * 8; wire [ 31: 0 ] o_CPU_dout = i_CPU_load_data >> data_sft; assign o_rib_sdout = o_CPU_dout; //=============================================================================== reg [ 31: 0 ] instr_fch_PC = 0; //=============================================================================== /* next_addr is relevent o_cpu_load_cs, for example : current program pc is 0x8000_0004, (load instruction ,data at 0x8000_01000), next_addr will be next program pc 0x8000_0004 + 4, after load instruction finish. */ reg [31:0] next_addr = 0; always @( posedge sys_clk ) if(!rst_n | i_cpu_reset) next_addr <= CPU_BASEADDR; else if(i_rib_sop) next_addr <= instr_fch_PC; //=============================================================================== assign o_exp_src = 0; wire nop_flag; // don't generate interrupt when cpu load & flush instruction assign o_irq_src = (long_ir_flag | nop_flag) ? 1'b0 : (i_ext_irq | i_sft_irq | i_tmr_irq); wire irq_exp_flag = (o_irq_src & o_glb_irq) | o_exp_src ; // edge o_irq_src high, then o_glb_irq low //=============================================================================== reg [31:0] jump_addr = 0; always @( * ) begin if(i_dbg_entry_set) jump_addr = i_dbg_entry_adr; else if(dret) jump_addr = i_dpc_r; else if (long_ir_cs) // long address & (addr == 0x8000_xxxx) jump_addr = {i_rib_saddr[31:2], 2'b00}; else if( irq_exp_flag ) jump_addr = i_vect_pc; // mtvec regitesr content else if( mret ) jump_addr = i_mepc; // The PC before enter interrupt else if( i_ir_hold_flag ) jump_addr = o_curr_pc; // long term instruction ,pc hold at current position else jump_addr = i_exe_next_PC; end wire w_jump_flag = i_dbg_entry_set | dret | irq_exp_flag | i_pc_chg_flag | mret | long_ir_cs | i_ir_hold_flag; //=============================================================================== //stage 1 always @( posedge sys_clk ) if (( !rst_n ) | i_cpu_reset ) instr_fch_PC <= CPU_BASEADDR; else begin if (w_jump_flag) instr_fch_PC <= jump_addr; else if( ~bus_hsk_ok & rib_delay_op ) // long period load & store instr_fch_PC <= next_addr; else instr_fch_PC <= instr_fch_PC + 4; end //=============================================================================== //stage 2 always @( posedge sys_clk or negedge rst_n ) if ( !rst_n ) o_curr_pc <= CPU_BASEADDR; else o_curr_pc <= i_ir_hold_flag ? o_curr_pc : (bus_hsk_ok & rib_delay_op) ? next_addr : instr_fch_PC; //else o_curr_pc <= instr_fch_PC; //=============================================================================== /* next clock should be output a inst_nop , because dual port ram , address jump, the pipeline address (current address + 4) will be ignore, */ reg waiting_r = 0; always @ (posedge sys_clk or negedge rst_n) if(!rst_n) waiting_r <= 0; else waiting_r <= w_jump_flag; //=============================================================================== // w_dbg_pc point to 0x8000_xxxxx address , we need get data from dual port ram , // so, w_dbg_pc need 2 clock to get the data(0x8000_xxxx) wire [31:0] w_dbg_pc = instr_fch_PC; localparam [ 31: 0 ] DBG_RAM_ADDR = 32'h0000_0400; // 0x400 - 0x800 wire dbg_addr_sel = (w_dbg_pc[31:12] == DBG_BASEADDR[31:12]) & (w_dbg_pc[11:10] != 2'b00) ? 1'b1 : 1'b0; reg dbg_addr_cs = 0; always @ (posedge sys_clk or negedge rst_n) if(!rst_n) dbg_addr_cs <= 0; else dbg_addr_cs <= dbg_addr_sel; //=============================================================================== wire [31:0] instr_data_sel = dbg_addr_cs ? i_dbg_instr : pc_instr ; assign o_curr_instr = (waiting_r) ? `INST_NOP : instr_data_sel; assign nop_flag = (o_curr_instr == `INST_NOP) ? 1'b1 : 1'b0; //=============================================================================== wire instr_ena = ( instr_fch_PC[ 31: 16 ] == CPU_BASEADDR[31:16] ) ? 1'b1 : 1'b0; TDP_RAM_INSTR program_inst ( .clka ( sys_clk ), .ena ( 1'b1 ), .wea ( itcm_wea ), .addra ( itcm_addra[31:2] ), .dina ( itcm_dina ), .douta ( ), .clkb ( sys_clk ), .enb ( instr_ena ), .web ( 1'b0 ), .addrb ( instr_fch_PC[ 31: 2 ] ), //8K 32bits, 32K byte .dinb ( 32'b0 ), .doutb ( pc_instr ) ); //=============================================================================== endmodule
端口说明:
input sys_clk, // 系统时钟
output reg [31:0] o_curr_pc, // 输出 当前的PC 值
output [31:0] o_curr_instr, // 输出 当前的 指令值
//=======================================================================
input i_pc_chg_flag, // 译码,执行模块通知 pc 值需要更改,不再是正常的 + 4
input i_ir_hold_flag, // 处理乘除法指令
input [31:0] i_exe_next_PC, // 译码,执行模块通知 下一个pc 值
output o_CPU_load_vld, // 普林斯顿架构 itcm 提供的数据有效
//=======================================================================
input i_ext_irq, // 外部中断
input i_sft_irq, // 软件中断
input i_tmr_irq, // timer中断
input o_glb_irq, // CSR 全局中断位,mstatus[3]; enable all Machine Mode interrupt
input [31:0] i_dbg_entry_adr, // jtag 调试入口地址,jtag debug entry address
input i_dbg_entry_set, // jtag 调试入口 enable, jtag debug entry address enable ( one clock )
input [31:0] w_dpc_r, // jtag 调试 pc
input [31:0] w_dbg_instr, // jtag 调试 指令
input dret, // jtag 调试中断 返回
input [31:0] i_mepc, // 中断时,存储的当前pc 值
input [31:0] i_vect_pc, // 中断时, 跳入中断的pc 值,mtvec regitesr content
output o_irq_src, // 通知 CSR 模块 中断信号线
output o_exp_src, // 通知 CSR 模块 异常信号线
input mret, // 中断返回 信号
input dbg_start, // 进入jtag 调试 信号
//=======================================================================
input itcm_wea, // ITCM 更新 写 enable
input [31:0] itcm_addra, // ITCM 更新 地址
input [31:0] itcm_dina, // ITCM 更新 数据
//=======================================================================
// riscv 内部总线 slave 总线
input [31:0] i_rib_saddr, // riscv 内部总线 地址
input [31:0] i_rib_sdin, // riscv 内部总线 写数据
output [31:0] o_rib_sdout,// riscv 内部总线 读数据
input i_rib_svalid, // riscv 内部总线 指令有效
output [1:0] o_rib_sready, // riscv 内部总线 ,外设读取的数据有效
input [3:0] i_rib_swe, // riscv 内部总线 写 enable
input i_rib_srd, // riscv 内部总线 读 信号
input i_rib_sop, // riscv 内部总线 指令操作 enable
//=======================================================================
// riscv 内部总线 master 总线,master bus signal
input i_rib_mvalid, // riscv 内部总线指令有效
input [1:0] i_rib_mready,// riscv 内部总线,外部读取的数据有效
//=======================================================================
长周期指令操作:
在长周期的操作时,需要插入空指令,保持流水线连续。
// cpu master bus operation
// long instruction : dec_instr should be set as ‘INST_NOP, during long instruction.
reg rib_delay_op = 0;
always @ ( posedge sys_clk or negedge rst_n)
if(!rst_n) rib_delay_op <= 0;
else if ( i_rib_mvalid ^ i_rib_mready[0] ) rib_delay_op <= 1;
else rib_delay_op <= 0;
wire bus_hsk_ok = i_rib_mvalid & i_rib_mready[0];
// long period load & store operation; normal operation long_ir_flag is 0
wire long_ir_flag = ( i_rib_mvalid & (~i_rib_mready[0]) ) | rib_delay_op;
// long period instruction pos edge; normal operation long_ir_cs is 0
wire long_ir_cs = ( i_rib_mvalid & (~i_rib_mready[0]) ) & i_rib_sop;
长周期指令,包括普林斯顿结构数据读取, jtag 相关指令。
普林斯顿架构下, 读取的数据延迟:
reg [1:0] ls_r = 0;
always @( posedge sys_clk or negedge rst_n)
if(!rst_n) ls_r <= 0;
else
ls_r <= {ls_r[0], o_CPU_load_cs};
assign o_CPU_load_vld = ls_r[1];
assign o_rib_sready = {1’b0, ls_r[1]};
由于使用的时双端口 memory, 需要延迟一个时钟周期,配合总线一起使用。
lb,lw,lh等load 指令的相关处理:
wire [ 4: 0 ] data_sft = {i_rib_saddr[ 1: 0 ], 3’b000}; //i_D_PC[ 1: 0 ] * 8;
wire [ 31: 0 ] o_CPU_dout = i_CPU_load_data >> data_sft;
assign o_rib_sdout = o_CPU_dout;
普林斯顿架构下,保存当前pc操作:
reg [31:0] next_addr = 0;
always @( posedge sys_clk )
if(!rst_n | i_cpu_reset) next_addr <= CPU_BASEADDR;
else if(i_rib_sop) next_addr <= instr_fch_PC;
中断源处理:
assign o_exp_src = 0;
wire nop_flag;
// don’t generate interrupt when cpu load & flush instruction
assign o_irq_src = (long_ir_flag | nop_flag) ? 1’b0 : (i_ext_irq | i_sft_irq | i_tmr_irq);
wire irq_exp_flag = (o_irq_src & o_glb_irq) | o_exp_src ; // edge w_irq_src high, then o_glb_irq low
长周期指令, 空指令等需要等待, 其他情况时,触发中断。
各种情况下的的pc 跳转:
reg [31:0] jump_addr = 0;
always @( * )
begin
if(i_dbg_entry_set)
jump_addr = i_dbg_entry_adr;
else if(dret)
jump_addr = i_dpc_r;
else if (long_ir_cs) // long address & (addr == 0x8000_xxxx)
jump_addr = {i_rib_saddr[31:2], 2’b00};
else if( irq_exp_flag )
jump_addr = i_vect_pc; // mtvec regitesr content
else if( mret )
jump_addr = i_mepc; // The PC before enter interrupt
else if( i_ir_hold_flag )
jump_addr = o_curr_pc; // long term instruction ,pc hold at current position
else jump_addr = i_exe_next_PC;
end
准备跳转的pc:
wire w_jump_flag = i_dbg_entry_set | dret | irq_exp_flag | i_pc_chg_flag | mret | long_ir_cs | i_ir_hold_flag;
always @( posedge sys_clk )
if (( !rst_n ) | i_cpu_reset ) instr_fch_PC <= CPU_BASEADDR;
else
begin
if (w_jump_flag)
instr_fch_PC <= jump_addr;
else if( ~bus_hsk_ok & rib_delay_op ) // long period load & store
instr_fch_PC <= next_addr;
else
instr_fch_PC <= instr_fch_PC + 4;
end
实际要跳转的pc:
always @( posedge sys_clk or negedge rst_n )
if ( !rst_n ) o_curr_pc <= CPU_BASEADDR;
else o_curr_pc <= i_ir_hold_flag ? o_curr_pc : (bus_hsk_ok & rib_delay_op) ? next_addr : instr_fch_PC;
空指令enable 信号:
当发生跳转时(不是正常的pc + 4),需要插入空指令,保持流水线连续,
reg waiting_r = 0;
always @ (posedge sys_clk or negedge rst_n)
if(!rst_n) waiting_r <= 0;
else waiting_r <= w_jump_flag;
assign o_curr_instr = (waiting_r) ? `INST_NOP : instr_data_sel;
流水线操作要比状态机操作复杂一些, 尤其时增加了jtag 的调式功能之后。这个部分可以参考以上的代码, 同时仿真整个risc-v 的工程,帮助了解流水线操作的相关cpu 运作。