一、控制流 (Control Flow)和 Trap
控制流(Control Flow)
branch(条件分支指令),jump(无条件跳转指令),由程序正常自主控制。
异常控制流(Exceptional Control Flow,简称ECP)
exception(异常),interrupt(中断),不在程序的控制范围内。
在 RISC-V 中把异常控制流统称为 Trap。

二、Exceptions, Traps, and Interrupts
在RISC-V官方手册的描述中:

1、异常(exception)指的是当前hart中运行的某条指令发生了一个非正常情况(unusual condition),如除法指令的除数为0,非法指令等。
2、中断(interrupt)则被描述为一个可能导致hart经历意外的控制转移的外部异步事件。
3、陷阱(trap)指的是由异常或中断引起的,到陷阱处理程序(trap handler)的控制转移。
这里我翻译的可能不太准确,下面是官方手册的英文版描述:

从在执行环境中运行的软件的角度来看,hart 在运行时遇到的 trap 可能有四种不同的类型,官方的描述如下:

Contained Trap
这类 trap 对执行环境中的软件可见,并且也由软件处理,如 user 模式下使用 ECALL 进行控制转移。

Requested Trap
这类 trap 是一个同步异常(synchronous exception),它是对执行环境的显式调用,代表执行环境中的软件请求操作,如系统调用(system call)。

Invisible Trap
执行环境会透明地(transparently)处理此类 trap(这里的透明地的意思是执行环境看不见,或者是意识不到这类 trap 的执行),并在处理后正常恢复执行(execution resume)。如缺页异常(page faults)。

Fatal Trap
这类 trap 一般代表发生了致命性的错误,会导致执行环境终止执行。

下表展示了各类 trap 的特征:

异常和中断的异同
一般来说,异常为当前执行的指令发生了“不正常的情况”,如除法指令的除数为0、缺页异常等等,异常发生后会跳转执行异常处理程序,视情况决定是否跳回发生异常的指令继续执行,如缺页异常在执行完缺页处理程序后会跳转到原指令继续执行,而非法指令引起的异常则不会跳回原指令继续执行。

而中断则是在当前执行流中发生了某个外部事件,需要暂停当前执行流去处理这个外部事件,和异常一样,处理完后也要视情况决定是否返回原执行流继续执行,但是中断一般是跳回发生中断的下一条指令继续执行,除了某些多周期指令被中断后需要重新执行,如除法指令。

中断又分本地(Local)中断和全局(Global)中断,本地中断又分为软件中断和定时器中断 ,系统调用就是一个典型的软件中断;全局中断为外部中断,如 uart、鼠标键盘等外设中断。

但是,站在处理器处理的过程来说,中断与异常其实并没有区别。当中断与异常发生时,处理器的表现形式就是,暂停当前执行的程序,转而执行处理中断或异常的处理程序,处理完后视情况恢复执行之前被暂停的程序。 通常我们所理解的中断与异常都可以被统称为广义上的异常。

广义上的异常被分为两种:
1、同步(synchronous)异常:执行某个程序流,能稳定复现的的异常,能比较精确的确定是那条指令引发的异常。(例如程序流里有一条非法指令,或者是ecall、ebreak指令。属于内因)
2、异步(asynchronous)异常:异常产生的原因与当前的程序流无关,与外部的中断事件有关。(由外部事件引起的,比如由定时器或者uart、鼠标等外设引起的中断。属于外因)

三、RISC-V的异常处理
RISC-V 定义的三种模式 User、Supervisor 和 Machine,均可发生异常。但是只有特权模式 Supervisor 和 Machine 才能处理异常,因为处理异常需要 CSR 寄存器。下面简单介绍一下 trap处理中比较重要的 csr 寄存器。

mtvec(Machine Trap-Vector Base-Address)
mtvec 的高30位 BASE 用来保存发生异常时处理器需要跳转到的地址,其低2位 MODE 用于控制入口函数的地址配置方式,所以基地址 BASE 必须保证四字节对齐。

根据 MODE 的配置,入口函数的配置分为 Direct 和 Vectored 两种方式:

Direct:所有的 exception 和 interrupt 发生后都跳转到 BASE 指定的地址处:

Vectored:exception 处理方式同 Direct;但 interrupt 的入口地址以数组方式排列:

mepc(Machine Exception Program Counter)
当 trap 发生时,pc 会被替换为 mtvec 设定的地址,同时 hart 会设置 mepc 为当前指令或者下一条指令的地址,当需要退出 trap 时可以调用特殊的 mret 指令,该指令会将 mepc 中的值恢复到 pc 中,从而实现返回的效果。当我们不想回到发生 trap 的地方执行的时候,我们可以在 trap 处理程序中修改 mepc 的值来达到改变 mret 返回地址的目的。

mcause(Machine Cause)
当 trap 发生时,hart 会设置该寄存器通知我们 trap 发生的原因,最高位 Interrupt 为1时说明当前 trap 为 interrupt,否则为 exception。

剩下的 Exception Code 用于标识具体的 interrupt 或 exception 的种类, 如下表:

mstatus(Machine Status)
用于跟踪和控制 hart 的状态,xIE(Interrupt Enable)用于打开或关闭对应模式下的全局中断,因为 riscv 默认是不支持嵌套中断的,所以在 trap 发生时,hart 会自动将 xIE 置0来关闭全局中断。xPIE(Previous Interrupt Enable),当 trap 发生时用于保存 trap 发生之前的 xIE 值。

RISC-V Trap 处理流程
在介绍完相关的 csr 寄存器后,接下来介绍一下 RISC-V 处理器在 Trap 发生后的处理流程。

Trap 处理又分为上半部分( Top Half )和下半部分( Bottom Half ):

上半部分由硬件执行, 执行流程如下:
1、处理器停止执行当前的程序流。
2、将 Trap 原因记录到 mcause 寄存器中。
3、将 Trap 的返回地址保存到 mepc 寄存器中。
4、将 Trap 发生时的存储器访问地址或者指令编码保存到 mtval 寄存器中。
5、更新 mstatus 状态寄存器(关闭全局中断位xIE,保存当前全局中断状态xIE到xPIE)。
6、最后 PC 跳转到 mtvec 寄存器定义的 BASE 地址开始执行( mtvec 寄存器的设置在 Trap 初始化步骤中完成)。

下半部分由软件执行,执行的程序基地址为 mtvec 寄存器中设置的 BASE 值,执行流程如下:

1、保存当前任务的上下文。
2、根据 mcause 中不同的 Trap 原因,跳转到不同的 Trap Handler 处理程序中执行。
3、从 Trap Handler 函数返回,mepc的值可能会有所调整。
4、恢复之前任务的上下文。
5、使用 mret 指令返回到 Trap 之前的状态。
四、实战
代码的 gitee 仓库如下:

一个基于RISC-V指令集的CPU实现

riscv_os: 一个RISC-V上的简易操作系统

Trap 上半部


在 cpu_prj 仓库的 FPGA/rtl/core/ 目录下的 clint.v 文件中,模块的输入输出引脚定义如下:

input    wire                    clk                 ,
input    wire                    rst_n               ,
    
input    wire[`INST_DATA_BUS]    ins_i               ,     
input    wire[`INST_ADDR_BUS]    ins_addr_i          , 
    
// from ex
input    wire                    jump_flag_i         ,
input    wire[`INST_ADDR_BUS]    jump_addr_i         ,
input    wire                    div_req_i           , // 除法操作执行请求信号
input    wire                    div_busy_i          , // 除法操作忙信号
    
// csr读写信号
output   reg                     wr_en_o             , // csr write enable
output   reg [`INST_ADDR_BUS]    wr_addr_o           , // csr write address
output   reg [`INST_REG_DATA]    wr_data_o           , // csr write data
output   reg [`INST_ADDR_BUS]    rd_addr_o           , // csr read address
input    wire[`INST_REG_DATA]    rd_data_i           , // csr read data
    
// from csr 
input    wire[`INST_REG_DATA]    csr_mtvec           , // mtvec寄存器
input    wire[`INST_REG_DATA]    csr_mepc            , // mepc寄存器
input    wire[`INST_REG_DATA]    csr_mstatus         , // mstatus寄存器
    
input    wire[`INT_BUS]          int_flag_i          , // 异步中断信号
output   wire                    clint_busy_o        , // 中断忙信号
output   reg [`INST_ADDR_BUS]    int_addr_o          , // 中断入口地址
output   reg                     int_assert_o          // 中断标志

中断仲裁逻辑代码如下, 首先判断 Trap 类型,是同步还是异步,还是特殊的 mret 指令,mret 指令用于返回到 mepc 中设置的地址,并且还原 mstatus的 内容。

    // 中断仲裁逻辑
    always @ (*) begin
        if (ins_i == `INS_ECALL || ins_i == `INS_EBREAK) begin
            // 如果执行阶段的指令为除法指令或者跳转指令,则先不处理同步中断
            if (div_req_i != 1'b1 && jump_flag_i != 1'b1) begin
                int_state = INT_SYNC_ASSERT;
            end 
            else begin
                int_state = INT_IDLE;
            end
        end 
        else if (int_flag_i != `INT_NONE && csr_mstatus[3] == 1'b1) begin
            int_state = INT_ASYNC_ASSERT;
        end 
        else if (ins_i == `INS_MRET) begin
            int_state = INT_MRET;
        end 
        else begin
            int_state = INT_IDLE;
        end
    end

根据不同的 Trap 类型,我们对 CSR 寄存器写不同的内容,如果是同步或异步 Trap ,我们要写 mepc、mcause、mstatus 寄存器;如果是 mret 返回指令,则只需要写 mstatus 寄存器。

   // 写CSR寄存器状态切换
    always @ (posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            csr_state <= CSR_IDLE;
            cause <= `ZERO_WORD;
            ins_addr <= `ZERO_WORD;
        end 
        else begin
            case (csr_state)
                CSR_IDLE: begin
                    // 同步中断
                    if (int_state == INT_SYNC_ASSERT) begin
                        csr_state <= CSR_MEPC;
                        // 在中断处理函数里会将中断返回地址加4
                        ins_addr <= ins_addr_i;
                        case (ins_i)
                            `INS_ECALL: begin
                                cause <= 32'd11;
                            end
                            `INS_EBREAK: begin
                                cause <= 32'd3;
                            end
                            default: begin
                                cause <= 32'd10;
                            end
                        endcase
                    end 
                    // 异步中断
                    else if (int_state == INT_ASYNC_ASSERT) begin
                        // timer中断
                        if (int_flag_i == `INT_TIMER) begin
                            cause <= 32'h80000004;
                        end
                        // uart中断,无总裁,目前这部分只用于测试
                        else if (int_flag_i == `INT_UART_REV) begin
                            cause <= 32'h8000000b;
                        end
                        else begin
                            cause <= 32'h0000000a;
                        end
                        
                        csr_state <= CSR_MEPC;
                        if (jump_flag_i == 1'b1) begin
                            ins_addr <= jump_addr_i;
                        end
                        // 异步中断可以中断除法指令的执行,中断处理完再重新执行除法指令
                        if (div_req_i == 1'b1 || div_busy_i == 1'b1) begin
                            ins_addr <= div_ins_addr;
                        end 
                        else begin
                            ins_addr <= ins_addr_i;
                        end
                    end 
                    // 中断返回
                    else if (int_state == INT_MRET) begin
                        csr_state <= CSR_MSTATUS_MRET;
                    end
                end
                CSR_MEPC: begin
                    csr_state <= CSR_MSTATUS;
                end
                CSR_MSTATUS: begin
                    csr_state <= CSR_MCAUSE;
                end
                CSR_MCAUSE: begin
                    csr_state <= CSR_IDLE;
                end
                CSR_MSTATUS_MRET: begin
                    csr_state <= CSR_IDLE;
                end
                default: begin
                    csr_state <= CSR_IDLE;
                end
            endcase
        end
    end
    // 发出中断信号前,先写几个CSR寄存器
    always @ (posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            wr_en_o <= 1'b0;
            wr_addr_o <= `ZERO_WORD;
            wr_data_o <= `ZERO_WORD;
        end 
        else begin
            case (csr_state)
                // 将mepc寄存器的值设为当前指令地址
                CSR_MEPC: begin
                    wr_en_o <= 1'b1;
                    wr_addr_o <= {20'h0, `CSR_MEPC};
                    wr_data_o <= ins_addr;
                end
                // 写中断产生的原因
                CSR_MCAUSE: begin
                    wr_en_o <= 1'b1;
                    wr_addr_o <= {20'h0, `CSR_MCAUSE};
                    wr_data_o <= cause;
                end
                // 关闭全局中断
                CSR_MSTATUS: begin
                    wr_en_o <= 1'b1;
                    wr_addr_o <= {20'h0, `CSR_MSTATUS};
                    wr_data_o <= {csr_mstatus[31:4], 1'b0, csr_mstatus[2:0]};
                end
                // 中断返回
                CSR_MSTATUS_MRET: begin
                    wr_en_o <= 1'b1;
                    wr_addr_o <= {20'h0, `CSR_MSTATUS};
                    wr_data_o <= {csr_mstatus[31:4], csr_mstatus[7], csr_mstatus[2:0]};
                end
                default: begin
                    wr_en_o <= 1'b0;
                    wr_addr_o <= `ZERO_WORD;
                    wr_data_o <= `ZERO_WORD;
                end
            endcase
        end
    end

Trap 下半部


源码在 riscv_os 仓库的 04_Traps 目录下,主要是 entry.S 和 trap.c 两个文件。

在 entry.S 汇编文件中定义了 trap_vector 函数,mtvec 寄存器中的值要保存为 trap_vector 函数的地址。大致执行流程为先保存当前任务的上下文,然后调用 trap_handler 函数,并将 mepc 、mcause 寄存器的值作为参数传给它,trap_handler 函数将修改后的mepc的值返回,然后存入 mepc 寄存器中,最后在恢复之前任务的上下文,再使用 mret 指令返回。

trap_vector:
        # save context(registers).
        csrrw   t6, mscratch, t6        # swap t6 and mscratch
        reg_save t6
 
        # Save the actual t6 register, which we swapped into
        # mscratch
        mv      t5, t6          # t5 points to the context of current task
        csrr    t6, mscratch    # read t6 back from mscratch
        sw      t6, 120(t5)     # save t6 with t5 as base
 
        # Restore the context pointer into mscratch
        csrw    mscratch, t5
 
        # call the C trap handler in trap.c
        csrr    a0, mepc
        csrr    a1, mcause
        call    trap_handler
 
        # trap_handler will return the return address via a0.
        csrw    mepc, a0
 
        # restore context(registers).
        csrr    t6, mscratch
        reg_restore t6
 
        # return to whatever we were doing before trap.
        mret

在 trap.c 文件中定义了 trap_init() 和 trap_handler() 函数,第一个用于初始化 mtvec 寄存器的内容为 trap_vector 函数的地址,并且开启 mstatus 寄存器中 machine 模式下的全局中断;第二个用于根据不同的 mcause 来进行相应的处理。

void trap_init()
{
        /*
         * set the trap-vector base-address for machine-mode
         */
        w_mtvec((reg_t)trap_vector);
        w_mstatus((reg_t)0x88);
}
 
reg_t trap_handler(reg_t epc, reg_t cause)
{
        reg_t return_pc = epc;
        reg_t cause_code = cause & 0xfff;
        if(cause & 0x80000000) {
                /* Asynchronous trap - interrupt */
                switch(cause_code) {
                        case 3:
                                uart_puts("software interruption!\n");
                                break;
                        case 4:
                                uart_puts("user timer interruption!\n");
                                break;
                        case 11:
                                uart_puts("external interruption!\n");
                                uart_int_handler();
                                break;
                        default:
                                uart_puts("unknown async exception!\n");
                                break;
                }
        } else {
                /* Synchronous trap - exception */
                printf("cause = %d\n", cause);
                uart_puts("OOPS! What can I do!\n");
                return_pc = return_pc + 4;
        }
        //printf("return_pc = %d\n", return_pc);
        return return_pc;
}

上板测试

使用 uart 的串口接收中断来模拟异步 trap,下面是 uart 在接收到数据后进行的中断处理函数。

void uart_int_handler(void)
{
        char begin_char = uart_getc();
        if (begin_char == 'e') {
                uart_puts("Input your command, and end with 'Enter':\n");
                while (1) {
                        char c = uart_getc();
                        uart_putc(c);
                        if (c == '\n') {
                                break;
                        }
                }
                uart_puts("Received your command!\n");
        } else {
                uart_puts("Please send 'e' first, then enter your command\n");
        }
}

在 user.c 中定义了一个 ebreak 函数来模拟同步 trap 。

下面将程序 make 编译构建后,烧录到板子上,打开串口调试工具,可以看到如下输出,说明 ebreak 指令成功执行,进入 trap 处理程序执行后返回原来的任务继续执行:


接下来测试 uart 的异步 trap,首先发送字符 e ,可以看到 uart 中断处理程序成功执行,等待用户的进一步输入:


然后输入想要打印的指令,最后一定要加一个回车!!(因为程序中是通过回车来判断结尾的):


点击发送后可以看到指令成功被打印,打印之后程序继续返回原来的 task 继续执行:


至此,trap 的上板实验结束,有了 trap 里面的异常和中断,我们可以进一步实现操作系统里更加高级的功能——定时器中断和系统调用!!