Using IP Tastefully

Fall 2022

You are not logged in.

Please Log In for full access to the web site.
Note that this link will take you to an external site (https://shimmer.mit.edu) to authenticate, and then you will be redirected back to this page.

Working with IP is a little tricky. If we were working with Vivado normally through it's GUI, we'd go to the IP catalog, configure our IP and add it to our project. The steps are simple enough, but behind the scenes it's really obnoxious because:

  • Vivado has a few things that are implemented differently from the Verilog standard which makes it slightly imcompatible with iVerilog for simulation (there's a reason we had to include iverilog_hack.svh on lab03). We could just use Vivado's built-in simulator (called xsim) but it's slow, proprietary, and doesn't run everywhere. All the normal problems that Vivado has.
  • Some IP modules (like DDR controllers) use proprietary models for simulation, and those are hard to decouple from the IP module itself.
  • Using the Vivado GUI to configure IP requires you to either come into lab or use local Vivado (if supported). That's pretty restrictive for non Windows/Linux, and defeats the purpose of having minimal build tools like lab-bc
  • There's a bajillion ways that Vivado can store IP (either as XML that describes the IP configuration, Verilog that describes the hardware implementation, or as presynthesized modules that are dropped in at build time) and tracking normal Verilog is the easiest for us.

As a result, we've provided some tastefully selected and prepared IP for you here, along with some usage notes. Some things (like clocks) are easiest to configure on a lab machine and export to your project, and while other things (like BRAMs) have templates that let you bypass the Vivado configuration tools. We'll talk about the best way of dealing with each below.

There's also a lot more IP out there than what we talk about here - things like FFTs, CORDIC math, and DDR controllers are all game. And there's also more good Verilog than what's in the IP catalog - look around on GitHub! For instance there's a fantastic open-source implementation of the HDMI 1.4 standard here that I've used for a few things, and the author has a bunch of other super neat modules too.

Please reach out if you have questions about any of this! Issues with IP are nuanced and we don't want you to struggle with problems that we can probably solve :)

1) Clock Generation

1.1) How it Works

Our Artix-7 Xilinx chips use two types of primitives to generate clocks - MMCMs and PLLs. Both work by multiplying and dividing the frequency of a reference clock to produce a desired output frequency. We can really only multiply and divide our clock frequency by integers, so we usually have to do this in a few stages to get good resolution in output frequency. For instance, we can divide down a clock with a counter, just like you saw when we generated a lower-frequency output clock in pset04. Since we can have our counter reset at any integer number of input clock cycles, we can produce a clock that's 1/nth the frequency of the input clock.

We use this trick to multiply too. It's pretty easy to make a circuit that matches the frequency of two oscillators if one of them has adjustable frequency - most of the time a Voltage-Controlled Oscillator (VCO) is used for that. Adding a 1/n clock divider between the controllable oscillator and the frequency-matching circuit tricks the system into setting the VCO to a frequency that's n times higher than the refrence clock, effectively multiplying it.

Increasing/decreasing frequency in integer ratios over and over lets us choose our output frequency with pretty respectable precision. Internally it's very hard for the VCOs to generate stable frequencies above 700-800 MHz and below like 5 MHz or so, so we need multiply/divide paths that stays within those two bounds. That's part of the job that the IP configuration tool does. Not all frequencies are achievable, but a good number are.

1.2) Making IP

If you need to generate a clock, it's best to fire up Vivado's GUI on either a lab computer or locally, configure your IP, copy the resulting Verilog file, and put that in your src/ folder. Here's what that looks like:

_TODO: Add screenshots here - going to do once I'm in lab and can look at Vivado -fischerm _

Most of the settings are fairly straightforward, but there's a few things to note:

  • If you need to generate multiple clocks, try to generate them all with the same clock generator IP. Each MMCM/PLL primitive can make multiple clocks, and Vivado has an easier job routing clocks through clock domains when they all go through the same primitive. This also saves you MMCM/PLL primitives, as there's a finite number of them on the board. At the very least don't daisy-chain them one after another, that'll drive Vivado mad when it builds your design.
  • Leaving the jitter/skew settings to their defaults is almost always the way to go. You shouldn't need to be worried about manually setting picosecond timings in your design - if your design is dependent on super super precise timings, then it's worth taking a serious look at your system architecture.
  • These generated clocks won't work in simulation! If you look at the contents of their Verilog, they explictly instantiate primitives on the chip - primitives that iVerilog has no idea about! You'll want to use an always statment inside your testbenches to generate your simulation clocks, just like we've been doing in all of our testbenches thus far.

2) Block Memory (BROM/BRAM)

If you need a BRAM, download one of our templates from the block_memory section of this GitHub repo. You don't need to open Vivado to configure these - they're fully determined by the parameters you set when you instantiate them. Choosing the right type of block memory, setting it's width and height appropriately, and properly loading the init file should give you the memory that you need.

It's worth noting that these BRAMs are inferred, meaning that we don't explictly tell Vivado to map them to block memory, it just figures out based on the structure of the Verilog files that we give it. We pulled our block memory templates from Vivado's built-in examples so it should always figure out that we want it to use BRAM, but it's possible that something goes wrong and it'll use LUTRAM or some other type of memory instead. If you're worried about this, just check the resource utilization numbers in your build logs, and make sure that you're using the amount of BRAM that you expect.

Only other thing to note is that we've included a macro in block_memory/iverilog_hack.svh to make simulating these modules a little easier. This fixes some silly Vivado behavior where it expects filenames containing the BRAM init data (the .mem files) to be provided without the data/ prefix, even though we store them in the data/ folder. This macro automatically detects if the module is being used in synthesis or simulation, and fixes the filename accordingly. This does mean that you need to use .INIT_FILE(`FPATH(data/filename.mem)) when you instantiate the BRAM instead of just .INIT_FILE("data/filename.mem"). If you don't do this then Vivado won't load your .mem file, which will only generate a warning in your build logs, not an error! This means this can fail quietly and your BRAMs will be empty! Be careful!

3) Everything Else

For everything else, it's best to use the IP catalog to generate your IP and export the corresponding Verilog, just like you'd do for clock generation. Only thing that's worth noting is that some IP blocks aren't just single Verilog files that are super portable, some IP is Verilog wrappers for big ugly VHDL blocks, and some IP even synthesizes more IP to support itself. It's kind of a wild west out there, let us know what you're trying to do and we'll experiment with it together and find something that works.