 Greetings, RISC-5 friends. So today we're going to talk a little bit about interrupts and CSRs. These are control and status registers and why exactly we need CSRs to handle interrupts. And it's going to be a shorter video than usual because I'm actually not yet done. It turns out that in formally verifying some of the exceptions that the machine should be able to handle, I encountered and uncovered more than a few bugs. So that's my fault, of course, but, you know, at least formal verification caught them. So that's what I've been working on. So unfortunately, it did require a little bit of extensive work. So I'm not actually at the point where I can present anything. But I just thought I would go over the background of what CSRs are and how RISC-5 handles things like exceptions and external interrupts or even internal interrupts. So we start with CSR and a CSR is a control and status register. And just like the words say, you can control certain aspects of the processor by flipping bits in a control and status register. And you can get the status of the machine simply by reading one of these registers and looking at the status bits. So here are a few examples. So we have, let me choose another color here. So we have these four registers, which basically tell you what make and model you're running. So these would be pure status registers. It doesn't really make sense to write any of these registers. They don't control anything. They just tell you about the processor. So what vendor you're running, what architecture you're running, I guess what implementation you're running, what hardware thread you happen to be running on. And these include things like what extensions the processor supports. And the privilege here is called MRO. So in RISC-5, we have different privilege levels. We have the machine level, which is basically the lowest level that has access to anything and everything on the machine. That's why it's called the machine level. And then you have the supervisor level and the user level. And the supervisor level is sort of, I guess it's basically the operating system and the user level is a process running in the operating system. So because I'm not going to have any privilege levels except the machine level, we don't have to deal with supervisor or user levels. So the privilege is also MRO, which means machine read only. So you can't write the vendor ID. It doesn't make any sense. OK, now we can also see here that each CSR register is associated with a number. And the thing is that CSRs exist in kind of a separate address space. So you know that we have memory, we have registers, and now we have CSRs. So memory can be all of 32 bits, registers, there are 32 of them. And in terms of CSRs, there are 4,096 of them, which is basically 12 address bits. So they exist in a completely separate address space. So if you read CSR number F11, then you get back the vendor ID. Again, this isn't a memory location, it's a CSR. So of course, we're going to need separate CSR instructions to access that separate address space. And risk five gives you six CSR instructions. So the first one is CSR-RW, I mean CSR read and write. And the idea here is that the data that you want to write into the register is in RS1. And the data that you read from the register will be put into RD. So it looks something like this. And in fact, this happens atomically. So you don't write RS1 into the CSR and then read it out to get the destination register because, well, you've written to it before you read it. So really what you want to do is you want to sort of read and write it at the same time, or at least read it first and then write it afterwards. The other interesting thing about CSR-RW is that if the destination register is zero, then we don't actually do a read. And the reason for this is that control and status registers can actually affect the environment merely by reading the register. It may not even matter what the data is, but just the act of reading it may trigger some events. So that's why in the spec, if you write into register zero, then you don't actually want to read from the register at all. So all you're doing is writing to the register. Now this isn't the case with writing from register zero because that just means that you're going to write a zero into the control and status register. So that's allowed. Okay, there's also CSR-RWI, which is the immediate form of this instruction. So it's basically the same thing, except that for the immediate value. So the interesting thing about the immediate value here is that you only get five bits in the instruction. Those five bits are actually zero extended unlike all of the other immediate values in all of the other instructions, which are sign extended all the way out to 32 bits. So this is great if you want to manipulate the first five bits of control and status register. And what the risk five organization or foundation tried to do is put the most important bits in those lower five bits, if possible. So that's what CSR-RWI does. Now there's also CSR-RS. And what this means is read and set bits. So again, it kind of looks like this, except in this case, oddly enough, you're always reading from the CSR, even if you're writing to destination register zero. So the difference is that RS1 contains a mask of bits that you want to set in the CSR register. So the zero bits don't actually get cleared, they just don't get set. So this is an easy way of setting certain bits. And the other interesting thing is that if the source register one is actually the zero register, then you're not setting any bits. And in that case, we don't write the CSR at all. Number one, because if you're not setting any bits, you're not making a change to the CSR. But also, again, there could be side effects of writing a CSR, and we eliminate those side effects by simply not writing to the CSR. Then there is CSR-RSI, which is, again, the immediate form. So again, it's exactly the same thing. You're always going to be writing. You're always going to be reading the CSR and writing it to the destination register, even if the destination register is zero. But the immediate form has five bits. Those are the lower five bits, so it's zero extended, which means that you can use CSR-RSI to set any one or any combination of the lowest five bits in the CSR. And again, if the immediate value is zero, then we don't write the CSR at all. Now the corresponding instructions for clearing bits are CSR-RC and CSR-RCI. So again, it's exactly the same, except that the one bits in the register or the immediate value are the ones that you want to clear in the CSR. And similarly, if you are clearing using register zero or a zero immediate value, then you don't write to the CSR at all. So these are the general ways of reading and writing CSRs. Now there are some pseudo-assembler instructions like CSR-R. So this is just read from a CSR register. Now since we want to read the CSR register, we're going to want to use one of the instructions that actually does read the CSR register. So the actual way that it's mapped is by using the CSR-RS instruction, specifically with source register one being register zero, so that you're not actually setting any bits and you're not writing the CSR. All you're doing is reading the CSR into the destination register. So that's one of the pseudo-instructions. And of course, there are a few other pseudo-instructions like CSR-W, which is just write to a CSR. And CSR-W write immediate, which is just write an immediate to a CSR. And you can imagine that those are alias to CSR-RW, CSR-RWI, using the source register one and the immediate, where the destination register is always zero. That just makes sense. Okay. So what does this have to do with handling exceptions? So the idea behind handling an interrupt is that generally you want to finish up the instruction that you're working on, and then jump to an interrupt routine, handle the interrupt, and then return from that interrupt routine. And that of course means storing your return address so that you know where in the program you left off. So normally this would be done using the stack, but the stack is actually defined in the ISA. And it may not necessarily be the ISA that you're using. So we have this CSR called MEPC, the Machine Exception Program Counter. And we can see from here, from this paragraph, that when a trap is taken, and a trap is either an exception or an interrupt, then the MEPC is written with the PC of the instruction that was interrupted or had an exception. So if we're executing an instruction and we get an interrupt, then I think what we should do is finish up the instruction, load the program counter of that instruction into MEPC, and then go to the interrupt routine, handle the interrupt, and then when we return, I guess what we're going to do is read the PC out of MEPC and then add four, because that was I guess the next instruction that we should go to. Now there can also be exceptions. So things like, for example, let's suppose you're doing a load of 32-bit words, so that's an LW instruction, except that your address isn't 32-bit aligned. That's actually going to cause a load misaligned exception. So again, that's a trap, that's a trap. So again, that's a trap, and that is handled by an exception routine, essentially. Now you can also define exceptions as fatal, which basically means that the execution environment is just going to halt. And that's actually what I'm going to do, because again, I don't really have an operating system. I only have machine-level privileges, and that's the only privilege level I have. And on an exception, I'm just going to halt the processor, raise a line that says halted, and that's it. I'm done. Obviously, I could do something else, but I'm not going to at this point. However, that does raise the question, well, if an exception or an interrupt, in other words, a general trap happens, then how does the system actually know what happened? So there's this machine-cause register, which tells us exactly what happened. So the machine-cause register is a 32-bit register, and it has encoded all of this stuff. There's one bit which tells you whether it was an interrupt or an exception, and then you've got the code, which tells you what kind of interrupt or exception it is. So right over here, you can see that when interrupt is set to zero and the cause code is set to four, that means that it was a load address misaligned exception. And you could choose to handle that if you have an exception handler for it, but if you don't, then you're allowed to declare it as a fatal trap. Same thing for store. We can see that there's a store address misaligned. There's an illegal instruction, and this is another thing that I was working on. Originally, and this was actually in the comments, originally I thought that if you had an op code that wasn't specified, like let's suppose it was an instruction in an extension that I don't have, or maybe it was one of the op codes, and the function to perform was in one of the holes, which wasn't specified. Well, what you should do is throw an illegal exception, an illegal instruction exception, because it really is an illegal instruction. You can't handle it. You can't just skip past it like I was thinking of doing. So this is one of the things that I was working very hard on is to find all those illegal instructions and throw an illegal instruction exception when that happened, and then formally verifying that that actually happened. So that took a long time. So anyway, there's the illegal instruction. There's also instruction address misaligned. So this is what happens when you try to jump to an address that is misaligned. Now again, because I'm not implementing the compressed instruction set, all instructions have to be aligned on 32-bit boundaries. Otherwise they could actually be on 16-bit boundaries, and then you would try to execute that. But if you try to jump or branch to a misaligned address, you throw an instruction address misaligned exception. And again, I'm handling all exceptions as fatal traps, which means that upon encountering this, it'll set the cause and then immediately halt the processor. So in terms of diagnostics, you probably could just look at the cause, look at the address. MEPC will tell you what instruction caused the exception, and M-cause will tell you what the problem was. So there you go. So now we get to the interrupt section. So this is what would happen if, let's say, there's an external timer interrupt, which goes off, say, every second or every 100 milliseconds or every 10 milliseconds. Or maybe there is an external device that sends an interrupt to the processor. So what we have right here are machine timer interrupt and machine external interrupt. So external interrupt is from an external signal. Machine timer interrupt is a regular periodic interrupt that goes off. We also have machine software interrupt, which is interesting because I think, but I'm not sure that this is what E call and E break are actually for. So that would call some sort of an interrupt routine. I'm not entirely certain, and hopefully somebody in the comments will tell me. But in any case, I'm really interested in machine external interrupt, and this is what I have not yet gotten to implementing. So again, it's going to involve, so you're executing an instruction at some program counter. You get an interrupt. We need to finish up the instruction, and then we need to load the program counter into MEPC. We need to load the cause into M-cause. There is a bunch of other registers that are involved in going to the interrupt routine. So one of those registers is MT-VEC, which is a vector table of traps. MT-VEC stands for machine trap vectors. And there are different modes that it can be in. One is all interrupts and exceptions are handled by one exception handler or one interrupt handler. The handler is going to be required to read the cause in order to figure out what it needs to do. The other mode is the vector to address mode. So based on which one of these happened, oops, based on which one of these happened, you can see that there's an exception code. Well that maps directly into a table. So index zero would be the address to jump to when there's a user software interrupt. The 11 in the table would be where to jump to when you get a machine external interrupt. So that's basically how interrupts are handled in risk five. So the CSR is our MT-VEC. There was MEPC that we mentioned. There was M-cause. There's also M status, which among other things allows you to enable and disable interrupts. And there's also one register called MT-VAL, which is the trap value. And that contains some useful information. Like for example, if you were to get a load address misaligned, then MT-VAL actually contains the address that you tried to read. While MEPC contains the program counter for the instruction that caused that exception. So there are all these useful bits of information in all of these registers. And that is also what I'm currently working on implementing, but haven't yet fully implemented it. So I seem to have implemented the whole exception handling properly, because again, that's pretty easy. You just load up these registers with what they need, and then you halt the processor. That seems to work. I haven't implemented interrupt yet. So that's probably what I'll be going over next week. Now in other news, I have been working on a series of graded exercises in and myGen, which takes you from the very simple, just strict combinatorial logic up to synchronous logic and some tips and tricks and things. So if you're interested, check the link down below. It's a work in progress, and I've only got the first five sections. The other interesting thing is that one of the people who was interested in my work in and myGen created a Docker image. So you can actually start up a Docker image that contains YoSys, Python 3, and myGen, and a whole bunch of other FPGA tools. So that is really useful. All that is mentioned in the and myGen exercises in the first section, which is installation or introduction. So I guess that's about it again, short video, sorry, but it's a lot of work, and hope to see you next week. Bye.