Monday 14 November 2022

SD Card Access for a Arty A7: Part 2

Foreword

In the previous post we started a multi part series for interfacing the Arty A7 with an SD Card that will be used for non-volatile storage. This non-volatile storage is crucial for running an Amiga core on the Arty A7, storing the necessary disk images and ROMS. 

In the previous post we covered the hardware we will use for interfacing with an SD Card.

In this post we will continue our journey and continue with the software side of things. In particular we will be looking at an opensource core for interfacing with an SD Card and see if we can issue a basic command to an SD Card in simulation.

The SD Card interface by Dan Gisselquist

In this post we will be using the SD Card interface by Dan Gisselquist, here. This core does all of its communications with an SD Card by means of SPI (Serial Peripheral Interface), rather than the native SD Card interface. The SPI interface is slower than the native interface, because the native interface sends/receives four bits of data at a time, whereas SPI only deals with one bit at a time. However, the speed of SPI will be sufficient for our purposes.

Within the FPGA, this core is accessed via a Wishbone bus. A Wishbone bus is similar in function to an AXI bus you find in ARM devices. We will touch a bit on the technical of the Wishbone bus a bit later in this post.

There is one final feature of the Gisselquist core source code which I think is very cool. This is the fact that the source code contains a Test Bed which can simulate responses from an SD Card. Very useful, since the SD Card itself is a very complex state machine, and thus if we don't need to worry about this it makes life so much simpler.

The Test Bed for simulating SD Card responses is written in C++. To get this Test Bed to work is quite an exercise, needing to install a number of dependencies, working with Verilator.

I will deviate a bit from the steps on how the Test Bed will be used. For starters, I will be using Vivado for simulation instead of Verilator. To be frank, I haven't used an C++ module in simulation with Vivado before, but I have found a nice tutorial for this on the Internet, which I will share in the next section.

Using C++ in an Vivado simulation

As mentioned in the previous section, I have never worked with C++ code in a Vivado Simulation before.

So, is it even possible? It turns out that it is indeed possible, if one have a look at this write up by Adam Taylor. The instructions Taylor gives is using Vivado in Windows. However, it is not so difficult to adapt so it can be used in Linux, which I will be using.

Taylor also mentions that Vivado provides a counter example for showing how to interface Vivado with C++. From this example I will just point out a couple of important operations one needs to use with C++ in a Vivado simuation.

The first operation is get_port_number. Here is an example how this function is used:

        int i_clk = Xsi_Instance.get_port_number("i_clk");
This is typically the first step if you want to operate on a port of the top module. The number returned is like a handle that you will use to operate on that port like setting a value or reading a value.

Let us see how one would set the value on a port, using the i_clk port again as the example:

    const s_xsi_vlog_logicval logic_val  = {0X00000001, 0X00000000};
    Xsi_Instance.put_value(i_clk, &logic_val);
This will set the value on the i_clk port to one. There is quite a bit going on in this code snippet, but basically one needs to send a two valued structure to the function put_value. At first, these two valued structures can be very confusing, but luckily Vivado does provide us with some documentation within the comments of their code for this structure:


So, basically with the two integers together, we have a two bit value for every bit position. This kind of setup is necessary because apart from 0's and 1's, we also need to cater for X's (Unknowns) and Z's (High impendence).

However, in our case we are not really interested in X's and Z's, so it would be sufficient just to set the value's of 1's and 0's we want in the first integer, and just leave the second integer zero.

If we want to read a value from a port, instead of writing, we will use get_value:

    s_xsi_vlog_logicval count_val = {0X00000000, 0X00000000};
    Xsi_Instance.get_value(port, &count_val);
Again the rules of two valued structures is applicable.

One important thing to do when using C++ code with Vivado, is that you need to advance in time in order to do anything useful. To advance in time, you need to call run(), like this:

        Xsi_Instance.run(10);
The parameter needs to be in time precision units. In Verilog, time precision is typically defined with a timescale directive in the beginning of a Verilog file, like this:

`timescale 1ns/1ps
In this case, one time precision unit would be 1ps. Calling the run() method with a value of 10 in this scenario would thus advance time by 10ps.

Let us wrap up this section by looking at a concrete c++ example, generating a 100MHz clock signal:

const s_xsi_vlog_logicval one_val  = {0X00000001, 0X00000000};
const s_xsi_vlog_logicval zero_val = {0X00000000, 0X00000000};
int i_clk = Xsi_Instance.get_port_number("i_clk");

while (1) {
    Xsi_Instance.put_value(i_clk, &zero_val);
    Xsi_Instance.run(5000);
    Xsi_Instance.put_value(i_clk, &one_val);
    Xsi_Instance.run(5000);
}
This example starts by setting the i_clk port to zero, and waiting 5000 time precision units (e.g. 5000ps). This is 5ns and is a half 100MHz clock cycle. After waiting 5ns, the i_clk port is set to a one, waiting another 5ns, and then repeating the whole process.

Basics of using the Gisselquist Test Bench

Now, let us have a look at using the Gisselquist Test Bench. The key file we will use from this test bench is https://github.com/ZipCPU/sdspi/blob/master/bench/cpp/sdspisim.cpp.

This file simulates responses from an SD Card. In this file there is only one method we are interested in:

...
int	SDSPISIM::operator()(const int csn, const int sck, const int mosi) {
...
}
...
This method needs to be called repeatedly during a simulation, at least once per clock transition. The parameters to this method corresponds to a handful of output ports of the SDSPI main module:
  • o_cs_n
  • o_sck
  • o_mosi
So, each time this method is called, Xsi_Instance.get_value(port, &count_val) needs to be called for each of these ports, and passing all the values as parameters to operator(). Keep in mind count_val is a two valued structure, so will need to pass the first value of the structure.

You will notice that operator() also returns a value. This is the value for the i_miso port of the SDSPI main module.  When calling operator() you should also do a put_value() every time for the i_miso port.

Let us write a quick outline how we will use operator() during simulation:

...
int get_port_value(Xsi::Loader& xsi, int port) {
    s_xsi_vlog_logicval count_val = {0X00000000, 0X00000000};
    xsi.get_value(port, &count_val);
    return count_val.aVal;
}
...
void update_spsi(Xsi::Loader& xsi) {
    int o_cs_n = xsi.get_port_number("o_cs_n");
    int o_sck = xsi.get_port_number("o_sck");
    int o_mosi = xsi.get_port_number("o_mosi");
    int m_value = (*m_sdspi)(get_port_value(xsi, o_cs_n), get_port_value(xsi, o_sck), get_port_value(xsi, o_mosi));
    xsi.put_value(i_miso, m_value ? &one_val : &zero_val);
}
...

const s_xsi_vlog_logicval one_val  = {0X00000001, 0X00000000};
const s_xsi_vlog_logicval zero_val = {0X00000000, 0X00000000};
int i_clk = Xsi_Instance.get_port_number("i_clk");

while (1) {
    Xsi_Instance.put_value(i_clk, &zero_val);
    Xsi_Instance.run(5000);
    update_spsi(Xsi_Instance);
    Xsi_Instance.put_value(i_clk, &one_val);
    Xsi_Instance.run(5000);
    update_spsi(Xsi_Instance);
}
...
This concludes an high level overview of using the Test Bench. We still need to fill in the remaining details, like using the Wishbone bus and issuing commands to the SD controller. We will cover this in the remaining sections of this post.

Using the Wishbone bus

As mentioned earlier, we need to make use of the Wisbone bus to access the Sd Card core from Gisselquist.

Let us start by having a quick look at the signals involved in the wishbone bus:

		input	wire		i_wb_stb, i_wb_we,
		input	wire [AW-1:0]	i_wb_addr,
		input	wire [DW-1:0]	i_wb_data,
		output	wire		o_wb_stall,
		output	reg		o_wb_ack,
		output	reg [DW-1:0]	o_wb_data,
Here follows a discussion on the signals:
  • i_wb_stb: Signals a command to read/write. This signal needs to be de-asserted at the next clock cycle.
  • i_wb_we: Signals whether the command is a read or a write.
  • i_wb_addr; Address to read/write
  • i_wb_data & o_wb_data: data for writing or result of read
  • o_wb_stall: Indicate is the core is unable to accept a command at a point in time.
  • o_wb_ack: Indicates whether a command has completed.
At this point the question is, how do we address the SDCard module so that an SD Card comes to live? To answer this question, let us have a look at some of the protocols involved with an SD Card. The following links gives a nice overview of these protocols:

https://openlabpro.com/guide/interfacing-microcontrollers-with-sd-card/

The following diagram, which I also used from the above link, gives an example of a typical command issued to an SD Card:

Every command start with a command byte, followed by 4 bytes of arguments. So in total a command consists of 5 bytes. However, a wishbone bus can only work with 4 bytes of data at a time. The SD Card module deals with this via two separate addresses, e.g. address 0 and address 1:
  • Write the command byte to address 0
  • Write the four arguments bytes to address 1. This address is actually a data register that will store the argument bytes for later use.
Actually, when sending a command to the SD Card module, you should write the parts in the reverse order. That is, first write the argument bytes to address 1, the data register, and then write the command byte to address 0.

As soon as you have written the command byte to address 0, the SD Card module will start transmitting the whole command to the SD Card via the MOSI port, starting with the command byte, and following with the arguments stored in the data register.

Results of the simulation

Time to run a simulation with the Gisselquist core. The goal of the simulation is to give a simple command to the SC Card, which in this case is simulated by a test bench, and see if we get the appropriate response back.

Now, the Test Bench provided by Gisselquist generates a wdb waveform of the simulation, which can be viewed by GTKWave. However, should it be preferred to rather view the waveform in Vivado, Vivado can also open wdb files.

To open a wdb file in Vivado, from the flow menu item in the menu bar simply select Open Static Simulation. You can then browse for the wdb file in the file system and open it.

Let us look at some waveforms. First, here is where we issue a command:


The command is been output on the o_mosi line, which in this case is 0x40, which instructs the SD Card to go into idle mode.

If you have a look at the timescale, you will see that o_sck is clocking the SD Card at 10MHz. This is compared to the usual 400KHz which you should clock the SD Card at from startup. We will address this in the next post, when we will try to run the command on a real FPGA.

Next, let us look at the response from SD Card:


The response is provide in the i_miso signal. The value we get back, is 0x01, which means in idle state.

In Summary

In this post we gave a high level overview of the SD Card core by Dan Gisselquist. We also did a very simple simulation via the Test Bench that Dan Gisselquist provides.

While writing this post, I actually discovered that Dan Gisselquist maintains a blog where he discusses a variety of topics regarding FPGA development. If you are interested, head over to his blog at www.zipcpu.com.

In the next post we will be trying out the SD Card Command example on a real Arty A7.

Until next time!