First project

This project haven't been tested in the hardware yet, so it could still had some bugs. Keep that in mind !

There are tons of starting tutorials for zynq based platforms ( for the red pitaya I personally recommend this, also this youtube videos are awesome). We are going only to take a quick view of how use the vivado platform.

To start we clone the following repository:

git clone https://github.com/sebajor/redpitaya_base

It just contain a base model of the red pitaya with it correspondent constraint file. To create the project you have open Vivado and at the bottom of the window you should see a console prompt. Move into the cloned repository and issue:

source make_project.tcl

This should create the project under the tmp folder.

The script that you just run is a tcl based script, which are a nice feature of vivado, when you make a change in the project in the TCL console appears the correspondent tcl command so you could join all the commands and create a file to be able to replicate the model just sourcing the script. This also opens the possibility of not working with the gui at all, because things like configure Xilinx IP could be done by tcl commands. (If you are curious of how do not use the gui you should check also the non-project mode of Vivado).


The base project we are using is quite simple. Consists in the PS zynq block, the ADC interface, the DAC connected to an Digital Synthesizer who is controlled by a 2 port GPIO block which is connected as slave of one AXI interconnect where the master is the zynq PS.

You should have something similar to this diagram after running the tcl command.

We are going to take a little review of the important blocks.

Zynq PS:

It mask the PS side, I you click into the block it pop up a large configuration block, which shows some interfaces of the PS. There is a lot you could do here, but for time sake we only are going to review two little things.

Zynq PS dialog block

Click into the PS-PL configuration. In this tab again you have a lot of configurations, but we typically are only interested in four options, the GP Master AXI interface ,GP Slave AXI interface, HP Slave AXI interface and the ACP Slave interface which are ports that interface directly with the PL.

By construction our base model uses the GP0 port to be a master of the AXI bus. The GP, HP and ACP slaves are used to make transfers from the PL to the PS. In this tutorial we are not going to use them, so we should deactivate the HP0 port which is enable by default in our base project.

Click into HP slave tab and un-mark the S AXI HP0 interface option.

The other usual feature you have to look at is the clock configuration. If you look at the base project diagram you would found that there is one FCLK port this is a clock generated by the PS and in our model it feeds the AXI Interconnect and other blocks.

The PS is able to generate different clocks to feed logic inside the FPGA, this is really handy if you need different clock domains. For our case we only use one clock running at 125MHz.

Now click into Ok and the layout of the PS block should get modify to get rid of the HP port. Note that there is a new command in the TCL console if you want to add it to the base project you have to copy it in the bottom of the make_project.tcl and the next time you sourcing it, the model created is going to be in this stage. So is a nice way to keep track of the version of your project (in the case you break it you could return into a stage that it works).


ADCs:

A nice feature of the vivado enviroment is that enables you to utilize your own HDL, generate a package and import into the diagram. Indeed the ADCs are a Pavel Demin Verilog code that you could found in here , we use a copy of some of his verilog codes that are located in the cores folder.

The ADC block is straight forward, first he uses a IBUFGDS interface, which is only a Input Buffer for the clock LVDS signal, then takes a sample every rising edge of the clock and put the equivalent 2-complement signals in the output.

If you look carefully at the end of the code Pavel decides to utilize the AXI stream interface for his core which later is translate in a special type of packaging in the diagram (The M_AXIS in the previous figure). He also override the handshake of the AXI stream hardcoding a 1 in the tvalid signal, is not fancy but it works.

See also that both ADCs input data are tied together in the 32 bit tdata signal, so if you want to work with it you have to keep that in mind.

//A scratch of the Pavel Demin code

IBUFGDS adc_clk_inst0 (.I(adc_clk_p), .IB(adc_clk_n), .O(int_clk0));
  BUFG adc_clk_inst (.I(int_clk0), .O(int_clk));

  always @(posedge int_clk)
  begin
    int_dat_a_reg <= adc_dat_a;
    int_dat_b_reg <= adc_dat_b;
  end

  assign adc_clk = int_clk;

  assign adc_csn = 1'b1;

  assign m_axis_tvalid = 1'b1;

  assign m_axis_tdata = {{(PADDING_WIDTH+1){int_dat_b_reg[ADC_DATA_WIDTH-1]}}, ~int_dat_b_reg[ADC_DATA_WIDTH-2:0],{(PADDING_WIDTH+1){int_dat_a_reg[ADC_DATA_WIDTH-1]}}, ~int_dat_a_reg[ADC_DATA_WIDTH-2:0]};

The important lesson about this is that if you don't like any feature of the blocks you are not compel to use them, you always could write your own, also its is not necessary to use the gui interface you could code everything in HDL and just use Vivado to compile.

Like Xilinx uses the AXI interface you could also look for HDL code, make an AXI wrapper and you should be able to package and import into the Vivado environment. That's really nice!

DAC subsystem

DAC system:

Starting from the left the blocks that compose the DAC subsystem are:

  • AXI GPIO: This is an slave of the AXI interconnect, so is controlled by the PS. You could write into the address of the GPIO and then you are going to set a value into the gpio2_io_o port which controls the frequency of the DDS.
  • AXI constant: Other of the Pavel cores, this is just a synchronizer between two clock domains. The AXI GPIO is running using the PS clock and we would like to have the DAC locked at the same clock of the ADCs so we have to cross a clock domain to deliver the PS information to the DDS. Like the FCLK is running at 125MHz and the ADC crystal also runs at that frequency is an easy crossing, if you want more info read the verilog code.
  • DDS compiler: Well, it generate a sine and a cosine that are encoded in the AXI stream output port. Is a typical xilinx block you could find the documentation here.
  • Clock Wizard: Like we want to utilize the DAC to generate signals in the range [0-125]MHz we have to clock the DAC at 250MHz (Nyquist Theorem). So we use this Xilinx block to generate a 250MHz clock signal using as input the ADC clock. If you are wondering how a FPGA achieve this task, I only going to say that the working basis is the Phase Look Loop (PLL).
  • Red Pitaya DAC: Another Pavel's block. Kudos for he and his work!


Modifying the base project

If you look carefully the ADCs is not connected, so we are going to add some components to storage the data in a BRAM.

In the flow navigator you could press IP catalog this is going to open a list of the available IP, we want the block memory generator IP, search for it in the browser, double click on it and press add IP to the design.

Now we want to add the AXI BRAM controller IP, again we search for it in the IP catalog tab or use the shortcut ctrl+i to search and add it.

The AXI BRAM controller translate the request that comes in AXI to the block memory interface, so we can connect it to the PS and send request to the BRAM.


When you add the AXI BRAM controller, a green message should appears at the top of the design diagram. This is a wizard who helps to make the connections. Press Run Connection automation and it should deploys the following message:

Each of those options are ports that the system recognize as "connectable". For example it recognize that the AXI BRAM controller has an AXI slave interface so the auto connection shows all the AXI masters in the diagram (in our case the PS).

The BRAM A port and BRAM B port gets mapped into the block memory IP and the GPIO is mapped to one output port as the possible connections.

Its your job as designer to decide which of this connection has to be made. We select only the S_AXI in the AXI BRAM controller and press OK.

Again the diagram get modified, in specific there is a new port in the AXI interconnect.


Open the AXI bram and set the number of interfaces to 1 (we only need one port to make read and write transactions).

Now open the memory generator. In the Basic page, change the Mode to Standalone, also mark the option generate interface with 32 bit address, finally set the memory type as True dual port RAM. Then move to the Port A options page and unmark all the optional output registers, do the same in the for the port B option page.

Finally press Ok.

This configuration enables you to have a shared memory between the two ports with different clocks, also you could set different sizes in the port A and B.



Now we make the connections. Press the "Port A +" button in the mask of the AXI BRAM controller and also press the "PORT B +" off the block memory generator and connect each pin with his pair (When I tried to compile connecting only the top mask, Vivado throws me an error).


There is a tweak we have to present. The AXI BRAM controller uses byte address, when you set the memory generator to work with 32 bit interface it also gets configured to use byte address instead of word addressing.

So, to save the data in the bram using the Port A of the BRAM we need to generate that type of addressing. For the 32 bit or 4 bytes of data, we have to increase the address in 4 each cycle. You could do it in different ways, for example you could use the Xilinx Counter IP and set the increment in 4, count normally and then shift the values by two, etc.

AXI BRAM controller and block memory connection

We are going to take a different direction. We are going to create our own HDL wrapper and inserting it in the design. Before start to code, it is worth to mention that the parameter defined in the HDL instantiation gets mapped into the masks of the blocks. So for example options like the Mode and Memory type that we set in the memory generator are parameters in the HDL.

To write the HDL we go to add sources, select the add or create design sources click next, now click create file, we choose to code in verilog and set the name into "bram_intf.v", click ok and finish. There is going to appear a box asking for the port information, just ignore and press ok.

Now you should have a new file in the Design sources panel, press it and you are ready to start to code.

Here we provided a sample code, so you could just take it and paste (under your risk) it into the bram_intf.v file.

Note that the we use three parameters: word_addr, word_size and freeze. The address and size are obvious, freeze just tells if the counter is a free running one or it should stops when reaching the final address. Note also that we use the generate statement in order to define if we should create some logic or not depending on the user parameters.

The idea is to generate your own HDL scripts and save it in order to have them available when you need it, so try to make your code in the most general way that you could imagine.

module bram_intf #(
    parameter word_addr = 4096,
    parameter word_size = 64,
    parameter freeze = 0
)

    (
    input clk,
    input arst,     //active high rst!
    input [word_size-1:0] indata_tdata,
    input indata_tvalid,
    output indata_tready,
    
    output [31:0] addr,
    output [word_size-1:0] dout,
    output en,
    output rst, 
    output [(word_size/8)-1:0] we
    );
    reg rst_sys;
    reg rst_fifo = 2'h0;
    always@(posedge clk or posedge arst)begin
            if(arst)
                {rst_sys, rst_fifo} <= {rst_fifo, 1'b1};
            else
                {rst_sys, rst_fifo} <= 3'h0;
    end
    
    reg [$clog2(word_addr)-1:0] addr_counter;
    reg [(word_size/8)-1:0] we_r;
    reg [word_size-1:0] dout_r;
    
    generate    
    if(freeze==1)begin    
            always@(posedge clk or posedge rst_sys)begin
                if(rst_sys) begin
                    addr_counter <= 0;
                    we_r <= {(word_size/8){1'b0}};
                    dout_r <= indata_tdata;
                end
                else begin
                    dout_r <= indata_tdata;
                    if(indata_tvalid) begin
                        if(addr_counter == word_addr-1)begin
                            we_r <= 0;
                            addr_counter <=addr_counter;
                        end
                        else begin
                            we_r <= {(word_size/8){1'b1}};
                            addr_counter <= addr_counter +1;
                        end
                    end
                    else begin
                        we_r<=0;
                        addr_counter <=addr_counter;
                    end
                end
            end
    end
        else begin   
            always@(posedge clk or posedge rst_sys)begin
                if(rst_sys) begin
                    addr_counter <= 0;
                    we_r <= {(word_size/8){1'b0}};
                    dout_r <= indata_tdata;
                end
                else begin
                    dout_r <= indata_tdata;
                    if(indata_tvalid) begin
                        we_r <= {(word_size/8){1'b1}};
                        addr_counter <= addr_counter +1;
                    end
                    else begin
                        we_r <= 0;
                        addr_counter <= addr_counter;
                    end
                end
            end
        end
   
    endgenerate
    localparam aux = $clog2(word_size/8-1); 
    localparam aux2 = 31-$clog2(word_addr)+aux;

    assign addr[31:$clog2(word_addr)+aux] = {aux2{1'b0}};
    assign addr[aux-1:0] = {aux{1'b0}};
    assign en = 1'b1;
    assign rst = 1'b0;
    assign indata_tready = 1'b1;
    assign addr[$clog2(word_addr)-1+aux:aux] = addr_counter;
    assign dout = dout_r;
    assign we = we_r;
    
endmodule


Now return to the design source panel where all the HDL is listed, and right click into the bram_intf.v. Among other options you should see Add module to block design, press it and voila you have your HDL in the diagram.

Move to the design diagram tab and you are going to note that in some place appears an unconnected RTL block, this is our wrapper. Note that the indata port is shown in a reduce form, thats because when we made the code we use the AXI stream convention so the Vivado system recognize those signals and mask them in a special way.

Now click over the bram_intf block and a configuration window should appear. Change the word_size to 32 and the word_addr to 8192 (which is the default in the AXI controller and memory generator).

We are almost ready, we have to make the connections between the ADC to the wrapper and to the PortA of the bram, taking care that this part of the logic is working at the ADC clock. Other thing to look at is that the wrapper is active high reset and the usual in the Vivado environment is low active, to take care of the reset we are going to use the port 1 of the AXI GPIO. For that we press "ctrl+i" and search the IP slice to just use the least significant bit of the gpio_io_o port.


The connections should look similar to the next image:

The final step we need to make is to look at the addresses of the AXI slaves. If we summarize what we have done until now:

  • We have the ADCs connected to a shared BRAM where the other port is connected to the GP0 port of the PS.
  • We have the DAC connected to a DDS which its frequency is controlled by the PS using a AXI GPIO.

So we need to know how to talk to those devices and check if the memories range are right and annotate them because we are going to use them when we program the Software side (Its easier than the Vivado part I promise).

There should be a tab named Address editor where the tool saves the address for each device. In this case the GPIO has his starting address at 0x4200 000, remembering that the gpio_io_o is connected to the reset of the bram_intf if we want to make a reset we have to issue something like:

mwr 0x42000000 0x1

Where mwr is the command to write a value into an address (won't be afraid we are going to code in C not in assembly).

To write the gpio2_io_o which is connected to the DDS we have to write the address 0x42000008 (check the AXI GPIO datasheet).

To read the BRAM address we could issue:

mrd 0x40000000 8192

Where mrd is the command to read the data, then we add the direction and finally quantity of reads that we want to make.

Compile!

Finally we are here!

First go to the diagram tab, in the top of the window should be a option with a check inside of it who says validate design (optionally you could press f6). If you design doesnt had errors you could run the synthesis .

When you are running the synthesis should appear some warnings about port mismatch an a lot of warnings. Some time ago I read the following statement and couldn't be more true: "If everything is warning nothing its", as a rule of thumbs I only look at the critical warnings and the errors but is up to you to check the reasons of those warnings.


When the synthesis is ready you could make several things, look at the reports, set physical and timing constraints, look the schematics, set the debug signals if you want to debug in hardware using the ILA and the JTAG.

We choose only to run the implementation and cross fingers to everything works.

After a tons of warnings the implementation should be ready. Now is a good practice to look if the timing constrains are met. For that we Open the implemented design and this should deploys the timing summary, mine looks like this:

The timing constrain are met. So we are happy with that (even the tool says that there are some warnings).

We now could generate the bitstream, pressing the option Generate Bitstream. Again we wait until it finish.

The last step in the Vivado interface is to export the Hardware, for that you have to go to the top of the software window and in the tab "File" go to "export" and press "export hardware".

And now we are ready to start with the Software part! Go to File and press Launch SDK.



Summary

Now we finish with the FPGA side is good to summarize what we have done:

  • Run a tcl script which give us a basic diagram connection. After every modification to the project the TCL command give us the correspondent tcl command so we could store them.
  • We take a quick view of the PS configuration, in specific to the PL-PS port interface and clocks that the PS could generate which are the typical modification we use.
  • The base model consist in two ADCs with AXI interface running at a clock feed by an external crystal, 2 DACs connected to a DDS controlled by an AXI GPIO which is controlled by the PS.
  • Use the IP catalog to import a block memory generator and AXI BRAM controller to been able to read it from the PS. We configure the BRAM as a dual RAM in standalone mode.
  • Create a HDL wrapper for the ADC-BRAM interface and put it in the design gui.


Xilinx SDK

Now is time to tell the truth... I lied, we are going to code everything in assembly Mwuahahaha

Not really.. I havent test the model in hardware yet so I dont have the codes for the software part, so it seems this tutorial is going to have a second part. But everything is in C I promise.

If you need the software part, a good starting point are the Anton Potočnik tutorials. Basically to upload the bitstream you have to scp the bit file to the red pitaya enviroment and then you could program the fpga issuing: "cat bitfile.bit> /dev/xdevcfg".

When the fpga is ready the redpitaya default OS has the command monitor which allows you to read and write to the AXI addresses.

If you dont want to use that, is good to know that the AXI addresses are mapped into the "/dev/mem" file, so you could use the C command mmap to write and read from the AXI slaves.