# Implementation of USB DFU device in Python. # # To run, just execute this file on a device with machine.USBDevice support. The device # will then change to DFU mode. # # For example, use `mpremote` (the `--no-follow` option starts the script running # without waiting for a response, because there won't be a response, the USB will change # to a DFU device): # # $ mpremote run --no-follow usb_dfu_device.py # # Then you can access the DFU device using the `pydfu.py` script in this repository, to # list DFU device, copy a file to the device, then exit DFU mode: # # $ ../../tools/pydfu.py -l # $ ../../tools/pydfu.py -u # # After running the last command above, the USB CDC device and REPL should reappear. import struct, machine # USB constants for bmRequestType. USB_REQ_RECIP_INTERFACE = 0x01 USB_REQ_TYPE_CLASS = 0x20 USB_DIR_OUT = 0x00 USB_DIR_IN = 0x80 # String describing the memory layout of the DFU device. MEMORY_LAYOUT = b"@Internal Flash /0x08000000/16*128Kg" # VID and PID of the DFU device (these are the ST values). VID = 0x0483 PID = 0xDF11 # Maximum transfer size for RX and TX. wTransferSize = 2048 # DFU device descriptor. _desc_dev = bytes( [ 0x12, # bLength 0x01, # bDescriptorType: Device 0x00, 0x02, # USB version: 2.00 0x00, # bDeviceClass 0x00, # bDeviceSubClass 0x00, # bDeviceProtocol 0x40, # bMaxPacketSize VID & 0xFF, VID >> 8, # VID PID & 0xFF, PID >> 8, # PID 0x00, 0x01, # bcdDevice: 1.00 0x11, # iManufacturer 0x12, # iProduct 0x13, # iSerialNumber 0x01, # bNumConfigurations: 1 ] ) # DFU configuration descriptor. _desc_cfg = bytes( [ # Configuration Descriptor. 0x09, # bLength 0x02, # bDescriptorType 0x1B, 0x00, # wTotalLength: 27 0x01, # bNumInterfaces 0x01, # bConfigurationValue 0x00, # iConfiguration 0x80, # bmAttributes (bus powered) 0x32, # bMaxPower # Interface Descriptor. 0x09, # bLength 0x04, # bDescriptorType 0x00, # bInterfaceNumber 0x00, # bNumEndpointns 0x00, # bAlternateSetting 0xFE, # bInterfaceClass: application specific interface 0x01, # bInterfaceSubClasse: device firmware update 0x02, # bInterfaceProtocol 0x14, # iInterface # Device Firmware Upgrade Interface Descriptor. 0x09, # bLength 0x21, # bDescriptorType 0x0B, # bmAttributes (will detach, upload supported, download supported) 0xFF, 0x00, # wDetatchTimeout wTransferSize & 0xFF, wTransferSize >> 8, # wTransferSize 0x1A, 0x01, # bcdDFUVersion ] ) # DFU strings. _desc_strs = { 0x11: b"iManufacturer", 0x12: b"iProduct", 0x13: b"iSerialNumber", 0x14: MEMORY_LAYOUT, } # This class handles the DFU USB device logic. class DFUOverUSB: def __init__(self, dfu): # USB buffer for transfers. self.usb_buf = bytearray(wTransferSize) # Instance of the DFU state machine. self.dfu = dfu def _control_xfer_cb(self, stage, request): bmRequestType, bRequest, wValue, wIndex, wLength = struct.unpack("= 2: self.cmd = DFU.CMD_UPLOAD addr = (arg - 2) * len(buf) + self.addr self.do_read(addr, buf) return buf return None elif cmd == DFU.GETSTATUS and len(buf) == 6: if self.cmd == DFU.CMD_NONE: pass elif self.cmd == DFU.CMD_EXIT: self.state = DFU.STATE_MANIFEST elif self.cmd == DFU.CMD_UPLOAD: self.state = DFU.STATE_UPLOAD_IDLE elif self.cmd == DFU.CMD_DNLOAD: self.state = DFU.STATE_BUSY else: self.state = DFU.STATE_BUSY # Populate the buffer to return to the host. buf[0] = self.status buf[1] = 0 buf[2] = 0 buf[3] = 0 buf[4] = self.state buf[5] = self.error # Clear errors now they've been sent to host. self.status = DFU.STATUS_OK self.error = 0 return buf else: return None def process(self): # Transition the DFU state machine. if self.state == DFU.STATE_MANIFEST: self.leave_dfu = True elif self.state == DFU.STATE_BUSY: if self.cmd == DFU.CMD_DNLOAD: self.cmd = DFU.CMD_NONE self.state = self.process_dnload() def process_dnload(self): ret = -1 # Assume error. if self.dnload_block_num == 0: # Download control commands. if self.dnload_len >= 1 and self.dnload_buf[0] == DFU.CMD_DNLOAD_ERASE: if self.dnload_len == 1: # Mass erase. ret = self.do_mass_erase() if ret != 0: self.cmd = DFU.CMD_NONE elif self.dnload_len == 5: # Erase page. addr = struct.unpack_from("= 1 and self.dnload_buf[0] == DFU.CMD_DNLOAD_SET_ADDRESS: if self.dnload_len == 5: # Set address. self.addr = struct.unpack_from(" 1: # Write data to memory. addr = (self.dnload_block_num - 2) * wTransferSize + self.addr ret = self.do_write(addr, self.dnload_len, self.dnload_buf) if ret == 0: return DFU.STATE_DNLOAD_IDLE else: return DFU.STATE_ERROR def do_mass_erase(self): # This function would implement a mass erase of flash memory. return 0 # indicate success def do_page_erase(self, addr): # This function would implement an erase of a page in flash memory. return 0 # indicate success def do_read(self, addr, buf): # This function would implement a read at the given address of flash memory. # Return some dummy bytes. for i in range(len(buf)): buf[i] = i & 0xFF return 0 # indicate success def do_write(self, addr, size, buf): # This function would implement a write of the given data to flash memory. return 0 # indicate success # Create an instance of the DFU state machine. dfu = DFU() # Create an instance of the DFU USB handler. dfu_usb = DFUOverUSB(dfu) # Switch the USB device to the custom DFU driver. usbd = machine.USBDevice() usbd.active(0) usbd.builtin_driver = usbd.BUILTIN_NONE usbd.config( desc_dev=_desc_dev, desc_cfg=_desc_cfg, desc_strs=_desc_strs, control_xfer_cb=dfu_usb._control_xfer_cb, ) usbd.active(1) # Wait for the DFU state machine to complete. while not dfu.leave_dfu: machine.idle() # Switch the USB device back to the default built-in driver. usbd.active(0) usbd.builtin_driver = usbd.BUILTIN_DEFAULT usbd.config( desc_dev=usbd.builtin_driver.desc_dev, desc_cfg=usbd.builtin_driver.desc_cfg, desc_strs=(), ) usbd.active(1)