Menu Close

RISC-V C语言编程2(3)中断及定时器中断工程

1.中断简介

相关参考文章:

RISC-V教学教案

 

在深入研究工程之前,先简单介绍一下中断。中断是指CPU在正常运行程序时,由于内部/外部事件引起CPU 中断当前运行的程序,而转到为内部/外部事件的服务程序中去, 当服务完毕后,又返回执行cpu的正常程序。

中断是处理器对需要软件注意的事件的响应。中断条件将警告处理器,并用作对处理器的请求,在允许后,中断当前正在执行的代码,以便可以及时处理该事件。 如果中断请求被接受,处理器将通过挂起其当前活动,保存其状态并执行称为中断处理程序(interrupt service routine,ISR)的功能来处理该事件来作出响应。

中断是临时的,除非中断指示致命错误,否则在中断处理程序完成后,处理器将恢复正常活动。

中断示意图如图1所示

%title插图%num

图1 中断示意图

 

更复杂的是,处理器可能会面临处理多个中断的情况。 在这种情况下,应考虑中断优先级(interrupt priority level,IPL)。 通常,一次只允许一个中断,其他中断应等待直到其完成。 有时,在中断中可能会再次发生中断,这称为中断嵌套。中断优先级的仲裁和中断嵌套会在后续文章中详细介绍。

 

 

2.RISCV_timer_irq

RISCV_timer_irq避免了复杂的中断发生,仅实现机器模式计时器中断。这里通过一个每隔250毫秒点亮LED计数器和通过串口打印输出计数值的例子,来说明如何使用定时器中断来控制精准的时间。

要实现定时器中断,需要注意以下几点:

  1. 计时器
  2. 中断相关的CSR寄存器
  3. RISC-V CPU( RTL, register transfer level)和软件的协同合作实现,大致流程见下表:

%title插图%num

 

与以前相同的步骤导入工程。 相应的Freedom Studio Project Explorer如图2所示。

%title插图%num

图2 RISCV_timer_irq

在此使用新的头文件fii_irq.h。 除此之外,还将介绍entry.S和main.c。 其余的头文件与之前的工程(见RISC-V C语言编程2(1)数码管工程)相同。

  • Fii_irq.h
#ifndef __FII_IRQ_H 

#define __FII_IRQ_H

    #ifdef __cplusplus

     extern "C" {

    #endif 

    #include "fii_types.h"

    #include "encoding.h"

    

    #define CLINT_CTRL_ADDR (0x02000000)      //定义内核中断地址空间
    #define PLIC_CTRL_ADDR  (0x0C000000)      //定义外部中断地址空间 

    #define TIME_ADDR       CLINT_CTRL_ADDR   //计时器中断地址    

    #define RTC_FREQ        32768             //RTC计时器频率

    #define MCAUSE_INT      0x80000000        //mcause位31屏蔽,决策判断,‘1’为中断,‘0’为异常

    #define MCAUSE_CAUSE    0x7FFFFFFF        //mcause位30-0屏蔽, 决策判断,异常代码具体数值

                                              //机器模式计时器中断的异常代码是‘7’

    

    //定义内核中断基地址的偏移量

    #define CLINT_CTRL_REG  (0x0000 << 2)    //0x0000, 内核中断寄存器
    #define TM_CTRL_REG     (0x0001 << 2)    //0x0004, 计时器控制寄存器
    #define TM_L_REG        (0x0002 << 2)    //0x0008, timer_val寄存器低32位
    #define TM_H_REG        (0x0003 << 2)    //0x000c, timer_val寄存器高32位
    #define TMCMP_L_REG     (0x0004 << 2)    //0x0010, 计时器比较寄存器低32位
    #define TMCMP_H_REG     (0x0005 << 2)    //0x0014, 计时器比较寄存器高32位

    unsigned int handle_trap(unsigned int mcause, unsigned int epc);     //定义中断入口函数

    

    #ifdef __cplusplus

    }

    #endif 

#endif  // __FII_IRQ_H

 

注意mcause寄存器的实现遵循版本1.9《RISC-V指令集特权体系结构手册》(RISC-V instruction set manual privileged architecture)中的定义。 如图3所示。

%title插图%num

图3 mcause寄存器(位31 =中断,位30-0 =异常代码)

中断工程将使用entry.S文件,该文件用于调用中断入口函数。 在主程序的初始化函数中,trap_entry的地址将分配给RISCV CPU控制和状态寄存器(control and status register,CSR)mtvec。 当实际发生中断时,RISCV CPU程序计数器(program counter,PC)将从主要执行程序中挂起,并指向trap_entry函数的地址。 中断处理程序完成后,PC将恢复主要执行程序。

  • Entry.S
.equ REGBYTES,  (1 << 2)      #定义REGBYTES为4

#.file "encoding.h"

  .section      .text.entry   #定义为代码段

  .align 2                    #2^2 = 4字节对齐

  .global trap_entry          #声明为global函数

trap_entry:                   #函数开始

  ADDI sp, sp, -32*REGBYTES   # 定义当前所使用的栈深度,将指针指向栈底

  #将X1-X31寄存器存入栈
  SW x1, 1*REGBYTES(sp)
  SW x2, 2*REGBYTES(sp)
  SW x3, 3*REGBYTES(sp)
  SW x4, 4*REGBYTES(sp)
  SW x5, 5*REGBYTES(sp)
  SW x6, 6*REGBYTES(sp)
  SW x7, 7*REGBYTES(sp)
  SW x8, 8*REGBYTES(sp)
  SW x9, 9*REGBYTES(sp)
  SW x10, 10*REGBYTES(sp)
  SW x11, 11*REGBYTES(sp)
  SW x12, 12*REGBYTES(sp)
  SW x13, 13*REGBYTES(sp)
  SW x14, 14*REGBYTES(sp)
  SW x15, 15*REGBYTES(sp)
  SW x16, 16*REGBYTES(sp)
  SW x17, 17*REGBYTES(sp)
  SW x18, 18*REGBYTES(sp)
  SW x19, 19*REGBYTES(sp)
  SW x20, 20*REGBYTES(sp)
  SW x21, 21*REGBYTES(sp)
  SW x22, 22*REGBYTES(sp)
  SW x23, 23*REGBYTES(sp)
  SW x24, 24*REGBYTES(sp)
  SW x25, 25*REGBYTES(sp)
  SW x26, 26*REGBYTES(sp)
  SW x27, 27*REGBYTES(sp)
  SW x28, 28*REGBYTES(sp)
  SW x29, 29*REGBYTES(sp)
  SW x30, 30*REGBYTES(sp)
  SW x31, 31*REGBYTES(sp)

  #在进入handle_trap函数时,传递参数(a0/x10,a1/x11),退出函数时,传递return参数(a0/x10)

  csrr a0, mcause           #x[a0] = CSRs[mcause]
  csrr a1, mepc             #x[a1] = CSRs[mepc]
  mv a2, sp                 #x[a2] = x[sp]

  call handle_trap          #调用handle_trap函数
  csrw mepc, a0             #CSRs[mepc] = x[a0]

  #将栈里的内容还原给X1-X31
  LW x1, 1*REGBYTES(sp)
  LW x2, 2*REGBYTES(sp)
  LW x3, 3*REGBYTES(sp)
  LW x4, 4*REGBYTES(sp)
  LW x5, 5*REGBYTES(sp)
  LW x6, 6*REGBYTES(sp)
  LW x7, 7*REGBYTES(sp)
  LW x8, 8*REGBYTES(sp)
  LW x9, 9*REGBYTES(sp)
  LW x10, 10*REGBYTES(sp)
  LW x11, 11*REGBYTES(sp)
  LW x12, 12*REGBYTES(sp)
  LW x13, 13*REGBYTES(sp)
  LW x14, 14*REGBYTES(sp)
  LW x15, 15*REGBYTES(sp)
  LW x16, 16*REGBYTES(sp)
  LW x17, 17*REGBYTES(sp)
  LW x18, 18*REGBYTES(sp)
  LW x19, 19*REGBYTES(sp)
  LW x20, 20*REGBYTES(sp)
  LW x21, 21*REGBYTES(sp)
  LW x22, 22*REGBYTES(sp)
  LW x23, 23*REGBYTES(sp)
  LW x24, 24*REGBYTES(sp)
  LW x25, 25*REGBYTES(sp)
  LW x26, 26*REGBYTES(sp)
  LW x27, 27*REGBYTES(sp)
  LW x28, 28*REGBYTES(sp)
  LW x29, 29*REGBYTES(sp)
  LW x30, 30*REGBYTES(sp)
  LW x31, 31*REGBYTES(sp)

  addi sp, sp, 32*REGBYTES   #将栈全部释放掉,栈指针复位到进入中断之前的位置

  mret                       #退出中断,恢复到当前系统的PC指针

.weak handle_trap

handle_trap:

1:

  j 1b

 

如图4所示,之前使用的栈原理如下:

1:当前程序运行,栈指针的位置

2:中断到来时,将栈指针移至-32 x 4字节位置

3:将31个寄存器存储在1-2之间,并且栈指针停留在位置2

4:在中断执行结束后,将3到4之间的数据还原回31个寄存器,然后将栈指针移回4的位置

注意在整个过程之后,栈指针将返回其原始位置。 尽管栈为所有32个寄存器留出了空间,但x0硬连线为0,该值没有改变,因此仅操作31个寄存器。

%title插图%num

图4 栈原理

 

  • Main.c
#include <stdio.h>          //引用标准的I / O库,主要用于声明printf函数
#include "platform.h"       //用户自定义的函数库,包括GPIO配置,内存空间以及相关的子函数和参数的定义


extern void trap_entry();   //声明trap_entry函数是从外部调用的(entry.S)

void set_timer(u64_t msec)  //定义设置计时器的函数
{
    //用两个32位寄存器组合定义64位计时器
    u64_t now;              //定义64位计时器‘now’

    now   = TIME_REG(TM_H_REG);     //先赋值高32位
    now   = now << 32;              //把高32位移到正确的位置
    now   |= TIME_REG(TM_L_REG);    //把低32位按位或,组成完整的64位计时器

    //在当前计数器的数值上增加时间(转成计数值,毫秒msec), 得到新的64位计数器‘then’
    u64_t then = now + msec*(RTC_FREQ/1000);    //将RTC_FREQ (32768)转化为msec (1000)

    //将‘then’低32位写入计时器比较寄存器
    TIME_REG( TMCMP_L_REG ) = then & 0xffffffff;
    TIME_REG( TMCMP_H_REG ) = then >> 32;       //将‘then’高32位写入计时器比较寄存器

    return;
}

//计时器 计数器(counter)
unsigned int time_cnt = 0;

/*声明计时器中断的入口函数*/
void handle_m_time_interrupt(){
    clear_csr(mie, MIP_MTIP);    //mie位7是机器模式计时器中断使能
                                 //在进入中断处理程序后关闭MIP_MTIP位避免中断嵌套
    set_timer(250);              //重新设置下一次中断在250ms后产生

    //检查进入中断的次数,每隔4次亮一次LED,打印一次信息
    if((time_cnt % 4 ) == 0)     // 1s, 250ms*4
    {
        GPIO_REG(LED_VAL) = ~((time_cnt >> 2) & 0xff);    //每一秒亮一次LED,只保留有效的低8位 
                                                          //只有8位LED灯(0xff),取反参见原理图接线
        printf("time_irq = 0x%08x \r\n", time_cnt);       //输出打印
    }

    time_cnt ++;         //计时器计数器加一

    //重新启动计时器中断

    set_csr(mie, MIP_MTIP);       //mie位7是机器模式计时器中断使能
                                  //在退出中断处理程序之前设置MIP_MTIP位

}

//定义总中断入口函数

unsigned int handle_trap(unsigned int mcause, unsigned int epc)
{
    //决策判断
    //中断或是异常(mcause位31)
    //异常代码是否为7(mcause位30-0)
    //同时满足时,确认为计时器中断

    if ((mcause & MCAUSE_INT) && ((mcause & MCAUSE_CAUSE) == IRQ_M_TIMER))
    {
        handle_m_time_interrupt();        //调用中断处理程序
    }
    else 
    {
        //输出调试信息

        printf("There is not any handle_trap available \n");
        printf("mstatus    = 0x%08x\n" , read_csr(mstatus) );      //机器状态寄存器mstatus: machine status register
        printf("mie        = 0x%08x\n" , read_csr(mie) );          //机器中断使能寄存器mie: machine interrupt-enable register
        printf("mcause     = 0x%08x\n" , read_csr(mcause) );       //机器陷阱原因mcause: machine trap cause
        printf("mtvec      = 0x%08x\n" , read_csr(mtvec));         //机器自陷向量基址寄存器mtvec: machine trap-handler base address
    }

    return epc;
}

//初始化函数

void _init(void)
{
    time_cnt = 0;                       //初始化计时器计数器

    printf("Program Initial... \n");

    write_csr(mtvec, &trap_entry);      //赋值trap_entry函数地址给mtvec
                                        //当中断开始时,PC指向trap_entry函数
    return;
}

//LED初始化
static void init_led(void)
{
    GPIO_REG(LED_VAL) = ~0L;            //初始化LED默认不亮
                                        //取反参见原理图接线
    GPIO_REG(LED_DIR) = 0;              //初始化LED方向为输出

    return;
}

int main(void)//主函数
{
    int  i = 0;                        //用于延迟循环

    _init();                           //初始函数

    printf("\r\nRiscV Program : Invoke timer IRQ. \r\n");

    init_led();                        //LED初始化

    set_timer(250);                    //第一次设置计时器

    set_csr(mie, MIP_MTIP);            //启动计时器中断

    write_csr(mstatus, MSTATUS_MIE);   //启动全局中断

    TIME_REG( TM_CTRL_REG ) = 0x80000001;     //计时器使能,启动计时器计数

    

    while ( 1 )//循环
    {
        for(i = 0; i < 0x100000; i ++ )
            asm("nop");               //每0x100000次,运行汇编指令'nop'
    }
}

 

 

3. RISCV_seg_irq工程

接下来,将计时器中断工程和数码管工程组合成RISCV_seg_irq工程。 按照与之前相同的步骤导入工程。 仅main.c文件与RISCV_timer_irq中的main.c文件略有不同。

  • Main.c
#include <stdio.h>            //引用标准的I / O库,主要用于声明printf函数

#include "platform.h"         //用户自定义的函数库,包括GPIO配置,内存空间以及相关的子函数和参数的定义




#define  NOP_DELAY  0x100       //定义宏

extern void trap_entry();       //声明trap_entry函数是从外部调用的(entry.S)

void set_timer(u64_t msec)      //定义设置计时器的函数
{
    //用两个32位寄存器组合定义64位计时器
    u64_t now;                  //定义64位计时器‘now’

    now   = TIME_REG(TM_H_REG);          //先赋值高32位
    now   = now << 32;                   //把高32位移到正确的位置
    now   |= TIME_REG(TM_L_REG);         //把低32位按位或,组成完整的64位计时器

    //在当前计数器的数值上增加时间(转成计数值,毫秒msec), 得到新的64位计数器‘then’
    u64_t then = now + msec*(RTC_FREQ/1000);             //将RTC_FREQ (32768)转化为msec (1000)

    //将‘then’低32位写入计时器比较寄存器
    TIME_REG( TMCMP_L_REG ) = then & 0xffffffff;
    TIME_REG( TMCMP_H_REG ) = then >> 32;                //将‘then’高32位写入计时器比较寄存器

    return;
}

//计时器 计数器(counter)
unsigned int time_cnt = 0;

/*声明计时器中断的入口函数*/
void handle_m_time_interrupt(){
    clear_csr(mie, MIP_MTIP);       //mie位7是机器模式计时器中断使能
                                    //在进入中断处理程序后关闭MIP_MTIP位
                                    //避免中断嵌套
    set_timer(500);                 // 重新设置下一次中断在500ms后产生

    //检查进入中断的次数,每隔4次亮一次LED,打印一次信息
    if((time_cnt % 4 ) == 0)        // 2s, 500ms*4
    {
        GPIO_REG(LED_VAL) = ~((time_cnt >> 2) & 0xff);       //每两秒亮一次LED,只保留有效的低8位 
                                                             //只有8位LED灯(0xff),取反参见原理图接线
        printf("time_irq = 0x%08x \r\n", time_cnt);          //输出打印
    }

    time_cnt ++;         //计时器计数器加一

    //重新启动计时器中断
    set_csr(mie, MIP_MTIP);           //mie位7是机器模式计时器中断使能
                                      //在退出中断处理程序之前设置MIP_MTIP位

}

//定义总中断入口函数
unsigned int handle_trap(unsigned int mcause, unsigned int epc)
{
    //决策判断
    //中断或是异常(mcause位31)
    //异常代码是否为7(mcause位30-0)
    //同时满足时,确认为计时器中断

    if ((mcause & MCAUSE_INT) && ((mcause & MCAUSE_CAUSE) == IRQ_M_TIMER))
    {
        handle_m_time_interrupt();       //调用中断处理程序
    }
    else 
    {
         //输出调试信息
        printf("There is not any handle_trap available \n");
        printf("mstatus    = 0x%08x\n" , read_csr(mstatus) );       //机器状态寄存器mstatus: machine status register
        printf("mie        = 0x%08x\n" , read_csr(mie) );           //机器中断使能寄存器mie: machine interrupt-enable register
        printf("mcause     = 0x%08x\n" , read_csr(mcause) );        //机器陷阱原因mcause: machine trap cause
        printf("mtvec      = 0x%08x\n" , read_csr(mtvec));          //机器自陷向量基址寄存器mtvec: machine trap-handler base address
    }

    return epc;
}

//初始化函数
void _init(void)
{
    time_cnt = 0;         //初始化计时器计数器

    printf("RiscV Program : Display segment number and invoke timer IRQ \r\n");
    write_csr(mtvec, &trap_entry);          //赋值trap_entry函数地址给mtvec
                                            //当中断开始时,PC指向trap_entry函数

    

    //增加初始化GPIO地址
    GPIO_REG(SEAT_DIR) = 0;     //位选
    GPIO_REG(SEG_DIR) = 0;      //段选

    return;
}

//声明数组用来索引字模

//这里使用十六进制(0~f)
const unsigned char font[] =
{
    SEG_0, SEG_1, SEG_2, SEG_3,
    SEG_4, SEG_5, SEG_6, SEG_7,
    SEG_8, SEG_9, SEG_A, SEG_B,
    SEG_C, SEG_D, SEG_E, SEG_F
};

//LED初始化
static void init_led(void)
{
    GPIO_REG(LED_VAL) = ~0L;     //初始化LED默认不亮
                                 //取反参见原理图接线
    GPIO_REG(LED_DIR) = 0;       //初始化LED方向为输出

    return;
}

//增加数码管显示函数
//输入数码管显示字模和位选参数

void display_seg(unsigned char font_idx, unsigned int font_seat)
{
    GPIO_REG(SEG_VAL) = ~(font[font_idx]);        //数码管显示字模,取反参见原理图接线
    GPIO_REG(SEAT_VAL) = ~font_seat;              //数码管位选,取反参见原理图接线

    return;
}

int main(void)          //主函数
{
    int  i = 0;         //用于延迟循环

    //数码管初始化
    unsigned int  curr_seat = 0x01;       //初始化数码管初始位置
    unsigned int  curr_seg = 0;           //数码管显示的值
    unsigned char out_char = 0;           //引索数码管字模

    _init();           //初始函数

    printf("\r\nRun Segment Timer IRQ Program \r\n");

    init_led();        //LED初始化

    set_timer(500);    //第一次设置计时器

    set_csr(mie, MIP_MTIP);                //启动计时器中断

    write_csr(mstatus, MSTATUS_MIE);       //启动全局中断

    TIME_REG( TM_CTRL_REG ) = 0x80000001;  //计时器使能,启动计时器计数

    

    while ( 1 )        //主循环
    {
        display_seg(out_char, curr_seat);       //调用数码管显示函数

        for(i = 0; i < NOP_DELAY ; i ++ )
            asm("nop");                         //运行汇编指令'nop'

        curr_seat = curr_seat << 1;             //点亮的数码管左移一位

        if(curr_seat == 0x40)                   //决策判断数码管是否显示到最高位
        {
            curr_seg = time_cnt;                //将计时器计数器的值赋给数码管显示
            curr_seat = 0x01;                   //重新设置数码管显示为最低位
        }
        else
            curr_seg = curr_seg >> 4;           //当数码管显示位置不是最高位,当前显示的内容右移4位(16进制右移一位)

        out_char = curr_seg & 0xf;              //一次只显示一位数字(十六进制)
    }
}

 

 

Posted in C语言, FPGA, RISC-V, RISC-V, RISC-V 教案, 应用开发, 开发板, 文章, 编程语言

发表评论

相关链接