This is part 2 from a series of tutorials (full list is given at the bottom) describing a fast inter-arrival time pulse counter implemented in FPGA. Today our goal is to take the hardware design we've made in part 1 and allow it to easily interface with the outside world. For that, we'll be using the AXI interface which is the standard used by AMD (Xilinx) FPGA hardware designs for interacting with each other.
2. Prerequisites- basic knowledge of VHDL (you should be familiar with VHDL syntax)
- installed Vivado 2022.2 (an earlier version is also fine)
Now that we have a working FPGA pulse counter, we'll want to preserve the data generated by our device. Our component is implemented in the programmable logic (PL) part of the Zynq chip, and we'll need to transfer the data to the processing (PS) part to analyze it or transfer to a personal computer for processing. AMD designs use AXI interface for this purpose which can be rather complicated. Fortunately for us, we are dealing with the simplest possible scenario which is illustrated in this figure (described in detail here):
During the transfer, the source of DATA (our counter) raises the Tvalid flag when it is ready to transmit data, and the transaction is successful as long as the destination is ready to receive the data (Tready is high). You'll notice that our counter (described in detail in part 1) already basically fits into this protocol. All we need to do is create a wrapper around it and name the input and output ports according to the convention followed by Vivado so that the software can automatically recognize our interface as the AXI stream interface.
But what happens if the destination is not ready (Tvalid is high but Tready is low)? Well, our counter is not equipped to deal with this situation so it's going to have to push forward as if nothing happened. But it should at least inform the user that a data entry has been dropped, and therefore we are going to add an overflow flag to our design that will be used later to trigger an interrupt in the processor.
It is time to write the wrapper we've just talked about that I call iatcollector2ch_axis. Here is the diagram; the code and the testbench can be found below among the attachments.
We have 5 ports with the m_axis prefix which stands for Master AXIStream. This prefix guarantees that our design will be recognized by Vivado's IP integrator as having the AXI stream interface.
Here is the portion of the simulation result that illustrates overflow generation:
You can see that while the ports have been renamed, the principles remain the same. m_axis_tvalid reports when our data (m_axis_tdata) is ready to be passed down. The overflow signal is generated when the destination is not ready to receive the data (m_axis_tready is low). You may also notice that the output of the counter is delayed by two clock cycles now. The reason for this is the implementation of the reset as required by the AXI protocol. However, the performance of the counter is still unaffected since it accurately reports inter-arrival time between two pulses.
4. Smart streaming fast counterOur next task is to simplify reusing the counter in a block design. If you have some experience with IPs included with Vivado, you know that many of them have a Slave AXI-Lite interface (S_AXI_Lite). This allows the software to dynamically control the behavior of your design. We have two parameters that are good candidates for such control, enable_cnt and max_count. Adding S_AXI interface will allow us simple access to writing a new value of max_count or enabling the counter, and for that reason I'm calling this design iatcollector2chSmart_axis.
Slave AXI Lite interface is rather complicated, even in its most basic form. For that reason, I'm not going to discuss it any further (you can find the working code among the attachments). Instead, here are some useful resources in case you want to dive deeper. You can skip the next section if you are not interested.
5. Useful resources for understanding AXI protocols.5.1 One quick way to get your feet wet with AXI Lite is to let Vivado generate an example component for you. This is exactly what I did while designing my custom IP, and most of the code for iatcollector2chSmart was auto-generated by Vivado.
You need to:
- go to Tools -> Create and Package New IP...,
- click Next
- select the Createa new AXI peripheral option at the bottom
- name your IP in the next screen
which will get you to the following screen
where you can define which interfaces you want to include. If you select the Edit IP option on the next screen, another instance of Vivado will open a project where you can edit the VHDL code and define your own components while staying within the framework of the AXI protocol. A tutorial that describes creating a custom AXI streaming IP is available here, and more tutorials on the subject can be found online.
5.2. If you are struggling with understanding or defining your custom IP but want to move forward and test other components of your project (the subject of the rest of the tutorials in this series), I suggest using XADC Wizard, an IP block available with Vivado. Here is a picture of the block after I disabled all alarms and enabled AXI streaming.
As you can see, the two interfaces we've been working on are available here out of the box. This IP is useful for many things, but for our purposes it can stream the readings of the chip temperature. There is an excellent tutorial that fully describes both hardware and software aspects of using XADC.
5.3. Finally, if you really want to understand the protocol, there is no substitute for reading the manual (AMBA AXI Protocol Specifications).
6. Packaging the smart streaming fast counterAt the end of the previous tutorial (part 1) I left you with three VHDL files that describe our hardware: detector, iatcounter, and iatcollector2ch. Since the start of this tutorial we've added two more VHDL files: iatcollector2ch_axis and iatcollector2chSmart_axis. At this point, you should have the following Design Sources (I've disabled all testbench files)
It is now time to package our IP so that we can easily reuse it as a standalone block.
First, decide where you are going to keep your custom IPs and create a folder named ip_repo and another folder named iatcollector2chSmart_axis inside it.
Then, select Tools > Create and Package New IPs... in Vivado; click Next.
We are going to package our current project:
Select the folder you created in the previous step, confirm your choice, then click Finish.
A new instance of Vivado will open with a window that controls various aspects of your IP. We are going to accept all these autogenerated settings, click on Review and Package, and then the "Re-Package IP" button; confirm that you want to close the project.
Finally, we need to make sure that Vivado is going to be able to find our IP in the future. Go to Tools > Settings... and under Tool Settings > IP Defaults category add the path to your ip_repo folder
If you now restart Vivado and create a new blank project, you'll be able to add our IP to the block design. Using this design will be the subject of our next tutorial.
This IP is the crowning achievement of this tutorial but unfortunately we are not quite done yet. There are two more (much simpler) IPs that we are going to design and package next. Both are going to make our life much simpler in the later stages of the project.
8. Dealing with metastabilityOur ultimate goal is to detect external pulses which are not synchronized to the FPGA clock. In such cases metastability can cause our device to malfunction and drop (fail to detect) a legitimate pulse. Fortunately, this failure is easily preventable by placing a cascade of 2 or 3 flip-flops before feeding the signal to the counter. So we are going to define and package a component with 3 flip-flops (as usual, the code can be found below). You can follow the workflow established earlier in this tutorial by creating a new project and packaging it into your own IP:
Once again, this component will delay an input pulse but will not have any effect on the performance of our inter-arrival counter.
9. Event simulatorWhile developing this project, I found it extremely useful to have a source of high quality 100% predictable pulses. Although not mimicking every aspect of real life signals, events can be easily generated by another IP implemented in the FPGA. I called my event simulator eventSimSmart, it can be controlled by writing to registers via S_AXI_Lite protocol. The simulator produces a series of output pulses separated by time intervals starting at 2 and incremented by 1 up to max_value. For example, if max_value = 6, the simulator will produce the following series of time delays: 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, 3,... where each number is the number of clock periods between simulated pulses. Here is a section of the testbench result and the final IP block.
In this tutorial we have created three IPs for future incorporation into a block design. One is a streaming IP for recording inter-arrival times between events, the other two are auxiliary and will make our final design more robust and easier to test.
11. Full list of tutorials in this series1. Pulse counter implemented in FPGA: hardware (VHDL) design
2. Pulse counter streaming using AXI interface and packaging the counter as a custom IP.
3. Full hardware design of the streaming pulse counter on Zynq.
4. Setting up DMA transfers to receive data from the streaming pulse counter.
5. USB2 bulk transfers and interrupts for high data transfer rates.
6. Working event counter with USB2 transfers and communications.
7. External (PC) testing software for receiving data from the counter.
Comments
Please log in or sign up to comment.