10 KiB
1. A MicroPython Flash memory driver
This driver supports the Cypress S25FL256L and S25FL128L chips, providing 64MiB and 32MiB respectively. These have 100K cycles of write endurance (compared to 10K for Pyboard Flash memory).
Multiple chips may be used to construct a single logical nonvolatile memory module. The driver allows the memory either to be mounted in the target filesystem as a disk device or to be addressed as an array of bytes.
The driver has the following attributes:
- It supports multiple Flash chips to configure a single array.
- It is cross-platform.
- The SPI bus can be shared with other chips.
- It supports filesystem mounting.
- Alternatively it can support byte-level access using Python slice syntax.
Flash technology requires a sector buffer. Consequently this driver uses 4KiB of RAM (compared to minuscule amounts for the FRAM and EEPROM drivers). This is an inevitable price for the large capacity of flash chips.
FAT and littlefs filesystems are supported but the latter is preferred owing to its resilience and wear levelling characteristics.
2. Connections
Any SPI interface may be used. The table below assumes a Pyboard running SPI(2)
as per the test program. To wire up a single flash chip, connect to a Pyboard
as below. Pin numbers an 8 pin SOIC or WSON package. Inputs marked nc
may be
connected to 3V3 or left unconnected.
Flash | Signal | PB | Signal |
---|---|---|---|
1 | CS/ | Y5 | SS/ |
2 | SO | Y7 | MISO |
3 | WP/ | nc | - |
4 | Vss | Gnd | Gnd |
5 | SI | Y8 | MOSI |
6 | SCK | Y6 | SCK |
7 | RESET/ | nc | - |
8 | Vcc | 3V3 | 3V3 |
For multiple chips a separate CS pin must be assigned to each chip: each one must be wired to a single chip's CS line. Multiple chips should have 3V3, Gnd, SCL, MOSI and MISO lines wired in parallel.
If you use a Pyboard D and power the chips from the 3V3 output you will need to enable the voltage rail by issuing:
machine.Pin.board.EN_3V3.value(1)
Other platforms may vary.
2.1 SPI Bus
The devices support baudrates up to 50MHz. In practice MicroPython targets do not support such high rates. In testing I found it necessary to specify 5MHz otherwise erratic results occurred. This was probably because of my breadboard test setup. On a PCB I would hope to run at a sunbstantially higher rate. The SPI bus is fast: wiring should be short and direct.
3. Files
flash_spi.py
Device driver.bdevice.py
(In root directory) Base class for the device driver.flash_test.py
Test programs for above.littlefs_test.py
Torture test for the littlefs filesystem on the flash array.
Installation: copy files 1 and 2 (3 & 4 are optional) to the target filesystem. Test scripts assume two chips with CS/ pins wired to Pyboard pins Y4 and Y5.
4. The device driver
The driver supports mounting the Flash chips as a filesystem. Initially the device will be unformatted so it is necessary to issue code along these lines to format the device. Code assumes two devices and also assumes the littlefs filesystem:
import os
from machine import SPI, Pin
from flash_spi import FLASH
cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1))
flash = FLASH(SPI(2, baudrate=20_000_000), cspins)
# Format the filesystem
os.VfsLfs2.mkfs(flash) # Omit this to mount an existing filesystem
os.mount(flash,'/fl_ext')
The above will reformat a drive with an existing filesystem: to mount an existing filesystem simply omit the commented line.
Note that, at the outset, you need to decide whether to use the array as a mounted filesystem or as a byte array. The filesystem is relatively small but has high integrity owing to the hardware longevity. Typical use-cases involve files which are frequently updated. These include files used for storing Python objects serialised using Pickle/ujson or files holding a btree database.
The SPI bus must be instantiated using the machine
module.
4.1 The FLASH class
An FLASH
instance represents a logical flash memory: this may consist of
multiple physical devices on a common SPI bus.
4.1.1 Constructor
This tests each chip in the list of chip select pins - if a chip is detected on
each chip select line a flash array is instantiated. A RuntimeError
will be
raised if a device is not detected on a CS line.
Arguments:
spi
Mandatory. An initialised SPI bus created bymachine
.cspins
A list or tuple ofPin
instances. EachPin
must be initialised as an output (Pin.OUT
) and withvalue=1
and be created bymachine
.size=16384
Chip size in KiB. Set to 32768 for the S25FL256L chip.verbose=True
IfTrue
, the constructor issues information on the flash devices it has detected.sec_size=4096
Chip sector size.block_size=9
The block size reported to the filesystem. The size in bytes is2**block_size
so is 512 bytes by default.
4.1.2 Methods providing byte level access
It is possible to read and write individual bytes or arrays of arbitrary size. Because of the very large size of the supported devices this mode is most likely to be of use for debugging. When writing in this mode it is necessary to be aware of the characteristics of flash devices. The memory is structured in blocks of 4096 bytes. To write a byte a block has to be read into RAM and the byte changed. The block on chip is erased then the new data written out. This process is slow (~300ms). In practice writing is deferred until it is necessary to access a different block: it is therefore faster to write data to consecutive addresses. Writing individual bytes to random addresses would be slow and cause undue wear because of the repeated need to erase and write sectors.
The examples below assume two devices, one with CS
connected to Pyboard pin
Y4 and the other with CS
connected to Y5.
4.1.2.1 __getitem__
and __setitem__
These provides single byte or multi-byte access using slice notation. Example of single byte access:
from machine import SPI, Pin
from flash_spi import FLASH
cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1))
flash = FLASH(SPI(2, baudrate=20_000_000), cspins)
flash[2000] = 42
print(flash[2000]) # Return an integer
It is also possible to use slice notation to read or write multiple bytes. If writing, the size of the slice must match the length of the buffer:
from machine import SPI, Pin
from flash_spi import FLASH
cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1))
flash = FLASH(SPI(2, baudrate=20_000_000), cspins)
flash[2000:2002] = bytearray((42, 43))
print(flash[2000:2002]) # Returns a bytearray
Three argument slices are not supported: a third arg (other than 1) will cause
an exception. One argument slices (flash[:5]
or flash[13100:]
) and negative
args are supported. See section 4.2
for a typical application.
4.1.2.2 readwrite
This is a byte-level alternative to slice notation. It has the potential advantage when reading of using a pre-allocated buffer. Arguments:
addr
Starting byte addressbuf
Abytearray
orbytes
instance containing data to write. In the read case it must be a (mutable)bytearray
to hold data read.read
IfTrue
, perform a read otherwise write. The size of the buffer determines the quantity of data read or written. ARuntimeError
will be thrown if the read or write extends beyond the end of the physical space.
4.1.3 Other methods
synchronise
This causes the cached sector to be written to the device. Should be called prior to power down. TODO: check flush/synchronise
The len() operator
The size of the flash array in bytes may be retrieved by issuing len(flash)
where flash
is the FLASH
instance.
scan
Activate each chip select in turn checking for a valid device and returns the
number of flash devices detected. A RuntimeError
will be raised if any CS
pin does not correspond to a valid chip.
Other than for debugging there is no need to call scan()
: the constructor
will throw a RuntimeError
if it fails to communicate with and correctly
identify the chip.
erase
Erases the entire array. Beware: this takes many minutes.
4.1.4 Methods providing the block protocol
These are provided by the base class. For the protocol definition see the pyb documentation also here.
readblocks()
writeblocks()
ioctl()
5. Test program flash_spi.py
This assumes a Pyboard 1.x or Pyboard D with two chips wired to SPI(2) as
above with chip selects connected to pins Y4
and Y5
. It provides the
following.
5.1 test()
This performs a basic test of single and multi-byte access to chip 0. The test reports how many chips can be accessed. Existing array data will be lost. This primarily tests the driver: as a hardware test it is not exhaustive.
5.2 full_test()
This is a hardware test. Tests the entire array. Fills each 256 byte page with random data, reads it back, and checks the outcome. Existing array data will be lost. TODO long run time.
5.3 fstest(format=False)
If True
is passed, formats the flash array as a littlefs filesystem and mounts
the device on /fl_ext
. If no arg is passed it mounts the array and lists the
contents. It also prints the outcome of uos.statvfs
on the array.
5.4 cptest()
Tests copying the source files to the filesystem. The test will fail if the
filesystem was not formatted. Lists the contents of the mountpoint and prints
the outcome of uos.statvfs
.
5.5 File copy
A rudimentary cp(source, dest)
function is provided as a generic file copy
routine for setup and debugging purposes at the REPL. The first argument is the
full pathname to the source file. The second may be a full path to the
destination file or a directory specifier which must have a trailing '/'. If an
OSError is thrown (e.g. by the source file not existing or the flash becoming
full) it is up to the caller to handle it. For example (assuming the flash is
mounted on /fl_ext):
cp('/flash/main.py','/fl_ext/')
See upysh
in micropython-lib
for other filesystem tools for use at the REPL.