The thing is: Inside an ISR (Interrupt Service Routine) not only does time measuring with millis() not work, delay() also won't work (it uses the same interrupt mechanism as millis() in the background). Neither does anything else, that depends on their own ISR to execute - like receiving serial data. The hardware Serial interface can receive single bytes without depending on interrupts, but to get these bytes into the buffer (and to actually use them via Serial) you need to let other ISRs execute.
You should NEVER write an ISR, which executes for a long time. Keep it in the microseconds, maximum in the very low millisecond range. How long you can get here until you see negative effects depends on other factors. You really should only do minimal work in an ISR and the rest in the main code.
A common code structure for this looks as follows:
- In your main code you don't use any
delay() calls. Everything timed should be done with millis() (like in the BlinkWithoutDelay example that comes with the Arduino IDE), so that it won't block execution. Also no long loops inside of loop(). Make sure, that your loop() function can iterate freely and fast. - In global scope you define a flag variable. A simple one byte variable, marked as
volatile. - In your ISR you are doing nothing but setting this variable. Depending on your situation you might wanna set it depending on an digital input via
digitalRead(), which is still fast, so OK to do. - In your main code you use an
if statement to check for this flag variable to be set. On most loop() iterations this won't be executed. Only, if the flag variable got set inside the ISR, it will get executed. This is the place where you write the code handling the event. At the end of the block you then only need to reset the flag variable, so that you are ready for the next interrupt to happen.
This would look somewhat like this:
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){ // Handle interrupt here button_flag = false; // Reset flag variable to be ready for next interrupt } } void button_ISR(){ button_flag = true; }
Note, that the button_flag variable is defined as volatile, so that the compiler doesn't cache its value is some register and instead always reads the actual current value of it (not optimizing it).
In your code you should probably use another coding concept named Finite State Machine (FSM). This is the easiest method to implement a code with different states/stages while still keeping it non-blocking. You can read up about FSMs on the web (or for example in my answer to this question).
The gist is, that you use a variable, that represents the state, that your code is currently in. In loop() you then only ever execute the code for the current state. Changing to a different state is done by setting the state variable. The state, that your current code needs, are probably CHECK_FOR_SINGLE_RFID_TAG and ASSOCIATE_SINGE_RFID_TAG (though you can name these like you want). A simple FSM with this can look like the following:
#define CHECK_FOR_SINGLE_RFID_TAG 0 #define ASSOCIATE_SINGLE_RFID_TAG 1 // single byte variable so that we don't need to bother about atomic reads/writes uint8_t state = CHECK_FOR_SINGLE_RFID_TAG; void loop(){ switch(state){ case CHECK_FOR_SINGLE_RFID_TAG: if(Serial1.available()){ // read Serial1 here, check the RFID tag ID against the saved one // and execute code accordingly } break; case ASSOCIATE_SINGLE_RFID_TAG: if(Serial1.available()){ // read Serial1 here and save the new RFID tag ID in a variable // then reset the state variable to automatically go back to check mode // after the new RFID tag is enrolled state = CHECK_FOR_SINGLE_RFID_TAG; } // Use the BlinkWithoutDelay idiom to blink your LED non-blocking // something like // if(millis() - led_timestamp > led_interval){ // digitalWrite(led, !digitalRead(led)); // } break; } }
You see, how we define out possible states, save the current one in a variable and act according to it inside of loop()? This concept is really powerful, so it is totally worth it to learn.
Note: I've used a variable of type uint8_t (a single byte) to keep this interrupt safe (you can read up about this by searching for something like arduino interrupt atomic read or arduino interrupt safe variable) for the next part and the defines to give each value a readable name. You could use an enum for that, though while writing this I got on a tangent about the size of an enum, which seems 16 bits on AVR (like the Arduino Mega), unless you use a specific compiler flag, according to this blog article. This explanation is not that important for you right now, but I also wanted to explain, why this doesn't use enums.
Now we can combine this with our interrupt code from above. In this case we don't need a seperate flag variable anymore. We can directly set the state variable inside the 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()){ // read Serial1 here, check the RFID tag ID against the saved one // and execute code accordingly } break; case ASSOCIATE_SINGLE_RFID_TAG: if(Serial1.available()){ // read Serial1 here and save the new RFID tag ID in a variable // then reset the state variable to automatically go back to check mode // after the new RFID tag is enrolled state = CHECK_FOR_SINGLE_RFID_TAG; } // Use the BlinkWithoutDelay idiom to blink your LED non-blocking // something like // if(millis() - led_timestamp > led_interval){ // digitalWrite(led, !digitalRead(led)); // } break; } }
You can see, the button_ISR() is very small, thus very fast. And our loop() also runs unblocked, so the next iteration of it will come very fast and it will then execute the state, that we set in the ISR.
Note: When reading from Serial, make sure, that you don't use functions, that block for a long time. For example Serial.readString() will take 1s to execute (since that is the default timeout), which would block the non-blocking code (no good).