Help with AttachInterrupt (Controlling a Traffic Light Sequence)

I would like to control a sequence with three LED lights. The sequence being GREEN (on for 5 seconds then off), BLUE (on for 1second then off), RED (on for five seconds, then off). So a total of 11 seconds of activity then all lights are off and wait for a second button press.

When the button is held down, the sequence performs. The difficulty I am having, is creating an interrupt where when the button is depressed, the sequence I described above comes to a dead halt and all LEDs go dark.

Ive tried many iterations of this code using the online help center for the attachInterrupt command. There was another method I saw using for loops to interrupt the sequence and continually check for the button being HIGH , but attachInterrupt seemed more elegant. I'd really like to learn how to use it.

In the current version of the code below, this is the behaviour:
-All lights are OFF
-Press button Lights go through sequence between 2 and 3 times (should only be once)
*At this time further slow button presses are ignored, length of time spent holding button is ignored
*Can speed up the sequence by pressing the button multiple times very very quickly. The lights will switch from green to blue to red quickly. After the button has stopped being pressed, the sequence will perform on it's on two more times at the normal speed

int RED = 13;
int BLUE = 12;
int GREEN = 11;
const int buttonPin = 2;
volatile int buttonState = digitalRead(buttonPin);

void setup () {
  // configure data communication
  Serial.begin(9600); //Baud Rate
  // configure hardware peripherals
  pinMode(RED, OUTPUT); //Pin 13, Red LED
  pinMode(BLUE, OUTPUT); //Pin 12, yellow LED
  pinMode(GREEN, OUTPUT); //Pin 12, green LED
  pinMode(buttonPin, INPUT); //Pin 2, buttonPin
  attachInterrupt(digitalPinToInterrupt(buttonPin), pin_ISR, LOW);
} 

void pin_ISR() {
  buttonState = !buttonState;
}

void loop () {
  if (buttonState){
    digitalWrite(GREEN, HIGH);
    delay(5000);
    digitalWrite(GREEN,LOW);
    digitalWrite(BLUE, HIGH); 
    delay(1000);
    digitalWrite(BLUE,LOW);
    digitalWrite(RED,HIGH);
    delay(5000);
    digitalWrite(RED,LOW);
  }
  else{
    digitalWrite(GREEN, LOW);
    digitalWrite(BLUE, LOW);
    digitalWrite(RED, LOW);
  }
}

You should not be using interrupts for this. You need to re-write your code to use millis() instead of delay(). Study the "blink without delay" example sketch to understand the principle of how this works.

Thank you for the reply. Could you or another user offer more guidance?

I'm having difficulty wrapping my head around the millis() function as it pertains to constantly checking the code for state changes. Ive modifed the "blink without delay" example with poor results. I've keywords that reflect what I'm trying to accomplish with "millis()" but I'm not finding much online that seems to be the same as what Im loking for.

Also, I was able to use attachInterrupts in one version of code (overwritten by accident so I cant paste it here) which worked very well. The only issue is, releasing the button would stop the code whereever it was at and a second push would start the code from this spot. This means the LEDS might start at blue or red instead of going to the start of the sequence and starting with yellow again.

As a lay person, going by the description of the attachInterrupt command, it appears to do exactly what I want. Why shouldnt I use it?

Because as a lay person you clearly don’t understand it.

There are many many examples of what you need to do on line. I even have my own page describing the technique.

http://www.thebox.myzen.co.uk/Tutorial/State_Machine.html
Or even the forum has a post on this
https://forum.arduino.cc/t/demonstration-code-for-several-things-at-the-same-time/217158/4

As to the code you posted.
Remove the interrupt calls altogether and put a read of your button at the start of the loop. Then add a delay(500) as the last thing in your list of three lines in the else clause.

Well, you already said it yourself! :astonished:

Which is to say using the interrupt clearly did not do what you wanted, so is completely inappropriate. :roll_eyes:


That is not what interrupts are [b]for[/b]!

As a beginner, it is incredibly unlikely that interrupts will be useful to you.

A common "newbie" misunderstanding is that an interrupt is a mechanism for altering the flow of a program - to execute an alternate function. Nothing could be further from the truth! :astonished:

An interrupt is a mechanism for performing an action which can be executed in "no time at all" with an urgency that it must be performed immediately or else data - information - will be lost or some harm will occur. It then returns to the main task without disturbing that task in any way though the main task may well check at the appropriate point for a "flag" set by the interrupt.

Now these criteria are in a microprocessor time scale - microseconds. This must not be confused with a human time scale of tens or hundreds of milliseconds or indeed, a couple of seconds. A switch operation is in this latter category and even a mechanical operation perhaps several milliseconds; the period of a 6000 RPM shaft rotation is ten milliseconds. Sending messages to a video terminal is clearly in no way urgent,

Unless it is a very complex procedure, you would expect the loop() to cycle many times per millisecond. If it does not, there is most likely an error in code planning; while the delay() function is provided for testing purposes, its action goes strictly against effective programming methods. The loop() will be successively testing a number of contingencies as to whether each requires action, only one of which may be whether a particular timing criteria has expired. Unless an action must be executed in the order of mere microseconds, it will be handled in the loop().

So what sort of actions do require such immediate attention? Well, generally those which result from the computer hardware itself, such as high speed transfer of data in UARTs(, USARTs) or disk controllers.

An alternate use of interrupts, for context switching in RTOSs, is rarely relevant to this category of microprocessors as it is more efficient to write cooperative code as described above.

does this mean you want to have a sequence

green - for 5 seconds
blue - for 1 second
red - for 5 seconds
off

and just want to (re) start the sequence again after a button press?

or do you want to run the sequence only as along as the button is pressed?

green - for 5 seconds if the button is pressed
blue - for 1 second if the button is still pressed
red - for 5 seconds if the button is still pressed
off

There really is no reason not to use an interrupt in this application.
However when detecting a switch change - however you do it - you may need to prevent a response to switch bounce.
In your code the interrupt will be triggered MANY times - so you wont know the outcome as the
buttonState
will change each time.

As a very crude but simple test you could try these changes:


volatile int buttonState = 0;


void pin_ISR() {
  buttonState = 1;
}


void loop () {
  if (buttonState){  // there has been a button press
    noInterrupts(); //pevent another change - for now!
    digitalWrite(GREEN, HIGH);
    delay(5000);
    digitalWrite(GREEN,LOW);
    digitalWrite(BLUE, HIGH); 
    delay(1000);
    digitalWrite(BLUE,LOW);
    digitalWrite(RED,HIGH);
    delay(5000);
    digitalWrite(RED,LOW);
  }
buttonState = 0;
interrupts();
}
1 Like

From the "if it helps dept", I like using interrupts to detect button presses; not because they're necessarily the best way to handle a button press (they may - or may not - be ... jury is out on that one as far as I'm concerned) but more because button pressing is (I think) something that's simple enough to help people learn how to write interrupt service routines ("ISRs") - which in-turn is helpful for future projects.

I recently finished a project that used ISRs to detect button presses for the grip heaters for my motorbike - and posted the entire code in another thread here:

https://forum.arduino.cc/t/a-collection-of-arduino-techniques-that-ive-found-useful

So you may (or may not) find (portions of) that code of use/interest to you.

One thing I did discover is that interrupts can quickly bite one in the bum; they're so darn fast that you can end up with several activations due to switch contact bounce - which leads to undesired consequences. Switch debouncing is thus required in the code ... and I discovered that techniques that are commonly used in regular code often don't work well in ISRs; I spent a couple of days tearing my hair out before getting a technique that worked well most of the time.

Another "gotcha" with them is that even when you disable them - and a trigger arrives - even though it won't activate the interrupt at the time ... it'll fire once the first one has finished ... which needs to be handled.

The millis() function took me a while to get my head around; in essence it's just a timestamp - nothing more, nothing less - and thus by comparing timestamps you can tell how much time has passed between two events ... or alternatively only do something after a certain amount of time eg if you want to turn a light off after 5 seconds you could set it up with something like:

stopTime = millis + 5000 // Calculate the required stop time

Then in the loop just do a continual test:

if (millis() > stopTime)
{
runLightsOffRoutine()
}

Does that help?

I can probably take a look at your code later if you'd like - just let me know - happy to try and help.

1 Like

Interrupts allow inserting polling logic everywhere in the program without actually inserting polling logic. There cases were you will not see a gain from interrupts, in these cases you basically write polling logic anyways. Could be cooperative or preemptive, which is a huge topic.

If you can accumulate multiple interrupts into a single poll call there is a benefit. DMA, FIFOs, etc. also work the exact same way. This lets you spend more time away in your logic which makes the code cleaner and faster.

In this particular case you do not care if you ignore an interrupt. You could poll here, but you would need to debounce. What you are doing is hoping that the overhead for the ISR jump will be enough for it to settle. On fast architectures this is not stable. For example instruction set reduces the number of instructions or allows fast context swap. On slower ones it is. For example a cache miss, down clocked processor, huge number of instructions, etc.

1 Like

@jfseneca

I've put this together for you; I tested it at my end and I think it does what you want - if not then it'll probably still give you some interesting things to learn from.

One quick note; not 100% how you've wired your switch but I think the easiest way is to tell the chip to use it's internal 20kOhm resistor to pull the line high - so all you need to do is ground pin 2 via the switch when you press the button (so it's "active low") - that way you don't need to worry about using a pullup / pull down physical resistor.

You'll see I've tweaked a few other parts of your code too (constants, interrupt type, and a couple of other nuggets.

You'll also see that I've added my own delay routine ("cjsDelay"). If you're wondering why, it's because the regular delay() seemed to stop working when I executed lots of interrupts. To be honest, I really didn't expect that; I know that delay relies on interrupts - and only 1 interrupt can work at a time - thus "delay() doesn't work in an ISR" - but as we have it here, it wasn't in the ISR - so the expected behaviour for me was that (at worst) it would have been suspended whilst the ISR was executing and then resume ... but what appeared to happen when I repeatedly pressed the button was that the delay was effectively abandoned. I would be interested to understand the reason for that if anyone can shed any authoritive light on it. No biggie - easily worked around by using a loop of delayMicroseconds() which doesn't rely on interrupts and can be used in ISRs ... just didn't expect to have to use it here.

Hope this helps.

const int RED             = 13;                                       // Assign LEDs to pins
const int BLUE            = 12;
const int GREEN           = 11;

const int buttonPin       = 2;                                        // Assign button to pin

volatile bool buttonFlag = LOW;                                       // Assume button hasn't been pressed to start with

void setup () 
{
  pinMode(RED, OUTPUT);                                               //Pin 13, Red LED
  pinMode(BLUE, OUTPUT);                                              //Pin 12, yellow LED
  pinMode(GREEN, OUTPUT);                                             //Pin 11, green LED
  pinMode(buttonPin, INPUT_PULLUP);                                   //Pin 2, buttonPin
  
  attachInterrupt(digitalPinToInterrupt(buttonPin), pin_ISR, CHANGE); // Point to the interrupt handler
} 

void loop () 
{
  if (buttonFlag == HIGH)                                             // Button been pressed? If so then ...
  {
    digitalWrite(GREEN, HIGH);                                        // Green LED on
    
    for (int16_t cjsDelay = 5000; cjsDelay > 0; cjsDelay--)           // 5 second delay
    {
      delayMicroseconds(1000);
    }
    
    digitalWrite(GREEN,LOW);                                          // Green LED off
    digitalWrite(BLUE, HIGH);                                         // Yellow LED on
    
    for (int16_t cjsDelay = 1000; cjsDelay > 0; cjsDelay--)           // 1 Second delay
    {
      delayMicroseconds(1000);
    }

    digitalWrite(BLUE,LOW);                                           // Yellow LED off
    digitalWrite(RED,HIGH);                                           // Red LED on
    
    for (int16_t cjsDelay = 5000; cjsDelay > 0; cjsDelay--)           // 5 Second delay
    {
      delayMicroseconds(1000);
    }
    
    digitalWrite(RED,LOW);                                            // Red LED off

    buttonFlag = LOW;                                                 // Reset button flag
  }
  
  if (buttonFlag == LOW)                                              // If we've finished the sequence and button hasn't been pressed then turn everything off
  {
    digitalWrite(GREEN, LOW);                                         // Green LED off
    digitalWrite(BLUE, LOW);                                          // Yellow LED off
    digitalWrite(RED, LOW);                                           // Red LED off
  }
}

void pin_ISR() 
{
  static unsigned long lastInterruptTime = 0;                         // Setup for key bounce & stuck button handling
  unsigned long interruptTime = millis();                             // Document interrupt time
  
  if (interruptTime - lastInterruptTime > 1000)                       // If it's been at least 1 second since last interrupt then proceed
  {
    buttonFlag = HIGH;                                                // Indicate button press
    lastInterruptTime = interruptTime;                                // Document last EFFECTIVE interrupt time for debounce & stuck button handling
  }
}

No, they allow you to store that a push has taken place but it only affects the program when you look at this stored value. This is not much difference from looking at the push button at the time you want to use it. If there is sufficient delay between the push and your program looking at it is too long then it will appear to the user that it is doing random stuff.

I once had to manage a software engineer who caught and stored in a FIFO every button press. But the rest of his code was so long, because as he says he did it right, that the product was unusable. This is because if a push gets no response then a user is going to push again, and again. I am sure you can imagine how that went down with the customer, but he couldn’t see it was his fault and wouldn’t flush his buffer.

1 Like

Excellent points of view from both you and @dthacher IMHO.

From where I'm sitting, using interrupts to capture button activations gave me the freedom and flexibility to check for the press on a schedule that worked-in better for other approaches I'd used in the program - such as being able to leave a block of time critical code alone long enough that it may have missed a press if it had to poll for it; thus if I was constrained to using polling then I would have had to add additional logic/overhead to break the time-critical task into smaller time slices.

I guess my "rule of thumb" is to use whatever is easier/"more eloquent" so long as it works and is robust.

Alright, I think my post would suggest this case is where interrupts are not very helpful. My apologies for the length and to the OP.

Couple use cases: (Not going to say this is complete list.)

  1. Program has events very often at high speed. You might as well do polling. If you do not understand the event, know when it is coming, or cannot combine multiple events. Super loops can be faster than interrupts, however these generally means the program does next to nothing. Likely caused by very bad planning in the first place. Interrupts can help by pulling the event from register space into memory, before next clobbers it.
  2. Program has events which happen very far apart but are very critical. Here you will need an interrupt to get and possibly process the event. If the processing conflicts with other interrupts you will be forced into polling.
  3. Program has random and non-critical events you can use interrupts to pull in data. However polling would work just as well.
  4. Program uses fixed protocol message size or can figure out protocol message size from state machine. In this case the Interrupt can be used to accumulate data. Does not need to use FIFO, simple vector or array would work.
  5. Program uses DMA to accumulate events, Interrupt could be used to switch DMA buffers before next set of bytes arrive. Assuming you have fixed set of bytes.

If you have a single interrupt you can possibly block, creating a second thread of execution. You can also make these a low priority and let other interrupts run on top of it assuming system supports this.

Some RTOS support having interrupt triggering task swap. Which is not very clean in super loop or cooperative models. This would avoid the need to process directly inside ISR. However could be a little heavy handed. You can poll with cooperative systems, which is easy but at scale can be hard. You can poll with preemptive systems, which can be easy or hard depending on the context.

Speaking of polling. You can poll your own flag. Have the ISR accumulate then set random flag which gets polled.

Point being is interrupts can be used to randomly respond to an event at the time of the event. Without writing nasty code which checks constantly. Events themselves affect the timing of the main loop a lot. As if you save the event you need to have it available next spin.

How much time you have to make a spin is very ridged with polling without hardware FIFO. Software FIFO requires special consideration, but this is area where education creates a blind spot. If you use a single register you must ensure the register is never clobbered. With ISR you can do that. You can poll ISR value plus register value. Which could double the time you can spend away. If you use FIFO, DMA, array, etc. you may be able to spend even more away. Allowing for less time wasted polling for events which do not exist.

Hardware offload is almost always better. It will sit there in the background working in parallel. However this generally only goes so far. There are lots of things which come into play.

Hardware FIFOs for IPC or general purpose programming would make life a lot easier. You can get them on many controller's IO hardware. RP2040 and dsPIC33CH are the only ones I know which use them for IPC. However there are work arounds generally speaking.

1 Like

@dthacher I quite agree with that last post, although I think the discussion has wandered off the original intent of the post into a much more advanced discussion of interrupts in general.

The one big use I see for interrupts for beginners is in the decoding of signals from a rotary encoder. In most cases you can’t pole fast enough to catch every pulse and while most of the time there is no activity from the encoder, when there is it is rapid. If done right you get automatic debounce as well.

2 Likes