 We've built our micro address counter and our register file. So now we can talk about the other bits of hardware that we have to implement. This is the variable hardware, the ALU, the address ALU, and the memory interface. Then after that, we're going to talk about the actual micro program. So let's talk about the variable ALU and address ALU hardware. These are going to be very easy because they're just combinatorial. There are no registers involved, no clocks involved, so it should be relatively straightforward. So for the ALU, I've defined an enumeration with basically three different operations, increment, or because that's what we're going to do, and sub, which is subtract. The ALU itself is again pretty straightforward. We have the signals coming in are just the command, two operands and the output result, and also a Z flag because telling when the result is zero is often quite useful. There is also a combinatorial block, and we basically have just a unique case on the command, and whatever that command is, we do. In the case where the command is none of the above, we simply set the result to zero, and then regardless of what command or not we chose, we take a look at the result and determine whether it was zero, and if it was zero, we set the Z flag to one. So that's the ALU types, and we have a test. Let's open the file, address ALU test. Now that's the address ALU, open the file, ALU test. So this is just more of the same. I specify some increments, tests, some OR tests, some subtract tests. I'm also testing overflow for increments so that we can test the zero flag. Same thing with OR, if the result is zero, I want to test that the zero flag got set properly. With subtract, I want to make sure that there aren't any problems when we go negative, so when we have a zero and we subtract one, that we roll over to all ones, and then the test code is basically just more of the same. The only difference, of course, is that because we don't have a clock, we simply run the evaluate method once, and check the results. All right, so that's the ALU. The address ALU is more of the same, except in this case, we only have a single operation. We can add more. This is just more of the same. We have an increment operation. One of the special things that we had to do is because we only have one operation, and none of the operations involved the second operand, varilator gives us an error message actually saying that Y is not used. So this is the secret varilator command, which tells it to ignore this unused variable. So you turn the linter off, and then immediately after, you turn the linter on again. So just some special syntax. And the address ALU test looks exactly the same. We have X, and we have Y, and the expected Z, and the expected Z flag. Here we're just testing increment overflow, and of course remember that this operates on addresses, which are 17 bits. So to test the Z flag, we simply take one FFFF, increment it, and the result should be zero, and the Z flag should be one. And that's it. Now for vars, there actually aren't any commands. The var hardware always computes its address, and the address is based on whatever variable we're looking for, except for zero, because with zero remember that variable zero means pushing and popping onto the stack, so we can't immediately compute an address for that. So we have the frame pointer, and we have the global pointer. So of course, if the variable v is greater than or equal to one, or less than or equal to f, then we simply subtract one, multiply by two, and add that to the frame pointer, and that's the address of local variable v. This is the way to indicate numbers. I suppose I could have just chosen one and 15. That would be a decimal number, which would be perfectly fine, but here I chose to use hexadecimal, so you have to specify the base, I'm sorry, the size, and then a tick, and then the base, which is h for hex, it would be b for binary, and then that's how you specify a hexadecimal number. This is some special syntax, which is the cast operator, because v is an unsigned byte, and v minus one of course is also an unsigned byte. There would be an, and two times v minus one would also be an unsigned byte. The problem is that now I'm adding it to a 17-bit number, so what you have to do is put in a cast somewhere to the 17-byte number. So now if v is greater than or equal to hex one zero, then you have to subtract hex one zero, cast it to an address, multiply by two, and add it to the global pointer. Otherwise I'm just setting the address to zero, that's basically to capture any other cases. So the var's test is pretty much exactly the same. I'm testing an invalid variable zero, and the result should be zero. I'm testing both local one and local two, because I want to make sure that multiplying by two actually works. So here, just for the sakes of argument, I'm saying that the frame pointer is at f zero one two, and then there's local variable one, and that should be located at f zero one two. And local variable two should be located at f zero one four. Same deal with the globals, I just set the globals to address one two. Global one zero should be at address one two, global one one should be at address one four, and that's pretty much that. So of course I can simply run, I can do make ALU, which will work, and then run that test, make address ALU, and then run that test, and that works, and then make var's, and that test also works. Okay, so that was pretty simple, just three combinatorial blocks. So the next thing that we're going to talk about is the memory interface, and that's going to be a lot more complicated. Okay, so if we take a look at the datasheet for the SRAM, and you can go to the reference card that I built, and over on the right hand side there is a list of SRAM pins along with a link to the datasheet, so that's how you can get to it. So there will be a certain amount of time for all of this micro program of logic to get from the positive edge of the clock to where the address that we want to read along with the command gets sent to the SRAM. So we don't know how much that is, I'm just going to say it's some sort of a delta t. We don't know how much it is, but hopefully it's quite fast because this is an FPGA, and internally FPGA has propagation delays internally, you know, for under a nanosecond. So it's probably quite small. So the next thing is if you look at the datasheet for the read cycle, you can see that between the time that the address changes and the time that the SRAM data goes away, that's the whole time. So that's the amount of time that the SRAM sort of keeps its output, and then notices that the address has changed. This is two nanoseconds minimum. Now we don't use that, we don't really care, it's just there. After that, the data is basically invalid until this point over here, when the data suddenly becomes valid, this is the actual data corresponding to the input address. And that time on the datasheet is called TAA, and that's 10 nanoseconds max. That's pretty much why this is called 10 nanosecond memory, because it takes 10 nanoseconds to go between the address changing and the data being valid or being stable. Now of course the data can be stable before that, and that's where overclocking comes in, but we're not doing any overclocking here. So you can see that, first of all, our clock period definitely has to be longer than 10 nanoseconds, because if it's shorter than 10 nanoseconds, then we're not giving the SRAM hardware enough time to change its data before we clock the data in. So that's a limitation on our clock. We've immediately limited our clock to 100 megahertz or less, and of course it's going to be even less than that because of the internal propagation delays of the FPGA. So what does this mean? Well the point that I wanted to make is that the data is valid on the next edge of the clock. Now where this edge, where the negative edge of the clock happens, doesn't actually matter. I mean if this were a 10 megahertz clock, then this would be 100 nanoseconds wide, so this thing would be somewhere in there, really compressed. So we could do our read cycles over and over and over according to this, because if say the next command from the micro address program were to simply change the SRAM address so that we read maybe from the next address, so we're doing two reads in a row, well that would just mean that the address then changes as a result of this clock edge like that, and then the data changes as a result of the address changing, and this is typically how you mark that one cause has this effect. So that's invalid data, and then there's data number two, and that's address number two. And you can do this over and over and over again, and so we can see that we can have single cycle reads. That's how reads work. Rights, unfortunately, are a little more complicated. So here we have our write cycle. So where are we going to put our clock edges? Well, what we need to do is determine first of all where these signals change, okay? So there are actually three places we need to set write enable low, and then we need to set write enable high again. We also need to present the data on the SRAM lines, and then we won't need to take that data away so that we don't blow up the chip and transmit while the chip is transmitting. So again, from write enable low to data output goes to the chip going into listen mode essentially is a maximum of five nanoseconds. Also we need to hold our data on the data lines for at least six nanoseconds before we clock the write enable high, and that will clock the data in, writes the data, and then there's some propagation delay between write enable going high and the chip flipping again into transmit mode and sending us data in read mode. That's neither here nor there. So where are we going to put the clock edges? Well, certainly we want to put one of the clock edges right over here, and in fact it's going to happen slightly before then simply because we know that we have propagation delays. So there's our initial positive clock edge. Now we know that on the positive clock edge the micro program does whatever it needs to it sends commands out to all of the hardware which includes the SRAM interfacing hardware and one of the jobs of the SRAM interfacing hardware is going to be to notice that the micro program says, I want you to write this register at this address into the SRAM and that's where this write enable line is going to go low. So it's going to be after some very, very short, hopefully, propagation delay. In addition, this address is probably going to change because maybe initially we were reading and now we're actually writing. So let's put a little transition in there as well to represent that transition. Okay, so this is our write address now. Okay, so this goes, huh, our clock goes high, some extremely short period after that our address changes and the write enable goes low, five nanoseconds maximum after that the SRAM chip flips from transmit mode to receive mode. We're still in receive mode. So now the question is, when do we actually present our data? Well, let's flip the question around and say, when do we stop presenting our data? Well, we're going to stop presenting our data maybe on the next positive edge of the clock. If we do that, then this positive edge, there is that time, will cause the write enable to go high after a certain period of time and it will cause the data to stop being presented and for us to flip from transmit mode to receive mode so that we can receive, you know, whatever the SRAM is doing. Like for example, maybe we want to do an immediate read after that. So there's a little transition here and this is our next address, you know, maybe it's a read, maybe it's another write, I don't know. Great. So the question is, when do we present our data? Well, it seems that the only real choice that we have is to present it after the negative edge of the clock. So in fact, this negative edge causes the data to be output and the positive edge, when the positive edge occurs, that will cause our data to not be presented anymore. That will cause the write enable line to go high and everything should work. So one interesting thing is that if indeed the clock being low results in the data being written to the chip, that means that this cycle or this half cycle has to be greater than six nanoseconds. Now because we have a 50 percent duty cycle clock, which we don't necessarily have to have, but we're keeping things very simple. We're having a single clock, no phase shifts, no strange duty cycles. It means that the other half of this also has to be greater than six nanoseconds, which means of course that the time of the entire cycle has to be greater than 12 nanoseconds, which means that we're still way less than 100 megahertz. Okay. In addition, notice that it is a very good thing that the other half of this cycle is greater than six nanoseconds, because if we were to present data to the chip during this time over here, when the SRAM still is in transmit mode, then we would be transmitting and the SRAM chip would be transmitting and that's no fun for anyone. So that's also a good thing. So this seems to work. And if we're doing a write and then it's followed by a read, well that seems to work too, because if we have this goes low and this goes high and that's when we clock the data in, well that seems to work, because this is the data out, data out, this is our read address, write enable was high as it should be, and everything seems to work. So now the question is, what happens if we have two write cycles? If we have two write cycles, then at this positive edge, we're basically saying, no, don't put write enable high, put it low. Well, if we put it low, then there's no write cycle. So that doesn't work. So how do we fix this? One possibility that you might think of, and that I might think of, is that we could take this negative edge on the write enable and move it to after the negative edge of this clock pulse. So the negative edge of this clock pulse results in write enable going low. But that's a problem, because the negative edge of the clock pulse is also supposed to present the data to the SRAM. But if we're moving this entire section all the way over here, then all of a sudden we're transmitting and the SRAM is transmitting and that's no good. So now what do we do? Now another possibility that I alluded to before is maybe we could have two clocks that are out of phase slightly so that one clock controls the data while the other clock, which is slightly out of phase, controls when the write enable goes down. That means that we could actually have a pulse over here like that, where this is one phase and this is another phase. Now obviously they're not going to be 180 degrees out of phase. There may be going to be 90 degrees out of phase or 45 degrees out of phase. Some small difference between them. But we said that we're only going to have one clock, so that doesn't work. And now there's basically only one conclusion that we have left. So remember that we analyzed what would happen if a read cycle happened after a write cycle and that worked. Well, it seems as though after every write cycle we must have a read cycle in between. We don't have to put that data anywhere. We don't have to change the write address, but we do have to change the write enable line and we do have to not present the data in. So that has to stay. But nevertheless after a write cycle we need a read cycle before we can write again. And that's something that we need to put in our micro program. There are two cycles actually to a read to a write. So the first thing is to do a write and the next thing is to basically do a null read. So we're going to have to implement that in our micro program. Here's a summary of the timing and there is actually a problem. Maybe you've noticed it. I just noticed it. So here's the clock on the top and this edge causes that edge to drop. This edge causes the data in to be presented to the SRAM and this edge causes both the write enable to go high thus clocking the data in to the SRAM and it causes the data to be removed from the line so that we can listen to the SRAM. And there is some propagation delay between write enable going high and the data finally being presented out from the SRAM. So the problem is that this edge causes both of these things to happen simultaneously. But things never happen simultaneously. The problem is that these are not actually registered at the SRAM. So they may be registered at the FPGA, but then when they get output to the SRAM, well they're not going to be simultaneously presented to the SRAM. Even worse, this data in is eight lines. It's eight parallel lines. So one, two, three, four, five, six, seven, eight, and then you've got the write enable line. That's nine lines that have to change their state all at the same time. Well, eight lines anyway. So you have, so if these are the data lines, they all have to be stable when the write enable line goes high. But the write enable line will go high at the SRAM some time after we tell the FPGA to pull the write enable line high. There's going to be some delay. You know, maybe it's a fraction of a nanosecond. And the data lines are also simultaneously going to be released by the FPGA. So maybe they'll get released before the write enable line gets a chance to make it to the SRAM. So this is just not going to work. We might actually take the data in and maybe move it out by a half o'clock cycle. But now the problem is that we're transmitting and the SRAM is transmitting and that's no good. So this just isn't going to work. So another day, another cat shirt, another diagram and a different microphone. So we'll see if the sound is a little better. Okay, so we determined that our previous attempt at doing writes wouldn't actually work. So there is another method. This is under write cycle number two in the data sheet where you use the output enable in order to control the output high impedance mode. So we can basically flip the SRAM chip into receive mode by driving output enable high, which is pretty good because it means that we can control the SRAM's output so that we're not stepping on it when we want to send the data to the chip. So the idea is that according to the data sheet, we have this delay between output enable going high and the data going high impedance. And that is T-H-Z-O-E, that's high impedance based on O-E. And according to the data sheet, that's a maximum of four nanoseconds. And similarly, when we have output enable go low, according to the data sheet, there is a minimum of zero nanoseconds when the data output goes low impedance. So let's see what else. Okay, so write enable in this mode, the data sheet tells us that write enable has to be low for at least eight nanoseconds. And in addition, we see that as usual, the data has to be stable for at least six nanoseconds before it gets clocked in by write enable. So by driving output enable high and write enable low at the same time, we can do what we want and then we can wait and then we can present the data to the input and of course it will have to be after four nanoseconds after output enable goes high, which is not a problem. What is odd though, is that if you have to have the data remain stable for six nanoseconds, that means that six nanoseconds plus four nanoseconds is 10 nanoseconds. But the write enable pulse has to be low for a minimum of eight nanoseconds. So I don't know why they wouldn't have just said 10 nanoseconds, maybe there's another mode. Anyway, so and here's the address line, I just put it over there just to show that we do have an address and we're outputting it. So the idea is that we set write enable high that clocks the data into the SRAM and we're keeping the data stable. Because remember, we said that if we release the data at the same time as we released the write enable line, there is no guarantee that one will arrive at the same time or before the other. So in this particular case, we are guaranteeing that write enable goes high and the data in is being held for however long we need to and then we can release it and then we can release the output enable also, we drive it low and that enables the output again. So would this actually work first of all with two write cycles? Well, yes, because if we wanted, say this transition to indicate that we want to do a second write cycle, well, we simply don't enable the output. This never happens. And we have another pulse over here, which is great. It works. So the question now becomes, okay, where are our transitions? So we have one transition here. This is when we drive output enable and write enable. The second transition is here where we apply the data to the SRAM. The third transition is here where we set write enable high to clock the data in. The fourth transition is here where we where we release the data. And then the fifth transition is here where we finally release the output enable line. So let's draw our clock. So we have one clock here, one edge. Then we have another edge here. We're going to have another edge here. This is the other edge and this is the final edge. Okay. So basically, we have one positive edge, two positive edge, three positive edge. That's the that's basically the next cycle. So we have one cycle here. And we have a second cycle here. Two cycles. We'll just call it write zero and write one. So again, writes this time legitimately, legitimately take two cycles. We don't have to do a separate or a null read. Write just happens to take more transitions. There are more signals to move around. And then after this, we could do a read, we could do another write, we could do whatever we want. So this is what we're going to implement. So we can see that we've got several things going on. So first of all, we have these two signals change over here on the positive edge. Likewise, this signal changes on the positive edge. This signal changes on the positive edge. This is great so far. We've got everything changing on the positive edge. The only things we have not changing on positive edges is the data. So that changes on the negative edge over here and the negative edge over here. And that's going to turn out to be pretty good for us. So let's go see how we're going to implement this in hardware. And here's the code. I've decided to try something different and maybe not show my face during this presentation, this part of the video. I don't know if I'll like it or not, but we'll see. All right, so this is a bit of a complicated module, mainly because of all the interactions with the various flip flops. It's probably easier to just look at the schematic, but very briefly, we can go over the inputs and outputs. So this section is really the interface from our micro program to the memory controller module. So obviously we have the clock, we have our usual reset, we have the command, the address that we want to read or write, and then we've split the write data and the read data. So obviously for the read command, the read data will be output to the rest of the processor. And for the two write phases, we have the write data being input. So that's fairly straightforward. And then we have the SRAM connections. The digital board has 19 bits of address, so we do that. The SRAM data is, of course, eight bits. However, it's not bit. It's logic because we do need high impedance states. These are Z states. And the thing about the wire I'll get to in a moment. This is both an input and an output, so it can be driven or read from. And then we have the three control outputs, not chip enable, not output enable, and not write enable. And I'm initially setting these two to one so that when the FPGA starts up, it starts up without actually transmitting to the chip, which also may be transmitting. And that would be bad. So we want to disable write and also disable output. So nobody's transmitting. We've got some internal registers here. This is an assignment statement. This is called a continuous assignment statement. It's different from an always combinatorial block in some vague sense that I'm not quite sure about. But the general rule seems to be that if you have a wire, then you must have an assign in order to decide what that wire will be driven by, I guess. Anyway, so we have a combinatorial block over here, which does the usual state machine thing where we have some internal next lines. And then we have a positive edge, always block and a negative edge, always block. Remember that the only thing for the negative edge was that we wanted to output data. I have an internal register called data direction, which controls which direction the SRAM data is going to be. And here in this assignment, we can see that if the data direction is one, this is writing to the SRAM, then we want to output our write data. Otherwise, we want to go high impedance, which basically means that we are allowed to read from it. Of course, we're always allowed to read from it. It's just that we're reading from it without actually putting anything onto the data lines. Okay, so here's the schematic that corresponds to the code. So we can see that let's start over here. We've got two control lines, output enable and write enable, and those are both controlled by registers. We also have the chip enable, which is set to be zero by default, so the chip is always enabled. We've got some address lines which flow straight from inside our processor. There are only 17 bits. The rest are filled with zeros, and that gets you the 19 bits output. Same deal kind of with the data, except we have this tri-state register over here, and the tri-state is controlled by a register. That's our data direction. So we can set tri-state to one, which basically means that this signal is not connected to the output, which means that we're just going to read the SRAM data. If tri-state is off, it means that this input is connected to the output, which means that we are driving the SRAM data lines with write data, coincidentally also outputting that to read data, but that's okay, because we're not doing a read, we're doing a write, so that will just be ignored. We have an internal register here just to clock in the command that we are sending, and if you sort of go through the diagram that we showed before, you can see that we really do need to register that. Let's see. Okay, now here's the problem. The problem is that Verilator doesn't seem to handle tri-states very well. It does some sort of concession to the fact that a tri-state may be on or off, but it sort of doesn't let you drive the tri-state output from the other end. So in other words, we can't really simulate SRAM data when we're reading from it. Maybe you can. I don't know. I stared at the generated code and I just could not figure out what needed to be done. So we get to look at simulation using system Verilog. So simulation or in Verilog language verification, that's I guess what the very stands for. Anyway, so I've written this simple test bench, and a test bench is kind of like a top-level module. So of course, you can do import, you can you declare your module, you can declare your internal variables. Here's this assign statement again, because SRAM data is a wire you need to assign. Ah, that's what I wanted to do. I wanted to explain wire. All right, so I found this page here from Mentor Graphics, which deals with system Verilog verification. Anyway, this is kind of a picture of what we have. So maybe on the right side we can think of this is our SRAM and on the left side, this is our FPGA, where it's our memory controller, and we have the module has an in-out port. Basically, the rule is that if you have an in-out port, you must have a wire rather than something else. And the reason is that a wire is a thing that can be driven from both ends. Or in another way of saying it, a wire is a thing that can be driven by more than one driver. There is some sort of internal logic which decides who wins. You get all sorts of interesting ways that you can drive wires, like strong pull-ups, weak pull-ups. You can declare a wire as a wire or I think even a wire and maybe. So in our case, we just have a simple wire that's either tri-stated or not. In other words, it's either driven by one end or driven by the other end. And it's never going to be driven by two drivers at a time. So we don't have to worry about that resolution. But anyway, the point is that an in-out must be a wire and a wire can only be driven by an assigned statement. A procedural statement is something else. A procedural statement is what happens in those always blocks. And it says you cannot mix procedural and continuous assignments to the same variable. So the assign was a continuous assignment. And then there's all sorts of other explanations and reasons and so on. So this web page makes some interesting reading. There is some that's not really germane to anything that we do. Well, here's an example of a wire over here with a bunch of drivers. So the output of each of these concurrent processes drives a net in what is called a continuous assignment because the process continuously updates the value it wants to drive on the net. And you might say to yourself, well, isn't that what an always block does? I have no answer to that. So anyway, so here's our test bench. Remember, again, SRAM data is a wire. So we must drive it with an assign. And basically what I'm doing is I'm simulating the SRAM part of it. So if the SRAM is not output enabled, then drive that line with high impedance, otherwise drive it with some test data, just A1 in this case. All right, here's how you declare using a module or making an instance of a module in a higher level module. You just say what the module type is and you give it a name. And then you say what its ports are connected to. And this here is special syntax, which says, and we'll get to the syntax once we look at the higher level pluge module, which connects all these modules together. But this syntax basically says, okay, whatever is named in the port for mem, use the same exact name in the higher level module. So basically this is shorthand for connect mems clock to our clock, connect mems reset to our reset, and so on all down the line. This is another bit of test bench trick. It basically says, okay, well, we want clock to flip itself every 100 time units. So time scale tells you what size the unit is. This is the unit size and this is the precision of the unit. So basically, you know, if I had instead of one nanosecond one picosecond that I could say something like pound 100.001. So this basically tells me the precision of my unit. And this tells me the time scale of the unit. So that is 100 nanoseconds. And I can't say 100.6, it will actually round to 101. So basically I'm just driving the clock with a period of 200 nanoseconds. That's all that means. Okay. Now, I'm just going to do this the simplest way. I'm sure that there are many other ways to work with verification and system verilog. There are things like programs and tasks. I'm just going to do it the really simple way. I'm just going to declare a single initial block. And then nothing else. That's it. And at the very end, I'm using a system verilog command called finish, which means quit. So I'm initially setting the clock to zero and the reset to one. And I'm waiting a full clock period so that the reset can reset the things, all the things. And now I'm setting reset to zero. So the next thing that I'm doing, and you have to sort of keep in mind where the clock is at this point, the clock is going to be low, right? Because the clock was low over here. 200 nanoseconds is one period later. So the clock is still low. So 90 90 nanoseconds after that, which is 10 nanoseconds to the rising edge, I'm going to change the command to write zero. I'm going to change the address to 98. And I'm going to change the right data to BC. So here what I'm doing is I'm setting up my right. Then I wait 10 nanoseconds. That gets me to the positive edge. And then I wait another 100 nanoseconds. That gets me to the negative edge. Then I wait another 90 nanoseconds. And now I'm again, 10 nanoseconds before the positive edge. And I set up a right one cycle. So right zero cycle, right one cycle, wait another 10 to the edge, wait another 100 to the negative edge, wait another 90. Again, I'm about to hit the positive edge. So I set up the next command that I want to do. Now you might say, well, you know, why don't you just set up your command immediately? The answer will be apparent shortly when I show you the waveforms. So then I set up my read. I just wait and then wait another clock cycle. And that's it. You can actually do assertions. And you can do print statements. So if you wanted to, and I probably should at some point, put in some assertions over here along with some print statements, that will print out fail if things aren't output as expected. And then I suppose I could actually assign a variable up here to count up the number of successes. And then, you know, just before finish print out a line that says, you know, how many tests passed, just like I would do in C++ and varilator. But I'm not going to do that at this point. I'm just kind of annoyed at this. And I just want to do the least work possible. Okay, so we go over to run simulation. And there are a couple of weird things that you have to do. So there are different simulations that you can run. There's a behavioral simulation. There are functional and timing simulations. Functional, I'm not sure of the difference between functional and timing. Obviously, timing has to do with timing. But again, just like in Vivato, there are various stages. There's elaboration, there's synthesis, and there's implementation, which is actual routing on the chip. You can simulate from the perspective of, okay, I'm just going to test my varilog module, or I'm going to test the synthesis of the varilog module, or I'm going to test the actual implementation on the chip, which isn't, of course, the actual implementation, but it does take into account some of the internal timings, which might be interesting to take a look at. So anyway, let's run the simplest simulation, which is the behavioral simulation. So I'm going to get it to start itself up, and it's going to compile. And the really annoying thing is that if there's anything wrong, it's going to display a message that says something's wrong. And then you'll see in the messages, or actually in the tickle console, it'll say, something's wrong, go look at this file to find out what it was, which is really annoying, because then you have to look at the log file to see what the error actually was. Anyway, I've already done that for you. I've gone through all the tens of errors that I've accumulated, and I've finally gotten a clean compile. So this is another stupid thing. When you look at the waveform, basically, you can see that it is precise down to the picosecond. That's the default. And I don't know that there's anything you can do to change that. In addition, it only shows the last five or six picoseconds, which is kind of useless. So what you have to do, there's this button over here. This is zoom in, zoom out, and fit. You want fit, which will fit the entire waveform, the entire test into the window. Now you'll notice that it stopped after 900 nanoseconds. That's because it hit that finish command. So it just basically stopped at that point, which is cool, because, and this is important, if you plan on running more than 1000 nanoseconds, you must go to simulation settings. And if you look at simulation, you can see runtime 1000 nanoseconds. So when you start up the simulation, it will only run for 1000 nanoseconds. You can change that by changing this value up here, resetting the simulation, and then running, I guess, all or whatever. We can restart it and then we can run all. And then, of course, it changes to here. You got to change it back to here. And you can see that it has regenerated all of the waveforms. So in terms of the waveform, it will display all the things in your top level module. It will not display anything internal to the module, but you can debug it by clicking on mem, which is our internal module, and then you get a whole bunch of other signals over here. So for example, if I wanted to see what the command register was, I can right click on it and say add to wave window, which will add it and then not give you a waveform because it never computed it for you. Then you have to restart and run all again. Great. Okay, now it tells you what the command register is. And this is basically why I wanted to put the command as close as possible to the positive edge because I wanted it to kind of sort of look like the internal state of our module. This is the internal state, write zero. This is the command that I wanted it to run. I could have put this transition all the way back here to just after the positive edge, but then it would kind of look like it was doing a write zero over here, which it obviously is not. It's doing it over here. So we can stare at this and see what's going on. So we can see that we are sending a read. That's good. We can look at the initial states of output enable and write enable. Those are both one. That's good. That's not what the actual chip might do. Because remember, this is only testing your Veralog. So we'll take a look at what the actual chip tries to do or what the software says the chip does in a moment. So we started off with a read and basically all zeros everywhere. I guess these are the initial registers. This is the high impedance state for SRAM data. So now you can see that the real test starts over here where we do our write zero. So you can see maybe hard to see. Let's see. Where should we go? Here's our SRAM address. This is from our test fixture. So we're saying that we want to write address 98. And let's see. This is our write data from our test fixture. So we want to write BC. Oh, this is the address right over here. So you can see that our address makes it out to the SRAM data. But our write data does not. In fact, there's a high impedance over here. Well, that makes sense because that's the way we wanted our timing to work out. And you can see also that write enable went low. Now it remains low. We now put the data on the SRAM data lines and then write goes high right over there. And then it remains like that. Let's see. Output enable is still high. So that's good. So that means that output is disabled. And you can see that it's disabled because of these high impedance things. Remember, this is from our test fixture where we're sort of simulating the SRAM chip. So we remove the data from the data lines at this point. This is during the write one cycle. That's good because now we're at high impedance. So we've got this nice buffer over here. And then we do a read and our read. We're simulating the SRAM outputting A1. And yes, A1 makes it to the read data. So you could verify all this using assert statements. And that's what you would want to do. Again, I'm kind of annoyed that I have to do this in the first place because it would be nice to just be able to type a command and run it within like a fraction of a second. But this doesn't do that. Yes, there is a command line interface, but no, it doesn't run immediately. And the idea behind tests is that they should be very, very fast so that there's no cost involved in running it, which allows you to simply say, run everything, no matter what tiny change I make, run all the tests. Okay, so let's see what happens if we do a run simulation and we do it post synthesis and we'll do a timing simulation. Let's see, I don't think anything different is going to happen. Obviously, it's going to synthesize and that may, you know, throw some errors if there's a problem with synthesis, but I really don't see any, you know, anything that should be different about this. So we are executing the steps and we're looking at the tickle console and any errors will show up. Great, that's it. So now we go to the waveform and we click fit. And does it look the same? No, it doesn't look the same. There's actually some differences. There's this thing. Let's see if we can zoom in on that. Oops, that didn't really work. Maybe we have to put, I don't know, I'm clicking and nothing's really happening. Let's go to fit again. Maybe I have to take this and drag it. That seems kind of weird. Okay, now I'm zooming in around here. Now, apparently, what synthesis is saying is that the SRAM data is, oops, wow. Okay, that was interesting. The SRAM data is unknown at this point. So it's saying, I don't know what the SRAM data is. And look, see, write enable is low and output enable is low for some reason. What is this? Something like 5.26 nanoseconds here. And then output enable goes high, 6.44 nanoseconds. So, you know, you can see that for some reason, Vivato has decided that there's a delay in the initial state of these signals, which is kind of unfortunate because that means that there could be some time where we are driving data and the SRAM is driving data and that's kind of unfortunate. But I don't know what to do about that. I suppose I could have one special line that's sort of like disable all outputs from the FPGA and, you know, wait a couple of clock cycles before I enable that. And that, you know, sort of, you know, pulls the tab out of the battery compartment and lets things run. I don't know. Okay, so that was, that's interesting. So let's see what's here. I don't know. I have no idea. Here we go. So if I move this around, this one says 80, 80. Maybe it's taking into account different. Oh, yeah. Okay, this is like a transition between zero and A1. So not all the bits are going to change at the same time. And I guess it's modeling this sort of default transition time and maybe randomizing it. So, but that's okay. I mean, if we zoom out, the whole thing looks correct anyway. And remember, things basically only change on the positive and negative edges of these signals. So as long as these, as long as the SRAM outputs are kind of stable during these transitions, especially this transition where we're doing a write, that's all we really care about. And we designed our module to make the data be stable on this transition. Great. Let's look at the other simulation, which is the implementation timing simulation. So now it's actually going to place and route all of the blocks on the chip. Okay. And I guess that's what it means. It didn't seem to take very long and implementation usually takes a long amount of time. Let's take a look at our waveforms. Well, again, it does look slightly different, especially in the beginning. So let's scroll all the way to the beginning and put in some pluses. Well, no, actually, it doesn't look significantly different. There is this unknown phase. This has stretched a bit maybe. Did it? This is about 10 nanoseconds now for the output enable to be driven high. Again, I don't know why that would be, but that is the case. So anyway, that is the memory module. It would be interesting to write a little bit of Arduino code and actually exercise the memory to just see if this actually works in practice. What we would do in that case is we don't want to have to assign, whereas this address, this has 17 bits to it, we don't want to have to assign all 17 address lines to IOs and then wire them up to the Arduino and control them. We don't really have to. We can temporarily change this to something like bit one to zero so that we're only dealing with four addresses. That's all we really want and really care about. I mean, if four addresses work, then all of them are going to work, unless, of course, you mismapped the SRAM pins, in which case it won't, but that's okay. We'll assume that that works. We can maybe write some other program which cycles through all the addresses internal in the FPGA and then maybe lights up a light or something if it writes data to the SRAM and then reads back the same data and everything's hunky-dory. Anyway, all we would need is a clock, a reset, two lines for the command, two lines for the address, eight lines for the write data, eight lines for the read data. So that's what, 16, 22 lines total. This is all internal to the Digilent board, so you just need 22 lines and maybe we'll do that on the next video. So I think until then, I will close out this video and I will talk to you next time. Bye.