Add several low-level debugging tools

This commit is contained in:
Yuri D'Elia 2021-05-25 00:26:32 +02:00 committed by DRracer
parent 4580b8a78c
commit 1095b26570
6 changed files with 290 additions and 0 deletions

37
tools/README.md Normal file
View File

@ -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
tools/dump_eeprom Executable file
View File

@ -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
tools/dump_sram Executable file
View File

@ -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
tools/elf_mem_map Executable file
View File

@ -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
tools/noreset Executable file
View File

@ -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
tools/update_eeprom Executable file
View File

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