Mathias Rasmussen
4 years ago
2 changed files with 456 additions and 2 deletions
-
4Makefile
-
454tools/efm8load.py
@ -0,0 +1,454 @@ |
|||||
|
#!/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) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue