Fork from bluejay at github and modified for my custom ESC. I need to modify it because some mistake design on my ESC hardware.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

454 lines
18 KiB

  1. #!/usr/bin/env python3
  2. #
  3. # This file is part of efm8load. efm8load is free software: you can
  4. # redistribute it and/or modify it under the terms of the GNU General Public
  5. # License as published by the Free Software Foundation, version 3.
  6. #
  7. # This program is distributed in the hope that it will be useful, but WITHOUT
  8. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  9. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
  10. # details.
  11. #
  12. # You should have received a copy of the GNU General Public License along with
  13. # this program; if not, write to the Free Software Foundation, Inc., 51
  14. # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  15. #
  16. # Copyright 2020 fishpepper.de
  17. #
  18. import argparse
  19. import operator
  20. import sys
  21. import crcmod
  22. import serial
  23. from intelhex import IntelHex
  24. # make sure to install the python3 modules for serial, crcmod, and pip3:
  25. # sudo apt intstall python3-crcmod python3-serial python3-pip
  26. # if you are missing the intelhex package, you can install it afterwars by
  27. # pip3 install intelhex --user
  28. class COMMAND:
  29. IDENTIFY = 0x30
  30. SETUP = 0x31
  31. ERASE = 0x32
  32. WRITE = 0x33
  33. VERIFY = 0x34
  34. RESET = 0x36
  35. class RESPONSE:
  36. ACK = 0x40
  37. RANGE_ERROR = 0x41
  38. BAD_ID = 0x42
  39. CRC_ERROR = 0x43
  40. TO_STR = { ACK: "ACK", RANGE_ERROR : "RANGE_ERROR", BAD_ID : "BAD_ID", CRC_ERROR : "CRC_ERROR" }
  41. @staticmethod
  42. def to_string(res):
  43. if res in RESPONSE.TO_STR:
  44. return RESPONSE.TO_STR[res]
  45. else:
  46. return "unknown response"
  47. class EFM8Loader:
  48. """A python implementation of the EFM8 bootloader protocol"""
  49. devicelist = {
  50. # DEVICE_ID : [ NAME, { DICT OF VARIANT_IDS } ]
  51. # VARIANT_ID: VARIANT_ID, VARIANT_NAME, FLASH_SIZE, PAGE_SIZE, SECURITY_PAGE_SIZE]
  52. 0x16 : ["EFM8SB2", { } ],
  53. 0x25 : ["EFM8SB1", {
  54. 0x01: ["EFM8SB10F8G_QFN24", 8*1024, 512, 512],
  55. 0x02: ["EFM8SB10F8G_QSOP24", 8*1024, 512, 512],
  56. 0x03: ["EFM8SB10F8G_QFN20", 8*1024, 512, 512],
  57. 0x06: ["EFM8SB10F4G_QFN20", 4*1024, 512, 512],
  58. 0x09: ["EFM8SB10F2G_QFN20", 2*1024, 512, 512]
  59. }],
  60. 0x30 : ["EFM8BB1", {
  61. 0x01: ["EFM8BB10F8G_QSOP24", 8*1024, 512, 512 ],
  62. 0x02: ["EFM8BB10F8G_QFN20" , 8*1024, 512, 512 ],
  63. 0x03: ["EFM8BB10F8G_SOIC16", 8*1024, 512, 512 ],
  64. 0x05: ["EFM8BB10F4G_QFN20" , 4*1024, 512, 512 ],
  65. 0x08: ["EFM8BB10F2G_QFN20" , 2*1024, 512, 512 ],
  66. 0x12: ["EFM8BB10F8I_QFN20" , 8*1024, 512, 521 ]
  67. }],
  68. 0x32 : ["EFM8BB2", {
  69. 0x01: ["EFM8BB22F16G_QFN28" , 16*1024, 512, 512],
  70. 0x02: ["EFM8BB21F16G_QSOP24", 16*1024, 512, 512],
  71. 0x03: ["EFM8BB21F16G_QFN20" , 16*1024, 512, 512]
  72. }],
  73. 0x34 : ["EFM8BB3", {
  74. 0x01: ["EFM8BB31F64G-QFN32" , 64*1024, 512, 512],
  75. }]
  76. }
  77. def __init__(self, port, baud, debug = False):
  78. self.debug = debug
  79. self.serial = serial.Serial()
  80. self.serial.port = port
  81. self.serial.baudrate = baud
  82. self.serial.timeout = 1
  83. #defaults
  84. self.flash_page_size = 512
  85. self.flash_size = 16*1024
  86. self.flash_security_size = 512
  87. #open serial connection
  88. self.open_port()
  89. def __del__(self):
  90. self.close_port()
  91. def open_port(self):
  92. print("> opening port '%s' (%d baud)" % (self.serial.port, self.serial.baudrate))
  93. try:
  94. self.serial.open()
  95. except:
  96. sys.exit("ERROR: failed to open serial port '%s'!" % (self.serial.port))
  97. def close_port(self):
  98. try:
  99. self.serial.close()
  100. except serial.SerialException:
  101. sys.exit("ERROR: failed to close serial port")
  102. def send_autobaud_training(self):
  103. if (self.debug): print("> sending training char 0xFF")
  104. for i in range(2):
  105. self.send_byte(0xff)
  106. def send_byte(self, b):
  107. try:
  108. self.serial.write(b.to_bytes(1, 'little'))
  109. except serial.SerialException:
  110. sys.exit("ERROR: failed to send byte to serial port")
  111. def identify_chip(self):
  112. print("> checking for device")
  113. #send autobaud training
  114. self.send_autobaud_training()
  115. #enable flash access
  116. self.enable_flash_access()
  117. #we will now iterate through all known device ids
  118. for device_id, device in self.devicelist.items():
  119. device_name = device[0]
  120. variant_ids = device[1]
  121. if (self.debug): print("> checking for device %s" % (device_name))
  122. for variant_id, config in variant_ids.items():
  123. #test all possible variant ids
  124. variant_name = config[0]
  125. if (self.check_id(device_id, variant_id)):
  126. print("> success, detected %s cpu (variant %s)" % (device_name, variant_name))
  127. #set up chip data
  128. self.flash_size = config[1]
  129. self.flash_page_size = config[2]
  130. self.flash_security_page_size = config[3]
  131. print("> detected %s cpu (variant %s, flash_size=%d, pagesize=%d)" % (device_name, variant_name, self.flash_size, self.flash_page_size))
  132. return 1
  133. #we did not detect a known device, scann all posible ids:
  134. for device_id in range(0xFF):
  135. print("\r> checking device_id 0x%02X..." % (device_id), end="")
  136. sys.stdout.flush()
  137. for variant_id in range(24):
  138. if (self.check_id(device_id, variant_id)):
  139. sys.exit("\n> ERROR: unknown device detected: id=0x%02X, variant=0x%02X\n"\
  140. " please add it to the devicelist. will exit now\n" % (device_id, variant_id))
  141. sys.exit("> ERROR: could not find any device...")
  142. def send_reset(self):
  143. print("> send reset command")
  144. if (self.send(COMMAND.RESET, [255, 255]) == RESPONSE.ACK):
  145. print("> success, device restarted...")
  146. def send(self, cmd, data):
  147. length = len(data)
  148. #check length
  149. if (length < 2) or (length > 130):
  150. sys.exit("> ERROR: invalid data length! allowed 2...130, got %d" % (length))
  151. try:
  152. if (self.debug):
  153. data_str = "".join('0x{:02x} '.format(x) for x in data[:16])
  154. if (length > 16): data_str = data_str + "..."
  155. print("> sending $ len=%d cmd=0x%02X data={ %s}" % (length, cmd, data_str))
  156. self.serial.write(b'$')
  157. self.serial.write((length + 1).to_bytes(1, 'little'))
  158. self.serial.write(cmd.to_bytes(1, 'little'))
  159. self.serial.write(bytearray(data))
  160. #read back reply
  161. res_bytes = self.serial.read(1)
  162. #res_bytes = b"\x40"
  163. if (len(res_bytes) != 1):
  164. sys.exit("> ERROR: serial read timed out")
  165. return 0
  166. else:
  167. res = res_bytes[0]
  168. if(self.debug): print("> reply 0x%02X" % (res))
  169. return res
  170. except serial.SerialException:
  171. sys.exit("ERROR: failed to send data")
  172. def check_id(self, device_id, derivative_id):
  173. #verify that the given id matches the target
  174. return self.send(COMMAND.IDENTIFY, [device_id, derivative_id]) == RESPONSE.ACK
  175. def enable_flash_access(self):
  176. res = self.send(COMMAND.SETUP, [0xA5, 0xF1, 0x00])
  177. if (res != RESPONSE.ACK):
  178. sys.exit("> ERROR enabling flash access, error code 0x%02X (%s)" % (res, RESPONSE.to_string(res)))
  179. def erase_page(self, page):
  180. start = page * self.flash_page_size
  181. end = start + self.flash_page_size-1
  182. start_hi = (start >> 8) & 0xFF
  183. start_lo = start & 0xFF
  184. print("> will erase page %d (0x%04X-0x%04X)" % (page, start, end))
  185. return self.send(COMMAND.ERASE, [start_hi, start_lo])
  186. def write(self, address, data):
  187. if (len(data) > 128):
  188. sys.exit("ERROR: invalid chunksize, maximum allowed write is 128 bytes (%d)" % (len(data)))
  189. #print some of the data as debug info
  190. if (len(data) > 8):
  191. data_excerpt = "".join('0x{:02x} '.format(x) for x in data[:4]) + \
  192. "... " + \
  193. "".join('0x{:02x} '.format(x) for x in data[-4:])
  194. else:
  195. data_excerpt = "".join('0x{:02x} '.format(x) for x in data)
  196. print("> write at 0x%04X (%3d): %s" % (address, len(data), data_excerpt))
  197. #send request
  198. address_hi = (address >> 8) & 0xFF
  199. address_lo = address & 0xFF
  200. res = self.send(COMMAND.WRITE, [address_hi, address_lo] + data)
  201. if not (res == RESPONSE.ACK):
  202. sys.exit("ERROR: write failed at address 0x%04X (response = %s)" % (address, RESPONSE.to_string(res)))
  203. return res
  204. def verify(self, address, data):
  205. length = len(data)
  206. crc16 = crcmod.predefined.mkCrcFun('xmodem')(bytearray(data))
  207. if (self.debug): print("> verify address 0x%04X (len=%d, crc16=0x%04X)" % (address, length, crc16))
  208. start_hi = (address >> 8) & 0xFF
  209. start_lo = address & 0xFF
  210. end = address + length - 1
  211. end_hi = (end >> 8) & 0xFF
  212. end_lo = end & 0xFF
  213. crc_hi = (crc16 >> 8) & 0xFF
  214. crc_lo = crc16 & 0xFF
  215. res = self.send(COMMAND.VERIFY, [start_hi, start_lo] + [end_hi, end_lo] + [crc_hi, crc_lo])
  216. return res
  217. def download(self, filename):
  218. print("> dumping flash content to '%s'" % filename)
  219. print("> please note that this will take long")
  220. #check for chip
  221. self.identify_chip()
  222. self.debug = False
  223. #send autobaud training character
  224. self.send_autobaud_training()
  225. #enable flash access
  226. self.enable_flash_access()
  227. #the bootloader protocol does not allow reading flash
  228. #however it allows to verify written bytes
  229. #we will exploit this feature to dump the flash contents
  230. #for now assume 8kb flash
  231. flash_size = 8 * 1024
  232. ih = IntelHex()
  233. for address in range(flash_size):
  234. #test one byte by byte
  235. #first check 0x00
  236. byte = 0
  237. if (self.verify(address, [byte]) == RESPONSE.ACK):
  238. ih[address] = byte
  239. else:
  240. #now start with 0xFF (empty flash)
  241. for byte in range(0xFF, -1, -1):
  242. if (self.verify(address, [byte]) == RESPONSE.ACK):
  243. #success, the flash content on this address euals <byte>
  244. ih[address] = byte
  245. break
  246. print("\r> flash[0x%04X] = 0x%02X" % (address, byte), end="")
  247. sys.stdout.flush()
  248. print("\n> finished")
  249. #done, all flash contents have been read, now store this to the file
  250. ih.write_hex_file(filename)
  251. def upload(self, filename):
  252. print("> uploading file '%s'" % (filename))
  253. #identify chip
  254. self.identify_chip()
  255. #read hex file
  256. ih = IntelHex()
  257. ih.loadhex(filename)
  258. #send autobaud training character
  259. self.send_autobaud_training()
  260. #enable flash access
  261. self.enable_flash_access()
  262. #erase pages where we are going to write
  263. self.erase_pages_ih(ih)
  264. #write all data bytes
  265. self.write_pages_ih(ih)
  266. self.verify_pages_ih(ih)
  267. def erase_pages_ih(self, ih):
  268. """ erase all pages that are occupied """
  269. last_address = ih.addresses()[-1]
  270. last_page = int(last_address / self.flash_page_size)
  271. for page in range(last_page+1):
  272. start = page * self.flash_page_size
  273. end = start + self.flash_page_size-1
  274. page_used = False
  275. for x in ih.addresses():
  276. if x >= start and x <= end:
  277. page_used = True
  278. break
  279. #always erase page 0 to retain bootloader access
  280. if (page == 0) or (page_used):
  281. self.erase_page(page)
  282. def write_pages_ih(self, ih):
  283. """ write all segments from this ihex to flash"""
  284. #NOTE: it is important to keep flash location 0
  285. # equal to 0xFF until we are almost finished...
  286. # therefore the bootloader will still be functional in case
  287. # something goes wrong in the process.
  288. # (the bootloader will be executed as long the first flash
  289. # content equals 0xFF)
  290. byte_zero = -1
  291. for start,end in ih.segments():
  292. print("> writing segment 0x%04X-0x%04X" % (start, end-1))
  293. #fetch data
  294. data = []
  295. for x in range(start,end):
  296. data.append(ih[x])
  297. #write in 128byte blobs
  298. data_pos = 0
  299. #keep byte zero 0xFF in order to keep bootloader active (for now)
  300. if (start == 0):
  301. print("> delaying write of flash[0] = 0x%02X to the end" % (data[0]))
  302. byte_zero = data[0]
  303. start = start + 1
  304. data.pop(0)
  305. while ((data_pos + start) < end):
  306. length = min(128, end - (data_pos + start))
  307. self.write(start + data_pos, data[data_pos:data_pos+length])
  308. data_pos = data_pos + length
  309. #now verify this segment
  310. print("> verifying segment... ", end="")
  311. sys.stdout.flush()
  312. if (self.verify(start, data) == RESPONSE.ACK):
  313. print("OK")
  314. else :
  315. sys.exit("FAILURE. will abort now\n")
  316. #all bytes except byte zero were written, do this now
  317. if (byte_zero != -1):
  318. print("> will now write flash[0] = 0x%02X" % (byte_zero))
  319. res = self.write(0, [byte_zero])
  320. if (res != RESPONSE.ACK):
  321. print("> ERROR, write of flash[0] failed (response = %s)" % (RESPONSE.to_string(res)))
  322. self.restore_bootloader_autostart()
  323. sys.exit("FAILED")
  324. #verify
  325. res = self.verify(0, [byte_zero])
  326. if (res != RESPONSE.ACK):
  327. print("> ERROR, verify of flash[0] failed (response = %s)" % (RESPONSE.to_string(res)))
  328. self.self.restore_bootloader_autostarti()
  329. sys.exit("FAILED")
  330. def restore_bootloader_autostart(self):
  331. #the bootloader will always start if flash[0] = 0xFF
  332. #in case something went wrong during programming,
  333. #call this in order to clear page 0 so that the bootloader
  334. #will always start
  335. print("> will now erase page 0 in order to re-enable bootloader autorun");
  336. self.erase_page(0)
  337. def verify_pages_ih(self, ih):
  338. """ verify written data """
  339. #do a pagewise compare to find the position of
  340. #the mismatch
  341. for start,end in ih.segments():
  342. print("> verifying segment 0x%04X-0x%04X... " % (start, end-1), end="")
  343. sys.stdout.flush()
  344. #fetch data
  345. data = []
  346. for x in range(start,end):
  347. data.append(ih[x])
  348. #calc crc16
  349. if (self.verify(start, data) == RESPONSE.ACK):
  350. print("OK")
  351. else :
  352. sys.exit("FAILURE. will abort now\n")
  353. return 1
  354. if __name__ == "__main__":
  355. argp = argparse.ArgumentParser(description='efm8load - a plain python implementation for the EFM8 usart bootloader protocol')
  356. group = argp.add_mutually_exclusive_group()
  357. group.add_argument("-w", "--write", metavar="filename", help="upload the given hex file to the flash memory")
  358. group.add_argument("-r", "--read", metavar="filename", help="download the flash memory contents to the given filename") #action="store_true", nargs=1)
  359. group.add_argument("-i", "--identify", help="identify the chip", action="store_true")
  360. group.add_argument("-s", "--reset", help="send reset command", action="store_true")
  361. #argp.add_argument('filename', help='firmware file to upload to the mcu')
  362. argp.add_argument('-b', '--baudrate', type=int, default=115200, help='baudrate (default is 115200 baud)')
  363. argp.add_argument('-p', '--port', default="/dev/ttyUSB0", help='port (default is /dev/ttyUSB0)')
  364. argp.add_argument('-v', '--verbose', action='store_true', help='Verbose mode')
  365. args = argp.parse_args()
  366. print("########################################")
  367. print("# efm8load.py - (c) 2020 fishpepper.de #")
  368. print("########################################")
  369. print("")
  370. efm8loader = EFM8Loader(args.port, args.baudrate, debug=args.verbose)
  371. if (args.identify):
  372. efm8loader.identify_chip()
  373. elif (args.write):
  374. efm8loader.upload(args.write)
  375. elif (args.read):
  376. efm8loader.download(args.read)
  377. elif (args.reset):
  378. efm8loader.send_reset()
  379. else:
  380. argp.print_help()
  381. sys.exit(1)
  382. print()
  383. sys.exit(0)