diff --git a/apollo_fpga/commands/cli.py b/apollo_fpga/commands/cli.py index 2f699a3..a92e60a 100755 --- a/apollo_fpga/commands/cli.py +++ b/apollo_fpga/commands/cli.py @@ -15,6 +15,7 @@ import errno import logging import argparse +from collections import namedtuple from apollo_fpga import ApolloDebugger from apollo_fpga.jtag import JTAGChain, JTAGPatternError @@ -22,22 +23,6 @@ from apollo_fpga.onboard_jtag import * -COMMAND_HELP_TEXT = \ -"""configure -- Uploads a bitstream to the device's FPGA over JTAG. -program -- Programs the target bitstream onto the attached FPGA. -force-offline -- Forces the board's FPGA offline; useful for recovering a "bricked" JTAG connection. -jtag-scan -- Prints information about devices on the onboard JTAG chain. -flash-info -- Prints information about the FPGA's attached configuration flash. -flash-erase -- Erases the contents of the FPGA's flash memory. -flash-program -- Programs the target bitstream onto the attached FPGA. -svf -- Plays a given SVF file over JTAG. -spi -- Sends the given list of bytes over debug-SPI, and returns the response. -spi-inv -- Sends the given list of bytes over SPI with inverted CS. -spi-reg -- Reads or writes to a provided register over the debug-SPI. -jtag-spi -- Sends the given list of bytes over SPI-over-JTAG, and returns the response. -jtag-reg -- Reads or writes to a provided register of JTAG-tunneled debug SPI. -""" - # # Common JEDEC manufacturer IDs for SPI flash chips. # @@ -122,13 +107,13 @@ def print_chain_info(device, args): def play_svf_file(device, args): """ Command that prints the relevant flash chip's information to the console. """ - if not args.argument: + if not args.file: logging.error("You must provide an SVF filename to play!\n") sys.exit(-1) with device.jtag as jtag: try: - jtag.play_svf_file(args.argument) + jtag.play_svf_file(args.file) except JTAGPatternError: # Our SVF player has already logged the error to stderr. logging.error("") @@ -140,7 +125,7 @@ def configure_fpga(device, args): with device.jtag as jtag: programmer = device.create_jtag_programmer(jtag) - with open(args.argument, "rb") as f: + with open(args.file, "rb") as f: bitstream = f.read() programmer.configure(bitstream) @@ -163,24 +148,28 @@ def erase_flash(device, args): def program_flash(device, args): with device.jtag as jtag: programmer = device.create_jtag_programmer(jtag) + offset = ast.literal_eval(args.offset) if args.offset else 0 - with open(args.argument, "rb") as f: + with open(args.file, "rb") as f: bitstream = f.read() - programmer.flash(bitstream) + programmer.flash(bitstream, offset=offset) device.soft_reset() def read_back_flash(device, args): # XXX abstract this? - length = ast.literal_eval(args.value) if args.value else (4 * 1024 * 1024) + length = ast.literal_eval(args.length) if args.length else (4 * 1024 * 1024) + offset = ast.literal_eval(args.offset) if args.offset else 0 + if offset: + length = min(length, 4 * 1024 * 1024 - offset) with device.jtag as jtag: programmer = device.create_jtag_programmer(jtag) - with open(args.argument, "wb") as f: - bitstream = programmer.read_flash(length) + with open(args.file, "wb") as f: + bitstream = programmer.read_flash(length, offset=offset) f.write(bitstream) device.soft_reset() @@ -231,7 +220,7 @@ def force_fpga_offline(device, args): def _do_debug_spi(device, spi, args, *, invert_cs): # Try to figure out what data the user wants to send. - data_raw = ast.literal_eval(args.argument) + data_raw = ast.literal_eval(args.bytes) if isinstance(data_raw, int): data_raw = [data_raw] @@ -254,7 +243,7 @@ def jtag_debug_spi(device, args): def set_led_pattern(device, args): - device.set_led_pattern(int(args.argument)) + device.set_led_pattern(int(args.pattern)) def debug_spi_inv(device, args): debug_spi(device, args, invert_cs=True) @@ -263,7 +252,7 @@ def debug_spi_inv(device, args): def _do_debug_spi_register(device, spi, args): # Try to figure out what data the user wants to send. - address = int(args.argument, 0) + address = int(args.address, 0) if args.value: value = int(args.value, 0) is_write = True @@ -286,59 +275,79 @@ def jtag_debug_spi_register(device, args): _do_debug_spi_register(device, reg, args) -def main(): +Command = namedtuple("Command", ("name", "alias", "args", "help", "handler"), + defaults=(None, [], [], None, None)) - commands = { +def main(): + + commands = [ # Info queries - 'info': print_device_info, - 'jtag-scan': print_chain_info, - 'flash-info': print_flash_info, + Command("info", handler=print_device_info, + help="Print device info.", ), + Command("jtag-scan", handler=print_chain_info, + help="Prints information about devices on the onboard JTAG chain."), + Command("flash-info", handler=print_flash_info, + help="Prints information about the FPGA's attached configuration flash."), # Flash commands - 'flash-erase': erase_flash, - 'flash': program_flash, - 'flash-program': program_flash, - 'flash-read': read_back_flash, + Command("flash-erase", handler=erase_flash, + help="Erases the contents of the FPGA's flash memory."), + Command("flash-program", alias=["flash"], args=["file", "--offset"], handler=program_flash, + help="Programs the target bitstream onto the attached FPGA."), + Command("flash-read", args=["file", "--offset", "--length"], handler=read_back_flash, + help="Reads the contents of the attached FPGA's configuration flash."), # JTAG commands - 'svf': play_svf_file, - 'configure': configure_fpga, - 'reconfigure': reconfigure_fpga, - 'force-offline': force_fpga_offline, + Command("svf", args=["file"], handler=play_svf_file, + help="Plays a given SVF file over JTAG."), + Command("configure", args=["file"], handler=configure_fpga, + help="Uploads a bitstream to the device's FPGA over JTAG."), + Command("reconfigure", handler=reconfigure_fpga, + help="Requests the attached ECP5 reconfigure itself from its SPI flash."), + Command("force-offline", handler=force_fpga_offline, + help="Forces the board's FPGA offline; useful for recovering a \"bricked\" JTAG connection."), # SPI debug exchanges - 'spi': debug_spi, - 'spi-inv': debug_spi_inv, - 'spi-reg': debug_spi_register, + Command("spi", args=["bytes"], handler=debug_spi, + help="Sends the given list of bytes over debug-SPI, and returns the response."), + Command("spi-inv", args=["bytes"], handler=debug_spi_inv, + help="Sends the given list of bytes over SPI with inverted CS."), + Command("spi-reg", args=["address", "value"], handler=debug_spi_register, + help="Reads or writes to a provided register over the debug-SPI."), # JTAG-SPI debug exchanges. - 'jtag-spi': jtag_debug_spi, - 'jtag-reg': jtag_debug_spi_register, + Command("jtag-spi", args=["bytes"], handler=jtag_debug_spi, + help="Sends the given list of bytes over SPI-over-JTAG, and returns the response."), + Command("jtag-reg", args=["address", "value"], handler=jtag_debug_spi_register, + help="Reads or writes to a provided register of JTAG-tunneled debug SPI."), # Misc - 'leds': set_led_pattern, - - } - + Command("leds", args=["pattern"], handler=set_led_pattern, + help="Sets the specified pattern for the Debug LEDs."), + ] # Set up a simple argument parser. parser = argparse.ArgumentParser(description="Apollo FPGA Configuration / Debug tool", formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument('command', metavar='command:', choices=commands, help=COMMAND_HELP_TEXT) - parser.add_argument('argument', metavar="[argument]", nargs='?', - help='the argument to the given command; often a filename') - parser.add_argument('value', metavar="[value]", nargs='?', - help='the value to a register write command, or the length for flash read') + sub_parsers = parser.add_subparsers(dest="command", metavar="command") + for command in commands: + cmd_parser = sub_parsers.add_parser(command.name, aliases=command.alias, help=command.help) + cmd_parser.set_defaults(func=command.handler) + for arg in command.args: + cmd_parser.add_argument(arg) args = parser.parse_args() + if not args.command: + parser.print_help() + return + device = ApolloDebugger() # Set up python's logging to act as a simple print, for now. logging.basicConfig(level=logging.INFO, format="%(message)-s") # Execute the relevant command. - command = commands[args.command] - command(device, args) + args.func(device, args) if __name__ == '__main__': diff --git a/apollo_fpga/ecp5.py b/apollo_fpga/ecp5.py index 5c53fe5..b214c9c 100644 --- a/apollo_fpga/ecp5.py +++ b/apollo_fpga/ecp5.py @@ -192,6 +192,11 @@ class FlashOpcode(IntEnum): # Erase the full flash chip. CHIP_ERASE = 0xC7 + # Erase by sectors/blocks + ERASE_SEC_4K = 0x20 + ERASE_BLK_32K = 0x52 + ERASE_BLK_64K = 0xD8 + def __init__(self, cfg_pins=None, init_pin=None, program_pin=None, done_pin=None, verbose_function=None): """ Captures the common fields for all ECP5 programmers. @@ -572,8 +577,6 @@ def _flash_wait_for_completion(self): """ Blocks until the flash has compelted any pending operations. """ while True: - time.sleep(1e-6) - # Read the flash's status register... flash_status = self._background_spi_transfer([self.FlashOpcode.READ_STATUS1, 0]) @@ -581,6 +584,7 @@ def _flash_wait_for_completion(self): if (flash_status[1] & self.SPI_FLASH_BUSY_MASK) == 0: break + time.sleep(1e-6) def _get_flash_status(self): """ Retrieves the FPGA's configuration flash's status register. """ @@ -594,7 +598,7 @@ def _enable_writing_to_flash(self): self._flash_wait_for_completion() # Request that the flash enter write-enabled mode... - self._background_spi_transfer([self.FlashOpcode.ENABLE_WRITE]) + self._background_spi_transfer([self.FlashOpcode.ENABLE_WRITE], ignore_response=True) if (self._get_flash_status() & self.SPI_FLASH_WRITE_MASK) == 0: raise IOError("Flash did not enter a writeable state!") @@ -610,10 +614,39 @@ def erase_flash(self): # Erase the connected SPI flash. # TODO: support more granular erases? - self._background_spi_transfer([self.FlashOpcode.CHIP_ERASE]) + self._background_spi_transfer([self.FlashOpcode.CHIP_ERASE], ignore_response=True) self._flash_wait_for_completion() + def _flash_erase(self, address, size): + """ Erases from the defined address and size with sector / block operations. + + Both arguments are rounded to multiples of 4K (min. granularity). + """ + + opcode_table = { + 65536: self.FlashOpcode.ERASE_BLK_64K, + 32768: self.FlashOpcode.ERASE_BLK_32K, + 4096: self.FlashOpcode.ERASE_SEC_4K, + } + + # Find address aligned to 4K boundaries + align = address % 4096 + address -= align + size += align + + # Issue sequence of erase calls. + while size > 0: + for chunk_sz, opcode in opcode_table.items(): + if size >= chunk_sz and address % chunk_sz == 0: + break + address_bytes = address.to_bytes(3, byteorder='big') + self._enable_writing_to_flash() + self._background_spi_transfer([opcode, *address_bytes], ignore_response=True) + self._flash_wait_for_completion() + address += chunk_sz + size -= chunk_sz + def _flash_write_page(self, address, data): """ Programs a single flash page. """ @@ -622,7 +655,7 @@ def _flash_write_page(self, address, data): # Send the write command... address_bytes = address.to_bytes(3, byteorder='big') - self._background_spi_transfer([self.FlashOpcode.WRITE_PAGE, *address_bytes, *data]) + self._background_spi_transfer([self.FlashOpcode.WRITE_PAGE, *address_bytes, *data], ignore_response=True) # ... and wait for it to complete. self._flash_wait_for_completion() @@ -640,9 +673,14 @@ def _flash_read_page(self, address, size): - def flash(self, bitstream, erase_first=True, disable_protections=False): + def flash(self, bitstream, erase_first=True, disable_protections=False, offset=0): """ Writes the relevant bitstream to a flash connected to the ECP5.""" + # Only allow page-aligned writes for now, but unaligned writes are feasible + if offset % self.SPI_FLASH_PAGE_SIZE: + raise ValueError(f"Offset address ({offset}) must be a multiple of the page " + f"size ({self.SPI_FLASH_PAGE_SIZE})).") + # Take control of the FPGA's SPI lines. self._enter_background_spi() @@ -654,19 +692,16 @@ def flash(self, bitstream, erase_first=True, disable_protections=False): # Disable any write protections, if requested. if disable_protections: self._enable_writing_to_flash() - self._background_spi_transfer([self.FlashOpcode.WRITE_STATUS1, 0]) + self._background_spi_transfer([self.FlashOpcode.WRITE_STATUS1, 0], ignore_response=True) # Prepare for writing by erasing the chip. - # TODO: potentially support more granular erases, here? if erase_first: - self._enable_writing_to_flash() - self._background_spi_transfer([self.FlashOpcode.CHIP_ERASE]) - self._flash_wait_for_completion() + self._flash_erase(0, len(bitstream)) # # Finally, program the bitstream itself. # - address = 0 + address = offset data_remaining = bytearray(bitstream) while data_remaining: @@ -681,9 +716,14 @@ def flash(self, bitstream, erase_first=True, disable_protections=False): self.trigger_reconfiguration() - def read_flash(self, length): + def read_flash(self, length, offset=0): """ Reads the contents of the attached FPGA's configuration flash. """ + # Only allow page-aligned reads for now, but unaligned reads are feasible + if offset % self.SPI_FLASH_PAGE_SIZE: + raise ValueError(f"Offset address ({offset}) must be a multiple of the page " + f"size ({self.SPI_FLASH_PAGE_SIZE})).") + # Take control of the FPGA's SPI lines. self._enter_background_spi() @@ -694,7 +734,7 @@ def read_flash(self, length): # Read our data back , one page at a time. data = bytearray() - address = 0 + address = offset bytes_remaining = length while bytes_remaining: @@ -999,7 +1039,7 @@ def _enter_background_spi(self, reset_flash=True): - def _background_spi_transfer(self, data, reverse=False): + def _background_spi_transfer(self, data, reverse=False, ignore_response=False): """ Performs a background SPI transfer, targeting the configuration flash.""" # Our JTAG protocol is bit-oriented; while our SPI is byte-oriented. In order to @@ -1026,11 +1066,12 @@ def reverse_bits(num): bits_to_send = len(jtag_ready_data) * 8 # Issue the command, and capture any data send in response. - response = self.chain.shift_data(tdi=jtag_ready_data, length=bits_to_send) + response = self.chain.shift_data(tdi=jtag_ready_data, length=bits_to_send, ignore_response=ignore_response) # Bit-reverse the data we capture in response, compensating for MSB-first ordering. - response = [reverse_bits(b) for b in bytes(response)] - return bytes(response) + if response: + response = [reverse_bits(b) for b in bytes(response)] + return bytes(response) diff --git a/apollo_fpga/jtag.py b/apollo_fpga/jtag.py index 0f1dca3..aede9a5 100644 --- a/apollo_fpga/jtag.py +++ b/apollo_fpga/jtag.py @@ -385,11 +385,6 @@ def _ensure_in_state(self, state): state_number = self.STATE_NUMBERS[state] self.debugger.out_request(REQUEST_JTAG_GO_TO_STATE, value=state_number) - # TODO: remove this; this if for debugging only - current_state_raw = self.debugger.in_request(REQUEST_JTAG_GET_STATE, length=1) - current_state_number = int.from_bytes(current_state_raw, byteorder='little') - assert(current_state_number == state_number) - def move_to_state(self, state_name): """ Moves the JTAG scan chain to the relevant state.