diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..179590b --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,48 @@ +name: Lint + +on: + - push + - pull_request + +defaults: + run: + shell: bash + +jobs: + ruff: + strategy: + matrix: + os: + - ubuntu-latest + python-version: + - "3.14" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install ruff + run: pip install ruff + - name: Run ruff format check + run: ruff format --check src/stm32loader + - name: Run ruff lint check + run: ruff check src/stm32loader + pylint: + strategy: + fail-fast: true + matrix: + os: + - ubuntu-latest + python-version: + - "3.14" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install lint dependencies + run: pip install pylint pyserial intelhex + - name: Run pylint + run: pylint src/stm32loader diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..19b025b --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,38 @@ +name: Test + +on: + - push + - pull_request + +defaults: + run: + shell: bash + +jobs: + pytest: + strategy: + fail-fast: true + matrix: + os: + - ubuntu-latest + - windows-latest + python-version: + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + - "pypy-3.9" + - "pypy-3.10" + - "pypy-3.11" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install test dependencies + run: pip install tox tox-gh-actions pyserial pytest intelhex + - name: Run setup and tests as defined in tox.ini + run: tox diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09e66b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +build/ +dist/ +*.egg-info/ +.tox/ +.venv + +__pycache__/ +*.py[cod] +.nox/ +uv.lock diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..0e18dc7 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,16 @@ +[FORMAT] +max-line-length=98 + +[MESSAGES CONTROL] +disable= + fixme, # TO DOs are not errors. + consider-using-f-string, # We're not on Python >= 3.6 yet. + +[REPORT] +score=no + +[BASIC] +good-names= + i, + e, + namespace, diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ad3d1ee --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +language: python + +matrix: + include: + # Lint using nox on native Python 3.6 + - python: "3.6" + env: NOXSESSION="lint" + + # Test using nox on native Python 3.5/3.6/3.7/3.8 + - python: "3.5" + env: NOXSESSION="tests-3.5" + - python: "3.6" + env: NOXSESSION="tests-3.6" + - python: "3.7" + env: NOXSESSION="tests-3.7" + dist: xenial # necessary for Python 3.7 + sudo: required # necessary for Python 3.7 + - python: "3.8" + env: NOXSESSION="tests-3.8" + dist: xenial # necessary for Python 3.8 + sudo: required # necessary for Python 3.8? + + # Test using nox on non-native Python version 3.6 + # (nox does not natively run on 3.4) + - python: "3.6" + env: NOXSESSION="tests-3.4" + +install: + - pip install --upgrade pip setuptools nox pyserial progress + - pip install . + +script: nox --session "$NOXSESSION" diff --git a/COPYING3 b/LICENSE similarity index 100% rename from COPYING3 rename to LICENSE diff --git a/README b/README deleted file mode 100644 index ac096dc..0000000 --- a/README +++ /dev/null @@ -1,29 +0,0 @@ -STM32Loader -=========== - -Python script which will talk to the STM32 bootloader to upload and download firmware. - -Original Version by: Ivan A-R - - -Usage: ./stm32loader.py [-hqVewvr] [-l length] [-p port] [-b baud] [-a addr] [file.bin] - -h This help - -q Quiet - -V Verbose - -e Erase - -w Write - -v Verify - -r Read - -l length Length of read - -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) - -b baud Baud speed (default: 115200) - -a addr Target address - - ./stm32loader.py -e -w -v example/main.bin - - -Example: -stm32loader.py -e -w -v somefile.bin - -This will pre-erase flash, write somefile.bin to the flash on the device, and then perform a verification after writing is finished. - diff --git a/README.md b/README.md new file mode 100644 index 0000000..cab735b --- /dev/null +++ b/README.md @@ -0,0 +1,192 @@ +# STM32Loader + +[![PyPI package](https://badge.fury.io/py/stm32loader.svg)](https://badge.fury.io/py/stm32loader) +[![Test](https://github.com/florisla/stm32loader/actions/workflows/test.yaml/badge.svg)](https://github.com/florisla/stm32loader/actions/workflows/test.yaml) +[![Lint](https://github.com/florisla/stm32loader/actions/workflows/lint.yaml/badge.svg)](https://github.com/florisla/stm32loader/actions/workflows/lint.yaml) +[![License](https://img.shields.io/pypi/l/stm32loader.svg)](https://pypi.org/project/stm32loader/) +[![Downloads](https://pepy.tech/badge/stm32loader)](https://pepy.tech/project/stm32loader) + +Python module to upload or download firmware to / from +ST Microelectronics STM32 microcontrollers over UART. + +Also supports ST BlueNRG devices, and the SweetPeas bootloader +for Wiznet W7500. + +Compatible with Python version 3.9-3.14 and PyPy 3.10-3.11. + + +## Run using `uvx` + + uvx stm32loader + + +## Installation with pip + + pip install stm32loader + +To install the latest development version: + + pip install git+https://github.com/florisla/stm32loader.git + + +## Usage + + +``` +usage: stm32loader [-h] [-e] [-u] [-x] [-w] [-v] [-r] [-l LENGTH] -p PORT [-b BAUD] [-a ADDRESS] [-g ADDRESS] [-f FAMILY] [-V] [-q] [-s] [-R] [-B] [-n] [-P {even,none}] [--version] [FILE.BIN] + +Flash firmware to STM32 microcontrollers. + +positional arguments: + FILE.BIN File to read from or store to flash. + +options: + -h, --help show this help message and exit + -e, --erase Erase the full flash memory or a specific region (support --address and --length). Note: this is required on previously written memory. + -u, --unprotect Unprotect flash from readout. + -x, --protect Protect flash against readout. + -w, --write Write file content to flash. + -v, --verify Verify flash content versus local file (recommended). + -r, --read Read from flash and store in local file. + -l, --length LENGTH Length of read or erase. + -p, --port PORT Serial port (default: $STM32LOADER_SERIAL_PORT). + -b, --baud BAUD Baudrate. (default: 115200) + -a, --address ADDRESS + Target address for read or write. For erase, this is used when you supply --length. (default: 134217728) + -g, --go-address ADDRESS + Start executing from address (0x08000000, usually). + -f, --family FAMILY Device family to read out device UID and flash size; e.g F1 for STM32F1xx. Possible values: F0, F1, F3, F4, F7, H7, L4, L0, G0, G4, NRG. (default: $STM32LOADER_FAMILY). + -V, --verbose Verbose mode. + -q, --quiet Quiet mode. + -s, --swap-rts-dtr Swap RTS and DTR: use RTS for reset and DTR for boot0. + -R, --reset-active-high + Make RESET active high. + -B, --boot0-active-low + Make BOOT0 active low. + -n, --no-progress Don't show progress bar. + -P, --parity {even,none} + Parity: "even" for STM32, "none" for BlueNRG. (default: even) + --version show program's version number and exit + +examples: + stm32loader --port COM7 --family F1 + stm32loader --erase --write --verify example/main.bin +``` + + +## Command-line example + +``` +stm32loader --port /dev/tty.usbserial-ftCYPMYJ --erase --write --verify somefile.bin +``` + +This will pre-erase flash, write `somefile.bin` to the flash on the device, and then +perform a verification after writing is finished. + +You can skip the `--port` option by configuring environment variable +`STM32LOADER_SERIAL_PORT`. +Similarly, `--family` may be supplied through `STM32LOADER_FAMILY`. + +To read out firmware and store it in a file: + +``` +stm32loader --read --port /dev/cu.usbserial-A5XK3RJT --family F1 --length 0x10000 --address 0x08000000 dump.bin +``` + + +To erase the full device: + +``` +stm32loader --erase --port /dev/cu.usbserial-A5XK3RJT +``` + +Or erase only a specific region of the flash: + +``` +stm32loader --erase --address 0x08000000 --length 0x2000 --port /dev/cu.usbserial-A5XK3RJT +``` + + + +## Reference documents + +* ST `AN2606`: STM32 microcontroller system memory boot mode +* ST `AN3155`: USART protocol used in the STM32 bootloader +* ST `AN4872`: BlueNRG-1 and BlueNRG-2 UART bootloader protocol + + +## Acknowledgement + +Original Version by Ivan A-R (tuxotronic.org). +Contributions by Domen Puncer, James Snyder, Floris Lambrechts, +Atokulus, sam-bristow, NINI1988, Omer Kilic, Szymon Szantula, rdaforno, +Mikolaj Stawiski, Tyler Coy, Alex Klimaj, Ondrej Mikle, denniszollo, +emilzay, michsens, blueskull, Mattia Maldini, etrommer, jadeaffenjaeger, +tosmaz. + +Inspiration for features from: + +* Configurable RTS/DTR and polarity, extended erase with pages: + https://github.com/pazzarpj/stm32loader + +* Memory unprotect + https://github.com/3drobotics/stm32loader + +* Correct checksum calculation for paged erase: + https://github.com/jsnyder/stm32loader/pull/4 + +* ST BlueNRG chip support + https://github.com/lchish/stm32loader + +* Wiznet W7500 chip / SweetPeas custom bootloader support + https://github.com/Sweet-Peas/WiznetLoader + + +## Alternatives + +If you don't need the flexibility of a Python tool, you can take +a look at other similar tools in `ALTERNATIVES.md`. + + +## Electrically + +The below assumes you are connecting an STM32F10x. +For other chips, the serial pins and/or the BOOT0 / BOOT1 values +may differ. + +Make the following connections: + +- Serial adapter `GND` to MCU `GND`. +- Serial adapter power to MCU power or vice versa (either 3.3 or 5 Volt). +- Note if you're using 5 Volt signaling or 3V3 on the serial adapter. +- Serial `TX` to MCU `RX` (`PA10`). +- Serial `RX` to MCU `TX` (`PA9`). +- Serial `DTR` to MCU `RESET`. +- Serial `RTS` to MCU `BOOT0` (or `BOOT0` to 3.3V). +- MCU `BOOT1` to `GND`. + +If either `RTS` or `DTR` are not available on your serial adapter, you'll have to +manually push buttons or work with jumpers. +When given a choice, set `BOOT0` manually high and drive `RESET` through the serial +adapter (it needs to toggle, whereas `BOOT0` does not). + + +## Not currently supported + +* Command-line argument for write protection for some devices (e.g. those with dual-bank flash). +* STM8 devices (ST `UM0560`). +* Other bootloader protocols (e.g. I2C, HEX -> implemented in `stm32flash`). diff --git a/docs/ALTERNATIVES.md b/docs/ALTERNATIVES.md new file mode 100644 index 0000000..557bf4d --- /dev/null +++ b/docs/ALTERNATIVES.md @@ -0,0 +1,65 @@ + +# Alternative tools + + +## Stm32CubeProgrammer + +Official cross-platform tool by ST, with GUI and CLI versions. + +Supports all bootloader protocols (UART, I2C, SPI, USB DFU, CAN). +Supports OTP memory and option bytes. + +https://www.st.com/en/development-tools/stm32cubeprog.html + + +## STM32 Flash loader demonstrator (Flasher-stm32) + +Windows tool by ST. + +It comes with a command-line version and source code. + +https://www.st.com/en/development-tools/flasher-stm32.html + + +## STM32 ST-Link utility + +This is replaced by Stm32CubProgrammer (see above). + +https://www.st.com/en/development-tools/stsw-link004.html + + +## stm32flash + +Open source flashing tool in C. + +Supports UART and SPI, and covers many STM32 devices. +This is probably a good choice if you're looking for speed. + +https://sourceforge.net/projects/stm32flash/ + + +## ST-flash + +This is open re-implementation of the ST-Link tools. + +https://github.com/stlink-org/stlink st-flash + + +## stm32flash-lib + +Java library allowing to flash STM32 microcontrollers over UART. + +https://github.com/grevaillot/stm32flash-lib + + +## stm32isp.py + +Simple, more limited too to flash STM32 over UART in Python. + +https://github.com/wagiminator/MCU-Flash-Tools?tab=readme-ov-file#stm32isp + + +## GigaDevice tools + +GigaDevice offers the GD-Link Programmer to work with the GD-Link debug adapter, +and GigaDevice MCU ISP Programmer. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..a0d76a8 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,170 @@ + +# Changelog +What changed in which version. + + +## vnext + +### Changed +* `#91` Drop the `--family` argument; do auto-detect instead. +* `#90` Move docs to `docs` folder. + +## [0.8.0] - TBD + +### Added +* `#81` Support STM32G4 family. +* `#87` Officially support Python 3.12/3.13/3.14, PyPy 3.10/3.11. +* `#69` Print details about erased pages. + +## Changed +* `#88` Use uv as build backend. +* `#88` Use uv in usage examples. +* `#71` Use `ruff` as formatter. + + +## [0.7.1] - 2023-10-18 + + +### Added +* `#67` Support STM32H750. + +### Fixed +* Integer division error when erasing specific region of flash. +* Print erase/write/verify in correct order. + +### Cleaned +* Extract method for range-to-pages calculation. + + +## [0.7.0] - 2023-10-12 + +### Added +* Support ST BlueNRG-1 and BlueNRG-2 devices. +* Support ST STM32H7 series devices. +* Allow to erase specific pages of flash memory. +* Add command-line switch to protect flash against readout. +* Support Intel hex file format. +* Adopt `flit` as build system. +* Adopt `bump-my-version` as version bumper. + +### Fixed +* Erasing was impossible due to --length not being supplied. + +### Cleaned +* Move argument-parsing code to separate file. +* Use long-form argument names in help text and error messages. +* Use IntEnum for commands and responses. + + + +## [0.6.0] - 2023-10-09 + +Yanked on 2023-10-12 due to bug when erasing. Use 0.7.0 instead. + +### Added +* `#59` Continuous Integration: start running tests and linters on GitHub Actions. +* `#42` `#43` Find flash size for non-standard MCUs (F4, L0). +* Support STM32H7 series. +* Packaging: auto-generate the help output using `cog`. +* Support STM32WL. +* Support Python 3.9 - 3.11. + +### Changed +* `#46` `#48` Flush the UART read buffer after MCU reset. +* Use argparse instead of optparse. +* Drop support for Python 2, 3.4 - 3.8. + +### Fixed +* `#44` Support flash page size higher than 255. +* `#64` Properly parse address and length given as hexadecimal value. +* `#62` Properly pass device family argument. + +### Documented +* `#13` Describe how to extend Stm32Loader. +* `#52` Describe alternative ways to execute the module. +* `#58` Add a list of similar tools. + + +## [0.5.1] - 2019-12-31 +* `#25` Fix bug: Mass memory erase by byq77. +* `#28` Add support for STM32L4 by rdaforno. +* `#29` Add support for more STM32F0 ids by stawiski . +* `#30` Add support for STM32F3 by float32. +* `#32` Add support for STM32G0x1 by AlexKlimaj. +* `#33` More robust bootloader activation by hiviah. +* `#35` Support Python 3.8 +* `#20` Add a 'read flash' example to README +* `#34` Add --version argument + + +## [0.5.0] - 2019-05-02 +* `#17` Add support for STM32F03xx4/6 by omerk. +* Drop support for Python 3.2 and 3.3. + + +## [0.4.0] - 2019-04-19 +* `#8`: Add support for STM32F7 mcus. By sam-bristow. +* `#9`: Support data writes smaller than 256 bytes. By NINI1988. +* `#10`: Make stm32loader useful as a library. +* `#4`: Bring back support for progress bar. +* `#12`: Allow to supply the serial port as an environment variable. +* `#11`: Support paged erase in extended (two-byte addressing) erase mode. + Note: this is not yet tested on hardware. +* Start using code linting and unit tests. +* Start using Continuous Integration (Travis CI). + + +## [0.3.3] - 2018-08-08 +* Bugfix: write data, not [data]. By Atokulus. + + +## [0.3.2] - 2018-07-31 +* Publish on Python Package Index. +* Make stm32loader executable as a module. +* Expose stm32loader as a console script (stm32loader.exe on Windows). + + +## [0.3.1] -- 2018-07-31 +* Make stm32loader installable and importable as a package. +* Make write_memory faster (by Atokulus, see `#1`). + + +## [0.3.0] - 2018-04-27 +* Add version number. +* Add this changelog. +* Improve documentation. +* Support ST BlueNRG devices (configurable parity). +* Add Wiznet W7500 / SweetPeas bootloader chip ID. +* Fix ack-related bugs in (un)protect methods. +* Add 'unprotect' command-line option. +* Read device UID. +* Read device flash size. +* Refactor __main__ functionality into methods. + + +## 2018-05 +* Make RTS/DTR (boot0/reset) configurable (polarity, swap). + + +## 2018-04 +* Restore Python 2 compatibility. + + +## 2018-03 +* Add support for Python 3. +* Remove Psyco and progressbar support. +* Fix checksum calculation bug for paged erase. + + +## 2014-04 +* Add `-g
` (GO command). +* Add known chip IDs. +* Implement extended erase for STM32 F2/F4. + + +## 2013-10 +* Add Windows compatibility. + + +## 2009-04 +* Add GPL license. diff --git a/docs/DEVELOP.md b/docs/DEVELOP.md new file mode 100644 index 0000000..2d1e2f7 --- /dev/null +++ b/docs/DEVELOP.md @@ -0,0 +1,65 @@ + +# How to contribute + + +## Installation + +Checkout the latest master. + + git clone https://github.com/florisla/stm32loader.git + +Using `uv` you can simply run the tool + + uv run stm32loader + +Otherwise, install in editable mode with development tools (preferable in a virtual +environment). + + python -m venv .venv + .\.venv\bin\activate + pip uninstall stm32loader + pip install --editable .[dev] + + +## Testing + +Run pytest. + + uv run pytest . + + +## Linting + +Run ruff and pylint. + + uv run ruff check . + uv run ruff format --check . + uv run pylint . + + +## Updating --help info in the README + + uv run cog -r README.md + + +## Commit messages + +I try to follow the 'conventional commits' commit message style; +see https://www.conventionalcommits.org/ . + + +## Bump the version number + + bump-my-version --new-version 1.0.8-dev bogus-part + + +## Tag a release + +First, bump the version number to a release version. +Then create the git tag. + + git tag -a "v1.0.9" -m "release: Tag version v1.0.9" + +Also push it to upstream. + + git push origin v1.0.9 diff --git a/docs/EXTEND.md b/docs/EXTEND.md new file mode 100644 index 0000000..059d503 --- /dev/null +++ b/docs/EXTEND.md @@ -0,0 +1,64 @@ + +# Extending stm32loader + +You can create your own extensions on top of stm32loader's classes. + + +## Example: Use Raspberry Pi GPIO pins to toggle `BOOT0` and `RESET` + +Subclass the `SerialConnection` and override `enable_reset` and `enable_boot0`. + +```python3 + +from RPi import GPIO +from stm32loader.uart import SerialConnection + + +class RaspberrySerialWithGpio(SerialConnection): + # Configure which GPIO pins are connected to the STM32's BOOT0 and RESET pins. + BOOT0_PIN = 2 + RESET_PIN = 3 + + def __init__(self, serial_port, baud_rate, parity): + super().__init__(serial_port, baude_rate, parity) + + GPIO.setup(self.BOOT0_PIN, GPIO.OUT, initial=GPIO.LOW) + GPIO.setup(self.RESET_PIN, GPIO.OUT, initial=GPIO.HIGH) + + def enable_reset(self, enable=True): + """Enable or disable the reset IO line.""" + # Reset is active low. + # To enter reset, write a 0. + level = 1 - int(enable) + + GPIO.output(self.RESET_PIN, level) + + def enable_boot0(self, enable=True): + """Enable or disable the boot0 IO line.""" + level = int(enable) + + GPIO.output(self.BOOT0_PIN, level) +``` + +Connect to the UART and instantiate a Bootloader object. + +```python3 + +from stm32loader.bootloader import Stm32Bootloader + +from raspberrystm32 import RaspberrySerialWithGpio + + +connection = RaspberrySerialWithGpio("/dev/cu.usbserial-A5XK3RJT") +connection.connect() +stm32 = Stm32Bootloader(connection, device_family="F1") +``` + +Now you can use all of the Stm32Bootloader methods. + +```python3 +stm32.reset_from_system_memory() +print(stm32.get_version()) +print(stm32.get_id()) +print(stm32.get_flash_size()) +``` diff --git a/docs/RUN.md b/docs/RUN.md new file mode 100644 index 0000000..f33ec11 --- /dev/null +++ b/docs/RUN.md @@ -0,0 +1,52 @@ + +# Running stm32loader + + +## Execute as a module + +After installing stm32loader with `pip`, it's available as a Python module. + +You can execute this with `python -m [modulename]`. + +```shell +python3 -m stm32loader +``` + + +## Execute as a module without installing + +You can also run `stm32loader` without installing it. You do need `pyserial` though. + +Make sure you are in the root of the repository, or the repository is in `PYTHONPATH`. + +```shell +python3 -m pip install pyserial --user +python3 -m stm32loader +``` + + +## Execute main.py directly + +The file `main.py` also runs the `stm32loader` program when executed. +Make sure the module can be found; add the folder of the repository to `PYTHONPATH`. + +```shell +PYTHONPATH=. python3 stm32loader/main.py +``` + + +## Use from Python + +You can use the classes of `stm32loader` from a Python script. + +Example: + +```python +from stm32loader.main import Stm32Loader + +loader = Stm32Loader() +loader.configuration.port = "/dev/cu.usbserial-A5XK3RJT" +loader.connect() +loader.stm32.readout_unprotect() +loader.disconnect() +``` diff --git a/firmware/generic_boot20_pc13.binary.bin b/firmware/generic_boot20_pc13.binary.bin new file mode 100644 index 0000000..01173aa Binary files /dev/null and b/firmware/generic_boot20_pc13.binary.bin differ diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..25afcaf --- /dev/null +++ b/noxfile.py @@ -0,0 +1,34 @@ +""" +Run unit tests in a fresh virtualenv using nox. + +Usage: + + uv run nox +""" + +from nox import Session, options +from nox_uv import session + +options.default_venv_backend = "uv" + + +PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy-3.10", "pypy-3.11"] +DEFAULT_PYTHON_VERSION = "3.14" + + +@session(python=PYTHON_VERSIONS, uv_groups=("test", "hex")) +def test(session: Session) -> None: + """Execute unit tests.""" + session.run("pytest") + + +@session(python=DEFAULT_PYTHON_VERSION, uv_groups=("lint",)) +def lint(session: Session) -> None: + """ + Run code verification tools ruff and pylint. + + Do this in order of expected failures for performance reasons. + """ + session.run("ruff", "format", "--check", "src/stm32loader") + session.run("ruff", "check", "src/stm32loader") + session.run("pylint", "stm32loader") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3c6737e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,133 @@ +[project] +name = "stm32loader" +description = "Flash firmware to STM32 microcontrollers using Python." +version = "1.0.0-dev1" +readme = "README.md" +authors = [ + {name = "jsnyder"}, + {name = "Floris Lambrechts", email = "florisla@gmail.com"}, +] +requires-python = ">=3.9" +license = {file = "LICENSE"} +classifiers = [ + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Natural Language :: English", + "Operating System :: OS Independent", +] +dependencies = [ + "pyserial", + "progress", +] + +[build-system] +requires = ["uv_build>=0.9.17,<0.10.0"] +build-backend = "uv_build" + +[dependency-groups] +hex = [ + "intelhex", +] +lint = [ + "ruff", + "pylint", +] +test = [ + "pytest", + "intelhex", + "nox>=2026.2.9", + "nox-uv>=0.7.1", + "tox>=4.30.3", +] +release = [ + "bump-my-version", + "cogapp", +] +dev = [ + { include-group = "test" }, + { include-group = "hex" }, + { include-group = "lint" }, + { include-group = "release" }, +] + +[project.scripts] +stm32loader = "stm32loader.__main__:main" + +[project.urls] +Home = "https://github.com/florisla/stm32loader" +BugTracker = "https://github.com/florisla/stm32loader/issues" +SourceCode = "https://github.com/florisla/stm32loader" + + +[tool.bumpversion] +commit = true +tag = true +message = "release: Bump version number from v{current_version} to v{new_version}" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(-(?P[^\\d]+)(?P\\d+))?" +serialize = [ + "{major}.{minor}.{patch}-{release}{devrelease}", + "{major}.{minor}.{patch}", +] + +[tool.bumpversion.parts.release] +optional_value = "release" +values = [ + "dev", + "release", +] + +[[tool.bumpversion.files]] +filename = "src/stm32loader/__init__.py" +parse = "\\((?P\\d+),\\s(?P\\d+),\\s(?P\\d+)(\\s*,\\s*\"(?P[^\"]+)\"\\s*,\\s*(?P\\d+))?\\)" +serialize = [ + "({major}, {minor}, {patch}, \"{release}\", {devrelease})", + "({major}, {minor}, {patch})", +] +search = "__version_info__ = {current_version}" +replace = "__version_info__ = {new_version}" + +[[tool.bumpversion.files]] +filename = "src/stm32loader/__init__.py" +search = "__version__ = \"{current_version}\"" +replace = "__version__ = \"{new_version}\"" + + +[tool.ruff] +line-length = 98 +target-version = "py39" +exclude = [ + ".git", + ".idea", + "__pycache__", + "build", + "dist", + "*.egg-info", +] + +[tool.ruff.lint] +# E/W: pycodestyle, F: pyflakes, I: isort +# Replaces flake8 + flake8-isort; ruff format replaces black +select = ["E", "F", "W", "I"] + +[tool.ruff.lint.pycodestyle] +max-doc-length = 78 + +[tool.ruff.format] +docstring-code-format = true + + +[tool.pytest.ini_options] +addopts = "--strict-markers -m 'not (hardware or hardware_missing)'" +markers = [ + "hardware", + "missing_hardware", +] diff --git a/src/stm32loader/.gitignore b/src/stm32loader/.gitignore new file mode 100644 index 0000000..6937180 --- /dev/null +++ b/src/stm32loader/.gitignore @@ -0,0 +1 @@ +.vscode/settings.json \ No newline at end of file diff --git a/src/stm32loader/__init__.py b/src/stm32loader/__init__.py new file mode 100644 index 0000000..d81accd --- /dev/null +++ b/src/stm32loader/__init__.py @@ -0,0 +1,4 @@ +"""Flash firmware to STM32 microcontrollers over a serial connection.""" + +__version_info__ = (1, 0, 0, "dev", 1) +__version__ = "1.0.0-dev1" diff --git a/src/stm32loader/__main__.py b/src/stm32loader/__main__.py new file mode 100644 index 0000000..664665d --- /dev/null +++ b/src/stm32loader/__main__.py @@ -0,0 +1,41 @@ +# Author: Floris Lambrechts +# GitHub repository: https://github.com/florisla/stm32loader +# +# This file is part of stm32loader. +# +# stm32loader is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3, or (at your option) any later +# version. +# +# stm32loader is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License +# along with stm32loader; see the file LICENSE. If not see +# . +""" +Execute stm32loader as a module. + +This does exactly the same as manually calling 'python stm32loader.py'. +""" + +import sys + +from stm32loader.main import main as stm32loader_main + + +def main(): + """ + Separate main method, different from stm32loader.main. + + This way it it can be used as an entry point for a console script. + :return None: + """ + stm32loader_main(*sys.argv[1:]) + + +if __name__ == "__main__": + main() diff --git a/src/stm32loader/args.py b/src/stm32loader/args.py new file mode 100644 index 0000000..60f65d1 --- /dev/null +++ b/src/stm32loader/args.py @@ -0,0 +1,239 @@ +"""Parse command-line arguments.""" + +import argparse +import atexit +import copy +import os +import sys + +from stm32loader import __version__ + +DEFAULT_VERBOSITY = 5 + + +class HelpFormatter(argparse.RawDescriptionHelpFormatter, argparse.ArgumentDefaultsHelpFormatter): + """Custom help formatter -- don't print confusing default values.""" + + def _get_help_string(self, action): + action = copy.copy(action) + # Don't show "(default: None)" for arguments without defaults, + # or "(default: False)" for boolean flags, and hide the + # (default: 5) from --verbose's help because it's confusing. + if not action.default or action.dest == "verbosity": + action.default = argparse.SUPPRESS + return super()._get_help_string(action) + + def _format_actions_usage(self, actions, groups): + # Always treat -p/--port as required. See the note about the + # argparse hack in Stm32Loader.parse_arguments for why. + def tweak_action(action): + action = copy.copy(action) + if action.dest == "port": + action.required = True + return action + + return super()._format_actions_usage(map(tweak_action, actions), groups) # pylint: disable=no-member + + +def _auto_int(x): + """Convert to int with automatic base detection.""" + # This supports 0x10 == 16 and 10 == 10 + return int(x, 0) + + +def parse_arguments(arguments): + """Parse the given command-line arguments and return the configuration.""" + + parser = argparse.ArgumentParser( + prog="stm32loader", + description="Flash firmware to STM32 microcontrollers.", + epilog="\n".join( + [ + "examples:", + " %(prog)s --port COM7 --family F1", + " %(prog)s --erase --write --verify example/main.bin", + ] + ), + formatter_class=HelpFormatter, + ) + + data_file_arg = parser.add_argument( + "data_file", + metavar="FILE.BIN", + type=str, + nargs="?", + help="File to read from or store to flash.", + ) + + parser.add_argument( + "-e", + "--erase", + action="store_true", + help=( + "Erase the full flash memory or a specific region (support --address and --length)." + " Note: this is required on previously written memory." + ), + ) + + parser.add_argument( + "-u", "--unprotect", action="store_true", help="Unprotect flash from readout." + ) + + parser.add_argument( + "-x", "--protect", action="store_true", help="Protect flash against readout." + ) + + parser.add_argument( + "--write-unprotect", action="store_true", help="Disable write protection before flashing" + ) + + parser.add_argument( + "--write-protect", action="store_true", help="Enable write protection after flashing" + ) + + parser.add_argument("-w", "--write", action="store_true", help="Write file content to flash.") + + parser.add_argument( + "-v", + "--verify", + action="store_true", + help="Verify flash content versus local file (recommended).", + ) + + parser.add_argument( + "-r", "--read", action="store_true", help="Read from flash and store in local file." + ) + + length_arg = parser.add_argument( + "-l", "--length", action="store", type=_auto_int, help="Length of read or erase." + ) + + default_port = os.environ.get("STM32LOADER_SERIAL_PORT") + port_arg = parser.add_argument( + "-p", + "--port", + action="store", + type=str, # morally required=True + default=default_port, + help=("Serial port" + ("." if default_port else " (default: $STM32LOADER_SERIAL_PORT).")), + ) + + parser.add_argument( + "-b", "--baud", action="store", type=int, default=115200, help="Baudrate." + ) + + address_arg = parser.add_argument( + "-a", + "--address", + action="store", + type=_auto_int, + default=0x08000000, + help=( + "Target address for read or write. For erase, this is used when you supply --length." + ), + ) + + parser.add_argument( + "-g", + "--go-address", + action="store", + type=_auto_int, + metavar="ADDRESS", + help="Start executing from address (0x08000000, usually).", + ) + + default_family = os.environ.get("STM32LOADER_FAMILY") + parser.add_argument( + "-f", + "--family", + action="store", + type=str, + default=default_family, + help=( + "Device family to read out device UID and flash size; " + "e.g F1 for STM32F1xx. Possible values: F0, F1, F3, F4, F7, H7, L4, L0, G0, G4, NRG." + + ("." if default_family else " (default: $STM32LOADER_FAMILY).") + ), + ) + + parser.add_argument( + "-V", + "--verbose", + dest="verbosity", + action="store_const", + const=10, + default=DEFAULT_VERBOSITY, + help="Verbose mode.", + ) + + parser.add_argument( + "-q", "--quiet", dest="verbosity", action="store_const", const=0, help="Quiet mode." + ) + + parser.add_argument( + "-s", + "--swap-rts-dtr", + action="store_true", + help="Swap RTS and DTR: use RTS for reset and DTR for boot0.", + ) + + parser.add_argument( + "-R", "--reset-active-high", action="store_true", help="Make RESET active high." + ) + + parser.add_argument( + "-B", "--boot0-active-low", action="store_true", help="Make BOOT0 active low." + ) + + parser.add_argument( + "-n", "--no-progress", action="store_true", help="Don't show progress bar." + ) + + parser.add_argument( + "-P", + "--parity", + action="store", + type=str, + default="even", + choices=["even", "none"], + help='Parity: "even" for STM32, "none" for BlueNRG.', + ) + + parser.add_argument("--version", action="version", version=__version__) + + # Hack: We want certain arguments to be required when one + # of -rwv is specified, but argparse doesn't support + # conditional dependencies like that. Instead, we add the + # requirements post-facto and re-run the parse to get the error + # messages we want. A better solution would be to use + # subcommands instead of options for -rwv, but this would + # change the command-line interface. + # + # We also use this gross hack to provide a hint about the + # STM32LOADER_SERIAL_PORT environment variable when -p + # is omitted; we only set --port as required after the first + # parse so we can hook in a custom error message. + + configuration = parser.parse_args(arguments) + + if not configuration.port: + port_arg.required = True + atexit.register( + lambda: print( + f"{parser.prog}: note: you can also set the environment" + " variable STM32LOADER_SERIAL_PORT", + file=sys.stderr, + ) + ) + + if configuration.read or configuration.write or configuration.verify: + data_file_arg.nargs = None + data_file_arg.required = True + + if configuration.read: + length_arg.required = True + address_arg.required = True + + parser.parse_args(arguments) + + return configuration diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py new file mode 100644 index 0000000..d90bcd3 --- /dev/null +++ b/src/stm32loader/bootloader.py @@ -0,0 +1,1075 @@ +# Authors: Ivan A-R, Floris Lambrechts +# GitHub repository: https://github.com/florisla/stm32loader +# +# This file is part of stm32loader. +# +# stm32loader is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3, or (at your option) any later +# version. +# +# stm32loader is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License +# along with stm32loader; see the file LICENSE. If not see +# . + +"""Talk to an STM32 native bootloader (see ST AN3155).""" + +from __future__ import annotations + +import enum +import math +import operator +import struct +import time +from functools import lru_cache, reduce + +from stm32loader.device_family import DeviceFamily, DeviceFlag +from stm32loader.device_info import DeviceInfo +from stm32loader.devices import DEVICES + +# pylint: disable=too-many-lines + +CHIP_IDS = { + # see ST AN2606 Table 136 Bootloader device-dependent parameters + # 16 to 32 KiB + 0x412: "STM32F10x Low-density", + 0x444: "STM32F03xx4/6", + # 64 to 128 KiB + 0x410: "STM32F10x Medium-density", + 0x420: "STM32F10x Medium-density value line", + 0x460: "STM32G0x1", + 0x468: "STM32G431xx/STM32G441xx", + # 128 to 512 KiB, + 0x469: "STM32G47xxx/48xxx", + # 256 to 512 KiB (5128 Kbyte is probably a typo?) + 0x414: "STM32F10x High-density", + 0x428: "STM32F10x High-density value line", + # 768 to 1024 KiB + 0x430: "STM3210xx XL-density", + # flash size to be looked up + 0x417: "STM32L05xxx/06xxx", + 0x416: "STM32L1xxx6(8/B) Medium-density ultralow power line", + 0x411: "STM32F2xxx", + 0x433: "STM32F4xxD/E", + # STM32F3 + 0x432: "STM32F373xx/378xx", + 0x422: "STM32F302xB(C)/303xB(C)/358xx", + 0x439: "STM32F301xx/302x4(6/8)/318xx", + 0x438: "STM32F303x4(6/8)/334xx/328xx", + 0x446: "STM32F302xD(E)/303xD(E)/398xx", + # RM0090 in ( 38.6.1 MCU device ID code ) + 0x413: "STM32F405xx/07xx and STM32F415xx/17xx", + 0x419: "STM32F42xxx and STM32F43xxx", + # AN2606 + 0x452: "STM32F72xxx/73xxx", + 0x449: "STM32F74xxx/75xxx", + 0x451: "STM32F76xxx/77xxx", + 0x483: "STM32H72xxx/73xxx", + 0x450: "STM32H74xxx/75xxx", + 0x480: "STM32H7A3xx/B3xx", + # RM0394 46.6.1 MCU device ID code + 0x435: "STM32L4xx", + # ST BlueNRG series; see ST AN4872. + # Three-byte ID where we mask out byte 1 (metal fix) + # and byte 2 (mask set). + # Requires parity None. + 0x000003: "BlueNRG-1 160kB", + 0x00002F: "BlueNRG-2 256kB", + # STM32F0 RM0091 Table 136. DEV_ID and REV_ID field values + 0x440: "STM32F030x8", + 0x445: "STM32F070x6", + 0x448: "STM32F070xB", + 0x442: "STM32F030xC", + 0x457: "STM32L01xxx/02xxx", + 0x497: "STM32WLE5xx/WL55xx", + # Cortex-M0 MCU with hardware TCP/IP and MAC + # (SweetPeas custom bootloader) + 0x801: "Wiznet W7500", + # GigaDevice GD32VW55x series + # Uses 0x06 command to get part number + # (pid is 4-byte ASCII in little-endian) + 0x50494B36: "GD32VW553KIQ6", + 0x504D4B36: "GD32VW553KMQ6", + 0x50494836: "GD32VW553HIQ6", + 0x504D4836: "GD32VW553HMQ6", + 0x50494B37: "GD32VW553KIQ7", + 0x504D4B37: "GD32VW553KMQ7", + 0x50494837: "GD32VW553HIQ7", + 0x504D4837: "GD32VW553HMQ7", +} + + +class Stm32LoaderError(Exception): + """Generic exception type for errors occurring in stm32loader.""" + + +class CommandError(Stm32LoaderError, IOError): + """Exception: a command in the STM32 native bootloader failed.""" + + +class PageIndexError(Stm32LoaderError, ValueError): + """Exception: invalid page index given.""" + + +class DataLengthError(Stm32LoaderError, ValueError): + """Exception: invalid data length given.""" + + +class DataMismatchError(Stm32LoaderError): + """Exception: data comparison failed.""" + + +class MissingDependencyError(Stm32LoaderError): + """Exception: required dependency is missing.""" + + +class DeviceDetectionError(Stm32LoaderError): + """Exception: could not detect device type.""" + + +class ShowProgress: + """ + Show progress through a progress bar, as a context manager. + + Return the progress bar object on context enter, allowing the + caller to to call next(). + + Allow to supply the desired progress bar as None, to disable + progress bar output. + """ + + class _NoProgressBar: + """ + Stub to replace a real progress.bar.Bar. + + Use this if you don't want progress bar output, or if + there's an ImportError of progress module. + """ + + def next(self): # noqa + """Do nothing; be compatible to progress.bar.Bar.""" + + def finish(self): + """Do nothing; be compatible to progress.bar.Bar.""" + + def __init__(self, progress_bar_type): + """ + Construct the context manager object. + + :param progress_bar_type type: Type of progress bar to use. + Set to None if you don't want progress bar output. + """ + self.progress_bar_type = progress_bar_type + self.progress_bar = None + + def __call__(self, message, maximum): + """ + Return a context manager for a progress bar. + + :param str message: Message to show next to the progress bar. + :param int maximum: Maximum value of the progress bar (value at 100%). + E.g. 256. + :return ShowProgress: Context manager object. + """ + if not self.progress_bar_type: + self.progress_bar = self._NoProgressBar() + else: + self.progress_bar = self.progress_bar_type( + message, max=maximum, suffix="%(index)d/%(max)d" + ) + + return self + + def __enter__(self): + """Enter context: return progress bar to allow calling next().""" + return self.progress_bar + + def __exit__(self, exc_type, exc_val, exc_tb): + """Exit context: clean up by finish()ing the progress bar.""" + self.progress_bar.finish() + + +class Stm32Bootloader: # pylint: disable=too-many-instance-attributes + """Talk to the STM32 native bootloader.""" + + # pylint: disable=too-many-public-methods + + @enum.unique + class Command(enum.IntEnum): + """ + STM32 native bootloader command values. + + Refer to ST AN3155, AN4872, AN2606. + """ + + # pylint: disable=too-few-public-methods + + # Get bootloader protocol version and supported commands. + GET = 0x00 + # Get bootloader protocol version. + GET_VERSION = 0x01 + # Get chip/product/device ID. ot available on STM32F103. + GET_ID = 0x02 + READ_MEMORY = 0x11 + GO = 0x21 + WRITE_MEMORY = 0x31 + ERASE = 0x43 + READOUT_PROTECT = 0x82 + READOUT_UNPROTECT = 0x92 + # these not supported on BlueNRG + EXTENDED_ERASE = 0x44 + WRITE_PROTECT = 0x63 + WRITE_UNPROTECT = 0x73 + + # GD-specific command to get part number (GD32VW553 series) + GET_GD_ID = 0x06 + + # not really listed under commands, but still... + # 'wake the bootloader' == 'activate USART' == 'synchronize' + SYNCHRONIZE = 0x7F + + @enum.unique + class Reply(enum.IntEnum): + """STM32 native bootloader reply status codes.""" + + # pylint: disable=too-few-public-methods + + # See ST AN3155, AN4872 + ACK = 0x79 + NACK = 0x1F + + UID_ADDRESS = { + # No unique id for these parts + "F0": None, + # ST RM0008 section 30.1 Unique device ID register + # F101, F102, F103, F105, F107 + "F1": 0x1FFFF7E8, + # ST RM0366 section 29.1 Unique device ID register + # ST RM0365 section 34.1 Unique device ID register + # ST RM0316 section 34.1 Unique device ID register + # ST RM0313 section 32.1 Unique device ID register + # F303/328/358/398, F301/318, F302, F37x + "F3": 0x1FFFF7AC, + # ST RM0090 section 39.1 Unique device ID register + # F405/415, F407/417, F427/437, F429/439 + "F4": 0x1FFF7A10, + # ST RM0385 section 41.2 Unique device ID register + "F7": 0x1FF0F420, + # ST RM0433 section 61.1 Unique device ID register + "H7": 0x1FF1E800, + # ST RM0394 47.1 Unique device ID register (96 bits) + "L4": 0x1FFF7590, + # ST RM0451 25.2 Unique device ID register (96 bits) + "L0": 0x1FF80050, + # ST RM0444 section 38.1 Unique device ID register + "G0": 0x1FFF7590, + # ST RM0453 section 39.1.1 Unique device ID register + "WL": 0x1FFF7590, + # ST BlueNRG has DIE_ID register with PRODUCT, but no UID. + "NRG": None, + # ST RM0440 section 48.1 Unique device ID register (96 bits) + "G4": 0x1FFF7590, + } + + UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] + + # stm32loader does not know the address for the unique ID + UID_ADDRESS_UNKNOWN = -1 + + # Flash size can not be read. + FLASH_SIZE_UNKNOWN = -2 + + # Part does not support unique ID feature + UID_NOT_SUPPORTED = 3 + + FLASH_SIZE_ADDRESS = { + # ST RM0360 section 27.1 Memory size data register + # F030x4/x6/x8/xC, F070x6/xB + "F0": 0x1FFFF7CC, + # ST RM0008 section 30.2 Memory size registers + # F101, F102, F103, F105, F107 + "F1": 0x1FFFF7E0, + # ST RM0366 section 29.2 Memory size data register + # ST RM0365 section 34.2 Memory size data register + # ST RM0316 section 34.2 Memory size data register + # ST RM0313 section 32.2 Flash memory size data register + # F303/328/358/398, F301/318, F302, F37x + "F3": 0x1FFFF7CC, + # ST RM0090 section 39.2 Flash size + # F405/415, F407/417, F427/437, F429/439 + "F4": 0x1FFF7A22, + # ST RM0385 section 41.2 Flash size + "F7": 0x1FF0F442, + # ST RM0433 61.2 Flash size + "H7": 0x1FF1E880, + # ST RM0394 + "L4": 0x1FFF75E0, + # ST RM4510 25.1 Memory size register + "L0": 0x1FF8007C, + # ST RM0444 section 38.2 Flash memory size data register + "G0": 0x1FFF75E0, + # ST RM0453 section 39.1.2 Flash size data register + "WL": 0x1FFF75E0, + # ST BlueNRG-2 datasheet + "NRG": 0x40100014, + # ST RM0440 section 48.2 Flash size data register + "G4": 0x1FFF75E0, + } + + DATA_TRANSFER_SIZE = { + # In bytes. + # Some devices(like L0) may support only 128 bytes each transfer + "default": 128, + "F0": 256, + "F1": 256, + "F3": 256, + "F4": 256, + "F7": 256, + "L4": 256, + "L0": 128, + "G0": 256, + "WL": 256, + "NRG": 256, + # ST RM0433 section 4.2 FLASH main features + "H7": 256, + "G4": 256, + # GigaDevice GD32VW553 series. + # In GD32 ISP Tool they send 240 bytes per transfer. + # In AN027 they suggest 252 bytes per transfer. + "GD32VW55X": 240, + } + + FLASH_PAGE_SIZE = { + # In bytes. + "default": 1024, + # ST RM0360 section 27.1 Memory size data register + # F030x4/x6/x8/xC, F070x6/xB + "F0": 1024, + # ST RM0008 section 30.2 Memory size registers + # F101, F102, F103, F105, F107 + "F1": 1024, + # ST RM0366 section 29.2 Memory size data register + # ST RM0365 section 34.2 Memory size data register + # ST RM0316 section 34.2 Memory size data register + # ST RM0313 section 32.2 Flash memory size data register + # F303/328/358/398, F301/318, F302, F37x + "F3": 2048, + # ST RM0090 section 39.2 Flash size + # F405/415, F407/417, F427/437, F429/439 + "F4": 1024, + # ST RM0385 section 41.2 Flash size + "F7": 1024, + # ST RM0394 + "L4": 1024, + # ST RM4510 25.1 Memory size register + "L0": 128, + # ST RM0444 section 38.2 Flash memory size data register + "G0": 1024, + "WL": 1024, + # ST BlueNRG-2 data sheet: 128 pages of 8 * 64 * 4 bytes + "NRG": 2048, + # ST RM0440 section 3.3.1 Flash memory organization + "G4": 2048, # this is valid only for dual bank mode + # ST RM0433 section 4.2 FLASH main features + "H7": 128 * 1024, + # GD32VW55x series + "GD32VW55X": 4096, + } + + device: DeviceInfo | None + + SYNCHRONIZE_ATTEMPTS = 2 + + def __init__( # pylint: disable=too-many-positional-arguments,too-many-arguments + self, connection, device=None, device_family=None, verbosity=5, show_progress=None + ): + """ + Construct the Stm32Bootloader object. + + The supplied connection can be any object that supports + read() and write(). Optionally, it may also offer + enable_reset() and enable_boot0(); it should advertise this by + setting TOGGLES_RESET and TOGGLES_BOOT0 to True. + + The default implementation is stm32loader.connection.SerialConnection, + but a straight pyserial serial.Serial object can also be used. + + :param connection: Object supporting read() and write(). + E.g. serial.Serial(). + :param int verbosity: Verbosity level. 0 is quiet, 10 is verbose. + :param ShowProgress show_progress: ShowProgress context manager. + Set to None to disable progress bar output. + """ + self.connection = connection + self.verbosity = verbosity + self.show_progress = show_progress or ShowProgress(None) + self.extended_erase = False + self.supported_commands = {} + + # Try to use given device or device family. + if device: + # When given both device and device_family, use device first. + self.device = device + self.device_family = device.family.name + if device_family and device_family != self.device_family: + self.debug( + 0, f"Device family mismatch with the given device family: {device_family}" + ) + self.debug(0, f"Use device family: {self.device_family}") + elif device_family: + self.device_family = device_family + self.device = None + else: + # No device or device_family given. + self.device_family = None + self.device = None + self.update_transfer_info() + + def update_transfer_info(self): + """Update transfer info based on the device family.""" + self.data_transfer_size = self.DATA_TRANSFER_SIZE.get(self.device_family or "default") + self.flash_page_size = self.FLASH_PAGE_SIZE.get(self.device_family or "default") + self.debug(6, f"Data transfer size updated: {self.data_transfer_size} bytes") + self.debug(6, f"Flash page size updated: {self.flash_page_size} bytes") + + def write(self, *data): + """Write the given data to the MCU.""" + for data_bytes in data: + if isinstance(data_bytes, int): + data_bytes = struct.pack("B", data_bytes) + self.connection.write(data_bytes) + + def write_and_ack(self, message, *data): + """Write data to the MCU and wait until it replies with ACK.""" + # Note: this is a separate method from write() because a keyword + # argument after *args was not possible in Python 2 + self.write(*data) + return self._wait_for_ack(message) + + def debug(self, level, message): + """Print the given message if its level is low enough.""" + if self.verbosity >= level: + print(message) + + def reset_from_system_memory(self): + """Reset the MCU with boot0 enabled to enter the bootloader.""" + self._enable_boot0(True) + self._reset() + + # Flush the input buffer to avoid reading old data. + # It's known that the CP2102N at high baudrate fails to flush + # its buffer when the port is opened. + if hasattr(self.connection, "flush_input_buffer"): + self.connection.flush_input_buffer() + + # Try the 0x7F synchronize that selects UART in bootloader mode + # (see ST application notes AN3155 and AN2606). + # If we are right after reset, it returns ACK, otherwise first + # time nothing, then NACK. + # This is not documented in STM32 docs fully, but ST official + # tools use the same algorithm. + # This is likely an artifact/side effect of each command being + # 2-bytes and having xor of bytes equal to 0xFF. + + for attempt in range(self.SYNCHRONIZE_ATTEMPTS): + if attempt: + print("Bootloader activation timeout -- retrying") + self.write(self.Command.SYNCHRONIZE) + read_data = bytearray(self.connection.read()) + + if read_data and read_data[0] in (self.Reply.ACK, self.Reply.NACK): + # success + return + + # not successful + raise CommandError("Bad reply from bootloader") + + def reset_from_flash(self): + """Reset the MCU with boot0 disabled.""" + self._enable_boot0(False) + self._reset() + + def command(self, command, description): + """ + Send the given command to the MCU. + + Raise CommandError if there's no ACK replied. + """ + self.debug(10, "*** Command: %s" % description) + ack_received = self.write_and_ack("Command", command, command ^ 0xFF) + if not ack_received: + raise CommandError("%s (%s) failed: no ack" % (description, command)) + + def get(self): + """Return the bootloader version and remember supported commands.""" + self.command(self.Command.GET, "Get") + length = bytearray(self.connection.read())[0] + version = bytearray(self.connection.read())[0] + self.debug(10, " Bootloader version: " + hex(version)) + supported_commands = bytearray(self.connection.read(length)) + self.supported_commands = {command: True for command in supported_commands} + self.extended_erase = self.Command.EXTENDED_ERASE in self.supported_commands + self.debug( + 10, " Available commands: " + ", ".join(hex(b) for b in self.supported_commands) + ) + self._wait_for_ack("0x00 end") + return version + + def get_version(self): + """ + Return the bootloader protocol version. + + Read protection status readout is not yet implemented. + """ + self.command(self.Command.GET_VERSION, "Get version") + data = bytearray(self.connection.read(3)) + version = data[0] + option_byte1 = data[1] + option_byte2 = data[2] + self._wait_for_ack("0x01 end") + self.debug(10, " Bootloader version: " + hex(version)) + self.debug(10, " Option byte 1: " + hex(option_byte1)) + self.debug(10, " Option byte 2: " + hex(option_byte2)) + return version + + def get_id(self): + """Send the 'Get ID' command and return the chip/product/device ID.""" + self.command(self.Command.GET_ID, "Get ID") + length = bytearray(self.connection.read())[0] + id_data = bytearray(self.connection.read(length + 1)) + self._wait_for_ack("0x02 end") + if self.device_family == DeviceFamily.NRG.value: + # BlueNRG-lineage devices hold the PID in the 3rd byte + return id_data[2] + _device_id = reduce(lambda x, y: x * 0x100 + y, id_data) + return _device_id + + def get_gd_id(self): + """Send the 'Get GD ID' command and return the device ID.""" + self.command(self.Command.GET_GD_ID, "Get GD ID") + length = bytearray(self.connection.read())[0] + # GD32 0x06 command returns N+1 bytes where N is the count, + # but the last byte is ACK, not part of data + id_data = bytearray(self.connection.read(length)) + self._wait_for_ack("0x06 end") + _device_id = self._gd_part_number_to_pid(id_data) + return _device_id + + def get_flash_size(self): + """Return the MCU's flash size in kilobytes.""" + if self.device.flags & DeviceFlag.LONG_UID_ACCESS: + # F4, L0 families. + flash_size, _uid = self._get_flash_size_and_uid_bulk() + return flash_size + if self.device.family.flash_size_address is None: + return int(self.device.flash_size / 1024) + return self._get_flash_size_raw() + + def get_uid(self): + """ + Send the 'Get UID' command and return the device UID. + + Return UID_NOT_SUPPORTED if the device does not have + a UID. + Return UID_ADDRESS_UNKNOWN if the address of the device's + UID is not known. + + :return byterary: UID bytes of the device, or 0 or -1 when + not available. + """ + if self.device.flags & DeviceFlag.LONG_UID_ACCESS: + # F4 and L0 families. + _flash_size, uid = self._get_flash_size_and_uid_bulk() + else: + if not self.device.family.uid_address: + return self.UID_NOT_SUPPORTED + uid = self.read_memory(self.device.family.uid_address, 12) + + return uid + + @lru_cache(maxsize=2) + def _get_flash_size_raw(self): + """Perform a direct 2-byte read of the flash size.""" + flash_size_address = self.device.family.flash_size_address + flash_size_bytes = self.read_memory(flash_size_address, 2) + flash_size = flash_size_bytes[0] + (flash_size_bytes[1] << 8) + return flash_size + + @lru_cache(maxsize=2) + def _get_uid_raw(self): + """Perform a direct 12-byte read of the device UID.""" + uid_address = self.UID_ADDRESS.get(self.device_family, self.UID_ADDRESS_UNKNOWN) + if uid_address is None: + return self.UID_NOT_SUPPORTED + if uid_address == self.UID_ADDRESS_UNKNOWN: + return self.UID_ADDRESS_UNKNOWN + + uid = self.read_memory(uid_address, 12) + return uid + + @lru_cache(maxsize=2) + def _get_flash_size_and_uid_bulk(self): + """ + Return device_uid and flash_size using a 256-byte bulk read. + + This workaround is used for F4 and L0 families. + """ + flash_size_address = self.FLASH_SIZE_ADDRESS[self.device_family] + uid_address = self.UID_ADDRESS.get(self.device_family) + + if uid_address is None: + return None, None + + # Start address is the start of the 256-byte block + # containing uid_address and flash_size_address. + data_start_address = uid_address & 0xFFFFFF00 + flash_size_lsb_address = flash_size_address - data_start_address + uid_lsb_address = uid_address - data_start_address + + self.debug(10, "flash_size_address = 0x%X" % flash_size_address) + self.debug(10, "uid_address = 0x%X" % uid_address) + + data = self.read_memory(data_start_address, self.data_transfer_size) + device_uid = data[uid_lsb_address : uid_lsb_address + 12] + flash_size = data[flash_size_lsb_address] + (data[flash_size_lsb_address + 1] << 8) + + return flash_size, device_uid + + def detect_device(self) -> None: + """Detect the device type and store in `device`.""" + product_id = None + try: + product_id = self.get_id() + except CommandError: + product_id = self.get_gd_id() + + # Look up device details based on ID *without* bootloader ID. + self.device = DEVICES.get((product_id, None)) + + if not self.device: + raise DeviceDetectionError( + f"Unknown device type: no type known for product id: 0x{product_id:03X}" + ) + + # Look up the product's bootloader ID. + bootloader_id = self.get_bootloader_id() + + # Now we can possibly *refine* the product: look up + # with product ID *and* bootloader ID. + self.device = DEVICES.get((product_id, bootloader_id), self.device) + + if self.device: + if self.device_family is None: + # Device family is not manually set. + # Take from auto-detected info. + self.device_family = self.device.family.name + else: + # Device family is manually set. + self.debug( + 1, f"Device family is already set to {self.device_family}. Not updating." + ) + self.update_transfer_info() + + def get_bootloader_id(self): + """Get the bootloader ID by reading the 'bootloader ID' register.""" + if not self.device.bootloader_id_address: + return None + + bootloader_id_byte = self.read_memory_data(self.device.bootloader_id_address, 1) + bootloader_id = struct.unpack("B", bootloader_id_byte)[0] + + return bootloader_id + + @classmethod + def format_uid(cls, uid): + """Return a readable string from the given UID.""" + if uid == cls.UID_NOT_SUPPORTED: + return "UID not supported in this part" + if uid == cls.UID_ADDRESS_UNKNOWN: + return "UID address unknown" + + swapped_data = [[uid[b] for b in part] for part in Stm32Bootloader.UID_SWAP] + uid_string = "-".join("".join(format(b, "02X") for b in part) for part in swapped_data) + return uid_string + + def read_memory(self, address, length): + """ + Return the memory contents of flash at the given address. + + Supports maximum 256 bytes. + """ + if length > self.data_transfer_size: + raise DataLengthError("Can not read more than 256 bytes at once.") + self.command(self.Command.READ_MEMORY, "Read memory") + self.write_and_ack("0x11 address failed", self._encode_address(address)) + nr_of_bytes = (length - 1) & 0xFF + checksum = nr_of_bytes ^ 0xFF + self.write_and_ack("0x11 length failed", nr_of_bytes, checksum) + return bytearray(self.connection.read(length)) + + def go(self, address): + """Send the 'Go' command to start execution of firmware.""" + # pylint: disable=invalid-name + self.command(self.Command.GO, "Go") + self.write_and_ack("0x21 go failed", self._encode_address(address)) + + def write_memory(self, address, data): + """ + Write the given data to flash at the given address. + + Supports maximum 256 bytes. + """ + nr_of_bytes = len(data) + if nr_of_bytes == 0: + return + if nr_of_bytes > self.data_transfer_size: + raise DataLengthError("Can not write more than 256 bytes at once.") + self.command(self.Command.WRITE_MEMORY, "Write memory") + self.write_and_ack("0x31 address failed", self._encode_address(address)) + + # pad data length to multiple of 4 bytes + if nr_of_bytes % 4 != 0: + padding_bytes = 4 - (nr_of_bytes % 4) + nr_of_bytes += padding_bytes + # append value 0xFF: flash memory value after erase + data = bytearray(data) + data.extend([0xFF] * padding_bytes) + + self.debug(10, " %s bytes to write" % [nr_of_bytes]) + checksum = reduce(operator.xor, data, nr_of_bytes - 1) + self.write_and_ack("0x31 programming failed", nr_of_bytes - 1, data, checksum) + self.debug(10, " Write memory done") + + def erase_memory(self, pages=None): + """ + Erase flash memory at the given pages. + + Set pages to None to erase the full memory ('global erase'). + + :param iterable pages: Iterable of integer page addresses, zero-based. + Set to None to trigger global mass erase. + """ + if self.extended_erase: + # Use erase with two-byte addresses instead. + self.extended_erase_memory(pages) + return + + self.command(self.Command.ERASE, "Erase memory") + + if not pages and self.device_family == "L0": + # Special case: L0 erase should do each page separately. + flash_size = self.get_flash_size() + page_count = (flash_size * 1024) // self.flash_page_size + if page_count > 255: + raise PageIndexError("Can not erase more than 255 pages for L0 family.") + pages = range(page_count) + + if pages: + # page erase, see ST AN3155 + if len(pages) > 255: + raise PageIndexError( + "Can not erase more than 255 pages at once.\n" + "Set pages to None to do global erase or supply fewer pages." + ) + page_count = len(pages) - 1 + page_numbers = bytearray(pages) + checksum = reduce(operator.xor, page_numbers, page_count) + self.debug(5, f"Flash erase {page_count} pages from {pages[0]} to {pages[-1]}") + self.write(page_count, page_numbers, checksum) + else: + # global erase: n=255 (page count) + self.debug(5, "Flash global erase") + self.write(255, 0) + + self._wait_for_ack("0x43 erase failed") + self.debug(10, " Erase memory done") + + def extended_erase_memory(self, pages=None): + """ + Erase flash memory using two-byte addressing at the given pages. + + Set pages to None to erase the full memory. + + Not all devices support the extended erase command. + + :param iterable pages: Iterable of integer page addresses, zero-based. + Set to None to trigger global mass erase. + """ + if not pages and self.device_family in ("L0",): + # L0 devices do not support mass erase. + # Instead, erase all pages individually. + flash_size = self.get_flash_size() + pages = list(range(0, (flash_size * 1024) // self.flash_page_size)) + + self.command(self.Command.EXTENDED_ERASE, "Extended erase memory") + + if pages: + # page erase, see ST AN3155 + if len(pages) > 65535: + raise PageIndexError( + "Can not erase more than 65535 pages at once.\n" + "Set pages to None to do global erase or supply fewer pages." + ) + page_count = (len(pages) & 0xFFFF) - 1 + page_count_bytes = bytearray(struct.pack(">H", page_count)) + page_bytes = bytearray(len(pages) * 2) + for i, page in enumerate(pages): + struct.pack_into(">H", page_bytes, i * 2, page) + checksum = reduce(operator.xor, page_count_bytes) + checksum = reduce(operator.xor, page_bytes, checksum) + self.debug(5, f"Flash erase {page_count + 1} pages from {pages[0]} to {pages[-1]}") + self.write(page_count_bytes, page_bytes, checksum) + else: + # global mass erase: n=0xffff (page count) + checksum + # TO DO: support 0xfffe bank 1 erase / 0xfffe bank 2 erase + self.debug(5, "Flash global erase") + self.write(b"\xff\xff\x00") + + previous_timeout_value = self.connection.timeout + self.connection.timeout = 30 + print("Extended erase (0x44), this can take ten seconds or more") + try: + self._wait_for_ack("0x44 erasing failed") + finally: + self.connection.timeout = previous_timeout_value + self.debug(10, " Extended Erase memory done") + + def write_protect(self, sectors=None) -> None: + """Enable write protection on the given flash sectors.""" + + if self.device is None: + raise Stm32LoaderError( + "Device type must be detected before write protection can be enabled." + ) + + if not self.device.write_protect_supported: + raise Stm32LoaderError( + f"Write protection support for '{self.device}' not currently implemented." + ) + + self.debug(10, "Enabling write protection") + if sectors is None: + if self.device.flash is None: + raise Stm32LoaderError( + f"Device flash info is missing for family '{self.device.family.name}'" + ) + if self.device.flash.num_pages() is None: + raise Stm32LoaderError( + f"Device flash page info is missing for device '{self.device.device_name}'" + ) + num_sectors = self.device.flash.num_sectors() + if num_sectors is None: + raise Stm32LoaderError( + f"Device flash sector info is missing for device '{self.device.device_name}'" + ) + # The 'number of sectors' is 0-based + num_sectors -= 1 + sectors = bytearray(list(range(0, num_sectors + 1))) + else: + num_sectors = len(sectors) - 1 + bad_sectors = [s for s in sectors if s < 0 or s > 255] + if len(bad_sectors) > 0: + raise PageIndexError( + "Write protection only supports sector indices up to 255, but got " + f"{bad_sectors}." + ) + + if num_sectors > 255: + raise DataLengthError("Write protection only supports up to 256 sectors.") + + self.debug( + 5, f"Write protecting {num_sectors} sectors, flash size: {self.device.flash.size}" + ) + + self.command(self.Command.WRITE_PROTECT, "Write protect") + checksum = reduce(operator.xor, sectors, num_sectors) + self.write_and_ack("0x63 write protect failed", num_sectors, sectors, checksum) + + time.sleep(0.1) + self.reset_from_system_memory() + self.debug(10, " Write protect done") + + def write_unprotect(self) -> None: + """Disable write protection of the flash memory.""" + self.debug(10, "Disabling write protection") + self.command(self.Command.WRITE_UNPROTECT, "Write unprotect") + self._wait_for_ack("0x73 write unprotect failed") + + time.sleep(0.1) + self.reset_from_system_memory() + self.debug(10, " Write Unprotect done") + + def readout_protect(self): + """Enable readout protection of the flash memory.""" + self.command(self.Command.READOUT_PROTECT, "Readout protect") + self._wait_for_ack("0x82 readout protect failed") + self.debug(10, " Read protect done") + + def readout_unprotect(self): + """ + Disable readout protection of the flash memory. + + Beware, this will erase the flash content. + """ + self.command(self.Command.READOUT_UNPROTECT, "Readout unprotect") + self._wait_for_ack("0x92 readout unprotect failed") + self.debug(20, " Mass erase -- this may take a while") + time.sleep(20) + self.debug(20, " Unprotect / mass erase done") + self.debug(20, " Reset after automatic chip reset due to readout unprotect") + self.reset_from_system_memory() + + def read_memory_data(self, address, length): + """ + Return flash content from the given address and byte count. + + Length may be more than 256 bytes. + """ + data = bytearray() + chunk_count = int(math.ceil(length / float(self.data_transfer_size))) + self.debug( + 10, "Read %7d bytes in %3d chunks at address 0x%X..." % (length, chunk_count, address) + ) + with self.show_progress("Reading", maximum=chunk_count) as progress_bar: + while length: + read_length = min(length, self.data_transfer_size) + self.debug( + 10, + "Read %(len)d bytes at 0x%(address)X" + % {"address": address, "len": read_length}, + ) + data = data + self.read_memory(address, read_length) + progress_bar.next() + length = length - read_length + address = address + read_length + return data + + def write_memory_data(self, address, data): + """ + Write the given data to flash. + + Data length may be more than 256 bytes. + """ + length = len(data) + chunk_count = int(math.ceil(length / float(self.data_transfer_size))) + offset = 0 + self.debug( + 5, "Write %6d bytes in %3d chunks at address 0x%X..." % (length, chunk_count, address) + ) + + with self.show_progress("Writing", maximum=chunk_count) as progress_bar: + while length: + write_length = min(length, self.data_transfer_size) + self.debug( + 10, + "Write %(len)d bytes at 0x%(address)X" + % {"address": address, "len": write_length}, + ) + self.write_memory(address, data[offset : offset + write_length]) + progress_bar.next() + length -= write_length + offset += write_length + address += write_length + + @staticmethod + def verify_data(read_data, reference_data): + """ + Raise an error if the given data does not match its reference. + + Error type is DataMismatchError. + + :param read_data: Data to compare. + :param reference_data: Data to compare, as reference. + :return None: + """ + if read_data == reference_data: + return + + if len(read_data) != len(reference_data): + raise DataMismatchError( + "Data length does not match: %d bytes vs %d bytes." + % (len(read_data), len(reference_data)) + ) + + # data differs; find out where and raise VerifyError + for address, data_pair in enumerate(zip(reference_data, read_data)): + reference_byte, read_byte = data_pair + if reference_byte != read_byte: + raise DataMismatchError( + "Verification data does not match read data. " + "First mismatch at address: 0x%X read 0x%X vs 0x%X expected." + % (address, bytearray([read_byte])[0], bytearray([reference_byte])[0]) + ) + + def pages_from_range(self, start, end): + """Return page indices for the given memory range.""" + if start % self.flash_page_size != 0: + raise PageIndexError( + f"Erase start address should be at a flash page boundary: 0x{start:08X}" + f" (page size 0x{self.flash_page_size:04X}).", + ) + if end % self.flash_page_size != 0: + raise PageIndexError( + f"Erase end address should be at a flash page boundary: 0x{end:08X}" + f" (page size 0x{self.flash_page_size:04X}).", + ) + + # Assemble the list of pages to erase. + first_page = start // self.flash_page_size + last_page = end // self.flash_page_size + pages = list(range(first_page, last_page)) + + return pages + + def _reset(self): + """Enable or disable the reset IO line (if possible).""" + if not hasattr(self.connection, "enable_reset"): + return + self.connection.enable_reset(True) + time.sleep(0.1) + self.connection.enable_reset(False) + time.sleep(0.5) + + def _enable_boot0(self, enable=True): + """Enable or disable the boot0 IO line (if possible).""" + if not hasattr(self.connection, "enable_boot0"): + return + + self.connection.enable_boot0(enable) + + def _wait_for_ack(self, info=""): + """Read a byte and raise CommandError if it's not ACK.""" + read_data = bytearray(self.connection.read()) + if not read_data: + raise CommandError("Can't read port or timeout") + reply = read_data[0] + if reply == self.Reply.NACK: + raise CommandError("NACK " + info) + if reply != self.Reply.ACK: + raise CommandError("Unknown response. " + info + ": " + hex(reply)) + + return 1 + + @staticmethod + def _encode_address(address): + """Return the given address as big-endian bytes with a checksum.""" + # address in four bytes, big-endian + address_bytes = bytearray(struct.pack(">I", address)) + # checksum as single byte + checksum_byte = struct.pack("B", reduce(operator.xor, address_bytes)) + return address_bytes + checksum_byte + + @staticmethod + def _gd_part_number_to_pid(data): + # Convert 4-byte part number to pid (little-endian 32-bit integer) + # For GD32 devices, the part number is returned as ASCII characters + # Example: "7HMP" -> 0x37 0x48 0x4D 0x50 -> 0x504D4837 + pid = 0 + if len(data) >= 4: + pid = (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | data[0] + return pid diff --git a/src/stm32loader/device_family.py b/src/stm32loader/device_family.py new file mode 100644 index 0000000..e9fca3a --- /dev/null +++ b/src/stm32loader/device_family.py @@ -0,0 +1,243 @@ +"""Offer information about STM32 device families.""" + +import enum + + +@enum.unique +class DeviceFamily(enum.Enum): + """Enumeration of STM32 device families.""" + + # AN2606 + C0 = "C0" + F0 = "F0" + F1 = "F1" + F2 = "F2" + F3 = "F3" + F4 = "F4" + F7 = "F7" + G0 = "G0" + G4 = "G4" + H5 = "H5" + H7 = "H7" + L0 = "L0" + L1 = "L1" + L4 = "L4" + L5 = "L5" + WBA = "WBA" + WB0 = "WB0" + WB = "WB" + WL = "WL" + U5 = "U5" + # Not sure if these really exist? + W = "W" + + # BlueNRG-lineage devices + NRG = "NRG" + + # Non-STM devices. + WIZ = "WIZ" + GD32VW55X = "GD32VW55X" + + +@enum.unique +class DeviceFlag(enum.IntEnum): + """Represent device functionality as composable flags.""" + + NONE = 0 + OBL_LAUNCH = 1 + CLEAR_PEMPTY = 2 + # For some reason, F4 (at least, NUCLEO F401RE) can't read the 12 or 2 + # bytes for UID and flash size directly. + # Reading a whole chunk of 256 bytes at 0x1FFFA700 does work and + # requires some data extraction. + LONG_UID_ACCESS = 8 + FORCE_PARITY_NONE = 16 + + +class DeviceFamilyInfo: # pylint: disable=too-few-public-methods,too-many-instance-attributes + """Hold info about an STM32 device family.""" + + def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments + self, + name, + uid_address=None, + flash_size_address=None, + flash_page_size=1024, + transfer_size=256, + mass_erase=True, + option_bytes=None, + bootloader_id_address=None, + flags=DeviceFlag.NONE, + ): + self.name = name + self.uid_address = uid_address + self.flash_size_address = flash_size_address + self.flash_page_size = flash_page_size + self.transfer_size = transfer_size + self.mass_erase = mass_erase + self.option_bytes = option_bytes + self.bootloader_id_address = bootloader_id_address + self.family_default_flags = flags + + +DEVICE_FAMILIES = { + DeviceFamily.C0: DeviceFamilyInfo("C0", bootloader_id_address=0x_1FFF_17FE), + # RM0360 + DeviceFamily.F0: DeviceFamilyInfo( + "F0", + flash_size_address=0x_1FFF_F7CC, + option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F), + ), + # RM0008 + DeviceFamily.F1: DeviceFamilyInfo( + "F1", + uid_address=0x_1FFF_F7E8, + flash_size_address=0x_1FFF_F7E0, + option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F), + ), + # RM0033 + DeviceFamily.F2: DeviceFamilyInfo( + "F2", + option_bytes=(0x_1FFF_C000, 0x_1FFF_C00F), + bootloader_id_address=0x_1FFF_77DE, + ), + # RM0366, RM0365, RM0316, RM0313, RM4510 + DeviceFamily.F3: DeviceFamilyInfo( + "F3", + uid_address=0x_1FFF_F7AC, + flash_size_address=0x_1FFF_F7CC, + flash_page_size=2048, + bootloader_id_address=0x_1FFF_F796, + ), + # RM0090, RM0390, RM0383, RM0402, RM0401, RM0368, RM0430, RM0386 + DeviceFamily.F4: DeviceFamilyInfo( + "F4", + uid_address=0x_1FFF_7A10, + flash_size_address=0x_1FFF_7A22, + bootloader_id_address=0x_1FFF_76DE, + flags=DeviceFlag.LONG_UID_ACCESS, + ), + # RM0385, RM0431 + DeviceFamily.F7: DeviceFamilyInfo( + "F7", + uid_address=0x_1FF0_F420, + flash_size_address=0x_1FF0_F442, + bootloader_id_address=0x_1FF0_EDBE, + ), + # RM0444 + DeviceFamily.G0: DeviceFamilyInfo( + "G0", uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0 + ), + DeviceFamily.G4: DeviceFamilyInfo( + "G4", + uid_address=0x1FFF7590, + flash_size_address=0x1FFF75E0, + bootloader_id_address=0x_1FFF_6FFE, + ), + DeviceFamily.H5: DeviceFamilyInfo( + "H5", + ), + # RM0433 + DeviceFamily.H7: DeviceFamilyInfo( + "H7", + uid_address=0x_1FF1_E800, + flash_size_address=0x_1FF1_E880, + flash_page_size=128 * 1024, + ), + # FIXME TWO RMs? + # RM0451, RM4510 + DeviceFamily.L0: DeviceFamilyInfo( + "L0", + uid_address=0x_1FF8_0050, + flash_size_address=0x_1FF8_007C, + transfer_size=128, + flash_page_size=128, + mass_erase=False, + flags=DeviceFlag.LONG_UID_ACCESS, + ), + DeviceFamily.L1: DeviceFamilyInfo("L1", mass_erase=False), + # RM0394 + DeviceFamily.L4: DeviceFamilyInfo( + "L4", + uid_address=0x_1FFF_7590, + flash_size_address=0x_1FFF_75E0, + bootloader_id_address=0x_1FFF_6FFE, + ), + DeviceFamily.L5: DeviceFamilyInfo( + "L5", + ), + DeviceFamily.WBA: DeviceFamilyInfo( + "WBA", + ), + DeviceFamily.WB: DeviceFamilyInfo( + "WB", + ), + DeviceFamily.WB0: DeviceFamilyInfo( + "WB0", + # TODO: mark write protection as not supported + ), + # RM0453 + DeviceFamily.WL: DeviceFamilyInfo( + "WL", uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0 + ), + DeviceFamily.U5: DeviceFamilyInfo( + "U5", + ), + DeviceFamily.W: DeviceFamilyInfo( + "W", + ), + # BlueNRG-lineage devices + # AN4872: BlueNRG-1/2 + # AN5471: STM32WB05/06/07/09 (previously sold as BlueNRG-LP/LPS) + # AN5920: STM32WL3x + # The bootloader requires parity 'none'! + # The GetID command returns 3 or 4 bytes: + # Byte 1: Silicon metal fix version + # Byte 2: Silicon mask set version + # Byte 3: Product ID + # [03] BlueNRG-1 + # [2F] BlueNRG-2 + # [3B] STM32WB05 (BlueNRG-LPS) + # [3F] STM32WB06/07 (BlueNRG-LP) + # [5F] STM32WL3 + # [06] STM32WB09 + # Byte 4: (0x1F, only on STM32WB09) + # There is no access to peripherals/system memory from bootloader, + # so flash size and UID can not be read directly + # NRG-1/2: flash_size_address=0x_4010_0014, uid_address=0x_1000_07F4 + # others: flash_size_address=0x_4000_1014, uid_address=0x_1000_1EF0 + DeviceFamily.NRG: DeviceFamilyInfo( + "NRG", flags=DeviceFlag.FORCE_PARITY_NONE, flash_page_size=2048 + ), + DeviceFamily.WIZ: DeviceFamilyInfo( + "WIZ", + ), + # GigaDevice GD32VW55x series. + # Uses 0x06 command to get part number instead of standard 0x02. + DeviceFamily.GD32VW55X: DeviceFamilyInfo( + "GD32VW55X", + flash_page_size=4096, + transfer_size=240, + ), +} + + +@enum.unique +class BootloaderSerialPeripherals(enum.IntEnum): + """Enumeration of bootloader serial interface peripherals.""" + + # AN2606 + USART = 1 + DUAL_USART = 2 + UART_CAN_DFU = 3 + USART_DFU = 4 + USART_I2C = 5 + I2C = 6 + I2C_CAN_DFU_I2C = 7 + I2C_SPI = 8 + USART_CAN_FDCAN_DFU_I2C_SPI = 9 + USART_DFU_FDCAN_SPI = 10 + USART_I2C_SPI = 11 + USART_SPI = 12 + USART_DFU_I2C_SPI = 13 + USART_DFU_I2C_I3C_FDCAN_SPI = 14 diff --git a/src/stm32loader/device_info.py b/src/stm32loader/device_info.py new file mode 100644 index 0000000..c8173a5 --- /dev/null +++ b/src/stm32loader/device_info.py @@ -0,0 +1,177 @@ +"""Hold information about different STM32 devices.""" + +from __future__ import annotations + +from stm32loader.device_family import DEVICE_FAMILIES, DeviceFamily, DeviceFlag + +kB = 1024 # pylint: disable=invalid-name + + +class DeviceInfo: # pylint: disable=too-many-instance-attributes + """Hold info about an STM32 device.""" + + write_protect_supported: bool + + def __init__( # pylint: disable=too-many-positional-arguments,too-many-arguments + self, + device_family, + device_name, + pid, + bid, + variant=None, + line=None, + ram=None, + flash=None, + system=None, + option=None, + bootloader_id_address=None, + flags=DeviceFlag.NONE, + write_protect_supported=False, + ): + self.family = DEVICE_FAMILIES[DeviceFamily[device_family]] + self.device_name = device_name + self.product_id = pid + self.bootloader_id = bid + self.variant = variant + self.product_line = line + self.ram = ram + self.flash = Flash(*(flash or [])) + self.system_memory = system + self.option_bytes = option + self.flags = flags | self.family.family_default_flags + self.bootloader_id_address = bootloader_id_address or self.family.bootloader_id_address + self.write_protect_supported = write_protect_supported + + @property + def ram_size(self): + """Return the device's RAM memory size in bytes.""" + if self.ram is None: + return 0 + + assert isinstance(self.ram, tuple) + + if isinstance(self.ram[0], tuple): + # Multiple ranges. + ram_size = 0 + for ram_range in self.ram: + ram_size += ram_range[1] - ram_range[0] + + return ram_size + + start, end = self.ram + ram_size = end - start + return ram_size + + @property + def flash_size(self): + """Return the device's flash memory size in bytes.""" + return self.flash.size + + @property + def system_memory_size(self): + """Return the size of the system memory in bytes.""" + if self.system_memory is None: + return 0 + + assert isinstance(self.system_memory, tuple) + + if isinstance(self.system_memory[0], tuple): + # Multiple ranges. + flash_size = 0 + for flash_range in self.system_memory: + flash_size += flash_range[1] - flash_range[0] + + return flash_size + + start, end = self.system_memory + flash_size = end - start + return flash_size + + def __str__(self): + name = self.device_name + if self.variant: + name += f"-{self.variant}" + if self.product_line: + name += f"-{self.product_line}" + return name + + def __repr__(self): + return f"DeviceInfo(device_name={self.device_name!r}, variant={self.product_line!r})" + + +class Flash: # pylint: disable=too-few-public-methods + """Represent info about a device's flash layout.""" + + start: int | None + end: int | None + page_size: int | list[int] | None + pages_per_sector: int | None + max_write_protection_sectors: int + + # RM0090 4 sectors of 16 Kbytes, 1 sector of 64 Kbytes, + # 7 sectors of 128 Kbytes + F2_F4_PAGE_SIZE = 4 * [16 * kB] + [64 * kB] + 7 * [128 * kB] + + F4_EXTENDED_PAGE_SIZE = 4 * [16 * kB] + [64 * kB] + 11 * [128 * kB] + + # RM0090 4 sectors of 16 Kbytes, 1 sector of 64 Kbytes, + # 7 sectors of 128 Kbytes but then per bank + F4_DUAL_BANK_PAGE_SIZE = (4 * [16 * kB] + [64 * kB] + 7 * [128 * kB]) * 2 + + F7_PAGE_SIZE = 4 * [32 * kB] + [128 * kB] + 7 * [256 * kB] + + def __init__( + self, + start=None, + end=None, + page_size=None, + pages_per_sector=None, + max_write_protection_sectors=63, + ): # pylint: disable=too-many-arguments,too-many-positional-arguments + self.start = start + self.end = end + self.page_size = page_size + self.pages_per_sector = pages_per_sector + # Some devices, like the STM32F101, use a regular scheme up to 62 + # sectors, and the 63rd "sector" applies to the remaining flash. This + # parameter allows for devices which have a different max number of + # sectors. + self.max_write_protection_sectors = max_write_protection_sectors + + @property + def size(self) -> int | None: + """Return the size of the flash memory in bytes.""" + if self.start is None or self.end is None: + return None + + return self.end - self.start + + def num_pages(self) -> int | None: + """Return the number of pages in the flash memory.""" + if self.size is None or self.page_size is None: + return None + + if isinstance(self.page_size, int): + return self.size // self.page_size + + flash_size = self.size + + num_pages = 0 + for page_size in self.page_size: + flash_size -= page_size + num_pages += 1 + if flash_size <= 0: + return num_pages + + raise ValueError( + "Flash size is larger than the total size of all pages. " + f"Flash size: {self.size}, total page size: {sum(self.page_size)}" + ) + + def num_sectors(self) -> int | None: + """Return the number of sectors in the flash memory.""" + num_pages = self.num_pages() + if num_pages is None or self.pages_per_sector is None: + return None + + return min(num_pages // self.pages_per_sector, self.max_write_protection_sectors) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py new file mode 100644 index 0000000..e7c8bbb --- /dev/null +++ b/src/stm32loader/devices.py @@ -0,0 +1,1184 @@ +"""Offer information about the various STM32 device families.""" + +from stm32loader.device_family import DeviceFlag +from stm32loader.device_info import DeviceInfo, Flash + +# pylint: disable=too-many-lines + + +kB = 1024 # pylint: disable=invalid-name + +DEVICE_DETAILS = [ + # Based on ST AN2606 section "Device-dependent bootloader parameters". + # Flash range, option bytes and flags gleaned from + # stm32flash, dev_table.c. + # Other possibly known devices from AN2606 rev 66: + # 0x44C STM32C051xx + # 0x493 STM32C071xx + # 0x44D STM32C091xx/92xx + # 0x474 STM32H503xx + # 0x484 STM32H563xx/573xx + # 0x478 STM32H523xx/33xxx + # 0x485 STM32H7Rxxx/7Sxxx + # 0x459 STM32U031xx + # 0x489 STM32U073xx/83xx + # 0x454 STM32U375xx/85xx + # 0x455 STM32U535xx/545xx + # 0x481 STM32U595xx/599xx/5A9xx + # 0x476 STM32U5F7xx/5F9xx/5G7xx/5G9xx + # 0x492 STM32WBA52xx/54xx/55xx + # 0x4B0 STM32WBA62xx/64xx/65xx + # FIXME flash? + DeviceInfo( + "C0", + "STM32C011xx", + 0x443, + 0x51, + ram=(0x_2000_1000, 0x_2000_3000), + system=(0x_1FFF_0000, 0x_1FFF_1800), + flash=(0x_0800_0000, 0x_0800_8000), + option=(0x_1FFF_7800, 0x_1FFF_787F), + ), + # FIXME flash? + # Error in AN2606? Ram is mentioned as 0x_2000_2000 - 0x_2000_17FF + DeviceInfo( + "C0", + "STM32C031xx", + 0x453, + 0x52, + ram=(0x_2000_2000, 0x_2000_2800), + system=(0x_1FFF_0000, 0x_1FFF_1800), + flash=None, + option=None, + ), + DeviceInfo( + "F0", + "STM32F05xxx/030x8", + 0x440, + 0x21, + ram=(0x_2000_0800, 0x_2000_2000), + system=(0x_1FFF_EC00, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0801_0000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7A6, + write_protect_supported=True, + ), + DeviceInfo( + "F0", + "STM32F03xx4/6", + 0x444, + 0x10, + ram=(0x_2000_0800, 0x_2000_1000), + system=(0x_1FFF_EC00, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7A6, + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x442 ? + DeviceInfo( + "F0", + "STM32F030xC", + 0x442, + 0x52, + ram=(0x_2000_1800, 0x_2000_8000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F796, + flags=DeviceFlag.OBL_LAUNCH, + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x445 ? + DeviceInfo( + "F0", + "STM32F04xxx", + 0x445, + 0xA1, + ram=None, + system=(0x_1FFF_C400, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F6A6, + write_protect_supported=True, + ), + DeviceInfo( + "F0", + "STM32F070x6", + 0x445, + 0xA2, + ram=None, + system=(0x_1FFF_C400, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F6A6, + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x448 ? + DeviceInfo( + "F0", + "STM32F070xB", + 0x448, + 0xA3, + ram=(0x_1FFF_C800, 0x_1FFF_F800), + system=None, + flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F6A6, + write_protect_supported=True, + ), + DeviceInfo( + "F0", + "STM32F071xx/072xx", + 0x448, + 0xA1, + ram=(0x_2000_1800, 0x_2000_4000), + system=(0x_1FFF_C800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F6A6, + write_protect_supported=True, + ), + DeviceInfo( + "F0", + "STM32F09xxx", + 0x442, + 0x50, + ram=(0x_1FFF_D800, 0x_1FFF_F800), + system=None, + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F796, + flags=DeviceFlag.OBL_LAUNCH, + write_protect_supported=True, + ), + DeviceInfo( + "F1", + "STM32F10xxx", + line="Low-density", + pid=0x412, + bid=None, + ram=(0x_2000_0200, 0x_2000_2800), + system=(0x_1FFF_F000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, + ), + DeviceInfo( + "F1", + "STM32F10xxx", + line="Medium-density", + pid=0x410, + bid=None, + ram=(0x_2000_0200, 0x_2000_5000), + system=(0x_1FFF_F000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, + ), + DeviceInfo( + "F1", + "STM32F10xxx", + line="High-density", + pid=0x414, + bid=None, + ram=(0x_2000_0200, 0x_2001_0000), + system=(0x_1FFF_F000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), + DeviceInfo( + "F1", + "STM32F10xxx", + line="Medium-density value", + pid=0x420, + bid=0x10, + ram=(0x_2000_0200, 0x_2000_2000), + system=(0x_1FFF_F000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7D6, + write_protect_supported=True, + ), + DeviceInfo( + "F1", + "STM32F10xxx", + line="High-density value", + pid=0x428, + bid=0x10, + ram=(0x_2000_0200, 0x_2000_8000), + system=(0x_1FFF_F000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7D6, + write_protect_supported=True, + ), + DeviceInfo( + "F1", + "STM32F105xx/107xx", + line="Connectivity", + pid=0x418, + bid=None, + ram=(0x_2000_1000, 0x_2001_0000), + system=(0x_1FFF_B000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, + ), + DeviceInfo( + "F1", + "STM32F10xxx", + line="XL-density", + pid=0x430, + bid=0x21, + ram=(0x_2000_0800, 0x_2001_8000), + system=(0x_1FFF_E000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0810_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7D6, + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x411 ? + DeviceInfo( + "F2", + "STM32F2xxxx", + 0x411, + 0x20, + ram=(0x_2000_2000, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE, 1), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, + ), + DeviceInfo( + "F2", + "STM32F2xxxx", + 0x411, + 0x33, + ram=(0x_2000_2000, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE, 1), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x432 ? + DeviceInfo( + "F3", + "STM32F373xx", + 0x432, + 0x41, + ram=(0x_2000_1400, 0x_2000_8000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7A6, + write_protect_supported=True, + ), + DeviceInfo( + "F3", + "STM32F378xx", + 0x432, + 0x50, + ram=(0x_2000_1000, 0x_2000_8000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7A6, + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x422 ? + DeviceInfo( + "F3", + "STM32F302xB(C)/303xB(C)", + 0x422, + 0x41, + ram=(0x_2000_1400, 0x_2000_A000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, + ), + DeviceInfo( + "F3", + "STM32F358xx", + 0x422, + 0x50, + ram=(0x_2000_1400, 0x_2000_A000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x439 ? + DeviceInfo( + "F3", + "STM32F301xx/302x4(6/8)", + 0x439, + 0x40, + ram=(0x_2000_1800, 0x_2000_4000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, + ), + DeviceInfo( + "F3", + "STM32F318xx", + 0x439, + 0x50, + ram=(0x_2000_1800, 0x_2000_4000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, + ), + DeviceInfo( + "F3", + "STM32F303x4(6/8)/334xx/328xx", + 0x438, + 0x50, + ram=(0x_2000_1800, 0x_2000_3000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x446 ? + DeviceInfo( + "F3", + "STM32F302xD(E)/303xD(E)", + 0x446, + 0x40, + ram=(0x_2000_1800, 0x_2001_0000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, + ), + DeviceInfo( + "F3", + "STM32F398xx", + 0x446, + 0x50, + ram=(0x_2000_1800, 0x_2001_0000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x413 ? + DeviceInfo( + "F4", + "STM32F40xxx/41xxx", + 0x413, + 0x31, + ram=(0x_2000_2000, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + bootloader_id_address=0x_1FFF_77DE, + ), + DeviceInfo( + "F4", + "STM32F40xxx/41xxx", + 0x413, + 0x91, + ram=(0x_2000_3000, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE, 1), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + bootloader_id_address=0x_1FFF_77DE, + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x419 ? + DeviceInfo( + "F4", + "STM32F42xxx/43xxx", + 0x419, + 0x70, + ram=(0x_2000_3000, 0x_2003_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), + option=(0x_1FFE_C000, 0x_1FFF_C00F), + ), + DeviceInfo( + "F4", + "STM32F42xxx/43xxx", + 0x419, + 0x91, + ram=(0x_2000_3000, 0x_2003_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), + option=(0x_1FFE_C000, 0x_1FFF_C00F), + ), + # FIXME Check RAM upper end. + DeviceInfo( + "F4", + "STM32F401xB(C)", + 0x423, + 0xD1, + ram=(0x_2000_3000, 0x_2001_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0804_0000, Flash.F2_F4_PAGE_SIZE, 1), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, + ), + DeviceInfo( + "F4", + "STM32F401xD(E)", + 0x433, + 0xD1, + ram=(0x_2000_3000, 0x_2001_8000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE, 1), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, + ), + DeviceInfo( + "F4", + "STM32F410xx", + 0x458, + 0xB1, + ram=(0x_2000_3000, 0x_2000_8000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0802_0000, Flash.F2_F4_PAGE_SIZE, 1), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, + ), + DeviceInfo( + "F4", + "STM32F411xx", + 0x431, + 0xD0, + ram=(0x_2000_3000, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE, 1), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, + ), + DeviceInfo( + "F4", + "STM32F412xx", + 0x441, + 0x90, + ram=(0x_2000_3000, 0x_2004_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE, 1), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, + ), + DeviceInfo( + "F4", + "STM32F446xx", + 0x421, + 0x90, + ram=(0x_2000_3000, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE, 1), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, + ), + DeviceInfo( + "F4", + "STM32F469xx/479xx", + 0x434, + 0x90, + ram=(0x_2000_3000, 0x_2006_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), + option=(0x_1FFE_C000, 0x_1FFF_C00F), + ), + DeviceInfo( + "F4", + "STM32F413xx/423xx", + 0x463, + 0x90, + ram=(0x_2000_3000, 0x_2005_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0818_0000, Flash.F4_EXTENDED_PAGE_SIZE), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + ), + DeviceInfo( + "F7", + "STM32F72xxx/73xxx", + 0x452, + 0x90, + ram=(0x_2000_4000, 0x_2004_0000), + system=(0x_1FF0_0000, 0x_1FF0_EDC0), + flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE, 1), + option=(0x_1FFF_0000, 0x_1FFF_001F), + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x449 ? + DeviceInfo( + "F7", + "STM32F74xxx/75xxx", + 0x449, + 0x70, + ram=(0x_2000_4000, 0x_2005_0000), + system=(0x_1FF0_0000, 0x_1FF0_EDC0), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE, 1), + option=(0x_1FFF_0000, 0x_1FFF_001F), + write_protect_supported=True, + ), + DeviceInfo( + "F7", + "STM32F74xxx/75xxx", + 0x449, + 0x90, + ram=(0x_2000_4000, 0x_2005_0000), + system=(0x_1FF0_0000, 0x_1FF0_EDC0), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE, 1), + option=(0x_1FFF_0000, 0x_1FFF_001F), + write_protect_supported=True, + ), + DeviceInfo( + "F7", + "STM32F76xxx/77xxx", + 0x451, + 0x93, + ram=(0x_2000_4000, 0x_2008_0000), + system=(0x_1FF0_0000, 0x_1FF0_EDC0), + flash=(0x_0800_0000, 0x_0820_0000, Flash.F7_PAGE_SIZE, 1), + option=(0x_1FFF_0000, 0x_1FFF_001F), + write_protect_supported=True, + ), + DeviceInfo( + "G0", + "STM32G03xxx/04xxx", + 0x466, + 0x53, + ram=(0x_2000_1000, 0x_2000_2000), + system=(0x_1FFF_0000, 0x_1FFF_2000), + flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 1), + option=(0x_1FFF_7800, 0x_1FFF_787F), + bootloader_id_address=0x_1FFF_1FFE, + write_protect_supported=True, + ), + DeviceInfo( + "G0", + "STM32G07xxx/08xxx", + 0x460, + 0xB3, + ram=(0x_2000_2700, 0x_2000_9000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 1), + option=(0x_1FFF_7800, 0x_1FFF_787F), + bootloader_id_address=0x_1FFF_6FFE, + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x467 ? + # FIXME dual banks for system + DeviceInfo( + "G0", + "STM32G0B0xx", + 0x467, + 0xD0, + ram=(0x_2000_4000, 0x_2002_0000), + system=((0x_1FFF_0000, 0x_1FFF_7000), (0x_1FFF_8000, 0x_1FFF_F000)), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_787F), + bootloader_id_address=0x_1FFF_9FFE, + ), + # FIXME dual banks for system + DeviceInfo( + "G0", + "STM32G0B1xx/0C1xx", + 0x467, + 0x92, + ram=(0x_2000_4000, 0x_2002_0000), + system=((0x_1FFF_0000, 0x_1FFF_6000), (0x_1FFF_8000, 0x_1FFF_F000)), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_787F), + bootloader_id_address=0x_1FFF_9FFE, + ), + # FIXME: STM32flash has 0x_2000_4800 as upper system range. + DeviceInfo( + "G0", + "STM32G05xxx/061xx", + 0x456, + 0x51, + ram=(0x_2000_1000, 0x_2000_2000), + system=(0x_1FFF_0000, 0x_1FFF_1000), + flash=(0x_0800_0000, 0x_0801_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_787F), + bootloader_id_address=0x_1FFF_1FFE, + ), + DeviceInfo( + "G4", + "STM32G431xx/441xx", + 0x468, + 0xD4, + ram=(0x_2000_4000, 0x_2000_5800), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0802_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_782F), + ), + DeviceInfo( + "G4", + "STM32G47xxx/48xxx", + 0x469, + 0xD5, + ram=(0x_2000_4000, 0x_2001_8000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_782F), + ), + DeviceInfo( + "G4", + "STM32G491xx/A1xx", + 0x479, + 0xD2, + ram=(0x_2000_4000, 0x_2001_C000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_782F), + ), + # FIXME Flash and option bytes? + DeviceInfo( + "H5", + "STM32H503xx", + 0x474, + 0xE1, + ram=(0x_2000_4000, 0x_2000_8000), + system=(0x_0BF8_7000, 0x_0BF9_0000), + bootloader_id_address=0x_0BF8_FFFE, + ), + # FIXME Flash and option bytes? + DeviceInfo( + "H5", + "STM32H563xx/573xx", + 0x484, + 0xE3, + ram=(0x_2000_0000, 0x_200A_0000), + system=(0x_0BF9_7000, 0x_0BFA_0000), + bootloader_id_address=0x_0BF9_FAFE, + ), + DeviceInfo( + "H7", + "STM32H72xxx/73xxx", + 0x483, + 0x93, + ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_4000, 0x_2405_0000)), + system=(0x_1FF0_0000, 0x_1FF1_E800), + flash=(0x_0800_0000, 0x_0810_0000, 128 * kB, 1), + option=None, + bootloader_id_address=0x_1FF1_E7FE, + write_protect_supported=True, + ), + DeviceInfo( + "H7", + "STM32H74xxx/75xxx", + 0x450, + 0x91, + ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_5000, 0x_2408_0000)), + system=(0x_1FF0_0000, 0x_1FF1_E800), + flash=(0x_0800_0000, 0x_0820_0000, 128 * kB, 1), + option=None, + bootloader_id_address=0x_1FF1_E7FE, + write_protect_supported=True, + ), + DeviceInfo( + "H7", + "STM32H7A3xx/B3xx", + 0x480, + 0x92, + ram=((0x_2000_4100, 0x_2002_0000), (0x_2403_4000, 0x_2408_0000)), + system=(0x_1FF0_0000, 0x_1FF1_4000), + flash=(0x_0800_0000, 0x_0810_0000, 8 * kB), + option=None, + bootloader_id_address=0x_1FF1_3FFE, + ), + DeviceInfo( + "L0", + "STM32L01xxx/02xxx", + 0x457, + 0xC3, + ram=None, + system=(0x_1FF0_0000, 0x_1FF0_1000), + flash=(0x_0800_0000, 0x_0800_4000, 128, 32), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_0FFE, + write_protect_supported=True, + ), + DeviceInfo( + "L0", + "STM32L031xx/041xx", + 0x425, + 0xC0, + ram=(0x_2000_1000, 0x_2000_2000), + system=(0x_1FF0_0000, 0x_1FF0_1000), + flash=(0x_0800_0000, 0x_0800_8000, 128, 32), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_0FFE, + write_protect_supported=True, + ), + DeviceInfo( + "L0", + "STM32L05xxx/06xxx", + 0x417, + 0xC0, + ram=(0x_2000_1000, 0x_2000_2000), + system=(0x_1FF0_0000, 0x_1FF0_1000), + flash=(0x_0800_0000, 0x_0801_0000, 128, 32), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_0FFE, + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x447 ? + # Note: STM32flash has 0x_2000_2000 as lower system range. + DeviceInfo( + "L0", + "STM32L07xxx/08xxx", + 0x447, + 0x41, + ram=(0x_2000_1000, 0x_2000_5000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0803_0000, 128, 32), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_1FFE, + write_protect_supported=True, + ), + DeviceInfo( + "L0", + "STM32L07xxx/08xxx", + 0x447, + 0xB2, + ram=(0x_2000_1400, 0x_2000_5000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0803_0000, 128, 32), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_1FFE, + write_protect_supported=True, + ), + DeviceInfo( + "L1", + "STM32L1xxx6(8/B)", + line="Medium-density ULP", + pid=0x416, + bid=0x20, + ram=(0x_2000_0800, 0x_2000_4000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0802_0000, 256, 16), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_0FFE, + write_protect_supported=True, + ), + DeviceInfo( + "L1", + "STM32L1xxx6(8/B)A", + 0x429, + 0x20, + ram=(0x_2000_1000, 0x_2000_8000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0802_0000, 256, 16), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_0FFE, + write_protect_supported=True, + ), + DeviceInfo( + "L1", + "STM32L1xxxC", + 0x427, + 0x40, + ram=(0x_2000_1000, 0x_2000_8000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0804_0000, 256, 16), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_1FFE, + write_protect_supported=True, + ), + DeviceInfo( + "L1", + "STM32L1xxxD", + 0x436, + 0x45, + ram=(0x_2000_1000, 0x_2000_C000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0806_0000, 256, 16), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_1FFE, + write_protect_supported=True, + ), + DeviceInfo( + "L1", + "STM32L1xxxE", + 0x437, + 0x40, + ram=(0x_2000_1000, 0x_2001_4000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0808_0000, 256, 16), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_1FFE, + write_protect_supported=True, + ), + # Note: Stm32flash has 0x_2000_3100 as ram start. + DeviceInfo( + "L4", + "STM32L412xx/422xx", + line="Low-density", + pid=0x464, + bid=0xD1, + ram=(0x_2000_2100, 0x_2000_8000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 1, 256), + option=(0x_1FFF_7800, 0x_1FFF_780F), + write_protect_supported=True, + ), + DeviceInfo( + "L4", + "STM32L43xxx/44xxx", + 0x435, + 0x91, + ram=(0x_2000_3100, 0x_2000_C000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 1, 256), + option=(0x_1FFF_7800, 0x_1FFF_780F), + write_protect_supported=True, + ), + DeviceInfo( + "L4", + "STM32L45xxx/46xxx", + 0x462, + 0x92, + ram=(0x_2000_3100, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 1, 256), + option=(0x_1FFF_7800, 0x_1FFF_780F), + flags=DeviceFlag.CLEAR_PEMPTY, + write_protect_supported=True, + ), + # FIXME different flash size for both devices with PID=0x415 ? + DeviceInfo( + "L4", + "STM32L47xxx/48xxx", + 0x415, + 0xA3, + ram=(0x_2000_3000, 0x_2001_8000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_F80F), + ), + DeviceInfo( + "L4", + "STM32L47xxx/48xxx", + 0x415, + 0x92, + ram=(0x_2000_3100, 0x_2001_8000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_F80F), + ), + DeviceInfo( + "L4", + "STM32L496xx/4A6xx", + 0x461, + 0x93, + ram=(0x_2000_3100, 0x_2004_0000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_F80F), + ), + DeviceInfo( + "L4", + "STM32L4Rxx/4Sxx", + 0x470, + 0x95, + ram=(0x_2000_3200, 0x_200A_0000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_F80F), + ), + DeviceInfo( + "L4", + "STM32L4P5xx/Q5xx", + 0x471, + 0x90, + ram=(0x_2000_4000, 0x_2005_0000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0810_0000, 4 * kB), + option=(0x_1FF0_0000, 0x_1FF0_000F), + ), + DeviceInfo( + "L5", + "STM32L552xx/562xx", + 0x472, + 0x92, + ram=(0x_2000_4000, 0x_2004_0000), + system=(0x_0BF9_0000, 0x_0BF9_8000), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), + option=None, + bootloader_id_address=0x_0BF9_7FFE, + ), + # FIXME flash config ? + DeviceInfo( + "WBA", + "STM32WBA52xx", + 0x492, + 0xB0, + ram=(0x_2000_0000, 0x_2000_2000), + system=(0x_0BF8_8000, 0x_0BF9_0000), + bootloader_id_address=0x_0BF8_FEFE, + ), + DeviceInfo( + "WB", + "STM32WB10xx/15xx", + 0x494, + 0xB1, + ram=(0x_2000_5000, 0x_2004_0000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0805_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_787F), + bootloader_id_address=0x_1FFF_6FFE, + ), + DeviceInfo( + "WB", + "STM32WB30xx/35xx/50xx/55xx", + 0x495, + 0xD5, + ram=(0x_2000_4000, 0x_2000_C000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0810_0000, 4 * kB), + option=(0x_1FFF_8000, 0x_1FFF_807F), + bootloader_id_address=0x_1FFF_6FFE, + ), + DeviceInfo( + "WL", + "STM32WLE5xx/WL55xx", + 0x497, + 0xC4, + ram=(0x_2000_2000, 0x_2001_0000), + system=(0x_1FFF_0000, 0x_1FFF_4000), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_7FFF), + bootloader_id_address=0x_1FFF_3EFE, + ), + # FIXME flash config? + DeviceInfo( + "U5", + "STM32U535xx/545xx", + 0x455, + 0x91, + ram=(0x_2000_4000, 0x_2024_0000), + system=(0x_0BF9_0000, 0x_0BFA_0000), + bootloader_id_address=0x_0BF9_9EFE, + ), + # + DeviceInfo( + "U5", + "STM32U575xx/585xx", + 0x482, + 0x92, + ram=(0x_2000_4000, 0x_200C_0000), + system=(0x_0BF9_0000, 0x_0BFA_0000), + flash=(0x_0800_0000, 0x_0820_0000, 8 * kB), + option=None, + bootloader_id_address=0x_0BF9_9EFE, + ), + # FIXME flash config? + DeviceInfo( + "U5", + "STM32U595xx/599xx/5A9xx", + 0x481, + 0x92, + ram=(0x_2000_4000, 0x_2027_0000), + system=(0x_0BF9_0000, 0x_0BFA_0000), + bootloader_id_address=0x_0BF9_9EFE, + ), + # Not yet in AN2606. Bootloader IDs are unknown. + # Assumption: 'Medium-density performance' refers to F1 series, and F103. + # FIXME No bootloader ID address? + DeviceInfo( + "F1", + "STM32F103x8/B", + line="Medium-density performance", + pid=0x641, + bid=None, + ram=(0x_2000_0200, 0x_2000_5000), + system=(0x_1FFF_F000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), + # WBA, WB, WL or simply 'W'? + # FIXME bootloader ID address? + DeviceInfo( + "W", + "STM32W", + variant="128kB", + pid=0x9A8, + bid=None, + ram=(0x_2000_0200, 0x_2000_2000), + system=(0x_0804_0000, 0x_0804_0800), + flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), + option=(0x_0804_0800, 0x_0804_080F), + ), + DeviceInfo( + "W", + "STM32W", + variant="256kB", + pid=0x9B0, + bid=None, + ram=(0x_2000_0200, 0x_2000_4000), + system=(0x_0804_0000, 0x_0804_0800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 4), + option=(0x_0804_0800, 0x_0804_080F), + ), + # ST BlueNRG + DeviceInfo( + "NRG", + "BlueNRG-1", + pid=0x03, + bid=None, + ram=(0x_2000_0000, 0x_2000_6000), + system=(0x_1000_0000, 0x_1000_0800), + flash=(0x_1004_0000, 0x_1006_8000, 2 * kB), + ), + DeviceInfo( + "NRG", + "BlueNRG-2", + pid=0x2F, + bid=None, + ram=(0x_2000_0000, 0x_2000_6000), + system=(0x_1000_0000, 0x_1000_0800), + flash=(0x_1004_0000, 0x_1008_0000, 2 * kB), + ), + DeviceInfo( + "NRG", + "STM32WB06/07 (BlueNRG-LP)", + pid=0x3F, + bid=None, + ram=(0x_2000_0000, 0x_2001_0000), + system=(0x_1000_0000, 0x_1000_1800), + flash=(0x_1004_0000, 0x_1008_0000, 2 * kB), + ), + DeviceInfo( + "NRG", + "STM32WB05 (BlueNRG-LPS)", + pid=0x3B, + bid=None, + ram=(0x_2000_0000, 0x_2000_6000), + system=(0x_1000_0000, 0x_1000_1800), + flash=(0x_1004_0000, 0x_1007_0000, 2 * kB), + ), + DeviceInfo( + "NRG", + "STM32WB09", + pid=0x06, + bid=None, + ram=(0x_2000_0000, 0x_2001_0000), + system=(0x_1000_0000, 0x_1000_1800), + flash=(0x_1004_0000, 0x_100C_0000, 2 * kB), + ), + DeviceInfo( + "NRG", + "STM32WL3x", + pid=0x5F, + bid=None, + ram=(0x_2000_0000, 0x_2000_8000), + system=(0x_1000_0000, 0x_1000_1800), + flash=(0x_1004_0000, 0x_1008_0000, 2 * kB), + ), + # Wiznet W7500 + DeviceInfo("WIZ", "Wiznet W7500", 0x801, bid=None, ram=None, system=None), + # GigaDevice GD32VW553 series + # Uses 0x06 command to get part number (pid is 4-byte ASCII + # in little-endian). + # Find these pid in GD32_ISP_CLI(Linux)->libGD_MCU_DLL.so + # ->BuildMap_GD32103 + # pid "6KIP" = 0x36 0x4B 0x49 0x50 -> 0x50494B36 + DeviceInfo( + "GD32VW55X", + "GD32VW553KIQ6", + pid=0x50494B36, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08200000, 4 * kB), # 2MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "6KMP" = 0x36 0x4B 0x4D 0x50 -> 0x504D4B36 + DeviceInfo( + "GD32VW55X", + "GD32VW553KMQ6", + pid=0x504D4B36, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08400000, 4 * kB), # 4MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "6HIP" = 0x36 0x48 0x49 0x50 -> 0x50494836 + DeviceInfo( + "GD32VW55X", + "GD32VW553HIQ6", + pid=0x50494836, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08200000, 4 * kB), # 2MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "6HMP" = 0x36 0x48 0x4D 0x50 -> 0x504D4836 + DeviceInfo( + "GD32VW55X", + "GD32VW553HMQ6", + pid=0x504D4836, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08400000, 4 * kB), # 4MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "7KIP" = 0x37 0x4B 0x49 0x50 -> 0x50494B37 + DeviceInfo( + "GD32VW55X", + "GD32VW553KIQ7", + pid=0x50494B37, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08200000, 4 * kB), # 2MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "7KMP" = 0x37 0x4B 0x4D 0x50 -> 0x504D4B37 + DeviceInfo( + "GD32VW55X", + "GD32VW553KMQ7", + pid=0x504D4B37, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08400000, 4 * kB), # 4MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "7HIP" = 0x37 0x48 0x49 0x50 -> 0x50494837 + DeviceInfo( + "GD32VW55X", + "GD32VW553HIQ7", + pid=0x50494837, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08200000, 4 * kB), # 2MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "7HMP" = 0x37 0x48 0x4D 0x50 -> 0x504D4837 + DeviceInfo( + "GD32VW55X", + "GD32VW553HMQ7", + pid=0x504D4837, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08400000, 4 * kB), # 4MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), +] + +DEVICES = {(dev.product_id, dev.bootloader_id): dev for dev in DEVICE_DETAILS} + +# If devices are not yet registered with bootloader_id == None, then do so. +for device in DEVICE_DETAILS: + if (device.product_id, None) in DEVICES: + continue + DEVICES[(device.product_id, None)] = device diff --git a/src/stm32loader/emulated/fake.py b/src/stm32loader/emulated/fake.py new file mode 100644 index 0000000..6223d0f --- /dev/null +++ b/src/stm32loader/emulated/fake.py @@ -0,0 +1,168 @@ +"""Fake bootloader connection for testing purposes.""" + +import struct + +from stm32loader.bootloader import Stm32Bootloader + +# pylint: disable=too-many-arguments +# pylint: disable=too-many-positional-arguments +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-few-public-methods +# pylint: disable=missing-function-docstring + + +class FakeConnection: + """Emulate a bootloader connection.""" + + ACK = Stm32Bootloader.Reply.ACK.value + Command = Stm32Bootloader.Command + + COMMAND_RESPONSES = { + # Return length, bootloader version, commands + # Version 5, 0x0=GET 0x01=GET_VERSION 0x02=GET_ID + # 0x11=READ_MEMORY 0x31=WRITE_MEMORY + # 0x43=ERASE 0x44=EXTENDED_ERASE + Command.GET: [7, 0x05, [0x0, 0x01, 0x02, 0x11, 0x31, 0x43, 0x44], ACK], + # Product ID: 0x422 + Command.GET_ID: [1, [0x04, 0x22]], + } + + READ_RESPONSES = { + # Read flash size, F1. + (0x_1FFF_F7E0, 2): [[0x00, 0x01]], + # Read flash size, F3. + (0x_1FFF_F7CC, 2): [[0x00, 0x01]], + # Read device UID, F1. + (0x_1FFF_F7E8, 12): [[1, 0, 3, 2, 7, 6, 5, 4, 0xB, 0xA, 9, 8]], + # Read device UID, F3. + (0x_1FFF_F7AC, 12): [[1, 0, 3, 2, 7, 6, 5, 4, 0xB, 0xA, 9, 8]], + # Read Bootloader ID. + (0x_1FFF_F796, 1): [0x41], + } + + def __init__(self): + self.next_return = [] + self.timeout = 2 + self.receiver = self.receive() + + self.flash_offset = 0x_0800_0000 + self.flash_size = 2 * 1024 * 1024 + self.flash_memory = bytearray(2 * 1024 * 1024) + + # Start coroutine. + next(self.receiver) + + def ack(self): + self.next_return.append(self.ACK) + + def receive(self): + while True: + # Receive a command coming in. + command_bytes = yield + command_value = struct.unpack("B", command_bytes)[0] + + # No CRC is sent for SYNCHRONIZE + if command_value == self.Command.SYNCHRONIZE: + continue + + # Receive CRC byte. + yield + self.ack() + + if command_value in self.COMMAND_RESPONSES: + self.next_return.extend(self.COMMAND_RESPONSES[command_value]) + elif command_value == self.Command.READ_MEMORY.value: + # Receive address with CRC. + address_bytes = yield + address = struct.unpack(">I", address_bytes[0:4])[0] + self.ack() + + # Receive number of bytes + length_bytes = yield + length = struct.unpack("B", length_bytes)[0] + 1 + + # Receive CRC + yield + self.ack() + + # Set up data to respond. + if self.flash_offset <= address < self.flash_offset + self.flash_size: + # Return flash data. + flash_offset = address - self.flash_offset + self.next_return.append( + list(self.flash_memory[flash_offset : flash_offset + length]) + ) + else: + self.next_return.extend(self.READ_RESPONSES[(address, length)]) + elif command_value == self.Command.EXTENDED_ERASE.value: + pages_bytes = yield + pages = struct.unpack(">H", pages_bytes[0:2]) + if pages == 0xFFFF: + # Erase all. + self.flash_memory[:] = 0xFF + self.next_return.append(self.ACK) + elif command_value == self.Command.WRITE_MEMORY.value: + address_bytes = yield + address = struct.unpack(">I", address_bytes[0:4])[0] + size_bytes = yield + byte_count = struct.unpack("B", size_bytes)[0] + 1 + data = yield + _crc = yield + + assert len(data) == byte_count, ( + f"Length does not match byte count: {len(data)} vs {byte_count}" + ) + + # Record data in flash memory. + flash_offset = address - 0x_0800_0000 + self.flash_memory[flash_offset : flash_offset + byte_count] = data + + elif command_value == self.Command.WRITE_PROTECT.value: + number_of_pages_bytes = yield + number_of_pages = struct.unpack("B", number_of_pages_bytes)[0] + _page_numbers_bytes = yield + assert len(_page_numbers_bytes) == number_of_pages + 1, ( + f"{len(_page_numbers_bytes)} != {number_of_pages + 1}" + ) + _crc = yield + + elif command_value == self.Command.WRITE_UNPROTECT.value: + pass + + else: + raise NotImplementedError(hex(command_value)) + + def write(self, data): + # Send to coroutine. + self.receiver.send(data) + + def read(self, length=1): # pylint: disable=unused-argument + if self.next_return: + value = self.next_return.pop(0) + if isinstance(value, int): + return [value] + return value + + return [self.ACK] + + +class FakeConfiguration: + """Represent a configuration for test purposes.""" + + def __init__( + self, erase, write, verify, write_protect, write_unprotect, firmware_file, family=None + ): + self.erase = erase + self.write = write + self.read = False + self.verify = verify + self.write_protect = write_protect + self.write_unprotect = write_unprotect + self.data_file = firmware_file + self.unprotect = False + self.protect = False + self.length = None + self.verbosity = 5 + self.address = 0x_0800_0000 + self.go_address = None + self.family = family diff --git a/src/stm32loader/hexfile.py b/src/stm32loader/hexfile.py new file mode 100644 index 0000000..298af0c --- /dev/null +++ b/src/stm32loader/hexfile.py @@ -0,0 +1,30 @@ +"""Load binary data from a file in Intel hex format.""" + +from stm32loader.bootloader import MissingDependencyError + +try: + import intelhex +except ImportError: + intelhex = None + + +def load_hex(file_path: str) -> bytes: + """ + Return bytes from the given hex file. + + Addresses should start at zero and always increment. + """ + if intelhex is None: + raise MissingDependencyError( + "Please install package 'intelhex' in order to read .hex files." + ) + + hex_content = intelhex.IntelHex() + hex_content.loadhex(str(file_path)) + hex_dict = hex_content.todict() + + addresses = list(hex_dict.keys()) + assert addresses[0] == 0 + assert addresses[-1] == len(addresses) - 1 + + return bytes(hex_content.todict().values()) diff --git a/src/stm32loader/main.py b/src/stm32loader/main.py new file mode 100644 index 0000000..4bbccc1 --- /dev/null +++ b/src/stm32loader/main.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python +# Authors: Ivan A-R, Floris Lambrechts +# GitHub repository: https://github.com/florisla/stm32loader +# +# This file is part of stm32loader. +# +# stm32loader is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3, or (at your option) any later +# version. +# +# stm32loader is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License +# along with stm32loader; see the file LICENSE. If not see +# . + +"""Flash firmware to STM32 microcontrollers over a serial connection.""" + +import sys +from pathlib import Path +from types import SimpleNamespace + +import serial + +try: + from progress.bar import ChargingBar as progress_bar +except ImportError: + progress_bar = None + +from stm32loader import args, bootloader, hexfile +from stm32loader.device_family import DEVICE_FAMILIES, DeviceFamily, DeviceFlag +from stm32loader.uart import SerialConnection + + +class Stm32Loader: + """Main application: parse arguments and handle commands.""" + + # serial link bit parity, compatible to pyserial serial.PARTIY_EVEN + PARITY = {"even": serial.PARITY_EVEN, "none": serial.PARITY_NONE} + + def __init__(self): + """Construct Stm32Loader object with default settings.""" + self.stm32 = None + self.configuration = SimpleNamespace() + + def debug(self, level, message): + """Log a message to stderror if its level is low enough.""" + if self.configuration.verbosity >= level: + print(message) + + def parse_arguments(self, arguments): + """Parse the list of command-line arguments.""" + self.configuration = args.parse_arguments(arguments) + + # parse successful, process options further + self.configuration.parity = Stm32Loader.PARITY[self.configuration.parity.lower()] + + if self.configuration.family: + family = DeviceFamily[self.configuration.family] + family_flags = DEVICE_FAMILIES[family].family_default_flags + if family_flags & DeviceFlag.FORCE_PARITY_NONE: + self.configuration.parity = serial.PARITY_NONE + + def connect(self): + """Connect to the bootloader UART over an RS-232 serial port.""" + serial_connection = SerialConnection( + self.configuration.port, self.configuration.baud, self.configuration.parity + ) + self.debug( + 10, + "Open port %(port)s, baud %(baud)d" + % {"port": self.configuration.port, "baud": self.configuration.baud}, + ) + try: + serial_connection.connect() + except IOError as e: + print(str(e) + "\n", file=sys.stderr) + print( + "Is the device connected and powered correctly?\n" + "Please use the --port option to select the correct serial port. Examples:\n" + " --port COM3\n" + " --port /dev/ttyS0\n" + " --port /dev/ttyUSB0\n" + " --port /dev/tty.usbserial-ftCYPMYJ\n", + file=sys.stderr, + ) + sys.exit(1) + + serial_connection.swap_rts_dtr = self.configuration.swap_rts_dtr + serial_connection.reset_active_high = self.configuration.reset_active_high + serial_connection.boot0_active_low = self.configuration.boot0_active_low + + show_progress = self._get_progress_bar(self.configuration.no_progress) + + self.stm32 = bootloader.Stm32Bootloader( + serial_connection, + verbosity=self.configuration.verbosity, + show_progress=show_progress, + device_family=self.configuration.family, + ) + + try: + print("Activating bootloader (select UART)") + self.stm32.reset_from_system_memory() + except bootloader.CommandError: + print( + "Can't init into bootloader. Ensure that BOOT0 is enabled and reset the device.", + file=sys.stderr, + ) + self.stm32.reset_from_flash() + sys.exit(1) + + def perform_commands(self): + """Run all operations as defined by the configuration.""" + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + binary_data = None + if self.configuration.write or self.configuration.verify: + data_file_path = Path(self.configuration.data_file) + if data_file_path.suffix == ".hex": + binary_data = hexfile.load_hex(data_file_path) + else: + binary_data = data_file_path.read_bytes() + if self.configuration.unprotect: + try: + self.stm32.readout_unprotect() + except bootloader.CommandError: + self.debug(0, "Flash readout unprotect failed") + self.debug(0, "Quit") + self.stm32.reset_from_flash() + sys.exit(1) + if self.configuration.protect: + try: + self.stm32.readout_protect() + except bootloader.CommandError: + self.debug(0, "Flash readout protect failed") + self.debug(0, "Quit") + self.stm32.reset_from_flash() + sys.exit(1) + + if self.configuration.write_unprotect: + try: + self.stm32.write_unprotect() + except bootloader.CommandError: + self.debug(0, "Flash write unprotect failed") + self.debug(0, "Quit") + self.stm32.reset_from_flash() + sys.exit(1) + + if self.configuration.erase: + try: + if self.configuration.length is None: + # Erase full device. + self.debug(0, "Performing full erase...") + self.stm32.erase_memory(pages=None) + else: + # Erase from address to address + length. + start_address = self.configuration.address + end_address = self.configuration.address + self.configuration.length + pages = self.stm32.pages_from_range(start_address, end_address) + self.debug( + 0, + f"Performing partial erase (0x{start_address:X} - 0x{end_address:X}," + f" {len(pages)} pages)... ", + ) + self.stm32.erase_memory(pages) + + except bootloader.CommandError: + # may be caused by readout protection + self.debug( + 0, + "Erase failed -- probably due to readout protection.\n" + "Consider using the --unprotect option.", + ) + self.stm32.reset_from_flash() + sys.exit(1) + if self.configuration.write: + self.stm32.write_memory_data(self.configuration.address, binary_data) + + if self.configuration.write_protect: + try: + self.stm32.write_protect(sectors=None) + except bootloader.CommandError: + self.debug(0, "Flash write protect failed") + self.debug(0, "Quit") + self.stm32.reset_from_flash() + sys.exit(1) + + if self.configuration.verify: + read_data = self.stm32.read_memory_data(self.configuration.address, len(binary_data)) + try: + bootloader.Stm32Bootloader.verify_data(read_data, binary_data) + print("Verification OK") + except bootloader.DataMismatchError as e: + print("Verification FAILED: %s" % e, file=sys.stderr) + sys.exit(1) + if not self.configuration.write and self.configuration.read: + read_data = self.stm32.read_memory_data( + self.configuration.address, self.configuration.length + ) + with open(self.configuration.data_file, "wb") as out_file: + out_file.write(read_data) + if self.configuration.go_address is not None: + self.stm32.go(self.configuration.go_address) + + def reset(self): + """Reset the microcontroller.""" + self.stm32.reset_from_flash() + + def detect_device(self) -> None: + """Detect the STM32 device type by querying bootloader and regs.""" + boot_version = self.stm32.get() + self.debug(0, "Bootloader version: 0x%X" % boot_version) + self.stm32.detect_device() + if self.stm32.device.bootloader_id is not None: + self.debug(5, f"Bootloader ID: 0x{self.stm32.device.bootloader_id:02X}") + self.debug(0, f"Chip ID: 0x{self.stm32.device.product_id:03X}") + self.debug(0, f"Chip model: {self.stm32.device.device_name}") + + def read_device_uid(self): + """Show chip UID.""" + try: + device_uid = self.stm32.get_uid() + except bootloader.CommandError as e: + self.debug( + 0, + "Something was wrong with reading chip UID: " + str(e), + ) + return + + if device_uid != bootloader.Stm32Bootloader.UID_NOT_SUPPORTED: + device_uid_string = self.stm32.format_uid(device_uid) + self.debug(0, "Device UID: %s" % device_uid_string) + + def read_flash_size(self): + """Show chip flash size.""" + try: + flash_size = self.stm32.get_flash_size() + except bootloader.CommandError as e: + self.debug( + 0, + "Something was wrong with reading chip flash size: " + str(e), + ) + return + + if flash_size != bootloader.Stm32Bootloader.FLASH_SIZE_UNKNOWN: + self.debug(0, f"Flash size: {flash_size} kiB") + + @staticmethod + def _get_progress_bar(no_progress=False): + if no_progress or not progress_bar: + return None + + return bootloader.ShowProgress(progress_bar) + + +def main(*arguments, **kwargs): + """ + Parse arguments and execute tasks. + + Default usage is to supply *sys.argv[1:]. + """ + try: + loader = Stm32Loader() + loader.parse_arguments(arguments) + loader.connect() + try: + loader.detect_device() + loader.read_device_uid() + loader.read_flash_size() + loader.perform_commands() + finally: + loader.reset() + except SystemExit: + if not kwargs.get("avoid_system_exit", False): + raise + + +if __name__ == "__main__": + main(*sys.argv[1:]) diff --git a/src/stm32loader/uart.py b/src/stm32loader/uart.py new file mode 100644 index 0000000..db26790 --- /dev/null +++ b/src/stm32loader/uart.py @@ -0,0 +1,125 @@ +# Author: Floris Lambrechts +# GitHub repository: https://github.com/florisla/stm32loader +# +# This file is part of stm32loader. +# +# stm32loader is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3, or (at your option) any later +# version. +# +# stm32loader is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License +# along with stm32loader; see the file LICENSE. If not see +# . + +""" +Handle RS-232 serial communication through pyserial. + +Offer support for toggling RESET and BOOT0. +""" + +# Note: this file not named 'serial' because that name-clashed in Python 2 + +import serial + + +class SerialConnection: + """Wrap a serial.Serial connection and toggle reset and boot0.""" + + # pylint: disable=too-many-instance-attributes + + def __init__(self, serial_port, baud_rate=115200, parity="E"): + """Construct a SerialConnection (not yet connected).""" + self.serial_port = serial_port + self.baud_rate = baud_rate + self.parity = parity + + self.swap_rts_dtr = False + self.reset_active_high = False + self.boot0_active_low = False + + # don't connect yet; caller should use connect() separately + self.serial_connection = None + + self._timeout = 5 + + @property + def timeout(self): + """Get timeout.""" + return self._timeout + + @timeout.setter + def timeout(self, timeout): + """Set timeout.""" + self._timeout = timeout + self.serial_connection.timeout = timeout + + def connect(self): + """Connect to the UART serial port.""" + self.serial_connection = serial.Serial( + port=self.serial_port, + baudrate=self.baud_rate, + # number of write_data bits + bytesize=8, + parity=self.parity, + stopbits=1, + # don't enable software flow control + xonxoff=0, + # don't enable RTS/CTS flow control + rtscts=0, + # set a timeout value, None for waiting forever + timeout=self._timeout, + ) + + def disconnect(self): + """Close the connection.""" + if not self.serial_connection: + return + + self.serial_connection.close() + self.serial_connection = None + + def write(self, *args, **kwargs): + """Write the given data to the serial connection.""" + return self.serial_connection.write(*args, **kwargs) + + def read(self, *args, **kwargs): + """Read the given amount of bytes from the serial connection.""" + return self.serial_connection.read(*args, **kwargs) + + def enable_reset(self, enable=True): + """Enable or disable the reset IO line.""" + # reset on the STM32 is active low (0 Volt puts the MCU in reset) + # but the RS-232 modem control DTR and RTS signals are active low + # themselves, so these get inverted -- writing a logical 1 outputs + # a low voltage, i.e. enables reset) + level = int(enable) + if self.reset_active_high: + level = 1 - level + + if self.swap_rts_dtr: + self.serial_connection.setRTS(level) + else: + self.serial_connection.setDTR(level) + + def enable_boot0(self, enable=True): + """Enable or disable the boot0 IO line.""" + level = int(enable) + + # by default, this is active high + if not self.boot0_active_low: + level = 1 - level + + if self.swap_rts_dtr: + self.serial_connection.setDTR(level) + else: + self.serial_connection.setRTS(level) + + def flush_imput_buffer(self): + """Flush the input buffer to remove any stale read data.""" + self.serial_connection.reset_input_buffer() diff --git a/stm32loader.py b/stm32loader.py deleted file mode 100755 index 95adc05..0000000 --- a/stm32loader.py +++ /dev/null @@ -1,475 +0,0 @@ -#!/usr/bin/env python - -# -*- coding: utf-8 -*- -# vim: sw=4:ts=4:si:et:enc=utf-8 - -# Author: Ivan A-R -# Project page: http://tuxotronic.org/wiki/projects/stm32loader -# -# This file is part of stm32loader. -# -# stm32loader is free software; you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation; either version 3, or (at your option) any later -# version. -# -# stm32loader is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# for more details. -# -# You should have received a copy of the GNU General Public License -# along with stm32loader; see the file COPYING3. If not see -# . - -import sys, getopt -import serial -import time - -try: - from progressbar import * - usepbar = 1 -except: - usepbar = 0 - -# Verbose level -QUIET = 20 - -# these come from AN2606 -chip_ids = { - 0x412: "STM32 Low-density", - 0x410: "STM32 Medium-density", - 0x414: "STM32 High-density", - 0x420: "STM32 Medium-density value line", - 0x428: "STM32 High-density value line", - 0x430: "STM32 XL-density", - 0x416: "STM32 Medium-density ultralow power line", - 0x411: "STM32F2xx", - 0x413: "STM32F4xx", -} - -def mdebug(level, message): - if(QUIET >= level): - print >> sys.stderr , message - - -class CmdException(Exception): - pass - -class CommandInterface: - extended_erase = 0 - - def open(self, aport='/dev/tty.usbserial-ftCYPMYJ', abaudrate=115200) : - self.sp = serial.Serial( - port=aport, - baudrate=abaudrate, # baudrate - bytesize=8, # number of databits - parity=serial.PARITY_EVEN, - stopbits=1, - xonxoff=0, # don't enable software flow control - rtscts=0, # don't enable RTS/CTS flow control - timeout=5 # set a timeout value, None for waiting forever - ) - - - def _wait_for_ask(self, info = ""): - # wait for ask - try: - ask = ord(self.sp.read()) - except: - raise CmdException("Can't read port or timeout") - else: - if ask == 0x79: - # ACK - return 1 - else: - if ask == 0x1F: - # NACK - raise CmdException("NACK "+info) - else: - # Unknown responce - raise CmdException("Unknown response. "+info+": "+hex(ask)) - - - def reset(self): - self.sp.setDTR(0) - time.sleep(0.1) - self.sp.setDTR(1) - time.sleep(0.5) - - def initChip(self): - # Set boot - self.sp.setRTS(0) - self.reset() - - self.sp.write("\x7F") # Syncro - return self._wait_for_ask("Syncro") - - def releaseChip(self): - self.sp.setRTS(1) - self.reset() - - def cmdGeneric(self, cmd): - self.sp.write(chr(cmd)) - self.sp.write(chr(cmd ^ 0xFF)) # Control byte - return self._wait_for_ask(hex(cmd)) - - def cmdGet(self): - if self.cmdGeneric(0x00): - mdebug(10, "*** Get command"); - len = ord(self.sp.read()) - version = ord(self.sp.read()) - mdebug(10, " Bootloader version: "+hex(version)) - dat = map(lambda c: hex(ord(c)), self.sp.read(len)) - if '0x44' in dat: - self.extended_erase = 1 - mdebug(10, " Available commands: "+", ".join(dat)) - self._wait_for_ask("0x00 end") - return version - else: - raise CmdException("Get (0x00) failed") - - def cmdGetVersion(self): - if self.cmdGeneric(0x01): - mdebug(10, "*** GetVersion command") - version = ord(self.sp.read()) - self.sp.read(2) - self._wait_for_ask("0x01 end") - mdebug(10, " Bootloader version: "+hex(version)) - return version - else: - raise CmdException("GetVersion (0x01) failed") - - def cmdGetID(self): - if self.cmdGeneric(0x02): - mdebug(10, "*** GetID command") - len = ord(self.sp.read()) - id = self.sp.read(len+1) - self._wait_for_ask("0x02 end") - return reduce(lambda x, y: x*0x100+y, map(ord, id)) - else: - raise CmdException("GetID (0x02) failed") - - - def _encode_addr(self, addr): - byte3 = (addr >> 0) & 0xFF - byte2 = (addr >> 8) & 0xFF - byte1 = (addr >> 16) & 0xFF - byte0 = (addr >> 24) & 0xFF - crc = byte0 ^ byte1 ^ byte2 ^ byte3 - return (chr(byte0) + chr(byte1) + chr(byte2) + chr(byte3) + chr(crc)) - - - def cmdReadMemory(self, addr, lng): - assert(lng <= 256) - if self.cmdGeneric(0x11): - mdebug(10, "*** ReadMemory command") - self.sp.write(self._encode_addr(addr)) - self._wait_for_ask("0x11 address failed") - N = (lng - 1) & 0xFF - crc = N ^ 0xFF - self.sp.write(chr(N) + chr(crc)) - self._wait_for_ask("0x11 length failed") - return map(lambda c: ord(c), self.sp.read(lng)) - else: - raise CmdException("ReadMemory (0x11) failed") - - - def cmdGo(self, addr): - if self.cmdGeneric(0x21): - mdebug(10, "*** Go command") - self.sp.write(self._encode_addr(addr)) - self._wait_for_ask("0x21 go failed") - else: - raise CmdException("Go (0x21) failed") - - - def cmdWriteMemory(self, addr, data): - assert(len(data) <= 256) - if self.cmdGeneric(0x31): - mdebug(10, "*** Write memory command") - self.sp.write(self._encode_addr(addr)) - self._wait_for_ask("0x31 address failed") - #map(lambda c: hex(ord(c)), data) - lng = (len(data)-1) & 0xFF - mdebug(10, " %s bytes to write" % [lng+1]); - self.sp.write(chr(lng)) # len really - crc = 0xFF - for c in data: - crc = crc ^ c - self.sp.write(chr(c)) - self.sp.write(chr(crc)) - self._wait_for_ask("0x31 programming failed") - mdebug(10, " Write memory done") - else: - raise CmdException("Write memory (0x31) failed") - - - def cmdEraseMemory(self, sectors = None): - if self.extended_erase: - return cmd.cmdExtendedEraseMemory() - - if self.cmdGeneric(0x43): - mdebug(10, "*** Erase memory command") - if sectors is None: - # Global erase - self.sp.write(chr(0xFF)) - self.sp.write(chr(0x00)) - else: - # Sectors erase - self.sp.write(chr((len(sectors)-1) & 0xFF)) - crc = 0xFF - for c in sectors: - crc = crc ^ c - self.sp.write(chr(c)) - self.sp.write(chr(crc)) - self._wait_for_ask("0x43 erasing failed") - mdebug(10, " Erase memory done") - else: - raise CmdException("Erase memory (0x43) failed") - - def cmdExtendedEraseMemory(self): - if self.cmdGeneric(0x44): - mdebug(10, "*** Extended Erase memory command") - # Global mass erase - self.sp.write(chr(0xFF)) - self.sp.write(chr(0xFF)) - # Checksum - self.sp.write(chr(0x00)) - tmp = self.sp.timeout - self.sp.timeout = 30 - print "Extended erase (0x44), this can take ten seconds or more" - self._wait_for_ask("0x44 erasing failed") - self.sp.timeout = tmp - mdebug(10, " Extended Erase memory done") - else: - raise CmdException("Extended Erase memory (0x44) failed") - - def cmdWriteProtect(self, sectors): - if self.cmdGeneric(0x63): - mdebug(10, "*** Write protect command") - self.sp.write(chr((len(sectors)-1) & 0xFF)) - crc = 0xFF - for c in sectors: - crc = crc ^ c - self.sp.write(chr(c)) - self.sp.write(chr(crc)) - self._wait_for_ask("0x63 write protect failed") - mdebug(10, " Write protect done") - else: - raise CmdException("Write Protect memory (0x63) failed") - - def cmdWriteUnprotect(self): - if self.cmdGeneric(0x73): - mdebug(10, "*** Write Unprotect command") - self._wait_for_ask("0x73 write unprotect failed") - self._wait_for_ask("0x73 write unprotect 2 failed") - mdebug(10, " Write Unprotect done") - else: - raise CmdException("Write Unprotect (0x73) failed") - - def cmdReadoutProtect(self): - if self.cmdGeneric(0x82): - mdebug(10, "*** Readout protect command") - self._wait_for_ask("0x82 readout protect failed") - self._wait_for_ask("0x82 readout protect 2 failed") - mdebug(10, " Read protect done") - else: - raise CmdException("Readout protect (0x82) failed") - - def cmdReadoutUnprotect(self): - if self.cmdGeneric(0x92): - mdebug(10, "*** Readout Unprotect command") - self._wait_for_ask("0x92 readout unprotect failed") - self._wait_for_ask("0x92 readout unprotect 2 failed") - mdebug(10, " Read Unprotect done") - else: - raise CmdException("Readout unprotect (0x92) failed") - - -# Complex commands section - - def readMemory(self, addr, lng): - data = [] - if usepbar: - widgets = ['Reading: ', Percentage(),', ', ETA(), ' ', Bar()] - pbar = ProgressBar(widgets=widgets,maxval=lng, term_width=79).start() - - while lng > 256: - if usepbar: - pbar.update(pbar.maxval-lng) - else: - mdebug(5, "Read %(len)d bytes at 0x%(addr)X" % {'addr': addr, 'len': 256}) - data = data + self.cmdReadMemory(addr, 256) - addr = addr + 256 - lng = lng - 256 - if usepbar: - pbar.update(pbar.maxval-lng) - pbar.finish() - else: - mdebug(5, "Read %(len)d bytes at 0x%(addr)X" % {'addr': addr, 'len': 256}) - data = data + self.cmdReadMemory(addr, lng) - return data - - def writeMemory(self, addr, data): - lng = len(data) - if usepbar: - widgets = ['Writing: ', Percentage(),' ', ETA(), ' ', Bar()] - pbar = ProgressBar(widgets=widgets, maxval=lng, term_width=79).start() - - offs = 0 - while lng > 256: - if usepbar: - pbar.update(pbar.maxval-lng) - else: - mdebug(5, "Write %(len)d bytes at 0x%(addr)X" % {'addr': addr, 'len': 256}) - self.cmdWriteMemory(addr, data[offs:offs+256]) - offs = offs + 256 - addr = addr + 256 - lng = lng - 256 - if usepbar: - pbar.update(pbar.maxval-lng) - pbar.finish() - else: - mdebug(5, "Write %(len)d bytes at 0x%(addr)X" % {'addr': addr, 'len': 256}) - self.cmdWriteMemory(addr, data[offs:offs+lng] + ([0xFF] * (256-lng)) ) - - - - - def __init__(self) : - pass - - -def usage(): - print """Usage: %s [-hqVewvr] [-l length] [-p port] [-b baud] [-a addr] [-g addr] [file.bin] - -h This help - -q Quiet - -V Verbose - -e Erase - -w Write - -v Verify - -r Read - -l length Length of read - -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) - -b baud Baud speed (default: 115200) - -a addr Target address - -g addr Address to start running at (0x08000000, usually) - - ./stm32loader.py -e -w -v example/main.bin - - """ % sys.argv[0] - - -if __name__ == "__main__": - - # Import Psyco if available - try: - import psyco - psyco.full() - print "Using Psyco..." - except ImportError: - pass - - conf = { - 'port': '/dev/tty.usbserial-ftCYPMYJ', - 'baud': 115200, - 'address': 0x08000000, - 'erase': 0, - 'write': 0, - 'verify': 0, - 'read': 0, - 'go_addr':-1, - } - -# http://www.python.org/doc/2.5.2/lib/module-getopt.html - - try: - opts, args = getopt.getopt(sys.argv[1:], "hqVewvrp:b:a:l:g:") - except getopt.GetoptError, err: - # print help information and exit: - print str(err) # will print something like "option -a not recognized" - usage() - sys.exit(2) - - QUIET = 5 - - for o, a in opts: - if o == '-V': - QUIET = 10 - elif o == '-q': - QUIET = 0 - elif o == '-h': - usage() - sys.exit(0) - elif o == '-e': - conf['erase'] = 1 - elif o == '-w': - conf['write'] = 1 - elif o == '-v': - conf['verify'] = 1 - elif o == '-r': - conf['read'] = 1 - elif o == '-p': - conf['port'] = a - elif o == '-b': - conf['baud'] = eval(a) - elif o == '-a': - conf['address'] = eval(a) - elif o == '-g': - conf['go_addr'] = eval(a) - elif o == '-l': - conf['len'] = eval(a) - else: - assert False, "unhandled option" - - cmd = CommandInterface() - cmd.open(conf['port'], conf['baud']) - mdebug(10, "Open port %(port)s, baud %(baud)d" % {'port':conf['port'], 'baud':conf['baud']}) - try: - try: - cmd.initChip() - except: - print "Can't init. Ensure that BOOT0 is enabled and reset device" - - - bootversion = cmd.cmdGet() - mdebug(0, "Bootloader version %X" % bootversion) - id = cmd.cmdGetID() - mdebug(0, "Chip id: 0x%x (%s)" % (id, chip_ids.get(id, "Unknown"))) -# cmd.cmdGetVersion() -# cmd.cmdGetID() -# cmd.cmdReadoutUnprotect() -# cmd.cmdWriteUnprotect() -# cmd.cmdWriteProtect([0, 1]) - - if (conf['write'] or conf['verify']): - data = map(lambda c: ord(c), file(args[0], 'rb').read()) - - if conf['erase']: - cmd.cmdEraseMemory() - - if conf['write']: - cmd.writeMemory(conf['address'], data) - - if conf['verify']: - verify = cmd.readMemory(conf['address'], len(data)) - if(data == verify): - print "Verification OK" - else: - print "Verification FAILED" - print str(len(data)) + ' vs ' + str(len(verify)) - for i in xrange(0, len(data)): - if data[i] != verify[i]: - print hex(i) + ': ' + hex(data[i]) + ' vs ' + hex(verify[i]) - - if not conf['write'] and conf['read']: - rdata = cmd.readMemory(conf['address'], conf['len']) - file(args[0], 'wb').write(''.join(map(chr,rdata))) - - if conf['go_addr'] != -1: - cmd.cmdGo(conf['go_addr']) - - finally: - cmd.releaseChip() - diff --git a/tests/data/small.hex b/tests/data/small.hex new file mode 100644 index 0000000..270a249 --- /dev/null +++ b/tests/data/small.hex @@ -0,0 +1,2 @@ +:10000000000102030405060708090A0B0C0D0E0F78 +:00000001FF diff --git a/tests/integration/test_full_cycle.py b/tests/integration/test_full_cycle.py new file mode 100644 index 0000000..c0dbf6a --- /dev/null +++ b/tests/integration/test_full_cycle.py @@ -0,0 +1,47 @@ +from pathlib import Path + +from stm32loader.bootloader import Stm32Bootloader +from stm32loader.emulated.fake import FakeConfiguration, FakeConnection +from stm32loader.main import Stm32Loader + +FIRMWARE_FILE = Path(__file__).parent / "../../firmware/generic_boot20_pc13.binary.bin" + + +def test_erase_write_verify_passes(): + loader = Stm32Loader() + loader.configuration = FakeConfiguration( + erase=True, + write=True, + verify=True, + write_protect=False, + write_unprotect=False, + firmware_file=FIRMWARE_FILE, + family=None, + ) + loader.connection = FakeConnection() + loader.stm32 = Stm32Bootloader(loader.connection, device_family="F1", verbosity=5) + + loader.detect_device() + loader.read_device_uid() + loader.read_flash_size() + loader.perform_commands() + + +def test_write_protect_passes(): + loader = Stm32Loader() + loader.configuration = FakeConfiguration( + erase=False, + write=False, + verify=False, + write_protect=True, + write_unprotect=True, + firmware_file=None, + family=None, + ) + loader.connection = FakeConnection() + loader.stm32 = Stm32Bootloader(loader.connection, verbosity=5) + + loader.detect_device() + loader.read_device_uid() + loader.read_flash_size() + loader.perform_commands() diff --git a/tests/integration/test_stm32bootloader.py b/tests/integration/test_stm32bootloader.py new file mode 100644 index 0000000..b4443e4 --- /dev/null +++ b/tests/integration/test_stm32bootloader.py @@ -0,0 +1,63 @@ +""" +Tests for the stm32loader.bootloader. + +Several of these tests require an actual STM32 microcontroller to be +connected, and to be programmable (including RESET and BOOT0 toggling). + +These hardware tests are disabled by default. +To enable them, configure the device parameters below and +supply the following as argument to pytest: + + -m "hardware" + +""" + +import pytest + +from stm32loader.bootloader import Stm32Bootloader +from stm32loader.uart import SerialConnection + +SERIAL_PORT = "COM7" +BAUD_RATE = 9600 + +# pylint: disable=missing-docstring, redefined-outer-name + + +@pytest.fixture +def serial_connection(): + serial_connection = SerialConnection(SERIAL_PORT, BAUD_RATE) + serial_connection.connect() + return serial_connection + + +@pytest.fixture +def stm32(serial_connection): + stm32 = Stm32Bootloader(serial_connection) + return stm32 + + +@pytest.mark.hardware +def test_erase_with_page_erases_only_that_page(stm32): + stm32.reset_from_system_memory() + base = 0x08000000 + before, middle, after = base + 0, base + 1024, base + 2048 + + # erase full device and check that it reset data bytes + stm32.erase_memory() + assert all(byte == 0xFF for byte in stm32.read_memory(before, 16)) + assert all(byte == 0xFF for byte in stm32.read_memory(middle, 16)) + assert all(byte == 0xFF for byte in stm32.read_memory(after, 16)) + + # write zeros to three pages and verify data has changed + stm32.write_memory(before, bytearray([0x00] * 16)) + stm32.write_memory(middle, bytearray([0x00] * 16)) + stm32.write_memory(after, bytearray([0x00] * 16)) + assert all(byte == 0x00 for byte in stm32.read_memory(before, 16)) + assert all(byte == 0x00 for byte in stm32.read_memory(middle, 16)) + assert all(byte == 0x00 for byte in stm32.read_memory(after, 16)) + + # erase only the middle page and check only that one's bytes are rest + stm32.erase_memory(pages=[1]) + assert all(byte == 0x00 for byte in stm32.read_memory(before, 16)) + assert all(byte == 0xFF for byte in stm32.read_memory(middle, 256)) + assert all(byte == 0x00 for byte in stm32.read_memory(after, 16)) diff --git a/tests/integration/test_stm32loader.py b/tests/integration/test_stm32loader.py new file mode 100644 index 0000000..de97ba4 --- /dev/null +++ b/tests/integration/test_stm32loader.py @@ -0,0 +1,141 @@ +""" +Tests for the stm32loader executable and main() method. + +Several of these tests require an actual STM32 microcontroller to be +connected, and to be programmable (including RESET and BOOT0 toggling). + +These hardware tests are disabled by default. +To enable them, configure the device parameters below and +supply the following as argument to pytest: + + -m "hardware" + +""" + +import os +import subprocess + +import pytest + +from stm32loader.main import main + +HERE = os.path.split(os.path.abspath(__file__))[0] + +# Device dependant details +# HyTiny on Windows with FTDI adapter +STM32_CHIP_FAMILY = "F1" +STM32_CHIP_ID = "0x410" +STM32_CHIP_TYPE = "STM32F10x Medium-density" +SERIAL_PORT = "COM7" +# Flaky cable setup, cheap serial adapter... +BAUD_RATE = 9600 +KBYTE = 2**10 +SIZE = 32 * KBYTE +DUMP_FILE = "dump.bin" +FIRMWARE_FILE = os.path.join(HERE, "../../firmware/generic_boot20_pc13.binary.bin") + +# pylint: disable=missing-docstring, redefined-outer-name + + +@pytest.fixture(scope="module") +def stm32loader(): + def main_with_default_arguments(*args): + main( + "--port", + SERIAL_PORT, + "--baud", + str(BAUD_RATE), + "--quiet", + *args, + avoid_system_exit=True, + ) + + return main_with_default_arguments + + +@pytest.fixture +def dump_file(tmpdir): + return os.path.join(str(tmpdir), DUMP_FILE) + + +def test_stm32loader_is_executable(): + subprocess.call(["stm32loader", "--help"]) + + +def test_unexisting_serial_port_prints_readable_error(capsys): + main("-p", "COM108", avoid_system_exit=True) + captured = capsys.readouterr() + assert "could not open port " in captured.err + assert "port 'COM108'" in captured.err or "port COM108" in captured.err + assert "Is the device connected and powered correctly?" in captured.err + + +def test_env_var_stm32loader_serial_port_defines_port(capsys): + os.environ["STM32LOADER_SERIAL_PORT"] = "COM109" + main(avoid_system_exit=True) + captured = capsys.readouterr() + assert "port 'COM109'" in captured.err or "port COM109" in captured.err + + +def test_argument_port_overrules_env_var_for_serial_port(capsys): + os.environ["STM32LOADER_SERIAL_PORT"] = "COM120" + main("--port", "COM121", avoid_system_exit=True) + captured = capsys.readouterr() + assert "port 'COM121'" in captured.err or "port COM121" in captured.err + + +@pytest.mark.hardware +@pytest.mark.missing_hardware +def test_device_not_connected_prints_readable_error(stm32loader, capsys): + stm32loader() + captured = capsys.readouterr() + assert "Can't init into bootloader." in captured.err + assert "Ensure that BOOT0 is enabled and reset the device." in captured.err + + +@pytest.mark.hardware +def test_argument_family_prints_chip_id_and_device_type(stm32loader, capsys): + stm32loader("--family", STM32_CHIP_FAMILY) + captured = capsys.readouterr() + assert STM32_CHIP_ID in captured.err + assert STM32_CHIP_TYPE in captured.err + + +@pytest.mark.hardware +def test_read_produces_file_of_correct_length(stm32loader, dump_file): + stm32loader("--read", "--length", "1024", dump_file) + assert os.stat(dump_file).st_size == 1024 + + +@pytest.mark.hardware +def test_erase_resets_memory_to_all_ones(stm32loader, dump_file): + # erase + stm32loader("--erase") + # read all bytes and check if they're 0xFF + stm32loader("-r", "-l", "1024", dump_file) + read_data = bytearray(open(dump_file, "rb").read()) + assert all(byte == 0xFF for byte in read_data) + + +@pytest.mark.hardware +def test_write_saves_correct_data(stm32loader, dump_file): + # erase and write + stm32loader("--erase", "--write", FIRMWARE_FILE) + + # read and compare data with file on disk + stm32loader("--read", "--length", str(SIZE), dump_file) + read_data = open(dump_file, "rb").read() + original_data = open(FIRMWARE_FILE, "rb").read() + + for address, data in enumerate(zip(read_data, original_data)): + read_byte, original_byte = data + assert read_byte == original_byte, "Data mismatch at byte %s: %d vs %d" % ( + address, + read_byte, + original_byte, + ) + + +@pytest.mark.hardware +def test_erase_write_verify_passes(stm32loader): + stm32loader("--erase", "--write", "--verify", FIRMWARE_FILE) diff --git a/tests/unit/devices_stm32flash.py b/tests/unit/devices_stm32flash.py new file mode 100644 index 0000000..7872016 --- /dev/null +++ b/tests/unit/devices_stm32flash.py @@ -0,0 +1,1033 @@ +p_128 = 128 +p_256 = 128 +p_1k = 1024 +p_2k = 2 * 1024 +p_4k = 4 * 1024 +p_8k = 8 * 1024 +p_128k = 128 * 1024 +F_OBLL = "unknown" +f2f4 = "unknown" +f4db = "unknown" +f7 = "unknown" +F_NO_ME = "no-mass-erase" +F_PEMPTY = "unknown" + +# ruff: noqa: E501 +# ruff: noqa: W505 + +DEVICE_TABLE = [ + # ID name SRAM-address-range FLASH-address-range PPS PSize Option-byte-addr-range System-mem-addr-range Flags */ + # C0 + # (0x443, "STM32C011xx" , 0x20001000, 0x20003000, 0x08000000, x , x, x , x , x , 0x1FFF0000, 0x1FFF1800, 0) + # (0x453, "STM32C031xx" , 0x20001000, 0x20001800, 0x08000000, x , x, x , x , x , 0x1FFF0000, 0x1FFF1800, 0) + # F0 + ( + 0x440, + "STM32F030x8/F05xxx", + 0x20000800, + 0x20002000, + 0x08000000, + 0x08010000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFEC00, + 0x1FFFF800, + 0, + ), + ( + 0x444, + "STM32F03xx4/6", + 0x20000800, + 0x20001000, + 0x08000000, + 0x08008000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFEC00, + 0x1FFFF800, + 0, + ), + ( + 0x442, + "STM32F030xC/F09xxx", + 0x20001800, + 0x20008000, + 0x08000000, + 0x08040000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFD800, + 0x1FFFF800, + F_OBLL, + ), + ( + 0x445, + "STM32F04xxx/F070x6", + 0x20001800, + 0x20001800, + 0x08000000, + 0x08008000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFC400, + 0x1FFFF800, + 0, + ), + ( + 0x448, + "STM32F070xB/F071xx/F072xx", + 0x20001800, + 0x20004000, + 0x08000000, + 0x08020000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFC800, + 0x1FFFF800, + 0, + ), + # F1 + ( + 0x412, + "STM32F10xxx Low-density", + 0x20000200, + 0x20002800, + 0x08000000, + 0x08008000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFF000, + 0x1FFFF800, + 0, + ), + ( + 0x410, + "STM32F10xxx Medium-density", + 0x20000200, + 0x20005000, + 0x08000000, + 0x08020000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFF000, + 0x1FFFF800, + 0, + ), + ( + 0x414, + "STM32F10xxx High-density", + 0x20000200, + 0x20010000, + 0x08000000, + 0x08080000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFF000, + 0x1FFFF800, + 0, + ), + ( + 0x420, + "STM32F10xxx Medium-density VL", + 0x20000200, + 0x20002000, + 0x08000000, + 0x08020000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFF000, + 0x1FFFF800, + 0, + ), + ( + 0x428, + "STM32F10xxx High-density VL", + 0x20000200, + 0x20008000, + 0x08000000, + 0x08080000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFF000, + 0x1FFFF800, + 0, + ), + ( + 0x418, + "STM32F105xx/F107xx", + 0x20001000, + 0x20010000, + 0x08000000, + 0x08040000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFB000, + 0x1FFFF800, + 0, + ), + ( + 0x430, + "STM32F10xxx XL-density", + 0x20000800, + 0x20018000, + 0x08000000, + 0x08100000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFE000, + 0x1FFFF800, + 0, + ), + # F2 + ( + 0x411, + "STM32F2xxxx", + 0x20002000, + 0x20020000, + 0x08000000, + 0x08100000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + # F3 + ( + 0x432, + "STM32F373xx/F378xx", + 0x20001400, + 0x20008000, + 0x08000000, + 0x08040000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFD800, + 0x1FFFF800, + 0, + ), + ( + 0x422, + "STM32F302xB(C)/F303xB(C)/F358xx", + 0x20001400, + 0x2000A000, + 0x08000000, + 0x08040000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFD800, + 0x1FFFF800, + 0, + ), + ( + 0x439, + "STM32F301xx/F302x4(6/8)/F318xx", + 0x20001800, + 0x20004000, + 0x08000000, + 0x08010000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFD800, + 0x1FFFF800, + 0, + ), + ( + 0x438, + "STM32F303x4(6/8)/F334xx/F328xx", + 0x20001800, + 0x20003000, + 0x08000000, + 0x08010000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFD800, + 0x1FFFF800, + 0, + ), + ( + 0x446, + "STM32F302xD(E)/F303xD(E)/F398xx", + 0x20001800, + 0x20010000, + 0x08000000, + 0x08080000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFD800, + 0x1FFFF800, + 0, + ), + # F4 + ( + 0x413, + "STM32F40xxx/41xxx", + 0x20003000, + 0x20020000, + 0x08000000, + 0x08100000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x419, + "STM32F42xxx/43xxx", + 0x20003000, + 0x20030000, + 0x08000000, + 0x08200000, + 1, + f4db, + 0x1FFEC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x423, + "STM32F401xB(C)", + 0x20003000, + 0x20010000, + 0x08000000, + 0x08040000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x433, + "STM32F401xD(E)", + 0x20003000, + 0x20018000, + 0x08000000, + 0x08080000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x458, + "STM32F410xx", + 0x20003000, + 0x20008000, + 0x08000000, + 0x08020000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x431, + "STM32F411xx", + 0x20003000, + 0x20020000, + 0x08000000, + 0x08080000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x441, + "STM32F412xx", + 0x20003000, + 0x20040000, + 0x08000000, + 0x08100000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x421, + "STM32F446xx", + 0x20003000, + 0x20020000, + 0x08000000, + 0x08080000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x434, + "STM32F469xx/479xx", + 0x20003000, + 0x20060000, + 0x08000000, + 0x08200000, + 1, + f4db, + 0x1FFEC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x463, + "STM32F413xx/423xx", + 0x20003000, + 0x20050000, + 0x08000000, + 0x08180000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + # F7 + ( + 0x452, + "STM32F72xxx/73xxx", + 0x20004000, + 0x20040000, + 0x08000000, + 0x08080000, + 1, + f2f4, + 0x1FFF0000, + 0x1FFF001F, + 0x1FF00000, + 0x1FF0EDC0, + 0, + ), + ( + 0x449, + "STM32F74xxx/75xxx", + 0x20004000, + 0x20050000, + 0x08000000, + 0x08100000, + 1, + f7, + 0x1FFF0000, + 0x1FFF001F, + 0x1FF00000, + 0x1FF0EDC0, + 0, + ), + ( + 0x451, + "STM32F76xxx/77xxx", + 0x20004000, + 0x20080000, + 0x08000000, + 0x08200000, + 1, + f7, + 0x1FFF0000, + 0x1FFF001F, + 0x1FF00000, + 0x1FF0EDC0, + 0, + ), + # G0 + ( + 0x466, + "STM32G03xxx/04xxx", + 0x20001000, + 0x20002000, + 0x08000000, + 0x08010000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF787F, + 0x1FFF0000, + 0x1FFF2000, + 0, + ), + ( + 0x460, + "STM32G07xxx/08xxx", + 0x20002700, + 0x20009000, + 0x08000000, + 0x08020000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF787F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x467, + "STM32G0B0/B1/C1xx", + 0x20004000, + 0x20020000, + 0x08000000, + 0x08080000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF787F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x456, + "STM32G05xxx/061xx", + 0x20001000, + 0x20004800, + 0x08000000, + 0x08010000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF787F, + 0x1FFF0000, + 0x1FFF2000, + 0, + ), + # G4 + ( + 0x468, + "STM32G431xx/441xx", + 0x20004000, + 0x20005800, + 0x08000000, + 0x08020000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF782F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x469, + "STM32G47xxx/48xxx", + 0x20004000, + 0x20018000, + 0x08000000, + 0x08080000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF782F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x479, + "STM32G491xx/A1xx", + 0x20004000, + 0x2001C000, + 0x08000000, + 0x08080000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF782F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + # H7 + ( + 0x483, + "STM32H72xxx/73xxx", + 0x20004100, + 0x20020000, + 0x08000000, + 0x08100000, + 1, + p_128k, + 0, + 0, + 0x1FF00000, + 0x1FF1E800, + 0, + ), + ( + 0x450, + "STM32H74xxx/75xxx", + 0x20004100, + 0x20020000, + 0x08000000, + 0x08200000, + 1, + p_128k, + 0, + 0, + 0x1FF00000, + 0x1FF1E800, + 0, + ), + ( + 0x480, + "STM32H7A3xx/B3xx", + 0x20004100, + 0x20020000, + 0x08000000, + 0x08100000, + 1, + p_8k, + 0, + 0, + 0x1FF00000, + 0x1FF14000, + 0, + ), + # L0 + ( + 0x457, + "STM32L01xxx/02xxx", + 0x20000800, + 0x20000800, + 0x08000000, + 0x08004000, + 32, + p_128, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF01000, + F_NO_ME, + ), + ( + 0x425, + "STM32L031xx/041xx", + 0x20001000, + 0x20002000, + 0x08000000, + 0x08008000, + 32, + p_128, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF01000, + F_NO_ME, + ), + ( + 0x417, + "STM32L05xxx/06xxx", + 0x20001000, + 0x20002000, + 0x08000000, + 0x08010000, + 32, + p_128, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF01000, + F_NO_ME, + ), + ( + 0x447, + "STM32L07xxx/08xxx", + 0x20002000, + 0x20005000, + 0x08000000, + 0x08030000, + 32, + p_128, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF02000, + F_NO_ME, + ), + # L1 + ( + 0x416, + "STM32L1xxx6(8/B)", + 0x20000800, + 0x20004000, + 0x08000000, + 0x08020000, + 16, + p_256, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF01000, + F_NO_ME, + ), + ( + 0x429, + "STM32L1xxx6(8/B)A", + 0x20001000, + 0x20008000, + 0x08000000, + 0x08020000, + 16, + p_256, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF01000, + F_NO_ME, + ), + ( + 0x427, + "STM32L1xxxC", + 0x20001000, + 0x20008000, + 0x08000000, + 0x08040000, + 16, + p_256, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF02000, + F_NO_ME, + ), + ( + 0x436, + "STM32L1xxxD", + 0x20001000, + 0x2000C000, + 0x08000000, + 0x08060000, + 16, + p_256, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF02000, + F_NO_ME, + ), + ( + 0x437, + "STM32L1xxxE", + 0x20001000, + 0x20014000, + 0x08000000, + 0x08080000, + 16, + p_256, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF02000, + F_NO_ME, + ), + # L4 + ( + 0x464, + "STM32L412xx/422xx", + 0x20003100, + 0x20008000, + 0x08000000, + 0x08020000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF780F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x435, + "STM32L43xxx/44xxx", + 0x20003100, + 0x2000C000, + 0x08000000, + 0x08040000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF780F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x462, + "STM32L45xxx/46xxx", + 0x20003100, + 0x20020000, + 0x08000000, + 0x08080000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF780F, + 0x1FFF0000, + 0x1FFF7000, + F_PEMPTY, + ), + ( + 0x415, + "STM32L47xxx/48xxx", + 0x20003100, + 0x20018000, + 0x08000000, + 0x08100000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFFF80F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x461, + "STM32L496xx/4A6xx", + 0x20003100, + 0x20040000, + 0x08000000, + 0x08100000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFFF80F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x470, + "STM32L4Rxx/4Sxx", + 0x20003200, + 0x200A0000, + 0x08000000, + 0x08100000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFFF80F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x471, + "STM32L4P5xx/Q5xx", + 0x20004000, + 0x20050000, + 0x08000000, + 0x08100000, + 1, + p_4k, + 0x1FF00000, + 0x1FF0000F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), # dual-bank + # L5 + ( + 0x472, + "STM32L552xx/562xx", + 0x20004000, + 0x20040000, + 0x08000000, + 0x08080000, + 1, + p_2k, + 0, + 0, + 0x0BF90000, + 0x0BF98000, + 0, + ), # dual-bank + # WB + ( + 0x494, + "STM32WB10xx/15xx", + 0x20005000, + 0x20040000, + 0x08000000, + 0x08050000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF787F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x495, + "STM32WB30(5)xx/50(5)xx", + 0x20004000, + 0x2000C000, + 0x08000000, + 0x08100000, + 1, + p_4k, + 0x1FFF8000, + 0x1FFF807F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + # WL + ( + 0x497, + "STM32WLE5xx/WL55xx", + 0x20002000, + 0x20010000, + 0x08000000, + 0x08040000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF8000, + 0x1FFF0000, + 0x1FFF4000, + 0, + ), + # U5 + ( + 0x482, + "STM32U575xx/585xx", + 0x20004000, + 0x200C0000, + 0x08000000, + 0x08200000, + 1, + p_8k, + 0, + 0, + 0x0BF90000, + 0x0BFA0000, + 0, + ), + # These are not (yet) in AN2606: + ( + 0x641, + "Medium_Density PL", + 0x20000200, + 0x20005000, + 0x08000000, + 0x08020000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFF000, + 0x1FFFF800, + 0, + ), + ( + 0x9A8, + "STM32W-128K", + 0x20000200, + 0x20002000, + 0x08000000, + 0x08020000, + 4, + p_1k, + 0x08040800, + 0x0804080F, + 0x08040000, + 0x08040800, + 0, + ), + ( + 0x9B0, + "STM32W-256K", + 0x20000200, + 0x20004000, + 0x08000000, + 0x08040000, + 4, + p_2k, + 0x08040800, + 0x0804080F, + 0x08040000, + 0x08040800, + 0, + ), +] + +KEYS = [ + "product_id", + "device_name", + "ram_start", + "ram_end", + "flash_start", + "flash_end", + "pages_per_sector", + "page_size", + "option_start", + "option_end", + "system_mem_start", + "system_mem_end", + "flag", +] + +DEVICES = [dict(zip(KEYS, details)) for details in DEVICE_TABLE] diff --git a/tests/unit/test_arguments.py b/tests/unit/test_arguments.py new file mode 100644 index 0000000..c2690ca --- /dev/null +++ b/tests/unit/test_arguments.py @@ -0,0 +1,53 @@ +import atexit + +import pytest + +from stm32loader.main import Stm32Loader + + +@pytest.fixture +def program(): + return Stm32Loader() + + +def test_parse_arguments_without_args_raises_typeerror(program): + with pytest.raises(TypeError, match="missing.*required.*argument"): + program.parse_arguments() + + +def test_parse_arguments_with_standard_args_passes(program): + program.parse_arguments(["-p", "port", "-b", "9600", "-q"]) + + +@pytest.mark.parametrize( + "help_argument", + ["-h", "--help"], +) +def test_parse_arguments_with_help_raises_systemexit(program, help_argument): + with pytest.raises(SystemExit): + program.parse_arguments([help_argument]) + + +def test_parse_arguments_erase_without_port_complains_about_missing_argument(program, capsys): + try: + program.parse_arguments(["-e", "-w", "-v", "file.bin"]) + except SystemExit: + pass + + # Also call atexit functions so that the hint about using an env variable + # is printed. + atexit._run_exitfuncs() + + _output, error_output = capsys.readouterr() + if not error_output: + pytest.skip("Not sure why nothing is captured in some pytest runs?") + assert "arguments are required: -p/--port" in error_output + assert "STM32LOADER_SERIAL_PORT" in error_output + + +def test_parse_arguments_write_unprotect(program): + program.parse_arguments(["--write-unprotect"]) + + +def test_parse_arguments_write_protect(program): + program.parse_arguments(["--write-protect"]) diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py new file mode 100644 index 0000000..3ad9f24 --- /dev/null +++ b/tests/unit/test_bootloader.py @@ -0,0 +1,502 @@ +"""Unit tests for the Stm32Loader class.""" + +from unittest.mock import MagicMock + +import pytest + +from stm32loader import bootloader as Stm32 +from stm32loader.bootloader import PageIndexError, Stm32Bootloader +from stm32loader.device_info import DeviceInfo +from stm32loader.devices import DEVICES + +# pylint: disable=missing-docstring, redefined-outer-name + + +@pytest.fixture +def connection(): + connection = MagicMock() + connection.read.return_value = [Stm32Bootloader.Reply.ACK] + return connection + + +@pytest.fixture +def write(connection): + connection.write.written_data = bytearray() + + def log_written_data(data): + connection.write.written_data.extend(data) + + def data_was_written(data): + return data in connection.write.written_data + + connection.write.data_was_written = data_was_written + connection.write.side_effect = log_written_data + return connection.write + + +@pytest.fixture +def bootloader(connection): + # STM32F358xx + device = DEVICES.get((0x422, 0x50)) + assert device is not None, "Device not found in DEVICES mapping" + return Stm32Bootloader(connection, device=device) + + +def test_constructor_with_connection_none_passes(): + Stm32Bootloader(connection=None) + + +def test_constructor_does_not_use_connection_directly(connection): + Stm32Bootloader(connection) + assert not connection.method_calls + + +def test_write_without_data_sends_no_bytes(bootloader, write): + bootloader.write() + assert not write.written_data + + +def test_write_with_bytes_sends_bytes_verbatim(bootloader, write): + bootloader.write(b"\x00\x11") + assert write.data_was_written(b"\x00\x11") + + +def test_write_with_integers_sends_integers_as_bytes(bootloader, write): + bootloader.write(0x03, 0x0A) + assert write.data_was_written(b"\x03\x0a") + + +def test_write_and_ack_with_nack_response_raises_commandexception(bootloader): + bootloader.connection.read = MagicMock() + bootloader.connection.read.return_value = [Stm32Bootloader.Reply.NACK] + with pytest.raises(Stm32.CommandError, match="custom message"): + bootloader.write_and_ack("custom message", 0x00) + + +def test_write_memory_with_length_higher_than_256_raises_data_length_error(bootloader): + with pytest.raises( + Stm32.DataLengthError, match=r"Can not write more than 256 bytes at once\." + ): + bootloader.write_memory(0, [1] * 257) + + +def test_write_memory_with_zero_bytes_does_not_send_anything(bootloader, connection): + bootloader.write_memory(0, b"") + assert not connection.method_calls + + +def test_write_memory_with_single_byte_sends_four_data_bytes_padded_with_0xff(bootloader, write): + bootloader.write_memory(0, b"1") + assert write.data_was_written(b"1\xff\xff\xff") + + +def test_write_memory_sends_correct_number_of_bytes(bootloader, write): + bootloader.write_memory(0, bytearray([0] * 4)) + # command byte, control byte, 4 address bytes, address checksum, + # length byte, 4 data bytes, checksum byte + byte_count = 2 + 4 + 1 + 1 + 4 + 1 + assert len(write.written_data) == byte_count + + +def test_read_memory_with_length_higher_than_256_raises_data_length_error(bootloader): + with pytest.raises( + Stm32.DataLengthError, match=r"Can not read more than 256 bytes at once\." + ): + bootloader.read_memory(0, length=257) + + +def test_read_memory_sends_address_with_checksum(bootloader, write): + bootloader.read_memory(0x0F, 4) + assert write.data_was_written(b"\x00\x00\x00\x0f\x0f") + + +def test_read_memory_sends_length_with_checksum(bootloader, write): + bootloader.read_memory(0, 0x0F + 1) + assert write.data_was_written(b"\x0f\xf0") + + +def test_command_sends_command_and_control_bytes(bootloader, write): + bootloader.command(0x01, "bogus command") + assert write.data_was_written(b"\x01\xfe") + + +def test_reset_from_system_memory_sends_command_synchronize(bootloader, write): + bootloader.reset_from_system_memory() + synchro_command = Stm32Bootloader.Command.SYNCHRONIZE + assert write.data_was_written(bytearray([synchro_command])) + + +def test_encode_address_returns_correct_bytes_with_checksum(): + # pylint:disable=protected-access + encoded_address = Stm32Bootloader._encode_address(0x04030201) + assert bytes(encoded_address) == b"\x04\x03\x02\x01\x04" + + +def test_erase_memory_without_pages_sends_global_erase(bootloader, write): + bootloader.erase_memory() + assert write.data_was_written(b"\xff\x00") + + +def test_erase_memory_with_pages_sends_sector_count_and_eight_bit_page_indices(bootloader, write): + bootloader.erase_memory([0x11, 0x12, 0x13, 0x14]) + assert write.data_was_written(b"\x03") + assert write.data_was_written(b"\x11\x12\x13\x14") + + +def test_extended_erase_memory_with_pages_sends_sector_count_and_sixteen_bit_page_indices( + bootloader, write +): + bootloader.extended_erase_memory([0x11, 0x12, 0x13, 0x14]) + assert write.data_was_written(b"\x00\x03") + assert write.data_was_written(b"\x00\x11\x00\x12\x00\x13\x00\x14") + + +def test_erase_memory_with_pages_sends_sector_addresses_with_checksum(bootloader, write): + bootloader.erase_memory([0x01, 0x02, 0x04, 0x08]) + print(write.written_data) + assert write.data_was_written(b"\x01\x02\x04\x08\x0c") + + +def test_erase_memory_with_page_count_higher_than_255_raises_page_index_error(bootloader): + with pytest.raises(Stm32.PageIndexError, match="Can not erase more than 255 pages at once."): + bootloader.erase_memory([1] * 256) + + +def test_erase_memory_family_l0_without_pages_erases_individual_pages(connection, write): + pytest.skip("Port to device-table") + bootloader = Stm32Bootloader(connection, device_family="L0") + bootloader.command = MagicMock() + bootloader._get_flash_size_and_uid_bulk = MagicMock() + bootloader._get_flash_size_and_uid_bulk.return_value = (16, 0x01) + bootloader.erase_memory() + + # Page count - 1. + assert write.written_data[0] == 127 + # Pages. + assert write.written_data[1:3] == b"\x00\x01" + # Length: command + byte count + page-addresses + CRC + assert len(write.written_data) == 130 + + +def test_extended_erase_memory_without_pages_sends_global_mass_erase(bootloader, write): + bootloader.extended_erase_memory() + assert write.data_was_written(b"\xff\xff\x00") + + +def test_extended_erase_memory_with_page_count_higher_than_65535_raises_page_index_error( + bootloader, +): + with pytest.raises( + Stm32.PageIndexError, match="Can not erase more than 65535 pages at once." + ): + bootloader.extended_erase_memory([1] * 65536) + + +def test_extended_erase_memory_with_pages_sends_two_byte_sector_count(bootloader, write): + bootloader.extended_erase_memory([0x11, 0x12, 0x13, 0x14]) + assert write.data_was_written(b"\x00\x03") + + +def test_extended_erase_with_pages_sends_two_byte_sector_addresses_with_single_byte_checksum( + bootloader, write +): + bootloader.extended_erase_memory([0x01, 0x02, 0x04, 0x0FF0]) + assert write.data_was_written(b"\x00\x01\x00\x02\x00\x04\x0f\xf0\xfb") + + +def test_write_protect_sends_command_page_addresses_and_checksum(bootloader, write): + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 16 + bootloader.write_protect([0x01, 0x08]) + assert write.data_was_written(b"\x63\x9c\x01\x01\x08\x08\x7f"), write.written_data + + +def test_write_protect_fails_when_no_device(bootloader): + bootloader = Stm32Bootloader(connection, device_family="F0", device=None) + with pytest.raises( + Stm32.Stm32LoaderError, + match="Device type must be detected before write protection can be enabled.", + ): + bootloader.write_protect([0x01, 0x08]) + + +def test_write_protect_fails_when_not_supported(bootloader): + device = DeviceInfo( + device_family="F0", device_name="STM32F012", pid=0, bid=0, write_protect_supported=False + ) + bootloader = Stm32Bootloader(connection, device_family="F0", device=device) + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 16 + with pytest.raises( + Stm32.Stm32LoaderError, + match="Write protection support for 'STM32F012' not currently implemented.", + ): + bootloader.write_protect([0x01, 0x08]) + + +def test_write_protect_fails_on_invalid_sector_index(bootloader): + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 16 + with pytest.raises(Stm32.PageIndexError): + bootloader.write_protect([0x01, 0x08, 0x100]) + + +def test_write_protect_fails_on_too_many_sectors(bootloader): + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 16 + with pytest.raises(Stm32.DataLengthError): + bootloader.write_protect(list(range(256)) + [0, 1]) + + +def test_write_protect_fails_on_no_flash_info(connection, bootloader): + device = DeviceInfo( + device_family="F0", device_name="STM32F012", pid=0, bid=0, write_protect_supported=True + ) + device.flash = None + bootloader = Stm32Bootloader(connection, device_family="F0", device=device) + + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 16 + with pytest.raises( + Stm32.Stm32LoaderError, match="Device flash info is missing for family 'F0'" + ): + bootloader.write_protect() + + +def test_write_protect_fails_on_no_flash_sector_info(connection, bootloader): + device = DeviceInfo( + device_family="F0", device_name="STM32F012", pid=0, bid=0, write_protect_supported=True + ) + device.flash = MagicMock() + device.flash.num_sectors = MagicMock(return_value=None) + bootloader = Stm32Bootloader(connection, device_family="F0", device=device) + + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 16 + with pytest.raises( + Stm32.Stm32LoaderError, match="Device flash sector info is missing for device 'STM32F012'" + ): + bootloader.write_protect() + + +def test_write_protect_no_pages_specified(connection, write): + device = DEVICES.get((0x417, 0xC0)) + assert device is not None, "Device not found in DEVICES mapping" + + bootloader = Stm32Bootloader(connection, device_family="L0", device=device) + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 8 + # bootloader.write = write + bootloader.write_protect() + assert write.data_was_written(b"\x63"), write.written_data + # Write protect sector size is 4KB, flash size is 64KB + # 64KB / 4KB = 16 sectors (index 1) + # Checksum is 0x0f + sectors = bytes(range(0, 16)) + assert write.data_was_written(b"\x63\x9c\x0f" + sectors + b"\x0f\x7f"), write.written_data + + +def test_write_unprotect_sends_command(bootloader, write): + bootloader.write_unprotect() + assert write.data_was_written(b"\x73"), write.written_data + assert write.data_was_written(bytearray([Stm32Bootloader.Command.SYNCHRONIZE])), ( + write.written_data + ) + + +def test_verify_data_with_identical_data_passes(): + Stm32Bootloader.verify_data(b"\x05", b"\x05") + + +def test_verify_different_byte_count_raises_verify_error_complaining_about_length_difference(): + with pytest.raises( + Stm32.DataMismatchError, match=r"Data length does not match.*2.*vs.*1.*bytes" + ): + Stm32Bootloader.verify_data(b"\x05\x06", b"\x01") + + +def test_verify_non_identical_data_raises_verify_error_complaining_about_mismatched_byte(): + with pytest.raises( + Stm32.DataMismatchError, + match=r"Verification data does not match read data.*mismatch.*0x1.*0x6.*0x7", + ): + Stm32Bootloader.verify_data(b"\x05\x06", b"\x05\x07") + + +@pytest.mark.parametrize( + "pid_bid, uid_address", + [ + ((0x412, None), 0x_1FFF_F7E8), + ((0x432, 0x50), 0x_1FFF_F7AC), + ((0x452, 0x90), 0x_1FF0_F420), + ], + ids=("F1", "F3", "F7"), +) +def test_get_uid_for_known_device_reads_at_correct_address(connection, pid_bid, uid_address): + device = DEVICES.get(pid_bid) + bootloader = Stm32Bootloader(connection, device=device) + bootloader.read_memory = MagicMock() + + bootloader.get_uid() + bootloader.read_memory.assert_called_once_with(uid_address, 12) + + +def test_get_uid_for_family_without_uid_returns_uid_not_supported(connection): + device = DEVICES.get((0x443, 0x51)) + bootloader = Stm32Bootloader(connection, device=device) + assert bootloader.UID_NOT_SUPPORTED == bootloader.get_uid() + + +@pytest.mark.parametrize( + "family", + ["F4", "L0"], +) +def test_get_flash_size_and_uid_for_exception_families_returns_size_and_uid(connection, family): + pytest.skip(reason="Ditch 'family") + + bootloader = Stm32Bootloader(connection, device_family=family) + bootloader.read_memory = MagicMock() + + memory_block = bytearray([0] * 256) + + # Set up the 'UID' value (12 bytes) + # and flash_size value (2 bytes). + uid_address = bootloader.UID_ADDRESS[family] & 0xFF + memory_block[uid_address : uid_address + 12] = ( + b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c" + ) + flash_size_address = bootloader.FLASH_SIZE_ADDRESS[family] & 0xFF + memory_block[flash_size_address : flash_size_address + 2] = b"\x01\x02" + bootloader.read_memory.return_value = memory_block + + assert bootloader.get_flash_size() == 0x0201 + assert bootloader.get_uid() == b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c" + + +@pytest.mark.parametrize( + "uid_string", + [ + (Stm32Bootloader.UID_NOT_SUPPORTED, "UID not supported in this part"), + (Stm32Bootloader.UID_ADDRESS_UNKNOWN, "UID address unknown"), + ( + bytearray(b"\x12\x34\x56\x78\x9a\xbc\xde\x01\x12\x34\x56\x78"), + "3412-7856-01DEBC9A-78563412", + ), + ], +) +def test_format_uid_returns_correct_string(bootloader, uid_string): + uid, expected_description = uid_string + description = bootloader.format_uid(uid) + assert description == expected_description + + +def test_get_pages_from_range_with_invalid_start_address_raises_page_index_error(bootloader): + with pytest.raises( + PageIndexError, match=".*start address should be at a flash page boundary.*" + ): + bootloader.pages_from_range(10, 1024) + + +def test_get_pages_from_range_with_start_address_zero_returns_single_page(bootloader): + pages = bootloader.pages_from_range(0, 2048) + assert pages == [0] + + +def test_get_pages_from_range_with_end_too_small_raises_page_index_error(bootloader): + with pytest.raises( + PageIndexError, match="Erase .* address should be at a flash page boundary:.*0400.*0800" + ): + bootloader.pages_from_range(0, 1024) + + +def test_get_pages_from_large_range_returns_multiple_pages(bootloader): + pages = bootloader.pages_from_range(4 * 1024, 20 * 1024) + assert pages == list(range(2, 10)) + + +def test_get_flash_size_for_standard_family(connection): + bootloader = Stm32Bootloader(connection, device_family="F1") + bootloader.get_flash_size = MagicMock(return_value=128) + + assert bootloader.get_flash_size() == 128 + bootloader.get_flash_size.assert_called_once() + + +def test_get_uid_for_standard_family(connection): + bootloader = Stm32Bootloader(connection, device_family="F1") + bootloader.get_uid = MagicMock(return_value=b"some uid") + + assert bootloader.get_uid() == b"some uid" + bootloader.get_uid.assert_called_once() + + +def test_get_flash_size_for_exception_family_uses_bulk_read(connection): + pytest.skip("Port to device-table") + bootloader = Stm32Bootloader(connection, device_family="F4") + bootloader._get_flash_size_and_uid_bulk = MagicMock(return_value=(512, b"bulk uid")) + + assert bootloader.get_flash_size() == 512 + bootloader._get_flash_size_and_uid_bulk.assert_called_once() + + +def test_get_uid_for_exception_family_uses_bulk_read(connection): + pytest.skip(reason="Ditch 'family") + bootloader = Stm32Bootloader(connection, device_family="L0") + bootloader._get_flash_size_and_uid_bulk = MagicMock(return_value=(64, b"bulk uid")) + + assert bootloader.get_uid() == b"bulk uid" + bootloader._get_flash_size_and_uid_bulk.assert_called_once() + + +def test_get_flash_size_for_standard_family_uses_direct_read(connection): + pytest.skip(reason="Ditch 'family") + bootloader = Stm32Bootloader(connection, device_family="F1") + bootloader.read_memory = MagicMock(return_value=b"\x80\x00") # 128 KiB + + assert bootloader.get_flash_size() == 128 + bootloader.read_memory.assert_called_once() + + +def test_flash_size_and_uid_are_cached(connection): + pytest.skip(reason="Ditch 'family") + bootloader = Stm32Bootloader(connection, device_family="F1") + bootloader.read_memory = MagicMock() + bootloader.read_memory.side_effect = [b"\x80\x00", b"some uid 12b"] + + # First calls should trigger hardware reads via read_memory. + assert bootloader.get_flash_size() == 128 + assert bootloader.get_uid() == b"some uid 12b" + + assert bootloader.read_memory.call_count == 2 + + # Second calls should use cache, so read_memory should NOT be called again + assert bootloader.get_flash_size() == 128 + assert bootloader.get_uid() == b"some uid 12b" + + # read_memory should have been called exactly once for each (once for + # size, once for UID). + assert bootloader.read_memory.call_count == 2 + + +def test_get_flash_size_and_uid_bulk_populates_both_caches(connection): + pytest.skip(reason="Ditch 'family") + bootloader = Stm32Bootloader(connection, device_family="F4") + # Mock the internal bulk read method via read_memory. + bootloader.read_memory = MagicMock() + memory_block = bytearray([0] * 256) + uid_offset = bootloader.UID_ADDRESS["F4"] & 0xFF + flash_size_offset = bootloader.FLASH_SIZE_ADDRESS["F4"] & 0xFF + memory_block[uid_offset : uid_offset + 12] = b"bulk uid 12b" + memory_block[flash_size_offset : flash_size_offset + 2] = b"\x00\x02" # 512 + bootloader.read_memory.return_value = memory_block + + assert bootloader.get_flash_size() == 512 + assert bootloader.get_uid() == b"bulk uid 12b" + bootloader.read_memory.assert_called_once() + + # Subsequent calls to individual methods should use cache. + assert bootloader.get_flash_size() == 512 + assert bootloader.get_uid() == b"bulk uid 12b" + + # read_memory should still have been called only once. + bootloader.read_memory.assert_called_once() diff --git a/tests/unit/test_devices.py b/tests/unit/test_devices.py new file mode 100644 index 0000000..b06ec7e --- /dev/null +++ b/tests/unit/test_devices.py @@ -0,0 +1,366 @@ +import pytest +from devices_stm32flash import DEVICES as STM32FLASH_DEVICES + +from stm32loader.bootloader import CHIP_IDS, Stm32Bootloader +from stm32loader.device_family import DEVICE_FAMILIES, DeviceFamily +from stm32loader.device_info import Flash +from stm32loader.devices import DEVICES + +KNOWN_DUPLICATE_DEVICE_NAMES = [ + "STM32F2xxxx", + "STM32F40xxx/41xxx", + "STM32F42xxx/43xxx", + "STM32F74xxx/75xxx", + "STM32L07xxx/08xxx", + "STM32L47xxx/48xxx", + "STM32F10xxx", + "STM32W", +] + +KNOWN_RAM_EXCEPTIONS = [ + # Most of these belong to the 'known duplicate' category, + # they share the same product ID (with differing bootloader ID). + # The others received a comment in the device table. + 0x442, + 0x448, + 0x432, + 0x413, + 0x456, + 0x447, + 0x464, + 0x415, +] + + +def test_only_specific_device_names_occur_twice(): + all_names = set() + for (product_id, bootloader_id), dev in DEVICES.items(): + if bootloader_id is None: + continue + if dev.device_name in all_names: + assert dev.device_name in KNOWN_DUPLICATE_DEVICE_NAMES, dev.device_name + all_names.add(dev.device_name) + + +@pytest.mark.parametrize( + "ids", + DEVICES.keys(), + ids=lambda x: f"{x[0]}-{x[1]}", +) +def test_product_id_and_bootloader_id_match_device_properties(ids): + dev = DEVICES[ids] + device_id, bootloader_id = ids + assert dev.product_id == device_id + if bootloader_id is not None: + assert dev.bootloader_id == bootloader_id + + +@pytest.mark.parametrize( + "dev", + DEVICES.values(), + ids=lambda dev: str(dev).replace(" ", "-"), +) +def test_ram_size_is_multiple_of_256(dev): + if dev.ram_size == 0: + return + + assert isinstance(dev.ram_size, int) + assert dev.ram_size > 0, f"{dev} ram size not None but still too low: {dev.ram_size}" + assert dev.ram_size % 256 == 0, f"{dev} ram size not a multiple of 256: {dev.ram_size}" + + +@pytest.mark.parametrize( + "dev", + DEVICES.values(), + ids=lambda dev: str(dev).replace(" ", "-"), +) +def test_flash_size_multiple_of_16k(dev): + if dev.flash_size is None: + return + + assert isinstance(dev.flash_size, int) + assert dev.flash_size > 0, f"{dev} flash size not None but still too low: {dev.flash_size}" + assert dev.flash_size % (16 * 1024) == 0, ( + f"{dev} flash size not a multiple of 64: {dev.flash_size}" + ) + + +@pytest.mark.parametrize( + "dev", + DEVICES.values(), + ids=lambda dev: str(dev).replace(" ", "-"), +) +def test_system_memory_size_multiple_of_64(dev): + if dev.system_memory_size == 0: + return + + assert isinstance(dev.system_memory_size, int) + assert dev.system_memory_size > 0, ( + f"{dev} system memory size not None but still too low: {dev.system_memory_size}" + ) + assert dev.system_memory_size % 64 == 0, ( + f"{dev} system memory size not a multiple of 64: {dev.system_memory_size}" + ) + + +@pytest.mark.parametrize( + "dev", + DEVICES.values(), + ids=lambda dev: str(dev).replace(" ", "-"), +) +def test_device_name_does_not_contain_underscore(dev): + assert "_" not in dev.device_name, dev.device_name + + +def test_existing_product_ids_are_present_in_devices(): + all_product_ids = set(dev.product_id for dev in DEVICES.values()) + chip_ids = set(CHIP_IDS) + unknown_chip_ids = chip_ids - all_product_ids + assert len(unknown_chip_ids) == 0, unknown_chip_ids + + +def test_stm32flash_product_ids_are_present_in_devices(): + all_product_ids = set(dev.product_id for dev in DEVICES.values()) + chip_ids = set(dev["product_id"] for dev in STM32FLASH_DEVICES) + unknown_chip_ids = chip_ids - all_product_ids + assert len(unknown_chip_ids) == 0, unknown_chip_ids + + +@pytest.mark.parametrize( + "device", + DEVICES.values(), + ids=lambda device: str(device).replace(" ", "-"), +) +def test_stm32flash_device_names_match(device): + stm32flash_device = None + for dev in STM32FLASH_DEVICES: + if dev["product_id"] == device.product_id: + stm32flash_device = dev + break + + # Some devices don't exist in STM32Flash. + stm32loader_only = ( + 0x443, + 0x453, + 0x474, + 0x484, + 0x492, + 0x455, + 0x481, + 0x003, + 0x00F, + 0x0023, + 0x002F, + 0x801, + 0x03B, + 0x03F, + 0x05F, + 0x006, + 0x50494B36, + 0x504D4B36, + 0x50494836, + 0x504D4836, + 0x50494B37, + 0x504D4B37, + 0x50494837, + 0x504D4837, + ) + if not stm32flash_device and device.product_id in stm32loader_only: + return + + # Known / reviewed deviating names. + if device.product_id in [ + 0x440, + 0x442, + 0x445, + 0x448, + 0x412, + 0x410, + 0x414, + 0x420, + 0x428, + 0x418, + 0x430, + 0x432, + 0x422, + 0x439, + 0x438, + 0x446, + 0x467, + 0x495, + 0x641, + 0x9A8, + 0x9B0, + ]: + return + + assert stm32flash_device, f"{device.device_name} 0x{device.product_id:03X}" + assert stm32flash_device["device_name"] == device.device_name, ( + f"{device.device_name} 0x{device.product_id:03X}" + ) + + +@pytest.mark.parametrize( + "device", + DEVICES.values(), + ids=lambda device: str(device).replace(" ", "-"), +) +def test_stm32flash_ram_addresses_match(device): + ref = None + for _ref in STM32FLASH_DEVICES: + if _ref["product_id"] == device.product_id: + ref = _ref + break + + if ref is None: + # not found + return + + if device.product_id in KNOWN_RAM_EXCEPTIONS: + return + + if device.ram is None: + assert ref["ram_start"] == ref["ram_end"], ( + f"RAM size not 0 for device '{device.device_name}' 0x{device.product_id:03X}" + ) + return + + if isinstance(device.ram[0], tuple): + return + + # print(hex(device.product_id), device, device.ram, ref) + assert device.ram[0] == ref["ram_start"], ( + f"RAM start differs for device: '{device.device_name}' 0x{device.product_id:03X}:" + f" 0x{device.ram[0]:08X} vs 0x{ref['ram_start']:08X}." + ) + assert device.ram[1] == ref["ram_end"], ( + f"RAM end differs for device: '{device.device_name}' 0x{device.product_id:03X}:" + f" 0x{device.ram[1]:08X} vs 0x{ref['ram_end']:08X}." + ) + + +@pytest.mark.parametrize( + "device", + DEVICES.values(), + ids=lambda device: str(device).replace(" ", "-"), +) +def test_num_pages(device): + if device.flash is None: + return + + if device.flash.size is None or device.flash.page_size is None: + assert device.flash.num_pages() is None + return + + num_pages = device.flash.num_pages() + assert num_pages is not None, f"{device} flash page info is missing" + assert num_pages > 0, f"{device} flash page count is not positive: {num_pages}" + + +def test_flash_num_pages(): + # Like the STM32F301 + flash = Flash( + start=0x08000000, end=0x08000000 + 64 * 1024, page_size=2 * 1024, pages_per_sector=2 + ) + + assert flash.num_pages() == 32 + + +def test_flash_num_pages_mixed_page_sizes(): + flash = Flash( + start=0x08000000, + end=0x08000000 + 16 * 1024, + page_size=[2 * 1024] * 4 + [4 * 1024] * 2, + pages_per_sector=1, + ) + + assert flash.num_pages() == 6 + + +def test_flash_num_pages_no_flash_size(): + # Like the STM32F301 + flash = Flash(start=None, end=None, page_size=2 * 1024, pages_per_sector=2) + + assert flash.num_pages() is None + + +@pytest.mark.parametrize( + "device", + DEVICES.values(), + ids=lambda device: str(device).replace(" ", "-"), +) +def test_num_sectors_is_populated_when_write_protection_supported(device): + if device.flash is None or device.write_protect_supported is False: + return + + assert device.flash.num_sectors() is not None, f"{device} flash sector info is missing" + + +def test_flash_num_sectors(): + # Like the STM32F301 + flash = Flash( + start=0x08000000, end=0x08000000 + 64 * 1024, page_size=2 * 1024, pages_per_sector=2 + ) + + assert flash.num_sectors() == 16 + + +def test_flash_num_sectors_mixed_page_sizes(): + flash = Flash( + start=0x08000000, + end=0x08000000 + 16 * 1024, + page_size=[2 * 1024] * 4 + [4 * 1024] * 2, + pages_per_sector=1, + ) + + assert flash.num_sectors() == 6 + + +def test_flash_num_sectors_no_flash_size(): + flash = Flash(start=None, end=None, page_size=2 * 1024, pages_per_sector=2) + + assert flash.num_sectors() is None + + +def test_flash_num_sectors_with_max_num_sectors(): + flash = Flash( + start=0x08000000, end=0x08000000 + 1024 * 1024, page_size=2 * 1024, pages_per_sector=2 + ) + + assert flash.num_sectors() == 63 + + +def test_family_uid_address_matches_existing(): + for family_code, uid_address in Stm32Bootloader.UID_ADDRESS.items(): + if family_code == "NRG": + continue + family = DeviceFamily[family_code] + family_uid_address = DEVICE_FAMILIES[family].uid_address + assert uid_address == family_uid_address, ( + f"Device family UID address does not match: '{family_code}':" + f" 0x{uid_address:08X} vs 0x{family_uid_address:08X}." + ) + + +def test_family_flash_size_address_matches_existing(): + for family_code, size_address in Stm32Bootloader.FLASH_SIZE_ADDRESS.items(): + if family_code == "NRG": + continue + family = DeviceFamily[family_code] + family_size_address = DEVICE_FAMILIES[family].flash_size_address + assert size_address == family_size_address, ( + f"Device family flash size address does not match: '{family_code}':" + f" 0x{size_address:08X} vs 0x{family_size_address:08X}." + ) + + +def test_family_transfer_size_matches_existing(): + for family_code, transfer_size in Stm32Bootloader.DATA_TRANSFER_SIZE.items(): + if family_code in ["default", "NRG"]: + continue + family = DeviceFamily[family_code] + family_transfer_size = DEVICE_FAMILIES[family].transfer_size + assert transfer_size == family_transfer_size, ( + f"Device family transfer size does not match: '{family_code}':" + f" 0x{transfer_size:08X} vs 0x{family_transfer_size:08X}." + ) diff --git a/tests/unit/test_hexfile.py b/tests/unit/test_hexfile.py new file mode 100644 index 0000000..8f03744 --- /dev/null +++ b/tests/unit/test_hexfile.py @@ -0,0 +1,12 @@ +from pathlib import Path + +from stm32loader.hexfile import load_hex + +HERE = Path(__file__).parent +DATA = HERE / "../data" + + +def test_load_hex_delivers_bytes(): + small_hex_path = DATA / "small.hex" + data = load_hex(small_hex_path) + assert data == bytes(range(16)) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..d9cac12 --- /dev/null +++ b/tox.ini @@ -0,0 +1,26 @@ +[tox] +envlist = py39, py310, py311, pypy39, pypy310, pypy311 + +[testenv] +passenv = HOME +deps= + pytest + pyserial + intelhex +commands= + pytest -r a [] tests + +[pytest] +minversion= 2.0 +norecursedirs= .git .github .tox .nox build dist tmp* tests/integration + +[gh-actions] +python = + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 + 3.14: py314 + pypy-3.10: pypy310 + pypy-3.11: pypy311