ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ARM7异常向量表配置详解:从跑飞事故到启动代码实战

ARM7异常向量表配置详解:从跑飞事故到启动代码实战 1. 从一次跑飞事故说起为什么异常向量表值得单独拎出来讲刚接触ARM7那会儿我调试一块LPC2103的板子程序烧进去之后上电就跑飞串口一个字节都不吐。用仿真器单步跟发现PC指针莫名其妙跳到了一个完全不相干的地址程序像断了线的风筝。折腾了大半天最后定位到问题异常向量表的位置配错了。MEMMAP寄存器没有正确设置CPU在复位时从0x00000000取向量而我的向量表实际被链接到了0x00000000以外的区域取到的指令自然是一堆垃圾数据。这件事让我意识到ARM7的异常向量表虽然只有短短32个字节8条跳转指令但它是整个系统启动和运行的中枢神经。配错了程序连活着的资格都没有。而很多教程一上来就讲指令集、讲流水线偏偏把这个最基础、最容易翻车的东西一笔带过。这篇内容就是围绕ARM7异常向量表的配置展开的。我会从异常类型、向量表结构、MEMMAP映射机制、启动代码写法、IRQ/FIQ中断处理一直到实际调试中踩过的坑一步步拆开讲。适合正在用ARM7做裸机开发或者刚接触启动代码的嵌入式工程师也适合那些程序能跑但说不清为什么能跑的朋友。看完之后你至少能做到自己动手写一份可靠的启动代码知道每一行向量表配置背后的道理遇到跑飞问题时能快速定位到向量表相关的根因。2. ARM7的七种异常不只是中断那么简单2.1 异常与中断的关系辨析很多人把异常和中断混着说严格来讲异常是CPU对内部或外部事件的响应机制中断只是异常的一个子集。ARM7TDMI支持七种异常类型按优先级从高到低排列如下优先级异常类型向量地址触发条件1最高复位Reset0x00000000上电、复位引脚拉低2未定义指令Undefined Instruction0x00000004执行到无法识别的指令3软件中断SWI0x00000008执行SWI指令4预取指中止Prefetch Abort0x0000000C指令预取时内存访问失败5数据中止Data Abort0x00000010数据访问时内存访问失败6外部中断请求IRQ0x00000018IRQ引脚有效且CPSR中I位为07最低快速中断请求FIQ0x0000001CFIQ引脚有效且CPSR中F位为0注意向量地址0x00000014是保留的ARM7没有使用这个位置。这个细节在写向量表时很容易被忽略如果你按顺序排下去IRQ就会跑到0x00000014去整个表就错位了。2.2 各异常进入时的CPU行为当某个异常被触发时ARM7内核会自动完成一系列动作这些动作是硬件层面的不需要软件干预保存返回地址将当前PC值或PC-4、PC-8取决于异常类型存入对应模式的LR寄存器。比如IRQ模式下存入LR_irqFIQ模式下存入LR_fiq。保存CPSR将当前程序状态寄存器CPSR的值复制到对应模式的SPSR中。切换模式修改CPSR的低5位切换到对应的处理器模式IRQ模式、FIQ模式、管理模式等。关闭中断对于IRQ自动置位CPSR的I位对于FIQ自动置位F位和I位。跳转向量将PC设置为对应的异常向量地址。这里有个关键点FIQ比IRQ多拥有5个私有寄存器R8_fiq到R12_fiq这意味着FIQ处理程序不需要像IRQ那样先保存R8-R12上下文切换开销更小。这也是FIQ响应速度比IRQ快的重要原因之一。如果你的系统对某个中断的实时性要求极高可以考虑把它分配到FIQ上。2.3 异常向量表的物理结构异常向量表本质上就是一段连续的跳转指令每条指令占4个字节对应一个异常入口。最典型的写法是AREA Vectors, CODE, READONLY ENTRY LDR PC, Reset_Addr LDR PC, Undefined_Addr LDR PC, SWI_Addr LDR PC, Prefetch_Addr LDR PC, Data_Addr NOP ; 保留位置0x00000014 LDR PC, IRQ_Addr LDR PC, FIQ_Addr Reset_Addr: DCD Reset_Handler Undefined_Addr: DCD Undefined_Handler SWI_Addr: DCD SWI_Handler Prefetch_Addr: DCD Prefetch_Handler Data_Addr: DCD Data_Handler IRQ_Addr: DCD IRQ_Handler FIQ_Addr: DCD FIQ_Handler为什么用LDR PC, [地址]而不是直接B 目标因为B指令的跳转范围只有±32MB而LDR PC, 目标地址可以跳转到4GB空间的任意位置。对于向量表来说处理程序可能被链接到Flash的高端地址或者外部RAM中用LDR更保险。当然如果你确定处理程序就在附近用B指令也可以代码更紧凑。注意向量表中的每一条LDR指令占用4字节8条指令正好32字节0x00到0x1C。如果你在向量表中插入了额外的代码后面的向量地址就会偏移这是新手最容易犯的错误之一。3. MEMMAP寄存器决定向量表住在哪里的关键3.1 为什么需要MEMMAPARM7处理器复位后默认从地址0x00000000取第一条指令。但问题是0x00000000这个地址在不同的系统中可能映射到不同的物理存储器有的是片内Flash有的是片内RAM有的是外部存储器。而且在系统运行过程中我们可能希望把向量表从Flash搬到RAM里以便动态修改中断入口。MEMMAP寄存器就是用来解决这个问题的。它位于0xE01FC040以LPC2000系列为例通过设置不同的值可以把向量表映射到不同的存储区域MEMMAP值模式向量表位置典型用途0x00Boot Loader模式0x00000000片内Boot Block出厂ISP下载用户一般不使用0x01用户Flash模式0x00000000片内Flash程序固化在Flash中运行0x02用户RAM模式0x40000000片内RAM调试阶段或需要动态修改向量表0x03外部存储器模式0x80000000外部总线程序运行在外部存储器中复位后MEMMAP的默认值取决于BOOT引脚的状态。如果BOOT引脚为低电平系统进入Boot Loader模式如果为高电平进入用户Flash模式。这个细节在硬件设计时就要确定好否则会出现程序烧进去了但不运行的情况。3.2 什么时候需要切换MEMMAP大部分量产产品中程序固化在片内Flash里MEMMAP保持默认的用户Flash模式0x01即可向量表就放在Flash的起始地址。但在以下场景中你需要考虑切换到RAM模式场景一调试阶段频繁修改中断向量。在Flash中修改向量表需要擦除整个扇区速度慢且损耗Flash寿命。把向量表搬到RAM中可以随时修改中断入口地址调试效率大幅提升。场景二需要动态重定位中断处理程序。某些系统在运行过程中需要根据工作模式切换不同的中断处理策略比如从正常模式切换到低功耗模式时中断处理逻辑完全不同。这时候把向量表放在RAM中修改起来就非常灵活。场景三从外部存储器启动。如果程序存储在外部Nor Flash中需要把MEMMAP设置为外部存储器模式让CPU从0x80000000取向量。切换MEMMAP的代码非常简单// 切换到RAM模式向量表映射到0x40000000 #define MEMMAP (*((volatile unsigned char *)0xE01FC040)) MEMMAP 0x02;但要注意切换之前必须确保目标地址处已经有一份有效的向量表否则切换的瞬间CPU取到无效指令直接跑飞。正确的做法是先把向量表复制到RAM的起始位置再修改MEMMAP。3.3 向量表重映射的完整流程以从Flash模式切换到RAM模式为例完整的操作步骤如下在链接脚本中为RAM向量表预留空间。通常是在RAM起始地址处预留32字节或64字节留一些余量。在启动代码中复制向量表。把Flash中的向量表逐字复制到RAM的预留区域。修改向量表中的跳转目标。如果RAM中的处理程序地址和Flash中不同需要更新向量表中的地址。设置MEMMAP寄存器。写入0x02完成映射切换。验证。可以通过读取0x00000000处的值确认它和RAM中的向量表内容一致。这里有个容易踩的坑复制向量表时源地址和目标地址可能重叠。比如从Flash的0x00000000复制到RAM的0x40000000这没问题。但如果你想把向量表从RAM的一个位置搬到另一个位置就要注意复制顺序避免覆盖还未复制的数据。一般用memcpy函数时如果源和目的有重叠行为是未定义的需要自己写循环控制方向。4. 启动代码中的向量表配置从复位到main的完整链路4.1 复位处理程序的第一条指令复位异常被触发后CPU从0x00000000取第一条指令这条指令通常是LDR PC, Reset_Addr跳转到真正的复位处理程序。复位处理程序要完成的工作包括初始化各模式的堆栈指针初始化看门狗如果硬件上有配置系统时钟PLL初始化存储器加速模块MAM复制数据段和清零BSS段跳转到main函数这些步骤的顺序不能乱。比如堆栈指针必须在调用任何C函数之前初始化因为C函数需要使用堆栈。而系统时钟的配置要在存储器加速之前完成因为MAM的等待周期取决于系统时钟频率。4.2 各模式堆栈的初始化ARM7有七种处理器模式其中六种用户模式除外有独立的堆栈指针。在复位处理程序中需要为每种模式分配堆栈空间并初始化SP寄存器; 进入管理模式设置管理模式的堆栈 MSR CPSR_c, #0x13 LDR SP, SVC_Stack ; 进入IRQ模式设置IRQ模式的堆栈 MSR CPSR_c, #0x12 LDR SP, IRQ_Stack ; 进入FIQ模式设置FIQ模式的堆栈 MSR CPSR_c, #0x11 LDR SP, FIQ_Stack ; 进入中止模式设置中止模式的堆栈 MSR CPSR_c, #0x17 LDR SP, ABT_Stack ; 进入未定义模式设置未定义模式的堆栈 MSR CPSR_c, #0x1B LDR SP, UND_Stack ; 最后回到用户模式或管理模式 MSR CPSR_c, #0x13堆栈大小的分配需要根据实际需求来定。IRQ堆栈要能容纳中断处理程序的所有局部变量和嵌套调用一般至少分配256字节到1KB。FIQ堆栈类似。管理模式堆栈要容纳main函数及其调用链通常分配1KB到4KB。如果系统使用了RTOS每个任务还有自己的堆栈那是另外分配的。提示在设置堆栈指针时要注意堆栈的生长方向。ARM7的堆栈通常是满递减Full Descending的即SP指向最后一个压入的数据压栈时SP先减后存。链接脚本中定义的堆栈起始地址应该是堆栈区域的最高地址。4.3 从汇编跳到C语言的注意事项从汇编启动代码跳转到C语言的main函数之前必须确保堆栈指针已正确初始化。至少管理模式或用户模式的SP要指向有效的RAM区域。数据段已从Flash复制到RAM。C语言中的全局变量和静态变量如果有初值这些初值存储在Flash中运行时需要复制到RAM。BSS段已清零。未初始化的全局变量和静态变量位于BSS段C标准要求它们初始值为0。系统时钟已配置。如果C代码中有延时函数或串口通信时钟频率不对会导致时序全错。复制数据段和清零BSS段的典型代码如下; 复制数据段 LDR R0, |Image$$RO$$Limit| ; ROM中数据段的起始地址 LDR R1, |Image$$RW$$Base| ; RAM中数据段的起始地址 LDR R2, |Image$$RW$$Limit| ; RAM中数据段的结束地址 CMP R0, R1 BEQ BSS_CLEAR COPY_LOOP: CMP R1, R2 LDRCC R3, [R0], #4 STRCC R3, [R1], #4 BCC COPY_LOOP ; 清零BSS段 BSS_CLEAR: LDR R1, |Image$$ZI$$Base| LDR R2, |Image$$ZI$$Limit| MOV R3, #0 BSS_LOOP: CMP R1, R2 STRCC R3, [R1], #4 BCC BSS_LOOP ; 跳转到main LDR PC, main这些符号Image$$RO$$Limit等是ARM链接器自动生成的前提是在链接脚本中正确配置了段的布局。如果你用的是GCC工具链符号名会有所不同需要根据链接脚本调整。5. IRQ与FIQ处理向量表之后的真正战场5.1 IRQ处理程序的通用框架IRQ异常被触发后CPU跳转到0x00000018处的向量通常是一条LDR PC, IRQ_Addr指令跳转到真正的IRQ处理程序。一个完整的IRQ处理程序需要完成保存上下文保存被中断程序使用的寄存器R0-R12、LR。识别中断源读取中断控制器的状态寄存器确定是哪个外设触发了中断。调用对应的处理函数根据中断源跳转到相应的处理逻辑。清除中断标志在中断控制器和外设中清除中断标志否则退出后会立即再次进入中断。恢复上下文恢复之前保存的寄存器。返回将LR减去4IRQ返回地址是当前指令4后写回PC同时恢复CPSR。用汇编写的IRQ处理程序框架大致如下IRQ_Handler: SUB LR, LR, #4 ; 计算返回地址 STMFD SP!, {R0-R12, LR} ; 保存上下文 BL IRQ_Dispatcher ; 调用C语言的中断分发函数 LDMFD SP!, {R0-R12, PC}^ ; 恢复上下文并返回注意最后的^符号它表示在恢复PC的同时把SPSR的值复制回CPSR完成处理器模式的切换和中断使能状态的恢复。5.2 中断源识别与分发在C语言的中断分发函数中需要读取中断控制器的状态。以LPC2000系列为例VIC向量中断控制器提供了多个寄存器来管理中断寄存器地址功能VICIRQStatus0xFFFFF000IRQ中断状态每位对应一个中断源VICFIQStatus0xFFFFF004FIQ中断状态VICRawIntr0xFFFFF008原始中断状态未经过使能屏蔽VICIntEnable0xFFFFF010中断使能寄存器VICIntEnClr0xFFFFF014中断使能清除寄存器VICSoftInt0xFFFFF018软件中断寄存器VICVectAddr0xFFFFF030向量地址寄存器读取后自动更新优先级中断分发函数的典型实现void IRQ_Dispatcher(void) { unsigned long irq_status; unsigned long vect_addr; irq_status VICIRQStatus; // 根据中断状态位判断中断源 if (irq_status (1 4)) { // 定时器0中断 Timer0_ISR(); } if (irq_status (1 5)) { // 定时器1中断 Timer1_ISR(); } if (irq_status (1 6)) { // UART0中断 UART0_ISR(); } // ... 其他中断源 // 写VICVectAddr通知中断控制器处理完毕 VICVectAddr 0; }最后写VICVectAddr 0这一步非常关键。它告诉VIC中断处理已经完成VIC可以更新优先级状态允许同优先级或更低优先级的中断再次触发。如果忘了这一步系统会一直卡在最高优先级的中断上其他中断永远得不到响应。5.3 FIQ的独特优势与使用场景FIQ的设计初衷是提供一个比IRQ更快的中断响应通道。它的优势体现在三个方面第一向量表位置特殊。FIQ向量位于0x0000001C是向量表的最后一个条目。这意味着FIQ处理程序可以直接放在向量表之后不需要额外的跳转指令。CPU跳转到0x0000001C后下一条指令就是FIQ处理程序的第一条指令省去了一次跳转。第二私有寄存器多。FIQ模式拥有R8_fiq到R12_fiq共5个私有寄存器。如果FIQ处理程序只使用这些私有寄存器就不需要保存和恢复上下文中断延迟大幅降低。第三优先级最高。FIQ的优先级高于IRQ当两者同时触发时FIQ先被响应。而且FIQ会自动屏蔽IRQCPSR的I位被置位保证FIQ处理程序不被IRQ打断。FIQ适合处理对实时性要求极高的中断比如高速ADC采样、电机控制中的PWM紧急关断等。但FIQ资源有限只有一个要谨慎分配。如果系统中有多个高实时性需求可能需要重新评估架构。5.4 中断嵌套的实现默认情况下ARM7在进入IRQ模式时会自动关闭IRQCPSR的I位置1这意味着IRQ处理程序不会被另一个IRQ打断。但在某些系统中我们希望高优先级的中断能够打断低优先级的中断处理这就需要实现中断嵌套。实现中断嵌套的关键步骤在IRQ处理程序中重新使能IRQ。在保存上下文之后通过MSR CPSR_c, #0x12IRQ模式I位清零来重新打开中断。确保堆栈足够大。嵌套中断会消耗更多堆栈空间IRQ堆栈要预留足够的余量。中断控制器支持优先级。VIC可以根据优先级决定是否响应新的中断请求。在关键代码段中关闭中断。访问共享资源时要临时关闭中断防止数据竞争。中断嵌套是一把双刃剑。用好了能提升系统实时性用不好会导致堆栈溢出、优先级反转等问题。我的建议是除非确实有必要否则不要轻易开启中断嵌套。大部分嵌入式系统的中断处理程序都很短关中断的时间窗口很小不嵌套也能满足实时性要求。6. 调试实录那些年我在向量表上踩过的坑6.1 程序烧进去不跑串口无输出这是最经典的症状。可能的原因有MEMMAP配置错误。如果BOOT引脚状态和MEMMAP设置不匹配CPU可能从Boot Block取向量而不是从用户Flash。检查方法用仿真器读取0xE01FC040的值确认MEMMAP模式正确。向量表链接地址错误。链接脚本中向量表的起始地址必须是0x00000000或MEMMAP指定的地址。如果链接到了其他地址复位时CPU取不到正确的向量。复位处理程序中的堆栈初始化有问题。如果SP指向了无效地址第一次压栈就会触发数据中止程序跑飞。排查这类问题时我通常的做法是先用仿真器单步执行复位后的前几条指令观察PC的跳转路径。如果PC跳到了一个意料之外的地址基本可以确定是向量表或MEMMAP的问题。6.2 中断进不去或者进去一次就死机中断进不去的原因可能包括CPSR的I位或F位没有清零。复位后CPSR的I位和F位默认是置位的需要在启动代码中清除否则IRQ和FIQ被屏蔽。VIC中断使能寄存器没有配置。每个中断源都需要在VICIntEnable中使能否则中断信号到不了CPU。外设的中断使能位没有打开。比如UART中断除了VIC使能还需要在UART的控制寄存器中使能接收/发送中断。中断进去一次就死机最常见的原因是没有清除中断标志。中断处理程序执行完后如果外设的中断标志没有清除中断控制器会认为中断仍然有效CPU退出中断后立即再次进入形成死循环。表现就是程序卡在中断里出不来主循环完全不执行。6.3 向量表重映射后程序跑飞把向量表从Flash搬到RAM后程序跑飞通常是因为复制向量表之前就切换了MEMMAP。切换的瞬间CPU从RAM取向量但RAM中还没有有效数据取到的是随机值。RAM中的向量表地址没有更新。如果处理程序的地址在RAM和Flash中不同复制后需要修改向量表中的跳转地址。RAM区域没有正确初始化。某些ARM7芯片的片内RAM在上电后需要先使能通过PCONP或类似寄存器否则访问会触发数据中止。我的经验是重映射操作要放在启动代码的最后阶段在系统时钟、存储器、堆栈都初始化完成之后再执行向量表复制和MEMMAP切换。切换完成后立即用一个简单的测试比如触发一个软件中断来验证向量表是否生效。6.4 一个隐蔽的坑向量表中的NOP前面提到过0x00000014是保留位置。有些启动代码模板在这个位置放了NOP有些放了B .死循环有些甚至放了一个无效指令。如果这个位置的内容不对虽然不影响正常功能但在某些调试场景下会造成困惑。比如如果你在调试器中查看向量表发现0x00000014处的指令不是预期的NOP可能会怀疑向量表整体错位了。实际上只要IRQ和FIQ的向量地址正确0x18和0x1C中间这个位置放什么并不影响功能。但为了代码的可读性和可维护性建议还是放NOP或者一个明确的占位符。7. 一份可直接复用的向量表配置模板7.1 汇编启动文件; ; ARM7异常向量表与启动代码模板 ; 适用于LPC2000系列其他ARM7芯片请根据手册调整 ; PRESERVE8 AREA VECTORS, CODE, READONLY ENTRY EXPORT __start __start: ; 异常向量表 LDR PC, Reset_Addr LDR PC, Undefined_Addr LDR PC, SWI_Addr LDR PC, Prefetch_Addr LDR PC, Data_Addr NOP ; 保留位置 LDR PC, IRQ_Addr LDR PC, FIQ_Addr Reset_Addr: DCD Reset_Handler Undefined_Addr: DCD Undefined_Handler SWI_Addr: DCD SWI_Handler Prefetch_Addr: DCD Prefetch_Handler Data_Addr: DCD Data_Handler IRQ_Addr: DCD IRQ_Handler FIQ_Addr: DCD FIQ_Handler ; ; 复位处理程序 ; Reset_Handler: ; 设置各模式堆栈 MSR CPSR_c, #0xD3 ; 管理模式关中断 LDR SP, SVC_Stack_Top MSR CPSR_c, #0xD2 ; IRQ模式 LDR SP, IRQ_Stack_Top MSR CPSR_c, #0xD1 ; FIQ模式 LDR SP, FIQ_Stack_Top MSR CPSR_c, #0xD7 ; 中止模式 LDR SP, ABT_Stack_Top MSR CPSR_c, #0xDB ; 未定义模式 LDR SP, UND_Stack_Top MSR CPSR_c, #0xDF ; 系统模式 LDR SP, SYS_Stack_Top ; 复制数据段 LDR R0, |Image$$RO$$Limit| LDR R1, |Image$$RW$$Base| LDR R2, |Image$$RW$$Limit| CMP R0, R1 BEQ BSS_CLEAR COPY_LOOP: CMP R1, R2 LDRCC R3, [R0], #4 STRCC R3, [R1], #4 BCC COPY_LOOP ; 清零BSS段 BSS_CLEAR: LDR R1, |Image$$ZI$$Base| LDR R2, |Image$$ZI$$Limit| MOV R3, #0 BSS_LOOP: CMP R1, R2 STRCC R3, [R1], #4 BCC BSS_LOOP ; 跳转到main LDR PC, main ; ; 默认异常处理程序死循环便于调试定位 ; Undefined_Handler: B Undefined_Handler SWI_Handler: B SWI_Handler Prefetch_Handler: B Prefetch_Handler Data_Handler: B Data_Handler IRQ_Handler: SUB LR, LR, #4 STMFD SP!, {R0-R12, LR} BL IRQ_Dispatcher LDMFD SP!, {R0-R12, PC}^ FIQ_Handler: ; FIQ处理程序尽量简短 SUBS PC, LR, #4 ; ; 堆栈空间定义 ; AREA STACKS, DATA, NOINIT, READWRITE, ALIGN3 EXPORT SVC_Stack_Top EXPORT IRQ_Stack_Top EXPORT FIQ_Stack_Top EXPORT ABT_Stack_Top EXPORT UND_Stack_Top EXPORT SYS_Stack_Top SVC_Stack_Space: SPACE 1024 SVC_Stack_Top: DCD 0 IRQ_Stack_Space: SPACE 512 IRQ_Stack_Top: DCD 0 FIQ_Stack_Space: SPACE 256 FIQ_Stack_Top: DCD 0 ABT_Stack_Space: SPACE 256 ABT_Stack_Top: DCD 0 UND_Stack_Space: SPACE 256 UND_Stack_Top: DCD 0 SYS_Stack_Space: SPACE 1024 SYS_Stack_Top: DCD 0 END7.2 链接脚本关键配置以Keil MDK的分散加载文件为例LR_IROM1 0x00000000 0x00040000 { ; 加载区域Flash起始地址 ER_IROM1 0x00000000 0x00040000 { ; 执行区域Flash *.o (VECTORS, First) ; 向量表放在最前面 *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x40000000 0x00004000 { ; RAM区域 .ANY (RW ZI) } }关键点是*.o (VECTORS, First)它确保向量表被链接到Flash的最前面即0x00000000处。如果链接脚本中没有这一行链接器可能会把其他代码放在前面导致向量表地址偏移。7.3 中断分发函数的C实现#include lpc2000.h // 中断处理函数指针数组 typedef void (*isr_func_t)(void); static isr_func_t isr_table[32] {0}; // 注册中断处理函数 void register_isr(unsigned int irq_num, isr_func_t func) { if (irq_num 32) { isr_table[irq_num] func; } } // IRQ分发函数 void IRQ_Dispatcher(void) { unsigned long irq_status; unsigned long irq_num; irq_status VICIRQStatus; for (irq_num 0; irq_num 32; irq_num) { if (irq_status (1UL irq_num)) { if (isr_table[irq_num] ! 0) { isr_table[irq_num](); } } } // 通知VIC中断处理完毕 VICVectAddr 0; }这种表驱动的中断分发方式比一长串if-else更清晰也更容易扩展。新增中断源时只需要调用register_isr注册即可不需要修改分发函数本身。8. 几个容易被忽略的细节与个人经验8.1 向量表的对齐要求ARM7要求异常向量表必须32位对齐因为每条向量指令都是32位的。在汇编中AREA指令的ALIGN属性可以保证对齐。在C语言中定义向量表时要使用__attribute__((aligned(4)))或类似的编译器指令。如果向量表没有对齐CPU取指令时可能触发对齐异常或者取到错误的指令。这种问题在调试时很难发现因为编译器和链接器通常会自动对齐但如果你手动指定了地址就要格外小心。8.2 中断响应时间的估算从外设触发中断到中断处理程序的第一条指令执行这段时间称为中断延迟。它由以下几部分组成外设同步延迟中断信号经过外设内部的同步电路通常需要2-3个时钟周期。CPU响应延迟ARM7完成当前指令后才会响应中断最坏情况下需要等待当前指令执行完毕。ARM7TDMI的指令周期数因指令类型而异LDM/STM等多周期指令可能需要十几个周期。向量表跳转延迟从0x00000018取LDR指令再到处理程序的第一条指令需要额外的取指周期。上下文保存时间如果处理程序需要保存大量寄存器这部分时间也要计入。在50MHz的LPC2103上典型的中断延迟大约在1-2微秒。如果你的系统对中断延迟有严格要求需要仔细评估每个环节。8.3 调试时的一个实用技巧当你不确定向量表是否正确配置时可以在仿真器中直接查看内存地址0x00000000到0x0000001C的内容。正确的向量表应该能看到8条有效的指令每条指令的机器码对应LDR PC, [PC, #偏移]的格式。另外可以在复位处理程序的第一条指令处设置断点观察CPU是否真的从0x00000000开始执行。如果断点没有命中说明MEMMAP配置或BOOT引脚状态有问题。8.4 关于FIQ处理程序的一个建议FIQ处理程序应该尽可能简短最好只用R8_fiq到R12_fiq这几个私有寄存器。如果必须使用R0-R7记得在进入时保存、退出时恢复。但这样一来FIQ的速度优势就大打折扣了。我的做法是FIQ只用来处理最紧急的事件比如检测到过流信号后立即关闭PWM输出。这种操作只需要几条指令完全可以用私有寄存器完成。复杂的数据处理留给IRQ或主循环。8.5 向量表重映射后的验证方法切换到RAM模式后可以通过以下方法验证向量表是否生效读取0x00000000处的值和RAM中向量表的第一个字比较应该相同。触发一个软件中断执行SWI指令观察是否跳转到SWI_Handler。使能一个定时器中断观察IRQ_Handler是否被正确调用。如果验证失败先检查MEMMAP寄存器的值是否正确再检查RAM中的向量表内容是否完整。有时候问题出在复制过程中比如复制长度不对、源地址或目的地址计算错误等。9. 从向量表延伸出去启动代码的完整视角异常向量表只是启动代码的一部分。一个完整的ARM7启动流程还包括时钟配置、存储器加速、外设初始化、RTOS启动如果有等。向量表配置之所以重要是因为它是整个流程的入口入口错了后面的代码写得再好也没用。我在实际项目中养成了一个习惯每次新建工程时先写一个最小的启动代码只包含向量表和堆栈初始化然后点一个LED灯。确认这个最小系统能跑起来之后再逐步添加时钟配置、串口、中断等功能。这样一旦出问题排查范围很小不会在一堆代码里大海捞针。另外不同厂商的ARM7芯片在细节上可能有差异。比如NXP的LPC2000系列有MEMMAP寄存器而Atmel的AT91系列可能用不同的机制来映射向量表。拿到一颗新芯片时第一件事就是翻数据手册的Memory Mapping和Interrupt Controller章节确认向量表的地址和重映射方式。这个时间投入是值得的能帮你省下大量调试时间。最后分享一个我常用的调试手段在向量表的每个异常入口处放一条B .指令死循环然后在仿真器中触发对应的异常看PC是否停在了预期的地址。这个方法能快速验证向量表的每个条目是否正确比读寄存器状态更直观。
返回列表