26 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Скользкая дорожка для поклонников volatile

Содержание

Скользкая дорожка для поклонников volatile

Прошло уже практически 30 лет с тех пор, как в стандарте языка C появился модификатор volatile, но в наши дни это ключевое слово вызывает лишь больше вопросов и непонимания даже среди программистов, общающихся с железом на “ты”. Сейчас уже никого не удивишь многоядерным мобильным телефоном или компьютером, умещающимся в одном чипе. Прогресс не стоит на месте, компиляторы умнеют, задачи программистов усложняются, вынуждая помнить о барьерах компиляции и барьерах памяти работая на многопроцессорных системах, только volatile по-прежнему остается темным уголком стандарта, в котором лишь сказано, что доступ к такой изменчивой переменной “implementation-defined” (Стандарт C, 6.7.3/7), т.е. как решат ребята, разрабатывающие компилятор, так и будет.

Disclaimer

В данной статье я буду говорить об использовании volatile в языке С в контексте определения переменных, не касаясь ассемблерных вставок, помеченных как volatile. Так же я буду приводить примеры кода, генерируемого компилятором gcc для архитектуры x86-64, но все сказанное в полной мере относится вообще к любым современным компиляторам и архитектурам. А если ваш компилятор генерирует другой код, который работает, то это вовсе не значит, что со следующей версией поведение вашей программы, использующей volatile, не поменяется. Да и код, скорее всего, будет непереносим на другие платформы.

Зачем нужен volatile?

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

а вот ассемблерный код, генерируемый gcc 4.7.1 с опцией -O3:

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

Ключевое слово volatile и является одним из звеньев, связывающих абстрактную машину и реальную. В USENET много постов (1, 2, 3) 20-летней давности, проливающих свет на темную историю появления этого модификатора. Приведу хороший перевод одного из писем, взятый здесь:

На уровне железа многие процессоры просто резервируют блок адресов памяти для портов ввода-вывода. Большинство процессоров имеют отдельное пространство адресов ввода-вывода, со специальными инструкциями для доступа к ним, но это не универсально (на PDP-11 такого не было, например) и даже сейчас, производители железа могут предпочесть использовать для этого адресное пространство памяти, по разным причинам. Я сомневаюсь, что кто-то так делает на архитектуре 8086 — различные адресные ограничения делают это очень сложным. Я видел это на 8080, это очень частое решение на старой TI 9900. И это был единственный способ организовать ввод-вывод на наборе инструкций PDP-11, там просто не было отдельного адресного пространства ввода-вывода (Я думаю, то же самое верно и для VAX. И не забывайте, что большинство работы на С раньше проходило именно на этих двух процессорах).

Теперь рассмотрим один из первых последовательных портов, что я использовал: Intel 8051. Нормальным способом его инициализации было записать 0 три раза в порт управления. Если у вас MMIO, то код на С мог бы выглядеть примерно так:

Что прекрасно работало на всех компиляторах С. Но вы можете себе представить, что могла бы с этим сделать сама простая оптимизация. По этой причине и нужно было ключевое слово volatile, чтобы сказать компилятору не оптимизировать.

Таким образом, volatile обязывает компилятор каждый раз обращаться к памяти, избегая возможности кеширования значения в регистрах. Для разработчиков UNIX в 80-х этот модификатор был единственным способом борьбы с компилятором, который норовил соптимизировать и выкинуть действительно нужный код.

Распространенные заблуждения при использовании volatile

1. Обращение к volatile переменной атомарны

Рассмотрим такой код:

что создает компилятор:

Единственная инструкция addl $1, j(%rip) для переменной j выглядит куда более «атомарнее», чем три инструкции для volatile переменной i. Выводы:

  • volatile не имеет ничего общего с атомарностью
  • компилятор создает больше инструкций при обращении к volatile переменной, что плохо сказывается на эффективности кода
  • компилятор вправе нарушить порядок инструкций. Действительно, сначала вычитывается значение i в регистр, происходит инкремент j, после чего делается инкремент старого значения i из регистра

Как бороться? Для низкоуровневых приложений: использовать атомарные инструкции или отключать прерывания на однопроцессорных системах, для программ, работающих в пространстве пользователя, использовать mutex блокировки.

2. volatile помогает создать lockless код

Это наиболее распространненное и опасное заблуждение, которое кочует из исходника в исходник. Рассмотрим код:

Программист уверен, что такое простое решение, основанное на volatile флаге, позволит ему избежать «ненужного» и «долгого» lock’а (под lock’ом я подразумеваю вызов mutex, если речь идет о userspace приложении, или запрет прерывания, если речь идет о низкоуровневом коде). А вот что создает компилятор:

Ожидания программиста не оправдались, вместо мифической экономии на lock’ах получилась редкая проблема, воспроизведение которой зависит от:

  • случая
  • агрессивности оптимизатора
  • архитектуры, для которой создается приложения

С точки зрения стандарта C данное поведение компилятора абсолютно нормально, так как он вправе переставлять volatile и не volatile конструкции. Более того, на современных архитектурах сам процессор может нарушить порядок выполнения инструкций, выполнив ready = i до инициализации буфера, но тема слабой модели упорядочивания доступа к памяти не для данной статьи.

Как бороться? Использовать барьер компилятора. Это вот такая вот ассемблерная вставка, которая поддерживается большинством современных компиляторов (а если в вашем компиляторе такой инструкции нет, то значит есть какая-то другая):

asm volatile («» . «memory»);

которая является барьером, говорящая компилятору «сбрось все регистры в RAM до барьера и перечитай после».

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

Кстати говоря, барьером компилятора являются вызовы функций, if/for/while конструкции, return, etc. Для детального погружения в тему смотреть Стандарт C, Annex C Sequence points, 438 p.

3. volatile нужно использовать всегда, если переменная может измениться из нескольких контекстов выполнения

Часто можно встретить код, в котором все переменные, изменяемые в контексте прерывания или другом потоке, объявлены как volatile. Даже встречаются объявления целых структур:

Наверное, программист надеется, что при обращении к членам данной структуры, компилятор сделает всю работу, подумав об атомарности, «правильном» порядке доступа к переменным и необходимых lock’ах. Ничего подобного не будет. На выходе будет неэффективный код, который ничего «правильного» делать не будет, так как в стандарте C ничего не сказано о том, что вообще должен делать компилятор при доступе к volatile переменной. Если код работает так, как ожидается, то это случайность. Более того, компиляторы тоже содержат баги, которые проявляются при обращении к volatile переменным, а как результат — создается неверный ассемблерный код.

Как правильно использовать volatile?

1. в контексте «asm volatile»

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

2. приведение к volatile указателю там, где нужно

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

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

В остальных случаях вы получаете лишь баги с редким и сложным воспроизведением.

Эпилог

Я не зря начал статью со вступления, что прогресс не стоит на месте. Сегодня ваш код с volatile работает, а завтра вы ставите новую версию компилятора, запускаете агрессивную оптимизацию или пытаетесь портировать код на многопроцессорную новомодную ARM систему, а в итоге получаете массу проблем из-за неверного использование этого модификатора или недопонимания всех тонкостей современной архитектуры.

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Ну, цели вы не достигли. Применение ардуино тут вообще ничего не даёт. С тем же успехом вы могли просто положить его рядом на столе.

Эх, был бы шарп и мк в мои 17 лет, какую бы фигню только несотворил.

Картинка с троллейбусом уже была?

Не очень понятно, зачем [MTAThread] ? А использование Thread.Sleep это же лютый code smell, нет?

Обработчик прерываний — это низкоуровневый эквивалент обработчика событий.

Рядом не лежало.

Событие обрабатывается «когда руки дойдут» (если у вас не RTOS, конечно, но там тоже не так все просто)

Прерывание обрабатывается как только оно возникает. Писать обработчик прерываний тоже некоторый опыт нужен — из нынешнего поколения фреймворков у кого он есть? Кто знает как работать с прерыванием RTC? Ась?

В общем и целом, кабы я был экзаменатором, а вы студентом — послал бы вас учить WinAPI с написанием учебнойпрограммы на голом API без никаких библиотек и фреймворков. Просто для понимания как оно работает внутри.

Справедливости ради, в статье расписан обработчик событий, а не обработчик прерываний.

Прерывание — это процесс БЕЗУСЛОВНО переключающий ваш МК на другую задачу, и возобновляющий прерванную программу с того места где она прервалась.

Вот есть у вас Thread.Sleep(10); или delay() не суть важно. Увеличьте их значения до заоблачных, и попробуйте что вышло. Ваша программа ожидаемо застопорится на этой функции и больше не станет обрабатывать никакие нажатия или данные на последовательном порту. В случае с прерыванием было бы пофигу.

Как только эта программа стартует — она сразу же «зависает», исполняя delay(100500) и ей пофигу на любые данные на последовательном порту, rxtx и тд. Но стоит пину 2 поменять свое состояние — delay(100500) прервется, светодиод моргнет, и подпрограмма возвратится досиживать delay на оставшееся время.

Стоит отметить, что в WindowsForms присутствует элемент Timer который включается и выполняет код через определенный промежуток времени.

Таймер — это элемент WinAPI. Он всего лишь посылает сообщение WM_TIMER в очередь сообщений приложения через заданные промежутки времени. А выполнение кода обеспечивается диспетчером сообщений приложения в том случае, когда на это сообщение назначен соответствующий обработчик.

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

Так работает Windows и это надо понимать. Если у вас есть длинные циклы, то лучше бы внутри них периодически вызывать диспетчер сообщений (что-то типа метода ProcessMessages() или аналогичного)

Равно как и в коде потока периодически вызывать функцию, отдающую таймслайсы системе (Sleep, WaitObject и т.п.)

А настоящие аппаратные прерывания в Windows пользователю недоступны (разве что на уровне драйверов, но это отдельная тема).

Так что тут уже правильно сказали — ваша программа будет генерировать события, не более. А уж когда это событие будет обработано…

Прерывания в языке Arduino

Теперь давайте перейдем к практике и поговорим о том, как использовать прерывания в своих проектах.

Функция attachInterrupt используется для работы с прерываниями. Она служит для соединения внешнего прерывания с обработчиком.

Синтаксис вызова: attachInterrupt(interrupt, function, mode)

  • interrupt – номер вызываемого прерывания (стандартно 0 – для 2-го пина, для платы Ардуино Уно 1 – для 3-го пина),
  • function – название вызываемой функции при прерывании(важно – функция не должна ни принимать, ни возвращать какие-либо значения),
  • mode – условие срабатывания прерывания.

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

  • LOW – выполняется по низкому уровню сигнала, когда на контакте нулевое значение. Прерывание может циклично повторяться – например, при нажатой кнопке.
  • CHANGE – по фронту, прерывание происходит при изменении сигнала с высокого на низкий или наоборот. Выполняется один раз при любой смене сигнала.
  • RISING – выполнение прерывания один раз при изменении сигнала от LOW к HIGH.
  • FALLING – выполнение прерывания один раз при изменении сигнала от HIGH к LOW.4

Важные замечания

При работе с прерываниями нужно обязательно учитывать следующие важные ограничения:

  • Функция – обработчик не должна выполняться слишком долго. Все дело в том, что Ардуино не может обрабатывать несколько прерываний одновременно. Пока выполняется ваша функция-обработчик, все остальные прерывания останутся без внимания и вы можете пропустить важные события. Если надо делать что-то большое – просто передавайте обработку событий в основном цикле loop(). В обработчике вы можете лишь устанавливать флаг события, а в loop – проверять флаг и обрабатывать его.
  • Нужно быть очень аккуратными с переменными. Интеллектуальный компилятор C++ может “пере оптимизировать” вашу программу – убрать не нужные, на его взгляд, переменные. Компилятор просто не увидит, что вы устанавливаете какие-то переменные в одной части, а используете – в другой. Для устранения такой вероятности в случае с базовыми типами данных можно использовать ключевое слово volatile, например так: volatile boolean state = 0. Но этот метод не сработает со сложными структурами данных. Так что надо быть всегда на чеку.
  • Не рекомендуется использовать большое количество прерываний (старайтесь не использовать более 6-8). Большое количество разнообразных событий требует серьезного усложнения кода, а, значит, ведет к ошибкам. К тому же надо понимать, что ни о какой временной точности исполнения в системах с большим количеством прерываний речи быть не может – вы никогда точно не поймете, каков промежуток между вызовами важных для вас команд.
  • В обработчиках категорически нельзя использовать delay(). Механизм определения интервала задержки использует таймеры, а они тоже работают на прерываниях, которые заблокирует ваш обработчик. В итоге все будут ждать всех и программа зависнет. По этой же причине нельзя использовать протоколы связи, основанные на прерываниях (например, i2c).
Читать еще:  Кнопка Сменить пользователя не работает

Прерывания и многозадачность в Arduino

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

Это могут быть прерывания по таймеру или внешние прерывания, уведомляющие о событиях, воздействующих на систему извне.

Прерывания и их источники

В процессе работы управляющий процессор выполняет определенные операции, а прерывание вызывает их остановку и в соответствие с кодом заставляет выполнить операции с более высоким приоритетом.

Проще говоря, прерывания – это набор приоритетов для тех или иных процессов, исполняемых контроллером.

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

Источником сигнала прерывания может стать таймер Arduino или процесс изменения состояния одного из контактов (пинов). Еще одним источником может стать какой-либо вход внешних прерываний: нужный сигнал появится при изменении его состояния.

Прерывание можно заставить работать с помощью кода, который поможет отвечать на прерывание. При этом нет необходимости писать код в loop (), который используют для систематической проверки приоритета прерывания.

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

Особенности прерывания по таймеру

Как же управлять временем и запускать процессы в нужном вам порядке? При работе с микросхемой Arduino для этого можно использовать millis(), ее эффективность зависит от постоянного обращения к ней. Тогда, в случае вызова этой функции, можно будет понять – наступило время определенной операции или нет.

Добиться эффективности можно при вызове millis() несколько раз в миллисекунду, но это довольно расточительно. Для вызова функции раз в миллисекунду (что является оптимальным вариантом) необходимо использовать таймер. Его можно установить на интервал в миллисекунду и добиться желаемого результата.

Микроконтроллер Arduino Uno укомплектован тремя таймерами: из них timer0 предназначен для генерации прерываний с интервалом в одну миллисекунду. При этом будет постоянно обновляться счетчик, передающий информацию функции millis(). Вести точный подсчет таймеру позволяет определенная частота, получаемая из 16 МГц процессора.

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

Функция timer0 оперирует тактовым делителем на 64 и изменять это значение не стоит. Оно позволяет получить частоту прерывания, близкую к 1 кГц, оптимальную для целей большинства схемотехников. Если попытаться изменить данный параметр, то можно нарушить работу функции millis().

Регистры сравнения и их роль в генерации прерывания

Регистры сравнения выполняют функцию анализа хранимых данных с текущим состоянием счетчика прерывания. Например, регистр сравнения (OCR0A) эффективно применяется для прерывания в середине счета.

Программный код, пример которого приведен ниже, позволит генерировать функцию TIMER0_COMPA при прохождении счетчика xAF:

Для оптимизации работы кода и микроконтроллера следует полностью отказаться от loop(). Для этого необходимо определение по вектору обработчика прерывания с помощью функции TIMER0_COMPA_vect. Благодаря ей обработчик и будет выполнять все те операции, что раньше делались в loop().

Данный код позволит вернуться к использованию функции delay(), благодаря чему все классические мерцающие светодиоды или работающие сервоприводы будут функционировать без проблем, но при этом мы получим удобный и практичный таймер.

Какие внешние воздействия вызывают прерывание

Вызвать внешние прерывания могут определенные действия из внешней среды. Это может быть простым нажатием на кнопку или срабатыванием используемого датчика. При этом нет необходимости вести постоянный опрос вывода GPIO о происходящих изменениях.

Микроконтроллер Arduino может иметь несколько пинов, способных обрабатывать внешние прерывания. Так, на плате Arduino Uno их два, а на Arduino Mega 2560 – 6. Продемонстрировать их функционал можно с помощью кнопки сброса сервопривода. Для этого при написании кода в класс Sweeper необходимо добавит функцию reset(). Она способна установить нулевое положение и перетаскивать в нее сервопривод.

Соединить обработчик с внешним прерыванием поможет другая функция – attachInterrupt(). На приведенных в качестве примеров микроконтроллерах Interrupt0 реализована на втором контакте. Она сообщает микроконтроллеру о том, что на данном входе ожидается спад сигнала.

Достаточно нажать кнопку и сигнал действительно падает до минимального уровня, вызывая обработчик Reset.

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

Полный код программы с таймерами и внешними прерываниями:

Библиотеки прерываний

У схемотехников, благодаря Всемирной паутине есть доступ к широкому кругу библиотек, которые существенно облегчат работу с таймерами. Большинство из них предназначены для тех, кто применяет функцию millis(), но есть и такие, которые позволяют произвести желаемую настройку таймеров и сгенерировать прерывания. Оптимальным вариантом для этого являются библиотеки TimerThree и TimerOne, разработанные Paul Stoffregan.

Благодаря им, схемотехники получают широкий выбор возможностей, с помощью которых можно сконфигурировать прерывания с помощью таймера. Первая из этих библиотек не работает с Adruino Uno, но прекрасно зарекомендовала себя с платами Teensy, микроконтроллерами Adruino Mega2560 и Adruino Leonardo.

Недостатком Adruino Uno является наличие всего двух ходов, предназначенных для работы с внешними прерываниями. Если требуется большее количество подобных пинов, то отчаиваться не стоит, ведь этот микроконтроллер поддерживает pin-change – прерывания по изменению входа и работает это на всех восьми входах.

Их отличие от простых внешних прерываний – в сложности обработки, так как схемотехнику требуется отслеживать последнее из известных состояний пинов. Только в этом случае можно будет понять, какой из них вызвал прерывание.

Наиболее информативное и практичной библиотекой для прерываний по изменению входа является PinChangeInt.

Прерывания: основные правила работы

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

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

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

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

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

Если вызов функции Update() все же необходим, то предварительно необходимо будет проверить переменную состояния. Это позволит выяснить, необходима ли последующая обработка.

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

Какими функциями оперирует тот или иной таймер?

Для микроконтроллера Arduino Uno у каждого из трех таймеров свои операции.

Так Timer0 отвечает за ШИМ на пятом и шестом пине, функции millis(), micros(), delay().

Другой таймер – Timer1, используется с ШИМ на девятом и десятом пине, с библиотеками WaveHC и Servo.

Timer2 работает с ШИМ на 11 и 13 пинах, а также с Tone.

Схемотехник должен позаботиться о безопасном использовании обрабатываемых совместно данных. Ведь прерывание останавливает на миллисекунду все операции процессора, а обмен данных между loop() и обработчиками прерываний должен быть постоянным. Может возникнуть ситуация, когда компилятор ради достижения своей максимальной производительности начнет оптимизацию кода.

Результатом этого процесса будет сохранение в регистре копии основных переменных кода, что позволит обеспечить максимальную скорость доступа к ним.

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

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

Управление процессами ардуино. Прерывания Arduino с помощью attachInterrupt. Зачем нужны аппаратные прерывания

Если под вашу задачу требуется большее число пинов/портов/мегагерц/памяти, чем имеется в используемом вами микроконтроллере, то в ответ на эту проблему обычно советуют взять микроконтроллер «покрупнее». Ответ не лишенный смысла, однако мне удалось найти задачку, от которой так просто не отмахнешься. Героем сегодняшней статьи будет 4-х разрядный семисегментный индикатор с динамической индикацией.

Я уже упоминал о нем в статье про сдвиговые регистры, но тогда у меня не было на руках самой железки, и соответственно говорил я лишь теоретически. Сами ардуинщики об индикаторе отзываются не очень лестно, т.к. применение этого индикатора ограниченное из-за того, что вследствие динамической индикации его нужно постоянно обновлять, что накладывает серьезное ограничение на основную программу. Теоретически эту задачу можно было бы «скинуть» в прерывание таймера, но решение это спорное.

В модуле меня привлекла его компактность. К примеру, для приборной панели паяльной станции, где место сильно ограничено, это то что надо. После некоторого размышления я решил, что в целом модуль неплох, но. для него требуется отдельный управляющий микроконтроллер, сопроцессор, на котором будет крутиться динамическая индикация.

Индикатор не содержит подтягивающих резисторов(!), возможно здесь используются сдвиговые регистры с подтяжкой? Так или иначе, я замерял потребление модуля через EnargyTrace и получил значение около 23mA при питании 3.3 Вольт, что для такой «гирлянды» вполне нормально.

Китайские ATtiny13a в SO-8 корпусе стоят около 15₽, они имеют пять рабочих выводов, три из которых нужно будет отдать на индикатор, остаются два вывода для организации линии связи, что более чем достаточно, но простенький SPI сюда не посадишь, т.к. тот SPI который будет использоваться для управления индикатором, работает мастером, а для связи с «главным» микроконтроллером нужен будет слейв( запускать слейв на главном микроконтроллере — это не вариант). К сожалению или к счастью(смотря как посмотреть), АTtiny13a не поддерживает аппаратно абсолютно никаких протоколов.

Т.о. перед нами стоит задача на ATtiny13a организовать c использованием не более двух пинов скоростную и надежную линию для приема двухбайтного числа от главного микроконтроллера, и отобразить его на 4-х разрядном семисегментном индикаторе. В идеале было бы использование аппаратного протокола главным микроконтроллером и его программной реализации на ATtiny13a. Также хотелось бы, что чтобы код реализации протоколов занимал минимально возможное место на флеше, чтобы его потом можно было использовать в других более сложных проектах.

Т.к. подразумевается использование индикатора для отображения температуры паяльника, во всех примерах будут задействованы только три разряда индикатора.

Полные исходники вместе со сборочными файлами и скомпилированными прошивками можно скачать по ссылке к конце статьи.

1) Счетчик на ATiny13a и 4-х разрядном семисегментном индикаторе

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

Так как подключение и управление 4-х разрядным семисегментным индикатором подробно рассматривалась в статье про сдвиговые регистры, я не буду переливать из пустого в порожнее, и просто приведу готовый код, с небольшими ремарками

Создаём каталоги проекта:

В директории inc создаем заголовочный файл led.h с объявлением одной-единственной функции:

в директории src, в файле led.c размещаем код драйвера:

Этот драйвер мы в дальнейшем трогать не будем, работать мы будем с функцией main.c Для счетчика она выглядит следующим образом:

Т.е. линия тактирования SCLK подключается на PB2, защелка RCLK подключается на PB4, и линия данных DIO на PB3. Пины PB0 и PB1 у нас остаются зарезервированными для соединения с главным микроконтроллером. Частота обновления индикатора задается командой _delay_ms(5), и в данном случае она составляет 1000/5 = 200 Гц. Если величину задержки увеличить хотя бы до 10мс, то на индикаторе появится четко выраженное мерцание. И дальше — больше.

В работе конструкция выглядит примерно так:

Прошивка занимает 422 байта флеш-памяти микроконтроллера. Предполагается что микроконтроллер работает на частоте 1.2 МГц.

2) Простой протокол на счетчике импульсов

Моей первой идеей было сделать счетчик импульсов. У нас имеется два прерывания: PCINT0 на PB0 и INT0 на PB1. На первом прерывании пусть будет команда начала отчета, а на втором сам счетчик. Вроде бы проще некуда.

Для ATtiny13a функция main.c будет выглядеть так:

Здесь, в прерывании по PCINT, при прижимании PB0 к земле мы запускаем счетчик, а при высоком урове PB0 счетчик отключается. В прерывании по вектору INT0 находится сам счетчик. Прошивка для «весит» 482 байта.

Для Arduino скетч будет таким:

Здесь пин 2 Arduino соединяется с PB0 ATtyny13a, а пин 3 Arduino c PB1 ATtiny13a. Также для обоих микроконтроллеров будет не лишним соединить землю.

Arduino посылает на ATtiny13a два попеременно сменяющих друг-друга числа: 573 и 219. Если все сделано как надо, числа будут отображаться на индикаторе.

Ок. Теперь посмотрим что с у нас со скоростью. Скетч в Arduino приведем к следующему виду:

Откроем монитор последовательного порта, нам должен будет открыться такой лог:

Т.е. мы имеем 35мс для

500, значит если мы захочем послать число 2000, то задержка составит

140мс, а для 8000 — 560мс, целых полсекунды(!).

Если мы попытаемся отправлять числа быстрее, т.е. уменьшить величину задержки в команде delayMicroseconds(50), то числа будут приниматься некорректно, внешние прерывания ATtiny13a будут не успевать обрабатывать входящие прерывания, Arduino все-таки работает на частоте 16 МГц, а «тинька» на 1.2 МГц.

Поэтому увеличить скорость протокола можно будет подняв рабочую частоту ATtiny13a до 9.6 МГц, добавив для этого в начало функции main() две строки:

А вот чтобы заставить Arduino передавать в десять раз быстрее, придется прибегнуть к «хардкорному» программированию, родные функции digitalWrite() слишком медлительны для этого. Т.о. функция void send_number(uint16_t num) будет выглядеть как-то так:

Теперь глянем, что у нас у со скоростью:

Скорость выросла в пятнадцать раз, и с таким темпами уже вполне возможно передавать температуру паяльника, т.е. число в пределах 500. А вот если нужно будет передавать число с большим значением, то придется придумать что-то похитрее.

Читать еще:  Новая showtopic invision power board. Итак, что же мы сделали

3) Пакетная передача данных с использованием буфера

Мы можем существенно увеличить скорость передачи, если число которое нужно будет передать разложим на множители.

Например, если нам нужно передавать температуру паяльника, т.е. положительное число в пределах от нуля до 500, то мы можем разложить одно число на два с каким-то общим основанием: n=n/основание + n%основание. Вследствие отсутствия аппаратного умножения на ATtiny13a, в качестве основания нас будут интересовать только степени двойки. К примеру, для числа 500 и основания 32, разложение будет таким: 500 = 15*32+20. Тогда передача числа в таком формате займет 15+20=32 импульсов вместо 500.

Однако у нам нет способа обозначить начало передачи. Для разделения чисел можно к одному прибавлять минимальное заведомо большое число, т.е. само основание. А затем на принимающей стороне вычитать его. Тогда передача числа 500 будет занимать 15+32+20=67 импульсов вместо 500. Т.е. мы увеличиваем скорость почти на порядок, и один пакет мы сможем отправлять со скоростью 0.2 мс, тогда пропускная способность нашего протокола составит

10Кбайт/c(это только теоретически. На самом деле скорость будет существенно ниже).

Ок, посмотрим как все это будет работать на практике. Код для ATtiny13a у меня вышел таким:

Прошивка занимает 548 байт. Скетч для Arduino выглядит так:

Т.к. время передачи числа меньше чем одна миллисекунда, здесь в цикле передается двадцать чисел, после чего замеряется потраченное время. В мониторе последовательного порта получаем такой лог:

Делим полученные цифры на двадцать, и имеем где-то 0.5 мс на передачу одного числа. Прогресс, хотя и не такой сильный как я ожидал 😉

Протокол еще можно улучшать, видоизменять под свои задачи, но в целом, думаю, что здесь все понятно.

4) Программный UART для ATtiny13a

Другим вариантом решения задачи связи двух микроконтроллеров является использование UART-протокола. Т.к. у нас связь полудуплексная, то с помощью UART мы обойдемся всего одной линией, а т.к. Arduino поддерживает UART аппаратно, время затрачиваемое главным микроконтроллером на обслуживание нашей периферии с индикатором, сведется к минимуму. Минусом же будет то, протокол основан на временных задержках и если кто-то начнет «тормозить» то «кина, увы не будет». Поэтому потолком для программных UART’ов считается скорость 9600 bod, т.е. около одного Кбайт/c или передача одно одного байта за одну миллисекунду. Нам же нужно передавать пакет их двух байтов. Т.е. в сравнении с протоколом на счетчике импульсов имеем более медленный протокол с одной стороны, но аппаратную поддержку протокола с другой. Чтож, посмотрим кто кого победит.

Для начала можно попытаться сделать UART приемник для одного байта, т.е. числа в диапазоне от нуля до 256.

Вариант прошивки с UART приемником на прерывании у меня получился таким:

Прошивка весит 476 байт.

Скетч для Arduino будет таким:

Между Arduino и ATtiny13a соединяются «земля» между собой и пин 1(Tx) для Arduino соединяется с PB1 ATtiny13a. На индикаторе должны попеременно отображаться цифры 43 и 218.

Если все работает как надо, то код можно немного доработать для передачи двухбайтного числа. Для обозначения начала пакета будем использовать тот же прием, что и в прошлый раз, т.е. разложение на множители. Код для ATtiny13a:

Прошивка в этот раз потянула 594 байта.

код для Arduino:

Замеры показали время на передачу одного пакета в районе 1.4 мс, т.е. 0.7 мс на передачу одного байта.

В данном случае сложно сказать какой протокол лучше. Для передачи небольших значений вполне сгодится счетчик импульсов, для полноценной линии связи лучше все-таки использовать UART. Если большой поток данных, то можно поднять частоту простым изменением коэффициентов. Для счетчика импульсов это недоступно. Через UART можно посылать пакеты с контролем приема и целостности данных. Одним словом, счетчик импульсов это совсем для чего-то простого. А UART протокол штука более универсальная. С него когда-то начинались локальные сети.

Однако повышать скорость программного UART свыше 9600 является решением сомнительным. Если нужна большая скорость, нужно использовать синхронные протоколы.

5) Программный I 2 C Slave на ATtiny13a

Artwork by Yana Cot

Протокол I 2 C работает на частоте 100 кГц, т.е. он вдвое быстрее чем UART на скорости 9600 bod. Протокол является двунаправленным, синхронным, высоконадежным протоколом с обратной связью. На одну I 2 C шину можно посадить сразу несколько устройств. Протокол I 2 C аппаратно поддерживается Arduino, для ATtiny13a же придется использовать программную реализацию. Готовый драйвер слейва I 2 C можно скачать с гитхаба здесь: lnx13/twi-slave-software-emulation

Данный драйвер написан для ATtiny13 «без A», но у меня на «A» он без проблем работал, даже mcu-id в Makefile менять не пришлось. Драйвер занимает немного менее 700 байт на флеше, после добавления кода драйвера индикатора, вся прошивка будет «весить» около 970 байт. Т.е. в размер флеш памяти микроконтроллера мы отлично вписываемся.

Драйвер слейва I 2 C состоит из файлов TWI.h, TWI.c, main.c и makefile. В файл main.c мы поместим драйвер 4-х разрядного семисегментного индикатора, а в остальных нужно будет поправить по одной, две строке.

В TWI.c нужно будет вырезать или закомментировать объявление массива для входящих данных:

Объявление этого массива нужно будет перенести в TWI.h, чтобы мы имели доступ к нему из main.c:

makefile можно вообще не трогать, или внести незначительные изменения.

а) Переменную MCU можно поменять на с суффиксом «а»:

б) таргет writeflash можно поменять под свой программатор, в моем случае это USBasp:

Файл main.c для нашей задачи будет выглядеть так:

Данный слейв I 2 C работает следующим образом. При начале сессии, т.е. при состоянии START индекс массива устанавливается в ноль,при поступлении данных массив индекс прибавляет к себе по единице. Следить за предотвращением выхода индекса за пределы массива, должен сам программист.

При записи, данные записываются в буфер начиная с ячейки 1. В нулевую ячейку записывается I 2 C адрес, т.е. (0x5D 2 C сессию. И 0.1 мс на передачу одного байта, т.е. как и должно быть на 100 кГц шине. По-моему, это отлично)

Прерывания в Ардуино

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

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

Аппаратные и программные прерывания

Прерывания в Ардуино можно разделить на несколько видов:

  • Аппаратные прерывания. Прерывание на уровне микропроцессорной архитектуры. Самое событие может произойти в производительный момент от внешнего устройства – например, нажатие кнопки на клавиатуре, движение компьютерной мыши и т.п.
  • Программные прерывания. Запускаются внутри программы с помощью специальной инструкции. Используются для того, чтобы вызвать обработчик прерываний.
  • Внутренние (синхронные) прерывания. Внутреннее прерывание возникает в результате изменения или нарушения в исполнении программы (например, при обращении к недопустимому адресу, недопустимый код операции и другие).

Зачем нужны аппаратные прерывания

Аппаратные прерывания возникают в ответ на внешнее событие и исходят от внешнего аппаратного устройства. В Ардуино представлены 4 типа аппаратных прерываний. Все они различаются сигналом на контакте прерывания:

  • Контакт притянут к земле. Обработчик прерывания исполняется до тех пор, пока на пине прерывания будет сигнал LOW.
  • Изменение сигнала на контакте. В таком случае Ардуино выполняет обработчик прерывания, когда на пине прерывания происходит изменение сигнала.
  • Изменение сигнала от LOW к HIGH на контакте – при изменении с низкого сигнала на высокий будет исполняться обработчик прерывания.
  • Изменение сигнала от HIGH к LOW на контакте – при изменении с высокого сигнала на низкий будет исполняться обработчик прерывания.

Прерывания полезны в программах Ардуино, так как помогают решать проблемы синхронизации. Например, при работе с UART прерывания позволяют не отслеживать поступление каждого символа. Внешнее аппаратное устройство подает сигнал прерывания, процессор сразу же вызывает обработчик прерывания, который вовремя захватывает символ. Это позволяет экономить процессорное время, которое без прерываний тратилось бы на проверку статуса UART, вместо этого все необходимые действия выполняются обработчиком прерывания, не затрагивая главную программу. Особых возможностей от аппаратного устройства не требуется.

Основными причинами, по которым необходимо вызвать прерывание, являются:

  • Определение изменения состояния вывода;
  • Прерывание по таймеру;
  • Прерывания данных по SPI, I2C, USART;
  • Аналогово-цифровое преобразование;
  • Готовность использовать EEPROM, флеш-память.

Как реализуются прерывания в Ардуино

При поступлении сигнала прерывания работа в цикле loop() приостанавливается. Начинается выполнение функции, которая объявляется на выполнение при прерывании. Объявленная функция не может принимать входные значения и возвращать значения при завершении работы. На сам код в основном цикле программы прерывание не влияет. Для работы с прерываниями в Ардуино используется стандартная функция attachInterrupt().

Отличие реализации прерываний в разных платах Ардуино

В зависимости от аппаратной реализации конкретной модели микроконтроллера есть несколько прерываний. Плата Arduino Uno имеет 2 прерывания на втором и третьем пине, но если требуется более двух выходов, плата поддерживает специальный режим «pin-change». Этот режим работает по изменению входа для всех пинов. Отличие режима прерывания по изменению входа заключается в том, что прерывания могут генерироваться на любом из восьми контактов. Обработка в таком случае будет сложнее и дольше, так как придется отслеживать последнее состояние на каждом из контактов.

На других платах число прерываний выше. Например, плата Ардуино Мега 2560 имеет 6 пинов, которые могут обрабатывать внешние прерывания. Для всех плат Ардуино при работе с функцией attachInterrupt (interrupt, function, mode) аргумент Inerrupt 0 связан с цифровым пином 2.

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Ну, цели вы не достигли. Применение ардуино тут вообще ничего не даёт. С тем же успехом вы могли просто положить его рядом на столе.

Эх, был бы шарп и мк в мои 17 лет, какую бы фигню только несотворил.

Картинка с троллейбусом уже была?

Не очень понятно, зачем [MTAThread] ? А использование Thread.Sleep это же лютый code smell, нет?

Обработчик прерываний — это низкоуровневый эквивалент обработчика событий.

Рядом не лежало.

Событие обрабатывается «когда руки дойдут» (если у вас не RTOS, конечно, но там тоже не так все просто)

Прерывание обрабатывается как только оно возникает. Писать обработчик прерываний тоже некоторый опыт нужен — из нынешнего поколения фреймворков у кого он есть? Кто знает как работать с прерыванием RTC? Ась?

В общем и целом, кабы я был экзаменатором, а вы студентом — послал бы вас учить WinAPI с написанием учебнойпрограммы на голом API без никаких библиотек и фреймворков. Просто для понимания как оно работает внутри.

Справедливости ради, в статье расписан обработчик событий, а не обработчик прерываний.

Прерывание — это процесс БЕЗУСЛОВНО переключающий ваш МК на другую задачу, и возобновляющий прерванную программу с того места где она прервалась.

Вот есть у вас Thread.Sleep(10); или delay() не суть важно. Увеличьте их значения до заоблачных, и попробуйте что вышло. Ваша программа ожидаемо застопорится на этой функции и больше не станет обрабатывать никакие нажатия или данные на последовательном порту. В случае с прерыванием было бы пофигу.

Как только эта программа стартует — она сразу же «зависает», исполняя delay(100500) и ей пофигу на любые данные на последовательном порту, rxtx и тд. Но стоит пину 2 поменять свое состояние — delay(100500) прервется, светодиод моргнет, и подпрограмма возвратится досиживать delay на оставшееся время.

Стоит отметить, что в WindowsForms присутствует элемент Timer который включается и выполняет код через определенный промежуток времени.

Таймер — это элемент WinAPI. Он всего лишь посылает сообщение WM_TIMER в очередь сообщений приложения через заданные промежутки времени. А выполнение кода обеспечивается диспетчером сообщений приложения в том случае, когда на это сообщение назначен соответствующий обработчик.

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

Так работает Windows и это надо понимать. Если у вас есть длинные циклы, то лучше бы внутри них периодически вызывать диспетчер сообщений (что-то типа метода ProcessMessages() или аналогичного)

Равно как и в коде потока периодически вызывать функцию, отдающую таймслайсы системе (Sleep, WaitObject и т.п.)

А настоящие аппаратные прерывания в Windows пользователю недоступны (разве что на уровне драйверов, но это отдельная тема).

Так что тут уже правильно сказали — ваша программа будет генерировать события, не более. А уж когда это событие будет обработано…

Прерывания и многозадачность в Arduino

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

Это могут быть прерывания по таймеру или внешние прерывания, уведомляющие о событиях, воздействующих на систему извне.

Прерывания и их источники

В процессе работы управляющий процессор выполняет определенные операции, а прерывание вызывает их остановку и в соответствие с кодом заставляет выполнить операции с более высоким приоритетом.

Проще говоря, прерывания – это набор приоритетов для тех или иных процессов, исполняемых контроллером.

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

Источником сигнала прерывания может стать таймер Arduino или процесс изменения состояния одного из контактов (пинов). Еще одним источником может стать какой-либо вход внешних прерываний: нужный сигнал появится при изменении его состояния.

Прерывание можно заставить работать с помощью кода, который поможет отвечать на прерывание. При этом нет необходимости писать код в loop (), который используют для систематической проверки приоритета прерывания.

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

Особенности прерывания по таймеру

Как же управлять временем и запускать процессы в нужном вам порядке? При работе с микросхемой Arduino для этого можно использовать millis(), ее эффективность зависит от постоянного обращения к ней. Тогда, в случае вызова этой функции, можно будет понять – наступило время определенной операции или нет.

Читать еще:  ТОП-10 Лучших Сетевых Накопителей (NAS)

Добиться эффективности можно при вызове millis() несколько раз в миллисекунду, но это довольно расточительно. Для вызова функции раз в миллисекунду (что является оптимальным вариантом) необходимо использовать таймер. Его можно установить на интервал в миллисекунду и добиться желаемого результата.

Микроконтроллер Arduino Uno укомплектован тремя таймерами: из них timer0 предназначен для генерации прерываний с интервалом в одну миллисекунду. При этом будет постоянно обновляться счетчик, передающий информацию функции millis(). Вести точный подсчет таймеру позволяет определенная частота, получаемая из 16 МГц процессора.

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

Функция timer0 оперирует тактовым делителем на 64 и изменять это значение не стоит. Оно позволяет получить частоту прерывания, близкую к 1 кГц, оптимальную для целей большинства схемотехников. Если попытаться изменить данный параметр, то можно нарушить работу функции millis().

Регистры сравнения и их роль в генерации прерывания

Регистры сравнения выполняют функцию анализа хранимых данных с текущим состоянием счетчика прерывания. Например, регистр сравнения (OCR0A) эффективно применяется для прерывания в середине счета.

Программный код, пример которого приведен ниже, позволит генерировать функцию TIMER0_COMPA при прохождении счетчика xAF:

Для оптимизации работы кода и микроконтроллера следует полностью отказаться от loop(). Для этого необходимо определение по вектору обработчика прерывания с помощью функции TIMER0_COMPA_vect. Благодаря ей обработчик и будет выполнять все те операции, что раньше делались в loop().

Данный код позволит вернуться к использованию функции delay(), благодаря чему все классические мерцающие светодиоды или работающие сервоприводы будут функционировать без проблем, но при этом мы получим удобный и практичный таймер.

Какие внешние воздействия вызывают прерывание

Вызвать внешние прерывания могут определенные действия из внешней среды. Это может быть простым нажатием на кнопку или срабатыванием используемого датчика. При этом нет необходимости вести постоянный опрос вывода GPIO о происходящих изменениях.

Микроконтроллер Arduino может иметь несколько пинов, способных обрабатывать внешние прерывания. Так, на плате Arduino Uno их два, а на Arduino Mega 2560 – 6. Продемонстрировать их функционал можно с помощью кнопки сброса сервопривода. Для этого при написании кода в класс Sweeper необходимо добавит функцию reset(). Она способна установить нулевое положение и перетаскивать в нее сервопривод.

Соединить обработчик с внешним прерыванием поможет другая функция – attachInterrupt(). На приведенных в качестве примеров микроконтроллерах Interrupt0 реализована на втором контакте. Она сообщает микроконтроллеру о том, что на данном входе ожидается спад сигнала.

Достаточно нажать кнопку и сигнал действительно падает до минимального уровня, вызывая обработчик Reset.

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

Полный код программы с таймерами и внешними прерываниями:

Библиотеки прерываний

У схемотехников, благодаря Всемирной паутине есть доступ к широкому кругу библиотек, которые существенно облегчат работу с таймерами. Большинство из них предназначены для тех, кто применяет функцию millis(), но есть и такие, которые позволяют произвести желаемую настройку таймеров и сгенерировать прерывания. Оптимальным вариантом для этого являются библиотеки TimerThree и TimerOne, разработанные Paul Stoffregan.

Благодаря им, схемотехники получают широкий выбор возможностей, с помощью которых можно сконфигурировать прерывания с помощью таймера. Первая из этих библиотек не работает с Adruino Uno, но прекрасно зарекомендовала себя с платами Teensy, микроконтроллерами Adruino Mega2560 и Adruino Leonardo.

Недостатком Adruino Uno является наличие всего двух ходов, предназначенных для работы с внешними прерываниями. Если требуется большее количество подобных пинов, то отчаиваться не стоит, ведь этот микроконтроллер поддерживает pin-change – прерывания по изменению входа и работает это на всех восьми входах.

Их отличие от простых внешних прерываний – в сложности обработки, так как схемотехнику требуется отслеживать последнее из известных состояний пинов. Только в этом случае можно будет понять, какой из них вызвал прерывание.

Наиболее информативное и практичной библиотекой для прерываний по изменению входа является PinChangeInt.

Прерывания: основные правила работы

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

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

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

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

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

Если вызов функции Update() все же необходим, то предварительно необходимо будет проверить переменную состояния. Это позволит выяснить, необходима ли последующая обработка.

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

Какими функциями оперирует тот или иной таймер?

Для микроконтроллера Arduino Uno у каждого из трех таймеров свои операции.

Так Timer0 отвечает за ШИМ на пятом и шестом пине, функции millis(), micros(), delay().

Другой таймер – Timer1, используется с ШИМ на девятом и десятом пине, с библиотеками WaveHC и Servo.

Timer2 работает с ШИМ на 11 и 13 пинах, а также с Tone.

Схемотехник должен позаботиться о безопасном использовании обрабатываемых совместно данных. Ведь прерывание останавливает на миллисекунду все операции процессора, а обмен данных между loop() и обработчиками прерываний должен быть постоянным. Может возникнуть ситуация, когда компилятор ради достижения своей максимальной производительности начнет оптимизацию кода.

Результатом этого процесса будет сохранение в регистре копии основных переменных кода, что позволит обеспечить максимальную скорость доступа к ним.

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

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

Скользкая дорожка для поклонников volatile

Прошло уже практически 30 лет с тех пор, как в стандарте языка C появился модификатор volatile, но в наши дни это ключевое слово вызывает лишь больше вопросов и непонимания даже среди программистов, общающихся с железом на “ты”. Сейчас уже никого не удивишь многоядерным мобильным телефоном или компьютером, умещающимся в одном чипе. Прогресс не стоит на месте, компиляторы умнеют, задачи программистов усложняются, вынуждая помнить о барьерах компиляции и барьерах памяти работая на многопроцессорных системах, только volatile по-прежнему остается темным уголком стандарта, в котором лишь сказано, что доступ к такой изменчивой переменной “implementation-defined” (Стандарт C, 6.7.3/7), т.е. как решат ребята, разрабатывающие компилятор, так и будет.

Disclaimer

В данной статье я буду говорить об использовании volatile в языке С в контексте определения переменных, не касаясь ассемблерных вставок, помеченных как volatile. Так же я буду приводить примеры кода, генерируемого компилятором gcc для архитектуры x86-64, но все сказанное в полной мере относится вообще к любым современным компиляторам и архитектурам. А если ваш компилятор генерирует другой код, который работает, то это вовсе не значит, что со следующей версией поведение вашей программы, использующей volatile, не поменяется. Да и код, скорее всего, будет непереносим на другие платформы.

Зачем нужен volatile?

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

а вот ассемблерный код, генерируемый gcc 4.7.1 с опцией -O3:

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

Ключевое слово volatile и является одним из звеньев, связывающих абстрактную машину и реальную. В USENET много постов (1, 2, 3) 20-летней давности, проливающих свет на темную историю появления этого модификатора. Приведу хороший перевод одного из писем, взятый здесь:

На уровне железа многие процессоры просто резервируют блок адресов памяти для портов ввода-вывода. Большинство процессоров имеют отдельное пространство адресов ввода-вывода, со специальными инструкциями для доступа к ним, но это не универсально (на PDP-11 такого не было, например) и даже сейчас, производители железа могут предпочесть использовать для этого адресное пространство памяти, по разным причинам. Я сомневаюсь, что кто-то так делает на архитектуре 8086 — различные адресные ограничения делают это очень сложным. Я видел это на 8080, это очень частое решение на старой TI 9900. И это был единственный способ организовать ввод-вывод на наборе инструкций PDP-11, там просто не было отдельного адресного пространства ввода-вывода (Я думаю, то же самое верно и для VAX. И не забывайте, что большинство работы на С раньше проходило именно на этих двух процессорах).

Теперь рассмотрим один из первых последовательных портов, что я использовал: Intel 8051. Нормальным способом его инициализации было записать 0 три раза в порт управления. Если у вас MMIO, то код на С мог бы выглядеть примерно так:

Что прекрасно работало на всех компиляторах С. Но вы можете себе представить, что могла бы с этим сделать сама простая оптимизация. По этой причине и нужно было ключевое слово volatile, чтобы сказать компилятору не оптимизировать.

Таким образом, volatile обязывает компилятор каждый раз обращаться к памяти, избегая возможности кеширования значения в регистрах. Для разработчиков UNIX в 80-х этот модификатор был единственным способом борьбы с компилятором, который норовил соптимизировать и выкинуть действительно нужный код.

Распространенные заблуждения при использовании volatile

1. Обращение к volatile переменной атомарны

Рассмотрим такой код:

что создает компилятор:

Единственная инструкция addl $1, j(%rip) для переменной j выглядит куда более «атомарнее», чем три инструкции для volatile переменной i. Выводы:

  • volatile не имеет ничего общего с атомарностью
  • компилятор создает больше инструкций при обращении к volatile переменной, что плохо сказывается на эффективности кода
  • компилятор вправе нарушить порядок инструкций. Действительно, сначала вычитывается значение i в регистр, происходит инкремент j, после чего делается инкремент старого значения i из регистра

Как бороться? Для низкоуровневых приложений: использовать атомарные инструкции или отключать прерывания на однопроцессорных системах, для программ, работающих в пространстве пользователя, использовать mutex блокировки.

2. volatile помогает создать lockless код

Это наиболее распространненное и опасное заблуждение, которое кочует из исходника в исходник. Рассмотрим код:

Программист уверен, что такое простое решение, основанное на volatile флаге, позволит ему избежать «ненужного» и «долгого» lock’а (под lock’ом я подразумеваю вызов mutex, если речь идет о userspace приложении, или запрет прерывания, если речь идет о низкоуровневом коде). А вот что создает компилятор:

Ожидания программиста не оправдались, вместо мифической экономии на lock’ах получилась редкая проблема, воспроизведение которой зависит от:

  • случая
  • агрессивности оптимизатора
  • архитектуры, для которой создается приложения

С точки зрения стандарта C данное поведение компилятора абсолютно нормально, так как он вправе переставлять volatile и не volatile конструкции. Более того, на современных архитектурах сам процессор может нарушить порядок выполнения инструкций, выполнив ready = i до инициализации буфера, но тема слабой модели упорядочивания доступа к памяти не для данной статьи.

Как бороться? Использовать барьер компилятора. Это вот такая вот ассемблерная вставка, которая поддерживается большинством современных компиляторов (а если в вашем компиляторе такой инструкции нет, то значит есть какая-то другая):

asm volatile («» . «memory»);

которая является барьером, говорящая компилятору «сбрось все регистры в RAM до барьера и перечитай после».

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

Кстати говоря, барьером компилятора являются вызовы функций, if/for/while конструкции, return, etc. Для детального погружения в тему смотреть Стандарт C, Annex C Sequence points, 438 p.

3. volatile нужно использовать всегда, если переменная может измениться из нескольких контекстов выполнения

Часто можно встретить код, в котором все переменные, изменяемые в контексте прерывания или другом потоке, объявлены как volatile. Даже встречаются объявления целых структур:

Наверное, программист надеется, что при обращении к членам данной структуры, компилятор сделает всю работу, подумав об атомарности, «правильном» порядке доступа к переменным и необходимых lock’ах. Ничего подобного не будет. На выходе будет неэффективный код, который ничего «правильного» делать не будет, так как в стандарте C ничего не сказано о том, что вообще должен делать компилятор при доступе к volatile переменной. Если код работает так, как ожидается, то это случайность. Более того, компиляторы тоже содержат баги, которые проявляются при обращении к volatile переменным, а как результат — создается неверный ассемблерный код.

Как правильно использовать volatile?

1. в контексте «asm volatile»

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

2. приведение к volatile указателю там, где нужно

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

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

В остальных случаях вы получаете лишь баги с редким и сложным воспроизведением.

Эпилог

Я не зря начал статью со вступления, что прогресс не стоит на месте. Сегодня ваш код с volatile работает, а завтра вы ставите новую версию компилятора, запускаете агрессивную оптимизацию или пытаетесь портировать код на многопроцессорную новомодную ARM систему, а в итоге получаете массу проблем из-за неверного использование этого модификатора или недопонимания всех тонкостей современной архитектуры.

Ссылка на основную публикацию
Статьи c упоминанием слов:
Adblock
detector