嵌入式入门6(异常和中断)

940 阅读15分钟

一、概述

1.1、异常

CPU通常采用顺序执行的方式执行代码,假如需要在某个时刻,打断CPU当前执行流程,去执行一些特定操作,执行完后,再返回到之前的程序继续执行,这一系列操作就涉及到异常

ARM处理器中定义了7种类型的异常(Exception):

异常优先级注释
复位异常(Reset)1
数据异常(Data Abort)2
快速中断异常(FIQ)3
外部中断异常(IRQ)4
预取异常(Prefetch Abort)5
软中断异常(SWI)6
未定义指令异常(Undefined interrupt)6

由于正在执行的指令不可能既是一条软中断指令,又是一条未定义指令,所以软中断异常和未定义指令异常享有相同的优先级。

发生异常时,CPU会跳转到特定的地址执行,这个地址被称为异常向量,每种异常对应着不同的异常向量,用于它们紧密相邻,所以统称为异常向量表

image.png

每个异常向量中,都保存着对应的异常处理函数地址,这样当异常发生时,就能自动调用到异常处理函数。

如:u-boot-1.1.6\cpu\arm920t\start.S

.globl _start
_start:
    b	reset
    ldr	pc, _undefined_instruction
    ldr	pc, _software_interrupt
    ldr	pc, _prefetch_abort
    ldr	pc, _data_abort
    ldr	pc, _not_used
    ldr	pc, _irq
    ldr	pc, _fiq

1.2、中断

中断(Interrupt)也是异常的一种。

  • 中断源: 可以引起中断的信号源,如按键、定时器、网络数据等。

中断控制器可以发信号给CPU告诉它发生了哪些中断。

中断处理过程重点在于保存现场以及恢复现场:

1、保存现场(各种寄存器)
2、处理异常(中断属于一种异常)
3、恢复现场

1.3、arm对异常(中断)处理过程

  • 1、初始化:

1、设置中断源,让它可以产生中断
2、设置中断控制器(可以屏蔽某个中断,优先级)
3、设置CPU总开关,使能中断

  • 2、执行其他程序:正常程序
  • 3、产生中断:按下按键--->中断控制器--->CPU
  • 4、cpu每执行完一条指令都会检查有无中断/异常产生
  • 5、发现有中断/异常产生,开始处理。

二、CPU模式、状态和寄存器

可以参考书籍 《ARM体系结构与编程》作者:杜春雷

  • 7种Mode:

image.png

用户模式不可直接进入其他模式,而其余6个特权模式(privileged mode),可以通过编程操作CPSR寄存器直接进入其他模式。

在终止模式中

指令预取终止:读某条错误的指令导致终止运行
数据访问终止:读写某个地址,这个过程出错

  • 2种State:

ARM state:使用ARM指令集,每个指令4byte。
Thumb state:使用的是Thumb指令集,每个指令2byte。

ARM指令和Thumb指令的区别

  • 寄存器:

通用寄存器
备份寄存器(banked register)
当前程序状态寄存器:CPSR(Current Program Status Register)
CPSR的备份寄存器:SPSR(Save Program Status Register)

image.png

其中,带灰色三角形表示改寄存器为备份寄存器,它是该模式下所特有的。比如:r13(sp)寄存器和r14(lr)寄存器,它们虽然名字与System/User模式下r13、r14名字一致,但实际上它们是物理上不同的寄存器。

lr寄存器保存了发生异常时的指令地址。

image.png

Bit位含义接收
M4 ~ M0Mode bits表示当前CPU处于哪一种异常模式(Mode)
TState bits表示CPU工作于Thumb State还是ARM State
FFIQ disable禁止所有的FIQ中断
IIRQ disable禁止所有的IRQ中断,这个位是IRQ的总开关
Bit8 ~ Bit27保留位
Bit28 ~ Bit31状态位

M4~M0位的取值如下:

image.png

我们可以读取这5位来判断CPU处于哪一种模式,如果我们在特权模式下,可以通过修改这些模式位,让ARM运行在不同的异常模式下;

什么是状态位?比如说执行以下两条指令:

cmp R0, R1
beq xxx

当执行第一条指令时,会比较R0与R1是否相等,其结果会影响到Z位的值:如果相等,那么Z位等于1,否则等于0。

第二句指令,会去判断Z位的值,如果Z位等于1则跳转,否则继续执行。

SPSR:程序状态备份寄存器,当发生异常时,它保存被中断的模式下的CPSR。假如我们当前在系统模式下运行,CPSR是某个值,当发生中断时,会进入irq模式,这个CPSR_irq就保存系统模式下的CPSR。

image.png

我们来翻译一下:

发生异常时,我们的CPU会做什么事情

  • 1、把下一条指令的地址保存在LR寄存器里(某种异常模式的LR等于被中断的下一条指令的地址) 它有可能是PC + 4有可能是PC + 8,到底是那种取决于不同的情况
  • 2、把CPSR保存在SPSR里面(某一种异常模式下SPSR里面的值等于CPSR)
  • 3、修改CPSR的模式为进入异常模式(修改CPSR的M4 ~ M0进入异常模式)
  • 4、跳到向量表

退出异常怎么做?

  • 1、让LR减去某个值,让后赋值给PC(PC = 某个异常LR寄存器减去 offset)
  • 2、把CPSR的值恢复(CPSR 值等于某一个异常模式下的SPSR)
  • 3、清中断(如果是中断的话,对于其他异常不用设置)

退出异常时,LR减去什么值呢?也就是我们如何返回到原来的程序继续执行呢?可以根据下面这个表来取值:

image.png

比如: 发生了SWI异常,可以把 R14_svc复制给PC。 发生了IRQ异常,可以把R14_irq的值减去4赋值给PC。

二、Thumb指令示例程序

Makefile

all: init.o uart.o main.o start.o
	#arm-linux-ld -Ttext 0 -Tdata 0x700 start.o uart.o main.o -o sdram.elf
	arm-linux-ld -T sdram.lds start.o init.o uart.o main.o -o sdram.elf
	arm-linux-objcopy -O binary -S sdram.elf sdram.bin
	arm-linux-objdump -D sdram.elf > sdram.dis
clean:
	rm *.bin *.o *.elf *.dis

%.o: %.c
	arm-linux-gcc -mthumb -c -o $@ $<

%.o: %.S
	arm-linux-gcc -c -o $@ $<

start.S

.text
.global _start

_start:
    /* 省略以下代码:
     1、关闭看门狗
     2、设置时钟
     3、设置栈指针
    */

    /* 怎么从ARM State切换到Thumb State */
    adr r0, thumb_func
    add r0, r0, #1  /* bit0=1时,bx就会切换cpu State*/
    bx  r0

.code 16
thumb_func:
    bl sdram_init

    bl copy2sdram

    bl clean_bss

    bl uart0_init

    //bl main  /*bl相对跳转,程序仍在NOR/sram执行*/
    ldr r0, =main  /* 绝对跳转, 跳到SDRAM ,先把main的地址赋值给R0 */
    mov pc, r0  /*让后再移动到PC*/

halt:
    b halt

在编写Thumb指令时,先要使用伪指令CODE16声明,而且在ARM指令中要使用BX指令跳转到Thumb指令。

若目标地址的bit[0]为0,则跳转时自动将CPSR中的标志位T复位,即把目标地址的代码解释为ARM代码。
若目标地址的bit[0]为1,则跳转时自动将CPSR中的标志位T置位,即把目标地址的代码解释为Thumb代码。

三、und异常示例程序

exception.c

#include "uart.h"

void printException(unsigned int cpsr, char* str)
{
    puts("Exception! cpsr = ");
    printHex(cpsr);
    puts(" ");
    puts(str);
    puts("\n\r");
}

start.S

.text
.global _start

_start:
    b reset     /* vector 0: reset*/
    b do_und    /* vector 4: und*/

do_und:
    /* 执行到这里之前:
     * 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
     * 2. SPSR_und保存有被中断模式的CPSR
     * 3. CPSR中的M4-M0被设置为11011, 进入到und模式
     * 4. 跳到0x4的地方执行程序 
     */
    //设置sp_und
    ldr sp, =0x34000000

    //保存现场
    stmdb sp!, {r0-r12, lr}

    //处理und异常
    mrs r0, cpsr
    ldr r1, =und_string
    bl printException

    //恢复现场
    ldmia sp!, {r0-r12, pc}^ //^会把spsr的值恢复到cpsr里

und_string:
    .string "undefined instruction exception"

reset:
    /* 省略以下代码:
     1、关闭看门狗
     2、设置时钟
     3、设置栈指针
    */

    bl sdram_init

    bl copy2sdram

    bl clean_bss

    bl uart0_init

und_code:
    .word 0xdeadc0de // 故意加入一条未定义指令

    //bl main  /*bl相对跳转,程序仍在NOR/sram执行*/
    ldr pc, =main  /*绝对跳转,跳到SDRAM*/

halt:
    b halt

编译烧写运行,发现确实发生了未定义指令异常。

但是上面的代码还是存在问题:

  • 1、b do_und指令,会让后续代码在启动芯片上运行,而不是SDRAM上运行。
  • 2、由于string占据内存不固定,可能会造成下一行代码未进行4字节对齐,导致程序执行不正确。

改进如下:

.text
.global _start

_start:
    b reset            /* vector 0: reset*/
    ldr pc, und_addr   /* vector 4: und*/

und_addr:           //这里防止Nand启动时,去4k之外的地方读取do_und的地址。
    .word do_und

do_und:
    /* 执行到这里之前:
     * 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
     * 2. SPSR_und保存有被中断模式的CPSR
     * 3. CPSR中的M4-M0被设置为11011, 进入到und模式
     * 4. 跳到0x4的地方执行程序 
     */

    //设置sp_und
    ldr sp, =0x34000000

    //保存现场
    stmdb sp!, {r0-r12, lr}

    //处理und异常
    mrs r0, cpsr
    ldr r1, =und_string
    bl printException

    //恢复现场
    ldmia sp!, {r0-r12, pc}^ //^会把spsr的值恢复到cpsr里

und_string:
    .string "undefined instruction exception"

.align 4  // string可能导致reset函数没有4字节对齐

reset:
    /* 省略以下代码:
     1、关闭看门狗
     2、设置时钟
     3、设置栈指针
    */

    bl sdram_init

    bl copy2sdram

    bl clean_bss

    // 重定位完成后,跳转到SDRAM执行
    ldr pc, =sdram
sdram:

    bl uart0_init

und_code:
    .word 0xdeadc0de //故意加入一条未定义指令

    //bl main  /*bl相对跳转,程序仍在NOR/sram执行*/
    ldr pc, =main  /*绝对跳转,跳到SDRAM*/

halt:
    b halt

四、swi异常示例程序

在Linux系统中,App一般运行在用户模式,这是一种受限的模式,如:不能访问硬件。而如果APP想访问硬件,只能通过异常,来切换到特权模式。

而中断、und异常可遇不可求,此时就只能使用swi软中断。

exception.c

#include "uart.h"

void printException(unsigned int cpsr, char* str)
{
    puts("Exception! cpsr = ");
    printHex(cpsr);
    puts(" ");
    puts(str);
    puts("\n\r");
}

void printSWIVal(unsigned int* pSWI)
{
    unsigned int val = *pSWI & (~0xFF000000);
    puts("SWI Val: ");
    printHex(val);
    puts("\n\r");
}

void printMode(unsigned int cpsr)
{
    unsigned int mode = cpsr & 0x001f;
    puts("cpsr = ");
    printHex(cpsr);
    if(mode == 0x10) {
        puts(" User mode");
    } else if(mode == 0x11) {
        puts(" FIQ mode");
    } else if(mode == 0x12) {
        puts(" IRQ mode");
    } else if(mode == 0x13) {
        puts(" Supervisor mode");
    } else if(mode == 0x17) {
        puts(" Abort mode");
    } else if(mode == 0x1b) {
        puts(" Underfined mode");
    } else if(mode == 0x1f) {
        puts(" System mode");
    }
    puts("\n\r");
}

start.S

.text
.global _start

_start:
    b reset	   	   /* vector 0: reset*/
    ldr pc, und_addr   /* vector 4: und*/
    ldr pc, swi_addr   /* vector 8: swi*/

und_addr:
    .word do_und

swi_addr:
    .word do_swi

do_und:
    /* 执行到这里之前:
     * 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
     * 2. SPSR_und保存有被中断模式的CPSR
     * 3. CPSR中的M4-M0被设置为11011, 进入到und模式
     * 4. 跳到0x4的地方执行程序 
     */

    //设置sp_und
    ldr sp, =0x34000000

    //保存现场
    stmdb sp!, {r0-r12, lr}

    //处理und异常
    mrs r0, cpsr
    ldr r1, =und_string
    bl printException

    //恢复现场
    ldmia sp!, {r0-r12, pc}^ //^会把spsr的值恢复到cpsr里

und_string:
    .string "undefined instruction exception"

.align 4  // string可能导致reset函数没有4字节对齐

do_swi:
    /* 执行到这里之前:
     * 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址
     * 2. SPSR_svc保存有被中断模式的CPSR
     * 3. CPSR中的M4-M0被设置为11011, 进入到svc模式
     * 4. 跳到0x4的地方执行程序 
     */

    //设置sp_und
    ldr sp, =0x33e00000

    //保存现场
    stmdb sp!, {r0-r12, lr}
    //ATPCS规则:C函数会保存r4 ~ r11这几个寄存器,这些寄存器不会被c函数破坏
    mov r4, lr

    //处理svc异常
    mrs r0, cpsr
    bl printMode

    sub r0, r4, #4
    bl printSWIVal

    //恢复现场
    ldmia sp!, {r0-r12, pc}^ //^会把spsr的值恢复到cpsr里

reset:
    /* 省略以下代码:
     1、关闭看门狗
     2、设置时钟
     3、设置栈指针
    */

    bl sdram_init

    bl copy2sdram

    bl clean_bss

    // 重定位完成后,跳转到SDRAM执行
    ldr pc, =sdram
sdram:

    bl uart0_init

und_code:
    .word 0xdeadc0de //故意加入一条未定义指令

    //上电后是supervisor模式0b10011
    mrs r0, cpsr
    bl printMode //Spervisor mode

    mrs r0, cpsr      /* 读出cpsr 读到r0 */
    //使用bic命令 bitclean 把低4位清零
    bic r0, r0, #0xf  /* 修改M4-M0为0b10000, 进入usr模式 */
    msr cpsr, r0

    mrs r0, cpsr
    bl printMode //User mode

    swi 0x123 //执行此命令,会触发swi异常

    // bl main  /*bl相对跳转,程序仍在NOR/sram执行*/
    ldr pc, =main  /*绝对跳转,跳到SDRAM*/

halt:
    b halt

五、按键中断示例程序

本示例程序中,将通过按键中断,控制LED。

整个流程如下:

  • 1、初始化:
1、设置中断源,让它可以发出中断
2、设置中断控制器,让它可以把中断信号发送给CPU
3、设置CPU,CPSR的I位,它是中断总开关
  • 2、处理时:要分辨中断源,执行不同的中断处理函数
  • 3、处理完:清中断

我们想通过4个按键,控制3个LED灯。

image.png

具体控制信息如下:

按键按键GPIO引脚按键中断源控制的LEDLED GPIO引脚
EINT0GPF0EINT[0]nLED_1GPF4
EINT2GPF2EINT[2]nLED_2GPF5
EINT11GPG3EINT[11]nLED_4GPF6
EINT19GPG11EINT[19]nLED_1\2\4GPF4\5\6

5.1、配置LED GPIO

image.png

led.c

void init_led()
{
    /* 设置GPFCON让GPF4/5/6配置为输出引脚 */
    GPFCON &= ~((3<<8) | (3<<10) | (3<<12));
    GPFCON |=  ((1<<8) | (1<<10) | (1<<12));
}

5.2、配置按键中断

  • 1、配置按键引脚为中断引脚

image.png

  • 2、配置中断触发方式

image.png

这里EINT设置为111,表示双边缘触发,即按键按下抬起都会产生中断。
FLTEN表示是否打开滤波功能。 对于一些波形不规整的外部中断信号,可以通过滤波电路让其变成规整,这样会简化软件的编写。

  • 3、开启GPIO引脚中断

image.png

void key_eint_init()
{
    puts("key_eint_init\r\n");
    //设置gpio引脚为中断引脚
    GPFCON &= ~((3<<0)|(3<<4));
    GPFCON |=  ((2<<0)|(2<<4));
    GPGCON &= ~((3<<6)|(3<<22));
    GPGCON |=  ((2<<6)|(2<<22));

    //设置外部中断触发方式:双边缘触发
    EXTINT0 |= ((7<<0)|(7<<8));
    EXTINT1 |= (7<<12);
    EXTINT2 |= (7<<12);

    //设置外部中断屏蔽寄存器EINTMASK,使能eint11,19
    EINTMASK &= ~((1<<11)|(1<<19));
}

外部中断EINTPEND寄存器,通过读这个寄存器,可以知道发生了哪个外部中断(eint4~23),使用完成后,需要清除改寄存器中对应的中断位。

image.png

5.3、配置中断控制器

image.png

MASK:屏蔽寄存器
INTPND:等待处理,读取看哪个中断产生的

有些中断,可以直接到达SRCPND。
有些中断,必须先通过SUBSRCPND,再到SUBMASK,才能到达SRCPND。

image.png

这里可以看到外部中断0~3分别使用各自的中断线,4~7合用一条中断线,8~23合用一条中断线。
并且这些中断是Sources,而不是Sub Sources,即它们可以直接到达SRCPND寄存器。所以我们只需设置SRCPNDMASKINTPND三个寄存器即可。

SRCPND寄存器,表示发生了哪个中断,执行完中断处理程序后,需要清除该寄存器。

image.png

INTMASK,中断屏蔽寄存器,如果某个中断位设置为1,即使该中断发生了,CPU也不会处理该中断。

image.png

INTPND寄存器,用来显示当前优先级最高的、正在发生的中断源。

当多个中断同时产生,在SRCPND中,有多个位被设置1,它们经过优先级后,只会有一个中断通知CPU。执行完中断处理程序后,也需要清除该寄存器。

image.png

INTOFFSET寄存器,表示INTPND寄存器中,哪个中断正在被处理,可以显示INTPND寄存器哪一位被设置为1。

image.png

void interrupt_init()
{
    puts("interrupt_init\r\n");
    INTMSK &= ~((1<<0)|(1<<2)|(1<<5));
}

5.4、其余代码

.text
.global _start

_start:
    b reset            /* vector 0: reset*/
    ldr pc, und_addr   /* vector 4: und*/
    ldr pc, swi_addr   /* vector 8: swi*/
    b halt             /* vector 0x0c : prefetch aboot */
    b halt             /* vector 0x10 : data abort */
    b halt             /* vector 0x14 : reserved */
    ldr pc, irq_addr   /* vector 0x18 : irq */
    b halt             /* vector 0x1c : fiq */

und_addr:
    .word do_und

swi_addr:
    .word do_swi

irq_addr:
    .word do_irq

do_und:
    /* 执行到这里之前:
     * 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
     * 2. SPSR_und保存有被中断模式的CPSR
     * 3. CPSR中的M4-M0被设置为11011, 进入到und模式
     * 4. 跳到0x4的地方执行程序 
     */

    //设置sp_und
    ldr sp, =0x34000000

    //保存现场
    stmdb sp!, {r0-r12, lr}

    //处理und异常
    mrs r0, cpsr
    ldr r1, =und_string
    bl printException

    //恢复现场
    ldmia sp!, {r0-r12, pc}^ //^会把spsr的值恢复到cpsr里

und_string:
    .string "undefined instruction exception"

.align 4  // string可能导致reset函数没有4字节对齐

do_swi:
    /* 执行到这里之前:
     * 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址
     * 2. SPSR_svc保存有被中断模式的CPSR
     * 3. CPSR中的M4-M0被设置为11011, 进入到svc模式
     * 4. 跳到0x4的地方执行程序 
     */

    //设置sp_und
    ldr sp, =0x33e00000

    //保存现场
    stmdb sp!, {r0-r12, lr}
    //ATPCS规则:C函数会保存r4 ~ r11这几个寄存器
    mov r4, lr

    //处理svc异常
    mrs r0, cpsr
    bl printMode

    sub r0, r4, #4
    bl printSWIVal

    //恢复现场
    ldmia sp!, {r0-r12, pc}^ //^会把spsr的值恢复到cpsr里

do_irq:
    /* 执行到这里之前:
     * 1. lr_irq保存有被中断模式中的下一条即将执行的指令的地址
     * 2. SPSR_irq保存有被中断模式的CPSR
     * 3. CPSR中的M4-M0被设置为10010, 进入到irq模式
     * 4. 跳到0x18的地方执行程序 
     */

    //设置sp_irq
    ldr sp, =0x33d00000

    //保存现场
    // 在irq异常处理函数中有可能会修改r0-r12, 所以先保存
    // lr-4是异常处理完后的返回地址, 也要保存
    sub lr, lr, #4
    stmdb sp!, {r0-r12, lr}

    bl handle_irq_c

    //恢复现场
    ldmia sp!, {r0-r12, pc}^ //^会把spsr_irq的值恢复到cpsr里

reset:
    /* 省略以下代码:
     1、关闭看门狗
     2、设置时钟
     3、设置栈指针
    */

    bl sdram_init

    bl copy2sdram

    bl clean_bss

    // 重定位完成后,跳转到SDRAM执行
    ldr pc, =sdram
sdram:

    bl uart0_init

und_code:
    .word 0xdeadc0de //故意加入一条未定义指令

    //上电后是supervisor模式0b10011
    mrs r0, cpsr
    bl printMode  //Spervisor mode

    mrs r0, cpsr      /* 读出cpsr 读到r0 */
    //使用bic命令 bitclean 把低4位清零
    bic r0, r0, #0xf  /* 修改M4-M0为0b10000, 进入usr模式 */
    bic r0, r0, #(1<<7) /* 清除I位,使能中断 */
    msr cpsr, r0

    mrs r0, cpsr
    bl printMode  //User mode

    swi 0x123 //执行此命令,会触发swi异常

    bl init_led        /* 初始化LED引脚 */
    bl interrupt_init  /* 初始化中断控制器 */
    bl key_eint_init   /* 初始化按键,设为中断源 */

    bl main

halt:
    b halt

interrupt.c

#include "s3c2440_soc.h"

void key_eint_irq(int irq)
{
    unsigned int val = EINTPEND;
    unsigned int val1 = GPFDAT;
    unsigned int val2 = GPGDAT;

    if (irq == 0) /* eint0 : s2 控制 D12 */
    {
        if (val1 & (1<<0)) /* s2 --> gpf6 */
        {
            GPFDAT |= (1<<6);  // 松开
        } else {
            GPFDAT &= ~(1<<6);  // 按下
        }
    }
    else if (irq == 2) /* eint2 : s3 控制 D11 */
    {
        if (val1 & (1<<2)) /* s3 --> gpf5 */
        {
            GPFDAT |= (1<<5);   // 松开
        } else {
            GPFDAT &= ~(1<<5);  // 按下
        }
    }
    else if (irq == 5) /* eint8_23, eint11--s4 控制 D10, eint19---s5 控制所有LED */
    {
        if (val & (1<<11)) /* eint11 */
        {
            if (val2 & (1<<3)) /* s4 --> gpf4 */
            {
                GPFDAT |= (1<<4);    // 松开
            } else {
                GPFDAT &= ~(1<<4);    // 按下
            }
        }
        else if (val & (1<<19)) /* eint19 */
        {
            if (val2 & (1<<11))
            {
                GPFDAT |= ((1<<4) | (1<<5) | (1<<6)); //松开:熄灭所有LED
            } else {
                GPFDAT &= ~((1<<4) | (1<<5) | (1<<6));  //按下:点亮所有LED
            }
        }
    }

    EINTPEND = val;
}

void handle_irq_c()
{
    puts("handle_irq_c\r\n");
    // 1、分辨中断源
    // 读INTOFFSET在芯片手册里找到这个寄存器,它里面的值表示INTPND中哪一位被设置成1
    int bit = INTOFFSET;
    // 2、调用对应的处理函数
    if (bit == 0 || bit == 2 || bit == 5)  /* 对应eint0,2,eint8_23 */
    {
        /*我们会调用一个按键处理函数*/
        key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND */
    }

    /**
     * 3、清中断 : 从源头开始清
     * 先清除掉中断源里面的某些寄存器
     * 再清 SRCPND
     * 再清 INTPND
     */
    SRCPND = (1<<bit);
    INTPND = (1<<bit);	
}

六、定时器中断示例程序

  • S3C2440A有5个16位定时器。其中定时器0、1、2和3具有脉宽调制(PWM)功能。定时器4是一个无输出引脚的内部定时器。定时器0还包含用于大电流驱动的死区发生器。

  • 定时器0和1共用一个8位预分频器,定时器2、3和4共用另外的8位预分频器。每个定时器都有一个可以生成5种不同分频信号(1/2,1/4,1/8,1/16和TCLK)的时钟分频器。每个定时器模块从相应8位预分频器得到时钟的时钟分频器中得到其自己的时钟信号。8位预分频器是可编程的,并且按存储在TCFG0和TCFG1寄存器中的加载值来分频PCLK。

  • 定时计数缓冲寄存器(TCNTBn)包含了一个当使能了定时器时的被加载到递减计数器中的初始值。定时比较缓冲寄存器(TCMPBn)包含了一个被加载到比较寄存器中的与递减计数器相比较的初始值。这种TCNTBn和TCMPBn的双缓冲特征保证了改变频率和占空比时定时器产生稳定的输出。

  • 每个定时器拥有一个16位递减计数器,它是由自己的定时器时钟驱动的。当递减计数器到达零时,产生定时器中断请求通知CPU定时器操作已经完成。当定时器计数器到达零时,相应的TCNTBn的值将自动被加载到递减计数器以继续下一次操作。然而,如果定时器停止了,例如,在定时器运行模式期间清除TCONn的定时器使能位,TCNTBn的值将不会被重新加载到计数器中。

  • TCMPBn的值是用于脉宽调制(PWM)。当递减计数器的值与定时器控制逻辑中的比较寄存器的值相匹配时定时器控制逻辑改变输出电平。因此,比较寄存器决定PWM输出的开启时间(或关闭时间)。

image.png

image.png

每来一个CLK,TCNTn减1。当TCNTn等于0时,将产生定时器中断,并重新加载TCNTBn中的值。

怎么使用Timer?

  • 1、设置时钟
  • 2、设置初值
  • 3、加载初值,启动Timer
  • 4、设置为自动加载
  • 5、中断相关

6.1、设置Timer0

image.png

PWM时钟控制寄存器,这里涉及一个公式:

Timer input clock Frequency = PCLK / {prescaler value+1} / {divider value}

PCLK为50MHz,为了便于计算,将prescaler设置为99,divider value设置为1/16。可得到Timer0的设置频率为31250。也就是说,当TCNTn从31250减到0,才过去一秒钟。

image.png

所以这里TCFG1[3:0] = 3

image.png

再设置时钟控制寄存器TCON:

1、先设置TCON[1]=1,表示更新TCNTB0的值。
2、再设置TCON[1]=0,因为在写这个寄存器时,需要清除该位。
3、设置自动加载:TCON[3]=1
4、启动Timer0:TCON[0]=1

image.png

timer.c

void timer_init()
{
    /* 设置TIMER0的时钟
     * 
     * Timer clock = PCLK / {prescaler value+1} / {divider value}
     *             = 50000000 / (99 + 1) / 16
     *             = 31250
     * 也就是说,当TCNTn从31250减到0,才过去一秒钟
     */
    TCFG0 = 99;
    TCFG1 = 3;
    
    /* 设置TIMER0的初值 */
    TCNTB0 = 15625; /* 0.5s中断一次 */
    
    /* 设置手动更新 */
    TCON |= (1<<1); //update from TCNTB0 & TCMPB0
    
    /* 设置为自动加载, 并启动*/
    TCON &= ~(1<<1);
    TCON |= (1<<0) | (1<<3);
}

6.2、设置中断

前面已经配置好了Timer0中断源,现在,需要配置中断控制器中的Timer0屏蔽寄存器,让CPU能够响应Timer0的中断。

image.png

interrupt.c

void interrupt_init()
{
    puts("interrupt_init\r\n");

    /* 设置按键中断 */
    INTMSK &= ~((1<<0)|(1<<2)|(1<<5));

    /* 设置TIMER0中断 */
    INTMSK &= ~(1<<10);
}

// 省略部分代码

设置完成后,Timer0将会每隔0.5秒,就会产生一个中断。

6.3、响应中断

interrupt.c

// 省略部分代码

void handle_irq_c()
{
    puts("handle_irq_c\r\n");
    /*1 分辨中断源 */
    /*读INTOFFSET在芯片手册里找到这个寄存器,它里面的值表示INTPND中哪一位被设置成1*/
    int bit = INTOFFSET;
    /*2 调用对应的处理函数 */
    if (bit == 0 || bit == 2 || bit == 5)  /* 对应eint0,2,eint8_23 */
    {
        /*我们会调用一个按键处理函数*/
        key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND */
    } else if (bit == 10) /* 发生TIMER0时钟中断 */
    {
        timer_irq();
    }

    /*3 清中断 : 从源头开始清
     *先清除掉中断源里面的某些寄存器
     *再清 SRCPND
     *再清 INTPND
     */
    SRCPND = (1<<bit);
    INTPND = (1<<bit);	
}

timer.c

// 省略部分代码

void timer_irq()
{
    /* 电灯计数 */
    static int i = 4;
    //对数据寄存器4~6位取反
    GPFDAT ^= (1<<i);
    i++;
    if (i >= 7) {
        i=4;
    }
}

start.S

.text
.global _start

_start:
    b reset            /* vector 0: reset*/
    ldr pc, und_addr   /* vector 4: und*/
    ldr pc, swi_addr   /* vector 8: swi*/
    b halt             /* vector 0x0c : prefetch aboot */
    b halt             /* vector 0x10 : data abort */
    b halt             /* vector 0x14 : reserved */
    ldr pc, irq_addr   /* vector 0x18 : irq */
    b halt             /* vector 0x1c : fiq */

und_addr:
    .word do_und

swi_addr:
    .word do_swi

irq_addr:
    .word do_irq

do_und:
    /* 执行到这里之前:
     * 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
     * 2. SPSR_und保存有被中断模式的CPSR
     * 3. CPSR中的M4-M0被设置为11011, 进入到und模式
     * 4. 跳到0x4的地方执行程序 
     */

    //设置sp_und
    ldr sp, =0x34000000

    //保存现场
    stmdb sp!, {r0-r12, lr}

    //处理und异常
    mrs r0, cpsr
    ldr r1, =und_string
    bl printException

    //恢复现场
    ldmia sp!, {r0-r12, pc}^ //^会把spsr的值恢复到cpsr里

und_string:
    .string "undefined instruction exception"

.align 4  // string可能导致reset函数没有4字节对齐

do_swi:
    /* 执行到这里之前:
     * 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址
     * 2. SPSR_svc保存有被中断模式的CPSR
     * 3. CPSR中的M4-M0被设置为11011, 进入到svc模式
     * 4. 跳到0x4的地方执行程序 
     */

    //设置sp_und
    ldr sp, =0x33e00000

    //保存现场
    stmdb sp!, {r0-r12, lr}
    //ATPCS规则:C函数会保存r4 ~ r11这几个寄存器
    mov r4, lr

    //处理svc异常
    mrs r0, cpsr
    bl printMode

    sub r0, r4, #4
    bl printSWIVal

    //恢复现场
    ldmia sp!, {r0-r12, pc}^ //^会把spsr的值恢复到cpsr里

do_irq:
    /* 执行到这里之前:
     * 1. lr_irq保存有被中断模式中的下一条即将执行的指令的地址
     * 2. SPSR_irq保存有被中断模式的CPSR
     * 3. CPSR中的M4-M0被设置为10010, 进入到irq模式
     * 4. 跳到0x18的地方执行程序 
     */

    //设置sp_irq
    ldr sp, =0x33d00000

    //保存现场
    // 在irq异常处理函数中有可能会修改r0-r12, 所以先保存
    // lr-4是异常处理完后的返回地址, 也要保存
    sub lr, lr, #4
    stmdb sp!, {r0-r12, lr}

    bl handle_irq_c

    //恢复现场
    ldmia sp!, {r0-r12, pc}^ //^会把spsr_irq的值恢复到cpsr里

reset:
    /* 省略以下代码:
     1、关闭看门狗
     2、设置时钟
     3、设置栈指针
    */

    bl sdram_init

    bl copy2sdram

    bl clean_bss

    // 重定位完成后,跳转到SDRAM执行
    ldr pc, =sdram
sdram:

    bl uart0_init

und_code:
    .word 0xdeadc0de //故意加入一条未定义指令

    //上电后是supervisor模式0b10011
    mrs r0, cpsr
    bl printMode  //Spervisor mode

    mrs r0, cpsr      /* 读出cpsr 读到r0 */
    //使用bic命令 bitclean 把低4位清零
    bic r0, r0, #0xf  /* 修改M4-M0为0b10000, 进入usr模式 */
    bic r0, r0, #(1<<7) /* 清除I位,使能中断 */
    msr cpsr, r0
	
    mrs r0, cpsr
    bl printMode  //User mode

    swi 0x123 //执行此命令,会触发swi异常

    bl init_led /* 初始化LED引脚 */
    bl interrupt_init /* 初始化中断控制器 */
    bl key_eint_init  /* 初始化按键,设为中断源 */
    
    bl timer_init  /* 初始化时钟中断源 */
    
    bl main  /*bl相对跳转,程序仍在NOR/sram执行*/
    //ldr pc, =main  /*绝对跳转,跳到SDRAM*/

halt:
    b halt