处理中断和使用WFI RISC-V CPU指令的预期/正确方式是什么?

7

我对裸金属编程和中断处理都非常陌生,但我一直在使用基于RISC-V FE310-G002 SOC电源的开发板进行学习。

我一直在研究RISC-V WFI(等待中断)指令,并从手册中了解到,它似乎不能保证使核心真正进入睡眠状态。相反,它只是建议执行可以停止到系统,该指令应被视为NOP。然而,这对我来说似乎相当无用。请考虑以下汇编程序片段:

wfi_loop:
WFI
J wfi_loop

由于WFI是不可靠的,因此需要采取措施。但是,从中断处理程序返回时,您仍然会被困在循环中。因此,您必须将其设置为与全局变量相关联,该全局变量的值在中断处理程序中更新,并且必须具有条件性。这似乎非常混乱。
此外,如果您的实现实际上确实遵守了WFI指令,并且在执行WFI指令之前触发了中断,则整个核心将停止运行,直到触发其他中断为止,因为它将在WFI指令之前返回。
似乎唯一正确的使用方法是在内核调度程序中使用该指令,当没有任务要执行时。但即使是这样,我认为您也永远不会想要从中断处理程序返回到这样的代码中,而是要重新启动调度程序算法。但这也是一个问题,因为您必须以某种方式回滚堆栈等等...
我一直在脑海中来回思考这个问题,但似乎无法找到安全的用途。也许,如果您使用CSRRS原子地启用中断,然后立即调用WFI,就可以解决问题:
CSRRSI zero, mie, 0x80
wfi_loop:
WFI
J wfi_loop
NOP
NOP

在中断处理程序中调用MRET之前,请确保将mepc寄存器增加8个字节。在返回之前,还必须在中断处理程序内部的mie寄存器中再次禁用中断。这种解决方案仅在WFI、J和NOP被编码为4字节指令时才是安全的,无论是否使用压缩指令。它还取决于程序计数器在CSRRSI指令启用中断后到达WFI指令之前,以使中断能够在代码的安全位置触发,并以打破等待它的循环的方式返回。
我想我只是想了解硬件的行为,因此如何正确地调用和返回中断并使用WFI指令?

3
这是一种适用于事件驱动解决方案的工具,不适用于启用一个中断后调用wfi等待中断的情况,对于这种情况,不要中断处理器,请轮询外设。通俗来说,当您不关心前台等待下一个中断需要多长时间时,可以使用它。 - old_timer
在某些架构中,降低功耗的方法是采用事件驱动的方式,等待中断或事件的发生,处理完后再返回等待状态。如果除了设置之外没有前台代码,则可以将wfi放入无限循环中。 - old_timer
1
我不了解RISC-V,但这个指令看起来很像x86的hlt。它会“停止”直到下一个硬件中断请求(IRQ)发生。然而,它经常被误解为永久“停机”。因此,wfi是更好的助记符选择。 - ecm
话虽如此,这里是我的空闲函数,其中sti\ hlt作为最后一次尝试让机器处于空闲状态。在等待按键输入时(在检测到没有可用的按键之后),它会在此处被调用(https://hg.ulukai.org/ecm/ldebug/file/d6c84ad50855/source/lineio.asm#l1042)。我们不关心发生哪个中断请求,唯一需要的是如果按下键(IRQ 1),它将恢复。本质上,这是在等待时进行轮询和睡眠。(86-DOS应用程序假定它们是唯一的前台任务。) - ecm
由于hlt引起的睡眠可能很短(特别是在虚拟机或类似环境中运行时),但对于忙碌循环而言,我们认为很短的时间对于机器来说已经非常长了。适当地空闲可以使CPU在易于>95%的时间内降频和睡眠。我想wfi在这些情况下与hlt相同。 - ecm
2个回答

5
因此,您必须将其与全局变量的条件相结合,该变量的值在中断处理程序中更新。
无论 wfi 的实现如何,您都必须这样做,因为您不知道是什么事件导致 hart 醒来。当执行 wfi 时,可能启用 n 个中断,并且可能引发其中任何一个。 wfi 是一种优化,它可以节省电力,直到发生某些事情。正如您所指出的,操作系统调度程序可能会发现自己处于无法调度任何线程的状态(例如,它们都等待 I/O,或者根本没有),在这种情况下,它必须执行类似于以下内容的操作(具有所有必要的可见性和原子性语义):
while ( ! is_there_a_schedulable_thread());

这只是等待
但是调度程序可以使用以下方式,而不是旋转紧密的循环(可能会影响性能和功耗):

while ( ! is_there_a_schedulable_thread())
{
  __wfi();
}

最坏的情况下,它就像是一个紧密循环,但最好的情况下它会暂停处理器,直到发生外部中断(这意味着可能已经完成了一个IO操作,因此一个线程可以被释放来运行)。

即使没有线程存在的情况下,由于计时器中断而每隔x微秒唤醒比浪费电力的循环要好。

wfi 在嵌入式编程中也非常有用,如果您恰好所有的工作都在中断处理程序上(例如当按下按钮或类似的操作时)。
在这种情况下,main函数将永远循环,就像调度程序一样,但没有退出条件。
wfi指令将极大地提高电池寿命。

然而,并不是所有情况下都可以使用wfi,否则您可能会等待从未发生过的中断(事实上,这是一条特权指令)。

可以把它看作是与硬件协同的优化。

特别地,它并不是设计为确保中断已触发的方法:

void wait_for_int(int int_num)
{
   //Leave only interrupt int_num enabled
   enable_only_int(int_num);
   __wfi();
   restore_interrupts();
}

在特定的RISC-V实现中可以使用这种方式,但从伪代码中可以看出,这种方式并不是非常方便。
通常情况下,操作系统无法承受禁用除一个外的所有中断。
然而对于嵌入式应用程序来说,这是可能的。


4
应该有一个任务/线程/进程用于空闲,它应该看起来像你的第一段代码。
由于空闲线程被设置为具有最低优先级,如果空闲线程正在运行,这意味着要么没有其他线程可以运行,要么所有其他线程都被阻塞。
当发生解除某个其他线程阻塞的中断时,中断服务例程应该恢复那个被阻塞的线程,而不是被中断的空闲线程。
请注意,阻塞IO的线程本身也会被中断——它通过自己使用的ecall而被中断。该异常是对IO的请求,并导致该线程阻塞—直到满足IO请求之前,它无法恢复。
因此,阻塞在IO上的线程被暂停,就好像被中断一样——时钟中断或IO中断能够恢复不同于立即中断的进程,这将会发生在空闲进程正在运行并且某个等待进程的事件发生的情况下。
我的做法是使用 scratch csr 来指向当前正在运行的进程/线程的上下文块。在中断中,我只保存了必要的寄存器数量,以便(开始)处理中断。如果中断导致某个其他进程/线程变得可运行,那么在中断恢复时,我会检查进程优先级,并可能选择上下文切换而不是恢复被中断的内容。如果我恢复了被中断的内容,则是一个快速恢复。为了切换上下文,我完成保存中断线程的CPU上下文,然后恢复另一个进程/线程,同时切换scratch寄存器。
(对于嵌套中断,我不允许在恢复时进行上下文切换,但在保存当前上下文之后,我会将scratch csr 设置为一组上下文块的中断栈,然后重新启用更高优先级的中断。此外,作为非常小的优化,我们可以假设自定义编写的空闲线程除了其pc以外不需要保存/恢复任何信息。)

这应该给我一个方向。我需要实现一个线程/进程处理系统,但这应该是一个有趣的子项目。感谢您的帮助。我认为我太深入细节了,无法看到正确的使用方式。 - Echelon X-Ray
这是一个很好的回答,但是也许应该指出这并不是唯一的方法。如果我有时间的话,我会在这里发布我的另一种答案。 - undefined

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