Cornell University ECE4760
External Memory for

Pi Pico RP2040

External memory
PICO RAM is about 260 KB and flash program memory is about 2MB. There is often a need for more storage for audio, video, some simulations, and AI. This page explores ways of connecting external memory to the RP2040 in ways that do not use too many i/o resources. Possibilities include serial SRAM, serial FRAM, and SD card. Conviently reading and writing data may require several things, including support for data-types, named files with familiar function, and a scheme to copy data to/from a PC as well as the microcontroller.

FRAM basic SPI driver for 512KB MB85RS4MT on Adafruit #4719
FRAM is a nonvolatile form of RAM which is byte addressable, with full speed read/write. While much smaller than an SD card flash memory, it is much easier to quickly write data. The MB85RS4MT is a half-megabyte memory which can read/write at 5 MB/sec over SPI using a 40 MHz SPI clock. As usual however, when working on a solderless protoboard, the maximum clock speed you can use is around 20 MHz because of spurous pin capacitance. The Adafruit carrier board is shown below.

Pin connections are:

The code provides the ability to remap the SPI channel and the gpio pins connected to the SPI channel. The chip select does not use the automatic CS ability of an SPI channel because the select has to persist across several SPI transactions. Two macros do a simple map to a gpio pin:
//SPI configurations
#define PIN_SCK 10
#define PIN_SDO 11
#define PIN_SDIN 12
#define PIN_CS 13
#define SPI_PORT spi1
// chip select macros

#define CS_on gpio_put(PIN_CS,0)
#define CS_off gpio_put(PIN_CS,1)

There are four low level routines defined to handle the FRAM state machine.

A thread just writes a small pattern into FRAM, then reads it back, and prints it.

Code, ZIP

FRAM general data-type SPI driver for 512KB MB85RS4MT on Adafruit #4719
Given the basic communication from the previous paragraphs, we need a way to read/write arbitrary data types and structures to FRAM in a convenient way. If you cast the data input to a void pointer in the read/write functions, then the read/write just copies bytes regardless of the type of the original structure. Of course you must now tell the read/write function how many bytes to handle, rather than the number if items. The two functions are
void fram_write(spi_inst_t *spi, uint cs_pin, uint32_t addr, void * data, uint8_t len)
void fram_read(spi_inst_t *spi, uint cs_pin, uint32_t addr, void * buf, uint8_t len)
The test code shows how to use them. At a 20 MHz bus rate, a single float or int takes 4 usec to read/write, but if you read/write several values with one call, the address overhead is reduced and a read/write drops to 1.6 usec. This system is fully functional for data logging, but the named system below may be more handy, although a little less efficient.

Code, ZIP of project

Named files for FRAM and an interactive demo
This section is based on Simple file system by Paul Holmes (pholmes2012). It is a flat file system with no folders, 16-character names, and no ability to delete a file, unless you format the volume. It is designed to log data, or play back audio, not to be a replacement for FAT32. Description:

volume routines
void volume_create(int force)
The 'force' parameter should be set to true only if you are sure that you
want to format the device.

int volume_free(void)
Returns the number of fram free bytes, or -1 if there is no volume defined.

int volume_file_count(void)
Returns number of defined files or -1 if there is no volume defined.

file routines

int fCreate(char* fileName, uint32_t maxSize)
Create a file with a name and a maximum size it can grow to.
The inital actual size of a new file is zero.
The name can be any printable characters with max size 15 characters
Returns a file count or -1 if the file was not created.

int fOpen(char* fileName, open_file_index);         
Open an existing file, or return -1 if the file does not exist 
You choose the file index parameter in the range 0-9.

void fClose(file_open_index);                         
Close an open file by saving new file size

int fExists(char* fileName)
Returns the volume file index if the file exists or -1 if it does not

int fRead(int open_block_index, void * buf, uint8_t num_bytes)     
Read in data from the current file position  which starts at 0 when the file is opened.
then updates open_block read pointer.
Returns the number of bytes read, or -1 if the read fails.
File MUST be open!

int fWrite(int open_block_index, void * data, uint8_t num_bytes)
Write out data starting at the current end of file.
Updates open_block file size.
Returns the number of bytes written, or -1 if the write fails.
File MUST be open!

int fReadAt(int open_block_index, void * buf, uint32_t offset, uint32_t num_bytes)
Read in data from the offset position.
then updates open_block read pointer.
Returns the number of bytes read, or -1 if the read fails.
File MUST be open! You cannot read past the current file size!

int fWriteAt(int open_block_index, void * data, uint32_t location, uint32_t num_bytes)
Write out data starting at the specified offset. Updates open_block file size. Returns the number of bytes written, or -1 if the write fails. File MUST be open! You cannot write past the current file size! void volume_file_dir(void) Prints volume data and open file data when a serial terminal is attached.

Interactive test for named file system
It was useful to build a test program that allows a user to fiddle with the file system by further abstracting the file commands.
On a serial terminal you can issue the following commands:

For this example, only single integers are written for testing and to understand the logical dependencies of the file system. NOTE that all reported and required length parameters are in BYTES! Therefore, since we are writing integers, a file containing 3 integers will report a size of 11 (zero-based). To use readAt you would need to specify an offset of 0, 4, or 8 for int items 0, 1, 2. You might typically:

  1. format the volume
  2. create a file
  3. open the file
  4. write to the file,  then write again
  5. read the first value in the file
  6. close the file
  7. -- reset the processor --
  8. use dir to see persistent file is still there
  9. open the file again
  10. read the first value in the file

(screen capture of a few commands, and another)

Code, ZIP of project

FRAM voice recorder
Recording/playback of audio is a good test bed for the file system. You might create files with names like one or two, then speak the digits while recording the the files. The maximum length of the file you create determines the length of the recording. The sample rate is set to 8 KHz for voice, so you can record around 63 seconds of uncompressed speech on the 512K FRAM. But typical recording times might be around a second each for numerical digits. One SPI channel runs the FRAM, and the other runs the DAC. A timer alarm interrupt handles the actual copying of data to/from a buffer to the appripriate SPI channel. FRAM is actually read/written to the buffer in the serial control thread. In addition to the FRAM pinout above, a 12-bit DAC and microphone are attached to the PICO.

DAC MCP4822:
GPIO 5 (pin 7) ---> Chip select
GPIO 6 (pin 9) ---> SCK the spi clock
GPIO 7 (pin 10) ---> SDI data from rp2040 to DAC
Vdd ---> Vdd on DAC (3.3)
GND ---> Vss on DAC and to LDAC pin
VoutA ---> 3200 Hz RC lowpass to speakers

3.3    ---> Vcc on microphone
gnd    ---> GND on mic
GPIO26 ---> mic output

There are two new interactive commands and the interface is changed to full command line.
The command format is now:

Code, ZIP

PC to FRAM communication
A python script running on the PC sends a byte stream via serial interface to the RP2040, which copies it to the FRAM. Limit the data sent to printable characters, ASCII codes 0x20 to 0x7f.

Copyright Cornell University November 9, 2022