Закрыть
Многозадачность и прерывания на Arduino

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

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

Что такое прерывания?

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

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

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

  • Одним из таймеров Arduino, формируя прерывания по таймеру;
  • Изменением состояния одного из входов внешних прерываний;
  • Изменение состояния одного из группы пинов.

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

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

 

Прерывания по таймеру Arduino

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

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

Arduino имеет три таймера: Timer0, Timer1 и Timer2. Timer0 уже настроен для генерации миллисекундных прерываний, обновляя счетчик миллисекунд, передаваемый в millis ().  Это именно то, что нам нужно!

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

Timer0 являющийся 8-битным, считает от 0 до 255 и генерирует прерывание, при переполнении (превышении значения в 255). Он использует тактовый делитель на 64 по умолчанию, чтобы дать нам частоту прерываний 976.5625 Гц (достаточно близко к 1 кГц для наших целей). Мы не будем изменять частоту в Timer0, потому что это точно нарушит работу millis ()!

 

Регистры сравнения

В регистрах сравнения хранятся данные, которые постоянно сравниваются с состоянием таймера/счетчика. Установим регистр сравнения (OCR0A) для генерации другого прерывания где-то в середине этого счета. Приведенный ниже код будет генерировать прерывание TIMER0_COMPA всякий раз, когда значение счетчика проходит 0xAF.

// Timer0 уже используется millis() - мы создаем прерывание где-то
// в середине и вызываем ниже функцию "Compare A"
OCR0A = 0xAF;
TIMSK0 |= _BV(OCIE0A);

Затем определим обработчик прерывания для вектора прерывания по таймеру, называемому TIMER0_COMPA_vect. В этом обработчике прерывания будет делаться все то, что мы ранее делали в loop ().

// Прерывание вызывается один раз в миллисекунду
SIGNAL(TIMER0_COMPA_vect)
{
 unsigned long currentMillis = millis();
 sweeper1.Update(currentMillis);

 //if(digitalRead(2) == HIGH)
 {
  sweeper2.Update(currentMillis);
  led1.Update(currentMillis);
 }
 led2.Update(currentMillis);
 led3.Update(currentMillis);
}

Что оставляет нас с абсолютно пустым loop ()

void loop()
{
}

Здесь мы теперь можем делать все что угодно. Можно даже вернуться к использованию delay ()! Это не затронет наши мигающие светодиоды и вращающиеся сервы. Они будут по-прежнему независимо вызываться один раз в миллисекунду.

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

Показать/скрыть код
#include <Servo.h>

class Flasher
{
 // Переменные-члены класса
 // Устанавливаются при запуске
 int ledPin; // Номер пина со светодиодом
 long OnTime; // длительность ВКЛ в мс
 long OffTime; // длительность ВЫКЛ в мс

 // Текущее состояние
 int ledState; // устанавливает состояние светодиода
 unsigned long previousMillis; // время последнего обновления светодиода

 // Конструктор - создает Flasher
 // и инициализирует переменные-члены
 // и состояние
 public:
 Flasher(int pin, long on, long off)
 {
  ledPin = pin;
  pinMode(ledPin, OUTPUT); 

  OnTime = on;
  OffTime = off;

  ledState = LOW;
  previousMillis = 0;
 }

 void Update(unsigned long currentMillis)
 {
  if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime))
  {
   ledState = LOW; // ВЫКЛ
   previousMillis = currentMillis; // Запомнить время
   digitalWrite(ledPin, ledState); // Обновить состояние светодиода
  }
  else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime))
  {
   ledState = HIGH; // ВКЛ
   previousMillis = currentMillis; // Запомнить время
   digitalWrite(ledPin, ledState); // Обновить состояние светодиода
  }
 }
};

class Sweeper
{
 Servo servo; // объект servo
 int pos; // текущее положение сервы
 int increment; // увеличивать перемещение на каждом интервале
 int updateInterval; // время между обновлениями
 unsigned long lastUpdate; // последнее обновление положения

 public:
 Sweeper(int interval)
 {
  updateInterval = interval;
  increment = 1;
 }

 void Attach(int pin)
 {
  servo.attach(pin);
 }

 void Detach()
 {
  servo.detach();
 }

 void Update(unsigned long currentMillis)
 {
  if((currentMillis - lastUpdate) > updateInterval) // пришло время обновляться
  {
   lastUpdate = millis();
   pos += increment;
   servo.write(pos);
   if ((pos >= 180) || (pos <= 0)) // конец вращения
   {
    // обратное направление
    increment = -increment;
   }
  }
 }
};

Flasher led1(11, 123, 400);
Flasher led2(12, 350, 350);
Flasher led3(13, 200, 222);

Sweeper sweeper1(25);
Sweeper sweeper2(35);

void setup()
{
 sweeper1.Attach(9);
 sweeper2.Attach(10);

 // Timer0 уже используется millis() - прерываемся где-то
 // посередине и вызываем ниже функцию "Compare A"
 OCR0A = 0xAF;
 TIMSK0 |= _BV(OCIE0A);
} 

// Прерывание вызывается один раз в миллисекунду,
// ищет любые новые данные, и сохраняет их
SIGNAL(TIMER0_COMPA_vect)
{
 unsigned long currentMillis = millis();
 sweeper1.Update(currentMillis);

 if(digitalRead(2) == HIGH)
 {
  sweeper2.Update(currentMillis);
  led1.Update(currentMillis);
 }

 led2.Update(currentMillis);
 led3.Update(currentMillis);
}
void loop()
{
}

 

Внешние прерывания

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

На используемой мной Arduino Mega 2560 имеется 6 пинов, обрабатывающих внешние прерывания (на Arduino UNO таких входов только 2). В этом примере я подключу кнопку к одному из них и использую ее для сброса сервоприводов.

Во-первых, давайте добавим функцию reset () в наш класс Sweeper.  Функция reset () устанавливает положение в ноль и немедленно перемещает туда серву.

void reset()
{
 pos = 0;
 servo.write(pos);
 increment = abs(increment);
}

Затем, добавим вызов функции attachInterrupt (), соединяющую внешнее прерывание с обработчиком. На Arduino Mega 2560, также как и на Arduino UNO, Inerrupt 0 ассоциировано с цифровым пином 2. Мы говорим, что ждем «спада» фронта сигнала на этом выводе. При нажатии кнопки сигнал «спадает» от высокого до низкого уровня и вызывается обработчик прерывания Reset.

pinMode(2, INPUT_PULLUP);
attachInterrupt(0, Reset, FALLING);

И здесь есть обработчик прерывания. Он просто вызывает функции, сбрасывающие сервы.

void Reset()
{
 sweeper1.reset();
 sweeper2.reset();
}

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

Схема у нас та же, что и в прошлый раз.

multitasking_scheme04

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

Показать/скрыть код
#include <Servo.h>

class Flasher
{
 // Переменные-члены класса
 // Устанавливаются при запуске
 int ledPin; // Номер пина со светодиодом
 long OnTime; // длительность ВКЛ в мс
 long OffTime; // длительность ВЫКЛ в мс

 // Текущее состояние
 int ledState; // устанавливает состояние светодиода
 unsigned long previousMillis; // время последнего обновления светодиода

 // Конструктор - создает Flasher
 // и инициализирует переменные-члены
 // и состояние
 public:
 Flasher(int pin, long on, long off)
 {
  ledPin = pin;
  pinMode(ledPin, OUTPUT);

  OnTime = on;
  OffTime = off;

  ledState = LOW;
  previousMillis = 0;
 }

 void Update(unsigned long currentMillis)
 {
  if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime))
  {
   ledState = LOW; // ВЫКЛ
   previousMillis = currentMillis; // Запомнить время
   digitalWrite(ledPin, ledState); // Обновить состояние светодиода
  }
  else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime))
  {
   ledState = HIGH; // ВКЛ
   previousMillis = currentMillis; // Запомнить время
   digitalWrite(ledPin, ledState); // Обновить состояние светодиода
  }
 }
};

class Sweeper
{
 Servo servo; // объект servo
 int pos; // текущее положение сервы
 int increment; // увеличивать перемещение на каждом интервале
 int updateInterval; // время между обновлениями
 unsigned long lastUpdate; // последнее обновление положения

 public:
 Sweeper(int interval)
 {
  updateInterval = interval;
  increment = 1;
 }

 void Attach(int pin)
 {
  servo.attach(pin);
 }

 void Detach()
 {
  servo.detach();
 }

 void reset()
 {
  pos = 0;
  servo.write(pos);
  increment = abs(increment);
 }

 void Update(unsigned long currentMillis)
 {
  if((currentMillis - lastUpdate) > updateInterval) //пришло время обновляться
  {
   lastUpdate = millis();
   pos += increment;
   servo.write(pos);
   if ((pos >= 180) || (pos <= 0)) // конец вращения
   {
    // обратное направление
    increment = -increment;
   }
  }
 }
};

Flasher led1(11, 123, 400);
Flasher led2(12, 350, 350);
Flasher led3(13, 200, 222);

Sweeper sweeper1(25);
Sweeper sweeper2(35);

void setup()
{
 sweeper1.Attach(9);
 sweeper2.Attach(10);

 // Timer0 уже используется millis() - прерываемся где-то
 // посередине и вызываем ниже функцию "Compare A"
 OCR0A = 0xAF;
 TIMSK0 |= _BV(OCIE0A);

 pinMode(2, INPUT_PULLUP);
 attachInterrupt(0, Reset, FALLING);
}

void Reset()
{
 sweeper1.reset();
 sweeper2.reset();
}

// Прерывание вызывается один раз в миллисекунду,
// ищет любые новые данные, и сохраняет их
SIGNAL(TIMER0_COMPA_vect)
{
 unsigned long currentMillis = millis();
 sweeper1.Update(currentMillis);

 // if(digitalRead(2) == HIGH)
 {
  sweeper2.Update(currentMillis);
  led1.Update(currentMillis);
 }

 led2.Update(currentMillis);
 led3.Update(currentMillis);
}
void loop()
{
}

И результат

Библиотеки для работы с прерываниями

В сети можно найти несколько библиотек для работы с таймерами. Многие просто постоянно опрашивают millis () так же, как мы делали это в прошлый раз. Но есть некоторые, которые позволяют настроить таймеры для генерации прерываний.

Отличные библиотеки TimerOne и TimerThree от Paul Stoffregan дают множество низкоуровневых возможностей для конфигурирования прерываний по таймеру. Библиотека TimerThree не работает на Arduino UNO.  Ее можно использовать с Leonardo, Mega 2560 и некоторыми платами Teensy.

Arduino UNO имеет только два входа внешних прерываний. Но что делать если требуется более двух входов? К счастью Arduino UNO поддерживает прерывания «pin-change» (по изменению входа) для всех пинов.

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

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

 

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

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

Если все имеет высокий приоритет, значит высокого приоритета нет ни у чего

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

Одно прерывание в каждый момент времени

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

  • Работа, выполняемая в функции обработки прерывания должна быть короткой, чтобы не пропустить не одного прерывания.
  • Код, выполняемый в функции обработки прерывания не должен вызывать ничего такого, что требовало бы, чтобы прерывания были активны (например, delay () , или что-нибудь, использующее шину I2C). Это приведет к зависанию программы.

Отложите длительную обработку в loop ()

Если вам необходимо произвести обширную обработку в ответ на прерывание, используйте обработчик прерывания для того, чтобы сделать то, что необходимо, а затем установите переменную состояния volatile, показывающую, что дальнейшая обработка не требуется. При вызове функции Update () из loop () проверьте переменную состояния, на предмет того, не требуется ли какая-либо последующая обработка.

Проверка перед переконфигурированием таймера

Таймеры являются ограниченным ресурсом. На Arduino UNO их всего 3, и они используются для многих вещей. Если вы запутались с конфигурацией таймера, некоторые вещи могут перестать работать. Например, на Arduino UNO:

  • Timer0 - используется millis (), micros (), delay () и ШИМ на пинах 5 и 6
  • Timer1 - используется для Servo, библиотеки WaveHC и ШИМ на пинах 9 и 10
  • Timer2 - используется Tone и ШИМ на пинах 11 и 13

Безопасное совместное использование данных

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

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

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

[add_ratings]

25 thoughts on “Многозадачность и прерывания на Arduino

  1. Очень познавательная статья.

    Есть вопрос: Как будет работать программа когда переполнится и обнулится millis ()?

    1. Хороший вы задали вопрос, Валерий! Примерно через 50 дней программа работать перестанет: счетчик millis () обнулится и условия if ((currentMillis — lastUpdate) > updateInterval), (currentMillis — previousMillis >= OnTime) и (currentMillis — previousMillis >= OffTime) возможно никогда больше и не выполнятся. Чтобы этого избежать при условии, что не сильно критичен единичный сбой раз в 50 дней в длительности обновления состояний, то можно сравнивать не разность на превышение заданных интервалов, а модуль разностей. Например, для первого условия это будет выглядеть следующим образом:

      if(abs(currentMillis - lastUpdate) > updateInterval) { ... }

      Если же даже сбой один раз в 50 дней в работе программы неприемлем, то необходимо отдельно обработать событие переполнения счетчика, учитывая, что переменная типа unsigned long может принимать значения от 0 до (2^32-1).

      1. Добрый день всем.

        Я не волшебник я только учусь.

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

        вы написали ...

        как одно из решений

        if (abs (currentMillis — lastUpdate) > updateInterval)

        {

        ...

        }

        то есть, если я правильно понял — один раз в 50 дней доходим до максимального значения МИЛИСЕК-мах -потом сбрасывание в ноль будет «пропуск» одного интервала? ( если я правильно понял).

        а что если написать

        long interval = 1000;

        .

        .

        if (millis () % interval == 0 )

        {

        ...

        }

        все будет работать также ??

        1. Николай, проблема тут вот в чем. Ход мыслей у вас правильный, но операция взятия отстатка от деления работает только с числами типа int (оба операнда должны быть типа int). Функция millis возвращает значение типа unsigned long. В комментарии к другой статейке, также посвященной многозадачности, я предложил одно из возможных вариантов отслеживания сброса счетчика.

          1. Странно. У меня отлично работает операция взятия остатка (%), в случае, когда первый операнд типа unsigned long, а второй константа.

            Например:

            // Счетчики смены состояний unsigned long switchsCount; // Счетчик смены состояний unsigned long circlesCount; // Счетчик полных оборотов ... this->switchsCount++; if ((this->switchsCount % 4) == 0) { this->circlesCount++; }

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

          2. Андрей, не вводите людей в заблуждение!

            1) Все арифметические операции (сложение, вычитание, умножение, деление, деление по модулю) над всеми числами разрешены и поддерживаются. Это МК не поддерживает ряд операций, в частности деление и деление по модулю, а также операции с плавающей точкой, но Вы же пишите на Си/Си++, а здесь вся арифметика выполняется даже если она аппаратно не поддерживается (попросту эмулируется, точнее реализуется программно).

            2) выражение

            if(currentMillis - lastUpdate) > updateInterval))

            работает всегда, даже когда «millis переполняется» т.к. currentMillis и lastUpdate объявлены как unsigned т.е. беззнаковые, они по определению не могут быть отрицательными. В результате операции (currentMillis — lastUpdate) у вас получится положительное число (это магия переполнения 🙂 другое дело, что у вас происходит сравнение знакового и беззнакового, это не хорошо (наврерняка компилятор указал вам на это), ну и в теории результат вычитания может быть больше разрядной сетки updateInterval (это же int), т.е. максимальный интервал ~32секунды.

      2. Андрей, спасибо за невероятно познавательные и полезные статьи и ответы!

        Скажите. правильно ли я понимаю, что millis переполняется только при месяце непрерывной работы, без выключения питания? Или речь о месяце вообще, при обычном режиме включения-выключения?

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

  2. Есть такая функция

    void useInterrupt(boolean v) { if (v) { OCR0A = 0xAF; TIMSK0 |= _BV(OCIE0A); } else { // do not call the interrupt function COMPA anymore TIMSK0 &= ~_BV(OCIE0A); } }

    Как её переделать для Arduino DUE? Там ведь таймеры 32-бит...

    1. Для Arduino DUE работа с таймерами и прерываниями значительно отличается от описанного выше и выходит за рамки этой статьи. Могу порекомендовать вам ознакомится с осуждением работы с таймерами Arduino DUE на форуме arduino.cc. Пример кода взятый оттуда:

      volatile boolean l; //TC1 ch 0 void TC3_Handler() { TC_GetStatus(TC1, 0); digitalWrite(13, l = !l); } void startTimer(Tc *tc, uint32_t channel, IRQn_Type irq, uint32_t frequency) { pmc_set_writeprotect(false); pmc_enable_periph_clk((uint32_t)irq); TC_Configure(tc, channel, TC_CMR_WAVE | TC_CMR_WAVSEL_UP_RC | TC_CMR_TCCLKS_TIMER_CLOCK4); uint32_t rc = VARIANT_MCK/128/frequency; //128 because we selected TIMER_CLOCK4 above TC_SetRA(tc, channel, rc/2); //50% high, 50% low TC_SetRC(tc, channel, rc); TC_Start(tc, channel); tc->TC_CHANNEL[channel].TC_IER=TC_IER_CPCS; tc->TC_CHANNEL[channel].TC_IDR=~TC_IER_CPCS; NVIC_EnableIRQ(irq); } void setup() { pinMode(13,OUTPUT); startTimer(TC1, 0, TC3_IRQn, 4); //TC1 channel 0, the IRQ for that channel and the desired frequency } void loop(){}

  3. Здравствуйте! Подскажите пожалуйста почему неадекватно работает функция analogRead () во внешнем прерывании int0?

  4. особенно полезно последние предупреждения 🙂

    я почти что решил перейти на прерывания вместо отслеживания через millis ()

  5. > Помните, что прерывания отключены, пока вы находитесь в обработчике прерывания. Если вы попытаетесь сделать слишком много на уровне прерывания, вы получите худший ответ на другие прерывания.

    Это ошибочное утверждение.

    Позволю процитировать уважаемого DI HALT:

    что будет если во время обработки одного прерывания придет другое? Оно потеряется?

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

    (easyelectronics.ru/avr-uc... -si-chast-3.html)

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

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

  8. Опечатка:

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

  9. При нажатие на кнопку, получается следующиее, когда сервы не вернулись на в позицию «ноль» то светодиоды не работают, вернее не много сбиваются с ритма?

  10. Добрый день! Подскажите новичку, пожалуйста!

    Пишу код для определения скорости вращения. Датчик — оптрон с обвязкой, покупной. Этот код:

    #include OLED myOLED(SDA, SCL, 8); extern uint8_t SmallFont[]; extern uint8_t BigNumbers[]; volatile unsigned long oldTime = 0; volatile unsigned long newTime = 0; volatile byte newData = 0; float i = 0; float s = 0; void pinChange () { oldTime = newTime; newTime = millis(); newData = 1; } void setup() { Serial.begin(115200); myOLED.begin(); myOLED.setFont(BigNumbers); attachInterrupt (0, pinChange, RISING); // подключаем обработчик прерывания } void loop() { if (newData == 1) { i = newTime - oldTime; s = 1000 / i; Serial.print (newTime); Serial.print (" - "); Serial.print (oldTime); Serial.print (" = "); Serial.println (newTime - oldTime); Serial.println (i); Serial.println (s); Serial.println (); myOLED.clrScr(); myOLED.setFont(BigNumbers); myOLED.printNumF(s, 2, CENTER, 16); myOLED.setFont(SmallFont); myOLED.printNumF(i, 2, CENTER, 0); myOLED.printNumI(newTime, CENTER, 48); myOLED.update(); newData = 0; } }

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

    248508 - 248508 = 0 0.00 inf 249473 - 249474 = 0 0.00 inf 249521 - 249521 = 0 0.00 inf

    Особенно интересно второе значение, 249473 — 249474 = 0 КАК?

    Откуда берутся inf в результатах и как заставить код работать?

    1. Откуда берутся inf в результатах

      А что Вы ожидали увидеть при делении на 0?

      i = newTime - oldTime;надо бы привести к нужному типу

      i = (float)newTime - (float)oldTime; или i = (float)(newTime - oldTime);

      Вообще Ваш код должен в первый раз выдать значение newTime, ведь oldTime будет равно нулю.

    2. Константин, 249473 — 249474 = 0 КАК? это не ошибка, это тонкость, которую нужно знать, корни ее лежат в атомарности операции (точнее в данном случае в ее отсутсвии).

      У вас переменные oldTime и newTime объявлены как unsigned long, т.е. занимают 4 байта и они используются как в «фоне» (loop), так и в прерывании (pinChange). Не знаю с какой частотой у вас идут прерывания, но прерывания (вызов pinChange может произойти в любой момент выполнения любой команды в loop), и получается что пока выводилась первая строчка, значения переменной(ых) узменились... Но чтобы такого не происходило, есть один способ:

      void loop(){ if (newData == 1) { //запрещаем прерывания на момент сохранения значений переменных noInterrupts(); unsigned long new_t = newTime; unsigned long old_t = oldTime; //разрешаем прерывания interrupts(); // и дальше обращаться уже не непосредственно к // oldTime и newTime, а уже к их копиям ... }

      А что за датчик? Название, ссылка?

    3. Константин,

      Откуда берутся inf в результатах и как заставить код работать?

      это результат вычислений:

      i = newTime — oldTime; (равно нулю)

      s = 1000 / i; (равно бесконечности, т.к. вообще то на ноль делить нельзя...) поэтому выводится «inf» вместо бесконечности

      хотя, тут скорее ошибка с вашей стороны, «s» это же наверное секунды?

      может все таки s = i / 1000; ???

      если все верно то вероятно нужно обрабатывать этот случай (деление на ноль) по особому...

  11. TIMSK0 |= _BV(OCIE0A);

    А как входить в прерывание не каждую миллисекунду, а пореже, например, каждые 8 или 16 мс?

Оставить ответ

Ваш email не будет опубликован.Обязательны поля помечены *