Прерывание ардуино при смене контакта

Я использую функцию прерывания для заполнения массива значениями, полученными от digitalRead().

void setup() {
    Serial.begin(115200);
    attachInterrupt(0, test_func, CHANGE);
}

void test_func() {
    if (digitalRead(pin) == HIGH) {
        test_array[x] = 1;
    } else if(digitalRead(pin) == LOW) {
        test_array[x] = 0;
    }
    x = x + 1;
}

Эта проблема заключается в том, что когда я печатаю test_array, появляются такие значения, как: 111 или 000.

Насколько я понимаю, если я использую параметр CHANGE в функции attachInterrupt(), то последовательность данных всегда должна быть 0101010101 без повторения.

Данные изменяются довольно быстро, так как они поступают с радиомодуля.

, 👍9

Обсуждение

Прерывания не устраняют дребезг кнопки. Используете ли вы аппаратное устранение дребезга?, @Ignacio Vazquez-Abrams

Пожалуйста, опубликуйте полный код, включая определения pin, x и test_array, а также метод loop(); это позволило бы нам увидеть, может ли это быть проблемой параллелизма при доступе к переменным, измененным test_func., @jfpoilpret

Вы не должны использовать digitalRead() дважды в ISR: подумайте о том, что произойдет, если вы получите LOW при первом вызове и HIGH при втором. Вместо этого if (digitalRead(pin) == HIGH) ... else ...; или, еще лучше, этот однострочный ISR: test_array[x++] = digitalRead(pin);., @Edgar Bonet

@EdgarBonet хорошо! +1 к этому комментарию. Надеюсь, вы не возражаете, что я добавил кое-что в свой ответ, чтобы включить то, что вы здесь упомянули. Кроме того, если вы решите опубликовать свой собственный ответ, включая эту деталь, я удалю свое дополнение и проголосую за ваше, чтобы вы получили за него репутацию., @Clayton Mills

@Clayton Mills: Я готовлю (слишком длинный и слегка тангенциальный) ответ, но вы можете оставить свою правку, меня это вполне устраивает., @Edgar Bonet

Почему вы делаете это в первую очередь? Единственное разумное ожидание состоит в том, что в массиве будет 1010101010. Таким образом, если это не так, это просто ошибка измерения. Как умело объяснил Эдгар Боне, такая ошибка может произойти с быстрыми последовательностями., @Nick Gammon


2 ответа


6

Прерывание настроено на срабатывание при изменении, и ваш test_func установлен как процедура обслуживания прерываний (ISR), вызываемая для обслуживания этого прерывания. Затем ISR печатает значение ввода.

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

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

По сути, без какой-либо формы устранения дребезга у нас нет гарантии, что при изменении ввода и прерывании будет помечено для обслуживания, что ввод останется с тем же значением, когда мы доберемся до считывания его значения в ISR.

В качестве общего примера: Техническое описание ATmega328, используемое в Arduino Uno, подробно описывает время прерывания в разделе 6.7.1 — «Время отклика на прерывание». В нем указано, что для этого микроконтроллера минимальное время перехода к ISR для обслуживания составляет 4 такта, но может быть и больше (дополнительно, если выполняется многотактовая инструкция при прерывании, или 8 + время пробуждения в спящем режиме, если MCU находится в спящем режиме).

Как упоминалось в комментариях @EdgarBonet, вывод также может измениться во время выполнения ISR. Поскольку ISR считывает с вывода дважды, он ничего не добавит к test_array, если при первом чтении встретится LOW, а при втором HIGH. Но x все равно будет увеличиваться, оставляя этот слот в массиве неизменным (возможно, как неинициализированные данные, в зависимости от того, что было сделано с массивом ранее).

Его однострочный ISR test_array[x++] = digitalRead(pin); — идеальное решение этой проблемы.

,

29

В качестве пролога к этому слишком длинному ответу...

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

Клейтон Миллс уже объяснил в своем ответе, что существует некоторая задержка в ответ на прерывания. Здесь я сосредоточусь на количественной оценке задержка (которая огромна при использовании библиотек Arduino), а на значит свести к минимуму. Большая часть того, что следует ниже, относится к оборудованию Arduino Uno и подобных плат.

Минимизация задержки прерывания на Arduino

(или как уменьшить количество циклов с 99 до 5)

Я буду использовать исходный вопрос в качестве рабочего примера и перефразирую Проблема с задержкой прерывания. У нас есть некоторое внешнее событие, которое вызывает прерывание (здесь: INT0 при смене контакта). Нам нужно взять немного действие при срабатывании прерывания (здесь: чтение цифрового входа). проблема: есть некоторая задержка между запуском прерывания и принять соответствующие меры. Мы называем эту задержку "прерыванием задержка». Длительная задержка вредна во многих ситуациях. В этом конкретном примере входной сигнал может измениться во время задержки, в в этом случае мы получаем ошибочное чтение. Мы ничего не можем сделать, чтобы избежать задержка: она присуща тому, как работают прерывания. Мы можем, однако, постарайтесь сделать его как можно короче, что, мы надеемся, сведет к минимуму плохие последствия.

Первая очевидная вещь, которую мы можем сделать, — это срочное действие. внутри обработчика прерывания, как можно скорее. Это означает вызов digitalRead() один раз (и только один раз) в самом начале обработчик. Вот нулевая версия программы, на которой мы будем сборка:

#define INT_NUMBER 0
#define PIN_NUMBER 2    // прерывание 0 на контакте 2
#define MAX_COUNT  200

volatile uint8_t count_edges;  // количество фронтов сигнала
volatile uint8_t count_high;   // количество высоких уровней

/* Interrupt handler. */
void read_pin()
{
    int pin_state = digitalRead(PIN_NUMBER);  // сделайте это первым!
    if (count_edges >= MAX_COUNT) return;     // мы сделали
    count_edges++;
    if (pin_state == HIGH) count_high++;
}

void setup()
{
    Serial.begin(9600);
    attachInterrupt(INT_NUMBER, read_pin, CHANGE);
}

void loop()
{
    /* Wait for the interrupt handler to count MAX_COUNT edges. */
    while (count_edges < MAX_COUNT) { /* wait */ }

    /* Report result. */
    Serial.print("Counted ");
    Serial.print(count_high);
    Serial.print(" HIGH levels for ");
    Serial.print(count_edges);
    Serial.println(" edges");

    /* Count again. */
    count_high = 0;
    count_edges = 0;  // делаем это в последнюю очередь, чтобы избежать состояния гонки
}

Я тестировал эту программу и последующие версии, отправляя поезда импульсов разной ширины. Достаточное расстояние между импульсами чтобы убедиться, что ни один фронт не пропущен: даже если получен задний фронт до того, как будет выполнено предыдущее прерывание, второй запрос на прерывание будет быть приостановлено и в конечном итоге обслуживаться. Если импульс короче задержки прерывания, программа считывает 0 на обоих фронтах. Сообщается число ВЫСОКИХ уровней — это процент правильно считанных импульсов.

Что происходит, когда срабатывает прерывание?

Прежде чем пытаться улучшить приведенный выше код, мы рассмотрим события, которые разворачиваются сразу после срабатывания прерывания. Аппаратное обеспечение часть истории рассказывается в документации Atmel. Программная часть, путем дизассемблирования двоичного файла.

В большинстве случаев входящее прерывание обслуживается сразу. Это может случается, однако, что MCU (что означает "микроконтроллер") находится в в середине какой-либо критической по времени задачи, где обслуживание прерывания инвалид. Обычно это происходит, когда он уже обслуживается. другое прерывание. Когда это происходит, входящий запрос на прерывание приостанавливать и обслуживать только тогда, когда этот критичный по времени участок выполнен. Этой ситуации трудно избежать полностью, потому что некоторые из этих критических разделов в основной библиотеке Arduino (которые я вызвать "libcore" В следующих). К счастью, эти разделы короткие и запускать только время от времени. Таким образом, большую часть времени наше прерывание запрос будет немедленно обработан. В дальнейшем я буду предполагать что нас не волнуют те несколько случаев, когда это не так.

Тогда наш запрос будет немедленно обработан. Это по-прежнему требует много вещи, которые могут занять довольно много времени. Во-первых, существует запрограммированная последовательность. MCU завершит выполнение текущей инструкции. К счастью, большинство инструкции выполняются за один цикл, но некоторые могут выполняться до четырех циклов. Затем MCU сбрасывает внутренний флаг, запрещающий дальнейшее обслуживание прерывает. Это предназначено для предотвращения вложенных прерываний. Затем ПК сохраняется в стек. Стек — это область оперативной памяти, зарезервированная для этого своего рода временное хранилище. ПК (что означает "Счетчик программ") является внутренний регистр, содержащий адрес следующей инструкции MCU. собирается выполнить. Это то, что позволяет MCU знать, что делать дальше, и сохранить его необходимо, потому что его придется восстанавливать, чтобы для возобновления основной программы с того места, где она была прервана. ПК затем загружается с аппаратным адресом, специфичным для полученного запроса, и это конец запрограммированной последовательности, остальное управляется программным обеспечением.

Теперь MCU выполняет инструкцию с этого аппаратного адреса. Этот инструкция называется "вектором прерывания" и обычно является "переходом" инструкция, которая приведет нас к специальной процедуре, называемой ISR ("Прерывание процедуры обслуживания"). В этом случае ISR называется "__vector_1", он же "INT0_vect", неправильное название, потому что ISR, а не вектор. Этот конкретный ISR исходит от libcore. Как и любой ISR, он начинается с пролога, который экономит кучу внутреннего процессора. регистрируется в стеке. Это позволит ему использовать эти регистры и, когда это будет сделано, восстановите их прежние значения, чтобы не нарушить основную программу. Затем он будет искать обработчик прерывания. который был зарегистрирован с помощью attachInterrupt(), и вызовет его обработчик, который является нашей функцией read_pin() выше. Наша функция будет затем вызовите digitalRead() из libcore. digitalRead() рассмотрит некоторые таблицы, чтобы сопоставить номер порта Arduino с аппаратным вводом-выводом порт, который он должен прочитать, и соответствующий номер бита для проверки. Это также будет проверьте, есть ли на этом выводе канал ШИМ, который должен быть инвалид. Затем он прочитает порт ввода-вывода... и все готово. Ну, мы на самом деле выполняется не обслуживание прерывания, а срочная задача (чтение порта ввода-вывода) сделано, и это все, что имеет значение, когда мы глядя на задержку.

Вот краткий обзор всего вышеперечисленного вместе с соответствующими задержки в циклах процессора:

  1. запрограммированная последовательность: завершить текущую инструкцию, предотвратить вложенные прерывания, сохранение ПК, загрузка адреса вектора (≥ 4 тактов)
  2. выполнить вектор прерывания: перейти к ISR (3 цикла)
  3. Пролог ISR: сохранение регистров (32 цикла)
  4. Основная часть ISR: найти и вызвать зарегистрированную пользователем функцию (13 циклов)
  5. read_pin: вызов digitalRead (5 циклов)
  6. digitalRead: найти соответствующий порт и бит для проверки (41 цикл)
  7. digitalRead: чтение порта ввода/вывода (1 цикл)

Мы будем исходить из наилучшего сценария с 4 циклами для жестко запрограммированная последовательность. Это дает нам общую задержку 99 циклов, или около 6,2 мкс с тактовой частотой 16 МГц. В дальнейшем я буду изучить некоторые приемы, которые можно использовать для уменьшения этой задержки. Они приходят примерно в порядке возрастания сложности, но все они нуждаются в том, чтобы мы как-нибудь поковыряться во внутренностях MCU.

Использовать прямой доступ к порту

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

Что ж, это было слишком просто, чтобы развлекаться, лучше я покажу вам, как это сделать. это трудный путь. Цель состоит в том, чтобы мы начали заниматься низкоуровневыми вещами. Этот метод называется "прямой доступ к порту" и хорошо задокументирован на ссылка на Arduino на странице Port Регистры. В этот момент, рекомендуется загрузить и просмотреть ATmega328P техническое описание. Этот 650-страничный документ на первый взгляд может показаться несколько пугающим. Это, тем не менее, хорошо организованы в разделы, характерные для каждого из MCU периферийные устройства и функции. И нам нужно только проверить разделы релевантно тому, что мы делаем. В данном случае это раздел с именем Порты ввода/вывода. Вот краткое изложение того, что мы узнали из этих чтений:

  • Контакт 2 Arduino на самом деле называется PD2 (т. е. порт D, бит 2) на плате. Чип AVR.
  • Мы получаем сразу весь порт D, читая специальный регистр MCU, который называется "ПИНД".
  • Затем мы проверяем бит номер 2, выполняя побитовое логическое и (символ C '&' оператор) с 1 << 2.

Итак, вот наш модифицированный обработчик прерываний:

#define PIN_REG    PIND  // прерывание 0 находится на выводе PD2 AVR
#define PIN_BIT    2

/* Interrupt handler. */
void read_pin()
{
    uint8_t sampled_pin = PIN_REG;            // сделайте это первым!
    if (count_edges >= MAX_COUNT) return;     // мы сделали
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

Теперь наш обработчик будет считывать регистр ввода-вывода сразу после его вызова. задержка составляет 53 такта процессора. Этот простой трюк сэкономил нам 46 циклов!

Напишите свой собственный ISR

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

Это не так сложно, как кажется. ISR могут быть написаны как обычно функции, нам просто нужно знать их конкретные имена и определить их с помощью специального макроса ISR() из avr-libc. В этот момент было бы полезно взглянуть на документацию avr-libc по прерывает, и в разделе описания под названием Внешние прерывания. Здесь краткое содержание:

  • Нам нужно немного записать в специальный аппаратный регистр под названием EICRA. (Внешний регистр управления прерываниями A), чтобы настроить прерывание, которое будет запускаться при любом изменении значения вывода. Это будет делается в setup().
  • Нам нужно немного записать в другой аппаратный регистр EIMSK. (Регистр маски внешнего прерывания), чтобы активировать INT0. прерывать. Это также будет сделано в setup().
  • Мы должны определить ISR с синтаксисом ISR(INT0_vect) { ... }.

Вот код для ISR и setup(), все остальное без изменений:

/* Interrupt service routine for INT0. */
ISR(INT0_vect)
{
    uint8_t sampled_pin = PIN_REG;            // сделайте это первым!
    if (count_edges >= MAX_COUNT) return;     // мы сделали
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

void setup()
{
    Serial.begin(9600);
    EICRA = 1 << ISC00;  // чувствуем любое изменение на выводе INT0
    EIMSK = 1 << INT0;   // разрешить прерывание INT0
}

К этому прилагается бесплатный бонус: поскольку этот ISR проще, чем тот, который заменяет, для выполнения своей работы требуется меньше регистров, чем пролог с сохранением регистров короче. Теперь мы снизили задержку до 20 циклы. Неплохо, учитывая, что мы начали с почти 100!

На этом я бы сказал, что мы закончили. Миссия выполнена. какая следует только для тех, кто не боится попасть в руки грязный с какой-то сборкой AVR. В противном случае вы можете прекратить чтение здесь, и спасибо, что зашли так далеко.

Написать голый ISR

Все еще здесь? Хороший! Для дальнейших действий было бы полезно иметь в по крайней мере какое-то базовое представление о том, как работает сборка, и взглянуть на Inline Assembler Поваренная книга из документации avr-libc. В этот момент наша запись прерывания последовательность выглядит так:

  1. фиксированная последовательность (4 цикла)
  2. вектор прерывания: переход к ISR (3 цикла)
  3. Пролог ISR: сохранение регистров (12 циклов)
  4. первое, что нужно сделать в теле ISR: прочитать порт ввода-вывода (1 цикл)

Если мы хотим добиться большего успеха, мы должны переместить чтение порта в пролог. Идея такова: чтение регистра PIND затереть один регистр ЦП, поэтому нам нужно сохранить хотя бы один регистр перед этим, но другие регистры могут подождать. Затем нам нужно написать собственный пролог, который считывает порт ввода-вывода сразу после сохранения первый регистр. Вы уже видели в прерывании avr-libc документацию (вы читали ее, верно?), что можно сделать ISR голый, и в этом случае компилятор не выдаст ни пролога, ни эпилога, что позволяет нам написать собственную версию.

Проблема с этим подходом заключается в том, что мы, вероятно, в конечном итоге напишем весь ISR в сборе. Ничего страшного, но я бы предпочел компилятор напишет за меня эти скучные прологи и эпилоги. Итак, вот подвох: разделим ISR на две части:

  • первая часть будет коротким фрагментом сборки,
    • сохранить один регистр в стеке
    • считывать PIND в этот регистр
    • сохранить это значение в глобальной переменной
    • восстановить регистр из стека
    • перейти ко второй части
  • вторая часть будет представлять собой обычный код C с компилятором пролог и эпилог

Затем наш предыдущий ISR INT0 заменяется этим:

volatile uint8_t sampled_pin;    // теперь это глобальная переменная

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    push r0                \n"  // сохранить регистр r0
    "    in r0, %[pin]          \n"  // прочитать PIND в r0
    "    sts sampled_pin, r0    \n"  // сохраняем r0 в глобальном
    "    pop r0                 \n"  // восстановить предыдущий r0
    "    rjmp INT0_vect_part_2  \n"  // переходим к части 2
    :: [pin] "I" (_SFR_IO_ADDR(PIND)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges >= MAX_COUNT) return;     // мы сделали
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

Здесь мы используем макрос ISR(), чтобы инструмент компилятора INT0_vect_part_2 с необходимыми прологом и эпилогом. Компилятор будет жаловаться, что "'INT0_vect_part_2' выглядит с ошибкой обработчик сигнала», но предупреждение можно смело игнорировать. Теперь у ISR есть одна 2-тактная инструкция перед фактическим чтением порта, и общее задержка составляет всего 10 циклов.

Использовать регистр GPIOR0

Что, если бы мы могли зарезервировать регистр для этой конкретной работы? Затем, нам не нужно ничего сохранять перед чтением порта. Мы можем на самом деле попросите компилятор связать глобальную переменную с зарегистрироваться. Это, однако, потребовало бы от нас перекомпиляции всего ядра Arduino и libc, чтобы убедиться, что регистр всегда зарезервирован. Не совсем удобный. С другой стороны, ATmega328P имеет три регистры, которые не используются ни компилятором, ни какой-либо библиотекой, и доступны для хранения всего, что мы хотим. Они называются GPIOR0, GPIOR1 и GPIOR2 (Регистры ввода-вывода общего назначения). Хотя они нанесены на карту в адресном пространстве ввода-вывода MCU, на самом деле это не ввод-вывод регистры: это просто память, как три байта оперативной памяти, которые каким-то образом заблудился в автобусе и оказался не в том адресном пространстве. Эти не так функциональны, как внутренние регистры ЦП, и мы не можем копировать PIND в один из них с помощью инструкции in. GPIOR0 интересно, тем не менее, он побитовый, как и PIND. Это позволит нам передавать информацию, не забивая внутренний процессор зарегистрироваться.

Вот в чем хитрость: мы убедимся, что GPIOR0 изначально равен нулю (это на самом деле очищается аппаратно во время загрузки), то мы будем использовать sbic (пропустить следующую инструкцию, если какой-либо бит в каком-либо регистре ввода-вывода очищен) и инструкции sbi (установить 1 бит в каком-то регистре ввода-вывода) как следует:

sbic PIND, 2   ; пропустить следующее, если бит 2 PIND сброшен
sbi GPIOR0, 0  ; установить в 1 бит 0 GPIOR0

Таким образом, GPIOR0 окажется равным 0 или 1 в зависимости от того, какой бит нам нужен. читать из PIND. Инструкция sbic выполняется за 1 или 2 такта. в зависимости от того, является ли условие ложным или истинным. Очевидно, что ПИНД бит доступен в первом цикле. В этой новой версии кода глобальная переменная sampled_pin больше не нужна, так как она в основном заменен на GPIOR0:

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    sbic %[pin], %[bit]    \n"
    "    sbi %[gpio], 0         \n"
    "    rjmp INT0_vect_part_2  \n"
    :: [pin]  "I" (_SFR_IO_ADDR(PIND)),
       [bit]  "I" (PIN_BIT),
       [gpio] "I" (_SFR_IO_ADDR(GPIOR0)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

Следует отметить, что GPIOR0 всегда должен сбрасываться в ISR.

Сэмплирование регистра ввода-вывода PIND — это самое первое, что нужно сделать. внутри ИСР. Общая задержка составляет 8 циклов. Это лучшее, что мы можем сделать, прежде чем запятнать себя ужасно греховными kludges. Это снова хорошая возможность перестать читать...

Поместить критичный ко времени код в таблицу векторов

Для тех, кто еще здесь, вот наша текущая ситуация:

  1. фиксированная последовательность (4 цикла)
  2. вектор прерывания: переход к ISR (3 цикла)
  3. Тело ISR: чтение порта ввода-вывода (в 1-м цикле)

Очевидно, что улучшений мало. Единственный способ, которым мы могли сократить задержку в этот момент можно, заменив вектор прерывания сам по нашему коду. Имейте в виду, что это должно быть очень неприятно всем, кто ценит чистый дизайн программного обеспечения. Но это возможно, и я покажет вам, как это сделать.

Схему векторной таблицы ATmega328P можно найти в техническом описании, раздел Прерывания, подраздел Векторы прерываний в ATmega328 и ATmega328P. Либо дизассемблируя любую программу для этого чипа. Вот как это выглядит как. Я использую соглашения avr-gcc и avr-libc (__init вектор 0, адреса в байтах), которые отличаются от Atmel.

address │ instruction     │ comment
────────┼─────────────────┼──────────────────────
 0x0000 │ jmp __init      │ reset vector 
 0x0004 │ jmp __vector_1  │ a.k.a. INT0_vect
 0x0008 │ jmp __vector_2  │ a.k.a. INT1_vect
 0x000c │ jmp __vector_3  │ a.k.a. PCINT0_vect
  ...
 0x0064 │ jmp __vector_25 │ a.k.a. SPM_READY_vect

Каждый вектор имеет 4-байтовый слот, заполненный одной инструкцией jmp. Это 32-битная инструкция, в отличие от большинства инструкций AVR, которые 16-бит. Но 32-битный слот слишком мал, чтобы вместить первую часть нашего ISR: мы можем использовать инструкции sbic и sbi, но не rjmp. Если мы это сделаем, таблица векторов будет выглядеть так:

address │ instruction     │ comment
────────┼─────────────────┼──────────────────────
 0x0000 │ jmp __init      │ reset vector 
 0x0004 │ sbic PIND, 2    │ the first part...
 0x0006 │ sbi GPIOR0, 0   │ ...of our ISR
 0x0008 │ jmp __vector_2  │ a.k.a. INT1_vect
 0x000c │ jmp __vector_3  │ a.k.a. PCINT0_vect
  ...
 0x0064 │ jmp __vector_25 │ a.k.a. SPM_READY_vect

При срабатывании INT0 будет прочитан PIND, соответствующий бит будет скопирован в GPIOR0, после чего выполнение перейдет к следующему вектору. Тогда будет вызываться ISR для INT1 вместо ISR для INT0. Этот жутко, но так как мы все равно не используем INT1, мы просто "захватим" его вектор для обслуживания INT0.

Теперь нам просто нужно написать собственную таблицу векторов, чтобы переопределить по умолчанию один. Оказывается, это не так просто. Таблица векторов по умолчанию предоставленный дистрибутивом avr-libc, в объектном файле с именем crtm328p.o, который автоматически связывается с любой программой, которую мы создаем. В отличие от кода библиотеки, код объектного файла не предназначен для переопределения: попытка сделать это даст ошибку компоновщика об определяемой таблице дважды. Это означает, что мы должны заменить весь crtm328p.o на наш пользовательская версия. Один из вариантов — загрузить полный исходный код avr-libc. код, сделайте наш пользовательские модификации в gcrt1.S, затем соберите это как пользовательскую libc.

Здесь я выбрал более легкий альтернативный подход. я написал обычай crt.S, который является упрощенной версией оригинала от avr-libc. Это не хватает нескольких редко используемых функций, таких как возможность определить "улов все" ISR, или иметь возможность завершить программу (т.е. заморозить Arduino), вызвав exit(). Вот код. Я обрезал повторяющиеся часть векторной таблицы, чтобы свести к минимуму прокрутку:

#include <avr/io.h>

.weak __heap_end
.set  __heap_end, 0

.macro vector name
    .weak \name
    .set \name, __vectors
    jmp \name
.endm

.section .vectors
__vectors:
    jmp __init
    sbic _SFR_IO_ADDR(PIND), 2   ; these 2 lines...
    sbi _SFR_IO_ADDR(GPIOR0), 0  ; ...replace vector_1
    vector __vector_2
    vector __vector_3
    [...and so forth until...]
    vector __vector_25

.section .init2
__init:
    clr r1
    out _SFR_IO_ADDR(SREG), r1
    ldi r28, lo8(RAMEND)
    ldi r29, hi8(RAMEND)
    out _SFR_IO_ADDR(SPL), r28
    out _SFR_IO_ADDR(SPH), r29

.section .init9
    jmp main

Его можно скомпилировать с помощью следующей командной строки:

avr-gcc -c -mmcu=atmega328p silly-crt.S

Скетч идентичен предыдущему за исключением того, что нет INT0_vect, а INT0_vect_part_2 заменяется на INT1_vect:

/* Interrupt service routine for INT1 hijacked to service INT0. */
ISR(INT1_vect)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

Чтобы скомпилировать скетч, нам нужна пользовательская команда компиляции. Если у вас есть следуя до сих пор, вы, вероятно, знаете, как компилировать из командной строки. Вы должны явно запросить связь silly-crt.o с вашей программой, и добавьте параметр -nostartfiles, чтобы избежать ссылки в исходном crtm328p.o.

Теперь чтение порта ввода/вывода является самой первой выполняемой инструкцией. после срабатывания прерывания. Я протестировал эту версию, отправив ее коротко импульсы от другого Ардуино, и он может поймать (хотя и не надежно) высокий уровень импульсов всего 5 циклов. Нет ничего больше, чем мы можно сделать, чтобы сократить задержку прерывания на этом оборудовании.

,

Хорошее объяснение! +1, @Nick Gammon

(Для будущего редактирования: 1) *"терять сон"* → *"[терять](https://en.wiktionary.org/wiki/lose#Verb) сон"* 2) *"Пропустить следующее"* → * "пропустить дальше"*. В остальном отличное письмо. Одновременно можно исправить (вводящий в заблуждение) отступ в коде вопроса.), @Peter Mortensen

@PeterMortensen: Спасибо за исправление! Я сохранил прописную букву «S» в слове «Пропустить дальше», чтобы показать, откуда взялась аббревиатура «sbic»., @Edgar Bonet

...Некоторые говорят, что он до сих пор оптимизирует этот вектор прерывания..., @user515655