Interrupt service routine within a c++ class
Posted: Sun Nov 10, 2024 4:47 pm
I am building an application which receives a Manchester Encoded data stream from a simple 433MHz radio receiver. The code was originally written in 'C' for an ATMega328p using the Arduino IDE and uses a pin change interrupt and free running timer. It worked well.
For my new project I have migrated to VSCode/PlatformIO and am using an ESP32 S3. With a few small adaptations, the 'C' coded receiver is now working well on the ESP32 in a test application. However, in order to simplify the integration of the receiver into the main project, I want to create the receiver as a C++ class.
The main challenge here is including the Interrupt Service Routine within the class as a class method. The function must be declared static as must any other methods and attributes referenced by the ISR. As an added complication, I want to make the class into a library and so to avoid having to modify the code for each project I want to be able to set the size of the receive buffer when the object is created.
In order to achieve this, I have declared an array for the buffer which is not static so can be created by the constructor and a static pointer to that array which is initialised by the constructor.
I believe I have created the class in accordance with the rules. The code compiles fine but the linker returns an "Undefined Reference" error for each of the class attributes referenced within the ISR function.
The error is as follows;
Linking .pio\build\esp32dev\firmware.elf
c:/users/john/.platformio/packages/toolchain-xtensa-esp32@8.4.0+2021r2-patch5/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld.exe: .pio\build\esp32dev\src\McrRx_Lib.cpp.o:(.literal._ZN11mcrReceiver19sigInterruptHandlerEv+0x8): undefined reference to `mcrReceiver::pulseTimer'
c:/users/john/.platformio/packages/toolchain-xtensa-esp32@8.4.0+2021r2-patch5/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld.exe: .pio\build\esp32dev\src\McrRx_Lib.cpp.o:(.literal._ZN11mcrReceiver19sigInterruptHandlerEv+0x10): undefined reference to `mcrReceiver::rxPort'
c:/users/john/.platformio/packages/toolchain-xtensa-esp32@8.4.0+2021r2-patch5/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld.exe: .pio\build\esp32dev\src\McrRx_Lib.cpp.o:(.literal._ZN11mcrReceiver19sigInterruptHandlerEv+0x18): undefined reference to `mcrReceiver::readyFlag'
c:/users/john/.platformio/packages/toolchain-xtensa-esp32@8.4.0+2021r2-patch5/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld.exe: .pio\build\esp32dev\src\McrRx_Lib.cpp.o:(.literal._ZN11mcrReceiver19sigInterruptHandlerEv+0x2c): undefined reference to `mcrReceiver::receiveStatus'
c:/users/john/.platformio/packages/toolchain-xtensa-esp32@8.4.0+2021r2-patch5/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld.exe: .pio\build\esp32dev\src\IS_Server.cpp.o:(.literal.startup._GLOBAL__sub_I_rtc+0x0): undefined reference to `mcrReceiver::pRxBuffer'
Can someone give me some idea about what I may have got wrong.
The class declaration in McrRx_Lib.h is as follows, Note I have altered the way some elements are declared static or public/private in an attempt to fix the problem but nothing has worked.
The abbreviated ISR function in McrRx_Lib.cpp is....
The important bit of the main program file, where the object of type 'McrReceiver' is instantiated is as follows;
For my new project I have migrated to VSCode/PlatformIO and am using an ESP32 S3. With a few small adaptations, the 'C' coded receiver is now working well on the ESP32 in a test application. However, in order to simplify the integration of the receiver into the main project, I want to create the receiver as a C++ class.
The main challenge here is including the Interrupt Service Routine within the class as a class method. The function must be declared static as must any other methods and attributes referenced by the ISR. As an added complication, I want to make the class into a library and so to avoid having to modify the code for each project I want to be able to set the size of the receive buffer when the object is created.
In order to achieve this, I have declared an array for the buffer which is not static so can be created by the constructor and a static pointer to that array which is initialised by the constructor.
I believe I have created the class in accordance with the rules. The code compiles fine but the linker returns an "Undefined Reference" error for each of the class attributes referenced within the ISR function.
The error is as follows;
Linking .pio\build\esp32dev\firmware.elf
c:/users/john/.platformio/packages/toolchain-xtensa-esp32@8.4.0+2021r2-patch5/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld.exe: .pio\build\esp32dev\src\McrRx_Lib.cpp.o:(.literal._ZN11mcrReceiver19sigInterruptHandlerEv+0x8): undefined reference to `mcrReceiver::pulseTimer'
c:/users/john/.platformio/packages/toolchain-xtensa-esp32@8.4.0+2021r2-patch5/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld.exe: .pio\build\esp32dev\src\McrRx_Lib.cpp.o:(.literal._ZN11mcrReceiver19sigInterruptHandlerEv+0x10): undefined reference to `mcrReceiver::rxPort'
c:/users/john/.platformio/packages/toolchain-xtensa-esp32@8.4.0+2021r2-patch5/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld.exe: .pio\build\esp32dev\src\McrRx_Lib.cpp.o:(.literal._ZN11mcrReceiver19sigInterruptHandlerEv+0x18): undefined reference to `mcrReceiver::readyFlag'
c:/users/john/.platformio/packages/toolchain-xtensa-esp32@8.4.0+2021r2-patch5/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld.exe: .pio\build\esp32dev\src\McrRx_Lib.cpp.o:(.literal._ZN11mcrReceiver19sigInterruptHandlerEv+0x2c): undefined reference to `mcrReceiver::receiveStatus'
c:/users/john/.platformio/packages/toolchain-xtensa-esp32@8.4.0+2021r2-patch5/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld.exe: .pio\build\esp32dev\src\IS_Server.cpp.o:(.literal.startup._GLOBAL__sub_I_rtc+0x0): undefined reference to `mcrReceiver::pRxBuffer'
Can someone give me some idea about what I may have got wrong.
The class declaration in McrRx_Lib.h is as follows, Note I have altered the way some elements are declared static or public/private in an attempt to fix the problem but nothing has worked.
- class mcrReceiver {
- public:
- // Now for some enumerations used by the ISR
- // The pulse categorisations, the data stream is edge driven.
- typedef enum {
- invalidPulse, // Pulse length outside of acceptable limits
- shortRising, // Rising edge after a short low
- shortFalling, // Falling edge after a short high
- longRising, // Rising edge after a long low
- longFalling, // Falling edge after a long high
- msgComplete // The long high stop pulse which marks the end of a sequence
- } validPulse ;
- // The stages the receiver will go through during a sequence.
- typedef enum {
- // rxStRunning, // Not currently used
- rxStSyncLo, // Sync stages before actual message starts
- rxStSyncHi,
- rxStSyncStart,// Still receiving sync pulses but expecting a start marker
- rxStStart, // Start marker received and awaiting the first data pulse
- rxStPhase1, // The phases of receiving a data bit
- rxStPhase2,
- rxStMsgEnd, // Indicates we've received the stop pulse
- rxStError, // oops, Terminate reception, reset receiver and report error to user
- // rxStIdle, // Not currently used
- rxStOverrun, // Buffer overrun. Too many valid pulses but no stop received
- // rxStCount // Not currently used
- } rxStage ; //
- // A structure to report the completion status of a receive sequence
- typedef struct {
- rxStage exitStage; // The stage of the sequence when we terminated.
- rxStage exitStatus; // Indicator of where it went wrong if status is an error
- validPulse lastPulse; // The type of the last pulse we received
- int exitCount; // The number of good bytes we had received when we terminated the receive sequence
- } exitData ;
- static volatile bool readyFlag ; // To signify a complete message in the receive buffer
- static volatile uint8_t *pRxBuffer; // Pointer to the receive buffer. used by the ISR so must be static
- volatile uint8_t *rxBuffer ; // The actual receive buffer which is sized by the constructor so can't be static
- static volatile exitData receiveStatus; // A record of how a receive sequence ended
- static hw_timer_t *pulseTimer ; // The timer which will tell us the lenghts of the pulses
- static int rxPort ; // The port to which we will attach the receiver interrupt
- mcrReceiver (int bufLen) : rxBuffer(new uint8_t[bufLen]) {pRxBuffer = &rxBuffer[0] ;}; // Constructor. Sizes the receive buffer and sets a static pointer to it
- void startReceiver(int portNum); // Allows the user to start up the receive process (May consider a function to stop it if necessary)
- int getMessage (char* msgBuffer, int bufLen); // Allow the user task to check for and collect a new message
- exitData getRxStatus () ; // Allow user task to see how it ended
- // private:
- // Start with some values to categorise the incoming pulses. See above for value definitions
- static const int minSync = MIN_SYNC;
- static const uint64_t shortMaxHigh = SHORT_MAX_HIGH ;
- static const uint64_t shortMinHigh = SHORT_MIN_HIGH ;
- static const uint64_t shortMaxLow = SHORT_MAX_LOW ;
- static const uint64_t shortMinLow = SHORT_MIN_LOW ;
- static const uint64_t longMaxHigh = LONG_MAX_HIGH ;
- static const uint64_t longMinHigh = LONG_MIN_HIGH ;
- static const uint64_t longMaxLow = LONG_MAX_LOW ;
- static const uint64_t longMinLow =LONG_MIN_LOW ;
- static const uint64_t minEnd = MIN_END ;
- static const uint64_t maxEnd = MAX_END ;
- static validPulse validatePulse (uint64_t len, bool state); // To categorise the pulses
- static void sigInterruptHandler() ; // The ISR function that implements the receiver
- };
- void ICACHE_RAM_ATTR mcrReceiver::sigInterruptHandler()
- {
- // Variables that must hold their values between interrupts
- static rxStage rxState = rxStSyncLo ; // SyncLow is our starting point. a low to high transition starts everything off
- static uint8_t bitMask = 0x01 ; // To set bits in the current byte if required
- static volatile uint8_t* pCurrentByte = pRxBuffer; // A pointer to the current byte in the receive buffer
- static int rxIndex = 0; // The index of the current byte in the buffer
- static int syncCount = 0; // Tally of the number of sync bits we have received
- static bool needsReset = true ; // Tag to tell us when we need to reset the receive parameters
- // Things we'll only use once per call
- rxStage exitStage ; // A record of the receiver state when we exit. Mainly for error detection by the main task.
- validPulse pulseType; // To categorise the pulse we've just received.
- // First get all of the information about the interrupt
- uint64_t intPulseLen = timerRead(pulseTimer); // Get the length of the pulse
- timerWrite(pulseTimer,0); // Restart the timer ASAP in order to time the next pulse
- bool intPulseState = !digitalRead(rxPort); // the sense of the pulse is always opposite to the current state of the pin
- pulseType = validatePulse(intPulseLen, intPulseState) ; // Work out if the pulse meets the timing criteria
- /* For brevity, I have no included here the entire function. The function continues from here to implement the detection of a pulse, convert the pulse stream to data bits and add those bits to the receiver buffer. Not all of the referenced attributes are included within this snippet but if we fix one, we'll fix them all.*/
- } // end sigInterruptHandler
- void mcrReceiver::startReceiver(int portNum) // Function to start recognising the incomming pulse stream
- {
- rxPort = portNum ; // Index the correct IO port
- attachInterrupt(portNum, sigInterruptHandler, CHANGE); // Attach the ISR to the port. Need an interrupt for any state change
- pulseTimer = timerBegin(PULSE_TIMER, 80, true); // Instatiate the timer to time the incoming pulses
- timerWrite(pulseTimer,0); // and clear the timer. Probably not necessary as the first pulse will be rubbish anyway!!
- } // end startReceiver
- // Receive test V1 - Measures pulse timings with trigger from transmitter output.
- #include <Arduino.h>
- #include "IS_Server.h"
- #include "../../Common/McrCommon.h"
- #include "McrRx_Lib.h"
- #include "RTClib.h"
- #include "LiquidCrystal_I2C.h"
- #include <esp_task_wdt.h>
- RTC_DS1307 rtc ; // Instantiate the RTC
- LiquidCrystal_I2C lcd(0x27, 16, 2); // and the LCD
- mcrReceiver sensorRx(sizeof(packetData)); // and the radio receiver
- TaskHandle_t controlTask ; // Declare the task handle for the main controller which handles the
- // data from the receiver
- TaskHandle_t networkTask ; // and a task to handle the web server
- void controller(void* pControlParams) ; // Forward declare the task functions
- void webServer(void* pNetParams) ;
- void setup()
- {
- Serial.begin(115200);
- pinMode(LED_OUT_PIN, OUTPUT);
- taskPad.msgCount = 0;
- taskPad.update = false;
- createSemaphores();
- xTaskCreatePinnedToCore(
- controller, /* Task function. */
- "Controller", /* name of task. */
- 10000, /* Stack size of task */
- (void*)&taskPad, /* parameter of the task */
- 1, /* priority of the task */
- &controlTask, /* Task handle to keep track of created task */
- 1); /* pin task to core 0 */
- delay(500) ;
- xTaskCreatePinnedToCore(
- webServer, /* Task function. */
- "Server", /* name of task. */
- 10000, /* Stack size of task */
- (void*)&taskPad, /* parameter of the task */
- 1, /* priority of the task */
- &networkTask, /* Task handle to keep track of created task */
- 0); /* pin task to core 0 */
- delay (500);
- } // end setup
- /* Rest of code follows on from here but is not relevant to the current issue
- */