spi: Bug in presence detect.

pull/24/head
Peter Hinch 2024-01-12 08:39:07 +00:00
rodzic 9a9c588f9b
commit e06fd66bb3
5 zmienionych plików z 210 dodań i 168 usunięć

Wyświetl plik

@ -4,7 +4,7 @@
# Documentation in BASE_CLASSES.md
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2019 Peter Hinch
# Copyright (c) 2019-2024 Peter Hinch
from micropython import const
@ -80,6 +80,7 @@ class BlockDevice:
return 0
# Hardware agnostic base class for EEPROM arrays
class EepromDevice(BlockDevice):
def __init__(self, nbits, nchips, chip_size, page_size, verbose):
super().__init__(nbits, nchips, chip_size)
@ -96,26 +97,22 @@ class EepromDevice(BlockDevice):
def get_page_size(self): # For test script
return self._page_size
# Measuring page size should not be done in production code. See docs.
def _set_pagesize(self, page_size):
if page_size is None: # Measure it
if page_size is None: # Measure it.
self._psize(16) # Conservative
old = self[:129] # Save old contents (nonvolatile!)
self._psize(256) # Ambitious
r = (16, 32, 64, 128) # Legal page sizes + 256
for x in r:
self[x] = 255 # Write single bytes, don't invoke page write
self[0:129] = b"\0" * 129 # Zero 129 bytes attempting to use 256 byte pages
try:
self._psize(256) # Ambitious
r = (16, 32, 64, 128) # Legal page sizes + 256
for x in r:
self[x] = 255 # Write single bytes, don't invoke page write
self[0:129] = b"\0" * 129 # Zero 129 bytes attempting to use 256 byte pages
try:
ps = next(z for z in r if self[z])
except StopIteration:
ps = 256
self._psize(ps)
self[:129] = old
except: # If anything goes wrong, restore old data and raise
for n, v in enumerate(old):
self[n] = v
raise
ps = next(z for z in r if self[z])
except StopIteration:
ps = 256
self._psize(ps)
self[:129] = old
else: # Validated page_size was supplied
self._psize(page_size)

Wyświetl plik

@ -131,17 +131,13 @@ Arguments:
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.
5. `addr` override base address for first chip.
6. `max_chips_count` override max_chips_count.
5. `addr` Override base address for first chip. See
[4.1.6 Special configurations](./I2C.md#416-special-configurations).
6. `max_chips_count` Override max_chips_count - see above reference.
7. `page_size=None` EEPROM chips have a page buffer. By default the driver
determines the size of this automatically. It is possible to override this by
passing an integer being the page size in bytes: 16, 32, 64, 128 or 256. The
page size may vary between chips from different manufacturers even for the
same storage size. Note that specifying too large a value will most likely lead
to data corruption in write operations and will cause the test script's basic
test to fail. The correct value for a device may be found in in the chip
datasheet. It is also reported if `verbose` is set. Auto-detecting page size
carries a risk of data loss if power fails while auto-detect is in progress.
passing an integer being the page size in bytes: 16, 32, 64, 128 or 256. See
[4.1.5 Page size](./I2C.md#414-page-size) for issues surrounding this.
In most cases only the first two arguments are used, with an array being
instantiated with (for example):
@ -150,26 +146,13 @@ from machine import I2C
from eeprom_i2c import EEPROM, T24C512
eep = EEPROM(I2C(2), T24C512)
```
It is possible to configure multiple chips as multiple arrays. This is done by
means of the `addr` and `max_chips_count` args. Examples:
```python
eeprom0 = EEPROM(i2c, max_chips_count = 2)
eeprom1 = EEPROM(i2c, addr = 0x52, max_chips_count = 2)
```
1st array uses address 0x50 and 0x51 and 2nd uses address 0x52 and 0x53.
individual chip usage:
```python
eeprom0 = EEPROM(i2c, addr = 0x50, max_chips_count = 1)
eeprom1 = EEPROM(i2c, addr = 0x51, max_chips_count = 1)
```
### 4.1.2 Methods providing byte level access
It is possible to read and write individual bytes or arrays of arbitrary size.
Larger arrays are faster, especially when writing: the driver uses the chip's
hardware page access where possible. Writing a page (128 bytes) takes the same
time (~5ms) as writing a single byte.
hardware page access where possible. Writing a page takes the same time (~5ms)
as writing a single byte.
#### 4.1.2.1 `__getitem__` and `__setitem__`
@ -223,6 +206,10 @@ 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.
#### get_page_size
Return the page size in bytes.
### 4.1.4 Methods providing the block protocol
These are provided by the base class. For the protocol definition see
@ -236,6 +223,37 @@ their use in application code is not recommended.
`writeblocks()`
`ioctl()`
### 4.1.5 Page size
EEPROM chips have a RAM buffer enabling fast writing of data blocks. Writing a
page takes the same time (~5ms) as writing a single byte. The page size may vary
between chips from different manufacturers even for the same storage size.
Specifying too large a value will most likely lead to data corruption in write
operations and will cause the test script's basic test to fail. Too small a
value will impact write performance. The correct value for a device may be found
in in the chip datasheet. It is also reported if `verbose` is set and when
running the test scripts.
Auto-detecting page size carries a risk of data loss if power fails while
auto-detect is in progress. In production code the value should be specified
explicitly.
### 4.1.6 Special configurations
It is possible to configure multiple chips as multiple arrays. This is done by
means of the `addr` and `max_chips_count` args. Examples:
```python
eeprom0 = EEPROM(i2c, max_chips_count = 2)
eeprom1 = EEPROM(i2c, addr = 0x52, max_chips_count = 2)
```
1st array uses address 0x50 and 0x51 and 2nd uses address 0x52 and 0x53.
Individual chip usage:
```python
eeprom0 = EEPROM(i2c, addr = 0x50, max_chips_count = 1)
eeprom1 = EEPROM(i2c, addr = 0x51, max_chips_count = 1)
```
## 4.2 Byte addressing usage example
A sample application: saving a configuration dict (which might be large and
@ -268,20 +286,21 @@ 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
provides the following.
This assumes a Pyboard 1.x or Pyboard D with EEPROM(s) wired as above. On other
hardware, adapt `get_eep` at the start of the script. 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.
reports how many chips can be accessed. The current page size is printed and its
validity is tested. 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.
This is a hardware test. Tests the entire array. Fills the array with random
data in blocks of 256 byes. After each block is written, it is read back and the
contents compared to the data written. Existing array data will be lost.
## 5.3 fstest(format=False)

Wyświetl plik

@ -18,9 +18,12 @@ The driver has the following attributes:
7. Alternatively it can support byte-level access using Python slice syntax.
8. RAM allocations are minimised. Buffer sizes are tiny.
##### [Main readme](../../README.md)
## 1.1 Notes
## 1.1 This document
As of Jan 2024 this driver has been updated to fix a bug where the device page
size was less than 256. A further aim was to make the driver more generic, with
a better chance of working with other SPI EEPROM chips. The constructor has
additional optional args to support this.
Code samples assume one or more Microchip devices. If using the STM chip the
SPI baudrate should be 5MHz and the chip size must be specified to the `EEPROM`
@ -29,6 +32,8 @@ constructor, e.g.:
eep = EEPROM(SPI(2, baudrate=5_000_000), cspins, 256)
```
##### [Main readme](../../README.md)
# 2. Connections
Any SPI interface may be used. The table below assumes a Pyboard running SPI(2)
@ -91,7 +96,7 @@ import os
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 = EEPROM(SPI(2, baudrate=20_000_000), cspins, 128) # 128KiB chips
# Format the filesystem
os.VfsLfs2.mkfs(eep) # Omit this to mount an existing filesystem
os.mount(eep,'/eeprom')
@ -103,14 +108,15 @@ 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.
objects serialised using Pickle/json or files holding a 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.
physical devices on a common SPI bus. Alternatively multiple EEPROM instances
may share the bus, differentiated by their CS pins.
### 4.1.1 Constructor
@ -119,14 +125,21 @@ 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 created by `machine`.
1. `spi` 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` and be created by `machine`.
3. `size=128` Chip size in KiB. Set to 256 for the STM chip.
4. `verbose=True` If `True`, the constructor issues information on the EEPROM
devices it has detected.
3. `size` Chip size in KiB. Set to 256 for the STM chip, 128 for the Microchip.
4. `verbose=True` If `True`, the constructor performs a presence check for an
EEPROM on each chip select pin and reports devices it has detected. See
[4.1.5 Auto detection](./SPI.md#415-auto-detection) for observations on
production code.
5. `block_size=9` The block size reported to the filesystem. The size in bytes
is `2**block_size` so is 512 bytes by default.
6. `page_size=None` EEPROM devices have a RAM buffer enabling fast writes. The
driver determines this automatically by default. It is possible to override
this by passing an integer being the page size in bytes: 16, 32, 64, 128 or 256.
See [4.1.5 Auto detection](./SPI.md#415-auto-detection) for reasons why this
is advised in production code.
SPI baudrate: The 25LC1024 supports baudrates of upto 20MHz. If this value is
specified the platform will produce the highest available frequency not
@ -152,7 +165,7 @@ of single byte access:
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 = EEPROM(SPI(2, baudrate=20_000_000), cspins, 128)
eep[2000] = 42
print(eep[2000]) # Return an integer
```
@ -162,7 +175,7 @@ writing, the size of the slice must match the length of the buffer:
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 = EEPROM(SPI(2, baudrate=20_000_000), cspins, 128)
eep[2000:2002] = bytearray((42, 43))
print(eep[2000:2002]) # Returns a bytearray
```
@ -187,7 +200,7 @@ advantage when reading of using a pre-allocated buffer. Arguments:
#### The len operator
The size of the EEPROM array in bytes may be retrieved by issuing `len(eep)`
where `eep` is the `EEPROM` instance.
where `eep` is an `EEPROM` instance.
#### scan
@ -201,7 +214,11 @@ identify the chip.
#### erase
Erases the entire array. Available only on the Microchip device.
Zero the entire array. Can take several seconds.
#### get_page_size
Return the page size in bytes.
### 4.1.4 Methods providing the block protocol
@ -216,6 +233,19 @@ their use in application code is not recommended.
`writeblocks()`
`ioctl()`
### 4.1.5 Auto detection
The driver constructor uses auto-detection in two circumstances:
* If `verbose` is specified, it checks each chip select for chip presence.
* If `page_size` is set to `None` the value is determined by measurement.
In both cases data is written to the chips, then restored from RAM. If a power
outage were to occur while either process was in progress, corruption could
occur. It is therefore recommended that, in production code, `verbose` is
`False` and `page_size` is set to an integer. The page size may be determined
from the chip datasheet. It is also printed on instantiation if `verbose` is
set: running any of the test scripts will do this.
## 4.2 Byte addressing usage example
A sample application: saving a configuration dict (which might be large and
@ -225,7 +255,7 @@ 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)
eep = EEPROM(SPI(2, baudrate=20_000_000), cspins, 128)
d = {1:'one', 2:'two'} # Some kind of large object
wdata = ujson.dumps(d).encode('utf8')
sl = '{:10d}'.format(len(wdata)).encode('utf8')
@ -252,18 +282,20 @@ possible to use JSON/pickle to store objects in a filesystem.
This assumes a Pyboard 1.x or Pyboard D with two EEPROMs wired to SPI(2) as
above with chip selects connected to pins `Y4` and `Y5`. It provides the
following. In all cases the stm arg should be `True` if using the STM chips.
On other hardware, adapt `cspins` and `get_eep` at the start of the script.
## 5.1 test(stm=False)
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.
reports how many chips can be accessed. The current page size is printed and its
validity is tested. Existing array data will be lost. This primarily tests the
driver: as a hardware test it is not exhaustive.
## 5.2 full_test(stm=False)
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.
This is a hardware test. Tests the entire array. Fills the array with random
data in blocks of 256 byes. After each block is written, it is read back and the
contents compared to the data written. Existing array data will be lost.
## 5.3 fstest(format=False, stm=False)

Wyświetl plik

@ -19,7 +19,7 @@ def get_eep(stm):
if stm:
eep = EEPROM(SPI(2, baudrate=5_000_000), cspins, 256)
else:
eep = EEPROM(SPI(2, baudrate=20_000_000), cspins)
eep = EEPROM(SPI(2, baudrate=20_000_000), cspins, 128)
print("Instantiated EEPROM")
return eep
@ -28,13 +28,19 @@ def get_eep(stm):
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
try:
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
except OSError as e:
if e.errno == 28:
print("Insufficient space for copy.")
else:
raise
# ***** TEST OF DRIVER *****
@ -72,9 +78,7 @@ def test(stm=False):
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)
)
print("Fail at address {} data {} should be {}".format(sa + v, eep[sa + v], v))
break
else:
print("Test of byte addressing passed")
@ -101,6 +105,14 @@ def test(stm=False):
print(res)
else:
print("Test chip boundary skipped: only one chip!")
pe = eep.get_page_size() # One byte past page
eep[pe] = 0xFF
eep[:257] = b"\0" * 257
print("Test page size: ", end="")
if eep[pe]:
print("FAIL")
else:
print("passed")
# ***** TEST OF FILESYSTEM MOUNT *****
@ -137,8 +149,9 @@ def cptest(stm=False): # Assumes pre-existing filesystem of either type
print("Fail mounting device. Have you formatted it?")
return
print("Mounted device.")
cp("eep_spi.py", "/eeprom/")
cp("eeprom_spi.py", "/eeprom/")
cp(__file__, "/eeprom/")
# We may have the source file or a precompiled binary (*.mpy)
cp(__file__.replace("eep", "eeprom"), "/eeprom/")
print('Contents of "/eeprom": {}'.format(uos.listdir("/eeprom")))
print(uos.statvfs("/eeprom"))
@ -146,23 +159,29 @@ def cptest(stm=False): # Assumes pre-existing filesystem of either type
# ***** TEST OF HARDWARE *****
def full_test(stm=False):
eep = get_eep(stm)
page = 0
block = 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))
print(f"Block {block} passed")
else:
print("Page {} readback failed.".format(page))
print(f"Block {block} readback failed.")
break
page += 1
block += 1
test_str = """Available tests (see SPI.md):
test(stm=False) Basic hardware test.
full_test(stm=False) Thorough hardware test.
fstest(format=False, stm=False) Filesystem test (see doc).
cptest(stm=False) Copy files to filesystem (see doc).
def help():
test_str = """Available commands (see SPI.md):
help() Print this text.
test(stm=False) Basic hardware test.
full_test(stm=False) Thorough hardware test.
fstest(format=False, stm=False) Filesystem test (see doc).
cptest(stm=False) Copy files to filesystem (see doc).
stm: True is 256K chip, 5MHz bus. False is 128K chip, 20MHz bus.
"""
print(test_str)
print(test_str)
help()

Wyświetl plik

@ -2,114 +2,89 @@
# tested devices).
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2019-2022 Peter Hinch
# Copyright (c) 2019-2024 Peter Hinch
# Thanks are due to Abel Deuring for help in diagnosing and fixing a page size issue.
import time
from os import urandom
from micropython import const
from bdevice import BlockDevice
from bdevice import EepromDevice
# Supported instruction set - common to both chips:
_READ = const(3)
_WRITE = const(2)
_WREN = const(6) # Write enable
_RDSR = const(5) # Read status register
# Microchip only:
_RDID = const(0xAB) # Read chip ID
_CE = const(0xC7) # Chip erase
# STM only:
_RDID_STM = const(0x83) # Read ID page
_WRID_STM = const(0x82)
_STM_ID = const(0x30) # Arbitrary ID for STM chip
# Not implemented: Write disable and Write status register
# _WRDI = const(4)
# _WRSR = const(1)
# Logical EEPROM device comprising one or more physical chips sharing an SPI bus.
class EEPROM(BlockDevice):
def __init__(self, spi, cspins, size=128, verbose=True, block_size=9):
# args: virtual block size in bits, no. of chips, bytes in each chip
# args: SPI bus, tuple of CS Pin instances, chip size in KiB
# verbose: Test for chip presence and report
# block_size: Sector size for filesystems. See docs.
# erok: True if chip supports erase.
# page_size: None is auto detect. See docs.
class EEPROM(EepromDevice):
def __init__(self, spi, cspins, size, verbose=True, block_size=9, page_size=None):
if size not in (64, 128, 256):
print("Warning: possible unsupported chip. Size:", size)
super().__init__(block_size, len(cspins), size * 1024)
self._stm = size == 256
print(f"Warning: possible unsupported chip. Size: {size}KiB")
self._spi = spi
self._cspins = cspins
self._ccs = None # Chip select Pin object for current chip
self._size = size * 1024 # Chip size in bytes
self._bufp = bytearray(5) # instruction + 3 byte address + 1 byte value
self._mvp = memoryview(self._bufp) # cost-free slicing
self.scan(verbose)
if verbose: # Test for presence of devices
self.scan()
# superclass figures out _page_size and _page_mask
super().__init__(block_size, len(cspins), self._size, page_size, verbose)
if verbose:
print(f"Total EEPROM size {self._a_bytes:,} bytes.")
# Read ID block ID[0]
def _stm_rdid(self, n):
cs = self._cspins[n]
# Low level device presence detect. Reads a location, then writes to it. If
# a write value is passed, uses that, otherwise writes the one's complement
# of the value read.
def _devtest(self, cs, la, v=None):
buf = bytearray(1)
mvp = self._mvp
mvp[:] = b"\0\0\0\0\0"
mvp[0] = _RDID_STM
mvp[:] = b"\0" * 5
# mvp[1] = la >> 16
# mvp[2] = (la >> 8) & 0xFF
# mvp[3] = la & 0xFF
mvp[0] = _READ
cs(0)
self._spi.write_readinto(mvp, mvp)
self._spi.write(mvp[:4])
res = self._spi.read(1)
cs(1)
return mvp[4]
# Write a fixed value to ID[0]
def _stm_wrid(self, n):
cs = self._ccs
mvp = self._mvp
mvp[0] = _WREN
cs(0)
self._spi.write(mvp[:1]) # Enable write
self._spi.write(mvp[:1])
cs(1)
mvp[:] = b"\0\0\0\0\0"
mvp[0] = _WRID_STM
mvp[4] = _STM_ID
mvp[0] = _WRITE
cs(0)
self._spi.write(mvp)
cs(1)
self._wait_rdy()
self._spi.write(mvp[:4])
buf[0] = res[0] ^ 0xFF if v is None else v
self._spi.write(buf)
cs(1) # Trigger write start
self._ccs = cs
self._wait_rdy() # Wait until done (6ms max)
return res[0]
# Check for valid hardware on each CS pin: use ID block
def _stm_scan(self):
def scan(self):
# Generate a random address to minimise wear
la = int.from_bytes(urandom(3), "little") % self._size
for n, cs in enumerate(self._cspins):
self._ccs = cs
if self._stm_rdid(n) != _STM_ID:
self._stm_wrid(n)
if self._stm_rdid(n) != _STM_ID:
raise RuntimeError("M95M02 chip not found at cs[{}].".format(n))
old = self._devtest(cs, la)
new = self._devtest(cs, la, old)
if old != new ^ 0xFF:
raise RuntimeError(f"Chip not found at cs[{n}]")
print(f"{n + 1} chips detected.")
return n
# Scan for Microchip devices: read manf ID
def _mc_scan(self):
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, mvp)
cs(1)
if mvp[4] != 0x29:
raise RuntimeError("25xx1024 chip not found at cs[{}].".format(n))
return n
# Check for a valid hardware configuration
def scan(self, verbose):
n = self._stm_scan() if self._stm else self._mc_scan()
if verbose:
s = "{} chips detected. Total EEPROM size {}bytes."
print(s.format(n + 1, self._a_bytes))
def erase(self):
if self._stm:
raise RuntimeError("Erase not available on STM chip")
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
block = b"\0" * 256
for n in range(0, self._a_bytes, 256):
self[n : n + 256] = block
def _wait_rdy(self): # After a write, wait for device to become ready
mvp = self._mvp
@ -134,7 +109,7 @@ class EEPROM(BlockDevice):
mvp[1] = la >> 16
mvp[2] = (la >> 8) & 0xFF
mvp[3] = la & 0xFF
pe = (addr & ~0xFF) + 0x100 # byte 0 of next page
pe = (la & self._page_mask) + self._page_size # byte 0 of next page
return min(nbytes, pe - la)
# Read or write multiple bytes at an arbitrary address