kopia lustrzana https://github.com/peterhinch/micropython_eeprom
First pass at flash driver.
rodzic
04d8c9b85a
commit
49991d1e02
86
README.md
86
README.md
|
@ -2,7 +2,7 @@
|
|||
|
||||
These drivers support nonvolatile memory chips and the littlefs filesystem.
|
||||
|
||||
Currently supported devices use technologies having superior performance
|
||||
Currently supported devices include technologies having superior performance
|
||||
compared to flash. Resultant storage has much higher write endurance. In some
|
||||
cases read and write access times may be shorter. EEPROM and FRAM chips have
|
||||
much lower standby current than SD cards, benefiting micropower applications.
|
||||
|
@ -26,14 +26,15 @@ The drivers have the following common features:
|
|||
|
||||
## 1.2 Technologies
|
||||
|
||||
Currently supported technologies are EEPROM and FRAM (ferroelectric RAM). These
|
||||
are nonvolatile random access storage devices with much higher endurance than
|
||||
flash memory. Flash has a typical endurance of 10K writes per page. The figures
|
||||
for EEPROM and FRAM are 1-4M and 10^12 writes respectively. In the case of the
|
||||
FAT filing system 1M page writes probably corresponds to 1M filesystem writes
|
||||
because FAT repeatedly updates the allocation tables in the low numbered
|
||||
sectors. If `littlefs` is used I would expect the endurance to be substantially
|
||||
better owing to its wear levelling architecture.
|
||||
Currently supported technologies are Flash, EEPROM and FRAM (ferroelectric
|
||||
RAM). The latter two are nonvolatile random access storage devices with much
|
||||
higher endurance than flash memory. Flash has a typical endurance of 10-100K
|
||||
writes per page. The figures for EEPROM and FRAM are 1-4M and 10^12 writes
|
||||
respectively. In the case of the FAT filing system 1M page writes probably
|
||||
corresponds to 1M filesystem writes because FAT repeatedly updates the
|
||||
allocation tables in the low numbered sectors. Under `littlefs` I would expect
|
||||
the endurance to be substantially better owing to its wear levelling
|
||||
architecture.
|
||||
|
||||
## 1.3 Supported chips
|
||||
|
||||
|
@ -49,20 +50,23 @@ M95M02-DRMN6TP and M95M02-DWMN3TP/K. The latter has a wider temperature range.
|
|||
|
||||
In the table below the Interface column includes page size in bytes.
|
||||
|
||||
| Manufacturer | Part | Interface | Bytes | Technology | Docs |
|
||||
|:------------:|:---------:|:---------:|:------:|:----------:|:-------------------------:|
|
||||
| STM | M95M02-DR | SPI 256 | 256KiB | EEPROM | [SPI.md](./spi/SPI.md) |
|
||||
| Microchip | 25xx1024 | SPI 256 | 128KiB | EEPROM | [SPI.md](./spi/SPI.md) |
|
||||
| Microchip | 24xx512 | I2C 128 | 64KiB | EEPROM | [I2C.md](./i2c/I2C.md) |
|
||||
| Microchip | 24xx256 | I2C 128 | 32KiB | EEPROM | [I2C.md](./i2c/I2C.md) |
|
||||
| Microchip | 24xx128 | I2C 128 | 16KiB | EEPROM | [I2C.md](./i2c/I2C.md) |
|
||||
| Microchip | 24xx64 | I2C 128 | 8KiB | EEPROM | [I2C.md](./i2c/I2C.md) |
|
||||
| Adafruit | 1895 | I2C n/a | 32KiB | FRAM | [FRAM.md](./fram/FRAM.md) |
|
||||
| Manufacturer | Part | Interface | Bytes | Technology | Docs |
|
||||
|:------------:|:---------:|:---------:|:------:|:----------:|:----------------------------:|
|
||||
| Cypress | S25FL256L | SPI 4096 | 32MiB | Flash | [FLASH.md](./flash/FLASH.md) |
|
||||
| Cypress | S25FL128L | SPI 4096 | 16MiB | Flash | [FLASH.md](./flash/FLASH.md) |
|
||||
| STM | M95M02-DR | SPI 256 | 256KiB | EEPROM | [SPI.md](./spi/SPI.md) |
|
||||
| Microchip | 25xx1024 | SPI 256 | 128KiB | EEPROM | [SPI.md](./spi/SPI.md) |
|
||||
| Microchip | 24xx512 | I2C 128 | 64KiB | EEPROM | [I2C.md](./i2c/I2C.md) |
|
||||
| Microchip | 24xx256 | I2C 128 | 32KiB | EEPROM | [I2C.md](./i2c/I2C.md) |
|
||||
| Microchip | 24xx128 | I2C 128 | 16KiB | EEPROM | [I2C.md](./i2c/I2C.md) |
|
||||
| Microchip | 24xx64 | I2C 128 | 8KiB | EEPROM | [I2C.md](./i2c/I2C.md) |
|
||||
| Adafruit | 1895 | I2C n/a | 32KiB | FRAM | [FRAM.md](./fram/FRAM.md) |
|
||||
|
||||
Documentation:
|
||||
[SPI.md](./spi/SPI.md)
|
||||
[I2C.md](./i2c/I2C.md)
|
||||
[FRAM.md](./fram/FRAM.md)
|
||||
[FLASH.md](./flash/FLASH.md)
|
||||
|
||||
## 1.4 Performance
|
||||
|
||||
|
@ -78,6 +82,11 @@ The drivers provide the benefit of page writing in a way which is transparent.
|
|||
If you write a block of data to an arbitrary address, page writes will be used
|
||||
to minimise total time.
|
||||
|
||||
In the case of flash memory page writing is mandatory: a sector is written by
|
||||
first erasing it, a process which is slow. This physical limitation means that
|
||||
the driver must buffer an entire 4096 byte sector. This contrasts with FRAM and
|
||||
EEPROM drivers where the buffering comprises a few bytes.
|
||||
|
||||
# 2. Choice of interface
|
||||
|
||||
The principal merit of I2C is to minimise pin count. It uses two pins
|
||||
|
@ -91,21 +100,38 @@ apparent on reads: write speed is limited by the EEPROM device. In principle
|
|||
expansion is limited only by the number of available pins. (In practice
|
||||
electrical limits may also apply).
|
||||
|
||||
In the case of the Microchip devices supported, the SPI chip is larger at
|
||||
128KiB compared to a maximum of 64KiB in the I2C range.
|
||||
The larger capacity chips generally use SPI.
|
||||
|
||||
# 3. Design details and test results
|
||||
# 3. Design details, littlefs support
|
||||
|
||||
The fact that the API enables accessing blocks of data at arbitrary addresses
|
||||
implies that the handling of page addressing is done in the driver. This
|
||||
contrasts with drivers intended only for filesystem access. These devolve the
|
||||
detail of page addressing to the filesystem by specifying the correct page size
|
||||
in the ioctl and (if necessary) implementing a block erase method.
|
||||
A key aim of these drivers is support for littlefs. This requires the extended
|
||||
block device protocol as described
|
||||
[here](http://docs.micropython.org/en/latest/reference/filesystem.html) and
|
||||
[in the uos doc](http://docs.micropython.org/en/latest/library/uos.html).
|
||||
This protocol describes a block structured API capable of handling offsets into
|
||||
the block. It is therefore necessary for the device driver to deal with any
|
||||
block structuring inherent in the hardware. The device driver must enable
|
||||
access to varying amounts of data at arbitrary physical addresses.
|
||||
|
||||
The nature of the drivers in this repo implies that the page size in the ioctl
|
||||
is arbitrary. Littlefs requires a minimum size of 128 bytes -
|
||||
These drivers achieve this by implementing a device-dependent `readwrite`
|
||||
method which provides read and write access to arbitrary addresses, with data
|
||||
volumes which can span page and chip boundaries. A benefit of this is that the
|
||||
array of chips can be presented as a large byte array. This array is accessible
|
||||
by Python slice notation: behaviour provided by the hardware-independent base
|
||||
class.
|
||||
|
||||
A consequence of the above is that the page size in the ioctl does not have any
|
||||
necessary connection with the memory hardware, so the drivers enable the value
|
||||
to be specified as a constructor argument. Littlefs requires a minimum size of
|
||||
128 bytes -
|
||||
[theoretically 104](https://github.com/ARMmbed/littlefs/blob/master/DESIGN.md).
|
||||
The driver only allows powers of 2. Testing was done with 512 bytes.
|
||||
The drivers only allow powers of 2: in principle 128 bytes could be used. The
|
||||
default in MicroPython's littlefs implementation is 512 bytes and all testing
|
||||
was done with this value. FAT requires 512 bytes minimum: FAT testing was done
|
||||
with the same block size.
|
||||
|
||||
The test programs use littlefs and therefore require MicroPython V1.12 or
|
||||
later.
|
||||
later. On platforms that don't support littlefs the options are either to adapt
|
||||
the test programs for FAT (code is commented out) or to build firmware with
|
||||
littlefs support. This can be done by passing `MICROPY_VFS_LFS2=1` to the
|
||||
`make` command.
|
||||
|
|
81
bdevice.py
81
bdevice.py
|
@ -1,10 +1,12 @@
|
|||
# bdevice.py Hardware-agnostic base class for block devices.
|
||||
# bdevice.py Hardware-agnostic base classes.
|
||||
# BlockDevice Base class for general block devices e.g. EEPROM, FRAM.
|
||||
# FlashDevice Base class for generic Flash memory (subclass of BlockDevice).
|
||||
|
||||
# Released under the MIT License (MIT). See LICENSE.
|
||||
# Copyright (c) 2019 Peter Hinch
|
||||
|
||||
# Hardware-independent class implementing the uos.AbstractBlockDev protocol with
|
||||
# simple and extended interface. It should therefore support littlefs.
|
||||
# BlockDevice: hardware-independent class implementing the uos.AbstractBlockDev
|
||||
# protocol with extended interface. It supports littlefs.
|
||||
# http://docs.micropython.org/en/latest/reference/filesystem.html#custom-block-devices
|
||||
|
||||
# The subclass must implement .readwrite which can read or write arbitrary amounts
|
||||
|
@ -76,3 +78,76 @@ class BlockDevice:
|
|||
if op == 6: # Erase
|
||||
return 0
|
||||
|
||||
# Hardware agnostic base class for flash memory, where a single sector is cached.
|
||||
# This minimises RAM usage. Under FAT wear is reduced if you cache at least two
|
||||
# sectors. This driver is primarily intended for littlefs which has no such issue.
|
||||
|
||||
# Subclass must provide these hardware-dependent methods:
|
||||
# .rdchip(addr, mvb) Read from chip into memoryview: data guaranteed not to be cached.
|
||||
# .flush(cache, addr) Erase physical sector and write out an entire cached sector.
|
||||
# .readwrite As per base class.
|
||||
|
||||
class FlashDevice(BlockDevice):
|
||||
|
||||
def __init__(self, nbits, nchips, chip_size, sec_size):
|
||||
super().__init__(nbits, nchips, chip_size)
|
||||
self.sec_size = sec_size
|
||||
self._cache_mask = sec_size - 1 # For 4K sector size: 0xfff
|
||||
self._fmask = self._cache_mask ^ 0x3fffffff # 4K -> 0x3ffff000
|
||||
self._cache = bytearray(sec_size) # Cache always contains one sector
|
||||
self._mvd = memoryview(self._cache)
|
||||
self._acache = 0 # Address in chip of byte 0 of current cached sector
|
||||
|
||||
def read(self, addr, mvb):
|
||||
nbytes = len(mvb)
|
||||
next_sec = self._acache + self.sec_size # Start of next sector
|
||||
if addr >= next_sec or addr + nbytes <= self._acache:
|
||||
self.rdchip(addr, mvb) # No data is cached: just read from device
|
||||
else:
|
||||
# Some of address range is cached
|
||||
boff = 0 # Offset into buf
|
||||
if addr < self._acache: # Read data prior to cache from chip
|
||||
nr = self._acache - addr
|
||||
self.rdchip(addr, mvb[:nr])
|
||||
addr = self._acache # Start of cached data
|
||||
nbytes -= nr
|
||||
boff += nr
|
||||
# addr now >= self._acache: read from cache.
|
||||
sa = addr - self._acache # Offset into cache
|
||||
nr = min(nbytes, self._acache + self.sec_size - addr) # No of bytes to read from cache
|
||||
mvb[boff : boff + nr] = self._mvd[sa : sa + nr]
|
||||
if nbytes - nr: # Get any remaining data from chip
|
||||
self.rdchip(addr + nr, mvb[boff + nr : ])
|
||||
return mvb
|
||||
|
||||
def synchronise(self):
|
||||
# print('SYNCHRONISE')
|
||||
self.flush(self._mvd, self._acache) # Write out old data
|
||||
|
||||
# TODO Performance enhancement: if cache intersects address range, update it first.
|
||||
# Currently in this case it would be written twice.
|
||||
def write(self, addr, mvb):
|
||||
nbytes = len(mvb)
|
||||
acache = self._acache
|
||||
boff = 0 # Offset into buf.
|
||||
while nbytes:
|
||||
if (addr & self._fmask) != acache:
|
||||
self.synchronise() # Erase sector and write out old data
|
||||
self._fill_cache(addr) # Cache sector which includes addr
|
||||
offs = addr & self._cache_mask # Offset into cache
|
||||
npage = min(nbytes, self.sec_size - offs) # No. of bytes in current sector
|
||||
self._mvd[offs : offs + npage] = mvb[boff : boff + npage]
|
||||
nbytes -= npage
|
||||
boff += npage
|
||||
addr += npage
|
||||
return mvb
|
||||
|
||||
# Cache the sector which contains a given byte addresss. Save sector
|
||||
# start address.
|
||||
def _fill_cache(self, addr):
|
||||
addr &= self._fmask
|
||||
self.rdchip(addr, self._mvd)
|
||||
self._acache = addr
|
||||
|
||||
def initialise(self):
|
||||
self._fill_cache(0)
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
# 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:
|
||||
1. It supports multiple Flash chips to configure a single array.
|
||||
2. It is cross-platform.
|
||||
3. The SPI bus can be shared with other chips.
|
||||
4. It supports filesystem mounting.
|
||||
5. 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:
|
||||
```python
|
||||
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
|
||||
|
||||
1. `flash_spi.py` Device driver.
|
||||
2. `bdevice.py` (In root directory) Base class for the device driver.
|
||||
3. `flash_test.py` Test programs for above.
|
||||
|
||||
Installation: copy files 1 and 2 (optionally 3) to the target filesystem.
|
||||
|
||||
# 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:
|
||||
|
||||
```python
|
||||
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:
|
||||
1. `spi` Mandatory. An initialised SPI bus created by `machine`.
|
||||
2. `cspins` A list or tuple of `Pin` instances. Each `Pin` must be initialised
|
||||
as an output (`Pin.OUT`) and with `value=1` and be created by `machine`.
|
||||
3. `size=16384` Chip size in KiB. Set to 32768 for the S25FL256L chip.
|
||||
4. `verbose=True` If `True`, the constructor issues information on the flash
|
||||
devices it has detected.
|
||||
5. `sec_size=4096` Chip sector size.
|
||||
6. `block_size=9` The block size reported to the filesystem. The size in bytes
|
||||
is `2**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:
|
||||
|
||||
```python
|
||||
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:
|
||||
```python
|
||||
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](./SPI.md#42-byte-addressing-usage-example)
|
||||
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:
|
||||
1. `addr` Starting byte address
|
||||
2. `buf` A `bytearray` or `bytes` instance containing data to write. In the
|
||||
read case it must be a (mutable) `bytearray` to hold data read.
|
||||
3. `read` If `True`, perform a read otherwise write. The size of the buffer
|
||||
determines the quantity of data read or written. A `RuntimeError` 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](http://docs.micropython.org/en/latest/library/uos.html#uos.AbstractBlockDev)
|
||||
also [here](http://docs.micropython.org/en/latest/reference/filesystem.html#custom-block-devices).
|
||||
|
||||
`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):
|
||||
|
||||
```python
|
||||
cp('/flash/main.py','/fl_ext/')
|
||||
```
|
||||
|
||||
See `upysh` in [micropython-lib](https://github.com/micropython/micropython-lib.git)
|
||||
for other filesystem tools for use at the REPL.
|
|
@ -0,0 +1,164 @@
|
|||
# flash_spi.py MicroPython driver for Cypress S25FL128L 16MiB and S25FL256L 32MiB
|
||||
# flash devices.
|
||||
|
||||
# Released under the MIT License (MIT). See LICENSE.
|
||||
# Copyright (c) 2019 Peter Hinch
|
||||
|
||||
import time
|
||||
from micropython import const
|
||||
from bdevice import FlashDevice
|
||||
|
||||
# Supported instruction set:
|
||||
# 4 byte address commands
|
||||
_READ = const(0x13)
|
||||
_PP = const(0x12) # Page program
|
||||
_SE = const(0x21) # Sector erase
|
||||
# No address
|
||||
_WREN = const(6) # Write enable
|
||||
_RDSR1 = const(5) # Read status register 1
|
||||
_RDSR2 = const(7) # Read status register 2
|
||||
_RDID = const(0x9f) # Read manufacturer ID
|
||||
_CE = const(0xc7) # Chip erase (takes minutes)
|
||||
|
||||
_SEC_SIZE = const(4096) # Flash sector size 0x1000
|
||||
|
||||
# Logical Flash device comprising one or more physical chips sharing an SPI bus.
|
||||
class FLASH(FlashDevice):
|
||||
|
||||
def __init__(self, spi, cspins, size=16384, verbose=True, sec_size=_SEC_SIZE, block_size=9):
|
||||
# args: virtual block size in bits, no. of chips, bytes in each chip
|
||||
if size not in (16384, 32768):
|
||||
raise ValueError('Valid sizes: 16384 or 32768KiB')
|
||||
super().__init__(block_size, len(cspins), size * 1024, sec_size)
|
||||
self._spi = spi
|
||||
self._cspins = cspins
|
||||
self._ccs = None # Chip select Pin object for current chip
|
||||
self._bufp = bytearray(6) # instruction + 4 byte address + 1 byte value
|
||||
self._mvp = memoryview(self._bufp) # cost-free slicing
|
||||
self._page_size = 256 # Write uses 256 byte pages.
|
||||
self.scan(verbose)
|
||||
self.initialise() # Initially cache sector 0
|
||||
|
||||
# **** API SPECIAL METHODS ****
|
||||
# Scan: read manf ID
|
||||
def scan(self, verbose):
|
||||
mvp = self._mvp
|
||||
for n, cs in enumerate(self._cspins):
|
||||
mvp[:] = b'\0\0\0\0\0\0'
|
||||
mvp[0] = _RDID
|
||||
cs(0)
|
||||
self._spi.write_readinto(mvp[:4], mvp[:4])
|
||||
cs(1)
|
||||
if mvp[1] != 1 or mvp[2] != 0x60 or not (mvp[3] == 0x18 or mvp[3] == 0x19):
|
||||
raise RuntimeError('Flash not found at cs[{}].'.format(n))
|
||||
if verbose:
|
||||
s = '{} chips detected. Total flash size {}MiB.'
|
||||
print(s.format(n + 1, self._a_bytes // (1024 * 1024)))
|
||||
return n
|
||||
|
||||
# Chip erase. Can take minutes.
|
||||
def erase(self):
|
||||
mvp = self._mvp
|
||||
for cs in self._cspins: # For each chip
|
||||
mvp[0] = _WREN
|
||||
cs(0)
|
||||
self._spi.write(mvp[:1]) # Enable write
|
||||
cs(1)
|
||||
mvp[0] = _CE
|
||||
cs(0)
|
||||
self._spi.write(mvp[:1]) # Start erase
|
||||
cs(1)
|
||||
self._wait_rdy() # Wait for erase to complete
|
||||
|
||||
# **** INTERFACE FOR BASE CLASS ****
|
||||
# Write cache to a sector starting at byte address addr
|
||||
def flush(self, cache, addr): # cache is memoryview into buffer
|
||||
self._sector_erase(addr)
|
||||
mvp = self._mvp
|
||||
nbytes = self.sec_size
|
||||
ps = self._page_size
|
||||
start = 0 # Current offset into cache buffer
|
||||
while nbytes > 0:
|
||||
# write one page at a time
|
||||
self._getaddr(addr, 1)
|
||||
cs = self._ccs # Current chip select from _getaddr
|
||||
mvp[0] = _WREN
|
||||
cs(0)
|
||||
self._spi.write(mvp[:1]) # Enable write
|
||||
cs(1)
|
||||
mvp[0] = _PP
|
||||
cs(0)
|
||||
self._spi.write(mvp[:5]) # Start write
|
||||
self._spi.write(cache[start : start + ps])
|
||||
cs(1)
|
||||
self._wait_rdy() # Wait for write to complete
|
||||
nbytes -= ps
|
||||
start += ps
|
||||
addr += ps
|
||||
|
||||
# Read from chip into a memoryview. Address range guaranteed not to be cached.
|
||||
def rdchip(self, addr, mvb):
|
||||
nbytes = len(mvb)
|
||||
mvp = self._mvp
|
||||
start = 0 # Offset into buf.
|
||||
while nbytes > 0:
|
||||
npage = self._getaddr(addr, nbytes) # No. of bytes in current chip
|
||||
cs = self._ccs
|
||||
mvp[0] = _READ
|
||||
cs(0)
|
||||
self._spi.write(mvp[:5])
|
||||
self._spi.readinto(mvb[start : start + npage])
|
||||
cs(1)
|
||||
nbytes -= npage
|
||||
start += npage
|
||||
addr += npage
|
||||
|
||||
# Read or write multiple bytes at an arbitrary address.
|
||||
# **** Also part of API ****
|
||||
def readwrite(self, addr, buf, read):
|
||||
mvb = memoryview(buf)
|
||||
self.read(addr, mvb) if read else self.write(addr, mvb)
|
||||
return buf
|
||||
|
||||
# **** INTERNAL METHODS ****
|
||||
def _wait_rdy(self): # After a write, wait for device to become ready
|
||||
mvp = self._mvp
|
||||
cs = self._ccs # Chip is already current
|
||||
while True:
|
||||
mvp[0] = _RDSR1
|
||||
cs(0)
|
||||
self._spi.write_readinto(mvp[:2], mvp[:2])
|
||||
cs(1)
|
||||
if not (mvp[1] & 1):
|
||||
break
|
||||
time.sleep_ms(1)
|
||||
|
||||
# Given an address, set current chip select and address buffer.
|
||||
# Return the number of bytes that can be processed in the current chip.
|
||||
def _getaddr(self, addr, nbytes):
|
||||
if addr >= self._a_bytes:
|
||||
raise RuntimeError("Flash Address is out of range")
|
||||
ca, la = divmod(addr, self._c_bytes) # ca == chip no, la == offset into chip
|
||||
self._ccs = self._cspins[ca] # Current chip select
|
||||
mvp = self._mvp
|
||||
mvp[1] = la >> 24
|
||||
mvp[2] = la >> 16 & 0xff
|
||||
mvp[3] = (la >> 8) & 0xff
|
||||
mvp[4] = la & 0xff
|
||||
pe = (addr & -self._c_bytes) + self._c_bytes # Byte 0 of next chip
|
||||
return min(nbytes, pe - la)
|
||||
|
||||
# Erase sector. Address is start byte address of sector.
|
||||
def _sector_erase(self, addr):
|
||||
self._getaddr(addr, 1)
|
||||
cs = self._ccs # Current chip select from _getaddr
|
||||
mvp = self._mvp
|
||||
mvp[0] = _WREN
|
||||
cs(0)
|
||||
self._spi.write(mvp[:1]) # Enable write
|
||||
cs(1)
|
||||
mvp[0] = _SE
|
||||
cs(0)
|
||||
self._spi.write(mvp[:5]) # Start erase
|
||||
cs(1)
|
||||
self._wait_rdy() # Wait for erase to complete
|
|
@ -0,0 +1,138 @@
|
|||
# flash_test.py MicroPython test program for Cypress SPI Flash devices.
|
||||
|
||||
# Released under the MIT License (MIT). See LICENSE.
|
||||
# Copyright (c) 2019 Peter Hinch
|
||||
|
||||
import uos
|
||||
from machine import SPI, Pin
|
||||
from flash_spi import FLASH
|
||||
# Add extra pins if using multiple chips
|
||||
cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1))
|
||||
|
||||
# Return an EEPROM array. Adapt for platforms other than Pyboard.
|
||||
def get_device():
|
||||
if uos.uname().machine.split(' ')[0][:4] == 'PYBD':
|
||||
Pin.board.EN_3V3.value(1)
|
||||
flash = FLASH(SPI(2, baudrate=5_000_000), cspins)
|
||||
print('Instantiated Flash')
|
||||
return flash
|
||||
|
||||
# Dumb file copy utility to help with managing EEPROM contents at the REPL.
|
||||
def cp(source, dest):
|
||||
if dest.endswith('/'): # minimal way to allow
|
||||
dest = ''.join((dest, source.split('/')[-1])) # cp /sd/file /fl_ext/
|
||||
with open(source, 'rb') as infile: # Caller should handle any OSError
|
||||
with open(dest,'wb') as outfile: # e.g file not found
|
||||
while True:
|
||||
buf = infile.read(100)
|
||||
outfile.write(buf)
|
||||
if len(buf) < 100:
|
||||
break
|
||||
|
||||
# ***** TEST OF DRIVER *****
|
||||
def _testblock(eep, bs):
|
||||
d0 = b'this >'
|
||||
d1 = b'<is the boundary'
|
||||
d2 = d0 + d1
|
||||
garbage = b'xxxxxxxxxxxxxxxxxxx'
|
||||
start = bs - len(d0)
|
||||
end = start + len(garbage)
|
||||
eep[start : end] = garbage
|
||||
res = eep[start : end]
|
||||
if res != garbage:
|
||||
return 'Block test fail 1:' + str(list(res))
|
||||
end = start + len(d0)
|
||||
eep[start : end] = d0
|
||||
end = start + len(garbage)
|
||||
res = eep[start : end]
|
||||
if res != b'this >xxxxxxxxxxxxx':
|
||||
return 'Block test fail 2:' + str(list(res))
|
||||
start = bs
|
||||
end = bs + len(d1)
|
||||
eep[start : end] = d1
|
||||
start = bs - len(d0)
|
||||
end = start + len(d2)
|
||||
res = eep[start : end]
|
||||
if res != d2:
|
||||
return 'Block test fail 3:' + str(list(res))
|
||||
|
||||
def test():
|
||||
eep = get_device()
|
||||
sa = 1000
|
||||
for v in range(256):
|
||||
eep[sa + v] = v
|
||||
for v in range(256):
|
||||
if eep[sa + v] != v:
|
||||
print('Fail at address {} data {} should be {}'.format(sa + v, eep[sa + v], v))
|
||||
break
|
||||
else:
|
||||
print('Test of byte addressing passed')
|
||||
data = uos.urandom(30)
|
||||
sa = 2000
|
||||
eep[sa:sa + 30] = data
|
||||
if eep[sa:sa + 30] == data:
|
||||
print('Test of slice readback passed')
|
||||
|
||||
block = 256
|
||||
res = _testblock(eep, block)
|
||||
if res is None:
|
||||
print('Test block boundary {} passed'.format(block))
|
||||
else:
|
||||
print('Test block boundary {} fail'.format(block))
|
||||
print(res)
|
||||
block = eep._c_bytes
|
||||
if eep._a_bytes > block:
|
||||
res = _testblock(eep, block)
|
||||
if res is None:
|
||||
print('Test chip boundary {} passed'.format(block))
|
||||
else:
|
||||
print('Test chip boundary {} fail'.format(block))
|
||||
print(res)
|
||||
else:
|
||||
print('Test chip boundary skipped: only one chip!')
|
||||
|
||||
# ***** TEST OF FILESYSTEM MOUNT *****
|
||||
def fstest(format=False):
|
||||
eep = get_device()
|
||||
# ***** CODE FOR LITTLEFS *****
|
||||
if format:
|
||||
uos.VfsLfs2.mkfs(eep)
|
||||
try:
|
||||
uos.mount(eep,'/fl_ext')
|
||||
except OSError: # Already mounted
|
||||
pass
|
||||
print('Contents of "/": {}'.format(uos.listdir('/')))
|
||||
print('Contents of "/fl_ext": {}'.format(uos.listdir('/fl_ext')))
|
||||
print(uos.statvfs('/fl_ext'))
|
||||
|
||||
def cptest():
|
||||
eep = get_device()
|
||||
if 'fl_ext' in uos.listdir('/'):
|
||||
print('Device already mounted.')
|
||||
else:
|
||||
try:
|
||||
uos.mount(eep,'/fl_ext')
|
||||
except OSError:
|
||||
print('Fail mounting device. Have you formatted it?')
|
||||
return
|
||||
print('Mounted device.')
|
||||
cp('flash_test.py', '/fl_ext/')
|
||||
cp('flash_spi.py', '/fl_ext/')
|
||||
print('Contents of "/fl_ext": {}'.format(uos.listdir('/fl_ext')))
|
||||
print(uos.statvfs('/fl_ext'))
|
||||
|
||||
|
||||
# ***** TEST OF HARDWARE *****
|
||||
def full_test():
|
||||
eep = get_device()
|
||||
page = 0
|
||||
for sa in range(0, len(eep), 256):
|
||||
data = uos.urandom(256)
|
||||
eep[sa:sa + 256] = data
|
||||
got = eep[sa:sa + 256]
|
||||
if got == data:
|
||||
print('Page {} passed'.format(page))
|
||||
else:
|
||||
print('Page {} readback failed.'.format(page))
|
||||
break
|
||||
page += 1
|
|
@ -38,13 +38,13 @@ def _testblock(eep, bs):
|
|||
eep[start : end] = garbage
|
||||
res = eep[start : end]
|
||||
if res != garbage:
|
||||
return 'Block test fail 1:' + res
|
||||
return 'Block test fail 1:' + str(list(res))
|
||||
end = start + len(d0)
|
||||
eep[start : end] = d0
|
||||
end = start + len(garbage)
|
||||
res = eep[start : end]
|
||||
if res != b'this >xxxxxxxxxxxxx':
|
||||
return 'Block test fail 2:' + res
|
||||
return 'Block test fail 2:' + str(list(res))
|
||||
start = bs
|
||||
end = bs + len(d1)
|
||||
eep[start : end] = d1
|
||||
|
@ -52,7 +52,7 @@ def _testblock(eep, bs):
|
|||
end = start + len(d2)
|
||||
res = eep[start : end]
|
||||
if res != d2:
|
||||
return 'Block test fail 3:' + res
|
||||
return 'Block test fail 3:' + str(list(res))
|
||||
|
||||
def test():
|
||||
fram = get_fram()
|
||||
|
|
|
@ -240,9 +240,9 @@ lost.
|
|||
|
||||
## 5.3 fstest(format=False)
|
||||
|
||||
If `True` is passed, formats the EEPROM array as a FAT filesystem and mounts
|
||||
the device on `/eeprom`. If no arg is passed it mounts the array and lists the
|
||||
contents. It also prints the outcome of `uos.statvfs` on the array.
|
||||
If `True` is passed, formats the EEPROM array as a littlefs filesystem and
|
||||
mounts the device on `/eeprom`. 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()
|
||||
|
||||
|
|
|
@ -39,13 +39,13 @@ def _testblock(eep, bs):
|
|||
eep[start : end] = garbage
|
||||
res = eep[start : end]
|
||||
if res != garbage:
|
||||
return 'Block test fail 1:' + res
|
||||
return 'Block test fail 1:' + str(list(res))
|
||||
end = start + len(d0)
|
||||
eep[start : end] = d0
|
||||
end = start + len(garbage)
|
||||
res = eep[start : end]
|
||||
if res != b'this >xxxxxxxxxxxxx':
|
||||
return 'Block test fail 2:' + res
|
||||
return 'Block test fail 2:' + str(list(res))
|
||||
start = bs
|
||||
end = bs + len(d1)
|
||||
eep[start : end] = d1
|
||||
|
@ -53,7 +53,7 @@ def _testblock(eep, bs):
|
|||
end = start + len(d2)
|
||||
res = eep[start : end]
|
||||
if res != d2:
|
||||
return 'Block test fail 3:' + res
|
||||
return 'Block test fail 3:' + str(list(res))
|
||||
|
||||
def test():
|
||||
eep = get_eep()
|
||||
|
|
|
@ -257,9 +257,9 @@ lost.
|
|||
|
||||
## 5.3 fstest(format=False, stm=False)
|
||||
|
||||
If `True` is passed, formats the EEPROM array as a FAT filesystem and mounts
|
||||
the device on `/eeprom`. If no arg is passed it mounts the array and lists the
|
||||
contents. It also prints the outcome of `uos.statvfs` on the array.
|
||||
If `True` is passed, formats the EEPROM array as a littlefs filesystem and
|
||||
mounts the device on `/eeprom`. 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(stm=False)
|
||||
|
||||
|
|
|
@ -43,13 +43,13 @@ def _testblock(eep, bs):
|
|||
eep[start : end] = garbage
|
||||
res = eep[start : end]
|
||||
if res != garbage:
|
||||
return 'Block test fail 1:' + res
|
||||
return 'Block test fail 1:' + str(list(res))
|
||||
end = start + len(d0)
|
||||
eep[start : end] = d0
|
||||
end = start + len(garbage)
|
||||
res = eep[start : end]
|
||||
if res != b'this >xxxxxxxxxxxxx':
|
||||
return 'Block test fail 2:' + res
|
||||
return 'Block test fail 2:' + str(list(res))
|
||||
start = bs
|
||||
end = bs + len(d1)
|
||||
eep[start : end] = d1
|
||||
|
@ -57,7 +57,7 @@ def _testblock(eep, bs):
|
|||
end = start + len(d2)
|
||||
res = eep[start : end]
|
||||
if res != d2:
|
||||
return 'Block test fail 3:' + res
|
||||
return 'Block test fail 3:' + str(list(res))
|
||||
|
||||
def test(stm=False):
|
||||
eep = get_eep(stm)
|
||||
|
|
Ładowanie…
Reference in New Issue