прерывание с кнопки и ожидание, пока на последовательный порт 1 поступит сообщение

Я немного растерян, потому что учусь использовать прерывания на Arduino. Я создаю программу для считывания RFID-кода, который приходит, если RFID-передатчик находится близко к антенне (маленькая антенная плата RFID). У меня есть два режима работы: один, в котором я связываю свою плату с RFID, поэтому я буду действовать только там, где мой RFID-код идентифицирован (я сохраняю RFID-код на плате). и другой режим, в котором я считываю все RFID-коды и сохраняю их.

У меня есть один шаг конфигурации, где если пользователь нажимает кнопку, я хочу, чтобы светодиод мигал, чтобы указать, что мы ждем представления кода RFID для привязки к плате. Я пытаюсь создать код, и у меня возникают некоторые проблемы с выходом из состояния, когда я жду кода RFID (serial1).

Вот мой код:

#include <Wire.h>

//const int reception_rfid = 15;
const int btnAssociation = 2; // кнопка для индикации включения устройства RFID (прерывание)
const int SwitchModeConfig = 4;
const int LedAssociationEnCours = A0;

int compteurLedAsso;

const int BUFFER_SIZE_RFID = 13;
char buf[BUFFER_SIZE_RFID];

void setup() {
  Wire.begin(8);                // присоединяемся к шине i2c с адресом #8
  Wire.onRequest(requestEvent); // регистрируем событие
  Serial.begin(9600);
  Serial1.begin(9600);
  pinMode(SwitchModeConfig, INPUT_PULLUP); 
  pinMode(btnAssociation, INPUT_PULLUP);
  pinMode(LedAssociationEnCours, OUTPUT);
  ConfigurationAssociation(); 
  digitalWrite(LedAssociationEnCours, HIGH);
}

void loop() {
  delay(100);
}


void ConfigurationAssociation(){
  int stateSwitch = digitalRead(SwitchModeConfig);
  if(stateSwitch == LOW){ 
      attachInterrupt(digitalPinToInterrupt(btnAssociation), InterruptAssociation, RISING);
      Serial.println("Mode association");
    }else{ 
      detachInterrupt(digitalPinToInterrupt(btnAssociation)); 
      Serial.println("Mode aleatoire");
    }
}


void InterruptAssociation(){
  compteurLedAsso = 0;
    Serial.println("Ouverture de l'Association d'un code RFID");
    while (Serial1.available() == 0 && compteurLedAsso <= 600) 
    {
      // мигаем красным светодиодом на A0
      digitalWrite(LedAssociationEnCours, LOW);
      delay(5000);
      digitalWrite(LedAssociationEnCours, HIGH);
      delay(5000);
      compteurLedAsso++;
      Serial.println(compteurLedAsso);
    }

    if(Serial1.available() > 0){
      int codeRFID = Serial1.readBytes(buf, BUFFER_SIZE_RFID);
      Serial.println("CodeRFID: ");
      for(int i = 0; i < codeRFID; i++)
        Serial.print(buf[i]);
    }
      //Serial.println(compteurLedAsso);
      //compteurLedAsso++;
    Serial.println("Fin de l'attente code RFID");
}


void requestEvent() {

}
  1. Первая проблема: я знаю, что когда мы находимся внутри прерывания, мы не можем считать время, прошедшее некоторое время, я хотел бы, чтобы светодиод мигал 2 минуты: ожидание представления RFID-кода для сохранения, и как только 2 минуты пройдут, я выхожу из прерывания. Как заставить ждать 2 минуты RFID-код на порту serial1 внутри прерывания? моя приемная плата RFID работает нормально, а TX подключен к RX1 (контакт 19) Arduino mega (я проверяю на другом коде, где работает чтение RFID)

  2. Вторая проблема: когда моя плата находится внутри прерывания, которое поступает от кнопки (подключенной к контакту 2), я всегда остаюсь в цикле while, даже когда мой RFID-передатчик считывает RFID-код, я не могу выйти и не знаю почему! Может быть, вы можете помочь разобраться и найти решение, пожалуйста?

, 👍2


2 ответа


Лучший ответ:

3

Дело в том, что внутри ISR (Interrupt Service Routine) не только измерение времени с помощью millis() не работает, delay() также не будет работать (он использует тот же механизм прерываний, что и millis() в фоновом режиме). Как и все остальное, что зависит от их собственного ISR для выполнения — например, получение последовательных данных. Аппаратный последовательный интерфейс может получать отдельные байты, не завися от прерываний, но чтобы поместить эти байты в буфер (и фактически использовать их через Serial), вам нужно разрешить выполнение других ISR.

НИКОГДА не следует писать ISR, который выполняется долго. Держите его в микросекундах, максимум в очень низком диапазоне миллисекунд. То, как долго вы сможете дойти до этого, пока не увидите негативные эффекты, зависит от других факторов. Вам действительно следует выполнять только минимальную работу в ISR, а остальное в основном коде.

Общая структура кода для этого выглядит следующим образом:

  • В вашем основном коде вы не используете никаких вызовов delay(). Все хронометрированное должно быть сделано с помощью millis() (как в примере BlinkWithoutDelay, который поставляется с Arduino IDE), чтобы это не блокировало выполнение. Также никаких длинных циклов внутри loop(). Убедитесь, что ваша функция loop() может свободно и быстро итерироваться.
  • В глобальной области видимости вы определяете переменную-флаг. Простая однобайтовая переменная, помеченная как volatile.
  • В вашем ISR вы ничего не делаете, кроме установки этой переменной. В зависимости от ситуации вы можете захотеть установить ее в зависимости от цифрового входа через digitalRead(), что все еще быстро, так что это нормально.
  • В вашем основном коде вы используете оператор if для проверки установки этой переменной-флага. В большинстве итераций loop() это не будет выполнено. Только если переменная-флаг была установлена внутри ISR, она будет выполнена. Это место, где вы пишете код, обрабатывающий событие. В конце блока вам нужно только сбросить переменную-флаг, чтобы вы были готовы к следующему прерыванию.

Это будет выглядеть примерно так:

int button_pin = 2;
volatile bool button_flag = false;

void setup(){
  pinMode(button_pin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(button_pin), button_ISR, RISING);
}

void loop(){
  if(button_flag){
    // Обработка прерывания здесь
    button_flag = false; // Сбросить переменную флага, чтобы быть готовым к следующему прерыванию
  }
}

void button_ISR(){
  button_flag = true;
}

Обратите внимание, что переменная button_flag определена как volatile, поэтому компилятор не кэширует ее значение в каком-либо регистре, а вместо этого всегда считывает ее фактическое текущее значение (не оптимизируя его).

В вашем коде вам, вероятно, следует использовать другую концепцию кодирования, называемую конечным автоматом (FSM). Это самый простой способ реализовать код с различными состояниями/стадиями, сохраняя при этом его неблокируемость. Вы можете почитать о FSM в Интернете (или, например, в моем ответе на этот вопрос).

Суть в том, что вы используете переменную, которая представляет состояние, в котором в данный момент находится ваш код. В loop() вы затем выполняете код только для текущего состояния. Изменение в другое состояние осуществляется путем установки переменной состояния. Состояние, которое требуется вашему текущему коду, вероятно, CHECK_FOR_SINGLE_RFID_TAG и ASSOCIATE_SINGE_RFID_TAG (хотя вы можете назвать их так, как хотите). Простой FSM с этим может выглядеть следующим образом:

#define CHECK_FOR_SINGLE_RFID_TAG 0
#define ASSOCIATE_SINGLE_RFID_TAG 1

// однобайтовая переменная, поэтому нам не нужно беспокоиться об атомарных чтениях/записях
uint8_t state = CHECK_FOR_SINGLE_RFID_TAG;

void loop(){
  switch(state){
    case CHECK_FOR_SINGLE_RFID_TAG:
      if(Serial1.available()){
        // здесь считываем Serial1, сверяем идентификатор RFID-метки с сохраненным
        // и выполнить код соответствующим образом
      }
      break;
    
    case ASSOCIATE_SINGLE_RFID_TAG:
      if(Serial1.available()){
        // здесь считываем Serial1 и сохраняем новый идентификатор RFID-метки в переменной
        // затем сбросьте переменную состояния, чтобы автоматически вернуться в режим проверки
        // после регистрации новой RFID-метки
        state = CHECK_FOR_SINGLE_RFID_TAG;
      }
      // Используйте идиому BlinkWithoutDelay для неблокируемого мигания светодиода
      // что-то вроде
      // если (millis() - led_timestamp > led_interval) {
      // digitalWrite(led, !digitalRead(led));
      // }
      break;
  }
}

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


Примечание: я использовал переменную типа uint8 _t (один байт), чтобы сделать это прерывание безопасным (вы можете прочитать об этом, выполнив поиск по запросу arduino interrupt atomic read или arduino interrupt safe variable) для следующей части и определений, чтобы дать каждому значению читаемое имя. Вы можете использовать enum для этого, хотя во время написания этого я наткнулся на размер enum, который, кажется, составляет 16 бит на AVR (например, Arduino Mega), если вы не используете определенный флаг компилятора, согласно этой статье в блоге. Это объяснение сейчас не так уж и важно для вас, но я также хотел объяснить, почему здесь не используются перечисления.


Теперь мы можем объединить это с нашим кодом прерывания выше. В этом случае нам больше не нужна отдельная переменная флага. Мы можем напрямую установить переменную state внутри ISR:

#define CHECK_FOR_SINGLE_RFID_TAG 0
#define ASSOCIATE_SINGLE_RFID_TAG 1

volatile uint8_t state = CHECK_FOR_SINGLE_RFID_TAG;

int button_pin = 2;

void setup(){
  pinMode(button_pin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(button_pin), button_ISR, RISING);
  Serial.begin(115200);
  Serial1.begin(115200);
}

void button_ISR(){
  state = ASSOCIATE_SINGLE_RFID_TAG;
}

void loop(){
  switch(state){
    case CHECK_FOR_SINGLE_RFID_TAG:
      if(Serial1.available()){
        // здесь считываем Serial1, сверяем идентификатор RFID-метки с сохраненным
        // и выполнить код соответствующим образом
      }
      break;
    
    case ASSOCIATE_SINGLE_RFID_TAG:
      if(Serial1.available()){
        // здесь считываем Serial1 и сохраняем новый идентификатор RFID-метки в переменной
        // затем сбросьте переменную состояния, чтобы автоматически вернуться в режим проверки
        // после регистрации новой RFID-метки
        state = CHECK_FOR_SINGLE_RFID_TAG;
      }
      // Используйте идиому BlinkWithoutDelay для неблокируемого мигания светодиода
      // что-то вроде
      // если (millis() - led_timestamp > led_interval) {
      // digitalWrite(led, !digitalRead(led));
      // }
      break;
  }
}

Вы можете видеть, что button_ISR() очень маленький, поэтому очень быстрый. И наш loop() также работает без блокировки, поэтому его следующая итерация будет очень быстрой, и затем он выполнит состояние, которое мы установили в ISR.

Примечание: при чтении из Serial убедитесь, что вы не используете функции, которые блокируют на долгое время. Например, Serial.readString() займет 1 с для выполнения (так как это тайм-аут по умолчанию), что заблокирует неблокируемый код (нехорошо).

,

Привет! ух ты!! Большое спасибо за все эти супер объяснения, очень полные и понятные! :) Я собираюсь прочитать все и посмотреть примеры FSM, потому что я ничего об этом не знаю, если честно. Я вернусь с новостями или, может быть, с ответами по моей проблеме, если вы не против. Еще раз спасибо за вашу помощь :), @Aeva

*Я вернусь с новостями или, может быть, ВОПРОСАМИ :), @Aeva

Я вернулся с новостями: благодаря вашей помощи @chrisl и EdgarBonet я понял, как сделать FSM, чтобы улучшить мой код Arduino. Он работает! Еще раз огромное спасибо вам обоим! :), @Aeva


3

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

  • ISR, вероятно, не должен длиться более нескольких десятков микросекунд, и много плохих вещей может произойти во время 1 мс ISR (пропущенный тактовый сигнал) тики, потерян последовательный ввод...)
  • итерация loop() не должна занимать больше нескольких миллисекунд
  • нет смысла использовать прерывание для чтения кнопки: пользователь будет удерживать кнопку нажатой в течение многих миллисекунд, и у вас будет много возможностей для его перехвата в loop(); прерывания приносят сложность, которая имеет смысл только тогда, когда вам приходится иметь дело с время менее миллисекунды
  • не беспокойтесь о размере перечисления, если только вам не нужно беспокоиться о субмикросекундные тайминги

Теперь, мой взгляд на эту проблему. Первый шаг — определить состояние машина. Я могу назвать состояния НОРМАЛЬНОЕ и АССОЦИАЦИЯ. переходы будут такими:

  • НОРМАЛЬНОЕАССОЦИАЦИЯ при обнаружении нажатия кнопки
  • АССОЦИАЦИЯНОРМАЛЬНАЯ при считывании RFID-кода или после тайм-аут

Моя предварительная реализация:

// Попытка прочитать RFID без блокировки.
// Возвращает найденный код RFID как указатель на буфер с нулевым завершением,
// или nullptr, если в данный момент RFID недоступен.
char *read_rfid() {
    // Реализация оставлена в качестве упражнения читателю.
}

void loop() {
    static enum {NORMAL, ASSOCIATION} state;
    static uint32_t started_association;  // время начала ассоциации

    char *rfid = read_rfid();
    switch (state) {
        case NORMAL:
            if (rfid) {
                Serial.print("Read RFID code: ");
                Serial.println(rfid);
            }
            if (digitalRead(btnAssociation) == LOW) {
                Serial.println("Starting association");
                mode = ASSOCIATION;
                started_association = millis();
            }
            break;
        case ASSOCIATION:
            if (rfid) {
                assocate_rfid(rfid);
                Serial.println("Association successful, code: ");
                Serial.println(rfid);
                mode = NORMAL;
            }
            if (millis() - started_association >= assocation_timeout) {
                Serial.println("Association timeout");
                mode = NORMAL;
            }
            break;
    }
}

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

  • Мигание светодиода, которое должно быть сделано в стиле Blink Пример без задержки
  • Функция read_rfid(), которая считывает последовательный порт в неблокирующий мод. Для этого вам понадобится правило для решение о том, когда сообщение завершено. Это может быть сообщение терминатор (если читатель его отправляет) или тайм-аут.

Наконец, я бы порекомендовал вам прочитать пару записей в блоге Majenko:

  • Конечный автомат — это руководство по реализации конечные автоматы
  • Чтение последовательного порта на Arduino научит вас читать последовательный порт в неблокируемом режиме.
,

Большое спасибо за вашу помощь :) Благодаря вашим ответам я смог понять так много вещей и улучшить свой код :) Я продолжаю работать над своим проектом! Спасибо за вашу помощь! :), @Aeva