 Okay, so in order to start, we're going to need to install some software first. Now the software that I'm going to install, I'm going to run under Linux. So I'd rather not struggle with the Windows command line and all the strange things that Windows does to path names and so on. If you're on a Mac, this will probably work just fine, but I just install things on Linux. Now I have a Windows machine, so I'm going to install VirtualBox, which allows me to run Linux in a virtual machine on my Windows laptop. And of course, if you have a Mac, you can also download VirtualBox and run Linux inside that. The other nice thing is that I'm going to use all free software, so you don't have to pay for any of this, which is quite nice. So the other thing that I should caution you is that I can't really give you any installation instructions. Each of these products has extensive user forums. They can probably help you if you have any problems installing the software. So the first thing that I do is I would install VirtualBox. Once I've installed VirtualBox and all of the addresses of the software is down below. So after installing VirtualBox, I install Ubuntu Desktop as a virtual machine. And once I've got that up and running, here is Ubuntu Desktop running in a virtual machine on Windows. So that's working. Once I do that, I install Sublime Text. This is essentially a text editor, but it's quite nice because it has some syntax highlighting. And once you install Sublime Text, you should install package control in Sublime Text, which in turn allows you to do things like this. You hit Control Shift P and then you can install package. And it's got a whole bunch of packages that you can install. And in particular, you just search for Verilog and you download the very first package that appears. And that gives you syntax highlighting for Verilog, which is quite nice. So the final piece of software to download is called Verilator. Verilator is, again, it's free software. It basically converts Verilog to C++. And then you can write a test bench in C++ to basically stimulate that hardware and look at the responses, which is quite nice because that allows you to validate at least your hardware. Again, it doesn't do timing analysis. So you install Verilator. There is a link down here about Verilator installation. Yes, I highly suggest that you do not use the Verilator distribution that comes with Ubuntu because it's about a year out of date. I highly recommend just going through the process. And this will get you used to building software. Besides, you're going to have to build software on the command line to run your hardware anyway. So those are the six pieces of software that you should get set up before we actually start. Now that we have the software installed, let's actually start writing some code. So I have created a directory called Plug1. That's just what I've decided to call the first version of the CPU. If you don't know what Plug is, you can Google it. System Verilog has some of the good coding practices that we know from ordinary C++ or Java. It does have the concept of namespaces. It does have the concept of packages and imports. But it's not as full blown as a regular programming language. So let's first recall that we wanted to define a certain number of commands to the micro address counter. So first, I'm going to create a file and I'm just going to start it off with a package name. I'm going to call it, I don't know, package micro address. And we put a semicolon in there. Now I'm going to save it as micro address dot v. And that immediately starts up a sublime syntax highlighting. So now we have Verilog syntax, which is nice. Inside a package can go type defs and functions. So we're going to define an enumeration and logic is the type that we're going to use. And you have to specify or, okay, so I'm not a great expert in Verilog. In fact, I'm a newbie. So I may be doing things wrong. I'm going to do them so that they compile and I'm going to try to use good coding practice, but I might not necessarily use good Verilog or system Verilog programming practice. If you see something wrong, do it the right way. Maybe don't do it the way I did, but I'm doing things from a C++ slash Java programming background. So anyway, the type is logic and logic is basically a bit that can be zero or one, or it can be high impedance or the unknown value, but we're not going to care about that. We just care about zero and one. Now we had, let me just put curly braces around this. Now we had a certain number of commands that we wanted to send to the micro address hardware. So we always start off with a none command. This basically tells the micro address do nothing, keep your state. We also had a reset command. We had what else did we have? We had an increment and we also had a jump or actually let's just call it load. So that's it. There are four enumerations and that's why I used two bits of logic. So this is basically an array and it says the most significant bit is one and the least significant bit is zero. So you can always do it the other way around, which is zero to one, which would mean this is sort of a little endian protocol. So the most significant bit goes in the lowest position, but we are going to use big endian notation. So and then we give it a name, let's just call it command. And I believe that is it with the exception of something that we have to add for varilator, varilator, space, public, sort of as a comment in there, and semicolon. And what this does is it exports the enumerated type and all of its constants into C++ so that we can use it in our test bench. So there we go. And then we end the package. Okay, so like I said, packages can only contain type defs and functions. Now, what about the actual hardware that we're going to design? So let me open up a new file, File New. Okay, and I'm going to save this already. And I'm just going to call it micro address. Let's call it the counter dot V. Okay, and we start off with a module. So in system verilog, you've got modules and these are things that can be reused or you can place several modules or you can have other modules use modules. The unfortunate thing is that modules don't have namespaces. There is only a global namespace for modules. So we're going to call our module micro address counter. And we are going to say what the inputs and outputs for the module are. So obviously we need a clock. So we're going to say input. We have to specify the direction, which is either input, output, or in-out, which is bidirectional. It's a single line and we're just going to call it clock. It would be nice to have a reset signal. So let's do that, input, logic, reset. We're also going to need to know the command. So input, now here is where we can use namespaces, micro address colon colon command. So this basically says out of the micro address package, pull out the type def called command. And we're going to call that command. Let's see what else we need. We need a load address, right? So now here is where we need to decide on the number or the size of our micro program, or the size of the space of our micro program. I'm just arbitrarily going to choose, I don't know, 10 down to 0. So that's 11 bits, which is 2k. I'm sort of guesstimating this because I know we're going to have on the order of 256 op codes. So if for whatever reason we need eight instructions or micro instructions per op code, that comes out to 2k. It's a guess, we can change it later. So we're going to call that load address. And then finally, we have an output, which is a logic also 10 down to 0. And we're just going to call it address. Okay, these are the inputs and outputs to the module. So I put a semicolon there. So the next thing that we want to do is we want to describe how the thing works. There is going to be basically the address here is essentially a register. And the register is going to get loaded when the clock edge goes positive. But what we want to first define is what the new address is going to be based on the inputs. And this is basically a combinatorial circuit. So there's no registers involved at this point. So I'm going to declare essentially a local variable. Logic 10 down to 0, new, or maybe next address. Okay, and now I define a block called always combo. Always comb. It's not combo, of course. And what this says is this is a block that will always be awake and listening to the signals and loading your local variables or possibly outputs with whatever it computes. So we're going to begin a block, and a begin always ends with an end. Great. And we're going to here describe how the thing works. I'm going to start with a unique case statement on command. And this basically says it's like a switch case statement in an ordinary programming language, except the unique keyword says that we are never going to have two cases that are simultaneously active. Now you might think, well, I mean, in a switch case statement, that would be a compiler error, right? Well, not so for system verilog. It is possible to define a case statement where two of the cases are actually activated. With unique, we're basically telling the verilog compiler, no, that's never going to happen. So it can do some optimizations. So we're going to have case. So micro address, colon, colon. Now what was the first one? Well, it was none. Let's define that as a default. Well, actually, no. Let's define it just for fun. So what is the next address? It should be whatever the address is. Okay. Case micro address. What's the next one? Did I say it was reset? Yeah. We don't actually need a reset command, do we? Because we have a reset line. So what I'm going to do is I'm going to take that away. Okay. We still need two bits. And this is the other thing that's kind of annoying. Verilog should know how many enumerations I have. I have three. So two bits should be enough. Unfortunately, that's not how verilog works. We really do have to specify the size of the enumeration. So we go back to here. And what was the next one? Increment. Okay. So the next address is equal to address plus one. And verilog knows how to do this. Knows how to do addition. Okay. Micro address. What was the next one? Load. So when we want to do a load, obviously the next address is going to be load address. We can stick a default in here now in ordinary programs with switch case statements. If you have all of the cases declared, you really don't have to put in a default. It is good practice in system verilog because you may forget to add new cases to your case statement or new enumerations or new possibilities to your case statement, in which case in order to avoid various bad things like inducing a latch, which basically means, well, you didn't assign anything. So I'm going to have to store the previous value, which means that I have to put the previous value in a latch, which generates extra hardware. You would put a default in here. So what would the default be? Well, I guess it would be next address equals zero. Now, that's not actually going to be exercised because we have specified all the enumerations. But again, if we ever specify another enumeration and forget to include it in the case, this is what will happen. And then we end the case statement with an end case. Okay. So we've taken care of what happens when we give the thing a command. What we also want to do is what happens when we set the reset line to one. So if reset equals one, then next address equals zero. That's it. You see that these are equal signs. There's also another type of equality, which is sort of like an arrow. There's a difference between those two. The equals is a blocking a sign while the one with the arrow is a non-blocking a sign. So the theory is that when you're in a combinatorial block, you only use the equals. This is the blocking equals. And what Verilog will do is it will essentially step through, or when it compiles this to logic, it looks at the code that you've written and steps through line by line. And any variable that you assign with a blocking statement gets assigned right then and there. So for example, if I said next address equals one, next address equals next address plus one, well, the value of next address after this would be two. Now, you don't do this in combinatorial blocks, but in other blocks, you can use non-blocking assignments. And essentially what Verilog does is it does these assignments all at the same time in parallel, which means that these two statements would be pretty much illegal, or if system Verilog doesn't actually have any error messages for this, the result would be indeterminate. Maybe it would be one. Maybe it would be the previous next address plus one. Who knows? So don't do that. So anyway, so if you look at this, what happens if you give the micro address counter a command, such as increment, but also set reset to one? Well, reset takes precedence because it comes at the end. So you would go through the code and you would say, oh, increment. So the next address is whatever the address is plus one, but then you would say, oh, but reset is one, so next address is set to zero. Obviously, if reset is not one, whatever these commands did is what next address is set to. All right, so we've taken care of the combinatorial part of the counter. So now let's take care of the register part of the counter. So on the positive clock edge, what we want to do is store the result of next address into the output address lines. So what this is is an always ff, which means flip-flop, at, which tells it when to execute this block on the positive edge of clock. Begin, always end it with an end. And I don't know why it didn't pick this up, but I'll do it anyway, always ff. Okay, so all we want to do here is we want to say address non-blocking a sign next address. That's it. That's all that needs to be done. So in always ff blocks, you should always use non-blocking assignments. So that would be it, and now all we have to do is end the module. So end module, done. Okay, so that's pretty easy, again, to go over what we've written. We have one package. This can be used by other files, and we put the enumerations for the micro-address counter, or the commands for the micro-address counter, in a separate file, in a package, so that we can use it in other modules, because other modules may want to send commands to the micro-address counter. We've added varilator public, which means that in our test bench we can actually utilize these instead of having to remember what the number of each command is. We've defined the micro-address counter. We've defined the input lines and the output lines. We've also defined a local variable, which is essentially an internal wire. We've defined a combinatorial section. So anytime the inputs change, the combinatorial block will execute, essentially. And we've also defined a flip-flop block, or a sequential block. And what this does is on the positive edge of clock, it goes ahead and does whatever is inside the block. And in this case we take whatever the next address is, and we assign it to the output register address. And that's it. Now let's compile it. So I have varilator, as should you, minus wall, which means turn on all warnings. That's really important. You never ignore warnings, because a warning could be a bad thing. So the next thing that we need to do is say minus CC. And what this means is we want to output C++ code. There is another thing called system C. I don't know what that is. I don't know why you would use it. I assume that there's a good reason for it, but I know C++. There are no forward declarations in system varilog, or in varilog, or in VHDL. So you have to define, or you have to put down the files, each file before any other file that uses it. So micro-address.v has to come before micro-address counter.v, because micro-address counter.v refers to things that were defined in micro-address.v, namely the micro-address package with all of its enumerations and its type def. We will also define a prefix, and I'll just call it micro-address. So every class that's generated by varilator will be prefixed with the word micro-address. This is good because it keeps things nice and separate. And that's all we need to do right now. So let's hit enter and see if the code actually compiles. No, it did not compile. Exiting due to one error. Why? It says unexpected case expecting end case in micro-address counter.v line 13. So let's go there. Aha! I'm an idiot because you don't use case. That's what I get for talking and programming at the same time. It's very difficult to do. All right, there we go. No warnings. That's perfect. Now varilator puts all of its generated code in the objdir directory. So let's go there and see what was generated. So I'm going to skip the mk file for now. We have micro-address.cpp and micro-address.h. We also have micro-address micro-address.cpp and micro-address micro-address.h and a bunch of other files. The micro-address.h file actually corresponds to the micro-address.v package. And then there is micro-address.h, which corresponds to the module. So if we look at micro-address micro-address.h, you can see that we have an enumeration called cmd, and we have all of our enumerated values, none, ink, and load. And if you look at micro-address.h, this is very hard to understand, and you don't really need to understand everything about it, but you can see that our signals have been defined. There's a clock, which goes from 0 to 0. That essentially means it's an array of logic of size 1. Reset, we have command, which is an array of logic from 1 down to 0. We have load address and we have address, which are 10 to 0. There are some local signals here, local variables, whatever. It doesn't really matter because these are just simulation things. The important thing to look at is there should be an eval method. This is a public method called eval. And what eval does is it goes through the blocks and evaluates and changes state. So that's what we're going to use to write our test bench to exercise the micro-address counter to see if it actually does what we think it does. All right, now that we've written the system verilog code, so we have some hardware, we can test it with a test bench, and this involves writing some C++. So I've already written the C++ code, so I will just go over it. The first thing that you need are some includes. So we have micro-address.h is the header file for the main module. In this case, the main module, or maybe in all cases, the main module is the module that comes last on the verilator command line. This is the top-level module. So that gets a header file that is named after the prefix, and the prefix was micro-address, micro-addr, so we have micro-address.h. Now that is going to need micro-address underscore micro-address.h, which, if you'll remember, is the header file corresponding to micro-address.v. So this contains the enumeration declaration. And finally, we need to include verilated.h, which is a header file that comes with verilator. What I've done is I've used table-driven testing. So the idea is that you create a structure of your test cases, and you can give your test case a name, and then you give the test cases all the stimuli that it will need, and any information that it will need to verify that what happened is what you expected. So in this particular case, I'm going to set up a reset line. I'm just going to use an 8-bit integer. Why not? A reset line, the command, which, of course, is going to be two bits, and the load address. And for the output, we want to verify that the output is the same as the expected address. Both of these are going to be 16-bit addresses. Of course, we've only got the 11 bits, but that's okay. All the other bits are zero. And then what you do is you define your test cases. So in this case, I am going to have a reset. So, of course, the reset line gets one. The command is none. Now, because this is in a system verilog package, that package was called micro-address underscore micro-address, mainly because the first micro-address is your prefix. And micro-address is the actual package name, which you'll recall is in system verilog. And then colon, colon, because that's the C++ separator for names. And then cmd, which you'll recall is the name of the enumeration. So we have micro-address colon, colon, cmd. And then colon, colon, whatever enumeration you want. So in this case, I'm sending a reset signal. I'm setting it to one, and I'm going to set the command to none. I'm going to set the load address to zero, and I expect the output address to also be zero. Now, what is the state of the module when it first starts up? You can tell verilator to randomize the state, which is usually a good idea. I did not do that here. You can add it as an exercise to the student. So the idea is that you would set up all of these inputs, then you would pulse the clock line so that it has a positive edge, and then you would check the output. So after that, presuming that the output address is zero, the next thing that I test is increment. So I set the reset line to zero, I set the command to increment, I set the load address to zero, and of course I expect to see the output one. The next thing I test is to make sure that the none command actually did work, because remember, the only time that I used it before was in reset. So for the none command, I expect the output not to change. So reset is zero, command is none, load address is zero, I expect the output to be one. Then I try reset again. So of course I expect the output to be zero. The next thing I test is load. So I set reset to zero, the command to load, the address just for the sake of argument FA, and I expect the output to be FA. Then I test that increment does what we think it does again. So I set the command to increment, the load address to zero, and I expect the output to be FB. Finally, I do another reset with the reset line being one, the command being none, load address zero, and again I expect the output to be zero. So these tests run one after the other in order, and all of their outputs must be the same as expected. So what does the main look like? So here is the standard C++ main function. The first thing that you do is you give varilator the command line argument so that it can intercept all the command lines and do whatever it needs to. So that's varilator's initialization. The next thing that you do is you create a new micro address module, and you do that by simply as in C++ you create a micro address instance of you create an instance of the micro address class, and we called it counter, and of course you get back a pointer. So we set all the data in the counter. So we set the clock to zero. We set reset to zero. Here I'm just doing an ordinary reset. I set the command to none. I set the load address to zero. And then this is the important bit. I call the counters eval function, and what that does is it runs the module. So it goes through and it determines if anything has changed, and if so it runs through the logic and does whatever it needs to. In this case of course nothing's really going to happen, but by calling eval after every change of inputs, you at least get a clean instance of the module. So the next thing that I do is I determine the number of test cases, which is of course the size of the test cases structure. That's this whole structure over here in bytes divided by the size of a single test case in bytes. Then I step through each test case, and here is where I'm just loading up the test case, and I set the command to the test case command. I set the reset line to the test cases reset line, set the load address to the test cases load address, and then I call eval. Now I still haven't touched the clocks, so nothing should change. Now I set the clock to one and I call eval. Then I set the clock to zero and I call eval. At that point I know that I have given a positive edge to the clock, so I can now check the output. I check counter the address of the counter, and I test it against the expected address of my test case. And if it worked I just say that that test case passed, and if it failed I say that it failed, what I expected, and what it actually was. Then we go through the loop and when we're done, just to clean up what you should do is call final on your module, which basically runs whatever finalization is in the module. In this case we're not going to ever have any finalization. Still it's good code practice. Then you delete the counter and then you exit. Now why delete the counter just before you exit? Again, that's good coding practice. You allocated a new micro address counter, so you should delete it. And then you exit with a status code of zero because why not? That's it. That's all you need. So let's go ahead and run the veriliter command line to actually run this test. So it's going to be veriliter, and then the beginning of it is going to be exactly the same as before. So minus wall, always minus wall, minus CC to output generated code in C++. What have we got? Micro address dot v comes first because that is lowest in the dependency chain. And then micro address counter dot v is your top level module. And we're going to do minus prefix micro address so that the top level module is called micro address and all of the C files and all of the auxiliary files are prefixed by micro address. And then this is new. So we do minus minus exe, which basically says I am about to specify an execution source file, and it's going to be micro address counter test dot cpp. And the next thing that I do because I want to be sure that we're going to compile using the correct C++ standard because the greatest thing about standards is that there are so many to choose from. So I'm going to specify C flags space quote minus STD that's minus standard equals C++11. So I'm just coding in C++ version 2011, also known as C++11. So C flags is basically just a flag that gets passed to GCC, the compiler, and I hit enter, and it just went ahead and generated all the files that it needed in objdir. So if I look at objdir, we have a couple of extra files here. We have a micro address executable file, and we also have... Actually, I think that was from before. Yeah. Actually, okay. Why don't I just remove everything in objectdir? That was from my previous test. So again, let me do this. Let me go to objectdir and take a look. Oh, yeah. That's better. Okay. So we have micro address dot mk. That is going to be the important file. So what we do in order to create the executable, so dot mk is a make file. So we do make minus j8. What that does is it just uses eight threads to make the executable. So each thread can be allocated to do a compilation. It looks like we've got one, two, three, three files, so maybe it will use up three threads. I'm just going to use eight, because I think that's how many cores I have. And then minus f to specify the make file, and then it's micro address dot mk. Space, and then the name of your executable. I'm just going to call it micro address, and it's done. No errors. That's great. No warnings. That's great, because warnings and errors are bad, and warnings are even worse, because they could turn into errors, and you wouldn't even know it. Now we can take a look, and we can see this executable up here. So let me run it, and hey, everything passed. That's awesome. So what would happen if I, for whatever reason, made a mistake in my test case, and I said, oh, well, you know, after you increment from fA, you would get fC. So what we want to do, obviously we have to recompile everything. So in this case, I just have to run make, because all I've done, I haven't changed any of the system verilog source code. I've only changed my main CPP code. Well, okay, I lied. That's not quite true, because the CPP code, if you look over here, I don't know, that's actually correct. I'm sorry. I was looking at, I was confused by all these micro address prefixed files. I was looking for a micro address counter test.cpp, which is not in the object directory. So it must pull it from the parent directory. So I've changed the test bench. I just have to run make. Great. So all it did was compile the one file that changed. That's great. And now I can run micro address. And we can see that one of the tests failed, namely the increment test that expected fC, but was fB. So what I could do is I could say, well, is that correct or is that incorrect? Well, obviously in this case, I take a close look at the situation and I say, oh, I screwed up the test case. It should have been fB. And then of course I can run make again and I can run the test again and all the tests pass. So that's great. We have some minimal code, which can do increment and load and reset. And for the next video, what we're going to do is we're going to put this into the Xilinx IDE, so that we can download it to an actual FPGA, which I will hook up to an Arduino, which will, and the Arduino is the one that's going to stimulate the hardware, and we can see the Arduino output of the address. That is that results. So that will be for the next video. Until then, take care.