延迟 Arduino ISR 中的限位开关去抖动
Debouncing a limit switch in Arduino ISR with delays
我有一个限位开关连接到 arduino Mega 2650 用于运动控制。限位开关的两个常开触点连接到 Arduino 引脚和地,因此当限位开关接合时,Arduino 引脚短路到地。
不出所料,我在使用此设置时遇到了弹跳问题。我在我的 ISR 中使用计数器确认了它。最后,我编写了以下代码,似乎可以可靠地识别我的限位开关在任何给定时间点是否接合或脱离。
const int lsOuterLeftIn = 18; // lsOuterLeftIn is my Limit Switch
const int LED = 9;
volatile bool lsEngaged = false; // flag for limit switch engaged
void setup() {
pinMode(lsOuterLeftIn, INPUT_PULLUP);
pinMode(LED, OUTPUT);
attachInterrupt(digitalPinToInterrupt(lsOuterLeftIn), ISR1, FALLING);
attachInterrupt(digitalPinToInterrupt(lsOuterLeftIn), ISR2, RISING);
}
void loop() {
if (lsEngaged) digitalWrite(LED, HIGH);
else digitalWrite(LED, LOW);
}
void ISR1(){
delay(100);
lsEngaged = (digitalRead(lsOuterLeftIn));
}
void ISR2(){
delay(100);
lsEngaged = (digitalRead(lsOuterLeftIn));
}
但是,这是我的问题。我发现了这个 Arduino documentation page,上面写着
"Since delay() requires interrupts to work, it will not work if called
inside an ISR. "
但是,我确实在 ISR 中使用了 delay()
,它似乎有效,这是怎么回事?我是否遇到过目前一切正常但很容易崩溃的情况,因为 delay()
函数可能会像文档中所说的那样对我造成故障?
在 AVR 上,delay() 的实现如下。不涉及中断(micros() returns timer0 计数值,yield() 指的是您的简单草图中不会使用的调度程序)。
我认为评论是为了可移植性,您使用的环境适用于越来越多的平台。你所做的在 AVR 上很好,也许在另一个平台上就没那么好了。
我建议使用简单的 for 循环进行自旋等待。 cpu 没有做任何其他事情,除非功耗是一个问题,但这超出了这里的范围。
void delay(unsigned long ms)
{
uint16_t start = (uint16_t)micros();
while (ms > 0) {
yield();
if (((uint16_t)micros() - start) >= 1000) {
ms--;
start += 1000;
}
}
}
TomKeddie 的回答看起来很正确:您不会有任何问题。无论如何,在我看来,至少出于两个原因,您的代码在概念上是错误的。现在我来解释一下为什么。
有两种输入:一种是你必须立即回答的,另一种是你必须回答但不是直接威胁的。例如,安全止动器通常属于第一组,因为一旦你击中它,你就需要停止执行器。 UI 按钮在另一侧属于第二组,因为您不需要立即回答。
注意:在一个完善的程序中,您通常可以在十分之一毫秒内响应第二种输入,因此用户永远不会看到延迟。
现在,如果您的输入属于第二组输入,则您不应使用 ISR 来读取它,因为您可能会阻止更重要的内容。而是在主循环中读取它,适当地去抖动它。例如你可以使用 Bounce
库,或者自己实现它:
#define CHECK_EVERY_MS 20
#define MIN_STABLE_VALS 5
unsigned long previousMillis;
char stableVals;
...
void loop() {
if ((millis() - previousMillis) > CHECK_EVERY_MS)
{
previousMillis += CHECK_EVERY_MS;
if (digitalRead(lsOuterLeftIn) != lsEngaged)
{
stableVals++;
if (stableVals >= MIN_STABLE_VALS)
{
lsEngaged = !lsEngaged;
stableVals = 0;
}
}
else
stableVals = 0;
}
...
}
这将每 20 毫秒检查一次值是否更改。然而,该值只有在稳定超过 5 个周期(即 100 毫秒)时才会更新。
这样你就不会用那个任务阻塞你的主程序。
如果另一方面,您的输入对您的设备构成严重威胁(例如终点站),您需要尽快回答。如果是这种情况,您在回答之前等待 100 毫秒,这不符合您输入速度的需要。
当然你不能对这样的输入进行去抖动,因为去抖动会带来延迟。但是,您可以使一种状态优先于另一种状态。在接地端停止的情况下,严重的威胁是输入状态为接地时。所以我建议你以这样的方式设置你的变量:
- 当引脚下降时,您立即将其设置为 0
- 当引脚上升时,等待 100 毫秒(在主循环中),然后进行设置。
执行此操作的代码如下:
#define CHECK_EVERY_MS 20
#define MIN_STABLE_VALS 5
unsigned long previousMillis;
char stableVals;
attachInterrupt(digitalPinToInterrupt(lsOuterLeftIn), ISR1, FALLING);
...
void loop() {
if ((millis() - previousMillis) > CHECK_EVERY_MS)
{
previousMillis += CHECK_EVERY_MS;
if ((digitalRead(lsOuterLeftIn) == HIGH) && (lsEngaged == LOW))
{
stableVals++;
if (stableVals >= MIN_STABLE_VALS)
{
lsEngaged = HIGH;
stableVals = 0;
}
}
else
stableVals = 0;
}
...
}
void ISR1()
{
lsEngaged = LOW;
}
如您所见,唯一的中断是下降中断,最重要的是,它非常短。
如果您需要执行其他指令,例如停止电机,您可以在 ISR1 函数中(如果它们很短)。
请记住:ISR 必须尽可能短,因为当微控制器位于其中之一时,它就会对其他一切视而不见
从你的去抖动代码来看,你似乎可以节省 100 毫秒的反应时间来接通开关。
因此,如果您真的不需要在事件发生后的几微秒内做出反应,请考虑每隔 10 毫秒轮询一次输入(例如,来自定时器 ISR)。
(使用外部中断只有两个原因:1. 您需要对信号做出非常快的反应(微秒!),或者 2. 您需要从定时器的深度省电模式中唤醒不活跃。对于其他一切,您可以进行基于计时器的轮询。)
伪代码:
#define STABLE_SIGNAL_DURATION 5
uint8_t button_time_on = 0;
volatile bool button_is_pressed = false;
...
// Every 10ms do (can be done in a timer ISR):
if ( read_button_input() == ON ) {
if ( button_time_on >= STABLE_SIGNAL_DURATION ) {
button_is_pressed = true;
} else {
button_time_on++;
}
} else {
button_time_on = 0; // button not pressed (any more).
button_is_pressed = false;
}
...
并在 main()
中:
bool button_press_handled = false;
while(1) {
// do your other main loop stuff...
button_press_handled = button_press_handled && button_is_pressed;
if ( !button_press_handled && button_is_pressed ) {
// Handle press of the button
// ...
// Note that we handled the event for now:
button_press_handled = true;
}
}
比使用时间戳更容易
volatile bool buttonDirty = false;
void setup() {
attachInterrupt(digitalPinToInterrupt(buttonPin), buttonPress, FALLING);
}
void loop() {
while(1){
readButtons();
}
}
void buttonPress(){
if(buttonDirty) return;
buttonDirty = true;
}
void readButtons(){
if(!buttonDirty) return;
delay(100);
...........
}
我有一个限位开关连接到 arduino Mega 2650 用于运动控制。限位开关的两个常开触点连接到 Arduino 引脚和地,因此当限位开关接合时,Arduino 引脚短路到地。
不出所料,我在使用此设置时遇到了弹跳问题。我在我的 ISR 中使用计数器确认了它。最后,我编写了以下代码,似乎可以可靠地识别我的限位开关在任何给定时间点是否接合或脱离。
const int lsOuterLeftIn = 18; // lsOuterLeftIn is my Limit Switch
const int LED = 9;
volatile bool lsEngaged = false; // flag for limit switch engaged
void setup() {
pinMode(lsOuterLeftIn, INPUT_PULLUP);
pinMode(LED, OUTPUT);
attachInterrupt(digitalPinToInterrupt(lsOuterLeftIn), ISR1, FALLING);
attachInterrupt(digitalPinToInterrupt(lsOuterLeftIn), ISR2, RISING);
}
void loop() {
if (lsEngaged) digitalWrite(LED, HIGH);
else digitalWrite(LED, LOW);
}
void ISR1(){
delay(100);
lsEngaged = (digitalRead(lsOuterLeftIn));
}
void ISR2(){
delay(100);
lsEngaged = (digitalRead(lsOuterLeftIn));
}
但是,这是我的问题。我发现了这个 Arduino documentation page,上面写着
"Since delay() requires interrupts to work, it will not work if called inside an ISR. "
但是,我确实在 ISR 中使用了 delay()
,它似乎有效,这是怎么回事?我是否遇到过目前一切正常但很容易崩溃的情况,因为 delay()
函数可能会像文档中所说的那样对我造成故障?
在 AVR 上,delay() 的实现如下。不涉及中断(micros() returns timer0 计数值,yield() 指的是您的简单草图中不会使用的调度程序)。
我认为评论是为了可移植性,您使用的环境适用于越来越多的平台。你所做的在 AVR 上很好,也许在另一个平台上就没那么好了。
我建议使用简单的 for 循环进行自旋等待。 cpu 没有做任何其他事情,除非功耗是一个问题,但这超出了这里的范围。
void delay(unsigned long ms)
{
uint16_t start = (uint16_t)micros();
while (ms > 0) {
yield();
if (((uint16_t)micros() - start) >= 1000) {
ms--;
start += 1000;
}
}
}
TomKeddie 的回答看起来很正确:您不会有任何问题。无论如何,在我看来,至少出于两个原因,您的代码在概念上是错误的。现在我来解释一下为什么。
有两种输入:一种是你必须立即回答的,另一种是你必须回答但不是直接威胁的。例如,安全止动器通常属于第一组,因为一旦你击中它,你就需要停止执行器。 UI 按钮在另一侧属于第二组,因为您不需要立即回答。
注意:在一个完善的程序中,您通常可以在十分之一毫秒内响应第二种输入,因此用户永远不会看到延迟。
现在,如果您的输入属于第二组输入,则您不应使用 ISR 来读取它,因为您可能会阻止更重要的内容。而是在主循环中读取它,适当地去抖动它。例如你可以使用 Bounce
库,或者自己实现它:
#define CHECK_EVERY_MS 20
#define MIN_STABLE_VALS 5
unsigned long previousMillis;
char stableVals;
...
void loop() {
if ((millis() - previousMillis) > CHECK_EVERY_MS)
{
previousMillis += CHECK_EVERY_MS;
if (digitalRead(lsOuterLeftIn) != lsEngaged)
{
stableVals++;
if (stableVals >= MIN_STABLE_VALS)
{
lsEngaged = !lsEngaged;
stableVals = 0;
}
}
else
stableVals = 0;
}
...
}
这将每 20 毫秒检查一次值是否更改。然而,该值只有在稳定超过 5 个周期(即 100 毫秒)时才会更新。
这样你就不会用那个任务阻塞你的主程序。
如果另一方面,您的输入对您的设备构成严重威胁(例如终点站),您需要尽快回答。如果是这种情况,您在回答之前等待 100 毫秒,这不符合您输入速度的需要。
当然你不能对这样的输入进行去抖动,因为去抖动会带来延迟。但是,您可以使一种状态优先于另一种状态。在接地端停止的情况下,严重的威胁是输入状态为接地时。所以我建议你以这样的方式设置你的变量:
- 当引脚下降时,您立即将其设置为 0
- 当引脚上升时,等待 100 毫秒(在主循环中),然后进行设置。
执行此操作的代码如下:
#define CHECK_EVERY_MS 20
#define MIN_STABLE_VALS 5
unsigned long previousMillis;
char stableVals;
attachInterrupt(digitalPinToInterrupt(lsOuterLeftIn), ISR1, FALLING);
...
void loop() {
if ((millis() - previousMillis) > CHECK_EVERY_MS)
{
previousMillis += CHECK_EVERY_MS;
if ((digitalRead(lsOuterLeftIn) == HIGH) && (lsEngaged == LOW))
{
stableVals++;
if (stableVals >= MIN_STABLE_VALS)
{
lsEngaged = HIGH;
stableVals = 0;
}
}
else
stableVals = 0;
}
...
}
void ISR1()
{
lsEngaged = LOW;
}
如您所见,唯一的中断是下降中断,最重要的是,它非常短。
如果您需要执行其他指令,例如停止电机,您可以在 ISR1 函数中(如果它们很短)。
请记住:ISR 必须尽可能短,因为当微控制器位于其中之一时,它就会对其他一切视而不见
从你的去抖动代码来看,你似乎可以节省 100 毫秒的反应时间来接通开关。
因此,如果您真的不需要在事件发生后的几微秒内做出反应,请考虑每隔 10 毫秒轮询一次输入(例如,来自定时器 ISR)。
(使用外部中断只有两个原因:1. 您需要对信号做出非常快的反应(微秒!),或者 2. 您需要从定时器的深度省电模式中唤醒不活跃。对于其他一切,您可以进行基于计时器的轮询。)
伪代码:
#define STABLE_SIGNAL_DURATION 5
uint8_t button_time_on = 0;
volatile bool button_is_pressed = false;
...
// Every 10ms do (can be done in a timer ISR):
if ( read_button_input() == ON ) {
if ( button_time_on >= STABLE_SIGNAL_DURATION ) {
button_is_pressed = true;
} else {
button_time_on++;
}
} else {
button_time_on = 0; // button not pressed (any more).
button_is_pressed = false;
}
...
并在 main()
中:
bool button_press_handled = false;
while(1) {
// do your other main loop stuff...
button_press_handled = button_press_handled && button_is_pressed;
if ( !button_press_handled && button_is_pressed ) {
// Handle press of the button
// ...
// Note that we handled the event for now:
button_press_handled = true;
}
}
比使用时间戳更容易
volatile bool buttonDirty = false;
void setup() {
attachInterrupt(digitalPinToInterrupt(buttonPin), buttonPress, FALLING);
}
void loop() {
while(1){
readButtons();
}
}
void buttonPress(){
if(buttonDirty) return;
buttonDirty = true;
}
void readButtons(){
if(!buttonDirty) return;
delay(100);
...........
}