├── requirements-test.txt ├── requirements.txt ├── pyproject.toml ├── tests ├── blink_LED-DIO7_BSL-DIO13_zzh-only.bin ├── test_ext_call.py └── test_cc2538-bsl.py ├── .travis.yml ├── .gitignore ├── .github └── workflows │ └── main.yml ├── Makefile ├── README.md ├── setup.py ├── LICENSE.md └── llama_bsl.py /requirements-test.txt: -------------------------------------------------------------------------------- 1 | scripttest 2 | pytest 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # grab dependencies from setup.py: 2 | . 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /tests/blink_LED-DIO7_BSL-DIO13_zzh-only.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electrolama/llama-bsl/HEAD/tests/blink_LED-DIO7_BSL-DIO13_zzh-only.bin -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | - "3.7" 8 | 9 | # command to install dependencies 10 | install: 11 | - "pip install pyserial" 12 | - "pip install scripttest" 13 | 14 | # command to run tests 15 | script: nosetests -v ./tests/test_cc2538-bsl.py 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Unit test / coverage reports 10 | test-output/ 11 | htmlcov/ 12 | .tox/ 13 | .coverage 14 | .coverage.* 15 | .cache 16 | nosetests.xml 17 | coverage.xml 18 | *,cover 19 | 20 | # Stale firmware downloads 21 | fw-tmp/ 22 | 23 | # Dist build 24 | dist/ 25 | llama_bsl.egg-info 26 | llama-bsl-*/ 27 | 28 | # venv 29 | venv/ 30 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.8", "3.9", "3.10", "3.11"] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install . -r requirements-test.txt 23 | - name: Test with pytest 24 | run: | 25 | pytest 26 | -------------------------------------------------------------------------------- /tests/test_ext_call.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append("..") 4 | 5 | import llama_bsl 6 | 7 | if __name__ == "__main__": 8 | conf = { 9 | "port": "COM37", 10 | "baud": 500000, 11 | "force_speed": 0, 12 | "address": None, 13 | "force": 0, 14 | "erase": 1, 15 | "write": 1, 16 | "erase_page": 0, 17 | "verify": 1, 18 | "read": 0, 19 | "len": 0x80000, 20 | "fname": "", 21 | "ieee_address": 0, 22 | "bootloader_active_high": False, 23 | "bootloader_invert_lines": False, 24 | "disable-bootloader": 0, 25 | "board_type": None, 26 | "fw_role": None, 27 | "fw_stack": None, 28 | "download": False, 29 | "fw_downloaded": None, 30 | "fw_file": "blink_LED-DIO7_BSL-DIO13_zzh-only.bin", 31 | "index_url": None, 32 | } 33 | 34 | llama_bsl.run(conf) 35 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON ?= python 2 | 3 | ifeq ($(OS),Windows_NT) 4 | RM = powershell Remove-Item -Recurse -Force -ErrorAction Ignore 5 | else 6 | RM = rm -rf 7 | endif 8 | 9 | build: dist-clean 10 | @$(PYTHON) -m build 11 | 12 | release: build 13 | @$(PYTHON) -m twine upload dist/* 14 | 15 | release-test: build 16 | @$(PYTHON) -m twine upload --repository testpypi dist/* 17 | 18 | dist-clean: 19 | # echo here is just so Remove-Item's return code is ignored. 20 | $(RM) dist/, build/, llama_bsl.egg-info/, llama-bsl-*/ || echo "." 21 | 22 | pip-uninstall: 23 | @$(PYTHON) -m pip uninstall llama-bsl 24 | 25 | pip-install: 26 | @$(PYTHON) -m pip install llama-bsl 27 | 28 | pip-install-test: 29 | @$(PYTHON) -m pip install --index-url https://test.pypi.org/simple/ llama-bsl 30 | 31 | del-env: 32 | # echo here is just so Remove-Item's return code is ignored. 33 | $(RM) venv/ || echo "." 34 | 35 | new-env: del-env 36 | @$(PYTHON) -m venv venv 37 | 38 | install-build-dep: 39 | sudo @$(PYTHON) -m pip install virtualenv build wheel twine 40 | -------------------------------------------------------------------------------- /tests/test_cc2538-bsl.py: -------------------------------------------------------------------------------- 1 | from scripttest import TestFileEnvironment 2 | import shutil 3 | 4 | # Init test environment 5 | env = TestFileEnvironment("./test-output") 6 | 7 | # Tests 8 | 9 | 10 | # Make sure there is help output 11 | def test_help_output(): 12 | res = env.run("python", "./../llama-bsl.py", "-h", "--help") 13 | 14 | 15 | # Test for failure on no input file 16 | # TODO needs better checking 17 | def test_sanity_checks_no_input(): 18 | res = env.run("python", "./../llama-bsl.py", "-w", "-r", "-v", expect_error=1) 19 | 20 | 21 | # Test for not implemented feature of verify after read 22 | # TODO needs better checking 23 | def test_sanity_checks_verify_after_read(): 24 | res = env.run("python", "./../llama-bsl.py", "-r", "-v", expect_error=1) 25 | 26 | 27 | # Test for version output 28 | def test_version(): 29 | res = env.run("python", "./../llama-bsl.py", "--version") 30 | 31 | 32 | # Clean up after tests 33 | def teardown_module(module): 34 | print("Removing test-output folder") 35 | shutil.rmtree("./test-output") 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # llama-bsl 2 | 3 | This is a nascent fork of the ever-popular [cc2538-bsl.py](https://github.com/JelmerT/cc2538-bsl/) script that is widely used with TI's serial bootloader for CC13xx/CC26xx/CC2538 series of chips. 4 | 5 | ### Warning: This is an experimental fork with features that can disable BSL on your boards. Do not use if you're not comfortable with JTAG recovery until a fully tested release is published. 6 | 7 | 8 | ## Why fork? 9 | 10 | We're adding features that only make sense in limited context, therefore unlikely to be ever merged upstream. 11 | 12 | Any generally applicable additions/fixes will be submitted upstream. 13 | 14 | 15 | ## Usage 16 | 17 | Documentation for new features will be added once fully tested, refer to commit history to see changes. 18 | 19 | Original README can be found [here](https://github.com/JelmerT/cc2538-bsl/blob/master/README.md). 20 | 21 | 22 | ## Authors 23 | 24 | [@OmerK](https://twitter.com/omerk) and [contributors](https://github.com/electrolama/llama-bsl/graphs/contributors). 25 | 26 | llama-bsl is a fork of [cc2538-bsl.py](https://github.com/JelmerT/cc2538-bsl/) created by Jelmer Tiete , which is in turn based on [stm32loader](https://github.com/jsnyder/stm32loader) by Ivan A-R 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="llama-bsl", 5 | version="v0.5", 6 | description="Script to communicate with Texas Instruments CC13xx/CC2538/CC26xx Serial Boot Loader (Fork of cc2538-bsl.py)", 7 | long_description=open("README.md", encoding="utf-8").read(), 8 | keywords="cc2538, cc1310, cc13xx, bootloader, cc26xx, cc2650, cc2640", 9 | url="https://github.com/electrolama/llama-bsl", 10 | author="Omer Kilic", 11 | author_email="omerkilic@gmail.com", 12 | license="BSD-3-Clause", 13 | classifiers=[ 14 | "Development Status :: 4 - Beta", 15 | "Environment :: Console", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: BSD License", 18 | "Operating System :: POSIX :: Linux", 19 | "Operating System :: MacOS", 20 | "Operating System :: Microsoft :: Windows", 21 | "Programming Language :: Python :: 3", 22 | "Topic :: Scientific/Engineering", 23 | ], 24 | platforms="posix", 25 | python_requires=">=3.4", 26 | setup_requires=["setuptools_scm"], 27 | install_requires=[ 28 | "pip>=10", 29 | "setuptools", 30 | "wheel", 31 | "pyserial==3.5", 32 | "intelhex==2.3.0", 33 | "requests==2.26.0", 34 | ], 35 | entry_points={ 36 | "console_scripts": [ 37 | "llama-bsl=llama_bsl:main", 38 | ], 39 | }, 40 | ) 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2014, Jelmer Tiete . 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /llama_bsl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014, Jelmer Tiete . 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions 8 | # are met: 9 | # 1. Redistributions of source code must retain the above copyright 10 | # notice, this list of conditions and the following disclaimer. 11 | # 2. Redistributions in binary form must reproduce the above copyright 12 | # notice, this list of conditions and the following disclaimer in the 13 | # documentation and/or other materials provided with the distribution. 14 | # 3. The name of the author may not be used to endorse or promote 15 | # products derived from this software without specific prior 16 | # written permission. 17 | 18 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS 19 | # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 24 | # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | # Implementation based on stm32loader by Ivan A-R 31 | 32 | # Serial boot loader over UART for CC13xx / CC2538 / CC26xx 33 | # Based on the info found in TI's swru333a.pdf (spma029.pdf) 34 | # 35 | # Bootloader only starts if no valid image is found or if boot loader 36 | # backdoor is enabled. 37 | # Make sure you don't lock yourself out!! (enable backdoor in your firmware) 38 | # More info at https://github.com/JelmerT/cc2538-bsl 39 | 40 | from subprocess import Popen, PIPE 41 | 42 | import sys 43 | import getopt 44 | import glob 45 | import time 46 | import os 47 | import struct 48 | import binascii 49 | import traceback 50 | import io 51 | import shutil 52 | 53 | try: 54 | import magic 55 | 56 | magic.from_file 57 | have_magic = True 58 | except (ImportError, AttributeError): 59 | have_magic = False 60 | 61 | try: 62 | from intelhex import IntelHex 63 | 64 | have_hex_support = True 65 | except ImportError: 66 | have_hex_support = False 67 | 68 | try: 69 | import requests 70 | 71 | have_requests = True 72 | except ImportError: 73 | have_requests = False 74 | 75 | try: 76 | import zipfile 77 | 78 | have_zipfile = True 79 | except ImportError: 80 | have_zipfile = False 81 | 82 | # version 83 | __version__ = "2.1" 84 | 85 | # Verbose level 86 | QUIET = 5 87 | 88 | try: 89 | import serial 90 | except ImportError: 91 | print("{} requires the Python serial library".format(sys.argv[0])) 92 | print("Please install it with:") 93 | print("") 94 | print(" pip3 install pyserial") 95 | sys.exit(1) 96 | 97 | 98 | def mdebug(level, message, attr="\n"): 99 | if QUIET >= level: 100 | print(message, end=attr, file=sys.stderr) 101 | 102 | 103 | # Takes chip IDs (obtained via Get ID command) to human-readable names 104 | CHIP_ID_STRS = {0xB964: "CC2538", 0xB965: "CC2538"} 105 | 106 | RETURN_CMD_STRS = { 107 | 0x40: "Success", 108 | 0x41: "Unknown command", 109 | 0x42: "Invalid command", 110 | 0x43: "Invalid address", 111 | 0x44: "Flash fail", 112 | } 113 | 114 | COMMAND_RET_SUCCESS = 0x40 115 | COMMAND_RET_UNKNOWN_CMD = 0x41 116 | COMMAND_RET_INVALID_CMD = 0x42 117 | COMMAND_RET_INVALID_ADR = 0x43 118 | COMMAND_RET_FLASH_FAIL = 0x44 119 | 120 | 121 | class CmdException(Exception): 122 | pass 123 | 124 | 125 | class FirmwareFile(object): 126 | HEX_FILE_EXTENSIONS = ("hex", "ihx", "ihex") 127 | 128 | def __init__(self, path): 129 | """ 130 | Read a firmware file and store its data ready for device programming. 131 | 132 | This class will try to guess the file type if python-magic is available. 133 | 134 | If python-magic indicates a plain text file, and if IntelHex is 135 | available, then the file will be treated as one of Intel HEX format. 136 | 137 | In all other cases, the file will be treated as a raw binary file. 138 | 139 | In both cases, the file's contents are stored in bytes for subsequent 140 | usage to program a device or to perform a crc check. 141 | 142 | Parameters: 143 | path -- A str with the path to the firmware file. 144 | 145 | Attributes: 146 | bytes: A bytearray with firmware contents ready to send to the 147 | device 148 | """ 149 | self._crc32 = None 150 | firmware_is_hex = False 151 | 152 | if have_magic: 153 | file_type = magic.from_file(path, mime=True) 154 | 155 | if file_type == "text/plain": 156 | firmware_is_hex = True 157 | mdebug(5, "Firmware file: Intel Hex") 158 | elif file_type == "application/octet-stream": 159 | mdebug(5, "Firmware file: Raw Binary") 160 | else: 161 | error_str = ( 162 | "Could not determine firmware type. Magic " 163 | "indicates '%s'" % (file_type) 164 | ) 165 | raise CmdException(error_str) 166 | else: 167 | if os.path.splitext(path)[1][1:] in self.HEX_FILE_EXTENSIONS: 168 | firmware_is_hex = True 169 | mdebug(5, "Your firmware looks like an Intel Hex file") 170 | else: 171 | mdebug(5, "Cannot auto-detect firmware filetype: Assuming .bin") 172 | 173 | mdebug( 174 | 10, 175 | "For more solid firmware type auto-detection, install " "python-magic.", 176 | ) 177 | mdebug(10, "Please see the readme for more details.") 178 | 179 | if firmware_is_hex: 180 | if have_hex_support: 181 | self.bytes = bytearray(IntelHex(path).tobinarray()) 182 | return 183 | else: 184 | error_str = ( 185 | "Firmware is Intel Hex, but the IntelHex library " 186 | "could not be imported.\n" 187 | "Install IntelHex in site-packages or program " 188 | "your device with a raw binary (.bin) file.\n" 189 | "Please see the readme for more details." 190 | ) 191 | raise CmdException(error_str) 192 | 193 | with open(path, "rb") as f: 194 | self.bytes = bytearray(f.read()) 195 | 196 | def crc32(self): 197 | """ 198 | Return the crc32 checksum of the firmware image 199 | 200 | Return: 201 | The firmware's CRC32, ready for comparison with the CRC 202 | returned by the ROM bootloader's COMMAND_CRC32 203 | """ 204 | if self._crc32 == None: 205 | self._crc32 = binascii.crc32(bytearray(self.bytes)) & 0xFFFFFFFF 206 | 207 | return self._crc32 208 | 209 | 210 | class CommandInterface(object): 211 | ACK_BYTE = 0xCC 212 | NACK_BYTE = 0x33 213 | 214 | def open(self, aport=None, abaudrate=500000): 215 | # Try to create the object using serial_for_url(), or fall back to the 216 | # old serial.Serial() where serial_for_url() is not supported. 217 | # serial_for_url() is a factory class and will return a different 218 | # object based on the URL. For example serial_for_url("/dev/tty.") 219 | # will return a serialposix.Serial object for Ubuntu or Mac OS; 220 | # serial_for_url("COMx") will return a serialwin32.Serial oject for Windows OS. 221 | # For that reason, we need to make sure the port doesn't get opened at 222 | # this stage: We need to set its attributes up depending on what object 223 | # we get. 224 | try: 225 | self.sp = serial.serial_for_url( 226 | aport, do_not_open=True, timeout=10, write_timeout=10 227 | ) 228 | except AttributeError: 229 | self.sp = serial.Serial(port=None, timeout=10, write_timeout=10) 230 | self.sp.port = aport 231 | 232 | if (os.name == "nt" and isinstance(self.sp, serial.serialwin32.Serial)) or ( 233 | os.name == "posix" and isinstance(self.sp, serial.serialposix.Serial) 234 | ): 235 | self.sp.baudrate = abaudrate # baudrate 236 | self.sp.bytesize = 8 # number of databits 237 | self.sp.parity = serial.PARITY_NONE # parity 238 | self.sp.stopbits = 1 # stop bits 239 | self.sp.xonxoff = 0 # s/w (XON/XOFF) flow control 240 | self.sp.rtscts = 0 # h/w (RTS/CTS) flow control 241 | self.sp.timeout = 0.5 # set the timeout value 242 | 243 | self.sp.open() 244 | 245 | def invoke_bootloader(self, dtr_active_high=False, inverted=False): 246 | # Use the DTR and RTS lines to control bootloader and the !RESET pin. 247 | # This can automatically invoke the bootloader without the user 248 | # having to toggle any pins. 249 | # 250 | # If inverted is False (default): 251 | # DTR: connected to the bootloader pin 252 | # RTS: connected to !RESET 253 | # If inverted is True, pin connections are the other way round 254 | if inverted: 255 | set_bootloader_pin = self.sp.setRTS 256 | set_reset_pin = self.sp.setDTR 257 | else: 258 | set_bootloader_pin = self.sp.setDTR 259 | set_reset_pin = self.sp.setRTS 260 | 261 | set_bootloader_pin(1 if not dtr_active_high else 0) 262 | set_reset_pin(0) 263 | set_reset_pin(1) 264 | set_reset_pin(0) 265 | # Make sure the pin is still asserted when the chip 266 | # comes out of reset. This fixes an issue where 267 | # there wasn't enough delay here on Mac. 268 | time.sleep(0.002) 269 | set_bootloader_pin(0 if not dtr_active_high else 1) 270 | 271 | # Some boards have a co-processor that detects this sequence here and 272 | # then drives the main chip's BSL enable and !RESET pins. Depending on 273 | # board design and co-processor behaviour, the !RESET pin may get 274 | # asserted after we have finished the sequence here. In this case, we 275 | # need a small delay so as to avoid trying to talk to main chip before 276 | # it has actually entered its bootloader mode. 277 | # 278 | # See contiki-os/contiki#1533 279 | time.sleep(0.1) 280 | 281 | def close(self): 282 | self.sp.close() 283 | 284 | def _wait_for_ack(self, info="", timeout=1): 285 | stop = time.time() + timeout 286 | got = bytearray(2) 287 | while got[-2] != 00 or got[-1] not in ( 288 | CommandInterface.ACK_BYTE, 289 | CommandInterface.NACK_BYTE, 290 | ): 291 | got += self._read(1) 292 | if time.time() > stop: 293 | raise CmdException("Timeout waiting for ACK/NACK after '%s'" % (info,)) 294 | 295 | # Our bytearray's length is: 2 initial bytes + 2 bytes for the ACK/NACK 296 | # plus a possible N-4 additional (buffered) bytes 297 | mdebug(10, "Got %d additional bytes before ACK/NACK" % (len(got) - 4,)) 298 | 299 | # wait for ask 300 | ask = got[-1] 301 | 302 | if ask == CommandInterface.ACK_BYTE: 303 | # ACK 304 | return 1 305 | elif ask == CommandInterface.NACK_BYTE: 306 | # NACK 307 | mdebug(10, "Target replied with a NACK during %s" % info) 308 | return 0 309 | 310 | # Unknown response 311 | mdebug(10, "Unrecognised response 0x%x to %s" % (ask, info)) 312 | return 0 313 | 314 | def _encode_addr(self, addr): 315 | byte3 = (addr >> 0) & 0xFF 316 | byte2 = (addr >> 8) & 0xFF 317 | byte1 = (addr >> 16) & 0xFF 318 | byte0 = (addr >> 24) & 0xFF 319 | return bytes([byte0, byte1, byte2, byte3]) 320 | 321 | def _decode_addr(self, byte0, byte1, byte2, byte3): 322 | return (byte3 << 24) | (byte2 << 16) | (byte1 << 8) | (byte0 << 0) 323 | 324 | def _calc_checks(self, cmd, addr, size): 325 | return ( 326 | sum(bytearray(self._encode_addr(addr))) 327 | + sum(bytearray(self._encode_addr(size))) 328 | + cmd 329 | ) & 0xFF 330 | 331 | def _write(self, data, is_retry=False): 332 | if type(data) == int: 333 | assert data < 256 334 | goal = 1 335 | written = self.sp.write(bytes([data])) 336 | elif type(data) == bytes or type(data) == bytearray: 337 | goal = len(data) 338 | written = self.sp.write(data) 339 | else: 340 | raise CmdException("Internal Error. Bad data type: {}".format(type(data))) 341 | 342 | if written < goal: 343 | mdebug(10, "*** Only wrote {} of target {} bytes".format(written, goal)) 344 | if is_retry and written == 0: 345 | raise CmdException("Failed to write data on the serial bus") 346 | mdebug(10, "*** Retrying write for remainder") 347 | if type(data) == int: 348 | return self._write(data, is_retry=True) 349 | else: 350 | return self._write(data[written:], is_retry=True) 351 | 352 | def _read(self, length): 353 | return bytearray(self.sp.read(length)) 354 | 355 | def sendAck(self): 356 | self._write(0x00) 357 | self._write(0xCC) 358 | return 359 | 360 | def sendNAck(self): 361 | self._write(0x00) 362 | self._write(0x33) 363 | return 364 | 365 | def receivePacket(self): 366 | # stop = time.time() + 5 367 | # got = None 368 | # while not got: 369 | got = self._read(2) 370 | # if time.time() > stop: 371 | # break 372 | 373 | # if not got: 374 | # raise CmdException("No response to %s" % info) 375 | 376 | size = got[0] # rcv size 377 | chks = got[1] # rcv checksum 378 | data = bytearray(self._read(size - 2)) # rcv data 379 | 380 | mdebug(10, "*** received %x bytes" % size) 381 | if chks == sum(data) & 0xFF: 382 | self.sendAck() 383 | return data 384 | else: 385 | self.sendNAck() 386 | # TODO: retry receiving! 387 | raise CmdException("Received packet checksum error") 388 | return 0 389 | 390 | def sendSynch(self): 391 | cmd = 0x55 392 | 393 | # flush serial input buffer for first ACK reception 394 | self.sp.flushInput() 395 | 396 | mdebug(10, "*** sending synch sequence") 397 | self._write(cmd) # send U 398 | self._write(cmd) # send U 399 | return self._wait_for_ack("Synch (0x55 0x55)", 2) 400 | 401 | def checkLastCmd(self): 402 | stat = self.cmdGetStatus() 403 | if not (stat): 404 | raise CmdException( 405 | "No response from target on status request. " 406 | "(Did you disable the bootloader?)" 407 | ) 408 | 409 | if stat[0] == COMMAND_RET_SUCCESS: 410 | mdebug(10, "Command Successful") 411 | return 1 412 | else: 413 | stat_str = RETURN_CMD_STRS.get(stat[0], None) 414 | if stat_str == None: 415 | mdebug(0, "Warning: unrecognized status returned " "0x%x" % stat[0]) 416 | else: 417 | mdebug(0, "Target returned: 0x%x, %s" % (stat[0], stat_str)) 418 | return 0 419 | 420 | def cmdPing(self): 421 | cmd = 0x20 422 | lng = 3 423 | 424 | self._write(lng) # send size 425 | self._write(cmd) # send checksum 426 | self._write(cmd) # send data 427 | 428 | mdebug(10, "*** Ping command (0x20)") 429 | if self._wait_for_ack("Ping (0x20)"): 430 | return self.checkLastCmd() 431 | 432 | def cmdReset(self): 433 | cmd = 0x25 434 | lng = 3 435 | 436 | self._write(lng) # send size 437 | self._write(cmd) # send checksum 438 | self._write(cmd) # send data 439 | 440 | mdebug(10, "*** Reset command (0x25)") 441 | if self._wait_for_ack("Reset (0x25)"): 442 | return 1 443 | 444 | def cmdGetChipId(self): 445 | cmd = 0x28 446 | lng = 3 447 | 448 | self._write(lng) # send size 449 | self._write(cmd) # send checksum 450 | self._write(cmd) # send data 451 | 452 | mdebug(10, "*** GetChipId command (0x28)") 453 | if self._wait_for_ack("Get ChipID (0x28)"): 454 | # 4 byte answ, the 2 LSB hold chip ID 455 | version = self.receivePacket() 456 | if self.checkLastCmd(): 457 | assert len(version) == 4, "Unreasonable chip " "id: %s" % repr(version) 458 | mdebug(10, " Version 0x%02X%02X%02X%02X" % tuple(version)) 459 | chip_id = (version[2] << 8) | version[3] 460 | return chip_id 461 | else: 462 | raise CmdException("GetChipID (0x28) failed") 463 | 464 | def cmdGetStatus(self): 465 | cmd = 0x23 466 | lng = 3 467 | 468 | self._write(lng) # send size 469 | self._write(cmd) # send checksum 470 | self._write(cmd) # send data 471 | 472 | mdebug(10, "*** GetStatus command (0x23)") 473 | if self._wait_for_ack("Get Status (0x23)"): 474 | stat = self.receivePacket() 475 | return stat 476 | 477 | def cmdSetXOsc(self): 478 | cmd = 0x29 479 | lng = 3 480 | 481 | self._write(lng) # send size 482 | self._write(cmd) # send checksum 483 | self._write(cmd) # send data 484 | 485 | mdebug(10, "*** SetXOsc command (0x29)") 486 | if self._wait_for_ack("SetXOsc (0x29)"): 487 | return 1 488 | # UART speed (needs) to be changed! 489 | 490 | def cmdRun(self, addr): 491 | cmd = 0x22 492 | lng = 7 493 | 494 | self._write(lng) # send length 495 | self._write(self._calc_checks(cmd, addr, 0)) # send checksum 496 | self._write(cmd) # send cmd 497 | self._write(self._encode_addr(addr)) # send addr 498 | 499 | mdebug(10, "*** Run command(0x22)") 500 | return 1 501 | 502 | def cmdEraseMemory(self, addr, size): 503 | cmd = 0x26 504 | lng = 11 505 | 506 | self._write(lng) # send length 507 | self._write(self._calc_checks(cmd, addr, size)) # send checksum 508 | self._write(cmd) # send cmd 509 | self._write(self._encode_addr(addr)) # send addr 510 | self._write(self._encode_addr(size)) # send size 511 | 512 | mdebug(10, "*** Erase command(0x26)") 513 | if self._wait_for_ack("Erase memory (0x26)", 10): 514 | return self.checkLastCmd() 515 | 516 | def cmdBankErase(self): 517 | cmd = 0x2C 518 | lng = 3 519 | 520 | self._write(lng) # send length 521 | self._write(cmd) # send checksum 522 | self._write(cmd) # send cmd 523 | 524 | mdebug(10, "*** Bank Erase command(0x2C)") 525 | if self._wait_for_ack("Bank Erase (0x2C)", 10): 526 | return self.checkLastCmd() 527 | 528 | def cmdCRC32(self, addr, size): 529 | cmd = 0x27 530 | lng = 11 531 | 532 | self._write(lng) # send length 533 | self._write(self._calc_checks(cmd, addr, size)) # send checksum 534 | self._write(cmd) # send cmd 535 | self._write(self._encode_addr(addr)) # send addr 536 | self._write(self._encode_addr(size)) # send size 537 | 538 | mdebug(10, "*** CRC32 command(0x27)") 539 | if self._wait_for_ack("Get CRC32 (0x27)", 1): 540 | crc = self.receivePacket() 541 | if self.checkLastCmd(): 542 | return self._decode_addr(crc[3], crc[2], crc[1], crc[0]) 543 | 544 | def cmdCRC32CC26xx(self, addr, size): 545 | cmd = 0x27 546 | lng = 15 547 | 548 | self._write(lng) # send length 549 | self._write(self._calc_checks(cmd, addr, size)) # send checksum 550 | self._write(cmd) # send cmd 551 | self._write(self._encode_addr(addr)) # send addr 552 | self._write(self._encode_addr(size)) # send size 553 | self._write(self._encode_addr(0x00000000)) # send number of reads 554 | 555 | mdebug(10, "*** CRC32 command(0x27)") 556 | if self._wait_for_ack("Get CRC32 (0x27)", 1): 557 | crc = self.receivePacket() 558 | if self.checkLastCmd(): 559 | return self._decode_addr(crc[3], crc[2], crc[1], crc[0]) 560 | 561 | def cmdDownload(self, addr, size): 562 | cmd = 0x21 563 | lng = 11 564 | 565 | if (size % 4) != 0: # check for invalid data lengths 566 | raise Exception( 567 | "Invalid data size: %i. " "Size must be a multiple of 4." % size 568 | ) 569 | 570 | self._write(lng) # send length 571 | self._write(self._calc_checks(cmd, addr, size)) # send checksum 572 | self._write(cmd) # send cmd 573 | self._write(self._encode_addr(addr)) # send addr 574 | self._write(self._encode_addr(size)) # send size 575 | 576 | mdebug(10, "*** Download command (0x21)") 577 | if self._wait_for_ack("Download (0x21)", 2): 578 | return self.checkLastCmd() 579 | 580 | def cmdSendData(self, data): 581 | cmd = 0x24 582 | lng = len(data) + 3 583 | # TODO: check total size of data!! max 252 bytes! 584 | 585 | self._write(lng) # send size 586 | self._write((sum(bytearray(data)) + cmd) & 0xFF) # send checksum 587 | self._write(cmd) # send cmd 588 | self._write(bytearray(data)) # send data 589 | 590 | mdebug(10, "*** Send Data (0x24)") 591 | if self._wait_for_ack("Send data (0x24)", 10): 592 | return self.checkLastCmd() 593 | 594 | def cmdMemRead(self, addr): # untested 595 | cmd = 0x2A 596 | lng = 8 597 | 598 | self._write(lng) # send length 599 | self._write(self._calc_checks(cmd, addr, 4)) # send checksum 600 | self._write(cmd) # send cmd 601 | self._write(self._encode_addr(addr)) # send addr 602 | self._write(4) # send width, 4 bytes 603 | 604 | mdebug(10, "*** Mem Read (0x2A)") 605 | if self._wait_for_ack("Mem Read (0x2A)", 1): 606 | data = self.receivePacket() 607 | if self.checkLastCmd(): 608 | # self._decode_addr(ord(data[3]), 609 | # ord(data[2]),ord(data[1]),ord(data[0])) 610 | return data 611 | 612 | def cmdMemReadCC26xx(self, addr): 613 | cmd = 0x2A 614 | lng = 9 615 | 616 | self._write(lng) # send length 617 | self._write(self._calc_checks(cmd, addr, 2)) # send checksum 618 | self._write(cmd) # send cmd 619 | self._write(self._encode_addr(addr)) # send addr 620 | self._write(1) # send width, 4 bytes 621 | self._write(1) # send number of reads 622 | 623 | mdebug(10, "*** Mem Read (0x2A)") 624 | if self._wait_for_ack("Mem Read (0x2A)", 1): 625 | data = self.receivePacket() 626 | if self.checkLastCmd(): 627 | return data 628 | 629 | def cmdMemWrite(self, addr, data, width): 630 | if width != len(data): 631 | raise ValueError("width does not match len(data)") 632 | if width != 1 and width != 4: 633 | raise ValueError("width must be 1 or 4") 634 | 635 | cmd = 0x2B 636 | lng = 8 + len(data) 637 | 638 | content = ( 639 | bytearray([cmd]) 640 | + self._encode_addr(addr) 641 | + bytearray([1 if (width == 4) else 0]) 642 | + bytearray(data) 643 | ) 644 | 645 | self._write(lng) # send length 646 | self._write(sum(content) & 0xFF) # send checksum 647 | self._write(content) 648 | 649 | mdebug(10, "*** Mem write (0x2B)") 650 | if self._wait_for_ack("Mem Write (0x2B)", 2): 651 | return self.checkLastCmd() 652 | 653 | # Complex commands section 654 | 655 | def writeMemory(self, addr, data): 656 | lng = len(data) 657 | # amount of data bytes transferred per packet (theory: max 252 + 3) 658 | trsf_size = 248 659 | empty_packet = bytearray((0xFF,) * trsf_size) 660 | 661 | # Boot loader enable check 662 | # TODO: implement check for all chip sizes & take into account partial 663 | # firmware uploads 664 | if lng == 524288: # check if file is for 512K model 665 | # check the boot loader enable bit (only for 512K model) 666 | if not ((data[524247] & (1 << 4)) >> 4): 667 | if not ( 668 | conf["force"] 669 | or query_yes_no( 670 | "The boot loader backdoor is not enabled " 671 | "in the firmware you are about to write " 672 | "to the target. You will NOT be able to " 673 | "reprogram the target using this tool if " 674 | "you continue! " 675 | "Do you want to continue?", 676 | "no", 677 | ) 678 | ): 679 | raise Exception("Aborted by user.") 680 | 681 | mdebug( 682 | 5, 683 | "Writing %(lng)d bytes starting at address 0x%(addr)08X" 684 | % {"lng": lng, "addr": addr}, 685 | ) 686 | 687 | offs = 0 688 | addr_set = 0 689 | 690 | # check if amount of remaining data is less then packet size 691 | while lng > trsf_size: 692 | # skip packets filled with 0xFF 693 | if data[offs : offs + trsf_size] != empty_packet: 694 | if addr_set != 1: 695 | # set starting address if not set 696 | self.cmdDownload(addr, lng) 697 | addr_set = 1 698 | mdebug( 699 | 5, 700 | " Write %(len)d bytes at 0x%(addr)08X" 701 | % {"addr": addr, "len": trsf_size}, 702 | "\r", 703 | ) 704 | sys.stdout.flush() 705 | 706 | # send next data packet 707 | self.cmdSendData(data[offs : offs + trsf_size]) 708 | else: # skipped packet, address needs to be set 709 | addr_set = 0 710 | 711 | offs = offs + trsf_size 712 | addr = addr + trsf_size 713 | lng = lng - trsf_size 714 | 715 | mdebug(5, "Write %(len)d bytes at 0x%(addr)08X" % {"addr": addr, "len": lng}) 716 | self.cmdDownload(addr, lng) 717 | return self.cmdSendData(data[offs : offs + lng]) # send last data packet 718 | 719 | 720 | class Chip(object): 721 | def __init__(self, command_interface): 722 | self.command_interface = command_interface 723 | 724 | # Some defaults. The child can override. 725 | self.flash_start_addr = 0x00000000 726 | self.has_cmd_set_xosc = False 727 | self.page_size = 2048 728 | 729 | def page_to_addr(self, pages): 730 | addresses = [] 731 | for page in pages: 732 | addresses.append(int(device.flash_start_addr) + int(page) * self.page_size) 733 | return addresses 734 | 735 | def crc(self, address, size): 736 | return getattr(self.command_interface, self.crc_cmd)(address, size) 737 | 738 | def disable_bootloader(self): 739 | if not ( 740 | conf["force"] 741 | or query_yes_no( 742 | "Disabling the bootloader will prevent you from " 743 | "using this script until you re-enable the " 744 | "bootloader using JTAG. Do you want to continue?", 745 | "no", 746 | ) 747 | ): 748 | raise Exception("Aborted by user.") 749 | 750 | pattern = struct.pack("> 4 775 | if 0 < self.size <= 4: 776 | self.size *= 0x20000 # in bytes 777 | else: 778 | self.size = 0x10000 # in bytes 779 | self.bootloader_address = self.flash_start_addr + self.size - ccfg_len 780 | 781 | sram = (((model[2] << 8) | model[3]) & 0x380) >> 7 782 | sram = (2 - sram) << 3 if sram <= 1 else 32 # in KB 783 | 784 | pg = self.command_interface.cmdMemRead(FLASH_CTRL_DIECFG2) 785 | pg_major = (pg[2] & 0xF0) >> 4 786 | if pg_major == 0: 787 | pg_major = 1 788 | pg_minor = pg[2] & 0x0F 789 | 790 | ti_oui = bytearray([0x00, 0x12, 0x4B]) 791 | ieee_addr = self.command_interface.cmdMemRead(addr_ieee_address_primary) 792 | ieee_addr_end = self.command_interface.cmdMemRead(addr_ieee_address_primary + 4) 793 | if ieee_addr[:3] == ti_oui: 794 | ieee_addr += ieee_addr_end 795 | else: 796 | ieee_addr = ieee_addr_end + ieee_addr 797 | 798 | mdebug( 799 | 5, 800 | "CC2538 PG%d.%d: %dKB Flash, %dKB SRAM, CCFG at 0x%08X" 801 | % (pg_major, pg_minor, self.size >> 10, sram, self.bootloader_address), 802 | ) 803 | mdebug( 804 | 5, "Primary IEEE Address: %s" % (":".join("%02X" % x for x in ieee_addr)) 805 | ) 806 | 807 | def erase(self): 808 | mdebug( 809 | 5, 810 | "Erasing %s bytes starting at address 0x%08X" 811 | % (self.size, self.flash_start_addr), 812 | ) 813 | return self.command_interface.cmdEraseMemory(self.flash_start_addr, self.size) 814 | 815 | def read_memory(self, addr): 816 | # CC2538's COMMAND_MEMORY_READ sends each 4-byte number in inverted 817 | # byte order compared to what's written on the device 818 | data = self.command_interface.cmdMemRead(addr) 819 | return bytearray([data[x] for x in range(3, -1, -1)]) 820 | 821 | 822 | class CC26xx(Chip): 823 | # Class constants 824 | MISC_CONF_1 = 0x500010A0 825 | PROTO_MASK_BLE = 0x01 826 | PROTO_MASK_IEEE = 0x04 827 | PROTO_MASK_BOTH = 0x05 828 | 829 | def __init__(self, command_interface): 830 | super(CC26xx, self).__init__(command_interface) 831 | self.bootloader_dis_val = 0x00000000 832 | self.crc_cmd = "cmdCRC32CC26xx" 833 | self.page_size = 4096 834 | 835 | ICEPICK_DEVICE_ID = 0x50001318 836 | FCFG_USER_ID = 0x50001294 837 | PRCM_RAMHWOPT = 0x40082250 838 | FLASH_SIZE = 0x4003002C 839 | addr_ieee_address_primary = 0x500012F0 840 | ccfg_len = 88 841 | ieee_address_secondary_offset = 0x20 842 | bootloader_dis_offset = 0x30 843 | sram = "Unknown" 844 | 845 | # Determine CC13xx vs CC26xx via ICEPICK_DEVICE_ID::WAFER_ID and store 846 | # PG revision 847 | device_id = self.command_interface.cmdMemReadCC26xx(ICEPICK_DEVICE_ID) 848 | wafer_id = ( 849 | ((device_id[3] & 0x0F) << 16) + (device_id[2] << 8) + (device_id[1] & 0xF0) 850 | ) >> 4 851 | pg_rev = (device_id[3] & 0xF0) >> 4 852 | 853 | # Read FCFG1_USER_ID to get the package and supported protocols 854 | user_id = self.command_interface.cmdMemReadCC26xx(FCFG_USER_ID) 855 | package = { 856 | 0x00: "4x4mm", 857 | 0x01: "5x5mm", 858 | 0x02: "7x7mm", 859 | 0x03: "Wafer", 860 | 0x04: "2.7x2.7", 861 | 0x05: "7x7mm Q1", 862 | }.get(user_id[2] & 0x03, "Unknown") 863 | 864 | protocols = user_id[1] >> 4 865 | 866 | # We can now detect the exact device 867 | if wafer_id == 0xB99A: 868 | chip = self._identify_cc26xx(pg_rev, protocols) 869 | elif wafer_id == 0xB9BE: 870 | chip = self._identify_cc13xx(pg_rev, protocols) 871 | elif wafer_id == 0xBB41: 872 | chip = self._identify_cc13xx(pg_rev, protocols) 873 | self.page_size = 8192 874 | 875 | # Read flash size, calculate and store bootloader disable address 876 | self.size = ( 877 | self.command_interface.cmdMemReadCC26xx(FLASH_SIZE)[0] * self.page_size 878 | ) 879 | self.bootloader_address = self.size - ccfg_len + bootloader_dis_offset 880 | self.addr_ieee_address_secondary = ( 881 | self.size - ccfg_len + ieee_address_secondary_offset 882 | ) 883 | 884 | # RAM size 885 | ramhwopt_size = self.command_interface.cmdMemReadCC26xx(PRCM_RAMHWOPT)[0] & 3 886 | if ramhwopt_size == 3: 887 | sram = "20KB" 888 | elif ramhwopt_size == 2: 889 | sram = "16KB" 890 | else: 891 | sram = "Unknown" 892 | 893 | # Primary IEEE address. Stored with the MSB at the high address 894 | ieee_addr = self.command_interface.cmdMemReadCC26xx( 895 | addr_ieee_address_primary + 4 896 | )[::-1] 897 | ieee_addr += self.command_interface.cmdMemReadCC26xx(addr_ieee_address_primary)[ 898 | ::-1 899 | ] 900 | 901 | mdebug( 902 | 5, 903 | "%s (%s): %dKB Flash, %s SRAM, CCFG.BL_CONFIG at 0x%08X" 904 | % (chip, package, self.size >> 10, sram, self.bootloader_address), 905 | ) 906 | mdebug( 907 | 5, "Primary IEEE Address: %s" % (":".join("%02X" % x for x in ieee_addr)) 908 | ) 909 | 910 | def _identify_cc26xx(self, pg, protocols): 911 | chips_dict = { 912 | CC26xx.PROTO_MASK_IEEE: "CC2630", 913 | CC26xx.PROTO_MASK_BLE: "CC2640", 914 | CC26xx.PROTO_MASK_BOTH: "CC2650", 915 | } 916 | 917 | chip_str = chips_dict.get(protocols & CC26xx.PROTO_MASK_BOTH, "Unknown") 918 | 919 | if pg == 1: 920 | pg_str = "PG1.0" 921 | elif pg == 3: 922 | pg_str = "PG2.0" 923 | elif pg == 7: 924 | pg_str = "PG2.1" 925 | elif pg == 8 or pg == 0x0B: 926 | # CC26x0 PG2.2+ or CC26x0R2 927 | rev_minor = self.command_interface.cmdMemReadCC26xx(CC26xx.MISC_CONF_1)[0] 928 | if rev_minor == 0xFF: 929 | rev_minor = 0x00 930 | 931 | if pg == 8: 932 | # CC26x0 933 | pg_str = "PG2.%d" % (2 + rev_minor,) 934 | elif pg == 0x0B: 935 | # HW revision R2, update Chip name 936 | chip_str += "R2" 937 | pg_str = "PG%d.%d" % (1 + (rev_minor // 10), rev_minor % 10) 938 | 939 | return "%s %s" % (chip_str, pg_str) 940 | 941 | def _identify_cc13xx(self, pg, protocols): 942 | chip_str = "CC1310" 943 | if protocols & CC26xx.PROTO_MASK_IEEE == CC26xx.PROTO_MASK_IEEE: 944 | chip_str = "CC1350" 945 | 946 | if pg == 0: 947 | pg_str = "PG1.0" 948 | elif pg == 2 or pg == 3: 949 | rev_minor = self.command_interface.cmdMemReadCC26xx(CC26xx.MISC_CONF_1)[0] 950 | if rev_minor == 0xFF: 951 | rev_minor = 0x00 952 | pg_str = "PG2.%d" % (rev_minor,) 953 | 954 | return "%s %s" % (chip_str, pg_str) 955 | 956 | def erase(self): 957 | mdebug(5, "Erasing all main bank flash sectors") 958 | return self.command_interface.cmdBankErase() 959 | 960 | def read_memory(self, addr): 961 | # CC26xx COMMAND_MEMORY_READ returns contents in the same order as 962 | # they are stored on the device 963 | return self.command_interface.cmdMemReadCC26xx(addr) 964 | 965 | 966 | def query_yes_no(question, default="yes"): 967 | valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False} 968 | if default == None: 969 | prompt = " [y/n] " 970 | elif default == "yes": 971 | prompt = " [Y/n] " 972 | elif default == "no": 973 | prompt = " [y/N] " 974 | else: 975 | raise ValueError("invalid default answer: '%s'" % default) 976 | 977 | while True: 978 | sys.stdout.write(question + prompt) 979 | choice = input().lower() 980 | if default != None and choice == "": 981 | return valid[default] 982 | elif choice in valid: 983 | return valid[choice] 984 | else: 985 | sys.stdout.write("Please respond with 'yes' or 'no' " "(or 'y' or 'n').\n") 986 | 987 | 988 | # Convert the entered IEEE address into an integer 989 | def parse_ieee_address(inaddr): 990 | try: 991 | return int(inaddr, 16) 992 | except ValueError: 993 | # inaddr is not a hex string, look for other formats 994 | if ":" in inaddr: 995 | bytes = inaddr.split(":") 996 | elif "-" in inaddr: 997 | bytes = inaddr.split("-") 998 | if len(bytes) != 8: 999 | raise ValueError("Supplied IEEE address does not contain 8 bytes") 1000 | addr = 0 1001 | for i, b in zip(range(8), bytes): 1002 | try: 1003 | addr += int(b, 16) << (56 - (i * 8)) 1004 | except ValueError: 1005 | raise ValueError("IEEE address contains invalid bytes") 1006 | return addr 1007 | 1008 | 1009 | def _parse_range_values(device, values): 1010 | if len(values) and len(values) < 3: 1011 | page_addr_range = [] 1012 | try: 1013 | for value in values: 1014 | try: 1015 | if int(value) % int(device.page_size) != 0: 1016 | raise ValueError( 1017 | "Supplied addresses are not page_size: " 1018 | "{} aligned".format(device.page_size) 1019 | ) 1020 | page_addr_range.append(int(value)) 1021 | except ValueError: 1022 | if int(value, 16) % int(device.page_size) != 0: 1023 | raise ValueError( 1024 | "Supplied addresses are not page_size: " 1025 | "{} aligned".format(device.page_size) 1026 | ) 1027 | page_addr_range.append(int(value, 16)) 1028 | return page_addr_range 1029 | except ValueError: 1030 | raise ValueError("Supplied value is not a page or an address") 1031 | else: 1032 | raise ValueError("Supplied range is neither a page or address range") 1033 | 1034 | 1035 | def parse_page_address_range(device, pg_range): 1036 | """Convert the address/page range into a start address and byte length""" 1037 | values = pg_range.split(",") 1038 | page_addr = [] 1039 | # check if first argument is character 1040 | if values[0].isalpha(): 1041 | values[0].lower() 1042 | if values[0] == "p" or values[0] == "page": 1043 | if values[0] == "p": 1044 | values[1:] = device.page_to_addr(values[1:]) 1045 | elif values[0] != "a" and values[0] != "address": 1046 | raise ValueError("Prefix is neither a(address) or p(page)") 1047 | page_addr.extend(_parse_range_values(device, values[1:])) 1048 | else: 1049 | page_addr.extend(_parse_range_values(device, values)) 1050 | if len(page_addr) == 1: 1051 | return [page_addr[0], device.page_size] 1052 | else: 1053 | return [page_addr[0], (page_addr[1] - page_addr[0])] 1054 | 1055 | 1056 | def print_version(): 1057 | # Get the version using "git describe". 1058 | try: 1059 | p = Popen( 1060 | ["git", "describe", "--tags", "--match", "[0-9]*"], stdout=PIPE, stderr=PIPE 1061 | ) 1062 | p.stderr.close() 1063 | line = p.stdout.readlines()[0] 1064 | version = line.decode("utf-8").strip() 1065 | except: 1066 | # We're not in a git repo, or git failed, use fixed version string. 1067 | version = __version__ 1068 | print("%s %s" % (sys.argv[0], version)) 1069 | 1070 | 1071 | def download_firmware(conf): 1072 | # use default index_url unless specified as a parameter 1073 | if conf["index_url"] is None: 1074 | conf[ 1075 | "index_url" 1076 | ] = "https://raw.githubusercontent.com/Koenkk/Z-Stack-firmware/master/index.json" 1077 | 1078 | # grab and parse index.json from the repo 1079 | req = requests.get(url=conf["index_url"], timeout=5) 1080 | if req.status_code != 200: 1081 | raise Exception("Can not download index JSON, aborting") 1082 | fw_data = req.json() 1083 | 1084 | # check if fw_stack is specified, if not default to the one spec'd in index.json 1085 | if conf["fw_stack"] is None: 1086 | conf["fw_stack"] = fw_data["firmware_type"][conf["fw_role"]]["stack_default"] 1087 | 1088 | fw_config = fw_data["boards"][conf["board_type"]] 1089 | 1090 | # get the latest release for fw_type + fw_stack 1091 | stack_releases = fw_data["firmware_type"][conf["fw_role"]]["stack"] 1092 | fw_release = None 1093 | if len(stack_releases) > 1: 1094 | for s in stack_releases: 1095 | for stack, release in s.items(): 1096 | if stack == conf["fw_stack"]: 1097 | fw_release = release 1098 | else: 1099 | for stack, release in stack_releases: 1100 | if stack == conf["fw_stack"]: 1101 | fw_release = release 1102 | 1103 | mdebug(10, "Firmware release: %s" % fw_release) 1104 | if fw_release is None: 1105 | raise Exception("Can not find release for fw_stack specified, aborting.") 1106 | 1107 | # build firmware download url 1108 | fw_base = "{}_{}_{}".format(fw_config, conf["fw_role"], fw_release) 1109 | fw_url = "https://github.com/Koenkk/Z-Stack-firmware/blob/master/{}/{}/bin/{}.zip?raw=true".format( 1110 | conf["fw_role"], conf["fw_stack"], fw_base 1111 | ) 1112 | mdebug(5, "Firmware download URL: %s" % fw_url) 1113 | 1114 | # build download_path and remove any stale firmware files that might be lingering around 1115 | download_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fw-tmp") 1116 | if os.path.exists(download_path): 1117 | shutil.rmtree(download_path) 1118 | 1119 | # download firmware zip file and extract it to download_path 1120 | req_fw = requests.get(fw_url) 1121 | fw_zip = zipfile.ZipFile(io.BytesIO(req_fw.content)) 1122 | fw_zip.extractall(download_path) 1123 | 1124 | # pick the first hex file we see in download_path 1125 | # this means 1 hex file per each zip file in repo 1126 | hex_found = False 1127 | for file in os.listdir(download_path): 1128 | if file.endswith(".hex"): 1129 | hex_found = True 1130 | conf["fw_downloaded"] = os.path.join(download_path, file) 1131 | break 1132 | 1133 | if not hex_found: 1134 | raise Exception("No .hex files found in the downloaded archive, aborting.") 1135 | 1136 | mdebug(10, "fw_downloaded set to: %s" % conf["fw_downloaded"]) 1137 | 1138 | 1139 | # FIXME: add better description for new options added 1140 | def usage(): 1141 | print( 1142 | """Usage: %s [-DhqVfewvr] [-l length] [-p port] [-b baud] [-a addr] \ 1143 | [-i addr] [--bootloader-active-high] [--bootloader-invert-lines] [file.bin] 1144 | -h, --help This help 1145 | -q Quiet 1146 | -V Verbose 1147 | -f Force operation(s) without asking any questions 1148 | -e Mass erase 1149 | -E, --erase-page p/a,range Receives an address(a) range or page(p) range, 1150 | default is address(a) 1151 | eg: -E a,0x00000000,0x00001000, 1152 | -E p,1,4 1153 | -w Write 1154 | -v Verify (CRC32 check) 1155 | -r Read 1156 | -l length Length of read 1157 | -p port Serial port (default: first USB-like port in /dev) 1158 | -b baud Baud speed (default: 500000) 1159 | -a addr Target address 1160 | -i, --ieee-address addr Set the secondary 64 bit IEEE address 1161 | --bootloader-active-high Use active high signals to enter bootloader 1162 | --bootloader-invert-lines Inverts the use of RTS and DTR to enter bootloader 1163 | -D, --disable-bootloader After finishing, disable the bootloader 1164 | --board Board type, defined in the download index JSON 1165 | --role Firmware role, defined in the download index JSON 1166 | --stack Firmware stack, defined in the download index JSON 1167 | --download Download firmware release for specified board, role and stack 1168 | --index_url Index JSON URL 1169 | --version Print script version 1170 | 1171 | Examples: 1172 | ./%s -e -w -v example/main.bin 1173 | ./%s -e -w -v --ieee-address 00:12:4b:aa:bb:cc:dd:ee example/main.bin 1174 | 1175 | """ 1176 | % (sys.argv[0], sys.argv[0], sys.argv[0]) 1177 | ) 1178 | 1179 | 1180 | def run(conf): 1181 | cmd = CommandInterface() 1182 | cmd.open(conf["port"], conf["baud"]) 1183 | cmd.invoke_bootloader( 1184 | conf["bootloader_active_high"], conf["bootloader_invert_lines"] 1185 | ) 1186 | mdebug( 1187 | 5, 1188 | "Opening port %(port)s, baud %(baud)d" 1189 | % {"port": conf["port"], "baud": conf["baud"]}, 1190 | ) 1191 | if conf["write"] or conf["verify"]: 1192 | # use specified firmware file if we have not downloaded any 1193 | if conf["fw_downloaded"] == None: 1194 | mdebug(5, "Reading data from %s" % conf["fw_file"]) 1195 | firmware = FirmwareFile(conf["fw_file"]) 1196 | else: 1197 | mdebug(5, "Using downloaded firmware %s" % conf["fw_downloaded"]) 1198 | firmware = FirmwareFile(conf["fw_downloaded"]) 1199 | 1200 | mdebug(5, "Connecting to target...") 1201 | 1202 | if not cmd.sendSynch(): 1203 | raise CmdException( 1204 | "Can't connect to target. Ensure boot loader " 1205 | "is started. (no answer on synch sequence)" 1206 | ) 1207 | 1208 | # if (cmd.cmdPing() != 1): 1209 | # raise CmdException("Can't connect to target. Ensure boot loader " 1210 | # "is started. (no answer on ping command)") 1211 | 1212 | chip_id = cmd.cmdGetChipId() 1213 | chip_id_str = CHIP_ID_STRS.get(chip_id, None) 1214 | 1215 | if chip_id_str == None: 1216 | mdebug(10, " Unrecognized chip ID. Trying CC13xx/CC26xx") 1217 | device = CC26xx(cmd) 1218 | else: 1219 | mdebug(10, " Target id 0x%x, %s" % (chip_id, chip_id_str)) 1220 | device = CC2538(cmd) 1221 | 1222 | # Choose a good default address unless the user specified -a 1223 | if conf["address"] == None: 1224 | conf["address"] = device.flash_start_addr 1225 | 1226 | if conf["force_speed"] != 1 and device.has_cmd_set_xosc: 1227 | if cmd.cmdSetXOsc(): # switch to external clock source 1228 | cmd.close() 1229 | conf["baud"] = 1000000 1230 | cmd.open(conf["port"], conf["baud"]) 1231 | mdebug( 1232 | 6, 1233 | "Opening port %(port)s, baud %(baud)d" 1234 | % {"port": conf["port"], "baud": conf["baud"]}, 1235 | ) 1236 | mdebug(6, "Reconnecting to target at higher speed...") 1237 | if cmd.sendSynch() != 1: 1238 | raise CmdException( 1239 | "Can't connect to target after clock " 1240 | "source switch. (Check external " 1241 | "crystal)" 1242 | ) 1243 | else: 1244 | raise CmdException( 1245 | "Can't switch target to external clock " "source. (Try forcing speed)" 1246 | ) 1247 | 1248 | if conf["erase"]: 1249 | mdebug(5, " Performing mass erase") 1250 | if device.erase(): 1251 | mdebug(5, " Erase done") 1252 | else: 1253 | raise CmdException("Erase failed") 1254 | 1255 | if conf["erase_page"]: 1256 | erase_range = parse_page_address_range(device, conf["erase_page"]) 1257 | mdebug(5, "Erasing %d bytes at addres 0x%x" % (erase_range[1], erase_range[0])) 1258 | cmd.cmdEraseMemory(erase_range[0], erase_range[1]) 1259 | mdebug(5, " Partial erase done ") 1260 | 1261 | if conf["write"]: 1262 | # TODO: check if boot loader back-door is open, need to read 1263 | # flash size first to get address 1264 | if cmd.writeMemory(conf["address"], firmware.bytes): 1265 | mdebug(5, " Write done ") 1266 | else: 1267 | raise CmdException("Write failed ") 1268 | 1269 | if conf["verify"]: 1270 | mdebug(5, "Verifying by comparing CRC32 calculations.") 1271 | 1272 | crc_local = firmware.crc32() 1273 | # CRC of target will change according to length input file 1274 | crc_target = device.crc(conf["address"], len(firmware.bytes)) 1275 | 1276 | if crc_local == crc_target: 1277 | mdebug(5, " Verified (match: 0x%08x)" % crc_local) 1278 | else: 1279 | cmd.cmdReset() 1280 | raise Exception( 1281 | "NO CRC32 match: Local = 0x%x, " 1282 | "Target = 0x%x" % (crc_local, crc_target) 1283 | ) 1284 | 1285 | if conf["ieee_address"] != 0: 1286 | ieee_addr = parse_ieee_address(conf["ieee_address"]) 1287 | mdebug( 1288 | 5, 1289 | "Setting IEEE address to %s" 1290 | % (":".join(["%02x" % b for b in struct.pack(">Q", ieee_addr)])), 1291 | ) 1292 | ieee_addr_bytes = struct.pack("> 2): 1310 | # reading 4 bytes at a time 1311 | rdata = device.read_memory(conf["address"] + (i * 4)) 1312 | mdebug( 1313 | 5, 1314 | " 0x%x: 0x%02x%02x%02x%02x" 1315 | % ( 1316 | conf["address"] + (i * 4), 1317 | rdata[0], 1318 | rdata[1], 1319 | rdata[2], 1320 | rdata[3], 1321 | ), 1322 | "\r", 1323 | ) 1324 | f.write(rdata) 1325 | f.close() 1326 | mdebug(5, " Read done ") 1327 | 1328 | if conf["disable-bootloader"]: 1329 | device.disable_bootloader() 1330 | 1331 | cmd.cmdReset() 1332 | 1333 | 1334 | def main(): 1335 | # Build up the conf dictionary from the command line arguments 1336 | # Start with the defaults 1337 | conf = { 1338 | "port": "auto", 1339 | "baud": 500000, 1340 | "force_speed": 0, 1341 | "address": None, 1342 | "force": 0, 1343 | "erase": 0, 1344 | "write": 0, 1345 | "erase_page": 0, 1346 | "verify": 0, 1347 | "read": 0, 1348 | "len": 0x80000, 1349 | "fname": "", 1350 | "ieee_address": 0, 1351 | "bootloader_active_high": False, 1352 | "bootloader_invert_lines": False, 1353 | "disable-bootloader": 0, 1354 | "board_type": None, 1355 | "fw_role": None, 1356 | "fw_stack": None, 1357 | "download": False, 1358 | "fw_downloaded": None, 1359 | "fw_file": None, 1360 | "index_url": None, 1361 | } 1362 | 1363 | # Try parsing the command line arguments 1364 | try: 1365 | opts, args = getopt.getopt( 1366 | sys.argv[1:], 1367 | "DhqVfeE:wvrp:b:a:l:i:", 1368 | [ 1369 | "help", 1370 | "ieee-address=", 1371 | "erase-page=", 1372 | "disable-bootloader", 1373 | "bootloader-active-high", 1374 | "bootloader-invert-lines", 1375 | "version", 1376 | "board=", 1377 | "role=", 1378 | "stack=", 1379 | "download", 1380 | "index_url=", 1381 | ], 1382 | ) 1383 | except getopt.GetoptError as err: 1384 | # print help information and exit: 1385 | print(str(err)) # will print something like "option -a not recognized" 1386 | usage() 1387 | sys.exit(2) 1388 | 1389 | # default verbosity is quiet. 1390 | QUIET = 0 1391 | 1392 | for o, a in opts: 1393 | if o == "-V": 1394 | QUIET = 10 1395 | elif o == "-q": 1396 | QUIET = 0 1397 | elif o == "-h" or o == "--help": 1398 | usage() 1399 | sys.exit(0) 1400 | elif o == "-f": 1401 | conf["force"] = 1 1402 | elif o == "-e": 1403 | conf["erase"] = 1 1404 | elif o == "-w": 1405 | conf["write"] = 1 1406 | elif o == "-E" or o == "--erase-page": 1407 | conf["erase_page"] = str(a) 1408 | elif o == "-v": 1409 | conf["verify"] = 1 1410 | elif o == "-r": 1411 | conf["read"] = 1 1412 | elif o == "-p": 1413 | conf["port"] = a 1414 | elif o == "-b": 1415 | conf["baud"] = eval(a) 1416 | conf["force_speed"] = 1 1417 | elif o == "-a": 1418 | conf["address"] = eval(a) 1419 | elif o == "-l": 1420 | conf["len"] = eval(a) 1421 | elif o == "-i" or o == "--ieee-address": 1422 | conf["ieee_address"] = str(a) 1423 | elif o == "--bootloader-active-high": 1424 | conf["bootloader_active_high"] = True 1425 | elif o == "--bootloader-invert-lines": 1426 | conf["bootloader_invert_lines"] = True 1427 | elif o == "-D" or o == "--disable-bootloader": 1428 | conf["disable-bootloader"] = 1 1429 | elif o == "--version": 1430 | print_version() 1431 | sys.exit(0) 1432 | elif o == "--board": 1433 | conf["board_type"] = a 1434 | elif o == "--role": 1435 | conf["fw_role"] = a 1436 | elif o == "--stack": 1437 | conf["fw_stack"] = a 1438 | elif o == "--download": 1439 | conf["download"] = 1 1440 | elif o == "--index_url": 1441 | conf["index_url"] = a 1442 | else: 1443 | assert False, "Unhandled option" 1444 | 1445 | try: 1446 | # Sanity checks 1447 | # check for input/output file 1448 | if conf["write"] or conf["read"] or conf["verify"]: 1449 | try: 1450 | if not conf[ 1451 | "download" 1452 | ]: # skip this check if we are downloading a firmware 1453 | conf["fw_file"] = args[0] 1454 | except: 1455 | raise Exception("No file path given.") 1456 | 1457 | if conf["write"] and conf["read"]: 1458 | if not ( 1459 | conf["force"] 1460 | or query_yes_no( 1461 | "You are reading and writing to the same " 1462 | "file. This will overwrite your input file. " 1463 | "Do you want to continue?", 1464 | "no", 1465 | ) 1466 | ): 1467 | raise Exception("Aborted by user.") 1468 | if ( 1469 | (conf["erase"] and conf["read"]) 1470 | or (conf["erase_page"] and conf["read"]) 1471 | and not conf["write"] 1472 | ): 1473 | if not ( 1474 | conf["force"] 1475 | or query_yes_no( 1476 | "You are about to erase your target before " 1477 | "reading. Do you want to continue?", 1478 | "no", 1479 | ) 1480 | ): 1481 | raise Exception("Aborted by user.") 1482 | 1483 | if conf["read"] and not conf["write"] and conf["verify"]: 1484 | raise Exception("Verify after read not implemented.") 1485 | 1486 | if conf["len"] < 0: 1487 | raise Exception( 1488 | "Length must be positive but %d was provided" % (conf["len"],) 1489 | ) 1490 | 1491 | if conf["download"]: 1492 | # do we have the necesary modules to be able to download an unzip firmware? 1493 | if not have_requests and have_zipfile: 1494 | raise Exception( 1495 | "Please install Python modules requests and zipfile for the download option to work." 1496 | ) 1497 | 1498 | # do we know what board type and firmware role we are downloading for? 1499 | if not conf["board_type"] and not conf["fw_role"]: 1500 | raise Exception( 1501 | "Board type and firmware role need to be specified for the download option to work." 1502 | ) 1503 | 1504 | download_firmware(conf) 1505 | 1506 | # Try and find the port automatically 1507 | if conf["port"] == "auto": 1508 | ports = [] 1509 | 1510 | # Get a list of all USB-like names in /dev 1511 | for name in [ 1512 | "ttyACM", 1513 | "tty.usbserial", 1514 | "ttyUSB", 1515 | "tty.usbmodem", 1516 | "tty.SLAB_USBtoUART", 1517 | ]: 1518 | ports.extend(glob.glob("/dev/%s*" % name)) 1519 | 1520 | ports = sorted(ports) 1521 | 1522 | if ports: 1523 | # Found something - take it 1524 | conf["port"] = ports[0] 1525 | else: 1526 | raise Exception("No serial port found.") 1527 | 1528 | # Pass the config to run(), to invoke the CommandInterface 1529 | run(conf) 1530 | 1531 | except Exception as err: 1532 | if QUIET >= 10: 1533 | traceback.print_exc() 1534 | exit("ERROR: %s" % str(err)) 1535 | 1536 | 1537 | if __name__ == "__main__": 1538 | main() 1539 | --------------------------------------------------------------------------------