Browse Source

Merge branch 'MK3_3.10.1' into PFW-1271_PF-buildv20

DRracer 2 years ago
parent
commit
fd6dbba06f
14 changed files with 930 additions and 2 deletions
  1. 1 0
      .gitignore
  2. 2 2
      Firmware/Configuration.h
  3. 54 0
      tools/README.md
  4. 59 0
      tools/dump2bin
  5. 17 0
      tools/dump_crash
  6. 17 0
      tools/dump_eeprom
  7. 17 0
      tools/dump_sram
  8. 389 0
      tools/elf_mem_map
  9. 4 0
      tools/lib/avr.py
  10. 169 0
      tools/lib/dump.py
  11. 12 0
      tools/noreset
  12. 54 0
      tools/update_eeprom
  13. 89 0
      tools/utils.gdb
  14. 46 0
      tools/xfimg2dump

+ 1 - 0
.gitignore

@@ -2,6 +2,7 @@
 .project
 .cproject
 Debug
+__pycache__
 Firmware/Configuration_prusa.h
 Firmware/Doc
 /Firmware/.vs/Firmware/v14

+ 2 - 2
Firmware/Configuration.h

@@ -18,7 +18,7 @@ extern PGM_P sPrinterName;
 // Firmware version
 #define FW_MAJOR 3
 #define FW_MINOR 10
-#define FW_REVISION 0
+#define FW_REVISION 1
 //#define FW_FLAVOR RC      //uncomment if DEBUG, DEVEL, APLHA, BETA or RC
 //#define FW_FLAVERSION 1     //uncomment if FW_FLAVOR is defined and versioning is needed.
 #ifndef FW_FLAVOR
@@ -27,7 +27,7 @@ extern PGM_P sPrinterName;
     #define FW_VERSION STR(FW_MAJOR) "." STR(FW_MINOR) "." STR(FW_REVISION) "-" STR(FW_FLAVOR) "" STR(FW_FLAVERSION)
 #endif
 
-#define FW_COMMIT_NR 4481
+#define FW_COMMIT_NR 4587
 
 // FW_VERSION_UNKNOWN means this is an unofficial build.
 // The firmware should only be checked into github with this symbol.

+ 54 - 0
tools/README.md

@@ -0,0 +1,54 @@
+# Host debugging tools for Prusa MK3 firmware
+
+## Tools
+
+### ``dump_eeprom``
+
+Dump the content of the entire EEPROM using the D3 command.
+Requires ``printcore`` from [Pronterface].
+
+### ``dump_sram``
+
+Dump the content of the entire SRAM using the D2 command.
+Requires ``printcore`` from [Pronterface].
+
+### ``dump_crash``
+
+Dump the content of the last crash dump on MK3+ printers using D21.
+Requires ``printcore`` from [Pronterface].
+
+### ``elf_mem_map``
+
+Generate a symbol table map with decoded information starting directly from an ELF firmware with DWARF debugging information (which is the default using the stock board definition).
+
+When used along with a memory dump obtained from the D2 g-code, show the value of each symbol which is within the address range of the dump.
+
+When used with ``--map`` and a single elf file, generate a map consisting of memory location and source location for each statically-addressed variable.
+
+With ``--qdirstat`` and a single elf file, generate a [qdirstat](https://github.com/shundhammer/qdirstat) compatible cache file which can be loaded to inspect memory utilization interactively in a treemap.
+
+This assumes the running firmware generating the dump and the elf file are the same.
+Requires Python3 and the [pyelftools](https://github.com/eliben/pyelftools) module.
+
+### ``dump2bin``
+
+Parse and decode a memory dump obtained from the D2/D21/D23 g-code into readable metadata and binary. The output binary is padded and extended to fit the original address range.
+
+### ``xfimg2dump``
+
+Extract a crash dump from an external flash image and output the same format produced by the D21 g-code.
+
+### ``update_eeprom``
+
+Given one EEPROM dump, convert the dump to update instructions that can be sent to a printer.
+
+Given two EEPROM dumps, produces only the required instructions needed to update the contents from the first to the second. This is currently quite crude and assumes dumps are aligned (starting from the same address or same stride).
+
+Optionally writes the instructions to the specified port (requires ``printcore`` from [Pronterface]).
+
+### ``noreset``
+
+Set the required TTY flags on the specified port to avoid reset-on-connect for *subsequent* requests (issuing this command might still cause the printer to reset).
+
+
+[Pronterface]: https://github.com/kliment/Printrun

+ 59 - 0
tools/dump2bin

@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+import argparse
+import os, sys
+
+from lib.dump import decode_dump
+
+
+def main():
+    # parse the arguments
+    ap = argparse.ArgumentParser(description="""
+        Parse and decode a memory dump obtained from the D2/D21/D23 g-code
+        into readable metadata and binary. The output binary is padded and
+        extended to fit the original address range.
+    """)
+    ap.add_argument('-i', dest='info', action='store_true',
+                    help='display crash info only')
+    ap.add_argument('dump')
+    ap.add_argument('output', nargs='?')
+    args = ap.parse_args()
+
+    # decode the dump data
+    dump = decode_dump(args.dump)
+    if dump is None:
+        return os.EX_DATAERR
+
+    # output descriptors
+    if args.info:
+        o_fd = None
+        o_md = sys.stdout
+    elif args.output is None:
+        o_fd = sys.stdout.buffer
+        o_md = sys.stderr
+    else:
+        o_fd = open(args.output, 'wb')
+        o_md = sys.stdout
+
+    # output binary
+    if o_fd:
+        o_fd.write(dump.data)
+        o_fd.close()
+
+    # metadata
+    print('   dump type: {typ}\n'
+          'crash reason: {reason}\n'
+          '   registers: {regs}\n'
+          '          PC: {pc}\n'
+          '          SP: {sp}\n'
+          '      ranges: {ranges}'.format(
+              typ=dump.typ,
+              reason=dump.reason.name if dump.reason is not None else 'N/A',
+              regs=dump.regs,
+              pc='{:#x}'.format(dump.pc) if dump.pc is not None else 'N/A',
+              sp='{:#x}'.format(dump.sp) if dump.sp is not None else 'N/A',
+              ranges=str(dump.ranges)),
+          file=o_md)
+
+
+if __name__ == '__main__':
+    exit(main())

+ 17 - 0
tools/dump_crash

@@ -0,0 +1,17 @@
+#!/bin/sh
+prg=$(basename "$0")
+port="$1"
+if [ -z "$port" -o "$port" = "-h" ]
+then
+  echo "usage: $0 <port>" >&2
+  echo "Connect to <port> and dump the content of last crash using D21 to stdout" >&2
+  exit 1
+fi
+
+set -e
+tmp=$(mktemp)
+trap "rm -f \"$tmp\"" EXIT
+
+echo D21 > "$tmp"
+printcore -v "$port" "$tmp" 2>&1 | \
+    sed -ne '/^RECV: D21 /,/RECV: ok$/s/^RECV: //p'

+ 17 - 0
tools/dump_eeprom

@@ -0,0 +1,17 @@
+#!/bin/sh
+prg=$(basename "$0")
+port="$1"
+if [ -z "$port" -o "$port" = "-h" ]
+then
+  echo "usage: $0 <port>" >&2
+  echo "Connect to <port> and dump the content of the EEPROM using D3 to stdout" >&2
+  exit 1
+fi
+
+set -e
+tmp=$(mktemp)
+trap "rm -f \"$tmp\"" EXIT
+
+echo D3 > "$tmp"
+printcore -v "$port" "$tmp" 2>&1 | \
+    sed -ne '/^RECV: D3 /,/RECV: ok$/s/^RECV: //p'

+ 17 - 0
tools/dump_sram

@@ -0,0 +1,17 @@
+#!/bin/sh
+prg=$(basename "$0")
+port="$1"
+if [ -z "$port" -o "$port" = "-h" ]
+then
+  echo "usage: $0 <port>" >&2
+  echo "Connect to <port> and dump the content of the SRAM using D2 to stdout" >&2
+  exit 1
+fi
+
+set -e
+tmp=$(mktemp)
+trap "rm -f \"$tmp\"" EXIT
+
+echo D2 > "$tmp"
+printcore -v "$port" "$tmp" 2>&1 | \
+    sed -ne '/^RECV: D2 /,/RECV: ok$/s/^RECV: //p'

+ 389 - 0
tools/elf_mem_map

@@ -0,0 +1,389 @@
+#!/usr/bin/env python3
+import argparse
+import elftools.elf.elffile
+import elftools.dwarf.descriptions
+from collections import namedtuple
+from struct import unpack
+import os
+
+from lib.dump import decode_dump
+from lib.avr import *
+
+
+Entry = namedtuple('Entry', ['name', 'loc', 'size', 'declpos'])
+Member = namedtuple('Member', ['name', 'off', 'size'])
+
+
+def array_inc(loc, dim, idx=0):
+    if idx == len(dim):
+        return True
+    loc[idx] += 1
+    if loc[idx] == dim[idx]:
+        loc[idx] = 0
+        return array_inc(loc, dim, idx+1)
+    return False
+
+def get_type_size(type_DIE):
+    while True:
+        if 'DW_AT_byte_size' in type_DIE.attributes:
+            return type_DIE, type_DIE.attributes.get('DW_AT_byte_size').value
+        if 'DW_AT_type' not in type_DIE.attributes:
+            return None
+        type_DIE = type_DIE.get_DIE_from_attribute('DW_AT_type')
+
+def get_type_arrsize(type_DIE):
+    size = get_type_size(type_DIE)
+    if size is None:
+        return None
+    byte_size = size[1]
+    if size[0].tag != 'DW_TAG_pointer_type':
+        array_DIE = get_type_def(type_DIE, 'DW_TAG_array_type')
+        if array_DIE is not None:
+            for range_DIE in array_DIE.iter_children():
+                if range_DIE.tag == 'DW_TAG_subrange_type' and \
+                   'DW_AT_upper_bound' in range_DIE.attributes:
+                    dim = range_DIE.attributes['DW_AT_upper_bound'].value + 1
+                    byte_size *= dim
+    return byte_size
+
+def get_type_def(type_DIE, type_tag):
+    while True:
+        if type_DIE.tag == type_tag:
+            return type_DIE
+        if 'DW_AT_type' not in type_DIE.attributes:
+            return None
+        type_DIE = type_DIE.get_DIE_from_attribute('DW_AT_type')
+
+def get_FORM_block1(attr):
+    if attr.form != 'DW_FORM_block1':
+        return None
+    if attr.value[0] == 3: # OP_addr
+        return int.from_bytes(attr.value[1:], 'little')
+    if attr.value[0] == 35: # OP_plus_uconst (ULEB128)
+        v = 0
+        s = 0
+        for b in attr.value[1:]:
+            v |= (b & 0x7f) << s
+            if b & 0x80 == 0:
+                break
+            s += 7
+        return v
+    return None
+
+
+def get_array_dims(DIE):
+    array_DIE = get_type_def(DIE, 'DW_TAG_array_type')
+    if array_DIE is None:
+        return []
+
+    array_dim = []
+    for range_DIE in array_DIE.iter_children():
+        if range_DIE.tag == 'DW_TAG_subrange_type' and \
+           'DW_AT_upper_bound' in range_DIE.attributes:
+            array_dim.append(range_DIE.attributes['DW_AT_upper_bound'].value + 1)
+    return array_dim
+
+
+def get_struct_members(DIE, entry, expand_structs, struct_gaps):
+    struct_DIE = get_type_def(DIE, 'DW_TAG_structure_type')
+    if struct_DIE is None:
+        return []
+
+    members = []
+    for member_DIE in struct_DIE.iter_children():
+        if member_DIE.tag == 'DW_TAG_member' and 'DW_AT_name' in member_DIE.attributes:
+            m_name = member_DIE.attributes['DW_AT_name'].value.decode('ascii')
+            m_off = get_FORM_block1(member_DIE.attributes['DW_AT_data_member_location'])
+            m_byte_size = get_type_size(member_DIE)[1]
+
+            # still expand member arrays
+            m_array_dim = get_array_dims(member_DIE)
+
+            if m_byte_size == 1 and len(m_array_dim) > 1:
+                # likely string, remove one dimension
+                m_byte_size *= m_array_dim.pop()
+            if len(m_array_dim) == 0 or (len(m_array_dim) == 1 and m_array_dim[0] == 1):
+                # plain entry
+                members.append(Member(m_name, m_off, m_byte_size))
+            elif len(m_array_dim) == 1 and m_byte_size == 1:
+                # likely string, avoid expansion
+                members.append(Member(m_name + '[]', m_off, m_array_dim[0]))
+            else:
+                # expand array entries
+                m_array_pos = m_off
+                m_array_loc = [0] * len(m_array_dim)
+                while True:
+                    # location index
+                    sfx = ''
+                    for d in range(len(m_array_dim)):
+                        sfx += '[{}]'.format(str(m_array_loc[d]).rjust(len(str(m_array_dim[d]-1)), '0'))
+                    members.append(Member(m_name + sfx, m_array_pos, m_byte_size))
+                    # advance
+                    if array_inc(m_array_loc, m_array_dim):
+                        break
+                    m_array_pos += m_byte_size
+
+    if struct_gaps and len(members):
+        # fill gaps in the middle
+        members = list(sorted(members, key=lambda x: x.off))
+        last_end = 0
+        for n in range(len(members)):
+            member = members[n]
+            if member.off > last_end:
+                members.append(Member('*UNKNOWN*', last_end, member.off - last_end))
+            last_end = member.off + member.size
+
+    if struct_gaps and len(members):
+        # fill gap at the end
+        members = list(sorted(members, key=lambda x: x.off))
+        last = members[-1]
+        last_end = last.off + last.size
+        if entry.size > last_end:
+            members.append(Member('*UNKNOWN*', last_end, entry.size - last_end))
+
+    return members
+
+
+def get_elf_globals(path, expand_structs, struct_gaps=True):
+    fd = open(path, "rb")
+    if fd is None:
+        return
+    elffile = elftools.elf.elffile.ELFFile(fd)
+    if elffile is None or not elffile.has_dwarf_info():
+        return
+
+    # probably not needed, since we're decoding expressions manually
+    elftools.dwarf.descriptions.set_global_machine_arch(elffile.get_machine_arch())
+    dwarfinfo = elffile.get_dwarf_info()
+
+    grefs = []
+    for CU in dwarfinfo.iter_CUs():
+        file_entries = dwarfinfo.line_program_for_CU(CU).header["file_entry"]
+
+        for DIE in CU.iter_DIEs():
+            # handle only variable types
+            if DIE.tag != 'DW_TAG_variable':
+                continue
+            if 'DW_AT_location' not in DIE.attributes:
+                continue
+            if 'DW_AT_name' not in DIE.attributes and \
+               'DW_AT_abstract_origin' not in DIE.attributes:
+                continue
+
+            # handle locations encoded directly as DW_OP_addr (leaf globals)
+            loc = get_FORM_block1(DIE.attributes['DW_AT_location'])
+            if loc is None or loc < SRAM_OFFSET or loc >= EEPROM_OFFSET:
+                continue
+            loc -= SRAM_OFFSET
+
+            # variable name/type
+            if 'DW_AT_name' not in DIE.attributes and \
+               'DW_AT_abstract_origin' in DIE.attributes:
+                DIE = DIE.get_DIE_from_attribute('DW_AT_abstract_origin')
+                if 'DW_AT_location' in DIE.attributes:
+                    # duplicate reference (handled directly), skip
+                    continue
+            if 'DW_AT_name' not in DIE.attributes:
+                continue
+            if 'DW_AT_type' not in DIE.attributes:
+                continue
+
+            name = DIE.attributes['DW_AT_name'].value.decode('ascii')
+
+            # get final storage size
+            size = get_type_size(DIE)
+            if size is None:
+                continue
+            byte_size = size[1]
+
+            # location of main definition
+            declpos = ''
+            if 'DW_AT_decl_file' in DIE.attributes and \
+               'DW_AT_decl_line' in DIE.attributes:
+                line = DIE.attributes['DW_AT_decl_line'].value
+                fname = DIE.attributes['DW_AT_decl_file'].value
+                if fname and fname - 1 < len(file_entries):
+                    fname = file_entries[fname-1].name.decode('ascii')
+                    declpos = '{}:{}'.format(fname, line)
+
+            # fetch array dimensions (if known)
+            array_dim = get_array_dims(DIE)
+
+            # fetch structure members (one level only)
+            entry = Entry(name, loc, byte_size, declpos)
+            if not expand_structs or size[0].tag == 'DW_TAG_pointer_type':
+                members = []
+            else:
+                members = get_struct_members(DIE, entry, expand_structs, struct_gaps)
+
+            def expand_members(entry, members):
+                if len(members) == 0:
+                    grefs.append(entry)
+                else:
+                    for member in members:
+                        grefs.append(Entry(entry.name + '.' + member.name,
+                                           entry.loc + member.off, member.size,
+                                           entry.declpos))
+
+            if byte_size == 1 and len(array_dim) > 1:
+                # likely string, remove one dimension
+                byte_size *= array_dim.pop()
+            if len(array_dim) == 0 or (len(array_dim) == 1 and array_dim[0] == 1):
+                # plain entry
+                expand_members(entry, members)
+            elif len(array_dim) == 1 and byte_size == 1:
+                # likely string, avoid expansion
+                grefs.append(Entry(entry.name + '[]', entry.loc,
+                                   array_dim[0], entry.declpos))
+            else:
+                # expand array entries
+                array_pos = loc
+                array_loc = [0] * len(array_dim)
+                while True:
+                    # location index
+                    sfx = ''
+                    for d in range(len(array_dim)):
+                        sfx += '[{}]'.format(str(array_loc[d]).rjust(len(str(array_dim[d]-1)), '0'))
+                    expand_members(Entry(entry.name + sfx, array_pos,
+                                         byte_size, entry.declpos), members)
+                    # advance
+                    if array_inc(array_loc, array_dim):
+                        break
+                    array_pos += byte_size
+
+    return grefs
+
+
+def annotate_refs(grefs, addr, data, width, gaps=True, overlaps=True):
+    last_end = None
+    for entry in grefs:
+        if entry.loc < addr:
+            continue
+        if entry.loc + entry.size > addr + len(data):
+            continue
+
+        pos = entry.loc-addr
+        end_pos = pos + entry.size
+        buf = data[pos:end_pos]
+
+        buf_repr = ''
+        if len(buf) in [1, 2, 4]:
+            # attempt to decode as integers
+            buf_repr += ' I:' + str(int.from_bytes(buf, 'little')).rjust(10)
+        if len(buf) in [4, 8]:
+            # attempt to decode as floats
+            typ = 'f' if len(buf) == 4 else 'd'
+            buf_repr += ' F:' + '{:10.3f}'.format(unpack(typ, buf)[0])
+
+        if last_end is not None:
+            if gaps and last_end < pos:
+                # decode gaps
+                gap_size = pos - last_end
+                gap_buf = data[last_end:pos]
+                print('{:04x} {} {:4} R:{}'.format(addr+last_end, "*UNKNOWN*".ljust(width),
+                                                   gap_size, gap_buf.hex()))
+            if overlaps and last_end > pos + 1:
+                gap_size = pos - last_end
+                print('{:04x} {} {:4}'.format(addr+last_end, "*OVERLAP*".ljust(width), gap_size))
+
+        print('{:04x} {} {:4}{} R:{}'.format(entry.loc, entry.name.ljust(width),
+                                             entry.size, buf_repr, buf.hex()))
+        last_end = end_pos
+
+
+def print_map(grefs):
+    print('OFFSET\tSIZE\tNAME\tDECLPOS')
+    for entry in grefs:
+        print('{:x}\t{}\t{}\t{}'.format(entry.loc, entry.size, entry.name, entry.declpos))
+
+
+def print_qdirstat(grefs):
+    print('[qdirstat 1.0 cache file]')
+
+    entries = {}
+    for entry in grefs:
+        # do not output registers when looking at space usage
+        if entry.loc < SRAM_START:
+            continue
+
+        paths = list(filter(None, re.split(r'[\[\].]', entry.name)))
+        base = entries
+        for i in range(len(paths) - 1):
+            name = paths[i]
+            if name not in base:
+                base[name] = {}
+            base = base[name]
+        name = paths[-1]
+        if name in base:
+            name = '{}_{:x}'.format(entry.name, entry.loc)
+        base[name] = entry.size
+
+    def walker(root, prefix):
+        files = []
+        dirs = []
+
+        for name, entries in root.items():
+            if type(entries) == int:
+                files.append([name, entries])
+            else:
+                dirs.append([name, entries])
+
+        # print files
+        print('D\t{}\t{}\t0x0'.format(prefix, 0))
+        for name, size in files:
+            print('F\t{}\t{}\t0x0'.format(name, size))
+
+        # recurse directories
+        for name, entries in dirs:
+            walker(entries, prefix + '/' + name)
+
+    walker(entries, '/')
+
+
+def main():
+    ap = argparse.ArgumentParser(description="""
+        Generate a symbol table map starting directly from an ELF
+        firmware with DWARF3 debugging information.
+        When used along with a memory dump obtained from the D2/D21/D23 g-code,
+        show the value of each symbol which is within the address range.
+    """)
+    ap.add_argument('elf', help='ELF file containing DWARF debugging information')
+    ap.add_argument('--no-gaps', action='store_true',
+                    help='do not dump memory inbetween known symbols')
+    ap.add_argument('--no-expand-structs', action='store_true',
+                    help='do not decode structure data')
+    ap.add_argument('--overlaps', action='store_true',
+                    help='annotate overlaps greater than 1 byte')
+    ap.add_argument('--name-width', type=int, default=50,
+                    help='set name column width')
+    g = ap.add_mutually_exclusive_group(required=True)
+    g.add_argument('dump', nargs='?', help='RAM dump obtained from D2 g-code')
+    g.add_argument('--map', action='store_true', help='dump global memory map')
+    g.add_argument('--qdirstat', action='store_true',
+                   help='dump qdirstat-compatible size usage map')
+    args = ap.parse_args()
+
+    grefs = get_elf_globals(args.elf, expand_structs=not args.no_expand_structs)
+    grefs = list(sorted(grefs, key=lambda x: x.loc))
+    if args.map:
+        print_map(grefs)
+    elif args.qdirstat:
+        print_qdirstat(grefs)
+    else:
+        # fetch the memory data
+        dump = decode_dump(args.dump)
+        if dump is None:
+            return os.EX_DATAERR
+
+        # strip padding, if present
+        addr_start = dump.ranges[0][0]
+        addr_end = dump.ranges[-1][0]+dump.ranges[-1][1]
+        data = dump.data[addr_start:addr_end]
+
+        annotate_refs(grefs, addr_start, data,
+                      width=args.name_width,
+                      gaps=not args.no_gaps,
+                      overlaps=args.overlaps)
+
+if __name__ == '__main__':
+    exit(main())

+ 4 - 0
tools/lib/avr.py

@@ -0,0 +1,4 @@
+SRAM_START = 0x200
+SRAM_SIZE = 0x2000
+SRAM_OFFSET = 0x800000
+EEPROM_OFFSET = 0x810000

+ 169 - 0
tools/lib/dump.py

@@ -0,0 +1,169 @@
+import sys
+import re
+import enum
+import struct
+from . import avr
+
+
+FILL_BYTE   = b'\0'      # used to fill memory gaps in the dump
+DUMP_MAGIC  = 0x55525547 # XFLASH dump magic
+DUMP_OFFSET = 0x3d000    # XFLASH dump offset
+DUMP_SIZE   = 0x2300     # XFLASH dump size
+
+class CrashReason(enum.IntEnum):
+    MANUAL = 0
+    STACK_ERROR = 1
+    WATCHDOG = 2
+    BAD_ISR = 3
+
+class Dump():
+    def __init__(self, typ, reason, regs, pc, sp, data, ranges):
+        self.typ = typ
+        self.reason = reason
+        self.regs = regs
+        self.pc = pc
+        self.sp = sp
+        self.data = data
+        self.ranges = ranges
+
+
+# expand the buffer identified by addr+data to fill the region start+size
+def region_expand(addr, data, start, size):
+    if start < addr:
+        data = FILL_BYTE * (addr - start) + data
+        addr = start
+    end = start + size
+    data_end = addr + len(data)
+    if end > data_end:
+        data += FILL_BYTE * (data_end - end)
+    return addr, data
+
+
+def merge_ranges(ranges):
+    ranges = list(sorted(ranges, key=lambda x: x[0]))
+    if len(ranges) < 2:
+        return ranges
+
+    ret = [ranges[0]]
+    for cur in ranges[1:]:
+        last = ret[-1]
+        last_end = last[0] + last[1]
+        if last_end < cur[0]:
+            ret.append(cur)
+        else:
+            cur_end = cur[0] + cur[1]
+            last = (last[0], max(last_end, cur_end) - last[0])
+            ret[-1] = last
+    return ret
+
+
+def decode_dump(path):
+    fd = open(path, 'r')
+    if fd is None:
+        return None
+
+    buf_addr = None # starting address
+    buf_data = None # data
+
+    typ = None      # dump type
+    reason = None   # crash reason
+    regs = None     # registers present
+    pc = None       # PC address
+    sp = None       # SP address
+    ranges = []     # dumped ranges
+
+    in_dump = False
+    for line in enumerate(fd):
+        line = (line[0], line[1].rstrip())
+        tokens = line[1].split(maxsplit=1)
+
+        def line_error():
+            print('malformed line {}: {}'.format(*line), file=sys.stderr)
+
+        # handle metadata
+        if not in_dump:
+            if len(tokens) > 0 and tokens[0] in ['D2', 'D21', 'D23']:
+                in_dump = True
+                typ = tokens[0]
+            continue
+        else:
+            if len(tokens) == 0:
+                line_error()
+                continue
+            elif tokens[0] == 'ok':
+                break
+            elif tokens[0] == 'error:' and len(tokens) == 2:
+                values = tokens[1].split(' ')
+                if typ == 'D23' and len(values) >= 3:
+                    reason = CrashReason(int(values[0], 0))
+                    pc = int(values[1], 0)
+                    sp = int(values[2], 0)
+                else:
+                    line_error()
+                continue
+            elif len(tokens) != 2 or not re.match(r'^[0-9a-fA-F]+$', tokens[0]):
+                line_error()
+                continue
+
+        # decode hex data
+        addr = int.from_bytes(bytes.fromhex(tokens[0]), 'big')
+        data = bytes.fromhex(tokens[1])
+        ranges.append((addr, len(data)))
+
+        if buf_addr is None:
+            buf_addr = addr
+            buf_data = data
+        else:
+            # grow buffer as needed
+            buf_addr, buf_data = region_expand(buf_addr, buf_data,
+                                               addr, len(data))
+
+            # replace new part
+            rep_start = addr - buf_addr
+            rep_end = rep_start + len(data)
+            buf_data = buf_data[:rep_start] + data + buf_data[rep_end:]
+
+    # merge continuous ranges
+    ranges = merge_ranges(ranges)
+
+    if typ == 'D2':
+        # D2 doesn't guarantee registers to be present
+        regs = len(ranges) > 0 and \
+            ranges[0][0] == 0 and \
+            ranges[0][1] >= avr.SRAM_START
+
+        # fill to fit for easy loading
+        buf_addr, buf_data = region_expand(
+            buf_addr, buf_data, 0, avr.SRAM_START + avr.SRAM_SIZE)
+
+    elif typ == 'D23':
+        # check if the dump is complete
+        if len(ranges) != 1 or ranges[0][0] != 0 or \
+           ranges[0][1] != avr.SRAM_START + avr.SRAM_SIZE:
+            print('error: incomplete D23 dump', file=sys.stderr)
+            return None
+
+        regs = True
+        if reason is None:
+            print('warning: no error line in D23', file=sys.stderr)
+
+    elif typ == 'D21':
+        if len(ranges) != 1 or len(buf_data) != (avr.SRAM_START + avr.SRAM_SIZE + 256):
+            print('error: incomplete D21 dump', file=sys.stderr)
+            return None
+
+        # decode the header structure
+        magic, regs_present, crash_reason, pc, sp = struct.unpack('<LBBLH', buf_data[0:12])
+        if magic != DUMP_MAGIC:
+            print('error: invalid dump header in D21', file=sys.stderr)
+            return None
+
+        regs = bool(regs_present)
+        reason = CrashReason(crash_reason)
+
+        # extract the data section
+        buf_addr = 0
+        buf_data = buf_data[256:]
+        ranges[0] = (0, len(buf_data))
+
+    return Dump(typ, reason, regs, pc, sp, buf_data, ranges)

+ 12 - 0
tools/noreset

@@ -0,0 +1,12 @@
+#!/bin/sh
+prg=$(basename "$0")
+port="$1"
+if [ -z "$port" -o "$port" = "-h" ]
+then
+  echo "usage: $0 <port>" >&2
+  echo "Set TTY flags on <port> to avoid reset-on-connect" >&2
+  exit 1
+fi
+
+set -e
+stty -F "$port" -hup

+ 54 - 0
tools/update_eeprom

@@ -0,0 +1,54 @@
+#!/bin/sh
+prg=$(basename "$0")
+
+# parse arguments
+while getopts f:h optname
+do
+  case $optname in
+  f)    port="$OPTARG" ;;
+  *)    help=1 ;;
+  esac
+done
+shift `expr $OPTIND - 1`
+
+old="$1"
+new="$2"
+
+if [ -z "$old" -o "$help" = "-h" -o "$#" -gt 2 ]
+then
+  echo "usage: $0 [-f <port>] <old dump> [<new dump>]" >&2
+  echo "Convert <old dump> to instructions to update instructions." >&2
+  echo "With <new dump>, generate instructions to update EEPROM changes only." >&2
+  echo "Optionally write such changes directly if <port> if given." >&2
+  exit 1
+fi
+
+set -e
+instr=$(mktemp)
+trap "rm -f \"$instr\"" EXIT
+
+convert()
+{
+    sed -ne 's/^\([0-9a-f]\{4\}\) \([0-9a-f ]*\)$/D3 Ax\1 C16 X\2/p' "$@"
+}
+
+if [ -z "$new" ]; then
+    # convert the instructions to updates
+    convert "$old" > "$instr"
+else
+    tmp1=$(mktemp)
+    tmp2=$(mktemp)
+    trap "rm -f \"$tmp1\" \"$tmp2\"" EXIT
+
+    convert "$old" > "$tmp1"
+    convert "$new" > "$tmp2"
+
+    comm -13 "$tmp1" "$tmp2" > "$instr"
+fi
+
+# write the instructions if requested
+if [ -z "$port" ]; then
+    cat "$instr"
+else
+    printcore -v "$port" "$instr"
+fi

+ 89 - 0
tools/utils.gdb

@@ -0,0 +1,89 @@
+# -*- gdb-script -*-
+
+define load_dump
+  restore $arg0 binary 0x800000
+  set $pc = (((unsigned long)$arg1) - 2) << 1
+  set $sp = $arg2
+  where
+end
+
+document load_dump
+Load a crash dump, setup PC/SP and show the current backtrace
+Usage: load_dump <file> <PC-addr> <SP-addr>
+end
+
+
+define sp_skip
+  if $argc == 0
+    set $shift = 3
+  else
+    set $shift = $arg0
+  end
+  set $new_pc = ((((unsigned long)*(uint8_t*)($sp+$shift+1)) << 16) + \
+                 (((unsigned long)*(uint8_t*)($sp+$shift+2)) << 8) + \
+                 (((unsigned long)*(uint8_t*)($sp+$shift+3)) << 0)) << 1
+  set $new_sp = $sp+$shift+3
+  select-frame 0
+  set $saved_pc = $pc
+  set $saved_sp = $sp
+  set $pc = $new_pc
+  set $sp = $new_sp
+  where
+end
+
+document sp_skip
+Decode the PC address at SP+offset, then show the resulting stack.
+The default (and minimum) offset is 3.
+Usage: sp_skip [off]
+end
+
+
+define sp_restore
+  select-frame 0
+  set $pc = $saved_pc
+  set $sp = $saved_sp
+  where
+end
+
+document sp_restore
+Undo an sp_skip move (restore existing PC/SP positions)
+Usage: sp_restore
+end
+
+
+define sp_test
+  sp_skip $arg0
+  set $pc = $saved_pc
+  set $sp = $saved_sp
+end
+
+document sp_test
+Attempt to decode the PC address at SP+offset, then show the resulting stack.
+The default (and minimum) offset is 3.
+Usage: sp_test [off]
+end
+
+
+define sp_scan
+  dont-repeat
+
+  if $argc == 0
+    set $sp_end = 0x802200
+  else
+    set $sp_end = $arg0
+  end
+
+  set $sp_pos = $sp
+  while $sp_pos < ($sp_end-4)
+    set $sp_off = $sp_pos - $sp
+    printf "**** scanning %#x (+%u) ****\n", $sp_pos, $sp_off
+    sp_test $sp_off
+    set $sp_pos += 1
+  end
+end
+
+document sp_scan
+Attempt to decode PC at any location starting from the SP+3 and up to SP-end
+(by default the end of the SRAM) and show the resulting stack at all locations.
+Usage: sp_scan [SP-end]
+end

+ 46 - 0
tools/xfimg2dump

@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+import argparse
+import struct
+import os, sys
+
+from lib.dump import DUMP_MAGIC, DUMP_OFFSET, DUMP_SIZE
+
+
+def error(msg):
+    print(msg, file=sys.stderr)
+
+def main():
+    # parse the arguments
+    ap = argparse.ArgumentParser(description="""
+        Extract a crash dump from an external flash image and output
+        the same format produced by the D21 g-code.
+    """)
+    ap.add_argument('image')
+    args = ap.parse_args()
+
+    # read the image
+    off = DUMP_OFFSET
+    with open(args.image, 'rb') as fd:
+        fd.seek(off)
+        data = fd.read(DUMP_SIZE)
+    if len(data) != DUMP_SIZE:
+        error('incorrect image size')
+        return os.EX_DATAERR
+
+    # check for magic header
+    magic, = struct.unpack('<L', data[:4])
+    if magic != DUMP_MAGIC:
+        error('invalid dump magic or no dump')
+        return os.EX_DATAERR
+
+    # output D21 dump
+    print('D21 - read crash dump', end='')
+    for i in range(len(data)):
+        if i % 16 == 0:
+            print('\n{:06x} '.format(off + i), end='')
+        print(' {:02x}'.format(data[i]), end='')
+    print('\nok')
+
+
+if __name__ == '__main__':
+    exit(main())