diff --git a/srec2bin.py b/bin/srec2bin.py similarity index 64% rename from srec2bin.py rename to bin/srec2bin.py index 338b1c8..86a9fff 100644 --- a/srec2bin.py +++ b/bin/srec2bin.py @@ -13,19 +13,24 @@ # #----------------------------------------------------------------------------- +import argparse +import logging import os import sys -import argparse -from srecord import * + +from srec2bin import * #-- definitions -------------------------------------------------------------- SCRIPT_VERSION = '0.90' DEFAULT_OUT_FILE_EXT = '.bin' DEFAULT_OUT_FILE_NAME = 'out' + DEFAULT_OUT_FILE_EXT - #----------------------------------------------------------------------------- + +log = logging.getLogger(__name__) + + def _mkofn(out_fn, in_fn): """ if output file name is not specified, derive from input file name """ @@ -39,6 +44,7 @@ def _mkofn(out_fn, in_fn): #----------------------------------------------------------------------------- + def _auto_int(x): return int(x, 0) @@ -46,6 +52,7 @@ def _auto_int(x): # main program #----------------------------------------------------------------------------- + if __name__ == "__main__": parser = argparse.ArgumentParser( @@ -81,55 +88,28 @@ def _auto_int(x): args = parser.parse_args() fill_byte = (args.fill_byte % 0x100).to_bytes(1, 'big') - target_memory = bytearray(0x10000 * fill_byte) - target_memmap = bytearray(0x10000 * b'\xff') if args.verbose: - print("Start address: 0x{0:04x}".format(args.start_addr)) - print("End address: 0x{0:04x}".format(args.end_addr)) - print("Filling with: 0x{0:02x}".format(fill_byte[0])) - - srecs = [] - line_no = 1 - byte_cnt = 0 - for srec_line in args.srec_file: - srec = SRecord() - if not srec.process(srec_line): - if srec.type == "S1": - srecs.append(srec) - else: - if args.verbose: - print("Skipping", srec.type if srec.type!="" else "empty", "line") - else: - print("Error in", args.srec_file.name, "Line", str(line_no) + ":") - # to display the offending line we better use bytes type here in case - # the input is (mistakenly) binary stuff instead of a text file - print(srec_line.rstrip().encode('ASCII', 'ignore')) - print(srec.error) - print('Program terminated.') - sys.exit(1) - line_no += 1 + log.setLevel(logging.DEBUG) - if args.verbose: - print("S-Record lines processed: {0:d}".format(line_no - 1)) - - for srec in srecs: - addr = srec.addr - for b in srec.data: - if target_memmap[addr] != 0: - target_memmap[addr] = 0 - target_memory[addr] = b - byte_cnt += 1 - else: - print("Error: duplicate access to addr", "0x%04x" % addr) - addr += 1 + log.debug("Start address: 0x{0:04x}".format(args.start_addr) + + "End address: 0x{0:04x}".format(args.end_addr) + + "Filling with: 0x{0:02x}".format(fill_byte[0])) - if args.verbose: - print("Total bytes processed: {0:d}".format(byte_cnt)) + try: + srecords = load(args.srec_file) + except ParseException as e: + log.error("Error in {}:".format(args.srec_file.name)) + log.error(e) + log.error('Program terminated.') + sys.exit(1) + + target_memory, byte_cnt = to_binary(srecords, fill_byte) + log.debug("Total bytes processed: {0:d}".format(byte_cnt)) out_file_name = _mkofn(args.out_file, args.srec_file.name) - if args.verbose: - print("Writing to output file", out_file_name) + log.debug("Writing to output file", out_file_name) + with open(out_file_name, 'wb') as outfile: outfile.write(target_memory[args.start_addr:args.end_addr]) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..bb55c5f --- /dev/null +++ b/setup.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import codecs +import os +import re + +from setuptools import find_packages, setup + +NAME = 'srec2bin' +PACKAGES = find_packages(where='.') +META_PATH = os.path.join('srec2bin', '__init__.py') +KEYWORDS = ['motorola', 'srecord', 'S3'] +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'Environment :: Other Environment', + 'Natural Language :: English', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: POSIX', + 'Operating System :: Win32', + 'Programming Language :: Python :: 3.5', + 'Topic :: Software Development :: Libraries :: Python Modules', +] +INSTALL_REQUIRES = [ +] + +HERE = os.path.abspath(os.path.dirname(__file__)) + + +def read(*parts): + """ + Build an absolute path from *parts* and and return the contents of the + resulting file. Assume UTF-8 encoding. + """ + with codecs.open(os.path.join(HERE, *parts), 'rb', 'utf-8') as f: + return f.read() + + +META_FILE = read(META_PATH) + + +def find_meta(meta): + """ + Extract __*meta*__ from META_FILE. + """ + meta_match = re.search( + r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), + META_FILE, re.M + ) + if meta_match: + return meta_match.group(1) + raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) + + +if __name__ == '__main__': + setup( + name=NAME, + description=find_meta('description'), + license=find_meta('license'), + url=find_meta('uri'), + version=find_meta('version'), + author=find_meta('author'), + author_email=find_meta('email'), + maintainer=find_meta('author'), + maintainer_email=find_meta('email'), + keywords=KEYWORDS, + long_description=read('README.md'), + packages=PACKAGES, + # package_dir={'srec2bin': 'srec2bin'}, + # package_data={'srec2bin': ['*.rst']}, + scripts=['bin/srec2bin.py'], + classifiers=CLASSIFIERS, + install_requires=INSTALL_REQUIRES, + python_requires='>=3.5', + ) diff --git a/srec2bin/__init__.py b/srec2bin/__init__.py new file mode 100644 index 0000000..76ebdca --- /dev/null +++ b/srec2bin/__init__.py @@ -0,0 +1,11 @@ +__version__ = '1.0' +__title__ = 'srec2bin' +__description__ = 'This utility converts binary data from Motorola S-Record file format [1] to raw binary.' +__author__ = 'Oliver Thamm' +__email__ = 'support@elmicro.com' +__uri__ = 'https://github.com/mrbell321/srec2bin_py' +__doc__ = __description__ + ' <' + __uri__ + '>' +__license__ = 'MIT' + +from .srecord import SRecord, Purpose, Type +from .srec import to_image, to_pages, load \ No newline at end of file diff --git a/srec2bin/srec.py b/srec2bin/srec.py new file mode 100644 index 0000000..2cf09a3 --- /dev/null +++ b/srec2bin/srec.py @@ -0,0 +1,79 @@ +import logging +from typing import Generator, Iterable, Tuple, Optional + +from .srecord import SRecord, Purpose + +log = logging.getLogger(__name__) +log.setLevel(logging.INFO) + +MAX_PAGE_SIZE = 0x100000 + + +def load(filename: str): + if isinstance(filename, str): + with open(filename, 'r') as f: + srecords = (SRecord(line) for line in f.readlines()) + else: + srecords = (SRecord(line) for line in filename) + + return srecords + + +def to_image(srecords: Iterable[SRecord], fill_byte: bytes = b'\xff', offset: int = 0) -> Tuple[bytearray, int]: + log.debug("Converting SRecords to binary, fill: 0x{}, offset: 0x{:x}".format(fill_byte.hex(), offset)) + + target_memory = bytearray(fill_byte * 0x100000) + target_memmap = bytearray(0x100000 * b'\xff') + byte_cnt = 0 + high_address = 0 + for srecord in srecords: + if srecord.purpose == Purpose.Data: + addr = srecord.address - offset + log.debug("Processing SRecord: {} @ 0x{:08x}".format(srecord, addr)) + for b in srecord.data: + if target_memmap[addr] == 0: + log.debug("Non critical error: duplicate access to address: 0x{:04X}; {}".format(addr, srecord)) + else: + byte_cnt += 1 + target_memmap[addr] = 0 + target_memory[addr] = b + addr += 1 + if addr > high_address: + high_address = addr + else: + log.debug("Skipping non-data line") + + return target_memory[:high_address], byte_cnt + + +def to_pages(srecords: Iterable[SRecord], page_size: Optional[int] = None) -> Generator[ + Tuple[int, bytearray], None, None]: + page_size = page_size or MAX_PAGE_SIZE + log.debug("Converting SRecords to binary, page_size: 0x{:x}".format(page_size)) + + page = bytearray() + + page_address = -1 + for srecord in srecords: + if srecord.purpose == Purpose.Data: + log.debug("Processing SRecord: {}".format(srecord)) + if page_address < 0: + page_address = srecord.address + + elif srecord.address < page_address or srecord.address >= (page_address + page_size): + log.debug("Switching from page at 0x{:08X} to 0x{:08X}".format(page_address, srecord.address)) + yield page_address, page + page = bytearray() + page_address = srecord.address + + if len(page) + len(srecord.data) >= page_size: + remaining = page_size - len(page) + page[len(page):] = srecord.data[:remaining] + yield page_address, page + page = bytearray(srecord.data[remaining:]) + page_address = srecord.address + remaining + else: + page[len(page):] = srecord.data[:] + + if page: + yield page_address, page diff --git a/srec2bin/srecord.py b/srec2bin/srecord.py new file mode 100644 index 0000000..36bd64f --- /dev/null +++ b/srec2bin/srecord.py @@ -0,0 +1,136 @@ +from enum import Enum +from typing import AnyStr + + +# ----------------------------------------------------------------------------- +# +# srecord.py - Handling Module for Motorola S-Records +# by Oliver Thamm - Elektronikladen Microcomputer +# https://github.com/elmicro/srec2bin_py +# +# This software is Copyright (C)2017 by ELMICRO - https://elmicro.com +# and may be freely used, modified and distributed under the terms of +# the MIT License - see accompanying LICENSE.md for details +# +# ----------------------------------------------------------------------------- + + +class ParseException(Exception): + pass + + +class Purpose(Enum): + Header = 0 + Data = 1 + Count = 2 + StartAddress = 3 + + +class Type(Enum): + S0 = 0 + S1 = 1 + S2 = 2 + S3 = 3 + S5 = 5 + S6 = 6 + S7 = 7 + S8 = 8 + S9 = 9 + + +TypePurpose = { + Type.S0: Purpose.Header, + Type.S1: Purpose.Data, + Type.S2: Purpose.Data, + Type.S3: Purpose.Data, + Type.S5: Purpose.Count, + Type.S6: Purpose.Count, + Type.S7: Purpose.StartAddress, + Type.S8: Purpose.StartAddress, + Type.S9: Purpose.StartAddress, +} + +TypeAddressSize = { + Type.S0: 2, + Type.S1: 2, + Type.S2: 3, + Type.S3: 4, + Type.S5: 2, + Type.S6: 3, + Type.S7: 4, + Type.S8: 3, + Type.S9: 2 +} + + +class SRecord: + """ + An SRecord is an ASCII string of the following structure: + S | Type | Count | Address | Data | Checksum + S : The character 'S'(0x53) + Type : a numeric digit defining the type of the record + Count : two hex digits indicating the number of bytes in the rest of the record + Address : 4, 6 or 8 hex digits depending on record type in big endian + Data : a sequence of 2n hex digits for n bytes of data + Checksum : two hex digits, the least significant byte of ones' compliment of the sum of values of the count, + address and data fields + """ + + def __init__(self, source: AnyStr): + self.type: Type = None + self.byte_count: int = 0 + self.address: int = 0 + self.data: bytes = None + self.checksum: int = None + self.__process(source) + + def __repr__(self): + address_formatter = "{{:0{}X}}".format(self.address_length * 2) + formatter = "{} {:02x} " + address_formatter + " {} {:02X}" + return formatter.format(self.type.name, self.byte_count, self.address, self.data.hex().upper(), self.checksum) + + def __process(self, source: AnyStr): + """ determine type and starting address of a single S-Record line, + extract data bytes, verify syntax and checksum, ignore any + blank lines, return an empty string "" on success, + otherwise a string containing the error description + """ + # determine length of input + source = source.rstrip() + # gracefully ignore an empty line + if len(source) == 0: + return + + assert 2 < len(source), ParseException("Truncated S-record: " + source) + + try: + self.type = Type[source[:2]] + except KeyError: + ParseException("Incorrect S-record type specification: " + source[:2]) + + try: + self.byte_count = int(source[2:4], 16) + assert (self.byte_count << 1) == (len(source[4:])), \ + ParseException("Incorrect byte count(actual: {}; specified: {})".format( + len(source[4:]) >> 1, + self.byte_count)) + + self.address = int(source[4:4 + (self.address_length << 1)], 16) + self.data = bytes.fromhex(source[4 + (self.address_length << 1):2 + (self.byte_count << 1)]) + self.checksum = int(source[-2:], 16) + except Exception as e: + raise ParseException("Invalid characters in S-record") from e + + calculated_checksum = (sum(int(source[i:i + 2], 16) for i in range(2, len(source) - 2, 2)) & 0xFF) ^ 0xFF + assert self.checksum == calculated_checksum, \ + ParseException("Incorrect checksum: {} != {}".format(self.checksum, calculated_checksum)) + + @property + def address_length(self): + return TypeAddressSize[self.type] + + @property + def purpose(self): + return TypePurpose[self.type] + +# ----------------------------------------------------------------------------- diff --git a/srecord.py b/srecord.py deleted file mode 100644 index 10abf49..0000000 --- a/srecord.py +++ /dev/null @@ -1,81 +0,0 @@ -#----------------------------------------------------------------------------- -# -# srecord.py - Handling Module for Motorola S-Records -# by Oliver Thamm - Elektronikladen Microcomputer -# https://github.com/elmicro/srec2bin_py -# -# This software is Copyright (C)2017 by ELMICRO - https://elmicro.com -# and may be freely used, modified and distributed under the terms of -# the MIT License - see accompanying LICENSE.md for details -# -#----------------------------------------------------------------------------- - -class SRecord: - - _type2asiz = {"S0":2, "S1":2, "S2":3, "S3":4, "S5":2, "S6":3, "S7":4, "S8":3, "S9":2} - - def __init__(self): - self.type = "" - self.addr = 0 - self.data = bytearray() - self.error = "" - - def process(self, srecline): - """ determine type and starting address of a single S-Record line, - extract data bytes, verify syntax and checksum, ignore any - blank lines, return an empty string "" on success, - otherwise a string containing the error description - """ - self.type = "" - self.addr = 0 - self.data = bytearray() - - # determine length of input - self.line = srecline.rstrip() - self.line_len = len(self.line) - # gracefully ignore an empty line - if self.line_len == 0: - self.error = "" - return self.error - - # check size limits and test for even number of chars - if self.line_len < 10 or self.line_len > 514 or self.line_len % 2: - self.error = "s-record has wrong size" - return self.error - - self.type = self.line[:2] - self.addr_siz = self._type2asiz.get(self.type, 0) - if not self.addr_siz: - self.error = "s-record type unknown" - return self.error - - # construe char pairs as hex values - try: - self.values = bytearray([int(self.line[i:i+2], 16) for i in range(2,self.line_len, 2)]) - except ValueError: - self.error = "s-record contains invalid character(s)" - return self.error - - # check byte count field vs. number of char pairs - if self.values[0] != len(self.values)-1: - self.error = "s-record byte count mismatch" - return self.error - - # determine starting address - self.addr = self.values[1] * 0x100 + self.values[2] - if self.addr_siz > 2: - self.addr = self.addr * 0x100 + self.values[3] - if self.addr_siz > 3: - self.addr = self.addr * 0x100 + self.values[4] - - # verify check sum - if sum(self.values, 1) % 0x100: - self.error = "s-record checksum error" - return self.error - - # deliver the data bytes - self.data = self.values[self.addr_siz+1:-1] - self.error = "" - return self.error - -#-----------------------------------------------------------------------------