如何保持中断短小?

11
嵌入式编程中最常听到的建议是“保持你的中断时间短”。
现在我的情况是,在我的main()循环中有一个非常长的任务(将大块数据写入SD卡),可能需要100ms。所以为了保持系统响应,我把所有其他东西都移到了中断处理程序中。
例如,通常情况下,人们会在中断中处理传入的UART数据,然后在main()循环中处理传入的命令,然后发送回响应。但在我的情况下,由于我的main()循环可能会被阻塞相对较长的时间,因此整个处理/处理命令的过程也发生在中断中。
最佳解决方案是切换到RTOS,但我没有足够的RAM。是否有其他设计替代方案,可以使中断时间变短?

你有多少RAM?对于RTOS来说,16K足够了,如果不使用太多线程/堆栈,也许8K就可以。在我的系统中,最简单的“Flash LED”线程只需要12个字的堆栈(两个睡眠调用)。 - Martin James
你用RTOS就需要8k的内存?我不想引发争论,但是我还没有被证明需要任何RTOS,当有必要使用操作系统时,已经有适用于硬件的Linux版本。 - John U
@JohnU 他说最佳解决方案是实时操作系统,但他的内存不足。 - DarthRubik
4个回答

10

传统的方法是通过中断调度延迟程序,并尽快结束中断。

一旦中断完成,将按照从最重要到最不重要的顺序遍历延迟程序列表。

考虑这样一种情况:您有一个主要(较低优先级)操作和两个中断I1和I2,其中I2比主要操作更重要,但比I1不重要。

在这种情况下,假设您正在运行主要操作并且I1触发了。 I1安排了延迟程序并向硬件发出I1已完成的信号。 I1的DPC现在开始运行。突然,来自硬件的I2进入。 I2的中断接管了I1的DPC并安排了I2的DPC并向硬件发出完成信号。

调度程序然后返回到I1的DPC(因为它更重要),当I1的DPC完成时,I2的DPC开始(因为它比主要操作更重要),然后最终将执行返回到主要操作。

此设计允许您安排不同中断的重要性,鼓励您保持中断小,并允许您以有序和按顺序优先的方式完成DPC。


1
你的表述比我更好,所以我会删除我的回答。 - John Saunders
我不知道如何实现这样的“延迟程序”,但我认为它非常接近我现在正在使用的(ARM)PendSV中断。它可以从更高优先级的中断中设置为待处理状态,并在中断完成后立即执行。从那里开始我的主要工作。 - Maestro
在主循环中,为每个过程设置一个标志和一个调度程序。在每次迭代中,调度程序按优先级顺序检查标志,并运行第一个标志已设置的过程。 - starblue

10
有100种不同的方法可以完成这个任务,这取决于CPU架构(中断嵌套和优先级、软件中断支持等),但让我们采用一个相当直接简单且没有抢占内核的竞争条件和资源共享风险的方法。
(免责声明:我的第一选择通常是预先实时内核,其中许多可以在极度资源受限的系统中运行... SecurityMatt的建议很好,但如果您不熟悉实现自己的可抢占内核/任务切换器,尤其是处理异步(由中断触发的)抢占,您可能很快就会感到困惑。因此,我下面提出的方案不像基于抢占的内核那样响应迅速,但它更简单,通常足够)。
创建3个事件/工作队列:
1. Q1 是最低优先级,处理缓慢的后台SD卡写入。 2. Q2 保存处理传入UART数据包的请求。 3. Q3(最高优先级)保存UART RX FIFO读取请求。
我将UART RX FIFO的读取和读取包的处理分开,以便FIFO的读取始终优先于包的处理;也许您想将它们放在一起,这是您的选择。
"To make this work, you need to divide your large (~100ms) SD card write process into smaller, discrete steps that can be completed independently. For example, if you want to write 5 blocks, each taking 20ms, you first write the first block and then add "write next block" to Q1. After each step, you return to the scheduler and scan the queues in priority order, starting with Q3. If Q2 and Q3 are empty, you take the next event from Q1 ("write next block") and run that command for another 20ms before returning to scanning the queues again. If 20ms is not fast enough, you can break down each 20ms block write into more detailed steps and continuously add the next work step to Q1."
现在讲一下串口接收相关的内容;在串口RX ISR中,你只需将“读取串口FIFO”命令入队到Q3中,然后从中断返回到被打断的20毫秒“写入块”步骤。一旦CPU完成了写入操作,它就会回去按优先级顺序扫描队列(最坏情况的响应时间为20毫秒,如果块写刚好在中断发生时开始)。队列扫描程序(调度程序)会发现Q3现在有任务要做,然后在回去再次扫描之前运行该命令。
你的系统的响应性,最坏情况下,将由系统中最长的运行至完成步骤决定,而与优先级无关。通过以小、离散、运行至完成步骤的方式进行工作,可以使系统非常灵敏。
请注意,这里必须以概论形式讲解。也许你想在ISR中读取UART RX FIFO,将数据放入缓冲区,并仅推迟数据包处理,而不是实际读取FIFO(那么你只需要2个队列)。你必须自己解决这个问题。但我希望这种方法是有道理的。
这种优先队列驱动的事件处理方法正是Quantum Platform (QP)事件驱动框架所采用的方法。 QP实际上支持底层的非抢占式(协作式)调度程序,如本文所述,或者是每次排队一个事件就运行调度程序的抢占式调度程序(类似于SecurityMatt建议的方法)。 您可以在QP网站上查看QP协作式调度程序的代码/实现。

非常好而且全面的回答!但是我不能将microsd写入分成小步骤的原因是,即使我每次只写一个字节,在每个簇边界上,即使是最小的写操作也可能需要很长时间(比我想要的响应时间长得多)。除此之外,多扇区写入被证明更快,所以我尽可能地缓冲,然后一次性完成所有操作(需要很长时间)。 - Maestro
明白了。我看到你正在使用ARM Cortex和PendSV,这正是PendSV的设计目的(通常用于执行上下文切换,但其工作优先级低于任何其他中断,但高于任何后台活动)。 - Dan

3
另外一个解决方案如下:
在FAT库长时间占用处理器的任何地方,插入一个调用新函数的语句。这个新函数通常非常快,并且在几个机器周期后就可以返回给调用者。这样的快速函数不会影响读写SD Flash等耗时操作的实时性能。您需要在等待擦除Flash扇区的任何循环中插入这样的调用。同时,在每读写512字节之间也需要插入调用。
该函数的目标是执行大部分普通嵌入式设备中“while(1)”循环内的任务。它首先会递增一个整数并对新值进行快速求模运算,如果模数不等于任意常数,则返回。代码如下:
void premption_check(void)
{
    static int fast_modulo = 0;
    //divide the number of call
    fast_modulo++;
    if( (fast_modulo & 0x003F) != 3)
    {
        return;
    }
    //the processor would continue here only once every 64 calls to "premption_check"

下一步,您需要调用从串口中断中提取RS232字符/字符串的函数,并处理任何完整字符串收到的命令等。
上面使用的二进制掩码0x3F意味着我们只查看计数器的最低6位。当这6位恰好等于任意值5时,我们会继续调用可能需要一些微秒甚至毫秒执行的函数。根据您想要服务串口和其他操作的速度,您可以尝试更小或更大的二进制掩码。您甚至可以同时使用多个掩码来更快地服务某些操作。
当两个Flash擦除操作之间发生一些零散的延迟时,FAT库和SD卡不应该出现任何问题。
这里提供的解决方案即使是像8051的许多变体一样只有2K字节的微控制器也可以使用。令人难以置信的是,1980年到1990年的弹球机拥有几K的RAM,慢处理器(如10 MHz),它们能够测试一百个开关... 完全去抖动,更新X/Y矩阵显示,产生声音效果等。这些工程师开发的解决方案仍然可以用于提高大型系统的性能。即使是具有64 Gig RAM和许多Terabyte硬盘的最佳服务器,我认为当一些公司想要索引数十亿个WEB页面时,任何字节都很重要。

2

由于还没有人建议从这个方面入手,所以我会提出一个建议:

SD卡服务例程放在低优先级中断中,如果可能的话,可以加入一些DMA,这样就可以释放主循环和其他中断,使其更具响应性,而不是被困在main()循环中长时间等待某些操作完成。

需要注意的是,我不知道硬件是否有任何触发中断的方式,当SD卡准备好进行更多操作时,您可能需要通过运行轮询计时器来检查和强制中断。但如果您有多余的硬件计时器和中断,则可以使用非常少的开销来完成此操作。

对于像这样的事情,采用RTOS似乎有些过度设计和失败的承认... ;)


我不直接与卡通讯,而是与一个暴露出简单的POSIX函数(如f_read()和f_write())的FAT库通讯。因此,即使SPI传输是非阻塞的,对f_write()的调用仍将是阻塞的,因为它在整个写入完成之前无法返回。如果我使用低优先级中断来调用f_write(),我的main()循环仍将被阻塞(就像现在一样),因为它的优先级比中断还要低。因此,我看不到将sd卡处理移动到中断的任何优势。 - Maestro
我假设你没有使用RTOS,这样你就可以更直接地访问该过程或者自己编写程序。你是否拥有该库的源代码,这样你就可以进行修改了?那将是我下一个解决方案。 - John U
是的,这是非常流行的FatFS库(http://elm-chan.org/fsw/ff/00index_e.html)。但是将其重写为异步操作需要很多工作(我甚至怀疑是否可能)。 - Maestro
一切皆有可能。我并没有深入研究SD卡IO,但我不认为你不能修改驱动程序代码,在忙碌时返回一个新的、不同的状态,而不仅仅是阻塞。 - John U
换句话说,我认为老板不会允许我花半年时间编写完全不同的FAT库,只是为了使我的中断保持简短;) - Maestro
1
这总是一个约束问题;不了解您的项目,此问题可能值得付出努力,也可能微不足道。也许您的老板不会支付您花费半年时间移植到RTOS并调试它,再加上许可费用...也许您的老板会支付您在SD卡和主要微控制器之间放置一个小型廉价微控制器以消除瓶颈,或者由于硬件限制而无法实现...有很多选择。也许您的老板会支付编写库的人为您使其非阻塞。 - John U

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接