How to make a fully-fledged Local Memory Bus (LMB) IP in Vivado

This is a general tutorial on making LMB IPs in Xilinx Vivado. We gained such experience when working on projects using Enzian.

Xilinx Vivado has the interesting philosophy of using IP blocks and the GUI-based IP integrator. It eases the work of wiring and assigning addresses (well, slightly), but hugely complicates the process of creating your own IPs with non-AXI buses. Xilinx documentations, including UG896, UG994 and UG1118, provide little information other than what is obvious from the GUI.

On the other hand, sometimes you have to use the IP integrator. For instance, I didn’t find a way to instantiate MicroBlaze without involving the IP integrator. Yes, you can avoid touching the GUI by writing tcl script, but that’s still the “Vivado way”.

This is how the story starts. I need to write a component that can be integrated with the MicroBlaze using the Local Memory Bus (LMB). According to the MicroBlaze Product Reference Guide (UG984), LMB is “a synchronous bus used primarily to access on-chip block RAM.” It supports only one master device (the MicroBlaze), supports only 32- or 64-bit width, but it’s fast: read and write take only 1 cycle (unless the device blocks the bus). It also supports the Address Editor in the IP Integrator.

I had to create an IP block. Instantiating RTL blocks in the IP Integrator gave me a block, but the Address Editor refused to work with it, even after trying out all X_INTERFACE_PARAMETER in the VHDL code. Routing the port back to the HDL (by creating a LMB port) did not work either. Vivado raised errors when generating the stub for the block design. For AXI, AXI Lite or AXI Stream, both approaches work. But for LMB, creating an IP seemed the only way to go.

In the following text, I would assume your basic knowledge of HDL code and Vivado (especially the IP packager). To learn them, there are many wonderful tutorials on the Internet :)

Before we dive in, let’s take a closer look at the LMB protocol. This is best illustrated by UG984 in Figure 50-57. The following is a N-wait-cycle read operation.

LMB waveform on a N-wait-cycle read operation
LMB waveform on a N-wait-cycle read operation

If you are familiar with AXI, here are the important differences in LMB:

  • READY in LMB is an ACK rather than the handshaking signal in AXI. It should stay low when there is no transaction. Otherwise, the whole LMB bus is blocked.
  • The LMB interconnect IP (Local Memory Bus 1.0) neither translates the addresses nor isolates downstream IPs. All LMB peripherals see the raw addresses. It’s their responsibility to response to only the addresses they are assigned with. The interconnect simply ORs all READY signals, which is synthesized into combinatorial logic. Again, that means a broken LMB IP renders the whole bus useless.
  • The input address, data and strobe signals only hold for one cycle. In the wait cycles, those signals are deasserted. This means the IP must have the capability to register the input immediately.

Design a LMB-Based Simple 64-Bit Counter

Advanced hardware engineering is not the focus of this tutorial. Let’s look at a very simple component: a 64-bit counter counting upwards every cycle, without soft reset or any other advanced functionalities. The counter value is always available, so there is no need to block the bus via the WAIT signal.

The VHDL code is in lmb_simple_counter.vhd. Let’s look at these lines:

if S_AddrStrobe = '1' and (S_ABus and ADDR_MASK) = ADDR_CHECK then
    S_READY <= '1';
else
    S_READY <= '0';
end if;

When the AddrStrobe is asserted (either read or write) and the address falls into the assigned range, READY gets asserted in the next cycle. ADDR_MASK and ADDR_CHECK are constants from parameters C_BASEADDR , C_HIGHADDR  and C_MASK. We will explain these C_* parameters later.

The VHDL code should be intuitive and not our focus here. Instead, bridging the IP Integrator and those parameters are the key to create this IP.

IP Packager

If you add this IP repository in a Vivado project, and edit the IP in the IP Packager, you will see the following in tab Package IP.

First, in tab Customization Parameters, those VHDL parameters are already configured. In general, the IP Packager can detect parameters in the HDL code and add them to this tab automatically. By default, they are hidden. You can configure them and make them visible in the GUI configuration window of this IP when used in the IP Integrator.

Vivado IP Packager - Parameters
Vivado IP Packager - Parameters

Second, in tab Ports and Interface, you should see a LMB bus already configured and bound to the corresponding VHDL ports.

Vivado IP Packager - Ports and Interface
Vivado IP Packager - Ports and Interface

In the interface configuration - Port Mapping, note that the clock and the reset port are not mapped. If they are mapped, they become part of the interface and disappear from the IP block.

Vivado IP Packager - Port Mapping
Vivado IP Packager - Port Mapping

In tab Parameters, for LMB, there is nothing compulsory to be configured. For AXI, I have seen case where FREQ_HZ is said to be requiring settings but should not be set, otherwise the IP integrator complains at the validation time. This StackOverflow answer may be relevant.

Vivado IP Packager - Port Parameters
Vivado IP Packager - Port Parameters

OK, and here comes the tricky part: in “Addressing and Memory”. First, there is a Memory Map called “LMB_S”. Within it, there is an Address Blocks with size 4K called “REG”, which represents the register space. The Address Block has two parameters, OFFSET_BASE_PARAM and OFFSET_HIGH_PARAM, which are set to C_BASEADDR and C_HIGHADDR  respectively.

Vivado
     IP Packager - Addressing and Memory
Vivado IP Packager - Addressing and Memory

Where do the names of the two parameters come from, and what exactly they do, is a mystery. The closest clue is this answer on the Xilinx Forum.

And yet, this is not the end of the story. The configuration above creates a configurable address range in the Address Editor, but it is not applied to the generated instantiation code: the default values are used for C_BASEADDR and C_HIGHADDR.

After looking into some IPs from Xilinx, it turns out there is still big missing piece. The secret is in the file bd/bd.tcl.

Parameter Propagation

Taking a step back, I should first say how IP Integrator applies settings to IP blocks. Xilinx calls it parameter propagation, “one of the most powerful features available in IP integrator” (UG994). In fact, if you have ever observed those “auto” parameters, they are set by this mechanism.

However, this is not done automatically. If you are somewhat familiar with the tcl script and take a look at bd/bd.tcl, you will immediately see it: everything is done through tcl manually. You can also see the parameter C_BASEADDR and C_HIGHADDR being mentioned there (at this point, I even think those OFFSET_BASE/HIGH_PARAM we set before are useless).

The content of this tcl file is copied from an Xilinx IP and slightly adapted for our needs. I won’t dive into the details of it, but just point out one important functionality of it: calculating a minimal address mark for deciding whether an access falls into the address range by comparing minimal number of bits. This requires traversing the topology of the LMB network. It’s a smart trick - while we can always compare against the base address and the high address, comparing less bits is always better.

The interesting thing is, in UG1118, the guide of the IP Packager, the following is stated:

IP Packager Output Rules and Limitations: … Parameter Propagation: packager output does not provide access to parameter propagation. However, IP Packager can be guided by pragmas.

(Thank you Xilinx!)

I don’t know what does “guided by pragmas” means. By comparing with the component.xml from the Xilinx IP, the following lines are missing from the one output from the IP Packager.

    ...
        <spirit:fileSet>
          <spirit:name>xilinx_blockdiagram_view_fileset</spirit:name>
          <spirit:file>
            <spirit:name>bd/bd.tcl</spirit:name>
            <spirit:fileType>tclSource</spirit:fileType>
          </spirit:file>
        </spirit:fileSet>
    ...

The code is intuitive. It takes bd/bd.tcl as a special source. With these lines added, the IP block works. Settings in the Address Editors are applied to the generated HDL code for the IP block.

Note that these lines disappear when the XML is overwritten by the IP Package. I use Git and an CI/CD script to prevent this pitfall.

Conclusion

It’s a lot of fun playing with (fighting against) Vivado. If you happen to have more insights on how things can be done better, please follow up with emails (enzian-contact@lists.inf.ethz.ch)!

Have fun with Vivado!