Add FRAM. Various minor changes.

pull/1/head
Peter Hinch 2019-12-17 17:10:43 +00:00
rodzic 6a86e2aadd
commit 985c09ae4f
11 zmienionych plików z 800 dodań i 70 usunięć

Wyświetl plik

@ -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.

Wyświetl plik

@ -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)

246
fram/FRAM.md 100644
Wyświetl plik

@ -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)

91
fram/fram_i2c.py 100644
Wyświetl plik

@ -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

127
fram/fram_test.py 100644
Wyświetl plik

@ -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'<is the boundary'
d2 = d0 + d1
garbage = b'xxxxxxxxxxxxxxxxxxx'
start = bs - len(d0)
end = start + len(garbage)
eep[start : end] = garbage
res = eep[start : end]
if res != garbage:
return 'Block test fail 1:' + res
end = start + len(d0)
eep[start : end] = d0
end = start + len(garbage)
res = eep[start : end]
if res != b'this >xxxxxxxxxxxxx':
return 'Block test fail 2:' + res
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

Wyświetl plik

@ -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

Wyświetl plik

@ -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'<is the boundary'
d2 = d0 + d1
garbage = b'xxxxxxxxxxxxxxxxxxx'
start = bs - len(d0)
end = start + len(garbage)
eep[start : end] = garbage
res = eep[start : end]
if res != garbage:
return 'Block test fail 1:' + res
end = start + len(d0)
eep[start : end] = d0
end = start + len(garbage)
res = eep[start : end]
if res != b'this >xxxxxxxxxxxxx':
return 'Block test fail 2:' + res
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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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'<is the boundary'
d2 = d0 + d1
garbage = b'xxxxxxxxxxxxxxxxxxx'
start = bs - len(d0)
end = start + len(garbage)
eep[start : end] = garbage
res = eep[start : end]
if res != garbage:
return 'Block test fail 1:' + res
end = start + len(d0)
eep[start : end] = d0
end = start + len(garbage)
res = eep[start : end]
if res != b'this >xxxxxxxxxxxxx':
return 'Block test fail 2:' + res
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

Wyświetl plik

@ -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