Arduino параллельное выполнение. Прерывания Arduino с помощью attachInterrupt. Отличие реализации прерываний в разных платах Ардуино

Когда вы установите эту программу, то удивитесь — насколько она похожа на Arduino IDE. Не удивляйтесь, обе программы сделаны на одном движке.

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

Запустим Arduino IDE и выберем простейший пример вывода данных на Serial Port
:

Void setup() {
Serial.begin(9600);
}
void loop() {
Serial.println(«Hello Kitty!»);
// ждем 500 миллисекунд перед следующей отправкой
delay(500);
}

Запустим пример и убедимся, что код работает.

Получение данных

Теперь мы хотим получить этот же текст в . Запускаем новый проект и напишем код.

Первый шаг — импортировать библиотеку. Идем в Sketch | Import Library | Serial
. В скетче появится строка:

Import processing.serial.*;
Serial serial; // создаем объект последовательного порта
String received; // данные, получаемые с последовательного порта
void setup()
{
String port = Serial.list();
serial = new Serial(this, port, 9600);
}
void draw() {
if (serial.available() > 0) { // если есть данные,
received = serial.readStringUntil(«\n»); // считываем данные
}
println(received); //отображаем данные в консоли
}

Чтобы обеспечить прием данных с последовательного порта, нам нужен объект класса Serial
. Так как с Arduino мы отправляем данные типа String, нам надо получить строку и в Processing.

В методе setup()
нужно получить доступный последовательный порт. Как правило, это первый доступный порт из списка. После этого мы можем настроить объект Serial
, указав порт и скорость передачи данных (желательно, чтобы скорости совпадали).

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

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

Import processing.serial.*;
Serial serial; // создаем объект последовательного порта
String received; // данные, получаемые с последовательного порта
void setup()
{
size(320, 120);
String port = Serial.list();
serial = new Serial(this, port, 9600);
}
void draw() {
if (serial.available() > 0) { // если есть данные,
// считываем их и записываем в переменную received
received = serial.readStringUntil(«\n»);
}
// Настройки для текста
textSize(24);
clear();
if (received != null) {
text(received, 10, 30);
}
}

Запустим пример ещё раз и увидим окно с надписью, которое перерисовывается в одном месте.

Таким образом мы научились получать данные от Arduino. Это позволит нам рисовать красивые графики или создавать программы контроля за показаниями датчиков.

Отправка данных

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

Допустим, мы будем посылать символ «1» из Processing. Когда плата обнаружит присланный символ, включим светодиод на порту 13 (встроенный).

Скетч будет похож на предыдущий. Для примера создадим небольшое окно. При щелчке в области окна будем отсылать «1» и дублировать в консоли для проверки. Если щелчков не будет, то посылается команда «0».

Import processing.serial.*;
Serial serial; // создаем объект последовательного порта
String received; // данные, получаемые с последовательного порта
void setup()
{
size(320, 120);
String port = Serial.list();
serial = new Serial(this, port, 9600);
}
void draw() {
if (mousePressed == true) {
//если мы кликнули мышкой в пределах окна
serial.write(«1»); //отсылаем 1
println(«1»);
} else { //если щелчка не было
serial.write(«0»); //отсылаем 0
}
}

Теперь напишем скетч для Arduino.

Char commandValue; // данные, поступаемые с последовательного порта
int ledPin = 13; // встроенный светодиод
void setup() {
pinMode(ledPin, OUTPUT); // режим на вывод данных
Serial.begin(9600);
}
void loop() {
if (Serial.available()) {
commandValue = Serial.read();
}
if (commandValue == «1») {
digitalWrite(ledPin, HIGH); // включаем светодиод
}
else {
digitalWrite(ledPin, LOW); // в противном случае выключаем
}
delay(10); // задержка перед следующим чтением данных
}

Запускаем оба скетча. Щёлкаем внутри окна и замечаем, что светодиод загорается. Можно даже не щёлкать, а удерживать кнопку мыши нажатой — светодиод будет гореть постоянно.

Обмен данными

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

Для максимальной эффективности добавим булеву переменную. В результате у нас отпадает необходимость постоянно отсылать 1 или 0 от Processing и последовательный порт разгружается и не передает лишнюю информацию.

Когда плата обнаружит присланную единицу, то меняем булевое значение на противоположное относительно текущего состояния (LOW
на HIGH
и наоборот). В else
используем строку «Hello Kity», которую будем отправлять только в случае, когда не обнаружим «1».

Функция establishContact()
отсылает строку, которую мы ожидаем получить в Processing. Если ответ приходит, значит Processing может получить данные.

Char commandValue; // данные, поступаемые с последовательного порта
int ledPin = 13;
boolean ledState = LOW; //управляем состоянием светодиода
void setup() {
pinMode(ledPin, OUTPUT);
Serial.begin(9600);
establishContact(); // отсылаем байт для контакта, пока ресивер отвечает
}
void loop() {
// если можно прочитать данные
if (Serial.available() > 0) {
// считываем данные
commandValue = Serial.read();
if (commandValue == «1») {
ledState = !ledState;
digitalWrite(ledPin, ledState);
}
delay(100);
}
else {
// Отсылаем обратно
Serial.println(«Hello Kitty»);
}
delay(50);
}
void establishContact() {
while (Serial.available() <= 0) {
Serial.println(«A»); // отсылает заглавную A
delay(300);
}
}

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

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

В методе setup()
добавляем строку serial.bufferUntil(«\n»);
. Это позволяет хранить поступающие данные в буфере, пока мы не обнаружим определённый символ. В этом случае возвращаем (\n), так как мы отправляем Serial.println()
от Arduino. «\n»
в конце значит, что мы активируем новую строку, то есть это будут последние данные, которые мы увидим.

Так как мы постоянно отсылаем данные, метод serialEvent()
выполняет задачи цикла draw()
, то можно его оставить пустым.

Теперь рассмотрим основной метод serialEvent()
. Каждый раз, когда мы выходим на новую строку (\n), вызывается этот метод. И каждый раз проводится следующая последовательность действий:

  • Считываются поступающие данные;
  • Проверяется, содержат ли они какие-то значения (то есть, не передался ли нам пустой массив данных или «нуль»);
  • Удаляем пробелы;
  • Если мы первый раз получили необходимые данные, изменяем значение булевой переменной firstContact
    и сообщаем Arduino, что мы готовы принимать новые данные;
  • Если это не первый приём необходимого типа данных, отображаем их в консоли и отсылаем микроконтроллеру данные о клике, который совершался;
  • Собщаем Arduino, что мы готовы принимать новый пакет данных.

import processing.serial.*;
Serial serial; // создаем объект последовательного порта
String received; // данные, получаемые с последовательного порта
// Проверка на поступление данных от Arduino
boolean firstContact = false;
void setup()
{
size(320, 120);
String port = Serial.list();
serial = new Serial(this, port, 9600);
serial.bufferUntil(«\n»);
}
void draw() {
}
void serialEvent(Serial myPort) { //формируем строку из данных, которые поступают
// «\n» — разделитель — конец пакета данных
received = myPort.readStringUntil(«\n»); //убеждаемся, что наши данные не пустые перед тем, как продолжить
if (received != null) { //удаляем пробелы
received = trim(received);
println(received); //ищем нашу строку «A» , чтобы начать рукопожатие
//если находим, то очищаем буфер и отсылаем запрос на данные
if (firstContact == false) {
if (received.equals(«A»)) {
serial.clear();
firstContact = true;
myPort.write(«A»);
println(«contact»);
}
} else { //если контакт установлен, получаем и парсим данные
println(received);
if (mousePressed == true) { //если мы кликнули мышкой по окну
serial.write(«1»); //отсылаем 1
println(«1»);
} // когда вы все данные, делаем запрос на новый пакет
serial.write(«A»);
}
}
}

При подключении и запуске в консоли должна появится фраза «Hello Kitty». Когда вы будете щёлкать мышкой в окне Processing, светодиод на пине 13 будет включаться и выключаться.

Кроме Processing, вы можете использовать программы PuTTy или написать свою программу на C# использованием готовых классов для работы с портами.

04.Communication: Dimmer

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

Для примера понадобится стандартная схема с резистором и светодиодом на выводе 9.

Скетч для Arduino.

Const int ledPin = 9; // светодиод на выводе 9
void setup() {
Serial.begin(9600);
// устанавливаем режим на вывод
pinMode(ledPin, OUTPUT);
}
void loop() {
byte brightness;
// проверяем, есть ли данные от компьютера
if (Serial.available()) {
// читаем последние полученные байты от 0 до 255
brightness = Serial.read();
// устанавливаем яркость светодиода
analogWrite(ledPin, brightness);
}
}

Код для Processing

Import processing.serial.*;
Serial port;
void setup() {
size(256, 150);
println(«Available serial ports:»);
println(Serial.list());
// Uses the first port in this list (number 0). Change this to select the port
// corresponding to your Arduino board. The last parameter (e.g. 9600) is the
// speed of the communication. It has to correspond to the value passed to
// Serial.begin() in your Arduino sketch.
port = new Serial(this, Serial.list(), 9600);
// Если вы знаете имя порта, используемой платой Arduino board, то явно укажите
//port = new Serial(this, «COM1», 9600);
}
void draw() {
// рисуем градиент от чёрного к белому
for (int i = 0; i

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

04.Communication: PhysicalPixel (Зажигаем светодиод мышкой)

Немного изменим задачу. Будем проводить мышкой над квадратом и посылать символ «H» (High), чтобы зажечь светодиод на плате. Когда мышь покинет область квадрата, то пошлём символ «L» (Low), чтобы погасить светодиод.

Код для Arduino.

Const int ledPin = 13; // вывод 13 для светодиода
int incomingByte; // переменная для получения данных
void setup() {
Serial.begin(9600);
pinMode(ledPin, OUTPUT);
}
void loop() {
// если есть данные
if (Serial.available() > 0) {
// считываем байт в буфере
incomingByte = Serial.read();
// если это символ H (ASCII 72), то включаем светодиод
if (incomingByte == «H») {
digitalWrite(ledPin, HIGH);
}
// если это символ L (ASCII 76), то выключаем светодиод
if (incomingByte == «L») {
digitalWrite(ledPin, LOW);
}
}
}

Код для Processing.

Import processing.serial.*;
float boxX;
float boxY;
int boxSize = 20;
boolean mouseOverBox = false;
Serial port;
void setup() {
size(200, 200);
boxX = width / 2.0;
boxY = height / 2.0;
rectMode(RADIUS);
println(Serial.list());
// Open the port that the Arduino board is connected to (in this case #0)
// Make sure to open the port at the same speed Arduino is using (9600bps)
port = new Serial(this, Serial.list(), 9600);
}
void draw() {
background(0);
// Если курсор над квадратом
if (mouseX > boxX — boxSize && mouseX boxY — boxSize && mouseY

04.Communication: Graph (Рисуем график)

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


Аппаратные прерывания

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

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

External hardware interrupt
– это прерывание, вызванное изменением напряжения на пине микроконтроллера. Основная суть состоит в том, что микроконтроллер (вычислительное ядро) не занимается опросом пина
и не тратит на это время
, пином занимается другая “железка”. Как только напряжение на пине изменяется (имеется в виду цифровой сигнал, +5 подали/+5 убрали) – микроконтроллер получает сигнал, бросает все дела, обрабатывает прерывание, и возвращается к работе. Зачем это нужно? Чаще всего прерывания используются для детектирования коротких событий – импульсов, или даже для подсчёта их количества, не нагружая основной код. Аппаратное прерывание может поймать короткое нажатие кнопки или срабатывание датчика во время сложных долгих вычислений или задержек в коде, т.е. грубо говоря – пин опрашивается параллельно основному коду
. Также прерывания могут будить микроконтроллер из режимов энергосбережения, когда вообще практически вся периферия отключена. Посмотрим, как работать с аппаратными прерываниями в среде Arduino IDE.

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

Начнём с того, что не все пины “могут” в прерывания. Да, есть такая штука, как pinChangeInterrupts
, но о ней мы поговорим в продвинутых уроках. Сейчас нужно понять, что аппаратные прерывания могут генерировать только определённые пины:

МК / номер прерывания
INT 0
INT 1
INT 2
INT 3
INT 4
INT 5
ATmega 328/168 (Nano, UNO, Mini) D2 D3
ATmega 32U4 (Leonardo, Micro) D3 D2 D0 D1 D7
ATmega 2560 (Mega) D2 D3 D21 D20 D19 D18

Как вы поняли из таблицы, прерывания имеют свой номер, который отличается от номера пина. Есть кстати удобная функция digitalPinToInterrupt(pin)
, которая принимает номер пина и возвращает номер прерывания. Скормив этой функции цифру 3 на Ардуино нано, мы получим 1. Всё по таблице выше, функция для ленивых.

Подключается прерывание при помощи функции attachInterrupt(pin, handler, mode)
:

  • pin
    – номер прерывания
  • handler
    – имя функции-обработчика прерывания (её нужно создать самому)
  • mode
    – “режим” работы прерывания:

    • LOW
      (низкий) – срабатывает при сигнале LOW
      на пине
    • RISING
      (рост) – срабатывает при изменении сигнала на пине с LOW
      на HIGH
    • FALLING
      (падение) – срабатывает при изменении сигнала на пине с HIGH
      на LOW
    • CHANGE
      (изменение) – срабатывает при изменении сигнала (с LOW
      на HIGH
      и наоборот)

Также прерывание можно отключить при помощи функции detachInterrupt(pin)
, где pin – опять же номер прерывания
.

А ещё можно глобально запретить прерывания функцией noInterrupts()
и снова разрешить их при помощи interrupts()
. Аккуратнее с ними! noInterrupts()
остановит также прерывания таймеров, и у вас “сломаются” все функции времени и генерация ШИМ.

Давайте рассмотрим пример, в котором в прерывании считаются нажатия кнопки, а в основном цикле они выводятся с задержкой в 1 секунду. Работая с кнопкой в обычном режиме, совместить такой грубый вывод с задержкой – невозможно:

Volatile int counter = 0; // переменная-счётчик
void setup() {
Serial.begin(9600); // открыли порт для связи
// подключили кнопку на D2 и GND
pinMode(2, INPUT_PULLUP); \
// D2 это прерывание 0
// обработчик — функция buttonTick
// FALLING — при нажатии на кнопку будет сигнал 0, его и ловим
attachInterrupt(0, buttonTick, FALLING);
}
void buttonTick() {
counter++; // + нажатие
}
void loop() {
Serial.println(counter); // выводим
delay(1000); // ждём
}

Итак, наш код считает нажатия даже во время задержки! Здорово. Но что такое volatile
? Мы объявили глобальную переменную counter
, которая будет хранить количество нажатий на кнопку. Если значение переменной будет изменяться в прерывании, нужно сообщить об этом микроконтроллеру при помощи спецификатора volatile
, который пишется перед указанием типа данных переменной, иначе работа будет некорректной. Это просто нужно запомнить: если переменная меняется в прерывании – делайте её volatile
.

Ещё несколько важных моментов:

  • Переменные, изменяемые в прерывании, должны быть объявлены как volatile
  • В прерывании не работают задержки типа delay()
  • В прерывании не меняет своё значение millis()
    и micros()
  • В прерывании некорректно работает вывод в порт (Serial.print()
    ), также не стоит там его использовать – это нагружает ядро
  • В прерывании нужно стараться делать как можно меньше вычислений и вообще “долгих” действий – это будет тормозить работу МК при частых прерываниях! Что же делать? Читайте ниже.

Если прерывание отлавливает какое-то событие, которое необязательно обрабатывать сразу, то лучше использовать следующий алгоритм работы с прерыванием:

  • В обработчике прерывания просто поднимаем флаг
  • В основном цикле программы проверяем флаг, если поднят – сбрасываем его и выполняем нужные действия

volatile boolean intFlag = false; // флаг
void setup() {
Serial.begin(9600); // открыли порт для связи
// подключили кнопку на D2 и GND
pinMode(2, INPUT_PULLUP);
// D2 это прерывание 0
// обработчик — функция buttonTick
// FALLING — при нажатии на кнопку будет сигнал 0, его и ловим
attachInterrupt(0, buttonTick, FALLING);
}
void buttonTick() {
intFlag = true; // подняли флаг прерывания
}
void loop() {
if (intFlag) {
intFlag = false; // сбрасываем
// совершаем какие-то действия
Serial.println(«Interrupt!»);
}
}

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

Видео

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

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

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

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

Не стоит использовать длительную обработку в loop()

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

Если вызов функции Update()

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

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

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

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

Так Timer0

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

, micros()

, delay()

.

Другой таймер – Timer1,

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

Timer2

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

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

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

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

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

Чтобы этого не произошло нужно использовать переменную voltatile

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

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

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

Операция

Время цикла
Опрашивает 3 кнопки, обрабатывает сигналы с них для устранения дребезга 2 мс
Регенерирует данные семисегментных светодиодных индикаторов 2 мс
Вырабатывает сигналы управления для 2 датчиков температуры DS18B20 и считывает данные с них. Датчики имеют последовательный интерфейс 1-wire. 100 мкс для каждого бита,
1 сек общий цикл чтения
Чтение аналоговых значений тока и напряжения на элементе Пельтье, напряжения питания 100 мкс
Цифровая фильтрация аналоговых значений тока и напряжения 10 мс
Вычисление мощности на элементе Пельтье 10 мс
ПИД (пропорционально интегрально дифференциальный) регулятор стабилизации тока и напряжения 100 мкс
Регулятор мощности 10 мс
Регулятор температуры 1 сек
Защитные функции, контроль целостности данных 1 сек
Управление, общая логика работы системы 10 мс

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

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

В предыдущих уроках мы создали класс для объекта кнопка. Мы сказали, что это класс для обработки сигнала в параллельном процессе. Что для его нормальной работы необходимо вызывать функцию (метод) обработки сигнала в цикле с регулярным периодом (мы выбрали время 2 мс). И тогда в любом месте программы доступны признаки, показывающие текущее состояние кнопки или сигнала.

В одном цикле мы поместили код обработки состояния кнопок и управление светодиодами. А в конце цикла поставили функцию задержки delay(2). Но, время на выполнение программы в цикле меняет общее время цикла. И период цикла явно не равен 2 мс. К тому же, во время выполнения функции delay() программа зависает и не может производить других действий. На сложной программе получится полный хаос.

Выход — вызывать функцию обработки состояния кнопки по прерыванию от аппаратного таймера. Каждые 2 мс основной цикл программы должен прерываться, происходить обработка сигнала кнопки и управление возвращаться в основной цикл на код, где он был прерван. Короткое время на обработку сигнала кнопки не будет значительно влиять на выполнение основного цикла. Т.е. обработка кнопки будет происходить параллельно, незаметно для основной программы.

Аппаратное прерывание от таймера.

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

С точки зрения программы прерывание это вызов функции по внешнему, не связанному напрямую с программным кодом, событию.

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

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

Библиотека MsTimer2.

Библиотека предназначена для конфигурирования аппаратного прерывания от Таймера 2 микроконтроллера. Она имеет всего три функции:

  • MsTimer2::set(unsigned long ms, void (*f)())

Эта функция устанавливает время периода прерывания в мс. С таким периодом будет вызываться обработчик прерывания f. Он должен быть объявлен как void (не возвращает ничего) и не иметь аргументов. * f – это указатель на функцию. Вместо него надо написать имя функции.

  • MsTimer2::start()

Функция разрешает прерывания от таймера.

  • MsTimer2::stop()

Функция запрещает прерывания от таймера.

Перед именем функций надо писать MsTimer2::, т.к. библиотека написана с использованием директивы пространства имен namespace.

Для установки библиотеки скопируйте каталог MsTimer2 в папку libraries в рабочей папке Arduino IDE. За тем запустите программу Arduino IDE, откройте Скетч -> Подключить библиотеку
и посмотрите, что в списке библиотек присутствует библиотека MsTimer2.

Загрузить библиотеку MsTimer2 в zip-архиве можно . Для установки его надо распаковать.

Простая программа с параллельной обработкой сигнала кнопки.

Теперь напишем простую программу с одной кнопкой и светодиодом из урока 6. К плате Ардуино подключена одна кнопка по схеме:

Выглядит это так:

На каждое нажатие кнопки светодиод на плате Ардуино меняет свое состояние. Необходимо чтобы были установлены библиотеки MsTimer2 и Button:

MsTimer2

И оплатите. Всего 40 руб. в месяц за доступ ко всем ресурсам сайта!

// sketch_10_1 урока 10



// Нажатие на кнопку меняет состояние светодиода


#include


#include

#define LED_1_PIN 13 //


#define BUTTON_1_PIN 12 // кнопка подключена к выводу 12


Button button1(BUTTON_1_PIN, 15); // создание объекта — кнопка

void setup() {





MsTimer2::set(2, timerInterupt); // задаем период прерывания по таймеру 2 мс



MsTimer2::start(); //


}

void loop() {

// управление светодиодом



if (button1.flagClick == true) {


// был клик кнопки










}


}

// обработчик прерывания



void timerInterupt() {


button1.scanState(); // вызов метода ожидания стабильного состояния для кнопки



}

В функции setup()
задаем время цикла прерывания по таймеру 2 мс и указываем имя обработчика прерывания timerInterrupt
. Функция обработки сигнала кнопки button1.scanState()
вызывается в обработчике прерывания таймера каждые 2 мс.

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

Квалификатор volatile.

Давайте изменим цикл loop() в предыдущей программе.

void loop() {

while(true) {


if (button1.flagClick == true) break;


}

// был клик кнопки



button1.flagClick= false; // сброс признака



digitalWrite(LED_1_PIN, ! digitalRead(LED_1_PIN)); // инверсия светодиода



}

Логически ничего не поменялось.

  • В первом варианте программа проходила цикл loop до конца и в нем анализировала флаг button1.flagClick.
  • Во втором варианте программа анализирует флаг button1.flagClick в бесконечном цикле while. Когда флаг становится активным, то выходит из цикла while по break и инвертирует состояние светодиода.

Разница только в том, в каком цикле крутится программа в loop или в while.

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

#include


#define LED_1_PIN 13 // светодиод подключен к выводу 13



int count=0;

void setup() {


pinMode(LED_1_PIN, OUTPUT); // определяем вывод светодиода как выход



MsTimer2::set(500, timerInterupt); // задаем период прерывания по таймеру 500 мс



MsTimer2::start(); // разрешаем прерывание по таймеру



}

void loop() {

while (true) {


if (count != 0) break;


}

count= 0;


digitalWrite(LED_1_PIN, ! digitalRead(LED_1_PIN)); // инверсия состояния светодиода



}

// обработчик прерывания



void timerInterupt() {


count++;


}

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

Дело в том, что компилятор языка C++ по мере своего интеллекта оптимизирует программу. Иногда это не идет на пользу. Компилятор видит, что в цикле while никакие операции с переменной count не производятся. Поэтому он считает, что достаточно проверить состояние count только один раз. Зачем в цикле проверять, то, что никогда не может измениться. Компилятор корректирует код, оптимизируя его по времени исполнения. Проще говоря убирает из цикла код проверки переменной. Понять, что переменная count меняет свое состояние в обработчике прерывания, компилятор не может. В результате мы зависаем в цикле while.

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

Если, например, добавить в цикл while вызов функции delay(), то программа заработает.

while (true) {


if (count != 0) break;


delay(1);


}

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

Иногда это сделать непросто или не так эффективно. Тогда надо использовать квалификатор volatile. Он указывается при объявлении переменной и сообщает компилятору, что не надо пытаться оптимизировать ее использование. Он запрещает компилятору делать предположения по поводу значения переменной, так как переменная может быть изменена в другом программном блоке, например, в параллельном процессе. Также компилятор размещает переменную в ОЗУ, а не в регистрах общего назначения.

Достаточно в программе при объявлении count написать

volatile int count=0;

и все варианты будут работать.

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

volatile Button button1(BUTTON_1_PIN, 15); // создание объекта — кнопка

По моим наблюдениям применение квалификатора volatile никак не увеличивает длину кода программы.

Сравнение метода обработки сигнала кнопки с библиотекой Bounce.

Существует готовая библиотека для устранения дребезга кнопок Bounce. Проверка состояния кнопки происходит при вызове функции update(). В этой функции:

  • считывается сигнал кнопки;
  • сравнивается с состоянием во время предыдущего вызова update();
  • проверяется, сколько прошло времени с предыдущего вызова с помощью функции millis();
  • принимается решение о том, изменилось ли состояние кнопки.
  • Но это не параллельная обработка сигнала. Функцию update() обычно вызывают в основном, асинхронном цикле программы. Если ее не вызывать дольше определенного времени, то информация о сигнале кнопки будет потеряна. Нерегулярные вызовы приводят к неправильной работе алгоритма.
  • Сама функция имеет достаточно большой код и выполняется намного дольше функций библиотеки Button ().
  • Цифровой фильтрации сигналов по среднему значению там вообще нет.

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

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

Рубрика: . Вы можете добавить в закладки.

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

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

Параллельное подключение устройств к шине SPI

При параллельном подключении несколько ведомых устройств используют общие провода SCLK
, MOSI
и MISO
, при этом каждый ведомый имеет свою линию SS

. Ведущий определяет устройство, с которым осуществляется обмен
, путем формирования низкого сигнала на его SS



.
Видно, что для подключения n
устройств требуется n
линий SS

, то есть для функционирования SPI-среды с n
ведомыми нужно выделить под это n+3
ноги микроконтроллера.

Последовательное подключение устройств к шине SPI

При последовательном подключении устройств они используют общие провода SCLK
и SS

, а выход одного подсоединяется во вход другого. MOSI
ведущего подключается к первому устройству, а MISO
— к последнему. То есть для ведущего на шине SPI это как бы одно устройство.
Такое подключение позволяет построить, например, из двух 8-битных сдвиговых регистров один 16-битный, чем мы сейчас и займемся.
Остается отметить прелесть такого подключения: подключи хоть 3, хоть 8 устройств, это займет всего 4 ноги на контроллере.

Последовательное соединение двух сдвиговых регистров

Еще раз взглняем на сдвиговый регистр 74HC595:

Мы помним, что DS
— есть пин последовательного ввода, а Q0-Q7
пины последовательного вывода. Q7S
, который мы не использовали, когда у нас был всего один регистр в схеме, — это последовательный вывод регистра. Он находит свое применение, когда мы передаем больше 1 байта в регистры. Через этот пин последовательно протолкнутся все байты, предназначенные для последующих регистров, а последний передаваемый байт останется в первом регистре.

Подсоединяя пин Q7S одного первого регистра к пину DS второго (и так далее, если это необходимо), получаем двойной (тройной и т.д.) регистр.

Подключение двойного 7-сегментного дисплея

Двойной 7-семисегментный дисплей это, как правило, устройство с 18-ю ногами, по 9 на каждый символ. Взглянем на схему (мой дисплей имеет маркировку LIN-5622SR и есть большая вероятность того, что его схема подключения окажется уникальна):

Это дисплей с общим анодом, что говорит о необходимости подачи на com1 и com2 высокого уровня ТТЛ, а для зажигания диода — низкий уровень на соответствующей ноге. Если у вас дисплей с общим катодом, делать нужно наоборот!

Подключим дисплей, как показано на схеме:

Левый дисплей подключаем к первому регистру: 1A к ноге Q0, 1B к ноге Q1, 1C к ноге Q2 и т.д. Общий контакт com1 подключаем на землю. Точно так же поступаем с правым дисплеем: 2A к ноге Q0, 2B к ноге Q1, и т.д., общий контакт com2 — на землю.

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

Простая змейка на двойном 7-сегментном дисплее

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

Наш цикл будет состоять из восьми кадров, на каждом из которых будет зажигаться определенные три светодиода. На первом кадре будут гореть 1E, 1F, 1A (см. схему), на втором — 1F, 1A, 2A, на третьем — 1A, 2A, 2B и так далее, на восьмом — 1D, 1E, 1F.

Снова, для удобства, составим табличку байтов, помня, что по умолчанию биты передаются, начиная со старшего, т.е. 2h.

Кадр

1 abcd efgh

2 abcd efgh

hex

0111 0011

1111 1111

EC FF

0111 1011

0111 1111

ED EF

0111 1111

0011 1111

EF CF

1111 1111

0001 1111

FF 8F

1111 1111

1000 1111

FF 1F

1110 1111

1100 1111

7F 3F

1110 0111

1110 1111

7E 7F

1110 0011

1111 1111

7C FF

Ведущий должен выставить низкий (активный) уровень на проводе SS

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

#include <SPI
.h> //подключаем библиотеку SPI

enum { reg = 9 }; //выбираем линию

SS

регистра на 9-м пине Arduino

void setup

()
{

SPI
.begin
(); //инициализируем SPI

//переводим выбранный для передачи пин в режим вывода

pinMode
(reg, OUTPUT
);

}

void loop

()
{

//Заполняем массив байтами, которые будем передавать

static uint8_t digit =

{0xFF,0xCE,0xFF,0xDE,0xFC,0xFE,0xF8,0xFF,

0xF1,0xFF,0xF3,0xF7,0xF7,0xE7,0xFF,0xC7
};

//передаем по два байта из массива и защелкиваем регистры

for
(int
i=0;i<16;i+=2){

digitalWrite
(reg, LOW
);

SPI
.transfer
(digit[i]);

SPI
.transfer
(digit);

digitalWrite
(reg, HIGH
);

delay
(80); //пауза между кадрами

}

}

Видео работы программы:

Параллельные процессы в Arduino

Почему разработчики Arduino уделяют особое внимание примеру Blink without delay ?

Обычно программа Arduino линейна — сначала делает одно, потом другое. В примере выше мы использовали функциюdelay(80)

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

.

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

В Arduino есть штука, которая отсчитывает время с начала работы программы, называется она millis()

. С ее-то помощью и организуется «распараллеливание» задач.

Итоговый проект: часы и хитрая змейка

Соберем такую схему:

Левый и средний регистры у нас работают с точки зрения ведущего как одно устройство, а правый регистр — как другое. Видно, что эти два устройства используют один и тот же провод SCLK
(13-й пин Arduino, провод показан оранжевым) и MOSI
(11-й пин, желтый цвет), SS

используются разные (пины 8 и 9, зеленый цвет). Подключение 7-сегментных дисплеев к регистрам показано для моих конкретных моделей и, вероятно, не будет совпадать с вашим.

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

Последовательность байтов для этой змейки будет такая:

Static
uint8_t snake =

Теперь суть:
функция millis()

сидит и считает миллисекунды от начала начал. В начале каждого цикла loop мы запоминаем значение millis()

в переменную timer.
Заводим переменные snakeTimerPrev
и digitTimerPrev
, которые будут хранить в себе момент предыдущего события: для snakeTimerPrev
— это включение предыдущего кадра анимации змейки, для digitTimerPrev
— включение предыдущей цифры. Как только разница текущего времени (timer
) и предыдущего (snakeTimerPrev
или digitTimerPrev
) становится равна заданному периоду (в нашем случае — 80 и 1000 мс, соответственно), мы производим передачу следующего кадра/байта.

Таким образом,

  • каждые 80 мс контроллер будет опускать сигнал на линии SS

    двойного дисплея, передавать два байта и отпускать линию.
  • каждую секунду контроллер будет опускать сигнал на линии SS

    одиночного дисплея, передавать один байт и отпускать линию.

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

#include <SPI
.h>

enum { snakePin = 9, digitPin = 8 };

unsigned long
timer=0, snakeTimerPrev=0, digitTimerPrev=0;

int
i=0, j=0;

void setup

()
{

SPI.begin();

pinMode(digitPin, OUTPUT
);

pinMode(snakePin, OUTPUT
);

}

void loop

()
{

static
uint8_t digit =

{0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,

0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E};

static
uint8_t snake =

{0xFF,0x9E,0xFF,0xDC,0xFF,0xF8,0xFF,0xF1,

0xFF,0xE3,0xFF,0xA7,0xBF,0xAF,0xBD,0xBF,

0xBC,0xFF,0xDC,0xFF,0xCE,0xFF,0xC7,0xFF,

0xE3,0xFF,0xB3,0xFF,0xBB,0xBF,0xBF,0x9F};

timer=
millis
();

if
(timer-snakeTimerPrev>80){

digitalWrite
(snakePin, LOW
);

SPI.transfer
(snake[j]);

SPI.transfer
(snake);

digitalWrite
(snakePin, HIGH
);

j<30 ? j+=2: j=0;

snakeTimerPrev=timer;

}

if
(timer-digitTimerPrev>1000){

digitalWrite
(digitPin, LOW
);

SPI.transfer
(digit[i]);

Раздел: Без рубрики