ForewordIn the previous post we managed to boot the C64 system on the Zybo FPGA.
In this post we will explore how to interface the FPGA with the SDRAM on the Zybo board.
Having this knowledge will help us in future posts getting around the limitations of limited Block RAMS on the FPGA when doing tasks like resizing video frames to fit on an LCD screen.
An overview of AMBA/AXIYou might recall the following diagram from my Introduction Post:
This diagram summarises the components on the ZYNQ chip.
If you have a close look at the above diagram, you will see that the FPGA block (indicated in yellow) doesn't have a direct connection to the DDR controller. Should your FPGA design wish to transact with SDRAM, it will need to do so via AMBA Interconnect.
One might wonder why the AMBA Interconnect step is necessary. The answer is that there is a couple of peripherals on the ZYNQ (like USB and Gigabit Ethernet). Each of these peripherals can potentially compete for memory access.
This is where AMBA Interconnect comes in. AMBA Interconnect manages multiple memory accesses in an effecient and fair way.
AMBA Interconnect receives memory requests via AXI Ports. An AXI Port consists of an AXI Master and a AXI Slave. The AXI Master initiates a memory transaction (like a memory read or write request). The AXI slave is responsible for fulfulling the AXI Master request.
The finer detail of AXI Master and slaves will become clear during the course of this post.
Using the AXI Master Burst CoreCreating an AXI Master-capable IP can be quite a daunting task, especially if it is your first take on it.
To simplify the task somewhat Vivado provides the AXI Master Burst Core. Below is a diagram of this IP Block:
IPIC is the acronym for Intellectual Property InteConnect and is a Xilinx Standard. Compared to the AXI Master protocol IPIC is a simplified protocol.
Let us now have a look at how a typical IPIC communication session looks like between Use Logic and the AXI Master Burst Block. There is some nice timing diagrams in the Documentation illustration a couple of scenarios. Here is the timing diagrams for a burst write operation:
The wite operation is started by issuing a command from the User logic on the IPIC bus. There are four important signals that together forms the command.
Firstly, because this is a write, the mstwr_req line gets asserted.
Next, as part of the command mst_type also gets asserted. According to the documentation, the meaning of an asserted msty_type line is Fixed length burst. A pulled down mst_type line has the meaning of Single Data Beat. With our command we want to send multiple Data Beats, so asserting mst_type line is the way to go.
The other two signals for our command is mst_addr, indicating the destination address in memory, as well as mst_length. It is important to note that the size of mst_length is in bytes, although we are sending 32 bits of data at a time.
As soon as our AXI master burst block have accepted our command, it signals us via mst_cmdack.
Once we received above confirmation we need to keep an eye on the signal MstWr_dst_rdy_n. At each clock cycle, if this signal is low, we need to give the next piece of data via MstWr_d.
Another rdy signal we should be aware of is MstWr_src_rdy_n (aka source ready). Should we be in situation where we don't have the next piece of data available yet at the transition to the next clock cycle, we should ensure that the source ready signal is set to high.
Two final signals worth noting is MstWr_sof_n and MstWr_eof_n. The first signal, start of frame, you will pull low when you are at the point of sending your first piece of data. You are about to send your last piece of data, you will pull the MstWr_eof_n (aka end of frame) low.
Developing the user logicWith the Burst IP block discussed in the previous section, it would be nice to write some Test Verilog code and see if we can populate the SDRAM on the ZYBO board.
In this section we will start writing the IPIC interface code for the user logic and do the full block design in the next post.
I will start off by showing the full User Logic Verilog solution for the IPIC interface:
module burst_block( input wire clk, input wire reset, input wire write, input wire [31:0] write_data, output wire [31:0] ip2bus_mst_addr, output wire [11:0] ip2bus_mst_length, output wire [31:0] ip2bus_mstwr_d, output wire [4:0] ip2bus_inputs, input wire [5:0] ip2bus_otputs ); wire master_write_dst_rdy; //change to axi name wire cmd_ack; // change to axi name wire mstwrite_req; wire mst_type; reg [31:0] axi_start_address; wire [11:0] burst_len; wire [31:0] axi_d_out; //output rem wire sof; wire eof; reg master_write_src_rdy; reg [12:0] bytes_to_send; reg [12:0] count_in_buf; reg [3:0] state; wire read; reg [12:0] axi_data_inc; wire neg_clk; parameter IDLE = 4'h0, INIT_CMD = 4'h1, START = 4'h2, ACT = 4'h3, TRANSMITTING = 4'h4; parameter BURST_THRES = 5; assign burst_len = 12'h14; assign neg_clk = ~clk; fifo #( .DATA_WIDTH(32) ) data_buf ( .clk(clk), .reset(!reset), .read(read), .write(write), .write_data(write_data), .empty(), .full(), .read_data(axi_d_out) ); assign read = (state >= START & !master_write_dst_rdy) ? 1 : 0; always @(posedge clk) if (!reset) count_in_buf <= 0; else if (!read & write) count_in_buf <= count_in_buf + 1; else if (read & !write) count_in_buf <= count_in_buf - 1; always @(posedge clk) if (!reset) state <= 0; else case( state ) IDLE: if (count_in_buf > BURST_THRES) state <= INIT_CMD; INIT_CMD: state <= START; START: if (cmd_ack) state <= ACT; ACT: if (!master_write_dst_rdy) state <= TRANSMITTING; TRANSMITTING: if (!master_write_dst_rdy & bytes_to_send == 1) state <= IDLE; endcase always @(negedge clk) if (!reset) begin axi_start_address <= 0; axi_data_inc <= 0; end else if (state == INIT_CMD) begin axi_start_address <= axi_start_address + axi_data_inc; axi_data_inc <= BURST_THRES; end assign mstwrite_req = (state == START) ? 1 : 0; assign mst_type = (state == START) ? 1 : 0; assign sof = (state == START) | (state == ACT) ? 0 : 1; assign eof = (bytes_to_send == 1 & !master_write_dst_rdy) ? 0 : 1; always @* if (state == START) master_write_src_rdy = 0; else if (state > START & bytes_to_send != 0) master_write_src_rdy = 0; else master_write_src_rdy = 1; always @(posedge clk) if (state == START) bytes_to_send <= BURST_THRES; else if ((state > START) & !master_write_dst_rdy & bytes_to_send != 0) bytes_to_send <= bytes_to_send - 1; assign master_write_dst_rdy = ip2bus_otputs; assign cmd_ack = ip2bus_otputs; assign ip2bus_inputs = mstwrite_req; assign ip2bus_inputs = mst_type; assign ip2bus_mst_addr = axi_start_address; assign ip2bus_mst_length = 20; assign ip2bus_mstwr_d = axi_d_out; assign ip2bus_inputs = sof; assign ip2bus_inputs = eof; assign ip2bus_inputs = master_write_src_rdy; endmodule
Let us start by discussing the parameters of the module.
All the single bit signals of the IPIC interface i have combined into two wires: ip2bus_inputs and ip2bus_outputs. This will just make the creation of the block diagram easier by not needing to connect so many wires.
The other module parameters starting with ip2bus will look familiar from the previous section.
The other module parameters of interest is write and write_data. These inputs will pump our module with data that needs to be send to SDRAM. If, during a clock pulse, you have new data on write_data, you need to ensure that write is high.
For test purposes you can just create logic that that send numbers 0 - 5 on write_data.
In later posts we will be writing the output of a VIC-II implementation to write_data.
The data that we pump into our module gets stored temporary in a FIFO (First In First Out) Buffer. There is a number of FIFO implementations that you can use on the Net. The FIFO implementation I used was the following: https://embeddedthoughts.com/2016/07/13/fifo-buffer-using-block-ram-on-a-xilinx-spartan-3-fpga/
One issue with about all FIFO implementations is that it can't tell you how far it is from been full. This kind of information can aid in knowing when to start processing the data in the buffer to avoid a buffer overflow condition. We need to keep track of this count ourselves by keeping track of when we insert a item or remove one from the buffer. The following snippet taken from the complete module code above is responsible for keeping track of the FIFO fill level:
... always @(posedge clk) if (!reset) count_in_buf <= 0; else if (!read & write) count_in_buf <= count_in_buf + 1; else if (read & !write) count_in_buf <= count_in_buf - 1; ...
For any given clock cycle if there was a write and no read, our fill level will be one more. If there was a read and no write our fill level will be one less. The fill level will remain unchanged if there was both a read and a write.
You might have also noticed that I maintain a state machine within the module. We start off in the IDLE state and as soon as count_in_buf reached a threshold we transition to subsequent states for sending the data over the IPIC interface.
This conclude this section for developing the IPIC interface.
In SummaryIn this post we explored how to interface our FPGA to the SDRAM on the ZYBO board. We ended with developing the IPIC interface that will interface to the AXI Master Burst IP.
In the next post we will continue with the full block design.
Till next time!