几乎每个嵌入式系统都要使用中断服务例程(ISR)。如果您需要跟踪时间,则可能会有一个定时器中断来生成系统时钟(system tick)。如果您有USART串口,那么可能正在使用中断。使用DMA(直接内存访问)来实现更高效的数据传输?您也可能在使用中断。
中断是嵌入式系统不可或缺的一部分。遗憾的是,编写不当的ISR可能会导致系统出现竟态条件、响应能力差,甚至CPU使用率过高。
在这篇文章中,我们将探讨编写有效ISR的几种最佳实践。
当您的代码正在执行进程时发生了重要事件,程序将被中断以跳转到中断服务例程。当发生该跳转时,寄存器的当前状态需要被存储在所谓的中断帧中。
中断帧被推到堆栈上,然后进入ISR。中断运行,然后恢复中断帧,应用继续运行。可以想象,每次运行中断服务例程时,这个过程都会消耗CPU周期并产生开销。根据处理器和架构的不同,周期数也不尽相同,但在中断的每一侧,周期数可能在12到几百之间。
虽然我们对中断开销无能为力,但我们可以控制中断中使用的周期数。ISR执行所需的CPU周期越多,其对应用程序代码的影响就越大。冗长而缓慢的中断可能会导致程序不同部分出现抖动和其他时序问题。它们甚至可能导致其他中断丢失或延迟!
所以经验法则是保持中断简短而快速!(这是从指令的角度出发,不一定是从代码行数的角度!)。
如果您希望ISR简短而快速,就应避免在ISR内部进行函数调用。函数(尤其是开销较大或执行复杂任务的函数)会显著增加ISR的执行时间。执行时间的增加可能会导致错过中断或延迟处理其他关键任务,从而可能导致系统不稳定。
在ISR中调用函数时,需要执行一些额外的步骤,例如将当前上下文推到堆栈、跳转到函数代码以及返回ISR。这些额外步骤会消耗宝贵的CPU周期。如果函数包含阻塞调用、等待I/O操作或依赖于中断期间可能处于不一致状态的资源,则可能会严重影响系统的响应能力和可预测性。
现在,您可能会认为编写ISR会违反软件开发的最佳实践。毕竟,我们不应该模块化我们的代码吗?如果没有函数,我们的ISR不是会变得一团糟吗?
我喜欢使用一些技巧来规避“不要调用函数”的最佳实践:
1)使用静态编译或预处理器。
根据语言的不同,复杂的计算或至少其中的一部分可能会在编译时执行。通过在编译时执行这些计算,ISR在运行时需要执行的计算就会减少。
2)内联函数(Inline function)
您仍然可以将ISR代码放入函数中,但不要将它们保留为常规函数,而是使用inline关键字。inline关键字会建议编译器不要调用函数,而是将该函数的内容 "复制并粘贴 "到调用者中。
内联函数将消除与函数调用相关的开销。请注意!这只是对编译器的一个建议!您必须在汇编中验证该函数是否确实被内联了。
注意:虽然今天大多数编译器都会采纳我们的建议,但你还不能完全地信任它!
通过避免在ISR内进行函数调用,可以保持中断处理的效率和可靠性,确保系统在各种条件下都能保持响应速度和稳定性。
中断并不是设计用来执行大量繁重工作的。我们希望中断简短而快速,这意味着它应该只执行最少的工作。例如,如果您通过USART接收作为数据包一部分的字节,您不会在中断中处理数据包。而是先处理字节,然后设置一个标志,指示程序的另一部分处理数据。
通过将密集型进程卸载到其他线程,可以确保ISR保持高效和快速响应。此方法可用于裸机或多线程环境。以下是如何有效地将处理卸载到ISR之外:
1)设置标志
使用简单标志来指示事件已发生。主程序或其他线程可以监视这些标志,并在安全的情况下进行必要的处理。
2)使用队列
使用队列以将数据从ISR传递到其他线程。这样,ISR可以快速将数据插入队列并返回处理中断,而主程序或专用工作线程可以处理队列的数据。
3)线程同步
确保ISR与其他线程之间正确同步,以避免出现竟态条件和数据损坏。根据需要使用互斥锁、信号量或其他同步机制。
通过遵循这些做法,您可以保持嵌入式系统的高水平性能和可靠性。这将确保ISR保持快速高效,同时安全可控地处理更复杂的处理过程。
当ISR和主程序共享变量时,将它们声明为volatile至关重要。volatile关键字告诉编译器变量的值可能随时更改(超出程序流的控制范围),从而防止编译器应用特定的优化,即假定变量值不会发生意外变化。
如果没有volatile关键字,编译器可能会优化掉必要的读取或写入,从而导致不可预测的行为和难以诊断的错误。Volatile将为您做三件事:
1)阻止优化
编译器假定,除非程序明确修改,否则非易失性变量不会发生变化。对于共享变量,此假设是错误的,因为ISR可以随时更改变量。将变量声明为易失性变量可防止编译器优化必要的读取或写入。
2)确保数据是最新的
使用volatile时,编译器将始终从内存中读取值,而不是使用寄存器中的缓存值。这确保主程序看到的是ISR写入的最新值,反之亦然。
使用volatile来共享ISR和应用程序之间的变量很简单。请看下面的示例:
volatile int flag = 0;
void ISR_Handler() {
flag = 1; // ISR sets the flag
}
int main() {
while (!flag) {
// Wait for the ISR to set the flag
}
// Proceed with the following steps
}
在此示例中,如果没有volatile关键字,编译器可能会将while(!flag)循环优化为一个无限循环,假设标志值在循环范围内永远不会改变。通过将标志声明为volatile,我们可以确保循环始终从内存中读取标志的当前值。
在我的博客文章“你的默认ISR是怎么工作的,你知道吗?”中,我讨论了大多数芯片供应商提供的工具如何使用默认处理程序填充未使用的ISR。这些默认处理程序通常只会使系统陷入无限循环,而这并不是我们希望系统做的事情!
尽管只是有可能会出错,最终导致意外中断触发,但世事难料!我强烈建议您更新默认ISR,设置一个可通过系统诊断任务检查的标志。这样,如果发生意外中断,您就可以检测到它,并让您的应用程序决定如何安全地处理它。
否则,当系统突然进入无限循环时,谁知道系统会处于什么状态呢?
注:另一种策略可能是进入循环并让看门狗恢复系统。不过,这不一定适合现在的情况。
ISR对每个嵌入式系统都至关重要。如果您希望系统反应灵敏且高效,就必须正确执行中断。我经常遇到性能不佳的系统,其根本原因就是ISR写得不好。
如果您遵循本篇文章中的最佳实践,您的中断就会表现得更好,引起的问题也会更少。
(原文刊登于EDN姊妹网站Embedded,参考链接:5 best practices for writing interrupt service routines,由Ricardo Xie编译。)