AVR 中断服务程序没有像预期的那样快速执行(指令开销?)

电器工程 avr 大气压 中断 逻辑分析仪 时钟速度
2022-02-06 08:46:32

我正在开发一个带有 7 个输入的小型逻辑分析仪。我的目标设备是一个ATmega168时钟频率为 20MHz 的设备。为了检测逻辑变化,我使用引脚变化中断。现在我试图找出我可以检测到这些引脚变化的最低采样率。我确定了一个最小值为 5.6 µs (178.5 kHz)。低于此速率的每个信号我都无法正确捕获。

我的代码是用 C (avr-gcc) 编写的。我的例程如下:

ISR()
{
    pinc = PINC; // char
    timestamp_ll = TCNT1L; // char
    timestamp_lh = TCNT1H; // char
    timestamp_h = timerh; // 2 byte integer
    stack_counter++;
}

我捕获的信号变化位于pinc为了本地化它,我有一个 4 字节长的时间戳值。

在我阅读的数据表中,中断服务程序需要 5 个时钟跳入,5 个时钟返回主程序。我假设我的每个命令ISR()都需要 1 个时钟来执行;所以总而言之,应该有5 + 5 + 5 = 15时钟开销。一个时钟的持续时间应根据 20MHz 的时钟速率1/20000000 = 0.00000005 = 50 ns以秒为单位的总开销应该是:15 * 50 ns = 750 ns = 0.75 µs. 现在我不明白为什么我不能捕获低于 5.6 µs 的任何东西。谁能解释发生了什么?

2个回答

有几个问题:

  • 并非所有 AVR 命令都需要 1 个时钟来执行:如果您查看数据表的背面,它会显示执行每条指令所需的时钟数。所以,例如AND是一个时钟指令,MUL(乘)需要两个时钟,而LPM(加载程序存储器)是三个,CALL是4。所以,关于指令执行,它真的取决于指令。
  • 跳入 5 个时钟,返回 5 个时钟可能会产生误导。如果你看你的反汇编代码,你会发现除了跳转和RETI指令之外,编译器还添加了各种其他代码,这也需要时间。例如,您可能需要在堆栈上创建并且必须弹出的局部变量等。查看实际情况的最佳方法是查看反汇编。
  • 最后,请记住,当您在 ISR 例程中时,您的中断不会触发。这意味着您将无法从逻辑分析仪获得所需的性能,除非您知道信号电平的变化间隔比服务中断所需的时间长。需要明确的是,一旦您计算出执行 ISR 所需的时间,这将为您提供捕获一个信号的速度上限。如果您需要捕获两个信号,那么您就会遇到麻烦。要对此过于详细,请考虑以下情况:

在此处输入图像描述

如果x是服务中断所需的时间,那么信号 B 将永远不会被捕获。


如果我们获取您的 ISR 代码,将其粘贴到 ISR 例程(我使用过ISR(PCINT0_vect))例程中,声明所有变量volatile,并为 ATmega168P 编译,反汇编代码如下所示(有关更多信息,请参阅@jipple 的答案),然后再获取代码那“做某事”换句话说,您的 ISR 的序幕如下:

  37                    .loc 1 71 0
  38                    .cfi_startproc
  39 0000 1F92              push r1
  40                .LCFI0:
  41                    .cfi_def_cfa_offset 3
  42                    .cfi_offset 1, -2
  43 0002 0F92              push r0
  44                .LCFI1:
  45                    .cfi_def_cfa_offset 4
  46                    .cfi_offset 0, -3
  47 0004 0FB6              in r0,__SREG__
  48 0006 0F92              push r0
  49 0008 1124              clr __zero_reg__
  50 000a 8F93              push r24
  51                .LCFI2:
  52                    .cfi_def_cfa_offset 5
  53                    .cfi_offset 24, -4
  54 000c 9F93              push r25
  55                .LCFI3:
  56                    .cfi_def_cfa_offset 6
  57                    .cfi_offset 25, -5
  58                /* prologue: Signal */
  59                /* frame size = 0 */
  60                /* stack size = 5 */
  61                .L__stack_usage = 5

所以,PUSHx 5,inx 1,clrx 1。没有 jipple 的 32 位变量那么糟糕,但也不是什么都没有。

其中一些是必要的(在评论中展开讨论)。显然,由于 ISR 例程可以随时发生,它必须保留它使用的寄存器,除非您知道没有可能发生中断的代码使用与您的中断例程相同的寄存器。例如,反汇编 ISR 中的以下行:

push r24

是否存在,因为一切都经过r24:您pinc在进入内存之前已加载到那里,等等。所以您必须首先拥有它。__SREG__被加载r0然后推送:如果这可以通过,r24那么你可以为自己节省一个PUSH


一些可能的解决方案:

  • 使用Kaz在评论中建议的紧密轮询循环。这可能是最快的解决方案,无论您是用 C 语言还是汇编语言编写循环。
  • 在汇编中编写您的 ISR:通过这种方式,您可以优化寄存器的使用,使得在 ISR 期间需要保存的寄存器数量最少。
  • 声明你的 ISR 例程ISR_NAKED,尽管这更像是一个红鲱鱼解决方案。当您声明 ISR 例程ISR_NAKED时,gcc 不会生成序言/结尾代码,并且您负责保存您的代码修改的任何寄存器,以及调用reti(从中断返回)。不幸的是,没有办法直接在 avr-gcc C 中使用寄存器(显然您可以在汇编中),但是,您可以做的是使用+关键字将变量绑定到特定寄存器,如下所示:如果您这样做,对于 ISR,您将知道您在 ISR 中使用了哪些寄存器。那么问题是没有办法生成registerasmregister uint8_t counter asm("r3");pushpop在没有内联汇编的情况下保存使用的寄存器(参见第 1 点)。为了确保必须保存更少的寄存器,您还可以将所有非 ISR 变量绑定到特定的寄存器,但是,您会遇到 gcc 使用寄存器将数据移入和移出内存的问题。这意味着除非您查看反汇编,否则您将不知道您的主代码使用什么寄存器。所以如果你正在考虑ISR_NAKED,你不妨把ISR写成汇编。

在您的实际 ISR 开始之前,有很多 PUSH'ing 和 POP'ing 寄存器要堆栈,即在您提到的 5 个时钟周期之上。看一下生成的代码的反汇编。

根据您使用的工具链,以各种方式转储我们完成的程序集列表。我在 Linux 命令行上工作,这是我使用的命令(它需要 .elf 文件作为输入):

avr-objdump -C -d $(src).elf

看看我最近用于 ATtiny 的代码片段。这是 C 代码的样子:

ISR( INT0_vect ) {
        uint8_t myTIFR  = TIFR;
        uint8_t myTCNT1 = TCNT1;

这是为其生成的汇编代码:

00000056 <INT0_vect>:
  56:   1f 92           push    r1
  58:   0f 92           push    r0
  5a:   0f b6           in      r0, SREG        ; 0x3f
  5c:   0f 92           push    r0
  5e:   11 24           eor     r1, r1
  60:   2f 93           push    r18
  62:   3f 93           push    r19
  64:   4f 93           push    r20
  66:   8f 93           push    r24
  68:   9f 93           push    r25
  6a:   af 93           push    r26
  6c:   bf 93           push    r27
  6e:   48 b7           in      r20, TIFR       ; uint8_t myTIFR  = TIFR;
  70:   2f b5           in      r18, TCNT1      ; uint8_t myTCNT1 = TCNT1;

老实说,我的 C 例程使用了更多变量来导致所有这些 push 和 pop,但你明白了。

加载 32 位变量如下所示:

  ec:   80 91 78 00     lds     r24, 0x0078
  f0:   90 91 79 00     lds     r25, 0x0079
  f4:   a0 91 7a 00     lds     r26, 0x007A
  f8:   b0 91 7b 00     lds     r27, 0x007B

将 32 位变量增加 1 如下所示:

  5e:   11 24           eor     r1, r1
  d6:   01 96           adiw    r24, 0x01       ; 1
  d8:   a1 1d           adc     r26, r1
  da:   b1 1d           adc     r27, r1

存储 32 位变量如下所示:

  dc:   80 93 78 00     sts     0x0078, r24
  e0:   90 93 79 00     sts     0x0079, r25
  e4:   a0 93 7a 00     sts     0x007A, r26
  e8:   b0 93 7b 00     sts     0x007B, r27

当然,一旦您离开 ISR,您就必须弹出旧值:

 126:   bf 91           pop     r27
 128:   af 91           pop     r26
 12a:   9f 91           pop     r25
 12c:   8f 91           pop     r24
 12e:   4f 91           pop     r20
 130:   3f 91           pop     r19
 132:   2f 91           pop     r18
 134:   0f 90           pop     r0
 136:   0f be           out     SREG, r0        ; 0x3f
 138:   0f 90           pop     r0
 13a:   1f 90           pop     r1
 13c:   18 95           reti

根据数据表中的指令摘要,大多数指令是单周期的,但 PUSH 和 POP 是双周期的。你知道延迟是从哪里来的吗?