上一篇随笔中对中断进行了简要的总结,这一篇将结合程序加深对ISR的理解。
一、使用中断
ISR执行之前将进行以下初始化工作。
1.1 建立堆栈
前面讲到堆栈容量过小将导致栈溢出,因此计算容量很重要。较为简单的程序使用MSP,此时只需要开辟一个容量够大的堆栈,将MSP初始化到其顶即可。对于大型高性能程序,则需要两个堆栈配合使用,此时就需要谨慎计算两个堆栈的容量。(开发中一般在调试阶段把堆栈进程用过的最大的量输出,最终作为主堆栈的容量,以免浪费内存)
1.2 建立向量表
大多数情况下,程序只给每个中断固定的ISR,此时把向量表放在ROM中即可。有时候,为了应付复杂情况,需要动态改变ISR,就需要重定位向量表了,此时向量表需保存在R/W寄存器中。
重定位钱,首先将现有的向量表往新的位置复制一份,需要拷贝的主要是各种fault、NMI、SVC的服务例程。将所有必要向量填好之后,就可以启用新的向量表了。并往其中加入新的中断向量,程序如下:
;该子程序根据异常类型建立了相应的异常向量 ;入口条件:R0=异常类型编号,R1=向量地址 PUSH {R2, LR} LDR R2, =0xE000ED08 ;向量表偏移寄存器的地址 LDR R2, [R2] ;获取向量表的首地址 STR R1, [R2,RO,LSL #4] ;在偏移地址vectblOffset(偏移 ;量)+ExcpType(异常类型)*4地 ;址处写入向量ExcpType*4 POP {R2, PC}
另外,向量表写入的RAM是写缓冲的,那向量表更新可能会被延迟,因此建立所有向量后必须追加DSB指令。
1.3 分配各中断的优先级
找到寄存器直接写入即可。若要确定优先级位数,可以在寄存器中写入0xFF,读出1的个数即是位数。(可以采用RBIT和CLZ来配合读出1的个数)
1.4 使能和除能中断
使用SETENA/CLRENA,程序如下所示。由于4字节对齐,因此程序的主要内容需要计算位于8个寄存器中的哪一个32位寄存器和该32位寄存器中的哪一位。
;使能中断子程序 EnableIRQ ;入口条件:R0=中断号 PUSH {R0-R2, LR} AND.W R1, R0, #0x1F ;IRQ产生的移位量,.W表示强制转换为32位 MOV R2, #1 LSL R2, R2, R1 ;位旗标= (0x1 << (R0 & 0x1F)),即计算位于32位寄存器的哪一位
AND.W R1, R0, #0xE0 ; 若IRQ编号>31则为它生成下标偏移量,计算位于哪一个32位寄存器
LSR R1, R1, #3 ; 地址偏移量= (N/32)*4(每个IRQ一个位)
LDR R0, =0xE000E100 ; 加载SETENA寄存器阵列的首地址 STR R2, [R0, R1] ;写入该中断的位旗标,从而使能该中断 POP {R0-R2, PC} ;---------------------------------------------------------------------- ;除能中断子程序,与使能程序几乎一样 DisableIRQ ; 入口条件:R0=中断号 PUSH {R0-R2, LR} AND.W R1, R0, #0x1F ; 为该IRQ产生移位量 MOV R2, #1 LSL R2, R2, R1 ; 位旗标= (0x1 << (N & 0x1F)) AND.W R1, R0, #0xE0 ; 若IRQ编号>31则为它生成下标偏移量 LSR R1, R1, #3 ; 地址偏移量= (N/32)*4(每个IRQ一个位) LDR R0, =0xE000E180 ; 加载CLRENA寄存器阵列的首地址 STR R2, [R0, R1] ; 写入该中断的位旗标,从而除能该中断 POP {R0-R2, PC} ; 子程返回
二、中断服务例程
STACK_TOP EQU 0x20002000 ; MSP初始值 NVIC_SETEN EQU 0xE000E100 ; SETENA寄存器阵列的起始地址 NVIC_VECTTBL EQU 0xE000ED08 ; 向量表偏移寄存器的地址 NVIC_AIRCR EQU 0xE000ED0C ; 应用程序中断及复位控制寄存器的地址 NVIC_IRQPRI EQU 0xE000E400 ; 中断优先级寄存器阵列的起始地址 AREA | Header Code|, CODE DCD STACK_TOP ; MSP初始值 DCD Start ; 复位向量 DCD Nmi_Handler ; NMI服务例程 DCD Hf_Handler ; 硬fault服务例程 ENTRY Start ; 主程序开始 ; 初始化各寄存器 MOV r0, #0 MOV r1, #0 ... ; 把各个向量拷贝到新向量表中 LDR r0, =0 ;地址0处开始为复位向量 LDR r1, =VectorTableBase LDMIA r0!, {r2-r5} ; 拷贝4个向量(MSP, Reset, NMI, 硬fault) STMIA r1!, {r2-r5} DSB ; 数据同步隔离 ; 执行向量表重定位: LDR r0, =NVIC_VECTTBL ;将拷贝的向量保存到偏移地址处 LDR r1, =VectorTableBase STR r1, [r0] ... ; 设置优先级组寄存器,划分抢占优先级与亚优先级 LDR r0, =NVIC_AIRCR LDR r1, =0x05FA0500 ; 从位5处划分(共2个位表达抢占优先级) STR R1, [r0] ; 建立IRQ0的向量 MOV r0, #0 ; IRQ#0 LDR r1, =Irq0_Handler BL SetupIrqHandler ; 建立IRQ #0的优先级 LDR r0, =NVIC_IRQPRI LDR r1, =0xC0 ; IRQ#0的优先级 STRB r1, [r0,#0] ; 写入优先级寄存器中,用了按字节传送 DSB ; 数据同步隔离,保证开中断前一切都已各就各位 MOV r0, #0 ; 选择IRQ #0 BL EnableIRQ ... ;------------------------ ; 各函数 SetupIrqHandler
; 在向量表中建立ISR入口地址 ; 入口条件:R0 = IRQ编号 ; 入口条件:R1 = IRQ服务例程的入口地址 PUSH {R0, R2, LR} LDR R2, =NVIC_VECTTBL ; 获取向量表的地址 LDR R2, [R2] ADD R0, #16 ; 异常号 = IRQ编号+ 16 LSL R0, R0, #2 ; 乘以4 (每个向量4字节) ADD R2, R0 ; 找出向量地址 STR R1, [R2] ; 写入服务例程 POP {R0, R2, PC} ; 返回 EnableIRQ ; 入口条件:R0=中断号 PUSH {R0-R2, LR} AND.W R1, R0, #0x1F ; 为该IRQ产生移位量 MOV R2, #1 LSL R2, R2, R1 ; 位旗标= (0x1 << (N & 0x1F)) AND.W R1, R0, #0xE0 ; 若IRQ编号>31则为它生成下标偏移量 LSR R1, R1, #3 ; 地址偏移量= (N/32)*4(每个IRQ一个位) LDR R0, =NVIC_SETEN ; 加载SETENA寄存器阵列的首地址 STR R2, [R0, R1] ; 写入该中断的位旗标,从而使能该中断 POP {R0-R2, PC} ; 子程返回 ;------------------------ ; 异常服务程序 Hf_Handler ... ; 在此添加硬fault的处理代码 BX LR Nmi_Handler ... ; 在此添加NMI的响应代码 BX LR Irq0_Handler ... ; 在此添加IRQ #0的响应代码 BX LR ; Return ;------------------------ AREA | Header Data|, DATA ALIGN 4 ; 重定位的向量表 VectorTableBase SPACE 256 ; 保留256字节作向量表 VectorTableEnd ; (256 / 4 = 最多支持64个异常) MyData1 DCD 0 ; 定义变量 MyData2 DCD 0 END ; 文件结尾
三、SVC服务例程
在SVC服务例程中,需要读取本次触发SVC异常的SVC指令,首先通过EXC_RETURN判断使用的是MSP还是PSP,并提取出8位立即数所在的位段,来判断系统调用号。
svc_handler ;开始读取参数 TST LR, #0x4 ; 测试EXC_RETURN的比特2 ITE EQ ; 如果为0, MRSEQ R0, MSP ; 则使用的是主堆栈,故把MSP的值取出 MRSNE R0, PSP ; 否则, 使用的是进程堆栈,故把MSP的值取出 LDR R0, [R1,#0] ; 从堆栈中读取R0的值 LDR R1, [R1,#24] ; 从堆栈中读取当时的PC LDRB R1, [R1,#-2] ; 提取SVC指令中的8位立即数 ; 现在:R0存储了参数,R1存储了服务代号 PUSH {LR} ; 保护LR的值,因为后面将使用的BL指令 CBNZ R1, svc_handler_1 BL Puts ; 调用Puts B svc_handler_end svc_handler_1 CMP R1, #1 BNE svc_handler_2 BL Putc ; 调用Putc B svc_handler_end svc_handler_2 CMP R1, #2 BNE svc_handler_3 BL PutHex ; 调用PutHex B svc_handler_end svc_handler_3 CMP R1, #3 BNE svc_handler_4 BL PutDec ; 调用PutDec B svc_handler_end svc_handler_4 B error ; 未能识别的服务代号 ... svc_handler_end POP {PC} ; Return