当完成整个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 运作。


