Write to external flash

I am a new owner of a HiFive1 Rev B (technically the sparkfun Red-V) and I’m looking to fiddle around with reading and writing to the external qspi flash.

I wanted to check if anyone here has some experience with it. I’ve attempted to look around online but have been very unsuccessful and the Freedom-metal-sdk does not offer this functionality.

From what I can tell there are two possible avenues for me to be able to write to flash:

  1. Unlikely, but maybe just writing to a memory space associated with the QSPI (0x20010000+0x6a120)? This depends on whether or not the QSPI interface is able to handle writes by default, which my understanding is that it’s a read-only interface (this implementation is at least). But if possible as far as I know all I would need to do is modify the linker script to designate a section of that memory space as writable.

  2. Use the SPI0 peripheral. This one is the most likely the avenue I would need to take. My understanding is that if I enable SPI0 as a peripheral that will disable the QSPI interface. I have a few questions regarding this method:

    2a. Double check my understanding here: I see from the datasheet that I need two steps when it comes to changing SPI0 between IO and memory-mapped mode:

    1. Change the ‘en’ bit of the fctrl register (0x10014000+0x60) - 1 for memory-mapped and 0 for IO
    2. Change the ‘dir’ bit of the fmt register (0x10014000+0x40) - 1 for memory-mapped and 0 for IO
      Am I missing anything? Should I be saving+restoring the ffmt register as well?

    2b. Before I make the change from memory-mapped to IO mode I need to ensure that the Flash SPI write function is fully within cache or ITIM. I see there’s an example in the Freedom SDK on how I can add a function into ITIM, but ideally I should add the function at runtime since ITIM can be cleared and the whole thing working relies on the function being in ITIM. Any resources on how I may be able to add functions into cache/ITIM at runtime?

    2c. I will need to know the messaging protocol of the external flash in order to write to it. I suspect it’s relatively standard between flash manufacturers? I will take a look at the sparkfun red-v qpsi flash datasheet (IS25LP032D) but any knowledge on whether or not this part of the project would be portable between platforms?

    2d. Anything else I may be missing? I have a feeling that the QSPI flash chip won’t automatically switch between SPI and QSPI mode, so some insight in that matter would be great since I don’t have any experience with QSPI flash chips!

Thank you for giving this a read! Hopefully I am on the right track for this project… but more ideally I hope someone else has done this work already haha

@ManuelOnSiFive This is an excellent question! Though I haven’t yet worked out a simple self-contained example, here are a few pieces and thought which come to mind.

As long as code doesn’t run from, or access (read or write; load or store) memory from anything in the range from 0x2000 0000 thru 0x3FFF FFFF, there will be no CPU or system activity with QSPI0. Thus, you’d be free to use it for other things. In order to test this, you might want to set up the linker for “ram-only” mode, which is how I work all the time, to make sure of this. See my fe310-g002-ram.lds linker script and fe310-g002.cfg OpenOCD script at Demystifying OpenOCD for details.

In OpenOCD, you can read or write QSPI0 at will with a few simple steps which, assuming the data you want to write fits in four sectors of the flash chip, starting with sector 10 (i.e., banks 10, 11, 12, and 13), of the FE310-G002 chip, is shown below. NOTE: You might not want to start at sector 0 unless you wish to over-write whatever program or data is already loaded there, such as the original HiFive demo application!

Notice the fourth line is where you tell OpenOCD that the QSPI0 interface at 0x1001 4000 is to be used for memory access beginning at 0x2000 0000, a requirement of the FE310’s particular memory map; and the actual low-level code to do so is in fespi(.c, available easily from the freedom-metal or openocd repo’s).

transport select jtag
jtag newtap riscv cpu -irlen 5 -expected-id 0x20000913
target create riscv.cpu.0 riscv -chain-position riscv.cpu
flash bank spi0 fespi 0x20000000 0 0 0 riscv.cpu.0 0x10014000
flash protect 0 10 13 off
flash erase_sector 0 10 13
flash write_bank 0 my_binary_data_file
flash verify_bank 0 my_binary_data_file
flash protect 0 10 13 on

Sector size is flash device specific; it’s a bit tricky to calculate how many sectors some data will occupy. Here is an example of the calculation in tcl/tk (taken from asic_rom_load function in the link above):

set secsz [expr 0x1000]  ;# 4K sectors issi is25lp128d, typ. most NOR flash
set len [file size my_binary_data_file]
set endsec [expr (${len}/${secsz})+((${len}-(${len}/${secsz})*${secsz})>0)-1]

In bash, the same calculation is this:

$(eval SECSZ=$(shell echo "ibase=16; 1000" | bc))
$(eval LEN=$(shell <my_binary_data_file wc -c))
$(eval ENDSEC=$(shell echo "(${LEN}/${SECSZ})+((${LEN}-(${LEN}/${SECSZ})*${SECSZ})>0)-1" | bc))

Depending on the device connected to QSPI0, it may be necessary to first erase the range you (re)write, and possibly even to unprotect or later reprotect that range from further writing as well.

Every memory device is guaranteed to have at least one command in common: the Read function, command 0x03. That is how, for example, arbitrary code tests to see if a memory device is there, connected, and alive. Such use of this industry-standard command is in the (proprietary) Flash Recovery Mechanism at 0x2 0010. Many other commands are also common to the various manufacturers of memory devices; only sector size, page boundaries, and such would be size (capacity) and vendor specific.