Browse Source

Add several low-level debugging tools

Yuri D'Elia 2 years ago
parent
commit
1095b26570
6 changed files with 290 additions and 0 deletions
  1. 37 0
      tools/README.md
  2. 17 0
      tools/dump_eeprom
  3. 17 0
      tools/dump_sram
  4. 153 0
      tools/elf_mem_map
  5. 12 0
      tools/noreset
  6. 54 0
      tools/update_eeprom

+ 37 - 0
tools/README.md

@@ -0,0 +1,37 @@
+# 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].
+
+### ``elf_mem_map``
+
+Generate a symbol table map starting directly from an ELF firmware with DWARF2 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.
+
+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.
+
+### ``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

+ 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'

+ 153 - 0
tools/elf_mem_map

@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+import argparse
+import elftools.elf.elffile
+import elftools.dwarf.descriptions
+from struct import unpack
+
+SRAM_OFFSET = 0x800000
+EEPROM_OFFSET = 0x810000
+FILL_BYTE = b'\0'
+
+
+def get_elf_globals(path):
+    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():
+        for DIE in CU.iter_DIEs():
+            # handle only variable types
+            if DIE.tag != 'DW_TAG_variable':
+                continue
+            if 'DW_AT_name' not in DIE.attributes:
+                continue
+            if 'DW_AT_location' not in DIE.attributes:
+                continue
+            if 'DW_AT_type' not in DIE.attributes:
+                continue
+
+            # handle locations encoded directly as DW_OP_addr (leaf globals)
+            at_loc = DIE.attributes['DW_AT_location']
+            if at_loc.form != 'DW_FORM_block1' or at_loc.value[0] != 3:
+                continue
+            loc = (at_loc.value[1]) + (at_loc.value[2] << 8) \
+                + (at_loc.value[3] << 16) + (at_loc.value[4] << 24)
+            if loc < SRAM_OFFSET or loc >= EEPROM_OFFSET:
+                continue
+            loc -= SRAM_OFFSET
+
+            # variable name
+            name = DIE.attributes['DW_AT_name'].value.decode('ascii')
+
+            # recurse on type to find the leaf definition
+            type_DIE = DIE
+            while 'DW_AT_type' in type_DIE.attributes:
+                type_DIE = type_DIE.get_DIE_from_attribute('DW_AT_type')
+            byte_size = type_DIE.attributes.get('DW_AT_byte_size')
+            if byte_size is None:
+                continue
+            size = byte_size.value
+
+            grefs.append([name, loc, size])
+
+    return grefs
+
+
+def decode_dump(path):
+    fd = open(path, 'r')
+    if fd is None:
+        return None
+
+    buf_addr = None # starting address
+    buf_data = None # data
+
+    for line in fd:
+        tokens = line.split(maxsplit=1)
+        if len(tokens) == 0 or tokens[0] == 'ok':
+            break
+        elif len(tokens) < 2 or tokens[0] == 'D2':
+            continue
+
+        addr = int.from_bytes(bytes.fromhex(tokens[0]), 'big')
+        data = bytes.fromhex(tokens[1])
+
+        if buf_addr is None:
+            buf_addr = addr
+            buf_data = data
+        else:
+            # grow buffer as needed
+            if addr < buf_addr:
+                buf_data = FILL_BYTE * (buf_addr - addr)
+                buf_addr = addr
+            addr_end = addr + len(data)
+            buf_end = buf_addr + len(buf_data)
+            if addr_end > buf_end:
+                buf_data += FILL_BYTE * (addr_end - buf_end)
+
+            # 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:]
+
+    return (buf_addr, buf_data)
+
+
+def annotate_refs(grefs, addr, data, width=45):
+    for name, loc, size in grefs:
+        if loc < addr:
+            continue
+        if loc + size > addr + len(data):
+            continue
+
+        pos = loc-addr
+        buf = data[pos:pos+size]
+
+        buf_repr = ''
+        if len(buf) in [1, 2, 4]:
+            # attempt to decode as integers
+            buf_repr += ' I:' + str(int.from_bytes(buf, 'big')).rjust(10)
+        if len(buf) in [4, 8]:
+            # attempt to decode as floats
+            buf_repr += ' F:' + '{:10.3f}'.format(unpack('f', buf)[0])
+
+        print('{:04x} {} {:4}{} R:{}'.format(loc, name.ljust(width), size,
+                                             buf_repr, buf.hex()))
+
+
+def print_map(grefs):
+    print('OFFSET\tSIZE\tNAME')
+    for name, loc, size in grefs:
+        print('{:x}\t{}\t{}'.format(loc, size, name))
+
+
+def main():
+    ap = argparse.ArgumentParser(description="""
+        Generate a symbol table map starting directly from an ELF
+        firmware with DWARF2 debugging information.
+        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.
+    """)
+    ap.add_argument('elf', help='ELF file containing DWARF2 debugging information')
+    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')
+    args = ap.parse_args()
+
+    grefs = get_elf_globals(args.elf)
+    grefs = list(sorted(grefs, key=lambda x: x[1]))
+    if args.dump is None:
+        print_map(grefs)
+    else:
+        addr, data = decode_dump(args.dump)
+        annotate_refs(grefs, addr, data)
+
+if __name__ == '__main__':
+    exit(main())

+ 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