From efe6e366b342a753478d198f9044c93a2863e23b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 11 Dec 2019 09:23:17 +0000 Subject: [PATCH] Initial commit. --- README.md | 224 +++++++++++++++++++++++++++++++++++++++++++++++++++- eep_test.py | 71 +++++++++++++++++ eeprom.py | 125 +++++++++++++++++++++++++++++ 3 files changed, 418 insertions(+), 2 deletions(-) create mode 100644 eep_test.py create mode 100644 eeprom.py diff --git a/README.md b/README.md index b3004bb..f818abc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,222 @@ -# micropython_eeprom -MicroPython device driver for Microchip EEPROM chips (24LC512 and similar) +# 1. micropython_eeprom + +A driver to enable 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. + +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). 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. Even if boards have pullups connecting external +resistors will do no harm. + +# 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 a 64KiB device: + +```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 simply the array and lists the +contents. + +## 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/eep_test.py new file mode 100644 index 0000000..c385957 --- /dev/null +++ b/eep_test.py @@ -0,0 +1,71 @@ +# eep_test.py MicroPython driver for Microchip 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 + +# Return an EEPROM array. Adapt for platforms other than Pyboard or chips +# smaller than 64KiB. +def get_eep(): + if uos.uname().machine.split(' ')[0][:4] == 'PYBD': + Pin.board.EN_3V3.value(1) + eep = EEPROM(I2C(2), T24C512) + 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), 128): + data = uos.urandom(128) + eep[sa:sa + 128] = data + if eep[sa:sa + 128] == data: + print('Page {} passed'.format(page)) + else: + print('Page {} readback failed.'.format(page)) + page += 1 diff --git a/eeprom.py b/eeprom.py new file mode 100644 index 0000000..8913b8a --- /dev/null +++ b/eeprom.py @@ -0,0 +1,125 @@ +# eeprom.py MicroPython driver for Microchip EEPROM devices. + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2019 Peter Hinch + +import time +from micropython import const + +ADDR = const(0x50) # Base address of chip + +T24C512 = const(65536) # 64KiB 512Kbits +T24C256 = const(32768) # 32KiB 256Kbits +T24C128 = const(16384) # 16KiB 128Kbits +T24C64 = const(8192) # 8KiB 64Kbits + +# Logical EEPROM device consists of 1-8 physical chips. Chips must all be the +# same size, and must have contiguous addresses starting from 0x50. +class EEPROM(): + + def __init__(self, i2c, chip_size=T24C512, verbose=True): + 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 + self._c_bytes = chip_size # Size of chip in bytes + self._a_bytes = chip_size * nchips # Size of array + self._i2c_addr = 0 # I2C address of current chip + self._buf1 = bytearray(1) + self._addrbuf = bytearray(2) # Memory offset into current chip + + # 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 + nchips = len(eeproms) + if nchips == 0: + raise RuntimeError('EEPROM not found.') + if min(eeproms) != ADDR or (max(eeproms) - ADDR + 1) > nchips: + raise RuntimeError('Non-contiguous chip addresses', eeproms) + if verbose: + s = '{} chips detected. Total EEPROM size {}bytes.' + print(s.format(nchips, chip_size * nchips)) + return nchips + + def __len__(self): + return self._a_bytes + + def _wait_rdy(self): # After a write, wait for device to become ready + self._buf1[0] = 0 + while True: + try: + if self._i2c.writeto(self._i2c_addr, self._buf1): # Poll ACK + break + except OSError: + pass + finally: + time.sleep_ms(1) + + def __setitem__(self, addr, value): + if isinstance(addr, slice): + try: + if len(value) == (addr.stop - addr.start): + return self.readwrite(addr.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') + self._buf1[0] = value + self._getaddr(addr, 1) + self._i2c.writevto(self._i2c_addr, (self._addrbuf, self._buf1)) + self._wait_rdy() # Wait for write to complete + + def __getitem__(self, addr): + if isinstance(addr, slice): + buf = bytearray(addr.stop - addr.start) + return self.readwrite(addr.start, buf, True) + 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] + + # Given an address, set ._i2c_addr and ._addrbuf and return the number of + # bytes that can be processed in the current page + def _getaddr(self, addr, nbytes): # Set up _addrbuf and _i2c_addr + 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._addrbuf[0] = (la >> 8) & 0xff + self._addrbuf[1] = la & 0xff + self._i2c_addr = ADDR + ca + pe = (addr & ~0x7f) + 0x80 # 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) + start = 0 + while nbytes > 0: + npage = self._getaddr(addr, nbytes) # No. of bytes in current page + assert npage > 0 + if read: + self._i2c.writeto(self._i2c_addr, self._addrbuf) + self._i2c.readfrom_into(self._i2c_addr, mvb[start : start + npage]) + else: + self._i2c.writevto(self._i2c_addr, (self._addrbuf, buf[start: start + npage])) + self._wait_rdy() + nbytes -= npage + start += npage + addr += npage + return buf + + # IOCTL protocol. Emulate block size of 512 bytes for now. + 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