1

I recently wrote a simple script to...we'll say, "flash some LED's" and uploaded it to an attiny45. In short, the script cycles through 3 LED's and turns them on for a specified frequency, in a specified direction. With a button, I can cycle through different modes. There are 7 modes (default (LED's OFF), then 3 different speeds forwards and 3 different speeds backwards).

The script works fine-ish, however I've noticed that in order to read from the button input properly, the function that turns the LED's ON and OFF must be completed, meaning that for the longer cycles, the user may have to hold the button until the function completes. This is further complicated during the shorter cycles, where the input may detect multiple button-presses (despite adding a delay following the digital read.

NOTE: I could just make the delay longer, but this doesn't fix the issue during lower frequencies, when the function may take multiple seconds to complete.

I feel the best way to sort this (though I'd be open to other ideas) is to use the attachInterrupt() function; however, it doesn't work. The button no longer works and I'm not sure why.

// Pin Variables
int led1 = 2;
int led2 = 3;
int led3 = 4;
int leds[] = {led1, led2, led3};
int modePin = 0; // (physical pin 5 on the IC)

// Mode Speeds and setting
int fastTime = 100;
int mediumTime = 300;
int slowTime = 1000;
int offTime = 100;
int mode = 0;

void setup() {
  for (int i=0; i<3; i++){
    pinMode(leds[i], OUTPUT);
    digitalWrite(leds[i], LOW);
  }
  attachInterrupt(modePin, callMode, RISING);
}

void loop() {
  // If we get here, reset and go back to default (all LED's OFF)
  if (mode >= 7) {
    mode = 0;
  }
}

void callMode(){
  ++mode;
  //TIME: 1000, 300, 100 (milliseconds)
   switch (mode) {
     case 0:
       // Default, OFF
       for (int i=0; i<3; i++){
         digitalWrite(leds[i], LOW);
       }
       break;  
     case 1:
       // Forward, Fast
       ledMode(fastTime, offTime, true);
       break;
     case 2:
       // Forward, Medium
       ledMode(mediumTime, offTime, true);
       break;
     case 3:
       // Forward, Slow
       ledMode(slowTime, offTime, true);
       break;
     case 4:
       // Backwards, Fast
       ledMode(fastTime, offTime, false);
       break;
     case 5:
       // Backwards, Medium
       ledMode(mediumTime, offTime, false);
       break;
     case 6:
       // Backwards, Slow
       ledMode(slowTime, offTime, false);
       break;
     case 7:
       // Return to Zero
       for (int i=0; i<3; i++){
          digitalWrite(leds[i], LOW);
       }
       mode = 0;
       break;  
   }    
}

void ledMode(int onTime, int offTime, bool forward){
  if (forward) {
    for (int i = 0; i <= 2; i++) {
      digitalWrite(leds[i], HIGH);
      delay(onTime);                       
      digitalWrite(leds[i], LOW);
      delay(offTime);
    }       
  }  
  else {
    for (int i = 2; i >= 0; i--) {
      digitalWrite(leds[i], HIGH);
      delay(onTime);                       
      digitalWrite(leds[i], LOW);
      delay(offTime);
    }       
  }  
}

NOTE: callMode() and ledMode() are only split up into different functions because I know that the function passed to attachInterrupt() cannot contain any delays(). So each time the button is pressed, the mode variable should increment and then the switch-statement should cause the next mode to run. When we reach 7, let's reset to zero and enter the default/OFF mode.

I've been racking my head for a few days on this and not sure what the problem is. Perhaps I can only use the attachInterrupt() function to some other pin? Perhaps I'm not using that function correctly?

Thank you in advance for any and all help you can provide.

-Joe

3
  • 1. Re “I know that the function passed to attachInterrupt() cannot contain any delays()”: you should not call delay() from interrupt context. Whether delay() is called directly by the interrupt handler or indirectly through another function makes no difference. 2. Did you try the Blink Without Delay approach? Oct 6, 2021 at 16:24
  • use callMode() to set a flag, such as nextSeq = true; ... in ledMode(), check for flag ... if (nextSeq)
    – jsotola
    Oct 6, 2021 at 16:46
  • @EdgarBonet I think that would be doable with a single LED, but not when I'm cycling through 3 of them. I'm attempting to adopt it now, but it's definitely proving to be tricky, to say the least
    – JoeBro391
    Oct 6, 2021 at 18:19

1 Answer 1

3

Interrupts are overkill for this kind of ultra-slow problems, and they can introduce some extra complexity. Here the best solution seems (to me at least) to lie in the approach of the Blink Without Delay Arduino tutorial. Obviously, it must be adapted. In that tutorial, the system switches between only two states (LED off or on). Here the cycling of the LEDs goes through six phases:

  1. all LEDs are off
  2. LED[0] is on
  3. all LEDs are off
  4. LED[1] is on
  5. all LEDs are off
  6. LED[2] is on

The basic algorithm is the same: when millis() tells us it is time to move to the next phase, we do so and update the state of the LEDs.

Note that I numbered the phases in such a way that the last bit (phase&1) tells us whether an LED is on, and in that case the remaining bits (phase>>1) hold the index of the LED that is on.

Here is how to cycle the LEDs at a fixed speed:

if (millis() - last_change >= phase_time) {
    last_change += phase_time;

    /* If an LED is on, turn it off. */
    if (phase & 1)
        digitalWrite(leds[phase >> 1], LOW);

    /* Move to the next phase. */
    if (++phase >= 6) phase = 0;

    /* Turn on the LED if needed. */
    if (phase & 1)
        digitalWrite(leds[phase >> 1], HIGH);
}

On top of this you need a finite state machine that cycles between the different modes (speeds and directions). In order to avoid code repetition, I would split this mode into a “state” that can OFF, FORWARD or BACKWARDS, and an index into an array of delay times (with 0, 1, 2 meaning fast, medium and slow, respectively).

Putting it all together (warning: not tested):

const uint8_t leds[] = { 2, 3, 4 };
const uint32_t times[] = { 100, 300, 1000 };

void setup() {
    for (int i = 0; i < 3; i++)
        pinMode(leds[i], OUTPUT);
}

enum { OFF, FORWARD, BACKWARDS } state = OFF;  // global state
uint8_t time_idx;      // index in times[] (0 = fast)
int8_t phase;          // 0 = all off, 1 = led[0] on, 2 = all off...
uint32_t last_change;  // last time the phase changed

void loop() {

    /* Change mode on button press. */
    if (button_pressed()) {
        if (state == OFF) {
            state = FORWARD;
            time_idx = 0;  // start fast
        } else {
            time_idx++;    // slow down
            if (time_idx >= 3) {  // change direction or stop
                time_idx = 0;
                if (state == FORWARD)
                    state = BACKWARDS;
                else              // state is BACKWARDS
                    state = OFF;
            }
        }
    }

    /* Cycle the LEDs. */
    if (state != OFF && millis() - last_change >= times[time_idx]) {
        last_change += times[time_idx];

        /* If an LED is on, turn it off. */
        if (phase & 1)
            digitalWrite(leds[phase >> 1], LOW);

        /* Move to the next phase. */
        if (state == FORWARD) {
            if (++phase >= 6) phase = 0;
        } else {  // BACKWARDS
            if (--phase < 0) phase = 5;
        }

        /* Turn on the LED if needed. */
        if (phase & 1)
            digitalWrite(leds[phase >> 1], HIGH);
    }
}

I did not write the button_pressed() function, which is meant to debounce the button and detect the “just pressed” transition. It is left as an exercise to the reader (or you can just use a debouncing library).

1
  • Thank you so much for your feedback! Just to close the loop here, I was about to implement this on my project, but then I had another idea. I reverted back to an older pre-interrupt version of the firmware and following a button-press, I simply added a 4-second delay. This requires the user to press-&-hold the button until a press is detected (never more than the length of the current cycle), but works fairly well for this purpose. In all cases, the user presses until the LED's stop and then releases. Works like a charm. Talk about a simple solution.
    – JoeBro391
    Oct 15, 2021 at 20:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.