mirror of
https://github.com/esp8266/Arduino.git
synced 2025-04-19 23:22:16 +03:00
Properly handle Popen object, it takes care of everything we need to do like .communicate() and handle timeouts. Introduce a small timeout to run so we also know when that happens Clean-up with pythonic path join and formatting through f"" strings
257 lines
8.4 KiB
Python
Executable File
257 lines
8.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Generate an Arduino compatible BIN file from bootloader and sketch ELF
|
|
# Replaces esptool-ck.exe and emulates its behavior.
|
|
#
|
|
# Copyright (C) 2019 - Earle F. Philhower, III
|
|
#
|
|
# This program 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, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# 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, see <https://www.gnu.org/licenses/>.
|
|
|
|
from __future__ import print_function
|
|
import argparse
|
|
import io
|
|
import re
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
fmodeb = { 'dout': 3, 'dio': 2, 'qout': 1, 'qio': 0 }
|
|
ffreqb = { '40': 0, '26': 1, '20': 2, '80': 15 }
|
|
fsizeb = { '512K': 0, '256K': 1, '1M': 2, '2M': 3, '4M': 4, '8M': 8, '16M': 9 }
|
|
|
|
crcsize_offset = 4096 + 16
|
|
crcval_offset = 4096 + 16 + 4
|
|
|
|
|
|
class Elf2BinException(Exception):
|
|
pass
|
|
|
|
|
|
def get_elf_entry(elf, path):
|
|
result = subprocess.run(
|
|
[os.path.join(path, "xtensa-lx106-elf-readelf"), "-h", elf],
|
|
check=True,
|
|
timeout=15,
|
|
stdout=subprocess.PIPE,
|
|
universal_newlines=True,
|
|
)
|
|
|
|
lines = io.StringIO(result.stdout)
|
|
for line in lines.readlines():
|
|
if 'Entry point address' in line:
|
|
words = re.split('\s+', line)
|
|
entry_point = words[-2]
|
|
return int(entry_point, 16)
|
|
|
|
raise Elf2BinException(f'Unable to find entry point in file "{elf}"')
|
|
|
|
|
|
def get_segment_size_addr(elf, segment, path):
|
|
result = subprocess.run(
|
|
[os.path.join(path, "xtensa-lx106-elf-objdump"), "-h", "-j", segment, elf],
|
|
check=True,
|
|
timeout=15,
|
|
stdout=subprocess.PIPE,
|
|
universal_newlines=True,
|
|
)
|
|
|
|
lines = io.StringIO(result.stdout)
|
|
for line in lines.readlines():
|
|
if segment in line:
|
|
words = re.split('\s+', line)
|
|
size = int(words[3], 16)
|
|
addr = int(words[4], 16)
|
|
return [ size, addr ]
|
|
|
|
raise Elf2BinException(
|
|
f'Unable to find size and start point in file "{elf}" for "{segment}"'
|
|
)
|
|
|
|
|
|
def read_segment(elf, segment, path):
|
|
fd, tmpfile = tempfile.mkstemp()
|
|
os.close(fd)
|
|
try:
|
|
subprocess.check_call(
|
|
[
|
|
os.path.join(path, "xtensa-lx106-elf-objcopy"),
|
|
"-O",
|
|
"binary",
|
|
f"--only-section={segment}",
|
|
elf,
|
|
tmpfile,
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
with open(tmpfile, "rb") as f:
|
|
raw = f.read()
|
|
finally:
|
|
os.remove(tmpfile)
|
|
|
|
return raw
|
|
|
|
|
|
def write_bin(out, args, elf, segments, to_addr):
|
|
entry = int(get_elf_entry( elf, args.path ))
|
|
header = [ 0xe9, len(segments), fmodeb[args.flash_mode], ffreqb[args.flash_freq] + 16 * fsizeb[args.flash_size],
|
|
entry & 255, (entry>>8) & 255, (entry>>16) & 255, (entry>>24) & 255 ]
|
|
out.write(bytearray(header))
|
|
total_size = 8
|
|
checksum = 0xef
|
|
for segment in segments:
|
|
[size, addr] = get_segment_size_addr(elf, segment, args.path)
|
|
seghdr = [ addr & 255, (addr>>8) & 255, (addr>>16) & 255, (addr>>24) & 255,
|
|
size & 255, (size>>8) & 255, (size>>16) & 255, (size>>24) & 255]
|
|
out.write(bytearray(seghdr));
|
|
total_size += 8;
|
|
raw = read_segment(elf, segment, args.path)
|
|
if len(raw) != size:
|
|
raise Exception('Segment size doesn\'t match read data for "' + segment + '" in "' + elf + '"')
|
|
out.write(raw)
|
|
total_size += len(raw)
|
|
try:
|
|
for data in raw:
|
|
checksum = checksum ^ ord(data)
|
|
except:
|
|
for data in raw:
|
|
checksum = checksum ^ data
|
|
total_size += 1
|
|
while total_size & 15:
|
|
total_size += 1
|
|
out.write(bytearray([0]))
|
|
out.write(bytearray([checksum]))
|
|
if to_addr != 0:
|
|
if total_size + 8 > to_addr:
|
|
raise Elf2BinException(
|
|
f'Bin image of "{elf}" is too big! Actual size {str(total_size + 8)}, target size {str(to_addr)}'
|
|
)
|
|
while total_size < to_addr:
|
|
out.write(bytearray([0xaa]))
|
|
total_size += 1
|
|
|
|
|
|
def crc8266(ldata):
|
|
"Return the CRC of ldata using same algorithm as eboot"
|
|
crc = 0xffffffff
|
|
idx = 0
|
|
while idx < len(ldata):
|
|
byte = int(ldata[idx])
|
|
idx = idx + 1
|
|
for i in [0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01]:
|
|
bit = crc & 0x80000000
|
|
if (byte & i) != 0:
|
|
if bit == 0:
|
|
bit = 1
|
|
else:
|
|
bit = 0
|
|
crc = int(crc << 1) & 0xffffffff
|
|
if bit != 0:
|
|
crc = int(crc ^ 0x04c11db7)
|
|
return crc
|
|
|
|
|
|
def store_word(raw, offset, val):
|
|
"Place a 4-byte word in 8266-dependent order in the raw image"
|
|
raw[offset] = val & 255
|
|
raw[offset + 1] = (val >> 8) & 255
|
|
raw[offset + 2] = (val >> 16) & 255
|
|
raw[offset + 3] = (val >> 24) & 255
|
|
return raw
|
|
|
|
|
|
def add_crc(out):
|
|
with open(out, "rb") as binfile:
|
|
raw = bytearray(binfile.read())
|
|
|
|
# Zero out the spots we're going to overwrite to be idempotent
|
|
raw = store_word(raw, crcsize_offset, 0)
|
|
raw = store_word(raw, crcval_offset, 0)
|
|
crc = crc8266(raw)
|
|
raw = store_word(raw, crcsize_offset, len(raw))
|
|
raw = store_word(raw, crcval_offset, int(crc))
|
|
|
|
with open(out, "wb") as binfile:
|
|
binfile.write(raw)
|
|
|
|
|
|
def gzip_bin(mode, out):
|
|
import gzip
|
|
|
|
firmware_path = out
|
|
gzip_path = f"{firmware_path}.gz"
|
|
orig_path = f"{firmware_path}.orig"
|
|
if os.path.exists(gzip_path):
|
|
os.remove(gzip_path)
|
|
print(f'GZipping firmware {firmware_path}')
|
|
with open(firmware_path, 'rb') as firmware_file, \
|
|
gzip.open(gzip_path, 'wb') as dest:
|
|
data = firmware_file.read()
|
|
dest.write(data)
|
|
orig_size = os.stat(firmware_path).st_size
|
|
gzip_size = os.stat(gzip_path).st_size
|
|
print("New FW size {:d} bytes vs old {:d} bytes".format(
|
|
gzip_size, orig_size))
|
|
|
|
if mode == "PIO":
|
|
if os.path.exists(orig_path):
|
|
os.remove(orig_path)
|
|
print(f'Moving original firmware to {orig_path}')
|
|
os.rename(firmware_path, orig_path)
|
|
os.rename(gzip_path, firmware_path)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Create a BIN file from eboot.elf and Arduino sketch.elf for upload by esptool.py')
|
|
parser.add_argument('-e', '--eboot', action='store', required=True, help='Path to the Arduino eboot.elf bootloader')
|
|
parser.add_argument('-a', '--app', action='store', required=True, help='Path to the Arduino sketch ELF')
|
|
parser.add_argument('-m', '--flash_mode', action='store', required=True, choices=['dout', 'dio', 'qout', 'qio'], help='SPI flash mode')
|
|
parser.add_argument('-f', '--flash_freq', action='store', required=True, choices=['20', '26', '40', '80'], help='SPI flash speed')
|
|
parser.add_argument('-s', '--flash_size', action='store', required=True, choices=['256K', '512K', '1M', '2M', '4M', '8M', '16M'], help='SPI flash size')
|
|
parser.add_argument('-o', '--out', action='store', required=True, help='Output BIN filename')
|
|
parser.add_argument('-p', '--path', action='store', required=True, help='Path to Xtensa toolchain binaries')
|
|
parser.add_argument('-g', '--gzip', choices=['PIO', 'Arduino'], help='PIO - generate gzipped BIN file, Arduino - generate BIN and BIN.gz')
|
|
|
|
args = parser.parse_args()
|
|
|
|
print(f'Creating BIN file "{args.out}" using "{args.eboot}" and "{args.app}"')
|
|
|
|
with open(args.out, "wb") as out:
|
|
def wrapper(**kwargs):
|
|
write_bin(out=out, args=args, **kwargs)
|
|
|
|
wrapper(
|
|
elf=args.eboot,
|
|
segments=[".text", ".rodata"],
|
|
to_addr=4096
|
|
)
|
|
|
|
wrapper(
|
|
elf=args.app,
|
|
segments=[".irom0.text", ".text", ".text1", ".data", ".rodata"],
|
|
to_addr=0
|
|
)
|
|
|
|
# Because the CRC includes both eboot and app, can only calculate it after the entire BIN generated
|
|
add_crc(args.out)
|
|
|
|
if args.gzip:
|
|
gzip_bin(args.gzip, args.out)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|