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.
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:
s_xsi_vlog_logicval count_val = {0X00000000, 0X00000000}; Xsi_Instance.get_value(port, &count_val);Again the rules of two valued structures is applicable.
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/1psIn 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.
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
... 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
... 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
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.
https://openlabpro.com/guide/interfacing-microcontrollers-with-sd-card/
- 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.
No comments:
Post a Comment