Attaching Event Responder inside class

mborgerson

Well-known member
I am using the SPI EventResponder to good effect in receiving SPI data packets in the background from a Lepton 3.5 thermal imaging camera. The driver receives data frames as fast as the Lepton can send them (~9 frames per second due to ITAR limitations). It's all done in the background and uses about 5% of the CPU bandwidth on a T3.2 at 48MHz. The foreground polls a frame-complete flag and sends the frame data via USB Serial to a T4.1 host in the dead time between frames. The T4.1 receives the frame data using USBHost Serial, writes them to SD files, and sends the files to my PC using MTP.

In this working driver, I use an external proxy function to forward the End-of-packet event to my driver. An example of how that is done is in this code:
C++:
// Demo SPI with event handler callled from external proxy function
// compiles and runs on T4.1 TD 1.59  Arduino IDE 2.3.1
#include <SPI.h>

#define PKTLEN 20

EventResponder SPIEvent;
// define a simple SPI test class
class SPIDev_cl {
  private:
  const int pin_cs = 10;
  uint8_t pktbuffer[PKTLEN];
  uint32_t pktsrcvd = 0;

  public:
  void begin(void){
    SPI.begin();
  }
  void GetPacket(void){
    SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE1));
    digitalWriteFast(pin_cs, LOW);  // cs goes high in event responder
    SPI.transfer(NULL, pktbuffer, PKTLEN, SPIEvent);   
  }
  void Responder(void){ // ignore the results of read, increment pktsrcvd
    digitalWriteFast(pin_cs, HIGH);
    SPI.endTransaction();
    pktsrcvd++;
  }
  uint32_t PktsRcvd(void){ return pktsrcvd;}
}; // end of class definition

SPIDev_cl  spiDev1;  // create one instance of class

// Event responder is outside class
void SPIEventResponder(EventResponderRef event_responder) {
  spiDev1.Responder();
}

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  delay(1000);
  spiDev1.begin();
  SPIEvent.attachImmediate(&SPIEventResponder);
}

void loop() {
  // put your main code here, to run repeatedly:
  spiDev1.GetPacket();
  delay(1);  // 20 bytes at 10MHz SCLK shouldn't take long!
  Serial.print("pktsrcvd: ");
  Serial.println(spiDev1.PktsRcvd());
  delay(999);  // keep output slow enough to read
}

My next goal is to encapsulate the event responder declarations and code inside the SPIDev_cl class. That's not going so well. I've tried many ways to encapsulate the event response inside the class. None have succeeded.
Here is the test code, which varies in just a handful of lines:

C++:
// Demo SPI with event handler callled from internal function
// Doesn't compile.
#include <SPI.h>

#define PKTLEN 20

// define a simple SPI test class and try to encapsulate event responder setup

class SPIDev_cl {
  private:
  static const int pin_cs = 10;
  uint8_t pktbuffer[PKTLEN];

  EventResponder SPIEvent;
  uint32_t pktsrcvd = 0;

  void SPIEventResponder(EventResponderRef event_responder) {
    Responder();
  }
  public:
  void begin(void){
    SPI.begin();
    // Now try to attach the event responder
    //SPIEvent.attachImmediate(&SPIEventResponder);  // THis line  causes error
    SPIEvent.attachImmediate([this](){Responder();}); //attempted lambda function didn't work either
  }

  void GetPacket(void){
    SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE1));
    digitalWriteFast(pin_cs, LOW);  // cs goes high in event responder
    SPI.transfer(NULL, pktbuffer, PKTLEN, SPIEvent);   
  }
  void Responder(void){ // ignore the results of read, increment pktsrcvd
    digitalWriteFast(pin_cs, HIGH);
    SPI.endTransaction();
    pktsrcvd++;
  }
  uint32_t PktsRcvd(void){ return pktsrcvd;}
}; // end of class definition



SPIDev_cl  spiDev1;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  delay(1000);
  spiDev1.begin();
}

void loop() {
  // put your main code here, to run repeatedly:
  spiDev1.GetPacket();
  delay(1);  // 20 bytes at 10MHz SCLK shouldn't take long!
  Serial.print("pktsrcvd: ");
  Serial.println(spiDev1.PktsRcvd());
  delay(999);  // keep output slow enough to read
}

If anyone knows the magic words to accomplish this goal, please let me know.
 
Method A, use a static function and upcast from (base) EventResponder* to (derived) SPIDev_cl*:
Code:
// Demo SPI with event handler callled from external proxy function
// compiles and runs on T4.1 TD 1.59  Arduino IDE 2.3.1
#include <SPI.h>
#define PKTLEN 20
// define a simple SPI test class
class SPIDev_cl : private EventResponder {
  private:
  const int pin_cs = 10;
  uint8_t pktbuffer[PKTLEN];
  uint32_t pktsrcvd = 0;
  public:
  void begin(void){
    SPI.begin();
    attachImmediate(ResponderFwd);
  }
  void GetPacket(void){
    SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE1));
    digitalWriteFast(pin_cs, LOW);  // cs goes high in event responder
    SPI.transfer(NULL, pktbuffer, PKTLEN, *this);   
  }
  static void ResponderFwd(EventResponderRef p) { static_cast<SPIDev_cl*>(&p)->Responder(); }
  void Responder(void){ // ignore the results of read, increment pktsrcvd
    digitalWriteFast(pin_cs, HIGH);
    SPI.endTransaction();
    pktsrcvd++;
  }
  uint32_t PktsRcvd(void){ return pktsrcvd;}
}; // end of class definition
SPIDev_cl  spiDev1;  // create one instance of class
void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  delay(1000);
  spiDev1.begin();
}
void loop() {
  // put your main code here, to run repeatedly:
  spiDev1.GetPacket();
  delay(1);  // 20 bytes at 10MHz SCLK shouldn't take long!
  Serial.print("pktsrcvd: ");
  Serial.println(spiDev1.PktsRcvd());
  delay(999);  // keep output slow enough to read
}

Method B, just clobber/override the EventResponder trigger virtual function:
Code:
class SPIDev_cl : private EventResponder {
  private:
  const int pin_cs = 10;
  uint8_t pktbuffer[PKTLEN];
  uint32_t pktsrcvd = 0;
  public:
  void begin(void){
    SPI.begin();
  }
  void GetPacket(void){
    SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE1));
    digitalWriteFast(pin_cs, LOW);  // cs goes high in event responder
    SPI.transfer(NULL, pktbuffer, PKTLEN, *this);   
  }
  // replace EventResponder::triggerEvent
  void triggerEvent(int,void*) { Responder(); }
  void Responder(void) { // ignore the results of read, increment pktsrcvd
    digitalWriteFast(pin_cs, HIGH);
    SPI.endTransaction();
    pktsrcvd++;
  }
  uint32_t PktsRcvd(void){ return pktsrcvd;}
}; // end of class definition
 
The way I've done similar things in the past is using a static method for the responder to call in a similar way to jmarsh's solution A. However rather than then using a static cast to get to the instance of the class I make the class a singleton and reference the instance of that singleton.

Code:
class SPIDev_cl {
  private:
  SPIDev_cl(); // singleton so constructor is private.

  public:

  void begin(void){
    SPI.begin();
    attachImmediate(SPIDev_cl::Responder_static);
  }

  inline static SPIDev_cl* Instance() {
    static SPIDev_cl SPIDev_cl_inst;
    return &SPIDev_cl_inst;
  }
  static void Responder_static(EventResponderRef p) { SPIDev_cl::Instance()->Responder(); }
  void Responder(void){
   ...
  }

All accesses to the class from outside the class then become SPIDev_cl::Instance()-><method> which can look a little messy but will optimise down at compile time so it's not a performance hit.
This does also make it impossible to accidentally create two instances of the SPIDev_cl class. Admittedly not a common bug to get tripped up by but I work on the theory that if you can make it so that a possible bug becomes impossible then this is a good thing.
 
For now, I've decided to test @jmarsh's second option: make my class a derivative of EventResponder and override EventResponder.triggerevent. I think it has several advantages:

1. Minimal changes to source and no significant change in object code size.
2. No need to separately define an EventResponder or call Event.AttachImmediate, since my class is an EventResponder, and I am passed control as soon as the event is triggered. My triggerEvent member function can bypass most of the EventResponder overhead and go directly to my responder function.
3. It seems that this option will allow the use of more than one instance, each working with a separate SPI channel--selected by an input parameter to begin(SPI_Num). I don't have a need for that now, especially as the T3.2 connected to the Lepton has only one SPI channel.
4. This option refreshed my memory of the utility of derived subclasses and inheritance. I made good use of those attributes of object-oriented programming when I was writing Macintosh applications in the mid 1980s, but not so much recently. When you reach your late 70's, a memory refresh is a GOOD THING!

I'll build this option into my Lepton Driver class and test it . I'll report the results after testing with the T3.2 and Lepton.
 
Mods to the Lepton driver were straightforward and work as expected. Both SPI and the EventResponder are now contained in the driver class.
Thank you for your help! The mustache hides the smile of the happy programmer!
Happy Programmer.jpg
 
Back
Top