Slight Delay After Returning from Interrupt

2020-02-13 06:25发布

问题:

I've written a small program that uses a button on an STM32 Discovery board to act as a counter in either Binary/Decimal/Hexadecimal mode (screen cycles through the 3 options and once pressed, counts up to 16 for each press before resetting to cycling through options).

I'm encountering one small "bug" (read, not really) that has me a little confused. If I count up in Decimal/Hexadecimal, it returns to cycling through the options immediately but if I have counted up in Binary it takes ~1sec or so before doing so (a noticeable delay).

int main(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
    lcd_init();
    button_init();

    while (1)
    {
        while (!counting) {
            standard_output();
        }
    }

}

void standard_output(void) {
    state = 0;
    lcd_command(0x01);
    delay_microsec(2000);
    lcd_putstring("Binary");
    for (i=0; i<40; i++) delay_microsec(50000);     // keep display for 2 secs
    if (counting) return;                           // if we have pressed the button, want to exit this loop
    state = 1;
    lcd_command(0x01);
    delay_microsec(2000);
    lcd_putstring("Decimal");
    for (i=0; i<40; i++) delay_microsec(50000);     // keep display for 2 secs
    if (counting) return;                           // if we have pressed the button, want to exit this loop
    state = 2;
    lcd_command(0x01);
    delay_microsec(2000);
    lcd_putstring("Hexadecimal");
    for (i=0; i<40; i++) delay_microsec(50000);     // keep display for 2 secs
    if (counting) return;                           // if we have pressed the button, want to exit this loop

}

void EXTI0_IRQHandler(void) {
    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        if (!stillBouncing) {                               // a button press is only registered if stillBouncing == 0
            if (!counting) {                                // if we weren't already counting, a valid button press means we are now
                counting = 1;
                count = 0;                                  // starting count from 0
            }
            else {
                count++;
            }
            if (count < 16) {
                lcd_command(0x01);
                delay_microsec(2000);
                format_int(count);
            }
            else {
                counting = 0;                               // we are no longer counting if count >= 16
            }
        }
        stillBouncing = 10;                                 // every time a button press is registered, we set this to 10
        while (stillBouncing > 0) {                         // and check that it hasn't been pressed for 10 consecutive 1000microsec intervals
            if (!delay_millisec_or_user_pushed(1000)) {
                stillBouncing--;
            }
        }
    }
    EXTI_ClearITPendingBit(EXTI_Line0);
}

void format_int(unsigned int n) {
    if (state == 0) {                                       // if we selected binary
        for (i=0;i<4;++i) {
            num[i] = (n >> i) & 1;                          // generate array of bit values for the 4 least significant bits
        }
        i = 4;
        while (i>0) {
            i--;
            lcd_putint(num[i]);                             // put ints from array to lcd in reverse order to display correctly
        }
    }
    else if (state == 1) {                                  // if we selected decimal
        lcd_putint(n);                                      // lcd_putint is enough for decimal
    }
    else {                                                  // if we selected hex
        snprintf(hex, 4, "%x", n);                          // format string such that integer is represented as hex in string
        lcd_putstring(hex);                                 // put string to lcd
    }
}

int delay_millisec_or_user_pushed(unsigned int n)
{
    delay_microsec(n);
    if (!GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)) {
        return 0;
    }
    return 1;
}

I really have no idea why it is doing this and have played around with it now but still unable to figure it out. It's fine as is but I would like to know why it is doing this.

回答1:

Probably lcd_putint takes a lot of time to refresh the display. It probably convert each number to string and then put it to the screen. format_int() In binary case it loops 4 times, then 4 times more than Hex and Dec cases.

If you change the code as below, It will, I guess, be faster:

char bin[5];
sprintf(bin, "%d%d%d%d", ((n&0x08)>>3), ((n&0x04)>>2), ((n&0x02)>>1), (n&0x01));
lcd_putstring(bin);

I know there are a lot of solutions to convert number to binary string, but the key point is to use lcd_putstring that is surely faster then call 4 times lcd_putint



回答2:

First of all, you must ensure that the input pin connected to the button has pull resistors, either on the PCB or enabled internally in the microcontroller i/o port. If you haven't, the input will be in no defined state when the button is inactive, and you'll get garbage interrupts. Resistors should pull towards the inactive state.

As indicated in comments, you should never have any delays inside interrupt service routines. ISRs should be as small and fast as possible.

It is important to note that if you have an interrupt-triggering pin connected to a button, it means you will get interrupts for every bounce or other EMI noise that appears on the pin. These false, spurious interrupts will stall the main program and overall realtime performance will be suffering. This is a classic beginner mistake and it is present in your program.

You can use an interrupt-triggering pin for buttons, but then you must know what you are doing. You must disable the interrupt itself from inside the ISR as soon as you get the first edge trigger, in this way:

  • Make sure the button interrupt is set to trigger both on raising and falling edge.
  • Upon receiving interrupt, disable the interrupt from inside the ISR. From inside the ISR, start one of the on-chip hardware timers and have it trigger through a timer interrupt after x milliseconds.

    Pseudo code for such an ISR for a generic, fictive MCU, with fictive register names:

    void button_isr (void)
    {
      BUTTON_ISR_ENABLE = CLEAR; // some hw register that disables the ISR
      BUTTON_ISR_FLAG = CLEAR; // some hw register that clears the interrupt flag
      TIMER_COUNTER = CURRENT_TIME + DEBOUNCE_TIME; // some hw timer register
      TIMER_ISR_ENABLE = SET; // some hw timer register
    }
    
  • Typical debounce times are between 5ms to 20ms. You can measure the bounce on the particular switch with an oscilloscope.

  • When the timer runs out, the timer ISR is triggered and you read the input again. If they read equal, (both high), set a flag "button pressed". If not, you had some noise on the line, which should be ignored. Disable the timer but enable the button I/O interrupt again.

    Pseudo code for the timer ISR, for a generic, fictive MCU:

    static bool button_pressed = false;
    
    void timer_isr (void)
    {
      TIMER_ISR_FLAG = CLEAR;
      TIMER_ISR_ENABLE = CLEAR;
    
      if(BUTTON_PIN == ACTIVE) // whatever you have here, active high/active low
      {
        button_pressed = true;
      }
      else
      {
        button_pressed = false;
      }
    
      BUTTON_ISR_ENABLE = SET;
    }
    

In the real code, the register names will be something more cryptic, and how to set/clear flags will vary from MCU to MCU. Sometimes you clear by writing 1, sometimes by 0.

The above code should work well for standard applications. For applications with tighter realtime demands, you would have a timer/task running continuously, polling the button(s) at even time intervals. Demand that two following reads give the same value pressed/not pressed, in order to accept it as a change in the button state.

More advanced algorithms involve median filters that make multiple reads. A median filter with 3 reads is quite easy to implement and is sufficient even for many safety-critical applications.