在嵌入式系统中使用中断时避免全局变量

电器工程 微控制器 avr C 中断
2022-01-18 19:05:01

是否有一种避免全局变量的嵌入式系统在 ISR 和程序的其余部分之间实现通信的好方法?

似乎一般模式是有一个全局变量,它在 ISR 和程序的其余部分之间共享并用作标志,但这种全局变量的使用对我来说违背了原则。我已经包含了一个使用 avr-libc 样式 ISR 的简单示例:

volatile uint8_t flag;

int main() {
    ...

    if (flag == 1) {
        ...
    }
    ...
}

ISR(...) {
    ...
    flag = 1;
    ...
}

我无法绕过本质上是范围界定问题。ISR 和程序的其余部分都可以访问的任何变量都必须本质上是全局的,确定吗?尽管如此,我经常看到人们说“全局变量是实现 ISR 与程序其余部分之间通信的一种方式”(强调我的),这似乎暗示还有其他方法;如果还有其他方法,它们是什么?

4个回答

有一种事实上的标准方法来做到这一点(假设 C 编程):

  • 中断/ISR 是低级的,因此只能在与生成中断的硬件相关的驱动程序内部实现。它们不应该位于其他任何地方,而是位于该驱动程序内部。
  • 与 ISR 的所有通信都由驱动程序和仅驱动程序完成。如果程序的其他部分需要访问该信息,它必须通过 setter/getter 函数或类似函数从驱动程序请求它。
  • 您不应声明“全局”变量。具有外部链接的全局含义文件范围变量。那就是:可以用extern关键字或错误调用的变量。
  • 相反,为了在驱动程序内部强制私有封装,驱动​​程序和 ISR 之间共享的所有此类变量都应声明static这样的变量不是全局变量,而是仅限于声明它的文件。
  • 为防止编译器优化问题,此类变量也应声明为volatile. 注意:这不会提供原子访问或解决重入问题!
  • 驱动程序中通常需要某种方式的重入机制,以防 ISR 写入变量。示例:中断禁用、全局中断屏蔽、信号量/互斥量或保证原子读取。
这种对全局变量的使用对我不利

这是真正的问题。克服它。

现在,在下意识地立即咆哮这是多么不干净之前,让我稍微限定一下。过度使用全局变量肯定有危险。但是,它们也可以提高效率,这在资源有限的小型系统中有时很重要。

关键是要考虑何时可以合理使用它们并且不太可能让自己陷入困境,而不是等待发生的错误。总会有取舍。虽然通常避免在中断和前台代码之间进行通信的全局变量是一个可以理解的准则,但像大多数其他准则一样,将其带到宗教极端会适得其反。

我有时使用全局变量在中断和前台代码之间传递信息的一些示例是:

  1. 由系统时钟中断管理的时钟滴答计数器。我通常有一个每 1 毫秒运行一次的周期性时钟中断。这对于系统中的各种时序通常很有用。将这些信息从中断例程中获取到系统其余部分可以使用它的一种方法是保留一个全局时钟滴答计数器。中断例程在每个时钟滴答声中递增计数器。前台代码可以随时读取计数器。我经常这样做 10 毫秒、100 毫秒,甚至 1 秒的滴答声。

    我确保 1 毫秒、10 毫秒和 100 毫秒的刻度具有可以在单个原子操作中读取的字长。如果使用高级语言,请确保告诉编译器这些变量可以异步更改。例如,在 C 中,您将它们声明为extern volatile当然,这是包含在固定包含文件中的内容,因此您无需为每个项目记住这一点。

    我有时将 1 s 滴答计数器设为总经过时间计数器,因此将其设为 32 位宽。在我使用的许多小型微型计算机上,这无法在单个原子操作中读取,因此它不是全局的。相反,提供了一个读取多字值、处理读取之间可能的更新并返回结果的例程。

    当然,也可以通过例程来获得更小的 1 毫秒、10 毫秒等刻度计数器。然而,这对你真的没什么用,添加了很多指令而不是读取一个单词,并占用了另一个调用堆栈位置。

    有什么缺点?我想有人可能会打错字,不小心写入其中一个计数器,然后可能会弄乱系统中的其他时间。故意写信给计数器是没有意义的,所以这种错误需要是无意的,比如错字。似乎不太可能。我不记得在 100 多个小型微控制器项目中曾经发生过这种情况。

  2. 最终过滤和调整的 A/D 值。一个常见的做法是让中断例程处理来自 A/D 的读数。我通常比必要更快地读取模拟值,然后应用一点低通滤波。通常还会应用缩放和偏移。

    例如,A/D 可能正在读取分压器的 0 至 3 V 输出以测量 24 V 电源。许多读数经过一些过滤,然后进行缩放,以便最终值以毫伏为单位。如果电源为 24.015 V,则最终值为 24015。

    系统的其余部分只看到指示电源电压的实时更新值。它不知道也不需要关心确切的更新时间,特别是因为它的更新频率比低通滤波器建立时间要频繁得多。

    同样,可以使用接口例程,但您从中获得的好处很少。只需在需要电源电压时使用全局变量就简单多了。请记住,简单不仅适用于机器,而且更简单也意味着更少的人为错误机会。

任何特定的中断都将是全局资源。然而,有时让多个中断共享相同的代码可能会很有用。例如,一个系统可能有多个 UART,所有这些都应该使用类似的发送/接收逻辑。

一个很好的处理方法是将中断处理程序使用的东西或指向它们的指针放在结构对象中,然后让实际的硬件中断处理程序类似于:

void UART1_handler(void) { uart_handler(&uart1_info); }
void UART2_handler(void) { uart_handler(&uart2_info); }
void UART3_handler(void) { uart_handler(&uart3_info); }

对象uart1_infouart2_info等将是全局变量,但它们将是中断处理程序使用的唯一全局变量。处理程序将要接触的所有其他内容都将在其中处理。

请注意,中断处理程序和主线代码访问的任何内容都必须是限定的volatile最简单的方法可能是声明为volatile中断处理程序将使用的所有内容,但如果性能很重要,可能需要编写将信息复制到临时值的代码,对其进行操作,然后将它们写回。例如,不要写:

if (foo->timer)
  foo->timer--;

写:

uint32_t was_timer;
was_timer = foo->timer;
if (was_timer)
{
  was_timer--;
  foo->timer = was_timer;
}

前一种方法可能更容易阅读和理解,但效率不如后者。这是否是一个问题将取决于应用程序。

这里有三个想法:

将标志变量声明为静态以将范围限制为单个文件。

将标志变量设为私有并使用 getter 和 setter 函数来访问标志值。

使用信号量等信号对象代替标志变量。ISR 将设置/发布信号量。