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


13

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

似乎一般的模式是要有一个在ISR和程序的其余部分之间共享并用作标志的全局变量,但是这种全局变量的使用对我而言是不利的。我提供了一个使用avr-libc样式ISR的简单示例:

volatile uint8_t flag;

int main() {
    ...

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

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

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



1
程序的其余所有部分不一定都可以访问;如果将变量声明为静态,则只有声明了变量的文件才能看到它。在整个一个文件中都可以看到变量,但是在程序的其余部分却不可见,这可以帮上忙。
DiBosco

1
此外,必须将flag声明为volatile,因为您正在正常程序流之外使用/更改它。这迫使编译器不优化任何读/写标志,并执行实际的读/写操作。
next-hack

@ next-hack是的,绝对正确,对不起,我只是想快速提出一个例子。

Answers:


18

有一种事实上的标准方法(假设使用C编程):

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

注意:您可能必须通过标头公开ISR函数原型,才能将其放置在另一个文件中的向量表中。但这不是问题,只要您记录它是一个中断并且不应被程序调用即可。
伦丁

如果反对意见是使用setter / getting函数增加的开销(和额外的代码),您会说什么?我自己仔细研究了一下,为我们的8位嵌入式设备考虑了代码标准。
Leroy105 '18

2
@ Leroy105 C语言到目前为止已经支持内联函数。尽管甚至inline不再使用,但是由于编译器在优化代码方面变得越来越聪明。我会说担心开销是“过早的优化”-在大多数情况下,开销根本不重要,即使它甚至出现在机器代码中也是如此。
伦丁

2
话虽这么说,在编写ISR驱动程序的情况下,所有程序员中大约80-90%(在此并不夸张)总是在其中出错。结果是一些细微的错误:错误地清除了标志,由于缺少易失性,竞争条件,糟糕的实时性能,堆栈溢出等导致的错误的编译器优化等。如果ISR没有正确封装在驱动程序内部,则出现这种细微错误的可能性是进一步增加。在担心诸如外围设备设置程序带来一点点开销之类的外围问题之前,应集中精力编写一个无错误的驱动程序。
伦丁'18

10
全局变量的这种使用对我不利

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

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

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

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

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

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

    有时我将1 s滴答计数器设为已用完的总时间计数器,因此将其设为32位宽。在我使用的许多小型微型仪器上,无法通过单个原子操作来读取该信息,因此这不是全局的。而是提供了一个例程,该例程读取多字值,处理两次读取之间的可能更新,并返回结果。

    当然,可能会有例程获得较小的1 ms,10 ms等,滴答计数器。但是,这实际上对您几乎没有任何帮助,增加了很多指令来代替单个单词的读取,并耗尽了另一个调用堆栈的位置。

    缺点是什么?我想有人可能会打错字,而意外地写到其中一个计数器上,这可能会弄乱系统中的其他计时。故意写到计数器上是没有意义的,因此这种错误应该像打字错误一样是无意的。似乎非常不可能。我不记得在100多个小型微控制器项目中发生这种情况。

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

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

    系统的其余部分仅能看到指示电源电压的实时更新值。它不知道也不需要关心何时确切地对其进行更新,尤其是因为它比低通滤波器建立时间更频繁地被更新。

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


在一个缓慢的星期里,我一直在接受治疗,真的是想挑剔我的代码。我看到了Lundin关于限制变量访问的观点,但我查看了我的实际系统,并认为任何人实际上都会对系统关键的全局变量造成不利影响。Getter / Setter函数最终会花费您大量的开销,而不仅仅是使用全局函数,并且接受这些都是非常简单的程序……
Leroy105 '18

3
@ Leroy105问题不是“恐怖分子”故意滥用全局变量。在较大的项目中,命名空间污染可能是一个问题,但是可以通过良好的命名来解决。不,真正的问题是程序员试图按预期使用全局变量,但未能正确使用。要么是因为他们没有意识到所有ISR都存在竞争条件问题,要么是因为它们弄乱了强制保护机制的实现,或者仅仅是因为它们在整个代码中喷出了全局变量的使用,从而产生了紧密的耦合和无法读取的代码。
伦丁'18

您的积分有效期奥林,但即使在这些例子中,替换extern int ticks10msinline int getTicks10ms()将在编译的程序集完全没有区别,而在另一方面,它将使很难在程序的其他部分意外改变其价值,也让你一种“挂接到”此调用的方式(例如,在单元测试期间模拟时间,记录对该变量的访问或其他操作)。即使您认为有经验的程序员将此变量更改为零的机会,也不会花费内联获取器的代价。
Groo

@Groo:只有在使用支持内联函数的语言时,这才是正确的,这意味着getter函数的定义需要对所有人可见。实际上,当使用高级语言时,我会使用getter函数,而较少使用全局变量。在汇编中,抓住全局变量的值要比使用getter函数麻烦得多。
奥林·拉斯洛普

当然,如果您不能内联,那么选择就不是那么简单。我想说的是,对于内联函数(许多C99以前的编译器已经支持内联扩展),性能不能成为反对getter的理由。使用合理的优化编译器,您应该最终得到相同的装配体。
Groo

2

任何特定的中断都将是全局资源。但是,有时使多个中断共享同一代码可能很有用。例如,一个系统可能有多个UART,所有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;
}

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


0

这是三个想法:

将flag变量声明为static,以将范围限制为单个文件。

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

使用信号对象(例如信号量)代替标记变量。ISR将设置/发布信号量。


0

中断(即指向您的处理程序的向量)是全局资源。因此,即使您在堆栈或堆上使用一些变量:

volatile bool *flag;  // must be initialized before the interrupt is enabled

ISR(...) {
    *flag = true;
}

或具有“虚拟”功能的面向对象的代码:

HandlerObject *obj;

ISR(...) {
    obj->handler_function(obj);
}

…第一步必须涉及实际的全局(或至少静态)变量才能到达该其他数据。

所有这些机制都添加了一个间接寻址,因此,如果您想从中断处理程序中挤出最后一个循环,通常不会这样做。


您应该将标志声明为volatile int *。
next-hack

0

目前,我正在为Cortex M0 / M4进行编码,而我们在C ++中使用的方法(没有C ++标签,因此此答案可能是题外话)如下:

我们使用一个CInterruptVectorTable包含所有中断服务例程的类,这些例程存储在控制器的实际中断向量中:

#pragma location = ".intvec"
extern "C" const intvec_elem __vector_table[] =
{
  { .__ptr = __sfe( "CSTACK" ) },           // 0x00
  __iar_program_start,                      // 0x04

  CInterruptVectorTable::IsrNMI,            // 0x08
  CInterruptVectorTable::IsrHardFault,      // 0x0C
  //[...]
}

该类CInterruptVectorTable实现了中断向量的抽象,因此您可以在运行时将不同的函数绑定到中断向量。

该类的接口如下所示:

class CInterruptVectorTable  {
public :
    typedef void (*IsrCallbackfunction_t)(void);                      

    enum InterruptId_t {
        INTERRUPT_ID_NMI,
        INTERRUPT_ID_HARDFAULT,
        //[...]
    };

    typedef struct InterruptVectorTable_t {
        IsrCallbackfunction_t IsrNMI;
        IsrCallbackfunction_t IsrHardFault;
        //[...]
    } InterruptVectorTable_t;

    typedef InterruptVectorTable_t* PinterruptVectorTable_t;


public :
    CInterruptVectorTable(void);
    void SetIsrCallbackfunction(const InterruptId_t& interruptID, const IsrCallbackfunction_t& isrCallbackFunction);

private :

    static void IsrStandard(void);

public :
    static void IsrNMI(void);
    static void IsrHardFault(void);
    //[...]

private :

    volatile InterruptVectorTable_t virtualVectorTable;
    static volatile CInterruptVectorTable* pThis;
};

您需要制作存储在向量表中的函数,static因为控制器不能提供this-pointer,因为向量表不是对象。因此,要解决该问题,我们在中有一个静态pThis指针CInterruptVectorTable。进入静态中断函数之一后,它可以访问pThis-pointer以访问的一个对象的成员CInterruptVectorTable


现在在程序中,您可以使用SetIsrCallbackfunction来提供指向static发生中断时要调用的函数的函数指针。指针存储在中InterruptVectorTable_t virtualVectorTable

中断功能的实现如下所示:

void CInterruptVectorTable::IsrNMI(void) {
    pThis->virtualVectorTable.IsrNMI(); 
}

因此,它将调用static另一个类的方法(可以是private),然后该方法可以包含另一个static this-pointer来访问该对象的成员变量(仅一个)。

我猜您可以构建并连接IInterruptHandler和存储对象的指针,因此static this在所有这些类中都不需要-pointer。(也许我们会在我们的架构的下一个迭代中尝试)

另一种方法对我们来说很好用,因为唯一允许实现中断处理程序的对象是硬件抽象层内部的对象,并且每个硬件块通常只有一个对象,因此使用static this-pointer 可以很好地工作。硬件抽象层为中断提供了另一种抽象,称为中断,ICallback然后在硬件上方的设备层中实现该抽象。


您是否访问全局数据?当然可以,但是您可以使所需的大多数全局数据私有化,例如this-pointers和中断函数。

它不是防弹的,它增加了开销。您将很难使用这种方法来实现IO-Link堆栈。但是,如果您对时序的要求不是很严格,那么在不使用可从任何地方访问的全局变量的情况下,在模块中灵活地获取中断和通信的抽象效果很好。


1
“这样您就可以在运行时将不同的功能绑定到中断向量上”这听起来像是个坏主意。该程序的“圈复杂性”将一扫而光。所有用例组合都必须经过测试,以便不存在时序冲突或堆栈使用冲突。IMO的功能有限,令人头痛。(除非您有一个引导加载程序案例,否则就是另外一回事了。)总的来说,这有点像元编程。
隆丁

@伦丁我不太明白你的意思。如果DMA用于SPI,我们将其用于将DMA中断绑定到SPI中断处理程序;如果UART用于DMA,则将其绑定到UART中断处理程序。当然,两个处理程序都必须经过测试,但这不是问题。而且它肯定与元编程无关。
阿森纳

DMA是一回事,中断向量的运行时分配完全是另外一回事。让DMA驱动程序设置在运行时可变是很有意义的。向量表,不是很多。
伦丁

@Lundin我想我们对此有不同的看法,我们可以开始讨论,因为我仍然看不到您的问题-可能是我的答案写得很烂,整个概念都被误解了。
阿森纳
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.