From 992e35b994a7fc9d934208e4f10e10b6cbd63e12 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 13 Dec 2019 18:49:48 +0000 Subject: [PATCH] Add SPI support. Reorganise directories. --- README.md | 245 +++++---------------------------- i2c/I2C.md | 222 +++++++++++++++++++++++++++++ eep_test.py => i2c/eep_i2c.py | 4 +- eeprom.py => i2c/eeprom_i2c.py | 20 ++- spi/SPI.md | 214 ++++++++++++++++++++++++++++ spi/eep_spi.py | 74 ++++++++++ spi/eeprom_spi.py | 185 +++++++++++++++++++++++++ 7 files changed, 744 insertions(+), 220 deletions(-) create mode 100644 i2c/I2C.md rename eep_test.py => i2c/eep_i2c.py (94%) rename eeprom.py => i2c/eeprom_i2c.py (85%) create mode 100644 spi/SPI.md create mode 100644 spi/eep_spi.py create mode 100644 spi/eeprom_spi.py diff --git a/README.md b/README.md index d7cba19..627545c 100644 --- a/README.md +++ b/README.md @@ -1,224 +1,43 @@ -# 1. A MicroPython EEPROM driver +# MicroPython EEPROM drivers -This enables MicroPython to access Microchip EEPROM devices. Unlike flash -memory, EEPROMs may be written on a byte addressable basis. Its 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. +EEPROM is a form of nonvolatile random access storage. -From one to eight chips may be used to construct a nonvolatile memory module -with sizes upto 512KiB. 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. -Where multiple chips are used, all must be the same size. +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. -The work was inspired by [this one](https://github.com/dda/MicroPython.git). -This was written some five years ago. The driver in this repo employs some of -the subsequent improvements to MicroPython to achieve these advantages: - 1. It supports multiple EEPROM chips to configure a single array. - 2. Writes are up to 1000x faster by using ACK polling and page writes. - 3. Page access improves the speed of multi-byte reads. - 4. It is cross-platform. - 5. The I2C bus can be shared with other chips. - 6. It supports filesystem mounting. - 7. Alternatively it can support byte-level access using Python slice syntax. - 8. RAM allocations are reduced. +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). -# 2. Connections +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). -Any I2C interface may be used. The table below assumes a Pyboard running I2C(2) -as per the test program. To wire up a single EEPROM chip, connect to a Pyboard -as below. Pin numbers assume a PDIP package (8 pin plastic dual-in-line). +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. -| EEPROM | PB | -|:------:|:---:| -| 1 A0 | Gnd | -| 2 A1 | Gnd | -| 3 A2 | Gnd | -| 4 Vss | Gnd | -| 5 Sda | Y10 | -| 6 Scl | Y9 | -| 7 WPA1 | Gnd | -| 8 Vcc | 3V3 | +Devices or arrays of devices may be mounted as a filesystem or may be treated +as an array of bytes. -For multiple chips the address lines A0, A1 and A2 of each chip 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: +For I2C devices see [I2C.md](./i2c/I2C.md). For SPI see [SPI.md](./spi/SPI.md). -| Chip no. | A2 | A1 | A0 | -|:--------:|:---:|:---:|:---:| -| 0 | Gnd | Gnd | Gnd | -| 1 | Gnd | Gnd | 3V3 | -| 2 | Gnd | 3V3 | Gnd | -| 3 | Gnd | 3V3 | 3V3 | -| 4 | 3V3 | Gnd | Gnd | -| 5 | 3V3 | Gnd | 3V3 | -| 6 | 3V3 | 3V3 | Gnd | -| 7 | 3V3 | 3V3 | 3V3 | +# Choice of interface -Multiple chips should have 3V3, Gnd, SCL and SDA lines wired in parallel. +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 +those lines, although these may be provided on the target device. The +supported EEPROM devices limit expansion to a maximum of 8 chips on a bus. -The I2C interface requires pullups, typically 3.3KΩ to 3.3V although any value -up to 10KΩ will suffice. The Pyboard 1.x has these on board. The Pyboard D has -them only on I2C(1). Even if boards have pullups, additional externalresistors -will do no harm. +SPI requires no pullups, but uses three pins plus one for each connected chip. +It is much faster than I2C, but in the case of EEPROMs the benefit is only +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). -If you use a Pyboard D and power the EEPROMs 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. `eeprom.py` Device driver. - 2. `eep_test.py` Test programs for above. - -# 4. The device driver - -The driver supports mounting the EEPROM 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 64KiB devices: - -```python -import uos -from machine import I2C -from eeprom import EEPROM, T24C512 -eep = EEPROM(I2C(2), T24C512) -uos.VfsFat.mkfs(eep) # Omit this to mount an existing filesystem -vfs = uos.VfsFat(eep) -uos.mount(vfs,'/eeprom') -``` -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. As a filesystem the limited size is an -issue, but a potential use case is for pickling Python objects for example to -achieve persistence when issuing `pyb.standby()`; also for holding a small -frequently updated persistent btree database. - -The I2C bus must be instantiated using the `machine` module. - -## 4.1 The EEPROM class - -An `EEPROM` instance represents a logical EEPROM: this may consist of multiple -physical devices on a common I2C bus. - -### 4.1.1 Constructor - -This scans the I2C bus - if one or more correctly addressed chips are detected -an EEPROM array is instantiated. A `RuntimeError` will be raised if no device -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. - 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 - devices it has detected. - -### 4.1.2 Methods providing byte level access - -#### 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 eeprom import EEPROM, T24C512 -eep = EEPROM(I2C(1), T24C512) -eep[2000] = 42 -print(eep[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 eeprom import EEPROM, T24C512 -eep = EEPROM(I2C(1), T24C512) -eep[2000:2002] = bytearray((42, 43)) -print(eep[2000:2002]) # Returns a bytearray -``` -Three argument slices are not supported: a third arg will be ignored. - -#### 4.1.2.2 readwrite - -This is a byte-level alternative to slice notation. It has the potential -advantage 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 Methods providing the block protocol - -For the protocol definition see -[the pyb documentation](http://docs.micropython.org/en/latest/library/uos.html#uos.AbstractBlockDev) - -`readblocks()` -`writeblocks()` -`ioctl()` - -### 4.1.4 Other methods - -#### 4.1.4.1 The len() operator - -The size of the EEPROM array in bytes may be retrieved by issuing `len(eep)` -where `eep` is the `EEPROM` instance. - -#### 4.1.4.2 scan - -Scans the I2C bus and returns the number of EEPROM 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. - -# 5. Test program eep_test.py - -This assumes a Pyboard 1.x or Pyboard D with EEPROM(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. - -## 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. - -## 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. - -## 5.4 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 EEPROM becoming -full) it is up to the caller to handle it. For example (assuming the EEPROM is -mounted on /eeprom): - -```python -cp('/flash/main.py','/eeprom/') -``` - -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 EEPROM can be mounted. +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. diff --git a/i2c/I2C.md b/i2c/I2C.md new file mode 100644 index 0000000..fd049ff --- /dev/null +++ b/i2c/I2C.md @@ -0,0 +1,222 @@ +# 1. A MicroPython I2C EEPROM driver + +This driver supports chips from the 64KiB 25xx512 series and related chips with +smaller capacities. + +From one to eight chips may be used to construct a nonvolatile memory module +with sizes upto 512KiB. 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. +Where multiple chips are used, all must be the same size. + +The work was inspired by [this one](https://github.com/dda/MicroPython.git). +This was written some five years ago. The driver in this repo employs some of +the subsequent improvements to MicroPython to achieve these advantages: + 1. It supports multiple EEPROM chips to configure a single array. + 2. Writes are up to 1000x faster by using ACK polling and page writes. + 3. Page access improves the speed of multi-byte reads. + 4. It is cross-platform. + 5. The I2C bus can be shared with other chips. + 6. It supports filesystem mounting. + 7. Alternatively it can support byte-level access using Python slice syntax. + 8. RAM allocations are reduced. + +# 2. Connections + +Any I2C interface may be used. The table below assumes a Pyboard running I2C(2) +as per the test program. To wire up a single EEPROM chip, connect to a Pyboard +as below. Pin numbers assume a PDIP package (8 pin plastic dual-in-line). + +| EEPROM | PB | +|:------:|:---:| +| 1 A0 | Gnd | +| 2 A1 | Gnd | +| 3 A2 | Gnd | +| 4 Vss | Gnd | +| 5 Sda | Y10 | +| 6 Scl | Y9 | +| 7 WPA1 | Gnd | +| 8 Vcc | 3V3 | + +For multiple chips the address lines A0, A1 and A2 of each chip 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: + +| Chip no. | A2 | A1 | A0 | +|:--------:|:---:|:---:|:---:| +| 0 | Gnd | Gnd | Gnd | +| 1 | Gnd | Gnd | 3V3 | +| 2 | Gnd | 3V3 | Gnd | +| 3 | Gnd | 3V3 | 3V3 | +| 4 | 3V3 | Gnd | Gnd | +| 5 | 3V3 | Gnd | 3V3 | +| 6 | 3V3 | 3V3 | Gnd | +| 7 | 3V3 | 3V3 | 3V3 | + +Multiple chips should have 3V3, Gnd, SCL and SDA lines wired in parallel. + +The I2C interface requires pullups, typically 3.3KΩ to 3.3V although any value +up to 10KΩ will suffice. The Pyboard 1.x has these on board. The Pyboard D has +them only on I2C(1). Even if boards have pullups, additional externalresistors +will do no harm. + +If you use a Pyboard D and power the EEPROMs 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. `eeprom_i2c.py` Device driver. + 2. `eep_i2c.py` Test programs for above. + +# 4. The device driver + +The driver supports mounting the EEPROM 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 64KiB devices: + +```python +import uos +from machine import I2C +from eeprom_i2c import EEPROM, T24C512 +eep = EEPROM(I2C(2), T24C512) +uos.VfsFat.mkfs(eep) # Omit this to mount an existing filesystem +vfs = uos.VfsFat(eep) +uos.mount(vfs,'/eeprom') +``` +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. As a filesystem the limited size is an +issue, but a potential use case is for pickling Python objects for example to +achieve persistence when issuing `pyb.standby()`; also for holding a small +frequently updated persistent btree database. + +The I2C bus must be instantiated using the `machine` module. + +## 4.1 The EEPROM class + +An `EEPROM` instance represents a logical EEPROM: this may consist of multiple +physical devices on a common I2C bus. + +### 4.1.1 Constructor + +This scans the I2C bus - if one or more correctly addressed chips are detected +an EEPROM array is instantiated. A `RuntimeError` will be raised if no device +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. + 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 + devices it has detected. + +### 4.1.2 Methods providing byte level access + +#### 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 eeprom_i2c import EEPROM, T24C512 +eep = EEPROM(I2C(1), T24C512) +eep[2000] = 42 +print(eep[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 eeprom_i2c import EEPROM, T24C512 +eep = EEPROM(I2C(1), T24C512) +eep[2000:2002] = bytearray((42, 43)) +print(eep[2000:2002]) # Returns a bytearray +``` +Three argument slices are not supported: any third arg will be ignored. One +argument slices (`eep[:5]` or `eep[13100:]`) and negative args are supported. + +#### 4.1.2.2 readwrite + +This is a byte-level alternative to slice notation. It has the potential +advantage 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 Methods providing the block protocol + +For the protocol definition see +[the pyb documentation](http://docs.micropython.org/en/latest/library/uos.html#uos.AbstractBlockDev) + +`readblocks()` +`writeblocks()` +`ioctl()` + +### 4.1.4 Other methods + +#### 4.1.4.1 The len() operator + +The size of the EEPROM array in bytes may be retrieved by issuing `len(eep)` +where `eep` is the `EEPROM` instance. + +#### 4.1.4.2 scan + +Scans the I2C bus and returns the number of EEPROM 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. + +# 5. Test program eep_i2c.py + +This assumes a Pyboard 1.x or Pyboard D with EEPROM(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. + +## 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. + +## 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. + +## 5.4 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 EEPROM becoming +full) it is up to the caller to handle it. For example (assuming the EEPROM is +mounted on /eeprom): + +```python +cp('/flash/main.py','/eeprom/') +``` + +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 EEPROM can be mounted. diff --git a/eep_test.py b/i2c/eep_i2c.py similarity index 94% rename from eep_test.py rename to i2c/eep_i2c.py index c385957..b41d67f 100644 --- a/eep_test.py +++ b/i2c/eep_i2c.py @@ -1,11 +1,11 @@ -# eep_test.py MicroPython driver for Microchip EEPROM devices. +# eep_i2c.py MicroPython test program for Microchip I2C EEPROM devices. # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2019 Peter Hinch import uos from machine import I2C, Pin -from eeprom import EEPROM, T24C512 +from eeprom_i2c import EEPROM, T24C512 # Return an EEPROM array. Adapt for platforms other than Pyboard or chips # smaller than 64KiB. diff --git a/eeprom.py b/i2c/eeprom_i2c.py similarity index 85% rename from eeprom.py rename to i2c/eeprom_i2c.py index 8913b8a..f86ef51 100644 --- a/eeprom.py +++ b/i2c/eeprom_i2c.py @@ -1,4 +1,4 @@ -# eeprom.py MicroPython driver for Microchip EEPROM devices. +# eeprom_i2c.py MicroPython driver for Microchip I2C EEPROM devices. # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2019 Peter Hinch @@ -28,6 +28,14 @@ class EEPROM(): self._buf1 = bytearray(1) self._addrbuf = bytearray(2) # Memory offset into current chip + # Handle special cases of a slice. Always return a pair of positive indices. + def do_slice(self, addr): + start = addr.start if addr.start is not None else 0 + stop = addr.stop if addr.stop is not None else self._a_bytes + start = start if start >= 0 else self._a_bytes + start + stop = stop if stop >= 0 else self._a_bytes + stop + return start, stop + # Check for a valid hardware configuration def scan(self, verbose, chip_size): devices = self._i2c.scan() # All devices on I2C bus @@ -58,9 +66,10 @@ class EEPROM(): def __setitem__(self, addr, value): if isinstance(addr, slice): + start, stop = self.do_slice(addr) try: - if len(value) == (addr.stop - addr.start): - return self.readwrite(addr.start, value, False) + if len(value) == (stop - start): + return self.readwrite(start, value, False) else: raise RuntimeError('Slice must have same length as data') except TypeError: @@ -72,8 +81,9 @@ class EEPROM(): def __getitem__(self, addr): if isinstance(addr, slice): - buf = bytearray(addr.stop - addr.start) - return self.readwrite(addr.start, buf, True) + start, stop = self.do_slice(addr) + buf = bytearray(stop - start) + return self.readwrite(start, buf, True) self._getaddr(addr, 1) self._i2c.writeto(self._i2c_addr, self._addrbuf) self._i2c.readfrom_into(self._i2c_addr, self._buf1) diff --git a/spi/SPI.md b/spi/SPI.md new file mode 100644 index 0000000..4722805 --- /dev/null +++ b/spi/SPI.md @@ -0,0 +1,214 @@ +# 1. A MicroPython SPI EEPROM driver + +This driver supports the Microchip 25xx1024 series of 128KiB SPI EEPROMs. +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 EEPROM chips to configure a single array. + 2. For performance, writes use page writes where possible. + 3. Page access improves the speed of multi-byte reads. + 4. It is cross-platform. + 5. The SPI bus can be shared with other chips. + 6. It supports filesystem mounting. + 7. Alternatively it can support byte-level access using Python slice syntax. + 8. RAM allocations are minimised. + +# 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 EEPROM chip, connect to a Pyboard +as below. Pin numbers assume a PDIP package (8 pin plastic dual-in-line). + +| EEPROM | PB | Signal | +|:-------:|:---:|:------:| +| 1 CS | Y5 | SS/ | +| 2 SO | Y7 | MISO | +| 3 WP/ | 3V3 | | +| 4 Vss | Gnd | | +| 5 SI | Y8 | MOSI | +| 6 SCK | Y6 | SCK | +| 7 HOLD/ | 3V3 | | +| 8 Vcc | 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 EEPROMs 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. `eeprom_spi.py` Device driver. + 2. `eep_spi.py` Test programs for above. + +# 4. The device driver + +The driver supports mounting the EEPROM 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: + +```python +import uos +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) +uos.VfsFat.mkfs(eep) # Omit this to mount an existing filesystem +vfs = uos.VfsFat(eep) +uos.mount(vfs,'/eeprom') +``` +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. As a filesystem the limited size is an +issue, but a potential use case is for pickling Python objects for example to +achieve persistence when issuing `pyb.standby()`; also for holding a small +frequently updated persistent btree database. + +The SPI bus must be instantiated using the `machine` module. + +## 4.1 The EEPROM class + +An `EEPROM` instance represents a logical EEPROM: this may consist of multiple +physical devices on a common SPI bus. + +### 4.1.1 Constructor + +This test each chip in the list of chip select pins - if a chip is detected on +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. + 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 + devices it has detected. + +SPI baudrate: The 25LC1024 supports baudrates of upto 20MHz. If this value is +specified the platform will produce the highest available frequency not +exceeding this figure. + +### 4.1.2 Methods providing byte level access + +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 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) +eep[2000] = 42 +print(eep[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 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) +eep[2000:2002] = bytearray((42, 43)) +print(eep[2000:2002]) # Returns a bytearray +``` +Three argument slices are not supported: any third arg will be ignored. One +argument slices (`eep[:5]` or `eep[13100:]`) and negative args are supported. + +#### 4.1.2.2 readwrite + +This is a byte-level alternative to slice notation. It has the potential +advantage 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 Methods providing the block protocol + +For the protocol definition see +[the pyb documentation](http://docs.micropython.org/en/latest/library/uos.html#uos.AbstractBlockDev) + +`readblocks()` +`writeblocks()` +`ioctl()` + +### 4.1.4 Other methods + +#### 4.1.4.1 The len() operator + +The size of the EEPROM array in bytes may be retrieved by issuing `len(eep)` +where `eep` is the `EEPROM` instance. + +#### 4.1.4.2 scan + +Activate each chip select in turn checking for a valid device and returns the +number of EEPROM 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. + +#### 4.1.4.3 erase + +Erases the entire array. + +# 5. Test program eep_spi.py + +This assumes a Pyboard 1.x or Pyboard D with EEPROM(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. + +## 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. + +## 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. + +## 5.4 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 EEPROM becoming +full) it is up to the caller to handle it. For example (assuming the EEPROM is +mounted on /eeprom): + +```python +cp('/flash/main.py','/eeprom/') +``` + +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 EEPROM can be mounted. diff --git a/spi/eep_spi.py b/spi/eep_spi.py new file mode 100644 index 0000000..e9acfe7 --- /dev/null +++ b/spi/eep_spi.py @@ -0,0 +1,74 @@ +# eep_spi.py MicroPython test program for Microchip SPI EEPROM devices. + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2019 Peter Hinch + +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),) + +# Return an EEPROM array. Adapt for platforms other than Pyboard. +def get_eep(): + if uos.uname().machine.split(' ')[0][:4] == 'PYBD': + Pin.board.EN_3V3.value(1) + eep = EEPROM(SPI(2, baudrate=20_000_000), cspins) + print('Instantiated EEPROM') + return eep + +# 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 /eeprom/ + 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 + +def test(): + eep = get_eep() + 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') + +def fstest(format=False): + eep = get_eep() + if format: + uos.VfsFat.mkfs(eep) + vfs=uos.VfsFat(eep) + try: + uos.mount(vfs,'/eeprom') + except OSError: # Already mounted + pass + print('Contents of "/": {}'.format(uos.listdir('/'))) + print('Contents of "/eeprom": {}'.format(uos.listdir('/eeprom'))) + print(uos.statvfs('/eeprom')) + +def full_test(): + eep = get_eep() + 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/spi/eeprom_spi.py b/spi/eeprom_spi.py new file mode 100644 index 0000000..8983fdb --- /dev/null +++ b/spi/eeprom_spi.py @@ -0,0 +1,185 @@ +# eeprom_spi.py MicroPython driver for Microchip SPI EEPROM devices, +# currently only 25xx1024. + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2019 Peter Hinch + +import time +from micropython import const + +_SIZE = const(131072) # Chip size 128KiB +# Supported instruction set +_READ = const(3) +_WRITE = const(2) +_WREN = const(6) +_WRDI = const(4) +_RDSR = const(5) +_WRSR = const(1) +_RDID = const(0xab) +_CE = const(0xc7) + +# Logical EEPROM device consisting of an arbitrary number of physical chips +# sharing an SPI bus. +class EEPROM(): + + def __init__(self, spi, cspins, verbose=True): + self._spi = spi + self._cspins = cspins + nchips = len(cspins) # No. of EEPROM chips + # size as a bound variable for future bigger chips + self._c_bytes = _SIZE # Size of chip in bytes + self._a_bytes = _SIZE * nchips # Size of array + self._ccs = None # Chip select Pin object for current chip + self._bufp = bytearray(5) # instruction + 3 byte address + 1 byte value + self._mvp = memoryview(self._bufp) # cost-free slicing + self.scan(verbose) + + # Handle special cases of a slice. Always return a pair of positive indices. + def do_slice(self, addr): + start = addr.start if addr.start is not None else 0 + stop = addr.stop if addr.stop is not None else self._a_bytes + start = start if start >= 0 else self._a_bytes + start + stop = stop if stop >= 0 else self._a_bytes + stop + return start, stop + + # Check for a valid hardware configuration + def scan(self, verbose): + mvp = self._mvp + for n, cs in enumerate(self._cspins): + mvp[:] = b'\0\0\0\0\0' + mvp[0] = _RDID + cs(0) + self._spi.write_readinto(mvp[:5], mvp[:5]) + cs(1) + if mvp[4] != 0x29: + raise RuntimeError('EEPROM not found at cs[{}].'.format(n)) + if verbose: + s = '{} chips detected. Total EEPROM size {}bytes.' + print(s.format(n + 1, self._a_bytes)) + + 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 + + def __len__(self): + return self._a_bytes + + 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] = _RDSR + cs(0) + self._spi.write_readinto(mvp[:2], mvp[:2]) + cs(1) + assert not mvp[1] & 0xC # BP0, BP1 assumed 0 + if not (mvp[1] & 1): + break + 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') + mvp = self._mvp + mvp[0] = _WREN + self._getaddr(addr, 1) # Sets mv[1:4], updates ._ccs + cs = self._ccs # Retrieve current cs pin + cs(0) + self._spi.write(mvp[:1]) + cs(1) + mvp[0] = _WRITE + mvp[4] = value + cs(0) + self._spi.write(mvp[:5]) + cs(1) # Trigger write + self._wait_rdy() # Wait for write to complete + + def __getitem__(self, addr): + if isinstance(addr, slice): + start, stop = self.do_slice(addr) + buf = bytearray(stop - start) + return self.readwrite(start, buf, True) + mvp = self._mvp + mvp[0] = _READ + self._getaddr(addr, 1) + cs = self._ccs + cs(0) + self._spi.write_readinto(mvp[:5], mvp[:5]) + cs(1) + return mvp[4] + + # Given an address, set current chip select and address buffer. + # Return the number of bytes that can be processed in the current page. + def _getaddr(self, addr, nbytes): + if addr >= self._a_bytes: + raise RuntimeError("EEPROM 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 page + return min(nbytes, pe - la) + + # Read or write multiple bytes at an arbitrary address + def readwrite(self, addr, buf, read): + nbytes = len(buf) + mvb = memoryview(buf) + mvp = self._mvp + start = 0 + while nbytes > 0: + npage = self._getaddr(addr, nbytes) # No. of bytes in current page + cs = self._ccs + assert npage > 0 + if read: + mvp[0] = _READ + cs(0) + self._spi.write(mvp[:4]) + self._spi.readinto(mvb[start : start + npage]) + cs(1) + else: + mvp[0] = _WREN + cs(0) + self._spi.write(mvp[:1]) + cs(1) + mvp[0] = _WRITE + cs(0) + self._spi.write(mvp[:4]) + self._spi.write(mvb[start: start + npage]) + cs(1) # Trigger write start + self._wait_rdy() # Wait until done (6ms max) + nbytes -= npage + start += npage + addr += npage + return buf + + # IOCTL protocol. Emulate block size of 512 bytes. + def readblocks(self, blocknum, buf): + return self.readwrite(blocknum << 9, buf, True) + + def writeblocks(self, blocknum, buf): + self.readwrite(blocknum << 9, buf, False) + + def ioctl(self, op, arg): + #print("ioctl(%d, %r)" % (op, arg)) + if op == 4: # BP_IOCTL_SEC_COUNT + return self._a_bytes >> 9 + if op == 5: # BP_IOCTL_SEC_SIZE + return 512