Я разрабатываю небольшой логический анализатор с 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 мкс. Кто-нибудь может объяснить, что происходит?
Ответы:
Есть пара вопросов:
AND
одночасовой инструкцииMUL
(умножение) занимает два такта , в то время какLPM
(загрузка памяти программы) равно трем, аCALL
равно 4. Таким образом, в отношении выполнения инструкции она действительно зависит от инструкции.RETI
инструкциям компилятор добавляет все виды другого кода, что также требует времени. Например, вам могут понадобиться локальные переменные, которые создаются в стеке и должны быть извлечены, и т. Д. Чтобы увидеть, что на самом деле происходит, лучше всего посмотреть на разборку.Если
x
это время, необходимое для обслуживания вашего прерывания, то сигнал B никогда не будет захвачен.Если мы возьмем ваш код ISR, вставим его в процедуру ISR (я использовал
ISR(PCINT0_vect)
), объявим все переменныеvolatile
и скомпилируем для ATmega168P, дизассемблированный код будет выглядеть следующим образом (см. Ответ @ jipple для получения дополнительной информации), прежде чем мы перейдем к коду что "делает что-то" ; другими словами, пролог к вашему ISR выглядит следующим образом:так,
PUSH
х 5,in
х 1,clr
х 1. Не так плохо , как jipple 32-бит не вары, но до сих пор не ничего.Отчасти это необходимо (разверните обсуждение в комментариях). Очевидно, что поскольку процедура ISR может происходить в любое время, она должна сохранять предварительно используемые регистры, если только вы не знаете, что ни один код, в котором может произойти прерывание, не использует тот же регистр, что и ваша процедура прерывания. Например, следующая строка в разобранном ISR:
Это происходит потому, что все проходит
r24
: вашpinc
загружается до того, как он попадает в память и т. Д. Итак, вы должны иметь это в первую очередь.__SREG__
загружаетсяr0
и затем толкается: если это могло пройти,r24
то вы могли бы спасти себяPUSH
Некоторые возможные решения:
ISR_NAKED
, gcc не генерирует код пролога / эпилога, и вы несете ответственность за сохранение любых регистров, которые ваш код изменяет, а также за вызовreti
(возврат из прерывания). К сожалению, нет никакого способа использования регистров в АРНЕ-GCC C непосредственно (очевидно , вы можете в сборе), однако, что вы можете сделать , это переменные связывания для конкретных регистров сregister
+asm
ключевыми словами, как это:register uint8_t counter asm("r3");
. Если вы сделаете это, для ISR вы будете знать, какие регистры вы используете в ISR. Проблема в том, что нет способа генерироватьpush
иpop
сохранить использованные регистры без встроенной сборки (см. пункт 1). Чтобы обеспечить сохранение меньшего количества регистров, вы также можете привязать все переменные, не относящиеся к ISR, к конкретным регистрам, однако вы не столкнетесь с проблемой, что gcc использует регистры для перетаскивания данных в память и из памяти. Это означает, что если вы не посмотрите на разборку, вы не узнаете, какие регистры использует ваш основной код. Так что, если вы подумываетеISR_NAKED
, вы можете написать ISR в сборке.источник
Существует много регистров PUSH'а и POP'ов, которые собираются в стек до того, как ваш фактический ISR запускается, что превышает 5 упомянутых вами тактовых циклов. Посмотрите на разборку сгенерированного кода.
В зависимости от того, какую цепочку инструментов вы используете, выгрузка сборки с перечислением нас производится разными способами. Я работаю в командной строке Linux, и это команда, которую я использую (для ввода требуется файл .elf):
Взгляните на фрагмент кода, который я недавно использовал для ATtiny. Вот как выглядит C-код:
И это сгенерированный код сборки для него:
Честно говоря, моя подпрограмма C использует еще пару переменных, которые вызывают все эти push-сообщения и pop-ы, но вы поняли идею.
Загрузка 32-битной переменной выглядит следующим образом:
Увеличение 32-битной переменной на 1 выглядит следующим образом:
Хранение 32-битной переменной выглядит следующим образом:
Затем, конечно, вы должны вытолкнуть старые значения после выхода из ISR:
Согласно сводке инструкций в техническом описании, большинство инструкций являются одноразовыми, а PUSH и POP - двухцикловыми. Вы поняли, откуда берется задержка?
источник
avr-objdump -C -d $(src).elf
!avr-objdump
выпадают, они кратко объяснены в таблице данных в Сводке инструкций. На мой взгляд, хорошей практикой является знакомство с мнемоникой, поскольку она может очень помочь при отладке кода на языке Си.Makefile
: поэтому, когда вы создаете свой проект, он также автоматически разбирается, поэтому вам не нужно думать об этом или вспоминать, как это сделать вручную.