 Greetings, Risk Five friends. So today, we are going to be talking about formally verifying what I've written for the Risk Five CPU. I'm building a Risk Five processor, not on an FPGA. Now, the Risk Five CPU is not yet complete. One of the things that I left out was interrupts, and interrupts are going to be pretty important, something that I'm going to want, because otherwise, I'm just going to have to, like, you know, if I'm reading a keyboard, I just have to pull it. And then what is the main program going to be doing? Well, the main program has to allow the poll to happen. And that's just a big waste. So I definitely want to have interrupts. So the keyboard would send an interrupt to the processor, and then the processor would jump to some address, handle the interrupt, and then jump back to the main program. So that's something that I'll tackle on the next video, because it does involve adding more stuff, probably adding more cards. And if you know about CSRs, it's going to involve CSRs. But what I've done is I've formally verified whatever I've currently written for the Risk 5 CPU, which means the instructions, minus all the CSR instructions, and eBreak and eCall, which all pretty much require CSRs. Now, this is the first time that I'll be using ProveMode for YOSUS, or Symbiosis, in formal verification. Whereas before, all I've been doing is BMC, Bounded Model Checking. So let me see if I can try to explain what the difference is, and why ProveMode is so much more rigorous than Bounded Model Checking alone. So here's an example of a state diagram. So let's suppose it's a counter, it's a 3-bit counter, and I've written it so that it goes from 0 to 1 to 2, and then back to 0 again. And if I were to set the counter up to 3, it would go 3, 4, 5, 6, 7, and then, you know, maybe it would, I don't know, get stuck at 7, just as an example. Now, the red arrow is going to be a bug that I introduce. So obviously for a counter, you know, that wouldn't happen because it would go to some state, but let's suppose that it were a lot more complicated than a counter. You know, let's suppose that I output the value of a 3-bit register, and then it takes in a thousand other signals, and it determines what the next state is supposed to be, and the next state from 2 is supposed to be 0. But for whatever reason, I have a bug in my huge complex combinatorial logic that sometimes makes state 2 go to state 3. Okay, so let's suppose I were to set up Bounded Model Checking on this model, and I say, okay, well, you know, I know that my counter is going to go from 0 to 1 to 2, and then back to 0 again. So really, all I need to do is check for 4 states, you know, the initial state, and then 3 transitions after that. So the idea is that this would be state 1, this would be state 2, this would be the 3rd transition, and then for the 4th transition with Bounded Model Checking, we would go here and here, and because we didn't hit any assertions that this was an illegal state, Bounded Model Checking would say, hey, everything works. Now, let's suppose that I wanted to say, well, state 7 here is invalid. So I'm just going to circle this in red, basically saying that I'm asserting that we're never going to get into state 7. And again, you can see that Bounded Model Checking up to 4 states is going to work. In fact, I would have to do Bounded Model Checking up to 7 states or, you know, 8 states or whatever in order to hit that assertion. Now, you know, you would look at this and you would say, well, sure, I mean, that's a simple counter, but imagine that it weren't a counter, but instead, you know, each one of these states expanded into a huge state diagram. So, you know, maybe I would say, oh, well, I know that all of these states over here represented by this one state 7 are all invalid. And I did a simple check that said, oh, I can never do this at the same time that I'm doing that. And that's what, you know, this one state represents. So, you know, I obviously don't know enough about the rest of the states in order to properly say, well, you know, I need to assert that I will never get into state 3 and also never get into state 4 and also never get into state 5 and also never get into state 6 and also never get into state 7. So, this is a case where Bounded Model Checking can sort of fool you if you don't go far enough that your circuit is correct. And the problem is that maybe with Bounded Model Checking, if you do go that far ahead, it will just take forever to check, you know, just because the circuit is so complex. So, this is where Prove Mode comes in. And this is where Induction comes in. So, in Mathematics, let's suppose you have some function f that has one integer parameter k, say, or let's just call it n, where n is 0, 1, 2, or so on. And I'm sorry that I'm bringing math into here, but bear with me, I'm going to have a little pictorial analogy here. So, what you want to do is prove that some property holds for f of n for all n, no matter what that n is. So, obviously, you know, you don't want to say, you know, you don't want to start writing things down and say, oh, yeah, I've proven my assertion for f of 0. Now I'm going to prove my assertion for f of 1. Now I'm going to prove my assertion for f of 2. And, you know, keep doing that for all infinity numbers. So, sometimes it's just way too complicated than that. So, what Mathematical Induction is, is you just take one case, and that's called the base case. And, you know, typically if you're dealing with n going from 0 and n on, you would start with your base case being proving f of 0 is true. So, now what you say is, well, if f of k is true, then what you do is you prove that f of k plus 1 is true. So, if you can do that without actually having to say whether f of k is true or not, you just assume that it's true, and then you prove that if it's true, then f of k plus 1 is true. If you do that, then you can say, well, here's my base case, f of 0. And I also have this thing that says, if k is true, then k plus 1 is true. So, what I can do, let me see if I can sort of animate this. So, what I can do is I can take this and move it over here and say, oh, wow, because of this tool that I have, I've now proven that because 0 is true, and if 0 is true, then 1 is true. So, 1 must be true. So, now I can just take my tool, which I've proven, copy and paste. Yeah, that works. And take that and say, and apply it again. So, 0 is true, now I know that 1 is true, and of course, if 1 is true, then 2 is true. And then I just keep building on. And because of this, I can prove that f of n is true for any number. And that's what mathematical induction is. So, how does this relate to formal verification? Okay, so here I've written up on the left side what mathematical induction is. You have a base case, and then you prove that if 1 thing is true, then the next thing is true, and therefore your assertion works for any number. So, for formal induction, the way it works is like this. Let's suppose you do a bounded model check, and you say, okay, I'm going to do a bounded model check for four states, right? So, there's 1, 2, 3, and 4, and 4. And that's your base case. So, what you're saying is, I know that four steps starting from my initial condition will not hit any assertions. Great. So, now the induction step is, I need to prove that there are no sequences of k plus 1 steps that fail, regardless of where they start. So, that regardless of where they start thing is the equivalent of f of k. If f of k works, then f of k plus 1 works. So, what I want to do is, let me choose a different color. So, what I want to do is I want to say, okay, there are no sequences of k plus 1 steps that fail. So, what we'll do is we'll start from an assert that will fail. Here's 1. So, k plus 1 would be 5, right? So, that's my k plus 1 state. And that's where I'm going to start. So, now I just go 4. Now, because there's this transition here, this is also 4, right? Because I'm sort of working backwards, and I'm looking at all the different transitions where 7 can come from, and 7 can come from itself. However, in this case, what I'm going to say is, oh, well, I mean, if I hit an assertion over here, then I'm just going to take that away because that is actually not a valid series of steps that don't fail. So, 5, 4, right? 3, 2, 1. And that's the end. So, because I hit one case where a sequence of 5 steps can fail, I'll say that induction fails. So, let me show you an example of a sequence that doesn't fail. So, let's suppose I start from here, 5. Well, it can come from 4, and then that can come from either, no, it can't come from there. It can come from here, 2, 1, and that's it. So, that's an example of a path that doesn't fail. But the point is that I need to make sure that all such paths don't fail. And here, formal verification in the induction phase has shown that there is actually a sequence where there is a failure. So, how would you solve this? So, this is induction. And this basically, and the conclusion for the induction is that, well, if it fails for one of these cases, then you haven't proven that your circuit works, right? And the reason that I haven't proven that my circuit works is because I have this buggy transition over here. And really, the problematic state is this state right over here, 3. Because if that were also covered by an assertion, if I were to make this invalid, then A, bounded model checking would have found it, right? But B, because this path here during induction failed, then that is not a sequence that I can consider. I could only start from state 3. Okay. So, that's one way of actually sort of fixing the problem. If I say, if I look at the trace that induction gave me, and its trace is going to show me 3 to 4 to 5 to 6 to 7 where the assertion fails. So, I'm going to look at this, and I'm going to say, well, why did it get into that state? So, here is 3, and I'm going to say, well, that should never happen either. So, I'm just going to say, I'm just going to put in an assertion that 3 can never happen. And then I'm going to run induction again. Well, in that case, the base case is going to fail because we have, in the base case, let me just get rid of all these little annotations here. So, in the base case, we were looking at 1, 2, 3, 4, and 4, and we would hit this assertion. So, now formal verification is going to throw up this trace, 0, 1, 2, 3. And then I'm going to look and say, well, wait a minute, there's no way that 2 should have been able to get to 3. So, therefore, that's where the bug is. When I'm in state 2, some condition causes my circuit to go into state 3. So, that's why induction is more powerful than just bounded model checking alone. So, in EOSIS terms, or symbiosis terms, there are two modes. There's BMC mode and there's Prove mode. What Prove mode is, is the bounded model checking in order to establish the base case followed by the induction step. So, by running in Prove mode, I have a more rigorous check on my circuits. Okay. So, here we go, the formal verification of the processor. So, the first thing I need to do is basically create the buses and hook up all the cards together. So, that's what formal CPU is. So, here are the signals. These here are all the signals. We have the cards over here. We have the X, Y, and Z bus. And now we're going to hook up the buses. So, some of the cards get input from the buses. Some of the cards output to the buses. So, I just basically hook that all up. And this is what you do when there's sort of like a bi-directional bus where you can have multiple cards actually sending out data. Remember, I said that when a card is disabled from sending data, it actually sends out zero in the, in my gen. It would be tri-stated in the actual circuitry, of course. But because it outputs zero, we can or the outputs together. And that has basically the same effect. Okay. So, the next thing that I do is just hook up the control lines to each of the cards. That's fairly straightforward. There's the M-cycle end line as well. There's a memory bus now. The formal CPU has these as sort of like the external signals. So, these are the things that it will use to communicate with the outside world. And right now, the outside world just consists of the memory. So, that's what I'm hooking up right over here. And then, there is the illegal signal and the instruction complete signal that I also essentially route to the outside world so that I can see them. Okay. So, then there is a function that I'm using to decode the immediate value. And this is slightly different from the equivalent in the sequencer card. So, if we go to the sequencer card, and if I can find, decode immediate. So, here's decode immediate. And the way it works is that we take the immediate format of the instruction that we're decoding. And then depending on, you know, maybe it's an I format instruction, what we do is we create a temporary signal and then put the various instruction bits into that temporary signal. What I've done for the decode immediate in the formal CPU is I've taken a slightly different tack. And that is the immediate is always a 32-bit signal. So, we're not using a temporary. And then based on the opcode, we're not actually going to look at the format of the opcode or of the instruction. We're just going to look at the opcode itself. And then we are going to set the bits. And this is actually taken straight from the risk five spec. So, it tells us how to format an immediate value based on what bits we need to take out of the instruction. So, it's a slightly different way of writing the same code. And this is sort of a good check. You know, if I need the same functions in the verifier that I'm actually verifying, it's a good idea to switch things up and write the code in a different way so that at least, you know, you can be sure that things are consistent with themselves. So, that's what that is. And now, here comes the strategy for how we are going to formally verify the processor. So, the idea is that, let's suppose we wanted to formally verify one of the instructions. So, what we do is A, we assume that that instruction has completed, that that specific instruction has completed. So, I know we have an instruction complete signal. We also know what instruction we are currently executing. So, what we do is we just write an assume statement that says the instruction completes and this is the instruction that we're looking at. So, that way we don't have to run essentially all of the instructions in parallel when formally verifying things which can sort of slow things down. So, the idea is that, before the instruction executes, we take a snapshot of all the registers and the program counter. And then after the instruction executes, we also take a snapshot of the registers and the program counter. And during the instruction, we also keep track if there were any external memory reads or writes. So, that's what all of this is. This is collected. And inside collected, we have an array of the registers before the instruction and after the instruction. The program counter before and after, the instruction that was being executed, and then opcode RS1, RS2, and all the way down to ALU op. Those are just, you know, combinatorially taking the values from the instruction. Same thing with the immediate value. We also have some bits that allow us to keep track of whether a memory read happened or a memory write happened. Because aside from reading the instruction itself, you don't do any external memory read or memory write at the same time. It's either going to be a read or a write. So, that's why we only have one 32-bit register for the address that we accessed. And then what data we read, what data we wrote, and the right mask. Okay, so this is just, you know, the same thing, decoding the instruction into its various fields. This is decoding the immediate out of the instruction, and that's that. So, then I have a bunch of utility functions, which I'll get to later. What I want to do now is go to the main function, which is right here. So, this is my main function for formal verification. So, I set up an empty module and I add my CPU. And the CPU is the formal CPU, which is the, all the cards connected to each other. And then there's a clock that I make, which, you know, basically generates phase one and phase two. And I'm also going to pull out the M-cycle end out of the, out of the clock. Okay, so we create this data structure. And now what we're going to do is we're going to count machine cycles. So, we start at zero. And then, you know, as the instruction proceeds, we just count the number of machine cycles that happen. And just like in testing memory, we are going to set the registers to some random value and the program counter to some random value. So, that we're not always starting from all zeros. So, this way we can check that things are, you know, more generally working. So, the next thing that we do is we initialize the CPU's register and program counter with those random values. Now, they're not exactly random. It's not like you roll a die and that's what it is. Formal verification actually assumes any value. And if it finds a case where an assertion is violated, it will say, oh, it was violated. And by the way, these were the values that, you know, made it violate. Maybe they're a direct cause of the violation or maybe it doesn't matter. But the point is that, you know, at least it tells you. So, initially, what we do is we set the registers, the program counter here. And what we're also going to do is we're going to assume that the random value for the program counter is 32-bit aligned. So, that's what this assumption does. And here, what we're doing is we're making sure that our collected data will be the initial registers. And also that the X bank for the registers is also set to the initial register. Now, later on somewhere, I also have an assumption where the, I think I have an assumption, where the X bank and the Y bank are the same. So, of course, right here, all I'm doing is I'm making sure that the X bank is the same. But I say nothing about the Y bank. So, later on that will come into play. The next thing is let's go all the way down to the bottom where I return the module that I create and the signals that formal verification is allowed to monkey with. Now, if you think about it, really the only input into the CPU at this point is the external memory. It's the source of the instructions and it's also the source of any memory reads. And that's it. Everything else is an output signal. So, really the only thing that formal verification has available to it to exercise the CPU is the memory data that we're reading. So, that's why this has only one element, just the memory data that is supposed to be read by the CPU. Let's go back to the beginning again. And what we're going to say is that past the first phase, the memory has to be stable. So, remember that in the first phase we set up the address that we want to read and then we, and then during, so that's phase zero and then during phase one, we actually go ahead and latch the value that we read in. So, that's why after that, we just assume that the data is going to be stable. And let's see. The next thing is taking the actual snapshot. So, here it is. So, if we're at the very beginning of the instruction, so M cycle is zero and also phase count is one because remember with zero we're just going to read the instruction. So, with one and of course as long as there was no illegal instruction, which basically halts the processor, what we do is on phase two, which is the positive edge at the very end of phase one, we load the memory data that formal verification gave us into our collected instruction. So, this is the instruction that we're supposed to be executing. What we do is we take the program counter out of the CPU and store it away. We take the registers out of the register card and store those away. And then we initialize whether we read or wrote memory to zero because we haven't done anything yet. Now, all the time what we do is we're just going to copy the register data out of the X bank and put them in the after registers. So, we're not actually storing them, we're just sort of keeping track of them. And then when the instruction finally completes, those registers should tell us what the end result was. Same thing with PC after. So, we're not storing it, we're just sort of copying it over continuously. Okay, so here's really our first assertion of the day. So, this basically says that at no point ever should an instruction read memory and write memory at the same time, again with the exception of the instruction being read. So, that's did memory and did memory out of the collected data. All right, so we've got another bunch of assertions. So, the first thing is collecting any data that we read outside again of the instruction. So, if we are past the first cycle, right, and remember that all the load instructions and the store instructions are actually three cycles long. So, if we're past the first instruction and the CPU says we are now reading memory and we're on phase one and we haven't executed anything illegal yet, which means again that the processor would be halted, then first of all, we make sure that we haven't yet registered a memory read or a memory write. Because we also don't want an instruction to perform two memory reads or a memory read and a memory write or two memory writes. So, that's what that assertion does. And then we just store away what happened. So, we say yes, we did read whatever the the CPU was outputting on the memory address register. That's what we store in memory address access. And the data that is being read, well, we store it in our collected data. We can use this later on to make sure that the instruction did what it said it was supposed to do. So, for example, in a load instruction, this data that we read should be stored in the destination register, unless the destination register is zero, of course. Okay, the next thing is doing a memory write. So, it's basically the same thing, except we also store away the memory, the write mask. So, we know which bytes were actually written out of the 32-bit address, out of the 32-bit memory location. Okay, this is an assertion not on the collected data, but on the CPU itself. So, while the CPU is executing, we want to make sure that we never try to execute a memory read or a memory write at the same time. Okay, the next thing is, this is just a simple assertion, we know that during the first cycle you cannot do a write. You would only be reading the instruction and memory writes are just for the store instruction, and those are way more than one cycle long. So, that's that. This was just a cover to make sure that we actually did complete an instruction. This is just for debugging in the beginning, just to make sure that the whole mechanism actually works and that we got to a point where we can complete one instruction, any instruction. It doesn't matter. Okay, so these assertions, I set aside on their own, because these are assertions that I didn't have in the beginning, and I added them as a result of running prove mode. So, the idea is that I set up all the asserts that I think should be, and then I run prove mode, and it says, oh yeah, this one state that you thought you would never hit, okay, you probably will never hit it, but nevertheless, I need it marked as a bad state. So, because of those, I added these assertions and we'll go over them in a moment. But the meat of the formal verification that I'm doing is formally verifying each instruction separately. So, this is when I said we take an instruction and we formally verify it by assuming that an instruction completed, and that was the instruction that completed. So, here what I'm doing is I'm just writing down how many cycles I expect an instruction to take, and what the op codes are, and the widths of various loads and stores, and then, okay, so mode, what is mode? Well, this is, if we go to the bottom, this is basically just a flag that I'm passing to the main program. So, when I compile my code into IL, the Register Transfer Language Intermediate Language, I think that's what that stands for, I can say which instruction I'm actually expecting. So, for example, let's suppose I'm expecting op that I know that there is one cycle, that I have to run for one cycle, and that the op code is this value, which is a define. So, if the thing that I'm testing is actually an instruction, then what I'm going to assume is that on the very last phase of the very last cycle, instruction is complete. Otherwise, instruction complete is zero from that, from, for everything else. The other thing that I'm going to assert is that I never run into a case where the number of machine cycles is more than the number of cycles that the instruction is supposed to take. So, this is just a little quick check. For widths, these are for loads and stores, then what I want to make sure is that what I'm going to be doing is I'm going to assume that the width in the instruction is the same as the width that I'm testing. So, for example, if I want to test LB, then I want to make sure that font three in the instruction is set to that width. And for the op codes themselves, the first thing that I'm going to do is that I'm going to assume that at no point does the illegal signal ever go high. So, in other words, I'm only interested in formally verifying legal instructions. So, an illegal instruction would be something like the bottom 16 bits are all zero. That's defined as an illegal instruction. Or all 32 bits are one. That's an illegal instruction for an RV 32. Another example of an illegal instruction could be a misaligned memory load. Like, for example, let's suppose I'm loading a word and it's not 16-bit aligned. Well, that would be an illegal instruction in my implementation. So, what I'm basically saying is the only instructions that you can test are the legal instructions. The next thing that I do is I make sure that the op code in the instruction is the op code that I want to test. So, again, that's another assumption. And finally, this is the actual formal verification machinery. So, if in the past, by one phase, the instruction complete signal went off, then I'm going to verify the instruction. And that's going to depend on the instruction that I'm looking at. There's an extra mode here where I'm actually testing the illegal part, the illegal signal. So, what I'm doing is for illegal instructions, I'm assuming that an instruction never completes. So, I can only execute incomplete instructions. And if the illegal signal goes high, and it always goes high during phase two, then I run some checks on the illegal state that we ended up in. And finally, if we hit an illegal condition, then nothing changes. So, the idea is that if the illegal signal goes high, I can look at the data lines to try to figure out what exactly happened. So, for example, if I'm doing a... So, for example, memory address, I can look at that. I can look at the memory data that we're trying to write and the mask, and whether we're doing a write or a read, that sort of thing. And I want to make sure that when the illegal signal goes high, nothing changes. It's just, you know, constant. Okay. So, let's take a look at verify instruction. Actually, what I want to do is look at that section in the middle. So, this is what happened when I ran in prove mode. So, these are some of the things that prove mode made me assert, essentially. So, for example, one of the things is that if we're past phase zero, then the program counter doesn't change. Because at some point, it said, oh, hey, you know, your program counter changed. So, I want to make sure that it cannot start that induction step when in one of the states, the program counter changes in the middle of an instruction. Because it's allowed to do that. So, what we're doing is we're saying, no, during induction, we're not going to have any states where the program counter changes. And of course, if you do, it would fail. One of the other things is that if we're past the first memory cycle, the instruction never changes. Again, once I did induction, and it started changing the instruction on me. And I said, no, no, no, the instruction doesn't change. I wanted to make sure that the instruction didn't change past phase one of any machine cycle, and the ALU outputs don't change. Here's another assertion where, let's see, if we're on the first memory cycle, and we are on anything but phase zero, then the instruction is the same as what we're reading out of the memory. So, that's another thing. We also want to make sure that we're asserting that the memory cycle, there is never more, there are never more than three machine cycles. And this is one of the assertions where I can say that the Y bank must always be equal to the X bank. Let's see, here's another assertion that says that the machine cycle that I'm keeping track of is the same as the instruction phase that the CPU is keeping track of. Okay, so that's all that. So, let's take a look at some of the instructions that we're formally verifying. So, we can start with maybe op. So, in verify instruction right over here, so based on the op code, we're going to run verify op code op. So, let's go there. Okay. So, obviously, if the mode is not op, this is not an instruction that we're interested in, so we just don't assert anything. And what this does is for the argument, what we're going to do is we are going to get the contents of RS2 before the instruction executed. And then, based on the ALU op code, we're going to do some verification. So, let's look at verify add. So, first of all, we're going to make sure that we've bumped the program counter by four at the end of the instruction. We're going to make sure that all of the registers are the same with the exception of the destination register, which we don't check because we kind of assume that that's going to change. And then, we're going to take a result that we expect. And we're going to say that, remember, this is an add. So, as long as the destination register is not zero, the result is supposed to be whatever RS1 was before the instruction, plus the argument, which is whatever RS2 was before the instruction. The reason I use argument is because there's also the immediate version of this instruction. And I can just reuse the same code. So, that's the result. And then, we just assert that the contents of the destination register after the instruction has executed is the same as the result that we calculated it should be. And that's basically how all of the verifications work, at least for the single cycle instructions. So, we can take a look at, let's see. Okay, so those are all the op. And the op M is basically just the same, except the argument is just the immediate value that we collected. So, here's LUI. Again, it's just the same thing. AUIPC is the same thing. JAL is interesting because now, unlike all the other instructions, we can jump to a different instruction. So, what we do is we determine what we expect the new program counter to be. And this is basically the program counter before the instruction executed, plus the immediate value. And what we're going to do is assert that the program counter after the instruction is indeed what we expected. Again, we verify that the registers are the same, with the exception of the destination register, which could be zero. And what we're going to assert is that we didn't do a memory read or a memory write. I guess I don't really need that, do I? Okay, because that assertion is actually down below. Okay, so as long as the destination register is not zero, what we do is we take the program counter before the instruction, add four, and that's what we expect the contents of the destination register to be. Now, this is where I found a bug in JAL and JALR. I wrote this code and what actually happened was when the destination register was zero, we didn't jump to the correct address. And that's because I was using the destination register as a sort of temporary value to store. So, if you go back and rewatch the last video, you'll see my nice table of what an instruction actually does and what it puts on the buses. And one of the things that it did was I think it put the program counter before plus the immediate value and it stored that in the destination register. And then it took it out of the destination register, put that in the program counter, and then put the PC plus four back into the destination register. And I said that I had to do that because I didn't have enough buses available to do the calculations at the same time. So I broke it up into two cycles. Unfortunately, the temporary that I used could have been register zero, which you can't write to. So that was a problem and that showed up during formal verification. So I fixed that bug. Now, instead of using the destination register as a temporary, I'm just using the memory address that I'm outputting as the temporary. So because I'm going to be jumping to that address anyway and reading the instruction at that address, I said, well, okay, well, why don't we just output that address during, you know, during the first phase. We're allowed to do that. So that's what I did for JAL and JALR. Then there was branch. So for branch, what we're doing is we're just going to get RS1 and RS2. We're going to get the target program counter if the condition is true, the target program counter if the condition is false. And we're basically going to be doing the same thing. So depending on the comparison that we're doing, we're checking to make sure that the program counter is as expected. And that's that's kind of a lot of what I do here. I'm not going to go through everything. You can look at the code yourself. Let's see if there's anything else that is interesting. Well, here's verify illegal. Okay, so basically what we're doing is we're making sure that we can generate illegal instructions. And when we do generate illegal instructions, that it's one of these conditions. So the zero instruction is where the bottom 16 bits of the instruction are all zero. Again, that's defined by the specification as an illegal instruction. Then there is all ones. Now, if this were RV64, then it would be all 64 bits or ones. Since this is RV32, it would be all 32 bits or ones. If we try to do a misaligned load or a misaligned store or the program counter is misaligned, which can happen if you do a jump relative JAL or JALR. Let me, well, okay. One of those instructions. Then the program counter is misaligned and that should raise an illegal flag. So that's basically what I'm doing here is I'm just making sure that if the illegal signal goes high, then one of these conditions is actually true. And that's what this assertion is. And that's really that. So here I can show you, I think, an example of running prove mode on illegal instructions. So again, for prove mode, we can see that there is this base case right over here. And there is induction and they sort of run at the same time. So what it's trying to do is run bounded model checking for let me see how many steps. So for illegal prove mode runs for 18 steps. So this is the SBY file which sort of configures formal verification. And I found out that you can actually put some Python fragments in here. So now instead of having to write multiple SBY files, I can just write one and basically say if there's a particular target that I'm working on, I can set the depths of formal verification differently. So for example, all the one cycle instructions, well, you don't need to go anywhere. You don't need to go past seven cycles for BMC or six cycles for prove because that's one instruction. Same thing with the two cycle instructions and the three cycle instructions. Illegal, I count as a three cycle instruction because you can have a one cycle instruction being illegal or a two cycle instruction being illegal in the case of a misaligned program counter or a three cycle instruction being illegal in the case of loads and stores. So that's what all this stuff is. So yeah, the number I was interested in was 18. So if we look at the base case here, we can see that it starts from step zero right here and it continues all the way down to step 17. So that's 18 steps. And if we look at the induction part of it, so again, this is where we're starting from an illegal state and we want to make sure that we find a chain of legal states of 18 legal states and then make sure that nothing overlaps. So in other words, we never fall into that chain. So we start induction in step 18. So it's sort of working backwards from the invalid state and we go down, down, down until induction is successful. In that case, we found all of the chains of bad. So we found all of the bad chains, essentially, and then we've determined that bounded model checking and those things never encounter each other. So in other words, a successful induction happened. And that's basically that. So yeah, I guess I'm going to end it on this note. So one of the things that I wanted to talk about, let's just bring up the code again, in the sequencer card. So everything is great. The CPU works. It's just that it's not very useful because I don't have interrupts. So I looked a little closer at the risk five specification for interrupts and it starts talking about traps and CSRs, which are these these other registers, they're a completely separate address space. And basically, there's something in risk five called machine mode. And machine mode is the highest privilege mode. And in machine mode, you are required to have certain CSRs. And some of those are things like the status of interrupts. The type of exception that happened. And one of the types of exceptions that can happen is a misaligned memory address. So I'm actually going to have to implement CSRs. And if we look at the if we look at the sequencer, I put this line in here saying there's an external interrupt. And then and then I was like, how exactly does this work? Because I wasn't really sure how the processor was supposed to handle an external interrupt. And it turns out that the privileged section of the specification tells us exactly how interrupts are supposed to be handled. And it's through CSRs. Now, the other interesting thing is that when an interrupt happens, the interrupt is handled as pretty much all processors handle it. There's a vector table of jump locations. And depending on the type of interrupt that happened, and it could also be a fatal interrupt, well, not a fatal interrupt, but it could also be a trap, which is an exception, which is not an external interrupt. Or it could be a timer goes off, maybe in any case, you look up that table, and then you've got an address to jump to. Typically, what you do is you store away the program counter of the instruction that you're going to execute next, you store that somewhere. And that was one of the things that was like, Well, where do you store that? And it turns out you store it in the CSR. The next thing that you do is you store away all of your registers because the interrupt routine is going to start using registers. So now you have to store your registers somewhere. And usually the way that works is that you push the registers onto the stack. Now, risk five doesn't define a stack. Well, it does define a stack, but only if you're using a certain application binary interface. And then the stack pointer is specifically one of the registers. And I got to thinking, there was a comment on one of the videos. It was a very interesting comment. It was intriguing to me. It said that I've got way more memory in my register card than I have registers. And basically what I've been doing is I've been setting all those extra address bits to zero because I didn't need any of them. Well, to save all the registers, all I really need to do is change one of those address bits. And immediately I've got a fresh set of registers. And then when I want to restore the registers, I just set the address bits back to zero. So when I do an external interrupt or any sort of trap, all I have to do is save the program counter in the CSR and then switch the register page. And then I'm done. And I can do that for multiple tasks. So if I have multiple threads that I want to execute and I want to switch between them, you know, like every 10 milliseconds or whatever, all I have to do is save the program counter, swap the registers, and I'm done. So however much memory I have, however many copies of the registers I can have, I can set aside one for the main program, one for interrupts, and the rest are for, you know, other threads, other tasks. So that's kind of neat. And that's what I'm going to have to do on the next video. I'm going to have to start implementing the CSRs. Now CSRs are special beasts. They have special powers. They're not just stores of data. They actually affect the execution of the machine. So for example, one of the CSRs is a counter that counts up the number of instructions that you've executed. So obviously in order to implement that in hardware, well, I would have to have a counter. And then basically if that sits on a single card, it's going to have to monitor the instruction complete signal. And every time it sees that it has to increment the counter. Of course, it's a 64-bit counter. So now I need a 64-bit counter on a card and then some, you know, decoding circuitry that says, oh, this is, this is the specific CSR that you want to access. Well, now I'm going to dump my data. Maybe it's the high 32 bits. Maybe it's the low 32 bits on one of the buses, probably the Z bus, because then you can store that in a destination register. So I'm going to have to go through all of the CSRs that I need. Some of the CSRs I think are optional. And then I'm just going to have to go through that list and, you know, write the code for those CSRs. And a lot of those are going to be, are going to end up as single cards, which is kind of depressing because there are kind of a lot of CSRs, which is why I want to just, you know, get down to the bare minimum. I mean, do I really need an instruction counter? Do I really need a machine cycle counter? I don't really think so. So, you know, maybe I can, I can, you know, set aside space on the bus so that I could have a CSR card for that if I wanted to, but it's not absolutely needed. What is absolutely needed are the CSRs that handle interrupts and traps. So that's going to be the subject of the next video. So I hope to see you then. Hit the like button, or don't hit the like button, and hit the subscribe button if you want to see more videos. So thanks for watching. Bye.