4. 实现中断向量表

任务目标

在RISC-V M-Mode下完成中断向量表(mtvec)的初始化,实现一个中断服务程序(ISR),使其能够处理来自U-Mode触发的软件中断(MSIP),并在处理完成后正确返回到U-Mode继续执行。

必要环境与依赖

  • RISC-V 64位(RV64)或32位(RV32)工具链(如riscv64-unknown-elf-gcc)
  • QEMU模拟器(支持RISC-V virt机器)或等效硬件平台
  • 无需外部库,直接操作CSR寄存器

核心实现

以下代码在M-Mode下完成中断向量表设置,并通过ecall指令从U-Mode触发软件中断。中断服务程序读取mcause确认中断来源,清除软件中断挂起位,然后通过mret返回U-Mode。

#include <stdint.h>

// CSR寄存器地址定义
#define CSR_MTVEC    0x305
#define CSR_MSTATUS  0x300
#define CSR_MCAUSE   0x342
#define CSR_MIP      0x344
#define CSR_MIE      0x304

// 软件中断位(MSIP对应MIP的第3位)
#define MIP_MSIP     (1 << 3)

// 中断使能函数
static inline void csr_write(uint64_t csr, uint64_t value) {
    __asm__ volatile ("csrw %0, %1" : : "i"(csr), "r"(value));
}

static inline uint64_t csr_read(uint64_t csr) {
    uint64_t value;
    __asm__ volatile ("csrr %0, %1" : "=r"(value) : "i"(csr));
    return value;
}

// 中断服务程序(M-Mode)
__attribute__((interrupt))
void m_trap_handler(void) {
    uint64_t mcause = csr_read(CSR_MCAUSE);
    
    // 检查是否为软件中断(mcause[63] = 1 表示中断,低6位为3表示软件中断)
    if ((mcause & 0x8000000000000000ULL) && ((mcause & 0x3F) == 3)) {
        // 清除软件中断挂起位(写入MIP寄存器)
        uint64_t mip = csr_read(CSR_MIP);
        mip &= ~MIP_MSIP;
        csr_write(CSR_MIP, mip);
        
        // 设置返回地址,跳过ecall指令继续执行
        uint64_t mepc = 0;
        __asm__ volatile ("csrr %0, 0x341" : "=r"(mepc));
        mepc += 4;  // 跳过ecall指令(4字节)
        __asm__ volatile ("csrw 0x341, %0" : : "r"(mepc));
    }
    // 其他中断类型可在此扩展
}

// U-Mode软件中断触发函数
void trigger_software_interrupt(void) {
    __asm__ volatile ("ecall");
}

// 主函数
int main(void) {
    // 1. 设置中断向量表地址(M-Mode)
    csr_write(CSR_MTVEC, (uint64_t)&m_trap_handler);
    
    // 2. 使能M-Mode全局中断和软件中断
    uint64_t mstatus = csr_read(CSR_MSTATUS);
    mstatus |= (1 << 3);  // MIE位置1
    csr_write(CSR_MSTATUS, mstatus);
    
    uint64_t mie = csr_read(CSR_MIE);
    mie |= MIP_MSIP;  // 使能软件中断
    csr_write(CSR_MIE, mie);
    
    // 3. 切换到U-Mode并触发软件中断
    // 设置返回地址为中断触发函数
    uint64_t mepc = (uint64_t)&trigger_software_interrupt;
    __asm__ volatile ("csrw 0x341, %0" : : "r"(mepc));
    
    // 设置MPP为U-Mode(00),然后执行mret切换到U-Mode
    mstatus = csr_read(CSR_MSTATUS);
    mstatus &= ~(3 << 11);  // 清除MPP位
    mstatus |= (0 << 11);   // MPP = 00 (U-Mode)
    csr_write(CSR_MSTATUS, mstatus);
    
    __asm__ volatile ("mret");
    
    // 中断处理完成后会返回到这里(U-Mode)
    // 验证:如果到达此处,说明中断处理成功并正确返回
    volatile int success = 1;
    
    // 死循环,防止程序退出
    while(1) {
        // 空循环
    }
    
    return 0;
}

编译与运行

# 编译(RV64)
riscv64-unknown-elf-gcc -march=rv64gc -mabi=lp64d -nostdlib -nostartfiles -Ttext=0x80000000 -o interrupt_test.elf interrupt_test.c

# 使用QEMU运行
qemu-system-riscv64 -machine virt -kernel interrupt_test.elf -nographic

验证方法与判定标准

  1. 编译验证:上述编译命令应无错误通过,生成interrupt_test.elf文件。
  2. 运行验证:在QEMU中运行后,程序应进入while(1)死循环且不崩溃。若在调试器中设置断点于success = 1处,应能命中该断点,证明中断处理完成并正确返回U-Mode。
  3. 中断路径验证:在m_trap_handler入口处设置断点,运行后应能观察到程序进入该处理函数,且mcause值为0x8000000000000003(表示M-Mode软件中断)。
  4. 返回验证:单步执行mret后,mstatus.MPP应恢复为U-Mode(00),程序计数器应指向trigger_software_interrupt之后的指令(即ecall的下一条指令)。

关键说明

  • 中断向量表采用直接跳转模式(mtvec.MODE = 0),即mtvec直接存放处理函数地址。
  • U-Mode通过ecall触发环境调用,在M-Mode下表现为软件中断(mcause = 3)。
  • 处理软件中断时需清除mip.MSIP位,否则中断会持续触发。
  • 返回U-Mode前需调整mepc跳过ecall指令(RV64下为4字节),否则会陷入无限中断循环。
  • 本实现使用__attribute__((interrupt))确保编译器生成正确的保存/恢复上下文代码,无需手动保存寄存器。