 Rwy'n gael ychydig i'w ddweud fel Llyfrgellwyr yn dweud yn eich cyffredinol ar y cyflwytaeth fel Llyfrgellwyr. Rydyn ni'n ddweud am y dyfodol, dwi'n ddigitaliogu yn ei wneud yn ymddangos. Rydyn ni'n ddweud am ymddangos, dyfodol ymddangos ymddangos. Mae'r ddigitaliogu yn ei ddweud. Yn y ddweud o'r ddweud o'r ddweud, dyfodol i'r ddweud, ddweud yn ei ddweud syntax, but a bit more worthy and a bit less curly braces. You can use it for designing all kinds of different digital logic but I normally encounter it in the context of CPU designs. You can take your Verilog code and synthesize from it a silicon implementation or target in FPGA or you can simulate the Verilog code and that simulation is the subject of what I'm talking about. I usually encounter this kind of simulation technique in my work as a compiler engineer at Embercosm. Embercosm's customers are normally semi-conductor manufacturers who are developing their own processor architectures and designs and we provide them with an open source software toolchain so that includes things like the compiler, the assembler, the linker, the debugger and all the binary utilities. But we're normally working with them on these tools at the same time as they're developing their hardware. So it's very useful to be able to use simulation techniques for us to test the compiler and run the generated code on a model of their actual hardware. And it also helps tease out some hardware bugs as well early on in the process. So the way I'm going to approach this is just to go over a quick outline of Verilog. If you're not going to have Verilog before it will be a quick primer or maybe a refresher if you're a bit familiar, a bit of familiarity with it. Then I'll talk about different simulation approaches in particular for CPUs. We'll see where Cycloacric modeling fits in with that. Then I'll talk about what Verilog is and how you would use it. And then talk a bit more about how you can use trace files and trace dumps to look inside what's going on in the model. There's also a workshop tomorrow on this subject and I'll talk a bit more about that later. So this is a quick overview of Verilog. So you can use it to represent things like combinatorial logic. So things like gates and or not and combine them together to compute arbitrary Boolean functions. You can use it to represent sequential logic as well. So things like this is a flip flop. A flip flop can store one bit of state at a time. It has a clock input and its state can be updated once on each clock edge. So those are the basic building blocks but we wouldn't normally work at a level of individual gates or individual flip flops at a time because it's too fiddly and fine grained a level of abstraction. So in Verilog things are grouped together. So you make registers by grouping together flip flops. So one flip flop, sorry, a group of eight flip flops might be represented by an 8-bit register. You can also connect registers and other things together using networks of wires. So the very low declaration of these things looks a bit like this. Here we've got a declaration of a 32-bit register and beneath it an 8-bit network of wires. The structure in Verilog around these things is made, I think of as three things. So we use modules to create reusable, to abstract away functionality into reusable components. And then inside the modules we have the sequential and the combinatorial logic to implement, to provide their implementation. So we'll look at each of these three things in a little bit. First of all, a quick look at modules. So this is the simplest Verilog module that I can imagine. It is something which just has one output and it continuously assigns the value one to that output. So this idea of continuous execution is an important point. Unlike other programming languages that you might have seen like C, the Verilog code isn't executed as a sequence of steps that happen one after the other. But in some sense, all of the code is concurrently executed all of the time. So it's a bit hard to grasp that idea in the abstract sense in which I'm describing it now. So if you try and keep it in mind over the next few slides and code examples. This is an example of something that might be a slightly more interesting module. So this module might represent some chip and have some, it might have a clock input, several connections to an SRAM memory and some PMOD outputs for input and output, general purpose input and output. So what this shows is just the interface of that module. There's no definition of the implementation here. So if you wanted to make use of this module, you wouldn't need to know what's happening internally. You would just need to know what its interface is. You could think of this a bit like the function prototype in C or being analogous to that. So next we'll have a look at some combinatorial Verilog constructs. So there's a lot of code on the next few slides, but I don't want to try to focus too much on all the syntax on the slides, but more to give you a feel of the kind of things that you can express easily in Verilog. So there's logical bitwise primitives. So things like negation and or and not and excusable and that kind of thing. You can also shift values up and down in registers. So there's both arithmetic and logical shifts. Unlike in C, you can choose the type of shift by using the syntax rather than it depending on the type of the operands. It's often going to be the case that you'll have wires or registers of different bit widths that you'd like to assign to something else that's another different bit width. So what it can be quite useful to do is to concatenate some wires or register values together to create a wider bit width out value. So you can do that with this curly braces syntax. I'll just go through a couple of examples on the slide. So in the first, so from the fourth line down, we can take two one bit values A and B and concatenate them to assign them to a two bit value on the left hand side. You can also concatenate things with constant values. So we concatenate A with the constant one. Or you can concatenate any number of values and constants to create a wider output. There's a few more examples on the slide but I won't go into detail about all of them. The slides will be available after and there will be some more detail in the workshop about some of these things. You can also use if and else constructs to conditionally control the execution of some statements. So the top code example is a function that computes the minimum of two values. In general, when you write a conditional, that will depend on some boolean value and then you'll have some statements in the if branch and the else branch. If you want to have more than one statement in a branch, a bit like C, you need to put these begin and end keywords around them. So the begin and end keywords are in place of where you would have say curly braces in C. Something that's semantically a bit different from what you'll have seen in software programming languages are always blocks. So an always block contains some statements that are executed depending on what the timing controls say. So the timing control is specified after the always keyword with an act. So for the combinatorial logic that we've just looked at, you would always write at star and that means execute this block whenever one of its inputs changes. Right. So a quick look at a little bit of sequential variable log as well. So this is the clock's logic. So you can have an always block for sequential logic as well. In this case, instead of the timing control, depending on all of the inputs to the block, this example is of a block that will be executed or triggered on the positive edge of the clock or its input. You could also use the negative edge. In this example, the register A is going to be assigned the value of B on the next clock edge. So when we write this, the syntax for this assignment in sequential logic is also a bit different to what we've used in combinatorial logic. So we'll have a little bit more of a look at that. So these are called delayed or non-blocking assignments when we write less than or equals. It causes the value on the right-hand side to be transferred to the variable on the left-hand side on the next clock edge. Whereas when we were writing the plane equals assignment with sequential logic, the value will be assigned immediately. So it means that you can do some interesting things with registers. So in this code example, the values of B and A will be swapped on every clock cycle. The values of B and A on the right-hand side are both taken from the current clock cycle, and then they're both assigned to A and B on the next clock edge. So that was a bit of a whirlwind tour of Verilog. So just to try and take stock of the things that we've just had a quick look at, it's a language for hardware description with a syntax like C, but semantically it's quite different to C with its concurrent sort of execution, things like always blocks and delayed assignments. You use modules to create blocks of functionality that are reusable. And then inside the modules, the implementation uses combinatorial and sequential logic. So I'm just going to talk now a little bit about the different approaches to simulating Verilog. Okay, so there's different approaches to simulating CPUs in particular. So one thing you can do is to write an instruction set simulator, which would just be a piece of software that you would code up that implements your instruction set architecture. The disadvantage of doing that is that it's not actually got any relationship to any Verilog code. It's just a simulator for the architecture. So what you might do instead is use a tool to generate a cycle accurate model of the Verilog code. That model that you get out of it will be able to show you what's happening on each clock edge of the model. So it's a bit more of a powerful tool than an instruction set simulator for seeing what's going on with the Verilog. Or you could use an event driven simulator, which is going to produce a more accurate simulation or realistic simulation in some sense because it can also show how events occur within one clock cycle as well. So it's the most accurate approach to simulation. The reason you wouldn't necessarily always want to use that is that it's the slowest form of simulation as well. The instruction set simulator is the fastest form, but it's got the disadvantage that it doesn't give you a timing model or connection with your Verilog code. So in between those two approaches is cycle accurate modelling, which is a trade-off between accuracy and speed compared to the other two approaches. So that's what Verilog does. So Verilator is a tool that you can use to take your Verilog and it compiles it into a cycle accurate model written in C++ or system C. So I'm just going to talk about C++ because I don't know anything about system C. It's a free and open source project. The variable project that it comes from is for free and open source Verilog and system C tools. It's quite widely used in industry and academia. So I had a look on Wikipedia. There's big companies like NXP and ARM using it as well. Then there's smaller companies like M because I'm making use of it too. So this is the kind of flow if you're using Verilator to build a model of some Verilog code. On the left, if you have some Verilog code, you would run Verilator on that and that would generate all your C++ model files. So although you have the model, it's not going to do anything by itself. So you need to write a test bench to drive it. So that test bench will instantiate the model and drive it forwards in time. Then you can compile and link your test bench and the generated model together. That will give you an executable simulator that you can run just like any other program. I'm going to work through an example of using Verilator to model one very simple Verilog module, which is a counter. So this counter is a module that takes a clock signal as input and then it counts on each clock cycle. Every end clock cycle is going to increment the value of an external counter. It's got a couple of other signals as well that I'm just not going to worry about for the purpose of the example. So the first thing you need to do to model it is you write a top module in which you can instantiate the module that you're interested in. So the inputs and outputs of your top module are things that are going to be exposed to the C++ test bench that it will be able to interact with. So what I've done here is created four inputs and outputs that will be able to be seen from C++. Inside the module the counter is instantiated and then the inputs and outputs of the counter are hooked up to the inputs and outputs of the top module. So in this example this is just a trivial wrapper around the counter but for something more realistic you might have several different modules that you'll be instantiating and connecting some of their inputs and outputs together. You would only expose a subset of all those signals to the test bench. Once you've written the top module you need to verilate the Verilog code. So I normally sort this all out with a make file rather than go into the details of exactly what's in a make file. These are just the relevant invocations from it. So you would call Verilator. It needs to know what all your Verilog source files are. You tell it what the top module is and that you want to generate C++ code and that's enough for it to run. When it does run it will generate a folder usually called Objder that contains all of its generated outputs. So that's ready to compile. Inside there you get a make file as well for building the model that it's produced. So you need to call with that make file and then usually there's a few different targets that you would want to build. So a target in this case vtopall.a is the target for the top module. So the name of the target is derived from the name of the top module and then there's a couple of other object files that you would always build as well because they contain utility functions that would be used by your model. So that's sort of done on the Verilog and Verilator side. When you create your test bench, C++ source you will have a class for the model, so vtop, that you need to instantiate and then some of those inputs and outputs that we put in the interface of the module are now accessible as members of those in C++. So here in the example we're setting reset to one and able to zero. If you want to, once you've instantiated the model you probably want to actually drive it forward in time so that it will do something. So I normally wrap this in a function for the clock in the model. So as a clock it's just a signal that alternates between a high and a low state. The clock model function is going to alternately set the clock pin to zero and one. Every time it changes the value of the clock pin you need to call the eval method of the model and that propagates all the state updates inside the model. So once you've instantiated the model you can repeatedly clock it to drive it forward in time and that's sufficient for interacting with the model like a black box. Probably what you're going to want to do as well from the test benches is access the internal state to see what it is or maybe modify it. So you can do those with tasks and functions. So we'll have a quick look at an example of one of each of these but before we do that a bit about the internal implementation of the counter. So the counter has a register inside it called internal count. Every time the clock ticks if that internal count is about to roll over then we increment the external count otherwise just the internal count is incremented. So this internal count is always going on but you can't see it. So let's write a function and a task to access that internal count. So we can declare a function to access it using the function keyword. After the function keyword you're declaring the bit width of what the function returns then its name so we call this readInternalCounter. If you want Verilator to expose this to the C++ test bench then you need to Verilator public in a comment as well so it knows to do that. Whatever you assign back to the name of the function is what's returned back into C++ so here we'll just assign internal count back to readInternalCounter. So if we rebuild the model with Verilator and then recompile it then from the test bench we can access that function now. So the model object that you have in C++ has got a sort of object-oriented structure but mirrors the structure of the Verilog. So starting from the model object we have to go through the top module the counter with instantiated and then it's there that we can find that function that we've just added. Can I just check how I'm doing for time? Five minutes, okay. So if you wanted to write back the state with a task then, sorry, if you wanted to write back or modify the state then you can use a task to do that. So we're here with declare a task with the task keyword. Again it's going to be told Verilator public so it knows to be exposed to the test bench. That's going to take an input that's declared that's going to be the new value of the counter and then inside the body of the task we'll just assign the internal count that's passed in to the internal counter. If we rebuild the model again then from the test bench we can access the task call the task in a similar way to call in the function through the top module on the counter passing it the value that we want to assign to the internal counter. Then once we've done that straight away afterwards we need to call the eval method of the model again to actually get the model to update that state that we've just changed with the task. There is a little bit of subtlety about using a task in this way that I'm kind of glossing over because it's a bit, because I'm a little bit short on time but there's a little bit more about it that I was going to put in the workshop for tomorrow in the details of that. So using functions and tasks is a handy way to access the internal state to see what's going on but you might not want a code or a function or a task every time you want to check out some internal bit of state so instead what you can do is create trace files of the execution of the model that you can then visualize and that can give you a much easier way to see what's going on in the model. So in Verilator to do that you just have to create a new trace object look it up to your model and then when your simulation is or model is running once it's completed close the trace file and it will be written out to disk. So the Verilator does need to know when to dump its state into this trace file so what I would normally do is modify my clock model function so that every time the clock goes high or low the state of the model is written to the trace file. The trace file also needs to be told what the simulation time is as well because everything's got a time stamp so you need to keep track of what the simulation time is. You can find that because you're dumping everything at every clock edge this can make really big trace files so if you find that you're having that as a problem and you might need to think about selectively enabling dump just for points in time that you're interested in instead. So rather than trying to talk about GTK where everything's the easiest thing to do is to just see it in use so when you open it up with a file a trace file in the left hand pane you have your verilog modules on the signals in it and on the right hand side is the wave viewer. I know it's very difficult to see but you can basically navigate the structure of your verilog and then if you pick a particular module that's instantiated you can see all the signals on the left. So if you're interested in a signal then drag it over to the right hand pane and it will show you what the waves are sequentially in time. It's going to zoom out a bit. So you can see the clock there. We might have a look at the external counter and we can see that it starts incrementing after a few cycles. Why is it doing that? Maybe if we have a look at the internal counter we can see that that increments a lot faster than the external counter. The process doesn't seem to start if immediately there's a few clock cycles before anything happens. So we can have a look at the enable pin because I think that when you look at the waves in this way I think this is actually exposing a bug in the counter because as soon as it got enabled the internal counter tripped over to one instead of spending a cycle when it should have been at zero. So quite often you can look at these traces in GCK wave and see what's going on in quite an intuitive way to spot where things aren't quite working as you expected. So that's sort of the end of the quick verilater tutorial. We've used verilater to build the software model and then drive it with a C++ test bench. Then to access and work with the internal state we've added functions and tasks and to view the state of the model and visualize it we've used GCK wave to look at the change dumps in the trace file. OK. So that sort of concludes the tutorial. There is a workshop tomorrow where there will be some material sort of expanding on this counter example if you're sort of beginning verilog or verilater and want to try and work through it and see it in a bit more detail. On the other end of the spectrum there's also some open source RIS5 cores that you can build a verilater model off and see what the cores are doing and execute programs on them. That's that's all. So thank you for listening.