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
454 lines
18 KiB
#!/usr/bin/env python3
|
|
#
|
|
# This file is part of efm8load. efm8load is free software: you can
|
|
# redistribute it and/or modify it under the terms of the GNU General Public
|
|
# License as published by the Free Software Foundation, version 3.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# this program; if not, write to the Free Software Foundation, Inc., 51
|
|
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
#
|
|
# Copyright 2020 fishpepper.de
|
|
#
|
|
import argparse
|
|
import operator
|
|
import sys
|
|
|
|
import crcmod
|
|
import serial
|
|
from intelhex import IntelHex
|
|
|
|
|
|
# make sure to install the python3 modules for serial, crcmod, and pip3:
|
|
# sudo apt intstall python3-crcmod python3-serial python3-pip
|
|
# if you are missing the intelhex package, you can install it afterwars by
|
|
# pip3 install intelhex --user
|
|
|
|
class COMMAND:
|
|
IDENTIFY = 0x30
|
|
SETUP = 0x31
|
|
ERASE = 0x32
|
|
WRITE = 0x33
|
|
VERIFY = 0x34
|
|
RESET = 0x36
|
|
|
|
class RESPONSE:
|
|
ACK = 0x40
|
|
RANGE_ERROR = 0x41
|
|
BAD_ID = 0x42
|
|
CRC_ERROR = 0x43
|
|
TO_STR = { ACK: "ACK", RANGE_ERROR : "RANGE_ERROR", BAD_ID : "BAD_ID", CRC_ERROR : "CRC_ERROR" }
|
|
|
|
@staticmethod
|
|
def to_string(res):
|
|
if res in RESPONSE.TO_STR:
|
|
return RESPONSE.TO_STR[res]
|
|
else:
|
|
return "unknown response"
|
|
|
|
class EFM8Loader:
|
|
"""A python implementation of the EFM8 bootloader protocol"""
|
|
|
|
devicelist = {
|
|
# DEVICE_ID : [ NAME, { DICT OF VARIANT_IDS } ]
|
|
# VARIANT_ID: VARIANT_ID, VARIANT_NAME, FLASH_SIZE, PAGE_SIZE, SECURITY_PAGE_SIZE]
|
|
0x16 : ["EFM8SB2", { } ],
|
|
0x25 : ["EFM8SB1", {
|
|
0x01: ["EFM8SB10F8G_QFN24", 8*1024, 512, 512],
|
|
0x02: ["EFM8SB10F8G_QSOP24", 8*1024, 512, 512],
|
|
0x03: ["EFM8SB10F8G_QFN20", 8*1024, 512, 512],
|
|
0x06: ["EFM8SB10F4G_QFN20", 4*1024, 512, 512],
|
|
0x09: ["EFM8SB10F2G_QFN20", 2*1024, 512, 512]
|
|
}],
|
|
|
|
0x30 : ["EFM8BB1", {
|
|
0x01: ["EFM8BB10F8G_QSOP24", 8*1024, 512, 512 ],
|
|
0x02: ["EFM8BB10F8G_QFN20" , 8*1024, 512, 512 ],
|
|
0x03: ["EFM8BB10F8G_SOIC16", 8*1024, 512, 512 ],
|
|
0x05: ["EFM8BB10F4G_QFN20" , 4*1024, 512, 512 ],
|
|
0x08: ["EFM8BB10F2G_QFN20" , 2*1024, 512, 512 ],
|
|
0x12: ["EFM8BB10F8I_QFN20" , 8*1024, 512, 521 ]
|
|
}],
|
|
0x32 : ["EFM8BB2", {
|
|
0x01: ["EFM8BB22F16G_QFN28" , 16*1024, 512, 512],
|
|
0x02: ["EFM8BB21F16G_QSOP24", 16*1024, 512, 512],
|
|
0x03: ["EFM8BB21F16G_QFN20" , 16*1024, 512, 512]
|
|
}],
|
|
0x34 : ["EFM8BB3", {
|
|
0x01: ["EFM8BB31F64G-QFN32" , 64*1024, 512, 512],
|
|
}]
|
|
}
|
|
|
|
def __init__(self, port, baud, debug = False):
|
|
self.debug = debug
|
|
self.serial = serial.Serial()
|
|
self.serial.port = port
|
|
self.serial.baudrate = baud
|
|
self.serial.timeout = 1
|
|
#defaults
|
|
self.flash_page_size = 512
|
|
self.flash_size = 16*1024
|
|
self.flash_security_size = 512
|
|
#open serial connection
|
|
self.open_port()
|
|
|
|
def __del__(self):
|
|
self.close_port()
|
|
|
|
def open_port(self):
|
|
print("> opening port '%s' (%d baud)" % (self.serial.port, self.serial.baudrate))
|
|
try:
|
|
self.serial.open()
|
|
except:
|
|
sys.exit("ERROR: failed to open serial port '%s'!" % (self.serial.port))
|
|
|
|
def close_port(self):
|
|
try:
|
|
self.serial.close()
|
|
except serial.SerialException:
|
|
sys.exit("ERROR: failed to close serial port")
|
|
|
|
def send_autobaud_training(self):
|
|
if (self.debug): print("> sending training char 0xFF")
|
|
for i in range(2):
|
|
self.send_byte(0xff)
|
|
|
|
def send_byte(self, b):
|
|
try:
|
|
self.serial.write(b.to_bytes(1, 'little'))
|
|
except serial.SerialException:
|
|
sys.exit("ERROR: failed to send byte to serial port")
|
|
|
|
def identify_chip(self):
|
|
print("> checking for device")
|
|
|
|
#send autobaud training
|
|
self.send_autobaud_training()
|
|
|
|
#enable flash access
|
|
self.enable_flash_access()
|
|
|
|
#we will now iterate through all known device ids
|
|
for device_id, device in self.devicelist.items():
|
|
device_name = device[0]
|
|
variant_ids = device[1]
|
|
if (self.debug): print("> checking for device %s" % (device_name))
|
|
for variant_id, config in variant_ids.items():
|
|
#test all possible variant ids
|
|
variant_name = config[0]
|
|
|
|
if (self.check_id(device_id, variant_id)):
|
|
print("> success, detected %s cpu (variant %s)" % (device_name, variant_name))
|
|
#set up chip data
|
|
self.flash_size = config[1]
|
|
self.flash_page_size = config[2]
|
|
self.flash_security_page_size = config[3]
|
|
print("> detected %s cpu (variant %s, flash_size=%d, pagesize=%d)" % (device_name, variant_name, self.flash_size, self.flash_page_size))
|
|
return 1
|
|
|
|
#we did not detect a known device, scann all posible ids:
|
|
for device_id in range(0xFF):
|
|
print("\r> checking device_id 0x%02X..." % (device_id), end="")
|
|
sys.stdout.flush()
|
|
for variant_id in range(24):
|
|
if (self.check_id(device_id, variant_id)):
|
|
sys.exit("\n> ERROR: unknown device detected: id=0x%02X, variant=0x%02X\n"\
|
|
" please add it to the devicelist. will exit now\n" % (device_id, variant_id))
|
|
|
|
sys.exit("> ERROR: could not find any device...")
|
|
|
|
def send_reset(self):
|
|
print("> send reset command")
|
|
|
|
if (self.send(COMMAND.RESET, [255, 255]) == RESPONSE.ACK):
|
|
print("> success, device restarted...")
|
|
|
|
def send(self, cmd, data):
|
|
length = len(data)
|
|
|
|
#check length
|
|
if (length < 2) or (length > 130):
|
|
sys.exit("> ERROR: invalid data length! allowed 2...130, got %d" % (length))
|
|
|
|
try:
|
|
if (self.debug):
|
|
data_str = "".join('0x{:02x} '.format(x) for x in data[:16])
|
|
if (length > 16): data_str = data_str + "..."
|
|
print("> sending $ len=%d cmd=0x%02X data={ %s}" % (length, cmd, data_str))
|
|
self.serial.write(b'$')
|
|
self.serial.write((length + 1).to_bytes(1, 'little'))
|
|
self.serial.write(cmd.to_bytes(1, 'little'))
|
|
self.serial.write(bytearray(data))
|
|
|
|
#read back reply
|
|
res_bytes = self.serial.read(1)
|
|
#res_bytes = b"\x40"
|
|
if (len(res_bytes) != 1):
|
|
sys.exit("> ERROR: serial read timed out")
|
|
return 0
|
|
else:
|
|
res = res_bytes[0]
|
|
if(self.debug): print("> reply 0x%02X" % (res))
|
|
return res
|
|
|
|
except serial.SerialException:
|
|
sys.exit("ERROR: failed to send data")
|
|
|
|
|
|
def check_id(self, device_id, derivative_id):
|
|
#verify that the given id matches the target
|
|
return self.send(COMMAND.IDENTIFY, [device_id, derivative_id]) == RESPONSE.ACK
|
|
|
|
def enable_flash_access(self):
|
|
res = self.send(COMMAND.SETUP, [0xA5, 0xF1, 0x00])
|
|
if (res != RESPONSE.ACK):
|
|
sys.exit("> ERROR enabling flash access, error code 0x%02X (%s)" % (res, RESPONSE.to_string(res)))
|
|
|
|
def erase_page(self, page):
|
|
start = page * self.flash_page_size
|
|
end = start + self.flash_page_size-1
|
|
start_hi = (start >> 8) & 0xFF
|
|
start_lo = start & 0xFF
|
|
print("> will erase page %d (0x%04X-0x%04X)" % (page, start, end))
|
|
return self.send(COMMAND.ERASE, [start_hi, start_lo])
|
|
|
|
def write(self, address, data):
|
|
if (len(data) > 128):
|
|
sys.exit("ERROR: invalid chunksize, maximum allowed write is 128 bytes (%d)" % (len(data)))
|
|
#print some of the data as debug info
|
|
if (len(data) > 8):
|
|
data_excerpt = "".join('0x{:02x} '.format(x) for x in data[:4]) + \
|
|
"... " + \
|
|
"".join('0x{:02x} '.format(x) for x in data[-4:])
|
|
else:
|
|
data_excerpt = "".join('0x{:02x} '.format(x) for x in data)
|
|
|
|
print("> write at 0x%04X (%3d): %s" % (address, len(data), data_excerpt))
|
|
|
|
#send request
|
|
address_hi = (address >> 8) & 0xFF
|
|
address_lo = address & 0xFF
|
|
res = self.send(COMMAND.WRITE, [address_hi, address_lo] + data)
|
|
if not (res == RESPONSE.ACK):
|
|
sys.exit("ERROR: write failed at address 0x%04X (response = %s)" % (address, RESPONSE.to_string(res)))
|
|
return res
|
|
|
|
def verify(self, address, data):
|
|
length = len(data)
|
|
crc16 = crcmod.predefined.mkCrcFun('xmodem')(bytearray(data))
|
|
|
|
if (self.debug): print("> verify address 0x%04X (len=%d, crc16=0x%04X)" % (address, length, crc16))
|
|
start_hi = (address >> 8) & 0xFF
|
|
start_lo = address & 0xFF
|
|
end = address + length - 1
|
|
end_hi = (end >> 8) & 0xFF
|
|
end_lo = end & 0xFF
|
|
crc_hi = (crc16 >> 8) & 0xFF
|
|
crc_lo = crc16 & 0xFF
|
|
res = self.send(COMMAND.VERIFY, [start_hi, start_lo] + [end_hi, end_lo] + [crc_hi, crc_lo])
|
|
return res
|
|
|
|
def download(self, filename):
|
|
print("> dumping flash content to '%s'" % filename)
|
|
print("> please note that this will take long")
|
|
|
|
#check for chip
|
|
self.identify_chip()
|
|
|
|
self.debug = False
|
|
|
|
#send autobaud training character
|
|
self.send_autobaud_training()
|
|
|
|
#enable flash access
|
|
self.enable_flash_access()
|
|
|
|
#the bootloader protocol does not allow reading flash
|
|
#however it allows to verify written bytes
|
|
#we will exploit this feature to dump the flash contents
|
|
#for now assume 8kb flash
|
|
flash_size = 8 * 1024
|
|
ih = IntelHex()
|
|
for address in range(flash_size):
|
|
#test one byte by byte
|
|
#first check 0x00
|
|
byte = 0
|
|
if (self.verify(address, [byte]) == RESPONSE.ACK):
|
|
ih[address] = byte
|
|
else:
|
|
#now start with 0xFF (empty flash)
|
|
for byte in range(0xFF, -1, -1):
|
|
if (self.verify(address, [byte]) == RESPONSE.ACK):
|
|
#success, the flash content on this address euals <byte>
|
|
ih[address] = byte
|
|
break
|
|
print("\r> flash[0x%04X] = 0x%02X" % (address, byte), end="")
|
|
sys.stdout.flush()
|
|
|
|
print("\n> finished")
|
|
|
|
#done, all flash contents have been read, now store this to the file
|
|
ih.write_hex_file(filename)
|
|
|
|
|
|
def upload(self, filename):
|
|
print("> uploading file '%s'" % (filename))
|
|
|
|
#identify chip
|
|
self.identify_chip()
|
|
|
|
#read hex file
|
|
ih = IntelHex()
|
|
ih.loadhex(filename)
|
|
|
|
#send autobaud training character
|
|
self.send_autobaud_training()
|
|
|
|
#enable flash access
|
|
self.enable_flash_access()
|
|
|
|
#erase pages where we are going to write
|
|
self.erase_pages_ih(ih)
|
|
|
|
#write all data bytes
|
|
self.write_pages_ih(ih)
|
|
self.verify_pages_ih(ih)
|
|
|
|
def erase_pages_ih(self, ih):
|
|
""" erase all pages that are occupied """
|
|
last_address = ih.addresses()[-1]
|
|
last_page = int(last_address / self.flash_page_size)
|
|
for page in range(last_page+1):
|
|
start = page * self.flash_page_size
|
|
end = start + self.flash_page_size-1
|
|
page_used = False
|
|
for x in ih.addresses():
|
|
if x >= start and x <= end:
|
|
page_used = True
|
|
break
|
|
#always erase page 0 to retain bootloader access
|
|
if (page == 0) or (page_used):
|
|
self.erase_page(page)
|
|
|
|
def write_pages_ih(self, ih):
|
|
""" write all segments from this ihex to flash"""
|
|
#NOTE: it is important to keep flash location 0
|
|
# equal to 0xFF until we are almost finished...
|
|
# therefore the bootloader will still be functional in case
|
|
# something goes wrong in the process.
|
|
# (the bootloader will be executed as long the first flash
|
|
# content equals 0xFF)
|
|
byte_zero = -1
|
|
for start,end in ih.segments():
|
|
print("> writing segment 0x%04X-0x%04X" % (start, end-1))
|
|
|
|
#fetch data
|
|
data = []
|
|
for x in range(start,end):
|
|
data.append(ih[x])
|
|
#write in 128byte blobs
|
|
data_pos = 0
|
|
#keep byte zero 0xFF in order to keep bootloader active (for now)
|
|
if (start == 0):
|
|
print("> delaying write of flash[0] = 0x%02X to the end" % (data[0]))
|
|
byte_zero = data[0]
|
|
start = start + 1
|
|
data.pop(0)
|
|
while ((data_pos + start) < end):
|
|
length = min(128, end - (data_pos + start))
|
|
self.write(start + data_pos, data[data_pos:data_pos+length])
|
|
data_pos = data_pos + length
|
|
|
|
#now verify this segment
|
|
print("> verifying segment... ", end="")
|
|
sys.stdout.flush()
|
|
if (self.verify(start, data) == RESPONSE.ACK):
|
|
print("OK")
|
|
else :
|
|
sys.exit("FAILURE. will abort now\n")
|
|
|
|
#all bytes except byte zero were written, do this now
|
|
if (byte_zero != -1):
|
|
print("> will now write flash[0] = 0x%02X" % (byte_zero))
|
|
res = self.write(0, [byte_zero])
|
|
if (res != RESPONSE.ACK):
|
|
print("> ERROR, write of flash[0] failed (response = %s)" % (RESPONSE.to_string(res)))
|
|
self.restore_bootloader_autostart()
|
|
sys.exit("FAILED")
|
|
#verify
|
|
res = self.verify(0, [byte_zero])
|
|
if (res != RESPONSE.ACK):
|
|
print("> ERROR, verify of flash[0] failed (response = %s)" % (RESPONSE.to_string(res)))
|
|
self.self.restore_bootloader_autostarti()
|
|
sys.exit("FAILED")
|
|
|
|
def restore_bootloader_autostart(self):
|
|
#the bootloader will always start if flash[0] = 0xFF
|
|
#in case something went wrong during programming,
|
|
#call this in order to clear page 0 so that the bootloader
|
|
#will always start
|
|
print("> will now erase page 0 in order to re-enable bootloader autorun");
|
|
self.erase_page(0)
|
|
|
|
def verify_pages_ih(self, ih):
|
|
""" verify written data """
|
|
#do a pagewise compare to find the position of
|
|
#the mismatch
|
|
for start,end in ih.segments():
|
|
print("> verifying segment 0x%04X-0x%04X... " % (start, end-1), end="")
|
|
sys.stdout.flush()
|
|
|
|
#fetch data
|
|
data = []
|
|
for x in range(start,end):
|
|
data.append(ih[x])
|
|
|
|
#calc crc16
|
|
if (self.verify(start, data) == RESPONSE.ACK):
|
|
print("OK")
|
|
else :
|
|
sys.exit("FAILURE. will abort now\n")
|
|
|
|
return 1
|
|
|
|
if __name__ == "__main__":
|
|
argp = argparse.ArgumentParser(description='efm8load - a plain python implementation for the EFM8 usart bootloader protocol')
|
|
|
|
group = argp.add_mutually_exclusive_group()
|
|
group.add_argument("-w", "--write", metavar="filename", help="upload the given hex file to the flash memory")
|
|
group.add_argument("-r", "--read", metavar="filename", help="download the flash memory contents to the given filename") #action="store_true", nargs=1)
|
|
group.add_argument("-i", "--identify", help="identify the chip", action="store_true")
|
|
group.add_argument("-s", "--reset", help="send reset command", action="store_true")
|
|
|
|
#argp.add_argument('filename', help='firmware file to upload to the mcu')
|
|
argp.add_argument('-b', '--baudrate', type=int, default=115200, help='baudrate (default is 115200 baud)')
|
|
argp.add_argument('-p', '--port', default="/dev/ttyUSB0", help='port (default is /dev/ttyUSB0)')
|
|
argp.add_argument('-v', '--verbose', action='store_true', help='Verbose mode')
|
|
args = argp.parse_args()
|
|
|
|
print("########################################")
|
|
print("# efm8load.py - (c) 2020 fishpepper.de #")
|
|
print("########################################")
|
|
print("")
|
|
|
|
efm8loader = EFM8Loader(args.port, args.baudrate, debug=args.verbose)
|
|
|
|
if (args.identify):
|
|
efm8loader.identify_chip()
|
|
elif (args.write):
|
|
efm8loader.upload(args.write)
|
|
elif (args.read):
|
|
efm8loader.download(args.read)
|
|
elif (args.reset):
|
|
efm8loader.send_reset()
|
|
else:
|
|
argp.print_help()
|
|
sys.exit(1)
|
|
|
|
print()
|
|
sys.exit(0)
|