Разблокировка концевого выключателя в Arduino ISR с задержками

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

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

const int lsOuterLeftIn = 18; // lsOuterLeftIn is my Limit Switch
const int LED = 9;
volatile bool lsEngaged = false; // flag for limit switch engaged
void setup() {
    pinMode(lsOuterLeftIn, INPUT_PULLUP);
    pinMode(LED, OUTPUT);
    attachInterrupt(digitalPinToInterrupt(lsOuterLeftIn), ISR1, FALLING);
    attachInterrupt(digitalPinToInterrupt(lsOuterLeftIn), ISR2, RISING);
}
void  loop() {
    if (lsEngaged) digitalWrite(LED, HIGH);
    else digitalWrite(LED, LOW);
}
void ISR1(){
    delay(100);
    lsEngaged = (digitalRead(lsOuterLeftIn));
}
void ISR2(){
    delay(100);
    lsEngaged = (digitalRead(lsOuterLeftIn));
}

Но вот моя проблема. Я наткнулся на эту страницу документации Arduino, и там написано

"Так как delay () требует прерываний для работы, он не будет работать, если вызывается внутри ISR".

Но я использую delay() внутри ISR и похоже на работу, что происходит? Есть ли у меня ситуация, когда все работает в данный момент, но может легко сломаться, потому что delay() Функция может работать со мной, как указано в документации?

3 ответа

Решение

Ответ TomKeddie выглядит правильно: у вас не будет никаких проблем. В любом случае, по моему мнению, ваш код концептуально неверен по крайней мере по двум причинам. Теперь я объясню почему.

Существует два вида входных данных: те, на которые вы должны ответить НЕМЕДЛЕННО, и те, на которые вы должны ответить, но они не являются непосредственными угрозами. Например, обычно защитная заглушка попадает в первую группу, так как, как только вы нажмете на нее, вам нужно остановить привод. Кнопки пользовательского интерфейса, с другой стороны, попадают во вторую группу, так как вам не нужно сразу отвечать на них.

ПРИМЕЧАНИЕ: в хорошо выполненной программе вы обычно можете ответить на вход второго типа в течение десятой доли миллисекунды, поэтому пользователь никогда не увидит задержек.

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

#define CHECK_EVERY_MS 20
#define MIN_STABLE_VALS 5

unsigned long previousMillis;
char stableVals;

...

void  loop() {
    if ((millis() - previousMillis) > CHECK_EVERY_MS)
    {
        previousMillis += CHECK_EVERY_MS;
        if (digitalRead(lsOuterLeftIn) != lsEngaged)
        {
            stableVals++;
            if (stableVals >= MIN_STABLE_VALS)
            {
                lsEngaged = !lsEngaged;
                stableVals = 0;
            }
        }
        else
            stableVals = 0;
    }

    ...
}

Это будет проверять каждые 20 мс, если значение изменилось. Однако значение обновляется только в том случае, если оно стабильно более 5 циклов (т. Е. 100 мс).

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

Если, с другой стороны, ваш ввод представляет серьезную угрозу для вашего устройства (например, конечная остановка), вы ДОЛЖНЫ ответить как можно скорее. Если это ваш случай, вы ждете 100 мс, прежде чем ответить, и это противоречит необходимости быстрого ввода данных.

Конечно, вы НЕ МОЖЕТЕ опровергнуть такой ввод, так как опровержение приводит к задержкам. Вы можете, однако, отдать предпочтение одному государству над другим. В случае соединения с заземлением на конечную остановку серьезная угроза возникает, когда входное состояние заземлено. Поэтому я предлагаю вам установить переменную таким образом, чтобы:

  1. когда пин-код идет вниз, вы сразу же установите его на 0
  2. когда пин-код повышается, вы ждете 100 мс (в основной петле), а затем настраиваете его.

Код для этого выглядит примерно так:

#define CHECK_EVERY_MS 20
#define MIN_STABLE_VALS 5

unsigned long previousMillis;
char stableVals;

attachInterrupt(digitalPinToInterrupt(lsOuterLeftIn), ISR1, FALLING);

...

void  loop() {
    if ((millis() - previousMillis) > CHECK_EVERY_MS)
    {
        previousMillis += CHECK_EVERY_MS;
        if ((digitalRead(lsOuterLeftIn) == HIGH) && (lsEngaged == LOW))
        {
            stableVals++;
            if (stableVals >= MIN_STABLE_VALS)
            {
                lsEngaged = HIGH;
                stableVals = 0;
            }
        }
        else
            stableVals = 0;
    }

    ...
}

void ISR1()
{
    lsEngaged = LOW;
}

Как вы можете видеть, ЕДИНСТВЕННОЕ прерывание является падающим, и НАИБОЛЕЕ ВАЖНО, оно очень короткое.

Если вам нужно выполнить другие инструкции, такие как остановка двигателя, вы можете использовать функцию ISR1 (ЕСЛИ они довольно короткие).

Просто помните: ISR ДОЛЖНЫ быть как можно короче, так как, когда микроконтроллер находится в одном из них, он становится слепым ко всему остальному

На AVR задержка () реализована, как показано ниже. В этом нет прерываний (micros () возвращает значение счетчика timer0, yield () относится к планировщику, который не будет использоваться в вашем простом наброске).

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

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

С https://github.com/arduino/Arduino/blob/79f5715c21a81743443269a855979a64188c93df/hardware/arduino/avr/cores/arduino/wiring.c

void delay(unsigned long ms)
{
    uint16_t start = (uint16_t)micros();

    while (ms > 0) {
        yield();
        if (((uint16_t)micros() - start) >= 1000) {
            ms--;
            start += 1000;
        }
    }
}

Проще , чем использовать временные метки

volatile bool buttonDirty = false;

void setup() {
    attachInterrupt(digitalPinToInterrupt(buttonPin), buttonPress, FALLING);
}


void loop() {
  while(1){
    readButtons();
  }
}

void buttonPress(){
  if(buttonDirty) return;
  buttonDirty = true;  
}

void readButtons(){
  if(!buttonDirty) return;
  delay(100);
  ...........

}

Похоже, вы можете сэкономить 100 мс времени реакции на включение коммутатора.

Итак, если вам не нужно реагировать в течение микросекунд с события, рассмотрите возможность опроса ввода каждые, скажем, 10 мс (например, от таймера ISR).

(Есть только две причины для использования внешнего прерывания: 1. вам нужно реагировать на сигнал очень быстро (мкс!), Или 2. вам нужно проснуться из режима глубокого энергосбережения, когда таймеры не активны. все остальное вы можете пойти на опрос по таймеру.)

псевдокод:

#define STABLE_SIGNAL_DURATION 5

uint8_t button_time_on = 0;

volatile bool button_is_pressed = false;

...
// Every 10ms do (can be done in a timer ISR):

if ( read_button_input() == ON ) {

  if ( button_time_on >= STABLE_SIGNAL_DURATION ) {

    button_is_pressed = true;

  } else {
     button_time_on++;
  }

} else {
  button_time_on = 0; // button not pressed (any more).
  button_is_pressed = false;
}

...

И в main():

bool button_press_handled = false;

while(1) {
  // do your other main loop stuff...

  button_press_handled = button_press_handled && button_is_pressed;

  if ( !button_press_handled && button_is_pressed ) {

    // Handle press of the button

    // ...

    // Note that we handled the event for now:
    button_press_handled = true;
  }
}
Другие вопросы по тегам