diff --git a/BASE_CLASSES.md b/BASE_CLASSES.md new file mode 100644 index 0000000..4098546 --- /dev/null +++ b/BASE_CLASSES.md @@ -0,0 +1,163 @@ +# 1. Base classes for memory device drivers + +This doc is primarily to aid those wishing to use these base classes to write +drivers for additional memory devices. It describes the two classes in +`bdevice.py` namely `BlockDevice` and the subclass `FlashDevice`. Both provide +hardware-independent abstractions of memory devices. The base class provides +the API. This has the following characteristics: + 1. Support for single or multiple chips on the same bus. Multiple chips are + automatically configured as a single byte array. + 2. The byte array can be accessed using Python slice syntax. + 3. Alternatively the array can be formatted and mounted as a filesystem using + methods in the `uos` module. Any filesystem supported by the MicroPython build + may be employed: FAT and littlefs have been tested. The latter is recommended. + +The `BlockDevice` class supports byte-addressable technologies such as EEPROM +and FRAM. Such devices can be written on a single byte basis. Where a chip also +offers multi-byte writes this optimisation can be handled in the user driver: +see the EEPROM drivers for examples of this. + +`FlashDevice` subclasses `BlockDevice` to support devices which must buffer a +sector of data for writing. The API continues to support byte addressing: this +is achieved by modifying the buffer contents and writing it out when necessary. + +# 2. The BlockDevice class + +The class provides these characteristics: + 1. An API which represents multiple physical devices as a single byte array. + The physical means of achieving this is provided in the hardware subclass. + 2. An implementation of the `AbstractBlockDev` protocol with extended + interface as required by littlefs as documented + [here](http://docs.micropython.org/en/latest/library/uos.html). + 3. An API based on Python slice notation for byte level access to the array. + 4. Support for the `len` operator. + +## 2.1 Constructor + +Constructor args - mandatory, positional, integer + 1. `nbits` Block size reported to the filesystem expressed as a number of + bits: the block size is `2^nbits`. The usual value is 9 (512 bit block). + 2. `nchips` Number of chips in the array. + 3. `chip_size` Size of each chip in bytes. + +## 2.2 Necessary subclass support + +The subclass must provide a method `readwrite` taking the following args: + 1. `addr` Address relative to the start of the array. + 2. `buf` A buffer holding data to write or to contain data to be read. + 3. `read` Boolean: `True` to read, `False` to write. + +The amount of data read or written is defined by the length of the buffer. + +Return value: the buffer. + +The method must handle the case where a buffer crosses chip boundaries. This +involves physical accesses to each chip and reading or writing partial buffer +contents. Addresses are converted by the method to chip-relative addresses. + +## 2.3 The `AbstractBlockDev` protocol + +This is provided by the following methods: + 1. `sync()` In the `BlockDevice` class this does nothing. It is defined in the + `FlashDevice` class [section 3.3](./BASE_CLASSES.md#33-methods). + 2. `readblocks(blocknum, buf, offset=0)` Converts the block address and offset + to an absolute address into the array and calls `readwrite`. + 3. `writeblocks(blocknum, buf, offset=0` Works as above. + 4. `ioctl` This supports the following operands: + + 3. `sync` Calls the `.sync()` method. + 4. `sector count` Returns `chip_size` * `nchips` // `block_size` + 5. `block size` Returns block size calculated as in section 2.1. + 6. `erase` Necessary for correct filesystem operation: returns 0. + +The drivers make no use of the block size: it exists only for filesystems. The +`readwrite` method hides any physical device structure presenting an array of +bytes. The specified block size must match the intended filesystem. Littlefs +requires >=128 bytes, FATFS requires >=512 bytes. All testing was done with 512 +byte blocks. + +## 2.4 Byte level access + +This is provided by `__getitem__` and `__setitem__`. The `addr` arg can be an +integer or a slice, enabling the following syntax examples: +```python +a = eep[1000] # Read a single byte +eep[1000] = 42 # write a byte +eep[1000:1004] = b'\x11\x22\x33\x44' # Write 4 consecutive bytes +b = eep[1000:1004] # Read 4 consecutive bytes +``` +The last example necessarily performs allocation in the form of a buffer for +the resultant data. Applications can perform allocation-free reading by calling +the `readwrite` method directly. + +## 2.5 The len operator + +This returns the array size in bytes. + +# 3. The FlashDevice class + +By subclassing `BlockDevice`, `FlashDevice` provides the same API for flash +devices. At a hardware level reading is byte addressable in a similar way to +EEPROM and FRAM devices. These chips do not support writing arbitrary data to +individual byte addresses. Writing is done by erasing a block, then rewriting +it with new contents. To provide logical byte level writing it is necessary to +read and buffer the block containing the byte, update the byte, erase the block +and write out the buffer. + +In practice this would be slow and inefficient - erasure is a slow process and +results in wear. The `FlashDevice` class defers writing the buffer until it is +necessary to buffer a different block. + +The class caches a single sector. In currently supported devices this is 4KiB +of RAM. This is adequate for littlefs, however under FATFS wear can be reduced +by cacheing more than one sector. These drivers are primarily intended for +littlefs with its wear levelling design. + +## 3.1 Constructor + +Constructor args - mandatory, positional, integer + 1. `nbits` Block size reported to the filesystem expressed as a number of + bits: the block size is `2^nbits`. The usual value is 9 (512 bit block). + 2. `nchips` Number of chips in the array. + 3. `chip_size` Size of each chip in bytes. + 4. `sec_size` Physical sector size of the device in bytes. + +## 3.2 Necessary subclass support + +A subclass supporting a flash device must provide the following methods: + 1. `readwrite(addr, buf, read)` Args as defined in section 2.2. This calls the + `.read` or `.write` methods of `FlashDevice` as required. + 2. `rdchip(addr, mvb)` Args `addr`: address into the array, `mvb` a + `memoryview` into a buffer for read data. This reads from the chip into the + `memoryview`. + 3. `flush(cache, addr)` Args `cache` a buffer holding one sector of data, + `addr` address into the array of the start of a physical sector. Erase the + sector and write out the data in `cache`. + +The constructor must call `initialise()` after the hardware has been +initialised to ensure valid cache contents. + +## 3.3 Methods + + 1. `read(addr, mvb`) Args `addr` address into array, `mvb` a `memoryview` into + a buffer. Fills the `memoryview` with data read. If some or all of the data is + cached, the cached data is provided. + 2. `write(addr, mvb`) Args `addr` address into array, `mvb` a `memoryview` + into a buffer. If the address range is cached, the cache contents are updated. + More generally the currently cached data is written out using `flush`, a new + sector is cached, and the contents updated. Depending on the size of the data + buffer this may occur multiple times. + 3. `sync()` This flushes the current cache. An optimisation is provided by the + `._dirty` flag. This ensures that the cache is only flushed if its contents + have been modified since it was last written out. + 4. `is_empty(addr, ev=0xff)` Arg: `addr` start address of a sector. Reads the + sector returning `True` if all bytes match `ev`. Enables a subclass to avoid + erasing a sector which is already empty. + 5. `initialise()` Called by the subclass constructor to populate the cache + with the contents of sector 0. + +# 4. References + +[uos docs](http://docs.micropython.org/en/latest/library/uos.html) +[Custom block devices](http://docs.micropython.org/en/latest/reference/filesystem.html#custom-block-devices) +[Littlefs](https://github.com/ARMmbed/littlefs/blob/master/DESIGN.md) diff --git a/README.md b/README.md index 38c8119..6b26a11 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ These drivers support nonvolatile memory chips and the littlefs filesystem. +Now includes support for 256 and 512KiB FRAM devices. + 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 @@ -66,6 +68,8 @@ In the table below the Interface column includes page size in bytes. | Microchip | 24xx256 | I2C 128 | 32KiB | EEPROM | [I2C.md](./eeprom/i2c/I2C.md) | | Microchip | 24xx128 | I2C 128 | 16KiB | EEPROM | [I2C.md](./eeprom/i2c/I2C.md) | | Microchip | 24xx64 | I2C 128 | 8KiB | EEPROM | [I2C.md](./eeprom/i2c/I2C.md) | +| Adafruit | 4719 | SPI n/a | 512KiB | FRAM | [FRAM_SPI.md](./fram/FRAM_SPI.md) | +| Adafruit | 4718 | SPI n/a | 256KiB | FRAM | [FRAM_SPI.md](./fram/FRAM_SPI.md) | | Adafruit | 1895 | I2C n/a | 32KiB | FRAM | [FRAM.md](./fram/FRAM.md) | The flash driver now has the capability to support a variety of chips. The @@ -85,7 +89,7 @@ for compatibility. ## 1.5 Performance FRAM is truly byte-addressable: its speed is limited only by the speed of the -I2C interface. +I2C or SPI interface (SPI being much faster). Reading from EEPROM chips is fast. Writing is slower, typically around 5ms. However where multiple bytes are written, that 5ms applies to a page of data so diff --git a/fram/FRAM_SPI.md b/fram/FRAM_SPI.md new file mode 100644 index 0000000..597ceaf --- /dev/null +++ b/fram/FRAM_SPI.md @@ -0,0 +1,241 @@ +# 1. A MicroPython SPI FRAM driver + +A driver to enable the Pyboard to access Ferroelectric RAM (FRAM) boards from +Adafruit, namely [the 256KiB board](https://www.adafruit.com/product/4718) and +[the 512KiB board](https://www.adafruit.com/product/4719). FRAM is a technology +offering nonvolatile memory with extremely long endurance and fast access, +avoiding the +limitations of Flash memory. Its endurance is specified as 10**13 writes, +contrasted with 10,000 which is the quoted endurance of the Pyboard's onboard +Flash memory. In data logging applications the latter can be exceeded relatively +rapidly. Flash writes can be slow because of the need for a sector erase: this +is not a fast process. FRAM is byte addressable and is not subject to this +limitation. Compared to a Micro SD card fitted to the Pyboard it offers lower +power consumption and longer endurance, albeit at a smaller capacity. + +An arbitrary number of boards may be used to construct a nonvolatile memory +array with size from 256KiB upwards. The driver allows the memory either to be +mounted in the Pyboard filesystem as a disk device or to be addressed as an +array of bytes. + +For users interested in the technology [this](https://www.mouser.com/pdfDOCS/cypress-fram-whitepaper.pdf) +is worth reading. Clue: the FRAM cell contains no iron. + +##### [Main readme](../README.md) + +# 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 FRAM BOARD, connect to a Pyboard +as below (n/c indicates no connection): + +| FRAM Signal | PB | Signal | +|:-----------:|:---:|:------:| +| Vin | 3V3 | 3V3 | +| 3V3 | n/c | n/c | +| Gnd | Gnd | Gnd | +| SCK | Y6 | SCK | +| MISO | Y7 | MISO | +| MOSI | Y8 | MOSI | +| CS | Y5 | SS/ | +| WP/ | n/c | n/c | +| HOLD/ | n/c | n/c | + +For multiple boards a separate CS pin must be assigned to each one: each pin +must be wired to a single board's CS line. Multiple boards should have Vin, Gnd, +SCK, MOSI and MISO lines wired in parallel. + +If you use a Pyboard D and power the devicess from the 3V3 output you will need +to enable the voltage rail by issuing: +```python +machine.Pin.board.EN_3V3.value(1) +time.sleep(0.1) # Allow decouplers to charge +``` +Other platforms may vary. + +At the time of writing schematics for the Adafruit boards were unavailable but +measurement indicated that CS, WP/ and HOLD/ are pulled up with 10KΩ. It is +therefore safe to leave WP/ and HOLD/ unconnected, and CS will behave properly +at power-up. + +# 3. Files + + 1. `fram_spi.py` Device driver. + 2. `bdevice.py` (In root directory) Base class for the device driver. + 3. `fram_spi_test.py` Test programs for above. Assumes two 512KiB boards with + CS connected to pins Y4 and Y5 respectively. Adapt for other configurations. + +Installation: copy files 1 and 2 (optionally 3) to the target filesystem. + +# 4. The device driver + +The driver supports mounting the FRAM 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 one or more devices and also assumes the +littlefs filesystem: + +```python +import os +from machine import SPI, Pin +from fram_spi import FRAM +cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1),) +fram = FRAM(SPI(2, baudrate=25_000_000), cspins) +# Format the filesystem +os.VfsLfs2.mkfs(fram) # Omit this to mount an existing filesystem +os.mount(fram,'/fram') +``` +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. The chips are +specified to a baudrate of 40MHz. I tested on a Pyboard D, specifying 25MHz - +this produced an actual baudrate of 18MHz. + +## 4.1 The FRAM class + +An `FRAM` instance represents a logical FRAM: this may consist of multiple +physical devices on a common SPI bus. + +### 4.1.1 Constructor + +This checks each CS line for an attached board of the correct type and of the +specified size. A `RuntimeError` will occur in case of error, e.g. bad ID, no +device detected or size not matching that specified to the constructor. If all +is OK an FRAM instance is created. + +Arguments: + 1. `spi` Mandatory. An initialised SPIbus 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=512` Chip size in KiB. + 4. `verbose=True` If `True`, the constructor issues information on the FRAM + devices it has detected. + 5. `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. +Arrays will be somewhat faster owing to more efficient bus utilisation. + +#### 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 fram_spi import FRAM +cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1),) +fram = FRAM(SPI(2), cspins) +fram[2000] = 42 +print(fram[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 fram_spi import FRAM +cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1),) +fram = FRAM(SPI(2), cspins) +fram[2000:2002] = bytearray((42, 43)) +print(fram[2000:2002]) # Returns a bytearray +``` +Three argument slices are not supported: a third arg (other than 1) will cause +an exception. One argument slices (`fram[:5]` or `fram[32760:]`) and negative +args are supported. + +#### 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 + +#### The len() operator + +The size of the FRAM array in bytes may be retrieved by issuing `len(fram)` +where `fram` is the `FRAM` instance. + +### 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 fram_spi_test.py + +This assumes a Pyboard 1.x or Pyboard D with FRAM(s) wired as above. 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 128 byte page with +random data, reads it back, and checks the outcome. Existing array data will be +lost. + +## 5.3 fstest(format=False) + +If `True` is passed, formats the FRAM array as a FAT filesystem and mounts +the device on `/fram`. 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 FRAM becoming +full) it is up to the caller to handle it. For example (assuming the FRAM is +mounted on /fram): + +```python +cp('/flash/main.py','/fram/') +``` + +See `upysh` in [micropython-lib](https://github.com/micropython/micropython-lib.git) +for other filesystem tools for use at the REPL. + +# 6. Low power operation + +In the absence of an SPI clock signal the chip is specified to draw 50μA max. +This can be reduced to 8μA max by issuing a sleep command. Code to support this +is provided in `fram_spi.py` but is commented out; it is a somewhat specialised +requirement. + +# 7. References + +[256KiB Adafruit board](http://www.adafruit.com/product/4718) +[512KiB Adafruit board](http://www.adafruit.com/product/4719) +[256KiB Chip datasheet](https://cdn-shop.adafruit.com/product-files/4718/4718_MB85RS2MTA.pdf) +[512KiB Chip datasheet](https://cdn-shop.adafruit.com/product-files/4719/4719_MB85RS4MT.pdf) +[Technology](https://www.mouser.com/pdfDOCS/cypress-fram-whitepaper.pdf) diff --git a/fram/fram_fs_test.py b/fram/fram_fs_test.py new file mode 100644 index 0000000..88d3af2 --- /dev/null +++ b/fram/fram_fs_test.py @@ -0,0 +1,77 @@ +# littlefs_test.py Extended filesystem test of FRAM devices +# Create multiple binary files of varying length and verify that they can be +# read back correctly. Rewrite files with new lengths then check that all files +# are OK. + +import uos +from machine import SPI, Pin +from fram_spi_test import get_fram + +directory = '/fram' +a = bytearray(range(256)) +b = bytearray(256) +files = {} # n:length +errors = 0 + +def fname(n): + return '{}/{:05d}'.format(directory, n + 1) # Names start 00001 + +def fcreate(n): # Create a binary file of random length + length = int.from_bytes(uos.urandom(2), 'little') + 1 # 1-65536 bytes + length &= 0x3ff # 1-1023 for FRAM + linit = length + with open(fname(n), 'wb') as f: + while(length): + nw = min(length, 256) + f.write(a[:nw]) + length -= nw + files[n] = length + return linit + +def fcheck(n): + length = files[n] + with open(fname(n), 'rb') as f: + while(length): + nr = f.readinto(b) + if not nr: + return False + if a[:nr] != b[:nr]: + return False + length -= nr + return True + +def check_all(): + global errors + for n in files: + if fcheck(n): + print('File {:d} OK'.format(n)) + else: + print('Error in file', n) + errors += 1 + print('Total errors:', errors) + + +def remove_all(): + for n in files: + uos.remove(fname(n)) + +def main(): + fram = get_fram() + try: + uos.mount(fram, directory) + except OSError: # Already mounted + pass + for n in range(128): + length = fcreate(n) + print('Created', n, length) + print('Created files', files) + check_all() + for _ in range(100): + for x in range(5): # Rewrite 5 files with new lengths + n = int.from_bytes(uos.urandom(1), 'little') & 0x7f + length = fcreate(n) + print('Rewrote', n, length) + check_all() + remove_all() + +print('main() to run littlefs test. Filesystem must exist.') diff --git a/fram/fram_spi.py b/fram/fram_spi.py new file mode 100644 index 0000000..756e7bb --- /dev/null +++ b/fram/fram_spi.py @@ -0,0 +1,133 @@ +# fram_spi.py Supports Fujitsu 256KiB and 512KiB FRAM devices +# M85RS2MT Adafruit https://www.adafruit.com/product/4718 +# M85RS4MT Adafruit https://www.adafruit.com/product/4719 + +# These chips are almost identical. Command sets are identical. +# Product ID 1st byte, LS 4 bits is density 0x8 == 2MiB 0x9 == 4MiB + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +from micropython import const +from bdevice import BlockDevice +# import time # for sleep command + +# Command set +_WREN = const(6) +_WRDI = const(4) +_RDSR = const(5) # Read status reg +_WRSR = const(1) +_READ = const(3) +_WRITE = const(2) +_RDID = const(0x9f) +# _FSTRD = const(0x0b) No obvious difference to _READ +_SLEEP = const(0xb9) + + +class FRAM(BlockDevice): + def __init__(self, spi, cspins, size=512, verbose=True, block_size=9): + if size not in (256, 512): + raise ValueError('FRAM size must be 256 or 512') + super().__init__(block_size, len(cspins), size * 1024) + self._spi = spi + self._cspins = cspins + self._ccs = None # Chip select Pin object for current chip + self._bufp = bytearray(5) # instruction + 3 byte address + 1 byte value + mvp = memoryview(self._bufp) # cost-free slicing + self._mvp = mvp + # Check hardware + density = 8 if size == 256 else 9 + for n, cs in enumerate(cspins): + mvp[:] = b'\0\0\0\0\0' + mvp[0] = _RDID + cs(0) + self._spi.write_readinto(mvp, mvp) + cs(1) + # Ignore bits labelled "proprietary" + if mvp[1] != 4 or mvp[2] != 0x7f: + s = 'FRAM not found at cspins[{}].' + raise RuntimeError(s.format(n)) + if (mvp[3] & 0x1f) != density: + s = 'FRAM at cspins[{}] is incorrect size.' + raise RuntimeError(s.format(n)) + if verbose: + s = 'Total FRAM size {} bytes in {} devices.' + print(s.format(self._a_bytes, n + 1)) + # Set up status register on each chip + for cs in cspins: + self._wrctrl(cs, True) + mvp[0] = _WRSR + mvp[1] = 0 # No block protect or SR protect + cs(0) + self._spi.write(mvp[:2]) + cs(1) + self._wrctrl(cs, False) # Disable write to array + + for n, cs in enumerate(self._cspins): + mvp[0] = _RDSR + cs(0) + self._spi.write_readinto(mvp[:2], mvp[:2]) + cs(1) + if mvp[1]: + s = 'FRAM has bad status at cspins[{}].' + raise RuntimeError(s.format(n)) + + def _wrctrl(self, cs, en): # Enable/Disable device write + mvp = self._mvp + mvp[0] = _WREN if en else _WRDI + cs(0) + self._spi.write(mvp[:1]) + cs(1) + + #def sleep(self, on): + #mvp = self._mvp + #mvp[0] = _SLEEP + #for cs in self._cspins: + #cs(0) + #if on: + #self._spi.write(mvp[:1]) + #else: + #time.sleep_us(500) + #cs(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("FRAM 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 >> 16 + mvp[2] = (la >> 8) & 0xff + mvp[3] = la & 0xff + pe = (addr & ~0xff) + 0x100 # byte 0 of next chip + return min(nbytes, pe - la) + + # Interface to bdevice + def readwrite(self, addr, buf, read): + nbytes = len(buf) + mvb = memoryview(buf) + mvp = self._mvp + start = 0 # Offset into buf. + while nbytes > 0: + npage = self._getaddr(addr, nbytes) # No of bytes that fit on current chip + cs = self._ccs + if read: + mvp[0] = _READ + cs(0) + self._spi.write(mvp[:4]) + self._spi.readinto(mvb[start : start + npage]) + cs(1) + else: + self._wrctrl(cs, True) + mvp[0] = _WRITE + cs(0) + self._spi.write(mvp[:4]) + self._spi.write(mvb[start: start + npage]) + cs(1) + self._wrctrl(cs, False) + nbytes -= npage + start += npage + addr += npage + return buf diff --git a/fram/fram_spi_test.py b/fram/fram_spi_test.py new file mode 100644 index 0000000..b840266 --- /dev/null +++ b/fram/fram_spi_test.py @@ -0,0 +1,131 @@ +# fram_spi_ test.py MicroPython test program for Adafruit SPI FRAM devices. + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +import uos +import time +from machine import SPI, Pin +from fram_spi import FRAM + +cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) + +# Return an FRAM array. Adapt for platforms other than Pyboard. +def get_fram(): + if uos.uname().machine.split(' ')[0][:4] == 'PYBD': + Pin.board.EN_3V3.value(1) + time.sleep(0.1) # Allow decouplers to charge + fram = FRAM(SPI(2, baudrate=25_000_000), cspins, size=512) # Change size as required + print('Instantiated FRAM') + return fram + +# Dumb file copy utility to help with managing FRAM contents at the REPL. +def cp(source, dest): + if dest.endswith('/'): # minimal way to allow + dest = ''.join((dest, source.split('/')[-1])) # cp /sd/file /fram/ + 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(): + fram = get_fram() + sa = 1000 + for v in range(256): + fram[sa + v] = v + for v in range(256): + if fram[sa + v] != v: + print('Fail at address {} data {} should be {}'.format(sa + v, fram[sa + v], v)) + break + else: + print('Test of byte addressing passed') + data = uos.urandom(30) + sa = 2000 + fram[sa:sa + 30] = data + if fram[sa:sa + 30] == data: + print('Test of slice readback passed') + # On FRAM the only meaningful block test is on a chip boundary. + block = fram._c_bytes + if fram._a_bytes > block: + res = _testblock(fram, 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): + fram = get_fram() + if format: + uos.VfsFat.mkfs(fram) + vfs=uos.VfsFat(fram) + try: + uos.mount(vfs,'/fram') + except OSError: # Already mounted + pass + print('Contents of "/": {}'.format(uos.listdir('/'))) + print('Contents of "/fram": {}'.format(uos.listdir('/fram'))) + print(uos.statvfs('/fram')) + +def cptest(): + fram = get_fram() + if 'fram' in uos.listdir('/'): + print('Device already mounted.') + else: + vfs=uos.VfsFat(fram) + try: + uos.mount(vfs,'/fram') + except OSError: + print('Fail mounting device. Have you formatted it?') + return + print('Mounted device.') + cp('fram_spi_test.py', '/fram/') + cp('fram_spi.py', '/fram/') + print('Contents of "/fram": {}'.format(uos.listdir('/fram'))) + print(uos.statvfs('/fram')) + +# ***** TEST OF HARDWARE ***** +def full_test(): + fram = get_fram() + page = 0 + for sa in range(0, len(fram), 256): + data = uos.urandom(256) + fram[sa:sa + 256] = data + if fram[sa:sa + 256] == data: + print('Page {} passed'.format(page)) + else: + print('Page {} readback failed.'.format(page)) + page += 1