AVR中断服务例程的执行速度未达到预期的速度(指令开销?)


8

我正在开发一个具有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的任何东西。谁能解释这是怎么回事?


可能需要5个时钟来调度ISR代码,其中包括上下文保存和恢复您在C源代码中看不到的Epilog /序言。另外,中断中断时硬件在做什么?它处于某种睡眠状态吗?(我不知道AVR,但总的来说,中断某些状态的处理可能需要更长的时间。)
Kaz 2013年

@arminb另请参阅此问题,以获取有关如何更精确地捕获外部事件的更多想法。另外[此应用笔记](www.atmel.com/Images/doc2505.pdf)可能也很有趣。
angelatlarge

Answers:


10

有几个问题:

  • 并非所有AVR命令都需要1个时钟来执行:如果您看一下数据手册的背面,它具有执行每个指令所需要的时钟数。因此,例如AND一条时钟指令,MUL(相乘)需要两个时钟,而LPM(加载程序存储器)是3,并且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,clrx1。不如jipple的32位var差,但仍然没有。

其中一些是必要的(扩展评论中的讨论)。显然,由于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。

谢谢,所以我的C代码造成了巨大的开销?如果我用汇编器编写它会更快吗?关于第二件事,我知道这一点。
arminb

@arminb:我不足够回答这个问题。我的假设是,编译器相当聪明,并且出于某种原因执行了它的工作。话虽如此,我相信如果您花一些时间进行汇编,则可以从ISR例程中挤出几个时钟周期。
angelatlarge 2013年

1
我认为,如果您想要最快的响应,通常会避免中断并在紧密的循环中轮询引脚。
卡兹(Kaz)

1
考虑到特定的目标,可以使用汇编器优化代码。例如,编译器首先将所有使用的寄存器压入堆栈,然后开始执行实际例程。如果您有时间紧迫的东西,您可以向后推一些,将时间紧迫的东西向前拉。所以可以,您可以使用汇编器进行优化,但是编译器本身也非常聪明。我喜欢将编译后的代码用作起点,并针对我的特定要求手动对其进行修改。
jippie 2013年

1
真是个好答案。我还要补充一点,编译器会添加各种寄存器存储和还原,以满足大多数用户的需求。如果不需要所有开销,则可以编写自己的准系统中断处理程序。一些编译器甚至可能提供创建“快速”中断的选项,从而将大部分“簿记”留给程序员。如果我不能按时完成计划,我并不一定会陷入没有ISR的紧密循环。首先,我考虑使用更快的uC,然后确定是否可以使用某种胶合硬件,例如闩锁和RTC。
Scott Seidman

2

在您实际的ISR开始之前,有很多PUSH'和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's和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为双周期。您知道延迟来自何处?


感谢您的回答!现在我知道发生了什么。特别感谢您的命令avr-objdump -C -d $(src).elf
arminb 2013年

请花一点时间来了解avr-objdump吐出的汇编指令,它们在数据表的“指令摘要”下得到了简要说明。在我看来,熟悉助记符是一种很好的做法,因为它在调试C代码时会很有帮助。
jippie 2013年

实际上,将反汇编作为默认值的一部分非常有用Makefile:因此,每当构建项目时,它也会自动反汇编,因此您不必考虑它,也不必记住如何手动进行。
angelatlarge 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.