diff --git a/.gitignore b/.gitignore index bee8a64..79a5411 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ __pycache__ +.idea +venv \ No newline at end of file diff --git a/README.md b/README.md index 54caaa2..1b98b82 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,23 @@ Goal of this project is to bring more flexible support for different MCUs, very - allow to control breakpoints or watchpoints - support for more ST-Link devices connected at once - other file formats (SREC, HEX, ELF, ...) -- pip installer +- ~~pip installer~~ - proxy to GDB - and maybe GUI - support for ST-Link/V1 is NOT planed, use ST-Link/V2 or V2-1 instead + +## Usage + +```python +stlink = StlinkWrapper() +stlink\ + .reset()\ + .flash_erase(erase=True, verify=True, file='../firmware.bin', addr='0x8000000')\ + .dispatch(verbosity=2) +``` + + ## Install ### Requirements diff --git a/lib/dbg.py b/lib/dbg.py deleted file mode 100644 index 8032795..0000000 --- a/lib/dbg.py +++ /dev/null @@ -1,91 +0,0 @@ -import sys -import time - - -class Dbg(): - def __init__(self, verbose, bar_length=40): - self._verbose = verbose - self._bargraph_msg = None - self._bargraph_min = None - self._bargraph_max = None - self._newline = True - self._bar_length = bar_length - self._prev_percent = None - self._start_time = None - - def _msg(self, msg, level): - if self._verbose >= level: - if not self._newline: - sys.stderr.write('\n') - self._newline = True - sys.stderr.write('%s\n' % msg) - sys.stderr.flush() - - def debug(self, msg, level=3): - self._msg(msg, level) - - def verbose(self, msg, level=2): - self._msg(msg, level) - - def info(self, msg, level=1): - self._msg(msg, level) - - def message(self, msg, level=0): - self._msg(msg, level) - - def error(self, msg, level=0): - self._msg('*** %s ***' % msg, level) - - def warning(self, msg, level=0): - self._msg(' * %s' % msg, level) - - def print_bargraph(self, percent): - if percent == self._prev_percent: - return - bar = int(percent * self._bar_length) // 100 - sys.stderr.write('\r%s: [%s%s] %3d%%' % ( - self._bargraph_msg, - '=' * bar, - ' ' * (self._bar_length - bar), - percent, - )) - sys.stderr.flush() - self._prev_percent = percent - self._newline = False - - def bargraph_start(self, msg, value_min=0, value_max=100, level=1): - self._start_time = time.time() - if self._verbose < level: - return - self._bargraph_msg = msg - self._bargraph_min = value_min - self._bargraph_max = value_max - if not self._newline: - sys.stderr.write('\n') - self._newline = False - sys.stderr.write('%s' % msg) - self._prev_percent = None - self._newline = False - - def bargraph_update(self, value=0, percent=None): - if not self._bargraph_msg: - return - if percent is None: - if (self._bargraph_max - self._bargraph_min) > 0: - percent = 100 * (value - self._bargraph_min) // (self._bargraph_max - self._bargraph_min) - else: - percent = 0 - if percent > 100: - percent = 100 - self.print_bargraph(percent) - - def bargraph_done(self): - if not self._bargraph_msg: - return - sys.stderr.write('\r%s: [%s] done in %.2fs\n' % (self._bargraph_msg, '=' * self._bar_length, time.time() - self._start_time)) - sys.stderr.flush() - self._newline = True - self._bargraph_msg = None - - def set_verbose(self, verbose): - self._verbose = verbose diff --git a/pystlink b/pystlink index 24ad453..d5aab41 100755 --- a/pystlink +++ b/pystlink @@ -7,5 +7,4 @@ if [ -L "$exec_file" ] ; then fi work_dir=`dirname $exec_file` - -python3 $work_dir/pystlink.py $* +cd $work_dir && python3 -m stlink.pystlink $* \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..595b269 --- /dev/null +++ b/setup.py @@ -0,0 +1,58 @@ +import setuptools +import os +import codecs + + +_ABOUT = { + 'APP_NAME': 'pystlink', + 'VERSION': '1.0.0', + 'DESCRIPTION': 'Python tool for manipulating with STM32 MCUs using ST-Link in-system programmer and debugger.', + 'URL': 'https://github.com/pavelrevak/pystlink', + 'AUTHOR': 'Pavel Revak', + 'AUTHOR_EMAIL': 'pavel.revak@gmail.com' +} + + +def get_long_description(): + current_dir = os.path.abspath(os.path.dirname(__file__)) + readme_file = os.path.join(current_dir, 'README.md') + with codecs.open(readme_file, encoding='utf-8') as readme_file: + long_description = readme_file.read() + return long_description + + +setuptools.setup( + name=_ABOUT['APP_NAME'], + version=_ABOUT['VERSION'], + description=_ABOUT['DESCRIPTION'], + long_description=get_long_description(), + url=_ABOUT['URL'], + author=_ABOUT['AUTHOR'], + author_email=_ABOUT['AUTHOR_EMAIL'], + license='MIT', + keywords='SWD debugger STM32 STLINK CORTEX-M ARM', + + classifiers=[ + # https://pypi.python.org/pypi?%3Aaction=list_classifiers + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Embedded Systems', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + ], + + packages=[ + 'stlink' + '/lib' + ], + + install_requires=[ + 'pyusb (>=1.0.2)', + ], + + entry_points={ + 'console_scripts': [ + 'pystlink = stlink.cmd.run_cli' + ], + }, +) diff --git a/stlink/__init__.py b/stlink/__init__.py new file mode 100644 index 0000000..c5b0cb1 --- /dev/null +++ b/stlink/__init__.py @@ -0,0 +1,126 @@ +import logging +import time + +from stlink.pystlink import PyStlink +from typing import Optional, List, Union, Callable +from argparse import Namespace +# from .pystlink import PyStlink + +__all__ = [ + 'PyStlink', + 'StlinkWrapper' +] + + +class StlinkWrapper: + def __init__(self): + self._link = PyStlink() + self._commands: List[List] = [] + + def _dispatch(self, actions: Optional[List[List]], verbosity: int = 1, serial: str = None, hard: bool = False, + index: int = 0, cpu: List[str] = (), no_unmount=True, no_run=True, + bar_on_update: Callable[[int, str], None] = None): + args = { + 'action': [':'.join(filter(None, [] if action is None else action)) for action in actions], + 'serial': serial, 'index': index, 'hard': hard, 'verbosity': verbosity, 'cpu': cpu, + 'no_run': no_run, 'no_unmount': no_unmount + } + args = Namespace(**args) + self._link.start(args, bargraph_on_update=bar_on_update) + self._commands.clear() + + def dispatch(self, verbosity: int = 1, serial: str = None, hard: bool = False, + index: int = 0, cpu: List[str] = (), no_unmount=True, no_run=True, + bar_on_update: Callable[[int, str], None] = None): + self._dispatch(self._commands, verbosity, serial, hard, index, cpu, no_unmount, no_run, bar_on_update) + + def _add_command(self, commands: List): + self._commands.append(commands) + + def reset(self, halt: bool = False): + self._add_command(['reset', 'halt'] if halt else ['reset']) + return self + + def run(self): + self._add_command(['run']) + return self + + def sleep(self, duration: float): + self._add_command(['sleep', f'{duration}']) + return self + + def step(self): + self._add_command(['step']) + return self + + def halt(self): + self._add_command(['halt']) + return self + + """ + Verify flash {at address} against binary file ( or against .srec file) + """ + def flash_check(self, file: Optional[str] = None, addr: Optional[int] = None): + self._add_command(['flash', 'check', f'{file}', f'{addr}']) + return self + + def flash_erase(self, erase: bool, verify: bool, file: str, addr: Optional[str] = None): + self._add_command(['flash', 'erase' if erase else None, 'verify' if verify else None, addr if addr is not None else None, + file]) + return self + + """ + Write binary file into memory of sram + :arg addr Use sram to write value to sram + """ + def write(self, file: str, addr: Union[str, int]): + self._add_command(['fill', f'{addr}', file]) + return self + + """ + Fill memory or sram with pattern + """ + def fill(self, addr: Union[str, int], size: int, pattern: str): + self._add_command(['fill', f'{addr}', None if size is None else size, f'{pattern}']) + return self + + """ + Read memory/SRAM/flash into file + """ + def read(self, addr: Union[str, int], size: int, file: str): + self._add_command(['read', addr, f'{size}', file]) + return self + + """ + Set register or 32 bit memory + """ + def set(self, reg: str, data: str): + self._add_command(['set', reg, data]) + return self + + """ + Print all address value to the console + :args addr Possible values: "core", "sram", "flash", or integer value representing the address of the core register + """ + def dump(self, addr: Union[str, int], size: Optional[int]): + self._add_command(['dump', f'{addr}', size]) + return self + + def dump16(self, addr: int): + self._add_command(['dump16', addr]) + return self + + def dump8(self, addr: int): + self._add_command(['dump8', addr]) + return self + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + logging.StreamHandler.terminator = '' # Ugly trick to suppress '\n', they are manually managed by the DBG class + stlink = StlinkWrapper() + stlink\ + .reset()\ + .flash_erase(erase=True, verify=True, file='../firmware.bin', addr='0x8000000') \ + .reset()\ + .dispatch(verbosity=3, hard=True, no_run=False) diff --git a/stlink/cmd.py b/stlink/cmd.py new file mode 100644 index 0000000..84b5cf2 --- /dev/null +++ b/stlink/cmd.py @@ -0,0 +1,7 @@ +from stlink.pystlink import PyStlink + + +def run_cli(): + pystlink = PyStlink() + pystlink.start() + diff --git a/lib/__init__.py b/stlink/lib/__init__.py similarity index 100% rename from lib/__init__.py rename to stlink/lib/__init__.py diff --git a/stlink/lib/dbg.py b/stlink/lib/dbg.py new file mode 100644 index 0000000..5c9a7a8 --- /dev/null +++ b/stlink/lib/dbg.py @@ -0,0 +1,107 @@ +import sys +import time +import logging +from logging import LogRecord +from typing import Callable, Optional + +logger = logging.getLogger('pystlink') +logger.addHandler(logging.NullHandler()) + + +class Formatter(logging.Formatter): + def format(self, record: LogRecord) -> str: + record.msg = record.msg.strip() + return super(Formatter, self).format(record) + + +class Dbg: + def __init__(self, verbose, bar_length=40, bargraph_on_update: Optional[Callable[[int, str], None]] = None): + self._verbose = verbose + self._bargraph_msg = None + self._bargraph_min = None + self._bargraph_max = None + self._newline = True + self._bar_length = bar_length + self._prev_percent = None + self._start_time = None + self._bargraph_handler = self.print_bargraph if bargraph_on_update is None else bargraph_on_update + + def _msg(self, msg, level): + if self._verbose >= level: + if not self._newline: + sys.stderr.write('\n') + self._newline = True + sys.stderr.write('%s\n' % msg) + sys.stderr.flush() + + def debug(self, msg): + logger.debug(f'{msg}\n') + + def verbose(self, msg): + logger.debug(f'{msg}\n') + + def info(self, msg): + logger.info(f'{msg}\n') + + def message(self, msg): + logger.info(f'{msg}\n') + + def error(self, msg): + logger.error(f'*** {msg} ***\n') + + def warning(self, msg): + logger.warning(f' * {msg}\n') + + def print_bargraph(self, percent, msg): + if percent == self._prev_percent: + return + if percent == -2: + logger.info('\r%s: [%s] done in %.2fs\n' % (self._bargraph_msg, '=' * self._bar_length, time.time() - self._start_time)) + self._newline = True + self._bargraph_msg = None + elif percent == -1: + if not self._newline: + self._newline = False + logger.info('%s' % msg) + self._prev_percent = None + self._newline = False + else: + bar = int(percent * self._bar_length) // 100 + logger.info('\r%s: [%s%s] %3d%%' % (self._bargraph_msg, '=' * bar, ' ' * (self._bar_length - bar), percent, )) + self._prev_percent = percent + self._newline = False + + def bargraph_start(self, msg, value_min=0, value_max=100, level=1): + self._start_time = time.time() + if self._verbose < level: + return + self._bargraph_msg = msg + self._bargraph_min = value_min + self._bargraph_max = value_max + self._bargraph_handler(-1, msg) + if not self._newline: + logger.info('\n') + self._newline = False + logger.info('%s' % msg) + self._prev_percent = None + self._newline = False + + def bargraph_update(self, value=0, percent=None): + if not self._bargraph_msg: + return + if percent is None: + if (self._bargraph_max - self._bargraph_min) > 0: + percent = 100 * (value - self._bargraph_min) // (self._bargraph_max - self._bargraph_min) + else: + percent = 0 + if percent > 100: + percent = 100 + self._bargraph_handler(percent, self._bargraph_msg) + + def bargraph_done(self): + if not self._bargraph_msg: + return + self._bargraph_handler(-2, self._bargraph_msg) + + def set_verbose(self, verbose): + self._verbose = verbose diff --git a/lib/srec.py b/stlink/lib/srec.py similarity index 100% rename from lib/srec.py rename to stlink/lib/srec.py diff --git a/lib/stlinkex.py b/stlink/lib/stlinkex.py similarity index 100% rename from lib/stlinkex.py rename to stlink/lib/stlinkex.py diff --git a/lib/stlinkusb.py b/stlink/lib/stlinkusb.py similarity index 98% rename from lib/stlinkusb.py rename to stlink/lib/stlinkusb.py index 20e1404..298ea72 100644 --- a/lib/stlinkusb.py +++ b/stlink/lib/stlinkusb.py @@ -1,6 +1,7 @@ import usb.core import usb.util -import lib.stlinkex +import stlink.lib.stlinkex +import stlink.lib as lib import re @@ -168,3 +169,6 @@ def unmount_discovery(self): stderr=subprocess.PIPE, ) p.wait() + + def close(self): + self._dev.reset() diff --git a/lib/stlinkv2.py b/stlink/lib/stlinkv2.py similarity index 99% rename from lib/stlinkv2.py rename to stlink/lib/stlinkv2.py index 3cca696..01e90d8 100644 --- a/lib/stlinkv2.py +++ b/stlink/lib/stlinkv2.py @@ -1,4 +1,5 @@ -import lib.stlinkex +import stlink.lib.stlinkex +import stlink.lib as lib class Stlink(): diff --git a/lib/stm32.py b/stlink/lib/stm32.py similarity index 99% rename from lib/stm32.py rename to stlink/lib/stm32.py index 82b0e07..6719aca 100644 --- a/lib/stm32.py +++ b/stlink/lib/stm32.py @@ -1,5 +1,6 @@ -import lib.stm32devices -import lib.stlinkex +import stlink.lib.stm32devices +import stlink.lib.stlinkex +import stlink.lib as lib class Stm32(): diff --git a/lib/stm32devices.py b/stlink/lib/stm32devices.py similarity index 100% rename from lib/stm32devices.py rename to stlink/lib/stm32devices.py diff --git a/lib/stm32fp.py b/stlink/lib/stm32fp.py similarity index 99% rename from lib/stm32fp.py rename to stlink/lib/stm32fp.py index 93b5a26..04ad5c9 100644 --- a/lib/stm32fp.py +++ b/stlink/lib/stm32fp.py @@ -1,6 +1,7 @@ import time -import lib.stm32 -import lib.stlinkex +import stlink.lib.stm32 +import stlink.lib.stlinkex +import stlink.lib as lib class Flash(): diff --git a/lib/stm32fs.py b/stlink/lib/stm32fs.py similarity index 99% rename from lib/stm32fs.py rename to stlink/lib/stm32fs.py index f7a1ac2..c5c2ae5 100644 --- a/lib/stm32fs.py +++ b/stlink/lib/stm32fs.py @@ -1,6 +1,7 @@ import time -import lib.stm32 -import lib.stlinkex +import stlink.lib.stm32 +import stlink.lib.stlinkex +import stlink.lib as lib class Flash(): diff --git a/lib/stm32h7.py b/stlink/lib/stm32h7.py similarity index 99% rename from lib/stm32h7.py rename to stlink/lib/stm32h7.py index 5bb5359..8914c73 100644 --- a/lib/stm32h7.py +++ b/stlink/lib/stm32h7.py @@ -1,6 +1,7 @@ import time -import lib.stm32 -import lib.stlinkex +import stlink.lib.stm32 +import stlink.lib.stlinkex +import stlink.lib as lib # Stm32H7 programming class Flash(): diff --git a/lib/stm32l0.py b/stlink/lib/stm32l0.py similarity index 98% rename from lib/stm32l0.py rename to stlink/lib/stm32l0.py index 093c5a6..e2d250a 100644 --- a/lib/stm32l0.py +++ b/stlink/lib/stm32l0.py @@ -1,6 +1,7 @@ import time -import lib.stm32 -import lib.stlinkex +import stlink.lib.stm32 +import stlink.lib.stlinkex +import stlink.lib as lib # Stm32 L0 and L1 programming class Flash(): @@ -71,7 +72,7 @@ def unlock(self): # check if programing was unlocked if pecr & Flash.PECR_PELOCK: raise lib.stlinkex.StlinkException( - 'Error unlocking FLASH_CR: 0x%08x. Reset!' % prcr) + 'Error unlocking FLASH_CR: 0x%08x. Reset!' % pecr) def lock(self): self._stlink.set_debugreg32(self._nvm + Flash.PECR_OFFSET, diff --git a/lib/stm32l4.py b/stlink/lib/stm32l4.py similarity index 99% rename from lib/stm32l4.py rename to stlink/lib/stm32l4.py index 71376f4..41a9e39 100644 --- a/lib/stm32l4.py +++ b/stlink/lib/stm32l4.py @@ -1,6 +1,7 @@ import time -import lib.stm32 -import lib.stlinkex +import stlink.lib.stm32 +import stlink.lib.stlinkex +import stlink.lib as lib # Stm32 L4 and G0 programming class Flash(): diff --git a/list_new_stm32.py b/stlink/list_new_stm32.py similarity index 99% rename from list_new_stm32.py rename to stlink/list_new_stm32.py index ad70156..72430b8 100644 --- a/list_new_stm32.py +++ b/stlink/list_new_stm32.py @@ -1,6 +1,6 @@ import urllib.request import json -import lib.stm32devices +import stlink.lib.stm32devices def fix_cpu_type(cpu_type): diff --git a/pystlink.py b/stlink/pystlink.py similarity index 87% rename from pystlink.py rename to stlink/pystlink.py index e8016dc..298aef0 100644 --- a/pystlink.py +++ b/stlink/pystlink.py @@ -1,18 +1,22 @@ import sys import argparse import time -import lib.stlinkusb -import lib.stlinkv2 -import lib.stm32 -import lib.stm32fp -import lib.stm32fs -import lib.stm32l0 -import lib.stm32l4 -import lib.stm32h7 -import lib.stm32devices -import lib.stlinkex -import lib.dbg -import lib.srec +from typing import Optional, Callable +from argparse import Namespace +import logging +import stlink.lib.stlinkusb +import stlink.lib.stlinkv2 +import stlink.lib.stm32 +import stlink.lib.stm32fp +import stlink.lib.stm32fs +import stlink.lib.stm32l0 +import stlink.lib.stm32l4 +import stlink.lib.stm32h7 +import stlink.lib.stm32devices +import stlink.lib.stlinkex +import stlink.lib.dbg +import stlink.lib.srec +import stlink.lib as lib VERSION_STR = "pystlink v0.0.0 (ST-LinkV2)" @@ -436,25 +440,26 @@ def cmd(self, param): else: raise lib.stlinkex.StlinkExceptionBadParam() - def start(self): - parser = argparse.ArgumentParser(prog='pystlink', formatter_class=argparse.RawTextHelpFormatter, description=DESCRIPTION_STR, epilog=ACTIONS_HELP_STR) - group_verbose = parser.add_argument_group(title='set verbosity level').add_mutually_exclusive_group() - group_verbose.set_defaults(verbosity=1) - group_verbose.add_argument('-q', '--quiet', action='store_const', dest='verbosity', const=0) - group_verbose.add_argument('-i', '--info', action='store_const', dest='verbosity', const=1, help='default') - group_verbose.add_argument('-v', '--verbose', action='store_const', dest='verbosity', const=2) - group_verbose.add_argument('-d', '--debug', action='store_const', dest='verbosity', const=3) - parser.add_argument('-V', '--version', action='version', version=VERSION_STR) - parser.add_argument('-c', '--cpu', action='append', help='set expected CPU type [eg: STM32F051, STM32L4]') - parser.add_argument('-r', '--no-run', action='store_true', help='do not run core when program end (if core was halted)') - parser.add_argument('-u', '--no-unmount', action='store_true', help='do not unmount DISCOVERY from ST-Link/V2-1 on OS/X platform') - parser.add_argument('-s', '--serial', dest='serial', help='Use Stlink with given serial number') - parser.add_argument('-n', '--num-index', type=int, dest='index', default=0, help='Use Stlink with given index') - parser.add_argument('-H', '--hard', action='store_true', help='Reset device with NRST') - group_actions = parser.add_argument_group(title='actions') - group_actions.add_argument('action', nargs='*', help='actions will be processed sequentially') - args = parser.parse_args() - self._dbg = lib.dbg.Dbg(args.verbosity) + def start(self, args: Optional[Namespace] = None, bargraph_on_update: Callable[[int], None] = None): + if args is None: + parser = argparse.ArgumentParser(prog='pystlink', formatter_class=argparse.RawTextHelpFormatter, description=DESCRIPTION_STR, epilog=ACTIONS_HELP_STR) + group_verbose = parser.add_argument_group(title='set verbosity level').add_mutually_exclusive_group() + group_verbose.set_defaults(verbosity=1) + group_verbose.add_argument('-q', '--quiet', action='store_const', dest='verbosity', const=0) + group_verbose.add_argument('-i', '--info', action='store_const', dest='verbosity', const=1, help='default') + group_verbose.add_argument('-v', '--verbose', action='store_const', dest='verbosity', const=2) + group_verbose.add_argument('-d', '--debug', action='store_const', dest='verbosity', const=3) + parser.add_argument('-V', '--version', action='version', version=VERSION_STR) + parser.add_argument('-c', '--cpu', action='append', help='set expected CPU type [eg: STM32F051, STM32L4]') + parser.add_argument('-r', '--no-run', action='store_true', help='do not run core when program end (if core was halted)') + parser.add_argument('-u', '--no-unmount', action='store_true', help='do not unmount DISCOVERY from ST-Link/V2-1 on OS/X platform') + parser.add_argument('-s', '--serial', dest='serial', help='Use Stlink with given serial number') + parser.add_argument('-n', '--num-index', type=int, dest='index', default=0, help='Use Stlink with given index') + parser.add_argument('-H', '--hard', action='store_true', help='Reset device with NRST') + group_actions = parser.add_argument_group(title='actions') + group_actions.add_argument('action', nargs='*', help='actions will be processed sequentially') + args = parser.parse_args() + self._dbg = lib.dbg.Dbg(args.verbosity, bargraph_on_update=bargraph_on_update) self._serial = args.serial self._index = args.index self._hard = args.hard @@ -471,6 +476,8 @@ def start(self): raise e.set_cmd(action) except (lib.stlinkex.StlinkExceptionBadParam, lib.stlinkex.StlinkException) as e: self._dbg.error(e) + if args.verbosity >= 3: + raise e runtime_status = 1 except KeyboardInterrupt: self._dbg.error('Keyboard interrupt') @@ -486,17 +493,22 @@ def start(self): if not args.no_run: self._driver.core_nodebug() else: - self._dbg.warning('CPU may stay in halt mode', level=1) + self._dbg.warning('CPU may stay in halt mode') self._stlink.leave_state() self._stlink.clean_exit() except lib.stlinkex.StlinkException as e: self._dbg.error(e) + if args.verbosity >= 3: + raise e runtime_status = 1 self._dbg.verbose('DONE in %0.2fs' % (time.time() - self._start_time)) if runtime_status: sys.exit(runtime_status) + self._connector.close() if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + logging.StreamHandler.terminator = '' pystlink = PyStlink() pystlink.start() diff --git a/pystlink_test.py b/tests/test_core.py similarity index 100% rename from pystlink_test.py rename to tests/test_core.py diff --git a/pystlink_test_system.py b/tests/test_system.py similarity index 100% rename from pystlink_test_system.py rename to tests/test_system.py