 Greetings, RISC-5 friends. Let's talk about interrupts and exceptions. I'm building a RISC-5 processor, not on an FPGA. So last week I talked about implementing the CSR instructions and about implementing exceptions and interrupts and how I wasn't really done. Well, I think I'm done. I'm not too happy with my implementation and I might change it in the future. But for now, here is basically the way it works. So let's talk about exceptions first. So I think I mentioned last week that exceptions can happen when you've got, for example, an illegal instruction or when you have a load that isn't on the correct byte boundary or a store that isn't on the correct byte boundary. So the idea is that you have an instruction. So here's an instruction and let's suppose it's a three cycle instruction. So this would be like a store or a load. So this is a three machine cycle instruction. So when we determine that we are about to store or load to a misaligned address, that happens somewhere in the second machine cycle, I believe, or possibly the third machine cycle. The point is that it happens somewhere within the instruction. So when I find a misaligned access, I have this exception line that goes high. So this is the exception line. So at that point, what happens is I also set the cause, which is going to be something like load misaligned or something. And we go to a trap handler. So right over here on pH one, this is phase one. So this is what phase one looks like. And there would be would be the end of the instruction. So this goes to a trap handler. And the purpose of the trap handler is to look at the exception signal and to look at the cause and determine what to do. So the trap handler utilizes a bunch of CSRs. So one CSR is going to be M cause. That's the machine cause register. It's a 32 bit register and it stores what happened. And we went over that last time. There is a table. There is MT-VEC, which is the vector table. And I did make a mistake last time about this and I'll explain that in a moment. And then there is MT-VAL, which is any value that goes along with an exception in order to explain what happened. And then there's M-E-P-C, which is the program counter of the instruction that caused the problem. So with these, there's also M status, which I don't think comes into play because I'm just using it for global interrupt enabling. So let's ignore that for now. So what happens is the moment that we see this exception, we can load the cause and we can load the MT-VAL and we can load the M-E-P-C. So we know all that. The trap handler then gets called and then the trap handler looks at the cause and it says, oh yeah, this is an exception. And then it needs to go into MT-VEC. Now MT-VEC is a 32 bit register. The final two bits are the mode and the upper bits are the base address. The base address is, it's really a 32 bit address, but you ignore the bottom two bits, which would be in the mode. That makes it nicely aligned on 32 bit boundaries. The mode is 0, 4 direct, 1 for vectored, and 2 and 3 are reserved. So the idea is that for exceptions, for all exceptions, regardless of whether the mode is direct or vectored, what you do is you simply jump to the base address. That's it. That's how you handle an exception, unless of course you're handling a fatal exception in which case you just want to halt the processor. So in this particular case for all of these things like illegal instruction or misaligned addresses, like I said before, I'm just going to treat those as fatal exceptions and I'm just going to halt right then and there. There are exceptions which will jump and these are E call and E break, which I was puzzling over last time and have now figured out kind of how to handle them. Okay, so that is what happens when an instruction goes bad. So let's see what happens when an interrupt happens. So we'll get rid of the instruction. We'll get rid of the cause, get rid of the trap handler. And let's suppose we have an interrupt line. Now there are two interrupt lines that I'm having. There's a time interrupt request and an external interrupt request. And time is used to schedule regular things that happen, but I don't think it's really meant to be a strict time requirement. So, you know, if you want that timer to go off every one millisecond, this is not the way to do it. And external interrupt is just, you know, from like an external peripheral. And according to the spec, the external interrupt has higher priority than the time interrupt. So that sort of tells you that the time interrupt isn't like a real time interrupt. So the external interrupt, what happens is I only sample that interrupt line on the very last phase one on the very last phase of the instruction. And on the very last phase of the instruction, we have the signal that goes high. This is the instruction complete signal. And only if the instruction complete signal is high do I sample the interrupt request lines. So, you know, that basically puts a requirement to hold the interrupt line high until it can get handled. Again, not really happy about that. I may change this in the future to something like, you know, maybe it's maybe it's edge triggered or level triggered and, you know, it's stored somewhere. I don't know. So there are two additional CSRs. There's MIE and MIP. So this is the interrupt pending in CSR. And this is the interrupt enabled CSR. And then I mentioned M status. This is actually global interrupt enabled. So there's a whole bunch of different types of interrupts. So there is machine interrupts, supervisor interrupt. So there's machine interrupts, supervisor interrupts and user interrupts. And for each interrupt, there are three different kinds of interrupts. There's an external interrupt. There's a time interrupt and there's a software interrupt. So because I don't have supervisor or user modes, I just have machine mode that eliminates those. I'm also not having any software interrupts. I just have external and time interrupts. So those are basically two interrupts. What global interrupt enabled does is you can globally enable or disable the interrupts. Interrupt enabled tells which of these is actually enabled or not. And interrupt pending tells you whether there's an interrupt pending. So theoretically, what I really should do is the moment the external interrupt or time interrupt request lines go high and interrupts are enabled, I should load the interrupt pending bits in there. Instead, what I do is I just sample it on instruction complete. And if that happens, then I set the interrupt pending. Now, when I have a pending interrupt, then I go to a trap handler. So basically what this means is I'm always completing an instruction before I go to the interrupt to the trap handler. And the reason for this is that instructions take more than one cycle sometimes and they do things to the register. So I can't really break an instruction in the middle of it because now some registers have been changed and then go to an interrupt handler or an exception handler and then come back and expect to be able to restart the instruction. I just can't do that. So that's why I wait for the instruction to complete before going to the trap handler. Again, I don't have to do that on fatal exceptions because on fatal exceptions, I'm going to halt the processor anyway. So I don't have to go back to the instruction. So what the trap handler then does is it looks at the exception line. And of course, the exception line is going to be low because it's not an exception. We're going to look at the interrupt pending lines and set the appropriate cause. And one of the causes is machine time interrupt or machine external interrupt, something like that. MT val doesn't make any difference here. MEPC is going to be the PC of the instruction that we need to execute next. So it's not the instruction that we just completed, but it's the instruction that we would have followed next if we hadn't been interrupted. So when the interrupt handler is finished, it will jump back to the MEPC and then of course the program can continue where it left off. In the meantime, of course, the trap handler looks at the pending bits and it resets them because it's handled them. Now, again, let's suppose we get both a time interrupt and an external interrupt. Well, external interrupts have higher priorities. So what the trap handler is going to do is it's going to look at the interrupt pending bits and it's going to see that the external interrupt is high. And it's going to handle the external interrupt. And when it comes back, the next instruction executes and we've got another pending interrupt. So we'll look at it and see, oh, it's a time interrupt. Now, of course, in the meantime, just before you just before you trap the time interrupt, you could get another external interrupt, which has higher priorities. So you have to handle that. So basically the time interrupt becomes pending until there's no other interrupt and then it can be handled. So the other interesting thing that I learned is that global interrupts are disabled when you handle an interrupt. This kind of makes sense because we don't we don't handle nested interrupts. So while so when we get an external interrupt, we jump to the interrupt routine and also disable global interrupts so that no matter how many times you request an interrupt, you're not getting it and it doesn't go pending until we jump back out of the interrupt handler, which is done using the M ret instruction machine return from interrupt, I guess. And that enables global interrupts if they had been enabled before because you can always set that global interrupt enable bit in M status in a regular program and then you can disable interrupts from there. And that's it. So that is really how interrupts and fatal exceptions are handled. Now, let's take a look at one thing which was really driving me bonkers, which was what happens when you hit an E call or an e break instruction. So e call and e break mean environment call and environment break. And really what that does is it creates an exception and the exception that it creates for an e call is if you're in machine mode, machine mode environment call requested. Or if you call e break, well, it's a breakpoint exception. So in that particular case, let's suppose we're executing e call and that is a one cycle instruction. So here's e call. So what we're going to do is the moment we see e call, we're going to raise the exception line, right? Because it is an exception, but it's not a fatal exception. So the M cause is going to be set to, you know, e call requested. So the trap handler will then execute here. The trap handler will execute there. And of course, you know, it's going to look at the the the let's see what's what it's going. What is it going to look at? It's going to look at the M cause. It's going to see that it's an e call and then it's going to jump to the e call routine. Okay. So here's where mtvec comes into play. And yeah, let me go back to let me go back to interrupts. Okay, so for mtvec, again, there are two modes direct and vector. Now in direct mode, you're always going to jump to the base address that's in the mtvec register. So regardless of whether it's an interrupt or an exception, it's always going to go to the same address. And in that handler, you know, you are the one who is responsible for looking at the M cause and trying to figure out what happened and doing different things based on what happened. Now in vector mode, if there is an exception, it's as if you were in direct mode, you always jump to the base address. However, if it's an interrupt, then what you do is you jump to the base address plus the M cause times four. So the M cause, you know, could be zero or one or two or three or four and so on. And you know, let's suppose one of these is the machine time exception. And actually, I think it's maybe three and 11. No, that's not right. Well, whatever. One of them is the machine time interrupt. And another one is the machine external interrupt. So let's suppose it's just four and 11. I don't know what they are. I forget. So in vector mode, what's going to happen is you're going to read the base address and then you're going to add four times four or 16. Or if it's an external interrupt, you're going to add four times 11 or 44. So of course, the four times is because you want the address you're going to jump to to be aligned on a 32 bit boundary. So originally, I thought that the base address pointed to some table in memory. And then that was a table of addresses. And let's suppose this is three and this is 11. And what you would do is you would jump to there. But that's wrong. So in fact, what it is is you have to have a take you have to have, it's not really a table. It's not a vector table. It is just a bunch of instructions. So this is the zeroth instruction. And that's the one that will handle indirect mode, everything. And in indirect mode, it will handle all exceptions and interrupt number zero. And, you know, let's suppose number three, well, in that can only be called in vector mode by, well, okay, it wasn't three. It's four. That can only be called in vector mode by a time interrupt. So, you know, you're going to jump to here. So what do you put in those instructions? Well, probably a jump instruction, which would be a JALR with the destination register set to zero, right? JALR. So your quote vector table is really just a bunch of JALRs. Okay. So now what happens is when you get an interrupt, you go to the base address, you add four times the number of the interrupt. You jump to that address, which is an immediate jump to the actual interrupt routine. So that's how MTVEC works. Okay. So let's talk about E call and E break now. So with E call, again, we raise the exception line because it's an exception. We set the M cause to an E call was called. And the trap handler looks at that and it says, oh, an E call was called. That is not a fatal exception. So I am just going to go to the base address. It doesn't matter whether you're in direct mode or vector mode because exceptions are always jumping to the base address. Same thing with an E break, except this time the causes E break. And that's really it. So the one thing that I'm really not happy about is what happens if you're executing an E call instruction and an interrupt happens. So normally there would be this signal called instruction complete. And I only check interrupts when the instruction complete line is high. That's again at the very end of an instruction. Well, I don't really consider E call a an instruction because it's sort of like an exception causing instruction. So it doesn't actually complete, you know, just like a failed instruction like a load with a misaligned memory access never completes because it breaks in the middle. The moment the exception happens. So in this case with an E call, it's an immediate exception. The instruction doesn't the instruction doesn't complete, which means that if you raise one of the interrupt request lines, that interrupt request is not actually going to be pending. So because again, I only penned an interrupt when an instruction is completed. So this is a problem because I think the spec requires that things like they call them synchronous exceptions. So I guess that means E call and E break. I'm not quite sure those have to have lower priority than interrupts. So I sat around and scratched my head and I was wondering what happens if I'm executing an E call and I get an interrupt. Apparently I'm supposed to handle the interrupt first. But if I handle the interrupt, then I'm then when I return from it, I'm just going to pick up where I left off, which is after the E call instruction, which didn't make any sense to me because now the E call basically didn't do anything. And you can't like queue up an interrupt and then an E call. So the only way that I could figure out how to handle it is that the E call, the moment it starts executing, it causes an exception and jumps to its handler. And what that does is it disables interrupts. So when you call E call and E break that pretty much immediately disables interrupts. That's the only way that I could figure out how to handle it. And I'm not too happy about it, but you know, I'll live with it at this point. I don't honestly think it's that important. So I'm just going to leave it at that. Okay, so the next thing that I want to talk about the last thing that I want to talk about actually. And again, you can just look at the code in the GitHub repository where the link is down below. So the next thing that I want to talk about is this table right over here. Okay, so what happened? What happened was I have a bunch of tests, formal tests, and I do bounded model checking. Now before I was doing also prove mode and I explained what the difference between bounded model checking and proof by induction was. So I decided not to do proof by induction. And the reason is that with proof by induction, again, the formal verification engine basically gets to choose random states. And the problem is that when you do that and you run into a problem where you have to add an assert based on the internal state of what you're testing. Now what you're doing is you're sort of going into your internal states and you're testing for them, which can be pretty fragile. Because if you decide to change the logic that may change some of the internal states and now your asserts are wrong and you have to start all over again. So now in my machine, I know that I'm executing instruction after instruction, modulo exceptions and interrupts. And really all I care about is the state of the machine before the instruction and the state of the machine after the instruction. So what do I mean by state of the machine? I mean the program counter, the CSRs, the memory, the registers. And that's pretty much it. Everything else I don't really consider a state because even though there's a machine cycle counter and even though there are some other single bit registers that are manipulated during an instruction. By the time I end an instruction, all those things should be reset. Now whenever I say should, what I actually mean is if you were to design something that's mission critical or a medical device or that you're going to sell for a lot of money, when I say should, it means you put in an assert. So when I say something like, oh, you know, registers, well, register one is just like any other register. So if I execute an op instruction that says register one, register, what is it? RS1, RS2 and destination register. And I say, well, it doesn't matter if register one is one, two, three or 31, they're all a register. And as long as it accesses that register, we're fine. So all I really have to do is check one register. Doesn't matter which one. Same thing with RS2, same thing with RD, with the exception of the special cases of register zero, which is zero. So, you know, in my bounded model checking, I can say, okay, well, when you're looking at op instructions, assume that RS1 is either one or zero. And assume that RS2 is either, I don't know, two or zero, and assume that RD is one, two or zero. Right, because we want to cover the cases where the destination and source are the same. And that should be sufficient. And there's that word again, should. So if I were actually going to, again, put this in, you know, something mission critical or medical or actually sell this, I would run this in full proof mode. I would plan bounded model checking. And I would, you know, live with the fragility of all those extra asserts that I have to deal with. The other thing about deciding to just check one register and the zero register is that if you let even bounded model checking check all of the registers, it actually increases the amount of time quite a bit. So that's what this table is all about. So here on the left are the number of seconds for each of these tests that bounded model checking took when I did not restrict the format of the instruction. So for example, for these op instructions, on the left side is the number of seconds that it took to do a bounded model check when I did not restrict what RS1, RS2, and RD were. And it took something like five minutes. Well, when I said, okay, RS1 is either one or zero RS2 is either two or zero and RD could be 123 or zero. It took 195 seconds. Okay, that may not seem like a huge savings, but take a look at all these other ones, like for example, LH load half word. Well, again, I restricted just the registers that it could access, and it went from almost an hour down to almost a minute. And that was true for all of these instructions. So you can see that these instructions took, you know, from from two minutes up to about two hours. And we'll talk about fatal later. Whereas when I restricted some of the instructions that we're checking again on the theory that it doesn't really matter which register you choose, one register is as good as another should. All of the times went like two to three minutes. So to run the full suite of tests took about 11 minutes, which is really good, as opposed to leaving it running overnight and possibly for all of the next day. And improve mode for several days. So again, I would do prove mode again for something mission critical or medical or something that you're selling. And you know, you don't expect it to be thrown away for like five bucks, you're actually selling it for for a significant amount of money. Once you're done with everything, I would go the entire way and do and do an inductive proof on your design. So yeah, that's basically this table. So now let's talk about fatal. So what fatal does is it allows the formal verification to choose any instruction that it wants. It doesn't verify whether the instruction worked. It just verifies if the instruction becomes a fatal exception. And if it becomes a fatal exception, it just makes sure that that all the values of the CSRs were set correctly. The trap was called, you know, it was fatal and all that stuff. So so not only that, it's not when it's fatal, but it also looks at the instruction that is being attempted to be executed determines whether it should be fatal, and then asserts that it actually did end up in a fatal exception trap. Yeah. So that's actually it. Again, I'm not going to go over the code. You can read it yourself. I just wanted to explain, you know, some of my thoughts about interrupts and exceptions, and, you know, a little bit of philosophy about, you know, how far do you need to go when you test. And that's really all I wanted to say. Now, the good news is all of the tests pass. I've got interrupts and exceptions working. I've got all the instructions that I want to have working, which means that I can start cleaning up the code a little bit, you know, just seeing where I've got extra state where I don't need it, and then actually start lowering it down to the digital electronics level. It's called tech mapping, and I'm going to be manually tech mapping my design. So hopefully we can start a little bit on that next week. It's the holidays, at least in English land. So, yeah, I hope to see you then. And thanks for sticking with me. I know that, you know, as you continue down the line of episodes, the number of viewers gets lower and lower and lower. So by this time, we're left with sort of like the hardcore viewers, maybe. So you're hardcore. So that's about it. Thanks for watching, and I'll see you next week.