From 985c09ae4faea2903dd7ddae37b31ca9388c3502 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 17 Dec 2019 17:10:43 +0000 Subject: [PATCH] Add FRAM. Various minor changes. --- README.md | 93 ++++++++++++++---- bdevice.py | 18 +++- fram/FRAM.md | 246 ++++++++++++++++++++++++++++++++++++++++++++++ fram/fram_i2c.py | 91 +++++++++++++++++ fram/fram_test.py | 127 ++++++++++++++++++++++++ i2c/I2C.md | 55 +++++++++-- i2c/eep_i2c.py | 64 ++++++++++++ i2c/eeprom_i2c.py | 27 ++--- spi/SPI.md | 61 ++++++++++-- spi/eep_spi.py | 67 ++++++++++++- spi/eeprom_spi.py | 21 ++-- 11 files changed, 800 insertions(+), 70 deletions(-) create mode 100644 fram/FRAM.md create mode 100644 fram/fram_i2c.py create mode 100644 fram/fram_test.py diff --git a/README.md b/README.md index 627545c..ef9449e 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,76 @@ -# MicroPython EEPROM drivers +# 1. MicroPython drivers for nonvolatile memory -EEPROM is a form of nonvolatile random access storage. +These drivers support nonvolatile memory chips. -These drivers enable MicroPython to access Microchip EEPROM devices. There are -two variants, one for chips based on the I2C interface and a second for a 1MBit -SPI chip. +Currently supported devices use 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. -Unlike flash memory, EEPROMs may be written on a byte addressable basis. Their -endurance is specified as a million writes compared to the 10K typical of most -flash memory. In applications such as data logging the latter can be exceeded -relatively rapidly. For extreme endurance ferroelectric RAM has almost infinite -endurance but at higher cost per byte. See [this driver](https://github.com/peterhinch/micropython-fram). +The drivers present a common API having the features listed below. + +## 1.1 Features common to all drivers + +The drivers have the following common features: + 1. Support for single or multiple chips on the same bus. Multiple chips are + automatically configured as a single array. + 2. This can be accessed as an array of bytes, using Python slice syntax or via + a `readwrite` method. + 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. + 4. Drivers are portable: buses and pins should be instantiated using the + `machine` module. + 5. Buses may be shared with other hardware. This assumes that the application + pays due accord to differing electrical constraints such as baudrate. + +## 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 1M 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. + +## 1.3 Supported chips + +These currently include Microchip EEPROM chips and +[this Adafruit FRAM board](http://www.adafruit.com/product/1895). Note that the +largest EEPROM chip uses SPI: see [below](./README.md#2-choice-of-interface) +for a discussion of the merits and drawbacks of each interface. + +Supported devices. Microchip manufacture each chip in different variants with +letters denoted by "xx" below. The variants cover parameters such as minimum +Vcc value and do not affect the API. + +In the table below the Interface column includes page size in bytes. +| Manufacurer | Part | Interface | Bytes | Technology | Docs | +|:-----------:|:--------:|:---------:|:------:|:----------:|:------:| +| 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) | + +## 1.4 Performance + +FRAM is truly byte-addressable: its speed is limited only by the speed of the +I2C interface. 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 the mean time per byte is quicker by a factor of the page size (128 or 256 bytes depending on the device). -The drivers support creating multi-chip arrays. In the case of I2C chips, up to -eight devices may share the bus. In the case of SPI expansion has no absolute -limit as each chip has its own chip select line. +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. -Devices or arrays of devices may be mounted as a filesystem or may be treated -as an array of bytes. - -For I2C devices see [I2C.md](./i2c/I2C.md). For SPI see [SPI.md](./spi/SPI.md). - -# Choice of interface +# 2. Choice of interface The principal merit of I2C is to minimise pin count. It uses two pins regardless of the number of chips connected. It requires pullup resistors on @@ -41,3 +85,14 @@ 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. + +# 3. Design details + +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. + +The nature of the drivers in this repo implies that the block address in the +ioctl is arbitrary. diff --git a/bdevice.py b/bdevice.py index ee447f1..309982f 100644 --- a/bdevice.py +++ b/bdevice.py @@ -23,7 +23,7 @@ class BlockDevice: return self._a_bytes # Handle special cases of a slice. Always return a pair of positive indices. - def do_slice(self, addr): + def _do_slice(self, addr): if not (addr.step is None or addr.step == 1): raise NotImplementedError('only slices with step=1 (aka None) are supported') start = addr.start if addr.start is not None else 0 @@ -32,6 +32,22 @@ class BlockDevice: stop = stop if stop >= 0 else self._a_bytes + stop return start, stop + def wslice(self, addr, value): + start, stop = self._do_slice(addr) + try: + if len(value) == (stop - start): + res = self.readwrite(start, value, False) + else: + raise RuntimeError('Slice must have same length as data') + except TypeError: + raise RuntimeError('Can only assign bytes/bytearray to a slice') + return res + + def rslice(self, addr): + start, stop = self._do_slice(addr) + buf = bytearray(stop - start) + return self.readwrite(start, buf, True) + # IOCTL protocol. def readblocks(self, blocknum, buf, offset=0): return self.readwrite(offset + (blocknum << self._nbits), buf, True) diff --git a/fram/FRAM.md b/fram/FRAM.md new file mode 100644 index 0000000..98e02ec --- /dev/null +++ b/fram/FRAM.md @@ -0,0 +1,246 @@ +# 1. A MicroPython FRAM driver + +A driver to enable the Pyboard to access the Ferroelectric RAM (FRAM) board from +[Adafruit](http://www.adafruit.com/product/1895). 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**12 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. The downside is limited capacity. Compared to a Micro SD card fitted +to the Pyboard it offers lower power consumption and longer endurance. + +From one to eight boards may be used to construct a nonvolatile memory module +with size ranging from 32KiB to 256KiB. 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. + +## 1.1 Changes compared to the old FRAM driver + +API now matches other devices with support for slice syntax. Reduced RAM +allocation by virtue of `memorview` instances and pre-allocated buffers. Now +supports littlefs or FAT filesystems. + +# 2. Connections + +To wire up a single FRAM module, connect to the Pyboard as below (nc indicates +no connection). + +| FRAM | L | R | +|:-------:|:---:|:---:| +| Vcc | 3V3 | 3V3 | +| Gnd | GND | GND | +| WP | nc | nc | +| SCL | X9 | Y9 | +| SDA | X10 | Y10 | +| A2 | nc | nc | +| A1 | nc | nc | +| A0 | nc | nc | + +For multiple modules the address lines A0, A1 and A2 of each module need to be +wired to 3V3 in such a way as to give each device a unique address. These must +start at zero and be contiguous. Pins are internally pulled down, pins marked +`nc` may be left unconnected or linked to Gnd. +| Chip no. | A2 | A1 | A0 | +|:--------:|:---:|:---:|:---:| +| 0 | nc | nc | nc | +| 1 | nc | nc | 3V3 | +| 2 | nc | 3V3 | nc | +| 3 | nc | 3V3 | 3V3 | +| 4 | 3V3 | nc | nc | +| 5 | 3V3 | nc | 3V3 | +| 6 | 3V3 | 3V3 | nc | +| 7 | 3V3 | 3V3 | Gnd | + +Multiple modules should have 3V3, Gnd, SCL and SDA lines wired in parallel. + +The I2C interface requires pullups: these are provided on the Adafruit board. + +If you use a Pyboard D and power the FRAMs 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. + +# 3. Files + + 1. `fram_i2c.py` Device driver. + 2. `bdevice.py` (In root directory) Base class for the device driver. + 3. `fram_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 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: + +```python +import uos +from machine import I2C +from fram_i2c import FRAM +fram = FRAM(I2C(2)) +uos.VfsFat.mkfs(fram) # Omit this to mount an existing filesystem +vfs = uos.VfsFat(fram) +uos.mount(vfs,'/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 I2C bus must be instantiated using the `machine` module. + +## 4.1 The FRAM class + +An `FRAM` instance represents a logical FRAM: this may consist of multiple +physical devices on a common I2C bus. + +### 4.1.1 Constructor + +This scans the I2C bus and checks if one or more correctly addressed chips are +detected. Each chip is checked for correct ID data. A `RuntimeError` will occur +in case of error, e.g. bad ID, no device detected or device address lines not +wired as described in [Connections](./README.md#2-connections). If all is OK an +FRAM instance is created. + +Arguments: + 1. `i2c` Mandatory. An initialised master mode I2C bus created by `machine`. + 2. `verbose=True` If `True`, the constructor issues information on the FRAM + devices it has detected. + 3. `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 I2C +from fram_i2c import FRAM +fram = FRAM(I2C(2)) +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 I2C +from fram_i2c import FRAM +fram = FRAM(I2C(2)) +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. + +#### scan + +Scans the I2C bus and returns the number of FRAM devices detected. + +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(s). + +### 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_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. ESP8266 + +Currently the ESP8266 does not support concurrent mounting of multiple +filesystems. Consequently the onboard flash must be unmounted (with +`uos.umount()`) before the FRAM can be mounted. + +# 7. References + +[Adafruit board](http://www.adafruit.com/product/1895) +[Chip datasheet](https://cdn-learn.adafruit.com/assets/assets/000/043/904/original/MB85RC256V-DS501-00017-3v0-E.pdf?1500009796) +[Technology](https://www.mouser.com/pdfDOCS/cypress-fram-whitepaper.pdf) diff --git a/fram/fram_i2c.py b/fram/fram_i2c.py new file mode 100644 index 0000000..ce4784d --- /dev/null +++ b/fram/fram_i2c.py @@ -0,0 +1,91 @@ +# fram_i2c.py Driver for Adafruit 32K Ferroelectric RAM module (Fujitsu MB85RC256V) + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2019 Peter Hinch + +from micropython import const +from bdevice import BlockDevice + +_SIZE = const(32768) # Chip size 32KiB +_ADDR = const(0x50) # FRAM I2C address 0x50 to 0x57 +_FRAM_SLAVE_ID = const(0xf8) # FRAM device ID location +_MANF_ID = const(0x0a) +_PRODUCT_ID = const(0x510) + + +# A logical ferroelectric RAM made up of from 1 to 8 chips +class FRAM(BlockDevice): + def __init__(self, i2c, verbose=True, block_size=9): + self._i2c = i2c + self._buf1 = bytearray(1) + self._addrbuf = bytearray(2) # Memory offset into current chip + self._buf3 = bytearray(3) + self._nchips = self.scan(verbose, _SIZE) + super().__init__(block_size, self._nchips, _SIZE) + self._i2c_addr = None # i2c address of current chip + + def scan(self, verbose, chip_size): + devices = self._i2c.scan() + chips = [d for d in devices if d in range(_ADDR, _ADDR + 8)] + nchips = len(chips) + if nchips == 0: + raise RuntimeError('FRAM not found.') + if min(chips) != _ADDR or (max(chips) - _ADDR) >= nchips: + raise RuntimeError('Non-contiguous chip addresses', chips) + for chip in chips: + if not self._available(chip): + raise RuntimeError('FRAM at address 0x{:02x} reports an error'.format(chip)) + if verbose: + s = '{} chips detected. Total FRAM size {}bytes.' + print(s.format(nchips, chip_size * nchips)) + return nchips + + def _available(self, device_addr): + res = self._buf3 + self._i2c.readfrom_mem_into(_FRAM_SLAVE_ID >> 1, device_addr << 1, res) + manufacturerID = (res[0] << 4) + (res[1] >> 4) + productID = ((res[1] & 0x0F) << 8) + res[2] + return manufacturerID == _MANF_ID and productID == _PRODUCT_ID + + def __setitem__(self, addr, value): + if isinstance(addr, slice): + return self.wslice(addr, value) + self._buf1[0] = value + self._getaddr(addr, 1) + self._i2c.writevto(self._i2c_addr, (self._addrbuf, self._buf1)) + + def __getitem__(self, addr): + if isinstance(addr, slice): + return self.rslice(addr) + self._getaddr(addr, 1) + self._i2c.writeto(self._i2c_addr, self._addrbuf) + self._i2c.readfrom_into(self._i2c_addr, self._buf1) + return self._buf1[0] + + # In the context of FRAM a page == a chip. + # Args: an address and a no. of bytes. Set ._i2c_addr to correct chip. + # Return the no. of bytes available to access on that chip. + def _getaddr(self, addr, nbytes): # Set up _addrbuf and i2c_addr + 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._addrbuf[0] = (la >> 8) & 0xff + self._addrbuf[1] = la & 0xff + self._i2c_addr = _ADDR + ca + return min(nbytes, self._c_bytes - la) + + def readwrite(self, addr, buf, read): + nbytes = len(buf) + mvb = memoryview(buf) + start = 0 # Offset into buf. + while nbytes > 0: + npage = self._getaddr(addr, nbytes) # No of bytes that fit on current chip + if read: + self._i2c.writeto(self._i2c_addr, self._addrbuf) + self._i2c.readfrom_into(self._i2c_addr, mvb[start : start + npage]) # Sequential read + else: + self._i2c.writevto(self._i2c_addr, (self._addrbuf, buf[start: start + npage])) + nbytes -= npage + start += npage + addr += npage + return buf diff --git a/fram/fram_test.py b/fram/fram_test.py new file mode 100644 index 0000000..a093021 --- /dev/null +++ b/fram/fram_test.py @@ -0,0 +1,127 @@ +# fram_test.py MicroPython test program for Adafruit FRAM devices. + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2019 Peter Hinch + +import uos +from machine import I2C, Pin +from fram_i2c import FRAM + +# 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) + fram = FRAM(I2C(2)) + 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:' + 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:' + 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_test.py', '/fram/') + cp('fram_i2c.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 diff --git a/i2c/I2C.md b/i2c/I2C.md index cf8e37b..e2a2709 100644 --- a/i2c/I2C.md +++ b/i2c/I2C.md @@ -113,11 +113,13 @@ is detected or if device address lines are not wired as described in [Connections](./README.md#2-connections). Arguments: - 1. `i2c` Mandatory. An initialised master mode I2C bus. + 1. `i2c` Mandatory. An initialised master mode I2C bus created by `machine`. 2. `chip_size=T24C512` The chip size in bits. The module provides constants `T24C64`, `T24C128`, `T24C256`, `T24C512` for the supported chip sizes. - 3. `verbose=True` If True, the constructor issues information on the EEPROM + 3. `verbose=True` If `True`, the constructor issues information on the EEPROM devices it has detected. + 4. `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 @@ -149,7 +151,8 @@ print(eep[2000:2002]) # Returns a bytearray ``` Three argument slices are not supported: a third arg (other than 1) will cause an exception. One argument slices (`eep[:5]` or `eep[13100:]`) and negative -args are supported. +args are supported. See [section 4.2](./I2C.md#42-byte-addressing-usage-example) +for a typical application. #### 4.1.2.2 readwrite @@ -187,6 +190,36 @@ also [here](http://docs.micropython.org/en/latest/reference/filesystem.html#cust `writeblocks()` `ioctl()` +## 4.2 Byte addressing usage example + +A sample application: saving a configuration dict (which might be large and +complicated): +```python +import ujson +from machine import I2C +from eeprom_i2c import EEPROM, T24C512 +eep = EEPROM(I2C(2), T24C512) +d = {1:'one', 2:'two'} # Some kind of large object +wdata = ujson.dumps(d).encode('utf8') +sl = '{:10d}'.format(len(wdata)).encode('utf8') +eep[0 : len(sl)] = sl # Save data length in locations 0-9 +start = 10 # Data goes in 10: +end = start + len(wdata) +eep[start : end] = wdata +``` +After a power cycle the data may be read back. Instantiate `eep` as above, then +issue: +```python +slen = int(eep[:10].decode().strip()) # retrieve object size +start = 10 +end = start + slen +d = ujson.loads(eep[start : end]) +``` +It is much more efficient in space and performance to store data in binary form +but in many cases code simplicity matters, especially where the data structure +is subject to change. An alternative to JSON is the pickle module. It is also +possible to use JSON/pickle to store objects in a filesystem. + # 5. Test program eep_i2c.py This assumes a Pyboard 1.x or Pyboard D with EEPROM(s) wired as above. It @@ -195,12 +228,14 @@ 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. +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() -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. +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) @@ -208,7 +243,13 @@ 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. -## 5.4 File copy +## 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 diff --git a/i2c/eep_i2c.py b/i2c/eep_i2c.py index b41d67f..126f1c5 100644 --- a/i2c/eep_i2c.py +++ b/i2c/eep_i2c.py @@ -28,6 +28,33 @@ def cp(source, dest): if len(buf) < 100: break +# ***** TEST OF DRIVER ***** +def _testblock(eep, bs): + d0 = b'this >' + d1 = b'xxxxxxxxxxxxx': + return 'Block test fail 2:' + 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:' + res + def test(): eep = get_eep() sa = 1000 @@ -45,6 +72,25 @@ def test(): 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_eep() if format: @@ -58,6 +104,24 @@ def fstest(format=False): print('Contents of "/eeprom": {}'.format(uos.listdir('/eeprom'))) print(uos.statvfs('/eeprom')) +def cptest(): + eep = get_eep() + if 'eeprom' in uos.listdir('/'): + print('Device already mounted.') + else: + vfs=uos.VfsFat(eep) + try: + uos.mount(vfs,'/eeprom') + except OSError: + print('Fail mounting device. Have you formatted it?') + return + print('Mounted device.') + cp('eep_i2c.py', '/eeprom/') + cp('eeprom_i2c.py', '/eeprom/') + print('Contents of "/eeprom": {}'.format(uos.listdir('/eeprom'))) + print(uos.statvfs('/eeprom')) + +# ***** TEST OF HARDWARE ***** def full_test(): eep = get_eep() page = 0 diff --git a/i2c/eeprom_i2c.py b/i2c/eeprom_i2c.py index fb712ea..2c82801 100644 --- a/i2c/eeprom_i2c.py +++ b/i2c/eeprom_i2c.py @@ -7,7 +7,7 @@ import time from micropython import const from bdevice import BlockDevice -ADDR = const(0x50) # Base address of chip +_ADDR = const(0x50) # Base address of chip T24C512 = const(65536) # 64KiB 512Kbits T24C256 = const(32768) # 32KiB 256Kbits @@ -18,12 +18,12 @@ T24C64 = const(8192) # 8KiB 64Kbits # same size, and must have contiguous addresses starting from 0x50. class EEPROM(BlockDevice): - def __init__(self, i2c, chip_size=T24C512, verbose=True): + def __init__(self, i2c, chip_size=T24C512, verbose=True, block_size=9): self._i2c = i2c if chip_size not in (T24C64, T24C128, T24C256, T24C512): raise RuntimeError('Invalid chip size', chip_size) nchips = self.scan(verbose, chip_size) # No. of EEPROM chips - super().__init__(9, nchips, chip_size) + super().__init__(block_size, nchips, chip_size) self._i2c_addr = 0 # I2C address of current chip self._buf1 = bytearray(1) self._addrbuf = bytearray(2) # Memory offset into current chip @@ -31,11 +31,11 @@ class EEPROM(BlockDevice): # Check for a valid hardware configuration def scan(self, verbose, chip_size): devices = self._i2c.scan() # All devices on I2C bus - eeproms = [d for d in devices if ADDR <= d < ADDR + 8] # EEPROM chips + eeproms = [d for d in devices if _ADDR <= d < _ADDR + 8] # EEPROM chips nchips = len(eeproms) if nchips == 0: raise RuntimeError('EEPROM not found.') - if min(eeproms) != ADDR or (max(eeproms) - ADDR + 1) > nchips: + if min(eeproms) != _ADDR or (max(eeproms) - _ADDR) >= nchips: raise RuntimeError('Non-contiguous chip addresses', eeproms) if verbose: s = '{} chips detected. Total EEPROM size {}bytes.' @@ -55,14 +55,7 @@ class EEPROM(BlockDevice): def __setitem__(self, addr, value): if isinstance(addr, slice): - start, stop = self.do_slice(addr) - try: - if len(value) == (stop - start): - return self.readwrite(start, value, False) - else: - raise RuntimeError('Slice must have same length as data') - except TypeError: - raise RuntimeError('Can only assign bytes/bytearray to a slice') + return self.wslice(addr, value) self._buf1[0] = value self._getaddr(addr, 1) self._i2c.writevto(self._i2c_addr, (self._addrbuf, self._buf1)) @@ -70,9 +63,7 @@ class EEPROM(BlockDevice): def __getitem__(self, addr): if isinstance(addr, slice): - start, stop = self.do_slice(addr) - buf = bytearray(stop - start) - return self.readwrite(start, buf, True) + return self.rslice(addr) self._getaddr(addr, 1) self._i2c.writeto(self._i2c_addr, self._addrbuf) self._i2c.readfrom_into(self._i2c_addr, self._buf1) @@ -86,7 +77,7 @@ class EEPROM(BlockDevice): ca, la = divmod(addr, self._c_bytes) # ca == chip no, la == offset into chip self._addrbuf[0] = (la >> 8) & 0xff self._addrbuf[1] = la & 0xff - self._i2c_addr = ADDR + ca + self._i2c_addr = _ADDR + ca pe = (addr & ~0x7f) + 0x80 # byte 0 of next page return min(nbytes, pe - la) @@ -94,7 +85,7 @@ class EEPROM(BlockDevice): def readwrite(self, addr, buf, read): nbytes = len(buf) mvb = memoryview(buf) - start = 0 + start = 0 # Offset into buf. while nbytes > 0: npage = self._getaddr(addr, nbytes) # No. of bytes in current page assert npage > 0 diff --git a/spi/SPI.md b/spi/SPI.md index 2b63f43..2113d89 100644 --- a/spi/SPI.md +++ b/spi/SPI.md @@ -34,7 +34,8 @@ as below. Pin numbers assume a PDIP package (8 pin plastic dual-in-line). 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. +SCL, MOSI and MISO lines wired in parallel. The SPI bus is fast: wiring should +be short and direct. If you use a Pyboard D and power the EEPROMs from the 3V3 output you will need to enable the voltage rail by issuing: @@ -90,11 +91,13 @@ each chip select line an EEPROM 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. + 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`. - 3. `verbose=True` If True, the constructor issues information on the EEPROM + as an output (`Pin.OUT`) and with `value=1` and be created by `machine`. + 3. `verbose=True` If `True`, the constructor issues information on the EEPROM devices it has detected. + 4. `block_size=9` The block size reported to the filesystem. The size in bytes + is `2**block_size` so is 512 bytes by default. SPI baudrate: The 25LC1024 supports baudrates of upto 20MHz. If this value is specified the platform will produce the highest available frequency not @@ -135,7 +138,8 @@ print(eep[2000:2002]) # Returns a bytearray ``` Three argument slices are not supported: a third arg (other than 1) will cause an exception. One argument slices (`eep[:5]` or `eep[13100:]`) and negative -args are supported. +args are supported. See [section 4.2](./SPI.md#42-byte-addressing-usage-example) +for a typical application. #### 4.1.2.2 readwrite @@ -179,6 +183,37 @@ also [here](http://docs.micropython.org/en/latest/reference/filesystem.html#cust `writeblocks()` `ioctl()` +## 4.2 Byte addressing usage example + +A sample application: saving a configuration dict (which might be large and +complicated): +```python +import ujson +from machine import SPI, Pin +from eeprom_spi import EEPROM +cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1), Pin(Pin.board.Y4, Pin.OUT, value=1)) +eep = EEPROM(SPI(2, baudrate=20_000_000), cspins) +d = {1:'one', 2:'two'} # Some kind of large object +wdata = ujson.dumps(d).encode('utf8') +sl = '{:10d}'.format(len(wdata)).encode('utf8') +eep[0 : len(sl)] = sl # Save data length in locations 0-9 +start = 10 # Data goes in 10: +end = start + len(wdata) +eep[start : end] = wdata +``` +After a power cycle the data may be read back. Instantiate `eep` as above, then +issue: +```python +slen = int(eep[:10].decode().strip()) # retrieve object size +start = 10 +end = start + slen +d = ujson.loads(eep[start : end]) +``` +It is much more efficient in space and performance to store data in binary form +but in many cases code simplicity matters, especially where the data structure +is subject to change. An alternative to JSON is the pickle module. It is also +possible to use JSON/pickle to store objects in a filesystem. + # 5. Test program eep_spi.py This assumes a Pyboard 1.x or Pyboard D with EEPROM(s) wired as above. It @@ -187,12 +222,14 @@ 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. +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() -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. +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. ## 5.3 fstest(format=False) @@ -200,7 +237,13 @@ 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. -## 5.4 File copy +## 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 diff --git a/spi/eep_spi.py b/spi/eep_spi.py index e9acfe7..c3b1f22 100644 --- a/spi/eep_spi.py +++ b/spi/eep_spi.py @@ -7,7 +7,7 @@ import uos from machine import SPI, Pin from eeprom_spi import EEPROM # Add extra pins if using multiple chips -cspins = (Pin(Pin.board.Y5, Pin.OUT, value=1),) +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_eep(): @@ -29,6 +29,33 @@ def cp(source, dest): if len(buf) < 100: break +# ***** TEST OF DRIVER ***** +def _testblock(eep, bs): + d0 = b'this >' + d1 = b'xxxxxxxxxxxxx': + return 'Block test fail 2:' + 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:' + res + def test(): eep = get_eep() sa = 1000 @@ -46,6 +73,25 @@ def test(): 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_eep() if format: @@ -59,6 +105,25 @@ def fstest(format=False): print('Contents of "/eeprom": {}'.format(uos.listdir('/eeprom'))) print(uos.statvfs('/eeprom')) +def cptest(): + eep = get_eep() + if 'eeprom' in uos.listdir('/'): + print('Device already mounted.') + else: + vfs=uos.VfsFat(eep) + try: + uos.mount(vfs,'/eeprom') + except OSError: + print('Fail mounting device. Have you formatted it?') + return + print('Mounted device.') + cp('eep_spi.py', '/eeprom/') + cp('eeprom_spi.py', '/eeprom/') + print('Contents of "/eeprom": {}'.format(uos.listdir('/eeprom'))) + print(uos.statvfs('/eeprom')) + + +# ***** TEST OF HARDWARE ***** def full_test(): eep = get_eep() page = 0 diff --git a/spi/eeprom_spi.py b/spi/eeprom_spi.py index 9ba80d2..8c49d9b 100644 --- a/spi/eeprom_spi.py +++ b/spi/eeprom_spi.py @@ -23,9 +23,9 @@ _CE = const(0xc7) # Chip erase # Logical EEPROM device comprising one or more physical chips sharing an SPI bus. class EEPROM(BlockDevice): - def __init__(self, spi, cspins, verbose=True): + def __init__(self, spi, cspins, verbose=True, block_size=9): # args: virtual block size in bits, no. of chips, bytes in each chip - super().__init__(9, len(cspins), _SIZE) + super().__init__(block_size, len(cspins), _SIZE) self._spi = spi self._cspins = cspins self._ccs = None # Chip select Pin object for current chip @@ -74,15 +74,8 @@ class EEPROM(BlockDevice): time.sleep_ms(1) def __setitem__(self, addr, value): - if isinstance(addr, slice): # value is a buffer - start, stop = self.do_slice(addr) - try: - if len(value) == (stop - start): - return self.readwrite(start, value, False) - else: - raise RuntimeError('Slice must have same length as data') - except TypeError: - raise RuntimeError('Can only assign bytes/bytearray to a slice') + if isinstance(addr, slice): + return self.wslice(addr, value) mvp = self._mvp mvp[0] = _WREN self._getaddr(addr, 1) # Sets mv[1:4], updates ._ccs @@ -99,9 +92,7 @@ class EEPROM(BlockDevice): def __getitem__(self, addr): if isinstance(addr, slice): - start, stop = self.do_slice(addr) - buf = bytearray(stop - start) - return self.readwrite(start, buf, True) + return self.rslice(addr) mvp = self._mvp mvp[0] = _READ self._getaddr(addr, 1) @@ -130,7 +121,7 @@ class EEPROM(BlockDevice): nbytes = len(buf) mvb = memoryview(buf) mvp = self._mvp - start = 0 + start = 0 # Offset into buf. while nbytes > 0: npage = self._getaddr(addr, nbytes) # No. of bytes in current page cs = self._ccs