diff --git a/Firmware/Makefile b/Firmware/Makefile index 3ad6d167..35b5e684 100644 --- a/Firmware/Makefile +++ b/Firmware/Makefile @@ -194,6 +194,9 @@ clean: flash: $(BUILD_DIR)/$(TARGET).elf openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg -c init -c reset\ halt -c flash\ write_image\ erase\ $(BUILD_DIR)/$(TARGET).elf -c reset\ run -c exit +dfu: $(BUILD_DIR)/$(TARGET).hex + ../tools/dfu.py $(BUILD_DIR)/$(TARGET).hex + gdb: $(BUILD_DIR)/$(TARGET).elf arm-none-eabi-gdb $(BUILD_DIR)/$(TARGET).elf -x openocd.gdbinit diff --git a/Firmware/MotorControl/commands.cpp b/Firmware/MotorControl/commands.cpp index 35e5651d..6370e5b5 100644 --- a/Firmware/MotorControl/commands.cpp +++ b/Firmware/MotorControl/commands.cpp @@ -88,6 +88,11 @@ void motors_1_set_current_setpoint_func(void) { motors[1].set_current_setpoint_args.current_setpoint); } +void enter_dfu_mode() { + *((unsigned long *)0x2001C000) = 0xDEADBEEF; + NVIC_SystemReset(); +} + // This table specifies which fields and functions are exposed on the USB and UART ports. // TODO: Autogenerate this table. It will come up again very soon in the Arduino library. // clang-format off @@ -210,7 +215,9 @@ const Endpoint endpoints[] = { Endpoint::make_function("set_current_setpoint", &motors_1_set_current_setpoint_func), Endpoint::make_property("current_setpoint", &motors[1].set_current_setpoint_args.current_setpoint), Endpoint::close_tree(), - Endpoint::close_tree() // motor1 + Endpoint::close_tree(), // motor1 + Endpoint::make_function("enter_dfu_mode", &enter_dfu_mode), + Endpoint::close_tree() // enter_dfu_mode }; // clang-format on diff --git a/Firmware/Src/main.c b/Firmware/Src/main.c index cb7e0490..662a7689 100644 --- a/Firmware/Src/main.c +++ b/Firmware/Src/main.c @@ -80,6 +80,14 @@ void MX_FREERTOS_Init(void); /* USER CODE BEGIN 0 */ +void jump_to_builtin_bootloader(void) { + __set_MSP(0x20001000); + // http://www.st.com/content/ccc/resource/technical/document/application_note/6a/17/92/02/58/98/45/0c/CD00264379.pdf/files/CD00264379.pdf + void (*builtin_bootloader)(void) = (void (*)(void))(*((uint32_t *)0x1FFF0004)); + builtin_bootloader(); + for (;;); +} + /* USER CODE END 0 */ int main(void) @@ -87,6 +95,14 @@ int main(void) /* USER CODE BEGIN 1 */ + /* We could jump to the bootloader directly on demand without rebooting + but that requires us to reset several peripherals and interrupts for it + to function correctly. Therefore it's easier to just reset the entire chip. */ + + if(*((unsigned long *)0x2001C000) == 0xDEADBEEF) { + *((unsigned long *)0x2001C000) = 0xCAFEFEED; //Reset bootloader trigger + jump_to_builtin_bootloader(); + } /* USER CODE END 1 */ /* MCU Configuration----------------------------------------------------------*/ diff --git a/tools/dfu.py b/tools/dfu.py new file mode 100755 index 00000000..738ab1f6 --- /dev/null +++ b/tools/dfu.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +""" +Tool for flashing .hex files to the ODrive via the STM built-in USB DFU mode. +""" + +import argparse +import sys +import time +import dfuse +import usb.core +import usb.util +import odrive.core + +try: + from intelhex import IntelHex +except: + print("You need intelhex for this (sudo pip install IntelHex)", file=sys.stderr) + sys.exit(1) + + +SIZE_MULTIPLIERS = {' ': 1, 'K': 1024, 'M' : 1024*1024} +TRANSFER_SIZE = 2048 + + +def load_sectors(dfudev, hexfile): + """ + Checks for which on-device sectors there is data in the hex file and + returns a sector object for each touched sector. Each sector object + is filled with the associated data from the hex file. + """ + + for name, alt in dfudev.alternates(): + # example for name: + # '@Internal Flash /0x08000000/04*016Kg,01*064Kg,07*128Kg' + label, addr, layout = name.split('/') + addr = int(addr, 0) # convert hex to decimal + + for sector in layout.split(','): + repeat, size = map(int, sector[:-2].split('*')) + size *= SIZE_MULTIPLIERS[sector[-2].upper()] + mode = sector[-1] + + while repeat > 0: + # check if any segment from the hexfile overlaps with this sector + touched = False + for (start, end) in hexfile.segments(): + if start < addr and end > addr: + touched = True + break + elif start >= addr and start < addr + size: + touched = True + break + + if touched: + # TODO: verify if the section is writable + yield { + 'alt': alt, + 'addr': addr, + 'data': hexfile.tobinarray(addr, addr + size - 1) + } + + addr += size + repeat -= 1 + +def set_alternate_safe(dfudev, alt): + dfudev.set_alternate(alt) + if dfudev.get_state() == dfuse.DfuState.DFU_ERROR: + dfudev.clear_status() + dfudev.wait_while_state(dfuse.DfuState.DFU_ERROR) + +def erase(dfudev, sectors): + for i, sector in enumerate(sectors): + print("Erasing... (sector {}/{}) \r".format(i, len(sectors)), end='', flush=True) + set_alternate_safe(dfudev, sector['alt']) + dfudev.erase(sector['addr']) + status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY, timeout=len(sector['data'])/32) + if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: + raise RuntimeError("An error occured. Device Status: %r" % status) + print('Erasing... done ') + +def flash(dfudev, sectors): + for i, sector in enumerate(sectors): + print("Flashing... (sector {}/{}) \r".format(i, len(sectors)), end='', flush=True) + set_alternate_safe(dfudev, sector['alt']) + dfudev.set_address(sector['addr']) + status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) + if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: + raise RuntimeError("An error occured. Device Status: %r" % status) + + data = sector['data'] + blocks = [data[i:i + TRANSFER_SIZE] for i in range(0, len(data), TRANSFER_SIZE)] + for blocknum, block in enumerate(blocks): + #print('write to {:08X} ({} bytes)'.format( + # sector['addr'] + blocknum * TRANSFER_SIZE, len(block))) + dfudev.write(blocknum, block) + status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) + if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: + raise RuntimeError("An error occured. Device Status: %r" % status) + print('Flashing... done ') + + +# Results in usb.core.USBError. Probably the device should go to dfuIDLE first, but how? +#def verify(dfudev, sectors): +# for i, sector in enumerate(sectors): +# print("Verifying... (sector {}/{}) \r".format(i, len(sectors)), end='', flush=True) +# set_alternate_safe(dfudev, sector['alt']) +# dfudev.set_address(sector['addr']) +# status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) +# if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: +# raise RuntimeError("An error occured. Device Status: %r" % status) +# +# print("state: {}".format(dfudev.get_state())) +# #dfudev.clear_status() +# print("state: {}".format(dfudev.get_state())) +# data = sector['data'] +# blocks = [data[i:i + TRANSFER_SIZE] for i in range(0, len(data), TRANSFER_SIZE)] +# for blocknum, block in enumerate(blocks): +# print('read at {:08X}'.format(sector['addr'] + blocknum * TRANSFER_SIZE)) +# deviceBlock = dfudev.read(blocknum, TRANSFER_SIZE) +# print(dfudev.get_state()) +# if (deviceBlock != block): +# raise RuntimeError("verification failed at address {:08X}".format(sector['addr'] + blocknum * TRANSFER_SIZE)) +# print('Verifying... done ') + + +def jump_to_application(dfudev, address): + dfudev.set_address(address) + status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) + if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: + raise RuntimeError("An error occured. Device Status: {}".format(status[1])) + + dfudev.leave() + status = dfudev.wait_while_state(dfuse.DfuState.DFU_MANIFEST_SYNC) + if status[1] != dfuse.DfuState.DFU_MANIFEST: + raise RuntimeError("An error occured. Device Status: {}".format(status[1])) + + +### BEGINNING OF APPLICATION ### + +# parse arguments +parser = argparse.ArgumentParser(description='Program an STM32 in DFU mode.') +parser.add_argument('file', metavar='HEX', help='the .hex file to be flashed') +args = parser.parse_args() + + +# load hex file +hexfile = IntelHex(args.file) + +print("Contiguous segments in hex file:") +for start, end in hexfile.segments(): + print(" {:08X} to {:08X}".format(start, end - 1)) + + +# find an STM32 in DFU mode (if there is none, find an ODrive and put it in DFU mode) +usbdev = usb.core.find(idVendor=0x0483, idProduct=0xdf11) +if usbdev is None: + # Find a connected ODrive (this will block until you connect one) + print("Waiting for ODrive...") + my_drive = odrive.core.find_any(consider_usb=True, consider_serial=False) + print("Putting device into DFU mode...") + try: + my_drive.enter_dfu_mode() + except usb.core.USBError as ex: + if ex.errno != 32: + raise ex + time.sleep(1.0) + usbdev = usb.core.find(idVendor=0x0483, idProduct=0xdf11) + if usbdev is None: + raise ValueError('No STM32 DfuSe device found.') +dfudev = dfuse.DfuDevice(usbdev) + + +# fill sectors with data +sectors = list(load_sectors(dfudev, hexfile)) +print("Sectors to be flashed: ") +for sector in sectors: + print(" {:08X} to {:08X}".format(sector['addr'], sector['addr'] + len(sector['data']) - 1)) + +# flash! +erase(dfudev, sectors) +flash(dfudev, sectors) +#verify(dfudev, sectors) + +# If the flash operation failed for some reason, your device is bricked now. +# You can unbrick it as long as the device remains powered on. +# (or always with an STLink) +# So for debugging you should comment this last part out. + +# Jump to application +jump_to_application(dfudev, 0x08000000) + + + +# Note: the flashed image can be verified using: (0x12000 is the number of bytes to read) +# $ openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg -c init -c flash\ read_bank\ 0\ image.bin\ 0\ 0x12000 -c exit +# $ hexdump -C image.bin > image.bin.txt +# +# If you compare this with a reference image that was flashed with the STLink, you will see +# minor differences. This is because this script fills undefined sections with 0xff. +# $ diff image_ref.bin.txt image.bin.txt +# 21c21 +# < * +# --- +# > 00000180 d9 47 00 08 d9 47 00 08 ff ff ff ff ff ff ff ff |.G...G..........| +# 2553c2553 +# < 00009fc0 9e 46 70 47 00 00 00 00 52 20 96 3c 46 76 50 76 |.FpG....R . 00009fc0 9e 46 70 47 ff ff ff ff 52 20 96 3c 46 76 50 76 |.FpG....R .