Blogs

微控制器介绍 - 更多关于中断

Mike Silva. 2013年9月25日

快速链接

关于中断机制的详细信息

现在是时候仔细看看中断请求和响应时发生一些。 这是一般术语,不同的微控制器设计可能会有所不同,但基础仍然是相同的。 大多数但并非所有中断请求都是锁存,这意味着中断事件设置一个标志,即使中断事件也消失,即使中断事件也会消失。 它是此锁存标志,实际上生成了中断请求。 锁定标志可以以两种不同的方式清除。 首先,通常可以通过用户代码手动清除,通常通过将“1”写入关联寄存器中的标志位位置。 其次,当中断响应时,可以自动清除标志,并且ISR开始运行。   如果是这种情况,它意味着ISR不需要明确清除标志,因为它已经将在ISR开始运行时已经清除,但是 检查数据表是否有任何特定的中断源,以查看是否是这种情况。

如果设置了相关的中断启用,则设置的任何中断请求标志,据说是“待处理”。 如果启用了微控制器全局中断,并且启用了一个或多个单独的中断,则CPU硬件将在执行每个指令期间自动检查所有这些单独的中断标志。 如果仅找到一个标志,则在当前指令结束时,CPU跳转到或“向量,”中断的ISR。 在这样做时,可以自动清除中断标志(取决于设计),保存返回地址(将执行的下一个指令的地址)保存,并且可能保存一些CPU标志,并且可能一些CPU寄存器已切换,ISR开始执行。 许多盛藏和可能性,所以必须读取数据表!

本文以PDF格式提供,便于打印

如果找到了一个以上的启用中断的请求标志,那么如果多个中断是待处理的,那么事情会变得更加复杂。 将始终分配给每个中断源的优先级。 该优先级可以固定在硬件中,或者它可以由用户程序配置。 挂起的中断具有最高优先级的是将为中断(即谁将运行谁)。 其他中断请求标志将保持设置 - 也就是说,其他中断将保持在待定状态。

接下来会发生什么,使用ISR运行和其他中断挂起,还取决于CPU设计(请参阅此处?)。 一些微控制器,例如AVR,禁用全局中断作为中断矢量过程的一部分。 因此,ISR开始运行全局中断禁用以及暂停的任何其他挂起中断。 当ISR结束时,全局中断状态将被设置为在ISR末尾发生的状态恢复的一部分。 在此处设置全局中断状态是合法的,因为必须启用它首先输入的ISR。 因此,现在中断的后台代码的下一个指令将开始运行,并且CPU将识别出一个或多个挂起的中断,并且再次在该指令的末尾进行服务,并且剩余的任何其他指令都将在该指令的末尾进行服务挂起,所以它走了。

以后回来 - 如果禁用中断,则中断请求会发生什么?

如上所述,中断事件经常锁存。 这意味着当事件发生时,将设置标志,并且在某些不同的动作清除它之前,该标志仍​​然保持设置。 特别是,即使设置事件消失,标志也会仍然设置。 例如,我们可以在我们的外部中断引脚上获得低脉冲,该引脚设置内部中断标志。 即使脉冲然后结束(线路返回高),中断请求标志将保持设置。 这会发生中断是否已启用或禁用,是特定中断或全局中断。 该标志将保持设置,直到代码清除(通常通过将“1”写入相关寄存器中的标志位位置),或直到中断并服务,此时硬件或用户ISR将清除旗帜。

锁存中断标志的一个后果是,如果先前已经满足中断条件,则可以在首次启用中断时获得中断的突发。 可能发生这种情况的情况下,具有边缘触发的外部中断,其中触发边缘可能发生作为系统初始化的副作用,或者只是由于所有电子设备的波动的波动。 出于诸如这些之类的原因,在启用全局中断之前清除任何此类中断标志通常是一个好主意,以避免响应杂散中断请求。 最好的方法是使其成为一般规则,以便在代码之前应该清除特定的中断标志,以便在代码启用该中断。

对于锁存或边缘触发的中断,此图显示了中断事件如何设置锁存标志,然后在启用中断时,即使中断事件已经消失,也会在中断时生成中断请求。 如果这是一个级别触发的中断,则不会生成中断请求,因为中断事件在启用中断时消失。

锁定中断

打击 - 在ISR内部出现中断请求时会发生什么?

到目前为止,我们只讨论了中断背景代码的中断。 但是一个试图中断ISR的中断呢? 例如,假设我们有两个外部中断,INT1和INT2。 INT1已中断,并且INT1 ISR正在执行,然后在INT2上进行中断请求。 What happens now?

好吧,两件事之一可能发生。 新中断可能会被阻止,直到INT1 ISR完成,此时它将被维修。 或者,如果在INT1 ISR中已启用中断,则(或者对于某些微控制器设计,如果INT2已设置为比INT的更高优先级),则INT1 ISR将以与背景代码中断的相同方式中断,并且INT2 ISR将运行,并且当完成时,INT1 ISR将恢复运行。

此图显示了第一种行为,INT2被保持为OFER1完成。 请注意ISR1如何在INT2响应之前返回以执行一个或多个背景代码指令。 一些处理器设计,包括ARM Cortex,具有特殊的硬件,避免浪费一点努力,并且在ISR1结束后立即运行ISR 2,而无需短返回背景代码。

连续的中断

这 绘图显示第二个行为,INT2中断ISR1。 对于AVR,如果ISR1重新启用本身内部的全局中断,则会发生这种情况。 对于STM32,如果Int2比Int1更高的优先级,则会发生这种情况。

打断中断

第一件事先

写作您的ISR非常重要,以至于您对ISR的开始,您有一些需要立即完成的东西,并且稍后可以做些什么。 你经常想要改变 one or more GPIO输出或寄存器和/或读取 one or more GPIO输入或寄存器,在ISR的开始时。 然后,您可以完成其他家庭清洁必须在ISR中完成。 此规则非常紧密地取决于中断源是什么,以及所需的ISR操作是什么,以及在大多数情况下都是关联相关硬件需求的情况。 在简单的中断驱动的LED程序中,从最后一章中,首先更改LED输出,然后改变了下一个中断边缘,并且清除了当前中断标志。 在闪烁的情况下,LED的关注当然是完全不重要的,但它说明了这个概念,并且将有ISR在那里重要。 请记住一般原则 你想先做第一件事,第二件事之后。

中断的可怕方面

我在最后一章中说中断并不魔术。 I take it back.  中断表现得完全像魔法,如果你没有统治他们,那就是魔法的奇怪魔法。 中断可以垃圾丢失你的数据是似乎不可能的方式。 它们还可以拒绝执行清晰写入的代码。 但它们非常有用 - 真的 - 我们只需编写避免其危险的代码。

中断的大多数问题与其主要福利有关,即在几乎任何时候都可以中断运行代码,并且代码永远不会知道它被中断。 好像您可以冻结后台代码的时间,请执行数据或I / O的任何操作,然后重新启动后台代码的时间。 提供比喻,想象一下,你即将坐在椅子上。 你看起来并看到椅子可用,然后你转身开始坐下来。 然后为你冻结,并腐败中断移动椅子,或者将蛋糕放在上面,然后再次为您开始。 只有现在,正如你即将达到你所想到的安全椅登陆,你认为你知道它已经改变了,而不是更好。 这就是使用中断设计的软件。 您的代码可以遇到像值损坏的情况,没有警告,但每17天只有一次。 祝你好运! 更好地拯救您的理智并了解此类潜在问题背后的原因,并在可能发生之前防止它们。

数据是问题(或者,背景代码讨厌惊喜!)

在我们中断驱动的LED程序中,我们从未尝试过在后台代码和ISR之间的数据(忽略配置寄存器的设置)。 这实际上是一个罕见的中断情况。 大多数ISR都将涉及将数据传递到背景代码和/或从背景代码中检索数据,这是事情可能会变坏的地方。 每当后台代码处于访问数据的中间时,都会保证出现问题,并且中断以及相关的ISR然后访问相同的数据。

作为示例,想象一个系统,其中外部触发产生中断,并且在ISR中,从一些外部传感器读取新的温度和压力值对。 在背景码中,基于当前温度和压力来计算一些阀门位置。 现在想象在代码中的这个序列:

  • 背景码负载温度
  • 中断出现,更新温度和压力
  • 背景码负载压力
  • 背景码根据温度和压力计算阀门位置

刚刚发生的是,背景码现在已经加载了旧温度和新压力,并且将基于该不匹配的数据计算阀门位置,这既不适用于先前的测量,也不是目前的测量。 它是损坏的数据。 细节可能会发生变化,但基本问题是相同的:ISR或背景代码认为它正在获得一个Vaiid一组数据,但实际上它变得损坏了数据 - 一些新的,一些新的。

事情甚至可能会变得更糟。 即使是单一数据也可能损坏。 想象一个8位微控制器,可从ISR接收16位数据值。 要在8位设备上加载16位数据值,需要两个8位负载。 根据CPU设计,序列可以是读取LO 8位(最低有效字节或LSB),然后读取HI 8位(最重要的字节或MSB),或者它可以是MSB,后跟LSB。 在任何情况下,想象一下,背景代码已加载前8位(假设LSB),那么中断出现,并且ISR将新的16位值写入存储器变量。 在中断结束时,背景代码恢复并加载2nd 8位(MSB),但现在它具有从先前值的LSB和新值的MSB构建的16位值。 这既不是旧价值也不是新价值,而是两者的结合损坏了。 这是一个数字示例:

  • 16位变量x保持0x1234
  • 背景码加载0x34的LSB
  • 中断触发器和ISR将X设置为0x3421,然后返回
  • 背景码加载0x34的MSB
  • 现在背景代码包含0x3434(!!!)的x值

还有三分之一的中断可能导致数据损坏。 记住我们的第二个LED Blinky程序,其中包含以下无辜者 line:

PORTB ^= (1<<PB0);

这is what is called a read-modify-write (RMW) sequence.  实际发生的是:

  • 将端口值读入CPU
  • 修改CPU中的值(在这种情况下为XOR 1)
  • 将修改的CPU值写回端口

以下是C代码上面的实际AVR编译器输出(“IN”是读取指令,“OUT”写指令,“EOR”是独占或指令):

      PORTB ^= (1<<PB0);
91 e0     ldi r25, 0x01   ; 1
88 b3     in r24, 0x18 
24 89 27  eor r24, r25 
88 bb     out 0x18, r24   ; 24

想象一下,背景代码刚刚读取了端口值,它具有 PB0设置为“0”,并且发生中断,并且中断ISR将新的端口值设置为“1”。 然后,后台代码恢复并写出已将PB0设置为“1”的修改后的端口值 但没有改变任何其他比特。 这个新写的输出  不反映港口输出变化 in the ISR.  我们的端口值现在垃圾 - PB1是'0',它应该是'1':

  • 端口包含值0b00
  • 背景代码读取端口
  • 中断触发ISR将端口设置为0B10,然后返回
  • 背景代码将端口的值修改为0b01并将其写出来
  • 现在端口包含0b01而不是正确的0b11

内存位置上的RMW序列可以发生完全相同的事情。 毕竟,端口只是一种内存位置的形式。 所以这里是另一方面,中断可能导致数据损坏。 Are you scared yet?  You should be.  在涉及中断时,您从未在正常的顺序编程中进行两次考虑的事情可能会在脸上爆炸。 ISRS必须共享对数据的访问与背景代码有用,但如果没有严格控制,这一非常共享的访问将以多种方式传输您。

禁用危险中断围绕关键部分 - AVR版本

此代码段仅显示我们以前的AVR_INT1程序的main()函数,但是使用代码禁用,然后在输出端口上执行RMW的单行关键部分重新启用INT0中断。 stm32禁用和重新启用将是相同的,但使用exti_imr寄存器。

int main(void)
{
  DDRB = (3<<PB0);      // LED output on PB0 (blinking) and PB1 (interrupt-driven)
  PORTB = (3<<PB0);
  
  MCUCR = 0b10<<ISC00;  // negative edge trigger
  GIFR = 1<<INTF0;      // clear any pending interrupt
  GICR = 1<<INT0;       // enable INT0
  
  sei();                // enable all interrupts
  
  while(1)
  {
    GICR &= ~(1<<INT0); // disable INT0 - could use cli() here instead
    PORTB ^= (1<<PB0);  // toggle PB0 in the background - critical section!
    GICR |= (1<<INT0);  // re-enable INT0 - could use sei() here instead

    delay(80000);
  }

}

让它停下来!

好的,你永远从中断掉了吗?  不要,有一个解决方案。 所有此类案例所需的是要简单地防止这种重叠访问数据。 为此,必须允许任何代码,当开始访问共享数据时,必须允许对该数据的不间断访问。 在数据由一个或多个中断共享的情况下,这意味着在后台代码访问共享数据时,不得允许运行这些特定中断。 背景代码或ISR必须等待其转向访问数据。 如此安全的数据访问,其中没有可能中断或损坏,称为“原子访问”,这意味着访问是不可分割的。 任何必须具有对数据(或任何其他资源)的不可分割访问的代码部分称为“关键部分”。

实现此解决方案的沉重但简单的方法是在访问共享数据时简单地禁用所有中断,这可能是一个充足的解决方案。 一种更良好的表现良好的解决方案是只禁用将访问有问题的共享数据的中断,同时仍将其它中断留到启用状态。 毕竟,中断无法损坏被访问的数据 cannot be a threat.

另一种解决方案是防止第一次分享数据。 如果只有一条代码(背景或ISR)可以访问一段数据,则数据不能损坏。 For example, if 只要 背景码 或者 ISR访问了输出端口,从未发生过上面的情况。 有时可以重新排列一个程序,以便避免共享,但这可能只是意味着现在需要共享一些其他数据。

以下是我们的AVR中断驱动的LED程序 有适当的保护 反对RMW腐败 PORTB. 完成所有这些都是禁用背景代码修改PortB之前的数据共享中断,并在后立即重新启用它。 这实际上在这个简单的情况下禁用和启用全局中断,这实际上是为了禁用和启用全局中断(因为我们无论如何都没有其他中断),但它显示了该技术。 具体的中断或全局中断,进行分析并选择您的方法。

硬件救援

既然我们刚才看到如何保护自己免受对GPIO输出的共享访问,我们有好消息。 越来越多的微控制器正在解决原子访问问题 使用特殊硬件的GPIO输出。 STM32是一个很好的例子。 除了具有标准输入和输出GPIO寄存器之外,STM32每个端口有两个更多寄存器。 一个,端口复位寄存器允许立即清除所选择的比特而不改变任何其他位,另一个端口设置寄存器,允许立即设置所选择的比特,而不改变任何其他位。 由于这些操作不可中断,因此可以没有RMW腐败。 但它变得更好。 端口设置寄存器使用低16位来标识要设置的端口位,但它还使用高16位来识别端口引脚以在相同的指令中清除。 这意味着单个指令可以原子地设置或清除任何指定的输出比特组合。 Very nice!  这表明有时您需要远离您的标准方法(xoring输出端口位),并利用您特定UC必须提供的特殊好东西。

使用芯片特定的硬件删除关键部分 - STM32版本

此代码段还显示Main()函数,STM32_Int1程序的这一时间。 它添加了一些逻辑来使用原子集和清除输出端口寄存器。 通过这样做,我们完全消除了关键部分,因此我们不需要禁用并重新启用可能损坏此类关键部分的中断。 AVR版本可以做同样的事情 - 完全消除关键部分 -  通过使用切换的写入引脚寄存器方法,或使用SET-BIT和清除位指令(无论如何都应该生成AS6编译器,用于单位更改)。 它表明,有时您需要远离您的标准方法(xoring输出端口位),并利用您特定UC必须提供的特殊好东西。

int main(void)
{
  uint8_t flag = 0;

  RCC->CFGR = 0b100 << 24;      // HSI, 8 MHz, SYSCLK->MCO

  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // enable PORTA for button input
  GPIOA->CRL = (0b0100);        // CNF=1, MODE=0 (floating input)

  RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // enable PORTC for LED output
  GPIOC->CRH = 0b0010 | (0b0010 << 4);     // CNF=0, MODE=2 (2MHz output) (PC8,PC9)

  AFIO->EXTICR[0] = 0;          // EXTI0 is PA0
  EXTI->RTSR = 1;               // rising edge, EXTI0
  EXTI->IMR = 1;                // enable EXTI0
  NVIC->ISER[0] = (1 << EXTI0_IRQn);


  while (1)
  {
    if (flag)
    {
      GPIOC->BRR = (1<<9);      // atomic clear PORTC bit 9
      flag = 0;                 // toggle flag
    }
    else
    {
      GPIOC->BSRR = (1<<9);     // atomic set PORTC bit 9
      flag = 1;                 // toggle flag
    }

    delay(80000);
  }
}

一些微控制器,包括AVR,对此问题采取了不同的方法。 AVR具有单个指令,可以在某些内存区域中设置或清除单个位,包括GPIO端口。 任何良好的AVR编译器都应该在看到单个GPIO位并将其视为0或或者或或或或者1​​时生成这些指令。 因此,要设置或清除单个GPIO位,即使高级代码似乎意味着RMW周期似乎暗示,这些指令也会原子地完成作业。 不幸的是,这仅适用于一次设置或清除一位,与STM32能够一次在多个比特上运行。 AVR的较新型号也有另一个原子 诀窍,这是通过写入'1到某些GPIO寄存器的PIN位,该端口配置为输出的位 will toggle. 也就是说,通过写入'1到端口输入寄存器,任何端口 设置为输出的位将切换。 这是古怪的,但它可能很有用。

许多其他UC寄存器,在各种UC中 models, 也有一种原子访问。 这涉及清除硬件设置的事件或中断标志而不触摸寄存器中的任何其他位。 通常,这将需要一个RMW周期来归零所需的比特,但正如我们所看到的那样,RMW可以导致数据损坏。 因此,这种形式的原子访问允许我们将“1”写入任何我们想要清除的任何钻头,以及任何其他比特的“0”(不触摸的那些比特)。 这是有效的,因为我们通常只想清除他们通常的问题 set by the hardware. 您将跨越此“写入”1'来清除寄存器位“范式,因此,请注意它。 只是因为什么都不应该容易,一些UCS切换了一些事情,以便'0'清除寄存器位,而“1”没有。 我不能这么说 - 阅读数据表!

挥发或不挥发

就在你认为它终于安全地使用中断时,这是一个新的哥哥。 这不是一个根本的问题,但是关于一些编制者工作的警告。 在某些编译器的情况下,例如在Atmel Studio中使用的AVR-GCC,如果在后台代码和ISR中使用变量,则该变量必须被声明为“易失性”,或者优化器将过度达到此处的结果写入变量似乎不会发生。 因此,找出您的编译器是否需要为此类共享变量“易失利”,如果它确实如此,在使用关键字之前发布一个大笔记,直到使用关键字成为第二种性质。


要发布回复评论,请单击连接到每个注释的“回复”按钮。发布新的评论(不是回复评论),请在评论的顶部查看“写评论”选项卡。

注册将允许您参加所有相关网站的论坛,并为您提供所有PDF下载。

注册

我同意 使用条款隐私政策.

尝试我们偶尔但流行的时事通讯。非常容易取消订阅。
或登录