Foreword
In the previous post we managed to get the Source code of the Mini Amiga project to compile in Vivado and ran the resulting bitstream on the Basys 3 board.
With the core on the Basys 3, we did a very basic test, confirming whether the address requests to external SDRAM was targeted for the Kickstart ROM area.
Our next goal is to get the Kickstart ROM to run.
You might have noticed towards the end of the previous post that I am alternating between the term Kickstart ROM and AROS ROM. Many readers are more familiar with the term Kickstart ROM, whereas we are going to run the AROS ROM image in our design. So, just keep that in mind when I interchange between the two terms.
The Kickstart ROM is 512KB in size, more than the amount of Block RAM the Basys 3 can offer. We will therefore move in this post to the Zybo board, which supports external SDRAM.
Our primary focus in this post will be to develop an interface where, given an address, the interface will one word of data from that location in SDRAM.
Overview
Quite a while ago, while I was still developing a C64 block for an FPGA, I have also created a wrapper for accessing SDRAM on the Zybo board, here.
The purpose was to save the frames produced by the the VIC-II module and render it to a VGA screen at a different frame rate.
In the design I used the IP Burst block, provided by Xilinx, to shield me from the details of the AXI protocol.
The design I ended up with, was a streaming interface, that accessed data in a serial fashion. This is a suitable interface for rendering frames to a VGA screen, where the data required is also of a serial nature.
With the streaming interface we can easily predict which data will be required in the future and therefore prefetch the data, therefore mitigate the effect of latency.
In our Amiga design, however, the 68000 will be accessing the SDRAM in a much more random fashion, so a streaming interface will not give us any real benefit. So, in this post we will be designing a very simple SDRAM interface, where we provide an address, and the interface will return the relevant word of data from SDRAM.
Obviously, with this interface SDRAM latency will work against us, but for coming posts we will first try to get the system to work and attempt to fix the latency issues later on.
Creating an AXI Block
In a previous post, here, I explained how to create an AXI block in a Zybo block design.
In that post, I have also explained how to utilise IP Burst Block in your created AXI block. As mentioned earlier, the IP Burst block shields you from the technicalities of the AXI protocol.
Well, after a couple of years down the line, I tend to disagree with the statement in the previous paragraph. Working with AXI protocols is not that bad and using the IP Burst block is a bit of an overkill.
The thing is, when creating an AXI master block, the wizard do create a template for you that is basically an example of how to use the AXI protocol. A couple of years ago, however, this template just looked like a very complex state machine, and so I decided to use the IP Burst block as an alternative.
Having a look at the state machine that the AXI Block Wizard create, one can see that it is basically a memory tester. It starts off writing a certain test pattern of data to memory and afterwards read it back from memory, checking to see if it matches the test pattern.
It is easy enough to alter the path of this state machine to read or write on demand. We will tackle this in the next section.
Modifying the AXI block
Let us modify the AXI block to fit our needs.
First we need to look at the state machine that is implemented in case-statements. The state machine transitions as follows: IDLE > WRITE > READ > COMPARE > IDLE.
In order to implement our read on demand, we need to transition back to IDLE after a read or a write. Also in the IDLE state we need to directly transition directly to a READ or a WRITE given a command. We do this as follows:
if ( init_txn_pulse == 1'b1) begin mst_exec_state <= user_write ? INIT_WRITE : INIT_READ; ERROR <= 1'b0; compare_done <= 1'b0; end else begin mst_exec_state <= IDLE; endHere user_write is an input port we define on the module. If it is a 1, it is a write and a read otherwise.
... // Users to add ports here input [31:0] user_address, input [31:0] user_data, input user_write, // User ports ends ...So, we have a port to specify an aaddress for a read/write command. Also, for a write, we have a port to specify the data.
// Next address after AWREADY indicates previous address acceptance always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) begin axi_awaddr <= user_address; end else if (M_AXI_AWREADY && axi_awvalid) begin axi_awaddr <= axi_awaddr/* + burst_size_bytes*/; end else axi_awaddr <= axi_awaddr; end ... /* Write Data Generator Data pattern is only a simple incrementing count from 0 for each burst */ always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) axi_wdata <= user_data; //else if (wnext && axi_wlast) // axi_wdata <= 'b0; else if (wnext) axi_wdata <= axi_wdata/* + 1*/; else axi_wdata <= axi_wdata; end ... // Next address after ARREADY indicates previous address acceptance always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) begin axi_araddr <= user_address; end else if (M_AXI_ARREADY && axi_arvalid) begin axi_araddr <= axi_araddr/* + burst_size_bytes*/; end else axi_araddr <= axi_araddr; end ...In this snippet of code, we have kept the original template code more or less intact. It is just the pieces in bold that we have changed.
... output reg [31:0] user_data_out, output reg user_data_ready, ... always @(posedge M_AXI_ACLK) begin if (wnext || rnext) begin user_data_ready <= 1; end else if(!INIT_AXI_TXN) begin user_data_ready <= 0; end end ... always @(posedge M_AXI_ACLK) begin if (rnext) begin user_data_out <= M_AXI_RDATA; end end ...Keep in mind that M_AXI_ACLK is clocking at 100MHZ, so for one clock cycle rnext/wnext might be high, and low at the next one. The Mini Amiga block is clocking considerably slower than 100MHz, and will miss these notifications if we rely directly on rnext/wnext. It is for that reason why we are storing the value for user_data_ready and user_data_out.
Testing the AXI block
- bit 10: Read/Write
- bit 9/8: Index for generating read data/address
- bit 7-0: Lower part of counter
always @(*) begin if (counter[9:8] == 0) begin data_out = 20; end else if (counter[9:8] == 1) begin data_out = 120; end else if (counter[9:8] == 2) begin data_out = 30; end else if (counter[9:8] == 3) begin data_out = 10; end else begin data_out = 111; end end always @(*) begin if (counter[9:8] == 0) begin address = 20; end else if (counter[9:8] == 1) begin address = 120; end else if (counter[9:8] == 2) begin address = 30; end else if (counter[9:8] == 3) begin address = 10; end else begin address = 111; end endWe use the lower part of the counter, bits 7 - 0, to decide when to trigger the init_txn pulse. We use such a big range to ensure that we leave enough gap for latency:
always @(posedge clk) begin if (counter[7:0] == 20) begin init_txn <= 1; end else if (counter[7:0] == 118) begin init_txn <= 0; end endThis is enough coding for a test. Let us do some testing.