Я использую функцию прерывания, чтобы заполнить массив значениями, полученными из 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
без повторения.
Данные изменяются довольно быстро, так как они поступают с радиомодуля.
arduino-uno
c
isr
user277820
источник
источник
pin
,x
иtest_array
определения, а такжеloop()
метод; это позволило бы нам увидеть, может ли это быть проблемой параллелизма при доступе к переменным, измененным с помощьюtest_func
.if (digitalRead(pin) == HIGH) ... else ...;
или, еще лучше, это однострочный ISR:test_array[x++] = digitalRead(pin);
.Ответы:
Как своего рода пролог к этому слишком длинному ответу ...
Этот вопрос глубоко увлек меня проблемой латентности прерывания, вплоть до потери сна в циклах подсчета вместо овец. Я пишу этот ответ больше для того, чтобы делиться своими выводами, чем просто для того, чтобы ответить на вопрос: большая часть этого материала может на самом деле не быть на уровне, подходящем для правильного ответа. Я надеюсь, что это будет полезно для читателей, которые приезжают сюда в поисках решений проблем с задержками. Ожидается, что первые несколько разделов будут полезны для широкой аудитории, включая оригинальный постер. Тогда это становится волосатым по пути.
Клейтон Миллс уже объяснил в своем ответе, что в ответах на прерывания есть некоторая задержка. Здесь я сосредоточусь на количественной оценке задержки (которая огромна при использовании библиотек Arduino) и на способах ее минимизации. Большая часть нижеследующего относится к аппаратному обеспечению Arduino Uno и аналогичных плат.
Минимизация задержки прерывания на Arduino
(или как пройти от 99 до 5 циклов)
Я буду использовать исходный вопрос в качестве рабочего примера и переформулирую проблему с точки зрения задержки прерывания. У нас есть какое-то внешнее событие, которое вызывает прерывание (здесь: INT0 при смене контакта). Нам нужно предпринять некоторые действия при срабатывании прерывания (здесь: прочитать цифровой вход). Проблема в том, что между срабатыванием прерывания и принятием соответствующих мер существует некоторая задержка. Мы называем эту задержку "задержкой прерывания ". Долгая задержка вредна во многих ситуациях. В этом конкретном примере входной сигнал может измениться во время задержки, и в этом случае мы получим неправильное чтение. Мы ничего не можем сделать, чтобы избежать задержки: это свойственно тому, как прерывания работают. Однако мы можем попытаться сделать его как можно более коротким, что, мы надеемся, должно минимизировать негативные последствия.
Первая очевидная вещь, которую мы можем сделать, - это как можно скорее предпринять срочные действия внутри обработчика прерываний. Это означает вызов
digitalRead()
один раз (и только один раз) в самом начале обработчика. Вот нулевая версия программы, на которой мы будем строить:Я протестировал эту программу и последующие версии, посылая ей последовательности импульсов различной ширины. Между импульсами имеется достаточное расстояние, чтобы гарантировать отсутствие пропуска фронта: даже если задний фронт получен до того, как будет выполнено предыдущее прерывание, второй запрос прерывания будет приостановлен и в конечном итоге обслужен. Если импульс короче, чем задержка прерывания, программа считывает 0 на обоих фронтах. Сообщаемое количество HIGH-уровней - это процент правильно прочитанных импульсов.
Что происходит, когда прерывание срабатывает?
Прежде чем пытаться улучшить приведенный выше код, рассмотрим события, которые разворачиваются сразу после срабатывания прерывания. Аппаратная часть истории рассказана в документации 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 с портом аппаратного ввода-вывода, который он должен прочитать, и соответствующий битовый номер для проверки. Он также проверит, есть ли на этом выводе канал ШИМ, который необходимо отключить. Затем он прочитает порт ввода-вывода ... и все готово. Ну, на самом деле мы еще не закончили обслуживание прерывания, но критическая по времени задача (чтение порта ввода / вывода) выполнена, и это все, что имеет значение, когда мы смотрим на задержку.Вот краткое резюме всего вышеперечисленного, вместе с соответствующими задержками в циклах ЦП:
Мы примем сценарий наилучшего случая, с 4 циклами для аппаратной последовательности. Это дает нам общую задержку 99 циклов или около 6,2 мкс с тактовой частотой 16 МГц. Далее я расскажу о некоторых приемах, которые можно использовать для снижения этой задержки. Они приходят примерно в порядке возрастания сложности, но все они нуждаются в том, чтобы мы каким-то образом копались во внутреннем пространстве MCU.
Используйте прямой доступ к порту
Очевидная первая цель для сокращения времени ожидания
digitalRead()
. Эта функция обеспечивает хорошую абстракцию аппаратного обеспечения MCU, но она слишком неэффективна для работы, требующей срочного выполнения. Избавиться от этого на самом деле тривиально: нам просто нужно заменить егоdigitalReadFast()
из библиотеки digitalwritefast . Это сокращает время ожидания почти вдвое за счет небольшой загрузки!Ну, это было слишком легко, чтобы быть веселым, я скорее покажу вам, как сделать это трудным путем. Цель состоит в том, чтобы заставить нас заняться вещами низкого уровня. Этот метод называется « прямой доступ к порту » и хорошо документирован в справочнике Arduino на странице « Регистры портов» . На данный момент, это хорошая идея, чтобы загрузить и взглянуть на таблицу данных ATmega328P . Этот 650-страничный документ может показаться несколько пугающим на первый взгляд. Однако он хорошо организован в разделы, специфичные для каждого из периферийных устройств и функций MCU. И нам нужно только проверить разделы, относящиеся к тому, что мы делаем. В данном случае это раздел с именем I / O ports . Вот краткое изложение того, что мы узнаем из этих чтений:
1 << 2
.Итак, вот наш модифицированный обработчик прерываний:
Теперь наш обработчик будет читать регистр ввода-вывода, как только он будет вызван. Задержка составляет 53 такта процессора. Этот простой трюк спас нам 46 циклов!
Напишите свой собственный ISR
Следующей целью для циклической обрезки является INT0_vect ISR. Этот ISR необходим для обеспечения функциональности
attachInterrupt()
: мы можем изменить обработчики прерываний в любое время во время выполнения программы. Однако, хотя это приятно иметь, это не очень полезно для наших целей. Таким образом, вместо того, чтобы ISR в libcore находил и вызывал наш обработчик прерываний, мы сэкономим несколько циклов, заменив ISR нашим обработчиком.Это не так сложно, как кажется. ISR могут быть написаны как обычные функции, мы просто должны знать их конкретные имена и определять их, используя специальный
ISR()
макрос из avr-libc. На этом этапе было бы хорошо взглянуть на документацию avr-libc по прерываниям , а также на раздел с описанием внешних прерываний . Вот краткое резюме:setup()
.setup()
.ISR(INT0_vect) { ... }
.Вот код для ISR, и
setup()
все остальное без изменений:Это дает бесплатный бонус: поскольку этот ISR проще, чем тот, который он заменяет, ему нужно меньше регистров для выполнения своей работы, тогда пролог, сохраняющий регистры, короче. Теперь мы сократились до 20 циклов. Неплохо, учитывая, что мы начали около 100!
На данный момент я бы сказал, что мы сделали. Миссия выполнена. Далее следует только для тех, кто не боится испачкать руки при помощи сборки AVR. В противном случае вы можете перестать читать здесь, и спасибо за то, что так далеко.
Написать голый ISR
Все еще здесь? Хорошо! Для продолжения работы было бы полезно иметь хотя бы некоторую базовую идею о том, как работает сборка, и взглянуть на книгу рецептов Inline Assembler из документации avr-libc. На этом этапе наша последовательность ввода прерываний выглядит следующим образом:
Если мы хотим добиться большего, мы должны перенести показания порта в пролог. Идея заключается в следующем: чтение регистра PIND приведет к засорению одного из регистров ЦП, поэтому перед этим нужно сохранить хотя бы один регистр, но другие регистры могут подождать. Затем нам нужно написать собственный пролог, который считывает порт ввода-вывода сразу после сохранения первого регистра. Вы уже видели в документации по прерываниям avr-libc (вы ее прочитали, верно?), Что ISR можно сделать голым , и в этом случае компилятор не будет выпускать пролог или эпилог, что позволит нам написать нашу собственную версию.
Проблема с этим подходом состоит в том, что мы, вероятно, в конечном итоге напишем весь ISR в сборке. Ничего страшного, но я бы предпочел, чтобы компилятор написал эти скучные прологи и эпилоги для меня. Итак, вот подвох: мы разделим ISR на две части:
Наш предыдущий INT0 ISR затем заменяется этим:
Здесь мы используем макрос 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
(Пропустить следующую инструкцию, если какой-то бит в каком-либо регистре ввода-вывода равен Clear) иsbi
( Установите 1 бит в некоторых регистрах ввода / вывода) следующим образом:Таким образом, GPIOR0 будет равен 0 или 1 в зависимости от того, какой бит мы хотим прочитать из PIND. Инструкция sbic выполняется 1 или 2 цикла в зависимости от того, является ли условие ложным или истинным. Очевидно, что бит PIND доступен в первом цикле. В этой новой версии кода глобальная переменная
sampled_pin
больше не используется, так как она в основном заменена на GPIOR0:Следует отметить, что GPIOR0 должен всегда сбрасываться в ISR.
Теперь выборка из регистра ввода / вывода PIND - это первое, что делается внутри ISR. Общая задержка составляет 8 циклов. Это лучшее из того, что мы можем сделать, прежде чем запятнать ужасно грешные клуджи. Это снова хорошая возможность перестать читать ...
Поместите критичный по времени код в таблицу векторов
Для тех, кто еще здесь, вот наша текущая ситуация:
Там явно мало места для улучшения. Единственный способ сократить задержку на этом этапе - заменить сам вектор прерывания нашим кодом. Имейте в виду, что это должно быть чрезвычайно неприятным для всех, кто ценит чистый дизайн программного обеспечения. Но это возможно, и я покажу вам, как.
Расположение векторной таблицы ATmega328P можно найти в таблице данных, раздел Прерывания , подразделы Векторы прерываний в ATmega328 и ATmega328P . Или путем разборки любой программы для этого чипа. Вот как это выглядит. Я использую соглашения avr-gcc и avr-libc (__init это вектор 0, адреса в байтах), которые отличаются от Atmel.
Каждый вектор имеет 4-байтовый слот, заполненный одной
jmp
инструкцией. Это 32-битная инструкция, в отличие от большинства инструкций AVR, которые являются 16-битными. Но 32-битный слот слишком мал , чтобы провести первую часть нашего ISR: мы подправитьsbic
иsbi
инструкцию, но неrjmp
. Если мы сделаем это, таблица векторов будет выглядеть так:Когда срабатывает 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()
. Вот код Я обрезал повторяющуюся часть таблицы векторов, чтобы минимизировать прокрутку:Его можно скомпилировать с помощью следующей командной строки:
Эскиз идентичен предыдущему за исключением того, что нет INT0_vect, а INT0_vect_part_2 заменяется на INT1_vect:
Чтобы скомпилировать эскиз, нам нужна команда для компиляции. Если вы до сих пор следовали, вы, вероятно, знаете, как компилировать из командной строки. Вы должны явно запросить, чтобы silly-crt.o был связан с вашей программой, и добавьте
-nostartfiles
опцию, чтобы избежать ссылок в исходном crtm328p.o.Теперь чтение порта ввода / вывода - это самая первая инструкция, выполняемая после запуска прерывания. Я протестировал эту версию, посылая ей короткие импульсы от другого Arduino, и он может улавливать (хотя и не надежно) высокий уровень импульсов всего за 5 циклов. Мы больше ничего не можем сделать, чтобы сократить задержку прерывания на этом оборудовании.
источник
Прерывание устанавливается на срабатывание при изменении, а ваш 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);
является идеальным решением для этого.источник