RISC-V CPU & CUSTOM GPU ON AN FPGA PART 14 – SDCARD, BUTTONS/SWITCHES AND MODIFIED INTERRUPT SCHEME

BEFORE WE BEGIN

In this part we’ll talk about adding the SDCard, board button and switch input, and the modified interrupt scheme we’ll use to distinguish between devices.

But before we go further, and since the files are getting too large to post here, we need to grab them from a git repository. Please head over to https://github.com/ecilasun/nekoichiarticle and use the following command to pull the files for all parts of the series:

git clone https://github.com/ecilasun/nekoichiarticle.git

after creating and changing to a folder of your preference with sufficient free disk space, since this is where we’ll be working on all parts of the series from now on.

Buttons/Switches

So far we’ve only been listening to the UART port for input, lacking any sort of direct user interaction with the board. In this part we’ll add a very simple button/switch mechanism, disregarding any debouncing.

On the Arty7-100 board, we have 4 slider switches and 4 pushbuttons, and if you attach the SDCard PMOD, another switch for chip detect pin.

Button zero (the rightmost of the 4) is currently reserved for circuit reset. This is because the actual reset switch on the upper right hand corner also resets the UART, or the UART also invokes a reset when devices connect to it, which is not an ideal thing to occur when we want to attach to our running code with a debugger. The other 3 buttons are free for user input, alongside with the 4 slider switches.

First thing to note when listening to direct pin input tied to a pin is that you should not be pulling the pin input all the way into the device and reading directly from it. This will cause all sorts of drifts and may cause you to lose your timing closure, if there are no timing constraints set on the input pin.

To work around this, we’ll be using a slightly-pipelined design where we first assign the incoming pin to an internal register, and track state changes from the register instead of the pin itself.

In the devicerouter.sv file, you’ll find a pairing of a FIFO and a switch/button pin listener.

wire switchfull, switchempty;
logic [7:0] switchdatain;
wire [7:0] switchdataout;
wire switchvalid;
logic switchwe=1'b0;
logic switchre=1'b0;

switchfifo DeviceSwitshStates(
	// In
	.full(switchfull),
	.din(switchdatain),
	.wr_en(switchwe),
	.wr_clk(cpuclock),
	// Out
	.empty(switchempty),
	.dout(switchdataout),
	.rd_en(switchre),
	.rd_clk(cpuclock),
	.valid(switchvalid),
	// Clt
	.rst(reset_p) );

logic [7:0] prevswitchstate = 8'h00;
logic [7:0] newswitchstate = 8'h00;
wire [7:0] currentswitchstate = {spi_cd, buttons, switches};

always @(posedge cpuclock) begin
	if (reset_p) begin
		prevswitchstate <= currentswitchstate;
	end else begin

		switchwe <= 1'b0;

		// Pipelined action
		newswitchstate <= currentswitchstate;

		// Check if switch states have changed 
		if (newswitchstate != prevswitchstate) begin
			// Save previous state, and push switch state onto stack
			prevswitchstate <= newswitchstate;
			// Stash switch states into fifo
			switchwe <= 1'b1;
			switchdatain <= newswitchstate;
		end
	end
end

The always loop will initally start up the device with the current state of incoming pins. After that, for each clock, the external pin values are stashed into newswitchstate (remember: its value is available on the next clock), and on the next clock we check to see if newswitchstate is different from the one we remember from before. If they don’t match, we queue up one entry into our switch FIFO, which is an 8-bit FIFO with 1024 entries.

Software side can choose to either directly drain this FIFO by reading from the memory mapped switch address (currently set to 0x80000018) or wait for a hardware interrupt, and read at that point if the code doesn’t want to continuisly poll. Note that there’s a catch here: if software directly reads the switch value, the CPU will stall until there’s a switch activity, so the advised method is to use an interrupt handler for reading switches.

The grouping of the buttons/switches into the memory mapped byte at 0x80000018 is as follows:

 31   7       6     5     4     3    2    1    0
[...][SPI_CD][BTN2][BTN1][BTN0][SW3][SW2][SW1][SW0]

SPI_CD: SD Card inserted/removed state
BTN[0..2]: The three rightmost pushbuttons
SW[0..3]: The four slider switches

NekoIchi provides this and other devices’ memory mapped addresses in the utils.cpp file in the riscvtool repository.

Expanding machine external interrupts

Previously, we stored only a single status code denoting which source created a hardware interrupt (timer/software/external hardware). However, this is not enough information when we see the interrupt occur on hardware side, and we will need more information about the event.

There is one easy way with which we can improve this design. The current design of NekoIchi contains two sources for hardware interrupts: the UART and the SWITCHES mechanism mentioned above. We can start by assigning IDs to these devices:

DEVICE_UART 0x1
DEVICE_SWITCHES 0x2

High 16 bits of the mcause CSR that carries the interrupt reason has space in its upper 16 bits for user codes. If we were to OR in our device ID into these top 16 bits, we can get a combination of a ‘reason’ code, and a device that caused the interrupt, for machine external interrupts. For software(ebreak) and timer interrupts, we can leave the upper bits blank for now, since they don’t really matter as the source is never one of these two devices, but rather the CPU (which currently doesn’t have a device ID… yet)

This way, we’ll get the following mcause CSR layout:

 31     16    11      7       3 
[DEVICE_ID]..[MI][..][TI][..][SI][..]

DEVICE_ID: Device ID that caused the machine interrupt
MI: Machine interrupt
TI: Timer interrupt
SI: Debug/Soft interrupt

In the hardware, device IDs are ORed together, so we might trap two device’s hardware interrupts in the same clock. The code will then have to and with the device ID to figure out if a particular device has hardware interrupt and read its data or take appropriate action.

The SDCard device

This is entirely an optional device, though thoroughly recommended, even more so than the DVI output one, since a filesystem and access to files is somewhat interesting when implementing a computer system, and allows for the device to run in standalone mode without cables or other computers attached to it.

That said, the SD card interface uses an SDCard PMOD from Digilent attached to PMOD port C on the Arty A7-100 board.

Click the image to follow it to Digilent’s web site

To drive it, NekoIchi currently uses an existing SPI IP from Jakub Cabal’s work in this github repository It is the best of its kind that I could locate so far, and ‘just works’ under different clock domains by adjusting the CLK_FREQ parameter. Alternative would be to write one from scratch, but that would be besides the point of implementing a RISC-V system from scratch, and our focus should mostly go to our custom peripherals and the CPU.

So far, our Arty A7-100 board looks like this with the two PMODs used in the project

Having avoided writing a full SPI stack from scratch, it was quite easy to implement the rest. In the riscvtool depot, you’ll find the SDCARD.cpp file that implements access to the SPI module from software side. Initialization and access to the SDCard is handled by a single function:

# Place at startup, or in a switch interrupt handler
SDCardStartup();

But the code doesn’t have much use for this kind of initialization unless we wish to directly call SDReadMultipleBlocks() to read raw blocks of data from an SDCard (which is perfectly an acceptable reason if you want to do so).

To help read more meaningful data from SDCards, with the FAT32 layout, I’ve utilized yet another great piece of code from Petit FatFs project by ChaN The beauty of having our own computer that can run pretty much any code we can throw at it is that we can now benefit from any project aimed at microcontrollers, given a tiny bit of porting work if they access any hardware directly.

Combining the two hardware/software projects above, one can instead use the following method to boot both the SDCard and bring up the Petit FatFs:

int sdcardavailable = (pf_mount(&Fs) == FR_OK) ? 1 : 0;

One smart thing we can also do in the future is to install an interrupt handler, and if we receive a hardware interrupt from the SWITCHES device, check bit 7 of the value at address 0x80000018 to see if a card has been inserted, and take the appropriate initialization action. Keeping the sd card availability around as a flag allows further calls into the library to be avoided if there’s no card plugged in.

Next

This concludes part 14. Rest of the series will focus on improving the hardware and look at the software layer in more detail.