#! /usr/bin/env python3 import argparse import usb.core import usb.util import array import sys import hashlib import csv import urllib.request from progressbar.bar import ProgressBar class PrecursorUsb: def __init__(self, dev): self.dev = dev self.RDSR = 0x05 self.RDSCUR = 0x2B self.RDID = 0x9F self.WREN = 0x06 self.WRDI = 0x04 self.SE4B = 0x21 self.BE4B = 0xDC self.PP4B = 0x12 self.registers = {} self.regions = {} self.gitrev = '' def register(self, name): return int(self.registers[name], 0) def peek(self, addr, display=False): _dummy_s = '\x00'.encode('utf-8') data = array.array('B', _dummy_s * 4) numread = self.dev.ctrl_transfer(bmRequestType=(0x80 | 0x43), bRequest=0, wValue=(addr & 0xffff), wIndex=((addr >> 16) & 0xffff), data_or_wLength=data, timeout=500) read_data = int.from_bytes(data.tobytes(), byteorder='little', signed=False) if display == True: print("0x{:08x}".format(read_data)) return read_data def poke(self, addr, wdata, check=False, display=False): if check == True: _dummy_s = '\x00'.encode('utf-8') data = array.array('B', _dummy_s * 4) numread = self.dev.ctrl_transfer(bmRequestType=(0x80 | 0x43), bRequest=0, wValue=(addr & 0xffff), wIndex=((addr >> 16) & 0xffff), data_or_wLength=data, timeout=500) read_data = int.from_bytes(data.tobytes(), byteorder='little', signed=False) print("before poke: 0x{:08x}".format(read_data)) data = array.array('B', wdata.to_bytes(4, 'little')) numwritten = self.dev.ctrl_transfer(bmRequestType=(0x00 | 0x43), bRequest=0, wValue=(addr & 0xffff), wIndex=((addr >> 16) & 0xffff), data_or_wLength=data, timeout=500) if check == True: _dummy_s = '\x00'.encode('utf-8') data = array.array('B', _dummy_s * 4) numread = self.dev.ctrl_transfer(bmRequestType=(0x80 | 0x43), bRequest=0, wValue=(addr & 0xffff), wIndex=((addr >> 16) & 0xffff), data_or_wLength=data, timeout=500) read_data = int.from_bytes(data.tobytes(), byteorder='little', signed=False) print("after poke: 0x{:08x}".format(read_data)) if display == True: print("wrote 0x{:08x} to 0x{:08x}".format(wdata, addr)) def burst_read(self, addr, len): _dummy_s = '\x00'.encode('utf-8') maxlen = 4096 ret = bytearray() packet_count = len // maxlen if (len % maxlen) != 0: packet_count += 1 for pkt_num in range(packet_count): cur_addr = addr + pkt_num * maxlen if pkt_num == packet_count - 1: if len % maxlen != 0: bufsize = len % maxlen else: bufsize = maxlen else: bufsize = maxlen data = array.array('B', _dummy_s * bufsize) numread = self.dev.ctrl_transfer(bmRequestType=(0x80 | 0x43), bRequest=0, wValue=(cur_addr & 0xffff), wIndex=((cur_addr >> 16) & 0xffff), data_or_wLength=data, timeout=500) if numread != bufsize: print("Burst read error: {} bytes requested, {} bytes read at 0x{:08x}".format(bufsize, numread, cur_addr)) exit(1) ret = ret + data return ret def burst_write(self, addr, data): if len(data) == 0: return # the actual "addr" doesn't matter for a burst_write, because it's specified # as an argument to the flash_pp4b command. We lock out access to the base of # SPINOR because it's part of the gateware, so, we pick a "safe" address to # write to instead. The page write responder will aggregate any write data # to anywhere in the SPINOR address range. writebuf_addr = 0x2098_0000 # the current start address of the kernel, for example maxlen = 4096 packet_count = len(data) // maxlen if (len(data) % maxlen) != 0: packet_count += 1 for pkt_num in range(packet_count): cur_addr = addr + pkt_num * maxlen if pkt_num == packet_count - 1: if len(data) % maxlen != 0: bufsize = len(data) % maxlen else: bufsize = maxlen else: bufsize = maxlen wdata = array.array('B', data[(pkt_num * maxlen):(pkt_num * maxlen) + bufsize]) numwritten = self.dev.ctrl_transfer(bmRequestType=(0x00 | 0x43), bRequest=0, # note use of writebuf_addr instead of cur_addr -> see comment above about the quirk of write addressing wValue=(writebuf_addr & 0xffff), wIndex=((writebuf_addr >> 16) & 0xffff), data_or_wLength=wdata, timeout=500) if numwritten != bufsize: print("Burst write error: {} bytes requested, {} bytes written at 0x{:08x}".format(bufsize, numwritten, cur_addr)) exit(1) def ping_wdt(self): self.poke(self.register('wdt_watchdog'), 1, display=False) self.poke(self.register('wdt_watchdog'), 1, display=False) def spinor_command_value(self, exec=0, lock_reads=0, cmd_code=0, dummy_cycles=0, data_words=0, has_arg=0): return ((exec & 1) << 1 | (lock_reads & 1) << 24 | (cmd_code & 0xff) << 2 | (dummy_cycles & 0x1f) << 11 | (data_words & 0xff) << 16 | (has_arg & 1) << 10 ) def flash_rdsr(self, lock_reads): self.poke(self.register('spinor_cmd_arg'), 0) self.poke(self.register('spinor_command'), self.spinor_command_value(exec=1, lock_reads=lock_reads, cmd_code=self.RDSR, dummy_cycles=4, data_words=1, has_arg=1) ) return self.peek(self.register('spinor_cmd_rbk_data'), display=False) def flash_rdscur(self): self.poke(self.register('spinor_cmd_arg'), 0) self.poke(self.register('spinor_command'), self.spinor_command_value(exec=1, lock_reads=1, cmd_code=self.RDSCUR, dummy_cycles=4, data_words=1, has_arg=1) ) return self.peek(self.register('spinor_cmd_rbk_data'), display=False) def flash_rdid(self, offset): self.poke(self.register('spinor_cmd_arg'), 0) self.poke(self.register('spinor_command'), self.spinor_command_value(exec=1, cmd_code=self.RDID, dummy_cycles=4, data_words=offset, has_arg=1) ) return self.peek(self.register('spinor_cmd_rbk_data'), display=False) def flash_wren(self): self.poke(self.register('spinor_cmd_arg'), 0) self.poke(self.register('spinor_command'), self.spinor_command_value(exec=1, lock_reads=1, cmd_code=self.WREN) ) def flash_wrdi(self): self.poke(self.register('spinor_cmd_arg'), 0) self.poke(self.register('spinor_command'), self.spinor_command_value(exec=1, lock_reads=1, cmd_code=self.WRDI) ) def flash_se4b(self, sector_address): self.poke(self.register('spinor_cmd_arg'), sector_address) self.poke(self.register('spinor_command'), self.spinor_command_value(exec=1, lock_reads=1, cmd_code=self.SE4B, has_arg=1) ) def flash_be4b(self, block_address): self.poke(self.register('spinor_cmd_arg'), block_address) self.poke(self.register('spinor_command'), self.spinor_command_value(exec=1, lock_reads=1, cmd_code=self.BE4B, has_arg=1) ) def flash_pp4b(self, address, data_bytes): self.poke(self.register('spinor_cmd_arg'), address) self.poke(self.register('spinor_command'), self.spinor_command_value(exec=1, lock_reads=1, cmd_code=self.PP4B, has_arg=1, data_words=(data_bytes//2)) ) def load_csrs(self, fname=None): LOC_CSRCSV = 0x20277000 # this address shouldn't change because it's how we figure out our version number # CSR extraction: # dd if=soc_csr.bin of=csr_data_0.9.6.bin skip=2524 count=32 bs=1024 if fname == None: csr_data = self.burst_read(LOC_CSRCSV, 0x8000) else: with open(fname, "rb") as f: csr_data = f.read(0x8000) hasher = hashlib.sha512() hasher.update(csr_data[:0x7FC0]) digest = hasher.digest() if digest != csr_data[0x7fc0:]: print("Could not find a valid csr.csv descriptor on the device, aborting!") exit(1) csr_len = int.from_bytes(csr_data[:4], 'little') csr_extracted = csr_data[4:4+csr_len] decoded = csr_extracted.decode('utf-8') # strip comments stripped = [] for line in decoded.split('\n'): if line.startswith('#') == False: stripped.append(line) # create database csr_db = csv.reader(stripped) for row in csr_db: if len(row) > 1: if 'csr_register' in row[0]: self.registers[row[1]] = row[2] if 'memory_region' in row[0]: self.regions[row[1]] = [row[2], row[3]] if 'git_rev' in row[0]: self.gitrev = row[1] print("Using SoC {} registers".format(self.gitrev)) def erase_region(self, addr, length): # ID code check code = self.flash_rdid(1) print("ID code bytes 1-2: 0x{:08x}".format(code)) if code != 0x8080c2c2: print("ID code mismatch") exit(1) code = self.flash_rdid(2) print("ID code bytes 2-3: 0x{:08x}".format(code)) if code != 0x3b3b8080: print("ID code mismatch") exit(1) # block erase progress = ProgressBar(min_value=0, max_value=length, prefix='Erasing ').start() erased = 0 while erased < length: self.ping_wdt() if (length - erased >= 65536) and ((addr & 0xFFFF) == 0): blocksize = 65536 else: blocksize = 4096 while True: self.flash_wren() status = self.flash_rdsr(1) if status & 0x02 != 0: break if blocksize == 4096: self.flash_se4b(addr + erased) else: self.flash_be4b(addr + erased) erased += blocksize while (self.flash_rdsr(1) & 0x01) != 0: pass result = self.flash_rdscur() if result & 0x60 != 0: print("E_FAIL/P_FAIL set on erase, programming may fail, but trying anyways...") if self.flash_rdsr(1) & 0x02 != 0: self.flash_wrdi() while (self.flash_rdsr(1) & 0x02) != 0: pass if erased < length: progress.update(erased) progress.finish() print("Erase finished") # addr is relative to the base of FLASH (not absolute) def flash_program(self, addr, data, verify=True): flash_region = int(self.regions['spiflash'][0], 0) flash_len = int(self.regions['spiflash'][1], 0) if (addr + len(data) > flash_len): print("Write data out of bounds! Aborting.") exit(1) # ID code check code = self.flash_rdid(1) print("ID code bytes 1-2: 0x{:08x}".format(code)) if code != 0x8080c2c2: print("ID code mismatch") exit(1) code = self.flash_rdid(2) print("ID code bytes 2-3: 0x{:08x}".format(code)) if code != 0x3b3b8080: print("ID code mismatch") exit(1) # block erase progress = ProgressBar(min_value=0, max_value=len(data), prefix='Erasing ').start() erased = 0 while erased < len(data): self.ping_wdt() if (len(data) - erased >= 65536) and ((addr & 0xFFFF) == 0): blocksize = 65536 else: blocksize = 4096 while True: self.flash_wren() status = self.flash_rdsr(1) if status & 0x02 != 0: break if blocksize == 4096: self.flash_se4b(addr + erased) else: self.flash_be4b(addr + erased) erased += blocksize while (self.flash_rdsr(1) & 0x01) != 0: pass result = self.flash_rdscur() if result & 0x60 != 0: print("E_FAIL/P_FAIL set on erase, programming may fail, but trying anyways...") if self.flash_rdsr(1) & 0x02 != 0: self.flash_wrdi() while (self.flash_rdsr(1) & 0x02) != 0: pass if erased < len(data): progress.update(erased) progress.finish() print("Erase finished") # program # pad out to the nearest word length if len(data) % 4 != 0: data += bytearray([0xff] * (4 - (len(data) % 4))) written = 0 progress = ProgressBar(min_value=0, max_value=len(data), prefix='Writing ').start() while written < len(data): self.ping_wdt() if len(data) - written > 256: chunklen = 256 else: chunklen = len(data) - written while True: self.flash_wren() status = self.flash_rdsr(1) if status & 0x02 != 0: break self.burst_write(self.register('spinor_wdata'), data[written:(written+chunklen)]) self.flash_pp4b(addr + written, chunklen) written += chunklen if written < len(data): progress.update(written) progress.finish() print("Write finished") if self.flash_rdsr(1) & 0x02 != 0: self.flash_wrdi() while (self.flash_rdsr(1) & 0x02) != 0: pass # dummy reads to clear the "read lock" bit self.flash_rdsr(0) # verify self.ping_wdt() if verify: print("Performing readback for verification...") self.ping_wdt() rbk_data = self.burst_read(addr + flash_region, len(data)) if rbk_data != data: errs = 0 err_thresh = 64 for i in range(0, len(rbk_data)): if rbk_data[i] != data[i]: if errs < err_thresh: print("Error at 0x{:x}: {:x}->{:x}".format(i, data[i], rbk_data[i])) errs += 1 if errs == err_thresh: print("Too many errors, stopping print...") print("Errors were found in verification, programming failed") print("Total byte errors: {}".format(errs)) exit(1) else: print("Verification passed.") else: print("Skipped verification at user request") self.ping_wdt() def auto_int(x): return int(x, 0) def main(): parser = argparse.ArgumentParser(description="Update/upload to a Precursor device running Xous 0.8/0.9") parser.add_argument( "--config", required=False, help="Print the descriptor", action='store_true' ) parser.add_argument( "--override-csr", required=False, help="CSR file to use instead of CSR values stored with the image. Used to recover in case of partial update of soc_csr.bin", type=str, ) parser.add_argument( "--peek", required=False, help="Inspect an address", type=auto_int, metavar=('ADDR') ) args = parser.parse_args() dev = usb.core.find(idProduct=0x5bf0, idVendor=0x1209) if dev is None: raise ValueError('Precursor device not found; be sure that the debug core is enabled') dev.set_configuration() if args.config: cfg = dev.get_active_configuration() print(cfg) pc_usb = PrecursorUsb(dev) pc_usb.load_csrs(args.override_csr) # prime the CSR values if "v0.8" in pc_usb.gitrev: locs = { "LOC_SOC" : [0x0000_0000, "soc_csr.bin"], "LOC_STAGING": [0x0028_0000, "pass"], "LOC_LOADER" : [0x0050_0000, "loader.bin"], "LOC_KERNEL" : [0x0098_0000, "xous.img"], "LOC_WF200" : [0x07F8_0000, "pass"], "LOC_EC" : [0x07FC_E000, "pass"], "LOC_AUDIO" : [0x0634_0000, "short_8khz.wav"], "LEN_AUDIO" : [0x01C4_0000, "pass"], "LOC_PDDB" : [0x0100_0000, "pass"], } elif "v0.9" in pc_usb.gitrev: locs = { "LOC_SOC" : [0x0000_0000, "soc_csr.bin"], "LOC_STAGING": [0x0028_0000, "pass"], "LOC_LOADER" : [0x0050_0000, "loader.bin"], "LOC_KERNEL" : [0x0098_0000, "xous.img"], "LOC_WF200" : [0x07F8_0000, "pass"], "LOC_EC" : [0x07FC_E000, "pass"], "LOC_AUDIO" : [0x0634_0000, "short_8khz.wav"], "LEN_AUDIO" : [0x01C4_0000, "pass"], "LOC_PDDB" : [0x01D8_0000, "pass"], } elif args.force == True: # try the v0.9 offsets locs = { "LOC_SOC" : [0x00000000, "soc_csr.bin"], "LOC_STAGING": [0x00280000, "pass"], "LOC_LOADER" : [0x00500000, "loader.bin"], "LOC_KERNEL" : [0x00980000, "xous.img"], "LOC_WF200" : [0x07F80000, "pass"], "LOC_EC" : [0x07FCE000, "pass"], "LOC_AUDIO" : [0x06340000, "short_8khz.wav"], "LEN_AUDIO" : [0x01C40000, "pass"], "LOC_PDDB" : [0x01D80000, "pass"], } else: print("SoC is from an unknow rev '{}', use --force to continue anyways with v0.9 firmware offsets".format(pc_usb.load_csrs())) exit(1) vexdbg_addr = int(pc_usb.regions['vexriscv_debug'][0], 0) pc_usb.ping_wdt() print("Halting CPU.") pc_usb.poke(vexdbg_addr, 0x00020000) flash_region = int(pc_usb.regions['spiflash'][0], 0) if args.peek: pc_usb.peek(args.peek + flash_region, display=True) else: with open("backup.pddb", "wb") as file: start_addr = locs['LOC_PDDB'][0] - 0x1000 total_length = locs['LOC_EC'][0] - locs['LOC_PDDB'][0] + 0x1000 progress = ProgressBar(min_value=0, max_value=total_length, prefix='Backing up ').start() block_size = 4096 amount_read = 0 while amount_read < total_length: if amount_read % (block_size * 16) == 0: pc_usb.ping_wdt() backup = pc_usb.burst_read(start_addr + amount_read + flash_region, block_size) amount_read += block_size if amount_read < total_length: progress.update(amount_read) file.write(backup) progress.finish() file.close() print("Resuming CPU.") pc_usb.poke(vexdbg_addr, 0x02000000) print("Resetting SOC...") try: pc_usb.poke(pc_usb.register('reboot_soc_reset'), 0xac, display=False) except usb.core.USBError: pass # we expect an error because we reset the SOC and that includes the USB core # print("If you need to run more commands, please unplug and re-plug your device in, as the Precursor USB core was just reset") if __name__ == "__main__": main() exit(0)