diff --git a/README.md b/README.md index a0c0abf..5683a5e 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,16 @@ stcgal - STC MCU ISP flash tool stcgal is a command line flash programming tool for STC MCU Ltd. [1] 8051 compatible microcontrollers. The name was inspired by avrdude [2]. -STC microcontrollers have a UART based boot strap loader (BSL). It +STC microcontrollers have an UART/USB based boot strap loader (BSL). It utilizes a packet-based protocol to flash the code memory and IAP -memory over a serial link. This is referred to as in-system programming (ISP). -The BSL is also used to configure various (fuse-like) device +memory over a serial link. This is referred to as in-system programming +(ISP). The BSL is also used to configure various (fuse-like) device options. Unfortunately, this protocol is not publicly documented and STC only provide a (crude) Windows GUI application for programming. -stcgal is a full-featured Open Source replacement for STC's Windows software; -it supports a wide range of MCUs, it is very portable and suitable for automation. +stcgal is a full-featured Open Source replacement for STC's Windows +software; it supports a wide range of MCUs, it is very portable and +suitable for automation. [1] http://stcmcu.com/ [2] http://www.nongnu.org/avrdude/ @@ -43,13 +44,14 @@ So far, stcgal was tested with the following MCU models: * IAP15F2K61S2 (BSL version: 7.1.4S) * STC15L2K16S2 (BSL version: 7.2.4S) * STC15W408AS (BSL version: 7.2.4T) -* STC15W4K56S4 (BSL version: 7.3.4T) +* STC15W4K56S4 (BSL version: 7.3.4T, UART and USB mode) Compatibility reports, both negative and positive, are welcome. Features -------- +* UART and USB BSL support * Display part info * Determine operating frequency * Program flash memory @@ -58,12 +60,13 @@ Features * Read unique device ID (STC 10/11/12/15) * Trim RC oscillator frequency (STC 15) * Automatic power-cycling with DTR toggle -* Automatic protocol detection +* Automatic UART protocol detection Installation ------------ -stcgal requires Python 3.2 (or later) and pySerial. You can run stcgal +stcgal requires Python 3.2 (or later) and pySerial. USB support is +optional and requires pyusb 1.0.0b2 or later. You can run stcgal directly with the included ```stcgal.py``` script. The recommended method for permanent installation is to use Python's setuptools. Run ```./setup.py build``` to build and ```sudo ./setup.py install``` @@ -117,7 +120,8 @@ protocols and MCU series is as follows: * ```stc12``` Most STC10/11/12 series (default) * ```stc15a``` STC15x104E and STC15x204E(A) series * ```stc15``` Most STC15 series -* ```auto``` Automatic detection +* ```usb15``` USB support on STC15W4 series +* ```auto``` Automatic detection of UART based protocols The text files in the doc/ subdirectory provide an overview over the reverse engineered protocols used by the BSLs. For more details, @@ -276,6 +280,14 @@ error, such as a protocol error or I/O error, results in an exit status of 1. If the the user aborted stcgal by pressing CTRL-C, that results in an exit status of 2. +### USB support + +STC15W4 series have an USB-based BSL that can be optionally +used. USB support in stcgal is experimental and might change in the +future. USB mode is enabled by using the ```usb15``` protocol. The +port (```-p```) flag as well as the baudrate options are ignored for +the USB protocol. RC frequency trimming is not supported. + License ------- diff --git a/setup.py b/setup.py index 3a03154..4942884 100755 --- a/setup.py +++ b/setup.py @@ -29,6 +29,9 @@ setup( version = stcgal.__version__, packages = find_packages(exclude=["doc"]), install_requires = ["pyserial"], + extras_require = { + "usb": ["pyusb>=1.0.0"] + }, entry_points = { "console_scripts": [ "stcgal = stcgal.frontend:cli", diff --git a/stcgal/frontend.py b/stcgal/frontend.py index 9ac3d7a..c16b6d9 100644 --- a/stcgal/frontend.py +++ b/stcgal/frontend.py @@ -45,6 +45,8 @@ class StcGal: elif opts.protocol == "stc15": self.protocol = Stc15Protocol(opts.port, opts.handshake, opts.baud, round(opts.trim * 1000)) + elif opts.protocol == "usb15": + self.protocol = StcUsb15Protocol() else: self.protocol = StcBaseProtocol(opts.port, opts.handshake, opts.baud) @@ -192,7 +194,7 @@ def cli(): parser.add_argument("code_image", help="code segment file to flash (BIN/HEX)", type=argparse.FileType("rb"), nargs='?') parser.add_argument("eeprom_image", help="eeprom segment file to flash (BIN/HEX)", type=argparse.FileType("rb"), nargs='?') parser.add_argument("-a", "--autoreset", help="cycle power automatically by asserting DTR", action="store_true") - parser.add_argument("-P", "--protocol", help="protocol version", choices=["stc89", "stc12a", "stc12", "stc15a", "stc15", "auto"], default="stc12") + parser.add_argument("-P", "--protocol", help="protocol version", choices=["stc89", "stc12a", "stc12", "stc15a", "stc15", "usb15", "auto"], default="stc12") parser.add_argument("-p", "--port", help="serial port device", default="/dev/ttyUSB0") parser.add_argument("-b", "--baud", help="transfer baud rate (default: 19200)", type=BaudType(), default=19200) parser.add_argument("-l", "--handshake", help="handshake baud rate (default: 2400)", type=BaudType(), default=2400) diff --git a/stcgal/protocols.py b/stcgal/protocols.py index e3a8bf7..925d3d7 100644 --- a/stcgal/protocols.py +++ b/stcgal/protocols.py @@ -26,6 +26,14 @@ import argparse import collections from stcgal.models import MCUModelDatabase from stcgal.utils import Utils +import functools + +try: + import usb.core, usb.util + _usb_available = True +except ImportError: + _usb_available = False + class StcFramingException(Exception): """Something wrong with packet framing or checksum""" @@ -1937,15 +1945,12 @@ class Stc15Protocol(Stc15AProtocol): raise StcProtocolException("incorrect magic in finish packet") print("done") - def program_options(self): - print("Setting options: ", end="") - sys.stdout.flush() - msr = self.options.get_msr() + def build_options(self): + """Build a 64 byte packet of option data from the current + configuration.""" - packet = bytes([0x04, 0x00, 0x00]) - if self.bsl_version >= 0x72: - packet += bytes([0x5a, 0xa5]) - packet += bytes([0xff] * 23) + msr = self.options.get_msr() + packet = bytes([0xff] * 23) packet += bytes([(self.trim_frequency >> 24) & 0xff, 0xff, (self.trim_frequency >> 16) & 0xff, @@ -1963,6 +1968,16 @@ class Stc15Protocol(Stc15AProtocol): packet += bytes([0xff] * 3) packet += bytes([self.trim_value[0], self.trim_value[1] + 0x3f]) packet += msr[0:3] + return packet + + def program_options(self): + print("Setting options: ", end="") + sys.stdout.flush() + + packet = bytes([0x04, 0x00, 0x00]) + if self.bsl_version >= 0x72: + packet += bytes([0x5a, 0xa5]) + packet += self.build_options() self.write_packet(packet) response = self.read_packet() if response[0] != 0x04 or response[1] != 0x54: @@ -1970,3 +1985,167 @@ class Stc15Protocol(Stc15AProtocol): print("done") print("Target UID: %s" % Utils.hexstr(self.uid)) + + +class StcUsb15Protocol(Stc15Protocol): + """USB should use large blocks""" + PROGRAM_BLOCKSIZE = 128 + + """VID of STC devices""" + USB_VID = 0x5354 + + """PID of STC devices""" + USB_PID = 0x4312 + + """Control transfer from host to device""" + USB_HOST2DEV = usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE | usb.util.CTRL_OUT + + """Control transfer from device to host""" + USB_DEV2HOST = usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE | usb.util.CTRL_IN + + def __init__(self): + # XXX: this is really ugly! + Stc15Protocol.__init__(self, "", 0, 0, 0) + self.dev = None + + def dump_packet(self, data, request=0, value=0, index=0, receive=True): + if self.debug: + print("%s bRequest=%02X wValue=%04X wIndex=%04X data: %s" % (("<-" if receive else "->"), + request, value, index, Utils.hexstr(data, " ")), file=sys.stderr) + + def read_packet(self): + """Read a packet from the MCU""" + + packet = self.dev.ctrl_transfer(self.USB_DEV2HOST, 0, 0, 0, 132).tobytes() + if len(packet) < 5 or packet[0] != 0x46 or packet[1] != 0xb9: + self.dump_packet(packet) + raise StcFramingException("incorrect frame start") + + data_len = packet[2] + if (data_len) > len(packet) + 3: + self.dump_packet(packet) + raise StcFramingException("frame length mismatch") + + data = packet[2:-1] + csum = functools.reduce(lambda x, y: x - y, data, 0) & 0xff + if csum != packet[-1]: + self.dump_packet(packet) + raise StcFramingException("frame checksum mismatch") + + self.dump_packet(packet, receive=True) + return packet[3:3+data_len] + + def write_packet(self, request, value=0, index=0, data=bytes([0])): + """Write USB control packet""" + + # Control transfers are maximum of 8 bytes each, and every + # invidual partial transfer is checksummed individually. + i = 0 + chunks = bytes() + while i < len(data): + c = data[i:i+7] + csum = functools.reduce(lambda x, y: x - y, c, 0) & 0xff + chunks += c + bytes([csum]) + i += 7 + + self.dump_packet(chunks, request, value, index, receive=False) + self.dev.ctrl_transfer(self.USB_HOST2DEV, request, value, index, chunks); + + def connect(self, autoreset=False): + """Connect to USB device and read info packet""" + + # USB support is optional. Provide an error if pyusb is not available. + if _usb_available == False: + raise StcProtocolException("USB support not available. " + + "pyusb is not installed or not working correctly.") + + print("Waiting for MCU, please cycle power: ", end="") + sys.stdout.flush() + + self.status_packet = None + while not self.status_packet: + try: + self.dev = usb.core.find(idVendor=self.USB_VID, idProduct=self.USB_PID) + if self.dev: + self.dev.set_configuration() + self.status_packet = self.read_packet() + else: + time.sleep(0.5) + except (StcFramingException, usb.core.USBError): pass + + self.initialize_model() + print("done") + + def handshake(self): + print("Initializing: ", end="") + sys.stdout.flush() + + # handshake + self.write_packet(0x01, 0, 0, bytes([0x03])) + response = self.read_packet() + if response[0] != 0x01: + raise StcProtocolException("incorrect magic in handshake packet") + + # enable/unlock MCU + self.write_packet(0x05, 0xa55a, 0) + response = self.read_packet() + if response[0] == 0x0f: + raise StcProtocolException("MCU is locked") + if response[0] != 0x05: + raise StcProtocolException("incorrect magic in handshake packet") + + print("done") + + def erase_flash(self, code, eeprom): + print("Erasing flash: ", end="") + sys.stdout.flush() + self.write_packet(0x03, 0xa55a, 0) + # XXX: better way to detect MCU has finished + time.sleep(2) + packet = self.read_packet() + if packet[0] != 0x03: + raise StcProtocolException("incorrect magic in erase packet") + self.uid = packet[1:8] + print("done") + + def program_flash(self, data): + """Program the MCU's flash memory.""" + + print("Writing %d bytes: " % len(data), end="") + sys.stdout.flush() + for i in range(0, len(data), self.PROGRAM_BLOCKSIZE): + packet = data[i:i+self.PROGRAM_BLOCKSIZE] + while len(packet) < self.PROGRAM_BLOCKSIZE: packet += b"\x00" + self.write_packet(0x22 if i == 0 else 0x02, 0xa55a, i, packet) + # XXX: better way to detect MCU has finished + time.sleep(0.1) + response = self.read_packet() + if response[0] != 0x02 or response[1] != 0x54: + raise StcProtocolException("incorrect magic in write packet") + print(".", end="") + sys.stdout.flush() + print(" done") + + def program_options(self): + print("Setting options: ", end="") + sys.stdout.flush() + + # always use 24 MHz pre-tuned value for now + self.trim_value = (self.freq_count_24, 0x40) + self.trim_frequency = int(24E6) + + packet = self.build_options() + self.write_packet(0x04, 0xa55a, 0, packet) + # XXX: better way to detect MCU has finished + time.sleep(0.5) + response = self.read_packet() + if response[0] != 0x04 or response[1] != 0x54: + raise StcProtocolException("incorrect magic in option packet") + print("done") + + print("Target UID: %s" % Utils.hexstr(self.uid)) + + def disconnect(self): + if self.dev: + self.write_packet(0xff) + print("Disconnected!")