AVR割り込みサービスルーチンが期待どおりに実行されない(命令オーバーヘッド?)


8

7つの入力を持つ小さなロジックアナライザーを開発しています。ターゲットデバイスはATmega16820MHzのクロックレートです。ロジックの変更を検出するには、ピン変更割り込みを使用します。今、私はこれらのピンの変化を検出できる最低のサンプルレートを見つけようとしています。私は、最小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クロックのオーバーヘッドがあるはずです。1クロックの持続時間は、20MHzのクロックレートに従う必要があります1/20000000 = 0.00000005 = 50 ns。秒単位の合計オーバーヘッドは、次のようになります15 * 50 ns = 750 ns = 0.75 µs。5.6 µs未満をキャプチャできない理由がわかりません。誰かが何が起こっているのか説明できますか?


多分5クロックでISRコードをディスパッチします。これには、Cソースにないコンテキストの保存と復元のエピローグ/プロローグが含まれます。また、割り込みが発生したときにハードウェアは何をしていますか?スリープ状態ですか?(AVRはわかりませんが、一般的に、特定の状態の処理を中断すると時間がかかる場合があります。)
Kaz

@arminb より正確に外部イベントをキャプチャする方法の詳細については、この質問も参照してください。また、[このアプリケーションノート](www.atmel.com/Images/doc2505.pdf)も興味深いかもしれません。
angelatlarge 2013

回答:


10

いくつかの問題があります:

  • すべてのAVRコマンドの実行に1クロックかかるわけではありません。データシートの裏側を見ると、各命令の実行にかかるクロック数が含まれています。したがって、たとえばAND、1クロック命令の場合、MUL(乗算)は2クロックかかりますが、LPM(ロードプログラムメモリ)は3であり、CALL4です。したがって、命令の実行に関しては、それは実際には命令に依存します。
  • ジャンプする5クロックと戻る5クロックは誤解を招く可能性があります。逆アセンブルされたコードを見ると、ジャンプとRETI命令に加えて、コンパイラーが他のあらゆる種類のコードを追加していることもわかりますが、これにも時間がかかります。たとえば、スタック上に作成され、ポップオフされる必要のあるローカル変数が必要になる場合があります。実際に何が行われているのかを確認するには、逆アセンブリを確認するのが最善の方法です。
  • 最後に、ISRルーチンの実行中は、割り込みがトリガーされないことに注意してください。これは、割り込みの処理にかかるよりも長い間隔で信号レベルが変化することがわかっていない限り、ロジックアナライザーから求める種類のパフォーマンスを得ることができないことを意味します。明確に言うと、ISRの実行にかかる時間を計算すると、1つの信号をキャプチャできる速度の上限がわかります。2つの信号をキャプチャする必要がある場合、問題が発生し始めます。これについて過度に詳しく説明するには、次のシナリオを検討してください。

ここに画像の説明を入力してください

xが割り込みの処理にかかる時間である場合、信号Bはキャプチャされません。


私たちはあなたのISRコードを取る場合は、ISRルーチンにそれを貼り付け(私が使用ISR(PCINT0_vect))ルーチン、すべての変数宣言しvolatile、次のように(詳細はjippleの答え@参照)、およびATmega168Pのコンパイル、逆アセンブルコードのルックスを私たちは、コードに到達する前にそれは「何かをする」 ; つまり、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。ジップルの32ビット変数ほど悪くはありませんが、それでも何も問題はありません。

これのいくつかは必要です(コメントの議論を拡大してください)。言うまでもなく、ISRルーチンはいつでも発生する可能性があるため、割り込みが発生する可能性のあるコードが割り込みルーチンと同じレジスタを使用していないことがわかっている場合を除き、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コードは大きなオーバーヘッドになりますか?アセンブラで書いた方が速くなりますか?2番目については、私はそれを知っていました。
arminb 2013

@arminb:その質問に答えるのに十分な知識がありません。私の仮定は、コンパイラはかなりスマートであり、それが理由で行うことを実行することです。とはいえ、アセンブリに時間を費やした場合は、ISRルーチンからさらに数クロックサイクルを絞ることができると確信しています。
angelatlarge 2013

1
最速の応答が必要な場合、通常は割り込みを回避し、ピンをタイトループでポーリングすると思います。
Kaz

1
特定の目標を念頭に置いて、アセンブラを使用してコードを最適化することが可能です。たとえば、コンパイラーは、使用されているすべてのレジスターをスタックにプッシュすることから始め、次に実際のルーチンの実行を開始します。タイミングが重要なものがある場合は、プッシュの一部を後退させ、タイムが重要なものを前に進めることができます。つまり、アセンブラを使用して最適化できますが、コンパイラ自体もかなりスマートです。コンパイルされたコードを開始点として使用し、特定の要件に合わせて手動で変更するのが好きです。
ジッピー2013

1
本当にいい答えです。ほとんどのユーザーのニーズに合わせて、コンパイラーがあらゆる種類のレジスターのストレージと再ストレージを追加することを付け加えます。独自の必要最低限​​の割り込みハンドラーを作成することは可能です-そのオーバーヘッドをすべて必要としない場合。一部のコンパイラは、「高速」割り込みを作成するオプションを提供して、「ブックキーピング」のほとんどをプログラマーに任せる場合もあります。スケジュールを満たせなかったとしても、ISRのないタイトなループに必ずしも進むとは限りません。最初に、より高速なuCを検討し、次に、ラッチやRTCなど、ある種の接着剤ハードウェアを使用できるかどうかを考えます。
Scott Seidman、2013

2

実際のISRが開始する前に、スタックする多数のPUSHレジスターとPOPレジスターがあります。これは、言及した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ルーチンはこれらのプッシュとポップのすべてを引き起こすいくつかの変数を使用していますが、あなたはそのアイデアを理解しています。

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コードをデバッグするときに役立つので、ニーモニックに慣れることをお勧めします。
ジッピー2013

実際、逆アセンブルはデフォルトの一部として持つと便利ですMakefile。したがって、プロジェクトをビルドするたびに、自動的に逆アセンブルされるため、考えたり、手動で行う方法を覚えたりする必要がありません。
angelatlarge 2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.