Присоедините функцию Arduino ISR к члену класса

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

portEXIT_CRITICAL_ISR(&lock0);              
  }

hw_timer_t * timer[3] = NULL;

class myClass
{
private:
  irqCallback timmerCB;
  volatile uint8_t pri_var1, pri_var2;
  static uint8_t class_instance_number=0;
public:
  volatile uint8_t pub_var1, pub_var2;
  myClass(/* args */);
  ~myClass();
};

myClass::myClass(/* args */)
{
  timmerCB=timerCBArray[class_instance_number];
  timer[class_instance_number] = timerBegin(0, 80, true);  
  timerAttachInterrupt(timer[class_instance_number], &timmerCB, true);
  class_instance_number++;
}

Однако у меня есть такая проблема: как я могу изменить переменную внутри прерывания относительно каждого экземпляра класса?

, 👍3

Обсуждение

Я попытался перейти на SO, но он был заблокирован системой, @Juraj

Это, конечно, печально. Похоже, нам действительно придется ответить на его вопрос., @Nick Gammon


2 ответа


12

Процедура обслуживания прерываний (ISR) вне класса

Давайте рассмотрим простое использование прерываний:

volatile bool switchChanged;

void switchPressed ()
  {
  switchChanged = true;
  }  // конец переключателя нажат

void setup ()
  {
  pinMode (2, INPUT_PULLUP);
  attachInterrupt (0, switchPressed, CHANGE);
  }  // завершение настройки

void loop ()
  {
  // неважно    
  }  // конец цикла

Это отлично компилируется.


ISR внутри класса как функция (метод) класса

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

class myClass
  {
  volatile bool switchChanged;
  
  public:

  void begin ()
    {
    pinMode (2, INPUT_PULLUP);
    attachInterrupt (0, switchPressed, CHANGE);   // <--- строка 10
    }  // конец MyClass::begin
    
  void switchPressed ()
    {
    switchChanged = true;
    }  // конец MyClass::switchPressed
    
  };  // конец класса MyClass
  
myClass foo;  // создать экземпляр MyClass
  
void setup ()
  {
  foo.begin ();
  }  // завершение настройки

void loop ()
  {
  // неважно    
  }  // конец цикла

Это не компилируется:

ISR_in_class_test.ino: In member function ‘void myClass::begin()’:
ISR_in_class_test:10: error: argument of type ‘void (myClass::)()’ does not match ‘void (*)()’

Что здесь происходит?

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

Например, если у нас есть два экземпляра:

myClass foo;  
myClass bar; 

Если мы вызываем foo.begin(), то this-> указывает на "foo", а если мы вызываем bar.begin, то this-> указывает на "bar".

Однако ISR, когда он запускается процессором, не может знать, является ли this-> "foo" или "bar" или что-то еще. Таким образом, компилятор не может скомпилировать эту строку.


ISR внутри класса как статическая функция класса

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

class myClass
  {
  volatile bool switchChanged;   // <--- строка 3
  
  public:

  void begin ()
    {
    pinMode (2, INPUT_PULLUP);
    attachInterrupt (0, switchPressed, CHANGE);
    }  // конец MyClass::begin
    
  static void switchPressed ()
    {
    switchChanged = true;   // <--- строка 15
    }  // конец MyClass::switchPressed
    
  };  // конец класса MyClass
  
myClass foo;  // создать экземпляр MyClass
  
void setup ()
  {
  foo.begin ();
  }  // завершение настройки

void loop ()
  {
  // неважно    
  }  // конец цикла

Однако теперь у нас есть другая проблема:

ISR_in_class_test.ino: In static member function ‘static void myClass::switchPressed()’:
ISR_in_class_test:3: error: invalid use of member ‘myClass::switchChanged’ in static member function
ISR_in_class_test:15: error: from this location

Нестатическая переменная класса не может быть вызвана из функции статического класса. Почему? Потому что компилятор не знает, какая переменная вам нужна. Это foo.switchChanged или bar.switchChanged?


ISR внутри класса как статическая функция класса со статическими переменными

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

class myClass
  {
  static volatile bool switchChanged;  // объявления
  
  public:

  void begin ()
    {
    pinMode (2, INPUT_PULLUP);
    attachInterrupt (0, switchPressed, CHANGE);
    }  // конец MyClass::begin
    
  static void switchPressed ()
    {
    switchChanged = true;
    }  // конец MyClass::switchPressed
    
  };  // конец класса MyClass
  
volatile bool myClass::switchChanged;  // определить
  
myClass foo;  // создать экземпляр MyClass
  
void setup ()
  {
  foo.begin ();
  }  // завершение настройки

void loop ()
  {
  // неважно    
  }  // конец цикла

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


Процедуры склеивания

Чтобы обойти эту проблему, мы можем написать короткие процедуры "склеивания". Это функции, которые взаимодействуют между ISR и экземпляром класса.

class myClass
  {
  volatile bool switchChanged;

  static myClass * instances [2];
 
  static void switchPressedExt0 ()
    {
    if (myClass::instances [0] != NULL)
      myClass::instances [0]->switchPressed ();
    }  // конец MyClass::switchPressedExt0
  
  static void switchPressedExt1 ()
    {
    if (myClass::instances [1] != NULL)
      myClass::instances [1]->switchPressed ();
    }  // конец MyClass::switchPressedExt1


  public:

  void begin (const byte whichPin)
    {
    pinMode (whichPin, INPUT_PULLUP);
    switch (whichPin)
      {
      case 2: 
        attachInterrupt (0, switchPressedExt0, CHANGE);
        instances [0] = this;
        break;
        
      case 3: 
        attachInterrupt (1, switchPressedExt1, CHANGE);
        instances [1] = this;
        break;
        
      } // конец переключателя
    }  // конец MyClass::begin
    
  void switchPressed ()
    {
    switchChanged = true; 
    }
    
  };  // конец класса MyClass
  
myClass * myClass::instances [2] = { NULL, NULL };

// экземпляры нашего класса
myClass foo; 
myClass bar;

void setup ()
  {
  foo.begin (2);   // вывод D2
  bar.begin (3);   // вывод D3
  }  // завершение настройки

void loop ()
  {
  // неважно    
  }  // конец цикла

Это немного неудобно, однако то, что он делает, - это запоминает в массиве, какой экземпляр класса связан с каким прерыванием. Процедуры "склеивания" switchPressedExt0 и switchPressedExt1 вызывают соответствующий экземпляр функции switchPressed, используя запомненный указатель класса.

Теперь нестатическая функция switchPressed может обращаться к нестатическим переменным класса.


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

,

Подпрограммы Glue заставляют myclass работать только для определенных сценариев. Можно ли использовать myclass в качестве базового класса, а затем использовать наследование для создания новых определений классов для каждого сценария?, @Philipp Werminghausen

Я попробовал и понял, что наследование не работает, так как мы наследуем статическую переменную. Создание двух классов с идентичным кодом и разными именами работает... но это дублированный код, который также не является чистым решением., @Philipp Werminghausen

@PhilippWerminghausen У вас есть только фиксированное количество оборудования. Трудно создать общий класс (который вы можете создавать сотни раз), но который должен совместно использовать один таймер или прерывание. Приходится идти на компромиссы., @Nick Gammon


0

Я знаю, что это поздний ответ, но я думаю, что можно решить проблему с this, если вы используете std::bind

Ниже приведен полный пример, но если он слишком длинный для чтения, вот минимальный пример.

class ISRClass {
public:
  ISRClass(int pin) : pin_(pin) {}
  
  void setup() {
    attachInterrupt(digitalPinToInterrupt(pin_),
                    std::bind(&ISRClass::sensePinIsr, this), CHANGE);
  }

private:
  int pin_;
  ulong change_count_;

  void IRAM_ATTR sensePinIsr() { 
    // делаем что-то быстрое и простое
    change_count_++;
  }
};

Функция, возвращаемая std::bind, не принимает аргументов, даже это.

Полный пример следует.

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

#include "FunctionalInterrupt.h"
#include <Arduino.h>
#include <functional>
#include <optional>
#include <map>

using CallBack = std::function<void(int)>;

class PinMonitor {
public:
  PinMonitor(int pin) : pin_(pin) {}
  void setup() {
    pin_state_ = digitalRead(pin_);
    attachInterrupt(digitalPinToInterrupt(pin_),
                    std::bind(&PinMonitor::sensePinIsr, this), CHANGE);
  }

  std::optional<int> CheckForNewPinState(int debounce) {
    if (!change_count_) {
      return std::nullopt;
    }
    if (!is_debouncing_) {
      is_debouncing_ = true;
      debouce_started_at_ = millis();
      return std::nullopt;
    }
    auto time = millis();
    if (time < debouce_started_at_) {
      debouce_started_at_ = time; // handle timer rollover
    } 
    if (time < debouce_started_at_ + debounce) {
      return std::nullopt;
    }
    is_debouncing_ = false;
    change_count_ = 0;
    auto new_pin_state = digitalRead(pin_);
    if (new_pin_state != pin_state_) {
      pin_state_ = new_pin_state;
      for ( auto [_, callback] : callbacks_) {
        callback(pin_state_);
      }
      return pin_state_;
    }
    return std::nullopt;
  }

  int DoIfPinStateChanges(const CallBack callback) {
    next_callback_key++;
    callbacks_[next_callback_key] = callback;
    return next_callback_key;
  }

  void RemoveCallback(int key) {
    callbacks_.erase(key);
  }

private:
  int pin_;
  int pin_state_;
  ulong change_count_;
  CallBack callback_ = [](auto a) {};
  std::map<int, CallBack> callbacks_;
  bool is_debouncing_{};
  ulong debouce_started_at_{};
  int next_callback_key{};

  void IRAM_ATTR sensePinIsr() { change_count_++; }
};

Тогда в main вы можете использовать это, как показано ниже.

NetworkConnection networkConnection;
OTAHandler otaHandler;
Logger &logger = Logger::getInstance();

PinMonitor pinMonitor1(16);
PinMonitor pinMonitor2(17);

int call_count{};
int removable_callback{};

void setup() {
  networkConnection.connect();
  logger.startWebSocket();
  otaHandler.setup();
  pinMode(16, INPUT_PULLDOWN);
  pinMode(17, INPUT_PULLDOWN);
  auto chip_id = ESP.getEfuseMac();

  pinMonitor1.setup();
  pinMonitor1.DoIfPinStateChanges([chip_id](auto a) {
    logger.infoln("chip %12llx, reports pin1 new state of %d detected", chip_id, a);
  });
  removable_callback = pinMonitor1.DoIfPinStateChanges([chip_id](auto a) {
    logger.infoln("second call back was called on pin1, new state of %d detected", a);
    call_count++;
  });
  pinMonitor2.setup();
  pinMonitor2.DoIfPinStateChanges([chip_id](auto a) {
    logger.infoln("chip %12llx, reports pin2 new state of %d detected", chip_id, a);
  });

}

void loop() {
  otaHandler.loop();
  logger.checkWebSocket();
 
  std::optional<int> new_pin_state1;
  std::optional<int> new_pin_state2;
  
  new_pin_state1 = pinMonitor1.CheckForNewPinState(100);
  if (new_pin_state1) {logger.infoln("new pin1 state is %d", *new_pin_state1);}

  new_pin_state2 = pinMonitor2.CheckForNewPinState(100);
  if (new_pin_state2) {logger.infoln("new pin2 state is %d", *new_pin_state2);}
  

  if (call_count > 5) {pinMonitor1.RemoveCallback(removable_callback);}

}
,