Программа обработки прерываний AVR не выполняется так быстро, как ожидалось (накладные расходы на инструкции?)

8

Я разрабатываю небольшой логический анализатор с 7 входами. Мое целевое устройство ATmega168с тактовой частотой 20 МГц. Для обнаружения логических изменений я использую прерывания смены контактов. Сейчас я пытаюсь определить минимальную частоту дискретизации, которую я могу обнаружить при смене контактов. Я определил значение минимум 5,6 мкс (178,5 кГц). Каждый сигнал ниже этой скорости я не могу уловить правильно.

Мой код написан на 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часов. Продолжительность одного тактового генератора должна соответствовать тактовой частоте 20 МГц 1/20000000 = 0.00000005 = 50 ns. Общие накладные расходы в секундах должны быть то: 15 * 50 ns = 750 ns = 0.75 µs. Теперь я не понимаю, почему я не могу сделать снимок ниже 5,6 мкс. Кто-нибудь может объяснить, что происходит?

arminb
источник
может быть 5 часов для отправки кода ISR, который включает в себя сохранение и восстановление контекста эпилога / пролога, которого вы не видите в источнике Си. Кроме того, что делает оборудование, когда прерывание отключается? Это в каком-то состоянии сна. (Я не знаю AVR, но в целом прерывание обработки некоторых состояний может занять больше времени.)
Kaz
@arminb См. также этот вопрос для получения дополнительных идей о том, как захватывать внешние события с большей точностью. Также [это приложение] (www.atmel.com/Images/doc2505.pdf) может представлять интерес.
angelatlarge

Ответы:

10

Есть пара вопросов:

  • Не всем командам 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

так, PUSHх 5, inх 1, clrх 1. Не так плохо , как jipple 32-бит не вары, но до сих пор не ничего.

Отчасти это необходимо (разверните обсуждение в комментариях). Очевидно, что поскольку процедура ISR может происходить в любое время, она должна сохранять предварительно используемые регистры, если только вы не знаете, что ни один код, в котором может произойти прерывание, не использует тот же регистр, что и ваша процедура прерывания. Например, следующая строка в разобранном ISR:

push r24

Это происходит потому, что все проходит r24: ваш pincзагружается до того, как он попадает в память и т. Д. Итак, вы должны иметь это в первую очередь. __SREG__загружается r0и затем толкается: если это могло пройти, r24то вы могли бы спасти себяPUSH


Некоторые возможные решения:

  • Используйте жесткий цикл опроса, как это было предложено Казом в комментариях. Вероятно, это будет самое быстрое решение, независимо от того, пишете ли вы цикл на C или на ассемблере.
  • Напишите свой ISR в сборке: таким образом, вы можете оптимизировать использование регистра таким образом, чтобы наименьшее количество их нужно было сохранить во время ISR.
  • Объявите свои процедуры ISR ISR_NAKED , хотя это, скорее, решение для красной сельди. Когда вы объявляете процедуры ISR ISR_NAKED, gcc не генерирует код пролога / эпилога, и вы несете ответственность за сохранение любых регистров, которые ваш код изменяет, а также за вызов reti(возврат из прерывания). К сожалению, нет никакого способа использования регистров в АРНЕ-GCC C непосредственно (очевидно , вы можете в сборе), однако, что вы можете сделать , это переменные связывания для конкретных регистров с register+ asmключевыми словами, как это: register uint8_t counter asm("r3");. Если вы сделаете это, для ISR вы будете знать, какие регистры вы используете в ISR. Проблема в том, что нет способа генерировать pushиpopсохранить использованные регистры без встроенной сборки (см. пункт 1). Чтобы обеспечить сохранение меньшего количества регистров, вы также можете привязать все переменные, не относящиеся к ISR, к конкретным регистрам, однако вы не столкнетесь с проблемой, что gcc использует регистры для перетаскивания данных в память и из памяти. Это означает, что если вы не посмотрите на разборку, вы не узнаете, какие регистры использует ваш основной код. Так что, если вы подумываете ISR_NAKED, вы можете написать ISR в сборке.
angelatlarge
источник
Спасибо, так что мой код C делает огромные накладные расходы? Будет ли это быстрее, если я напишу это на ассемблере? Во-вторых, я знал об этом.
Армин
@arminb: я не знаю достаточно, чтобы ответить на этот вопрос. Я предполагаю, что компилятор достаточно умен, и он делает то, что делает по какой-то причине. Сказав это, я уверен, что если вы потратили некоторое время на сборку, вы могли бы выжать еще несколько тактов из своей процедуры ISR.
angelatlarge
1
Я думаю, что если вам нужен самый быстрый ответ, вы обычно избегаете прерываний и опрашиваете выводы в тесной петле.
Каз
1
Имея в виду конкретные цели, можно оптимизировать код с помощью ассемблера. Например, компилятор начинает с помещения всех регистров, используемых в стек, а затем начинает выполнение самой процедуры. Если у вас есть важные для времени вещи, вы можете переместить часть толчка назад и вытянуть критические вещи вперед. Да, вы можете оптимизировать с помощью ассемблера, но сам по себе компилятор тоже довольно умен. Мне нравится использовать скомпилированный код в качестве отправной точки и изменять его вручную в соответствии с моими конкретными требованиями.
Джиппи
1
Действительно хороший ответ. Я добавлю, что компилятор добавляет все виды хранения и восстановления регистров для удовлетворения потребностей большинства пользователей. Можно написать свой собственный обработчик прерываний, если вам не нужны все эти накладные расходы. Некоторые компиляторы могут даже предложить вариант создания «быстрого» прерывания, оставляя большую часть «бухгалтерии» программисту. Я не обязательно пошел бы прямо к узкому кругу без ISR, если бы я не мог выполнить свой график. Сначала я подумал бы о более быстром uC, а потом я бы подумал, смогу ли я использовать какое-нибудь клейкое оборудование, такое как защелка и RTC.
Скотт Сейдман
2

Существует много регистров PUSH'а и POP'ов, которые собираются в стек до того, как ваш фактический ISR запускается, что превышает 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 - двухцикловыми. Вы поняли, откуда берется задержка?

jippie
источник
Спасибо за Ваш ответ! Теперь я в курсе того, что происходит. Особенно спасибо за команду avr-objdump -C -d $(src).elf!
Армин
Потратьте несколько минут, чтобы понять инструкции по сборке, которые avr-objdumpвыпадают, они кратко объяснены в таблице данных в Сводке инструкций. На мой взгляд, хорошей практикой является знакомство с мнемоникой, поскольку она может очень помочь при отладке кода на языке Си.
Джиппи
На самом деле, дизассемблирование полезно иметь в качестве части по умолчанию Makefile: поэтому, когда вы создаете свой проект, он также автоматически разбирается, поэтому вам не нужно думать об этом или вспоминать, как это сделать вручную.
angelatlarge