GD32VF103中断机制(非向量中断)

目录

本文基于GD32VF103 MCU简单介绍RISCV的非向量中断方式, 并在RV-STAR开发板上实现验证。

完整程序下载地址:https://www.jiawei.site/downloads/gd32/gd32_irq_direct.zip

中断机制与Direct模式简介

中断与异常

在RISC-V架构中,trap是对所有“打断当前执行流”的统一称呼,包含:

  • 异常 (Exception):由指令执行引发,例如非法指令、地址对齐错误、环境调用 (ecall)。
  • 中断 (Interrupt):由外部事件引发,例如定时器溢出、外设信号。

这两类事件都会导致 CPU 跳转到一个预先设定的入口地址,进入 trap 处理程序。

中断处理入口:mtvec

Machine模式下,mtvec CSR (Machine Trap Vector Base Address) 定义了trap 的入口地址,并支持两种模式:

  • Direct 模式(非向量模式):所有trap都跳到同一个入口地址。
  • Vectored 模式(向量模式):中断根据ID跳转到不同的偏移地址,异常仍然跳到统一入口。

这两种模式由 mtvec 的低两位决定:

  • 00 = Direct
  • 01 = Vectored

在Direct模式下:

  • 任何中断或异常都会跳到 mtvec 所设定的同一个入口。
  • 软件需要在入口里读取mcause,解析 trap 的类型和编号, 然后再分派到相应的处理函数。

这样做的好处是实现简单,入口固定,适合资源有限的 MCU。 缺点是所有 trap 共用一个入口,调度逻辑完全由软件完成。

非向量模式中断相关CSR

  1. 中断入口与原因
  • mtvec(Machine Trap-Vector Base Address Register)

    • 保存陷入入口地址,决定中断/异常处理程序的起始点。
    • 可以设置为 direct 模式(所有 trap 到同一个入口)或 vectored 模式(中断向量表)。
  • mcause(Machine Cause Register)

    指示 trap 的来源:

    • 最高位(bit XLEN-1):1 = 中断,0 = 异常。
    • 低12位:表示具体的中断 ID 或异常原因码。
  1. 程序计数器保存与恢复
  • mepc(Machine Exception Program Counter)
    • 保存 trap 发生时的 PC。
    • 中断/异常处理结束后,执行 mret 会跳回 mepc 指向的地址 (通常是触发点的下一条指令)。
  1. 状态与中断使能
  • mstatus(Machine Status Register)

    • 关键字段:
      • MIE:全局中断使能位。
      • MPIE:保存进入 trap 前的中断使能状态(mret 时恢复)。
      • MPP:保存 trap 前的特权级(00=U, 11=M)。
    • 进入 trap 时硬件会自动修改这些位,返回时由 mret 恢复。
  • mie(Machine Interrupt Enable Register)

    • 控制各个中断源的使能。常见位:
      • MSIE:软件中断使能
      • MTIE:定时器中断使能
      • MEIE:外部中断使能
  • mip(Machine Interrupt Pending Register)

    • 显示哪些中断正在挂起(由硬件置位,软件可读)。

典型的中断处理流程

Direct 模式下,中断的典型处理流程如下:

  1. CPU 硬件保存
  • 将 pc 保存到 mepc。
  • 将 trap 原因写入 mcause。
  • 将出错地址写入 mtval(若适用)。
  1. 软件入口 (汇编)
  • 保存通用寄存器到栈。
  • 读取 mcause 判断 trap 来源。
  • 跳转到相应的 C 语言处理函数。
  1. 中断服务函数 (C)
  • 完成具体处理逻辑,例如定时器中断里更新mtime/mtimecmp,清除 pending。
  1. 恢复上下文并返回
  • 恢复寄存器。
  • mret 返回用户代码。

实践:定时器中断

对本站文章从零开始创建GD32VF103 Makefile工程 的程序进行扩充,添加定时器中断的处理部分,每隔 1秒进入中断,在中断处理函数中切换控制LED的IO口 的电平状态实现LED亮灭循环。

entry.S

.macro SAVE_CONTEXT
    addi sp, sp, -33 * 4
    sw x1, 0 * 4(sp)
    sw x2, 1 * 4(sp)
    sw x3, 2 * 4(sp)
    ; ...
    sw x31, 30 * 4(sp)
    csrr t0, mepc
    sw t0, 31 * 4(sp)
    csrr t0, mstatus 
    sw t0, 32 * 4(sp)
.endm

.macro RESTORE_CONTEXT
    lw t0, 31 * 4(sp)
    csrw mepc, t0
    lw t0, 32 * 4(sp)
    csrw mstatus, t0
    lw x1, 0 * 4(sp)
    lw x2, 1 * 4(sp)
    lw x3, 2 * 4(sp)
    ; ...
    lw x31, 30 * 4(sp)
    addi sp, sp, 33 * 4
.endm

    .section .text

    .align 6    // the base must be 64 bytes alignment
    .globl _trap_entry

_trap_entry:
    SAVE_CONTEXT

    csrr a0, mcause
    call handle_trap

    RESTORE_CONTEXT
    mret

_trap_entry是中断处理的入口,执行 保存现场 → 调用 C 层处理函数 → 恢复现场 → 返回。 调用C编写的handle_trap函数之前,读取mcause CSR 作为handle_trap的函数参数以对中断的原因进行判断。

main.c

#include <stdint.h>

#define RCU_APB2EN 0x40021018
#define GPIOA_CTL0 0x40010800
#define GPIOA_OCTL 0x4001080C

#define TIMER_FREQ ((uint32_t)8000000/4)
#define TIMER_CTRL_ADDR 0xd1000000
#define TIMER_MTIME 0x0
#define TIMER_MTIMECMP 0x8

extern void _trap_entry();

void gpio_init()
{
    // enable GPIOA clock
    *(uint32_t *)RCU_APB2EN |= (uint32_t)1 << 2;

    // set PA1 as output push pull
    *(uint32_t *)GPIOA_CTL0 = *(uint32_t *)GPIOA_CTL0 & ((uint32_t)0xffffff0f) |
                              (uint32_t)1 << 4;

    // PA1 output low
    *(uint32_t *)GPIOA_OCTL &= (uint32_t)0xfffffffd;
}

void timer_init()
{
    *(volatile uint32_t *)(TIMER_CTRL_ADDR + TIMER_MTIME + 4) = 0;
    *(volatile uint32_t *)(TIMER_CTRL_ADDR + TIMER_MTIME) = 0;
    *(volatile uint32_t *)(TIMER_CTRL_ADDR + TIMER_MTIMECMP + 4) = 0;
    *(volatile uint32_t *)(TIMER_CTRL_ADDR + TIMER_MTIMECMP) = TIMER_FREQ;
}

int main()
{
    gpio_init();
    timer_init();

    uint32_t base = (uint32_t)_trap_entry;
    asm volatile("csrw mtvec, %0" :: "r"(base));
    asm volatile("csrs mie, %0" :: "r"(1 << 7));
    asm volatile("csrs mstatus, %0" :: "r"(1 << 3));

    while(1);

    return 0;
}

void handle_trap(uint32_t mcause)
{
    if((mcause & 0x80000000) && (mcause & 0xfff) == 7){
        // reset mtime
        *(volatile uint32_t *)(TIMER_CTRL_ADDR + TIMER_MTIME + 4) = 0;
        *(volatile uint32_t *)(TIMER_CTRL_ADDR + TIMER_MTIME) = 0;

        // toggle PA1 output
        if(*(uint32_t *)GPIOA_OCTL & (uint32_t)0b10){
            *(uint32_t *)GPIOA_OCTL &= (uint32_t)0xfffffffd;
        } else{
            *(uint32_t *)GPIOA_OCTL |= (uint32_t)0b10;
        }
    } else{
        asm volatile("nop");
    }
}

main函数执行了一些初始化操作,使用内联汇编设置mtvec, 启用全局中断和定时器中断。

中断处理函数handle_trap判断中断的原因,如果发生了定时器 中断,则重新设置mtime的值以清除中断悬起状态标志,并翻转 控制LED的IO电平。