从中断返回后的轻微延迟
Slight Delay After Returning from Interrupt
我写了一个小程序,它使用 STM32 Discovery 开发板上的一个按钮作为任一 Binary/Decimal/Hexadecimal 模式下的计数器(屏幕循环显示 3 个选项,一旦按下,计数为 16在重置为循环选项之前每次按下。
我遇到了一个小 "bug"(阅读,不是真的)让我有点困惑。如果我在 Decimal/Hexadecimal 中计数,它 returns 会立即循环通过选项,但如果我在二进制中计数,则需要 ~1 秒左右才能这样做(明显的延迟)。
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;
}
我真的不知道它为什么要这样做,现在已经尝试过但仍然无法弄清楚。它很好,但我想知道为什么它这样做。
可能lcd_putint
刷新显示需要很长时间。它可能会将每个数字转换为字符串,然后将其显示在屏幕上。
format_int()
在二进制情况下它循环 4 次,然后是 Hex 和 Dec 情况的 4 倍。
如果您按如下方式更改代码,我想它会更快:
char bin[5];
sprintf(bin, "%d%d%d%d", ((n&0x08)>>3), ((n&0x04)>>2), ((n&0x02)>>1), (n&0x01));
lcd_putstring(bin);
我知道有很多将数字转换为二进制字符串的解决方案,但关键是使用 lcd_putstring
肯定比调用 4 次 lcd_putint
更快
首先,必须确保连接到按钮的输入引脚有拉电阻,要么在PCB上,要么在微控制器i/o端口内部启用。如果没有,当按钮处于非活动状态时,输入将处于未定义状态,并且您将获得垃圾中断。电阻器应拉向非活动状态。
如评论中所述,您永远不应在中断服务程序中有任何延迟。 ISR 应该尽可能小和快。
重要的是要注意,如果您有一个连接到按钮的中断触发引脚,这意味着您将在引脚上出现的每次弹跳或其他 EMI 噪声时获得中断。这些错误的、虚假的中断将使主程序停止,整体实时性能将受到影响。这是一个典型的初学者错误,它存在于您的程序中。
您可以为按钮使用中断触发引脚,但是您必须知道自己在做什么。您必须在获得第一个边沿触发后立即从 ISR 内部禁用中断本身,这样:
- 确保按钮中断设置为在上升沿和下降沿触发。
接收到中断后,从 ISR 内部禁用中断。从 ISR 内部,启动一个片上硬件定时器并让它在 x 毫秒后通过定时器中断触发。
具有虚构寄存器名称的通用虚构 MCU 的此类 ISR 的伪代码:
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
}
典型的去抖时间在 5 毫秒到 20 毫秒之间。您可以使用示波器测量特定开关上的反弹。
当计时器用完时,触发计时器ISR,您再次读取输入。如果它们读数相等(均高),则设置标志 "button pressed"。如果不是,则线路上有一些噪音,应该忽略。禁用计时器但再次启用按钮 I/O 中断。
定时器 ISR 的伪代码,用于通用的虚构 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;
}
在实际代码中,寄存器名称会更加神秘,set/clear 标志的方式因 MCU 而异。有时写 1 清除,有时写 0。
以上代码应该适用于标准应用程序。对于具有更严格实时要求的应用程序,您将有一个 timer/task 运行 连续,以均匀的时间间隔轮询按钮。要求两个后续读取给出相同的值 pressed/not pressed,以便接受它作为按钮状态的变化。
更高级的算法涉及进行多次读取的中值过滤器。具有 3 个读取的中值过滤器非常容易实现,甚至对于许多安全关键应用程序也足够了。
我写了一个小程序,它使用 STM32 Discovery 开发板上的一个按钮作为任一 Binary/Decimal/Hexadecimal 模式下的计数器(屏幕循环显示 3 个选项,一旦按下,计数为 16在重置为循环选项之前每次按下。
我遇到了一个小 "bug"(阅读,不是真的)让我有点困惑。如果我在 Decimal/Hexadecimal 中计数,它 returns 会立即循环通过选项,但如果我在二进制中计数,则需要 ~1 秒左右才能这样做(明显的延迟)。
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;
}
我真的不知道它为什么要这样做,现在已经尝试过但仍然无法弄清楚。它很好,但我想知道为什么它这样做。
可能lcd_putint
刷新显示需要很长时间。它可能会将每个数字转换为字符串,然后将其显示在屏幕上。
format_int()
在二进制情况下它循环 4 次,然后是 Hex 和 Dec 情况的 4 倍。
如果您按如下方式更改代码,我想它会更快:
char bin[5];
sprintf(bin, "%d%d%d%d", ((n&0x08)>>3), ((n&0x04)>>2), ((n&0x02)>>1), (n&0x01));
lcd_putstring(bin);
我知道有很多将数字转换为二进制字符串的解决方案,但关键是使用 lcd_putstring
肯定比调用 4 次 lcd_putint
首先,必须确保连接到按钮的输入引脚有拉电阻,要么在PCB上,要么在微控制器i/o端口内部启用。如果没有,当按钮处于非活动状态时,输入将处于未定义状态,并且您将获得垃圾中断。电阻器应拉向非活动状态。
如评论中所述,您永远不应在中断服务程序中有任何延迟。 ISR 应该尽可能小和快。
重要的是要注意,如果您有一个连接到按钮的中断触发引脚,这意味着您将在引脚上出现的每次弹跳或其他 EMI 噪声时获得中断。这些错误的、虚假的中断将使主程序停止,整体实时性能将受到影响。这是一个典型的初学者错误,它存在于您的程序中。
您可以为按钮使用中断触发引脚,但是您必须知道自己在做什么。您必须在获得第一个边沿触发后立即从 ISR 内部禁用中断本身,这样:
- 确保按钮中断设置为在上升沿和下降沿触发。
接收到中断后,从 ISR 内部禁用中断。从 ISR 内部,启动一个片上硬件定时器并让它在 x 毫秒后通过定时器中断触发。
具有虚构寄存器名称的通用虚构 MCU 的此类 ISR 的伪代码:
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 }
典型的去抖时间在 5 毫秒到 20 毫秒之间。您可以使用示波器测量特定开关上的反弹。
当计时器用完时,触发计时器ISR,您再次读取输入。如果它们读数相等(均高),则设置标志 "button pressed"。如果不是,则线路上有一些噪音,应该忽略。禁用计时器但再次启用按钮 I/O 中断。
定时器 ISR 的伪代码,用于通用的虚构 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; }
在实际代码中,寄存器名称会更加神秘,set/clear 标志的方式因 MCU 而异。有时写 1 清除,有时写 0。
以上代码应该适用于标准应用程序。对于具有更严格实时要求的应用程序,您将有一个 timer/task 运行 连续,以均匀的时间间隔轮询按钮。要求两个后续读取给出相同的值 pressed/not pressed,以便接受它作为按钮状态的变化。
更高级的算法涉及进行多次读取的中值过滤器。具有 3 个读取的中值过滤器非常容易实现,甚至对于许多安全关键应用程序也足够了。