├── .gitignore ├── LICENSE ├── README.md └── stm32bl.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 pavel.revak@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # STM32BL 2 | STM32 MCU serial firmware loader. 3 | 4 | ## requirements 5 | - python - tested only with python3.x 6 | - py-serial - python library for serial port handling 7 | 8 | ## supported MCUs 9 | Probably all STM32xxxx 10 | please report any problems 11 | 12 | ## Examples: 13 | - test connection 14 | `stm32loader -p /dev/tty.SLAB_USBtoUART` 15 | 16 | - dump content of FLASH memory 17 | `stm32loader -p /dev/tty.SLAB_USBtoUART -d` 18 | 19 | - save content of FLASH memory 20 | `stm32loader -p /dev/tty.SLAB_USBtoUART -r file.bin` 21 | 22 | - write bin file to FLASH from selected address 23 | `stm32loader -p /dev/tty.SLAB_USBtoUART -a 0x08003000 -w file.bin` 24 | 25 | - mass erase, write bin file to FLASH, verify and execute application 26 | `stm32loader -p /dev/tty.SLAB_USBtoUART -m -w file.bin -f -x` 27 | 28 | - help 29 | `stm32loader -h` 30 | -------------------------------------------------------------------------------- /stm32bl.py: -------------------------------------------------------------------------------- 1 | """STM32 MCU serial firmware loader""" 2 | 3 | import time 4 | import argparse 5 | import serial 6 | 7 | 8 | VERSION_STR = "stm32bl v0.0.0" 9 | 10 | DESCRIPTION_STR = VERSION_STR + """ 11 | (c) 2016 by pavel.revak@gmail.com 12 | https://github.com/pavelrevak/stm32bl 13 | """ 14 | 15 | 16 | class Stm32BLException(Exception): 17 | """General STM32 loader exception""" 18 | 19 | class SerialException(Stm32BLException): 20 | """Serial communication Exception""" 21 | 22 | class ConnectingException(Stm32BLException): 23 | """Connecting to boot-loader exception""" 24 | 25 | class NoAnswerException(Stm32BLException): 26 | """No answer exception""" 27 | 28 | class CommandNotAllowedException(Stm32BLException): 29 | """Command not allowed exception""" 30 | 31 | class UnexpectedAnswerException(Stm32BLException): 32 | """Unexpected answer exception""" 33 | 34 | class NoAckException(Stm32BLException): 35 | """General No ACK exception""" 36 | 37 | class NoAckCommandException(NoAckException): 38 | """No ACK after command exception""" 39 | 40 | class NoAckDataException(NoAckException): 41 | """No ACK after data exception""" 42 | 43 | 44 | class Stm32bl(): 45 | """STM32 firmware loader class""" 46 | 47 | CMD_INIT = 0x7f 48 | CMD_ACK = 0x79 49 | CMD_NOACK = 0x1f 50 | CMD_GET = 0x00 51 | CMD_GET_VERSION = 0x01 52 | CMD_GET_ID = 0x02 53 | CMD_READ_MEMORY = 0x11 54 | CMD_GO = 0x21 55 | CMD_WRITE_MEMORY = 0x31 56 | CMD_ERASE = 0x43 57 | CMD_EXTENDED_ERASE = 0x44 58 | CMD_WRITE_PROTECT = 0x63 59 | CMD_WRITE_UNPROTECT = 0x73 60 | CMD_READOUT_PROTECT = 0x82 61 | CMD_READOUT_UNPROTECT = 0x92 62 | 63 | FLASH_START = 0x08000000 64 | 65 | def __init__(self, port, baudrate=19200, verbosity=1): 66 | try: 67 | self._serial_port = serial.Serial( 68 | port=port, 69 | baudrate=baudrate, 70 | parity=serial.PARITY_EVEN, 71 | stopbits=1, 72 | timeout=1 73 | ) 74 | except (FileNotFoundError, serial.serialutil.SerialException): 75 | raise SerialException("Error opening serial port: %s" % port) 76 | self._verbosity = verbosity 77 | self._connect(5) 78 | self._allowed_commands = [self.CMD_GET, ] 79 | self._boot_version = self._cmd_get() 80 | self._option_bytes = self._cmd_get_version() 81 | self._dev_id = self._cmd_get_id() 82 | 83 | @staticmethod 84 | def print_buffer(addr, data, bytes_per_line=16): 85 | """print buffer""" 86 | prev_chunk = [] 87 | same_chunk = False 88 | for i in range(0, len(data), bytes_per_line): 89 | chunk = data[i:i + bytes_per_line] 90 | if prev_chunk != chunk: 91 | print('%08x %s%s %s' % ( 92 | addr, 93 | ' '.join(['%02x' % d for d in chunk]), 94 | ' ' * (16 - len(chunk)), 95 | ''.join([chr(d) if d >= 32 and d < 127 else '.' for d in chunk]), 96 | )) 97 | prev_chunk = chunk 98 | same_chunk = False 99 | elif not same_chunk: 100 | print('*') 101 | same_chunk = True 102 | addr += len(chunk) 103 | print('%08x' % addr) 104 | 105 | def log(self, message, operation=None, level=1): 106 | """logging printing""" 107 | if self._verbosity < level: 108 | return 109 | msg = '' 110 | if level > 0: 111 | msg += ':' * level 112 | if operation: 113 | msg += '%s: ' % operation 114 | msg += message 115 | print(msg) 116 | 117 | def _write(self, data): 118 | """Write data to serial port""" 119 | self.log(":".join(['%02x' % d for d in data]), 'WR', level=3) 120 | self._serial_port.write(bytes(data)) 121 | 122 | def _read(self, cnt=1, timeout=1): 123 | """Read data from serial port""" 124 | data = [] 125 | while not data and timeout > 0: 126 | data = list(self._serial_port.read(cnt)) 127 | timeout -= 1 128 | self.log(":".join(['%02x' % d for d in data]), 'RD', level=3) 129 | return data 130 | 131 | def _reset_mcu(self): 132 | """Reset MCU""" 133 | self._serial_port.setDTR(0) 134 | time.sleep(0.1) 135 | self._serial_port.setDTR(1) 136 | time.sleep(0.2) 137 | 138 | def _connect(self, repeat=1): 139 | """connect to boot-loader""" 140 | self.log("Connecting to boot-loader", level=1) 141 | self._serial_port.setRTS(0) 142 | self._reset_mcu() 143 | while repeat: 144 | self._write([self.CMD_INIT]) 145 | ret = self._read() 146 | if ret and ret[0] in (self.CMD_ACK, self.CMD_NOACK): 147 | return 148 | repeat -= 1 149 | raise ConnectingException("Can't connect to MCU boot-loader.") 150 | 151 | def exit_bootloader(self): 152 | """Exit boot-loader and restart MCU""" 153 | self._serial_port.setRTS(1) 154 | self._reset_mcu() 155 | 156 | def _talk(self, data_wr, cnt_rd, timeout=1): 157 | """talk with boot-loader""" 158 | if isinstance(data_wr, (tuple, list)): 159 | xor = data_wr[0] 160 | for i in data_wr[1:]: 161 | xor ^= i 162 | data_wr.append(xor) 163 | else: 164 | data_wr = [data_wr, data_wr ^ 0xff] 165 | self._write(data_wr) 166 | res = self._read(cnt_rd, timeout=timeout) 167 | if not res: 168 | raise NoAnswerException("No answer.") 169 | return res 170 | 171 | def _send_command(self, cmd, cnt_rd=None): 172 | """send command to boot-loader""" 173 | if cmd not in self._allowed_commands: 174 | raise CommandNotAllowedException("command %02x: is not supported by this device." % cmd) 175 | if cnt_rd is None: 176 | cnt_rd = 1 177 | else: 178 | cnt_rd += 2 179 | res = self._talk(cmd, cnt_rd) 180 | if res[0] != self.CMD_ACK or res[-1] != self.CMD_ACK: 181 | raise NoAckCommandException("NoACK for command.") 182 | return res[1:-1] 183 | 184 | def _send_data(self, data, cnt_rd=None, timeout=1): 185 | """send command to boot-loader""" 186 | res = self._talk(data, 1, timeout=timeout) 187 | if res[0] != self.CMD_ACK: 188 | raise NoAckDataException("NoACK for data.") 189 | if cnt_rd is not None: 190 | return self._read(cnt_rd, timeout=timeout) 191 | 192 | @staticmethod 193 | def _convert_version(ver): 194 | return 'v%d.%d' % (ver // 16, ver % 16) 195 | 196 | @staticmethod 197 | def _convert_32bit(val): 198 | return [ 199 | val >> 24, 200 | 0xff & (val >> 16), 201 | 0xff & (val >> 8), 202 | 0xff & val, 203 | ] 204 | 205 | @staticmethod 206 | def _convert_16bit(val): 207 | return [ 208 | val >> 8, 209 | 0xff & val, 210 | ] 211 | 212 | def _cmd_get(self): 213 | """Gets the version and the allowed commands supported 214 | by the current version of the boot-loader""" 215 | self.log("CMD_GET", level=2) 216 | res = self._send_command(self.CMD_GET, 13) 217 | if len(res) - 2 != res[0]: 218 | raise UnexpectedAnswerException("CMD_GET command: wrong result length.") 219 | boot_version = self._convert_version(res[1]) 220 | self.log(boot_version, 'BOOT_VERSION', level=1) 221 | # update list of allowed commands 222 | self._allowed_commands = res[2:] 223 | return boot_version 224 | 225 | def _cmd_get_version(self): 226 | """Gets the boot-loader version and the Read Protection 227 | status of the Flash memory""" 228 | self.log("CMD_GET_VERSION", level=2) 229 | res = self._send_command(self.CMD_GET_VERSION, 3) 230 | if len(res) != 3: 231 | raise UnexpectedAnswerException("CMD_GET_VERSION: wrong length of result") 232 | boot_version = self._convert_version(res[0]) 233 | if boot_version != self._boot_version: 234 | raise UnexpectedAnswerException("Version between GET and GET_VERSION are different.") 235 | option_bytes = res[1:] 236 | self.log(":".join(['%02x' % i for i in option_bytes]), 'OPTION_BYTES', level=1) 237 | return option_bytes 238 | 239 | def _cmd_get_id(self): 240 | """Gets the chip ID""" 241 | self.log("CMD_GET_ID", level=2) 242 | res = self._send_command(self.CMD_GET_ID, 3) 243 | if len(res) - 2 != res[0]: 244 | raise UnexpectedAnswerException("CMD_GET_ID: wrong result length.") 245 | dev_id = (res[1] << 8) + res[2] 246 | self.log("%04x" % dev_id, 'DEV_ID', level=1) 247 | return dev_id 248 | 249 | def _cmd_read_memory(self, address, length): 250 | """Reads up to 256 bytes of memory starting from an 251 | address specified by the application""" 252 | self.log("CMD_READ_MEMORY(%08x, %d)" % (address, length), level=2) 253 | self._send_command(self.CMD_READ_MEMORY) 254 | self._send_data(self._convert_32bit(address)) 255 | return self._send_data(length - 1, length) 256 | 257 | def cmd_go(self, address): 258 | """Jumps to user application code located in the internal 259 | Flash memory or in SRAM""" 260 | self.log("CMD_GO", level=2) 261 | self._send_command(self.CMD_GO) 262 | self._send_data(self._convert_32bit(address)) 263 | 264 | def _cmd_write_memory(self, address, data): 265 | """Writes up to 256 bytes to the RAM or Flash memory 266 | starting from an address specified by the application""" 267 | self.log("CMD_WRITE_MEMORY(%08x, %d)" % (address, len(data)), level=2) 268 | self._send_command(self.CMD_WRITE_MEMORY) 269 | self._send_data(self._convert_32bit(address)) 270 | return self._send_data([len(data) - 1] + data) 271 | 272 | def _cmd_erase(self, pages=0xff): 273 | """Erases from one to all the Flash memory pages""" 274 | self.log("CMD_ERASE(%d)" % pages, level=2) 275 | self._send_command(self.CMD_ERASE) 276 | if isinstance(pages, (list, tuple)): 277 | data = [len(pages) - 1] 278 | for page in pages: 279 | data.append(page) 280 | else: 281 | data = pages 282 | self._send_data(data, timeout=20) 283 | 284 | def _cmd_extended_erase(self, pages=0xffff): 285 | """Erases from one to all the Flash memory pages using 286 | two byte addressing mode (available only for v3.0 usart 287 | bootloader versions and above)""" 288 | self.log("CMD_EXTENDED_ERASE", level=2) 289 | self._send_command(self.CMD_EXTENDED_ERASE) 290 | if isinstance(pages, (list, tuple)): 291 | data = self._convert_16bit(len(pages) - 1) 292 | for page in pages: 293 | data += self._convert_16bit(page) 294 | else: 295 | data = self._convert_16bit(0xffff) 296 | self._send_data(data, timeout=20) 297 | 298 | def cmd_write_protect(self, sectors): 299 | """Enables the write protection for some sectors""" 300 | self.log("CMD_WRITE_PROTECT", level=2) 301 | data = [len(sectors) - 1] 302 | for sector in sectors: 303 | data.append(sector) 304 | self._send_data(data, timeout=20) 305 | self._connect(5) 306 | 307 | def cmd_write_unprotect(self): 308 | """Disables the write protection for all Flash memory sectors""" 309 | self.log("CMD_WRITE_UNPROTECT", level=2) 310 | self._send_command(self.CMD_WRITE_UNPROTECT, 0) 311 | self._connect(5) 312 | 313 | def cmd_readout_protect(self): 314 | """Enables the read protection""" 315 | self.log("CMD_READOUT_PROTECT", level=2) 316 | self._send_command(self.CMD_READOUT_PROTECT, 0) 317 | self.log("Set readout protection, device is restarted", level=1) 318 | self._connect(5) 319 | 320 | def cmd_readout_unprotect(self): 321 | """Disables the read protection""" 322 | self.log("CMD_READOUT_UNPROTECT", level=2) 323 | self._send_command(self.CMD_READOUT_UNPROTECT, 0) 324 | self.log("Removed readout protection, device is restarted", level=1) 325 | self._connect(5) 326 | 327 | def read_memory(self, address, size=None): 328 | """read memory""" 329 | mem = [] 330 | if size is None: 331 | self.log("address=0x%08x" % address, 'READ_MEMORY', level=1) 332 | while True: 333 | try: 334 | mem += self._cmd_read_memory(address, 256) 335 | except NoAckDataException: 336 | self._read() 337 | break 338 | address += 256 339 | self.log("done (%d Bytes)" % len(mem), 'READ_MEMORY', level=1) 340 | else: 341 | self.log("from 0x%08x (%d Bytes)" % (address, size), 'READ_MEMORY', level=1) 342 | while size > 0: 343 | _rd_size = size 344 | if size > 256: 345 | _rd_size = 256 346 | size -= _rd_size 347 | mem += self._cmd_read_memory(address, _rd_size) 348 | address += _rd_size 349 | self.log("done", 'READ_MEMORY', level=1) 350 | return mem 351 | 352 | def write_memory(self, address, data): 353 | """write memory""" 354 | self.log("from 0x%08x (%d Bytes)" % (address, len(data)), 'WRITE_MEMORY', level=1) 355 | _data = data[:] 356 | while _data: 357 | self._cmd_write_memory(address, _data[:256]) 358 | address += 256 359 | _data = _data[256:] 360 | self.log("done", 'WRITE_MEMORY', level=1) 361 | 362 | def write_file(self, address, file_name, verify=False): 363 | """Write file and or verify""" 364 | binfile = open(file_name, 'rb') 365 | mem = list(binfile.read()) 366 | size = len(mem) 367 | if size % 4: 368 | mem += [0] * (size % 4) 369 | size = len(mem) 370 | self.write_memory(address, mem) 371 | if not verify: 372 | return 373 | addr = address 374 | mem_verify = self.read_memory(address, size) 375 | _errors = 0 376 | for data_a, data_b in zip(mem, mem_verify): 377 | if data_a != data_b: 378 | if _errors < 10: 379 | self.log("0x%08x: 0x%02x != 0x%02x" % (addr, data_a, data_b), 'VERIFY', level=0) 380 | _errors += 1 381 | addr += 1 382 | if _errors >= 10: 383 | self.log(".. %d errors" % _errors, 'VERIFY', level=0) 384 | else: 385 | self.log("OK", 'VERIFY', level=1) 386 | 387 | def mass_erase(self): 388 | """Mass erase""" 389 | self.log("MASS_ERASE", level=1) 390 | if self.CMD_ERASE in self._allowed_commands: 391 | self._cmd_erase() 392 | return 393 | try: 394 | self._cmd_extended_erase() 395 | except NoAckException: 396 | # some chips don't support mass erase 397 | # protect and unprotect also make chip erase 398 | try: 399 | self.cmd_readout_protect() 400 | except NoAckException: 401 | # chip is already protected 402 | pass 403 | self.cmd_readout_unprotect() 404 | 405 | def erase_blocks(self, blocks): 406 | """Mass erase""" 407 | blocks = sorted(set(blocks)) 408 | self.log(",".join([str(b) for b in blocks]), 'ERASE_BLOCKS', level=1) 409 | if self.CMD_ERASE in self._allowed_commands: 410 | self._cmd_erase(blocks) 411 | return 412 | self._cmd_extended_erase(blocks) 413 | 414 | 415 | def main(): 416 | """Main application""" 417 | parser = argparse.ArgumentParser(description=DESCRIPTION_STR) 418 | parser.add_argument('-V', '--version', action='version', version=VERSION_STR) 419 | parser.add_argument('-v', '--verbose', action='count', help="increase verbosity *", default=0) 420 | parser.add_argument('-p', '--port', help="Serial port eg: /dev/ttyS0 or COM1", required=True) 421 | parser.add_argument('-b', '--baud', help="Baud-rate (9600 - 115200)", default=115200) 422 | parser.add_argument('-a', '--address', help="Set address for reading or writing") 423 | parser.add_argument('-s', '--size', help="Set size for reading") 424 | parser.add_argument('-r', '--read', help="Read content of memory to file") 425 | parser.add_argument('-d', '--dump', action='store_true', help="Dump content of memory") 426 | parser.add_argument('-m', '--mass-erase', action='store_true', help="Mass erase before writing") 427 | parser.add_argument('-e', '--erase-block', type=int, action='append', help="Erase block *") 428 | parser.add_argument('-w', '--write', action='append', help="Write file to memory *") 429 | parser.add_argument('-f', '--verify', action='store_true', help="Verify after writing") 430 | parser.add_argument('-x', '--execute', action='store_true', help="Start application") 431 | parser.add_argument('-t', '--reset', action='store_true', help="Reset MCU and exit boot-loader") 432 | parser.add_argument('-W', '--write-protect', type=int, action='append', help="WP sector *") 433 | parser.add_argument('-U', '--write-unprotect', action='store_true', help="Write unprotect all") 434 | parser.add_argument('-R', '--read-protect', action='store_true', help="Read Protect") 435 | parser.add_argument('-T', '--read-unprotect', action='store_true', help="Read unprotect") 436 | args = parser.parse_args() 437 | address = int(args.address, 0) if args.address is not None else Stm32bl.FLASH_START 438 | size = int(args.size, 0) if args.size is not None else None 439 | 440 | try: 441 | stm32bl = Stm32bl(port=args.port, baudrate=args.baud, verbosity=args.verbose) 442 | if args.read_unprotect: 443 | stm32bl.cmd_readout_unprotect() 444 | if args.write_unprotect: 445 | stm32bl.cmd_write_unprotect() 446 | if args.dump or args.read: 447 | mem = stm32bl.read_memory(address, size) 448 | if args.dump: 449 | stm32bl.print_buffer(address, mem) 450 | if args.read: 451 | binfile = open(args.read, 'wb') 452 | binfile.write(bytes(mem)) 453 | if args.mass_erase: 454 | stm32bl.mass_erase() 455 | elif args.erase_block: 456 | stm32bl.erase_blocks(args.erase_block) 457 | if args.write: 458 | stm32bl.write_file(address, args.write[0], args.verify) 459 | if args.write_protect: 460 | stm32bl.cmd_write_protect(args.write_protect) 461 | if args.read_protect: 462 | stm32bl.cmd_readout_protect() 463 | if args.execute: 464 | stm32bl.cmd_go(address) 465 | if args.reset: 466 | stm32bl.exit_bootloader() 467 | except Stm32BLException as err: 468 | print("ERROR: %s" % err) 469 | 470 | if __name__ == "__main__": 471 | main() 472 | --------------------------------------------------------------------------------