为什么ATmega328P的中断向量表中,每个向量项既可以是2 字节,也可以是1 字节,仍然能工作?

编程语言 2026-07-10

对于 Arduino-Uno 板,配合 ATmega328Pavr-g++ 编译器:

如果我对一个使用定时器中断的简单LED闪烁代码进行反汇编,结果显示中断向量表中的每个入口都是2 个字(4字节)。代码运行正常。

如果我不使用标准启动文件和库,手动编写自己的启动代码和中断向量表,反汇编的代码显示中断向量表中的每个入口是1 个字(2字节)。代码同样正常。

如果向量表条目的大小由处理器决定,那么一个或另一个方法都应该能工作,但为什么两者都在工作?


带有标准启动文件和库的代码:blink.cpp

#define F_CPU 16000000UL
#include <avr/io.h>
#include <avr/interrupt.h>

ISR(TIMER1_COMPA_vect) {
  PORTB ^= _BV(PB5); // Toggle LED
}

int main(void) {
  DDRB |= _BV(DDB5); // Set PB5 as output

  // Configure Timer1: CTC mode, prescaler 256, 1Hz interrupt
  TCCR1A = 0;
  TCCR1B = _BV(WGM12) | _BV(CS12); // CTC mode, prescaler 256
  OCR1A = 62500 - 50000; // 16MHz / 256 / 1Hz
  TIMSK1 = _BV(OCIE1A); // Enable Timer1 Compare Match A interrupt

  sei(); // Enable global interrupts

  while (1) ;
}

命令:

avr-g++ -mmcu=atmega328p -Os -o blink.elf blink.cpp -Wa,-adhlns=blink.lst -Wl,-Map,blink.map
avr-objcopy -O ihex blink.elf blink.hex
avr-objdump -d -S blink.elf > blink.final.lst

从blink.final.lst反汇编的代码:

...
00000000 <__vectors>:
   0:   0c 94 34 00     jmp 0x68    ; 0x68 <__ctors_end>
   4:   0c 94 3e 00     jmp 0x7c    ; 0x7c <__bad_interrupt>
...
  28:   0c 94 3e 00     jmp 0x7c    ; 0x7c <__bad_interrupt>
  2c:   0c 94 40 00     jmp 0x80    ; 0x80 <__vector_11>
  30:   0c 94 3e 00     jmp 0x7c    ; 0x7c <__bad_interrupt>
...
  64:   0c 94 3e 00     jmp 0x7c    ; 0x7c <__bad_interrupt>

显然,每个入口都是2 个字(4字节)。

以下是完整的 blink.hex 文件:

:100000000C9434000C943E000C943E000C943E0082
:100010000C943E000C943E000C943E000C943E0068
:100020000C943E000C943E000C943E000C94400056
:100030000C943E000C943E000C943E000C943E0048
:100040000C943E000C943E000C943E000C943E0038
:100050000C943E000C943E000C943E000C943E0028
:100060000C943E000C943E0011241FBECFEFD8E04C
:10007000DEBFCDBF0E9452000C9463000C940000C0
:100080001F920F920FB60F9211248F939F9385B1F9
:1000900090E2892785B99F918F910F900FBE0F90A5
:1000A0001F901895259A109280008CE08093810013
:1000B00084ED90E3909389008093880082E08093A0
:0A00C0006F007894FFCFF894FFCF93
:00000001FF

不带标准启动文件和库的代码:blink.S

    .section .vectors, "ax", @progbits

    .global __vectors
__vectors:
    rjmp reset_handler           ; Vector 0: Reset

    ; Other interrupts can be dummy jumps to safe handlers or zeros
    rjmp dummy_handler           ; Vector 1
...
    rjmp dummy_handler           ; Vector 10

    .extern __vector_11          ; Declare ISR from C++
    rjmp __vector_11             ; Vector 11: Timer/Counter1 Capture Event ISR

    ; Further vectors...
    rjmp dummy_handler           ; Vector 12
...
    rjmp dummy_handler           ; Vector 26

.equ RAMEND, 0x08FF   ; Last SRAM address for ATmega328P
.equ SPL, 0x3D     ; I/O register address for Stack Pointer Low
.equ SPH, 0x3E     ; I/O register address for Stack Pointer High

reset_handler:
    ; Initialize stack pointer
    ldi r16, lo8(RAMEND)
    out SPL, r16
    ldi r16, hi8(RAMEND)
    out SPH, r16

    ; Call main()
    call main
    rjmp .                       ; Loop forever if main returns

dummy_handler:
    rjmp .                       ; Loop forever for unexpected interrupts

    .global main

blink.cpp

#include <avr/io.h>
#include <avr/interrupt.h>

ISR(TIMER1_COMPA_vect) {
  PORTB ^= (1 << PB5);  // Toggle LED
}

void timer1CompASetup(uint16_t ocr1aValue) {
  TCCR1A = 0;

  TCCR1B = (1 << WGM12) | (1 << CS12); // CTC mode, prescaler 256
  OCR1A = ocr1aValue;                  // 16MHz / 256 / 1Hz

  TIMSK1 = (1 << OCIE1A); // Timer1 COMPA interrupt enable
}

int main(void) {
  timer1CompASetup(62500 - 1);

  DDRB |= (1 << 5); // PB5 output (LED pin)

  sei();

  while (1) ;
}

命令: (注:-nostartfiles -nostdlib -nodefaultlibs -ffreestanding 选项)

avr-g++ -mmcu=atmega328p -Os -nostartfiles -nostdlib -nodefaultlibs -ffreestanding -o blink.elf blink.S blink.cpp -Wa,-adhlns=blink.lst -Wl,-e,__vectors,-Map,blink.map,--section-start=.vectors=0x0000
avr-objcopy -O ihex blink.elf blink.hex
avr-objdump -d -S blink.elf > blink.final.lst

从blink.final.lst反汇编的代码:

00000000 <__vectors>:
   0:   19 c0           rjmp    .+50        ; 0x34 <reset_handler>
   2:   1f c0           rjmp    .+62        ; 0x42 <dummy_handler>
...
  14:   16 c0           rjmp    .+44        ; 0x42 <dummy_handler>
  16:   16 c0           rjmp    .+44        ; 0x44 <__vector_11>
  18:   14 c0           rjmp    .+40        ; 0x42 <dummy_handler>
...
  32:   07 c0           rjmp    .+14        ; 0x42 <dummy_handler>

显然每个入口是1 个字(2字节)。

以下是完整的 blink.hex 文件:

:1000000019C01FC01EC01DC01CC01BC01AC019C013
:1000100018C017C016C016C014C013C012C011C03B
:1000200010C00FC00EC00DC00CC00BC00AC009C06C
:1000300008C007C00FEF0DBF08E00EBF0E944100CF
:1000400000C000C01F920F920FB60F9211248F9321
:100050009F9385B190E2892785B99F918F910F90E9
:100060000FBE0F901F901895109280002CE02093E7
:100070008100909389008093880082E080936F00D4
:10008000089583E294EF0E943400259A7894FFCF7C
:00000001FF

解决方案

你的主要问题在于你对 rjmp 的用法,因为你把 . 作为它的参数。这是一种常见的用法,因此并非你个人的错。

显然这会把下一条指令设为目标。我费了很大力气去找关于这一行为的可靠文档,但没有找到。对LLVM我找到了一个bug报告,但这是GNU binutils的汇编器。因此,情况如上所述。

在你的源码中:

dummy_handler:
    rjmp .                       ; Loop forever for unexpected interrupts

在反汇编中,目标地址清楚地显示出来(你本来就应该看到这一点,然而):

  42:   00 c0           rjmp    .+0         ;  0x44
  44:   1f 92           push    r1
  ...

所以当定时器比较中断被触发时会发生以下情况:

  1. AVR按预期并有文档地在字节地址0x2c处调用中断服务例程。
  2. 它进入哑处理程序,但不是循环,而是跳到下一条指令。
  3. 这恰好是你的实际中断服务例程的第一条指令。

如何解决这个问题?

通过使用显式标签来纠正你的无休止循环:

dummy_handler:
    rjmp dummy_handler           ; Loop forever for unexpected interrupts
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章