I'm curious about what code is written in the back end (and beyond looking at how headers files go down a wormhole of bit's being defined in yet another file) for interrupts. So I know that when an interrupt occurs the processor saves the current state and runs off to a specified memory location. To me the user this translates into the automatic calling of a function that the chip manufacturer has predefined. But what code has the manufacturer written in order to have that function be placed in a certain physical location in memory?
I'll describe what RISC-V does, in a reasonably minimal but standard-compliant implementation that you might find in a microcontroller or a soft core in an FPGA. There are more sophisticated options, but what I describe here should work on any chip.
First you have to know what a CSR (Control and Status Register) is. It's a a group of 32 bits (on RV32) that directly control the operation of some part of the CPU core, or that show the status of some part of the CPU core. Each individual bit in a CSR might be read-only, and be connected directly to some part of the CPU's circuits. Or it might be read-only and always 0 or always 1. Or it might be a flip-flop that can be set by software, and then it directly feeds into controlling something in the logic. Or it might be a flip-flop that both software and hardware can change.
CSRs have numbers, a bit like memory addresses. On RISC-V the numbers go from 0 to 4095. The flip-flops for the CSRs might be scattered all around the chip, where they are convenient to monitor/control particular hardware. There is some kind of bus or buses to access them using special CSR read/write/update instructions. CSRs are used infrequently, so unlike RAM there is no guarantee that access to them is fast -- it could be over something like I2C or SPI. But on most practical cores it is not super slow.
Every RISC-V core should provide CSRs 0xC00 (cycle), 0xC01 (time), and 0xC02 (instret). These are all 64 bit counters and on 32 bit cores you can read the upper halves at 0xC80, 0xC81, 0xC82. CSRs from 0xC03 to 0xC1F (and the corresponding upper halves) are for performance monitoring counters.
The most important Machine mode CSRs are:
0x300 mstatus The keys to the machine!
0x301 misa The instruction set and extensions supported
0x304 mie Interrupt Enable
0x305 mtvec trap handler base address (vector)
0x340 mscratch store anything you like here
0x341 mepc the PC executing when an exception ocurred
0x342 mcause what happened e.g. interrupt, illegal instruction, memory protection
0x343 mtval the bad address or opcode
0x344 mip bits indicating pending interrupts, if any
The mstatus CSR has bitfields for (among others)...
MIE global interrupt enable. Both this and bits in the mie CSR need to be enabled
MPIE the MIE field before the current trap. (illegal instruction etc can happen with MIE=0)
MPP the privilege level before the current trap (User, Supervisor, Hypervisor, Machine)
Note: there is deliberately no field for the current privilege level. It is stored elsewhere. The machine knows but it won't tell you.
The mtvec CSR controls where execution will go to on an interrupt or exception. The simplest use is to simply store the address of your interrupt handler, which must be a multiple of 4 bytes. All interrupts and exceptions will jump to this address. You can also set the LSB to 1 in which case program exceptions jump to the address but interrupts jump to the address plus 4x the value in mcause.
On a very low end core mtvec might be read-only, in which case you need to put your handler where it says instead of telling it where you put your handler. You can tell by trying to write a setting into it and then reading it back and check whether it's what you tried to write. This is a general principle on many RISC-V CSRs, called WARL (Write Any, Read Legal) Worst case, just write a Jmp instruction to your handler at the fixed trap vector address it tells you.
When an interrupt is signalled the following happens:
- if mstatus.MIE is set and the mie bit for that interrupt is set and the interrupt level is >= the current interrupt level, the interrupt will be processed. Otherwise just set the pending bit for that interrupt.
- mcause and mtval are set. Interrupts set the hi bit in mcause, exceptions don't.
- mstatus.MIE is copied to mstatus.MPIE. mstatus.MIE is set to 0.
- the current privilege level is copied to mstatus.MPP and the current privilege level is set to M
- the PC is copied to the mepc CSR. mtvec (possibly plus 4x mcause) is copied to the PC
... and execution continues ...
That's it. That's all.
In particular, all user registers remain untouched. Nothing has happened to RAM -- nothing is pushed, nothing is written anywhere.
What happens next is up to you. Probably you want to free up some registers to work in. How many is up to you. Where you put them is up to you. If you never have nested interrupts then you could store some registers to absolute addresses -- but they better be in memory locations 0..2047 or in the top 2k of the address space because that's all you can access without getting a pointer into some register.
You can free up one register by writing it to the mscratch CSR. Then you can load a pointer into it and start storing other registers relative to that pointer.
You can keep a pointer to your register save area permanently in mscratch and just swap it with a register. You can use that register save area as a stack to support nested interrupts. Hey -- maybe the register you swap with mscratch is SP...
Or maybe you just trust that the running program always keeps SP valid, so you can just decrement it and save your registers there. The standard ABI says this should be ok. Compilers do make sure this is always true, and assembly language programmers should too. But careless or malicious code might not practice stack hygiene. Do you feel lucky?
Many embedded systems will just use a single stack and back themselves to get it right. But if you're paranoid you can keep another stack just for interrupt handling. And if you have U mode available you can prevent the normal code from messing with it.
How fast is this?
All the CSR shuffling happens in parallel. You should be up and running with a new PC value in 1 clock cycle. Then it's just a question of how long it takes to load the instruction from that address and start executing it. That should be the same as any unpredicted or mis-predicted branch/jump. Probably 2-3 clock cycles on a machine with SRAM and a short pipeline.
When you're done, restore any of the registers you touched and then the MRET instruction will take you back to the original program in 1 clock cycle (plus instruction fetch time)
This is real minimalist RISC stuff.
Current ARM Cortex-M CPUs do all kinds of fancy interrupt handling tricks. The CPU saves registers for you so you can jump right into C code. When you do a return from interrupt the CPU checks if other interrupts are pending and jumps right to them without pointlessly restoring and again saving registers. And a few other tricks. For example the CPU might start responding to a low priority interrupt and while the registers are being saved a higher priority interrupt comes in. The hardware can switch tracks and immediately go to the higher priority handler instead.
RISC-V hardware doesn't do any of that stuff. But because it does almost nothing at all, it is possible to write a standard interrupt entry handler that implements the same features in software -- and that runs just as quickly as the fancy ARM interrupt handling.
The interested can find details on various ways to do this here..
https://github.com/riscv/riscv-fast-interrupt/blob/master/clic.adoc#interrupt-handling-software