Search code examples
assemblyinterruptinterrupt-handlingriscvriscv32

What is intended/correct way to handle interrupts and use the WFI risc-v cpu instruction?


I am very new to bare metal programming and have never delt with interrupts before, but I've been learning on a RISC-V FE310-G002 SOC powered dev board.

I've been reading about the RISC-V WFI (Wait for interrupt) instruction and from the manuals, it doesn't sound like you can rely on it to actually sleep the core. Instead, it only suggests that execution can be halted to the system and that the instruction should be treated more like a NOP. However, this seems rather useless to me. Consider the following ASM program snippet:

wfi_loop:
WFI
J wfi_loop

This would have to be done since WFI can not be depended on. However, upon MRET from the interrupt handler, you would still be caught in the loop. So you would have to make it conditional against a global variable whose value is updated in the interrupt handler. This seems very messy.

Also, if your implementation does in fact honor the WFI instruction and the interrupt is triggered just prior to the execution of the WFI instruction, the entire core will stall until some other interrupt is triggered since it will return prior to the WFI instruction.

It seems that the only correct usage of the instruction would be inside of a kernel scheduler when there is no work to be done. But even then, I don't think you would ever want to return from the interrupt handler into such code, but rather restart the scheduler algorithm from the start. But that would be a problem too since you would somehow have to roll back the stack, etc....

I keep going round and round with this in my head and I can't seem to figure out a safe use. Maybe, if you atomically, enable interrupts in with CSRRS and then immediately call WFI like this:

CSRRSI zero, mie, 0x80
wfi_loop:
WFI
J wfi_loop
NOP
NOP

Then make sure to increment the mepc register by 8 bytes before calling MRET from the interrupt handler. The interrupt would also have to be disabled again in the mie register inside of the interrupt handler before returning. This solution would only be safe if WFI, J, and NOP are all encoded as 4 byte instructions, regardless of whether compressed instructions are used. It also depends on the program counter reaching the WFI instruction before it is possible for the interrupt to be triggered, after being enabled by the CSRRSI instruction. This would then allow the interrupt to be triggered in a safe place in code and to return in such a way that it breaks out of the loop that was waiting for it.

I guess I am just trying to understand what behavior I can expect from the hardware and, therefore, how to correctly call and return from interrupts and use the WFI instruction?


Solution

  • There should be one task/thread/process that is for idling, and it ought to look like your first bit of code.

    Since the idle thread is setup to have the lowest priority, if the idle thread is running, that means that either there are no other threads to run or that all other threads are blocked.

    When an interrupt happens that unblocks some other thread, the interrupt service routine should resume that blocked thread instead of the interrupted idle thread.

    Note that a thread that blocks on IO is itself also interrupted — it is interrupted via its own use of ecall.  That exception is a request for IO and causes this thread to block — it cannot be resumed until the IO request is satisfied.

    Thus, a thread that is blocked on IO is suspended just the same as if it was interrupted — and a clock interrupt or IO interrupt is capable of resuming a different process than the one immediately interrupted, which will happen in the case that the idle process was running and some event that a process was waiting for happens.


    What I do is use the scratch csr to point to the context block for the currently running process/thread.  On interrupt, I save the fewest amount of registers necessary to (start to) service the interrupt.  If the interrupt results in some other process/thread becoming runable, then when resuming from interupt, I check process priorities, and may choose a context switch instead of resuming whatever was interrupted.  If I resume what was interrupted, its a quick restore.  And to switch contexts, I finish saving the interrupted thread's CPU context, then resume another process/thread, switching the scratch register.

    (For nested interrupts, I don't allow context switches on resume, but on interrupts after saving current context, I do set up the scratch csr to an interrupt stack of context blocks before re-enabling higher priority interrupts.  Also, as a very minor optimization we can assume that a custom written idle thread doesn't need anything but its pc saved/restored.)