pydfu.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. #!/usr/bin/env python
  2. # This file is part of the OpenMV project.
  3. # Copyright (c) 2013/2014 Ibrahim Abdelkader <i.abdalkader@gmail.com>
  4. # This work is licensed under the MIT license, see the file LICENSE for
  5. # details.
  6. """This module implements enough functionality to program the STM32F4xx over
  7. DFU, without requiring dfu-util.
  8. See app note AN3156 for a description of the DFU protocol.
  9. See document UM0391 for a dscription of the DFuse file.
  10. """
  11. from __future__ import print_function
  12. import argparse
  13. import re
  14. import struct
  15. import sys
  16. import usb.core
  17. import usb.util
  18. import zlib
  19. # VID/PID
  20. __VID = 0x0483
  21. __PID = 0xdf11
  22. # USB request __TIMEOUT
  23. __TIMEOUT = 4000
  24. # DFU commands
  25. __DFU_DETACH = 0
  26. __DFU_DNLOAD = 1
  27. __DFU_UPLOAD = 2
  28. __DFU_GETSTATUS = 3
  29. __DFU_CLRSTATUS = 4
  30. __DFU_GETSTATE = 5
  31. __DFU_ABORT = 6
  32. # DFU status
  33. __DFU_STATE_APP_IDLE = 0x00
  34. __DFU_STATE_APP_DETACH = 0x01
  35. __DFU_STATE_DFU_IDLE = 0x02
  36. __DFU_STATE_DFU_DOWNLOAD_SYNC = 0x03
  37. __DFU_STATE_DFU_DOWNLOAD_BUSY = 0x04
  38. __DFU_STATE_DFU_DOWNLOAD_IDLE = 0x05
  39. __DFU_STATE_DFU_MANIFEST_SYNC = 0x06
  40. __DFU_STATE_DFU_MANIFEST = 0x07
  41. __DFU_STATE_DFU_MANIFEST_WAIT_RESET = 0x08
  42. __DFU_STATE_DFU_UPLOAD_IDLE = 0x09
  43. __DFU_STATE_DFU_ERROR = 0x0a
  44. _DFU_DESCRIPTOR_TYPE = 0x21
  45. # USB device handle
  46. __dev = None
  47. __verbose = None
  48. # USB DFU interface
  49. __DFU_INTERFACE = 0
  50. import inspect
  51. if 'length' in inspect.getfullargspec(usb.util.get_string).args:
  52. # PyUSB 1.0.0.b1 has the length argument
  53. def get_string(dev, index):
  54. return usb.util.get_string(dev, 255, index)
  55. else:
  56. # PyUSB 1.0.0.b2 dropped the length argument
  57. def get_string(dev, index):
  58. return usb.util.get_string(dev, index)
  59. def init():
  60. """Initializes the found DFU device so that we can program it."""
  61. global __dev
  62. devices = get_dfu_devices(idVendor=__VID, idProduct=__PID)
  63. if not devices:
  64. raise ValueError('No DFU device found')
  65. if len(devices) > 1:
  66. raise ValueError("Multiple DFU devices found")
  67. __dev = devices[0]
  68. __dev.set_configuration()
  69. # Claim DFU interface
  70. usb.util.claim_interface(__dev, __DFU_INTERFACE)
  71. # Clear status
  72. clr_status()
  73. def clr_status():
  74. """Clears any error status (perhaps left over from a previous session)."""
  75. __dev.ctrl_transfer(0x21, __DFU_CLRSTATUS, 0, __DFU_INTERFACE,
  76. None, __TIMEOUT)
  77. def get_status():
  78. """Get the status of the last operation."""
  79. stat = __dev.ctrl_transfer(0xA1, __DFU_GETSTATUS, 0, __DFU_INTERFACE,
  80. 6, 20000)
  81. # print (__DFU_STAT[stat[4]], stat)
  82. return stat[4]
  83. def mass_erase():
  84. """Performs a MASS erase (i.e. erases the entire device."""
  85. # Send DNLOAD with first byte=0x41
  86. __dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE,
  87. "\x41", __TIMEOUT)
  88. # Execute last command
  89. if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
  90. raise Exception("DFU: erase failed")
  91. # Check command state
  92. if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
  93. raise Exception("DFU: erase failed")
  94. def page_erase(addr):
  95. """Erases a single page."""
  96. if __verbose:
  97. print("Erasing page: 0x%x..." % (addr))
  98. # Send DNLOAD with first byte=0x41 and page address
  99. buf = struct.pack("<BI", 0x41, addr)
  100. __dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE, buf, __TIMEOUT)
  101. # Execute last command
  102. if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
  103. raise Exception("DFU: erase failed")
  104. # Check command state
  105. if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
  106. raise Exception("DFU: erase failed")
  107. def set_address(addr):
  108. """Sets the address for the next operation."""
  109. # Send DNLOAD with first byte=0x21 and page address
  110. buf = struct.pack("<BI", 0x21, addr)
  111. __dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE, buf, __TIMEOUT)
  112. # Execute last command
  113. if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
  114. raise Exception("DFU: set address failed")
  115. # Check command state
  116. if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
  117. raise Exception("DFU: set address failed")
  118. def write_memory(addr, buf, progress=None, progress_addr=0, progress_size=0):
  119. """Writes a buffer into memory. This routine assumes that memory has
  120. already been erased.
  121. """
  122. xfer_count = 0
  123. xfer_bytes = 0
  124. xfer_total = len(buf)
  125. xfer_base = addr
  126. while xfer_bytes < xfer_total:
  127. if __verbose and xfer_count % 512 == 0:
  128. print ("Addr 0x%x %dKBs/%dKBs..." % (xfer_base + xfer_bytes,
  129. xfer_bytes // 1024,
  130. xfer_total // 1024))
  131. if progress and xfer_count % 2 == 0:
  132. progress(progress_addr, xfer_base + xfer_bytes - progress_addr,
  133. progress_size)
  134. # Set mem write address
  135. set_address(xfer_base+xfer_bytes)
  136. # Send DNLOAD with fw data
  137. # the "2048" is the DFU transfer size supported by the ST DFU bootloader
  138. # TODO: this number should be extracted from the USB config descriptor
  139. chunk = min(2048, xfer_total-xfer_bytes)
  140. __dev.ctrl_transfer(0x21, __DFU_DNLOAD, 2, __DFU_INTERFACE,
  141. buf[xfer_bytes:xfer_bytes + chunk], __TIMEOUT)
  142. # Execute last command
  143. if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
  144. raise Exception("DFU: write memory failed")
  145. # Check command state
  146. if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
  147. raise Exception("DFU: write memory failed")
  148. xfer_count += 1
  149. xfer_bytes += chunk
  150. def write_page(buf, xfer_offset):
  151. """Writes a single page. This routine assumes that memory has already
  152. been erased.
  153. """
  154. xfer_base = 0x08000000
  155. # Set mem write address
  156. set_address(xfer_base+xfer_offset)
  157. # Send DNLOAD with fw data
  158. __dev.ctrl_transfer(0x21, __DFU_DNLOAD, 2, __DFU_INTERFACE, buf, __TIMEOUT)
  159. # Execute last command
  160. if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
  161. raise Exception("DFU: write memory failed")
  162. # Check command state
  163. if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
  164. raise Exception("DFU: write memory failed")
  165. if __verbose:
  166. print ("Write: 0x%x " % (xfer_base + xfer_offset))
  167. def exit_dfu():
  168. """Exit DFU mode, and start running the program."""
  169. # set jump address
  170. set_address(0x08000000)
  171. # Send DNLOAD with 0 length to exit DFU
  172. __dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE,
  173. None, __TIMEOUT)
  174. try:
  175. # Execute last command
  176. if get_status() != __DFU_STATE_DFU_MANIFEST:
  177. print("Failed to reset device")
  178. # Release device
  179. usb.util.dispose_resources(__dev)
  180. except:
  181. pass
  182. def named(values, names):
  183. """Creates a dict with `names` as fields, and `values` as values."""
  184. return dict(zip(names.split(), values))
  185. def consume(fmt, data, names):
  186. """Parses the struct defined by `fmt` from `data`, stores the parsed fields
  187. into a named tuple using `names`. Returns the named tuple, and the data
  188. with the struct stripped off."""
  189. size = struct.calcsize(fmt)
  190. return named(struct.unpack(fmt, data[:size]), names), data[size:]
  191. def cstring(string):
  192. """Extracts a null-terminated string from a byte array."""
  193. return string.decode('utf-8').split('\0', 1)[0]
  194. def compute_crc(data):
  195. """Computes the CRC32 value for the data passed in."""
  196. return 0xFFFFFFFF & -zlib.crc32(data) - 1
  197. def read_dfu_file(filename):
  198. """Reads a DFU file, and parses the individual elements from the file.
  199. Returns an array of elements. Each element is a dictionary with the
  200. following keys:
  201. num - The element index
  202. address - The address that the element data should be written to.
  203. size - The size of the element ddata.
  204. data - The element data.
  205. If an error occurs while parsing the file, then None is returned.
  206. """
  207. print("File: {}".format(filename))
  208. with open(filename, 'rb') as fin:
  209. data = fin.read()
  210. crc = compute_crc(data[:-4])
  211. elements = []
  212. # Decode the DFU Prefix
  213. #
  214. # <5sBIB
  215. # < little endian
  216. # 5s char[5] signature "DfuSe"
  217. # B uint8_t version 1
  218. # I uint32_t size Size of the DFU file (not including suffix)
  219. # B uint8_t targets Number of targets
  220. dfu_prefix, data = consume('<5sBIB', data,
  221. 'signature version size targets')
  222. print (" %(signature)s v%(version)d, image size: %(size)d, "
  223. "targets: %(targets)d" % dfu_prefix)
  224. for target_idx in range(dfu_prefix['targets']):
  225. # Decode the Image Prefix
  226. #
  227. # <6sBI255s2I
  228. # < little endian
  229. # 6s char[6] signature "Target"
  230. # B uint8_t altsetting
  231. # I uint32_t named bool indicating if a name was used
  232. # 255s char[255] name name of the target
  233. # I uint32_t size size of image (not incl prefix)
  234. # I uint32_t elements Number of elements in the image
  235. img_prefix, data = consume('<6sBI255s2I', data,
  236. 'signature altsetting named name '
  237. 'size elements')
  238. img_prefix['num'] = target_idx
  239. if img_prefix['named']:
  240. img_prefix['name'] = cstring(img_prefix['name'])
  241. else:
  242. img_prefix['name'] = ''
  243. print(' %(signature)s %(num)d, alt setting: %(altsetting)s, '
  244. 'name: "%(name)s", size: %(size)d, elements: %(elements)d'
  245. % img_prefix)
  246. target_size = img_prefix['size']
  247. target_data, data = data[:target_size], data[target_size:]
  248. for elem_idx in range(img_prefix['elements']):
  249. # Decode target prefix
  250. # < little endian
  251. # I uint32_t element address
  252. # I uint32_t element size
  253. elem_prefix, target_data = consume('<2I', target_data, 'addr size')
  254. elem_prefix['num'] = elem_idx
  255. print(' %(num)d, address: 0x%(addr)08x, size: %(size)d'
  256. % elem_prefix)
  257. elem_size = elem_prefix['size']
  258. elem_data = target_data[:elem_size]
  259. target_data = target_data[elem_size:]
  260. elem_prefix['data'] = elem_data
  261. elements.append(elem_prefix)
  262. if len(target_data):
  263. print("target %d PARSE ERROR" % target_idx)
  264. # Decode DFU Suffix
  265. # < little endian
  266. # H uint16_t device Firmware version
  267. # H uint16_t product
  268. # H uint16_t vendor
  269. # H uint16_t dfu 0x11a (DFU file format version)
  270. # 3s char[3] ufd 'UFD'
  271. # B uint8_t len 16
  272. # I uint32_t crc32
  273. dfu_suffix = named(struct.unpack('<4H3sBI', data[:16]),
  274. 'device product vendor dfu ufd len crc')
  275. print (' usb: %(vendor)04x:%(product)04x, device: 0x%(device)04x, '
  276. 'dfu: 0x%(dfu)04x, %(ufd)s, %(len)d, 0x%(crc)08x' % dfu_suffix)
  277. if crc != dfu_suffix['crc']:
  278. print("CRC ERROR: computed crc32 is 0x%08x" % crc)
  279. return
  280. data = data[16:]
  281. if data:
  282. print("PARSE ERROR")
  283. return
  284. return elements
  285. class FilterDFU(object):
  286. """Class for filtering USB devices to identify devices which are in DFU
  287. mode.
  288. """
  289. def __call__(self, device):
  290. for cfg in device:
  291. for intf in cfg:
  292. return (intf.bInterfaceClass == 0xFE and
  293. intf.bInterfaceSubClass == 1)
  294. def get_dfu_devices(*args, **kwargs):
  295. """Returns a list of USB device which are currently in DFU mode.
  296. Additional filters (like idProduct and idVendor) can be passed in to
  297. refine the search.
  298. """
  299. # convert to list for compatibility with newer pyusb
  300. return list(usb.core.find(*args, find_all=True,
  301. custom_match=FilterDFU(), **kwargs))
  302. def get_memory_layout(device):
  303. """Returns an array which identifies the memory layout. Each entry
  304. of the array will contain a dictionary with the following keys:
  305. addr - Address of this memory segment
  306. last_addr - Last address contained within the memory segment.
  307. size - size of the segment, in bytes
  308. num_pages - number of pages in the segment
  309. page_size - size of each page, in bytes
  310. """
  311. cfg = device[0]
  312. intf = cfg[(0, 0)]
  313. mem_layout_str = get_string(device, intf.iInterface)
  314. mem_layout = mem_layout_str.split('/')
  315. result = []
  316. for mem_layout_index in range(1, len(mem_layout), 2):
  317. addr = int(mem_layout[mem_layout_index], 0)
  318. segments = mem_layout[mem_layout_index + 1].split(',')
  319. seg_re = re.compile(r'(\d+)\*(\d+)(.)(.)')
  320. for segment in segments:
  321. seg_match = seg_re.match(segment)
  322. num_pages = int(seg_match.groups()[0], 10)
  323. page_size = int(seg_match.groups()[1], 10)
  324. multiplier = seg_match.groups()[2]
  325. if multiplier == 'K':
  326. page_size *= 1024
  327. if multiplier == 'M':
  328. page_size *= 1024 * 1024
  329. size = num_pages * page_size
  330. last_addr = addr + size - 1
  331. result.append(named((addr, last_addr, size, num_pages, page_size),
  332. "addr last_addr size num_pages page_size"))
  333. addr += size
  334. return result
  335. def list_dfu_devices(*args, **kwargs):
  336. """Prints a lits of devices detected in DFU mode."""
  337. devices = get_dfu_devices(*args, **kwargs)
  338. if not devices:
  339. print("No DFU capable devices found")
  340. return
  341. for device in devices:
  342. print("Bus {} Device {:03d}: ID {:04x}:{:04x}"
  343. .format(device.bus, device.address,
  344. device.idVendor, device.idProduct))
  345. layout = get_memory_layout(device)
  346. print("Memory Layout")
  347. for entry in layout:
  348. print(" 0x{:x} {:2d} pages of {:3d}K bytes"
  349. .format(entry['addr'], entry['num_pages'],
  350. entry['page_size'] // 1024))
  351. def write_elements(elements, mass_erase_used, progress=None):
  352. """Writes the indicated elements into the target memory,
  353. erasing as needed.
  354. """
  355. mem_layout = get_memory_layout(__dev)
  356. for elem in elements:
  357. addr = elem['addr']
  358. size = elem['size']
  359. data = elem['data']
  360. elem_size = size
  361. elem_addr = addr
  362. if progress:
  363. progress(elem_addr, 0, elem_size)
  364. while size > 0:
  365. write_size = size
  366. if not mass_erase_used:
  367. for segment in mem_layout:
  368. if addr >= segment['addr'] and \
  369. addr <= segment['last_addr']:
  370. # We found the page containing the address we want to
  371. # write, erase it
  372. page_size = segment['page_size']
  373. page_addr = addr & ~(page_size - 1)
  374. if addr + write_size > page_addr + page_size:
  375. write_size = page_addr + page_size - addr
  376. page_erase(page_addr)
  377. break
  378. write_memory(addr, data[:write_size], progress,
  379. elem_addr, elem_size)
  380. data = data[write_size:]
  381. addr += write_size
  382. size -= write_size
  383. if progress:
  384. progress(elem_addr, addr - elem_addr, elem_size)
  385. def cli_progress(addr, offset, size):
  386. """Prints a progress report suitable for use on the command line."""
  387. width = 25
  388. done = offset * width // size
  389. print("\r0x{:08x} {:7d} [{}{}] {:3d}% "
  390. .format(addr, size, '=' * done, ' ' * (width - done),
  391. offset * 100 // size), end="")
  392. sys.stdout.flush()
  393. if offset == size:
  394. print("")
  395. def main():
  396. """Test program for verifying this files functionality."""
  397. global __verbose
  398. # Parse CMD args
  399. parser = argparse.ArgumentParser(description='DFU Python Util')
  400. #parser.add_argument("path", help="file path")
  401. parser.add_argument(
  402. "-l", "--list",
  403. help="list available DFU devices",
  404. action="store_true",
  405. default=False
  406. )
  407. parser.add_argument(
  408. "-m", "--mass-erase",
  409. help="mass erase device",
  410. action="store_true",
  411. default=False
  412. )
  413. parser.add_argument(
  414. "-u", "--upload",
  415. help="read file from DFU device",
  416. dest="path",
  417. default=False
  418. )
  419. parser.add_argument(
  420. "-v", "--verbose",
  421. help="increase output verbosity",
  422. action="store_true",
  423. default=False
  424. )
  425. args = parser.parse_args()
  426. __verbose = args.verbose
  427. if args.list:
  428. list_dfu_devices(idVendor=__VID, idProduct=__PID)
  429. return
  430. init()
  431. if args.mass_erase:
  432. print ("Mass erase...")
  433. mass_erase()
  434. if args.path:
  435. elements = read_dfu_file(args.path)
  436. if not elements:
  437. return
  438. print("Writing memory...")
  439. write_elements(elements, args.mass_erase, progress=cli_progress)
  440. print("Exiting DFU...")
  441. exit_dfu()
  442. return
  443. print("No command specified")
  444. if __name__ == '__main__':
  445. main()