From 49991d1e02b3c24b69a2e1c16f75a02c8bd9377b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 24 Dec 2019 15:16:09 +0000 Subject: [PATCH] First pass at flash driver. --- README.md | 86 +++++++++------ bdevice.py | 81 +++++++++++++- flash/FLASH.md | 257 ++++++++++++++++++++++++++++++++++++++++++++ flash/flash_spi.py | 164 ++++++++++++++++++++++++++++ flash/flash_test.py | 138 ++++++++++++++++++++++++ fram/fram_test.py | 6 +- i2c/I2C.md | 6 +- i2c/eep_i2c.py | 6 +- spi/SPI.md | 6 +- spi/eep_spi.py | 6 +- 10 files changed, 708 insertions(+), 48 deletions(-) create mode 100644 flash/FLASH.md create mode 100644 flash/flash_spi.py create mode 100644 flash/flash_test.py diff --git a/README.md b/README.md index 219e263..e2a4c30 100644 --- a/README.md +++ b/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. diff --git a/bdevice.py b/bdevice.py index 7e25c2a..4c7c077 100644 --- a/bdevice.py +++ b/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) diff --git a/flash/FLASH.md b/flash/FLASH.md new file mode 100644 index 0000000..9621cb8 --- /dev/null +++ b/flash/FLASH.md @@ -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. diff --git a/flash/flash_spi.py b/flash/flash_spi.py new file mode 100644 index 0000000..9c1c885 --- /dev/null +++ b/flash/flash_spi.py @@ -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 diff --git a/flash/flash_test.py b/flash/flash_test.py new file mode 100644 index 0000000..4091e46 --- /dev/null +++ b/flash/flash_test.py @@ -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'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 diff --git a/fram/fram_test.py b/fram/fram_test.py index a093021..deb7318 100644 --- a/fram/fram_test.py +++ b/fram/fram_test.py @@ -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() diff --git a/i2c/I2C.md b/i2c/I2C.md index 8ec10ad..b9bf638 100644 --- a/i2c/I2C.md +++ b/i2c/I2C.md @@ -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() diff --git a/i2c/eep_i2c.py b/i2c/eep_i2c.py index b78869d..7d2088e 100644 --- a/i2c/eep_i2c.py +++ b/i2c/eep_i2c.py @@ -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() diff --git a/spi/SPI.md b/spi/SPI.md index cce76c1..9bc715a 100644 --- a/spi/SPI.md +++ b/spi/SPI.md @@ -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) diff --git a/spi/eep_spi.py b/spi/eep_spi.py index dafa1d8..4bf5e97 100644 --- a/spi/eep_spi.py +++ b/spi/eep_spi.py @@ -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)