Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: GlasgowEmbedded/glasgow
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: f0dedfd9c3b7
Choose a base ref
...
head repository: GlasgowEmbedded/glasgow
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: de16630bd380
Choose a head ref
  • 3 commits
  • 3 files changed
  • 1 contributor

Commits on Jul 24, 2020

  1. Copy the full SHA
    ac06273 View commit details
  2. support.logging: add dump_mapseq.

    dump_seq() cannot display the length of a map() or of a generator
    expression.
    whitequark committed Jul 24, 2020
    Copy the full SHA
    eef285f View commit details
  3. Copy the full SHA
    de16630 View commit details
Showing with 185 additions and 36 deletions.
  1. +139 −33 software/glasgow/applet/memory/prom/__init__.py
  2. +1 −2 software/glasgow/cli.py
  3. +45 −1 software/glasgow/support/logging.py
172 changes: 139 additions & 33 deletions software/glasgow/applet/memory/prom/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Ref: Microchip 27C512A 512K (64K x 8) CMOS EPROM
# Accession: G00057

import re
import math
import enum
import random
import logging
import asyncio
import argparse
@@ -157,6 +159,8 @@ def elaborate(self, platform):
with m.Case(_Command.READ):
m.d.sync += dq_index.eq(0)
m.next = "READ-PULSE"
with m.Else():
m.d.comb += in_fifo.flush.eq(1)

with m.State("SEEK-RECV"):
with m.If(a_index == a_bytes):
@@ -196,6 +200,30 @@ def elaborate(self, platform):


class MemoryPROMInterface:
class Data:
def __init__(self, raw_data, dq_bytes):
self.raw_data = raw_data
self.dq_bytes = dq_bytes

def __len__(self):
return len(self.raw_data) // self.dq_bytes

def __getitem__(self, index):
if index not in range(len(self)):
raise IndexError
elem = self.raw_data[index * self.dq_bytes:(index + 1) * self.dq_bytes]
return int.from_bytes(elem, byteorder="little")

def __eq__(self, other):
return isinstance(other, type(self)) and self.raw_data == other.raw_data

def difference(self, other):
assert isinstance(other, type(self)) and len(self) == len(other)
raw_diff = ((int.from_bytes(self.raw_data, "little") ^
int.from_bytes(other.raw_data, "little"))
.to_bytes(len(self.raw_data), "little"))
return set(m.start() // self.dq_bytes for m in re.finditer(rb"[^\x00]", raw_diff))

def __init__(self, interface, logger, a_bits, dq_bits):
self.lower = interface
self._logger = logger
@@ -206,35 +234,44 @@ def __init__(self, interface, logger, a_bits, dq_bits):
def _log(self, message, *args):
self._logger.log(self._level, "PROM: " + message, *args)

async def seek(self, address):
self._log("seek a=%#x", address)
async def read_linear(self, address, count):
self._log("read linear a=%#x n=%d", address, count)
await self.lower.write([
_Command.SEEK,
*address.to_bytes(self.a_bytes, byteorder="little")
*address.to_bytes(self.a_bytes, byteorder="little"),
*[
_Command.READ,
_Command.INCR,
] * count,
])

async def _read_cmd(self, count, *, incr):
if incr:
self._log("read-incr count=%d", count)
await self.lower.write([_Command.READ, _Command.INCR] * count)
else:
self._log("read count=%d", count)
await self.lower.write([_Command.READ] * count)

async def read_bytes(self, count, *, incr=False):
await self._read_cmd(count, incr=incr)

data = await self.lower.read(count * self.dq_bytes)
self._log("read q=<%s>", dump_hex(data))
data = self.Data(await self.lower.read(count * self.dq_bytes), self.dq_bytes)
self._log("read linear q=<%s>",
dump_mapseq(" ", lambda q: f"{q:0{self.dq_bytes * 2}x}", data))
return data

async def read_words(self, count, *, incr=False):
await self._read_cmd(count, incr=incr)

data = []
for _ in range(count):
data.append(int.from_bytes(await self.lower.read(self.dq_bytes), byteorder="little"))
self._log("read q=<%s>", " ".join(f"{q:x}" for q in data))
async def read_shuffled(self, address, count):
self._log("read shuffled a=%#x n=%d", address, count)
order = [offset for offset in range(count)]
random.shuffle(order)
commands = []
for offset in order:
commands += [
_Command.SEEK,
*(address + offset).to_bytes(self.a_bytes, byteorder="little"),
_Command.READ,
]
await self.lower.write(commands)

linear_raw_chunks = [None for _ in range(count)]
shuffled_raw_data = await self.lower.read(count * self.dq_bytes)
for shuffled_offset, linear_offset in enumerate(order):
linear_raw_chunks[linear_offset] = \
shuffled_raw_data[shuffled_offset * self.dq_bytes:
(shuffled_offset + 1) * self.dq_bytes]
data = self.Data(b"".join(linear_raw_chunks), self.dq_bytes)
self._log("read shuffled q=<%s>",
dump_mapseq(" ", lambda q: f"{q:0{self.dq_bytes * 2}x}", data))
return data


@@ -247,6 +284,10 @@ class MemoryPROMApplet(GlasgowApplet, name="memory-prom"):
"27X"/"28X"/"29X" where X is a letter in their part number. This applet can also read any other
directly addressable memory.
Floating gate based memories (27x EPROM, 28x EEPROM, 29x Flash) retain data for decades, but
not indefinitely, since the stored charge slowly decays. This applet can identify memories at
risk of data loss and estimate the level of decay. See `health --help` for details.
To handle the large amount of address lines used by parallel memories, this applet supports
two kinds of addressing: direct and indirect. The full address word (specified with
the --a-bits option) is split into low and high parts. The low part is presented directly on
@@ -294,7 +335,7 @@ def build(self, target, args):
)
iface.add_subtarget(MemoryPROMSubtarget(
bus=bus,
in_fifo=iface.get_in_fifo(),
in_fifo=iface.get_in_fifo(auto_flush=False),
out_fifo=iface.get_out_fifo(),
rd_delay=1e-9 * args.read_latency,
))
@@ -339,28 +380,93 @@ def length(arg):
"-e", "--endian", choices=("little", "big"), default="little",
help="read from file using given endianness")

p_health = p_operation.add_parser(
"health", help="estimate floating gate charge decay")

p_health_mode = p_health.add_subparsers(dest="mode", metavar="MODE", required=True)

p_health_check = p_health_mode.add_parser(
"check", help="rapidly triage a memory")
p_health_check.add_argument(
"--passes", metavar="COUNT", type=int, default=5,
help="read entire memory COUNT times (default: %(default)s)")

p_health_scan = p_health_mode.add_parser(
"scan", help="detect decayed words in a memory")
p_health_scan.add_argument(
"--confirmations", metavar="COUNT", type=int, default=10,
help="read entire memory repeatedly until COUNT consecutive passes "
"detect no new decayed words (default: %(default)s)")
p_health_scan.add_argument(
"-f", "--file", metavar="FILENAME", type=argparse.FileType("wt"),
help="write hex addresses of decayed cells to FILENAME")

async def interact(self, device, args, prom_iface):
a_bits = args.a_bits
dq_bits = len(args.pin_set_dq)
depth = 1 << a_bits

if args.operation == "read":
await prom_iface.seek(args.address)
if args.file:
args.file.write(await prom_iface.read_bytes(args.length, incr=True))
else:
for word in await prom_iface.read_words(args.length, incr=True):
data = await prom_iface.read_linear(args.address, args.length)
for word in data:
if args.file:
args.file.write(word.to_bytes(prom_iface.dq_bytes, args.endian))
else:
print("{:0{}x}".format(word, (dq_bits + 3) // 4))

if args.operation == "verify":
await prom_iface.seek(args.address)
golden_data = args.file.read()
actual_data = await prom_iface.read_bytes(len(golden_data) // prom_iface.dq_bytes,
incr=True)
golden_data = prom_iface.Data(args.file.read(), prom_iface.dq_bytes)
actual_data = await prom_iface.read_linear(args.address, len(golden_data))
if actual_data == golden_data:
self.logger.info("verify PASS")
else:
raise GlasgowAppletError("verify FAIL")

if args.operation == "health" and args.mode == "check":
decayed = set()
initial_data = await prom_iface.read_linear(0, depth)

for pass_num in range(args.passes):
self.logger.info("pass %d", pass_num)

current_data = await prom_iface.read_shuffled(0, depth)
current_decayed = initial_data.difference(current_data)
for index in sorted(current_decayed - decayed):
self.logger.warning("word %#x decayed", index)
decayed.update(current_decayed)

if decayed:
raise GlasgowAppletError("health check FAIL")

self.logger.info("health %s PASS", args.mode)

if args.operation == "health" and args.mode == "scan":
decayed = set()
initial_data = await prom_iface.read_linear(0, depth)

pass_num = 0
consecutive = 0
while consecutive < args.confirmations:
self.logger.info("pass %d", pass_num)
pass_num += 1
consecutive += 1

current_data = await prom_iface.read_shuffled(0, depth)
current_decayed = initial_data.difference(current_data)
for index in sorted(current_decayed - decayed):
self.logger.warning("word %#x decayed", index)
consecutive = 0
decayed.update(current_decayed)

if args.file:
for index in sorted(decayed):
args.file.write(f"{index:x}\n")

if not decayed:
self.logger.info("health %s PASS", args.mode)
else:
raise GlasgowAppletError("health scan FAIL ({} words decayed)"
.format(len(decayed)))

# -------------------------------------------------------------------------------------------------

3 changes: 1 addition & 2 deletions software/glasgow/cli.py
Original file line number Diff line number Diff line change
@@ -406,8 +406,7 @@ def create_logger(args):

level = logging.INFO + args.quiet * 10 - args.verbose * 10
if level < 0:
dump_hex.limit = None
dump_bin.limit = None
dump_hex.limit = dump_bin.limit = dump_seq.limit = dump_mapseq.limit = None

if args.log_file or args.filter_log:
term_handler.addFilter(SubjectFilter(level, args.filter_log))
46 changes: 45 additions & 1 deletion software/glasgow/support/logging.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import operator

from .lazy import *
from .bits import bits


__all__ = ["dump_hex", "dump_bin"]
__all__ = ["dump_hex", "dump_bin", "dump_seq", "dump_mapseq"]


def dump_hex(data):
@@ -32,3 +34,45 @@ def to_bin(data):
return lazy(lambda: to_bin(data))

dump_bin.limit = 64


def dump_seq(joiner, data):
def to_seq(data):
try:
data_length = len(data)
except TypeError:
try:
data_length = data.__length_hint__()
except AttributeError:
data_length = None
if dump_seq.limit is None or (data_length is not None and
data_length < dump_seq.limit):
return joiner.join(data)
else:
return "{}... ({} elements total)".format(
joiner.join(elem for elem, _ in zip(data, range(dump_seq.limit))),
data_length or "?")
return lazy(lambda: to_seq(data))

dump_seq.limit = 16


def dump_mapseq(joiner, mapper, data):
def to_mapseq(data):
try:
data_length = len(data)
except TypeError:
try:
data_length = data.__length_hint__()
except AttributeError:
data_length = None
if dump_mapseq.limit is None or (data_length is not None and
data_length < dump_mapseq.limit):
return joiner.join(map(mapper, data))
else:
return "{}... ({} elements total)".format(
joiner.join(mapper(elem) for elem, _ in zip(data, range(dump_mapseq.limit))),
data_length or "?")
return lazy(lambda: to_mapseq(data))

dump_mapseq.limit = 16