diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 6f84de6f60e..97b5e558f40 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,3 +1,21 @@ +# all: Apply code formatting to new paths. +48c7daaa3d7068c127c5bc395ffa72029ab9bba1 + +# all: Prune trailing whitespace. +dda9b9c6da5d3c31fa8769e581a753e95a270803 + +# all: Remove the "STATIC" macro and just use "static" instead. +decf8e6a8bb940d5829ca3296790631fcece7b21 + +# renesas-ra: Fix spelling mistakes found by codespell. +b3f2f18f927fa2fad10daf63d8c391331f5edf58 + +# all: Update Python formatting to ruff-format. +bbd8760bd9a2302e5abee29db279102bb11d7732 + +# all: Fix various spelling mistakes found by codespell 2.2.6. +cf490a70917a1b2d38ba9b58e763e0837d0f7ca7 + # all: Fix spelling mistakes based on codespell check. b1229efbd1509654dec6053865ab828d769e29db diff --git a/.gitattributes b/.gitattributes index e6d31d6aa31..04a84d3e098 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,14 +8,19 @@ # These are binary so should never be modified by git. *.a binary +*.FLM binary +*.ico binary *.png binary *.jpg binary *.dxf binary *.mpy binary +*.der binary +*.bin binary # These should also not be modified by git. tests/basics/string_cr_conversion.py -text tests/basics/string_crlf_conversion.py -text +tests/micropython/test_normalize_newlines.py.exp -text ports/stm32/pybcdc.inf_template -text ports/stm32/usbhost/** -text ports/cc3200/hal/aes.c -text diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 7bad9562964..00000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Bug report -about: Report an issue -title: '' -labels: bug -assignees: '' - ---- - -* Please search existing issues before raising a new issue. For questions about MicroPython or for help using MicroPython, or any sort of "how do I?" requests, please use the Discussions tab or raise a documentation request instead. - -* In your issue, please include a clear and concise description of what the bug is, the expected output, and how to replicate it. - -* If this issue involves external hardware, please include links to relevant datasheets and schematics. - -* If you are seeing code being executed incorrectly, please provide a minimal example and expected output (e.g. comparison to CPython). - -* For build issues, please include full details of your environment, compiler versions, command lines, and build output. - -* Please provide as much information as possible about the version of MicroPython you're running, such as: - - firmware file name - - git commit hash and port/board - - version information shown in the REPL (hit Ctrl-B to see the startup message) - -* Remove all placeholder text above before submitting. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000000..1ec6c7067f9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,109 @@ +name: Bug report +description: Report a bug or unexpected behaviour +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Please provide as much detail as you can, it really helps us find and fix bugs faster. + + #### Not a bug report? + + * If you have a question \"How Do I ...?\", please post it on [GitHub Discussions](https://github.com/orgs/micropython/discussions/) or [Discord](https://discord.gg/RB8HZSAExQ) instead of here. + * For missing or incorrect documentation, or feature requests, then please [choose a different issue type](https://github.com/micropython/micropython/issues/new/choose). + + #### Existing issue? + + * Please search for [existing issues](https://github.com/micropython/micropython/issues) matching this bug before reporting. + - type: input + id: port-board-hw + attributes: + label: Port, board and/or hardware + description: | + Which MicroPython port(s) and board(s) are you using? + placeholder: | + esp32 port, ESP32-Fantastic board. + validations: + required: true + - type: textarea + id: version + attributes: + label: MicroPython version + description: | + To find the version: + + 1. Open a serial REPL. + 2. Type Ctrl-B to see the startup message. + 3. Copy-paste that output here. + + If the issue is about building MicroPython, please provide output of `git describe --dirty` and as much information as possible about the build environment. + + If the version or configuration is modified from the official MicroPython releases or the master branch, please tell us the details of this as well. + placeholder: | + MicroPython v6.28.3 on 2029-01-23; PyBoard 9 with STM32F9 + validations: + required: true + - type: textarea + id: steps-reproduce + attributes: + label: Reproduction + description: | + What steps will reproduce the problem? Please include all details that could be relevant about the environment, configuration, etc. + + If there is Python code to reproduce this issue then please either: + a. Type it into a code block below ([code block guide](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks)), or + b. Post longer code to a [GitHub gist](https://gist.github.com/), or + c. Create a sample project on GitHub. + + For build issues, please provide the exact build commands that you ran. + placeholder: | + 1. Copy paste the code provided below into a new file + 2. Use `mpremote run` to execute it on the board. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behaviour + description: | + What did you expect MicroPython to do? If comparing output with CPython or a different MicroPython port/version then please provide that output here. + placeholder: | + Expected to print "Hello World". + + Here is the correct output, seen with previous MicroPython version v3.14.159: + + > [...] + - type: textarea + id: what-happened + attributes: + label: Observed behaviour + description: | + What actually happened? Where possible please paste exact output, or the complete build log, etc. Very long output can be linked in a [GitHub gist](https://gist.github.com/). + placeholder: | + This unexpected exception appears: + + > [...] + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional Information + description: | + Is there anything else that might help to resolve this issue? + value: No, I've provided everything above. + - type: dropdown + id: code-of-conduct + attributes: + label: Code of Conduct + description: | + Do you agree to follow the MicroPython [Code of Conduct](https://github.com/micropython/micropython/blob/master/CODEOFCONDUCT.md) to ensure a safe and respectful space for everyone? + options: + - "Yes, I agree" + multiple: true + validations: + required: true + - type: markdown + attributes: + value: | + Thanks for taking the time to help improve MicroPython. diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index e36fa62ac29..00000000000 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Documentation issue -about: Report areas of the documentation or examples that need improvement -title: 'docs: ' -labels: documentation -assignees: '' - ---- - -* Please search existing issues before raising a new issue. For questions about MicroPython or for help using MicroPython, or any sort of "how do I?" requests, please use the Discussions tab instead. - -* Describe what was missing from the documentation and/or what was incorrect/incomplete. - -* If possible, please link to the relevant page on https://docs.micropython.org/ - -* Remove all placeholder text above before submitting. diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 00000000000..93051e51c80 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,46 @@ +name: Documentation issue +description: Report areas of the documentation or examples that need improvement +title: "docs: " +labels: ["documentation"] +body: + - type: markdown + attributes: + value: | + This form is for reporting issues with the documentation or examples provided with MicroPython. + + If you have a general question \"How Do I ...?\", please post it on [GitHub Discussions](https://github.com/orgs/micropython/discussions/) or [Discord](https://discord.gg/RB8HZSAExQ) instead of here. + + #### Existing issue? + + * Please search for [existing issues](https://github.com/micropython/micropython/issues) before reporting a new one. + - type: input + id: page + attributes: + label: Documentation URL + description: | + Does this issue relate to a particular page in the [online documentation](https://docs.micropython.org/en/latest/)? If yes, please paste the URL of the page: + placeholder: | + https://docs.micropython.org/en/latest/ + - type: textarea + id: version + attributes: + label: Description + description: | + Please describe what was missing from the documentation and/or what was incorrect/incomplete. + validations: + required: true + - type: dropdown + id: code-of-conduct + attributes: + label: Code of Conduct + description: | + Do you agree to follow the MicroPython [Code of Conduct](https://github.com/micropython/micropython/blob/master/CODEOFCONDUCT.md) to ensure a safe and respectful space for everyone? + options: + - "Yes, I agree" + multiple: true + validations: + required: true + - type: markdown + attributes: + value: | + Thanks for taking the time to help improve MicroPython. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 81b55d98e0d..00000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: Feature request -about: Request a feature or improvement -title: '' -labels: enhancement -assignees: '' - ---- - -* Please search existing issues before raising a new issue. For questions about MicroPython or for help using MicroPython, or any sort of "how do I?" requests, please use the Discussions tab or raise a documentation request instead. - -* Describe the feature you'd like to see added to MicroPython. In particular, what does this feature enable and why is it useful. MicroPython aims to strike a balance between functionality and code size, so please consider whether this feature can be optionally enabled and whether it can be provided in other ways (e.g. pure-Python library). - -* For core Python features, where possible please include a link to the relevant PEP. - -* For new architectures / ports / boards, please provide links to relevant documentation, specifications, and toolchains. Any information about the popularity and unique features about this hardware would also be useful. - -* For features for existing ports (e.g. new peripherals or microcontroller features), please describe which port(s) it applies too, and whether this is could be an extension to the machine API or a port-specific module? - -* For drivers (e.g. for external hardware), please link to datasheets and/or existing drivers from other sources. - -* Who do you expect will implement the feature you are requesting? Would you be willing to sponsor this work? - -* Remove all placeholder text above before submitting. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000000..7d5162a32a4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,74 @@ +name: Feature request +description: Request a feature or improvement +labels: ['enhancement'] +body: + - type: markdown + attributes: + value: | + This form is for requesting features or improvements in MicroPython. + + #### Get feedback first + + Before submitting a new feature idea here, suggest starting a discussion on [Discord](https://discord.gg/RB8HZSAExQ) or [GitHub Discussions](https://github.com/orgs/micropython/discussions/) to get early feedback from the community and maintainers. + + #### Not a MicroPython core feature? + + * If you have a question \"How Do I ...?\", please post it on GitHub Discussions or Discord instead of here. + * Could this feature be implemented as a pure Python library? If so, please open the request on the [micropython-lib repository](https://github.com/micropython/micropython-lib/issues) instead. + + #### Existing issue? + + * Please search for [existing issues](https://github.com/micropython/micropython/issues) before opening a new one. + - type: textarea + id: feature + attributes: + label: Description + description: | + Describe the feature you'd like to see added to MicroPython. What does this feature enable and why is it useful? + + * For core Python features, where possible please include a link to the relevant PEP or CPython documentation. + * For new architectures / ports / boards, please provide links to relevant documentation, specifications, and toolchains. Any information about the popularity and unique features about this hardware would also be useful. + * For features for existing ports (e.g. new peripherals or microcontroller features), please describe which port(s) it applies to, and whether this is could be an extension to the machine API or a port-specific module? + * For drivers (e.g. for external hardware), please link to datasheets and/or existing drivers from other sources. + + If there is an existing discussion somewhere about this feature, please add a link to it as well. + validations: + required: true + - type: textarea + id: size + attributes: + label: Code Size + description: | + MicroPython aims to strike a balance between functionality and code size. Can this feature be optionally enabled? + + If you believe the usefulness of this feature would outweigh the additional code size, please explain. (It's OK to say you're unsure here, we're happy to discuss this with you.) + - type: dropdown + id: implementation + attributes: + label: Implementation + description: | + What is your suggestion for implementing this feature? + + (See also: [How to sponsor](https://github.com/sponsors/micropython#sponsors), [How to submit a Pull Request](https://github.com/micropython/micropython/wiki/ContributorGuidelines).) + options: + - I hope the MicroPython maintainers or community will implement this feature + - I intend to implement this feature and would submit a Pull Request if desirable + - I would like to sponsor development of this feature + multiple: true + validations: + required: true + - type: dropdown + id: code-of-conduct + attributes: + label: Code of Conduct + description: | + Do you agree to follow the MicroPython [Code of Conduct](https://github.com/micropython/micropython/blob/master/CODEOFCONDUCT.md) to ensure a safe and respectful space for everyone? + options: + - "Yes, I agree" + multiple: true + validations: + required: true + - type: markdown + attributes: + value: | + Thanks for taking the time to suggest improvements for MicroPython. diff --git a/.github/ISSUE_TEMPLATE/security.md b/.github/ISSUE_TEMPLATE/security.md deleted file mode 100644 index cfe4a4befdb..00000000000 --- a/.github/ISSUE_TEMPLATE/security.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Security report -about: Report a security issue or vulnerability in MicroPython -title: '' -labels: security -assignees: '' - ---- - -* If you need to raise this issue privately with the MicroPython team, please email contact@micropython.org instead. - -* Include a clear and concise description of what the security issue is. - -* What does this issue allow an attacker to do? - -* Remove all placeholder text above before submitting. diff --git a/.github/ISSUE_TEMPLATE/security.yml b/.github/ISSUE_TEMPLATE/security.yml new file mode 100644 index 00000000000..57c2a5885ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security.yml @@ -0,0 +1,60 @@ +name: Security report +description: Report a security issue or vulnerability in MicroPython +labels: ["security"] +body: + - type: markdown + attributes: + value: | + This form is for reporting security issues in MicroPython that are not readily exploitable. + + 1. For issues that are readily exploitable or have high impact, please email contact@micropython.org instead. + 1. If this is a question about security, please ask it in [Discussions](https://github.com/orgs/micropython/discussions/) or [Discord](https://discord.gg/RB8HZSAExQ) instead. + + #### Existing issue? + + * Please search for [existing issues](https://github.com/micropython/micropython/issues) before reporting a new one. + + - type: input + id: port-board-hw + attributes: + label: Port, board and/or hardware + description: | + Which MicroPython port(s) and board(s) are you using? + placeholder: | + esp32 port, ESP32-Duper board. + - type: textarea + id: version + attributes: + label: MicroPython version + description: | + To find the version: + + 1. Open a serial REPL. + 2. Type Ctrl-B to see the startup message. + 3. Copy-paste that output here. + + If the version or configuration is modified from the official MicroPython releases or the master branch, please tell us the details of this as well. + placeholder: | + MicroPython v6.28.3 on 2029-01-23; PyBoard 9 with STM32F9 + - type: textarea + id: report + attributes: + label: Issue Report + description: | + Please provide a clear and concise description of the security issue. + + * What does this issue allow an attacker to do? + * How does the attacker exploit this issue? + validations: + required: true + - type: dropdown + id: code-of-conduct + attributes: + label: Code of Conduct + description: | + Do you agree to follow the MicroPython [Code of Conduct](https://github.com/micropython/micropython/blob/master/CODEOFCONDUCT.md) to ensure a safe and respectful space for everyone? + options: + - "Yes, I agree" + multiple: true + validations: + required: true diff --git a/.github/actions/setup_esp32/action.yml b/.github/actions/setup_esp32/action.yml new file mode 100644 index 00000000000..3b5da9eca87 --- /dev/null +++ b/.github/actions/setup_esp32/action.yml @@ -0,0 +1,47 @@ +name: Setup ESP-IDF for CI +description: Install ESP-IDF +inputs: + idf_ver: + required: true + type: string + ccache_key: + required: true + type: string + +runs: + using: "composite" + + steps: + - id: python_ver + name: Read the Python version + run: echo PYTHON_VER=py$(python --version | cut -d' ' -f2) | tee "${GITHUB_OUTPUT}" + shell: bash + + - name: Cached ESP-IDF install + id: cache_esp_idf + uses: actions/cache@v6 + with: + path: | + ./esp-idf/ + ~/.espressif/ + !~/.espressif/dist/ + ~/.cache/pip/ + # Cache is keyed on both IDF version (from the job) and Python version (from the runner) + key: esp-idf-${{ inputs.idf_ver }}-${{ steps.python_ver.outputs.PYTHON_VER }} + + - name: Install ESP-IDF packages + if: steps.cache_esp_idf.outputs.cache-hit != 'true' + env: + IDF_VER: ${{ inputs.idf_ver }} + run: tools/ci.sh esp32_idf_setup + shell: bash + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: esp32-${{ inputs.idf_ver }}-${{ inputs.ccache_key }} + + - name: Enable CCache for ESP-IDF + run: echo "IDF_CCACHE_ENABLE=1" >> ${GITHUB_ENV} + shell: bash + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000000..8be63024039 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,48 @@ + + +### Summary + + + +### Testing + + + +### Trade-offs and Alternatives + + + + +### Generative AI + + + +I did not use generative AI tools when creating this PR. + +I used generative AI tools when creating this PR, but a human has checked the +code and is responsible for the code and the description above. + + diff --git a/.github/workflows/biome.yml b/.github/workflows/biome.yml new file mode 100644 index 00000000000..eba4e9f3908 --- /dev/null +++ b/.github/workflows/biome.yml @@ -0,0 +1,16 @@ +name: JavaScript code lint and formatting with Biome + +on: [push, pull_request] + +jobs: + eslint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Setup Biome + uses: biomejs/setup-biome@v2 + with: + version: 1.5.3 + - name: Run Biome + run: biome ci --indent-style=space --indent-width=4 tests/ ports/webassembly diff --git a/.github/workflows/code_formatting.yml b/.github/workflows/code_formatting.yml index 81a2715f1b3..5616fe33d7c 100644 --- a/.github/workflows/code_formatting.yml +++ b/.github/workflows/code_formatting.yml @@ -10,21 +10,11 @@ jobs: code-formatting: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 - name: Install packages - run: source tools/ci.sh && ci_code_formatting_setup + run: tools/ci.sh c_code_formatting_setup - name: Run code formatting - run: source tools/ci.sh && ci_code_formatting_run + run: tools/ci.sh c_code_formatting_run - name: Check code formatting run: git diff --exit-code - - code-spelling: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - - name: Install packages - run: source tools/ci.sh && ci_code_spell_setup - - name: Run spell checker - run: source tools/ci.sh && ci_code_spell_run diff --git a/.github/workflows/code_size.yml b/.github/workflows/code_size.yml index 5d955703b66..dc3a47ded26 100644 --- a/.github/workflows/code_size.yml +++ b/.github/workflows/code_size.yml @@ -1,16 +1,22 @@ name: Check code size on: - push: pull_request: paths: - '.github/workflows/*.yml' - 'tools/**' - 'py/**' - 'extmod/**' + - 'shared/**' - 'lib/**' - 'ports/bare-arm/**' + - 'ports/esp32/**' + - 'ports/mimxrt/**' - 'ports/minimal/**' + - 'ports/rp2/**' + - 'ports/samd/**' + - 'ports/stm32/**' + - 'ports/unix/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -18,17 +24,36 @@ concurrency: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 with: fetch-depth: 100 - name: Install packages - run: source tools/ci.sh && ci_code_size_setup + run: tools/ci.sh code_size_setup + + - name: Find IDF_NEWEST_VER + id: idf_ver + run: | + echo "IDF_VER="$(yq .env.IDF_NEWEST_VER < .github/workflows/ports_esp32.yml) \ + | tee "${GITHUB_OUTPUT}" + + # The reference and current commit may be configured for different ESP-IDF versions + # but we use the same ESP-IDF version to build both, so disable the + # MICROPY_MAINTAINER_BUILD check (it'll be verified on the normal esp32 CI). + - name: Disable extra checks for older ESP-IDF + run: echo "MICROPY_MAINTAINER_BUILD=0" >> ${GITHUB_ENV} + + - name: Setup ESP-IDF + uses: ./.github/actions/setup_esp32 + with: + idf_ver: ${{ steps.idf_ver.outputs.IDF_VER }} + ccache_key: code_size + - name: Build - run: source tools/ci.sh && ci_code_size_build + run: tools/ci.sh code_size_build - name: Compute code size difference - run: tools/metrics.py diff ~/size0 ~/size1 | tee diff + run: source tools/ci.sh && ci_code_size_report - name: Save PR number if: github.event_name == 'pull_request' env: @@ -36,7 +61,7 @@ jobs: run: echo $PR_NUMBER > pr_number - name: Upload diff if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v7 with: name: code-size-report path: | diff --git a/.github/workflows/code_size_comment.yml b/.github/workflows/code_size_comment.yml index 8baf76a47a7..ea9d45ddb87 100644 --- a/.github/workflows/code_size_comment.yml +++ b/.github/workflows/code_size_comment.yml @@ -11,11 +11,11 @@ concurrency: jobs: comment: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: 'Download artifact' id: download-artifact - uses: actions/github-script@v6 + uses: actions/github-script@v9 with: result-encoding: string script: | @@ -56,7 +56,7 @@ jobs: run: unzip code-size-report.zip - name: Post comment to pull request if: steps.download-artifact.outputs.result == 'ok' - uses: actions/github-script@v6 + uses: actions/github-script@v9 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 00000000000..6155a348b5d --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,18 @@ +name: Check spelling with codespell + +on: [push, pull_request] + +jobs: + codespell: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + # codespell version should be kept in sync with .pre-commit-config.yml + - run: pip install --user codespell==2.4.1 tomli + - run: codespell + # additionally check for misspelling of "MicroPython" + - run: | + if git grep -n Micropython -- ":(exclude).github/workflows/codespell.yml"; then + echo "Please correct capitalisation of MicroPython on the above lines" + exit 1 + fi diff --git a/.github/workflows/commit_formatting.yml b/.github/workflows/commit_formatting.yml index 0b27038f2d2..0e38ebcf868 100644 --- a/.github/workflows/commit_formatting.yml +++ b/.github/workflows/commit_formatting.yml @@ -1,6 +1,6 @@ name: Check commit message formatting -on: [push, pull_request] +on: [pull_request] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -10,9 +10,9 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 with: - fetch-depth: '100' - - uses: actions/setup-python@v4 + fetch-depth: 100 + - uses: actions/setup-python@v7 - name: Check commit message formatting - run: source tools/ci.sh && ci_commit_formatting_run + run: tools/ci.sh commit_formatting_run diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e9b17007476..a27196734ad 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,9 +1,12 @@ name: Build docs on: + push: pull_request: paths: - docs/** + - py/** + - tests/cpydiff/** concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -14,9 +17,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 - name: Install Python packages - run: pip install Sphinx + run: pip install -r docs/requirements.txt + - name: Build unix port + run: tools/ci.sh unix_build_helper - name: Build docs run: make -C docs/ html diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 450805a6bde..bbfc93b1f64 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -18,8 +18,6 @@ jobs: embedding: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Build - run: make -C examples/embedding -f micropython_embed.mk && make -C examples/embedding - - name: Run - run: ./examples/embedding/embed | grep "hello world" + run: tools/ci.sh embedding_build diff --git a/.github/workflows/mpremote.yml b/.github/workflows/mpremote.yml index 14aef03e077..65ae823a14b 100644 --- a/.github/workflows/mpremote.yml +++ b/.github/workflows/mpremote.yml @@ -11,18 +11,28 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 with: # Setting this to zero means fetch all history and tags, # which hatch-vcs can use to discover the version tag. fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v7 - name: Install build tools run: pip install build - name: Build mpremote wheel - run: cd tools/mpremote && python -m build --wheel + run: | + if ! git rev-parse --verify -q v1.23.0 >/dev/null; then + echo "::error::mpremote wheel build requires recent MicroPython version tags in the forked repo." + echo "" + echo "To fix, push tags from upstream:" + echo " git remote add upstream https://github.com/micropython/micropython.git" + echo " git fetch upstream --tags" + echo " git push origin --tags" + exit 1 + fi + cd tools/mpremote && python -m build --wheel - name: Archive mpremote wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v7 with: name: mpremote path: | diff --git a/.github/workflows/mpy_format.yml b/.github/workflows/mpy_format.yml index 66abb19b81a..965bcedc73e 100644 --- a/.github/workflows/mpy_format.yml +++ b/.github/workflows/mpy_format.yml @@ -6,6 +6,8 @@ on: paths: - '.github/workflows/*.yml' - 'examples/**' + - 'mpy-cross/**' + - 'py/**' - 'tests/**' - 'tools/**' @@ -15,10 +17,12 @@ concurrency: jobs: test: - runs-on: ubuntu-20.04 # use 20.04 to get python2 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Install packages - run: source tools/ci.sh && ci_mpy_format_setup + run: tools/ci.sh mpy_format_setup - name: Test mpy-tool.py - run: source tools/ci.sh && ci_mpy_format_test + run: tools/ci.sh mpy_format_test + - name: Test mpy-cross debug emitter + run: tools/ci.sh mpy_cross_debug_emitter diff --git a/.github/workflows/ports.yml b/.github/workflows/ports.yml index fb574ad9819..5da3f2be969 100644 --- a/.github/workflows/ports.yml +++ b/.github/workflows/ports.yml @@ -17,6 +17,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Build ports download metadata run: mkdir boards && ./tools/autobuild/build-downloads.py . ./boards diff --git a/.github/workflows/ports_alif.yml b/.github/workflows/ports_alif.yml new file mode 100644 index 00000000000..e7d1064c97a --- /dev/null +++ b/.github/workflows/ports_alif.yml @@ -0,0 +1,33 @@ +name: alif port + +on: + push: + pull_request: + paths: + - '.github/workflows/*.yml' + - 'tools/**' + - 'py/**' + - 'extmod/**' + - 'shared/**' + - 'lib/**' + - 'drivers/**' + - 'ports/alif/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build_alif: + strategy: + fail-fast: false + matrix: + ci_func: # names are functions in ci.sh + - alif_ae3_build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Install packages + run: tools/ci.sh alif_setup + - name: Build ci_${{matrix.ci_func }} + run: tools/ci.sh ${{ matrix.ci_func }} diff --git a/.github/workflows/ports_cc3200.yml b/.github/workflows/ports_cc3200.yml index b58bc24b58b..5f920efda78 100644 --- a/.github/workflows/ports_cc3200.yml +++ b/.github/workflows/ports_cc3200.yml @@ -21,8 +21,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Install packages - run: source tools/ci.sh && ci_cc3200_setup + run: tools/ci.sh cc3200_setup - name: Build - run: source tools/ci.sh && ci_cc3200_build + run: tools/ci.sh cc3200_build diff --git a/.github/workflows/ports_esp32.yml b/.github/workflows/ports_esp32.yml index 6fc009d4fee..e1b57a24b7b 100644 --- a/.github/workflows/ports_esp32.yml +++ b/.github/workflows/ports_esp32.yml @@ -17,21 +17,47 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + # Oldest and newest supported ESP-IDF versions, should match ports/esp32/README.md + IDF_OLDEST_VER: &oldest "v5.3" + IDF_NEWEST_VER: &newest "v5.5.5" + jobs: - build_idf402: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - - name: Install packages - run: source tools/ci.sh && ci_esp32_idf402_setup - - name: Build - run: source tools/ci.sh && ci_esp32_build - - build_idf44: - runs-on: ubuntu-20.04 + build_idf: + strategy: + fail-fast: false + matrix: + idf_ver: + - *oldest + - *newest + ci_func: # names are functions in ci.sh + - esp32_build_cmod_spiram_d2wd + - esp32_build_s2_s3_c3 + - esp32_build_c2_c5_c6 + - esp32_build_h2_p4 + exclude: + # Exclude some jobs on the oldest IDF version, to save resources + - idf_ver: *oldest + ci_func: esp32_build_c2_c5_c6 + - idf_ver: *oldest + ci_func: esp32_build_h2_p4 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Install packages - run: source tools/ci.sh && ci_esp32_idf44_setup - - name: Build - run: source tools/ci.sh && ci_esp32_build + - uses: actions/checkout@v7 + + # Only the newest IDF version will build the ESP-IDF lockfiles correctly, + # so we need to disable MICROPY_MAINTAINER_BUILD on older versions. + - name: Disable extra checks for older ESP-IDF + id: check_newest_ver + if: ${{ matrix.idf_ver != env.IDF_NEWEST_VER }} + run: echo "MICROPY_MAINTAINER_BUILD=0" >> ${GITHUB_ENV} + + - name: Setup ESP-IDF + uses: ./.github/actions/setup_esp32 + with: + idf_ver: ${{ matrix.idf_ver }} + ccache_key: ${{ matrix.ci_func }} + + + - name: Build ci_${{matrix.ci_func }} on ESP-IDF ${{ matrix.idf_ver }} + run: tools/ci.sh ${{ matrix.ci_func }} diff --git a/.github/workflows/ports_esp8266.yml b/.github/workflows/ports_esp8266.yml index ba89d5e9529..156f3181bbc 100644 --- a/.github/workflows/ports_esp8266.yml +++ b/.github/workflows/ports_esp8266.yml @@ -21,8 +21,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Install packages - run: source tools/ci.sh && ci_esp8266_setup && ci_esp8266_path >> $GITHUB_PATH + run: tools/ci.sh esp8266_setup && tools/ci.sh esp8266_path >> $GITHUB_PATH - name: Build - run: source tools/ci.sh && ci_esp8266_build + run: tools/ci.sh esp8266_build diff --git a/.github/workflows/ports_mimxrt.yml b/.github/workflows/ports_mimxrt.yml index d9156253418..4f171d11474 100644 --- a/.github/workflows/ports_mimxrt.yml +++ b/.github/workflows/ports_mimxrt.yml @@ -19,10 +19,15 @@ concurrency: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + defaults: + run: + working-directory: 'micropython repo' # test build with space in path steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 + with: + path: 'micropython repo' - name: Install packages - run: source tools/ci.sh && ci_mimxrt_setup + run: tools/ci.sh mimxrt_setup - name: Build - run: source tools/ci.sh && ci_mimxrt_build + run: tools/ci.sh mimxrt_build diff --git a/.github/workflows/ports_nrf.yml b/.github/workflows/ports_nrf.yml index 89211217800..53a5d6139d6 100644 --- a/.github/workflows/ports_nrf.yml +++ b/.github/workflows/ports_nrf.yml @@ -19,10 +19,10 @@ concurrency: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Install packages - run: source tools/ci.sh && ci_nrf_setup + run: tools/ci.sh nrf_setup - name: Build - run: source tools/ci.sh && ci_nrf_build + run: tools/ci.sh nrf_build diff --git a/.github/workflows/ports_powerpc.yml b/.github/workflows/ports_powerpc.yml deleted file mode 100644 index a15c4da97fa..00000000000 --- a/.github/workflows/ports_powerpc.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: powerpc port - -on: - push: - pull_request: - paths: - - '.github/workflows/*.yml' - - 'tools/**' - - 'py/**' - - 'extmod/**' - - 'shared/**' - - 'lib/**' - - 'drivers/**' - - 'ports/powerpc/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install packages - run: source tools/ci.sh && ci_powerpc_setup - - name: Build - run: source tools/ci.sh && ci_powerpc_build diff --git a/.github/workflows/ports_psoc-edge.yml b/.github/workflows/ports_psoc-edge.yml new file mode 100644 index 00000000000..f0f37e94939 --- /dev/null +++ b/.github/workflows/ports_psoc-edge.yml @@ -0,0 +1,28 @@ +name: psoc-edge port + +on: + push: + pull_request: + paths: + - '.github/workflows/*.yml' + - 'tools/**' + - 'py/**' + - 'extmod/**' + - 'shared/**' + - 'lib/**' + - 'drivers/**' + - 'ports/psoc-edge/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build_psoc_edge: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Install packages + run: tools/ci.sh psoc_edge_setup + - name: Build + run: tools/ci.sh psoc_edge_build diff --git a/.github/workflows/ports_qemu-arm.yml b/.github/workflows/ports_qemu-arm.yml deleted file mode 100644 index 93ec4da7670..00000000000 --- a/.github/workflows/ports_qemu-arm.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: qemu-arm port - -on: - push: - pull_request: - paths: - - '.github/workflows/*.yml' - - 'tools/**' - - 'py/**' - - 'extmod/**' - - 'shared/**' - - 'lib/**' - - 'drivers/**' - - 'ports/qemu-arm/**' - - 'tests/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build_and_test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install packages - run: source tools/ci.sh && ci_qemu_arm_setup - - name: Build and run test suite - run: source tools/ci.sh && ci_qemu_arm_build - - name: Print failures - if: failure() - run: grep --before-context=100 --text "FAIL" ports/qemu-arm/build/console.out diff --git a/.github/workflows/ports_qemu.yml b/.github/workflows/ports_qemu.yml new file mode 100644 index 00000000000..f064930d832 --- /dev/null +++ b/.github/workflows/ports_qemu.yml @@ -0,0 +1,76 @@ +name: qemu port + +on: + push: + pull_request: + paths: + - '.github/workflows/*.yml' + - 'tools/**' + - 'py/**' + - 'extmod/**' + - 'shared/**' + - 'lib/**' + - 'drivers/**' + - 'ports/qemu/**' + - 'tests/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build_and_test_arm: + strategy: + fail-fast: false + matrix: + ci_func: # names are functions in ci.sh + - bigendian + - sabrelite + - thumb_softfp + - thumb_hardfp + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Install packages + run: tools/ci.sh qemu_setup_arm + - name: Build and run test suite ci_qemu_build_arm_${{ matrix.ci_func }} + run: tools/ci.sh qemu_build_arm_${{ matrix.ci_func }} + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + build_and_test_rv32: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Install packages + run: tools/ci.sh qemu_setup_rv32 + - name: Build and run test suite + run: tools/ci.sh qemu_build_rv32 + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + build_and_test_rv64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Install packages + run: tools/ci.sh qemu_setup_rv64 + - name: Build and run test suite + run: tools/ci.sh qemu_build_rv64 + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + build_and_test_ppc64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Install packages + run: tools/ci.sh qemu_setup_ppc64 + - name: Build and run test suite + run: tools/ci.sh qemu_build_ppc64 + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures diff --git a/.github/workflows/ports_renesas-ra.yml b/.github/workflows/ports_renesas-ra.yml index 33e17a385a1..1e5bfb447e8 100644 --- a/.github/workflows/ports_renesas-ra.yml +++ b/.github/workflows/ports_renesas-ra.yml @@ -19,11 +19,11 @@ concurrency: jobs: build_renesas_ra_board: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Install packages - run: source tools/ci.sh && ci_renesas_ra_setup + run: tools/ci.sh renesas_ra_setup - name: Build - run: source tools/ci.sh && ci_renesas_ra_board_build + run: tools/ci.sh renesas_ra_board_build diff --git a/.github/workflows/ports_rp2.yml b/.github/workflows/ports_rp2.yml index f042ff1151a..27d6c60a5c5 100644 --- a/.github/workflows/ports_rp2.yml +++ b/.github/workflows/ports_rp2.yml @@ -20,9 +20,14 @@ concurrency: jobs: build: runs-on: ubuntu-latest + defaults: + run: + working-directory: 'micropython repo' # test build with space in path steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 + with: + path: 'micropython repo' - name: Install packages - run: source tools/ci.sh && ci_rp2_setup + run: tools/ci.sh rp2_setup - name: Build - run: source tools/ci.sh && ci_rp2_build + run: tools/ci.sh rp2_build diff --git a/.github/workflows/ports_samd.yml b/.github/workflows/ports_samd.yml index 9833a2fae2e..04cddeb76de 100644 --- a/.github/workflows/ports_samd.yml +++ b/.github/workflows/ports_samd.yml @@ -21,8 +21,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Install packages - run: source tools/ci.sh && ci_samd_setup + run: tools/ci.sh samd_setup - name: Build - run: source tools/ci.sh && ci_samd_build + run: tools/ci.sh samd_build diff --git a/.github/workflows/ports_stm32.yml b/.github/workflows/ports_stm32.yml index b278ea862ce..2099b8b7c03 100644 --- a/.github/workflows/ports_stm32.yml +++ b/.github/workflows/ports_stm32.yml @@ -18,20 +18,20 @@ concurrency: cancel-in-progress: true jobs: - build_pyb: - runs-on: ubuntu-20.04 + build_stm32: + strategy: + fail-fast: false + matrix: + ci_func: # names are functions in ci.sh + - stm32_pyb_build + - stm32_build_cmod + - stm32_nucleo_build + - stm32_misc_build + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Install packages - run: source tools/ci.sh && ci_stm32_setup - - name: Build - run: source tools/ci.sh && ci_stm32_pyb_build + run: tools/ci.sh stm32_setup && tools/ci.sh stm32_path >> $GITHUB_PATH + - name: Build ci_${{matrix.ci_func }} + run: tools/ci.sh ${{ matrix.ci_func }} - build_nucleo: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - - name: Install packages - run: source tools/ci.sh && ci_stm32_setup - - name: Build - run: source tools/ci.sh && ci_stm32_nucleo_build diff --git a/.github/workflows/ports_teensy.yml b/.github/workflows/ports_teensy.yml deleted file mode 100644 index f1299603259..00000000000 --- a/.github/workflows/ports_teensy.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: teensy port - -on: - push: - pull_request: - paths: - - '.github/workflows/*.yml' - - 'tools/**' - - 'py/**' - - 'extmod/**' - - 'shared/**' - - 'lib/**' - - 'drivers/**' - - 'ports/teensy/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install packages - run: source tools/ci.sh && ci_teensy_setup - - name: Build - run: source tools/ci.sh && ci_teensy_build diff --git a/.github/workflows/ports_unix.yml b/.github/workflows/ports_unix.yml index 87c58055b98..8231c1eb1ea 100644 --- a/.github/workflows/ports_unix.yml +++ b/.github/workflows/ports_unix.yml @@ -23,11 +23,11 @@ jobs: minimal: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Build - run: source tools/ci.sh && ci_unix_minimal_build + run: tools/ci.sh unix_minimal_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_minimal_run_tests + run: tools/ci.sh unix_minimal_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures @@ -35,9 +35,9 @@ jobs: reproducible: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Build with reproducible date - run: source tools/ci.sh && ci_unix_minimal_build + run: tools/ci.sh unix_minimal_build env: SOURCE_DATE_EPOCH: 1234567890 - name: Check reproducible build date @@ -46,11 +46,47 @@ jobs: standard: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Build - run: source tools/ci.sh && ci_unix_standard_build + run: tools/ci.sh unix_standard_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_standard_run_tests + run: tools/ci.sh unix_standard_run_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + standard_v2: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Build + run: tools/ci.sh unix_standard_v2_build + - name: Run main test suite + run: tools/ci.sh unix_standard_v2_run_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + standard_error_terse: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Build + run: tools/ci.sh unix_standard_error_terse_build + - name: Run main test suite + run: tools/ci.sh unix_standard_error_terse_run_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + standard_error_none: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Build + run: tools/ci.sh unix_standard_error_none_build + - name: Run main test suite + run: tools/ci.sh unix_standard_error_none_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures @@ -58,60 +94,115 @@ jobs: coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' - name: Install packages - run: source tools/ci.sh && ci_unix_coverage_setup + run: tools/ci.sh unix_coverage_setup - name: Build - run: source tools/ci.sh && ci_unix_coverage_build + run: tools/ci.sh unix_coverage_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_coverage_run_tests + run: tools/ci.sh unix_coverage_run_tests - name: Test merging .mpy files - run: source tools/ci.sh && ci_unix_coverage_run_mpy_merge_tests + run: tools/ci.sh unix_coverage_run_mpy_merge_tests - name: Build native mpy modules - run: source tools/ci.sh && ci_native_mpy_modules_build + run: tools/ci.sh native_mpy_modules_build - name: Test importing .mpy generated by mpy_ld.py - run: source tools/ci.sh && ci_unix_coverage_run_native_mpy_tests + run: tools/ci.sh unix_coverage_run_native_mpy_tests - name: Run gcov coverage analysis run: | (cd ports/unix && gcov -o build-coverage/py ../../py/*.c || true) (cd ports/unix && gcov -o build-coverage/extmod ../../extmod/*.c || true) - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v7 with: - fail_ci_if_error: true + # Only fail the job on error if a token is set, or we're running against upstream repo. + # This avoids the annoying situation of the job failing on every push to a fork (if no token is set). + fail_ci_if_error: ${{ secrets.CODECOV_TOKEN != '' || github.repository_owner == 'micropython' }} verbose: true + flags: unix-coverage-64bit + name: unix-coverage-64bit + # note: when a fork opens a PR into MicroPython repo, the pull_request trigger can't access + # secrets so this token value will be empty (codecov will do a 'tokenless' upload). + token: ${{ secrets.CODECOV_TOKEN }} - name: Print failures if: failure() run: tests/run-tests.py --print-failures coverage_32bit: - runs-on: ubuntu-20.04 # use 20.04 to get libffi-dev:i386 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' - name: Install packages - run: source tools/ci.sh && ci_unix_32bit_setup + run: tools/ci.sh unix_32bit_setup - name: Build - run: source tools/ci.sh && ci_unix_coverage_32bit_build + run: tools/ci.sh unix_coverage_32bit_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_coverage_32bit_run_tests + run: tools/ci.sh unix_coverage_32bit_run_tests - name: Build native mpy modules - run: source tools/ci.sh && ci_native_mpy_modules_32bit_build + run: tools/ci.sh native_mpy_modules_32bit_build - name: Test importing .mpy generated by mpy_ld.py - run: source tools/ci.sh && ci_unix_coverage_32bit_run_native_mpy_tests + run: tools/ci.sh unix_coverage_32bit_run_native_mpy_tests + - name: Run gcov coverage analysis + run: | + (cd ports/unix && gcov -o build-coverage/py ../../py/*.c || true) + (cd ports/unix && gcov -o build-coverage/extmod ../../extmod/*.c || true) + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v7 + with: + # See corresponding comment above. + fail_ci_if_error: ${{ secrets.CODECOV_TOKEN != '' || github.repository_owner == 'micropython' }} + verbose: true + flags: unix-coverage-32bit + name: unix-coverage-32bit + # See corresponding comment above. + token: ${{ secrets.CODECOV_TOKEN }} - name: Print failures if: failure() run: tests/run-tests.py --print-failures nanbox: - runs-on: ubuntu-20.04 # use 20.04 to get python2, and libffi-dev:i386 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' + - name: Install packages + run: tools/ci.sh unix_32bit_setup + - name: Build + run: tools/ci.sh unix_nanbox_build + - name: Run main test suite + run: tools/ci.sh unix_nanbox_run_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + longlong: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' - name: Install packages - run: source tools/ci.sh && ci_unix_32bit_setup + run: tools/ci.sh unix_32bit_setup - name: Build - run: source tools/ci.sh && ci_unix_nanbox_build + run: tools/ci.sh unix_longlong_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_nanbox_run_tests + run: tools/ci.sh unix_longlong_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures @@ -119,78 +210,106 @@ jobs: float: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Build - run: source tools/ci.sh && ci_unix_float_build + run: tools/ci.sh unix_float_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_float_run_tests + run: tools/ci.sh unix_float_run_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + gil_enabled: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Build + run: tools/ci.sh unix_gil_enabled_build + - name: Run main test suite + run: tools/ci.sh unix_gil_enabled_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures stackless_clang: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Install packages - run: source tools/ci.sh && ci_unix_clang_setup + run: tools/ci.sh unix_clang_setup - name: Build - run: source tools/ci.sh && ci_unix_stackless_clang_build + run: tools/ci.sh unix_stackless_clang_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_stackless_clang_run_tests + run: tools/ci.sh unix_stackless_clang_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures float_clang: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Install packages - run: source tools/ci.sh && ci_unix_clang_setup + run: tools/ci.sh unix_clang_setup - name: Build - run: source tools/ci.sh && ci_unix_float_clang_build + run: tools/ci.sh unix_float_clang_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_float_clang_run_tests + run: tools/ci.sh unix_float_clang_run_tests + - name: Build native mpy modules + run: tools/ci.sh native_mpy_modules_clang_build + - name: Test importing .mpy generated by mpy_ld.py + run: tools/ci.sh unix_standard_run_native_mpy_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures - settrace: + settrace_stackless: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' - name: Build - run: source tools/ci.sh && ci_unix_settrace_build + run: tools/ci.sh unix_settrace_stackless_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_settrace_run_tests + run: tools/ci.sh unix_settrace_stackless_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures - settrace_stackless: + repr_b: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' + - name: Install packages + run: tools/ci.sh unix_32bit_setup - name: Build - run: source tools/ci.sh && ci_unix_settrace_stackless_build + run: tools/ci.sh unix_repr_b_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_settrace_stackless_run_tests + run: tools/ci.sh unix_repr_b_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures macos: - runs-on: macos-11.0 + runs-on: macos-26 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 with: python-version: '3.8' - name: Build - run: source tools/ci.sh && ci_unix_macos_build + run: tools/ci.sh unix_macos_build - name: Run tests - run: source tools/ci.sh && ci_unix_macos_run_tests + run: tools/ci.sh unix_macos_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures @@ -198,13 +317,18 @@ jobs: qemu_mips: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' - name: Install packages - run: source tools/ci.sh && ci_unix_qemu_mips_setup + run: tools/ci.sh unix_qemu_mips_setup - name: Build - run: source tools/ci.sh && ci_unix_qemu_mips_build + run: tools/ci.sh unix_qemu_mips_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_qemu_mips_run_tests + run: tools/ci.sh unix_qemu_mips_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures @@ -212,13 +336,125 @@ jobs: qemu_arm: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' + - name: Install packages + run: tools/ci.sh unix_qemu_arm_setup + - name: Build + run: tools/ci.sh unix_qemu_arm_build + - name: Run main test suite + run: tools/ci.sh unix_qemu_arm_run_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + qemu_riscv64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' + - name: Install packages + run: tools/ci.sh unix_qemu_riscv64_setup + - name: Build + run: tools/ci.sh unix_qemu_riscv64_build + - name: Run main test suite + run: tools/ci.sh unix_qemu_riscv64_run_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + qemu_loong64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' + - name: Install packages + run: tools/ci.sh unix_qemu_loong64_setup + - name: Build + run: tools/ci.sh unix_qemu_loong64_build + - name: Run main test suite + run: tools/ci.sh unix_qemu_loong64_run_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + qemu_x64: + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' + - name: Install packages + run: tools/ci.sh unix_qemu_x64_setup + - name: Build + run: tools/ci.sh unix_qemu_x64_build + - name: Run main test suite + run: tools/ci.sh unix_qemu_x64_run_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + sanitize_address: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' + - name: Install packages + run: tools/ci.sh unix_coverage_setup + - name: Build + run: tools/ci.sh unix_sanitize_address_build + - name: Run main test suite + run: tools/ci.sh unix_sanitize_address_run_tests + - name: Test merging .mpy files + run: tools/ci.sh unix_coverage_run_mpy_merge_tests + - name: Build native mpy modules + run: tools/ci.sh native_mpy_modules_build + - name: Test importing .mpy generated by mpy_ld.py + run: tools/ci.sh unix_coverage_run_native_mpy_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + sanitize_undefined: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' - name: Install packages - run: source tools/ci.sh && ci_unix_qemu_arm_setup + run: tools/ci.sh unix_coverage_setup - name: Build - run: source tools/ci.sh && ci_unix_qemu_arm_build + run: tools/ci.sh unix_sanitize_undefined_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_qemu_arm_run_tests + run: tools/ci.sh unix_sanitize_undefined_run_tests + - name: Test merging .mpy files + run: tools/ci.sh unix_coverage_run_mpy_merge_tests + - name: Build native mpy modules + run: tools/ci.sh native_mpy_modules_build + - name: Test importing .mpy generated by mpy_ld.py + run: tools/ci.sh unix_coverage_run_native_mpy_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures diff --git a/.github/workflows/ports_webassembly.yml b/.github/workflows/ports_webassembly.yml index 2e0865662f0..ac2050129ef 100644 --- a/.github/workflows/ports_webassembly.yml +++ b/.github/workflows/ports_webassembly.yml @@ -11,6 +11,7 @@ on: - 'shared/**' - 'lib/**' - 'ports/webassembly/**' + - 'tests/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -20,13 +21,13 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Install packages - run: source tools/ci.sh && ci_webassembly_setup + run: tools/ci.sh webassembly_setup - name: Build - run: source tools/ci.sh && ci_webassembly_build + run: tools/ci.sh webassembly_build - name: Run tests - run: source tools/ci.sh && ci_webassembly_run_tests + run: tools/ci.sh webassembly_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures diff --git a/.github/workflows/ports_windows.yml b/.github/workflows/ports_windows.yml index b31718c5914..af830ca194e 100644 --- a/.github/workflows/ports_windows.yml +++ b/.github/workflows/ports_windows.yml @@ -18,11 +18,130 @@ concurrency: cancel-in-progress: true jobs: - build: + build-vs: + strategy: + fail-fast: false + matrix: + platform: [x86, x64] + configuration: [Debug, Release] + variant: [dev, standard] + visualstudio: ['2019', '2022'] + include: + - visualstudio: '2019' + # The v142 toolset (VS 2019 compiler) is pre-installed on + # windows-2022 as a component of VS 2022. Use VS 2022's + # MSBuild and select the v142 toolset via PlatformToolset. + vs_version: '[17, 18)' + platform_toolset: v142 + - visualstudio: '2022' + vs_version: '[17, 18)' + platform_toolset: v143 + # trim down the number of jobs in the matrix + exclude: + - variant: standard + configuration: Debug + - visualstudio: '2019' + configuration: Debug + runs-on: windows-2022 + env: + CI_BUILD_CONFIGURATION: ${{ matrix.configuration }} + steps: + - name: Install Python 3.11 + # As of 20260112 the default Python version in Windows image is 3.12, which breaks settrace tests + # Use 3.11 for now + uses: actions/setup-python@v7 + with: + python-version: '3.11' + - uses: microsoft/setup-msbuild@v3 + with: + vs-version: ${{ matrix.vs_version }} + - uses: actions/checkout@v7 + - name: Build mpy-cross.exe + run: msbuild mpy-cross\mpy-cross.vcxproj -maxcpucount -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} -property:PlatformToolset=${{ matrix.platform_toolset }} + - name: Update submodules + run: git submodule update --init lib/micropython-lib + - name: Build micropython.exe + run: msbuild ports\windows\micropython.vcxproj -maxcpucount -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} -property:PyVariant=${{ matrix.variant }} -property:PlatformToolset=${{ matrix.platform_toolset }} + - name: Get micropython.exe path + id: get_path + run: | + $exePath="$(msbuild ports\windows\micropython.vcxproj -nologo -v:m -t:ShowTargetPath -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} -property:PyVariant=${{ matrix.variant }} -property:PlatformToolset=${{ matrix.platform_toolset }})" + echo ("micropython=" + $exePath.Trim()) >> $env:GITHUB_OUTPUT + - name: Run tests + id: test + env: + MICROPY_MICROPYTHON: ${{ steps.get_path.outputs.micropython }} + working-directory: tests + run: python run-tests.py + - name: Print failures + if: failure() && steps.test.conclusion == 'failure' + working-directory: tests + run: python run-tests.py --print-failures + - name: Run mpy tests + id: test_mpy + env: + MICROPY_MICROPYTHON: ${{ steps.get_path.outputs.micropython }} + working-directory: tests + run: python run-tests.py --via-mpy -d basics float micropython + - name: Print mpy failures + if: failure() && steps.test_mpy.conclusion == 'failure' + working-directory: tests + run: python run-tests.py --print-failures + + build-mingw: + strategy: + fail-fast: false + matrix: + variant: [dev, standard] + sys: [mingw32, mingw64] + include: + - sys: mingw32 + env: i686 + - sys: mingw64 + env: x86_64 + runs-on: windows-latest + env: + CHERE_INVOKING: enabled_from_arguments + defaults: + run: + shell: msys2 {0} + steps: + - uses: actions/setup-python@v7 + # note: can go back to installing mingw-w64-${{ matrix.env }}-python after + # MSYS2 updates to Python >3.12 (due to settrace compatibility issue) + with: + python-version: '3.11' + - uses: msys2/setup-msys2@v2 + with: + msystem: ${{ matrix.sys }} + update: true + install: >- + make + mingw-w64-${{ matrix.env }}-gcc + pkg-config + git + diffutils + path-type: inherit # Remove when setup-python is removed + - uses: actions/checkout@v7 + - name: Build mpy-cross.exe + run: make -C mpy-cross -j2 + - name: Update submodules + run: make -C ports/windows VARIANT=${{ matrix.variant }} submodules + - name: Build micropython.exe + run: make -C ports/windows -j2 VARIANT=${{ matrix.variant }} + - name: Run tests + id: test + run: make -C ports/windows test_full VARIANT=${{ matrix.variant }} + - name: Print failures + if: failure() && steps.test.conclusion == 'failure' + working-directory: tests + run: python run-tests.py --print-failures + + cross-build-on-linux: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Install packages - run: source tools/ci.sh && ci_windows_setup + run: tools/ci.sh windows_setup - name: Build - run: source tools/ci.sh && ci_windows_build + run: tools/ci.sh windows_build diff --git a/.github/workflows/ports_zephyr.yml b/.github/workflows/ports_zephyr.yml index f64401b316a..8ed9845927f 100644 --- a/.github/workflows/ports_zephyr.yml +++ b/.github/workflows/ports_zephyr.yml @@ -11,6 +11,7 @@ on: - 'shared/**' - 'lib/**' - 'ports/zephyr/**' + - 'tests/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -20,10 +21,44 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: jlumbroso/free-disk-space@main + with: + # Only free up a few things so this step runs quickly. + # (android would save 9.6GiB, but takes about 13m) + # (large-packages would save 4.6GiB, but takes about 3m) + android: false + dotnet: true + haskell: true + large-packages: false + docker-images: false + tool-cache: true + swap-storage: false + - uses: actions/checkout@v7 + - id: versions + name: Read Zephyr version + run: source tools/ci.sh && echo "ZEPHYR=$ZEPHYR_VERSION" | tee "$GITHUB_OUTPUT" + - name: Cached Zephyr Workspace + id: cache_workspace + uses: actions/cache@v6 + with: + # note that the Zephyr CI docker image is 15GB. At time of writing + # GitHub caches are limited to 10GB total for a project. So we only + # cache the "workspace" + path: ./zephyrproject + key: zephyr-workspace-${{ steps.versions.outputs.ZEPHYR }} + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: zephyr - name: Install packages - run: source tools/ci.sh && ci_zephyr_setup + run: tools/ci.sh zephyr_setup - name: Install Zephyr - run: source tools/ci.sh && ci_zephyr_install + if: steps.cache_workspace.outputs.cache-hit != 'true' + run: tools/ci.sh zephyr_install - name: Build - run: source tools/ci.sh && ci_zephyr_build + run: tools/ci.sh zephyr_build + - name: Run main test suite + run: tools/ci.sh zephyr_run_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index b8e43dc78f1..a5a6fc7e26c 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -1,10 +1,13 @@ -# https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python -name: Python code lint with ruff +name: Python code lint and formatting with ruff + on: [push, pull_request] + jobs: ruff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - run: pip install --user ruff - - run: ruff --format=github . + - uses: actions/checkout@v7 + # ruff version should be kept in sync with .pre-commit-config.yaml & also micropython-lib + - run: pipx install ruff==0.11.6 + - run: ruff check --output-format=github . + - run: ruff format --diff . diff --git a/.gitignore b/.gitignore index 2d20cb18970..b17423ca050 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,11 @@ build/ build-*/ docs/genrst/ +.mpy_ld_cache-*/ -# Test failure outputs +# Test failure outputs and intermediate artefacts tests/results/* +tests/ports/unix/ffi_lib.so # Python cache files __pycache__/ @@ -23,3 +25,11 @@ user.props # MacOS desktop metadata files .DS_Store + +# Created by ci.sh zephyr targets +/.ccache +/zephyrproject + +# Created by ci.sh esp8266 targets +/xtensa-lx106-elf-standalone.tar.gz +/xtensa-lx106-elf/ diff --git a/.gitmodules b/.gitmodules index 992fec3d183..425bf7fd333 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,13 +3,13 @@ url = https://github.com/micropython/axtls.git [submodule "lib/libffi"] path = lib/libffi - url = https://github.com/atgreen/libffi + url = https://github.com/libffi/libffi [submodule "lib/lwip"] path = lib/lwip url = https://github.com/lwip-tcpip/lwip.git [submodule "lib/berkeley-db-1.xx"] path = lib/berkeley-db-1.xx - url = https://github.com/pfalcon/berkeley-db-1.xx + url = https://github.com/micropython/berkeley-db-1.xx [submodule "lib/stm32lib"] path = lib/stm32lib url = https://github.com/micropython/stm32lib @@ -26,7 +26,7 @@ branch = circuitpython [submodule "lib/tinyusb"] path = lib/tinyusb - url = https://github.com/hathach/tinyusb + url = https://github.com/micropython/tinyusb.git [submodule "lib/mynewt-nimble"] path = lib/mynewt-nimble url = https://github.com/micropython/mynewt-nimble.git @@ -35,7 +35,7 @@ url = https://github.com/bluekitchen/btstack.git [submodule "lib/nxp_driver"] path = lib/nxp_driver - url = https://github.com/hathach/nxp_driver.git + url = https://github.com/micropython/nxp_driver.git [submodule "lib/libhydrogen"] path = lib/libhydrogen url = https://github.com/jedisct1/libhydrogen.git @@ -56,3 +56,51 @@ [submodule "lib/micropython-lib"] path = lib/micropython-lib url = https://github.com/micropython/micropython-lib.git +[submodule "lib/protobuf-c"] + path = lib/protobuf-c + url = https://github.com/protobuf-c/protobuf-c.git +[submodule "lib/open-amp"] + path = lib/open-amp + url = https://github.com/OpenAMP/open-amp.git +[submodule "lib/libmetal"] + path = lib/libmetal + url = https://github.com/OpenAMP/libmetal.git +[submodule "lib/arduino-lib"] + path = lib/arduino-lib + url = https://github.com/arduino/arduino-lib-mpy.git +[submodule "lib/alif_ensemble-cmsis-dfp"] + path = lib/alif_ensemble-cmsis-dfp + url = https://github.com/alifsemi/alif_ensemble-cmsis-dfp.git +[submodule "lib/alif-security-toolkit"] + path = lib/alif-security-toolkit + url = https://github.com/micropython/alif-security-toolkit.git +[submodule "lib/CMSIS_5"] + path = lib/CMSIS_5 + url = https://github.com/ARM-software/CMSIS_5.git +[submodule "lib/CMSIS_6"] + path = lib/CMSIS_6 + url = https://github.com/ARM-software/CMSIS_6.git +[submodule "lib/psoc-edge/TARGET_KIT_PSE84_AI"] + path = lib/psoc-edge/TARGET_KIT_PSE84_AI + url = https://github.com/Infineon/TARGET_KIT_PSE84_AI.git +[submodule "lib/psoc-edge/core-lib"] + path = lib/psoc-edge/core-lib + url = https://github.com/Infineon/core-lib.git +[submodule "lib/psoc-edge/mtb-dsl-pse8xxgp"] + path = lib/psoc-edge/mtb-dsl-pse8xxgp + url = https://github.com/Infineon/mtb-dsl-pse8xxgp.git +[submodule "lib/psoc-edge/mtb-srf"] + path = lib/psoc-edge/mtb-srf + url = https://github.com/Infineon/mtb-srf.git +[submodule "lib/psoc-edge/se-rt-services-utils"] + path = lib/psoc-edge/se-rt-services-utils + url = https://github.com/Infineon/se-rt-services-utils.git +[submodule "lib/psoc-edge/serial-memory"] + path = lib/psoc-edge/serial-memory + url = https://github.com/Infineon/serial-memory.git +[submodule "lib/psoc-edge/mtb-ipc"] + path = lib/psoc-edge/mtb-ipc + url = https://github.com/Infineon/mtb-ipc.git +[submodule "lib/psoc-edge/async-transfer"] + path = lib/psoc-edge/async-transfer + url = https://github.com/Infineon/async-transfer.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9815a4b2e6..ad4136c59fc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,9 +2,11 @@ repos: - repo: local hooks: - id: codeformat - name: MicroPython codeformat.py for changed files - entry: tools/codeformat.py -v -f + name: MicroPython codeformat.py for changed C files + entry: tools/codeformat.py -v -c -f language: python + additional_dependencies: + - micropython-uncrustify==1.0.0.post1 - id: verifygitlog name: MicroPython git commit message format checker entry: tools/verifygitlog.py --check-file --ignore-rebase @@ -12,6 +14,16 @@ repos: verbose: true stages: [commit-msg] - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.265 + # Version should be kept in sync with .github/workflows/ruff.yml & also micropython-lib + rev: v0.11.6 hooks: - id: ruff + - id: ruff-format + - repo: https://github.com/codespell-project/codespell + # Version should be kept in sync with .github/workflows/codespell.yml + rev: v2.4.1 + hooks: + - id: codespell + name: Spellcheck for changed files (codespell) + additional_dependencies: + - tomli diff --git a/CODECONVENTIONS.md b/CODECONVENTIONS.md index d44b382b25e..0f40156b878 100644 --- a/CODECONVENTIONS.md +++ b/CODECONVENTIONS.md @@ -53,13 +53,16 @@ are then certifying and signing off against the following: Code auto-formatting ==================== -Both C and Python code are auto-formatted using the `tools/codeformat.py` -script. This uses [uncrustify](https://github.com/uncrustify/uncrustify) to -format C code and [black](https://github.com/psf/black) to format Python code. -After making changes, and before committing, run this tool to reformat your -changes to the correct style. Without arguments this tool will reformat all -source code (and may take some time to run). Otherwise pass as arguments to -the tool the files that changed and it will only reformat those. +Both C and Python code formatting are controlled for consistency across the +MicroPython codebase. C code is formatted using the `tools/codeformat.py` +script which uses [uncrustify](https://github.com/uncrustify/uncrustify). +Python code is linted and formatted using +[ruff & ruff format](https://github.com/astral-sh/ruff). +After making changes, and before committing, run `tools/codeformat.py` to +reformat your C code and `ruff format` for any Python code. Without +arguments this tool will reformat all source code (and may take some time +to run). Otherwise pass as arguments to the tool the files that changed, +and it will only reformat those. uncrustify ========== @@ -69,36 +72,44 @@ be used for MicroPython. Different uncrustify versions produce slightly different formatting, and the configuration file formats are often incompatible. v0.73 or newer *will not work*. -Depending on your operating system version, it may be possible to install a pre-compiled -uncrustify version: +Depending on your operating system version, it may be possible to install a +compatible pre-compiled uncrustify version. Otherwise, a compatible version is +available via the PyPI package archive: -Ubuntu, Debian --------------- - -Ubuntu versions 21.10 or 22.04LTS, and Debian versions bullseye or bookworm all -include v0.72 so can be installed directly: +Pip +--- ``` -$ apt install uncrustify +pip install micropython-uncrustify ``` -Arch Linux ----------- - -The current Arch uncrustify version is too new. There is an [old Arch package -for v0.72](https://archive.archlinux.org/packages/u/uncrustify/) that can be -installed from the Arch Linux archive ([more -information](https://wiki.archlinux.org/title/Downgrading_packages#Arch_Linux_Archive)). Use -the [IgnorePkg feature](https://wiki.archlinux.org/title/Pacman#Skip_package_from_being_upgraded) -to prevent it re-updating. +This installs a native compiled uncrustify binary as a Python executable, so it +can be installed into a virtualenv. -Brew +Pipx ---- -This command may work, please raise a new Issue if it doesn't: +It's also possible to install via [pipx](https://pipx.pypa.io/) if not using a +virtualenv: ``` -curl -L https://github.com/Homebrew/homebrew-core/raw/2b07d8192623365078a8b855a164ebcdf81494a6/Formula/uncrustify.rb > uncrustify.rb && brew install uncrustify.rb && rm uncrustify.rb +pipx install micropython-uncrustify +``` + +Code spell checking +=================== + +Code spell checking is done using [codespell](https://github.com/codespell-project/codespell#codespell) +and runs in a GitHub action in CI. Codespell is configured via `pyproject.toml` +to avoid false positives. It is recommended run codespell before submitting a +PR. To simplify this, codespell is configured as a pre-commit hook and will be +installed if you run `pre-commit install` (see below). + +If you want to install and run codespell manually, you can do so by running: + +``` +$ pip install codespell tomli +$ codespell ``` Automatic Pre-Commit Hooks @@ -108,6 +119,9 @@ To have code formatting and commit message conventions automatically checked, a configuration file is provided for the [pre-commit](https://pre-commit.com/) tool. +Pre-commit will automatically install the correct version of dependencies +such as codespell, uncrustify, etc. + First install `pre-commit`, either from your system package manager or via `pip`. When installing `pre-commit` via pip, it is recommended to use a virtual environment. Other sources, such as Brew are also available, see @@ -120,10 +134,6 @@ $ brew install pre-commit # Brew $ pip install pre-commit # PyPI ``` -Next, install [uncrustify (see above)](#uncrustify). Other dependencies are managed by -pre-commit automatically, but uncrustify needs to be installed and available on -the PATH. - Then, inside the MicroPython repository, register the git hooks for pre-commit by running: @@ -151,12 +161,22 @@ Tips: * To ignore the pre-commit message format check temporarily, start the commit message subject line with "WIP" (for "Work In Progress"). +Running pre-commit manually +=========================== + +Once pre-commit is installed as per the previous section it can be manually +run against the MicroPython python codebase to update file formatting on +demand, with either: +* `pre-commit run --all-files` to fix all files in the MicroPython codebase +* `pre-commit run --file ./path/to/my/file` to fix just one file +* `pre-commit run --file ./path/to/my/folder/*` to fix just one folder + Python code conventions ======================= Python code follows [PEP 8](https://legacy.python.org/dev/peps/pep-0008/) and -is auto-formatted using [black](https://github.com/psf/black) with a line-length -of 99 characters. +is auto-formatted using [ruff format](https://docs.astral.sh/ruff/formatter) +with a line-length of 99 characters. Naming conventions: - Module names are short and all lowercase; eg pyb, stm. @@ -177,14 +197,21 @@ adhere to the existing style and use `tools/codeformat.py` to check any changes. The main conventions, and things not enforceable via the auto-formatter, are described below. -White space: +As the MicroPython code base is over ten years old, not every source file +conforms fully to these conventions. If making small changes to existing code, +then it's usually acceptable to follow the existing code's style. New code or +major changes should follow the conventions described here. + +## White space + - Expand tabs to 4 spaces. - Don't leave trailing whitespace at the end of a line. - For control blocks (if, for, while), put 1 space between the keyword and the opening parenthesis. - Put 1 space after a comma, and 1 space around operators. -Braces: +## Braces + - Use braces for all blocks, even no-line and single-line pieces of code. - Put opening braces on the end of the line it belongs to, not on @@ -192,18 +219,43 @@ Braces: - For else-statements, put the else on the same line as the previous closing brace. -Header files: +## Header files + - Header files should be protected from multiple inclusion with #if directives. See an existing header for naming convention. -Names: +## Names + - Use underscore_case, not camelCase for all names. - Use CAPS_WITH_UNDERSCORE for enums and macros. - When defining a type use underscore_case and put '_t' after it. -Integer types: MicroPython runs on 16, 32, and 64 bit machines, so it's -important to use the correctly-sized (and signed) integer types. The -general guidelines are: +### Public names (declared in headers) + +- MicroPython-specific names (especially any declared in `py/` and `extmod/` + directories) should generally start with `mp_` or `MP_`. +- Functions and variables declared in a header should generally share a longer + common prefix. Usually the prefix matches the file name (i.e. items defined in + `py/obj.c` are declared in `py/obj.h` and should be prefixed `mp_obj_`). There + are exceptions, for example where one header file contains declarations + implemented in multiple source files for expediency. + +### Private names (specific to a single .c file) + +- For static functions and variables exposed to Python (i.e. a static C function + that is wrapped in `MP_DEFINE_CONST_FUN_...` and attached to a module), use + the file-level shared common prefix, i.e. name them as if the function or + variable was not static. +- Other static definitions in source files (i.e. functions or variables defined + in a .c file that are only used within that .c file) don't need any prefix + (specifically: no `s_` or `_` prefix, and generally avoid adding the + file-level common prefix). + +## Integer types + +MicroPython runs on 16, 32, and 64 bit machines, so it's important to use the +correctly-sized (and signed) integer types. The general guidelines are: + - For most cases use mp_int_t for signed and mp_uint_t for unsigned integer values. These are guaranteed to be machine-word sized and therefore big enough to hold the value from a MicroPython small-int @@ -212,11 +264,13 @@ general guidelines are: - You can use int/uint, but remember that they may be 16-bits wide. - If in doubt, use mp_int_t/mp_uint_t. -Comments: +## Comments + - Be concise and only write comments for things that are not obvious. - Use `// ` prefix, NOT `/* ... */`. No extra fluff. -Memory allocation: +## Memory allocation + - Use m_new, m_renew, m_del (and friends) to allocate and free heap memory. These macros are defined in py/misc.h. diff --git a/LICENSE b/LICENSE index 31eb9813f2d..b9078c7c71c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2023 Damien P. George +Copyright (c) 2013-2026 Damien P. George Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -37,6 +37,8 @@ used during the build process and is not part of the compiled source code. /drivers /cc3100 (BSD-3-clause) /lib + /CMSIS_5 (Apache-2.0) + /CMSIS_6 (Apache-2.0) /asf4 (Apache-2.0) /axtls (BSD-3-clause) /config @@ -45,19 +47,19 @@ used during the build process and is not part of the compiled source code. /Rules.mak (GPL-2.0) /berkeley-db-1xx (BSD-4-clause) /btstack (See btstack/LICENSE) - /cmsis (BSD-3-clause) /crypto-algorithms (NONE) /libhydrogen (ISC) + /libmetal (BSD-3-clause) /littlefs (BSD-3-clause) /lwip (BSD-3-clause) /mynewt-nimble (Apache-2.0) /nrfx (BSD-3-clause) /nxp_driver (BSD-3-Clause) /oofatfs (BSD-1-clause) + /open-amp (BSD-3-clause) /pico-sdk (BSD-3-clause) /re15 (BSD-3-clause) /stm32lib (BSD-3-clause) - /tinytest (BSD-3-clause) /tinyusb (MIT) /uzlib (Zlib) /wiznet5k (MIT) @@ -67,6 +69,11 @@ used during the build process and is not part of the compiled source code. /hal (BSD-3-clause) /simplelink (BSD-3-clause) /FreeRTOS (GPL-2.0 with FreeRTOS exception) + /esp32 + /ppp_set_auth.* (Apache-2.0) + /rp2 + /mutex_extra.c (BSD-3-clause) + /clocks_extra.c (BSD-3-clause) /stm32 /usbd*.c (MCD-ST Liberty SW License Agreement V2) /stm32_it.* (MIT + BSD-3-clause) @@ -76,8 +83,6 @@ used during the build process and is not part of the compiled source code. /*/stm32*.h (BSD-3-clause) /usbdev (MCD-ST Liberty SW License Agreement V2) /usbhost (MCD-ST Liberty SW License Agreement V2) - /teensy - /core (PJRC.COM) /zephyr /src (Apache-2.0) /tools diff --git a/README.md b/README.md index 6482899b251..51bd471e428 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![CI badge](https://github.com/micropython/micropython/workflows/unix%20port/badge.svg)](https://github.com/micropython/micropython/actions?query=branch%3Amaster+event%3Apush) [![codecov](https://codecov.io/gh/micropython/micropython/branch/master/graph/badge.svg?token=I92PfD05sD)](https://codecov.io/gh/micropython/micropython) +[![Unix CI badge](https://github.com/micropython/micropython/actions/workflows/ports_unix.yml/badge.svg)](https://github.com/micropython/micropython/actions?query=branch%3Amaster+event%3Apush) [![STM32 CI badge](https://github.com/micropython/micropython/actions/workflows/ports_stm32.yml/badge.svg)](https://github.com/micropython/micropython/actions?query=branch%3Amaster+event%3Apush) [![Docs CI badge](https://github.com/micropython/micropython/actions/workflows/docs.yml/badge.svg)](https://docs.micropython.org/) [![codecov](https://codecov.io/gh/micropython/micropython/branch/master/graph/badge.svg?token=I92PfD05sD)](https://codecov.io/gh/micropython/micropython) The MicroPython project ======================= @@ -6,20 +6,17 @@ The MicroPython project MicroPython Logo

-This is the MicroPython project, which aims to put an implementation -of Python 3.x on microcontrollers and small embedded systems. +This is the MicroPython project, an implementation of Python 3.x for +microcontrollers, embedded systems and other constrained platforms. You can find the official website at [micropython.org](http://www.micropython.org). -WARNING: this project is in beta stage and is subject to changes of the -code-base, including project-wide name changes and API changes. - MicroPython implements the entire Python 3.4 syntax (including exceptions, `with`, `yield from`, etc., and additionally `async`/`await` keywords from Python 3.5 and some select features from later versions). The following core datatypes are provided: `str`(including basic Unicode support), `bytes`, `bytearray`, `tuple`, `list`, `dict`, `set`, `frozenset`, `array.array`, `collections.namedtuple`, classes and instances. Builtin modules include -`os`, `sys`, `time`, `re`, and `struct`, etc. Select ports have support for +`os`, `sys`, `time`, `re`, and `struct`, etc. Some ports have support for `_thread` module (multithreading), `socket` and `ssl` for networking, and `asyncio`. Note that only a subset of Python 3 functionality is implemented for the data types and modules. @@ -35,8 +32,8 @@ DAC, PWM, SPI, I2C, CAN, Bluetooth, and USB. Getting started --------------- -See the [online documentation](https://docs.micropython.org/) for API -references and information about using MicroPython and information about how +See the [online documentation](https://docs.micropython.org/) for the API +reference and information about using MicroPython and information about how it is implemented. We use [GitHub Discussions](https://github.com/micropython/micropython/discussions) @@ -53,6 +50,44 @@ the officially supported board from the see the [schematics and pinouts](http://github.com/micropython/pyboard) and [documentation](https://docs.micropython.org/en/latest/pyboard/quickref.html). +MicroPython design values +------------------------- + +"Perfection is achieved, not when there is nothing more to add, but when there +is nothing left to take away." ―- Antoine de Saint-Exupéry. + +For its design and implementation, MicroPython aims to follow a set of values. +Although not a strict set of rules, these values and principles serve as a +useful guide for new and seasoned contributors, as well as maintainers. + +MicroPython is at heart a combination of "Micro" and "Python": it's about +resource constrained systems running the Python programming language. Both of +these concepts balance off against each other in all parts of MicroPython's +design and implementation. + +The key concepts that focus the development of MicroPython are: +- Minimalism: do lots with little. +- Efficiency: engineering, build, execution, storage, power consumption. +- Consistency: the whole system feels like it was designed at once. + +When using MicroPython, the Python language is used as the human interface to a +system, giving fine control over the entities attached to that system. +In a hardware setting, MicroPython aims to give the user a bare-metal feeling: +one should feel like they have complete control over the system, with very +little between the programmer and the physical world. + +MicroPython recognises that systems can be very complex. The existing Python +libraries in combination with the MicroPython-specific libraries provide a +user-friendly way to harness the complexity of a system. + +Python language compatibility is very important to MicroPython, and at first +glance MicroPython should look just like regular Python. In the first instance, +most Python scripts should run unchanged on MicroPython, even on devices with very +tight resources. Beyond that, there are ways to extend MicroPython if needed to +better match Python. The provided built-in modules are an efficient subset of +the corresponding Python ones, without duplication of functionality, and allow +extension in Python if needed. + Contributing ------------ @@ -80,9 +115,8 @@ This repository contains the following components: - [examples/](examples/) -- a few example Python scripts. "make" is used to build the components, or "gmake" on BSD-based systems. -You will also need bash, gcc, and Python 3.3+ available as the command `python3` -(if your system only has Python 2.7 then invoke make with the additional option -`PYTHON=python2`). Some ports (rp2 and esp32) additionally use CMake. +You will also need bash, gcc, and Python 3.3+ available as the command `python3`. +Some ports (rp2 and esp32) additionally use CMake. Supported platforms & architectures ----------------------------------- @@ -99,29 +133,74 @@ development and testing of MicroPython itself, as well as providing lightweight alternative to CPython on these platforms (in particular on embedded Linux systems). -The ["minimal"](ports/minimal) port provides an example of a very basic -MicroPython port and can be compiled as both a standalone Linux binary as -well as for ARM Cortex M4. Start with this if you want to port MicroPython to -another microcontroller. Additionally the ["bare-arm"](ports/bare-arm) port -is an example of the absolute minimum configuration, and is used to keep -track of the code size of the core runtime and VM. - -In addition, the following ports are provided in this repository: - - [cc3200](ports/cc3200) -- Texas Instruments CC3200 (including PyCom WiPy). - - [esp32](ports/esp32) -- Espressif ESP32 SoC (including ESP32S2, ESP32S3, ESP32C3). - - [esp8266](ports/esp8266) -- Espressif ESP8266 SoC. - - [mimxrt](ports/mimxrt) -- NXP m.iMX RT (including Teensy 4.x). - - [nrf](ports/nrf) -- Nordic Semiconductor nRF51 and nRF52. - - [pic16bit](ports/pic16bit) -- Microchip PIC 16-bit. - - [powerpc](ports/powerpc) -- IBM PowerPC (including Microwatt) - - [qemu-arm](ports/qemu-arm) -- QEMU-based emulated target, for testing) - - [renesas-ra](ports/renesas-ra) -- Renesas RA family. - - [rp2](ports/rp2) -- Raspberry Pi RP2040 (including Pico and Pico W). - - [samd](ports/samd) -- Microchip (formerly Atmel) SAMD21 and SAMD51. - - [stm32](ports/stm32) -- STMicroelectronics STM32 family (including F0, F4, F7, G0, G4, H7, L0, L4, WB) - - [teensy](ports/teensy) -- Teensy 3.x. - - [webassembly](ports/webassembly) -- Emscripten port targeting browsers and NodeJS. - - [zephyr](ports/zephyr) -- Zephyr RTOS. +Over twenty different MicroPython ports are provided in this repository, +split across three +[MicroPython Support Tiers](https://docs.micropython.org/en/latest/develop/support_tiers.html). + +Tier 1 Ports +============ + +👑 Ports in [Tier 1](https://docs.micropython.org/en/latest/develop/support_tiers.html) +are mature and have the most active development, support and testing: + +| Port | Target | Quick Reference | +|--------------------------|----------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| [esp32](ports/esp32)* | Espressif ESP32 SoCs (ESP32, ESP32S2, ESP32S3, ESP32C3, ESP32C6) | [here](https://docs.micropython.org/en/latest/esp32/quickref.html) | +| [mimxrt](ports/mimxrt) | NXP m.iMX RT | [here](https://docs.micropython.org/en/latest/mimxrt/quickref.html) | +| [rp2](ports/rp2) | Raspberry Pi RP2040 and RP2350 | [here](https://docs.micropython.org/en/latest/rp2/quickref.html) | +| [samd](ports/samd) | Microchip (formerly Atmel) SAMD21 and SAMD51 | [here](https://docs.micropython.org/en/latest/samd/quickref.html) | +| [stm32](ports/stm32) | STMicroelectronics STM32 MCUs (F0, F4, F7, G0, G4, H5, H7, L0, L1, L4, N6, WB, WL) | [here](https://docs.micropython.org/en/latest/pyboard/quickref.html) | +| [unix](ports/unix) | Linux, BSD, macOS, WSL | [here](https://docs.micropython.org/en/latest/unix/quickref.html) | +| [windows](ports/windows) | Microsoft Windows | [here](https://docs.micropython.org/en/latest/unix/quickref.html) | + +An asterisk indicates that the port has ongoing financial support from the vendor. + +Tier 2 Ports +============ + +✔ Ports in [Tier 2](https://docs.micropython.org/en/latest/develop/support_tiers.html) +are less mature and less actively developed and tested than Tier 1, but +still fully supported: + +| Port | Target | Quick Reference | +|----------------------------------|-------------------------------------------------------------|-------------------------------------------------------------------------| +| [alif](ports/alif) | Alif Semiconductor Ensemble MCUs (E3, E7) | | +| [embed](ports/embed) | Generates a set of .c/.h files for embedding into a project | | +| [nrf](ports/nrf) | Nordic Semiconductor nRF51 and nRF52 | | +| [psoc-edge](ports/psoc-edge) | Infineon PSOC™ Edge | [here](https://docs.micropython.org/en/latest/psoc-edge/quickref.html) | +| [renesas-ra](ports/renesas-ra) | Renesas RA family | [here](https://docs.micropython.org/en/latest/renesas-ra/quickref.html) | +| [webassembly](ports/webassembly) | Emscripten port targeting browsers and NodeJS | | +| [zephyr](ports/zephyr) | Zephyr RTOS | [here](https://docs.micropython.org/en/latest/zephyr/quickref.html) | + +Tier 3 Ports +============ + +Ports in [Tier 3](https://docs.micropython.org/en/latest/develop/support_tiers.html) +are built in CI but not regularly tested by the MicroPython maintainers: + +| Port | Target | Quick Reference | +|----------------------------|-------------------------------------------------------------------|-------------------------------------------------------------------------| +| [cc3200](ports/cc3200) | Texas Instruments CC3200 | [For WiPy](https://docs.micropython.org/en/latest/wipy/quickref.html) | +| [esp8266](ports/esp8266) | Espressif ESP8266 SoC | [here](https://docs.micropython.org/en/latest/esp8266/quickref.html) | +| [pic16bit](ports/pic16bit) | Microchip PIC 16-bit | | + +Additional Ports +================ + +In addition to the above there is a Tier M containing ports that are used +primarily for maintenance, development and testing: + +- The ["bare-arm"](ports/bare-arm) port is an example of the absolute minimum + configuration that still includes the compiler, and is used to keep track + of the code size of the core runtime and VM. + +- The ["minimal"](ports/minimal) port provides an example of a very basic + MicroPython port and can be compiled as both a standalone Linux binary as + well as for ARM Cortex-M4. Start with this if you want to port MicroPython + to another microcontroller. + +- The [qemu](ports/qemu) port is a QEMU-based emulated target for Cortex-A, + Cortex-M, RISC-V 32-bit, RISC-V 64-bit, and PowerPC 64-bit architectures. The MicroPython cross-compiler, mpy-cross ----------------------------------------- diff --git a/docs/README.md b/docs/README.md index 892726ba17f..9b3b036e063 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,8 +13,7 @@ Building the documentation locally If you're making changes to the documentation, you may want to build the documentation locally so that you can preview your changes. -Install Sphinx, and optionally (for the RTD-styling), sphinx_rtd_theme, -preferably in a virtualenv: +Install Sphinx and sphinx_rtd_theme, preferably in a virtualenv: pip install sphinx pip install sphinx_rtd_theme @@ -25,6 +24,21 @@ In `micropython/docs`, build the docs: You'll find the index page at `micropython/docs/build/html/index.html`. +Documentation autobuild +----------------------- + +For a more convenient development experience, you can use `sphinx-autobuild` +to automatically rebuild and serve the documentation when you make changes: + + pip install sphinx-autobuild + +Then run from the `micropython/docs` directory: + + sphinx-autobuild . build/html + +This will start a local web server (typically at `http://127.0.0.1:8000`) +and automatically rebuild the documentation whenever you save changes to the source files. + Having readthedocs.org build the documentation ---------------------------------------------- diff --git a/docs/conf.py b/docs/conf.py index a966b3a0257..f80ca97edca 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,55 +19,60 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath(".")) + +# The MICROPY_VERSION env var should be "vX.Y.Z" (or unset). +micropy_version = os.getenv("MICROPY_VERSION") or "latest" +micropy_all_versions = (os.getenv("MICROPY_ALL_VERSIONS") or "latest").split(",") +url_pattern = "%s/en/%%s" % (os.getenv("MICROPY_URL_PREFIX") or "/",) # The members of the html_context dict are available inside topindex.html -micropy_version = os.getenv('MICROPY_VERSION') or 'latest' -micropy_all_versions = (os.getenv('MICROPY_ALL_VERSIONS') or 'latest').split(',') -url_pattern = '%s/en/%%s' % (os.getenv('MICROPY_URL_PREFIX') or '/',) html_context = { - 'cur_version':micropy_version, - 'all_versions':[ - (ver, url_pattern % ver) for ver in micropy_all_versions - ], - 'downloads':[ - ('PDF', url_pattern % micropy_version + '/micropython-docs.pdf'), + "cur_version": micropy_version, + "all_versions": [(ver, url_pattern % ver) for ver in micropy_all_versions], + "downloads": [ + ("PDF", url_pattern % micropy_version + "/micropython-docs.pdf"), ], - 'is_release': micropy_version != 'latest', + "is_release": micropy_version != "latest", } +# Authors used in various parts of the documentation. +micropy_authors = "MicroPython authors and contributors" + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinxcontrib.jquery", + "sphinx_rtd_theme", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['templates'] +templates_path = ["templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'MicroPython' -copyright = '- The MicroPython Documentation is Copyright © 2014-2023, Damien P. George, Paul Sokolovsky, and contributors' +project = "MicroPython" +copyright = "- The MicroPython Documentation is Copyright © 2014-2026, " + micropy_authors # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -79,41 +84,41 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['build', '.venv'] +exclude_patterns = ["build", ".venv"] # The reST default role (used for this markup: `text`) to use for all # documents. -default_role = 'any' +default_role = "any" # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # Global include files. Sphinx docs suggest using rst_epilog in preference # of rst_prolog, so we follow. Absolute paths below mean "from the base @@ -124,145 +129,138 @@ # -- Options for HTML output ---------------------------------------------- -# on_rtd is whether we are on readthedocs.org -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +import sphinx_rtd_theme -if not on_rtd: # only import and set the theme if we're building docs locally - try: - import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' - html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), '.'] - except: - html_theme = 'default' - html_theme_path = ['.'] -else: - html_theme_path = ['.'] +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = ['.'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = '../../logo/trans-logo.png' +# html_logo = '../../logo/trans-logo.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -html_favicon = 'static/favicon.ico' +html_favicon = "static/favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['static'] +html_static_path = ["static"] # Add a custom CSS file for HTML generation html_css_files = [ - 'custom.css', + "custom.css", ] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -html_last_updated_fmt = '%d %b %Y' +html_last_updated_fmt = "%d %b %Y" # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. html_additional_pages = {"index": "topindex.html"} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'MicroPythondoc' +htmlhelp_basename = "MicroPythondoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -# Include 3 levels of headers in PDF ToC -'preamble': r'\setcounter{tocdepth}{2}', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Include 3 levels of headers in PDF ToC + "preamble": r"\setcounter{tocdepth}{2}", } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'MicroPython.tex', 'MicroPython Documentation', - 'Damien P. George, Paul Sokolovsky, and contributors', 'manual'), + ( + master_doc, + "MicroPython.tex", + "MicroPython Documentation", + micropy_authors, + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # Enable better Unicode support so that `make latexpdf` doesn't fail latex_engine = "xelatex" @@ -272,12 +270,17 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'micropython', 'MicroPython Documentation', - ['Damien P. George, Paul Sokolovsky, and contributors'], 1), + ( + "index", + "micropython", + "MicroPython Documentation", + [micropy_authors], + 1, + ), ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -286,23 +289,29 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'MicroPython', 'MicroPython Documentation', - 'Damien P. George, Paul Sokolovsky, and contributors', 'MicroPython', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "MicroPython", + "MicroPython Documentation", + micropy_authors, + "MicroPython", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'python': ('https://docs.python.org/3.5', None)} +intersphinx_mapping = {"python": ("https://docs.python.org/3.5", None)} diff --git a/docs/develop/cmodules.rst b/docs/develop/cmodules.rst index 75dbc953c06..e1c6658f075 100644 --- a/docs/develop/cmodules.rst +++ b/docs/develop/cmodules.rst @@ -8,8 +8,8 @@ limitations with the Python environment, often due to an inability to access certain hardware resources or Python speed limitations. If your limitations can't be resolved with suggestions in :ref:`speed_python`, -writing some or all of your module in C (and/or C++ if implemented for your port) -is a viable option. +writing some or all of your module in C (and/or +:ref:`C++ if implemented for your port`) is a viable option. If your module is designed to access or work with commonly available hardware or libraries please consider implementing it inside the MicroPython @@ -59,7 +59,7 @@ A MicroPython user C module is a directory with the following files: SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c Similarly, use ``SRC_USERMOD_CXX`` and ``SRC_USERMOD_LIB_CXX`` for C++ - source files. + source files. If you want to include assembly files use ``SRC_USERMOD_LIB_ASM``. If you have custom compiler options (like ``-I`` to add directories to search for header files), these should be added to ``CFLAGS_USERMOD`` for C code @@ -264,6 +264,32 @@ structures. If not done correctly it will compile but importing will fail to find the module. +Specifying C modules via a manifest +----------------------------------- + +As an alternative to passing ``USER_C_MODULES`` on the command line, C modules +can be listed inside a frozen manifest using ``c_module()``. This is convenient +when a board or project always pulls in the same set of C modules: the manifest +becomes the single place that declares both frozen Python code and the C +modules required to support it. + +.. code-block:: python3 + + # In ports/myboard/boards/MYBOARD/manifest.py + include("$(PORT_DIR)/boards/manifest.py") + c_module("$(MPY_DIR)/examples/usercmodule/cexample") + c_module("$(BOARD_DIR)/../../drivers/sensor") + +The manifest will need to be loaded via ``FROZEN_MANIFEST`` (either set in +``mpconfigboard.{mk,cmake}`` or passed on the make command line), and modules +listed with ``c_module()`` combine additively with any paths supplied via +``USER_C_MODULES`` on the command line. Duplicate paths are de-duplicated, so +mixing the two is safe. + +See :ref:`manifest` for the full ``c_module()`` API and supported +``$(VAR)`` path substitutions. + + Module usage in MicroPython --------------------------- @@ -285,3 +311,73 @@ can now be accessed in Python just like any other builtin module, e.g. sleep_ms(1000) print(watch.time()) # should display approximately 1000 + + +.. _c_heap: + +C Dynamic Memory Allocation +--------------------------- + +MicroPython uses its own "Python heap" for `memorymanagement`, +which is not the same as the "C heap" used by C library functions ``malloc()``, +``free()``, etc. Not every MicroPython port comes with a "C heap" at all. + +Tier 1 & 2 ports have varying support for C dynamic memory allocation via a "C +heap": + +- unix, windows, esp32 and webassembly ports support C dynamic memory + allocation. +- rp2 port will fail to allocate any memory at runtime unless the firmware is + built with ``MICROPY_C_HEAP_SIZE=n`` to reserve ``n`` bytes of memory for a C + heap. This memory will not be available for Python code to use. +- alif, mimxrt, nrf, renesas-ra, samd, and stm32 port builds that include + dynamic C allocation will fail at link-time with errors such as ``undefined + reference to `malloc'``. MicroPython has no built-in support for dynamic C + allocation on these ports. Any solution requires manually adding a C heap + implementation to the custom build. +- zephyr port currently does not support building with user modules. + +Python heap as C heap +~~~~~~~~~~~~~~~~~~~~~ + +It may be practical for C code to call "Python heap" dynamic allocation +functions such ``m_malloc()``, ``m_malloc0()`` and ``m_free()`` instead. + +See `python_memory_from_c` for more information about this approach. + +.. _cxx_support: + +C++ Modules +----------- + +Most Tier 1 & 2 MicroPython ports (and some Tier 3) support building C++ user +modules, using the C++-specific environment variables described above. + +Integrating C++ and MicroPython successfully involves some additional +considerations: + +C++ Dynamic Memory Allocation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +C++ programs (as well as C++ Standard Library features) typically use dynamic +memory allocation. The C++ default memory allocator (i.e. operators ``new`` and +``delete``) is typically implemented as a layer on top of `c_heap`. + +For MicroPython ports which don't include C dynamic memory allocation support, +C++ dynamic memory allocation can be supported in one of two ways: + +- Implement C dynamic memory allocation in your custom build. +- Implement a custom C++ allocator in your custom build. + +Linkage Considerations +~~~~~~~~~~~~~~~~~~~~~~ + +Because MicroPython is a C-based project, any symbols which link to or from +MicroPython need to be qualified ``extern "C"`` in C++ code. + +It's strongly recommended to follow the pattern demonstrated in +`examples/usercmodule/cppexample +`_, +where the Python module is implemented in a minimal C file wrapper around the +C++ code. + diff --git a/docs/develop/compiler.rst b/docs/develop/compiler.rst index cac92585ff4..0c25ad3a01e 100644 --- a/docs/develop/compiler.rst +++ b/docs/develop/compiler.rst @@ -98,7 +98,7 @@ Then also edit ``py/lexer.c`` to add the new keyword literal text: .. code-block:: c :emphasize-lines: 12 - STATIC const char *const tok_kw[] = { + static const char *const tok_kw[] = { ... "or", "pass", @@ -157,7 +157,7 @@ The most relevant method you should know about is this: mp_compile_to_raw_code(parse_tree, source_file, is_repl, &cm); // Create and return a function object that executes the outer module. - return mp_make_function_from_raw_code(cm.rc, cm.context, NULL); + return mp_make_function_from_proto_fun(cm.rc, cm.context, NULL); } The compiler compiles the code in four passes: scope, stack size, code size and emit. @@ -301,7 +301,7 @@ code statement: .. code-block:: c - STATIC void emit_native_unary_op(emit_t *emit, mp_unary_op_t op) { + static void emit_native_unary_op(emit_t *emit, mp_unary_op_t op) { vtype_kind_t vtype; emit_pre_pop_reg(emit, &vtype, REG_ARG_2); if (vtype == VTYPE_PYOBJ) { diff --git a/docs/develop/gettingstarted.rst b/docs/develop/gettingstarted.rst index c2d3816d425..a6afc5cad84 100644 --- a/docs/develop/gettingstarted.rst +++ b/docs/develop/gettingstarted.rst @@ -100,19 +100,19 @@ For the stm32 port, the ARM cross-compiler is required: .. code-block:: bash - $ sudo apt-get install arm-none-eabi-gcc arm-none-eabi-binutils arm-none-eabi-newlib + $ sudo apt-get install gcc-arm-none-eabi libnewlib-arm-none-eabi See the `ARM GCC toolchain `_ for the latest details. -Python is also required. Python 2 is supported for now, but we recommend using Python 3. +Python 3 is also required. Check that you have Python available on your system: .. code-block:: bash $ python3 - Python 3.5.0 (default, Jul 17 2020, 14:04:10) + Python 3.5.0 (default, Jul 17 2020, 14:04:10) [GCC 5.4.0 20160609] on linux Type "help", "copyright", "credits" or "license" for more information. >>> @@ -228,7 +228,7 @@ You can also specify which board to use: .. code-block:: bash $ cd ports/stm32 - $ make submodules + $ make BOARD= submodules $ make BOARD= See `ports/stm32/boards `_ @@ -245,7 +245,7 @@ that you use a virtual environment: $ python3 -m venv env $ source env/bin/activate - $ pip install sphinx + $ pip install -r docs/requirements.txt Navigate to the ``docs`` directory: @@ -278,10 +278,86 @@ To run a selection of tests on a board/device connected over USB use: .. code-block:: bash $ cd tests - $ ./run-tests.py --target minimal --device /dev/ttyACM0 + $ ./run-tests.py -t /dev/ttyACM0 See also :ref:`writingtests`. +Additional make targets for developers +-------------------------------------- + +In all ``make``-based ports, there is a target to print the size of a specific object file. +When a change is confined to a single file, this is useful when testing variations to find smaller alternatives. + +For instance, to print the size of ``objstr.o`` in the ``py/`` directory when making a unix standard build: + +.. code-block:: bash + + $ make build-standard/py/objstr.sz + +Similarly, there is a target to save the preprocessed version of a file: + +.. code-block:: bash + + $ make build-standard/py/objstr.pp + +In ``ports/unix`` there are additional targets related to running tests: + +.. code-block:: bash + + $ make test//int # Run all tests matching the pattern "int" + $ make test/ports/unix # Run all tests in ports/unix + $ make test-failures # Re-run only the failed tests + $ make print-failures # print the differences for failed tests + $ make clean-failures # delete the .exp and .out files from failed tests + +Using ci.sh locally +------------------- + +MicroPython uses GitHub Actions for continuous integration. +To reduce dependence on any specific CI system, the actual build steps for Unix-based builds are in the file ``tools/ci.sh``. +This can also be used as a script on developer desktops, with caveats: + +* For most steps, An Ubuntu/Debian system similar to the one used during CI is assumed. +* Some specific steps assume specific Ubuntu versions. +* The setup steps may invoke the system package manager to install packages, + download and install software from the internet, etc. + +To get a usage message including the list of commands, run: + +.. code-block:: bash + + $ tools/ci.sh --help + +As an example, you can build and test the unix minimal port with: + +.. code-block:: bash + + $ tools/ci.sh unix_minimal_build unix_minimal_run_tests + +If you use the bash shell, you can add a ``ci`` command with tab completion: + +.. code-block:: bash + + $ eval $(tools/ci.sh --bash-completion) + +For the zsh shell, replace ``--bash-completion`` with ``--zsh-completion``. +For the fish shell, replace ``--bash-completion`` with ``--fish-completion``. + +Then, typing: + +.. code-block:: bash + + $ ci unix_cov + +This will complete the ci step name to ``unix_coverage_``. +Pressing tab a second time will show the list of matching steps: + +.. code-block:: bash + + $ ci unix_coverage_ + unix_coverage_32bit_build + unix_coverage_32bit_run_native_mpy_tests… + Folder structure ---------------- diff --git a/docs/develop/index.rst b/docs/develop/index.rst index 327038f1978..1026b43d0c3 100644 --- a/docs/develop/index.rst +++ b/docs/develop/index.rst @@ -24,3 +24,4 @@ MicroPython to a new platform and implementing a core MicroPython library. publiccapi.rst extendingmicropython.rst porting.rst + support_tiers.rst diff --git a/docs/develop/library.rst b/docs/develop/library.rst index c2a86ea1699..6ff9bfe023d 100644 --- a/docs/develop/library.rst +++ b/docs/develop/library.rst @@ -26,15 +26,9 @@ Implementing a core module -------------------------- Like CPython, MicroPython has core builtin modules that can be accessed through import statements. -An example is the ``gc`` module discussed in :ref:`memorymanagement`. +An example is the :mod:`gc` module discussed in :ref:`memorymanagement`. -.. code-block:: bash - - >>> import gc - >>> gc.enable() - >>> - -MicroPython has several other builtin standard/core modules like ``io``, ``array`` etc. +MicroPython has several other builtin standard/core modules like :mod:`io`, :mod:`array`, etc. Adding a new core module involves several modifications. First, create the ``C`` file in the ``py/`` directory. In this example we are adding a @@ -48,16 +42,16 @@ hypothetical new module ``subsystem`` in the file ``modsubsystem.c``: #if MICROPY_PY_SUBSYSTEM // info() - STATIC mp_obj_t py_subsystem_info(void) { + static mp_obj_t py_subsystem_info(void) { return MP_OBJ_NEW_SMALL_INT(42); } MP_DEFINE_CONST_FUN_OBJ_0(subsystem_info_obj, py_subsystem_info); - STATIC const mp_rom_map_elem_t mp_module_subsystem_globals_table[] = { + static const mp_rom_map_elem_t mp_module_subsystem_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_subsystem) }, { MP_ROM_QSTR(MP_QSTR_info), MP_ROM_PTR(&subsystem_info_obj) }, }; - STATIC MP_DEFINE_CONST_DICT(mp_module_subsystem_globals, mp_module_subsystem_globals_table); + static MP_DEFINE_CONST_DICT(mp_module_subsystem_globals, mp_module_subsystem_globals_table); const mp_obj_module_t mp_module_subsystem = { .base = { &mp_type_module }, diff --git a/docs/develop/memorymgt.rst b/docs/develop/memorymgt.rst index 5b1690cc827..4ae80120044 100644 --- a/docs/develop/memorymgt.rst +++ b/docs/develop/memorymgt.rst @@ -4,43 +4,80 @@ Memory Management ================= Unlike programming languages such as C/C++, MicroPython hides memory management -details from the developer by supporting automatic memory management. -Automatic memory management is a technique used by operating systems or applications to automatically manage -the allocation and deallocation of memory. This eliminates challenges such as forgetting to -free the memory allocated to an object. Automatic memory management also avoids the critical issue of using memory -that is already released. Automatic memory management takes many forms, one of them being -garbage collection (GC). - -The garbage collector usually has two responsibilities; - -#. Allocate new objects in available memory. -#. Free unused memory. - -There are many GC algorithms but MicroPython uses the -`Mark and Sweep `_ -policy for managing memory. This algorithm has a mark phase that traverses the heap marking all -live objects while the sweep phase goes through the heap reclaiming all unmarked objects. - -Garbage collection functionality in MicroPython is available through the ``gc`` built-in -module: - -.. code-block:: bash - - >>> x = 5 - >>> x - 5 - >>> import gc - >>> gc.enable() - >>> gc.mem_alloc() - 1312 - >>> gc.mem_free() - 2071392 - >>> gc.collect() - 19 - >>> gc.disable() - >>> - -Even when ``gc.disable()`` is invoked, collection can be triggered with ``gc.collect()``. +details from the developer by supporting automatic memory management of a +":ref:`Python heap`" that holds all Python objects. MicroPython uses +garbage collection (GC) for automatic memory management. The garbage collector +is responsible for freeing memory which is no longer in use. + +Specifically, MicroPython uses a `Mark and Sweep +`_ +garbage collection algorithm. This algorithm has a mark phase that scans the +heap marking all live objects, and then a sweep phase goes through the heap +reclaiming all unmarked objects. + +The MicroPython garbage collector is by default automatic, but manual control is +available through the :mod:`gc` built-in module. + +.. _python_memory_from_c: + +MicroPython Memory from C code +------------------------------ + +Awareness of the garbage collector is needed when writing C code that allocates +memory from the "Python heap" (i.e. functions ``m_malloc()``, ``m_malloc0()``, +``m_free()``, etc). + +The mark phase of the garbage collector scans for live pointers to heap memory +starting from the following roots: + +- The stack of the main Python runtime (or REPL). +- The stacks of each "Python thread", for ports which implement Python threads + on top of native operating system threads or tasks. +- The "root pointers" defined in C code using the macro + ``MP_REGISTER_ROOT_POINTER``. These are the recommended way to have statically + scoped pointers to the Python heap. +- Tracked allocations made with the ``m_tracked_calloc()``, ``m_tracked_realloc`` + and ``m_tracked_free()`` functions. These special functions allow allocating a + block of memory which is always considered live by the garbage collector. + Similar to memory allocation in C, this memory is only freed by calling + ``m_tracked_free()`` or by soft reset. There is a small memory usage and + runtime overhead to each tracked allocation. This feature is not enabled by + default on all ports. + +The garbage collector then recursively scans and marks all the memory pointed to +by the root pointers, until all addresses are exhausted. This is sufficient to +find all Python objects that are still in use by the MicroPython runtime. + +However, the following memory will **not** be scanned by the garbage collector +and could be freed prematurely: + +- Static or global C variables which contain pointers to heap memory. +- Pointers which don't point to the "head" of an allocated buffer (i.e. to the + exact address returned by ``m_malloc()``), but instead to an address inside + the allocated buffer (for example, a pointer to a nested struct). For + performance reasons, the garbage collector doesn't mark the enclosing buffer + in these cases. +- The stack of any thread or RTOS task which isn't running Python code or + manually registered as a "Python thread" (for ports which support native + threads or tasks). + +Ways to avoid use-after-free in these scenarios: + +- Use the tracked allocation API ``m_tracked_calloc()``, ``m_tracked_realloc()`` + and ``m_tracked_free()``. +- Register a root pointer (see above), instead of storing a pointer in a static + variable. +- Restructure the code, for example by having an API where Python code + initialises a singleton Python object (implemented in C) which holds all of the + relevant pointers instead of having them in static variables. + +.. note:: :ref:`soft_reset` always clears the Python heap and frees all memory. + It's important not to hold any pointers to the heap after a soft + reset, as they will become dangling pointers to freed memory. + + Some ports support a "C heap" as well (see `c_heap`), in which case + you can allocate memory that will stay valid over soft reset by + calling standard C functions ``malloc``, etc. The object model ---------------- diff --git a/docs/develop/natmod.rst b/docs/develop/natmod.rst index 6d15f867bcf..e0f7bdaaa89 100644 --- a/docs/develop/natmod.rst +++ b/docs/develop/natmod.rst @@ -39,11 +39,18 @@ options for the ``ARCH`` variable, see below): * ``armv7emsp`` (ARM Thumb 2, single precision float, eg Cortex-M4F, Cortex-M7) * ``armv7emdp`` (ARM Thumb 2, double precision float, eg Cortex-M7) * ``xtensa`` (non-windowed, eg ESP8266) -* ``xtensawin`` (windowed with window size 8, eg ESP32) +* ``xtensawin`` (windowed with window size 8, eg ESP32, ESP32S3) +* ``rv32imc`` (RISC-V 32 bits with compressed instructions, eg ESP32C3, ESP32C6) +* ``rv64imc`` (RISC-V 64 bits with compressed instructions) + +If the chosen platform supports explicit architecture flags and you want to let +the output .mpy file carry those flags' value, you must pass them to the +``ARCH_FLAGS`` flags variable when building the .mpy file. When compiling and linking the native .mpy file the architecture must be chosen -and the corresponding file can only be imported on that architecture. For more -details about .mpy files see :ref:`mpy_files`. +and the corresponding file can only be imported on that architecture (and if +architecture flags are present, only if they match the target's capabilities). +For more details about .mpy files see :ref:`mpy_files`. Native code must be compiled as position independent code (PIC) and use a global offset table (GOT), although the details of this varies from architecture to @@ -66,14 +73,31 @@ The known limitations are: * static BSS variables are not supported; workaround: use global BSS variables +* thread-local storage variables are not supported on rv32imc; workaround: use + global BSS variables or allocate some space on the heap to store them + So, if your C code has writable data, make sure the data is defined globally, without an initialiser, and only written to within functions. +The native module is not automatically linked against the standard static libraries +like ``libm.a`` and ``libgcc.a``, which can lead to ``undefined symbol`` errors. +You can link the runtime libraries by setting ``LINK_RUNTIME = 1`` +in your Makefile. Custom static libraries can also be linked by adding +``MPY_LD_FLAGS += -l path/to/library.a``. Note that these are linked into +the native module and will not be shared with other modules or the system. + Linker limitation: the native module is not linked against the symbol table of the full MicroPython firmware. Rather, it is linked against an explicit table of exported symbols found in ``mp_fun_table`` (in ``py/nativeglue.h``), that is fixed at firmware build time. It is thus not possible to simply call some arbitrary HAL/OS/RTOS/system -function, for example. +function, for example, unless that resides at a fixed address. In that case, the path +of a linkerscript containing a series of symbol names and their fixed address can be +passed to ``mpy_ld.py`` via the ``--externs`` command line argument. That way symbols +appearing in the linkerscript will take precedence over what is provided from object +files, but at the moment the object files' implementation will still reside in the +final MPY file. The linkerscript parser is limited in its capabilities, and is +currently used only for parsing the ESP8266 port ROM symbols list (see +``ports/esp8266/boards/eagle.rom.addr.v6.ld``). New symbols can be added to the end of the table and the firmware rebuilt. The symbols also need to be added to ``tools/mpy_ld.py``'s ``fun_table`` dict in the @@ -105,7 +129,8 @@ The filesystem layout consists of two main parts, the source files and the Makef location of the MicroPython repository (to find header files, the relevant Makefile fragment, and the ``mpy_ld.py`` tool), ``MOD`` as the name of the module, ``SRC`` as the list of source files, optionally specify the machine architecture via ``ARCH``, - and then include ``py/dynruntime.mk``. + along with optional machine architecture flags specified via ``ARCH_FLAGS``, and + then include ``py/dynruntime.mk``. Minimal example --------------- @@ -128,7 +153,7 @@ The file ``factorial.c`` contains: #include "py/dynruntime.h" // Helper function to compute factorial - STATIC mp_int_t factorial_helper(mp_int_t x) { + static mp_int_t factorial_helper(mp_int_t x) { if (x == 0) { return 1; } @@ -136,7 +161,7 @@ The file ``factorial.c`` contains: } // This is the function which will be called from Python, as factorial(x) - STATIC mp_obj_t factorial(mp_obj_t x_obj) { + static mp_obj_t factorial(mp_obj_t x_obj) { // Extract the integer from the MicroPython input object mp_int_t x = mp_obj_get_int(x_obj); // Calculate the factorial @@ -145,7 +170,7 @@ The file ``factorial.c`` contains: return mp_obj_new_int(result); } // Define a Python reference to the function above - STATIC MP_DEFINE_CONST_FUN_OBJ_1(factorial_obj, factorial); + static MP_DEFINE_CONST_FUN_OBJ_1(factorial_obj, factorial); // This is the entry point and is called when the module is imported mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) { @@ -172,7 +197,7 @@ The file ``Makefile`` contains: # Source files (.c or .py) SRC = factorial.c - # Architecture to build for (x86, x64, armv6m, armv7m, xtensa, xtensawin) + # Architecture to build for (x86, x64, armv6m, armv7m, xtensa, xtensawin, rv32imc, rv64imc) ARCH = x64 # Include to get the rules for compiling and linking the module @@ -198,6 +223,10 @@ Without modifying the Makefile you can specify the target architecture via:: $ make ARCH=armv7m +Same applies for optional architecture flags via:: + + $ make ARCH=rv32imc ARCH_FLAGS=zba + Module usage in MicroPython --------------------------- @@ -210,6 +239,26 @@ other module, for example:: print(factorial.factorial(10)) # should display 3628800 +Using Picolibc when building modules +------------------------------------ + +Using `Picolibc `_ as your C standard +library is not only supported, but in fact it is the default for the rv32imc and +rv64imc platforms. However, there are a couple of things worth mentioning to make +sure you don't run into problems later when building code. + +Some pre-built Picolibc versions (for example, those provided by Ubuntu Linux +as the ``picolibc-arm-none-eabi``, ``picolibc-riscv64-unknown-elf``, and +``picolibc-xtensa-lx106-elf`` packages) assume thread-local storage (TLS) is +available at runtime, but unfortunately MicroPython modules do not support that +on some architectures (namely ``rv32imc`` and ``rv64imc``). This means that some +functionalities provided by Picolibc will default to use TLS, returning an +error either during compilation or during linking. + +For an example on how this may affect you, the ``examples/natmod/btree`` +example module contains a workaround to make sure ``errno`` works (look for +``__PICOLIBC_ERRNO_FUNCTION`` in the Makefile and follow the trail from there). + Further examples ---------------- diff --git a/docs/develop/optimizations.rst b/docs/develop/optimizations.rst index 7f2c8cbe728..3f07aebbc66 100644 --- a/docs/develop/optimizations.rst +++ b/docs/develop/optimizations.rst @@ -33,7 +33,7 @@ Variables MicroPython processes local and global variables differently. Global variables are stored and looked up from a global dictionary that is allocated on the heap (note that each module has its own separate dict, so separate namespace). -Local variables on the other hand are are stored on the Python value stack, which may +Local variables on the other hand are stored on the Python value stack, which may live on the C stack or on the heap. They are accessed directly by their offset within the Python stack, which is more efficient than a global lookup in a dict. @@ -59,6 +59,9 @@ Compiles to: X = 1 foo(1, 2) +See :func:`micropython.const` for complete details on usage requirements and +limitations. + Allocation of memory -------------------- diff --git a/docs/develop/porting.rst b/docs/develop/porting.rst index fab8a751b84..28d7b3dd513 100644 --- a/docs/develop/porting.rst +++ b/docs/develop/porting.rst @@ -42,7 +42,6 @@ The basic MicroPython firmware is implemented in the main port file, e.g ``main. #include "py/compile.h" #include "py/gc.h" #include "py/mperrno.h" - #include "py/stackctrl.h" #include "shared/runtime/gchelper.h" #include "shared/runtime/pyexec.h" @@ -51,7 +50,7 @@ The basic MicroPython firmware is implemented in the main port file, e.g ``main. int main(int argc, char **argv) { // Initialise the MicroPython runtime. - mp_stack_ctrl_init(); + mp_cstack_init_with_sp_here(2048); gc_init(heap, heap + sizeof(heap)); mp_init(); @@ -83,7 +82,7 @@ The basic MicroPython firmware is implemented in the main port file, e.g ``main. } // There is no filesystem so opening a file raises an exception. - mp_lexer_t *mp_lexer_new_from_file(const char *filename) { + mp_lexer_t *mp_lexer_new_from_file(qstr filename) { mp_raise_OSError(MP_ENOENT); } @@ -151,9 +150,6 @@ The following is an example of an ``mpconfigport.h`` file: #define MICROPY_ERROR_REPORTING (MICROPY_ERROR_REPORTING_TERSE) #define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_FLOAT) - // Enable u-modules to be imported with their standard name, like sys. - #define MICROPY_MODULE_WEAK_LINKS (1) - // Fine control over Python builtins, classes, modules, etc. #define MICROPY_PY_ASYNC_AWAIT (0) #define MICROPY_PY_BUILTINS_SET (0) @@ -165,8 +161,6 @@ The following is an example of an ``mpconfigport.h`` file: // Type definitions for the specific machine. - typedef intptr_t mp_int_t; // must be pointer size - typedef uintptr_t mp_uint_t; // must be pointer size typedef long mp_off_t; // We need to provide a declaration/definition of alloca(). @@ -247,10 +241,12 @@ That should give a MicroPython REPL. You can then run commands like: .. code-block:: bash - MicroPython v1.13 on 2021-01-01; example-board with unknown-cpu - >>> import sys - >>> sys.implementation - ('micropython', (1, 13, 0)) + MicroPython v1.26.0-preview on 2025-08-01; minimal with unknown-cpu + >>> def sum(n, m): + ... return n + m + ... + >>> 3, 4, sum(3, 4) + (3, 4, 7) >>> Use Ctrl-D to exit, and then run ``reset`` to reset the terminal. @@ -265,17 +261,17 @@ To add a custom module like ``myport``, first add the module definition in a fil #include "py/runtime.h" - STATIC mp_obj_t myport_info(void) { + static mp_obj_t myport_info(void) { mp_printf(&mp_plat_print, "info about my port\n"); return mp_const_none; } - STATIC MP_DEFINE_CONST_FUN_OBJ_0(myport_info_obj, myport_info); + static MP_DEFINE_CONST_FUN_OBJ_0(myport_info_obj, myport_info); - STATIC const mp_rom_map_elem_t myport_module_globals_table[] = { + static const mp_rom_map_elem_t myport_module_globals_table[] = { { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_myport) }, { MP_ROM_QSTR(MP_QSTR_info), MP_ROM_PTR(&myport_info_obj) }, }; - STATIC MP_DEFINE_CONST_DICT(myport_module_globals, myport_module_globals_table); + static MP_DEFINE_CONST_DICT(myport_module_globals, myport_module_globals_table); const mp_obj_module_t myport_module = { .base = { &mp_type_module }, diff --git a/docs/develop/support_tiers.rst b/docs/develop/support_tiers.rst new file mode 100644 index 00000000000..6c62c495d16 --- /dev/null +++ b/docs/develop/support_tiers.rst @@ -0,0 +1,68 @@ +MicroPython Support Tiers +========================= + +MicroPython operates with a set of Support Tier levels for the various ports. +Tiers 1, 2 and 3 are the main Tier levels with Tier 1 being the most mature and +actively maintained. There is also Tier M for additional ports used primarily +for maintenance, development and testing. These Tier levels are defined in the +table below. + +.. table:: + :widths: 40 9 9 9 9 + + +-----------------------------------------------+--------+--------+--------+--------+ + | | Tier 1 | Tier 2 | Tier 3 | Tier M | + +-----------------------------------------------+--------+--------+--------+--------+ + | builds pass under CI | ✔ | ✔ | ✔ | ✔ | + +-----------------------------------------------+--------+--------+--------+--------+ + | tests run under CI (where possible) | ✔ | ✔ | ✔ | ✔ | + +-----------------------------------------------+--------+--------+--------+--------+ + | actively maintained | ✔ | ✔ | | ✔ | + +-----------------------------------------------+--------+--------+--------+--------+ + | stable Python API | ✔ | ✔ | | | + +-----------------------------------------------+--------+--------+--------+--------+ + | new features actively developed | ✔ | ✔ | | | + +-----------------------------------------------+--------+--------+--------+--------+ + | tested on hardware for releases | ✔ | ✔ | | | + +-----------------------------------------------+--------+--------+--------+--------+ + | prioritized bug reports | ✔ | | | ✔ | + +-----------------------------------------------+--------+--------+--------+--------+ + | regressions warrant a patch release | ✔ | | | ✔ | + +-----------------------------------------------+--------+--------+--------+--------+ + | has port-specific documentation | ✔ | | | | + +-----------------------------------------------+--------+--------+--------+--------+ + +Lower Tiers may tick more boxes, but the above table defines the minimum requirements +for a port to belong to a Tier. + +Tier 1 ports: + + - esp32 + - mimxrt + - rp2 + - samd + - stm32 + - unix + - windows + +Tier 2 ports: + + - alif + - embed + - nrf + - psoc-edge + - renesas-ra + - webassembly + - zephyr + +Tier 3 ports: + + - cc3200 + - esp8266 + - pic16bit + +Tier M ports: + + - bare-arm + - minimal + - qemu diff --git a/docs/develop/writingtests.rst b/docs/develop/writingtests.rst index 9bb5178f55f..7ac7e038ca8 100644 --- a/docs/develop/writingtests.rst +++ b/docs/develop/writingtests.rst @@ -46,6 +46,11 @@ If you run your tests, this test should appear in the test output: Tests are run by comparing the output from the test target against the output from CPython. So any test should use print statements to indicate test results. +When writing tests for name or string-related functionality, please add both English/ASCII +as well as non-English/non-ASCII text and include Unicode examples. +Please do add comments in English explaining the meaning and intent of the Unicode text. +This help ensure Unicode support is tested and verified across different platforms. + For tests that can't be compared to CPython (i.e. micropython-specific functionality), you can provide a ``.py.exp`` file which will be used as the truth for comparison. @@ -60,7 +65,7 @@ Then to run on a board: .. code-block:: bash - $ ./run-tests.py --target minimal --device /dev/ttyACM0 + $ ./run-tests.py -t /dev/ttyACM0 And to run only a certain set of tests (eg a directory): @@ -68,3 +73,76 @@ And to run only a certain set of tests (eg a directory): $ ./run-tests.py -d basics $ ./run-tests.py float/builtin*.py + +Using run-tests.py +------------------ + +The ``run-tests.py`` script supports several parameters to customize test execution: + +**Target and Device Selection:** + +* ``-t, --test-instance`` + +The -t option accepts the following for the test instance: + +- **unix** - use the unix port of MicroPython, specified by the MICROPY_MICROPYTHON + environment variable (which defaults to the standard variant of either the unix + or windows ports, depending on the host platform) +- **webassembly** - use the webassembly port of MicroPython, specified by the + MICROPY_MICROPYTHON_MJS environment variable (which defaults to the standard + variant of the webassembly port) +- **port:** - connect to and use the given serial port device +- **a** - connect to and use /dev/ttyACM +- **u** - connect to and use /dev/ttyUSB +- **c** - connect to and use COM +- **exec:** - execute a command and attach to it's stdin/stdout +- **execpty:** - execute a command and attach to the printed /dev/pts/ device +- **...** - connect to the given IPv4 address +- anything else specifies a serial port + +**Test Selection:** + +* ``-d, --test-dirs`` - Specify one or more test directories to run +* ``-i, --include REGEX`` - Include tests matching regex pattern +* ``-e, --exclude REGEX`` - Exclude tests matching regex pattern +* ``files`` - Specific test files to run + +**Execution Options:** + +* ``--emit `` - MicroPython emitter, EMITTER can be bytecode or native. Default: bytecode +* ``--via-mpy`` - Compile .py files to .mpy first +* ``--heapsize`` - Set heap size for tests +* ``-j, --jobs N`` - Number of tests to run simultaneously + +Set the MICROPY_MPYCROSS environment variable to use a specific version of ``mpy-cross`` when using ``--via-mpy``. + +**Result Management:** + +* ``-r, --result-dir`` - Directory for test results. Default: results/ +* ``--print-failures`` - Show diff of failed tests and exit +* ``--clean-failures`` - Delete .exp and .out files from prior failed tests +* ``--run-failures`` - Re-run only previously failed tests + +**Examples:** + +.. code-block:: bash + + # Run only basic tests with native emitter + $ ./run-tests.py --emit native -d basics extmod + + # Run tests excluding async functionality + $ ./run-tests.py -e async + + # Run tests matching *_pep_* + $ ./run-tests.py -i *_pep_* + + # Run specific test files in parallel + $ ./run-tests.py -j 4 basics/list*.py + + # Test on connected ESP32 board + $ ./run-tests.py -t /dev/ttyUSB0 + # or + $ ./run-tests.py -t u0 + + # Re-run only failed tests from previous run + $ ./run-tests.py --run-failures diff --git a/docs/differences/modules_preamble.txt b/docs/differences/modules_preamble.txt new file mode 100644 index 00000000000..1958f0084d2 --- /dev/null +++ b/docs/differences/modules_preamble.txt @@ -0,0 +1,33 @@ +.. Preamble section inserted into generated output + +Positional-only Parameters +-------------------------- + +To save code size, many functions that accept keyword arguments in CPython only accept positional arguments in MicroPython. + +MicroPython marks positional-only parameters in the same way as CPython, by inserting a ``/`` to mark the end of the positional parameters. Any function whose signature ends in ``/`` takes *only* positional arguments. For more details, see `PEP 570 `_. + +Example +~~~~~~~ + +For example, in CPython 3.4 this is the signature of the constructor ``socket.socket``:: + + socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None) + +However, the signature documented in :func:`MicroPython` is:: + + socket(af=AF_INET, type=SOCK_STREAM, proto=IPPROTO_TCP, /) + +The ``/`` at the end of the parameters indicates that they are all positional-only in MicroPython. The following code works in CPython but not in most MicroPython ports:: + + import socket + s = socket.socket(type=socket.SOCK_DGRAM) + +MicroPython will raise an exception:: + + TypeError: function doesn't take keyword arguments + +The following code will work in both CPython and MicroPython:: + + import socket + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) diff --git a/docs/differences/python_36.rst b/docs/differences/python_36.rst index 3315b0594da..18da79f8f84 100644 --- a/docs/differences/python_36.rst +++ b/docs/differences/python_36.rst @@ -25,7 +25,8 @@ Python 3.6 beta 1 was released on 12 Sep 2016, and a summary of the new features +--------------------------------------------------------+--------------------------------------------------+-----------------+ | `PEP 468 `_ | Preserving the order of *kwargs* in a function | | +--------------------------------------------------------+--------------------------------------------------+-----------------+ - | `PEP 487 `_ | Simpler customization of class creation | | + | `PEP 487 `_ | Simpler customization of class creation | Partial | + | | | [#setname]_ | +--------------------------------------------------------+--------------------------------------------------+-----------------+ | `PEP 520 `_ | Preserving Class Attribute Definition Order | | +--------------------------------------------------------+--------------------------------------------------+-----------------+ @@ -198,3 +199,7 @@ Changes to built-in modules: +--------------------------------------------------------------------------------------------------------------+----------------+ | The *compress()* and *decompress()* functions now accept keyword arguments | | +--------------------------------------------------------------------------------------------------------------+----------------+ + +.. rubric:: Notes + +.. [#setname] Currently, only :func:`__set_name__` is implemented. diff --git a/docs/differences/python_37.rst b/docs/differences/python_37.rst index 86d1b6e81f9..2b9a089217f 100644 --- a/docs/differences/python_37.rst +++ b/docs/differences/python_37.rst @@ -105,4 +105,4 @@ Changes to built-in modules: .. rubric:: Notes -.. [#ftimenanosec] Only :func:`time.time_ns` is implemented. +.. [#ftimenanosec] Only :func:`time.time_ns` is implemented. diff --git a/docs/esp32/quickref.rst b/docs/esp32/quickref.rst index e19c6ecb408..6b3fa951653 100644 --- a/docs/esp32/quickref.rst +++ b/docs/esp32/quickref.rst @@ -18,6 +18,9 @@ working with this board it may be useful to get an overview of the microcontroll general.rst tutorial/index.rst +Note that there are several varieties of ESP32 -- ESP32, ESP32C3, ESP32C6, ESP32S2, ESP32S3 -- +supported by MicroPython, with some differences in functionality between them. + Installing MicroPython ---------------------- @@ -57,52 +60,57 @@ The :mod:`esp32` module:: import esp32 - esp32.hall_sensor() # read the internal hall sensor esp32.raw_temperature() # read the internal temperature of the MCU, in Fahrenheit - esp32.ULP() # access to the Ultra-Low-Power Co-processor + esp32.ULP() # access to the Ultra-Low-Power Co-processor, not on ESP32C3/C6 Note that the temperature sensor in the ESP32 will typically read higher than ambient due to the IC getting warm while it runs. This effect can be minimised by reading the temperature sensor immediately after waking up from sleep. +ESP32C3, ESP32C6, ESP32S2, and ESP32S3 also have an internal temperature sensor available. +It is implemented a bit differently to the ESP32 and returns the temperature in +Celsius:: + + esp32.mcu_temperature() # read the internal temperature of the MCU, in Celsius + Networking ---------- WLAN ^^^^ -The :mod:`network` module:: +The :class:`network.WLAN` class in the :mod:`network` module:: import network - wlan = network.WLAN(network.STA_IF) # create station interface - wlan.active(True) # activate the interface - wlan.scan() # scan for access points - wlan.isconnected() # check if the station is connected to an AP + wlan = network.WLAN() # create station interface (the default, see below for an access point interface) + wlan.active(True) # activate the interface + wlan.scan() # scan for access points + wlan.isconnected() # check if the station is connected to an AP wlan.connect('ssid', 'key') # connect to an AP - wlan.config('mac') # get the interface's MAC address - wlan.ifconfig() # get the interface's IP/netmask/gw/DNS addresses + wlan.config('mac') # get the interface's MAC address + wlan.ipconfig('addr4') # get the interface's IPv4 addresses - ap = network.WLAN(network.AP_IF) # create access-point interface - ap.config(ssid='ESP-AP') # set the SSID of the access point - ap.config(max_clients=10) # set how many clients can connect to the network - ap.active(True) # activate the interface + ap = network.WLAN(network.WLAN.IF_AP) # create access-point interface + ap.config(ssid='ESP-AP') # set the SSID of the access point + ap.config(max_clients=10) # set how many clients can connect to the network + ap.active(True) # activate the interface A useful function for connecting to your local WiFi network is:: def do_connect(): - import network - wlan = network.WLAN(network.STA_IF) + import machine, network + wlan = network.WLAN() wlan.active(True) if not wlan.isconnected(): print('connecting to network...') wlan.connect('ssid', 'key') while not wlan.isconnected(): - pass - print('network config:', wlan.ifconfig()) + machine.idle() + print('network config:', wlan.ipconfig('addr4')) Once the network is established the :mod:`socket ` module can be used -to create and use TCP/UDP sockets as usual, and the ``urequests`` module for +to create and use TCP/UDP sockets as usual, and the ``requests`` module for convenient HTTP requests. After a call to ``wlan.connect()``, the device will by default retry to connect @@ -113,44 +121,61 @@ calling ``wlan.config(reconnects=n)``, where n are the number of desired reconne attempts (0 means it won't retry, -1 will restore the default behaviour of trying to reconnect forever). +.. _esp32_network_lan: + LAN ^^^ -To use the wired interfaces one has to specify the pins and mode :: +Built-in MAC (original ESP32) +""""""""""""""""""""""""""""" + +The original ESP32 SoC has a built-in Ethernet MAC. Using this MAC requires an +external Ethernet PHY to be wired to the chip's EMAC pins. Most of the EMAC pin +assignments are fixed, consult the ESP32 datasheet for details. + +If the PHY is connected, the internal Ethernet MAC can be configured via +the :class:`network.LAN` constructor:: import network lan = network.LAN(mdc=PIN_MDC, ...) # Set the pin and mode configuration lan.active(True) # activate the interface - lan.ifconfig() # get the interface's IP/netmask/gw/DNS addresses + lan.ipconfig('addr4') # get the interface's IPv4 addresses + + +Required keyword arguments for the constructor: +- ``mdc`` and ``mdio`` - :class:`machine.Pin` objects (or integers) specifying + the MDC and MDIO pins. +- ``phy_type`` - Select the PHY device type. Supported devices are + ``PHY_GENERIC``, + ``PHY_LAN8710``, ``PHY_LAN8720``, ``PHY_IP101``, ``PHY_RTL8201``, + ``PHY_DP83848``, ``PHY_KSZ8041`` and ``PHY_KSZ8081``. These values are all + constants defined in the ``network`` module. +- ``phy_addr`` - The address number of the PHY device. Must be an integer in the + range 0x00 to 0x1f, inclusive. Common values are ``0`` and ``1``. -The keyword arguments for the constructor defining the PHY type and interface are: +All of the above keyword arguments must be present to configure the interface. -- mdc=pin-object # set the mdc and mdio pins. -- mdio=pin-object -- power=pin-object # set the pin which switches the power of the PHY device. -- phy_type= # Select the PHY device type. Supported devices are PHY_LAN8710, - PHY_LAN8720, PH_IP101, PHY_RTL8201, PHY_DP83848 and PHY_KSZ8041 -- phy_addr=number # The address number of the PHY device. -- ref_clk_mode=mode # Defines, whether the ref_clk at the ESP32 is an input - or output. Suitable values are Pin.IN and Pin.OUT. -- ref_clk=pin-object # defines the Pin used for ref_clk. +Optional keyword arguments: -The options ref_clk_mode and ref_clk require at least esp-idf version 4.4. For -earlier esp-idf versions, these parameters must be defined by kconfig board options. +- ``reset`` - :class:`machine.Pin` object (or integer) specifying the PHY reset pin. +- ``power`` - :class:`machine.Pin` object (or integer) specifying a pin which + switches the power of the PHY device. +- ``ref_clk`` - :class:`machine.Pin` object (or integer) specifying the pin used + for the EMAC ``ref_clk`` signal. If not specified, the board default is used + (typically GPIO 0, but may be different if a particular board has Ethernet.) +- ``ref_clk_mode`` - Defines whether the EMAC ``ref_clk`` pin of the ESP32 + should be an input or an output. Suitable values are ``machine.Pin.IN`` and + ``machine.Pin.OUT``. If not specified, the board default is used + (typically input, but may be different if a particular board has Ethernet.) -These are working configurations for LAN interfaces of popular boards:: +These are working configurations for LAN interfaces of some popular ESP32 boards:: # Olimex ESP32-GATEWAY: power controlled by Pin(5) # Olimex ESP32 PoE and ESP32-PoE ISO: power controlled by Pin(12) lan = network.LAN(mdc=machine.Pin(23), mdio=machine.Pin(18), power=machine.Pin(5), - phy_type=network.PHY_LAN8720, phy_addr=0) - - # or with dynamic ref_clk pin configuration - - lan = network.LAN(mdc=machine.Pin(23), mdio=machine.Pin(18), power=machine.Pin(5), phy_type=network.PHY_LAN8720, phy_addr=0, ref_clk=machine.Pin(17), ref_clk_mode=machine.Pin.OUT) @@ -159,20 +184,86 @@ These are working configurations for LAN interfaces of popular boards:: lan = network.LAN(mdc=machine.Pin(23), mdio=machine.Pin(18), phy_type=network.PHY_LAN8720, phy_addr=1, power=None) + # Wireless-Tag's WT32-ETH01 v1.4 + + lan = network.LAN(mdc=machine.Pin(23), mdio=machine.Pin(18), + phy_type=network.PHY_LAN8720, phy_addr=1, + power=machine.Pin(16)) + # Espressif ESP32-Ethernet-Kit_A_V1.2 - lan = network.LAN(id=0, mdc=Pin(23), mdio=Pin(18), power=Pin(5), + lan = network.LAN(id=0, mdc=machine.Pin(23), mdio=machine.Pin(18), power=machine.Pin(5), phy_type=network.PHY_IP101, phy_addr=1) -A suitable definition of the PHY interface in a sdkconfig.board file is:: + # ESP32-WROOM-32UE with KSZ8863RLL (Integrated 3-Port 10/100 Managed Switch with PHYs) + + lan = network.LAN(mdc=machine.Pin(23), # connected to SCL_MDC pin of KSZ8863RLL + mdio=machine.Pin(15), # connected to SDA_MDIO pin of KSZ8863RLL + power=machine.Pin(16), # connected to RSTN pin of KSZ8863RLL + phy_type=network.PHY_GENERIC, + phy_addr=3, + ref_clk_mode=machine.Pin.IN, + ref_clk=machine.Pin(0)) # REF_CLK 50MHz from KSZ8863RLL + + +.. _esp32_spi_ethernet: + +SPI Ethernet Interface +"""""""""""""""""""""" + +All ESP32 SoCs support external SPI Ethernet interface chips. These are Ethernet +interfaces that connect via a SPI bus, rather than an Ethernet RMII interface. + +.. note:: The only exception is the ESP32 ``d2wd`` variant, where this feature is disabled + to save code size. + +SPI Ethernet uses the same :class:`network.LAN` constructor, with a different +set of keyword arguments:: + + import machine, network + + spi = machine.SPI(1, sck=SCK_PIN, mosi=MOSI_PIN, miso=MISO_PIN) + lan = network.LAN(spi=spi, cs=CS_PIN, ...) # Set the pin and mode configuration + lan.active(True) # activate the interface + lan.ipconfig('addr4') # get the interface's IPv4 addresses + +Required keyword arguments for the constructor: + +- ``spi`` - Should be a :class:`machine.SPI` object configured for this + connection. Note that any clock speed configured on the SPI object is ignored, + the SPI Ethernet clock speed is configured at compile time. +- ``cs`` - :class:`machine.Pin` object (or integer) specifying the CS pin + connected to the interface. +- ``int`` - :class:`machine.Pin` object (or integer) specifying the INT pin + connected to the interface. +- ``phy_type`` - Select the SPI Ethernet interface type. Supported devices are + ``PHY_KSZ8851SNL``, ``PHY_DM9051``, ``PHY_W5500``. These values are all + constants defined in the ``network`` module. +- ``phy_addr`` - The address number of the PHY device. Must be an integer in the + range 0x00 to 0x1f, inclusive. This is usually ``0`` for SPI Ethernet devices. + +All of the above keyword arguments must be present to configure the interface. + +Optional keyword arguments for the constructor: + +- ``reset`` - :class:`machine.Pin` object (or integer) specifying the SPI Ethernet + interface reset pin. +- ``power`` - :class:`machine.Pin` object (or integer) specifying a pin which + switches the power of the SPI Ethernet interface. + +Here is a sample configuration for a WIZNet W5500 chip connected to pins on +an ESP32-S3 development board:: + + import machine, network + from machine import Pin, SPI - CONFIG_ETH_PHY_INTERFACE_RMII=y - CONFIG_ETH_RMII_CLK_OUTPUT=y - CONFIG_ETH_RMII_CLK_OUT_GPIO=17 - CONFIG_LWIP_LOCAL_HOSTNAME="ESP32_POE" + spi = SPI(1, sck=Pin(12), mosi=Pin(13), miso=Pin(14)) + lan = network.LAN(spi=spi, phy_type=network.PHY_W5500, phy_addr=0, + cs=Pin(10), int=Pin(11)) -The value assigned to CONFIG_ETH_RMII_CLK_OUT_GPIO may vary depending on the -board's wiring. +.. note:: WIZnet W5500 Ethernet is also supported on some other MicroPython + ports, but using a :ref:`different software interface + `. Delay and timing ---------------- @@ -190,8 +281,10 @@ Use the :mod:`time

- This is the v{{ release }} version of the MicroPython + This is the {{ release }} version of the MicroPython documentation. The latest development version of this page may be more current.

diff --git a/docs/templates/topindex.html b/docs/templates/topindex.html index e3bcd7cce14..344b971ea79 100644 --- a/docs/templates/topindex.html +++ b/docs/templates/topindex.html @@ -57,6 +57,10 @@

MicroPython documentation

Quick reference for the ESP32
pinout for ESP32-based boards, snippets of useful code, and a tutorial

+

-    
   
 
@@ -88,6 +123,10 @@ MicroPython code execution will suspend the browser so be sure to atomize usage
 within this environment. Unfortunately interrupts have not been implemented for the
 browser.
 
+In Node.js, pressing Ctrl+C during Python execution sends a keyboard interrupt
+(``KeyboardInterrupt``) to the running MicroPython instance via ``mp_js_hook``,
+which periodically polls stdin for the interrupt character.
+
 Testing
 -------
 
@@ -98,31 +137,55 @@ Run the test suite using:
 API
 ---
 
-The following functions have been exposed to javascript.
+The following functions have been exposed to JavaScript through the interpreter
+context, created and returned by `loadMicroPython()`.
 
-```
-mp_js_init(stack_size)
-```
+- `PyProxy`: the type of the object that proxies Python objects.
 
-Initialize MicroPython with the given stack size in bytes. This must be
-called before attempting to interact with MicroPython.
+- `FS`: the Emscripten filesystem object.
 
-```
-mp_js_do_str(code)
-```
+- `globals`: an object exposing the globals from the Python `__main__` module,
+  with methods `get(key)`, `set(key, value)` and `delete(key)`.
 
-Execute the input code. `code` must be a `string`.
+- `registerJsModule(name, module)`: register a JavaScript object as importable
+  from Python with the given name.
 
-```
-mp_js_init_repl()
-```
+- `pyimport`: import a Python module and return it.
 
-Initialize MicroPython repl. Must be called before entering characters into
-the repl.
+- `runPython(code)`: execute Python code and return the result.
 
-```
-mp_js_process_char(char)
-```
+- `runPythonAsync(code)`: execute Python code and return the result, allowing for
+  top-level await expressions (this call must be await'ed on the JavaScript side).
+
+- `replInit()`: initialise the REPL.
+
+- `replProcessChar(chr)`: process an incoming character at the REPL.
+
+- `replProcessCharWithAsyncify(chr)`: process an incoming character at the REPL,
+  for use when ASYNCIFY is enabled.
+
+Type conversions
+----------------
+
+Read-only objects (booleanns, numbers, strings, etc) are converted when passed between
+Python and JavaScript.  The conversions are:
+
+- JavaScript `null` converts to/from Python `None`.
+- JavaScript `undefined` converts to/from Python `js.undefined`.
+
+The conversion between `null` and `None` matches the behaviour of the Python `json`
+module.
+
+Proxying
+--------
+
+A Python `dict` instance is proxied such that:
+
+    for (const key in dict) {
+        print(key, dict[key]);
+    }
 
-Input character into MicroPython repl. `char` must be of type `number`. This
-will execute MicroPython code when necessary.
+works as expected on the JavaScript side and iterates through the keys of the
+Python `dict`.  Furthermore, when JavaScript accesses a key that does not exist
+in the Python dict, the JavaScript code receives `undefined` instead of a
+`KeyError` exception being raised.
diff --git a/ports/webassembly/api.js b/ports/webassembly/api.js
new file mode 100644
index 00000000000..0718d13d678
--- /dev/null
+++ b/ports/webassembly/api.js
@@ -0,0 +1,297 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+// Options:
+// - pystack: size in words of the MicroPython Python stack.
+// - heapsize: size in bytes of the MicroPython GC heap.
+// - url: location to load `micropython.mjs`.
+// - stdin: function to return input characters.
+// - stdout: function that takes one argument, and is passed lines of stdout
+//   output as they are produced.  By default this is handled by Emscripten
+//   and in a browser goes to console, in node goes to process.stdout.write.
+// - stderr: same behaviour as stdout but for error output.
+// - linebuffer: whether to buffer line-by-line to stdout/stderr.
+export async function loadMicroPython(options) {
+    const { pystack, heapsize, url, stdin, stdout, stderr, linebuffer } =
+        Object.assign(
+            { pystack: 2 * 1024, heapsize: 1024 * 1024, linebuffer: true },
+            options,
+        );
+    let Module = {};
+    Module.locateFile = (path, scriptDirectory) =>
+        url || scriptDirectory + path;
+    Module._textDecoder = new TextDecoder();
+    if (stdin !== undefined) {
+        Module.stdin = stdin;
+    }
+    if (stdout !== undefined) {
+        if (linebuffer) {
+            Module._stdoutBuffer = [];
+            Module.stdout = (c) => {
+                if (c === 10) {
+                    stdout(
+                        Module._textDecoder.decode(
+                            new Uint8Array(Module._stdoutBuffer),
+                        ),
+                    );
+                    Module._stdoutBuffer = [];
+                } else {
+                    Module._stdoutBuffer.push(c);
+                }
+            };
+        } else {
+            Module.stdout = (c) => stdout(new Uint8Array([c]));
+        }
+    }
+    if (stderr !== undefined) {
+        if (linebuffer) {
+            Module._stderrBuffer = [];
+            Module.stderr = (c) => {
+                if (c === 10) {
+                    stderr(
+                        Module._textDecoder.decode(
+                            new Uint8Array(Module._stderrBuffer),
+                        ),
+                    );
+                    Module._stderrBuffer = [];
+                } else {
+                    Module._stderrBuffer.push(c);
+                }
+            };
+        } else {
+            Module.stderr = (c) => stderr(new Uint8Array([c]));
+        }
+    }
+    Module = await _createMicroPythonModule(Module);
+    globalThis.Module = Module;
+    proxy_js_init();
+    const pyimport = (name) => {
+        const value = Module._malloc(3 * 4);
+        Module.ccall(
+            "mp_js_do_import",
+            "null",
+            ["string", "pointer"],
+            [name, value],
+        );
+        return proxy_convert_mp_to_js_obj_jsside_with_free(value);
+    };
+    Module.ccall(
+        "mp_js_init",
+        "null",
+        ["number", "number"],
+        [pystack, heapsize],
+    );
+    Module.ccall("proxy_c_init", "null", [], []);
+    return {
+        _module: Module,
+        PyProxy: PyProxy,
+        FS: Module.FS,
+        globals: {
+            __dict__: pyimport("__main__").__dict__,
+            get(key) {
+                return this.__dict__[key];
+            },
+            set(key, value) {
+                this.__dict__[key] = value;
+            },
+            delete(key) {
+                delete this.__dict__[key];
+            },
+        },
+        registerJsModule(name, module) {
+            const value = Module._malloc(3 * 4);
+            proxy_convert_js_to_mp_obj_jsside(module, value);
+            Module.ccall(
+                "mp_js_register_js_module",
+                "null",
+                ["string", "pointer"],
+                [name, value],
+            );
+            Module._free(value);
+        },
+        pyimport: pyimport,
+        runPython(code) {
+            const len = Module.lengthBytesUTF8(code);
+            const buf = Module._malloc(len + 1);
+            Module.stringToUTF8(code, buf, len + 1);
+            const value = Module._malloc(3 * 4);
+            Module.ccall(
+                "mp_js_do_exec",
+                "number",
+                ["pointer", "number", "pointer"],
+                [buf, len, value],
+            );
+            Module._free(buf);
+            return proxy_convert_mp_to_js_obj_jsside_with_free(value);
+        },
+        runPythonAsync(code) {
+            const len = Module.lengthBytesUTF8(code);
+            const buf = Module._malloc(len + 1);
+            Module.stringToUTF8(code, buf, len + 1);
+            const value = Module._malloc(3 * 4);
+            Module.ccall(
+                "mp_js_do_exec_async",
+                "number",
+                ["pointer", "number", "pointer"],
+                [buf, len, value],
+            );
+            Module._free(buf);
+            const ret = proxy_convert_mp_to_js_obj_jsside_with_free(value);
+            if (ret instanceof PyProxyThenable) {
+                return Promise.resolve(ret);
+            }
+            return ret;
+        },
+        replInit() {
+            Module.ccall("mp_js_repl_init", "null", ["null"]);
+        },
+        replProcessChar(chr) {
+            return Module.ccall(
+                "mp_js_repl_process_char",
+                "number",
+                ["number"],
+                [chr],
+            );
+        },
+        // Needed if the GC/asyncify is enabled.
+        async replProcessCharWithAsyncify(chr) {
+            return Module.ccall(
+                "mp_js_repl_process_char",
+                "number",
+                ["number"],
+                [chr],
+                { async: true },
+            );
+        },
+    };
+}
+
+globalThis.loadMicroPython = loadMicroPython;
+
+async function runCLI() {
+    const fs = await import("fs");
+    let heap_size = 128 * 1024;
+    let contents = "";
+    let repl = true;
+
+    for (let i = 2; i < process.argv.length; i++) {
+        if (process.argv[i] === "-X" && i < process.argv.length - 1) {
+            if (process.argv[i + 1].includes("heapsize=")) {
+                heap_size = parseInt(process.argv[i + 1].split("heapsize=")[1]);
+                const suffix = process.argv[i + 1].substr(-1).toLowerCase();
+                if (suffix === "k") {
+                    heap_size *= 1024;
+                } else if (suffix === "m") {
+                    heap_size *= 1024 * 1024;
+                }
+                ++i;
+            }
+        } else {
+            contents += fs.readFileSync(process.argv[i], "utf8");
+            repl = false;
+        }
+    }
+
+    if (process.stdin.isTTY === false) {
+        contents = fs.readFileSync(0, "utf8");
+        repl = false;
+    }
+
+    const mp = await loadMicroPython({
+        heapsize: heap_size,
+        stdout: (data) => process.stdout.write(data),
+        linebuffer: false,
+    });
+
+    if (repl) {
+        mp.replInit();
+        process.stdin.setRawMode(true);
+        process.stdin.on("data", (data) => {
+            for (let i = 0; i < data.length; i++) {
+                mp.replProcessCharWithAsyncify(data[i]).then((result) => {
+                    if (result) {
+                        process.exit();
+                    }
+                });
+            }
+        });
+    } else {
+        // If the script to run ends with a running of the asyncio main loop, then inject
+        // a simple `asyncio.run` hook that starts the main task.  This is primarily to
+        // support running the standard asyncio tests.
+        if (contents.endsWith("asyncio.run(main())\n")) {
+            const asyncio = mp.pyimport("asyncio");
+            asyncio.run = async (task) => {
+                await asyncio.create_task(task);
+            };
+        }
+
+        try {
+            mp.runPython(contents);
+        } catch (error) {
+            if (error.name === "PythonError") {
+                if (error.type === "SystemExit") {
+                    // SystemExit, this is a valid exception to successfully end a script.
+                } else {
+                    // An unhandled Python exception, print in out.
+                    console.error(error.message);
+                }
+            } else {
+                // A non-Python exception.  Re-raise it.
+                throw error;
+            }
+        }
+    }
+}
+
+// Check if Node is running (equivalent to ENVIRONMENT_IS_NODE).
+if (
+    typeof process === "object" &&
+    typeof process.versions === "object" &&
+    typeof process.versions.node === "string"
+) {
+    // Check if this module is run from the command line via `node micropython.mjs`.
+    //
+    // See https://stackoverflow.com/questions/6398196/detect-if-called-through-require-or-directly-by-command-line/66309132#66309132
+    //
+    // Note:
+    // - `resolve()` is used to handle symlinks
+    // - `includes()` is used to handle cases where the file extension was omitted when passed to node
+
+    if (process.argv.length > 1) {
+        const path = await import("path");
+        const url = await import("url");
+
+        const pathToThisFile = path.resolve(url.fileURLToPath(import.meta.url));
+        const pathPassedToNode = path.resolve(process.argv[1]);
+        const isThisFileBeingRunViaCLI =
+            pathToThisFile.includes(pathPassedToNode);
+
+        if (isThisFileBeingRunViaCLI) {
+            runCLI();
+        }
+    }
+}
diff --git a/ports/webassembly/asyncio/__init__.py b/ports/webassembly/asyncio/__init__.py
new file mode 100644
index 00000000000..ba1ca635148
--- /dev/null
+++ b/ports/webassembly/asyncio/__init__.py
@@ -0,0 +1,9 @@
+# MicroPython asyncio module, for use with webassembly port
+# MIT license; Copyright (c) 2024 Damien P. George
+
+from .core import *
+from .funcs import wait_for, wait_for_ms, gather
+from .event import Event
+from .lock import Lock
+
+__version__ = (3, 0, 0)
diff --git a/ports/webassembly/asyncio/core.py b/ports/webassembly/asyncio/core.py
new file mode 100644
index 00000000000..eeffa253bcf
--- /dev/null
+++ b/ports/webassembly/asyncio/core.py
@@ -0,0 +1,267 @@
+# MicroPython asyncio module, for use with webassembly port
+# MIT license; Copyright (c) 2019-2024 Damien P. George
+
+from time import ticks_ms as ticks, ticks_diff, ticks_add
+import sys, js, jsffi
+
+# Import TaskQueue and Task from built-in C code.
+from _asyncio import TaskQueue, Task
+
+
+################################################################################
+# Exceptions
+
+
+class CancelledError(BaseException):
+    pass
+
+
+class TimeoutError(Exception):
+    pass
+
+
+# Used when calling Loop.call_exception_handler.
+_exc_context = {"message": "Task exception wasn't retrieved", "exception": None, "future": None}
+
+
+################################################################################
+# Sleep functions
+
+
+# "Yield" once, then raise StopIteration
+class SingletonGenerator:
+    def __init__(self):
+        self.state = None
+        self.exc = StopIteration()
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        if self.state is not None:
+            _task_queue.push(cur_task, self.state)
+            self.state = None
+            return None
+        else:
+            self.exc.__traceback__ = None
+            raise self.exc
+
+
+# Pause task execution for the given time (integer in milliseconds, MicroPython extension)
+# Use a SingletonGenerator to do it without allocating on the heap
+def sleep_ms(t, sgen=SingletonGenerator()):
+    assert sgen.state is None
+    sgen.state = ticks_add(ticks(), max(0, t))
+    return sgen
+
+
+# Pause task execution for the given time (in seconds)
+def sleep(t):
+    return sleep_ms(int(t * 1000))
+
+
+################################################################################
+# Main run loop
+
+asyncio_timer = None
+
+
+class TopLevelCoro:
+    @staticmethod
+    def set(resolve, reject):
+        TopLevelCoro.resolve = resolve
+        TopLevelCoro.reject = reject
+
+    @staticmethod
+    def send(value):
+        TopLevelCoro.resolve()
+
+
+class ThenableEvent:
+    def __init__(self, thenable):
+        self.waiting = None  # Task waiting on completion of this thenable
+        thenable.then(self.set, self.cancel)
+
+    def set(self, value=None):
+        # Thenable/Promise is fulfilled, set result and schedule any waiting task.
+        self.result = value
+        if self.waiting:
+            _task_queue.push(self.waiting)
+            self.waiting = None
+
+    def cancel(self, value=None):
+        # Thenable/Promise is rejected, set error and schedule any waiting task.
+        self.error = jsffi.JsException(
+            value, getattr(value, "name", None), getattr(value, "message", None)
+        )
+        if self.waiting:
+            _task_queue.push(self.waiting)
+            self.waiting = None
+
+    def remove(self, task):
+        self.waiting = None
+
+    # async
+    def wait(self):
+        # Set the calling task as the task waiting on this thenable.
+        self.waiting = cur_task
+        # Set calling task's data to this object so it can be removed if needed.
+        cur_task.data = self
+        # Wait for the thenable to fulfill.
+        yield
+        # Raise the error, or return the result, of the thenable.
+        if hasattr(self, "error"):
+            raise self.error
+        return self.result
+
+
+# Ensure the awaitable is a task
+def _promote_to_task(aw):
+    return aw if isinstance(aw, Task) else create_task(aw)
+
+
+def _schedule_run_iter(dt):
+    global asyncio_timer
+    if asyncio_timer is not None:
+        js.clearTimeout(asyncio_timer)
+    asyncio_timer = js.setTimeout(_run_iter, dt)
+
+
+def _run_iter():
+    global cur_task
+    excs_all = (CancelledError, Exception)  # To prevent heap allocation in loop
+    excs_stop = (CancelledError, StopIteration)  # To prevent heap allocation in loop
+    while True:
+        # Wait until the head of _task_queue is ready to run
+        t = _task_queue.peek()
+        if t:
+            # A task waiting on _task_queue; "ph_key" is time to schedule task at
+            dt = max(0, ticks_diff(t.ph_key, ticks()))
+        else:
+            # No tasks can be woken so finished running
+            cur_task = _top_level_task
+            return
+
+        if dt > 0:
+            # schedule to call again later
+            cur_task = _top_level_task
+            _schedule_run_iter(dt)
+            return
+
+        # Get next task to run and continue it
+        t = _task_queue.pop()
+        cur_task = t
+        try:
+            # Continue running the coroutine, it's responsible for rescheduling itself
+            exc = t.data
+            if not exc:
+                t.coro.send(None)
+            else:
+                # If the task is finished and on the run queue and gets here, then it
+                # had an exception and was not await'ed on.  Throwing into it now will
+                # raise StopIteration and the code below will catch this and run the
+                # call_exception_handler function.
+                t.data = None
+                t.coro.throw(exc)
+        except excs_all as er:
+            # Check the task is not on any event queue
+            assert t.data is None
+            # This task is done.
+            if t.state:
+                # Task was running but is now finished.
+                waiting = False
+                if t.state is True:
+                    # "None" indicates that the task is complete and not await'ed on (yet).
+                    t.state = None
+                elif callable(t.state):
+                    # The task has a callback registered to be called on completion.
+                    t.state(t, er)
+                    t.state = False
+                    waiting = True
+                else:
+                    # Schedule any other tasks waiting on the completion of this task.
+                    while t.state.peek():
+                        _task_queue.push(t.state.pop())
+                        waiting = True
+                    # "False" indicates that the task is complete and has been await'ed on.
+                    t.state = False
+                if not waiting and not isinstance(er, excs_stop):
+                    # An exception ended this detached task, so queue it for later
+                    # execution to handle the uncaught exception if no other task retrieves
+                    # the exception in the meantime (this is handled by Task.throw).
+                    _task_queue.push(t)
+                # Save return value of coro to pass up to caller.
+                t.data = er
+            elif t.state is None:
+                # Task is already finished and nothing await'ed on the task,
+                # so call the exception handler.
+
+                # Save exception raised by the coro for later use.
+                t.data = exc
+
+                # Create exception context and call the exception handler.
+                _exc_context["exception"] = exc
+                _exc_context["future"] = t
+                Loop.call_exception_handler(_exc_context)
+
+
+# Create and schedule a new task from a coroutine.
+def create_task(coro):
+    if not hasattr(coro, "send"):
+        raise TypeError("coroutine expected")
+    t = Task(coro, globals())
+    _task_queue.push(t)
+    return t
+
+
+# Task used to suspend and resume top-level await.
+_top_level_task = Task(TopLevelCoro, globals())
+
+################################################################################
+# Event loop wrapper
+
+
+cur_task = _top_level_task
+
+
+class Loop:
+    _exc_handler = None
+
+    def create_task(coro):
+        return create_task(coro)
+
+    def close():
+        pass
+
+    def set_exception_handler(handler):
+        Loop._exc_handler = handler
+
+    def get_exception_handler():
+        return Loop._exc_handler
+
+    def default_exception_handler(loop, context):
+        print(context["message"], file=sys.stderr)
+        print("future:", context["future"], "coro=", context["future"].coro, file=sys.stderr)
+        sys.print_exception(context["exception"], sys.stderr)
+
+    def call_exception_handler(context):
+        (Loop._exc_handler or Loop.default_exception_handler)(Loop, context)
+
+
+def get_event_loop():
+    return Loop
+
+
+def current_task():
+    assert cur_task is not None
+    return cur_task
+
+
+def new_event_loop():
+    global _task_queue
+    _task_queue = TaskQueue(_schedule_run_iter)  # TaskQueue of Task instances.
+    return Loop
+
+
+# Initialise default event loop.
+new_event_loop()
diff --git a/ports/webassembly/lexer_dedent.c b/ports/webassembly/lexer_dedent.c
new file mode 100644
index 00000000000..555caea8969
--- /dev/null
+++ b/ports/webassembly/lexer_dedent.c
@@ -0,0 +1,105 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "lexer_dedent.h"
+
+typedef struct _mp_reader_mem_dedent_t {
+    size_t free_len; // if >0 mem is freed on close by: m_free(beg, free_len)
+    const byte *beg;
+    const byte *cur;
+    const byte *end;
+    size_t dedent_prefix;
+} mp_reader_mem_dedent_t;
+
+// Work out the amount of common whitespace among all non-empty lines.
+static size_t dedent(const byte *text, size_t len) {
+    size_t min_prefix = -1;
+    size_t cur_prefix = 0;
+    bool start_of_line = true;
+    for (const byte *t = text; t < text + len; ++t) {
+        if (*t == '\n') {
+            start_of_line = true;
+            cur_prefix = 0;
+        } else if (start_of_line) {
+            if (unichar_isspace(*t)) {
+                ++cur_prefix;
+            } else {
+                if (cur_prefix < min_prefix) {
+                    min_prefix = cur_prefix;
+                    if (min_prefix == 0) {
+                        return min_prefix;
+                    }
+                }
+                start_of_line = false;
+            }
+        }
+    }
+    return min_prefix;
+}
+
+static mp_uint_t mp_reader_mem_dedent_readbyte(void *data) {
+    mp_reader_mem_dedent_t *reader = (mp_reader_mem_dedent_t *)data;
+    if (reader->cur < reader->end) {
+        byte c = *reader->cur++;
+        if (c == '\n') {
+            for (size_t i = 0; i < reader->dedent_prefix; ++i) {
+                if (*reader->cur == '\n') {
+                    break;
+                }
+                ++reader->cur;
+            }
+        }
+        return c;
+    } else {
+        return MP_READER_EOF;
+    }
+}
+
+static void mp_reader_mem_dedent_close(void *data) {
+    mp_reader_mem_dedent_t *reader = (mp_reader_mem_dedent_t *)data;
+    if (reader->free_len > 0) {
+        m_del(char, (char *)reader->beg, reader->free_len);
+    }
+    m_del_obj(mp_reader_mem_dedent_t, reader);
+}
+
+static void mp_reader_new_mem_dedent(mp_reader_t *reader, const byte *buf, size_t len, size_t free_len) {
+    mp_reader_mem_dedent_t *rm = m_new_obj(mp_reader_mem_dedent_t);
+    rm->free_len = free_len;
+    rm->beg = buf;
+    rm->cur = buf;
+    rm->end = buf + len;
+    rm->dedent_prefix = dedent(buf, len);
+    reader->data = rm;
+    reader->readbyte = mp_reader_mem_dedent_readbyte;
+    reader->close = mp_reader_mem_dedent_close;
+}
+
+mp_lexer_t *mp_lexer_new_from_str_len_dedent(qstr src_name, const char *str, size_t len, size_t free_len) {
+    mp_reader_t reader;
+    mp_reader_new_mem_dedent(&reader, (const byte *)str, len, free_len);
+    return mp_lexer_new(src_name, reader);
+}
diff --git a/ports/webassembly/lexer_dedent.h b/ports/webassembly/lexer_dedent.h
new file mode 100644
index 00000000000..a8cc2526b4f
--- /dev/null
+++ b/ports/webassembly/lexer_dedent.h
@@ -0,0 +1,36 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+#ifndef MICROPY_INCLUDED_WEBASSEMBLY_LEXER_DEDENT_H
+#define MICROPY_INCLUDED_WEBASSEMBLY_LEXER_DEDENT_H
+
+#include "py/lexer.h"
+
+// This function creates a new "dedenting lexer" which automatically dedents the input
+// source code if every non-empty line in that source starts with a common whitespace
+// prefix.  It does this dedenting inplace as the memory is read.
+mp_lexer_t *mp_lexer_new_from_str_len_dedent(qstr src_name, const char *str, size_t len, size_t free_len);
+
+#endif // MICROPY_INCLUDED_WEBASSEMBLY_LEXER_DEDENT_H
diff --git a/ports/webassembly/library.h b/ports/webassembly/library.h
index 47982e06400..04b408d71a2 100644
--- a/ports/webassembly/library.h
+++ b/ports/webassembly/library.h
@@ -29,3 +29,5 @@
 extern void mp_js_write(const char *str, mp_uint_t len);
 extern int mp_js_ticks_ms(void);
 extern void mp_js_hook(void);
+extern double mp_js_time_ms(void);
+extern uint32_t mp_js_random_u32(void);
diff --git a/ports/webassembly/library.js b/ports/webassembly/library.js
index fc5b542488a..6fcd9f2e254 100644
--- a/ports/webassembly/library.js
+++ b/ports/webassembly/library.js
@@ -25,41 +25,51 @@
  */
 
 mergeInto(LibraryManager.library, {
-    mp_js_write: function(ptr, len) {
-        const buffer = HEAPU8.subarray(ptr, ptr + len)
-        if (typeof window === 'undefined') {
-            process.stdout.write(buffer);
-        } else {
-            const printEvent = new CustomEvent('micropython-print', { detail: buffer });
-            document.dispatchEvent(printEvent);
-        }
-    },
+    // This string will be emitted directly into the output file by Emscripten.
+    mp_js_ticks_ms__postset: "var MP_JS_EPOCH = Date.now()",
 
-    mp_js_ticks_ms: function() {
-        return Date.now() - MP_JS_EPOCH;
-    },
+    mp_js_ticks_ms: () => Date.now() - MP_JS_EPOCH,
 
-    mp_js_hook: function() {
-        if (typeof window === 'undefined') {
-            var mp_interrupt_char = Module.ccall('mp_hal_get_interrupt_char', 'number', ['number'], ['null']);
-            var fs = require('fs');
+    mp_js_hook: () => {
+        if (ENVIRONMENT_IS_NODE) {
+            const mp_interrupt_char = Module.ccall(
+                "mp_hal_get_interrupt_char",
+                "number",
+                [],
+                [],
+            );
+            const fs = require("fs");
 
-            var buf = Buffer.alloc(1);
+            const buf = Buffer.alloc(1);
             try {
-                var n = fs.readSync(process.stdin.fd, buf, 0, 1);
+                const n = fs.readSync(process.stdin.fd, buf, 0, 1);
                 if (n > 0) {
-                    if (buf[0] == mp_interrupt_char) {
-                        Module.ccall('mp_sched_keyboard_interrupt', 'null', ['null'], ['null']);
+                    if (buf[0] === mp_interrupt_char) {
+                        Module.ccall(
+                            "mp_sched_keyboard_interrupt",
+                            "null",
+                            ["null"],
+                            ["null"],
+                        );
                     } else {
                         process.stdout.write(String.fromCharCode(buf[0]));
                     }
                 }
             } catch (e) {
-                if (e.code === 'EAGAIN') {
+                if (e.code === "EAGAIN") {
                 } else {
                     throw e;
                 }
             }
         }
     },
+
+    mp_js_time_ms: () => Date.now(),
+
+    // Node prior to v19 did not expose "crypto" as a global, so make sure it exists.
+    mp_js_random_u32__postset:
+        "if (globalThis.crypto === undefined) { globalThis.crypto = require('crypto'); }",
+
+    mp_js_random_u32: () =>
+        globalThis.crypto.getRandomValues(new Uint32Array(1))[0],
 });
diff --git a/ports/webassembly/main.c b/ports/webassembly/main.c
index e6273282608..670d404da06 100644
--- a/ports/webassembly/main.c
+++ b/ports/webassembly/main.c
@@ -40,57 +40,58 @@
 #include "shared/runtime/pyexec.h"
 
 #include "emscripten.h"
+#include "lexer_dedent.h"
 #include "library.h"
+#include "proxy_c.h"
 
-#if MICROPY_ENABLE_COMPILER
-int do_str(const char *src, mp_parse_input_kind_t input_kind) {
-    int ret = 0;
-    nlr_buf_t nlr;
-    if (nlr_push(&nlr) == 0) {
-        mp_lexer_t *lex = mp_lexer_new_from_str_len(MP_QSTR__lt_stdin_gt_, src, strlen(src), 0);
-        qstr source_name = lex->source_name;
-        mp_parse_tree_t parse_tree = mp_parse(lex, input_kind);
-        mp_obj_t module_fun = mp_compile(&parse_tree, source_name, false);
-        mp_call_function_0(module_fun);
-        nlr_pop();
-    } else {
-        // uncaught exception
-        if (mp_obj_is_subclass_fast(mp_obj_get_type((mp_obj_t)nlr.ret_val), &mp_type_SystemExit)) {
-            mp_obj_t exit_val = mp_obj_exception_get_value(MP_OBJ_FROM_PTR(nlr.ret_val));
-            if (exit_val != mp_const_none) {
-                mp_int_t int_val;
-                if (mp_obj_get_int_maybe(exit_val, &int_val)) {
-                    ret = int_val & 255;
-                } else {
-                    ret = 1;
-                }
-            }
-        } else {
-            mp_obj_print_exception(&mp_plat_print, (mp_obj_t)nlr.ret_val);
-            ret = 1;
-        }
-    }
-    return ret;
-}
+// This counter tracks the current depth of calls into C code that originated
+// externally, ie from JavaScript.  When the counter is 0 that corresponds to
+// the top-level call into C.
+#if MICROPY_GC_SPLIT_HEAP_AUTO
+static size_t external_call_depth = 0;
+#endif
+
+// Emscripten defaults to a 64k C-stack, so our limit should be less than that.
+#define CSTACK_SIZE (32 * 1024)
+
+#if MICROPY_GC_SPLIT_HEAP_AUTO
+static void gc_collect_top_level(mp_obj_t root_obj);
 #endif
 
-int mp_js_do_str(const char *code) {
-    return do_str(code, MP_PARSE_FILE_INPUT);
+void external_call_depth_inc(void) {
+    #if MICROPY_GC_SPLIT_HEAP_AUTO
+    ++external_call_depth;
+    #endif
 }
 
-int mp_js_process_char(int c) {
-    return pyexec_event_repl_process_char(c);
+void external_call_depth_dec(mp_obj_t root_obj) {
+    #if MICROPY_GC_SPLIT_HEAP_AUTO
+    --external_call_depth;
+    if (external_call_depth == 0) {
+        gc_collect_top_level(root_obj);
+    }
+    #endif
 }
 
-void mp_js_init(int heap_size) {
+void mp_js_init(int pystack_size, int heap_size) {
+    mp_cstack_init_with_sp_here(CSTACK_SIZE);
+
+    #if MICROPY_ENABLE_PYSTACK
+    mp_obj_t *pystack = (mp_obj_t *)malloc(pystack_size * sizeof(mp_obj_t));
+    mp_pystack_init(pystack, pystack + pystack_size);
+    #endif
+
     #if MICROPY_ENABLE_GC
     char *heap = (char *)malloc(heap_size * sizeof(char));
     gc_init(heap, heap + heap_size);
     #endif
 
-    #if MICROPY_ENABLE_PYSTACK
-    static mp_obj_t pystack[1024];
-    mp_pystack_init(pystack, &pystack[MP_ARRAY_SIZE(pystack)]);
+    #if MICROPY_GC_SPLIT_HEAP_AUTO
+    // When MICROPY_GC_SPLIT_HEAP_AUTO is enabled, set the GC threshold to a low
+    // value so that a collection is triggered before the heap fills up.  The actual
+    // garbage collection will happen later when control returns to the top-level,
+    // via the `gc_collect_pending` flag and `gc_collect_top_level()`.
+    MP_STATE_MEM(gc_alloc_threshold) = 16 * 1024 / MICROPY_BYTES_PER_GC_BLOCK;
     #endif
 
     mp_init();
@@ -105,14 +106,110 @@ void mp_js_init(int heap_size) {
         mp_vfs_mount(2, args, (mp_map_t *)&mp_const_empty_map);
         MP_STATE_VM(vfs_cur) = MP_STATE_VM(vfs_mount_table);
     }
+    mp_obj_list_append(mp_sys_path, MP_OBJ_NEW_QSTR(MP_QSTR__slash_lib));
     #endif
 }
 
-void mp_js_init_repl() {
+void mp_js_register_js_module(const char *name, uint32_t *value) {
+    mp_obj_t module_name = MP_OBJ_NEW_QSTR(qstr_from_str(name));
+    mp_obj_t module = proxy_convert_js_to_mp_obj_cside(value);
+    mp_map_t *mp_loaded_modules_map = &MP_STATE_VM(mp_loaded_modules_dict).map;
+    mp_map_lookup(mp_loaded_modules_map, module_name, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND)->value = module;
+}
+
+void mp_js_do_import(const char *name, uint32_t *out) {
+    external_call_depth_inc();
+    nlr_buf_t nlr;
+    if (nlr_push(&nlr) == 0) {
+        mp_obj_t ret = mp_import_name(qstr_from_str(name), mp_const_none, MP_OBJ_NEW_SMALL_INT(0));
+        // Return the leaf of the import, eg for "a.b.c" return "c".
+        const char *m = name;
+        const char *n = name;
+        for (;; ++n) {
+            if (*n == '\0' || *n == '.') {
+                if (m != name) {
+                    ret = mp_load_attr(ret, qstr_from_strn(m, n - m));
+                }
+                m = n + 1;
+                if (*n == '\0') {
+                    break;
+                }
+            }
+        }
+        nlr_pop();
+        proxy_convert_mp_to_js_obj_cside(ret, out);
+        external_call_depth_dec(ret);
+    } else {
+        // uncaught exception
+        proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out);
+        external_call_depth_dec(nlr.ret_val);
+    }
+}
+
+void mp_js_do_exec(const char *src, size_t len, uint32_t *out) {
+    external_call_depth_inc();
+    mp_parse_input_kind_t input_kind = MP_PARSE_FILE_INPUT;
+    nlr_buf_t nlr;
+    if (nlr_push(&nlr) == 0) {
+        mp_lexer_t *lex = mp_lexer_new_from_str_len_dedent(MP_QSTR__lt_stdin_gt_, src, len, 0);
+        qstr source_name = lex->source_name;
+        mp_parse_tree_t parse_tree = mp_parse(lex, input_kind);
+        mp_obj_t module_fun = mp_compile(&parse_tree, source_name, false);
+        mp_obj_t ret = mp_call_function_0(module_fun);
+        nlr_pop();
+        proxy_convert_mp_to_js_obj_cside(ret, out);
+        external_call_depth_dec(ret);
+    } else {
+        // uncaught exception
+        proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out);
+        external_call_depth_dec(nlr.ret_val);
+    }
+}
+
+void mp_js_do_exec_async(const char *src, size_t len, uint32_t *out) {
+    mp_compile_allow_top_level_await = true;
+    mp_js_do_exec(src, len, out);
+    mp_compile_allow_top_level_await = false;
+}
+
+void mp_js_repl_init(void) {
     pyexec_event_repl_init();
 }
 
-STATIC void gc_scan_func(void *begin, void *end) {
+int mp_js_repl_process_char(int c) {
+    external_call_depth_inc();
+    int ret = pyexec_event_repl_process_char(c);
+    external_call_depth_dec(MP_OBJ_NULL);
+    return ret;
+}
+
+#if MICROPY_GC_SPLIT_HEAP_AUTO
+
+static bool gc_collect_pending = false;
+
+// The largest new region that is available to become Python heap.
+size_t gc_get_max_new_split(void) {
+    return 128 * 1024 * 1024;
+}
+
+// Don't collect anything.  Instead require the heap to grow.
+void gc_collect(void) {
+    gc_collect_pending = true;
+}
+
+// Collect at the top-level, where there are no root pointers from stack/registers.
+static void gc_collect_top_level(mp_obj_t root_obj) {
+    if (gc_collect_pending) {
+        gc_collect_pending = false;
+        gc_collect_start();
+        gc_collect_root(&root_obj, 1);
+        gc_collect_end();
+    }
+}
+
+#else
+
+static void gc_scan_func(void *begin, void *end) {
     gc_collect_root((void **)begin, (void **)end - (void **)begin + 1);
 }
 
@@ -123,8 +220,10 @@ void gc_collect(void) {
     gc_collect_end();
 }
 
+#endif
+
 #if !MICROPY_VFS
-mp_lexer_t *mp_lexer_new_from_file(const char *filename) {
+mp_lexer_t *mp_lexer_new_from_file(qstr filename) {
     mp_raise_OSError(MP_ENOENT);
 }
 
@@ -144,7 +243,7 @@ void nlr_jump_fail(void *val) {
     }
 }
 
-void NORETURN __fatal_error(const char *msg) {
+void MP_NORETURN __fatal_error(const char *msg) {
     while (1) {
         ;
     }
diff --git a/ports/webassembly/modjs.c b/ports/webassembly/modjs.c
new file mode 100644
index 00000000000..2f91a012f0f
--- /dev/null
+++ b/ports/webassembly/modjs.c
@@ -0,0 +1,53 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "py/objmodule.h"
+#include "py/runtime.h"
+#include "proxy_c.h"
+
+#if MICROPY_PY_JS
+
+/******************************************************************************/
+// js module
+
+void mp_module_js_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    mp_obj_jsproxy_global_this_attr(attr, dest);
+}
+
+static const mp_rom_map_elem_t mp_module_js_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_js) },
+};
+static MP_DEFINE_CONST_DICT(mp_module_js_globals, mp_module_js_globals_table);
+
+const mp_obj_module_t mp_module_js = {
+    .base = { &mp_type_module },
+    .globals = (mp_obj_dict_t *)&mp_module_js_globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_js, mp_module_js);
+MP_REGISTER_MODULE_DELEGATION(mp_module_js, mp_module_js_attr);
+
+#endif // MICROPY_PY_JS
diff --git a/ports/webassembly/modjsffi.c b/ports/webassembly/modjsffi.c
new file mode 100644
index 00000000000..ac3d8602352
--- /dev/null
+++ b/ports/webassembly/modjsffi.c
@@ -0,0 +1,121 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "emscripten.h"
+#include "py/gc.h"
+#include "py/objmodule.h"
+#include "py/runtime.h"
+#include "proxy_c.h"
+
+#if MICROPY_PY_JSFFI
+
+/******************************************************************************/
+// jsffi module
+
+EM_JS(void, proxy_convert_mp_to_js_then_js_to_mp_obj_jsside, (uint32_t * out), {
+    const ret = proxy_convert_mp_to_js_obj_jsside(out);
+    proxy_convert_js_to_mp_obj_jsside_force_double_proxy(ret, out);
+});
+
+static mp_obj_t mp_jsffi_create_proxy(mp_obj_t arg) {
+    uint32_t out[3];
+    proxy_convert_mp_to_js_obj_cside(arg, out);
+    proxy_convert_mp_to_js_then_js_to_mp_obj_jsside(out);
+    return proxy_convert_js_to_mp_obj_cside(out);
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_jsffi_create_proxy_obj, mp_jsffi_create_proxy);
+
+EM_JS(void, proxy_convert_mp_to_js_then_js_to_js_then_js_to_mp_obj_jsside, (uint32_t * out), {
+    const ret = proxy_convert_mp_to_js_obj_jsside(out);
+    const js_obj = PyProxy.toJs(ret);
+    proxy_convert_js_to_mp_obj_jsside(js_obj, out);
+});
+
+static mp_obj_t mp_jsffi_to_js(mp_obj_t arg) {
+    uint32_t out[3];
+    proxy_convert_mp_to_js_obj_cside(arg, out);
+    proxy_convert_mp_to_js_then_js_to_js_then_js_to_mp_obj_jsside(out);
+    return proxy_convert_js_to_mp_obj_cside(out);
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_jsffi_to_js_obj, mp_jsffi_to_js);
+
+// *FORMAT-OFF*
+EM_JS(void, js_get_proxy_js_ref_info, (uint32_t * out), {
+    let used = 0;
+    for (const elem of proxy_js_ref) {
+        if (elem !== undefined) {
+            ++used;
+        }
+    }
+    Module.setValue(out, proxy_js_ref.length, "i32");
+    Module.setValue(out + 4, used, "i32");
+});
+// *FORMAT-ON*
+
+static mp_obj_t mp_jsffi_mem_info(void) {
+    mp_obj_list_t *l = (mp_obj_list_t *)MP_OBJ_TO_PTR(MP_STATE_PORT(proxy_c_ref));
+    mp_int_t used = 0;
+    for (size_t i = 0; i < l->len; ++i) {
+        if (l->items[i] != MP_OBJ_NULL) {
+            ++used;
+        }
+    }
+    uint32_t proxy_js_ref_info[2];
+    js_get_proxy_js_ref_info(proxy_js_ref_info);
+    gc_info_t info;
+    gc_info(&info);
+    mp_obj_t elems[] = {
+        MP_OBJ_NEW_SMALL_INT(info.total), // GC heap total bytes
+        MP_OBJ_NEW_SMALL_INT(info.used), // GC heap used bytes
+        MP_OBJ_NEW_SMALL_INT(info.free), // GC heap free bytes
+        MP_OBJ_NEW_SMALL_INT(l->len), // proxy_c_ref allocated size
+        MP_OBJ_NEW_SMALL_INT(used), // proxy_c_ref used
+        MP_OBJ_NEW_SMALL_INT(proxy_js_ref_info[0]), // proxy_js_ref allocated size
+        MP_OBJ_NEW_SMALL_INT(proxy_js_ref_info[1]), // proxy_js_ref used
+    };
+    return mp_obj_new_tuple(MP_ARRAY_SIZE(elems), elems);
+}
+static MP_DEFINE_CONST_FUN_OBJ_0(mp_jsffi_mem_info_obj, mp_jsffi_mem_info);
+
+static const mp_rom_map_elem_t mp_module_jsffi_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_jsffi) },
+
+    { MP_ROM_QSTR(MP_QSTR_JsProxy), MP_ROM_PTR(&mp_type_jsproxy) },
+    { MP_ROM_QSTR(MP_QSTR_JsException), MP_ROM_PTR(&mp_type_JsException) },
+    { MP_ROM_QSTR(MP_QSTR_create_proxy), MP_ROM_PTR(&mp_jsffi_create_proxy_obj) },
+    { MP_ROM_QSTR(MP_QSTR_to_js), MP_ROM_PTR(&mp_jsffi_to_js_obj) },
+    { MP_ROM_QSTR(MP_QSTR_mem_info), MP_ROM_PTR(&mp_jsffi_mem_info_obj) },
+};
+static MP_DEFINE_CONST_DICT(mp_module_jsffi_globals, mp_module_jsffi_globals_table);
+
+const mp_obj_module_t mp_module_jsffi = {
+    .base = { &mp_type_module },
+    .globals = (mp_obj_dict_t *)&mp_module_jsffi_globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_jsffi, mp_module_jsffi);
+
+#endif // MICROPY_PY_JSFFI
diff --git a/ports/webassembly/modtime.c b/ports/webassembly/modtime.c
new file mode 100644
index 00000000000..b6c0cda96da
--- /dev/null
+++ b/ports/webassembly/modtime.c
@@ -0,0 +1,39 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "py/obj.h"
+#include "shared/timeutils/timeutils.h"
+#include "library.h"
+
+// Get the localtime.
+static void mp_time_localtime_get(timeutils_struct_time_t *tm) {
+    timeutils_seconds_since_epoch_to_struct_time(mp_hal_time_ms() / 1000, tm);
+}
+
+// Returns the number of seconds, as a float, since the Epoch.
+static mp_obj_t mp_time_time_get(void) {
+    return mp_obj_new_float((mp_float_t)mp_hal_time_ms() / 1000);
+}
diff --git a/ports/webassembly/mpconfigport.h b/ports/webassembly/mpconfigport.h
index e0d1af0cce2..b4aad22dbf7 100644
--- a/ports/webassembly/mpconfigport.h
+++ b/ports/webassembly/mpconfigport.h
@@ -25,43 +25,62 @@
  * THE SOFTWARE.
  */
 
+// Options to control how MicroPython is built for this port, overriding
+// defaults in py/mpconfig.h.
+
 #include 
+#include  // for malloc, for MICROPY_GC_SPLIT_HEAP_AUTO
 
-// options to control how MicroPython is built
+// Variant-specific definitions.
+#include "mpconfigvariant.h"
 
+#ifndef MICROPY_CONFIG_ROM_LEVEL
 #define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_EXTRA_FEATURES)
-
-// You can disable the built-in MicroPython compiler by setting the following
-// config option to 0.  If you do this then you won't get a REPL prompt, but you
-// will still be able to execute pre-compiled scripts, compiled with mpy-cross.
-#define MICROPY_ENABLE_COMPILER     (1)
+#endif
 
 #define MICROPY_ALLOC_PATH_MAX      (256)
+#define MICROPY_PERSISTENT_CODE_LOAD (1)
+#define MICROPY_COMP_ALLOW_TOP_LEVEL_AWAIT (1)
 #define MICROPY_READER_VFS          (MICROPY_VFS)
 #define MICROPY_ENABLE_GC           (1)
 #define MICROPY_ENABLE_PYSTACK      (1)
-#define MICROPY_STACK_CHECK         (0)
 #define MICROPY_KBD_EXCEPTION       (1)
 #define MICROPY_REPL_EVENT_DRIVEN   (1)
 #define MICROPY_LONGINT_IMPL        (MICROPY_LONGINT_IMPL_MPZ)
 #define MICROPY_ENABLE_DOC_STRING   (1)
 #define MICROPY_WARNINGS            (1)
+#define MICROPY_ERROR_PRINTER       (&mp_stderr_print)
 #define MICROPY_FLOAT_IMPL          (MICROPY_FLOAT_IMPL_DOUBLE)
 #define MICROPY_USE_INTERNAL_ERRNO  (1)
 #define MICROPY_USE_INTERNAL_PRINTF (0)
+
+#define MICROPY_EPOCH_IS_1970       (1)
+#define MICROPY_PY_ASYNCIO_TASK_QUEUE_PUSH_CALLBACK (1)
+#define MICROPY_PY_RANDOM_SEED_INIT_FUNC (mp_js_random_u32())
+#define MICROPY_PY_TIME_GMTIME_LOCALTIME_MKTIME (1)
+#define MICROPY_PY_TIME_TIME_TIME_NS (1)
+#define MICROPY_PY_TIME_INCLUDEFILE "ports/webassembly/modtime.c"
 #ifndef MICROPY_VFS
 #define MICROPY_VFS                 (1)
 #endif
 #define MICROPY_VFS_POSIX           (MICROPY_VFS)
 #define MICROPY_PY_SYS_PLATFORM     "webassembly"
-#define MICROPY_PY_SYS_STDFILES     (0)
 
-#define MICROPY_EVENT_POLL_HOOK \
-    do { \
-        extern void mp_handle_pending(bool); \
-        mp_handle_pending(true); \
-    } while (0);
+#ifndef MICROPY_PY_JS
+#define MICROPY_PY_JS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#endif
+
+#ifndef MICROPY_PY_JSFFI
+#define MICROPY_PY_JSFFI (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#endif
+
+// Whether the VM will periodically call mp_js_hook(), which checks for
+// interrupt characters on stdin (or equivalent input).
+#ifndef MICROPY_VARIANT_ENABLE_JS_HOOK
+#define MICROPY_VARIANT_ENABLE_JS_HOOK (0)
+#endif
 
+#if MICROPY_VARIANT_ENABLE_JS_HOOK
 #define MICROPY_VM_HOOK_COUNT (10)
 #define MICROPY_VM_HOOK_INIT static uint vm_hook_divisor = MICROPY_VM_HOOK_COUNT;
 #define MICROPY_VM_HOOK_POLL if (--vm_hook_divisor == 0) { \
@@ -71,19 +90,12 @@
 }
 #define MICROPY_VM_HOOK_LOOP MICROPY_VM_HOOK_POLL
 #define MICROPY_VM_HOOK_RETURN MICROPY_VM_HOOK_POLL
+#endif
 
 // type definitions for the specific machine
 
 #define MP_SSIZE_MAX (0x7fffffff)
 
-// This port is intended to be 32-bit, but unfortunately, int32_t for
-// different targets may be defined in different ways - either as int
-// or as long. This requires different printf formatting specifiers
-// to print such value. So, we avoid int32_t and use int directly.
-#define UINT_FMT "%u"
-#define INT_FMT "%d"
-typedef int mp_int_t; // must be pointer size
-typedef unsigned mp_uint_t; // must be pointer size
 typedef long mp_off_t;
 
 #define MICROPY_HW_BOARD_NAME "JS"
@@ -95,3 +107,7 @@ typedef long mp_off_t;
 // _GNU_SOURCE must be defined to get definitions of DT_xxx symbols from dirent.h.
 #define _GNU_SOURCE
 #endif
+
+extern const struct _mp_print_t mp_stderr_print;
+
+uint32_t mp_js_random_u32(void);
diff --git a/ports/webassembly/mphalport.c b/ports/webassembly/mphalport.c
index c938a639288..3935068b96b 100644
--- a/ports/webassembly/mphalport.c
+++ b/ports/webassembly/mphalport.c
@@ -24,11 +24,19 @@
  * THE SOFTWARE.
  */
 
+#include 
+#include "py/mphal.h"
 #include "library.h"
-#include "mphalport.h"
 
-void mp_hal_stdout_tx_strn(const char *str, size_t len) {
-    mp_js_write(str, len);
+static void stderr_print_strn(void *env, const char *str, size_t len) {
+    (void)env;
+    write(2, str, len);
+}
+
+const mp_print_t mp_stderr_print = {NULL, stderr_print_strn};
+
+mp_uint_t mp_hal_stdout_tx_strn(const char *str, size_t len) {
+    return write(1, str, len);
 }
 
 void mp_hal_delay_ms(mp_uint_t ms) {
@@ -55,6 +63,15 @@ mp_uint_t mp_hal_ticks_cpu(void) {
     return 0;
 }
 
+uint64_t mp_hal_time_ms(void) {
+    double mm = mp_js_time_ms();
+    return (uint64_t)mm;
+}
+
+uint64_t mp_hal_time_ns(void) {
+    return mp_hal_time_ms() * 1000000ULL;
+}
+
 extern int mp_interrupt_char;
 
 int mp_hal_get_interrupt_char(void) {
diff --git a/ports/webassembly/mphalport.h b/ports/webassembly/mphalport.h
index 89efee69747..cd22383ee05 100644
--- a/ports/webassembly/mphalport.h
+++ b/ports/webassembly/mphalport.h
@@ -28,13 +28,14 @@
 #include "shared/runtime/interrupt_char.h"
 
 #define mp_hal_stdin_rx_chr() (0)
-void mp_hal_stdout_tx_strn(const char *str, size_t len);
+mp_uint_t mp_hal_stdout_tx_strn(const char *str, size_t len);
 
 void mp_hal_delay_ms(mp_uint_t ms);
 void mp_hal_delay_us(mp_uint_t us);
 mp_uint_t mp_hal_ticks_ms(void);
 mp_uint_t mp_hal_ticks_us(void);
 mp_uint_t mp_hal_ticks_cpu(void);
+uint64_t mp_hal_time_ms(void);
 
 int mp_hal_get_interrupt_char(void);
 
@@ -50,7 +51,7 @@ int mp_hal_get_interrupt_char(void);
             if (ret == -1) { \
                 int err = errno; \
                 if (err == EINTR) { \
-                    mp_handle_pending(true); \
+                    mp_handle_pending(MP_HANDLE_PENDING_CALLBACKS_AND_EXCEPTIONS); \
                     continue; \
                 } \
                 raise; \
diff --git a/ports/webassembly/objjsproxy.c b/ports/webassembly/objjsproxy.c
new file mode 100644
index 00000000000..7d6e0203b7b
--- /dev/null
+++ b/ports/webassembly/objjsproxy.c
@@ -0,0 +1,653 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include 
+#include 
+
+#include "emscripten.h"
+#include "py/objmodule.h"
+#include "py/runtime.h"
+#include "proxy_c.h"
+
+static mp_obj_t *jsproxy_table = NULL;
+static size_t jsproxy_table_len = 0;
+
+EM_JS(bool, has_attr, (int jsref, const char *str), {
+    const base = proxy_js_ref[jsref];
+    const attr = UTF8ToString(str);
+    if (attr in base) {
+        return true;
+    } else {
+        return false;
+    }
+});
+
+// *FORMAT-OFF*
+EM_JS(int, lookup_attr, (int jsref, const char *str, uint32_t * out), {
+    const base = proxy_js_ref[jsref];
+    const attr = UTF8ToString(str);
+
+    // Attempt to lookup the requested attribute from the base object:
+    // - If the value is not `undefined` then the attribute exists with that value.
+    // - Otherwise if the value is `undefined` and the `in` operator returns true, then
+    //   that attribute does exist and is intended to have a value of `undefined`.
+    // - Otherwise, the attribute does not exist.
+    let value = base[attr];
+    if (value !== undefined || attr in base) {
+        proxy_convert_js_to_mp_obj_jsside(value, out);
+        if (typeof value === "function" && !("_ref" in value)) {
+            // Attribute found and it's a JavaScript function.
+            return 2;
+        } else {
+            // Attribute found.
+            return 1;
+        }
+    } else {
+        // Attribute not found.
+        return 0;
+    }
+});
+// *FORMAT-ON*
+
+EM_JS(void, store_attr, (int jsref, const char *attr_ptr, uint32_t * value_ref), {
+    const attr = UTF8ToString(attr_ptr);
+    const value = proxy_convert_mp_to_js_obj_jsside(value_ref);
+    proxy_js_ref[jsref][attr] = value;
+});
+
+EM_JS(void, call0, (int f_ref, uint32_t * out), {
+    // Because of JavaScript "this" semantics, we must extract the target function
+    // to a variable before calling it, so "this" is bound to the correct value.
+    //
+    // In detail:
+    // In JavaScript, proxy_js_ref[f_ref] acts like a function call
+    // proxy_js_ref.at(f_ref), and "this" will be bound to proxy_js_ref if
+    // there is a chain of calls, such as proxy_js_ref.at(f_ref)().
+    // But proxy_js_ref is not "this" in the context of the call, so we
+    // must extract the function to an independent variable and then call
+    // that variable, so that "this" is correct (it will be "undefined").
+
+    const f = proxy_js_ref[f_ref];
+    const ret = f();
+    proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(int, call1, (int f_ref, bool via_call, uint32_t * a0, uint32_t * out), {
+    const a0_js = proxy_convert_mp_to_js_obj_jsside(a0);
+    const f = proxy_js_ref[f_ref];
+    let ret;
+    if (via_call) {
+        ret = f.call(a0_js);
+    } else {
+        ret = f(a0_js);
+    }
+    proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(int, call2, (int f_ref, bool via_call, uint32_t * a0, uint32_t * a1, uint32_t * out), {
+    const a0_js = proxy_convert_mp_to_js_obj_jsside(a0);
+    const a1_js = proxy_convert_mp_to_js_obj_jsside(a1);
+    const f = proxy_js_ref[f_ref];
+    let ret;
+    if (via_call) {
+        ret = f.call(a0_js, a1_js);
+    } else {
+        ret = f(a0_js, a1_js);
+    }
+    proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(int, calln, (int f_ref, bool via_call, uint32_t n_args, uint32_t * value, uint32_t * out), {
+    const f = proxy_js_ref[f_ref];
+    const a = [];
+    for (let i = 0; i < n_args; ++i) {
+        const v = proxy_convert_mp_to_js_obj_jsside(value + i * 3 * 4);
+        a.push(v);
+    }
+    let ret;
+    if (via_call) {
+        ret = f.call(... a);
+    } else {
+        ret = f(... a);
+    }
+    proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(void, call0_kwarg, (int f_ref, bool via_call, uint32_t n_kw, uint32_t * key, uint32_t * value, uint32_t * out), {
+    const f = proxy_js_ref[f_ref];
+    const a = {};
+    for (let i = 0; i < n_kw; ++i) {
+        const k = UTF8ToString(getValue(key + i * 4, "i32"));
+        const v = proxy_convert_mp_to_js_obj_jsside(value + i * 3 * 4);
+        a[k] = v;
+    }
+    let ret;
+    if (via_call) {
+        ret = f.call(a);
+    } else {
+        ret = f(a);
+    }
+    proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(void, calln_kwarg, (int f_ref, bool via_call, uint32_t n_args, uint32_t * args_value, uint32_t n_kw, uint32_t * kw_key, uint32_t * kw_value, uint32_t * out), {
+    const f = proxy_js_ref[f_ref];
+    const a = [];
+    for (let i = 0; i < n_args; ++i) {
+        const v = proxy_convert_mp_to_js_obj_jsside(args_value + i * 3 * 4);
+        a.push(v);
+    }
+    const ks = {};
+    for (let i = 0; i < n_kw; ++i) {
+        const k = UTF8ToString(getValue(kw_key + i * 4, "i32"));
+        const v = proxy_convert_mp_to_js_obj_jsside(kw_value + i * 3 * 4);
+        ks[k] = v;
+    }
+    let ret;
+    if (via_call) {
+        ret = f.call(... a, ks);
+    } else {
+        ret = f(... a, ks);
+    }
+    proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(void, js_reflect_construct, (int f_ref, uint32_t n_args, uint32_t * args, uint32_t * out), {
+    const f = proxy_js_ref[f_ref];
+    const as = [];
+    for (let i = 0; i < n_args; ++i) {
+        as.push(proxy_convert_mp_to_js_obj_jsside(args + i * 3 * 4));
+    }
+    const ret = Reflect.construct(f, as);
+    proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(void, js_get_iter, (int f_ref, uint32_t * out), {
+    const f = proxy_js_ref[f_ref];
+    const ret = f[Symbol.iterator]();
+    proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(bool, js_iter_next, (int f_ref, uint32_t * out), {
+    const f = proxy_js_ref[f_ref];
+    const ret = f.next();
+    if (ret.done) {
+        return false;
+    } else {
+        proxy_convert_js_to_mp_obj_jsside(ret.value, out);
+        return true;
+    }
+});
+
+EM_JS(void, js_subscr_load, (int f_ref, uint32_t * index_ref, uint32_t * out), {
+    const target = proxy_js_ref[f_ref];
+    const index = python_index_semantics(target, proxy_convert_mp_to_js_obj_jsside(index_ref));
+    const ret = target[index];
+    proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(void, js_subscr_store, (int f_ref, uint32_t * idx, uint32_t * value), {
+    const f = proxy_js_ref[f_ref];
+    f[proxy_convert_mp_to_js_obj_jsside(idx)] = proxy_convert_mp_to_js_obj_jsside(value);
+});
+
+static void jsproxy_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+    mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
+    mp_printf(print, "", self->ref);
+}
+
+static mp_obj_t jsproxy_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+    mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
+
+    mp_arg_check_num(n_args, n_kw, 0, MP_OBJ_FUN_ARGS_MAX, true);
+
+    if (n_kw != 0) {
+        uint32_t key[n_kw];
+        uint32_t value[PVN * n_kw];
+        for (int i = 0; i < n_kw; ++i) {
+            key[i] = (uintptr_t)mp_obj_str_get_str(args[n_args + i * 2]);
+            proxy_convert_mp_to_js_obj_cside(args[n_args + i * 2 + 1], &value[i * PVN]);
+        }
+        uint32_t out[3];
+        if (n_args == 0) {
+            call0_kwarg(self->ref, self->bind_to_self, n_kw, key, value, out);
+        } else {
+            uint32_t value_args[PVN * n_args];
+            for (int i = 0; i < n_args; ++i) {
+                proxy_convert_mp_to_js_obj_cside(args[i], &value_args[i * PVN]);
+            }
+            calln_kwarg(self->ref, self->bind_to_self, n_args, value_args, n_kw, key, value, out);
+        }
+        return proxy_convert_js_to_mp_obj_cside(out);
+    }
+
+    if (n_args == 0) {
+        uint32_t out[3];
+        call0(self->ref, out);
+        return proxy_convert_js_to_mp_obj_cside(out);
+    } else if (n_args == 1) {
+        uint32_t arg0[PVN];
+        uint32_t out[PVN];
+        proxy_convert_mp_to_js_obj_cside(args[0], arg0);
+        call1(self->ref, self->bind_to_self, arg0, out);
+        return proxy_convert_js_to_mp_obj_cside(out);
+    } else if (n_args == 2) {
+        uint32_t arg0[PVN];
+        proxy_convert_mp_to_js_obj_cside(args[0], arg0);
+        uint32_t arg1[PVN];
+        proxy_convert_mp_to_js_obj_cside(args[1], arg1);
+        uint32_t out[3];
+        call2(self->ref, self->bind_to_self, arg0, arg1, out);
+        return proxy_convert_js_to_mp_obj_cside(out);
+    } else {
+        uint32_t value[PVN * n_args];
+        for (int i = 0; i < n_args; ++i) {
+            proxy_convert_mp_to_js_obj_cside(args[i], &value[i * PVN]);
+        }
+        uint32_t out[3];
+        calln(self->ref, self->bind_to_self, n_args, value, out);
+        return proxy_convert_js_to_mp_obj_cside(out);
+    }
+}
+
+static mp_obj_t jsproxy_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
+    if (!mp_obj_is_type(rhs_in, &mp_type_jsproxy)) {
+        return MP_OBJ_NULL; // op not supported
+    }
+
+    mp_obj_jsproxy_t *lhs = MP_OBJ_TO_PTR(lhs_in);
+    mp_obj_jsproxy_t *rhs = MP_OBJ_TO_PTR(rhs_in);
+
+    switch (op) {
+        case MP_BINARY_OP_EQUAL:
+            return mp_obj_new_bool(lhs->ref == rhs->ref);
+
+        default:
+            return MP_OBJ_NULL; // op not supported
+    }
+}
+
+EM_JS(void, proxy_js_free_obj, (int js_ref), {
+    if (js_ref >= PROXY_JS_REF_NUM_STATIC) {
+        proxy_js_ref_map.delete(proxy_js_ref[js_ref]);
+        proxy_js_ref[js_ref] = undefined;
+        if (js_ref < proxy_js_ref_next) {
+            proxy_js_ref_next = js_ref;
+        }
+    }
+});
+
+static mp_obj_t jsproxy___del__(mp_obj_t self_in) {
+    mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
+    jsproxy_table[self->ref] = MP_OBJ_NULL;
+    proxy_js_free_obj(self->ref);
+    return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(jsproxy___del___obj, jsproxy___del__);
+
+static mp_obj_t jsproxy_reflect_construct(size_t n_args, const mp_obj_t *args) {
+    int arg0 = mp_obj_jsproxy_get_ref(args[0]);
+    n_args -= 1;
+    args += 1;
+    uint32_t args_conv[n_args];
+    for (unsigned int i = 0; i < n_args; ++i) {
+        proxy_convert_mp_to_js_obj_cside(args[i], &args_conv[i * PVN]);
+    }
+    uint32_t out[PVN];
+    js_reflect_construct(arg0, n_args, args_conv, out);
+    return proxy_convert_js_to_mp_obj_cside(out);
+}
+static MP_DEFINE_CONST_FUN_OBJ_VAR(jsproxy_reflect_construct_obj, 1, jsproxy_reflect_construct);
+
+static mp_obj_t jsproxy_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
+    mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
+    if (value == MP_OBJ_SENTINEL) {
+        // Load subscript.
+        uint32_t idx[PVN], out[PVN];
+        proxy_convert_mp_to_js_obj_cside(index, idx);
+        js_subscr_load(self->ref, idx, out);
+        return proxy_convert_js_to_mp_obj_cside(out);
+    } else if (value == MP_OBJ_NULL) {
+        // Delete subscript.
+        return MP_OBJ_NULL; // not supported
+    } else {
+        // Store subscript.
+        uint32_t idx[PVN], val[PVN];
+        proxy_convert_mp_to_js_obj_cside(index, idx);
+        proxy_convert_mp_to_js_obj_cside(value, val);
+        js_subscr_store(self->ref, idx, val);
+        return mp_const_none;
+    }
+}
+
+static void mp_obj_jsproxy_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
+    if (dest[0] == MP_OBJ_NULL) {
+        // Load attribute.
+        int lookup_ret;
+        uint32_t out[PVN];
+        if (attr == MP_QSTR___del__) {
+            // For finaliser.
+            dest[0] = MP_OBJ_FROM_PTR(&jsproxy___del___obj);
+            dest[1] = self_in;
+        } else if ((lookup_ret = lookup_attr(self->ref, qstr_str(attr), out)) != 0) {
+            dest[0] = proxy_convert_js_to_mp_obj_cside(out);
+            if (lookup_ret == 2) {
+                // The loaded attribute is a JavaScript method, which should be called
+                // with f.call(self, ...).  Indicate this via the bind_to_self member.
+                // This will either be called immediately (due to the mp_load_method
+                // optimisation) or turned into a bound_method and called later.
+                dest[1] = self_in;
+                ((mp_obj_jsproxy_t *)dest[0])->bind_to_self = true;
+            }
+        } else if (attr == MP_QSTR_new) {
+            // Special case to handle construction of JS objects.
+            // JS objects don't have a ".new" attribute, doing "Obj.new" is a Pyodide idiom for "new Obj".
+            // It translates to the JavaScript "Reflect.construct(Obj, Array(...args))".
+            dest[0] = MP_OBJ_FROM_PTR(&jsproxy_reflect_construct_obj);
+            dest[1] = self_in;
+        }
+    } else if (dest[1] == MP_OBJ_NULL) {
+        // Delete attribute.
+    } else {
+        // Store attribute.
+        uint32_t value[PVN];
+        proxy_convert_mp_to_js_obj_cside(dest[1], value);
+        store_attr(self->ref, qstr_str(attr), value);
+        dest[0] = MP_OBJ_NULL;
+    }
+}
+
+/******************************************************************************/
+// jsproxy iterator
+
+typedef struct _jsproxy_it_t {
+    mp_obj_base_t base;
+    mp_fun_1_t iternext;
+    mp_obj_jsproxy_t *iter;
+} jsproxy_it_t;
+
+static mp_obj_t jsproxy_it_iternext(mp_obj_t self_in) {
+    jsproxy_it_t *self = MP_OBJ_TO_PTR(self_in);
+    uint32_t out[3];
+    if (js_iter_next(self->iter->ref, out)) {
+        return proxy_convert_js_to_mp_obj_cside(out);
+    } else {
+        return MP_OBJ_STOP_ITERATION;
+    }
+}
+
+static mp_obj_t jsproxy_new_it(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
+    assert(sizeof(jsproxy_it_t) <= sizeof(mp_obj_iter_buf_t));
+    mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
+    jsproxy_it_t *o = (jsproxy_it_t *)iter_buf;
+    o->base.type = &mp_type_polymorph_iter;
+    o->iternext = jsproxy_it_iternext;
+    uint32_t out[3];
+    js_get_iter(self->ref, out);
+    o->iter = proxy_convert_js_to_mp_obj_cside(out);
+    return MP_OBJ_FROM_PTR(o);
+}
+
+/******************************************************************************/
+// jsproxy generator
+
+enum {
+    JSOBJ_GEN_STATE_WAITING,
+    JSOBJ_GEN_STATE_COMPLETED,
+    JSOBJ_GEN_STATE_EXHAUSTED,
+};
+
+typedef struct _jsproxy_gen_t {
+    mp_obj_base_t base;
+    mp_obj_t thenable;
+    int state;
+} jsproxy_gen_t;
+
+mp_vm_return_kind_t jsproxy_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_obj_t throw_value, mp_obj_t *ret_val) {
+    jsproxy_gen_t *self = MP_OBJ_TO_PTR(self_in);
+
+    if (throw_value) {
+        *ret_val = throw_value;
+        return MP_VM_RETURN_EXCEPTION;
+    }
+
+    switch (self->state) {
+        case JSOBJ_GEN_STATE_WAITING:
+            self->state = JSOBJ_GEN_STATE_COMPLETED;
+            *ret_val = self->thenable;
+            return MP_VM_RETURN_YIELD;
+
+        case JSOBJ_GEN_STATE_COMPLETED:
+            self->state = JSOBJ_GEN_STATE_EXHAUSTED;
+            *ret_val = send_value;
+            return MP_VM_RETURN_NORMAL;
+
+        case JSOBJ_GEN_STATE_EXHAUSTED:
+        default:
+            // Trying to resume an already stopped generator.
+            // This is an optimised "raise StopIteration(None)".
+            *ret_val = mp_const_none;
+            return MP_VM_RETURN_NORMAL;
+    }
+}
+
+static mp_obj_t jsproxy_gen_resume_and_raise(mp_obj_t self_in, mp_obj_t send_value, mp_obj_t throw_value, bool raise_stop_iteration) {
+    mp_obj_t ret;
+    switch (jsproxy_gen_resume(self_in, send_value, throw_value, &ret)) {
+        case MP_VM_RETURN_NORMAL:
+        default:
+            // A normal return is a StopIteration, either raise it or return
+            // MP_OBJ_STOP_ITERATION as an optimisation.
+            if (ret == mp_const_none) {
+                ret = MP_OBJ_NULL;
+            }
+            if (raise_stop_iteration) {
+                mp_raise_StopIteration(ret);
+            } else {
+                return mp_make_stop_iteration(ret);
+            }
+
+        case MP_VM_RETURN_YIELD:
+            return ret;
+
+        case MP_VM_RETURN_EXCEPTION:
+            nlr_raise(ret);
+    }
+}
+
+static mp_obj_t jsproxy_gen_instance_iternext(mp_obj_t self_in) {
+    return jsproxy_gen_resume_and_raise(self_in, mp_const_none, MP_OBJ_NULL, false);
+}
+
+static mp_obj_t jsproxy_gen_instance_send(mp_obj_t self_in, mp_obj_t send_value) {
+    return jsproxy_gen_resume_and_raise(self_in, send_value, MP_OBJ_NULL, true);
+}
+static MP_DEFINE_CONST_FUN_OBJ_2(jsproxy_gen_instance_send_obj, jsproxy_gen_instance_send);
+
+static mp_obj_t jsproxy_gen_instance_throw(size_t n_args, const mp_obj_t *args) {
+    // The signature of this function is: throw(type[, value[, traceback]])
+    // CPython will pass all given arguments through the call chain and process them
+    // at the point they are used (native generators will handle them differently to
+    // user-defined generators with a throw() method).  To save passing multiple
+    // values, MicroPython instead does partial processing here to reduce it down to
+    // one argument and passes that through:
+    // - if only args[1] is given, or args[2] is given but is None, args[1] is
+    //   passed through (in the standard case it is an exception class or instance)
+    // - if args[2] is given and not None it is passed through (in the standard
+    //   case it would be an exception instance and args[1] its corresponding class)
+    // - args[3] is always ignored
+
+    mp_obj_t exc = args[1];
+    if (n_args > 2 && args[2] != mp_const_none) {
+        exc = args[2];
+    }
+
+    return jsproxy_gen_resume_and_raise(args[0], mp_const_none, exc, true);
+}
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(jsproxy_gen_instance_throw_obj, 2, 4, jsproxy_gen_instance_throw);
+
+static mp_obj_t jsproxy_gen_instance_close(mp_obj_t self_in) {
+    mp_obj_t ret;
+    switch (jsproxy_gen_resume(self_in, mp_const_none, MP_OBJ_FROM_PTR(&mp_const_GeneratorExit_obj), &ret)) {
+        case MP_VM_RETURN_YIELD:
+            mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("generator ignored GeneratorExit"));
+
+        // Swallow GeneratorExit (== successful close), and re-raise any other
+        case MP_VM_RETURN_EXCEPTION:
+            // ret should always be an instance of an exception class
+            if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(mp_obj_get_type(ret)), MP_OBJ_FROM_PTR(&mp_type_GeneratorExit))) {
+                return mp_const_none;
+            }
+            nlr_raise(ret);
+
+        default:
+            // The only choice left is MP_VM_RETURN_NORMAL which is successful close
+            return mp_const_none;
+    }
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(jsproxy_gen_instance_close_obj, jsproxy_gen_instance_close);
+
+static const mp_rom_map_elem_t jsproxy_gen_instance_locals_dict_table[] = {
+    { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&jsproxy_gen_instance_close_obj) },
+    { MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&jsproxy_gen_instance_send_obj) },
+    { MP_ROM_QSTR(MP_QSTR_throw), MP_ROM_PTR(&jsproxy_gen_instance_throw_obj) },
+};
+static MP_DEFINE_CONST_DICT(jsproxy_gen_instance_locals_dict, jsproxy_gen_instance_locals_dict_table);
+
+MP_DEFINE_CONST_OBJ_TYPE(
+    mp_type_jsproxy_gen,
+    MP_QSTR_generator,
+    MP_TYPE_FLAG_ITER_IS_ITERNEXT,
+    iter, jsproxy_gen_instance_iternext,
+    locals_dict, &jsproxy_gen_instance_locals_dict
+    );
+
+static mp_obj_t jsproxy_new_gen(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
+    assert(sizeof(jsproxy_gen_t) <= sizeof(mp_obj_iter_buf_t));
+    jsproxy_gen_t *o = (jsproxy_gen_t *)iter_buf;
+    o->base.type = &mp_type_jsproxy_gen;
+    o->thenable = self_in;
+    o->state = JSOBJ_GEN_STATE_WAITING;
+    return MP_OBJ_FROM_PTR(o);
+}
+
+/******************************************************************************/
+
+#if MICROPY_PY_ASYNCIO
+extern mp_obj_t mp_asyncio_context;
+#endif
+
+static mp_obj_t jsproxy_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
+    mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
+    if (has_attr(self->ref, "then")) {
+        #if MICROPY_PY_ASYNCIO
+        // When asyncio is running and the caller here is a task, wrap the JavaScript
+        // thenable in a ThenableEvent, and get the task to wait on that event.  This
+        // decouples the task from the thenable and allows cancelling the task.
+        if (mp_asyncio_context != MP_OBJ_NULL) {
+            mp_obj_t cur_task = mp_obj_dict_get(mp_asyncio_context, MP_OBJ_NEW_QSTR(MP_QSTR_cur_task));
+            mp_obj_t top_level_task = mp_obj_dict_get(mp_asyncio_context, MP_OBJ_NEW_QSTR(MP_QSTR__top_level_task));
+            if (cur_task != top_level_task) {
+                mp_obj_t thenable_event_class = mp_obj_dict_get(mp_asyncio_context, MP_OBJ_NEW_QSTR(MP_QSTR_ThenableEvent));
+                mp_obj_t thenable_event = mp_call_function_1(thenable_event_class, self_in);
+                mp_obj_t dest[2];
+                mp_load_method(thenable_event, MP_QSTR_wait, dest);
+                mp_obj_t wait_gen = mp_call_method_n_kw(0, 0, dest);
+                return mp_getiter(wait_gen, iter_buf);
+            }
+        }
+        #endif
+        return jsproxy_new_gen(self_in, iter_buf);
+    } else {
+        return jsproxy_new_it(self_in, iter_buf);
+    }
+}
+
+MP_DEFINE_CONST_OBJ_TYPE(
+    mp_type_jsproxy,
+    MP_QSTR_JsProxy,
+    MP_TYPE_FLAG_ITER_IS_GETITER,
+    print, jsproxy_print,
+    call, jsproxy_call,
+    binary_op, jsproxy_binary_op,
+    attr, mp_obj_jsproxy_attr,
+    subscr, jsproxy_subscr,
+    iter, jsproxy_getiter
+    );
+
+void mp_obj_jsproxy_init(void) {
+    jsproxy_table = NULL;
+    jsproxy_table_len = 0;
+    MP_STATE_PORT(jsproxy_global_this) = mp_obj_new_jsproxy(MP_OBJ_JSPROXY_REF_GLOBAL_THIS);
+}
+
+MP_REGISTER_ROOT_POINTER(mp_obj_t jsproxy_global_this);
+
+mp_obj_t mp_obj_new_jsproxy(int ref) {
+    // The proxy for this ref should not exist.
+    assert(ref >= jsproxy_table_len || jsproxy_table[ref] == MP_OBJ_NULL);
+
+    mp_obj_jsproxy_t *o = mp_obj_malloc_with_finaliser(mp_obj_jsproxy_t, &mp_type_jsproxy);
+    o->ref = ref;
+    o->bind_to_self = false;
+    if (ref >= jsproxy_table_len) {
+        size_t new_len = MAX(16, ref * 2);
+        jsproxy_table = realloc(jsproxy_table, new_len * sizeof(mp_obj_t));
+        for (size_t i = jsproxy_table_len; i < new_len; ++i) {
+            jsproxy_table[i] = MP_OBJ_NULL;
+        }
+        jsproxy_table_len = new_len;
+    }
+    jsproxy_table[ref] = MP_OBJ_FROM_PTR(o);
+    return MP_OBJ_FROM_PTR(o);
+}
+
+mp_obj_t mp_obj_get_jsproxy(int ref) {
+    // The proxy for this ref should exist.
+    assert(ref < jsproxy_table_len && jsproxy_table[ref] != MP_OBJ_NULL);
+
+    return jsproxy_table[ref];
+}
+
+// Load/delete/store an attribute from/to the JavaScript globalThis entity.
+void mp_obj_jsproxy_global_this_attr(qstr attr, mp_obj_t *dest) {
+    if (dest[0] == MP_OBJ_NULL) {
+        // Load attribute.
+        uint32_t out[PVN];
+        if (lookup_attr(MP_OBJ_JSPROXY_REF_GLOBAL_THIS, qstr_str(attr), out)) {
+            dest[0] = proxy_convert_js_to_mp_obj_cside(out);
+        }
+    } else if (dest[1] == MP_OBJ_NULL) {
+        // Delete attribute.
+    } else {
+        // Store attribute.
+        uint32_t value[PVN];
+        proxy_convert_mp_to_js_obj_cside(dest[1], value);
+        store_attr(MP_OBJ_JSPROXY_REF_GLOBAL_THIS, qstr_str(attr), value);
+        dest[0] = MP_OBJ_NULL;
+    }
+}
diff --git a/ports/webassembly/objpyproxy.js b/ports/webassembly/objpyproxy.js
new file mode 100644
index 00000000000..6b132e19db0
--- /dev/null
+++ b/ports/webassembly/objpyproxy.js
@@ -0,0 +1,240 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+class PyProxy {
+    constructor(ref) {
+        this._ref = ref;
+    }
+
+    // Convert js_obj -- which is possibly a PyProxy -- to a JavaScript object.
+    static toJs(js_obj) {
+        if (!(js_obj instanceof PyProxy)) {
+            return js_obj;
+        }
+
+        const obj_data = Module._malloc(2 * 4);
+        const type = Module.ccall(
+            "proxy_c_to_js_get_type_and_data",
+            "number",
+            ["number", "pointer"],
+            [js_obj._ref, obj_data],
+        );
+
+        let js_obj_out;
+        if (type === 1 || type === 2) {
+            // List or tuple.
+            const item = Module._malloc(3 * 4);
+            const len = Module.getValue(obj_data, "i32");
+            const items_ptr = Module.getValue(obj_data + 4, "i32");
+            const js_array = [];
+            for (let i = 0; i < len; ++i) {
+                Module.ccall(
+                    "proxy_convert_mp_to_js_obj_cside",
+                    "null",
+                    ["pointer", "pointer"],
+                    [Module.getValue(items_ptr + i * 4, "i32"), item],
+                );
+                const js_item = proxy_convert_mp_to_js_obj_jsside(item);
+                js_array.push(PyProxy.toJs(js_item));
+            }
+            Module._free(item);
+            js_obj_out = js_array;
+        } else if (type === 3) {
+            // Dict.
+            const item = Module._malloc(3 * 4);
+            const alloc = Module.getValue(obj_data, "i32");
+            const table_ptr = Module.getValue(obj_data + 4, "i32");
+            const js_dict = {};
+            for (let i = 0; i < alloc; ++i) {
+                const mp_key = Module.getValue(table_ptr + i * 8, "i32");
+                if (mp_key > 8) {
+                    // Convert key to JS object.
+                    Module.ccall(
+                        "proxy_convert_mp_to_js_obj_cside",
+                        "null",
+                        ["pointer", "pointer"],
+                        [mp_key, item],
+                    );
+                    const js_key = proxy_convert_mp_to_js_obj_jsside(item);
+
+                    // Convert value to JS object.
+                    const mp_value = Module.getValue(
+                        table_ptr + i * 8 + 4,
+                        "i32",
+                    );
+                    Module.ccall(
+                        "proxy_convert_mp_to_js_obj_cside",
+                        "null",
+                        ["pointer", "pointer"],
+                        [mp_value, item],
+                    );
+                    const js_value = proxy_convert_mp_to_js_obj_jsside(item);
+
+                    // Populate JS dict.
+                    js_dict[js_key] = PyProxy.toJs(js_value);
+                }
+            }
+            Module._free(item);
+            js_obj_out = js_dict;
+        } else if (type === 4) {
+            // Buffer protocol; create a copy of the data to a new Uint8Array.
+            const len = Module.getValue(obj_data, "i32");
+            const buf = Module.getValue(obj_data + 4, "i32");
+            js_obj_out = Module.HEAPU8.slice(buf, buf + len);
+        } else {
+            // Cannot convert to JS, leave as a PyProxy.
+            js_obj_out = js_obj;
+        }
+
+        Module._free(obj_data);
+        return js_obj_out;
+    }
+}
+
+// This handler's goal is to allow minimal introspection
+// of Python references from the JS world/utilities.
+const py_proxy_handler = {
+    isExtensible() {
+        return true;
+    },
+    ownKeys(target) {
+        const value = Module._malloc(3 * 4);
+        Module.ccall(
+            "proxy_c_to_js_dir",
+            "null",
+            ["number", "pointer"],
+            [target._ref, value],
+        );
+        const dir = proxy_convert_mp_to_js_obj_jsside_with_free(value);
+        return PyProxy.toJs(dir).filter((attr) => !attr.startsWith("__"));
+    },
+    getOwnPropertyDescriptor(target, prop) {
+        return {
+            value: target[prop],
+            enumerable: true,
+            writable: true,
+            configurable: true,
+        };
+    },
+    has(target, prop) {
+        // avoid throwing on `Symbol() in proxy` checks
+        if (typeof prop !== "string") {
+            // returns true only on iterator because other
+            // symbols are not considered in the `get` trap
+            return prop === Symbol.iterator;
+        }
+        return Module.ccall(
+            "proxy_c_to_js_has_attr",
+            "number",
+            ["number", "string"],
+            [target._ref, prop],
+        );
+    },
+    get(target, prop) {
+        if (prop === "_ref") {
+            return target._ref;
+        }
+
+        // ignore both then and all symbols but Symbol.iterator
+        if (prop === "then" || typeof prop !== "string") {
+            if (prop === Symbol.iterator) {
+                // Get the Python object iterator, and return a JavaScript generator.
+                const iter_ref = Module.ccall(
+                    "proxy_c_to_js_get_iter",
+                    "number",
+                    ["number"],
+                    [target._ref],
+                );
+                return function* () {
+                    const value = Module._malloc(3 * 4);
+                    while (true) {
+                        const valid = Module.ccall(
+                            "proxy_c_to_js_iternext",
+                            "number",
+                            ["number", "pointer"],
+                            [iter_ref, value],
+                        );
+                        if (!valid) {
+                            break;
+                        }
+                        yield proxy_convert_mp_to_js_obj_jsside(value);
+                    }
+                    Module._free(value);
+                };
+            }
+            return undefined;
+        }
+
+        const value = Module._malloc(3 * 4);
+        Module.ccall(
+            "proxy_c_to_js_lookup_attr",
+            "null",
+            ["number", "string", "pointer"],
+            [target._ref, prop, value],
+        );
+        return proxy_convert_mp_to_js_obj_jsside_with_free(value);
+    },
+    set(target, prop, value) {
+        const value_conv = Module._malloc(3 * 4);
+        proxy_convert_js_to_mp_obj_jsside(value, value_conv);
+        const ret = Module.ccall(
+            "proxy_c_to_js_store_attr",
+            "number",
+            ["number", "string", "number"],
+            [target._ref, prop, value_conv],
+        );
+        Module._free(value_conv);
+        return ret;
+    },
+    deleteProperty(target, prop) {
+        return Module.ccall(
+            "proxy_c_to_js_delete_attr",
+            "number",
+            ["number", "string"],
+            [target._ref, prop],
+        );
+    },
+};
+
+// PyProxy of a Python generator, that implements the thenable interface.
+class PyProxyThenable {
+    constructor(ref) {
+        this._ref = ref;
+    }
+
+    then(resolve, reject) {
+        const values = Module._malloc(3 * 3 * 4);
+        proxy_convert_js_to_mp_obj_jsside(resolve, values + 3 * 4);
+        proxy_convert_js_to_mp_obj_jsside(reject, values + 2 * 3 * 4);
+        Module.ccall(
+            "proxy_c_to_js_resume",
+            "null",
+            ["number", "pointer"],
+            [this._ref, values],
+        );
+        return proxy_convert_mp_to_js_obj_jsside_with_free(values);
+    }
+}
diff --git a/ports/webassembly/proxy_c.c b/ports/webassembly/proxy_c.c
new file mode 100644
index 00000000000..7b2b8a9871e
--- /dev/null
+++ b/ports/webassembly/proxy_c.c
@@ -0,0 +1,621 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include 
+#include 
+
+#include "emscripten.h"
+#include "py/builtin.h"
+#include "py/runtime.h"
+#include "proxy_c.h"
+
+// Number of static entries at the start of proxy_c_ref.
+#define PROXY_C_REF_NUM_STATIC (1)
+
+// These constants should match the constants in proxy_js.js.
+
+enum {
+    PROXY_KIND_MP_EXCEPTION = -1,
+    PROXY_KIND_MP_NULL = 0,
+    PROXY_KIND_MP_NONE = 1,
+    PROXY_KIND_MP_BOOL = 2,
+    PROXY_KIND_MP_INT = 3,
+    PROXY_KIND_MP_FLOAT = 4,
+    PROXY_KIND_MP_STR = 5,
+    PROXY_KIND_MP_CALLABLE = 6,
+    PROXY_KIND_MP_GENERATOR = 7,
+    PROXY_KIND_MP_OBJECT = 8,
+    PROXY_KIND_MP_JSPROXY = 9,
+    PROXY_KIND_MP_EXISTING = 10,
+};
+
+enum {
+    PROXY_KIND_JS_UNDEFINED = 0,
+    PROXY_KIND_JS_NULL = 1,
+    PROXY_KIND_JS_BOOLEAN = 2,
+    PROXY_KIND_JS_INTEGER = 3,
+    PROXY_KIND_JS_DOUBLE = 4,
+    PROXY_KIND_JS_STRING = 5,
+    PROXY_KIND_JS_OBJECT_EXISTING = 6,
+    PROXY_KIND_JS_OBJECT = 7,
+    PROXY_KIND_JS_PYPROXY = 8,
+};
+
+MP_DEFINE_CONST_OBJ_TYPE(
+    mp_type_undefined,
+    MP_QSTR_undefined,
+    MP_TYPE_FLAG_NONE
+    );
+
+static const mp_obj_base_t mp_const_undefined_obj = {&mp_type_undefined};
+
+#define mp_const_undefined (MP_OBJ_FROM_PTR(&mp_const_undefined_obj))
+
+MP_DEFINE_EXCEPTION(JsException, Exception)
+
+// Index to start searching for the next available slot in proxy_c_ref.
+static size_t proxy_c_ref_next;
+
+void proxy_c_init(void) {
+    MP_STATE_PORT(proxy_c_ref) = mp_obj_new_list(0, NULL);
+    MP_STATE_PORT(proxy_c_dict) = mp_obj_new_dict(0);
+    mp_obj_list_append(MP_STATE_PORT(proxy_c_ref), MP_OBJ_NULL);
+    proxy_c_ref_next = PROXY_C_REF_NUM_STATIC;
+
+    void mp_obj_jsproxy_init(void);
+    mp_obj_jsproxy_init();
+}
+
+MP_REGISTER_ROOT_POINTER(mp_obj_t proxy_c_ref);
+MP_REGISTER_ROOT_POINTER(mp_obj_t proxy_c_dict);
+
+// obj cannot be MP_OBJ_NULL.
+static inline size_t proxy_c_add_obj(mp_obj_t obj) {
+    // Search for the first free slot in proxy_c_ref.
+    size_t id = 0;
+    mp_obj_list_t *l = (mp_obj_list_t *)MP_OBJ_TO_PTR(MP_STATE_PORT(proxy_c_ref));
+    while (proxy_c_ref_next < l->len) {
+        if (l->items[proxy_c_ref_next] == MP_OBJ_NULL) {
+            // Free slot found, reuse it.
+            id = proxy_c_ref_next;
+            ++proxy_c_ref_next;
+            l->items[id] = obj;
+            break;
+        }
+        ++proxy_c_ref_next;
+    }
+
+    if (id == 0) {
+        // No free slots, so grow proxy_c_ref by one (append at the end of the list).
+        id = l->len;
+        mp_obj_list_append(MP_STATE_PORT(proxy_c_ref), obj);
+        proxy_c_ref_next = l->len;
+    }
+
+    // Add the object to proxy_c_dict, keyed by the object pointer, with value the object id.
+    mp_obj_t obj_key = mp_obj_new_int_from_uint((uintptr_t)obj);
+    mp_map_elem_t *elem = mp_map_lookup(mp_obj_dict_get_map(MP_STATE_PORT(proxy_c_dict)), obj_key, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND);
+    elem->value = mp_obj_new_int_from_uint(id);
+
+    return id;
+}
+
+EM_JS(int, js_check_existing, (int c_ref), {
+    return proxy_js_check_existing(c_ref);
+});
+
+// obj cannot be MP_OBJ_NULL.
+static inline int proxy_c_check_existing(mp_obj_t obj) {
+    mp_obj_t obj_key = mp_obj_new_int_from_uint((uintptr_t)obj);
+    mp_map_elem_t *elem = mp_map_lookup(mp_obj_dict_get_map(MP_STATE_PORT(proxy_c_dict)), obj_key, MP_MAP_LOOKUP);
+    if (elem == NULL) {
+        return -1;
+    }
+    uint32_t c_ref = mp_obj_int_get_truncated(elem->value);
+    return js_check_existing(c_ref);
+}
+
+static inline mp_obj_t proxy_c_get_obj(uint32_t c_ref) {
+    return ((mp_obj_list_t *)MP_OBJ_TO_PTR(MP_STATE_PORT(proxy_c_ref)))->items[c_ref];
+}
+
+void proxy_c_free_obj(uint32_t c_ref) {
+    if (c_ref >= PROXY_C_REF_NUM_STATIC) {
+        // Remove the object from proxy_c_dict if the c_ref in that dict corresponds to this object.
+        // (It may be that this object exists in the dict but with a different c_ref from a more
+        // recent proxy of this object.)
+        mp_obj_t obj_key = mp_obj_new_int_from_uint((uintptr_t)proxy_c_get_obj(c_ref));
+        mp_map_elem_t *elem = mp_map_lookup(mp_obj_dict_get_map(MP_STATE_PORT(proxy_c_dict)), obj_key, MP_MAP_LOOKUP);
+        if (elem != NULL && mp_obj_int_get_truncated(elem->value) == c_ref) {
+            mp_map_lookup(mp_obj_dict_get_map(MP_STATE_PORT(proxy_c_dict)), obj_key, MP_MAP_LOOKUP_REMOVE_IF_FOUND);
+        }
+
+        // Clear the slot in proxy_c_ref used by this object, so the GC can reclaim the object.
+        ((mp_obj_list_t *)MP_OBJ_TO_PTR(MP_STATE_PORT(proxy_c_ref)))->items[c_ref] = MP_OBJ_NULL;
+        proxy_c_ref_next = MIN(proxy_c_ref_next, c_ref);
+    }
+}
+
+mp_obj_t proxy_convert_js_to_mp_obj_cside(uint32_t *value) {
+    if (value[0] == PROXY_KIND_JS_UNDEFINED) {
+        return mp_const_undefined;
+    } else if (value[0] == PROXY_KIND_JS_NULL) {
+        return mp_const_none;
+    } else if (value[0] == PROXY_KIND_JS_BOOLEAN) {
+        return mp_obj_new_bool(value[1]);
+    } else if (value[0] == PROXY_KIND_JS_INTEGER) {
+        return mp_obj_new_int(value[1]);
+    } else if (value[0] == PROXY_KIND_JS_DOUBLE) {
+        return mp_obj_new_float_from_d(*(double *)&value[1]);
+    } else if (value[0] == PROXY_KIND_JS_STRING) {
+        mp_obj_t s = mp_obj_new_str((void *)value[2], value[1]);
+        free((void *)value[2]);
+        return s;
+    } else if (value[0] == PROXY_KIND_JS_PYPROXY) {
+        return proxy_c_get_obj(value[1]);
+    } else if (value[0] == PROXY_KIND_JS_OBJECT_EXISTING) {
+        return mp_obj_get_jsproxy(value[1]);
+    } else {
+        // PROXY_KIND_JS_OBJECT
+        return mp_obj_new_jsproxy(value[1]);
+    }
+}
+
+void proxy_convert_mp_to_js_obj_cside(mp_obj_t obj, uint32_t *out) {
+    uint32_t kind;
+    int js_ref;
+    if (obj == MP_OBJ_NULL) {
+        kind = PROXY_KIND_MP_NULL;
+    } else if (obj == mp_const_none) {
+        kind = PROXY_KIND_MP_NONE;
+    } else if (mp_obj_is_bool(obj)) {
+        kind = PROXY_KIND_MP_BOOL;
+        out[1] = mp_obj_is_true(obj);
+    } else if (mp_obj_is_int(obj)) {
+        kind = PROXY_KIND_MP_INT;
+        out[1] = mp_obj_get_int_truncated(obj); // TODO support big int
+    } else if (mp_obj_is_float(obj)) {
+        kind = PROXY_KIND_MP_FLOAT;
+        *(double *)&out[1] = mp_obj_get_float(obj);
+    } else if (mp_obj_is_str(obj)) {
+        kind = PROXY_KIND_MP_STR;
+        size_t len;
+        const char *str = mp_obj_str_get_data(obj, &len);
+        out[1] = len;
+        out[2] = (uintptr_t)str;
+    } else if (obj == mp_const_undefined) {
+        kind = PROXY_KIND_MP_JSPROXY;
+        out[1] = MP_OBJ_JSPROXY_REF_UNDEFINED;
+    } else if (mp_obj_is_jsproxy(obj)) {
+        kind = PROXY_KIND_MP_JSPROXY;
+        out[1] = mp_obj_jsproxy_get_ref(obj);
+    } else if ((js_ref = proxy_c_check_existing(obj)) >= 0) {
+        kind = PROXY_KIND_MP_EXISTING;
+        out[1] = js_ref;
+    } else if (mp_obj_get_type(obj) == &mp_type_JsException) {
+        mp_obj_exception_t *exc = MP_OBJ_TO_PTR(obj);
+        if (exc->args->len > 0 && mp_obj_is_jsproxy(exc->args->items[0])) {
+            kind = PROXY_KIND_MP_JSPROXY;
+            out[1] = mp_obj_jsproxy_get_ref(exc->args->items[0]);
+        } else {
+            kind = PROXY_KIND_MP_OBJECT;
+            out[1] = proxy_c_add_obj(obj);
+        }
+    } else {
+        if (mp_obj_is_callable(obj)) {
+            kind = PROXY_KIND_MP_CALLABLE;
+        } else if (mp_obj_is_type(obj, &mp_type_gen_instance)) {
+            kind = PROXY_KIND_MP_GENERATOR;
+        } else {
+            kind = PROXY_KIND_MP_OBJECT;
+        }
+        out[1] = proxy_c_add_obj(obj);
+    }
+    out[0] = kind;
+}
+
+void proxy_convert_mp_to_js_exc_cside(void *exc, uint32_t *out) {
+    out[0] = PROXY_KIND_MP_EXCEPTION;
+    vstr_t vstr;
+    mp_print_t print;
+    vstr_init_print(&vstr, 64, &print);
+    vstr_add_str(&vstr, qstr_str(mp_obj_get_type(MP_OBJ_FROM_PTR(exc))->name));
+    vstr_add_char(&vstr, '\x04');
+    mp_obj_print_exception(&print, MP_OBJ_FROM_PTR(exc));
+    char *s = malloc(vstr_len(&vstr) + 1);
+    memcpy(s, vstr_str(&vstr), vstr_len(&vstr));
+    out[1] = vstr_len(&vstr);
+    out[2] = (uintptr_t)s;
+    vstr_clear(&vstr);
+}
+
+void proxy_c_to_js_call(uint32_t c_ref, uint32_t n_args, uint32_t *args_value, uint32_t *out) {
+    external_call_depth_inc();
+    nlr_buf_t nlr;
+    if (nlr_push(&nlr) == 0) {
+        mp_obj_t args[n_args];
+        for (size_t i = 0; i < n_args; ++i) {
+            args[i] = proxy_convert_js_to_mp_obj_cside(args_value + i * 3);
+        }
+        mp_obj_t obj = proxy_c_get_obj(c_ref);
+        mp_obj_t member = mp_call_function_n_kw(obj, n_args, 0, args);
+        nlr_pop();
+        proxy_convert_mp_to_js_obj_cside(member, out);
+        external_call_depth_dec(member);
+    } else {
+        // uncaught exception
+        proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out);
+        external_call_depth_dec(nlr.ret_val);
+    }
+}
+
+void proxy_c_to_js_dir(uint32_t c_ref, uint32_t *out) {
+    external_call_depth_inc();
+    nlr_buf_t nlr;
+    if (nlr_push(&nlr) == 0) {
+        mp_obj_t obj = proxy_c_get_obj(c_ref);
+        mp_obj_t dir;
+        if (mp_obj_is_dict_or_ordereddict(obj)) {
+            mp_map_t *map = mp_obj_dict_get_map(obj);
+            dir = mp_obj_new_list(0, NULL);
+            for (size_t i = 0; i < map->alloc; i++) {
+                if (mp_map_slot_is_filled(map, i)) {
+                    mp_obj_list_append(dir, map->table[i].key);
+                }
+            }
+        } else {
+            mp_obj_t args[1] = { obj };
+            dir = mp_builtin_dir_obj.fun.var(1, args);
+        }
+        nlr_pop();
+        proxy_convert_mp_to_js_obj_cside(dir, out);
+        external_call_depth_dec(dir);
+    } else {
+        // uncaught exception
+        proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out);
+        external_call_depth_dec(nlr.ret_val);
+    }
+}
+
+bool proxy_c_to_js_has_attr(uint32_t c_ref, const char *attr_in) {
+    mp_obj_t obj = proxy_c_get_obj(c_ref);
+    qstr attr = qstr_from_str(attr_in);
+    if (mp_obj_is_dict_or_ordereddict(obj)) {
+        mp_map_t *map = mp_obj_dict_get_map(obj);
+        mp_map_elem_t *elem = mp_map_lookup(map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP);
+        return elem != NULL;
+    } else {
+        mp_obj_t dest[2];
+        mp_load_method_protected(obj, attr, dest, true);
+        if (dest[0] != MP_OBJ_NULL) {
+            return true;
+        }
+    }
+    return false;
+}
+
+void proxy_c_to_js_lookup_attr(uint32_t c_ref, const char *attr_in, uint32_t *out) {
+    external_call_depth_inc();
+    nlr_buf_t nlr;
+    if (nlr_push(&nlr) == 0) {
+        mp_obj_t obj = proxy_c_get_obj(c_ref);
+        qstr attr = qstr_from_str(attr_in);
+        mp_obj_t member;
+        if (mp_obj_is_dict_or_ordereddict(obj)) {
+            // Lookup the requested attribute as a key in the target dict, and
+            // return `undefined` if not found (instead of raising `KeyError`).
+            mp_obj_dict_t *self = MP_OBJ_TO_PTR(obj);
+            mp_map_elem_t *elem = mp_map_lookup(&self->map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP);
+            if (elem == NULL) {
+                member = mp_const_undefined;
+            } else {
+                member = elem->value;
+            }
+        } else {
+            // Lookup the requested attribute as a member/method of the target object.
+            member = mp_load_attr(obj, attr);
+        }
+        nlr_pop();
+        proxy_convert_mp_to_js_obj_cside(member, out);
+        external_call_depth_dec(member);
+    } else {
+        // uncaught exception
+        proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out);
+        external_call_depth_dec(nlr.ret_val);
+    }
+}
+
+static bool proxy_c_to_js_store_helper(uint32_t c_ref, const char *attr_in, uint32_t *value_in) {
+    external_call_depth_inc();
+    nlr_buf_t nlr;
+    if (nlr_push(&nlr) == 0) {
+        mp_obj_t obj = proxy_c_get_obj(c_ref);
+        qstr attr = qstr_from_str(attr_in);
+
+        mp_obj_t value = MP_OBJ_NULL;
+        if (value_in != NULL) {
+            value = proxy_convert_js_to_mp_obj_cside(value_in);
+        }
+
+        if (mp_obj_is_dict_or_ordereddict(obj)) {
+            if (value == MP_OBJ_NULL) {
+                mp_obj_dict_delete(obj, MP_OBJ_NEW_QSTR(attr));
+            } else {
+                mp_obj_dict_store(obj, MP_OBJ_NEW_QSTR(attr), value);
+            }
+        } else {
+            mp_store_attr(obj, attr, value);
+        }
+        nlr_pop();
+        external_call_depth_dec(MP_OBJ_NULL);
+        return true;
+    } else {
+        // uncaught exception
+        external_call_depth_dec(MP_OBJ_NULL);
+        return false;
+    }
+}
+
+bool proxy_c_to_js_store_attr(uint32_t c_ref, const char *attr_in, uint32_t *value_in) {
+    return proxy_c_to_js_store_helper(c_ref, attr_in, value_in);
+}
+
+bool proxy_c_to_js_delete_attr(uint32_t c_ref, const char *attr_in) {
+    return proxy_c_to_js_store_helper(c_ref, attr_in, NULL);
+}
+
+uint32_t proxy_c_to_js_get_type_and_data(uint32_t c_ref, uint32_t *out) {
+    mp_obj_t obj = proxy_c_get_obj(c_ref);
+    const mp_obj_type_t *type = mp_obj_get_type(obj);
+    mp_buffer_info_t bufinfo;
+    if (type == &mp_type_tuple || type == &mp_type_list) {
+        size_t len;
+        mp_obj_t *items;
+        mp_obj_get_array(obj, &len, &items);
+        out[0] = len;
+        out[1] = (uintptr_t)items;
+        if (type == &mp_type_tuple) {
+            return 1;
+        } else {
+            return 2;
+        }
+    } else if (type == &mp_type_dict) {
+        mp_map_t *map = mp_obj_dict_get_map(obj);
+        out[0] = map->alloc;
+        out[1] = (uintptr_t)map->table;
+        return 3;
+    } else if (mp_get_buffer(obj, &bufinfo, MP_BUFFER_READ)) {
+        out[0] = bufinfo.len;
+        out[1] = (uintptr_t)bufinfo.buf;
+        return 4;
+    } else {
+        return 0;
+    }
+}
+
+EM_JS(void, js_get_error_info, (int jsref, uint32_t * out_name, uint32_t * out_message), {
+    const error = proxy_js_ref[jsref];
+    proxy_convert_js_to_mp_obj_jsside(error.name, out_name);
+    proxy_convert_js_to_mp_obj_jsside(error.message, out_message);
+});
+
+mp_obj_t mp_obj_jsproxy_make_js_exception(mp_obj_t error) {
+    uint32_t out_name[PVN];
+    uint32_t out_message[PVN];
+    js_get_error_info(mp_obj_jsproxy_get_ref(error), out_name, out_message);
+    mp_obj_t args[3] = {
+        error,
+        proxy_convert_js_to_mp_obj_cside(out_name),
+        proxy_convert_js_to_mp_obj_cside(out_message),
+    };
+    return mp_obj_new_exception_args(&mp_type_JsException, MP_ARRAY_SIZE(args), args);
+}
+
+/******************************************************************************/
+// Bridge Python iterator to JavaScript iterator protocol.
+
+uint32_t proxy_c_to_js_get_iter(uint32_t c_ref) {
+    mp_obj_t obj = proxy_c_get_obj(c_ref);
+    mp_obj_t iter = mp_getiter(obj, NULL);
+    return proxy_c_add_obj(iter);
+}
+
+bool proxy_c_to_js_iternext(uint32_t c_ref, uint32_t *out) {
+    external_call_depth_inc();
+    nlr_buf_t nlr;
+    if (nlr_push(&nlr) == 0) {
+        mp_obj_t obj = proxy_c_get_obj(c_ref);
+        mp_obj_t iter = mp_iternext_allow_raise(obj);
+        if (iter == MP_OBJ_STOP_ITERATION) {
+            external_call_depth_dec(MP_OBJ_NULL);
+            nlr_pop();
+            return false;
+        }
+        nlr_pop();
+        proxy_convert_mp_to_js_obj_cside(iter, out);
+        external_call_depth_dec(iter);
+        return true;
+    } else {
+        if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(((mp_obj_base_t *)nlr.ret_val)->type), MP_OBJ_FROM_PTR(&mp_type_StopIteration))) {
+            external_call_depth_dec(MP_OBJ_NULL);
+            return false;
+        } else {
+            // uncaught exception
+            proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out);
+            external_call_depth_dec(nlr.ret_val);
+            return true;
+        }
+    }
+}
+
+/******************************************************************************/
+// Bridge Python generator to JavaScript thenable.
+
+static const mp_obj_fun_builtin_var_t resume_obj;
+
+EM_JS(void, js_then_resolve, (uint32_t * ret_value, uint32_t * resolve), {
+    const ret_value_js = proxy_convert_mp_to_js_obj_jsside(ret_value);
+    const resolve_js = proxy_convert_mp_to_js_obj_jsside(resolve);
+    resolve_js(ret_value_js);
+});
+
+EM_JS(void, js_then_reject, (uint32_t * ret_value, uint32_t * reject), {
+    // The ret_value object should be a Python exception.  Convert it to a
+    // JavaScript PythonError and pass it as the reason to reject the promise.
+    let ret_value_js;
+    try {
+        ret_value_js = proxy_convert_mp_to_js_obj_jsside(ret_value);
+    } catch(error) {
+        ret_value_js = error;
+    }
+    const reject_js = proxy_convert_mp_to_js_obj_jsside(reject);
+    reject_js(ret_value_js);
+});
+
+// *FORMAT-OFF*
+EM_JS(void, js_then_continue, (int jsref, uint32_t * py_resume, uint32_t * resolve, uint32_t * reject, uint32_t * out), {
+    const py_resume_js = proxy_convert_mp_to_js_obj_jsside(py_resume);
+    const resolve_js = proxy_convert_mp_to_js_obj_jsside(resolve);
+    const reject_js = proxy_convert_mp_to_js_obj_jsside(reject);
+    const ret = proxy_js_ref[jsref].then(
+        (result) => {
+            // The Promise is fulfilled on the JavaScript side.  Take the result and
+            // send it to the encapsulating generator on the Python side, so it
+            // becomes the result of the "yield from" that deferred to this Promise.
+            py_resume_js(result, null, resolve_js, reject_js);
+        },
+        (reason) => {
+            // The Promise is rejected on the JavaScript side.  Take the reason and
+            // throw it into the encapsulating generator on the Python side.
+            py_resume_js(null, reason, resolve_js, reject_js);
+        },
+    );
+    proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+// *FORMAT-ON*
+
+EM_JS(void, create_promise, (uint32_t * out_set, uint32_t * out_promise), {
+    const out_set_js = proxy_convert_mp_to_js_obj_jsside(out_set);
+    const promise = new Promise(out_set_js);
+    proxy_convert_js_to_mp_obj_jsside(promise, out_promise);
+});
+
+static mp_obj_t proxy_resume_execute(mp_obj_t self_in, mp_obj_t send_value, mp_obj_t throw_value, mp_obj_t resolve, mp_obj_t reject) {
+    if (throw_value != MP_OBJ_NULL && throw_value != mp_const_none) {
+        if (send_value == mp_const_none) {
+            send_value = MP_OBJ_NULL;
+        }
+        // Ensure that the `throw_value` is a proper Python exception instance.
+        if (mp_obj_is_jsproxy(throw_value)) {
+            throw_value = mp_obj_jsproxy_make_js_exception(throw_value);
+        } else {
+            throw_value = mp_make_raise_obj(throw_value);
+        }
+    } else {
+        throw_value = MP_OBJ_NULL;
+        if (send_value == mp_const_undefined) {
+            send_value = mp_const_none;
+        }
+    }
+
+    mp_obj_t ret_value;
+    mp_vm_return_kind_t ret_kind = mp_resume(self_in, send_value, throw_value, &ret_value);
+
+    if (ret_kind == MP_VM_RETURN_NORMAL) {
+        uint32_t out_ret_value[PVN];
+        uint32_t out_resolve[PVN];
+        proxy_convert_mp_to_js_obj_cside(ret_value, out_ret_value);
+        proxy_convert_mp_to_js_obj_cside(resolve, out_resolve);
+        js_then_resolve(out_ret_value, out_resolve);
+        return mp_const_none;
+    } else if (ret_kind == MP_VM_RETURN_YIELD) {
+        // If ret_value is None then there has been a top-level await of an asyncio primitive.
+        // Otherwise, ret_value should be a JS thenable.
+
+        if (ret_value == mp_const_none) {
+            // Waiting on an asyncio primitive to complete, eg a Task or Event.
+            //
+            // Completion of this primitive will occur when the asyncio.core._top_level_task
+            // Task is made runable and its coroutine's send() method is called.  Need to
+            // construct a Promise that resolves when that send() method is called, because
+            // that will resume the top-level await from the JavaScript side.
+            //
+            // This is accomplished via the asyncio.core.TopLevelCoro class and its methods.
+            mp_obj_t asyncio = mp_import_name(MP_QSTR_asyncio_dot_core, mp_const_none, MP_OBJ_NEW_SMALL_INT(0));
+            mp_obj_t asyncio_core = mp_load_attr(asyncio, MP_QSTR_core);
+            mp_obj_t top_level_coro = mp_load_attr(asyncio_core, MP_QSTR_TopLevelCoro);
+            mp_obj_t top_level_coro_set = mp_load_attr(top_level_coro, MP_QSTR_set);
+            uint32_t out_set[PVN];
+            proxy_convert_mp_to_js_obj_cside(top_level_coro_set, out_set);
+            uint32_t out_promise[PVN];
+            create_promise(out_set, out_promise);
+            ret_value = proxy_convert_js_to_mp_obj_cside(out_promise);
+        }
+
+        mp_obj_t py_resume = mp_obj_new_bound_meth(MP_OBJ_FROM_PTR(&resume_obj), self_in);
+        int ref = mp_obj_jsproxy_get_ref(ret_value);
+        uint32_t out_py_resume[PVN];
+        uint32_t out_resolve[PVN];
+        uint32_t out_reject[PVN];
+        proxy_convert_mp_to_js_obj_cside(py_resume, out_py_resume);
+        proxy_convert_mp_to_js_obj_cside(resolve, out_resolve);
+        proxy_convert_mp_to_js_obj_cside(reject, out_reject);
+        uint32_t out[PVN];
+        js_then_continue(ref, out_py_resume, out_resolve, out_reject, out);
+        return proxy_convert_js_to_mp_obj_cside(out);
+    } else { // ret_kind == MP_VM_RETURN_EXCEPTION;
+        // Pass the exception through as an object to reject the promise (don't raise/throw it).
+        uint32_t out_ret_value[PVN];
+        uint32_t out_reject[PVN];
+        proxy_convert_mp_to_js_exc_cside(ret_value, out_ret_value);
+        proxy_convert_mp_to_js_obj_cside(reject, out_reject);
+        js_then_reject(out_ret_value, out_reject);
+        return mp_const_none;
+    }
+}
+
+static mp_obj_t resume_fun(size_t n_args, const mp_obj_t *args) {
+    return proxy_resume_execute(args[0], args[1], args[2], args[3], args[4]);
+}
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(resume_obj, 5, 5, resume_fun);
+
+void proxy_c_to_js_resume(uint32_t c_ref, uint32_t *args) {
+    external_call_depth_inc();
+    nlr_buf_t nlr;
+    if (nlr_push(&nlr) == 0) {
+        mp_obj_t obj = proxy_c_get_obj(c_ref);
+        mp_obj_t resolve = proxy_convert_js_to_mp_obj_cside(args + 1 * 3);
+        mp_obj_t reject = proxy_convert_js_to_mp_obj_cside(args + 2 * 3);
+        mp_obj_t ret = proxy_resume_execute(obj, mp_const_none, mp_const_none, resolve, reject);
+        nlr_pop();
+        proxy_convert_mp_to_js_obj_cside(ret, args);
+        external_call_depth_dec(ret);
+    } else {
+        // uncaught exception
+        proxy_convert_mp_to_js_exc_cside(nlr.ret_val, args);
+        external_call_depth_dec(nlr.ret_val);
+    }
+}
diff --git a/ports/webassembly/proxy_c.h b/ports/webassembly/proxy_c.h
new file mode 100644
index 00000000000..ae2b4213ad2
--- /dev/null
+++ b/ports/webassembly/proxy_c.h
@@ -0,0 +1,68 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+#ifndef MICROPY_INCLUDED_WEBASSEMBLY_PROXY_C_H
+#define MICROPY_INCLUDED_WEBASSEMBLY_PROXY_C_H
+
+#include "py/obj.h"
+
+// Fixed JsProxy references.
+#define MP_OBJ_JSPROXY_REF_GLOBAL_THIS (0)
+#define MP_OBJ_JSPROXY_REF_UNDEFINED (1)
+
+// proxy value number of items
+#define PVN (3)
+
+typedef struct _mp_obj_jsproxy_t {
+    mp_obj_base_t base;
+    int ref;
+    bool bind_to_self;
+} mp_obj_jsproxy_t;
+
+extern const mp_obj_type_t mp_type_jsproxy;
+extern const mp_obj_type_t mp_type_JsException;
+
+void external_call_depth_inc(void);
+void external_call_depth_dec(mp_obj_t root_obj);
+
+void proxy_c_init(void);
+mp_obj_t proxy_convert_js_to_mp_obj_cside(uint32_t *value);
+void proxy_convert_mp_to_js_obj_cside(mp_obj_t obj, uint32_t *out);
+void proxy_convert_mp_to_js_exc_cside(void *exc, uint32_t *out);
+
+mp_obj_t mp_obj_new_jsproxy(int ref);
+mp_obj_t mp_obj_get_jsproxy(int ref);
+void mp_obj_jsproxy_global_this_attr(qstr attr, mp_obj_t *dest);
+
+static inline bool mp_obj_is_jsproxy(mp_obj_t o) {
+    return mp_obj_get_type(o) == &mp_type_jsproxy;
+}
+
+static inline int mp_obj_jsproxy_get_ref(mp_obj_t o) {
+    mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(o);
+    return self->ref;
+}
+
+#endif // MICROPY_INCLUDED_WEBASSEMBLY_PROXY_C_H
diff --git a/ports/webassembly/proxy_js.js b/ports/webassembly/proxy_js.js
new file mode 100644
index 00000000000..60b832d81fd
--- /dev/null
+++ b/ports/webassembly/proxy_js.js
@@ -0,0 +1,321 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+// Number of static entries at the start of proxy_js_ref.
+const PROXY_JS_REF_NUM_STATIC = 2;
+
+// These constants should match the constants in proxy_c.c.
+
+const PROXY_KIND_MP_EXCEPTION = -1;
+const PROXY_KIND_MP_NULL = 0;
+const PROXY_KIND_MP_NONE = 1;
+const PROXY_KIND_MP_BOOL = 2;
+const PROXY_KIND_MP_INT = 3;
+const PROXY_KIND_MP_FLOAT = 4;
+const PROXY_KIND_MP_STR = 5;
+const PROXY_KIND_MP_CALLABLE = 6;
+const PROXY_KIND_MP_GENERATOR = 7;
+const PROXY_KIND_MP_OBJECT = 8;
+const PROXY_KIND_MP_JSPROXY = 9;
+const PROXY_KIND_MP_EXISTING = 10;
+
+const PROXY_KIND_JS_UNDEFINED = 0;
+const PROXY_KIND_JS_NULL = 1;
+const PROXY_KIND_JS_BOOLEAN = 2;
+const PROXY_KIND_JS_INTEGER = 3;
+const PROXY_KIND_JS_DOUBLE = 4;
+const PROXY_KIND_JS_STRING = 5;
+const PROXY_KIND_JS_OBJECT_EXISTING = 6;
+const PROXY_KIND_JS_OBJECT = 7;
+const PROXY_KIND_JS_PYPROXY = 8;
+
+class PythonError extends Error {
+    constructor(exc_type, exc_details) {
+        super(exc_details);
+        this.name = "PythonError";
+        this.type = exc_type;
+    }
+}
+
+function proxy_js_init() {
+    globalThis.proxy_js_ref = [globalThis, undefined];
+    globalThis.proxy_js_ref_next = PROXY_JS_REF_NUM_STATIC;
+    globalThis.proxy_js_ref_map = new Map();
+    globalThis.proxy_js_ref_map.set(globalThis, 0);
+    globalThis.proxy_js_map = new Map();
+    globalThis.proxy_js_existing = [undefined];
+    globalThis.pyProxyFinalizationRegistry = new FinalizationRegistry(
+        (cRef) => {
+            globalThis.proxy_js_map.delete(cRef);
+            Module.ccall("proxy_c_free_obj", "null", ["number"], [cRef]);
+        },
+    );
+}
+
+// Check if the c_ref (Python proxy index) has a corresponding JavaScript-side PyProxy
+// associated with it.  If so, take a concrete reference to this PyProxy from the WeakRef
+// and put it in proxy_js_existing, to be referenced and reused by PROXY_KIND_MP_EXISTING.
+function proxy_js_check_existing(c_ref) {
+    const existing_obj = globalThis.proxy_js_map.get(c_ref)?.deref();
+    if (existing_obj === undefined) {
+        return -1;
+    }
+
+    // Search for a free slot in proxy_js_existing.
+    for (let i = 0; i < globalThis.proxy_js_existing.length; ++i) {
+        if (globalThis.proxy_js_existing[i] === undefined) {
+            // Free slot found, put existing_obj here and return the index.
+            globalThis.proxy_js_existing[i] = existing_obj;
+            return i;
+        }
+    }
+
+    // No free slot, so append to proxy_js_existing and return the new index.
+    globalThis.proxy_js_existing.push(existing_obj);
+    return globalThis.proxy_js_existing.length - 1;
+}
+
+// The `js_obj` argument cannot be `undefined`.
+// Returns an integer reference to the given `js_obj`.
+function proxy_js_add_obj(js_obj) {
+    // Search for the first free slot in proxy_js_ref.
+    while (proxy_js_ref_next < proxy_js_ref.length) {
+        if (proxy_js_ref[proxy_js_ref_next] === undefined) {
+            // Free slot found, reuse it.
+            const id = proxy_js_ref_next;
+            ++proxy_js_ref_next;
+            proxy_js_ref[id] = js_obj;
+            proxy_js_ref_map.set(js_obj, id);
+            return id;
+        }
+        ++proxy_js_ref_next;
+    }
+
+    // No free slots, so grow proxy_js_ref by one (append at the end of the array).
+    const id = proxy_js_ref.length;
+    proxy_js_ref[id] = js_obj;
+    proxy_js_ref_next = proxy_js_ref.length;
+    proxy_js_ref_map.set(js_obj, id);
+    return id;
+}
+
+function proxy_call_python(target, argumentsList) {
+    let args = 0;
+
+    // Strip trailing "undefined" arguments.
+    while (
+        argumentsList.length > 0 &&
+        argumentsList[argumentsList.length - 1] === undefined
+    ) {
+        argumentsList.pop();
+    }
+
+    if (argumentsList.length > 0) {
+        // TODO use stackAlloc/stackRestore?
+        args = Module._malloc(argumentsList.length * 3 * 4);
+        for (const i in argumentsList) {
+            proxy_convert_js_to_mp_obj_jsside(
+                argumentsList[i],
+                args + i * 3 * 4,
+            );
+        }
+    }
+    const value = Module._malloc(3 * 4);
+    Module.ccall(
+        "proxy_c_to_js_call",
+        "null",
+        ["number", "number", "number", "pointer"],
+        [target, argumentsList.length, args, value],
+    );
+    if (argumentsList.length > 0) {
+        Module._free(args);
+    }
+    const ret = proxy_convert_mp_to_js_obj_jsside_with_free(value);
+    if (ret instanceof PyProxyThenable) {
+        // In Python when an async function is called it creates the
+        // corresponding "generator", which must then be executed at
+        // the top level by an asyncio-like scheduler.  In JavaScript
+        // the semantics for async functions is that they are started
+        // immediately (their non-async prefix code is executed immediately)
+        // and only if they await do they return a Promise to delay the
+        // execution of the remainder of the function.
+        //
+        // Emulate the JavaScript behaviour here by resolving the Python
+        // async function.  We assume that the caller who gets this
+        // return is JavaScript.
+        return Promise.resolve(ret);
+    }
+    return ret;
+}
+
+function proxy_convert_js_to_mp_obj_jsside_helper(js_obj, out, allow_pyproxy) {
+    let kind;
+    if (js_obj === undefined) {
+        kind = PROXY_KIND_JS_UNDEFINED;
+    } else if (js_obj === null) {
+        kind = PROXY_KIND_JS_NULL;
+    } else if (typeof js_obj === "boolean") {
+        kind = PROXY_KIND_JS_BOOLEAN;
+        Module.setValue(out + 4, js_obj, "i32");
+    } else if (typeof js_obj === "number") {
+        if (Number.isInteger(js_obj)) {
+            kind = PROXY_KIND_JS_INTEGER;
+            Module.setValue(out + 4, js_obj, "i32");
+        } else {
+            kind = PROXY_KIND_JS_DOUBLE;
+            // double must be stored to an address that's a multiple of 8
+            const temp = (out + 4) & ~7;
+            Module.setValue(temp, js_obj, "double");
+            const double_lo = Module.getValue(temp, "i32");
+            const double_hi = Module.getValue(temp + 4, "i32");
+            Module.setValue(out + 4, double_lo, "i32");
+            Module.setValue(out + 8, double_hi, "i32");
+        }
+    } else if (typeof js_obj === "string") {
+        kind = PROXY_KIND_JS_STRING;
+        const len = Module.lengthBytesUTF8(js_obj);
+        const buf = Module._malloc(len + 1);
+        Module.stringToUTF8(js_obj, buf, len + 1);
+        Module.setValue(out + 4, len, "i32");
+        Module.setValue(out + 8, buf, "i32");
+    } else if (
+        allow_pyproxy &&
+        (js_obj instanceof PyProxy ||
+            (typeof js_obj === "function" && "_ref" in js_obj) ||
+            js_obj instanceof PyProxyThenable)
+    ) {
+        kind = PROXY_KIND_JS_PYPROXY;
+        Module.setValue(out + 4, js_obj._ref, "i32");
+    } else {
+        let id;
+        // See if there is an existing JsProxy reference, and use that if there is.
+        const existing_ref = proxy_js_ref_map.get(js_obj);
+        if (existing_ref !== undefined) {
+            kind = PROXY_KIND_JS_OBJECT_EXISTING;
+            id = existing_ref;
+        } else {
+            kind = PROXY_KIND_JS_OBJECT;
+            id = proxy_js_add_obj(js_obj);
+        }
+        Module.setValue(out + 4, id, "i32");
+    }
+    Module.setValue(out + 0, kind, "i32");
+}
+
+function proxy_convert_js_to_mp_obj_jsside(js_obj, out) {
+    proxy_convert_js_to_mp_obj_jsside_helper(js_obj, out, true);
+}
+
+function proxy_convert_js_to_mp_obj_jsside_force_double_proxy(js_obj, out) {
+    proxy_convert_js_to_mp_obj_jsside_helper(js_obj, out, false);
+}
+
+function proxy_convert_mp_to_js_obj_jsside(value) {
+    const kind = Module.getValue(value, "i32");
+    let obj;
+    if (kind === PROXY_KIND_MP_EXCEPTION) {
+        // Exception
+        const str_len = Module.getValue(value + 4, "i32");
+        const str_ptr = Module.getValue(value + 8, "i32");
+        const str = Module.UTF8ToString(str_ptr, str_len);
+        Module._free(str_ptr);
+        const str_split = str.split("\x04");
+        throw new PythonError(str_split[0], str_split[1]);
+    }
+    if (kind === PROXY_KIND_MP_NULL) {
+        // MP_OBJ_NULL
+        throw new Error("NULL object");
+    }
+    if (kind === PROXY_KIND_MP_NONE) {
+        // None
+        obj = null;
+    } else if (kind === PROXY_KIND_MP_BOOL) {
+        // bool
+        obj = Module.getValue(value + 4, "i32") ? true : false;
+    } else if (kind === PROXY_KIND_MP_INT) {
+        // int
+        obj = Module.getValue(value + 4, "i32");
+    } else if (kind === PROXY_KIND_MP_FLOAT) {
+        // float
+        // double must be loaded from an address that's a multiple of 8
+        const temp = (value + 4) & ~7;
+        const double_lo = Module.getValue(value + 4, "i32");
+        const double_hi = Module.getValue(value + 8, "i32");
+        Module.setValue(temp, double_lo, "i32");
+        Module.setValue(temp + 4, double_hi, "i32");
+        obj = Module.getValue(temp, "double");
+    } else if (kind === PROXY_KIND_MP_STR) {
+        // str
+        const str_len = Module.getValue(value + 4, "i32");
+        const str_ptr = Module.getValue(value + 8, "i32");
+        obj = Module.UTF8ToString(str_ptr, str_len);
+    } else if (kind === PROXY_KIND_MP_JSPROXY) {
+        // js proxy
+        const id = Module.getValue(value + 4, "i32");
+        obj = proxy_js_ref[id];
+    } else if (kind === PROXY_KIND_MP_EXISTING) {
+        const id = Module.getValue(value + 4, "i32");
+        obj = globalThis.proxy_js_existing[id];
+        globalThis.proxy_js_existing[id] = undefined;
+    } else {
+        // obj
+        const id = Module.getValue(value + 4, "i32");
+        if (kind === PROXY_KIND_MP_CALLABLE) {
+            obj = (...args) => {
+                return proxy_call_python(id, args);
+            };
+            obj._ref = id;
+        } else if (kind === PROXY_KIND_MP_GENERATOR) {
+            obj = new PyProxyThenable(id);
+        } else {
+            // PROXY_KIND_MP_OBJECT
+            const target = new PyProxy(id);
+            obj = new Proxy(target, py_proxy_handler);
+        }
+        globalThis.pyProxyFinalizationRegistry.register(obj, id);
+        globalThis.proxy_js_map.set(id, new WeakRef(obj));
+    }
+    return obj;
+}
+
+function proxy_convert_mp_to_js_obj_jsside_with_free(value) {
+    const ret = proxy_convert_mp_to_js_obj_jsside(value);
+    Module._free(value);
+    return ret;
+}
+
+function python_index_semantics(target, index_in) {
+    let index = index_in;
+    if (typeof index === "number") {
+        if (index < 0) {
+            index += target.length;
+        }
+        if (index < 0 || index >= target.length) {
+            throw new PythonError("IndexError", "index out of range");
+        }
+    }
+    return index;
+}
diff --git a/ports/webassembly/qstrdefsport.h b/ports/webassembly/qstrdefsport.h
index 00d3e2ae3c5..421344bd494 100644
--- a/ports/webassembly/qstrdefsport.h
+++ b/ports/webassembly/qstrdefsport.h
@@ -1,2 +1,4 @@
 // qstrs specific to this port
 // *FORMAT-OFF*
+Q(/lib)
+Q(asyncio.core)
diff --git a/ports/webassembly/variants/manifest.py b/ports/webassembly/variants/manifest.py
new file mode 100644
index 00000000000..b2ee8cd640d
--- /dev/null
+++ b/ports/webassembly/variants/manifest.py
@@ -0,0 +1,24 @@
+# The asyncio package is built from the standard implementation but with the
+# core scheduler replaced with a custom scheduler that uses the JavaScript
+# runtime (with setTimeout an Promise's) to contrtol the scheduling.
+
+package(
+    "asyncio",
+    (
+        "event.py",
+        "funcs.py",
+        "lock.py",
+    ),
+    base_path="$(MPY_DIR)/extmod",
+    opt=3,
+)
+
+package(
+    "asyncio",
+    (
+        "__init__.py",
+        "core.py",
+    ),
+    base_path="$(PORT_DIR)",
+    opt=3,
+)
diff --git a/ports/webassembly/variants/pyscript/manifest.py b/ports/webassembly/variants/pyscript/manifest.py
new file mode 100644
index 00000000000..db088e70d0c
--- /dev/null
+++ b/ports/webassembly/variants/pyscript/manifest.py
@@ -0,0 +1,29 @@
+include("$(PORT_DIR)/variants/manifest.py")
+
+require("abc")
+require("base64")
+require("collections")
+require("collections-defaultdict")
+require("copy")
+require("datetime")
+require("fnmatch")
+require("functools")
+require("gzip")
+require("hmac")
+require("html")
+require("inspect")
+require("io")
+require("itertools")
+require("locale")
+require("logging")
+require("operator")
+require("os")
+require("os-path")
+require("pathlib")
+require("stat")
+require("tarfile")
+require("tarfile-write")
+require("time")
+require("unittest")
+require("uu")
+require("zlib")
diff --git a/ports/webassembly/variants/pyscript/mpconfigvariant.h b/ports/webassembly/variants/pyscript/mpconfigvariant.h
new file mode 100644
index 00000000000..0b77efc4b32
--- /dev/null
+++ b/ports/webassembly/variants/pyscript/mpconfigvariant.h
@@ -0,0 +1,4 @@
+#define MICROPY_CONFIG_ROM_LEVEL                (MICROPY_CONFIG_ROM_LEVEL_FULL_FEATURES)
+#define MICROPY_GC_SPLIT_HEAP                   (1)
+#define MICROPY_GC_SPLIT_HEAP_AUTO              (1)
+#define MICROPY_PY_WEAKREF                      (1)
diff --git a/ports/webassembly/variants/pyscript/mpconfigvariant.mk b/ports/webassembly/variants/pyscript/mpconfigvariant.mk
new file mode 100644
index 00000000000..016b96a99af
--- /dev/null
+++ b/ports/webassembly/variants/pyscript/mpconfigvariant.mk
@@ -0,0 +1,3 @@
+JSFLAGS += -s ALLOW_MEMORY_GROWTH
+
+FROZEN_MANIFEST ?= variants/pyscript/manifest.py
diff --git a/ports/webassembly/variants/standard/mpconfigvariant.h b/ports/webassembly/variants/standard/mpconfigvariant.h
new file mode 100644
index 00000000000..7be62ea7f4f
--- /dev/null
+++ b/ports/webassembly/variants/standard/mpconfigvariant.h
@@ -0,0 +1 @@
+#define MICROPY_VARIANT_ENABLE_JS_HOOK (1)
diff --git a/ports/webassembly/variants/standard/mpconfigvariant.mk b/ports/webassembly/variants/standard/mpconfigvariant.mk
new file mode 100644
index 00000000000..62ee161907d
--- /dev/null
+++ b/ports/webassembly/variants/standard/mpconfigvariant.mk
@@ -0,0 +1 @@
+JSFLAGS += -s ASYNCIFY
diff --git a/ports/webassembly/wrapper.js b/ports/webassembly/wrapper.js
deleted file mode 100644
index 981ade5d801..00000000000
--- a/ports/webassembly/wrapper.js
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * This file is part of the MicroPython project, http://micropython.org/
- *
- * The MIT License (MIT)
- *
- * Copyright (c) 2017, 2018 Rami Ali
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-var Module = {};
-
-var mainProgram = function()
-{
-  mp_js_init = Module.cwrap('mp_js_init', 'null', ['number']);
-  mp_js_do_str = Module.cwrap('mp_js_do_str', 'number', ['string']);
-  mp_js_init_repl = Module.cwrap('mp_js_init_repl', 'null', ['null']);
-  mp_js_process_char = Module.cwrap('mp_js_process_char', 'number', ['number']);
-
-  MP_JS_EPOCH = Date.now();
-
-  if (typeof window === 'undefined' && require.main === module) {
-      var fs = require('fs');
-      var heap_size = 128 * 1024;
-      var contents = '';
-      var repl = true;
-
-      for (var i = 0; i < process.argv.length; i++) {
-          if (process.argv[i] === '-X' && i < process.argv.length - 1) {
-              if (process.argv[i + 1].includes('heapsize=')) {
-                  heap_size = parseInt(process.argv[i + 1].split('heapsize=')[1]);
-                  if (process.argv[i + 1].substr(-1).toLowerCase() === 'k') {
-                      heap_size *= 1024;
-                  } else if (process.argv[i + 1].substr(-1).toLowerCase() === 'm') {
-                      heap_size *= 1024 * 1024;
-                  }
-              }
-          } else if (process.argv[i].includes('.py')) {
-              contents += fs.readFileSync(process.argv[i], 'utf8');
-              repl = false;;
-          }
-      }
-
-      if (process.stdin.isTTY === false) {
-          contents = fs.readFileSync(0, 'utf8');
-          repl = 0;
-      }
-
-      mp_js_init(heap_size);
-
-      if (repl) {
-          mp_js_init_repl();
-          process.stdin.setRawMode(true);
-          process.stdin.on('data', function (data) {
-              for (var i = 0; i < data.length; i++) {
-                  if (mp_js_process_char(data[i])) {
-                      process.exit()
-                  }
-              }
-          });
-      } else {
-          process.exitCode = mp_js_do_str(contents);
-      }
-  }
-}
-
-Module["onRuntimeInitialized"] = mainProgram;
diff --git a/ports/windows/.appveyor.yml b/ports/windows/.appveyor.yml
deleted file mode 100644
index c1a114a0f89..00000000000
--- a/ports/windows/.appveyor.yml
+++ /dev/null
@@ -1,90 +0,0 @@
-image: Visual Studio 2017
-clone_depth: 1
-skip_tags: true
-
-environment:
-  # Python version used
-  MICROPY_CPYTHON3: c:/python38/python.exe
-  # The variants.
-  matrix:
-    - PyVariant: dev
-    - PyVariant: standard
-
-init:
-  # Set build version number to commit to be travis-like
-- ps: Update-AppveyorBuild -Version $env:appveyor_repo_commit.substring(0,8)
-
-configuration:
-- Debug
-- Release
-
-platform:
-- x86
-- x64
-
-matrix:
-  # One debug build is enough.
-  exclude:
-    - configuration: Debug
-      PyVariant: dev
-
-before_build:
-- ps: |
-    @"
-    
-    
-      
-        
-      
-    
-    "@ | Set-Content build.proj
-
-build:
-  project: build.proj
-  parallel: true
-  verbosity: normal
-
-test_script:
-- ps: |
-    $env:MICROPY_MICROPYTHON=(msbuild ports\windows\micropython.vcxproj /nologo /v:m /t:ShowTargetPath).Trim()
-    cd (Join-Path $env:APPVEYOR_BUILD_FOLDER 'tests')
-    & $env:MICROPY_CPYTHON3 run-tests.py
-    if ($LASTEXITCODE -ne 0) {
-      & $env:MICROPY_CPYTHON3 run-tests.py --print-failures
-      throw "Test failure"
-    }
-    & $env:MICROPY_CPYTHON3 run-tests.py --via-mpy -d basics float micropython
-    if ($LASTEXITCODE -ne 0) {
-      & $env:MICROPY_CPYTHON3 run-tests.py --print-failures
-      throw "Test failure"
-    }
-
-# After the build/test phase for the MSVC build completes,
-# build and test with mingw-w64, release versions only.
-after_test:
-- ps: |
-    if ($env:configuration -eq 'Debug') {
-      return
-    }
-    $env:MSYSTEM = if ($platform -eq 'x86') {'MINGW32'} else {'MINGW64'}
-    $env:CHERE_INVOKING = 'enabled_from_arguments'
-    cd (Join-Path $env:APPVEYOR_BUILD_FOLDER 'mpy-cross')
-    C:\msys64\usr\bin\bash.exe -l -c "make -B -j4 V=1"
-    if ($LASTEXITCODE -ne 0) {
-      throw "$env:MSYSTEM mpy_cross build exited with code $LASTEXITCODE"
-    }
-    cd (Join-Path $env:APPVEYOR_BUILD_FOLDER 'ports/windows')
-    C:\msys64\usr\bin\bash.exe -l -c "make -B VARIANT=$($env:PyVariant) submodules"
-    if ($LASTEXITCODE -ne 0) {
-      throw "$env:MSYSTEM build exited with code $LASTEXITCODE"
-    }
-    C:\msys64\usr\bin\bash.exe -l -c "make -B -j4 V=1 MICROPY_MPYCROSS=../../mpy-cross/build/mpy-cross.exe VARIANT=$($env:PyVariant)"
-    if ($LASTEXITCODE -ne 0) {
-      throw "$env:MSYSTEM build exited with code $LASTEXITCODE"
-    }
-    C:\msys64\usr\bin\bash.exe -l -c "make V=1 test_full VARIANT=$($env:PyVariant)"
-    if ($LASTEXITCODE -ne 0) {
-      cd (Join-Path $env:APPVEYOR_BUILD_FOLDER 'tests')
-      & $env:MICROPY_CPYTHON3 run-tests.py --print-failures
-      throw "Test failure"
-    }
diff --git a/ports/windows/Makefile b/ports/windows/Makefile
index 7753fa25d4e..82fa25f21a4 100644
--- a/ports/windows/Makefile
+++ b/ports/windows/Makefile
@@ -29,6 +29,13 @@ PROG ?= micropython
 QSTR_DEFS += ../unix/qstrdefsport.h
 QSTR_GLOBAL_DEPENDENCIES += $(VARIANT_DIR)/mpconfigvariant.h
 
+# Enable building 32-bit code on 64-bit hosts.
+ifeq ($(MICROPY_FORCE_32BIT),1)
+CC += -m32
+CXX += -m32
+LD += -m32
+endif
+
 # include py core make definitions
 include $(TOP)/py/py.mk
 include $(TOP)/extmod/extmod.mk
@@ -42,6 +49,14 @@ INC += -I$(VARIANT_DIR)
 CFLAGS += $(INC) -Wall -Wpointer-arith -Wdouble-promotion -Werror -std=gnu99 -DUNIX -D__USE_MINGW_ANSI_STDIO=1 $(COPT) $(CFLAGS_EXTRA)
 LDFLAGS += -lm -lbcrypt $(LDFLAGS_EXTRA)
 
+# Force the use of 64-bits for file sizes in C library functions on 32-bit platforms.
+# This option has no effect on 64-bit builds.
+CFLAGS += -D_FILE_OFFSET_BITS=64
+
+# Force the use of 64-bits for time_t in C library functions on 32-bit platforms.
+# This option has no effect on 64-bit builds.
+CFLAGS += -D_TIME_BITS=64
+
 # Debugging/Optimization
 ifdef DEBUG
 CFLAGS += -g
@@ -56,7 +71,6 @@ SRC_C = \
 	shared/runtime/gchelper_generic.c \
 	ports/unix/main.c \
 	ports/unix/input.c \
-	ports/unix/modmachine.c \
 	ports/unix/gccollect.c \
 	windows_mphal.c \
 	realpath.c \
@@ -76,26 +90,34 @@ OBJ += $(addprefix $(BUILD)/, $(LIB_SRC_C:.c=.o))
 ifeq ($(MICROPY_USE_READLINE),1)
 CFLAGS += -DMICROPY_USE_READLINE=1
 SRC_C += shared/readline/readline.c
+SRC_C += shared/runtime/pyexec.c
 endif
 
-LIB += -lws2_32
+LIBS += -lws2_32
 
 # List of sources for qstr extraction
 SRC_QSTR += $(SRC_C) $(SRC_CXX) $(SHARED_SRC_C)
-# Append any auto-generated sources that are needed by sources listed in
-# SRC_QSTR
-SRC_QSTR_AUTO_DEPS +=
 
 ifneq ($(FROZEN_MANIFEST),)
-CFLAGS += -DMICROPY_QSTR_EXTRA_POOL=mp_qstr_frozen_const_pool -DMICROPY_MODULE_FROZEN_MPY=1 -DMPZ_DIG_SIZE=16
+CFLAGS += -DMPZ_DIG_SIZE=16
 endif
 
-CXXFLAGS += $(filter-out -std=gnu99,$(CFLAGS))
+ifeq ($(shell $(CC) -dumpmachine),i686-w64-mingw32)
+# GCC disables the SSE instruction set by default on i366 targets and we have
+# to specify all three of these options to enable it.
+# https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html (see -mfpmath=unit section)
+# Enabling the SSE instruction set is necessary to get correct rounding of floating points.
+# https://lemire.me/blog/2020/06/26/gcc-not-nearest
+CFLAGS += -msse -mfpmath=sse -march=pentium4
+endif
 
 include $(TOP)/py/mkrules.mk
 
 .PHONY: test test_full
 
+# Note for recent gcc versions like 13.2:
+# - mingw64-x86_64 gcc builds will pass the math_domain_special test
+# - mingw64-ucrt64 gcc builds will pass all of the below tests
 RUN_TESTS_SKIP += -e math_fun -e float2int_double -e float_parse -e math_domain_special
 
 test: $(BUILD)/$(PROG) $(TOP)/tests/run-tests.py
@@ -105,3 +127,9 @@ test: $(BUILD)/$(PROG) $(TOP)/tests/run-tests.py
 test_full: test
 	$(eval DIRNAME=ports/$(notdir $(CURDIR)))
 	cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(BUILD)/$(PROG) $(PYTHON) ./run-tests.py --via-mpy $(RUN_TESTS_MPY_CROSS_FLAGS) $(RUN_TESTS_SKIP) -d basics float micropython
+
+$(BUILD)/$(PROG): $(BUILD)/micropython.res
+
+$(BUILD)/%.res: %.rc
+	$(ECHO) "WINDRES $<"
+	$(Q)$(WINDRES) $< -O coff -o $@
diff --git a/ports/windows/README.md b/ports/windows/README.md
index 7b5f92f10a4..57ec0e19149 100644
--- a/ports/windows/README.md
+++ b/ports/windows/README.md
@@ -45,7 +45,7 @@ Install MSYS2 from http://repo.msys2.org/distrib, start the msys2.exe shell and
 install the build tools:
 
     pacman -Syuu
-    pacman -S make mingw-w64-x86_64-gcc pkg-config python3
+    pacman -S make mingw-w64-x86_64-gcc pkg-config mingw-w64-x86_64-python3
 
 Start the mingw64.exe shell and build:
 
@@ -135,3 +135,13 @@ For more info, see https://www.winehq.org/docs/wineusr-guide/cui-programs .
 If built without line editing and history capabilities
 (MICROPY_USE_READLINE=0), the resulting binary can be run using the standard
 `wine` tool.
+
+
+Generating the icon file
+------------------------
+The windows builds use a .ico file for the executable logo.
+To generate such file from a .png file use ImageMagick, as was done for the icons in the logo/ directory:
+
+    magick convert vector-logo-2.png -define icon:auto-resize="256,128,96,64,48,32,16" vector-logo-2.ico
+
+Note that for versions prior to 7.0 the command is `convert` instead of `magick convert`.
diff --git a/ports/windows/fmode.c b/ports/windows/fmode.c
index a7976b87e32..128c951ec40 100644
--- a/ports/windows/fmode.c
+++ b/ports/windows/fmode.c
@@ -31,7 +31,7 @@
 
 // Workaround for setting file translation mode: we must distinguish toolsets
 // since mingw has no _set_fmode, and altering msvc's _fmode directly has no effect
-STATIC int set_fmode_impl(int mode) {
+static int set_fmode_impl(int mode) {
     #ifndef _MSC_VER
     _fmode = mode;
     return 0;
diff --git a/ports/windows/micropython.rc b/ports/windows/micropython.rc
new file mode 100644
index 00000000000..8d92bb0d81e
--- /dev/null
+++ b/ports/windows/micropython.rc
@@ -0,0 +1 @@
+app     ICON    "../../logo/vector-logo-2.ico"
diff --git a/ports/windows/micropython.vcxproj b/ports/windows/micropython.vcxproj
index de04975cd49..69ccbfcd6df 100644
--- a/ports/windows/micropython.vcxproj
+++ b/ports/windows/micropython.vcxproj
@@ -87,14 +87,15 @@
   
     
     
+    
     
     
+    
     
     
     
     
     
-    
     
   
   
@@ -103,6 +104,9 @@
     
     
   
+  
+    
+  
   
   
   
diff --git a/ports/windows/mpconfigport.h b/ports/windows/mpconfigport.h
index fd3a76408cd..1edcd0eded1 100644
--- a/ports/windows/mpconfigport.h
+++ b/ports/windows/mpconfigport.h
@@ -72,7 +72,6 @@
 #endif
 #define MICROPY_STREAMS_POSIX_API   (1)
 #define MICROPY_OPT_COMPUTED_GOTO   (0)
-#define MICROPY_MODULE_WEAK_LINKS   (1)
 #define MICROPY_MODULE_OVERRIDE_MAIN_IMPORT (1)
 #define MICROPY_CAN_OVERRIDE_BUILTINS (1)
 #ifndef MICROPY_ENABLE_SCHEDULER
@@ -118,6 +117,8 @@
 #define MICROPY_PY_SYS_STDFILES     (1)
 #define MICROPY_PY_SYS_EXC_INFO     (1)
 #define MICROPY_PY_COLLECTIONS_DEQUE (1)
+#define MICROPY_PY_COLLECTIONS_DEQUE_ITER (1)
+#define MICROPY_PY_COLLECTIONS_DEQUE_SUBSCR (1)
 #define MICROPY_PY_COLLECTIONS_ORDEREDDICT (1)
 #ifndef MICROPY_PY_MATH_SPECIAL_FUNCTIONS
 #define MICROPY_PY_MATH_SPECIAL_FUNCTIONS (1)
@@ -131,34 +132,41 @@
 #define MICROPY_STACKLESS_STRICT    (0)
 #endif
 
-#define MICROPY_PY_UOS              (1)
-#define MICROPY_PY_UOS_INCLUDEFILE  "ports/unix/moduos.c"
-#define MICROPY_PY_UOS_ERRNO        (1)
-#define MICROPY_PY_UOS_GETENV_PUTENV_UNSETENV (1)
-#define MICROPY_PY_UOS_SEP          (1)
-#define MICROPY_PY_UOS_STATVFS      (0)
-#define MICROPY_PY_UOS_SYSTEM       (1)
-#define MICROPY_PY_UOS_URANDOM      (1)
-#define MICROPY_PY_UTIME            (1)
-#define MICROPY_PY_UTIME_TIME_TIME_NS (1)
-#define MICROPY_PY_UTIME_CUSTOM_SLEEP (1)
-#define MICROPY_PY_UTIME_INCLUDEFILE "ports/unix/modutime.c"
-#define MICROPY_PY_UERRNO           (1)
+#define MICROPY_PY_OS               (1)
+#define MICROPY_PY_OS_INCLUDEFILE   "ports/unix/modos.c"
+#define MICROPY_PY_OS_ERRNO         (1)
+#define MICROPY_PY_OS_GETENV_PUTENV_UNSETENV (1)
+#define MICROPY_PY_OS_STATVFS       (0)
+#define MICROPY_PY_OS_SYSTEM        (1)
+#define MICROPY_PY_OS_URANDOM       (1)
+#define MICROPY_PY_TIME             (1)
+#define MICROPY_PY_TIME_TIME_TIME_NS (1)
+#define MICROPY_PY_TIME_CUSTOM_SLEEP (1)
+#define MICROPY_PY_TIME_INCLUDEFILE "ports/unix/modtime.c"
+#define MICROPY_PY_ERRNO            (1)
 #define MICROPY_PY_UCTYPES          (1)
-#define MICROPY_PY_UZLIB            (1)
-#define MICROPY_PY_UJSON            (1)
-#define MICROPY_PY_URE              (1)
-#define MICROPY_PY_UHEAPQ           (1)
-#define MICROPY_PY_UTIMEQ           (1)
-#define MICROPY_PY_UHASHLIB         (1)
-#define MICROPY_PY_UBINASCII        (1)
-#define MICROPY_PY_UBINASCII_CRC32  (1)
-#define MICROPY_PY_URANDOM          (1)
+#define MICROPY_PY_DEFLATE          (1)
+#define MICROPY_PY_DEFLATE_COMPRESS (1)
+#define MICROPY_PY_JSON             (1)
+#define MICROPY_PY_RE               (1)
+#define MICROPY_PY_HEAPQ            (1)
+#define MICROPY_PY_HASHLIB          (1)
+#define MICROPY_PY_BINASCII         (1)
+#define MICROPY_PY_BINASCII_CRC32   (1)
+#define MICROPY_PY_RANDOM           (1)
 #define MICROPY_PY_MACHINE          (1)
+#define MICROPY_PY_MACHINE_INCLUDEFILE "ports/unix/modmachine.c"
 #define MICROPY_PY_MACHINE_PULSE    (1)
+#define MICROPY_PY_MACHINE_PIN_BASE (1)
 #define MICROPY_MACHINE_MEM_GET_READ_ADDR   mod_machine_mem_get_addr
 #define MICROPY_MACHINE_MEM_GET_WRITE_ADDR  mod_machine_mem_get_addr
 
+// Enable support for compile-only mode.
+#define MICROPY_PYEXEC_COMPILE_ONLY (1)
+
+// Enable handling of sys.exit() exit codes.
+#define MICROPY_PYEXEC_ENABLE_EXIT_CODE_HANDLING (1)
+
 #define MICROPY_ERROR_REPORTING     (MICROPY_ERROR_REPORTING_DETAILED)
 #define MICROPY_ERROR_PRINTER       (&mp_stderr_print)
 #define MICROPY_WARNINGS            (1)
@@ -183,22 +191,8 @@ extern const struct _mp_print_t mp_stderr_print;
 
 // type definitions for the specific machine
 
-#if defined(__MINGW32__) && defined(__LP64__)
-typedef long mp_int_t; // must be pointer size
-typedef unsigned long mp_uint_t; // must be pointer size
-#elif defined(__MINGW32__) && defined(_WIN64)
-#include 
-typedef __int64 mp_int_t;
-typedef unsigned __int64 mp_uint_t;
+#if defined(__MINGW32__) && defined(_WIN64)
 #define MP_SSIZE_MAX __INT64_MAX__
-#elif defined(_MSC_VER) && defined(_WIN64)
-typedef __int64 mp_int_t;
-typedef unsigned __int64 mp_uint_t;
-#else
-// These are definitions for machines where sizeof(int) == sizeof(void*),
-// regardless for actual size.
-typedef int mp_int_t; // must be pointer size
-typedef unsigned int mp_uint_t; // must be pointer size
 #endif
 
 typedef long suseconds_t;
@@ -223,18 +217,6 @@ typedef long mp_off_t;
 
 #include "realpath.h"
 #include "init.h"
-#include "sleep.h"
-
-#if MICROPY_ENABLE_SCHEDULER
-// Use 1mSec sleep to make sure there is effectively a wait period:
-// something like usleep(500) truncates and ends up calling Sleep(0).
-#define MICROPY_EVENT_POLL_HOOK \
-    do { \
-        extern void mp_handle_pending(bool); \
-        mp_handle_pending(true); \
-        msec_sleep(1.0); \
-    } while (0);
-#endif
 
 #ifdef __GNUC__
 #define MP_NOINLINE __attribute__((noinline))
@@ -252,7 +234,7 @@ typedef long mp_off_t;
 
 // CL specific overrides from mpconfig
 
-#define NORETURN                    __declspec(noreturn)
+#define MP_NORETURN                 __declspec(noreturn)
 #define MP_WEAK
 #define MP_NOINLINE                 __declspec(noinline)
 #define MP_ALWAYSINLINE             __forceinline
@@ -276,6 +258,11 @@ typedef long mp_off_t;
 #endif
 #endif
 
+// VC++ 2017 fixes
+#if (_MSC_VER < 1920)
+#define MICROPY_PY_MATH_COPYSIGN_FIX_NAN (1)
+#endif
+
 // CL specific definitions
 
 #ifndef __cplusplus
diff --git a/ports/windows/msvc/common.props b/ports/windows/msvc/common.props
index 55c9c934f22..d49d5da72a0 100644
--- a/ports/windows/msvc/common.props
+++ b/ports/windows/msvc/common.props
@@ -19,10 +19,14 @@
     $(PyBuildDir)copycookie$(Configuration)$(Platform)
     MultiByte
   
+  
+    $(PyIncDirs);$(PyVariantDir)
+  
   
     
       $(PyIncDirs);%(AdditionalIncludeDirectories)
       _USE_MATH_DEFINES;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_WARNINGS;_MBCS;%(PreprocessorDefinitions)
+      MICROPY_MODULE_FROZEN_MPY=1;MICROPY_QSTR_EXTRA_POOL=mp_qstr_frozen_const_pool;MPZ_DIG_SIZE=16;%(PreprocessorDefinitions)
       false
       Level1
       false
diff --git a/ports/windows/msvc/genhdr.targets b/ports/windows/msvc/genhdr.targets
index 7bd8364a9ed..57ec707ef91 100644
--- a/ports/windows/msvc/genhdr.targets
+++ b/ports/windows/msvc/genhdr.targets
@@ -168,9 +168,6 @@ using(var outFile = System.IO.File.CreateText(OutputFile)) {
   
     
       
-      
-        MICROPY_MODULE_FROZEN_MPY=1;MICROPY_QSTR_EXTRA_POOL=mp_qstr_frozen_const_pool;MPZ_DIG_SIZE=16;%(PreprocessorDefinitions)
-      
     
     
     
diff --git a/ports/windows/msvc/paths.props b/ports/windows/msvc/paths.props
index 767772a1aaf..e94e1f12f6a 100644
--- a/ports/windows/msvc/paths.props
+++ b/ports/windows/msvc/paths.props
@@ -31,8 +31,8 @@
     $(PyWinDir)variants\$(PyVariant)\
     $(PyBuildDir)
 
-    
-    $(PyIncDirs);$(PyBaseDir);$(PyWinDir);$(PyBuildDir);$(PyWinDir)msvc;$(PyVariantDir)
+    
+    $(PyIncDirs);$(PyBaseDir);$(PyWinDir);$(PyBuildDir);$(PyWinDir)msvc
 
      'a={}'.format(a)
                         vstr_add_strn(&lex->vstr, lex->fstring_args.buf + i, lex->fstring_args.len - i);
                         // remove the trailing '='
                         lex->fstring_args.len--;
+                        #if MICROPY_PY_TSTRINGS
+                        was_debug = true;
+                        #endif
+                    }
+                    #if MICROPY_PY_TSTRINGS
+                    if (is_tstring) {
+                        // truncate trailing spaces
+                        while (lex->fstring_args.len && unichar_isspace(lex->fstring_args.buf[lex->fstring_args.len - 1])) {
+                            lex->fstring_args.len--;
+                        }
+                    }
+                    #endif
+                    if (lex->fstring_args.len == i) {
+                        // empty format, eg f'{}'
+                        // (should apply to both f-strings and t-strings, needs test)
+                        lex->tok_kind = MP_TOKEN_MALFORMED_FSTRING;
                     }
                     // close the paren-wrapped arg to .format().
                     vstr_add_byte(&lex->fstring_args, ')');
                     // comma-separate args to .format().
                     vstr_add_byte(&lex->fstring_args, ',');
+                    #if MICROPY_PY_TSTRINGS
+                    if (is_tstring) {
+                        // start the interpolation part
+
+                        // duplicate expression to a string
+                        vstr_add_byte(&lex->fstring_args, quote_char);
+                        size_t nn = lex->fstring_args.len - i - 3;
+                        for (size_t j = 0; j < nn; ++j) {
+                            byte b = lex->fstring_args.buf[i + j];
+                            if (b == quote_char) {
+                                vstr_add_byte(&lex->fstring_args, '\\');
+                            } else if (b == '\\') {
+                                vstr_add_byte(&lex->fstring_args, '\\');
+                            }
+                            vstr_add_byte(&lex->fstring_args, b);
+                        }
+                        vstr_add_byte(&lex->fstring_args, quote_char);
+                        vstr_add_byte(&lex->fstring_args, ',');
+
+                        // start next part of string as next __template__ argument
+                        for (size_t q = 0; q < num_quotes; ++q) {
+                            vstr_add_byte(&lex->vstr, quote_char);
+                        }
+                        vstr_add_byte(&lex->vstr, ',');
+                        for (size_t q = 0; q < num_quotes; ++q) {
+                            vstr_add_byte(&lex->vstr, quote_char);
+                        }
+
+                        // process conv and format spec
+                        if (is_char(lex, '!')) {
+                            next_char(lex);
+                            vstr_add_byte(&lex->fstring_args, quote_char);
+                            vstr_add_byte(&lex->fstring_args, CUR_CHAR(lex));
+                            next_char(lex);
+                            vstr_add_byte(&lex->fstring_args, quote_char);
+                            vstr_add_byte(&lex->fstring_args, ',');
+                        } else if (was_debug && !is_char(lex, ':')) {
+                            vstr_add_str(&lex->fstring_args, "'r',");
+                        } else {
+                            vstr_add_str(&lex->fstring_args, "None,");
+                        }
+
+                        // start format str
+                        if (is_char(lex, ':')) {
+                            next_char(lex);
+                        }
+                        nested_formatting_in_tstring = 1;
+                        end_of_format_index = lex->vstr.len;
+                    }
+                    #endif
                 }
                 vstr_add_byte(&lex->vstr, '{');
+                goto continue_outer;
             }
             #endif
 
-            if (is_char(lex, '\\')) {
+            if (is_tstring && is_char(lex, '\\')) {
+                // it'll be reparsed as a string
+                vstr_add_byte(&lex->vstr, '\\');
+                if (is_raw) {
+                    vstr_add_byte(&lex->vstr, '\\');
+                } else {
+                    next_char(lex);
+                    vstr_add_byte(&lex->vstr, CUR_CHAR(lex));
+                }
+            } else if (is_char(lex, '\\')) {
                 next_char(lex);
                 unichar c = CUR_CHAR(lex);
                 if (is_raw) {
@@ -402,11 +538,9 @@ STATIC void parse_string_literal(mp_lexer_t *lex, bool is_raw, bool is_fstring)
                     vstr_add_char(&lex->vstr, '\\');
                 } else {
                     switch (c) {
-                        // note: "c" can never be MP_LEXER_EOF because next_char
-                        // always inserts a newline at the end of the input stream
                         case '\n':
-                            c = MP_LEXER_EOF;
-                            break;                          // backslash escape the newline, just ignore it
+                            // backslash escape the newline, just ignore it
+                            goto continue_parsing_string_literal;
                         case '\\':
                             break;
                         case '\'':
@@ -477,33 +611,61 @@ STATIC void parse_string_literal(mp_lexer_t *lex, bool is_raw, bool is_fstring)
                             break;
                     }
                 }
-                if (c != MP_LEXER_EOF) {
-                    #if MICROPY_PY_BUILTINS_STR_UNICODE
-                    if (c < 0x110000 && lex->tok_kind == MP_TOKEN_STRING) {
-                        // Valid unicode character in a str object.
-                        vstr_add_char(&lex->vstr, c);
-                    } else if (c < 0x100 && lex->tok_kind == MP_TOKEN_BYTES) {
-                        // Valid byte in a bytes object.
-                        vstr_add_byte(&lex->vstr, c);
-                    }
-                    #else
-                    if (c < 0x100) {
-                        // Without unicode everything is just added as an 8-bit byte.
-                        vstr_add_byte(&lex->vstr, c);
-                    }
-                    #endif
-                    else {
-                        // Character out of range; this raises a generic SyntaxError.
-                        lex->tok_kind = MP_TOKEN_INVALID;
+                #if MICROPY_PY_BUILTINS_STR_UNICODE
+                if (c < 0x110000 && lex->tok_kind == MP_TOKEN_STRING) {
+                    // Valid unicode character in a str object.
+                    vstr_add_char(&lex->vstr, c);
+                } else if (c < 0x100 && lex->tok_kind == MP_TOKEN_BYTES) {
+                    // Valid byte in a bytes object.
+                    vstr_add_byte(&lex->vstr, c);
+                }
+                #else
+                if (c < 0x100) {
+                    // Without unicode everything is just added as an 8-bit byte.
+                    vstr_add_byte(&lex->vstr, c);
+                }
+                #endif
+                else {
+                    // Character out of range; this raises a generic SyntaxError.
+                    lex->tok_kind = MP_TOKEN_INVALID;
+                }
+            #if MICROPY_PY_TSTRINGS
+            } else if (is_tstring && nested_formatting_in_tstring && is_char(lex, '}')) {
+                if (--nested_formatting_in_tstring > 0) {
+                    nested_formatting_needs_fstring = true;
+                    vstr_add_byte(&lex->vstr, CUR_CHAR(lex));
+                } else {
+                    // finished the current interpolation
+                    ++tstring_num_interpolations;
+                    if (nested_formatting_needs_fstring) {
+                        vstr_add_byte(&lex->fstring_args, 'f');
+                        nested_formatting_needs_fstring = false;
                     }
+                    vstr_add_byte(&lex->fstring_args, quote_char);
+                    vstr_add_strn(&lex->fstring_args, lex->vstr.buf + end_of_format_index + 1, lex->vstr.len - end_of_format_index - 1);
+                    lex->vstr.len = end_of_format_index;
+                    vstr_add_byte(&lex->fstring_args, quote_char);
+                    vstr_add_byte(&lex->fstring_args, ',');
                 }
+            #endif
             } else {
                 // Add the "character" as a byte so that we remain 8-bit clean.
                 // This way, strings are parsed correctly whether or not they contain utf-8 chars.
                 vstr_add_byte(&lex->vstr, CUR_CHAR(lex));
+                #if MICROPY_PY_TSTRINGS
+                if (is_tstring && is_char_and(lex, '}', '}')) {
+                    next_char(lex);
+                } else if (is_tstring && is_char(lex, '}')) {
+                    lex->tok_kind = MP_TOKEN_MALFORMED_FSTRING;
+                }
+                #endif
             }
         }
+    continue_parsing_string_literal:
         next_char(lex);
+        #if MICROPY_PY_FSTRINGS
+    continue_outer:;
+        #endif
     }
 
     // check we got the required end quotes
@@ -511,18 +673,33 @@ STATIC void parse_string_literal(mp_lexer_t *lex, bool is_raw, bool is_fstring)
         lex->tok_kind = MP_TOKEN_LONELY_STRING_OPEN;
     }
 
-    // cut off the end quotes from the token text
-    vstr_cut_tail_bytes(&lex->vstr, n_closing);
+    #if MICROPY_PY_TSTRINGS
+    if (is_tstring) {
+        if (nested_formatting_in_tstring > 0) {
+            lex->tok_kind = MP_TOKEN_MALFORMED_FSTRING;
+        }
+
+        if (1 + tstring_num_interpolations * 4 > 255) {
+            // too many arguments for function call, so wrap interpolations in a tuple
+            vstr_ins_byte(&lex->fstring_args, 0, '(');
+            vstr_add_byte(&lex->fstring_args, ')');
+        }
+    } else
+    #endif
+    {
+        // cut off the end quotes from the token text
+        vstr_cut_tail_bytes(&lex->vstr, n_closing);
+    }
 }
 
-STATIC bool skip_whitespace(mp_lexer_t *lex, bool stop_at_newline) {
-    bool had_physical_newline = false;
+// This function returns whether it has crossed a newline or not.
+// It therefore always return true if stop_at_newline is true
+static bool skip_whitespace(mp_lexer_t *lex, bool stop_at_newline) {
     while (!is_end(lex)) {
         if (is_physical_newline(lex)) {
             if (stop_at_newline && lex->nested_bracket_level == 0) {
-                break;
+                return true;
             }
-            had_physical_newline = true;
             next_char(lex);
         } else if (is_whitespace(lex)) {
             next_char(lex);
@@ -531,41 +708,27 @@ STATIC bool skip_whitespace(mp_lexer_t *lex, bool stop_at_newline) {
             while (!is_end(lex) && !is_physical_newline(lex)) {
                 next_char(lex);
             }
-            // had_physical_newline will be set on next loop
+            // will return true on next loop
         } else if (is_char_and(lex, '\\', '\n')) {
-            // line-continuation, so don't set had_physical_newline
+            // line-continuation, so don't return true
             next_char(lex);
             next_char(lex);
         } else {
             break;
         }
     }
-    return had_physical_newline;
+    return false;
 }
 
 void mp_lexer_to_next(mp_lexer_t *lex) {
-    #if MICROPY_PY_FSTRINGS
-    if (lex->fstring_args.len && lex->fstring_args_idx == 0) {
-        // moving onto the next token means the literal string is complete.
-        // switch into injecting the format args.
-        vstr_add_byte(&lex->fstring_args, ')');
-        lex->chr0_saved = lex->chr0;
-        lex->chr1_saved = lex->chr1;
-        lex->chr2_saved = lex->chr2;
-        lex->chr0 = lex->fstring_args.buf[0];
-        lex->chr1 = lex->fstring_args.buf[1];
-        lex->chr2 = lex->fstring_args.buf[2];
-        // we've already extracted 3 chars, but setting this non-zero also
-        // means we'll start consuming the fstring data
-        lex->fstring_args_idx = 3;
-    }
-    #endif
-
     // start new token text
     vstr_reset(&lex->vstr);
 
     // skip white space and comments
-    bool had_physical_newline = skip_whitespace(lex, false);
+    // set the newline tokens at the line and column of the preceding line:
+    // only advance on the pointer until a new line is crossed, save the
+    // line and column, and then readvance it
+    bool had_physical_newline = skip_whitespace(lex, true);
 
     // set token source information
     lex->tok_line = lex->line;
@@ -579,7 +742,12 @@ void mp_lexer_to_next(mp_lexer_t *lex) {
         lex->tok_kind = MP_TOKEN_INDENT;
         lex->emit_dent -= 1;
 
-    } else if (had_physical_newline && lex->nested_bracket_level == 0) {
+    } else if (had_physical_newline) {
+        // The cursor is at the end of the previous line, pointing to a
+        // physical newline. Skip any remaining whitespace, comments, and
+        // newlines.
+        skip_whitespace(lex, false);
+
         lex->tok_kind = MP_TOKEN_NEWLINE;
 
         size_t num_spaces = lex->column - 1;
@@ -612,11 +780,16 @@ void mp_lexer_to_next(mp_lexer_t *lex) {
         // MP_TOKEN_END is used to indicate that this is the first string token
         lex->tok_kind = MP_TOKEN_END;
 
+        #if MICROPY_PY_TSTRINGS
+        bool had_tstring = false;
+        #endif
+
         // Loop to accumulate string/bytes literals
         do {
             // parse type codes
             bool is_raw = false;
             bool is_fstring = false;
+            bool is_tstring = false;
             mp_token_kind_t kind = MP_TOKEN_STRING;
             int n_char = 0;
             if (is_char(lex, 'u')) {
@@ -636,22 +809,42 @@ void mp_lexer_to_next(mp_lexer_t *lex) {
                     n_char = 2;
                 }
                 #if MICROPY_PY_FSTRINGS
-                if (is_char_following(lex, 'f')) {
-                    // raw-f-strings unsupported, immediately return (invalid) token.
-                    lex->tok_kind = MP_TOKEN_FSTRING_RAW;
-                    break;
+                else if (is_char_following(lex, 'f')) {
+                    is_fstring = true;
+                    n_char = 2;
+                }
+                #endif
+                #if MICROPY_PY_TSTRINGS
+                else if (is_char_following(lex, 't')) {
+                    is_tstring = true;
+                    n_char = 2;
                 }
                 #endif
             }
             #if MICROPY_PY_FSTRINGS
             else if (is_char(lex, 'f')) {
+                is_fstring = true;
+                n_char = 1;
                 if (is_char_following(lex, 'r')) {
-                    // raw-f-strings unsupported, immediately return (invalid) token.
-                    lex->tok_kind = MP_TOKEN_FSTRING_RAW;
-                    break;
+                    is_raw = true;
+                    n_char = 2;
                 }
+            }
+            #endif
+            #if MICROPY_PY_TSTRINGS
+            else if (is_char(lex, 't')) {
+                is_tstring = true;
                 n_char = 1;
-                is_fstring = true;
+                if (is_char_following(lex, 'r')) {
+                    is_raw = true;
+                    n_char = 2;
+                }
+            }
+            #endif
+
+            #if MICROPY_PY_TSTRINGS
+            if (is_tstring) {
+                had_tstring = true;
             }
             #endif
 
@@ -672,13 +865,52 @@ void mp_lexer_to_next(mp_lexer_t *lex) {
             }
 
             // Parse the literal
-            parse_string_literal(lex, is_raw, is_fstring);
+            parse_string_literal(lex, is_raw, is_fstring, is_tstring);
 
             // Skip whitespace so we can check if there's another string following
             skip_whitespace(lex, true);
 
         } while (is_string_or_bytes(lex));
 
+        #if MICROPY_PY_TSTRINGS
+        if (had_tstring) {
+            vstr_add_byte(&lex->vstr, ',');
+            vstr_add_byte(&lex->vstr, ')');
+            vstr_add_byte(&lex->vstr, ',');
+            vstr_ins_strn(&lex->fstring_args, 0, lex->vstr.buf, lex->vstr.len);
+            if (lex->tok_kind > MP_TOKEN_MALFORMED_FSTRING) {
+                // next token is __template__ for the function
+                lex->tok_kind = MP_TOKEN_NAME;
+                vstr_reset(&lex->vstr);
+                vstr_add_str(&lex->vstr, "__template__");
+            }
+        }
+        #endif
+
+        #if MICROPY_PY_FSTRINGS
+        if (lex->fstring_args.len) {
+            // If there was an f-string then it's now complete.
+            // Switch into injecting the format args.
+            vstr_add_byte(&lex->fstring_args, ')');
+            if (lex->inject_chrs_idx == 0) {
+                // switch from stream to inject_chrs
+                char *s = vstr_add_len(&lex->inject_chrs, 3);
+                s[0] = lex->chr0;
+                s[1] = lex->chr1;
+                s[2] = lex->chr2;
+            } else {
+                // already consuming from inject_chrs, rewind cached chars to insert new ones
+                assert(lex->inject_chrs_idx >= 3);
+                lex->inject_chrs_idx -= 3;
+            }
+            vstr_ins_strn(&lex->inject_chrs, lex->inject_chrs_idx, lex->fstring_args.buf, lex->fstring_args.len);
+            vstr_reset(&lex->fstring_args);
+            lex->chr0 = lex->inject_chrs.buf[lex->inject_chrs_idx++];
+            lex->chr1 = lex->inject_chrs.buf[lex->inject_chrs_idx++];
+            lex->chr2 = lex->inject_chrs.buf[lex->inject_chrs_idx++];
+        }
+        #endif
+
     } else if (is_head_of_identifier(lex)) {
         lex->tok_kind = MP_TOKEN_NAME;
 
@@ -752,6 +984,9 @@ void mp_lexer_to_next(mp_lexer_t *lex) {
     } else {
         // search for encoded delimiter or operator
 
+        // assert that the token enum value fits in a byte, so they all fit in tok_enc_kind
+        MP_STATIC_ASSERT(MP_TOKEN_NUMBER_OF <= 256);
+
         const char *t = tok_enc;
         size_t tok_enc_index = 0;
         for (; *t != 0 && !is_char(lex, *t); t += 1) {
@@ -833,6 +1068,8 @@ mp_lexer_t *mp_lexer_new(qstr src_name, mp_reader_t reader) {
     lex->indent_level = m_new(uint16_t, lex->alloc_indent_level);
     vstr_init(&lex->vstr, 32);
     #if MICROPY_PY_FSTRINGS
+    vstr_init(&lex->inject_chrs, 0);
+    lex->inject_chrs_idx = 0;
     vstr_init(&lex->fstring_args, 0);
     #endif
 
@@ -849,9 +1086,10 @@ mp_lexer_t *mp_lexer_new(qstr src_name, mp_reader_t reader) {
     // preload first token
     mp_lexer_to_next(lex);
 
-    // Check that the first token is in the first column.  If it's not then we
-    // convert the token kind to INDENT so that the parser gives a syntax error.
-    if (lex->tok_column != 1) {
+    // Check that the first token is in the first column unless it is a
+    // newline. Otherwise we convert the token kind to INDENT so that
+    // the parser gives a syntax error.
+    if (lex->tok_column != 1 && lex->tok_kind != MP_TOKEN_NEWLINE) {
         lex->tok_kind = MP_TOKEN_INDENT;
     }
 
@@ -866,10 +1104,10 @@ mp_lexer_t *mp_lexer_new_from_str_len(qstr src_name, const char *str, size_t len
 
 #if MICROPY_READER_POSIX || MICROPY_READER_VFS
 
-mp_lexer_t *mp_lexer_new_from_file(const char *filename) {
+mp_lexer_t *mp_lexer_new_from_file(qstr filename) {
     mp_reader_t reader;
     mp_reader_new_file(&reader, filename);
-    return mp_lexer_new(qstr_from_str(filename), reader);
+    return mp_lexer_new(filename, reader);
 }
 
 #if MICROPY_HELPER_LEXER_UNIX
@@ -889,6 +1127,7 @@ void mp_lexer_free(mp_lexer_t *lex) {
         lex->reader.close(lex->reader.data);
         vstr_clear(&lex->vstr);
         #if MICROPY_PY_FSTRINGS
+        vstr_clear(&lex->inject_chrs);
         vstr_clear(&lex->fstring_args);
         #endif
         m_del(uint16_t, lex->indent_level, lex->alloc_indent_level);
diff --git a/py/lexer.h b/py/lexer.h
index 8295dec0f71..b1257d55948 100644
--- a/py/lexer.h
+++ b/py/lexer.h
@@ -46,7 +46,6 @@ typedef enum _mp_token_kind_t {
     MP_TOKEN_LONELY_STRING_OPEN,
     #if MICROPY_PY_FSTRINGS
     MP_TOKEN_MALFORMED_FSTRING,
-    MP_TOKEN_FSTRING_RAW,
     #endif
 
     MP_TOKEN_NEWLINE,
@@ -153,6 +152,8 @@ typedef enum _mp_token_kind_t {
     MP_TOKEN_DEL_SEMICOLON,
     MP_TOKEN_DEL_EQUAL,
     MP_TOKEN_DEL_MINUS_MORE,
+
+    MP_TOKEN_NUMBER_OF,
 } mp_token_kind_t;
 
 // this data structure is exposed for efficiency
@@ -161,10 +162,8 @@ typedef struct _mp_lexer_t {
     qstr source_name;           // name of source
     mp_reader_t reader;         // stream source
 
-    unichar chr0, chr1, chr2;   // current cached characters from source
-    #if MICROPY_PY_FSTRINGS
-    unichar chr0_saved, chr1_saved, chr2_saved; // current cached characters from alt source
-    #endif
+    uint32_t chr0;              // first cached byte from source (32-bits for efficient access)
+    uint8_t chr1, chr2;         // subsequent cached bytes from source
 
     size_t line;                // current source line
     size_t column;              // current source column
@@ -181,8 +180,9 @@ typedef struct _mp_lexer_t {
     mp_token_kind_t tok_kind;   // token kind
     vstr_t vstr;                // token data
     #if MICROPY_PY_FSTRINGS
+    vstr_t inject_chrs;         // characters currently being injected into the stream
+    size_t inject_chrs_idx;     // current index into inject_chrs
     vstr_t fstring_args;        // extracted arguments to pass to .format()
-    size_t fstring_args_idx;    // how many bytes of fstring_args have been read
     #endif
 } mp_lexer_t;
 
@@ -191,7 +191,7 @@ mp_lexer_t *mp_lexer_new_from_str_len(qstr src_name, const char *str, size_t len
 
 // If MICROPY_READER_POSIX or MICROPY_READER_VFS aren't enabled then
 // this function must be implemented by the port.
-mp_lexer_t *mp_lexer_new_from_file(const char *filename);
+mp_lexer_t *mp_lexer_new_from_file(qstr filename);
 
 #if MICROPY_HELPER_LEXER_UNIX
 mp_lexer_t *mp_lexer_new_from_fd(qstr filename, int fd, bool close_fd);
diff --git a/py/make_root_pointers.py b/py/make_root_pointers.py
index efe398b8227..5da343c1270 100644
--- a/py/make_root_pointers.py
+++ b/py/make_root_pointers.py
@@ -6,8 +6,6 @@
 "struct _mp_state_vm_t" in py/mpstate.h
 """
 
-from __future__ import print_function
-
 import argparse
 import io
 import re
diff --git a/py/makecompresseddata.py b/py/makecompresseddata.py
index 1bce3e8e837..a6fa7db00b2 100644
--- a/py/makecompresseddata.py
+++ b/py/makecompresseddata.py
@@ -1,5 +1,3 @@
-from __future__ import print_function
-
 import collections
 import re
 import sys
diff --git a/py/makemoduledefs.py b/py/makemoduledefs.py
index 9061cd890be..c8fdc21f6fb 100644
--- a/py/makemoduledefs.py
+++ b/py/makemoduledefs.py
@@ -1,19 +1,34 @@
 """
 This pre-processor parses a single file containing a list of
-MP_REGISTER_MODULE(module_name, obj_module)
-These are used to generate a header with the required entries for
-"mp_rom_map_elem_t mp_builtin_module_table[]" in py/objmodule.c
+`MP_REGISTER_MODULE(MP_QSTR_module_name, obj_module)` or
+`MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_module_name, obj_module)`
+(i.e. the output of `py/makeqstrdefs.py cat module`).
+
+The output is a header (typically moduledefs.h) which is included by
+py/objmodule.c that contains entries to be included in the definition of
+ - mp_rom_map_elem_t mp_builtin_module_table[]
+ - mp_rom_map_elem_t mp_builtin_extensible_module_table[]
+
+Extensible modules are modules that can be overridden from the filesystem, see
+py/builtinimnport.c:process_import_at_level. Regular modules will always use
+the built-in version.
 """
 
-from __future__ import print_function
-
 import sys
 import re
 import io
 import argparse
 
 
-pattern = re.compile(r"\s*MP_REGISTER_MODULE\((.*?),\s*(.*?)\);", flags=re.DOTALL)
+register_pattern = re.compile(
+    r"\s*(MP_REGISTER_MODULE|MP_REGISTER_EXTENSIBLE_MODULE)\(MP_QSTR_(.*?),\s*(.*?)\);",
+    flags=re.DOTALL,
+)
+
+delegation_pattern = re.compile(
+    r"\s*(?:MP_REGISTER_MODULE_DELEGATION)\((.*?),\s*(.*?)\);",
+    flags=re.DOTALL,
+)
 
 
 def find_module_registrations(filename):
@@ -25,7 +40,8 @@ def find_module_registrations(filename):
     global pattern
 
     with io.open(filename, encoding="utf-8") as c_file_obj:
-        return set(re.findall(pattern, c_file_obj.read()))
+        c = c_file_obj.read()
+        return set(re.findall(register_pattern, c)), set(re.findall(delegation_pattern, c))
 
 
 def generate_module_table_header(modules):
@@ -37,14 +53,22 @@ def generate_module_table_header(modules):
 
     # Print header file for all external modules.
     mod_defs = set()
-    print("// Automatically generated by makemoduledefs.py.\n")
-    for module_name, obj_module in modules:
+    extensible_mod_defs = set()
+    for macro_name, module_name, obj_module in modules:
         mod_def = "MODULE_DEF_{}".format(module_name.upper())
-        mod_defs.add(mod_def)
+        if macro_name == "MP_REGISTER_MODULE":
+            mod_defs.add(mod_def)
+        elif macro_name == "MP_REGISTER_EXTENSIBLE_MODULE":
+            extensible_mod_defs.add(mod_def)
         if "," in obj_module:
             print(
-                "ERROR: Call to MP_REGISTER_MODULE({}, {}) should be MP_REGISTER_MODULE({}, {})\n".format(
-                    module_name, obj_module, module_name, obj_module.split(",")[0]
+                "ERROR: Call to {}({}, {}) should be {}({}, {})\n".format(
+                    macro_name,
+                    module_name,
+                    obj_module,
+                    macro_name,
+                    module_name,
+                    obj_module.split(",")[0],
                 ),
                 file=sys.stderr,
             )
@@ -53,7 +77,7 @@ def generate_module_table_header(modules):
             (
                 "extern const struct _mp_obj_module_t {obj_module};\n"
                 "#undef {mod_def}\n"
-                "#define {mod_def} {{ MP_ROM_QSTR({module_name}), MP_ROM_PTR(&{obj_module}) }},\n"
+                "#define {mod_def} {{ MP_ROM_QSTR(MP_QSTR_{module_name}), MP_ROM_PTR(&{obj_module}) }},\n"
             ).format(
                 module_name=module_name,
                 obj_module=obj_module,
@@ -61,21 +85,54 @@ def generate_module_table_header(modules):
             )
         )
 
+    # There should always be at least one module (__main__ in runtime.c)
+    assert mod_defs
+
     print("\n#define MICROPY_REGISTERED_MODULES \\")
 
     for mod_def in sorted(mod_defs):
         print("    {mod_def} \\".format(mod_def=mod_def))
-
     print("// MICROPY_REGISTERED_MODULES")
 
+    # There are not necessarily any extensible modules (e.g., bare-arm or minimal x86)
+    print("\n#define MICROPY_HAVE_REGISTERED_EXTENSIBLE_MODULES ", len(extensible_mod_defs))
+
+    if extensible_mod_defs:
+        print("\n#define MICROPY_REGISTERED_EXTENSIBLE_MODULES \\")
+
+        for mod_def in sorted(extensible_mod_defs):
+            print("    {mod_def} \\".format(mod_def=mod_def))
+
+        print("// MICROPY_REGISTERED_EXTENSIBLE_MODULES")
+
+
+def generate_module_delegations(delegations):
+    if not delegations:
+        return
+
+    print()
+    for obj_module, fun_name in delegations:
+        print("extern void {}(mp_obj_t self_in, qstr attr, mp_obj_t *dest);".format(fun_name))
+    print("#define MICROPY_MODULE_DELEGATIONS \\")
+    for obj_module, fun_name in delegations:
+        print(
+            "    {{ MP_ROM_PTR(&{obj_module}), {fun_name} }}, \\".format(
+                obj_module=obj_module, fun_name=fun_name
+            )
+        )
+    print("// MICROPY_MODULE_DELEGATIONS")
+
 
 def main():
     parser = argparse.ArgumentParser()
     parser.add_argument("file", nargs=1, help="file with MP_REGISTER_MODULE definitions")
     args = parser.parse_args()
 
-    modules = find_module_registrations(args.file[0])
+    print("// Automatically generated by makemoduledefs.py.\n")
+
+    modules, delegations = find_module_registrations(args.file[0])
     generate_module_table_header(sorted(modules))
+    generate_module_delegations(sorted(delegations))
 
 
 if __name__ == "__main__":
diff --git a/py/makeqstrdata.py b/py/makeqstrdata.py
index 71f529fb629..0280a4aa683 100644
--- a/py/makeqstrdata.py
+++ b/py/makeqstrdata.py
@@ -1,26 +1,54 @@
 """
 Process raw qstr file and output qstr data with length, hash and data bytes.
 
-This script works with Python 2.6, 2.7, 3.3 and 3.4.
+This script works with Python 3.3+.
 """
 
-from __future__ import print_function
-
 import re
 import sys
 
-# Python 2/3 compatibility:
-#   - iterating through bytes is different
-#   - codepoint2name lives in a different module
-import platform
-
-if platform.python_version_tuple()[0] == "2":
-    bytes_cons = lambda val, enc=None: bytearray(val)
-    from htmlentitydefs import codepoint2name
-elif platform.python_version_tuple()[0] == "3":
-    bytes_cons = bytes
-    from html.entities import codepoint2name
-# end compatibility code
+bytes_cons = bytes
+
+# fmt: off
+codepoint2name = {
+    198: "AElig", 193: "Aacute", 194: "Acirc", 192: "Agrave", 913: "Alpha", 197: "Aring", 195: "Atilde",
+    196: "Auml", 914: "Beta", 199: "Ccedil", 935: "Chi", 8225: "Dagger", 916: "Delta", 208: "ETH",
+    201: "Eacute", 202: "Ecirc", 200: "Egrave", 917: "Epsilon", 919: "Eta", 203: "Euml", 915: "Gamma",
+    205: "Iacute", 206: "Icirc", 204: "Igrave", 921: "Iota", 207: "Iuml", 922: "Kappa", 923: "Lambda",
+    924: "Mu", 209: "Ntilde", 925: "Nu", 338: "OElig", 211: "Oacute", 212: "Ocirc", 210: "Ograve",
+    937: "Omega", 927: "Omicron", 216: "Oslash", 213: "Otilde", 214: "Ouml", 934: "Phi", 928: "Pi",
+    8243: "Prime", 936: "Psi", 929: "Rho", 352: "Scaron", 931: "Sigma", 222: "THORN", 932: "Tau",
+    920: "Theta", 218: "Uacute", 219: "Ucirc", 217: "Ugrave", 933: "Upsilon", 220: "Uuml", 926: "Xi",
+    221: "Yacute", 376: "Yuml", 918: "Zeta", 225: "aacute", 226: "acirc", 180: "acute", 230: "aelig",
+    224: "agrave", 8501: "alefsym", 945: "alpha", 38: "amp", 8743: "and", 8736: "ang", 229: "aring",
+    8776: "asymp", 227: "atilde", 228: "auml", 8222: "bdquo", 946: "beta", 166: "brvbar", 8226: "bull",
+    8745: "cap", 231: "ccedil", 184: "cedil", 162: "cent", 967: "chi", 710: "circ", 9827: "clubs",
+    8773: "cong", 169: "copy", 8629: "crarr", 8746: "cup", 164: "curren", 8659: "dArr", 8224: "dagger",
+    8595: "darr", 176: "deg", 948: "delta", 9830: "diams", 247: "divide", 233: "eacute", 234: "ecirc",
+    232: "egrave", 8709: "empty", 8195: "emsp", 8194: "ensp", 949: "epsilon", 8801: "equiv", 951: "eta",
+    240: "eth", 235: "euml", 8364: "euro", 8707: "exist", 402: "fnof", 8704: "forall", 189: "frac12",
+    188: "frac14", 190: "frac34", 8260: "frasl", 947: "gamma", 8805: "ge", 62: "gt", 8660: "hArr",
+    8596: "harr", 9829: "hearts", 8230: "hellip", 237: "iacute", 238: "icirc", 161: "iexcl", 236: "igrave",
+    8465: "image", 8734: "infin", 8747: "int", 953: "iota", 191: "iquest", 8712: "isin", 239: "iuml",
+    954: "kappa", 8656: "lArr", 955: "lambda", 9001: "lang", 171: "laquo", 8592: "larr", 8968: "lceil",
+    8220: "ldquo", 8804: "le", 8970: "lfloor", 8727: "lowast", 9674: "loz", 8206: "lrm", 8249: "lsaquo",
+    8216: "lsquo", 60: "lt", 175: "macr", 8212: "mdash", 181: "micro", 183: "middot", 8722: "minus",
+    956: "mu", 8711: "nabla", 160: "nbsp", 8211: "ndash", 8800: "ne", 8715: "ni", 172: "not", 8713: "notin",
+    8836: "nsub", 241: "ntilde", 957: "nu", 243: "oacute", 244: "ocirc", 339: "oelig", 242: "ograve",
+    8254: "oline", 969: "omega", 959: "omicron", 8853: "oplus", 8744: "or", 170: "ordf", 186: "ordm",
+    248: "oslash", 245: "otilde", 8855: "otimes", 246: "ouml", 182: "para", 8706: "part", 8240: "permil",
+    8869: "perp", 966: "phi", 960: "pi", 982: "piv", 177: "plusmn", 163: "pound", 8242: "prime",
+    8719: "prod", 8733: "prop", 968: "psi", 34: "quot", 8658: "rArr", 8730: "radic", 9002: "rang",
+    187: "raquo", 8594: "rarr", 8969: "rceil", 8221: "rdquo", 8476: "real", 174: "reg", 8971: "rfloor",
+    961: "rho", 8207: "rlm", 8250: "rsaquo", 8217: "rsquo", 8218: "sbquo", 353: "scaron", 8901: "sdot",
+    167: "sect", 173: "shy", 963: "sigma", 962: "sigmaf", 8764: "sim", 9824: "spades", 8834: "sub",
+    8838: "sube", 8721: "sum", 8835: "sup", 185: "sup1", 178: "sup2", 179: "sup3", 8839: "supe",
+    223: "szlig", 964: "tau", 8756: "there4", 952: "theta", 977: "thetasym", 8201: "thinsp", 254: "thorn",
+    732: "tilde", 215: "times", 8482: "trade", 8657: "uArr", 250: "uacute", 8593: "uarr", 251: "ucirc",
+    249: "ugrave", 168: "uml", 978: "upsih", 965: "upsilon", 252: "uuml", 8472: "weierp", 958: "xi",
+    253: "yacute", 165: "yen", 255: "yuml", 950: "zeta", 8205: "zwj", 8204: "zwnj"
+}
+# fmt: on
 
 codepoint2name[ord("-")] = "hyphen"
 
@@ -52,7 +80,8 @@
 codepoint2name[ord("|")] = "pipe"
 codepoint2name[ord("~")] = "tilde"
 
-# static qstrs, should be sorted
+# static qstrs, these must maintain a specific order for .mpy compatibility
+# See QSTR_LAST_STATIC at the top of py/persistentcode.c
 
 static_qstr_list = [
     "",
@@ -222,6 +251,81 @@
     "zip",
 ]
 
+# Additional QSTRs that must have index <255 because they are stored as `byte` values.
+# These are not part of the .mpy compatibility list, but we place them in the
+# fixed unsorted pool (i.e. QDEF0) to ensure their indices are small.
+unsorted_qstr_list = {
+    # From py/objtype.c: used in the `mp_binary_op_method_name` and `mp_unary_op_method_name` tables.
+    "__bool__",
+    "__pos__",
+    "__neg__",
+    "__invert__",
+    "__abs__",
+    "__float__",
+    "__complex__",
+    "__sizeof__",
+    "__lt__",
+    "__gt__",
+    "__eq__",
+    "__le__",
+    "__ge__",
+    "__ne__",
+    "__contains__",
+    "__iadd__",
+    "__isub__",
+    "__imul__",
+    "__imatmul__",
+    "__ifloordiv__",
+    "__itruediv__",
+    "__imod__",
+    "__ipow__",
+    "__ior__",
+    "__ixor__",
+    "__iand__",
+    "__ilshift__",
+    "__irshift__",
+    "__add__",
+    "__sub__",
+    "__mul__",
+    "__matmul__",
+    "__floordiv__",
+    "__truediv__",
+    "__mod__",
+    "__divmod__",
+    "__pow__",
+    "__or__",
+    "__xor__",
+    "__and__",
+    "__lshift__",
+    "__rshift__",
+    "__radd__",
+    "__rsub__",
+    "__rmul__",
+    "__rmatmul__",
+    "__rfloordiv__",
+    "__rtruediv__",
+    "__rmod__",
+    "__rpow__",
+    "__ror__",
+    "__rxor__",
+    "__rand__",
+    "__rlshift__",
+    "__rrshift__",
+    "__get__",
+    "__set__",
+    "__delete__",
+    # From py/scope.c: used in `scope_simple_name_table` table.
+    # Note: "" is already in `static_qstr_list`.
+    "",
+    "",
+    "",
+    "",
+    "",
+}
+
+# Matches any string that needs no escaping (alphanum + _ only)
+RE_NO_ESCAPE = re.compile(r"^[a-zA-Z0-9_]$")
+
 
 # this must match the equivalent function in qstr.c
 def compute_hash(qstr, bytes_hash):
@@ -229,37 +333,31 @@ def compute_hash(qstr, bytes_hash):
     for b in qstr:
         hash = (hash * 33) ^ b
     # Make sure that valid hash is never zero, zero means "hash not computed"
-    return (hash & ((1 << (8 * bytes_hash)) - 1)) or 1
+    # if bytes_hash is zero, assume a 16-bit mask (to match qstr.c)
+    return (hash & ((1 << (8 * (bytes_hash or 2))) - 1)) or 1
 
 
 def qstr_escape(qst):
-    def esc_char(m):
-        c = ord(m.group(0))
+    def esc_char(c):
+        if RE_NO_ESCAPE.match(c):
+            return c
+        c = ord(c)
         try:
             name = codepoint2name[c]
         except KeyError:
             name = "0x%02x" % c
         return "_" + name + "_"
 
-    return re.sub(r"[^A-Za-z0-9_]", esc_char, qst)
+    return "".join(map(esc_char, qst))
+
+
+static_qstr_list_ident = list(map(qstr_escape, static_qstr_list))
 
 
 def parse_input_headers(infiles):
     qcfgs = {}
     qstrs = {}
 
-    # add static qstrs
-    for qstr in static_qstr_list:
-        # work out the corresponding qstr name
-        ident = qstr_escape(qstr)
-
-        # don't add duplicates
-        assert ident not in qstrs
-
-        # add the qstr to the list, with order number to retain original order in file
-        order = len(qstrs) - 300000
-        qstrs[ident] = (order, ident, qstr)
-
     # read the qstrs in from the input files
     for infile in infiles:
         with open(infile, "rt") as f:
@@ -294,22 +392,12 @@ def parse_input_headers(infiles):
                 ident = qstr_escape(qstr)
 
                 # don't add duplicates
+                if ident in static_qstr_list_ident:
+                    continue
                 if ident in qstrs:
                     continue
 
-                # add the qstr to the list, with order number to retain original order in file
-                order = len(qstrs)
-                # but put special method names like __add__ at the top of list, so
-                # that their id's fit into a byte
-                if ident == "":
-                    # Sort empty qstr above all still
-                    order = -200000
-                elif ident == "__dir__":
-                    # Put __dir__ after empty qstr for builtin dir() to work
-                    order = -190000
-                elif ident.startswith("__"):
-                    order -= 100000
-                qstrs[ident] = (order, ident, qstr)
+                qstrs[ident] = (ident, qstr)
 
     if not qcfgs:
         sys.stderr.write("ERROR: Empty preprocessor output - check for errors above\n")
@@ -348,12 +436,19 @@ def print_qstr_data(qcfgs, qstrs):
     print("")
 
     # add NULL qstr with no hash or data
-    print('QDEF(MP_QSTRnull, 0, 0, "")')
+    print('QDEF0(MP_QSTRnull, 0, 0, "")')
+
+    # add static qstrs to the first unsorted pool
+    for qstr in static_qstr_list:
+        qbytes = make_bytes(cfg_bytes_len, cfg_bytes_hash, qstr)
+        print("QDEF0(MP_QSTR_%s, %s)" % (qstr_escape(qstr), qbytes))
 
-    # go through each qstr and print it out
-    for order, ident, qstr in sorted(qstrs.values(), key=lambda x: x[0]):
+    # add remaining qstrs to the sorted (by value) pool (unless they're in
+    # unsorted_qstr_list, in which case add them to the unsorted pool)
+    for ident, qstr in sorted(qstrs.values(), key=lambda x: x[1]):
         qbytes = make_bytes(cfg_bytes_len, cfg_bytes_hash, qstr)
-        print("QDEF(MP_QSTR_%s, %s)" % (ident, qbytes))
+        pool = 0 if qstr in unsorted_qstr_list else 1
+        print("QDEF%d(MP_QSTR_%s, %s)" % (pool, ident, qbytes))
 
 
 def do_work(infiles):
diff --git a/py/makeqstrdefs.py b/py/makeqstrdefs.py
index 13a8b54d67c..e1046fa2b19 100644
--- a/py/makeqstrdefs.py
+++ b/py/makeqstrdefs.py
@@ -2,11 +2,9 @@
 This script processes the output from the C preprocessor and extracts all
 qstr. Each qstr is transformed into a qstr definition of the form 'Q(...)'.
 
-This script works with Python 2.6, 2.7, 3.3 and 3.4.
+This script works with Python 3.3+.
 """
 
-from __future__ import print_function
-
 import io
 import os
 import re
@@ -21,13 +19,17 @@
 # Extract MP_COMPRESSED_ROM_TEXT("") macros.  (Which come from MP_ERROR_TEXT)
 _MODE_COMPRESS = "compress"
 
-# Extract MP_REGISTER_MODULE(...) macros.
+# Extract MP_REGISTER_(EXTENSIBLE_)MODULE(...) macros.
 _MODE_MODULE = "module"
 
 # Extract MP_REGISTER_ROOT_POINTER(...) macros.
 _MODE_ROOT_POINTER = "root_pointer"
 
 
+class PreprocessorError(Exception):
+    pass
+
+
 def is_c_source(fname):
     return os.path.splitext(fname)[1] in [".c"]
 
@@ -55,9 +57,36 @@ def preprocess():
     except OSError:
         pass
 
+    # These regex's are used to filter the preprocessed data, keeping only those lines
+    # that are subsequently needed by the `process_file` step.  The regexs are kept
+    # short so they are as efficient as possible. (The stm32 port needs symbols of the
+    # form `micropy_hw_xxx` so they are also kept.)
+    re_line_file = re.compile(rb"^#(?:line)?\s+\d+\s\"")
+    re_mp_info = re.compile(rb"MP_COMP|MP_QSTR|MP_REGI|micropy_hw")
+
     def pp(flags):
         def run(files):
-            return subprocess.check_output(args.pp + flags + files)
+            try:
+                filtered_lines = []
+                cmd = args.pp + flags + files
+                with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc:
+                    recent_file = None
+                    for line in proc.stdout:
+                        if line.isspace():
+                            pass
+                        elif re_line_file.match(line):
+                            recent_file = line
+                        elif re_mp_info.search(line):
+                            if recent_file:
+                                filtered_lines.append(recent_file)
+                                recent_file = None
+                            filtered_lines.append(line)
+                    proc.wait()
+                    if proc.returncode:
+                        raise PreprocessorError("command failed: " + " ".join(cmd))
+                return b"".join(filtered_lines)
+            except subprocess.CalledProcessError as er:
+                raise PreprocessorError(str(er))
 
         return run
 
@@ -93,7 +122,9 @@ def process_file(f):
     elif args.mode == _MODE_COMPRESS:
         re_match = re.compile(r'MP_COMPRESSED_ROM_TEXT\("([^"]*)"\)')
     elif args.mode == _MODE_MODULE:
-        re_match = re.compile(r"MP_REGISTER_MODULE\(.*?,\s*.*?\);")
+        re_match = re.compile(
+            r"(?:MP_REGISTER_MODULE|MP_REGISTER_EXTENSIBLE_MODULE|MP_REGISTER_MODULE_DELEGATION)\(.*?,\s*.*?\);"
+        )
     elif args.mode == _MODE_ROOT_POINTER:
         re_match = re.compile(r"MP_REGISTER_ROOT_POINTER\(.*?\);")
     output = []
@@ -129,15 +160,12 @@ def cat_together():
 
     hasher = hashlib.md5()
     all_lines = []
-    outf = open(args.output_dir + "/out", "wb")
     for fname in glob.glob(args.output_dir + "/*." + args.mode):
         with open(fname, "rb") as f:
             lines = f.readlines()
             all_lines += lines
     all_lines.sort()
     all_lines = b"\n".join(all_lines)
-    outf.write(all_lines)
-    outf.close()
     hasher.update(all_lines)
     new_hash = hasher.hexdigest()
     # print(new_hash)
@@ -154,14 +182,11 @@ def cat_together():
         mode_full = "Module registrations"
     elif args.mode == _MODE_ROOT_POINTER:
         mode_full = "Root pointer registrations"
-    if old_hash != new_hash:
+    if old_hash != new_hash or not os.path.exists(args.output_file):
         print(mode_full, "updated")
-        try:
-            # rename below might fail if file exists
-            os.remove(args.output_file)
-        except:
-            pass
-        os.rename(args.output_dir + "/out", args.output_file)
+
+        with open(args.output_file, "wb") as outf:
+            outf.write(all_lines)
         with open(args.output_file + ".hash", "w") as f:
             f.write(new_hash)
     else:
@@ -206,7 +231,12 @@ class Args:
         for k, v in named_args.items():
             setattr(args, k, v)
 
-        preprocess()
+        try:
+            preprocess()
+        except PreprocessorError as er:
+            print(er)
+            sys.exit(1)
+
         sys.exit(0)
 
     args.mode = sys.argv[2]
diff --git a/py/makeversionhdr.py b/py/makeversionhdr.py
index e682a4e139c..249ec1830c1 100644
--- a/py/makeversionhdr.py
+++ b/py/makeversionhdr.py
@@ -1,11 +1,9 @@
 """
 Generate header file with macros defining MicroPython version info.
 
-This script works with Python 2.6, 2.7, 3.3 and 3.4.
+This script works with Python 3.3+.
 """
 
-from __future__ import print_function
-
 import argparse
 import sys
 import os
@@ -13,63 +11,69 @@
 import subprocess
 
 
+# The MicroPython repository tags a release commit as "vX.Y.Z", and the commit
+# immediately following as "vX.(Y+1).Z-preview".
+# This function will return:
+#   "vX.Y.Z" -- building at the release commit
+#   "vX.Y.Z-preview" -- building at the first commit in the next cycle
+#   "vX.Y.Z-preview.N.gHASH" -- building at any subsequent commit in the cycle
+#   "vX.Y.Z-preview.N.gHASH.dirty" -- building at any subsequent commit in the cycle
+#                                     with local changes
 def get_version_info_from_git(repo_path):
-    # Python 2.6 doesn't have check_output, so check for that
-    try:
-        subprocess.check_output
-        subprocess.check_call
-    except AttributeError:
-        return None
-
     # Note: git describe doesn't work if no tag is available
     try:
         git_tag = subprocess.check_output(
-            ["git", "describe", "--tags", "--dirty", "--always", "--match", "v[1-9].*"],
+            [
+                "git",
+                "describe",
+                "--tags",
+                "--dirty",
+                "--always",
+                "--match",
+                "v[1-9].*",
+                "--abbrev=10",
+            ],
             cwd=repo_path,
             stderr=subprocess.STDOUT,
             universal_newlines=True,
         ).strip()
-    except subprocess.CalledProcessError as er:
-        if er.returncode == 128:
-            # git exit code of 128 means no repository found
-            return None
-        git_tag = ""
-    except OSError:
+        # Turn git-describe's output into semver compatible (dot-separated
+        # identifiers inside the prerelease field).
+        git_tag = git_tag.split("-", 1)
+        if len(git_tag) == 1:
+            return git_tag[0]
+        else:
+            return git_tag[0] + "-" + git_tag[1].replace("-", ".")
+    except (subprocess.CalledProcessError, OSError):
         return None
+
+
+def get_hash_from_git(repo_path):
     try:
-        git_hash = subprocess.check_output(
+        return subprocess.check_output(
             ["git", "rev-parse", "--short", "HEAD"],
             cwd=repo_path,
             stderr=subprocess.STDOUT,
             universal_newlines=True,
         ).strip()
-    except subprocess.CalledProcessError:
-        git_hash = "unknown"
-    except OSError:
+    except (subprocess.CalledProcessError, OSError):
         return None
 
-    try:
-        # Check if there are any modified files.
-        subprocess.check_call(
-            ["git", "diff", "--no-ext-diff", "--quiet", "--exit-code"],
-            cwd=repo_path,
-            stderr=subprocess.STDOUT,
-        )
-        # Check if there are any staged files.
-        subprocess.check_call(
-            ["git", "diff-index", "--cached", "--quiet", "HEAD", "--"],
-            cwd=repo_path,
-            stderr=subprocess.STDOUT,
-        )
-    except subprocess.CalledProcessError:
-        git_hash += "-dirty"
-    except OSError:
-        return None
-
-    return git_tag, git_hash
-
 
+# When building from a source tarball (or any situation where the git repo
+# isn't available), this function will use the info in mpconfig.h as a
+# fallback. The release commit sets MICROPY_VERSION_PRERELEASE to 0, and the
+# commit immediately following increments MICROPY_VERSION_MINOR and sets
+# MICROPY_VERSION_PRERELEASE back to 1.
+# This function will return:
+#    "vX.Y.Z" -- building at the release commit
+#    "vX.Y.Z-preview" -- building at any other commit
 def get_version_info_from_mpconfig(repo_path):
+    print(
+        "makeversionhdr.py: Warning: No git repo or tag info available, falling back to mpconfig.h version info.",
+        file=sys.stderr,
+    )
+
     with open(os.path.join(repo_path, "py", "mpconfig.h")) as f:
         for line in f:
             if line.startswith("#define MICROPY_VERSION_MAJOR "):
@@ -78,29 +82,42 @@ def get_version_info_from_mpconfig(repo_path):
                 ver_minor = int(line.strip().split()[2])
             elif line.startswith("#define MICROPY_VERSION_MICRO "):
                 ver_micro = int(line.strip().split()[2])
-                git_tag = "v%d.%d.%d" % (ver_major, ver_minor, ver_micro)
-                return git_tag, ""
+            elif line.startswith("#define MICROPY_VERSION_PRERELEASE "):
+                ver_prerelease = int(line.strip().split()[2])
+                git_tag = "v%d.%d.%d%s" % (
+                    ver_major,
+                    ver_minor,
+                    ver_micro,
+                    "-preview" if ver_prerelease else "",
+                )
+                return git_tag
     return None
 
 
 def make_version_header(repo_path, filename):
-    info = None
+    git_tag = None
+    git_hash = None
     if "MICROPY_GIT_TAG" in os.environ:
-        info = [os.environ["MICROPY_GIT_TAG"], os.environ["MICROPY_GIT_HASH"]]
-    if info is None:
-        info = get_version_info_from_git(repo_path)
-    if info is None:
-        info = get_version_info_from_mpconfig(repo_path)
-
-    git_tag, git_hash = info
+        git_tag = os.environ["MICROPY_GIT_TAG"]
+        git_hash = os.environ.get("MICROPY_GIT_HASH")
+    if git_tag is None:
+        git_tag = get_version_info_from_git(repo_path)
+        git_hash = get_hash_from_git(repo_path)
+    if git_tag is None:
+        git_tag = get_version_info_from_mpconfig(repo_path)
+
+    if not git_tag:
+        print("makeversionhdr.py: Error: No version information available.")
+        sys.exit(1)
 
     build_date = datetime.date.today()
     if "SOURCE_DATE_EPOCH" in os.environ:
-        build_date = datetime.datetime.utcfromtimestamp(
-            int(os.environ["SOURCE_DATE_EPOCH"])
+        build_date = datetime.datetime.fromtimestamp(
+            int(os.environ["SOURCE_DATE_EPOCH"]), datetime.timezone.utc
         ).date()
 
     # Generate the file with the git and version info
+    # Note: MICROPY_GIT_HASH may be used by third-party code.
     file_data = """\
 // This file was generated by py/makeversionhdr.py
 #define MICROPY_GIT_TAG "%s"
@@ -108,7 +125,7 @@ def make_version_header(repo_path, filename):
 #define MICROPY_BUILD_DATE "%s"
 """ % (
         git_tag,
-        git_hash,
+        git_hash or "",
         build_date.strftime("%Y-%m-%d"),
     )
 
diff --git a/py/malloc.c b/py/malloc.c
index efdff753969..959034e7cb1 100644
--- a/py/malloc.c
+++ b/py/malloc.c
@@ -56,8 +56,8 @@
 #undef malloc
 #undef free
 #undef realloc
-#define malloc(b) gc_alloc((b), false)
-#define malloc_with_finaliser(b) gc_alloc((b), true)
+#define malloc(b) gc_alloc((b), 0)
+#define malloc_with_finaliser(b) gc_alloc((b), GC_ALLOC_FLAG_HAS_FINALISER)
 #define free gc_free
 #define realloc(ptr, n) gc_realloc(ptr, n, true)
 #define realloc_ext(ptr, n, mv) gc_realloc(ptr, n, mv)
@@ -69,7 +69,7 @@
 #error MICROPY_ENABLE_FINALISER requires MICROPY_ENABLE_GC
 #endif
 
-STATIC void *realloc_ext(void *ptr, size_t n_bytes, bool allow_move) {
+static void *realloc_ext(void *ptr, size_t n_bytes, bool allow_move) {
     if (allow_move) {
         return realloc(ptr, n_bytes);
     } else {
@@ -185,7 +185,7 @@ void *m_realloc_maybe(void *ptr, size_t new_num_bytes, bool allow_move)
     #if MICROPY_MALLOC_USES_ALLOCATED_SIZE
     DEBUG_printf("realloc %p, %d, %d : %p\n", ptr, old_num_bytes, new_num_bytes, new_ptr);
     #else
-    DEBUG_printf("realloc %p, %d, %d : %p\n", ptr, new_num_bytes, new_ptr);
+    DEBUG_printf("realloc %p, %d : %p\n", ptr, new_num_bytes, new_ptr);
     #endif
     return new_ptr;
 }
@@ -209,6 +209,31 @@ void m_free(void *ptr)
 
 #if MICROPY_TRACKED_ALLOC
 
+#if MICROPY_PY_THREAD && !MICROPY_PY_THREAD_GIL
+// If there's no GIL, use the GC recursive mutex to protect the tracked node linked list
+// under m_tracked_head.
+//
+// (For ports with GIL, the expectation is to only call tracked alloc functions
+// while holding the GIL.)
+
+static inline void m_tracked_node_lock(void) {
+    mp_thread_recursive_mutex_lock(&MP_STATE_MEM(gc_mutex), 1);
+}
+
+static inline void m_tracked_node_unlock(void) {
+    mp_thread_recursive_mutex_unlock(&MP_STATE_MEM(gc_mutex));
+}
+
+#else
+
+static inline void m_tracked_node_lock(void) {
+}
+
+static inline void m_tracked_node_unlock(void) {
+}
+
+#endif
+
 #define MICROPY_TRACKED_ALLOC_STORE_SIZE (!MICROPY_ENABLE_GC)
 
 typedef struct _m_tracked_node_t {
@@ -220,8 +245,18 @@ typedef struct _m_tracked_node_t {
     uint8_t data[];
 } m_tracked_node_t;
 
+// Helper to get data size of a tracked node, abstracting MICROPY_TRACKED_ALLOC_STORE_SIZE.
+static inline size_t get_tracked_node_size(m_tracked_node_t *node) {
+    #if MICROPY_TRACKED_ALLOC_STORE_SIZE
+    return node->size;
+    #else
+    return gc_nbytes(node) - sizeof(m_tracked_node_t);
+    #endif
+}
+
 #if MICROPY_DEBUG_VERBOSE
-STATIC size_t m_tracked_count_links(size_t *nb) {
+static size_t m_tracked_count_links(size_t *nb) {
+    m_tracked_node_lock();
     m_tracked_node_t *node = MP_STATE_VM(m_tracked_head);
     size_t n = 0;
     *nb = 0;
@@ -234,6 +269,7 @@ STATIC size_t m_tracked_count_links(size_t *nb) {
         #endif
         node = node->next;
     }
+    m_tracked_node_unlock();
     return n;
 }
 #endif
@@ -248,12 +284,14 @@ void *m_tracked_calloc(size_t nmemb, size_t size) {
     size_t n = m_tracked_count_links(&nb);
     DEBUG_printf("m_tracked_calloc(%u, %u) -> (%u;%u) %p\n", (int)nmemb, (int)size, (int)n, (int)nb, node);
     #endif
+    m_tracked_node_lock();
     if (MP_STATE_VM(m_tracked_head) != NULL) {
         MP_STATE_VM(m_tracked_head)->prev = node;
     }
     node->prev = NULL;
     node->next = MP_STATE_VM(m_tracked_head);
     MP_STATE_VM(m_tracked_head) = node;
+    m_tracked_node_unlock();
     #if MICROPY_TRACKED_ALLOC_STORE_SIZE
     node->size = nmemb * size;
     #endif
@@ -263,6 +301,41 @@ void *m_tracked_calloc(size_t nmemb, size_t size) {
     return &node->data[0];
 }
 
+void *m_tracked_realloc(void *ptr_in, size_t n_bytes) {
+    // Handle pure allocation
+    if (ptr_in == NULL) {
+        return m_tracked_calloc(1, n_bytes);
+    }
+
+    // Handle pure free
+    if (n_bytes == 0) {
+        m_tracked_free(ptr_in);
+        return NULL;
+    }
+    // To keep the implementation simple, we always allocate a new buffer and copy the old data into it.
+    // This could be optimised if faster performance or lower worst-case memory usage is required.
+
+    // Get old size
+    m_tracked_node_t *old_node = (m_tracked_node_t *)((uint8_t *)ptr_in - sizeof(m_tracked_node_t));
+    size_t old_size = get_tracked_node_size(old_node);
+
+    // Allocate new buffer
+    void *new_ptr = m_tracked_calloc(1, n_bytes);
+    if (new_ptr == NULL) {
+        // Allocation failed, return NULL but leave original pointer intact
+        return NULL;
+    }
+
+    // Copy data (minimum of old and new size)
+    size_t copy_size = MIN(old_size, n_bytes);
+    memcpy(new_ptr, ptr_in, copy_size);
+
+    // Free old buffer
+    m_tracked_free(ptr_in);
+
+    return new_ptr;
+}
+
 void m_tracked_free(void *ptr_in) {
     if (ptr_in == NULL) {
         return;
@@ -270,15 +343,12 @@ void m_tracked_free(void *ptr_in) {
     m_tracked_node_t *node = (m_tracked_node_t *)((uint8_t *)ptr_in - sizeof(m_tracked_node_t));
     #if MICROPY_DEBUG_VERBOSE
     size_t data_bytes;
-    #if MICROPY_TRACKED_ALLOC_STORE_SIZE
-    data_bytes = node->size;
-    #else
-    data_bytes = gc_nbytes(node);
-    #endif
+    data_bytes = get_tracked_node_size(node);
     size_t nb;
     size_t n = m_tracked_count_links(&nb);
     DEBUG_printf("m_tracked_free(%p, [%p, %p], nbytes=%u, links=%u;%u)\n", node, node->prev, node->next, (int)data_bytes, (int)n, (int)nb);
-    #endif
+    #endif // MICROPY_DEBUG_VERBOSE
+    m_tracked_node_lock();
     if (node->next != NULL) {
         node->next->prev = node->prev;
     }
@@ -287,6 +357,7 @@ void m_tracked_free(void *ptr_in) {
     } else {
         MP_STATE_VM(m_tracked_head) = node->next;
     }
+    m_tracked_node_unlock();
     m_free(node
         #if MICROPY_MALLOC_USES_ALLOCATED_SIZE
         #if MICROPY_TRACKED_ALLOC_STORE_SIZE
diff --git a/py/manifest.cmake b/py/manifest.cmake
new file mode 100644
index 00000000000..446fb063b7e
--- /dev/null
+++ b/py/manifest.cmake
@@ -0,0 +1,73 @@
+# Extract c_module() entries from MICROPY_FROZEN_MANIFEST and append them to
+# USER_C_MODULES. Included from py/usermod.cmake; the port must have resolved
+# MICROPY_FROZEN_MANIFEST (and any board-config default / cmake-command-line
+# override) before usermod.cmake runs.
+#
+# Also sets default MICROPY_MANIFEST_* path variables that are forwarded to
+# makemanifest.py here for c_module extraction and again from py/mkrules.cmake
+# for the frozen content generation step, so both passes substitute the same
+# values into manifest path expressions.
+
+if(MICROPY_FROZEN_MANIFEST)
+    # Set default path variables to be passed to makemanifest.py. These are
+    # available in path substitutions inside the manifest. Additional variables
+    # can be set per-board in mpconfigboard.cmake or on the cmake command line.
+    # MICROPY_LIB_DIR is set in py.cmake (included before usermod.cmake).
+    if(NOT DEFINED MICROPY_MANIFEST_PORT_DIR)
+        set(MICROPY_MANIFEST_PORT_DIR ${MICROPY_PORT_DIR})
+    endif()
+    if(NOT DEFINED MICROPY_MANIFEST_BOARD_DIR)
+        set(MICROPY_MANIFEST_BOARD_DIR ${MICROPY_BOARD_DIR})
+    endif()
+    if(NOT DEFINED MICROPY_MANIFEST_MPY_DIR)
+        set(MICROPY_MANIFEST_MPY_DIR ${MICROPY_DIR})
+    endif()
+    if(NOT DEFINED MICROPY_MANIFEST_MPY_LIB_DIR)
+        set(MICROPY_MANIFEST_MPY_LIB_DIR ${MICROPY_LIB_DIR})
+    endif()
+
+    # Find all MICROPY_MANIFEST_* variables and turn them into command line
+    # arguments. Stored in MICROPY_MAKEMANIFEST_ARGS (outside the
+    # MICROPY_MANIFEST_ namespace) so it doesn't self-match the regex above
+    # on subsequent cmake configures.
+    get_cmake_property(_manifest_vars VARIABLES)
+    list(FILTER _manifest_vars INCLUDE REGEX "MICROPY_MANIFEST_.*")
+    set(MICROPY_MAKEMANIFEST_ARGS)
+    foreach(_manifest_var IN LISTS _manifest_vars)
+        list(APPEND MICROPY_MAKEMANIFEST_ARGS "-v")
+        string(REGEX REPLACE "MICROPY_MANIFEST_(.*)" "\\1" _manifest_var_name ${_manifest_var})
+        list(APPEND MICROPY_MAKEMANIFEST_ARGS "${_manifest_var_name}=${${_manifest_var}}")
+    endforeach()
+
+    # Skip extraction during UPDATE_SUBMODULES (require() would fail) and when
+    # micropython-lib is not initialised. Matches py/manifest.mk behaviour:
+    # warn and continue with an empty c_module list rather than fatal.
+    if(EXISTS ${MICROPY_FROZEN_MANIFEST} AND NOT UPDATE_SUBMODULES)
+        if(EXISTS ${MICROPY_LIB_DIR}/README.md)
+            if(NOT Python3_EXECUTABLE)
+                find_package(Python3 REQUIRED COMPONENTS Interpreter)
+            endif()
+
+            execute_process(
+                COMMAND "${Python3_EXECUTABLE}" "${MICROPY_DIR}/tools/makemanifest.py"
+                    --list-c-modules ${MICROPY_MAKEMANIFEST_ARGS} "${MICROPY_FROZEN_MANIFEST}"
+                OUTPUT_VARIABLE MANIFEST_C_MODULES
+                ERROR_VARIABLE MANIFEST_ERROR
+                OUTPUT_STRIP_TRAILING_WHITESPACE
+                RESULT_VARIABLE MANIFEST_RESULT
+            )
+
+            if(NOT MANIFEST_RESULT EQUAL 0)
+                message(FATAL_ERROR "Failed to extract C modules from manifest: ${MICROPY_FROZEN_MANIFEST}\nError: ${MANIFEST_ERROR}")
+            endif()
+
+            if(MANIFEST_C_MODULES)
+                string(REPLACE "\n" ";" MANIFEST_C_MODULES_LIST "${MANIFEST_C_MODULES}")
+                list(APPEND USER_C_MODULES ${MANIFEST_C_MODULES_LIST})
+                list(REMOVE_DUPLICATES USER_C_MODULES)
+            endif()
+        else()
+            message(WARNING "c_module() extraction skipped: micropython-lib not initialised, run 'make BOARD=${MICROPY_BOARD} submodules'")
+        endif()
+    endif()
+endif()
diff --git a/py/manifest.mk b/py/manifest.mk
new file mode 100644
index 00000000000..8b415ebe859
--- /dev/null
+++ b/py/manifest.mk
@@ -0,0 +1,56 @@
+# Manifest processing for freeze(), require(), include(), and c_module().
+# This file handles both C module extraction and frozen Python content generation.
+
+ifneq ($(FROZEN_MANIFEST),)
+
+# Set default path variables to be passed to makemanifest.py. These will be
+# available in path substitutions. Additional variables can be set per-board
+# in mpconfigboard.mk or on the make command line.
+MICROPY_MANIFEST_PORT_DIR ?= $(CURDIR)
+MICROPY_MANIFEST_BOARD_DIR ?= $(BOARD_DIR)
+MICROPY_MANIFEST_MPY_DIR ?= $(TOP)
+MICROPY_MANIFEST_MPY_LIB_DIR ?= $(MPY_LIB_DIR)
+
+# Find all MICROPY_MANIFEST_* variables and turn them into command line
+# arguments. Values must not contain whitespace (GNU make word-splits).
+MANIFEST_VARIABLES = $(foreach var,$(filter MICROPY_MANIFEST_%, $(.VARIABLES)),-v "$(subst MICROPY_MANIFEST_,,$(var))=$($(var))")
+
+# Skip c_module extraction for targets that don't need a build.
+MANIFEST_NON_BUILD_GOALS := clean clean-prog submodules help print-cfg print-def
+MANIFEST_GOALS := $(filter-out $(MANIFEST_NON_BUILD_GOALS),$(or $(MAKECMDGOALS),build))
+
+# Extract C module paths up front so downstream rules see them. Skipped when
+# micropython-lib isn't initialised (require() would fail) or for non-build
+# targets. $(shell) collapses newlines, so c_module() paths cannot contain
+# whitespace on the make side (the cmake side handles it correctly).
+MANIFEST_C_MODULES :=
+ifneq ($(MANIFEST_GOALS),)
+ifeq ($(wildcard $(FROZEN_MANIFEST)),$(FROZEN_MANIFEST))
+ifeq ($(wildcard $(MPY_LIB_DIR)/README.md),$(MPY_LIB_DIR)/README.md)
+MANIFEST_C_MODULES := $(shell $(MAKE_MANIFEST) --list-c-modules $(MANIFEST_VARIABLES) $(FROZEN_MANIFEST))
+# Treat a populated, non-zero .SHELLSTATUS as failure. GNU make < 4.2 (eg
+# macOS 3.81) leaves .SHELLSTATUS empty, in which case skip the check; an
+# empty filter result means "succeed (status was 0, or unavailable)".
+ifneq ($(filter-out 0,$(.SHELLSTATUS)),)
+$(error makemanifest.py --list-c-modules failed (exit $(.SHELLSTATUS)) for $(FROZEN_MANIFEST))
+endif
+else
+$(warning c_module() extraction skipped: micropython-lib not initialised, run 'make submodules')
+endif
+endif
+endif
+
+# Merge manifest c_modules into USER_C_MODULES. $(sort) also de-duplicates;
+# this changes USER_C_MODULES iteration order vs cmake's list(REMOVE_DUPLICATES)
+# which preserves first occurrence. Module include order should not be
+# behaviour-sensitive so this is acceptable.
+USER_C_MODULES := $(sort $(USER_C_MODULES) $(MANIFEST_C_MODULES))
+
+# Frozen content rule, requires $(BUILD) and other variables from the port.
+ifdef BUILD
+$(BUILD)/frozen_content.c: FORCE $(BUILD)/genhdr/qstrdefs.generated.h $(BUILD)/genhdr/root_pointers.h | $(MICROPY_MPYCROSS_DEPENDENCY)
+	$(Q)test -e "$(MPY_LIB_DIR)/README.md" || (echo -e $(HELP_MPY_LIB_SUBMODULE); false)
+	$(Q)$(MAKE_MANIFEST) -o $@ $(MANIFEST_VARIABLES) -b "$(BUILD)" $(if $(MPY_CROSS_FLAGS),-f"$(MPY_CROSS_FLAGS)",) --mpy-tool-flags="$(MPY_TOOL_FLAGS)" $(FROZEN_MANIFEST)
+endif
+
+endif # FROZEN_MANIFEST
diff --git a/py/map.c b/py/map.c
index c18df5a9f33..d40e3dc4d02 100644
--- a/py/map.c
+++ b/py/map.c
@@ -65,14 +65,14 @@
 // The first set of sizes are chosen so the allocation fits exactly in a
 // 4-word GC block, and it's not so important for these small values to be
 // prime.  The latter sizes are prime and increase at an increasing rate.
-STATIC const uint16_t hash_allocation_sizes[] = {
+static const uint16_t hash_allocation_sizes[] = {
     0, 2, 4, 6, 8, 10, 12, // +2
     17, 23, 29, 37, 47, 59, 73, // *1.25
     97, 127, 167, 223, 293, 389, 521, 691, 919, 1223, 1627, 2161, // *1.33
     3229, 4831, 7243, 10861, 16273, 24407, 36607, 54907, // *1.5
 };
 
-STATIC size_t get_hash_alloc_greater_or_equal_to(size_t x) {
+static size_t get_hash_alloc_greater_or_equal_to(size_t x) {
     for (size_t i = 0; i < MP_ARRAY_SIZE(hash_allocation_sizes); i++) {
         if (hash_allocation_sizes[i] >= x) {
             return hash_allocation_sizes[i];
@@ -128,7 +128,7 @@ void mp_map_clear(mp_map_t *map) {
     map->table = NULL;
 }
 
-STATIC void mp_map_rehash(mp_map_t *map) {
+static void mp_map_rehash(mp_map_t *map) {
     size_t old_alloc = map->alloc;
     size_t new_alloc = get_hash_alloc_greater_or_equal_to(map->alloc + 1);
     DEBUG_printf("mp_map_rehash(%p): " UINT_FMT " -> " UINT_FMT "\n", map, old_alloc, new_alloc);
@@ -332,7 +332,7 @@ void mp_set_init(mp_set_t *set, size_t n) {
     set->table = m_new0(mp_obj_t, set->alloc);
 }
 
-STATIC void mp_set_rehash(mp_set_t *set) {
+static void mp_set_rehash(mp_set_t *set) {
     size_t old_alloc = set->alloc;
     mp_obj_t *old_table = set->table;
     set->alloc = get_hash_alloc_greater_or_equal_to(set->alloc + 1);
diff --git a/py/misc.h b/py/misc.h
index 32061a009ed..7afa01789bc 100644
--- a/py/misc.h
+++ b/py/misc.h
@@ -26,6 +26,9 @@
 #ifndef MICROPY_INCLUDED_PY_MISC_H
 #define MICROPY_INCLUDED_PY_MISC_H
 
+#include 
+#include "py/mpconfig.h"
+
 // a mini library of useful types and functions
 
 /** types *******************************************************/
@@ -33,10 +36,24 @@
 #include 
 #include 
 #include 
+#if __cplusplus // Required on at least one compiler to get ULLONG_MAX
+#include 
+#else
+#include 
+#endif
 
 typedef unsigned char byte;
 typedef unsigned int uint;
 
+#ifndef __has_builtin
+#define __has_builtin(x) (0)
+#endif
+#ifndef __has_feature
+// This macro is supported by Clang and gcc>=14
+#define __has_feature(x) (0)
+#endif
+
+
 /** generic ops *************************************************/
 
 #ifndef MIN
@@ -51,11 +68,26 @@ typedef unsigned int uint;
 #define MP_STRINGIFY(x) MP_STRINGIFY_HELPER(x)
 
 // Static assertion macro
+#if __cplusplus
+#define MP_STATIC_ASSERT(cond) static_assert((cond), #cond)
+#elif __GNUC__ >= 5 || __STDC_VERSION__ >= 201112L
+#define MP_STATIC_ASSERT(cond) _Static_assert((cond), #cond)
+#else
 #define MP_STATIC_ASSERT(cond) ((void)sizeof(char[1 - 2 * !(cond)]))
-#if defined(_MSC_VER)
-#define MP_STATIC_ASSERT_NOT_MSC(cond) (1)
+#endif
+
+// In C++ things like comparing extern const pointers are not constant-expressions so cannot be used
+// in MP_STATIC_ASSERT. Note that not all possible compiler versions will reject this. Some gcc versions
+// do, others only with -Werror=vla, msvc always does.
+// The (void) is needed to avoid "left operand of comma operator has no effect [-Werror=unused-value]"
+// when using this macro on the left-hand side of a comma.
+#if defined(_MSC_VER) || defined(__cplusplus)
+#define MP_STATIC_ASSERT_NONCONSTEXPR(cond) ((void)1)
 #else
-#define MP_STATIC_ASSERT_NOT_MSC(cond) MP_STATIC_ASSERT(cond)
+#if __clang__
+#pragma GCC diagnostic ignored "-Wgnu-folding-constant"
+#endif
+#define MP_STATIC_ASSERT_NONCONSTEXPR(cond) ((void)sizeof(char[1 - 2 * !(cond)]))
 #endif
 
 // Round-up integer division
@@ -71,26 +103,19 @@ typedef unsigned int uint;
 #define m_new0(type, num) ((type *)(m_malloc0(sizeof(type) * (num))))
 #define m_new_obj(type) (m_new(type, 1))
 #define m_new_obj_maybe(type) (m_new_maybe(type, 1))
-#define m_new_obj_var(obj_type, var_type, var_num) ((obj_type *)m_malloc(sizeof(obj_type) + sizeof(var_type) * (var_num)))
-#define m_new_obj_var0(obj_type, var_type, var_num) ((obj_type *)m_malloc0(sizeof(obj_type) + sizeof(var_type) * (var_num)))
-#define m_new_obj_var_maybe(obj_type, var_type, var_num) ((obj_type *)m_malloc_maybe(sizeof(obj_type) + sizeof(var_type) * (var_num)))
-#if MICROPY_ENABLE_FINALISER
-#define m_new_obj_with_finaliser(type) ((type *)(m_malloc_with_finaliser(sizeof(type))))
-#define m_new_obj_var_with_finaliser(type, var_type, var_num) ((type *)m_malloc_with_finaliser(sizeof(type) + sizeof(var_type) * (var_num)))
-#else
-#define m_new_obj_with_finaliser(type) m_new_obj(type)
-#define m_new_obj_var_with_finaliser(type, var_type, var_num) m_new_obj_var(type, var_type, var_num)
-#endif
+#define m_new_obj_var(obj_type, var_field, var_type, var_num) ((obj_type *)m_malloc(offsetof(obj_type, var_field) + sizeof(var_type) * (var_num)))
+#define m_new_obj_var0(obj_type, var_field, var_type, var_num) ((obj_type *)m_malloc0(offsetof(obj_type, var_field) + sizeof(var_type) * (var_num)))
+#define m_new_obj_var_maybe(obj_type, var_field, var_type, var_num) ((obj_type *)m_malloc_maybe(offsetof(obj_type, var_field) + sizeof(var_type) * (var_num)))
 #if MICROPY_MALLOC_USES_ALLOCATED_SIZE
 #define m_renew(type, ptr, old_num, new_num) ((type *)(m_realloc((ptr), sizeof(type) * (old_num), sizeof(type) * (new_num))))
 #define m_renew_maybe(type, ptr, old_num, new_num, allow_move) ((type *)(m_realloc_maybe((ptr), sizeof(type) * (old_num), sizeof(type) * (new_num), (allow_move))))
 #define m_del(type, ptr, num) m_free(ptr, sizeof(type) * (num))
-#define m_del_var(obj_type, var_type, var_num, ptr) (m_free(ptr, sizeof(obj_type) + sizeof(var_type) * (var_num)))
+#define m_del_var(obj_type, var_field, var_type, var_num, ptr) (m_free(ptr, offsetof(obj_type, var_field) + sizeof(var_type) * (var_num)))
 #else
 #define m_renew(type, ptr, old_num, new_num) ((type *)(m_realloc((ptr), sizeof(type) * (new_num))))
 #define m_renew_maybe(type, ptr, old_num, new_num, allow_move) ((type *)(m_realloc_maybe((ptr), sizeof(type) * (new_num), (allow_move))))
 #define m_del(type, ptr, num) ((void)(num), m_free(ptr))
-#define m_del_var(obj_type, var_type, var_num, ptr) ((void)(var_num), m_free(ptr))
+#define m_del_var(obj_type, var_field, var_type, var_num, ptr) ((void)(var_num), m_free(ptr))
 #endif
 #define m_del_obj(type, ptr) (m_del(type, ptr, 1))
 
@@ -107,12 +132,13 @@ void *m_realloc(void *ptr, size_t new_num_bytes);
 void *m_realloc_maybe(void *ptr, size_t new_num_bytes, bool allow_move);
 void m_free(void *ptr);
 #endif
-NORETURN void m_malloc_fail(size_t num_bytes);
+MP_NORETURN void m_malloc_fail(size_t num_bytes);
 
 #if MICROPY_TRACKED_ALLOC
 // These alloc/free functions track the pointers in a linked list so the GC does not reclaim
 // them.  They can be used by code that requires traditional C malloc/free semantics.
 void *m_tracked_calloc(size_t nmemb, size_t size);
+void *m_tracked_realloc(void *ptr_in, size_t n_bytes);
 void m_tracked_free(void *ptr_in);
 #endif
 
@@ -210,33 +236,31 @@ void vstr_add_byte(vstr_t *vstr, byte v);
 void vstr_add_char(vstr_t *vstr, unichar chr);
 void vstr_add_str(vstr_t *vstr, const char *str);
 void vstr_add_strn(vstr_t *vstr, const char *str, size_t len);
-void vstr_ins_byte(vstr_t *vstr, size_t byte_pos, byte b);
-void vstr_ins_char(vstr_t *vstr, size_t char_pos, unichar chr);
+char *vstr_ins_blank_bytes(vstr_t *vstr, size_t byte_pos, size_t byte_len);
+static inline void vstr_ins_byte(vstr_t *vstr, size_t byte_pos, byte b) {
+    char *s = vstr_ins_blank_bytes(vstr, byte_pos, 1);
+    *s = b;
+}
+static inline void vstr_ins_char(vstr_t *vstr, size_t char_pos, unichar chr) {
+    // TODO UNICODE
+    char *s = vstr_ins_blank_bytes(vstr, char_pos, 1);
+    *s = (char)chr;
+}
+static inline void vstr_ins_strn(vstr_t *vstr, size_t byte_pos, const char *str, size_t len) {
+    char *s = vstr_ins_blank_bytes(vstr, byte_pos, len);
+    memcpy(s, str, len);
+}
 void vstr_cut_head_bytes(vstr_t *vstr, size_t bytes_to_cut);
 void vstr_cut_tail_bytes(vstr_t *vstr, size_t bytes_to_cut);
 void vstr_cut_out_bytes(vstr_t *vstr, size_t byte_pos, size_t bytes_to_cut);
 void vstr_printf(vstr_t *vstr, const char *fmt, ...);
-
-/** non-dynamic size-bounded variable buffer/string *************/
-
-#define CHECKBUF(buf, max_size) char buf[max_size + 1]; size_t buf##_len = max_size; char *buf##_p = buf;
-#define CHECKBUF_RESET(buf, max_size) buf##_len = max_size; buf##_p = buf;
-#define CHECKBUF_APPEND(buf, src, src_len) \
-    { size_t l = MIN(src_len, buf##_len); \
-      memcpy(buf##_p, src, l); \
-      buf##_len -= l; \
-      buf##_p += l; }
-#define CHECKBUF_APPEND_0(buf) { *buf##_p = 0; }
-#define CHECKBUF_LEN(buf) (buf##_p - buf)
-
 #ifdef va_start
 void vstr_vprintf(vstr_t *vstr, const char *fmt, va_list ap);
 #endif
 
-// Debugging helpers
-int DEBUG_printf(const char *fmt, ...);
+/** debugging helpers *******************************************/
 
-extern mp_uint_t mp_verbose_flag;
+int DEBUG_printf(const char *fmt, ...);
 
 /** float internals *************/
 
@@ -274,6 +298,25 @@ typedef union _mp_float_union_t {
     mp_float_uint_t i;
 } mp_float_union_t;
 
+#if MICROPY_FLOAT_FORMAT_IMPL == MICROPY_FLOAT_FORMAT_IMPL_EXACT
+
+#if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT
+// Exact float conversion requires using internally a bigger sort of floating point
+typedef double mp_large_float_t;
+#elif MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_DOUBLE
+typedef long double mp_large_float_t;
+#endif
+// Always use a 64 bit mantissa for formatting and parsing
+typedef uint64_t mp_large_float_uint_t;
+
+#else // MICROPY_FLOAT_FORMAT_IMPL != MICROPY_FLOAT_FORMAT_IMPL_EXACT
+
+// No bigger floating points
+typedef mp_float_t mp_large_float_t;
+typedef mp_float_uint_t mp_large_float_uint_t;
+
+#endif
+
 #endif // MICROPY_PY_BUILTINS_FLOAT
 
 /** ROM string compression *************/
@@ -336,4 +379,204 @@ typedef const char *mp_rom_error_text_t;
 // For now, forward directly to MP_COMPRESSED_ROM_TEXT.
 #define MP_ERROR_TEXT(x) (mp_rom_error_text_t)MP_COMPRESSED_ROM_TEXT(x)
 
+// Portable implementations of CLZ and CTZ intrinsics
+#ifdef _MSC_VER
+#include 
+
+static inline uint32_t mp_clz(uint32_t x) {
+    unsigned long lz = 0;
+    return _BitScanReverse(&lz, x) ? (sizeof(x) * 8 - 1) - lz : 0;
+}
+
+static inline uint32_t mp_clzl(unsigned long x) {
+    unsigned long lz = 0;
+    return _BitScanReverse(&lz, x) ? (sizeof(x) * 8 - 1) - lz : 0;
+}
+
+#ifdef _WIN64
+static inline uint32_t mp_clzll(unsigned long long x) {
+    unsigned long lz = 0;
+    return _BitScanReverse64(&lz, x) ? (sizeof(x) * 8 - 1) - lz : 0;
+}
+#else
+// Microsoft don't ship _BitScanReverse64 on Win32, so emulate it
+static inline uint32_t mp_clzll(unsigned long long x) {
+    unsigned long h = x >> 32;
+    return h ? mp_clzl(h) : (mp_clzl((unsigned long)x) + 32);
+}
+#endif
+
+static inline uint32_t mp_ctz(uint32_t x) {
+    unsigned long tz = 0;
+    return _BitScanForward(&tz, x) ? tz : 0;
+}
+
+static inline uint32_t mp_popcount(uint32_t x) {
+    return __popcnt(x);
+}
+#else // _MSC_VER
+#define mp_clz(x) __builtin_clz(x)
+#define mp_clzl(x) __builtin_clzl(x)
+#define mp_clzll(x) __builtin_clzll(x)
+#define mp_ctz(x) __builtin_ctz(x)
+#if __has_builtin(__builtin_popcount)
+#define mp_popcount(x) __builtin_popcount(x)
+#else
+static inline uint32_t mp_popcount(uint32_t x) {
+    x = x - ((x >> 1) & 0x55555555);
+    x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
+    x = (x + (x >> 4)) & 0x0F0F0F0F;
+    return (x * 0x01010101) >> 24;
+}
+#endif // __has_builtin(__builtin_popcount)
+#endif // _MSC_VER
+
+#define MP_FIT_UNSIGNED(bits, value) (((value) & (~0U << (bits))) == 0)
+#define MP_FIT_SIGNED(bits, value) \
+    (MP_FIT_UNSIGNED(((bits) - 1), (value)) || \
+    (((value) & (~0U << ((bits) - 1))) == (~0U << ((bits) - 1))))
+
+// mp_int_t can be larger than long, i.e. Windows 64-bit, nan-box variants
+static inline uint32_t mp_clz_mpi(mp_int_t x) {
+    #ifdef __XC16__
+    mp_uint_t mask = MP_OBJ_WORD_MSBIT_HIGH;
+    mp_uint_t zeroes = 0;
+    while (mask != 0) {
+        if (mask & (mp_uint_t)x) {
+            break;
+        }
+        zeroes++;
+        mask >>= 1;
+    }
+    return zeroes;
+    #else
+    #if MP_INT_MAX == INT_MAX
+    return mp_clz((unsigned)x);
+    #elif MP_INT_MAX == LONG_MAX
+    return mp_clzl((unsigned long)x);
+    #elif MP_INT_MAX == LLONG_MAX
+    return mp_clzll((unsigned long long)x);
+    #else
+    #error Unexpected MP_INT_MAX value
+    #endif
+    #endif
+}
+
+// Overflow-checked operations
+
+// Integer overflow builtins were added to GCC 5, but __has_builtin only in GCC 10
+//
+// Note that the builtins has a defined result when overflow occurs, whereas the custom
+// functions below don't update the result if an overflow would occur (to avoid UB).
+#define MP_GCC_HAS_BUILTIN_OVERFLOW (__GNUC__ >= 5)
+
+#if MICROPY_USE_GCC_MUL_OVERFLOW_INTRINSIC
+
+#define mp_mul_ull_overflow __builtin_umulll_overflow
+#define mp_mul_ll_overflow __builtin_smulll_overflow
+static inline bool mp_mul_mp_int_t_overflow(mp_int_t x, mp_int_t y, mp_int_t *res) {
+    // __builtin_mul_overflow is a type-generic function, this inline ensures the argument
+    // types are checked to match mp_int_t.
+    return __builtin_mul_overflow(x, y, res);
+}
+
+#else
+
+bool mp_mul_ll_overflow(long long int x, long long int y, long long int *res);
+bool mp_mul_mp_int_t_overflow(mp_int_t x, mp_int_t y, mp_int_t *res);
+static inline bool mp_mul_ull_overflow(unsigned long long int x, unsigned long long int y, unsigned long long int *res) {
+    if (y > 0 && x > (ULLONG_MAX / y)) {
+        return true; // overflow
+    }
+    *res = x * y;
+    return false;
+}
+
+#endif
+
+#if __has_builtin(__builtin_saddll_overflow) || MP_GCC_HAS_BUILTIN_OVERFLOW
+#define mp_add_ll_overflow __builtin_saddll_overflow
+#else
+static inline bool mp_add_ll_overflow(long long int lhs, long long int rhs, long long int *res) {
+    bool overflow;
+
+    if (rhs > 0) {
+        overflow = (lhs > LLONG_MAX - rhs);
+    } else {
+        overflow = (lhs < LLONG_MIN - rhs);
+    }
+
+    if (!overflow) {
+        *res = lhs + rhs;
+    }
+
+    return overflow;
+}
+#endif
+
+#if __has_builtin(__builtin_ssubll_overflow) || MP_GCC_HAS_BUILTIN_OVERFLOW
+#define mp_sub_ll_overflow __builtin_ssubll_overflow
+#else
+static inline bool mp_sub_ll_overflow(long long int lhs, long long int rhs, long long int *res) {
+    bool overflow;
+
+    if (rhs > 0) {
+        overflow = (lhs < LLONG_MIN + rhs);
+    } else {
+        overflow = (lhs > LLONG_MAX + rhs);
+    }
+
+    if (!overflow) {
+        *res = lhs - rhs;
+    }
+
+    return overflow;
+}
+#endif
+
+
+// Helper macros for detecting if sanitizers are enabled
+//
+// Use sparingly, not for masking issues reported by sanitizers!
+//
+// Can be detected automatically in Clang and gcc>=14, need to be
+// set manually otherwise.
+#ifndef MP_UBSAN
+#define MP_UBSAN __has_feature(undefined_behavior_sanitizer)
+#endif
+
+#ifndef MP_ASAN
+#define MP_ASAN __has_feature(address_sanitizer)
+#endif
+
+#ifndef MP_SANITIZER_BUILD
+#define MP_SANITIZER_BUILD (MP_UBSAN || MP_ASAN)
+#endif
+
+// halfword/word/longword swapping macros
+
+#if __has_builtin(__builtin_bswap16)
+#define MP_BSWAP16(x) __builtin_bswap16(x)
+#else
+#define MP_BSWAP16(x) ((uint16_t)((((x) & 0xFF) << 8) | (((x) >> 8) & 0xFF)))
+#endif
+
+#if __has_builtin(__builtin_bswap32)
+#define MP_BSWAP32(x) __builtin_bswap32(x)
+#else
+#define MP_BSWAP32(x) \
+    ((uint32_t)((((x) & 0xFF) << 24) | (((x) & 0xFF00) << 8) | \
+    (((x) >> 8) & 0xFF00) | (((x) >> 24) & 0xFF)))
+#endif
+
+#if __has_builtin(__builtin_bswap64)
+#define MP_BSWAP64(x) __builtin_bswap64(x)
+#else
+#define MP_BSWAP64(x) \
+    ((uint64_t)((((x) & 0xFF) << 56) | (((x) & 0xFF00) << 40) | \
+    (((x) & 0xFF0000) << 24) | (((x) & 0xFF000000) << 8) | \
+    (((x) >> 8) & 0xFF000000) | (((x) >> 24) & 0xFF0000) | \
+    (((x) >> 40) & 0xFF00) | (((x) >> 56) & 0xFF)))
+#endif
+
 #endif // MICROPY_INCLUDED_PY_MISC_H
diff --git a/py/mkenv.mk b/py/mkenv.mk
index 5368279e290..4656d113665 100644
--- a/py/mkenv.mk
+++ b/py/mkenv.mk
@@ -12,22 +12,7 @@ endif
 THIS_MAKEFILE := $(lastword $(MAKEFILE_LIST))
 TOP := $(patsubst %/py/mkenv.mk,%,$(THIS_MAKEFILE))
 
-# Turn on increased build verbosity by defining BUILD_VERBOSE in your main
-# Makefile or in your environment. You can also use V=1 on the make command
-# line.
-
-ifeq ("$(origin V)", "command line")
-BUILD_VERBOSE=$(V)
-endif
-ifndef BUILD_VERBOSE
-$(info Use make V=1 or set BUILD_VERBOSE in your environment to increase build verbosity.)
-BUILD_VERBOSE = 0
-endif
-ifeq ($(BUILD_VERBOSE),0)
-Q = @
-else
-Q =
-endif
+include $(TOP)/py/verbose.mk
 
 # default settings; can be overridden in main Makefile
 
@@ -42,6 +27,7 @@ SED = sed
 CAT = cat
 TOUCH = touch
 PYTHON = python3
+ZIP = zip
 
 AS = $(CROSS_COMPILE)as
 CC = $(CROSS_COMPILE)gcc
@@ -53,6 +39,7 @@ OBJCOPY = $(CROSS_COMPILE)objcopy
 SIZE = $(CROSS_COMPILE)size
 STRIP = $(CROSS_COMPILE)strip
 AR = $(CROSS_COMPILE)ar
+WINDRES = $(CROSS_COMPILE)windres
 
 MAKE_MANIFEST = $(PYTHON) $(TOP)/tools/makemanifest.py
 MAKE_FROZEN = $(PYTHON) $(TOP)/tools/make-frozen.py
diff --git a/py/mkrules.cmake b/py/mkrules.cmake
index 7eb5fcf0183..0903f81d897 100644
--- a/py/mkrules.cmake
+++ b/py/mkrules.cmake
@@ -14,6 +14,33 @@ set(MICROPY_MODULEDEFS "${MICROPY_GENHDR_DIR}/moduledefs.h")
 set(MICROPY_ROOT_POINTERS_SPLIT "${MICROPY_GENHDR_DIR}/root_pointers.split")
 set(MICROPY_ROOT_POINTERS_COLLECTED "${MICROPY_GENHDR_DIR}/root_pointers.collected")
 set(MICROPY_ROOT_POINTERS "${MICROPY_GENHDR_DIR}/root_pointers.h")
+set(MICROPY_COMPRESSED_SPLIT "${MICROPY_GENHDR_DIR}/compressed.split")
+set(MICROPY_COMPRESSED_COLLECTED "${MICROPY_GENHDR_DIR}/compressed.collected")
+set(MICROPY_COMPRESSED_DATA "${MICROPY_GENHDR_DIR}/compressed.data.h")
+
+if(NOT MICROPY_PREVIEW_VERSION_2)
+    set(MICROPY_PREVIEW_VERSION_2 0)
+endif()
+
+# Set the board name.
+if(MICROPY_BOARD)
+    if(MICROPY_BOARD_VARIANT)
+        set(MICROPY_BOARD_BUILD_NAME ${MICROPY_BOARD}-${MICROPY_BOARD_VARIANT})
+    else()
+        set(MICROPY_BOARD_BUILD_NAME ${MICROPY_BOARD})
+    endif()
+
+    target_compile_definitions(${MICROPY_TARGET} PRIVATE
+        MICROPY_BOARD_BUILD_NAME="${MICROPY_BOARD_BUILD_NAME}"
+    )
+endif()
+
+# Need to do this before extracting MICROPY_CPP_DEF below.
+if(MICROPY_ROM_TEXT_COMPRESSION)
+    target_compile_definitions(${MICROPY_TARGET} PUBLIC
+        MICROPY_ROM_TEXT_COMPRESSION=\(1\)
+    )
+endif()
 
 # Need to do this before extracting MICROPY_CPP_DEF below. Rest of frozen
 # manifest handling is at the end of this file.
@@ -24,6 +51,12 @@ if(MICROPY_FROZEN_MANIFEST)
     )
 endif()
 
+if(MICROPY_PREVIEW_VERSION_2)
+    target_compile_definitions(${MICROPY_TARGET} PUBLIC
+        MICROPY_PREVIEW_VERSION_2=\(1\)
+    )
+endif()
+
 # Provide defaults for preprocessor flags if not already defined
 if(NOT MICROPY_CPP_FLAGS)
     get_target_property(MICROPY_CPP_INC ${MICROPY_TARGET} INCLUDE_DIRECTORIES)
@@ -43,6 +76,15 @@ foreach(_arg ${MICROPY_CPP_DEF})
 endforeach()
 list(APPEND MICROPY_CPP_FLAGS ${MICROPY_CPP_FLAGS_EXTRA})
 
+# Include anything passed in via CFLAGS_EXTRA
+# in both MICROPY_CPP_FLAGS and CMAKE_C_FLAGS
+if(DEFINED ENV{CFLAGS_EXTRA})
+  set(CFLAGS_EXTRA $ENV{CFLAGS_EXTRA})
+  string(APPEND CMAKE_C_FLAGS " ${CFLAGS_EXTRA}")  # ... not a list
+  separate_arguments(CFLAGS_EXTRA)
+  list(APPEND MICROPY_CPP_FLAGS ${CFLAGS_EXTRA})  # ... a list
+endif()
+
 find_package(Python3 REQUIRED COMPONENTS Interpreter)
 
 target_sources(${MICROPY_TARGET} PRIVATE
@@ -52,6 +94,18 @@ target_sources(${MICROPY_TARGET} PRIVATE
     ${MICROPY_ROOT_POINTERS}
 )
 
+if(MICROPY_ROM_TEXT_COMPRESSION)
+    target_sources(${MICROPY_TARGET} PRIVATE
+        ${MICROPY_COMPRESSED_DATA}
+    )
+endif()
+
+# Ensure genhdr directory is removed on clean
+
+set_property(TARGET ${MICROPY_TARGET} APPEND PROPERTY ADDITIONAL_CLEAN_FILES
+    "${MICROPY_GENHDR_DIR}"
+)
+
 # Command to force the build of another command
 
 # Generate mpversion.h
@@ -89,6 +143,7 @@ add_custom_command(
 add_custom_command(
     OUTPUT ${MICROPY_QSTRDEFS_COLLECTED}
     COMMAND ${Python3_EXECUTABLE} ${MICROPY_PY_DIR}/makeqstrdefs.py cat qstr _ ${MICROPY_GENHDR_DIR}/qstr ${MICROPY_QSTRDEFS_COLLECTED}
+    BYPRODUCTS "${MICROPY_QSTRDEFS_COLLECTED}.hash"
     DEPENDS ${MICROPY_QSTRDEFS_SPLIT}
     VERBATIM
     COMMAND_EXPAND_LISTS
@@ -126,6 +181,7 @@ add_custom_command(
 add_custom_command(
     OUTPUT ${MICROPY_MODULEDEFS_COLLECTED}
     COMMAND ${Python3_EXECUTABLE} ${MICROPY_PY_DIR}/makeqstrdefs.py cat module _ ${MICROPY_GENHDR_DIR}/module ${MICROPY_MODULEDEFS_COLLECTED}
+    BYPRODUCTS "${MICROPY_MODULEDEFS_COLLECTED}.hash"
     DEPENDS ${MICROPY_MODULEDEFS_SPLIT}
     VERBATIM
     COMMAND_EXPAND_LISTS
@@ -151,6 +207,7 @@ add_custom_command(
 add_custom_command(
     OUTPUT ${MICROPY_ROOT_POINTERS_COLLECTED}
     COMMAND ${Python3_EXECUTABLE} ${MICROPY_PY_DIR}/makeqstrdefs.py cat root_pointer _ ${MICROPY_GENHDR_DIR}/root_pointer ${MICROPY_ROOT_POINTERS_COLLECTED}
+    BYPRODUCTS "${MICROPY_ROOT_POINTERS_COLLECTED}.hash"
     DEPENDS ${MICROPY_ROOT_POINTERS_SPLIT}
     VERBATIM
     COMMAND_EXPAND_LISTS
@@ -162,23 +219,56 @@ add_custom_command(
     DEPENDS ${MICROPY_ROOT_POINTERS_COLLECTED} ${MICROPY_PY_DIR}/make_root_pointers.py
 )
 
+# Generate compressed.data.h
+
+add_custom_command(
+    OUTPUT ${MICROPY_COMPRESSED_SPLIT}
+    COMMAND ${Python3_EXECUTABLE} ${MICROPY_PY_DIR}/makeqstrdefs.py split compress ${MICROPY_QSTRDEFS_LAST} ${MICROPY_GENHDR_DIR}/compress _
+    COMMAND touch ${MICROPY_COMPRESSED_SPLIT}
+    DEPENDS ${MICROPY_QSTRDEFS_LAST}
+    VERBATIM
+    COMMAND_EXPAND_LISTS
+)
+
+add_custom_command(
+    OUTPUT ${MICROPY_COMPRESSED_COLLECTED}
+    COMMAND ${Python3_EXECUTABLE} ${MICROPY_PY_DIR}/makeqstrdefs.py cat compress _ ${MICROPY_GENHDR_DIR}/compress ${MICROPY_COMPRESSED_COLLECTED}
+    BYPRODUCTS "${MICROPY_COMPRESSED_COLLECTED}.hash"
+    DEPENDS ${MICROPY_COMPRESSED_SPLIT}
+    VERBATIM
+    COMMAND_EXPAND_LISTS
+)
+
+add_custom_command(
+    OUTPUT ${MICROPY_COMPRESSED_DATA}
+    COMMAND ${Python3_EXECUTABLE} ${MICROPY_PY_DIR}/makecompresseddata.py ${MICROPY_COMPRESSED_COLLECTED} > ${MICROPY_COMPRESSED_DATA}
+    DEPENDS ${MICROPY_COMPRESSED_COLLECTED} ${MICROPY_PY_DIR}/makecompresseddata.py
+)
+
 # Build frozen code if enabled
 
 if(MICROPY_FROZEN_MANIFEST)
     set(MICROPY_FROZEN_CONTENT "${CMAKE_BINARY_DIR}/frozen_content.c")
 
+    set_property(TARGET ${MICROPY_TARGET} APPEND PROPERTY ADDITIONAL_CLEAN_FILES
+        "${CMAKE_BINARY_DIR}/frozen_mpy"
+    )
+
     target_sources(${MICROPY_TARGET} PRIVATE
         ${MICROPY_FROZEN_CONTENT}
     )
 
     # Note: target_compile_definitions already added earlier.
 
-    if(NOT MICROPY_LIB_DIR)
-        string(CONCAT GIT_SUBMODULES "${GIT_SUBMODULES} " lib/micropython-lib)
-        set(MICROPY_LIB_DIR ${MICROPY_DIR}/lib/micropython-lib)
+    # Ensure micropython-lib is included in submodule updates, but only when
+    # MICROPY_LIB_DIR points at the in-tree submodule location; a user
+    # overriding MICROPY_LIB_DIR to an out-of-tree checkout shouldn't have
+    # the submodule updated.
+    if(MICROPY_LIB_DIR STREQUAL "${MICROPY_DIR}/lib/micropython-lib")
+        list(APPEND GIT_SUBMODULES lib/micropython-lib)
     endif()
 
-    if(NOT (${ECHO_SUBMODULES}) AND NOT EXISTS ${MICROPY_LIB_DIR}/README.md)
+    if(NOT UPDATE_SUBMODULES AND NOT EXISTS ${MICROPY_LIB_DIR}/README.md)
         message(FATAL_ERROR " micropython-lib not initialized.\n Run 'make BOARD=${MICROPY_BOARD} submodules'")
     endif()
 
@@ -191,16 +281,29 @@ if(MICROPY_FROZEN_MANIFEST)
         if(NOT MICROPY_MAKE_EXECUTABLE)
             set(MICROPY_MAKE_EXECUTABLE make)
         endif()
+        # Clear FROZEN_MANIFEST/USER_C_MODULES in the mpy-cross sub-make so a
+        # shell-env FROZEN_MANIFEST=... doesn't leak in and trigger
+        # manifest.mk against the wrong cwd. mkrules.mk does the same for the
+        # make-based port path.
         add_custom_command(
             OUTPUT ${MICROPY_MPYCROSS_DEPENDENCY}
-            COMMAND ${MICROPY_MAKE_EXECUTABLE} -C ${MICROPY_DIR}/mpy-cross
+            COMMAND ${MICROPY_MAKE_EXECUTABLE} -C ${MICROPY_DIR}/mpy-cross USER_C_MODULES= FROZEN_MANIFEST=
         )
     endif()
 
+    if(NOT MICROPY_CROSS_FLAGS)
+        set(MICROPY_CROSS_FLAGS "")
+    else()
+        set(MICROPY_CROSS_FLAGS "-f${MICROPY_CROSS_FLAGS}")
+    endif()
+
+    # MICROPY_MAKEMANIFEST_ARGS is populated by py/manifest.cmake (included via
+    # py/usermod.cmake before this file). Reuse that list so both the c_module
+    # extraction pass and the frozen content step substitute the same values.
     add_custom_target(
         BUILD_FROZEN_CONTENT ALL
         BYPRODUCTS ${MICROPY_FROZEN_CONTENT}
-        COMMAND ${Python3_EXECUTABLE} ${MICROPY_DIR}/tools/makemanifest.py -o ${MICROPY_FROZEN_CONTENT} -v "MPY_DIR=${MICROPY_DIR}" -v "MPY_LIB_DIR=${MICROPY_LIB_DIR}" -v "PORT_DIR=${MICROPY_PORT_DIR}" -v "BOARD_DIR=${MICROPY_BOARD_DIR}" -b "${CMAKE_BINARY_DIR}" -f${MICROPY_CROSS_FLAGS} --mpy-tool-flags=${MICROPY_MPY_TOOL_FLAGS} ${MICROPY_FROZEN_MANIFEST}
+        COMMAND ${Python3_EXECUTABLE} ${MICROPY_DIR}/tools/makemanifest.py -o ${MICROPY_FROZEN_CONTENT} ${MICROPY_MAKEMANIFEST_ARGS} -b "${CMAKE_BINARY_DIR}" ${MICROPY_CROSS_FLAGS} --mpy-tool-flags=${MICROPY_MPY_TOOL_FLAGS} ${MICROPY_FROZEN_MANIFEST}
         DEPENDS
             ${MICROPY_QSTRDEFS_GENERATED}
             ${MICROPY_ROOT_POINTERS}
@@ -209,18 +312,29 @@ if(MICROPY_FROZEN_MANIFEST)
     )
 endif()
 
-# Update submodules
-if(ECHO_SUBMODULES)
-    # If cmake is run with GIT_SUBMODULES defined on command line, process the port / board
-    # settings then print the final GIT_SUBMODULES variable and exit.
-    # Note: the GIT_SUBMODULES is done via echo rather than message, as message splits
-    # the output onto multiple lines
-    execute_process(COMMAND ${CMAKE_COMMAND} -E echo "GIT_SUBMODULES=${GIT_SUBMODULES}")
-    message(FATAL_ERROR "Done")
-endif()
+# Update submodules, this is invoked on some ports via 'make submodules'.
+#
+# Note: This logic has a Makefile equivalent in py/mkrules.mk
+if(UPDATE_SUBMODULES AND GIT_SUBMODULES)
+    macro(run_git)
+      execute_process(COMMAND git ${ARGV} WORKING_DIRECTORY ${MICROPY_DIR}
+          RESULT_VARIABLE RES)
+    endmacro()
+
+    list(JOIN GIT_SUBMODULES " " GIT_SUBMODULES_MSG)
+    message("Updating submodules: ${GIT_SUBMODULES_MSG}")
+    run_git(submodule sync ${GIT_SUBMODULES})
+    if(RES EQUAL 0)
+        # If available, do blobless partial clones of submodules to save time and space.
+        # A blobless partial clone lazily fetches data as needed, but has all the metadata available (tags, etc.).
+        run_git(submodule update --init --filter=blob:none ${GIT_SUBMODULES})
+        # Fallback to standard submodule update if blobless isn't available (earlier than git 2.36.0)
+        if (NOT RES EQUAL 0)
+            run_git(submodule update --init ${GIT_SUBMODULES})
+        endif()
+    endif()
 
-# Display BOARD_VARIANTS
-if(ECHO_BOARD_VARIANTS)
-    execute_process(COMMAND ${CMAKE_COMMAND} -E echo "BOARD_VARIANTS=${BOARD_VARIANTS}")
-    message(FATAL_ERROR "Done")
+    if (NOT RES EQUAL 0)
+        message(FATAL_ERROR "Submodule update failed")
+    endif()
 endif()
diff --git a/py/mkrules.mk b/py/mkrules.mk
index aa7f77bd2a0..a04cd141943 100644
--- a/py/mkrules.mk
+++ b/py/mkrules.mk
@@ -4,6 +4,13 @@ THIS_MAKEFILE = $(lastword $(MAKEFILE_LIST))
 include $(dir $(THIS_MAKEFILE))mkenv.mk
 endif
 
+# Enable in-progress/breaking changes that are slated for MicroPython 2.x.
+MICROPY_PREVIEW_VERSION_2 ?= 0
+
+ifeq ($(MICROPY_PREVIEW_VERSION_2),1)
+CFLAGS += -DMICROPY_PREVIEW_VERSION_2=1
+endif
+
 HELP_BUILD_ERROR ?= "See \033[1;31mhttps://github.com/micropython/micropython/wiki/Build-Troubleshooting\033[0m"
 HELP_MPY_LIB_SUBMODULE ?= "\033[1;31mError: micropython-lib submodule is not initialized.\033[0m Run 'make submodules'"
 
@@ -20,10 +27,33 @@ OBJ_EXTRA_ORDER_DEPS += $(HEADER_BUILD)/compressed.data.h
 CFLAGS += -DMICROPY_ROM_TEXT_COMPRESSION=1
 endif
 
+# Set the variant or board name.
+ifneq ($(VARIANT),)
+CFLAGS += -DMICROPY_BOARD_BUILD_NAME=\"$(VARIANT)\"
+else ifneq ($(BOARD),)
+ifeq ($(BOARD_VARIANT),)
+CFLAGS += -DMICROPY_BOARD_BUILD_NAME=\"$(BOARD)\"
+else
+CFLAGS += -DMICROPY_BOARD_BUILD_NAME=\"$(BOARD)-$(BOARD_VARIANT)\"
+endif
+endif
+
+# Add default C++ compiler flags based on CFLAGS. For use with C++ user modules.
+CXXFLAGS += $(filter-out -Wmissing-prototypes -Wold-style-definition -std=gnu99 -std=c99 -std=c11,$(CFLAGS) $(CXXFLAGS_MOD))
+
+# Add LDFLAGS to link libstdc++ on bare metal ports. Added only if a port has
+# -nostdlib in LDFLAGS and C++ source files are provided.
+ifneq ($(findstring nostdlib,"$(LDFLAGS)"),)
+ifneq ($(SRC_CXX)$(SRC_USERMOD_CXX)$(SRC_USERMOD_LIB_CXX),)
+LIBSTDCPP_FILE_NAME = "$(shell $(CXX) $(CXXFLAGS) -print-file-name=libstdc++.a)"
+LDFLAGS += -L"$(shell dirname $(LIBSTDCPP_FILE_NAME))"
+endif
+endif
+
 # QSTR generation uses the same CFLAGS, with these modifications.
 QSTR_GEN_FLAGS = -DNO_QSTR
-# Note: := to force evalulation immediately.
-QSTR_GEN_CFLAGS := $(CFLAGS)
+# Note: := to force evaluation immediately.
+QSTR_GEN_CFLAGS := $(filter-out -g%,$(CFLAGS))
 QSTR_GEN_CFLAGS += $(QSTR_GEN_FLAGS)
 QSTR_GEN_CXXFLAGS := $(CXXFLAGS)
 QSTR_GEN_CXXFLAGS += $(QSTR_GEN_FLAGS)
@@ -44,19 +74,19 @@ QSTR_GEN_CXXFLAGS += $(QSTR_GEN_FLAGS)
 # can be located. By following this scheme, it allows a single build rule
 # to be used to compile all .c files.
 
-vpath %.S . $(TOP) $(USER_C_MODULES)
+vpath %.S . $(TOP) $(USER_C_MODULES) $(USERMOD_DIR_PARENTS)
 $(BUILD)/%.o: %.S
 	$(ECHO) "CC $<"
 	$(Q)$(CC) $(CFLAGS) -c -o $@ $<
 
-vpath %.s . $(TOP) $(USER_C_MODULES)
+vpath %.s . $(TOP) $(USER_C_MODULES) $(USERMOD_DIR_PARENTS)
 $(BUILD)/%.o: %.s
 	$(ECHO) "AS $<"
-	$(Q)$(AS) -o $@ $<
+	$(Q)$(AS) $(AFLAGS) -o $@ $<
 
 define compile_c
 $(ECHO) "CC $<"
-$(Q)$(CC) $(CFLAGS) -c -MD -o $@ $< || (echo -e $(HELP_BUILD_ERROR); false)
+$(Q)$(CC) $(CFLAGS) -c -MD -MF $(@:.o=.d) -o $@ $< || (echo -e $(HELP_BUILD_ERROR); false)
 @# The following fixes the dependency file.
 @# See http://make.paulandlesley.org/autodep.html for details.
 @# Regex adjusted from the above to play better with Windows paths, etc.
@@ -68,7 +98,7 @@ endef
 
 define compile_cxx
 $(ECHO) "CXX $<"
-$(Q)$(CXX) $(CXXFLAGS) -c -MD -o $@ $< || (echo -e $(HELP_BUILD_ERROR); false)
+$(Q)$(CXX) $(CXXFLAGS) -c -MD -MF $(@:.o=.d) -o $@ $< || (echo -e $(HELP_BUILD_ERROR); false)
 @# The following fixes the dependency file.
 @# See http://make.paulandlesley.org/autodep.html for details.
 @# Regex adjusted from the above to play better with Windows paths, etc.
@@ -78,18 +108,26 @@ $(Q)$(CXX) $(CXXFLAGS) -c -MD -o $@ $< || (echo -e $(HELP_BUILD_ERROR); false)
   $(RM) -f $(@:.o=.d)
 endef
 
-vpath %.c . $(TOP) $(USER_C_MODULES)
+vpath %.c . $(TOP) $(USER_C_MODULES) $(USERMOD_DIR_PARENTS)
 $(BUILD)/%.o: %.c
 	$(call compile_c)
 
-vpath %.cpp . $(TOP) $(USER_C_MODULES)
+vpath %.cpp . $(TOP) $(USER_C_MODULES) $(USERMOD_DIR_PARENTS)
 $(BUILD)/%.o: %.cpp
 	$(call compile_cxx)
 
-$(BUILD)/%.pp: %.c
+$(BUILD)/%.pp: %.c FORCE
 	$(ECHO) "PreProcess $<"
 	$(Q)$(CPP) $(CFLAGS) -Wp,-C,-dD,-dI -o $@ $<
 
+.PHONY: $(BUILD)/%.sz
+$(BUILD)/%.sz: $(BUILD)/%.o
+	$(Q)$(SIZE) $<
+
+# Special case for compiling auto-generated source files.
+$(BUILD)/%.o: $(BUILD)/%.c
+	$(call compile_c)
+
 # The following rule uses | to create an order only prerequisite. Order only
 # prerequisites only get built if they don't exist. They don't cause timestamp
 # checking to be performed.
@@ -163,9 +201,12 @@ $(HEADER_BUILD):
 	$(MKDIR) -p $@
 
 ifneq ($(MICROPY_MPYCROSS_DEPENDENCY),)
-# to automatically build mpy-cross, if needed
+# Build mpy-cross automatically if needed. Clear USER_C_MODULES and
+# FROZEN_MANIFEST so a port build with either set doesn't leak them into the
+# mpy-cross sub-make and cause manifest.mk there to parse the port's manifest
+# from the wrong cwd.
 $(MICROPY_MPYCROSS_DEPENDENCY):
-	$(MAKE) -C $(abspath $(dir $@)..)
+	$(MAKE) -C "$(abspath $(dir $@)..)" USER_C_MODULES= FROZEN_MANIFEST=
 endif
 
 ifneq ($(FROZEN_DIR),)
@@ -183,10 +224,10 @@ ifeq ($(MPY_LIB_DIR),$(MPY_LIB_SUBMODULE_DIR))
 GIT_SUBMODULES += lib/micropython-lib
 endif
 
-# to build frozen_content.c from a manifest
-$(BUILD)/frozen_content.c: FORCE $(BUILD)/genhdr/qstrdefs.generated.h $(BUILD)/genhdr/root_pointers.h | $(MICROPY_MPYCROSS_DEPENDENCY)
-	$(Q)test -e "$(MPY_LIB_DIR)/README.md" || (echo -e $(HELP_MPY_LIB_SUBMODULE); false)
-	$(Q)$(MAKE_MANIFEST) -o $@ -v "MPY_DIR=$(TOP)" -v "MPY_LIB_DIR=$(MPY_LIB_DIR)" -v "PORT_DIR=$(shell pwd)" -v "BOARD_DIR=$(BOARD_DIR)" -b "$(BUILD)" $(if $(MPY_CROSS_FLAGS),-f"$(MPY_CROSS_FLAGS)",) --mpy-tool-flags="$(MPY_TOOL_FLAGS)" $(FROZEN_MANIFEST)
+# Set compile options needed to enable frozen code.
+CFLAGS += -DMICROPY_QSTR_EXTRA_POOL=mp_qstr_frozen_const_pool
+CFLAGS += -DMICROPY_MODULE_FROZEN_MPY
+CFLAGS += -DMICROPY_MODULE_FROZEN_STR
 endif
 
 ifneq ($(PROG),)
@@ -205,9 +246,11 @@ $(BUILD)/$(PROG): $(OBJ)
 	$(ECHO) "LINK $@"
 # Do not pass COPT here - it's *C* compiler optimizations. For example,
 # we may want to compile using Thumb, but link with non-Thumb libc.
-	$(Q)$(CC) -o $@ $^ $(LIB) $(LDFLAGS)
+	$(Q)$(CC) -o $@ $^ $(LIBS) $(LDFLAGS)
 ifndef DEBUG
+ifdef STRIP
 	$(Q)$(STRIP) $(STRIPFLAGS_EXTRA) $@
+endif
 endif
 	$(Q)$(SIZE) $$(find $(BUILD) -path "$(BUILD)/build/frozen*.o") $@
 
@@ -219,11 +262,17 @@ clean-prog:
 .PHONY: clean-prog
 endif
 
+# If available, do blobless partial clones of submodules to save time and space.
+# A blobless partial clone lazily fetches data as needed, but has all the metadata available (tags, etc.).
+# Fallback to standard submodule update if blobless isn't available (earlier than 2.36.0)
+#
+# Note: This target has a CMake equivalent in py/mkrules.cmake
 submodules:
 	$(ECHO) "Updating submodules: $(GIT_SUBMODULES)"
 ifneq ($(GIT_SUBMODULES),)
-	$(Q)git submodule sync $(addprefix $(TOP)/,$(GIT_SUBMODULES))
-	$(Q)git submodule update --init $(addprefix $(TOP)/,$(GIT_SUBMODULES))
+	$(Q)cd $(TOP) && git submodule sync $(GIT_SUBMODULES)
+	$(Q)cd $(TOP) && git submodule update --init --filter=blob:none $(GIT_SUBMODULES) 2>/dev/null || \
+	  git submodule update --init $(GIT_SUBMODULES)
 endif
 .PHONY: submodules
 
diff --git a/py/modarray.c b/py/modarray.c
index d9f7a045288..116c844e8eb 100644
--- a/py/modarray.c
+++ b/py/modarray.c
@@ -28,18 +28,18 @@
 
 #if MICROPY_PY_ARRAY
 
-STATIC const mp_rom_map_elem_t mp_module_array_globals_table[] = {
-    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_uarray) },
+static const mp_rom_map_elem_t mp_module_array_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_array) },
     { MP_ROM_QSTR(MP_QSTR_array), MP_ROM_PTR(&mp_type_array) },
 };
 
-STATIC MP_DEFINE_CONST_DICT(mp_module_array_globals, mp_module_array_globals_table);
+static MP_DEFINE_CONST_DICT(mp_module_array_globals, mp_module_array_globals_table);
 
-const mp_obj_module_t mp_module_uarray = {
+const mp_obj_module_t mp_module_array = {
     .base = { &mp_type_module },
     .globals = (mp_obj_dict_t *)&mp_module_array_globals,
 };
 
-MP_REGISTER_MODULE(MP_QSTR_uarray, mp_module_uarray);
+MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_array, mp_module_array);
 
 #endif
diff --git a/py/modbuiltins.c b/py/modbuiltins.c
index da29e5b67da..caa8c28dcb9 100644
--- a/py/modbuiltins.c
+++ b/py/modbuiltins.c
@@ -46,7 +46,7 @@ extern struct _mp_dummy_t mp_sys_stdout_obj; // type is irrelevant, just need po
 // args[0] is function from class body
 // args[1] is class name
 // args[2:] are base objects
-STATIC mp_obj_t mp_builtin___build_class__(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mp_builtin___build_class__(size_t n_args, const mp_obj_t *args) {
     assert(2 <= n_args);
 
     // set the new classes __locals__ object
@@ -88,12 +88,12 @@ STATIC mp_obj_t mp_builtin___build_class__(size_t n_args, const mp_obj_t *args)
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR(mp_builtin___build_class___obj, 2, mp_builtin___build_class__);
 
-STATIC mp_obj_t mp_builtin_abs(mp_obj_t o_in) {
+static mp_obj_t mp_builtin_abs(mp_obj_t o_in) {
     return mp_unary_op(MP_UNARY_OP_ABS, o_in);
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_abs_obj, mp_builtin_abs);
 
-STATIC mp_obj_t mp_builtin_all(mp_obj_t o_in) {
+static mp_obj_t mp_builtin_all(mp_obj_t o_in) {
     mp_obj_iter_buf_t iter_buf;
     mp_obj_t iterable = mp_getiter(o_in, &iter_buf);
     mp_obj_t item;
@@ -106,7 +106,7 @@ STATIC mp_obj_t mp_builtin_all(mp_obj_t o_in) {
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_all_obj, mp_builtin_all);
 
-STATIC mp_obj_t mp_builtin_any(mp_obj_t o_in) {
+static mp_obj_t mp_builtin_any(mp_obj_t o_in) {
     mp_obj_iter_buf_t iter_buf;
     mp_obj_t iterable = mp_getiter(o_in, &iter_buf);
     mp_obj_t item;
@@ -119,13 +119,13 @@ STATIC mp_obj_t mp_builtin_any(mp_obj_t o_in) {
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_any_obj, mp_builtin_any);
 
-STATIC mp_obj_t mp_builtin_bin(mp_obj_t o_in) {
+static mp_obj_t mp_builtin_bin(mp_obj_t o_in) {
     mp_obj_t args[] = { MP_OBJ_NEW_QSTR(MP_QSTR__brace_open__colon__hash_b_brace_close_), o_in };
     return mp_obj_str_format(MP_ARRAY_SIZE(args), args, NULL);
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_bin_obj, mp_builtin_bin);
 
-STATIC mp_obj_t mp_builtin_callable(mp_obj_t o_in) {
+static mp_obj_t mp_builtin_callable(mp_obj_t o_in) {
     if (mp_obj_is_callable(o_in)) {
         return mp_const_true;
     } else {
@@ -134,33 +134,15 @@ STATIC mp_obj_t mp_builtin_callable(mp_obj_t o_in) {
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_callable_obj, mp_builtin_callable);
 
-STATIC mp_obj_t mp_builtin_chr(mp_obj_t o_in) {
+static mp_obj_t mp_builtin_chr(mp_obj_t o_in) {
     #if MICROPY_PY_BUILTINS_STR_UNICODE
     mp_uint_t c = mp_obj_get_int(o_in);
-    uint8_t str[4];
-    int len = 0;
-    if (c < 0x80) {
-        *str = c;
-        len = 1;
-    } else if (c < 0x800) {
-        str[0] = (c >> 6) | 0xC0;
-        str[1] = (c & 0x3F) | 0x80;
-        len = 2;
-    } else if (c < 0x10000) {
-        str[0] = (c >> 12) | 0xE0;
-        str[1] = ((c >> 6) & 0x3F) | 0x80;
-        str[2] = (c & 0x3F) | 0x80;
-        len = 3;
-    } else if (c < 0x110000) {
-        str[0] = (c >> 18) | 0xF0;
-        str[1] = ((c >> 12) & 0x3F) | 0x80;
-        str[2] = ((c >> 6) & 0x3F) | 0x80;
-        str[3] = (c & 0x3F) | 0x80;
-        len = 4;
-    } else {
-        mp_raise_ValueError(MP_ERROR_TEXT("chr() arg not in range(0x110000)"));
+    if (c >= 0x110000) {
+        mp_raise_ValueError(MP_ERROR_TEXT("char not in range(0x110000)"));
     }
-    return mp_obj_new_str_via_qstr((char *)str, len);
+    VSTR_FIXED(buf, 4);
+    vstr_add_char(&buf, c);
+    return mp_obj_new_str_via_qstr(buf.buf, buf.len);
     #else
     mp_int_t ord = mp_obj_get_int(o_in);
     if (0 <= ord && ord <= 0xff) {
@@ -173,7 +155,8 @@ STATIC mp_obj_t mp_builtin_chr(mp_obj_t o_in) {
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_chr_obj, mp_builtin_chr);
 
-STATIC mp_obj_t mp_builtin_dir(size_t n_args, const mp_obj_t *args) {
+#if MICROPY_PY_BUILTINS_DIR
+static mp_obj_t mp_builtin_dir(size_t n_args, const mp_obj_t *args) {
     mp_obj_t dir = mp_obj_new_list(0, NULL);
     if (n_args == 0) {
         // Make a list of names in the local namespace
@@ -205,19 +188,20 @@ STATIC mp_obj_t mp_builtin_dir(size_t n_args, const mp_obj_t *args) {
     return dir;
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_builtin_dir_obj, 0, 1, mp_builtin_dir);
+#endif
 
-STATIC mp_obj_t mp_builtin_divmod(mp_obj_t o1_in, mp_obj_t o2_in) {
+static mp_obj_t mp_builtin_divmod(mp_obj_t o1_in, mp_obj_t o2_in) {
     return mp_binary_op(MP_BINARY_OP_DIVMOD, o1_in, o2_in);
 }
 MP_DEFINE_CONST_FUN_OBJ_2(mp_builtin_divmod_obj, mp_builtin_divmod);
 
-STATIC mp_obj_t mp_builtin_hash(mp_obj_t o_in) {
+static mp_obj_t mp_builtin_hash(mp_obj_t o_in) {
     // result is guaranteed to be a (small) int
     return mp_unary_op(MP_UNARY_OP_HASH, o_in);
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_hash_obj, mp_builtin_hash);
 
-STATIC mp_obj_t mp_builtin_hex(mp_obj_t o_in) {
+static mp_obj_t mp_builtin_hex(mp_obj_t o_in) {
     #if MICROPY_PY_BUILTINS_STR_OP_MODULO
     return mp_binary_op(MP_BINARY_OP_MODULO, MP_OBJ_NEW_QSTR(MP_QSTR__percent__hash_x), o_in);
     #else
@@ -237,7 +221,7 @@ MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_hex_obj, mp_builtin_hex);
 #define mp_hal_readline readline
 #endif
 
-STATIC mp_obj_t mp_builtin_input(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mp_builtin_input(size_t n_args, const mp_obj_t *args) {
     if (n_args == 1) {
         mp_obj_print(args[0], PRINT_STR);
     }
@@ -256,14 +240,14 @@ MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_builtin_input_obj, 0, 1, mp_builtin_input
 
 #endif
 
-STATIC mp_obj_t mp_builtin_iter(mp_obj_t o_in) {
+static mp_obj_t mp_builtin_iter(mp_obj_t o_in) {
     return mp_getiter(o_in, NULL);
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_iter_obj, mp_builtin_iter);
 
 #if MICROPY_PY_BUILTINS_MIN_MAX
 
-STATIC mp_obj_t mp_builtin_min_max(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs, mp_uint_t op) {
+static mp_obj_t mp_builtin_min_max(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs, mp_uint_t op) {
     mp_map_elem_t *key_elem = mp_map_lookup(kwargs, MP_OBJ_NEW_QSTR(MP_QSTR_key), MP_MAP_LOOKUP);
     mp_map_elem_t *default_elem;
     mp_obj_t key_fn = key_elem == NULL ? MP_OBJ_NULL : key_elem->value;
@@ -305,12 +289,12 @@ STATIC mp_obj_t mp_builtin_min_max(size_t n_args, const mp_obj_t *args, mp_map_t
     }
 }
 
-STATIC mp_obj_t mp_builtin_max(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) {
+static mp_obj_t mp_builtin_max(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) {
     return mp_builtin_min_max(n_args, args, kwargs, MP_BINARY_OP_MORE);
 }
 MP_DEFINE_CONST_FUN_OBJ_KW(mp_builtin_max_obj, 1, mp_builtin_max);
 
-STATIC mp_obj_t mp_builtin_min(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) {
+static mp_obj_t mp_builtin_min(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) {
     return mp_builtin_min_max(n_args, args, kwargs, MP_BINARY_OP_LESS);
 }
 MP_DEFINE_CONST_FUN_OBJ_KW(mp_builtin_min_obj, 1, mp_builtin_min);
@@ -318,7 +302,7 @@ MP_DEFINE_CONST_FUN_OBJ_KW(mp_builtin_min_obj, 1, mp_builtin_min);
 #endif
 
 #if MICROPY_PY_BUILTINS_NEXT2
-STATIC mp_obj_t mp_builtin_next(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mp_builtin_next(size_t n_args, const mp_obj_t *args) {
     if (n_args == 1) {
         mp_obj_t ret = mp_iternext_allow_raise(args[0]);
         if (ret == MP_OBJ_STOP_ITERATION) {
@@ -333,7 +317,7 @@ STATIC mp_obj_t mp_builtin_next(size_t n_args, const mp_obj_t *args) {
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_builtin_next_obj, 1, 2, mp_builtin_next);
 #else
-STATIC mp_obj_t mp_builtin_next(mp_obj_t o) {
+static mp_obj_t mp_builtin_next(mp_obj_t o) {
     mp_obj_t ret = mp_iternext_allow_raise(o);
     if (ret == MP_OBJ_STOP_ITERATION) {
         mp_raise_StopIteration(MP_STATE_THREAD(stop_iteration_arg));
@@ -344,7 +328,7 @@ STATIC mp_obj_t mp_builtin_next(mp_obj_t o) {
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_next_obj, mp_builtin_next);
 #endif
 
-STATIC mp_obj_t mp_builtin_oct(mp_obj_t o_in) {
+static mp_obj_t mp_builtin_oct(mp_obj_t o_in) {
     #if MICROPY_PY_BUILTINS_STR_OP_MODULO
     return mp_binary_op(MP_BINARY_OP_MODULO, MP_OBJ_NEW_QSTR(MP_QSTR__percent__hash_o), o_in);
     #else
@@ -354,7 +338,7 @@ STATIC mp_obj_t mp_builtin_oct(mp_obj_t o_in) {
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_oct_obj, mp_builtin_oct);
 
-STATIC mp_obj_t mp_builtin_ord(mp_obj_t o_in) {
+static mp_obj_t mp_builtin_ord(mp_obj_t o_in) {
     size_t len;
     const byte *str = (const byte *)mp_obj_str_get_data(o_in, &len);
     #if MICROPY_PY_BUILTINS_STR_UNICODE
@@ -381,23 +365,26 @@ STATIC mp_obj_t mp_builtin_ord(mp_obj_t o_in) {
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_ord_obj, mp_builtin_ord);
 
-STATIC mp_obj_t mp_builtin_pow(size_t n_args, const mp_obj_t *args) {
-    switch (n_args) {
-        case 2:
-            return mp_binary_op(MP_BINARY_OP_POWER, args[0], args[1]);
-        default:
-            #if !MICROPY_PY_BUILTINS_POW3
-            mp_raise_NotImplementedError(MP_ERROR_TEXT("3-arg pow() not supported"));
-            #elif MICROPY_LONGINT_IMPL != MICROPY_LONGINT_IMPL_MPZ
-            return mp_binary_op(MP_BINARY_OP_MODULO, mp_binary_op(MP_BINARY_OP_POWER, args[0], args[1]), args[2]);
-            #else
-            return mp_obj_int_pow3(args[0], args[1], args[2]);
-            #endif
+static mp_obj_t mp_builtin_pow(size_t n_args, const mp_obj_t *args) {
+    // Treat pow(x, y, None) as pow(x, y), matching CPython.
+    if (n_args == 2
+        #if MICROPY_PY_BUILTINS_POW3
+        || args[2] == mp_const_none
+        #endif
+        ) {
+        return mp_binary_op(MP_BINARY_OP_POWER, args[0], args[1]);
     }
+    #if !MICROPY_PY_BUILTINS_POW3
+    mp_raise_NotImplementedError(MP_ERROR_TEXT("3-arg pow() not supported"));
+    #elif MICROPY_LONGINT_IMPL != MICROPY_LONGINT_IMPL_MPZ
+    return mp_binary_op(MP_BINARY_OP_MODULO, mp_binary_op(MP_BINARY_OP_POWER, args[0], args[1]), args[2]);
+    #else
+    return mp_obj_int_pow3(args[0], args[1], args[2]);
+    #endif
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_builtin_pow_obj, 2, 3, mp_builtin_pow);
 
-STATIC mp_obj_t mp_builtin_print(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
+static mp_obj_t mp_builtin_print(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
     enum { ARG_sep, ARG_end, ARG_file };
     static const mp_arg_t allowed_args[] = {
         { MP_QSTR_sep, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_QSTR(MP_QSTR__space_)} },
@@ -448,7 +435,7 @@ STATIC mp_obj_t mp_builtin_print(size_t n_args, const mp_obj_t *pos_args, mp_map
 }
 MP_DEFINE_CONST_FUN_OBJ_KW(mp_builtin_print_obj, 0, mp_builtin_print);
 
-STATIC mp_obj_t mp_builtin___repl_print__(mp_obj_t o) {
+static mp_obj_t mp_builtin___repl_print__(mp_obj_t o) {
     if (o != mp_const_none) {
         mp_obj_print_helper(MP_PYTHON_PRINTER, o, PRINT_REPR);
         mp_print_str(MP_PYTHON_PRINTER, "\n");
@@ -462,7 +449,7 @@ STATIC mp_obj_t mp_builtin___repl_print__(mp_obj_t o) {
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin___repl_print___obj, mp_builtin___repl_print__);
 
-STATIC mp_obj_t mp_builtin_repr(mp_obj_t o_in) {
+static mp_obj_t mp_builtin_repr(mp_obj_t o_in) {
     vstr_t vstr;
     mp_print_t print;
     vstr_init_print(&vstr, 16, &print);
@@ -471,7 +458,7 @@ STATIC mp_obj_t mp_builtin_repr(mp_obj_t o_in) {
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_repr_obj, mp_builtin_repr);
 
-STATIC mp_obj_t mp_builtin_round(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mp_builtin_round(size_t n_args, const mp_obj_t *args) {
     mp_obj_t o_in = args[0];
     if (mp_obj_is_int(o_in)) {
         if (n_args <= 1) {
@@ -523,7 +510,7 @@ STATIC mp_obj_t mp_builtin_round(size_t n_args, const mp_obj_t *args) {
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_builtin_round_obj, 1, 2, mp_builtin_round);
 
-STATIC mp_obj_t mp_builtin_sum(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mp_builtin_sum(size_t n_args, const mp_obj_t *args) {
     mp_obj_t value;
     switch (n_args) {
         case 1:
@@ -543,7 +530,7 @@ STATIC mp_obj_t mp_builtin_sum(size_t n_args, const mp_obj_t *args) {
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_builtin_sum_obj, 1, 2, mp_builtin_sum);
 
-STATIC mp_obj_t mp_builtin_sorted(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) {
+static mp_obj_t mp_builtin_sorted(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) {
     if (n_args > 1) {
         mp_raise_TypeError(MP_ERROR_TEXT("must use keyword argument for key function"));
     }
@@ -574,7 +561,7 @@ static inline mp_obj_t mp_load_attr_default(mp_obj_t base, qstr attr, mp_obj_t d
     }
 }
 
-STATIC mp_obj_t mp_builtin_getattr(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mp_builtin_getattr(size_t n_args, const mp_obj_t *args) {
     mp_obj_t defval = MP_OBJ_NULL;
     if (n_args > 2) {
         defval = args[2];
@@ -583,20 +570,20 @@ STATIC mp_obj_t mp_builtin_getattr(size_t n_args, const mp_obj_t *args) {
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_builtin_getattr_obj, 2, 3, mp_builtin_getattr);
 
-STATIC mp_obj_t mp_builtin_setattr(mp_obj_t base, mp_obj_t attr, mp_obj_t value) {
+static mp_obj_t mp_builtin_setattr(mp_obj_t base, mp_obj_t attr, mp_obj_t value) {
     mp_store_attr(base, mp_obj_str_get_qstr(attr), value);
     return mp_const_none;
 }
 MP_DEFINE_CONST_FUN_OBJ_3(mp_builtin_setattr_obj, mp_builtin_setattr);
 
 #if MICROPY_CPYTHON_COMPAT
-STATIC mp_obj_t mp_builtin_delattr(mp_obj_t base, mp_obj_t attr) {
+static mp_obj_t mp_builtin_delattr(mp_obj_t base, mp_obj_t attr) {
     return mp_builtin_setattr(base, attr, MP_OBJ_NULL);
 }
 MP_DEFINE_CONST_FUN_OBJ_2(mp_builtin_delattr_obj, mp_builtin_delattr);
 #endif
 
-STATIC mp_obj_t mp_builtin_hasattr(mp_obj_t object_in, mp_obj_t attr_in) {
+static mp_obj_t mp_builtin_hasattr(mp_obj_t object_in, mp_obj_t attr_in) {
     qstr attr = mp_obj_str_get_qstr(attr_in);
     mp_obj_t dest[2];
     mp_load_method_protected(object_in, attr, dest, false);
@@ -604,12 +591,12 @@ STATIC mp_obj_t mp_builtin_hasattr(mp_obj_t object_in, mp_obj_t attr_in) {
 }
 MP_DEFINE_CONST_FUN_OBJ_2(mp_builtin_hasattr_obj, mp_builtin_hasattr);
 
-STATIC mp_obj_t mp_builtin_globals(void) {
+static mp_obj_t mp_builtin_globals(void) {
     return MP_OBJ_FROM_PTR(mp_globals_get());
 }
 MP_DEFINE_CONST_FUN_OBJ_0(mp_builtin_globals_obj, mp_builtin_globals);
 
-STATIC mp_obj_t mp_builtin_locals(void) {
+static mp_obj_t mp_builtin_locals(void) {
     return MP_OBJ_FROM_PTR(mp_locals_get());
 }
 MP_DEFINE_CONST_FUN_OBJ_0(mp_builtin_locals_obj, mp_builtin_locals);
@@ -617,14 +604,20 @@ MP_DEFINE_CONST_FUN_OBJ_0(mp_builtin_locals_obj, mp_builtin_locals);
 // These are defined in terms of MicroPython API functions right away
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_id_obj, mp_obj_id);
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_len_obj, mp_obj_len);
+#if MICROPY_PY_TSTRINGS
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_builtin___template___obj, 1, MP_OBJ_FUN_ARGS_MAX, mp_obj_new_template);
+#endif
 
-STATIC const mp_rom_map_elem_t mp_module_builtins_globals_table[] = {
+static const mp_rom_map_elem_t mp_module_builtins_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_builtins) },
 
     // built-in core functions
     { MP_ROM_QSTR(MP_QSTR___build_class__), MP_ROM_PTR(&mp_builtin___build_class___obj) },
     { MP_ROM_QSTR(MP_QSTR___import__), MP_ROM_PTR(&mp_builtin___import___obj) },
     { MP_ROM_QSTR(MP_QSTR___repl_print__), MP_ROM_PTR(&mp_builtin___repl_print___obj) },
+    #if MICROPY_PY_TSTRINGS
+    { MP_ROM_QSTR(MP_QSTR___template__), MP_ROM_PTR(&mp_builtin___template___obj) },
+    #endif
 
     // built-in types
     { MP_ROM_QSTR(MP_QSTR_bool), MP_ROM_PTR(&mp_type_bool) },
@@ -696,7 +689,9 @@ STATIC const mp_rom_map_elem_t mp_module_builtins_globals_table[] = {
     #if MICROPY_CPYTHON_COMPAT
     { MP_ROM_QSTR(MP_QSTR_delattr), MP_ROM_PTR(&mp_builtin_delattr_obj) },
     #endif
+    #if MICROPY_PY_BUILTINS_DIR
     { MP_ROM_QSTR(MP_QSTR_dir), MP_ROM_PTR(&mp_builtin_dir_obj) },
+    #endif
     { MP_ROM_QSTR(MP_QSTR_divmod), MP_ROM_PTR(&mp_builtin_divmod_obj) },
     #if MICROPY_PY_BUILTINS_EVAL_EXEC
     { MP_ROM_QSTR(MP_QSTR_eval), MP_ROM_PTR(&mp_builtin_eval_obj) },
diff --git a/py/modcmath.c b/py/modcmath.c
index 1418362ad9b..33cb00cbe7e 100644
--- a/py/modcmath.c
+++ b/py/modcmath.c
@@ -31,15 +31,15 @@
 #include 
 
 // phase(z): returns the phase of the number z in the range (-pi, +pi]
-STATIC mp_obj_t mp_cmath_phase(mp_obj_t z_obj) {
+static mp_obj_t mp_cmath_phase(mp_obj_t z_obj) {
     mp_float_t real, imag;
     mp_obj_get_complex(z_obj, &real, &imag);
     return mp_obj_new_float(MICROPY_FLOAT_C_FUN(atan2)(imag, real));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_phase_obj, mp_cmath_phase);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_phase_obj, mp_cmath_phase);
 
 // polar(z): returns the polar form of z as a tuple
-STATIC mp_obj_t mp_cmath_polar(mp_obj_t z_obj) {
+static mp_obj_t mp_cmath_polar(mp_obj_t z_obj) {
     mp_float_t real, imag;
     mp_obj_get_complex(z_obj, &real, &imag);
     mp_obj_t tuple[2] = {
@@ -48,71 +48,71 @@ STATIC mp_obj_t mp_cmath_polar(mp_obj_t z_obj) {
     };
     return mp_obj_new_tuple(2, tuple);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_polar_obj, mp_cmath_polar);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_polar_obj, mp_cmath_polar);
 
 // rect(r, phi): returns the complex number with modulus r and phase phi
-STATIC mp_obj_t mp_cmath_rect(mp_obj_t r_obj, mp_obj_t phi_obj) {
+static mp_obj_t mp_cmath_rect(mp_obj_t r_obj, mp_obj_t phi_obj) {
     mp_float_t r = mp_obj_get_float(r_obj);
     mp_float_t phi = mp_obj_get_float(phi_obj);
     return mp_obj_new_complex(r * MICROPY_FLOAT_C_FUN(cos)(phi), r * MICROPY_FLOAT_C_FUN(sin)(phi));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_cmath_rect_obj, mp_cmath_rect);
+static MP_DEFINE_CONST_FUN_OBJ_2(mp_cmath_rect_obj, mp_cmath_rect);
 
 // exp(z): return the exponential of z
-STATIC mp_obj_t mp_cmath_exp(mp_obj_t z_obj) {
+static mp_obj_t mp_cmath_exp(mp_obj_t z_obj) {
     mp_float_t real, imag;
     mp_obj_get_complex(z_obj, &real, &imag);
     mp_float_t exp_real = MICROPY_FLOAT_C_FUN(exp)(real);
     return mp_obj_new_complex(exp_real * MICROPY_FLOAT_C_FUN(cos)(imag), exp_real * MICROPY_FLOAT_C_FUN(sin)(imag));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_exp_obj, mp_cmath_exp);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_exp_obj, mp_cmath_exp);
 
 // log(z): return the natural logarithm of z, with branch cut along the negative real axis
 // TODO can take second argument, being the base
-STATIC mp_obj_t mp_cmath_log(mp_obj_t z_obj) {
+static mp_obj_t mp_cmath_log(mp_obj_t z_obj) {
     mp_float_t real, imag;
     mp_obj_get_complex(z_obj, &real, &imag);
     return mp_obj_new_complex(MICROPY_FLOAT_CONST(0.5) * MICROPY_FLOAT_C_FUN(log)(real * real + imag * imag), MICROPY_FLOAT_C_FUN(atan2)(imag, real));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_log_obj, mp_cmath_log);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_log_obj, mp_cmath_log);
 
 #if MICROPY_PY_MATH_SPECIAL_FUNCTIONS
 // log10(z): return the base-10 logarithm of z, with branch cut along the negative real axis
-STATIC mp_obj_t mp_cmath_log10(mp_obj_t z_obj) {
+static mp_obj_t mp_cmath_log10(mp_obj_t z_obj) {
     mp_float_t real, imag;
     mp_obj_get_complex(z_obj, &real, &imag);
     return mp_obj_new_complex(MICROPY_FLOAT_CONST(0.5) * MICROPY_FLOAT_C_FUN(log10)(real * real + imag * imag), MICROPY_FLOAT_CONST(0.4342944819032518) * MICROPY_FLOAT_C_FUN(atan2)(imag, real));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_log10_obj, mp_cmath_log10);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_log10_obj, mp_cmath_log10);
 #endif
 
 // sqrt(z): return the square-root of z
-STATIC mp_obj_t mp_cmath_sqrt(mp_obj_t z_obj) {
+static mp_obj_t mp_cmath_sqrt(mp_obj_t z_obj) {
     mp_float_t real, imag;
     mp_obj_get_complex(z_obj, &real, &imag);
     mp_float_t sqrt_abs = MICROPY_FLOAT_C_FUN(pow)(real * real + imag * imag, MICROPY_FLOAT_CONST(0.25));
     mp_float_t theta = MICROPY_FLOAT_CONST(0.5) * MICROPY_FLOAT_C_FUN(atan2)(imag, real);
     return mp_obj_new_complex(sqrt_abs * MICROPY_FLOAT_C_FUN(cos)(theta), sqrt_abs * MICROPY_FLOAT_C_FUN(sin)(theta));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_sqrt_obj, mp_cmath_sqrt);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_sqrt_obj, mp_cmath_sqrt);
 
 // cos(z): return the cosine of z
-STATIC mp_obj_t mp_cmath_cos(mp_obj_t z_obj) {
+static mp_obj_t mp_cmath_cos(mp_obj_t z_obj) {
     mp_float_t real, imag;
     mp_obj_get_complex(z_obj, &real, &imag);
     return mp_obj_new_complex(MICROPY_FLOAT_C_FUN(cos)(real) * MICROPY_FLOAT_C_FUN(cosh)(imag), -MICROPY_FLOAT_C_FUN(sin)(real) * MICROPY_FLOAT_C_FUN(sinh)(imag));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_cos_obj, mp_cmath_cos);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_cos_obj, mp_cmath_cos);
 
 // sin(z): return the sine of z
-STATIC mp_obj_t mp_cmath_sin(mp_obj_t z_obj) {
+static mp_obj_t mp_cmath_sin(mp_obj_t z_obj) {
     mp_float_t real, imag;
     mp_obj_get_complex(z_obj, &real, &imag);
     return mp_obj_new_complex(MICROPY_FLOAT_C_FUN(sin)(real) * MICROPY_FLOAT_C_FUN(cosh)(imag), MICROPY_FLOAT_C_FUN(cos)(real) * MICROPY_FLOAT_C_FUN(sinh)(imag));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_sin_obj, mp_cmath_sin);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_cmath_sin_obj, mp_cmath_sin);
 
-STATIC const mp_rom_map_elem_t mp_module_cmath_globals_table[] = {
+static const mp_rom_map_elem_t mp_module_cmath_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_cmath) },
     { MP_ROM_QSTR(MP_QSTR_e), mp_const_float_e },
     { MP_ROM_QSTR(MP_QSTR_pi), mp_const_float_pi },
@@ -142,7 +142,7 @@ STATIC const mp_rom_map_elem_t mp_module_cmath_globals_table[] = {
     // { MP_ROM_QSTR(MP_QSTR_isnan), MP_ROM_PTR(&mp_cmath_isnan_obj) },
 };
 
-STATIC MP_DEFINE_CONST_DICT(mp_module_cmath_globals, mp_module_cmath_globals_table);
+static MP_DEFINE_CONST_DICT(mp_module_cmath_globals, mp_module_cmath_globals_table);
 
 const mp_obj_module_t mp_module_cmath = {
     .base = { &mp_type_module },
diff --git a/py/modcollections.c b/py/modcollections.c
index 8c62f34db7e..46326d13eef 100644
--- a/py/modcollections.c
+++ b/py/modcollections.c
@@ -28,8 +28,8 @@
 
 #if MICROPY_PY_COLLECTIONS
 
-STATIC const mp_rom_map_elem_t mp_module_collections_globals_table[] = {
-    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_ucollections) },
+static const mp_rom_map_elem_t mp_module_collections_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_collections) },
     #if MICROPY_PY_COLLECTIONS_DEQUE
     { MP_ROM_QSTR(MP_QSTR_deque), MP_ROM_PTR(&mp_type_deque) },
     #endif
@@ -39,13 +39,13 @@ STATIC const mp_rom_map_elem_t mp_module_collections_globals_table[] = {
     #endif
 };
 
-STATIC MP_DEFINE_CONST_DICT(mp_module_collections_globals, mp_module_collections_globals_table);
+static MP_DEFINE_CONST_DICT(mp_module_collections_globals, mp_module_collections_globals_table);
 
 const mp_obj_module_t mp_module_collections = {
     .base = { &mp_type_module },
     .globals = (mp_obj_dict_t *)&mp_module_collections_globals,
 };
 
-MP_REGISTER_MODULE(MP_QSTR_ucollections, mp_module_collections);
+MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_collections, mp_module_collections);
 
 #endif // MICROPY_PY_COLLECTIONS
diff --git a/py/moderrno.c b/py/moderrno.c
new file mode 100644
index 00000000000..58a141c1020
--- /dev/null
+++ b/py/moderrno.c
@@ -0,0 +1,124 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2016 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include 
+#include 
+
+#include "py/obj.h"
+#include "py/mperrno.h"
+
+#if MICROPY_PY_ERRNO
+
+// This list can be defined per port in mpconfigport.h to tailor it to a
+// specific port's needs.  If it's not defined then we provide a default.
+#ifndef MICROPY_PY_ERRNO_LIST
+#define MICROPY_PY_ERRNO_LIST \
+    X(EPERM) \
+    X(ENOENT) \
+    X(EIO) \
+    X(EBADF) \
+    X(EAGAIN) \
+    X(ENOMEM) \
+    X(EACCES) \
+    X(EEXIST) \
+    X(ENODEV) \
+    X(EISDIR) \
+    X(EINVAL) \
+    X(EOPNOTSUPP) \
+    X(EADDRINUSE) \
+    X(ECONNABORTED) \
+    X(ECONNRESET) \
+    X(ENOBUFS) \
+    X(ENOTCONN) \
+    X(ETIMEDOUT) \
+    X(ECONNREFUSED) \
+    X(EHOSTUNREACH) \
+    X(EALREADY) \
+    X(EINPROGRESS) \
+
+#endif
+
+#if MICROPY_PY_ERRNO_ERRORCODE
+static const mp_rom_map_elem_t errorcode_table[] = {
+    #define X(e) { MP_ROM_INT(MP_##e), MP_ROM_QSTR(MP_QSTR_##e) },
+    MICROPY_PY_ERRNO_LIST
+#undef X
+};
+
+static const mp_obj_dict_t errorcode_dict = {
+    .base = {&mp_type_dict},
+    .map = {
+        .all_keys_are_qstrs = 0, // keys are integers
+        .is_fixed = 1,
+        .is_ordered = 1,
+        .used = MP_ARRAY_SIZE(errorcode_table),
+        .alloc = MP_ARRAY_SIZE(errorcode_table),
+        .table = (mp_map_elem_t *)(mp_rom_map_elem_t *)errorcode_table,
+    },
+};
+#endif
+
+static const mp_rom_map_elem_t mp_module_errno_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_errno) },
+    #if MICROPY_PY_ERRNO_ERRORCODE
+    { MP_ROM_QSTR(MP_QSTR_errorcode), MP_ROM_PTR(&errorcode_dict) },
+    #endif
+
+    #define X(e) { MP_ROM_QSTR(MP_QSTR_##e), MP_ROM_INT(MP_##e) },
+    MICROPY_PY_ERRNO_LIST
+#undef X
+};
+
+static MP_DEFINE_CONST_DICT(mp_module_errno_globals, mp_module_errno_globals_table);
+
+const mp_obj_module_t mp_module_errno = {
+    .base = { &mp_type_module },
+    .globals = (mp_obj_dict_t *)&mp_module_errno_globals,
+};
+
+MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_errno, mp_module_errno);
+
+qstr mp_errno_to_str(mp_obj_t errno_val) {
+    #if MICROPY_PY_ERRNO_ERRORCODE
+    // We have the errorcode dict so can do a lookup using the hash map
+    mp_map_elem_t *elem = mp_map_lookup((mp_map_t *)&errorcode_dict.map, errno_val, MP_MAP_LOOKUP);
+    if (elem == NULL) {
+        return MP_QSTRnull;
+    } else {
+        return MP_OBJ_QSTR_VALUE(elem->value);
+    }
+    #else
+    // We don't have the errorcode dict so do a simple search in the modules dict
+    for (size_t i = 0; i < MP_ARRAY_SIZE(mp_module_errno_globals_table); ++i) {
+        if (errno_val == mp_module_errno_globals_table[i].value) {
+            return MP_OBJ_QSTR_VALUE(mp_module_errno_globals_table[i].key);
+        }
+    }
+    return MP_QSTRnull;
+    #endif
+}
+
+#endif // MICROPY_PY_ERRNO
diff --git a/py/modgc.c b/py/modgc.c
index c11bcaecd7d..47902d8c95b 100644
--- a/py/modgc.c
+++ b/py/modgc.c
@@ -31,7 +31,7 @@
 #if MICROPY_PY_GC && MICROPY_ENABLE_GC
 
 // collect(): run a garbage collection
-STATIC mp_obj_t py_gc_collect(void) {
+static mp_obj_t py_gc_collect(void) {
     gc_collect();
     #if MICROPY_PY_GC_COLLECT_RETVAL
     return MP_OBJ_NEW_SMALL_INT(MP_STATE_MEM(gc_collected));
@@ -42,34 +42,39 @@ STATIC mp_obj_t py_gc_collect(void) {
 MP_DEFINE_CONST_FUN_OBJ_0(gc_collect_obj, py_gc_collect);
 
 // disable(): disable the garbage collector
-STATIC mp_obj_t gc_disable(void) {
+static mp_obj_t gc_disable(void) {
     MP_STATE_MEM(gc_auto_collect_enabled) = 0;
     return mp_const_none;
 }
 MP_DEFINE_CONST_FUN_OBJ_0(gc_disable_obj, gc_disable);
 
 // enable(): enable the garbage collector
-STATIC mp_obj_t gc_enable(void) {
+static mp_obj_t gc_enable(void) {
     MP_STATE_MEM(gc_auto_collect_enabled) = 1;
     return mp_const_none;
 }
 MP_DEFINE_CONST_FUN_OBJ_0(gc_enable_obj, gc_enable);
 
-STATIC mp_obj_t gc_isenabled(void) {
+static mp_obj_t gc_isenabled(void) {
     return mp_obj_new_bool(MP_STATE_MEM(gc_auto_collect_enabled));
 }
 MP_DEFINE_CONST_FUN_OBJ_0(gc_isenabled_obj, gc_isenabled);
 
 // mem_free(): return the number of bytes of available heap RAM
-STATIC mp_obj_t gc_mem_free(void) {
+static mp_obj_t gc_mem_free(void) {
     gc_info_t info;
     gc_info(&info);
+    #if MICROPY_GC_SPLIT_HEAP_AUTO
+    // Include max_new_split value here as a more useful heuristic
+    return MP_OBJ_NEW_SMALL_INT(info.free + info.max_new_split);
+    #else
     return MP_OBJ_NEW_SMALL_INT(info.free);
+    #endif
 }
 MP_DEFINE_CONST_FUN_OBJ_0(gc_mem_free_obj, gc_mem_free);
 
 // mem_alloc(): return the number of bytes of heap RAM that are allocated
-STATIC mp_obj_t gc_mem_alloc(void) {
+static mp_obj_t gc_mem_alloc(void) {
     gc_info_t info;
     gc_info(&info);
     return MP_OBJ_NEW_SMALL_INT(info.used);
@@ -77,7 +82,7 @@ STATIC mp_obj_t gc_mem_alloc(void) {
 MP_DEFINE_CONST_FUN_OBJ_0(gc_mem_alloc_obj, gc_mem_alloc);
 
 #if MICROPY_GC_ALLOC_THRESHOLD
-STATIC mp_obj_t gc_threshold(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t gc_threshold(size_t n_args, const mp_obj_t *args) {
     if (n_args == 0) {
         if (MP_STATE_MEM(gc_alloc_threshold) == (size_t)-1) {
             return MP_OBJ_NEW_SMALL_INT(-1);
@@ -95,7 +100,7 @@ STATIC mp_obj_t gc_threshold(size_t n_args, const mp_obj_t *args) {
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(gc_threshold_obj, 0, 1, gc_threshold);
 #endif
 
-STATIC const mp_rom_map_elem_t mp_module_gc_globals_table[] = {
+static const mp_rom_map_elem_t mp_module_gc_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_gc) },
     { MP_ROM_QSTR(MP_QSTR_collect), MP_ROM_PTR(&gc_collect_obj) },
     { MP_ROM_QSTR(MP_QSTR_disable), MP_ROM_PTR(&gc_disable_obj) },
@@ -108,7 +113,7 @@ STATIC const mp_rom_map_elem_t mp_module_gc_globals_table[] = {
     #endif
 };
 
-STATIC MP_DEFINE_CONST_DICT(mp_module_gc_globals, mp_module_gc_globals_table);
+static MP_DEFINE_CONST_DICT(mp_module_gc_globals, mp_module_gc_globals_table);
 
 const mp_obj_module_t mp_module_gc = {
     .base = { &mp_type_module },
diff --git a/py/modio.c b/py/modio.c
index 9ec6bbcc4ec..fdee484376f 100644
--- a/py/modio.c
+++ b/py/modio.c
@@ -39,11 +39,11 @@
 
 #if MICROPY_PY_IO_IOBASE
 
-STATIC const mp_obj_type_t mp_type_iobase;
+static const mp_obj_type_t mp_type_iobase;
 
-STATIC const mp_obj_base_t iobase_singleton = {&mp_type_iobase};
+static const mp_obj_base_t iobase_singleton = {&mp_type_iobase};
 
-STATIC mp_obj_t iobase_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t iobase_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)type;
     (void)n_args;
     (void)n_kw;
@@ -51,7 +51,7 @@ STATIC mp_obj_t iobase_make_new(const mp_obj_type_t *type, size_t n_args, size_t
     return MP_OBJ_FROM_PTR(&iobase_singleton);
 }
 
-STATIC mp_uint_t iobase_read_write(mp_obj_t obj, void *buf, mp_uint_t size, int *errcode, qstr qst) {
+static mp_uint_t iobase_read_write(mp_obj_t obj, void *buf, mp_uint_t size, int *errcode, qstr qst) {
     mp_obj_t dest[3];
     mp_load_method(obj, qst, dest);
     mp_obj_array_t ar = {{&mp_type_bytearray}, BYTEARRAY_TYPECODE, 0, size, buf};
@@ -63,21 +63,25 @@ STATIC mp_uint_t iobase_read_write(mp_obj_t obj, void *buf, mp_uint_t size, int
     }
     mp_int_t ret = mp_obj_get_int(ret_obj);
     if (ret >= 0) {
+        if ((mp_uint_t)ret > size) {
+            *errcode = MP_EIO;
+            return MP_STREAM_ERROR;
+        }
         return ret;
     } else {
         *errcode = -ret;
         return MP_STREAM_ERROR;
     }
 }
-STATIC mp_uint_t iobase_read(mp_obj_t obj, void *buf, mp_uint_t size, int *errcode) {
+static mp_uint_t iobase_read(mp_obj_t obj, void *buf, mp_uint_t size, int *errcode) {
     return iobase_read_write(obj, buf, size, errcode, MP_QSTR_readinto);
 }
 
-STATIC mp_uint_t iobase_write(mp_obj_t obj, const void *buf, mp_uint_t size, int *errcode) {
+static mp_uint_t iobase_write(mp_obj_t obj, const void *buf, mp_uint_t size, int *errcode) {
     return iobase_read_write(obj, (void *)buf, size, errcode, MP_QSTR_write);
 }
 
-STATIC mp_uint_t iobase_ioctl(mp_obj_t obj, mp_uint_t request, uintptr_t arg, int *errcode) {
+static mp_uint_t iobase_ioctl(mp_obj_t obj, mp_uint_t request, uintptr_t arg, int *errcode) {
     mp_obj_t dest[4];
     mp_load_method(obj, MP_QSTR_ioctl, dest);
     dest[2] = mp_obj_new_int_from_uint(request);
@@ -91,13 +95,13 @@ STATIC mp_uint_t iobase_ioctl(mp_obj_t obj, mp_uint_t request, uintptr_t arg, in
     }
 }
 
-STATIC const mp_stream_p_t iobase_p = {
+static const mp_stream_p_t iobase_p = {
     .read = iobase_read,
     .write = iobase_write,
     .ioctl = iobase_ioctl,
 };
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+static MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_iobase,
     MP_QSTR_IOBase,
     MP_TYPE_FLAG_NONE,
@@ -116,17 +120,17 @@ typedef struct _mp_obj_bufwriter_t {
     byte buf[0];
 } mp_obj_bufwriter_t;
 
-STATIC mp_obj_t bufwriter_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t bufwriter_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     mp_arg_check_num(n_args, n_kw, 2, 2, false);
     size_t alloc = mp_obj_get_int(args[1]);
-    mp_obj_bufwriter_t *o = mp_obj_malloc_var(mp_obj_bufwriter_t, byte, alloc, type);
+    mp_obj_bufwriter_t *o = mp_obj_malloc_var(mp_obj_bufwriter_t, buf, byte, alloc, type);
     o->stream = args[0];
     o->alloc = alloc;
     o->len = 0;
     return o;
 }
 
-STATIC mp_uint_t bufwriter_write(mp_obj_t self_in, const void *buf, mp_uint_t size, int *errcode) {
+static mp_uint_t bufwriter_write(mp_obj_t self_in, const void *buf, mp_uint_t size, int *errcode) {
     mp_obj_bufwriter_t *self = MP_OBJ_TO_PTR(self_in);
 
     mp_uint_t org_size = size;
@@ -162,37 +166,38 @@ STATIC mp_uint_t bufwriter_write(mp_obj_t self_in, const void *buf, mp_uint_t si
     return org_size;
 }
 
-STATIC mp_obj_t bufwriter_flush(mp_obj_t self_in) {
+static mp_obj_t bufwriter_flush(mp_obj_t self_in) {
     mp_obj_bufwriter_t *self = MP_OBJ_TO_PTR(self_in);
 
     if (self->len != 0) {
         int err;
         mp_uint_t out_sz = mp_stream_write_exactly(self->stream, self->buf, self->len, &err);
         (void)out_sz;
-        // TODO: try to recover from a case of non-blocking stream, e.g. move
-        // remaining chunk to the beginning of buffer.
-        assert(out_sz == self->len);
-        self->len = 0;
         if (err != 0) {
             mp_raise_OSError(err);
+        } else {
+            // TODO: try to recover from a case of non-blocking stream, e.g. move
+            // remaining chunk to the beginning of buffer.
+            assert(out_sz == self->len);
+            self->len = 0;
         }
     }
 
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(bufwriter_flush_obj, bufwriter_flush);
+static MP_DEFINE_CONST_FUN_OBJ_1(bufwriter_flush_obj, bufwriter_flush);
 
-STATIC const mp_rom_map_elem_t bufwriter_locals_dict_table[] = {
+static const mp_rom_map_elem_t bufwriter_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&mp_stream_write_obj) },
     { MP_ROM_QSTR(MP_QSTR_flush), MP_ROM_PTR(&bufwriter_flush_obj) },
 };
-STATIC MP_DEFINE_CONST_DICT(bufwriter_locals_dict, bufwriter_locals_dict_table);
+static MP_DEFINE_CONST_DICT(bufwriter_locals_dict, bufwriter_locals_dict_table);
 
-STATIC const mp_stream_p_t bufwriter_stream_p = {
+static const mp_stream_p_t bufwriter_stream_p = {
     .write = bufwriter_write,
 };
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+static MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_bufwriter,
     MP_QSTR_BufferedWriter,
     MP_TYPE_FLAG_NONE,
@@ -202,8 +207,8 @@ STATIC MP_DEFINE_CONST_OBJ_TYPE(
     );
 #endif // MICROPY_PY_IO_BUFFEREDWRITER
 
-STATIC const mp_rom_map_elem_t mp_module_io_globals_table[] = {
-    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_uio) },
+static const mp_rom_map_elem_t mp_module_io_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_io) },
     // Note: mp_builtin_open_obj should be defined by port, it's not
     // part of the core.
     { MP_ROM_QSTR(MP_QSTR_open), MP_ROM_PTR(&mp_builtin_open_obj) },
@@ -219,13 +224,13 @@ STATIC const mp_rom_map_elem_t mp_module_io_globals_table[] = {
     #endif
 };
 
-STATIC MP_DEFINE_CONST_DICT(mp_module_io_globals, mp_module_io_globals_table);
+static MP_DEFINE_CONST_DICT(mp_module_io_globals, mp_module_io_globals_table);
 
 const mp_obj_module_t mp_module_io = {
     .base = { &mp_type_module },
     .globals = (mp_obj_dict_t *)&mp_module_io_globals,
 };
 
-MP_REGISTER_MODULE(MP_QSTR_uio, mp_module_io);
+MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_io, mp_module_io);
 
 #endif
diff --git a/py/modmath.c b/py/modmath.c
index 861a23b48b3..045c842150e 100644
--- a/py/modmath.c
+++ b/py/modmath.c
@@ -37,11 +37,11 @@
 #define MP_PI_4 MICROPY_FLOAT_CONST(0.78539816339744830962)
 #define MP_3_PI_4 MICROPY_FLOAT_CONST(2.35619449019234492885)
 
-STATIC NORETURN void math_error(void) {
+static MP_NORETURN void math_error(void) {
     mp_raise_ValueError(MP_ERROR_TEXT("math domain error"));
 }
 
-STATIC mp_obj_t math_generic_1(mp_obj_t x_obj, mp_float_t (*f)(mp_float_t)) {
+static mp_obj_t math_generic_1(mp_obj_t x_obj, mp_float_t (*f)(mp_float_t)) {
     mp_float_t x = mp_obj_get_float(x_obj);
     mp_float_t ans = f(x);
     if ((isnan(ans) && !isnan(x)) || (isinf(ans) && !isinf(x))) {
@@ -50,7 +50,7 @@ STATIC mp_obj_t math_generic_1(mp_obj_t x_obj, mp_float_t (*f)(mp_float_t)) {
     return mp_obj_new_float(ans);
 }
 
-STATIC mp_obj_t math_generic_2(mp_obj_t x_obj, mp_obj_t y_obj, mp_float_t (*f)(mp_float_t, mp_float_t)) {
+static mp_obj_t math_generic_2(mp_obj_t x_obj, mp_obj_t y_obj, mp_float_t (*f)(mp_float_t, mp_float_t)) {
     mp_float_t x = mp_obj_get_float(x_obj);
     mp_float_t y = mp_obj_get_float(y_obj);
     mp_float_t ans = f(x, y);
@@ -61,30 +61,30 @@ STATIC mp_obj_t math_generic_2(mp_obj_t x_obj, mp_obj_t y_obj, mp_float_t (*f)(m
 }
 
 #define MATH_FUN_1(py_name, c_name) \
-    STATIC mp_obj_t mp_math_##py_name(mp_obj_t x_obj) { \
+    static mp_obj_t mp_math_##py_name(mp_obj_t x_obj) { \
         return math_generic_1(x_obj, MICROPY_FLOAT_C_FUN(c_name)); \
     } \
-    STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_math_##py_name##_obj, mp_math_##py_name);
+    static MP_DEFINE_CONST_FUN_OBJ_1(mp_math_##py_name##_obj, mp_math_##py_name);
 
 #define MATH_FUN_1_TO_BOOL(py_name, c_name) \
-    STATIC mp_obj_t mp_math_##py_name(mp_obj_t x_obj) { return mp_obj_new_bool(c_name(mp_obj_get_float(x_obj))); } \
-    STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_math_##py_name##_obj, mp_math_##py_name);
+    static mp_obj_t mp_math_##py_name(mp_obj_t x_obj) { return mp_obj_new_bool(c_name(mp_obj_get_float(x_obj))); } \
+    static MP_DEFINE_CONST_FUN_OBJ_1(mp_math_##py_name##_obj, mp_math_##py_name);
 
 #define MATH_FUN_1_TO_INT(py_name, c_name) \
-    STATIC mp_obj_t mp_math_##py_name(mp_obj_t x_obj) { return mp_obj_new_int_from_float(MICROPY_FLOAT_C_FUN(c_name)(mp_obj_get_float(x_obj))); } \
-    STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_math_##py_name##_obj, mp_math_##py_name);
+    static mp_obj_t mp_math_##py_name(mp_obj_t x_obj) { return mp_obj_new_int_from_float(MICROPY_FLOAT_C_FUN(c_name)(mp_obj_get_float(x_obj))); } \
+    static MP_DEFINE_CONST_FUN_OBJ_1(mp_math_##py_name##_obj, mp_math_##py_name);
 
 #define MATH_FUN_2(py_name, c_name) \
-    STATIC mp_obj_t mp_math_##py_name(mp_obj_t x_obj, mp_obj_t y_obj) { \
+    static mp_obj_t mp_math_##py_name(mp_obj_t x_obj, mp_obj_t y_obj) { \
         return math_generic_2(x_obj, y_obj, MICROPY_FLOAT_C_FUN(c_name)); \
     } \
-    STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_math_##py_name##_obj, mp_math_##py_name);
+    static MP_DEFINE_CONST_FUN_OBJ_2(mp_math_##py_name##_obj, mp_math_##py_name);
 
 #define MATH_FUN_2_FLT_INT(py_name, c_name) \
-    STATIC mp_obj_t mp_math_##py_name(mp_obj_t x_obj, mp_obj_t y_obj) { \
+    static mp_obj_t mp_math_##py_name(mp_obj_t x_obj, mp_obj_t y_obj) { \
         return mp_obj_new_float(MICROPY_FLOAT_C_FUN(c_name)(mp_obj_get_float(x_obj), mp_obj_get_int(y_obj))); \
     } \
-    STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_math_##py_name##_obj, mp_math_##py_name);
+    static MP_DEFINE_CONST_FUN_OBJ_2(mp_math_##py_name##_obj, mp_math_##py_name);
 
 #if MP_NEED_LOG2
 #undef log2
@@ -99,12 +99,16 @@ mp_float_t MICROPY_FLOAT_C_FUN(log2)(mp_float_t x) {
 MATH_FUN_1(sqrt, sqrt)
 // pow(x, y): returns x to the power of y
 #if MICROPY_PY_MATH_POW_FIX_NAN
-mp_float_t pow_func(mp_float_t x, mp_float_t y) {
+mp_float_t MICROPY_FLOAT_C_FUN(pow_func)(mp_float_t x, mp_float_t y) {
     // pow(base, 0) returns 1 for any base, even when base is NaN
     // pow(+1, exponent) returns 1 for any exponent, even when exponent is NaN
     if (x == MICROPY_FLOAT_CONST(1.0) || y == MICROPY_FLOAT_CONST(0.0)) {
         return MICROPY_FLOAT_CONST(1.0);
     }
+    // pow(base, NaN) returns NaN for any other value of base
+    if (isnan(y)) {
+        return y;
+    }
     return MICROPY_FLOAT_C_FUN(pow)(x, y);
 }
 MATH_FUN_2(pow, pow_func)
@@ -160,12 +164,17 @@ MATH_FUN_2(atan2, atan2)
 // ceil(x)
 MATH_FUN_1_TO_INT(ceil, ceil)
 // copysign(x, y)
-STATIC mp_float_t MICROPY_FLOAT_C_FUN(copysign_func)(mp_float_t x, mp_float_t y) {
+static mp_float_t MICROPY_FLOAT_C_FUN(copysign_func)(mp_float_t x, mp_float_t y) {
+    #if MICROPY_PY_MATH_COPYSIGN_FIX_NAN
+    if (isnan(y)) {
+        y = 0.0;
+    }
+    #endif
     return MICROPY_FLOAT_C_FUN(copysign)(x, y);
 }
 MATH_FUN_2(copysign, copysign_func)
 // fabs(x)
-STATIC mp_float_t MICROPY_FLOAT_C_FUN(fabs_func)(mp_float_t x) {
+static mp_float_t MICROPY_FLOAT_C_FUN(fabs_func)(mp_float_t x) {
     return MICROPY_FLOAT_C_FUN(fabs)(x);
 }
 MATH_FUN_1(fabs, fabs_func)
@@ -196,14 +205,24 @@ MATH_FUN_1(erf, erf)
 // erfc(x): return the complementary error function of x
 MATH_FUN_1(erfc, erfc)
 // gamma(x): return the gamma function of x
+#if MICROPY_PY_MATH_GAMMA_FIX_NEGINF
+static mp_float_t MICROPY_FLOAT_C_FUN(tgamma_func)(mp_float_t x) {
+    if (isinf(x) && x < 0) {
+        math_error();
+    }
+    return MICROPY_FLOAT_C_FUN(tgamma)(x);
+}
+MATH_FUN_1(gamma, tgamma_func)
+#else
 MATH_FUN_1(gamma, tgamma)
+#endif
 // lgamma(x): return the natural logarithm of the gamma function of x
 MATH_FUN_1(lgamma, lgamma)
 #endif
 // TODO: fsum
 
 #if MICROPY_PY_MATH_ISCLOSE
-STATIC mp_obj_t mp_math_isclose(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
+static mp_obj_t mp_math_isclose(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
     enum { ARG_rel_tol, ARG_abs_tol };
     static const mp_arg_t allowed_args[] = {
         {MP_QSTR_rel_tol, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
@@ -239,7 +258,7 @@ MP_DEFINE_CONST_FUN_OBJ_KW(mp_math_isclose_obj, 2, mp_math_isclose);
 // Function that takes a variable number of arguments
 
 // log(x[, base])
-STATIC mp_obj_t mp_math_log(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mp_math_log(size_t n_args, const mp_obj_t *args) {
     mp_float_t x = mp_obj_get_float(args[0]);
     if (x <= (mp_float_t)0.0) {
         math_error();
@@ -257,12 +276,12 @@ STATIC mp_obj_t mp_math_log(size_t n_args, const mp_obj_t *args) {
         return mp_obj_new_float(l / MICROPY_FLOAT_C_FUN(log)(base));
     }
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_math_log_obj, 1, 2, mp_math_log);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_math_log_obj, 1, 2, mp_math_log);
 
 // Functions that return a tuple
 
 // frexp(x): converts a floating-point number to fractional and integral components
-STATIC mp_obj_t mp_math_frexp(mp_obj_t x_obj) {
+static mp_obj_t mp_math_frexp(mp_obj_t x_obj) {
     int int_exponent = 0;
     mp_float_t significand = MICROPY_FLOAT_C_FUN(frexp)(mp_obj_get_float(x_obj), &int_exponent);
     mp_obj_t tuple[2];
@@ -270,10 +289,10 @@ STATIC mp_obj_t mp_math_frexp(mp_obj_t x_obj) {
     tuple[1] = mp_obj_new_int(int_exponent);
     return mp_obj_new_tuple(2, tuple);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_math_frexp_obj, mp_math_frexp);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_math_frexp_obj, mp_math_frexp);
 
 // modf(x)
-STATIC mp_obj_t mp_math_modf(mp_obj_t x_obj) {
+static mp_obj_t mp_math_modf(mp_obj_t x_obj) {
     mp_float_t int_part = 0.0;
     mp_float_t x = mp_obj_get_float(x_obj);
     mp_float_t fractional_part = MICROPY_FLOAT_C_FUN(modf)(x, &int_part);
@@ -287,28 +306,28 @@ STATIC mp_obj_t mp_math_modf(mp_obj_t x_obj) {
     tuple[1] = mp_obj_new_float(int_part);
     return mp_obj_new_tuple(2, tuple);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_math_modf_obj, mp_math_modf);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_math_modf_obj, mp_math_modf);
 
 // Angular conversions
 
 // radians(x)
-STATIC mp_obj_t mp_math_radians(mp_obj_t x_obj) {
+static mp_obj_t mp_math_radians(mp_obj_t x_obj) {
     return mp_obj_new_float(mp_obj_get_float(x_obj) * (MP_PI / MICROPY_FLOAT_CONST(180.0)));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_math_radians_obj, mp_math_radians);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_math_radians_obj, mp_math_radians);
 
 // degrees(x)
-STATIC mp_obj_t mp_math_degrees(mp_obj_t x_obj) {
+static mp_obj_t mp_math_degrees(mp_obj_t x_obj) {
     return mp_obj_new_float(mp_obj_get_float(x_obj) * (MICROPY_FLOAT_CONST(180.0) / MP_PI));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_math_degrees_obj, mp_math_degrees);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_math_degrees_obj, mp_math_degrees);
 
 #if MICROPY_PY_MATH_FACTORIAL
 
 #if MICROPY_OPT_MATH_FACTORIAL
 
 // factorial(x): slightly efficient recursive implementation
-STATIC mp_obj_t mp_math_factorial_inner(mp_uint_t start, mp_uint_t end) {
+static mp_obj_t mp_math_factorial_inner(mp_uint_t start, mp_uint_t end) {
     if (start == end) {
         return mp_obj_new_int(start);
     } else if (end - start == 1) {
@@ -326,7 +345,7 @@ STATIC mp_obj_t mp_math_factorial_inner(mp_uint_t start, mp_uint_t end) {
         return mp_binary_op(MP_BINARY_OP_MULTIPLY, left, right);
     }
 }
-STATIC mp_obj_t mp_math_factorial(mp_obj_t x_obj) {
+static mp_obj_t mp_math_factorial(mp_obj_t x_obj) {
     mp_int_t max = mp_obj_get_int(x_obj);
     if (max < 0) {
         mp_raise_ValueError(MP_ERROR_TEXT("negative factorial"));
@@ -340,7 +359,7 @@ STATIC mp_obj_t mp_math_factorial(mp_obj_t x_obj) {
 
 // factorial(x): squared difference implementation
 // based on http://www.luschny.de/math/factorial/index.html
-STATIC mp_obj_t mp_math_factorial(mp_obj_t x_obj) {
+static mp_obj_t mp_math_factorial(mp_obj_t x_obj) {
     mp_int_t max = mp_obj_get_int(x_obj);
     if (max < 0) {
         mp_raise_ValueError(MP_ERROR_TEXT("negative factorial"));
@@ -363,11 +382,11 @@ STATIC mp_obj_t mp_math_factorial(mp_obj_t x_obj) {
 
 #endif
 
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_math_factorial_obj, mp_math_factorial);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_math_factorial_obj, mp_math_factorial);
 
 #endif
 
-STATIC const mp_rom_map_elem_t mp_module_math_globals_table[] = {
+static const mp_rom_map_elem_t mp_module_math_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_math) },
     { MP_ROM_QSTR(MP_QSTR_e), mp_const_float_e },
     { MP_ROM_QSTR(MP_QSTR_pi), mp_const_float_pi },
@@ -428,7 +447,7 @@ STATIC const mp_rom_map_elem_t mp_module_math_globals_table[] = {
     #endif
 };
 
-STATIC MP_DEFINE_CONST_DICT(mp_module_math_globals, mp_module_math_globals_table);
+static MP_DEFINE_CONST_DICT(mp_module_math_globals, mp_module_math_globals_table);
 
 const mp_obj_module_t mp_module_math = {
     .base = { &mp_type_module },
diff --git a/py/modmicropython.c b/py/modmicropython.c
index bdb1e8b9b4c..35ca98eb881 100644
--- a/py/modmicropython.c
+++ b/py/modmicropython.c
@@ -27,7 +27,7 @@
 #include 
 
 #include "py/builtin.h"
-#include "py/stackctrl.h"
+#include "py/cstack.h"
 #include "py/runtime.h"
 #include "py/gc.h"
 #include "py/mphal.h"
@@ -38,7 +38,7 @@
 // living in micropython module
 
 #if MICROPY_ENABLE_COMPILER
-STATIC mp_obj_t mp_micropython_opt_level(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mp_micropython_opt_level(size_t n_args, const mp_obj_t *args) {
     if (n_args == 0) {
         return MP_OBJ_NEW_SMALL_INT(MP_STATE_VM(mp_optimise_value));
     } else {
@@ -46,26 +46,26 @@ STATIC mp_obj_t mp_micropython_opt_level(size_t n_args, const mp_obj_t *args) {
         return mp_const_none;
     }
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_micropython_opt_level_obj, 0, 1, mp_micropython_opt_level);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_micropython_opt_level_obj, 0, 1, mp_micropython_opt_level);
 #endif
 
 #if MICROPY_PY_MICROPYTHON_MEM_INFO
 
 #if MICROPY_MEM_STATS
-STATIC mp_obj_t mp_micropython_mem_total(void) {
+static mp_obj_t mp_micropython_mem_total(void) {
     return MP_OBJ_NEW_SMALL_INT(m_get_total_bytes_allocated());
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_mem_total_obj, mp_micropython_mem_total);
+static MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_mem_total_obj, mp_micropython_mem_total);
 
-STATIC mp_obj_t mp_micropython_mem_current(void) {
+static mp_obj_t mp_micropython_mem_current(void) {
     return MP_OBJ_NEW_SMALL_INT(m_get_current_bytes_allocated());
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_mem_current_obj, mp_micropython_mem_current);
+static MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_mem_current_obj, mp_micropython_mem_current);
 
-STATIC mp_obj_t mp_micropython_mem_peak(void) {
+static mp_obj_t mp_micropython_mem_peak(void) {
     return MP_OBJ_NEW_SMALL_INT(m_get_peak_bytes_allocated());
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_mem_peak_obj, mp_micropython_mem_peak);
+static MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_mem_peak_obj, mp_micropython_mem_peak);
 #endif
 
 mp_obj_t mp_micropython_mem_info(size_t n_args, const mp_obj_t *args) {
@@ -76,9 +76,9 @@ mp_obj_t mp_micropython_mem_info(size_t n_args, const mp_obj_t *args) {
     #endif
     #if MICROPY_STACK_CHECK
     mp_printf(&mp_plat_print, "stack: " UINT_FMT " out of " UINT_FMT "\n",
-        mp_stack_usage(), (mp_uint_t)MP_STATE_THREAD(stack_limit));
+        mp_cstack_usage(), (mp_uint_t)MP_STATE_THREAD(stack_limit));
     #else
-    mp_printf(&mp_plat_print, "stack: " UINT_FMT "\n", mp_stack_usage());
+    mp_printf(&mp_plat_print, "stack: " UINT_FMT "\n", mp_cstack_usage());
     #endif
     #if MICROPY_ENABLE_GC
     gc_dump_info(&mp_plat_print);
@@ -91,82 +91,104 @@ mp_obj_t mp_micropython_mem_info(size_t n_args, const mp_obj_t *args) {
     #endif
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_micropython_mem_info_obj, 0, 1, mp_micropython_mem_info);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_micropython_mem_info_obj, 0, 1, mp_micropython_mem_info);
 
-STATIC mp_obj_t mp_micropython_qstr_info(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mp_micropython_qstr_info(size_t n_args, const mp_obj_t *args) {
     (void)args;
     size_t n_pool, n_qstr, n_str_data_bytes, n_total_bytes;
     qstr_pool_info(&n_pool, &n_qstr, &n_str_data_bytes, &n_total_bytes);
     mp_printf(&mp_plat_print, "qstr pool: n_pool=%u, n_qstr=%u, n_str_data_bytes=%u, n_total_bytes=%u\n",
-        n_pool, n_qstr, n_str_data_bytes, n_total_bytes);
+        (uint)n_pool, (uint)n_qstr, (uint)n_str_data_bytes, (uint)n_total_bytes);
     if (n_args == 1) {
         // arg given means dump qstr data
         qstr_dump_data();
     }
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_micropython_qstr_info_obj, 0, 1, mp_micropython_qstr_info);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_micropython_qstr_info_obj, 0, 1, mp_micropython_qstr_info);
 
 #endif // MICROPY_PY_MICROPYTHON_MEM_INFO
 
 #if MICROPY_PY_MICROPYTHON_STACK_USE
-STATIC mp_obj_t mp_micropython_stack_use(void) {
-    return MP_OBJ_NEW_SMALL_INT(mp_stack_usage());
+static mp_obj_t mp_micropython_stack_use(void) {
+    return MP_OBJ_NEW_SMALL_INT(mp_cstack_usage());
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_stack_use_obj, mp_micropython_stack_use);
+static MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_stack_use_obj, mp_micropython_stack_use);
 #endif
 
 #if MICROPY_ENABLE_PYSTACK
-STATIC mp_obj_t mp_micropython_pystack_use(void) {
+static mp_obj_t mp_micropython_pystack_use(void) {
     return MP_OBJ_NEW_SMALL_INT(mp_pystack_usage());
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_pystack_use_obj, mp_micropython_pystack_use);
+static MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_pystack_use_obj, mp_micropython_pystack_use);
 #endif
 
 #if MICROPY_ENABLE_GC
-STATIC mp_obj_t mp_micropython_heap_lock(void) {
+static mp_obj_t mp_micropython_heap_lock(void) {
     gc_lock();
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_heap_lock_obj, mp_micropython_heap_lock);
+static MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_heap_lock_obj, mp_micropython_heap_lock);
 
-STATIC mp_obj_t mp_micropython_heap_unlock(void) {
+static mp_obj_t mp_micropython_heap_unlock(void) {
     gc_unlock();
-    return MP_OBJ_NEW_SMALL_INT(MP_STATE_THREAD(gc_lock_depth));
+    return MP_OBJ_NEW_SMALL_INT(MP_STATE_THREAD(gc_lock_depth) >> GC_LOCK_DEPTH_SHIFT);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_heap_unlock_obj, mp_micropython_heap_unlock);
+static MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_heap_unlock_obj, mp_micropython_heap_unlock);
 
 #if MICROPY_PY_MICROPYTHON_HEAP_LOCKED
-STATIC mp_obj_t mp_micropython_heap_locked(void) {
-    return MP_OBJ_NEW_SMALL_INT(MP_STATE_THREAD(gc_lock_depth));
+static mp_obj_t mp_micropython_heap_locked(void) {
+    return MP_OBJ_NEW_SMALL_INT(MP_STATE_THREAD(gc_lock_depth) >> GC_LOCK_DEPTH_SHIFT);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_heap_locked_obj, mp_micropython_heap_locked);
+static MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_heap_locked_obj, mp_micropython_heap_locked);
 #endif
 #endif
 
 #if MICROPY_ENABLE_EMERGENCY_EXCEPTION_BUF && (MICROPY_EMERGENCY_EXCEPTION_BUF_SIZE == 0)
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_alloc_emergency_exception_buf_obj, mp_alloc_emergency_exception_buf);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_alloc_emergency_exception_buf_obj, mp_alloc_emergency_exception_buf);
 #endif
 
 #if MICROPY_KBD_EXCEPTION
-STATIC mp_obj_t mp_micropython_kbd_intr(mp_obj_t int_chr_in) {
+static mp_obj_t mp_micropython_kbd_intr(mp_obj_t int_chr_in) {
     mp_hal_set_interrupt_char(mp_obj_get_int(int_chr_in));
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_micropython_kbd_intr_obj, mp_micropython_kbd_intr);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_micropython_kbd_intr_obj, mp_micropython_kbd_intr);
 #endif
 
 #if MICROPY_ENABLE_SCHEDULER
-STATIC mp_obj_t mp_micropython_schedule(mp_obj_t function, mp_obj_t arg) {
+
+#if MICROPY_KBD_EXCEPTION && MICROPY_SCHEDULER_STATIC_NODES
+static mp_sched_node_t mp_keyboard_interrupt_sched_node;
+static void mp_sched_keyboard_interrupt_wrapper(mp_sched_node_t *node) {
+    mp_sched_keyboard_interrupt();
+}
+#endif
+
+static mp_obj_t mp_micropython_schedule(mp_obj_t function, mp_obj_t arg) {
+    #if MICROPY_KBD_EXCEPTION
+    if (function == MP_OBJ_FROM_PTR(&mp_micropython_kbd_intr_obj)) {
+        #if MICROPY_SCHEDULER_STATIC_NODES
+        // Allow calling `micropython.schedule(micropython.kbd_intr, None)` as a
+        // special case to schedule a keyboard interrupt.
+        mp_sched_schedule_node(&mp_keyboard_interrupt_sched_node, mp_sched_keyboard_interrupt_wrapper);
+        return mp_const_none;
+        #else
+        // Signal that passing `micropython.kbd_intr` is not supported.
+        mp_raise_ValueError(NULL);
+        #endif
+    }
+    #endif
     if (!mp_sched_schedule(function, arg)) {
         mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("schedule queue full"));
     }
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_micropython_schedule_obj, mp_micropython_schedule);
+static MP_DEFINE_CONST_FUN_OBJ_2(mp_micropython_schedule_obj, mp_micropython_schedule);
+
 #endif
 
-STATIC const mp_rom_map_elem_t mp_module_micropython_globals_table[] = {
+static const mp_rom_map_elem_t mp_module_micropython_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_micropython) },
     { MP_ROM_QSTR(MP_QSTR_const), MP_ROM_PTR(&mp_identity_obj) },
     #if MICROPY_ENABLE_COMPILER
@@ -200,12 +222,15 @@ STATIC const mp_rom_map_elem_t mp_module_micropython_globals_table[] = {
     #if MICROPY_KBD_EXCEPTION
     { MP_ROM_QSTR(MP_QSTR_kbd_intr), MP_ROM_PTR(&mp_micropython_kbd_intr_obj) },
     #endif
+    #if MICROPY_PY_MICROPYTHON_RINGIO
+    { MP_ROM_QSTR(MP_QSTR_RingIO), MP_ROM_PTR(&mp_type_ringio) },
+    #endif
     #if MICROPY_ENABLE_SCHEDULER
     { MP_ROM_QSTR(MP_QSTR_schedule), MP_ROM_PTR(&mp_micropython_schedule_obj) },
     #endif
 };
 
-STATIC MP_DEFINE_CONST_DICT(mp_module_micropython_globals, mp_module_micropython_globals_table);
+static MP_DEFINE_CONST_DICT(mp_module_micropython_globals, mp_module_micropython_globals_table);
 
 const mp_obj_module_t mp_module_micropython = {
     .base = { &mp_type_module },
diff --git a/py/modstring.c b/py/modstring.c
new file mode 100644
index 00000000000..26e0c2d83cb
--- /dev/null
+++ b/py/modstring.c
@@ -0,0 +1,56 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2026 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "py/obj.h"
+
+#if MICROPY_PY_TSTRINGS
+
+static const mp_rom_map_elem_t mp_module_string_templatelib_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_string_dot_templatelib) },
+    { MP_ROM_QSTR(MP_QSTR_Template), MP_ROM_PTR(&mp_type_template) },
+    { MP_ROM_QSTR(MP_QSTR_Interpolation), MP_ROM_PTR(&mp_type_interpolation) },
+};
+static MP_DEFINE_CONST_DICT(mp_module_string_templatelib_globals, mp_module_string_templatelib_globals_table);
+
+static const mp_obj_module_t mp_module_string_templatelib = {
+    .base = { &mp_type_module },
+    .globals = (mp_obj_dict_t *)&mp_module_string_templatelib_globals,
+};
+
+static const mp_rom_map_elem_t mp_module_string_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_string) },
+    { MP_ROM_QSTR(MP_QSTR_templatelib), MP_ROM_PTR(&mp_module_string_templatelib) },
+};
+static MP_DEFINE_CONST_DICT(mp_module_string_globals, mp_module_string_globals_table);
+
+const mp_obj_module_t mp_module_string = {
+    .base = { &mp_type_module },
+    .globals = (mp_obj_dict_t *)&mp_module_string_globals,
+};
+
+MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_string, mp_module_string);
+
+#endif // MICROPY_PY_TSTRINGS
diff --git a/py/modstruct.c b/py/modstruct.c
index ec86d4b9c29..3b9dd30aa70 100644
--- a/py/modstruct.c
+++ b/py/modstruct.c
@@ -52,7 +52,7 @@
         character data".
  */
 
-STATIC char get_fmt_type(const char **fmt) {
+static char get_fmt_type(const char **fmt) {
     char t = **fmt;
     switch (t) {
         case '!':
@@ -71,7 +71,7 @@ STATIC char get_fmt_type(const char **fmt) {
     return t;
 }
 
-STATIC mp_uint_t get_fmt_num(const char **p) {
+static mp_uint_t get_fmt_num(const char **p) {
     const char *num = *p;
     uint len = 1;
     while (unichar_isdigit(*++num)) {
@@ -82,7 +82,7 @@ STATIC mp_uint_t get_fmt_num(const char **p) {
     return val;
 }
 
-STATIC size_t calc_size_items(const char *fmt, size_t *total_sz) {
+static size_t calc_size_items(const char *fmt, size_t *total_sz) {
     char fmt_type = get_fmt_type(&fmt);
     size_t total_cnt = 0;
     size_t size;
@@ -92,7 +92,9 @@ STATIC size_t calc_size_items(const char *fmt, size_t *total_sz) {
             cnt = get_fmt_num(&fmt);
         }
 
-        if (*fmt == 's') {
+        if (*fmt == 'x') {
+            size += cnt;
+        } else if (*fmt == 's') {
             total_cnt += 1;
             size += cnt;
         } else {
@@ -110,7 +112,7 @@ STATIC size_t calc_size_items(const char *fmt, size_t *total_sz) {
     return total_cnt;
 }
 
-STATIC mp_obj_t struct_calcsize(mp_obj_t fmt_in) {
+static mp_obj_t struct_calcsize(mp_obj_t fmt_in) {
     const char *fmt = mp_obj_str_get_str(fmt_in);
     size_t size;
     calc_size_items(fmt, &size);
@@ -118,7 +120,7 @@ STATIC mp_obj_t struct_calcsize(mp_obj_t fmt_in) {
 }
 MP_DEFINE_CONST_FUN_OBJ_1(struct_calcsize_obj, struct_calcsize);
 
-STATIC mp_obj_t struct_unpack_from(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t struct_unpack_from(size_t n_args, const mp_obj_t *args) {
     // unpack requires that the buffer be exactly the right size.
     // unpack_from requires that the buffer be "big enough".
     // Since we implement unpack and unpack_from using the same function
@@ -159,7 +161,9 @@ STATIC mp_obj_t struct_unpack_from(size_t n_args, const mp_obj_t *args) {
             cnt = get_fmt_num(&fmt);
         }
         mp_obj_t item;
-        if (*fmt == 's') {
+        if (*fmt == 'x') {
+            p += cnt;
+        } else if (*fmt == 's') {
             item = mp_obj_new_bytes(p, cnt);
             p += cnt;
             res->items[i++] = item;
@@ -176,7 +180,7 @@ STATIC mp_obj_t struct_unpack_from(size_t n_args, const mp_obj_t *args) {
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(struct_unpack_from_obj, 2, 3, struct_unpack_from);
 
 // This function assumes there is enough room in p to store all the values
-STATIC void struct_pack_into_internal(mp_obj_t fmt_in, byte *p, size_t n_args, const mp_obj_t *args) {
+static void struct_pack_into_internal(mp_obj_t fmt_in, byte *p, size_t n_args, const mp_obj_t *args) {
     const char *fmt = mp_obj_str_get_str(fmt_in);
     char fmt_type = get_fmt_type(&fmt);
 
@@ -192,7 +196,10 @@ STATIC void struct_pack_into_internal(mp_obj_t fmt_in, byte *p, size_t n_args, c
             cnt = get_fmt_num(&fmt);
         }
 
-        if (*fmt == 's') {
+        if (*fmt == 'x') {
+            memset(p, 0, cnt);
+            p += cnt;
+        } else if (*fmt == 's') {
             mp_buffer_info_t bufinfo;
             mp_get_buffer_raise(args[i++], &bufinfo, MP_BUFFER_READ);
             mp_uint_t to_copy = cnt;
@@ -212,7 +219,7 @@ STATIC void struct_pack_into_internal(mp_obj_t fmt_in, byte *p, size_t n_args, c
     }
 }
 
-STATIC mp_obj_t struct_pack(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t struct_pack(size_t n_args, const mp_obj_t *args) {
     // TODO: "The arguments must match the values required by the format exactly."
     mp_int_t size = MP_OBJ_SMALL_INT_VALUE(struct_calcsize(args[0]));
     vstr_t vstr;
@@ -224,7 +231,7 @@ STATIC mp_obj_t struct_pack(size_t n_args, const mp_obj_t *args) {
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(struct_pack_obj, 1, MP_OBJ_FUN_ARGS_MAX, struct_pack);
 
-STATIC mp_obj_t struct_pack_into(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t struct_pack_into(size_t n_args, const mp_obj_t *args) {
     mp_buffer_info_t bufinfo;
     mp_get_buffer_raise(args[1], &bufinfo, MP_BUFFER_WRITE);
     mp_int_t offset = mp_obj_get_int(args[2]);
@@ -250,8 +257,8 @@ STATIC mp_obj_t struct_pack_into(size_t n_args, const mp_obj_t *args) {
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(struct_pack_into_obj, 3, MP_OBJ_FUN_ARGS_MAX, struct_pack_into);
 
-STATIC const mp_rom_map_elem_t mp_module_struct_globals_table[] = {
-    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_ustruct) },
+static const mp_rom_map_elem_t mp_module_struct_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_struct) },
     { MP_ROM_QSTR(MP_QSTR_calcsize), MP_ROM_PTR(&struct_calcsize_obj) },
     { MP_ROM_QSTR(MP_QSTR_pack), MP_ROM_PTR(&struct_pack_obj) },
     { MP_ROM_QSTR(MP_QSTR_pack_into), MP_ROM_PTR(&struct_pack_into_obj) },
@@ -259,13 +266,13 @@ STATIC const mp_rom_map_elem_t mp_module_struct_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR_unpack_from), MP_ROM_PTR(&struct_unpack_from_obj) },
 };
 
-STATIC MP_DEFINE_CONST_DICT(mp_module_struct_globals, mp_module_struct_globals_table);
+static MP_DEFINE_CONST_DICT(mp_module_struct_globals, mp_module_struct_globals_table);
 
-const mp_obj_module_t mp_module_ustruct = {
+const mp_obj_module_t mp_module_struct = {
     .base = { &mp_type_module },
     .globals = (mp_obj_dict_t *)&mp_module_struct_globals,
 };
 
-MP_REGISTER_MODULE(MP_QSTR_ustruct, mp_module_ustruct);
+MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_struct, mp_module_struct);
 
 #endif
diff --git a/py/modsys.c b/py/modsys.c
index 35af761a08f..3dae92681ab 100644
--- a/py/modsys.c
+++ b/py/modsys.c
@@ -36,7 +36,7 @@
 #include "py/smallint.h"
 #include "py/runtime.h"
 #include "py/persistentcode.h"
-#include "extmod/moduplatform.h"
+#include "extmod/modplatform.h"
 #include "genhdr/mpversion.h"
 
 #if MICROPY_PY_SYS_SETTRACE
@@ -56,54 +56,119 @@ const mp_print_t mp_sys_stdout_print = {&mp_sys_stdout_obj, mp_stream_write_adap
 #endif
 
 // version - Python language version that this implementation conforms to, as a string
-STATIC const MP_DEFINE_STR_OBJ(mp_sys_version_obj, "3.4.0; " MICROPY_BANNER_NAME_AND_VERSION);
+static const MP_DEFINE_STR_OBJ(mp_sys_version_obj, "3.4.0; " MICROPY_BANNER_NAME_AND_VERSION);
 
 // version_info - Python language version that this implementation conforms to, as a tuple of ints
-#define I(n) MP_OBJ_NEW_SMALL_INT(n)
-// TODO: CPython is now at 5-element array, but save 2 els so far...
-STATIC const mp_obj_tuple_t mp_sys_version_info_obj = {{&mp_type_tuple}, 3, {I(3), I(4), I(0)}};
+// TODO: CPython is now at 5-element array (major, minor, micro, releaselevel, serial), but save 2 els so far...
+static const mp_rom_obj_tuple_t mp_sys_version_info_obj = {{&mp_type_tuple}, 3, {MP_ROM_INT(3), MP_ROM_INT(4), MP_ROM_INT(0)}};
 
 // sys.implementation object
 // this holds the MicroPython version
-STATIC const mp_obj_tuple_t mp_sys_implementation_version_info_obj = {
+static const mp_rom_obj_tuple_t mp_sys_implementation_version_info_obj = {
     {&mp_type_tuple},
-    3,
-    { I(MICROPY_VERSION_MAJOR), I(MICROPY_VERSION_MINOR), I(MICROPY_VERSION_MICRO) }
+    4,
+    {
+        MP_ROM_INT(MICROPY_VERSION_MAJOR),
+        MP_ROM_INT(MICROPY_VERSION_MINOR),
+        MP_ROM_INT(MICROPY_VERSION_MICRO),
+        #if MICROPY_VERSION_PRERELEASE
+        MP_ROM_QSTR(MP_QSTR_preview),
+        #else
+        MP_ROM_QSTR(MP_QSTR_),
+        #endif
+    }
 };
-STATIC const MP_DEFINE_STR_OBJ(mp_sys_implementation_machine_obj, MICROPY_BANNER_MACHINE);
-#if MICROPY_PERSISTENT_CODE_LOAD
-#define SYS_IMPLEMENTATION_ELEMS \
-    MP_ROM_QSTR(MP_QSTR_micropython), \
-    MP_ROM_PTR(&mp_sys_implementation_version_info_obj), \
-    MP_ROM_PTR(&mp_sys_implementation_machine_obj), \
-    MP_ROM_INT(MPY_FILE_HEADER_INT)
-#else
-#define SYS_IMPLEMENTATION_ELEMS \
+static const MP_DEFINE_STR_OBJ(mp_sys_implementation_machine_obj, MICROPY_BANNER_MACHINE);
+#define SYS_IMPLEMENTATION_ELEMS_BASE \
     MP_ROM_QSTR(MP_QSTR_micropython), \
     MP_ROM_PTR(&mp_sys_implementation_version_info_obj), \
     MP_ROM_PTR(&mp_sys_implementation_machine_obj)
+
+#if MICROPY_PERSISTENT_CODE_LOAD
+// Note: Adding architecture flags to _mpy will break if the flags information
+//       takes up more bits than what is available in a small-int value.
+#if MICROPY_EMIT_RV32
+#include "py/asmrv32.h"
+#define MPY_FILE_ARCH_FLAGS (MICROPY_RV32_EXTENSIONS << 16)
+#else
+#define MPY_FILE_ARCH_FLAGS (0)
 #endif
+#define SYS_IMPLEMENTATION_ELEMS__MPY \
+    , MP_ROM_INT(MPY_FILE_HEADER_INT | MPY_FILE_ARCH_FLAGS)
+#else
+#define SYS_IMPLEMENTATION_ELEMS__MPY
+#endif
+
 #if MICROPY_PY_ATTRTUPLE
-STATIC const qstr impl_fields[] = {
+
+#if defined(MICROPY_BOARD_BUILD_NAME)
+static const MP_DEFINE_STR_OBJ(mp_sys_implementation__build_obj, MICROPY_BOARD_BUILD_NAME);
+#define MICROPY_BOARD_BUILD (1)
+#define SYS_IMPLEMENTATION_ELEMS__BUILD \
+    , MP_ROM_PTR(&mp_sys_implementation__build_obj)
+#else
+#define MICROPY_BOARD_BUILD (0)
+#define SYS_IMPLEMENTATION_ELEMS__BUILD
+#endif
+
+#if MICROPY_PY_THREAD
+#if MICROPY_PY_THREAD_GIL
+#define SYS_IMPLEMENTATION_ELEMS__THREAD \
+    , MP_ROM_QSTR(MP_QSTR_GIL)
+#else
+#define SYS_IMPLEMENTATION_ELEMS__THREAD \
+    , MP_ROM_QSTR(MP_QSTR_unsafe)
+#endif
+#else
+#define SYS_IMPLEMENTATION_ELEMS__THREAD
+#endif
+
+#if MICROPY_PREVIEW_VERSION_2
+#define SYS_IMPLEMENTATION_ELEMS__V2 \
+    , MP_ROM_TRUE
+#else
+#define SYS_IMPLEMENTATION_ELEMS__V2
+#endif
+
+static const qstr impl_fields[] = {
     MP_QSTR_name,
     MP_QSTR_version,
     MP_QSTR__machine,
     #if MICROPY_PERSISTENT_CODE_LOAD
     MP_QSTR__mpy,
     #endif
+    #if defined(MICROPY_BOARD_BUILD_NAME)
+    MP_QSTR__build,
+    #endif
+    #if MICROPY_PY_THREAD
+    MP_QSTR__thread,
+    #endif
+    #if MICROPY_PREVIEW_VERSION_2
+    MP_QSTR__v2,
+    #endif
 };
-STATIC MP_DEFINE_ATTRTUPLE(
+static MP_DEFINE_ATTRTUPLE(
     mp_sys_implementation_obj,
     impl_fields,
-    3 + MICROPY_PERSISTENT_CODE_LOAD,
-    SYS_IMPLEMENTATION_ELEMS
+    3 + MICROPY_PERSISTENT_CODE_LOAD + MICROPY_BOARD_BUILD + MICROPY_PY_THREAD + MICROPY_PREVIEW_VERSION_2,
+    SYS_IMPLEMENTATION_ELEMS_BASE
+    SYS_IMPLEMENTATION_ELEMS__MPY
+    SYS_IMPLEMENTATION_ELEMS__BUILD
+    SYS_IMPLEMENTATION_ELEMS__THREAD
+    SYS_IMPLEMENTATION_ELEMS__V2
     );
 #else
-STATIC const mp_rom_obj_tuple_t mp_sys_implementation_obj = {
+static const mp_rom_obj_tuple_t mp_sys_implementation_obj = {
     {&mp_type_tuple},
     3 + MICROPY_PERSISTENT_CODE_LOAD,
+    // Do not include SYS_IMPLEMENTATION_ELEMS__BUILD, SYS_IMPLEMENTATION_ELEMS__THREAD
+    // or SYS_IMPLEMENTATION_ELEMS__V2 because SYS_IMPLEMENTATION_ELEMS__MPY may be empty if
+    // MICROPY_PERSISTENT_CODE_LOAD is disabled, which means they'll share
+    // the same index. Cannot query _build, _thread or _v2 if MICROPY_PY_ATTRTUPLE is
+    // disabled.
     {
-        SYS_IMPLEMENTATION_ELEMS
+        SYS_IMPLEMENTATION_ELEMS_BASE
+                                SYS_IMPLEMENTATION_ELEMS__MPY
     }
 };
 #endif
@@ -112,7 +177,7 @@ STATIC const mp_rom_obj_tuple_t mp_sys_implementation_obj = {
 
 #ifdef MICROPY_PY_SYS_PLATFORM
 // platform - the platform that MicroPython is running on
-STATIC const MP_DEFINE_STR_OBJ(mp_sys_platform_obj, MICROPY_PY_SYS_PLATFORM);
+static const MP_DEFINE_STR_OBJ(mp_sys_platform_obj, MICROPY_PY_SYS_PLATFORM);
 #endif
 
 #ifdef MICROPY_PY_SYS_EXECUTABLE
@@ -121,8 +186,12 @@ STATIC const MP_DEFINE_STR_OBJ(mp_sys_platform_obj, MICROPY_PY_SYS_PLATFORM);
 MP_DEFINE_STR_OBJ(mp_sys_executable_obj, "");
 #endif
 
+#if MICROPY_PY_SYS_INTERN
+MP_DEFINE_CONST_FUN_OBJ_1(mp_sys_intern_obj, mp_obj_str_intern_checked);
+#endif
+
 // exit([retval]): raise SystemExit, with optional argument given to the exception
-STATIC mp_obj_t mp_sys_exit(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mp_sys_exit(size_t n_args, const mp_obj_t *args) {
     if (n_args == 0) {
         mp_raise_type(&mp_type_SystemExit);
     } else {
@@ -131,7 +200,7 @@ STATIC mp_obj_t mp_sys_exit(size_t n_args, const mp_obj_t *args) {
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_sys_exit_obj, 0, 1, mp_sys_exit);
 
-STATIC mp_obj_t mp_sys_print_exception(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mp_sys_print_exception(size_t n_args, const mp_obj_t *args) {
     #if MICROPY_PY_IO && MICROPY_PY_SYS_STDFILES
     void *stream_obj = &mp_sys_stdout_obj;
     if (n_args > 1) {
@@ -151,7 +220,7 @@ STATIC mp_obj_t mp_sys_print_exception(size_t n_args, const mp_obj_t *args) {
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_sys_print_exception_obj, 1, 2, mp_sys_print_exception);
 
 #if MICROPY_PY_SYS_EXC_INFO
-STATIC mp_obj_t mp_sys_exc_info(void) {
+static mp_obj_t mp_sys_exc_info(void) {
     mp_obj_t cur_exc = MP_OBJ_FROM_PTR(MP_STATE_VM(cur_exception));
     mp_obj_tuple_t *t = MP_OBJ_TO_PTR(mp_obj_new_tuple(3, NULL));
 
@@ -171,32 +240,54 @@ MP_DEFINE_CONST_FUN_OBJ_0(mp_sys_exc_info_obj, mp_sys_exc_info);
 #endif
 
 #if MICROPY_PY_SYS_GETSIZEOF
-STATIC mp_obj_t mp_sys_getsizeof(mp_obj_t obj) {
+static mp_obj_t mp_sys_getsizeof(mp_obj_t obj) {
     return mp_unary_op(MP_UNARY_OP_SIZEOF, obj);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_sys_getsizeof_obj, mp_sys_getsizeof);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_sys_getsizeof_obj, mp_sys_getsizeof);
 #endif
 
 #if MICROPY_PY_SYS_ATEXIT
 // atexit(callback): Callback is called when sys.exit is called.
-STATIC mp_obj_t mp_sys_atexit(mp_obj_t obj) {
+static mp_obj_t mp_sys_atexit(mp_obj_t obj) {
     mp_obj_t old = MP_STATE_VM(sys_exitfunc);
     MP_STATE_VM(sys_exitfunc) = obj;
     return old;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_sys_atexit_obj, mp_sys_atexit);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_sys_atexit_obj, mp_sys_atexit);
 #endif
 
 #if MICROPY_PY_SYS_SETTRACE
 // settrace(tracefunc): Set the system's trace function.
-STATIC mp_obj_t mp_sys_settrace(mp_obj_t obj) {
+static mp_obj_t mp_sys_settrace(mp_obj_t obj) {
     return mp_prof_settrace(obj);
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_sys_settrace_obj, mp_sys_settrace);
 #endif // MICROPY_PY_SYS_SETTRACE
 
+#if MICROPY_PY_SYS_PATH && !MICROPY_PY_SYS_ATTR_DELEGATION
+#error "MICROPY_PY_SYS_PATH requires MICROPY_PY_SYS_ATTR_DELEGATION"
+#endif
+
+#if MICROPY_PY_SYS_PS1_PS2 && !MICROPY_PY_SYS_ATTR_DELEGATION
+#error "MICROPY_PY_SYS_PS1_PS2 requires MICROPY_PY_SYS_ATTR_DELEGATION"
+#endif
+
+#if MICROPY_PY_SYS_TRACEBACKLIMIT && !MICROPY_PY_SYS_ATTR_DELEGATION
+#error "MICROPY_PY_SYS_TRACEBACKLIMIT requires MICROPY_PY_SYS_ATTR_DELEGATION"
+#endif
+
+#if MICROPY_PY_SYS_ATTR_DELEGATION && !MICROPY_MODULE_ATTR_DELEGATION
+#error "MICROPY_PY_SYS_ATTR_DELEGATION requires MICROPY_MODULE_ATTR_DELEGATION"
+#endif
+
 #if MICROPY_PY_SYS_ATTR_DELEGATION
-STATIC const uint16_t sys_mutable_keys[] = {
+// Must be kept in sync with the enum at the top of mpstate.h.
+static const uint16_t sys_mutable_keys[] = {
+    #if MICROPY_PY_SYS_PATH
+    // Code should access this (as an mp_obj_t) for use with e.g.
+    // mp_obj_list_append by using the `mp_sys_path` macro defined in runtime.h.
+    MP_QSTR_path,
+    #endif
     #if MICROPY_PY_SYS_PS1_PS2
     MP_QSTR_ps1,
     MP_QSTR_ps2,
@@ -207,18 +298,19 @@ STATIC const uint16_t sys_mutable_keys[] = {
     MP_QSTRnull,
 };
 
-STATIC void mp_module_sys_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+void mp_module_sys_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     MP_STATIC_ASSERT(MP_ARRAY_SIZE(sys_mutable_keys) == MP_SYS_MUTABLE_NUM + 1);
     MP_STATIC_ASSERT(MP_ARRAY_SIZE(MP_STATE_VM(sys_mutable)) == MP_SYS_MUTABLE_NUM);
     mp_module_generic_attr(attr, dest, sys_mutable_keys, MP_STATE_VM(sys_mutable));
 }
 #endif
 
-STATIC const mp_rom_map_elem_t mp_module_sys_globals_table[] = {
+static const mp_rom_map_elem_t mp_module_sys_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sys) },
 
-    { MP_ROM_QSTR(MP_QSTR_path), MP_ROM_PTR(&MP_STATE_VM(mp_sys_path_obj)) },
+    #if MICROPY_PY_SYS_ARGV
     { MP_ROM_QSTR(MP_QSTR_argv), MP_ROM_PTR(&MP_STATE_VM(mp_sys_argv_obj)) },
+    #endif
     { MP_ROM_QSTR(MP_QSTR_version), MP_ROM_PTR(&mp_sys_version_obj) },
     { MP_ROM_QSTR(MP_QSTR_version_info), MP_ROM_PTR(&mp_sys_version_info_obj) },
     { MP_ROM_QSTR(MP_QSTR_implementation), MP_ROM_PTR(&mp_sys_implementation_obj) },
@@ -244,6 +336,10 @@ STATIC const mp_rom_map_elem_t mp_module_sys_globals_table[] = {
     #endif
     #endif
 
+    #if MICROPY_PY_SYS_INTERN
+    { MP_ROM_QSTR(MP_QSTR_intern), MP_ROM_PTR(&mp_sys_intern_obj) },
+    #endif
+
     #if MICROPY_PY_SYS_EXIT
     { MP_ROM_QSTR(MP_QSTR_exit), MP_ROM_PTR(&mp_sys_exit_obj) },
     #endif
@@ -280,26 +376,25 @@ STATIC const mp_rom_map_elem_t mp_module_sys_globals_table[] = {
     #if MICROPY_PY_SYS_ATEXIT
     { MP_ROM_QSTR(MP_QSTR_atexit), MP_ROM_PTR(&mp_sys_atexit_obj) },
     #endif
-
-    #if MICROPY_PY_SYS_ATTR_DELEGATION
-    // Delegation of attr lookup.
-    MP_MODULE_ATTR_DELEGATION_ENTRY(&mp_module_sys_attr),
-    #endif
 };
 
-STATIC MP_DEFINE_CONST_DICT(mp_module_sys_globals, mp_module_sys_globals_table);
+static MP_DEFINE_CONST_DICT(mp_module_sys_globals, mp_module_sys_globals_table);
 
 const mp_obj_module_t mp_module_sys = {
     .base = { &mp_type_module },
     .globals = (mp_obj_dict_t *)&mp_module_sys_globals,
 };
 
-MP_REGISTER_MODULE(MP_QSTR_usys, mp_module_sys);
+// Unlike the other CPython-compatible modules, sys is not extensible from the
+// filesystem. We rely on it to work so that things like sys.path are always
+// available.
+MP_REGISTER_MODULE(MP_QSTR_sys, mp_module_sys);
 
-// If MICROPY_PY_SYS_PATH_ARGV_DEFAULTS is not enabled then these two lists
-// must be initialised after the call to mp_init.
-MP_REGISTER_ROOT_POINTER(mp_obj_list_t mp_sys_path_obj);
+#if MICROPY_PY_SYS_ARGV
+// Code should access this (as an mp_obj_t) for use with e.g.
+// mp_obj_list_append by using the `mp_sys_argv` macro defined in runtime.h.
 MP_REGISTER_ROOT_POINTER(mp_obj_list_t mp_sys_argv_obj);
+#endif
 
 #if MICROPY_PY_SYS_EXC_INFO
 // current exception being handled, for sys.exc_info()
@@ -314,6 +409,7 @@ MP_REGISTER_ROOT_POINTER(mp_obj_t sys_exitfunc);
 #if MICROPY_PY_SYS_ATTR_DELEGATION
 // Contains mutable sys attributes.
 MP_REGISTER_ROOT_POINTER(mp_obj_t sys_mutable[MP_SYS_MUTABLE_NUM]);
+MP_REGISTER_MODULE_DELEGATION(mp_module_sys, mp_module_sys_attr);
 #endif
 
 #endif // MICROPY_PY_SYS
diff --git a/py/modthread.c b/py/modthread.c
index 51d63e47037..7742ff68471 100644
--- a/py/modthread.c
+++ b/py/modthread.c
@@ -28,7 +28,6 @@
 #include 
 
 #include "py/runtime.h"
-#include "py/stackctrl.h"
 
 #if MICROPY_PY_THREAD
 
@@ -45,7 +44,7 @@
 /****************************************************************/
 // Lock object
 
-STATIC const mp_obj_type_t mp_type_thread_lock;
+static const mp_obj_type_t mp_type_thread_lock;
 
 typedef struct _mp_obj_thread_lock_t {
     mp_obj_base_t base;
@@ -53,14 +52,14 @@ typedef struct _mp_obj_thread_lock_t {
     volatile bool locked;
 } mp_obj_thread_lock_t;
 
-STATIC mp_obj_thread_lock_t *mp_obj_new_thread_lock(void) {
+static mp_obj_thread_lock_t *mp_obj_new_thread_lock(void) {
     mp_obj_thread_lock_t *self = mp_obj_malloc(mp_obj_thread_lock_t, &mp_type_thread_lock);
     mp_thread_mutex_init(&self->mutex);
     self->locked = false;
     return self;
 }
 
-STATIC mp_obj_t thread_lock_acquire(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t thread_lock_acquire(size_t n_args, const mp_obj_t *args) {
     mp_obj_thread_lock_t *self = MP_OBJ_TO_PTR(args[0]);
     bool wait = true;
     if (n_args > 1) {
@@ -79,9 +78,9 @@ STATIC mp_obj_t thread_lock_acquire(size_t n_args, const mp_obj_t *args) {
         mp_raise_OSError(-ret);
     }
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(thread_lock_acquire_obj, 1, 3, thread_lock_acquire);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(thread_lock_acquire_obj, 1, 3, thread_lock_acquire);
 
-STATIC mp_obj_t thread_lock_release(mp_obj_t self_in) {
+static mp_obj_t thread_lock_release(mp_obj_t self_in) {
     mp_obj_thread_lock_t *self = MP_OBJ_TO_PTR(self_in);
     if (!self->locked) {
         mp_raise_msg(&mp_type_RuntimeError, NULL);
@@ -92,21 +91,21 @@ STATIC mp_obj_t thread_lock_release(mp_obj_t self_in) {
     MP_THREAD_GIL_ENTER();
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(thread_lock_release_obj, thread_lock_release);
+static MP_DEFINE_CONST_FUN_OBJ_1(thread_lock_release_obj, thread_lock_release);
 
-STATIC mp_obj_t thread_lock_locked(mp_obj_t self_in) {
+static mp_obj_t thread_lock_locked(mp_obj_t self_in) {
     mp_obj_thread_lock_t *self = MP_OBJ_TO_PTR(self_in);
     return mp_obj_new_bool(self->locked);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(thread_lock_locked_obj, thread_lock_locked);
+static MP_DEFINE_CONST_FUN_OBJ_1(thread_lock_locked_obj, thread_lock_locked);
 
-STATIC mp_obj_t thread_lock___exit__(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t thread_lock___exit__(size_t n_args, const mp_obj_t *args) {
     (void)n_args; // unused
     return thread_lock_release(args[0]);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(thread_lock___exit___obj, 4, 4, thread_lock___exit__);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(thread_lock___exit___obj, 4, 4, thread_lock___exit__);
 
-STATIC const mp_rom_map_elem_t thread_lock_locals_dict_table[] = {
+static const mp_rom_map_elem_t thread_lock_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_acquire), MP_ROM_PTR(&thread_lock_acquire_obj) },
     { MP_ROM_QSTR(MP_QSTR_release), MP_ROM_PTR(&thread_lock_release_obj) },
     { MP_ROM_QSTR(MP_QSTR_locked), MP_ROM_PTR(&thread_lock_locked_obj) },
@@ -114,9 +113,9 @@ STATIC const mp_rom_map_elem_t thread_lock_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&thread_lock___exit___obj) },
 };
 
-STATIC MP_DEFINE_CONST_DICT(thread_lock_locals_dict, thread_lock_locals_dict_table);
+static MP_DEFINE_CONST_DICT(thread_lock_locals_dict, thread_lock_locals_dict_table);
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+static MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_thread_lock,
     MP_QSTR_lock,
     MP_TYPE_FLAG_NONE,
@@ -126,14 +125,14 @@ STATIC MP_DEFINE_CONST_OBJ_TYPE(
 /****************************************************************/
 // _thread module
 
-STATIC size_t thread_stack_size = 0;
+static size_t thread_stack_size = 0;
 
-STATIC mp_obj_t mod_thread_get_ident(void) {
-    return mp_obj_new_int_from_uint((uintptr_t)mp_thread_get_state());
+static mp_obj_t mod_thread_get_ident(void) {
+    return mp_obj_new_int_from_uint(mp_thread_get_id());
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_thread_get_ident_obj, mod_thread_get_ident);
+static MP_DEFINE_CONST_FUN_OBJ_0(mod_thread_get_ident_obj, mod_thread_get_ident);
 
-STATIC mp_obj_t mod_thread_stack_size(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mod_thread_stack_size(size_t n_args, const mp_obj_t *args) {
     mp_obj_t ret = mp_obj_new_int_from_uint(thread_stack_size);
     if (n_args == 0) {
         thread_stack_size = 0;
@@ -142,7 +141,7 @@ STATIC mp_obj_t mod_thread_stack_size(size_t n_args, const mp_obj_t *args) {
     }
     return ret;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_thread_stack_size_obj, 0, 1, mod_thread_stack_size);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_thread_stack_size_obj, 0, 1, mod_thread_stack_size);
 
 typedef struct _thread_entry_args_t {
     mp_obj_dict_t *dict_locals;
@@ -154,16 +153,13 @@ typedef struct _thread_entry_args_t {
     mp_obj_t args[];
 } thread_entry_args_t;
 
-STATIC void *thread_entry(void *args_in) {
+static void *thread_entry(void *args_in) {
     // Execution begins here for a new thread.  We do not have the GIL.
 
     thread_entry_args_t *args = (thread_entry_args_t *)args_in;
 
     mp_state_thread_t ts;
-    mp_thread_set_state(&ts);
-
-    mp_stack_set_top(&ts + 1); // need to include ts in root-pointer scan
-    mp_stack_set_limit(args->stack_size);
+    mp_thread_init_state(&ts, args->stack_size, args->dict_locals, args->dict_globals);
 
     #if MICROPY_ENABLE_PYSTACK
     // TODO threading and pystack is not fully supported, for now just make a small stack
@@ -171,15 +167,6 @@ STATIC void *thread_entry(void *args_in) {
     mp_pystack_init(mini_pystack, &mini_pystack[128]);
     #endif
 
-    // The GC starts off unlocked on this thread.
-    ts.gc_lock_depth = 0;
-
-    ts.mp_pending_exception = MP_OBJ_NULL;
-
-    // set locals and globals from the calling context
-    mp_locals_set(args->dict_locals);
-    mp_globals_set(args->dict_globals);
-
     MP_THREAD_GIL_ENTER();
 
     // signal that we are set up and running
@@ -219,7 +206,7 @@ STATIC void *thread_entry(void *args_in) {
     return NULL;
 }
 
-STATIC mp_obj_t mod_thread_start_new_thread(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mod_thread_start_new_thread(size_t n_args, const mp_obj_t *args) {
     // This structure holds the Python function and arguments for thread entry.
     // We copy all arguments into this structure to keep ownership of them.
     // We must be very careful about root pointers because this pointer may
@@ -234,7 +221,7 @@ STATIC mp_obj_t mod_thread_start_new_thread(size_t n_args, const mp_obj_t *args)
     // check for keyword arguments
     if (n_args == 2) {
         // just position arguments
-        th_args = m_new_obj_var(thread_entry_args_t, mp_obj_t, pos_args_len);
+        th_args = m_new_obj_var(thread_entry_args_t, args, mp_obj_t, pos_args_len);
         th_args->n_kw = 0;
     } else {
         // positional and keyword arguments
@@ -242,7 +229,7 @@ STATIC mp_obj_t mod_thread_start_new_thread(size_t n_args, const mp_obj_t *args)
             mp_raise_TypeError(MP_ERROR_TEXT("expecting a dict for keyword args"));
         }
         mp_map_t *map = &((mp_obj_dict_t *)MP_OBJ_TO_PTR(args[2]))->map;
-        th_args = m_new_obj_var(thread_entry_args_t, mp_obj_t, pos_args_len + 2 * map->used);
+        th_args = m_new_obj_var(thread_entry_args_t, args, mp_obj_t, pos_args_len + 2 * map->used);
         th_args->n_kw = map->used;
         // copy across the keyword arguments
         for (size_t i = 0, n = pos_args_len; i < map->alloc; ++i) {
@@ -268,23 +255,21 @@ STATIC mp_obj_t mod_thread_start_new_thread(size_t n_args, const mp_obj_t *args)
     th_args->fun = args[0];
 
     // spawn the thread!
-    mp_thread_create(thread_entry, th_args, &th_args->stack_size);
-
-    return mp_const_none;
+    return mp_obj_new_int_from_uint(mp_thread_create(thread_entry, th_args, &th_args->stack_size));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_thread_start_new_thread_obj, 2, 3, mod_thread_start_new_thread);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_thread_start_new_thread_obj, 2, 3, mod_thread_start_new_thread);
 
-STATIC mp_obj_t mod_thread_exit(void) {
+static mp_obj_t mod_thread_exit(void) {
     mp_raise_type(&mp_type_SystemExit);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_thread_exit_obj, mod_thread_exit);
+static MP_DEFINE_CONST_FUN_OBJ_0(mod_thread_exit_obj, mod_thread_exit);
 
-STATIC mp_obj_t mod_thread_allocate_lock(void) {
+static mp_obj_t mod_thread_allocate_lock(void) {
     return MP_OBJ_FROM_PTR(mp_obj_new_thread_lock());
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_thread_allocate_lock_obj, mod_thread_allocate_lock);
+static MP_DEFINE_CONST_FUN_OBJ_0(mod_thread_allocate_lock_obj, mod_thread_allocate_lock);
 
-STATIC const mp_rom_map_elem_t mp_module_thread_globals_table[] = {
+static const mp_rom_map_elem_t mp_module_thread_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR__thread) },
     { MP_ROM_QSTR(MP_QSTR_LockType), MP_ROM_PTR(&mp_type_thread_lock) },
     { MP_ROM_QSTR(MP_QSTR_get_ident), MP_ROM_PTR(&mod_thread_get_ident_obj) },
@@ -294,7 +279,7 @@ STATIC const mp_rom_map_elem_t mp_module_thread_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR_allocate_lock), MP_ROM_PTR(&mod_thread_allocate_lock_obj) },
 };
 
-STATIC MP_DEFINE_CONST_DICT(mp_module_thread_globals, mp_module_thread_globals_table);
+static MP_DEFINE_CONST_DICT(mp_module_thread_globals, mp_module_thread_globals_table);
 
 const mp_obj_module_t mp_module_thread = {
     .base = { &mp_type_module },
diff --git a/py/moduerrno.c b/py/moduerrno.c
deleted file mode 100644
index 1b16fd9d9ed..00000000000
--- a/py/moduerrno.c
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * This file is part of the MicroPython project, http://micropython.org/
- *
- * The MIT License (MIT)
- *
- * Copyright (c) 2016 Damien P. George
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-#include 
-#include 
-
-#include "py/obj.h"
-#include "py/mperrno.h"
-
-#if MICROPY_PY_UERRNO
-
-// This list can be defined per port in mpconfigport.h to tailor it to a
-// specific port's needs.  If it's not defined then we provide a default.
-#ifndef MICROPY_PY_UERRNO_LIST
-#define MICROPY_PY_UERRNO_LIST \
-    X(EPERM) \
-    X(ENOENT) \
-    X(EIO) \
-    X(EBADF) \
-    X(EAGAIN) \
-    X(ENOMEM) \
-    X(EACCES) \
-    X(EEXIST) \
-    X(ENODEV) \
-    X(EISDIR) \
-    X(EINVAL) \
-    X(EOPNOTSUPP) \
-    X(EADDRINUSE) \
-    X(ECONNABORTED) \
-    X(ECONNRESET) \
-    X(ENOBUFS) \
-    X(ENOTCONN) \
-    X(ETIMEDOUT) \
-    X(ECONNREFUSED) \
-    X(EHOSTUNREACH) \
-    X(EALREADY) \
-    X(EINPROGRESS) \
-
-#endif
-
-#if MICROPY_PY_UERRNO_ERRORCODE
-STATIC const mp_rom_map_elem_t errorcode_table[] = {
-    #define X(e) { MP_ROM_INT(MP_##e), MP_ROM_QSTR(MP_QSTR_##e) },
-    MICROPY_PY_UERRNO_LIST
-#undef X
-};
-
-STATIC const mp_obj_dict_t errorcode_dict = {
-    .base = {&mp_type_dict},
-    .map = {
-        .all_keys_are_qstrs = 0, // keys are integers
-        .is_fixed = 1,
-        .is_ordered = 1,
-        .used = MP_ARRAY_SIZE(errorcode_table),
-        .alloc = MP_ARRAY_SIZE(errorcode_table),
-        .table = (mp_map_elem_t *)(mp_rom_map_elem_t *)errorcode_table,
-    },
-};
-#endif
-
-STATIC const mp_rom_map_elem_t mp_module_uerrno_globals_table[] = {
-    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_uerrno) },
-    #if MICROPY_PY_UERRNO_ERRORCODE
-    { MP_ROM_QSTR(MP_QSTR_errorcode), MP_ROM_PTR(&errorcode_dict) },
-    #endif
-
-    #define X(e) { MP_ROM_QSTR(MP_QSTR_##e), MP_ROM_INT(MP_##e) },
-    MICROPY_PY_UERRNO_LIST
-#undef X
-};
-
-STATIC MP_DEFINE_CONST_DICT(mp_module_uerrno_globals, mp_module_uerrno_globals_table);
-
-const mp_obj_module_t mp_module_uerrno = {
-    .base = { &mp_type_module },
-    .globals = (mp_obj_dict_t *)&mp_module_uerrno_globals,
-};
-
-MP_REGISTER_MODULE(MP_QSTR_uerrno, mp_module_uerrno);
-
-qstr mp_errno_to_str(mp_obj_t errno_val) {
-    #if MICROPY_PY_UERRNO_ERRORCODE
-    // We have the errorcode dict so can do a lookup using the hash map
-    mp_map_elem_t *elem = mp_map_lookup((mp_map_t *)&errorcode_dict.map, errno_val, MP_MAP_LOOKUP);
-    if (elem == NULL) {
-        return MP_QSTRnull;
-    } else {
-        return MP_OBJ_QSTR_VALUE(elem->value);
-    }
-    #else
-    // We don't have the errorcode dict so do a simple search in the modules dict
-    for (size_t i = 0; i < MP_ARRAY_SIZE(mp_module_uerrno_globals_table); ++i) {
-        if (errno_val == mp_module_uerrno_globals_table[i].value) {
-            return MP_OBJ_QSTR_VALUE(mp_module_uerrno_globals_table[i].key);
-        }
-    }
-    return MP_QSTRnull;
-    #endif
-}
-
-#endif // MICROPY_PY_UERRNO
diff --git a/py/modweakref.c b/py/modweakref.c
new file mode 100644
index 00000000000..3360a4e2095
--- /dev/null
+++ b/py/modweakref.c
@@ -0,0 +1,314 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2026 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "py/gc.h"
+#include "py/runtime.h"
+
+#if MICROPY_PY_WEAKREF
+
+// Macros to obfuscate a heap pointer as a small integer object.
+#define PTR_TO_INT_OBJ(ptr) (MP_OBJ_NEW_SMALL_INT(((uintptr_t)ptr) >> 1))
+#define PTR_FROM_INT_OBJ(obj) ((void *)(MP_OBJ_SMALL_INT_VALUE((obj)) << 1))
+
+// Macros to convert between a weak reference and a heap pointer.
+#define WEAK_REFERENCE_FROM_HEAP_PTR(ptr) PTR_TO_INT_OBJ(ptr)
+#define WEAK_REFERENCE_TO_HEAP_PTR(weak_ref) PTR_FROM_INT_OBJ(weak_ref)
+
+// Macros to manage ref-finalizer linked-list pointers.
+// - mp_obj_ref_t is obfuscated as a small integer object so it's not traced by the GC.
+// - mp_obj_finalize_t is stored as-is so it is traced by the GC.
+#define REF_FIN_LIST_OBJ_IS_FIN(r) (!mp_obj_is_small_int((r)))
+#define REF_FIN_LIST_OBJ_TO_PTR(r) ((mp_obj_ref_t *)(mp_obj_is_small_int((r)) ? PTR_FROM_INT_OBJ((r)) : MP_OBJ_TO_PTR((r))))
+#define REF_FIN_LIST_OBJ_FROM_REF(r) (PTR_TO_INT_OBJ((r)))
+#define REF_FIN_LIST_OBJ_FROM_FIN(r) (MP_OBJ_FROM_PTR((r)))
+#define REF_FIN_LIST_OBJ_TAIL (PTR_TO_INT_OBJ(NULL))
+
+// weakref.ref() instance.
+typedef struct _mp_obj_ref_t {
+    mp_obj_base_t base;
+    mp_obj_t ref_fin_next;
+    mp_obj_t obj_weak_ref;
+    mp_obj_t callback;
+} mp_obj_ref_t;
+
+// weakref.finalize() instance.
+// This is an extension of weakref.ref() and shares a lot of code with it.
+typedef struct _mp_obj_finalize_t {
+    mp_obj_ref_t base;
+    size_t n_args;
+    size_t n_kw;
+    mp_obj_t *args;
+} mp_obj_finalize_t;
+
+static const mp_obj_type_t mp_type_ref;
+static const mp_obj_type_t mp_type_finalize;
+
+static mp_obj_t ref___del__(mp_obj_t self_in);
+
+void gc_weakref_about_to_be_freed(void *ptr) {
+    mp_obj_t idx = WEAK_REFERENCE_FROM_HEAP_PTR(ptr);
+    mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_weakref_map), idx, MP_MAP_LOOKUP);
+    if (elem != NULL) {
+        // Mark element as being freed.
+        elem->key = mp_const_none;
+    }
+}
+
+void gc_weakref_sweep(void) {
+    mp_map_t *map = &MP_STATE_VM(mp_weakref_map);
+    for (size_t i = 0; i < map->alloc; i++) {
+        if (map->table[i].key == mp_const_none) {
+            // Element was just freed, so call all the registered callbacks.
+            --map->used;
+            map->table[i].key = MP_OBJ_SENTINEL;
+            mp_obj_ref_t *ref = REF_FIN_LIST_OBJ_TO_PTR(map->table[i].value);
+            map->table[i].value = MP_OBJ_NULL;
+            while (ref != NULL) {
+                // Invalidate the weak reference.
+                assert(ref->obj_weak_ref != mp_const_none);
+                ref->obj_weak_ref = mp_const_none;
+
+                // Call any registered callbacks.
+                if (ref->callback != mp_const_none) {
+                    nlr_buf_t nlr;
+                    if (nlr_push(&nlr) == 0) {
+                        if (ref->base.type == &mp_type_ref) {
+                            // weakref.ref() type.
+                            mp_call_function_1(ref->callback, MP_OBJ_FROM_PTR(ref));
+                        } else {
+                            // weakref.finalize() type.
+                            mp_obj_finalize_t *fin = (mp_obj_finalize_t *)ref;
+                            mp_call_function_n_kw(fin->base.callback, fin->n_args, fin->n_kw, fin->args);
+                        }
+                        nlr_pop();
+                    } else {
+                        mp_printf(MICROPY_ERROR_PRINTER, "Unhandled exception in weakref callback:\n");
+                        mp_obj_print_exception(MICROPY_ERROR_PRINTER, MP_OBJ_FROM_PTR(nlr.ret_val));
+                    }
+                }
+
+                // Unlink the node.
+                mp_obj_ref_t *ref_fin_next = REF_FIN_LIST_OBJ_TO_PTR(ref->ref_fin_next);
+                ref->ref_fin_next = REF_FIN_LIST_OBJ_TAIL;
+                ref = ref_fin_next;
+            }
+        }
+    }
+}
+
+static mp_obj_t mp_obj_ref_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+    if (type == &mp_type_ref) {
+        // weakref.ref() type.
+        mp_arg_check_num(n_args, n_kw, 1, 2, false);
+    } else {
+        // weakref.finalize() type.
+        mp_arg_check_num(n_args, n_kw, 2, MP_OBJ_FUN_ARGS_MAX, true);
+    }
+
+    // Validate the input object can have a weakref.
+    void *ptr = NULL;
+    if (mp_obj_is_obj(args[0])) {
+        ptr = MP_OBJ_TO_PTR(args[0]);
+        if (gc_nbytes(ptr) == 0) {
+            ptr = NULL;
+        }
+    }
+    if (ptr == NULL) {
+        mp_raise_TypeError(MP_ERROR_TEXT("not a heap object"));
+    }
+
+    // Create or get the entry in mp_weakref_map corresponding to this object.
+    mp_obj_t obj_weak_reference = WEAK_REFERENCE_FROM_HEAP_PTR(ptr);
+    mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_weakref_map), obj_weak_reference, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND);
+    if (elem->value == MP_OBJ_NULL) {
+        // This heap object does not have any existing weakref's, so initialise it.
+        elem->value = REF_FIN_LIST_OBJ_TAIL;
+        gc_weakref_mark(ptr);
+    }
+
+    mp_obj_ref_t *self;
+    if (type == &mp_type_ref) {
+        // Create a new weakref.ref() object.
+        self = mp_obj_malloc_with_finaliser(mp_obj_ref_t, type);
+        // Link this new ref into the list of all refs/finalizers pointing to this object.
+        // To ensure it will *NOT* be traced by the GC (the user must manually hold onto it),
+        // store an integer version of the object after any weakref.finalize() objects (so
+        // the weakref.finalize() objects continue to be traced by the GC).
+        mp_obj_t *link = &elem->value;
+        while (REF_FIN_LIST_OBJ_IS_FIN(*link)) {
+            link = &REF_FIN_LIST_OBJ_TO_PTR(*link)->ref_fin_next;
+        }
+        self->ref_fin_next = *link;
+        *link = REF_FIN_LIST_OBJ_FROM_REF(self);
+    } else {
+        // Create a new weakref.finalize() object.
+        mp_obj_finalize_t *self_fin = mp_obj_malloc(mp_obj_finalize_t, type);
+        self_fin->n_args = n_args - 2;
+        self_fin->n_kw = n_kw;
+        size_t n_args_kw = self_fin->n_args + self_fin->n_kw * 2;
+        if (n_args_kw == 0) {
+            self_fin->args = NULL;
+        } else {
+            self_fin->args = m_new(mp_obj_t, n_args_kw);
+            memcpy(self_fin->args, args + 2, n_args_kw * sizeof(mp_obj_t));
+        }
+        self = &self_fin->base;
+        // Link this new finalizer into the list of all refs/finalizers pointing to this object.
+        // To ensure it will be traced by the GC, store its pointer at the start of the list.
+        self->ref_fin_next = elem->value;
+        elem->value = REF_FIN_LIST_OBJ_FROM_FIN(self_fin);
+    }
+
+    // Populate the object weak reference, and the callback.
+    self->obj_weak_ref = obj_weak_reference;
+    if (n_args > 1) {
+        self->callback = args[1];
+    } else {
+        self->callback = mp_const_none;
+    }
+
+    return MP_OBJ_FROM_PTR(self);
+}
+
+static mp_obj_t mp_obj_ref_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+    mp_obj_ref_t *self = MP_OBJ_TO_PTR(self_in);
+    if (self->obj_weak_ref == mp_const_none) {
+        return mp_const_none;
+    }
+    if (self->base.type == &mp_type_ref) {
+        // weakref.ref() type.
+        return MP_OBJ_FROM_PTR(WEAK_REFERENCE_TO_HEAP_PTR(self->obj_weak_ref));
+    } else {
+        // weakref.finalize() type.
+        mp_obj_finalize_t *self_fin = MP_OBJ_TO_PTR(self_in);
+        ref___del__(self_in);
+        return mp_call_function_n_kw(self_fin->base.callback, self_fin->n_args, self_fin->n_kw, self_fin->args);
+    }
+}
+
+static mp_obj_t ref___del__(mp_obj_t self_in) {
+    mp_obj_ref_t *self = MP_OBJ_TO_PTR(self_in);
+    mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_weakref_map), self->obj_weak_ref, MP_MAP_LOOKUP);
+    if (elem != NULL) {
+        for (mp_obj_t *link = &elem->value; REF_FIN_LIST_OBJ_TO_PTR(*link) != NULL; link = &REF_FIN_LIST_OBJ_TO_PTR(*link)->ref_fin_next) {
+            if (self == REF_FIN_LIST_OBJ_TO_PTR(*link)) {
+                // Unlink and clear this node.
+                *link = self->ref_fin_next;
+                self->ref_fin_next = REF_FIN_LIST_OBJ_TAIL;
+                self->obj_weak_ref = mp_const_none;
+                break;
+            }
+        }
+    }
+    return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(ref___del___obj, ref___del__);
+
+static mp_obj_t finalize_peek_detach_helper(mp_obj_t self_in, bool detach) {
+    mp_obj_finalize_t *self = MP_OBJ_TO_PTR(self_in);
+    if (self->base.obj_weak_ref == mp_const_none) {
+        return mp_const_none;
+    }
+    mp_obj_t tuple[4] = {
+        MP_OBJ_FROM_PTR(WEAK_REFERENCE_TO_HEAP_PTR(self->base.obj_weak_ref)),
+        self->base.callback,
+        mp_obj_new_tuple(self->n_args, self->args),
+        mp_obj_dict_make_new(&mp_type_dict, 0, self->n_kw, self->args + self->n_args),
+    };
+    if (detach) {
+        ref___del__(self_in);
+    }
+    return mp_obj_new_tuple(MP_ARRAY_SIZE(tuple), tuple);
+}
+
+static mp_obj_t finalize_peek(mp_obj_t self_in) {
+    return finalize_peek_detach_helper(self_in, false);
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(finalize_peek_obj, finalize_peek);
+
+static mp_obj_t finalize_detach(mp_obj_t self_in) {
+    return finalize_peek_detach_helper(self_in, true);
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(finalize_detach_obj, finalize_detach);
+
+static void mp_obj_finalize_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    if (dest[0] != MP_OBJ_NULL) {
+        // Store/delete attribute, unsupported.
+        return;
+    }
+
+    if (attr == MP_QSTR_alive) {
+        mp_obj_finalize_t *self = MP_OBJ_TO_PTR(self_in);
+        dest[0] = mp_obj_new_bool(self->base.obj_weak_ref != mp_const_none);
+        return;
+    } else if (attr == MP_QSTR_peek) {
+        dest[0] = MP_OBJ_FROM_PTR(&finalize_peek_obj);
+        dest[1] = self_in;
+    } else if (attr == MP_QSTR_detach) {
+        dest[0] = MP_OBJ_FROM_PTR(&finalize_detach_obj);
+        dest[1] = self_in;
+    }
+}
+
+static const mp_rom_map_elem_t ref_locals_dict_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&ref___del___obj) },
+};
+static MP_DEFINE_CONST_DICT(ref_locals_dict, ref_locals_dict_table);
+
+static MP_DEFINE_CONST_OBJ_TYPE(
+    mp_type_ref,
+    MP_QSTR_ref,
+    MP_TYPE_FLAG_NONE,
+    make_new, mp_obj_ref_make_new,
+    call, mp_obj_ref_call,
+    locals_dict, &ref_locals_dict
+    );
+
+static MP_DEFINE_CONST_OBJ_TYPE(
+    mp_type_finalize,
+    MP_QSTR_finalize,
+    MP_TYPE_FLAG_NONE,
+    make_new, mp_obj_ref_make_new,
+    call, mp_obj_ref_call,
+    attr, mp_obj_finalize_attr
+    );
+
+static const mp_rom_map_elem_t mp_module_weakref_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_weakref) },
+    { MP_ROM_QSTR(MP_QSTR_ref), MP_ROM_PTR(&mp_type_ref) },
+    { MP_ROM_QSTR(MP_QSTR_finalize), MP_ROM_PTR(&mp_type_finalize) },
+};
+static MP_DEFINE_CONST_DICT(mp_module_weakref_globals, mp_module_weakref_globals_table);
+
+const mp_obj_module_t mp_module_weakref = {
+    .base = { &mp_type_module },
+    .globals = (mp_obj_dict_t *)&mp_module_weakref_globals,
+};
+
+MP_REGISTER_ROOT_POINTER(mp_map_t mp_weakref_map);
+MP_REGISTER_MODULE(MP_QSTR_weakref, mp_module_weakref);
+
+#endif // MICROPY_PY_WEAKREF
diff --git a/py/mpconfig.h b/py/mpconfig.h
index afef744ab5b..1edc0804eff 100644
--- a/py/mpconfig.h
+++ b/py/mpconfig.h
@@ -26,22 +26,47 @@
 #ifndef MICROPY_INCLUDED_PY_MPCONFIG_H
 #define MICROPY_INCLUDED_PY_MPCONFIG_H
 
-// Current version of MicroPython
-#define MICROPY_VERSION_MAJOR 1
-#define MICROPY_VERSION_MINOR 20
-#define MICROPY_VERSION_MICRO 0
+#include 
 
-// Combined version as a 32-bit number for convenience
-#define MICROPY_VERSION ( \
-    MICROPY_VERSION_MAJOR << 16 \
-        | MICROPY_VERSION_MINOR << 8 \
-        | MICROPY_VERSION_MICRO)
+#if defined(__cplusplus) // Required on at least one compiler to get ULLONG_MAX
+#include 
+#else
+#include 
+#endif
 
-// String version
-#define MICROPY_VERSION_STRING \
+
+// Current version of MicroPython. This is used by sys.implementation.version
+// as well as a fallback to generate MICROPY_GIT_TAG if the git repo or tags
+// are unavailable.
+#define MICROPY_VERSION_MAJOR 1
+#define MICROPY_VERSION_MINOR 30
+#define MICROPY_VERSION_MICRO 0
+#define MICROPY_VERSION_PRERELEASE 1
+
+// Combined version as a 32-bit number for convenience to allow version
+// comparison. Doesn't include prerelease state.
+// e.g. #if MICROPY_VERSION < MICROPY_MAKE_VERSION(1, 22, 0)
+#define MICROPY_MAKE_VERSION(major, minor, patch) (major << 16 | minor << 8 | patch)
+#define MICROPY_VERSION MICROPY_MAKE_VERSION(MICROPY_VERSION_MAJOR, MICROPY_VERSION_MINOR, MICROPY_VERSION_MICRO)
+
+// String version. This is only used directly for platform.platform and
+// os.uname().release. All other version info available in the firmware (e.g.
+// the REPL banner) comes from MICROPY_GIT_TAG.
+#define MICROPY_VERSION_STRING_BASE \
     MP_STRINGIFY(MICROPY_VERSION_MAJOR) "." \
     MP_STRINGIFY(MICROPY_VERSION_MINOR) "." \
     MP_STRINGIFY(MICROPY_VERSION_MICRO)
+#if MICROPY_VERSION_PRERELEASE
+#define MICROPY_VERSION_STRING MICROPY_VERSION_STRING_BASE "-preview"
+#else
+#define MICROPY_VERSION_STRING MICROPY_VERSION_STRING_BASE
+#endif
+
+// If this is enabled, then in-progress/breaking changes slated for the 2.x
+// release will be enabled.
+#ifndef MICROPY_PREVIEW_VERSION_2
+#define MICROPY_PREVIEW_VERSION_2 (0)
+#endif
 
 // This file contains default configuration settings for MicroPython.
 // You can override any of the options below using mpconfigport.h file
@@ -145,6 +170,78 @@
 #define MICROPY_OBJ_IMMEDIATE_OBJS (MICROPY_OBJ_REPR != MICROPY_OBJ_REPR_D)
 #endif
 
+// Definition of the `mp_int_t` and `mp_uint_t` types and associated macros.
+// Normally, it suffices for the platform to do nothing: A type as wide
+// as a pointer is chosen, unless nanboxing (REPR_D) is selected, in
+// which case a 64-bit type is chosen to match the assumed size of
+// double-precision floats.
+//
+// In the case of exceptions, the port, board, or variant must define
+// MP_INT_TYPE as MP_INT_TYPE_OTHER and provide all the typedefs and
+// defines.
+#define MP_INT_TYPE_INTPTR (0)
+#define MP_INT_TYPE_INT64 (1)
+#define MP_INT_TYPE_OTHER (2)
+
+#if !defined(MP_INT_TYPE)
+#if MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_D
+#define MP_INT_TYPE (MP_INT_TYPE_INT64)
+#else
+#define MP_INT_TYPE (MP_INT_TYPE_INTPTR)
+#endif
+#endif
+
+#if MP_INT_TYPE == MP_INT_TYPE_INTPTR
+typedef intptr_t mp_int_t;
+typedef uintptr_t mp_uint_t;
+#define MP_INT_MAX INTPTR_MAX
+#define MP_INT_MIN INTPTR_MIN
+#define MP_UINT_MAX INTPTR_UMAX
+#elif MP_INT_TYPE == MP_INT_TYPE_INT64
+typedef int64_t mp_int_t;
+typedef uint64_t mp_uint_t;
+#define MP_INT_MAX INT64_MAX
+#define MP_INT_MIN INT64_MIN
+#define MP_UINT_MAX INT64_UMAX
+#endif
+
+// mp_printf format support for mp_int_t. In the unusual case that MP_INT_MAX doesn't
+// match any of the standard C types (int/long/long long), provide all 3
+// macros. Otherwise, rely on these automatic definitions.
+#if !defined(INT_FMT)
+#if MP_INT_MAX == INT_MAX
+#define INT_FMT "%d"
+#define UINT_FMT "%u"
+#define HEX_FMT "%x"
+#elif MP_INT_MAX == LONG_MAX
+#define INT_FMT "%ld"
+#define UINT_FMT "%lu"
+#define HEX_FMT "%lx"
+#elif MP_INT_MAX == LLONG_MAX
+#define INT_FMT "%lld"
+#define UINT_FMT "%llu"
+#define HEX_FMT "%llx"
+#else
+#error Unexpected MP_INT_MAX value
+#endif
+#endif
+
+// mp_printf format support for size_t. In the unusual case that SIZE_MAX doesn't
+// match any of the standard C types (int/long/long long), provide a
+// macro. Otherwise, rely on these automatic definitions.
+#if !defined(SIZE_FMT)
+#if SIZE_MAX == UINT_MAX
+#define SIZE_FMT "%u"
+#elif SIZE_MAX == ULONG_MAX
+#define SIZE_FMT "%lu"
+#elif SIZE_MAX == ULLONG_MAX
+#define SIZE_FMT "%llu"
+#else
+#error Unexpected SIZE_MAX value
+#endif
+#endif
+
+
 /*****************************************************************************/
 /* Memory allocation policy                                                  */
 
@@ -274,10 +371,12 @@
 
 // Number of bytes used to store qstr hash
 #ifndef MICROPY_QSTR_BYTES_IN_HASH
-#if MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES
+#if MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES
 #define MICROPY_QSTR_BYTES_IN_HASH (2)
-#else
+#elif MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES
 #define MICROPY_QSTR_BYTES_IN_HASH (1)
+#else
+#define MICROPY_QSTR_BYTES_IN_HASH (0)
 #endif
 #endif
 
@@ -312,9 +411,16 @@
 #define MICROPY_PERSISTENT_CODE_LOAD (0)
 #endif
 
-// Whether to support saving of persistent code
+// Whether to support loading of persistent native code
+#ifndef MICROPY_PERSISTENT_CODE_LOAD_NATIVE
+#define MICROPY_PERSISTENT_CODE_LOAD_NATIVE (MICROPY_EMIT_MACHINE_CODE)
+#endif
+
+// Whether to support saving of persistent code, i.e. for mpy-cross to
+// generate .mpy files. Enabling this enables additional metadata on raw code
+// objects which is also required for sys.settrace.
 #ifndef MICROPY_PERSISTENT_CODE_SAVE
-#define MICROPY_PERSISTENT_CODE_SAVE (0)
+#define MICROPY_PERSISTENT_CODE_SAVE (MICROPY_PY_SYS_SETTRACE)
 #endif
 
 // Whether to support saving persistent code to a file via mp_raw_code_save_file
@@ -322,10 +428,15 @@
 #define MICROPY_PERSISTENT_CODE_SAVE_FILE (0)
 #endif
 
+// Whether to support converting functions to persistent code (bytes)
+#ifndef MICROPY_PERSISTENT_CODE_SAVE_FUN
+#define MICROPY_PERSISTENT_CODE_SAVE_FUN (MICROPY_PY_MARSHAL)
+#endif
+
 // Whether generated code can persist independently of the VM/runtime instance
 // This is enabled automatically when needed by other features
 #ifndef MICROPY_PERSISTENT_CODE
-#define MICROPY_PERSISTENT_CODE (MICROPY_PERSISTENT_CODE_LOAD || MICROPY_PERSISTENT_CODE_SAVE || MICROPY_MODULE_FROZEN_MPY)
+#define MICROPY_PERSISTENT_CODE (MICROPY_PERSISTENT_CODE_LOAD || MICROPY_PERSISTENT_CODE_LOAD_NATIVE || MICROPY_PERSISTENT_CODE_SAVE || MICROPY_MODULE_FROZEN_MPY)
 #endif
 
 // Whether bytecode uses a qstr_table to map internal qstr indices in the bytecode
@@ -353,7 +464,11 @@
 
 // Whether to emit ARMv7-M instruction support in thumb native code
 #ifndef MICROPY_EMIT_THUMB_ARMV7M
+#if defined(__ARM_ARCH_ISA_THUMB) && __ARM_ARCH_ISA_THUMB == 2
 #define MICROPY_EMIT_THUMB_ARMV7M (1)
+#else
+#define MICROPY_EMIT_THUMB_ARMV7M (0)
+#endif
 #endif
 
 // Whether to enable the thumb inline assembler
@@ -363,7 +478,11 @@
 
 // Whether to enable float support in the Thumb2 inline assembler
 #ifndef MICROPY_EMIT_INLINE_THUMB_FLOAT
+#if defined(__ARM_ARCH_ISA_THUMB) && __ARM_ARCH_ISA_THUMB == 2 && defined(__ARM_FP)
 #define MICROPY_EMIT_INLINE_THUMB_FLOAT (1)
+#else
+#define MICROPY_EMIT_INLINE_THUMB_FLOAT (0)
+#endif
 #endif
 
 // Whether to emit ARM native code
@@ -381,13 +500,43 @@
 #define MICROPY_EMIT_INLINE_XTENSA (0)
 #endif
 
+// Whether to support uncommon Xtensa inline assembler opcodes
+#ifndef MICROPY_EMIT_INLINE_XTENSA_UNCOMMON_OPCODES
+#define MICROPY_EMIT_INLINE_XTENSA_UNCOMMON_OPCODES (0)
+#endif
+
 // Whether to emit Xtensa-Windowed native code
 #ifndef MICROPY_EMIT_XTENSAWIN
 #define MICROPY_EMIT_XTENSAWIN (0)
 #endif
 
+// Whether to emit RISC-V RV32 native code
+#ifndef MICROPY_EMIT_RV32
+#define MICROPY_EMIT_RV32 (0)
+#endif
+
+// Whether to emit RISC-V RV32 Zba opcodes in native code
+#ifndef MICROPY_EMIT_RV32_ZBA
+#define MICROPY_EMIT_RV32_ZBA (0)
+#endif
+
+// Whether to emit RISC-V RV32 Zcmp opcodes in native code
+#ifndef MICROPY_EMIT_RV32_ZCMP
+#define MICROPY_EMIT_RV32_ZCMP (0)
+#endif
+
+// Whether to enable the RISC-V RV32 inline assembler
+#ifndef MICROPY_EMIT_INLINE_RV32
+#define MICROPY_EMIT_INLINE_RV32 (0)
+#endif
+
+// Whether to enable the human-readable native instructions emitter
+#ifndef MICROPY_EMIT_NATIVE_DEBUG
+#define MICROPY_EMIT_NATIVE_DEBUG (0)
+#endif
+
 // Convenience definition for whether any native emitter is enabled
-#define MICROPY_EMIT_NATIVE (MICROPY_EMIT_X64 || MICROPY_EMIT_X86 || MICROPY_EMIT_THUMB || MICROPY_EMIT_ARM || MICROPY_EMIT_XTENSA || MICROPY_EMIT_XTENSAWIN)
+#define MICROPY_EMIT_NATIVE (MICROPY_EMIT_X64 || MICROPY_EMIT_X86 || MICROPY_EMIT_THUMB || MICROPY_EMIT_ARM || MICROPY_EMIT_XTENSA || MICROPY_EMIT_XTENSAWIN || MICROPY_EMIT_RV32 || MICROPY_EMIT_NATIVE_DEBUG)
 
 // Some architectures cannot read byte-wise from executable memory.  In this case
 // the prelude for a native function (which usually sits after the machine code)
@@ -395,22 +544,14 @@
 #define MICROPY_EMIT_NATIVE_PRELUDE_SEPARATE_FROM_MACHINE_CODE (MICROPY_EMIT_XTENSAWIN)
 
 // Convenience definition for whether any inline assembler emitter is enabled
-#define MICROPY_EMIT_INLINE_ASM (MICROPY_EMIT_INLINE_THUMB || MICROPY_EMIT_INLINE_XTENSA)
+#define MICROPY_EMIT_INLINE_ASM (MICROPY_EMIT_INLINE_THUMB || MICROPY_EMIT_INLINE_XTENSA || MICROPY_EMIT_INLINE_RV32)
 
 // Convenience definition for whether any native or inline assembler emitter is enabled
 #define MICROPY_EMIT_MACHINE_CODE (MICROPY_EMIT_NATIVE || MICROPY_EMIT_INLINE_ASM)
 
-// Whether native relocatable code loaded from .mpy files is explicitly tracked
-// so that the GC cannot reclaim it.  Needed on architectures that allocate
-// executable memory on the MicroPython heap and don't explicitly track this
-// data some other way.
-#ifndef MICROPY_PERSISTENT_CODE_TRACK_RELOC_CODE
-#if !MICROPY_EMIT_MACHINE_CODE || defined(MP_PLAT_ALLOC_EXEC) || defined(MP_PLAT_COMMIT_EXEC)
-#define MICROPY_PERSISTENT_CODE_TRACK_RELOC_CODE (0)
-#else
-#define MICROPY_PERSISTENT_CODE_TRACK_RELOC_CODE (1)
-#endif
-#endif
+// Convenience definition for whether native code has to be dealt with (either
+// generated or loaded from a file).  This does not cover inline asm code.
+#define MICROPY_ENABLE_NATIVE_CODE (MICROPY_EMIT_NATIVE || MICROPY_PERSISTENT_CODE_LOAD_NATIVE)
 
 /*****************************************************************************/
 /* Compiler configuration                                                    */
@@ -426,6 +567,11 @@
 #define MICROPY_DYNAMIC_COMPILER (0)
 #endif
 
+// Whether the compiler allows compiling top-level await expressions
+#ifndef MICROPY_COMP_ALLOW_TOP_LEVEL_AWAIT
+#define MICROPY_COMP_ALLOW_TOP_LEVEL_AWAIT (0)
+#endif
+
 // Whether to enable constant folding; eg 1+2 rewritten as 3
 #ifndef MICROPY_COMP_CONST_FOLDING
 #define MICROPY_COMP_CONST_FOLDING (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
@@ -452,6 +598,13 @@
 #define MICROPY_COMP_CONST (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
 #endif
 
+// Whether to enable float constant folding like 1.2+3.4 (when MICROPY_COMP_CONST_FOLDING is also enabled)
+// and constant optimisation like id = const(1.2) (when MICROPY_COMP_CONST is also enabled)
+// and constant lookup like math.inf (when MICROPY_COMP_MODULE_CONST is also enabled)
+#ifndef MICROPY_COMP_CONST_FLOAT
+#define MICROPY_COMP_CONST_FLOAT (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
+#endif
+
 // Whether to enable optimisation of: a, b = c, d
 // Costs 124 bytes (Thumb2)
 #ifndef MICROPY_COMP_DOUBLE_TUPLE_ASSIGN
@@ -562,6 +715,12 @@
 /*****************************************************************************/
 /* Python internal features                                                  */
 
+// Use a special long jump in nlrthumb.c, which may be necessary if nlr.o and
+// nlrthumb.o are linked far apart from each other.
+#ifndef MICROPY_NLR_THUMB_USE_LONG_JUMP
+#define MICROPY_NLR_THUMB_USE_LONG_JUMP (0)
+#endif
+
 // Whether to enable import of external modules
 // When disabled, only importing of built-in modules is supported
 // When enabled, a port must implement mp_import_stat (among other things)
@@ -616,6 +775,11 @@
 #define MICROPY_GC_SPLIT_HEAP (0)
 #endif
 
+// Whether regions should be added/removed from the split heap as needed.
+#ifndef MICROPY_GC_SPLIT_HEAP_AUTO
+#define MICROPY_GC_SPLIT_HEAP_AUTO (0)
+#endif
+
 // Hook to run code during time consuming garbage collector operations
 // *i* is the loop index variable (e.g. can be used to run every x loops)
 #ifndef MICROPY_GC_HOOK_LOOP
@@ -649,6 +813,20 @@
 #define MICROPY_STACK_CHECK (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
+// Additional margin between the places in the runtime where Python stack is
+// checked and the actual end of the C stack. Needs to be large enough to avoid
+// overflows from function calls made between checks.
+#ifndef MICROPY_STACK_CHECK_MARGIN
+#define MICROPY_STACK_CHECK_MARGIN (0)
+#endif
+
+// The size of a separate stack used for hard IRQ handlers, which should be
+// checked instead of the main stack when running a hard callback. 0 implies
+// there is no separate ISR stack to check.
+#ifndef MICROPY_STACK_SIZE_HARD_IRQ
+#define MICROPY_STACK_SIZE_HARD_IRQ (0)
+#endif
+
 // Whether to have an emergency exception buffer
 #ifndef MICROPY_ENABLE_EMERGENCY_EXCEPTION_BUF
 #define MICROPY_ENABLE_EMERGENCY_EXCEPTION_BUF (0)
@@ -805,6 +983,36 @@ typedef double mp_float_t;
 #define MICROPY_PY_BUILTINS_COMPLEX (MICROPY_PY_BUILTINS_FLOAT)
 #endif
 
+// Float to string conversion implementations
+//
+// Note that the EXACT method is only available if the compiler supports
+// floating points larger than mp_float_t:
+// - with MICROPY_FLOAT_IMPL_FLOAT, the compiler needs to support `double`
+// - with MICROPY_FLOAT_IMPL_DOUBLE, the compiler needs to support `long double`
+//
+#define MICROPY_FLOAT_FORMAT_IMPL_BASIC (0)  // smallest code, but inexact
+#define MICROPY_FLOAT_FORMAT_IMPL_APPROX (1) // slightly bigger, almost perfect
+#define MICROPY_FLOAT_FORMAT_IMPL_EXACT (2)  // bigger code, and 100% exact repr
+
+#ifndef MICROPY_FLOAT_FORMAT_IMPL
+#if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT
+#define MICROPY_FLOAT_FORMAT_IMPL (MICROPY_FLOAT_FORMAT_IMPL_APPROX)
+#elif defined(__SIZEOF_LONG_DOUBLE__) && __SIZEOF_LONG_DOUBLE__ > __SIZEOF_DOUBLE__
+#define MICROPY_FLOAT_FORMAT_IMPL (MICROPY_FLOAT_FORMAT_IMPL_EXACT)
+#else
+#define MICROPY_FLOAT_FORMAT_IMPL (MICROPY_FLOAT_FORMAT_IMPL_APPROX)
+#endif
+#endif
+
+// Whether to use the native _Float16 for 16-bit float support
+#ifndef MICROPY_FLOAT_USE_NATIVE_FLT16
+#ifdef __FLT16_MAX__
+#define MICROPY_FLOAT_USE_NATIVE_FLT16 (1)
+#else
+#define MICROPY_FLOAT_USE_NATIVE_FLT16 (0)
+#endif
+#endif
+
 // Whether to provide a high-quality hash for float and complex numbers.
 // Otherwise the default is a very simple but correct hashing function.
 #ifndef MICROPY_FLOAT_HIGH_QUALITY_HASH
@@ -828,6 +1036,64 @@ typedef double mp_float_t;
 #define MICROPY_FULL_CHECKS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
 #endif
 
+// Ports can choose to use timestamps based on 2000-01-01 or 1970-01-01
+// Default is timestamps based on 2000-01-01
+#if !defined(MICROPY_EPOCH_IS_2000) && !defined(MICROPY_EPOCH_IS_1970)
+#define MICROPY_EPOCH_IS_2000 (1)
+#define MICROPY_EPOCH_IS_1970 (0)
+#elif !defined(MICROPY_EPOCH_IS_1970)
+#define MICROPY_EPOCH_IS_1970 (1 - (MICROPY_EPOCH_IS_2000))
+#elif !defined(MICROPY_EPOCH_IS_2000)
+#define MICROPY_EPOCH_IS_2000 (1 - (MICROPY_EPOCH_IS_1970))
+#endif
+
+// To maintain reasonable compatibility with CPython on embedded systems,
+// and avoid breaking anytime soon, time functions are defined to work
+// at least between 1970 and 2099 (included) on any machine.
+//
+// Specific ports can enable extended date support
+// - after 2099 using MICROPY_TIME_SUPPORT_Y2100_AND_BEYOND
+// - before 1970 using MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE
+// The largest possible range is year 1600 to year 3000
+//
+// By default, extended date support is only enabled for machines using 64 bit pointers,
+// but it can be enabled by specific ports
+#ifndef MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE
+#if MP_SSIZE_MAX > 2147483647
+#define MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE (1)
+#else
+#define MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE (0)
+#endif
+#endif
+
+// When support for dates <1970 is enabled, supporting >=2100 does not cost anything
+#ifndef MICROPY_TIME_SUPPORT_Y2100_AND_BEYOND
+#define MICROPY_TIME_SUPPORT_Y2100_AND_BEYOND (MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE)
+#endif
+
+// The type to be used to represent platform-specific timestamps depends on the choices above
+#define MICROPY_TIMESTAMP_IMPL_LONG_LONG (0)
+#define MICROPY_TIMESTAMP_IMPL_UINT (1)
+#define MICROPY_TIMESTAMP_IMPL_TIME_T (2)
+
+#ifndef MICROPY_TIMESTAMP_IMPL
+#if MICROPY_TIME_SUPPORT_Y2100_AND_BEYOND || MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE || MICROPY_EPOCH_IS_2000
+#define MICROPY_TIMESTAMP_IMPL (MICROPY_TIMESTAMP_IMPL_LONG_LONG)
+#else
+#define MICROPY_TIMESTAMP_IMPL (MICROPY_TIMESTAMP_IMPL_UINT)
+#endif
+#endif
+
+// `mp_timestamp_t` is the type that should be used by the port
+// to represent timestamps, and is referenced to the platform epoch
+#if MICROPY_TIMESTAMP_IMPL == MICROPY_TIMESTAMP_IMPL_LONG_LONG
+typedef long long mp_timestamp_t;
+#elif MICROPY_TIMESTAMP_IMPL == MICROPY_TIMESTAMP_IMPL_UINT
+typedef mp_uint_t mp_timestamp_t;
+#elif MICROPY_TIMESTAMP_IMPL == MICROPY_TIMESTAMP_IMPL_TIME_T
+typedef time_t mp_timestamp_t;
+#endif
+
 // Whether POSIX-semantics non-blocking streams are supported
 #ifndef MICROPY_STREAMS_NON_BLOCK
 #define MICROPY_STREAMS_NON_BLOCK (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
@@ -839,10 +1105,26 @@ typedef double mp_float_t;
 #define MICROPY_STREAMS_POSIX_API (0)
 #endif
 
-// Whether modules can use MP_MODULE_ATTR_DELEGATION_ENTRY() to delegate failed
-// attribute lookups.
+// Whether to delegate error raising to stream implementations using the
+// MP_STREAM_RAISE_ERROR ioctl to support raising more detailed messages.
+#ifndef MICROPY_STREAMS_DELEGATE_ERROR
+#define MICROPY_STREAMS_DELEGATE_ERROR (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#endif
+
+// Whether to process __all__ when importing all public symbols from a module.
+#ifndef MICROPY_MODULE___ALL__
+#define MICROPY_MODULE___ALL__ (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_BASIC_FEATURES)
+#endif
+
+// Whether to set __file__ on imported modules.
+#ifndef MICROPY_MODULE___FILE__
+#define MICROPY_MODULE___FILE__ (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
+#endif
+
+// Whether modules can use MP_REGISTER_MODULE_DELEGATION() to delegate failed
+// attribute lookups to a custom handler function.
 #ifndef MICROPY_MODULE_ATTR_DELEGATION
-#define MICROPY_MODULE_ATTR_DELEGATION (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#define MICROPY_MODULE_ATTR_DELEGATION (MICROPY_PY_SYS_ATTR_DELEGATION || MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
 // Whether to call __init__ when importing builtin modules for the first time.
@@ -863,7 +1145,7 @@ typedef double mp_float_t;
 // have __init__ methods. Instead, the top-level package's __init__ should
 // initialise all sub-packages.
 #ifndef MICROPY_MODULE_BUILTIN_SUBPACKAGES
-#define MICROPY_MODULE_BUILTIN_SUBPACKAGES (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
+#define MICROPY_MODULE_BUILTIN_SUBPACKAGES (MICROPY_PY_TSTRINGS || MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
 #endif
 
 // Whether to support module-level __getattr__ (see PEP 562)
@@ -871,11 +1153,6 @@ typedef double mp_float_t;
 #define MICROPY_MODULE_GETATTR (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
 #endif
 
-// Whether module weak links are supported
-#ifndef MICROPY_MODULE_WEAK_LINKS
-#define MICROPY_MODULE_WEAK_LINKS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
-#endif
-
 // Whether to enable importing foo.py with __name__ set to '__main__'
 // Used by the unix port for the -m flag.
 #ifndef MICROPY_MODULE_OVERRIDE_MAIN_IMPORT
@@ -931,6 +1208,16 @@ typedef double mp_float_t;
 #define MICROPY_ENABLE_VM_ABORT (0)
 #endif
 
+// Whether to handle abort behavior in pyexec code
+#ifndef MICROPY_PYEXEC_ENABLE_VM_ABORT
+#define MICROPY_PYEXEC_ENABLE_VM_ABORT (0)
+#endif
+
+// Whether to set exit codes according to the exit reason (keyboard interrupt, crash, normal exit, ...)
+#ifndef MICROPY_PYEXEC_ENABLE_EXIT_CODE_HANDLING
+#define MICROPY_PYEXEC_ENABLE_EXIT_CODE_HANDLING (0)
+#endif
+
 // Support for internal scheduler
 #ifndef MICROPY_ENABLE_SCHEDULER
 #define MICROPY_ENABLE_SCHEDULER (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
@@ -951,11 +1238,31 @@ typedef double mp_float_t;
 #define MICROPY_VFS (0)
 #endif
 
+// Whether to include support for fast native block devices.
+#ifndef MICROPY_VFS_BLOCKDEV_NATIVE
+#define MICROPY_VFS_BLOCKDEV_NATIVE (0)
+#endif
+
+// Whether to include support for writable filesystems.
+#ifndef MICROPY_VFS_WRITABLE
+#define MICROPY_VFS_WRITABLE (1)
+#endif
+
+// Whether to enable the mp_vfs_rom_ioctl C function, and vfs.rom_ioctl Python function
+#ifndef MICROPY_VFS_ROM_IOCTL
+#define MICROPY_VFS_ROM_IOCTL (MICROPY_VFS_ROM)
+#endif
+
 // Support for VFS POSIX component, to mount a POSIX filesystem within VFS
 #ifndef MICROPY_VFS_POSIX
 #define MICROPY_VFS_POSIX (0)
 #endif
 
+// Whether to include support for writable POSIX filesystems.
+#ifndef MICROPY_VFS_POSIX_WRITABLE
+#define MICROPY_VFS_POSIX_WRITABLE (1)
+#endif
+
 // Support for VFS FAT component, to mount a FAT filesystem within VFS
 #ifndef MICROPY_VFS_FAT
 #define MICROPY_VFS_FAT (0)
@@ -971,6 +1278,11 @@ typedef double mp_float_t;
 #define MICROPY_VFS_LFS2 (0)
 #endif
 
+// Support for ROMFS.
+#ifndef MICROPY_VFS_ROM
+#define MICROPY_VFS_ROM (0)
+#endif
+
 /*****************************************************************************/
 /* Fine control over Python builtins, classes, modules, etc                  */
 
@@ -986,7 +1298,20 @@ typedef double mp_float_t;
 #define MICROPY_PY_FUNCTION_ATTRS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-// Whether to support the descriptors __get__, __set__, __delete__
+// Whether to implement the __code__ attribute on functions, and function constructor
+#ifndef MICROPY_PY_FUNCTION_ATTRS_CODE
+#define MICROPY_PY_FUNCTION_ATTRS_CODE (MICROPY_PY_MARSHAL || MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_FULL_FEATURES)
+#endif
+
+// Whether bound_method can just use == (feature disabled), or requires a call to
+// mp_obj_equal (feature enabled), to test equality of the self and meth entities.
+// This is only needed if objects and functions can be identical without being the
+// same thing, eg when using an object proxy.
+#ifndef MICROPY_PY_BOUND_METHOD_FULL_EQUALITY_CHECK
+#define MICROPY_PY_BOUND_METHOD_FULL_EQUALITY_CHECK (0)
+#endif
+
+// Whether to support the descriptors __get__, __set__, __delete__, __set_name__
 // This costs some code size and makes load/store/delete of instance
 // attributes slower for the classes that use this feature
 #ifndef MICROPY_PY_DESCRIPTORS
@@ -1010,6 +1335,12 @@ typedef double mp_float_t;
 #define MICROPY_PY_FSTRINGS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
+// Support for template strings, t-strings (see PEP 750, Python 3.14+)
+// Requires MICROPY_PY_FSTRINGS to be enabled.
+#ifndef MICROPY_PY_TSTRINGS
+#define MICROPY_PY_TSTRINGS (MICROPY_PY_FSTRINGS && MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_FULL_FEATURES)
+#endif
+
 // Support for assignment expressions with := (see PEP 572, Python 3.8+)
 #ifndef MICROPY_PY_ASSIGN_EXPR
 #define MICROPY_PY_ASSIGN_EXPR (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
@@ -1036,7 +1367,7 @@ typedef double mp_float_t;
 
 // Whether str object is proper unicode
 #ifndef MICROPY_PY_BUILTINS_STR_UNICODE
-#define MICROPY_PY_BUILTINS_STR_UNICODE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#define MICROPY_PY_BUILTINS_STR_UNICODE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_BASIC_FEATURES)
 #endif
 
 // Whether to check for valid UTF-8 when converting bytes to str
@@ -1044,6 +1375,11 @@ typedef double mp_float_t;
 #define MICROPY_PY_BUILTINS_STR_UNICODE_CHECK (MICROPY_PY_BUILTINS_STR_UNICODE)
 #endif
 
+// Whether bytes.decode() supports the 'ignore' and 'replace' error handlers
+#ifndef MICROPY_PY_BUILTINS_BYTES_DECODE_ERRORS
+#define MICROPY_PY_BUILTINS_BYTES_DECODE_ERRORS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#endif
+
 // Whether str.center() method provided
 #ifndef MICROPY_PY_BUILTINS_STR_CENTER
 #define MICROPY_PY_BUILTINS_STR_CENTER (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
@@ -1074,6 +1410,15 @@ typedef double mp_float_t;
 #define MICROPY_PY_BUILTINS_BYTEARRAY (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
 #endif
 
+// Whether to support code objects, and how many features they have
+#define MICROPY_PY_BUILTINS_CODE_NONE       (0)
+#define MICROPY_PY_BUILTINS_CODE_MINIMUM    (1)
+#define MICROPY_PY_BUILTINS_CODE_BASIC      (2)
+#define MICROPY_PY_BUILTINS_CODE_FULL       (3)
+#ifndef MICROPY_PY_BUILTINS_CODE
+#define MICROPY_PY_BUILTINS_CODE            (MICROPY_PY_SYS_SETTRACE ? MICROPY_PY_BUILTINS_CODE_FULL : (MICROPY_PY_FUNCTION_ATTRS_CODE ? MICROPY_PY_BUILTINS_CODE_BASIC : (MICROPY_PY_BUILTINS_COMPILE ? MICROPY_PY_BUILTINS_CODE_MINIMUM : MICROPY_PY_BUILTINS_CODE_NONE)))
+#endif
+
 // Whether to support dict.fromkeys() class method
 #ifndef MICROPY_PY_BUILTINS_DICT_FROMKEYS
 #define MICROPY_PY_BUILTINS_DICT_FROMKEYS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
@@ -1086,7 +1431,7 @@ typedef double mp_float_t;
 
 // Whether to support memoryview.itemsize attribute
 #ifndef MICROPY_PY_BUILTINS_MEMORYVIEW_ITEMSIZE
-#define MICROPY_PY_BUILTINS_MEMORYVIEW_ITEMSIZE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
+#define MICROPY_PY_BUILTINS_MEMORYVIEW_ITEMSIZE (MICROPY_PY_MACHINE_MEM_BACKUP || MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_BASIC_FEATURES)
 #endif
 
 // Whether to support set object
@@ -1134,9 +1479,9 @@ typedef double mp_float_t;
 #define MICROPY_PY_BUILTINS_RANGE_BINOP (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
 #endif
 
-// Support for callling next() with second argument
+// Support for calling next() with second argument
 #ifndef MICROPY_PY_BUILTINS_NEXT2
-#define MICROPY_PY_BUILTINS_NEXT2 (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
+#define MICROPY_PY_BUILTINS_NEXT2 (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_BASIC_FEATURES)
 #endif
 
 // Whether to support rounding of integers (incl bignum); eg round(123,-1)=120
@@ -1144,6 +1489,11 @@ typedef double mp_float_t;
 #define MICROPY_PY_BUILTINS_ROUND_INT (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
+// Whether to implement dir() to enumerate object fields.
+#ifndef MICROPY_PY_BUILTINS_DIR
+#define MICROPY_PY_BUILTINS_DIR (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
+#endif
+
 // Whether to support complete set of special methods for user
 // classes, or only the most used ones. "Inplace" methods are
 // controlled by MICROPY_PY_ALL_INPLACE_SPECIAL_METHODS below.
@@ -1153,13 +1503,13 @@ typedef double mp_float_t;
 #define MICROPY_PY_ALL_SPECIAL_METHODS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-// Whether to support all inplace arithmetic operarion methods
+// Whether to support all inplace arithmetic operation methods
 // (__imul__, etc.)
 #ifndef MICROPY_PY_ALL_INPLACE_SPECIAL_METHODS
 #define MICROPY_PY_ALL_INPLACE_SPECIAL_METHODS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
 #endif
 
-// Whether to support reverse arithmetic operarion methods
+// Whether to support reverse arithmetic operation methods
 // (__radd__, etc.). Additionally gated by
 // MICROPY_PY_ALL_SPECIAL_METHODS.
 #ifndef MICROPY_PY_REVERSE_SPECIAL_METHODS
@@ -1234,9 +1584,14 @@ typedef double mp_float_t;
 #define MICROPY_PY_BUILTINS_HELP_MODULES (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-// Whether to set __file__ for imported modules
-#ifndef MICROPY_PY___FILE__
-#define MICROPY_PY___FILE__ (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
+// Use this to configure output of help('modules')
+#ifndef MICROPY_PY_BUILTINS_HELP_NUM_COLUMNS
+#define MICROPY_PY_BUILTINS_HELP_NUM_COLUMNS (4)
+#endif
+
+// Use this to configure output of help('modules')
+#ifndef MICROPY_PY_BUILTINS_HELP_COLUMN_WIDTH
+#define MICROPY_PY_BUILTINS_HELP_COLUMN_WIDTH (18)
 #endif
 
 // Whether to provide mem-info related functions in micropython module
@@ -1254,6 +1609,11 @@ typedef double mp_float_t;
 #define MICROPY_PY_MICROPYTHON_HEAP_LOCKED (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
 #endif
 
+// Support for micropython.RingIO()
+#ifndef MICROPY_PY_MICROPYTHON_RINGIO
+#define MICROPY_PY_MICROPYTHON_RINGIO (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#endif
+
 // Whether to provide "array" module. Note that large chunk of the
 // underlying code is shared with "bytearray" builtin type, so to
 // get real savings, it should be disabled too.
@@ -1278,11 +1638,21 @@ typedef double mp_float_t;
 #define MICROPY_PY_COLLECTIONS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
 #endif
 
-// Whether to provide "ucollections.deque" type
+// Whether to provide "collections.deque" type
 #ifndef MICROPY_PY_COLLECTIONS_DEQUE
 #define MICROPY_PY_COLLECTIONS_DEQUE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
+// Whether "collections.deque" supports iteration
+#ifndef MICROPY_PY_COLLECTIONS_DEQUE_ITER
+#define MICROPY_PY_COLLECTIONS_DEQUE_ITER (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#endif
+
+// Whether "collections.deque" supports subscription
+#ifndef MICROPY_PY_COLLECTIONS_DEQUE_SUBSCR
+#define MICROPY_PY_COLLECTIONS_DEQUE_SUBSCR (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#endif
+
 // Whether to provide "collections.OrderedDict" type
 #ifndef MICROPY_PY_COLLECTIONS_ORDEREDDICT
 #define MICROPY_PY_COLLECTIONS_ORDEREDDICT (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
@@ -1293,6 +1663,11 @@ typedef double mp_float_t;
 #define MICROPY_PY_COLLECTIONS_NAMEDTUPLE__ASDICT (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
 #endif
 
+// Whether to provide "marshal" module
+#ifndef MICROPY_PY_MARSHAL
+#define MICROPY_PY_MARSHAL (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
+#endif
+
 // Whether to provide "math" module
 #ifndef MICROPY_PY_MATH
 #define MICROPY_PY_MATH (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
@@ -1334,10 +1709,16 @@ typedef double mp_float_t;
 #endif
 
 // Whether to provide fix for pow(1, NaN) and pow(NaN, 0), which both should be 1 not NaN.
+// Also fixes pow(base, NaN) to return NaN for other values of base.
 #ifndef MICROPY_PY_MATH_POW_FIX_NAN
 #define MICROPY_PY_MATH_POW_FIX_NAN (0)
 #endif
 
+// Whether to provide fix for gamma(-inf) to raise ValueError
+#ifndef MICROPY_PY_MATH_GAMMA_FIX_NEGINF
+#define MICROPY_PY_MATH_GAMMA_FIX_NEGINF (0)
+#endif
+
 // Whether to provide "cmath" module
 #ifndef MICROPY_PY_CMATH
 #define MICROPY_PY_CMATH (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
@@ -1365,7 +1746,7 @@ typedef double mp_float_t;
 
 // Whether to provide "io.IOBase" class to support user streams
 #ifndef MICROPY_PY_IO_IOBASE
-#define MICROPY_PY_IO_IOBASE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#define MICROPY_PY_IO_IOBASE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
 #endif
 
 // Whether to provide "io.BytesIO" class
@@ -1383,9 +1764,16 @@ typedef double mp_float_t;
 #define MICROPY_PY_STRUCT (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
 #endif
 
+// Whether struct module provides unsafe and non-standard typecodes O, P, S.
+// These typecodes are not in CPython and can cause crashes by accessing arbitrary
+// memory.
+#ifndef MICROPY_PY_STRUCT_UNSAFE_TYPECODES
+#define MICROPY_PY_STRUCT_UNSAFE_TYPECODES (1)
+#endif
+
 // Whether to provide "sys" module
 #ifndef MICROPY_PY_SYS
-#define MICROPY_PY_SYS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
+#define MICROPY_PY_SYS (1)
 #endif
 
 // Whether to initialise "sys.path" and "sys.argv" to their defaults in mp_init()
@@ -1416,6 +1804,11 @@ typedef double mp_float_t;
 #define MICROPY_PY_SYS_EXECUTABLE (0)
 #endif
 
+// Whether to provide "sys.intern"
+#ifndef MICROPY_PY_SYS_INTERN
+#define MICROPY_PY_SYS_INTERN (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
+#endif
+
 // Whether to provide "sys.exit" function
 #ifndef MICROPY_PY_SYS_EXIT
 #define MICROPY_PY_SYS_EXIT (1)
@@ -1426,6 +1819,23 @@ typedef double mp_float_t;
 #define MICROPY_PY_SYS_ATEXIT (0)
 #endif
 
+// Whether to provide the "sys.path" attribute (which forces module delegation
+// and mutable sys attributes to be enabled).
+// If MICROPY_PY_SYS_PATH_ARGV_DEFAULTS is enabled, this is initialised in
+// mp_init to an empty list. Otherwise the port must initialise it using
+// `mp_sys_path = mp_obj_new_list(...)`.
+#ifndef MICROPY_PY_SYS_PATH
+#define MICROPY_PY_SYS_PATH (1)
+#endif
+
+// Whether to provide the "sys.argv" attribute.
+// If MICROPY_PY_SYS_PATH_ARGV_DEFAULTS is enabled, this is initialised in
+// mp_init to an empty list. Otherwise the port must initialise it using
+// `mp_obj_list_init(MP_OBJ_TO_PTR(mp_sys_argv), ...);`
+#ifndef MICROPY_PY_SYS_ARGV
+#define MICROPY_PY_SYS_ARGV (1)
+#endif
+
 // Whether to provide sys.{ps1,ps2} mutable attributes, to control REPL prompts
 #ifndef MICROPY_PY_SYS_PS1_PS2
 #define MICROPY_PY_SYS_PS1_PS2 (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
@@ -1460,54 +1870,59 @@ typedef double mp_float_t;
 // Whether the sys module supports attribute delegation
 // This is enabled automatically when needed by other features
 #ifndef MICROPY_PY_SYS_ATTR_DELEGATION
-#define MICROPY_PY_SYS_ATTR_DELEGATION (MICROPY_PY_SYS_PS1_PS2 || MICROPY_PY_SYS_TRACEBACKLIMIT)
+#define MICROPY_PY_SYS_ATTR_DELEGATION (MICROPY_PY_SYS_PATH || MICROPY_PY_SYS_PS1_PS2 || MICROPY_PY_SYS_TRACEBACKLIMIT)
 #endif
 
-// Whether to provide "uerrno" module
-#ifndef MICROPY_PY_UERRNO
-#define MICROPY_PY_UERRNO (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+// Whether to provide "errno" module
+#ifndef MICROPY_PY_ERRNO
+#define MICROPY_PY_ERRNO (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-// Whether to provide the uerrno.errorcode dict
-#ifndef MICROPY_PY_UERRNO_ERRORCODE
-#define MICROPY_PY_UERRNO_ERRORCODE (1)
+// Whether to provide the errno.errorcode dict
+#ifndef MICROPY_PY_ERRNO_ERRORCODE
+#define MICROPY_PY_ERRNO_ERRORCODE (1)
 #endif
 
-// Whether to provide "uselect" module (baremetal implementation)
-#ifndef MICROPY_PY_USELECT
-#define MICROPY_PY_USELECT (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+// Whether to provide "select" module
+#ifndef MICROPY_PY_SELECT
+#define MICROPY_PY_SELECT (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-// Whether to enable the select() function in the "uselect" module (baremetal
+// Whether to enable POSIX optimisations in the "select" module (requires system poll)
+#ifndef MICROPY_PY_SELECT_POSIX_OPTIMISATIONS
+#define MICROPY_PY_SELECT_POSIX_OPTIMISATIONS (0)
+#endif
+
+// Whether to enable the select() function in the "select" module (baremetal
 // implementation). This is present for compatibility but can be disabled to
 // save space.
-#ifndef MICROPY_PY_USELECT_SELECT
-#define MICROPY_PY_USELECT_SELECT (1)
+#ifndef MICROPY_PY_SELECT_SELECT
+#define MICROPY_PY_SELECT_SELECT (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-// Whether to provide the "utime" module
-#ifndef MICROPY_PY_UTIME
-#define MICROPY_PY_UTIME (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_BASIC_FEATURES)
+// Whether to provide the "time" module
+#ifndef MICROPY_PY_TIME
+#define MICROPY_PY_TIME (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_BASIC_FEATURES)
 #endif
 
-// Whether to provide utime.gmtime/localtime/mktime functions
-#ifndef MICROPY_PY_UTIME_GMTIME_LOCALTIME_MKTIME
-#define MICROPY_PY_UTIME_GMTIME_LOCALTIME_MKTIME (0)
+// Whether to provide time.gmtime/localtime/mktime functions
+#ifndef MICROPY_PY_TIME_GMTIME_LOCALTIME_MKTIME
+#define MICROPY_PY_TIME_GMTIME_LOCALTIME_MKTIME (0)
 #endif
 
-// Whether to provide utime.time/time_ns functions
-#ifndef MICROPY_PY_UTIME_TIME_TIME_NS
-#define MICROPY_PY_UTIME_TIME_TIME_NS (0)
+// Whether to provide time.time/time_ns functions
+#ifndef MICROPY_PY_TIME_TIME_TIME_NS
+#define MICROPY_PY_TIME_TIME_TIME_NS (0)
 #endif
 
-// Period of values returned by utime.ticks_ms(), ticks_us(), ticks_cpu()
+// Period of values returned by time.ticks_ms(), ticks_us(), ticks_cpu()
 // functions. Should be power of two. All functions above use the same
 // period, so if underlying hardware/API has different periods, the
 // minimum of them should be used. The value below is the maximum value
 // this parameter can take (corresponding to 30 bit tick values on 32-bit
 // system).
-#ifndef MICROPY_PY_UTIME_TICKS_PERIOD
-#define MICROPY_PY_UTIME_TICKS_PERIOD (MP_SMALL_INT_POSITIVE_MASK + 1)
+#ifndef MICROPY_PY_TIME_TICKS_PERIOD
+#define MICROPY_PY_TIME_TICKS_PERIOD (MP_SMALL_INT_POSITIVE_MASK + 1)
 #endif
 
 // Whether to provide "_thread" module
@@ -1527,10 +1942,24 @@ typedef double mp_float_t;
 #define MICROPY_PY_THREAD_GIL_VM_DIVISOR (32)
 #endif
 
+// Is a recursive mutex type in use?
+#ifndef MICROPY_PY_THREAD_RECURSIVE_MUTEX
+#define MICROPY_PY_THREAD_RECURSIVE_MUTEX (MICROPY_PY_THREAD && !MICROPY_PY_THREAD_GIL)
+#endif
+
+// Whether to provide the "weakref" module.
+#ifndef MICROPY_PY_WEAKREF
+#define MICROPY_PY_WEAKREF (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
+#endif
+
 // Extended modules
 
-#ifndef MICROPY_PY_UASYNCIO
-#define MICROPY_PY_UASYNCIO (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#ifndef MICROPY_PY_ASYNCIO
+#define MICROPY_PY_ASYNCIO (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#endif
+
+#ifndef MICROPY_PY_ASYNCIO_TASK_QUEUE_PUSH_CALLBACK
+#define MICROPY_PY_ASYNCIO_TASK_QUEUE_PUSH_CALLBACK (0)
 #endif
 
 #ifndef MICROPY_PY_UCTYPES
@@ -1543,107 +1972,118 @@ typedef double mp_float_t;
 #define MICROPY_PY_UCTYPES_NATIVE_C_TYPES (1)
 #endif
 
-#ifndef MICROPY_PY_UZLIB
-#define MICROPY_PY_UZLIB (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+// Whether to provide "deflate" module (decompression-only by default)
+#ifndef MICROPY_PY_DEFLATE
+#define MICROPY_PY_DEFLATE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-#ifndef MICROPY_PY_UJSON
-#define MICROPY_PY_UJSON (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+// Whether to provide compression support in "deflate" module
+#ifndef MICROPY_PY_DEFLATE_COMPRESS
+#define MICROPY_PY_DEFLATE_COMPRESS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_FULL_FEATURES)
 #endif
 
-// Whether to support the "separators" argument to dump, dumps
-#ifndef MICROPY_PY_UJSON_SEPARATORS
-#define MICROPY_PY_UJSON_SEPARATORS (1)
+#ifndef MICROPY_PY_JSON
+#define MICROPY_PY_JSON (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-#ifndef MICROPY_PY_UOS
-#define MICROPY_PY_UOS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+// Whether to support the "separators" argument to dump, dumps
+#ifndef MICROPY_PY_JSON_SEPARATORS
+#define MICROPY_PY_JSON_SEPARATORS (1)
 #endif
 
-#ifndef MICROPY_PY_UOS_STATVFS
-#define MICROPY_PY_UOS_STATVFS (MICROPY_PY_UOS)
+#ifndef MICROPY_PY_OS
+#define MICROPY_PY_OS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-#ifndef MICROPY_PY_URE
-#define MICROPY_PY_URE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#ifndef MICROPY_PY_OS_STATVFS
+#define MICROPY_PY_OS_STATVFS (MICROPY_PY_OS)
 #endif
 
-#ifndef MICROPY_PY_URE_DEBUG
-#define MICROPY_PY_URE_DEBUG (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
+#ifndef MICROPY_PY_RE
+#define MICROPY_PY_RE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-#ifndef MICROPY_PY_URE_MATCH_GROUPS
-#define MICROPY_PY_URE_MATCH_GROUPS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
+#ifndef MICROPY_PY_RE_DEBUG
+#define MICROPY_PY_RE_DEBUG (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
 #endif
 
-#ifndef MICROPY_PY_URE_MATCH_SPAN_START_END
-#define MICROPY_PY_URE_MATCH_SPAN_START_END (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
+#ifndef MICROPY_PY_RE_MATCH_GROUPS
+#define MICROPY_PY_RE_MATCH_GROUPS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
 #endif
 
-#ifndef MICROPY_PY_URE_SUB
-#define MICROPY_PY_URE_SUB (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#ifndef MICROPY_PY_RE_MATCH_SPAN_START_END
+#define MICROPY_PY_RE_MATCH_SPAN_START_END (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
 #endif
 
-#ifndef MICROPY_PY_UHEAPQ
-#define MICROPY_PY_UHEAPQ (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#ifndef MICROPY_PY_RE_SUB
+#define MICROPY_PY_RE_SUB (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-// Optimized heap queue for relative timestamps (only used by uasyncio v2)
-#ifndef MICROPY_PY_UTIMEQ
-#define MICROPY_PY_UTIMEQ (0)
+#ifndef MICROPY_PY_HEAPQ
+#define MICROPY_PY_HEAPQ (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-#ifndef MICROPY_PY_UHASHLIB
-#define MICROPY_PY_UHASHLIB (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#ifndef MICROPY_PY_HASHLIB
+#define MICROPY_PY_HASHLIB (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-#ifndef MICROPY_PY_UHASHLIB_MD5
-#define MICROPY_PY_UHASHLIB_MD5 (0)
+#ifndef MICROPY_PY_HASHLIB_MD5
+#define MICROPY_PY_HASHLIB_MD5 (MICROPY_PY_SSL)
 #endif
 
-#ifndef MICROPY_PY_UHASHLIB_SHA1
-#define MICROPY_PY_UHASHLIB_SHA1  (0)
+#ifndef MICROPY_PY_HASHLIB_SHA1
+#define MICROPY_PY_HASHLIB_SHA1  (MICROPY_PY_SSL)
 #endif
 
-#ifndef MICROPY_PY_UHASHLIB_SHA256
-#define MICROPY_PY_UHASHLIB_SHA256 (1)
+#ifndef MICROPY_PY_HASHLIB_SHA256
+#define MICROPY_PY_HASHLIB_SHA256 (1)
 #endif
 
-#ifndef MICROPY_PY_UCRYPTOLIB
-#define MICROPY_PY_UCRYPTOLIB (0)
+#ifndef MICROPY_PY_CRYPTOLIB
+#define MICROPY_PY_CRYPTOLIB (MICROPY_PY_SSL)
 #endif
 
-// Depends on MICROPY_PY_UCRYPTOLIB
-#ifndef MICROPY_PY_UCRYPTOLIB_CTR
-#define MICROPY_PY_UCRYPTOLIB_CTR (0)
+// Depends on MICROPY_PY_CRYPTOLIB
+#ifndef MICROPY_PY_CRYPTOLIB_CTR
+#define MICROPY_PY_CRYPTOLIB_CTR (0)
 #endif
 
-#ifndef MICROPY_PY_UCRYPTOLIB_CONSTS
-#define MICROPY_PY_UCRYPTOLIB_CONSTS (0)
+#ifndef MICROPY_PY_CRYPTOLIB_CONSTS
+#define MICROPY_PY_CRYPTOLIB_CONSTS (0)
 #endif
 
-#ifndef MICROPY_PY_UBINASCII
-#define MICROPY_PY_UBINASCII (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#ifndef MICROPY_PY_BINASCII
+#define MICROPY_PY_BINASCII (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-// Depends on MICROPY_PY_UZLIB
-#ifndef MICROPY_PY_UBINASCII_CRC32
-#define MICROPY_PY_UBINASCII_CRC32 (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+// Depends on MICROPY_PY_DEFLATE
+#ifndef MICROPY_PY_BINASCII_CRC32
+#define MICROPY_PY_BINASCII_CRC32 (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-#ifndef MICROPY_PY_URANDOM
-#define MICROPY_PY_URANDOM (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#ifndef MICROPY_PY_RANDOM
+#define MICROPY_PY_RANDOM (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
 // Whether to include: randrange, randint, choice, random, uniform
-#ifndef MICROPY_PY_URANDOM_EXTRA_FUNCS
-#define MICROPY_PY_URANDOM_EXTRA_FUNCS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#ifndef MICROPY_PY_RANDOM_EXTRA_FUNCS
+#define MICROPY_PY_RANDOM_EXTRA_FUNCS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
 #ifndef MICROPY_PY_MACHINE
 #define MICROPY_PY_MACHINE (0)
 #endif
 
+// Whether to include: reset, reset_cause
+#ifndef MICROPY_PY_MACHINE_RESET
+#define MICROPY_PY_MACHINE_RESET (0)
+#endif
+
+// Maximum number of arguments for machine.freq()
+#ifndef MICROPY_PY_MACHINE_FREQ_NUM_ARGS_MAX
+#define MICROPY_PY_MACHINE_FREQ_NUM_ARGS_MAX (1)
+#endif
+
 // Whether to include: bitstream
 #ifndef MICROPY_PY_MACHINE_BITSTREAM
 #define MICROPY_PY_MACHINE_BITSTREAM (0)
@@ -1654,6 +2094,21 @@ typedef double mp_float_t;
 #define MICROPY_PY_MACHINE_PULSE (0)
 #endif
 
+// Whether to provide the "machine.mem8/16/32" objects
+#ifndef MICROPY_PY_MACHINE_MEMX
+#define MICROPY_PY_MACHINE_MEMX (MICROPY_PY_MACHINE)
+#endif
+
+// Whether to provide the "machine.mem_backup" function
+#ifndef MICROPY_PY_MACHINE_MEM_BACKUP
+#define MICROPY_PY_MACHINE_MEM_BACKUP (0)
+#endif
+
+// Whether to provide the "machine.Signal" class
+#ifndef MICROPY_PY_MACHINE_SIGNAL
+#define MICROPY_PY_MACHINE_SIGNAL (MICROPY_PY_MACHINE)
+#endif
+
 #ifndef MICROPY_PY_MACHINE_I2C
 #define MICROPY_PY_MACHINE_I2C (0)
 #endif
@@ -1677,27 +2132,58 @@ typedef double mp_float_t;
 #define MICROPY_PY_MACHINE_SOFTSPI (0)
 #endif
 
+// Values of SPI.MSB and SPI.LSB constants
+#ifndef MICROPY_PY_MACHINE_SPI_MSB
+#define MICROPY_PY_MACHINE_SPI_MSB (0)
+#define MICROPY_PY_MACHINE_SPI_LSB (1)
+#endif
+
 // Whether to provide the "machine.Timer" class
 #ifndef MICROPY_PY_MACHINE_TIMER
 #define MICROPY_PY_MACHINE_TIMER (0)
 #endif
 
 // The default backlog value for socket.listen(backlog)
-#ifndef MICROPY_PY_USOCKET_LISTEN_BACKLOG_DEFAULT
-#define MICROPY_PY_USOCKET_LISTEN_BACKLOG_DEFAULT (2)
+#ifndef MICROPY_PY_SOCKET_LISTEN_BACKLOG_DEFAULT
+#define MICROPY_PY_SOCKET_LISTEN_BACKLOG_DEFAULT (2)
+#endif
+
+// Whether to enable lwIP bindings to be used as the implementation of the `socket` module
+#ifndef MICROPY_PY_LWIP
+#define MICROPY_PY_LWIP (0)
+#endif
+
+// Whether to support raw sockets via the `socket.SOCK_RAW` constant
+#ifndef MICROPY_PY_LWIP_SOCK_RAW
+#define MICROPY_PY_LWIP_SOCK_RAW (MICROPY_PY_LWIP)
+#endif
+
+#ifndef MICROPY_PY_SSL
+#define MICROPY_PY_SSL (0)
+#endif
+
+// Whether to add finaliser code to ssl objects
+#ifndef MICROPY_PY_SSL_FINALISER
+#define MICROPY_PY_SSL_FINALISER (MICROPY_ENABLE_FINALISER)
+#endif
+
+// Whether to add a root pointer for the current ssl object
+#ifndef MICROPY_PY_SSL_MBEDTLS_NEED_ACTIVE_CONTEXT
+#define MICROPY_PY_SSL_MBEDTLS_NEED_ACTIVE_CONTEXT (MICROPY_PY_SSL_ECDSA_SIGN_ALT)
 #endif
 
-#ifndef MICROPY_PY_USSL
-#define MICROPY_PY_USSL (0)
+// Whether to support DTLS protocol (non-CPython feature)
+#ifndef MICROPY_PY_SSL_DTLS
+#define MICROPY_PY_SSL_DTLS (MICROPY_SSL_MBEDTLS && MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
-// Whether to add finaliser code to ussl objects
-#ifndef MICROPY_PY_USSL_FINALISER
-#define MICROPY_PY_USSL_FINALISER (0)
+// Whether to provide the "vfs" module
+#ifndef MICROPY_PY_VFS
+#define MICROPY_PY_VFS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES && MICROPY_VFS)
 #endif
 
-#ifndef MICROPY_PY_UWEBSOCKET
-#define MICROPY_PY_UWEBSOCKET (0)
+#ifndef MICROPY_PY_WEBSOCKET
+#define MICROPY_PY_WEBSOCKET (0)
 #endif
 
 #ifndef MICROPY_PY_FRAMEBUF
@@ -1713,6 +2199,11 @@ typedef double mp_float_t;
 #define MICROPY_PY_ONEWIRE (0)
 #endif
 
+// Whether to provide the "platform" module
+#ifndef MICROPY_PY_PLATFORM
+#define MICROPY_PY_PLATFORM (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#endif
+
 /*****************************************************************************/
 /* Hooks for a port to add builtins                                          */
 
@@ -1778,7 +2269,7 @@ typedef double mp_float_t;
 /*****************************************************************************/
 /* Miscellaneous settings                                                    */
 
-// All uPy objects in ROM must be aligned on at least a 4 byte boundary
+// All MicroPython objects in ROM must be aligned on at least a 4 byte boundary
 // so that the small-int/qstr/pointer distinction can be made.  For machines
 // that don't do this (eg 16-bit CPU), define the following macro to something
 // like __attribute__((aligned(4))).
@@ -1788,8 +2279,12 @@ typedef double mp_float_t;
 
 // String used for the banner, and sys.version additional information
 #ifndef MICROPY_BANNER_NAME_AND_VERSION
+#if MICROPY_PREVIEW_VERSION_2
+#define MICROPY_BANNER_NAME_AND_VERSION "MicroPython (with v2.0 preview) " MICROPY_GIT_TAG " on " MICROPY_BUILD_DATE
+#else
 #define MICROPY_BANNER_NAME_AND_VERSION "MicroPython " MICROPY_GIT_TAG " on " MICROPY_BUILD_DATE
 #endif
+#endif
 
 // String used for the second part of the banner, and sys.implementation._machine
 #ifndef MICROPY_BANNER_MACHINE
@@ -1800,20 +2295,6 @@ typedef double mp_float_t;
 #endif
 #endif
 
-// On embedded platforms, these will typically enable/disable irqs.
-#ifndef MICROPY_BEGIN_ATOMIC_SECTION
-#define MICROPY_BEGIN_ATOMIC_SECTION() (0)
-#endif
-#ifndef MICROPY_END_ATOMIC_SECTION
-#define MICROPY_END_ATOMIC_SECTION(state) (void)(state)
-#endif
-
-// Allow to override static modifier for global objects, e.g. to use with
-// object code analysis tools which don't support static symbols.
-#ifndef STATIC
-#define STATIC static
-#endif
-
 // Number of bytes in an object word: mp_obj_t, mp_uint_t, mp_uint_t
 #ifndef MP_BYTES_PER_OBJ_WORD
 #define MP_BYTES_PER_OBJ_WORD (sizeof(mp_uint_t))
@@ -1862,15 +2343,59 @@ typedef double mp_float_t;
 #define MICROPY_MAKE_POINTER_CALLABLE(p) (p)
 #endif
 
-// If these MP_PLAT_*_EXEC macros are overridden then the memory allocated by them
-// must be somehow reachable for marking by the GC, since the native code
-// generators store pointers to GC managed memory in the code.
+// Whether native text/BSS/rodata memory loaded from .mpy files is explicitly tracked
+// so that the GC cannot reclaim it.
+//
+// In general a port should let these options have their defaults, but the defaults here
+// can be overridden if needed by defining both MICROPY_PERSISTENT_CODE_TRACK_FUN_DATA
+// and MICROPY_PERSISTENT_CODE_TRACK_BSS_RODATA.
+#ifndef MICROPY_PERSISTENT_CODE_TRACK_FUN_DATA
+#if (MICROPY_EMIT_INLINE_ASM || MICROPY_ENABLE_NATIVE_CODE) && MICROPY_PERSISTENT_CODE_LOAD
+// Pointer tracking is required when loading native code is enabled.
+#if defined(MP_PLAT_ALLOC_EXEC) || defined(MP_PLAT_COMMIT_EXEC)
+// If a port defined a custom allocator or commit function for native text, then the
+// text does not need to be tracked (its allocation is managed by the port).  But the
+// BSS/rodata must be tracked (if there is any) because if there are any pointers to it
+// in the function data, they aren't traced by the GC.
+#define MICROPY_PERSISTENT_CODE_TRACK_FUN_DATA (0)
+#define MICROPY_PERSISTENT_CODE_TRACK_BSS_RODATA (1)
+#else
+// If a port uses the default allocator (the GC heap) then all native text is allocated
+// on the GC heap.  But it's not guaranteed that a pointer to the head of the block of
+// native text (which may contain multiple native functions) will be retained for the GC
+// to trace.  This is because native functions can start inside the big block of text
+// and so it's possible that the only GC-reachable pointers are pointers inside.
+// Therefore the big block is explicitly tracked. If there is any BSS/rodata memory,
+// then it does not need to be explicitly tracked because a pointer to it is stored into
+// the function text via `mp_native_relocate()`.
+#define MICROPY_PERSISTENT_CODE_TRACK_FUN_DATA (1)
+#define MICROPY_PERSISTENT_CODE_TRACK_BSS_RODATA (0)
+#endif
+#else // (MICROPY_EMIT_INLINE_ASM || MICROPY_ENABLE_NATIVE_CODE) && MICROPY_PERSISTENT_CODE_LOAD
+// Pointer tracking not needed when loading native code is disabled.
+#define MICROPY_PERSISTENT_CODE_TRACK_FUN_DATA (0)
+#define MICROPY_PERSISTENT_CODE_TRACK_BSS_RODATA (0)
+#endif
+#endif
+
+// If these macros are defined then the memory allocated by them does not need to be
+// traced by the GC.  But if they are left undefined then the GC heap will be used as
+// the allocator and the memory must be traced by the GC.  See also above logic for
+// enabling MICROPY_PERSISTENT_CODE_TRACK_FUN_DATA and
+// MICROPY_PERSISTENT_CODE_TRACK_BSS_RODATA.
 #ifndef MP_PLAT_ALLOC_EXEC
 #define MP_PLAT_ALLOC_EXEC(min_size, ptr, size) do { *ptr = m_new(byte, min_size); *size = min_size; } while (0)
+#define MP_PLAT_FREE_EXEC(ptr, size) m_del(byte, ptr, size)
 #endif
 
-#ifndef MP_PLAT_FREE_EXEC
-#define MP_PLAT_FREE_EXEC(ptr, size) m_del(byte, ptr, size)
+// Allocating new heap area at runtime requires port to be able to allocate from system heap
+#if MICROPY_GC_SPLIT_HEAP_AUTO
+#ifndef MP_PLAT_ALLOC_HEAP
+#define MP_PLAT_ALLOC_HEAP(size) malloc(size)
+#endif
+#ifndef MP_PLAT_FREE_HEAP
+#define MP_PLAT_FREE_HEAP(ptr) free(ptr)
+#endif
 #endif
 
 // This macro is used to do all output (except when MICROPY_PY_IO is defined)
@@ -1882,25 +2407,13 @@ typedef double mp_float_t;
 #define MP_SSIZE_MAX SSIZE_MAX
 #endif
 
-// printf format spec to use for mp_int_t and friends
-#ifndef INT_FMT
-#if defined(__LP64__)
-// Archs where mp_int_t == long, long != int
-#define UINT_FMT "%lu"
-#define INT_FMT "%ld"
-#elif defined(_WIN64)
-#define UINT_FMT "%llu"
-#define INT_FMT "%lld"
-#else
-// Archs where mp_int_t == int
-#define UINT_FMT "%u"
-#define INT_FMT "%d"
+// Modifier for function which doesn't return
+#ifndef MP_NORETURN
+#define MP_NORETURN __attribute__((noreturn))
 #endif
-#endif // INT_FMT
 
-// Modifier for function which doesn't return
-#ifndef NORETURN
-#define NORETURN __attribute__((noreturn))
+#if !MICROPY_PREVIEW_VERSION_2
+#define NORETURN MP_NORETURN
 #endif
 
 // Modifier for weak functions
@@ -1946,7 +2459,7 @@ typedef double mp_float_t;
 
 #ifndef MP_HTOBE16
 #if MP_ENDIANNESS_LITTLE
-#define MP_HTOBE16(x) ((uint16_t)((((x) & 0xff) << 8) | (((x) >> 8) & 0xff)))
+#define MP_HTOBE16(x) MP_BSWAP16(x)
 #define MP_BE16TOH(x) MP_HTOBE16(x)
 #else
 #define MP_HTOBE16(x) (x)
@@ -1956,7 +2469,7 @@ typedef double mp_float_t;
 
 #ifndef MP_HTOBE32
 #if MP_ENDIANNESS_LITTLE
-#define MP_HTOBE32(x) ((uint32_t)((((x) & 0xff) << 24) | (((x) & 0xff00) << 8) | (((x) >> 8) & 0xff00) | (((x) >> 24) & 0xff)))
+#define MP_HTOBE32(x) MP_BSWAP32(x)
 #define MP_BE32TOH(x) MP_HTOBE32(x)
 #else
 #define MP_HTOBE32(x) (x)
@@ -1975,13 +2488,22 @@ typedef double mp_float_t;
 #define MP_WARN_CAT(x) (NULL)
 #endif
 
-// Feature dependency check.
-#if MICROPY_PY_SYS_SETTRACE
-#if !MICROPY_PERSISTENT_CODE_SAVE
-#error "MICROPY_PY_SYS_SETTRACE requires MICROPY_PERSISTENT_CODE_SAVE to be enabled"
-#endif
-#if MICROPY_COMP_CONST
-#error "MICROPY_PY_SYS_SETTRACE requires MICROPY_COMP_CONST to be disabled"
+// If true, use __builtin_mul_overflow (a gcc intrinsic supported by clang) for
+// overflow checking when multiplying two small ints. Otherwise, use a portable
+// algorithm.
+//
+// Most MCUs have a 32x32->64 bit multiply instruction, in which case the
+// intrinsic is likely to be faster and generate smaller code. The main exception is
+// cortex-m0 with __ARM_ARCH_ISA_THUMB == 1.
+//
+// The intrinsic is in GCC starting with version 5.
+#ifndef MICROPY_USE_GCC_MUL_OVERFLOW_INTRINSIC
+#if defined(__ARM_ARCH_ISA_THUMB) && (__GNUC__ >= 5)
+#define MICROPY_USE_GCC_MUL_OVERFLOW_INTRINSIC (__ARM_ARCH_ISA_THUMB >= 2)
+#elif (__GNUC__ >= 5)
+#define MICROPY_USE_GCC_MUL_OVERFLOW_INTRINSIC (1)
+#else
+#define MICROPY_USE_GCC_MUL_OVERFLOW_INTRINSIC (0)
 #endif
 #endif
 
diff --git a/py/mperrno.h b/py/mperrno.h
index 6f6d816b8f4..be2a65a088c 100644
--- a/py/mperrno.h
+++ b/py/mperrno.h
@@ -141,7 +141,7 @@
 
 #endif
 
-#if MICROPY_PY_UERRNO
+#if MICROPY_PY_ERRNO
 
 #include "py/obj.h"
 
diff --git a/py/mphal.h b/py/mphal.h
index 0d4b1224e5c..d52e10be44c 100644
--- a/py/mphal.h
+++ b/py/mphal.h
@@ -27,6 +27,7 @@
 #define MICROPY_INCLUDED_PY_MPHAL_H
 
 #include 
+#include 
 #include "py/mpconfig.h"
 
 #ifdef MICROPY_MPHALPORT_H
@@ -35,6 +36,14 @@
 #include 
 #endif
 
+// On embedded platforms, these will typically enable/disable irqs.
+#ifndef MICROPY_BEGIN_ATOMIC_SECTION
+#define MICROPY_BEGIN_ATOMIC_SECTION() (0)
+#endif
+#ifndef MICROPY_END_ATOMIC_SECTION
+#define MICROPY_END_ATOMIC_SECTION(state) (void)(state)
+#endif
+
 #ifndef mp_hal_stdio_poll
 uintptr_t mp_hal_stdio_poll(uintptr_t poll_flags);
 #endif
@@ -48,7 +57,7 @@ void mp_hal_stdout_tx_str(const char *str);
 #endif
 
 #ifndef mp_hal_stdout_tx_strn
-void mp_hal_stdout_tx_strn(const char *str, size_t len);
+mp_uint_t mp_hal_stdout_tx_strn(const char *str, size_t len);
 #endif
 
 #ifndef mp_hal_stdout_tx_strn_cooked
@@ -90,4 +99,17 @@ uint64_t mp_hal_time_ns(void);
 #include "extmod/virtpin.h"
 #endif
 
+// Event handling and wait-for-event functions.
+
+#ifndef MICROPY_INTERNAL_WFE
+// Fallback definition for ports that don't need to suspend the CPU.
+#define MICROPY_INTERNAL_WFE(TIMEOUT_MS) (void)0
+#endif
+
+#ifndef MICROPY_INTERNAL_EVENT_HOOK
+// Fallback definition for ports that don't need any port-specific
+// non-blocking event processing.
+#define MICROPY_INTERNAL_EVENT_HOOK (void)0
+#endif
+
 #endif // MICROPY_INCLUDED_PY_MPHAL_H
diff --git a/py/mpprint.c b/py/mpprint.c
index 3218bd2f4dc..fa37a4d07ff 100644
--- a/py/mpprint.c
+++ b/py/mpprint.c
@@ -40,10 +40,22 @@
 #include "py/formatfloat.h"
 #endif
 
-static const char pad_spaces[] = "                ";
-static const char pad_zeroes[] = "0000000000000000";
-
-STATIC void plat_print_strn(void *env, const char *str, size_t len) {
+static const char pad_spaces[16] = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '};
+#define pad_spaces_size  (sizeof(pad_spaces))
+static const char pad_common[23] = {'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '_', '0', '0', '0', ',', '0', '0'};
+// The contents of pad_common is arranged to provide the following padding
+// strings with minimal flash size:
+//     0000000000000000 <- pad_zeroes
+//                 0000_000 <- pad_zeroes_underscore (offset: 12, size 5)
+//                      000,00 <- pad_zeroes_comma (offset: 17, size 4)
+#define pad_zeroes       (pad_common + 0)
+#define pad_zeroes_size  (16)
+#define pad_zeroes_underscore (pad_common + 12)
+#define pad_zeroes_underscore_size  (5)
+#define pad_zeroes_comma (pad_common + 17)
+#define pad_zeroes_comma_size  (4)
+
+static void plat_print_strn(void *env, const char *str, size_t len) {
     (void)env;
     MP_PLAT_PRINT_STRN(str, len);
 }
@@ -58,20 +70,37 @@ int mp_print_str(const mp_print_t *print, const char *str) {
     return len;
 }
 
-int mp_print_strn(const mp_print_t *print, const char *str, size_t len, int flags, char fill, int width) {
+int mp_print_strn(const mp_print_t *print, const char *str, size_t len, unsigned int flags, char fill, int width) {
     int left_pad = 0;
     int right_pad = 0;
     int pad = width - len;
     int pad_size;
     int total_chars_printed = 0;
     const char *pad_chars;
+    char grouping = flags >> PF_FLAG_SEP_POS;
 
     if (!fill || fill == ' ') {
         pad_chars = pad_spaces;
-        pad_size = sizeof(pad_spaces) - 1;
-    } else if (fill == '0') {
+        pad_size = pad_spaces_size;
+    } else if (fill == '0' && !grouping) {
         pad_chars = pad_zeroes;
-        pad_size = sizeof(pad_zeroes) - 1;
+        pad_size = pad_zeroes_size;
+    } else if (fill == '0') {
+        if (grouping == '_') {
+            pad_chars = pad_zeroes_underscore;
+            pad_size = pad_zeroes_underscore_size;
+        } else {
+            pad_chars = pad_zeroes_comma;
+            pad_size = pad_zeroes_comma_size;
+        }
+        // The result will never start with a grouping character. An extra leading zero is added.
+        // width is dead after this so we can use it in calculation
+        if (width % pad_size == 0) {
+            pad++;
+            width++;
+        }
+        // position the grouping character correctly within the pad repetition
+        pad_chars += pad_size - 1 - width % pad_size;
     } else {
         // Other pad characters are fairly unusual, so we'll take the hit
         // and output them 1 at a time.
@@ -127,7 +156,7 @@ int mp_print_strn(const mp_print_t *print, const char *str, size_t len, int flag
 
 // This function is used exclusively by mp_vprintf to format ints.
 // It needs to be a separate function to mp_print_mp_int, since converting to a mp_int looses the MSB.
-STATIC int mp_print_int(const mp_print_t *print, mp_uint_t x, int sgn, int base, int base_char, int flags, char fill, int width) {
+static int mp_print_int(const mp_print_t *print, mp_uint_t x, int sgn, int base, int base_char, int flags, char fill, int width) {
     char sign = 0;
     if (sgn) {
         if ((mp_int_t)x < 0) {
@@ -201,7 +230,7 @@ STATIC int mp_print_int(const mp_print_t *print, mp_uint_t x, int sgn, int base,
     return len;
 }
 
-int mp_print_mp_int(const mp_print_t *print, mp_obj_t x, int base, int base_char, int flags, char fill, int width, int prec) {
+int mp_print_mp_int(const mp_print_t *print, mp_obj_t x, unsigned int base, int base_char, int flags, char fill, int width, int prec) {
     // These are the only values for "base" that are required to be supported by this
     // function, since Python only allows the user to format integers in these bases.
     // If needed this function could be generalised to handle other values.
@@ -248,10 +277,7 @@ int mp_print_mp_int(const mp_print_t *print, mp_obj_t x, int base, int base_char
     int prefix_len = prefix - prefix_buf;
     prefix = prefix_buf;
 
-    char comma = '\0';
-    if (flags & PF_FLAG_SHOW_COMMA) {
-        comma = ',';
-    }
+    char comma = flags >> PF_FLAG_SEP_POS;
 
     // The size of this buffer is rather arbitrary. If it's not large
     // enough, a dynamic one will be allocated.
@@ -340,8 +366,8 @@ int mp_print_mp_int(const mp_print_t *print, mp_obj_t x, int base, int base_char
 }
 
 #if MICROPY_PY_BUILTINS_FLOAT
-int mp_print_float(const mp_print_t *print, mp_float_t f, char fmt, int flags, char fill, int width, int prec) {
-    char buf[32];
+int mp_print_float(const mp_print_t *print, mp_float_t f, char fmt, unsigned int flags, char fill, int width, int prec) {
+    char buf[36];
     char sign = '\0';
     int chrs = 0;
 
@@ -352,11 +378,17 @@ int mp_print_float(const mp_print_t *print, mp_float_t f, char fmt, int flags, c
         sign = ' ';
     }
 
-    int len = mp_format_float(f, buf, sizeof(buf), fmt, prec, sign);
+    int len = mp_format_float(f, buf, sizeof(buf) - 3, fmt, prec, sign);
 
     char *s = buf;
 
-    if ((flags & PF_FLAG_ADD_PERCENT) && (size_t)(len + 1) < sizeof(buf)) {
+    if ((flags & PF_FLAG_ALWAYS_DECIMAL) && strchr(buf, '.') == NULL && strchr(buf, 'e') == NULL && strchr(buf, 'n') == NULL) {
+        buf[len++] = '.';
+        buf[len++] = '0';
+        buf[len] = '\0';
+    }
+
+    if (flags & PF_FLAG_ADD_PERCENT) {
         buf[len++] = '%';
         buf[len] = '\0';
     }
@@ -416,8 +448,6 @@ int mp_vprintf(const mp_print_t *print, const char *fmt, va_list args) {
                 flags |= PF_FLAG_SHOW_SIGN;
             } else if (*fmt == ' ') {
                 flags |= PF_FLAG_SPACE_SIGN;
-            } else if (*fmt == '!') {
-                flags |= PF_FLAG_NO_TRAILZ;
             } else if (*fmt == '0') {
                 flags |= PF_FLAG_PAD_AFTER_SIGN;
                 fill = '0';
@@ -451,16 +481,36 @@ int mp_vprintf(const mp_print_t *print, const char *fmt, va_list args) {
             }
         }
 
-        // parse long specifiers (only for LP64 model where they make a difference)
-        #ifndef __LP64__
-        const
+        // parse long and long long specifiers (only where they make a difference)
+        #if (MP_INT_MAX > INT_MAX && MP_INT_MAX == LONG_MAX) || defined(MICROPY_UNIX_COVERAGE)
+        #define SUPPORT_L_FORMAT (1)
+        #else
+        #define SUPPORT_L_FORMAT (0)
         #endif
+        #if SUPPORT_L_FORMAT
         bool long_arg = false;
+        #endif
+
+        #if (MP_INT_MAX > LONG_MAX) || defined(MICROPY_UNIX_COVERAGE)
+        #define SUPPORT_LL_FORMAT (1)
+        #else
+        #define SUPPORT_LL_FORMAT (0)
+        #endif
+        #if SUPPORT_LL_FORMAT
+        bool long_long_arg = false;
+        #endif
+
         if (*fmt == 'l') {
             ++fmt;
-            #ifdef __LP64__
+            #if SUPPORT_L_FORMAT
             long_arg = true;
             #endif
+            #if SUPPORT_LL_FORMAT
+            if (*fmt == 'l') {
+                ++fmt;
+                long_long_arg = true;
+            }
+            #endif
         }
 
         if (*fmt == '\0') {
@@ -506,35 +556,50 @@ int mp_vprintf(const mp_print_t *print, const char *fmt, va_list args) {
                 chrs += mp_print_strn(print, str, len, flags, fill, width);
                 break;
             }
-            case 'd': {
-                mp_int_t val;
-                if (long_arg) {
-                    val = va_arg(args, long int);
-                } else {
-                    val = va_arg(args, int);
-                }
-                chrs += mp_print_int(print, val, 1, 10, 'a', flags, fill, width);
-                break;
-            }
+            case 'd':
+            case 'p':
+            case 'P':
             case 'u':
             case 'x':
             case 'X': {
-                int base = 16 - ((*fmt + 1) & 6); // maps char u/x/X to base 10/16/16
-                char fmt_c = (*fmt & 0xf0) - 'P' + 'A'; // maps char u/x/X to char a/a/A
+                char fmt_chr = *fmt;
                 mp_uint_t val;
-                if (long_arg) {
-                    val = va_arg(args, unsigned long int);
+                if (fmt_chr == 'p' || fmt_chr == 'P') {
+                    val = va_arg(args, uintptr_t);
+                }
+                #if SUPPORT_LL_FORMAT
+                else if (long_long_arg) {
+                    val = va_arg(args, unsigned long long);
+                }
+                #endif
+                #if SUPPORT_L_FORMAT
+                else if (long_arg) {
+                    if (sizeof(long) != sizeof(mp_uint_t) && fmt_chr == 'd') {
+                        val = va_arg(args, long);
+                    } else {
+                        val = va_arg(args, unsigned long);
+                    }
+                }
+                #endif
+                else {
+                    if (sizeof(int) != sizeof(mp_uint_t) && fmt_chr == 'd') {
+                        val = va_arg(args, int);
+                    } else {
+                        val = va_arg(args, unsigned);
+                    }
+                }
+                int base;
+                // Map format char x/p/X/P to a/a/A/A for hex letters.
+                // It doesn't matter what d/u map to.
+                char fmt_c = (fmt_chr & 0xf0) - 'P' + 'A';
+                if (fmt_chr == 'd' || fmt_chr == 'u') {
+                    base = 10;
                 } else {
-                    val = va_arg(args, unsigned int);
+                    base = 16;
                 }
-                chrs += mp_print_int(print, val, 0, base, fmt_c, flags, fill, width);
+                chrs += mp_print_int(print, val, fmt_chr == 'd', base, fmt_c, flags, fill, width);
                 break;
             }
-            case 'p':
-            case 'P': // don't bother to handle upcase for 'P'
-                // Use unsigned long int to work on both ILP32 and LP64 systems
-                chrs += mp_print_int(print, va_arg(args, unsigned long int), 0, 16, 'a', flags, fill, width);
-                break;
             #if MICROPY_PY_BUILTINS_FLOAT
             case 'e':
             case 'E':
@@ -550,18 +615,6 @@ int mp_vprintf(const mp_print_t *print, const char *fmt, va_list args) {
                 #endif
                 break;
             }
-            #endif
-                // Because 'l' is eaten above, another 'l' means %ll.  We need to support
-                // this length specifier for OBJ_REPR_D (64-bit NaN boxing).
-                // TODO Either enable this unconditionally, or provide a specific config var.
-            #if (MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_D) || defined(_WIN64)
-            case 'l': {
-                unsigned long long int arg_value = va_arg(args, unsigned long long int);
-                ++fmt;
-                assert(*fmt == 'u' || *fmt == 'd' || !"unsupported fmt char");
-                chrs += mp_print_int(print, arg_value, *fmt == 'd', 10, 'a', flags, fill, width);
-                break;
-            }
             #endif
             default:
                 // if it's not %% then it's an unsupported format character
diff --git a/py/mpprint.h b/py/mpprint.h
index 8383ea85794..250ea24b878 100644
--- a/py/mpprint.h
+++ b/py/mpprint.h
@@ -31,13 +31,13 @@
 #define PF_FLAG_LEFT_ADJUST       (0x001)
 #define PF_FLAG_SHOW_SIGN         (0x002)
 #define PF_FLAG_SPACE_SIGN        (0x004)
-#define PF_FLAG_NO_TRAILZ         (0x008)
-#define PF_FLAG_SHOW_PREFIX       (0x010)
-#define PF_FLAG_SHOW_COMMA        (0x020)
-#define PF_FLAG_PAD_AFTER_SIGN    (0x040)
-#define PF_FLAG_CENTER_ADJUST     (0x080)
-#define PF_FLAG_ADD_PERCENT       (0x100)
-#define PF_FLAG_SHOW_OCTAL_LETTER (0x200)
+#define PF_FLAG_SHOW_PREFIX       (0x008)
+#define PF_FLAG_PAD_AFTER_SIGN    (0x010)
+#define PF_FLAG_CENTER_ADJUST     (0x020)
+#define PF_FLAG_ADD_PERCENT       (0x040)
+#define PF_FLAG_SHOW_OCTAL_LETTER (0x080)
+#define PF_FLAG_ALWAYS_DECIMAL    (0x100)
+#define PF_FLAG_SEP_POS           (9) // must be above all the above PF_FLAGs
 
 #if MICROPY_PY_IO && MICROPY_PY_SYS_STDFILES
 #define MP_PYTHON_PRINTER &mp_sys_stdout_print
@@ -69,9 +69,9 @@ extern const mp_print_t mp_sys_stdout_print;
 #endif
 
 int mp_print_str(const mp_print_t *print, const char *str);
-int mp_print_strn(const mp_print_t *print, const char *str, size_t len, int flags, char fill, int width);
+int mp_print_strn(const mp_print_t *print, const char *str, size_t len, unsigned int flags, char fill, int width);
 #if MICROPY_PY_BUILTINS_FLOAT
-int mp_print_float(const mp_print_t *print, mp_float_t f, char fmt, int flags, char fill, int width, int prec);
+int mp_print_float(const mp_print_t *print, mp_float_t f, char fmt, unsigned int flags, char fill, int width, int prec);
 #endif
 
 int mp_printf(const mp_print_t *print, const char *fmt, ...);
diff --git a/py/mpstate.h b/py/mpstate.h
index 80b49cb6b64..73353092073 100644
--- a/py/mpstate.h
+++ b/py/mpstate.h
@@ -40,7 +40,12 @@
 // memory system, runtime and virtual machine.  The state is a global
 // variable, but in the future it is hoped that the state can become local.
 
+#if MICROPY_PY_SYS_ATTR_DELEGATION
+// Must be kept in sync with sys_mutable_keys in modsys.c.
 enum {
+    #if MICROPY_PY_SYS_PATH
+    MP_SYS_MUTABLE_PATH,
+    #endif
     #if MICROPY_PY_SYS_PS1_PS2
     MP_SYS_MUTABLE_PS1,
     MP_SYS_MUTABLE_PS2,
@@ -50,13 +55,23 @@ enum {
     #endif
     MP_SYS_MUTABLE_NUM,
 };
+#endif // MICROPY_PY_SYS_ATTR_DELEGATION
 
 // This structure contains dynamic configuration for the compiler.
 #if MICROPY_DYNAMIC_COMPILER
 typedef struct mp_dynamic_compiler_t {
+    // This is used to let mpy-cross pass options to the emitter chosen with
+    // `native_arch`.  The main use case for the time being is to give the
+    // RV32 emitter extended information about which extensions can be
+    // optionally used, in order to generate code that's better suited for the
+    // hardware platform the code will run on.
+    void *backend_options;
     uint8_t small_int_bits; // must be <= host small_int_bits
     uint8_t native_arch;
     uint8_t nlr_buf_num_regs;
+    #if MICROPY_ENABLE_SOURCE_LINE
+    bool include_source_lines;
+    #endif
 } mp_dynamic_compiler_t;
 extern mp_dynamic_compiler_t mp_dynamic_compiler;
 #endif
@@ -71,6 +86,18 @@ typedef struct _mp_sched_item_t {
     mp_obj_t arg;
 } mp_sched_item_t;
 
+// gc_lock_depth field is a combination of the GC_COLLECT_FLAG
+// bit and a lock depth shifted GC_LOCK_DEPTH_SHIFT bits left.
+#if MICROPY_ENABLE_FINALISER
+#define GC_COLLECT_FLAG 1
+#define GC_LOCK_DEPTH_SHIFT 1
+#else
+// If finalisers are disabled then this check doesn't matter, as gc_lock()
+// is called anywhere else that heap can't be changed. So save some code size.
+#define GC_COLLECT_FLAG 0
+#define GC_LOCK_DEPTH_SHIFT 0
+#endif
+
 // This structure holds information about a single contiguous area of
 // memory reserved for the memory manager.
 typedef struct _mp_state_mem_area_t {
@@ -83,10 +110,14 @@ typedef struct _mp_state_mem_area_t {
     #if MICROPY_ENABLE_FINALISER
     byte *gc_finaliser_table_start;
     #endif
+    #if MICROPY_PY_WEAKREF
+    byte *gc_weakref_table_start;
+    #endif
     byte *gc_pool_start;
     byte *gc_pool_end;
 
     size_t gc_last_free_atb_index;
+    size_t gc_last_used_block; // The block ID of the highest block allocated in the area
 } mp_state_mem_area_t;
 
 // This structure hold information about the memory allocation system.
@@ -98,6 +129,10 @@ typedef struct _mp_state_mem_t {
     #endif
 
     mp_state_mem_area_t area;
+    #if MICROPY_GC_SPLIT_HEAP
+    byte *area_pool_min;  // Min of all gc_pool_start values across all areas
+    byte *area_pool_max;  // Max of all gc_pool_end values across all areas
+    #endif
 
     int gc_stack_overflow;
     MICROPY_GC_STACK_ENTRY_TYPE gc_block_stack[MICROPY_ALLOC_GC_STACK_SIZE];
@@ -126,7 +161,7 @@ typedef struct _mp_state_mem_t {
 
     #if MICROPY_PY_THREAD && !MICROPY_PY_THREAD_GIL
     // This is a global mutex used to make the GC thread-safe.
-    mp_thread_mutex_t gc_mutex;
+    mp_thread_recursive_mutex_t gc_mutex;
     #endif
 } mp_state_mem_t;
 
@@ -203,6 +238,9 @@ typedef struct _mp_state_vm_t {
     #if MICROPY_EMIT_NATIVE
     uint8_t default_emit_opt; // one of MP_EMIT_OPT_xxx
     #endif
+    #if MICROPY_DEBUG_PRINTERS
+    mp_uint_t mp_verbose_flag;
+    #endif
     #endif
 
     // size of the emergency exception buf, if it's dynamically allocated
@@ -242,8 +280,10 @@ typedef struct _mp_state_vm_t {
     #endif
 } mp_state_vm_t;
 
-// This structure holds state that is specific to a given thread.
-// Everything in this structure is scanned for root pointers.
+// This structure holds state that is specific to a given thread. Everything
+// in this structure is scanned for root pointers.  Anything added to this
+// structure must have corresponding initialisation added to thread_entry (in
+// py/modthread.c).
 typedef struct _mp_state_thread_t {
     // Stack top at the start of program
     char *stack_top;
@@ -259,6 +299,7 @@ typedef struct _mp_state_thread_t {
     #endif
 
     // Locking of the GC is done per thread.
+    // See GC_LOCK_DEPTH_SHIFT for an explanation of this field.
     uint16_t gc_lock_depth;
 
     ////////////////////////////////////////////////////////////
@@ -284,6 +325,10 @@ typedef struct _mp_state_thread_t {
     bool prof_callback_is_executing;
     struct _mp_code_state_t *current_code_state;
     #endif
+
+    #if MICROPY_PY_SSL_MBEDTLS_NEED_ACTIVE_CONTEXT
+    struct _mp_obj_ssl_context_t *tls_ssl_context;
+    #endif
 } mp_state_thread_t;
 
 // This structure combines the above 3 structures.
@@ -301,7 +346,6 @@ extern mp_state_ctx_t mp_state_ctx;
 #define MP_STATE_MAIN_THREAD(x) (mp_state_ctx.thread.x)
 
 #if MICROPY_PY_THREAD
-extern mp_state_thread_t *mp_thread_get_state(void);
 #define MP_STATE_THREAD(x) (mp_thread_get_state()->x)
 #define mp_thread_is_main_thread() (mp_thread_get_state() == &mp_state_ctx.thread)
 #else
diff --git a/py/mpthread.h b/py/mpthread.h
index e611ef4c119..795f230bb4a 100644
--- a/py/mpthread.h
+++ b/py/mpthread.h
@@ -40,13 +40,20 @@ struct _mp_state_thread_t;
 
 struct _mp_state_thread_t *mp_thread_get_state(void);
 void mp_thread_set_state(struct _mp_state_thread_t *state);
-void mp_thread_create(void *(*entry)(void *), void *arg, size_t *stack_size);
+mp_uint_t mp_thread_create(void *(*entry)(void *), void *arg, size_t *stack_size);
+mp_uint_t mp_thread_get_id(void);
 void mp_thread_start(void);
 void mp_thread_finish(void);
 void mp_thread_mutex_init(mp_thread_mutex_t *mutex);
 int mp_thread_mutex_lock(mp_thread_mutex_t *mutex, int wait);
 void mp_thread_mutex_unlock(mp_thread_mutex_t *mutex);
 
+#if MICROPY_PY_THREAD_RECURSIVE_MUTEX
+void mp_thread_recursive_mutex_init(mp_thread_recursive_mutex_t *mutex);
+int mp_thread_recursive_mutex_lock(mp_thread_recursive_mutex_t *mutex, int wait);
+void mp_thread_recursive_mutex_unlock(mp_thread_recursive_mutex_t *mutex);
+#endif
+
 #endif // MICROPY_PY_THREAD
 
 #if MICROPY_PY_THREAD && MICROPY_PY_THREAD_GIL
diff --git a/py/mpz.c b/py/mpz.c
index b61997e2fd4..e9fd0f45f02 100644
--- a/py/mpz.c
+++ b/py/mpz.c
@@ -49,7 +49,7 @@
  Definition of normalise: ?
 */
 
-STATIC size_t mpn_remove_trailing_zeros(mpz_dig_t *oidig, mpz_dig_t *idig) {
+static size_t mpn_remove_trailing_zeros(mpz_dig_t *oidig, mpz_dig_t *idig) {
     for (--idig; idig >= oidig && *idig == 0; --idig) {
     }
     return idig + 1 - oidig;
@@ -59,7 +59,7 @@ STATIC size_t mpn_remove_trailing_zeros(mpz_dig_t *oidig, mpz_dig_t *idig) {
    returns sign(i - j)
    assumes i, j are normalised
 */
-STATIC int mpn_cmp(const mpz_dig_t *idig, size_t ilen, const mpz_dig_t *jdig, size_t jlen) {
+static int mpn_cmp(const mpz_dig_t *idig, size_t ilen, const mpz_dig_t *jdig, size_t jlen) {
     if (ilen < jlen) {
         return -1;
     }
@@ -85,7 +85,7 @@ STATIC int mpn_cmp(const mpz_dig_t *idig, size_t ilen, const mpz_dig_t *jdig, si
    assumes enough memory in i; assumes normalised j; assumes n > 0
    can have i, j pointing to same memory
 */
-STATIC size_t mpn_shl(mpz_dig_t *idig, mpz_dig_t *jdig, size_t jlen, mp_uint_t n) {
+static size_t mpn_shl(mpz_dig_t *idig, mpz_dig_t *jdig, size_t jlen, mp_uint_t n) {
     mp_uint_t n_whole = (n + DIG_SIZE - 1) / DIG_SIZE;
     mp_uint_t n_part = n % DIG_SIZE;
     if (n_part == 0) {
@@ -124,7 +124,7 @@ STATIC size_t mpn_shl(mpz_dig_t *idig, mpz_dig_t *jdig, size_t jlen, mp_uint_t n
    assumes enough memory in i; assumes normalised j; assumes n > 0
    can have i, j pointing to same memory
 */
-STATIC size_t mpn_shr(mpz_dig_t *idig, mpz_dig_t *jdig, size_t jlen, mp_uint_t n) {
+static size_t mpn_shr(mpz_dig_t *idig, mpz_dig_t *jdig, size_t jlen, mp_uint_t n) {
     mp_uint_t n_whole = n / DIG_SIZE;
     mp_uint_t n_part = n % DIG_SIZE;
 
@@ -156,7 +156,7 @@ STATIC size_t mpn_shr(mpz_dig_t *idig, mpz_dig_t *jdig, size_t jlen, mp_uint_t n
    assumes enough memory in i; assumes normalised j, k; assumes jlen >= klen
    can have i, j, k pointing to same memory
 */
-STATIC size_t mpn_add(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen) {
+static size_t mpn_add(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen) {
     mpz_dig_t *oidig = idig;
     mpz_dbl_dig_t carry = 0;
 
@@ -186,7 +186,7 @@ STATIC size_t mpn_add(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const
    assumes enough memory in i; assumes normalised j, k; assumes j >= k
    can have i, j, k pointing to same memory
 */
-STATIC size_t mpn_sub(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen) {
+static size_t mpn_sub(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen) {
     mpz_dig_t *oidig = idig;
     mpz_dbl_dig_signed_t borrow = 0;
 
@@ -214,7 +214,7 @@ STATIC size_t mpn_sub(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const
    assumes enough memory in i; assumes normalised j, k; assumes jlen >= klen (jlen argument not needed)
    can have i, j, k pointing to same memory
 */
-STATIC size_t mpn_and(mpz_dig_t *idig, const mpz_dig_t *jdig, const mpz_dig_t *kdig, size_t klen) {
+static size_t mpn_and(mpz_dig_t *idig, const mpz_dig_t *jdig, const mpz_dig_t *kdig, size_t klen) {
     mpz_dig_t *oidig = idig;
 
     for (; klen > 0; --klen, ++idig, ++jdig, ++kdig) {
@@ -235,7 +235,7 @@ STATIC size_t mpn_and(mpz_dig_t *idig, const mpz_dig_t *jdig, const mpz_dig_t *k
    assumes enough memory in i; assumes normalised j, k; assumes length j >= length k
    can have i, j, k pointing to same memory
 */
-STATIC size_t mpn_and_neg(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen,
+static size_t mpn_and_neg(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen,
     mpz_dbl_dig_t carryi, mpz_dbl_dig_t carryj, mpz_dbl_dig_t carryk) {
     mpz_dig_t *oidig = idig;
     mpz_dig_t imask = (0 == carryi) ? 0 : DIG_MASK;
@@ -266,7 +266,7 @@ STATIC size_t mpn_and_neg(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, c
    assumes enough memory in i; assumes normalised j, k; assumes jlen >= klen
    can have i, j, k pointing to same memory
 */
-STATIC size_t mpn_or(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen) {
+static size_t mpn_or(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen) {
     mpz_dig_t *oidig = idig;
 
     jlen -= klen;
@@ -296,7 +296,7 @@ STATIC size_t mpn_or(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const
 
 #if MICROPY_OPT_MPZ_BITWISE
 
-STATIC size_t mpn_or_neg(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen,
+static size_t mpn_or_neg(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen,
     mpz_dbl_dig_t carryj, mpz_dbl_dig_t carryk) {
     mpz_dig_t *oidig = idig;
     mpz_dbl_dig_t carryi = 1;
@@ -326,7 +326,7 @@ STATIC size_t mpn_or_neg(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, co
 
 #else
 
-STATIC size_t mpn_or_neg(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen,
+static size_t mpn_or_neg(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen,
     mpz_dbl_dig_t carryi, mpz_dbl_dig_t carryj, mpz_dbl_dig_t carryk) {
     mpz_dig_t *oidig = idig;
     mpz_dig_t imask = (0 == carryi) ? 0 : DIG_MASK;
@@ -358,7 +358,7 @@ STATIC size_t mpn_or_neg(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, co
    assumes enough memory in i; assumes normalised j, k; assumes jlen >= klen
    can have i, j, k pointing to same memory
 */
-STATIC size_t mpn_xor(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen) {
+static size_t mpn_xor(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen) {
     mpz_dig_t *oidig = idig;
 
     jlen -= klen;
@@ -385,7 +385,7 @@ STATIC size_t mpn_xor(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const
    assumes enough memory in i; assumes normalised j, k; assumes length j >= length k
    can have i, j, k pointing to same memory
 */
-STATIC size_t mpn_xor_neg(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen,
+static size_t mpn_xor_neg(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, const mpz_dig_t *kdig, size_t klen,
     mpz_dbl_dig_t carryi, mpz_dbl_dig_t carryj, mpz_dbl_dig_t carryk) {
     mpz_dig_t *oidig = idig;
 
@@ -410,7 +410,7 @@ STATIC size_t mpn_xor_neg(mpz_dig_t *idig, const mpz_dig_t *jdig, size_t jlen, c
    returns number of digits in i
    assumes enough memory in i; assumes normalised i; assumes dmul != 0
 */
-STATIC size_t mpn_mul_dig_add_dig(mpz_dig_t *idig, size_t ilen, mpz_dig_t dmul, mpz_dig_t dadd) {
+static size_t mpn_mul_dig_add_dig(mpz_dig_t *idig, size_t ilen, mpz_dig_t dmul, mpz_dig_t dadd) {
     mpz_dig_t *oidig = idig;
     mpz_dbl_dig_t carry = dadd;
 
@@ -432,7 +432,7 @@ STATIC size_t mpn_mul_dig_add_dig(mpz_dig_t *idig, size_t ilen, mpz_dig_t dmul,
    assumes enough memory in i; assumes i is zeroed; assumes normalised j, k
    can have j, k point to same memory
 */
-STATIC size_t mpn_mul(mpz_dig_t *idig, mpz_dig_t *jdig, size_t jlen, mpz_dig_t *kdig, size_t klen) {
+static size_t mpn_mul(mpz_dig_t *idig, mpz_dig_t *jdig, size_t jlen, mpz_dig_t *kdig, size_t klen) {
     mpz_dig_t *oidig = idig;
     size_t ilen = 0;
 
@@ -463,7 +463,7 @@ STATIC size_t mpn_mul(mpz_dig_t *idig, mpz_dig_t *jdig, size_t jlen, mpz_dig_t *
    assumes quo_dig has enough memory (as many digits as num)
    assumes quo_dig is filled with zeros
 */
-STATIC void mpn_div(mpz_dig_t *num_dig, size_t *num_len, const mpz_dig_t *den_dig, size_t den_len, mpz_dig_t *quo_dig, size_t *quo_len) {
+static void mpn_div(mpz_dig_t *num_dig, size_t *num_len, const mpz_dig_t *den_dig, size_t den_len, mpz_dig_t *quo_dig, size_t *quo_len) {
     mpz_dig_t *orig_num_dig = num_dig;
     mpz_dig_t *orig_quo_dig = quo_dig;
     mpz_dig_t norm_shift = 0;
@@ -668,14 +668,14 @@ mpz_t *mpz_from_str(const char *str, size_t len, bool neg, unsigned int base) {
 }
 #endif
 
-STATIC void mpz_free(mpz_t *z) {
+static void mpz_free(mpz_t *z) {
     if (z != NULL) {
         m_del(mpz_dig_t, z->dig, z->alloc);
         m_del_obj(mpz_t, z);
     }
 }
 
-STATIC void mpz_need_dig(mpz_t *z, size_t need) {
+static void mpz_need_dig(mpz_t *z, size_t need) {
     if (need < MIN_ALLOC) {
         need = MIN_ALLOC;
     }
@@ -689,7 +689,7 @@ STATIC void mpz_need_dig(mpz_t *z, size_t need) {
     }
 }
 
-STATIC mpz_t *mpz_clone(const mpz_t *src) {
+static mpz_t *mpz_clone(const mpz_t *src) {
     assert(src->alloc != 0);
     mpz_t *z = m_new_obj(mpz_t);
     z->neg = src->neg;
@@ -1356,7 +1356,7 @@ void mpz_pow_inpl(mpz_t *dest, const mpz_t *lhs, const mpz_t *rhs) {
    can have dest, lhs, rhs the same; mod can't be the same as dest
 */
 void mpz_pow3_inpl(mpz_t *dest, const mpz_t *lhs, const mpz_t *rhs, const mpz_t *mod) {
-    if (lhs->len == 0 || rhs->neg != 0 || (mod->len == 1 && mod->dig[0] == 1)) {
+    if (rhs->neg != 0 || (mod->len == 1 && mod->dig[0] == 1)) {
         mpz_set_from_int(dest, 0);
         return;
     }
@@ -1364,6 +1364,15 @@ void mpz_pow3_inpl(mpz_t *dest, const mpz_t *lhs, const mpz_t *rhs, const mpz_t
     mpz_set_from_int(dest, 1);
 
     if (rhs->len == 0) {
+        // Python style modulo: 1 % mod is 1 + mod, as abs(mod) >= 2 here
+        if (mod->neg) {
+            mpz_add_inpl(dest, dest, mod);
+        }
+        return;
+    }
+
+    if (lhs->len == 0) {
+        mpz_set_from_int(dest, 0);
         return;
     }
 
@@ -1537,7 +1546,8 @@ mp_int_t mpz_hash(const mpz_t *z) {
     mp_uint_t val = 0;
     mpz_dig_t *d = z->dig + z->len;
 
-    while (d-- > z->dig) {
+    while (d > z->dig) {
+        d--;
         val = (val << DIG_SIZE) | *d;
     }
 
@@ -1552,11 +1562,12 @@ bool mpz_as_int_checked(const mpz_t *i, mp_int_t *value) {
     mp_uint_t val = 0;
     mpz_dig_t *d = i->dig + i->len;
 
-    while (d-- > i->dig) {
+    while (d > i->dig) {
         if (val > (~(MP_OBJ_WORD_MSBIT_HIGH) >> DIG_SIZE)) {
             // will overflow
             return false;
         }
+        d--;
         val = (val << DIG_SIZE) | *d;
     }
 
@@ -1577,11 +1588,12 @@ bool mpz_as_uint_checked(const mpz_t *i, mp_uint_t *value) {
     mp_uint_t val = 0;
     mpz_dig_t *d = i->dig + i->len;
 
-    while (d-- > i->dig) {
+    while (d > i->dig) {
         if (val > (~(MP_OBJ_WORD_MSBIT_HIGH) >> (DIG_SIZE - 1))) {
             // will overflow
             return false;
         }
+        d--;
         val = (val << DIG_SIZE) | *d;
     }
 
@@ -1589,46 +1601,56 @@ bool mpz_as_uint_checked(const mpz_t *i, mp_uint_t *value) {
     return true;
 }
 
-void mpz_as_bytes(const mpz_t *z, bool big_endian, size_t len, byte *buf) {
+bool mpz_as_bytes(const mpz_t *z, bool big_endian, bool as_signed, size_t len, byte *buf) {
     byte *b = buf;
     if (big_endian) {
         b += len;
     }
     mpz_dig_t *zdig = z->dig;
+    byte fill_byte = z->neg ? 0xFF : 0x00;
     int bits = 0;
     mpz_dbl_dig_t d = 0;
     mpz_dbl_dig_t carry = 1;
+    mpz_dig_t val = 0;
+    size_t olen = len; // bytes in output buffer
     for (size_t zlen = z->len; zlen > 0; --zlen) {
         bits += DIG_SIZE;
         d = (d << DIG_SIZE) | *zdig++;
         for (; bits >= 8; bits -= 8, d >>= 8) {
-            mpz_dig_t val = d;
+            val = d;
             if (z->neg) {
                 val = (~val & 0xff) + carry;
                 carry = val >> 8;
             }
+
+            if (!olen) {
+                // Buffer is full, only OK if all remaining bytes are zeroes
+                if ((byte)val != fill_byte) {
+                    return false;
+                }
+                continue;
+            }
+
             if (big_endian) {
                 *--b = val;
-                if (b == buf) {
-                    return;
-                }
             } else {
                 *b++ = val;
-                if (b == buf + len) {
-                    return;
-                }
             }
+            olen--;
         }
     }
 
-    // fill remainder of buf with zero/sign extension of the integer
-    if (big_endian) {
-        len = b - buf;
-    } else {
-        len = buf + len - b;
-        buf = b;
+    // Check if the most significant bit is set incorrectly for a signed value
+    if (olen == 0 && as_signed && ((val & 0x80) != (fill_byte & 0x80))) {
+        return false;
     }
-    memset(buf, z->neg ? 0xff : 0x00, len);
+
+    if (olen > 0) {
+        // fill remainder of buf with zero/sign extension of the integer
+        memset(big_endian ? buf : b, fill_byte, olen);
+    }
+
+    return true;
 }
 
 #if MICROPY_PY_BUILTINS_FLOAT
@@ -1636,7 +1658,8 @@ mp_float_t mpz_as_float(const mpz_t *i) {
     mp_float_t val = 0;
     mpz_dig_t *d = i->dig + i->len;
 
-    while (d-- > i->dig) {
+    while (d > i->dig) {
+        d--;
         val = val * DIG_BASE + *d;
     }
 
@@ -1666,6 +1689,8 @@ size_t mpz_as_str_inpl(const mpz_t *i, unsigned int base, const char *prefix, ch
 
     size_t ilen = i->len;
 
+    int n_comma = (base == 10) ? 3 : 4;
+
     char *s = str;
     if (ilen == 0) {
         if (prefix) {
@@ -1711,7 +1736,7 @@ size_t mpz_as_str_inpl(const mpz_t *i, unsigned int base, const char *prefix, ch
                 break;
             }
         }
-        if (comma && (s - last_comma) == 3) {
+        if (!done && comma && (s - last_comma) == n_comma) {
             *s++ = comma;
             last_comma = s;
         }
diff --git a/py/mpz.h b/py/mpz.h
index d27f5724047..6f1ac930b02 100644
--- a/py/mpz.h
+++ b/py/mpz.h
@@ -93,9 +93,9 @@ typedef int8_t mpz_dbl_dig_signed_t;
 typedef struct _mpz_t {
     // Zero has neg=0, len=0.  Negative zero is not allowed.
     size_t neg : 1;
-    size_t fixed_dig : 1;
-    size_t alloc : (8 * sizeof(size_t) - 2);
-    size_t len;
+    size_t fixed_dig : 1; // flag, 'dig' buffer cannot be reallocated
+    size_t alloc : (8 * sizeof(size_t) - 2); // number of entries allocated in 'dig'
+    size_t len; // number of entries used in 'dig'
     mpz_dig_t *dig;
 } mpz_t;
 
@@ -145,7 +145,8 @@ static inline size_t mpz_max_num_bits(const mpz_t *z) {
 mp_int_t mpz_hash(const mpz_t *z);
 bool mpz_as_int_checked(const mpz_t *z, mp_int_t *value);
 bool mpz_as_uint_checked(const mpz_t *z, mp_uint_t *value);
-void mpz_as_bytes(const mpz_t *z, bool big_endian, size_t len, byte *buf);
+// Returns true if 'z' fit into 'len' bytes of 'buf' without overflowing, 'buf' is truncated otherwise.
+bool mpz_as_bytes(const mpz_t *z, bool big_endian, bool as_signed, size_t len, byte *buf);
 #if MICROPY_PY_BUILTINS_FLOAT
 mp_float_t mpz_as_float(const mpz_t *z);
 #endif
diff --git a/py/nativeglue.c b/py/nativeglue.c
index 743ff38ccbb..e9164ee4c20 100644
--- a/py/nativeglue.c
+++ b/py/nativeglue.c
@@ -29,6 +29,7 @@
 #include 
 #include 
 
+#include "py/binary.h"
 #include "py/runtime.h"
 #include "py/smallint.h"
 #include "py/nativeglue.h"
@@ -40,7 +41,7 @@
 #define DEBUG_printf(...) (void)0
 #endif
 
-#if MICROPY_EMIT_NATIVE
+#if MICROPY_ENABLE_NATIVE_CODE
 
 int mp_native_type_from_qstr(qstr qst) {
     switch (qst) {
@@ -90,7 +91,7 @@ mp_uint_t mp_native_from_obj(mp_obj_t obj, mp_uint_t type) {
 
 #endif
 
-#if MICROPY_EMIT_MACHINE_CODE
+#if MICROPY_EMIT_INLINE_ASM || MICROPY_ENABLE_NATIVE_CODE
 
 // convert a native value to a MicroPython object based on type
 mp_obj_t mp_native_to_obj(mp_uint_t val, mp_uint_t type) {
@@ -104,6 +105,8 @@ mp_obj_t mp_native_to_obj(mp_uint_t val, mp_uint_t type) {
             return mp_obj_new_int(val);
         case MP_NATIVE_TYPE_UINT:
             return mp_obj_new_int_from_uint(val);
+        case MP_NATIVE_TYPE_QSTR:
+            return MP_OBJ_NEW_QSTR(val);
         default: // a pointer
             // we return just the value of the pointer as an integer
             return mp_obj_new_int_from_uint(val);
@@ -112,7 +115,7 @@ mp_obj_t mp_native_to_obj(mp_uint_t val, mp_uint_t type) {
 
 #endif
 
-#if MICROPY_EMIT_NATIVE && !MICROPY_DYNAMIC_COMPILER
+#if MICROPY_ENABLE_NATIVE_CODE && !MICROPY_DYNAMIC_COMPILER
 
 #if !MICROPY_PY_BUILTINS_SET
 mp_obj_t mp_obj_new_set(size_t n_args, mp_obj_t *items) {
@@ -137,7 +140,7 @@ mp_obj_t mp_obj_new_slice(mp_obj_t ostart, mp_obj_t ostop, mp_obj_t ostep) {
 }
 #endif
 
-STATIC mp_obj_dict_t *mp_native_swap_globals(mp_obj_dict_t *new_globals) {
+static mp_obj_dict_t *mp_native_swap_globals(mp_obj_dict_t *new_globals) {
     if (new_globals == NULL) {
         // Globals were the originally the same so don't restore them
         return NULL;
@@ -153,20 +156,20 @@ STATIC mp_obj_dict_t *mp_native_swap_globals(mp_obj_dict_t *new_globals) {
 
 // wrapper that accepts n_args and n_kw in one argument
 // (native emitter can only pass at most 3 arguments to a function)
-STATIC mp_obj_t mp_native_call_function_n_kw(mp_obj_t fun_in, size_t n_args_kw, const mp_obj_t *args) {
+static mp_obj_t mp_native_call_function_n_kw(mp_obj_t fun_in, size_t n_args_kw, const mp_obj_t *args) {
     return mp_call_function_n_kw(fun_in, n_args_kw & 0xff, (n_args_kw >> 8) & 0xff, args);
 }
 
 // wrapper that makes raise obj and raises it
 // END_FINALLY opcode requires that we don't raise if o==None
-STATIC void mp_native_raise(mp_obj_t o) {
+static void mp_native_raise(mp_obj_t o) {
     if (o != MP_OBJ_NULL && o != mp_const_none) {
         nlr_raise(mp_make_raise_obj(o));
     }
 }
 
 // wrapper that handles iterator buffer
-STATIC mp_obj_t mp_native_getiter(mp_obj_t obj, mp_obj_iter_buf_t *iter) {
+static mp_obj_t mp_native_getiter(mp_obj_t obj, mp_obj_iter_buf_t *iter) {
     if (iter == NULL) {
         return mp_getiter(obj, NULL);
     } else {
@@ -181,7 +184,7 @@ STATIC mp_obj_t mp_native_getiter(mp_obj_t obj, mp_obj_iter_buf_t *iter) {
 }
 
 // wrapper that handles iterator buffer
-STATIC mp_obj_t mp_native_iternext(mp_obj_iter_buf_t *iter) {
+static mp_obj_t mp_native_iternext(mp_obj_iter_buf_t *iter) {
     mp_obj_t obj;
     if (iter->base.type == MP_OBJ_NULL) {
         obj = iter->buf[0];
@@ -191,7 +194,7 @@ STATIC mp_obj_t mp_native_iternext(mp_obj_iter_buf_t *iter) {
     return mp_iternext(obj);
 }
 
-STATIC bool mp_native_yield_from(mp_obj_t gen, mp_obj_t send_value, mp_obj_t *ret_value) {
+static bool mp_native_yield_from(mp_obj_t gen, mp_obj_t send_value, mp_obj_t *ret_value) {
     mp_vm_return_kind_t ret_kind;
     nlr_buf_t nlr_buf;
     mp_obj_t throw_value = *ret_value;
@@ -229,22 +232,22 @@ STATIC bool mp_native_yield_from(mp_obj_t gen, mp_obj_t send_value, mp_obj_t *re
 
 #if !MICROPY_PY_BUILTINS_FLOAT
 
-STATIC mp_obj_t mp_obj_new_float_from_f(float f) {
+static mp_obj_t mp_obj_new_float_from_f(float f) {
     (void)f;
     mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("float unsupported"));
 }
 
-STATIC mp_obj_t mp_obj_new_float_from_d(double d) {
+static mp_obj_t mp_obj_new_float_from_d(double d) {
     (void)d;
     mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("float unsupported"));
 }
 
-STATIC float mp_obj_get_float_to_f(mp_obj_t o) {
+static float mp_obj_get_float_to_f(mp_obj_t o) {
     (void)o;
     mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("float unsupported"));
 }
 
-STATIC double mp_obj_get_float_to_d(mp_obj_t o) {
+static double mp_obj_get_float_to_d(mp_obj_t o) {
     (void)o;
     mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("float unsupported"));
 }
@@ -279,7 +282,7 @@ const mp_fun_table_t mp_fun_table = {
     mp_obj_set_store,
     mp_obj_list_append,
     mp_obj_dict_store,
-    mp_make_function_from_raw_code,
+    mp_make_function_from_proto_fun,
     mp_native_call_function_n_kw,
     mp_call_method_n_kw,
     mp_call_method_n_kw_var,
@@ -317,7 +320,11 @@ const mp_fun_table_t mp_fun_table = {
     gc_realloc,
     mp_printf,
     mp_vprintf,
+    #if MICROPY_ERROR_REPORTING == MICROPY_ERROR_REPORTING_NONE
+    NULL,
+    #else
     mp_raise_msg,
+    #endif
     mp_obj_get_type,
     mp_obj_new_str,
     mp_obj_new_bytes,
@@ -326,8 +333,14 @@ const mp_fun_table_t mp_fun_table = {
     mp_obj_new_float_from_d,
     mp_obj_get_float_to_f,
     mp_obj_get_float_to_d,
-    mp_get_buffer_raise,
+    mp_load_method_maybe,
+    mp_get_buffer,
     mp_get_stream_raise,
+    mp_arg_parse_all,
+    mp_arg_parse_all_kw_array,
+    mp_binary_get_size,
+    mp_binary_get_val_array,
+    mp_binary_set_val_array,
     &mp_plat_print,
     &mp_type_type,
     &mp_type_str,
@@ -338,14 +351,15 @@ const mp_fun_table_t mp_fun_table = {
     &mp_type_fun_builtin_2,
     &mp_type_fun_builtin_3,
     &mp_type_fun_builtin_var,
+    &mp_type_Exception,
     &mp_stream_read_obj,
     &mp_stream_readinto_obj,
     &mp_stream_unbuffered_readline_obj,
     &mp_stream_write_obj,
 };
 
-#elif MICROPY_EMIT_NATIVE && MICROPY_DYNAMIC_COMPILER
+#elif MICROPY_ENABLE_NATIVE_CODE && MICROPY_DYNAMIC_COMPILER
 
 const int mp_fun_table;
 
-#endif // MICROPY_EMIT_NATIVE
+#endif // MICROPY_ENABLE_NATIVE_CODE
diff --git a/py/nativeglue.h b/py/nativeglue.h
index 7b1ccd8d464..92d3889df39 100644
--- a/py/nativeglue.h
+++ b/py/nativeglue.h
@@ -58,7 +58,7 @@ typedef enum {
     MP_F_STORE_SET,
     MP_F_LIST_APPEND,
     MP_F_STORE_MAP,
-    MP_F_MAKE_FUNCTION_FROM_RAW_CODE,
+    MP_F_MAKE_FUNCTION_FROM_PROTO_FUN,
     MP_F_NATIVE_CALL_FUNCTION_N_KW,
     MP_F_CALL_METHOD_N_KW,
     MP_F_CALL_METHOD_N_KW_VAR,
@@ -112,7 +112,7 @@ typedef struct _mp_fun_table_t {
     void (*set_store)(mp_obj_t self_in, mp_obj_t item);
     mp_obj_t (*list_append)(mp_obj_t self_in, mp_obj_t arg);
     mp_obj_t (*dict_store)(mp_obj_t self_in, mp_obj_t key, mp_obj_t value);
-    mp_obj_t (*make_function_from_raw_code)(const mp_raw_code_t *rc, const mp_module_context_t *cm, const mp_obj_t *def_args);
+    mp_obj_t (*make_function_from_proto_fun)(mp_proto_fun_t proto_fun, const mp_module_context_t *cm, const mp_obj_t *def_args);
     mp_obj_t (*call_function_n_kw)(mp_obj_t fun_in, size_t n_args_kw, const mp_obj_t *args);
     mp_obj_t (*call_method_n_kw)(size_t n_args, size_t n_kw, const mp_obj_t *args);
     mp_obj_t (*call_method_n_kw_var)(bool have_self, size_t n_args_n_kw, const mp_obj_t *args);
@@ -143,7 +143,7 @@ typedef struct _mp_fun_table_t {
     int (*printf_)(const mp_print_t *print, const char *fmt, ...);
     int (*vprintf_)(const mp_print_t *print, const char *fmt, va_list args);
     #if defined(__GNUC__)
-    NORETURN // Only certain compilers support no-return attributes in function pointer declarations
+    MP_NORETURN // Only certain compilers support no-return attributes in function pointer declarations
     #endif
     void (*raise_msg)(const mp_obj_type_t *exc_type, mp_rom_error_text_t msg);
     const mp_obj_type_t *(*obj_get_type)(mp_const_obj_t o_in);
@@ -154,9 +154,17 @@ typedef struct _mp_fun_table_t {
     mp_obj_t (*obj_new_float_from_d)(double d);
     float (*obj_get_float_to_f)(mp_obj_t o);
     double (*obj_get_float_to_d)(mp_obj_t o);
-    void (*get_buffer_raise)(mp_obj_t obj, mp_buffer_info_t *bufinfo, mp_uint_t flags);
+    void (*load_method_maybe)(mp_obj_t base, qstr attr, mp_obj_t *dest);
+    bool (*get_buffer)(mp_obj_t obj, mp_buffer_info_t *bufinfo, mp_uint_t flags);
     const mp_stream_p_t *(*get_stream_raise)(mp_obj_t self_in, int flags);
+    void (*arg_parse_all)(size_t n_pos, const mp_obj_t *pos, mp_map_t *kws, size_t n_allowed, const mp_arg_t *allowed, mp_arg_val_t *out_vals);
+    void (*arg_parse_all_kw_array)(size_t n_pos, size_t n_kw, const mp_obj_t *args, size_t n_allowed, const mp_arg_t *allowed, mp_arg_val_t *out_vals);
+    size_t (*binary_get_size)(char struct_type, char val_type, size_t *palign);
+    mp_obj_t (*binary_get_val_array)(char typecode, void *p, size_t index);
+    void (*binary_set_val_array)(char typecode, void *p, size_t index, mp_obj_t val_in);
     const mp_print_t *plat_print;
+    // The following entries start at index 73 and are referenced by tools-mpy_ld.py,
+    // see constant MP_FUN_TABLE_MP_TYPE_TYPE_OFFSET.
     const mp_obj_type_t *type_type;
     const mp_obj_type_t *type_str;
     const mp_obj_type_t *type_list;
@@ -166,15 +174,16 @@ typedef struct _mp_fun_table_t {
     const mp_obj_type_t *type_fun_builtin_2;
     const mp_obj_type_t *type_fun_builtin_3;
     const mp_obj_type_t *type_fun_builtin_var;
+    const mp_obj_type_t *type_Exception;
     const mp_obj_fun_builtin_var_t *stream_read_obj;
     const mp_obj_fun_builtin_var_t *stream_readinto_obj;
     const mp_obj_fun_builtin_var_t *stream_unbuffered_readline_obj;
     const mp_obj_fun_builtin_var_t *stream_write_obj;
 } mp_fun_table_t;
 
-#if (MICROPY_EMIT_NATIVE && !MICROPY_DYNAMIC_COMPILER) || MICROPY_ENABLE_DYNRUNTIME
+#if (MICROPY_ENABLE_NATIVE_CODE && !MICROPY_DYNAMIC_COMPILER) || MICROPY_ENABLE_DYNRUNTIME
 extern const mp_fun_table_t mp_fun_table;
-#elif MICROPY_EMIT_NATIVE && MICROPY_DYNAMIC_COMPILER
+#elif MICROPY_ENABLE_NATIVE_CODE && MICROPY_DYNAMIC_COMPILER
 // In dynamic-compiler mode eliminate dependency on entries in mp_fun_table.
 // This only needs to be an independent pointer, content doesn't matter.
 extern const int mp_fun_table;
diff --git a/py/nlr.c b/py/nlr.c
index 7ab0c0955a2..de2a38ceff3 100644
--- a/py/nlr.c
+++ b/py/nlr.c
@@ -81,7 +81,7 @@ void nlr_call_jump_callbacks(nlr_buf_t *nlr) {
 }
 
 #if MICROPY_ENABLE_VM_ABORT
-NORETURN void nlr_jump_abort(void) {
+MP_NORETURN void nlr_jump_abort(void) {
     MP_STATE_THREAD(nlr_top) = MP_STATE_VM(nlr_abort);
     nlr_jump(NULL);
 }
diff --git a/py/nlr.h b/py/nlr.h
index 62972dba627..49560ec0561 100644
--- a/py/nlr.h
+++ b/py/nlr.h
@@ -44,6 +44,9 @@
 #define MICROPY_NLR_NUM_REGS_MIPS           (13)
 #define MICROPY_NLR_NUM_REGS_XTENSA         (10)
 #define MICROPY_NLR_NUM_REGS_XTENSAWIN      (17)
+#define MICROPY_NLR_NUM_REGS_RV32I          (14)
+#define MICROPY_NLR_NUM_REGS_RV64I          (14)
+#define MICROPY_NLR_NUM_REGS_LOONG64        (13)
 
 // *FORMAT-OFF*
 
@@ -88,6 +91,23 @@
 #elif defined(__mips__)
     #define MICROPY_NLR_MIPS (1)
     #define MICROPY_NLR_NUM_REGS (MICROPY_NLR_NUM_REGS_MIPS)
+#elif defined(__riscv)
+    #if __riscv_xlen == 32
+        #define MICROPY_NLR_NUM_REGS (MICROPY_NLR_NUM_REGS_RV32I)
+        #define MICROPY_NLR_RV32I (1)
+    #elif __riscv_xlen == 64
+        #define MICROPY_NLR_NUM_REGS (MICROPY_NLR_NUM_REGS_RV64I)
+        #define MICROPY_NLR_RV64I (1)
+    #else
+        #error Unsupported RISC-V variant.
+    #endif
+#elif defined(__loongarch__)
+    #if defined(__loongarch64)
+        #define MICROPY_NLR_LOONG64 (1)
+        #define MICROPY_NLR_NUM_REGS (MICROPY_NLR_NUM_REGS_LOONG64)
+    #else
+        #error Unsupported Loongarch variant.
+    #endif
 #else
     #define MICROPY_NLR_SETJMP (1)
     //#warning "No native NLR support for this arch, using setjmp implementation"
@@ -165,18 +185,18 @@ unsigned int nlr_push(nlr_buf_t *);
 
 unsigned int nlr_push_tail(nlr_buf_t *top);
 void nlr_pop(void);
-NORETURN void nlr_jump(void *val);
+MP_NORETURN void nlr_jump(void *val);
 
 #if MICROPY_ENABLE_VM_ABORT
 #define nlr_set_abort(buf) MP_STATE_VM(nlr_abort) = buf
 #define nlr_get_abort() MP_STATE_VM(nlr_abort)
-NORETURN void nlr_jump_abort(void);
+MP_NORETURN void nlr_jump_abort(void);
 #endif
 
 // This must be implemented by a port.  It's called by nlr_jump
 // if no nlr buf has been pushed.  It must not return, but rather
 // should bail out with a fatal error.
-NORETURN void nlr_jump_fail(void *val);
+MP_NORETURN void nlr_jump_fail(void *val);
 
 // use nlr_raise instead of nlr_jump so that debugging is easier
 #ifndef MICROPY_DEBUG_NLR
diff --git a/py/nlraarch64.c b/py/nlraarch64.c
index 1295351cbe8..3318004b5e0 100644
--- a/py/nlraarch64.c
+++ b/py/nlraarch64.c
@@ -56,25 +56,26 @@ __asm(
     #endif
     );
 
-NORETURN void nlr_jump(void *val) {
+MP_NORETURN void nlr_jump(void *val) {
     MP_NLR_JUMP_HEAD(val, top)
 
     MP_STATIC_ASSERT(offsetof(nlr_buf_t, regs) == 16); // asm assumes it
 
     __asm volatile (
-        "ldr x29,      [%0, #112]\n"
-        "ldp x27, x28, [%0,  #96]\n"
-        "ldp x25, x26, [%0,  #80]\n"
-        "ldp x23, x24, [%0,  #64]\n"
-        "ldp x21, x22, [%0,  #48]\n"
-        "ldp x19, x20, [%0,  #32]\n"
-        "ldp lr,  x9,  [%0,  #16]\n" // 16 == offsetof(nlr_buf_t, regs)
+        "mov x0, %0              \n"
+        "ldr x29,      [x0, #112]\n"
+        "ldp x27, x28, [x0,  #96]\n"
+        "ldp x25, x26, [x0,  #80]\n"
+        "ldp x23, x24, [x0,  #64]\n"
+        "ldp x21, x22, [x0,  #48]\n"
+        "ldp x19, x20, [x0,  #32]\n"
+        "ldp lr,  x9,  [x0,  #16]\n" // 16 == offsetof(nlr_buf_t, regs)
         "mov sp, x9              \n"
         "mov x0, #1              \n"  // non-local return
         "ret                     \n"
         :
         : "r" (top)
-        :
+        : "memory"
         );
 
     MP_UNREACHABLE
diff --git a/py/nlrloong64.c b/py/nlrloong64.c
new file mode 100644
index 00000000000..7a12b2ec904
--- /dev/null
+++ b/py/nlrloong64.c
@@ -0,0 +1,80 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2026 Alessandro Gatti
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "py/mpstate.h"
+
+#if MICROPY_NLR_LOONG64
+
+__attribute__((used)) unsigned int nlr_push_tail(nlr_buf_t *nlr);
+
+__asm(
+    ".globl nlr_push     \n"
+    "nlr_push:           \n"
+    ".cfi_startproc      \n"
+    "st.d $r1,  $r4, 16  \n" /* Store RA. */
+    "st.d $r23, $r4, 24  \n" /* Store S0. */
+    "st.d $r24, $r4, 32  \n" /* Store S1. */
+    "st.d $r25, $r4, 40  \n" /* Store S2. */
+    "st.d $r26, $r4, 48  \n" /* Store S3. */
+    "st.d $r27, $r4, 56  \n" /* Store S4. */
+    "st.d $r28, $r4, 64  \n" /* Store S5. */
+    "st.d $r29, $r4, 72  \n" /* Store S6. */
+    "st.d $r30, $r4, 80  \n" /* Store S7. */
+    "st.d $r31, $r4, 88  \n" /* Store S8. */
+    "st.d $r22, $r4, 96  \n" /* Store S9. */
+    "st.d $r21, $r4, 104 \n" /* Marked as reserved in the ABI. */
+    "st.d $r3,  $r4, 112 \n" /* Store SP. */
+    "b    nlr_push_tail  \n" /* Jump to the C part. */
+    ".cfi_endproc        \n"
+    );
+
+MP_NORETURN void nlr_jump(void *val) {
+    MP_NLR_JUMP_HEAD(val, top)
+    __asm volatile (
+        "add.d  $r4,  $r0, %0  \n"
+        "ld.d   $r1,  $r4, 16  \n" /* Retrieve RA. */
+        "ld.d   $r23, $r4, 24  \n" /* Retrieve S0. */
+        "ld.d   $r24, $r4, 32  \n" /* Retrieve S1. */
+        "ld.d   $r25, $r4, 40  \n" /* Retrieve S2. */
+        "ld.d   $r26, $r4, 48  \n" /* Retrieve S3. */
+        "ld.d   $r27, $r4, 56  \n" /* Retrieve S4. */
+        "ld.d   $r28, $r4, 64  \n" /* Retrieve S5. */
+        "ld.d   $r29, $r4, 72  \n" /* Retrieve S6. */
+        "ld.d   $r30, $r4, 80  \n" /* Retrieve S7. */
+        "ld.d   $r31, $r4, 88  \n" /* Retrieve S8. */
+        "ld.d   $r22, $r4, 96  \n" /* Retrieve S9. */
+        "ld.d   $r21, $r4, 104 \n"
+        "ld.d   $r3,  $r4, 112 \n" /* Retrieve SP. */
+        "addi.d $r4,  $r0, 1   \n" /* Return 1 for a non-local return. */
+        "ret                   \n" /* Return. */
+        :
+        : "r" (top)
+        : "memory"
+        );
+    MP_UNREACHABLE
+}
+
+#endif
diff --git a/py/nlrmips.c b/py/nlrmips.c
index bd5d73b6f76..5c55db7e268 100644
--- a/py/nlrmips.c
+++ b/py/nlrmips.c
@@ -57,7 +57,7 @@ __asm(
     ".end nlr_push            \n"
     );
 
-NORETURN void nlr_jump(void *val) {
+MP_NORETURN void nlr_jump(void *val) {
     MP_NLR_JUMP_HEAD(val, top)
     __asm(
         "move $4, %0    \n"
@@ -78,7 +78,7 @@ NORETURN void nlr_jump(void *val) {
         "nop            \n"
         :
         : "r" (top)
-        :
+        : "memory"
         );
     MP_UNREACHABLE
 }
diff --git a/py/nlrpowerpc.c b/py/nlrpowerpc.c
index 448459216b6..cb94473968d 100644
--- a/py/nlrpowerpc.c
+++ b/py/nlrpowerpc.c
@@ -72,49 +72,50 @@ unsigned int nlr_push(nlr_buf_t *nlr) {
         "bctr  ;"
         :
         : "r" (&nlr->regs), "r" (nlr)
-        :
+        : "r4"
         );
 
     return 0;
 }
 
-NORETURN void nlr_jump(void *val) {
+MP_NORETURN void nlr_jump(void *val) {
     MP_NLR_JUMP_HEAD(val, top)
 
     __asm__ volatile (
-        "ld    3, 0x0(%0) ;"
+        "mr    4, %0 ;"
+        "ld    3, 0x0(4) ;"
         "cmpdi 3, 0x4eed ; " // Check canary
         "bne   . ; "
-        "ld    0,  0x08(%0) ;"
-        "ld    1,  0x10(%0) ;"
-        "ld    2,  0x18(%0) ;"
-        "ld    14, 0x20(%0) ;"
-        "ld    15, 0x28(%0) ;"
-        "ld    16, 0x30(%0) ;"
-        "ld    17, 0x38(%0) ;"
-        "ld    18, 0x40(%0) ;"
-        "ld    19, 0x48(%0) ;"
-        "ld    20, 0x50(%0) ;"
-        "ld    21, 0x58(%0) ;"
-        "ld    22, 0x60(%0) ;"
-        "ld    23, 0x68(%0) ;"
-        "ld    24, 0x70(%0) ;"
-        "ld    25, 0x78(%0) ;"
-        "ld    26, 0x80(%0) ;"
-        "ld    27, 0x88(%0) ;"
-        "ld    28, 0x90(%0) ;"
-        "ld    29, 0x98(%0) ;"
-        "ld    30, 0xA0(%0) ;"
-        "ld    31, 0xA8(%0) ;"
-        "ld    3,  0xB0(%0) ;"
+        "ld    0,  0x08(4) ;"
+        "ld    1,  0x10(4) ;"
+        "ld    2,  0x18(4) ;"
+        "ld    14, 0x20(4) ;"
+        "ld    15, 0x28(4) ;"
+        "ld    16, 0x30(4) ;"
+        "ld    17, 0x38(4) ;"
+        "ld    18, 0x40(4) ;"
+        "ld    19, 0x48(4) ;"
+        "ld    20, 0x50(4) ;"
+        "ld    21, 0x58(4) ;"
+        "ld    22, 0x60(4) ;"
+        "ld    23, 0x68(4) ;"
+        "ld    24, 0x70(4) ;"
+        "ld    25, 0x78(4) ;"
+        "ld    26, 0x80(4) ;"
+        "ld    27, 0x88(4) ;"
+        "ld    28, 0x90(4) ;"
+        "ld    29, 0x98(4) ;"
+        "ld    30, 0xA0(4) ;"
+        "ld    31, 0xA8(4) ;"
+        "ld    3,  0xB0(4) ;"
         "mtcr  3 ;"
-        "ld    3, 0xB8(%0) ;"
+        "ld    3, 0xB8(4) ;"
         "mtlr  3 ; "
         "li    3, 1;"
         "blr ;"
         :
         : "r" (&top->regs)
-        :
+        : "memory"
         );
 
     MP_UNREACHABLE;
@@ -161,49 +162,50 @@ unsigned int nlr_push(nlr_buf_t *nlr) {
         "bctr  ;"
         :
         : "r" (&nlr->regs), "r" (nlr)
-        :
+        : "r4"
         );
 
     return 0;
 }
 
-NORETURN void nlr_jump(void *val) {
+MP_NORETURN void nlr_jump(void *val) {
     MP_NLR_JUMP_HEAD(val, top)
 
     __asm__ volatile (
-        "l    3, 0x0(%0) ;"
+        "mr    4, %0 ;"
+        "l    3, 0x0(4) ;"
         "cmpdi 3, 0x4eed ; " // Check canary
         "bne   . ; "
-        "l    0,  0x04(%0) ;"
-        "l    1,  0x08(%0) ;"
-        "l    2,  0x0c(%0) ;"
-        "l    14, 0x10(%0) ;"
-        "l    15, 0x14(%0) ;"
-        "l    16, 0x18(%0) ;"
-        "l    17, 0x1c(%0) ;"
-        "l    18, 0x20(%0) ;"
-        "l    19, 0x24(%0) ;"
-        "l    20, 0x28(%0) ;"
-        "l    21, 0x2c(%0) ;"
-        "l    22, 0x30(%0) ;"
-        "l    23, 0x34(%0) ;"
-        "l    24, 0x38(%0) ;"
-        "l    25, 0x3c(%0) ;"
-        "l    26, 0x40(%0) ;"
-        "l    27, 0x44(%0) ;"
-        "l    28, 0x48(%0) ;"
-        "l    29, 0x4c(%0) ;"
-        "l    30, 0x50(%0) ;"
-        "l    31, 0x54(%0) ;"
-        "l    3,  0x58(%0) ;"
+        "l    0,  0x04(4) ;"
+        "l    1,  0x08(4) ;"
+        "l    2,  0x0c(4) ;"
+        "l    14, 0x10(4) ;"
+        "l    15, 0x14(4) ;"
+        "l    16, 0x18(4) ;"
+        "l    17, 0x1c(4) ;"
+        "l    18, 0x20(4) ;"
+        "l    19, 0x24(4) ;"
+        "l    20, 0x28(4) ;"
+        "l    21, 0x2c(4) ;"
+        "l    22, 0x30(4) ;"
+        "l    23, 0x34(4) ;"
+        "l    24, 0x38(4) ;"
+        "l    25, 0x3c(4) ;"
+        "l    26, 0x40(4) ;"
+        "l    27, 0x44(4) ;"
+        "l    28, 0x48(4) ;"
+        "l    29, 0x4c(4) ;"
+        "l    30, 0x50(4) ;"
+        "l    31, 0x54(4) ;"
+        "l    3,  0x58(4) ;"
         "mtcr  3 ;"
-        "l    3, 0x5c(%0) ;"
+        "l    3, 0x5c(4) ;"
         "mtlr  3 ; "
         "li    3, 1;"
         "blr ;"
         :
         : "r" (&top->regs)
-        :
+        : "memory"
         );
 
     MP_UNREACHABLE;
diff --git a/py/nlrrv32.c b/py/nlrrv32.c
new file mode 100644
index 00000000000..565a8629db5
--- /dev/null
+++ b/py/nlrrv32.c
@@ -0,0 +1,81 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2024 Alessandro Gatti
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "py/mpstate.h"
+
+#if MICROPY_NLR_RV32I
+
+#undef nlr_push
+
+__attribute__((naked)) unsigned int nlr_push(nlr_buf_t *nlr) {
+    __asm volatile (
+        "sw   x1,  8(x10)       \n" // Store RA.
+        "sw   x8, 12(x10)       \n" // Store S0.
+        "sw   x9, 16(x10)       \n" // Store S1.
+        "sw  x18, 20(x10)       \n" // Store S2.
+        "sw  x19, 24(x10)       \n" // Store S3.
+        "sw  x20, 28(x10)       \n" // Store S4.
+        "sw  x21, 32(x10)       \n" // Store S5.
+        "sw  x22, 36(x10)       \n" // Store S6.
+        "sw  x23, 40(x10)       \n" // Store S7.
+        "sw  x24, 44(x10)       \n" // Store S8.
+        "sw  x25, 48(x10)       \n" // Store S9.
+        "sw  x26, 52(x10)       \n" // Store S10.
+        "sw  x27, 56(x10)       \n" // Store S11.
+        "sw   x2, 60(x10)       \n" // Store SP.
+        "jal  x0, nlr_push_tail \n" // Jump to the C part.
+        );
+}
+
+MP_NORETURN void nlr_jump(void *val) {
+    MP_NLR_JUMP_HEAD(val, top)
+    __asm volatile (
+        "add  x10, x0, %0  \n" // Load nlr_buf address.
+        "lw   x1,  8(x10)  \n" // Retrieve RA.
+        "lw   x8, 12(x10)  \n" // Retrieve S0.
+        "lw   x9, 16(x10)  \n" // Retrieve S1.
+        "lw  x18, 20(x10)  \n" // Retrieve S2.
+        "lw  x19, 24(x10)  \n" // Retrieve S3.
+        "lw  x20, 28(x10)  \n" // Retrieve S4.
+        "lw  x21, 32(x10)  \n" // Retrieve S5.
+        "lw  x22, 36(x10)  \n" // Retrieve S6.
+        "lw  x23, 40(x10)  \n" // Retrieve S7.
+        "lw  x24, 44(x10)  \n" // Retrieve S8.
+        "lw  x25, 48(x10)  \n" // Retrieve S9.
+        "lw  x26, 52(x10)  \n" // Retrieve S10.
+        "lw  x27, 56(x10)  \n" // Retrieve S11.
+        "lw   x2, 60(x10)  \n" // Retrieve SP.
+        "addi x10, x0, 1   \n" // Return 1 for a non-local return.
+        "jalr  x0, x1, 0   \n" // Return.
+        :                      // Outputs.
+        : "r" (top)            // Inputs.
+        : "memory"             // Clobbered.
+        );
+
+    MP_UNREACHABLE
+}
+
+#endif // MICROPY_NLR_RV32I
diff --git a/py/nlrrv64.c b/py/nlrrv64.c
new file mode 100644
index 00000000000..b7d1467b8f6
--- /dev/null
+++ b/py/nlrrv64.c
@@ -0,0 +1,81 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2024 Alessandro Gatti
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "py/mpstate.h"
+
+#if MICROPY_NLR_RV64I
+
+#undef nlr_push
+
+__attribute__((naked)) unsigned int nlr_push(nlr_buf_t *nlr) {
+    __asm volatile (
+        "sd   x1, 16(x10)       \n" // Store RA.
+        "sd   x8, 24(x10)       \n" // Store S0.
+        "sd   x9, 32(x10)       \n" // Store S1.
+        "sd  x18, 40(x10)       \n" // Store S2.
+        "sd  x19, 48(x10)       \n" // Store S3.
+        "sd  x20, 56(x10)       \n" // Store S4.
+        "sd  x21, 64(x10)       \n" // Store S5.
+        "sd  x22, 72(x10)       \n" // Store S6.
+        "sd  x23, 80(x10)       \n" // Store S7.
+        "sd  x24, 88(x10)       \n" // Store S8.
+        "sd  x25, 96(x10)       \n" // Store S9.
+        "sd  x26, 104(x10)      \n" // Store S10.
+        "sd  x27, 112(x10)      \n" // Store S11.
+        "sd   x2, 120(x10)      \n" // Store SP.
+        "jal  x0, nlr_push_tail \n" // Jump to the C part.
+        );
+}
+
+MP_NORETURN void nlr_jump(void *val) {
+    MP_NLR_JUMP_HEAD(val, top)
+    __asm volatile (
+        "add  x10, x0, %0  \n" // Load nlr_buf address.
+        "ld   x1, 16(x10)  \n" // Retrieve RA.
+        "ld   x8, 24(x10)  \n" // Retrieve S0.
+        "ld   x9, 32(x10)  \n" // Retrieve S1.
+        "ld  x18, 40(x10)  \n" // Retrieve S2.
+        "ld  x19, 48(x10)  \n" // Retrieve S3.
+        "ld  x20, 56(x10)  \n" // Retrieve S4.
+        "ld  x21, 64(x10)  \n" // Retrieve S5.
+        "ld  x22, 72(x10)  \n" // Retrieve S6.
+        "ld  x23, 80(x10)  \n" // Retrieve S7.
+        "ld  x24, 88(x10)  \n" // Retrieve S8.
+        "ld  x25, 96(x10)  \n" // Retrieve S9.
+        "ld  x26, 104(x10) \n" // Retrieve S10.
+        "ld  x27, 112(x10) \n" // Retrieve S11.
+        "ld   x2, 120(x10) \n" // Retrieve SP.
+        "addi x10, x0, 1   \n" // Return 1 for a non-local return.
+        "jalr  x0, x1, 0   \n" // Return.
+        :                      // Outputs.
+        : "r" (top)            // Inputs.
+        : "memory"             // Clobbered.
+        );
+
+    MP_UNREACHABLE
+}
+
+#endif // MICROPY_NLR_RV64I
diff --git a/py/nlrthumb.c b/py/nlrthumb.c
index a8ffecc4703..8546308a3de 100644
--- a/py/nlrthumb.c
+++ b/py/nlrthumb.c
@@ -38,6 +38,14 @@
 
 __attribute__((naked)) unsigned int nlr_push(nlr_buf_t *nlr) {
 
+    // If you get a linker error here, indicating that a relocation doesn't
+    // fit, try the following (in that order):
+    //
+    // 1. Ensure that nlr.o nlrthumb.o are linked closely together, i.e.
+    //    there aren't too many other files between them in the linker list
+    //    (PY_CORE_O_BASENAME in py/py.mk)
+    // 2. Set -DMICROPY_NLR_THUMB_USE_LONG_JUMP=1 during the build
+    //
     __asm volatile (
         "str    r4, [r0, #12]       \n" // store r4 into nlr_buf
         "str    r5, [r0, #16]       \n" // store r5 into nlr_buf
@@ -71,7 +79,7 @@ __attribute__((naked)) unsigned int nlr_push(nlr_buf_t *nlr) {
         "str    lr, [r0, #8]        \n" // store lr into nlr_buf
         #endif
 
-        #if !defined(__thumb2__)
+        #if MICROPY_NLR_THUMB_USE_LONG_JUMP
         "ldr    r1, nlr_push_tail_var \n"
         "bx     r1                  \n" // do the rest in C
         ".align 2                   \n"
@@ -92,7 +100,7 @@ __attribute__((naked)) unsigned int nlr_push(nlr_buf_t *nlr) {
     #endif
 }
 
-NORETURN void nlr_jump(void *val) {
+MP_NORETURN void nlr_jump(void *val) {
     MP_NLR_JUMP_HEAD(val, top)
 
     __asm volatile (
@@ -132,7 +140,7 @@ NORETURN void nlr_jump(void *val) {
         "bx     lr                  \n" // return
         :                           // output operands
         : "r" (top)                 // input operands
-        :                           // clobbered registers
+        : "memory"                  // clobbered registers
         );
 
     MP_UNREACHABLE
diff --git a/py/nlrx64.c b/py/nlrx64.c
index 6f006e755ec..51224729fc9 100644
--- a/py/nlrx64.c
+++ b/py/nlrx64.c
@@ -35,8 +35,27 @@
 
 __attribute__((used)) unsigned int nlr_push_tail(nlr_buf_t *nlr);
 
+#if !MICROPY_NLR_OS_WINDOWS
+#if defined(__clang__) || (defined(__GNUC__) && __GNUC__ >= 8)
+#define USE_NAKED 1
+#else
+// On older gcc the equivalent here is to force omit-frame-pointer
+__attribute__((optimize("omit-frame-pointer")))
+#endif
+#endif
+
+#if !defined(USE_NAKED)
+#define USE_NAKED 0
+#endif
+
+#if USE_NAKED
+// nlr_push prelude should never push frame pointer register ebp onto the stack
+__attribute__((naked))
+#endif
 unsigned int nlr_push(nlr_buf_t *nlr) {
+    #if !USE_NAKED
     (void)nlr;
+    #endif
 
     #if MICROPY_NLR_OS_WINDOWS
 
@@ -58,9 +77,6 @@ unsigned int nlr_push(nlr_buf_t *nlr) {
     #else
 
     __asm volatile (
-        #if defined(__APPLE__) && defined(__MACH__)
-        "pop    %rbp                \n" // undo function's prelude
-        #endif
         "movq   (%rsp), %rax        \n" // load return %rip
         "movq   %rax, 16(%rdi)      \n" // store %rip into nlr_buf
         "movq   %rbp, 24(%rdi)      \n" // store %rbp into nlr_buf
@@ -79,10 +95,12 @@ unsigned int nlr_push(nlr_buf_t *nlr) {
 
     #endif
 
+    #if !USE_NAKED
     return 0; // needed to silence compiler warning
+    #endif
 }
 
-NORETURN void nlr_jump(void *val) {
+MP_NORETURN void nlr_jump(void *val) {
     MP_NLR_JUMP_HEAD(val, top)
 
     __asm volatile (
@@ -105,7 +123,7 @@ NORETURN void nlr_jump(void *val) {
         "ret                        \n" // return
         :                           // output operands
         : "r" (top)                 // input operands
-        :                           // clobbered registers
+        : "memory"                  // clobbered registers
         );
 
     MP_UNREACHABLE
diff --git a/py/nlrx86.c b/py/nlrx86.c
index f658d41910c..82e139dd8e8 100644
--- a/py/nlrx86.c
+++ b/py/nlrx86.c
@@ -40,23 +40,35 @@ __attribute__((used)) unsigned int nlr_push_tail(nlr_buf_t *nlr);
 #endif
 
 #if !defined(__clang__) && defined(__GNUC__) && __GNUC__ >= 8
-// Since gcc 8.0 the naked attribute is supported
-#define USE_NAKED (1)
-#define UNDO_PRELUDE (0)
+// Since gcc 8.0 the naked and no-sanitize attributes are supported
+    #define NLR_PUSH_ATTRIBUTE __attribute__((naked, no_sanitize("unreachable")))
+    #define UNDO_PRELUDE (0)
+    #define ARG_USED(x) (void)x;
+    #define NLR_UNREACHABLE __builtin_unreachable();
 #elif defined(__ZEPHYR__) || defined(__ANDROID__)
 // Zephyr and Android use a different calling convention by default
-#define USE_NAKED (0)
-#define UNDO_PRELUDE (0)
+    #define NLR_PUSH_ATTRIBUTE /* NOTHING */
+    #define UNDO_PRELUDE (0)
+    #define ARG_USED(x) (void)x;
+    #define NLR_UNREACHABLE return 0;
+#elif defined(__clang__)
+// clang on Ubuntu 24.04 enables -fsanitize=unreachable by default, but this
+// destroys the content of the ebx register.
+    #define NLR_PUSH_ATTRIBUTE __attribute__((naked, no_sanitize("unreachable")))
+    #define UNDO_PRELUDE (0)
+    #define ARG_USED(x) /* NOTHING */
+    #define NLR_UNREACHABLE /* NOTHING */
 #else
-#define USE_NAKED (0)
-#define UNDO_PRELUDE (1)
+// gcc before 8 unavoidably emits a 'push %ebp' prologue instruction
+    #define NLR_PUSH_ATTRIBUTE /* NOTHING */
+    #define UNDO_PRELUDE (1)
+    #define ARG_USED(x) (void)x;
+    #define NLR_UNREACHABLE return 0;
 #endif
 
-#if USE_NAKED
-__attribute__((naked))
-#endif
+NLR_PUSH_ATTRIBUTE
 unsigned int nlr_push(nlr_buf_t *nlr) {
-    (void)nlr;
+    ARG_USED(nlr)
 
     __asm volatile (
         #if UNDO_PRELUDE
@@ -73,12 +85,10 @@ unsigned int nlr_push(nlr_buf_t *nlr) {
         "jmp    nlr_push_tail       \n" // do the rest in C
         );
 
-    #if !USE_NAKED
-    return 0; // needed to silence compiler warning
-    #endif
+    NLR_UNREACHABLE
 }
 
-NORETURN void nlr_jump(void *val) {
+MP_NORETURN void nlr_jump(void *val) {
     MP_NLR_JUMP_HEAD(val, top)
 
     __asm volatile (
@@ -95,7 +105,7 @@ NORETURN void nlr_jump(void *val) {
         "ret                        \n" // return
         :                           // output operands
         : "r" (top)                 // input operands
-        :                           // clobbered registers
+        : "memory"                  // clobbered registers
         );
 
     MP_UNREACHABLE
diff --git a/py/nlrxtensa.c b/py/nlrxtensa.c
index abe9042af9f..2d1bf35e381 100644
--- a/py/nlrxtensa.c
+++ b/py/nlrxtensa.c
@@ -55,7 +55,7 @@ unsigned int nlr_push(nlr_buf_t *nlr) {
     return 0; // needed to silence compiler warning
 }
 
-NORETURN void nlr_jump(void *val) {
+MP_NORETURN void nlr_jump(void *val) {
     MP_NLR_JUMP_HEAD(val, top)
 
     __asm volatile (
@@ -74,7 +74,7 @@ NORETURN void nlr_jump(void *val) {
         "ret.n                      \n" // return
         :                           // output operands
         : "r" (top)                 // input operands
-        :                           // clobbered registers
+        : "memory"                  // clobbered registers
         );
 
     MP_UNREACHABLE
diff --git a/py/obj.c b/py/obj.c
index 64c52eeb76c..252e7372fcf 100644
--- a/py/obj.c
+++ b/py/obj.c
@@ -34,7 +34,7 @@
 #include "py/objint.h"
 #include "py/objstr.h"
 #include "py/runtime.h"
-#include "py/stackctrl.h"
+#include "py/cstack.h"
 #include "py/stream.h" // for mp_obj_print
 
 // Allocates an object and also sets type, for mp_obj_malloc{,_var} macros.
@@ -44,6 +44,15 @@ MP_NOINLINE void *mp_obj_malloc_helper(size_t num_bytes, const mp_obj_type_t *ty
     return base;
 }
 
+#if MICROPY_ENABLE_FINALISER
+// Allocates an object and also sets type, for mp_obj_malloc{,_var}_with_finaliser macros.
+MP_NOINLINE void *mp_obj_malloc_with_finaliser_helper(size_t num_bytes, const mp_obj_type_t *type) {
+    mp_obj_base_t *base = (mp_obj_base_t *)m_malloc_with_finaliser(num_bytes);
+    base->type = type;
+    return base;
+}
+#endif
+
 const mp_obj_type_t *MICROPY_WRAP_MP_OBJ_GET_TYPE(mp_obj_get_type)(mp_const_obj_t o_in) {
     #if MICROPY_OBJ_IMMEDIATE_OBJS && MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_A
 
@@ -108,7 +117,7 @@ const char *mp_obj_get_type_str(mp_const_obj_t o_in) {
 
 void mp_obj_print_helper(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     // There can be data structures nested too deep, or just recursive
-    MP_STACK_CHECK();
+    mp_cstack_check();
     #ifndef NDEBUG
     if (o_in == MP_OBJ_NULL) {
         mp_print_str(print, "(nil)");
@@ -119,7 +128,7 @@ void mp_obj_print_helper(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t
     if (MP_OBJ_TYPE_HAS_SLOT(type, print)) {
         MP_OBJ_TYPE_GET_SLOT(type, print)((mp_print_t *)print, o_in, kind);
     } else {
-        mp_printf(print, "<%q>", type->name);
+        mp_printf(print, "<%q>", (qstr)type->name);
     }
 }
 
@@ -286,7 +295,7 @@ mp_obj_t mp_obj_equal_not_equal(mp_binary_op_t op, mp_obj_t o1, mp_obj_t o2) {
         o2 = temp;
     }
 
-    // equality not implemented, so fall back to pointer conparison
+    // equality not implemented, so fall back to pointer comparison
     return (o1 == o2) ? local_true : local_false;
 }
 
@@ -305,6 +314,36 @@ mp_int_t mp_obj_get_int(mp_const_obj_t arg) {
     return val;
 }
 
+#if MICROPY_LONGINT_IMPL != MICROPY_LONGINT_IMPL_NONE
+mp_uint_t mp_obj_get_uint(mp_const_obj_t arg) {
+    if (!mp_obj_is_exact_type(arg, &mp_type_int)) {
+        mp_obj_t as_int = mp_unary_op(MP_UNARY_OP_INT_MAYBE, (mp_obj_t)arg);
+        if (as_int == MP_OBJ_NULL) {
+            mp_raise_TypeError_int_conversion(arg);
+        }
+        arg = as_int;
+    }
+    return mp_obj_int_get_uint_checked(arg);
+}
+
+long long mp_obj_get_ll(mp_const_obj_t arg) {
+    if (!mp_obj_is_exact_type(arg, &mp_type_int)) {
+        mp_obj_t as_int = mp_unary_op(MP_UNARY_OP_INT_MAYBE, (mp_obj_t)arg);
+        if (as_int == MP_OBJ_NULL) {
+            mp_raise_TypeError_int_conversion(arg);
+        }
+        arg = as_int;
+    }
+    if (mp_obj_is_small_int(arg)) {
+        return MP_OBJ_SMALL_INT_VALUE(arg);
+    } else {
+        long long res;
+        mp_obj_int_to_bytes((mp_obj_t)arg, sizeof(res), (byte *)&res, MP_ENDIANNESS_BIG, false, false);
+        return res;
+    }
+}
+#endif
+
 mp_int_t mp_obj_get_int_truncated(mp_const_obj_t arg) {
     if (mp_obj_is_int(arg)) {
         return mp_obj_int_get_truncated(arg);
@@ -577,18 +616,12 @@ MP_DEFINE_CONST_FUN_OBJ_1(mp_identity_obj, mp_identity);
 
 bool mp_get_buffer(mp_obj_t obj, mp_buffer_info_t *bufinfo, mp_uint_t flags) {
     const mp_obj_type_t *type = mp_obj_get_type(obj);
-    if (!MP_OBJ_TYPE_HAS_SLOT(type, buffer)) {
-        return false;
-    }
-    int ret = MP_OBJ_TYPE_GET_SLOT(type, buffer)(obj, bufinfo, flags);
-    if (ret != 0) {
-        return false;
+    if (MP_OBJ_TYPE_HAS_SLOT(type, buffer)
+        && MP_OBJ_TYPE_GET_SLOT(type, buffer)(obj, bufinfo, flags & MP_BUFFER_RW) == 0) {
+        return true;
     }
-    return true;
-}
-
-void mp_get_buffer_raise(mp_obj_t obj, mp_buffer_info_t *bufinfo, mp_uint_t flags) {
-    if (!mp_get_buffer(obj, bufinfo, flags)) {
+    if (flags & MP_BUFFER_RAISE_IF_UNSUPPORTED) {
         mp_raise_TypeError(MP_ERROR_TEXT("object with buffer protocol required"));
     }
+    return false;
 }
diff --git a/py/obj.h b/py/obj.h
index 3735b72c22c..c7933cd867c 100644
--- a/py/obj.h
+++ b/py/obj.h
@@ -117,7 +117,7 @@ extern const struct _mp_obj_float_t mp_const_float_inf_obj;
 extern const struct _mp_obj_float_t mp_const_float_nan_obj;
 #endif
 
-#define mp_obj_is_float(o) mp_obj_is_type((o), &mp_type_float)
+#define mp_obj_is_float(o) mp_obj_is_exact_type((o), &mp_type_float)
 mp_float_t mp_obj_float_get(mp_obj_t self_in);
 mp_obj_t mp_obj_new_float(mp_float_t value);
 #endif
@@ -162,7 +162,7 @@ extern const struct _mp_obj_float_t mp_const_float_inf_obj;
 extern const struct _mp_obj_float_t mp_const_float_nan_obj;
 #endif
 
-#define mp_obj_is_float(o) mp_obj_is_type((o), &mp_type_float)
+#define mp_obj_is_float(o) mp_obj_is_exact_type((o), &mp_type_float)
 mp_float_t mp_obj_float_get(mp_obj_t self_in);
 mp_obj_t mp_obj_new_float(mp_float_t value);
 #endif
@@ -184,12 +184,15 @@ static inline bool mp_obj_is_small_int(mp_const_obj_t o) {
 #define MP_OBJ_NEW_SMALL_INT(small_int) ((mp_obj_t)((((mp_uint_t)(small_int)) << 1) | 1))
 
 #if MICROPY_PY_BUILTINS_FLOAT
-#define mp_const_float_e MP_ROM_PTR((mp_obj_t)(((0x402df854 & ~3) | 2) + 0x80800000))
-#define mp_const_float_pi MP_ROM_PTR((mp_obj_t)(((0x40490fdb & ~3) | 2) + 0x80800000))
+#include 
+// note: MP_OBJ_NEW_CONST_FLOAT should be a MP_ROM_PTR but that macro isn't available yet
+#define MP_OBJ_NEW_CONST_FLOAT(f) ((mp_obj_t)((((((uint64_t)f) & ~3) | 2) + 0x80800000) & 0xffffffff))
+#define mp_const_float_e  MP_OBJ_NEW_CONST_FLOAT(0x402df854)
+#define mp_const_float_pi MP_OBJ_NEW_CONST_FLOAT(0x40490fdb)
+#define mp_const_float_nan MP_OBJ_NEW_CONST_FLOAT(0x7fc00000)
 #if MICROPY_PY_MATH_CONSTANTS
-#define mp_const_float_tau MP_ROM_PTR((mp_obj_t)(((0x40c90fdb & ~3) | 2) + 0x80800000))
-#define mp_const_float_inf MP_ROM_PTR((mp_obj_t)(((0x7f800000 & ~3) | 2) + 0x80800000))
-#define mp_const_float_nan MP_ROM_PTR((mp_obj_t)(((0xffc00000 & ~3) | 2) + 0x80800000))
+#define mp_const_float_tau MP_OBJ_NEW_CONST_FLOAT(0x40c90fdb)
+#define mp_const_float_inf MP_OBJ_NEW_CONST_FLOAT(0x7f800000)
 #endif
 
 static inline bool mp_obj_is_float(mp_const_obj_t o) {
@@ -202,15 +205,23 @@ static inline mp_float_t mp_obj_float_get(mp_const_obj_t o) {
     union {
         mp_float_t f;
         mp_uint_t u;
-    } num = {.u = ((mp_uint_t)o - 0x80800000) & ~3};
+    } num = {.u = ((mp_uint_t)o - 0x80800000u) & ~3u};
+    // Rather than always truncating toward zero, which creates a strong
+    // bias, copy the two previous bits to fill in the two missing bits.
+    // This appears to be a pretty good heuristic.
+    num.u |= (num.u >> 2) & 3u;
     return num.f;
 }
 static inline mp_obj_t mp_obj_new_float(mp_float_t f) {
+    if (isnan(f)) {
+        // prevent creation of bad nanboxed pointers via array.array or struct
+        return mp_const_float_nan;
+    }
     union {
         mp_float_t f;
         mp_uint_t u;
     } num = {.f = f};
-    return (mp_obj_t)(((num.u & ~0x3) | 2) + 0x80800000);
+    return (mp_obj_t)(((num.u & ~0x3u) | 2u) + 0x80800000u);
 }
 #endif
 
@@ -256,12 +267,13 @@ static inline bool mp_obj_is_immediate_obj(mp_const_obj_t o) {
 #error MICROPY_OBJ_REPR_D requires MICROPY_FLOAT_IMPL_DOUBLE
 #endif
 
+#include 
 #define mp_const_float_e {((mp_obj_t)((uint64_t)0x4005bf0a8b145769 + 0x8004000000000000))}
 #define mp_const_float_pi {((mp_obj_t)((uint64_t)0x400921fb54442d18 + 0x8004000000000000))}
+#define mp_const_float_nan {((mp_obj_t)((uint64_t)0x7ff8000000000000 + 0x8004000000000000))}
 #if MICROPY_PY_MATH_CONSTANTS
 #define mp_const_float_tau {((mp_obj_t)((uint64_t)0x401921fb54442d18 + 0x8004000000000000))}
 #define mp_const_float_inf {((mp_obj_t)((uint64_t)0x7ff0000000000000 + 0x8004000000000000))}
-#define mp_const_float_nan {((mp_obj_t)((uint64_t)0xfff8000000000000 + 0x8004000000000000))}
 #endif
 
 static inline bool mp_obj_is_float(mp_const_obj_t o) {
@@ -275,6 +287,13 @@ static inline mp_float_t mp_obj_float_get(mp_const_obj_t o) {
     return num.f;
 }
 static inline mp_obj_t mp_obj_new_float(mp_float_t f) {
+    if (isnan(f)) {
+        // prevent creation of bad nanboxed pointers via array.array or struct
+        struct {
+            uint64_t r;
+        } num = mp_const_float_nan;
+        return num.r;
+    }
     union {
         mp_float_t f;
         uint64_t r;
@@ -370,25 +389,25 @@ typedef struct _mp_rom_obj_t { mp_const_obj_t o; } mp_rom_obj_t;
 
 #define MP_DEFINE_CONST_FUN_OBJ_0(obj_name, fun_name) \
     const mp_obj_fun_builtin_fixed_t obj_name = \
-    {{&mp_type_fun_builtin_0}, .fun._0 = fun_name}
+    {.base = {.type = &mp_type_fun_builtin_0}, .fun = {._0 = fun_name}}
 #define MP_DEFINE_CONST_FUN_OBJ_1(obj_name, fun_name) \
     const mp_obj_fun_builtin_fixed_t obj_name = \
-    {{&mp_type_fun_builtin_1}, .fun._1 = fun_name}
+    {.base = {.type = &mp_type_fun_builtin_1}, .fun = {._1 = fun_name}}
 #define MP_DEFINE_CONST_FUN_OBJ_2(obj_name, fun_name) \
     const mp_obj_fun_builtin_fixed_t obj_name = \
-    {{&mp_type_fun_builtin_2}, .fun._2 = fun_name}
+    {.base = {.type = &mp_type_fun_builtin_2}, .fun = {._2 = fun_name}}
 #define MP_DEFINE_CONST_FUN_OBJ_3(obj_name, fun_name) \
     const mp_obj_fun_builtin_fixed_t obj_name = \
-    {{&mp_type_fun_builtin_3}, .fun._3 = fun_name}
+    {.base = {.type = &mp_type_fun_builtin_3}, .fun = {._3 = fun_name}}
 #define MP_DEFINE_CONST_FUN_OBJ_VAR(obj_name, n_args_min, fun_name) \
     const mp_obj_fun_builtin_var_t obj_name = \
-    {{&mp_type_fun_builtin_var}, MP_OBJ_FUN_MAKE_SIG(n_args_min, MP_OBJ_FUN_ARGS_MAX, false), .fun.var = fun_name}
+    {.base = {.type = &mp_type_fun_builtin_var}, .sig = MP_OBJ_FUN_MAKE_SIG(n_args_min, MP_OBJ_FUN_ARGS_MAX, false), .fun = {.var = fun_name}}
 #define MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(obj_name, n_args_min, n_args_max, fun_name) \
     const mp_obj_fun_builtin_var_t obj_name = \
-    {{&mp_type_fun_builtin_var}, MP_OBJ_FUN_MAKE_SIG(n_args_min, n_args_max, false), .fun.var = fun_name}
+    {.base = {.type = &mp_type_fun_builtin_var}, .sig = MP_OBJ_FUN_MAKE_SIG(n_args_min, n_args_max, false), .fun = {.var = fun_name}}
 #define MP_DEFINE_CONST_FUN_OBJ_KW(obj_name, n_args_min, fun_name) \
     const mp_obj_fun_builtin_var_t obj_name = \
-    {{&mp_type_fun_builtin_var}, MP_OBJ_FUN_MAKE_SIG(n_args_min, MP_OBJ_FUN_ARGS_MAX, true), .fun.kw = fun_name}
+    {.base = {.type = &mp_type_fun_builtin_var}, .sig = MP_OBJ_FUN_MAKE_SIG(n_args_min, MP_OBJ_FUN_ARGS_MAX, true), .fun = {.kw = fun_name}}
 
 // These macros are used to define constant map/dict objects
 // You can put "static" in front of the definition to make it local
@@ -434,6 +453,13 @@ typedef struct _mp_rom_obj_t { mp_const_obj_t o; } mp_rom_obj_t;
 // param obj_module: mp_obj_module_t instance
 #define MP_REGISTER_MODULE(module_name, obj_module)
 
+// As above, but allow this module to be extended from the filesystem.
+#define MP_REGISTER_EXTENSIBLE_MODULE(module_name, obj_module)
+
+// Add a custom handler for a builtin module that will be called to delegate
+// failed attribute lookups.
+#define MP_REGISTER_MODULE_DELEGATION(obj_module, fun_name)
+
 // Declare a root pointer (to avoid garbage collection of a global static variable).
 // param variable_declaration: a valid C variable declaration
 #define MP_REGISTER_ROOT_POINTER(variable_declaration)
@@ -476,9 +502,7 @@ static inline bool mp_map_slot_is_filled(const mp_map_t *map, size_t pos) {
 
 void mp_map_init(mp_map_t *map, size_t n);
 void mp_map_init_fixed_table(mp_map_t *map, size_t n, const mp_obj_t *table);
-mp_map_t *mp_map_new(size_t n);
 void mp_map_deinit(mp_map_t *map);
-void mp_map_free(mp_map_t *map);
 mp_map_elem_t *mp_map_lookup(mp_map_t *map, mp_obj_t index, mp_map_lookup_kind_t lookup_kind);
 void mp_map_clear(mp_map_t *map);
 void mp_map_dump(mp_map_t *map);
@@ -509,9 +533,16 @@ typedef mp_obj_t (*mp_fun_3_t)(mp_obj_t, mp_obj_t, mp_obj_t);
 typedef mp_obj_t (*mp_fun_var_t)(size_t n, const mp_obj_t *);
 // mp_fun_kw_t takes mp_map_t* (and not const mp_map_t*) to ease passing
 // this arg to mp_map_lookup().
+// Note that the mp_obj_t* array will contain all arguments, positional and keyword, with the keyword
+// ones starting at offset n, like: arg0 arg1 ... arg key0 value0 key1 value1 ..., and the mp_map_t*
+// gets those same keyword arguments but as a map for convenience; see fun_builtin_var_call.
 typedef mp_obj_t (*mp_fun_kw_t)(size_t n, const mp_obj_t *, mp_map_t *);
 
 // Flags for type behaviour (mp_obj_type_t.flags)
+// If MP_TYPE_FLAG_IS_SUBCLASSED is set, then subclasses of this class have been created.
+//   Mutations to this class that would require updating all subclasses must be rejected.
+// If MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS is set, then attribute lookups involving this
+//   class need to additionally check for special accessor methods, such as from descriptors.
 // If MP_TYPE_FLAG_EQ_NOT_REFLEXIVE is clear then __eq__ is reflexive (A==A returns True).
 // If MP_TYPE_FLAG_EQ_CHECKS_OTHER_TYPE is clear then the type can't be equal to an
 //   instance of any different class that also clears this flag.  If this flag is set
@@ -529,6 +560,8 @@ typedef mp_obj_t (*mp_fun_kw_t)(size_t n, const mp_obj_t *, mp_map_t *);
 // If MP_TYPE_FLAG_ITER_IS_STREAM is set then the type implicitly gets a "return self"
 //   getiter, and mp_stream_unbuffered_iter for iternext.
 // If MP_TYPE_FLAG_INSTANCE_TYPE is set then this is an instance type (i.e. defined in Python).
+// If MP_TYPE_FLAG_SUBSCR_ALLOWS_STACK_SLICE is set then the "subscr" slot allows a stack
+//   allocated slice to be passed in (no references to it will be retained after the call).
 #define MP_TYPE_FLAG_NONE (0x0000)
 #define MP_TYPE_FLAG_IS_SUBCLASSED (0x0001)
 #define MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS (0x0002)
@@ -542,6 +575,7 @@ typedef mp_obj_t (*mp_fun_kw_t)(size_t n, const mp_obj_t *, mp_map_t *);
 #define MP_TYPE_FLAG_ITER_IS_CUSTOM (0x0100)
 #define MP_TYPE_FLAG_ITER_IS_STREAM (MP_TYPE_FLAG_ITER_IS_ITERNEXT | MP_TYPE_FLAG_ITER_IS_CUSTOM)
 #define MP_TYPE_FLAG_INSTANCE_TYPE (0x0200)
+#define MP_TYPE_FLAG_SUBSCR_ALLOWS_STACK_SLICE (0x0400)
 
 typedef enum {
     PRINT_STR = 0,
@@ -578,17 +612,25 @@ typedef struct _mp_getiter_iternext_custom_t {
 } mp_getiter_iternext_custom_t;
 
 // Buffer protocol
+
 typedef struct _mp_buffer_info_t {
     void *buf;      // can be NULL if len == 0
     size_t len;     // in bytes
     int typecode;   // as per binary.h
 } mp_buffer_info_t;
+
 #define MP_BUFFER_READ  (1)
 #define MP_BUFFER_WRITE (2)
 #define MP_BUFFER_RW (MP_BUFFER_READ | MP_BUFFER_WRITE)
+#define MP_BUFFER_RAISE_IF_UNSUPPORTED (4)
+
 typedef mp_int_t (*mp_buffer_fun_t)(mp_obj_t obj, mp_buffer_info_t *bufinfo, mp_uint_t flags);
+
 bool mp_get_buffer(mp_obj_t obj, mp_buffer_info_t *bufinfo, mp_uint_t flags);
-void mp_get_buffer_raise(mp_obj_t obj, mp_buffer_info_t *bufinfo, mp_uint_t flags);
+
+static inline void mp_get_buffer_raise(mp_obj_t obj, mp_buffer_info_t *bufinfo, mp_uint_t flags) {
+    mp_get_buffer(obj, bufinfo, flags | MP_BUFFER_RAISE_IF_UNSUPPORTED);
+}
 
 // This struct will be updated to become a variable sized struct. In order to
 // use this as a member, or allocate dynamically, use the mp_obj_empty_type_t
@@ -734,25 +776,6 @@ typedef struct _mp_obj_full_type_t {
 #define _MP_OBJ_TYPE_SLOT_TYPE_parent (const void *)
 #define _MP_OBJ_TYPE_SLOT_TYPE_locals_dict (struct _mp_obj_dict_t *)
 
-// Implementation of MP_DEFINE_CONST_OBJ_TYPE for each number of arguments.
-// Do not use these directly, instead use MP_DEFINE_CONST_OBJ_TYPE.
-// Generated with:
-// for i in range(13):
-//     print(f"#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_{i}(_struct_type, _typename, _name, _flags{''.join(f', f{j+1}, v{j+1}' for j in range(i))}) const _struct_type _typename = {{ .base = {{ &mp_type_type }}, .name = _name, .flags = _flags{''.join(f', .slot_index_##f{j+1} = {j+1}' for j in range(i))}{', .slots = { ' + ''.join(f'v{j+1}, ' for j in range(i)) + '}' if i else '' } }}")
-#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_0(_struct_type, _typename, _name, _flags) const _struct_type _typename = { .base = { &mp_type_type }, .name = _name, .flags = _flags }
-#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_1(_struct_type, _typename, _name, _flags, f1, v1) const _struct_type _typename = { .base = { &mp_type_type }, .name = _name, .flags = _flags, .slot_index_##f1 = 1, .slots = { v1, } }
-#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_2(_struct_type, _typename, _name, _flags, f1, v1, f2, v2) const _struct_type _typename = { .base = { &mp_type_type }, .name = _name, .flags = _flags, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slots = { v1, v2, } }
-#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_3(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3) const _struct_type _typename = { .base = { &mp_type_type }, .name = _name, .flags = _flags, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slots = { v1, v2, v3, } }
-#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_4(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4) const _struct_type _typename = { .base = { &mp_type_type }, .name = _name, .flags = _flags, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slots = { v1, v2, v3, v4, } }
-#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_5(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5) const _struct_type _typename = { .base = { &mp_type_type }, .name = _name, .flags = _flags, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slots = { v1, v2, v3, v4, v5, } }
-#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_6(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6) const _struct_type _typename = { .base = { &mp_type_type }, .name = _name, .flags = _flags, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slots = { v1, v2, v3, v4, v5, v6, } }
-#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_7(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6, f7, v7) const _struct_type _typename = { .base = { &mp_type_type }, .name = _name, .flags = _flags, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slot_index_##f7 = 7, .slots = { v1, v2, v3, v4, v5, v6, v7, } }
-#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_8(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6, f7, v7, f8, v8) const _struct_type _typename = { .base = { &mp_type_type }, .name = _name, .flags = _flags, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slot_index_##f7 = 7, .slot_index_##f8 = 8, .slots = { v1, v2, v3, v4, v5, v6, v7, v8, } }
-#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_9(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6, f7, v7, f8, v8, f9, v9) const _struct_type _typename = { .base = { &mp_type_type }, .name = _name, .flags = _flags, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slot_index_##f7 = 7, .slot_index_##f8 = 8, .slot_index_##f9 = 9, .slots = { v1, v2, v3, v4, v5, v6, v7, v8, v9, } }
-#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_10(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6, f7, v7, f8, v8, f9, v9, f10, v10) const _struct_type _typename = { .base = { &mp_type_type }, .name = _name, .flags = _flags, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slot_index_##f7 = 7, .slot_index_##f8 = 8, .slot_index_##f9 = 9, .slot_index_##f10 = 10, .slots = { v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, } }
-#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_11(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6, f7, v7, f8, v8, f9, v9, f10, v10, f11, v11) const _struct_type _typename = { .base = { &mp_type_type }, .name = _name, .flags = _flags, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slot_index_##f7 = 7, .slot_index_##f8 = 8, .slot_index_##f9 = 9, .slot_index_##f10 = 10, .slot_index_##f11 = 11, .slots = { v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, } }
-#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_12(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6, f7, v7, f8, v8, f9, v9, f10, v10, f11, v11, f12, v12) const _struct_type _typename = { .base = { &mp_type_type }, .name = _name, .flags = _flags, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slot_index_##f7 = 7, .slot_index_##f8 = 8, .slot_index_##f9 = 9, .slot_index_##f10 = 10, .slot_index_##f11 = 11, .slot_index_##f12 = 12, .slots = { v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, } }
-
 // Because the mp_obj_type_t instances are in (zero-initialised) ROM, we take
 // slot_index_foo=0 to mean that the slot is unset. This also simplifies checking
 // if the slot is set. That means that we need to store index+1 in slot_index_foo
@@ -766,6 +789,34 @@ typedef struct _mp_obj_full_type_t {
 #define MP_OBJ_TYPE_OFFSETOF_SLOT(f) (offsetof(mp_obj_type_t, slot_index_##f))
 #define MP_OBJ_TYPE_HAS_SLOT_BY_OFFSET(t, offset) (*(uint8_t *)((char *)(t) + (offset)) != 0)
 
+// Implementation of MP_DEFINE_CONST_OBJ_TYPE for each number of arguments.
+// Do not use these directly, instead use MP_DEFINE_CONST_OBJ_TYPE.
+// Generated with:
+// for i in range(13):
+//     args = ['_struct_type', '_typename', '_name', '_flags']
+//     struct = ['.base = { &mp_type_type }', '.flags = _flags', '.name = _name']
+//     slots = []
+//     for j in range(i):
+//         args += [f"f{j+1}", f"v{j+1}"]
+//         struct += [f".slot_index_##f{j+1} = {j+1}"]
+//         slots += [f"(const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f{j+1} v{j+1}"]
+//     if slots:
+//         struct += [f".slots = {{ {', '.join(slots)} }}"]
+//     print(f"#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_{i}({', '.join(args)}) const _struct_type _typename = {{ {', '.join(struct)} }}")
+#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_0(_struct_type, _typename, _name, _flags) const _struct_type _typename = { .base = { &mp_type_type }, .flags = _flags, .name = _name }
+#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_1(_struct_type, _typename, _name, _flags, f1, v1) const _struct_type _typename = { .base = { &mp_type_type }, .flags = _flags, .name = _name, .slot_index_##f1 = 1, .slots = { (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f1 v1 } }
+#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_2(_struct_type, _typename, _name, _flags, f1, v1, f2, v2) const _struct_type _typename = { .base = { &mp_type_type }, .flags = _flags, .name = _name, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slots = { (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f1 v1, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f2 v2 } }
+#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_3(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3) const _struct_type _typename = { .base = { &mp_type_type }, .flags = _flags, .name = _name, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slots = { (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f1 v1, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f2 v2, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f3 v3 } }
+#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_4(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4) const _struct_type _typename = { .base = { &mp_type_type }, .flags = _flags, .name = _name, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slots = { (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f1 v1, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f2 v2, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f3 v3, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f4 v4 } }
+#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_5(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5) const _struct_type _typename = { .base = { &mp_type_type }, .flags = _flags, .name = _name, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slots = { (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f1 v1, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f2 v2, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f3 v3, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f4 v4, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f5 v5 } }
+#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_6(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6) const _struct_type _typename = { .base = { &mp_type_type }, .flags = _flags, .name = _name, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slots = { (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f1 v1, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f2 v2, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f3 v3, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f4 v4, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f5 v5, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f6 v6 } }
+#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_7(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6, f7, v7) const _struct_type _typename = { .base = { &mp_type_type }, .flags = _flags, .name = _name, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slot_index_##f7 = 7, .slots = { (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f1 v1, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f2 v2, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f3 v3, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f4 v4, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f5 v5, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f6 v6, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f7 v7 } }
+#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_8(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6, f7, v7, f8, v8) const _struct_type _typename = { .base = { &mp_type_type }, .flags = _flags, .name = _name, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slot_index_##f7 = 7, .slot_index_##f8 = 8, .slots = { (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f1 v1, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f2 v2, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f3 v3, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f4 v4, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f5 v5, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f6 v6, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f7 v7, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f8 v8 } }
+#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_9(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6, f7, v7, f8, v8, f9, v9) const _struct_type _typename = { .base = { &mp_type_type }, .flags = _flags, .name = _name, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slot_index_##f7 = 7, .slot_index_##f8 = 8, .slot_index_##f9 = 9, .slots = { (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f1 v1, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f2 v2, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f3 v3, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f4 v4, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f5 v5, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f6 v6, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f7 v7, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f8 v8, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f9 v9 } }
+#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_10(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6, f7, v7, f8, v8, f9, v9, f10, v10) const _struct_type _typename = { .base = { &mp_type_type }, .flags = _flags, .name = _name, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slot_index_##f7 = 7, .slot_index_##f8 = 8, .slot_index_##f9 = 9, .slot_index_##f10 = 10, .slots = { (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f1 v1, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f2 v2, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f3 v3, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f4 v4, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f5 v5, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f6 v6, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f7 v7, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f8 v8, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f9 v9, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f10 v10 } }
+#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_11(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6, f7, v7, f8, v8, f9, v9, f10, v10, f11, v11) const _struct_type _typename = { .base = { &mp_type_type }, .flags = _flags, .name = _name, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slot_index_##f7 = 7, .slot_index_##f8 = 8, .slot_index_##f9 = 9, .slot_index_##f10 = 10, .slot_index_##f11 = 11, .slots = { (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f1 v1, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f2 v2, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f3 v3, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f4 v4, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f5 v5, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f6 v6, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f7 v7, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f8 v8, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f9 v9, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f10 v10, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f11 v11 } }
+#define MP_DEFINE_CONST_OBJ_TYPE_NARGS_12(_struct_type, _typename, _name, _flags, f1, v1, f2, v2, f3, v3, f4, v4, f5, v5, f6, v6, f7, v7, f8, v8, f9, v9, f10, v10, f11, v11, f12, v12) const _struct_type _typename = { .base = { &mp_type_type }, .flags = _flags, .name = _name, .slot_index_##f1 = 1, .slot_index_##f2 = 2, .slot_index_##f3 = 3, .slot_index_##f4 = 4, .slot_index_##f5 = 5, .slot_index_##f6 = 6, .slot_index_##f7 = 7, .slot_index_##f8 = 8, .slot_index_##f9 = 9, .slot_index_##f10 = 10, .slot_index_##f11 = 11, .slot_index_##f12 = 12, .slots = { (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f1 v1, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f2 v2, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f3 v3, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f4 v4, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f5 v5, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f6 v6, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f7 v7, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f8 v8, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f9 v9, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f10 v10, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f11 v11, (const void *)_MP_OBJ_TYPE_SLOT_TYPE_##f12 v12 } }
+
 // Workaround for https://docs.microsoft.com/en-us/cpp/preprocessor/preprocessor-experimental-overview?view=msvc-160#macro-arguments-are-unpacked
 #define MP_DEFINE_CONST_OBJ_TYPE_EXPAND(x) x
 
@@ -774,7 +825,7 @@ typedef struct _mp_obj_full_type_t {
 #define MP_DEFINE_CONST_OBJ_TYPE_NARGS(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, N, ...) MP_DEFINE_CONST_OBJ_TYPE_NARGS_##N
 
 // This macros is used to define a object type in ROM.
-// Invoke as MP_DEFINE_CONST_OBJ_TYPE(_typename, _name, _flags, _make_new [, slot, func]*)
+// Invoke as MP_DEFINE_CONST_OBJ_TYPE(_typename, _name, _flags, [, slot, func]*)
 // It uses the number of arguments to select which MP_DEFINE_CONST_OBJ_TYPE_*
 // macro to use based on the number of arguments. It works by shifting the
 // numeric values 12, 11, ... 0 by the number of arguments, such that the
@@ -789,6 +840,8 @@ extern const mp_obj_type_t mp_type_NoneType;
 extern const mp_obj_type_t mp_type_bool;
 extern const mp_obj_type_t mp_type_int;
 extern const mp_obj_type_t mp_type_str;
+extern const mp_obj_type_t mp_type_template;
+extern const mp_obj_type_t mp_type_interpolation;
 extern const mp_obj_type_t mp_type_bytes;
 extern const mp_obj_type_t mp_type_bytearray;
 extern const mp_obj_type_t mp_type_memoryview;
@@ -818,12 +871,18 @@ extern const mp_obj_type_t mp_type_fun_builtin_2;
 extern const mp_obj_type_t mp_type_fun_builtin_3;
 extern const mp_obj_type_t mp_type_fun_builtin_var;
 extern const mp_obj_type_t mp_type_fun_bc;
+extern const mp_obj_type_t mp_type_fun_native;
+extern const mp_obj_type_t mp_type_fun_viper;
+extern const mp_obj_type_t mp_type_fun_asm;
+extern const mp_obj_type_t mp_type_code;
 extern const mp_obj_type_t mp_type_module;
 extern const mp_obj_type_t mp_type_staticmethod;
 extern const mp_obj_type_t mp_type_classmethod;
+extern const mp_obj_type_t mp_type_bound_meth;
 extern const mp_obj_type_t mp_type_property;
 extern const mp_obj_type_t mp_type_stringio;
 extern const mp_obj_type_t mp_type_bytesio;
+extern const mp_obj_type_t mp_type_ringio;
 extern const mp_obj_type_t mp_type_reversed;
 extern const mp_obj_type_t mp_type_polymorph_iter;
 #if MICROPY_ENABLE_FINALISER
@@ -897,9 +956,19 @@ extern const struct _mp_obj_exception_t mp_const_GeneratorExit_obj;
 // Helper versions of m_new_obj when you need to immediately set base.type.
 // Implementing this as a call rather than inline saves 8 bytes per usage.
 #define mp_obj_malloc(struct_type, obj_type) ((struct_type *)mp_obj_malloc_helper(sizeof(struct_type), obj_type))
-#define mp_obj_malloc_var(struct_type, var_type, var_num, obj_type) ((struct_type *)mp_obj_malloc_helper(sizeof(struct_type) + sizeof(var_type) * (var_num), obj_type))
+#define mp_obj_malloc_var(struct_type, var_field, var_type, var_num, obj_type) ((struct_type *)mp_obj_malloc_helper(offsetof(struct_type, var_field) + sizeof(var_type) * (var_num), obj_type))
 void *mp_obj_malloc_helper(size_t num_bytes, const mp_obj_type_t *type);
 
+// Object allocation macros for allocating objects that have a finaliser.
+#if MICROPY_ENABLE_FINALISER
+#define mp_obj_malloc_with_finaliser(struct_type, obj_type) ((struct_type *)mp_obj_malloc_with_finaliser_helper(sizeof(struct_type), obj_type))
+#define mp_obj_malloc_var_with_finaliser(struct_type, var_field, var_type, var_num, obj_type) ((struct_type *)mp_obj_malloc_with_finaliser_helper(offsetof(struct_type, var_field) + sizeof(var_type) * (var_num), obj_type))
+void *mp_obj_malloc_with_finaliser_helper(size_t num_bytes, const mp_obj_type_t *type);
+#else
+#define mp_obj_malloc_with_finaliser(struct_type, obj_type) mp_obj_malloc(struct_type, obj_type)
+#define mp_obj_malloc_var_with_finaliser(struct_type, var_field, var_type, var_num, obj_type) mp_obj_malloc_var(struct_type, var_field, var_type, var_num, obj_type)
+#endif
+
 // These macros are derived from more primitive ones and are used to
 // check for more specific object types.
 // Note: these are kept as macros because inline functions sometimes use much
@@ -912,13 +981,18 @@ void *mp_obj_malloc_helper(size_t num_bytes, const mp_obj_type_t *type);
 // optimizations (other tricks like using ({ expr; exper; }) or (exp, expr, expr) in mp_obj_is_type() result
 // in missed optimizations)
 #define mp_type_assert_not_bool_int_str_nonetype(t) (                                     \
-    MP_STATIC_ASSERT_NOT_MSC((t) != &mp_type_bool), assert((t) != &mp_type_bool),         \
-    MP_STATIC_ASSERT_NOT_MSC((t) != &mp_type_int), assert((t) != &mp_type_int),           \
-    MP_STATIC_ASSERT_NOT_MSC((t) != &mp_type_str), assert((t) != &mp_type_str),           \
-    MP_STATIC_ASSERT_NOT_MSC((t) != &mp_type_NoneType), assert((t) != &mp_type_NoneType), \
+    MP_STATIC_ASSERT_NONCONSTEXPR((t) != &mp_type_bool), assert((t) != &mp_type_bool),         \
+    MP_STATIC_ASSERT_NONCONSTEXPR((t) != &mp_type_int), assert((t) != &mp_type_int),           \
+    MP_STATIC_ASSERT_NONCONSTEXPR((t) != &mp_type_str), assert((t) != &mp_type_str),           \
+    MP_STATIC_ASSERT_NONCONSTEXPR((t) != &mp_type_NoneType), assert((t) != &mp_type_NoneType), \
     1)
+#if MICROPY_PY_BUILTINS_FLOAT
+#define mp_type_assert_not_float(t) (MP_STATIC_ASSERT_NONCONSTEXPR((t) != &mp_type_float), assert((t) != &mp_type_float), 1)
+#else
+#define mp_type_assert_not_float(t) (1)
+#endif
 
-#define mp_obj_is_type(o, t) (mp_type_assert_not_bool_int_str_nonetype(t) && mp_obj_is_exact_type(o, t))
+#define mp_obj_is_type(o, t) (mp_type_assert_not_bool_int_str_nonetype(t) && mp_type_assert_not_float(t) && mp_obj_is_exact_type(o, t))
 #if MICROPY_OBJ_IMMEDIATE_OBJS
 // bool's are immediates, not real objects, so test for the 2 possible values.
 #define mp_obj_is_bool(o) ((o) == mp_const_false || (o) == mp_const_true)
@@ -931,7 +1005,6 @@ void *mp_obj_malloc_helper(size_t num_bytes, const mp_obj_type_t *type);
 bool mp_obj_is_dict_or_ordereddict(mp_obj_t o);
 #define mp_obj_is_fun(o) (mp_obj_is_obj(o) && (((mp_obj_base_t *)MP_OBJ_TO_PTR(o))->type->name == MP_QSTR_function))
 
-mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict);
 static inline mp_obj_t mp_obj_new_bool(mp_int_t x) {
     return x ? mp_const_true : mp_const_false;
 }
@@ -942,6 +1015,7 @@ mp_obj_t mp_obj_new_int_from_str_len(const char **str, size_t len, bool neg, uns
 mp_obj_t mp_obj_new_int_from_ll(long long val); // this must return a multi-precision integer object (or raise an overflow exception)
 mp_obj_t mp_obj_new_int_from_ull(unsigned long long val); // this must return a multi-precision integer object (or raise an overflow exception)
 mp_obj_t mp_obj_new_str(const char *data, size_t len); // will check utf-8 (raises UnicodeError)
+mp_obj_t mp_obj_new_str_from_cstr(const char *str); // // accepts null-terminated string, will check utf-8 (raises UnicodeError)
 mp_obj_t mp_obj_new_str_via_qstr(const char *data, size_t len); // input data must be valid utf-8
 mp_obj_t mp_obj_new_str_from_vstr(vstr_t *vstr); // will check utf-8 (raises UnicodeError)
 #if MICROPY_PY_BUILTINS_STR_UNICODE && MICROPY_PY_BUILTINS_STR_UNICODE_CHECK
@@ -953,6 +1027,9 @@ mp_obj_t mp_obj_new_bytes_from_vstr(vstr_t *vstr);
 mp_obj_t mp_obj_new_bytes(const byte *data, size_t len);
 mp_obj_t mp_obj_new_bytearray(size_t n, const void *items);
 mp_obj_t mp_obj_new_bytearray_by_ref(size_t n, void *items);
+#if MICROPY_PY_TSTRINGS
+mp_obj_t mp_obj_new_template(size_t n_args, const mp_obj_t *args);
+#endif
 #if MICROPY_PY_BUILTINS_FLOAT
 mp_obj_t mp_obj_new_int_from_float(mp_float_t val);
 mp_obj_t mp_obj_new_complex(mp_float_t real, mp_float_t imag);
@@ -1001,6 +1078,8 @@ static inline bool mp_obj_is_integer(mp_const_obj_t o) {
 }
 
 mp_int_t mp_obj_get_int(mp_const_obj_t arg);
+mp_uint_t mp_obj_get_uint(mp_const_obj_t arg);
+long long mp_obj_get_ll(mp_const_obj_t arg);
 mp_int_t mp_obj_get_int_truncated(mp_const_obj_t arg);
 bool mp_obj_get_int_maybe(mp_const_obj_t arg, mp_int_t *value);
 #if MICROPY_PY_BUILTINS_FLOAT
@@ -1119,19 +1198,6 @@ mp_obj_t mp_obj_complex_binary_op(mp_binary_op_t op, mp_float_t lhs_real, mp_flo
 #define mp_obj_is_float(o) (false)
 #endif
 
-// tuple
-void mp_obj_tuple_get(mp_obj_t self_in, size_t *len, mp_obj_t **items);
-void mp_obj_tuple_del(mp_obj_t self_in);
-mp_int_t mp_obj_tuple_hash(mp_obj_t self_in);
-
-// list
-mp_obj_t mp_obj_list_append(mp_obj_t self_in, mp_obj_t arg);
-mp_obj_t mp_obj_list_remove(mp_obj_t self_in, mp_obj_t value);
-void mp_obj_list_get(mp_obj_t self_in, size_t *len, mp_obj_t **items);
-void mp_obj_list_set_len(mp_obj_t self_in, size_t len);
-void mp_obj_list_store(mp_obj_t self_in, mp_obj_t index, mp_obj_t value);
-mp_obj_t mp_obj_list_sort(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs);
-
 // dict
 typedef struct _mp_obj_dict_t {
     mp_obj_base_t base;
@@ -1188,8 +1254,6 @@ typedef struct _mp_obj_fun_builtin_var_t {
     } fun;
 } mp_obj_fun_builtin_var_t;
 
-qstr mp_obj_fun_get_name(mp_const_obj_t fun);
-
 mp_obj_t mp_identity(mp_obj_t self);
 MP_DECLARE_CONST_FUN_OBJ_1(mp_identity_obj);
 
@@ -1198,9 +1262,6 @@ typedef struct _mp_obj_module_t {
     mp_obj_base_t base;
     mp_obj_dict_t *globals;
 } mp_obj_module_t;
-static inline mp_obj_dict_t *mp_obj_module_get_globals(mp_obj_t module) {
-    return ((mp_obj_module_t *)MP_OBJ_TO_PTR(module))->globals;
-}
 
 // staticmethod and classmethod types; defined here so we can make const versions
 // this structure is used for instances of both staticmethod and classmethod
@@ -1228,7 +1289,7 @@ bool mp_seq_cmp_bytes(mp_uint_t op, const byte *data1, size_t len1, const byte *
 bool mp_seq_cmp_objs(mp_uint_t op, const mp_obj_t *items1, size_t len1, const mp_obj_t *items2, size_t len2);
 mp_obj_t mp_seq_index_obj(const mp_obj_t *items, size_t len, size_t n_args, const mp_obj_t *args);
 mp_obj_t mp_seq_count_obj(const mp_obj_t *items, size_t len, mp_obj_t value);
-mp_obj_t mp_seq_extract_slice(size_t len, const mp_obj_t *seq, mp_bound_slice_t *indexes);
+mp_obj_t mp_seq_extract_slice(const mp_obj_t *seq, mp_bound_slice_t *indexes);
 
 // Helper to clear stale pointers from allocated, but unused memory, to preclude GC problems
 #define mp_seq_clear(start, len, alloc_len, item_sz) memset((byte *)(start) + (len) * (item_sz), 0, ((alloc_len) - (len)) * (item_sz))
@@ -1243,6 +1304,8 @@ mp_obj_t mp_seq_extract_slice(size_t len, const mp_obj_t *seq, mp_bound_slice_t
     memmove(((char *)dest) + (beg + slice_len) * (item_sz), ((char *)dest) + (end) * (item_sz), ((dest_len) + (len_adj) - ((beg) + (slice_len))) * (item_sz)); \
     memmove(((char *)dest) + (beg) * (item_sz), slice, slice_len * (item_sz));
 
+#if !MICROPY_PREVIEW_VERSION_2
+
 // Provide translation for legacy API
 #define MP_OBJ_IS_SMALL_INT mp_obj_is_small_int
 #define MP_OBJ_IS_QSTR mp_obj_is_qstr
@@ -1255,4 +1318,6 @@ mp_obj_t mp_seq_extract_slice(size_t len, const mp_obj_t *seq, mp_bound_slice_t
 #define MP_MAP_SLOT_IS_FILLED mp_map_slot_is_filled
 #define MP_SET_SLOT_IS_FILLED mp_set_slot_is_filled
 
+#endif
+
 #endif // MICROPY_INCLUDED_PY_OBJ_H
diff --git a/py/objarray.c b/py/objarray.c
index a3adb255e83..609d95568a4 100644
--- a/py/objarray.c
+++ b/py/objarray.c
@@ -61,16 +61,16 @@
 // so not defined to catch errors
 #endif
 
-STATIC mp_obj_t array_iterator_new(mp_obj_t array_in, mp_obj_iter_buf_t *iter_buf);
-STATIC mp_obj_t array_append(mp_obj_t self_in, mp_obj_t arg);
-STATIC mp_obj_t array_extend(mp_obj_t self_in, mp_obj_t arg_in);
-STATIC mp_int_t array_get_buffer(mp_obj_t o_in, mp_buffer_info_t *bufinfo, mp_uint_t flags);
+static mp_obj_t array_iterator_new(mp_obj_t array_in, mp_obj_iter_buf_t *iter_buf);
+static mp_obj_t array_append(mp_obj_t self_in, mp_obj_t arg);
+static mp_obj_t array_extend(mp_obj_t self_in, mp_obj_t arg_in);
+static mp_int_t array_get_buffer(mp_obj_t o_in, mp_buffer_info_t *bufinfo, mp_uint_t flags);
 
 /******************************************************************************/
 // array
 
 #if MICROPY_PY_BUILTINS_BYTEARRAY || MICROPY_PY_ARRAY
-STATIC void array_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
+static void array_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_array_t *o = MP_OBJ_TO_PTR(o_in);
     if (o->typecode == BYTEARRAY_TYPECODE) {
@@ -94,7 +94,7 @@ STATIC void array_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t
 #endif
 
 #if MICROPY_PY_BUILTINS_BYTEARRAY || MICROPY_PY_ARRAY
-STATIC mp_obj_array_t *array_new(char typecode, size_t n) {
+static mp_obj_array_t *array_new(char typecode, size_t n) {
     int typecode_size = mp_binary_get_size('@', typecode, NULL);
     mp_obj_array_t *o = m_new_obj(mp_obj_array_t);
     #if MICROPY_PY_BUILTINS_BYTEARRAY && MICROPY_PY_ARRAY
@@ -113,7 +113,20 @@ STATIC mp_obj_array_t *array_new(char typecode, size_t n) {
 #endif
 
 #if MICROPY_PY_BUILTINS_BYTEARRAY || MICROPY_PY_ARRAY
-STATIC mp_obj_t array_construct(char typecode, mp_obj_t initializer) {
+static void array_extend_impl(mp_obj_array_t *array, mp_obj_t arg, char typecode, size_t len) {
+    mp_obj_t iterable = mp_getiter(arg, NULL);
+    mp_obj_t item;
+    size_t i = 0;
+    while ((item = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) {
+        if (len == 0) {
+            array_append(MP_OBJ_FROM_PTR(array), item);
+        } else {
+            mp_binary_set_val_array(typecode, array->items, i++, item);
+        }
+    }
+}
+
+static mp_obj_t array_construct(char typecode, mp_obj_t initializer) {
     // bytearrays can be raw-initialised from anything with the buffer protocol
     // other arrays can only be raw-initialised from bytes and bytearray objects
     mp_buffer_info_t bufinfo;
@@ -142,24 +155,13 @@ STATIC mp_obj_t array_construct(char typecode, mp_obj_t initializer) {
     }
 
     mp_obj_array_t *array = array_new(typecode, len);
-
-    mp_obj_t iterable = mp_getiter(initializer, NULL);
-    mp_obj_t item;
-    size_t i = 0;
-    while ((item = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) {
-        if (len == 0) {
-            array_append(MP_OBJ_FROM_PTR(array), item);
-        } else {
-            mp_binary_set_val_array(typecode, array->items, i++, item);
-        }
-    }
-
+    array_extend_impl(array, initializer, typecode, len);
     return MP_OBJ_FROM_PTR(array);
 }
 #endif
 
 #if MICROPY_PY_ARRAY
-STATIC mp_obj_t array_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t array_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)type_in;
     mp_arg_check_num(n_args, n_kw, 1, 2, false);
 
@@ -177,7 +179,7 @@ STATIC mp_obj_t array_make_new(const mp_obj_type_t *type_in, size_t n_args, size
 #endif
 
 #if MICROPY_PY_BUILTINS_BYTEARRAY
-STATIC mp_obj_t bytearray_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t bytearray_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)type_in;
     // Can take 2nd/3rd arg if constructs from str
     mp_arg_check_num(n_args, n_kw, 0, 3, false);
@@ -189,7 +191,10 @@ STATIC mp_obj_t bytearray_make_new(const mp_obj_type_t *type_in, size_t n_args,
         // 1 arg, an integer: construct a blank bytearray of that length
         mp_uint_t len = mp_obj_get_int(args[0]);
         mp_obj_array_t *o = array_new(BYTEARRAY_TYPECODE, len);
+        // If this config is set then the GC clears all memory, so we don't need to.
+        #if !MICROPY_GC_CONSERVATIVE_CLEAR
         memset(o->items, 0, len);
+        #endif
         return MP_OBJ_FROM_PTR(o);
     } else {
         // 1 arg: construct the bytearray from that
@@ -214,7 +219,7 @@ mp_obj_t mp_obj_new_memoryview(byte typecode, size_t nitems, void *items) {
     return MP_OBJ_FROM_PTR(self);
 }
 
-STATIC mp_obj_t memoryview_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t memoryview_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)type_in;
 
     // TODO possibly allow memoryview constructor to take start/stop so that one
@@ -246,7 +251,7 @@ STATIC mp_obj_t memoryview_make_new(const mp_obj_type_t *type_in, size_t n_args,
 }
 
 #if MICROPY_PY_BUILTINS_MEMORYVIEW_ITEMSIZE
-STATIC void memoryview_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+static void memoryview_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     if (dest[0] != MP_OBJ_NULL) {
         return;
     }
@@ -265,7 +270,7 @@ STATIC void memoryview_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
 
 #endif
 
-STATIC mp_obj_t array_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
+static mp_obj_t array_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
     mp_obj_array_t *o = MP_OBJ_TO_PTR(o_in);
     switch (op) {
         case MP_UNARY_OP_BOOL:
@@ -277,7 +282,7 @@ STATIC mp_obj_t array_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
     }
 }
 
-STATIC int typecode_for_comparison(int typecode, bool *is_unsigned) {
+static int typecode_for_comparison(int typecode, bool *is_unsigned) {
     if (typecode == BYTEARRAY_TYPECODE) {
         typecode = 'B';
     }
@@ -288,7 +293,7 @@ STATIC int typecode_for_comparison(int typecode, bool *is_unsigned) {
     return typecode;
 }
 
-STATIC mp_obj_t array_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
+static mp_obj_t array_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
     mp_obj_array_t *lhs = MP_OBJ_TO_PTR(lhs_in);
     switch (op) {
         case MP_BINARY_OP_ADD: {
@@ -383,7 +388,7 @@ STATIC mp_obj_t array_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs
 }
 
 #if MICROPY_PY_BUILTINS_BYTEARRAY || MICROPY_PY_ARRAY
-STATIC mp_obj_t array_append(mp_obj_t self_in, mp_obj_t arg) {
+static mp_obj_t array_append(mp_obj_t self_in, mp_obj_t arg) {
     // self is not a memoryview, so we don't need to use (& TYPECODE_MASK)
     assert((MICROPY_PY_BUILTINS_BYTEARRAY && mp_obj_is_type(self_in, &mp_type_bytearray))
         || (MICROPY_PY_ARRAY && mp_obj_is_type(self_in, &mp_type_array)));
@@ -392,8 +397,9 @@ STATIC mp_obj_t array_append(mp_obj_t self_in, mp_obj_t arg) {
     if (self->free == 0) {
         size_t item_sz = mp_binary_get_size('@', self->typecode, NULL);
         // TODO: alloc policy
-        self->free = 8;
-        self->items = m_renew(byte, self->items, item_sz * self->len, item_sz * (self->len + self->free));
+        size_t add_cnt = 8;
+        self->items = m_renew(byte, self->items, item_sz * self->len, item_sz * (self->len + add_cnt));
+        self->free = add_cnt;
         mp_seq_clear(self->items, self->len + 1, self->len + self->free, item_sz);
     }
     mp_binary_set_val_array(self->typecode, self->items, self->len, arg);
@@ -404,7 +410,7 @@ STATIC mp_obj_t array_append(mp_obj_t self_in, mp_obj_t arg) {
 }
 MP_DEFINE_CONST_FUN_OBJ_2(mp_obj_array_append_obj, array_append);
 
-STATIC mp_obj_t array_extend(mp_obj_t self_in, mp_obj_t arg_in) {
+static mp_obj_t array_extend(mp_obj_t self_in, mp_obj_t arg_in) {
     // self is not a memoryview, so we don't need to use (& TYPECODE_MASK)
     assert((MICROPY_PY_BUILTINS_BYTEARRAY && mp_obj_is_type(self_in, &mp_type_bytearray))
         || (MICROPY_PY_ARRAY && mp_obj_is_type(self_in, &mp_type_array)));
@@ -412,7 +418,10 @@ STATIC mp_obj_t array_extend(mp_obj_t self_in, mp_obj_t arg_in) {
 
     // allow to extend by anything that has the buffer protocol (extension to CPython)
     mp_buffer_info_t arg_bufinfo;
-    mp_get_buffer_raise(arg_in, &arg_bufinfo, MP_BUFFER_READ);
+    if (!mp_get_buffer(arg_in, &arg_bufinfo, MP_BUFFER_READ)) {
+        array_extend_impl(self, arg_in, 0, 0);
+        return mp_const_none;
+    }
 
     size_t sz = mp_binary_get_size('@', self->typecode, NULL);
 
@@ -424,6 +433,13 @@ STATIC mp_obj_t array_extend(mp_obj_t self_in, mp_obj_t arg_in) {
     if (self->free < len) {
         self->items = m_renew(byte, self->items, (self->len + self->free) * sz, (self->len + len) * sz);
         self->free = 0;
+
+        if (self_in == arg_in) {
+            // Get arg_bufinfo again in case self->items has moved
+            //
+            // (Note not possible to handle case that arg_in is a memoryview into self)
+            mp_get_buffer_raise(arg_in, &arg_bufinfo, MP_BUFFER_READ);
+        }
     } else {
         self->free -= len;
     }
@@ -437,7 +453,7 @@ STATIC mp_obj_t array_extend(mp_obj_t self_in, mp_obj_t arg_in) {
 MP_DEFINE_CONST_FUN_OBJ_2(mp_obj_array_extend_obj, array_extend);
 #endif
 
-STATIC mp_obj_t array_subscr(mp_obj_t self_in, mp_obj_t index_in, mp_obj_t value) {
+static mp_obj_t array_subscr(mp_obj_t self_in, mp_obj_t index_in, mp_obj_t value) {
     if (value == MP_OBJ_NULL) {
         // delete item
         // TODO implement
@@ -456,7 +472,8 @@ STATIC mp_obj_t array_subscr(mp_obj_t self_in, mp_obj_t index_in, mp_obj_t value
                 #if MICROPY_PY_ARRAY_SLICE_ASSIGN
                 // Assign
                 size_t src_len;
-                void *src_items;
+                uint8_t *src_items;
+                size_t src_offs = 0;
                 size_t item_sz = mp_binary_get_size('@', o->typecode & TYPECODE_MASK, NULL);
                 if (mp_obj_is_obj(value) && MP_OBJ_TYPE_GET_SLOT_OR_NULL(((mp_obj_base_t *)MP_OBJ_TO_PTR(value))->type, subscr) == array_subscr) {
                     // value is array, bytearray or memoryview
@@ -469,7 +486,7 @@ STATIC mp_obj_t array_subscr(mp_obj_t self_in, mp_obj_t index_in, mp_obj_t value
                     src_items = src_slice->items;
                     #if MICROPY_PY_BUILTINS_MEMORYVIEW
                     if (mp_obj_is_type(value, &mp_type_memoryview)) {
-                        src_items = (uint8_t *)src_items + (src_slice->memview_offset * item_sz);
+                        src_offs = src_slice->memview_offset * item_sz;
                     }
                     #endif
                 } else if (mp_obj_is_type(value, &mp_type_bytes)) {
@@ -504,20 +521,26 @@ STATIC mp_obj_t array_subscr(mp_obj_t self_in, mp_obj_t index_in, mp_obj_t value
                         // TODO: alloc policy; at the moment we go conservative
                         o->items = m_renew(byte, o->items, (o->len + o->free) * item_sz, (o->len + len_adj) * item_sz);
                         o->free = len_adj;
+                        // m_renew may have moved o->items
+                        if (src_items == dest_items) {
+                            src_items = o->items;
+                        }
                         dest_items = o->items;
                     }
                     mp_seq_replace_slice_grow_inplace(dest_items, o->len,
-                        slice.start, slice.stop, src_items, src_len, len_adj, item_sz);
+                        slice.start, slice.stop, src_items + src_offs, src_len, len_adj, item_sz);
                 } else {
                     mp_seq_replace_slice_no_grow(dest_items, o->len,
-                        slice.start, slice.stop, src_items, src_len, item_sz);
+                        slice.start, slice.stop, src_items + src_offs, src_len, item_sz);
                     // Clear "freed" elements at the end of list
                     // TODO: This is actually only needed for typecode=='O'
                     mp_seq_clear(dest_items, o->len + len_adj, o->len, item_sz);
                     // TODO: alloc policy after shrinking
                 }
-                o->free -= len_adj;
-                o->len += len_adj;
+                if (len_adj != 0) {
+                    o->free -= len_adj;
+                    o->len += len_adj;
+                }
                 return mp_const_none;
                 #else
                 return MP_OBJ_NULL; // op not supported
@@ -568,7 +591,7 @@ STATIC mp_obj_t array_subscr(mp_obj_t self_in, mp_obj_t index_in, mp_obj_t value
     }
 }
 
-STATIC mp_int_t array_get_buffer(mp_obj_t o_in, mp_buffer_info_t *bufinfo, mp_uint_t flags) {
+static mp_int_t array_get_buffer(mp_obj_t o_in, mp_buffer_info_t *bufinfo, mp_uint_t flags) {
     mp_obj_array_t *o = MP_OBJ_TO_PTR(o_in);
     size_t sz = mp_binary_get_size('@', o->typecode & TYPECODE_MASK, NULL);
     bufinfo->buf = o->items;
@@ -608,7 +631,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_bytearray,
     MP_QSTR_bytearray,
-    MP_TYPE_FLAG_EQ_CHECKS_OTHER_TYPE | MP_TYPE_FLAG_ITER_IS_GETITER,
+    MP_TYPE_FLAG_EQ_CHECKS_OTHER_TYPE | MP_TYPE_FLAG_ITER_IS_GETITER | MP_TYPE_FLAG_SUBSCR_ALLOWS_STACK_SLICE,
     make_new, bytearray_make_new,
     print, array_print,
     iter, array_iterator_new,
@@ -636,7 +659,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_memoryview,
     MP_QSTR_memoryview,
-    MP_TYPE_FLAG_EQ_CHECKS_OTHER_TYPE | MP_TYPE_FLAG_ITER_IS_GETITER,
+    MP_TYPE_FLAG_EQ_CHECKS_OTHER_TYPE | MP_TYPE_FLAG_ITER_IS_GETITER | MP_TYPE_FLAG_SUBSCR_ALLOWS_STACK_SLICE,
     make_new, memoryview_make_new,
     iter, array_iterator_new,
     unary_op, array_unary_op,
@@ -682,7 +705,7 @@ typedef struct _mp_obj_array_it_t {
     size_t cur;
 } mp_obj_array_it_t;
 
-STATIC mp_obj_t array_it_iternext(mp_obj_t self_in) {
+static mp_obj_t array_it_iternext(mp_obj_t self_in) {
     mp_obj_array_it_t *self = MP_OBJ_TO_PTR(self_in);
     if (self->cur < self->array->len) {
         return mp_binary_get_val_array(self->array->typecode & TYPECODE_MASK, self->array->items, self->offset + self->cur++);
@@ -691,14 +714,14 @@ STATIC mp_obj_t array_it_iternext(mp_obj_t self_in) {
     }
 }
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+static MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_array_it,
     MP_QSTR_iterator,
     MP_TYPE_FLAG_ITER_IS_ITERNEXT,
     iter, array_it_iternext
     );
 
-STATIC mp_obj_t array_iterator_new(mp_obj_t array_in, mp_obj_iter_buf_t *iter_buf) {
+static mp_obj_t array_iterator_new(mp_obj_t array_in, mp_obj_iter_buf_t *iter_buf) {
     assert(sizeof(mp_obj_array_t) <= sizeof(mp_obj_iter_buf_t));
     mp_obj_array_t *array = MP_OBJ_TO_PTR(array_in);
     mp_obj_array_it_t *o = (mp_obj_array_it_t *)iter_buf;
diff --git a/py/objarray.h b/py/objarray.h
index 4a0e8a983fe..bb7a514b979 100644
--- a/py/objarray.h
+++ b/py/objarray.h
@@ -53,6 +53,10 @@ typedef struct _mp_obj_array_t {
 } mp_obj_array_t;
 
 #if MICROPY_PY_BUILTINS_MEMORYVIEW
+
+#define MP_DEFINE_MEMORYVIEW_OBJ(obj_name, typecode, offset, len, ptr) \
+    mp_obj_array_t obj_name = {{&mp_type_memoryview}, (typecode), (offset), (len), (ptr)}
+
 static inline void mp_obj_memoryview_init(mp_obj_array_t *self, size_t typecode, size_t offset, size_t len, void *items) {
     self->base.type = &mp_type_memoryview;
     self->typecode = typecode;
@@ -60,6 +64,7 @@ static inline void mp_obj_memoryview_init(mp_obj_array_t *self, size_t typecode,
     self->len = len;
     self->items = items;
 }
+
 #endif
 
 #if MICROPY_PY_ARRAY || MICROPY_PY_BUILTINS_BYTEARRAY
diff --git a/py/objattrtuple.c b/py/objattrtuple.c
index fbe04bedb87..1280e330893 100644
--- a/py/objattrtuple.c
+++ b/py/objattrtuple.c
@@ -30,7 +30,7 @@
 
 // this helper function is used by collections.namedtuple
 #if !MICROPY_PY_COLLECTIONS
-STATIC
+static
 #endif
 void mp_obj_attrtuple_print_helper(const mp_print_t *print, const qstr *fields, mp_obj_tuple_t *o) {
     mp_print_str(print, "(");
@@ -48,14 +48,14 @@ void mp_obj_attrtuple_print_helper(const mp_print_t *print, const qstr *fields,
 
 #if MICROPY_PY_ATTRTUPLE
 
-STATIC void mp_obj_attrtuple_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
+static void mp_obj_attrtuple_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_tuple_t *o = MP_OBJ_TO_PTR(o_in);
     const qstr *fields = (const qstr *)MP_OBJ_TO_PTR(o->items[o->len]);
     mp_obj_attrtuple_print_helper(print, fields, o);
 }
 
-STATIC void mp_obj_attrtuple_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+static void mp_obj_attrtuple_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     if (dest[0] == MP_OBJ_NULL) {
         // load attribute
         mp_obj_tuple_t *self = MP_OBJ_TO_PTR(self_in);
@@ -71,7 +71,7 @@ STATIC void mp_obj_attrtuple_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
 }
 
 mp_obj_t mp_obj_new_attrtuple(const qstr *fields, size_t n, const mp_obj_t *items) {
-    mp_obj_tuple_t *o = mp_obj_malloc_var(mp_obj_tuple_t, mp_obj_t, n + 1, &mp_type_attrtuple);
+    mp_obj_tuple_t *o = mp_obj_malloc_var(mp_obj_tuple_t, items, mp_obj_t, n + 1, &mp_type_attrtuple);
     o->len = n;
     for (size_t i = 0; i < n; i++) {
         o->items[i] = items[i];
diff --git a/py/objbool.c b/py/objbool.c
index 3267ff98bb8..5b3e3660e95 100644
--- a/py/objbool.c
+++ b/py/objbool.c
@@ -43,9 +43,9 @@ typedef struct _mp_obj_bool_t {
 
 #endif
 
-STATIC void bool_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void bool_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     bool value = BOOL_VALUE(self_in);
-    if (MICROPY_PY_UJSON && kind == PRINT_JSON) {
+    if (MICROPY_PY_JSON && kind == PRINT_JSON) {
         if (value) {
             mp_print_str(print, "true");
         } else {
@@ -60,7 +60,7 @@ STATIC void bool_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_
     }
 }
 
-STATIC mp_obj_t bool_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t bool_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)type_in;
     mp_arg_check_num(n_args, n_kw, 0, 1, false);
 
@@ -71,7 +71,7 @@ STATIC mp_obj_t bool_make_new(const mp_obj_type_t *type_in, size_t n_args, size_
     }
 }
 
-STATIC mp_obj_t bool_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
+static mp_obj_t bool_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
     if (op == MP_UNARY_OP_LEN) {
         return MP_OBJ_NULL;
     }
@@ -79,7 +79,7 @@ STATIC mp_obj_t bool_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
     return mp_unary_op(op, MP_OBJ_NEW_SMALL_INT(value));
 }
 
-STATIC mp_obj_t bool_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
+static mp_obj_t bool_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
     bool value = BOOL_VALUE(lhs_in);
     return mp_binary_op(op, MP_OBJ_NEW_SMALL_INT(value), rhs_in);
 }
diff --git a/py/objboundmeth.c b/py/objboundmeth.c
index 8486f876f9a..6df67f7bf9e 100644
--- a/py/objboundmeth.c
+++ b/py/objboundmeth.c
@@ -36,7 +36,7 @@ typedef struct _mp_obj_bound_meth_t {
 } mp_obj_bound_meth_t;
 
 #if MICROPY_ERROR_REPORTING == MICROPY_ERROR_REPORTING_DETAILED
-STATIC void bound_meth_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
+static void bound_meth_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_bound_meth_t *o = MP_OBJ_TO_PTR(o_in);
     mp_printf(print, "meth, self->self, n_args, n_kw, args);
 }
 
+static mp_obj_t bound_meth_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
+    mp_obj_bound_meth_t *self = MP_OBJ_TO_PTR(self_in);
+    switch (op) {
+        case MP_UNARY_OP_HASH:
+            return MP_OBJ_NEW_SMALL_INT((mp_uint_t)self->self ^ (mp_uint_t)self->meth);
+        default:
+            return MP_OBJ_NULL; // op not supported
+    }
+}
+
+static mp_obj_t bound_meth_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
+    // The MP_TYPE_FLAG_EQ_CHECKS_OTHER_TYPE flag is clear for this type, so if this
+    // function is called with MP_BINARY_OP_EQUAL then lhs_in and rhs_in must have the
+    // same type, which is mp_type_bound_meth.
+    if (op != MP_BINARY_OP_EQUAL) {
+        return MP_OBJ_NULL; // op not supported
+    }
+    mp_obj_bound_meth_t *lhs = MP_OBJ_TO_PTR(lhs_in);
+    mp_obj_bound_meth_t *rhs = MP_OBJ_TO_PTR(rhs_in);
+    #if MICROPY_PY_BOUND_METHOD_FULL_EQUALITY_CHECK
+    return mp_obj_new_bool(mp_obj_equal(lhs->self, rhs->self) && mp_obj_equal(lhs->meth, rhs->meth));
+    #else
+    return mp_obj_new_bool(lhs->self == rhs->self && lhs->meth == rhs->meth);
+    #endif
+}
+
 #if MICROPY_PY_FUNCTION_ATTRS
-STATIC void bound_meth_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+static void bound_meth_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     if (dest[0] != MP_OBJ_NULL) {
         // not load attribute
         return;
@@ -107,13 +133,15 @@ STATIC void bound_meth_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
 #define BOUND_METH_TYPE_ATTR
 #endif
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_bound_meth,
     MP_QSTR_bound_method,
     MP_TYPE_FLAG_NONE,
     BOUND_METH_TYPE_PRINT
     BOUND_METH_TYPE_ATTR
-    call, bound_meth_call
+    call, bound_meth_call,
+    unary_op, bound_meth_unary_op,
+    binary_op, bound_meth_binary_op
     );
 
 mp_obj_t mp_obj_new_bound_meth(mp_obj_t meth, mp_obj_t self) {
diff --git a/py/objcell.c b/py/objcell.c
index 0a74e29d206..5c030c7405a 100644
--- a/py/objcell.c
+++ b/py/objcell.c
@@ -27,10 +27,10 @@
 #include "py/obj.h"
 
 #if MICROPY_ERROR_REPORTING == MICROPY_ERROR_REPORTING_DETAILED
-STATIC void cell_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
+static void cell_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_cell_t *o = MP_OBJ_TO_PTR(o_in);
-    mp_printf(print, "obj);
+    mp_printf(print, "obj);
     if (o->obj == MP_OBJ_NULL) {
         mp_print_str(print, "(nil)");
     } else {
@@ -46,7 +46,7 @@ STATIC void cell_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t k
 #define CELL_TYPE_PRINT
 #endif
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+static MP_DEFINE_CONST_OBJ_TYPE(
     // cell representation is just value in < >
     mp_type_cell, MP_QSTR_, MP_TYPE_FLAG_NONE
     CELL_TYPE_PRINT
diff --git a/py/objclosure.c b/py/objclosure.c
index 6059d18100e..3ba507b9593 100644
--- a/py/objclosure.c
+++ b/py/objclosure.c
@@ -36,7 +36,7 @@ typedef struct _mp_obj_closure_t {
     mp_obj_t closed[];
 } mp_obj_closure_t;
 
-STATIC mp_obj_t closure_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t closure_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     mp_obj_closure_t *self = MP_OBJ_TO_PTR(self_in);
 
     // need to concatenate closed-over-vars and args
@@ -60,7 +60,7 @@ STATIC mp_obj_t closure_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const
 }
 
 #if MICROPY_ERROR_REPORTING == MICROPY_ERROR_REPORTING_DETAILED
-STATIC void closure_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
+static void closure_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_closure_t *o = MP_OBJ_TO_PTR(o_in);
     mp_print_str(print, "fun
     mp_obj_closure_t *o = MP_OBJ_TO_PTR(self_in);
     mp_load_method_maybe(o->fun, attr, dest);
@@ -105,7 +105,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
     );
 
 mp_obj_t mp_obj_new_closure(mp_obj_t fun, size_t n_closed_over, const mp_obj_t *closed) {
-    mp_obj_closure_t *o = mp_obj_malloc_var(mp_obj_closure_t, mp_obj_t, n_closed_over, &mp_type_closure);
+    mp_obj_closure_t *o = mp_obj_malloc_var(mp_obj_closure_t, closed, mp_obj_t, n_closed_over, &mp_type_closure);
     o->fun = fun;
     o->n_closed = n_closed_over;
     memcpy(o->closed, closed, n_closed_over * sizeof(mp_obj_t));
diff --git a/py/objcode.c b/py/objcode.c
new file mode 100644
index 00000000000..09904f10f36
--- /dev/null
+++ b/py/objcode.c
@@ -0,0 +1,246 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "py/objcode.h"
+
+#if MICROPY_PY_BUILTINS_CODE == MICROPY_PY_BUILTINS_CODE_NONE
+
+// Code object not implemented at this configuration level.
+
+#elif MICROPY_PY_BUILTINS_CODE <= MICROPY_PY_BUILTINS_CODE_BASIC
+
+MP_DEFINE_CONST_OBJ_TYPE(
+    mp_type_code,
+    MP_QSTR_code,
+    MP_TYPE_FLAG_NONE
+    );
+
+#elif MICROPY_PY_BUILTINS_CODE <= MICROPY_PY_BUILTINS_CODE_FULL
+
+#include "py/profile.h"
+
+static void code_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
+    (void)kind;
+    mp_obj_code_t *o = MP_OBJ_TO_PTR(o_in);
+    const mp_raw_code_t *rc = o->rc;
+    const mp_bytecode_prelude_t *prelude = &rc->prelude;
+    mp_printf(print,
+        "",
+        MP_CODE_QSTR_MAP(o->context, prelude->qstr_block_name_idx),
+        o,
+        MP_CODE_QSTR_MAP(o->context, 0),
+        rc->line_of_definition
+        );
+}
+
+static mp_obj_tuple_t *code_consts(const mp_module_context_t *context, const mp_raw_code_t *rc) {
+    mp_obj_tuple_t *consts = MP_OBJ_TO_PTR(mp_obj_new_tuple(rc->n_children + 1, NULL));
+
+    size_t const_no = 0;
+    for (size_t i = 0; i < rc->n_children; ++i) {
+        mp_obj_t code = mp_obj_new_code(context, rc->children[i], true);
+        consts->items[const_no++] = code;
+    }
+    consts->items[const_no++] = mp_const_none;
+
+    return consts;
+}
+
+#if !MICROPY_PREVIEW_VERSION_2
+static mp_obj_t raw_code_lnotab(const mp_raw_code_t *rc) {
+    // const mp_bytecode_prelude_t *prelude = &rc->prelude;
+    uint start = 0;
+    uint stop = rc->fun_data_len - start;
+
+    uint last_lineno = mp_prof_bytecode_lineno(rc, start);
+    uint lasti = 0;
+
+    const uint buffer_chunk_size = (stop - start) >> 2; // heuristic magic
+    uint buffer_size = buffer_chunk_size;
+    byte *buffer = m_new(byte, buffer_size);
+    uint buffer_index = 0;
+
+    for (uint i = start; i < stop; ++i) {
+        uint lineno = mp_prof_bytecode_lineno(rc, i);
+        size_t line_diff = lineno - last_lineno;
+        if (line_diff > 0) {
+            uint instr_diff = (i - start) - lasti;
+
+            assert(instr_diff < 256);
+            assert(line_diff < 256);
+
+            if (buffer_index + 2 > buffer_size) {
+                buffer = m_renew(byte, buffer, buffer_size, buffer_size + buffer_chunk_size);
+                buffer_size = buffer_size + buffer_chunk_size;
+            }
+            last_lineno = lineno;
+            lasti = i - start;
+            buffer[buffer_index++] = instr_diff;
+            buffer[buffer_index++] = line_diff;
+        }
+    }
+
+    mp_obj_t o = mp_obj_new_bytes(buffer, buffer_index);
+    m_del(byte, buffer, buffer_size);
+    return o;
+}
+#endif
+
+static mp_obj_t code_colines_iter(mp_obj_t);
+static mp_obj_t code_colines_next(mp_obj_t);
+typedef struct _mp_obj_colines_iter_t {
+    mp_obj_base_t base;
+    mp_fun_1_t iternext;
+    const mp_raw_code_t *rc;
+    mp_uint_t bc;
+    mp_uint_t source_line;
+    const byte *ci;
+} mp_obj_colines_iter_t;
+
+static mp_obj_t code_colines_iter(mp_obj_t self_in) {
+    mp_obj_code_t *self = MP_OBJ_TO_PTR(self_in);
+    mp_obj_colines_iter_t *iter = mp_obj_malloc(mp_obj_colines_iter_t, &mp_type_polymorph_iter);
+    iter->iternext = code_colines_next;
+    iter->rc = self->rc;
+    iter->bc = 0;
+    iter->source_line = 1;
+    iter->ci = self->rc->prelude.line_info;
+    return MP_OBJ_FROM_PTR(iter);
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(code_colines_obj, code_colines_iter);
+
+static mp_obj_t code_colines_next(mp_obj_t iter_in) {
+    mp_obj_colines_iter_t *iter = MP_OBJ_TO_PTR(iter_in);
+    const byte *ci_end = iter->rc->prelude.line_info_top;
+
+    mp_uint_t start = iter->bc;
+    mp_uint_t line_no = iter->source_line;
+    bool another = true;
+
+    while (another && iter->ci < ci_end) {
+        another = false;
+        mp_code_lineinfo_t decoded = mp_bytecode_decode_lineinfo(&iter->ci);
+        iter->bc += decoded.bc_increment;
+        iter->source_line += decoded.line_increment;
+
+        if (decoded.bc_increment == 0) {
+            line_no = iter->source_line;
+            another = true;
+        } else if (decoded.line_increment == 0) {
+            another = true;
+        }
+    }
+
+    if (another) {
+        mp_uint_t prelude_size = (iter->rc->prelude.opcodes - (const byte *)iter->rc->fun_data);
+        mp_uint_t bc_end = iter->rc->fun_data_len - prelude_size;
+        if (iter->bc >= bc_end) {
+            return MP_OBJ_STOP_ITERATION;
+        } else {
+            iter->bc = bc_end;
+        }
+    }
+
+    mp_uint_t end = iter->bc;
+    mp_obj_t next[3] = {MP_OBJ_NEW_SMALL_INT(start), MP_OBJ_NEW_SMALL_INT(end), MP_OBJ_NEW_SMALL_INT(line_no)};
+
+    return mp_obj_new_tuple(MP_ARRAY_SIZE(next), next);
+}
+
+static void code_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    if (dest[0] != MP_OBJ_NULL) {
+        // not load attribute
+        return;
+    }
+    mp_obj_code_t *o = MP_OBJ_TO_PTR(self_in);
+    const mp_raw_code_t *rc = o->rc;
+    const mp_bytecode_prelude_t *prelude = &rc->prelude;
+    switch (attr) {
+        case MP_QSTR_co_code:
+            dest[0] = mp_obj_new_bytes(
+                (void *)prelude->opcodes,
+                rc->fun_data_len - (prelude->opcodes - (const byte *)rc->fun_data)
+                );
+            break;
+        case MP_QSTR_co_consts:
+            dest[0] = MP_OBJ_FROM_PTR(code_consts(o->context, rc));
+            break;
+        case MP_QSTR_co_filename:
+            dest[0] = MP_OBJ_NEW_QSTR(MP_CODE_QSTR_MAP(o->context, 0));
+            break;
+        case MP_QSTR_co_firstlineno:
+            dest[0] = MP_OBJ_NEW_SMALL_INT(mp_prof_bytecode_lineno(rc, 0));
+            break;
+        case MP_QSTR_co_name:
+            dest[0] = MP_OBJ_NEW_QSTR(MP_CODE_QSTR_MAP(o->context, prelude->qstr_block_name_idx));
+            break;
+        case MP_QSTR_co_names:
+            dest[0] = MP_OBJ_FROM_PTR(o->dict_locals);
+            break;
+        #if !MICROPY_PREVIEW_VERSION_2
+        case MP_QSTR_co_lnotab:
+            if (!o->lnotab) {
+                o->lnotab = raw_code_lnotab(rc);
+            }
+            dest[0] = o->lnotab;
+            break;
+        #endif
+        case MP_QSTR_co_lines:
+            dest[0] = MP_OBJ_FROM_PTR(&code_colines_obj);
+            dest[1] = self_in;
+            break;
+    }
+}
+
+MP_DEFINE_CONST_OBJ_TYPE(
+    mp_type_code,
+    MP_QSTR_code,
+    MP_TYPE_FLAG_NONE,
+    print, code_print,
+    attr, code_attr
+    );
+
+mp_obj_t mp_obj_new_code(const mp_module_context_t *context, const mp_raw_code_t *rc, bool result_required) {
+    mp_obj_code_t *o;
+    if (result_required) {
+        o = m_new_obj(mp_obj_code_t);
+    } else {
+        o = m_new_obj_maybe(mp_obj_code_t);
+        if (o == NULL) {
+            return MP_OBJ_NULL;
+        }
+    }
+    o->base.type = &mp_type_code;
+    o->context = context;
+    o->rc = rc;
+    o->dict_locals = mp_locals_get(); // this is a wrong! how to do this properly?
+    #if !MICROPY_PREVIEW_VERSION_2
+    o->lnotab = MP_OBJ_NULL;
+    #endif
+    return MP_OBJ_FROM_PTR(o);
+}
+
+#endif
diff --git a/py/objcode.h b/py/objcode.h
new file mode 100644
index 00000000000..7be15e23f52
--- /dev/null
+++ b/py/objcode.h
@@ -0,0 +1,100 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+#ifndef MICROPY_INCLUDED_PY_OBJCODE_H
+#define MICROPY_INCLUDED_PY_OBJCODE_H
+
+#include "py/bc.h"
+
+#if MICROPY_PY_BUILTINS_CODE == MICROPY_PY_BUILTINS_CODE_NONE
+
+// Code object not implemented at this configuration level.
+
+#elif MICROPY_PY_BUILTINS_CODE <= MICROPY_PY_BUILTINS_CODE_MINIMUM
+
+typedef struct _mp_obj_code_t {
+    mp_obj_base_t base;
+    mp_obj_t module_fun;
+} mp_obj_code_t;
+
+static inline mp_obj_t mp_obj_new_code(mp_obj_t module_fun) {
+    mp_obj_code_t *code = mp_obj_malloc(mp_obj_code_t, &mp_type_code);
+    code->module_fun = module_fun;
+    return MP_OBJ_FROM_PTR(code);
+}
+
+#elif MICROPY_PY_BUILTINS_CODE <= MICROPY_PY_BUILTINS_CODE_BASIC
+
+typedef struct _mp_obj_code_t {
+    mp_obj_base_t base;
+    mp_module_constants_t constants;
+    const void *proto_fun;
+} mp_obj_code_t;
+
+static inline mp_obj_t mp_obj_new_code(const mp_module_constants_t constants, const void *proto_fun) {
+    mp_obj_code_t *code = mp_obj_malloc(mp_obj_code_t, &mp_type_code);
+    code->constants = constants;
+    code->proto_fun = proto_fun;
+    return MP_OBJ_FROM_PTR(code);
+}
+
+static inline const mp_module_constants_t *mp_code_get_constants(mp_obj_code_t *self) {
+    return &self->constants;
+}
+
+static inline const void *mp_code_get_proto_fun(mp_obj_code_t *self) {
+    return self->proto_fun;
+}
+
+#elif MICROPY_PY_BUILTINS_CODE <= MICROPY_PY_BUILTINS_CODE_FULL
+
+#include "py/emitglue.h"
+
+#define MP_CODE_QSTR_MAP(context, idx) ((qstr)(context->constants.qstr_table[idx]))
+
+typedef struct _mp_obj_code_t {
+    mp_obj_base_t base;
+    const mp_module_context_t *context;
+    const mp_raw_code_t *rc;
+    mp_obj_dict_t *dict_locals;
+    #if !MICROPY_PREVIEW_VERSION_2
+    mp_obj_t lnotab;
+    #endif
+} mp_obj_code_t;
+
+mp_obj_t mp_obj_new_code(const mp_module_context_t *context, const mp_raw_code_t *rc, bool result_required);
+
+static inline const mp_module_constants_t *mp_code_get_constants(mp_obj_code_t *self) {
+    return &self->context->constants;
+}
+
+static inline const void *mp_code_get_proto_fun(mp_obj_code_t *self) {
+    // A mp_raw_code_t is always a proto_fun (but not the other way around).
+    return self->rc;
+}
+
+#endif
+
+#endif // MICROPY_INCLUDED_PY_OBJCODE_H
diff --git a/py/objcomplex.c b/py/objcomplex.c
index ddd103eeb46..805899edf43 100644
--- a/py/objcomplex.c
+++ b/py/objcomplex.c
@@ -42,35 +42,24 @@ typedef struct _mp_obj_complex_t {
     mp_float_t imag;
 } mp_obj_complex_t;
 
-STATIC void complex_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
+static void complex_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_complex_t *o = MP_OBJ_TO_PTR(o_in);
-    #if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT
-    char buf[16];
-    #if MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_C
-    const int precision = 6;
-    #else
-    const int precision = 7;
-    #endif
-    #else
-    char buf[32];
-    const int precision = 16;
-    #endif
-    if (o->real == 0) {
-        mp_format_float(o->imag, buf, sizeof(buf), 'g', precision, '\0');
-        mp_printf(print, "%sj", buf);
+    const char *suffix;
+    int flags = 0;
+    if (o->real != 0) {
+        mp_print_str(print, "(");
+        mp_print_float(print, o->real, 'g', 0, '\0', -1, MP_FLOAT_REPR_PREC);
+        flags = PF_FLAG_SHOW_SIGN;
+        suffix = "j)";
     } else {
-        mp_format_float(o->real, buf, sizeof(buf), 'g', precision, '\0');
-        mp_printf(print, "(%s", buf);
-        if (o->imag >= 0 || isnan(o->imag)) {
-            mp_print_str(print, "+");
-        }
-        mp_format_float(o->imag, buf, sizeof(buf), 'g', precision, '\0');
-        mp_printf(print, "%sj)", buf);
+        suffix = "j";
     }
+    mp_print_float(print, o->imag, 'g', flags, '\0', -1, MP_FLOAT_REPR_PREC);
+    mp_print_str(print, suffix);
 }
 
-STATIC mp_obj_t complex_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t complex_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)type_in;
     mp_arg_check_num(n_args, n_kw, 0, 2, false);
 
@@ -115,7 +104,7 @@ STATIC mp_obj_t complex_make_new(const mp_obj_type_t *type_in, size_t n_args, si
     }
 }
 
-STATIC mp_obj_t complex_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
+static mp_obj_t complex_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
     mp_obj_complex_t *o = MP_OBJ_TO_PTR(o_in);
     switch (op) {
         case MP_UNARY_OP_BOOL:
@@ -133,12 +122,12 @@ STATIC mp_obj_t complex_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
     }
 }
 
-STATIC mp_obj_t complex_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
+static mp_obj_t complex_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
     mp_obj_complex_t *lhs = MP_OBJ_TO_PTR(lhs_in);
     return mp_obj_complex_binary_op(op, lhs->real, lhs->imag, rhs_in);
 }
 
-STATIC void complex_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+static void complex_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     if (dest[0] != MP_OBJ_NULL) {
         // not load attribute
         return;
diff --git a/py/objdeque.c b/py/objdeque.c
index 8b52b8d3873..22c380a05a9 100644
--- a/py/objdeque.c
+++ b/py/objdeque.c
@@ -25,13 +25,11 @@
  */
 
 #include  // for ssize_t
-#include 
-
-#include "py/mpconfig.h"
-#if MICROPY_PY_COLLECTIONS_DEQUE
 
 #include "py/runtime.h"
 
+#if MICROPY_PY_COLLECTIONS_DEQUE
+
 typedef struct _mp_obj_deque_t {
     mp_obj_base_t base;
     size_t alloc;
@@ -42,14 +40,14 @@ typedef struct _mp_obj_deque_t {
     #define FLAG_CHECK_OVERFLOW 1
 } mp_obj_deque_t;
 
-STATIC mp_obj_t deque_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
-    mp_arg_check_num(n_args, n_kw, 2, 3, false);
+static mp_obj_t mp_obj_deque_append(mp_obj_t self_in, mp_obj_t arg);
+static mp_obj_t mp_obj_deque_extend(mp_obj_t self_in, mp_obj_t arg_in);
+#if MICROPY_PY_COLLECTIONS_DEQUE_ITER
+static mp_obj_t mp_obj_new_deque_it(mp_obj_t deque, mp_obj_iter_buf_t *iter_buf);
+#endif
 
-    /* Initialization from existing sequence is not supported, so an empty
-       tuple must be passed as such. */
-    if (args[0] != mp_const_empty_tuple) {
-        mp_raise_ValueError(NULL);
-    }
+static mp_obj_t deque_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+    mp_arg_check_num(n_args, n_kw, 2, 3, false);
 
     // Protect against -1 leading to zero-length allocation and bad array access
     mp_int_t maxlen = mp_obj_get_int(args[1]);
@@ -66,21 +64,27 @@ STATIC mp_obj_t deque_make_new(const mp_obj_type_t *type, size_t n_args, size_t
         o->flags = mp_obj_get_int(args[2]);
     }
 
+    mp_obj_deque_extend(MP_OBJ_FROM_PTR(o), args[0]);
+
     return MP_OBJ_FROM_PTR(o);
 }
 
-STATIC mp_obj_t deque_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
+static size_t deque_len(mp_obj_deque_t *self) {
+    ssize_t len = self->i_put - self->i_get;
+    if (len < 0) {
+        len += self->alloc;
+    }
+    return len;
+}
+
+static mp_obj_t deque_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
     mp_obj_deque_t *self = MP_OBJ_TO_PTR(self_in);
     switch (op) {
         case MP_UNARY_OP_BOOL:
             return mp_obj_new_bool(self->i_get != self->i_put);
-        case MP_UNARY_OP_LEN: {
-            ssize_t len = self->i_put - self->i_get;
-            if (len < 0) {
-                len += self->alloc;
-            }
-            return MP_OBJ_NEW_SMALL_INT(len);
-        }
+        case MP_UNARY_OP_LEN:
+            return MP_OBJ_NEW_SMALL_INT(deque_len(self));
+
         #if MICROPY_PY_SYS_GETSIZEOF
         case MP_UNARY_OP_SIZEOF: {
             size_t sz = sizeof(*self) + sizeof(mp_obj_t) * self->alloc;
@@ -92,7 +96,7 @@ STATIC mp_obj_t deque_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
     }
 }
 
-STATIC mp_obj_t mp_obj_deque_append(mp_obj_t self_in, mp_obj_t arg) {
+static mp_obj_t mp_obj_deque_append(mp_obj_t self_in, mp_obj_t arg) {
     mp_obj_deque_t *self = MP_OBJ_TO_PTR(self_in);
 
     size_t new_i_put = self->i_put + 1;
@@ -115,9 +119,48 @@ STATIC mp_obj_t mp_obj_deque_append(mp_obj_t self_in, mp_obj_t arg) {
 
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(deque_append_obj, mp_obj_deque_append);
+static MP_DEFINE_CONST_FUN_OBJ_2(deque_append_obj, mp_obj_deque_append);
+
+static mp_obj_t mp_obj_deque_appendleft(mp_obj_t self_in, mp_obj_t arg) {
+    mp_obj_deque_t *self = MP_OBJ_TO_PTR(self_in);
+
+    size_t new_i_get = self->i_get - 1;
+    if (self->i_get == 0) {
+        new_i_get = self->alloc - 1;
+    }
+
+    if (self->flags & FLAG_CHECK_OVERFLOW && new_i_get == self->i_put) {
+        mp_raise_msg(&mp_type_IndexError, MP_ERROR_TEXT("full"));
+    }
+
+    self->i_get = new_i_get;
+    self->items[self->i_get] = arg;
+
+    // overwriting first element in deque
+    if (self->i_put == new_i_get) {
+        if (self->i_put == 0) {
+            self->i_put = self->alloc - 1;
+        } else {
+            self->i_put--;
+        }
+    }
+
+    return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_2(deque_appendleft_obj, mp_obj_deque_appendleft);
 
-STATIC mp_obj_t deque_popleft(mp_obj_t self_in) {
+static mp_obj_t mp_obj_deque_extend(mp_obj_t self_in, mp_obj_t arg_in) {
+    mp_obj_iter_buf_t iter_buf;
+    mp_obj_t iter = mp_getiter(arg_in, &iter_buf);
+    mp_obj_t item;
+    while ((item = mp_iternext(iter)) != MP_OBJ_STOP_ITERATION) {
+        mp_obj_deque_append(self_in, item);
+    }
+    return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_2(deque_extend_obj, mp_obj_deque_extend);
+
+static mp_obj_t deque_popleft(mp_obj_t self_in) {
     mp_obj_deque_t *self = MP_OBJ_TO_PTR(self_in);
 
     if (self->i_get == self->i_put) {
@@ -133,35 +176,139 @@ STATIC mp_obj_t deque_popleft(mp_obj_t self_in) {
 
     return ret;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(deque_popleft_obj, deque_popleft);
+static MP_DEFINE_CONST_FUN_OBJ_1(deque_popleft_obj, deque_popleft);
+
+static mp_obj_t deque_pop(mp_obj_t self_in) {
+    mp_obj_deque_t *self = MP_OBJ_TO_PTR(self_in);
+
+    if (self->i_get == self->i_put) {
+        mp_raise_msg(&mp_type_IndexError, MP_ERROR_TEXT("empty"));
+    }
+
+    if (self->i_put == 0) {
+        self->i_put = self->alloc - 1;
+    } else {
+        self->i_put--;
+    }
+
+    mp_obj_t ret = self->items[self->i_put];
+    self->items[self->i_put] = MP_OBJ_NULL;
+
+    return ret;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(deque_pop_obj, deque_pop);
+
+#if MICROPY_PY_COLLECTIONS_DEQUE_SUBSCR
+static mp_obj_t deque_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
+    if (value == MP_OBJ_NULL) {
+        // delete not supported, fall back to mp_obj_subscr() error message
+        return MP_OBJ_NULL;
+    }
+    mp_obj_deque_t *self = MP_OBJ_TO_PTR(self_in);
+
+    size_t offset = mp_get_index(self->base.type, deque_len(self), index, false);
+    size_t index_val = self->i_get + offset;
+    if (index_val >= self->alloc) {
+        index_val -= self->alloc;
+    }
+
+    if (value == MP_OBJ_SENTINEL) {
+        // load
+        return self->items[index_val];
+    } else {
+        // store into deque
+        self->items[index_val] = value;
+        return mp_const_none;
+    }
+}
+#endif
 
 #if 0
-STATIC mp_obj_t deque_clear(mp_obj_t self_in) {
+static mp_obj_t deque_clear(mp_obj_t self_in) {
     mp_obj_deque_t *self = MP_OBJ_TO_PTR(self_in);
     self->i_get = self->i_put = 0;
     mp_seq_clear(self->items, 0, self->alloc, sizeof(*self->items));
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(deque_clear_obj, deque_clear);
+static MP_DEFINE_CONST_FUN_OBJ_1(deque_clear_obj, deque_clear);
 #endif
 
-STATIC const mp_rom_map_elem_t deque_locals_dict_table[] = {
+static const mp_rom_map_elem_t deque_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_append), MP_ROM_PTR(&deque_append_obj) },
+    { MP_ROM_QSTR(MP_QSTR_appendleft), MP_ROM_PTR(&deque_appendleft_obj) },
+    { MP_ROM_QSTR(MP_QSTR_extend), MP_ROM_PTR(&deque_extend_obj) },
     #if 0
     { MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&deque_clear_obj) },
     #endif
+    { MP_ROM_QSTR(MP_QSTR_pop), MP_ROM_PTR(&deque_pop_obj) },
     { MP_ROM_QSTR(MP_QSTR_popleft), MP_ROM_PTR(&deque_popleft_obj) },
 };
 
-STATIC MP_DEFINE_CONST_DICT(deque_locals_dict, deque_locals_dict_table);
+static MP_DEFINE_CONST_DICT(deque_locals_dict, deque_locals_dict_table);
+
+#if MICROPY_PY_COLLECTIONS_DEQUE_ITER
+#define DEQUE_TYPE_FLAGS MP_TYPE_FLAG_ITER_IS_GETITER
+#define DEQUE_TYPE_ITER iter, mp_obj_new_deque_it,
+#else
+#define DEQUE_TYPE_FLAGS MP_TYPE_FLAG_NONE
+#define DEQUE_TYPE_ITER
+#endif
+
+#if MICROPY_PY_COLLECTIONS_DEQUE_SUBSCR
+#define DEQUE_TYPE_SUBSCR subscr, deque_subscr,
+#else
+#define DEQUE_TYPE_SUBSCR
+#endif
 
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_deque,
     MP_QSTR_deque,
-    MP_TYPE_FLAG_NONE,
+    DEQUE_TYPE_FLAGS,
     make_new, deque_make_new,
     unary_op, deque_unary_op,
+    DEQUE_TYPE_SUBSCR
+    DEQUE_TYPE_ITER
     locals_dict, &deque_locals_dict
     );
 
+/******************************************************************************/
+/* deque iterator                                                             */
+
+#if MICROPY_PY_COLLECTIONS_DEQUE_ITER
+
+typedef struct _mp_obj_deque_it_t {
+    mp_obj_base_t base;
+    mp_fun_1_t iternext;
+    mp_obj_t deque;
+    size_t cur;
+} mp_obj_deque_it_t;
+
+static mp_obj_t deque_it_iternext(mp_obj_t self_in) {
+    mp_obj_deque_it_t *self = MP_OBJ_TO_PTR(self_in);
+    mp_obj_deque_t *deque = MP_OBJ_TO_PTR(self->deque);
+    if (self->cur != deque->i_put) {
+        mp_obj_t o_out = deque->items[self->cur];
+        if (++self->cur == deque->alloc) {
+            self->cur = 0;
+        }
+        return o_out;
+    } else {
+        return MP_OBJ_STOP_ITERATION;
+    }
+}
+
+static mp_obj_t mp_obj_new_deque_it(mp_obj_t deque, mp_obj_iter_buf_t *iter_buf) {
+    mp_obj_deque_t *deque_ = MP_OBJ_TO_PTR(deque);
+    size_t i_get = deque_->i_get;
+    assert(sizeof(mp_obj_deque_it_t) <= sizeof(mp_obj_iter_buf_t));
+    mp_obj_deque_it_t *o = (mp_obj_deque_it_t *)iter_buf;
+    o->base.type = &mp_type_polymorph_iter;
+    o->iternext = deque_it_iternext;
+    o->deque = deque;
+    o->cur = i_get;
+    return MP_OBJ_FROM_PTR(o);
+}
+
+#endif
+
 #endif // MICROPY_PY_COLLECTIONS_DEQUE
diff --git a/py/objdict.c b/py/objdict.c
index a621c4feafb..1baeeada338 100644
--- a/py/objdict.c
+++ b/py/objdict.c
@@ -49,12 +49,12 @@ const mp_obj_dict_t mp_const_empty_dict_obj = {
     }
 };
 
-STATIC mp_obj_t dict_update(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs);
+static mp_obj_t dict_update(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs);
 
 // This is a helper function to iterate through a dictionary.  The state of
 // the iteration is held in *cur and should be initialised with zero for the
 // first call.  Will return NULL when no more elements are available.
-STATIC mp_map_elem_t *dict_iter_next(mp_obj_dict_t *dict, size_t *cur) {
+static mp_map_elem_t *dict_iter_next(mp_obj_dict_t *dict, size_t *cur) {
     size_t max = dict->map.alloc;
     mp_map_t *map = &dict->map;
 
@@ -70,21 +70,21 @@ STATIC mp_map_elem_t *dict_iter_next(mp_obj_dict_t *dict, size_t *cur) {
     return NULL;
 }
 
-STATIC void dict_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void dict_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     mp_obj_dict_t *self = MP_OBJ_TO_PTR(self_in);
     bool first = true;
     const char *item_separator = ", ";
     const char *key_separator = ": ";
-    if (!(MICROPY_PY_UJSON && kind == PRINT_JSON)) {
+    if (!(MICROPY_PY_JSON && kind == PRINT_JSON)) {
         kind = PRINT_REPR;
     } else {
-        #if MICROPY_PY_UJSON_SEPARATORS
+        #if MICROPY_PY_JSON_SEPARATORS
         item_separator = MP_PRINT_GET_EXT(print)->item_separator;
         key_separator = MP_PRINT_GET_EXT(print)->key_separator;
         #endif
     }
     if (MICROPY_PY_COLLECTIONS_ORDEREDDICT && self->base.type != &mp_type_dict && kind != PRINT_JSON) {
-        mp_printf(print, "%q(", self->base.type->name);
+        mp_printf(print, "%q(", (qstr)self->base.type->name);
     }
     mp_print_str(print, "{");
     size_t cur = 0;
@@ -94,7 +94,7 @@ STATIC void dict_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_
             mp_print_str(print, item_separator);
         }
         first = false;
-        bool add_quote = MICROPY_PY_UJSON && kind == PRINT_JSON && !mp_obj_is_str_or_bytes(next->key);
+        bool add_quote = MICROPY_PY_JSON && kind == PRINT_JSON && !mp_obj_is_str_or_bytes(next->key);
         if (add_quote) {
             mp_print_str(print, "\"");
         }
@@ -129,7 +129,7 @@ mp_obj_t mp_obj_dict_make_new(const mp_obj_type_t *type, size_t n_args, size_t n
     return dict_out;
 }
 
-STATIC mp_obj_t dict_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
+static mp_obj_t dict_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
     mp_obj_dict_t *self = MP_OBJ_TO_PTR(self_in);
     switch (op) {
         case MP_UNARY_OP_BOOL:
@@ -147,7 +147,7 @@ STATIC mp_obj_t dict_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
     }
 }
 
-STATIC mp_obj_t dict_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
+static mp_obj_t dict_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
     mp_obj_dict_t *o = MP_OBJ_TO_PTR(lhs_in);
     switch (op) {
         case MP_BINARY_OP_CONTAINS: {
@@ -218,7 +218,7 @@ mp_obj_t mp_obj_dict_get(mp_obj_t self_in, mp_obj_t index) {
     }
 }
 
-STATIC mp_obj_t dict_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
+static mp_obj_t dict_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
     if (value == MP_OBJ_NULL) {
         // delete
         mp_obj_dict_delete(self_in, index);
@@ -242,13 +242,13 @@ STATIC mp_obj_t dict_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
 /******************************************************************************/
 /* dict methods                                                               */
 
-STATIC void mp_ensure_not_fixed(const mp_obj_dict_t *dict) {
+static void mp_ensure_not_fixed(const mp_obj_dict_t *dict) {
     if (dict->map.is_fixed) {
         mp_raise_TypeError(NULL);
     }
 }
 
-STATIC mp_obj_t dict_clear(mp_obj_t self_in) {
+static mp_obj_t dict_clear(mp_obj_t self_in) {
     mp_check_self(mp_obj_is_dict_or_ordereddict(self_in));
     mp_obj_dict_t *self = MP_OBJ_TO_PTR(self_in);
     mp_ensure_not_fixed(self);
@@ -257,7 +257,7 @@ STATIC mp_obj_t dict_clear(mp_obj_t self_in) {
 
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(dict_clear_obj, dict_clear);
+static MP_DEFINE_CONST_FUN_OBJ_1(dict_clear_obj, dict_clear);
 
 mp_obj_t mp_obj_dict_copy(mp_obj_t self_in) {
     mp_check_self(mp_obj_is_dict_or_ordereddict(self_in));
@@ -272,11 +272,11 @@ mp_obj_t mp_obj_dict_copy(mp_obj_t self_in) {
     memcpy(other->map.table, self->map.table, self->map.alloc * sizeof(mp_map_elem_t));
     return other_out;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(dict_copy_obj, mp_obj_dict_copy);
+static MP_DEFINE_CONST_FUN_OBJ_1(dict_copy_obj, mp_obj_dict_copy);
 
 #if MICROPY_PY_BUILTINS_DICT_FROMKEYS
 // this is a classmethod
-STATIC mp_obj_t dict_fromkeys(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t dict_fromkeys(size_t n_args, const mp_obj_t *args) {
     mp_obj_t iter = mp_getiter(args[1], NULL);
     mp_obj_t value = mp_const_none;
     mp_obj_t next = MP_OBJ_NULL;
@@ -296,17 +296,23 @@ STATIC mp_obj_t dict_fromkeys(size_t n_args, const mp_obj_t *args) {
     }
 
     mp_obj_dict_t *self = MP_OBJ_TO_PTR(self_out);
+    #if MICROPY_PY_COLLECTIONS_ORDEREDDICT
+    if (args[0] == MP_OBJ_FROM_PTR(&mp_type_ordereddict)) {
+        self->base.type = &mp_type_ordereddict;
+        self->map.is_ordered = 1;
+    }
+    #endif
     while ((next = mp_iternext(iter)) != MP_OBJ_STOP_ITERATION) {
         mp_map_lookup(&self->map, next, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND)->value = value;
     }
 
     return self_out;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(dict_fromkeys_fun_obj, 2, 3, dict_fromkeys);
-STATIC MP_DEFINE_CONST_CLASSMETHOD_OBJ(dict_fromkeys_obj, MP_ROM_PTR(&dict_fromkeys_fun_obj));
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(dict_fromkeys_fun_obj, 2, 3, dict_fromkeys);
+static MP_DEFINE_CONST_CLASSMETHOD_OBJ(dict_fromkeys_obj, MP_ROM_PTR(&dict_fromkeys_fun_obj));
 #endif
 
-STATIC mp_obj_t dict_get_helper(size_t n_args, const mp_obj_t *args, mp_map_lookup_kind_t lookup_kind) {
+static mp_obj_t dict_get_helper(size_t n_args, const mp_obj_t *args, mp_map_lookup_kind_t lookup_kind) {
     mp_check_self(mp_obj_is_dict_or_ordereddict(args[0]));
     mp_obj_dict_t *self = MP_OBJ_TO_PTR(args[0]);
     if (lookup_kind != MP_MAP_LOOKUP) {
@@ -336,22 +342,22 @@ STATIC mp_obj_t dict_get_helper(size_t n_args, const mp_obj_t *args, mp_map_look
     return value;
 }
 
-STATIC mp_obj_t dict_get(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t dict_get(size_t n_args, const mp_obj_t *args) {
     return dict_get_helper(n_args, args, MP_MAP_LOOKUP);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(dict_get_obj, 2, 3, dict_get);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(dict_get_obj, 2, 3, dict_get);
 
-STATIC mp_obj_t dict_pop(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t dict_pop(size_t n_args, const mp_obj_t *args) {
     return dict_get_helper(n_args, args, MP_MAP_LOOKUP_REMOVE_IF_FOUND);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(dict_pop_obj, 2, 3, dict_pop);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(dict_pop_obj, 2, 3, dict_pop);
 
-STATIC mp_obj_t dict_setdefault(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t dict_setdefault(size_t n_args, const mp_obj_t *args) {
     return dict_get_helper(n_args, args, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(dict_setdefault_obj, 2, 3, dict_setdefault);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(dict_setdefault_obj, 2, 3, dict_setdefault);
 
-STATIC mp_obj_t dict_popitem(mp_obj_t self_in) {
+static mp_obj_t dict_popitem(mp_obj_t self_in) {
     mp_check_self(mp_obj_is_dict_or_ordereddict(self_in));
     mp_obj_dict_t *self = MP_OBJ_TO_PTR(self_in);
     mp_ensure_not_fixed(self);
@@ -374,9 +380,9 @@ STATIC mp_obj_t dict_popitem(mp_obj_t self_in) {
 
     return tuple;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(dict_popitem_obj, dict_popitem);
+static MP_DEFINE_CONST_FUN_OBJ_1(dict_popitem_obj, dict_popitem);
 
-STATIC mp_obj_t dict_update(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) {
+static mp_obj_t dict_update(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) {
     mp_check_self(mp_obj_is_dict_or_ordereddict(args[0]));
     mp_obj_dict_t *self = MP_OBJ_TO_PTR(args[0]);
     mp_ensure_not_fixed(self);
@@ -424,14 +430,14 @@ STATIC mp_obj_t dict_update(size_t n_args, const mp_obj_t *args, mp_map_t *kwarg
 
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_KW(dict_update_obj, 1, dict_update);
+static MP_DEFINE_CONST_FUN_OBJ_KW(dict_update_obj, 1, dict_update);
 
 
 /******************************************************************************/
 /* dict views                                                                 */
 
-STATIC const mp_obj_type_t mp_type_dict_view;
-STATIC const mp_obj_type_t mp_type_dict_view_it;
+static const mp_obj_type_t mp_type_dict_view;
+static const mp_obj_type_t mp_type_dict_view_it;
 
 typedef enum _mp_dict_view_kind_t {
     MP_DICT_VIEW_ITEMS,
@@ -439,7 +445,7 @@ typedef enum _mp_dict_view_kind_t {
     MP_DICT_VIEW_VALUES,
 } mp_dict_view_kind_t;
 
-STATIC const char *const mp_dict_view_names[] = {"dict_items", "dict_keys", "dict_values"};
+static const char *const mp_dict_view_names[] = {"dict_items", "dict_keys", "dict_values"};
 
 typedef struct _mp_obj_dict_view_it_t {
     mp_obj_base_t base;
@@ -454,7 +460,7 @@ typedef struct _mp_obj_dict_view_t {
     mp_dict_view_kind_t kind;
 } mp_obj_dict_view_t;
 
-STATIC mp_obj_t dict_view_it_iternext(mp_obj_t self_in) {
+static mp_obj_t dict_view_it_iternext(mp_obj_t self_in) {
     mp_check_self(mp_obj_is_type(self_in, &mp_type_dict_view_it));
     mp_obj_dict_view_it_t *self = MP_OBJ_TO_PTR(self_in);
     mp_map_elem_t *next = dict_iter_next(MP_OBJ_TO_PTR(self->dict), &self->cur);
@@ -476,14 +482,14 @@ STATIC mp_obj_t dict_view_it_iternext(mp_obj_t self_in) {
     }
 }
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+static MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_dict_view_it,
     MP_QSTR_iterator,
     MP_TYPE_FLAG_ITER_IS_ITERNEXT,
     iter, dict_view_it_iternext
     );
 
-STATIC mp_obj_t dict_view_getiter(mp_obj_t view_in, mp_obj_iter_buf_t *iter_buf) {
+static mp_obj_t dict_view_getiter(mp_obj_t view_in, mp_obj_iter_buf_t *iter_buf) {
     assert(sizeof(mp_obj_dict_view_it_t) <= sizeof(mp_obj_iter_buf_t));
     mp_check_self(mp_obj_is_type(view_in, &mp_type_dict_view));
     mp_obj_dict_view_t *view = MP_OBJ_TO_PTR(view_in);
@@ -495,7 +501,7 @@ STATIC mp_obj_t dict_view_getiter(mp_obj_t view_in, mp_obj_iter_buf_t *iter_buf)
     return MP_OBJ_FROM_PTR(o);
 }
 
-STATIC void dict_view_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void dict_view_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     (void)kind;
     mp_check_self(mp_obj_is_type(self_in, &mp_type_dict_view));
     mp_obj_dict_view_t *self = MP_OBJ_TO_PTR(self_in);
@@ -515,16 +521,17 @@ STATIC void dict_view_print(const mp_print_t *print, mp_obj_t self_in, mp_print_
     mp_print_str(print, "])");
 }
 
-STATIC mp_obj_t dict_view_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
+static mp_obj_t dict_view_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
     mp_obj_dict_view_t *o = MP_OBJ_TO_PTR(o_in);
     // only dict.values() supports __hash__.
     if (op == MP_UNARY_OP_HASH && o->kind == MP_DICT_VIEW_VALUES) {
         return MP_OBJ_NEW_SMALL_INT((mp_uint_t)o_in);
     }
-    return MP_OBJ_NULL;
+    // delegate all other ops to dict unary op handler
+    return dict_unary_op(op, o->dict);
 }
 
-STATIC mp_obj_t dict_view_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
+static mp_obj_t dict_view_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
     // only supported for the 'keys' kind until sets and dicts are refactored
     mp_obj_dict_view_t *o = MP_OBJ_TO_PTR(lhs_in);
     if (o->kind != MP_DICT_VIEW_KEYS) {
@@ -536,7 +543,7 @@ STATIC mp_obj_t dict_view_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t
     return dict_binary_op(op, o->dict, rhs_in);
 }
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+static MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_dict_view,
     MP_QSTR_dict_view,
     MP_TYPE_FLAG_ITER_IS_GETITER,
@@ -546,37 +553,37 @@ STATIC MP_DEFINE_CONST_OBJ_TYPE(
     iter, dict_view_getiter
     );
 
-STATIC mp_obj_t mp_obj_new_dict_view(mp_obj_t dict, mp_dict_view_kind_t kind) {
+static mp_obj_t mp_obj_new_dict_view(mp_obj_t dict, mp_dict_view_kind_t kind) {
     mp_obj_dict_view_t *o = mp_obj_malloc(mp_obj_dict_view_t, &mp_type_dict_view);
     o->dict = dict;
     o->kind = kind;
     return MP_OBJ_FROM_PTR(o);
 }
 
-STATIC mp_obj_t dict_view(mp_obj_t self_in, mp_dict_view_kind_t kind) {
+static mp_obj_t dict_view(mp_obj_t self_in, mp_dict_view_kind_t kind) {
     mp_check_self(mp_obj_is_dict_or_ordereddict(self_in));
     return mp_obj_new_dict_view(self_in, kind);
 }
 
-STATIC mp_obj_t dict_items(mp_obj_t self_in) {
+static mp_obj_t dict_items(mp_obj_t self_in) {
     return dict_view(self_in, MP_DICT_VIEW_ITEMS);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(dict_items_obj, dict_items);
+static MP_DEFINE_CONST_FUN_OBJ_1(dict_items_obj, dict_items);
 
-STATIC mp_obj_t dict_keys(mp_obj_t self_in) {
+static mp_obj_t dict_keys(mp_obj_t self_in) {
     return dict_view(self_in, MP_DICT_VIEW_KEYS);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(dict_keys_obj, dict_keys);
+static MP_DEFINE_CONST_FUN_OBJ_1(dict_keys_obj, dict_keys);
 
-STATIC mp_obj_t dict_values(mp_obj_t self_in) {
+static mp_obj_t dict_values(mp_obj_t self_in) {
     return dict_view(self_in, MP_DICT_VIEW_VALUES);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(dict_values_obj, dict_values);
+static MP_DEFINE_CONST_FUN_OBJ_1(dict_values_obj, dict_values);
 
 /******************************************************************************/
 /* dict iterator                                                              */
 
-STATIC mp_obj_t dict_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
+static mp_obj_t dict_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
     assert(sizeof(mp_obj_dict_view_it_t) <= sizeof(mp_obj_iter_buf_t));
     mp_check_self(mp_obj_is_dict_or_ordereddict(self_in));
     mp_obj_dict_view_it_t *o = (mp_obj_dict_view_it_t *)iter_buf;
@@ -590,7 +597,7 @@ STATIC mp_obj_t dict_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
 /******************************************************************************/
 /* dict constructors & public C API                                           */
 
-STATIC const mp_rom_map_elem_t dict_locals_dict_table[] = {
+static const mp_rom_map_elem_t dict_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&dict_clear_obj) },
     { MP_ROM_QSTR(MP_QSTR_copy), MP_ROM_PTR(&dict_copy_obj) },
     #if MICROPY_PY_BUILTINS_DICT_FROMKEYS
@@ -609,7 +616,7 @@ STATIC const mp_rom_map_elem_t dict_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR___delitem__), MP_ROM_PTR(&mp_op_delitem_obj) },
 };
 
-STATIC MP_DEFINE_CONST_DICT(dict_locals_dict, dict_locals_dict_table);
+static MP_DEFINE_CONST_DICT(dict_locals_dict, dict_locals_dict_table);
 
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_dict,
diff --git a/py/objenumerate.c b/py/objenumerate.c
index 40bed919ed7..8217a0d4f99 100644
--- a/py/objenumerate.c
+++ b/py/objenumerate.c
@@ -37,9 +37,9 @@ typedef struct _mp_obj_enumerate_t {
     mp_int_t cur;
 } mp_obj_enumerate_t;
 
-STATIC mp_obj_t enumerate_iternext(mp_obj_t self_in);
+static mp_obj_t enumerate_iternext(mp_obj_t self_in);
 
-STATIC mp_obj_t enumerate_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t enumerate_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     #if MICROPY_CPYTHON_COMPAT
     static const mp_arg_t allowed_args[] = {
         { MP_QSTR_iterable, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
@@ -75,7 +75,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
     iter, enumerate_iternext
     );
 
-STATIC mp_obj_t enumerate_iternext(mp_obj_t self_in) {
+static mp_obj_t enumerate_iternext(mp_obj_t self_in) {
     assert(mp_obj_is_type(self_in, &mp_type_enumerate));
     mp_obj_enumerate_t *self = MP_OBJ_TO_PTR(self_in);
     mp_obj_t next = mp_iternext(self->iter);
diff --git a/py/objexcept.c b/py/objexcept.c
index cc0ddd9dc5f..a3c302c3819 100644
--- a/py/objexcept.c
+++ b/py/objexcept.c
@@ -81,6 +81,8 @@ void mp_init_emergency_exception_buf(void) {
 #else
 #define mp_emergency_exception_buf_size MP_STATE_VM(mp_emergency_exception_buf_size)
 
+#include "py/mphal.h" // for MICROPY_BEGIN_ATOMIC_SECTION/MICROPY_END_ATOMIC_SECTION
+
 void mp_init_emergency_exception_buf(void) {
     mp_emergency_exception_buf_size = 0;
     MP_STATE_VM(mp_emergency_exception_buf) = NULL;
@@ -115,7 +117,7 @@ bool mp_obj_is_native_exception_instance(mp_obj_t self_in) {
     return MP_OBJ_TYPE_GET_SLOT_OR_NULL(mp_obj_get_type(self_in), make_new) == mp_obj_exception_make_new;
 }
 
-STATIC mp_obj_exception_t *get_native_exception(mp_obj_t self_in) {
+static mp_obj_exception_t *get_native_exception(mp_obj_t self_in) {
     assert(mp_obj_is_exception_instance(self_in));
     if (mp_obj_is_native_exception_instance(self_in)) {
         return MP_OBJ_TO_PTR(self_in);
@@ -124,7 +126,7 @@ STATIC mp_obj_exception_t *get_native_exception(mp_obj_t self_in) {
     }
 }
 
-STATIC void decompress_error_text_maybe(mp_obj_exception_t *o) {
+static void decompress_error_text_maybe(mp_obj_exception_t *o) {
     #if MICROPY_ROM_TEXT_COMPRESSION
     if (o->args->len == 1 && mp_obj_is_exact_type(o->args->items[0], &mp_type_str)) {
         mp_obj_str_t *o_str = MP_OBJ_TO_PTR(o->args->items[0]);
@@ -163,7 +165,7 @@ void mp_obj_exception_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kin
     mp_print_kind_t k = kind & ~PRINT_EXC_SUBCLASS;
     bool is_subclass = kind & PRINT_EXC_SUBCLASS;
     if (!is_subclass && (k == PRINT_REPR || k == PRINT_EXC)) {
-        mp_print_str(print, qstr_str(o->base.type->name));
+        mp_print_str(print, mp_obj_get_type_str(o_in));
     }
 
     if (k == PRINT_EXC) {
@@ -178,7 +180,7 @@ void mp_obj_exception_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kin
             return;
         }
 
-        #if MICROPY_PY_UERRNO
+        #if MICROPY_PY_ERRNO
         // try to provide a nice OSError error message
         if (o->base.type == &mp_type_OSError && o->args->len > 0 && o->args->len < 3 && mp_obj_is_small_int(o->args->items[0])) {
             qstr qst = mp_errno_to_str(o->args->items[0]);
@@ -221,7 +223,7 @@ mp_obj_t mp_obj_exception_make_new(const mp_obj_type_t *type, size_t n_args, siz
         o_tuple = (mp_obj_tuple_t *)&mp_const_empty_tuple_obj;
     } else {
         // Try to allocate memory for the tuple containing the args
-        o_tuple = m_new_obj_var_maybe(mp_obj_tuple_t, mp_obj_t, n_args);
+        o_tuple = m_new_obj_var_maybe(mp_obj_tuple_t, items, mp_obj_t, n_args);
 
         #if MICROPY_ENABLE_EMERGENCY_EXCEPTION_BUF
         // If we are called by mp_obj_new_exception_msg_varg then it will have
@@ -270,7 +272,7 @@ void mp_obj_exception_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
             // We allow 'exc.__traceback__ = None' assignment as low-level
             // optimization of pre-allocating exception instance and raising
             // it repeatedly - this avoids memory allocation during raise.
-            // However, uPy will keep adding traceback entries to such
+            // However, MicroPython will keep adding traceback entries to such
             // exception instance, so before throwing it, traceback should
             // be cleared like above.
             self->traceback_len = 0;
@@ -437,7 +439,7 @@ struct _exc_printer_t {
     byte *buf;
 };
 
-STATIC void exc_add_strn(void *data, const char *str, size_t len) {
+static void exc_add_strn(void *data, const char *str, size_t len) {
     struct _exc_printer_t *pr = data;
     if (pr->len + len >= pr->alloc) {
         // Not enough room for data plus a null byte so try to grow the buffer
@@ -551,7 +553,14 @@ bool mp_obj_is_exception_type(mp_obj_t self_in) {
 
 // return true if the given object is an instance of an exception type
 bool mp_obj_is_exception_instance(mp_obj_t self_in) {
-    return mp_obj_is_exception_type(MP_OBJ_FROM_PTR(mp_obj_get_type(self_in)));
+    if (mp_obj_is_native_exception_instance(self_in)) {
+        return true;
+    }
+    if (!mp_obj_is_exception_type(MP_OBJ_FROM_PTR(mp_obj_get_type(self_in)))) {
+        return false;
+    }
+    mp_obj_instance_t *self = MP_OBJ_TO_PTR(self_in);
+    return self->subobj[0] != MP_OBJ_FROM_PTR((void *)&mp_native_base_init_wrapper_obj);
 }
 
 // Return true if exception (type or instance) is a subclass of given
diff --git a/py/objfilter.c b/py/objfilter.c
index 2a657fde4b7..7f1f700f6a6 100644
--- a/py/objfilter.c
+++ b/py/objfilter.c
@@ -34,7 +34,7 @@ typedef struct _mp_obj_filter_t {
     mp_obj_t iter;
 } mp_obj_filter_t;
 
-STATIC mp_obj_t filter_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t filter_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     mp_arg_check_num(n_args, n_kw, 2, 2, false);
     mp_obj_filter_t *o = mp_obj_malloc(mp_obj_filter_t, type);
     o->fun = args[0];
@@ -42,7 +42,7 @@ STATIC mp_obj_t filter_make_new(const mp_obj_type_t *type, size_t n_args, size_t
     return MP_OBJ_FROM_PTR(o);
 }
 
-STATIC mp_obj_t filter_iternext(mp_obj_t self_in) {
+static mp_obj_t filter_iternext(mp_obj_t self_in) {
     mp_check_self(mp_obj_is_type(self_in, &mp_type_filter));
     mp_obj_filter_t *self = MP_OBJ_TO_PTR(self_in);
     mp_obj_t next;
diff --git a/py/objfloat.c b/py/objfloat.c
index c862b4843b2..b0ad70de4a3 100644
--- a/py/objfloat.c
+++ b/py/objfloat.c
@@ -34,6 +34,11 @@
 
 #if MICROPY_PY_BUILTINS_FLOAT
 
+// Workaround a bug in Windows SDK version 10.0.26100.0, where NAN is no longer constant.
+#if defined(_MSC_VER) && !defined(_UCRT_NOISY_NAN)
+#define _UCRT_NOISY_NAN
+#endif
+
 #include 
 #include "py/formatfloat.h"
 
@@ -102,29 +107,13 @@ mp_int_t mp_float_hash(mp_float_t src) {
 }
 #endif
 
-STATIC void float_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
+static void float_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     (void)kind;
     mp_float_t o_val = mp_obj_float_get(o_in);
-    #if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT
-    char buf[16];
-    #if MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_C
-    const int precision = 6;
-    #else
-    const int precision = 7;
-    #endif
-    #else
-    char buf[32];
-    const int precision = 16;
-    #endif
-    mp_format_float(o_val, buf, sizeof(buf), 'g', precision, '\0');
-    mp_print_str(print, buf);
-    if (strchr(buf, '.') == NULL && strchr(buf, 'e') == NULL && strchr(buf, 'n') == NULL) {
-        // Python floats always have decimal point (unless inf or nan)
-        mp_print_str(print, ".0");
-    }
+    mp_print_float(print, o_val, 'g', PF_FLAG_ALWAYS_DECIMAL, '\0', -1, MP_FLOAT_REPR_PREC);
 }
 
-STATIC mp_obj_t float_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t float_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)type_in;
     mp_arg_check_num(n_args, n_kw, 0, 1, false);
 
@@ -149,7 +138,7 @@ STATIC mp_obj_t float_make_new(const mp_obj_type_t *type_in, size_t n_args, size
     }
 }
 
-STATIC mp_obj_t float_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
+static mp_obj_t float_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
     mp_float_t val = mp_obj_float_get(o_in);
     switch (op) {
         case MP_UNARY_OP_BOOL:
@@ -172,7 +161,7 @@ STATIC mp_obj_t float_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
     }
 }
 
-STATIC mp_obj_t float_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
+static mp_obj_t float_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
     mp_float_t lhs_val = mp_obj_float_get(lhs_in);
     #if MICROPY_PY_BUILTINS_COMPLEX
     if (mp_obj_is_type(rhs_in, &mp_type_complex)) {
@@ -208,7 +197,7 @@ mp_float_t mp_obj_float_get(mp_obj_t self_in) {
 
 #endif
 
-STATIC void mp_obj_float_divmod(mp_float_t *x, mp_float_t *y) {
+static void mp_obj_float_divmod(mp_float_t *x, mp_float_t *y) {
     // logic here follows that of CPython
     // https://docs.python.org/3/reference/expressions.html#binary-arithmetic-operations
     // x == (x//y)*y + (x%y)
@@ -312,6 +301,10 @@ mp_obj_t mp_obj_float_binary_op(mp_binary_op_t op, mp_float_t lhs_val, mp_obj_t
                 lhs_val = MICROPY_FLOAT_CONST(1.0);
                 break;
             }
+            if (isnan(rhs_val)) {
+                lhs_val = rhs_val;
+                break;
+            }
             #endif
             lhs_val = MICROPY_FLOAT_C_FUN(pow)(lhs_val, rhs_val);
             break;
diff --git a/py/objfun.c b/py/objfun.c
index 94b33cd5bd8..5f2a10c6e4a 100644
--- a/py/objfun.c
+++ b/py/objfun.c
@@ -28,11 +28,13 @@
 #include 
 #include 
 
+#include "py/emitglue.h"
+#include "py/objcode.h"
 #include "py/objtuple.h"
 #include "py/objfun.h"
 #include "py/runtime.h"
 #include "py/bc.h"
-#include "py/stackctrl.h"
+#include "py/cstack.h"
 
 #if MICROPY_DEBUG_VERBOSE // print debugging info
 #define DEBUG_PRINT (1)
@@ -48,7 +50,7 @@
 /******************************************************************************/
 /* builtin functions                                                          */
 
-STATIC mp_obj_t fun_builtin_0_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t fun_builtin_0_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)args;
     assert(mp_obj_is_type(self_in, &mp_type_fun_builtin_0));
     mp_obj_fun_builtin_fixed_t *self = MP_OBJ_TO_PTR(self_in);
@@ -61,7 +63,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
     call, fun_builtin_0_call
     );
 
-STATIC mp_obj_t fun_builtin_1_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t fun_builtin_1_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     assert(mp_obj_is_type(self_in, &mp_type_fun_builtin_1));
     mp_obj_fun_builtin_fixed_t *self = MP_OBJ_TO_PTR(self_in);
     mp_arg_check_num(n_args, n_kw, 1, 1, false);
@@ -73,7 +75,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
     call, fun_builtin_1_call
     );
 
-STATIC mp_obj_t fun_builtin_2_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t fun_builtin_2_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     assert(mp_obj_is_type(self_in, &mp_type_fun_builtin_2));
     mp_obj_fun_builtin_fixed_t *self = MP_OBJ_TO_PTR(self_in);
     mp_arg_check_num(n_args, n_kw, 2, 2, false);
@@ -85,7 +87,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
     call, fun_builtin_2_call
     );
 
-STATIC mp_obj_t fun_builtin_3_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t fun_builtin_3_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     assert(mp_obj_is_type(self_in, &mp_type_fun_builtin_3));
     mp_obj_fun_builtin_fixed_t *self = MP_OBJ_TO_PTR(self_in);
     mp_arg_check_num(n_args, n_kw, 3, 3, false);
@@ -97,7 +99,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
     call, fun_builtin_3_call
     );
 
-STATIC mp_obj_t fun_builtin_var_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t fun_builtin_var_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     assert(mp_obj_is_type(self_in, &mp_type_fun_builtin_var));
     mp_obj_fun_builtin_var_t *self = MP_OBJ_TO_PTR(self_in);
 
@@ -107,7 +109,9 @@ STATIC mp_obj_t fun_builtin_var_call(mp_obj_t self_in, size_t n_args, size_t n_k
     if (self->sig & 1) {
         // function allows keywords
 
-        // we create a map directly from the given args array
+        // we create a map directly from the given args array; self->fun.kw does still
+        // expect args to have both positional and keyword arguments, ordered as:
+        // arg0 arg1 ... arg key0 value0 key1 value1 ... key value
         mp_map_t kw_args;
         mp_map_init_fixed_table(&kw_args, n_kw, args + n_args);
 
@@ -128,43 +132,60 @@ MP_DEFINE_CONST_OBJ_TYPE(
 /******************************************************************************/
 /* byte code functions                                                        */
 
-STATIC qstr mp_obj_code_get_name(const mp_obj_fun_bc_t *fun, const byte *code_info) {
-    MP_BC_PRELUDE_SIZE_DECODE(code_info);
-    mp_uint_t name = mp_decode_uint_value(code_info);
+qstr mp_obj_fun_bc_get_name(const mp_obj_fun_bc_t *fun) {
+    const byte *bc = fun->bytecode;
+
+    #if MICROPY_ENABLE_NATIVE_CODE
+    if (fun->base.type == &mp_type_fun_native || fun->base.type == &mp_type_native_gen_wrap) {
+        bc = mp_obj_fun_native_get_prelude_ptr(fun);
+    }
+    #endif
+
+    MP_BC_PRELUDE_SIG_DECODE(bc);
+    MP_BC_PRELUDE_SIZE_DECODE(bc);
+
+    mp_uint_t name = mp_decode_uint_value(bc);
     #if MICROPY_EMIT_BYTECODE_USES_QSTR_TABLE
     name = fun->context->constants.qstr_table[name];
     #endif
+
     return name;
 }
 
-#if MICROPY_EMIT_NATIVE
-STATIC const mp_obj_type_t mp_type_fun_native;
-#endif
+#if MICROPY_PY_FUNCTION_ATTRS_CODE
+static mp_obj_t fun_bc_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+    (void)type;
+    mp_arg_check_num(n_args, n_kw, 2, 2, false);
 
-qstr mp_obj_fun_get_name(mp_const_obj_t fun_in) {
-    const mp_obj_fun_bc_t *fun = MP_OBJ_TO_PTR(fun_in);
-    #if MICROPY_EMIT_NATIVE
-    if (fun->base.type == &mp_type_fun_native || fun->base.type == &mp_type_native_gen_wrap) {
-        // TODO native functions don't have name stored
-        return MP_QSTR_;
+    if (!mp_obj_is_type(args[0], &mp_type_code)) {
+        mp_raise_TypeError(NULL);
+    }
+    if (!mp_obj_is_type(args[1], &mp_type_dict)) {
+        mp_raise_TypeError(NULL);
     }
-    #endif
 
-    const byte *bc = fun->bytecode;
-    MP_BC_PRELUDE_SIG_DECODE(bc);
-    return mp_obj_code_get_name(fun, bc);
+    mp_obj_code_t *code = MP_OBJ_TO_PTR(args[0]);
+    mp_obj_t globals = args[1];
+
+    mp_module_context_t *module_context = m_new_obj(mp_module_context_t);
+    module_context->module.base.type = &mp_type_module;
+    module_context->module.globals = MP_OBJ_TO_PTR(globals);
+    module_context->constants = *mp_code_get_constants(code);
+
+    return mp_make_function_from_proto_fun(mp_code_get_proto_fun(code), module_context, NULL);
 }
+#endif
 
 #if MICROPY_CPYTHON_COMPAT
-STATIC void fun_bc_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
+static void fun_bc_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_fun_bc_t *o = MP_OBJ_TO_PTR(o_in);
-    mp_printf(print, "", mp_obj_fun_get_name(o_in), o);
+    mp_printf(print, "", mp_obj_fun_bc_get_name(o), o);
 }
 #endif
 
 #if DEBUG_PRINT
-STATIC void dump_args(const mp_obj_t *a, size_t sz) {
+static void dump_args(const mp_obj_t *a, size_t sz) {
     DEBUG_printf("%p: ", a);
     for (size_t i = 0; i < sz; i++) {
         DEBUG_printf("%p ", a[i]);
@@ -200,7 +221,7 @@ STATIC void dump_args(const mp_obj_t *a, size_t sz) {
 
 #if MICROPY_STACKLESS
 mp_code_state_t *mp_obj_fun_bc_prepare_codestate(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
-    MP_STACK_CHECK();
+    mp_cstack_check();
     mp_obj_fun_bc_t *self = MP_OBJ_TO_PTR(self_in);
 
     size_t n_state, state_size;
@@ -215,7 +236,7 @@ mp_code_state_t *mp_obj_fun_bc_prepare_codestate(mp_obj_t self_in, size_t n_args
     // RuntimeError should be raised instead. So, we use m_new_obj_var_maybe(),
     // return NULL, then vm.c takes the needed action (either raise
     // RuntimeError or fallback to stack allocation).
-    code_state = m_new_obj_var_maybe(mp_code_state_t, byte, state_size);
+    code_state = m_new_obj_var_maybe(mp_code_state_t, state, byte, state_size);
     if (!code_state) {
         return NULL;
     }
@@ -230,8 +251,8 @@ mp_code_state_t *mp_obj_fun_bc_prepare_codestate(mp_obj_t self_in, size_t n_args
 }
 #endif
 
-STATIC mp_obj_t fun_bc_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
-    MP_STACK_CHECK();
+static mp_obj_t fun_bc_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+    mp_cstack_check();
 
     DEBUG_printf("Input n_args: " UINT_FMT ", n_kw: " UINT_FMT "\n", n_args, n_kw);
     DEBUG_printf("Input pos args: ");
@@ -247,10 +268,10 @@ STATIC mp_obj_t fun_bc_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const
     // allocate state for locals and stack
     mp_code_state_t *code_state = NULL;
     #if MICROPY_ENABLE_PYSTACK
-    code_state = mp_pystack_alloc(sizeof(mp_code_state_t) + state_size);
+    code_state = mp_pystack_alloc(offsetof(mp_code_state_t, state) + state_size);
     #else
     if (state_size > VM_MAX_STATE_ON_STACK) {
-        code_state = m_new_obj_var_maybe(mp_code_state_t, byte, state_size);
+        code_state = m_new_obj_var_maybe(mp_code_state_t, state, byte, state_size);
         #if MICROPY_DEBUG_VM_STACK_OVERFLOW
         if (code_state != NULL) {
             memset(code_state->state, 0, state_size);
@@ -258,7 +279,7 @@ STATIC mp_obj_t fun_bc_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const
         #endif
     }
     if (code_state == NULL) {
-        code_state = alloca(sizeof(mp_code_state_t) + state_size);
+        code_state = alloca(offsetof(mp_code_state_t, state) + state_size);
         #if MICROPY_DEBUG_VM_STACK_OVERFLOW
         memset(code_state->state, 0, state_size);
         #endif
@@ -320,7 +341,7 @@ STATIC mp_obj_t fun_bc_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const
     #else
     // free the state if it was allocated on the heap
     if (state_size != 0) {
-        m_del_var(mp_code_state_t, byte, state_size, code_state);
+        m_del_var(mp_code_state_t, state, byte, state_size, code_state);
     }
     #endif
 
@@ -337,16 +358,48 @@ void mp_obj_fun_bc_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
         // not load attribute
         return;
     }
+    const mp_obj_fun_bc_t *self = MP_OBJ_TO_PTR(self_in);
     if (attr == MP_QSTR___name__) {
-        dest[0] = MP_OBJ_NEW_QSTR(mp_obj_fun_get_name(self_in));
+        dest[0] = MP_OBJ_NEW_QSTR(mp_obj_fun_bc_get_name(self));
     }
     if (attr == MP_QSTR___globals__) {
-        mp_obj_fun_bc_t *self = MP_OBJ_TO_PTR(self_in);
         dest[0] = MP_OBJ_FROM_PTR(self->context->module.globals);
     }
+    #if MICROPY_PY_FUNCTION_ATTRS_CODE
+    if (attr == MP_QSTR___code__) {
+        if (self->base.type == &mp_type_fun_bc
+            || self->base.type == &mp_type_gen_wrap) {
+            #if MICROPY_PY_BUILTINS_CODE <= MICROPY_PY_BUILTINS_CODE_BASIC
+            #if MICROPY_PERSISTENT_CODE_SAVE
+            #error "MICROPY_PERSISTENT_CODE_SAVE can't be enabled at this level of MICROPY_PY_BUILTINS_CODE"
+            #endif
+            mp_proto_fun_t proto_fun;
+            if (self->child_table != NULL) {
+                mp_raw_code_truncated_t *rc = m_new0(mp_raw_code_truncated_t, 1);
+                rc->kind = MP_CODE_BYTECODE;
+                rc->is_generator = self->base.type == &mp_type_gen_wrap;
+                rc->fun_data = self->bytecode;
+                rc->children = (mp_raw_code_t **)self->child_table;
+                proto_fun = rc;
+            } else {
+                proto_fun = self->bytecode;
+            }
+            dest[0] = mp_obj_new_code(self->context->constants, proto_fun);
+            #else
+            dest[0] = mp_obj_new_code(self->context, self->rc, true);
+            #endif
+        }
+    }
+    #endif
 }
 #endif
 
+#if MICROPY_PY_FUNCTION_ATTRS_CODE
+#define FUN_BC_MAKE_NEW make_new, fun_bc_make_new,
+#else
+#define FUN_BC_MAKE_NEW
+#endif
+
 #if MICROPY_CPYTHON_COMPAT
 #define FUN_BC_TYPE_PRINT print, fun_bc_print,
 #else
@@ -363,6 +416,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_fun_bc,
     MP_QSTR_function,
     MP_TYPE_FLAG_BINDS_SELF,
+    FUN_BC_MAKE_NEW
     FUN_BC_TYPE_PRINT
     FUN_BC_TYPE_ATTR
     call, fun_bc_call
@@ -384,7 +438,7 @@ mp_obj_t mp_obj_new_fun_bc(const mp_obj_t *def_args, const byte *code, const mp_
         def_kw_args = def_args[1];
         n_extra_args += 1;
     }
-    mp_obj_fun_bc_t *o = mp_obj_malloc_var(mp_obj_fun_bc_t, mp_obj_t, n_extra_args, &mp_type_fun_bc);
+    mp_obj_fun_bc_t *o = mp_obj_malloc_var(mp_obj_fun_bc_t, extra_args, mp_obj_t, n_extra_args, &mp_type_fun_bc);
     o->bytecode = code;
     o->context = context;
     o->child_table = child_table;
@@ -400,12 +454,12 @@ mp_obj_t mp_obj_new_fun_bc(const mp_obj_t *def_args, const byte *code, const mp_
 /******************************************************************************/
 /* native functions                                                           */
 
-#if MICROPY_EMIT_NATIVE
+#if MICROPY_ENABLE_NATIVE_CODE
 
-STATIC mp_obj_t fun_native_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
-    MP_STACK_CHECK();
+static mp_obj_t fun_native_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+    mp_cstack_check();
     mp_obj_fun_bc_t *self = MP_OBJ_TO_PTR(self_in);
-    mp_call_fun_t fun = MICROPY_MAKE_POINTER_CALLABLE((void *)self->bytecode);
+    mp_call_fun_t fun = mp_obj_fun_native_get_function_start(self);
     return fun(self_in, n_args, n_kw, args);
 }
 
@@ -420,7 +474,7 @@ STATIC mp_obj_t fun_native_call(mp_obj_t self_in, size_t n_args, size_t n_kw, co
 #define FUN_BC_TYPE_ATTR
 #endif
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_fun_native,
     MP_QSTR_function,
     MP_TYPE_FLAG_BINDS_SELF,
@@ -429,26 +483,30 @@ STATIC MP_DEFINE_CONST_OBJ_TYPE(
     call, fun_native_call
     );
 
-mp_obj_t mp_obj_new_fun_native(const mp_obj_t *def_args, const void *fun_data, const mp_module_context_t *mc, struct _mp_raw_code_t *const *child_table) {
-    mp_obj_fun_bc_t *o = MP_OBJ_TO_PTR(mp_obj_new_fun_bc(def_args, (const byte *)fun_data, mc, child_table));
-    o->base.type = &mp_type_fun_native;
-    return MP_OBJ_FROM_PTR(o);
+/******************************************************************************/
+/* viper functions                                                           */
+
+static mp_obj_t fun_viper_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+    mp_cstack_check();
+    mp_obj_fun_bc_t *self = MP_OBJ_TO_PTR(self_in);
+    mp_call_fun_t fun = MICROPY_MAKE_POINTER_CALLABLE((void *)self->bytecode);
+    return fun(self_in, n_args, n_kw, args);
 }
 
-#endif // MICROPY_EMIT_NATIVE
+MP_DEFINE_CONST_OBJ_TYPE(
+    mp_type_fun_viper,
+    MP_QSTR_function,
+    MP_TYPE_FLAG_BINDS_SELF,
+    call, fun_viper_call
+    );
+
+#endif // MICROPY_ENABLE_NATIVE_CODE
 
 /******************************************************************************/
 /* inline assembler functions                                                 */
 
 #if MICROPY_EMIT_INLINE_ASM
 
-typedef struct _mp_obj_fun_asm_t {
-    mp_obj_base_t base;
-    size_t n_args;
-    const void *fun_data; // GC must be able to trace this pointer
-    mp_uint_t type_sig;
-} mp_obj_fun_asm_t;
-
 typedef mp_uint_t (*inline_asm_fun_0_t)(void);
 typedef mp_uint_t (*inline_asm_fun_1_t)(mp_uint_t);
 typedef mp_uint_t (*inline_asm_fun_2_t)(mp_uint_t, mp_uint_t);
@@ -456,7 +514,7 @@ typedef mp_uint_t (*inline_asm_fun_3_t)(mp_uint_t, mp_uint_t, mp_uint_t);
 typedef mp_uint_t (*inline_asm_fun_4_t)(mp_uint_t, mp_uint_t, mp_uint_t, mp_uint_t);
 
 // convert a MicroPython object to a sensible value for inline asm
-STATIC mp_uint_t convert_obj_for_inline_asm(mp_obj_t obj) {
+static mp_uint_t convert_obj_for_inline_asm(mp_obj_t obj) {
     // TODO for byte_array, pass pointer to the array
     if (mp_obj_is_small_int(obj)) {
         return MP_OBJ_SMALL_INT_VALUE(obj);
@@ -499,7 +557,7 @@ STATIC mp_uint_t convert_obj_for_inline_asm(mp_obj_t obj) {
     }
 }
 
-STATIC mp_obj_t fun_asm_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t fun_asm_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     mp_obj_fun_asm_t *self = MP_OBJ_TO_PTR(self_in);
 
     mp_arg_check_num(n_args, n_kw, self->n_args, self->n_args, false);
@@ -529,19 +587,11 @@ STATIC mp_obj_t fun_asm_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const
     return mp_native_to_obj(ret, self->type_sig);
 }
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_fun_asm,
     MP_QSTR_function,
     MP_TYPE_FLAG_BINDS_SELF,
     call, fun_asm_call
     );
 
-mp_obj_t mp_obj_new_fun_asm(size_t n_args, const void *fun_data, mp_uint_t type_sig) {
-    mp_obj_fun_asm_t *o = mp_obj_malloc(mp_obj_fun_asm_t, &mp_type_fun_asm);
-    o->n_args = n_args;
-    o->fun_data = fun_data;
-    o->type_sig = type_sig;
-    return MP_OBJ_FROM_PTR(o);
-}
-
 #endif // MICROPY_EMIT_INLINE_ASM
diff --git a/py/objfun.h b/py/objfun.h
index 9de15b88415..0294a44ae78 100644
--- a/py/objfun.h
+++ b/py/objfun.h
@@ -43,9 +43,71 @@ typedef struct _mp_obj_fun_bc_t {
     mp_obj_t extra_args[];
 } mp_obj_fun_bc_t;
 
+typedef struct _mp_obj_fun_asm_t {
+    mp_obj_base_t base;
+    size_t n_args;
+    const void *fun_data; // GC must be able to trace this pointer
+    mp_uint_t type_sig;
+} mp_obj_fun_asm_t;
+
 mp_obj_t mp_obj_new_fun_bc(const mp_obj_t *def_args, const byte *code, const mp_module_context_t *cm, struct _mp_raw_code_t *const *raw_code_table);
-mp_obj_t mp_obj_new_fun_native(const mp_obj_t *def_args, const void *fun_data, const mp_module_context_t *cm, struct _mp_raw_code_t *const *raw_code_table);
-mp_obj_t mp_obj_new_fun_asm(size_t n_args, const void *fun_data, mp_uint_t type_sig);
+qstr mp_obj_fun_bc_get_name(const mp_obj_fun_bc_t *fun);
 void mp_obj_fun_bc_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest);
 
+#if MICROPY_ENABLE_NATIVE_CODE
+
+static inline mp_obj_t mp_obj_new_fun_native(const mp_obj_t *def_args, const void *fun_data, const mp_module_context_t *mc, struct _mp_raw_code_t *const *child_table) {
+    mp_obj_fun_bc_t *o = (mp_obj_fun_bc_t *)MP_OBJ_TO_PTR(mp_obj_new_fun_bc(def_args, (const byte *)fun_data, mc, child_table));
+    o->base.type = &mp_type_fun_native;
+    return MP_OBJ_FROM_PTR(o);
+}
+
+static inline mp_obj_t mp_obj_new_fun_viper(const void *fun_data, const mp_module_context_t *mc, struct _mp_raw_code_t *const *child_table) {
+    mp_obj_fun_bc_t *o = mp_obj_malloc(mp_obj_fun_bc_t, &mp_type_fun_viper);
+    o->bytecode = (const byte *)fun_data;
+    o->context = mc;
+    o->child_table = child_table;
+    return MP_OBJ_FROM_PTR(o);
+}
+
+static inline const uint8_t *mp_obj_fun_native_get_prelude_ptr(const mp_obj_fun_bc_t *fun_native) {
+    // Obtain a pointer to the start of the function prelude, based on prelude_ptr_index.
+    uintptr_t prelude_ptr_index = ((uintptr_t *)fun_native->bytecode)[0];
+    const uint8_t *prelude_ptr;
+    if (prelude_ptr_index == 0) {
+        prelude_ptr = (const uint8_t *)fun_native->child_table;
+    } else {
+        prelude_ptr = (const uint8_t *)fun_native->child_table[prelude_ptr_index];
+    }
+    return prelude_ptr;
+}
+
+static inline void *mp_obj_fun_native_get_function_start(const mp_obj_fun_bc_t *fun_native) {
+    // Obtain a pointer to the start of the function executable machine code.
+    return MICROPY_MAKE_POINTER_CALLABLE((void *)(fun_native->bytecode + sizeof(uintptr_t)));
+}
+
+static inline void *mp_obj_fun_native_get_generator_start(const mp_obj_fun_bc_t *fun_native) {
+    // Obtain a pointer to the start of the generator executable machine code.
+    uintptr_t start_offset = ((uintptr_t *)fun_native->bytecode)[1];
+    return MICROPY_MAKE_POINTER_CALLABLE((void *)(fun_native->bytecode + start_offset));
+}
+
+static inline void *mp_obj_fun_native_get_generator_resume(const mp_obj_fun_bc_t *fun_native) {
+    // Obtain a pointer to the resume location of the generator executable machine code.
+    return MICROPY_MAKE_POINTER_CALLABLE((void *)&((uintptr_t *)fun_native->bytecode)[2]);
+}
+
+#endif
+
+#if MICROPY_EMIT_INLINE_ASM
+static inline mp_obj_t mp_obj_new_fun_asm(size_t n_args, const void *fun_data, mp_uint_t type_sig) {
+    mp_obj_fun_asm_t *o = (mp_obj_fun_asm_t *)mp_obj_malloc(mp_obj_fun_asm_t, &mp_type_fun_asm);
+    o->n_args = n_args;
+    o->fun_data = (const byte *)fun_data;
+    o->type_sig = type_sig;
+    return MP_OBJ_FROM_PTR(o);
+}
+#endif
+
 #endif // MICROPY_INCLUDED_PY_OBJFUN_H
diff --git a/py/objgenerator.c b/py/objgenerator.c
index ecd1f583ea0..a0cee5d481d 100644
--- a/py/objgenerator.c
+++ b/py/objgenerator.c
@@ -33,7 +33,7 @@
 #include "py/objstr.h"
 #include "py/objgenerator.h"
 #include "py/objfun.h"
-#include "py/stackctrl.h"
+#include "py/cstack.h"
 
 // Instance of GeneratorExit exception - needed by generator.close()
 const mp_obj_exception_t mp_const_GeneratorExit_obj = {{&mp_type_GeneratorExit}, 0, 0, NULL, (mp_obj_tuple_t *)&mp_const_empty_tuple_obj};
@@ -50,7 +50,7 @@ typedef struct _mp_obj_gen_instance_t {
     mp_code_state_t code_state;
 } mp_obj_gen_instance_t;
 
-STATIC mp_obj_t gen_wrap_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t gen_wrap_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     // A generating function is just a bytecode function with type mp_type_gen_wrap
     mp_obj_fun_bc_t *self_fun = MP_OBJ_TO_PTR(self_in);
 
@@ -59,7 +59,7 @@ STATIC mp_obj_t gen_wrap_call(mp_obj_t self_in, size_t n_args, size_t n_kw, cons
     MP_BC_PRELUDE_SIG_DECODE(ip);
 
     // allocate the generator object, with room for local stack and exception stack
-    mp_obj_gen_instance_t *o = mp_obj_malloc_var(mp_obj_gen_instance_t, byte,
+    mp_obj_gen_instance_t *o = mp_obj_malloc_var(mp_obj_gen_instance_t, code_state.state, byte,
         n_state * sizeof(mp_obj_t) + n_exc_stack * sizeof(mp_exc_stack_t),
         &mp_type_gen_instance);
 
@@ -87,7 +87,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
 /******************************************************************************/
 // native generator wrapper
 
-#if MICROPY_EMIT_NATIVE
+#if MICROPY_ENABLE_NATIVE_CODE
 
 // Based on mp_obj_gen_instance_t.
 typedef struct _mp_obj_gen_instance_native_t {
@@ -96,40 +96,31 @@ typedef struct _mp_obj_gen_instance_native_t {
     mp_code_state_native_t code_state;
 } mp_obj_gen_instance_native_t;
 
-STATIC mp_obj_t native_gen_wrap_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t native_gen_wrap_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     // The state for a native generating function is held in the same struct as a bytecode function
     mp_obj_fun_bc_t *self_fun = MP_OBJ_TO_PTR(self_in);
 
     // Determine start of prelude.
-    uintptr_t prelude_ptr_index = ((uintptr_t *)self_fun->bytecode)[0];
-    const uint8_t *prelude_ptr;
-    if (prelude_ptr_index == 0) {
-        prelude_ptr = (void *)self_fun->child_table;
-    } else {
-        prelude_ptr = (void *)self_fun->child_table[prelude_ptr_index];
-    }
+    const uint8_t *prelude_ptr = mp_obj_fun_native_get_prelude_ptr(self_fun);
 
     // Extract n_state from the prelude.
     const uint8_t *ip = prelude_ptr;
     MP_BC_PRELUDE_SIG_DECODE(ip);
 
     // Allocate the generator object, with room for local stack (exception stack not needed).
-    mp_obj_gen_instance_native_t *o = mp_obj_malloc_var(mp_obj_gen_instance_native_t, byte, n_state * sizeof(mp_obj_t), &mp_type_gen_instance);
+    mp_obj_gen_instance_native_t *o = mp_obj_malloc_var(mp_obj_gen_instance_native_t, code_state.state, byte, n_state * sizeof(mp_obj_t), &mp_type_gen_instance);
 
     // Parse the input arguments and set up the code state
     o->pend_exc = mp_const_none;
     o->code_state.fun_bc = self_fun;
-    o->code_state.ip = prelude_ptr;
     o->code_state.n_state = n_state;
-    o->code_state.sp = &o->code_state.state[0] - 1;
     mp_setup_code_state_native(&o->code_state, n_args, n_kw, args);
 
     // Indicate we are a native function, which doesn't use this variable
     o->code_state.exc_sp_idx = MP_CODE_STATE_EXC_SP_IDX_SENTINEL;
 
     // Prepare the generator instance for execution
-    uintptr_t start_offset = ((uintptr_t *)self_fun->bytecode)[1];
-    o->code_state.ip = MICROPY_MAKE_POINTER_CALLABLE((void *)(self_fun->bytecode + start_offset));
+    o->code_state.ip = mp_obj_fun_native_get_generator_start(self_fun);
 
     return MP_OBJ_FROM_PTR(o);
 }
@@ -148,19 +139,19 @@ MP_DEFINE_CONST_OBJ_TYPE(
     NATIVE_GEN_WRAP_TYPE_ATTR
     );
 
-#endif // MICROPY_EMIT_NATIVE
+#endif // MICROPY_ENABLE_NATIVE_CODE
 
 /******************************************************************************/
 /* generator instance                                                         */
 
-STATIC void gen_instance_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void gen_instance_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_gen_instance_t *self = MP_OBJ_TO_PTR(self_in);
-    mp_printf(print, "", mp_obj_fun_get_name(MP_OBJ_FROM_PTR(self->code_state.fun_bc)), self);
+    mp_printf(print, "", mp_obj_fun_bc_get_name(self->code_state.fun_bc), self);
 }
 
 mp_vm_return_kind_t mp_obj_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_obj_t throw_value, mp_obj_t *ret_val) {
-    MP_STACK_CHECK();
+    mp_cstack_check();
     mp_check_self(mp_obj_is_type(self_in, &mp_type_gen_instance));
     mp_obj_gen_instance_t *self = MP_OBJ_TO_PTR(self_in);
     if (self->code_state.ip == 0) {
@@ -184,7 +175,7 @@ mp_vm_return_kind_t mp_obj_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_
 
     // If the generator is started, allow sending a value.
     void *state_start = self->code_state.state - 1;
-    #if MICROPY_EMIT_NATIVE
+    #if MICROPY_ENABLE_NATIVE_CODE
     if (self->code_state.exc_sp_idx == MP_CODE_STATE_EXC_SP_IDX_SENTINEL) {
         state_start = ((mp_obj_gen_instance_native_t *)self)->code_state.state - 1;
     }
@@ -206,11 +197,11 @@ mp_vm_return_kind_t mp_obj_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_
 
     mp_vm_return_kind_t ret_kind;
 
-    #if MICROPY_EMIT_NATIVE
+    #if MICROPY_ENABLE_NATIVE_CODE
     if (self->code_state.exc_sp_idx == MP_CODE_STATE_EXC_SP_IDX_SENTINEL) {
-        // A native generator, with entry point 2 words into the "bytecode" pointer
+        // A native generator.
         typedef uintptr_t (*mp_fun_native_gen_t)(void *, mp_obj_t);
-        mp_fun_native_gen_t fun = MICROPY_MAKE_POINTER_CALLABLE((const void *)(self->code_state.fun_bc->bytecode + 2 * sizeof(uintptr_t)));
+        mp_fun_native_gen_t fun = mp_obj_fun_native_get_generator_resume(self->code_state.fun_bc);
         ret_kind = fun((void *)&self->code_state, throw_value);
     } else
     #endif
@@ -244,7 +235,7 @@ mp_vm_return_kind_t mp_obj_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_
 
         case MP_VM_RETURN_EXCEPTION: {
             self->code_state.ip = 0;
-            #if MICROPY_EMIT_NATIVE
+            #if MICROPY_ENABLE_NATIVE_CODE
             if (self->code_state.exc_sp_idx == MP_CODE_STATE_EXC_SP_IDX_SENTINEL) {
                 *ret_val = ((mp_obj_gen_instance_native_t *)self)->code_state.state[0];
             } else
@@ -263,7 +254,7 @@ mp_vm_return_kind_t mp_obj_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_
     return ret_kind;
 }
 
-STATIC mp_obj_t gen_resume_and_raise(mp_obj_t self_in, mp_obj_t send_value, mp_obj_t throw_value, bool raise_stop_iteration) {
+static mp_obj_t gen_resume_and_raise(mp_obj_t self_in, mp_obj_t send_value, mp_obj_t throw_value, bool raise_stop_iteration) {
     mp_obj_t ret;
     switch (mp_obj_gen_resume(self_in, send_value, throw_value, &ret)) {
         case MP_VM_RETURN_NORMAL:
@@ -287,16 +278,16 @@ STATIC mp_obj_t gen_resume_and_raise(mp_obj_t self_in, mp_obj_t send_value, mp_o
     }
 }
 
-STATIC mp_obj_t gen_instance_iternext(mp_obj_t self_in) {
+static mp_obj_t gen_instance_iternext(mp_obj_t self_in) {
     return gen_resume_and_raise(self_in, mp_const_none, MP_OBJ_NULL, false);
 }
 
-STATIC mp_obj_t gen_instance_send(mp_obj_t self_in, mp_obj_t send_value) {
+static mp_obj_t gen_instance_send(mp_obj_t self_in, mp_obj_t send_value) {
     return gen_resume_and_raise(self_in, send_value, MP_OBJ_NULL, true);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(gen_instance_send_obj, gen_instance_send);
+static MP_DEFINE_CONST_FUN_OBJ_2(gen_instance_send_obj, gen_instance_send);
 
-STATIC mp_obj_t gen_instance_throw(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t gen_instance_throw(size_t n_args, const mp_obj_t *args) {
     // The signature of this function is: throw(type[, value[, traceback]])
     // CPython will pass all given arguments through the call chain and process them
     // at the point they are used (native generators will handle them differently to
@@ -316,9 +307,9 @@ STATIC mp_obj_t gen_instance_throw(size_t n_args, const mp_obj_t *args) {
 
     return gen_resume_and_raise(args[0], mp_const_none, exc, true);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(gen_instance_throw_obj, 2, 4, gen_instance_throw);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(gen_instance_throw_obj, 2, 4, gen_instance_throw);
 
-STATIC mp_obj_t gen_instance_close(mp_obj_t self_in) {
+static mp_obj_t gen_instance_close(mp_obj_t self_in) {
     mp_obj_t ret;
     switch (mp_obj_gen_resume(self_in, mp_const_none, MP_OBJ_FROM_PTR(&mp_const_GeneratorExit_obj), &ret)) {
         case MP_VM_RETURN_YIELD:
@@ -337,10 +328,10 @@ STATIC mp_obj_t gen_instance_close(mp_obj_t self_in) {
             return mp_const_none;
     }
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(gen_instance_close_obj, gen_instance_close);
+static MP_DEFINE_CONST_FUN_OBJ_1(gen_instance_close_obj, gen_instance_close);
 
 #if MICROPY_PY_GENERATOR_PEND_THROW
-STATIC mp_obj_t gen_instance_pend_throw(mp_obj_t self_in, mp_obj_t exc_in) {
+static mp_obj_t gen_instance_pend_throw(mp_obj_t self_in, mp_obj_t exc_in) {
     mp_obj_gen_instance_t *self = MP_OBJ_TO_PTR(self_in);
     if (self->pend_exc == MP_OBJ_NULL) {
         mp_raise_ValueError(MP_ERROR_TEXT("generator already executing"));
@@ -349,10 +340,10 @@ STATIC mp_obj_t gen_instance_pend_throw(mp_obj_t self_in, mp_obj_t exc_in) {
     self->pend_exc = exc_in;
     return prev;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(gen_instance_pend_throw_obj, gen_instance_pend_throw);
+static MP_DEFINE_CONST_FUN_OBJ_2(gen_instance_pend_throw_obj, gen_instance_pend_throw);
 #endif
 
-STATIC const mp_rom_map_elem_t gen_instance_locals_dict_table[] = {
+static const mp_rom_map_elem_t gen_instance_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&gen_instance_close_obj) },
     { MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&gen_instance_send_obj) },
     { MP_ROM_QSTR(MP_QSTR_throw), MP_ROM_PTR(&gen_instance_throw_obj) },
@@ -361,7 +352,7 @@ STATIC const mp_rom_map_elem_t gen_instance_locals_dict_table[] = {
     #endif
 };
 
-STATIC MP_DEFINE_CONST_DICT(gen_instance_locals_dict, gen_instance_locals_dict_table);
+static MP_DEFINE_CONST_DICT(gen_instance_locals_dict, gen_instance_locals_dict_table);
 
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_gen_instance,
diff --git a/py/objgetitemiter.c b/py/objgetitemiter.c
index c598d1daaca..c735c65b652 100644
--- a/py/objgetitemiter.c
+++ b/py/objgetitemiter.c
@@ -35,7 +35,7 @@ typedef struct _mp_obj_getitem_iter_t {
     mp_obj_t args[3];
 } mp_obj_getitem_iter_t;
 
-STATIC mp_obj_t it_iternext(mp_obj_t self_in) {
+static mp_obj_t it_iternext(mp_obj_t self_in) {
     mp_obj_getitem_iter_t *self = MP_OBJ_TO_PTR(self_in);
     nlr_buf_t nlr;
     if (nlr_push(&nlr) == 0) {
@@ -56,7 +56,7 @@ STATIC mp_obj_t it_iternext(mp_obj_t self_in) {
     }
 }
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+static MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_it,
     MP_QSTR_iterator,
     MP_TYPE_FLAG_ITER_IS_ITERNEXT,
diff --git a/py/objint.c b/py/objint.c
index be5f4653a7d..ea5a0477aff 100644
--- a/py/objint.c
+++ b/py/objint.c
@@ -30,7 +30,7 @@
 
 #include "py/parsenum.h"
 #include "py/smallint.h"
-#include "py/objint.h"
+#include "py/objint_impl.h"
 #include "py/objstr.h"
 #include "py/runtime.h"
 #include "py/binary.h"
@@ -40,7 +40,7 @@
 #endif
 
 // This dispatcher function is expected to be independent of the implementation of long int
-STATIC mp_obj_t mp_obj_int_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t mp_obj_int_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)type_in;
     mp_arg_check_num(n_args, n_kw, 0, 2, false);
 
@@ -55,7 +55,7 @@ STATIC mp_obj_t mp_obj_int_make_new(const mp_obj_type_t *type_in, size_t n_args,
                 return o;
             } else if (mp_get_buffer(args[0], &bufinfo, MP_BUFFER_READ)) {
                 // a textual representation, parse it
-                return mp_parse_num_integer(bufinfo.buf, bufinfo.len, 0, NULL);
+                return mp_parse_num_integer(bufinfo.buf, bufinfo.len, 10, NULL);
             #if MICROPY_PY_BUILTINS_FLOAT
             } else if (mp_obj_is_float(args[0])) {
                 return mp_obj_new_int_from_float(mp_obj_float_get(args[0]));
@@ -83,7 +83,7 @@ typedef enum {
     MP_FP_CLASS_OVERFLOW
 } mp_fp_as_int_class_t;
 
-STATIC mp_fp_as_int_class_t mp_classify_fp_as_int(mp_float_t val) {
+static mp_fp_as_int_class_t mp_classify_fp_as_int(mp_float_t val) {
     union {
         mp_float_t f;
         #if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT
@@ -99,8 +99,8 @@ STATIC mp_fp_as_int_class_t mp_classify_fp_as_int(mp_float_t val) {
     #elif MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_DOUBLE
     e = u.i[MP_ENDIANNESS_LITTLE];
     #endif
-#define MP_FLOAT_SIGN_SHIFT_I32 ((MP_FLOAT_FRAC_BITS + MP_FLOAT_EXP_BITS) % 32)
-#define MP_FLOAT_EXP_SHIFT_I32 (MP_FLOAT_FRAC_BITS % 32)
+    #define MP_FLOAT_SIGN_SHIFT_I32 ((MP_FLOAT_FRAC_BITS + MP_FLOAT_EXP_BITS) % 32)
+    #define MP_FLOAT_EXP_SHIFT_I32 (MP_FLOAT_FRAC_BITS % 32)
 
     if (e & (1U << MP_FLOAT_SIGN_SHIFT_I32)) {
         #if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_DOUBLE
@@ -117,9 +117,15 @@ STATIC mp_fp_as_int_class_t mp_classify_fp_as_int(mp_float_t val) {
     }
     // 8 * sizeof(uintptr_t) counts the number of bits for a small int
     // TODO provide a way to configure this properly
+    #if MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_B
+    if (e <= ((8 * sizeof(uintptr_t) + MP_FLOAT_EXP_BIAS - 4) << MP_FLOAT_EXP_SHIFT_I32)) {
+        return MP_FP_CLASS_FIT_SMALLINT;
+    }
+    #else
     if (e <= ((8 * sizeof(uintptr_t) + MP_FLOAT_EXP_BIAS - 3) << MP_FLOAT_EXP_SHIFT_I32)) {
         return MP_FP_CLASS_FIT_SMALLINT;
     }
+    #endif
     #if MICROPY_LONGINT_IMPL == MICROPY_LONGINT_IMPL_LONGLONG
     if (e <= (((sizeof(long long) * MP_BITS_PER_BYTE) + MP_FLOAT_EXP_BIAS - 2) << MP_FLOAT_EXP_SHIFT_I32)) {
         return MP_FP_CLASS_FIT_LONGINT;
@@ -193,23 +199,23 @@ void mp_obj_int_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t
     }
 }
 
-STATIC const uint8_t log_base2_floor[] = {
+static const uint8_t log_base2_floor[] = {
     0, 1, 1, 2,
     2, 2, 2, 3,
     3, 3, 3, 3,
     3, 3, 3, 4,
     /* if needed, these are the values for higher bases
-    4, 4, 4, 4,
-    4, 4, 4, 4,
-    4, 4, 4, 4,
-    4, 4, 4, 5
+       4, 4, 4, 4,
+       4, 4, 4, 4,
+       4, 4, 4, 4,
+       4, 4, 4, 5
     */
 };
 
 size_t mp_int_format_size(size_t num_bits, int base, const char *prefix, char comma) {
     assert(2 <= base && base <= 16);
     size_t num_digits = num_bits / log_base2_floor[base - 1] + 1;
-    size_t num_commas = comma ? num_digits / 3 : 0;
+    size_t num_commas = comma ? (base == 10 ? num_digits / 3 : num_digits / 4): 0;
     size_t prefix_len = prefix ? strlen(prefix) : 0;
     return num_digits + num_commas + prefix_len + 2; // +1 for sign, +1 for null byte
 }
@@ -247,10 +253,11 @@ char *mp_obj_int_formatted(char **buf, size_t *buf_size, size_t *fmt_size, mp_co
 
     char sign = '\0';
     if (num < 0) {
-        num = -num;
+        num = -(fmt_uint_t)num;
         sign = '-';
     }
 
+    int n_comma = (base == 10) ? 3 : 4;
     size_t needed_size = mp_int_format_size(sizeof(fmt_int_t) * 8, base, prefix, comma);
     if (needed_size > *buf_size) {
         *buf = m_new(char, needed_size);
@@ -275,7 +282,7 @@ char *mp_obj_int_formatted(char **buf, size_t *buf_size, size_t *fmt_size, mp_co
                 c += '0';
             }
             *(--b) = c;
-            if (comma && num != 0 && b > str && (last_comma - b) == 3) {
+            if (comma && num != 0 && b > str && (last_comma - b) == n_comma) {
                 *(--b) = comma;
                 last_comma = b;
             }
@@ -367,6 +374,10 @@ mp_int_t mp_obj_int_get_checked(mp_const_obj_t self_in) {
     return MP_OBJ_SMALL_INT_VALUE(self_in);
 }
 
+void mp_obj_int_to_bytes(mp_obj_t self_in, size_t buf_len, byte *buf, bool big_endian, bool is_signed, bool overflow_check) {
+    mp_obj_small_int_to_bytes(MP_OBJ_SMALL_INT_VALUE(self_in), buf_len, buf, big_endian, is_signed, overflow_check);
+}
+
 #endif // MICROPY_LONGINT_IMPL == MICROPY_LONGINT_IMPL_NONE
 
 // This dispatcher function is expected to be independent of the implementation of long int
@@ -376,7 +387,7 @@ mp_obj_t mp_obj_int_binary_op_extra_cases(mp_binary_op_t op, mp_obj_t lhs_in, mp
         // false acts as 0
         return mp_binary_op(op, lhs_in, MP_OBJ_NEW_SMALL_INT(0));
     } else if (rhs_in == mp_const_true) {
-        // true acts as 0
+        // true acts as 1
         return mp_binary_op(op, lhs_in, MP_OBJ_NEW_SMALL_INT(1));
     } else if (op == MP_BINARY_OP_MULTIPLY) {
         if (mp_obj_is_str_or_bytes(rhs_in) || mp_obj_is_type(rhs_in, &mp_type_tuple) || mp_obj_is_type(rhs_in, &mp_type_list)) {
@@ -388,9 +399,8 @@ mp_obj_t mp_obj_int_binary_op_extra_cases(mp_binary_op_t op, mp_obj_t lhs_in, mp
 }
 
 // this is a classmethod
-STATIC mp_obj_t int_from_bytes(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t int_from_bytes(size_t n_args, const mp_obj_t *args) {
     // TODO: Support signed param (assumes signed=False at the moment)
-    (void)n_args;
 
     // get the buffer info
     mp_buffer_info_t bufinfo;
@@ -398,7 +408,8 @@ STATIC mp_obj_t int_from_bytes(size_t n_args, const mp_obj_t *args) {
 
     const byte *buf = (const byte *)bufinfo.buf;
     int delta = 1;
-    if (args[2] == MP_OBJ_NEW_QSTR(MP_QSTR_little)) {
+    bool big_endian = n_args < 3 || args[2] != MP_OBJ_NEW_QSTR(MP_QSTR_little);
+    if (!big_endian) {
         buf += bufinfo.len - 1;
         delta = -1;
     }
@@ -409,7 +420,7 @@ STATIC mp_obj_t int_from_bytes(size_t n_args, const mp_obj_t *args) {
         #if MICROPY_LONGINT_IMPL != MICROPY_LONGINT_IMPL_NONE
         if (value > (MP_SMALL_INT_MAX >> 8)) {
             // Result will overflow a small-int so construct a big-int
-            return mp_obj_int_from_bytes_impl(args[2] != MP_OBJ_NEW_QSTR(MP_QSTR_little), bufinfo.len, bufinfo.buf);
+            return mp_obj_int_from_bytes_impl(big_endian, bufinfo.len, bufinfo.buf);
         }
         #endif
         value = (value << 8) | *buf;
@@ -417,45 +428,45 @@ STATIC mp_obj_t int_from_bytes(size_t n_args, const mp_obj_t *args) {
     return mp_obj_new_int_from_uint(value);
 }
 
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(int_from_bytes_fun_obj, 3, 4, int_from_bytes);
-STATIC MP_DEFINE_CONST_CLASSMETHOD_OBJ(int_from_bytes_obj, MP_ROM_PTR(&int_from_bytes_fun_obj));
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(int_from_bytes_fun_obj, 2, 4, int_from_bytes);
+static MP_DEFINE_CONST_CLASSMETHOD_OBJ(int_from_bytes_obj, MP_ROM_PTR(&int_from_bytes_fun_obj));
+
+static mp_obj_t int_to_bytes(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
+    enum { ARG_length, ARG_byteorder, ARG_signed };
+    static const mp_arg_t allowed_args[] = {
+        { MP_QSTR_length,    MP_ARG_INT, { .u_int = 1 } },
+        { MP_QSTR_byteorder, MP_ARG_OBJ, { .u_rom_obj = MP_ROM_QSTR(MP_QSTR_big) } },
+        { MP_QSTR_signed,    MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} },
+    };
+    mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
+    mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
 
-STATIC mp_obj_t int_to_bytes(size_t n_args, const mp_obj_t *args) {
-    // TODO: Support signed param (assumes signed=False)
-    (void)n_args;
+    mp_obj_t self = pos_args[0];
 
-    mp_int_t len = mp_obj_get_int(args[1]);
-    if (len < 0) {
+    mp_int_t dlen = args[ARG_length].u_int;
+    if (dlen < 0) {
         mp_raise_ValueError(NULL);
     }
-    bool big_endian = args[2] != MP_OBJ_NEW_QSTR(MP_QSTR_little);
 
     vstr_t vstr;
-    vstr_init_len(&vstr, len);
+    vstr_init_len(&vstr, dlen);
     byte *data = (byte *)vstr.buf;
-    memset(data, 0, len);
 
-    #if MICROPY_LONGINT_IMPL != MICROPY_LONGINT_IMPL_NONE
-    if (!mp_obj_is_small_int(args[0])) {
-        mp_obj_int_to_bytes_impl(args[0], big_endian, len, data);
-    } else
-    #endif
-    {
-        mp_int_t val = MP_OBJ_SMALL_INT_VALUE(args[0]);
-        size_t l = MIN((size_t)len, sizeof(val));
-        mp_binary_set_int(l, big_endian, data + (big_endian ? (len - l) : 0), val);
-    }
+    bool big_endian = args[ARG_byteorder].u_obj != MP_OBJ_NEW_QSTR(MP_QSTR_little);
+    bool signed_ = args[ARG_signed].u_bool;
+
+    mp_obj_int_to_bytes(self, dlen, data, big_endian, signed_, true);
 
     return mp_obj_new_bytes_from_vstr(&vstr);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(int_to_bytes_obj, 3, 4, int_to_bytes);
+static MP_DEFINE_CONST_FUN_OBJ_KW(int_to_bytes_obj, 1, int_to_bytes);
 
-STATIC const mp_rom_map_elem_t int_locals_dict_table[] = {
+static const mp_rom_map_elem_t int_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_from_bytes), MP_ROM_PTR(&int_from_bytes_obj) },
     { MP_ROM_QSTR(MP_QSTR_to_bytes), MP_ROM_PTR(&int_to_bytes_obj) },
 };
 
-STATIC MP_DEFINE_CONST_DICT(int_locals_dict, int_locals_dict_table);
+static MP_DEFINE_CONST_DICT(int_locals_dict, int_locals_dict_table);
 
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_int,
diff --git a/py/objint.h b/py/objint.h
index 5eed87705de..f6a551ea961 100644
--- a/py/objint.h
+++ b/py/objint.h
@@ -53,9 +53,12 @@ char *mp_obj_int_formatted(char **buf, size_t *buf_size, size_t *fmt_size, mp_co
     int base, const char *prefix, char base_char, char comma);
 char *mp_obj_int_formatted_impl(char **buf, size_t *buf_size, size_t *fmt_size, mp_const_obj_t self_in,
     int base, const char *prefix, char base_char, char comma);
+
 mp_int_t mp_obj_int_hash(mp_obj_t self_in);
 mp_obj_t mp_obj_int_from_bytes_impl(bool big_endian, size_t len, const byte *buf);
-void mp_obj_int_to_bytes_impl(mp_obj_t self_in, bool big_endian, size_t len, byte *buf);
+// Write an integer to a byte sequence.
+// If overflow_check is true, raises OverflowError if 'self_in' doesn't fit. If false, truncate to fit.
+void mp_obj_int_to_bytes(mp_obj_t self_in, size_t buf_len, byte *buf, bool big_endian, bool is_signed, bool overflow_check);
 int mp_obj_int_sign(mp_obj_t self_in);
 mp_obj_t mp_obj_int_unary_op(mp_unary_op_t op, mp_obj_t o_in);
 mp_obj_t mp_obj_int_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in);
diff --git a/py/objint_impl.h b/py/objint_impl.h
new file mode 100644
index 00000000000..b1af4d6244d
--- /dev/null
+++ b/py/objint_impl.h
@@ -0,0 +1,98 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2026 Angus Gratton
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* This header provides some inline implementations of functions used by multiple objint*.c
+   files.
+
+   Implementations here should only be called by one of the objint*.c files. If
+   called from more than one place in a single firmware, place into objint.c and
+   define in objint.h
+ */
+#ifndef MICROPY_INCLUDED_PY_OBJINT_IMPL_H
+#define MICROPY_INCLUDED_PY_OBJINT_IMPL_H
+
+#include "py/binary.h"
+#include "py/objint.h"
+#include "py/runtime.h"
+
+static void mp_obj_int_raise_to_bytes_overflow_error(size_t nbytes) {
+    mp_raise_msg_varg(&mp_type_OverflowError, MP_ERROR_TEXT("value would overflow a %d byte buffer"), nbytes);
+}
+
+static void mp_obj_int_raise_unsigned_negative_overflow_error(void) {
+    mp_raise_msg_varg(&mp_type_OverflowError, MP_ERROR_TEXT("can't convert negative int to unsigned"));
+}
+
+static void mp_obj_small_int_buffer_overflow_check(mp_int_t val, size_t nbytes, bool is_signed) {
+    // Fast path for zero.
+    if (val == 0) {
+        return;
+    }
+
+    if (!is_signed && val < 0) {
+        // Trying to store negative values in unsigned bytes
+        mp_obj_int_raise_unsigned_negative_overflow_error();
+    }
+
+    if (nbytes >= sizeof(val)) {
+        // All N bit small integers fit in an unsigned N bit integer.
+        // This case prevents shifting too far below.
+        return;
+    }
+
+    if (nbytes == 0) {
+        // Can't fit a non-negative value in 0 bytes (prevents negative left shift, below)
+        goto raise;
+    }
+
+    if (is_signed) {
+        mp_int_t edge = ((mp_int_t)1 << (nbytes * 8 - 1));
+        if (-edge <= val && val < edge) {
+            return;
+        }
+        // Out of range, fall through to raise.
+    } else {
+        // Unsigned. We already know val >= 0.
+        mp_int_t edge = ((mp_int_t)1 << (nbytes * 8));
+        if (val < edge) {
+            return;
+        }
+        // Fall through to raise.
+    }
+
+raise:
+    mp_obj_int_raise_to_bytes_overflow_error(nbytes);
+}
+
+static inline void mp_obj_small_int_to_bytes(mp_int_t val, size_t buf_len, byte *buf, bool big_endian, bool is_signed, bool overflow_check) {
+    if (overflow_check) {
+        mp_obj_small_int_buffer_overflow_check(val, buf_len, is_signed);
+    }
+    mp_binary_set_int(buf_len, buf, sizeof(val), val, big_endian);
+
+}
+
+#endif // MICROPY_INCLUDED_PY_OBJINT_IMPL_H
diff --git a/py/objint_longlong.c b/py/objint_longlong.c
index ee499e0265b..a00e634b317 100644
--- a/py/objint_longlong.c
+++ b/py/objint_longlong.c
@@ -29,8 +29,9 @@
 #include 
 
 #include "py/smallint.h"
-#include "py/objint.h"
+#include "py/objint_impl.h"
 #include "py/runtime.h"
+#include "py/misc.h"
 
 #if MICROPY_PY_BUILTINS_FLOAT
 #include 
@@ -43,6 +44,10 @@
 const mp_obj_int_t mp_sys_maxsize_obj = {{&mp_type_int}, MP_SSIZE_MAX};
 #endif
 
+static void raise_long_long_overflow(void) {
+    mp_raise_msg(&mp_type_OverflowError, MP_ERROR_TEXT("result overflows long long storage"));
+}
+
 mp_obj_t mp_obj_int_from_bytes_impl(bool big_endian, size_t len, const byte *buf) {
     int delta = 1;
     if (!big_endian) {
@@ -57,24 +62,6 @@ mp_obj_t mp_obj_int_from_bytes_impl(bool big_endian, size_t len, const byte *buf
     return mp_obj_new_int_from_ll(value);
 }
 
-void mp_obj_int_to_bytes_impl(mp_obj_t self_in, bool big_endian, size_t len, byte *buf) {
-    assert(mp_obj_is_exact_type(self_in, &mp_type_int));
-    mp_obj_int_t *self = self_in;
-    long long val = self->val;
-    if (big_endian) {
-        byte *b = buf + len;
-        while (b > buf) {
-            *--b = val;
-            val >>= 8;
-        }
-    } else {
-        for (; len > 0; --len) {
-            *buf++ = val;
-            val >>= 8;
-        }
-    }
-}
-
 int mp_obj_int_sign(mp_obj_t self_in) {
     mp_longint_impl_t val;
     if (mp_obj_is_small_int(self_in)) {
@@ -102,7 +89,6 @@ mp_obj_t mp_obj_int_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
         // small int if the value fits without truncation
         case MP_UNARY_OP_HASH:
             return MP_OBJ_NEW_SMALL_INT((mp_int_t)o->val);
-
         case MP_UNARY_OP_POSITIVE:
             return o_in;
         case MP_UNARY_OP_NEGATIVE:
@@ -129,6 +115,8 @@ mp_obj_t mp_obj_int_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
 mp_obj_t mp_obj_int_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
     long long lhs_val;
     long long rhs_val;
+    bool overflow = false;
+    long long result;
 
     if (mp_obj_is_small_int(lhs_in)) {
         lhs_val = MP_OBJ_SMALL_INT_VALUE(lhs_in);
@@ -141,21 +129,41 @@ mp_obj_t mp_obj_int_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_i
         rhs_val = MP_OBJ_SMALL_INT_VALUE(rhs_in);
     } else if (mp_obj_is_exact_type(rhs_in, &mp_type_int)) {
         rhs_val = ((mp_obj_int_t *)rhs_in)->val;
+    #if MICROPY_PY_BUILTINS_FLOAT
+    } else if (mp_obj_is_float(rhs_in)) {
+        return mp_obj_float_binary_op(op, (mp_float_t)lhs_val, rhs_in);
+    #endif
+    #if MICROPY_PY_BUILTINS_COMPLEX
+    } else if (mp_obj_is_type(rhs_in, &mp_type_complex)) {
+        return mp_obj_complex_binary_op(op, (mp_float_t)lhs_val, 0, rhs_in);
+    #endif
     } else {
         // delegate to generic function to check for extra cases
         return mp_obj_int_binary_op_extra_cases(op, lhs_in, rhs_in);
     }
 
+    #if MICROPY_PY_BUILTINS_FLOAT
+    if (op == MP_BINARY_OP_TRUE_DIVIDE || op == MP_BINARY_OP_INPLACE_TRUE_DIVIDE) {
+        if (rhs_val == 0) {
+            goto zero_division;
+        }
+        return mp_obj_new_float((mp_float_t)lhs_val / (mp_float_t)rhs_val);
+    }
+    #endif
+
     switch (op) {
         case MP_BINARY_OP_ADD:
         case MP_BINARY_OP_INPLACE_ADD:
-            return mp_obj_new_int_from_ll(lhs_val + rhs_val);
+            overflow = mp_add_ll_overflow(lhs_val, rhs_val, &result);
+            break;
         case MP_BINARY_OP_SUBTRACT:
         case MP_BINARY_OP_INPLACE_SUBTRACT:
-            return mp_obj_new_int_from_ll(lhs_val - rhs_val);
+            overflow = mp_sub_ll_overflow(lhs_val, rhs_val, &result);
+            break;
         case MP_BINARY_OP_MULTIPLY:
         case MP_BINARY_OP_INPLACE_MULTIPLY:
-            return mp_obj_new_int_from_ll(lhs_val * rhs_val);
+            overflow = mp_mul_ll_overflow(lhs_val, rhs_val, &result);
+            break;
         case MP_BINARY_OP_FLOOR_DIVIDE:
         case MP_BINARY_OP_INPLACE_FLOOR_DIVIDE:
             if (rhs_val == 0) {
@@ -181,9 +189,21 @@ mp_obj_t mp_obj_int_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_i
 
         case MP_BINARY_OP_LSHIFT:
         case MP_BINARY_OP_INPLACE_LSHIFT:
-            return mp_obj_new_int_from_ll(lhs_val << (int)rhs_val);
+            if (rhs_val < 0) {
+                // negative shift not allowed
+                mp_raise_ValueError(MP_ERROR_TEXT("negative shift count"));
+            }
+            overflow = rhs_val >= (long long)(sizeof(long long) * MP_BITS_PER_BYTE)
+                || lhs_val > (LLONG_MAX >> rhs_val)
+                || lhs_val < (LLONG_MIN >> rhs_val);
+            result = (unsigned long long)lhs_val << rhs_val;
+            break;
         case MP_BINARY_OP_RSHIFT:
         case MP_BINARY_OP_INPLACE_RSHIFT:
+            if ((int)rhs_val < 0) {
+                // negative shift not allowed
+                mp_raise_ValueError(MP_ERROR_TEXT("negative shift count"));
+            }
             return mp_obj_new_int_from_ll(lhs_val >> (int)rhs_val);
 
         case MP_BINARY_OP_POWER:
@@ -195,18 +215,18 @@ mp_obj_t mp_obj_int_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_i
                 mp_raise_ValueError(MP_ERROR_TEXT("negative power with no float support"));
                 #endif
             }
-            long long ans = 1;
-            while (rhs_val > 0) {
+            result = 1;
+            while (rhs_val > 0 && !overflow) {
                 if (rhs_val & 1) {
-                    ans *= lhs_val;
+                    overflow = mp_mul_ll_overflow(result, lhs_val, &result);
                 }
-                if (rhs_val == 1) {
+                if (rhs_val == 1 || overflow) {
                     break;
                 }
                 rhs_val /= 2;
-                lhs_val *= lhs_val;
+                overflow = mp_mul_ll_overflow(lhs_val, lhs_val, &lhs_val);
             }
-            return mp_obj_new_int_from_ll(ans);
+            break;
         }
 
         case MP_BINARY_OP_LESS:
@@ -224,50 +244,39 @@ mp_obj_t mp_obj_int_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_i
             return MP_OBJ_NULL; // op not supported
     }
 
+    if (overflow) {
+        raise_long_long_overflow();
+    }
+
+    return mp_obj_new_int_from_ll(result);
+
 zero_division:
     mp_raise_msg(&mp_type_ZeroDivisionError, MP_ERROR_TEXT("divide by zero"));
 }
 
 mp_obj_t mp_obj_new_int(mp_int_t value) {
-    if (MP_SMALL_INT_FITS(value)) {
-        return MP_OBJ_NEW_SMALL_INT(value);
-    }
     return mp_obj_new_int_from_ll(value);
 }
 
 mp_obj_t mp_obj_new_int_from_uint(mp_uint_t value) {
-    // SMALL_INT accepts only signed numbers, so make sure the input
-    // value fits completely in the small-int positive range.
-    if ((value & ~MP_SMALL_INT_POSITIVE_MASK) == 0) {
-        return MP_OBJ_NEW_SMALL_INT(value);
-    }
     return mp_obj_new_int_from_ll(value);
 }
 
 mp_obj_t mp_obj_new_int_from_ll(long long val) {
+    if ((long long)(mp_int_t)val == val && MP_SMALL_INT_FITS(val)) {
+        return MP_OBJ_NEW_SMALL_INT(val);
+    }
+
     mp_obj_int_t *o = mp_obj_malloc(mp_obj_int_t, &mp_type_int);
     o->val = val;
-    return o;
+    return MP_OBJ_FROM_PTR(o);
 }
 
 mp_obj_t mp_obj_new_int_from_ull(unsigned long long val) {
-    // TODO raise an exception if the unsigned long long won't fit
     if (val >> (sizeof(unsigned long long) * 8 - 1) != 0) {
-        mp_raise_msg(&mp_type_OverflowError, MP_ERROR_TEXT("ulonglong too large"));
+        raise_long_long_overflow();
     }
-    mp_obj_int_t *o = mp_obj_malloc(mp_obj_int_t, &mp_type_int);
-    o->val = val;
-    return o;
-}
-
-mp_obj_t mp_obj_new_int_from_str_len(const char **str, size_t len, bool neg, unsigned int base) {
-    // TODO this does not honor the given length of the string, but it all cases it should anyway be null terminated
-    // TODO check overflow
-    mp_obj_int_t *o = mp_obj_malloc(mp_obj_int_t, &mp_type_int);
-    char *endptr;
-    o->val = strtoll(*str, &endptr, base);
-    *str = endptr;
-    return o;
+    return mp_obj_new_int_from_ll(val);
 }
 
 mp_int_t mp_obj_int_get_truncated(mp_const_obj_t self_in) {
@@ -280,8 +289,33 @@ mp_int_t mp_obj_int_get_truncated(mp_const_obj_t self_in) {
 }
 
 mp_int_t mp_obj_int_get_checked(mp_const_obj_t self_in) {
-    // TODO: Check overflow
-    return mp_obj_int_get_truncated(self_in);
+    if (mp_obj_is_small_int(self_in)) {
+        return MP_OBJ_SMALL_INT_VALUE(self_in);
+    } else {
+        const mp_obj_int_t *self = self_in;
+        long long value = self->val;
+        mp_int_t truncated = (mp_int_t)value;
+        if ((long long)truncated == value) {
+            return truncated;
+        }
+    }
+    mp_raise_msg(&mp_type_OverflowError, MP_ERROR_TEXT("overflow converting long int to machine word"));
+}
+
+mp_uint_t mp_obj_int_get_uint_checked(mp_const_obj_t self_in) {
+    if (mp_obj_is_small_int(self_in)) {
+        if (MP_OBJ_SMALL_INT_VALUE(self_in) >= 0) {
+            return MP_OBJ_SMALL_INT_VALUE(self_in);
+        }
+    } else {
+        const mp_obj_int_t *self = self_in;
+        long long value = self->val;
+        mp_uint_t truncated = (mp_uint_t)value;
+        if (value >= 0 && (long long)truncated == value) {
+            return truncated;
+        }
+    }
+    mp_raise_msg(&mp_type_OverflowError, MP_ERROR_TEXT("overflow converting long int to machine word"));
 }
 
 #if MICROPY_PY_BUILTINS_FLOAT
@@ -292,4 +326,88 @@ mp_float_t mp_obj_int_as_float_impl(mp_obj_t self_in) {
 }
 #endif
 
+// Same as the general mp_small_int_buffer_overflow_check() in objint_impl.h, but using 64-bit integers
+static void longint_buffer_overflow_check(mp_longint_impl_t val, size_t nbytes, bool is_signed) {
+    // Fast path for zero.
+    if (val == 0) {
+        return;
+    }
+
+    if (!is_signed && val < 0) {
+        // Trying to store negative values in unsigned bytes
+        mp_obj_int_raise_unsigned_negative_overflow_error();
+    }
+
+    if (nbytes >= sizeof(val)) {
+        // All non-negative N bit signed integers fit in an unsigned N bit integer.
+        // This case prevents shifting too far below.
+        return;
+    }
+
+    if (nbytes == 0) {
+        // Can't fit a non-zero value in 0 bytes (prevents negative left shift below)
+        goto raise;
+    }
+
+    if (is_signed) {
+        mp_longint_impl_t edge = 1LL << (nbytes * 8 - 1);
+        if (-edge <= val && val < edge) {
+            return;
+        }
+        // Out of range, fall through to failure.
+    } else {
+        // Unsigned. We already know val >= 0.
+        mp_longint_impl_t edge = 1LL << (nbytes * 8);
+        if (val < edge) {
+            return;
+        }
+        // Fall through to failure.
+    }
+
+raise:
+    mp_obj_int_raise_to_bytes_overflow_error(nbytes);
+}
+
+static void longint_to_bytes(long long val, bool big_endian, size_t len, byte *buf) {
+    MP_STATIC_ASSERT(sizeof(mp_uint_t) == 4);
+    mp_uint_t lower = val;
+    mp_uint_t upper = (val >> 32);
+
+    if (big_endian) {
+        if (len > 4) {
+            // write the least significant 4 bytes at the end
+            mp_binary_set_int(4, buf + len - 4, sizeof(lower), lower, true);
+            // write most significant bytes at the start, extending if necessary
+            mp_binary_set_int(len - 4, buf, sizeof(upper), upper, true);
+        } else {
+            mp_binary_set_int(len, buf, sizeof(lower), lower, true);
+        }
+    } else {
+        // write the least significant 4 bytes at the start
+        mp_binary_set_int(len > 4 ? len - 4 : len, buf, sizeof(lower), lower, false);
+        if (len > 4) {
+            // write the most significant bytes at the end, extending if necessary
+            mp_binary_set_int(len - 4, buf + 4, sizeof(upper), upper, false);
+        }
+    }
+}
+
+void mp_obj_int_to_bytes(mp_obj_t self_in, size_t buf_len, byte *buf, bool big_endian, bool is_signed, bool overflow_check) {
+    mp_longint_impl_t val;
+    if (mp_obj_is_exact_type(self_in, &mp_type_int)) {
+        const mp_obj_int_t *self = MP_OBJ_TO_PTR(self_in);
+        val = self->val;
+    } else {
+        // self_in is either a smallint, or another type convertible to mp_int_t (i.e. bool)
+        val = mp_obj_get_int(self_in);
+    }
+
+    // Note: to save code size we don't call mp_obj_small_int_to_bytes() here,
+    // as the longint implementation is very similar
+    if (overflow_check) {
+        longint_buffer_overflow_check(val, buf_len, is_signed);
+    }
+    longint_to_bytes(val, big_endian, buf_len, buf);
+}
+
 #endif
diff --git a/py/objint_mpz.c b/py/objint_mpz.c
index 8078441d66a..045afd5fd00 100644
--- a/py/objint_mpz.c
+++ b/py/objint_mpz.c
@@ -30,7 +30,7 @@
 
 #include "py/parsenumbase.h"
 #include "py/smallint.h"
-#include "py/objint.h"
+#include "py/objint_impl.h"
 #include "py/runtime.h"
 
 #if MICROPY_PY_BUILTINS_FLOAT
@@ -43,7 +43,7 @@
 // Export value for sys.maxsize
 // *FORMAT-OFF*
 #define DIG_MASK ((MPZ_LONG_1 << MPZ_DIG_SIZE) - 1)
-STATIC const mpz_dig_t maxsize_dig[] = {
+static const mpz_dig_t maxsize_dig[] = {
     #define NUM_DIG 1
     (MP_SSIZE_MAX >> MPZ_DIG_SIZE * 0) & DIG_MASK,
     #if (MP_SSIZE_MAX >> MPZ_DIG_SIZE * 0) > DIG_MASK
@@ -112,12 +112,6 @@ mp_obj_t mp_obj_int_from_bytes_impl(bool big_endian, size_t len, const byte *buf
     return MP_OBJ_FROM_PTR(o);
 }
 
-void mp_obj_int_to_bytes_impl(mp_obj_t self_in, bool big_endian, size_t len, byte *buf) {
-    assert(mp_obj_is_exact_type(self_in, &mp_type_int));
-    mp_obj_int_t *self = MP_OBJ_TO_PTR(self_in);
-    mpz_as_bytes(&self->mpz, big_endian, len, buf);
-}
-
 int mp_obj_int_sign(mp_obj_t self_in) {
     if (mp_obj_is_small_int(self_in)) {
         mp_int_t val = MP_OBJ_SMALL_INT_VALUE(self_in);
@@ -312,6 +306,14 @@ mp_obj_t mp_obj_int_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_i
                 return MP_OBJ_NULL; // op not supported
         }
 
+        // Check if the result fits in a small-int, and if so just return that.
+        mp_int_t res_small;
+        if (mpz_as_int_checked(&res->mpz, &res_small)) {
+            if (MP_SMALL_INT_FITS(res_small)) {
+                return MP_OBJ_NEW_SMALL_INT(res_small);
+            }
+        }
+
         return MP_OBJ_FROM_PTR(res);
 
     } else {
@@ -335,7 +337,7 @@ mp_obj_t mp_obj_int_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_i
 }
 
 #if MICROPY_PY_BUILTINS_POW3
-STATIC mpz_t *mp_mpz_for_int(mp_obj_t arg, mpz_t *temp) {
+static mpz_t *mp_mpz_for_int(mp_obj_t arg, mpz_t *temp) {
     if (mp_obj_is_small_int(arg)) {
         mpz_init_from_int(temp, MP_OBJ_SMALL_INT_VALUE(arg));
         return temp;
@@ -348,9 +350,10 @@ STATIC mpz_t *mp_mpz_for_int(mp_obj_t arg, mpz_t *temp) {
 mp_obj_t mp_obj_int_pow3(mp_obj_t base, mp_obj_t exponent,  mp_obj_t modulus) {
     if (!mp_obj_is_int(base) || !mp_obj_is_int(exponent) || !mp_obj_is_int(modulus)) {
         mp_raise_TypeError(MP_ERROR_TEXT("pow() with 3 arguments requires integers"));
+    } else if (modulus == MP_OBJ_NEW_SMALL_INT(0)) {
+        mp_raise_ValueError(MP_ERROR_TEXT("divide by zero"));
     } else {
-        mp_obj_t result = mp_obj_new_int_from_ull(0); // Use the _from_ull version as this forces an mpz int
-        mp_obj_int_t *res_p = (mp_obj_int_t *)MP_OBJ_TO_PTR(result);
+        mp_obj_int_t *res_p = mp_obj_int_new_mpz();
 
         mpz_t l_temp, r_temp, m_temp;
         mpz_t *lhs = mp_mpz_for_int(base,     &l_temp);
@@ -368,7 +371,7 @@ mp_obj_t mp_obj_int_pow3(mp_obj_t base, mp_obj_t exponent,  mp_obj_t modulus) {
         if (mod == &m_temp) {
             mpz_deinit(mod);
         }
-        return result;
+        return MP_OBJ_FROM_PTR(res_p);
     }
 }
 #endif
@@ -425,6 +428,10 @@ mp_int_t mp_obj_int_get_checked(mp_const_obj_t self_in) {
         const mp_obj_int_t *self = MP_OBJ_TO_PTR(self_in);
         mp_int_t value;
         if (mpz_as_int_checked(&self->mpz, &value)) {
+            // mp_obj_int_t objects should always contain a value that is a large
+            // integer (if the value fits in a small-int then it should have been
+            // converted to a small-int object), and so this code-path should never
+            // be taken in normal circumstances.
             return value;
         } else {
             // overflow
@@ -457,4 +464,22 @@ mp_float_t mp_obj_int_as_float_impl(mp_obj_t self_in) {
 }
 #endif
 
+void mp_obj_int_to_bytes(mp_obj_t self_in, size_t buf_len, byte *buf, bool big_endian, bool is_signed, bool overflow_check) {
+    if (mp_obj_is_exact_type(self_in, &mp_type_int)) {
+        const mp_obj_int_t *self = MP_OBJ_TO_PTR(self_in);
+        const mpz_t *mpz = &self->mpz;
+        if (overflow_check && !is_signed && mpz->neg) {
+            mp_obj_int_raise_unsigned_negative_overflow_error();
+        }
+        if (!mpz_as_bytes(mpz, big_endian, is_signed, buf_len, buf) && overflow_check) {
+            mp_obj_int_raise_to_bytes_overflow_error(buf_len);
+        }
+    } else {
+        // self_in is either a smallint, or another type convertible to mp_int_t (i.e. bool)
+        mp_int_t val = mp_obj_get_int(self_in);
+        mp_obj_small_int_to_bytes(val, buf_len, buf, big_endian, is_signed, overflow_check);
+    }
+}
+
+
 #endif
diff --git a/py/objlist.c b/py/objlist.c
index 18da91ba3ab..41c920511d2 100644
--- a/py/objlist.c
+++ b/py/objlist.c
@@ -29,12 +29,12 @@
 
 #include "py/objlist.h"
 #include "py/runtime.h"
-#include "py/stackctrl.h"
+#include "py/cstack.h"
 
-STATIC mp_obj_t mp_obj_new_list_iterator(mp_obj_t list, size_t cur, mp_obj_iter_buf_t *iter_buf);
-STATIC mp_obj_list_t *list_new(size_t n);
-STATIC mp_obj_t list_extend(mp_obj_t self_in, mp_obj_t arg_in);
-STATIC mp_obj_t list_pop(size_t n_args, const mp_obj_t *args);
+static mp_obj_t mp_obj_new_list_iterator(mp_obj_t list, size_t cur, mp_obj_iter_buf_t *iter_buf);
+static mp_obj_list_t *list_new(size_t n);
+static mp_obj_t list_extend(mp_obj_t self_in, mp_obj_t arg_in);
+static mp_obj_t list_pop(size_t n_args, const mp_obj_t *args);
 
 // TODO: Move to mpconfig.h
 #define LIST_MIN_ALLOC 4
@@ -42,13 +42,13 @@ STATIC mp_obj_t list_pop(size_t n_args, const mp_obj_t *args);
 /******************************************************************************/
 /* list                                                                       */
 
-STATIC void list_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
+static void list_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     mp_obj_list_t *o = MP_OBJ_TO_PTR(o_in);
     const char *item_separator = ", ";
-    if (!(MICROPY_PY_UJSON && kind == PRINT_JSON)) {
+    if (!(MICROPY_PY_JSON && kind == PRINT_JSON)) {
         kind = PRINT_REPR;
     } else {
-        #if MICROPY_PY_UJSON_SEPARATORS
+        #if MICROPY_PY_JSON_SEPARATORS
         item_separator = MP_PRINT_GET_EXT(print)->item_separator;
         #endif
     }
@@ -62,7 +62,7 @@ STATIC void list_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t k
     mp_print_str(print, "]");
 }
 
-STATIC mp_obj_t list_extend_from_iter(mp_obj_t list, mp_obj_t iterable) {
+static mp_obj_t list_extend_from_iter(mp_obj_t list, mp_obj_t iterable) {
     mp_obj_t iter = mp_getiter(iterable, NULL);
     mp_obj_t item;
     while ((item = mp_iternext(iter)) != MP_OBJ_STOP_ITERATION) {
@@ -90,7 +90,7 @@ mp_obj_t mp_obj_list_make_new(const mp_obj_type_t *type_in, size_t n_args, size_
     }
 }
 
-STATIC mp_obj_t list_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
+static mp_obj_t list_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
     mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
     switch (op) {
         case MP_UNARY_OP_BOOL:
@@ -108,7 +108,7 @@ STATIC mp_obj_t list_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
     }
 }
 
-STATIC mp_obj_t list_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs) {
+static mp_obj_t list_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs) {
     mp_obj_list_t *o = MP_OBJ_TO_PTR(lhs);
     switch (op) {
         case MP_BINARY_OP_ADD: {
@@ -158,83 +158,70 @@ STATIC mp_obj_t list_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs) {
     }
 }
 
-STATIC mp_obj_t list_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
-    if (value == MP_OBJ_NULL) {
-        // delete
-        #if MICROPY_PY_BUILTINS_SLICE
-        if (mp_obj_is_type(index, &mp_type_slice)) {
-            mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
-            mp_bound_slice_t slice;
-            if (!mp_seq_get_fast_slice_indexes(self->len, index, &slice)) {
-                mp_raise_NotImplementedError(NULL);
+static mp_obj_t list_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
+    #if MICROPY_PY_BUILTINS_SLICE
+    if (mp_obj_is_type(index, &mp_type_slice)) {
+        mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
+        mp_bound_slice_t slice;
+        bool fast = mp_seq_get_fast_slice_indexes(self->len, index, &slice);
+        if (value == MP_OBJ_SENTINEL) {
+            // load
+            if (!fast) {
+                return mp_seq_extract_slice(self->items, &slice);
             }
-
-            mp_int_t len_adj = slice.start - slice.stop;
-            assert(len_adj <= 0);
-            mp_seq_replace_slice_no_grow(self->items, self->len, slice.start, slice.stop, self->items /*NULL*/, 0, sizeof(*self->items));
+            mp_obj_list_t *res = list_new(slice.stop - slice.start);
+            mp_seq_copy(res->items, self->items + slice.start, res->len, mp_obj_t);
+            return MP_OBJ_FROM_PTR(res);
+        }
+        // assign/delete
+        if (value == MP_OBJ_NULL) {
+            // delete is equivalent to slice assignment of an empty sequence
+            value = mp_const_empty_tuple;
+        }
+        if (!fast) {
+            mp_raise_NotImplementedError(NULL);
+        }
+        size_t value_len;
+        mp_obj_t *value_items;
+        mp_obj_get_array(value, &value_len, &value_items);
+        mp_int_t len_adj = value_len - (slice.stop - slice.start);
+        if (len_adj > 0) {
+            if (self->len + len_adj > self->alloc) {
+                // TODO: Might optimize memory copies here by checking if block can
+                // be grown inplace or not
+                self->items = m_renew(mp_obj_t, self->items, self->alloc, self->len + len_adj);
+                self->alloc = self->len + len_adj;
+            }
+            mp_seq_replace_slice_grow_inplace(self->items, self->len,
+                slice.start, slice.stop, value_items, value_len, len_adj, sizeof(*self->items));
+        } else {
+            mp_seq_replace_slice_no_grow(self->items, self->len,
+                slice.start, slice.stop, value_items, value_len, sizeof(*self->items));
             // Clear "freed" elements at the end of list
             mp_seq_clear(self->items, self->len + len_adj, self->len, sizeof(*self->items));
-            self->len += len_adj;
-            return mp_const_none;
+            // TODO: apply allocation policy re: alloc_size
         }
-        #endif
+        self->len += len_adj;
+        return mp_const_none;
+    }
+    #endif
+    if (value == MP_OBJ_NULL) {
+        // delete
         mp_obj_t args[2] = {self_in, index};
         list_pop(2, args);
         return mp_const_none;
     } else if (value == MP_OBJ_SENTINEL) {
         // load
         mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
-        #if MICROPY_PY_BUILTINS_SLICE
-        if (mp_obj_is_type(index, &mp_type_slice)) {
-            mp_bound_slice_t slice;
-            if (!mp_seq_get_fast_slice_indexes(self->len, index, &slice)) {
-                return mp_seq_extract_slice(self->len, self->items, &slice);
-            }
-            mp_obj_list_t *res = list_new(slice.stop - slice.start);
-            mp_seq_copy(res->items, self->items + slice.start, res->len, mp_obj_t);
-            return MP_OBJ_FROM_PTR(res);
-        }
-        #endif
         size_t index_val = mp_get_index(self->base.type, self->len, index, false);
         return self->items[index_val];
     } else {
-        #if MICROPY_PY_BUILTINS_SLICE
-        if (mp_obj_is_type(index, &mp_type_slice)) {
-            mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
-            size_t value_len;
-            mp_obj_t *value_items;
-            mp_obj_get_array(value, &value_len, &value_items);
-            mp_bound_slice_t slice_out;
-            if (!mp_seq_get_fast_slice_indexes(self->len, index, &slice_out)) {
-                mp_raise_NotImplementedError(NULL);
-            }
-            mp_int_t len_adj = value_len - (slice_out.stop - slice_out.start);
-            if (len_adj > 0) {
-                if (self->len + len_adj > self->alloc) {
-                    // TODO: Might optimize memory copies here by checking if block can
-                    // be grown inplace or not
-                    self->items = m_renew(mp_obj_t, self->items, self->alloc, self->len + len_adj);
-                    self->alloc = self->len + len_adj;
-                }
-                mp_seq_replace_slice_grow_inplace(self->items, self->len,
-                    slice_out.start, slice_out.stop, value_items, value_len, len_adj, sizeof(*self->items));
-            } else {
-                mp_seq_replace_slice_no_grow(self->items, self->len,
-                    slice_out.start, slice_out.stop, value_items, value_len, sizeof(*self->items));
-                // Clear "freed" elements at the end of list
-                mp_seq_clear(self->items, self->len + len_adj, self->len, sizeof(*self->items));
-                // TODO: apply allocation policy re: alloc_size
-            }
-            self->len += len_adj;
-            return mp_const_none;
-        }
-        #endif
         mp_obj_list_store(self_in, index, value);
         return mp_const_none;
     }
 }
 
-STATIC mp_obj_t list_getiter(mp_obj_t o_in, mp_obj_iter_buf_t *iter_buf) {
+static mp_obj_t list_getiter(mp_obj_t o_in, mp_obj_iter_buf_t *iter_buf) {
     return mp_obj_new_list_iterator(o_in, 0, iter_buf);
 }
 
@@ -250,7 +237,7 @@ mp_obj_t mp_obj_list_append(mp_obj_t self_in, mp_obj_t arg) {
     return mp_const_none; // return None, as per CPython
 }
 
-STATIC mp_obj_t list_extend(mp_obj_t self_in, mp_obj_t arg_in) {
+static mp_obj_t list_extend(mp_obj_t self_in, mp_obj_t arg_in) {
     mp_check_self(mp_obj_is_type(self_in, &mp_type_list));
     if (mp_obj_is_type(arg_in, &mp_type_list)) {
         mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
@@ -271,7 +258,7 @@ STATIC mp_obj_t list_extend(mp_obj_t self_in, mp_obj_t arg_in) {
     return mp_const_none; // return None, as per CPython
 }
 
-STATIC mp_obj_t list_pop(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t list_pop(size_t n_args, const mp_obj_t *args) {
     mp_check_self(mp_obj_is_type(args[0], &mp_type_list));
     mp_obj_list_t *self = MP_OBJ_TO_PTR(args[0]);
     if (self->len == 0) {
@@ -290,10 +277,20 @@ STATIC mp_obj_t list_pop(size_t n_args, const mp_obj_t *args) {
     return ret;
 }
 
-STATIC void mp_quicksort(mp_obj_t *head, mp_obj_t *tail, mp_obj_t key_fn, mp_obj_t binop_less_result) {
-    MP_STACK_CHECK();
-    while (head < tail) {
-        mp_obj_t *h = head - 1;
+// "head" is actually the *exclusive lower bound* of the range to sort. That is,
+// the first element to be sorted is `head[1]`, not `head[0]`. Similarly `tail`
+// is an *inclusive upper bound* of the range to sort. That is, the final
+// element to sort is `tail[0]`, not `tail[-1]`.
+//
+// The pivot element is always chosen as `tail[0]`.
+//
+// These unusual choices allows structuring the partitioning
+// process as a do/while loop, which generates smaller code than the equivalent
+// code with usual C bounds & a while or for loop.
+static void mp_quicksort(mp_obj_t *head, mp_obj_t *tail, mp_obj_t key_fn, mp_obj_t binop_less_result) {
+    mp_cstack_check();
+    while (tail - head > 1) { // So long as at least 2 elements remain
+        mp_obj_t *h = head;
         mp_obj_t *t = tail;
         mp_obj_t v = key_fn == MP_OBJ_NULL ? tail[0] : mp_call_function_1(key_fn, tail[0]); // get pivot using key_fn
         for (;;) {
@@ -304,19 +301,21 @@ STATIC void mp_quicksort(mp_obj_t *head, mp_obj_t *tail, mp_obj_t key_fn, mp_obj
             if (h >= t) {
                 break;
             }
+            // A pair of objects must be swapped to the other side of the partition
             mp_obj_t x = h[0];
             h[0] = t[0];
             t[0] = x;
         }
+        // Place the pivot element in the proper position
         mp_obj_t x = h[0];
         h[0] = tail[0];
         tail[0] = x;
         // do the smaller recursive call first, to keep stack within O(log(N))
-        if (t - head < tail - h - 1) {
+        if (t - head < tail - h) {
             mp_quicksort(head, t, key_fn, binop_less_result);
-            head = h + 1;
+            head = h;
         } else {
-            mp_quicksort(h + 1, tail, key_fn, binop_less_result);
+            mp_quicksort(h, tail, key_fn, binop_less_result);
             tail = t;
         }
     }
@@ -340,7 +339,7 @@ mp_obj_t mp_obj_list_sort(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_
     mp_obj_list_t *self = MP_OBJ_TO_PTR(pos_args[0]);
 
     if (self->len > 1) {
-        mp_quicksort(self->items, self->items + self->len - 1,
+        mp_quicksort(self->items - 1, self->items + self->len - 1,
             args.key.u_obj == mp_const_none ? MP_OBJ_NULL : args.key.u_obj,
             args.reverse.u_bool ? mp_const_false : mp_const_true);
     }
@@ -348,7 +347,7 @@ mp_obj_t mp_obj_list_sort(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_
     return mp_const_none;
 }
 
-STATIC mp_obj_t list_clear(mp_obj_t self_in) {
+static mp_obj_t list_clear(mp_obj_t self_in) {
     mp_check_self(mp_obj_is_type(self_in, &mp_type_list));
     mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
     self->len = 0;
@@ -358,25 +357,25 @@ STATIC mp_obj_t list_clear(mp_obj_t self_in) {
     return mp_const_none;
 }
 
-STATIC mp_obj_t list_copy(mp_obj_t self_in) {
+static mp_obj_t list_copy(mp_obj_t self_in) {
     mp_check_self(mp_obj_is_type(self_in, &mp_type_list));
     mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
     return mp_obj_new_list(self->len, self->items);
 }
 
-STATIC mp_obj_t list_count(mp_obj_t self_in, mp_obj_t value) {
+static mp_obj_t list_count(mp_obj_t self_in, mp_obj_t value) {
     mp_check_self(mp_obj_is_type(self_in, &mp_type_list));
     mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
     return mp_seq_count_obj(self->items, self->len, value);
 }
 
-STATIC mp_obj_t list_index(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t list_index(size_t n_args, const mp_obj_t *args) {
     mp_check_self(mp_obj_is_type(args[0], &mp_type_list));
     mp_obj_list_t *self = MP_OBJ_TO_PTR(args[0]);
     return mp_seq_index_obj(self->items, self->len, n_args, args);
 }
 
-STATIC mp_obj_t list_insert(mp_obj_t self_in, mp_obj_t idx, mp_obj_t obj) {
+static mp_obj_t list_insert(mp_obj_t self_in, mp_obj_t idx, mp_obj_t obj) {
     mp_check_self(mp_obj_is_type(self_in, &mp_type_list));
     mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
     // insert has its own strange index logic
@@ -410,7 +409,7 @@ mp_obj_t mp_obj_list_remove(mp_obj_t self_in, mp_obj_t value) {
     return mp_const_none;
 }
 
-STATIC mp_obj_t list_reverse(mp_obj_t self_in) {
+static mp_obj_t list_reverse(mp_obj_t self_in) {
     mp_check_self(mp_obj_is_type(self_in, &mp_type_list));
     mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
 
@@ -424,19 +423,19 @@ STATIC mp_obj_t list_reverse(mp_obj_t self_in) {
     return mp_const_none;
 }
 
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(list_append_obj, mp_obj_list_append);
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(list_extend_obj, list_extend);
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(list_clear_obj, list_clear);
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(list_copy_obj, list_copy);
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(list_count_obj, list_count);
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(list_index_obj, 2, 4, list_index);
-STATIC MP_DEFINE_CONST_FUN_OBJ_3(list_insert_obj, list_insert);
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(list_pop_obj, 1, 2, list_pop);
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(list_remove_obj, mp_obj_list_remove);
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(list_reverse_obj, list_reverse);
-STATIC MP_DEFINE_CONST_FUN_OBJ_KW(list_sort_obj, 1, mp_obj_list_sort);
-
-STATIC const mp_rom_map_elem_t list_locals_dict_table[] = {
+static MP_DEFINE_CONST_FUN_OBJ_2(list_append_obj, mp_obj_list_append);
+static MP_DEFINE_CONST_FUN_OBJ_2(list_extend_obj, list_extend);
+static MP_DEFINE_CONST_FUN_OBJ_1(list_clear_obj, list_clear);
+static MP_DEFINE_CONST_FUN_OBJ_1(list_copy_obj, list_copy);
+static MP_DEFINE_CONST_FUN_OBJ_2(list_count_obj, list_count);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(list_index_obj, 2, 4, list_index);
+static MP_DEFINE_CONST_FUN_OBJ_3(list_insert_obj, list_insert);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(list_pop_obj, 1, 2, list_pop);
+static MP_DEFINE_CONST_FUN_OBJ_2(list_remove_obj, mp_obj_list_remove);
+static MP_DEFINE_CONST_FUN_OBJ_1(list_reverse_obj, list_reverse);
+static MP_DEFINE_CONST_FUN_OBJ_KW(list_sort_obj, 1, mp_obj_list_sort);
+
+static const mp_rom_map_elem_t list_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_append), MP_ROM_PTR(&list_append_obj) },
     { MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&list_clear_obj) },
     { MP_ROM_QSTR(MP_QSTR_copy), MP_ROM_PTR(&list_copy_obj) },
@@ -450,7 +449,7 @@ STATIC const mp_rom_map_elem_t list_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_sort), MP_ROM_PTR(&list_sort_obj) },
 };
 
-STATIC MP_DEFINE_CONST_DICT(list_locals_dict, list_locals_dict_table);
+static MP_DEFINE_CONST_DICT(list_locals_dict, list_locals_dict_table);
 
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_list,
@@ -474,7 +473,7 @@ void mp_obj_list_init(mp_obj_list_t *o, size_t n) {
     mp_seq_clear(o->items, n, o->alloc, sizeof(*o->items));
 }
 
-STATIC mp_obj_list_t *list_new(size_t n) {
+static mp_obj_list_t *list_new(size_t n) {
     mp_obj_list_t *o = m_new_obj(mp_obj_list_t);
     mp_obj_list_init(o, n);
     return o;
@@ -490,25 +489,6 @@ mp_obj_t mp_obj_new_list(size_t n, mp_obj_t *items) {
     return MP_OBJ_FROM_PTR(o);
 }
 
-void mp_obj_list_get(mp_obj_t self_in, size_t *len, mp_obj_t **items) {
-    mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
-    *len = self->len;
-    *items = self->items;
-}
-
-void mp_obj_list_set_len(mp_obj_t self_in, size_t len) {
-    // trust that the caller knows what it's doing
-    // TODO realloc if len got much smaller than alloc
-    mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
-    self->len = len;
-}
-
-void mp_obj_list_store(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
-    mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);
-    size_t i = mp_get_index(self->base.type, self->len, index, false);
-    self->items[i] = value;
-}
-
 /******************************************************************************/
 /* list iterator                                                              */
 
@@ -519,7 +499,7 @@ typedef struct _mp_obj_list_it_t {
     size_t cur;
 } mp_obj_list_it_t;
 
-STATIC mp_obj_t list_it_iternext(mp_obj_t self_in) {
+static mp_obj_t list_it_iternext(mp_obj_t self_in) {
     mp_obj_list_it_t *self = MP_OBJ_TO_PTR(self_in);
     mp_obj_list_t *list = MP_OBJ_TO_PTR(self->list);
     if (self->cur < list->len) {
@@ -540,3 +520,22 @@ mp_obj_t mp_obj_new_list_iterator(mp_obj_t list, size_t cur, mp_obj_iter_buf_t *
     o->cur = cur;
     return MP_OBJ_FROM_PTR(o);
 }
+
+mp_obj_list_t *mp_obj_list_optional_arg(mp_obj_t arg_in, size_t min_len) {
+    if (arg_in == MP_OBJ_NULL || arg_in == mp_const_none) {
+        return MP_OBJ_TO_PTR(mp_obj_new_list(min_len, NULL));
+    } else {
+        return mp_obj_list_ensure(arg_in, min_len);
+    }
+}
+
+mp_obj_list_t *mp_obj_list_ensure(mp_obj_t in, size_t min_len) {
+    if (!mp_obj_is_type(in, &mp_type_list)) {
+        mp_raise_TypeError(NULL);
+    }
+    mp_obj_list_t *list = MP_OBJ_TO_PTR(in);
+    if (list->len < min_len) {
+        mp_raise_ValueError(NULL);
+    }
+    return list;
+}
diff --git a/py/objlist.h b/py/objlist.h
index a3bba68028f..e72778266a3 100644
--- a/py/objlist.h
+++ b/py/objlist.h
@@ -37,5 +37,34 @@ typedef struct _mp_obj_list_t {
 
 void mp_obj_list_init(mp_obj_list_t *o, size_t n);
 mp_obj_t mp_obj_list_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args);
+mp_obj_t mp_obj_list_append(mp_obj_t self_in, mp_obj_t arg);
+mp_obj_t mp_obj_list_sort(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs);
+mp_obj_t mp_obj_list_remove(mp_obj_t self_in, mp_obj_t value);
+
+static inline void mp_obj_list_get(mp_obj_t self_in, size_t *len, mp_obj_t **items) {
+    mp_obj_list_t *self = (mp_obj_list_t *)MP_OBJ_TO_PTR(self_in);
+    *len = self->len;
+    *items = self->items;
+}
+
+static inline void mp_obj_list_set_len(mp_obj_t self_in, size_t len) {
+    // trust that the caller knows what it's doing
+    // TODO realloc if len got much smaller than alloc
+    mp_obj_list_t *self = (mp_obj_list_t *)MP_OBJ_TO_PTR(self_in);
+    self->len = len;
+}
+
+static inline void mp_obj_list_store(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
+    mp_obj_list_t *self = (mp_obj_list_t *)MP_OBJ_TO_PTR(self_in);
+    size_t i = mp_get_index(self->base.type, self->len, index, false);
+    self->items[i] = value;
+}
+
+// Helper function for pattern of an optional argument which can be a list of a specified size, and is
+// allocated on-demand otherwise
+mp_obj_list_t *mp_obj_list_optional_arg(mp_obj_t arg_in, size_t min_len);
+
+// Ensure provided object is a list of minimum length min_len. Raises TypeError & ValueError otherwise.
+mp_obj_list_t *mp_obj_list_ensure(mp_obj_t in, size_t min_len);
 
 #endif // MICROPY_INCLUDED_PY_OBJLIST_H
diff --git a/py/objmap.c b/py/objmap.c
index e7e594cd2c5..1911a7510ab 100644
--- a/py/objmap.c
+++ b/py/objmap.c
@@ -36,9 +36,9 @@ typedef struct _mp_obj_map_t {
     mp_obj_t iters[];
 } mp_obj_map_t;
 
-STATIC mp_obj_t map_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t map_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     mp_arg_check_num(n_args, n_kw, 2, MP_OBJ_FUN_ARGS_MAX, false);
-    mp_obj_map_t *o = mp_obj_malloc_var(mp_obj_map_t, mp_obj_t, n_args - 1, type);
+    mp_obj_map_t *o = mp_obj_malloc_var(mp_obj_map_t, iters, mp_obj_t, n_args - 1, type);
     o->n_iters = n_args - 1;
     o->fun = args[0];
     for (size_t i = 0; i < n_args - 1; i++) {
@@ -47,7 +47,7 @@ STATIC mp_obj_t map_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_
     return MP_OBJ_FROM_PTR(o);
 }
 
-STATIC mp_obj_t map_iternext(mp_obj_t self_in) {
+static mp_obj_t map_iternext(mp_obj_t self_in) {
     mp_check_self(mp_obj_is_type(self_in, &mp_type_map));
     mp_obj_map_t *self = MP_OBJ_TO_PTR(self_in);
     mp_obj_t *nextses = m_new(mp_obj_t, self->n_iters);
diff --git a/py/objmodule.c b/py/objmodule.c
index 7326fbe2d12..2ffa7847f42 100644
--- a/py/objmodule.c
+++ b/py/objmodule.c
@@ -34,13 +34,7 @@
 #include "py/runtime.h"
 #include "py/builtin.h"
 
-#ifndef NO_QSTR
-// Only include module definitions when not doing qstr extraction, because the
-// qstr extraction stage also generates this module definition header file.
-#include "genhdr/moduledefs.h"
-#endif
-
-STATIC void module_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void module_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_module_t *self = MP_OBJ_TO_PTR(self_in);
 
@@ -50,7 +44,7 @@ STATIC void module_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kin
         module_name = mp_obj_str_get_str(elem->value);
     }
 
-    #if MICROPY_PY___FILE__
+    #if MICROPY_MODULE___FILE__
     // If we store __file__ to imported modules then try to lookup this
     // symbol to give more information about the module.
     elem = mp_map_lookup(&self->globals->map, MP_OBJ_NEW_QSTR(MP_QSTR___file__), MP_MAP_LOOKUP);
@@ -63,22 +57,9 @@ STATIC void module_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kin
     mp_printf(print, "", module_name);
 }
 
-STATIC void module_attr_try_delegation(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
-    #if MICROPY_MODULE_ATTR_DELEGATION
-    // Delegate lookup to a module's custom attr method (found in last lot of globals dict).
-    mp_obj_module_t *self = MP_OBJ_TO_PTR(self_in);
-    mp_map_t *map = &self->globals->map;
-    if (map->table[map->alloc - 1].key == MP_OBJ_NEW_QSTR(MP_QSTRnull)) {
-        ((mp_attr_fun_t)MP_OBJ_TO_PTR(map->table[map->alloc - 1].value))(self_in, attr, dest);
-    }
-    #else
-    (void)self_in;
-    (void)attr;
-    (void)dest;
-    #endif
-}
+static void module_attr_try_delegation(mp_obj_t self_in, qstr attr, mp_obj_t *dest);
 
-STATIC void module_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+static void module_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     mp_obj_module_t *self = MP_OBJ_TO_PTR(self_in);
     if (dest[0] == MP_OBJ_NULL) {
         // load attribute
@@ -165,19 +146,78 @@ mp_obj_t mp_obj_new_module(qstr module_name) {
 /******************************************************************************/
 // Global module table and related functions
 
-STATIC const mp_rom_map_elem_t mp_builtin_module_table[] = {
-    // builtin modules declared with MP_REGISTER_MODULE()
+static const mp_rom_map_elem_t mp_builtin_module_table[] = {
+    // built-in modules declared with MP_REGISTER_MODULE()
     MICROPY_REGISTERED_MODULES
 };
-
 MP_DEFINE_CONST_MAP(mp_builtin_module_map, mp_builtin_module_table);
 
-// Attempts to find (and initialise) a builtin, otherwise returns
+#if MICROPY_HAVE_REGISTERED_EXTENSIBLE_MODULES
+static const mp_rom_map_elem_t mp_builtin_extensible_module_table[] = {
+    // built-in modules declared with MP_REGISTER_EXTENSIBLE_MODULE()
+    MICROPY_REGISTERED_EXTENSIBLE_MODULES
+};
+MP_DEFINE_CONST_MAP(mp_builtin_extensible_module_map, mp_builtin_extensible_module_table);
+#endif
+
+#if MICROPY_MODULE_ATTR_DELEGATION && defined(MICROPY_MODULE_DELEGATIONS)
+typedef struct _mp_module_delegation_entry_t {
+    mp_rom_obj_t mod;
+    mp_attr_fun_t fun;
+} mp_module_delegation_entry_t;
+
+static const mp_module_delegation_entry_t mp_builtin_module_delegation_table[] = {
+    // delegation entries declared with MP_REGISTER_MODULE_DELEGATION()
+    MICROPY_MODULE_DELEGATIONS
+};
+#endif
+
+// Attempts to find (and initialise) a built-in, otherwise returns
 // MP_OBJ_NULL.
-mp_obj_t mp_module_get_builtin(qstr module_name) {
-    mp_map_elem_t *elem = mp_map_lookup((mp_map_t *)&mp_builtin_module_map, MP_OBJ_NEW_QSTR(module_name), MP_MAP_LOOKUP);
+mp_obj_t mp_module_get_builtin(qstr module_name, bool extensible) {
+    #if MICROPY_HAVE_REGISTERED_EXTENSIBLE_MODULES
+    const mp_map_t *map = extensible ? &mp_builtin_extensible_module_map : &mp_builtin_module_map;
+    #else
+    const mp_map_t *map = &mp_builtin_module_map;
+    #endif
+    mp_map_elem_t *elem = mp_map_lookup((mp_map_t *)map, MP_OBJ_NEW_QSTR(module_name), MP_MAP_LOOKUP);
+
     if (!elem) {
-        return MP_OBJ_NULL;
+        #if MICROPY_PY_SYS
+        // Special case for sys, which isn't extensible but can always be
+        // imported with the alias `usys`.
+        if (module_name == MP_QSTR_usys) {
+            return MP_OBJ_FROM_PTR(&mp_module_sys);
+        }
+        #endif
+
+        #if MICROPY_HAVE_REGISTERED_EXTENSIBLE_MODULES
+        if (extensible) {
+            // At this point we've already tried non-extensible built-ins, the
+            // filesystem, and now extensible built-ins. No match, so fail
+            // the import.
+            return MP_OBJ_NULL;
+        }
+
+        // We're trying to match a non-extensible built-in (i.e. before trying
+        // the filesystem), but if the user is importing `ufoo`, _and_ `foo`
+        // is an extensible module, then allow it as a way of forcing the
+        // built-in. Essentially, this makes it as if all the extensible
+        // built-ins also had non-extensible aliases named `ufoo`. Newer code
+        // should be using sys.path to force the built-in, but this retains
+        // the old behaviour of the u-prefix being used to force a built-in
+        // import.
+        size_t module_name_len;
+        const char *module_name_str = (const char *)qstr_data(module_name, &module_name_len);
+        if (module_name_str[0] != 'u') {
+            return MP_OBJ_NULL;
+        }
+        elem = mp_map_lookup((mp_map_t *)&mp_builtin_extensible_module_map, MP_OBJ_NEW_QSTR(qstr_find_strn(module_name_str + 1, module_name_len - 1)), MP_MAP_LOOKUP);
+        #endif
+
+        if (!elem) {
+            return MP_OBJ_NULL;
+        }
     }
 
     #if MICROPY_MODULE_BUILTIN_INIT
@@ -194,6 +234,23 @@ mp_obj_t mp_module_get_builtin(qstr module_name) {
     return elem->value;
 }
 
+static void module_attr_try_delegation(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    #if MICROPY_MODULE_ATTR_DELEGATION && defined(MICROPY_MODULE_DELEGATIONS)
+    // Delegate lookup to a module's custom attr method.
+    size_t n = MP_ARRAY_SIZE(mp_builtin_module_delegation_table);
+    for (size_t i = 0; i < n; ++i) {
+        if (*(mp_obj_t *)(&mp_builtin_module_delegation_table[i].mod) == self_in) {
+            mp_builtin_module_delegation_table[i].fun(self_in, attr, dest);
+            break;
+        }
+    }
+    #else
+    (void)self_in;
+    (void)attr;
+    (void)dest;
+    #endif
+}
+
 void mp_module_generic_attr(qstr attr, mp_obj_t *dest, const uint16_t *keys, mp_obj_t *values) {
     for (size_t i = 0; keys[i] != MP_QSTRnull; ++i) {
         if (attr == keys[i]) {
diff --git a/py/objmodule.h b/py/objmodule.h
index 11bd7bb42fd..3ca097da948 100644
--- a/py/objmodule.h
+++ b/py/objmodule.h
@@ -28,13 +28,24 @@
 
 #include "py/obj.h"
 
-// Place at the very end of a module's globals_table.
-#define MP_MODULE_ATTR_DELEGATION_ENTRY(ptr) { MP_ROM_QSTR(MP_QSTRnull), MP_ROM_PTR(ptr) }
+#ifndef NO_QSTR
+// Only include module definitions when not doing qstr extraction, because the
+// qstr extraction stage also generates this module definition header file.
+#include "genhdr/moduledefs.h"
+#endif
 
 extern const mp_map_t mp_builtin_module_map;
 
-mp_obj_t mp_module_get_builtin(qstr module_name);
+#if MICROPY_HAVE_REGISTERED_EXTENSIBLE_MODULES
+extern const mp_map_t mp_builtin_extensible_module_map;
+#endif
+
+mp_obj_t mp_module_get_builtin(qstr module_name, bool extensible);
 
 void mp_module_generic_attr(qstr attr, mp_obj_t *dest, const uint16_t *keys, mp_obj_t *values);
 
+static inline mp_obj_dict_t *mp_obj_module_get_globals(mp_obj_t module) {
+    return ((mp_obj_module_t *)MP_OBJ_TO_PTR(module))->globals;
+}
+
 #endif // MICROPY_INCLUDED_PY_OBJMODULE_H
diff --git a/py/objnamedtuple.c b/py/objnamedtuple.c
index 75e21494b4d..e8447ee31ef 100644
--- a/py/objnamedtuple.c
+++ b/py/objnamedtuple.c
@@ -44,7 +44,7 @@ size_t mp_obj_namedtuple_find_field(const mp_obj_namedtuple_type_t *type, qstr n
 }
 
 #if MICROPY_PY_COLLECTIONS_NAMEDTUPLE__ASDICT
-STATIC mp_obj_t namedtuple_asdict(mp_obj_t self_in) {
+static mp_obj_t namedtuple_asdict(mp_obj_t self_in) {
     mp_obj_namedtuple_t *self = MP_OBJ_TO_PTR(self_in);
     const qstr *fields = ((mp_obj_namedtuple_type_t *)self->tuple.base.type)->fields;
     mp_obj_t dict = mp_obj_new_dict(self->tuple.len);
@@ -60,15 +60,15 @@ STATIC mp_obj_t namedtuple_asdict(mp_obj_t self_in) {
 MP_DEFINE_CONST_FUN_OBJ_1(namedtuple_asdict_obj, namedtuple_asdict);
 #endif
 
-STATIC void namedtuple_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
+static void namedtuple_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_namedtuple_t *o = MP_OBJ_TO_PTR(o_in);
-    mp_printf(print, "%q", o->tuple.base.type->name);
+    mp_printf(print, "%q", (qstr)o->tuple.base.type->name);
     const qstr *fields = ((mp_obj_namedtuple_type_t *)o->tuple.base.type)->fields;
     mp_obj_attrtuple_print_helper(print, fields, &o->tuple);
 }
 
-STATIC void namedtuple_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+static void namedtuple_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     if (dest[0] == MP_OBJ_NULL) {
         // load attribute
         mp_obj_namedtuple_t *self = MP_OBJ_TO_PTR(self_in);
@@ -91,7 +91,7 @@ STATIC void namedtuple_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     }
 }
 
-STATIC mp_obj_t namedtuple_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t namedtuple_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     const mp_obj_namedtuple_type_t *type = (const mp_obj_namedtuple_type_t *)type_in;
     size_t num_fields = type->n_fields;
     if (n_args + n_kw != num_fields) {
@@ -110,7 +110,7 @@ STATIC mp_obj_t namedtuple_make_new(const mp_obj_type_t *type_in, size_t n_args,
 
     // Create a namedtuple with explicit malloc. Calling mp_obj_new_tuple
     // with num_fields=0 returns a read-only object.
-    mp_obj_tuple_t *tuple = mp_obj_malloc_var(mp_obj_tuple_t, mp_obj_t, num_fields, type_in);
+    mp_obj_tuple_t *tuple = mp_obj_malloc_var(mp_obj_tuple_t, items, mp_obj_t, num_fields, type_in);
     tuple->len = num_fields;
 
     // Copy the positional args into the first slots of the namedtuple
@@ -143,7 +143,7 @@ STATIC mp_obj_t namedtuple_make_new(const mp_obj_type_t *type_in, size_t n_args,
 }
 
 mp_obj_namedtuple_type_t *mp_obj_new_namedtuple_base(size_t n_fields, mp_obj_t *fields) {
-    mp_obj_namedtuple_type_t *o = m_new_obj_var0(mp_obj_namedtuple_type_t, qstr, n_fields);
+    mp_obj_namedtuple_type_t *o = m_new_obj_var0(mp_obj_namedtuple_type_t, fields, qstr, n_fields);
     o->n_fields = n_fields;
     for (size_t i = 0; i < n_fields; i++) {
         o->fields[i] = mp_obj_str_get_qstr(fields[i]);
@@ -151,7 +151,7 @@ mp_obj_namedtuple_type_t *mp_obj_new_namedtuple_base(size_t n_fields, mp_obj_t *
     return o;
 }
 
-STATIC mp_obj_t mp_obj_new_namedtuple_type(qstr name, size_t n_fields, mp_obj_t *fields) {
+static mp_obj_t mp_obj_new_namedtuple_type(qstr name, size_t n_fields, mp_obj_t *fields) {
     mp_obj_namedtuple_type_t *o = mp_obj_new_namedtuple_base(n_fields, fields);
     mp_obj_type_t *type = (mp_obj_type_t *)&o->base;
     type->base.type = &mp_type_type;
@@ -168,7 +168,7 @@ STATIC mp_obj_t mp_obj_new_namedtuple_type(qstr name, size_t n_fields, mp_obj_t
     return MP_OBJ_FROM_PTR(o);
 }
 
-STATIC mp_obj_t new_namedtuple_type(mp_obj_t name_in, mp_obj_t fields_in) {
+static mp_obj_t new_namedtuple_type(mp_obj_t name_in, mp_obj_t fields_in) {
     qstr name = mp_obj_str_get_qstr(name_in);
     size_t n_fields;
     mp_obj_t *fields;
diff --git a/py/objnone.c b/py/objnone.c
index 4b0696d3a14..a8ce8ebfedf 100644
--- a/py/objnone.c
+++ b/py/objnone.c
@@ -34,9 +34,9 @@ typedef struct _mp_obj_none_t {
 } mp_obj_none_t;
 #endif
 
-STATIC void none_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void none_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     (void)self_in;
-    if (MICROPY_PY_UJSON && kind == PRINT_JSON) {
+    if (MICROPY_PY_JSON && kind == PRINT_JSON) {
         mp_print_str(print, "null");
     } else {
         mp_print_str(print, "None");
diff --git a/py/objobject.c b/py/objobject.c
index 1acae6d00b1..ff93fd0827d 100644
--- a/py/objobject.c
+++ b/py/objobject.c
@@ -33,7 +33,7 @@ typedef struct _mp_obj_object_t {
     mp_obj_base_t base;
 } mp_obj_object_t;
 
-STATIC mp_obj_t object_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t object_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)args;
     mp_arg_check_num(n_args, n_kw, 0, 0, false);
     mp_obj_object_t *o = mp_obj_malloc(mp_obj_object_t, type);
@@ -41,13 +41,13 @@ STATIC mp_obj_t object_make_new(const mp_obj_type_t *type, size_t n_args, size_t
 }
 
 #if MICROPY_CPYTHON_COMPAT
-STATIC mp_obj_t object___init__(mp_obj_t self) {
+static mp_obj_t object___init__(mp_obj_t self) {
     (void)self;
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(object___init___obj, object___init__);
+static MP_DEFINE_CONST_FUN_OBJ_1(object___init___obj, object___init__);
 
-STATIC mp_obj_t object___new__(mp_obj_t cls) {
+static mp_obj_t object___new__(mp_obj_t cls) {
     if (!mp_obj_is_type(cls, &mp_type_type) || !mp_obj_is_instance_type((mp_obj_type_t *)MP_OBJ_TO_PTR(cls))) {
         mp_raise_TypeError(MP_ERROR_TEXT("arg must be user-type"));
     }
@@ -58,11 +58,11 @@ STATIC mp_obj_t object___new__(mp_obj_t cls) {
     const mp_obj_type_t *native_base;
     return MP_OBJ_FROM_PTR(mp_obj_new_instance(MP_OBJ_TO_PTR(cls), &native_base));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(object___new___fun_obj, object___new__);
-STATIC MP_DEFINE_CONST_STATICMETHOD_OBJ(object___new___obj, MP_ROM_PTR(&object___new___fun_obj));
+static MP_DEFINE_CONST_FUN_OBJ_1(object___new___fun_obj, object___new__);
+static MP_DEFINE_CONST_STATICMETHOD_OBJ(object___new___obj, MP_ROM_PTR(&object___new___fun_obj));
 
 #if MICROPY_PY_DELATTR_SETATTR
-STATIC mp_obj_t object___setattr__(mp_obj_t self_in, mp_obj_t attr, mp_obj_t value) {
+static mp_obj_t object___setattr__(mp_obj_t self_in, mp_obj_t attr, mp_obj_t value) {
     if (!mp_obj_is_instance_type(mp_obj_get_type(self_in))) {
         mp_raise_TypeError(MP_ERROR_TEXT("arg must be user-type"));
     }
@@ -75,9 +75,9 @@ STATIC mp_obj_t object___setattr__(mp_obj_t self_in, mp_obj_t attr, mp_obj_t val
     mp_map_lookup(&self->members, attr, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND)->value = value;
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_3(object___setattr___obj, object___setattr__);
+static MP_DEFINE_CONST_FUN_OBJ_3(object___setattr___obj, object___setattr__);
 
-STATIC mp_obj_t object___delattr__(mp_obj_t self_in, mp_obj_t attr) {
+static mp_obj_t object___delattr__(mp_obj_t self_in, mp_obj_t attr) {
     if (!mp_obj_is_instance_type(mp_obj_get_type(self_in))) {
         mp_raise_TypeError(MP_ERROR_TEXT("arg must be user-type"));
     }
@@ -92,10 +92,10 @@ STATIC mp_obj_t object___delattr__(mp_obj_t self_in, mp_obj_t attr) {
     }
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(object___delattr___obj, object___delattr__);
+static MP_DEFINE_CONST_FUN_OBJ_2(object___delattr___obj, object___delattr__);
 #endif
 
-STATIC const mp_rom_map_elem_t object_locals_dict_table[] = {
+static const mp_rom_map_elem_t object_locals_dict_table[] = {
     #if MICROPY_CPYTHON_COMPAT
     { MP_ROM_QSTR(MP_QSTR___init__), MP_ROM_PTR(&object___init___obj) },
     #endif
@@ -108,7 +108,7 @@ STATIC const mp_rom_map_elem_t object_locals_dict_table[] = {
     #endif
 };
 
-STATIC MP_DEFINE_CONST_DICT(object_locals_dict, object_locals_dict_table);
+static MP_DEFINE_CONST_DICT(object_locals_dict, object_locals_dict_table);
 #endif
 
 #if MICROPY_CPYTHON_COMPAT
diff --git a/py/objpolyiter.c b/py/objpolyiter.c
index 78b600abacd..65c1f182ec8 100644
--- a/py/objpolyiter.c
+++ b/py/objpolyiter.c
@@ -39,7 +39,7 @@ typedef struct _mp_obj_polymorph_iter_t {
     mp_fun_1_t iternext;
 } mp_obj_polymorph_iter_t;
 
-STATIC mp_obj_t polymorph_it_iternext(mp_obj_t self_in) {
+static mp_obj_t polymorph_it_iternext(mp_obj_t self_in) {
     mp_obj_polymorph_iter_t *self = MP_OBJ_TO_PTR(self_in);
     // Redirect call to object instance's iternext method
     return self->iternext(self_in);
@@ -64,17 +64,17 @@ typedef struct _mp_obj_polymorph_iter_with_finaliser_t {
     mp_fun_1_t finaliser;
 } mp_obj_polymorph_with_finaliser_iter_t;
 
-STATIC mp_obj_t mp_obj_polymorph_iter_del(mp_obj_t self_in) {
+static mp_obj_t mp_obj_polymorph_iter_del(mp_obj_t self_in) {
     mp_obj_polymorph_with_finaliser_iter_t *self = MP_OBJ_TO_PTR(self_in);
     // Redirect call to object instance's finaliser method
     return self->finaliser(self_in);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_obj_polymorph_iter_del_obj, mp_obj_polymorph_iter_del);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_obj_polymorph_iter_del_obj, mp_obj_polymorph_iter_del);
 
-STATIC const mp_rom_map_elem_t mp_obj_polymorph_iter_locals_dict_table[] = {
+static const mp_rom_map_elem_t mp_obj_polymorph_iter_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mp_obj_polymorph_iter_del_obj) },
 };
-STATIC MP_DEFINE_CONST_DICT(mp_obj_polymorph_iter_locals_dict, mp_obj_polymorph_iter_locals_dict_table);
+static MP_DEFINE_CONST_DICT(mp_obj_polymorph_iter_locals_dict, mp_obj_polymorph_iter_locals_dict_table);
 
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_polymorph_iter_with_finaliser,
diff --git a/py/objproperty.c b/py/objproperty.c
index 2d3af10e8c7..155ffb16b58 100644
--- a/py/objproperty.c
+++ b/py/objproperty.c
@@ -36,7 +36,7 @@ typedef struct _mp_obj_property_t {
     mp_obj_t proxy[3]; // getter, setter, deleter
 } mp_obj_property_t;
 
-STATIC mp_obj_t property_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t property_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     enum { ARG_fget, ARG_fset, ARG_fdel, ARG_doc };
     static const mp_arg_t allowed_args[] = {
         { MP_QSTR_, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} },
@@ -55,40 +55,40 @@ STATIC mp_obj_t property_make_new(const mp_obj_type_t *type, size_t n_args, size
     return MP_OBJ_FROM_PTR(o);
 }
 
-STATIC mp_obj_t property_getter(mp_obj_t self_in, mp_obj_t getter) {
+static mp_obj_t property_getter(mp_obj_t self_in, mp_obj_t getter) {
     mp_obj_property_t *p2 = m_new_obj(mp_obj_property_t);
     *p2 = *(mp_obj_property_t *)MP_OBJ_TO_PTR(self_in);
     p2->proxy[0] = getter;
     return MP_OBJ_FROM_PTR(p2);
 }
 
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(property_getter_obj, property_getter);
+static MP_DEFINE_CONST_FUN_OBJ_2(property_getter_obj, property_getter);
 
-STATIC mp_obj_t property_setter(mp_obj_t self_in, mp_obj_t setter) {
+static mp_obj_t property_setter(mp_obj_t self_in, mp_obj_t setter) {
     mp_obj_property_t *p2 = m_new_obj(mp_obj_property_t);
     *p2 = *(mp_obj_property_t *)MP_OBJ_TO_PTR(self_in);
     p2->proxy[1] = setter;
     return MP_OBJ_FROM_PTR(p2);
 }
 
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(property_setter_obj, property_setter);
+static MP_DEFINE_CONST_FUN_OBJ_2(property_setter_obj, property_setter);
 
-STATIC mp_obj_t property_deleter(mp_obj_t self_in, mp_obj_t deleter) {
+static mp_obj_t property_deleter(mp_obj_t self_in, mp_obj_t deleter) {
     mp_obj_property_t *p2 = m_new_obj(mp_obj_property_t);
     *p2 = *(mp_obj_property_t *)MP_OBJ_TO_PTR(self_in);
     p2->proxy[2] = deleter;
     return MP_OBJ_FROM_PTR(p2);
 }
 
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(property_deleter_obj, property_deleter);
+static MP_DEFINE_CONST_FUN_OBJ_2(property_deleter_obj, property_deleter);
 
-STATIC const mp_rom_map_elem_t property_locals_dict_table[] = {
+static const mp_rom_map_elem_t property_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_getter), MP_ROM_PTR(&property_getter_obj) },
     { MP_ROM_QSTR(MP_QSTR_setter), MP_ROM_PTR(&property_setter_obj) },
     { MP_ROM_QSTR(MP_QSTR_deleter), MP_ROM_PTR(&property_deleter_obj) },
 };
 
-STATIC MP_DEFINE_CONST_DICT(property_locals_dict, property_locals_dict_table);
+static MP_DEFINE_CONST_DICT(property_locals_dict, property_locals_dict_table);
 
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_property,
diff --git a/py/objrange.c b/py/objrange.c
index f0fe56d9db1..e19d6c4dfcd 100644
--- a/py/objrange.c
+++ b/py/objrange.c
@@ -33,31 +33,30 @@
 
 typedef struct _mp_obj_range_it_t {
     mp_obj_base_t base;
-    // TODO make these values generic objects or something
     mp_int_t cur;
     mp_int_t stop;
     mp_int_t step;
 } mp_obj_range_it_t;
 
-STATIC mp_obj_t range_it_iternext(mp_obj_t o_in) {
+static mp_obj_t range_it_iternext(mp_obj_t o_in) {
     mp_obj_range_it_t *o = MP_OBJ_TO_PTR(o_in);
     if ((o->step > 0 && o->cur < o->stop) || (o->step < 0 && o->cur > o->stop)) {
-        mp_obj_t o_out = MP_OBJ_NEW_SMALL_INT(o->cur);
+        mp_int_t cur = o->cur;
         o->cur += o->step;
-        return o_out;
+        return mp_obj_new_int(cur);
     } else {
         return MP_OBJ_STOP_ITERATION;
     }
 }
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+static MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_range_it,
     MP_QSTR_iterator,
     MP_TYPE_FLAG_ITER_IS_ITERNEXT,
     iter, range_it_iternext
     );
 
-STATIC mp_obj_t mp_obj_new_range_iterator(mp_int_t cur, mp_int_t stop, mp_int_t step, mp_obj_iter_buf_t *iter_buf) {
+static mp_obj_t mp_obj_new_range_iterator(mp_int_t cur, mp_int_t stop, mp_int_t step, mp_obj_iter_buf_t *iter_buf) {
     assert(sizeof(mp_obj_range_it_t) <= sizeof(mp_obj_iter_buf_t));
     mp_obj_range_it_t *o = (mp_obj_range_it_t *)iter_buf;
     o->base.type = &mp_type_range_it;
@@ -72,13 +71,12 @@ STATIC mp_obj_t mp_obj_new_range_iterator(mp_int_t cur, mp_int_t stop, mp_int_t
 
 typedef struct _mp_obj_range_t {
     mp_obj_base_t base;
-    // TODO make these values generic objects or something
     mp_int_t start;
     mp_int_t stop;
     mp_int_t step;
 } mp_obj_range_t;
 
-STATIC void range_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void range_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_range_t *self = MP_OBJ_TO_PTR(self_in);
     mp_printf(print, "range(" INT_FMT ", " INT_FMT "", self->start, self->stop);
@@ -89,7 +87,7 @@ STATIC void range_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind
     }
 }
 
-STATIC mp_obj_t range_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t range_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     mp_arg_check_num(n_args, n_kw, 1, 3, false);
 
     mp_obj_range_t *o = mp_obj_malloc(mp_obj_range_t, type);
@@ -112,7 +110,7 @@ STATIC mp_obj_t range_make_new(const mp_obj_type_t *type, size_t n_args, size_t
     return MP_OBJ_FROM_PTR(o);
 }
 
-STATIC mp_int_t range_len(mp_obj_range_t *self) {
+static mp_int_t range_len(mp_obj_range_t *self) {
     // When computing length, need to take into account step!=1 and step<0.
     mp_int_t len = self->stop - self->start + self->step;
     if (self->step > 0) {
@@ -127,21 +125,21 @@ STATIC mp_int_t range_len(mp_obj_range_t *self) {
     return len;
 }
 
-STATIC mp_obj_t range_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
+static mp_obj_t range_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
     mp_obj_range_t *self = MP_OBJ_TO_PTR(self_in);
     mp_int_t len = range_len(self);
     switch (op) {
         case MP_UNARY_OP_BOOL:
             return mp_obj_new_bool(len > 0);
         case MP_UNARY_OP_LEN:
-            return MP_OBJ_NEW_SMALL_INT(len);
+            return mp_obj_new_int(len);
         default:
             return MP_OBJ_NULL;      // op not supported
     }
 }
 
 #if MICROPY_PY_BUILTINS_RANGE_BINOP
-STATIC mp_obj_t range_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
+static mp_obj_t range_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
     if (!mp_obj_is_type(rhs_in, &mp_type_range) || op != MP_BINARY_OP_EQUAL) {
         return MP_OBJ_NULL; // op not supported
     }
@@ -158,7 +156,7 @@ STATIC mp_obj_t range_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs
 }
 #endif
 
-STATIC mp_obj_t range_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
+static mp_obj_t range_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
     if (value == MP_OBJ_SENTINEL) {
         // load
         mp_obj_range_t *self = MP_OBJ_TO_PTR(self_in);
@@ -166,33 +164,29 @@ STATIC mp_obj_t range_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
         #if MICROPY_PY_BUILTINS_SLICE
         if (mp_obj_is_type(index, &mp_type_slice)) {
             mp_bound_slice_t slice;
-            mp_seq_get_fast_slice_indexes(len, index, &slice);
+            mp_obj_slice_indices(index, len, &slice);
             mp_obj_range_t *o = mp_obj_malloc(mp_obj_range_t, &mp_type_range);
             o->start = self->start + slice.start * self->step;
             o->stop = self->start + slice.stop * self->step;
             o->step = slice.step * self->step;
-            if (slice.step < 0) {
-                // Negative slice steps have inclusive stop, so adjust for exclusive
-                o->stop -= self->step;
-            }
             return MP_OBJ_FROM_PTR(o);
         }
         #endif
         size_t index_val = mp_get_index(self->base.type, len, index, false);
-        return MP_OBJ_NEW_SMALL_INT(self->start + index_val * self->step);
+        return mp_obj_new_int(self->start + index_val * self->step);
     } else {
         return MP_OBJ_NULL; // op not supported
     }
 }
 
-STATIC mp_obj_t range_getiter(mp_obj_t o_in, mp_obj_iter_buf_t *iter_buf) {
+static mp_obj_t range_getiter(mp_obj_t o_in, mp_obj_iter_buf_t *iter_buf) {
     mp_obj_range_t *o = MP_OBJ_TO_PTR(o_in);
     return mp_obj_new_range_iterator(o->start, o->stop, o->step, iter_buf);
 }
 
 
 #if MICROPY_PY_BUILTINS_RANGE_ATTRS
-STATIC void range_attr(mp_obj_t o_in, qstr attr, mp_obj_t *dest) {
+static void range_attr(mp_obj_t o_in, qstr attr, mp_obj_t *dest) {
     if (dest[0] != MP_OBJ_NULL) {
         // not load attribute
         return;
diff --git a/py/objreversed.c b/py/objreversed.c
index c66698f028a..c580ee286b7 100644
--- a/py/objreversed.c
+++ b/py/objreversed.c
@@ -37,7 +37,7 @@ typedef struct _mp_obj_reversed_t {
     mp_uint_t cur_index;    // current index, plus 1; 0=no more, 1=last one (index 0)
 } mp_obj_reversed_t;
 
-STATIC mp_obj_t reversed_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t reversed_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     mp_arg_check_num(n_args, n_kw, 1, 1, false);
 
     // check if __reversed__ exists, and if so delegate to it
@@ -54,7 +54,7 @@ STATIC mp_obj_t reversed_make_new(const mp_obj_type_t *type, size_t n_args, size
     return MP_OBJ_FROM_PTR(o);
 }
 
-STATIC mp_obj_t reversed_iternext(mp_obj_t self_in) {
+static mp_obj_t reversed_iternext(mp_obj_t self_in) {
     mp_check_self(mp_obj_is_type(self_in, &mp_type_reversed));
     mp_obj_reversed_t *self = MP_OBJ_TO_PTR(self_in);
 
diff --git a/py/objringio.c b/py/objringio.c
new file mode 100644
index 00000000000..0025b26be3b
--- /dev/null
+++ b/py/objringio.c
@@ -0,0 +1,127 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2024 Andrew Leech
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "ringbuf.h"
+#include "py/mpconfig.h"
+
+#if MICROPY_PY_MICROPYTHON_RINGIO
+
+#include "py/runtime.h"
+#include "py/stream.h"
+
+typedef struct _micropython_ringio_obj_t {
+    mp_obj_base_t base;
+    ringbuf_t ringbuffer;
+} micropython_ringio_obj_t;
+
+static mp_obj_t micropython_ringio_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+    mp_arg_check_num(n_args, n_kw, 1, 1, false);
+    mp_buffer_info_t bufinfo = {NULL, 0, 0};
+
+    if (!mp_get_buffer(args[0], &bufinfo, MP_BUFFER_RW)) {
+        bufinfo.len = mp_obj_get_int(args[0]) + 1;
+        bufinfo.buf = m_new(uint8_t, bufinfo.len);
+    }
+    if (bufinfo.len < 2 || bufinfo.len > UINT16_MAX) {
+        mp_raise_ValueError(NULL);
+    }
+    micropython_ringio_obj_t *self = mp_obj_malloc(micropython_ringio_obj_t, type);
+    self->ringbuffer.buf = bufinfo.buf;
+    self->ringbuffer.size = bufinfo.len;
+    self->ringbuffer.iget = self->ringbuffer.iput = 0;
+    return MP_OBJ_FROM_PTR(self);
+}
+
+static mp_uint_t micropython_ringio_read(mp_obj_t self_in, void *buf_in, mp_uint_t size, int *errcode) {
+    micropython_ringio_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    size = MIN(size, ringbuf_avail(&self->ringbuffer));
+    ringbuf_memcpy_get_internal(&(self->ringbuffer), buf_in, size);
+    *errcode = 0;
+    return size;
+}
+
+static mp_uint_t micropython_ringio_write(mp_obj_t self_in, const void *buf_in, mp_uint_t size, int *errcode) {
+    micropython_ringio_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    size = MIN(size, ringbuf_free(&self->ringbuffer));
+    ringbuf_memcpy_put_internal(&(self->ringbuffer), buf_in, size);
+    *errcode = 0;
+    return size;
+}
+
+static mp_uint_t micropython_ringio_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) {
+    micropython_ringio_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    switch (request) {
+        case MP_STREAM_POLL: {
+            mp_uint_t ret = 0;
+            if ((arg & MP_STREAM_POLL_RD) && ringbuf_avail(&self->ringbuffer) > 0) {
+                ret |= MP_STREAM_POLL_RD;
+            }
+            if ((arg & MP_STREAM_POLL_WR) && ringbuf_free(&self->ringbuffer) > 0) {
+                ret |= MP_STREAM_POLL_WR;
+            }
+            return ret;
+        }
+        case MP_STREAM_CLOSE:
+            return 0;
+    }
+    *errcode = MP_EINVAL;
+    return MP_STREAM_ERROR;
+}
+
+static mp_obj_t micropython_ringio_any(mp_obj_t self_in) {
+    micropython_ringio_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    return MP_OBJ_NEW_SMALL_INT(ringbuf_avail(&self->ringbuffer));
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(micropython_ringio_any_obj, micropython_ringio_any);
+
+static const mp_rom_map_elem_t micropython_ringio_locals_dict_table[] = {
+    { MP_ROM_QSTR(MP_QSTR_any), MP_ROM_PTR(µpython_ringio_any_obj) },
+    { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mp_stream_read_obj) },
+    { MP_ROM_QSTR(MP_QSTR_readline), MP_ROM_PTR(&mp_stream_unbuffered_readline_obj) },
+    { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&mp_stream_readinto_obj) },
+    { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&mp_stream_write_obj) },
+    { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&mp_stream_close_obj) },
+
+};
+static MP_DEFINE_CONST_DICT(micropython_ringio_locals_dict, micropython_ringio_locals_dict_table);
+
+static const mp_stream_p_t ringio_stream_p = {
+    .read = micropython_ringio_read,
+    .write = micropython_ringio_write,
+    .ioctl = micropython_ringio_ioctl,
+    .is_text = false,
+};
+
+MP_DEFINE_CONST_OBJ_TYPE(
+    mp_type_ringio,
+    MP_QSTR_RingIO,
+    MP_TYPE_FLAG_NONE,
+    make_new, micropython_ringio_make_new,
+    protocol, &ringio_stream_p,
+    locals_dict, µpython_ringio_locals_dict
+    );
+
+#endif // MICROPY_PY_MICROPYTHON_RINGIO
diff --git a/py/objset.c b/py/objset.c
index 906807889ab..fbabc9d33e5 100644
--- a/py/objset.c
+++ b/py/objset.c
@@ -45,7 +45,7 @@ typedef struct _mp_obj_set_it_t {
     size_t cur;
 } mp_obj_set_it_t;
 
-STATIC bool is_set_or_frozenset(mp_obj_t o) {
+static bool is_set_or_frozenset(mp_obj_t o) {
     return mp_obj_is_type(o, &mp_type_set)
            #if MICROPY_PY_BUILTINS_FROZENSET
            || mp_obj_is_type(o, &mp_type_frozenset)
@@ -60,7 +60,7 @@ STATIC bool is_set_or_frozenset(mp_obj_t o) {
 // set or frozenset for methods that operate on both of these types.
 #define check_set_or_frozenset(o) mp_check_self(is_set_or_frozenset(o))
 
-STATIC void set_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void set_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_set_t *self = MP_OBJ_TO_PTR(self_in);
     #if MICROPY_PY_BUILTINS_FROZENSET
@@ -99,7 +99,7 @@ STATIC void set_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t
     #endif
 }
 
-STATIC mp_obj_t set_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t set_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     mp_arg_check_num(n_args, n_kw, 0, 1, false);
 
     switch (n_args) {
@@ -127,7 +127,7 @@ STATIC mp_obj_t set_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_
     }
 }
 
-STATIC mp_obj_t set_it_iternext(mp_obj_t self_in) {
+static mp_obj_t set_it_iternext(mp_obj_t self_in) {
     mp_obj_set_it_t *self = MP_OBJ_TO_PTR(self_in);
     size_t max = self->set->set.alloc;
     mp_set_t *set = &self->set->set;
@@ -142,7 +142,7 @@ STATIC mp_obj_t set_it_iternext(mp_obj_t self_in) {
     return MP_OBJ_STOP_ITERATION;
 }
 
-STATIC mp_obj_t set_getiter(mp_obj_t set_in, mp_obj_iter_buf_t *iter_buf) {
+static mp_obj_t set_getiter(mp_obj_t set_in, mp_obj_iter_buf_t *iter_buf) {
     assert(sizeof(mp_obj_set_it_t) <= sizeof(mp_obj_iter_buf_t));
     mp_obj_set_it_t *o = (mp_obj_set_it_t *)iter_buf;
     o->base.type = &mp_type_polymorph_iter;
@@ -155,23 +155,23 @@ STATIC mp_obj_t set_getiter(mp_obj_t set_in, mp_obj_iter_buf_t *iter_buf) {
 /******************************************************************************/
 /* set methods                                                                */
 
-STATIC mp_obj_t set_add(mp_obj_t self_in, mp_obj_t item) {
+static mp_obj_t set_add(mp_obj_t self_in, mp_obj_t item) {
     check_set(self_in);
     mp_obj_set_t *self = MP_OBJ_TO_PTR(self_in);
     mp_set_lookup(&self->set, item, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND);
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(set_add_obj, set_add);
+static MP_DEFINE_CONST_FUN_OBJ_2(set_add_obj, set_add);
 
-STATIC mp_obj_t set_clear(mp_obj_t self_in) {
+static mp_obj_t set_clear(mp_obj_t self_in) {
     check_set(self_in);
     mp_obj_set_t *self = MP_OBJ_TO_PTR(self_in);
     mp_set_clear(&self->set);
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(set_clear_obj, set_clear);
+static MP_DEFINE_CONST_FUN_OBJ_1(set_clear_obj, set_clear);
 
-STATIC mp_obj_t set_copy(mp_obj_t self_in) {
+static mp_obj_t set_copy(mp_obj_t self_in) {
     check_set_or_frozenset(self_in);
     mp_obj_set_t *self = MP_OBJ_TO_PTR(self_in);
     mp_obj_set_t *other = mp_obj_malloc(mp_obj_set_t, self->base.type);
@@ -180,17 +180,17 @@ STATIC mp_obj_t set_copy(mp_obj_t self_in) {
     memcpy(other->set.table, self->set.table, self->set.alloc * sizeof(mp_obj_t));
     return MP_OBJ_FROM_PTR(other);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(set_copy_obj, set_copy);
+static MP_DEFINE_CONST_FUN_OBJ_1(set_copy_obj, set_copy);
 
-STATIC mp_obj_t set_discard(mp_obj_t self_in, mp_obj_t item) {
+static mp_obj_t set_discard(mp_obj_t self_in, mp_obj_t item) {
     check_set(self_in);
     mp_obj_set_t *self = MP_OBJ_TO_PTR(self_in);
     mp_set_lookup(&self->set, item, MP_MAP_LOOKUP_REMOVE_IF_FOUND);
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(set_discard_obj, set_discard);
+static MP_DEFINE_CONST_FUN_OBJ_2(set_discard_obj, set_discard);
 
-STATIC mp_obj_t set_diff_int(size_t n_args, const mp_obj_t *args, bool update) {
+static mp_obj_t set_diff_int(size_t n_args, const mp_obj_t *args, bool update) {
     mp_obj_t self;
     if (update) {
         check_set(args[0]);
@@ -216,60 +216,70 @@ STATIC mp_obj_t set_diff_int(size_t n_args, const mp_obj_t *args, bool update) {
     return self;
 }
 
-STATIC mp_obj_t set_diff(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t set_diff(size_t n_args, const mp_obj_t *args) {
     return set_diff_int(n_args, args, false);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR(set_diff_obj, 1, set_diff);
+static MP_DEFINE_CONST_FUN_OBJ_VAR(set_diff_obj, 1, set_diff);
 
-STATIC mp_obj_t set_diff_update(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t set_diff_update(size_t n_args, const mp_obj_t *args) {
     set_diff_int(n_args, args, true);
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR(set_diff_update_obj, 1, set_diff_update);
+static MP_DEFINE_CONST_FUN_OBJ_VAR(set_diff_update_obj, 1, set_diff_update);
 
-STATIC mp_obj_t set_intersect_int(mp_obj_t self_in, mp_obj_t other, bool update) {
-    if (update) {
-        check_set(self_in);
-    } else {
-        check_set_or_frozenset(self_in);
-    }
-
-    if (self_in == other) {
-        return update ? mp_const_none : set_copy(self_in);
-    }
-
-    mp_obj_set_t *self = MP_OBJ_TO_PTR(self_in);
-    mp_obj_set_t *out = MP_OBJ_TO_PTR(mp_obj_new_set(0, NULL));
-
-    mp_obj_t iter = mp_getiter(other, NULL);
+static mp_obj_set_t *set_intersect_one(mp_set_t *src, mp_obj_t iterable) {
+    mp_obj_set_t *out_ptr = MP_OBJ_TO_PTR(mp_obj_new_set(0, NULL));
+    mp_obj_t iter = mp_getiter(iterable, NULL);
     mp_obj_t next;
     while ((next = mp_iternext(iter)) != MP_OBJ_STOP_ITERATION) {
-        if (mp_set_lookup(&self->set, next, MP_MAP_LOOKUP)) {
-            set_add(MP_OBJ_FROM_PTR(out), next);
+        if (mp_set_lookup(src, next, MP_MAP_LOOKUP)) {
+            set_add(MP_OBJ_FROM_PTR(out_ptr), next);
         }
     }
+    return out_ptr;
+}
+
+static mp_obj_t set_intersect_int(size_t n_args, const mp_obj_t *args, bool update) {
+    mp_obj_set_t *in_ptr;
+    size_t start = 1;
 
     if (update) {
-        m_del(mp_obj_t, self->set.table, self->set.alloc);
-        self->set.alloc = out->set.alloc;
-        self->set.used = out->set.used;
-        self->set.table = out->set.table;
+        check_set(args[0]);
+        in_ptr = MP_OBJ_TO_PTR(args[0]);
+    } else {
+        check_set_or_frozenset(args[0]);
+        if (n_args == 1) {
+            return set_copy(args[0]);
+        }
+        mp_obj_set_t *src = MP_OBJ_TO_PTR(args[0]);
+        in_ptr = set_intersect_one(&src->set, args[1]);
+        start = 2;
+    }
+
+    for (size_t i = start; i < n_args; i++) {
+        if (MP_OBJ_FROM_PTR(in_ptr) != args[i]) {
+            mp_obj_set_t *out_ptr = set_intersect_one(&in_ptr->set, args[i]);
+            m_del(mp_obj_t, in_ptr->set.table, in_ptr->set.alloc);
+            in_ptr->set.alloc = out_ptr->set.alloc;
+            in_ptr->set.used = out_ptr->set.used;
+            in_ptr->set.table = out_ptr->set.table;
+        }
     }
 
-    return update ? mp_const_none : MP_OBJ_FROM_PTR(out);
+    return update ? mp_const_none : MP_OBJ_FROM_PTR(in_ptr);
 }
 
-STATIC mp_obj_t set_intersect(mp_obj_t self_in, mp_obj_t other) {
-    return set_intersect_int(self_in, other, false);
+static mp_obj_t set_intersect(size_t n_args, const mp_obj_t *args) {
+    return set_intersect_int(n_args, args, false);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(set_intersect_obj, set_intersect);
+static MP_DEFINE_CONST_FUN_OBJ_VAR(set_intersect_obj, 1, set_intersect);
 
-STATIC mp_obj_t set_intersect_update(mp_obj_t self_in, mp_obj_t other) {
-    return set_intersect_int(self_in, other, true);
+static mp_obj_t set_intersect_update(size_t n_args, const mp_obj_t *args) {
+    return set_intersect_int(n_args, args, true);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(set_intersect_update_obj, set_intersect_update);
+static MP_DEFINE_CONST_FUN_OBJ_VAR(set_intersect_update_obj, 1, set_intersect_update);
 
-STATIC mp_obj_t set_isdisjoint(mp_obj_t self_in, mp_obj_t other) {
+static mp_obj_t set_isdisjoint(mp_obj_t self_in, mp_obj_t other) {
     check_set_or_frozenset(self_in);
     mp_obj_set_t *self = MP_OBJ_TO_PTR(self_in);
 
@@ -283,9 +293,9 @@ STATIC mp_obj_t set_isdisjoint(mp_obj_t self_in, mp_obj_t other) {
     }
     return mp_const_true;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(set_isdisjoint_obj, set_isdisjoint);
+static MP_DEFINE_CONST_FUN_OBJ_2(set_isdisjoint_obj, set_isdisjoint);
 
-STATIC mp_obj_t set_issubset_internal(mp_obj_t self_in, mp_obj_t other_in, bool proper) {
+static mp_obj_t set_issubset_internal(mp_obj_t self_in, mp_obj_t other_in, bool proper) {
     mp_obj_set_t *self;
     bool cleanup_self = false;
     if (is_set_or_frozenset(self_in)) {
@@ -327,25 +337,25 @@ STATIC mp_obj_t set_issubset_internal(mp_obj_t self_in, mp_obj_t other_in, bool
     return out;
 }
 
-STATIC mp_obj_t set_issubset(mp_obj_t self_in, mp_obj_t other_in) {
+static mp_obj_t set_issubset(mp_obj_t self_in, mp_obj_t other_in) {
     return set_issubset_internal(self_in, other_in, false);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(set_issubset_obj, set_issubset);
+static MP_DEFINE_CONST_FUN_OBJ_2(set_issubset_obj, set_issubset);
 
-STATIC mp_obj_t set_issubset_proper(mp_obj_t self_in, mp_obj_t other_in) {
+static mp_obj_t set_issubset_proper(mp_obj_t self_in, mp_obj_t other_in) {
     return set_issubset_internal(self_in, other_in, true);
 }
 
-STATIC mp_obj_t set_issuperset(mp_obj_t self_in, mp_obj_t other_in) {
+static mp_obj_t set_issuperset(mp_obj_t self_in, mp_obj_t other_in) {
     return set_issubset_internal(other_in, self_in, false);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(set_issuperset_obj, set_issuperset);
+static MP_DEFINE_CONST_FUN_OBJ_2(set_issuperset_obj, set_issuperset);
 
-STATIC mp_obj_t set_issuperset_proper(mp_obj_t self_in, mp_obj_t other_in) {
+static mp_obj_t set_issuperset_proper(mp_obj_t self_in, mp_obj_t other_in) {
     return set_issubset_internal(other_in, self_in, true);
 }
 
-STATIC mp_obj_t set_equal(mp_obj_t self_in, mp_obj_t other_in) {
+static mp_obj_t set_equal(mp_obj_t self_in, mp_obj_t other_in) {
     assert(is_set_or_frozenset(other_in));
     check_set_or_frozenset(self_in);
     mp_obj_set_t *self = MP_OBJ_TO_PTR(self_in);
@@ -356,7 +366,7 @@ STATIC mp_obj_t set_equal(mp_obj_t self_in, mp_obj_t other_in) {
     return set_issubset(self_in, other_in);
 }
 
-STATIC mp_obj_t set_pop(mp_obj_t self_in) {
+static mp_obj_t set_pop(mp_obj_t self_in) {
     check_set(self_in);
     mp_obj_set_t *self = MP_OBJ_TO_PTR(self_in);
     mp_obj_t obj = mp_set_remove_first(&self->set);
@@ -365,9 +375,9 @@ STATIC mp_obj_t set_pop(mp_obj_t self_in) {
     }
     return obj;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(set_pop_obj, set_pop);
+static MP_DEFINE_CONST_FUN_OBJ_1(set_pop_obj, set_pop);
 
-STATIC mp_obj_t set_remove(mp_obj_t self_in, mp_obj_t item) {
+static mp_obj_t set_remove(mp_obj_t self_in, mp_obj_t item) {
     check_set(self_in);
     mp_obj_set_t *self = MP_OBJ_TO_PTR(self_in);
     if (mp_set_lookup(&self->set, item, MP_MAP_LOOKUP_REMOVE_IF_FOUND) == MP_OBJ_NULL) {
@@ -375,9 +385,9 @@ STATIC mp_obj_t set_remove(mp_obj_t self_in, mp_obj_t item) {
     }
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(set_remove_obj, set_remove);
+static MP_DEFINE_CONST_FUN_OBJ_2(set_remove_obj, set_remove);
 
-STATIC mp_obj_t set_symmetric_difference_update(mp_obj_t self_in, mp_obj_t other_in) {
+static mp_obj_t set_symmetric_difference_update(mp_obj_t self_in, mp_obj_t other_in) {
     check_set_or_frozenset(self_in); // can be frozenset due to call from set_symmetric_difference
     mp_obj_set_t *self = MP_OBJ_TO_PTR(self_in);
     mp_obj_t iter = mp_getiter(other_in, NULL);
@@ -387,16 +397,16 @@ STATIC mp_obj_t set_symmetric_difference_update(mp_obj_t self_in, mp_obj_t other
     }
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(set_symmetric_difference_update_obj, set_symmetric_difference_update);
+static MP_DEFINE_CONST_FUN_OBJ_2(set_symmetric_difference_update_obj, set_symmetric_difference_update);
 
-STATIC mp_obj_t set_symmetric_difference(mp_obj_t self_in, mp_obj_t other_in) {
+static mp_obj_t set_symmetric_difference(mp_obj_t self_in, mp_obj_t other_in) {
     mp_obj_t self_out = set_copy(self_in);
     set_symmetric_difference_update(self_out, other_in);
     return self_out;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(set_symmetric_difference_obj, set_symmetric_difference);
+static MP_DEFINE_CONST_FUN_OBJ_2(set_symmetric_difference_obj, set_symmetric_difference);
 
-STATIC void set_update_int(mp_obj_set_t *self, mp_obj_t other_in) {
+static void set_update_int(mp_obj_set_t *self, mp_obj_t other_in) {
     mp_obj_t iter = mp_getiter(other_in, NULL);
     mp_obj_t next;
     while ((next = mp_iternext(iter)) != MP_OBJ_STOP_ITERATION) {
@@ -404,7 +414,7 @@ STATIC void set_update_int(mp_obj_set_t *self, mp_obj_t other_in) {
     }
 }
 
-STATIC mp_obj_t set_update(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t set_update(size_t n_args, const mp_obj_t *args) {
     check_set(args[0]);
     for (size_t i = 1; i < n_args; i++) {
         set_update_int(MP_OBJ_TO_PTR(args[0]), args[i]);
@@ -412,17 +422,17 @@ STATIC mp_obj_t set_update(size_t n_args, const mp_obj_t *args) {
 
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR(set_update_obj, 1, set_update);
+static MP_DEFINE_CONST_FUN_OBJ_VAR(set_update_obj, 1, set_update);
 
-STATIC mp_obj_t set_union(mp_obj_t self_in, mp_obj_t other_in) {
+static mp_obj_t set_union(mp_obj_t self_in, mp_obj_t other_in) {
     check_set_or_frozenset(self_in);
     mp_obj_t self = set_copy(self_in);
     set_update_int(MP_OBJ_TO_PTR(self), other_in);
     return self;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(set_union_obj, set_union);
+static MP_DEFINE_CONST_FUN_OBJ_2(set_union_obj, set_union);
 
-STATIC mp_obj_t set_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
+static mp_obj_t set_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
     mp_obj_set_t *self = MP_OBJ_TO_PTR(self_in);
     switch (op) {
         case MP_UNARY_OP_BOOL:
@@ -451,7 +461,7 @@ STATIC mp_obj_t set_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
     }
 }
 
-STATIC mp_obj_t set_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs) {
+static mp_obj_t set_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs) {
     mp_obj_t args[] = {lhs, rhs};
     #if MICROPY_PY_BUILTINS_FROZENSET
     bool update = mp_obj_is_type(lhs, &mp_type_set);
@@ -468,7 +478,7 @@ STATIC mp_obj_t set_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs) {
         case MP_BINARY_OP_XOR:
             return set_symmetric_difference(lhs, rhs);
         case MP_BINARY_OP_AND:
-            return set_intersect(lhs, rhs);
+            return set_intersect_int(2, args, false);
         case MP_BINARY_OP_SUBTRACT:
             return set_diff(2, args);
         case MP_BINARY_OP_INPLACE_OR:
@@ -486,7 +496,7 @@ STATIC mp_obj_t set_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs) {
                 return set_symmetric_difference(lhs, rhs);
             }
         case MP_BINARY_OP_INPLACE_AND:
-            rhs = set_intersect_int(lhs, rhs, update);
+            rhs = set_intersect_int(2, args, update);
             if (update) {
                 return lhs;
             } else {
@@ -517,7 +527,7 @@ STATIC mp_obj_t set_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs) {
 /******************************************************************************/
 /* set constructors & public C API                                            */
 
-STATIC const mp_rom_map_elem_t set_locals_dict_table[] = {
+static const mp_rom_map_elem_t set_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_add), MP_ROM_PTR(&set_add_obj) },
     { MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&set_clear_obj) },
     { MP_ROM_QSTR(MP_QSTR_copy), MP_ROM_PTR(&set_copy_obj) },
@@ -537,7 +547,7 @@ STATIC const mp_rom_map_elem_t set_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_update), MP_ROM_PTR(&set_update_obj) },
     { MP_ROM_QSTR(MP_QSTR___contains__), MP_ROM_PTR(&mp_op_contains_obj) },
 };
-STATIC MP_DEFINE_CONST_DICT(set_locals_dict, set_locals_dict_table);
+static MP_DEFINE_CONST_DICT(set_locals_dict, set_locals_dict_table);
 
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_set,
@@ -552,7 +562,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
     );
 
 #if MICROPY_PY_BUILTINS_FROZENSET
-STATIC const mp_rom_map_elem_t frozenset_locals_dict_table[] = {
+static const mp_rom_map_elem_t frozenset_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_copy), MP_ROM_PTR(&set_copy_obj) },
     { MP_ROM_QSTR(MP_QSTR_difference), MP_ROM_PTR(&set_diff_obj) },
     { MP_ROM_QSTR(MP_QSTR_intersection), MP_ROM_PTR(&set_intersect_obj) },
@@ -563,7 +573,7 @@ STATIC const mp_rom_map_elem_t frozenset_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_union), MP_ROM_PTR(&set_union_obj) },
     { MP_ROM_QSTR(MP_QSTR___contains__), MP_ROM_PTR(&mp_op_contains_obj) },
 };
-STATIC MP_DEFINE_CONST_DICT(frozenset_locals_dict, frozenset_locals_dict_table);
+static MP_DEFINE_CONST_DICT(frozenset_locals_dict, frozenset_locals_dict_table);
 
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_frozenset,
diff --git a/py/objsingleton.c b/py/objsingleton.c
index 6537676c523..c1f102c94c0 100644
--- a/py/objsingleton.c
+++ b/py/objsingleton.c
@@ -37,7 +37,7 @@ typedef struct _mp_obj_singleton_t {
     qstr name;
 } mp_obj_singleton_t;
 
-STATIC void singleton_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void singleton_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_singleton_t *self = MP_OBJ_TO_PTR(self_in);
     mp_printf(print, "%q", self->name);
diff --git a/py/objslice.c b/py/objslice.c
index 75fa3bc3f55..3e7575522b2 100644
--- a/py/objslice.c
+++ b/py/objslice.c
@@ -34,7 +34,7 @@
 
 #if MICROPY_PY_BUILTINS_SLICE
 
-STATIC void slice_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
+static void slice_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_slice_t *o = MP_OBJ_TO_PTR(o_in);
     mp_print_str(print, "slice(");
@@ -46,15 +46,15 @@ STATIC void slice_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t
     mp_print_str(print, ")");
 }
 
-STATIC mp_obj_t slice_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
+static mp_obj_t slice_unary_op(mp_unary_op_t op, mp_obj_t o_in) {
     // Needed to explicitly opt out of default __hash__.
     // REVISIT: CPython implements comparison operators for slice.
     return MP_OBJ_NULL;
 }
 
 #if MICROPY_PY_BUILTINS_SLICE_INDICES
-STATIC mp_obj_t slice_indices(mp_obj_t self_in, mp_obj_t length_obj) {
-    mp_int_t length = mp_obj_int_get_checked(length_obj);
+static mp_obj_t slice_indices(mp_obj_t self_in, mp_obj_t length_obj) {
+    mp_int_t length = mp_obj_get_int(length_obj);
     mp_bound_slice_t bound_indices;
     mp_obj_slice_indices(self_in, length, &bound_indices);
 
@@ -65,11 +65,11 @@ STATIC mp_obj_t slice_indices(mp_obj_t self_in, mp_obj_t length_obj) {
     };
     return mp_obj_new_tuple(3, results);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(slice_indices_obj, slice_indices);
+static MP_DEFINE_CONST_FUN_OBJ_2(slice_indices_obj, slice_indices);
 #endif
 
 #if MICROPY_PY_BUILTINS_SLICE_ATTRS
-STATIC void slice_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+static void slice_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     if (dest[0] != MP_OBJ_NULL) {
         // not load attribute
         return;
@@ -92,10 +92,10 @@ STATIC void slice_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
 #endif
 
 #if MICROPY_PY_BUILTINS_SLICE_INDICES && !MICROPY_PY_BUILTINS_SLICE_ATTRS
-STATIC const mp_rom_map_elem_t slice_locals_dict_table[] = {
+static const mp_rom_map_elem_t slice_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_indices), MP_ROM_PTR(&slice_indices_obj) },
 };
-STATIC MP_DEFINE_CONST_DICT(slice_locals_dict, slice_locals_dict_table);
+static MP_DEFINE_CONST_DICT(slice_locals_dict, slice_locals_dict_table);
 #endif
 
 #if MICROPY_PY_BUILTINS_SLICE_ATTRS
diff --git a/py/objstr.c b/py/objstr.c
index e6c5ee71cf7..b2516faec5e 100644
--- a/py/objstr.c
+++ b/py/objstr.c
@@ -32,18 +32,19 @@
 #include "py/objstr.h"
 #include "py/objlist.h"
 #include "py/runtime.h"
-#include "py/stackctrl.h"
+#include "py/cstack.h"
+#include "py/objtuple.h"
 
 #if MICROPY_PY_BUILTINS_STR_OP_MODULO
-STATIC mp_obj_t str_modulo_format(mp_obj_t pattern, size_t n_args, const mp_obj_t *args, mp_obj_t dict);
+static mp_obj_t str_modulo_format(mp_obj_t pattern, size_t n_args, const mp_obj_t *args, mp_obj_t dict);
 #endif
 
-STATIC mp_obj_t mp_obj_new_bytes_iterator(mp_obj_t str, mp_obj_iter_buf_t *iter_buf);
-STATIC NORETURN void bad_implicit_conversion(mp_obj_t self_in);
+static mp_obj_t mp_obj_new_bytes_iterator(mp_obj_t str, mp_obj_iter_buf_t *iter_buf);
+static MP_NORETURN void bad_implicit_conversion(mp_obj_t self_in);
 
-STATIC mp_obj_t mp_obj_new_str_type_from_vstr(const mp_obj_type_t *type, vstr_t *vstr);
+static mp_obj_t mp_obj_new_str_type_from_vstr(const mp_obj_type_t *type, vstr_t *vstr);
 
-STATIC void str_check_arg_type(const mp_obj_type_t *self_type, const mp_obj_t arg) {
+static void str_check_arg_type(const mp_obj_type_t *self_type, const mp_obj_t arg) {
     // String operations generally need the args type to match the object they're called on,
     // e.g. str.find(str), byte.startswith(byte)
     // with the exception that bytes may be used for bytearray and vice versa.
@@ -63,10 +64,38 @@ STATIC void str_check_arg_type(const mp_obj_type_t *self_type, const mp_obj_t ar
     }
 }
 
-STATIC void check_is_str_or_bytes(mp_obj_t self_in) {
+static void check_is_str_or_bytes(mp_obj_t self_in) {
     mp_check_self(mp_obj_is_str_or_bytes(self_in));
 }
 
+static mp_obj_t make_empty_str_of_type(const mp_obj_type_t *type) {
+    if (type == &mp_type_str) {
+        return MP_OBJ_NEW_QSTR(MP_QSTR_); // empty str
+    } else {
+        return mp_const_empty_bytes;
+    }
+}
+
+static const byte *get_substring_data(const mp_obj_t obj, size_t n_args, const mp_obj_t *args, size_t *len) {
+    // Get substring data from obj, using args[0,1] to specify start and end indices.
+    GET_STR_DATA_LEN(obj, str, str_len);
+    if (n_args > 0) {
+        const mp_obj_type_t *self_type = mp_obj_get_type(obj);
+        const byte *end = str + str_len;
+        if (n_args > 1 && args[1] != mp_const_none) {
+            end = str_index_to_ptr(self_type, str, str_len, args[1], true);
+        }
+        if (args[0] != mp_const_none) {
+            str = str_index_to_ptr(self_type, str, str_len, args[0], true);
+        }
+        str_len = MAX(end - str, 0);
+    }
+    if (len) {
+        *len = str_len;
+    }
+    return str;
+}
+
 /******************************************************************************/
 /* str                                                                        */
 
@@ -109,7 +138,7 @@ void mp_str_print_quoted(const mp_print_t *print, const byte *str_data, size_t s
     mp_printf(print, "%c", quote_char);
 }
 
-#if MICROPY_PY_UJSON
+#if MICROPY_PY_JSON
 void mp_str_print_json(const mp_print_t *print, const byte *str_data, size_t str_len) {
     // for JSON spec, see http://www.ietf.org/rfc/rfc4627.txt
     // if we are given a valid utf8-encoded string, we will print it in a JSON-conforming way
@@ -135,9 +164,9 @@ void mp_str_print_json(const mp_print_t *print, const byte *str_data, size_t str
 }
 #endif
 
-STATIC void str_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void str_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     GET_STR_DATA_LEN(self_in, str_data, str_len);
-    #if MICROPY_PY_UJSON
+    #if MICROPY_PY_JSON
     if (kind == PRINT_JSON) {
         mp_str_print_json(print, str_data, str_len);
         return;
@@ -158,6 +187,79 @@ STATIC void str_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t
     }
 }
 
+#if MICROPY_PY_BUILTINS_STR_UNICODE_CHECK && MICROPY_PY_BUILTINS_BYTES_DECODE_ERRORS
+// Build a new string from data containing invalid UTF-8 or ASCII, either skipping the
+// invalid bytes (errors=="ignore") or replacing them with U+FFFD
+// (errors=="replace").
+static mp_obj_t str_from_invalid(mp_encoding_t encoding, const mp_obj_type_t *type, const byte *str_data, size_t str_len, qstr errors) {
+    bool do_replace = (errors == MP_QSTR_replace);
+    bool is_utf8 = (encoding == MP_ENCODING_UTF8);
+    vstr_t vstr;
+    vstr_init(&vstr, str_len);
+    const byte *p = str_data;
+    const byte *end = str_data + str_len;
+
+    while (p < end) {
+        byte c = *p;
+        if (c < 0x80) {
+            // Valid ASCII
+            vstr_add_byte(&vstr, c);
+            p++;
+        } else if (is_utf8 && c >= 0xc0 && c < 0xf8) {
+            // Potential multi-byte sequence
+            uint8_t need = (0xe5 >> ((c >> 3) & 0x6)) & 3;
+            const byte *seq_start = p;
+            p++;
+
+            // Check continuation bytes
+            uint8_t got = 0;
+            while (got < need && p < end && UTF8_IS_CONT(*p)) {
+                got++;
+                p++;
+            }
+
+            if (got == need) {
+                // Valid complete sequence, decode and add the character
+                unichar ch = *seq_start & (0x7f >> need);
+                for (uint8_t i = 0; i < need; i++) {
+                    ch = (ch << 6) | (seq_start[i + 1] & 0x3f);
+                }
+                vstr_add_char(&vstr, ch);
+            } else if (do_replace) {
+                // Invalid or incomplete sequence - replace with U+FFFD
+                vstr_add_char(&vstr, 0xFFFD);
+            }
+            // For 'ignore' mode, do nothing (skip invalid bytes)
+        } else if (do_replace) {
+            // Invalid start byte or non-ascii char - replace with U+FFFD
+            vstr_add_char(&vstr, 0xFFFD);
+            p++;
+        } else {
+            // Invalid start byte or non-ascii char - skip for 'ignore' mode
+            p++;
+        }
+    }
+
+    return mp_obj_new_str_type_from_vstr(type, &vstr);
+}
+#endif // MICROPY_PY_BUILTINS_STR_UNICODE_CHECK && MICROPY_PY_BUILTINS_BYTES_DECODE_ERRORS
+
+#if MICROPY_PY_BUILTINS_STR_UNICODE_CHECK
+static mp_encoding_t parse_encoding_arg(qstr encoding) {
+    if (encoding == MP_QSTR_utf_hyphen_8 || encoding == MP_QSTR_utf8) {
+        return MP_ENCODING_UTF8;
+    }
+    if (encoding == MP_QSTR_ascii) {
+        return MP_ENCODING_ASCII;
+    }
+    #if MICROPY_ERROR_REPORTING <= MICROPY_ERROR_REPORTING_TERSE
+    mp_raise_type(&mp_type_LookupError);
+    #else
+    mp_raise_msg_varg(&mp_type_LookupError, MP_ERROR_TEXT("unknown encoding: %q"), encoding);
+    #endif
+}
+#endif
+
 mp_obj_t mp_obj_str_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     #if MICROPY_CPYTHON_COMPAT
     if (n_kw != 0) {
@@ -179,40 +281,65 @@ mp_obj_t mp_obj_str_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_
             return mp_obj_new_str_type_from_vstr(type, &vstr);
         }
 
-        default: // 2 or 3 args
-            // TODO: validate 2nd/3rd args
+        default: { // 2 or 3 args
+            // Extract the source data.
+            const byte *str_data;
+            size_t str_len;
             if (mp_obj_is_type(args[0], &mp_type_bytes)) {
-                GET_STR_DATA_LEN(args[0], str_data, str_len);
-                GET_STR_HASH(args[0], str_hash);
-                if (str_hash == 0) {
-                    str_hash = qstr_compute_hash(str_data, str_len);
+                // Immutable bytes can be referenced directly (zero-copy);
+                GET_STR_DATA_LEN(args[0], bytes_data, bytes_len);
+                str_data = bytes_data;
+                str_len = bytes_len;
+            } else {
+                // any other buffer object is mutable sob its data must be copied.
+                mp_buffer_info_t bufinfo;
+                mp_get_buffer_raise(args[0], &bufinfo, MP_BUFFER_READ);
+                str_data = bufinfo.buf;
+                str_len = bufinfo.len;
+            }
+
+            #if MICROPY_PY_BUILTINS_STR_UNICODE_CHECK
+            mp_encoding_t encoding = parse_encoding_arg(mp_obj_str_get_qstr(args[1]));
+            if (!unicode_encoding_check(encoding, str_data, str_len)) {
+                #if MICROPY_PY_BUILTINS_BYTES_DECODE_ERRORS
+                // Check if error handler is specified (3rd argument)
+                qstr errors = MP_QSTR_; // default to ""
+                if (n_args >= 3 && args[2] != mp_const_none) {
+                    errors = mp_obj_str_get_qstr(args[2]);
                 }
-                #if MICROPY_PY_BUILTINS_STR_UNICODE_CHECK
-                if (!utf8_check(str_data, str_len)) {
-                    mp_raise_msg(&mp_type_UnicodeError, NULL);
+                if (errors == MP_QSTR_ignore || errors == MP_QSTR_replace) {
+                    return str_from_invalid(encoding, type, str_data, str_len, errors);
                 }
-                #endif
+                #endif // MICROPY_PY_BUILTINS_BYTES_DECODE_ERRORS
+                mp_raise_msg(&mp_type_UnicodeError, NULL);
+            }
+            #endif // MICROPY_PY_BUILTINS_STR_UNICODE_CHECK
 
-                // Check if a qstr with this data already exists
-                qstr q = qstr_find_strn((const char *)str_data, str_len);
-                if (q != MP_QSTRnull) {
-                    return MP_OBJ_NEW_QSTR(q);
-                }
+            // Check if a qstr with this data already exists
+            qstr q = qstr_find_strn((const char *)str_data, str_len);
+            if (q != MP_QSTRnull) {
+                return MP_OBJ_NEW_QSTR(q);
+            }
 
-                mp_obj_str_t *o = MP_OBJ_TO_PTR(mp_obj_new_str_copy(type, NULL, str_len));
-                o->data = str_data;
-                o->hash = str_hash;
-                return MP_OBJ_FROM_PTR(o);
-            } else {
-                mp_buffer_info_t bufinfo;
-                mp_get_buffer_raise(args[0], &bufinfo, MP_BUFFER_READ);
-                // This will utf-8 check the input.
-                return mp_obj_new_str(bufinfo.buf, bufinfo.len);
+            if (!mp_obj_is_type(args[0], &mp_type_bytes)) {
+                // Source is a mutable buffer: copy the data.
+                return mp_obj_new_str_copy(type, str_data, str_len);
             }
+
+            // Source is immutable bytes: reference its data without copying.
+            GET_STR_HASH(args[0], str_hash);
+            if (str_hash == 0) {
+                str_hash = qstr_compute_hash(str_data, str_len);
+            }
+            mp_obj_str_t *o = MP_OBJ_TO_PTR(mp_obj_new_str_copy(type, NULL, str_len));
+            o->data = str_data;
+            o->hash = str_hash;
+            return MP_OBJ_FROM_PTR(o);
+        }
     }
 }
 
-STATIC mp_obj_t bytes_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t bytes_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)type_in;
 
     #if MICROPY_CPYTHON_COMPAT
@@ -239,11 +366,20 @@ STATIC mp_obj_t bytes_make_new(const mp_obj_type_t *type_in, size_t n_args, size
             mp_raise_TypeError(MP_ERROR_TEXT("string argument without an encoding"));
             #endif
         }
+
         GET_STR_DATA_LEN(args[0], str_data, str_len);
         GET_STR_HASH(args[0], str_hash);
         if (str_hash == 0) {
             str_hash = qstr_compute_hash(str_data, str_len);
         }
+
+        #if MICROPY_PY_BUILTINS_STR_UNICODE_CHECK
+        mp_encoding_t encoding = parse_encoding_arg(mp_obj_str_get_qstr(args[1]));
+        if (encoding == MP_ENCODING_ASCII && !unicode_encoding_check(MP_ENCODING_ASCII, str_data, str_len)) {
+            mp_raise_msg(&mp_type_UnicodeError, NULL);
+        }
+        #endif
+
         mp_obj_str_t *o = MP_OBJ_TO_PTR(mp_obj_new_str_copy(&mp_type_bytes, NULL, str_len));
         o->data = str_data;
         o->hash = str_hash;
@@ -261,7 +397,10 @@ STATIC mp_obj_t bytes_make_new(const mp_obj_type_t *type_in, size_t n_args, size
         }
         vstr_t vstr;
         vstr_init_len(&vstr, len);
+        // If this config is set then the GC clears all memory, so we don't need to.
+        #if !MICROPY_GC_CONSERVATIVE_CLEAR
         memset(vstr.buf, 0, len);
+        #endif
         return mp_obj_new_bytes_from_vstr(&vstr);
     }
 
@@ -337,8 +476,7 @@ mp_obj_t mp_obj_str_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_i
         mp_obj_t *args = &rhs_in;
         size_t n_args = 1;
         mp_obj_t dict = MP_OBJ_NULL;
-        if (mp_obj_is_type(rhs_in, &mp_type_tuple)) {
-            // TODO: Support tuple subclasses?
+        if (mp_obj_is_tuple_compatible(rhs_in)) {
             mp_obj_tuple_get(rhs_in, &n_args, &args);
         } else if (mp_obj_is_type(rhs_in, &mp_type_dict)) {
             dict = rhs_in;
@@ -360,11 +498,7 @@ mp_obj_t mp_obj_str_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_i
             return MP_OBJ_NULL; // op not supported
         }
         if (n <= 0) {
-            if (lhs_type == &mp_type_str) {
-                return MP_OBJ_NEW_QSTR(MP_QSTR_); // empty str
-            } else {
-                return mp_const_empty_bytes;
-            }
+            return make_empty_str_of_type(lhs_type);
         }
         vstr_t vstr;
         vstr_init_len(&vstr, lhs_len * n);
@@ -458,7 +592,7 @@ const byte *str_index_to_ptr(const mp_obj_type_t *type, const byte *self_data, s
 #endif
 
 // This is used for both bytes and 8-bit strings. This is not used for unicode strings.
-STATIC mp_obj_t bytes_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
+static mp_obj_t bytes_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
     const mp_obj_type_t *type = mp_obj_get_type(self_in);
     GET_STR_DATA_LEN(self_in, self_data, self_len);
     if (value == MP_OBJ_SENTINEL) {
@@ -484,7 +618,7 @@ STATIC mp_obj_t bytes_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
     }
 }
 
-STATIC mp_obj_t str_join(mp_obj_t self_in, mp_obj_t arg) {
+static mp_obj_t str_join(mp_obj_t self_in, mp_obj_t arg) {
     check_is_str_or_bytes(self_in);
     const mp_obj_type_t *self_type = mp_obj_get_type(self_in);
     const mp_obj_type_t *ret_type = self_type;
@@ -628,7 +762,7 @@ mp_obj_t mp_obj_str_split(size_t n_args, const mp_obj_t *args) {
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_split_obj, 1, 3, mp_obj_str_split);
 
 #if MICROPY_PY_BUILTINS_STR_SPLITLINES
-STATIC mp_obj_t str_splitlines(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
+static mp_obj_t str_splitlines(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
     enum { ARG_keepends };
     static const mp_arg_t allowed_args[] = {
         { MP_QSTR_keepends, MP_ARG_BOOL, {.u_bool = false} },
@@ -674,7 +808,7 @@ STATIC mp_obj_t str_splitlines(size_t n_args, const mp_obj_t *pos_args, mp_map_t
 MP_DEFINE_CONST_FUN_OBJ_KW(str_splitlines_obj, 1, str_splitlines);
 #endif
 
-STATIC mp_obj_t str_rsplit(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t str_rsplit(size_t n_args, const mp_obj_t *args) {
     if (n_args < 3) {
         // If we don't have split limit, it doesn't matter from which side
         // we split.
@@ -739,15 +873,33 @@ STATIC mp_obj_t str_rsplit(size_t n_args, const mp_obj_t *args) {
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_rsplit_obj, 1, 3, str_rsplit);
 
-STATIC mp_obj_t str_finder(size_t n_args, const mp_obj_t *args, int direction, bool is_index) {
+static mp_obj_t str_finder(size_t n_args, const mp_obj_t *args, int direction, bool is_index) {
     const mp_obj_type_t *self_type = mp_obj_get_type(args[0]);
     check_is_str_or_bytes(args[0]);
 
-    // check argument type
-    str_check_arg_type(self_type, args[1]);
-
     GET_STR_DATA_LEN(args[0], haystack, haystack_len);
-    GET_STR_DATA_LEN(args[1], needle, needle_len);
+
+    mp_int_t val;
+    byte needle_data;
+    const byte *needle;
+    size_t needle_len;
+    if (self_type != &mp_type_str && mp_obj_get_int_maybe(args[1], &val)) {
+        // Allow {bytes/bytearray}.{find,index}(int).
+        #if MICROPY_FULL_CHECKS
+        if (val < 0 || val > 255) {
+            mp_raise_ValueError(MP_ERROR_TEXT("bytes value out of range"));
+        }
+        #endif
+        needle_data = val;
+        needle = &needle_data;
+        needle_len = 1;
+    } else {
+        // check argument type
+        str_check_arg_type(self_type, args[1]);
+        GET_STR_DATA_LEN(args[1], needle_tmp, needle_len_tmp);
+        needle = needle_tmp;
+        needle_len = needle_len_tmp;
+    }
 
     const byte *start = haystack;
     const byte *end = haystack + haystack_len;
@@ -782,61 +934,58 @@ STATIC mp_obj_t str_finder(size_t n_args, const mp_obj_t *args, int direction, b
     }
 }
 
-STATIC mp_obj_t str_find(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t str_find(size_t n_args, const mp_obj_t *args) {
     return str_finder(n_args, args, 1, false);
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_find_obj, 2, 4, str_find);
 
-STATIC mp_obj_t str_rfind(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t str_rfind(size_t n_args, const mp_obj_t *args) {
     return str_finder(n_args, args, -1, false);
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_rfind_obj, 2, 4, str_rfind);
 
-STATIC mp_obj_t str_index(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t str_index(size_t n_args, const mp_obj_t *args) {
     return str_finder(n_args, args, 1, true);
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_index_obj, 2, 4, str_index);
 
-STATIC mp_obj_t str_rindex(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t str_rindex(size_t n_args, const mp_obj_t *args) {
     return str_finder(n_args, args, -1, true);
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_rindex_obj, 2, 4, str_rindex);
 
-// TODO: (Much) more variety in args
-STATIC mp_obj_t str_startswith(size_t n_args, const mp_obj_t *args) {
-    const mp_obj_type_t *self_type = mp_obj_get_type(args[0]);
-    GET_STR_DATA_LEN(args[0], str, str_len);
-    size_t prefix_len;
-    const char *prefix = mp_obj_str_get_data(args[1], &prefix_len);
-    const byte *start = str;
-    if (n_args > 2) {
-        start = str_index_to_ptr(self_type, str, str_len, args[2], true);
+static mp_obj_t str_startendswith(size_t n_args, const mp_obj_t *args, bool ends_with) {
+    size_t str_len;
+    const byte *str = get_substring_data(args[0], n_args - 2, args + 2, &str_len);
+    mp_obj_t *prefixes = (mp_obj_t *)&args[1];
+    size_t n_prefixes = 1;
+    if (mp_obj_is_type(args[1], &mp_type_tuple)) {
+        mp_obj_tuple_get(args[1], &n_prefixes, &prefixes);
     }
-    if (prefix_len + (start - str) > str_len) {
-        return mp_const_false;
+    size_t prefix_len;
+    for (size_t i = 0; i < n_prefixes; i++) {
+        const char *prefix = mp_obj_str_get_data(prefixes[i], &prefix_len);
+        const byte *s = str + (ends_with ? str_len - prefix_len : 0);
+        if (prefix_len <= str_len && memcmp(s, prefix, prefix_len) == 0) {
+            return mp_const_true;
+        }
     }
-    return mp_obj_new_bool(memcmp(start, prefix, prefix_len) == 0);
+    return mp_const_false;
 }
-MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_startswith_obj, 2, 3, str_startswith);
 
-STATIC mp_obj_t str_endswith(size_t n_args, const mp_obj_t *args) {
-    GET_STR_DATA_LEN(args[0], str, str_len);
-    size_t suffix_len;
-    const char *suffix = mp_obj_str_get_data(args[1], &suffix_len);
-    if (n_args > 2) {
-        mp_raise_NotImplementedError(MP_ERROR_TEXT("start/end indices"));
-    }
+static mp_obj_t str_startswith(size_t n_args, const mp_obj_t *args) {
+    return str_startendswith(n_args, args, false);
+}
+MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_startswith_obj, 2, 4, str_startswith);
 
-    if (suffix_len > str_len) {
-        return mp_const_false;
-    }
-    return mp_obj_new_bool(memcmp(str + (str_len - suffix_len), suffix, suffix_len) == 0);
+static mp_obj_t str_endswith(size_t n_args, const mp_obj_t *args) {
+    return str_startendswith(n_args, args, true);
 }
-MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_endswith_obj, 2, 3, str_endswith);
+MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_endswith_obj, 2, 4, str_endswith);
 
 enum { LSTRIP, RSTRIP, STRIP };
 
-STATIC mp_obj_t str_uni_strip(int type, size_t n_args, const mp_obj_t *args) {
+static mp_obj_t str_uni_strip(int type, size_t n_args, const mp_obj_t *args) {
     check_is_str_or_bytes(args[0]);
     const mp_obj_type_t *self_type = mp_obj_get_type(args[0]);
 
@@ -886,11 +1035,7 @@ STATIC mp_obj_t str_uni_strip(int type, size_t n_args, const mp_obj_t *args) {
 
     if (!first_good_char_pos_set) {
         // string is all whitespace, return ''
-        if (self_type == &mp_type_str) {
-            return MP_OBJ_NEW_QSTR(MP_QSTR_);
-        } else {
-            return mp_const_empty_bytes;
-        }
+        return make_empty_str_of_type(self_type);
     }
 
     assert(last_good_char_pos >= first_good_char_pos);
@@ -905,33 +1050,52 @@ STATIC mp_obj_t str_uni_strip(int type, size_t n_args, const mp_obj_t *args) {
     return mp_obj_new_str_of_type(self_type, orig_str + first_good_char_pos, stripped_len);
 }
 
-STATIC mp_obj_t str_strip(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t str_strip(size_t n_args, const mp_obj_t *args) {
     return str_uni_strip(STRIP, n_args, args);
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_strip_obj, 1, 2, str_strip);
 
-STATIC mp_obj_t str_lstrip(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t str_lstrip(size_t n_args, const mp_obj_t *args) {
     return str_uni_strip(LSTRIP, n_args, args);
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_lstrip_obj, 1, 2, str_lstrip);
 
-STATIC mp_obj_t str_rstrip(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t str_rstrip(size_t n_args, const mp_obj_t *args) {
     return str_uni_strip(RSTRIP, n_args, args);
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_rstrip_obj, 1, 2, str_rstrip);
 
 #if MICROPY_PY_BUILTINS_STR_CENTER
-STATIC mp_obj_t str_center(mp_obj_t str_in, mp_obj_t width_in) {
+static mp_obj_t str_center(mp_obj_t str_in, mp_obj_t width_in) {
     GET_STR_DATA_LEN(str_in, str, str_len);
     mp_uint_t width = mp_obj_get_int(width_in);
+
+    #if MICROPY_PY_BUILTINS_STR_UNICODE
+    // Get character count (not byte count) for proper Unicode handling
+    size_t char_len = utf8_charlen(str, str_len);
+    if (char_len >= width) {
+        return str_in;
+    }
+    // Calculate padding: width is in characters, need to convert to bytes for allocation
+    mp_uint_t padding_chars = width - char_len;
+    // Padding is always spaces (1 byte each), plus the original string bytes
+    mp_uint_t total_bytes = padding_chars + str_len;
+    #else
+    // Non-Unicode build: byte length equals character length
     if (str_len >= width) {
         return str_in;
     }
+    mp_uint_t total_bytes = width;
+    #endif // MICROPY_PY_BUILTINS_STR_UNICODE
 
     vstr_t vstr;
-    vstr_init_len(&vstr, width);
-    memset(vstr.buf, ' ', width);
+    vstr_init_len(&vstr, total_bytes);
+    memset(vstr.buf, ' ', total_bytes);
+    #if MICROPY_PY_BUILTINS_STR_UNICODE
+    int left = padding_chars / 2;
+    #else
     int left = (width - str_len) / 2;
+    #endif // MICROPY_PY_BUILTINS_STR_UNICODE
     memcpy(vstr.buf + left, str, str_len);
     return mp_obj_new_str_type_from_vstr(mp_obj_get_type(str_in), &vstr);
 }
@@ -940,7 +1104,7 @@ MP_DEFINE_CONST_FUN_OBJ_2(str_center_obj, str_center);
 
 // Takes an int arg, but only parses unsigned numbers, and only changes
 // *num if at least one digit was parsed.
-STATIC const char *str_to_int(const char *str, const char *top, int *num) {
+static const char *str_to_int(const char *str, const char *top, int *num) {
     if (str < top && '0' <= *str && *str <= '9') {
         *num = 0;
         do {
@@ -952,19 +1116,19 @@ STATIC const char *str_to_int(const char *str, const char *top, int *num) {
     return str;
 }
 
-STATIC bool isalignment(char ch) {
+static bool isalignment(char ch) {
     return ch && strchr("<>=^", ch) != NULL;
 }
 
-STATIC bool istype(char ch) {
+static bool istype(char ch) {
     return ch && strchr("bcdeEfFgGnosxX%", ch) != NULL;
 }
 
-STATIC bool arg_looks_integer(mp_obj_t arg) {
+static bool arg_looks_integer(mp_obj_t arg) {
     return mp_obj_is_bool(arg) || mp_obj_is_int(arg);
 }
 
-STATIC bool arg_looks_numeric(mp_obj_t arg) {
+static bool arg_looks_numeric(mp_obj_t arg) {
     return arg_looks_integer(arg)
            #if MICROPY_PY_BUILTINS_FLOAT
            || mp_obj_is_float(arg)
@@ -973,7 +1137,7 @@ STATIC bool arg_looks_numeric(mp_obj_t arg) {
 }
 
 #if MICROPY_PY_BUILTINS_STR_OP_MODULO
-STATIC mp_obj_t arg_as_int(mp_obj_t arg) {
+static mp_obj_t arg_as_int(mp_obj_t arg) {
     #if MICROPY_PY_BUILTINS_FLOAT
     if (mp_obj_is_float(arg)) {
         return mp_obj_new_int_from_float(mp_obj_float_get(arg));
@@ -984,7 +1148,7 @@ STATIC mp_obj_t arg_as_int(mp_obj_t arg) {
 #endif
 
 #if MICROPY_ERROR_REPORTING <= MICROPY_ERROR_REPORTING_TERSE
-STATIC NORETURN void terse_str_format_value_error(void) {
+static MP_NORETURN void terse_str_format_value_error(void) {
     mp_raise_ValueError(MP_ERROR_TEXT("bad format string"));
 }
 #else
@@ -992,7 +1156,24 @@ STATIC NORETURN void terse_str_format_value_error(void) {
 #define terse_str_format_value_error()
 #endif
 
-STATIC vstr_t mp_obj_str_format_helper(const char *str, const char *top, int *arg_i, size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) {
+// Print the character with the code point given by the integer object arg.
+// Used by both the str.format and the modulo (%c) formatters.
+static void mp_print_char(const mp_print_t *print, mp_obj_t arg, unsigned int flags, char fill, int width) {
+    #if MICROPY_FULL_CHECKS
+    mp_uint_t c = mp_obj_get_int(arg);
+    if (c >= 0x110000) {
+        mp_raise_msg(&mp_type_OverflowError, MP_ERROR_TEXT("char not in range(0x110000)"));
+    }
+    VSTR_FIXED(ch_vstr, 4);
+    vstr_add_char(&ch_vstr, c);
+    mp_print_strn(print, ch_vstr.buf, ch_vstr.len, flags, fill, width);
+    #else
+    char ch = mp_obj_get_int(arg);
+    mp_print_strn(print, &ch, 1, flags, fill, width);
+    #endif
+}
+
+static vstr_t mp_obj_str_format_helper(const char *str, const char *top, int *arg_i, size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) {
     vstr_t vstr;
     mp_print_t print;
     vstr_init_print(&vstr, 16, &print);
@@ -1167,7 +1348,7 @@ STATIC vstr_t mp_obj_str_format_helper(const char *str, const char *top, int *ar
         int width = -1;
         int precision = -1;
         char type = '\0';
-        int flags = 0;
+        unsigned int flags = 0;
 
         if (format_spec) {
             // The format specifier (from http://docs.python.org/2/library/string.html#formatspec)
@@ -1181,7 +1362,7 @@ STATIC vstr_t mp_obj_str_format_helper(const char *str, const char *top, int *ar
             // type        ::=  "b" | "c" | "d" | "e" | "E" | "f" | "F" | "g" | "G" | "n" | "o" | "s" | "x" | "X" | "%"
 
             // recursively call the formatter to format any nested specifiers
-            MP_STACK_CHECK();
+            mp_cstack_check();
             vstr_t format_spec_vstr = mp_obj_str_format_helper(format_spec, str, arg_i, n_args, args, kwargs);
             const char *s = vstr_null_terminated_str(&format_spec_vstr);
             const char *stop = s + format_spec_vstr.len;
@@ -1212,8 +1393,9 @@ STATIC vstr_t mp_obj_str_format_helper(const char *str, const char *top, int *ar
                 }
             }
             s = str_to_int(s, stop, &width);
-            if (*s == ',') {
-                flags |= PF_FLAG_SHOW_COMMA;
+            if (*s == ',' || *s == '_') {
+                MP_STATIC_ASSERT((unsigned)'_' << PF_FLAG_SEP_POS >> PF_FLAG_SEP_POS == '_');
+                flags |= (unsigned)*s << PF_FLAG_SEP_POS;
                 s++;
             }
             if (*s == '.') {
@@ -1280,13 +1462,12 @@ STATIC vstr_t mp_obj_str_format_helper(const char *str, const char *top, int *ar
                     continue;
 
                 case 'c': {
-                    char ch = mp_obj_get_int(arg);
-                    mp_print_strn(&print, &ch, 1, flags, fill, width);
+                    mp_print_char(&print, arg, flags, fill, width);
                     continue;
                 }
 
                 case '\0':  // No explicit format type implies 'd'
-                case 'n':   // I don't think we support locales in uPy so use 'd'
+                case 'n':   // I don't think we support locales in MicroPython so use 'd'
                 case 'd':
                     mp_print_mp_int(&print, arg, 10, 'a', flags, fill, width, 0);
                     continue;
@@ -1449,7 +1630,7 @@ mp_obj_t mp_obj_str_format(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs
 MP_DEFINE_CONST_FUN_OBJ_KW(str_format_obj, 1, mp_obj_str_format);
 
 #if MICROPY_PY_BUILTINS_STR_OP_MODULO
-STATIC mp_obj_t str_modulo_format(mp_obj_t pattern, size_t n_args, const mp_obj_t *args, mp_obj_t dict) {
+static mp_obj_t str_modulo_format(mp_obj_t pattern, size_t n_args, const mp_obj_t *args, mp_obj_t dict) {
     check_is_str_or_bytes(pattern);
 
     GET_STR_DATA_LEN(pattern, str, len);
@@ -1574,8 +1755,7 @@ STATIC mp_obj_t str_modulo_format(mp_obj_t pattern, size_t n_args, const mp_obj_
                     }
                     mp_print_strn(&print, s, 1, flags, ' ', width);
                 } else if (arg_looks_integer(arg)) {
-                    char ch = mp_obj_get_int(arg);
-                    mp_print_strn(&print, &ch, 1, flags, ' ', width);
+                    mp_print_char(&print, arg, flags, ' ', width);
                 } else {
                     mp_raise_TypeError(MP_ERROR_TEXT("integer needed"));
                 }
@@ -1645,7 +1825,9 @@ STATIC mp_obj_t str_modulo_format(mp_obj_t pattern, size_t n_args, const mp_obj_
         }
     }
 
-    if (arg_i != n_args) {
+    if (dict == MP_OBJ_NULL && arg_i != n_args) {
+        // NOTE: if `dict` exists, then `n_args` is 1 and the dict is always consumed; either
+        // positionally, or as a map of named args, even if none were actually referenced.
         mp_raise_TypeError(MP_ERROR_TEXT("format string didn't convert all arguments"));
     }
 
@@ -1655,7 +1837,7 @@ STATIC mp_obj_t str_modulo_format(mp_obj_t pattern, size_t n_args, const mp_obj_
 
 // The implementation is optimized, returning the original string if there's
 // nothing to replace.
-STATIC mp_obj_t str_replace(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t str_replace(size_t n_args, const mp_obj_t *args) {
     check_is_str_or_bytes(args[0]);
 
     mp_int_t max_rep = -1;
@@ -1757,7 +1939,7 @@ STATIC mp_obj_t str_replace(size_t n_args, const mp_obj_t *args) {
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_replace_obj, 3, 4, str_replace);
 
 #if MICROPY_PY_BUILTINS_STR_COUNT
-STATIC mp_obj_t str_count(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t str_count(size_t n_args, const mp_obj_t *args) {
     const mp_obj_type_t *self_type = mp_obj_get_type(args[0]);
     check_is_str_or_bytes(args[0]);
 
@@ -1800,7 +1982,7 @@ MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(str_count_obj, 2, 4, str_count);
 #endif
 
 #if MICROPY_PY_BUILTINS_STR_PARTITION
-STATIC mp_obj_t str_partitioner(mp_obj_t self_in, mp_obj_t arg, int direction) {
+static mp_obj_t str_partitioner(mp_obj_t self_in, mp_obj_t arg, int direction) {
     check_is_str_or_bytes(self_in);
     const mp_obj_type_t *self_type = mp_obj_get_type(self_in);
     str_check_arg_type(self_type, arg);
@@ -1813,15 +1995,9 @@ STATIC mp_obj_t str_partitioner(mp_obj_t self_in, mp_obj_t arg, int direction) {
     }
 
     mp_obj_t result[3];
-    if (self_type == &mp_type_str) {
-        result[0] = MP_OBJ_NEW_QSTR(MP_QSTR_);
-        result[1] = MP_OBJ_NEW_QSTR(MP_QSTR_);
-        result[2] = MP_OBJ_NEW_QSTR(MP_QSTR_);
-    } else {
-        result[0] = mp_const_empty_bytes;
-        result[1] = mp_const_empty_bytes;
-        result[2] = mp_const_empty_bytes;
-    }
+    result[0] = make_empty_str_of_type(self_type);
+    result[1] = make_empty_str_of_type(self_type);
+    result[2] = make_empty_str_of_type(self_type);
 
     if (direction > 0) {
         result[0] = self_in;
@@ -1846,19 +2022,19 @@ STATIC mp_obj_t str_partitioner(mp_obj_t self_in, mp_obj_t arg, int direction) {
     return mp_obj_new_tuple(3, result);
 }
 
-STATIC mp_obj_t str_partition(mp_obj_t self_in, mp_obj_t arg) {
+static mp_obj_t str_partition(mp_obj_t self_in, mp_obj_t arg) {
     return str_partitioner(self_in, arg, 1);
 }
 MP_DEFINE_CONST_FUN_OBJ_2(str_partition_obj, str_partition);
 
-STATIC mp_obj_t str_rpartition(mp_obj_t self_in, mp_obj_t arg) {
+static mp_obj_t str_rpartition(mp_obj_t self_in, mp_obj_t arg) {
     return str_partitioner(self_in, arg, -1);
 }
 MP_DEFINE_CONST_FUN_OBJ_2(str_rpartition_obj, str_rpartition);
 #endif
 
 // Supposedly not too critical operations, so optimize for code size
-STATIC mp_obj_t str_caseconv(unichar (*op)(unichar), mp_obj_t self_in) {
+static mp_obj_t str_caseconv(unichar (*op)(unichar), mp_obj_t self_in) {
     GET_STR_DATA_LEN(self_in, self_data, self_len);
     vstr_t vstr;
     vstr_init_len(&vstr, self_len);
@@ -1869,17 +2045,17 @@ STATIC mp_obj_t str_caseconv(unichar (*op)(unichar), mp_obj_t self_in) {
     return mp_obj_new_str_type_from_vstr(mp_obj_get_type(self_in), &vstr);
 }
 
-STATIC mp_obj_t str_lower(mp_obj_t self_in) {
+static mp_obj_t str_lower(mp_obj_t self_in) {
     return str_caseconv(unichar_tolower, self_in);
 }
 MP_DEFINE_CONST_FUN_OBJ_1(str_lower_obj, str_lower);
 
-STATIC mp_obj_t str_upper(mp_obj_t self_in) {
+static mp_obj_t str_upper(mp_obj_t self_in) {
     return str_caseconv(unichar_toupper, self_in);
 }
 MP_DEFINE_CONST_FUN_OBJ_1(str_upper_obj, str_upper);
 
-STATIC mp_obj_t str_uni_istype(bool (*f)(unichar), mp_obj_t self_in) {
+static mp_obj_t str_uni_istype(bool (*f)(unichar), mp_obj_t self_in) {
     GET_STR_DATA_LEN(self_in, self_data, self_len);
 
     if (self_len == 0) {
@@ -1912,27 +2088,27 @@ STATIC mp_obj_t str_uni_istype(bool (*f)(unichar), mp_obj_t self_in) {
     return mp_const_true;
 }
 
-STATIC mp_obj_t str_isspace(mp_obj_t self_in) {
+static mp_obj_t str_isspace(mp_obj_t self_in) {
     return str_uni_istype(unichar_isspace, self_in);
 }
 MP_DEFINE_CONST_FUN_OBJ_1(str_isspace_obj, str_isspace);
 
-STATIC mp_obj_t str_isalpha(mp_obj_t self_in) {
+static mp_obj_t str_isalpha(mp_obj_t self_in) {
     return str_uni_istype(unichar_isalpha, self_in);
 }
 MP_DEFINE_CONST_FUN_OBJ_1(str_isalpha_obj, str_isalpha);
 
-STATIC mp_obj_t str_isdigit(mp_obj_t self_in) {
+static mp_obj_t str_isdigit(mp_obj_t self_in) {
     return str_uni_istype(unichar_isdigit, self_in);
 }
 MP_DEFINE_CONST_FUN_OBJ_1(str_isdigit_obj, str_isdigit);
 
-STATIC mp_obj_t str_isupper(mp_obj_t self_in) {
+static mp_obj_t str_isupper(mp_obj_t self_in) {
     return str_uni_istype(unichar_isupper, self_in);
 }
 MP_DEFINE_CONST_FUN_OBJ_1(str_isupper_obj, str_isupper);
 
-STATIC mp_obj_t str_islower(mp_obj_t self_in) {
+static mp_obj_t str_islower(mp_obj_t self_in) {
     return str_uni_istype(unichar_islower, self_in);
 }
 MP_DEFINE_CONST_FUN_OBJ_1(str_islower_obj, str_islower);
@@ -1940,8 +2116,9 @@ MP_DEFINE_CONST_FUN_OBJ_1(str_islower_obj, str_islower);
 #if MICROPY_CPYTHON_COMPAT
 // These methods are superfluous in the presence of str() and bytes()
 // constructors.
+
 // TODO: should accept kwargs too
-STATIC mp_obj_t bytes_decode(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t bytes_decode(size_t n_args, const mp_obj_t *args) {
     mp_obj_t new_args[2];
     if (n_args == 1) {
         new_args[0] = args[0];
@@ -1954,7 +2131,7 @@ STATIC mp_obj_t bytes_decode(size_t n_args, const mp_obj_t *args) {
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(bytes_decode_obj, 1, 3, bytes_decode);
 
 // TODO: should accept kwargs too
-STATIC mp_obj_t str_encode(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t str_encode(size_t n_args, const mp_obj_t *args) {
     mp_obj_t new_args[2];
     if (n_args == 1) {
         new_args[0] = args[0];
@@ -1978,7 +2155,7 @@ mp_obj_t mp_obj_bytes_hex(size_t n_args, const mp_obj_t *args, const mp_obj_type
     // Code below assumes non-zero buffer length when computing size with
     // separator, so handle the zero-length case here.
     if (bufinfo.len == 0) {
-        return mp_const_empty_bytes;
+        return make_empty_str_of_type(type);
     }
 
     vstr_t vstr;
@@ -2012,37 +2189,31 @@ mp_obj_t mp_obj_bytes_fromhex(mp_obj_t type_in, mp_obj_t data) {
     mp_buffer_info_t bufinfo;
     mp_get_buffer_raise(data, &bufinfo, MP_BUFFER_READ);
 
-    if ((bufinfo.len & 1) != 0) {
-        mp_raise_ValueError(MP_ERROR_TEXT("odd-length string"));
-    }
     vstr_t vstr;
     vstr_init_len(&vstr, bufinfo.len / 2);
     byte *in = bufinfo.buf, *out = (byte *)vstr.buf;
-    byte hex_byte = 0;
-    for (mp_uint_t i = bufinfo.len; i--;) {
-        byte hex_ch = *in++;
-        if (unichar_isxdigit(hex_ch)) {
-            hex_byte += unichar_xdigit_value(hex_ch);
-        } else {
-            mp_raise_ValueError(MP_ERROR_TEXT("non-hex digit found"));
+    byte *in_end = in + bufinfo.len;
+    mp_uint_t ch1, ch2;
+    while (in < in_end) {
+        if (unichar_isspace(ch1 = *in++)) {
+            continue;  // Skip whitespace between hex digit pairs
         }
-        if (i & 1) {
-            hex_byte <<= 4;
-        } else {
-            *out++ = hex_byte;
-            hex_byte = 0;
+        if (in == in_end || !unichar_isxdigit(ch1) || !unichar_isxdigit(ch2 = *in++)) {
+            mp_raise_ValueError(MP_ERROR_TEXT("non-hex digit"));
         }
+        *out++ = (byte)((unichar_xdigit_value(ch1) << 4) | unichar_xdigit_value(ch2));
     }
+    vstr.len = out - (byte *)vstr.buf;  // Length may be shorter due to whitespace in input
     return mp_obj_new_str_type_from_vstr(MP_OBJ_TO_PTR(type_in), &vstr);
 }
 
-STATIC mp_obj_t bytes_hex_as_str(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t bytes_hex_as_str(size_t n_args, const mp_obj_t *args) {
     return mp_obj_bytes_hex(n_args, args, &mp_type_str);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(bytes_hex_as_str_obj, 1, 2, bytes_hex_as_str);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(bytes_hex_as_str_obj, 1, 2, bytes_hex_as_str);
 
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(bytes_fromhex_obj, mp_obj_bytes_fromhex);
-STATIC MP_DEFINE_CONST_CLASSMETHOD_OBJ(bytes_fromhex_classmethod_obj, MP_ROM_PTR(&bytes_fromhex_obj));
+static MP_DEFINE_CONST_FUN_OBJ_2(bytes_fromhex_obj, mp_obj_bytes_fromhex);
+static MP_DEFINE_CONST_CLASSMETHOD_OBJ(bytes_fromhex_classmethod_obj, MP_ROM_PTR(&bytes_fromhex_obj));
 #endif // MICROPY_PY_BUILTINS_BYTES_HEX
 
 mp_int_t mp_obj_str_get_buffer(mp_obj_t self_in, mp_buffer_info_t *bufinfo, mp_uint_t flags) {
@@ -2066,7 +2237,7 @@ void mp_obj_str_set_data(mp_obj_str_t *str, const byte *data, size_t len) {
 
 // This locals table is used for the following types: str, bytes, bytearray, array.array.
 // Each type takes a different section (start to end offset) of this table.
-STATIC const mp_rom_map_elem_t array_bytearray_str_bytes_locals_table[] = {
+static const mp_rom_map_elem_t array_bytearray_str_bytes_locals_table[] = {
     #if MICROPY_PY_ARRAY || MICROPY_PY_BUILTINS_BYTEARRAY
     { MP_ROM_QSTR(MP_QSTR_append), MP_ROM_PTR(&mp_obj_array_append_obj) },
     { MP_ROM_QSTR(MP_QSTR_extend), MP_ROM_PTR(&mp_obj_array_extend_obj) },
@@ -2166,7 +2337,7 @@ MP_DEFINE_CONST_DICT_WITH_SIZE(mp_obj_memoryview_locals_dict,
 #endif
 
 #if !MICROPY_PY_BUILTINS_STR_UNICODE
-STATIC mp_obj_t mp_obj_new_str_iterator(mp_obj_t str, mp_obj_iter_buf_t *iter_buf);
+static mp_obj_t mp_obj_new_str_iterator(mp_obj_t str, mp_obj_iter_buf_t *iter_buf);
 
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_str,
@@ -2238,7 +2409,7 @@ mp_obj_t mp_obj_new_str_via_qstr(const char *data, size_t len) {
 // Create a str/bytes object from the given vstr.  The vstr buffer is resized to
 // the exact length required and then reused for the str/bytes object.  The vstr
 // is cleared and can safely be passed to vstr_free if it was heap allocated.
-STATIC mp_obj_t mp_obj_new_str_type_from_vstr(const mp_obj_type_t *type, vstr_t *vstr) {
+static mp_obj_t mp_obj_new_str_type_from_vstr(const mp_obj_type_t *type, vstr_t *vstr) {
     // if not a bytes object, look if a qstr with this data already exists
     if (type == &mp_type_str) {
         qstr q = qstr_find_strn(vstr->buf, vstr->len);
@@ -2272,7 +2443,7 @@ STATIC mp_obj_t mp_obj_new_str_type_from_vstr(const mp_obj_type_t *type, vstr_t
 
 mp_obj_t mp_obj_new_str_from_vstr(vstr_t *vstr) {
     #if MICROPY_PY_BUILTINS_STR_UNICODE && MICROPY_PY_BUILTINS_STR_UNICODE_CHECK
-    if (!utf8_check((byte *)vstr->buf, vstr->len)) {
+    if (!unicode_encoding_check(MP_ENCODING_UTF8, (byte *)vstr->buf, vstr->len)) {
         mp_raise_msg(&mp_type_UnicodeError, NULL);
     }
     #endif // MICROPY_PY_BUILTINS_STR_UNICODE && MICROPY_PY_BUILTINS_STR_UNICODE_CHECK
@@ -2292,7 +2463,7 @@ mp_obj_t mp_obj_new_bytes_from_vstr(vstr_t *vstr) {
 
 mp_obj_t mp_obj_new_str(const char *data, size_t len) {
     #if MICROPY_PY_BUILTINS_STR_UNICODE && MICROPY_PY_BUILTINS_STR_UNICODE_CHECK
-    if (!utf8_check((byte *)data, len)) {
+    if (!unicode_encoding_check(MP_ENCODING_UTF8, (byte *)data, len)) {
         mp_raise_msg(&mp_type_UnicodeError, NULL);
     }
     #endif
@@ -2306,6 +2477,10 @@ mp_obj_t mp_obj_new_str(const char *data, size_t len) {
     }
 }
 
+mp_obj_t mp_obj_new_str_from_cstr(const char *str) {
+    return mp_obj_new_str(str, strlen(str));
+}
+
 mp_obj_t mp_obj_str_intern(mp_obj_t str) {
     GET_STR_DATA_LEN(str, data, len);
     return mp_obj_new_str_via_qstr((const char *)data, len);
@@ -2340,7 +2515,7 @@ bool mp_obj_str_equal(mp_obj_t s1, mp_obj_t s2) {
     }
 }
 
-STATIC NORETURN void bad_implicit_conversion(mp_obj_t self_in) {
+static MP_NORETURN void bad_implicit_conversion(mp_obj_t self_in) {
     #if MICROPY_ERROR_REPORTING <= MICROPY_ERROR_REPORTING_TERSE
     mp_raise_TypeError(MP_ERROR_TEXT("can't convert to str implicitly"));
     #else
@@ -2409,7 +2584,7 @@ typedef struct _mp_obj_str8_it_t {
 } mp_obj_str8_it_t;
 
 #if !MICROPY_PY_BUILTINS_STR_UNICODE
-STATIC mp_obj_t str_it_iternext(mp_obj_t self_in) {
+static mp_obj_t str_it_iternext(mp_obj_t self_in) {
     mp_obj_str8_it_t *self = MP_OBJ_TO_PTR(self_in);
     GET_STR_DATA_LEN(self->str, str, len);
     if (self->cur < len) {
@@ -2421,7 +2596,7 @@ STATIC mp_obj_t str_it_iternext(mp_obj_t self_in) {
     }
 }
 
-STATIC mp_obj_t mp_obj_new_str_iterator(mp_obj_t str, mp_obj_iter_buf_t *iter_buf) {
+static mp_obj_t mp_obj_new_str_iterator(mp_obj_t str, mp_obj_iter_buf_t *iter_buf) {
     assert(sizeof(mp_obj_str8_it_t) <= sizeof(mp_obj_iter_buf_t));
     mp_obj_str8_it_t *o = (mp_obj_str8_it_t *)iter_buf;
     o->base.type = &mp_type_polymorph_iter;
@@ -2432,7 +2607,7 @@ STATIC mp_obj_t mp_obj_new_str_iterator(mp_obj_t str, mp_obj_iter_buf_t *iter_bu
 }
 #endif
 
-STATIC mp_obj_t bytes_it_iternext(mp_obj_t self_in) {
+static mp_obj_t bytes_it_iternext(mp_obj_t self_in) {
     mp_obj_str8_it_t *self = MP_OBJ_TO_PTR(self_in);
     GET_STR_DATA_LEN(self->str, str, len);
     if (self->cur < len) {
diff --git a/py/objstr.h b/py/objstr.h
index 72fe1cfef01..028fc9597ff 100644
--- a/py/objstr.h
+++ b/py/objstr.h
@@ -100,6 +100,8 @@ const byte *str_index_to_ptr(const mp_obj_type_t *type, const byte *self_data, s
     mp_obj_t index, bool is_slice);
 const byte *find_subbytes(const byte *haystack, size_t hlen, const byte *needle, size_t nlen, int direction);
 
+#define MP_DEFINE_BYTES_OBJ(obj_name, target, len) mp_obj_str_t obj_name = {{&mp_type_bytes}, 0, (len), (const byte *)(target)}
+
 mp_obj_t mp_obj_bytes_hex(size_t n_args, const mp_obj_t *args, const mp_obj_type_t *type);
 mp_obj_t mp_obj_bytes_fromhex(mp_obj_t type_in, mp_obj_t data);
 
diff --git a/py/objstringio.c b/py/objstringio.c
index 1a083449baf..a4dc8cfc915 100644
--- a/py/objstringio.c
+++ b/py/objstringio.c
@@ -36,7 +36,7 @@
 #if MICROPY_PY_IO
 
 #if MICROPY_CPYTHON_COMPAT
-STATIC void check_stringio_is_open(const mp_obj_stringio_t *o) {
+static void check_stringio_is_open(const mp_obj_stringio_t *o) {
     if (o->vstr == NULL) {
         mp_raise_ValueError(MP_ERROR_TEXT("I/O operation on closed file"));
     }
@@ -45,13 +45,13 @@ STATIC void check_stringio_is_open(const mp_obj_stringio_t *o) {
 #define check_stringio_is_open(o)
 #endif
 
-STATIC void stringio_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void stringio_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_stringio_t *self = MP_OBJ_TO_PTR(self_in);
     mp_printf(print, self->base.type == &mp_type_stringio ? "" : "", self);
 }
 
-STATIC mp_uint_t stringio_read(mp_obj_t o_in, void *buf, mp_uint_t size, int *errcode) {
+static mp_uint_t stringio_read(mp_obj_t o_in, void *buf, mp_uint_t size, int *errcode) {
     (void)errcode;
     mp_obj_stringio_t *o = MP_OBJ_TO_PTR(o_in);
     check_stringio_is_open(o);
@@ -67,7 +67,7 @@ STATIC mp_uint_t stringio_read(mp_obj_t o_in, void *buf, mp_uint_t size, int *er
     return size;
 }
 
-STATIC void stringio_copy_on_write(mp_obj_stringio_t *o) {
+static void stringio_copy_on_write(mp_obj_stringio_t *o) {
     const void *buf = o->vstr->buf;
     o->vstr->buf = m_new(char, o->vstr->len);
     o->vstr->fixed_buf = false;
@@ -75,7 +75,7 @@ STATIC void stringio_copy_on_write(mp_obj_stringio_t *o) {
     memcpy(o->vstr->buf, buf, o->vstr->len);
 }
 
-STATIC mp_uint_t stringio_write(mp_obj_t o_in, const void *buf, mp_uint_t size, int *errcode) {
+static mp_uint_t stringio_write(mp_obj_t o_in, const void *buf, mp_uint_t size, int *errcode) {
     (void)errcode;
     mp_obj_stringio_t *o = MP_OBJ_TO_PTR(o_in);
     check_stringio_is_open(o);
@@ -109,7 +109,7 @@ STATIC mp_uint_t stringio_write(mp_obj_t o_in, const void *buf, mp_uint_t size,
     return size;
 }
 
-STATIC mp_uint_t stringio_ioctl(mp_obj_t o_in, mp_uint_t request, uintptr_t arg, int *errcode) {
+static mp_uint_t stringio_ioctl(mp_obj_t o_in, mp_uint_t request, uintptr_t arg, int *errcode) {
     (void)errcode;
     mp_obj_stringio_t *o = MP_OBJ_TO_PTR(o_in);
     switch (request) {
@@ -162,28 +162,22 @@ STATIC mp_uint_t stringio_ioctl(mp_obj_t o_in, mp_uint_t request, uintptr_t arg,
 
 #define STREAM_TO_CONTENT_TYPE(o) (((o)->base.type == &mp_type_stringio) ? &mp_type_str : &mp_type_bytes)
 
-STATIC mp_obj_t stringio_getvalue(mp_obj_t self_in) {
+static mp_obj_t stringio_getvalue(mp_obj_t self_in) {
     mp_obj_stringio_t *self = MP_OBJ_TO_PTR(self_in);
     check_stringio_is_open(self);
     // TODO: Try to avoid copying string
     return mp_obj_new_str_of_type(STREAM_TO_CONTENT_TYPE(self), (byte *)self->vstr->buf, self->vstr->len);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(stringio_getvalue_obj, stringio_getvalue);
+static MP_DEFINE_CONST_FUN_OBJ_1(stringio_getvalue_obj, stringio_getvalue);
 
-STATIC mp_obj_t stringio___exit__(size_t n_args, const mp_obj_t *args) {
-    (void)n_args;
-    return mp_stream_close(args[0]);
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(stringio___exit___obj, 4, 4, stringio___exit__);
-
-STATIC mp_obj_stringio_t *stringio_new(const mp_obj_type_t *type) {
+static mp_obj_stringio_t *stringio_new(const mp_obj_type_t *type) {
     mp_obj_stringio_t *o = mp_obj_malloc(mp_obj_stringio_t, type);
     o->pos = 0;
     o->ref_obj = MP_OBJ_NULL;
     return o;
 }
 
-STATIC mp_obj_t stringio_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t stringio_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)n_kw; // TODO check n_kw==0
 
     mp_uint_t sz = 16;
@@ -221,7 +215,7 @@ STATIC mp_obj_t stringio_make_new(const mp_obj_type_t *type_in, size_t n_args, s
     return MP_OBJ_FROM_PTR(o);
 }
 
-STATIC const mp_rom_map_elem_t stringio_locals_dict_table[] = {
+static const mp_rom_map_elem_t stringio_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mp_stream_read_obj) },
     { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&mp_stream_readinto_obj) },
     { MP_ROM_QSTR(MP_QSTR_readline), MP_ROM_PTR(&mp_stream_unbuffered_readline_obj) },
@@ -232,12 +226,12 @@ STATIC const mp_rom_map_elem_t stringio_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&mp_stream_close_obj) },
     { MP_ROM_QSTR(MP_QSTR_getvalue), MP_ROM_PTR(&stringio_getvalue_obj) },
     { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&mp_identity_obj) },
-    { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&stringio___exit___obj) },
+    { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&mp_stream___exit___obj) },
 };
 
-STATIC MP_DEFINE_CONST_DICT(stringio_locals_dict, stringio_locals_dict_table);
+static MP_DEFINE_CONST_DICT(stringio_locals_dict, stringio_locals_dict_table);
 
-STATIC const mp_stream_p_t stringio_stream_p = {
+static const mp_stream_p_t stringio_stream_p = {
     .read = stringio_read,
     .write = stringio_write,
     .ioctl = stringio_ioctl,
@@ -255,7 +249,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
     );
 
 #if MICROPY_PY_IO_BYTESIO
-STATIC const mp_stream_p_t bytesio_stream_p = {
+static const mp_stream_p_t bytesio_stream_p = {
     .read = stringio_read,
     .write = stringio_write,
     .ioctl = stringio_ioctl,
diff --git a/py/objstrunicode.c b/py/objstrunicode.c
index 93383b3c19e..fc511667a36 100644
--- a/py/objstrunicode.c
+++ b/py/objstrunicode.c
@@ -34,12 +34,12 @@
 
 #if MICROPY_PY_BUILTINS_STR_UNICODE
 
-STATIC mp_obj_t mp_obj_new_str_iterator(mp_obj_t str, mp_obj_iter_buf_t *iter_buf);
+static mp_obj_t mp_obj_new_str_iterator(mp_obj_t str, mp_obj_iter_buf_t *iter_buf);
 
 /******************************************************************************/
 /* str                                                                        */
 
-STATIC void uni_print_quoted(const mp_print_t *print, const byte *str_data, uint str_len) {
+static void uni_print_quoted(const mp_print_t *print, const byte *str_data, uint str_len) {
     // this escapes characters, but it will be very slow to print (calling print many times)
     bool has_single_quote = false;
     bool has_double_quote = false;
@@ -57,9 +57,11 @@ STATIC void uni_print_quoted(const mp_print_t *print, const byte *str_data, uint
     mp_printf(print, "%c", quote_char);
     const byte *s = str_data, *top = str_data + str_len;
     while (s < top) {
+        const byte *seq_start = s;
         unichar ch;
         ch = utf8_get_char(s);
         s = utf8_next_char(s);
+        size_t seq_len = s - seq_start;
         if (ch == quote_char) {
             mp_printf(print, "\\%c", quote_char);
         } else if (ch == '\\') {
@@ -72,20 +74,28 @@ STATIC void uni_print_quoted(const mp_print_t *print, const byte *str_data, uint
             mp_print_str(print, "\\r");
         } else if (ch == '\t') {
             mp_print_str(print, "\\t");
-        } else if (ch < 0x100) {
+        } else if (ch <= 127) {
             mp_printf(print, "\\x%02x", ch);
+        } else if (ch < 0xD800) {
+            // Printable Unicode character (excluding surrogates) - output UTF-8 bytes directly
+            print->print_strn(print->data, (const char *)seq_start, seq_len);
+        } else if (ch >= 0xE000 && ch < 0x110000) {
+            // Printable Unicode character (after surrogates) - output UTF-8 bytes directly
+            print->print_strn(print->data, (const char *)seq_start, seq_len);
         } else if (ch < 0x10000) {
+            // Surrogate (0xD800-0xDFFF) - output as \uXXXX escape.
             mp_printf(print, "\\u%04x", ch);
         } else {
+            // Invalid (>=0x110000) - output as \UXXXXXXXX escape.
             mp_printf(print, "\\U%08x", ch);
         }
     }
     mp_printf(print, "%c", quote_char);
 }
 
-STATIC void uni_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void uni_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     GET_STR_DATA_LEN(self_in, str_data, str_len);
-    #if MICROPY_PY_UJSON
+    #if MICROPY_PY_JSON
     if (kind == PRINT_JSON) {
         mp_str_print_json(print, str_data, str_len);
         return;
@@ -98,7 +108,7 @@ STATIC void uni_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t
     }
 }
 
-STATIC mp_obj_t uni_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
+static mp_obj_t uni_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
     GET_STR_DATA_LEN(self_in, str_data, str_len);
     switch (op) {
         case MP_UNARY_OP_BOOL:
@@ -178,7 +188,7 @@ const byte *str_index_to_ptr(const mp_obj_type_t *type, const byte *self_data, s
     return s;
 }
 
-STATIC mp_obj_t str_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
+static mp_obj_t str_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
     const mp_obj_type_t *type = mp_obj_get_type(self_in);
     assert(type == &mp_type_str);
     GET_STR_DATA_LEN(self_in, self_data, self_len);
@@ -253,7 +263,7 @@ typedef struct _mp_obj_str_it_t {
     size_t cur;
 } mp_obj_str_it_t;
 
-STATIC mp_obj_t str_it_iternext(mp_obj_t self_in) {
+static mp_obj_t str_it_iternext(mp_obj_t self_in) {
     mp_obj_str_it_t *self = MP_OBJ_TO_PTR(self_in);
     GET_STR_DATA_LEN(self->str, str, len);
     if (self->cur < len) {
@@ -267,7 +277,7 @@ STATIC mp_obj_t str_it_iternext(mp_obj_t self_in) {
     }
 }
 
-STATIC mp_obj_t mp_obj_new_str_iterator(mp_obj_t str, mp_obj_iter_buf_t *iter_buf) {
+static mp_obj_t mp_obj_new_str_iterator(mp_obj_t str, mp_obj_iter_buf_t *iter_buf) {
     assert(sizeof(mp_obj_str_it_t) <= sizeof(mp_obj_iter_buf_t));
     mp_obj_str_it_t *o = (mp_obj_str_it_t *)iter_buf;
     o->base.type = &mp_type_polymorph_iter;
diff --git a/py/objtemplate.c b/py/objtemplate.c
new file mode 100644
index 00000000000..86451350a21
--- /dev/null
+++ b/py/objtemplate.c
@@ -0,0 +1,395 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 Koudai Aono
+ * Copyright (c) 2026 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "py/runtime.h"
+
+#if MICROPY_PY_TSTRINGS
+
+typedef struct _mp_obj_template_t {
+    mp_obj_base_t base;
+    mp_obj_t strings;
+    mp_obj_t interpolations;
+} mp_obj_template_t;
+
+typedef struct _mp_obj_interpolation_t {
+    mp_obj_base_t base;
+    mp_obj_t value;
+    mp_obj_t expression;
+    mp_obj_t conversion;
+    mp_obj_t format_spec;
+} mp_obj_interpolation_t;
+
+static mp_obj_t mp_obj_new_interpolation(mp_obj_t value, mp_obj_t expr, mp_obj_t conv, mp_obj_t spec);
+
+static mp_obj_t mp_obj_template_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+    mp_arg_check_num(n_args, n_kw, 0, MP_OBJ_FUN_ARGS_MAX, false);
+
+    mp_obj_t strings_obj;
+    mp_obj_t interpolations_obj;
+
+    if (n_args == 0) {
+        mp_obj_t empty = MP_OBJ_NEW_QSTR(MP_QSTR_);
+        strings_obj = mp_obj_new_tuple(1, &empty);
+        interpolations_obj = mp_obj_new_tuple(0, NULL);
+    } else {
+        size_t n_interpolations = 0;
+        size_t n_str_args = 0;
+        for (size_t i = 0; i < n_args; i++) {
+            if (mp_obj_is_exact_type(args[i], &mp_type_interpolation)) {
+                n_interpolations++;
+            } else if (mp_obj_is_str(args[i])) {
+                n_str_args++;
+            } else {
+                mp_raise_TypeError(MP_ERROR_TEXT("expected str or Interpolation"));
+            }
+        }
+
+        if (n_interpolations == 0) {
+            if (n_str_args == 1) {
+                strings_obj = mp_obj_new_tuple(1, &args[0]);
+            } else {
+                size_t total_len = 0;
+                for (size_t i = 0; i < n_args; i++) {
+                    size_t str_len;
+                    (void)mp_obj_str_get_data(args[i], &str_len);
+                    total_len += str_len;
+                }
+                vstr_t vstr;
+                vstr_init(&vstr, total_len);
+                for (size_t i = 0; i < n_args; i++) {
+                    size_t str_len;
+                    const char *str_data = mp_obj_str_get_data(args[i], &str_len);
+                    vstr_add_strn(&vstr, str_data, str_len);
+                }
+                mp_obj_t str_items[1];
+                str_items[0] = mp_obj_new_str_from_vstr(&vstr);
+                strings_obj = mp_obj_new_tuple(1, str_items);
+            }
+            interpolations_obj = mp_obj_new_tuple(0, NULL);
+        } else {
+            size_t n_strings = n_interpolations + 1;
+            mp_obj_tuple_t *strings_tuple = mp_obj_malloc_var(mp_obj_tuple_t, items, mp_obj_t, n_strings, &mp_type_tuple);
+            mp_obj_tuple_t *interpolations_tuple = mp_obj_malloc_var(mp_obj_tuple_t, items, mp_obj_t, n_interpolations, &mp_type_tuple);
+            strings_tuple->len = n_strings;
+            interpolations_tuple->len = n_interpolations;
+
+            size_t string_idx = 0;
+            size_t interp_idx = 0;
+            mp_obj_t current_str = MP_OBJ_NULL;
+            bool current_vstr_active = false;
+            vstr_t current_vstr = {0};
+
+            for (size_t i = 0; i <= n_args; i++) {
+                if (i == n_args || mp_obj_is_exact_type(args[i], &mp_type_interpolation)) {
+                    mp_obj_t out_str;
+                    if (current_vstr_active) {
+                        out_str = mp_obj_new_str_from_vstr(¤t_vstr);
+                        current_vstr_active = false;
+                    } else if (current_str != MP_OBJ_NULL) {
+                        out_str = current_str;
+                    } else {
+                        out_str = MP_OBJ_NEW_QSTR(MP_QSTR_);
+                    }
+                    strings_tuple->items[string_idx++] = out_str;
+                    current_str = MP_OBJ_NULL;
+                    if (i < n_args) {
+                        interpolations_tuple->items[interp_idx++] = args[i];
+                    }
+                } else {
+                    size_t str_len;
+                    const char *str_data = mp_obj_str_get_data(args[i], &str_len);
+                    if (current_vstr_active) {
+                        vstr_add_strn(¤t_vstr, str_data, str_len);
+                    } else if (current_str == MP_OBJ_NULL) {
+                        current_str = args[i];
+                    } else {
+                        size_t prev_len;
+                        const char *prev_data = mp_obj_str_get_data(current_str, &prev_len);
+                        vstr_init(¤t_vstr, prev_len + str_len);
+                        vstr_add_strn(¤t_vstr, prev_data, prev_len);
+                        vstr_add_strn(¤t_vstr, str_data, str_len);
+                        current_vstr_active = true;
+                        current_str = MP_OBJ_NULL;
+                    }
+                }
+            }
+
+            strings_obj = MP_OBJ_FROM_PTR(strings_tuple);
+            interpolations_obj = MP_OBJ_FROM_PTR(interpolations_tuple);
+        }
+    }
+
+    mp_obj_template_t *self = mp_obj_malloc(mp_obj_template_t, type);
+    self->strings = strings_obj;
+    self->interpolations = interpolations_obj;
+    return MP_OBJ_FROM_PTR(self);
+}
+
+static void mp_obj_template_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+    (void)kind;
+    mp_obj_template_t *self = MP_OBJ_TO_PTR(self_in);
+    mp_printf(print, "%q(%q=", (qstr)MP_QSTR_Template, (qstr)MP_QSTR_strings);
+    mp_obj_print_helper(print, self->strings, PRINT_REPR);
+    mp_printf(print, ", %q=", (qstr)MP_QSTR_interpolations);
+    mp_obj_print_helper(print, self->interpolations, PRINT_REPR);
+    mp_print_str(print, ")");
+}
+
+static mp_obj_t mp_obj_template_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
+    mp_obj_template_t *lhs = MP_OBJ_TO_PTR(lhs_in);
+
+    switch (op) {
+        case MP_BINARY_OP_ADD: {
+            if (!mp_obj_is_exact_type(rhs_in, &mp_type_template)) {
+                return MP_OBJ_NULL; // op not supported
+            }
+
+            mp_obj_template_t *rhs = MP_OBJ_TO_PTR(rhs_in);
+
+            mp_obj_tuple_t *lhs_strings = MP_OBJ_TO_PTR(lhs->strings);
+            mp_obj_tuple_t *lhs_interps = MP_OBJ_TO_PTR(lhs->interpolations);
+            mp_obj_tuple_t *rhs_strings = MP_OBJ_TO_PTR(rhs->strings);
+            mp_obj_tuple_t *rhs_interps = MP_OBJ_TO_PTR(rhs->interpolations);
+
+            size_t new_strings_len = lhs_strings->len + rhs_strings->len - 1;
+            size_t new_interps_len = lhs_interps->len + rhs_interps->len;
+
+            // Create tuples directly to avoid GC issues.
+            mp_obj_tuple_t *new_strings_tuple = mp_obj_malloc_var(mp_obj_tuple_t, items, mp_obj_t, new_strings_len, &mp_type_tuple);
+            mp_obj_tuple_t *new_interps_tuple = mp_obj_malloc_var(mp_obj_tuple_t, items, mp_obj_t, new_interps_len, &mp_type_tuple);
+            new_strings_tuple->len = new_strings_len;
+            new_interps_tuple->len = new_interps_len;
+
+            // Copy all but the last string from lhs.
+            for (size_t i = 0; i < lhs_strings->len - 1; i++) {
+                new_strings_tuple->items[i] = lhs_strings->items[i];
+            }
+
+            // Merge last string from lhs with first string from rhs.
+            size_t lhs_last_len, rhs_first_len;
+            const char *lhs_last_str = mp_obj_str_get_data(lhs_strings->items[lhs_strings->len - 1], &lhs_last_len);
+            const char *rhs_first_str = mp_obj_str_get_data(rhs_strings->items[0], &rhs_first_len);
+
+            vstr_t vstr;
+            vstr_init(&vstr, lhs_last_len + rhs_first_len);
+            vstr_add_strn(&vstr, lhs_last_str, lhs_last_len);
+            vstr_add_strn(&vstr, rhs_first_str, rhs_first_len);
+            new_strings_tuple->items[lhs_strings->len - 1] = mp_obj_new_str_from_vstr(&vstr);
+
+            // Copy remaining strings from rhs.
+            for (size_t i = 1; i < rhs_strings->len; i++) {
+                new_strings_tuple->items[lhs_strings->len - 1 + i] = rhs_strings->items[i];
+            }
+
+            // Copy interpolations from both sides.
+            for (size_t i = 0; i < lhs_interps->len; i++) {
+                new_interps_tuple->items[i] = lhs_interps->items[i];
+            }
+
+            for (size_t i = 0; i < rhs_interps->len; i++) {
+                new_interps_tuple->items[lhs_interps->len + i] = rhs_interps->items[i];
+            }
+
+            mp_obj_template_t *result = mp_obj_malloc(mp_obj_template_t, &mp_type_template);
+            result->strings = MP_OBJ_FROM_PTR(new_strings_tuple);
+            result->interpolations = MP_OBJ_FROM_PTR(new_interps_tuple);
+            return MP_OBJ_FROM_PTR(result);
+        }
+
+        default:
+            return MP_OBJ_NULL; // op not supported
+    }
+}
+
+static void mp_obj_template_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    mp_obj_template_t *self = MP_OBJ_TO_PTR(self_in);
+
+    if (dest[0] == MP_OBJ_NULL) {
+        // Load attribute.
+        if (attr == MP_QSTR_strings) {
+            dest[0] = self->strings;
+        } else if (attr == MP_QSTR_interpolations) {
+            dest[0] = self->interpolations;
+        } else if (attr == MP_QSTR_values) {
+            mp_obj_tuple_t *interps = MP_OBJ_TO_PTR(self->interpolations);
+            mp_obj_tuple_t *values_tuple = MP_OBJ_TO_PTR(mp_obj_new_tuple(interps->len, NULL));
+            for (size_t i = 0; i < interps->len; i++) {
+                mp_obj_interpolation_t *interp = MP_OBJ_TO_PTR(interps->items[i]);
+                values_tuple->items[i] = interp->value;
+            }
+            dest[0] = MP_OBJ_FROM_PTR(values_tuple);
+        }
+    }
+}
+
+typedef struct _mp_obj_template_iter_t {
+    mp_obj_base_t base;
+    mp_fun_1_t iternext;
+    mp_obj_t template;
+    size_t index;
+} mp_obj_template_iter_t;
+
+static mp_obj_t template_iter_iternext(mp_obj_t self_in) {
+    mp_obj_template_iter_t *self = MP_OBJ_TO_PTR(self_in);
+    mp_obj_template_t *tmpl = MP_OBJ_TO_PTR(self->template);
+    mp_obj_tuple_t *strings = MP_OBJ_TO_PTR(tmpl->strings);
+    mp_obj_tuple_t *interps = MP_OBJ_TO_PTR(tmpl->interpolations);
+
+    while (self->index < strings->len + interps->len) {
+        if ((self->index & 1) == 0) {
+            // A string.
+            mp_obj_t str_obj = strings->items[self->index++ / 2];
+            size_t str_len;
+            mp_obj_str_get_data(str_obj, &str_len);
+            if (str_len > 0) {
+                return str_obj;
+            }
+        } else {
+            // An interpolation.
+            return interps->items[self->index++ / 2];
+        }
+    }
+
+    return MP_OBJ_STOP_ITERATION;
+}
+
+static mp_obj_t mp_obj_template_iter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
+    assert(sizeof(mp_obj_template_iter_t) <= sizeof(mp_obj_iter_buf_t));
+    mp_obj_template_iter_t *iter = (mp_obj_template_iter_t *)iter_buf;
+    iter->base.type = &mp_type_polymorph_iter;
+    iter->iternext = template_iter_iternext;
+    iter->template = self_in;
+    iter->index = 0;
+    return MP_OBJ_FROM_PTR(iter);
+}
+
+MP_DEFINE_CONST_OBJ_TYPE(
+    mp_type_template,
+    MP_QSTR_Template,
+    MP_TYPE_FLAG_NONE,
+    make_new, mp_obj_template_make_new,
+    print, mp_obj_template_print,
+    binary_op, mp_obj_template_binary_op,
+    attr, mp_obj_template_attr,
+    iter, mp_obj_template_iter
+    );
+
+mp_obj_t mp_obj_new_template(size_t n_args, const mp_obj_t *args) {
+    mp_obj_template_t *o = mp_obj_malloc(mp_obj_template_t, &mp_type_template);
+    o->strings = args[0];
+    if (n_args == 2) {
+        // Unpack interpolations from second argument (which is a tuple).
+        mp_obj_t *iargs;
+        mp_obj_get_array(args[1], &n_args, &iargs);
+        args = iargs;
+    } else {
+        // Unpack interpolations directly from arguments.
+        --n_args;
+        ++args;
+    }
+    size_t n_interpolations = n_args / 4;
+    mp_obj_tuple_t *interpolations = MP_OBJ_TO_PTR(mp_obj_new_tuple(n_interpolations, NULL));
+    for (size_t i = 0; i < n_interpolations; ++i) {
+        interpolations->items[i] = mp_obj_new_interpolation(args[i * 4], args[i * 4 + 1], args[i * 4 + 2], args[i * 4 + 3]);
+    }
+    o->interpolations = MP_OBJ_FROM_PTR(interpolations);
+    return MP_OBJ_FROM_PTR(o);
+}
+
+/////////////////////////////////////////////////////////////////
+
+static mp_obj_t mp_obj_interpolation_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
+    enum { ARG_value, ARG_expression, ARG_conversion, ARG_format_spec };
+    static const mp_arg_t allowed_args[] = {
+        { MP_QSTR_value, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
+        { MP_QSTR_expression, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_QSTR(MP_QSTR_)} },
+        { MP_QSTR_conversion, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} },
+        { MP_QSTR_format_spec, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_QSTR(MP_QSTR_)} },
+    };
+
+    mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
+    mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
+
+    mp_obj_interpolation_t *self = mp_obj_malloc(mp_obj_interpolation_t, &mp_type_interpolation);
+    self->value = args[ARG_value].u_obj;
+    self->expression = args[ARG_expression].u_obj;
+    self->conversion = args[ARG_conversion].u_obj;
+    self->format_spec = args[ARG_format_spec].u_obj;
+
+    return MP_OBJ_FROM_PTR(self);
+}
+
+static void mp_obj_interpolation_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+    (void)kind;
+    mp_obj_interpolation_t *self = MP_OBJ_TO_PTR(self_in);
+    mp_printf(print, "%q(", (qstr)MP_QSTR_Interpolation);
+    mp_obj_print_helper(print, self->value, PRINT_REPR);
+    mp_print_str(print, ", ");
+    mp_obj_print_helper(print, self->expression, PRINT_REPR);
+    mp_print_str(print, ", ");
+    mp_obj_print_helper(print, self->conversion, PRINT_REPR);
+    mp_print_str(print, ", ");
+    mp_obj_print_helper(print, self->format_spec, PRINT_REPR);
+    mp_print_str(print, ")");
+}
+
+static void mp_obj_interpolation_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    mp_obj_interpolation_t *self = MP_OBJ_TO_PTR(self_in);
+
+    if (dest[0] == MP_OBJ_NULL) {
+        // load attribute
+        if (attr == MP_QSTR_value) {
+            dest[0] = self->value;
+        } else if (attr == MP_QSTR_expression) {
+            dest[0] = self->expression;
+        } else if (attr == MP_QSTR_conversion) {
+            dest[0] = self->conversion;
+        } else if (attr == MP_QSTR_format_spec) {
+            dest[0] = self->format_spec;
+        }
+    }
+}
+
+static mp_obj_t mp_obj_new_interpolation(mp_obj_t value, mp_obj_t expression, mp_obj_t conversion, mp_obj_t format_spec) {
+    mp_obj_interpolation_t *o = mp_obj_malloc(mp_obj_interpolation_t, &mp_type_interpolation);
+    o->value = value;
+    o->expression = expression;
+    o->conversion = conversion;
+    o->format_spec = format_spec;
+    return MP_OBJ_FROM_PTR(o);
+}
+
+MP_DEFINE_CONST_OBJ_TYPE(
+    mp_type_interpolation,
+    MP_QSTR_Interpolation,
+    MP_TYPE_FLAG_NONE,
+    make_new, mp_obj_interpolation_make_new,
+    print, mp_obj_interpolation_print,
+    attr, mp_obj_interpolation_attr
+    );
+
+#endif // MICROPY_PY_TSTRINGS
diff --git a/py/objtuple.c b/py/objtuple.c
index 9d6797b4ffc..c0ba1fd0d11 100644
--- a/py/objtuple.c
+++ b/py/objtuple.c
@@ -31,18 +31,15 @@
 #include "py/objtuple.h"
 #include "py/runtime.h"
 
-// type check is done on getiter method to allow tuple, namedtuple, attrtuple
-#define mp_obj_is_tuple_compatible(o) (MP_OBJ_TYPE_GET_SLOT_OR_NULL(mp_obj_get_type(o), iter) == mp_obj_tuple_getiter)
-
 /******************************************************************************/
 /* tuple                                                                      */
 
 void mp_obj_tuple_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     mp_obj_tuple_t *o = MP_OBJ_TO_PTR(o_in);
     const char *item_separator = ", ";
-    if (MICROPY_PY_UJSON && kind == PRINT_JSON) {
+    if (MICROPY_PY_JSON && kind == PRINT_JSON) {
         mp_print_str(print, "[");
-        #if MICROPY_PY_UJSON_SEPARATORS
+        #if MICROPY_PY_JSON_SEPARATORS
         item_separator = MP_PRINT_GET_EXT(print)->item_separator;
         #endif
     } else {
@@ -55,7 +52,7 @@ void mp_obj_tuple_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t
         }
         mp_obj_print_helper(print, o->items[i], kind);
     }
-    if (MICROPY_PY_UJSON && kind == PRINT_JSON) {
+    if (MICROPY_PY_JSON && kind == PRINT_JSON) {
         mp_print_str(print, "]");
     } else {
         if (o->len == 1) {
@@ -65,7 +62,7 @@ void mp_obj_tuple_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t
     }
 }
 
-STATIC mp_obj_t mp_obj_tuple_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t mp_obj_tuple_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)type_in;
 
     mp_arg_check_num(n_args, n_kw, 0, 1, false);
@@ -106,20 +103,27 @@ STATIC mp_obj_t mp_obj_tuple_make_new(const mp_obj_type_t *type_in, size_t n_arg
     }
 }
 
-// Don't pass MP_BINARY_OP_NOT_EQUAL here
-STATIC mp_obj_t tuple_cmp_helper(mp_uint_t op, mp_obj_t self_in, mp_obj_t another_in) {
-    mp_check_self(mp_obj_is_tuple_compatible(self_in));
-    const mp_obj_type_t *another_type = mp_obj_get_type(another_in);
-    mp_obj_tuple_t *self = MP_OBJ_TO_PTR(self_in);
-    if (MP_OBJ_TYPE_GET_SLOT_OR_NULL(another_type, iter) != mp_obj_tuple_getiter) {
+static mp_obj_tuple_t *tuple_subclass_helper(mp_obj_t obj) {
+    assert(obj != MP_OBJ_NULL);
+    const mp_obj_type_t *tuple_type = mp_obj_get_type(obj);
+    if (MP_OBJ_TYPE_GET_SLOT_OR_NULL(tuple_type, iter) != mp_obj_tuple_getiter) {
         // Slow path for user subclasses
-        another_in = mp_obj_cast_to_native_base(another_in, MP_OBJ_FROM_PTR(&mp_type_tuple));
-        if (another_in == MP_OBJ_NULL) {
-            return MP_OBJ_NULL;
+        obj = mp_obj_cast_to_native_base(obj, MP_OBJ_FROM_PTR(&mp_type_tuple));
+        if (obj == MP_OBJ_NULL) {
+            return NULL;
         }
     }
-    mp_obj_tuple_t *another = MP_OBJ_TO_PTR(another_in);
+    return MP_OBJ_TO_PTR(obj);
+}
 
+// Don't pass MP_BINARY_OP_NOT_EQUAL here
+static mp_obj_t tuple_cmp_helper(mp_uint_t op, mp_obj_t self_in, mp_obj_t another_in) {
+    mp_check_self(mp_obj_is_tuple_compatible(self_in));
+    mp_obj_tuple_t *self = MP_OBJ_TO_PTR(self_in);
+    mp_obj_tuple_t *another = tuple_subclass_helper(another_in);
+    if (!another) {
+        return MP_OBJ_NULL;
+    }
     return mp_obj_new_bool(mp_seq_cmp_objs(op, self->items, self->len, another->items, another->len));
 }
 
@@ -148,10 +152,10 @@ mp_obj_t mp_obj_tuple_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs) {
     switch (op) {
         case MP_BINARY_OP_ADD:
         case MP_BINARY_OP_INPLACE_ADD: {
-            if (!mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(mp_obj_get_type(rhs)), MP_OBJ_FROM_PTR(&mp_type_tuple))) {
+            mp_obj_tuple_t *p = tuple_subclass_helper(rhs);
+            if (!p) {
                 return MP_OBJ_NULL; // op not supported
             }
-            mp_obj_tuple_t *p = MP_OBJ_TO_PTR(rhs);
             mp_obj_tuple_t *s = MP_OBJ_TO_PTR(mp_obj_new_tuple(o->len + p->len, NULL));
             mp_seq_cat(s->items, o->items, o->len, p->items, p->len, mp_obj_t);
             return MP_OBJ_FROM_PTR(s);
@@ -203,26 +207,26 @@ mp_obj_t mp_obj_tuple_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
     }
 }
 
-STATIC mp_obj_t tuple_count(mp_obj_t self_in, mp_obj_t value) {
+static mp_obj_t tuple_count(mp_obj_t self_in, mp_obj_t value) {
     mp_check_self(mp_obj_is_type(self_in, &mp_type_tuple));
     mp_obj_tuple_t *self = MP_OBJ_TO_PTR(self_in);
     return mp_seq_count_obj(self->items, self->len, value);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(tuple_count_obj, tuple_count);
+static MP_DEFINE_CONST_FUN_OBJ_2(tuple_count_obj, tuple_count);
 
-STATIC mp_obj_t tuple_index(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t tuple_index(size_t n_args, const mp_obj_t *args) {
     mp_check_self(mp_obj_is_type(args[0], &mp_type_tuple));
     mp_obj_tuple_t *self = MP_OBJ_TO_PTR(args[0]);
     return mp_seq_index_obj(self->items, self->len, n_args, args);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tuple_index_obj, 2, 4, tuple_index);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tuple_index_obj, 2, 4, tuple_index);
 
-STATIC const mp_rom_map_elem_t tuple_locals_dict_table[] = {
+static const mp_rom_map_elem_t tuple_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_count), MP_ROM_PTR(&tuple_count_obj) },
     { MP_ROM_QSTR(MP_QSTR_index), MP_ROM_PTR(&tuple_index_obj) },
 };
 
-STATIC MP_DEFINE_CONST_DICT(tuple_locals_dict, tuple_locals_dict_table);
+static MP_DEFINE_CONST_DICT(tuple_locals_dict, tuple_locals_dict_table);
 
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_tuple,
@@ -244,7 +248,7 @@ mp_obj_t mp_obj_new_tuple(size_t n, const mp_obj_t *items) {
     if (n == 0) {
         return mp_const_empty_tuple;
     }
-    mp_obj_tuple_t *o = mp_obj_malloc_var(mp_obj_tuple_t, mp_obj_t, n, &mp_type_tuple);
+    mp_obj_tuple_t *o = mp_obj_malloc_var(mp_obj_tuple_t, items, mp_obj_t, n, &mp_type_tuple);
     o->len = n;
     if (items) {
         for (size_t i = 0; i < n; i++) {
@@ -254,19 +258,6 @@ mp_obj_t mp_obj_new_tuple(size_t n, const mp_obj_t *items) {
     return MP_OBJ_FROM_PTR(o);
 }
 
-void mp_obj_tuple_get(mp_obj_t self_in, size_t *len, mp_obj_t **items) {
-    assert(mp_obj_is_tuple_compatible(self_in));
-    mp_obj_tuple_t *self = MP_OBJ_TO_PTR(self_in);
-    *len = self->len;
-    *items = &self->items[0];
-}
-
-void mp_obj_tuple_del(mp_obj_t self_in) {
-    assert(mp_obj_is_type(self_in, &mp_type_tuple));
-    mp_obj_tuple_t *self = MP_OBJ_TO_PTR(self_in);
-    m_del_var(mp_obj_tuple_t, mp_obj_t, self->len, self);
-}
-
 /******************************************************************************/
 /* tuple iterator                                                             */
 
@@ -277,7 +268,7 @@ typedef struct _mp_obj_tuple_it_t {
     size_t cur;
 } mp_obj_tuple_it_t;
 
-STATIC mp_obj_t tuple_it_iternext(mp_obj_t self_in) {
+static mp_obj_t tuple_it_iternext(mp_obj_t self_in) {
     mp_obj_tuple_it_t *self = MP_OBJ_TO_PTR(self_in);
     if (self->cur < self->tuple->len) {
         mp_obj_t o_out = self->tuple->items[self->cur];
diff --git a/py/objtuple.h b/py/objtuple.h
index cc42aa6df39..892c5771b69 100644
--- a/py/objtuple.h
+++ b/py/objtuple.h
@@ -28,6 +28,9 @@
 
 #include "py/obj.h"
 
+// type check is done on getiter method to allow tuple, namedtuple, attrtuple
+#define mp_obj_is_tuple_compatible(o) (MP_OBJ_TYPE_GET_SLOT_OR_NULL(mp_obj_get_type(o), iter) == mp_obj_tuple_getiter)
+
 typedef struct _mp_obj_tuple_t {
     mp_obj_base_t base;
     size_t len;
@@ -46,11 +49,18 @@ mp_obj_t mp_obj_tuple_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs);
 mp_obj_t mp_obj_tuple_subscr(mp_obj_t base, mp_obj_t index, mp_obj_t value);
 mp_obj_t mp_obj_tuple_getiter(mp_obj_t o_in, mp_obj_iter_buf_t *iter_buf);
 
+static inline void mp_obj_tuple_get(mp_obj_t self_in, size_t *len, mp_obj_t **items) {
+    assert(mp_obj_is_tuple_compatible(self_in));
+    mp_obj_tuple_t *self = (mp_obj_tuple_t *)MP_OBJ_TO_PTR(self_in);
+    *len = self->len;
+    *items = &self->items[0];
+}
+
 extern const mp_obj_type_t mp_type_attrtuple;
 
 #define MP_DEFINE_ATTRTUPLE(tuple_obj_name, fields, nitems, ...) \
     const mp_rom_obj_tuple_t tuple_obj_name = { \
-        .base = {&mp_type_attrtuple}, \
+        .base = {.type = &mp_type_attrtuple}, \
         .len = nitems, \
         .items = { __VA_ARGS__, MP_ROM_PTR((void *)fields) } \
     }
diff --git a/py/objtype.c b/py/objtype.c
index 772f25d744d..95761245783 100644
--- a/py/objtype.c
+++ b/py/objtype.c
@@ -44,12 +44,14 @@
 #define ENABLE_SPECIAL_ACCESSORS \
     (MICROPY_PY_DESCRIPTORS || MICROPY_PY_DELATTR_SETATTR || MICROPY_PY_BUILTINS_PROPERTY)
 
-STATIC mp_obj_t static_class_method_make_new(const mp_obj_type_t *self_in, size_t n_args, size_t n_kw, const mp_obj_t *args);
+static mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict);
+static mp_obj_t mp_obj_is_subclass(mp_obj_t object, mp_obj_t classinfo);
+static mp_obj_t static_class_method_make_new(const mp_obj_type_t *self_in, size_t n_args, size_t n_kw, const mp_obj_t *args);
 
 /******************************************************************************/
 // instance object
 
-STATIC int instance_count_native_bases(const mp_obj_type_t *type, const mp_obj_type_t **last_native_base) {
+static int instance_count_native_bases(const mp_obj_type_t *type, const mp_obj_type_t **last_native_base) {
     int count = 0;
     for (;;) {
         if (type == &mp_type_object) {
@@ -82,30 +84,30 @@ STATIC int instance_count_native_bases(const mp_obj_type_t *type, const mp_obj_t
     }
 }
 
-// This wrapper function is allows a subclass of a native type to call the
+// This wrapper function allows a subclass of a native type to call the
 // __init__() method (corresponding to type->make_new) of the native type.
-STATIC mp_obj_t native_base_init_wrapper(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t native_base_init_wrapper(size_t n_args, const mp_obj_t *args, mp_map_t *kw_args) {
     mp_obj_instance_t *self = MP_OBJ_TO_PTR(args[0]);
     const mp_obj_type_t *native_base = NULL;
     instance_count_native_bases(self->base.type, &native_base);
-    self->subobj[0] = MP_OBJ_TYPE_GET_SLOT(native_base, make_new)(native_base, n_args - 1, 0, args + 1);
+    self->subobj[0] = MP_OBJ_TYPE_GET_SLOT(native_base, make_new)(native_base, n_args - 1, kw_args->used, args + 1);
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(native_base_init_wrapper_obj, 1, MP_OBJ_FUN_ARGS_MAX, native_base_init_wrapper);
+MP_DEFINE_CONST_FUN_OBJ_KW(mp_native_base_init_wrapper_obj, 1, native_base_init_wrapper);
 
 #if !MICROPY_CPYTHON_COMPAT
-STATIC
+static
 #endif
 mp_obj_instance_t *mp_obj_new_instance(const mp_obj_type_t *class, const mp_obj_type_t **native_base) {
     size_t num_native_bases = instance_count_native_bases(class, native_base);
     assert(num_native_bases < 2);
-    mp_obj_instance_t *o = mp_obj_malloc_var(mp_obj_instance_t, mp_obj_t, num_native_bases, class);
+    mp_obj_instance_t *o = mp_obj_malloc_var(mp_obj_instance_t, subobj, mp_obj_t, num_native_bases, class);
     mp_map_init(&o->members, 0);
     // Initialise the native base-class slot (should be 1 at most) with a valid
     // object.  It doesn't matter which object, so long as it can be uniquely
     // distinguished from a native class that is initialised.
     if (num_native_bases != 0) {
-        o->subobj[0] = MP_OBJ_FROM_PTR(&native_base_init_wrapper_obj);
+        o->subobj[0] = MP_OBJ_FROM_PTR(&mp_native_base_init_wrapper_obj);
     }
     return o;
 }
@@ -132,7 +134,7 @@ struct class_lookup_data {
     bool is_type;
 };
 
-STATIC void mp_obj_class_lookup(struct class_lookup_data *lookup, const mp_obj_type_t *type) {
+static void mp_obj_class_lookup(struct class_lookup_data *lookup, const mp_obj_type_t *type) {
     assert(lookup->dest[0] == MP_OBJ_NULL);
     assert(lookup->dest[1] == MP_OBJ_NULL);
     for (;;) {
@@ -170,6 +172,12 @@ STATIC void mp_obj_class_lookup(struct class_lookup_data *lookup, const mp_obj_t
                     if (obj != NULL && mp_obj_is_native_type(type) && type != &mp_type_object /* object is not a real type */) {
                         // If we're dealing with native base class, then it applies to native sub-object
                         obj_obj = obj->subobj[0];
+                        #if MICROPY_BUILTIN_METHOD_CHECK_SELF_ARG
+                        if (obj_obj == MP_OBJ_FROM_PTR(&mp_native_base_init_wrapper_obj)) {
+                            // But we shouldn't attempt lookups on object that is not yet instantiated.
+                            mp_raise_msg(&mp_type_AttributeError, MP_ERROR_TEXT("call super().__init__() first"));
+                        }
+                        #endif // MICROPY_BUILTIN_METHOD_CHECK_SELF_ARG
                     } else {
                         obj_obj = MP_OBJ_FROM_PTR(obj);
                     }
@@ -235,7 +243,7 @@ STATIC void mp_obj_class_lookup(struct class_lookup_data *lookup, const mp_obj_t
     }
 }
 
-STATIC void instance_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void instance_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     mp_obj_instance_t *self = MP_OBJ_TO_PTR(self_in);
     qstr meth = (kind == PRINT_STR) ? MP_QSTR___str__ : MP_QSTR___repr__;
     mp_obj_t member[2] = {MP_OBJ_NULL};
@@ -277,7 +285,7 @@ STATIC void instance_print(const mp_print_t *print, mp_obj_t self_in, mp_print_k
     mp_printf(print, "<%s object at %p>", mp_obj_get_type_str(self_in), self);
 }
 
-STATIC mp_obj_t mp_obj_instance_make_new(const mp_obj_type_t *self, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t mp_obj_instance_make_new(const mp_obj_type_t *self, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     assert(mp_obj_is_instance_type(self));
 
     // look for __new__ function
@@ -361,7 +369,7 @@ STATIC mp_obj_t mp_obj_instance_make_new(const mp_obj_type_t *self, size_t n_arg
 
     // If the type had a native base that was not explicitly initialised
     // (constructed) by the Python __init__() method then construct it now.
-    if (native_base != NULL && o->subobj[0] == MP_OBJ_FROM_PTR(&native_base_init_wrapper_obj)) {
+    if (native_base != NULL && o->subobj[0] == MP_OBJ_FROM_PTR(&mp_native_base_init_wrapper_obj)) {
         o->subobj[0] = MP_OBJ_TYPE_GET_SLOT(native_base, make_new)(native_base, n_args, n_kw, args);
     }
 
@@ -370,6 +378,8 @@ STATIC mp_obj_t mp_obj_instance_make_new(const mp_obj_type_t *self, size_t n_arg
 
 // Qstrs for special methods are guaranteed to have a small value, so we use byte
 // type to represent them.
+// The (unescaped) names appear in `unsorted_str_list` in the QSTR
+// generator script py/makeqstrdata.py to ensure they are assigned low numbers.
 const byte mp_unary_op_method_name[MP_UNARY_OP_NUM_RUNTIME] = {
     [MP_UNARY_OP_BOOL] = MP_QSTR___bool__,
     [MP_UNARY_OP_LEN] = MP_QSTR___len__,
@@ -392,7 +402,7 @@ const byte mp_unary_op_method_name[MP_UNARY_OP_NUM_RUNTIME] = {
     #endif
 };
 
-STATIC mp_obj_t instance_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
+static mp_obj_t instance_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
     mp_obj_instance_t *self = MP_OBJ_TO_PTR(self_in);
 
     #if MICROPY_PY_SYS_GETSIZEOF
@@ -433,6 +443,7 @@ STATIC mp_obj_t instance_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
                 val = MP_OBJ_NEW_SMALL_INT(mp_obj_get_int_truncated(val));
                 break;
             case MP_UNARY_OP_INT_MAYBE:
+            case MP_UNARY_OP_LEN:
                 // Must return int
                 if (!mp_obj_is_int(val)) {
                     mp_raise_TypeError(NULL);
@@ -468,6 +479,8 @@ STATIC mp_obj_t instance_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
 // fail).  They can be added at the expense of code size for the qstr.
 // Qstrs for special methods are guaranteed to have a small value, so we use byte
 // type to represent them.
+// The (unescaped) names appear in `unsorted_str_list` in the QSTR
+// generator script py/makeqstrdata.py to ensure they are assigned low numbers.
 const byte mp_binary_op_method_name[MP_BINARY_OP_NUM_RUNTIME] = {
     [MP_BINARY_OP_LESS] = MP_QSTR___lt__,
     [MP_BINARY_OP_MORE] = MP_QSTR___gt__,
@@ -530,7 +543,7 @@ const byte mp_binary_op_method_name[MP_BINARY_OP_NUM_RUNTIME] = {
     #endif
 };
 
-STATIC mp_obj_t instance_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
+static mp_obj_t instance_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) {
     // Note: For ducktyping, CPython does not look in the instance members or use
     // __getattr__ or __getattribute__.  It only looks in the class dictionary.
     mp_obj_instance_t *lhs = MP_OBJ_TO_PTR(lhs_in);
@@ -572,7 +585,7 @@ STATIC mp_obj_t instance_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t
     return res;
 }
 
-STATIC void mp_obj_instance_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+static void mp_obj_instance_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     // logic: look in instance members then class locals
     assert(mp_obj_is_instance_type(mp_obj_get_type(self_in)));
     mp_obj_instance_t *self = MP_OBJ_TO_PTR(self_in);
@@ -649,6 +662,13 @@ STATIC void mp_obj_instance_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *des
 
     // try __getattr__
     if (attr != MP_QSTR___getattr__) {
+        #if MICROPY_PY_DESCRIPTORS
+        // With descriptors enabled, don't delegate lookups of __get__/__set__/__delete__/__set_name__.
+        if (attr == MP_QSTR___get__ || attr == MP_QSTR___set__ || attr == MP_QSTR___delete__ || attr == MP_QSTR___set_name__) {
+            return;
+        }
+        #endif
+
         #if MICROPY_PY_DELATTR_SETATTR
         // If the requested attr is __setattr__/__delattr__ then don't delegate the lookup
         // to __getattr__.  If we followed CPython's behaviour then __setattr__/__delattr__
@@ -669,7 +689,7 @@ STATIC void mp_obj_instance_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *des
     }
 }
 
-STATIC bool mp_obj_instance_store_attr(mp_obj_t self_in, qstr attr, mp_obj_t value) {
+static bool mp_obj_instance_store_attr(mp_obj_t self_in, qstr attr, mp_obj_t value) {
     mp_obj_instance_t *self = MP_OBJ_TO_PTR(self_in);
 
     if (!(self->base.type->flags & MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS)) {
@@ -792,7 +812,7 @@ STATIC bool mp_obj_instance_store_attr(mp_obj_t self_in, qstr attr, mp_obj_t val
     }
 }
 
-STATIC void mp_obj_instance_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+static void mp_obj_instance_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     if (dest[0] == MP_OBJ_NULL) {
         mp_obj_instance_load_attr(self_in, attr, dest);
     } else {
@@ -802,7 +822,7 @@ STATIC void mp_obj_instance_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     }
 }
 
-STATIC mp_obj_t instance_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
+static mp_obj_t instance_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
     mp_obj_instance_t *self = MP_OBJ_TO_PTR(self_in);
     mp_obj_t member[4] = {MP_OBJ_NULL, MP_OBJ_NULL, index, value};
     struct class_lookup_data lookup = {
@@ -837,7 +857,7 @@ STATIC mp_obj_t instance_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value
     }
 }
 
-STATIC mp_obj_t mp_obj_instance_get_call(mp_obj_t self_in, mp_obj_t *member) {
+static mp_obj_t mp_obj_instance_get_call(mp_obj_t self_in, mp_obj_t *member) {
     mp_obj_instance_t *self = MP_OBJ_TO_PTR(self_in);
     struct class_lookup_data lookup = {
         .obj = self,
@@ -903,7 +923,7 @@ mp_obj_t mp_obj_instance_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf)
     }
 }
 
-STATIC mp_int_t instance_get_buffer(mp_obj_t self_in, mp_buffer_info_t *bufinfo, mp_uint_t flags) {
+static mp_int_t instance_get_buffer(mp_obj_t self_in, mp_buffer_info_t *bufinfo, mp_uint_t flags) {
     mp_obj_instance_t *self = MP_OBJ_TO_PTR(self_in);
     mp_obj_t member[2] = {MP_OBJ_NULL};
     struct class_lookup_data lookup = {
@@ -929,7 +949,7 @@ STATIC mp_int_t instance_get_buffer(mp_obj_t self_in, mp_buffer_info_t *bufinfo,
 //  - creating a new class (a new type) creates a new mp_obj_type_t
 
 #if ENABLE_SPECIAL_ACCESSORS
-STATIC bool check_for_special_accessors(mp_obj_t key, mp_obj_t value) {
+static bool check_for_special_accessors(mp_obj_t key, mp_obj_t value) {
     #if MICROPY_PY_DELATTR_SETATTR
     if (key == MP_OBJ_NEW_QSTR(MP_QSTR___setattr__) || key == MP_OBJ_NEW_QSTR(MP_QSTR___delattr__)) {
         return true;
@@ -942,7 +962,7 @@ STATIC bool check_for_special_accessors(mp_obj_t key, mp_obj_t value) {
     #endif
     #if MICROPY_PY_DESCRIPTORS
     static const uint8_t to_check[] = {
-        MP_QSTR___get__, MP_QSTR___set__, MP_QSTR___delete__,
+        MP_QSTR___get__, MP_QSTR___set__, MP_QSTR___delete__, // not needed for MP_QSTR___set_name__ though
     };
     for (size_t i = 0; i < MP_ARRAY_SIZE(to_check); ++i) {
         mp_obj_t dest_temp[2];
@@ -956,13 +976,55 @@ STATIC bool check_for_special_accessors(mp_obj_t key, mp_obj_t value) {
 }
 #endif
 
-STATIC void type_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+#if MICROPY_PY_DESCRIPTORS
+// Shared data layout for the __set_name__ call and a linked list of calls to be made.
+typedef union _setname_list_t setname_list_t;
+union _setname_list_t {
+    mp_obj_t call[4];
+    struct {
+        mp_obj_t _meth;
+        mp_obj_t _self;
+        setname_list_t *next; // can use the "owner" argument position temporarily for the linked list
+        mp_obj_t _name;
+    };
+};
+
+// Append any `__set_name__` method on `value` to the setname list, with its per-attr args
+static setname_list_t *setname_maybe_bind_append(setname_list_t *tail, mp_obj_t name, mp_obj_t value) {
+    // make certain our type-punning is safe:
+    MP_STATIC_ASSERT_NONCONSTEXPR(offsetof(setname_list_t, next) == offsetof(setname_list_t, call[2]));
+
+    // tail is a blank list entry
+    mp_load_method_maybe(value, MP_QSTR___set_name__, tail->call);
+    if (tail->call[1] != MP_OBJ_NULL) {
+        // Each time a __set_name__ is found, leave it in-place in the former tail and allocate a new tail
+        tail->next = m_new_obj(setname_list_t);
+        tail->next->next = NULL;
+        tail->call[3] = name;
+        return tail->next;
+    } else {
+        return tail;
+    }
+}
+
+// Execute the captured `__set_name__` calls, destroying the setname list in the process.
+static inline void setname_consume_call_all(setname_list_t *head, mp_obj_t owner) {
+    setname_list_t *next;
+    while ((next = head->next) != NULL) {
+        head->call[2] = owner;
+        mp_call_method_n_kw(2, 0, head->call);
+        head = next;
+    }
+}
+#endif
+
+static void type_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_type_t *self = MP_OBJ_TO_PTR(self_in);
-    mp_printf(print, "", self->name);
+    mp_printf(print, "", (qstr)self->name);
 }
 
-STATIC mp_obj_t type_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t type_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)type_in;
 
     mp_arg_check_num(n_args, n_kw, 1, 3, false);
@@ -982,7 +1044,7 @@ STATIC mp_obj_t type_make_new(const mp_obj_type_t *type_in, size_t n_args, size_
     }
 }
 
-STATIC mp_obj_t type_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t type_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     // instantiate an instance of a class
 
     mp_obj_type_t *self = MP_OBJ_TO_PTR(self_in);
@@ -1002,7 +1064,7 @@ STATIC mp_obj_t type_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp
     return o;
 }
 
-STATIC void type_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+static void type_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     assert(mp_obj_is_type(self_in, &mp_type_type));
     mp_obj_type_t *self = MP_OBJ_TO_PTR(self_in);
 
@@ -1104,7 +1166,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
     attr, type_attr
     );
 
-mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict) {
+static mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict) {
     // Verify input objects have expected type
     if (!mp_obj_is_type(bases_tuple, &mp_type_tuple)) {
         mp_raise_TypeError(NULL);
@@ -1155,7 +1217,7 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict)
     // (currently 10, plus 1 for base, plus 1 for base-protocol).
     // Note: mp_obj_type_t is (2 + 3 + #slots) words, so going from 11 to 12 slots
     // moves from 4 to 5 gc blocks.
-    mp_obj_type_t *o = m_new_obj_var0(mp_obj_type_t, void *, 10 + (bases_len ? 1 : 0) + (base_protocol ? 1 : 0));
+    mp_obj_type_t *o = m_new_obj_var0(mp_obj_type_t, slots, void *, 10 + (bases_len ? 1 : 0) + (base_protocol ? 1 : 0));
     o->base.type = &mp_type_type;
     o->flags = base_flags;
     o->name = name;
@@ -1192,20 +1254,38 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict)
         }
     }
 
+    #if MICROPY_PY_DESCRIPTORS
+    // To avoid any dynamic allocations when no __set_name__ exists,
+    // the head of this list is kept on the stack (marked blank with `next = NULL`).
+    setname_list_t setname_list = { .next = NULL };
+    setname_list_t *setname_tail = &setname_list;
+    #endif
+
     #if ENABLE_SPECIAL_ACCESSORS
-    // Check if the class has any special accessor methods
-    if (!(o->flags & MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS)) {
-        for (size_t i = 0; i < locals_ptr->map.alloc; i++) {
-            if (mp_map_slot_is_filled(&locals_ptr->map, i)) {
-                const mp_map_elem_t *elem = &locals_ptr->map.table[i];
-                if (check_for_special_accessors(elem->key, elem->value)) {
-                    o->flags |= MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS;
-                    break;
-                }
+    // Check if the class has any special accessor methods,
+    // and accumulate bound __set_name__ methods that need to be called
+    for (size_t i = 0; i < locals_ptr->map.alloc; i++) {
+        #if !MICROPY_PY_DESCRIPTORS
+        // __set_name__ needs to scan the entire locals map, can't early-terminate
+        if (o->flags & MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS) {
+            break;
+        }
+        #endif
+
+        if (mp_map_slot_is_filled(&locals_ptr->map, i)) {
+            const mp_map_elem_t *elem = &locals_ptr->map.table[i];
+
+            if (!(o->flags & MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS) // elidable when the early-termination check is enabled
+                && check_for_special_accessors(elem->key, elem->value)) {
+                o->flags |= MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS;
             }
+
+            #if MICROPY_PY_DESCRIPTORS
+            setname_tail = setname_maybe_bind_append(setname_tail, elem->key, elem->value);
+            #endif
         }
     }
-    #endif
+    #endif // ENABLE_SPECIAL_ACCESSORS
 
     const mp_obj_type_t *native_base;
     size_t num_native_bases = instance_count_native_bases(o, &native_base);
@@ -1213,8 +1293,7 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict)
         mp_raise_TypeError(MP_ERROR_TEXT("multiple bases have instance lay-out conflict"));
     }
 
-    mp_map_t *locals_map = &MP_OBJ_TYPE_GET_SLOT(o, locals_dict)->map;
-    mp_map_elem_t *elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(MP_QSTR___new__), MP_MAP_LOOKUP);
+    mp_map_elem_t *elem = mp_map_lookup(&locals_ptr->map, MP_OBJ_NEW_QSTR(MP_QSTR___new__), MP_MAP_LOOKUP);
     if (elem != NULL) {
         // __new__ slot exists; check if it is a function
         if (mp_obj_is_fun(elem->value)) {
@@ -1223,6 +1302,10 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict)
         }
     }
 
+    #if MICROPY_PY_DESCRIPTORS
+    setname_consume_call_all(&setname_list, MP_OBJ_FROM_PTR(o));
+    #endif
+
     return MP_OBJ_FROM_PTR(o);
 }
 
@@ -1235,7 +1318,7 @@ typedef struct _mp_obj_super_t {
     mp_obj_t obj;
 } mp_obj_super_t;
 
-STATIC void super_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void super_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_super_t *self = MP_OBJ_TO_PTR(self_in);
     mp_print_str(print, "");
 }
 
-STATIC mp_obj_t super_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t super_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     (void)type_in;
     // 0 arguments are turned into 2 in the compiler
     // 1 argument is not yet implemented
     mp_arg_check_num(n_args, n_kw, 2, 2, false);
-    if (!mp_obj_is_type(args[0], &mp_type_type)) {
+
+    // Per CPython: "If the second argument is an object, isinstance(obj, type) must be true.
+    // If the second argument is a type, issubclass(type2, type) must be true (this is useful for classmethods)."
+    const mp_obj_type_t *second_arg_type = mp_obj_get_type(args[1]);
+    mp_obj_t second_arg_obj = second_arg_type == &mp_type_type ? args[1] : MP_OBJ_FROM_PTR(second_arg_type);
+    if (mp_obj_is_subclass(second_arg_obj, args[0]) == mp_const_false) {
         mp_raise_TypeError(NULL);
     }
+
     mp_obj_super_t *o = m_new_obj(mp_obj_super_t);
     *o = (mp_obj_super_t) {{type_in}, args[0], args[1]};
     return MP_OBJ_FROM_PTR(o);
 }
 
-STATIC void super_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+static void super_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     if (dest[0] != MP_OBJ_NULL) {
         // not load attribute
         return;
@@ -1312,7 +1401,7 @@ STATIC void super_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     if (dest[0] != MP_OBJ_NULL) {
         if (dest[0] == MP_OBJ_SENTINEL) {
             // Looked up native __init__ so defer to it
-            dest[0] = MP_OBJ_FROM_PTR(&native_base_init_wrapper_obj);
+            dest[0] = MP_OBJ_FROM_PTR(&mp_native_base_init_wrapper_obj);
             dest[1] = self->obj;
         }
         return;
@@ -1386,7 +1475,7 @@ bool mp_obj_is_subclass_fast(mp_const_obj_t object, mp_const_obj_t classinfo) {
     }
 }
 
-STATIC mp_obj_t mp_obj_is_subclass(mp_obj_t object, mp_obj_t classinfo) {
+static mp_obj_t mp_obj_is_subclass(mp_obj_t object, mp_obj_t classinfo) {
     size_t len;
     mp_obj_t *items;
     if (mp_obj_is_type(classinfo, &mp_type_type)) {
@@ -1407,7 +1496,7 @@ STATIC mp_obj_t mp_obj_is_subclass(mp_obj_t object, mp_obj_t classinfo) {
     return mp_const_false;
 }
 
-STATIC mp_obj_t mp_builtin_issubclass(mp_obj_t object, mp_obj_t classinfo) {
+static mp_obj_t mp_builtin_issubclass(mp_obj_t object, mp_obj_t classinfo) {
     if (!mp_obj_is_type(object, &mp_type_type)) {
         mp_raise_TypeError(MP_ERROR_TEXT("issubclass() arg 1 must be a class"));
     }
@@ -1416,7 +1505,7 @@ STATIC mp_obj_t mp_builtin_issubclass(mp_obj_t object, mp_obj_t classinfo) {
 
 MP_DEFINE_CONST_FUN_OBJ_2(mp_builtin_issubclass_obj, mp_builtin_issubclass);
 
-STATIC mp_obj_t mp_builtin_isinstance(mp_obj_t object, mp_obj_t classinfo) {
+static mp_obj_t mp_builtin_isinstance(mp_obj_t object, mp_obj_t classinfo) {
     return mp_obj_is_subclass(MP_OBJ_FROM_PTR(mp_obj_get_type(object)), classinfo);
 }
 
@@ -1438,7 +1527,7 @@ mp_obj_t mp_obj_cast_to_native_base(mp_obj_t self_in, mp_const_obj_t native_type
 /******************************************************************************/
 // staticmethod and classmethod types (probably should go in a different file)
 
-STATIC mp_obj_t static_class_method_make_new(const mp_obj_type_t *self, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t static_class_method_make_new(const mp_obj_type_t *self, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     assert(self == &mp_type_staticmethod || self == &mp_type_classmethod);
 
     mp_arg_check_num(n_args, n_kw, 1, 1, false);
diff --git a/py/objtype.h b/py/objtype.h
index 839cc6d146b..8f6853ce1dc 100644
--- a/py/objtype.h
+++ b/py/objtype.h
@@ -52,4 +52,6 @@ mp_obj_t mp_obj_instance_call(mp_obj_t self_in, size_t n_args, size_t n_kw, cons
 // this needs to be exposed for mp_getiter
 mp_obj_t mp_obj_instance_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf);
 
+MP_DECLARE_CONST_FUN_OBJ_KW(mp_native_base_init_wrapper_obj);
+
 #endif // MICROPY_INCLUDED_PY_OBJTYPE_H
diff --git a/py/objzip.c b/py/objzip.c
index 3c3138180a2..300448b665f 100644
--- a/py/objzip.c
+++ b/py/objzip.c
@@ -36,10 +36,10 @@ typedef struct _mp_obj_zip_t {
     mp_obj_t iters[];
 } mp_obj_zip_t;
 
-STATIC mp_obj_t zip_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t zip_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     mp_arg_check_num(n_args, n_kw, 0, MP_OBJ_FUN_ARGS_MAX, false);
 
-    mp_obj_zip_t *o = mp_obj_malloc_var(mp_obj_zip_t, mp_obj_t, n_args, type);
+    mp_obj_zip_t *o = mp_obj_malloc_var(mp_obj_zip_t, iters, mp_obj_t, n_args, type);
     o->n_iters = n_args;
     for (size_t i = 0; i < n_args; i++) {
         o->iters[i] = mp_getiter(args[i], NULL);
@@ -47,22 +47,23 @@ STATIC mp_obj_t zip_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_
     return MP_OBJ_FROM_PTR(o);
 }
 
-STATIC mp_obj_t zip_iternext(mp_obj_t self_in) {
+static mp_obj_t zip_iternext(mp_obj_t self_in) {
     mp_check_self(mp_obj_is_type(self_in, &mp_type_zip));
     mp_obj_zip_t *self = MP_OBJ_TO_PTR(self_in);
-    if (self->n_iters == 0) {
-        return MP_OBJ_STOP_ITERATION;
-    }
-    mp_obj_tuple_t *tuple = MP_OBJ_TO_PTR(mp_obj_new_tuple(self->n_iters, NULL));
-
+    mp_obj_tuple_t *tuple = NULL;
     for (size_t i = 0; i < self->n_iters; i++) {
         mp_obj_t next = mp_iternext(self->iters[i]);
         if (next == MP_OBJ_STOP_ITERATION) {
-            mp_obj_tuple_del(MP_OBJ_FROM_PTR(tuple));
             return MP_OBJ_STOP_ITERATION;
         }
+        if (tuple == NULL) {
+            tuple = MP_OBJ_TO_PTR(mp_obj_new_tuple(self->n_iters, NULL));
+        }
         tuple->items[i] = next;
     }
+    if (tuple == NULL) {
+        return MP_OBJ_STOP_ITERATION;
+    }
     return MP_OBJ_FROM_PTR(tuple);
 }
 
diff --git a/py/opmethods.c b/py/opmethods.c
index c3931fd3584..32ab187b055 100644
--- a/py/opmethods.c
+++ b/py/opmethods.c
@@ -27,28 +27,28 @@
 #include "py/obj.h"
 #include "py/builtin.h"
 
-STATIC mp_obj_t op_getitem(mp_obj_t self_in, mp_obj_t key_in) {
+static mp_obj_t op_getitem(mp_obj_t self_in, mp_obj_t key_in) {
     const mp_obj_type_t *type = mp_obj_get_type(self_in);
     // Note: assumes type must have subscr (only used by dict).
     return MP_OBJ_TYPE_GET_SLOT(type, subscr)(self_in, key_in, MP_OBJ_SENTINEL);
 }
 MP_DEFINE_CONST_FUN_OBJ_2(mp_op_getitem_obj, op_getitem);
 
-STATIC mp_obj_t op_setitem(mp_obj_t self_in, mp_obj_t key_in, mp_obj_t value_in) {
+static mp_obj_t op_setitem(mp_obj_t self_in, mp_obj_t key_in, mp_obj_t value_in) {
     const mp_obj_type_t *type = mp_obj_get_type(self_in);
     // Note: assumes type must have subscr (only used by dict).
     return MP_OBJ_TYPE_GET_SLOT(type, subscr)(self_in, key_in, value_in);
 }
 MP_DEFINE_CONST_FUN_OBJ_3(mp_op_setitem_obj, op_setitem);
 
-STATIC mp_obj_t op_delitem(mp_obj_t self_in, mp_obj_t key_in) {
+static mp_obj_t op_delitem(mp_obj_t self_in, mp_obj_t key_in) {
     const mp_obj_type_t *type = mp_obj_get_type(self_in);
     // Note: assumes type must have subscr (only used by dict).
     return MP_OBJ_TYPE_GET_SLOT(type, subscr)(self_in, key_in, MP_OBJ_NULL);
 }
 MP_DEFINE_CONST_FUN_OBJ_2(mp_op_delitem_obj, op_delitem);
 
-STATIC mp_obj_t op_contains(mp_obj_t lhs_in, mp_obj_t rhs_in) {
+static mp_obj_t op_contains(mp_obj_t lhs_in, mp_obj_t rhs_in) {
     const mp_obj_type_t *type = mp_obj_get_type(lhs_in);
     // Note: assumes type must have binary_op (only used by set/frozenset).
     return MP_OBJ_TYPE_GET_SLOT(type, binary_op)(MP_BINARY_OP_CONTAINS, lhs_in, rhs_in);
diff --git a/py/parse.c b/py/parse.c
index 86198224fda..5937427f19e 100644
--- a/py/parse.c
+++ b/py/parse.c
@@ -75,7 +75,7 @@ enum {
 };
 
 // Define an array of actions corresponding to each rule
-STATIC const uint8_t rule_act_table[] = {
+static const uint8_t rule_act_table[] = {
 #define or(n)                   (RULE_ACT_OR | n)
 #define and(n)                  (RULE_ACT_AND | n)
 #define and_ident(n)            (RULE_ACT_AND | n | RULE_ACT_ALLOW_IDENT)
@@ -108,7 +108,7 @@ STATIC const uint8_t rule_act_table[] = {
 };
 
 // Define the argument data for each rule, as a combined array
-STATIC const uint16_t rule_arg_combined_table[] = {
+static const uint16_t rule_arg_combined_table[] = {
 #define tok(t)                  (RULE_ARG_TOK | MP_TOKEN_##t)
 #define rule(r)                 (RULE_ARG_RULE | RULE_##r)
 #define opt_rule(r)             (RULE_ARG_OPT_RULE | RULE_##r)
@@ -161,7 +161,7 @@ enum {
 // data, which indexes rule_arg_combined_table.  The offsets require 9 bits of
 // storage but only the lower 8 bits are stored here.  The 9th bit is computed
 // in get_rule_arg using the FIRST_RULE_WITH_OFFSET_ABOVE_255 constant.
-STATIC const uint8_t rule_arg_offset_table[] = {
+static const uint8_t rule_arg_offset_table[] = {
 #define DEF_RULE(rule, comp, kind, ...) RULE_ARG_OFFSET(rule, __VA_ARGS__) & 0xff,
 #define DEF_RULE_NC(rule, kind, ...)
 #include "py/grammar.h"
@@ -191,7 +191,7 @@ static const size_t FIRST_RULE_WITH_OFFSET_ABOVE_255 =
 
 #if MICROPY_DEBUG_PARSE_RULE_NAME
 // Define an array of rule names corresponding to each rule
-STATIC const char *const rule_name_table[] = {
+static const char *const rule_name_table[] = {
 #define DEF_RULE(rule, comp, kind, ...) #rule,
 #define DEF_RULE_NC(rule, kind, ...)
 #include "py/grammar.h"
@@ -242,9 +242,9 @@ typedef struct _parser_t {
     #endif
 } parser_t;
 
-STATIC void push_result_rule(parser_t *parser, size_t src_line, uint8_t rule_id, size_t num_args);
+static void push_result_rule(parser_t *parser, size_t src_line, uint8_t rule_id, size_t num_args);
 
-STATIC const uint16_t *get_rule_arg(uint8_t r_id) {
+static const uint16_t *get_rule_arg(uint8_t r_id) {
     size_t off = rule_arg_offset_table[r_id];
     if (r_id >= FIRST_RULE_WITH_OFFSET_ABOVE_255) {
         off |= 0x100;
@@ -252,7 +252,7 @@ STATIC const uint16_t *get_rule_arg(uint8_t r_id) {
     return &rule_arg_combined_table[off];
 }
 
-STATIC void *parser_alloc(parser_t *parser, size_t num_bytes) {
+static void *parser_alloc(parser_t *parser, size_t num_bytes) {
     // use a custom memory allocator to store parse nodes sequentially in large chunks
 
     mp_parse_chunk_t *chunk = parser->cur_chunk;
@@ -294,7 +294,7 @@ STATIC void *parser_alloc(parser_t *parser, size_t num_bytes) {
 }
 
 #if MICROPY_COMP_CONST_TUPLE
-STATIC void parser_free_parse_node_struct(parser_t *parser, mp_parse_node_struct_t *pns) {
+static void parser_free_parse_node_struct(parser_t *parser, mp_parse_node_struct_t *pns) {
     mp_parse_chunk_t *chunk = parser->cur_chunk;
     if (chunk->data <= (byte *)pns && (byte *)pns < chunk->data + chunk->union_.used) {
         size_t num_bytes = sizeof(mp_parse_node_struct_t) + sizeof(mp_parse_node_t) * MP_PARSE_NODE_STRUCT_NUM_NODES(pns);
@@ -303,7 +303,7 @@ STATIC void parser_free_parse_node_struct(parser_t *parser, mp_parse_node_struct
 }
 #endif
 
-STATIC void push_rule(parser_t *parser, size_t src_line, uint8_t rule_id, size_t arg_i) {
+static void push_rule(parser_t *parser, size_t src_line, uint8_t rule_id, size_t arg_i) {
     if (parser->rule_stack_top >= parser->rule_stack_alloc) {
         rule_stack_t *rs = m_renew(rule_stack_t, parser->rule_stack, parser->rule_stack_alloc, parser->rule_stack_alloc + MICROPY_ALLOC_PARSE_RULE_INC);
         parser->rule_stack = rs;
@@ -315,13 +315,13 @@ STATIC void push_rule(parser_t *parser, size_t src_line, uint8_t rule_id, size_t
     rs->arg_i = arg_i;
 }
 
-STATIC void push_rule_from_arg(parser_t *parser, size_t arg) {
+static void push_rule_from_arg(parser_t *parser, size_t arg) {
     assert((arg & RULE_ARG_KIND_MASK) == RULE_ARG_RULE || (arg & RULE_ARG_KIND_MASK) == RULE_ARG_OPT_RULE);
     size_t rule_id = arg & RULE_ARG_ARG_MASK;
     push_rule(parser, parser->lexer->tok_line, rule_id, 0);
 }
 
-STATIC uint8_t pop_rule(parser_t *parser, size_t *arg_i, size_t *src_line) {
+static uint8_t pop_rule(parser_t *parser, size_t *arg_i, size_t *src_line) {
     parser->rule_stack_top -= 1;
     uint8_t rule_id = parser->rule_stack[parser->rule_stack_top].rule_id;
     *arg_i = parser->rule_stack[parser->rule_stack_top].arg_i;
@@ -330,27 +330,43 @@ STATIC uint8_t pop_rule(parser_t *parser, size_t *arg_i, size_t *src_line) {
 }
 
 #if MICROPY_COMP_CONST_TUPLE
-STATIC uint8_t peek_rule(parser_t *parser, size_t n) {
+static uint8_t peek_rule(parser_t *parser, size_t n) {
     assert(parser->rule_stack_top > n);
     return parser->rule_stack[parser->rule_stack_top - 1 - n].rule_id;
 }
 #endif
 
-bool mp_parse_node_get_int_maybe(mp_parse_node_t pn, mp_obj_t *o) {
+#if MICROPY_COMP_CONST_FOLDING || MICROPY_EMIT_INLINE_ASM
+static bool mp_parse_node_get_number_maybe(mp_parse_node_t pn, mp_obj_t *o) {
     if (MP_PARSE_NODE_IS_SMALL_INT(pn)) {
         *o = MP_OBJ_NEW_SMALL_INT(MP_PARSE_NODE_LEAF_SMALL_INT(pn));
         return true;
     } else if (MP_PARSE_NODE_IS_STRUCT_KIND(pn, RULE_const_object)) {
         mp_parse_node_struct_t *pns = (mp_parse_node_struct_t *)pn;
         *o = mp_parse_node_extract_const_object(pns);
-        return mp_obj_is_int(*o);
+        return mp_obj_is_int(*o)
+               #if MICROPY_COMP_CONST_FLOAT
+               || mp_obj_is_float(*o)
+               #endif
+        ;
     } else {
         return false;
     }
 }
+#endif
+
+#if MICROPY_EMIT_INLINE_ASM
+bool mp_parse_node_get_int_maybe(mp_parse_node_t pn, mp_obj_t *o) {
+    return mp_parse_node_get_number_maybe(pn, o)
+           #if MICROPY_COMP_CONST_FLOAT
+           && mp_obj_is_int(*o)
+           #endif
+    ;
+}
+#endif
 
 #if MICROPY_COMP_CONST_TUPLE || MICROPY_COMP_CONST
-STATIC bool mp_parse_node_is_const(mp_parse_node_t pn) {
+static bool mp_parse_node_is_const(mp_parse_node_t pn) {
     if (MP_PARSE_NODE_IS_SMALL_INT(pn)) {
         // Small integer.
         return true;
@@ -377,7 +393,7 @@ STATIC bool mp_parse_node_is_const(mp_parse_node_t pn) {
     return false;
 }
 
-STATIC mp_obj_t mp_parse_node_convert_to_obj(mp_parse_node_t pn) {
+static mp_obj_t mp_parse_node_convert_to_obj(mp_parse_node_t pn) {
     assert(mp_parse_node_is_const(pn));
     if (MP_PARSE_NODE_IS_SMALL_INT(pn)) {
         mp_int_t arg = MP_PARSE_NODE_LEAF_SMALL_INT(pn);
@@ -419,7 +435,7 @@ STATIC mp_obj_t mp_parse_node_convert_to_obj(mp_parse_node_t pn) {
 }
 #endif
 
-STATIC bool parse_node_is_const_bool(mp_parse_node_t pn, bool value) {
+static bool parse_node_is_const_bool(mp_parse_node_t pn, bool value) {
     // Returns true if 'pn' is a constant whose boolean value is equivalent to 'value'
     #if MICROPY_COMP_CONST_TUPLE || MICROPY_COMP_CONST
     return mp_parse_node_is_const(pn) && mp_obj_is_true(mp_parse_node_convert_to_obj(pn)) == value;
@@ -475,10 +491,10 @@ void mp_parse_node_print(const mp_print_t *print, mp_parse_node_t pn, size_t ind
         uintptr_t arg = MP_PARSE_NODE_LEAF_ARG(pn);
         switch (MP_PARSE_NODE_LEAF_KIND(pn)) {
             case MP_PARSE_NODE_ID:
-                mp_printf(print, "id(%s)\n", qstr_str(arg));
+                mp_printf(print, "id(%q)\n", (qstr)arg);
                 break;
             case MP_PARSE_NODE_STRING:
-                mp_printf(print, "str(%s)\n", qstr_str(arg));
+                mp_printf(print, "str(%q)\n", (qstr)arg);
                 break;
             default:
                 assert(MP_PARSE_NODE_LEAF_KIND(pn) == MP_PARSE_NODE_TOKEN);
@@ -513,7 +529,7 @@ void mp_parse_node_print(const mp_print_t *print, mp_parse_node_t pn, size_t ind
 #endif // MICROPY_DEBUG_PRINTERS
 
 /*
-STATIC void result_stack_show(const mp_print_t *print, parser_t *parser) {
+static void result_stack_show(const mp_print_t *print, parser_t *parser) {
     mp_printf(print, "result stack, most recent first\n");
     for (ssize_t i = parser->result_stack_top - 1; i >= 0; i--) {
         mp_parse_node_print(print, parser->result_stack[i], 0);
@@ -521,17 +537,17 @@ STATIC void result_stack_show(const mp_print_t *print, parser_t *parser) {
 }
 */
 
-STATIC mp_parse_node_t pop_result(parser_t *parser) {
+static mp_parse_node_t pop_result(parser_t *parser) {
     assert(parser->result_stack_top > 0);
     return parser->result_stack[--parser->result_stack_top];
 }
 
-STATIC mp_parse_node_t peek_result(parser_t *parser, size_t pos) {
+static mp_parse_node_t peek_result(parser_t *parser, size_t pos) {
     assert(parser->result_stack_top > pos);
     return parser->result_stack[parser->result_stack_top - 1 - pos];
 }
 
-STATIC void push_result_node(parser_t *parser, mp_parse_node_t pn) {
+static void push_result_node(parser_t *parser, mp_parse_node_t pn) {
     if (parser->result_stack_top >= parser->result_stack_alloc) {
         mp_parse_node_t *stack = m_renew(mp_parse_node_t, parser->result_stack, parser->result_stack_alloc, parser->result_stack_alloc + MICROPY_ALLOC_PARSE_RESULT_INC);
         parser->result_stack = stack;
@@ -540,7 +556,7 @@ STATIC void push_result_node(parser_t *parser, mp_parse_node_t pn) {
     parser->result_stack[parser->result_stack_top++] = pn;
 }
 
-STATIC mp_parse_node_t make_node_const_object(parser_t *parser, size_t src_line, mp_obj_t obj) {
+static mp_parse_node_t make_node_const_object(parser_t *parser, size_t src_line, mp_obj_t obj) {
     mp_parse_node_struct_t *pn = parser_alloc(parser, sizeof(mp_parse_node_struct_t) + sizeof(mp_obj_t));
     pn->source_line = src_line;
     #if MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_D
@@ -557,7 +573,7 @@ STATIC mp_parse_node_t make_node_const_object(parser_t *parser, size_t src_line,
 
 // Create a parse node representing a constant object, possibly optimising the case of
 // an integer, by putting the (small) integer value directly in the parse node itself.
-STATIC mp_parse_node_t make_node_const_object_optimised(parser_t *parser, size_t src_line, mp_obj_t obj) {
+static mp_parse_node_t make_node_const_object_optimised(parser_t *parser, size_t src_line, mp_obj_t obj) {
     if (mp_obj_is_small_int(obj)) {
         mp_int_t val = MP_OBJ_SMALL_INT_VALUE(obj);
         #if MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_D
@@ -579,7 +595,7 @@ STATIC mp_parse_node_t make_node_const_object_optimised(parser_t *parser, size_t
     }
 }
 
-STATIC void push_result_token(parser_t *parser, uint8_t rule_id) {
+static void push_result_token(parser_t *parser, uint8_t rule_id) {
     mp_parse_node_t pn;
     mp_lexer_t *lex = parser->lexer;
     if (lex->tok_kind == MP_TOKEN_NAME) {
@@ -635,25 +651,40 @@ STATIC void push_result_token(parser_t *parser, uint8_t rule_id) {
 #if MICROPY_COMP_CONST_FOLDING
 
 #if MICROPY_COMP_MODULE_CONST
-STATIC const mp_rom_map_elem_t mp_constants_table[] = {
-    #if MICROPY_PY_UERRNO
-    { MP_ROM_QSTR(MP_QSTR_errno), MP_ROM_PTR(&mp_module_uerrno) },
+static const mp_rom_map_elem_t mp_constants_table[] = {
+    #if MICROPY_PY_ERRNO
+    { MP_ROM_QSTR(MP_QSTR_errno), MP_ROM_PTR(&mp_module_errno) },
     #endif
     #if MICROPY_PY_UCTYPES
     { MP_ROM_QSTR(MP_QSTR_uctypes), MP_ROM_PTR(&mp_module_uctypes) },
     #endif
+    #if MICROPY_PY_BUILTINS_FLOAT && MICROPY_PY_MATH && MICROPY_COMP_CONST_FLOAT
+    { MP_ROM_QSTR(MP_QSTR_math), MP_ROM_PTR(&mp_module_math) },
+    #endif
     // Extra constants as defined by a port
     MICROPY_PORT_CONSTANTS
 };
-STATIC MP_DEFINE_CONST_MAP(mp_constants_map, mp_constants_table);
+static MP_DEFINE_CONST_MAP(mp_constants_map, mp_constants_table);
 #endif
 
-#if MICROPY_COMP_CONST_FOLDING_COMPILER_WORKAROUND
-// Some versions of the xtensa-esp32-elf-gcc compiler generate wrong code if this
-// function is static, so provide a hook for them to work around this problem.
-MP_NOINLINE
-#endif
-STATIC bool fold_logical_constants(parser_t *parser, uint8_t rule_id, size_t *num_args) {
+static bool binary_op_maybe(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs, mp_obj_t *res) {
+    nlr_buf_t nlr;
+    if (nlr_push(&nlr) == 0) {
+        mp_obj_t tmp = mp_binary_op(op, lhs, rhs);
+        nlr_pop();
+        #if MICROPY_PY_BUILTINS_COMPLEX
+        if (mp_obj_is_type(tmp, &mp_type_complex)) {
+            return false;
+        }
+        #endif
+        *res = tmp;
+        return true;
+    } else {
+        return false;
+    }
+}
+
+static bool fold_logical_constants(parser_t *parser, uint8_t rule_id, size_t *num_args) {
     if (rule_id == RULE_or_test
         || rule_id == RULE_and_test) {
         // folding for binary logical ops: or and
@@ -710,8 +741,8 @@ STATIC bool fold_logical_constants(parser_t *parser, uint8_t rule_id, size_t *nu
     return false;
 }
 
-STATIC bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) {
-    // this code does folding of arbitrary integer expressions, eg 1 + 2 * 3 + 4
+static bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) {
+    // this code does folding of arbitrary numeric expressions, eg 1 + 2 * 3 + 4
     // it does not do partial folding, eg 1 + 2 + x -> 3 + x
 
     mp_obj_t arg0;
@@ -721,7 +752,7 @@ STATIC bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) {
         || rule_id == RULE_power) {
         // folding for binary ops: | ^ & **
         mp_parse_node_t pn = peek_result(parser, num_args - 1);
-        if (!mp_parse_node_get_int_maybe(pn, &arg0)) {
+        if (!mp_parse_node_get_number_maybe(pn, &arg0)) {
             return false;
         }
         mp_binary_op_t op;
@@ -737,58 +768,45 @@ STATIC bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) {
         for (ssize_t i = num_args - 2; i >= 0; --i) {
             pn = peek_result(parser, i);
             mp_obj_t arg1;
-            if (!mp_parse_node_get_int_maybe(pn, &arg1)) {
+            if (!mp_parse_node_get_number_maybe(pn, &arg1)) {
                 return false;
             }
-            if (op == MP_BINARY_OP_POWER && mp_obj_int_sign(arg1) < 0) {
-                // ** can't have negative rhs
+            if (!binary_op_maybe(op, arg0, arg1, &arg0)) {
                 return false;
             }
-            arg0 = mp_binary_op(op, arg0, arg1);
         }
     } else if (rule_id == RULE_shift_expr
                || rule_id == RULE_arith_expr
                || rule_id == RULE_term) {
         // folding for binary ops: << >> + - * @ / % //
         mp_parse_node_t pn = peek_result(parser, num_args - 1);
-        if (!mp_parse_node_get_int_maybe(pn, &arg0)) {
+        if (!mp_parse_node_get_number_maybe(pn, &arg0)) {
             return false;
         }
         for (ssize_t i = num_args - 2; i >= 1; i -= 2) {
             pn = peek_result(parser, i - 1);
             mp_obj_t arg1;
-            if (!mp_parse_node_get_int_maybe(pn, &arg1)) {
+            if (!mp_parse_node_get_number_maybe(pn, &arg1)) {
                 return false;
             }
             mp_token_kind_t tok = MP_PARSE_NODE_LEAF_ARG(peek_result(parser, i));
-            if (tok == MP_TOKEN_OP_AT || tok == MP_TOKEN_OP_SLASH) {
-                // Can't fold @ or /
-                return false;
-            }
             mp_binary_op_t op = MP_BINARY_OP_LSHIFT + (tok - MP_TOKEN_OP_DBL_LESS);
-            int rhs_sign = mp_obj_int_sign(arg1);
-            if (op <= MP_BINARY_OP_RSHIFT) {
-                // << and >> can't have negative rhs
-                if (rhs_sign < 0) {
-                    return false;
-                }
-            } else if (op >= MP_BINARY_OP_FLOOR_DIVIDE) {
-                // % and // can't have zero rhs
-                if (rhs_sign == 0) {
-                    return false;
-                }
+            if (!binary_op_maybe(op, arg0, arg1, &arg0)) {
+                return false;
             }
-            arg0 = mp_binary_op(op, arg0, arg1);
         }
     } else if (rule_id == RULE_factor_2) {
         // folding for unary ops: + - ~
         mp_parse_node_t pn = peek_result(parser, 0);
-        if (!mp_parse_node_get_int_maybe(pn, &arg0)) {
+        if (!mp_parse_node_get_number_maybe(pn, &arg0)) {
             return false;
         }
         mp_token_kind_t tok = MP_PARSE_NODE_LEAF_ARG(peek_result(parser, 1));
         mp_unary_op_t op;
         if (tok == MP_TOKEN_OP_TILDE) {
+            if (!mp_obj_is_int(arg0)) {
+                return false;
+            }
             op = MP_UNARY_OP_INVERT;
         } else {
             assert(tok == MP_TOKEN_OP_PLUS || tok == MP_TOKEN_OP_MINUS); // should be
@@ -802,50 +820,54 @@ STATIC bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) {
         if (!MP_PARSE_NODE_IS_NULL(pn1)
             && !(MP_PARSE_NODE_IS_STRUCT_KIND(pn1, RULE_expr_stmt_augassign)
                  || MP_PARSE_NODE_IS_STRUCT_KIND(pn1, RULE_expr_stmt_assign_list))) {
-            // this node is of the form  = 
+            // this node is of the form  =  or  :  = 
             mp_parse_node_t pn0 = peek_result(parser, 1);
-            if (MP_PARSE_NODE_IS_ID(pn0)
-                && MP_PARSE_NODE_IS_STRUCT_KIND(pn1, RULE_atom_expr_normal)
-                && MP_PARSE_NODE_IS_ID(((mp_parse_node_struct_t *)pn1)->nodes[0])
-                && MP_PARSE_NODE_LEAF_ARG(((mp_parse_node_struct_t *)pn1)->nodes[0]) == MP_QSTR_const
-                && MP_PARSE_NODE_IS_STRUCT_KIND(((mp_parse_node_struct_t *)pn1)->nodes[1], RULE_trailer_paren)
-                ) {
-                // code to assign dynamic constants: id = const(value)
-
-                // get the id
-                qstr id = MP_PARSE_NODE_LEAF_ARG(pn0);
-
-                // get the value
-                mp_parse_node_t pn_value = ((mp_parse_node_struct_t *)((mp_parse_node_struct_t *)pn1)->nodes[1])->nodes[0];
-                if (!mp_parse_node_is_const(pn_value)) {
-                    mp_obj_t exc = mp_obj_new_exception_msg(&mp_type_SyntaxError,
-                        MP_ERROR_TEXT("not a constant"));
-                    mp_obj_exception_add_traceback(exc, parser->lexer->source_name,
-                        ((mp_parse_node_struct_t *)pn1)->source_line, MP_QSTRnull);
-                    nlr_raise(exc);
-                }
-                mp_obj_t value = mp_parse_node_convert_to_obj(pn_value);
-
-                // store the value in the table of dynamic constants
-                mp_map_elem_t *elem = mp_map_lookup(&parser->consts, MP_OBJ_NEW_QSTR(id), MP_MAP_LOOKUP_ADD_IF_NOT_FOUND);
-                assert(elem->value == MP_OBJ_NULL);
-                elem->value = value;
-
-                // If the constant starts with an underscore then treat it as a private
-                // variable and don't emit any code to store the value to the id.
-                if (qstr_str(id)[0] == '_') {
-                    pop_result(parser); // pop const(value)
-                    pop_result(parser); // pop id
-                    push_result_rule(parser, 0, RULE_pass_stmt, 0); // replace with "pass"
-                    return true;
+            if (MP_PARSE_NODE_IS_ID(pn0)) {
+                if (MP_PARSE_NODE_IS_STRUCT_KIND(pn1, RULE_annassign)) {
+                    // extract annassign rhs
+                    pn1 = ((mp_parse_node_struct_t *)pn1)->nodes[1];
                 }
+                if (MP_PARSE_NODE_IS_STRUCT_KIND(pn1, RULE_atom_expr_normal)
+                    && MP_PARSE_NODE_IS_ID(((mp_parse_node_struct_t *)pn1)->nodes[0])
+                    && MP_PARSE_NODE_LEAF_ARG(((mp_parse_node_struct_t *)pn1)->nodes[0]) == MP_QSTR_const
+                    && MP_PARSE_NODE_IS_STRUCT_KIND(((mp_parse_node_struct_t *)pn1)->nodes[1], RULE_trailer_paren)) {
+                    // code to assign dynamic constants: id = const(value)
+
+                    // get the id
+                    qstr id = MP_PARSE_NODE_LEAF_ARG(pn0);
+
+                    // get the value
+                    mp_parse_node_t pn_value = ((mp_parse_node_struct_t *)((mp_parse_node_struct_t *)pn1)->nodes[1])->nodes[0];
+                    if (!mp_parse_node_is_const(pn_value)) {
+                        mp_obj_t exc = mp_obj_new_exception_msg(&mp_type_SyntaxError,
+                            MP_ERROR_TEXT("not a constant"));
+                        mp_obj_exception_add_traceback(exc, parser->lexer->source_name,
+                            ((mp_parse_node_struct_t *)pn1)->source_line, MP_QSTRnull);
+                        nlr_raise(exc);
+                    }
+                    mp_obj_t value = mp_parse_node_convert_to_obj(pn_value);
+
+                    // store the value in the table of dynamic constants
+                    mp_map_elem_t *elem = mp_map_lookup(&parser->consts, MP_OBJ_NEW_QSTR(id), MP_MAP_LOOKUP_ADD_IF_NOT_FOUND);
+                    assert(elem->value == MP_OBJ_NULL);
+                    elem->value = value;
+
+                    // If the constant starts with an underscore then treat it as a private
+                    // variable and don't emit any code to store the value to the id.
+                    if (qstr_str(id)[0] == '_') {
+                        pop_result(parser); // pop const(value)
+                        pop_result(parser); // pop id
+                        push_result_rule(parser, 0, RULE_pass_stmt, 0); // replace with "pass"
+                        return true;
+                    }
 
-                // replace const(value) with value
-                pop_result(parser);
-                push_result_node(parser, pn_value);
+                    // replace const(value) with value
+                    pop_result(parser);
+                    push_result_node(parser, pn_value);
 
-                // finished folding this assignment, but we still want it to be part of the tree
-                return false;
+                    // finished folding this assignment, but we still want it to be part of the tree
+                    return false;
+                }
             }
         }
         return false;
@@ -860,7 +882,7 @@ STATIC bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) {
             return false;
         }
         // id1.id2
-        // look it up in constant table, see if it can be replaced with an integer
+        // look it up in constant table, see if it can be replaced with an integer or a float
         mp_parse_node_struct_t *pns1 = (mp_parse_node_struct_t *)pn1;
         assert(MP_PARSE_NODE_IS_ID(pns1->nodes[0]));
         qstr q_base = MP_PARSE_NODE_LEAF_ARG(pn0);
@@ -871,7 +893,7 @@ STATIC bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) {
         }
         mp_obj_t dest[2];
         mp_load_method_maybe(elem->value, q_attr, dest);
-        if (!(dest[0] != MP_OBJ_NULL && mp_obj_is_int(dest[0]) && dest[1] == MP_OBJ_NULL)) {
+        if (!(dest[0] != MP_OBJ_NULL && (mp_obj_is_int(dest[0]) || mp_obj_is_float(dest[0])) && dest[1] == MP_OBJ_NULL)) {
             return false;
         }
         arg0 = dest[0];
@@ -894,7 +916,7 @@ STATIC bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) {
 #endif // MICROPY_COMP_CONST_FOLDING
 
 #if MICROPY_COMP_CONST_TUPLE
-STATIC bool build_tuple_from_stack(parser_t *parser, size_t src_line, size_t num_args) {
+static bool build_tuple_from_stack(parser_t *parser, size_t src_line, size_t num_args) {
     for (size_t i = num_args; i > 0;) {
         mp_parse_node_t pn = peek_result(parser, --i);
         if (!mp_parse_node_is_const(pn)) {
@@ -913,7 +935,7 @@ STATIC bool build_tuple_from_stack(parser_t *parser, size_t src_line, size_t num
     return true;
 }
 
-STATIC bool build_tuple(parser_t *parser, size_t src_line, uint8_t rule_id, size_t num_args) {
+static bool build_tuple(parser_t *parser, size_t src_line, uint8_t rule_id, size_t num_args) {
     if (rule_id == RULE_testlist_comp) {
         if (peek_rule(parser, 0) == RULE_atom_paren) {
             // Tuple of the form "(a,)".
@@ -946,7 +968,7 @@ STATIC bool build_tuple(parser_t *parser, size_t src_line, uint8_t rule_id, size
 }
 #endif
 
-STATIC void push_result_rule(parser_t *parser, size_t src_line, uint8_t rule_id, size_t num_args) {
+static void push_result_rule(parser_t *parser, size_t src_line, uint8_t rule_id, size_t num_args) {
     // Simplify and optimise certain rules, to reduce memory usage and simplify the compiler.
     if (rule_id == RULE_atom_paren) {
         // Remove parenthesis around a single expression if possible.
@@ -1023,6 +1045,9 @@ STATIC void push_result_rule(parser_t *parser, size_t src_line, uint8_t rule_id,
 }
 
 mp_parse_tree_t mp_parse(mp_lexer_t *lex, mp_parse_input_kind_t input_kind) {
+    // Set exception handler to free the lexer if an exception is raised.
+    MP_DEFINE_NLR_JUMP_CALLBACK_FUNCTION_1(ctx, mp_lexer_free, lex);
+    nlr_push_jump_callback(&ctx.callback, mp_call_function_1_from_nlr_jump_callback);
 
     // initialise parser and allocate memory for its stacks
 
@@ -1348,9 +1373,6 @@ mp_parse_tree_t mp_parse(mp_lexer_t *lex, mp_parse_input_kind_t input_kind) {
         } else if (lex->tok_kind == MP_TOKEN_MALFORMED_FSTRING) {
             exc = mp_obj_new_exception_msg(&mp_type_SyntaxError,
                 MP_ERROR_TEXT("malformed f-string"));
-        } else if (lex->tok_kind == MP_TOKEN_FSTRING_RAW) {
-            exc = mp_obj_new_exception_msg(&mp_type_SyntaxError,
-                MP_ERROR_TEXT("raw f-strings are not supported"));
         #endif
         } else {
             exc = mp_obj_new_exception_msg(&mp_type_SyntaxError,
@@ -1370,8 +1392,8 @@ mp_parse_tree_t mp_parse(mp_lexer_t *lex, mp_parse_input_kind_t input_kind) {
     m_del(rule_stack_t, parser.rule_stack, parser.rule_stack_alloc);
     m_del(mp_parse_node_t, parser.result_stack, parser.result_stack_alloc);
 
-    // we also free the lexer on behalf of the caller
-    mp_lexer_free(lex);
+    // Deregister exception handler and free the lexer.
+    nlr_pop_jump_callback(true);
 
     return parser.tree;
 }
@@ -1383,6 +1405,7 @@ void mp_parse_tree_clear(mp_parse_tree_t *tree) {
         m_del(byte, chunk, sizeof(mp_parse_chunk_t) + chunk->alloc);
         chunk = next;
     }
+    tree->chunk = NULL; // Avoid dangling pointer that may live on stack
 }
 
 #endif // MICROPY_ENABLE_COMPILER
diff --git a/py/parsenum.c b/py/parsenum.c
index d59715fdc93..05bcc0f0a70 100644
--- a/py/parsenum.c
+++ b/py/parsenum.c
@@ -28,6 +28,7 @@
 #include 
 
 #include "py/runtime.h"
+#include "py/misc.h"
 #include "py/parsenumbase.h"
 #include "py/parsenum.h"
 #include "py/smallint.h"
@@ -36,7 +37,7 @@
 #include 
 #endif
 
-STATIC NORETURN void raise_exc(mp_obj_t exc, mp_lexer_t *lex) {
+static MP_NORETURN void raise_exc(mp_obj_t exc, mp_lexer_t *lex) {
     // if lex!=NULL then the parser called us and we need to convert the
     // exception's type from ValueError to SyntaxError and add traceback info
     if (lex != NULL) {
@@ -46,6 +47,27 @@ STATIC NORETURN void raise_exc(mp_obj_t exc, mp_lexer_t *lex) {
     nlr_raise(exc);
 }
 
+#if MICROPY_LONGINT_IMPL != MICROPY_LONGINT_IMPL_LONGLONG
+// For the common small integer parsing case, we parse directly to mp_int_t and
+// check that the value doesn't overflow a smallint (in which case we fail over
+// to bigint parsing if supported)
+typedef mp_int_t parsed_int_t;
+
+#define PARSED_INT_MUL_OVERFLOW mp_mul_mp_int_t_overflow
+#define PARSED_INT_FITS MP_SMALL_INT_FITS
+#else
+// In the special case where bigint support is long long, we save code size by
+// parsing directly to long long and then return either a bigint or smallint
+// from the same result.
+//
+// To avoid pulling in (slow) signed 64-bit math routines we do the initial
+// parsing to an unsigned long long and only convert to signed at the end.
+typedef unsigned long long parsed_int_t;
+
+#define PARSED_INT_MUL_OVERFLOW mp_mul_ull_overflow
+#define PARSED_INT_FITS(I) ((I) <= (unsigned long long)LLONG_MAX + 1)
+#endif
+
 mp_obj_t mp_parse_num_integer(const char *restrict str_, size_t len, int base, mp_lexer_t *lex) {
     const byte *restrict str = (const byte *)str_;
     const byte *restrict top = str + len;
@@ -76,7 +98,7 @@ mp_obj_t mp_parse_num_integer(const char *restrict str_, size_t len, int base, m
     str += mp_parse_num_base((const char *)str, top - str, &base);
 
     // string should be an integer number
-    mp_int_t int_val = 0;
+    parsed_int_t parsed_val = 0;
     const byte *restrict str_val_start = str;
     for (; str < top; str++) {
         // get next digit as a value
@@ -98,25 +120,32 @@ mp_obj_t mp_parse_num_integer(const char *restrict str_, size_t len, int base, m
             break;
         }
 
-        // add next digi and check for overflow
-        if (mp_small_int_mul_overflow(int_val, base)) {
+        // add next digit and check for overflow
+        if (PARSED_INT_MUL_OVERFLOW(parsed_val, base, &parsed_val)) {
             goto overflow;
         }
-        int_val = int_val * base + dig;
-        if (!MP_SMALL_INT_FITS(int_val)) {
+        parsed_val += dig;
+        if (!PARSED_INT_FITS(parsed_val)) {
             goto overflow;
         }
     }
 
-    // negate value if needed
-    if (neg) {
-        int_val = -int_val;
+    #if MICROPY_LONGINT_IMPL != MICROPY_LONGINT_IMPL_LONGLONG
+    // The PARSED_INT_FITS check above ensures parsed_val fits in small int representation
+    ret_val = MP_OBJ_NEW_SMALL_INT(neg ? (-parsed_val) : parsed_val);
+have_ret_val:
+    #else
+    // The PARSED_INT_FITS check above ensures parsed_val won't overflow signed long long
+    long long signed_val = -parsed_val;
+    if (!neg) {
+        if (signed_val == LLONG_MIN) {
+            goto overflow;
+        }
+        signed_val = -signed_val;
     }
+    ret_val = mp_obj_new_int_from_ll(signed_val); // Could be large or small int
+    #endif
 
-    // create the small int
-    ret_val = MP_OBJ_NEW_SMALL_INT(int_val);
-
-have_ret_val:
     // check we parsed something
     if (str == str_val_start) {
         goto value_error;
@@ -135,6 +164,7 @@ mp_obj_t mp_parse_num_integer(const char *restrict str_, size_t len, int base, m
     return ret_val;
 
 overflow:
+    #if MICROPY_LONGINT_IMPL != MICROPY_LONGINT_IMPL_LONGLONG
     // reparse using long int
     {
         const char *s2 = (const char *)str_val_start;
@@ -142,6 +172,9 @@ mp_obj_t mp_parse_num_integer(const char *restrict str_, size_t len, int base, m
         str = (const byte *)s2;
         goto have_ret_val;
     }
+    #else
+    mp_raise_msg(&mp_type_OverflowError, MP_ERROR_TEXT("result overflows long long storage"));
+    #endif
 
 value_error:
     {
@@ -151,13 +184,13 @@ mp_obj_t mp_parse_num_integer(const char *restrict str_, size_t len, int base, m
         raise_exc(exc, lex);
         #elif MICROPY_ERROR_REPORTING == MICROPY_ERROR_REPORTING_NORMAL
         mp_obj_t exc = mp_obj_new_exception_msg_varg(&mp_type_ValueError,
-            MP_ERROR_TEXT("invalid syntax for integer with base %d"), base);
+            MP_ERROR_TEXT("invalid syntax for integer with base %d"), base == 1 ? 0 : base);
         raise_exc(exc, lex);
         #else
         vstr_t vstr;
         mp_print_t print;
         vstr_init_print(&vstr, 50, &print);
-        mp_printf(&print, "invalid syntax for integer with base %d: ", base);
+        mp_printf(&print, "invalid syntax for integer with base %d: ", base == 1 ? 0 : base);
         mp_str_print_quoted(&print, str_val_start, top - str_val_start, true);
         mp_obj_t exc = mp_obj_new_exception_arg1(&mp_type_ValueError,
             mp_obj_new_str_from_utf8_vstr(&vstr));
@@ -166,6 +199,8 @@ mp_obj_t mp_parse_num_integer(const char *restrict str_, size_t len, int base, m
     }
 }
 
+#if MICROPY_PY_BUILTINS_FLOAT
+
 enum {
     REAL_IMAG_STATE_START = 0,
     REAL_IMAG_STATE_HAVE_REAL = 1,
@@ -178,43 +213,173 @@ typedef enum {
     PARSE_DEC_IN_EXP,
 } parse_dec_in_t;
 
-#if MICROPY_PY_BUILTINS_FLOAT
-// DEC_VAL_MAX only needs to be rough and is used to retain precision while not overflowing
-// SMALL_NORMAL_VAL is the smallest power of 10 that is still a normal float
-// EXACT_POWER_OF_10 is the largest value of x so that 10^x can be stored exactly in a float
-//   Note: EXACT_POWER_OF_10 is at least floor(log_5(2^mantissa_length)). Indeed, 10^n = 2^n * 5^n
-//   so we only have to store the 5^n part in the mantissa (the 2^n part will go into the float's
-//   exponent).
+// MANTISSA_MAX is used to retain precision while not overflowing mantissa
+#define MANTISSA_MAX (sizeof(mp_large_float_uint_t) == 8 ? 0x1999999999999998ULL : 0x19999998U)
+
+// MAX_EXACT_POWER_OF_5 is the largest value of x so that 5^x can be stored exactly in a float
 #if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT
-#define DEC_VAL_MAX 1e20F
-#define SMALL_NORMAL_VAL (1e-37F)
-#define SMALL_NORMAL_EXP (-37)
-#define EXACT_POWER_OF_10 (9)
+#define MAX_EXACT_POWER_OF_5 (10)
 #elif MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_DOUBLE
-#define DEC_VAL_MAX 1e200
-#define SMALL_NORMAL_VAL (1e-307)
-#define SMALL_NORMAL_EXP (-307)
-#define EXACT_POWER_OF_10 (22)
+#define MAX_EXACT_POWER_OF_5 (22)
 #endif
 
+// Helper to compute `num * (10.0 ** dec_exp)`
+mp_large_float_t mp_decimal_exp(mp_large_float_t num, int dec_exp) {
+    if (dec_exp == 0 || num == (mp_large_float_t)(0.0)) {
+        return num;
+    }
+
+    #if MICROPY_FLOAT_FORMAT_IMPL == MICROPY_FLOAT_FORMAT_IMPL_EXACT
+
+    // If the assert below fails, it means you have chosen MICROPY_FLOAT_FORMAT_IMPL_EXACT
+    // manually on a platform where `larger floats` are not supported, which would
+    // result in inexact conversions. To fix this issue, change your `mpconfigport.h`
+    // and select MICROPY_FLOAT_FORMAT_IMPL_APPROX instead
+    assert(sizeof(mp_large_float_t) > sizeof(mp_float_t));
+
+    // Perform power using simple multiplications, to avoid
+    // dependency to higher-precision pow() function
+    int neg_exp = (dec_exp < 0);
+    if (neg_exp) {
+        dec_exp = -dec_exp;
+    }
+    mp_large_float_t res = num;
+    mp_large_float_t expo = (mp_large_float_t)10.0;
+    while (dec_exp) {
+        if (dec_exp & 1) {
+            if (neg_exp) {
+                res /= expo;
+            } else {
+                res *= expo;
+            }
+        }
+        dec_exp >>= 1;
+        if (dec_exp) {
+            expo *= expo;
+        }
+    }
+    return res;
+
+    #else
+    // MICROPY_FLOAT_FORMAT_IMPL != MICROPY_FLOAT_FORMAT_IMPL_EXACT
+
+    mp_float_union_t res = {num};
+    // Multiply first by (2.0 ** dec_exp) via the exponent
+    // - this will ensure that the result of `pow()` is always in mp_float_t range
+    //   when the result is expected to be in mp_float_t range (e.g. during format)
+    // - we don't need to care about p.exp overflow, as (5.0 ** dec_exp) will anyway
+    //   force the final result toward the proper edge if needed (0.0 or inf)
+    res.p.exp += dec_exp;
+    // Use positive exponents when they are more precise then negative
+    if (dec_exp < 0 && dec_exp >= -MAX_EXACT_POWER_OF_5) {
+        res.f /= MICROPY_FLOAT_C_FUN(pow)(5, -dec_exp);
+    } else {
+        res.f *= MICROPY_FLOAT_C_FUN(pow)(5, dec_exp);
+    }
+    return (mp_large_float_t)res.f;
+
+    #endif
+}
+
+
 // Break out inner digit accumulation routine to ease trailing zero deferral.
-static void accept_digit(mp_float_t *p_dec_val, int dig, int *p_exp_extra, int in) {
+static mp_large_float_uint_t accept_digit(mp_large_float_uint_t p_mantissa, unsigned int dig, int *p_exp_extra, int in) {
     // Core routine to ingest an additional digit.
-    if (*p_dec_val < DEC_VAL_MAX) {
+    if (p_mantissa < MANTISSA_MAX) {
         // dec_val won't overflow so keep accumulating
-        *p_dec_val = 10 * *p_dec_val + dig;
         if (in == PARSE_DEC_IN_FRAC) {
             --(*p_exp_extra);
         }
+        return 10u * p_mantissa + dig;
     } else {
         // dec_val might overflow and we anyway can't represent more digits
         // of precision, so ignore the digit and just adjust the exponent
         if (in == PARSE_DEC_IN_INTG) {
             ++(*p_exp_extra);
         }
+        return p_mantissa;
+    }
+}
+
+// Helper to parse an unsigned decimal number into a mp_float_t
+const char *mp_parse_float_internal(const char *str, size_t len, mp_float_t *res) {
+    const char *top = str + len;
+
+    parse_dec_in_t in = PARSE_DEC_IN_INTG;
+    bool exp_neg = false;
+    mp_large_float_uint_t mantissa = 0;
+    int exp_val = 0;
+    int exp_extra = 0;
+    int trailing_zeros_intg = 0, trailing_zeros_frac = 0;
+    while (str < top) {
+        unsigned int dig = *str++;
+        if ('0' <= dig && dig <= '9') {
+            dig -= '0';
+            if (in == PARSE_DEC_IN_EXP) {
+                // don't overflow exp_val when adding next digit, instead just truncate
+                // it and the resulting float will still be correct, either inf or 0.0
+                // (use INT_MAX/2 to allow adding exp_extra at the end without overflow)
+                if (exp_val < (INT_MAX / 2 - 9) / 10) {
+                    exp_val = 10 * exp_val + dig;
+                }
+            } else {
+                if (dig == 0 || mantissa >= MANTISSA_MAX) {
+                    // Defer treatment of zeros in fractional part.  If nothing comes afterwards, ignore them.
+                    // Also, once we reach MANTISSA_MAX, treat every additional digit as a trailing zero.
+                    if (in == PARSE_DEC_IN_INTG) {
+                        ++trailing_zeros_intg;
+                    } else {
+                        ++trailing_zeros_frac;
+                    }
+                } else {
+                    // Time to un-defer any trailing zeros.  Intg zeros first.
+                    while (trailing_zeros_intg) {
+                        mantissa = accept_digit(mantissa, 0, &exp_extra, PARSE_DEC_IN_INTG);
+                        --trailing_zeros_intg;
+                    }
+                    while (trailing_zeros_frac) {
+                        mantissa = accept_digit(mantissa, 0, &exp_extra, PARSE_DEC_IN_FRAC);
+                        --trailing_zeros_frac;
+                    }
+                    mantissa = accept_digit(mantissa, dig, &exp_extra, in);
+                }
+            }
+        } else if (in == PARSE_DEC_IN_INTG && dig == '.') {
+            in = PARSE_DEC_IN_FRAC;
+        } else if (in != PARSE_DEC_IN_EXP && ((dig | 0x20) == 'e')) {
+            in = PARSE_DEC_IN_EXP;
+            if (str < top) {
+                if (str[0] == '+') {
+                    str++;
+                } else if (str[0] == '-') {
+                    str++;
+                    exp_neg = true;
+                }
+            }
+            if (str == top) {
+                return NULL;
+            }
+        } else if (dig == '_') {
+            continue;
+        } else {
+            // unknown character
+            str--;
+            break;
+        }
     }
+
+    // work out the exponent
+    if (exp_neg) {
+        exp_val = -exp_val;
+    }
+    exp_val += exp_extra + trailing_zeros_intg;
+
+    // At this point, we just need to multiply the mantissa by its base 10 exponent.
+    *res = (mp_float_t)mp_decimal_exp(mantissa, exp_val);
+
+    return str;
 }
-#endif // MICROPY_BUILTINS_FLOAT
+#endif // MICROPY_PY_BUILTINS_FLOAT
 
 #if MICROPY_PY_BUILTINS_COMPLEX
 mp_obj_t mp_parse_num_decimal(const char *str, size_t len, bool allow_imag, bool force_complex, mp_lexer_t *lex)
@@ -226,13 +391,13 @@ mp_obj_t mp_parse_num_float(const char *str, size_t len, bool allow_imag, mp_lex
 
     const char *top = str + len;
     mp_float_t dec_val = 0;
-    bool dec_neg = false;
 
     #if MICROPY_PY_BUILTINS_COMPLEX
     unsigned int real_imag_state = REAL_IMAG_STATE_START;
     mp_float_t dec_real = 0;
-parse_start:
+parse_start:;
     #endif
+    bool dec_neg = false;
 
     // skip leading space
     for (; str < top && unichar_isspace(*str); str++) {
@@ -251,109 +416,23 @@ mp_obj_t mp_parse_num_float(const char *str, size_t len, bool allow_imag, mp_lex
     const char *str_val_start = str;
 
     // determine what the string is
-    if (str < top && (str[0] | 0x20) == 'i') {
-        // string starts with 'i', should be 'inf' or 'infinity' (case insensitive)
-        if (str + 2 < top && (str[1] | 0x20) == 'n' && (str[2] | 0x20) == 'f') {
-            // inf
-            str += 3;
-            dec_val = (mp_float_t)INFINITY;
-            if (str + 4 < top && (str[0] | 0x20) == 'i' && (str[1] | 0x20) == 'n' && (str[2] | 0x20) == 'i' && (str[3] | 0x20) == 't' && (str[4] | 0x20) == 'y') {
-                // infinity
-                str += 5;
-            }
-        }
-    } else if (str < top && (str[0] | 0x20) == 'n') {
-        // string starts with 'n', should be 'nan' (case insensitive)
-        if (str + 2 < top && (str[1] | 0x20) == 'a' && (str[2] | 0x20) == 'n') {
-            // NaN
-            str += 3;
-            dec_val = MICROPY_FLOAT_C_FUN(nan)("");
+    if (str + 2 < top && (str[0] | 0x20) == 'i' && (str[1] | 0x20) == 'n' && (str[2] | 0x20) == 'f') {
+        // 'inf' or 'infinity' (case insensitive)
+        str += 3;
+        dec_val = (mp_float_t)INFINITY;
+        if (str + 4 < top && (str[0] | 0x20) == 'i' && (str[1] | 0x20) == 'n' && (str[2] | 0x20) == 'i' && (str[3] | 0x20) == 't' && (str[4] | 0x20) == 'y') {
+            // infinity
+            str += 5;
         }
+    } else if (str + 2 < top && (str[0] | 0x20) == 'n' && (str[1] | 0x20) == 'a' && (str[2] | 0x20) == 'n') {
+        // 'nan' (case insensitive)
+        str += 3;
+        dec_val = MICROPY_FLOAT_C_FUN(nan)("");
     } else {
         // string should be a decimal number
-        parse_dec_in_t in = PARSE_DEC_IN_INTG;
-        bool exp_neg = false;
-        int exp_val = 0;
-        int exp_extra = 0;
-        int trailing_zeros_intg = 0, trailing_zeros_frac = 0;
-        while (str < top) {
-            unsigned int dig = *str++;
-            if ('0' <= dig && dig <= '9') {
-                dig -= '0';
-                if (in == PARSE_DEC_IN_EXP) {
-                    // don't overflow exp_val when adding next digit, instead just truncate
-                    // it and the resulting float will still be correct, either inf or 0.0
-                    // (use INT_MAX/2 to allow adding exp_extra at the end without overflow)
-                    if (exp_val < (INT_MAX / 2 - 9) / 10) {
-                        exp_val = 10 * exp_val + dig;
-                    }
-                } else {
-                    if (dig == 0 || dec_val >= DEC_VAL_MAX) {
-                        // Defer treatment of zeros in fractional part.  If nothing comes afterwards, ignore them.
-                        // Also, once we reach DEC_VAL_MAX, treat every additional digit as a trailing zero.
-                        if (in == PARSE_DEC_IN_INTG) {
-                            ++trailing_zeros_intg;
-                        } else {
-                            ++trailing_zeros_frac;
-                        }
-                    } else {
-                        // Time to un-defer any trailing zeros.  Intg zeros first.
-                        while (trailing_zeros_intg) {
-                            accept_digit(&dec_val, 0, &exp_extra, PARSE_DEC_IN_INTG);
-                            --trailing_zeros_intg;
-                        }
-                        while (trailing_zeros_frac) {
-                            accept_digit(&dec_val, 0, &exp_extra, PARSE_DEC_IN_FRAC);
-                            --trailing_zeros_frac;
-                        }
-                        accept_digit(&dec_val, dig, &exp_extra, in);
-                    }
-                }
-            } else if (in == PARSE_DEC_IN_INTG && dig == '.') {
-                in = PARSE_DEC_IN_FRAC;
-            } else if (in != PARSE_DEC_IN_EXP && ((dig | 0x20) == 'e')) {
-                in = PARSE_DEC_IN_EXP;
-                if (str < top) {
-                    if (str[0] == '+') {
-                        str++;
-                    } else if (str[0] == '-') {
-                        str++;
-                        exp_neg = true;
-                    }
-                }
-                if (str == top) {
-                    goto value_error;
-                }
-            } else if (dig == '_') {
-                continue;
-            } else {
-                // unknown character
-                str--;
-                break;
-            }
-        }
-
-        // work out the exponent
-        if (exp_neg) {
-            exp_val = -exp_val;
-        }
-
-        // apply the exponent, making sure it's not a subnormal value
-        exp_val += exp_extra + trailing_zeros_intg;
-        if (exp_val < SMALL_NORMAL_EXP) {
-            exp_val -= SMALL_NORMAL_EXP;
-            dec_val *= SMALL_NORMAL_VAL;
-        }
-
-        // At this point, we need to multiply the mantissa by its base 10 exponent. If possible,
-        // we would rather manipulate numbers that have an exact representation in IEEE754. It
-        // turns out small positive powers of 10 do, whereas small negative powers of 10 don't.
-        // So in that case, we'll yield a division of exact values rather than a multiplication
-        // of slightly erroneous values.
-        if (exp_val < 0 && exp_val >= -EXACT_POWER_OF_10) {
-            dec_val /= MICROPY_FLOAT_C_FUN(pow)(10, -exp_val);
-        } else {
-            dec_val *= MICROPY_FLOAT_C_FUN(pow)(10, exp_val);
+        str = mp_parse_float_internal(str, top - str, &dec_val);
+        if (!str) {
+            goto value_error;
         }
     }
 
diff --git a/py/parsenum.h b/py/parsenum.h
index f444632d230..d532cb194a5 100644
--- a/py/parsenum.h
+++ b/py/parsenum.h
@@ -34,6 +34,11 @@
 
 mp_obj_t mp_parse_num_integer(const char *restrict str, size_t len, int base, mp_lexer_t *lex);
 
+#if MICROPY_PY_BUILTINS_FLOAT
+mp_large_float_t mp_decimal_exp(mp_large_float_t num, int dec_exp);
+const char *mp_parse_float_internal(const char *str, size_t len, mp_float_t *res);
+#endif
+
 #if MICROPY_PY_BUILTINS_COMPLEX
 mp_obj_t mp_parse_num_decimal(const char *str, size_t len, bool allow_imag, bool force_complex, mp_lexer_t *lex);
 
diff --git a/py/parsenumbase.c b/py/parsenumbase.c
index 94523a666d3..fbf07a11958 100644
--- a/py/parsenumbase.c
+++ b/py/parsenumbase.c
@@ -30,35 +30,28 @@
 
 // find real radix base, and strip preceding '0x', '0o' and '0b'
 // puts base in *base, and returns number of bytes to skip the prefix
+// in base-0, puts 1 in *base to indicate a number that starts with 0, to provoke a
+// ValueError if it's not all-digits-zero.
 size_t mp_parse_num_base(const char *str, size_t len, int *base) {
     const byte *p = (const byte *)str;
     if (len <= 1) {
         goto no_prefix;
     }
     unichar c = *(p++);
-    if ((*base == 0 || *base == 16) && c == '0') {
-        c = *(p++);
-        if ((c | 32) == 'x') {
+    if (c == '0') {
+        c = *(p++) | 32;
+        int b = *base;
+        if (c == 'x' && (b == 0 || b == 16)) {
             *base = 16;
-        } else if (*base == 0 && (c | 32) == 'o') {
+        } else if (c == 'o' && (b == 0 || b == 8)) {
             *base = 8;
-        } else if (*base == 0 && (c | 32) == 'b') {
+        } else if (c == 'b' && (b == 0 || b == 2)) {
             *base = 2;
         } else {
-            if (*base == 0) {
-                *base = 10;
-            }
-            p -= 2;
-        }
-    } else if (*base == 8 && c == '0') {
-        c = *(p++);
-        if ((c | 32) != 'o') {
-            p -= 2;
-        }
-    } else if (*base == 2 && c == '0') {
-        c = *(p++);
-        if ((c | 32) != 'b') {
             p -= 2;
+            if (b == 0) {
+                *base = 1;
+            }
         }
     } else {
         p--;
diff --git a/py/persistentcode.c b/py/persistentcode.c
index fdc87d5cc8c..5a22287c6d9 100644
--- a/py/persistentcode.c
+++ b/py/persistentcode.c
@@ -40,6 +40,11 @@
 
 #include "py/smallint.h"
 
+// makeqstrdata.py has a fixed list of qstrs at the start that we can assume
+// are available with know indices on all MicroPython implementations, and
+// avoid needing to duplicate the string data in the .mpy file. This is the
+// last one in that list (anything with a qstr less than or equal to this is
+// assumed to be in the list).
 #define QSTR_LAST_STATIC MP_QSTR_zip
 
 #if MICROPY_DYNAMIC_COMPILER
@@ -58,16 +63,34 @@ typedef struct _bytecode_prelude_t {
     uint code_info_size;
 } bytecode_prelude_t;
 
+#if MICROPY_EMIT_RV32
+#include "py/asmrv32.h"
+#endif
+
 #endif // MICROPY_PERSISTENT_CODE_LOAD || MICROPY_PERSISTENT_CODE_SAVE
 
 #if MICROPY_PERSISTENT_CODE_LOAD
 
 #include "py/parsenum.h"
 
-STATIC int read_byte(mp_reader_t *reader);
-STATIC size_t read_uint(mp_reader_t *reader);
+static int read_byte(mp_reader_t *reader);
+static size_t read_uint(mp_reader_t *reader);
+
+#if MICROPY_EMIT_INLINE_ASM || MICROPY_ENABLE_NATIVE_CODE
+
+#if MICROPY_PERSISTENT_CODE_TRACK_FUN_DATA || MICROPY_PERSISTENT_CODE_TRACK_BSS_RODATA
+
+// An mp_obj_list_t that tracks native text/BSS/rodata to prevent the GC from reclaiming them.
+MP_REGISTER_ROOT_POINTER(mp_obj_t persistent_code_root_pointers);
 
-#if MICROPY_EMIT_MACHINE_CODE
+static void track_root_pointer(void *ptr) {
+    if (MP_STATE_PORT(persistent_code_root_pointers) == MP_OBJ_NULL) {
+        MP_STATE_PORT(persistent_code_root_pointers) = mp_obj_new_list(0, NULL);
+    }
+    mp_obj_list_append(MP_STATE_PORT(persistent_code_root_pointers), MP_OBJ_FROM_PTR(ptr));
+}
+
+#endif
 
 typedef struct _reloc_info_t {
     mp_reader_t *reader;
@@ -133,17 +156,17 @@ void mp_native_relocate(void *ri_in, uint8_t *text, uintptr_t reloc_text) {
 
 #endif
 
-STATIC int read_byte(mp_reader_t *reader) {
+static int read_byte(mp_reader_t *reader) {
     return reader->readbyte(reader->data);
 }
 
-STATIC void read_bytes(mp_reader_t *reader, byte *buf, size_t len) {
+static void read_bytes(mp_reader_t *reader, byte *buf, size_t len) {
     while (len-- > 0) {
         *buf++ = reader->readbyte(reader->data);
     }
 }
 
-STATIC size_t read_uint(mp_reader_t *reader) {
+static size_t read_uint(mp_reader_t *reader) {
     size_t unum = 0;
     for (;;) {
         byte b = reader->readbyte(reader->data);
@@ -155,13 +178,22 @@ STATIC size_t read_uint(mp_reader_t *reader) {
     return unum;
 }
 
-STATIC qstr load_qstr(mp_reader_t *reader) {
+static qstr load_qstr(mp_reader_t *reader) {
     size_t len = read_uint(reader);
     if (len & 1) {
         // static qstr
         return len >> 1;
     }
     len >>= 1;
+
+    #if MICROPY_VFS_ROM
+    // If possible, create the qstr from the memory-mapped string data.
+    const uint8_t *memmap = mp_reader_try_read_rom(reader, len + 1);
+    if (memmap != NULL) {
+        return qstr_from_strn_static((const char *)memmap, len);
+    }
+    #endif
+
     char *str = m_new(char, len);
     read_bytes(reader, (byte *)str, len);
     read_byte(reader); // read and discard null terminator
@@ -170,9 +202,27 @@ STATIC qstr load_qstr(mp_reader_t *reader) {
     return qst;
 }
 
-STATIC mp_obj_t load_obj(mp_reader_t *reader) {
+#if MICROPY_VFS_ROM
+// Create a str/bytes object that can forever reference the given data.
+static mp_obj_t mp_obj_new_str_static(const mp_obj_type_t *type, const byte *data, size_t len) {
+    if (type == &mp_type_str) {
+        qstr q = qstr_find_strn((const char *)data, len);
+        if (q != MP_QSTRnull) {
+            return MP_OBJ_NEW_QSTR(q);
+        }
+    }
+    assert(data[len] == '\0');
+    mp_obj_str_t *o = mp_obj_malloc(mp_obj_str_t, type);
+    o->len = len;
+    o->hash = qstr_compute_hash(data, len);
+    o->data = data;
+    return MP_OBJ_FROM_PTR(o);
+}
+#endif
+
+static mp_obj_t load_obj(mp_reader_t *reader) {
     byte obj_type = read_byte(reader);
-    #if MICROPY_EMIT_MACHINE_CODE
+    #if MICROPY_EMIT_INLINE_ASM || MICROPY_ENABLE_NATIVE_CODE
     if (obj_type == MP_PERSISTENT_OBJ_FUN_TABLE) {
         return MP_OBJ_FROM_PTR(&mp_fun_table);
     } else
@@ -187,6 +237,8 @@ STATIC mp_obj_t load_obj(mp_reader_t *reader) {
         return MP_OBJ_FROM_PTR(&mp_const_ellipsis_obj);
     } else {
         size_t len = read_uint(reader);
+
+        // Handle empty bytes object, and tuple objects.
         if (len == 0 && obj_type == MP_PERSISTENT_OBJ_BYTES) {
             read_byte(reader); // skip null terminator
             return mp_const_empty_bytes;
@@ -197,11 +249,31 @@ STATIC mp_obj_t load_obj(mp_reader_t *reader) {
             }
             return MP_OBJ_FROM_PTR(tuple);
         }
+
+        // Read in the object's data, either from ROM or into RAM.
+        const uint8_t *memmap = NULL;
         vstr_t vstr;
-        vstr_init_len(&vstr, len);
-        read_bytes(reader, (byte *)vstr.buf, len);
+        #if MICROPY_VFS_ROM
+        memmap = mp_reader_try_read_rom(reader, len);
+        vstr.buf = (void *)memmap;
+        vstr.len = len;
+        #endif
+        if (memmap == NULL) {
+            // Data could not be memory-mapped, so allocate it in RAM and read it in.
+            vstr_init_len(&vstr, len);
+            read_bytes(reader, (byte *)vstr.buf, len);
+        }
+
+        // Create and return the object.
         if (obj_type == MP_PERSISTENT_OBJ_STR || obj_type == MP_PERSISTENT_OBJ_BYTES) {
-            read_byte(reader); // skip null terminator
+            read_byte(reader); // skip null terminator (it needs to be there for ROM str objects)
+            #if MICROPY_VFS_ROM
+            if (memmap != NULL) {
+                // Create a str/bytes that references the memory-mapped data.
+                const mp_obj_type_t *t = obj_type == MP_PERSISTENT_OBJ_STR ? &mp_type_str : &mp_type_bytes;
+                return mp_obj_new_str_static(t, memmap, len);
+            }
+            #endif
             if (obj_type == MP_PERSISTENT_OBJ_STR) {
                 return mp_obj_new_str_from_utf8_vstr(&vstr);
             } else {
@@ -216,21 +288,21 @@ STATIC mp_obj_t load_obj(mp_reader_t *reader) {
     }
 }
 
-STATIC mp_raw_code_t *load_raw_code(mp_reader_t *reader, mp_module_context_t *context) {
+static mp_raw_code_t *load_raw_code(mp_reader_t *reader, mp_module_context_t *context) {
     // Load function kind and data length
     size_t kind_len = read_uint(reader);
     int kind = (kind_len & 3) + MP_CODE_BYTECODE;
     bool has_children = !!(kind_len & 4);
     size_t fun_data_len = kind_len >> 3;
 
-    #if !MICROPY_EMIT_MACHINE_CODE
+    #if !(MICROPY_EMIT_INLINE_ASM || MICROPY_ENABLE_NATIVE_CODE)
     if (kind != MP_CODE_BYTECODE) {
         mp_raise_ValueError(MP_ERROR_TEXT("incompatible .mpy file"));
     }
     #endif
 
     uint8_t *fun_data = NULL;
-    #if MICROPY_EMIT_MACHINE_CODE
+    #if MICROPY_EMIT_INLINE_ASM || MICROPY_ENABLE_NATIVE_CODE
     size_t prelude_offset = 0;
     mp_uint_t native_scope_flags = 0;
     mp_uint_t native_n_pos_args = 0;
@@ -238,12 +310,19 @@ STATIC mp_raw_code_t *load_raw_code(mp_reader_t *reader, mp_module_context_t *co
     #endif
 
     if (kind == MP_CODE_BYTECODE) {
-        // Allocate memory for the bytecode
-        fun_data = m_new(uint8_t, fun_data_len);
-        // Load bytecode
-        read_bytes(reader, fun_data, fun_data_len);
+        #if MICROPY_VFS_ROM
+        // Try to reference memory-mapped data for the bytecode.
+        fun_data = (uint8_t *)mp_reader_try_read_rom(reader, fun_data_len);
+        #endif
 
-    #if MICROPY_EMIT_MACHINE_CODE
+        if (fun_data == NULL) {
+            // Allocate memory for the bytecode.
+            fun_data = m_new(uint8_t, fun_data_len);
+            // Load bytecode.
+            read_bytes(reader, fun_data, fun_data_len);
+        }
+
+    #if MICROPY_EMIT_INLINE_ASM || MICROPY_ENABLE_NATIVE_CODE
     } else {
         // Allocate memory for native data and load it
         size_t fun_alloc;
@@ -270,7 +349,7 @@ STATIC mp_raw_code_t *load_raw_code(mp_reader_t *reader, mp_module_context_t *co
     size_t n_children = 0;
     mp_raw_code_t **children = NULL;
 
-    #if MICROPY_EMIT_MACHINE_CODE
+    #if MICROPY_EMIT_INLINE_ASM || MICROPY_ENABLE_NATIVE_CODE
     // Load optional BSS/rodata for viper.
     uint8_t *rodata = NULL;
     uint8_t *bss = NULL;
@@ -294,11 +373,10 @@ STATIC mp_raw_code_t *load_raw_code(mp_reader_t *reader, mp_module_context_t *co
                 read_bytes(reader, rodata, rodata_size);
             }
 
-            // Viper code with BSS/rodata should not have any children.
-            // Reuse the children pointer to reference the BSS/rodata
-            // memory so that it is not reclaimed by the GC.
-            assert(!has_children);
-            children = (void *)data;
+            #if MICROPY_PERSISTENT_CODE_TRACK_BSS_RODATA
+            // Track the BSS/rodata memory so it's not reclaimed by the GC.
+            track_root_pointer(data);
+            #endif
         }
     }
     #endif
@@ -319,18 +397,16 @@ STATIC mp_raw_code_t *load_raw_code(mp_reader_t *reader, mp_module_context_t *co
         MP_BC_PRELUDE_SIG_DECODE(ip);
         // Assign bytecode to raw code object
         mp_emit_glue_assign_bytecode(rc, fun_data,
-            #if MICROPY_PERSISTENT_CODE_SAVE || MICROPY_DEBUG_PRINTERS
-            fun_data_len,
-            #endif
             children,
             #if MICROPY_PERSISTENT_CODE_SAVE
+            fun_data_len,
             n_children,
             #endif
             scope_flags);
 
-    #if MICROPY_EMIT_MACHINE_CODE
+    #if MICROPY_EMIT_INLINE_ASM || MICROPY_ENABLE_NATIVE_CODE
     } else {
-        const uint8_t *prelude_ptr;
+        const uint8_t *prelude_ptr = NULL;
         #if MICROPY_EMIT_NATIVE_PRELUDE_SEPARATE_FROM_MACHINE_CODE
         if (kind == MP_CODE_NATIVE_PY) {
             // Executable code cannot be accessed byte-wise on this architecture, so copy
@@ -343,22 +419,17 @@ STATIC mp_raw_code_t *load_raw_code(mp_reader_t *reader, mp_module_context_t *co
 
         // Relocate and commit code to executable address space
         reloc_info_t ri = {reader, context, rodata, bss};
+        #if MICROPY_PERSISTENT_CODE_TRACK_FUN_DATA
+        if (native_scope_flags & MP_SCOPE_FLAG_VIPERRELOC) {
+            // Track the function data memory so it's not reclaimed by the GC.
+            track_root_pointer(fun_data);
+        }
+        #endif
         #if defined(MP_PLAT_COMMIT_EXEC)
         void *opt_ri = (native_scope_flags & MP_SCOPE_FLAG_VIPERRELOC) ? &ri : NULL;
         fun_data = MP_PLAT_COMMIT_EXEC(fun_data, fun_data_len, opt_ri);
         #else
         if (native_scope_flags & MP_SCOPE_FLAG_VIPERRELOC) {
-            #if MICROPY_PERSISTENT_CODE_TRACK_RELOC_CODE
-            // If native code needs relocations then it's not guaranteed that a pointer to
-            // the head of `buf` (containing the machine code) will be retained for the GC
-            // to trace.  This is because native functions can start inside `buf` and so
-            // it's possible that the only GC-reachable pointers are pointers inside `buf`.
-            // So put this `buf` on a list of reachable root pointers.
-            if (MP_STATE_PORT(track_reloc_code_list) == MP_OBJ_NULL) {
-                MP_STATE_PORT(track_reloc_code_list) = mp_obj_new_list(0, NULL);
-            }
-            mp_obj_list_append(MP_STATE_PORT(track_reloc_code_list), MP_OBJ_FROM_PTR(fun_data));
-            #endif
             // Do the relocations.
             mp_native_relocate(&ri, fun_data, (uintptr_t)fun_data);
         }
@@ -391,6 +462,10 @@ STATIC mp_raw_code_t *load_raw_code(mp_reader_t *reader, mp_module_context_t *co
 }
 
 void mp_raw_code_load(mp_reader_t *reader, mp_compiled_module_t *cm) {
+    // Set exception handler to close the reader if an exception is raised.
+    MP_DEFINE_NLR_JUMP_CALLBACK_FUNCTION_1(ctx, reader->close, reader->data);
+    nlr_push_jump_callback(&ctx.callback, mp_call_function_1_from_nlr_jump_callback);
+
     byte header[4];
     read_bytes(reader, header, sizeof(header));
     byte arch = MPY_FEATURE_DECODE_ARCH(header[2]);
@@ -400,7 +475,7 @@ void mp_raw_code_load(mp_reader_t *reader, mp_compiled_module_t *cm) {
         || header[3] > MP_SMALL_INT_BITS) {
         mp_raise_ValueError(MP_ERROR_TEXT("incompatible .mpy file"));
     }
-    if (MPY_FEATURE_DECODE_ARCH(header[2]) != MP_NATIVE_ARCH_NONE) {
+    if (arch != MP_NATIVE_ARCH_NONE) {
         if (!MPY_FEATURE_ARCH_TEST(arch)) {
             if (MPY_FEATURE_ARCH_TEST(MP_NATIVE_ARCH_NONE)) {
                 // On supported ports this can be resolved by enabling feature, eg
@@ -412,6 +487,23 @@ void mp_raw_code_load(mp_reader_t *reader, mp_compiled_module_t *cm) {
         }
     }
 
+    size_t arch_flags = 0;
+    if (MPY_FEATURE_ARCH_FLAGS_TEST(header[2])) {
+        #if MICROPY_EMIT_RV32
+        arch_flags = read_uint(reader);
+
+        if (MPY_FEATURE_ARCH_TEST(MP_NATIVE_ARCH_RV32IMC)) {
+            if ((arch_flags & (size_t)asm_rv32_allowed_extensions()) != arch_flags) {
+                mp_raise_ValueError(MP_ERROR_TEXT("incompatible .mpy file"));
+            }
+        } else
+        #endif
+        {
+            (void)arch_flags;
+            mp_raise_ValueError(MP_ERROR_TEXT("incompatible .mpy file"));
+        }
+    }
+
     size_t n_qstr = read_uint(reader);
     size_t n_obj = read_uint(reader);
     mp_module_context_alloc_tables(cm->context, n_qstr, n_obj);
@@ -433,9 +525,11 @@ void mp_raw_code_load(mp_reader_t *reader, mp_compiled_module_t *cm) {
     cm->has_native = MPY_FEATURE_DECODE_ARCH(header[2]) != MP_NATIVE_ARCH_NONE;
     cm->n_qstr = n_qstr;
     cm->n_obj = n_obj;
+    cm->arch_flags = arch_flags;
     #endif
 
-    reader->close(reader->data);
+    // Deregister exception handler and close the reader.
+    nlr_pop_jump_callback(true);
 }
 
 void mp_raw_code_load_mem(const byte *buf, size_t len, mp_compiled_module_t *context) {
@@ -446,7 +540,7 @@ void mp_raw_code_load_mem(const byte *buf, size_t len, mp_compiled_module_t *con
 
 #if MICROPY_HAS_FILE_READER
 
-void mp_raw_code_load_file(const char *filename, mp_compiled_module_t *context) {
+void mp_raw_code_load_file(qstr filename, mp_compiled_module_t *context) {
     mp_reader_t reader;
     mp_reader_new_file(&reader, filename);
     mp_raw_code_load(&reader, context);
@@ -456,17 +550,16 @@ void mp_raw_code_load_file(const char *filename, mp_compiled_module_t *context)
 
 #endif // MICROPY_PERSISTENT_CODE_LOAD
 
-#if MICROPY_PERSISTENT_CODE_SAVE
+#if MICROPY_PERSISTENT_CODE_SAVE || MICROPY_PERSISTENT_CODE_SAVE_FUN
 
 #include "py/objstr.h"
 
-STATIC void mp_print_bytes(mp_print_t *print, const byte *data, size_t len) {
+static void mp_print_bytes(mp_print_t *print, const byte *data, size_t len) {
     print->print_strn(print->data, (const char *)data, len);
 }
 
-#define BYTES_FOR_INT ((MP_BYTES_PER_OBJ_WORD * 8 + 6) / 7)
-STATIC void mp_print_uint(mp_print_t *print, size_t n) {
-    byte buf[BYTES_FOR_INT];
+static void mp_print_uint(mp_print_t *print, size_t n) {
+    byte buf[MP_ENCODE_UINT_MAX_BYTES];
     byte *p = buf + sizeof(buf);
     *--p = n & 0x7f;
     n >>= 7;
@@ -476,7 +569,7 @@ STATIC void mp_print_uint(mp_print_t *print, size_t n) {
     print->print_strn(print->data, (char *)p, buf + sizeof(buf) - p);
 }
 
-STATIC void save_qstr(mp_print_t *print, qstr qst) {
+static void save_qstr(mp_print_t *print, qstr qst) {
     if (qst <= QSTR_LAST_STATIC) {
         // encode static qstr
         mp_print_uint(print, qst << 1 | 1);
@@ -488,7 +581,7 @@ STATIC void save_qstr(mp_print_t *print, qstr qst) {
     mp_print_bytes(print, str, len + 1); // +1 to store null terminator
 }
 
-STATIC void save_obj(mp_print_t *print, mp_obj_t o) {
+static void save_obj(mp_print_t *print, mp_obj_t o) {
     #if MICROPY_EMIT_MACHINE_CODE
     if (o == MP_OBJ_FROM_PTR(&mp_fun_table)) {
         byte obj_type = MP_PERSISTENT_OBJ_FUN_TABLE;
@@ -554,7 +647,11 @@ STATIC void save_obj(mp_print_t *print, mp_obj_t o) {
     }
 }
 
-STATIC void save_raw_code(mp_print_t *print, const mp_raw_code_t *rc) {
+#endif // MICROPY_PERSISTENT_CODE_SAVE || MICROPY_PERSISTENT_CODE_SAVE_FUN
+
+#if MICROPY_PERSISTENT_CODE_SAVE
+
+static void save_raw_code(mp_print_t *print, const mp_raw_code_t *rc) {
     // Save function kind and data length
     mp_print_uint(print, (rc->fun_data_len << 3) | ((rc->n_children != 0) << 2) | (rc->kind - MP_CODE_BYTECODE));
 
@@ -567,11 +664,15 @@ STATIC void save_raw_code(mp_print_t *print, const mp_raw_code_t *rc) {
         mp_print_uint(print, rc->prelude_offset);
     } else if (rc->kind == MP_CODE_NATIVE_VIPER || rc->kind == MP_CODE_NATIVE_ASM) {
         // Save basic scope info for viper and asm
-        mp_print_uint(print, rc->scope_flags & MP_SCOPE_FLAG_ALL_SIG);
+        // Viper/asm functions don't support generator, variable args, or default keyword args
+        // so (scope_flags & MP_SCOPE_FLAG_ALL_SIG) for these functions is always 0.
+        mp_print_uint(print, 0);
+        #if MICROPY_EMIT_INLINE_ASM
         if (rc->kind == MP_CODE_NATIVE_ASM) {
-            mp_print_uint(print, rc->n_pos_args);
-            mp_print_uint(print, rc->type_sig);
+            mp_print_uint(print, rc->asm_n_pos_args);
+            mp_print_uint(print, rc->asm_type_sig);
         }
+        #endif
     }
     #endif
 
@@ -592,7 +693,7 @@ void mp_raw_code_save(mp_compiled_module_t *cm, mp_print_t *print) {
     byte header[4] = {
         'M',
         MPY_VERSION,
-        cm->has_native ? MPY_FEATURE_ENCODE_SUB_VERSION(MPY_SUB_VERSION) | MPY_FEATURE_ENCODE_ARCH(MPY_FEATURE_ARCH_DYNAMIC) : 0,
+        (cm->arch_flags != 0 ? MPY_FEATURE_ARCH_FLAGS : 0) | (cm->has_native ? MPY_FEATURE_ENCODE_SUB_VERSION(MPY_SUB_VERSION) | MPY_FEATURE_ENCODE_ARCH(MPY_FEATURE_ARCH_DYNAMIC) : 0),
         #if MICROPY_DYNAMIC_COMPILER
         mp_dynamic_compiler.small_int_bits,
         #else
@@ -601,6 +702,10 @@ void mp_raw_code_save(mp_compiled_module_t *cm, mp_print_t *print) {
     };
     mp_print_bytes(print, header, sizeof(header));
 
+    if (cm->arch_flags) {
+        mp_print_uint(print, cm->arch_flags);
+    }
+
     // Number of entries in constant table.
     mp_print_uint(print, cm->n_qstr);
     mp_print_uint(print, cm->n_obj);
@@ -619,13 +724,15 @@ void mp_raw_code_save(mp_compiled_module_t *cm, mp_print_t *print) {
     save_raw_code(print, cm->rc);
 }
 
+#endif // MICROPY_PERSISTENT_CODE_SAVE
+
 #if MICROPY_PERSISTENT_CODE_SAVE_FILE
 
 #include 
 #include 
 #include 
 
-STATIC void fd_print_strn(void *env, const char *str, size_t len) {
+static void fd_print_strn(void *env, const char *str, size_t len) {
     int fd = (intptr_t)env;
     MP_THREAD_GIL_EXIT();
     ssize_t ret = write(fd, str, len);
@@ -633,12 +740,12 @@ STATIC void fd_print_strn(void *env, const char *str, size_t len) {
     (void)ret;
 }
 
-void mp_raw_code_save_file(mp_compiled_module_t *cm, const char *filename) {
+void mp_raw_code_save_file(mp_compiled_module_t *cm, qstr filename) {
     MP_THREAD_GIL_EXIT();
-    int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+    int fd = open(qstr_str(filename), O_WRONLY | O_CREAT | O_TRUNC, 0644);
     MP_THREAD_GIL_ENTER();
     if (fd < 0) {
-        mp_raise_OSError_with_filename(errno, filename);
+        mp_raise_OSError_with_filename(errno, qstr_str(filename));
     }
     mp_print_t fd_print = {(void *)(intptr_t)fd, fd_print_strn};
     mp_raw_code_save(cm, &fd_print);
@@ -649,7 +756,245 @@ void mp_raw_code_save_file(mp_compiled_module_t *cm, const char *filename) {
 
 #endif // MICROPY_PERSISTENT_CODE_SAVE_FILE
 
-#endif // MICROPY_PERSISTENT_CODE_SAVE
+#if MICROPY_PERSISTENT_CODE_SAVE_FUN
+
+#include "py/bc0.h"
+#include "py/objfun.h"
+#include "py/smallint.h"
+#include "py/gc.h"
+
+#define MP_BC_OPCODE_HAS_SIGNED_OFFSET(opcode) (MP_BC_UNWIND_JUMP <= (opcode) && (opcode) <= MP_BC_POP_JUMP_IF_FALSE)
+
+typedef struct _bit_vector_t {
+    size_t max_bit_set;
+    size_t alloc;
+    uintptr_t *bits;
+} bit_vector_t;
+
+static void bit_vector_init(bit_vector_t *self) {
+    self->max_bit_set = 0;
+    self->alloc = 1;
+    self->bits = m_new(uintptr_t, self->alloc);
+}
+
+static void bit_vector_clear(bit_vector_t *self) {
+    m_del(uintptr_t, self->bits, self->alloc);
+}
+
+static bool bit_vector_is_set(bit_vector_t *self, size_t index) {
+    const size_t bits_size = sizeof(*self->bits) * MP_BITS_PER_BYTE;
+    return index / bits_size < self->alloc
+           && (self->bits[index / bits_size] & ((uintptr_t)1 << (index % bits_size))) != 0;
+}
+
+static void bit_vector_set(bit_vector_t *self, size_t index) {
+    const size_t bits_size = sizeof(*self->bits) * MP_BITS_PER_BYTE;
+    self->max_bit_set = MAX(self->max_bit_set, index);
+    if (index / bits_size >= self->alloc) {
+        size_t new_alloc = self->alloc * 2;
+        self->bits = m_renew(uintptr_t, self->bits, self->alloc, new_alloc);
+        self->alloc = new_alloc;
+    }
+    self->bits[index / bits_size] |= (uintptr_t)1 << (index % bits_size);
+}
+
+typedef struct _mp_opcode_t {
+    uint8_t opcode;
+    uint8_t format;
+    uint8_t size;
+    mp_int_t arg;
+    uint8_t extra_arg;
+} mp_opcode_t;
+
+static mp_opcode_t mp_opcode_decode(const uint8_t *ip) {
+    const uint8_t *ip_start = ip;
+    uint8_t opcode = *ip++;
+    uint8_t opcode_format = MP_BC_FORMAT(opcode);
+    mp_uint_t arg = 0;
+    uint8_t extra_arg = 0;
+    if (opcode_format == MP_BC_FORMAT_QSTR || opcode_format == MP_BC_FORMAT_VAR_UINT) {
+        arg = *ip & 0x7f;
+        if (opcode == MP_BC_LOAD_CONST_SMALL_INT && (arg & 0x40) != 0) {
+            arg |= (mp_uint_t)(-1) << 7;
+        }
+        while ((*ip & 0x80) != 0) {
+            arg = (arg << 7) | (*++ip & 0x7f);
+        }
+        ++ip;
+    } else if (opcode_format == MP_BC_FORMAT_OFFSET) {
+        if ((*ip & 0x80) == 0) {
+            arg = *ip++;
+            if (MP_BC_OPCODE_HAS_SIGNED_OFFSET(opcode)) {
+                arg -= 0x40;
+            }
+        } else {
+            arg = (ip[0] & 0x7f) | (ip[1] << 7);
+            ip += 2;
+            if (MP_BC_OPCODE_HAS_SIGNED_OFFSET(opcode)) {
+                arg -= 0x4000;
+            }
+        }
+    }
+    if ((opcode & MP_BC_MASK_EXTRA_BYTE) == 0) {
+        extra_arg = *ip++;
+    }
+
+    mp_opcode_t op = { opcode, opcode_format, ip - ip_start, arg, extra_arg };
+    return op;
+}
+
+typedef struct _mp_raw_code_simplified_t {
+    const uint8_t *fun_data;
+    struct _mp_raw_code_simplified_t *children;
+    size_t fun_data_len;
+    size_t n_children;
+} mp_raw_code_simplified_t;
+
+static void proto_fun_to_raw_code_simplified(const void *proto_fun, bit_vector_t *qstr_table_used, bit_vector_t *obj_table_used, mp_raw_code_simplified_t *rcs) {
+    const uint8_t *fun_data;
+    mp_raw_code_t **children;
+    if (mp_proto_fun_is_bytecode(proto_fun)) {
+        fun_data = proto_fun;
+        children = NULL;
+    } else {
+        const mp_raw_code_t *rc = proto_fun;
+        if (rc->kind != MP_CODE_BYTECODE) {
+            mp_raise_ValueError(MP_ERROR_TEXT("function must be bytecode"));
+        }
+        fun_data = rc->fun_data;
+        children = rc->children;
+    }
+
+    const uint8_t *fun_data_top = fun_data + gc_nbytes(fun_data);
+
+    // Extract function information.
+    const byte *ip = fun_data;
+    MP_BC_PRELUDE_SIG_DECODE(ip);
+    MP_BC_PRELUDE_SIZE_DECODE(ip);
+
+    const byte *ip_names = ip;
+    mp_uint_t simple_name = mp_decode_uint(&ip_names);
+    bit_vector_set(qstr_table_used, simple_name);
+    for (size_t i = 0; i < n_pos_args + n_kwonly_args; ++i) {
+        mp_uint_t arg_name = mp_decode_uint(&ip_names);
+        bit_vector_set(qstr_table_used, arg_name);
+    }
+
+    // Skip pass source code info and cell info.
+    // Then ip points to the start of the opcodes.
+    ip += n_info + n_cell;
+
+    // Decode bytecode.
+    size_t n_children = 0;
+    while (ip < fun_data_top) {
+        mp_opcode_t op = mp_opcode_decode(ip);
+        if (op.opcode == MP_BC_BASE_RESERVED) {
+            // End of opcodes.
+            fun_data_top = ip;
+        } else if (op.format == MP_BC_FORMAT_QSTR) {
+            bit_vector_set(qstr_table_used, op.arg);
+        } else if (op.opcode == MP_BC_LOAD_CONST_OBJ) {
+            bit_vector_set(obj_table_used, op.arg);
+        } else if (op.opcode == MP_BC_MAKE_FUNCTION
+                   || op.opcode == MP_BC_MAKE_FUNCTION_DEFARGS
+                   || op.opcode == MP_BC_MAKE_CLOSURE
+                   || op.opcode == MP_BC_MAKE_CLOSURE_DEFARGS) {
+            if ((mp_uint_t)op.arg + 1 > n_children) {
+                n_children = (mp_uint_t)op.arg + 1;
+            }
+        }
+        ip += op.size;
+    }
+
+    rcs->fun_data = fun_data;
+    rcs->fun_data_len = fun_data_top - fun_data;
+    rcs->n_children = n_children;
+    rcs->children = NULL;
+
+    if (n_children) {
+        rcs->children = m_new(mp_raw_code_simplified_t, n_children);
+        for (size_t i = 0; i < n_children; ++i) {
+            proto_fun_to_raw_code_simplified(children[i], qstr_table_used, obj_table_used, &rcs->children[i]);
+        }
+    }
+}
+
+static void save_raw_code_simplified(mp_print_t *print, const mp_raw_code_simplified_t *rcs) {
+    // Save function kind and data length.
+    mp_print_uint(print, rcs->fun_data_len << 3 | (rcs->n_children != 0) << 2);
+
+    // Save function code.
+    mp_print_bytes(print, rcs->fun_data, rcs->fun_data_len);
+
+    // Save (and free) children.
+    if (rcs->n_children) {
+        mp_print_uint(print, rcs->n_children);
+        for (size_t i = 0; i < rcs->n_children; ++i) {
+            save_raw_code_simplified(print, &rcs->children[i]);
+        }
+        m_del(mp_raw_code_simplified_t, rcs->children, rcs->n_children);
+    }
+}
+
+mp_obj_t mp_raw_code_save_fun_to_bytes(const mp_module_constants_t *consts, mp_proto_fun_t proto_fun) {
+    // Track the qstrs used by the function.
+    bit_vector_t qstr_table_used;
+    bit_vector_init(&qstr_table_used);
+
+    // Track the objects used by the function.
+    bit_vector_t obj_table_used;
+    bit_vector_init(&obj_table_used);
+
+    #if MICROPY_PY_BUILTINS_CODE >= MICROPY_PY_BUILTINS_CODE_FULL
+    // Make sure the filename appears in the qstr table.
+    bit_vector_set(&qstr_table_used, 0);
+    #endif
+
+    // Convert function into a simplified raw code tree.
+    mp_raw_code_simplified_t rcs;
+    proto_fun_to_raw_code_simplified(proto_fun, &qstr_table_used, &obj_table_used, &rcs);
+
+    mp_print_t print;
+    vstr_t vstr;
+    vstr_init_print(&vstr, 64, &print);
+
+    // Start with .mpy header.
+    const uint8_t header[4] = { 'M', MPY_VERSION, 0, MP_SMALL_INT_BITS };
+    mp_print_bytes(&print, header, sizeof(header));
+
+    // Number of entries in constant table.
+    mp_print_uint(&print, qstr_table_used.max_bit_set + 1);
+    mp_print_uint(&print, obj_table_used.max_bit_set + 1);
+
+    // Save qstrs.
+    for (size_t i = 0; i <= qstr_table_used.max_bit_set; ++i) {
+        if (bit_vector_is_set(&qstr_table_used, i)) {
+            save_qstr(&print, consts->qstr_table[i]);
+        } else {
+            save_qstr(&print, MP_QSTR_);
+        }
+    }
+
+    // Save constant objects.
+    for (size_t i = 0; i <= obj_table_used.max_bit_set; ++i) {
+        if (bit_vector_is_set(&obj_table_used, i)) {
+            save_obj(&print, consts->obj_table[i]);
+        } else {
+            save_obj(&print, mp_const_none);
+        }
+    }
+
+    bit_vector_clear(&qstr_table_used);
+    bit_vector_clear(&obj_table_used);
+
+    // Save the bytecode data (also free the simplified raw code tree at the same time).
+    save_raw_code_simplified(&print, &rcs);
+
+    // Create and return bytes representing the .mpy data.
+    return mp_obj_new_bytes_from_vstr(&vstr);
+}
+
+#endif // MICROPY_PERSISTENT_CODE_SAVE_FUN
 
 #if MICROPY_PERSISTENT_CODE_TRACK_RELOC_CODE
 // An mp_obj_list_t that tracks relocated native code to prevent the GC from reclaiming them.
diff --git a/py/persistentcode.h b/py/persistentcode.h
index d363f544ad5..23cb0936f2f 100644
--- a/py/persistentcode.h
+++ b/py/persistentcode.h
@@ -35,7 +35,7 @@
 // set) must also match MPY_SUB_VERSION. This allows 3 additional updates to
 // the native ABI per bytecode revision.
 #define MPY_VERSION 6
-#define MPY_SUB_VERSION 1
+#define MPY_SUB_VERSION 3
 
 // Macros to encode/decode sub-version to/from the feature byte. This replaces
 // the bits previously used to encode the flags (map caching and unicode)
@@ -45,32 +45,47 @@
 
 // Macros to encode/decode native architecture to/from the feature byte
 #define MPY_FEATURE_ENCODE_ARCH(arch) ((arch) << 2)
-#define MPY_FEATURE_DECODE_ARCH(feat) ((feat) >> 2)
+#define MPY_FEATURE_DECODE_ARCH(feat) (((feat) >> 2) & 0x2F)
 
 // Define the host architecture
-#if MICROPY_EMIT_X86
-    #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_X86)
-#elif MICROPY_EMIT_X64
-    #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_X64)
-#elif MICROPY_EMIT_THUMB
-    #if defined(__thumb2__)
-        #if defined(__ARM_FP) && (__ARM_FP & 8) == 8
-            #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_ARMV7EMDP)
-        #elif defined(__ARM_FP) && (__ARM_FP & 4) == 4
-            #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_ARMV7EMSP)
+#if MICROPY_PERSISTENT_CODE_LOAD_NATIVE
+    #if defined(__i386__) || defined(_M_IX86)
+        #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_X86)
+    #elif defined(__x86_64__) || defined(_M_X64)
+        #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_X64)
+    #elif defined(__thumb2__) || defined(__thumb__)
+        #if defined(__thumb2__)
+            #if defined(__ARM_FP) && (__ARM_FP & 8) == 8
+                #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_ARMV7EMDP)
+            #elif defined(__ARM_FP) && (__ARM_FP & 4) == 4
+                #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_ARMV7EMSP)
+            #else
+                #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_ARMV7EM)
+            #endif
         #else
-            #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_ARMV7EM)
+            #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_ARMV6M)
+        #endif
+        #define MPY_FEATURE_ARCH_TEST(x) (MP_NATIVE_ARCH_ARMV6M <= (x) && (x) <= MPY_FEATURE_ARCH)
+    #elif defined(__arm__)
+        #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_ARMV6)
+    #elif defined(__xtensa__)
+        #include 
+        #if XCHAL_HAVE_WINDOWED
+            #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_XTENSAWIN)
+        #else
+            #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_XTENSA)
+        #endif
+    #elif defined(__riscv)
+        #if __riscv_xlen == 32
+            #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_RV32IMC)
+        #elif __riscv_xlen == 64
+            #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_RV64IMC)
+        #else
+            #error "Unsupported RISC-V architecture."
         #endif
     #else
-        #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_ARMV6M)
+        #error "Unsupported native architecture."
     #endif
-    #define MPY_FEATURE_ARCH_TEST(x) (MP_NATIVE_ARCH_ARMV6M <= (x) && (x) <= MPY_FEATURE_ARCH)
-#elif MICROPY_EMIT_ARM
-    #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_ARMV6)
-#elif MICROPY_EMIT_XTENSA
-    #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_XTENSA)
-#elif MICROPY_EMIT_XTENSAWIN
-    #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_XTENSAWIN)
 #else
     #define MPY_FEATURE_ARCH (MP_NATIVE_ARCH_NONE)
 #endif
@@ -83,6 +98,10 @@
 #define MPY_FILE_HEADER_INT (MPY_VERSION \
     | (MPY_FEATURE_ENCODE_SUB_VERSION(MPY_SUB_VERSION) | MPY_FEATURE_ENCODE_ARCH(MPY_FEATURE_ARCH)) << 8)
 
+// Architecture-specific flags are present in the .mpy file
+#define MPY_FEATURE_ARCH_FLAGS (0x40)
+#define MPY_FEATURE_ARCH_FLAGS_TEST(x) (((x) & MPY_FEATURE_ARCH_FLAGS) == MPY_FEATURE_ARCH_FLAGS)
+
 enum {
     MP_NATIVE_ARCH_NONE = 0,
     MP_NATIVE_ARCH_X86,
@@ -95,6 +114,9 @@ enum {
     MP_NATIVE_ARCH_ARMV7EMDP,
     MP_NATIVE_ARCH_XTENSA,
     MP_NATIVE_ARCH_XTENSAWIN,
+    MP_NATIVE_ARCH_RV32IMC,
+    MP_NATIVE_ARCH_RV64IMC,
+    MP_NATIVE_ARCH_DEBUG, // this entry should always be last
 };
 
 enum {
@@ -113,10 +135,11 @@ enum {
 
 void mp_raw_code_load(mp_reader_t *reader, mp_compiled_module_t *ctx);
 void mp_raw_code_load_mem(const byte *buf, size_t len, mp_compiled_module_t *ctx);
-void mp_raw_code_load_file(const char *filename, mp_compiled_module_t *ctx);
+void mp_raw_code_load_file(qstr filename, mp_compiled_module_t *ctx);
 
 void mp_raw_code_save(mp_compiled_module_t *cm, mp_print_t *print);
-void mp_raw_code_save_file(mp_compiled_module_t *cm, const char *filename);
+void mp_raw_code_save_file(mp_compiled_module_t *cm, qstr filename);
+mp_obj_t mp_raw_code_save_fun_to_bytes(const mp_module_constants_t *consts, mp_proto_fun_t proto_fun);
 
 void mp_native_relocate(void *reloc, uint8_t *text, uintptr_t reloc_text);
 
diff --git a/py/profile.c b/py/profile.c
index 89af8640a03..0523a1806e7 100644
--- a/py/profile.c
+++ b/py/profile.c
@@ -31,10 +31,15 @@
 
 #if MICROPY_PY_SYS_SETTRACE
 
+#if !MICROPY_PERSISTENT_CODE_SAVE
+// The settrace feature requires that we maintain additional metadata on the raw
+// code object which is normally only done when writing .mpy files.
+#error "MICROPY_PY_SYS_SETTRACE requires MICROPY_PERSISTENT_CODE_SAVE to be enabled"
+#endif
+
 #define prof_trace_cb MP_STATE_THREAD(prof_trace_callback)
-#define QSTR_MAP(context, idx) (context->constants.qstr_table[idx])
 
-STATIC uint mp_prof_bytecode_lineno(const mp_raw_code_t *rc, size_t bc) {
+uint mp_prof_bytecode_lineno(const mp_raw_code_t *rc, size_t bc) {
     const mp_bytecode_prelude_t *prelude = &rc->prelude;
     return mp_bytecode_get_source_line(prelude->line_info, prelude->line_info_top, bc);
 }
@@ -62,141 +67,10 @@ void mp_prof_extract_prelude(const byte *bytecode, mp_bytecode_prelude_t *prelud
     prelude->line_info = ip;
 }
 
-/******************************************************************************/
-// code object
-
-STATIC void code_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
-    (void)kind;
-    mp_obj_code_t *o = MP_OBJ_TO_PTR(o_in);
-    const mp_raw_code_t *rc = o->rc;
-    const mp_bytecode_prelude_t *prelude = &rc->prelude;
-    mp_printf(print,
-        "",
-        QSTR_MAP(o->context, prelude->qstr_block_name_idx),
-        o,
-        QSTR_MAP(o->context, 0),
-        rc->line_of_definition
-        );
-}
-
-STATIC mp_obj_tuple_t *code_consts(const mp_module_context_t *context, const mp_raw_code_t *rc) {
-    mp_obj_tuple_t *consts = MP_OBJ_TO_PTR(mp_obj_new_tuple(rc->n_children + 1, NULL));
-
-    size_t const_no = 0;
-    for (size_t i = 0; i < rc->n_children; ++i) {
-        mp_obj_t code = mp_obj_new_code(context, rc->children[i]);
-        if (code == MP_OBJ_NULL) {
-            m_malloc_fail(sizeof(mp_obj_code_t));
-        }
-        consts->items[const_no++] = code;
-    }
-    consts->items[const_no++] = mp_const_none;
-
-    return consts;
-}
-
-STATIC mp_obj_t raw_code_lnotab(const mp_raw_code_t *rc) {
-    // const mp_bytecode_prelude_t *prelude = &rc->prelude;
-    uint start = 0;
-    uint stop = rc->fun_data_len - start;
-
-    uint last_lineno = mp_prof_bytecode_lineno(rc, start);
-    uint lasti = 0;
-
-    const uint buffer_chunk_size = (stop - start) >> 2; // heuristic magic
-    uint buffer_size = buffer_chunk_size;
-    byte *buffer = m_new(byte, buffer_size);
-    uint buffer_index = 0;
-
-    for (uint i = start; i < stop; ++i) {
-        uint lineno = mp_prof_bytecode_lineno(rc, i);
-        size_t line_diff = lineno - last_lineno;
-        if (line_diff > 0) {
-            uint instr_diff = (i - start) - lasti;
-
-            assert(instr_diff < 256);
-            assert(line_diff < 256);
-
-            if (buffer_index + 2 > buffer_size) {
-                buffer = m_renew(byte, buffer, buffer_size, buffer_size + buffer_chunk_size);
-                buffer_size = buffer_size + buffer_chunk_size;
-            }
-            last_lineno = lineno;
-            lasti = i - start;
-            buffer[buffer_index++] = instr_diff;
-            buffer[buffer_index++] = line_diff;
-        }
-    }
-
-    mp_obj_t o = mp_obj_new_bytes(buffer, buffer_index);
-    m_del(byte, buffer, buffer_size);
-    return o;
-}
-
-STATIC void code_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
-    if (dest[0] != MP_OBJ_NULL) {
-        // not load attribute
-        return;
-    }
-    mp_obj_code_t *o = MP_OBJ_TO_PTR(self_in);
-    const mp_raw_code_t *rc = o->rc;
-    const mp_bytecode_prelude_t *prelude = &rc->prelude;
-    switch (attr) {
-        case MP_QSTR_co_code:
-            dest[0] = mp_obj_new_bytes(
-                (void *)prelude->opcodes,
-                rc->fun_data_len - (prelude->opcodes - (const byte *)rc->fun_data)
-                );
-            break;
-        case MP_QSTR_co_consts:
-            dest[0] = MP_OBJ_FROM_PTR(code_consts(o->context, rc));
-            break;
-        case MP_QSTR_co_filename:
-            dest[0] = MP_OBJ_NEW_QSTR(QSTR_MAP(o->context, 0));
-            break;
-        case MP_QSTR_co_firstlineno:
-            dest[0] = MP_OBJ_NEW_SMALL_INT(mp_prof_bytecode_lineno(rc, 0));
-            break;
-        case MP_QSTR_co_name:
-            dest[0] = MP_OBJ_NEW_QSTR(QSTR_MAP(o->context, prelude->qstr_block_name_idx));
-            break;
-        case MP_QSTR_co_names:
-            dest[0] = MP_OBJ_FROM_PTR(o->dict_locals);
-            break;
-        case MP_QSTR_co_lnotab:
-            if (!o->lnotab) {
-                o->lnotab = raw_code_lnotab(rc);
-            }
-            dest[0] = o->lnotab;
-            break;
-    }
-}
-
-MP_DEFINE_CONST_OBJ_TYPE(
-    mp_type_settrace_codeobj,
-    MP_QSTR_code,
-    MP_TYPE_FLAG_NONE,
-    print, code_print,
-    attr, code_attr
-    );
-
-mp_obj_t mp_obj_new_code(const mp_module_context_t *context, const mp_raw_code_t *rc) {
-    mp_obj_code_t *o = m_new_obj_maybe(mp_obj_code_t);
-    if (o == NULL) {
-        return MP_OBJ_NULL;
-    }
-    o->base.type = &mp_type_settrace_codeobj;
-    o->context = context;
-    o->rc = rc;
-    o->dict_locals = mp_locals_get(); // this is a wrong! how to do this properly?
-    o->lnotab = MP_OBJ_NULL;
-    return MP_OBJ_FROM_PTR(o);
-}
-
 /******************************************************************************/
 // frame object
 
-STATIC void frame_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
+static void frame_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_frame_t *frame = MP_OBJ_TO_PTR(o_in);
     mp_obj_code_t *code = frame->code;
@@ -205,13 +79,13 @@ STATIC void frame_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t
     mp_printf(print,
         "",
         frame,
-        QSTR_MAP(code->context, 0),
-        frame->lineno,
-        QSTR_MAP(code->context, prelude->qstr_block_name_idx)
+        MP_CODE_QSTR_MAP(code->context, 0),
+        (int)frame->lineno,
+        MP_CODE_QSTR_MAP(code->context, prelude->qstr_block_name_idx)
         );
 }
 
-STATIC void frame_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+static void frame_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     if (dest[0] != MP_OBJ_NULL) {
         // not load attribute
         return;
@@ -259,7 +133,7 @@ mp_obj_t mp_obj_new_frame(const mp_code_state_t *code_state) {
         return MP_OBJ_NULL;
     }
 
-    mp_obj_code_t *code = o->code = MP_OBJ_TO_PTR(mp_obj_new_code(code_state->fun_bc->context, code_state->fun_bc->rc));
+    mp_obj_code_t *code = o->code = MP_OBJ_TO_PTR(mp_obj_new_code(code_state->fun_bc->context, code_state->fun_bc->rc, false));
     if (code == NULL) {
         return MP_OBJ_NULL;
     }
@@ -288,7 +162,7 @@ typedef struct {
     mp_obj_t arg;
 } prof_callback_args_t;
 
-STATIC mp_obj_t mp_prof_callback_invoke(mp_obj_t callback, prof_callback_args_t *args) {
+static mp_obj_t mp_prof_callback_invoke(mp_obj_t callback, prof_callback_args_t *args) {
     assert(mp_obj_is_callable(callback));
 
     mp_prof_is_executing = true;
@@ -299,7 +173,7 @@ STATIC mp_obj_t mp_prof_callback_invoke(mp_obj_t callback, prof_callback_args_t
     mp_prof_is_executing = false;
 
     if (MP_STATE_THREAD(mp_pending_exception) != MP_OBJ_NULL) {
-        mp_handle_pending(true);
+        mp_handle_pending(MP_HANDLE_PENDING_CALLBACKS_AND_EXCEPTIONS);
     }
     return top;
 }
@@ -468,7 +342,7 @@ typedef struct _mp_dis_instruction_t {
     mp_obj_t argobjex_cache;
 } mp_dis_instruction_t;
 
-STATIC const byte *mp_prof_opcode_decode(const byte *ip, const mp_uint_t *const_table, mp_dis_instruction_t *instruction) {
+static const byte *mp_prof_opcode_decode(const byte *ip, const mp_uint_t *const_table, mp_dis_instruction_t *instruction) {
     mp_uint_t unum;
     const byte *ptr;
     mp_obj_t obj;
diff --git a/py/profile.h b/py/profile.h
index 7f3f9140346..db72b9f0768 100644
--- a/py/profile.h
+++ b/py/profile.h
@@ -28,20 +28,12 @@
 #define MICROPY_INCLUDED_PY_PROFILING_H
 
 #include "py/emitglue.h"
+#include "py/objcode.h"
 
 #if MICROPY_PY_SYS_SETTRACE
 
 #define mp_prof_is_executing MP_STATE_THREAD(prof_callback_is_executing)
 
-typedef struct _mp_obj_code_t {
-    // TODO this was 4 words
-    mp_obj_base_t base;
-    const mp_module_context_t *context;
-    const mp_raw_code_t *rc;
-    mp_obj_dict_t *dict_locals;
-    mp_obj_t lnotab;
-} mp_obj_code_t;
-
 typedef struct _mp_obj_frame_t {
     mp_obj_base_t base;
     const mp_code_state_t *code_state;
@@ -53,9 +45,9 @@ typedef struct _mp_obj_frame_t {
     bool trace_opcodes;
 } mp_obj_frame_t;
 
+uint mp_prof_bytecode_lineno(const mp_raw_code_t *rc, size_t bc);
 void mp_prof_extract_prelude(const byte *bytecode, mp_bytecode_prelude_t *prelude);
 
-mp_obj_t mp_obj_new_code(const mp_module_context_t *mc, const mp_raw_code_t *rc);
 mp_obj_t mp_obj_new_frame(const mp_code_state_t *code_state);
 
 // This is the implementation for the sys.settrace
diff --git a/py/py.cmake b/py/py.cmake
index 07d1fe05ffa..e423bf8c184 100644
--- a/py/py.cmake
+++ b/py/py.cmake
@@ -4,11 +4,18 @@ set(MICROPY_PY_DIR "${MICROPY_DIR}/py")
 
 list(APPEND MICROPY_INC_CORE "${MICROPY_DIR}")
 
+# Set MICROPY_LIB_DIR default if not already set by the port.
+# This needs to happen before usermod.cmake is included (for c_module() in manifests).
+if(NOT MICROPY_LIB_DIR)
+    set(MICROPY_LIB_DIR ${MICROPY_DIR}/lib/micropython-lib)
+endif()
+
 # All py/ source files
 set(MICROPY_SOURCE_PY
     ${MICROPY_PY_DIR}/argcheck.c
     ${MICROPY_PY_DIR}/asmarm.c
     ${MICROPY_PY_DIR}/asmbase.c
+    ${MICROPY_PY_DIR}/asmrv32.c
     ${MICROPY_PY_DIR}/asmthumb.c
     ${MICROPY_PY_DIR}/asmx64.c
     ${MICROPY_PY_DIR}/asmx86.c
@@ -19,12 +26,16 @@ set(MICROPY_SOURCE_PY
     ${MICROPY_PY_DIR}/builtinhelp.c
     ${MICROPY_PY_DIR}/builtinimport.c
     ${MICROPY_PY_DIR}/compile.c
+    ${MICROPY_PY_DIR}/cstack.c
     ${MICROPY_PY_DIR}/emitbc.c
     ${MICROPY_PY_DIR}/emitcommon.c
     ${MICROPY_PY_DIR}/emitglue.c
+    ${MICROPY_PY_DIR}/emitinlinerv32.c
     ${MICROPY_PY_DIR}/emitinlinethumb.c
     ${MICROPY_PY_DIR}/emitinlinextensa.c
     ${MICROPY_PY_DIR}/emitnarm.c
+    ${MICROPY_PY_DIR}/emitndebug.c
+    ${MICROPY_PY_DIR}/emitnrv32.c
     ${MICROPY_PY_DIR}/emitnthumb.c
     ${MICROPY_PY_DIR}/emitnx64.c
     ${MICROPY_PY_DIR}/emitnx86.c
@@ -44,17 +55,23 @@ set(MICROPY_SOURCE_PY
     ${MICROPY_PY_DIR}/modio.c
     ${MICROPY_PY_DIR}/modmath.c
     ${MICROPY_PY_DIR}/modmicropython.c
+    ${MICROPY_PY_DIR}/modstring.c
     ${MICROPY_PY_DIR}/modstruct.c
     ${MICROPY_PY_DIR}/modsys.c
     ${MICROPY_PY_DIR}/modthread.c
-    ${MICROPY_PY_DIR}/moduerrno.c
+    ${MICROPY_PY_DIR}/moderrno.c
+    ${MICROPY_PY_DIR}/modweakref.c
     ${MICROPY_PY_DIR}/mpprint.c
     ${MICROPY_PY_DIR}/mpstate.c
     ${MICROPY_PY_DIR}/mpz.c
     ${MICROPY_PY_DIR}/nativeglue.c
     ${MICROPY_PY_DIR}/nlr.c
+    ${MICROPY_PY_DIR}/nlraarch64.c
+    ${MICROPY_PY_DIR}/nlrloong64.c
     ${MICROPY_PY_DIR}/nlrmips.c
     ${MICROPY_PY_DIR}/nlrpowerpc.c
+    ${MICROPY_PY_DIR}/nlrrv32.c
+    ${MICROPY_PY_DIR}/nlrrv64.c
     ${MICROPY_PY_DIR}/nlrsetjmp.c
     ${MICROPY_PY_DIR}/nlrthumb.c
     ${MICROPY_PY_DIR}/nlrx64.c
@@ -65,6 +82,7 @@ set(MICROPY_SOURCE_PY
     ${MICROPY_PY_DIR}/objattrtuple.c
     ${MICROPY_PY_DIR}/objbool.c
     ${MICROPY_PY_DIR}/objboundmeth.c
+    ${MICROPY_PY_DIR}/objcode.c
     ${MICROPY_PY_DIR}/objcell.c
     ${MICROPY_PY_DIR}/objclosure.c
     ${MICROPY_PY_DIR}/objcomplex.c
@@ -90,12 +108,14 @@ set(MICROPY_SOURCE_PY
     ${MICROPY_PY_DIR}/objproperty.c
     ${MICROPY_PY_DIR}/objrange.c
     ${MICROPY_PY_DIR}/objreversed.c
+    ${MICROPY_PY_DIR}/objringio.c
     ${MICROPY_PY_DIR}/objset.c
     ${MICROPY_PY_DIR}/objsingleton.c
     ${MICROPY_PY_DIR}/objslice.c
     ${MICROPY_PY_DIR}/objstr.c
     ${MICROPY_PY_DIR}/objstringio.c
     ${MICROPY_PY_DIR}/objstrunicode.c
+    ${MICROPY_PY_DIR}/objtemplate.c
     ${MICROPY_PY_DIR}/objtuple.c
     ${MICROPY_PY_DIR}/objtype.c
     ${MICROPY_PY_DIR}/objzip.c
diff --git a/py/py.mk b/py/py.mk
index 1dc0c8641f3..558f37d7d59 100644
--- a/py/py.mk
+++ b/py/py.mk
@@ -21,14 +21,11 @@ QSTR_GLOBAL_REQUIREMENTS += $(HEADER_BUILD)/mpversion.h
 # some code is performance bottleneck and compiled with other optimization options
 CSUPEROPT = -O3
 
-# Enable building 32-bit code on 64-bit host.
-ifeq ($(MICROPY_FORCE_32BIT),1)
-CC += -m32
-CXX += -m32
-LD += -m32
-endif
-
 # External modules written in C.
+
+# Process manifest for C module extraction (appends to USER_C_MODULES).
+include $(TOP)/py/manifest.mk
+
 ifneq ($(USER_C_MODULES),)
 # pre-define USERMOD variables as expanded so that variables are immediate
 # expanded as they're added to them
@@ -36,40 +33,73 @@ ifneq ($(USER_C_MODULES),)
 # C/C++ files that are included in the QSTR/module build
 SRC_USERMOD_C :=
 SRC_USERMOD_CXX :=
-# Other C/C++ files (e.g. libraries or helpers)
+# Other C/C++/Assembly files (e.g. libraries or helpers)
 SRC_USERMOD_LIB_C :=
 SRC_USERMOD_LIB_CXX :=
+SRC_USERMOD_LIB_ASM :=
 # Optionally set flags
 CFLAGS_USERMOD :=
 CXXFLAGS_USERMOD :=
 LDFLAGS_USERMOD :=
+LIBS_USERMOD :=
 
 # Backwards compatibility with older user c modules that set SRC_USERMOD
 # added to SRC_USERMOD_C below
 SRC_USERMOD :=
 
-$(foreach module, $(wildcard $(USER_C_MODULES)/*/micropython.mk), \
-    $(eval USERMOD_DIR = $(patsubst %/,%,$(dir $(module))))\
-    $(info Including User C Module from $(USERMOD_DIR))\
-	$(eval include $(module))\
+# For each C module directory, scan for micropython.mk and include it.
+# Accumulate USERMOD_DIRS so we can later strip each module's parent path
+# from its source files while preserving the module basename in the build
+# tree (e.g. build/cexample/foo.o, not build/foo.o, so files that share a
+# name across modules don't collide on the same .o target).
+USERMOD_DIRS :=
+$(foreach _UDIR, $(USER_C_MODULES), \
+    $(if $(wildcard $(_UDIR)/.),,$(error USER_C_MODULES path doesn't exist: $(abspath $(_UDIR)))) \
+    $(foreach module, $(wildcard $(_UDIR)/micropython.mk) $(wildcard $(_UDIR)/*/micropython.mk), \
+        $(eval USERMOD_DIR = $(patsubst %/,%,$(dir $(module))))\
+        $(eval USERMOD_DIRS += $(USERMOD_DIR))\
+        $(info Including User C Module from $(USERMOD_DIR))\
+        $(eval include $(module))\
+    )\
 )
 
 SRC_USERMOD_C += $(SRC_USERMOD)
 
-SRC_USERMOD_PATHFIX_C += $(patsubst $(USER_C_MODULES)/%.c,%.c,$(SRC_USERMOD_C))
-SRC_USERMOD_PATHFIX_CXX += $(patsubst $(USER_C_MODULES)/%.cpp,%.cpp,$(SRC_USERMOD_CXX))
-SRC_USERMOD_PATHFIX_LIB_C += $(patsubst $(USER_C_MODULES)/%.c,%.c,$(SRC_USERMOD_LIB_C))
-SRC_USERMOD_PATHFIX_LIB_CXX += $(patsubst $(USER_C_MODULES)/%.cpp,%.cpp,$(SRC_USERMOD_LIB_CXX))
+# Strip each USERMOD_DIR's leading path from source files, keeping the module
+# basename as the leading directory so build outputs land at
+# BUILD//.o. Uses per-directory patsubst on the paths as-is
+# rather than $(abspath) so a parent directory containing whitespace doesn't
+# break word-based patsubst.
+SRC_USERMOD_PATHFIX_C := $(SRC_USERMOD_C)
+SRC_USERMOD_PATHFIX_CXX := $(SRC_USERMOD_CXX)
+SRC_USERMOD_PATHFIX_LIB_C := $(SRC_USERMOD_LIB_C)
+SRC_USERMOD_PATHFIX_LIB_CXX := $(SRC_USERMOD_LIB_CXX)
+SRC_USERMOD_PATHFIX_LIB_ASM := $(SRC_USERMOD_LIB_ASM)
+USERMOD_DIRS := $(sort $(USERMOD_DIRS))
+$(foreach _MDIR, $(USERMOD_DIRS), \
+    $(eval _MBASE := $(notdir $(_MDIR))) \
+    $(eval SRC_USERMOD_PATHFIX_C := $$(patsubst $(_MDIR)/%,$(_MBASE)/%,$$(SRC_USERMOD_PATHFIX_C))) \
+    $(eval SRC_USERMOD_PATHFIX_CXX := $$(patsubst $(_MDIR)/%,$(_MBASE)/%,$$(SRC_USERMOD_PATHFIX_CXX))) \
+    $(eval SRC_USERMOD_PATHFIX_LIB_C := $$(patsubst $(_MDIR)/%,$(_MBASE)/%,$$(SRC_USERMOD_PATHFIX_LIB_C))) \
+    $(eval SRC_USERMOD_PATHFIX_LIB_CXX := $$(patsubst $(_MDIR)/%,$(_MBASE)/%,$$(SRC_USERMOD_PATHFIX_LIB_CXX))) \
+    $(eval SRC_USERMOD_PATHFIX_LIB_ASM := $$(patsubst $(_MDIR)/%,$(_MBASE)/%,$$(SRC_USERMOD_PATHFIX_LIB_ASM))) \
+)
+
+# Parent directories of each USERMOD_DIR, added to vpath below so paths like
+# cexample/foo.c (produced by the patsubst above) resolve to source files.
+USERMOD_DIR_PARENTS := $(sort $(foreach _MDIR, $(USERMOD_DIRS), $(patsubst %/,%,$(dir $(_MDIR)))))
 
 CFLAGS += $(CFLAGS_USERMOD)
 CXXFLAGS += $(CXXFLAGS_USERMOD)
 LDFLAGS += $(LDFLAGS_USERMOD)
+LIBS += $(LIBS_USERMOD)
 
 SRC_QSTR += $(SRC_USERMOD_PATHFIX_C) $(SRC_USERMOD_PATHFIX_CXX)
 PY_O += $(addprefix $(BUILD)/, $(SRC_USERMOD_PATHFIX_C:.c=.o))
 PY_O += $(addprefix $(BUILD)/, $(SRC_USERMOD_PATHFIX_CXX:.cpp=.o))
 PY_O += $(addprefix $(BUILD)/, $(SRC_USERMOD_PATHFIX_LIB_C:.c=.o))
 PY_O += $(addprefix $(BUILD)/, $(SRC_USERMOD_PATHFIX_LIB_CXX:.cpp=.o))
+PY_O += $(addprefix $(BUILD)/, $(SRC_USERMOD_PATHFIX_LIB_ASM:.S=.o))
 endif
 
 # py object files
@@ -83,6 +113,9 @@ PY_CORE_O_BASENAME = $(addprefix py/,\
 	nlrmips.o \
 	nlrpowerpc.o \
 	nlrxtensa.o \
+	nlrrv32.o \
+	nlrrv64.o \
+	nlrloong64.o \
 	nlrsetjmp.o \
 	malloc.o \
 	gc.o \
@@ -113,6 +146,10 @@ PY_CORE_O_BASENAME = $(addprefix py/,\
 	emitnxtensa.o \
 	emitinlinextensa.o \
 	emitnxtensawin.o \
+	asmrv32.o \
+	emitnrv32.o \
+	emitinlinerv32.o \
+	emitndebug.o \
 	formatfloat.o \
 	parsenumbase.o \
 	parsenum.o \
@@ -124,6 +161,7 @@ PY_CORE_O_BASENAME = $(addprefix py/,\
 	nativeglue.o \
 	pairheap.o \
 	ringbuf.o \
+	cstack.o \
 	stackctrl.o \
 	argcheck.o \
 	warning.o \
@@ -136,6 +174,7 @@ PY_CORE_O_BASENAME = $(addprefix py/,\
 	objboundmeth.o \
 	objcell.o \
 	objclosure.o \
+	objcode.o \
 	objcomplex.o \
 	objdeque.o \
 	objdict.o \
@@ -159,12 +198,14 @@ PY_CORE_O_BASENAME = $(addprefix py/,\
 	objnamedtuple.o \
 	objrange.o \
 	objreversed.o \
+	objringio.o \
 	objset.o \
 	objsingleton.o \
 	objslice.o \
 	objstr.o \
 	objstrunicode.o \
 	objstringio.o \
+	objtemplate.o \
 	objtuple.o \
 	objtype.o \
 	objzip.o \
@@ -183,10 +224,12 @@ PY_CORE_O_BASENAME = $(addprefix py/,\
 	modmath.o \
 	modcmath.o \
 	modmicropython.o \
+	modstring.o \
 	modstruct.o \
 	modsys.o \
-	moduerrno.o \
+	moderrno.o \
 	modthread.o \
+	modweakref.o \
 	vm.o \
 	bc.o \
 	showbc.o \
@@ -203,7 +246,7 @@ PY_O += $(PY_CORE_O)
 
 # object file for frozen code specified via a manifest
 ifneq ($(FROZEN_MANIFEST),)
-PY_O += $(BUILD)/$(BUILD)/frozen_content.o
+PY_O += $(BUILD)/frozen_content.o
 endif
 
 # Sources that may contain qstrings
@@ -237,7 +280,7 @@ $(HEADER_BUILD)/compressed.data.h: $(HEADER_BUILD)/compressed.collected
 	$(Q)$(PYTHON) $(PY_SRC)/makecompresseddata.py $< > $@
 
 # build a list of registered modules for py/objmodule.c.
-$(HEADER_BUILD)/moduledefs.h: $(HEADER_BUILD)/moduledefs.collected
+$(HEADER_BUILD)/moduledefs.h: $(HEADER_BUILD)/moduledefs.collected $(PY_SRC)/makemoduledefs.py
 	@$(ECHO) "GEN $@"
 	$(Q)$(PYTHON) $(PY_SRC)/makemoduledefs.py $< > $@
 
diff --git a/py/qstr.c b/py/qstr.c
index ea700566f4f..7902646a92d 100644
--- a/py/qstr.c
+++ b/py/qstr.c
@@ -33,9 +33,6 @@
 #include "py/gc.h"
 #include "py/runtime.h"
 
-// NOTE: we are using linear arrays to store and search for qstr's (unique strings, interned strings)
-// ultimately we will replace this with a static hash table of some kind
-
 #if MICROPY_DEBUG_VERBOSE // print debugging info
 #define DEBUG_printf DEBUG_printf
 #else // don't print debugging info
@@ -45,7 +42,11 @@
 // A qstr is an index into the qstr pool.
 // The data for a qstr is \0 terminated (so they can be printed using printf)
 
+#if MICROPY_QSTR_BYTES_IN_HASH
 #define Q_HASH_MASK ((1 << (8 * MICROPY_QSTR_BYTES_IN_HASH)) - 1)
+#else
+#define Q_HASH_MASK (0xffff)
+#endif
 
 #if MICROPY_PY_THREAD && !MICROPY_PY_THREAD_GIL
 #define QSTR_ENTER() mp_thread_mutex_lock(&MP_STATE_VM(qstr_mutex), 1)
@@ -74,38 +75,102 @@ size_t qstr_compute_hash(const byte *data, size_t len) {
     return hash;
 }
 
+// The first pool is the static qstr table. The contents must remain stable as
+// it is part of the .mpy ABI. See the top of py/persistentcode.c and
+// static_qstr_list in makeqstrdata.py. This pool is unsorted (although in a
+// future .mpy version we could re-order them and make it sorted). It also
+// contains additional qstrs that must have IDs <256, see unsorted_qstr_list
+// in makeqstrdata.py.
+#if MICROPY_QSTR_BYTES_IN_HASH
+const qstr_hash_t mp_qstr_const_hashes_static[] = {
+    #ifndef NO_QSTR
+#define QDEF0(id, hash, len, str) hash,
+#define QDEF1(id, hash, len, str)
+    #include "genhdr/qstrdefs.generated.h"
+#undef QDEF0
+#undef QDEF1
+    #endif
+};
+#endif
+
+const qstr_len_t mp_qstr_const_lengths_static[] = {
+    #ifndef NO_QSTR
+#define QDEF0(id, hash, len, str) len,
+#define QDEF1(id, hash, len, str)
+    #include "genhdr/qstrdefs.generated.h"
+#undef QDEF0
+#undef QDEF1
+    #endif
+};
+
+const qstr_pool_t mp_qstr_const_pool_static = {
+    NULL,               // no previous pool
+    0,                  // no previous pool
+    false,              // is_sorted
+    MICROPY_ALLOC_QSTR_ENTRIES_INIT,
+    MP_QSTRnumber_of_static,   // corresponds to number of strings in array just below
+    #if MICROPY_QSTR_BYTES_IN_HASH
+    (qstr_hash_t *)mp_qstr_const_hashes_static,
+    #endif
+    (qstr_len_t *)mp_qstr_const_lengths_static,
+    {
+        #ifndef NO_QSTR
+#define QDEF0(id, hash, len, str) str,
+#define QDEF1(id, hash, len, str)
+        #include "genhdr/qstrdefs.generated.h"
+#undef QDEF0
+#undef QDEF1
+        #endif
+    },
+};
+
+// The next pool is the remainder of the qstrs defined in the firmware. This
+// is sorted.
+#if MICROPY_QSTR_BYTES_IN_HASH
 const qstr_hash_t mp_qstr_const_hashes[] = {
     #ifndef NO_QSTR
-#define QDEF(id, hash, len, str) hash,
+#define QDEF0(id, hash, len, str)
+#define QDEF1(id, hash, len, str) hash,
     #include "genhdr/qstrdefs.generated.h"
-#undef QDEF
+#undef QDEF0
+#undef QDEF1
     #endif
 };
+#endif
 
 const qstr_len_t mp_qstr_const_lengths[] = {
     #ifndef NO_QSTR
-#define QDEF(id, hash, len, str) len,
+#define QDEF0(id, hash, len, str)
+#define QDEF1(id, hash, len, str) len,
     #include "genhdr/qstrdefs.generated.h"
-#undef QDEF
+#undef QDEF0
+#undef QDEF1
     #endif
 };
 
 const qstr_pool_t mp_qstr_const_pool = {
-    NULL,               // no previous pool
-    0,                  // no previous pool
+    &mp_qstr_const_pool_static,
+    MP_QSTRnumber_of_static,
+    true,               // is_sorted
     MICROPY_ALLOC_QSTR_ENTRIES_INIT,
-    MP_QSTRnumber_of,   // corresponds to number of strings in array just below
+    MP_QSTRnumber_of - MP_QSTRnumber_of_static,   // corresponds to number of strings in array just below
+    #if MICROPY_QSTR_BYTES_IN_HASH
     (qstr_hash_t *)mp_qstr_const_hashes,
+    #endif
     (qstr_len_t *)mp_qstr_const_lengths,
     {
         #ifndef NO_QSTR
-#define QDEF(id, hash, len, str) str,
+#define QDEF0(id, hash, len, str)
+#define QDEF1(id, hash, len, str) str,
         #include "genhdr/qstrdefs.generated.h"
-#undef QDEF
+#undef QDEF0
+#undef QDEF1
         #endif
     },
 };
 
+// If frozen code is enabled, then there is an additional, sorted, ROM pool
+// containing additional qstrs required by the frozen code.
 #ifdef MICROPY_QSTR_EXTRA_POOL
 extern const qstr_pool_t MICROPY_QSTR_EXTRA_POOL;
 #define CONST_POOL MICROPY_QSTR_EXTRA_POOL
@@ -122,7 +187,7 @@ void qstr_init(void) {
     #endif
 }
 
-STATIC const qstr_pool_t *find_qstr(qstr *q) {
+static const qstr_pool_t *find_qstr(qstr *q) {
     // search pool for this qstr
     // total_prev_len==0 in the final pool, so the loop will always terminate
     const qstr_pool_t *pool = MP_STATE_VM(last_pool);
@@ -135,8 +200,13 @@ STATIC const qstr_pool_t *find_qstr(qstr *q) {
 }
 
 // qstr_mutex must be taken while in this function
-STATIC qstr qstr_add(mp_uint_t hash, mp_uint_t len, const char *q_ptr) {
+static qstr qstr_add(mp_uint_t len, const char *q_ptr) {
+    #if MICROPY_QSTR_BYTES_IN_HASH
+    mp_uint_t hash = qstr_compute_hash((const byte *)q_ptr, len);
     DEBUG_printf("QSTR: add hash=%d len=%d data=%.*s\n", hash, len, len, q_ptr);
+    #else
+    DEBUG_printf("QSTR: add len=%d data=%.*s\n", len, len, q_ptr);
+    #endif
 
     // make sure we have room in the pool for a new qstr
     if (MP_STATE_VM(last_pool)->len >= MP_STATE_VM(last_pool)->alloc) {
@@ -146,7 +216,11 @@ STATIC qstr qstr_add(mp_uint_t hash, mp_uint_t len, const char *q_ptr) {
         new_alloc = MAX(MICROPY_ALLOC_QSTR_ENTRIES_INIT, new_alloc);
         #endif
         mp_uint_t pool_size = sizeof(qstr_pool_t)
-            + (sizeof(const char *) + sizeof(qstr_hash_t) + sizeof(qstr_len_t)) * new_alloc;
+            + (sizeof(const char *)
+                #if MICROPY_QSTR_BYTES_IN_HASH
+                + sizeof(qstr_hash_t)
+                #endif
+                + sizeof(qstr_len_t)) * new_alloc;
         qstr_pool_t *pool = (qstr_pool_t *)m_malloc_maybe(pool_size);
         if (pool == NULL) {
             // Keep qstr_last_chunk consistent with qstr_pool_t: qstr_last_chunk is not scanned
@@ -158,8 +232,12 @@ STATIC qstr qstr_add(mp_uint_t hash, mp_uint_t len, const char *q_ptr) {
             QSTR_EXIT();
             m_malloc_fail(new_alloc);
         }
+        #if MICROPY_QSTR_BYTES_IN_HASH
         pool->hashes = (qstr_hash_t *)(pool->qstrs + new_alloc);
         pool->lengths = (qstr_len_t *)(pool->hashes + new_alloc);
+        #else
+        pool->lengths = (qstr_len_t *)(pool->qstrs + new_alloc);
+        #endif
         pool->prev = MP_STATE_VM(last_pool);
         pool->total_prev_len = MP_STATE_VM(last_pool)->total_prev_len + MP_STATE_VM(last_pool)->len;
         pool->alloc = new_alloc;
@@ -170,7 +248,9 @@ STATIC qstr qstr_add(mp_uint_t hash, mp_uint_t len, const char *q_ptr) {
 
     // add the new qstr
     mp_uint_t at = MP_STATE_VM(last_pool)->len;
+    #if MICROPY_QSTR_BYTES_IN_HASH
     MP_STATE_VM(last_pool)->hashes[at] = hash;
+    #endif
     MP_STATE_VM(last_pool)->lengths[at] = len;
     MP_STATE_VM(last_pool)->qstrs[at] = q_ptr;
     MP_STATE_VM(last_pool)->len++;
@@ -180,13 +260,41 @@ STATIC qstr qstr_add(mp_uint_t hash, mp_uint_t len, const char *q_ptr) {
 }
 
 qstr qstr_find_strn(const char *str, size_t str_len) {
+    if (str_len == 0) {
+        // strncmp behaviour is undefined for str==NULL.
+        return MP_QSTR_;
+    }
+
+    #if MICROPY_QSTR_BYTES_IN_HASH
     // work out hash of str
     size_t str_hash = qstr_compute_hash((const byte *)str, str_len);
+    #endif
 
     // search pools for the data
     for (const qstr_pool_t *pool = MP_STATE_VM(last_pool); pool != NULL; pool = pool->prev) {
-        for (mp_uint_t at = 0, top = pool->len; at < top; at++) {
-            if (pool->hashes[at] == str_hash && pool->lengths[at] == str_len
+        size_t low = 0;
+        size_t high = pool->len - 1;
+
+        // binary search inside the pool
+        if (pool->is_sorted) {
+            while (high - low > 1) {
+                size_t mid = (low + high) / 2;
+                int cmp = strncmp(str, pool->qstrs[mid], str_len);
+                if (cmp <= 0) {
+                    high = mid;
+                } else {
+                    low = mid;
+                }
+            }
+        }
+
+        // sequential search for the remaining strings
+        for (mp_uint_t at = low; at < high + 1; at++) {
+            if (
+                #if MICROPY_QSTR_BYTES_IN_HASH
+                pool->hashes[at] == str_hash &&
+                #endif
+                pool->lengths[at] == str_len
                 && memcmp(pool->qstrs[at], str, str_len) == 0) {
                 return pool->total_prev_len + at;
             }
@@ -194,14 +302,14 @@ qstr qstr_find_strn(const char *str, size_t str_len) {
     }
 
     // not found; return null qstr
-    return 0;
+    return MP_QSTRnull;
 }
 
 qstr qstr_from_str(const char *str) {
     return qstr_from_strn(str, strlen(str));
 }
 
-qstr qstr_from_strn(const char *str, size_t len) {
+static qstr qstr_from_strn_helper(const char *str, size_t len, bool data_is_static) {
     QSTR_ENTER();
     qstr q = qstr_find_strn(str, len);
     if (q == 0) {
@@ -213,6 +321,12 @@ qstr qstr_from_strn(const char *str, size_t len) {
             mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("name too long"));
         }
 
+        if (data_is_static) {
+            // Given string data will be forever available so use it directly.
+            assert(str[len] == '\0');
+            goto add;
+        }
+
         // compute number of bytes needed to intern this string
         size_t n_bytes = len + 1;
 
@@ -254,18 +368,35 @@ qstr qstr_from_strn(const char *str, size_t len) {
         MP_STATE_VM(qstr_last_used) += n_bytes;
 
         // store the interned strings' data
-        size_t hash = qstr_compute_hash((const byte *)str, len);
         memcpy(q_ptr, str, len);
         q_ptr[len] = '\0';
-        q = qstr_add(hash, len, q_ptr);
+        str = q_ptr;
+
+    add:
+        q = qstr_add(len, str);
     }
     QSTR_EXIT();
     return q;
 }
 
+qstr qstr_from_strn(const char *str, size_t len) {
+    return qstr_from_strn_helper(str, len, false);
+}
+
+#if MICROPY_VFS_ROM
+// Create a new qstr that can forever reference the given string data.
+qstr qstr_from_strn_static(const char *str, size_t len) {
+    return qstr_from_strn_helper(str, len, true);
+}
+#endif
+
 mp_uint_t qstr_hash(qstr q) {
     const qstr_pool_t *pool = find_qstr(&q);
+    #if MICROPY_QSTR_BYTES_IN_HASH
     return pool->hashes[q];
+    #else
+    return qstr_compute_hash((byte *)pool->qstrs[q], pool->lengths[q]);
+    #endif
 }
 
 size_t qstr_len(qstr q) {
@@ -300,7 +431,11 @@ void qstr_pool_info(size_t *n_pool, size_t *n_qstr, size_t *n_str_data_bytes, si
         *n_total_bytes += gc_nbytes(pool); // this counts actual bytes used in heap
         #else
         *n_total_bytes += sizeof(qstr_pool_t)
-            + (sizeof(const char *) + sizeof(qstr_hash_t) + sizeof(qstr_len_t)) * pool->alloc;
+            + (sizeof(const char *)
+                #if MICROPY_QSTR_BYTES_IN_HASH
+                + sizeof(qstr_hash_t)
+                #endif
+                + sizeof(qstr_len_t)) * pool->alloc;
         #endif
     }
     *n_total_bytes += *n_str_data_bytes;
@@ -329,7 +464,7 @@ void qstr_dump_data(void) {
 #else
 
 // Emit the compressed_string_data string.
-#define MP_COMPRESSED_DATA(x) STATIC const char *compressed_string_data = x;
+#define MP_COMPRESSED_DATA(x) static const char *compressed_string_data = x;
 #define MP_MATCH_COMPRESSED(a, b)
 #include "genhdr/compressed.data.h"
 #undef MP_COMPRESSED_DATA
@@ -343,7 +478,7 @@ void qstr_dump_data(void) {
 // The compressed string data is delimited by setting high bit in the final char of each word.
 // e.g. aaaa<0x80|a>bbbbbb<0x80|b>....
 // This method finds the n'th string.
-STATIC const byte *find_uncompressed_string(uint8_t n) {
+static const byte *find_uncompressed_string(uint8_t n) {
     const byte *c = (byte *)compressed_string_data;
     while (n > 0) {
         while ((*c & 0x80) == 0) {
diff --git a/py/qstr.h b/py/qstr.h
index 0ef861f33e8..0cde6062eb1 100644
--- a/py/qstr.h
+++ b/py/qstr.h
@@ -38,9 +38,21 @@
 // first entry in enum will be MP_QSTRnull=0, which indicates invalid/no qstr
 enum {
     #ifndef NO_QSTR
-#define QDEF(id, hash, len, str) id,
+#define QDEF0(id, hash, len, str) id,
+#define QDEF1(id, hash, len, str)
     #include "genhdr/qstrdefs.generated.h"
-#undef QDEF
+#undef QDEF0
+#undef QDEF1
+    #endif
+    MP_QSTRnumber_of_static,
+    MP_QSTRstart_of_main = MP_QSTRnumber_of_static - 1, // unused but shifts the enum counter back one
+
+    #ifndef NO_QSTR
+#define QDEF0(id, hash, len, str)
+#define QDEF1(id, hash, len, str) id,
+    #include "genhdr/qstrdefs.generated.h"
+#undef QDEF0
+#undef QDEF1
     #endif
     MP_QSTRnumber_of, // no underscore so it can't clash with any of the above
 };
@@ -48,7 +60,9 @@ enum {
 typedef size_t qstr;
 typedef uint16_t qstr_short_t;
 
-#if MICROPY_QSTR_BYTES_IN_HASH == 1
+#if MICROPY_QSTR_BYTES_IN_HASH == 0
+// No qstr_hash_t type needed.
+#elif MICROPY_QSTR_BYTES_IN_HASH == 1
 typedef uint8_t qstr_hash_t;
 #elif MICROPY_QSTR_BYTES_IN_HASH == 2
 typedef uint16_t qstr_hash_t;
@@ -66,10 +80,13 @@ typedef uint16_t qstr_len_t;
 
 typedef struct _qstr_pool_t {
     const struct _qstr_pool_t *prev;
-    size_t total_prev_len;
+    size_t total_prev_len : (8 * sizeof(size_t) - 1);
+    size_t is_sorted : 1;
     size_t alloc;
     size_t len;
+    #if MICROPY_QSTR_BYTES_IN_HASH
     qstr_hash_t *hashes;
+    #endif
     qstr_len_t *lengths;
     const char *qstrs[];
 } qstr_pool_t;
@@ -79,10 +96,14 @@ typedef struct _qstr_pool_t {
 void qstr_init(void);
 
 size_t qstr_compute_hash(const byte *data, size_t len);
+
 qstr qstr_find_strn(const char *str, size_t str_len); // returns MP_QSTRnull if not found
 
 qstr qstr_from_str(const char *str);
 qstr qstr_from_strn(const char *str, size_t len);
+#if MICROPY_VFS_ROM
+qstr qstr_from_strn_static(const char *str, size_t len);
+#endif
 
 mp_uint_t qstr_hash(qstr q);
 const char *qstr_str(qstr q);
diff --git a/py/qstrdefs.h b/py/qstrdefs.h
index 5003636df3d..3c20666ab13 100644
--- a/py/qstrdefs.h
+++ b/py/qstrdefs.h
@@ -53,7 +53,9 @@ Q({:#x})
 Q({:#b})
 Q( )
 Q(\n)
+#if MICROPY_STACK_CHECK
 Q(maximum recursion depth exceeded)
+#endif
 Q()
 Q()
 Q()
@@ -68,6 +70,15 @@ Q(utf-8)
 Q(.frozen)
 #endif
 
+#if MICROPY_VFS_ROM && MICROPY_VFS_ROM_IOCTL
+Q(/rom)
+Q(/rom/lib)
+#endif
+
 #if MICROPY_ENABLE_PYSTACK
 Q(pystack exhausted)
 #endif
+
+#if MICROPY_PY_TSTRINGS
+Q(string.templatelib)
+#endif
diff --git a/py/reader.c b/py/reader.c
index 57b12b3b655..8feb6d75275 100644
--- a/py/reader.c
+++ b/py/reader.c
@@ -39,7 +39,7 @@ typedef struct _mp_reader_mem_t {
     const byte *end;
 } mp_reader_mem_t;
 
-STATIC mp_uint_t mp_reader_mem_readbyte(void *data) {
+static mp_uint_t mp_reader_mem_readbyte(void *data) {
     mp_reader_mem_t *reader = (mp_reader_mem_t *)data;
     if (reader->cur < reader->end) {
         return *reader->cur++;
@@ -48,9 +48,9 @@ STATIC mp_uint_t mp_reader_mem_readbyte(void *data) {
     }
 }
 
-STATIC void mp_reader_mem_close(void *data) {
+static void mp_reader_mem_close(void *data) {
     mp_reader_mem_t *reader = (mp_reader_mem_t *)data;
-    if (reader->free_len > 0) {
+    if (reader->free_len > 0 && reader->free_len != MP_READER_IS_ROM) {
         m_del(char, (char *)reader->beg, reader->free_len);
     }
     m_del_obj(mp_reader_mem_t, reader);
@@ -67,6 +67,19 @@ void mp_reader_new_mem(mp_reader_t *reader, const byte *buf, size_t len, size_t
     reader->close = mp_reader_mem_close;
 }
 
+const uint8_t *mp_reader_try_read_rom(mp_reader_t *reader, size_t len) {
+    if (reader->readbyte != mp_reader_mem_readbyte) {
+        return NULL;
+    }
+    mp_reader_mem_t *m = reader->data;
+    if (m->free_len != MP_READER_IS_ROM) {
+        return NULL;
+    }
+    const uint8_t *data = m->cur;
+    m->cur += len;
+    return data;
+}
+
 #if MICROPY_READER_POSIX
 
 #include 
@@ -81,7 +94,7 @@ typedef struct _mp_reader_posix_t {
     byte buf[20];
 } mp_reader_posix_t;
 
-STATIC mp_uint_t mp_reader_posix_readbyte(void *data) {
+static mp_uint_t mp_reader_posix_readbyte(void *data) {
     mp_reader_posix_t *reader = (mp_reader_posix_t *)data;
     if (reader->pos >= reader->len) {
         if (reader->len == 0) {
@@ -101,7 +114,7 @@ STATIC mp_uint_t mp_reader_posix_readbyte(void *data) {
     return reader->buf[reader->pos++];
 }
 
-STATIC void mp_reader_posix_close(void *data) {
+static void mp_reader_posix_close(void *data) {
     mp_reader_posix_t *reader = (mp_reader_posix_t *)data;
     if (reader->close_fd) {
         MP_THREAD_GIL_EXIT();
@@ -134,12 +147,12 @@ void mp_reader_new_file_from_fd(mp_reader_t *reader, int fd, bool close_fd) {
 
 #if !MICROPY_VFS_POSIX
 // If MICROPY_VFS_POSIX is defined then this function is provided by the VFS layer
-void mp_reader_new_file(mp_reader_t *reader, const char *filename) {
+void mp_reader_new_file(mp_reader_t *reader, qstr filename) {
     MP_THREAD_GIL_EXIT();
-    int fd = open(filename, O_RDONLY, 0644);
+    int fd = open(qstr_str(filename), O_RDONLY, 0644);
     MP_THREAD_GIL_ENTER();
     if (fd < 0) {
-        mp_raise_OSError_with_filename(errno, filename);
+        mp_raise_OSError_with_filename(errno, qstr_str(filename));
     }
     mp_reader_new_file_from_fd(reader, fd, true);
 }
diff --git a/py/reader.h b/py/reader.h
index 8511c72ce50..6378457007c 100644
--- a/py/reader.h
+++ b/py/reader.h
@@ -28,6 +28,10 @@
 
 #include "py/obj.h"
 
+// Pass to the `free_len` argument to `mp_reader_new_mem` to indicate that the data is in ROM.
+// This means that the data is addressable and will remain valid at least until a soft reset.
+#define MP_READER_IS_ROM ((size_t)-1)
+
 // the readbyte function must return the next byte in the input stream
 // it must return MP_READER_EOF if end of stream
 // it can be called again after returning MP_READER_EOF, and in that case must return MP_READER_EOF
@@ -40,7 +44,12 @@ typedef struct _mp_reader_t {
 } mp_reader_t;
 
 void mp_reader_new_mem(mp_reader_t *reader, const byte *buf, size_t len, size_t free_len);
-void mp_reader_new_file(mp_reader_t *reader, const char *filename);
+void mp_reader_new_file(mp_reader_t *reader, qstr filename);
 void mp_reader_new_file_from_fd(mp_reader_t *reader, int fd, bool close_fd);
 
+// Try to efficiently read the given number of bytes from a ROM-based reader.
+// Returns a valid, non-NULL pointer to the requested data if the reader points to ROM.
+// Returns NULL if the reader does not point to ROM.
+const uint8_t *mp_reader_try_read_rom(mp_reader_t *reader, size_t len);
+
 #endif // MICROPY_INCLUDED_PY_READER_H
diff --git a/py/repl.c b/py/repl.c
index 0369b021951..87cb50d4785 100644
--- a/py/repl.c
+++ b/py/repl.c
@@ -43,7 +43,7 @@ const char *mp_repl_get_psx(unsigned int entry) {
 }
 #endif
 
-STATIC bool str_startswith_word(const char *str, const char *head) {
+static bool str_startswith_word(const char *str, const char *head) {
     size_t i;
     for (i = 0; str[i] && head[i]; i++) {
         if (str[i] != head[i]) {
@@ -154,7 +154,7 @@ bool mp_repl_continue_with_input(const char *input) {
     return false;
 }
 
-STATIC bool test_qstr(mp_obj_t obj, qstr name) {
+static bool test_qstr(mp_obj_t obj, qstr name) {
     if (obj) {
         // try object member
         mp_obj_t dest[2];
@@ -162,12 +162,15 @@ STATIC bool test_qstr(mp_obj_t obj, qstr name) {
         return dest[0] != MP_OBJ_NULL;
     } else {
         // try builtin module
-        return mp_map_lookup((mp_map_t *)&mp_builtin_module_map,
-            MP_OBJ_NEW_QSTR(name), MP_MAP_LOOKUP);
+        return mp_map_lookup((mp_map_t *)&mp_builtin_module_map, MP_OBJ_NEW_QSTR(name), MP_MAP_LOOKUP)
+               #if MICROPY_HAVE_REGISTERED_EXTENSIBLE_MODULES
+               || mp_map_lookup((mp_map_t *)&mp_builtin_extensible_module_map, MP_OBJ_NEW_QSTR(name), MP_MAP_LOOKUP)
+               #endif
+        ;
     }
 }
 
-STATIC const char *find_completions(const char *s_start, size_t s_len,
+static const char *find_completions(const char *s_start, size_t s_len,
     mp_obj_t obj, size_t *match_len, qstr *q_first, qstr *q_last) {
 
     const char *match_str = NULL;
@@ -207,7 +210,7 @@ STATIC const char *find_completions(const char *s_start, size_t s_len,
     return match_str;
 }
 
-STATIC void print_completions(const mp_print_t *print,
+static void print_completions(const mp_print_t *print,
     const char *s_start, size_t s_len,
     mp_obj_t obj, qstr q_first, qstr q_last) {
 
@@ -218,6 +221,10 @@ STATIC void print_completions(const mp_print_t *print,
     for (qstr q = q_first; q <= q_last; ++q) {
         size_t d_len;
         const char *d_str = (const char *)qstr_data(q, &d_len);
+        // filter out words that begin with underscore unless there's already a partial match
+        if (s_len == 0 && d_str[0] == '_') {
+            continue;
+        }
         if (s_len <= d_len && strncmp(s_start, d_str, s_len) == 0) {
             if (test_qstr(obj, q)) {
                 int gap = (line_len + WORD_SLOT_LEN - 1) / WORD_SLOT_LEN * WORD_SLOT_LEN - line_len;
@@ -225,7 +232,6 @@ STATIC void print_completions(const mp_print_t *print,
                     gap += WORD_SLOT_LEN;
                 }
                 if (line_len + gap + d_len <= MAX_LINE_LEN) {
-                    // TODO optimise printing of gap?
                     for (int j = 0; j < gap; ++j) {
                         mp_print_str(print, " ");
                     }
diff --git a/py/ringbuf.c b/py/ringbuf.c
index 10dca62081e..5f77271a06a 100644
--- a/py/ringbuf.c
+++ b/py/ringbuf.c
@@ -24,8 +24,6 @@
  * THE SOFTWARE.
  */
 
-#include 
-
 #include "ringbuf.h"
 
 int ringbuf_get16(ringbuf_t *r) {
@@ -83,17 +81,7 @@ int ringbuf_get_bytes(ringbuf_t *r, uint8_t *data, size_t data_len) {
     if (ringbuf_avail(r) < data_len) {
         return (r->size <= data_len) ? -2 : -1;
     }
-    uint32_t iget = r->iget;
-    uint32_t iget_a = (iget + data_len) % r->size;
-    uint8_t *datap = data;
-    if (iget_a < iget) {
-        // Copy part of the data from the space left at the end of the buffer
-        memcpy(datap, r->buf + iget, r->size - iget);
-        datap += (r->size - iget);
-        iget = 0;
-    }
-    memcpy(datap, r->buf + iget, iget_a - iget);
-    r->iget = iget_a;
+    ringbuf_memcpy_get_internal(r, data, data_len);
     return 0;
 }
 
@@ -105,16 +93,6 @@ int ringbuf_put_bytes(ringbuf_t *r, const uint8_t *data, size_t data_len) {
     if (ringbuf_free(r) < data_len) {
         return (r->size <= data_len) ? -2 : -1;
     }
-    uint32_t iput = r->iput;
-    uint32_t iput_a = (iput + data_len) % r->size;
-    const uint8_t *datap = data;
-    if (iput_a < iput) {
-        // Copy part of the data to the end of the buffer
-        memcpy(r->buf + iput, datap, r->size - iput);
-        datap += (r->size - iput);
-        iput = 0;
-    }
-    memcpy(r->buf + iput, datap, iput_a - iput);
-    r->iput = iput_a;
+    ringbuf_memcpy_put_internal(r, data, data_len);
     return 0;
 }
diff --git a/py/ringbuf.h b/py/ringbuf.h
index c8508c07edf..d3c0f460138 100644
--- a/py/ringbuf.h
+++ b/py/ringbuf.h
@@ -28,10 +28,9 @@
 
 #include 
 #include 
+#include 
 
-#ifdef _MSC_VER
-#include "py/mpconfig.h" // For inline.
-#endif
+#include "py/mpconfig.h"
 
 typedef struct _ringbuf_t {
     uint8_t *buf;
@@ -52,6 +51,11 @@ typedef struct _ringbuf_t {
         (r)->iget = (r)->iput = 0; \
     }
 
+static inline void ringbuf_reset(ringbuf_t *r) {
+    // Reset the ringbuffer to empty
+    r->iget = r->iput = 0;
+}
+
 static inline int ringbuf_get(ringbuf_t *r) {
     if (r->iget == r->iput) {
         return -1;
@@ -91,6 +95,38 @@ static inline size_t ringbuf_avail(ringbuf_t *r) {
     return (r->size + r->iput - r->iget) % r->size;
 }
 
+static inline void ringbuf_memcpy_get_internal(ringbuf_t *r, uint8_t *data, size_t data_len) {
+    // No bounds / space checking is performed here so ensure available size is checked before running this
+    // otherwise data loss or buffer overflow can occur.
+    uint32_t iget = r->iget;
+    uint32_t iget_a = (iget + data_len) % r->size;
+    uint8_t *datap = data;
+    if (iget_a < iget) {
+        // Copy part of the data from the space left at the end of the buffer
+        memcpy(datap, r->buf + iget, r->size - iget);
+        datap += (r->size - iget);
+        iget = 0;
+    }
+    memcpy(datap, r->buf + iget, iget_a - iget);
+    r->iget = iget_a;
+}
+
+static inline void ringbuf_memcpy_put_internal(ringbuf_t *r, const uint8_t *data, size_t data_len) {
+    // No bounds / space checking is performed here so ensure free size is checked before running this
+    // otherwise data loss or buffer overflow can occur.
+    uint32_t iput = r->iput;
+    uint32_t iput_a = (iput + data_len) % r->size;
+    const uint8_t *datap = data;
+    if (iput_a < iput) {
+        // Copy part of the data to the end of the buffer
+        memcpy(r->buf + iput, datap, r->size - iput);
+        datap += (r->size - iput);
+        iput = 0;
+    }
+    memcpy(r->buf + iput, datap, iput_a - iput);
+    r->iput = iput_a;
+}
+
 // Note: big-endian. No-op if not enough room available for both bytes.
 int ringbuf_get16(ringbuf_t *r);
 int ringbuf_peek16(ringbuf_t *r);
diff --git a/py/runtime.c b/py/runtime.c
index 2326dfb3cac..dcfb9689a70 100644
--- a/py/runtime.c
+++ b/py/runtime.c
@@ -40,11 +40,16 @@
 #include "py/objmodule.h"
 #include "py/objgenerator.h"
 #include "py/smallint.h"
+#include "py/stream.h"
 #include "py/runtime.h"
 #include "py/builtin.h"
-#include "py/stackctrl.h"
+#include "py/cstack.h"
 #include "py/gc.h"
 
+#if MICROPY_VFS_ROM && MICROPY_VFS_ROM_IOCTL
+#include "extmod/vfs.h"
+#endif
+
 #if MICROPY_DEBUG_VERBOSE // print debugging info
 #define DEBUG_PRINT (1)
 #define DEBUG_printf DEBUG_printf
@@ -69,11 +74,10 @@ void mp_init(void) {
     // no pending exceptions to start with
     MP_STATE_THREAD(mp_pending_exception) = MP_OBJ_NULL;
     #if MICROPY_ENABLE_SCHEDULER
+    // no pending callbacks to start with
+    MP_STATE_VM(sched_state) = MP_SCHED_IDLE;
     #if MICROPY_SCHEDULER_STATIC_NODES
-    if (MP_STATE_VM(sched_head) == NULL) {
-        // no pending callbacks to start with
-        MP_STATE_VM(sched_state) = MP_SCHED_IDLE;
-    } else {
+    if (MP_STATE_VM(sched_head) != NULL) {
         // pending callbacks are on the list, eg from before a soft reset
         MP_STATE_VM(sched_state) = MP_SCHED_PENDING;
     }
@@ -101,6 +105,9 @@ void mp_init(void) {
     #if MICROPY_EMIT_NATIVE
     MP_STATE_VM(default_emit_opt) = MP_EMIT_OPT_NONE;
     #endif
+    #if MICROPY_DEBUG_PRINTERS
+    MP_STATE_VM(mp_verbose_flag) = 0;
+    #endif
     #endif
 
     // init global module dict
@@ -119,8 +126,8 @@ void mp_init(void) {
     MP_STATE_VM(mp_module_builtins_override_dict) = NULL;
     #endif
 
-    #if MICROPY_PERSISTENT_CODE_TRACK_RELOC_CODE
-    MP_STATE_VM(track_reloc_code_list) = MP_OBJ_NULL;
+    #if (MICROPY_EMIT_INLINE_ASM || MICROPY_ENABLE_NATIVE_CODE) && (MICROPY_PERSISTENT_CODE_TRACK_FUN_DATA || MICROPY_PERSISTENT_CODE_TRACK_BSS_RODATA)
+    MP_STATE_VM(persistent_code_root_pointers) = MP_OBJ_NULL;
     #endif
 
     #if MICROPY_PY_OS_DUPTERM
@@ -136,13 +143,17 @@ void mp_init(void) {
     #endif
 
     #if MICROPY_PY_SYS_PATH_ARGV_DEFAULTS
-    mp_obj_list_init(MP_OBJ_TO_PTR(mp_sys_path), 0);
+    #if MICROPY_PY_SYS_PATH
+    mp_sys_path = mp_obj_new_list(0, NULL);
     mp_obj_list_append(mp_sys_path, MP_OBJ_NEW_QSTR(MP_QSTR_)); // current dir (or base dir of the script)
     #if MICROPY_MODULE_FROZEN
     mp_obj_list_append(mp_sys_path, MP_OBJ_NEW_QSTR(MP_QSTR__dot_frozen));
     #endif
+    #endif
+    #if MICROPY_PY_SYS_ARGV
     mp_obj_list_init(MP_OBJ_TO_PTR(mp_sys_argv), 0);
     #endif
+    #endif // MICROPY_PY_SYS_PATH_ARGV_DEFAULTS
 
     #if MICROPY_PY_SYS_ATEXIT
     MP_STATE_VM(sys_exitfunc) = mp_const_none;
@@ -167,6 +178,14 @@ void mp_init(void) {
     MP_STATE_VM(bluetooth) = MP_OBJ_NULL;
     #endif
 
+    #if MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
+    MP_STATE_VM(usbd) = MP_OBJ_NULL;
+    #endif
+
+    #if MICROPY_PY_WEAKREF
+    mp_map_init(&MP_STATE_VM(mp_weakref_map), 0);
+    #endif
+
     #if MICROPY_PY_THREAD_GIL
     mp_thread_mutex_init(&MP_STATE_VM(gil_mutex));
     #endif
@@ -177,6 +196,11 @@ void mp_init(void) {
     #endif
 
     MP_THREAD_GIL_ENTER();
+
+    #if MICROPY_VFS_ROM && MICROPY_VFS_ROM_IOCTL
+    // Mount ROMFS if it exists.
+    mp_vfs_mount_romfs_protected();
+    #endif
 }
 
 void mp_deinit(void) {
@@ -194,6 +218,11 @@ void mp_globals_locals_set_from_nlr_jump_callback(void *ctx_in) {
     mp_locals_set(ctx->locals);
 }
 
+void mp_call_function_1_from_nlr_jump_callback(void *ctx_in) {
+    nlr_jump_callback_node_call_function_1_t *ctx = ctx_in;
+    ctx->func(ctx->arg);
+}
+
 mp_obj_t MICROPY_WRAP_MP_LOAD_NAME(mp_load_name)(qstr qst) {
     // logic: search locals, globals, builtins
     DEBUG_OP_printf("load name %s\n", qstr_str(qst));
@@ -408,7 +437,7 @@ mp_obj_t MICROPY_WRAP_MP_BINARY_OP(mp_binary_op)(mp_binary_op_t op, mp_obj_t lhs
             // Operations that can overflow:
             //      +       result always fits in mp_int_t, then handled by SMALL_INT check
             //      -       result always fits in mp_int_t, then handled by SMALL_INT check
-            //      *       checked explicitly
+            //      *       checked explicitly for fit in mp_int_t, then handled by SMALL_INT check
             //      /       if lhs=MIN and rhs=-1; result always fits in mp_int_t, then handled by SMALL_INT check
             //      %       if lhs=MIN and rhs=-1; result always fits in mp_int_t, then handled by SMALL_INT check
             //      <<      checked explicitly
@@ -467,30 +496,16 @@ mp_obj_t MICROPY_WRAP_MP_BINARY_OP(mp_binary_op)(mp_binary_op_t op, mp_obj_t lhs
                     break;
                 case MP_BINARY_OP_MULTIPLY:
                 case MP_BINARY_OP_INPLACE_MULTIPLY: {
-
-                    // If long long type exists and is larger than mp_int_t, then
-                    // we can use the following code to perform overflow-checked multiplication.
-                    // Otherwise (eg in x64 case) we must use mp_small_int_mul_overflow.
-                    #if 0
-                    // compute result using long long precision
-                    long long res = (long long)lhs_val * (long long)rhs_val;
-                    if (res > MP_SMALL_INT_MAX || res < MP_SMALL_INT_MIN) {
-                        // result overflowed SMALL_INT, so return higher precision integer
-                        return mp_obj_new_int_from_ll(res);
-                    } else {
-                        // use standard precision
-                        lhs_val = (mp_int_t)res;
-                    }
-                    #endif
-
-                    if (mp_small_int_mul_overflow(lhs_val, rhs_val)) {
+                    mp_int_t int_res;
+                    if (mp_mul_mp_int_t_overflow(lhs_val, rhs_val, &int_res)) {
                         // use higher precision
                         lhs = mp_obj_new_int_from_ll(lhs_val);
                         goto generic_binary_op;
                     } else {
                         // use standard precision
-                        return MP_OBJ_NEW_SMALL_INT(lhs_val * rhs_val);
+                        lhs_val = int_res;
                     }
+                    break;
                 }
                 case MP_BINARY_OP_FLOOR_DIVIDE:
                 case MP_BINARY_OP_INPLACE_FLOOR_DIVIDE:
@@ -530,19 +545,19 @@ mp_obj_t MICROPY_WRAP_MP_BINARY_OP(mp_binary_op)(mp_binary_op_t op, mp_obj_t lhs
                         mp_int_t ans = 1;
                         while (rhs_val > 0) {
                             if (rhs_val & 1) {
-                                if (mp_small_int_mul_overflow(ans, lhs_val)) {
+                                if (mp_mul_mp_int_t_overflow(ans, lhs_val, &ans)) {
                                     goto power_overflow;
                                 }
-                                ans *= lhs_val;
                             }
                             if (rhs_val == 1) {
                                 break;
                             }
                             rhs_val /= 2;
-                            if (mp_small_int_mul_overflow(lhs_val, lhs_val)) {
+                            mp_int_t int_res;
+                            if (mp_mul_mp_int_t_overflow(lhs_val, lhs_val, &int_res)) {
                                 goto power_overflow;
                             }
-                            lhs_val *= lhs_val;
+                            lhs_val = int_res;
                         }
                         lhs_val = ans;
                     }
@@ -722,7 +737,7 @@ mp_obj_t mp_call_method_n_kw(size_t n_args, size_t n_kw, const mp_obj_t *args) {
 
 // This function only needs to be exposed externally when in stackless mode.
 #if !MICROPY_STACKLESS
-STATIC
+static
 #endif
 void mp_call_prepare_args_n_kw_var(bool have_self, size_t n_args_n_kw, const mp_obj_t *args, mp_call_args_t *out_args) {
     mp_obj_t fun = *args++;
@@ -1065,7 +1080,7 @@ typedef struct _mp_obj_checked_fun_t {
     mp_obj_t fun;
 } mp_obj_checked_fun_t;
 
-STATIC mp_obj_t checked_fun_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t checked_fun_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     mp_obj_checked_fun_t *self = MP_OBJ_TO_PTR(self_in);
     if (n_args > 0) {
         const mp_obj_type_t *arg0_type = mp_obj_get_type(args[0]);
@@ -1081,14 +1096,14 @@ STATIC mp_obj_t checked_fun_call(mp_obj_t self_in, size_t n_args, size_t n_kw, c
     return mp_call_function_n_kw(self->fun, n_args, n_kw, args);
 }
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+static MP_DEFINE_CONST_OBJ_TYPE(
     mp_type_checked_fun,
     MP_QSTR_function,
     MP_TYPE_FLAG_BINDS_SELF,
     call, checked_fun_call
     );
 
-STATIC mp_obj_t mp_obj_new_checked_fun(const mp_obj_type_t *type, mp_obj_t fun) {
+static mp_obj_t mp_obj_new_checked_fun(const mp_obj_type_t *type, mp_obj_t fun) {
     mp_obj_checked_fun_t *o = mp_obj_malloc(mp_obj_checked_fun_t, &mp_type_checked_fun);
     o->type = type;
     o->fun = fun;
@@ -1140,6 +1155,10 @@ void mp_convert_member_lookup(mp_obj_t self, const mp_obj_type_t *type, mp_obj_t
             // base type (which is what is passed in the `type` argument to this function).
             if (self != MP_OBJ_NULL) {
                 type = mp_obj_get_type(self);
+                if (type == &mp_type_type) {
+                    // `self` is already a type, so use `self` directly.
+                    type = MP_OBJ_TO_PTR(self);
+                }
             }
             dest[0] = ((mp_obj_static_class_method_t *)MP_OBJ_TO_PTR(member))->fun;
             dest[1] = MP_OBJ_FROM_PTR(type);
@@ -1221,6 +1240,19 @@ void mp_load_method(mp_obj_t base, qstr attr, mp_obj_t *dest) {
             mp_raise_msg_varg(&mp_type_AttributeError,
                 MP_ERROR_TEXT("type object '%q' has no attribute '%q'"),
                 ((mp_obj_type_t *)MP_OBJ_TO_PTR(base))->name, attr);
+        #if MICROPY_MODULE___ALL__ && MICROPY_ERROR_REPORTING >= MICROPY_ERROR_REPORTING_DETAILED
+        } else if (mp_obj_is_type(base, &mp_type_module)) {
+            // report errors in __all__ as done by CPython
+            mp_obj_t dest_name[2];
+            qstr module_name = MP_QSTR_;
+            mp_load_method_maybe(base, MP_QSTR___name__, dest_name);
+            if (mp_obj_is_qstr(dest_name[0])) {
+                module_name = mp_obj_str_get_qstr(dest_name[0]);
+            }
+            mp_raise_msg_varg(&mp_type_AttributeError,
+                MP_ERROR_TEXT("module '%q' has no attribute '%q'"),
+                module_name, attr);
+        #endif
         } else {
             mp_raise_msg_varg(&mp_type_AttributeError,
                 MP_ERROR_TEXT("'%s' object has no attribute '%q'"),
@@ -1317,9 +1349,8 @@ mp_obj_t mp_getiter(mp_obj_t o_in, mp_obj_iter_buf_t *iter_buf) {
 
 }
 
-STATIC mp_fun_1_t type_get_iternext(const mp_obj_type_t *type) {
+static mp_fun_1_t type_get_iternext(const mp_obj_type_t *type) {
     if ((type->flags & MP_TYPE_FLAG_ITER_IS_STREAM) == MP_TYPE_FLAG_ITER_IS_STREAM) {
-        mp_obj_t mp_stream_unbuffered_iter(mp_obj_t self);
         return mp_stream_unbuffered_iter;
     } else if (type->flags & MP_TYPE_FLAG_ITER_IS_ITERNEXT) {
         return (mp_fun_1_t)MP_OBJ_TYPE_GET_SLOT(type, iter);
@@ -1358,7 +1389,7 @@ mp_obj_t mp_iternext_allow_raise(mp_obj_t o_in) {
 // will always return MP_OBJ_STOP_ITERATION instead of raising StopIteration() (or any subclass thereof)
 // may raise other exceptions
 mp_obj_t mp_iternext(mp_obj_t o_in) {
-    MP_STACK_CHECK(); // enumerate, filter, map and zip can recursively call mp_iternext
+    mp_cstack_check(); // enumerate, filter, map and zip can recursively call mp_iternext
     const mp_obj_type_t *type = mp_obj_get_type(o_in);
     if (TYPE_HAS_ITERNEXT(type)) {
         MP_STATE_THREAD(stop_iteration_arg) = MP_OBJ_NULL;
@@ -1484,7 +1515,7 @@ mp_obj_t mp_make_raise_obj(mp_obj_t o) {
     }
 
     if (mp_obj_is_exception_instance(o)) {
-        // o is an instance of an exception, so use it as the exception
+        // o is a fully-constructed instance of an exception, so use it as the exception
         return o;
     } else {
         // o cannot be used as an exception, so return a type error (which will be raised by the caller)
@@ -1498,7 +1529,7 @@ mp_obj_t mp_import_name(qstr name, mp_obj_t fromlist, mp_obj_t level) {
     // build args array
     mp_obj_t args[5];
     args[0] = MP_OBJ_NEW_QSTR(name);
-    args[1] = mp_const_none; // TODO should be globals
+    args[1] = MP_OBJ_FROM_PTR(mp_globals_get()); // globals of the current context
     args[2] = mp_const_none; // TODO should be locals
     args[3] = fromlist;
     args[4] = level;
@@ -1523,14 +1554,11 @@ mp_obj_t mp_import_from(mp_obj_t module, qstr name) {
     mp_obj_t dest[2];
 
     mp_load_method_maybe(module, name, dest);
-
     if (dest[1] != MP_OBJ_NULL) {
-        // Hopefully we can't import bound method from an object
-    import_error:
-        mp_raise_msg_varg(&mp_type_ImportError, MP_ERROR_TEXT("can't import name %q"), name);
-    }
-
-    if (dest[0] != MP_OBJ_NULL) {
+        // Importing a bound method from a class instance.
+        return mp_obj_new_bound_meth(dest[0], dest[1]);
+    } else if (dest[0] != MP_OBJ_NULL) {
+        // Importing a function or attribute.
         return dest[0];
     }
 
@@ -1563,13 +1591,46 @@ mp_obj_t mp_import_from(mp_obj_t module, qstr name) {
     goto import_error;
 
     #endif
+
+import_error:
+    mp_raise_msg_varg(&mp_type_ImportError, MP_ERROR_TEXT("can't import name %q"), name);
 }
 
 void mp_import_all(mp_obj_t module) {
     DEBUG_printf("import all %p\n", module);
 
-    // TODO: Support __all__
-    mp_map_t *map = &mp_obj_module_get_globals(module)->map;
+    mp_obj_t dest[2];
+
+    #if MICROPY_MODULE___ALL__
+    mp_load_method_protected(module, MP_QSTR___all__, dest, false);
+    if (dest[0] != MP_OBJ_NULL) {
+        // When __all__ is defined, we must explicitly load all specified
+        // symbols, possibly invoking the module __getattr__ function
+        size_t len;
+        mp_obj_t *items;
+        mp_obj_get_array(dest[0], &len, &items);
+        for (size_t i = 0; i < len; i++) {
+            qstr qname = mp_obj_str_get_qstr(items[i]);
+            mp_load_method(module, qname, dest);
+            mp_store_name(qname, dest[0]);
+        }
+        return;
+    }
+    #endif
+
+    #if MICROPY_CPYTHON_COMPAT
+    // Load the dict from the module.  In MicroPython, if __dict__ is
+    // available then it always returns a native mp_obj_dict_t instance.
+    mp_load_method(module, MP_QSTR___dict__, dest);
+    #else
+    // Without MICROPY_CPYTHON_COMPAT __dict__ is not available, so just
+    // assume the given module is actually an mp_obj_module_t instance.
+    dest[0] = MP_OBJ_FROM_PTR(mp_obj_module_get_globals(module));
+    #endif
+
+    // By default, the set of public names includes all names found in the module's
+    // namespace which do not begin with an underscore character ('_')
+    mp_map_t *map = mp_obj_dict_get_map(dest[0]);
     for (size_t i = 0; i < map->alloc; i++) {
         if (mp_map_slot_is_filled(map, i)) {
             // Entry in module global scope may be generated programmatically
@@ -1604,10 +1665,13 @@ mp_obj_t mp_parse_compile_execute(mp_lexer_t *lex, mp_parse_input_kind_t parse_i
     mp_obj_t module_fun = mp_compile(&parse_tree, source_name, parse_input_kind == MP_PARSE_SINGLE_INPUT);
 
     mp_obj_t ret;
-    if (MICROPY_PY_BUILTINS_COMPILE && globals == NULL) {
+    #if MICROPY_PY_BUILTINS_COMPILE && MICROPY_PY_BUILTINS_CODE == MICROPY_PY_BUILTINS_CODE_MINIMUM
+    if (globals == NULL) {
         // for compile only, return value is the module function
         ret = module_fun;
-    } else {
+    } else
+    #endif
+    {
         // execute module function and get return value
         ret = mp_call_function_0(module_fun);
     }
@@ -1621,38 +1685,38 @@ mp_obj_t mp_parse_compile_execute(mp_lexer_t *lex, mp_parse_input_kind_t parse_i
 
 #endif // MICROPY_ENABLE_COMPILER
 
-NORETURN void m_malloc_fail(size_t num_bytes) {
-    DEBUG_printf("memory allocation failed, allocating %u bytes\n", (uint)num_bytes);
+MP_NORETURN void m_malloc_fail(size_t num_bytes) {
+    DEBUG_printf("memory allocation failed, allocating " SIZE_FMT " bytes\n", num_bytes);
     #if MICROPY_ENABLE_GC
     if (gc_is_locked()) {
         mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("memory allocation failed, heap is locked"));
     }
     #endif
     mp_raise_msg_varg(&mp_type_MemoryError,
-        MP_ERROR_TEXT("memory allocation failed, allocating %u bytes"), (uint)num_bytes);
+        MP_ERROR_TEXT("memory allocation failed, allocating " SIZE_FMT " bytes"), num_bytes);
 }
 
 #if MICROPY_ERROR_REPORTING == MICROPY_ERROR_REPORTING_NONE
 
-NORETURN void mp_raise_type(const mp_obj_type_t *exc_type) {
+MP_NORETURN void mp_raise_type(const mp_obj_type_t *exc_type) {
     nlr_raise(mp_obj_new_exception(exc_type));
 }
 
-NORETURN void mp_raise_ValueError_no_msg(void) {
+MP_NORETURN void mp_raise_ValueError_no_msg(void) {
     mp_raise_type(&mp_type_ValueError);
 }
 
-NORETURN void mp_raise_TypeError_no_msg(void) {
+MP_NORETURN void mp_raise_TypeError_no_msg(void) {
     mp_raise_type(&mp_type_TypeError);
 }
 
-NORETURN void mp_raise_NotImplementedError_no_msg(void) {
+MP_NORETURN void mp_raise_NotImplementedError_no_msg(void) {
     mp_raise_type(&mp_type_NotImplementedError);
 }
 
 #else
 
-NORETURN void mp_raise_msg(const mp_obj_type_t *exc_type, mp_rom_error_text_t msg) {
+MP_NORETURN void mp_raise_msg(const mp_obj_type_t *exc_type, mp_rom_error_text_t msg) {
     if (msg == NULL) {
         nlr_raise(mp_obj_new_exception(exc_type));
     } else {
@@ -1660,7 +1724,7 @@ NORETURN void mp_raise_msg(const mp_obj_type_t *exc_type, mp_rom_error_text_t ms
     }
 }
 
-NORETURN void mp_raise_msg_varg(const mp_obj_type_t *exc_type, mp_rom_error_text_t fmt, ...) {
+MP_NORETURN void mp_raise_msg_varg(const mp_obj_type_t *exc_type, mp_rom_error_text_t fmt, ...) {
     va_list args;
     va_start(args, fmt);
     mp_obj_t exc = mp_obj_new_exception_msg_vlist(exc_type, fmt, args);
@@ -1668,25 +1732,25 @@ NORETURN void mp_raise_msg_varg(const mp_obj_type_t *exc_type, mp_rom_error_text
     nlr_raise(exc);
 }
 
-NORETURN void mp_raise_ValueError(mp_rom_error_text_t msg) {
+MP_NORETURN void mp_raise_ValueError(mp_rom_error_text_t msg) {
     mp_raise_msg(&mp_type_ValueError, msg);
 }
 
-NORETURN void mp_raise_TypeError(mp_rom_error_text_t msg) {
+MP_NORETURN void mp_raise_TypeError(mp_rom_error_text_t msg) {
     mp_raise_msg(&mp_type_TypeError, msg);
 }
 
-NORETURN void mp_raise_NotImplementedError(mp_rom_error_text_t msg) {
+MP_NORETURN void mp_raise_NotImplementedError(mp_rom_error_text_t msg) {
     mp_raise_msg(&mp_type_NotImplementedError, msg);
 }
 
 #endif
 
-NORETURN void mp_raise_type_arg(const mp_obj_type_t *exc_type, mp_obj_t arg) {
+MP_NORETURN void mp_raise_type_arg(const mp_obj_type_t *exc_type, mp_obj_t arg) {
     nlr_raise(mp_obj_new_exception_arg1(exc_type, arg));
 }
 
-NORETURN void mp_raise_StopIteration(mp_obj_t arg) {
+MP_NORETURN void mp_raise_StopIteration(mp_obj_t arg) {
     if (arg == MP_OBJ_NULL) {
         mp_raise_type(&mp_type_StopIteration);
     } else {
@@ -1694,7 +1758,7 @@ NORETURN void mp_raise_StopIteration(mp_obj_t arg) {
     }
 }
 
-NORETURN void mp_raise_TypeError_int_conversion(mp_const_obj_t arg) {
+MP_NORETURN void mp_raise_TypeError_int_conversion(mp_const_obj_t arg) {
     #if MICROPY_ERROR_REPORTING <= MICROPY_ERROR_REPORTING_TERSE
     (void)arg;
     mp_raise_TypeError(MP_ERROR_TEXT("can't convert to int"));
@@ -1704,11 +1768,11 @@ NORETURN void mp_raise_TypeError_int_conversion(mp_const_obj_t arg) {
     #endif
 }
 
-NORETURN void mp_raise_OSError(int errno_) {
+MP_NORETURN void mp_raise_OSError(int errno_) {
     mp_raise_type_arg(&mp_type_OSError, MP_OBJ_NEW_SMALL_INT(errno_));
 }
 
-NORETURN void mp_raise_OSError_with_filename(int errno_, const char *filename) {
+MP_NORETURN void mp_raise_OSError_with_filename(int errno_, const char *filename) {
     vstr_t vstr;
     vstr_init(&vstr, 32);
     vstr_printf(&vstr, "can't open %s", filename);
@@ -1717,8 +1781,8 @@ NORETURN void mp_raise_OSError_with_filename(int errno_, const char *filename) {
     nlr_raise(mp_obj_exception_make_new(&mp_type_OSError, 2, 0, args));
 }
 
-#if MICROPY_STACK_CHECK || MICROPY_ENABLE_PYSTACK
-NORETURN void mp_raise_recursion_depth(void) {
+#if MICROPY_STACK_CHECK
+MP_NORETURN void mp_raise_recursion_depth(void) {
     mp_raise_type_arg(&mp_type_RuntimeError, MP_OBJ_NEW_QSTR(MP_QSTR_maximum_space_recursion_space_depth_space_exceeded));
 }
 #endif
diff --git a/py/runtime.h b/py/runtime.h
index 78194973d61..7547caddd03 100644
--- a/py/runtime.h
+++ b/py/runtime.h
@@ -28,6 +28,14 @@
 
 #include "py/mpstate.h"
 #include "py/pystack.h"
+#include "py/cstack.h"
+
+// Initialize an nlr_jump_callback_node_call_function_1_t struct for use with
+// nlr_push_jump_callback(&ctx.callback, mp_call_function_1_from_nlr_jump_callback);
+#define MP_DEFINE_NLR_JUMP_CALLBACK_FUNCTION_1(ctx, f, a) \
+    nlr_jump_callback_node_call_function_1_t ctx; \
+    ctx.func = (void (*)(void *))(f); \
+    ctx.arg = (a)
 
 typedef enum {
     MP_VM_RETURN_NORMAL,
@@ -44,6 +52,13 @@ typedef enum {
     MP_ARG_KW_ONLY   = 0x200,
 } mp_arg_flag_t;
 
+// These first two enum values match the original signature of `mp_handle_pending(bool)`.
+typedef enum {
+    MP_HANDLE_PENDING_CALLBACKS_AND_CLEAR_EXCEPTIONS = false,
+    MP_HANDLE_PENDING_CALLBACKS_AND_EXCEPTIONS = true,
+    MP_HANDLE_PENDING_CALLBACKS_ONLY,
+} mp_handle_pending_behaviour_t;
+
 typedef union _mp_arg_val_t {
     bool u_bool;
     mp_int_t u_int;
@@ -73,6 +88,13 @@ typedef struct _nlr_jump_callback_node_globals_locals_t {
     mp_obj_dict_t *locals;
 } nlr_jump_callback_node_globals_locals_t;
 
+// For use with mp_call_function_1_from_nlr_jump_callback.
+typedef struct _nlr_jump_callback_node_call_function_1_t {
+    nlr_jump_callback_node_t callback;
+    void (*func)(void *);
+    void *arg;
+} nlr_jump_callback_node_call_function_1_t;
+
 // Tables mapping operator enums to qstrs, defined in objtype.c
 extern const byte mp_unary_op_method_name[];
 extern const byte mp_binary_op_method_name[];
@@ -85,7 +107,8 @@ void mp_sched_keyboard_interrupt(void);
 #if MICROPY_ENABLE_VM_ABORT
 void mp_sched_vm_abort(void);
 #endif
-void mp_handle_pending(bool raise_exc);
+
+void mp_handle_pending(mp_handle_pending_behaviour_t behavior);
 
 #if MICROPY_ENABLE_SCHEDULER
 void mp_sched_lock(void);
@@ -95,8 +118,25 @@ bool mp_sched_schedule(mp_obj_t function, mp_obj_t arg);
 bool mp_sched_schedule_node(mp_sched_node_t *node, mp_sched_callback_t callback);
 #endif
 
+// Handles any pending MicroPython events without waiting for an interrupt or event.
+void mp_event_handle_nowait(void);
+
+// Handles any pending MicroPython events and then suspends execution until the
+// next interrupt or event.
+//
+// Note: on "tickless" ports this can suspend execution for a long time,
+// don't call unless you know an interrupt is coming to continue execution.
+// On "ticked" ports it may return early due to the tick interrupt.
+void mp_event_wait_indefinite(void);
+
+// Handle any pending MicroPython events and then suspends execution until the
+// next interrupt or event, or until timeout_ms milliseconds have elapsed.
+//
+// On "ticked" ports it may return early due to the tick interrupt.
+void mp_event_wait_ms(mp_uint_t timeout_ms);
+
 // extra printing method specifically for mp_obj_t's which are integral type
-int mp_print_mp_int(const mp_print_t *print, mp_obj_t x, int base, int base_char, int flags, char fill, int width, int prec);
+int mp_print_mp_int(const mp_print_t *print, mp_obj_t x, unsigned base, int base_char, int flags, char fill, int width, int prec);
 
 void mp_arg_check_num_sig(size_t n_args, size_t n_kw, uint32_t sig);
 static inline void mp_arg_check_num(size_t n_args, size_t n_kw, size_t n_args_min, size_t n_args_max, bool takes_kw) {
@@ -104,8 +144,8 @@ static inline void mp_arg_check_num(size_t n_args, size_t n_kw, size_t n_args_mi
 }
 void mp_arg_parse_all(size_t n_pos, const mp_obj_t *pos, mp_map_t *kws, size_t n_allowed, const mp_arg_t *allowed, mp_arg_val_t *out_vals);
 void mp_arg_parse_all_kw_array(size_t n_pos, size_t n_kw, const mp_obj_t *args, size_t n_allowed, const mp_arg_t *allowed, mp_arg_val_t *out_vals);
-NORETURN void mp_arg_error_terse_mismatch(void);
-NORETURN void mp_arg_error_unimpl_kw(void);
+MP_NORETURN void mp_arg_error_terse_mismatch(void);
+MP_NORETURN void mp_arg_error_unimpl_kw(void);
 
 static inline mp_obj_dict_t *mp_locals_get(void) {
     return MP_STATE_THREAD(dict_locals);
@@ -121,6 +161,39 @@ static inline void mp_globals_set(mp_obj_dict_t *d) {
 }
 
 void mp_globals_locals_set_from_nlr_jump_callback(void *ctx_in);
+void mp_call_function_1_from_nlr_jump_callback(void *ctx_in);
+
+#if MICROPY_PY_THREAD
+static inline void mp_thread_init_state(mp_state_thread_t *ts, size_t stack_size, mp_obj_dict_t *locals, mp_obj_dict_t *globals) {
+    mp_thread_set_state(ts);
+
+    mp_cstack_init_with_top(ts + 1, stack_size); // need to include ts in root-pointer scan
+
+    // GC starts off unlocked
+    ts->gc_lock_depth = 0;
+
+    // There are no pending jump callbacks or exceptions yet
+    ts->nlr_top = NULL;
+    ts->nlr_jump_callback_top = NULL;
+    ts->mp_pending_exception = MP_OBJ_NULL;
+
+    #if MICROPY_PY_SYS_SETTRACE
+    ts->prof_trace_callback = MP_OBJ_NULL;
+    ts->prof_callback_is_executing = false;
+    ts->current_code_state = NULL;
+    #endif
+
+    // If locals/globals are not given, inherit from main thread
+    if (locals == NULL) {
+        locals = mp_state_ctx.thread.dict_locals;
+    }
+    if (globals == NULL) {
+        globals = mp_state_ctx.thread.dict_globals;
+    }
+    mp_locals_set(locals);
+    mp_globals_set(globals);
+}
+#endif
 
 mp_obj_t mp_load_name(qstr qst);
 mp_obj_t mp_load_global(qstr qst);
@@ -187,10 +260,10 @@ mp_obj_t mp_import_from(mp_obj_t module, qstr name);
 void mp_import_all(mp_obj_t module);
 
 #if MICROPY_ERROR_REPORTING == MICROPY_ERROR_REPORTING_NONE
-NORETURN void mp_raise_type(const mp_obj_type_t *exc_type);
-NORETURN void mp_raise_ValueError_no_msg(void);
-NORETURN void mp_raise_TypeError_no_msg(void);
-NORETURN void mp_raise_NotImplementedError_no_msg(void);
+MP_NORETURN void mp_raise_type(const mp_obj_type_t *exc_type);
+MP_NORETURN void mp_raise_ValueError_no_msg(void);
+MP_NORETURN void mp_raise_TypeError_no_msg(void);
+MP_NORETURN void mp_raise_NotImplementedError_no_msg(void);
 #define mp_raise_msg(exc_type, msg) mp_raise_type(exc_type)
 #define mp_raise_msg_varg(exc_type, ...) mp_raise_type(exc_type)
 #define mp_raise_ValueError(msg) mp_raise_ValueError_no_msg()
@@ -198,19 +271,19 @@ NORETURN void mp_raise_NotImplementedError_no_msg(void);
 #define mp_raise_NotImplementedError(msg) mp_raise_NotImplementedError_no_msg()
 #else
 #define mp_raise_type(exc_type) mp_raise_msg(exc_type, NULL)
-NORETURN void mp_raise_msg(const mp_obj_type_t *exc_type, mp_rom_error_text_t msg);
-NORETURN void mp_raise_msg_varg(const mp_obj_type_t *exc_type, mp_rom_error_text_t fmt, ...);
-NORETURN void mp_raise_ValueError(mp_rom_error_text_t msg);
-NORETURN void mp_raise_TypeError(mp_rom_error_text_t msg);
-NORETURN void mp_raise_NotImplementedError(mp_rom_error_text_t msg);
+MP_NORETURN void mp_raise_msg(const mp_obj_type_t *exc_type, mp_rom_error_text_t msg);
+MP_NORETURN void mp_raise_msg_varg(const mp_obj_type_t *exc_type, mp_rom_error_text_t fmt, ...);
+MP_NORETURN void mp_raise_ValueError(mp_rom_error_text_t msg);
+MP_NORETURN void mp_raise_TypeError(mp_rom_error_text_t msg);
+MP_NORETURN void mp_raise_NotImplementedError(mp_rom_error_text_t msg);
 #endif
 
-NORETURN void mp_raise_type_arg(const mp_obj_type_t *exc_type, mp_obj_t arg);
-NORETURN void mp_raise_StopIteration(mp_obj_t arg);
-NORETURN void mp_raise_TypeError_int_conversion(mp_const_obj_t arg);
-NORETURN void mp_raise_OSError(int errno_);
-NORETURN void mp_raise_OSError_with_filename(int errno_, const char *filename);
-NORETURN void mp_raise_recursion_depth(void);
+MP_NORETURN void mp_raise_type_arg(const mp_obj_type_t *exc_type, mp_obj_t arg);
+MP_NORETURN void mp_raise_StopIteration(mp_obj_t arg);
+MP_NORETURN void mp_raise_TypeError_int_conversion(mp_const_obj_t arg);
+MP_NORETURN void mp_raise_OSError(int errno_);
+MP_NORETURN void mp_raise_OSError_with_filename(int errno_, const char *filename);
+MP_NORETURN void mp_raise_recursion_depth(void);
 
 #if MICROPY_BUILTIN_METHOD_CHECK_SELF_ARG
 #undef mp_check_self
@@ -227,8 +300,13 @@ int mp_native_type_from_qstr(qstr qst);
 mp_uint_t mp_native_from_obj(mp_obj_t obj, mp_uint_t type);
 mp_obj_t mp_native_to_obj(mp_uint_t val, mp_uint_t type);
 
-#define mp_sys_path (MP_OBJ_FROM_PTR(&MP_STATE_VM(mp_sys_path_obj)))
+#if MICROPY_PY_SYS_PATH
+#define mp_sys_path (MP_STATE_VM(sys_mutable[MP_SYS_MUTABLE_PATH]))
+#endif
+
+#if MICROPY_PY_SYS_ARGV
 #define mp_sys_argv (MP_OBJ_FROM_PTR(&MP_STATE_VM(mp_sys_argv_obj)))
+#endif
 
 #if MICROPY_WARNINGS
 #ifndef mp_warning
diff --git a/py/runtime0.h b/py/runtime0.h
index 6ef2d727c1e..9c6f0e079fb 100644
--- a/py/runtime0.h
+++ b/py/runtime0.h
@@ -26,8 +26,11 @@
 #ifndef MICROPY_INCLUDED_PY_RUNTIME0_H
 #define MICROPY_INCLUDED_PY_RUNTIME0_H
 
-// The first four must fit in 8 bits, see emitbc.c
-// The remaining must fit in 16 bits, see scope.h
+// These constants are used by:
+// - mp_raw_code_t::is_generator (only MP_SCOPE_FLAG_GENERATOR)
+// - scope_t::scope_flags (16 bits)
+// - MP_BC_PRELUDE_SIG_ENCODE macro, masked by MP_SCOPE_FLAG_ALL_SIG (4 bits)
+// - tools/mpy_ld.py, when generating mpy files (maximum 7 bits)
 #define MP_SCOPE_FLAG_ALL_SIG      (0x0f)
 #define MP_SCOPE_FLAG_GENERATOR    (0x01)
 #define MP_SCOPE_FLAG_VARKEYWORDS  (0x02)
@@ -50,6 +53,9 @@
 #define MP_NATIVE_TYPE_PTR16 (0x06)
 #define MP_NATIVE_TYPE_PTR32 (0x07)
 
+// Not use for viper, but for dynamic native modules
+#define MP_NATIVE_TYPE_QSTR (0x08)
+
 // Bytecode and runtime boundaries for unary ops
 #define MP_UNARY_OP_NUM_BYTECODE    (MP_UNARY_OP_NOT + 1)
 #define MP_UNARY_OP_NUM_RUNTIME     (MP_UNARY_OP_SIZEOF + 1)
diff --git a/py/runtime_utils.c b/py/runtime_utils.c
index b92c6bd767c..3b0ae279aa7 100644
--- a/py/runtime_utils.c
+++ b/py/runtime_utils.c
@@ -50,3 +50,59 @@ mp_obj_t mp_call_function_2_protected(mp_obj_t fun, mp_obj_t arg1, mp_obj_t arg2
         return MP_OBJ_NULL;
     }
 }
+
+#if !MICROPY_USE_GCC_MUL_OVERFLOW_INTRINSIC
+bool mp_mul_ll_overflow(long long int x, long long int y, long long int *res) {
+    bool overflow;
+
+    // Check for multiply overflow; see CERT INT32-C
+    if (x > 0) { // x is positive
+        if (y > 0) { // x and y are positive
+            overflow = (x > (LLONG_MAX / y));
+        } else { // x positive, y nonpositive
+            overflow = (y < (LLONG_MIN / x));
+        } // x positive, y nonpositive
+    } else { // x is nonpositive
+        if (y > 0) { // x is nonpositive, y is positive
+            overflow = (x < (LLONG_MIN / y));
+        } else { // x and y are nonpositive
+            overflow = (x != 0 && y < (LLONG_MAX / x));
+        } // End if x and y are nonpositive
+    } // End if x is nonpositive
+
+    if (!overflow) {
+        *res = x * y;
+    }
+
+    return overflow;
+}
+
+bool mp_mul_mp_int_t_overflow(mp_int_t x, mp_int_t y, mp_int_t *res) {
+    // Check for multiply overflow; see CERT INT32-C
+    if (x > 0) { // x is positive
+        if (y > 0) { // x and y are positive
+            if (x > (MP_INT_MAX / y)) {
+                return true;
+            }
+        } else { // x positive, y nonpositive
+            if (y < (MP_INT_MIN / x)) {
+                return true;
+            }
+        } // x positive, y nonpositive
+    } else { // x is nonpositive
+        if (y > 0) { // x is nonpositive, y is positive
+            if (x < (MP_INT_MIN / y)) {
+                return true;
+            }
+        } else { // x and y are nonpositive
+            if (x != 0 && y < (MP_INT_MAX / x)) {
+                return true;
+            }
+        } // End if x and y are nonpositive
+    } // End if x is nonpositive
+
+    // Result doesn't overflow
+    *res = x * y;
+    return false;
+}
+#endif
diff --git a/py/scheduler.c b/py/scheduler.c
index 165b26dc865..7849fcfe298 100644
--- a/py/scheduler.c
+++ b/py/scheduler.c
@@ -26,6 +26,7 @@
 
 #include 
 
+#include "py/mphal.h"
 #include "py/runtime.h"
 
 // Schedules an exception on the main thread (for exceptions "thrown" by async
@@ -87,17 +88,21 @@ static inline void mp_sched_run_pending(void) {
 
     #if MICROPY_SCHEDULER_STATIC_NODES
     // Run all pending C callbacks.
-    while (MP_STATE_VM(sched_head) != NULL) {
-        mp_sched_node_t *node = MP_STATE_VM(sched_head);
-        MP_STATE_VM(sched_head) = node->next;
-        if (MP_STATE_VM(sched_head) == NULL) {
-            MP_STATE_VM(sched_tail) = NULL;
-        }
-        mp_sched_callback_t callback = node->callback;
-        node->callback = NULL;
-        MICROPY_END_ATOMIC_SECTION(atomic_state);
-        callback(node);
-        atomic_state = MICROPY_BEGIN_ATOMIC_SECTION();
+    mp_sched_node_t *original_tail = MP_STATE_VM(sched_tail);
+    if (original_tail != NULL) {
+        mp_sched_node_t *node;
+        do {
+            node = MP_STATE_VM(sched_head);
+            MP_STATE_VM(sched_head) = node->next;
+            if (MP_STATE_VM(sched_head) == NULL) {
+                MP_STATE_VM(sched_tail) = NULL;
+            }
+            mp_sched_callback_t callback = node->callback;
+            node->callback = NULL;
+            MICROPY_END_ATOMIC_SECTION(atomic_state);
+            callback(node);
+            atomic_state = MICROPY_BEGIN_ATOMIC_SECTION();
+        } while (node != original_tail); // Don't execute any callbacks scheduled during this run
     }
     #endif
 
@@ -208,24 +213,27 @@ MP_REGISTER_ROOT_POINTER(mp_sched_item_t sched_queue[MICROPY_SCHEDULER_DEPTH]);
 
 // Called periodically from the VM or from "waiting" code (e.g. sleep) to
 // process background tasks and pending exceptions (e.g. KeyboardInterrupt).
-void mp_handle_pending(bool raise_exc) {
+void mp_handle_pending(mp_handle_pending_behaviour_t behavior) {
+    bool handle_exceptions = (behavior != MP_HANDLE_PENDING_CALLBACKS_ONLY);
+    bool raise_exceptions = (behavior == MP_HANDLE_PENDING_CALLBACKS_AND_EXCEPTIONS);
+
     // Handle pending VM abort.
     #if MICROPY_ENABLE_VM_ABORT
-    if (MP_STATE_VM(vm_abort) && mp_thread_is_main_thread()) {
+    if (handle_exceptions && MP_STATE_VM(vm_abort) && mp_thread_is_main_thread()) {
         MP_STATE_VM(vm_abort) = false;
-        if (raise_exc && nlr_get_abort() != NULL) {
+        if (raise_exceptions && nlr_get_abort() != NULL) {
             nlr_jump_abort();
         }
     }
     #endif
 
     // Handle any pending exception.
-    if (MP_STATE_THREAD(mp_pending_exception) != MP_OBJ_NULL) {
+    if (handle_exceptions && MP_STATE_THREAD(mp_pending_exception) != MP_OBJ_NULL) {
         mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION();
         mp_obj_t obj = MP_STATE_THREAD(mp_pending_exception);
         if (obj != MP_OBJ_NULL) {
             MP_STATE_THREAD(mp_pending_exception) = MP_OBJ_NULL;
-            if (raise_exc) {
+            if (raise_exceptions) {
                 MICROPY_END_ATOMIC_SECTION(atomic_state);
                 nlr_raise(obj);
             }
@@ -235,8 +243,50 @@ void mp_handle_pending(bool raise_exc) {
 
     // Handle any pending callbacks.
     #if MICROPY_ENABLE_SCHEDULER
-    if (MP_STATE_VM(sched_state) == MP_SCHED_PENDING) {
+    bool run_scheduler = (MP_STATE_VM(sched_state) == MP_SCHED_PENDING);
+    #if MICROPY_PY_THREAD && !MICROPY_PY_THREAD_GIL
+    // Avoid races by running the scheduler on the main thread, only.
+    // (Not needed if GIL enabled, as GIL ensures thread safety here.)
+    run_scheduler = run_scheduler && mp_thread_is_main_thread();
+    #endif
+    if (run_scheduler) {
         mp_sched_run_pending();
     }
     #endif
 }
+
+// Handles any pending MicroPython events without waiting for an interrupt or event.
+void mp_event_handle_nowait(void) {
+    #if defined(MICROPY_EVENT_POLL_HOOK_FAST) && !MICROPY_PREVIEW_VERSION_2
+    // For ports still using the old macros.
+    MICROPY_EVENT_POLL_HOOK_FAST
+    #else
+    // Process any port layer (non-blocking) events.
+    MICROPY_INTERNAL_EVENT_HOOK;
+    mp_handle_pending(MP_HANDLE_PENDING_CALLBACKS_AND_EXCEPTIONS);
+    #endif
+}
+
+// Handles any pending MicroPython events and then suspends execution until the
+// next interrupt or event.
+void mp_event_wait_indefinite(void) {
+    #if defined(MICROPY_EVENT_POLL_HOOK) && !MICROPY_PREVIEW_VERSION_2
+    // For ports still using the old macros.
+    MICROPY_EVENT_POLL_HOOK
+    #else
+    mp_event_handle_nowait();
+    MICROPY_INTERNAL_WFE(-1);
+    #endif
+}
+
+// Handle any pending MicroPython events and then suspends execution until the
+// next interrupt or event, or until timeout_ms milliseconds have elapsed.
+void mp_event_wait_ms(mp_uint_t timeout_ms) {
+    #if defined(MICROPY_EVENT_POLL_HOOK) && !MICROPY_PREVIEW_VERSION_2
+    // For ports still using the old macros.
+    MICROPY_EVENT_POLL_HOOK
+    #else
+    mp_event_handle_nowait();
+    MICROPY_INTERNAL_WFE(timeout_ms);
+    #endif
+}
diff --git a/py/scope.c b/py/scope.c
index 8fc0943289b..4893e7cc4e5 100644
--- a/py/scope.c
+++ b/py/scope.c
@@ -31,7 +31,9 @@
 #if MICROPY_ENABLE_COMPILER
 
 // These low numbered qstrs should fit in 8 bits.  See assertions below.
-STATIC const uint8_t scope_simple_name_table[] = {
+// The (unescaped) names appear in `unsorted_str_list` in the QSTR
+// generator script py/makeqstrdata.py to ensure they are assigned low numbers.
+static const uint8_t scope_simple_name_table[] = {
     [SCOPE_MODULE] = MP_QSTR__lt_module_gt_,
     [SCOPE_LAMBDA] = MP_QSTR__lt_lambda_gt_,
     [SCOPE_LIST_COMP] = MP_QSTR__lt_listcomp_gt_,
@@ -111,7 +113,7 @@ id_info_t *scope_find_global(scope_t *scope, qstr qst) {
     return scope_find(scope, qst);
 }
 
-STATIC void scope_close_over_in_parents(scope_t *scope, qstr qst) {
+static void scope_close_over_in_parents(scope_t *scope, qstr qst) {
     assert(scope->parent != NULL); // we should have at least 1 parent
     for (scope_t *s = scope->parent;; s = s->parent) {
         assert(s->parent != NULL); // we should not get to the outer scope
diff --git a/py/scope.h b/py/scope.h
index e7d2a304f79..927c4a7b93e 100644
--- a/py/scope.h
+++ b/py/scope.h
@@ -76,6 +76,9 @@ typedef struct _scope_t {
     struct _scope_t *next;
     mp_parse_node_t pn;
     mp_raw_code_t *raw_code;
+    #if MICROPY_DEBUG_PRINTERS
+    size_t raw_code_data_len; // for mp_bytecode_print
+    #endif
     uint16_t simple_name; // a qstr
     uint16_t scope_flags;  // see runtime0.h
     uint16_t emit_options; // see emitglue.h
diff --git a/py/sequence.c b/py/sequence.c
index 58386074312..9eb67ada6e8 100644
--- a/py/sequence.c
+++ b/py/sequence.c
@@ -63,11 +63,7 @@ bool mp_seq_get_fast_slice_indexes(mp_uint_t len, mp_obj_t slice, mp_bound_slice
     return indexes->step == 1;
 }
 
-#endif
-
-mp_obj_t mp_seq_extract_slice(size_t len, const mp_obj_t *seq, mp_bound_slice_t *indexes) {
-    (void)len; // TODO can we remove len from the arg list?
-
+mp_obj_t mp_seq_extract_slice(const mp_obj_t *seq, mp_bound_slice_t *indexes) {
     mp_int_t start = indexes->start, stop = indexes->stop;
     mp_int_t step = indexes->step;
 
@@ -87,6 +83,8 @@ mp_obj_t mp_seq_extract_slice(size_t len, const mp_obj_t *seq, mp_bound_slice_t
     return res;
 }
 
+#endif
+
 // Special-case comparison function for sequences of bytes
 // Don't pass MP_BINARY_OP_NOT_EQUAL here
 bool mp_seq_cmp_bytes(mp_uint_t op, const byte *data1, size_t len1, const byte *data2, size_t len2) {
diff --git a/py/showbc.c b/py/showbc.c
index f9c334b93bb..68e2f33138c 100644
--- a/py/showbc.c
+++ b/py/showbc.c
@@ -83,7 +83,7 @@
     DECODE_UINT; \
     unum = (mp_uint_t)obj_table[unum]
 
-void mp_bytecode_print(const mp_print_t *print, const mp_raw_code_t *rc, const mp_module_constants_t *cm) {
+void mp_bytecode_print(const mp_print_t *print, const mp_raw_code_t *rc, size_t fun_data_len, const mp_module_constants_t *cm) {
     const byte *ip_start = rc->fun_data;
     const byte *ip = rc->fun_data;
 
@@ -99,14 +99,14 @@ void mp_bytecode_print(const mp_print_t *print, const mp_raw_code_t *rc, const m
     #else
     qstr source_file = cm->source_file;
     #endif
-    mp_printf(print, "File %s, code block '%s' (descriptor: %p, bytecode @%p %u bytes)\n",
-        qstr_str(source_file), qstr_str(block_name), rc, ip_start, (unsigned)rc->fun_data_len);
+    mp_printf(print, "File %q, code block '%q' (descriptor: %p, bytecode @%p %u bytes)\n",
+        source_file, block_name, rc, ip_start, (unsigned)fun_data_len);
 
     // raw bytecode dump
     size_t prelude_size = ip - ip_start + n_info + n_cell;
     mp_printf(print, "Raw bytecode (code_info_size=%u, bytecode_size=%u):\n",
-        (unsigned)prelude_size, (unsigned)(rc->fun_data_len - prelude_size));
-    for (size_t i = 0; i < rc->fun_data_len; i++) {
+        (unsigned)prelude_size, (unsigned)(fun_data_len - prelude_size));
+    for (size_t i = 0; i < fun_data_len; i++) {
         if (i > 0 && i % 16 == 0) {
             mp_printf(print, "\n");
         }
@@ -121,7 +121,7 @@ void mp_bytecode_print(const mp_print_t *print, const mp_raw_code_t *rc, const m
         #if MICROPY_EMIT_BYTECODE_USES_QSTR_TABLE
         qst = cm->qstr_table[qst];
         #endif
-        mp_printf(print, " %s", qstr_str(qst));
+        mp_printf(print, " %q", qst);
     }
     mp_printf(print, "\n");
 
@@ -144,21 +144,13 @@ void mp_bytecode_print(const mp_print_t *print, const mp_raw_code_t *rc, const m
         mp_uint_t source_line = 1;
         mp_printf(print, "  bc=" INT_FMT " line=" UINT_FMT "\n", bc, source_line);
         for (const byte *ci = code_info; ci < line_info_top;) {
-            if ((ci[0] & 0x80) == 0) {
-                // 0b0LLBBBBB encoding
-                bc += ci[0] & 0x1f;
-                source_line += ci[0] >> 5;
-                ci += 1;
-            } else {
-                // 0b1LLLBBBB 0bLLLLLLLL encoding (l's LSB in second byte)
-                bc += ci[0] & 0xf;
-                source_line += ((ci[0] << 4) & 0x700) | ci[1];
-                ci += 2;
-            }
+            mp_code_lineinfo_t decoded = mp_bytecode_decode_lineinfo(&ci);
+            bc += decoded.bc_increment;
+            source_line += decoded.line_increment;
             mp_printf(print, "  bc=" INT_FMT " line=" UINT_FMT "\n", bc, source_line);
         }
     }
-    mp_bytecode_print2(print, ip, rc->fun_data_len - prelude_size, rc->children, cm);
+    mp_bytecode_print2(print, ip, fun_data_len - prelude_size, rc->children, cm);
 }
 
 const byte *mp_bytecode_print_str(const mp_print_t *print, const byte *ip_start, const byte *ip, mp_raw_code_t *const *child_table, const mp_module_constants_t *cm) {
@@ -197,7 +189,7 @@ const byte *mp_bytecode_print_str(const mp_print_t *print, const byte *ip_start,
 
         case MP_BC_LOAD_CONST_STRING:
             DECODE_QSTR;
-            mp_printf(print, "LOAD_CONST_STRING '%s'", qstr_str(qst));
+            mp_printf(print, "LOAD_CONST_STRING '%q'", qst);
             break;
 
         case MP_BC_LOAD_CONST_OBJ:
@@ -222,27 +214,27 @@ const byte *mp_bytecode_print_str(const mp_print_t *print, const byte *ip_start,
 
         case MP_BC_LOAD_NAME:
             DECODE_QSTR;
-            mp_printf(print, "LOAD_NAME %s", qstr_str(qst));
+            mp_printf(print, "LOAD_NAME %q", qst);
             break;
 
         case MP_BC_LOAD_GLOBAL:
             DECODE_QSTR;
-            mp_printf(print, "LOAD_GLOBAL %s", qstr_str(qst));
+            mp_printf(print, "LOAD_GLOBAL %q", qst);
             break;
 
         case MP_BC_LOAD_ATTR:
             DECODE_QSTR;
-            mp_printf(print, "LOAD_ATTR %s", qstr_str(qst));
+            mp_printf(print, "LOAD_ATTR %q", qst);
             break;
 
         case MP_BC_LOAD_METHOD:
             DECODE_QSTR;
-            mp_printf(print, "LOAD_METHOD %s", qstr_str(qst));
+            mp_printf(print, "LOAD_METHOD %q", qst);
             break;
 
         case MP_BC_LOAD_SUPER_METHOD:
             DECODE_QSTR;
-            mp_printf(print, "LOAD_SUPER_METHOD %s", qstr_str(qst));
+            mp_printf(print, "LOAD_SUPER_METHOD %q", qst);
             break;
 
         case MP_BC_LOAD_BUILD_CLASS:
@@ -265,17 +257,17 @@ const byte *mp_bytecode_print_str(const mp_print_t *print, const byte *ip_start,
 
         case MP_BC_STORE_NAME:
             DECODE_QSTR;
-            mp_printf(print, "STORE_NAME %s", qstr_str(qst));
+            mp_printf(print, "STORE_NAME %q", qst);
             break;
 
         case MP_BC_STORE_GLOBAL:
             DECODE_QSTR;
-            mp_printf(print, "STORE_GLOBAL %s", qstr_str(qst));
+            mp_printf(print, "STORE_GLOBAL %q", qst);
             break;
 
         case MP_BC_STORE_ATTR:
             DECODE_QSTR;
-            mp_printf(print, "STORE_ATTR %s", qstr_str(qst));
+            mp_printf(print, "STORE_ATTR %q", qst);
             break;
 
         case MP_BC_STORE_SUBSCR:
@@ -294,12 +286,12 @@ const byte *mp_bytecode_print_str(const mp_print_t *print, const byte *ip_start,
 
         case MP_BC_DELETE_NAME:
             DECODE_QSTR;
-            mp_printf(print, "DELETE_NAME %s", qstr_str(qst));
+            mp_printf(print, "DELETE_NAME %q", qst);
             break;
 
         case MP_BC_DELETE_GLOBAL:
             DECODE_QSTR;
-            mp_printf(print, "DELETE_GLOBAL %s", qstr_str(qst));
+            mp_printf(print, "DELETE_GLOBAL %q", qst);
             break;
 
         case MP_BC_DUP_TOP:
@@ -514,12 +506,12 @@ const byte *mp_bytecode_print_str(const mp_print_t *print, const byte *ip_start,
 
         case MP_BC_IMPORT_NAME:
             DECODE_QSTR;
-            mp_printf(print, "IMPORT_NAME '%s'", qstr_str(qst));
+            mp_printf(print, "IMPORT_NAME '%q'", qst);
             break;
 
         case MP_BC_IMPORT_FROM:
             DECODE_QSTR;
-            mp_printf(print, "IMPORT_FROM '%s'", qstr_str(qst));
+            mp_printf(print, "IMPORT_FROM '%q'", qst);
             break;
 
         case MP_BC_IMPORT_STAR:
@@ -535,10 +527,10 @@ const byte *mp_bytecode_print_str(const mp_print_t *print, const byte *ip_start,
                 mp_printf(print, "STORE_FAST " UINT_FMT, (mp_uint_t)ip[-1] - MP_BC_STORE_FAST_MULTI);
             } else if (ip[-1] < MP_BC_UNARY_OP_MULTI + MP_UNARY_OP_NUM_BYTECODE) {
                 mp_uint_t op = ip[-1] - MP_BC_UNARY_OP_MULTI;
-                mp_printf(print, "UNARY_OP " UINT_FMT " %s", op, qstr_str(mp_unary_op_method_name[op]));
+                mp_printf(print, "UNARY_OP " UINT_FMT " %q", op, (qstr)mp_unary_op_method_name[op]);
             } else if (ip[-1] < MP_BC_BINARY_OP_MULTI + MP_BINARY_OP_NUM_BYTECODE) {
                 mp_uint_t op = ip[-1] - MP_BC_BINARY_OP_MULTI;
-                mp_printf(print, "BINARY_OP " UINT_FMT " %s", op, qstr_str(mp_binary_op_method_name[op]));
+                mp_printf(print, "BINARY_OP " UINT_FMT " %q", op, (qstr)mp_binary_op_method_name[op]);
             } else {
                 mp_printf(print, "code %p, byte code 0x%02x not implemented\n", ip - 1, ip[-1]);
                 assert(0);
diff --git a/py/smallint.c b/py/smallint.c
index aa542ca7bf2..eb99b58667a 100644
--- a/py/smallint.c
+++ b/py/smallint.c
@@ -26,32 +26,6 @@
 
 #include "py/smallint.h"
 
-bool mp_small_int_mul_overflow(mp_int_t x, mp_int_t y) {
-    // Check for multiply overflow; see CERT INT32-C
-    if (x > 0) { // x is positive
-        if (y > 0) { // x and y are positive
-            if (x > (MP_SMALL_INT_MAX / y)) {
-                return true;
-            }
-        } else { // x positive, y nonpositive
-            if (y < (MP_SMALL_INT_MIN / x)) {
-                return true;
-            }
-        } // x positive, y nonpositive
-    } else { // x is nonpositive
-        if (y > 0) { // x is nonpositive, y is positive
-            if (x < (MP_SMALL_INT_MIN / y)) {
-                return true;
-            }
-        } else { // x and y are nonpositive
-            if (x != 0 && y < (MP_SMALL_INT_MAX / x)) {
-                return true;
-            }
-        } // End if x and y are nonpositive
-    } // End if x is nonpositive
-    return false;
-}
-
 mp_int_t mp_small_int_modulo(mp_int_t dividend, mp_int_t divisor) {
     // Python specs require that mod has same sign as second operand
     dividend %= divisor;
diff --git a/py/smallint.h b/py/smallint.h
index 584e0018d1b..ec5b0af3b28 100644
--- a/py/smallint.h
+++ b/py/smallint.h
@@ -68,7 +68,6 @@
 // The number of bits in a MP_SMALL_INT including the sign bit.
 #define MP_SMALL_INT_BITS (MP_IMAX_BITS(MP_SMALL_INT_MAX) + 1)
 
-bool mp_small_int_mul_overflow(mp_int_t x, mp_int_t y);
 mp_int_t mp_small_int_modulo(mp_int_t dividend, mp_int_t divisor);
 mp_int_t mp_small_int_floor_divide(mp_int_t num, mp_int_t denom);
 
diff --git a/py/stackctrl.c b/py/stackctrl.c
index c2566ebad92..303e9cffff1 100644
--- a/py/stackctrl.c
+++ b/py/stackctrl.c
@@ -24,7 +24,12 @@
  * THE SOFTWARE.
  */
 
+// This API is deprecated, please use py/cstack.h instead
+
 #include "py/runtime.h"
+
+#if !MICROPY_PREVIEW_VERSION_2
+
 #include "py/stackctrl.h"
 
 void mp_stack_ctrl_init(void) {
@@ -62,3 +67,5 @@ void mp_stack_check(void) {
 }
 
 #endif // MICROPY_STACK_CHECK
+
+#endif // !MICROPY_PREVIEW_VERSION_2
diff --git a/py/stackctrl.h b/py/stackctrl.h
index c21288b2b1f..9bf7330ccc3 100644
--- a/py/stackctrl.h
+++ b/py/stackctrl.h
@@ -26,8 +26,12 @@
 #ifndef MICROPY_INCLUDED_PY_STACKCTRL_H
 #define MICROPY_INCLUDED_PY_STACKCTRL_H
 
+// This API is deprecated, please use py/cstack.h instead
+
 #include "py/mpconfig.h"
 
+#if !MICROPY_PREVIEW_VERSION_2
+
 void mp_stack_ctrl_init(void);
 void mp_stack_set_top(void *top);
 mp_uint_t mp_stack_usage(void);
@@ -43,6 +47,8 @@ void mp_stack_check(void);
 #define mp_stack_set_limit(limit) (void)(limit)
 #define MP_STACK_CHECK()
 
-#endif
+#endif // MICROPY_STACK_CHECK
+
+#endif // !MICROPY_PREVIEW_VERSION_2
 
 #endif // MICROPY_INCLUDED_PY_STACKCTRL_H
diff --git a/py/stream.c b/py/stream.c
index 36325e7d414..4d71964e7a3 100644
--- a/py/stream.c
+++ b/py/stream.c
@@ -38,7 +38,7 @@
 // TODO: should be in mpconfig.h
 #define DEFAULT_BUFFER_SIZE 256
 
-STATIC mp_obj_t stream_readall(mp_obj_t self_in);
+static mp_obj_t stream_readall(mp_obj_t self_in);
 
 // Returns error condition in *errcode, if non-zero, return value is number of bytes written
 // before error condition occurred. If *errcode == 0, returns total bytes written (which will
@@ -82,6 +82,18 @@ mp_uint_t mp_stream_rw(mp_obj_t stream, void *buf_, mp_uint_t size, int *errcode
     return done;
 }
 
+mp_off_t mp_stream_seek(mp_obj_t stream, mp_off_t offset, int whence, int *errcode) {
+    struct mp_stream_seek_t seek_s;
+    seek_s.offset = offset;
+    seek_s.whence = whence;
+    const mp_stream_p_t *stream_p = mp_get_stream(stream);
+    mp_uint_t res = stream_p->ioctl(MP_OBJ_FROM_PTR(stream), MP_STREAM_SEEK, (mp_uint_t)(uintptr_t)&seek_s, errcode);
+    if (res == MP_STREAM_ERROR) {
+        return (mp_off_t)-1;
+    }
+    return seek_s.offset;
+}
+
 const mp_stream_p_t *mp_get_stream_raise(mp_obj_t self_in, int flags) {
     const mp_obj_type_t *type = mp_obj_get_type(self_in);
     if (MP_OBJ_TYPE_HAS_SLOT(type, protocol)) {
@@ -96,7 +108,18 @@ const mp_stream_p_t *mp_get_stream_raise(mp_obj_t self_in, int flags) {
     mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("stream operation not supported"));
 }
 
-STATIC mp_obj_t stream_read_generic(size_t n_args, const mp_obj_t *args, byte flags) {
+static MP_NORETURN void mp_stream_raise_error(mp_obj_t stream, int error) {
+    #if MICROPY_STREAMS_DELEGATE_ERROR
+    const mp_stream_p_t *stream_p = mp_get_stream(stream);
+    if (stream_p->ioctl != NULL) {
+        int err;
+        stream_p->ioctl(stream, MP_STREAM_RAISE_ERROR, error, &err);
+    }
+    #endif
+    mp_raise_OSError(error);
+}
+
+static mp_obj_t stream_read_generic(size_t n_args, const mp_obj_t *args, byte flags) {
     // What to do if sz < -1?  Python docs don't specify this case.
     // CPython does a readall, but here we silently let negatives through,
     // and they will cause a MemoryError.
@@ -139,7 +162,7 @@ STATIC mp_obj_t stream_read_generic(size_t n_args, const mp_obj_t *args, byte fl
                     }
                     break;
                 }
-                mp_raise_OSError(error);
+                mp_stream_raise_error(args[0], error);
             }
 
             if (out_sz < more_bytes) {
@@ -207,7 +230,7 @@ STATIC mp_obj_t stream_read_generic(size_t n_args, const mp_obj_t *args, byte fl
             // this as EOF.
             return mp_const_none;
         }
-        mp_raise_OSError(error);
+        mp_stream_raise_error(args[0], error);
     } else {
         vstr.len = out_sz;
         if (stream_p->is_text) {
@@ -218,12 +241,12 @@ STATIC mp_obj_t stream_read_generic(size_t n_args, const mp_obj_t *args, byte fl
     }
 }
 
-STATIC mp_obj_t stream_read(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t stream_read(size_t n_args, const mp_obj_t *args) {
     return stream_read_generic(n_args, args, MP_STREAM_RW_READ);
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_read_obj, 1, 2, stream_read);
 
-STATIC mp_obj_t stream_read1(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t stream_read1(size_t n_args, const mp_obj_t *args) {
     return stream_read_generic(n_args, args, MP_STREAM_RW_READ | MP_STREAM_RW_ONCE);
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_read1_obj, 1, 2, stream_read1);
@@ -238,7 +261,7 @@ mp_obj_t mp_stream_write(mp_obj_t self_in, const void *buf, size_t len, byte fla
             // no single byte could be readily written to it."
             return mp_const_none;
         }
-        mp_raise_OSError(error);
+        mp_stream_raise_error(self_in, error);
     } else {
         return MP_OBJ_NEW_SMALL_INT(out_sz);
     }
@@ -249,9 +272,14 @@ void mp_stream_write_adaptor(void *self, const char *buf, size_t len) {
     mp_stream_write(MP_OBJ_FROM_PTR(self), buf, len, MP_STREAM_RW_WRITE);
 }
 
-STATIC mp_obj_t stream_write_method(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t stream_readinto_write_generic(size_t n_args, const mp_obj_t *args, byte flags) {
     mp_buffer_info_t bufinfo;
-    mp_get_buffer_raise(args[1], &bufinfo, MP_BUFFER_READ);
+    mp_get_buffer_raise(args[1], &bufinfo, (flags & MP_STREAM_RW_WRITE) ? MP_BUFFER_READ : MP_BUFFER_WRITE);
+
+    // CPython extension, allow optional maximum length and offset:
+    // - stream.operation(buf, max_len)
+    // - stream.operation(buf, off, max_len)
+    // Similar to https://docs.python.org/3/library/socket.html#socket.socket.recv_into
     size_t max_len = (size_t)-1;
     size_t off = 0;
     if (n_args == 3) {
@@ -264,46 +292,32 @@ STATIC mp_obj_t stream_write_method(size_t n_args, const mp_obj_t *args) {
         }
     }
     bufinfo.len -= off;
-    return mp_stream_write(args[0], (byte *)bufinfo.buf + off, MIN(bufinfo.len, max_len), MP_STREAM_RW_WRITE);
-}
-MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_write_obj, 2, 4, stream_write_method);
 
-STATIC mp_obj_t stream_write1_method(mp_obj_t self_in, mp_obj_t arg) {
-    mp_buffer_info_t bufinfo;
-    mp_get_buffer_raise(arg, &bufinfo, MP_BUFFER_READ);
-    return mp_stream_write(self_in, bufinfo.buf, bufinfo.len, MP_STREAM_RW_WRITE | MP_STREAM_RW_ONCE);
+    // Perform the readinto or write operation.
+    return mp_stream_write(args[0], (byte *)bufinfo.buf + off, MIN(bufinfo.len, max_len), flags);
 }
-MP_DEFINE_CONST_FUN_OBJ_2(mp_stream_write1_obj, stream_write1_method);
 
-STATIC mp_obj_t stream_readinto(size_t n_args, const mp_obj_t *args) {
-    mp_buffer_info_t bufinfo;
-    mp_get_buffer_raise(args[1], &bufinfo, MP_BUFFER_WRITE);
+static mp_obj_t stream_write_method(size_t n_args, const mp_obj_t *args) {
+    return stream_readinto_write_generic(n_args, args, MP_STREAM_RW_WRITE);
+}
+MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_write_obj, 2, 4, stream_write_method);
 
-    // CPython extension: if 2nd arg is provided, that's max len to read,
-    // instead of full buffer. Similar to
-    // https://docs.python.org/3/library/socket.html#socket.socket.recv_into
-    mp_uint_t len = bufinfo.len;
-    if (n_args > 2) {
-        len = mp_obj_get_int(args[2]);
-        if (len > bufinfo.len) {
-            len = bufinfo.len;
-        }
-    }
+static mp_obj_t stream_write1_method(size_t n_args, const mp_obj_t *args) {
+    return stream_readinto_write_generic(n_args, args, MP_STREAM_RW_WRITE | MP_STREAM_RW_ONCE);
+}
+MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_write1_obj, 2, 4, stream_write1_method);
 
-    int error;
-    mp_uint_t out_sz = mp_stream_read_exactly(args[0], bufinfo.buf, len, &error);
-    if (error != 0) {
-        if (mp_is_nonblocking_error(error)) {
-            return mp_const_none;
-        }
-        mp_raise_OSError(error);
-    } else {
-        return MP_OBJ_NEW_SMALL_INT(out_sz);
-    }
+static mp_obj_t stream_readinto(size_t n_args, const mp_obj_t *args) {
+    return stream_readinto_write_generic(n_args, args, MP_STREAM_RW_READ);
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_readinto_obj, 2, 3, stream_readinto);
 
-STATIC mp_obj_t stream_readall(mp_obj_t self_in) {
+static mp_obj_t stream_readinto1(size_t n_args, const mp_obj_t *args) {
+    return stream_readinto_write_generic(n_args, args, MP_STREAM_RW_READ | MP_STREAM_RW_ONCE);
+}
+MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_readinto1_obj, 2, 3, stream_readinto1);
+
+static mp_obj_t stream_readall(mp_obj_t self_in) {
     const mp_stream_p_t *stream_p = mp_get_stream(self_in);
 
     mp_uint_t total_size = 0;
@@ -324,7 +338,7 @@ STATIC mp_obj_t stream_readall(mp_obj_t self_in) {
                 }
                 break;
             }
-            mp_raise_OSError(error);
+            mp_stream_raise_error(self_in, error);
         }
         if (out_sz == 0) {
             break;
@@ -348,7 +362,7 @@ STATIC mp_obj_t stream_readall(mp_obj_t self_in) {
 }
 
 // Unbuffered, inefficient implementation of readline() for raw I/O files.
-STATIC mp_obj_t stream_unbuffered_readline(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t stream_unbuffered_readline(size_t n_args, const mp_obj_t *args) {
     const mp_stream_p_t *stream_p = mp_get_stream(args[0]);
 
     mp_int_t max_size = -1;
@@ -382,7 +396,7 @@ STATIC mp_obj_t stream_unbuffered_readline(size_t n_args, const mp_obj_t *args)
                     goto done;
                 }
             }
-            mp_raise_OSError(error);
+            mp_stream_raise_error(args[0], error);
         }
         if (out_sz == 0) {
         done:
@@ -406,7 +420,7 @@ STATIC mp_obj_t stream_unbuffered_readline(size_t n_args, const mp_obj_t *args)
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_unbuffered_readline_obj, 1, 2, stream_unbuffered_readline);
 
 // TODO take an optional extra argument (what does it do exactly?)
-STATIC mp_obj_t stream_unbuffered_readlines(mp_obj_t self) {
+static mp_obj_t stream_unbuffered_readlines(mp_obj_t self) {
     mp_obj_t lines = mp_obj_new_list(0, NULL);
     for (;;) {
         mp_obj_t line = stream_unbuffered_readline(1, &self);
@@ -432,58 +446,62 @@ mp_obj_t mp_stream_close(mp_obj_t stream) {
     int error;
     mp_uint_t res = stream_p->ioctl(stream, MP_STREAM_CLOSE, 0, &error);
     if (res == MP_STREAM_ERROR) {
-        mp_raise_OSError(error);
+        mp_stream_raise_error(stream, error);
     }
     return mp_const_none;
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_stream_close_obj, mp_stream_close);
 
-STATIC mp_obj_t stream_seek(size_t n_args, const mp_obj_t *args) {
-    struct mp_stream_seek_t seek_s;
+static mp_obj_t mp_stream___exit__(size_t n_args, const mp_obj_t *args) {
+    (void)n_args;
+    return mp_stream_close(args[0]);
+}
+MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream___exit___obj, 4, 4, mp_stream___exit__);
+
+static mp_obj_t stream_seek(size_t n_args, const mp_obj_t *args) {
     // TODO: Could be uint64
-    seek_s.offset = mp_obj_get_int(args[1]);
-    seek_s.whence = SEEK_SET;
+    mp_off_t offset = mp_obj_get_int(args[1]);
+    int whence = MP_SEEK_SET;
     if (n_args == 3) {
-        seek_s.whence = mp_obj_get_int(args[2]);
+        whence = mp_obj_get_int(args[2]);
     }
 
     // In POSIX, it's error to seek before end of stream, we enforce it here.
-    if (seek_s.whence == SEEK_SET && seek_s.offset < 0) {
+    if (whence == MP_SEEK_SET && offset < 0) {
         mp_raise_OSError(MP_EINVAL);
     }
 
-    const mp_stream_p_t *stream_p = mp_get_stream(args[0]);
     int error;
-    mp_uint_t res = stream_p->ioctl(args[0], MP_STREAM_SEEK, (mp_uint_t)(uintptr_t)&seek_s, &error);
-    if (res == MP_STREAM_ERROR) {
-        mp_raise_OSError(error);
+    mp_off_t res = mp_stream_seek(args[0], offset, whence, &error);
+    if (res == (mp_off_t)-1) {
+        mp_stream_raise_error(args[0], error);
     }
 
     // TODO: Could be uint64
-    return mp_obj_new_int_from_uint(seek_s.offset);
+    return mp_obj_new_int_from_uint(res);
 }
 MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_seek_obj, 2, 3, stream_seek);
 
-STATIC mp_obj_t stream_tell(mp_obj_t self) {
+static mp_obj_t stream_tell(mp_obj_t self) {
     mp_obj_t offset = MP_OBJ_NEW_SMALL_INT(0);
-    mp_obj_t whence = MP_OBJ_NEW_SMALL_INT(SEEK_CUR);
+    mp_obj_t whence = MP_OBJ_NEW_SMALL_INT(MP_SEEK_CUR);
     const mp_obj_t args[3] = {self, offset, whence};
     return stream_seek(3, args);
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_stream_tell_obj, stream_tell);
 
-STATIC mp_obj_t stream_flush(mp_obj_t self) {
+static mp_obj_t stream_flush(mp_obj_t self) {
     const mp_stream_p_t *stream_p = mp_get_stream(self);
     int error;
     mp_uint_t res = stream_p->ioctl(self, MP_STREAM_FLUSH, 0, &error);
     if (res == MP_STREAM_ERROR) {
-        mp_raise_OSError(error);
+        mp_stream_raise_error(self, error);
     }
     return mp_const_none;
 }
 MP_DEFINE_CONST_FUN_OBJ_1(mp_stream_flush_obj, stream_flush);
 
-STATIC mp_obj_t stream_ioctl(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t stream_ioctl(size_t n_args, const mp_obj_t *args) {
     mp_buffer_info_t bufinfo;
     uintptr_t val = 0;
     if (n_args > 2) {
@@ -498,7 +516,7 @@ STATIC mp_obj_t stream_ioctl(size_t n_args, const mp_obj_t *args) {
     int error;
     mp_uint_t res = stream_p->ioctl(args[0], mp_obj_get_int(args[1]), val, &error);
     if (res == MP_STREAM_ERROR) {
-        mp_raise_OSError(error);
+        mp_stream_raise_error(args[0], error);
     }
 
     return mp_obj_new_int(res);
@@ -539,16 +557,11 @@ ssize_t mp_stream_posix_read(void *stream, void *buf, size_t len) {
 }
 
 off_t mp_stream_posix_lseek(void *stream, off_t offset, int whence) {
-    const mp_obj_base_t *o = stream;
-    const mp_stream_p_t *stream_p = MP_OBJ_TYPE_GET_SLOT(o->type, protocol);
-    struct mp_stream_seek_t seek_s;
-    seek_s.offset = offset;
-    seek_s.whence = whence;
-    mp_uint_t res = stream_p->ioctl(MP_OBJ_FROM_PTR(stream), MP_STREAM_SEEK, (mp_uint_t)(uintptr_t)&seek_s, &errno);
-    if (res == MP_STREAM_ERROR) {
+    mp_off_t res = mp_stream_seek(MP_OBJ_FROM_PTR(stream), offset, whence, &errno);
+    if (res == (mp_off_t)-1) {
         return -1;
     }
-    return seek_s.offset;
+    return res;
 }
 
 int mp_stream_posix_fsync(void *stream) {
diff --git a/py/stream.h b/py/stream.h
index 4bc329b3e79..df5e6d0cd45 100644
--- a/py/stream.h
+++ b/py/stream.h
@@ -27,22 +27,27 @@
 #ifndef MICROPY_INCLUDED_PY_STREAM_H
 #define MICROPY_INCLUDED_PY_STREAM_H
 
+#include "py/mpconfig.h"
 #include "py/obj.h"
 #include "py/mperrno.h"
 
 #define MP_STREAM_ERROR ((mp_uint_t)-1)
 
 // Stream ioctl request codes
-#define MP_STREAM_FLUSH (1)
-#define MP_STREAM_SEEK  (2)
-#define MP_STREAM_POLL  (3)
-#define MP_STREAM_CLOSE         (4)
-#define MP_STREAM_TIMEOUT       (5)  // Get/set timeout (single op)
-#define MP_STREAM_GET_OPTS      (6)  // Get stream options
-#define MP_STREAM_SET_OPTS      (7)  // Set stream options
-#define MP_STREAM_GET_DATA_OPTS (8)  // Get data/message options
-#define MP_STREAM_SET_DATA_OPTS (9)  // Set data/message options
-#define MP_STREAM_GET_FILENO    (10) // Get fileno of underlying file
+#define MP_STREAM_FLUSH             (1)
+#define MP_STREAM_SEEK              (2)
+#define MP_STREAM_POLL              (3)
+#define MP_STREAM_CLOSE             (4)
+#define MP_STREAM_TIMEOUT           (5)  // Get/set timeout (single op)
+#define MP_STREAM_GET_OPTS          (6)  // Get stream options
+#define MP_STREAM_SET_OPTS          (7)  // Set stream options
+#define MP_STREAM_GET_DATA_OPTS     (8)  // Get data/message options
+#define MP_STREAM_SET_DATA_OPTS     (9)  // Set data/message options
+#define MP_STREAM_GET_FILENO        (10) // Get fileno of underlying file
+#define MP_STREAM_GET_BUFFER_SIZE   (11) // Get preferred buffer size for file
+#if MICROPY_STREAMS_DELEGATE_ERROR
+#define MP_STREAM_RAISE_ERROR       (12) // Raise an error with detailed error string
+#endif
 
 // These poll ioctl values are compatible with Linux
 #define MP_STREAM_POLL_RD       (0x0001)
@@ -67,8 +72,12 @@ struct mp_stream_seek_t {
 
 // Stream protocol
 typedef struct _mp_stream_p_t {
-    // On error, functions should return MP_STREAM_ERROR and fill in *errcode (values
-    // are implementation-dependent, but will be exposed to user, e.g. via exception).
+    // On error, functions should return MP_STREAM_ERROR and fill in *errcode
+    // (values are are implementation-dependent, but will be exposed to user).
+    // If MICROPY_STREAMS_DELEGATE_ERROR is enabled and ioctl is not null the
+    // stream will receive a MP_STREAM_RAISE_ERROR ioctl request with the arg
+    // containing the errcode, so it may raise a more detailed error. If that
+    // ioctl returns without raising, an OSError with the errcode will raised.
     mp_uint_t (*read)(mp_obj_t obj, void *buf, mp_uint_t size, int *errcode);
     mp_uint_t (*write)(mp_obj_t obj, const void *buf, mp_uint_t size, int *errcode);
     mp_uint_t (*ioctl)(mp_obj_t obj, mp_uint_t request, uintptr_t arg, int *errcode);
@@ -78,11 +87,13 @@ typedef struct _mp_stream_p_t {
 MP_DECLARE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_read_obj);
 MP_DECLARE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_read1_obj);
 MP_DECLARE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_readinto_obj);
+MP_DECLARE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_readinto1_obj);
 MP_DECLARE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_unbuffered_readline_obj);
 MP_DECLARE_CONST_FUN_OBJ_1(mp_stream_unbuffered_readlines_obj);
 MP_DECLARE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_write_obj);
-MP_DECLARE_CONST_FUN_OBJ_2(mp_stream_write1_obj);
+MP_DECLARE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_write1_obj);
 MP_DECLARE_CONST_FUN_OBJ_1(mp_stream_close_obj);
+MP_DECLARE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream___exit___obj);
 MP_DECLARE_CONST_FUN_OBJ_VAR_BETWEEN(mp_stream_seek_obj);
 MP_DECLARE_CONST_FUN_OBJ_1(mp_stream_tell_obj);
 MP_DECLARE_CONST_FUN_OBJ_1(mp_stream_flush_obj);
@@ -113,6 +124,7 @@ mp_obj_t mp_stream_write(mp_obj_t self_in, const void *buf, size_t len, byte fla
 mp_uint_t mp_stream_rw(mp_obj_t stream, void *buf, mp_uint_t size, int *errcode, byte flags);
 #define mp_stream_write_exactly(stream, buf, size, err) mp_stream_rw(stream, (byte *)buf, size, err, MP_STREAM_RW_WRITE)
 #define mp_stream_read_exactly(stream, buf, size, err) mp_stream_rw(stream, buf, size, err, MP_STREAM_RW_READ)
+mp_off_t mp_stream_seek(mp_obj_t stream, mp_off_t offset, int whence, int *errcode);
 
 void mp_stream_write_adaptor(void *self, const char *buf, size_t len);
 
diff --git a/py/unicode.c b/py/unicode.c
index 5acaad37884..4f6a273fe74 100644
--- a/py/unicode.c
+++ b/py/unicode.c
@@ -48,7 +48,7 @@
 #define AT_LX (FL_LOWER | FL_ALPHA | FL_PRINT | FL_XDIGIT)
 
 // table of attributes for ascii characters
-STATIC const uint8_t attr[] = {
+static const uint8_t attr[] = {
     0, 0, 0, 0, 0, 0, 0, 0,
     0, AT_SP, AT_SP, AT_SP, AT_SP, AT_SP, 0, 0,
     0, 0, 0, 0, 0, 0, 0, 0,
@@ -180,7 +180,7 @@ mp_uint_t unichar_xdigit_value(unichar c) {
 
 #if MICROPY_PY_BUILTINS_STR_UNICODE
 
-bool utf8_check(const byte *p, size_t len) {
+bool unicode_encoding_check(mp_encoding_t encoding, const byte *p, size_t len) {
     uint8_t need = 0;
     const byte *end = p + len;
     for (; p < end; p++) {
@@ -193,7 +193,7 @@ bool utf8_check(const byte *p, size_t len) {
                 return 0;
             }
         } else {
-            if (c >= 0xc0) {
+            if (encoding == MP_ENCODING_UTF8 && c >= 0xc0) {
                 if (c >= 0xf8) {
                     // mismatch
                     return 0;
diff --git a/py/unicode.h b/py/unicode.h
index c1fb517894f..7b82c466e51 100644
--- a/py/unicode.h
+++ b/py/unicode.h
@@ -29,7 +29,14 @@
 #include "py/mpconfig.h"
 #include "py/misc.h"
 
+typedef enum {
+    MP_ENCODING_UTF8,
+    MP_ENCODING_ASCII,
+} mp_encoding_t;
+
 mp_uint_t utf8_ptr_to_index(const byte *s, const byte *ptr);
-bool utf8_check(const byte *p, size_t len);
+
+// Returns true if bytes in buffer 'p' are valid according to encoding.
+bool unicode_encoding_check(mp_encoding_t encoding, const byte *p, size_t len);
 
 #endif // MICROPY_INCLUDED_PY_UNICODE_H
diff --git a/py/usermod.cmake b/py/usermod.cmake
index 85327628374..648f315b9d5 100644
--- a/py/usermod.cmake
+++ b/py/usermod.cmake
@@ -5,6 +5,10 @@ function(usermod_gather_sources SOURCES_VARNAME INCLUDE_DIRECTORIES_VARNAME INCL
     if (NOT ${LIB} IN_LIST ${INCLUDED_VARNAME})
         list(APPEND ${INCLUDED_VARNAME} ${LIB})
 
+        if (NOT TARGET ${LIB})
+            return()
+        endif()
+
         # Gather library sources
         get_target_property(lib_sources ${LIB} INTERFACE_SOURCES)
         if (lib_sources)
@@ -35,9 +39,26 @@ function(usermod_gather_sources SOURCES_VARNAME INCLUDE_DIRECTORIES_VARNAME INCL
     endif()
 endfunction()
 
+# Extract c_module() entries from MICROPY_FROZEN_MANIFEST and merge them into
+# USER_C_MODULES, so the loop below picks them up alongside USER_C_MODULES paths
+# passed on the cmake command line. The port is responsible for resolving
+# MICROPY_FROZEN_MANIFEST (including any board-config defaults) before
+# including this file.
+include(${MICROPY_DIR}/py/manifest.cmake)
+
 # Include CMake files for user modules.
 if (USER_C_MODULES)
     foreach(USER_C_MODULE_PATH ${USER_C_MODULES})
+        # If a directory is given, append the micropython.cmake to it.
+        if (IS_DIRECTORY ${USER_C_MODULE_PATH})
+            set(USER_C_MODULE_PATH "${USER_C_MODULE_PATH}/micropython.cmake")
+        endif()
+        # Confirm the provided path exists, show abspath if not to make it clearer to fix.
+        if (NOT EXISTS ${USER_C_MODULE_PATH})
+            get_filename_component(USER_C_MODULES_ABS "${USER_C_MODULE_PATH}" ABSOLUTE)
+            message(FATAL_ERROR "USER_C_MODULES doesn't exist: ${USER_C_MODULES_ABS}")
+        endif()
+
         message("Including User C Module(s) from ${USER_C_MODULE_PATH}")
         include(${USER_C_MODULE_PATH})
     endforeach()
diff --git a/py/verbose.mk b/py/verbose.mk
new file mode 100644
index 00000000000..734623a21e8
--- /dev/null
+++ b/py/verbose.mk
@@ -0,0 +1,16 @@
+# Turn on increased build verbosity by defining BUILD_VERBOSE in your main
+# Makefile or in your environment. You can also use V=1 on the make command
+# line.
+
+ifeq ("$(origin V)", "command line")
+BUILD_VERBOSE=$(V)
+endif
+ifndef BUILD_VERBOSE
+$(info Use make V=1 or set BUILD_VERBOSE in your environment to increase build verbosity.)
+BUILD_VERBOSE = 0
+endif
+ifeq ($(BUILD_VERBOSE),0)
+Q = @
+else
+Q =
+endif
diff --git a/py/vm.c b/py/vm.c
index 385d13ee409..ed0ddd69330 100644
--- a/py/vm.c
+++ b/py/vm.c
@@ -195,6 +195,22 @@
 #define TRACE_TICK(current_ip, current_sp, is_exception)
 #endif // MICROPY_PY_SYS_SETTRACE
 
+#if MICROPY_PY_BUILTINS_SLICE
+// This function is marked "no inline" so it doesn't increase the C stack usage of the main VM function.
+MP_NOINLINE static mp_obj_t *build_slice_stack_allocated(byte op, mp_obj_t *sp, mp_obj_t step) {
+    mp_obj_t stop = sp[2];
+    mp_obj_t start = sp[1];
+    mp_obj_slice_t slice = { .base = { .type = &mp_type_slice }, .start = start, .stop = stop, .step = step };
+    if (op == MP_BC_LOAD_SUBSCR) {
+        SET_TOP(mp_obj_subscr(TOP(), MP_OBJ_FROM_PTR(&slice), MP_OBJ_SENTINEL));
+    } else { // MP_BC_STORE_SUBSCR
+        mp_obj_subscr(TOP(), MP_OBJ_FROM_PTR(&slice), sp[-1]);
+        sp -= 2;
+    }
+    return sp;
+}
+#endif
+
 // fastn has items in reverse order (fastn[0] is local[0], fastn[-1] is local[1], etc)
 // sp points to bottom of stack which grows up
 // returns:
@@ -849,9 +865,20 @@ unwind_jump:;
                         // 3-argument slice includes step
                         step = POP();
                     }
-                    mp_obj_t stop = POP();
-                    mp_obj_t start = TOP();
-                    SET_TOP(mp_obj_new_slice(start, stop, step));
+                    if ((*ip == MP_BC_LOAD_SUBSCR || *ip == MP_BC_STORE_SUBSCR)
+                        && (mp_obj_get_type(sp[-2])->flags & MP_TYPE_FLAG_SUBSCR_ALLOWS_STACK_SLICE)) {
+                        // Fast path optimisation for when the BUILD_SLICE is immediately followed
+                        // by a LOAD/STORE_SUBSCR for an accepting type, to avoid needing to allocate
+                        // the slice on the heap.  In some cases (e.g. a[1:3] = x) this can result
+                        // in no allocations at all.  We can't do this for instance types because
+                        // the get/set/delattr implementation may keep a reference to the slice.
+                        byte op = *ip++;
+                        sp = build_slice_stack_allocated(op, sp - 2, step);
+                    } else {
+                        mp_obj_t stop = POP();
+                        mp_obj_t start = TOP();
+                        SET_TOP(mp_obj_new_slice(start, stop, step));
+                    }
                     DISPATCH();
                 }
                 #endif
@@ -893,7 +920,7 @@ unwind_jump:;
 
                 ENTRY(MP_BC_MAKE_FUNCTION): {
                     DECODE_PTR;
-                    PUSH(mp_make_function_from_raw_code(ptr, code_state->fun_bc->context, NULL));
+                    PUSH(mp_make_function_from_proto_fun(ptr, code_state->fun_bc->context, NULL));
                     DISPATCH();
                 }
 
@@ -901,7 +928,7 @@ unwind_jump:;
                     DECODE_PTR;
                     // Stack layout: def_tuple def_dict <- TOS
                     sp -= 1;
-                    SET_TOP(mp_make_function_from_raw_code(ptr, code_state->fun_bc->context, sp));
+                    SET_TOP(mp_make_function_from_proto_fun(ptr, code_state->fun_bc->context, sp));
                     DISPATCH();
                 }
 
@@ -910,7 +937,7 @@ unwind_jump:;
                     size_t n_closed_over = *ip++;
                     // Stack layout: closed_overs <- TOS
                     sp -= n_closed_over - 1;
-                    SET_TOP(mp_make_closure_from_raw_code(ptr, code_state->fun_bc->context, n_closed_over, sp));
+                    SET_TOP(mp_make_closure_from_proto_fun(ptr, code_state->fun_bc->context, n_closed_over, sp));
                     DISPATCH();
                 }
 
@@ -919,7 +946,7 @@ unwind_jump:;
                     size_t n_closed_over = *ip++;
                     // Stack layout: def_tuple def_dict closed_overs <- TOS
                     sp -= 2 + n_closed_over - 1;
-                    SET_TOP(mp_make_closure_from_raw_code(ptr, code_state->fun_bc->context, 0x100 | n_closed_over, sp));
+                    SET_TOP(mp_make_closure_from_proto_fun(ptr, code_state->fun_bc->context, 0x100 | n_closed_over, sp));
                     DISPATCH();
                 }
 
@@ -1171,8 +1198,10 @@ unwind_jump:;
 
                 ENTRY(MP_BC_RAISE_FROM): {
                     MARK_EXC_IP_SELECTIVE();
-                    mp_warning(NULL, "exception chaining not supported");
-                    sp--; // ignore (pop) "from" argument
+                    mp_obj_t from_value = POP();
+                    if (from_value != mp_const_none) {
+                        mp_warning(NULL, "exception chaining not supported");
+                    }
                     mp_obj_t obj = mp_make_raise_obj(TOP());
                     RAISE(obj);
                 }
@@ -1317,7 +1346,7 @@ unwind_jump:;
                 MICROPY_VM_HOOK_LOOP
 
                 // Check for pending exceptions or scheduled tasks to run.
-                // Note: it's safe to just call mp_handle_pending(true), but
+                // Note: it's safe to just call mp_handle_pending(...), but
                 // we can inline the check for the common case where there is
                 // neither.
                 if (
@@ -1339,7 +1368,7 @@ unwind_jump:;
                 #endif
                 ) {
                     MARK_EXC_IP_SELECTIVE();
-                    mp_handle_pending(true);
+                    mp_handle_pending(MP_HANDLE_PENDING_CALLBACKS_AND_EXCEPTIONS);
                 }
 
                 #if MICROPY_PY_THREAD_GIL
diff --git a/py/vstr.c b/py/vstr.c
index 56327b5f7ac..faf216d657a 100644
--- a/py/vstr.c
+++ b/py/vstr.c
@@ -104,7 +104,7 @@ char *vstr_extend(vstr_t *vstr, size_t size) {
     return p;
 }
 
-STATIC void vstr_ensure_extra(vstr_t *vstr, size_t size) {
+static void vstr_ensure_extra(vstr_t *vstr, size_t size) {
     if (vstr->len + size > vstr->alloc) {
         if (vstr->fixed_buf) {
             // We can't reallocate, and the caller is expecting the space to
@@ -183,33 +183,21 @@ void vstr_add_strn(vstr_t *vstr, const char *str, size_t len) {
     vstr->len += len;
 }
 
-STATIC char *vstr_ins_blank_bytes(vstr_t *vstr, size_t byte_pos, size_t byte_len) {
+char *vstr_ins_blank_bytes(vstr_t *vstr, size_t byte_pos, size_t byte_len) {
     size_t l = vstr->len;
     if (byte_pos > l) {
         byte_pos = l;
     }
-    if (byte_len > 0) {
-        // ensure room for the new bytes
-        vstr_ensure_extra(vstr, byte_len);
-        // copy up the string to make room for the new bytes
-        memmove(vstr->buf + byte_pos + byte_len, vstr->buf + byte_pos, l - byte_pos);
-        // increase the length
-        vstr->len += byte_len;
-    }
+    // ensure room for the new bytes
+    vstr_ensure_extra(vstr, byte_len);
+    // copy up the string to make room for the new bytes
+    memmove(vstr->buf + byte_pos + byte_len, vstr->buf + byte_pos, l - byte_pos);
+    // increase the length
+    vstr->len += byte_len;
+    // return a pointer to the location to insert new bytes at
     return vstr->buf + byte_pos;
 }
 
-void vstr_ins_byte(vstr_t *vstr, size_t byte_pos, byte b) {
-    char *s = vstr_ins_blank_bytes(vstr, byte_pos, 1);
-    *s = b;
-}
-
-void vstr_ins_char(vstr_t *vstr, size_t char_pos, unichar chr) {
-    // TODO UNICODE
-    char *s = vstr_ins_blank_bytes(vstr, char_pos, 1);
-    *s = chr;
-}
-
 void vstr_cut_head_bytes(vstr_t *vstr, size_t bytes_to_cut) {
     vstr_cut_out_bytes(vstr, 0, bytes_to_cut);
 }
diff --git a/pyproject.toml b/pyproject.toml
index a52796860ae..46665df2930 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,7 @@
-[tool.black]
-line-length = 99
-
 [tool.codespell]
 count = ""
 ignore-regex = '\b[A-Z]{3}\b'
-ignore-words-list = "ans,asend,deques,dout,extint,hsi,iput,numer,technic,ure"
+ignore-words-list = "ans,asend,deques,dout,emac,extint,hsi,iput,mis,notin,numer,ser,shft,som,synopsys,technic,ure,curren"
 quiet-level = 3
 skip = """
 */build*,\
@@ -16,16 +13,57 @@ skip = """
 ./ports/cc3200/hal,\
 ./ports/cc3200/simplelink,\
 ./ports/cc3200/telnet,\
+./ports/esp32/managed_components,\
 ./ports/nrf/drivers/bluetooth/s1*,\
-./ports/renesas-ra,\
+./ports/psoc-edge/boards/*/bsp-cfg,\
 ./ports/stm32/usbhost,\
 ./tests,\
 ACKNOWLEDGEMENTS,\
 """
 
 [tool.ruff]
-exclude = ["lib", "tests"]
-extend-select = ["C9", "PLC"]
+# Exclude third-party code from linting and formatting
+extend-exclude = [
+  "lib",
+  "esp-idf",
+  "pico-sdk",
+  "emsdk",
+  "tests/cpydiff/syntax_assign_expr.py" # intentionally incorrect CPython code
+]
+# Include Python source files that don't end with .py
+extend-include = ["tools/cc1"]
+line-length = 99
+target-version = "py38"
+builtins = ["ptr", "ptr8", "ptr16", "ptr32", "uint", "micropython", "const", "execfile"]
+
+[tool.ruff.lint]
+exclude = [  # Ruff finds Python SyntaxError in these files
+  "tests/basics/string_module_tstring.py",
+  "tests/basics/string_tstring_basic.py",
+  "tests/basics/string_tstring_basic1.py",
+  "tests/basics/string_tstring_constructor.py",
+  "tests/basics/string_tstring_errors1.py",
+  "tests/basics/string_tstring_format1.py",
+  "tests/basics/string_tstring_interpolation1.py",
+  "tests/basics/string_tstring_operations.py",
+  "tests/basics/string_tstring_parser1.py",
+  "tests/cmdline/cmd_compile_only_error.py",
+  "tests/cmdline/repl_autocomplete.py",
+  "tests/cmdline/repl_autocomplete_underscore.py",
+  "tests/cmdline/repl_autoindent.py",
+  "tests/cmdline/repl_basic.py",
+  "tests/cmdline/repl_cont.py",
+  "tests/cmdline/repl_ctrl_c_interrupt_execution.py",
+  "tests/cmdline/repl_emacs_keys.py",
+  "tests/cmdline/repl_paste.py",
+  "tests/cmdline/repl_words_move.py",
+  "tests/feature_check/repl_emacs_check.py",
+  "tests/feature_check/repl_words_move_check.py",
+  "tests/feature_check/tstring.py",
+  "tests/micropython/heapalloc_fail_tstring.py",
+  "tests/micropython/viper_args.py",
+]
+extend-select = ["C9", "PLC", "W292"]
 ignore = [
   "E401",
   "E402",
@@ -35,14 +73,33 @@ ignore = [
   "F401",
   "F403",
   "F405",
-  "F821",
-  "PLC1901",
+  "PLC0206",
+  "PLC0415", # conditional imports are common in MicroPython
 ]
-line-length = 337
-target-version = "py37"
-
-[tool.ruff.mccabe]
-max-complexity = 40
+mccabe.max-complexity = 40
 
-[tool.ruff.per-file-ignores]
+[tool.ruff.lint.per-file-ignores]
+# Exclude all tests from linting.
+"tests/**/*.py" = ["ALL"]
 "ports/cc3200/tools/uniflash.py" = ["E711"]
+# manifest.py files are evaluated with some global names pre-defined
+"**/manifest.py" = ["F821"]
+"ports/**/boards/**/manifest_*.py" = ["F821"]
+# Uses assignment expressions.
+"tests/cpydiff/syntax_assign_expr.py" = ["F821"]
+
+[tool.ruff.format]
+# Exclude third-party code, and exclude the following tests:
+#   basics: needs careful attention before applying automatic formatting
+#   repl_: not real python files
+#   tstring: ruff does not support template strings
+#   viper_args: uses f(*)
+exclude = [
+  "tests/basics/*.py",
+  "tests/*/repl_*.py",
+  "tests/cmdline/cmd_compile_only_error.py",
+  "tests/feature_check/tstring.py",
+  "tests/micropython/heapalloc_fail_tstring.py",
+  "tests/micropython/test_normalize_newlines.py",
+  "tests/micropython/viper_args.py",
+]
diff --git a/shared/libc/abort_.c b/shared/libc/abort_.c
index 3051eae81e0..54eab67d3fb 100644
--- a/shared/libc/abort_.c
+++ b/shared/libc/abort_.c
@@ -1,7 +1,7 @@
 #include 
 
-NORETURN void abort_(void);
+MP_NORETURN void abort_(void);
 
-NORETURN void abort_(void) {
+MP_NORETURN void abort_(void) {
     mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("abort() called"));
 }
diff --git a/shared/libc/printf.c b/shared/libc/printf.c
index 715181229e4..50b04ce9be0 100644
--- a/shared/libc/printf.c
+++ b/shared/libc/printf.c
@@ -87,7 +87,7 @@ typedef struct _strn_print_env_t {
     size_t remain;
 } strn_print_env_t;
 
-STATIC void strn_print_strn(void *data, const char *str, size_t len) {
+static void strn_print_strn(void *data, const char *str, size_t len) {
     strn_print_env_t *strn_print_env = data;
     if (len > strn_print_env->remain) {
         len = strn_print_env->remain;
diff --git a/shared/libc/string0.c b/shared/libc/string0.c
index a3b268e4416..de39768ca2b 100644
--- a/shared/libc/string0.c
+++ b/shared/libc/string0.c
@@ -42,14 +42,14 @@ void *memcpy(void *dst, const void *src, size_t n) {
 
         if (n & 2) {
             // copy half-word
-            *(uint16_t*)d = *(const uint16_t*)s;
-            d = (uint32_t*)((uint16_t*)d + 1);
-            s = (const uint32_t*)((const uint16_t*)s + 1);
+            *(uint16_t *)d = *(const uint16_t *)s;
+            d = (uint32_t *)((uint16_t *)d + 1);
+            s = (const uint32_t *)((const uint16_t *)s + 1);
         }
 
         if (n & 1) {
             // copy byte
-            *((uint8_t*)d) = *((const uint8_t*)s);
+            *((uint8_t *)d) = *((const uint8_t *)s);
         }
     } else {
         // unaligned access, copy bytes
@@ -72,10 +72,10 @@ void *__memcpy_chk(void *dest, const void *src, size_t len, size_t slen) {
 }
 
 void *memmove(void *dest, const void *src, size_t n) {
-    if (src < dest && (uint8_t*)dest < (const uint8_t*)src + n) {
+    if (src < dest && (uint8_t *)dest < (const uint8_t *)src + n) {
         // need to copy backwards
-        uint8_t *d = (uint8_t*)dest + n - 1;
-        const uint8_t *s = (const uint8_t*)src + n - 1;
+        uint8_t *d = (uint8_t *)dest + n - 1;
+        const uint8_t *s = (const uint8_t *)src + n - 1;
         for (; n > 0; n--) {
             *d-- = *s--;
         }
@@ -94,11 +94,11 @@ void *memset(void *s, int c, size_t n) {
             *s32++ = 0;
         }
         if (n & 2) {
-            *((uint16_t*)s32) = 0;
-            s32 = (uint32_t*)((uint16_t*)s32 + 1);
+            *((uint16_t *)s32) = 0;
+            s32 = (uint32_t *)((uint16_t *)s32 + 1);
         }
         if (n & 1) {
-            *((uint8_t*)s32) = 0;
+            *((uint8_t *)s32) = 0;
         }
     } else {
         uint8_t *s2 = s;
@@ -115,8 +115,11 @@ int memcmp(const void *s1, const void *s2, size_t n) {
     while (n--) {
         char c1 = *s1_8++;
         char c2 = *s2_8++;
-        if (c1 < c2) return -1;
-        else if (c1 > c2) return 1;
+        if (c1 < c2) {
+            return -1;
+        } else if (c1 > c2) {
+            return 1;
+        }
     }
     return 0;
 }
@@ -126,8 +129,9 @@ void *memchr(const void *s, int c, size_t n) {
         const unsigned char *p = s;
 
         do {
-            if (*p++ == c)
-                return ((void *)(p - 1));
+            if (*p++ == c) {
+                return (void *)(p - 1);
+            }
         } while (--n != 0);
     }
     return 0;
@@ -145,26 +149,41 @@ int strcmp(const char *s1, const char *s2) {
     while (*s1 && *s2) {
         char c1 = *s1++; // XXX UTF8 get char, next char
         char c2 = *s2++; // XXX UTF8 get char, next char
-        if (c1 < c2) return -1;
-        else if (c1 > c2) return 1;
+        if (c1 < c2) {
+            return -1;
+        } else if (c1 > c2) {
+            return 1;
+        }
+    }
+    if (*s2) {
+        return -1;
+    } else if (*s1) {
+        return 1;
+    } else {
+        return 0;
     }
-    if (*s2) return -1;
-    else if (*s1) return 1;
-    else return 0;
 }
 
 int strncmp(const char *s1, const char *s2, size_t n) {
-    while (*s1 && *s2 && n > 0) {
+    while (n > 0 && *s1 && *s2) {
         char c1 = *s1++; // XXX UTF8 get char, next char
         char c2 = *s2++; // XXX UTF8 get char, next char
         n--;
-        if (c1 < c2) return -1;
-        else if (c1 > c2) return 1;
+        if (c1 < c2) {
+            return -1;
+        } else if (c1 > c2) {
+            return 1;
+        }
+    }
+    if (n == 0) {
+        return 0;
+    } else if (*s2) {
+        return -1;
+    } else if (*s1) {
+        return 1;
+    } else {
+        return 0;
     }
-    if (n == 0) return 0;
-    else if (*s2) return -1;
-    else if (*s1) return 1;
-    else return 0;
 }
 
 char *strcpy(char *dest, const char *src) {
@@ -179,21 +198,21 @@ char *strcpy(char *dest, const char *src) {
 // Public Domain implementation of strncpy from:
 // http://en.wikibooks.org/wiki/C_Programming/Strings#The_strncpy_function
 char *strncpy(char *s1, const char *s2, size_t n) {
-     char *dst = s1;
-     const char *src = s2;
-     /* Copy bytes, one at a time.  */
-     while (n > 0) {
-         n--;
-         if ((*dst++ = *src++) == '\0') {
-             /* If we get here, we found a null character at the end
-                of s2, so use memset to put null bytes at the end of
-                s1.  */
-             memset(dst, '\0', n);
-             break;
-         }
-     }
-     return s1;
- }
+    char *dst = s1;
+    const char *src = s2;
+    /* Copy bytes, one at a time.  */
+    while (n > 0) {
+        n--;
+        if ((*dst++ = *src++) == '\0') {
+            /* If we get here, we found a null character at the end
+               of s2, so use memset to put null bytes at the end of
+               s1.  */
+            memset(dst, '\0', n);
+            break;
+        }
+    }
+    return s1;
+}
 
 // needed because gcc optimises strcpy + strcat to this
 char *stpcpy(char *dest, const char *src) {
@@ -218,29 +237,31 @@ char *strcat(char *dest, const char *src) {
 
 // Public Domain implementation of strchr from:
 // http://en.wikibooks.org/wiki/C_Programming/Strings#The_strchr_function
-char *strchr(const char *s, int c)
-{
+char *strchr(const char *s, int c) {
     /* Scan s for the character.  When this loop is finished,
        s will either point to the end of the string or the
        character we were looking for.  */
-    while (*s != '\0' && *s != (char)c)
+    while (*s != '\0' && *s != (char)c) {
         s++;
-    return ((*s == c) ? (char *) s : 0);
+    }
+    return (*s == c) ? (char *)s : 0;
 }
 
 
 // Public Domain implementation of strstr from:
 // http://en.wikibooks.org/wiki/C_Programming/Strings#The_strstr_function
-char *strstr(const char *haystack, const char *needle)
-{
+char *strstr(const char *haystack, const char *needle) {
     size_t needlelen;
     /* Check for the null needle case.  */
-    if (*needle == '\0')
-        return (char *) haystack;
+    if (*needle == '\0') {
+        return (char *)haystack;
+    }
     needlelen = strlen(needle);
-    for (; (haystack = strchr(haystack, *needle)) != 0; haystack++)
-        if (strncmp(haystack, needle, needlelen) == 0)
-            return (char *) haystack;
+    for (; (haystack = strchr(haystack, *needle)) != 0; haystack++) {
+        if (strncmp(haystack, needle, needlelen) == 0) {
+            return (char *)haystack;
+        }
+    }
     return 0;
 }
 
@@ -259,3 +280,13 @@ size_t strcspn(const char *s, const char *reject) {
     }
     return s - ss;
 }
+
+// Decimal-only, non-negative integers; no leading whitespace handling.
+// Marked weak so a libc-provided atoi() takes precedence if available.
+__attribute__((weak)) int atoi(const char *num) {
+    int value = 0;
+    while (*num >= '0' && *num <= '9') {
+        value = value * 10 + (*num++ - '0');
+    }
+    return value;
+}
diff --git a/shared/memzip/README.md b/shared/memzip/README.md
index 3938e3198c9..6ffe1d2c6fb 100644
--- a/shared/memzip/README.md
+++ b/shared/memzip/README.md
@@ -18,9 +18,6 @@ OBJ += $(BUILD)/memzip-files.o
 
 MAKE_MEMZIP = ../shared/memzip/make-memzip.py
 
-$(BUILD)/memzip-files.o: $(BUILD)/memzip-files.c
-    $(call compile_c)
-
 $(BUILD)/memzip-files.c: $(shell find ${MEMZIP_DIR} -type f)
     @$(ECHO) "Creating $@"
     $(Q)$(PYTHON) $(MAKE_MEMZIP) --zip-file $(BUILD)/memzip-files.zip --c-file $@ $(MEMZIP_DIR)
diff --git a/shared/memzip/lexermemzip.c b/shared/memzip/lexermemzip.c
index 6b26961bdc8..a31fc7f1cdc 100644
--- a/shared/memzip/lexermemzip.c
+++ b/shared/memzip/lexermemzip.c
@@ -5,15 +5,13 @@
 #include "py/mperrno.h"
 #include "memzip.h"
 
-mp_lexer_t *mp_lexer_new_from_file(const char *filename)
-{
+mp_lexer_t *mp_lexer_new_from_file(qstr filename) {
     void *data;
     size_t len;
 
-    if (memzip_locate(filename, &data, &len) != MZ_OK) {
+    if (memzip_locate(qstr_str(filename), &data, &len) != MZ_OK) {
         mp_raise_OSError(MP_ENOENT);
     }
 
-    return mp_lexer_new_from_str_len(qstr_from_str(filename), (const char *)data, (mp_uint_t)len, 0);
+    return mp_lexer_new_from_str_len(filename, (const char *)data, (mp_uint_t)len, 0);
 }
-
diff --git a/shared/memzip/make-memzip.py b/shared/memzip/make-memzip.py
index cc0df820774..c060745e6cf 100755
--- a/shared/memzip/make-memzip.py
+++ b/shared/memzip/make-memzip.py
@@ -7,13 +7,13 @@
 # This is somewhat like frozen modules in python, but allows arbitrary files
 # to be used.
 
-from __future__ import print_function
-
 import argparse
 import os
+import pathlib
+import shutil
 import subprocess
 import sys
-import types
+
 
 def create_zip(zip_filename, zip_dir):
     abs_zip_filename = os.path.abspath(zip_filename)
@@ -21,59 +21,67 @@ def create_zip(zip_filename, zip_dir):
     os.chdir(zip_dir)
     if os.path.exists(abs_zip_filename):
         os.remove(abs_zip_filename)
-    subprocess.check_call(['zip', '-0', '-r', '-D', abs_zip_filename, '.'])
+    subprocess.check_call(["zip", "-0", "-r", "-D", abs_zip_filename, "."])
     os.chdir(save_cwd)
 
+
 def create_c_from_file(c_filename, zip_filename):
-    with open(zip_filename, 'rb') as zip_file:
-        with open(c_filename, 'wb') as c_file:
-            print('#include ', file=c_file)
-            print('', file=c_file)
-            print('const uint8_t memzip_data[] = {', file=c_file)
+    with open(zip_filename, "rb") as zip_file:
+        with open(c_filename, "wt") as c_file:
+            print("#include ", file=c_file)
+            print("", file=c_file)
+            print("const uint8_t memzip_data[] = {", file=c_file)
             while True:
                 buf = zip_file.read(16)
                 if not buf:
                     break
-                print('   ', end='', file=c_file)
+                print("   ", end="", file=c_file)
                 for byte in buf:
-                    if isinstance(byte, types.StringType):
-                        print(' 0x{:02x},'.format(ord(byte)), end='', file=c_file)
-                    else:
-                        print(' 0x{:02x},'.format(byte), end='', file=c_file)
-                print('', file=c_file)
-            print('};', file=c_file)
+                    print(" 0x{:02x},".format(byte), end="", file=c_file)
+                print("", file=c_file)
+            print("};", file=c_file)
+
 
 def main():
     parser = argparse.ArgumentParser(
-        prog='make-memzip.py',
-        usage='%(prog)s [options] [command]',
-        description='Generates a C source memzip file.'
-    )
-    parser.add_argument(
-        '-z', '--zip-file',
-        dest='zip_filename',
-        help='Specifies the name of the created zip file.',
-        default='memzip_files.zip'
+        prog="make-memzip.py",
+        usage="%(prog)s [options] [command]",
+        description="Generates a C source memzip file.",
     )
     parser.add_argument(
-        '-c', '--c-file',
-        dest='c_filename',
-        help='Specifies the name of the created C source file.',
-        default='memzip_files.c'
+        "-z",
+        "--zip-file",
+        dest="zip_filename",
+        help="Specifies the name of the created zip file.",
+        default="memzip_files.zip",
     )
     parser.add_argument(
-        dest='source_dir',
-        default='memzip_files'
+        "-c",
+        "--c-file",
+        dest="c_filename",
+        help="Specifies the name of the created C source file.",
+        default="memzip_files.c",
     )
+    parser.add_argument(dest="source_dir", default="memzip_files")
     args = parser.parse_args(sys.argv[1:])
 
-    print('args.zip_filename =', args.zip_filename)
-    print('args.c_filename =', args.c_filename)
-    print('args.source_dir =', args.source_dir)
+    output_zip = pathlib.Path(args.zip_filename)
+    if output_zip.suffix != ".zip":
+        args.zip_filename = output_zip.with_suffix(".zip")
+    output_c = pathlib.Path(args.c_filename)
+    if output_c.suffix != ".c":
+        args.c_filename = output_c.with_suffix(".c")
+
+    print("args.zip_filename =", args.zip_filename)
+    print("args.c_filename =", args.c_filename)
+    print("args.source_dir =", args.source_dir)
+
+    if not shutil.which("zip"):
+        raise FileNotFoundError("zip archiver not available")
 
     create_zip(args.zip_filename, args.source_dir)
     create_c_from_file(args.c_filename, args.zip_filename)
 
+
 if __name__ == "__main__":
     main()
-
diff --git a/shared/memzip/memzip.c b/shared/memzip/memzip.c
index 3fbea8e1e91..32fd27ffeb4 100644
--- a/shared/memzip/memzip.c
+++ b/shared/memzip/memzip.c
@@ -65,8 +65,7 @@ bool memzip_is_dir(const char *filename) {
 
 }
 
-MEMZIP_RESULT memzip_locate(const char *filename, void **data, size_t *len)
-{
+MEMZIP_RESULT memzip_locate(const char *filename, void **data, size_t *len) {
     const MEMZIP_FILE_HDR *file_hdr = memzip_find_file_header(filename);
     if (file_hdr == NULL) {
         return MZ_NO_FILE;
diff --git a/shared/memzip/memzip.h b/shared/memzip/memzip.h
index 667e2df7e13..fe27d9584a3 100644
--- a/shared/memzip/memzip.h
+++ b/shared/memzip/memzip.h
@@ -3,17 +3,17 @@
 #define MEMZIP_FILE_HEADER_SIGNATURE 0x04034b50
 typedef struct
 {
-    uint32_t    signature;
-    uint16_t    version;
-    uint16_t    flags;
-    uint16_t    compression_method;
-    uint16_t    last_mod_time;
-    uint16_t    last_mod_date;
-    uint32_t    crc32;
-    uint32_t    compressed_size;
-    uint32_t    uncompressed_size;
-    uint16_t    filename_len;
-    uint16_t    extra_len;
+    uint32_t signature;
+    uint16_t version;
+    uint16_t flags;
+    uint16_t compression_method;
+    uint16_t last_mod_time;
+    uint16_t last_mod_date;
+    uint32_t crc32;
+    uint32_t compressed_size;
+    uint32_t uncompressed_size;
+    uint16_t filename_len;
+    uint16_t extra_len;
 
     /* char     filename[filename_len]  */
     /* uint8_t  extra[extra_len]        */
@@ -23,22 +23,22 @@ typedef struct
 #define MEMZIP_CENTRAL_DIRECTORY_SIGNATURE 0x02014b50
 typedef struct
 {
-    uint32_t    signature;
-    uint16_t    version_made_by;
-    uint16_t    version_read_with;
-    uint16_t    flags;
-    uint16_t    compression_method;
-    uint16_t    last_mod_time;
-    uint16_t    last_mod_date;
-    uint32_t    crc32;
-    uint32_t    compressed_size;
-    uint32_t    uncompressed_size;
-    uint16_t    filename_len;
-    uint16_t    extra_len;
-    uint16_t    disk_num;
-    uint16_t    internal_file_attributes;
-    uint32_t    external_file_attributes;
-    uint32_t    file_header_offset;
+    uint32_t signature;
+    uint16_t version_made_by;
+    uint16_t version_read_with;
+    uint16_t flags;
+    uint16_t compression_method;
+    uint16_t last_mod_time;
+    uint16_t last_mod_date;
+    uint32_t crc32;
+    uint32_t compressed_size;
+    uint32_t uncompressed_size;
+    uint16_t filename_len;
+    uint16_t extra_len;
+    uint16_t disk_num;
+    uint16_t internal_file_attributes;
+    uint32_t external_file_attributes;
+    uint32_t file_header_offset;
 
     /* char     filename[filename_len]  */
     /* uint8_t  extra[extra_len]        */
@@ -48,14 +48,14 @@ typedef struct
 #define MEMZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE 0x06054b50
 typedef struct
 {
-    uint32_t    signature;
-    uint16_t    disk_num;
-    uint16_t    central_directory_disk;
-    uint16_t    num_central_directories_this_disk;
-    uint16_t    total_central_directories;
-    uint32_t    central_directory_size;
-    uint32_t    central_directory_offset;
-    uint16_t    comment_len;
+    uint32_t signature;
+    uint16_t disk_num;
+    uint16_t central_directory_disk;
+    uint16_t num_central_directories_this_disk;
+    uint16_t total_central_directories;
+    uint32_t central_directory_size;
+    uint32_t central_directory_offset;
+    uint16_t comment_len;
 
     /* char     comment[comment_len]    */
 
@@ -71,10 +71,10 @@ typedef enum {
 } MEMZIP_RESULT;
 
 typedef struct {
-    uint32_t    file_size;
-    uint16_t    last_mod_date;
-    uint16_t    last_mod_time;
-    uint8_t     is_dir;
+    uint32_t file_size;
+    uint16_t last_mod_date;
+    uint16_t last_mod_time;
+    uint8_t is_dir;
 
 } MEMZIP_FILE_INFO;
 
diff --git a/shared/netutils/dhcpserver.c b/shared/netutils/dhcpserver.c
index 50cd69a1eb5..c54dad357a7 100644
--- a/shared/netutils/dhcpserver.c
+++ b/shared/netutils/dhcpserver.c
@@ -65,11 +65,9 @@
 #define PORT_DHCP_SERVER (67)
 #define PORT_DHCP_CLIENT (68)
 
-#define DEFAULT_DNS MAKE_IP4(192, 168, 4, 1)
 #define DEFAULT_LEASE_TIME_S (24 * 60 * 60) // in seconds
 
 #define MAC_LEN (6)
-#define MAKE_IP4(a, b, c, d) ((a) << 24 | (b) << 16 | (c) << 8 | (d))
 
 typedef struct {
     uint8_t op; // message opcode
@@ -113,12 +111,12 @@ static void dhcp_socket_free(struct udp_pcb **udp) {
 
 static int dhcp_socket_bind(struct udp_pcb **udp, uint32_t ip, uint16_t port) {
     ip_addr_t addr;
-    IP4_ADDR(&addr, ip >> 24 & 0xff, ip >> 16 & 0xff, ip >> 8 & 0xff, ip & 0xff);
+    IP_ADDR4(&addr, ip >> 24 & 0xff, ip >> 16 & 0xff, ip >> 8 & 0xff, ip & 0xff);
     // TODO convert lwIP errors to errno
     return udp_bind(*udp, &addr, port);
 }
 
-static int dhcp_socket_sendto(struct udp_pcb **udp, const void *buf, size_t len, uint32_t ip, uint16_t port) {
+static int dhcp_socket_sendto(struct udp_pcb **udp, struct netif *netif, const void *buf, size_t len, uint32_t ip, uint16_t port) {
     if (len > 0xffff) {
         len = 0xffff;
     }
@@ -131,8 +129,13 @@ static int dhcp_socket_sendto(struct udp_pcb **udp, const void *buf, size_t len,
     memcpy(p->payload, buf, len);
 
     ip_addr_t dest;
-    IP4_ADDR(&dest, ip >> 24 & 0xff, ip >> 16 & 0xff, ip >> 8 & 0xff, ip & 0xff);
-    err_t err = udp_sendto(*udp, p, &dest, port);
+    IP_ADDR4(&dest, ip >> 24 & 0xff, ip >> 16 & 0xff, ip >> 8 & 0xff, ip & 0xff);
+    err_t err;
+    if (netif != NULL) {
+        err = udp_sendto_if(*udp, p, &dest, port, netif);
+    } else {
+        err = udp_sendto(*udp, p, &dest, port);
+    }
 
     pbuf_free(p);
 
@@ -153,7 +156,7 @@ static uint8_t *opt_find(uint8_t *opt, uint8_t cmd) {
     return NULL;
 }
 
-static void opt_write_n(uint8_t **opt, uint8_t cmd, size_t n, void *data) {
+static void opt_write_n(uint8_t **opt, uint8_t cmd, size_t n, const void *data) {
     uint8_t *o = *opt;
     *o++ = cmd;
     *o++ = n;
@@ -200,7 +203,7 @@ static void dhcp_server_process(void *arg, struct udp_pcb *upcb, struct pbuf *p,
     }
 
     dhcp_msg.op = DHCPOFFER;
-    memcpy(&dhcp_msg.yiaddr, &d->ip.addr, 4);
+    memcpy(&dhcp_msg.yiaddr, &ip_2_ip4(&d->ip)->addr, 4);
 
     uint8_t *opt = (uint8_t *)&dhcp_msg.options;
     opt += 4; // assume magic cookie: 99, 130, 83, 99
@@ -243,7 +246,7 @@ static void dhcp_server_process(void *arg, struct udp_pcb *upcb, struct pbuf *p,
                 // Should be NACK
                 goto ignore_request;
             }
-            if (memcmp(o + 2, &d->ip.addr, 3) != 0) {
+            if (memcmp(o + 2, &ip_2_ip4(&d->ip)->addr, 3) != 0) {
                 // Should be NACK
                 goto ignore_request;
             }
@@ -275,13 +278,16 @@ static void dhcp_server_process(void *arg, struct udp_pcb *upcb, struct pbuf *p,
             goto ignore_request;
     }
 
-    opt_write_n(&opt, DHCP_OPT_SERVER_ID, 4, &d->ip.addr);
-    opt_write_n(&opt, DHCP_OPT_SUBNET_MASK, 4, &d->nm.addr);
-    opt_write_n(&opt, DHCP_OPT_ROUTER, 4, &d->ip.addr); // aka gateway; can have multiple addresses
-    opt_write_u32(&opt, DHCP_OPT_DNS, DEFAULT_DNS); // can have multiple addresses
+    opt_write_n(&opt, DHCP_OPT_SERVER_ID, 4, &ip_2_ip4(&d->ip)->addr);
+    opt_write_n(&opt, DHCP_OPT_SUBNET_MASK, 4, &ip_2_ip4(&d->nm)->addr);
+    if (d->send_router) {
+        opt_write_n(&opt, DHCP_OPT_ROUTER, 4, &ip_2_ip4(&d->ip)->addr); // aka gateway; can have multiple addresses
+    }
+    opt_write_n(&opt, DHCP_OPT_DNS, 4, &ip_2_ip4(&d->ip)->addr);
     opt_write_u32(&opt, DHCP_OPT_IP_LEASE_TIME, DEFAULT_LEASE_TIME_S);
     *opt++ = DHCP_OPT_END;
-    dhcp_socket_sendto(&d->udp, &dhcp_msg, opt - (uint8_t *)&dhcp_msg, 0xffffffff, PORT_DHCP_CLIENT);
+    struct netif *netif = ip_current_input_netif();
+    dhcp_socket_sendto(&d->udp, netif, &dhcp_msg, opt - (uint8_t *)&dhcp_msg, 0xffffffff, PORT_DHCP_CLIENT);
 
 ignore_request:
     pbuf_free(p);
@@ -291,6 +297,7 @@ void dhcp_server_init(dhcp_server_t *d, ip_addr_t *ip, ip_addr_t *nm) {
     ip_addr_copy(d->ip, *ip);
     ip_addr_copy(d->nm, *nm);
     memset(d->lease, 0, sizeof(d->lease));
+    d->send_router = true;
     if (dhcp_socket_new_dgram(&d->udp, d, dhcp_server_process) != 0) {
         return;
     }
diff --git a/shared/netutils/dhcpserver.h b/shared/netutils/dhcpserver.h
index 2349d2ea427..24224d6aadd 100644
--- a/shared/netutils/dhcpserver.h
+++ b/shared/netutils/dhcpserver.h
@@ -41,6 +41,7 @@ typedef struct _dhcp_server_t {
     ip_addr_t nm;
     dhcp_server_lease_t lease[DHCPS_MAX_IP];
     struct udp_pcb *udp;
+    bool send_router; // advertise server IP as default gateway
 } dhcp_server_t;
 
 void dhcp_server_init(dhcp_server_t *d, ip_addr_t *ip, ip_addr_t *nm);
diff --git a/shared/netutils/netutils.c b/shared/netutils/netutils.c
index 84b4405c41d..cd1422f7c80 100644
--- a/shared/netutils/netutils.c
+++ b/shared/netutils/netutils.c
@@ -63,7 +63,13 @@ void netutils_parse_ipv4_addr(mp_obj_t addr_in, uint8_t *out_ip, netutils_endian
         return;
     }
     const char *s = addr_str;
-    const char *s_top = addr_str + addr_len;
+    const char *s_top;
+    // Scan for the end of valid address characters
+    for (s_top = addr_str; s_top < addr_str + addr_len; s_top++) {
+        if (!(*s_top == '.' || (*s_top >= '0' && *s_top <= '9'))) {
+            break;
+        }
+    }
     for (mp_uint_t i = 3; ; i--) {
         mp_uint_t val = 0;
         for (; s < s_top && *s != '.'; s++) {
diff --git a/shared/netutils/trace.c b/shared/netutils/trace.c
index a6dfb42c28f..24af4d5ca31 100644
--- a/shared/netutils/trace.c
+++ b/shared/netutils/trace.c
@@ -56,7 +56,7 @@ static const char *ethertype_str(uint16_t type) {
 }
 
 void netutils_ethernet_trace(const mp_print_t *print, size_t len, const uint8_t *buf, unsigned int flags) {
-    mp_printf(print, "[% 8d] ETH%cX len=%u", mp_hal_ticks_ms(), flags & NETUTILS_TRACE_IS_TX ? 'T' : 'R', len);
+    mp_printf(print, "[% 8u] ETH%cX len=%u", (unsigned)mp_hal_ticks_ms(), flags & NETUTILS_TRACE_IS_TX ? 'T' : 'R', len);
     mp_printf(print, " dst=%02x:%02x:%02x:%02x:%02x:%02x", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]);
     mp_printf(print, " src=%02x:%02x:%02x:%02x:%02x:%02x", buf[6], buf[7], buf[8], buf[9], buf[10], buf[11]);
 
diff --git a/shared/readline/readline.c b/shared/readline/readline.c
index b85bbd5bea8..f44f6cca444 100644
--- a/shared/readline/readline.c
+++ b/shared/readline/readline.c
@@ -55,7 +55,7 @@ void readline_init0(void) {
     memset(MP_STATE_PORT(readline_hist), 0, MICROPY_READLINE_HISTORY_SIZE * sizeof(const char*));
 }
 
-STATIC char *str_dup_maybe(const char *str) {
+static char *str_dup_maybe(const char *str) {
     uint32_t len = strlen(str);
     char *s2 = m_new_maybe(char, len + 1);
     if (s2 == NULL) {
@@ -72,7 +72,7 @@ STATIC char *str_dup_maybe(const char *str) {
 
 // ...and provide the implementation using them
 #if MICROPY_HAL_HAS_VT100
-STATIC void mp_hal_move_cursor_back(uint pos) {
+static void mp_hal_move_cursor_back(uint pos) {
     if (pos <= 4) {
         // fast path for most common case of 1 step back
         mp_hal_stdout_tx_strn("\b\b\b\b", pos);
@@ -88,7 +88,7 @@ STATIC void mp_hal_move_cursor_back(uint pos) {
     }
 }
 
-STATIC void mp_hal_erase_line_from_cursor(uint n_chars_to_erase) {
+static void mp_hal_erase_line_from_cursor(uint n_chars_to_erase) {
     (void)n_chars_to_erase;
     mp_hal_stdout_tx_strn("\x1b[K", 3);
 }
@@ -101,16 +101,17 @@ typedef struct _readline_t {
     int hist_cur;
     size_t cursor_pos;
     char escape_seq_buf[1];
+    char last_nl;
     #if MICROPY_REPL_AUTO_INDENT
     uint8_t auto_indent_state;
     #endif
     const char *prompt;
 } readline_t;
 
-STATIC readline_t rl;
+static readline_t rl;
 
 #if MICROPY_REPL_EMACS_WORDS_MOVE
-STATIC size_t cursor_count_word(int forward) {
+static size_t cursor_count_word(int forward) {
     const char *line_buf = vstr_str(rl.line);
     size_t pos = rl.cursor_pos;
     bool in_word = false;
@@ -138,6 +139,17 @@ STATIC size_t cursor_count_word(int forward) {
 }
 #endif
 
+// Function returns true if newline character shall be processed.
+static bool process_nl(int c) {
+    if ((c == '\r' || c == '\n') && (rl.last_nl == 0 || rl.last_nl == c)) {
+        rl.last_nl = c;
+        return true;
+    }
+
+    rl.last_nl = 0;
+    return false;
+}
+
 int readline_process_char(int c) {
     size_t last_line_len = rl.line->len;
     int redraw_step_back = 0;
@@ -192,7 +204,7 @@ int readline_process_char(int c) {
         } else if (c == CHAR_CTRL_W) {
             goto backward_kill_word;
         #endif
-        } else if (c == '\r') {
+        } else if (process_nl(c)) {
             // newline
             mp_hal_stdout_tx_str("\r\n");
             readline_push_history(vstr_null_terminated_str(rl.line) + rl.orig_line_len);
@@ -481,7 +493,7 @@ int readline_process_char(int c) {
 }
 
 #if MICROPY_REPL_AUTO_INDENT
-STATIC void readline_auto_indent(void) {
+static void readline_auto_indent(void) {
     if (!(rl.auto_indent_state & AUTO_INDENT_ENABLED)) {
         return;
     }
diff --git a/shared/runtime/gchelper.h b/shared/runtime/gchelper.h
index 645ee837f51..9617bc7f9fe 100644
--- a/shared/runtime/gchelper.h
+++ b/shared/runtime/gchelper.h
@@ -41,6 +41,12 @@ typedef uintptr_t gc_helper_regs_t[4];
 typedef uintptr_t gc_helper_regs_t[10];
 #elif defined(__aarch64__)
 typedef uintptr_t gc_helper_regs_t[11]; // x19-x29
+#elif defined(__riscv) && (__riscv_xlen <= 64)
+typedef uintptr_t gc_helper_regs_t[12]; // S0-S11
+#elif defined(__loongarch__) && defined(__loongarch64)
+typedef uintptr_t gc_helper_regs_t[10]; // S0-S9
+#elif defined(__powerpc__) && defined(__powerpc64__)
+typedef uintptr_t gc_helper_regs_t[18]; // r14-r31
 #endif
 
 #endif
diff --git a/shared/runtime/gchelper_generic.c b/shared/runtime/gchelper_generic.c
index 272e37056aa..e7d46233fb3 100644
--- a/shared/runtime/gchelper_generic.c
+++ b/shared/runtime/gchelper_generic.c
@@ -42,7 +42,7 @@
 // stack already by the caller.
 #if defined(__x86_64__)
 
-STATIC void gc_helper_get_regs(gc_helper_regs_t arr) {
+static void gc_helper_get_regs(gc_helper_regs_t arr) {
     register long rbx asm ("rbx");
     register long rbp asm ("rbp");
     register long r12 asm ("r12");
@@ -73,7 +73,7 @@ STATIC void gc_helper_get_regs(gc_helper_regs_t arr) {
 
 #elif defined(__i386__)
 
-STATIC void gc_helper_get_regs(gc_helper_regs_t arr) {
+static void gc_helper_get_regs(gc_helper_regs_t arr) {
     register long ebx asm ("ebx");
     register long esi asm ("esi");
     register long edi asm ("edi");
@@ -100,7 +100,11 @@ STATIC void gc_helper_get_regs(gc_helper_regs_t arr) {
 
 // Fallback implementation, prefer gchelper_thumb1.s or gchelper_thumb2.s
 
-STATIC void gc_helper_get_regs(gc_helper_regs_t arr) {
+static void gc_helper_get_regs(gc_helper_regs_t arr) {
+    #ifdef __clang__
+    #pragma clang diagnostic push
+    #pragma clang diagnostic ignored "-Wuninitialized"
+    #endif
     register long r4 asm ("r4");
     register long r5 asm ("r5");
     register long r6 asm ("r6");
@@ -121,11 +125,14 @@ STATIC void gc_helper_get_regs(gc_helper_regs_t arr) {
     arr[7] = r11;
     arr[8] = r12;
     arr[9] = r13;
+    #ifdef __clang__
+    #pragma clang diagnostic pop
+    #endif
 }
 
 #elif defined(__aarch64__)
 
-STATIC void gc_helper_get_regs(gc_helper_regs_t arr) {
+static void gc_helper_get_regs(gc_helper_regs_t arr) {
     const register long x19 asm ("x19");
     const register long x20 asm ("x20");
     const register long x21 asm ("x21");
@@ -150,6 +157,105 @@ STATIC void gc_helper_get_regs(gc_helper_regs_t arr) {
     arr[10] = x29;
 }
 
+#elif defined(__riscv) && (__riscv_xlen <= 64)
+
+// Fallback implementation for RV32I and RV64I, prefer gchelper_rv32i.s
+// for RV32I targets or gchelper_rv64i.s for RV64I targets.
+
+static void gc_helper_get_regs(gc_helper_regs_t arr) {
+    register uintptr_t s0 asm ("x8");
+    register uintptr_t s1 asm ("x9");
+    register uintptr_t s2 asm ("x18");
+    register uintptr_t s3 asm ("x19");
+    register uintptr_t s4 asm ("x20");
+    register uintptr_t s5 asm ("x21");
+    register uintptr_t s6 asm ("x22");
+    register uintptr_t s7 asm ("x23");
+    register uintptr_t s8 asm ("x24");
+    register uintptr_t s9 asm ("x25");
+    register uintptr_t s10 asm ("x26");
+    register uintptr_t s11 asm ("x27");
+    arr[0] = s0;
+    arr[1] = s1;
+    arr[2] = s2;
+    arr[3] = s3;
+    arr[4] = s4;
+    arr[5] = s5;
+    arr[6] = s6;
+    arr[7] = s7;
+    arr[8] = s8;
+    arr[9] = s9;
+    arr[10] = s10;
+    arr[11] = s11;
+}
+
+#elif defined(__loongarch__) && defined(__loongarch64)
+
+// Fallback implementation for LOONG64, prefer gchelper_loong64.s.
+static void gc_helper_get_regs(gc_helper_regs_t arr) {
+    register uintptr_t s0 asm ("r23");
+    register uintptr_t s1 asm ("r24");
+    register uintptr_t s2 asm ("r25");
+    register uintptr_t s3 asm ("r26");
+    register uintptr_t s4 asm ("r27");
+    register uintptr_t s5 asm ("r28");
+    register uintptr_t s6 asm ("r29");
+    register uintptr_t s7 asm ("r30");
+    register uintptr_t s8 asm ("r31");
+    register uintptr_t s9 asm ("r22");
+    arr[0] = s0;
+    arr[1] = s1;
+    arr[2] = s2;
+    arr[3] = s3;
+    arr[4] = s4;
+    arr[5] = s5;
+    arr[6] = s6;
+    arr[7] = s7;
+    arr[8] = s8;
+    arr[9] = s9;
+}
+
+#elif defined(__powerpc__) && defined(__powerpc64__)
+
+static void gc_helper_get_regs(gc_helper_regs_t arr) {
+    register uintptr_t r14 __asm("r14");
+    register uintptr_t r15 __asm("r15");
+    register uintptr_t r16 __asm("r16");
+    register uintptr_t r17 __asm("r17");
+    register uintptr_t r18 __asm("r18");
+    register uintptr_t r19 __asm("r19");
+    register uintptr_t r20 __asm("r20");
+    register uintptr_t r21 __asm("r21");
+    register uintptr_t r22 __asm("r22");
+    register uintptr_t r23 __asm("r23");
+    register uintptr_t r24 __asm("r24");
+    register uintptr_t r25 __asm("r25");
+    register uintptr_t r26 __asm("r26");
+    register uintptr_t r27 __asm("r27");
+    register uintptr_t r28 __asm("r28");
+    register uintptr_t r29 __asm("r29");
+    register uintptr_t r30 __asm("r30");
+    register uintptr_t r31 __asm("r31");
+    arr[0] = r14;
+    arr[1] = r15;
+    arr[2] = r16;
+    arr[3] = r17;
+    arr[4] = r18;
+    arr[5] = r19;
+    arr[6] = r20;
+    arr[7] = r21;
+    arr[8] = r22;
+    arr[9] = r23;
+    arr[10] = r24;
+    arr[11] = r25;
+    arr[12] = r26;
+    arr[13] = r27;
+    arr[14] = r28;
+    arr[15] = r29;
+    arr[16] = r30;
+    arr[17] = r31;
+}
+
 #else
 
 #error "Architecture not supported for gc_helper_get_regs. Set MICROPY_GCREGS_SETJMP to use the fallback implementation."
@@ -161,7 +267,7 @@ STATIC void gc_helper_get_regs(gc_helper_regs_t arr) {
 // Even if we have specific support for an architecture, it is
 // possible to force use of setjmp-based implementation.
 
-STATIC void gc_helper_get_regs(gc_helper_regs_t arr) {
+static void gc_helper_get_regs(gc_helper_regs_t arr) {
     setjmp(arr);
 }
 
diff --git a/shared/runtime/gchelper_loong64.s b/shared/runtime/gchelper_loong64.s
new file mode 100644
index 00000000000..5180e1e24df
--- /dev/null
+++ b/shared/runtime/gchelper_loong64.s
@@ -0,0 +1,50 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2026 Alessandro Gatti
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+    .global gc_helper_get_regs_and_sp
+    .type   gc_helper_get_regs_and_sp, @function
+
+gc_helper_get_regs_and_sp:
+
+    /* Store registers into the given array. */
+
+    st.d  $r23, $r4, 0   /* Save S0.  */
+    st.d  $r24, $r4, 8   /* Save S1.  */
+    st.d  $r25, $r4, 16  /* Save S2.  */
+    st.d  $r26, $r4, 24  /* Save S3.  */
+    st.d  $r27, $r4, 32  /* Save S4.  */
+    st.d  $r28, $r4, 40  /* Save S5.  */
+    st.d  $r29, $r4, 48  /* Save S6.  */
+    st.d  $r30, $r4, 56  /* Save S7.  */
+    st.d  $r31, $r4, 64  /* Save S8.  */
+    st.d  $r22, $r4, 72  /* Save S9.  */
+
+    /* Return the stack pointer. */
+
+    add.d $r4, $r0, $r3
+    jirl  $r0, $r1, 0
+
+    .size gc_helper_get_regs_and_sp, .-gc_helper_get_regs_and_sp
diff --git a/shared/runtime/gchelper_rv32i.s b/shared/runtime/gchelper_rv32i.s
new file mode 100644
index 00000000000..64248e771a8
--- /dev/null
+++ b/shared/runtime/gchelper_rv32i.s
@@ -0,0 +1,52 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2024 Alessandro Gatti
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+    .global gc_helper_get_regs_and_sp
+    .type   gc_helper_get_regs_and_sp, @function
+
+gc_helper_get_regs_and_sp:
+
+    /* Store registers into the given array. */
+
+    sw    x8,  0(x10)  /* Save S0.  */
+    sw    x9,  4(x10)  /* Save S1.  */
+    sw   x18,  8(x10)  /* Save S2.  */
+    sw   x19, 12(x10)  /* Save S3.  */
+    sw   x20, 16(x10)  /* Save S4.  */
+    sw   x21, 20(x10)  /* Save S5.  */
+    sw   x22, 24(x10)  /* Save S6.  */
+    sw   x23, 28(x10)  /* Save S7.  */
+    sw   x24, 32(x10)  /* Save S8.  */
+    sw   x25, 36(x10)  /* Save S9.  */
+    sw   x26, 40(x10)  /* Save S10. */
+    sw   x27, 44(x10)  /* Save S11. */
+
+    /* Return the stack pointer. */
+
+    add  x10, x0, x2
+    jalr  x0, x1, 0
+
+    .size gc_helper_get_regs_and_sp, .-gc_helper_get_regs_and_sp
diff --git a/shared/runtime/gchelper_rv64i.s b/shared/runtime/gchelper_rv64i.s
new file mode 100644
index 00000000000..147a0e2bf5b
--- /dev/null
+++ b/shared/runtime/gchelper_rv64i.s
@@ -0,0 +1,52 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2024 Alessandro Gatti
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+    .global gc_helper_get_regs_and_sp
+    .type   gc_helper_get_regs_and_sp, @function
+
+gc_helper_get_regs_and_sp:
+
+    /* Store registers into the given array. */
+
+    sd    x8,  0(x10)  /* Save S0.  */
+    sd    x9,  8(x10)  /* Save S1.  */
+    sd   x18, 16(x10)  /* Save S2.  */
+    sd   x19, 24(x10)  /* Save S3.  */
+    sd   x20, 32(x10)  /* Save S4.  */
+    sd   x21, 40(x10)  /* Save S5.  */
+    sd   x22, 48(x10)  /* Save S6.  */
+    sd   x23, 56(x10)  /* Save S7.  */
+    sd   x24, 64(x10)  /* Save S8.  */
+    sd   x25, 72(x10)  /* Save S9.  */
+    sd   x26, 80(x10)  /* Save S10. */
+    sd   x27, 88(x10)  /* Save S11. */
+
+    /* Return the stack pointer. */
+
+    add  x10, x0, x2
+    jalr  x0, x1, 0
+
+    .size gc_helper_get_regs_and_sp, .-gc_helper_get_regs_and_sp
diff --git a/shared/runtime/mpirq.c b/shared/runtime/mpirq.c
index acd727403b5..4d848ae7e91 100644
--- a/shared/runtime/mpirq.c
+++ b/shared/runtime/mpirq.c
@@ -65,9 +65,19 @@ void mp_irq_init(mp_irq_obj_t *self, const mp_irq_methods_t *methods, mp_obj_t p
     self->ishard = false;
 }
 
-void mp_irq_handler(mp_irq_obj_t *self) {
-    if (self->handler != mp_const_none) {
-        if (self->ishard) {
+int mp_irq_dispatch(mp_obj_t handler, mp_obj_t parent, bool ishard) {
+    int result = 0;
+    if (handler != mp_const_none) {
+        if (ishard) {
+            #if MICROPY_STACK_CHECK && MICROPY_STACK_SIZE_HARD_IRQ > 0
+            // This callback executes in an ISR context so the stack-limit
+            // check must be changed to use the ISR stack for the duration
+            // of this function.
+            char *orig_stack_top = MP_STATE_THREAD(stack_top);
+            size_t orig_stack_limit = MP_STATE_THREAD(stack_limit);
+            mp_cstack_init_with_sp_here(MICROPY_STACK_SIZE_HARD_IRQ);
+            #endif
+
             // When executing code within a handler we must lock the scheduler to
             // prevent any scheduled callbacks from running, and lock the GC to
             // prevent any memory allocations.
@@ -75,34 +85,48 @@ void mp_irq_handler(mp_irq_obj_t *self) {
             gc_lock();
             nlr_buf_t nlr;
             if (nlr_push(&nlr) == 0) {
-                mp_call_function_1(self->handler, self->parent);
+                mp_call_function_1(handler, parent);
                 nlr_pop();
             } else {
-                // Uncaught exception; disable the callback so that it doesn't run again
-                self->methods->trigger(self->parent, 0);
-                self->handler = mp_const_none;
                 mp_printf(MICROPY_ERROR_PRINTER, "Uncaught exception in IRQ callback handler\n");
                 mp_obj_print_exception(MICROPY_ERROR_PRINTER, MP_OBJ_FROM_PTR(nlr.ret_val));
+                result = -1;
             }
             gc_unlock();
             mp_sched_unlock();
+
+            #if MICROPY_STACK_CHECK && MICROPY_STACK_SIZE_HARD_IRQ > 0
+            // Restore original stack-limit checking values.
+            MP_STATE_THREAD(stack_top) = orig_stack_top;
+            MP_STATE_THREAD(stack_limit) = orig_stack_limit;
+            #endif
         } else {
             // Schedule call to user function
-            mp_sched_schedule(self->handler, self->parent);
+            mp_sched_schedule(handler, parent);
         }
     }
+    return result;
+}
+
+
+void mp_irq_handler(mp_irq_obj_t *self) {
+    if (mp_irq_dispatch(self->handler, self->parent, self->ishard) < 0) {
+        // Uncaught exception; disable the callback so that it doesn't run again
+        self->methods->trigger(self->parent, 0);
+        self->handler = mp_const_none;
+    }
 }
 
 /******************************************************************************/
 // MicroPython bindings
 
-STATIC mp_obj_t mp_irq_flags(mp_obj_t self_in) {
+static mp_obj_t mp_irq_flags(mp_obj_t self_in) {
     mp_irq_obj_t *self = MP_OBJ_TO_PTR(self_in);
     return mp_obj_new_int(self->methods->info(self->parent, MP_IRQ_INFO_FLAGS));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_irq_flags_obj, mp_irq_flags);
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_irq_flags_obj, mp_irq_flags);
 
-STATIC mp_obj_t mp_irq_trigger(size_t n_args, const mp_obj_t *args) {
+static mp_obj_t mp_irq_trigger(size_t n_args, const mp_obj_t *args) {
     mp_irq_obj_t *self = MP_OBJ_TO_PTR(args[0]);
     mp_obj_t ret_obj = mp_obj_new_int(self->methods->info(self->parent, MP_IRQ_INFO_TRIGGERS));
     if (n_args == 2) {
@@ -111,19 +135,19 @@ STATIC mp_obj_t mp_irq_trigger(size_t n_args, const mp_obj_t *args) {
     }
     return ret_obj;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_irq_trigger_obj, 1, 2, mp_irq_trigger);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_irq_trigger_obj, 1, 2, mp_irq_trigger);
 
-STATIC mp_obj_t mp_irq_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+static mp_obj_t mp_irq_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
     mp_arg_check_num(n_args, n_kw, 0, 0, false);
     mp_irq_handler(MP_OBJ_TO_PTR(self_in));
     return mp_const_none;
 }
 
-STATIC const mp_rom_map_elem_t mp_irq_locals_dict_table[] = {
+static const mp_rom_map_elem_t mp_irq_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_flags),               MP_ROM_PTR(&mp_irq_flags_obj) },
     { MP_ROM_QSTR(MP_QSTR_trigger),             MP_ROM_PTR(&mp_irq_trigger_obj) },
 };
-STATIC MP_DEFINE_CONST_DICT(mp_irq_locals_dict, mp_irq_locals_dict_table);
+static MP_DEFINE_CONST_DICT(mp_irq_locals_dict, mp_irq_locals_dict_table);
 
 MP_DEFINE_CONST_OBJ_TYPE(
     mp_irq_type,
diff --git a/shared/runtime/mpirq.h b/shared/runtime/mpirq.h
index dd423c01011..c65741e0e49 100644
--- a/shared/runtime/mpirq.h
+++ b/shared/runtime/mpirq.h
@@ -77,6 +77,7 @@ extern const mp_obj_type_t mp_irq_type;
 
 mp_irq_obj_t *mp_irq_new(const mp_irq_methods_t *methods, mp_obj_t parent);
 void mp_irq_init(mp_irq_obj_t *self, const mp_irq_methods_t *methods, mp_obj_t parent);
+int mp_irq_dispatch(mp_obj_t handler, mp_obj_t parent, bool ishard);
 void mp_irq_handler(mp_irq_obj_t *self);
 
 #endif // MICROPY_INCLUDED_LIB_UTILS_MPIRQ_H
diff --git a/shared/runtime/pyexec.c b/shared/runtime/pyexec.c
index e32150e5e73..bbb85543c11 100644
--- a/shared/runtime/pyexec.c
+++ b/shared/runtime/pyexec.c
@@ -35,19 +35,15 @@
 #include "py/gc.h"
 #include "py/frozenmod.h"
 #include "py/mphal.h"
-#if MICROPY_HW_ENABLE_USB
-#include "irq.h"
-#include "usb.h"
-#endif
 #include "shared/readline/readline.h"
 #include "shared/runtime/pyexec.h"
+#include "extmod/modplatform.h"
 #include "genhdr/mpversion.h"
 
 pyexec_mode_kind_t pyexec_mode_kind = PYEXEC_MODE_FRIENDLY_REPL;
-int pyexec_system_exit = 0;
 
 #if MICROPY_REPL_INFO
-STATIC bool repl_display_debugging_info = 0;
+static bool repl_display_debugging_info = 0;
 #endif
 
 #define EXEC_FLAG_PRINT_EOF             (1 << 0)
@@ -64,7 +60,7 @@ STATIC bool repl_display_debugging_info = 0;
 // EXEC_FLAG_PRINT_EOF prints 2 EOF chars: 1 after normal output, 1 after exception output
 // EXEC_FLAG_ALLOW_DEBUGGING allows debugging info to be printed after executing the code
 // EXEC_FLAG_IS_REPL is used for REPL inputs (flag passed on to mp_compile)
-STATIC int parse_compile_execute(const void *source, mp_parse_input_kind_t input_kind, mp_uint_t exec_flags) {
+static int parse_compile_execute(const void *source, mp_parse_input_kind_t input_kind, mp_uint_t exec_flags) {
     int ret = 0;
     #if MICROPY_REPL_INFO
     uint32_t start = 0;
@@ -74,12 +70,13 @@ STATIC int parse_compile_execute(const void *source, mp_parse_input_kind_t input
     MICROPY_BOARD_BEFORE_PYTHON_EXEC(input_kind, exec_flags);
     #endif
 
-    // by default a SystemExit exception returns 0
-    pyexec_system_exit = 0;
-
     nlr_buf_t nlr;
     nlr.ret_val = NULL;
     if (nlr_push(&nlr) == 0) {
+        #if MICROPY_PYEXEC_ENABLE_VM_ABORT
+        nlr_set_abort(&nlr);
+        #endif
+
         mp_obj_t module_fun;
         #if MICROPY_MODULE_FROZEN_MPY
         if (exec_flags & EXEC_FLAG_SOURCE_IS_RAW_CODE) {
@@ -88,7 +85,7 @@ STATIC int parse_compile_execute(const void *source, mp_parse_input_kind_t input
             mp_module_context_t *ctx = m_new_obj(mp_module_context_t);
             ctx->module.globals = mp_globals_get();
             ctx->constants = frozen->constants;
-            module_fun = mp_make_function_from_raw_code(frozen->rc, ctx, NULL);
+            module_fun = mp_make_function_from_proto_fun(frozen->proto_fun, ctx, NULL);
         } else
         #endif
         {
@@ -100,13 +97,26 @@ STATIC int parse_compile_execute(const void *source, mp_parse_input_kind_t input
             } else if (exec_flags & EXEC_FLAG_SOURCE_IS_READER) {
                 lex = mp_lexer_new(MP_QSTR__lt_stdin_gt_, *(mp_reader_t *)source);
             } else if (exec_flags & EXEC_FLAG_SOURCE_IS_FILENAME) {
-                lex = mp_lexer_new_from_file(source);
+                lex = mp_lexer_new_from_file(qstr_from_str(source));
             } else {
                 lex = (mp_lexer_t *)source;
             }
             // source is a lexer, parse and compile the script
             qstr source_name = lex->source_name;
+            #if MICROPY_MODULE___FILE__
+            if (input_kind == MP_PARSE_FILE_INPUT) {
+                mp_store_global(MP_QSTR___file__, MP_OBJ_NEW_QSTR(source_name));
+            }
+            #endif
             mp_parse_tree_t parse_tree = mp_parse(lex, input_kind);
+            #if defined(MICROPY_UNIX_COVERAGE)
+            // allow to print the parse tree in the coverage build
+            if (MP_STATE_VM(mp_verbose_flag) >= 3) {
+                printf("----------------\n");
+                mp_parse_node_print(&mp_plat_print, parse_tree.root, 0);
+                printf("----------------\n");
+            }
+            #endif
             module_fun = mp_compile(&parse_tree, source_name, exec_flags & EXEC_FLAG_IS_REPL);
             #else
             mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("script compilation not supported"));
@@ -120,18 +130,23 @@ STATIC int parse_compile_execute(const void *source, mp_parse_input_kind_t input
         #if MICROPY_REPL_INFO
         start = mp_hal_ticks_ms();
         #endif
-        mp_call_function_0(module_fun);
+        #if MICROPY_PYEXEC_COMPILE_ONLY
+        if (!mp_compile_only)
+        #endif
+        {
+            mp_call_function_0(module_fun);
+        }
         mp_hal_set_interrupt_char(-1); // disable interrupt
-        mp_handle_pending(true); // handle any pending exceptions (and any callbacks)
+        mp_handle_pending(MP_HANDLE_PENDING_CALLBACKS_AND_EXCEPTIONS);
         nlr_pop();
-        ret = 1;
+        ret = PYEXEC_NORMAL_EXIT;
         if (exec_flags & EXEC_FLAG_PRINT_EOF) {
             mp_hal_stdout_tx_strn("\x04", 1);
         }
     } else {
         // uncaught exception
         mp_hal_set_interrupt_char(-1); // disable interrupt
-        mp_handle_pending(false); // clear any pending exceptions (and run any callbacks)
+        mp_handle_pending(MP_HANDLE_PENDING_CALLBACKS_AND_CLEAR_EXCEPTIONS);
 
         if (exec_flags & EXEC_FLAG_SOURCE_IS_READER) {
             const mp_reader_t *reader = source;
@@ -143,15 +158,44 @@ STATIC int parse_compile_execute(const void *source, mp_parse_input_kind_t input
             mp_hal_stdout_tx_strn("\x04", 1);
         }
 
-        // check for SystemExit
-        if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(((mp_obj_base_t *)nlr.ret_val)->type), MP_OBJ_FROM_PTR(&mp_type_SystemExit))) {
-            // at the moment, the value of SystemExit is unused
-            ret = pyexec_system_exit;
-        } else {
+        #if MICROPY_PYEXEC_ENABLE_VM_ABORT
+        if (nlr.ret_val == NULL) { // abort
+            ret = PYEXEC_ABORT;
+        } else
+        #endif
+        if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(((mp_obj_base_t *)nlr.ret_val)->type), MP_OBJ_FROM_PTR(&mp_type_SystemExit))) { // system exit
+            #if MICROPY_PYEXEC_ENABLE_EXIT_CODE_HANDLING
+            // None is an exit value of 0; an int is its value; anything else is 1
+            mp_obj_t val = mp_obj_exception_get_value(MP_OBJ_FROM_PTR(nlr.ret_val));
+            if (val != mp_const_none) {
+                if (mp_obj_is_int(val)) {
+                    ret = (int)mp_obj_int_get_truncated(val);
+                } else {
+                    mp_obj_print_helper(MICROPY_ERROR_PRINTER, val, PRINT_STR);
+                    mp_print_str(MICROPY_ERROR_PRINTER, "\n");
+                    ret = PYEXEC_UNHANDLED_EXCEPTION;
+                }
+            } else {
+                ret = PYEXEC_NORMAL_EXIT;
+            }
+            // Set PYEXEC_FORCED_EXIT flag so REPL knows to exit
+            ret |= PYEXEC_FORCED_EXIT;
+            #else
+            ret = PYEXEC_FORCED_EXIT;
+            #endif
+        } else { // other exception
             mp_obj_print_exception(&mp_plat_print, MP_OBJ_FROM_PTR(nlr.ret_val));
-            ret = 0;
+            ret = PYEXEC_UNHANDLED_EXCEPTION;
+            #if MICROPY_PYEXEC_ENABLE_EXIT_CODE_HANDLING
+            if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(((mp_obj_base_t *)nlr.ret_val)->type), MP_OBJ_FROM_PTR(&mp_type_KeyboardInterrupt))) { // keyboard interrupt
+                ret = PYEXEC_KEYBOARD_INTERRUPT;
+            }
+            #endif
         }
     }
+    #if MICROPY_PYEXEC_ENABLE_VM_ABORT
+    nlr_set_abort(NULL);
+    #endif
 
     #if MICROPY_REPL_INFO
     // display debugging info if wanted
@@ -201,7 +245,7 @@ typedef struct _mp_reader_stdin_t {
     uint16_t window_remain;
 } mp_reader_stdin_t;
 
-STATIC mp_uint_t mp_reader_stdin_readbyte(void *data) {
+static mp_uint_t mp_reader_stdin_readbyte(void *data) {
     mp_reader_stdin_t *reader = (mp_reader_stdin_t *)data;
 
     if (reader->eof) {
@@ -233,7 +277,7 @@ STATIC mp_uint_t mp_reader_stdin_readbyte(void *data) {
     return c;
 }
 
-STATIC void mp_reader_stdin_close(void *data) {
+static void mp_reader_stdin_close(void *data) {
     mp_reader_stdin_t *reader = (mp_reader_stdin_t *)data;
     if (!reader->eof) {
         reader->eof = true;
@@ -247,7 +291,7 @@ STATIC void mp_reader_stdin_close(void *data) {
     }
 }
 
-STATIC void mp_reader_new_stdin(mp_reader_t *reader, mp_reader_stdin_t *reader_stdin, uint16_t buf_max) {
+static void mp_reader_new_stdin(mp_reader_t *reader, mp_reader_stdin_t *reader_stdin, uint16_t buf_max) {
     // Make flow-control window half the buffer size, and indicate to the host that 2x windows are
     // free (sending the window size implicitly indicates that a window is free, and then the 0x01
     // indicates that another window is free).
@@ -263,7 +307,7 @@ STATIC void mp_reader_new_stdin(mp_reader_t *reader, mp_reader_stdin_t *reader_s
     reader->close = mp_reader_stdin_close;
 }
 
-STATIC int do_reader_stdin(int c) {
+static int do_reader_stdin(int c) {
     if (c != 'A') {
         // Unsupported command.
         mp_hal_stdout_tx_strn("R\x00", 2);
@@ -294,8 +338,8 @@ typedef struct _repl_t {
 
 repl_t repl;
 
-STATIC int pyexec_raw_repl_process_char(int c);
-STATIC int pyexec_friendly_repl_process_char(int c);
+static int pyexec_raw_repl_process_char(int c);
+static int pyexec_friendly_repl_process_char(int c);
 
 void pyexec_event_repl_init(void) {
     MP_STATE_VM(repl_line) = vstr_new(32);
@@ -310,7 +354,7 @@ void pyexec_event_repl_init(void) {
     }
 }
 
-STATIC int pyexec_raw_repl_process_char(int c) {
+static int pyexec_raw_repl_process_char(int c) {
     if (c == CHAR_CTRL_A) {
         // reset raw REPL
         if (vstr_len(MP_STATE_VM(repl_line)) == 2 && vstr_str(MP_STATE_VM(repl_line))[0] == CHAR_CTRL_E) {
@@ -364,7 +408,7 @@ STATIC int pyexec_raw_repl_process_char(int c) {
     return 0;
 }
 
-STATIC int pyexec_friendly_repl_process_char(int c) {
+static int pyexec_friendly_repl_process_char(int c) {
     if (repl.paste_mode) {
         if (c == CHAR_CTRL_C) {
             // cancel everything
@@ -426,6 +470,8 @@ STATIC int pyexec_friendly_repl_process_char(int c) {
             vstr_reset(MP_STATE_VM(repl_line));
             repl.paste_mode = true;
             return 0;
+        } else if (vstr_len(MP_STATE_VM(repl_line)) == 0) {
+            goto input_restart;
         }
 
         if (ret < 0) {
@@ -495,10 +541,20 @@ MP_REGISTER_ROOT_POINTER(vstr_t * repl_line);
 
 #else // MICROPY_REPL_EVENT_DRIVEN
 
+#if !MICROPY_HAL_HAS_STDIO_MODE_SWITCH
+// If the port doesn't need any stdio mode switching calls then provide trivial ones.
+static inline void mp_hal_stdio_mode_raw(void) {
+}
+static inline void mp_hal_stdio_mode_orig(void) {
+}
+#endif
+
 int pyexec_raw_repl(void) {
     vstr_t line;
     vstr_init(&line, 32);
 
+    mp_hal_stdio_mode_raw();
+
 raw_repl_reset:
     mp_hal_stdout_tx_str("raw REPL; CTRL-B to exit\r\n");
 
@@ -512,6 +568,7 @@ int pyexec_raw_repl(void) {
                 if (vstr_len(&line) == 2 && vstr_str(&line)[0] == CHAR_CTRL_E) {
                     int ret = do_reader_stdin(vstr_str(&line)[1]);
                     if (ret & PYEXEC_FORCED_EXIT) {
+                        mp_hal_stdio_mode_orig();
                         return ret;
                     }
                     vstr_reset(&line);
@@ -524,6 +581,7 @@ int pyexec_raw_repl(void) {
                 mp_hal_stdout_tx_str("\r\n");
                 vstr_clear(&line);
                 pyexec_mode_kind = PYEXEC_MODE_FRIENDLY_REPL;
+                mp_hal_stdio_mode_orig();
                 return 0;
             } else if (c == CHAR_CTRL_C) {
                 // clear line
@@ -544,13 +602,17 @@ int pyexec_raw_repl(void) {
             // exit for a soft reset
             mp_hal_stdout_tx_str("\r\n");
             vstr_clear(&line);
+            mp_hal_stdio_mode_orig();
             return PYEXEC_FORCED_EXIT;
         }
 
+        // Switch to original terminal mode to execute code, eg to support keyboard interrupt (SIGINT).
+        mp_hal_stdio_mode_orig();
         int ret = parse_compile_execute(&line, MP_PARSE_FILE_INPUT, EXEC_FLAG_PRINT_EOF | EXEC_FLAG_SOURCE_IS_VSTR);
         if (ret & PYEXEC_FORCED_EXIT) {
             return ret;
         }
+        mp_hal_stdio_mode_raw();
     }
 }
 
@@ -558,6 +620,8 @@ int pyexec_friendly_repl(void) {
     vstr_t line;
     vstr_init(&line, 32);
 
+    mp_hal_stdio_mode_raw();
+
 friendly_repl_reset:
     mp_hal_stdout_tx_str(MICROPY_BANNER_NAME_AND_VERSION);
     mp_hal_stdout_tx_str("; " MICROPY_BANNER_MACHINE);
@@ -586,25 +650,9 @@ int pyexec_friendly_repl(void) {
 
     for (;;) {
     input_restart:
-
-        #if MICROPY_HW_ENABLE_USB
-        if (usb_vcp_is_enabled()) {
-            // If the user gets to here and interrupts are disabled then
-            // they'll never see the prompt, traceback etc. The USB REPL needs
-            // interrupts to be enabled or no transfers occur. So we try to
-            // do the user a favor and re-enable interrupts.
-            if (query_irq() == IRQ_STATE_DISABLED) {
-                enable_irq(IRQ_STATE_ENABLED);
-                mp_hal_stdout_tx_str("MPY: enabling IRQs\r\n");
-            }
-        }
-        #endif
-
         // If the GC is locked at this point there is no way out except a reset,
         // so force the GC to be unlocked to help the user debug what went wrong.
-        if (MP_STATE_THREAD(gc_lock_depth) != 0) {
-            MP_STATE_THREAD(gc_lock_depth) = 0;
-        }
+        MP_STATE_THREAD(gc_lock_depth) = 0;
 
         vstr_reset(&line);
         int ret = readline(&line, mp_repl_get_ps1());
@@ -615,6 +663,7 @@ int pyexec_friendly_repl(void) {
             mp_hal_stdout_tx_str("\r\n");
             vstr_clear(&line);
             pyexec_mode_kind = PYEXEC_MODE_RAW_REPL;
+            mp_hal_stdio_mode_orig();
             return 0;
         } else if (ret == CHAR_CTRL_B) {
             // reset friendly REPL
@@ -628,6 +677,7 @@ int pyexec_friendly_repl(void) {
             // exit for a soft reset
             mp_hal_stdout_tx_str("\r\n");
             vstr_clear(&line);
+            mp_hal_stdio_mode_orig();
             return PYEXEC_FORCED_EXIT;
         } else if (ret == CHAR_CTRL_E) {
             // paste mode
@@ -672,10 +722,13 @@ int pyexec_friendly_repl(void) {
             }
         }
 
+        // Switch to original terminal mode to execute code, eg to support keyboard interrupt (SIGINT).
+        mp_hal_stdio_mode_orig();
         ret = parse_compile_execute(&line, parse_input_kind, EXEC_FLAG_ALLOW_DEBUGGING | EXEC_FLAG_IS_REPL | EXEC_FLAG_SOURCE_IS_VSTR);
         if (ret & PYEXEC_FORCED_EXIT) {
             return ret;
         }
+        mp_hal_stdio_mode_raw();
     }
 }
 
@@ -686,6 +739,7 @@ int pyexec_file(const char *filename) {
     return parse_compile_execute(filename, MP_PARSE_FILE_INPUT, EXEC_FLAG_SOURCE_IS_FILENAME);
 }
 
+
 int pyexec_file_if_exists(const char *filename) {
     #if MICROPY_MODULE_FROZEN
     if (mp_find_frozen_module(filename, NULL, NULL) == MP_IMPORT_STAT_FILE) {
@@ -724,6 +778,11 @@ int pyexec_frozen_module(const char *name, bool allow_keyboard_interrupt) {
 }
 #endif
 
+int pyexec_vstr(vstr_t *str, bool allow_keyboard_interrupt) {
+    mp_uint_t exec_flags = allow_keyboard_interrupt ? 0 : EXEC_FLAG_NO_INTERRUPT;
+    return parse_compile_execute(str, MP_PARSE_FILE_INPUT, exec_flags | EXEC_FLAG_SOURCE_IS_VSTR);
+}
+
 #if MICROPY_REPL_INFO
 mp_obj_t pyb_set_repl_info(mp_obj_t o_value) {
     repl_display_debugging_info = mp_obj_get_int(o_value);
diff --git a/shared/runtime/pyexec.h b/shared/runtime/pyexec.h
index 64c5ef94340..fb52c09b1ce 100644
--- a/shared/runtime/pyexec.h
+++ b/shared/runtime/pyexec.h
@@ -35,18 +35,25 @@ typedef enum {
 
 extern pyexec_mode_kind_t pyexec_mode_kind;
 
-// Set this to the value (eg PYEXEC_FORCED_EXIT) that will be propagated through
-// the pyexec functions if a SystemExit exception is raised by the running code.
-// It will reset to 0 at the start of each execution (eg each REPL entry).
-extern int pyexec_system_exit;
-
 #define PYEXEC_FORCED_EXIT (0x100)
 
+#if MICROPY_PYEXEC_ENABLE_EXIT_CODE_HANDLING
+#define PYEXEC_NORMAL_EXIT (0)
+#define PYEXEC_UNHANDLED_EXCEPTION (1)
+#define PYEXEC_KEYBOARD_INTERRUPT (128 + 2) // same as SIG INT exit code
+#define PYEXEC_ABORT (128 + 9)              // same as SIG KILL exit code
+#else
+#define PYEXEC_NORMAL_EXIT (1)
+#define PYEXEC_UNHANDLED_EXCEPTION (0)
+#define PYEXEC_ABORT PYEXEC_FORCED_EXIT
+#endif
+
 int pyexec_raw_repl(void);
 int pyexec_friendly_repl(void);
 int pyexec_file(const char *filename);
 int pyexec_file_if_exists(const char *filename);
 int pyexec_frozen_module(const char *name, bool allow_keyboard_interrupt);
+int pyexec_vstr(vstr_t *str, bool allow_keyboard_interrupt);
 void pyexec_event_repl_init(void);
 int pyexec_event_repl_process_char(int c);
 extern uint8_t pyexec_repl_active;
diff --git a/shared/runtime/semihosting.c b/shared/runtime/semihosting.c
deleted file mode 100644
index 18c7f5d57a3..00000000000
--- a/shared/runtime/semihosting.c
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * This file is part of the MicroPython project, http://micropython.org/
- *
- * The MIT License (MIT)
- *
- * Copyright (c) 2018 Ayke van Laethem
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-#include "semihosting.h"
-
-// Resources:
-// http://embed.rs/articles/2016/semi-hosting-rust/
-// https://wiki.dlang.org/Minimal_semihosted_ARM_Cortex-M_%22Hello_World%22
-// https://github.com/arduino/OpenOCD/blob/master/src/target/arm_semihosting.c
-
-#define SYS_OPEN   0x01
-#define SYS_WRITEC 0x03
-#define SYS_WRITE  0x05
-#define SYS_READC  0x07
-
-// Constants:
-#define OPEN_MODE_READ  (0) // mode "r"
-#define OPEN_MODE_WRITE (4) // mode "w"
-
-#ifndef __thumb__
-#error Semihosting is only implemented for ARM microcontrollers.
-#endif
-
-static int mp_semihosting_stdout;
-
-static uint32_t mp_semihosting_call(uint32_t num, const void *arg) {
-    // A semihosting call works as follows, similar to a SVCall:
-    //   * the call is invoked by a special breakpoint: 0xAB
-    //   * the command is placed in r0
-    //   * a pointer to the arguments is placed in r1
-    //   * the return value is placed in r0
-    // Note that because it uses the breakpoint instruction, applications
-    // will hang if they're not connected to a debugger. And they'll be
-    // stuck in a breakpoint if semihosting is not specifically enabled in
-    // the debugger.
-    // Also note that semihosting is extremely slow (sometimes >100ms per
-    // call).
-    register uint32_t num_reg __asm__ ("r0") = num;
-    register const void *args_reg __asm__ ("r1") = arg;
-    __asm__ __volatile__ (
-        "bkpt 0xAB\n"         // invoke semihosting call
-        : "+r" (num_reg)      // call number and result
-        : "r" (args_reg)      // arguments
-        : "memory");          // make sure args aren't optimized away
-    return num_reg; // r0, which became the result
-}
-
-static int mp_semihosting_open_console(uint32_t mode) {
-    struct {
-        char *name;
-        uint32_t mode;
-        uint32_t name_len;
-    } args = {
-        .name = ":tt",     // magic path to console
-        .mode = mode,      // e.g. "r", "w" (see OPEN_MODE_* constants)
-        .name_len = 3,     // strlen(":tt")
-    };
-    return mp_semihosting_call(SYS_OPEN, &args);
-}
-
-void mp_semihosting_init() {
-    mp_semihosting_stdout = mp_semihosting_open_console(OPEN_MODE_WRITE);
-}
-
-int mp_semihosting_rx_char() {
-    return mp_semihosting_call(SYS_READC, NULL);
-}
-
-static void mp_semihosting_tx_char(char c) {
-    mp_semihosting_call(SYS_WRITEC, &c);
-}
-
-uint32_t mp_semihosting_tx_strn(const char *str, size_t len) {
-    if (len == 0) {
-        return 0; // nothing to do
-    }
-    if (len == 1) {
-        mp_semihosting_tx_char(*str); // maybe faster?
-        return 0;
-    }
-
-    struct {
-        uint32_t fd;
-        const char *str;
-        uint32_t len;
-    } args = {
-        .fd = mp_semihosting_stdout,
-        .str = str,
-        .len = len,
-    };
-    return mp_semihosting_call(SYS_WRITE, &args);
-}
-
-uint32_t mp_semihosting_tx_strn_cooked(const char *str, size_t len) {
-    // Write chunks of data until (excluding) the first '\n' character,
-    // insert a '\r' character, and then continue with the next chunk
-    // (starting with '\n').
-    // Doing byte-by-byte writes would be easier to implement but is far
-    // too slow.
-    size_t start = 0;
-    for (size_t i = 0; i < len; i++) {
-        if (str[i] == '\n') {
-            mp_semihosting_tx_strn(str + start, i - start);
-            mp_semihosting_tx_char('\r');
-            start = i;
-        }
-    }
-    return mp_semihosting_tx_strn(str + start, len - start);
-}
diff --git a/shared/runtime/semihosting.h b/shared/runtime/semihosting.h
deleted file mode 100644
index d053a03edaa..00000000000
--- a/shared/runtime/semihosting.h
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * This file is part of the MicroPython project, http://micropython.org/
- *
- * The MIT License (MIT)
- *
- * Copyright (c) 2018 Ayke van Laethem
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-#ifndef MICROPY_INCLUDED_LIB_UTILS_SEMIHOSTING_H
-#define MICROPY_INCLUDED_LIB_UTILS_SEMIHOSTING_H
-
-/*
-
-To use semi-hosting for a replacement UART:
-- Add lib/semihosting/semihosting.c to the Makefile sources.
-- Call mp_semihosting_init() in main(), around the time UART is initialized.
-- Replace mp_hal_stdin_rx_chr and similar in mphalport.c with the semihosting equivalent.
-- Include lib/semihosting/semihosting.h in the relevant files.
-
-Then make sure the debugger is attached and enables semihosting.  In OpenOCD this is
-done with ARM semihosting enable followed by reset.  The terminal will need further
-configuration to work with MicroPython (bash: stty raw -echo).
-
-*/
-
-#include 
-#include 
-
-void mp_semihosting_init();
-int mp_semihosting_rx_char();
-uint32_t mp_semihosting_tx_strn(const char *str, size_t len);
-uint32_t mp_semihosting_tx_strn_cooked(const char *str, size_t len);
-
-#endif // MICROPY_INCLUDED_LIB_UTILS_SEMIHOSTING_H
diff --git a/shared/runtime/semihosting_arm.c b/shared/runtime/semihosting_arm.c
new file mode 100644
index 00000000000..f4d168f79bc
--- /dev/null
+++ b/shared/runtime/semihosting_arm.c
@@ -0,0 +1,157 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2018 Ayke van Laethem
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "semihosting_arm.h"
+
+// Resources:
+// http://embed.rs/articles/2016/semi-hosting-rust/
+// https://wiki.dlang.org/Minimal_semihosted_ARM_Cortex-M_%22Hello_World%22
+// https://github.com/arduino/OpenOCD/blob/master/src/target/arm_semihosting.c
+
+#define SYS_OPEN   0x01
+#define SYS_WRITEC 0x03
+#define SYS_WRITE  0x05
+#define SYS_READ   0x06
+#define SYS_READC  0x07
+#define SYS_EXIT   0x18
+
+// Constants:
+#define OPEN_MODE_READ  (0) // mode "r"
+#define OPEN_MODE_WRITE (4) // mode "w"
+
+static int mp_semihosting_stdout;
+
+static uint32_t mp_semihosting_call(uint32_t num, const void *arg) {
+    // A semihosting call works as follows, similar to a SVCall:
+    //   * the call is invoked by a special breakpoint: 0xAB
+    //   * the command is placed in r0
+    //   * a pointer to the arguments is placed in r1
+    //   * the return value is placed in r0
+    // Note that because it uses the breakpoint instruction, applications
+    // will hang if they're not connected to a debugger. And they'll be
+    // stuck in a breakpoint if semihosting is not specifically enabled in
+    // the debugger.
+    // Also note that semihosting is extremely slow (sometimes >100ms per
+    // call).
+    register uint32_t num_reg __asm__ ("r0") = num;
+    register const void *args_reg __asm__ ("r1") = arg;
+    __asm__ __volatile__ (
+        #if defined(__ARM_ARCH_ISA_ARM)
+        "svc 0x00123456\n"    // invoke semihosting call
+        #elif defined(__ARM_ARCH_ISA_THUMB)
+        "bkpt 0xAB\n"         // invoke semihosting call
+        #else
+        #error Unknown architecture
+        #endif
+        : "+r" (num_reg)      // call number and result
+        : "r" (args_reg)      // arguments
+        : "memory");          // make sure args aren't optimized away
+    return num_reg; // r0, which became the result
+}
+
+static int mp_semihosting_open_console(uint32_t mode) {
+    struct {
+        char *name;
+        uint32_t mode;
+        uint32_t name_len;
+    } args = {
+        .name = ":tt",     // magic path to console
+        .mode = mode,      // e.g. "r", "w" (see OPEN_MODE_* constants)
+        .name_len = 3,     // strlen(":tt")
+    };
+    return mp_semihosting_call(SYS_OPEN, &args);
+}
+
+void mp_semihosting_init() {
+    mp_semihosting_stdout = mp_semihosting_open_console(OPEN_MODE_WRITE);
+}
+
+void mp_semihosting_exit(int status) {
+    if (status == 0) {
+        status = 0x20026;
+    }
+    mp_semihosting_call(SYS_EXIT, (void *)(uintptr_t)status);
+}
+
+int mp_semihosting_rx_char() {
+    return mp_semihosting_call(SYS_READC, NULL);
+}
+
+// Returns 0 on success.
+int mp_semihosting_rx_chars(char *str, size_t len) {
+    struct {
+        uint32_t fd;
+        const char *str;
+        uint32_t len;
+    } args = {
+        .fd = mp_semihosting_stdout,
+        .str = str,
+        .len = len,
+    };
+    return mp_semihosting_call(SYS_READ, &args);
+}
+
+static void mp_semihosting_tx_char(char c) {
+    mp_semihosting_call(SYS_WRITEC, &c);
+}
+
+uint32_t mp_semihosting_tx_strn(const char *str, size_t len) {
+    if (len == 0) {
+        return 0; // nothing to do
+    }
+    if (len == 1) {
+        mp_semihosting_tx_char(*str); // maybe faster?
+        return 0;
+    }
+
+    struct {
+        uint32_t fd;
+        const char *str;
+        uint32_t len;
+    } args = {
+        .fd = mp_semihosting_stdout,
+        .str = str,
+        .len = len,
+    };
+    return mp_semihosting_call(SYS_WRITE, &args);
+}
+
+uint32_t mp_semihosting_tx_strn_cooked(const char *str, size_t len) {
+    // Write chunks of data until (excluding) the first '\n' character,
+    // insert a '\r' character, and then continue with the next chunk
+    // (starting with '\n').
+    // Doing byte-by-byte writes would be easier to implement but is far
+    // too slow.
+    size_t start = 0;
+    for (size_t i = 0; i < len; i++) {
+        if (str[i] == '\n') {
+            mp_semihosting_tx_strn(str + start, i - start);
+            mp_semihosting_tx_char('\r');
+            start = i;
+        }
+    }
+    return mp_semihosting_tx_strn(str + start, len - start);
+}
diff --git a/shared/runtime/semihosting_arm.h b/shared/runtime/semihosting_arm.h
new file mode 100644
index 00000000000..08fb66578ac
--- /dev/null
+++ b/shared/runtime/semihosting_arm.h
@@ -0,0 +1,55 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2018 Ayke van Laethem
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+#ifndef MICROPY_INCLUDED_SHARED_RUNTIME_SEMIHOSTING_ARM_H
+#define MICROPY_INCLUDED_SHARED_RUNTIME_SEMIHOSTING_ARM_H
+
+/*
+
+To use semi-hosting for a replacement UART:
+- Add shared/runtime/semihosting_arm.c to the Makefile sources.
+- Call mp_semihosting_init() in main(), around the time UART is initialized.
+- Replace mp_hal_stdin_rx_chr and similar in mphalport.c with the semihosting equivalent.
+- Include shared/runtime/semihosting_arm.h in the relevant files.
+
+Then make sure the debugger is attached and enables semihosting.  In OpenOCD this is
+done with ARM semihosting enable followed by reset.  The terminal will need further
+configuration to work with MicroPython (bash: stty raw -echo).
+
+If mp_semihosting_rx_char() doesn't work then try mp_semihosting_rx_chars(str, 1).
+
+*/
+
+#include 
+#include 
+
+void mp_semihosting_init();
+void mp_semihosting_exit(int status);
+int mp_semihosting_rx_char();
+int mp_semihosting_rx_chars(char *str, size_t len);
+uint32_t mp_semihosting_tx_strn(const char *str, size_t len);
+uint32_t mp_semihosting_tx_strn_cooked(const char *str, size_t len);
+
+#endif // MICROPY_INCLUDED_SHARED_RUNTIME_SEMIHOSTING_ARM_H
diff --git a/shared/runtime/semihosting_rv32.c b/shared/runtime/semihosting_rv32.c
new file mode 100644
index 00000000000..1d02b69b23e
--- /dev/null
+++ b/shared/runtime/semihosting_rv32.c
@@ -0,0 +1,481 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2024 Alessandro Gatti
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include 
+
+#include "semihosting_rv32.h"
+
+#if !defined(__riscv) || !defined(__riscv_xlen) || (__riscv_xlen != 32)
+#error "This semihosting support code is only available for RV32 targets."
+#endif
+
+// Features file magic header.
+#define MAGIC_SIZE 4
+#define MAGIC_0 0x53 // 'S'
+#define MAGIC_1 0x48 // 'H'
+#define MAGIC_2 0x46 // 'B'
+#define MAGIC_3 0x42 // 'F'
+
+#define CMDLINE_MIN_BUFFER_SIZE 80
+
+#define SYS_OPEN          0x01
+#define SYS_CLOSE         0x02
+#define SYS_WRITEC        0x03
+#define SYS_WRITE0        0x04
+#define SYS_WRITE         0x05
+#define SYS_READ          0x06
+#define SYS_READC         0x07
+#define SYS_ISERROR       0x08
+#define SYS_ISTTY         0x09
+#define SYS_SEEK          0x0A
+#define SYS_FLEN          0x0C
+#define SYS_TMPNAM        0x0D
+#define SYS_REMOVE        0x0E
+#define SYS_RENAME        0x0F
+#define SYS_CLOCK         0x10
+#define SYS_TIME          0x11
+#define SYS_SYSTEM        0x12
+#define SYS_ERRNO         0x13
+#define SYS_GET_CMDLINE   0x15
+#define SYS_HEAPINFO      0x16
+#define SYS_EXIT          0x18
+#define SYS_EXIT_EXTENDED 0x20
+#define SYS_ELAPSED       0x30
+#define SYS_TICKFREQ      0x31
+
+// Extended features availability flags.
+static bool exit_extended_available = false;
+static bool split_stdout_stderr = false;
+
+// Perform a semihosting call with the given call number and using the given
+// parameters block.
+int mp_semihosting_call(uint32_t num, void *arg);
+
+// Convert the given fopen(3) open mode string into the appropriate integer
+// value required by SYS_OPEN.  If the mode is invalid, it will return -1.
+static int mp_lookup_open_mode(const char *mode);
+
+// Computes the length of the given string.  If it gets passed a NULL pointer
+// it will return -1.
+static int mp_strlen(const char *string);
+
+// Check which extended features are advertised by the host system.
+static void mp_check_extended_features_availability(void);
+
+// Write the given string to the host system's debug console.
+static int mp_write_to_debug_console(const char *string, size_t length);
+
+// The host system's STDOUT file handle.
+int mp_semihosting_stdout = -1;
+
+// The host system's STDERR file handle.
+int mp_semihosting_stderr = -1;
+
+int mp_semihosting_open(const char *file_name, const char *file_mode) {
+    if (file_name == NULL || file_mode == NULL) {
+        return -1;
+    }
+
+    int file_name_length = mp_strlen(file_name);
+    if (file_name_length <= 0) {
+        return -1;
+    }
+    int file_open_mode = mp_lookup_open_mode(file_mode);
+    if (file_open_mode < 0) {
+        return -1;
+    }
+
+    uint32_t arguments[3] = { (uintptr_t)file_name, file_open_mode, file_name_length };
+    return mp_semihosting_call(SYS_OPEN, arguments);
+}
+
+int mp_semihosting_close(int handle) {
+    uint32_t arguments[] = { handle };
+    return mp_semihosting_call(SYS_CLOSE, arguments);
+}
+
+void mp_semihosting_writec(char character) {
+    uint32_t arguments[] = { character };
+    mp_semihosting_call(SYS_WRITEC, arguments);
+}
+
+void mp_semihosting_write0(const char *string) {
+    if (string == NULL) {
+        return;
+    }
+    uint32_t arguments[] = { (uintptr_t)string };
+    mp_semihosting_call(SYS_WRITE0, arguments);
+}
+
+int mp_semihosting_write(int handle, const void *data, size_t length) {
+    if (data == NULL) {
+        return length;
+    }
+    if (length == 0) {
+        return 0;
+    }
+
+    uint32_t arguments[] = { handle, (uintptr_t)data, length };
+    return mp_semihosting_call(SYS_WRITE, arguments);
+}
+
+int mp_semihosting_read(int handle, void *data, size_t length) {
+    if (data == NULL) {
+        return -1;
+    }
+    if (length == 0) {
+        return 0;
+    }
+
+    uint32_t arguments[] = { handle, (uintptr_t)data, length };
+    return mp_semihosting_call(SYS_READ, arguments);
+}
+
+inline int mp_semihosting_readc(void) {
+    return mp_semihosting_call(SYS_READC, NULL);
+}
+
+int mp_semihosting_iserror(int code) {
+    uint32_t arguments[] = { code };
+    return mp_semihosting_call(SYS_ISERROR, arguments);
+}
+
+int mp_semihosting_istty(int handle) {
+    uint32_t arguments[] = { handle };
+    return mp_semihosting_call(SYS_ISTTY, arguments);
+}
+
+int mp_semihosting_seek(int handle, uint32_t offset) {
+    uint32_t arguments[] = { handle, offset };
+    return mp_semihosting_call(SYS_SEEK, arguments);
+}
+
+int mp_semihosting_flen(int handle) {
+    uint32_t arguments[] = { handle };
+    return mp_semihosting_call(SYS_FLEN, arguments);
+}
+
+int mp_semihosting_tmpnam(uint8_t identifier, void *buffer, size_t buffer_length) {
+    if (buffer == NULL || buffer_length == 0) {
+        return -1;
+    }
+
+    uint32_t arguments[] = { (uintptr_t)buffer, identifier, buffer_length };
+    return mp_semihosting_call(SYS_TMPNAM, arguments);
+}
+
+int mp_semihosting_remove(const char *file_name) {
+    if (file_name == NULL) {
+        return -1;
+    }
+
+    int file_name_length = mp_strlen(file_name);
+    if (file_name_length <= 0) {
+        return -1;
+    }
+
+    uint32_t arguments[] = { (uintptr_t)file_name, file_name_length };
+    return mp_semihosting_call(SYS_REMOVE, arguments);
+}
+
+int mp_semihosting_rename(const char *old_name, const char *new_name) {
+    if (old_name == NULL || new_name == NULL) {
+        return -1;
+    }
+
+    int old_name_length = mp_strlen(old_name);
+    if (old_name_length <= 0) {
+        return -1;
+    }
+
+    int new_name_length = mp_strlen(new_name);
+    if (new_name_length <= 0) {
+        return -1;
+    }
+
+    uint32_t arguments[] = {
+        (uintptr_t)old_name, old_name_length, (uintptr_t)new_name, new_name_length
+    };
+    return mp_semihosting_call(SYS_RENAME, arguments);
+}
+
+inline int mp_semihosting_clock(void) {
+    return mp_semihosting_call(SYS_CLOCK, NULL);
+}
+
+inline int mp_semihosting_time(void) {
+    return mp_semihosting_call(SYS_TIME, NULL);
+}
+
+int mp_semihosting_system(const char *command) {
+    if (command == NULL) {
+        return -1;
+    }
+
+    int command_length = mp_strlen(command);
+    if (command_length <= 0) {
+        return -1;
+    }
+
+    uint32_t arguments[] = { (uintptr_t)command, command_length };
+    return mp_semihosting_call(SYS_SYSTEM, arguments);
+}
+
+inline int mp_semihosting_errno(void) {
+    return mp_semihosting_call(SYS_ERRNO, NULL);
+}
+
+int mp_semihosting_get_cmdline(void *buffer, size_t buffer_length) {
+    if (buffer == NULL || buffer_length < CMDLINE_MIN_BUFFER_SIZE) {
+        return -1;
+    }
+
+    uint32_t arguments[] = { (uintptr_t)buffer, buffer_length };
+    return mp_semihosting_call(SYS_GET_CMDLINE, arguments);
+}
+
+void mp_semihosting_heapinfo(mp_semihosting_heap_info_t *block) {
+    if (block == NULL) {
+        return;
+    }
+
+    uint32_t arguments[] = { (uintptr_t)block };
+    mp_semihosting_call(SYS_HEAPINFO, arguments);
+}
+
+void mp_semihosting_exit(uint32_t code, uint32_t subcode) {
+    uint32_t arguments[] = { code, subcode };
+    mp_semihosting_call(SYS_EXIT, arguments);
+    for (;;) {}
+}
+
+void mp_semihosting_exit_extended(uint32_t code, uint32_t subcode) {
+    uint32_t arguments[] = { code, subcode };
+    mp_semihosting_call(SYS_EXIT_EXTENDED, arguments);
+    for (;;) {}
+}
+
+int mp_semihosting_elapsed(mp_semihosting_elapsed_ticks_t *ticks) {
+    if (ticks == NULL) {
+        return -1;
+    }
+
+    uint32_t arguments[] = { (uintptr_t)ticks };
+    return mp_semihosting_call(SYS_ELAPSED, arguments);
+}
+
+inline int mp_semihosting_tickfreq(void) {
+    return mp_semihosting_call(SYS_TICKFREQ, NULL);
+}
+
+void mp_semihosting_init() {
+    mp_check_extended_features_availability();
+    mp_semihosting_stdout = mp_semihosting_open(":tt", "w");
+    if (split_stdout_stderr) {
+        mp_semihosting_stderr = mp_semihosting_open(":tt", "a");
+    } else {
+        mp_semihosting_stderr = mp_semihosting_stdout;
+    }
+}
+
+void mp_check_extended_features_availability(void) {
+    int features_handle = mp_semihosting_open(":semihosting-features", "r");
+    if (features_handle < 0) {
+        return;
+    }
+
+    uint8_t magic_buffer[MAGIC_SIZE];
+    if (mp_semihosting_flen(features_handle) < sizeof(magic_buffer)) {
+        mp_semihosting_close(features_handle);
+        return;
+    }
+
+    if (mp_semihosting_read(features_handle, magic_buffer, sizeof(magic_buffer)) != 0) {
+        mp_semihosting_close(features_handle);
+        return;
+    }
+
+    if (magic_buffer[0] != MAGIC_0 ||
+        magic_buffer[1] != MAGIC_1 ||
+        magic_buffer[2] != MAGIC_2 ||
+        magic_buffer[3] != MAGIC_3) {
+        mp_semihosting_close(features_handle);
+        return;
+    }
+
+    uint8_t features_byte = 0;
+    if (mp_semihosting_read(features_handle, &features_byte, sizeof(features_byte)) != 0) {
+        mp_semihosting_close(features_handle);
+        return;
+    }
+
+    mp_semihosting_close(features_handle);
+
+    exit_extended_available = (features_byte & 0x01) != 0;
+    split_stdout_stderr = (features_byte & 0x02) != 0;
+}
+
+int mp_strlen(const char *string) {
+    int length = 0;
+    while (*string++ != 0) {
+        length += 1;
+    }
+    return length;
+}
+
+int mp_lookup_open_mode(const char *mode) {
+    if (mode == NULL) {
+        return -1;
+    }
+
+    int mode_found;
+
+    switch (mode[0]) {
+        case 'r':
+            mode_found = 0x00;
+            break;
+        case 'w':
+            mode_found = 0x04;
+            break;
+        case 'a':
+            mode_found = 0x08;
+            break;
+        default:
+            return -1;
+    }
+
+    switch (mode[1]) {
+        case 'b':
+            mode_found |= 0x01;
+            break;
+        case '+':
+            mode_found |= 0x02;
+            break;
+        case '\0':
+            return mode_found;
+        default:
+            return -1;
+    }
+
+    switch (mode[2]) {
+        case 'b':
+            if (mode_found & 0x01) {
+                // 'b' was already seen.
+                return -1;
+            }
+            mode_found |= 1;
+            break;
+        case '+':
+            if (mode_found & 0x02) {
+                // '+' was already seen.
+                return -1;
+            }
+            mode_found |= 2;
+            break;
+        case '\0':
+            return mode_found;
+        default:
+            return -1;
+    }
+
+    return mode[3] == '\0' ? mode_found : -1;
+}
+
+int mp_semihosting_call(uint32_t num, void *arg) {
+    register uint32_t call_number_register __asm__ ("x10") = num;
+    register void *arguments_register __asm__ ("x11") = arg;
+
+    __asm volatile (
+        ".option push            \n" // Transient options
+        ".option norvc           \n" // Do not emit compressed instructions
+        ".align 4                \n" // 16 bytes alignment
+        "slli   zero, zero, 0x1F \n" // Entry NOP
+        "ebreak                  \n" // Give control to the debugger
+        "srai   zero, zero, 7    \n" // Semihosting call
+        ".option pop             \n" // Restore previous options set
+        : "+r" (call_number_register)
+        : "r" (arguments_register)
+        : "memory"
+        );
+
+    return call_number_register;
+}
+
+inline int mp_semihosting_rx_char() {
+    return mp_semihosting_call(SYS_READC, NULL);
+}
+
+int mp_write_to_debug_console(const char *string, size_t length) {
+    if (length == 0) {
+        return 0;
+    }
+
+    if (length == 1) {
+        mp_semihosting_writec(*string);
+        return 0;
+    }
+
+    return mp_semihosting_write(mp_semihosting_stdout, string, length);
+}
+
+void mp_semihosting_terminate(uint32_t code, uint32_t subcode) {
+    if (exit_extended_available) {
+        mp_semihosting_exit_extended(code, subcode);
+    } else {
+        mp_semihosting_exit(code, subcode);
+    }
+}
+
+int mp_semihosting_tx_strn(const char *string, size_t length) {
+    if (string == NULL) {
+        return -1;
+    }
+
+    return mp_write_to_debug_console(string, length);
+}
+
+int mp_semihosting_tx_strn_cooked(const char *string, size_t length) {
+    if (string == NULL) {
+        return -1;
+    }
+
+    if (length == 0) {
+        return 0;
+    }
+
+    size_t current_offset = 0;
+    for (size_t index = 0; index < length; index++) {
+        if (string[index] != '\n') {
+            continue;
+        }
+
+        mp_write_to_debug_console(string + current_offset, index - current_offset);
+        mp_semihosting_writec('\r');
+        current_offset = index;
+    }
+
+    return mp_write_to_debug_console(string + current_offset, length - current_offset);
+}
diff --git a/shared/runtime/semihosting_rv32.h b/shared/runtime/semihosting_rv32.h
new file mode 100644
index 00000000000..7cabd1692c1
--- /dev/null
+++ b/shared/runtime/semihosting_rv32.h
@@ -0,0 +1,247 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2024 Alessandro Gatti
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef MICROPY_INCLUDED_SHARED_RUNTIME_SEMIHOSTING_RV32_H
+#define MICROPY_INCLUDED_SHARED_RUNTIME_SEMIHOSTING_RV32_H
+
+/*
+ * To integrate semihosting, make sure to call mp_semihosting_init() first.
+ * Then, if the host system's STDOUT should be used instead of a UART, replace
+ * mp_hal_stdin_rx_chr and similar calls in mphalport.c with the semihosting
+ * equivalents.
+ *
+ * At runtime, make sure that the debugger is attached and semihosting is
+ * enabled on its end.  The terminal must be configured in raw mode with local
+ * echo disabled, on Linux this can be done with "stty raw -echo" on the
+ * command line.
+ */
+
+/*
+ * This follows the RISC-V Semihosting specification version 0.3.
+ *
+ * That document can be downloaded from
+ * https://github.com/riscv-non-isa/riscv-semihosting/releases/
+ *
+ * Version 0.3 claims that the current RISC-V Semihosting implementation
+ * should follow Arm's, and more precisely the "Semihosting for AArch32
+ * and AArch64" document, revision 2023Q3.
+ */
+
+#include 
+#include 
+#include 
+
+// A container for heap and stack pointers as returned by SYS_HEAPINFO.
+typedef struct {
+    void *heap_base;
+    void *heap_limit;
+    void *stack_base;
+    void *stack_limit;
+} mp_semihosting_heap_info_t;
+
+// A 64-bits value indicating how many ticks were counted since the target
+// image's execution started.
+typedef struct {
+    uint32_t low;
+    uint32_t high;
+} mp_semihosting_elapsed_ticks_t;
+
+// The host system's STDOUT file handle.
+extern int mp_semihosting_stdout;
+
+// The host system's STDERR file handle.  If the host system does not support
+// explicit STDOUT and STDERR handles, this handle will be aliased to STDOUT
+// instead.
+extern int mp_semihosting_stderr;
+
+/*
+ * Even though exit codes from 0x20000 to 0x20007 are part of the original Arm
+ * specification document, they are omitted due to them being tied to hardware
+ * events.  Whilst some of them may still have a meaning on the RISC-V
+ * platform, it is not yet clear which ones are available and which ones are
+ * not.  Thus, only "soft" error codes are provided here although the SYS_EXIT
+ * and SYS_EXIT_EXTENDED semihosting calls accept any 32-bits integer as an
+ * exit code.
+ */
+
+enum {
+    MP_SEMIHOSTING_EXIT_BREAKPOINT = 0x20020,
+    MP_SEMIHOSTING_EXIT_WATCHPOINT,
+    MP_SEMIHOSTING_EXIT_STEP_COMPLETE,
+    MP_SEMIHOSTING_EXIT_RUNTIME_ERROR_UNKNOWN,
+    MP_SEMIHOSTING_EXIT_INTERNAL_ERROR,
+    MP_SEMIHOSTING_EXIT_USER_INTERRUPTION,
+    MP_SEMIHOSTING_EXIT_APPLICATION_EXIT,
+    MP_SEMIHOSTING_EXIT_STACK_OVERFLOW,
+    MP_SEMIHOSTING_EXIT_DIVISION_BY_ZERO,
+    MP_SEMIHOSTING_EXIT_OS_SPECIFIC
+};
+
+// Initialises semihosting support.
+void mp_semihosting_init();
+
+// Read a character from the host system's STDIN stream.
+int mp_semihosting_rx_char();
+
+// Write the given string to the host system's STDOUT stream.
+int mp_semihosting_tx_strn(const char *string, size_t length);
+
+// Write the given string to the host system's STDOUT stream, writing a CR byte
+// before each LF byte to be written.
+int mp_semihosting_tx_strn_cooked(const char *string, size_t length);
+
+// Terminates execution with the given code and an optional subcode.  This
+// will choose the appropriate semihosting call (either SYS_EXIT or
+// SYS_EXIT_EXTENDED) depending on the host system's reported capabilities.
+noreturn void mp_semihosting_terminate(uint32_t code, uint32_t subcode);
+
+// Direct semihosting calls access.
+
+// Open a file on the host system with the given name and file mode.
+// The file mode follows fopen(3)'s syntax.  The function will return -1 if it
+// failed to open the required file, or a file handle number if the operation
+// succeeded.  To see why the operation failed, call mp_semihosting_errno.
+int mp_semihosting_open(const char *file_name, const char *file_mode);
+
+// Close a file previously opened with mp_semihosting_open.  If the file cannot
+// be closed, the function will return -1, otherwise it will return 0.  To see
+// why the operation failed, call mp_semihosting_errno.
+int mp_semihosting_close(int handle);
+
+// Write the given character to the host system's STDOUT file handle.
+void mp_semihosting_writec(char character);
+
+// Write the given NULL-terminated string to the host system's STDOUT file
+// handle.
+void mp_semihosting_write0(const char *string);
+
+// Write the given buffer to the given host system file handle.  The function
+// will return how many characters were left to be written (0 if the operation
+// wrote the whole buffer), or -1 if the input buffer pointer is NULL.
+int mp_semihosting_write(int handle, const void *data, size_t length);
+
+// Read from the given host system file handle into the given buffer. The
+// function will return how many characters were left to be read (0 if the
+// operation read whole buffer), or -1 if the input buffer pointer is NULL.
+int mp_semihosting_read(int handle, void *data, size_t length);
+
+// Read a single character from the host system's STDIN file handle.
+int mp_semihosting_readc(void);
+
+// Check whether the given result code represents an error.  The function will
+// return a non-zero value if the code is indeed an error, or zero otherwise.
+int mp_semihosting_iserror(int code);
+
+// Check whether the given host system file handle is mapped to an interactive
+// device.  The function will return 1 if the handle is mapped to an
+// interactive device, 0 if it is mapped to a regular file, and anything else
+// if an error occurred.
+int mp_semihosting_istty(int handle);
+
+// Move the file pointer on the given host system's file handle to the given
+// absolute offset (in bytes).  The function will return 0 if the file pointer
+// was moved to the requested position, or a negative value if it was not
+// possible to do so.  To see why the operation failed, call
+// mp_semihosting_errno.
+int mp_semihosting_seek(int handle, uint32_t offset);
+
+// Get the length (in bytes) of the host system's file mapped to the given
+// handle.  The function will return a negative value if an error occurred, or
+// the requested file's length (in bytes).
+int mp_semihosting_flen(int handle);
+
+// Create a temporary file on the host system.  The function requires a file
+// identifier between 0 and 255 (inclusive) that will be bound to the requested
+// temporary file.  Subsequent calls to mp_semihosting_tmpnam with the same
+// identifier will always return the same host system file name.  On success,
+// the function will fill the given buffer with the host system's file name,
+// and will return 0.  If the buffer pointer is NULL, the buffer name area is
+// too small, or the operation failed on the host system's end, the function
+// will return -1 instead.  Make sure that the buffer is big enough to contain
+// a host system's full path name.
+int mp_semihosting_tmpnam(uint8_t identifier, void *buffer, size_t buffer_length);
+
+// Delete a file on the host system's matching the given file name.  The
+// function will return 0 if the deletion operation succeeded, or a host system
+// dependent error code instead.
+int mp_semihosting_remove(const char *file_name);
+
+// Rename a file on the host system's name matching the given file name to the
+// new chosen name.  The function will return 0 if the rename operation
+// succeeded, or a host system dependent error code instead.
+int mp_semihosting_rename(const char *old_name, const char *new_name);
+
+// Get how many hundredths of a second passed since execution started.  If an
+// error occurred whilst retrieving clock value, the function will return -1.
+int mp_semihosting_clock(void);
+
+// Get the host system's clock in seconds since midnight of January 1st, 1970
+// at UTC.
+int mp_semihosting_time(void);
+
+// Execute the given command on the host system.  The function will return the
+// command's result code retrieved on the host system.
+int mp_semihosting_system(const char *command);
+
+// Get the last operation's status code.  The function will return the host
+// system's errno variable contents, and can be used to see the exact result
+// code for failed I/O operations.
+int mp_semihosting_errno(void);
+
+// Get the host system's command line that started execution of the target
+// image.  The function will fill the given buffer with the command line
+// arguments passed to the target executable.  The function will return 0 on
+// success, or -1 if it failed.  Make sure that the buffer can contain at
+// least 80 bytes, as it is the minimum supported size defined by the
+// specifications document.
+int mp_semihosting_get_cmdline(void *buffer, size_t buffer_length);
+
+// Fill the given heap info structure with the system's stack and heap
+// start/end addresses.
+void mp_semihosting_heapinfo(mp_semihosting_heap_info_t *block);
+
+// Terminate the execution with the given reason code and optional subcode.
+// This should be preferred over mp_semihosting_exit_extended if the host
+// system does not support the SYS_EXIT_EXTENDED semihosting call.  In doubt
+// use mp_semihosting_terminate instead.
+noreturn void mp_semihosting_exit(uint32_t code, uint32_t subcode);
+
+// Terminate the execution with the given reason code and optional subcode.
+// This should be preferred over mp_semihosting_exit if the host system
+// supports this semihosting call.  In doubt use mp_semihosting_terminate
+// instead.
+noreturn void mp_semihosting_exit_extended(uint32_t code, uint32_t subcode);
+
+// Fill the given structure with how many ticks were counted since execution
+// started.  On success, the function will return 0, or -1 if it was not
+// possible to compute the ticks count.
+int mp_semihosting_elapsed(mp_semihosting_elapsed_ticks_t *ticks);
+
+// Get the system's tick frequency.  If this value is not known, the function
+// will return -1 instead.
+int mp_semihosting_tickfreq(void);
+
+#endif // MICROPY_INCLUDED_SHARED_SEMIHOSTING_RUNTIME_RV32_H
diff --git a/shared/runtime/softtimer.c b/shared/runtime/softtimer.c
index 267610b2694..ff6d3fdd584 100644
--- a/shared/runtime/softtimer.c
+++ b/shared/runtime/softtimer.c
@@ -25,91 +25,101 @@
  */
 
 #include 
+#include 
 #include "py/gc.h"
 #include "py/mphal.h"
 #include "py/runtime.h"
+#include "shared/runtime/mpirq.h"
 #include "softtimer.h"
 
-extern __IO uint32_t MICROPY_SOFT_TIMER_TICKS_MS;
+#ifdef MICROPY_SOFT_TIMER_TICKS_MS
 
-volatile uint32_t soft_timer_next;
-
-// Pointer to the pairheap of soft timer objects.
-// This may contain bss/data pointers as well as GC-heap pointers,
-// and is explicitly GC traced by soft_timer_gc_mark_all().
-STATIC soft_timer_entry_t *soft_timer_heap;
+extern volatile uint32_t MICROPY_SOFT_TIMER_TICKS_MS;
 
-static inline int32_t ticks_diff(uint32_t t1, uint32_t t0) {
-    // t1 is after t0 (i.e. positive result) if there exists a uint32_t X <= INT_MAX
-    // such that t0 + X = t1. Otherwise t1 is interpreted to be earlier than
-    // t0 (negative result).
-    return t1 - t0;
-}
+volatile uint32_t soft_timer_next;
 
-STATIC int soft_timer_lt(mp_pairheap_t *n1, mp_pairheap_t *n2) {
-    soft_timer_entry_t *e1 = (soft_timer_entry_t *)n1;
-    soft_timer_entry_t *e2 = (soft_timer_entry_t *)n2;
-    return ticks_diff(e1->expiry_ms, e2->expiry_ms) < 0;
+static inline uint32_t soft_timer_get_ms(void) {
+    return MICROPY_SOFT_TIMER_TICKS_MS;
 }
 
-STATIC void soft_timer_schedule_systick(uint32_t ticks_ms) {
-    uint32_t irq_state = disable_irq();
+static void soft_timer_schedule_at_ms(uint32_t ticks_ms) {
+    uint32_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION();
     uint32_t uw_tick = MICROPY_SOFT_TIMER_TICKS_MS;
-    if (ticks_diff(ticks_ms, uw_tick) <= 0) {
+    if (soft_timer_ticks_diff(ticks_ms, uw_tick) <= 0) {
         soft_timer_next = uw_tick + 1;
     } else {
         soft_timer_next = ticks_ms;
     }
-    enable_irq(irq_state);
+    MICROPY_END_ATOMIC_SECTION(atomic_state);
+}
+
+#endif
+
+// Pointer to the pairheap of soft timer objects.
+// This may contain bss/data pointers as well as GC-heap pointers,
+// and is explicitly GC traced by soft_timer_gc_mark_all().
+static soft_timer_entry_t *soft_timer_heap;
+
+// Safe pairheap accessor that avoids the undefined behaviour of
+// &e->pairheap when e is NULL. Note this is valid only because
+// pairheap is the first member (guaranteed by MP_STATIC_ASSERT).
+#define SOFT_TIMER_HEAP(e)  ((mp_pairheap_t *)(e))
+
+static int soft_timer_lt(mp_pairheap_t *n1, mp_pairheap_t *n2) {
+    soft_timer_entry_t *e1 = (soft_timer_entry_t *)n1;
+    soft_timer_entry_t *e2 = (soft_timer_entry_t *)n2;
+    return soft_timer_ticks_diff(e1->expiry_ms, e2->expiry_ms) < 0;
 }
 
 void soft_timer_deinit(void) {
     // Pop off all the nodes which are allocated on the GC-heap.
-    uint32_t irq_state = raise_irq_pri(IRQ_PRI_PENDSV);
+    MICROPY_PY_PENDSV_ENTER;
     soft_timer_entry_t *heap_from = soft_timer_heap;
     soft_timer_entry_t *heap_to = (soft_timer_entry_t *)mp_pairheap_new(soft_timer_lt);
     while (heap_from != NULL) {
         soft_timer_entry_t *entry = (soft_timer_entry_t *)mp_pairheap_peek(soft_timer_lt, &heap_from->pairheap);
         heap_from = (soft_timer_entry_t *)mp_pairheap_pop(soft_timer_lt, &heap_from->pairheap);
         if (!(entry->flags & SOFT_TIMER_FLAG_GC_ALLOCATED)) {
-            heap_to = (soft_timer_entry_t *)mp_pairheap_push(soft_timer_lt, &heap_to->pairheap, &entry->pairheap);
+            heap_to = (soft_timer_entry_t *)mp_pairheap_push(soft_timer_lt, SOFT_TIMER_HEAP(heap_to), &entry->pairheap);
         }
     }
     soft_timer_heap = heap_to;
-    restore_irq_pri(irq_state);
+    MICROPY_PY_PENDSV_EXIT;
 }
 
 // Must be executed at IRQ_PRI_PENDSV
 void soft_timer_handler(void) {
-    uint32_t ticks_ms = MICROPY_SOFT_TIMER_TICKS_MS;
+    uint32_t ticks_ms = soft_timer_get_ms();
     soft_timer_entry_t *heap = soft_timer_heap;
-    while (heap != NULL && ticks_diff(heap->expiry_ms, ticks_ms) <= 0) {
+    while (heap != NULL && soft_timer_ticks_diff(heap->expiry_ms, ticks_ms) <= 0) {
         soft_timer_entry_t *entry = heap;
         heap = (soft_timer_entry_t *)mp_pairheap_pop(soft_timer_lt, &heap->pairheap);
         if (entry->flags & SOFT_TIMER_FLAG_PY_CALLBACK) {
-            mp_sched_schedule(entry->py_callback, MP_OBJ_FROM_PTR(entry));
-        } else {
+            if (mp_irq_dispatch(entry->py_callback, MP_OBJ_FROM_PTR(entry),
+                entry->flags & SOFT_TIMER_FLAG_HARD_CALLBACK)) {
+                // Uncaught exception; disable the callback so it doesn't run again.
+                entry->mode = SOFT_TIMER_MODE_ONE_SHOT;
+            }
+        } else if (entry->c_callback) {
             entry->c_callback(entry);
         }
         if (entry->mode == SOFT_TIMER_MODE_PERIODIC) {
             entry->expiry_ms += entry->delta_ms;
-            heap = (soft_timer_entry_t *)mp_pairheap_push(soft_timer_lt, &heap->pairheap, &entry->pairheap);
+            heap = (soft_timer_entry_t *)mp_pairheap_push(soft_timer_lt, SOFT_TIMER_HEAP(heap), &entry->pairheap);
         }
     }
     soft_timer_heap = heap;
-    if (heap == NULL) {
-        // No more timers left, set largest delay possible
-        soft_timer_next = MICROPY_SOFT_TIMER_TICKS_MS;
-    } else {
-        // Set soft_timer_next so SysTick calls us back at the correct time
-        soft_timer_schedule_systick(heap->expiry_ms);
+
+    // Schedule the port's timer to call us back at the correct time.
+    if (heap != NULL) {
+        soft_timer_schedule_at_ms(heap->expiry_ms);
     }
 }
 
 void soft_timer_gc_mark_all(void) {
     // Mark all soft timer nodes that are allocated on the GC-heap.
     // To avoid deep C recursion, pop and recreate the pairheap as nodes are marked.
-    uint32_t irq_state = raise_irq_pri(IRQ_PRI_PENDSV);
+    MICROPY_PY_PENDSV_ENTER;
     soft_timer_entry_t *heap_from = soft_timer_heap;
     soft_timer_entry_t *heap_to = (soft_timer_entry_t *)mp_pairheap_new(soft_timer_lt);
     while (heap_from != NULL) {
@@ -118,13 +128,17 @@ void soft_timer_gc_mark_all(void) {
         if (entry->flags & SOFT_TIMER_FLAG_GC_ALLOCATED) {
             gc_collect_root((void **)&entry, 1);
         }
-        heap_to = (soft_timer_entry_t *)mp_pairheap_push(soft_timer_lt, &heap_to->pairheap, &entry->pairheap);
+        heap_to = (soft_timer_entry_t *)mp_pairheap_push(soft_timer_lt, SOFT_TIMER_HEAP(heap_to), &entry->pairheap);
     }
     soft_timer_heap = heap_to;
-    restore_irq_pri(irq_state);
+    MICROPY_PY_PENDSV_EXIT;
 }
 
 void soft_timer_static_init(soft_timer_entry_t *entry, uint16_t mode, uint32_t delta_ms, void (*cb)(soft_timer_entry_t *)) {
+    // Ensure that pairheap is always the first member of soft_timer_entry_t
+    // to allow casting between pointers of soft_timer_entry_t and mp_pairheap_t.
+    MP_STATIC_ASSERT(offsetof(soft_timer_entry_t, pairheap) == 0);
+
     mp_pairheap_init_node(soft_timer_lt, &entry->pairheap);
     entry->flags = 0;
     entry->mode = mode;
@@ -134,18 +148,18 @@ void soft_timer_static_init(soft_timer_entry_t *entry, uint16_t mode, uint32_t d
 
 void soft_timer_insert(soft_timer_entry_t *entry, uint32_t initial_delta_ms) {
     mp_pairheap_init_node(soft_timer_lt, &entry->pairheap);
-    entry->expiry_ms = MICROPY_SOFT_TIMER_TICKS_MS + initial_delta_ms;
-    uint32_t irq_state = raise_irq_pri(IRQ_PRI_PENDSV);
-    soft_timer_heap = (soft_timer_entry_t *)mp_pairheap_push(soft_timer_lt, &soft_timer_heap->pairheap, &entry->pairheap);
+    entry->expiry_ms = soft_timer_get_ms() + initial_delta_ms;
+    MICROPY_PY_PENDSV_ENTER;
+    soft_timer_heap = (soft_timer_entry_t *)mp_pairheap_push(soft_timer_lt, SOFT_TIMER_HEAP(soft_timer_heap), &entry->pairheap);
     if (entry == soft_timer_heap) {
-        // This new timer became the earliest one so set soft_timer_next
-        soft_timer_schedule_systick(entry->expiry_ms);
+        // This new timer became the earliest one so schedule a callback.
+        soft_timer_schedule_at_ms(entry->expiry_ms);
     }
-    restore_irq_pri(irq_state);
+    MICROPY_PY_PENDSV_EXIT;
 }
 
 void soft_timer_remove(soft_timer_entry_t *entry) {
-    uint32_t irq_state = raise_irq_pri(IRQ_PRI_PENDSV);
-    soft_timer_heap = (soft_timer_entry_t *)mp_pairheap_delete(soft_timer_lt, &soft_timer_heap->pairheap, &entry->pairheap);
-    restore_irq_pri(irq_state);
+    MICROPY_PY_PENDSV_ENTER;
+    soft_timer_heap = (soft_timer_entry_t *)mp_pairheap_delete(soft_timer_lt, SOFT_TIMER_HEAP(soft_timer_heap), &entry->pairheap);
+    MICROPY_PY_PENDSV_EXIT;
 }
diff --git a/shared/runtime/softtimer.h b/shared/runtime/softtimer.h
index 8603ea13f07..464d2067ede 100644
--- a/shared/runtime/softtimer.h
+++ b/shared/runtime/softtimer.h
@@ -30,6 +30,7 @@
 
 #define SOFT_TIMER_FLAG_PY_CALLBACK (1)
 #define SOFT_TIMER_FLAG_GC_ALLOCATED (2)
+#define SOFT_TIMER_FLAG_HARD_CALLBACK (4)
 
 #define SOFT_TIMER_MODE_ONE_SHOT (1)
 #define SOFT_TIMER_MODE_PERIODIC (2)
@@ -48,6 +49,13 @@ typedef struct _soft_timer_entry_t {
 
 extern volatile uint32_t soft_timer_next;
 
+static inline int32_t soft_timer_ticks_diff(uint32_t t1, uint32_t t0) {
+    // t1 is after t0 (i.e. positive result) if there exists a uint32_t X <= INT_MAX
+    // such that t0 + X = t1. Otherwise t1 is interpreted to be earlier than
+    // t0 (negative result).
+    return t1 - t0;
+}
+
 void soft_timer_deinit(void);
 void soft_timer_handler(void);
 void soft_timer_gc_mark_all(void);
@@ -63,4 +71,20 @@ static inline void soft_timer_reinsert(soft_timer_entry_t *entry, uint32_t initi
     soft_timer_insert(entry, initial_delta_ms);
 }
 
+#if !defined(MICROPY_SOFT_TIMER_TICKS_MS)
+// IF MICROPY_SOFT_TIMER_TICKS_MS is not defined then the port must provide the
+// following timer functions:
+// - soft_timer_get_ms() must return a 32-bit millisecond counter that wraps around.
+// - soft_timer_schedule_at_ms(ticks_ms) must schedule a callback of soft_timer_handler()
+//   when the above millisecond counter reaches the given ticks_ms value.  If ticks_ms
+//   is behind the current counter (using int32_t arithmetic) then the callback should
+//   be scheduled immediately.  The callback of soft_timer_handler() should be made at
+//   pend-SV IRQ level, or equivalent.
+uint32_t soft_timer_get_ms(void);
+void soft_timer_schedule_at_ms(uint32_t ticks_ms);
+
+// Optional port-specific initialisation function (provided and called by the port if needed).
+void soft_timer_init(void);
+#endif
+
 #endif // MICROPY_INCLUDED_SHARED_RUNTIME_SOFTTIMER_H
diff --git a/shared/runtime/sys_stdio_mphal.c b/shared/runtime/sys_stdio_mphal.c
index ab5bdd34e73..b82725bfa7b 100644
--- a/shared/runtime/sys_stdio_mphal.c
+++ b/shared/runtime/sys_stdio_mphal.c
@@ -49,15 +49,15 @@ typedef struct _sys_stdio_obj_t {
 } sys_stdio_obj_t;
 
 #if MICROPY_PY_SYS_STDIO_BUFFER
-STATIC const sys_stdio_obj_t stdio_buffer_obj;
+static const sys_stdio_obj_t stdio_buffer_obj;
 #endif
 
-STATIC void stdio_obj_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+static void stdio_obj_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     sys_stdio_obj_t *self = MP_OBJ_TO_PTR(self_in);
-    mp_printf(print, "", self->fd);
+    mp_printf(print, "", mp_obj_get_type_str(self_in), self->fd);
 }
 
-STATIC mp_uint_t stdio_read(mp_obj_t self_in, void *buf, mp_uint_t size, int *errcode) {
+static mp_uint_t stdio_read(mp_obj_t self_in, void *buf, mp_uint_t size, int *errcode) {
     sys_stdio_obj_t *self = MP_OBJ_TO_PTR(self_in);
     if (self->fd == STDIO_FD_IN) {
         for (uint i = 0; i < size; i++) {
@@ -74,7 +74,7 @@ STATIC mp_uint_t stdio_read(mp_obj_t self_in, void *buf, mp_uint_t size, int *er
     }
 }
 
-STATIC mp_uint_t stdio_write(mp_obj_t self_in, const void *buf, mp_uint_t size, int *errcode) {
+static mp_uint_t stdio_write(mp_obj_t self_in, const void *buf, mp_uint_t size, int *errcode) {
     sys_stdio_obj_t *self = MP_OBJ_TO_PTR(self_in);
     if (self->fd == STDIO_FD_OUT || self->fd == STDIO_FD_ERR) {
         mp_hal_stdout_tx_strn_cooked(buf, size);
@@ -85,22 +85,19 @@ STATIC mp_uint_t stdio_write(mp_obj_t self_in, const void *buf, mp_uint_t size,
     }
 }
 
-STATIC mp_uint_t stdio_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) {
+static mp_uint_t stdio_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) {
     (void)self_in;
     if (request == MP_STREAM_POLL) {
         return mp_hal_stdio_poll(arg);
+    } else if (request == MP_STREAM_CLOSE) {
+        return 0;
     } else {
         *errcode = MP_EINVAL;
         return MP_STREAM_ERROR;
     }
 }
 
-STATIC mp_obj_t stdio_obj___exit__(size_t n_args, const mp_obj_t *args) {
-    return mp_const_none;
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(stdio_obj___exit___obj, 4, 4, stdio_obj___exit__);
-
-STATIC const mp_rom_map_elem_t stdio_locals_dict_table[] = {
+static const mp_rom_map_elem_t stdio_locals_dict_table[] = {
     #if MICROPY_PY_SYS_STDIO_BUFFER
     { MP_ROM_QSTR(MP_QSTR_buffer), MP_ROM_PTR(&stdio_buffer_obj) },
     #endif
@@ -111,12 +108,12 @@ STATIC const mp_rom_map_elem_t stdio_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&mp_stream_write_obj) },
     { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&mp_identity_obj) },
     { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&mp_identity_obj) },
-    { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&stdio_obj___exit___obj) },
+    { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&mp_stream___exit___obj) },
 };
 
-STATIC MP_DEFINE_CONST_DICT(stdio_locals_dict, stdio_locals_dict_table);
+static MP_DEFINE_CONST_DICT(stdio_locals_dict, stdio_locals_dict_table);
 
-STATIC const mp_stream_p_t stdio_obj_stream_p = {
+static const mp_stream_p_t stdio_obj_stream_p = {
     .read = stdio_read,
     .write = stdio_write,
     .ioctl = stdio_ioctl,
@@ -125,7 +122,7 @@ STATIC const mp_stream_p_t stdio_obj_stream_p = {
 
 MP_DEFINE_CONST_OBJ_TYPE(
     stdio_obj_type,
-    MP_QSTR_FileIO,
+    MP_QSTR_TextIOWrapper,
     MP_TYPE_FLAG_ITER_IS_STREAM,
     print, stdio_obj_print,
     protocol, &stdio_obj_stream_p,
@@ -137,26 +134,25 @@ const sys_stdio_obj_t mp_sys_stdout_obj = {{&stdio_obj_type}, .fd = STDIO_FD_OUT
 const sys_stdio_obj_t mp_sys_stderr_obj = {{&stdio_obj_type}, .fd = STDIO_FD_ERR};
 
 #if MICROPY_PY_SYS_STDIO_BUFFER
-STATIC mp_uint_t stdio_buffer_read(mp_obj_t self_in, void *buf, mp_uint_t size, int *errcode) {
+static mp_uint_t stdio_buffer_read(mp_obj_t self_in, void *buf, mp_uint_t size, int *errcode) {
     for (uint i = 0; i < size; i++) {
         ((byte *)buf)[i] = mp_hal_stdin_rx_chr();
     }
     return size;
 }
 
-STATIC mp_uint_t stdio_buffer_write(mp_obj_t self_in, const void *buf, mp_uint_t size, int *errcode) {
-    mp_hal_stdout_tx_strn(buf, size);
-    return size;
+static mp_uint_t stdio_buffer_write(mp_obj_t self_in, const void *buf, mp_uint_t size, int *errcode) {
+    return mp_hal_stdout_tx_strn(buf, size);
 }
 
-STATIC const mp_stream_p_t stdio_buffer_obj_stream_p = {
+static const mp_stream_p_t stdio_buffer_obj_stream_p = {
     .read = stdio_buffer_read,
     .write = stdio_buffer_write,
     .ioctl = stdio_ioctl,
     .is_text = false,
 };
 
-STATIC MP_DEFINE_CONST_OBJ_TYPE(
+static MP_DEFINE_CONST_OBJ_TYPE(
     stdio_buffer_obj_type,
     MP_QSTR_FileIO,
     MP_TYPE_FLAG_ITER_IS_STREAM,
@@ -165,5 +161,5 @@ STATIC MP_DEFINE_CONST_OBJ_TYPE(
     locals_dict, &stdio_locals_dict
     );
 
-STATIC const sys_stdio_obj_t stdio_buffer_obj = {{&stdio_buffer_obj_type}, .fd = 0}; // fd unused
+static const sys_stdio_obj_t stdio_buffer_obj = {{&stdio_buffer_obj_type}, .fd = 0}; // fd unused
 #endif
diff --git a/shared/timeutils/timeutils.c b/shared/timeutils/timeutils.c
index 6bf3eca84a8..0c6916e06dd 100644
--- a/shared/timeutils/timeutils.c
+++ b/shared/timeutils/timeutils.c
@@ -29,21 +29,48 @@
 
 #include "shared/timeutils/timeutils.h"
 
-// LEAPOCH corresponds to 2000-03-01, which is a mod-400 year, immediately
-// after Feb 29. We calculate seconds as a signed integer relative to that.
+// To maintain reasonable compatibility with CPython on embedded systems,
+// and avoid breaking anytime soon, timeutils functions are required to
+// work properly between 1970 and 2099 on all ports.
 //
-// Our timebase is relative to 2000-01-01.
-
-#define LEAPOCH ((31 + 29) * 86400)
+// During that period of time, leap years occur every 4 years without
+// exception, so we can keep the code short for 32 bit machines.
+
+// The last leap day before the required period is Feb 29, 1968.
+// This is the number of days to add to get to that date.
+#define PREV_LEAP_DAY  ((mp_uint_t)(365 + 366 - (31 + 29)))
+#define PREV_LEAP_YEAR 1968
+
+// On ports where either MICROPY_TIME_SUPPORT_Y2100_AND_BEYOND or
+// MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE is enabled, we include extra
+// code to support leap years outside of the 'easy' period.
+// Computation is then made based on 1600 (a mod-400 year).
+// This is the number of days between 1600 and 1968.
+#define QC_BASE_DAY  134409
+#define QC_LEAP_YEAR 1600
+// This is the number of leap days between 1600 and 1970
+#define QC_LEAP_DAYS 89
 
 #define DAYS_PER_400Y (365 * 400 + 97)
 #define DAYS_PER_100Y (365 * 100 + 24)
 #define DAYS_PER_4Y   (365 * 4 + 1)
 
-STATIC const uint16_t days_since_jan1[] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 };
+static const uint16_t days_since_jan1[] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 };
+
+// type used internally to count small integers relative to epoch
+// (using uint when possible produces smaller code on some platforms)
+#if MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE
+typedef mp_int_t relint_t;
+#else
+typedef mp_uint_t relint_t;
+#endif
 
 bool timeutils_is_leap_year(mp_uint_t year) {
+    #if MICROPY_TIME_SUPPORT_Y2100_AND_BEYOND || MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE
     return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
+    #else
+    return year % 4 == 0;
+    #endif
 }
 
 // month is one based
@@ -65,67 +92,67 @@ mp_uint_t timeutils_year_day(mp_uint_t year, mp_uint_t month, mp_uint_t date) {
     return yday;
 }
 
-void timeutils_seconds_since_2000_to_struct_time(mp_uint_t t, timeutils_struct_time_t *tm) {
-    // The following algorithm was adapted from musl's __secs_to_tm and adapted
-    // for differences in MicroPython's timebase.
-
-    mp_int_t seconds = t - LEAPOCH;
+void timeutils_seconds_since_1970_to_struct_time(timeutils_timestamp_t seconds, timeutils_struct_time_t *tm) {
+    // The following algorithm was inspired from musl's __secs_to_tm
+    // and simplified to reduce code footprint in the simple case
 
-    mp_int_t days = seconds / 86400;
+    relint_t days = seconds / 86400;
     seconds %= 86400;
+    #if MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE
     if (seconds < 0) {
         seconds += 86400;
         days -= 1;
     }
+    #endif
     tm->tm_hour = seconds / 3600;
     tm->tm_min = seconds / 60 % 60;
     tm->tm_sec = seconds % 60;
 
-    mp_int_t wday = (days + 2) % 7;   // Mar 1, 2000 was a Wednesday (2)
+    relint_t wday = (days + 3) % 7;   // Jan 1, 1970 was a Thursday (3)
+    #if MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE
     if (wday < 0) {
         wday += 7;
     }
+    #endif
     tm->tm_wday = wday;
 
-    mp_int_t qc_cycles = days / DAYS_PER_400Y;
+    days += PREV_LEAP_DAY;
+
+    #if MICROPY_TIME_SUPPORT_Y2100_AND_BEYOND || MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE
+    // rebase day to the oldest supported date (=> always positive)
+    mp_uint_t base_year = QC_LEAP_YEAR;
+    days += QC_BASE_DAY;
+    mp_uint_t qc_cycles = days / DAYS_PER_400Y;
     days %= DAYS_PER_400Y;
-    if (days < 0) {
-        days += DAYS_PER_400Y;
-        qc_cycles--;
-    }
-    mp_int_t c_cycles = days / DAYS_PER_100Y;
+    mp_uint_t c_cycles = days / DAYS_PER_100Y;
     if (c_cycles == 4) {
         c_cycles--;
     }
     days -= (c_cycles * DAYS_PER_100Y);
-
-    mp_int_t q_cycles = days / DAYS_PER_4Y;
+    #else
+    mp_uint_t base_year = PREV_LEAP_YEAR;
+    mp_uint_t qc_cycles = 0;
+    mp_uint_t c_cycles = 0;
+    #endif
+
+    mp_uint_t q_cycles = days / DAYS_PER_4Y;
+    #if MICROPY_TIME_SUPPORT_Y2100_AND_BEYOND || MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE
     if (q_cycles == 25) {
         q_cycles--;
     }
+    #endif
     days -= q_cycles * DAYS_PER_4Y;
 
-    mp_int_t years = days / 365;
+    relint_t years = days / 365;
     if (years == 4) {
         years--;
     }
     days -= (years * 365);
 
-    /* We will compute tm_yday at the very end
-    mp_int_t leap = !years && (q_cycles || !c_cycles);
-
-    tm->tm_yday = days + 31 + 28 + leap;
-    if (tm->tm_yday >= 365 + leap) {
-        tm->tm_yday -= 365 + leap;
-    }
-
-    tm->tm_yday++;  // Make one based
-    */
-
-    tm->tm_year = 2000 + years + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles;
+    tm->tm_year = base_year + years + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles;
 
     // Note: days_in_month[0] corresponds to March
-    STATIC const int8_t days_in_month[] = {31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29};
+    static const uint8_t days_in_month[] = {31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29};
 
     mp_int_t month;
     for (month = 0; days_in_month[month] <= days; month++) {
@@ -144,21 +171,28 @@ void timeutils_seconds_since_2000_to_struct_time(mp_uint_t t, timeutils_struct_t
 }
 
 // returns the number of seconds, as an integer, since 2000-01-01
-mp_uint_t timeutils_seconds_since_2000(mp_uint_t year, mp_uint_t month,
+timeutils_timestamp_t timeutils_seconds_since_1970(mp_uint_t year, mp_uint_t month,
     mp_uint_t date, mp_uint_t hour, mp_uint_t minute, mp_uint_t second) {
-    return
-        second
-        + minute * 60
-        + hour * 3600
-        + (timeutils_year_day(year, month, date) - 1
-            + ((year - 2000 + 3) / 4) // add a day each 4 years starting with 2001
-            - ((year - 2000 + 99) / 100) // subtract a day each 100 years starting with 2001
-            + ((year - 2000 + 399) / 400) // add a day each 400 years starting with 2001
-            ) * 86400
-        + (year - 2000) * 31536000;
+    #if MICROPY_TIME_SUPPORT_Y2100_AND_BEYOND || MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE
+    mp_uint_t ref_year = QC_LEAP_YEAR;
+    #else
+    mp_uint_t ref_year = PREV_LEAP_YEAR;
+    #endif
+    timeutils_timestamp_t res;
+    res = ((relint_t)year - 1970) * 365;
+    res += (year - (ref_year + 1)) / 4; // add a day each 4 years
+    #if MICROPY_TIME_SUPPORT_Y2100_AND_BEYOND || MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE
+    res -= (year - (ref_year + 1)) / 100; // subtract a day each 100 years
+    res += (year - (ref_year + 1)) / 400; // add a day each 400 years
+    res -= QC_LEAP_DAYS;
+    #endif
+    res += timeutils_year_day(year, month, date) - 1;
+    res *= 86400;
+    res += hour * 3600 + minute * 60 + second;
+    return res;
 }
 
-mp_uint_t timeutils_mktime_2000(mp_uint_t year, mp_int_t month, mp_int_t mday,
+timeutils_timestamp_t timeutils_mktime_1970(mp_uint_t year, mp_int_t month, mp_int_t mday,
     mp_int_t hours, mp_int_t minutes, mp_int_t seconds) {
 
     // Normalize the tuple. This allows things like:
@@ -211,12 +245,16 @@ mp_uint_t timeutils_mktime_2000(mp_uint_t year, mp_int_t month, mp_int_t mday,
             year++;
         }
     }
-    return timeutils_seconds_since_2000(year, month, mday, hours, minutes, seconds);
+    return timeutils_seconds_since_1970(year, month, mday, hours, minutes, seconds);
 }
 
 // Calculate the weekday from the date.
 // The result is zero based with 0 = Monday.
 // by Michael Keith and Tom Craver, 1990.
 int timeutils_calc_weekday(int y, int m, int d) {
-    return ((d += m < 3 ? y-- : y - 2, 23 * m / 9 + d + 4 + y / 4 - y / 100 + y / 400) + 6) % 7;
+    return ((d += m < 3 ? y-- : y - 2, 23 * m / 9 + d + 4 + y / 4
+        #if MICROPY_TIME_SUPPORT_Y2100_AND_BEYOND || MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE
+        - y / 100 + y / 400
+        #endif
+        ) + 6) % 7;
 }
diff --git a/shared/timeutils/timeutils.h b/shared/timeutils/timeutils.h
index 9f4b500caa7..35356b462aa 100644
--- a/shared/timeutils/timeutils.h
+++ b/shared/timeutils/timeutils.h
@@ -27,9 +27,23 @@
 #ifndef MICROPY_INCLUDED_LIB_TIMEUTILS_TIMEUTILS_H
 #define MICROPY_INCLUDED_LIB_TIMEUTILS_TIMEUTILS_H
 
+#include "py/obj.h"
+#if MICROPY_PY_BUILTINS_FLOAT && MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_DOUBLE
+#include  // required for trunc()
+#endif
+
+// `timeutils_timestamp_t` is the type used internally by timeutils to
+// represent timestamps, and is always referenced to 1970.
+// It may not match the platform-specific `mp_timestamp_t`.
+#if MICROPY_TIME_SUPPORT_Y2100_AND_BEYOND || MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE
+typedef long long timeutils_timestamp_t;
+#else
+typedef mp_uint_t timeutils_timestamp_t;
+#endif
+
 // The number of seconds between 1970/1/1 and 2000/1/1 is calculated using:
 // time.mktime((2000,1,1,0,0,0,0,0,0)) - time.mktime((1970,1,1,0,0,0,0,0,0))
-#define TIMEUTILS_SECONDS_1970_TO_2000 (946684800ULL)
+#define TIMEUTILS_SECONDS_1970_TO_2000 (946684800LL)
 
 typedef struct _timeutils_struct_time_t {
     uint16_t tm_year;       // i.e. 2014
@@ -45,62 +59,116 @@ typedef struct _timeutils_struct_time_t {
 bool timeutils_is_leap_year(mp_uint_t year);
 mp_uint_t timeutils_days_in_month(mp_uint_t year, mp_uint_t month);
 mp_uint_t timeutils_year_day(mp_uint_t year, mp_uint_t month, mp_uint_t date);
+int timeutils_calc_weekday(int y, int m, int d);
 
-void timeutils_seconds_since_2000_to_struct_time(mp_uint_t t,
+void timeutils_seconds_since_1970_to_struct_time(timeutils_timestamp_t t,
     timeutils_struct_time_t *tm);
 
-mp_uint_t timeutils_seconds_since_2000(mp_uint_t year, mp_uint_t month,
+// Year is absolute, month/date are 1-based, hour/minute/second are 0-based.
+timeutils_timestamp_t timeutils_seconds_since_1970(mp_uint_t year, mp_uint_t month,
     mp_uint_t date, mp_uint_t hour, mp_uint_t minute, mp_uint_t second);
 
-mp_uint_t timeutils_mktime_2000(mp_uint_t year, mp_int_t month, mp_int_t mday,
+// Year is absolute, month/mday are 1-based, hours/minutes/seconds are 0-based.
+timeutils_timestamp_t timeutils_mktime_1970(mp_uint_t year, mp_int_t month, mp_int_t mday,
     mp_int_t hours, mp_int_t minutes, mp_int_t seconds);
 
+static inline mp_timestamp_t timeutils_obj_get_timestamp(mp_obj_t o_in) {
+    #if MICROPY_PY_BUILTINS_FLOAT && MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_DOUBLE
+    mp_float_t val = mp_obj_get_float(o_in);
+    return (mp_timestamp_t)MICROPY_FLOAT_C_FUN(trunc)(val);
+    #elif MICROPY_TIMESTAMP_IMPL == MICROPY_TIMESTAMP_IMPL_UINT
+    return mp_obj_get_uint(o_in);
+    #else
+    return mp_obj_get_ll(o_in);
+    #endif
+}
+
+static inline mp_obj_t timeutils_obj_from_timestamp(mp_timestamp_t t) {
+    #if MICROPY_TIMESTAMP_IMPL == MICROPY_TIMESTAMP_IMPL_UINT
+    return mp_obj_new_int_from_uint(t);
+    #else
+    return mp_obj_new_int_from_ll(t);
+    #endif
+}
+
+static inline void timeutils_seconds_since_2000_to_struct_time(mp_timestamp_t t, timeutils_struct_time_t *tm) {
+    timeutils_seconds_since_1970_to_struct_time((timeutils_timestamp_t)(t + TIMEUTILS_SECONDS_1970_TO_2000), tm);
+}
+
+// Year is absolute, month/date are 1-based, hour/minute/second are 0-based.
+static inline mp_timestamp_t timeutils_seconds_since_2000(mp_uint_t year, mp_uint_t month, mp_uint_t date,
+    mp_uint_t hour, mp_uint_t minute, mp_uint_t second) {
+    return (mp_timestamp_t)timeutils_seconds_since_1970(year, month, date, hour, minute, second) - TIMEUTILS_SECONDS_1970_TO_2000;
+}
+
+// Year is absolute, month/mday are 1-based, hours/minutes/seconds are 0-based.
+static inline mp_timestamp_t timeutils_mktime_2000(mp_uint_t year, mp_int_t month, mp_int_t mday,
+    mp_int_t hours, mp_int_t minutes, mp_int_t seconds) {
+    return (mp_timestamp_t)timeutils_mktime_1970(year, month, mday, hours, minutes, seconds) - TIMEUTILS_SECONDS_1970_TO_2000;
+}
+
+
 // Select the Epoch used by the port.
 #if MICROPY_EPOCH_IS_1970
 
-static inline void timeutils_seconds_since_epoch_to_struct_time(uint64_t t, timeutils_struct_time_t *tm) {
-    // TODO this will give incorrect results for dates before 2000/1/1
-    return timeutils_seconds_since_2000_to_struct_time(t - TIMEUTILS_SECONDS_1970_TO_2000, tm);
+static inline void timeutils_seconds_since_epoch_to_struct_time(mp_timestamp_t t, timeutils_struct_time_t *tm) {
+    timeutils_seconds_since_1970_to_struct_time(t, tm);
 }
 
-static inline uint64_t timeutils_mktime(mp_uint_t year, mp_int_t month, mp_int_t mday, mp_int_t hours, mp_int_t minutes, mp_int_t seconds) {
-    return timeutils_mktime_2000(year, month, mday, hours, minutes, seconds) + TIMEUTILS_SECONDS_1970_TO_2000;
+// Year is absolute, month/date are 1-based, hour/minute/second are 0-based.
+static inline mp_timestamp_t timeutils_seconds_since_epoch(mp_uint_t year, mp_uint_t month, mp_uint_t date,
+    mp_uint_t hour, mp_uint_t minute, mp_uint_t second) {
+    return timeutils_seconds_since_1970(year, month, date, hour, minute, second);
 }
 
-static inline uint64_t timeutils_seconds_since_epoch(mp_uint_t year, mp_uint_t month,
-    mp_uint_t date, mp_uint_t hour, mp_uint_t minute, mp_uint_t second) {
-    // TODO this will give incorrect results for dates before 2000/1/1
-    return timeutils_seconds_since_2000(year, month, date, hour, minute, second) + TIMEUTILS_SECONDS_1970_TO_2000;
+// Year is absolute, month/mday are 1-based, hours/minutes/seconds are 0-based.
+static inline mp_timestamp_t timeutils_mktime(mp_uint_t year, mp_int_t month, mp_int_t mday,
+    mp_int_t hours, mp_int_t minutes, mp_int_t seconds) {
+    return timeutils_mktime_1970(year, month, mday, hours, minutes, seconds);
 }
 
-static inline mp_uint_t timeutils_seconds_since_epoch_from_nanoseconds_since_1970(uint64_t ns) {
-    return ns / 1000000000ULL;
+static inline mp_timestamp_t timeutils_seconds_since_epoch_from_nanoseconds_since_1970(int64_t ns) {
+    return (mp_timestamp_t)(ns / 1000000000LL);
 }
 
-static inline uint64_t timeutils_nanoseconds_since_epoch_to_nanoseconds_since_1970(uint64_t ns) {
+static inline int64_t timeutils_seconds_since_epoch_to_nanoseconds_since_1970(mp_timestamp_t s) {
+    return (int64_t)s * 1000000000LL;
+}
+
+static inline int64_t timeutils_nanoseconds_since_epoch_to_nanoseconds_since_1970(int64_t ns) {
     return ns;
 }
 
 #else // Epoch is 2000
 
-#define timeutils_seconds_since_epoch_to_struct_time timeutils_seconds_since_2000_to_struct_time
-#define timeutils_seconds_since_epoch timeutils_seconds_since_2000
-#define timeutils_mktime timeutils_mktime_2000
+static inline void timeutils_seconds_since_epoch_to_struct_time(mp_timestamp_t t, timeutils_struct_time_t *tm) {
+    timeutils_seconds_since_2000_to_struct_time(t, tm);
+}
 
-static inline uint64_t timeutils_seconds_since_epoch_to_nanoseconds_since_1970(mp_uint_t s) {
-    return ((uint64_t)s + TIMEUTILS_SECONDS_1970_TO_2000) * 1000000000ULL;
+// Year is absolute, month/date are 1-based, hour/minute/second are 0-based.
+static inline mp_timestamp_t timeutils_seconds_since_epoch(mp_uint_t year, mp_uint_t month, mp_uint_t date,
+    mp_uint_t hour, mp_uint_t minute, mp_uint_t second) {
+    return timeutils_seconds_since_2000(year, month, date, hour, minute, second);
 }
 
-static inline mp_uint_t timeutils_seconds_since_epoch_from_nanoseconds_since_1970(uint64_t ns) {
-    return ns / 1000000000ULL - TIMEUTILS_SECONDS_1970_TO_2000;
+// Year is absolute, month/mday are 1-based, hours/minutes/seconds are 0-based.
+static inline mp_timestamp_t timeutils_mktime(mp_uint_t year, mp_int_t month, mp_int_t mday,
+    mp_int_t hours, mp_int_t minutes, mp_int_t seconds) {
+    return timeutils_mktime_2000(year, month, mday, hours, minutes, seconds);
+}
+
+static inline mp_timestamp_t timeutils_seconds_since_epoch_from_nanoseconds_since_1970(int64_t ns) {
+    return (mp_timestamp_t)(ns / 1000000000LL - TIMEUTILS_SECONDS_1970_TO_2000);
+}
+
+static inline int64_t timeutils_seconds_since_epoch_to_nanoseconds_since_1970(mp_timestamp_t s) {
+    return ((int64_t)s + TIMEUTILS_SECONDS_1970_TO_2000) * 1000000000LL;
 }
 
 static inline int64_t timeutils_nanoseconds_since_epoch_to_nanoseconds_since_1970(int64_t ns) {
-    return ns + TIMEUTILS_SECONDS_1970_TO_2000 * 1000000000ULL;
+    return ns + TIMEUTILS_SECONDS_1970_TO_2000 * 1000000000LL;
 }
 
 #endif
 
-int timeutils_calc_weekday(int y, int m, int d);
-
 #endif // MICROPY_INCLUDED_LIB_TIMEUTILS_TIMEUTILS_H
diff --git a/shared/tinyusb/mp_cdc_common.c b/shared/tinyusb/mp_cdc_common.c
deleted file mode 100644
index cd4f5d1013b..00000000000
--- a/shared/tinyusb/mp_cdc_common.c
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * This file is part of the MicroPython project, http://micropython.org/
- *
- * The MIT License (MIT)
- *
- * Copyright (c) 2022 Ibrahim Abdelkader 
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-#include "py/runtime.h"
-#include "py/mphal.h"
-#include "modmachine.h"
-
-#if MICROPY_HW_USB_CDC_1200BPS_TOUCH && MICROPY_HW_ENABLE_USBDEV
-
-#include "tusb.h"
-
-static mp_sched_node_t mp_bootloader_sched_node;
-
-STATIC void usbd_cdc_run_bootloader_task(mp_sched_node_t *node) {
-    mp_hal_delay_ms(250);
-    machine_bootloader(0, NULL);
-}
-
-void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts) {
-    if (dtr == false && rts == false) {
-        // Device is disconnected.
-        cdc_line_coding_t line_coding;
-        tud_cdc_n_get_line_coding(itf, &line_coding);
-        if (line_coding.bit_rate == 1200) {
-            // Delay bootloader jump to allow the USB stack to service endpoints.
-            mp_sched_schedule_node(&mp_bootloader_sched_node, usbd_cdc_run_bootloader_task);
-        }
-    }
-}
-
-#endif
diff --git a/shared/tinyusb/mp_usbd.c b/shared/tinyusb/mp_usbd.c
index ea1de674bc1..a21e5f027bb 100644
--- a/shared/tinyusb/mp_usbd.c
+++ b/shared/tinyusb/mp_usbd.c
@@ -24,20 +24,44 @@
  * THE SOFTWARE.
  */
 
-#include 
-
-#include "py/mpconfig.h"
+#include "py/mphal.h"
 
 #if MICROPY_HW_ENABLE_USBDEV
 
-#ifndef NO_QSTR
-#include "tusb.h" // TinyUSB is not available when running the string preprocessor
-#include "device/usbd.h"
-#include "device/usbd_pvt.h"
-#endif
+#include "mp_usbd.h"
+
+#if !MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
 
-void usbd_task(void) {
+void mp_usbd_task(void) {
     tud_task_ext(0, false);
 }
 
-#endif
+void mp_usbd_task_callback(mp_sched_node_t *node) {
+    (void)node;
+    mp_usbd_task();
+}
+
+#endif // !MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
+
+// Schedule the TinyUSB task on demand, when there is a new USB device event
+TU_ATTR_FAST_FUNC void MICROPY_WRAP_TUD_EVENT_HOOK_CB(tud_event_hook_cb)(uint8_t rhport, uint32_t eventid, bool in_isr) {
+    mp_usbd_schedule_task();
+    mp_hal_wake_main_task_from_isr();
+}
+
+TU_ATTR_FAST_FUNC void mp_usbd_schedule_task(void) {
+    static mp_sched_node_t usbd_task_node;
+    mp_sched_schedule_node(&usbd_task_node, mp_usbd_task_callback);
+}
+
+void mp_usbd_hex_str(char *out_str, const uint8_t *bytes, size_t bytes_len) {
+    size_t hex_len = bytes_len * 2;
+    for (int i = 0; i < hex_len; i += 2) {
+        static const char *hexdig = "0123456789abcdef";
+        out_str[i] = hexdig[bytes[i / 2] >> 4];
+        out_str[i + 1] = hexdig[bytes[i / 2] & 0x0f];
+    }
+    out_str[hex_len] = 0;
+}
+
+#endif // MICROPY_HW_ENABLE_USBDEV
diff --git a/shared/tinyusb/mp_usbd.h b/shared/tinyusb/mp_usbd.h
index 3a93b929c58..e2343e54228 100644
--- a/shared/tinyusb/mp_usbd.h
+++ b/shared/tinyusb/mp_usbd.h
@@ -27,13 +27,155 @@
 #ifndef MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_H
 #define MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_H
 
+#include "py/mpconfig.h"
+
+#ifndef MICROPY_WRAP_TUD_SOF_CB
+#define MICROPY_WRAP_TUD_SOF_CB(name) name
+#endif
+
+#ifndef MICROPY_WRAP_TUD_CDC_RX_CB
+#define MICROPY_WRAP_TUD_CDC_RX_CB(name) name
+#endif
+
+#ifndef MICROPY_WRAP_TUD_CDC_LINE_STATE_CB
+#define MICROPY_WRAP_TUD_CDC_LINE_STATE_CB(name) name
+#endif
+
+#ifndef MICROPY_WRAP_TUD_EVENT_HOOK_CB
+#define MICROPY_WRAP_TUD_EVENT_HOOK_CB(name) name
+#endif
+
+#ifndef MICROPY_HW_TINYUSB_LL_INIT
+#define MICROPY_HW_TINYUSB_LL_INIT()
+#endif
+
+#if MICROPY_HW_ENABLE_USBDEV
+
 #include "py/obj.h"
+#include "py/objarray.h"
+#include "py/runtime.h"
+
+#ifndef NO_QSTR
+#include "tusb.h"
+#include "device/dcd.h"
+#include "class/cdc/cdc_device.h"
+#endif
 
-// Call instead of tud_task()
+// Initialise TinyUSB device.
+static inline void mp_usbd_init_tud(void) {
+    tusb_init();
+    #if TUSB_VERSION_NUMBER < 2100 && MICROPY_HW_USB_CDC
+    // TinyUSB prior to 0.21.0 had TX persistence configurable at runtime.
+    tud_cdc_configure_t cfg = {
+        .rx_persistent = 0,
+        .tx_persistent = 1,
+        .tx_overwritabe_if_not_connected = 1,
+    };
+    tud_cdc_configure(&cfg);
+    #endif
+}
+
+// Run the TinyUSB device task
 void mp_usbd_task(void);
 
+// Schedule a call to mp_usbd_task(), even if no USB interrupt has occurred
+void mp_usbd_schedule_task(void);
+
 // Function to be implemented in port code.
 // Can write a string up to MICROPY_HW_USB_DESC_STR_MAX characters long, plus terminating byte.
 extern void mp_usbd_port_get_serial_number(char *buf);
 
+// Most ports need to write a hexadecimal serial number from a byte array. This
+// is a helper function for this. out_str must be long enough to hold a string of total
+// length (2 * bytes_len + 1) (including NUL terminator).
+void mp_usbd_hex_str(char *out_str, const uint8_t *bytes, size_t bytes_len);
+
+// Length of built-in configuration descriptor
+#define MP_USBD_BUILTIN_DESC_CFG_LEN ( \
+    (CFG_TUD_CDC ? (TUD_CDC_DESC_LEN) : 0) + \
+    (CFG_TUD_MSC ? (TUD_MSC_DESC_LEN) : 0) + \
+    (CFG_TUD_NCM ? (TUD_CDC_NCM_DESC_LEN) : 0) + \
+    TUD_CONFIG_DESC_LEN)
+
+// Built-in USB device and configuration descriptor values
+extern const tusb_desc_device_t mp_usbd_builtin_desc_dev;
+extern const uint8_t mp_usbd_builtin_desc_cfg[MP_USBD_BUILTIN_DESC_CFG_LEN];
+#if (CFG_TUD_MAX_SPEED == OPT_MODE_HIGH_SPEED)
+extern const tusb_desc_device_qualifier_t mp_usbd_builtin_desc_qual;
+#endif
+
+void mp_usbd_task_callback(mp_sched_node_t *node);
+
+#if !MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
+
+static inline void mp_usbd_init(void) {
+    // Without runtime USB support, this can be a thin wrapper wrapper around tusb_init()
+    // which is called in the below helper function.
+    MICROPY_HW_TINYUSB_LL_INIT();
+    mp_usbd_init_tud();
+}
+
+static inline void mp_usbd_deinit(void) {
+    // Called in soft reset path. No-op if no runtime USB devices require cleanup.
+}
+
+#else
+// Runtime USB Device support requires more complex init/deinit
+void mp_usbd_init(void);
+void mp_usbd_deinit(void);
+
+const char *mp_usbd_runtime_string_cb(uint8_t index);
+
+// Maximum number of pending exceptions per single TinyUSB task execution
+#define MP_USBD_MAX_PEND_EXCS 2
+
+typedef struct {
+    mp_obj_base_t base;
+
+    mp_obj_t desc_dev; // Device descriptor bytes
+    mp_obj_t desc_cfg; // Configuration descriptor bytes
+    mp_obj_t desc_strs; // List/dict/similar to look up string descriptors by index
+
+    // Runtime device driver callback functions
+    mp_obj_t open_itf_cb;
+    mp_obj_t reset_cb;
+    mp_obj_t control_xfer_cb;
+    mp_obj_t xfer_cb;
+
+    mp_obj_t builtin_driver; // Points to one of mp_type_usb_device_builtin_nnn
+
+    bool active; // Has the user set the USB device active?
+    bool trigger; // Has the user requested the active state change (or re-activate)?
+
+    // Temporary pointers for xfer data in progress on each endpoint
+    // Ensuring they aren't garbage collected until the xfer completes
+    mp_obj_t xfer_data[CFG_TUD_ENDPPOINT_MAX][2];
+
+    // Pointer to a memoryview that is reused to refer to various pieces of
+    // control transfer data that are pushed to USB control transfer
+    // callbacks. Python code can't rely on the memoryview contents
+    // to remain valid after the callback returns!
+    mp_obj_array_t *control_data;
+
+    // Pointers to exceptions thrown inside Python callbacks. See
+    // usbd_callback_function_n().
+    mp_uint_t num_pend_excs;
+    mp_obj_t pend_excs[MP_USBD_MAX_PEND_EXCS];
+} mp_obj_usb_device_t;
+
+// Built-in constant objects, possible values of builtin_driver
+//
+// (Currently not possible to change built-in drivers at runtime, just enable/disable.)
+extern const mp_obj_type_t mp_type_usb_device_builtin_default;
+extern const mp_obj_type_t mp_type_usb_device_builtin_none;
+
+// Return true if any built-in driver is enabled
+static inline bool mp_usb_device_builtin_enabled(const mp_obj_usb_device_t *usbd) {
+    return usbd->builtin_driver != MP_OBJ_FROM_PTR(&mp_type_usb_device_builtin_none);
+}
+
+#endif // MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
+
+#endif // MICROPY_HW_ENABLE_USBDEV
+
 #endif // MICROPY_INCLUDED_SHARED_TINYUSB_USBD_H
diff --git a/shared/tinyusb/mp_usbd_cdc.c b/shared/tinyusb/mp_usbd_cdc.c
new file mode 100644
index 00000000000..8018032d36c
--- /dev/null
+++ b/shared/tinyusb/mp_usbd_cdc.c
@@ -0,0 +1,220 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2022 Ibrahim Abdelkader 
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "py/runtime.h"
+#include "py/mphal.h"
+#include "py/stream.h"
+#include "extmod/modmachine.h"
+
+#include "mp_usbd.h"
+#include "mp_usbd_cdc.h"
+
+#if MICROPY_HW_USB_CDC && MICROPY_HW_ENABLE_USBDEV && !MICROPY_EXCLUDE_SHARED_TINYUSB_USBD_CDC
+
+// TinyUSB has no public API for endpoint stall detection/clearing; this
+// private header is the intended interface for class drivers (all built-in
+// TinyUSB class drivers include it for the same purpose).
+#include "device/usbd_pvt.h"
+
+static uint8_t cdc_itf_pending; // keep track of cdc interfaces which need attention to poll
+static int8_t cdc_connected_flush_delay = 0;
+
+uintptr_t mp_usbd_cdc_poll_interfaces(uintptr_t poll_flags) {
+    uintptr_t ret = 0;
+    if (!cdc_itf_pending) {
+        // Explicitly run the USB stack as the scheduler may be locked (eg we are in
+        // an interrupt handler) while there is data pending.
+        mp_usbd_task();
+    }
+
+    // any CDC interfaces left to poll?
+    if (cdc_itf_pending && ringbuf_free(&stdin_ringbuf)) {
+        for (uint8_t itf = 0; itf < 8; ++itf) {
+            if (cdc_itf_pending & (1 << itf)) {
+                tud_cdc_rx_cb(itf);
+                if (!cdc_itf_pending) {
+                    break;
+                }
+            }
+        }
+    }
+    if ((poll_flags & MP_STREAM_POLL_RD) && ringbuf_peek(&stdin_ringbuf) != -1) {
+        ret |= MP_STREAM_POLL_RD;
+    }
+    if ((poll_flags & MP_STREAM_POLL_WR) &&
+        (!tud_cdc_connected() || (tud_cdc_connected() && tud_cdc_write_available() > 0))) {
+        // Always allow write when not connected, fifo will retain latest.
+        // When connected operate as blocking, only allow if space is available.
+        ret |= MP_STREAM_POLL_WR;
+    }
+    return ret;
+}
+
+void MICROPY_WRAP_TUD_CDC_RX_CB(tud_cdc_rx_cb)(uint8_t itf) {
+    // consume pending USB data immediately to free usb buffer and keep the endpoint from stalling.
+    // in case the ringbuffer is full, mark the CDC interface that need attention later on for polling
+    cdc_itf_pending &= ~(1 << itf);
+    for (uint32_t bytes_avail = tud_cdc_n_available(itf); bytes_avail > 0; --bytes_avail) {
+        if (ringbuf_free(&stdin_ringbuf)) {
+            int data_char = tud_cdc_read_char();
+            #if MICROPY_KBD_EXCEPTION
+            if (data_char == mp_interrupt_char) {
+                // Clear the ring buffer
+                stdin_ringbuf.iget = stdin_ringbuf.iput = 0;
+                // and stop
+                mp_sched_keyboard_interrupt();
+            } else {
+                ringbuf_put(&stdin_ringbuf, data_char);
+            }
+            #else
+            ringbuf_put(&stdin_ringbuf, data_char);
+            #endif
+        } else {
+            cdc_itf_pending |= (1 << itf);
+            return;
+        }
+    }
+}
+
+mp_uint_t mp_usbd_cdc_tx_strn(const char *str, mp_uint_t len) {
+    if (!tusb_inited()) {
+        return 0;
+    }
+    mp_uint_t last_write = mp_hal_ticks_ms();
+    size_t i = 0;
+    while (i < len) {
+        uint32_t n = len - i;
+
+        if (tud_cdc_connected()) {
+            // Limit write to available space in tx buffer when connected.
+            //
+            // (If not connected then we write everything to the fifo, expecting
+            // it to overwrite old data so it will have latest data buffered
+            // when host connects.)
+            n = MIN(n, tud_cdc_write_available());
+        }
+
+        uint32_t n2 = tud_cdc_write(str + i, n);
+        tud_cdc_write_flush();
+        i += n2;
+
+        if (i < len) {
+            if (n2 > 0) {
+                // reset the timeout each time we successfully write to the FIFO
+                last_write = mp_hal_ticks_ms();
+            } else {
+                if ((mp_uint_t)(mp_hal_ticks_ms() - last_write) >= MICROPY_HW_USB_CDC_TX_TIMEOUT) {
+                    break; // Timeout
+                }
+
+                if (tud_cdc_connected()) {
+                    // If we know we're connected then we can wait for host to make
+                    // more space
+                    mp_event_wait_ms(1);
+                }
+            }
+
+            // Always explicitly run the USB stack as the scheduler may be
+            // locked (eg we are in an interrupt handler), while there is data
+            // or a state change pending.
+            mp_usbd_task();
+        }
+    }
+    return i;
+}
+
+void MICROPY_WRAP_TUD_SOF_CB(tud_sof_cb)(uint32_t frame_count) {
+    if (--cdc_connected_flush_delay < 0) {
+        // Finished on-connection delay, disable SOF interrupt again.
+        tud_sof_cb_enable(false);
+        tud_cdc_write_flush();
+    }
+}
+
+#endif
+
+#if MICROPY_HW_ENABLE_USBDEV && ( \
+    MICROPY_HW_USB_CDC_1200BPS_TOUCH || \
+    MICROPY_HW_USB_CDC || \
+    MICROPY_HW_USB_CDC_DTR_RTS_BOOTLOADER)
+
+#if MICROPY_HW_USB_CDC_1200BPS_TOUCH || MICROPY_HW_USB_CDC_DTR_RTS_BOOTLOADER
+static mp_sched_node_t mp_bootloader_sched_node;
+
+static void usbd_cdc_run_bootloader_task(mp_sched_node_t *node) {
+    mp_hal_delay_ms(250);
+    machine_bootloader(0, NULL);
+}
+#endif
+
+#if MICROPY_HW_USB_CDC_DTR_RTS_BOOTLOADER
+static struct {
+    bool dtr : 1;
+    bool rts : 1;
+} prev_line_state = {0};
+#endif
+
+void MICROPY_WRAP_TUD_CDC_LINE_STATE_CB(tud_cdc_line_state_cb)(uint8_t itf, bool dtr, bool rts) {
+    #if MICROPY_HW_USB_CDC && !MICROPY_EXCLUDE_SHARED_TINYUSB_USBD_CDC
+    if (dtr) {
+        // A host application has started to open the cdc serial port.
+        // USBD_CDC_EP_IN is the IN endpoint for itf 0; only clear stall for itf 0.
+        if (itf == 0 && usbd_edpt_stalled(TUD_OPT_RHPORT, USBD_CDC_EP_IN)) {
+            usbd_edpt_clear_stall(TUD_OPT_RHPORT, USBD_CDC_EP_IN);
+        }
+        // Wait a few ms for host to be ready then send tx buffer.
+        // High speed connection SOF fires at 125us, full speed at 1ms.
+        cdc_connected_flush_delay = (tud_speed_get() == TUSB_SPEED_HIGH) ? 128 : 16;
+        tud_sof_cb_enable(true);
+    } else {
+        // Host has closed the cdc serial port. Discard pending TX data to
+        // avoid a full FIFO blocking writes on the next connection.
+        tud_cdc_n_write_clear(itf);
+    }
+    #endif
+    #if MICROPY_HW_USB_CDC_DTR_RTS_BOOTLOADER
+    if (dtr && !rts) {
+        if (prev_line_state.rts && !prev_line_state.dtr) {
+            mp_sched_schedule_node(&mp_bootloader_sched_node, usbd_cdc_run_bootloader_task);
+        }
+    }
+    prev_line_state.rts = rts;
+    prev_line_state.dtr = dtr;
+    #endif
+    #if MICROPY_HW_USB_CDC_1200BPS_TOUCH
+    if (dtr == false && rts == false) {
+        // Device is disconnected.
+        cdc_line_coding_t line_coding;
+        tud_cdc_n_get_line_coding(itf, &line_coding);
+        if (line_coding.bit_rate == 1200) {
+            // Delay bootloader jump to allow the USB stack to service endpoints.
+            mp_sched_schedule_node(&mp_bootloader_sched_node, usbd_cdc_run_bootloader_task);
+        }
+    }
+    #endif
+}
+
+#endif
diff --git a/shared/tinyusb/mp_usbd_cdc.h b/shared/tinyusb/mp_usbd_cdc.h
new file mode 100644
index 00000000000..6d653a1d09b
--- /dev/null
+++ b/shared/tinyusb/mp_usbd_cdc.h
@@ -0,0 +1,46 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2022 Blake W. Felt & Angus Gratton
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_CDC_H
+#define MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_CDC_H
+
+#include "mp_usbd.h"
+
+#ifndef MICROPY_HW_USB_CDC_TX_TIMEOUT
+#define MICROPY_HW_USB_CDC_TX_TIMEOUT (500)
+#endif
+
+// This is typically only enabled on esp32
+// parts which have an internal usb peripheral.
+#ifndef MICROPY_HW_USB_CDC_DTR_RTS_BOOTLOADER
+#define MICROPY_HW_USB_CDC_DTR_RTS_BOOTLOADER (0)
+#endif
+
+uintptr_t mp_usbd_cdc_poll_interfaces(uintptr_t poll_flags);
+void MICROPY_WRAP_TUD_CDC_RX_CB(tud_cdc_rx_cb)(uint8_t itf);
+mp_uint_t mp_usbd_cdc_tx_strn(const char *str, mp_uint_t len);
+
+#endif // MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_CDC_H
diff --git a/shared/tinyusb/mp_usbd_descriptor.c b/shared/tinyusb/mp_usbd_descriptor.c
index 8fab599b673..f6b1fe9297d 100644
--- a/shared/tinyusb/mp_usbd_descriptor.c
+++ b/shared/tinyusb/mp_usbd_descriptor.c
@@ -31,12 +31,13 @@
 
 #include "tusb.h"
 #include "mp_usbd.h"
-#include "mp_usbd_internal.h"
 
 #define USBD_CDC_CMD_MAX_SIZE (8)
-#define USBD_CDC_IN_OUT_MAX_SIZE (64)
+#define USBD_CDC_IN_OUT_MAX_SIZE ((CFG_TUD_MAX_SPEED == OPT_MODE_HIGH_SPEED) ? 512 : 64)
+#define USBD_MSC_IN_OUT_MAX_SIZE ((CFG_TUD_MAX_SPEED == OPT_MODE_HIGH_SPEED) ? 512 : 64)
+#define USBD_NCM_IN_OUT_MAX_SIZE ((CFG_TUD_MAX_SPEED == OPT_MODE_HIGH_SPEED) ? 512 : 64)
 
-const tusb_desc_device_t mp_usbd_desc_device_static = {
+const tusb_desc_device_t mp_usbd_builtin_desc_dev = {
     .bLength = sizeof(tusb_desc_device_t),
     .bDescriptorType = TUSB_DESC_DEVICE,
     .bcdUSB = 0x0200,
@@ -53,8 +54,23 @@ const tusb_desc_device_t mp_usbd_desc_device_static = {
     .bNumConfigurations = 1,
 };
 
-const uint8_t mp_usbd_desc_cfg_static[USBD_STATIC_DESC_LEN] = {
-    TUD_CONFIG_DESCRIPTOR(1, USBD_ITF_STATIC_MAX, USBD_STR_0, USBD_STATIC_DESC_LEN,
+#if (CFG_TUD_MAX_SPEED == OPT_MODE_HIGH_SPEED)
+// Device qualifier descriptor for high-speed devices.
+const tusb_desc_device_qualifier_t mp_usbd_builtin_desc_qual = {
+    .bLength = sizeof(tusb_desc_device_qualifier_t),
+    .bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER,
+    .bcdUSB = 0x0200,
+    .bDeviceClass = TUSB_CLASS_MISC,
+    .bDeviceSubClass = MISC_SUBCLASS_COMMON,
+    .bDeviceProtocol = MISC_PROTOCOL_IAD,
+    .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
+    .bNumConfigurations = 0x01,
+    .bReserved = 0x00,
+};
+#endif
+
+const uint8_t mp_usbd_builtin_desc_cfg[MP_USBD_BUILTIN_DESC_CFG_LEN] = {
+    TUD_CONFIG_DESCRIPTOR(1, USBD_ITF_BUILTIN_MAX, USBD_STR_0, MP_USBD_BUILTIN_DESC_CFG_LEN,
         0, USBD_MAX_POWER_MA),
 
     #if CFG_TUD_CDC
@@ -62,58 +78,88 @@ const uint8_t mp_usbd_desc_cfg_static[USBD_STATIC_DESC_LEN] = {
         USBD_CDC_CMD_MAX_SIZE, USBD_CDC_EP_OUT, USBD_CDC_EP_IN, USBD_CDC_IN_OUT_MAX_SIZE),
     #endif
     #if CFG_TUD_MSC
-    TUD_MSC_DESCRIPTOR(USBD_ITF_MSC, 5, EPNUM_MSC_OUT, EPNUM_MSC_IN, 64),
+    TUD_MSC_DESCRIPTOR(USBD_ITF_MSC, USBD_STR_MSC, USBD_MSC_EP_OUT, USBD_MSC_EP_IN, USBD_MSC_IN_OUT_MAX_SIZE),
+    #endif
+    #if CFG_TUD_NCM
+    // Interface number, description string index, MAC address string index, EP notification address and size, EP data address (out, in), and size, max segment size, notification interval, network capabilities.
+    TUD_CDC_NCM_DESCRIPTOR(USBD_ITF_NCM, USBD_STR_NCM, USBD_STR_NCM_MAC, USBD_NCM_EP_CMD, 64, USBD_NCM_EP_OUT, USBD_NCM_EP_IN, USBD_NCM_IN_OUT_MAX_SIZE, CFG_TUD_NET_MTU, 50, 0),
     #endif
 };
 
 const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
     char serial_buf[MICROPY_HW_USB_DESC_STR_MAX + 1]; // Includes terminating NUL byte
     static uint16_t desc_wstr[MICROPY_HW_USB_DESC_STR_MAX + 1]; // Includes prefix uint16_t
-    const char *desc_str;
+    const char *desc_str = NULL;
     uint16_t desc_len;
 
-    switch (index) {
-        case 0:
-            desc_wstr[1] = 0x0409; // supported language is English
-            desc_len = 4;
-            break;
-        case USBD_STR_SERIAL:
-            // TODO: make a port-specific serial number callback
-            mp_usbd_port_get_serial_number(serial_buf);
-            desc_str = serial_buf;
-            break;
-        case USBD_STR_MANUF:
-            desc_str = MICROPY_HW_USB_MANUFACTURER_STRING;
-            break;
-        case USBD_STR_PRODUCT:
-            desc_str = MICROPY_HW_USB_PRODUCT_FS_STRING;
-            break;
-        #if CFG_TUD_CDC
-        case USBD_STR_CDC:
-            desc_str = MICROPY_HW_USB_CDC_INTERFACE_STRING;
-            break;
-        #endif
-        #if CFG_TUD_MSC
-        case USBD_STR_MSC:
-            desc_str = MICROPY_HW_USB_MSC_INTERFACE_STRING;
-            break;
-        #endif
-        default:
-            desc_str = NULL;
-    }
+    #if MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
+    desc_str = mp_usbd_runtime_string_cb(index);
+    #endif
 
-    if (index != 0) {
+    if (index == 0) {
+        // String descriptor 0 is special, see USB 2.0 section 9.6.7 String
+        //
+        // Expect any runtime value in desc_str to be a fully formed descriptor
         if (desc_str == NULL) {
-            return NULL; // Will STALL the endpoint
+            desc_str = "\x04\x03\x09\x04"; // Descriptor for "English"
+        }
+        if (desc_str[0] < sizeof(desc_wstr)) {
+            memcpy(desc_wstr, desc_str, desc_str[0]);
+            return desc_wstr;
         }
+        return NULL; // Descriptor length too long (or malformed), stall endpoint
+    }
 
-        // Convert from narrow string to wide string
-        desc_len = 2;
-        for (int i = 0; i < MICROPY_HW_USB_DESC_STR_MAX && desc_str[i] != 0; i++) {
-            desc_wstr[1 + i] = desc_str[i];
-            desc_len += 2;
+    // Otherwise, generate a "UNICODE" string descriptor from the C string
+
+    if (desc_str == NULL) {
+        // Fall back to the "static" string
+        switch (index) {
+            case USBD_STR_SERIAL:
+                mp_usbd_port_get_serial_number(serial_buf);
+                desc_str = serial_buf;
+                break;
+            case USBD_STR_MANUF:
+                desc_str = MICROPY_HW_USB_MANUFACTURER_STRING;
+                break;
+            case USBD_STR_PRODUCT:
+                desc_str = MICROPY_HW_USB_PRODUCT_FS_STRING;
+                break;
+            #if CFG_TUD_CDC
+            case USBD_STR_CDC:
+                desc_str = MICROPY_HW_USB_CDC_INTERFACE_STRING;
+                break;
+            #endif
+            #if CFG_TUD_MSC
+            case USBD_STR_MSC:
+                desc_str = MICROPY_HW_USB_MSC_INTERFACE_STRING;
+                break;
+            #endif
+            #if CFG_TUD_NCM
+            case USBD_STR_NCM:
+                desc_str = MICROPY_PY_NETWORK_USBD_NCM_INTERFACE_STRING;
+                break;
+            case USBD_STR_NCM_MAC:
+                mp_usbd_hex_str(serial_buf, (uint8_t *)tud_network_mac_address, sizeof(tud_network_mac_address));
+                desc_str = serial_buf;
+                break;
+            #endif
+            default:
+                break;
         }
     }
+
+    if (desc_str == NULL) {
+        return NULL; // No string, STALL the endpoint
+    }
+
+    // Convert from narrow string to wide string
+    desc_len = 2;
+    for (int i = 0; i < MICROPY_HW_USB_DESC_STR_MAX && desc_str[i] != 0; i++) {
+        desc_wstr[1 + i] = desc_str[i];
+        desc_len += 2;
+    }
+
     // first byte is length (including header), second byte is string type
     desc_wstr[0] = (TUSB_DESC_STRING << 8) | desc_len;
 
@@ -121,13 +167,27 @@ const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
 }
 
 
+#if !MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
+
 const uint8_t *tud_descriptor_device_cb(void) {
-    return (const void *)&mp_usbd_desc_device_static;
+    return (const void *)&mp_usbd_builtin_desc_dev;
 }
 
 const uint8_t *tud_descriptor_configuration_cb(uint8_t index) {
     (void)index;
-    return mp_usbd_desc_cfg_static;
+    return mp_usbd_builtin_desc_cfg;
 }
 
+#if (CFG_TUD_MAX_SPEED == OPT_MODE_HIGH_SPEED)
+uint8_t const *tud_descriptor_device_qualifier_cb(void) {
+    return (uint8_t const *)&mp_usbd_builtin_desc_qual;
+}
 #endif
+
+#else
+
+// If runtime device support is enabled, descriptor callbacks are implemented in usbd.c
+
+#endif // !MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
+
+#endif // MICROPY_HW_ENABLE_USBDEV
diff --git a/shared/tinyusb/mp_usbd_internal.h b/shared/tinyusb/mp_usbd_internal.h
deleted file mode 100644
index 8b8f50bae88..00000000000
--- a/shared/tinyusb/mp_usbd_internal.h
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * This file is part of the MicroPython project, http://micropython.org/
- *
- * The MIT License (MIT)
- *
- * Copyright (c) 2022 Angus Gratton
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-#ifndef MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_INTERNAL_H
-#define MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_INTERNAL_H
-#include "tusb.h"
-
-// Static USB device descriptor values
-extern const tusb_desc_device_t mp_usbd_desc_device_static;
-extern const uint8_t mp_usbd_desc_cfg_static[USBD_STATIC_DESC_LEN];
-
-#endif // MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_INTERNAL_H
diff --git a/shared/tinyusb/mp_usbd_runtime.c b/shared/tinyusb/mp_usbd_runtime.c
new file mode 100644
index 00000000000..4a1de40791b
--- /dev/null
+++ b/shared/tinyusb/mp_usbd_runtime.c
@@ -0,0 +1,588 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2022 Blake W. Felt
+ * Copyright (c) 2022-2023 Angus Gratton
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+#include 
+
+#include "mp_usbd.h"
+#include "py/mpconfig.h"
+#include "py/mperrno.h"
+#include "py/mphal.h"
+#include "py/obj.h"
+#include "py/objarray.h"
+#include "py/objstr.h"
+#include "py/runtime.h"
+
+#if MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
+
+#ifndef NO_QSTR
+#include "tusb.h" // TinyUSB is not available when running the string preprocessor
+#include "device/dcd.h"
+#include "device/usbd.h"
+#include "device/usbd_pvt.h"
+#endif
+
+#define RHPORT TUD_OPT_RHPORT
+
+static bool in_usbd_task; // Flags if mp_usbd_task() is currently running
+
+// Some top-level functions that manage global TinyUSB USBD state, not the
+// singleton object visible to Python
+static void mp_usbd_disconnect(mp_obj_usb_device_t *usbd);
+static void mp_usbd_task_inner(void);
+
+// Pend an exception raise in a USBD callback to print when safe.
+//
+// We can't raise any exceptions out of the TinyUSB task, as it may still need
+// to do some state cleanup.
+//
+// The requirement for this becomes very similar to
+// mp_call_function_x_protected() for interrupts, but it's more restrictive: if
+// the C-based USB-CDC serial port is in use, we can't print from inside a
+// TinyUSB callback as it might try to recursively call into TinyUSB to flush
+// the CDC port and make room. Therefore, we have to store the exception and
+// print it as we exit the TinyUSB task.
+//
+// (Worse, a single TinyUSB task can process multiple callbacks and therefore generate
+// multiple exceptions...)
+static void usbd_pend_exception(mp_obj_t exception) {
+    mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd));
+    assert(usbd != NULL);
+    if (usbd->num_pend_excs < MP_USBD_MAX_PEND_EXCS) {
+        usbd->pend_excs[usbd->num_pend_excs] = exception;
+    }
+    usbd->num_pend_excs++;
+}
+
+// Call a Python function from inside a TinyUSB callback.
+//
+// Handles any exception using usbd_pend_exception()
+static mp_obj_t usbd_callback_function_n(mp_obj_t fun, size_t n_args, const mp_obj_t *args) {
+    nlr_buf_t nlr;
+    if (nlr_push(&nlr) == 0) {
+        mp_obj_t ret = mp_call_function_n_kw(fun, n_args, 0, args);
+        nlr_pop();
+        return ret;
+    } else {
+        usbd_pend_exception(MP_OBJ_FROM_PTR(nlr.ret_val));
+        return MP_OBJ_NULL;
+    }
+}
+
+// Return a pointer to the data inside a Python buffer provided in a callback
+static void *usbd_get_buffer_in_cb(mp_obj_t obj, mp_uint_t flags) {
+    mp_buffer_info_t buf_info;
+    if (obj == mp_const_none) {
+        // This is only if the user somehow
+        return NULL;
+    } else if (mp_get_buffer(obj, &buf_info, flags)) {
+        return buf_info.buf;
+    } else {
+        mp_obj_t exc = mp_obj_new_exception_msg(&mp_type_TypeError,
+            MP_ERROR_TEXT("object with buffer protocol required"));
+        usbd_pend_exception(exc);
+        return NULL;
+    }
+}
+
+const uint8_t *tud_descriptor_device_cb(void) {
+    mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd));
+    const void *result = NULL;
+    if (usbd) {
+        result = usbd_get_buffer_in_cb(usbd->desc_dev, MP_BUFFER_READ);
+    }
+    return result ? result : &mp_usbd_builtin_desc_dev;
+}
+
+const uint8_t *tud_descriptor_configuration_cb(uint8_t index) {
+    (void)index;
+    mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd));
+    const void *result = NULL;
+    if (usbd) {
+        result = usbd_get_buffer_in_cb(usbd->desc_cfg, MP_BUFFER_READ);
+    }
+    return result ? result : &mp_usbd_builtin_desc_cfg;
+}
+
+#if (CFG_TUD_MAX_SPEED == OPT_MODE_HIGH_SPEED)
+uint8_t const *tud_descriptor_device_qualifier_cb(void) {
+    return (const uint8_t *)&mp_usbd_builtin_desc_qual;
+}
+#endif
+
+const char *mp_usbd_runtime_string_cb(uint8_t index) {
+    mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd));
+    nlr_buf_t nlr;
+
+    if (usbd == NULL || usbd->desc_strs == mp_const_none) {
+        return NULL;
+    }
+
+    if (nlr_push(&nlr) == 0) {
+        mp_obj_t res = mp_obj_subscr(usbd->desc_strs, mp_obj_new_int(index), MP_OBJ_SENTINEL);
+        nlr_pop();
+        if (res != mp_const_none) {
+            return usbd_get_buffer_in_cb(res, MP_BUFFER_READ);
+        }
+    } else {
+        mp_obj_t exception = MP_OBJ_FROM_PTR(nlr.ret_val);
+        if (!(mp_obj_is_type(exception, &mp_type_KeyError) || mp_obj_is_type(exception, &mp_type_IndexError))) {
+            // Don't print KeyError or IndexError, allowing dicts or lists to have missing entries.
+            // but log any more exotic errors that pop up
+            usbd_pend_exception(exception);
+        }
+    }
+
+    return NULL;
+}
+
+bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) {
+    return false; // Currently no support for Vendor control transfers on the Python side
+}
+
+// Generic "runtime device" TinyUSB class driver, delegates everything to Python callbacks
+
+static void runtime_dev_init(void) {
+}
+
+static void runtime_dev_reset(uint8_t rhport) {
+    mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd));
+    if (!usbd) {
+        return;
+    }
+
+    for (int epnum = 0; epnum < CFG_TUD_ENDPPOINT_MAX; epnum++) {
+        for (int dir = 0; dir < 2; dir++) {
+            usbd->xfer_data[epnum][dir] = mp_const_none;
+        }
+    }
+
+    if (mp_obj_is_callable(usbd->reset_cb)) {
+        usbd_callback_function_n(usbd->reset_cb, 0, NULL);
+    }
+}
+
+// Calculate how many interfaces TinyUSB expects us to claim from
+// driver open().
+//
+// Annoyingly, the calling function (process_set_config() in TinyUSB) knows
+// this but doesn't pass the information to us.
+//
+// The answer is:
+// - If an Interface Association Descriptor (IAD) is immediately before itf_desc
+//   in the configuration descriptor, then claim all of the associated interfaces.
+// - Otherwise, claim exactly one interface
+//
+// Relying on the implementation detail that itf_desc is a pointer inside the
+// tud_descriptor_configuration_cb() result. Therefore, we can iterate through
+// from the beginning to check for an IAD immediately preceding it.
+//
+// Returns the number of associated interfaces to claim.
+static uint8_t _runtime_dev_count_itfs(tusb_desc_interface_t const *itf_desc) {
+    const tusb_desc_configuration_t *cfg_desc = (const void *)tud_descriptor_configuration_cb(0);
+    const uint8_t *p_desc = (const void *)cfg_desc;
+    const uint8_t *p_end = p_desc + cfg_desc->wTotalLength;
+    assert(p_desc <= (const uint8_t *)itf_desc && (const uint8_t *)itf_desc < p_end);
+    while (p_desc != (const void *)itf_desc && p_desc < p_end) {
+        const uint8_t *next = tu_desc_next(p_desc);
+
+        if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE_ASSOCIATION
+            && next == (const void *)itf_desc) {
+            const tusb_desc_interface_assoc_t *desc_iad = (const void *)p_desc;
+            return desc_iad->bInterfaceCount;
+        }
+        p_desc = next;
+    }
+    return 1; // No IAD found
+}
+
+// Scan the other descriptors after these interface(s) to find the total associated length to claim
+// from driver open().
+//
+// Total number of interfaces to scan for is assoc_itf_count.
+//
+// Opens any associated endpoints so they can have transfers submitted against them.
+//
+// Returns the total number of descriptor bytes to claim.
+static uint16_t _runtime_dev_claim_itfs(tusb_desc_interface_t const *itf_desc, uint8_t assoc_itf_count, uint16_t max_len) {
+    const uint8_t *p_desc = (const void *)itf_desc;
+    const uint8_t *p_end = p_desc + max_len;
+    while (p_desc < p_end) {
+        if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) {
+            if (assoc_itf_count > 0) {
+                // Claim this interface descriptor
+                assoc_itf_count--;
+            } else {
+                // This is the end of the previous interface's associated descriptors
+                break;
+            }
+        } else if (tu_desc_type(p_desc) == TUSB_DESC_ENDPOINT) {
+            // Open any endpoints that we come across
+            if (tu_desc_type(p_desc) == TUSB_DESC_ENDPOINT) {
+                bool r = usbd_edpt_open(RHPORT, (const void *)p_desc);
+                if (!r) {
+                    mp_obj_t exc = mp_obj_new_exception_arg1(&mp_type_OSError, MP_OBJ_NEW_SMALL_INT(MP_ENODEV));
+                    usbd_pend_exception(exc);
+                    break;
+                }
+            }
+        }
+        p_desc = tu_desc_next(p_desc);
+    }
+    return p_desc - (const uint8_t *)itf_desc;
+}
+
+// TinyUSB "Application driver" open callback. Called when the USB host sets
+// configuration. Returns number of bytes to claim from descriptors pointed to
+// by itf_desc.
+//
+// This is a little fiddly as it's called before any built-in TinyUSB drivers,
+// but we don't want to override those.
+//
+// Also, TinyUSB expects us to know how many interfaces to claim for each time
+// this function is called, and will behave unexpectedly if we claim the wrong
+// number of interfaces. However, unlike a "normal" USB driver we don't know at
+// compile time how many interfaces we've implemented. Instead, we have to look
+// back through the configuration descriptor to figure this out.
+static uint16_t runtime_dev_open(uint8_t rhport, tusb_desc_interface_t const *itf_desc, uint16_t max_len) {
+    mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd));
+
+    // Runtime USB isn't initialised
+    if (!usbd) {
+        return 0;
+    }
+
+    // If TinyUSB built-in drivers are enabled, don't claim any interface in the built-in range
+    #if USBD_ITF_BUILTIN_MAX > 0
+    if (mp_usb_device_builtin_enabled(usbd) && itf_desc->bInterfaceNumber < USBD_ITF_BUILTIN_MAX) {
+        return 0;
+    }
+    #endif
+
+    // Determine the total descriptor length of the interface(s) we are going to claim
+    uint8_t assoc_itf_count = _runtime_dev_count_itfs(itf_desc);
+    uint16_t claim_len = _runtime_dev_claim_itfs(itf_desc, assoc_itf_count, max_len);
+
+    // Call the Python callback to allow the driver to start working with these interface(s)
+
+    if (mp_obj_is_callable(usbd->open_itf_cb)) {
+        // Repurpose the control_data memoryview to point into itf_desc for this one call
+        usbd->control_data->items = (void *)itf_desc;
+        usbd->control_data->len = claim_len;
+        mp_obj_t args[] = { MP_OBJ_FROM_PTR(usbd->control_data) };
+        usbd_callback_function_n(usbd->open_itf_cb, 1, args);
+        usbd->control_data->len = 0;
+        usbd->control_data->items = NULL;
+    }
+
+    return claim_len;
+}
+
+static bool runtime_dev_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) {
+    mp_obj_t cb_res = mp_const_false;
+    mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd));
+    tusb_dir_t dir = request->bmRequestType_bit.direction;
+    mp_buffer_info_t buf_info;
+    bool result;
+
+    if (!usbd) {
+        return false;
+    }
+
+    if (mp_obj_is_callable(usbd->control_xfer_cb)) {
+        usbd->control_data->items = (void *)request;
+        usbd->control_data->len = sizeof(tusb_control_request_t);
+        mp_obj_t args[] = {
+            mp_obj_new_int(stage),
+            MP_OBJ_FROM_PTR(usbd->control_data),
+        };
+        cb_res = usbd_callback_function_n(usbd->control_xfer_cb, MP_ARRAY_SIZE(args), args);
+        usbd->control_data->items = NULL;
+        usbd->control_data->len = 0;
+
+        if (cb_res == MP_OBJ_NULL) {
+            // Exception occurred in the callback handler, stall this transfer
+            cb_res = mp_const_false;
+        }
+    }
+
+    // Check if callback returned any data to submit
+    if (mp_get_buffer(cb_res, &buf_info, dir == TUSB_DIR_IN ? MP_BUFFER_READ : MP_BUFFER_RW)) {
+        result = tud_control_xfer(RHPORT,
+            request,
+            buf_info.buf,
+            buf_info.len);
+
+        if (result) {
+            // Keep buffer object alive until the transfer completes
+            usbd->xfer_data[0][dir] = cb_res;
+        }
+    } else {
+        // Expect True or False to stall or continue
+        result = mp_obj_is_true(cb_res);
+
+        if (stage == CONTROL_STAGE_SETUP && result) {
+            // If no additional data but callback says to continue transfer then
+            // queue a status response.
+            tud_control_status(rhport, request);
+        } else if (stage == CONTROL_STAGE_ACK) {
+            // Allow data to be GCed once it's no longer in use
+            usbd->xfer_data[0][dir] = mp_const_none;
+        }
+    }
+
+    return result;
+}
+
+static bool runtime_dev_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) {
+    mp_obj_t ep = mp_obj_new_int(ep_addr);
+    mp_obj_t cb_res = mp_const_false;
+    mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd));
+    if (!usbd) {
+        return false;
+    }
+
+    if (mp_obj_is_callable(usbd->xfer_cb)) {
+        mp_obj_t args[] = {
+            ep,
+            MP_OBJ_NEW_SMALL_INT(result),
+            MP_OBJ_NEW_SMALL_INT(xferred_bytes),
+        };
+        cb_res = usbd_callback_function_n(usbd->xfer_cb, MP_ARRAY_SIZE(args), args);
+    }
+
+    // Clear any xfer_data for this endpoint
+    usbd->xfer_data[tu_edpt_number(ep_addr)][tu_edpt_dir(ep_addr)] = mp_const_none;
+
+    return cb_res != MP_OBJ_NULL && mp_obj_is_true(cb_res);
+}
+
+static usbd_class_driver_t const _runtime_dev_driver =
+{
+    #if CFG_TUSB_DEBUG >= 2
+    .name = "runtime_dev",
+    #endif
+    .init = runtime_dev_init,
+    .reset = runtime_dev_reset,
+    .open = runtime_dev_open,
+    .control_xfer_cb = runtime_dev_control_xfer_cb,
+    .xfer_cb = runtime_dev_xfer_cb,
+    .sof = NULL
+};
+
+usbd_class_driver_t const *usbd_app_driver_get_cb(uint8_t *driver_count) {
+    *driver_count = 1;
+    return &_runtime_dev_driver;
+}
+
+
+// Functions below here (named mp_usbd_xyz) apply to the whole TinyUSB C-based subsystem
+// and not necessarily the USBD singleton object (named usbd_xyz).
+
+// To support soft reset clearing USB runtime state, we manage three TinyUSB states:
+//
+// - "Not initialised" - tusb_inited() returns false, no USB at all). Only way
+//   back to this state is hard reset.
+//
+// - "Activated" - tusb_inited() returns true, USB device "connected" at device
+//   end and available to host.
+//
+// - "Deactivated" - tusb_inited() returns true, but USB device "disconnected"
+//   at device end and host can't see it.
+
+
+// Top-level USB device subsystem init.
+//
+// Initialises TinyUSB and/or re-activates it, provided USB is needed.
+//
+// This is called on any soft reset after boot.py runs, or on demand if the
+// user activates USB and it hasn't activated yet.
+void mp_usbd_init(void) {
+    mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd));
+    bool need_usb;
+
+    if (usbd == NULL) {
+        // No runtime USB device
+        #if CFG_TUD_CDC || CFG_TUD_MSC
+        // Builtin  drivers are available, so initialise as defaults
+        need_usb = true;
+        #else
+        // No builtin drivers, nothing to initialise
+        need_usb = false;
+        #endif
+    } else {
+        // Otherwise, initialise based on whether runtime USB is active
+        need_usb = usbd->active;
+    }
+
+    if (need_usb) {
+        // Call any port-specific initialization code.
+        MICROPY_HW_TINYUSB_LL_INIT();
+        // The following will call tusb_init(), which is safe to call redundantly.
+        mp_usbd_init_tud();
+        // Reconnect if mp_usbd_deinit() has disconnected.
+        tud_connect();
+    }
+}
+
+// Top-level USB device deinit.
+//
+// This variant is called from soft reset, NULLs out the USB device
+// singleton instance from MP_STATE_VM, and disconnects the port if a
+// runtime device was active.
+void mp_usbd_deinit(void) {
+    mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd));
+    MP_STATE_VM(usbd) = MP_OBJ_NULL;
+    if (usbd && usbd->active) {
+        // Disconnect if a runtime USB device was active
+        mp_usbd_disconnect(usbd);
+    }
+}
+
+// Thin wrapper around tud_disconnect() that tells TinyUSB all endpoints
+// have stalled, to prevent it getting confused if a transfer is in progress.
+static void mp_usbd_disconnect(mp_obj_usb_device_t *usbd) {
+    if (!tusb_inited()) {
+        return; // TinyUSB hasn't initialised
+    }
+
+    if (usbd) {
+        // There might be USB transfers in progress right now, so need to stall any live
+        // endpoints
+        //
+        // TODO: figure out if we really need this
+        for (int epnum = 0; epnum < CFG_TUD_ENDPPOINT_MAX; epnum++) {
+            for (int dir = 0; dir < 2; dir++) {
+                if (usbd->xfer_data[epnum][dir] != mp_const_none) {
+                    usbd_edpt_stall(RHPORT, tu_edpt_addr(epnum, dir));
+                    usbd->xfer_data[epnum][dir] = mp_const_none;
+                }
+            }
+        }
+    }
+
+    #if MICROPY_HW_USB_CDC
+    // Ensure no pending static CDC writes, as these can cause TinyUSB to crash
+    tud_cdc_write_clear();
+    // Prevent cdc write flush from initiating any new transfers while disconnecting
+    usbd_edpt_stall(RHPORT, USBD_CDC_EP_IN);
+    #endif
+
+    bool was_connected = tud_connected();
+    tud_disconnect();
+    if (was_connected) {
+        // Need to ensure a long enough delay before TinyUSB re-connects that
+        // the host triggers a bus reset. This may happen anyway, but delaying here
+        // lets us be "belt and braces" sure.
+        mp_hal_delay_ms(50);
+    }
+}
+
+// Thjs callback is queued by mp_usbd_schedule_task() to process USB later.
+void mp_usbd_task_callback(mp_sched_node_t *node) {
+    if (tud_inited() && !in_usbd_task) {
+        mp_usbd_task_inner();
+    }
+    // If in_usbd_task is set, it means something else has already manually called
+    // mp_usbd_task() (most likely: C-based USB-CDC serial port). Now the MP
+    // scheduler is running inside there and triggering this callback. It's OK
+    // to skip, the already-running outer TinyUSB task will process all pending
+    // events before it returns.
+}
+
+// Task function can be called manually to force processing of USB events
+// (mostly from USB-CDC serial port when blocking.)
+void mp_usbd_task(void) {
+    #if MICROPY_PY_THREAD && !MICROPY_PY_THREAD_GIL
+    if (!mp_thread_is_main_thread()) {
+        // Avoid race with the scheduler callback by scheduling TinyUSB to run
+        // on the main thread.
+        mp_usbd_schedule_task();
+        return;
+    }
+    #endif
+
+    if (in_usbd_task) {
+        // If this exception triggers, it means a USB callback tried to do
+        // something that itself became blocked on TinyUSB (most likely: read or
+        // write from a C-based USB-CDC serial port.)
+        mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("TinyUSB callback can't recurse"));
+    }
+
+    mp_usbd_task_inner();
+}
+
+static void mp_usbd_task_inner(void) {
+    in_usbd_task = true;
+
+    tud_task_ext(0, false);
+
+    mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd));
+
+    // Check for a triggered change to/from active state
+    if (usbd && usbd->trigger) {
+        if (usbd->active) {
+            if (tud_connected()) {
+                // If a SETUP packet has been received, first disconnect
+                // and wait for the host to recognise this and trigger a bus reset.
+                //
+                // Effectively this forces it to re-enumerate the device.
+                mp_usbd_disconnect(usbd);
+            }
+            tud_connect();
+        } else {
+            mp_usbd_disconnect(usbd);
+        }
+        usbd->trigger = false;
+    }
+
+    in_usbd_task = false;
+
+    if (usbd) {
+        // Print any exceptions that were raised by Python callbacks
+        // inside tud_task_ext(). See usbd_callback_function_n.
+
+        // As printing exceptions to USB-CDC may recursively call mp_usbd_task(),
+        // first copy out the pending data to the local stack
+        mp_uint_t num_pend_excs = usbd->num_pend_excs;
+        mp_obj_t pend_excs[MP_USBD_MAX_PEND_EXCS];
+        for (mp_uint_t i = 0; i < MIN(MP_USBD_MAX_PEND_EXCS, num_pend_excs); i++) {
+            pend_excs[i] = usbd->pend_excs[i];
+            usbd->pend_excs[i] = mp_const_none;
+        }
+        usbd->num_pend_excs = 0;
+
+        // Now print the exceptions stored from this mp_usbd_task() call
+        for (mp_uint_t i = 0; i < MIN(MP_USBD_MAX_PEND_EXCS, num_pend_excs); i++) {
+            mp_obj_print_exception(&mp_plat_print, pend_excs[i]);
+        }
+        if (num_pend_excs > MP_USBD_MAX_PEND_EXCS) {
+            mp_printf(&mp_plat_print, "%u additional exceptions in USB callbacks\n",
+                num_pend_excs - MP_USBD_MAX_PEND_EXCS);
+        }
+    }
+}
+
+#endif // MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
diff --git a/shared/tinyusb/tusb_config.h b/shared/tinyusb/tusb_config.h
index 28bee09a594..5432b9ca22f 100644
--- a/shared/tinyusb/tusb_config.h
+++ b/shared/tinyusb/tusb_config.h
@@ -31,6 +31,10 @@
 
 #if MICROPY_HW_ENABLE_USBDEV
 
+#ifndef MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
+#define MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE 0
+#endif
+
 #ifndef MICROPY_HW_USB_MANUFACTURER_STRING
 #define MICROPY_HW_USB_MANUFACTURER_STRING "MicroPython"
 #endif
@@ -43,7 +47,21 @@
 #define MICROPY_HW_USB_CDC_INTERFACE_STRING "Board CDC"
 #endif
 
+#ifndef MICROPY_HW_USB_MSC_INQUIRY_VENDOR_STRING
+#define MICROPY_HW_USB_MSC_INQUIRY_VENDOR_STRING "MicroPy"
+#endif
+
+#ifndef MICROPY_HW_USB_MSC_INQUIRY_PRODUCT_STRING
+#define MICROPY_HW_USB_MSC_INQUIRY_PRODUCT_STRING "Mass Storage"
+#endif
+
+#ifndef MICROPY_HW_USB_MSC_INQUIRY_REVISION_STRING
+#define MICROPY_HW_USB_MSC_INQUIRY_REVISION_STRING "1.00"
+#endif
+
+#if !defined(CFG_TUSB_RHPORT0_MODE) && !defined(CFG_TUSB_RHPORT1_MODE)
 #define CFG_TUSB_RHPORT0_MODE   (OPT_MODE_DEVICE)
+#endif
 
 #if MICROPY_HW_USB_CDC
 #define CFG_TUD_CDC             (1)
@@ -59,9 +77,14 @@
 
 // CDC Configuration
 #if CFG_TUD_CDC
-#define CFG_TUD_CDC_RX_BUFSIZE  (256)
-#define CFG_TUD_CDC_TX_BUFSIZE  (256)
+#ifndef CFG_TUD_CDC_RX_BUFSIZE
+#define CFG_TUD_CDC_RX_BUFSIZE  ((CFG_TUD_MAX_SPEED == OPT_MODE_HIGH_SPEED) ? 512 : 256)
 #endif
+#ifndef CFG_TUD_CDC_TX_BUFSIZE
+#define CFG_TUD_CDC_TX_BUFSIZE  ((CFG_TUD_MAX_SPEED == OPT_MODE_HIGH_SPEED) ? 512 : 256)
+#endif
+#define CFG_TUD_CDC_TX_PERSISTENT (1)
+#endif // CFG_TUD_CDC
 
 // MSC Configuration
 #if CFG_TUD_MSC
@@ -70,62 +93,122 @@
 #endif
 // Set MSC EP buffer size to FatFS block size to avoid partial read/writes (offset arg).
 #define CFG_TUD_MSC_BUFSIZE (MICROPY_FATFS_MAX_SS)
-#endif
+#endif // CFG_TUD_MSC
 
-// Define static descriptor size and interface count based on the above config
+#if MICROPY_PY_NETWORK_USBD_NCM
+// TinyUSB supports both RNDIS and NCM protocols.
+// NCM is now recommended by Microsoft on Win 11 and above.
+#define CFG_TUD_NCM             (1)
 
-#define USBD_STATIC_DESC_LEN (TUD_CONFIG_DESC_LEN +                     \
-    (CFG_TUD_CDC ? (TUD_CDC_DESC_LEN) : 0) +  \
-    (CFG_TUD_MSC ? (TUD_MSC_DESC_LEN) : 0)    \
-    )
+// NCM TinyUSB buffer configuration.
+// These depend on lwIP parameters (TCP_MSS, LWIP_MDNS_RESPONDER); lwipopts.h
+// is safe to include here because MICROPY_PY_NETWORK_USBD_NCM implies LWIP.
+#include "lwipopts.h"
 
-#define USBD_STR_0 (0x00)
-#define USBD_STR_MANUF (0x01)
-#define USBD_STR_PRODUCT (0x02)
-#define USBD_STR_SERIAL (0x03)
-#define USBD_STR_CDC (0x04)
-#define USBD_STR_MSC (0x05)
+// Must be >> MTU; can be set to 2048 without impact.
+#ifndef CFG_TUD_NCM_IN_NTB_MAX_SIZE
+#define CFG_TUD_NCM_IN_NTB_MAX_SIZE  ((2 + LWIP_MDNS_RESPONDER) * TCP_MSS + 100)
+#endif
+#ifndef CFG_TUD_NCM_OUT_NTB_MAX_SIZE
+#define CFG_TUD_NCM_OUT_NTB_MAX_SIZE ((2 + LWIP_MDNS_RESPONDER) * TCP_MSS + 100)
+#endif
+
+// Number of NCM transfer blocks for reception side.
+#ifndef CFG_TUD_NCM_OUT_NTB_N
+#define CFG_TUD_NCM_OUT_NTB_N 1
+#endif
+// Number of NCM transfer blocks for transmission side.
+#ifndef CFG_TUD_NCM_IN_NTB_N
+#define CFG_TUD_NCM_IN_NTB_N  (2 + LWIP_MDNS_RESPONDER)
+#endif
+
+#ifndef MICROPY_PY_NETWORK_USBD_NCM_INTERFACE_STRING
+#define MICROPY_PY_NETWORK_USBD_NCM_INTERFACE_STRING "Board NET"
+#endif
+#endif // MICROPY_PY_NETWORK_USBD_NCM
+
+// Define built-in interface, string and endpoint numbering based on the above config
+
+enum _USBD_STR {
+    USBD_STR_0 = (0x00),
+    USBD_STR_MANUF = (0x01),
+    USBD_STR_PRODUCT = (0x02),
+    USBD_STR_SERIAL = (0x03),
+    #if CFG_TUD_CDC
+    USBD_STR_CDC,
+    #endif
+    #if CFG_TUD_MSC
+    USBD_STR_MSC,
+    #endif
+    #if CFG_TUD_NCM
+    USBD_STR_NCM,
+    USBD_STR_NCM_MAC,
+    #endif
+    // One more than the highest built-in string index; runtime device string
+    // indexes should start at or above this value to avoid collisions.
+    USBD_STR_BUILTIN_MAX,
+};
 
 #define USBD_MAX_POWER_MA (250)
 
 #ifndef MICROPY_HW_USB_DESC_STR_MAX
-#define MICROPY_HW_USB_DESC_STR_MAX (20)
+#define MICROPY_HW_USB_DESC_STR_MAX (40)
 #endif
 
+enum _USBD_ITF {
+    #if CFG_TUD_CDC
+    USBD_ITF_CDC,
+    USBD_ITF_CDC_I2,
+    #endif // CFG_TUD_CDC
+    #if CFG_TUD_MSC
+    USBD_ITF_MSC,
+    #endif // CFG_TUD_MSC
+    #if CFG_TUD_NCM
+    USBD_ITF_NCM,
+    USBD_ITF_NCM_DATA,
+    #endif
+};
+
+enum _USBD_EP {
+    USBD_EP_BASE = 0x80,
+    #if CFG_TUD_CDC
+    USBD_CDC_EP_CMD,
+    USBD_CDC_EP_IN,
+    #endif // CFG_TUD_CDC
+    #if CFG_TUD_MSC
+    USBD_MSC_EP_IN,
+    #endif // CFG_TUD_MSC
+    #if CFG_TUD_NCM
+    USBD_NCM_EP_CMD,
+    USBD_NCM_EP_IN,
+    #endif // CFG_TUD_NCM
+};
+
+// define the matching in endpoints to each EP_OUT
 #if CFG_TUD_CDC
-#define USBD_ITF_CDC (0) // needs 2 interfaces
-#define USBD_CDC_EP_CMD (0x81)
-#define USBD_CDC_EP_OUT (0x02)
-#define USBD_CDC_EP_IN (0x82)
-#endif // CFG_TUD_CDC
-
-#if CFG_TUD_MSC
-// Interface & Endpoint numbers for MSC come after CDC, if it is enabled
-#if CFG_TUD_CDC
-#define USBD_ITF_MSC (2)
-#define EPNUM_MSC_OUT (0x03)
-#define EPNUM_MSC_IN (0x83)
-#else
-#define USBD_ITF_MSC (0)
-#define EPNUM_MSC_OUT (0x01)
-#define EPNUM_MSC_IN (0x81)
-#endif // CFG_TUD_CDC
-#endif // CFG_TUD_MSC
-
-/* Limits of statically defined USB interfaces, endpoints, strings */
+#define USBD_CDC_EP_OUT  (USBD_CDC_EP_IN & ~TUSB_DIR_IN_MASK)
+#endif
 #if CFG_TUD_MSC
-#define USBD_ITF_STATIC_MAX (USBD_ITF_MSC + 1)
-#define USBD_STR_STATIC_MAX (USBD_STR_MSC + 1)
-#define USBD_EP_STATIC_MAX (EPNUM_MSC_OUT + 1)
-#elif CFG_TUD_CDC
-#define USBD_ITF_STATIC_MAX (USBD_ITF_CDC + 2)
-#define USBD_STR_STATIC_MAX (USBD_STR_CDC + 1)
-#define USBD_EP_STATIC_MAX (((EPNUM_CDC_EP_IN)&~TUSB_DIR_IN_MASK) + 1)
-#else // !CFG_TUD_MSC && !CFG_TUD_CDC
-#define USBD_ITF_STATIC_MAX (0)
-#define USBD_STR_STATIC_MAX (0)
-#define USBD_EP_STATIC_MAX (0)
+#define USBD_MSC_EP_OUT  (USBD_MSC_EP_IN & ~TUSB_DIR_IN_MASK)
 #endif
+#if CFG_TUD_NCM
+#define USBD_NCM_EP_OUT  (USBD_NCM_EP_IN & ~TUSB_DIR_IN_MASK)
+#endif
+
+
+/* Limits of builtin USB interfaces, endpoints, strings */
+// Number of interfaces used by all enabled classes
+#define USBD_ITF_BUILTIN_MAX ( \
+    (CFG_TUD_CDC ? 2 : 0) + \
+    (CFG_TUD_MSC ? 1 : 0) + \
+    (CFG_TUD_NCM ? 2 : 0))
+
+// 1 plus the number of interfaces used by all enabled classes
+#define USBD_EP_BUILTIN_MAX ( \
+    (CFG_TUD_CDC ? 2 : 0) + \
+    (CFG_TUD_MSC ? 1 : 0) + \
+    (CFG_TUD_NCM ? 2 : 0) + \
+    1)
 
 #endif // MICROPY_HW_ENABLE_USBDEV
 
diff --git a/shared/upytesthelper/upytesthelper.c b/shared/upytesthelper/upytesthelper.c
deleted file mode 100644
index 12fa8276b0b..00000000000
--- a/shared/upytesthelper/upytesthelper.c
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * This file is part of the MicroPython project, http://micropython.org/
- *
- * The MIT License (MIT)
- *
- * Copyright (c) 2017 Linaro Limited
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-#include 
-
-#include "py/mphal.h"
-#include "py/gc.h"
-#include "py/runtime.h"
-#include "py/compile.h"
-#include "upytesthelper.h"
-
-static const char *test_exp_output;
-static int test_exp_output_len, test_rem_output_len;
-static int test_failed;
-static void *heap_start, *heap_end;
-
-void upytest_set_heap(void *start, void *end) {
-    heap_start = start;
-    heap_end = end;
-}
-
-void upytest_set_expected_output(const char *output, unsigned len) {
-    test_exp_output = output;
-    test_exp_output_len = test_rem_output_len = len;
-    test_failed = false;
-}
-
-bool upytest_is_failed(void) {
-    if (test_failed) {
-        return true;
-    }
-    #if 0
-    if (test_rem_output_len != 0) {
-        printf("remaining len: %d\n", test_rem_output_len);
-    }
-    #endif
-    return test_rem_output_len != 0;
-}
-
-// MP_PLAT_PRINT_STRN() should be redirected to this function.
-// It will pass-through any content to mp_hal_stdout_tx_strn_cooked()
-// (the default value of MP_PLAT_PRINT_STRN), but will also match
-// it to the expected output as set by upytest_set_expected_output().
-// If mismatch happens, upytest_is_failed() returns true.
-void upytest_output(const char *str, mp_uint_t len) {
-    if (!test_failed) {
-        if (len > test_rem_output_len) {
-            test_failed = true;
-        } else {
-            test_failed = memcmp(test_exp_output, str, len);
-            #if 0
-            if (test_failed) {
-                printf("failed after char %u, within %d chars, res: %d\n",
-                    test_exp_output_len - test_rem_output_len, (int)len, test_failed);
-                for (int i = 0; i < len; i++) {
-                    if (str[i] != test_exp_output[i]) {
-                        printf("%d %02x %02x\n", i, str[i], test_exp_output[i]);
-                    }
-                }
-            }
-            #endif
-            test_exp_output += len;
-            test_rem_output_len -= len;
-        }
-    }
-    mp_hal_stdout_tx_strn_cooked(str, len);
-}
-
-void upytest_execute_test(const char *src) {
-    // To provide clean room for each test, interpreter and heap are
-    // reinitialized before running each.
-    gc_init(heap_start, heap_end);
-    mp_init();
-    mp_obj_list_init(mp_sys_path, 0);
-    #if MICROPY_MODULE_FROZEN
-    mp_obj_list_append(mp_sys_path, MP_OBJ_NEW_QSTR(MP_QSTR__dot_frozen));
-    #endif
-    mp_obj_list_init(mp_sys_argv, 0);
-
-    nlr_buf_t nlr;
-    if (nlr_push(&nlr) == 0) {
-        mp_lexer_t *lex = mp_lexer_new_from_str_len(MP_QSTR__lt_stdin_gt_, src, strlen(src), 0);
-        qstr source_name = lex->source_name;
-        mp_parse_tree_t parse_tree = mp_parse(lex, MP_PARSE_FILE_INPUT);
-        mp_obj_t module_fun = mp_compile(&parse_tree, source_name, false);
-        mp_call_function_0(module_fun);
-        nlr_pop();
-    } else {
-        mp_obj_t exc = (mp_obj_t)nlr.ret_val;
-        if (mp_obj_is_subclass_fast(mp_obj_get_type(exc), &mp_type_SystemExit)) {
-            // Assume that sys.exit() is called to skip the test.
-            // TODO: That can be always true, we should set up convention to
-            // use specific exit code as skip indicator.
-            tinytest_set_test_skipped_();
-            goto end;
-        }
-        mp_obj_print_exception(&mp_plat_print, exc);
-        tt_abort_msg("Uncaught exception\n");
-    }
-
-    if (upytest_is_failed()) {
-        tinytest_set_test_failed_();
-    }
-
-end:
-    mp_deinit();
-}
diff --git a/shared/upytesthelper/upytesthelper.h b/shared/upytesthelper/upytesthelper.h
deleted file mode 100644
index 3a292befd77..00000000000
--- a/shared/upytesthelper/upytesthelper.h
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * This file is part of the MicroPython project, http://micropython.org/
- *
- * The MIT License (MIT)
- *
- * Copyright (c) 2017 Linaro Limited
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-#include 
-#include 
-#include "py/mpconfig.h"
-#include "lib/tinytest/tinytest.h"
-#include "lib/tinytest/tinytest_macros.h"
-
-void upytest_set_heap(void *start, void *end);
-void upytest_set_expected_output(const char *output, unsigned len);
-void upytest_execute_test(const char *src);
-void upytest_output(const char *str, mp_uint_t len);
-bool upytest_is_failed(void);
diff --git a/tests/README.md b/tests/README.md
index b0bff4872ed..4edd3e1088e 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -1,7 +1,57 @@
 # MicroPython Test Suite
 
-This directory contains tests for various functionality areas of MicroPython.
-To run all stable tests, run "run-tests.py" script in this directory.
+This directory contains tests for most parts of MicroPython.  To run it you will need
+CPython 3.8.2 or newer, which is used to validate MicroPython's behaviour.
+
+The tests are organized into several categories:
+- Unit and regression tests: tests for MicroPython's core functionality, including the
+  compiler, runtime, and built-in modules
+- perf_bench: performance benchmarks
+- internal_bench: internal performance benchmarks
+- Serial reliability and performance test
+- Test key/certificates
+- CPython vs MicroPython Differences
+
+## Unit and regression tests
+
+To run all stable tests, run the "run-tests.py" script in this directory.  By default
+that will run the test suite against the unix port of MicroPython.
+
+To run the test suite against a bare-metal target (a board running MicroPython firmware)
+use the `-t` option to specify the serial port.  This will automatically detect the
+target platform and run the appropriate set of tests for that platform.  For example:
+
+    $ ./run-tests.py -t /dev/ttyACM0
+
+That will run tests on the `/dev/ttyACM0` serial port.  You can also use shortcut
+device names like `a` for `/dev/ttyACM` and `c` for `COM`.  Use
+`./run-tests.py --help` to see all of the device possibilities, and other options.
+
+There are three kinds of tests:
+
+* Tests that use `unittest`: these tests require `unittest` to be installed on the
+  target (eg via `mpremote mip install unittest`), and are used to test things that are
+  MicroPython-specific, such as behaviour that is different to CPython, modules that
+  aren't available in CPython, and hardware tests.  These tests are run only under
+  MicroPython and the test passes if the `unittest` runner prints "OK" at the end of the
+  run.  Other output may be printed, eg for use as diagnostics, and this output does not
+  affect the result of the test.
+
+* Tests with a corresponding `.exp` file: similar to the `unittest` tests, these tests
+  are for features that generally cannot be run under CPython.  In this case the test is
+  run under MicroPython only and the output from MicroPython is compared against the
+  provided `.exp` file.  The test passes if the output matches exactly.
+
+* Tests without a corresponding `.exp` file (and don't use `unittest`): these tests are
+  used to test MicroPython behaviour that should precisely match CPython.  These tests
+  are first run under CPython and the output captured, and then run under MicroPython
+  and the output compared to the CPython output.  The test passes if the output matches
+  exactly.  If the output differs then the test fails and the outputs are saved in a
+  `.exp` and a `.out` file respectively.
+
+In all three cases above, the test can usually be run directly on the target MicroPython
+instance, either using the unix port with `micropython `, or on a board with
+`mpremote run `.  This is useful for creating and debugging tests.
 
 Tests of capabilities not supported on all platforms should be written
 to check for the capability being present. If it is not, the test
@@ -15,15 +65,6 @@ condition a test. The run-tests.py script uses small scripts in the
 feature_check directory to check whether each such feature is present,
 and skips the relevant tests if not.
 
-Tests are generally verified by running the test both in MicroPython and
-in CPython and comparing the outputs. If the output differs the test fails
-and the outputs are saved in a .out and a .exp file respectively.
-For tests that cannot be run in CPython, for example because they use
-the machine module, a .exp file can be provided next to the test's .py
-file. A convenient way to generate that is to run the test, let it fail
-(because CPython cannot run it) and then copy the .out file (but not
-before checking it manually!)
-
 When creating new tests, anything that relies on float support should go in the
 float/ subdirectory.  Anything that relies on import x, where x is not a built-in
 module, should go in the import/ subdirectory.
@@ -33,21 +74,19 @@ module, should go in the import/ subdirectory.
 The `perf_bench` directory contains some performance benchmarks that can be used
 to benchmark different MicroPython firmwares or host ports.
 
-The runner utility is `run-perfbench,py`. Execute `./run-perfbench.py --help`
+The runner utility is `run-perfbench.py`. Execute `./run-perfbench.py --help`
 for a full list of command line options.
 
 ### Benchmarking a target
 
-To run tests on a firmware target using `pyboard.py`, run the command line like
+To run tests on a firmware target using a serial port, run the command line like
 this:
 
 ```
-./run-perfbench.py -p -d /dev/ttyACM0 168 100
+./run-perfbench.py -t /dev/ttyACM0 168 100
 ```
 
-* `-p` indicates running on a remote target via pyboard.py, not the host.
-* `-d PORTNAME` is the serial port, `/dev/ttyACM0` is the default if not
-  provided.
+* `-t PORTNAME` is the serial port to use (and it supports shorthand like `a0`).
 * `168` is value `N`, the approximate CPU frequency in MHz (in this case Pyboard
   V1.1 is 168MHz). It's possible to choose other values as well: lower values
   like `10` will run much the tests much quicker, higher values like `1000` will
@@ -107,11 +146,11 @@ Usually you want to know if something is faster or slower than a reference. To
 do this, copy the output of each `run-perfbench.py` run to a text file.
 
 This can be done multiple ways, but one way on Linux/macOS is with the `tee`
-utility: `./run-perfbench.py -p 168 100 | tee pyb-run1.txt`
+utility: `./run-perfbench.py -t a0 168 100 | tee pyb-run1.txt`
 
 Once you have two files with output from two different runs (maybe with
 different code or configuration), compare the runtimes with `./run-perfbench.py
--t pybv-run1.txt pybv-run2.txt` or compare scores with `./run-perfbench.py -s
+-m pybv-run1.txt pybv-run2.txt` or compare scores with `./run-perfbench.py -s
 pybv-run1.txt pybv-run2.txt`:
 
 ```
@@ -147,3 +186,161 @@ the test runs, and the absolute difference value is unreliable. High error
 percentages are particularly common on PC builds, where the host OS may
 influence test run times. Increasing the `N` value may help average this out by
 running each test longer.
+
+## internal_bench
+
+The `internal_bench` directory contains a set of tests for benchmarking
+different internal Python features. By default, tests are run on the (unix or
+Windows) host, but the `--pyboard` option allows them to be run on an attached
+board instead.
+
+Tests are grouped by the first part of the file name, and the test runner compares
+output between each group of tests.
+
+The benchmarks measure the elapsed (wall time) for each test, according
+to MicroPython's own time module.
+
+If run without any arguments, all test groups are run. Otherwise, it's possible
+to manually specify which test cases to run.
+
+Example:
+
+```
+$ ./run-internalbench.py internal_bench/bytebuf-*.py
+internal_bench/bytebuf:
+    0.094s (+00.00%) internal_bench/bytebuf-1-inplace.py
+    0.471s (+399.24%) internal_bench/bytebuf-2-join_map_bytes.py
+    0.177s (+87.78%) internal_bench/bytebuf-3-bytarray_map.py
+1 tests performed (3 individual testcases)
+```
+
+## Serial reliability and performance test
+
+Serial port reliability and performance can be tested using the `serial_test.py` script.
+Pass the name of the port to test against, for example:
+
+    $ ./serial_test.py -t /dev/ttyACM0
+
+If no port is specified then `/dev/ttyACM0` is used as the default.
+
+The test will send data out to the target, and receive data from the target, in various
+chunk sizes.  The throughput of the serial connection will be reported for each sub-test.
+
+## Test key/certificates
+
+SSL/TLS tests in `multi_net` and `net_inet` use self-signed key/cert pairs
+that are randomly generated to be used for testing/demonstration only.
+
+To run tests on-device the `.der` files should be copied and the current time
+set to ensure certs validity. This can be done with:
+```
+$ mpremote rtc --set cp multi_net/*.der net_inet/*.der :
+```
+
+### Generating new test key/certificates
+
+The keys used for the unit tests are included in the tests folders so don't generally
+need to be re-created by end users. This section is included here for reference only.
+
+A new self-signed RSA key/cert pair can be created with openssl:
+```
+$ openssl req -x509 -newkey rsa:2048 -keyout rsa_key.pem -out rsa_cert.pem -days 3650 -nodes -subj '/CN=micropython.local/O=MicroPython/C=AU'
+```
+In this case CN is: micropython.local
+
+Convert them to DER format:
+```
+$ openssl pkey -in rsa_key.pem -out rsa_key.der -outform DER
+$ openssl x509 -in rsa_cert.pem -out rsa_cert.der -outform DER
+```
+
+For elliptic curve tests using key/cert pairs, create a key then a certificate using:
+```
+$ openssl ecparam -name prime256v1 -genkey -noout -out ec_key.pem
+$ openssl pkey -in ec_key.pem -out ec_key.der -outform DER
+$ openssl req -new -x509 -key ec_key.pem -out ec_cert.der -outform DER -days 3650 -nodes -subj '/CN=micropython.local/O=MicroPython/C=AU'
+```
+
+## CPython vs MicroPython Differences
+
+The `tests/cpydiff` folder contains test files that document and verify the differences
+between the CPython and MicroPython implementations.
+
+These tests are designed to:
+- Execute the same code on both CPython and MicroPython
+- Document behavioral differences between the two implementations
+- Generate documentation pages that help users understand these differences
+
+### How It Works
+
+1. Each test file contains Python code that demonstrates a specific difference.
+2. The tests are executed on both CPython and MicroPython.
+3. The output from both implementations is captured and compared. If the outputs are the
+   same, the generation will fail (because the outputs should be different).
+4. The results, along with metadata from the file docstrings, are used to generate
+   documentation.
+
+### Documentation Generation
+
+The documentation is automatically generated using:
+```
+tools/gen-cpydiff.py
+```
+
+This script:
+- Parses the docstring metadata from each test file
+- Runs the tests on both implementations
+- Combines the results to create comprehensive documentation pages
+- Outputs formatted reStructuredText documentation showing the differences
+
+**Note:** This script is automatically executed as part of the documentation publishing
+process when building the docs.
+
+### Test File Format
+
+The test filename should match the categories of the test. For example, a test with
+categories `Syntax,Operators` should be named `syntax_operators_*.py`.
+
+Each test file should include a docstring with the following format, and include a
+minimal code snippet that reproduces the difference when run on both CPython and the
+unix port of MicroPython:
+
+```python
+"""
+categories: Category,Subcategory
+description: Brief description of the difference being tested
+cause: Explanation of why this difference exists
+workaround: How to work around this difference (or "Unknown" if none)
+"""
+# Minimal Python code reproducing the difference
+import sys
+print(sys.implementation.name)
+```
+
+The categories and subcategories are used to organize the documentation into sections.
+Files with the same category and/or subcategory will be placed in the same section.
+
+Common categories include:
+- Syntax
+- Core (Core language)
+- Types (Builtin types)
+- Modules
+
+### Building the Documentation
+
+The documentation is automatically regenerated during the documentation build process.
+To manually regenerate after adding or modifying tests:
+
+1. Set environment variables if needed:
+   - `MICROPY_MICROPYTHON`: Path to MicroPython executable
+   - `MICROPY_CPYTHON3`: Path to CPython 3.x executable (default: "python3")
+
+2. Run the generation script from the project root:
+   ```
+   python tools/gen-cpydiff.py
+   ```
+
+The generated documentation will be placed in `docs/genrst/` as reStructuredText files.
+
+Also see `docs/README.md` for more information on building the documentation locally
+to validate the rendering of the resulting documentation page(s).
diff --git a/tests/assets/Makefile b/tests/assets/Makefile
new file mode 100644
index 00000000000..6048ee2a405
--- /dev/null
+++ b/tests/assets/Makefile
@@ -0,0 +1,29 @@
+MPY_DIR = ../..
+
+PYTHON := $(command -v python3 2> /dev/null)
+ifndef PYTHON
+PYTHON = python
+endif
+
+# Files created with dd if=/dev/urandom ... - the file name is the CRC32
+# of the data contained in the file itself.
+
+ROMFS_FILES = romfs_source/0x30d83fe5.bin \
+	      romfs_source/0x37bef0eb.bin \
+	      romfs_source/0x442f3b5f.bin \
+	      romfs_source/0x648793fb.bin \
+	      romfs_source/0x913837b6.bin \
+	      romfs_source/0xdb14aac7.bin \
+	      romfs_source/romfs_sentinel.txt
+
+.PHONY: romfs
+romfs: all
+
+.PHONY: clean
+clean:
+	rm -f random_romfs.bin
+
+all: random_romfs.bin
+
+random_romfs.bin: $(ROMFS_FILES)
+	$(PYTHON) $(MPY_DIR)/tools/mpremote/mpremote.py romfs --partition 0 --no-mpy --output $@ build romfs_source
diff --git a/tests/assets/README.md b/tests/assets/README.md
new file mode 100644
index 00000000000..a5f935ee1b9
--- /dev/null
+++ b/tests/assets/README.md
@@ -0,0 +1,8 @@
+This directory contains assets for other tests to use:
+
+- A .mpy built against the current .mpy version that can be used to test
+  freezing without a dependency on mpy-cross (`frozentest.py` and
+  `frozentest.mpy`)
+- A ROMFS image containing random binary files for testing mounting and reading
+  from the partition without having to generate a custom image
+  (`random_romfs.bin`, the `romfs_source` directory, and `Makefile`)
diff --git a/tests/assets/frozentest.mpy b/tests/assets/frozentest.mpy
new file mode 100644
index 00000000000..ccdf5e1a957
Binary files /dev/null and b/tests/assets/frozentest.mpy differ
diff --git a/tests/frozen/frozentest.py b/tests/assets/frozentest.py
similarity index 90%
rename from tests/frozen/frozentest.py
rename to tests/assets/frozentest.py
index 78cdd60bf04..74bdfb3b267 100644
--- a/tests/frozen/frozentest.py
+++ b/tests/assets/frozentest.py
@@ -1,4 +1,4 @@
-print("uPy")
+print("interned")
 print("a long string that is not interned")
 print("a string that has unicode αβγ chars")
 print(b"bytes 1234\x01")
diff --git a/tests/assets/random_romfs.bin b/tests/assets/random_romfs.bin
new file mode 100644
index 00000000000..387bd7ec476
Binary files /dev/null and b/tests/assets/random_romfs.bin differ
diff --git a/tests/assets/romfs_source/0x30d83fe5.bin b/tests/assets/romfs_source/0x30d83fe5.bin
new file mode 100644
index 00000000000..625e51fb8ae
Binary files /dev/null and b/tests/assets/romfs_source/0x30d83fe5.bin differ
diff --git a/tests/assets/romfs_source/0x37bef0eb.bin b/tests/assets/romfs_source/0x37bef0eb.bin
new file mode 100644
index 00000000000..11e1c67871c
Binary files /dev/null and b/tests/assets/romfs_source/0x37bef0eb.bin differ
diff --git a/tests/assets/romfs_source/0x442f3b5f.bin b/tests/assets/romfs_source/0x442f3b5f.bin
new file mode 100644
index 00000000000..ffe9f8f0e2c
Binary files /dev/null and b/tests/assets/romfs_source/0x442f3b5f.bin differ
diff --git a/tests/assets/romfs_source/0x648793fb.bin b/tests/assets/romfs_source/0x648793fb.bin
new file mode 100644
index 00000000000..f99b58c610a
Binary files /dev/null and b/tests/assets/romfs_source/0x648793fb.bin differ
diff --git a/tests/assets/romfs_source/0x913837b6.bin b/tests/assets/romfs_source/0x913837b6.bin
new file mode 100644
index 00000000000..6511184346a
Binary files /dev/null and b/tests/assets/romfs_source/0x913837b6.bin differ
diff --git a/tests/assets/romfs_source/0xdb14aac7.bin b/tests/assets/romfs_source/0xdb14aac7.bin
new file mode 100644
index 00000000000..51754d4ecd8
Binary files /dev/null and b/tests/assets/romfs_source/0xdb14aac7.bin differ
diff --git a/tests/assets/romfs_source/romfs_sentinel.txt b/tests/assets/romfs_source/romfs_sentinel.txt
new file mode 100644
index 00000000000..6ff9311d70e
--- /dev/null
+++ b/tests/assets/romfs_source/romfs_sentinel.txt
@@ -0,0 +1,3 @@
+*MPY-ROMFS-TEST-PARTITION*
+
+This is a ROMFS partition to be used in MicroPython tests, usually for CI.
diff --git a/tests/basics/annotate_var.py.exp b/tests/basics/annotate_var.py.exp
deleted file mode 100644
index 9b6536e966b..00000000000
--- a/tests/basics/annotate_var.py.exp
+++ /dev/null
@@ -1,5 +0,0 @@
-False
-1
-(1, 2)
-NameError
-1
diff --git a/tests/basics/array1.py b/tests/basics/array1.py
index f21ad4bd75f..5c5d13a5816 100644
--- a/tests/basics/array1.py
+++ b/tests/basics/array1.py
@@ -1,11 +1,8 @@
 try:
-    import uarray as array
+    import array
 except ImportError:
-    try:
-        import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 a = array.array('B', [1, 2, 3])
 print(a, len(a))
diff --git a/tests/basics/array_add.py b/tests/basics/array_add.py
index 8335eb6b82b..e78615541c3 100644
--- a/tests/basics/array_add.py
+++ b/tests/basics/array_add.py
@@ -1,12 +1,9 @@
 # test array + array
 try:
-    import uarray as array
+    import array
 except ImportError:
-    try:
-        import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 a1 = array.array('I', [1])
 a2 = array.array('I', [2])
@@ -17,3 +14,9 @@
 
 a1.extend(array.array('I', [5]))
 print(a1)
+
+a1.extend([6, 7])
+print(a1)
+
+a1.extend(i for i in (8, 9))
+print(a1)
diff --git a/tests/basics/array_construct.py b/tests/basics/array_construct.py
index 4985244d13b..2221de99068 100644
--- a/tests/basics/array_construct.py
+++ b/tests/basics/array_construct.py
@@ -1,13 +1,10 @@
 # test construction of array.array from different objects
 
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 # tuple, list
 print(array('b', (1, 2)))
diff --git a/tests/basics/array_construct2.py b/tests/basics/array_construct2.py
index d1b1e9d158c..c305b7f011d 100644
--- a/tests/basics/array_construct2.py
+++ b/tests/basics/array_construct2.py
@@ -1,11 +1,8 @@
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 # construct from something with unknown length (requires generators)
 print(array('i', (i for i in range(10))))
diff --git a/tests/basics/array_construct_endian.py b/tests/basics/array_construct_endian.py
index 82a962fbe0d..990d7b1ea03 100644
--- a/tests/basics/array_construct_endian.py
+++ b/tests/basics/array_construct_endian.py
@@ -1,13 +1,10 @@
 # test construction of array.array from different objects
 
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 # raw copy from bytes, bytearray
 print(array('h', b'12'))
diff --git a/tests/basics/array_int_repr.py b/tests/basics/array_int_repr.py
new file mode 100644
index 00000000000..55a2d5998eb
--- /dev/null
+++ b/tests/basics/array_int_repr.py
@@ -0,0 +1,85 @@
+# Test array integer representations in memory
+#
+# This has to be a unit test because correct internal representation depends on
+# native endianness
+#
+# These test cases should pass on both CPython and MicroPython.
+
+try:
+    from array import array
+    from sys import byteorder
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    import unittest
+except MemoryError:
+    print("SKIP-TOO-LARGE")  # some small boards can't fit unittest in RAM
+    raise SystemExit
+
+# Ports without bigint support don't support typecode 'q'
+try:
+    array("q", [])
+    array_has_typecode_q = True
+except:
+    array_has_typecode_q = False
+
+
+class TestIntReprs(unittest.TestCase):
+    def _test_repr(self, typecode, values):
+        # create an array with the specified typecode and list of values
+        a = array(typecode, values)
+        a_hex = memoryview(a).hex()
+        print(a, a_hex)
+
+        self.assertEqual(len(a_hex) % len(values), 0)
+        # no array.itemsize in MicroPython, so calculate item size
+        sz = len(a_hex) // 2 // len(values)
+        if hasattr(a, "itemsize"):
+            self.assertEqual(a.itemsize, sz)
+
+        # build alternative hex representation of the array using int.to_bytes()
+        # on each value
+        values_hex = ""
+        for v in values:
+            v_bytes = v.to_bytes(sz, byteorder=byteorder, signed=typecode.islower())
+            values_hex += v_bytes.hex()
+
+        # compare with the raw array contents
+        self.assertEqual(a_hex, values_hex)
+
+    def test_smaller_ints(self):
+        for typecode, initialiser in (
+            ("b", [1, -1, 120, -120]),
+            ("B", [1, 5, 220]),
+            ("h", [5, -1, 32_000, -32_000]),
+            ("H", [5, 1, 32_000, 65_535]),
+            ("i", [5, -1, 32_000, -32_000]),  # CPython only guarantees min 2 bytes, C style!
+            ("I", [5, 1, 32_000, 65_535]),
+            ("l", [5, -1, 2_000_000, -2_000_000, 0x7FFF_FFFF]),
+            ("L", [5, 1, 65_536, 2_000_000, 0x7FFF_FFFF, 0xFFFF_FFFF]),
+        ):
+            self._test_repr(typecode, initialiser)
+
+    @unittest.skipIf(not array_has_typecode_q, "port has no bigint support")
+    def test_bigints(self):
+        # Note: need to be careful not to write any literal expressions that can't be compiled on non-bigint MP
+        a = 0x1FFF_FFF
+        b = 62
+
+        try:
+            # this calculation will trigger OverflowError if bigint is set to long long
+            max_uint64 = [2 ** (b + 1)]
+        except OverflowError:
+            max_uint64 = []
+
+        for typecode, initialiser in (
+            ("q", [a * 5, -a * 10, 2**b, (2**b) * -1]),
+            ("Q", [a * 5, a * 10, 2**b, (2**b) - 1, (2**b) + 1] + max_uint64),
+        ):
+            self._test_repr(typecode, initialiser)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/basics/array_intbig.py b/tests/basics/array_intbig.py
index ba7f9ef985a..5702a8ae635 100644
--- a/tests/basics/array_intbig.py
+++ b/tests/basics/array_intbig.py
@@ -1,13 +1,10 @@
 # test array types QqLl that require big-ints
 
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 print(array('L', [0, 2**32-1]))
 print(array('l', [-2**31, 0, 2**31-1]))
diff --git a/tests/basics/array_limits_intbig.py b/tests/basics/array_limits_intbig.py
new file mode 100644
index 00000000000..b21d4752fd3
--- /dev/null
+++ b/tests/basics/array_limits_intbig.py
@@ -0,0 +1,119 @@
+# Test behaviour when array module is provided out of bounds values
+#
+# This test is intended to also pass on CPython.
+
+try:
+    from array import array
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    import unittest
+except MemoryError:
+    print("SKIP-TOO-LARGE")  # some small boards can't fit unittest in RAM
+    raise SystemExit
+
+
+# MicroPython V2.0 will enforce bounds on items (same as CPython), V1.x truncates
+#
+# Note: once  error
-ValueError
-enter
-exit  
-BaseException
diff --git a/tests/basics/async_with2.py b/tests/basics/async_with2.py
index 4dd1386240f..bcdbfa906ac 100644
--- a/tests/basics/async_with2.py
+++ b/tests/basics/async_with2.py
@@ -1,11 +1,8 @@
 # test waiting within async with enter/exit functions
 
-try:
-    import usys as sys
-except ImportError:
-    import sys
+import sys
 if sys.implementation.name == 'micropython':
-    # uPy allows normal generators to be awaitables
+    # MicroPython allows normal generators to be awaitables
     coroutine = lambda f: f
 else:
     import types
diff --git a/tests/basics/async_with2.py.exp b/tests/basics/async_with2.py.exp
deleted file mode 100644
index 76b173b4c24..00000000000
--- a/tests/basics/async_with2.py.exp
+++ /dev/null
@@ -1,17 +0,0 @@
-enter
-f start: 10
-coro yielded: 11
-coro yielded: 12
-f returned: 13
-body start
-f start: 30
-coro yielded: 31
-coro yielded: 32
-body f returned: 33
-body end
-exit None None
-f start: 20
-coro yielded: 21
-coro yielded: 22
-f returned: 23
-finished
diff --git a/tests/basics/async_with_break.py.exp b/tests/basics/async_with_break.py.exp
deleted file mode 100644
index d077a88fad0..00000000000
--- a/tests/basics/async_with_break.py.exp
+++ /dev/null
@@ -1,15 +0,0 @@
-enter
-body
-exit None None
-finished
-enter
-body
-exit None None
-finally
-finished
-enter
-body
-exit None None
-finally inner
-finally outer
-finished
diff --git a/tests/basics/async_with_return.py.exp b/tests/basics/async_with_return.py.exp
deleted file mode 100644
index d077a88fad0..00000000000
--- a/tests/basics/async_with_return.py.exp
+++ /dev/null
@@ -1,15 +0,0 @@
-enter
-body
-exit None None
-finished
-enter
-body
-exit None None
-finally
-finished
-enter
-body
-exit None None
-finally inner
-finally outer
-finished
diff --git a/tests/basics/attrtuple1.py b/tests/basics/attrtuple1.py
index 249c030bb4f..78a0fbed1b7 100644
--- a/tests/basics/attrtuple1.py
+++ b/tests/basics/attrtuple1.py
@@ -1,10 +1,7 @@
 # test attrtuple
 # we can't test this type directly so we use sys.implementation object
 
-try:
-    import usys as sys
-except ImportError:
-    import sys
+import sys
 t = sys.implementation
 
 # It can be just a normal tuple on small ports
diff --git a/tests/basics/attrtuple2.py b/tests/basics/attrtuple2.py
new file mode 100644
index 00000000000..081d24b6ae9
--- /dev/null
+++ b/tests/basics/attrtuple2.py
@@ -0,0 +1,25 @@
+# test os.uname() attrtuple, if available
+try:
+    import os
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    u = os.uname()
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+# test printing of attrtuple
+print(str(u).find("machine=") > 0)
+
+# test read attr
+print(isinstance(u.machine, str))
+
+# test str modulo operator for attrtuple
+impl_str = ("%s " * len(u)) % u
+test_str = ""
+for val in u:
+    test_str += val + " "
+print(impl_str == test_str)
diff --git a/tests/basics/boundmeth1.py b/tests/basics/boundmeth1.py
index f483ba406de..9f08d9bbb25 100644
--- a/tests/basics/boundmeth1.py
+++ b/tests/basics/boundmeth1.py
@@ -1,16 +1,20 @@
 # tests basics of bound methods
 
-# uPy and CPython differ when printing a bound method, so just print the type
+# MicroPython and CPython differ when printing a bound method, so just print the type
 print(type(repr([].append)))
 
+
 class A:
     def f(self):
         return 0
+
     def g(self, a):
         return a
+
     def h(self, a, b, c, d, e, f):
         return a + b + c + d + e + f
 
+
 # bound method with no extra args
 m = A().f
 print(m())
@@ -27,4 +31,46 @@ def h(self, a, b, c, d, e, f):
 try:
     A().f.x = 1
 except AttributeError:
-    print('AttributeError')
+    print("AttributeError")
+
+print("--------")
+
+# bound method comparison with same object
+a = A()
+m1 = a.f
+m2 = a.f
+print(m1 == a.f)  # should result in True
+print(m2 == a.f)  # should result in True
+print(m1 == m2)  # should result in True
+print(m1 != a.f)  # should result in False
+
+# bound method comparison with different objects
+a1 = A()
+a2 = A()
+m1 = a1.f
+m2 = a2.f
+print(m1 == a2.f)  # should result in False
+print(m2 == a1.f)  # should result in False
+print(m1 != a2.f)  # should result in True
+
+# bound method comparison with non-bound-method objects
+print(A().f == None)  # should result in False
+print(A().f != None)  # should result in True
+print(None == A().f)  # should result in False
+print(None != A().f)  # should result in True
+
+print("--------")
+
+# bound method hashing
+a = A()
+m1 = a.f
+m2 = a.f
+print(hash(m1) == hash(a.f))  # should result in True
+print(hash(m2) == hash(a.f))  # should result in True
+print(hash(m1) == hash(m2))  # should result in True
+print(hash(m1) != hash(a.g))  # should result in True
+
+# bound method hashing with different objects
+a2 = A()
+m2 = a2.f
+print(hash(m1) == hash(a2.f))  # should result in False
diff --git a/tests/basics/builtin_callable.py b/tests/basics/builtin_callable.py
index c0a9d0c473a..3ae49f004d1 100644
--- a/tests/basics/builtin_callable.py
+++ b/tests/basics/builtin_callable.py
@@ -7,10 +7,7 @@
 print(callable("dfsd"))
 
 # modules should not be callabe
-try:
-    import usys as sys
-except ImportError:
-    import sys
+import sys
 print(callable(sys))
 
 # builtins should be callable
diff --git a/tests/basics/builtin_compile.py b/tests/basics/builtin_compile.py
index 41dc746ad16..361d7ec53a7 100644
--- a/tests/basics/builtin_compile.py
+++ b/tests/basics/builtin_compile.py
@@ -29,6 +29,9 @@ def test():
     exec(compile("print(10 + 2)", "file", "single"))
     print(eval(compile("10 + 3", "file", "eval")))
 
+    # test accessing a function's globals from within a compile
+    exec(compile("def func():pass\nprint('x', func.__globals__['x'])", "file", "exec"))
+
     # bad mode
     try:
         compile('1', 'file', '')
diff --git a/tests/basics/builtin_dir.py b/tests/basics/builtin_dir.py
index 1f2b498d77f..2aa89dfe0a2 100644
--- a/tests/basics/builtin_dir.py
+++ b/tests/basics/builtin_dir.py
@@ -1,13 +1,17 @@
 # test builtin dir
 
+try:
+    dir
+except NameError:
+    print("SKIP")
+    raise SystemExit
+
+
 # dir of locals
 print('__name__' in dir())
 
 # dir of module
-try:
-    import usys as sys
-except ImportError:
-    import sys
+import sys
 print('version' in dir(sys))
 
 # dir of type
diff --git a/tests/basics/builtin_help.py b/tests/basics/builtin_help.py
index 6ec39653fe9..3d4e4f26bd2 100644
--- a/tests/basics/builtin_help.py
+++ b/tests/basics/builtin_help.py
@@ -14,4 +14,10 @@
 help(micropython) # help for a module
 help('modules') # list available modules
 
+class A:
+    x = 1
+    y = 2
+    del x
+help(A)
+
 print('done') # so last bit of output is predictable
diff --git a/tests/basics/builtin_len_user.py b/tests/basics/builtin_len_user.py
new file mode 100644
index 00000000000..cc9d7346772
--- /dev/null
+++ b/tests/basics/builtin_len_user.py
@@ -0,0 +1,41 @@
+# test builtin len with user-defined __len__
+
+class A:
+    def __len__(self):
+        return 3
+
+
+print(len(A()))
+
+
+class B:
+    def __len__(self):
+        return None
+
+
+try:
+    len(B())
+except TypeError:
+    print("TypeError")
+
+
+class C:
+    def __len__(self):
+        return ""
+
+
+try:
+    len(C())
+except TypeError:
+    print("TypeError")
+
+
+class D:
+    def __len__(self):
+        return ()
+
+
+try:
+    len(D())
+except TypeError:
+    print("TypeError")
diff --git a/tests/basics/builtin_pow3.py b/tests/basics/builtin_pow3.py
index 94e657bc441..3a183a80467 100644
--- a/tests/basics/builtin_pow3.py
+++ b/tests/basics/builtin_pow3.py
@@ -12,6 +12,16 @@
 print(pow(0, 1, 1))
 print(pow(1, 0, 1))
 print(pow(1, 0, 2))
+print(pow(0, 0, 5))
+print(pow(0, 1, 5))
+print(pow(0, 0, 1))
+
+# a negative modulus gives a result with the sign of the modulus
+print(pow(1, 0, -1))
+print(pow(1, 0, -2))
+print(pow(3, 0, -5))
+print(pow(-3, 0, -5))
+print(pow(3, 4, -5))
 
 # 3 arg pow is defined to only work on integers
 try:
@@ -28,3 +38,9 @@
     print(pow(4, 5, "z"))
 except TypeError:
     print("TypeError expected")
+
+# pow(x, y, None) is equivalent to pow(x, y)
+print(pow(0, 1, None))
+print(pow(1, 0, None))
+print(pow(-2, 3, None))
+print(pow(3, 8, None))
diff --git a/tests/basics/builtin_pow3_intbig.py b/tests/basics/builtin_pow3_intbig.py
index bedc8b36b7e..c91b4d173dd 100644
--- a/tests/basics/builtin_pow3_intbig.py
+++ b/tests/basics/builtin_pow3_intbig.py
@@ -9,6 +9,9 @@
 
 print(pow(555557, 1000002, 1000003))
 
+# a zero exponent with a large negative modulus
+print(pow(3, 0, -(2**70)))
+
 # Tests for 3 arg pow with large values
 
 # This value happens to be prime
@@ -20,3 +23,8 @@
 print(hex(pow(y, x-1, x))) # Should be 1, since x is prime
 print(hex(pow(y, y-1, x))) # Should be a 'big value'
 print(hex(pow(y, y-1, y))) # Should be a 'big value'
+
+try:
+    print(pow(1, 2, 0))
+except ValueError:
+    print("ValueError")
diff --git a/tests/basics/builtin_range.py b/tests/basics/builtin_range.py
index 66226bad16a..9608a816310 100644
--- a/tests/basics/builtin_range.py
+++ b/tests/basics/builtin_range.py
@@ -32,13 +32,29 @@
 print(range(1, 4)[0:])
 print(range(1, 4)[1:])
 print(range(1, 4)[:-1])
+print(range(4, 1)[:])
+print(range(4, 1)[0:])
+print(range(4, 1)[1:])
+print(range(4, 1)[:-1])
 print(range(7, -2, -4)[:])
 print(range(1, 100, 5)[5:15:3])
 print(range(1, 100, 5)[15:5:-3])
 print(range(100, 1, -5)[5:15:3])
 print(range(100, 1, -5)[15:5:-3])
+print(range(1, 100, 5)[5:15:-3])
+print(range(1, 100, 5)[15:5:3])
+print(range(100, 1, -5)[5:15:-3])
+print(range(100, 1, -5)[15:5:3])
+print(range(1, 100, 5)[5:15:3])
+print(range(1, 100, 5)[15:5:-3])
+print(range(1, 100, -5)[5:15:3])
+print(range(1, 100, -5)[15:5:-3])
+print(range(1, 100, 5)[5:15:-3])
+print(range(1, 100, 5)[15:5:3])
+print(range(1, 100, -5)[5:15:-3])
+print(range(1, 100, -5)[15:5:3])
 
-# for this case uPy gives a different stop value but the listed elements are still correct
+# for this case MicroPython gives a different stop value but the listed elements are still correct
 print(list(range(7, -2, -4)[2:-2:]))
 
 # zero step
diff --git a/tests/basics/builtin_range_maxsize.py b/tests/basics/builtin_range_maxsize.py
new file mode 100644
index 00000000000..b0f3a5e5129
--- /dev/null
+++ b/tests/basics/builtin_range_maxsize.py
@@ -0,0 +1,38 @@
+try:
+    from sys import maxsize
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Test the range builtin at extreme values. (https://github.com/micropython/micropython/issues/17684)
+#
+# This is written using asserts instead of prints because the value of `maxsize` differs.
+#
+# Numbers & counts right up against the max of mp_int_t also cause overflows, such as
+# objrange.c:115:14: runtime error: signed integer overflow: 9223372036854775807 + 1 cannot be represented in type 'long int'
+# so just avoid them for the purposes of this test.
+
+r = range(-maxsize + 1, -1)
+assert r.start == -maxsize + 1
+assert r.stop == -1
+assert r[0] == -maxsize + 1
+assert r[1] == -maxsize + 2
+assert r[-1] == -2
+assert r[-2] == -3
+
+ir = iter(r)
+assert next(ir) == -maxsize + 1
+assert next(ir) == -maxsize + 2
+
+r = range(0, maxsize - 1)
+assert len(r) == maxsize - 1
+assert r.stop == maxsize - 1
+
+r = range(maxsize, 0, -1)
+assert len(r) == maxsize
+assert r.start == maxsize
+assert r[0] == maxsize
+assert r[1] == maxsize - 1
+ir = iter(r)
+assert next(ir) == maxsize
+assert next(ir) == maxsize - 1
diff --git a/tests/basics/builtin_setattr.py b/tests/basics/builtin_setattr.py
index e4acb14cac3..2c9a452c327 100644
--- a/tests/basics/builtin_setattr.py
+++ b/tests/basics/builtin_setattr.py
@@ -21,5 +21,5 @@ def __init__(self):
 try:
     setattr(int, 'to_bytes', 1)
 except (AttributeError, TypeError):
-    # uPy raises AttributeError, CPython raises TypeError
+    # MicroPython raises AttributeError, CPython raises TypeError
     print('AttributeError/TypeError')
diff --git a/tests/basics/builtin_slice.py b/tests/basics/builtin_slice.py
index df84d5c57bb..5197a7cada9 100644
--- a/tests/basics/builtin_slice.py
+++ b/tests/basics/builtin_slice.py
@@ -1,11 +1,44 @@
 # test builtin slice
 
+# ensures that slices passed to user types are heap-allocated and can be
+# safely stored as well as not overriden by subsequent slices.
+
 # print slice
 class A:
     def __getitem__(self, idx):
-        print(idx)
+        print("get", idx)
+        print("abc"[1:])
+        print("get", idx)
+        return idx
+
+    def __setitem__(self, idx, value):
+        print("set", idx)
+        print("abc"[1:])
+        print("set", idx)
+        self.saved_idx = idx
+        return idx
+
+    def __delitem__(self, idx):
+        print("del", idx)
+        print("abc"[1:])
+        print("del", idx)
         return idx
-s = A()[1:2:3]
+
+
+a = A()
+s = a[1:2:3]
+a[4:5:6] = s
+del a[7:8:9]
+
+print(a.saved_idx)
+
+# nested slicing
+print(A()[1 : A()[A()[2:3:4] : 5]])
+
+# tuple slicing
+a[1:2, 4:5, 7:8]
+a[1, 4:5, 7:8, 2]
+a[1:2, a[3:4], 5:6]
 
 # check type
 print(type(s) is slice)
diff --git a/tests/basics/builtin_str_hex.py b/tests/basics/builtin_str_hex.py
index 7390c8eaee1..f77fe7c4fa9 100644
--- a/tests/basics/builtin_str_hex.py
+++ b/tests/basics/builtin_str_hex.py
@@ -3,6 +3,7 @@
     raise SystemExit
 
 for x in (
+    b"",
     b"\x00\x01\x02\x03\x04\x05\x06\x07",
     b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
     b"\x7f\x80\xff",
@@ -20,5 +21,20 @@
     "08090a0b0c0d0e0f",
     "7f80ff",
     "313233344142434461626364",
+    "ab\tcd\n  ef ",
+    "ab cd ef",
+    "ab cd ef ",
+    " ab cd ef ",
+    # Invalid hex strings:
+    "abcde",  # Odd number of hex digits
+    "ab cd e",
+    "a b cd ef",  # Spaces between hex pairs
+    "ab cd e f ",
+    "abga",  # Invalid hex digits
+    "ab_cd",
+    "ab:cd",
 ):
-    print(bytes.fromhex(x))
+    try:
+        print(bytes.fromhex(x))
+    except ValueError:
+        print("ValueError", x)
diff --git a/tests/basics/builtin_str_hex.py.exp b/tests/basics/builtin_str_hex.py.exp
deleted file mode 100644
index 990dd857076..00000000000
--- a/tests/basics/builtin_str_hex.py.exp
+++ /dev/null
@@ -1,28 +0,0 @@
-0001020304050607
-0001020304050607
-0001020304050607
-00:01:02:03:04:05:06:07
-00:01:02:03:04:05:06:07
-00:01:02:03:04:05:06:07
-08090a0b0c0d0e0f
-08090a0b0c0d0e0f
-08090a0b0c0d0e0f
-08:09:0a:0b:0c:0d:0e:0f
-08:09:0a:0b:0c:0d:0e:0f
-08:09:0a:0b:0c:0d:0e:0f
-7f80ff
-7f80ff
-7f80ff
-7f:80:ff
-7f:80:ff
-7f:80:ff
-313233344142434461626364
-313233344142434461626364
-313233344142434461626364
-31:32:33:34:41:42:43:44:61:62:63:64
-31:32:33:34:41:42:43:44:61:62:63:64
-31:32:33:34:41:42:43:44:61:62:63:64
-b'\x00\x01\x02\x03\x04\x05\x06\x07'
-b'\x08\t\n\x0b\x0c\r\x0e\x0f'
-b'\x7f\x80\xff'
-b'1234ABCDabcd'
diff --git a/tests/basics/builtin_super.py b/tests/basics/builtin_super.py
new file mode 100644
index 00000000000..5f7c3ae017e
--- /dev/null
+++ b/tests/basics/builtin_super.py
@@ -0,0 +1,15 @@
+# Check that super rejects invalid arguments.
+try:
+    super(str, 0)
+except TypeError:
+    print("TypeError")
+
+try:
+    super(str, int)
+except TypeError:
+    print("TypeError")
+
+try:
+    super(0, int)
+except TypeError:
+    print("TypeError")
diff --git a/tests/basics/bytearray_add.py b/tests/basics/bytearray_add.py
index a7e2b573742..1f30a3b740e 100644
--- a/tests/basics/bytearray_add.py
+++ b/tests/basics/bytearray_add.py
@@ -15,4 +15,11 @@
 
 # this inplace add tests the code when the buffer doesn't need to be increased
 b = bytearray()
-b += b''
+b += b""
+
+# extend a bytearray from itself
+b = bytearray(b"abcdefgh")
+for _ in range(4):
+    c = bytearray(b)  # extra allocation, as above
+    b.extend(b)
+print(b)
diff --git a/tests/basics/bytearray_add_self.py b/tests/basics/bytearray_add_self.py
new file mode 100644
index 00000000000..94ae8689fd1
--- /dev/null
+++ b/tests/basics/bytearray_add_self.py
@@ -0,0 +1,8 @@
+# add a bytearray to itself
+# This is not supported by CPython as of 3.11.18.
+
+b = bytearray(b"123456789")
+for _ in range(4):
+    c = bytearray(b)  # extra allocation increases chance 'b' has to relocate
+    b += b
+print(b)
diff --git a/tests/basics/bytearray_add_self.py.exp b/tests/basics/bytearray_add_self.py.exp
new file mode 100644
index 00000000000..5ef948157ac
--- /dev/null
+++ b/tests/basics/bytearray_add_self.py.exp
@@ -0,0 +1 @@
+bytearray(b'123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789')
diff --git a/tests/basics/bytearray_byte_operations.py b/tests/basics/bytearray_byte_operations.py
index 7ce53cac535..794fe8cafa2 100644
--- a/tests/basics/bytearray_byte_operations.py
+++ b/tests/basics/bytearray_byte_operations.py
@@ -1,7 +1,8 @@
-# test bytearray with its re-use of byte functions
+# test bytearray with its reuse of byte functions
 
 print(bytearray(b"hello world").find(b"ll"))
 print(bytearray(b"hello\x00world").rfind(b"l"))
+print(bytearray(b"hello world").find(ord(b"l")))
 
 print(bytearray(b"abc  efg ").strip(b"g a"))
 print(bytearray(b"   spacious   ").lstrip())
@@ -14,10 +15,18 @@
 
 print(bytearray(b"asdfasdf").replace(b"a", b"b"))
 
-print("00\x0000".index("0", 0))
-print("00\x0000".index("0", 3))
-print("00\x0000".rindex("0", 0))
-print("00\x0000".rindex("0", 3))
+print(b"00\x0000".index(b"0", 0))
+print(b"00\x0000".index(b"0", 3))
+print(b"00\x0000".index(ord("0"), 0))
+print(b"00\x0000".index(ord("0"), 3))
+print(b"00\x0000".index(0, 0))
+try:
+    print(b"00\x0000".index(0, 3))
+except ValueError:
+    print("ValueError")
+print(b"00\x0000".index(b"0", 3))
+print(b"00\x0000".rindex(b"0", 0))
+print(b"00\x0000".rindex(b"0", 3))
 
 print(bytearray(b"foobar").endswith(b"bar"))
 print(bytearray(b"1foo").startswith(b"foo", 1))
diff --git a/tests/basics/bytearray_construct_array.py b/tests/basics/bytearray_construct_array.py
index 52eaa7c6efb..bde5fa08bd2 100644
--- a/tests/basics/bytearray_construct_array.py
+++ b/tests/basics/bytearray_construct_array.py
@@ -1,12 +1,9 @@
 # test construction of bytearray from different objects
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 # arrays
 print(bytearray(array('b', [1, 2])))
diff --git a/tests/basics/bytearray_construct_endian.py b/tests/basics/bytearray_construct_endian.py
index 332b43e6861..0002f19c5f9 100644
--- a/tests/basics/bytearray_construct_endian.py
+++ b/tests/basics/bytearray_construct_endian.py
@@ -1,12 +1,9 @@
 # test construction of bytearray from different objects
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 # arrays
 print(bytearray(array('h', [1, 2])))
diff --git a/tests/basics/bytearray_slice_assign.py b/tests/basics/bytearray_slice_assign.py
index fa7878e10dd..4de0819042a 100644
--- a/tests/basics/bytearray_slice_assign.py
+++ b/tests/basics/bytearray_slice_assign.py
@@ -18,7 +18,7 @@
 l[1:3] = bytearray()
 print(l)
 l = bytearray(x)
-#del l[1:3]
+# del l[1:3]
 print(l)
 
 l = bytearray(x)
@@ -28,7 +28,7 @@
 l[:3] = bytearray()
 print(l)
 l = bytearray(x)
-#del l[:3]
+# del l[:3]
 print(l)
 
 l = bytearray(x)
@@ -38,7 +38,7 @@
 l[:-3] = bytearray()
 print(l)
 l = bytearray(x)
-#del l[:-3]
+# del l[:-3]
 print(l)
 
 # slice assignment that extends the array
@@ -61,8 +61,14 @@
 print(b)
 
 # Growth of bytearray via slice extension
-b = bytearray(b'12345678')
-b.append(57) # expand and add a bit of unused space at end of the bytearray
+b = bytearray(b"12345678")
+b.append(57)  # expand and add a bit of unused space at end of the bytearray
 for i in range(400):
-    b[-1:] = b'ab' # grow slowly into the unused space
+    b[-1:] = b"ab"  # grow slowly into the unused space
+print(len(b), b)
+
+# Growth of bytearray via slice extension from itself
+b = bytearray(b"1234567")
+for i in range(3):
+    b[-1:] = b
 print(len(b), b)
diff --git a/tests/basics/bytes.py b/tests/basics/bytes.py
index 0b6b14fa55b..ba14c26da8d 100644
--- a/tests/basics/bytes.py
+++ b/tests/basics/bytes.py
@@ -2,7 +2,6 @@
 print(b'123')
 print(br'123')
 print(rb'123')
-print(b'\u1234')
 
 # construction
 print(bytes())
diff --git a/tests/basics/bytes_add_array.py b/tests/basics/bytes_add_array.py
index c6382bed749..b17556d83cb 100644
--- a/tests/basics/bytes_add_array.py
+++ b/tests/basics/bytes_add_array.py
@@ -1,12 +1,9 @@
 # test bytes + other
 try:
-    import uarray as array
+    import array
 except ImportError:
-    try:
-        import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 # should be byteorder-neutral
 print(b"123" + array.array('h', [0x1515]))
diff --git a/tests/basics/bytes_add_endian.py b/tests/basics/bytes_add_endian.py
index 40b3de7d615..8cfffa7b6ac 100644
--- a/tests/basics/bytes_add_endian.py
+++ b/tests/basics/bytes_add_endian.py
@@ -1,11 +1,8 @@
 # test bytes + other
 try:
-    import uarray as array
+    import array
 except ImportError:
-    try:
-        import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 print(b"123" + array.array('i', [1]))
diff --git a/tests/basics/bytes_compare_array.py b/tests/basics/bytes_compare_array.py
index 6bad50b55a1..ad378de70c5 100644
--- a/tests/basics/bytes_compare_array.py
+++ b/tests/basics/bytes_compare_array.py
@@ -1,11 +1,8 @@
 try:
-    import uarray as array
+    import array
 except ImportError:
-    try:
-        import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 print(array.array('b', [1, 2]) in b'\x01\x02\x03')
 # CPython gives False here
diff --git a/tests/basics/bytes_construct_array.py b/tests/basics/bytes_construct_array.py
index 7bdd8f10df4..453eb590103 100644
--- a/tests/basics/bytes_construct_array.py
+++ b/tests/basics/bytes_construct_array.py
@@ -1,12 +1,9 @@
 # test construction of bytes from different objects
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 # arrays
 print(bytes(array('b', [1, 2])))
diff --git a/tests/basics/bytes_construct_endian.py b/tests/basics/bytes_construct_endian.py
index 294c5f23f5c..cf1a9f408fa 100644
--- a/tests/basics/bytes_construct_endian.py
+++ b/tests/basics/bytes_construct_endian.py
@@ -1,13 +1,10 @@
 # test construction of bytes from different objects
 
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 # arrays
 print(bytes(array('h', [1, 2])))
diff --git a/tests/basics/bytes_escape_unicode.py b/tests/basics/bytes_escape_unicode.py
new file mode 100644
index 00000000000..1e450696f4f
--- /dev/null
+++ b/tests/basics/bytes_escape_unicode.py
@@ -0,0 +1,3 @@
+# Coverage test for unicode escape in a bytes literal.
+# CPython issues a SyntaxWarning for this.
+print(b"\u1234")
diff --git a/tests/basics/bytes_escape_unicode.py.exp b/tests/basics/bytes_escape_unicode.py.exp
new file mode 100644
index 00000000000..6affe51896e
--- /dev/null
+++ b/tests/basics/bytes_escape_unicode.py.exp
@@ -0,0 +1 @@
+b'\\u1234'
diff --git a/tests/basics/bytes_find.py b/tests/basics/bytes_find.py
index 75ef9796cd1..1ecbf13b0cf 100644
--- a/tests/basics/bytes_find.py
+++ b/tests/basics/bytes_find.py
@@ -21,6 +21,44 @@
 print(b"0000".find(b'1', 3))
 print(b"0000".find(b'1', 4))
 print(b"0000".find(b'1', 5))
+print(b"0000".find(ord(b'0')))
+print(b"0000".find(ord(b'0'), 0))
+print(b"0000".find(ord(b'0'), 1))
+print(b"0000".find(ord(b'0'), 2))
+print(b"0000".find(ord(b'0'), 3))
+print(b"0000".find(ord(b'0'), 4))
+print(b"0000".find(ord(b'0'), 5))
+print(b"0000".find(ord(b'x'), 3))
+print(b"0000".find(ord(b'1'), 3))
+print(b"0000".find(ord(b'1'), 4))
+print(b"0000".find(ord(b'1'), 5))
 
 # Non-ascii values (make sure not treated as unicode-like)
 print(b"\x80abc".find(b"a", 1))
+
+# Int-like conversion.
+print(b"00\x0000".find(b'0', True))
+
+# Out of bounds int.
+try:
+    print(b"0000".find(b'0', -1))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".find(b'0', 256))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".find(b'0', 91273611))
+except ValueError:
+    print("ValueError")
+
+# Out of bounds search argument.
+try:
+    print(b"0000".find(-1))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".find(256))
+except ValueError:
+    print("ValueError")
diff --git a/tests/basics/bytes_format_modulo.py.exp b/tests/basics/bytes_format_modulo.py.exp
deleted file mode 100644
index 782b7f91fcb..00000000000
--- a/tests/basics/bytes_format_modulo.py.exp
+++ /dev/null
@@ -1,5 +0,0 @@
-b'%'
-b'=1='
-b'=1=2='
-b'=str='
-b"=b'str'="
diff --git a/tests/basics/bytes_index.py b/tests/basics/bytes_index.py
new file mode 100644
index 00000000000..d99b325192e
--- /dev/null
+++ b/tests/basics/bytes_index.py
@@ -0,0 +1,109 @@
+print(b"hello world".index(b"ll"))
+print(b"hello world".index(b"ll", None))
+print(b"hello world".index(b"ll", 1))
+print(b"hello world".index(b"ll", 1, None))
+print(b"hello world".index(b"ll", None, None))
+print(b"hello world".index(b"ll", 1, -1))
+try:
+    print(b"hello world".index(b"ll", 1, 1))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"hello world".index(b"ll", 1, 2))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"hello world".index(b"ll", 1, 3))
+except ValueError:
+    print("ValueError")
+print(b"hello world".index(b"ll", 1, 4))
+print(b"hello world".index(b"ll", 1, 5))
+print(b"hello world".index(b"ll", -100))
+print(b"0000".index(b'0'))
+print(b"0000".index(b'0', 0))
+print(b"0000".index(b'0', 1))
+print(b"0000".index(b'0', 2))
+print(b"0000".index(b'0', 3))
+try:
+    print(b"0000".index(b'0', 4))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".index(b'0', 5))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".index(b'-1', 3))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".index(b'1', 3))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".index(b'1', 4))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".index(b'1', 5))
+except ValueError:
+    print("ValueError")
+print(b"0000".index(ord(b'0')))
+print(b"0000".index(ord(b'0'), 0))
+print(b"0000".index(ord(b'0'), 1))
+print(b"0000".index(ord(b'0'), 2))
+print(b"0000".index(ord(b'0'), 3))
+try:
+    print(b"0000".index(ord(b'0'), 4))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".index(ord(b'0'), 5))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".index(ord(b'x'), 3))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".index(ord(b'1'), 3))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".index(ord(b'1'), 4))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".index(ord(b'1'), 5))
+except ValueError:
+    print("ValueError")
+
+# Non-ascii values (make sure not treated as unicode-like)
+print(b"\x80abc".index(b"a", 1))
+
+# Int-like conversion.
+print(b"00\x0000".index(b'0', True))
+
+# Out of bounds int.
+try:
+    print(b"0000".index(b'0', -1))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".index(b'0', 256))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".index(b'0', 91273611))
+except ValueError:
+    print("ValueError")
+
+# Out of bounds search argument.
+try:
+    print(b"0000".index(-1))
+except ValueError:
+    print("ValueError")
+try:
+    print(b"0000".index(256))
+except ValueError:
+    print("ValueError")
diff --git a/tests/basics/class_bind_self.py b/tests/basics/class_bind_self.py
index 813f876446e..8bece902ca2 100644
--- a/tests/basics/class_bind_self.py
+++ b/tests/basics/class_bind_self.py
@@ -52,7 +52,7 @@ def f4(self, arg): # generator
 print(C.f8(13))
 print(C.f9(14))
 
-# not working in uPy
+# not working in MicroPython
 #class C(list):
 #    # this acts like a method and binds self
 #    f1 = list.extend
diff --git a/tests/basics/class_descriptor.py b/tests/basics/class_descriptor.py
index 54f386230fe..feaed2fbb2a 100644
--- a/tests/basics/class_descriptor.py
+++ b/tests/basics/class_descriptor.py
@@ -1,34 +1,67 @@
 class Descriptor:
     def __get__(self, obj, cls):
-        print('get')
+        print("get")
         print(type(obj) is Main)
         print(cls is Main)
-        return 'result'
+        return "result"
 
     def __set__(self, obj, val):
-        print('set')
+        print("set")
         print(type(obj) is Main)
         print(val)
 
     def __delete__(self, obj):
-        print('delete')
+        print("delete")
         print(type(obj) is Main)
 
+    def __set_name__(self, owner, name):
+        print("set_name", name)
+        print(owner.__name__ == "Main")
+
+
 class Main:
     Forward = Descriptor()
 
+
 m = Main()
 try:
     m.__class__
 except AttributeError:
+    # Target doesn't support __class__.
     print("SKIP")
     raise SystemExit
 
 r = m.Forward
-if 'Descriptor' in repr(r.__class__):
-    print('SKIP')
+if "Descriptor" in repr(r.__class__):
+    # Target doesn't support descriptors.
+    print("SKIP")
     raise SystemExit
 
+# Test assignment and deletion.
+
 print(r)
-m.Forward = 'a'
+m.Forward = "a"
 del m.Forward
+
+# Test that lookup of descriptors like __get__ are not passed into __getattr__.
+
+
+class NonDescriptor:
+    def __getattr__(self, attr):
+        print("getattr", attr)
+
+
+class TestClass:
+    non_descriptor = NonDescriptor()
+
+
+print(isinstance(TestClass().non_descriptor, NonDescriptor))
+
+t = TestClass()
+t.non_descriptor = 123
+print(t.non_descriptor)
+
+try:
+    del TestClass().non_descriptor
+except AttributeError:
+    print("AttributeError")
diff --git a/tests/basics/class_inplace_op2.py.exp b/tests/basics/class_inplace_op2.py.exp
deleted file mode 100644
index 8c323b5178e..00000000000
--- a/tests/basics/class_inplace_op2.py.exp
+++ /dev/null
@@ -1,12 +0,0 @@
-__imul__
-__imatmul__
-__ifloordiv__
-__itruediv__
-__imod__
-__ipow__
-__ior__
-__ixor__
-__iand__
-__ilshift__
-__irshift__
-TypeError
diff --git a/tests/basics/class_ordereddict.py b/tests/basics/class_ordereddict.py
index 4dd25eb6e15..03a211ddad2 100644
--- a/tests/basics/class_ordereddict.py
+++ b/tests/basics/class_ordereddict.py
@@ -1,13 +1,10 @@
 # test using an OrderedDict as the locals to construct a class
 
 try:
-    from ucollections import OrderedDict
+    from collections import OrderedDict
 except ImportError:
-    try:
-        from collections import OrderedDict
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 if not hasattr(int, "__dict__"):
     print("SKIP")
diff --git a/tests/basics/class_ordereddict.py.exp b/tests/basics/class_ordereddict.py.exp
deleted file mode 100644
index b723e327515..00000000000
--- a/tests/basics/class_ordereddict.py.exp
+++ /dev/null
@@ -1 +0,0 @@
-['a', 'b', 'c', 'd', 'e']
diff --git a/tests/basics/class_setname_hazard.py b/tests/basics/class_setname_hazard.py
new file mode 100644
index 00000000000..77c04093462
--- /dev/null
+++ b/tests/basics/class_setname_hazard.py
@@ -0,0 +1,182 @@
+# Test that __set_name__ can access and mutate its owner argument.
+
+
+def skip_if_no_descriptors():
+    class Descriptor:
+        def __get__(self, obj, cls):
+            return
+
+    class TestClass:
+        Forward = Descriptor()
+
+    a = TestClass()
+    try:
+        a.__class__
+    except AttributeError:
+        # Target doesn't support __class__.
+        print("SKIP")
+        raise SystemExit
+
+    b = a.Forward
+    if "Descriptor" in repr(b.__class__):
+        # Target doesn't support descriptors.
+        print("SKIP")
+        raise SystemExit
+
+
+skip_if_no_descriptors()
+
+
+# Test basic accesses and mutations.
+
+
+class GetSibling:
+    def __set_name__(self, owner, name):
+        print(getattr(owner, name + "_sib"))
+
+
+class GetSiblingTest:
+    desc = GetSibling()
+    desc_sib = 111
+
+
+t110 = GetSiblingTest()
+
+
+class SetSibling:
+    def __set_name__(self, owner, name):
+        setattr(owner, name + "_sib", 121)
+
+
+class SetSiblingTest:
+    desc = SetSibling()
+
+
+t120 = SetSiblingTest()
+
+print(t120.desc_sib)
+
+
+class DelSibling:
+    def __set_name__(self, owner, name):
+        delattr(owner, name + "_sib")
+
+
+class DelSiblingTest:
+    desc = DelSibling()
+    desc_sib = 131
+
+
+t130 = DelSiblingTest()
+
+try:
+    print(t130.desc_sib)
+except AttributeError:
+    print("AttributeError")
+
+
+class GetSelf:
+    x = 211
+
+    def __set_name__(self, owner, name):
+        print(getattr(owner, name).x)
+
+
+class GetSelfTest:
+    desc = GetSelf()
+
+
+t210 = GetSelfTest()
+
+
+class SetSelf:
+    def __set_name__(self, owner, name):
+        setattr(owner, name, 221)
+
+
+class SetSelfTest:
+    desc = SetSelf()
+
+
+t220 = SetSelfTest()
+
+print(t220.desc)
+
+
+class DelSelf:
+    def __set_name__(self, owner, name):
+        delattr(owner, name)
+
+
+class DelSelfTest:
+    desc = DelSelf()
+
+
+t230 = DelSelfTest()
+
+try:
+    print(t230.desc)
+except AttributeError:
+    print("AttributeError")
+
+
+# Test exception behavior.
+
+
+class Raise:
+    def __set_name__(self, owner, name):
+        raise Exception()
+
+
+try:
+
+    class RaiseTest:
+        desc = Raise()
+except Exception as e:  # CPython raises RuntimeError, MicroPython propagates the original exception
+    print("Exception")
+
+
+# Ensure removed/overwritten class members still get __set_name__ called.
+
+
+class SetSpecific:
+    def __init__(self, sib_name, sib_replace):
+        self.sib_name = sib_name
+        self.sib_replace = sib_replace
+
+    def __set_name__(self, owner, name):
+        setattr(owner, self.sib_name, self.sib_replace)
+
+
+class SetReplaceTest:
+    a = SetSpecific("b", 312)  # one of these is changed first
+    b = SetSpecific("a", 311)
+
+
+t310 = SetReplaceTest()
+print(t310.a)
+print(t310.b)
+
+
+class DelSpecific:
+    def __init__(self, sib_name):
+        self.sib_name = sib_name
+
+    def __set_name__(self, owner, name):
+        delattr(owner, self.sib_name)
+
+
+class DelReplaceTest:
+    a = DelSpecific("b")  # one of these is removed first
+    b = DelSpecific("a")
+
+
+t320 = DelReplaceTest()
+try:
+    print(t320.a)
+except AttributeError:
+    print("AttributeError")
+try:
+    print(t320.b)
+except AttributeError:
+    print("AttributeError")
diff --git a/tests/basics/class_setname_hazard_rand.py b/tests/basics/class_setname_hazard_rand.py
new file mode 100644
index 00000000000..4c9934c3bf0
--- /dev/null
+++ b/tests/basics/class_setname_hazard_rand.py
@@ -0,0 +1,111 @@
+# Test to make sure there's no sequence hazard even when a __set_name__ implementation
+# mutates and reorders the namespace of its owner class.
+# VERY hard bug to prove out except via a stochastic test.
+
+
+try:
+    from random import choice
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def skip_if_no_descriptors():
+    class Descriptor:
+        def __get__(self, obj, cls):
+            return
+
+    class TestClass:
+        Forward = Descriptor()
+
+    a = TestClass()
+    try:
+        a.__class__
+    except AttributeError:
+        # Target doesn't support __class__.
+        print("SKIP")
+        raise SystemExit
+
+    b = a.Forward
+    if "Descriptor" in repr(b.__class__):
+        # Target doesn't support descriptors.
+        print("SKIP")
+        raise SystemExit
+
+
+skip_if_no_descriptors()
+
+letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+# Would be r"[A-Z]{5}", but not all ports support the {n} quantifier.
+junk_re = re.compile(r"[A-Z][A-Z][A-Z][A-Z][A-Z]")
+
+
+def junk_fill(obj, n=10):  # Add randomly-generated attributes to an object.
+    for i in range(n):
+        name = "".join(choice(letters) for j in range(5))
+        setattr(obj, name, object())
+
+
+def junk_clear(obj):  # Remove attributes added by junk_fill.
+    to_del = [name for name in dir(obj) if junk_re.match(name)]
+    for name in to_del:
+        delattr(obj, name)
+
+
+def junk_sequencer():
+    global runs
+    try:
+        while True:
+            owner, name = yield
+            runs[name] = runs.get(name, 0) + 1
+            junk_fill(owner)
+    finally:
+        junk_clear(owner)
+
+
+class JunkMaker:
+    def __set_name__(self, owner, name):
+        global seq
+        seq.send((owner, name))
+
+
+runs = {}
+seq = junk_sequencer()
+next(seq)
+
+
+class Main:
+    a = JunkMaker()
+    b = JunkMaker()
+    c = JunkMaker()
+    d = JunkMaker()
+    e = JunkMaker()
+    f = JunkMaker()
+    g = JunkMaker()
+    h = JunkMaker()
+    i = JunkMaker()
+    j = JunkMaker()
+    k = JunkMaker()
+    l = JunkMaker()
+    m = JunkMaker()
+    n = JunkMaker()
+    o = JunkMaker()
+    p = JunkMaker()
+    q = JunkMaker()
+    r = JunkMaker()
+    s = JunkMaker()
+    t = JunkMaker()
+    u = JunkMaker()
+    v = JunkMaker()
+    w = JunkMaker()
+    x = JunkMaker()
+    y = JunkMaker()
+    z = JunkMaker()
+
+
+seq.close()
+
+for k in letters.lower():
+    print(k, runs.get(k, 0))
diff --git a/tests/basics/class_store_class.py b/tests/basics/class_store_class.py
index 797f88f8526..8ee2f8db11b 100644
--- a/tests/basics/class_store_class.py
+++ b/tests/basics/class_store_class.py
@@ -5,11 +5,8 @@
 try:
     from collections import namedtuple
 except ImportError:
-    try:
-        from ucollections import namedtuple
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 _DefragResultBase = namedtuple('DefragResult', [ 'foo', 'bar' ])
 
diff --git a/tests/basics/del_attr.py b/tests/basics/del_attr.py
index 8487b97e311..90a8a5a6f83 100644
--- a/tests/basics/del_attr.py
+++ b/tests/basics/del_attr.py
@@ -35,5 +35,5 @@ def f():
 try:
     del int.to_bytes
 except (AttributeError, TypeError):
-    # uPy raises AttributeError, CPython raises TypeError
+    # MicroPython raises AttributeError, CPython raises TypeError
     print('AttributeError/TypeError')
diff --git a/tests/basics/del_global.py b/tests/basics/del_global.py
index d740357b033..e1aa074b97a 100644
--- a/tests/basics/del_global.py
+++ b/tests/basics/del_global.py
@@ -14,7 +14,7 @@ def do_del():
 try:
     do_del()
 except: # NameError:
-    # FIXME uPy returns KeyError for this
+    # FIXME MicroPython returns KeyError for this
     print("NameError")
 
 # delete globals using a list
diff --git a/tests/basics/del_name.py b/tests/basics/del_name.py
index c92be54d3b6..8393b2df92a 100644
--- a/tests/basics/del_name.py
+++ b/tests/basics/del_name.py
@@ -10,7 +10,7 @@
 try:
     del x
 except: # NameError:
-    # FIXME uPy returns KeyError for this
+    # FIXME MicroPython returns KeyError for this
     print("NameError")
 
 class C:
diff --git a/tests/basics/deque1.py b/tests/basics/deque1.py
index 19966fcb078..188069bcbf3 100644
--- a/tests/basics/deque1.py
+++ b/tests/basics/deque1.py
@@ -1,8 +1,5 @@
 try:
-    try:
-        from ucollections import deque
-    except ImportError:
-        from collections import deque
+    from collections import deque
 except ImportError:
     print("SKIP")
     raise SystemExit
@@ -66,3 +63,52 @@
     ~d
 except TypeError:
     print("TypeError")
+
+
+# Same tests, but now with pop() and appendleft()
+
+d = deque((), 2)
+print(len(d))
+print(bool(d))
+
+try:
+    d.popleft()
+except IndexError:
+    print("IndexError")
+
+print(d.append(1))
+print(len(d))
+print(bool(d))
+print(d.popleft())
+print(len(d))
+
+d.append(2)
+print(d.popleft())
+
+d.append(3)
+d.append(4)
+print(len(d))
+print(d.popleft(), d.popleft())
+try:
+    d.popleft()
+except IndexError:
+    print("IndexError")
+
+d.append(5)
+d.append(6)
+d.append(7)
+print(len(d))
+print(d.popleft(), d.popleft())
+print(len(d))
+try:
+    d.popleft()
+except IndexError:
+    print("IndexError")
+
+d = deque((), 2)
+d.appendleft(1)
+d.appendleft(2)
+d.appendleft(3)
+d.appendleft(4)
+d.appendleft(5)
+print(d.pop(), d.pop())
\ No newline at end of file
diff --git a/tests/basics/deque2.py b/tests/basics/deque2.py
index 22d370e943b..ebc0872c7b4 100644
--- a/tests/basics/deque2.py
+++ b/tests/basics/deque2.py
@@ -1,66 +1,53 @@
-# Tests for deques with "check overflow" flag and other extensions
-# wrt to CPython.
 try:
-    try:
-        from ucollections import deque
-    except ImportError:
-        from collections import deque
+    from collections import deque
 except ImportError:
     print("SKIP")
     raise SystemExit
 
+# Initial sequence is supported
+d = deque([1, 2, 3], 10)
 
-# Initial sequence is not supported
-try:
-    deque([1, 2, 3], 10)
-except ValueError:
-    print("ValueError")
+# iteration over sequence
+for x in d:
+    print(x)
 
-# Not even empty list, only empty tuple
-try:
-    deque([], 10)
-except ValueError:
-    print("ValueError")
+# Iterables larger than maxlen have the beginnings removed, also tests
+# iteration through conversion to list
+d = deque([1, 2, 3, 4, 5], 3)
+print(list(d))
 
-# Only fixed-size deques are supported, so length arg is mandatory
-try:
-    deque(())
-except TypeError:
-    print("TypeError")
+# Empty iterables are also supported
+deque([], 10)
 
-d = deque((), 2, True)
+# Extending deques with iterables
+d.extend([6, 7])
+print(list(d))
 
-try:
-    d.popleft()
-except IndexError:
-    print("IndexError")
+# Accessing queue elements via index
+d = deque((0, 1, 2, 3), 5)
+print(d[0], d[1], d[-1])
 
-print(d.append(1))
-print(d.popleft())
+# Writing queue elements via index
+d[3] = 5
+print(d[3])
 
-d.append(2)
-print(d.popleft())
+# Access the last element via index, when the last element is at various locations
+d = deque((), 2)
+for i in range(4):
+    d.append(i)
+    print(i, d[-1])
 
-d.append(3)
-d.append(4)
-print(d.popleft(), d.popleft())
-try:
-    d.popleft()
-except IndexError as e:
-    print(repr(e))
+# Write the last element then access all elements from the end
+d[-1] = 4
+print(d[-2], d[-1])
 
-d.append(5)
-d.append(6)
-print(len(d))
+# Accessing indices out of bounds raises IndexError
 try:
-    d.append(7)
-except IndexError as e:
-    print(repr(e))
-print(len(d))
+    d[4]
+except IndexError:
+    print("IndexError")
 
-print(d.popleft(), d.popleft())
-print(len(d))
 try:
-    d.popleft()
-except IndexError as e:
-    print(repr(e))
+    d[4] = 0
+except IndexError:
+    print("IndexError")
diff --git a/tests/basics/deque2.py.exp b/tests/basics/deque2.py.exp
deleted file mode 100644
index 3df8acf4056..00000000000
--- a/tests/basics/deque2.py.exp
+++ /dev/null
@@ -1,15 +0,0 @@
-ValueError
-ValueError
-TypeError
-IndexError
-None
-1
-2
-3 4
-IndexError('empty',)
-2
-IndexError('full',)
-2
-5 6
-0
-IndexError('empty',)
diff --git a/tests/basics/deque_micropython.py b/tests/basics/deque_micropython.py
new file mode 100644
index 00000000000..fb9771fefeb
--- /dev/null
+++ b/tests/basics/deque_micropython.py
@@ -0,0 +1,75 @@
+# Test MicroPython-specific features of collections.deque.
+
+try:
+    from collections import deque
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+# Only fixed-size deques are supported, so length arg is mandatory.
+try:
+    deque(())
+except TypeError:
+    print("TypeError")
+
+# Test third argument: flags when True means check for under/overflow
+d = deque((), 2, True)
+
+try:
+    d.popleft()
+except IndexError:
+    print("IndexError")
+
+try:
+    d.pop()
+except IndexError:
+    print("IndexError")
+
+# Removing elements with del is not supported, fallback to
+# mp_obj_subscr() error message.
+try:
+    del d[0]
+except TypeError:
+    print("TypeError")
+
+print(d.append(1))
+print(d.popleft())
+
+d.append(2)
+print(d.popleft())
+
+d.append(3)
+d.append(4)
+print(d.popleft(), d.popleft())
+try:
+    d.popleft()
+except IndexError as e:
+    print(str(e) or 'empty', len(d))
+
+try:
+    d.pop()
+except IndexError as e:
+    print(str(e) or 'empty', len(d))
+
+d.append(5)
+d.append(6)
+print(len(d))
+try:
+    d.append(7)
+except IndexError as e:
+    print(str(e) or 'full', len(d))
+
+try:
+    d.appendleft(8)
+except IndexError as e:
+    print(str(e) or 'full', len(d))
+
+print(len(d))
+
+print(d.popleft(), d.popleft())
+print(len(d))
+try:
+    d.popleft()
+except IndexError as e:
+    print(str(e) or 'empty', len(d))
diff --git a/tests/basics/deque_micropython.py.exp b/tests/basics/deque_micropython.py.exp
new file mode 100644
index 00000000000..7869dc8cacc
--- /dev/null
+++ b/tests/basics/deque_micropython.py.exp
@@ -0,0 +1,17 @@
+TypeError
+IndexError
+IndexError
+TypeError
+None
+1
+2
+3 4
+empty 0
+empty 0
+2
+full 2
+full 2
+2
+5 6
+0
+empty 0
diff --git a/tests/basics/deque_slice.py b/tests/basics/deque_slice.py
new file mode 100644
index 00000000000..367aeea3a17
--- /dev/null
+++ b/tests/basics/deque_slice.py
@@ -0,0 +1,29 @@
+try:
+    from collections import deque
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+d = deque((), 10)
+
+d.append(1)
+d.append(2)
+d.append(3)
+
+# Index slicing for reads is not supported in CPython
+try:
+    d[0:1]
+except TypeError:
+    print("TypeError")
+
+# Index slicing for writes is not supported in CPython
+try:
+    d[0:1] = (-1, -2)
+except TypeError:
+    print("TypeError")
+
+# Index slicing for writes is not supported in CPython
+try:
+    del d[0:1]
+except TypeError:
+    print("TypeError")
diff --git a/tests/basics/dict_fixed.py b/tests/basics/dict_fixed.py
index 4261a06557a..cfd3b71f574 100644
--- a/tests/basics/dict_fixed.py
+++ b/tests/basics/dict_fixed.py
@@ -1,48 +1,48 @@
 # test that fixed dictionaries cannot be modified
 
 try:
-    import uerrno
+    import errno
 except ImportError:
     print("SKIP")
     raise SystemExit
 
-# Save a copy of uerrno.errorcode, so we can check later
+# Save a copy of errno.errorcode, so we can check later
 # that it hasn't been modified.
-errorcode_copy = uerrno.errorcode.copy()
+errorcode_copy = errno.errorcode.copy()
 
 try:
-    uerrno.errorcode.popitem()
+    errno.errorcode.popitem()
 except TypeError:
     print("TypeError")
 
 try:
-    uerrno.errorcode.pop(0)
+    errno.errorcode.pop(0)
 except TypeError:
     print("TypeError")
 
 try:
-    uerrno.errorcode.setdefault(0, 0)
+    errno.errorcode.setdefault(0, 0)
 except TypeError:
     print("TypeError")
 
 try:
-    uerrno.errorcode.update([(1, 2)])
+    errno.errorcode.update([(1, 2)])
 except TypeError:
     print("TypeError")
 
 try:
-    del uerrno.errorcode[1]
+    del errno.errorcode[1]
 except TypeError:
     print("TypeError")
 
 try:
-    uerrno.errorcode[1] = 'foo'
+    errno.errorcode[1] = 'foo'
 except TypeError:
     print("TypeError")
 
 try:
-    uerrno.errorcode.clear()
+    errno.errorcode.clear()
 except TypeError:
     print("TypeError")
 
-assert uerrno.errorcode == errorcode_copy
+assert errno.errorcode == errorcode_copy
diff --git a/tests/basics/dict_views.py b/tests/basics/dict_views.py
index a82f47b6be2..45f3238f1c6 100644
--- a/tests/basics/dict_views.py
+++ b/tests/basics/dict_views.py
@@ -6,6 +6,11 @@
 # print a view with more than one item
 print({1:1, 2:1}.values())
 
+# `bool` and `len` unary ops
+for d in ({}, {1: 2}, {1: 2, 3: 4}):
+    for op in (bool, len):
+        print(op(d.keys()), op(d.values()), op(d.items()))
+
 # unsupported binary op on a dict values view
 try:
     {1:1}.values() + 1
diff --git a/tests/basics/errno1.py b/tests/basics/errno1.py
index d9a895a972d..0773c926fde 100644
--- a/tests/basics/errno1.py
+++ b/tests/basics/errno1.py
@@ -1,25 +1,25 @@
-# test errno's and uerrno module
+# test errno's and errno module
 
 try:
-    import uerrno
+    import errno
 except ImportError:
     print("SKIP")
     raise SystemExit
 
 # check that constants exist and are integers
-print(type(uerrno.EIO))
+print(type(errno.EIO))
 
 # check that errors are rendered in a nice way
-msg = str(OSError(uerrno.EIO))
+msg = str(OSError(errno.EIO))
 print(msg[:7], msg[-5:])
-msg = str(OSError(uerrno.EIO, "details"))
+msg = str(OSError(errno.EIO, "details"))
 print(msg[:7], msg[-14:])
-msg = str(OSError(uerrno.EIO, "details", "more details"))
+msg = str(OSError(errno.EIO, "details", "more details"))
 print(msg[:1], msg[-28:])
 
 # check that unknown errno is still rendered
 print(str(OSError(9999)))
 
 # this tests a failed constant lookup in errno
-errno = uerrno
+errno = errno
 print(errno.__name__)
diff --git a/tests/basics/errno1.py.exp b/tests/basics/errno1.py.exp
index 58605b4767a..7a9ffea8f7c 100644
--- a/tests/basics/errno1.py.exp
+++ b/tests/basics/errno1.py.exp
@@ -3,4 +3,4 @@
 [Errno  ] EIO: details
 ( , 'details', 'more details')
 9999
-uerrno
+errno
diff --git a/tests/basics/exception_chain.py b/tests/basics/exception_chain.py
index c3a7d6b1133..cf19f049500 100644
--- a/tests/basics/exception_chain.py
+++ b/tests/basics/exception_chain.py
@@ -1,6 +1,22 @@
 # Exception chaining is not supported, but check that basic
 # exception works as expected.
+
+import sys
+
+# The unix minimal build doesn't enable MICROPY_WARNINGS (required for this test).
+if getattr(sys.implementation, "_build", None) == "minimal":
+    print("SKIP")
+    raise SystemExit
+
 try:
     raise Exception from None
 except Exception:
     print("Caught Exception")
+
+try:
+    try:
+        raise ValueError("Value")
+    except Exception as exc:
+        raise RuntimeError("Runtime") from exc
+except Exception as ex2:
+    print("Caught Exception:", ex2)
diff --git a/tests/basics/exception_chain.py.exp b/tests/basics/exception_chain.py.exp
index 13635b3cde0..4369e07a2c3 100644
--- a/tests/basics/exception_chain.py.exp
+++ b/tests/basics/exception_chain.py.exp
@@ -1,2 +1,3 @@
-Warning: exception chaining not supported
 Caught Exception
+Warning: exception chaining not supported
+Caught Exception: Runtime
diff --git a/tests/basics/frozenset_set.py b/tests/basics/frozenset_set.py
index 3bf456acfd7..2b3c6832609 100644
--- a/tests/basics/frozenset_set.py
+++ b/tests/basics/frozenset_set.py
@@ -8,5 +8,5 @@
 # "Instances of set are compared to instances of frozenset based on their
 # members. For example:"
 print(set('abc') == frozenset('abc'))
-# This doesn't work in uPy
+# This doesn't work in MicroPython
 #print(set('abc') in set([frozenset('abc')]))
diff --git a/tests/basics/fun_callstar_kwarg.py b/tests/basics/fun_callstar_kwarg.py
new file mode 100644
index 00000000000..15bc19e216e
--- /dev/null
+++ b/tests/basics/fun_callstar_kwarg.py
@@ -0,0 +1,8 @@
+# A CPython syntax feature not supported by MicroPython. For more information
+# see `this issue `_.
+# and tests/cpydiff/core_function_star.py
+
+try:
+    exec("f(y=1, *(3,))")
+except SyntaxError as e:
+    print("SyntaxError")
diff --git a/tests/basics/fun_callstar_kwarg.py.exp b/tests/basics/fun_callstar_kwarg.py.exp
new file mode 100644
index 00000000000..8729fc43437
--- /dev/null
+++ b/tests/basics/fun_callstar_kwarg.py.exp
@@ -0,0 +1 @@
+SyntaxError
diff --git a/tests/basics/fun_code.py b/tests/basics/fun_code.py
new file mode 100644
index 00000000000..841357e8aad
--- /dev/null
+++ b/tests/basics/fun_code.py
@@ -0,0 +1,41 @@
+# Test function.__code__ attribute.
+
+try:
+    (lambda: 0).__code__
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+
+def f():
+    return a
+
+
+ftype = type(f)
+
+# Test __code__ access and function constructor.
+code = f.__code__
+print(type(ftype(code, {})) is ftype)
+
+# Test instantiating multiple code's with different globals dicts.
+code = f.__code__
+f1 = ftype(code, {"a": 1})
+f2 = ftype(code, {"a": 2})
+print(f1(), f2())
+
+# Test bad first argument type.
+try:
+    ftype(None, {})
+except TypeError:
+    print("TypeError")
+
+# Test bad second argument type.
+try:
+    ftype(f.__code__, None)
+except TypeError:
+    print("TypeError")
+
+# Test __code__ on functions with children functions.
+code = (lambda: (lambda: a)).__code__
+print(ftype(code, {"a": 1})()())
+print(ftype(code, {"a": 2})()())
diff --git a/tests/basics/fun_code_colines.py b/tests/basics/fun_code_colines.py
new file mode 100644
index 00000000000..a8867770edd
--- /dev/null
+++ b/tests/basics/fun_code_colines.py
@@ -0,0 +1,81 @@
+# Check that we have sensical bytecode offsets in function.__code__.co_lines
+
+def f1(x, y, obj, obj2, obj3):
+    a = x + y # line 4: bc+4 line+4
+    b = x - y # line 5: bc+4 line+1
+    # line 6
+    # line 7
+    # line 8
+    # line 9
+    # line 10
+    # line 11
+    # line 12
+    # line 13
+    # line 14
+    # line 15
+    # line 16
+    # line 17
+    # line 18
+    # line 19
+    c = a * b # line 20: bc+4 line+15
+    obj.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.fun() # line 21: bc+31 line+1; bc+27 line+0
+    # line 22
+    # line 23
+    # line 24: bc+0 line+3
+    # line 25
+    # line 26
+    # line 27: bc+0 line+3
+    # line 28
+    # line 29
+    obj2.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.fun() # line 30: bc+31 line+3; bc+27 line+0
+    # line 31
+    # line 32
+    # line 33: bc+0 line+3
+    # line 34
+    # line 35
+    # line 36
+    # line 37
+    # line 38
+    # line 39
+    # line 40
+    # line 41
+    # line 42
+    # line 43
+    # line 44
+    # line 45
+    # line 46
+    # line 47
+    # line 48
+    # line 49
+    # line 50
+    # line 51
+    # line 52
+    # line 53
+    # line 54
+    # line 55
+    # line 56
+    # line 57
+    # line 58
+    # line 59
+    return obj3.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.fun() # line 60: bc+31 line+27; bc+27 line+0
+
+def f2(x, y):
+    a = x + y # line 63
+    b = x - y # line 64
+    return a * b # line 65
+
+try:
+    f1.__code__.co_lines
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+print("f1")
+for start, end, line_no in f1.__code__.co_lines():
+    print("line {} start: {}".format(line_no, start))
+    print("line {} end: {}".format(line_no, end))
+
+print("f2")
+for start, end, line_no in f2.__code__.co_lines():
+    print("line {} start: {}".format(line_no, start))
+    print("line {} end: {}".format(line_no, end))
diff --git a/tests/basics/fun_code_colines.py.exp b/tests/basics/fun_code_colines.py.exp
new file mode 100644
index 00000000000..19bd4ef6e2a
--- /dev/null
+++ b/tests/basics/fun_code_colines.py.exp
@@ -0,0 +1,20 @@
+f1
+line 4 start: 0
+line 4 end: 4
+line 5 start: 4
+line 5 end: 8
+line 20 start: 8
+line 20 end: 12
+line 21 start: 12
+line 21 end: 70
+line 30 start: 70
+line 30 end: 128
+line 60 start: 128
+line 60 end: 186
+f2
+line 63 start: 0
+line 63 end: 4
+line 64 start: 4
+line 64 end: 8
+line 65 start: 8
+line 65 end: 12
diff --git a/tests/basics/fun_code_full.py b/tests/basics/fun_code_full.py
new file mode 100644
index 00000000000..e1c867939a2
--- /dev/null
+++ b/tests/basics/fun_code_full.py
@@ -0,0 +1,40 @@
+# Test function.__code__ attributes not available with MICROPY_PY_BUILTINS_CODE <= MICROPY_PY_BUILTINS_CODE_BASIC
+
+try:
+    (lambda: 0).__code__.co_code
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+def f(x, y):
+    a = x + y
+    b = x - y
+    return a * b
+
+code = f.__code__
+
+print(type(code.co_code)) # both bytes (but mpy and cpy have different instruction sets)
+print(code.co_consts) # (not necessarily the same set, but in this function they are)
+print(code.co_filename.rsplit('/')[-1]) # same terminal filename but might be different paths on other ports
+print(type(code.co_firstlineno)) # both ints (but mpy points to first line inside, cpy points to declaration)
+print(code.co_name)
+print(iter(code.co_names) is not None) # both iterable (but mpy returns dict with names as keys, cpy only the names; and not necessarily the same set)
+
+co_lines = code.co_lines()
+
+l = list(co_lines)
+first_start = l[0][0]
+last_end = l[-1][1]
+print(first_start) # co_lines should start at the start of the bytecode
+print(len(code.co_code) - last_end) # and end at the end of the bytecode
+
+prev_end = 0
+for start, end, line_no in l:
+    if end != prev_end:
+        print("non-contiguous")
+        break # the offset ranges should be contiguous
+    prev_end = end
+else:
+    print("contiguous")
+
+
diff --git a/tests/basics/fun_code_full.py.exp b/tests/basics/fun_code_full.py.exp
new file mode 100644
index 00000000000..830effadfc6
--- /dev/null
+++ b/tests/basics/fun_code_full.py.exp
@@ -0,0 +1,9 @@
+
+(None,)
+fun_code_full.py
+
+f
+True
+0
+0
+non-contiguous
diff --git a/tests/basics/fun_code_lnotab.py b/tests/basics/fun_code_lnotab.py
new file mode 100644
index 00000000000..9223e5730f0
--- /dev/null
+++ b/tests/basics/fun_code_lnotab.py
@@ -0,0 +1,34 @@
+# Test deprecation of co_lnotab
+
+try:
+    (lambda: 0).__code__.co_code
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+
+import unittest
+import sys
+
+
+mpy_is_v2 = getattr(sys.implementation, '_v2', False)
+
+
+def f():
+    pass
+
+
+class Test(unittest.TestCase):
+
+    @unittest.skipIf(mpy_is_v2, "Removed in MicroPython v2 and later.")
+    def test_co_lnotab_exists(self):
+        self.assertIsInstance(f.__code__.co_lnotab, bytes)
+
+    @unittest.skipUnless(mpy_is_v2, "Not removed before MicroPython v2.")
+    def test_co_lnotab_removed(self):
+        with self.assertRaises(AttributeError):
+            f.__code__.co_lnotab
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/basics/fun_name.py b/tests/basics/fun_name.py
index b2642280a2a..8aac35eeafa 100644
--- a/tests/basics/fun_name.py
+++ b/tests/basics/fun_name.py
@@ -16,7 +16,7 @@ def Fun(self):
     print('SKIP')
     raise SystemExit
 
-# __name__ of a bound native method is not implemented in uPy
+# __name__ of a bound native method is not implemented in MicroPython
 # the test here is to make sure it doesn't crash
 try:
     str((1).to_bytes.__name__)
diff --git a/tests/basics/gc1.py b/tests/basics/gc1.py
index 332bf9744c0..dff9538b14b 100644
--- a/tests/basics/gc1.py
+++ b/tests/basics/gc1.py
@@ -15,13 +15,13 @@
 gc.collect()
 
 if hasattr(gc, 'mem_free'):
-    # uPy has these extra functions
+    # MicroPython has these extra functions
     # just test they execute and return an int
     assert type(gc.mem_free()) is int
     assert type(gc.mem_alloc()) is int
 
 if hasattr(gc, 'threshold'):
-    # uPy has this extra function
+    # MicroPython has this extra function
     # check execution and returns
     assert(gc.threshold(1) is None)
     assert(gc.threshold() == 0)
diff --git a/tests/basics/gen_yield_from_close.py b/tests/basics/gen_yield_from_close.py
index e3e0116ff7a..9437d9480ec 100644
--- a/tests/basics/gen_yield_from_close.py
+++ b/tests/basics/gen_yield_from_close.py
@@ -37,9 +37,9 @@ def gen4():
     yield -1
     try:
         print((yield from gen3()))
-    except GeneratorExit:
+    except GeneratorExit as e:
         print("delegating caught GeneratorExit")
-        raise
+        raise e
     yield 10
     yield 11
 
@@ -70,9 +70,9 @@ def gen6():
     yield -1
     try:
         print((yield from gen5()))
-    except GeneratorExit:
+    except GeneratorExit as e:
         print("delegating caught GeneratorExit")
-        raise
+        raise e
     yield 10
     yield 11
 
diff --git a/tests/basics/gen_yield_from_throw.py b/tests/basics/gen_yield_from_throw.py
index 1f76e13f3c3..063bed25e20 100644
--- a/tests/basics/gen_yield_from_throw.py
+++ b/tests/basics/gen_yield_from_throw.py
@@ -17,24 +17,6 @@ def gen2():
 except TypeError:
     print("got TypeError from downstream!")
 
-# passing None as second argument to throw
-g = gen2()
-print(next(g))
-print(g.throw(ValueError, None))
-try:
-    print(next(g))
-except TypeError:
-    print("got TypeError from downstream!")
-
-# passing an exception instance as second argument to throw
-g = gen2()
-print(next(g))
-print(g.throw(ValueError, ValueError(123)))
-try:
-    print(next(g))
-except TypeError:
-    print("got TypeError from downstream!")
-
 # thrown value is caught and then generator returns normally
 def gen():
     try:
diff --git a/tests/basics/gen_yield_from_throw_multi_arg.py b/tests/basics/gen_yield_from_throw_multi_arg.py
new file mode 100644
index 00000000000..86d580a6d53
--- /dev/null
+++ b/tests/basics/gen_yield_from_throw_multi_arg.py
@@ -0,0 +1,34 @@
+# Test generator .throw() with multiple arguments.
+# Using multiple arguments is deprecated since CPython 3.12.
+
+
+def gen():
+    try:
+        yield 1
+    except ValueError as e:
+        print("got ValueError from upstream!", repr(e.args))
+    yield "str1"
+    raise TypeError
+
+
+def gen2():
+    print((yield from gen()))
+
+
+# Passing None as second argument to throw.
+g = gen2()
+print(next(g))
+print(g.throw(ValueError, None))
+try:
+    print(next(g))
+except TypeError:
+    print("got TypeError from downstream!")
+
+# Passing an exception instance as second argument to throw.
+g = gen2()
+print(next(g))
+print(g.throw(ValueError, ValueError(123)))
+try:
+    print(next(g))
+except TypeError:
+    print("got TypeError from downstream!")
diff --git a/tests/basics/gen_yield_from_throw_multi_arg.py.exp b/tests/basics/gen_yield_from_throw_multi_arg.py.exp
new file mode 100644
index 00000000000..218e332e501
--- /dev/null
+++ b/tests/basics/gen_yield_from_throw_multi_arg.py.exp
@@ -0,0 +1,8 @@
+1
+got ValueError from upstream! ()
+str1
+got TypeError from downstream!
+1
+got ValueError from upstream! (123,)
+str1
+got TypeError from downstream!
diff --git a/tests/basics/gen_yield_from_throw_repeat.py b/tests/basics/gen_yield_from_throw_repeat.py
new file mode 100644
index 00000000000..96636a62443
--- /dev/null
+++ b/tests/basics/gen_yield_from_throw_repeat.py
@@ -0,0 +1,22 @@
+# Test throwing repeatedly into the same generator, where that generator
+# is yielding from another generator.
+
+
+def yielder():
+    yield 4
+    yield 5
+
+
+def gen():
+    while True:
+        try:
+            print("gen received:", (yield from yielder()))
+        except ValueError as exc:
+            print(repr(exc))
+
+
+g = gen()
+for i in range(2):
+    print("send, got:", g.send(None))
+    print("throw, got:", g.throw(ValueError("a", i)))
+    print("throw, got:", g.throw(ValueError("b", i)))
diff --git a/tests/basics/generator_pend_throw.py b/tests/basics/generator_pend_throw.py
index ae8c21189e9..6cecd418dd0 100644
--- a/tests/basics/generator_pend_throw.py
+++ b/tests/basics/generator_pend_throw.py
@@ -46,7 +46,7 @@ def gen_next():
 try:
     next(g)
 except Exception as e:
-    print("raised", repr(e))
+    print("raised {} {}".format(type(e), str(e) or "generator already executing"))
 
 
 # Verify that you can't pend_throw from within the running coroutine.
@@ -59,7 +59,7 @@ def gen_pend_throw():
 try:
     next(g)
 except Exception as e:
-    print("raised", repr(e))
+    print("raised {} {}".format(type(e), str(e) or "generator already executing"))
 
 
 # Verify that the pend_throw exception can be ignored.
diff --git a/tests/basics/generator_pend_throw.py.exp b/tests/basics/generator_pend_throw.py.exp
index 8a3dadfec7a..56fa68be252 100644
--- a/tests/basics/generator_pend_throw.py.exp
+++ b/tests/basics/generator_pend_throw.py.exp
@@ -3,8 +3,8 @@
 raised ValueError()
 ret was: None
 raised OSError()
-raised ValueError('generator already executing',)
-raised ValueError('generator already executing',)
+raised  generator already executing
+raised  generator already executing
 0
 ignore CancelledError
 1
diff --git a/tests/basics/generator_throw.py b/tests/basics/generator_throw.py
index 1b43c125eab..536f5008236 100644
--- a/tests/basics/generator_throw.py
+++ b/tests/basics/generator_throw.py
@@ -2,7 +2,7 @@
 def gen():
     yield 123
     yield 456
-        
+
 g = gen()
 print(next(g))
 try:
@@ -31,7 +31,7 @@ def gen():
     except GeneratorExit as e:
         print('GeneratorExit', repr(e.args))
     yield 456
-        
+
 # thrown a class
 g = gen()
 print(next(g))
@@ -41,13 +41,3 @@ def gen():
 g = gen()
 print(next(g))
 print(g.throw(GeneratorExit()))
-
-# thrown an instance with None as second arg
-g = gen()
-print(next(g))
-print(g.throw(GeneratorExit(), None))
-
-# thrown a class and instance
-g = gen()
-print(next(g))
-print(g.throw(GeneratorExit, GeneratorExit(123)))
diff --git a/tests/basics/generator_throw_multi_arg.py b/tests/basics/generator_throw_multi_arg.py
new file mode 100644
index 00000000000..acdb6a06c96
--- /dev/null
+++ b/tests/basics/generator_throw_multi_arg.py
@@ -0,0 +1,21 @@
+# Test generator .throw() with multiple arguments.
+# Using multiple arguments is deprecated since CPython 3.12.
+
+# Generator ignores a thrown GeneratorExit (this is allowed).
+def gen():
+    try:
+        yield 123
+    except GeneratorExit as e:
+        print("GeneratorExit", repr(e.args))
+    yield 456
+
+
+# Thrown an instance with None as second arg.
+g = gen()
+print(next(g))
+print(g.throw(GeneratorExit(), None))
+
+# Thrown a class and instance.
+g = gen()
+print(next(g))
+print(g.throw(GeneratorExit, GeneratorExit(123)))
diff --git a/tests/basics/generator_throw_multi_arg.py.exp b/tests/basics/generator_throw_multi_arg.py.exp
new file mode 100644
index 00000000000..6e976eab1db
--- /dev/null
+++ b/tests/basics/generator_throw_multi_arg.py.exp
@@ -0,0 +1,6 @@
+123
+GeneratorExit ()
+456
+123
+GeneratorExit (123,)
+456
diff --git a/tests/basics/generator_throw_repeat.py b/tests/basics/generator_throw_repeat.py
new file mode 100644
index 00000000000..6d6ef60a9b0
--- /dev/null
+++ b/tests/basics/generator_throw_repeat.py
@@ -0,0 +1,16 @@
+# Test throwing repeatedly into the same generator.
+
+
+def gen():
+    while True:
+        try:
+            print("gen received:", (yield "value"))
+        except ValueError as exc:
+            print(repr(exc))
+
+
+g = gen()
+for i in range(2):
+    print("send, got:", g.send(None))
+    print("throw, got:", g.throw(ValueError("a", i)))
+    print("throw, got:", g.throw(ValueError("b", i)))
diff --git a/tests/basics/import_instance_method.py b/tests/basics/import_instance_method.py
new file mode 100644
index 00000000000..d25b70ac5f0
--- /dev/null
+++ b/tests/basics/import_instance_method.py
@@ -0,0 +1,38 @@
+# Test importing a method from a class instance.
+# This is not a common thing to do, but ensures MicroPython has the same semantics as CPython.
+
+import sys
+
+if not hasattr(sys, "modules"):
+    print("SKIP")
+    raise SystemExit
+
+
+class A:
+    def __init__(self, value):
+        self.value = value
+
+    def meth(self):
+        return self.value
+
+    def meth_with_arg(self, a):
+        return [self.value, a]
+
+
+# Register a class instance as the module "mod".
+sys.modules["mod"] = A(1)
+
+# Try importing it as a module.
+import mod
+
+print(mod.meth())
+print(mod.meth_with_arg(2))
+
+# Change the module.
+sys.modules["mod"] = A(3)
+
+# Try importing it using "from ... import".
+from mod import meth, meth_with_arg
+
+print(meth())
+print(meth_with_arg(4))
diff --git a/tests/basics/import_star_nonmodule.py b/tests/basics/import_star_nonmodule.py
new file mode 100644
index 00000000000..8a98ef26ce5
--- /dev/null
+++ b/tests/basics/import_star_nonmodule.py
@@ -0,0 +1,65 @@
+# Test "from x import *" where x is something other than a module.
+
+import sys
+
+try:
+    next(iter([]), 42)
+except TypeError:
+    # Two-argument version of next() not supported.  We are probably not at
+    # MICROPY_CONFIG_ROM_LEVEL_BASIC_FEATURES which is needed for "import *".
+    print("SKIP")
+    raise SystemExit
+
+print("== test with a class as a module ==")
+
+
+class M:
+    x = "a1"
+
+    def __init__(self):
+        self.x = "a2"
+
+
+sys.modules["mod"] = M
+from mod import *
+
+print(x)
+
+sys.modules["mod"] = M()
+from mod import *
+
+print(x)
+
+print("== test with a class as a module that overrides __all__ ==")
+
+
+class M:
+    __all__ = ("y",)
+    x = "b1"
+    y = "b2"
+
+    def __init__(self):
+        self.__all__ = ("x",)
+        self.x = "b3"
+        self.y = "b4"
+
+
+sys.modules["mod"] = M
+x = None
+from mod import *
+
+print(x, y)
+
+sys.modules["mod"] = M()
+from mod import *
+
+print(x, y)
+
+print("== test with objects that don't have a __dict__ ==")
+
+sys.modules["mod"] = 1
+try:
+    from mod import *
+    # MicroPython raises AttributeError, CPython raises ImportError.
+except (AttributeError, ImportError):
+    print("ImportError")
diff --git a/tests/basics/int1.py b/tests/basics/int1.py
index 2d92105c73e..94723af4d00 100644
--- a/tests/basics/int1.py
+++ b/tests/basics/int1.py
@@ -13,6 +13,7 @@
 print(int('+1'))
 print(int('-1'))
 print(int('01'))
+print(int('00'))
 print(int('9'))
 print(int('10'))
 print(int('+10'))
@@ -31,6 +32,7 @@
 print(int('0', 10))
 print(int('1', 10))
 print(int(' \t 1 \t ', 10))
+print(int(' \t 00 \t ', 10))
 print(int('11', 10))
 print(int('11', 16))
 print(int('11', 8))
@@ -52,6 +54,17 @@
 print(int('0o12  \t  ', 8))
 print(int(b"12", 10))
 print(int(b"12"))
+print(int('000 ', 0))
+print(int('000 ', 2))
+print(int('000 ', 8))
+print(int('000 ', 10))
+print(int('000 ', 16))
+print(int('000 ', 36))
+print(int('010 ', 2))
+print(int('010 ', 8))
+print(int('010 ', 10))
+print(int('010 ', 16))
+print(int('010 ', 36))
 
 
 def test(value, base):
@@ -79,6 +92,8 @@ def test(value, base):
 test('0xg', 16)
 test('1 1', 16)
 test('123', 37)
+test('01', 0)
+test('01 ', 0)
 
 # check that we don't parse this as a floating point number
 print(0x1e+1)
diff --git a/tests/basics/int_64_basics.py b/tests/basics/int_64_basics.py
new file mode 100644
index 00000000000..b9d46c7d701
--- /dev/null
+++ b/tests/basics/int_64_basics.py
@@ -0,0 +1,161 @@
+# test support for 64-bit long integers
+# (some ports don't support arbitrary precision but do support these)
+
+# this test is adapted from int_big1.py with numbers kept within 64-bit signed range
+
+# to test arbitrary precision integers
+
+x = 1000000000000000000
+xn = -1000000000000000000
+y = 2000000000000000000
+
+# printing
+print(x)
+print(y)
+print('%#X' % (x - x)) # print prefix
+print('{:#,}'.format(x)) # print with commas
+
+# construction
+print(int(x))
+
+# addition
+print(x + 1)
+print(x + y)
+print(x + xn == 0)
+print(bool(x + xn))
+
+# subtraction
+print(x - 1)
+print(x - y)
+print(y - x)
+print(x - x == 0)
+print(bool(x - x))
+
+# multiplication
+print(x * 2)
+print(1090511627776 * 1048500)
+
+# integer division
+print(x // 2)
+print(y // x)
+
+# bit inversion
+print(~x)
+print(~(-x))
+
+# left shift
+print("left shift positive")
+x = 0x40000000
+for i in range(32):
+    x = x << 1
+    print(x)
+
+# right shift
+print("right shift positive")
+x = 0x2000000000000000  # TODO: why can't second-tip bit be set?
+for i in range(64):
+    x = x >> 1
+    print(x)
+
+# left shift of a negative number
+print("left shift negative")
+for i in range(8):
+    print(-10000000000000000 << i)
+    print(-10000000000000001 << i)
+    print(-10000000000000002 << i)
+    print(-10000000000000003 << i)
+    print(-10000000000000004 << i)
+
+
+# right shift of a negative number
+print("right shift negative")
+for i in range(8):
+    print(-1000000000000000000 >> i)
+    print(-1000000000000000001 >> i)
+    print(-1000000000000000002 >> i)
+    print(-1000000000000000003 >> i)
+    print(-1000000000000000004 >> i)
+
+# conversion from string
+print(int("1234567890123456789"))
+print(int("-1234567890123456789"))
+print(int("1234567890abcdef", 16))
+print(int("1234567890ABCDEF", 16))
+print(int("-1234567890ABCDEF", 16))
+print(int("ijklmnopqrsz", 36))
+
+# numbers close to 64-bit limits
+print(int("-9111222333444555666"))
+print(int("9111222333444555666"))
+
+# numbers with preceding 0s
+print(int("-00000000000000000000009111222333444555666"))
+print(int("0000000000000000000000009111222333444555666"))
+
+# invalid characters in string
+try:
+    print(int("1234567890abcdef"))
+except ValueError:
+    print('ValueError');
+try:
+    print(int("123456789\x01"))
+except ValueError:
+    print('ValueError');
+
+# test parsing ints just on threshold of small to big
+# for 32 bit archs
+x = 1073741823 # small
+x = -1073741823 # small
+x = 1073741824 # big
+x = -1073741824 # big
+# for 64 bit archs
+x = 4611686018427387903 # small
+x = -4611686018427387903 # small
+x = 4611686018427387904 # big
+x = -4611686018427387904 # big
+
+# sys.maxsize is a constant bigint, so test it's compatible with dynamic ones
+import sys
+if hasattr(sys, "maxsize"):
+    print(sys.maxsize - 1 + 1 == sys.maxsize)
+else:
+    print(True)  # No maxsize property in this config
+
+# test extraction of big int value via mp_obj_get_int_maybe
+x = 1 << 62
+print('a' * (x + 4 - x))
+
+# test overflow check in mp_obj_get_int_maybe
+x = 1 << 32
+r = None
+try:
+    r = range(0, x)
+except OverflowError:
+    # 32-bit target, correctly handled the overflow of x
+    print("ok")
+if r is not None:
+    if len(r) == x:
+        # 64-bit target, everything is just a small-int
+        print("ok")
+    else:
+        # 32-bit target that did not handle the overflow of x
+        print("unhandled overflow")
+
+# negative shifts are invalid
+try:
+    print((1 << 48) >> -4)
+except ValueError as e:
+    print(str(e) or "negative shift count")
+
+try:
+    print((1 << 48) << -6)
+except ValueError as e:
+    print(str(e) or "negative shift count")
+
+# Test that the most extreme 64 bit integer values all parse with int()
+print(int("-9223372036854775807"))
+print(int("-9223372036854775808"))
+print(int("9223372036854775807"))
+
+# Test that the most negative 64 bit integer can be formed via arithmetic
+print(-9223372036854775807-1)
diff --git a/tests/basics/int_big1.py b/tests/basics/int_big1.py
index ea48372b283..de2ba9fc4f2 100644
--- a/tests/basics/int_big1.py
+++ b/tests/basics/int_big1.py
@@ -105,10 +105,7 @@
 x = -4611686018427387904 # big
 
 # sys.maxsize is a constant mpz, so test it's compatible with dynamic ones
-try:
-    import usys as sys
-except ImportError:
-    import sys
+import sys
 print(sys.maxsize + 1 - 1 == sys.maxsize)
 
 # test extraction of big int value via mp_obj_get_int_maybe
diff --git a/tests/basics/int_big_and2.py b/tests/basics/int_big_and2.py
index f8c81fe0cd2..045a90ef750 100644
--- a/tests/basics/int_big_and2.py
+++ b/tests/basics/int_big_and2.py
@@ -1,4 +1,4 @@
-# test + + 
+# test + +
 
 print( 97989513389222316022151446562729620153292831887555425160965597396
      & 23716683549865351578586448630079789776107310103486834795830390982)
@@ -24,7 +24,7 @@
 print( 40019818573920230246248826511203818792007462193311949166285967147
      & 9487909752)
 
-# test - - 
+# test - -
 
 print( -97989513389222316022151446562729620153292831887555425160965597396
      & -23716683549865351578586448630079789776107310103486834795830390982)
diff --git a/tests/basics/int_big_and3.py b/tests/basics/int_big_and3.py
index 788ecd823bc..7a3b415af71 100644
--- a/tests/basics/int_big_and3.py
+++ b/tests/basics/int_big_and3.py
@@ -1,4 +1,4 @@
-# test - + 
+# test - +
 
 print( -97989513389222316022151446562729620153292831887555425160965597396
      & 23716683549865351578586448630079789776107310103486834795830390982)
@@ -24,7 +24,7 @@
 print( -40019818573920230246248826511203818792007462193311949166285967147
      & 9487909752)
 
-# test + - 
+# test + -
 
 print( 97989513389222316022151446562729620153292831887555425160965597396
      & -23716683549865351578586448630079789776107310103486834795830390982)
diff --git a/tests/basics/int_big_or.py b/tests/basics/int_big_or.py
index 17d9935265f..ccded377812 100644
--- a/tests/basics/int_big_or.py
+++ b/tests/basics/int_big_or.py
@@ -3,7 +3,7 @@
 a = 0xfffffffffffffffffffffffffffff
 print(a | (1 << 200))
 
-# test + + 
+# test + +
 
 print(0 | (1 << 80))
 print((1 << 80) | (1 << 80))
@@ -15,7 +15,7 @@
 print(a | a == 0)
 print(bool(a | a))
 
-# test - + 
+# test - +
 
 print((-1 << 80) | (1 << 80))
 print((-1 << 80) | 0)
@@ -25,7 +25,7 @@
 print((-a) | a == 0)
 print(bool((-a) | a))
 
-# test + - 
+# test + -
 
 print(0 | (-1 << 80))
 print((1 << 80) | (-1 << 80))
@@ -35,7 +35,7 @@
 print(a | (-a) == 0)
 print(bool(a | (-a)))
 
-# test - - 
+# test - -
 
 print((-1 << 80) | (-1 << 80))
 
diff --git a/tests/basics/int_big_or2.py b/tests/basics/int_big_or2.py
index 255495150ab..92d76c77034 100644
--- a/tests/basics/int_big_or2.py
+++ b/tests/basics/int_big_or2.py
@@ -1,4 +1,4 @@
-# test + + 
+# test + +
 print( 97989513389222316022151446562729620153292831887555425160965597396
      | 23716683549865351578586448630079789776107310103486834795830390982)
 
@@ -23,7 +23,7 @@
 print( 40019818573920230246248826511203818792007462193311949166285967147
      | 9487909752)
 
-# test - - 
+# test - -
 
 print( -97989513389222316022151446562729620153292831887555425160965597396
      | -23716683549865351578586448630079789776107310103486834795830390982)
diff --git a/tests/basics/int_big_or3.py b/tests/basics/int_big_or3.py
index 07edaea2df9..212b3da8a64 100644
--- a/tests/basics/int_big_or3.py
+++ b/tests/basics/int_big_or3.py
@@ -1,4 +1,4 @@
-# test - + 
+# test - +
 
 print( -97989513389222316022151446562729620153292831887555425160965597396
      | 23716683549865351578586448630079789776107310103486834795830390982)
@@ -24,7 +24,7 @@
 print( -40019818573920230246248826511203818792007462193311949166285967147
      | 9487909752)
 
-# test + - 
+# test + -
 
 print( 97989513389222316022151446562729620153292831887555425160965597396
      | -23716683549865351578586448630079789776107310103486834795830390982)
diff --git a/tests/basics/int_big_to_small.py b/tests/basics/int_big_to_small.py
new file mode 100644
index 00000000000..c92d263fc28
--- /dev/null
+++ b/tests/basics/int_big_to_small.py
@@ -0,0 +1,33 @@
+try:
+    import micropython
+    micropython.heap_lock
+except:
+    print("SKIP")
+    raise SystemExit
+
+# Skip this test on "REPR B" where (1<<29 + 1) is not a small int.
+k = 1 << 29
+micropython.heap_lock()
+try:
+    k = k + 1
+except MemoryError:
+    print("SKIP")
+    raise SystemExit
+finally:
+    micropython.heap_unlock()
+
+# All less than small int max.
+for d in (0, 27, 1<<29, -1861, -(1<<29)):
+    i = 1<<70
+    print(i)
+    j = (1<<70) + d
+    print(j)
+    # k should now be a small int.
+    k = j - i
+    print(k)
+
+    # Now verify that working with k doesn't allocate (i.e. it's a small int).
+    micropython.heap_lock()
+    print(k + 20)
+    print(k // 20)
+    micropython.heap_unlock()
diff --git a/tests/basics/int_big_to_small.py.exp b/tests/basics/int_big_to_small.py.exp
new file mode 100644
index 00000000000..1d4986e0443
--- /dev/null
+++ b/tests/basics/int_big_to_small.py.exp
@@ -0,0 +1,25 @@
+1180591620717411303424
+1180591620717411303424
+0
+20
+0
+1180591620717411303424
+1180591620717411303451
+27
+47
+1
+1180591620717411303424
+1180591620717948174336
+536870912
+536870932
+26843545
+1180591620717411303424
+1180591620717411301563
+-1861
+-1841
+-94
+1180591620717411303424
+1180591620716874432512
+-536870912
+-536870892
+-26843546
diff --git a/tests/basics/int_big_to_small_int29.py b/tests/basics/int_big_to_small_int29.py
new file mode 100644
index 00000000000..438a74d2bc6
--- /dev/null
+++ b/tests/basics/int_big_to_small_int29.py
@@ -0,0 +1,22 @@
+try:
+    import micropython
+    micropython.heap_lock
+except:
+    print("SKIP")
+    raise SystemExit
+
+# All less than small int max.
+for d in (0, 27, 1<<28, -1861, -(1<<28)):
+    i = 1<<70
+    print(i)
+    j = (1<<70) + d
+    print(j)
+    # k should now be a small int.
+    k = j - i
+    print(k)
+
+    # Now verify that working with k doesn't allocate (i.e. it's a small int).
+    micropython.heap_lock()
+    print(k + 20)
+    print(k // 20)
+    micropython.heap_unlock()
diff --git a/tests/basics/int_big_to_small_int29.py.exp b/tests/basics/int_big_to_small_int29.py.exp
new file mode 100644
index 00000000000..0920101924a
--- /dev/null
+++ b/tests/basics/int_big_to_small_int29.py.exp
@@ -0,0 +1,25 @@
+1180591620717411303424
+1180591620717411303424
+0
+20
+0
+1180591620717411303424
+1180591620717411303451
+27
+47
+1
+1180591620717411303424
+1180591620717679738880
+268435456
+268435476
+13421772
+1180591620717411303424
+1180591620717411301563
+-1861
+-1841
+-94
+1180591620717411303424
+1180591620717142867968
+-268435456
+-268435436
+-13421773
diff --git a/tests/basics/int_big_xor.py b/tests/basics/int_big_xor.py
index cd1d9ae97fa..28b191d3d94 100644
--- a/tests/basics/int_big_xor.py
+++ b/tests/basics/int_big_xor.py
@@ -1,4 +1,4 @@
-# test + + 
+# test + +
 
 print(0 ^ (1 << 80))
 print((1 << 80) ^ (1 << 80))
@@ -10,7 +10,7 @@
 print(a ^ a == 0)
 print(bool(a ^ a))
 
-# test - + 
+# test - +
 
 print((-1 << 80) ^ (1 << 80))
 print((-1 << 80) ^ 0)
@@ -22,7 +22,7 @@
 i = -1
 print(i ^ 0xffffffffffffffff) # carry overflows to higher digit
 
-# test + - 
+# test + -
 
 print(0 ^ (-1 << 80))
 print((1 << 80) ^ (-1 << 80))
@@ -32,7 +32,7 @@
 print(a ^ (-a) == 0)
 print(bool(a ^ (-a)))
 
-# test - - 
+# test - -
 
 print((-1 << 80) ^ (-1 << 80))
 
diff --git a/tests/basics/int_big_xor2.py b/tests/basics/int_big_xor2.py
index b5b3db60c69..21ef842f866 100644
--- a/tests/basics/int_big_xor2.py
+++ b/tests/basics/int_big_xor2.py
@@ -1,4 +1,4 @@
-# test + + 
+# test + +
 
 print( 97989513389222316022151446562729620153292831887555425160965597396
      ^ 23716683549865351578586448630079789776107310103486834795830390982)
@@ -24,7 +24,7 @@
 print( 40019818573920230246248826511203818792007462193311949166285967147
      ^ 9487909752)
 
-# test - - 
+# test - -
 
 print( -97989513389222316022151446562729620153292831887555425160965597396
      ^ -23716683549865351578586448630079789776107310103486834795830390982)
diff --git a/tests/basics/int_big_xor3.py b/tests/basics/int_big_xor3.py
index 00881e37409..ff5e9aeff2f 100644
--- a/tests/basics/int_big_xor3.py
+++ b/tests/basics/int_big_xor3.py
@@ -1,4 +1,4 @@
-# test - + 
+# test - +
 
 print( -97989513389222316022151446562729620153292831887555425160965597396
      ^ 23716683549865351578586448630079789776107310103486834795830390982)
@@ -24,7 +24,7 @@
 print( -40019818573920230246248826511203818792007462193311949166285967147
      ^ 9487909752)
 
-# test + - 
+# test + -
 
 print( 97989513389222316022151446562729620153292831887555425160965597396
      ^ -23716683549865351578586448630079789776107310103486834795830390982)
diff --git a/tests/basics/int_bytes.py b/tests/basics/int_bytes.py
index d1837ea75ca..7a1edc2ccec 100644
--- a/tests/basics/int_bytes.py
+++ b/tests/basics/int_bytes.py
@@ -1,9 +1,16 @@
+import sys
+
 print((10).to_bytes(1, "little"))
+print((-10).to_bytes(1, "little", signed=True))
+# Test fitting in length that's not a power of two.
+print((0x10000).to_bytes(3, "little"))
 print((111111).to_bytes(4, "little"))
+print((-111111).to_bytes(4, "little", signed=True))
 print((100).to_bytes(10, "little"))
 print(int.from_bytes(b"\x00\x01\0\0\0\0\0\0", "little"))
 print(int.from_bytes(b"\x01\0\0\0\0\0\0\0", "little"))
 print(int.from_bytes(b"\x00\x01\0\0\0\0\0\0", "little"))
+print((-100).to_bytes(10, "little", signed=True))
 
 # check that extra zero bytes don't change the internal int value
 print(int.from_bytes(bytes(20), "little") == 0)
@@ -11,7 +18,9 @@
 
 # big-endian conversion
 print((10).to_bytes(1, "big"))
+print((-10).to_bytes(1, "big", signed=True))
 print((100).to_bytes(10, "big"))
+print((-100).to_bytes(10, "big", signed=True))
 print(int.from_bytes(b"\0\0\0\0\0\0\0\0\0\x01", "big"))
 print(int.from_bytes(b"\x01\0", "big"))
 
@@ -20,3 +29,74 @@
     (1).to_bytes(-1, "little")
 except ValueError:
     print("ValueError")
+
+# zero byte destination should also raise an error
+try:
+    (1).to_bytes(0, "little")
+except OverflowError:
+    print("OverflowError")
+
+# except for converting 0 to a zero-length byte array
+print((0).to_bytes(0, "big"))
+
+# byte length can fit the integer directly
+print((0xFF).to_bytes(1, "little"))
+print((0xFF).to_bytes(1, "big"))
+print((0xEFF).to_bytes(2, "little"))
+print((0xEFF).to_bytes(2, "big"))
+print((0xCDEFF).to_bytes(3, "little"))
+print((0xCDEFF).to_bytes(3, "big"))
+
+# OverFlowError if not big enough
+
+try:
+    (0x123).to_bytes(1, "big")
+except OverflowError:
+    print("OverflowError")
+
+try:
+    (0x12345).to_bytes(2, "big")
+except OverflowError:
+    print("OverflowError")
+
+try:
+    (0x1234567).to_bytes(3, "big")
+except OverflowError:
+    print("OverflowError")
+
+
+# negative representations
+
+print((-1).to_bytes(1, "little", signed=True))
+print((-1).to_bytes(3, "little", signed=True))
+print((-1).to_bytes(1, "big", signed=True))
+print((-1).to_bytes(3, "big", signed=True))
+print((-128).to_bytes(1, "big", signed=True))
+print((-32768).to_bytes(2, "big", signed=True))
+print((-(1 << 23)).to_bytes(3, "big", signed=True))
+
+# negative numbers should raise an error if signed=False, regardless of fitting or not
+try:
+    (-256).to_bytes(2, "little", signed=False)
+except OverflowError:
+    print("OverflowError")
+
+try:
+    (-1).to_bytes(1, "little")
+except OverflowError:
+    print("OverflowError")
+
+try:
+    print((-129).to_bytes(1, "big"))
+except OverflowError:
+    print("OverflowError")
+
+try:
+    print((-32769).to_bytes(2, "big"))
+except OverflowError:
+    print("OverflowError")
+
+try:
+    print((-(1 << 23) - 1).to_bytes(2, "big"))
+except OverflowError:
+    print("OverflowError")
diff --git a/tests/basics/int_bytes_int64.py b/tests/basics/int_bytes_int64.py
new file mode 100644
index 00000000000..2ca87f20dee
--- /dev/null
+++ b/tests/basics/int_bytes_int64.py
@@ -0,0 +1,43 @@
+import sys
+
+# Depending on the port, the numbers in this test may be implemented as "small"
+# native 64 bit ints, arbitrary precision large ints, or large integers using 64-bit
+# long longs.
+
+try:
+    x = int.from_bytes(b"\x6f\xab\xcd\x12\x34\x56\x78\xfb", "big")
+except OverflowError:
+    print("SKIP")  # Port can't represent this size of integer at all
+    raise SystemExit
+
+print(hex(x))
+b = x.to_bytes(8, "little")
+print(b)
+print(x.to_bytes(8, "big"))
+
+# padding in output
+print(x.to_bytes(20, "little"))
+print(x.to_bytes(20, "big"))
+
+# check that extra zero bytes don't change the internal int value
+print(int.from_bytes(b + bytes(10), "little") == x)
+
+# can't write to a zero-length bytes object
+try:
+    x.to_bytes(0, "little")
+except OverflowError:
+    print("OverflowError")
+
+# or one that it too short
+try:
+    x.to_bytes(7, "big")
+except OverflowError:
+    print("OverflowError")
+
+# negative representations
+
+x = -x
+
+print(x.to_bytes(8, "little", signed=True))
+print(x.to_bytes(20, "big", signed=True))
+print(x.to_bytes(20, "little", signed=True))
diff --git a/tests/basics/int_bytes_intbig.py b/tests/basics/int_bytes_intbig.py
index 147362bef13..4b6c3491e75 100644
--- a/tests/basics/int_bytes_intbig.py
+++ b/tests/basics/int_bytes_intbig.py
@@ -1,5 +1,9 @@
+import sys
+
 print((2**64).to_bytes(9, "little"))
+print((-(2**64)).to_bytes(9, "little", signed=True))
 print((2**64).to_bytes(9, "big"))
+print((-(2**64)).to_bytes(9, "big", signed=True))
 
 b = bytes(range(20))
 
@@ -10,5 +14,76 @@
 print(il.to_bytes(20, "little"))
 print(ib.to_bytes(20, "big"))
 
+# check padding comes out correctly
+print(il.to_bytes(40, "little"))
+print(ib.to_bytes(40, "big"))
+
 # check that extra zero bytes don't change the internal int value
 print(int.from_bytes(b + bytes(10), "little") == int.from_bytes(b, "little"))
+
+# can't write to a zero-length bytes object
+try:
+    ib.to_bytes(0, "little")
+except OverflowError:
+    print("OverflowError")
+
+# or one that is too short
+try:
+    ib.to_bytes(18, "big")
+except OverflowError:
+    print("OverflowError")
+
+# including when signed
+try:
+    ib.to_bytes(18, "big", signed=True)
+except OverflowError:
+    print("OverflowError")
+
+# negative representations
+
+# negative numbers should raise an error if signed=False
+try:
+    (-(2**64)).to_bytes(9, "little", signed=False)
+except OverflowError:
+    print("OverflowError")
+
+
+print((-ib).to_bytes(20, "big", signed=True))
+print((ib * -ib).to_bytes(40, "big", signed=True))
+
+# cases where an additional byte is needed for sign bit
+
+MAX_U24 = (2**24) - 1
+MAX_U32 = (2**32) - 1
+MAX_U64 = (2**64) - 1
+
+for ib, nbytes in (
+    (-127, 1),
+    (255, 1),
+    (-255, 1),
+    (65535, 2),
+    (-65535, 2),
+    (-65534, 2),
+    (MAX_U24, 3),
+    (-MAX_U24, 3),
+    (1 - MAX_U24, 3),
+    (MAX_U32, 4),
+    (-MAX_U32, 4),
+    (1 - MAX_U32, 4),
+    (2 - MAX_U32, 4),
+    (MAX_U64, 8),
+    (-MAX_U64, 8),
+    (1 - MAX_U64, 8),
+    (2 - MAX_U64, 8),
+    (2 * MAX_U64, 8),
+    (-2 * MAX_U64, 8),
+):
+    print("ib", hex(ib), "nbytes", nbytes, ":")
+    for signed in False, True:
+        for endian in "little", "big":
+            for nbytes_offs in (-1, 0, 1):
+                try:
+                    as_bytes = ib.to_bytes(nbytes + nbytes_offs, endian, signed=signed)
+                except OverflowError:
+                    as_bytes = "OverflowError"
+                print(as_bytes, "signed", signed, "endian", endian, "nbytes_offs", nbytes_offs)
diff --git a/tests/basics/int_bytes_optional_args_cp311.py b/tests/basics/int_bytes_optional_args_cp311.py
new file mode 100644
index 00000000000..07fdcdd2821
--- /dev/null
+++ b/tests/basics/int_bytes_optional_args_cp311.py
@@ -0,0 +1,9 @@
+# Check optional byteorder argument (CPython 3.11+)
+print((10).to_bytes(1))
+print((100).to_bytes(10))
+print(int.from_bytes(b"\0\0\0\0\0\0\0\0\0\x01"))
+print(int.from_bytes(b"\x01\0"))
+
+# Check optional length argument (CPython 3.11+)
+print((10).to_bytes())
+print((100).to_bytes())
diff --git a/tests/basics/int_bytes_optional_args_cp311.py.exp b/tests/basics/int_bytes_optional_args_cp311.py.exp
new file mode 100644
index 00000000000..6dffa2577fc
--- /dev/null
+++ b/tests/basics/int_bytes_optional_args_cp311.py.exp
@@ -0,0 +1,6 @@
+b'\n'
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x00d'
+1
+256
+b'\n'
+b'd'
diff --git a/tests/basics/io_buffered_writer.py b/tests/basics/io_buffered_writer.py
index 0e943cb0a90..3cfee0103f7 100644
--- a/tests/basics/io_buffered_writer.py
+++ b/tests/basics/io_buffered_writer.py
@@ -1,9 +1,9 @@
-import uio as io
-
 try:
+    import io
+
     io.BytesIO
     io.BufferedWriter
-except AttributeError:
+except (AttributeError, ImportError):
     print('SKIP')
     raise SystemExit
 
@@ -28,3 +28,27 @@
 
 # hashing a BufferedWriter
 print(type(hash(buf)))
+
+# Test failing flush()
+class MyIO(io.IOBase):
+    def __init__(self):
+        self.count = 0
+
+    def write(self, buf):
+        self.count += 1
+        if self.count < 3:
+            return None
+        print("writing", buf)
+        return len(buf)
+
+
+buf = io.BufferedWriter(MyIO(), 8)
+
+buf.write(b"foobar")
+
+for _ in range(4):
+    try:
+        buf.flush()
+        print("flushed")
+    except OSError:
+        print("OSError")
diff --git a/tests/basics/io_buffered_writer.py.exp b/tests/basics/io_buffered_writer.py.exp
index 2209348f5af..d61eb148b45 100644
--- a/tests/basics/io_buffered_writer.py.exp
+++ b/tests/basics/io_buffered_writer.py.exp
@@ -4,3 +4,8 @@ b'foobarfoobar'
 b'foobarfoobar'
 b'foo'
 
+OSError
+OSError
+writing bytearray(b'foobar')
+flushed
+flushed
diff --git a/tests/basics/io_bytesio_cow.py b/tests/basics/io_bytesio_cow.py
index 92654a0003f..543c12ad42a 100644
--- a/tests/basics/io_bytesio_cow.py
+++ b/tests/basics/io_bytesio_cow.py
@@ -1,9 +1,11 @@
 # Make sure that write operations on io.BytesIO don't
 # change original object it was constructed from.
+
 try:
-    import uio as io
-except ImportError:
     import io
+except ImportError:
+    print("SKIP")
+    raise SystemExit
 
 b = b"foobar"
 
diff --git a/tests/basics/io_bytesio_ext.py b/tests/basics/io_bytesio_ext.py
index e454b2fd9f2..92e71517811 100644
--- a/tests/basics/io_bytesio_ext.py
+++ b/tests/basics/io_bytesio_ext.py
@@ -1,8 +1,10 @@
 # Extended stream operations on io.BytesIO
+
 try:
-    import uio as io
-except ImportError:
     import io
+except ImportError:
+    print("SKIP")
+    raise SystemExit
 
 a = io.BytesIO(b"foobar")
 a.seek(10)
diff --git a/tests/basics/io_bytesio_ext2.py b/tests/basics/io_bytesio_ext2.py
index 8f624fd58c5..f60a6a9a604 100644
--- a/tests/basics/io_bytesio_ext2.py
+++ b/tests/basics/io_bytesio_ext2.py
@@ -1,7 +1,8 @@
 try:
-    import uio as io
-except ImportError:
     import io
+except ImportError:
+    print("SKIP")
+    raise SystemExit
 
 a = io.BytesIO(b"foobar")
 try:
diff --git a/tests/basics/io_iobase.py b/tests/basics/io_iobase.py
index 6f554b00f08..de7e4a0b06b 100644
--- a/tests/basics/io_iobase.py
+++ b/tests/basics/io_iobase.py
@@ -1,18 +1,15 @@
 try:
-    import uio as io
-except:
     import io
 
-try:
     io.IOBase
-except AttributeError:
-    print('SKIP')
+except (AttributeError, ImportError):
+    print("SKIP")
     raise SystemExit
 
 
 class MyIO(io.IOBase):
     def write(self, buf):
-        # CPython and uPy pass in different types for buf (str vs bytearray)
+        # CPython and MicroPython pass in different types for buf (str vs bytearray)
         print('write', len(buf))
         return len(buf)
 
diff --git a/tests/basics/io_stringio1.py b/tests/basics/io_stringio1.py
index 41089f22d53..889e3ce6973 100644
--- a/tests/basics/io_stringio1.py
+++ b/tests/basics/io_stringio1.py
@@ -1,7 +1,8 @@
 try:
-    import uio as io
-except ImportError:
     import io
+except ImportError:
+    print("SKIP")
+    raise SystemExit
 
 a = io.StringIO()
 print('io.StringIO' in repr(a))
diff --git a/tests/basics/io_stringio_base.py b/tests/basics/io_stringio_base.py
index dffc879074a..c8890dab73a 100644
--- a/tests/basics/io_stringio_base.py
+++ b/tests/basics/io_stringio_base.py
@@ -2,9 +2,10 @@
 # MP_TYPE_FLAG_ITER_IS_STREAM will still have a getiter.
 
 try:
-    import uio as io
-except ImportError:
     import io
+except ImportError:
+    print("SKIP")
+    raise SystemExit
 
 a = io.StringIO()
 a.write("hello\nworld\nmicro\npython\n")
diff --git a/tests/basics/io_stringio_with.py b/tests/basics/io_stringio_with.py
index c35975445df..0155ad5382d 100644
--- a/tests/basics/io_stringio_with.py
+++ b/tests/basics/io_stringio_with.py
@@ -1,7 +1,8 @@
 try:
-    import uio as io
-except ImportError:
     import io
+except ImportError:
+    print("SKIP")
+    raise SystemExit
 
 # test __enter__/__exit__
 with io.StringIO() as b:
diff --git a/tests/basics/io_write_ext.py b/tests/basics/io_write_ext.py
index 5a6eaa35cf0..5af1de7a6c3 100644
--- a/tests/basics/io_write_ext.py
+++ b/tests/basics/io_write_ext.py
@@ -1,14 +1,15 @@
 # This tests extended (MicroPython-specific) form of write:
 # write(buf, len) and write(buf, offset, len)
-import uio
 
 try:
-    uio.BytesIO
-except AttributeError:
+    import io
+
+    io.BytesIO
+except (AttributeError, ImportError):
     print('SKIP')
     raise SystemExit
 
-buf = uio.BytesIO()
+buf = io.BytesIO()
 
 buf.write(b"foo", 2)
 print(buf.getvalue())
diff --git a/tests/basics/lexer.py b/tests/basics/lexer.py
index 181d62db1aa..dfdc0b9900d 100644
--- a/tests/basics/lexer.py
+++ b/tests/basics/lexer.py
@@ -83,3 +83,19 @@ def a(x):
     exec(r"'\U0000000'")
 except SyntaxError:
     print("SyntaxError")
+
+# Properly formed integer literals
+print(eval("00"))
+# badly formed integer literals
+try:
+    eval("01")
+except SyntaxError:
+    print("SyntaxError")
+
+# Bytes 0-8 inclusive are not allowed in input stream.
+# Earlier CPython (eg 3.10.12) raises ValueError, later CPython (eg 3.11.14) raises SyntaxError.
+for invalid_byte_value in range(0, 10):
+    try:
+        print(eval(b"123" + bytes([invalid_byte_value])))
+    except (ValueError, SyntaxError):
+        print("byte {}: SyntaxError".format(invalid_byte_value))
diff --git a/tests/basics/list_pop.py b/tests/basics/list_pop.py
index 87ed456f851..58bcdd91e41 100644
--- a/tests/basics/list_pop.py
+++ b/tests/basics/list_pop.py
@@ -10,7 +10,7 @@
 else:
     raise AssertionError("No IndexError raised")
 
-# popping such that list storage shrinks (tests implementation detail of uPy)
+# popping such that list storage shrinks (tests implementation detail of MicroPython)
 l = list(range(20))
 for i in range(len(l)):
     l.pop()
diff --git a/tests/basics/memoryview1.py b/tests/basics/memoryview1.py
index 4c20c91f494..1ebfbc53b92 100644
--- a/tests/basics/memoryview1.py
+++ b/tests/basics/memoryview1.py
@@ -5,13 +5,10 @@
     print("SKIP")
     raise SystemExit
 try:
-    import uarray as array
+    import array
 except ImportError:
-    try:
-        import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 # test reading from bytes
 b = b'1234'
diff --git a/tests/basics/memoryview2.py b/tests/basics/memoryview2.py
index eacc227c283..fa5514e0727 100644
--- a/tests/basics/memoryview2.py
+++ b/tests/basics/memoryview2.py
@@ -5,13 +5,10 @@
     print("SKIP")
     raise SystemExit
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 print(list(memoryview(b'\x7f\x80\x81\xff')))
 print(list(memoryview(array('b', [0x7f, -0x80]))))
diff --git a/tests/basics/memoryview_intbig.py b/tests/basics/memoryview_intbig.py
index 4800a70cc27..72951d6eaa8 100644
--- a/tests/basics/memoryview_intbig.py
+++ b/tests/basics/memoryview_intbig.py
@@ -5,13 +5,10 @@
     print("SKIP")
     raise SystemExit
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 print(list(memoryview(array('i', [0x7f000000, -0x80000000]))))
 print(list(memoryview(array('I', [0x7f000000, 0x80000000, 0x81000000, 0xffffffff]))))
diff --git a/tests/basics/memoryview_itemsize.py b/tests/basics/memoryview_itemsize.py
index 64a8822b8b2..5428a41785f 100644
--- a/tests/basics/memoryview_itemsize.py
+++ b/tests/basics/memoryview_itemsize.py
@@ -4,13 +4,10 @@
     print("SKIP")
     raise SystemExit
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 for code in ['b', 'h', 'i', 'q', 'f', 'd']:
     print(memoryview(array(code)).itemsize)
diff --git a/tests/basics/memoryview_slice_assign.py b/tests/basics/memoryview_slice_assign.py
index 74f6fae6f78..94f8073f0a1 100644
--- a/tests/basics/memoryview_slice_assign.py
+++ b/tests/basics/memoryview_slice_assign.py
@@ -7,13 +7,10 @@
     raise SystemExit
 
 try:
-    import uarray as array
+    import array
 except ImportError:
-    try:
-        import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 # test slice assignment between memoryviews
 b1 = bytearray(b'1234')
diff --git a/tests/basics/module2.py b/tests/basics/module2.py
index 5923a27e08d..d1dc18930f0 100644
--- a/tests/basics/module2.py
+++ b/tests/basics/module2.py
@@ -1,6 +1,6 @@
-# uPy behaviour only: builtin modules are read-only
-import usys
+# MicroPython behaviour only: builtin modules are read-only
+import sys
 try:
-    usys.x = 1
+    sys.x = 1
 except AttributeError:
     print("AttributeError")
diff --git a/tests/basics/namedtuple1.py b/tests/basics/namedtuple1.py
index e8247e4d6e1..b9689b58423 100644
--- a/tests/basics/namedtuple1.py
+++ b/tests/basics/namedtuple1.py
@@ -1,8 +1,5 @@
 try:
-    try:
-        from ucollections import namedtuple
-    except ImportError:
-        from collections import namedtuple
+    from collections import namedtuple
 except ImportError:
     print("SKIP")
     raise SystemExit
@@ -24,6 +21,9 @@
 
     print(isinstance(t, tuple))
 
+    # a NamedTuple can be used as a tuple
+    print("(%d, %d)" % t)
+
     # Check tuple can compare equal to namedtuple with same elements
     print(t == (t[0], t[1]), (t[0], t[1]) == t)
 
diff --git a/tests/basics/namedtuple_asdict.py b/tests/basics/namedtuple_asdict.py
index 34c4e6f7131..e85281bb73e 100644
--- a/tests/basics/namedtuple_asdict.py
+++ b/tests/basics/namedtuple_asdict.py
@@ -1,8 +1,5 @@
 try:
-    try:
-        from ucollections import namedtuple
-    except ImportError:
-        from collections import namedtuple
+    from collections import namedtuple
 except ImportError:
     print("SKIP")
     raise SystemExit
diff --git a/tests/basics/nanbox_smallint.py b/tests/basics/nanbox_smallint.py
index b3a502e447e..9451ab32846 100644
--- a/tests/basics/nanbox_smallint.py
+++ b/tests/basics/nanbox_smallint.py
@@ -23,17 +23,17 @@
     raise SystemExit
 
 micropython.heap_lock()
-print(int("0x80000000"))
+print(int("0x80000000", 16))
 micropython.heap_unlock()
 
 # This is the most positive small integer.
 micropython.heap_lock()
-print(int("0x3fffffffffff"))
+print(int("0x3fffffffffff", 16))
 micropython.heap_unlock()
 
 # This is the most negative small integer.
 micropython.heap_lock()
-print(int("-0x3fffffffffff") - 1)
+print(int("-0x3fffffffffff", 16) - 1)
 micropython.heap_unlock()
 
 x = 1
diff --git a/tests/basics/ordereddict1.py b/tests/basics/ordereddict1.py
index 270deab3846..a6f305ff78b 100644
--- a/tests/basics/ordereddict1.py
+++ b/tests/basics/ordereddict1.py
@@ -1,11 +1,8 @@
 try:
     from collections import OrderedDict
 except ImportError:
-    try:
-        from ucollections import OrderedDict
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 d = OrderedDict([(10, 20), ("b", 100), (1, 2)])
 print(len(d))
diff --git a/tests/basics/ordereddict_eq.py b/tests/basics/ordereddict_eq.py
index c69daf8802c..e103c867e23 100644
--- a/tests/basics/ordereddict_eq.py
+++ b/tests/basics/ordereddict_eq.py
@@ -1,11 +1,8 @@
 try:
     from collections import OrderedDict
 except ImportError:
-    try:
-        from ucollections import OrderedDict
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 x = OrderedDict()
 y = OrderedDict()
diff --git a/tests/basics/ordereddict_fromkeys.py b/tests/basics/ordereddict_fromkeys.py
new file mode 100644
index 00000000000..51369990ec0
--- /dev/null
+++ b/tests/basics/ordereddict_fromkeys.py
@@ -0,0 +1,16 @@
+try:
+    from collections import OrderedDict
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+d = OrderedDict.fromkeys("abcdefg")
+print(type(d).__name__)
+print("".join(d))
+print(list(d.keys()))
+
+d2 = OrderedDict.fromkeys([1, 2, 3], 42)
+print(list(d2.values()))
+
+plain = dict.fromkeys([3, 1, 2])
+print(type(plain).__name__)
diff --git a/tests/basics/parser.py b/tests/basics/parser.py
index 626b67ad7a9..6ae5f05ba45 100644
--- a/tests/basics/parser.py
+++ b/tests/basics/parser.py
@@ -7,7 +7,7 @@
     raise SystemExit
 
 # completely empty string
-# uPy and CPy differ for this case
+# MPy and CPy differ for this case
 #try:
 #    compile("", "stdin", "single")
 #except SyntaxError:
diff --git a/tests/basics/python34.py b/tests/basics/python34.py
index 36e25e20dd5..3071b55eff8 100644
--- a/tests/basics/python34.py
+++ b/tests/basics/python34.py
@@ -28,10 +28,10 @@ def test_syntax(code):
 test_syntax("del ()") # can't delete empty tuple (in 3.6 we can)
 
 # from basics/sys1.py
-# uPy prints version 3.4
-import usys
-print(usys.version[:3])
-print(usys.version_info[0], usys.version_info[1])
+# MicroPython prints version 3.4
+import sys
+print(sys.version[:3])
+print(sys.version_info[0], sys.version_info[1])
 
 # from basics/exception1.py
 # in 3.7 no comma is printed if there is only 1 arg (in 3.4-3.6 one is printed)
diff --git a/tests/basics/python36.py.exp b/tests/basics/python36.py.exp
deleted file mode 100644
index 4b65daafa18..00000000000
--- a/tests/basics/python36.py.exp
+++ /dev/null
@@ -1,5 +0,0 @@
-100000
-165
-65535
-123
-83
diff --git a/tests/basics/self_type_check.py b/tests/basics/self_type_check.py
index 947e362cdbd..c182afd783d 100644
--- a/tests/basics/self_type_check.py
+++ b/tests/basics/self_type_check.py
@@ -1,5 +1,13 @@
 # make sure type of first arg (self) to a builtin method is checked
 
+import sys
+
+# Minimal builds usually don't enable MICROPY_BUILTIN_METHOD_CHECK_SELF_ARG,
+# which is required for this test.
+if getattr(sys.implementation, "_build", None) == "minimal":
+    print("SKIP")
+    raise SystemExit
+
 list.append
 
 try:
diff --git a/tests/basics/set_intersection.py b/tests/basics/set_intersection.py
index 73804c840d6..d07ea5a2d4f 100644
--- a/tests/basics/set_intersection.py
+++ b/tests/basics/set_intersection.py
@@ -1,7 +1,22 @@
 s = {1, 2, 3, 4}
 print(sorted(s))
+print(sorted(s.intersection()))
+print(sorted(s))
+print(s.intersection() is not s)
+print(s.intersection_update())
+print(sorted(s))
 print(sorted(s.intersection({1, 3})))
+print(sorted(s))
 print(sorted(s.intersection([3, 4])))
+print(sorted(s))
+print(sorted(s.intersection({1, 2, 3}, {1, 4, 5}, {1})))
+print(sorted(s))
+print(sorted(s.intersection([1, 3], [1, 3, 5])))
+print(sorted(s))
 
 print(s.intersection_update([1]))
 print(sorted(s))
+
+s = {1, 2, 3}
+print(s.intersection_update({1, 2}, {1}))
+print(sorted(s))
diff --git a/tests/basics/slice_indices.py b/tests/basics/slice_indices.py
index b7f439ccca2..ccd7667e9b3 100644
--- a/tests/basics/slice_indices.py
+++ b/tests/basics/slice_indices.py
@@ -25,3 +25,8 @@ def __getitem__(self, idx):
 print(A()[2:7:-2].indices(5))
 print(A()[7:2:2].indices(5))
 print(A()[7:2:-2].indices(5))
+
+try:
+    print(A()[::].indices(None))
+except TypeError:
+    print("TypeError")
diff --git a/tests/basics/slice_op.py b/tests/basics/slice_op.py
index f1e83c5e27d..0bb6e365bfd 100644
--- a/tests/basics/slice_op.py
+++ b/tests/basics/slice_op.py
@@ -1,3 +1,5 @@
+# Test slice unary operations.
+# CPython allows hashing slices, but MicroPython does not.
 
 try:
     t = [][:]
diff --git a/tests/basics/string_endswith_upy.py.exp b/tests/basics/slice_op.py.exp
similarity index 100%
rename from tests/basics/string_endswith_upy.py.exp
rename to tests/basics/slice_op.py.exp
diff --git a/tests/basics/slice_optimise.py b/tests/basics/slice_optimise.py
new file mode 100644
index 00000000000..f663e16b8c2
--- /dev/null
+++ b/tests/basics/slice_optimise.py
@@ -0,0 +1,23 @@
+# Test that the slice-on-stack optimisation does not break various uses of slice
+# (see MP_TYPE_FLAG_SUBSCR_ALLOWS_STACK_SLICE type option).
+#
+# Note: this test has a corresponding .py.exp file because hashing slice objects
+# was not allowed in CPython until 3.12.
+
+try:
+    from collections import OrderedDict
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Attempt to index with a slice, error should contain the slice (failed key).
+try:
+    dict()[:]
+except KeyError as e:
+    print("KeyError", e.args)
+
+# Put a slice and another object into an OrderedDict, and retrieve them.
+x = OrderedDict()
+x[:"a"] = 1
+x["b"] = 2
+print(list(x.keys()), list(x.values()))
diff --git a/tests/basics/slice_optimise.py.exp b/tests/basics/slice_optimise.py.exp
new file mode 100644
index 00000000000..3fa59aae15a
--- /dev/null
+++ b/tests/basics/slice_optimise.py.exp
@@ -0,0 +1,2 @@
+KeyError (slice(None, None, None),)
+[slice(None, 'a', None), 'b'] [1, 2]
diff --git a/tests/basics/special_methods2.py.exp b/tests/basics/special_methods2.py.exp
deleted file mode 100644
index a9ae75be55c..00000000000
--- a/tests/basics/special_methods2.py.exp
+++ /dev/null
@@ -1,19 +0,0 @@
-__pos__ called
-__pos__ called
-__neg__ called
-__invert__ called
-__mul__ called
-__matmul__ called
-__truediv__ called
-__floordiv__ called
-__iadd__ called
-__isub__ called
-__mod__ called
-__pow__ called
-__or__ called
-__and__ called
-__xor__ called
-__lshift__ called
-__rshift__ called
-['a', 'b', 'c']
-False
diff --git a/tests/basics/string1.py b/tests/basics/string1.py
index b3abfb9c608..ca8dfd5399a 100644
--- a/tests/basics/string1.py
+++ b/tests/basics/string1.py
@@ -5,7 +5,6 @@
 print(r'abc')
 print(u'abc')
 print(repr('\a\b\t\n\v\f\r'))
-print('\z') # unrecognised escape char
 
 # construction
 print(str())
diff --git a/tests/basics/string_compare.py b/tests/basics/string_compare.py
index f34879df25b..6515809b364 100644
--- a/tests/basics/string_compare.py
+++ b/tests/basics/string_compare.py
@@ -51,10 +51,7 @@
 
 # this tests an internal string that doesn't have a hash with a string
 # that does have a hash, but the lengths of the two strings are different
-try:
-    import usys as sys
-except ImportError:
-    import sys
+import sys
 print(sys.version == 'a long string that has a hash')
 
 # this special string would have a hash of 0 but is incremented to 1
diff --git a/tests/basics/string_endswith.py b/tests/basics/string_endswith.py
index 683562d10c3..2b0a063988b 100644
--- a/tests/basics/string_endswith.py
+++ b/tests/basics/string_endswith.py
@@ -5,11 +5,28 @@
 print("foobar".endswith(""))
 print("foobar".endswith("foobarbaz"))
 
-#print("1foobar".startswith("foo", 1))
-#print("1foo".startswith("foo", 1))
-#print("1foo".startswith("1foo", 1))
-#print("1fo".startswith("foo", 1))
-#print("1fo".startswith("foo", 10))
+print("foobar".endswith("bar", 3))
+print("foobar".endswith("bar", 4))
+print("foobar".endswith("foo", 0, 3))
+print("foobar".endswith("foo", 0, 4))
+print("foobar".endswith("foo", 1, 3))
+print("foobar".endswith("foo", 1, 3))
+print("foobar".endswith("oo", 1, 3))
+print("foobar".endswith("o", 2, 3))
+print("foobar".endswith("o", 3, 3))
+print("foobar".endswith("o", 4, 3))
+
+print("foobar".endswith("bar", None, None))
+print("foobar".endswith("bar", None, 3))
+print("foobar".endswith("bar", 3, None))
+print("foobar".endswith("bar", 2, None))
+print("foobar".endswith("foo", None, 3))
+
+print("foobar".endswith(("bar", "foo")))
+print("foobar".endswith(("foo", "bar")))
+print("foobar".endswith(("foo", "bar1")))
+print("foobar".endswith(("bar", )))
+print("foobar".endswith(("foo", )))
 
 try:
     "foobar".endswith(1)
diff --git a/tests/basics/string_endswith_upy.py b/tests/basics/string_endswith_upy.py
deleted file mode 100644
index 06a4e71d2c9..00000000000
--- a/tests/basics/string_endswith_upy.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# MicroPython doesn't support tuple argument
-
-try:
-    "foobar".endswith(("bar", "sth"))
-except TypeError:
-    print("TypeError")
diff --git a/tests/basics/string_escape_invalid.py b/tests/basics/string_escape_invalid.py
new file mode 100644
index 00000000000..d9fdd6e5465
--- /dev/null
+++ b/tests/basics/string_escape_invalid.py
@@ -0,0 +1,4 @@
+# Test invalid escape characters.
+# CPython issues a SyntaxWarning for this.
+
+print("\z")
diff --git a/tests/basics/string_escape_invalid.py.exp b/tests/basics/string_escape_invalid.py.exp
new file mode 100644
index 00000000000..c642929c6e8
--- /dev/null
+++ b/tests/basics/string_escape_invalid.py.exp
@@ -0,0 +1 @@
+\z
diff --git a/tests/basics/string_find.py b/tests/basics/string_find.py
index f9fcad3e579..f37064ad291 100644
--- a/tests/basics/string_find.py
+++ b/tests/basics/string_find.py
@@ -24,6 +24,7 @@
 print("aaaaaaaaaaa".find("bbb", 9, 2))
 
 try:
+    # Only works on bytes/bytearray.
     'abc'.find(1)
 except TypeError:
     print('TypeError')
diff --git a/tests/basics/string_format.py b/tests/basics/string_format.py
index e8600f58361..11e7836a73e 100644
--- a/tests/basics/string_format.py
+++ b/tests/basics/string_format.py
@@ -22,7 +22,17 @@ def test(fmt, *args):
 test("{:4x}", 123)
 test("{:4X}", 123)
 
+test("{:4,d}", 1)
+test("{:4_d}", 1)
+test("{:4_o}", 1)
+test("{:4_b}", 1)
+test("{:4_x}", 1)
+
 test("{:4,d}", 12345678)
+test("{:4_d}", 12345678)
+test("{:4_o}", 12345678)
+test("{:4_b}", 12345678)
+test("{:4_x}", 12345678)
 
 test("{:#4b}", 10)
 test("{:#4o}", 123)
diff --git a/tests/basics/string_format_intbig.py b/tests/basics/string_format_intbig.py
new file mode 100644
index 00000000000..a36c36752e7
--- /dev/null
+++ b/tests/basics/string_format_intbig.py
@@ -0,0 +1,15 @@
+# basic functionality test for {} format string using large integers
+
+
+def test(fmt, *args):
+    print("{:8s}".format(fmt) + ">" + fmt.format(*args) + "<")
+
+
+# Separator formatter
+
+test("{:,}", 123_456_789_012_345_678_901_234_567)
+test("{:,}", 23_456_789_012_345_678_901_234_567)
+test("{:,}", 3_456_789_012_345_678_901_234_567)
+test("{:,}", -123_456_789_012_345_678_901_234_567)
+test("{:,}", -23_456_789_012_345_678_901_234_567)
+test("{:,}", -3_456_789_012_345_678_901_234_567)
diff --git a/tests/basics/string_format_modulo.py b/tests/basics/string_format_modulo.py
index 14b4a6a484b..7bddd96750d 100644
--- a/tests/basics/string_format_modulo.py
+++ b/tests/basics/string_format_modulo.py
@@ -51,8 +51,9 @@ def __int__(self):
 
 # Should be able to print dicts; in this case they aren't used
 # to lookup keywords in formats like %(foo)s
-print('%s' % {})
-print('%s' % ({},))
+print('%s' % {})      # dict treated as the single (positional) arg to %
+print('%s' % ({},))   # dict is the first (and only) arg in the positional arg tuple
+print('foo' % {})     # no error, dict treated as an empty map of named args
 
 # Cases when "*" used and there's not enough values total
 try:
@@ -65,7 +66,11 @@ def __int__(self):
     print("TypeError")
 
 print("%(foo)s" % {"foo": "bar", "baz": False})
-print("%s %(foo)s %(foo)s" % {"foo": 1})
+print("%s %(foo)s %(foo)s" % {"foo": 1})  # dict consumed positionally, then used as map - ok
+try:
+    print("%(foo)s %s %(foo)s" % {"foo": 1})  # used as map, then positionally - not enough args
+except TypeError:
+    print("TypeError")
 try:
     print("%(foo)s" % {})
 except KeyError:
diff --git a/tests/basics/string_format_sep.py b/tests/basics/string_format_sep.py
new file mode 100644
index 00000000000..de131fdaf33
--- /dev/null
+++ b/tests/basics/string_format_sep.py
@@ -0,0 +1,9 @@
+try:
+    "%d" % 1
+except TypeError:
+    print("SKIP")
+    raise SystemExit
+
+for v in (0, 0x10, 0x1000, -0x10, -0x1000):
+    for sz in range(1, 12): print(("{:0%d,d}" % sz).format(v))
+    for sz in range(1, 12): print(("{:0%d_x}" % sz).format(v))
diff --git a/tests/basics/string_fstring.py b/tests/basics/string_fstring.py
index 8907a5c4784..03e2d90a8f7 100644
--- a/tests/basics/string_fstring.py
+++ b/tests/basics/string_fstring.py
@@ -29,23 +29,10 @@ def foo(a, b):
 # Nested '{' and '}' characters.
 print(f"a{ {0,1,2}}")
 
-# PEP-0498 specifies that '\\' and '#' must be disallowed explicitly, whereas
-# MicroPython relies on the syntax error as a result of the substitution.
-
-print(f"\\")
-print(f'#')
-try:
-    eval("f'{\}'")
-except SyntaxError:
-    print('SyntaxError')
-try:
-    eval("f'{#}'")
-except SyntaxError:
-    print('SyntaxError')
-
-
 # PEP-0498 specifies that handling of double braces '{{' or '}}' should
 # behave like str.format.
+print(f'{{')
+print(f'}}')
 print(f'{{}}')
 print(f'{{{4*10}}}', '{40}')
 
@@ -61,3 +48,40 @@ def foo(a, b):
 print(f"a {1,} b")
 print(f"a {x,y,} b")
 print(f"a {x,1} b")
+
+# f-strings with conversion specifiers (only support !r and !s).
+a = "123"
+print(f"{a!r}")
+print(f"{a!s}")
+try:
+    eval('print(f"{a!x}")')
+except (ValueError, SyntaxError):
+    # CPython detects this at compile time, MicroPython fails with ValueError
+    # when the str.format is executed.
+    print("ValueError")
+
+# Mixing conversion specifiers with formatting.
+print(f"{a!r:8s}")
+print(f"{a!s:8s}")
+
+# Still allow ! in expressions.
+print(f"{'1' if a != '456' else '0'!r:8s}")
+print(f"{'1' if a != '456' else '0'!s:8s}")
+
+# Concatenation of adjacent f-strings.
+print(f"" f"")
+print(f"a" f"b")
+print(f"{x}" f"{y}")
+print(
+    f"a{x}b---------------------------------"
+    f"cd---------------------------------"
+    f"e{y}f---------------------------------"
+)
+
+# Raw f-strings.
+print(rf"\r\a\w {'f'} \s\t\r\i\n\g")
+print(fr"\r{x}")
+
+# Format specifiers with nested replacement fields
+radix = "x"
+print(f"{314:{radix}}")
diff --git a/tests/basics/string_fstring_debug.py.exp b/tests/basics/string_fstring_debug.py.exp
deleted file mode 100644
index f0309e1c98a..00000000000
--- a/tests/basics/string_fstring_debug.py.exp
+++ /dev/null
@@ -1,9 +0,0 @@
-x=1
-x=00000001
-a x=1 b 2 c
-a x=00000001 b 2 c
-a f() + g("foo") + h()=15 b
-a f() + g("foo") + h()=0000000f b
-a 1,=(1,) b
-a x,y,=(1, 2) b
-a x,1=(1, 1) b
diff --git a/tests/basics/string_fstring_invalid.py b/tests/basics/string_fstring_invalid.py
new file mode 100644
index 00000000000..6fce323c5bc
--- /dev/null
+++ b/tests/basics/string_fstring_invalid.py
@@ -0,0 +1,13 @@
+# PEP-0498 specifies that '\\' and '#' must be disallowed explicitly, whereas
+# MicroPython relies on the syntax error as a result of the substitution.
+
+print(f"\\")
+print(f"#")
+try:
+    eval("f'{\}'")
+except SyntaxError:
+    print("SyntaxError")
+try:
+    eval("f'{#}'")
+except SyntaxError:
+    print("SyntaxError")
diff --git a/tests/basics/string_fstring_invalid.py.exp b/tests/basics/string_fstring_invalid.py.exp
new file mode 100644
index 00000000000..bcb7f259e55
--- /dev/null
+++ b/tests/basics/string_fstring_invalid.py.exp
@@ -0,0 +1,4 @@
+\
+#
+SyntaxError
+SyntaxError
diff --git a/tests/basics/string_fstring_nested.py b/tests/basics/string_fstring_nested.py
new file mode 100644
index 00000000000..82de0bf6470
--- /dev/null
+++ b/tests/basics/string_fstring_nested.py
@@ -0,0 +1,9 @@
+# Test nesting of f-strings within f-strings.
+
+x = 1
+
+# 2-level nesting, with padding.
+print(f"a{f'b{x:2}c':>5}d")
+
+# 4-level nesting using the different styles of quotes.
+print(f"""a{f'''b{f"c{f'd{x}e'}f"}g'''}h""")
diff --git a/tests/basics/string_fstring_nested_py312.py b/tests/basics/string_fstring_nested_py312.py
new file mode 100644
index 00000000000..aa731b1637d
--- /dev/null
+++ b/tests/basics/string_fstring_nested_py312.py
@@ -0,0 +1,7 @@
+# Test nesting of f-strings within f-strings.
+# These test rely on Python 3.12+ to use the same quote style for nesting.
+
+x = 1
+
+# 8-level nesting using the same quote style.
+print(f"a{f"b{f"c{f"d{f"e{f"f{f"g{f"h{x}i"}j"}k"}l"}m"}n"}o"}p")
diff --git a/tests/basics/string_fstring_nested_py312.py.exp b/tests/basics/string_fstring_nested_py312.py.exp
new file mode 100644
index 00000000000..1d624933dff
--- /dev/null
+++ b/tests/basics/string_fstring_nested_py312.py.exp
@@ -0,0 +1 @@
+abcdefgh1ijklmnop
diff --git a/tests/basics/string_index.py b/tests/basics/string_index.py
index 31f6900e6c1..328f2dff54c 100644
--- a/tests/basics/string_index.py
+++ b/tests/basics/string_index.py
@@ -76,3 +76,9 @@
     print("Raised ValueError")
 else:
     print("Did not raise ValueError")
+
+try:
+    # Only works on bytes/bytearray.
+    'abc'.index(1)
+except TypeError:
+    print('TypeError')
diff --git a/tests/basics/string_module_tstring.py b/tests/basics/string_module_tstring.py
new file mode 100644
index 00000000000..83809926926
--- /dev/null
+++ b/tests/basics/string_module_tstring.py
@@ -0,0 +1,6 @@
+# Test basic templatelib functionality.
+# This test requires t-strings support.
+
+from string.templatelib import Template
+
+print("templatelib", isinstance(t"hi", Template))
diff --git a/tests/basics/string_module_tstring.py.exp b/tests/basics/string_module_tstring.py.exp
new file mode 100644
index 00000000000..26dba98f403
--- /dev/null
+++ b/tests/basics/string_module_tstring.py.exp
@@ -0,0 +1 @@
+templatelib True
diff --git a/tests/basics/string_startswith.py b/tests/basics/string_startswith.py
index e63ae3c1866..eefdea82198 100644
--- a/tests/basics/string_startswith.py
+++ b/tests/basics/string_startswith.py
@@ -10,6 +10,25 @@
 print("1fo".startswith("foo", 1))
 print("1fo".startswith("foo", 10))
 
+print("1foobar".startswith("foo", 1, 5))
+print("1foobar".startswith("foo", 1, 4))
+print("1foobar".startswith("foo", 1, 3))
+print("1foobar".startswith("oo", 2, 4))
+print("1foobar".startswith("o", 3, 4))
+print("1foobar".startswith("o", 4, 4))
+print("1foobar".startswith("o", 5, 4))
+
+print("foobar".startswith("foo", None, None))
+print("foobar".startswith("foo", None, 3))
+print("foobar".startswith("foo", None, 2))
+print("foobar".startswith("bar", 3, None))
+
+
+print("foobar".startswith(("foo", "sth")))
+print("foobar".startswith(("sth", "foo")))
+print("foobar".startswith(("sth", "foo2")))
+print("foobar".startswith(("foo", )))
+
 try:
     "foobar".startswith(1)
 except TypeError:
diff --git a/tests/basics/string_startswith_upy.py b/tests/basics/string_startswith_upy.py
deleted file mode 100644
index 9ea1796c218..00000000000
--- a/tests/basics/string_startswith_upy.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# MicroPython doesn't support tuple argument
-
-try:
-    "foobar".startswith(("foo", "sth"))
-except TypeError:
-    print("TypeError")
diff --git a/tests/basics/string_startswith_upy.py.exp b/tests/basics/string_startswith_upy.py.exp
deleted file mode 100644
index 6002b71c56e..00000000000
--- a/tests/basics/string_startswith_upy.py.exp
+++ /dev/null
@@ -1 +0,0 @@
-TypeError
diff --git a/tests/basics/string_tstring_basic.py b/tests/basics/string_tstring_basic.py
new file mode 100644
index 00000000000..aa9de327b59
--- /dev/null
+++ b/tests/basics/string_tstring_basic.py
@@ -0,0 +1,339 @@
+from string.templatelib import Template, Interpolation
+
+print("=== Basic functionality ===")
+t = t"Hello World"
+print(type(t).__name__)
+
+name = "World"
+t2 = t"Hello {name}"
+print(f"Strings: {t2.strings}")
+print(f"Value: {t2.interpolations[0].value}")
+print(f"str(): {str(t2)}")
+
+t_raw = rt"Path: C:\test\{name}"
+print(f"Raw: '{t_raw.strings[0]}'")
+
+t_tr = tr"Path: C:\test\{name}"
+print(f"tr: '{t_tr.strings[0]}'")
+
+print("\n=== Parser tests ===")
+data = {"a": {"b": [1, 2, 3]}}
+t_complex = t"{data['a']['b'][0]}"
+print(f"Complex: {str(t_complex)}")
+
+print(f"None: {str(t'{None}')}")
+print(f"True: {str(t'{True}')}")
+print(f"False: {str(t'{False}')}")
+print(f"Ellipsis: {str(t'{...}')}")
+
+obj = type('Obj', (), {
+    '__getattr__': lambda s, n: s,
+    '__getitem__': lambda s, k: s,
+    '__call__': lambda s, *a: 42,
+    '__str__': lambda s: "42"
+})()
+print(f"Deep nest: {str(t'{obj.a.b[0].c()}')}")
+
+print("\n=== Conversions and formatting ===")
+val = {"key": "value"}
+print(f"repr: {str(t'{val!r}')}")
+print(f"str: {str(t'{val!s}')}")
+# print(f"ascii: {str(t'{val!a}')}")
+
+print(f"Width: '{str(t'{42:10d}')}'")
+print(f"Precision: {str(t'{3.14159:.2f}')}")
+
+x = 42
+t_debug = t"{x=}"
+assert t_debug.strings == ("x=", "")
+assert t_debug.interpolations[0].expression == "x"
+assert t_debug.interpolations[0].conversion == "r"
+assert t_debug.interpolations[0].value == 42
+print(f"Debug: {str(t_debug)}")
+
+y = 10
+t_debug2 = t"{x + y=}"
+assert t_debug2.strings == ("x + y=", "")
+assert t_debug2.interpolations[0].expression == "x + y"
+assert t_debug2.interpolations[0].conversion == "r"
+assert t_debug2.interpolations[0].value == 52
+
+pi = 3.14159
+t_debug3 = t"{pi=:.2f}"
+assert t_debug3.strings == ("pi=", "")
+assert t_debug3.interpolations[0].expression == "pi"
+assert t_debug3.interpolations[0].conversion is None
+assert t_debug3.interpolations[0].format_spec == ".2f"
+assert t_debug3.interpolations[0].value == 3.14159
+
+print("\n=== Constructor tests ===")
+t_empty = Template()
+print(f"Empty: {t_empty.strings}")
+
+t_single = Template("single")
+print(f"Single: {t_single.strings}")
+
+t_multi = Template("a", "b", "c")
+print(f"Multi: {t_multi.strings}")
+
+i1 = Interpolation(1, "x")
+i2 = Interpolation(2, "y")
+t_mixed = Template("start", i1, "middle", i2, "end")
+print(f"Mixed: strings={t_mixed.strings}, values={t_mixed.values}")
+
+print("\n=== Operations ===")
+t1 = t"Hello"
+t2 = t" World"
+t_concat = t1 + t2
+print(f"Concat: '{str(t_concat)}'")
+
+items = list(t"a{1}b{2}c")
+print(f"Iterator: {[type(x).__name__ for x in items]}")
+
+t_attr = t"test{42}"
+print(f"strings attr: {t_attr.strings}")
+print(f"interpolations attr: {len(t_attr.interpolations)}")
+print(f"values attr: {t_attr.values}")
+
+print(f"repr: {repr(t2)[:50]}...")
+
+print("\n=== Escaped braces evaluation ===")
+t_escaped = Template("{", Interpolation(42, "x"), "}", Interpolation(42, "x"), "{{", Interpolation(42, "x"), "}}")
+print(f"Escaped eval: '{str(t_escaped)}'")
+
+t_braces = Template("{{hello}}", Interpolation(1, "a"), " {", Interpolation(2, "b"), "} ", Interpolation(3, "c"), "{{world}}")
+print(f"Braces in strings: '{str(t_braces)}'")
+
+print("\n=== Memory stress test ===")
+for n in [10, 20, 30]:
+    args = []
+    for i in range(n):
+        args.append("s")
+        args.append(Interpolation(i, f"var{i}"))
+    args.append("s")
+    t_mem = Template(*args)
+    result = str(t_mem)
+    print(f"Memory test [{n}]: {len(result)} chars")
+
+large_args = []
+for i in range(20):
+    large_args.append("")
+    large_args.append(Interpolation(i, f"v{i}"))
+large_args.append("")
+t_large = Template(*large_args)
+print(f"Large values: {len(t_large.values)} values")
+
+print("\n=== Nested quotes and braces tests ===")
+t_nested1 = t"{"{}"}"
+print(f"Nested quotes 1: {str(t_nested1)}")
+print(f"  Value: {t_nested1.interpolations[0].value}")
+print(f"  Expression: {t_nested1.interpolations[0].expression}")
+
+t_nested2 = t"{'hello'}"
+print(f"Nested quotes 2: {str(t_nested2)}")
+print(f"  Value: {t_nested2.interpolations[0].value}")
+
+t_nested3 = t'{"world"}'
+print(f"Nested quotes 3: {str(t_nested3)}")
+print(f"  Value: {t_nested3.interpolations[0].value}")
+
+t_nested4 = t"{['a', 'b', 'c'][1]}"
+print(f"Nested quotes 4: {str(t_nested4)}")
+print(f"  Value: {t_nested4.interpolations[0].value}")
+
+d = {"key": "value", "x": 123}
+t_nested5 = t"{d['key']}"
+print(f"Nested quotes 5: {str(t_nested5)}")
+print(f"  Value: {t_nested5.interpolations[0].value}")
+
+t_escaped1 = t'{{""}}'
+print(f"Escaped braces 1: {str(t_escaped1)}")
+print(f"  Strings: {t_escaped1.strings}")
+
+t_escaped2 = t"{{}}}}"
+print(f"Escaped braces 2: {str(t_escaped2)}")
+print(f"  Strings: {t_escaped2.strings}")
+
+x = "test"
+t_mixed = t"{{before}} {x} {{after}}"
+print(f"Mixed escaped: {str(t_mixed)}")
+print(f"  Strings: {t_mixed.strings}")
+print(f"  Value: {t_mixed.interpolations[0].value}")
+
+t_escape = t"{'\n\t'}"
+print(f"Escape sequences: {str(t_escape)}")
+print(f"  Value repr: {repr(t_escape.interpolations[0].value)}")
+
+inner = "{}"
+t_nested_expr = t"{inner}"
+print(f"Nested expr: {str(t_nested_expr)}")
+print(f"  Value: {t_nested_expr.interpolations[0].value}")
+
+print("\n=== Interpolation attribute tests ===")
+i_basic = Interpolation(42, "x")
+print(f"Basic conversion: {i_basic.conversion}")
+# Put in quotes to make empty string visible.
+print(f"Basic format_spec: '{i_basic.format_spec}'")
+
+i_with_conv = Interpolation(42, "x", "s")
+print(f"With conversion: {i_with_conv.conversion}")
+
+i_with_fmt = Interpolation(42, "x", None, ":>10")
+print(f"With format_spec: {i_with_fmt.format_spec}")
+
+i_full = Interpolation(42, "x", "r", ":>10")
+print(f"Full conversion: {i_full.conversion}")
+print(f"Full format_spec: {i_full.format_spec}")
+
+t_conv = t"{42!s}"
+print(f"Template conversion: {t_conv.interpolations[0].conversion}")
+
+t_fmt = t"{42:>10}"
+print(f"Template format_spec: {t_fmt.interpolations[0].format_spec}")
+
+t_both = t"{42!r:>10}"
+print(f"Both conversion: {t_both.interpolations[0].conversion}")
+print(f"Both format_spec: {t_both.interpolations[0].format_spec}")
+
+print("\n=== Escape sequence tests ===")
+print(repr(t"Line1\nLine2"))
+print(repr(t"Path\\to\\file"))
+print(repr(t"It\'s working"))
+print(repr(t"She said \"Hello\""))
+print(repr(t"Bell\a"))
+print(repr(t"Back\bspace"))
+print(repr(t"Tab\there"))
+print(repr(t"Vertical\vtab"))
+print(repr(t"Form\ffeed"))
+print(repr(t"Carriage\rreturn"))
+
+# Valid hex escapes
+print(repr(t"\x41"))
+print(repr(t"\x7F"))
+
+# Valid octal escapes
+print(repr(t"\0"))
+print(repr(t"\7"))
+print(repr(t"\77"))
+print(repr(t"\101"))
+
+print(repr(t"\1a"))
+print(repr(t"\123c"))
+
+# Line continuation
+print(repr(t"First \
+second"))
+print(repr(t"One\
+Two\
+Three"))
+
+# Multiple escapes
+print(repr(t"\n\t\r"))
+
+# Raw strings
+print(repr(rt"\n\t\x41"))
+print(repr(rt"C:\new\test"))
+
+# Triple quoted strings
+print(repr(t"""\n\t\x41"""))
+print(repr(rt"""\n\t\x41"""))
+
+print("\n=== Triple quote edge cases ===")
+doc = '''This is a
+multi-line
+docstring'''
+result1 = t"""Documentation:
+{doc}
+End of doc"""
+print(f"Triple in interpolation: {str(result1)}")
+print(f"   Value: {repr(result1.interpolations[0].value)}")
+
+data = {
+    "users": [
+        {"name": "Alice", "msg": 'Say "Hello"'},
+        {"name": "Bob", "msg": "It's great!"}
+    ]
+}
+complex_nested = t"""Users:
+{data['users'][0]['name']}: {data['users'][0]['msg']}
+{data['users'][1]['name']}: {data['users'][1]['msg']}"""
+print(f"Complex nested: {str(complex_nested)}")
+print(f"   Values: {complex_nested.values}")
+
+import os
+path_sep = os.sep
+raw_with_interp = rt"""Path: C:\Users\{path_sep}Documents
+Raw newline: \n
+Raw tab: \t"""
+print(f"Raw with interpolation: {str(raw_with_interp)}")
+print(f"   String 0 repr: {repr(raw_with_interp.strings[0])}")
+
+long_line = "x" * 50
+long_triple = t"""Start
+{long_line}
+End"""
+print(f"Long line: strings={long_triple.strings}")
+print(f"   Value length: {len(long_triple.values[0])}")
+
+inner = t"inner {42}"
+outer = t"""Outer template:
+{inner}
+End"""
+print(f"Nested templates: {str(outer)}")
+print(f"   Inner value: {outer.values[0]}")
+
+formatted = t"""Math constants:
+Pi: {314:.2f}
+E: {271:.1e}
+Sqrt(2): {141:.0f}"""
+print(f"Formatted values: {repr(formatted)}")
+print(f"    Interpolation count: {len(formatted.interpolations)}")
+print(f"    Format specs: {[i.format_spec for i in formatted.interpolations]}")
+
+x, y, z = 1, 2, 3
+debug_complex = t"""Debug info:
+{x + y=} {x * y * z=} {x < y < z=}"""
+print(f"Debug complex: {repr(debug_complex)}")
+print(f"    Expressions: {[i.expression for i in debug_complex.interpolations]}")
+
+part1 = t"""Part 1
+with newline"""
+part2 = t'''Part 2
+also multiline'''
+concatenated = part1 + part2
+print(f"Concatenated triple: {repr(concatenated)}")
+print(f"    Strings: {concatenated.strings}")
+
+print("\n=== PEP 701 brace compliance ===")
+t_brace1 = t"{{"
+print(f"Escaped {{{{: {t_brace1.strings}")
+assert t_brace1.strings == ('{',)
+
+t_brace2 = t"}}"
+print(f"Escaped }}}}: {t_brace2.strings}")
+assert t_brace2.strings == ('}',)
+
+t_brace3 = t"test{{escape}}here"
+print(f"Mixed: {t_brace3.strings}")
+assert t_brace3.strings == ('test{escape}here',)
+
+print("\n=== Format spec scope resolution ===")
+def test_format_scope():
+    width = 10
+    x = 42
+    t_scope = t"{x:{width}}"
+    print(f"Local vars: width={width}, x={x}")
+    print(f"Format spec: {t_scope.interpolations[0].format_spec}")
+    return t_scope.interpolations[0].format_spec == '10'
+
+result = test_format_scope()
+print(f"Scope test: {'PASS' if result else 'FAIL'}")
+assert result, "Format spec scope resolution failed"
+
+precision = 2
+value = 3.14159
+t_prec = t"{value:.{precision}f}"
+print(f"Precision: format_spec={t_prec.interpolations[0].format_spec}")
+assert t_prec.interpolations[0].format_spec == '.2f'
+
+print("\nBasic tests completed!")
diff --git a/tests/basics/string_tstring_basic.py.exp b/tests/basics/string_tstring_basic.py.exp
new file mode 100644
index 00000000000..60affaad499
--- /dev/null
+++ b/tests/basics/string_tstring_basic.py.exp
@@ -0,0 +1,141 @@
+=== Basic functionality ===
+Template
+Strings: ('Hello ', '')
+Value: World
+str(): Template(strings=('Hello ', ''), interpolations=(Interpolation('World', 'name', None, ''),))
+Raw: 'Path: C:\test\'
+tr: 'Path: C:\test\'
+
+=== Parser tests ===
+Complex: Template(strings=('', ''), interpolations=(Interpolation(1, "data['a']['b'][0]", None, ''),))
+None: Template(strings=('', ''), interpolations=(Interpolation(None, 'None', None, ''),))
+True: Template(strings=('', ''), interpolations=(Interpolation(True, 'True', None, ''),))
+False: Template(strings=('', ''), interpolations=(Interpolation(False, 'False', None, ''),))
+Ellipsis: Template(strings=('', ''), interpolations=(Interpolation(Ellipsis, '...', None, ''),))
+Deep nest: Template(strings=('', ''), interpolations=(Interpolation(42, 'obj.a.b[0].c()', None, ''),))
+
+=== Conversions and formatting ===
+repr: Template(strings=('', ''), interpolations=(Interpolation({'key': 'value'}, 'val', 'r', ''),))
+str: Template(strings=('', ''), interpolations=(Interpolation({'key': 'value'}, 'val', 's', ''),))
+Width: 'Template(strings=('', ''), interpolations=(Interpolation(42, '42', None, '10d'),))'
+Precision: Template(strings=('', ''), interpolations=(Interpolation(3.14159, '3.14159', None, '.2f'),))
+Debug: Template(strings=('x=', ''), interpolations=(Interpolation(42, 'x', 'r', ''),))
+
+=== Constructor tests ===
+Empty: ('',)
+Single: ('single',)
+Multi: ('abc',)
+Mixed: strings=('start', 'middle', 'end'), values=(1, 2)
+
+=== Operations ===
+Concat: 'Template(strings=('Hello World',), interpolations=())'
+Iterator: ['str', 'Interpolation', 'str', 'Interpolation', 'str']
+strings attr: ('test', '')
+interpolations attr: 1
+values attr: (42,)
+repr: Template(strings=(' World',), interpolations=())...
+
+=== Escaped braces evaluation ===
+Escaped eval: 'Template(strings=('{', '}', '{{', '}}'), interpolations=(Interpolation(42, 'x', None, ''), Interpolation(42, 'x', None, ''), Interpolation(42, 'x', None, '')))'
+Braces in strings: 'Template(strings=('{{hello}}', ' {', '} ', '{{world}}'), interpolations=(Interpolation(1, 'a', None, ''), Interpolation(2, 'b', None, ''), Interpolation(3, 'c', None, '')))'
+
+=== Memory stress test ===
+Memory test [10]: 450 chars
+Memory test [20]: 880 chars
+Memory test [30]: 1310 chars
+Large values: 20 values
+
+=== Nested quotes and braces tests ===
+Nested quotes 1: Template(strings=('', ''), interpolations=(Interpolation('{}', '"{}"', None, ''),))
+  Value: {}
+  Expression: "{}"
+Nested quotes 2: Template(strings=('', ''), interpolations=(Interpolation('hello', "'hello'", None, ''),))
+  Value: hello
+Nested quotes 3: Template(strings=('', ''), interpolations=(Interpolation('world', '"world"', None, ''),))
+  Value: world
+Nested quotes 4: Template(strings=('', ''), interpolations=(Interpolation('b', "['a', 'b', 'c'][1]", None, ''),))
+  Value: b
+Nested quotes 5: Template(strings=('', ''), interpolations=(Interpolation('value', "d['key']", None, ''),))
+  Value: value
+Escaped braces 1: Template(strings=('{""}',), interpolations=())
+  Strings: ('{""}',)
+Escaped braces 2: Template(strings=('{}}',), interpolations=())
+  Strings: ('{}}',)
+Mixed escaped: Template(strings=('{before} ', ' {after}'), interpolations=(Interpolation('test', 'x', None, ''),))
+  Strings: ('{before} ', ' {after}')
+  Value: test
+Escape sequences: Template(strings=('', ''), interpolations=(Interpolation('\n\t', "'\\n\\t'", None, ''),))
+  Value repr: '\n\t'
+Nested expr: Template(strings=('', ''), interpolations=(Interpolation('{}', 'inner', None, ''),))
+  Value: {}
+
+=== Interpolation attribute tests ===
+Basic conversion: None
+Basic format_spec: ''
+With conversion: s
+With format_spec: :>10
+Full conversion: r
+Full format_spec: :>10
+Template conversion: s
+Template format_spec: >10
+Both conversion: r
+Both format_spec: >10
+
+=== Escape sequence tests ===
+Template(strings=('Line1\nLine2',), interpolations=())
+Template(strings=('Path\\to\\file',), interpolations=())
+Template(strings=("It's working",), interpolations=())
+Template(strings=('She said "Hello"',), interpolations=())
+Template(strings=('Bell\x07',), interpolations=())
+Template(strings=('Back\x08space',), interpolations=())
+Template(strings=('Tab\there',), interpolations=())
+Template(strings=('Vertical\x0btab',), interpolations=())
+Template(strings=('Form\x0cfeed',), interpolations=())
+Template(strings=('Carriage\rreturn',), interpolations=())
+Template(strings=('A',), interpolations=())
+Template(strings=('\x7f',), interpolations=())
+Template(strings=('\x00',), interpolations=())
+Template(strings=('\x07',), interpolations=())
+Template(strings=('?',), interpolations=())
+Template(strings=('A',), interpolations=())
+Template(strings=('\x01a',), interpolations=())
+Template(strings=('Sc',), interpolations=())
+Template(strings=('First second',), interpolations=())
+Template(strings=('OneTwoThree',), interpolations=())
+Template(strings=('\n\t\r',), interpolations=())
+Template(strings=('\\n\\t\\x41',), interpolations=())
+Template(strings=('C:\\new\\test',), interpolations=())
+Template(strings=('\n\tA',), interpolations=())
+Template(strings=('\\n\\t\\x41',), interpolations=())
+
+=== Triple quote edge cases ===
+Triple in interpolation: Template(strings=('Documentation:\n', '\nEnd of doc'), interpolations=(Interpolation('This is a\nmulti-line\ndocstring', 'doc', None, ''),))
+   Value: 'This is a\nmulti-line\ndocstring'
+Complex nested: Template(strings=('Users:\n', ': ', '\n', ': ', ''), interpolations=(Interpolation('Alice', "data['users'][0]['name']", None, ''), Interpolation('Say "Hello"', "data['users'][0]['msg']", None, ''), Interpolation('Bob', "data['users'][1]['name']", None, ''), Interpolation("It's great!", "data['users'][1]['msg']", None, '')))
+   Values: ('Alice', 'Say "Hello"', 'Bob', "It's great!")
+Raw with interpolation: Template(strings=('Path: C:\\Users\\', 'Documents\nRaw newline: \\n\nRaw tab: \\t'), interpolations=(Interpolation('/', 'path_sep', None, ''),))
+   String 0 repr: 'Path: C:\\Users\\'
+Long line: strings=('Start\n', '\nEnd')
+   Value length: 50
+Nested templates: Template(strings=('Outer template:\n', '\nEnd'), interpolations=(Interpolation(Template(strings=('inner ', ''), interpolations=(Interpolation(42, '42', None, ''),)), 'inner', None, ''),))
+   Inner value: Template(strings=('inner ', ''), interpolations=(Interpolation(42, '42', None, ''),))
+Formatted values: Template(strings=('Math constants:\nPi: ', '\nE: ', '\nSqrt(2): ', ''), interpolations=(Interpolation(314, '314', None, '.2f'), Interpolation(271, '271', None, '.1e'), Interpolation(141, '141', None, '.0f')))
+    Interpolation count: 3
+    Format specs: ['.2f', '.1e', '.0f']
+Debug complex: Template(strings=('Debug info:\nx + y=', ' x * y * z=', ' x < y < z=', ''), interpolations=(Interpolation(3, 'x + y', 'r', ''), Interpolation(6, 'x * y * z', 'r', ''), Interpolation(True, 'x < y < z', 'r', '')))
+    Expressions: ['x + y', 'x * y * z', 'x < y < z']
+Concatenated triple: Template(strings=('Part 1\nwith newlinePart 2\nalso multiline',), interpolations=())
+    Strings: ('Part 1\nwith newlinePart 2\nalso multiline',)
+
+=== PEP 701 brace compliance ===
+Escaped {{: ('{',)
+Escaped }}: ('}',)
+Mixed: ('test{escape}here',)
+
+=== Format spec scope resolution ===
+Local vars: width=10, x=42
+Format spec: 10
+Scope test: PASS
+Precision: format_spec=.2f
+
+Basic tests completed!
diff --git a/tests/basics/string_tstring_basic1.py b/tests/basics/string_tstring_basic1.py
new file mode 100644
index 00000000000..89ee3e0d2d4
--- /dev/null
+++ b/tests/basics/string_tstring_basic1.py
@@ -0,0 +1,131 @@
+print("=== Parser error tests ===")
+try:
+    exec('t_empty = t"{}"')
+    print(f"Empty expr: {t_empty.interpolations[0].value}")
+except SyntaxError as e:
+    print(f"Empty expr: SyntaxError - {e}")
+
+# Whitespace in expression (Python semantics are to strip trailing)
+t_ws = t"{ 42 }"
+print(f"Whitespace: {str(t_ws)}")
+
+print("\n=== Error cases ===")
+try:
+    exec('t"{@}"')
+except SyntaxError:
+    print("Invalid syntax: SyntaxError")
+
+try:
+    long_expr = "x" * 10001
+    exec(f't"{{{long_expr}}}"')
+except (ValueError, SyntaxError, RuntimeError, NameError):
+    print("Long expr: Error")
+
+try:
+    exec('t"hello" "world"')
+except SyntaxError:
+    print("Mixed concat: SyntaxError")
+
+try:
+    exec('bt"test"')
+except SyntaxError:
+    print("bt prefix: SyntaxError")
+
+print("\n=== Escape sequence coverage tests ===")
+
+def expect_invalid_escape(label, expr):
+    try:
+        eval(expr)
+    except SyntaxError:
+        print(f"{label}: SyntaxError")
+    else:
+        print(f"{label}: ERROR (expected SyntaxError)")
+
+# Deprecated escape sequences (CPython warns, MicroPython accepts as literal)
+print(repr(t"\8"))
+print(repr(t"\9"))
+
+# Unknown escape chars
+print(repr(t"\z"))
+print(repr(t"\k"))
+
+# Invalid escapes
+expect_invalid_escape("Invalid \\x escape", 't"\\xGG"')
+expect_invalid_escape("Invalid \\u escape", 't"\\uGGGG"')
+expect_invalid_escape("Invalid \\U escape", 't"\\UGGGGGGGG"')
+
+# Unicode escapes (display format differs)
+print(repr(t"\x00\x01\xFF"))
+print(repr(t"\u0041"))
+print(repr(t"\u03B1"))
+print(repr(t"\u2764"))
+print(repr(t"\U00000041"))
+print(repr(t"\U0001F600"))
+print(repr(t"\x41\u0042\103"))
+
+# Unicode in triple-quoted strings
+unicode_test = t"""Unicode test:
+Emoji: {'\U0001f40d'}
+Special: {'\u03b1 \u03b2 \u03b3'}"""
+print(f"Unicode: {str(unicode_test)}")
+
+print("\n=== Trailing whitespace preservation (PEP 750) ===")
+x = 42
+tmpl_trail = t"{x   }"
+expr = tmpl_trail.interpolations[0].expression
+print(f"Expression with trailing spaces: |{expr}|")
+assert expr == "x", f"Expected 'x' but got '{expr}'"
+assert len(expr) == 1, f"Expected length 1 but got {len(expr)}"
+
+tmpl_both = t"{   x   }"
+expr2 = tmpl_both.interpolations[0].expression
+print(f"Expression with both spaces: |{expr2}|")
+assert expr2 == "   x", f"Expected '   x' but got '{expr2}'"
+assert len(expr2) == 4, f"Expected length 4 but got {len(expr2)}"
+
+tmpl_lead = t"{   x}"
+expr3 = tmpl_lead.interpolations[0].expression
+print(f"Expression with leading spaces: |{expr3}|")
+assert expr3 == "   x", f"Expected '   x' but got '{expr3}'"
+
+# Debug specifiers: leading space preserved, trailing space stripped from expression
+# PEP 750: "Whitespace is preserved in the debug specifier" (in strings part)
+z = 99
+t_debug4 = t"{z =}"
+assert t_debug4.strings == ("z =", "")
+assert t_debug4.interpolations[0].expression == "z"  # Trailing space stripped
+assert t_debug4.interpolations[0].conversion == "r"
+
+t_debug5 = t"{ z=}"
+assert t_debug5.strings == (" z=", "")
+assert t_debug5.interpolations[0].expression == " z"  # Leading space preserved
+assert t_debug5.interpolations[0].conversion == "r"
+
+t_debug6 = t"{ z =}"
+assert t_debug6.strings == (" z =", "")
+assert t_debug6.interpolations[0].expression == " z"  # Leading preserved, trailing stripped
+assert t_debug6.interpolations[0].conversion == "r"
+
+print("Trailing whitespace: PASS")
+
+print("\n=== Error message tests ===")
+try:
+    exec('t"}"')
+except SyntaxError:
+    print("Lone }} rejected (correct)")
+
+try:
+    exec('t"{"')
+except SyntaxError:
+    print("Unterminated {{ rejected (correct)")
+
+print("\n=== Triple quote empty interpolation ===")
+try:
+    exec('''empty_interp = t"""Start
+{}
+End"""''')
+    print(f"Empty interpolation: {str(empty_interp)}")
+except Exception as e:
+    print(f"Empty interpolation error: {type(e).__name__}: {e}")
+
+print("\nIncompatible tests completed!")
diff --git a/tests/basics/string_tstring_basic1.py.exp b/tests/basics/string_tstring_basic1.py.exp
new file mode 100644
index 00000000000..7f2b0d85b60
--- /dev/null
+++ b/tests/basics/string_tstring_basic1.py.exp
@@ -0,0 +1,41 @@
+=== Parser error tests ===
+Empty expr: SyntaxError - malformed f-string
+Whitespace: Template(strings=('', ''), interpolations=(Interpolation(42, ' 42', None, ''),))
+
+=== Error cases ===
+Invalid syntax: SyntaxError
+Long expr: Error
+Mixed concat: SyntaxError
+bt prefix: SyntaxError
+
+=== Escape sequence coverage tests ===
+Template(strings=('\\8',), interpolations=())
+Template(strings=('\\9',), interpolations=())
+Template(strings=('\\z',), interpolations=())
+Template(strings=('\\k',), interpolations=())
+Invalid \x escape: SyntaxError
+Invalid \u escape: SyntaxError
+Invalid \U escape: SyntaxError
+Template(strings=('\x00\x01ÿ',), interpolations=())
+Template(strings=('A',), interpolations=())
+Template(strings=('α',), interpolations=())
+Template(strings=('❤',), interpolations=())
+Template(strings=('A',), interpolations=())
+Template(strings=('😀',), interpolations=())
+Template(strings=('ABC',), interpolations=())
+Unicode: Template(strings=('Unicode test:\nEmoji: ', '\nSpecial: ', ''), interpolations=(Interpolation('🐍', "'\\U0001f40d'", None, ''), Interpolation('α β γ', "'\\u03b1 \\u03b2 \\u03b3'", None, '')))
+
+=== Trailing whitespace preservation (PEP 750) ===
+Expression with trailing spaces: |x|
+Expression with both spaces: |   x|
+Expression with leading spaces: |   x|
+Trailing whitespace: PASS
+
+=== Error message tests ===
+Lone }} rejected (correct)
+Unterminated {{ rejected (correct)
+
+=== Triple quote empty interpolation ===
+Empty interpolation error: SyntaxError: malformed f-string
+
+Incompatible tests completed!
diff --git a/tests/basics/string_tstring_constructor.py b/tests/basics/string_tstring_constructor.py
new file mode 100644
index 00000000000..dc4a09a9578
--- /dev/null
+++ b/tests/basics/string_tstring_constructor.py
@@ -0,0 +1,112 @@
+from string.templatelib import Template, Interpolation
+
+print("=== Constructor basic usage ===")
+t = Template("hello ", Interpolation(42, "x"), "world")
+print(f"Template repr: {repr(t)}")
+
+t_varargs = Template("Hello ", Interpolation("World", "name"), "!")
+print(f"Varargs constructor: strings={t_varargs.strings}, values={t_varargs.values}")
+
+t_concat = Template("A", "B", Interpolation(1, "value"), "C", "D")
+print(f"Varargs merged strings: {t_concat.strings}")
+
+t_leading = Template(Interpolation(1, "x"), " tail")
+print(f"Leading interpolation strings: {t_leading.strings}")
+
+t_trailing = Template("head ", Interpolation(2, "y"))
+print(f"Trailing interpolation strings: {t_trailing.strings}")
+
+t_interps_only = Template(Interpolation(1, "x"), Interpolation(2, "y"))
+print(f"Interpolation only strings: {t_interps_only.strings}")
+
+print("\n=== Special cases ===")
+
+i = Interpolation(42, "x", "s", ":>10")
+try:
+    i.value = 100
+except AttributeError:
+    print("Interp read-only: AttributeError")
+
+t_ws_trim = Template("", Interpolation(None, "   ", None, ""), "")
+print(f"Whitespace trim: '{t_ws_trim}'")
+
+t_debug = Template("", Interpolation(42, "x=", None, ""), "")
+print(f"Debug =: {t_debug}")
+
+
+class Custom:
+    def __repr__(self):
+        return "CustomRepr"
+
+    def __str__(self):
+        return "CustomStr"
+
+
+obj = Custom()
+print(f"Custom !r: {t'{obj!r}'}")
+print(f"Custom !s: {t'{obj!s}'}")
+
+t_empty_start = Template("", Interpolation(1, "1"), "text")
+print(f"Empty start iter: {[type(x).__name__ for x in t_empty_start]}")
+
+t_iter_edge = Template("", Interpolation(1, "1"), "", Interpolation(2, "2"), "")
+iter_items = []
+for item in t_iter_edge:
+    iter_items.append(type(item).__name__)
+print(f"Iterator edge: {iter_items}")
+
+print("\n=== Values property ===")
+for n in range(7):
+    args = []
+    for i in range(n):
+        args.append("")
+        args.append(Interpolation(i, str(i)))
+    args.append("")
+    t = Template(*args)
+    print(f"Values[{n}]: {t.values}")
+
+print("\n=== Multiple consecutive strings ===")
+try:
+    t = Template("first", "second", "third", Interpolation(42, "x"), "fourth", "fifth")
+    if t.strings == ("firstsecondthird", "fourthfifth"):
+        print("Multiple strings concatenated: OK")
+    else:
+        print(f"Multiple strings: strings={t.strings}")
+except Exception as e:
+    print(f"Multiple strings error: {e}")
+
+print("\n=== Template() constructor with many interpolations ===")
+try:
+    exprs = ["a", "b", "c", "d", "e"]
+    interps = [Interpolation(i, exprs[i % len(exprs)]) for i in range(20)]
+    strings = [""] * 21
+    t = Template(*strings, *interps)
+    print(f"Template() constructor: OK ({len(t.interpolations)} interpolations)")
+except Exception as e:
+    print(f"Template() constructor: {type(e).__name__}")
+
+print("\n=== vstr string concatenation ===")
+try:
+    t1 = Template("part1", "part2", "part3", "part4", Interpolation(1, "x"), "end")
+    result = str(t1)
+    print(f"vstr concat: '{result}'")
+except Exception as e:
+    print(f"vstr concat error: {e}")
+
+print("\n=== High byte handling ===")
+try:
+    result = t"\x7f\x80\x81\xfe\xff"
+    first_str = result.strings[0]
+    print(
+        f"High bytes: len={len(first_str)}, first=0x{ord(first_str[0]):02x}, last=0x{ord(first_str[-1]):02x}"
+    )
+except Exception as e:
+    print(f"High bytes error: {e}")
+
+try:
+    result = t"\200\201\377"
+    print(f"Octal high bytes: OK, len={len(result.strings[0])}")
+except Exception as e:
+    print(f"Octal high bytes error: {e}")
+
+print("\nConstructor tests completed!")
diff --git a/tests/basics/string_tstring_constructor.py.exp b/tests/basics/string_tstring_constructor.py.exp
new file mode 100644
index 00000000000..e7d6ea90a9b
--- /dev/null
+++ b/tests/basics/string_tstring_constructor.py.exp
@@ -0,0 +1,40 @@
+=== Constructor basic usage ===
+Template repr: Template(strings=('hello ', 'world'), interpolations=(Interpolation(42, 'x', None, ''),))
+Varargs constructor: strings=('Hello ', '!'), values=('World',)
+Varargs merged strings: ('AB', 'CD')
+Leading interpolation strings: ('', ' tail')
+Trailing interpolation strings: ('head ', '')
+Interpolation only strings: ('', '', '')
+
+=== Special cases ===
+Interp read-only: AttributeError
+Whitespace trim: 'Template(strings=('', ''), interpolations=(Interpolation(None, '   ', None, ''),))'
+Debug =: Template(strings=('', ''), interpolations=(Interpolation(42, 'x=', None, ''),))
+Custom !r: Template(strings=('', ''), interpolations=(Interpolation(CustomRepr, 'obj', 'r', ''),))
+Custom !s: Template(strings=('', ''), interpolations=(Interpolation(CustomRepr, 'obj', 's', ''),))
+Empty start iter: ['Interpolation', 'str']
+Iterator edge: ['Interpolation', 'Interpolation']
+
+=== Values property ===
+Values[0]: ()
+Values[1]: (0,)
+Values[2]: (0, 1)
+Values[3]: (0, 1, 2)
+Values[4]: (0, 1, 2, 3)
+Values[5]: (0, 1, 2, 3, 4)
+Values[6]: (0, 1, 2, 3, 4, 5)
+
+=== Multiple consecutive strings ===
+Multiple strings concatenated: OK
+
+=== Template() constructor with many interpolations ===
+Template() constructor: OK (20 interpolations)
+
+=== vstr string concatenation ===
+vstr concat: 'Template(strings=('part1part2part3part4', 'end'), interpolations=(Interpolation(1, 'x', None, ''),))'
+
+=== High byte handling ===
+High bytes: len=5, first=0x7f, last=0xff
+Octal high bytes: OK, len=3
+
+Constructor tests completed!
diff --git a/tests/basics/string_tstring_constructor1.py b/tests/basics/string_tstring_constructor1.py
new file mode 100644
index 00000000000..1593efa9d79
--- /dev/null
+++ b/tests/basics/string_tstring_constructor1.py
@@ -0,0 +1,24 @@
+# NOTE: Error messages are shortened in MicroPython to avoid stack overflow
+# during ROM compression on constrained platforms (Windows x86, ASan).
+# CPython 3.14 message: "Template.__new__ *args need to be of type 'str' or 'Interpolation', got int"
+# MicroPython message: "Template.__new__ args must be str or Interpolation, got 'int'"
+
+from string.templatelib import Template
+
+print("=== Constructor error messages ===")
+try:
+    Template(strings=("test",))
+except TypeError as e:
+    print(f"Keyword args: {e}")
+
+try:
+    Template("hello", 42, "world")
+except TypeError as e:
+    print(f"Invalid type: {e}")
+
+try:
+    Template("a", 42, "b")
+except TypeError as e:
+    print(f"Invalid type in varargs: {e}")
+
+print("\nConstructor error tests completed!")
diff --git a/tests/basics/string_tstring_constructor1.py.exp b/tests/basics/string_tstring_constructor1.py.exp
new file mode 100644
index 00000000000..932529c97b4
--- /dev/null
+++ b/tests/basics/string_tstring_constructor1.py.exp
@@ -0,0 +1,6 @@
+=== Constructor error messages ===
+Keyword args: function doesn't take keyword arguments
+Invalid type: expected str or Interpolation
+Invalid type in varargs: expected str or Interpolation
+
+Constructor error tests completed!
diff --git a/tests/basics/string_tstring_errors1.py b/tests/basics/string_tstring_errors1.py
new file mode 100644
index 00000000000..7a0c737ef67
--- /dev/null
+++ b/tests/basics/string_tstring_errors1.py
@@ -0,0 +1,246 @@
+from string.templatelib import Template, Interpolation
+
+print("\n=== Edge cases ===")
+t_empty = Template()
+print(f"Empty template: '{t_empty}'")
+
+t_empty_strs = Template("", Interpolation(1, "a"), "", Interpolation(2, "b"), "")
+print(f"Empty strings: {list(t_empty_strs)}")
+
+t_adj = t"{1}{2}{3}"
+print(f"Adjacent: '{t_adj}'")
+
+t_single = Template("only")
+print(f"Single iter: {list(t_single)}")
+
+t_self = t"test"
+print(f"Self+self: '{(t_self + t_self)}'")
+
+print("\n=== Additional coverage tests ===")
+
+t_str_literal = t"{'hello'}"
+print(f"String literal: {t_str_literal}")
+
+path = "/usr/local/bin"
+count = 42
+raw_path = rt"Path: {path}\n"
+print(f"Raw path strings: {raw_path.strings}, value={raw_path.interpolations[0].value}")
+
+raw_regex = rt"Regex: \\d+{count}"
+print(f"Raw regex strings: {raw_regex.strings}, value={raw_regex.interpolations[0].value}")
+
+try:
+    t_str_expr = t'{"test"}'
+    print(f"String expr: '{t_str_expr}'")
+except Exception as e:
+    print(f"String expr error: {e}")
+
+def raise_error():
+    raise ValueError("Special error")
+
+try:
+    t_exc = t"{raise_error()}"
+    print(t_exc)
+except ValueError as e:
+    print(f"Re-raised exception: {e}")
+
+try:
+    large_str = "x" * 100000
+    exec(f'very_long_name_{large_str} = t"test"')
+except (ValueError, MemoryError, SyntaxError, RuntimeError) as e:
+    print("Large template: SyntaxError")
+
+try:
+    exec('''t_triple = t"""Triple "quoted" string"""''')
+    print(f"Triple quoted: '{t_triple}'")
+except Exception as e:
+    print(f"Triple quoted error: {e}")
+
+try:
+    exec(r'''t_raw_triple = rt"""Raw triple\n{42}"""''')
+    print(f"Raw triple: '{t_raw_triple}'")
+except Exception as e:
+    print(f"Raw triple error: {e}")
+
+t_concat1 = t"a{1}b"
+t_concat2 = t"c{2}d{3}e"
+t_concat3 = t_concat1 + t_concat2
+print(f"Complex concat: strings={t_concat3.strings}, values={t_concat3.values}")
+
+t_empty = Template()
+t_nonempty = t"test{42}"
+t_concat_empty = t_empty + t_nonempty
+print(f"Empty concat: '{t_concat_empty}'")
+
+t_self_interp = t"x{1}y"
+t_self_concat = t_self_interp + t_self_interp
+print(f"Self interp concat: values={t_self_concat.values}")
+
+try:
+    exec('t"unterminated')
+except SyntaxError as e:
+    print(f"Lonely string: {type(e).__name__}")
+
+try:
+    t_edge = t""
+    print(f"Empty t-string: '{t_edge}'")
+except Exception as e:
+    print(f"Empty t-string error: {e}")
+
+
+print("\n=== High byte handling ===")
+try:
+    result = t'\x7F\x80\x81\xFE\xFF'
+    first_str = result.strings[0]
+    print(f"High bytes: len={len(first_str)}, first=0x{ord(first_str[0]):02x}, last=0x{ord(first_str[-1]):02x}")
+except Exception as e:
+    print(f"High bytes error: {e}")
+
+try:
+    result = t'\200\201\377'
+    print(f"Octal high bytes: OK, len={len(result.strings[0])}")
+except Exception as e:
+    print(f"Octal high bytes error: {e}")
+
+print("\n=== Deep nesting test ===")
+try:
+    nested = "1" + " + 1" * 150
+    code = f't"{{{nested}}}"'
+    exec(code)
+    print("Deep nesting: OK")
+except Exception as e:
+    print(f"Deep nesting error: {type(e).__name__}")
+
+print("\n=== Trailing whitespace in expression ===")
+try:
+    x = 42
+    code = 't"{x   }"'
+    result = eval(code)
+    print(f"Trailing whitespace: OK")
+except Exception as e:
+    print(f"Trailing whitespace error: {e}")
+
+print("\n=== Single closing brace ===")
+try:
+    exec('t"test }"')
+    print("ERROR: Single } should have raised SyntaxError")
+except SyntaxError as e:
+    print(f"Single }}: SyntaxError - {e}")
+
+print("\n=== Escape in string within interpolation ===")
+
+try:
+    compile('x = 1; t"{x=:"', '', 'exec')
+except SyntaxError:
+    print('Debug unclosed colon: SyntaxError')
+
+try:
+    compile('x = 1; t"{x=!"', '', 'exec')
+except SyntaxError:
+    print('Debug unclosed exclaim: SyntaxError')
+
+try:
+    compile('t"prefix{incomplete"', '', 'exec')
+except SyntaxError as e:
+    print('Unclosed brace literal: SyntaxError')
+
+try:
+    import sys
+    if hasattr(sys.implementation, '_mpy'):
+        compile('f"{x!invalid}"', '', 'exec')
+except SyntaxError:
+    print('Malformed f-string: SyntaxError')
+
+# NOTE: Error messages are shortened in MicroPython to avoid stack overflow
+# during ROM compression on constrained platforms (Windows x86, ASan).
+# CPython 3.14 message: "Template.__new__ *args need to be of type 'str' or 'Interpolation', got int"
+# MicroPython message: "Template.__new__ args must be str or Interpolation, got 'int'"
+
+try:
+    t = Template("string", 123)
+except TypeError as e:
+    print('Type error int:', e)
+
+try:
+    t = Template("a", "b", 3.14)
+except TypeError:
+    print('Type error float: TypeError')
+
+try:
+    compile('t"text { more text"', '', 'exec')
+except SyntaxError as e:
+    print('Unmatched brace:', e)
+
+try:
+    compile('x=1; t"{x= {nested}}"', '', 'exec')
+except SyntaxError:
+    print('Nested debug braces: SyntaxError')
+
+try:
+    compile('t"{x!}"', '', 'exec')
+except SyntaxError:
+    print('Missing conversion: SyntaxError')
+
+try:
+    compile('t"{x!z}"', '', 'exec')
+except SyntaxError as e:
+    print('Invalid conversion:', e)
+
+try:
+    compile('t"{x!r' + chr(0x0b) + ':10}"', '', 'exec')
+except SyntaxError as e:
+    print('Vertical tab:', e)
+
+try:
+    compile('t"{x!r@:10}"', '', 'exec')
+except SyntaxError as e:
+    print('Invalid char after conversion:', e)
+
+# MicroPython doesn't allow space after conversion.
+# try:
+#     x = 'test'
+#     exec('result = t"{x!r :10}"')
+#     print('Space after conversion: OK')
+# except Exception as e:
+#     print(f'Unexpected error: {e}')
+
+try:
+    x = 'test'
+    exec('result = t"{x!s\\t:10}"')
+    print('Tab after conversion: OK')
+except Exception as e:
+    print(f'Unexpected error: {e}')
+
+try:
+    x = 'test'
+    exec('result = t"{x!r\\n:10}"')
+    print('Newline after conversion: OK')
+except Exception as e:
+    print(f'Unexpected error: {e}')
+
+try:
+    x = 'test'
+    exec('result = t"{x!r\\r:10}"')
+    print('CR after conversion: OK')
+except Exception as e:
+    print(f'Unexpected error: {e}')
+
+try:
+    x = 'test'
+    exec('result = t"{x!s\\f:10}"')
+    print('Form-feed after conversion: OK')
+except Exception as e:
+    print(f'Unexpected error: {e}')
+
+print("\n=== Mixed prefixes ===")
+for src in ('ft"{x}"', 'tf"{x}"', 'frt"{x}"', 'rtf"{x}"', 'trf"{x}"'):
+    try:
+        exec(src)
+        print(src.split('"')[0] + ": OK (BUG!)")
+    except SyntaxError as e:
+        prefix = src.split('"')[0]
+        # Check if we get the specific error message or generic one
+        if "'f' and 't' prefixes are incompatible" in str(e):
+            print(f"{prefix}: incompatible prefixes")
+        else:
+            print(f"{prefix}: invalid syntax")
diff --git a/tests/basics/string_tstring_errors1.py.exp b/tests/basics/string_tstring_errors1.py.exp
new file mode 100644
index 00000000000..f8d993770d1
--- /dev/null
+++ b/tests/basics/string_tstring_errors1.py.exp
@@ -0,0 +1,60 @@
+
+=== Edge cases ===
+Empty template: 'Template(strings=('',), interpolations=())'
+Empty strings: [Interpolation(1, 'a', None, ''), Interpolation(2, 'b', None, '')]
+Adjacent: 'Template(strings=('', '', '', ''), interpolations=(Interpolation(1, '1', None, ''), Interpolation(2, '2', None, ''), Interpolation(3, '3', None, '')))'
+Single iter: ['only']
+Self+self: 'Template(strings=('testtest',), interpolations=())'
+
+=== Additional coverage tests ===
+String literal: Template(strings=('', ''), interpolations=(Interpolation('hello', "'hello'", None, ''),))
+Raw path strings: ('Path: ', '\\n'), value=/usr/local/bin
+Raw regex strings: ('Regex: \\\\d+', ''), value=42
+String expr: 'Template(strings=('', ''), interpolations=(Interpolation('test', '"test"', None, ''),))'
+Re-raised exception: Special error
+Large template: SyntaxError
+Triple quoted: 'Template(strings=('Triple "quoted" string',), interpolations=())'
+Raw triple: 'Template(strings=('Raw triple\\n', ''), interpolations=(Interpolation(42, '42', None, ''),))'
+Complex concat: strings=('a', 'bc', 'd', 'e'), values=(1, 2, 3)
+Empty concat: 'Template(strings=('test', ''), interpolations=(Interpolation(42, '42', None, ''),))'
+Self interp concat: values=(1, 1)
+Lonely string: SyntaxError
+Empty t-string: 'Template(strings=('',), interpolations=())'
+
+=== High byte handling ===
+High bytes: len=5, first=0x7f, last=0xff
+Octal high bytes: OK, len=3
+
+=== Deep nesting test ===
+Deep nesting: OK
+
+=== Trailing whitespace in expression ===
+Trailing whitespace: OK
+
+=== Single closing brace ===
+Single }: SyntaxError - malformed f-string
+
+=== Escape in string within interpolation ===
+Debug unclosed colon: SyntaxError
+Debug unclosed exclaim: SyntaxError
+Unclosed brace literal: SyntaxError
+Malformed f-string: SyntaxError
+Type error int: expected str or Interpolation
+Type error float: TypeError
+Unmatched brace: malformed f-string
+Nested debug braces: SyntaxError
+Missing conversion: SyntaxError
+Invalid conversion: invalid syntax
+Vertical tab: invalid syntax
+Invalid char after conversion: invalid syntax
+Unexpected error: invalid syntax
+Unexpected error: invalid syntax
+Unexpected error: invalid syntax
+Unexpected error: invalid syntax
+
+=== Mixed prefixes ===
+ft: invalid syntax
+tf: invalid syntax
+frt: invalid syntax
+rtf: invalid syntax
+trf: invalid syntax
diff --git a/tests/basics/string_tstring_format1.py b/tests/basics/string_tstring_format1.py
new file mode 100644
index 00000000000..54fb0f1e76b
--- /dev/null
+++ b/tests/basics/string_tstring_format1.py
@@ -0,0 +1,186 @@
+print("\n=== Format spec edge cases ===")
+print(f"Empty fmt: {t'{42:}'}")
+print(f"Width: '{t'{42:10}'}'")
+print(f"Conv+fmt: '{t'{42!r:>10}'}'")
+
+width = 10
+print(f"Interp fmt: '{t'{3.14:{width}.2f}'}'")
+
+try:
+    t_escaped = t"Hello {{name}} and {{{{value}}}}"
+    print(f"Escaped braces: '{t_escaped}'")
+except Exception as e:
+    print(f"Escaped braces error: {e}")
+
+try:
+    t_conv_fmt = t"{42!r:}"
+    print(f"Conv empty fmt: '{t_conv_fmt}'")
+except Exception as e:
+    print(f"Conv fmt error: {e}")
+
+print("\n=== Format spec edge cases ===")
+try:
+    x = 42
+    result = t'{x!r:0>+#10.5}'
+    print(f"Full format: '{result}'")
+except Exception as e:
+    print(f"Full format error: {e}")
+
+print("\n=== Format spec with special characters ===")
+try:
+    x = 42
+    result = t"{x:!r}"
+    print(f"Format spec '!r': {result.interpolations[0].format_spec == '!r'}")
+    print(f"Conversion is None: {result.interpolations[0].conversion is None}")
+except Exception as e:
+    print(f"Format spec error: {type(e).__name__}")
+
+print("\nCoverage tests completed!")
+print("\n=== Debug format edge cases ===")
+try:
+    x = 42
+    result = t'{x=!r}'
+    print(f"Debug with conv: '{result}'")
+except SyntaxError as e:
+    print(f"Debug conv: SyntaxError - {e}")
+
+
+print("\n=== Debug specifier tests ===")
+try:
+    value = 42
+    t1 = t'{value=}'
+    print(f"Debug basic: strings={t1.strings}, conv={t1.interpolations[0].conversion}")
+
+    t2 = t'{value=!s}'
+    print(f"Debug !s: strings={t2.strings}, conv={t2.interpolations[0].conversion}")
+
+    t3 = t'{value=:>10}'
+    print(f"Debug fmt: strings={t3.strings}, fmt_spec='{t3.interpolations[0].format_spec}'")
+except Exception as e:
+    print(f"Debug format error: {e}")
+
+
+print("\n=== Large number of interpolations ===")
+for N in (62, 63, 64, 65):
+    x = 1
+    code = 'result = str(t"' + '{x}' * N + '")'
+    exec(code)
+    expected = "Template(strings=" + repr(tuple("" for _ in range(N + 1))) + ", interpolations=(" + ", ".join("Interpolation(1, 'x', None, '')" for _ in range(N)) + "))"
+    print(N, result == expected)
+
+print("\n=== Malformed format specs (valid in CPython) ===")
+x = 42
+value = 42
+result = t'{x:10!r}'
+print(f"Malformed 1: format_spec: '{result.interpolations[0].format_spec}', conversion: {result.interpolations[0].conversion}")
+
+result = t'{value:^10!s}'
+print(f"Malformed 2: format_spec: '{result.interpolations[0].format_spec}', conversion: {result.interpolations[0].conversion}")
+
+result = t'{x:>!10}'
+print(f"Malformed 3: format_spec: '{result.interpolations[0].format_spec}', conversion: {result.interpolations[0].conversion}")
+
+try:
+    exec(r"t'\U00110000'")
+    print("ERROR: Should have failed")
+except SyntaxError as e:
+    print(f"Invalid unicode: SyntaxError - {e}")
+
+class CustomException(Exception):
+    pass
+
+class BadClass:
+    def __getattr__(self, name):
+        raise CustomException("Test exception")
+
+try:
+    bad_obj = BadClass()
+    result = t"{bad_obj.attr}"
+    print("ERROR: Should have raised exception")
+except CustomException as e:
+    print(f"Custom exception re-raised: {e}")
+except Exception as e:
+    print(f"Other exception: {type(e).__name__} - {e}")
+
+print("\nNULL lexer: Tested in heapalloc_fail_tstring.py")
+
+try:
+    exec(r"t'\N{LATIN SMALL LETTER A}'")
+    print("ERROR: \\N{{}} should not be supported")
+except (SyntaxError, NotImplementedError) as e:
+    print(f"\\N{{}} escape: SyntaxError")
+
+try:
+    result = rt'\u0041\U00000042'
+    print(f"Raw Unicode escapes: '{result.strings[0]}'")
+except Exception as e:
+    print(f"Raw Unicode error: {e}")
+
+try:
+    code_with_crlf = 't"line1\\r\\nline2"'
+    exec(code_with_crlf)
+    print("CR LF handling: OK")
+except Exception as e:
+    print(f"CR LF error: {e}")
+
+try:
+    code = ""
+    for i in range(20):
+        code += " " * i + "if True:\n"
+    code += " " * 20 + "x = t'deep indent'"
+    exec(code)
+    print("Deep indent: OK")
+except Exception as e:
+    print(f"Deep indent error: {e}")
+
+try:
+    exec("t'test\\")
+    print("ERROR: Trailing backslash should fail")
+except SyntaxError as e:
+    print(f"Trailing backslash: SyntaxError")
+
+print("\n=== Empty format spec node ===")
+try:
+    x = 42
+    result = t'{x:}'
+    print(f"Empty format after colon: OK")
+except Exception as e:
+    print(f"Empty format spec error: {e}")
+
+
+print("\n=== Format spec with special characters (coverage) ===")
+try:
+    align = "<"
+    width = 5
+    value = "test"
+    result = t'{value:{"^"*width}}'
+    print(f"Double quotes expr: format_spec={repr(result.interpolations[0].format_spec)}")
+except Exception as e:
+    print(f"Double quotes expr error: {type(e).__name__}: {e}")
+
+try:
+    align = "<"
+    width = 5
+    value = "test"
+    result = rt"{value:{align}{width}\\}"
+    print(f"Backslash raw: format_spec={repr(result.interpolations[0].format_spec)}")
+except Exception as e:
+    print(f"Backslash raw error: {type(e).__name__}: {e}")
+
+try:
+    align = "<"
+    width = 5
+    value = "test"
+    result = t"{value:{align}{width}\\}"
+    print(f"Backslash non-raw: format_spec={repr(result.interpolations[0].format_spec)}")
+except Exception as e:
+    print(f"Backslash non-raw error: {type(e).__name__}: {e}")
+
+try:
+    align = "<"
+    width = 5
+    value = "test"
+    result = t"{value:{align}{width}\n\t\r}"
+    print(f"Multiple escapes: format_spec={repr(result.interpolations[0].format_spec)}")
+except Exception as e:
+    print(f"Multiple escapes error: {type(e).__name__}: {e}")
diff --git a/tests/basics/string_tstring_format1.py.exp b/tests/basics/string_tstring_format1.py.exp
new file mode 100644
index 00000000000..601087ccfd3
--- /dev/null
+++ b/tests/basics/string_tstring_format1.py.exp
@@ -0,0 +1,54 @@
+
+=== Format spec edge cases ===
+Empty fmt: Template(strings=('', ''), interpolations=(Interpolation(42, '42', None, ''),))
+Width: 'Template(strings=('', ''), interpolations=(Interpolation(42, '42', None, '10'),))'
+Conv+fmt: 'Template(strings=('', ''), interpolations=(Interpolation(42, '42', 'r', '>10'),))'
+Interp fmt: 'Template(strings=('', ''), interpolations=(Interpolation(3.14, '3.14', None, '10.2f'),))'
+Escaped braces: 'Template(strings=('Hello {name} and {{value}}',), interpolations=())'
+Conv empty fmt: 'Template(strings=('', ''), interpolations=(Interpolation(42, '42', 'r', ''),))'
+
+=== Format spec edge cases ===
+Full format: 'Template(strings=('', ''), interpolations=(Interpolation(42, 'x', 'r', '0>+#10.5'),))'
+
+=== Format spec with special characters ===
+Format spec '!r': True
+Conversion is None: True
+
+Coverage tests completed!
+
+=== Debug format edge cases ===
+Debug with conv: 'Template(strings=('x=', ''), interpolations=(Interpolation(42, 'x', 'r', ''),))'
+
+=== Debug specifier tests ===
+Debug basic: strings=('value=', ''), conv=r
+Debug !s: strings=('value=', ''), conv=s
+Debug fmt: strings=('value=', ''), fmt_spec='>10'
+
+=== Large number of interpolations ===
+62 True
+63 True
+64 True
+65 True
+
+=== Malformed format specs (valid in CPython) ===
+Malformed 1: format_spec: '10!r', conversion: None
+Malformed 2: format_spec: '^10!s', conversion: None
+Malformed 3: format_spec: '>!10', conversion: None
+Invalid unicode: SyntaxError - invalid syntax
+Custom exception re-raised: Test exception
+
+NULL lexer: Tested in heapalloc_fail_tstring.py
+\N{} escape: SyntaxError
+Raw Unicode escapes: '\u0041\U00000042'
+CR LF handling: OK
+Deep indent: OK
+Trailing backslash: SyntaxError
+
+=== Empty format spec node ===
+Empty format after colon: OK
+
+=== Format spec with special characters (coverage) ===
+Double quotes expr: format_spec='^^^^^'
+Backslash raw: format_spec='<5\\\\'
+Backslash non-raw: format_spec='<5\\'
+Multiple escapes: format_spec='<5\n\t\r'
diff --git a/tests/basics/string_tstring_interpolation1.py b/tests/basics/string_tstring_interpolation1.py
new file mode 100644
index 00000000000..da9c469d2b3
--- /dev/null
+++ b/tests/basics/string_tstring_interpolation1.py
@@ -0,0 +1,72 @@
+print("\n=== Bracket/paren depth tracking ===")
+d = {'a': 1, 'b:c': 2}
+items = [10, 20, 30]
+colon_key = t"{d['b:c']}"
+print(f"Colon in key: expr={colon_key.interpolations[0].expression}, value={colon_key.interpolations[0].value}")
+
+slice_expr = t"{items[1:3]}"
+print(f"Slice colon: expr={slice_expr.interpolations[0].expression}, value={slice_expr.interpolations[0].value}")
+
+def pair(x, y=':'):
+    return (x, y)
+
+func_expr = t"{pair(1, 2)}"
+print(f"Function call: expr={func_expr.interpolations[0].expression}, value={func_expr.interpolations[0].value}")
+
+default_expr = t"{pair(3)}"
+print(f"Default arg colon: expr={default_expr.interpolations[0].expression}, value={default_expr.interpolations[0].value}")
+
+matrix = [[1, 2], [3, 4]]
+nested_expr = t"{matrix[0][1]}"
+print(f"Nested brackets: expr={nested_expr.interpolations[0].expression}, value={nested_expr.interpolations[0].value}")
+
+
+print("\n=== Inline template literal tests ===")
+
+try:
+    result = t"outer { t'' }"
+    print(f"Empty nested: {type(result.values[0]).__name__}")
+except Exception as e:
+    print(f"Empty nested: FAIL - {type(e).__name__}: {e}")
+
+try:
+    result = t"outer { t'inner' }"
+    print(f"With content: {type(result.values[0]).__name__}")
+except Exception as e:
+    print(f"With content: FAIL - {type(e).__name__}: {e}")
+
+try:
+    result = t"result: { t'a' + t'b' }"
+    print(f"Concatenation: {type(result.values[0]).__name__}")
+except Exception as e:
+    print(f"Concatenation: FAIL - {type(e).__name__}: {e}")
+
+try:
+    result = t'Single { t"double" }'
+    print(f"Mixed quotes: {type(result.values[0]).__name__}")
+except Exception as e:
+    print(f"Mixed quotes: FAIL - {type(e).__name__}: {e}")
+
+try:
+    x = 42
+    result = t"outer { t'inner {x}' }"
+    print(f"Nested interp: {type(result.values[0]).__name__}")
+except Exception as e:
+    print(f"Nested interp: FAIL - {type(e).__name__}: {e}")
+
+print("\n=== Backslashes not allowed in expressions (PEP 498/750) ===")
+try:
+    code = r"result = t'{\"test\"value\"}'"
+    exec(code)
+    print(f"ERROR: Backslash in expression should raise SyntaxError")
+except SyntaxError as e:
+    print(f"Backslash in expression: SyntaxError")
+
+try:
+    code = r"result = t'{\"\\n\"}'"
+    exec(code)
+    print(f"ERROR: Backslash in expression should raise SyntaxError")
+except SyntaxError as e:
+    print(f"Escaped newline: SyntaxError")
+
+print("\n=== Format spec with special characters ===")
diff --git a/tests/basics/string_tstring_interpolation1.py.exp b/tests/basics/string_tstring_interpolation1.py.exp
new file mode 100644
index 00000000000..8a62f331826
--- /dev/null
+++ b/tests/basics/string_tstring_interpolation1.py.exp
@@ -0,0 +1,20 @@
+
+=== Bracket/paren depth tracking ===
+Colon in key: expr=d['b:c'], value=2
+Slice colon: expr=items[1:3], value=[20, 30]
+Function call: expr=pair(1, 2), value=(1, 2)
+Default arg colon: expr=pair(3), value=(3, ':')
+Nested brackets: expr=matrix[0][1], value=2
+
+=== Inline template literal tests ===
+Empty nested: Template
+With content: Template
+Concatenation: Template
+Mixed quotes: Template
+Nested interp: Template
+
+=== Backslashes not allowed in expressions (PEP 498/750) ===
+Backslash in expression: SyntaxError
+Escaped newline: SyntaxError
+
+=== Format spec with special characters ===
diff --git a/tests/basics/string_tstring_operations.py b/tests/basics/string_tstring_operations.py
new file mode 100644
index 00000000000..3c3222025f8
--- /dev/null
+++ b/tests/basics/string_tstring_operations.py
@@ -0,0 +1,55 @@
+from string.templatelib import Template, Interpolation
+
+print("\n=== Binary operations ===")
+t1 = t"template"
+
+try:
+    t1 + "string"
+except TypeError as e:
+    print(f"Template+str: TypeError")
+
+try:
+    "string" + t1
+except TypeError as e:
+    print(f"str+Template: TypeError")
+
+try:
+    42 + t1
+except TypeError:
+    print("int+Template: TypeError")
+
+for op in ["-", "*", "/", "%", "**", "&", "|", "^", "<<", ">>"]:
+    try:
+        eval(f"t1 {op} t1")
+    except TypeError:
+        print(f"{op}: unsupported")
+
+print("\n=== Template.__add__ with multiple interpolations ===")
+try:
+    exprs = ["a", "b", "c", "d", "e"]
+    interps1 = [Interpolation(i, exprs[i % len(exprs)]) for i in range(20)]
+    strings1 = [""] * 21
+    t1 = Template(*strings1, *interps1)
+
+    interps2 = [Interpolation(i + 20, exprs[i % len(exprs)]) for i in range(20)]
+    strings2 = [""] * 21
+    t2 = Template(*strings2, *interps2)
+
+    result = t1 + t2
+    print(f"Template.__add__: OK ({len(result.interpolations)} interpolations)")
+except Exception as e:
+    print(f"Template.__add__: {type(e).__name__}")
+
+print("\n=== Template + non-string object ===")
+try:
+
+    class CustomObj:
+        pass
+
+    t1 = t"hello"
+    result = t1 + CustomObj()
+    print("ERROR: Should have raised TypeError")
+except TypeError as e:
+    print(f"Template + custom object: TypeError (correct)")
+
+print("\n=== 2-tuple constructor overflow ===")
diff --git a/tests/basics/string_tstring_operations.py.exp b/tests/basics/string_tstring_operations.py.exp
new file mode 100644
index 00000000000..a565db8fec9
--- /dev/null
+++ b/tests/basics/string_tstring_operations.py.exp
@@ -0,0 +1,23 @@
+
+=== Binary operations ===
+Template+str: TypeError
+str+Template: TypeError
+int+Template: TypeError
+-: unsupported
+*: unsupported
+/: unsupported
+%: unsupported
+**: unsupported
+&: unsupported
+|: unsupported
+^: unsupported
+<<: unsupported
+>>: unsupported
+
+=== Template.__add__ with multiple interpolations ===
+Template.__add__: OK (40 interpolations)
+
+=== Template + non-string object ===
+Template + custom object: TypeError (correct)
+
+=== 2-tuple constructor overflow ===
diff --git a/tests/basics/string_tstring_parser1.py b/tests/basics/string_tstring_parser1.py
new file mode 100644
index 00000000000..b8853ccd1e7
--- /dev/null
+++ b/tests/basics/string_tstring_parser1.py
@@ -0,0 +1,288 @@
+from string.templatelib import Template
+
+print("\n=== Empty expression tests ===")
+try:
+    exec("t'{}'")
+    print("ERROR: Empty expression should have raised SyntaxError")
+except SyntaxError as e:
+    print("Empty expr SyntaxError:", e)
+
+try:
+    exec("t'{   }'")
+    print("ERROR: Whitespace-only expression should have raised SyntaxError")
+except SyntaxError as e:
+    print("Whitespace expr SyntaxError:", e)
+
+try:
+    exec("t'{\\n\\t   \\n}'")
+    print("ERROR: Whitespace-only expr should fail")
+except SyntaxError as e:
+    print(f"Whitespace expr: SyntaxError")
+
+try:
+    exec("t'{value'")
+    print("ERROR: Unterminated expr should fail")
+except SyntaxError as e:
+    print(f"Unterminated expr: {type(e).__name__}")
+
+try:
+    exec("t'{   !r}'")
+    print("ERROR: Whitespace-conversion expr should fail")
+except SyntaxError as e:
+    print(f"Whitespace + conversion: {type(e).__name__}")
+
+try:
+    long_expr = 'x' * 104
+    code = f"t'{{{long_expr}}}'"
+    exec(f"{long_expr} = 'test'; result = {code}")
+    print("Long expression: OK")
+except Exception as e:
+    print(f"Long expr error: {type(e).__name__}: {e}")
+
+try:
+    large_str = 'x' * 1099
+    tmpl = Template(large_str)
+    result = str(tmpl)
+    print("Template.__str__ with large string: OK")
+except Exception as e:
+    print(f"Template.__str__ error: {type(e).__name__}: {e}")
+
+try:
+    x = 'a'
+    result = t'{x}'
+    print(f"Basic t-string: OK, result={result}")
+except Exception as e:
+    print(f"Basic t-string error: {type(e).__name__}: {e}")
+
+try:
+    code = 't"' + 'text{nested{x}}more' * 50 + '"'
+    x = 'test'
+    exec(f"x = 'test'; result = {code}")
+    print("Nested interpolations: OK")
+except Exception as e:
+    print(f"Nested interpolations error: {type(e).__name__}: {e}")
+
+width = 5
+x = 42
+try:
+    tmpl = t'{x:{width}{{literal}}}'
+    result = str(tmpl)
+    print(f'Escaped braces result: {result}')
+except ValueError as e:
+    print(f'Escaped braces ValueError: {e}')
+
+print("\n=== Expression parser tests ===")
+try:
+    exec("t'{[{(x,y):[1,2,3]} for x,y in [(1,2),(3,4)]]}'")
+    print("Complex expr: OK")
+except Exception as e:
+    print(f"Complex expr error: {type(e).__name__}")
+
+print("\n=== Lexer edge cases ===")
+print("Lexer NULL case: Tested via heapalloc_fail_tstring.py")
+
+print("\n=== Parser allocation edge cases ===")
+try:
+    deep_expr = "(" * 50 + "x" + ")" * 50
+    exec(f"x = 1; result = t'{{{deep_expr}}}'")
+    print("Deep nesting: OK")
+except Exception as e:
+    print(f"Deep nesting error: {type(e).__name__} - {e}")
+
+try:
+    long_expr = "(" * 100 + "x" + ")" * 100
+    exec(f"x = 1; result = t'{{{long_expr}}}'")
+    print("Very long expression: OK")
+except Exception as e:
+    print(f"Long expression error: {e}")
+
+
+print("\n=== Additional parser regression tests ===")
+
+try:
+    fmt = '"QUOTED"'
+    value = 'raw'
+    tmpl = rt"{value:{fmt}}"
+    print(f"Raw nested spec: {tmpl.interpolations[0].format_spec}")
+except Exception as e:
+    print(f"Raw nested spec error: {type(e).__name__}: {e}")
+
+try:
+    value = 'fmt'
+    tmpl = t'{value:"QUOTED"}'
+    print(f"Escaped quote spec: {tmpl.interpolations[0].format_spec}")
+except Exception as e:
+    print(f"Escaped quote spec error: {type(e).__name__}: {e}")
+
+try:
+    value = 'fmt'
+    tmpl = t"{value:\\n}"
+    print(f"Escaped backslash spec: {tmpl.interpolations[0].format_spec}")
+except Exception as e:
+    print(f"Escaped backslash spec error: {type(e).__name__}: {e}")
+
+try:
+    ns = {}
+    exec("value = 'fmt'\nresult = t\"{value:{'\\\\'}}\"", globals(), ns)
+    tmpl = ns['result']
+    print(f"Nested quoted spec: {tmpl.interpolations[0].format_spec}")
+except Exception as e:
+    print(f"Nested quoted spec error: {type(e).__name__}: {e}")
+
+try:
+    value = 'brace'
+    exec('tmpl = t"{value}}}}tail"')
+    print(f"Literal brace strings: {tmpl.strings}")
+except Exception as e:
+    print(f"Literal brace error: {type(e).__name__}: {e}")
+
+try:
+    exec('t"{value"')
+    print("ERROR: Unterminated field should have raised")
+except SyntaxError as e:
+    print(f"Unterminated field: {e}")
+
+
+print("\n=== Complex expression stress test ===")
+try:
+    vars_dict = {f"x{i}": i for i in range(60)}
+    exec_globals = globals().copy()
+    exec_globals.update(vars_dict)
+    expr_parts = [f"x{i}" for i in range(60)]
+    expr_str = " + ".join(expr_parts)
+    code = f't"{{{expr_str}}}"'
+    result = eval(code, exec_globals)
+    expected = sum(range(60))
+    actual = result.interpolations[0].value
+    if actual == expected:
+        print(f"60 terms: PASS")
+    else:
+        print(f"60 terms: FAIL (expected={expected}, got={actual})")
+except Exception as e:
+    print(f"60 terms: ERROR - {type(e).__name__}: {e}")
+
+try:
+    vars_dict = {f"x{i}": i for i in range(100)}
+    exec_globals = globals().copy()
+    exec_globals.update(vars_dict)
+    expr_parts = [f"x{i}" for i in range(100)]
+    expr_str = " + ".join(expr_parts)
+    code = f't"{{{expr_str}}}"'
+    result = eval(code, exec_globals)
+    expected = sum(range(100))
+    actual = result.interpolations[0].value
+    if actual == expected:
+        print(f"100 terms: PASS")
+    else:
+        print(f"100 terms: FAIL (expected={expected}, got={actual})")
+except Exception as e:
+    print(f"100 terms: ERROR - {type(e).__name__}: {e}")
+
+print("\n=== Stack expansion test (copy_parse_node) ===")
+try:
+    depth = 20
+    nested_expr = "[" * depth + "1" + "]" * depth
+    code = f't"{{{nested_expr}}}"'
+    result = eval(code)
+    if len(str(result.interpolations[0].value)) > 30:
+        print("Stack expansion: OK")
+    else:
+        print(f"Stack expansion: FAIL")
+except Exception as e:
+    print(f"Stack expansion error: {type(e).__name__}")
+
+print("\n=== Format spec with triple-quoted strings ===")
+try:
+    x = 42
+    code = '''t'{x:{"""test"""}}' '''
+    result = eval(code)
+    print(f"Triple-quote in format spec: OK")
+except Exception as e:
+    print(f"Triple-quote error: {type(e).__name__}: {e}")
+
+try:
+    x = 42
+    code = """t"{x:{'''test'''}}" """
+    result = eval(code)
+    print(f"Triple single-quote in format spec: OK")
+except Exception as e:
+    print(f"Triple single-quote error: {type(e).__name__}: {e}")
+
+print("\n=== Format spec parse error (exception handler) ===")
+try:
+    x = 42
+    code = "t\"{x:{1 + (}}\" "  # Unclosed parenthesis
+    result = eval(code)
+    print("ERROR: Should have raised SyntaxError")
+except SyntaxError as e:
+    print(f"Parse error handler: OK")
+except Exception as e:
+    print(f"Unexpected error: {type(e).__name__}: {e}")
+
+try:
+    x = 42
+    code = "t\"{x:{1 2 3}}\" "  # Invalid syntax
+    result = eval(code)
+    print("ERROR: Should have raised SyntaxError")
+except SyntaxError as e:
+    print(f"Parse error handler 2: OK")
+except Exception as e:
+    print(f"Unexpected error 2: {type(e).__name__}: {e}")
+
+print("\n=== Large integer in t-string ===")
+try:
+    # Use eval() to avoid compile-time overflow in longlong variant
+    large_int = eval("10**30")
+    tmpl = t"{large_int}"
+    if tmpl.interpolations[0].value == large_int:
+        print("Large integer: OK")
+    else:
+        print(f"Large integer: FAIL")
+except OverflowError:
+    # longlong variant doesn't support arbitrary precision integers
+    print("Large integer: OK")
+except Exception as e:
+    print(f"Large integer error: {type(e).__name__}: {e}")
+
+print("\n=== Bytes in expression ===")
+try:
+    data = b"test"
+    tmpl = t"{data}"
+    if tmpl.interpolations[0].value == b"test":
+        print("Bytes in expression: OK")
+    else:
+        print(f"Bytes: FAIL")
+except Exception as e:
+    print(f"Bytes error: {type(e).__name__}: {e}")
+
+print("\n=== Boolean constants ===")
+try:
+    tmpl = t"{True} {False}"
+    if tmpl.values == (True, False):
+        print("Boolean constants: OK")
+    else:
+        print(f"Booleans: FAIL")
+except Exception as e:
+    print(f"Boolean error: {type(e).__name__}: {e}")
+
+print("\n=== None constant ===")
+try:
+    tmpl = t"{None}"
+    if tmpl.values[0] is None:
+        print("None constant: OK")
+    else:
+        print(f"None: FAIL")
+except Exception as e:
+    print(f"None error: {type(e).__name__}: {e}")
+
+print("\n=== Deep nesting for rule stack expansion ===")
+try:
+    depth = 80
+    code = '(' * depth + '1' + ')' * depth
+    result = eval(code)
+    if result == 1:
+        print(f"Deep nesting (depth={depth}): OK")
+    else:
+        print(f"Deep nesting: FAIL (result={result})")
+except Exception as e:
+    print(f"Deep nesting error: {type(e).__name__}: {e}")
diff --git a/tests/basics/string_tstring_parser1.py.exp b/tests/basics/string_tstring_parser1.py.exp
new file mode 100644
index 00000000000..cd55b258546
--- /dev/null
+++ b/tests/basics/string_tstring_parser1.py.exp
@@ -0,0 +1,60 @@
+
+=== Empty expression tests ===
+Empty expr SyntaxError: malformed f-string
+Whitespace expr SyntaxError: malformed f-string
+Whitespace expr: SyntaxError
+Unterminated expr: SyntaxError
+Whitespace + conversion: SyntaxError
+Long expression: OK
+Template.__str__ with large string: OK
+Basic t-string: OK, result=Template(strings=('', ''), interpolations=(Interpolation('a', 'x', None, ''),))
+Nested interpolations error: SyntaxError: invalid syntax
+Escaped braces result: Template(strings=('', ''), interpolations=(Interpolation(42, 'x', None, '5{literal}'),))
+
+=== Expression parser tests ===
+Complex expr: OK
+
+=== Lexer edge cases ===
+Lexer NULL case: Tested via heapalloc_fail_tstring.py
+
+=== Parser allocation edge cases ===
+Deep nesting: OK
+Very long expression: OK
+
+=== Additional parser regression tests ===
+Raw nested spec: "QUOTED"
+Escaped quote spec: "QUOTED"
+Escaped backslash spec: \n
+Nested quoted spec: \
+Literal brace error: SyntaxError: malformed f-string
+Unterminated field: malformed f-string
+
+=== Complex expression stress test ===
+60 terms: PASS
+100 terms: PASS
+
+=== Stack expansion test (copy_parse_node) ===
+Stack expansion: OK
+
+=== Format spec with triple-quoted strings ===
+Triple-quote in format spec: OK
+Triple single-quote in format spec: OK
+
+=== Format spec parse error (exception handler) ===
+Parse error handler: OK
+Parse error handler 2: OK
+
+=== Large integer in t-string ===
+Large integer: OK
+
+=== Bytes in expression ===
+Bytes in expression: OK
+
+=== Boolean constants ===
+Boolean constants: OK
+
+=== None constant ===
+None constant: OK
+
+=== Deep nesting for rule stack expansion ===
+Deep nesting (depth=80): OK
diff --git a/tests/basics/string_tstring_whitespace.py b/tests/basics/string_tstring_whitespace.py
new file mode 100644
index 00000000000..08dd7933c38
--- /dev/null
+++ b/tests/basics/string_tstring_whitespace.py
@@ -0,0 +1,25 @@
+print("# Empty expression (whitespace only)")
+try:
+    exec('t"{ }"')
+    print("ERROR: Should have raised SyntaxError")
+except SyntaxError as e:
+    print("Empty expr (space): SyntaxError (correct)")
+
+try:
+    exec('t"{\t}"')
+    print("ERROR: Should have raised SyntaxError")
+except SyntaxError as e:
+    print("Empty expr (tab): SyntaxError (correct)")
+
+try:
+    exec('t"{\n}"')
+    print("ERROR: Should have raised SyntaxError")
+except SyntaxError as e:
+    print("Empty expr (newline): SyntaxError (correct)")
+
+print("\n# Lexer escape sequence: invalid then valid")
+try:
+    exec("t'\\U00200000\\x41'")
+    print("ERROR: Should have raised SyntaxError")
+except SyntaxError as e:
+    print("Invalid escape sequence: SyntaxError (correct)")
diff --git a/tests/basics/string_tstring_whitespace.py.exp b/tests/basics/string_tstring_whitespace.py.exp
new file mode 100644
index 00000000000..f5868faf68f
--- /dev/null
+++ b/tests/basics/string_tstring_whitespace.py.exp
@@ -0,0 +1,7 @@
+# Empty expression (whitespace only)
+Empty expr (space): SyntaxError (correct)
+Empty expr (tab): SyntaxError (correct)
+Empty expr (newline): SyntaxError (correct)
+
+# Lexer escape sequence: invalid then valid
+Invalid escape sequence: SyntaxError (correct)
diff --git a/tests/basics/struct1.py b/tests/basics/struct1.py
index c4960d2f801..e473ffcc118 100644
--- a/tests/basics/struct1.py
+++ b/tests/basics/struct1.py
@@ -1,11 +1,8 @@
 try:
-    import ustruct as struct
-except:
-    try:
-        import struct
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    import struct
+except ImportError:
+    print("SKIP")
+    raise SystemExit
 
 print(struct.calcsize("h", 1))
 print(struct.pack("b", 1))
+print(struct.pack("x"))
 
 print(struct.pack("bI", -128, 256))
@@ -32,6 +31,13 @@
 print(struct.unpack("<6sH", b"foo\0\0\0\x12\x34"))
 print(struct.pack("<6sH", b"foo", 10000))
 
+print(struct.calcsize("7xx"))
+print(struct.pack("7xx"))
+
+print(struct.calcsize(">bxI3xH"))
+print(struct.pack(">bxI3xH", 1, 2, 3))
+print(struct.unpack(">bxI3xH", b"\x01\0\0\0\0\x02\0\0\0\0\x03"))
+
 s = struct.pack("BHBI", 10, 100, 200, 300)
 v = struct.unpack("BHBI", s)
 print(v == (10, 100, 200, 300))
diff --git a/tests/basics/struct1_intbig.py b/tests/basics/struct1_intbig.py
index 24541c8a425..ef1f0579cfa 100644
--- a/tests/basics/struct1_intbig.py
+++ b/tests/basics/struct1_intbig.py
@@ -1,11 +1,8 @@
 try:
-    import ustruct as struct
-except:
-    try:
-        import struct
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    import struct
+except ImportError:
+    print("SKIP")
+    raise SystemExit
 
 # check maximum pack on 32-bit machine
 print(struct.pack(" good message
 BadException __new__
- exceptions must derive from BaseException
+ ('exceptions must derive from BaseException',)
 BadException __new__
- exceptions must derive from BaseException
+ ('exceptions must derive from BaseException',)
diff --git a/tests/basics/subclass_native_init.py b/tests/basics/subclass_native_init.py
index 38d2f23ac38..102befd551f 100644
--- a/tests/basics/subclass_native_init.py
+++ b/tests/basics/subclass_native_init.py
@@ -1,11 +1,44 @@
 # test subclassing a native type and overriding __init__
 
+if not hasattr(object, "__init__"):
+    print("SKIP")
+    raise SystemExit
+
 # overriding list.__init__()
 class L(list):
     def __init__(self, a, b):
         super().__init__([a, b])
 print(L(2, 3))
 
+# with keyword arguments, with star arguments and without because those use different C calls
+class D(dict):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+print(D())
+print(D([('a', 1)]))
+print(D([('a', 1)], a=2, b=3))
+print(D(a=2, b=3))
+
+class D(dict):
+    def __init__(self):
+        super().__init__()
+print(D())
+
+class D(dict):
+    def __init__(self):
+        super().__init__([])
+print(D())
+
+class D(dict):
+    def __init__(self):
+        super().__init__(a=1)
+print(D())
+
+class D(dict):
+    def __init__(self):
+        super().__init__([], a=1)
+print(D())
+
 # inherits implicitly from object
 class A:
     def __init__(self):
diff --git a/tests/basics/syntaxerror.py b/tests/basics/syntaxerror.py
index c0702cb2452..7b6ef9b7550 100644
--- a/tests/basics/syntaxerror.py
+++ b/tests/basics/syntaxerror.py
@@ -86,7 +86,7 @@ def test_syntax(code):
 test_syntax("nonlocal a")
 test_syntax("await 1")
 
-# error on uPy, warning on CPy
+# error on MPy, warning on CPy
 #test_syntax("def f():\n a = 1\n global a")
 
 # default except must be last
@@ -98,7 +98,7 @@ def test_syntax(code):
 # non-keyword after keyword
 test_syntax("f(a=1, 2)")
 
-# doesn't error on uPy but should
+# doesn't error on MPy but should
 #test_syntax("f(1, i for i in i)")
 
 # all elements of dict/set must be pairs or singles
diff --git a/tests/basics/sys1.py b/tests/basics/sys1.py
index a609f54b636..31081e4236a 100644
--- a/tests/basics/sys1.py
+++ b/tests/basics/sys1.py
@@ -1,10 +1,6 @@
 # test sys module
 
-try:
-    import usys as sys
-except ImportError:
-    import sys
-
+import sys
 print(sys.__name__)
 print(type(sys.path))
 print(type(sys.argv))
@@ -27,3 +23,30 @@
 else:
     # Effectively skip subtests
     print(int)
+
+if hasattr(sys.implementation, '_build'):
+    print(type(sys.implementation._build))
+else:
+    # Effectively skip subtests
+    print(str)
+
+if hasattr(sys.implementation, '_thread'):
+    print(sys.implementation._thread in ("GIL", "unsafe"))
+else:
+    # Effectively skip subtests
+    print(True)
+
+try:
+    print(sys.intern('micropython') == 'micropython')
+    has_intern = True
+except AttributeError:
+    has_intern = False
+    print(True)
+
+if has_intern:
+    try:
+        print(sys.intern(0))
+    except TypeError:
+        print(True)
+else:
+    print(True)
diff --git a/tests/basics/sys_exit.py b/tests/basics/sys_exit.py
index 4d640ae60e4..4994af4161c 100644
--- a/tests/basics/sys_exit.py
+++ b/tests/basics/sys_exit.py
@@ -1,10 +1,6 @@
 # test sys module's exit function
 
-try:
-    import usys as sys
-except ImportError:
-    import sys
-
+import sys
 try:
     sys.exit
 except AttributeError:
diff --git a/tests/basics/sys_getsizeof.py b/tests/basics/sys_getsizeof.py
index 4dc919848ca..779e992d1f6 100644
--- a/tests/basics/sys_getsizeof.py
+++ b/tests/basics/sys_getsizeof.py
@@ -1,9 +1,6 @@
 # test sys.getsizeof() function
 
-try:
-    import usys as sys
-except ImportError:
-    import sys
+import sys
 try:
     sys.getsizeof
 except AttributeError:
@@ -19,7 +16,7 @@ class A:
 
 # Only test deque if we have it
 try:
-    from ucollections import deque
+    from collections import deque
     assert sys.getsizeof(deque((), 1)) > 0
 except ImportError:
     pass
diff --git a/tests/basics/sys_path.py b/tests/basics/sys_path.py
index 6456e240197..576bd66c94d 100644
--- a/tests/basics/sys_path.py
+++ b/tests/basics/sys_path.py
@@ -1,10 +1,6 @@
 # test sys.path
 
-try:
-    import usys as sys
-except ImportError:
-    import sys
-
+import sys
 # check that this script was executed from a file of the same name
 if "__file__" not in globals() or "sys_path.py" not in __file__:
     print("SKIP")
diff --git a/tests/basics/sys_stdio.py b/tests/basics/sys_stdio.py
new file mode 100644
index 00000000000..8d746b8b4b2
--- /dev/null
+++ b/tests/basics/sys_stdio.py
@@ -0,0 +1,21 @@
+# Test sys.std* objects.
+
+import sys
+
+try:
+    sys.stdout
+    sys.stdin
+    sys.stderr
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+# CPython is more verbose; no need to match exactly
+
+print('TextIOWrapper' in str(sys.stdout))
+print('TextIOWrapper' in str(sys.stderr))
+print('TextIOWrapper' in str(sys.stdin))
+
+print('TextIOWrapper' in str(type(sys.stdout)))
+print('TextIOWrapper' in str(type(sys.stderr)))
+print('TextIOWrapper' in str(type(sys.stdin)))
diff --git a/tests/basics/sys_stdio_buffer.py b/tests/basics/sys_stdio_buffer.py
new file mode 100644
index 00000000000..ae354ec7fbe
--- /dev/null
+++ b/tests/basics/sys_stdio_buffer.py
@@ -0,0 +1,21 @@
+# Test sys.std*.buffer objects.
+
+import sys
+
+try:
+    sys.stdout.buffer
+    sys.stdin.buffer
+    sys.stderr.buffer
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+# CPython is more verbose; no need to match exactly
+
+print('FileIO' in str(sys.stdout.buffer))
+print('FileIO' in str(sys.stderr.buffer))
+print('FileIO' in str(sys.stdin.buffer))
+
+print('FileIO' in str(type(sys.stdout.buffer)))
+print('FileIO' in str(type(sys.stderr.buffer)))
+print('FileIO' in str(type(sys.stdin.buffer)))
diff --git a/tests/basics/sys_stdio_buffer.py.exp b/tests/basics/sys_stdio_buffer.py.exp
new file mode 100644
index 00000000000..681c7ec9e84
--- /dev/null
+++ b/tests/basics/sys_stdio_buffer.py.exp
@@ -0,0 +1,6 @@
+True
+True
+True
+True
+True
+True
diff --git a/tests/basics/sys_tracebacklimit.py b/tests/basics/sys_tracebacklimit.py
index 1ee638967f1..6f575fde654 100644
--- a/tests/basics/sys_tracebacklimit.py
+++ b/tests/basics/sys_tracebacklimit.py
@@ -1,12 +1,8 @@
 # test sys.tracebacklimit
 
 try:
-    try:
-        import usys as sys
-        import uio as io
-    except ImportError:
-        import sys
-        import io
+    import sys
+    import io
 except ImportError:
     print("SKIP")
     raise SystemExit
@@ -34,7 +30,7 @@ def print_exc(e):
         if l.startswith("  File "):
             l = l.split('"')
             print(l[0], l[2])
-        # uPy and CPy tracebacks differ in that CPy prints a source line for
+        # MPy and CPy tracebacks differ in that CPy prints a source line for
         # each traceback entry.  In this case, we know that offending line
         # has 4-space indent, so filter it out.
         elif not l.startswith("    "):
diff --git a/tests/basics/sys_tracebacklimit.py.exp b/tests/basics/sys_tracebacklimit.py.exp
index 3647280584d..39c54523a05 100644
--- a/tests/basics/sys_tracebacklimit.py.exp
+++ b/tests/basics/sys_tracebacklimit.py.exp
@@ -1,35 +1,35 @@
 Traceback (most recent call last):
-  File  , line 62, in ftop
-  File  , line 57, in f3
-  File  , line 53, in f2
-  File  , line 49, in f1
-  File  , line 45, in f0
+  File  , line 58, in ftop
+  File  , line 53, in f3
+  File  , line 49, in f2
+  File  , line 45, in f1
+  File  , line 41, in f0
 ValueError: value
 
 limit 4
 Traceback (most recent call last):
-  File  , line 62, in ftop
-  File  , line 57, in f3
-  File  , line 53, in f2
-  File  , line 49, in f1
+  File  , line 58, in ftop
+  File  , line 53, in f3
+  File  , line 49, in f2
+  File  , line 45, in f1
 ValueError: value
 
 limit 3
 Traceback (most recent call last):
-  File  , line 62, in ftop
-  File  , line 57, in f3
-  File  , line 53, in f2
+  File  , line 58, in ftop
+  File  , line 53, in f3
+  File  , line 49, in f2
 ValueError: value
 
 limit 2
 Traceback (most recent call last):
-  File  , line 62, in ftop
-  File  , line 57, in f3
+  File  , line 58, in ftop
+  File  , line 53, in f3
 ValueError: value
 
 limit 1
 Traceback (most recent call last):
-  File  , line 62, in ftop
+  File  , line 58, in ftop
 ValueError: value
 
 limit 0
diff --git a/tests/basics/sys_tracebacklimit.py.native.exp b/tests/basics/sys_tracebacklimit.py.native.exp
new file mode 100644
index 00000000000..f9d30c05856
--- /dev/null
+++ b/tests/basics/sys_tracebacklimit.py.native.exp
@@ -0,0 +1,22 @@
+ValueError: value
+
+limit 4
+ValueError: value
+
+limit 3
+ValueError: value
+
+limit 2
+ValueError: value
+
+limit 1
+ValueError: value
+
+limit 0
+ValueError: value
+
+limit -1
+ValueError: value
+
+True
+False
diff --git a/tests/basics/try_finally_break.py.exp b/tests/basics/try_finally_break.py.exp
new file mode 100644
index 00000000000..d0b36985489
--- /dev/null
+++ b/tests/basics/try_finally_break.py.exp
@@ -0,0 +1,25 @@
+1
+2
+5
+a 1
+1
+iter 0
+1
+None
+1
+2
+None
+1
+2
+None
+1
+3
+4
+7
+1
+2
+4
+7
+1
+4
+7
diff --git a/tests/basics/try_finally_break2.py.exp b/tests/basics/try_finally_break2.py.exp
new file mode 100644
index 00000000000..bc5e931fdb4
--- /dev/null
+++ b/tests/basics/try_finally_break2.py.exp
@@ -0,0 +1,17 @@
+4 0 0 1
+4 0 0 2
+4 0 0 3
+4 0 0 4
+4 1 0 1
+4 1 0 2
+4 1 0 3
+4 1 0 4
+4 2 0 1
+4 2 0 2
+4 2 0 3
+4 2 0 4
+4 3 0 1
+4 3 0 2
+4 3 0 3
+4 3 0 4
+None
diff --git a/tests/basics/try_finally_return.py b/tests/basics/try_finally_return.py
index 31a507e8d07..21e01ea21bf 100644
--- a/tests/basics/try_finally_return.py
+++ b/tests/basics/try_finally_return.py
@@ -70,3 +70,13 @@ def f():
     finally:
         print('finally 1')
 print(f())
+
+# the finally block uses a lot of Python stack and then a local is accessed
+# (tests that the use of the stack doesn't clobber the local)
+def f(x):
+    try:
+        return x
+    finally:
+        print(2, 3, 4, 5, 6)
+        print(x)
+print(f(1))
diff --git a/tests/basics/try_finally_return2.py.exp b/tests/basics/try_finally_return2.py.exp
new file mode 100644
index 00000000000..b4c364f81d9
--- /dev/null
+++ b/tests/basics/try_finally_return2.py.exp
@@ -0,0 +1,19 @@
+finally
+0
+finally 1
+finally 2
+2
+finally 1
+finally 2
+1
+finally 1
+finally 2
+2
+finally
+0
+finally
+0
+finally
+0
+finally
+0
diff --git a/tests/basics/try_finally_return3.py.exp b/tests/basics/try_finally_return3.py.exp
new file mode 100644
index 00000000000..5fef648ea67
--- /dev/null
+++ b/tests/basics/try_finally_return3.py.exp
@@ -0,0 +1,23 @@
+1
+42
+1
+2
+42
+1
+2
+3
+42
+2
+1
+42
+2
+1
+42
+1
+3
+2
+42
+1
+3
+2
+42
diff --git a/tests/basics/try_finally_return4.py.exp b/tests/basics/try_finally_return4.py.exp
new file mode 100644
index 00000000000..f92572ff611
--- /dev/null
+++ b/tests/basics/try_finally_return4.py.exp
@@ -0,0 +1,38 @@
+1
+2
+3
+4
+5
+None
+1
+2
+3
+5
+42
+1
+2
+5
+43
+1
+2
+5
+43
+1
+2
+5
+caught
+1
+2
+5
+caught
+1
+2
+3
+4
+5
+None
+1
+2
+3
+5
+42
diff --git a/tests/basics/try_finally_return5.py.exp b/tests/basics/try_finally_return5.py.exp
new file mode 100644
index 00000000000..dfa403cfd47
--- /dev/null
+++ b/tests/basics/try_finally_return5.py.exp
@@ -0,0 +1,3 @@
+4 0
+return
+43
diff --git a/tests/basics/tuple1.py b/tests/basics/tuple1.py
index 72bb3f01bff..70ae071898e 100644
--- a/tests/basics/tuple1.py
+++ b/tests/basics/tuple1.py
@@ -17,7 +17,7 @@
 x += (10, 11, 12)
 print(x)
 
-# construction of tuple from large iterator (tests implementation detail of uPy)
+# construction of tuple from large iterator (tests implementation detail of MicroPython)
 print(tuple(range(20)))
 
 # unsupported unary operation
diff --git a/tests/basics/tuple_subclass.py b/tests/basics/tuple_subclass.py
new file mode 100644
index 00000000000..a8ee3ff1a4f
--- /dev/null
+++ b/tests/basics/tuple_subclass.py
@@ -0,0 +1,62 @@
+try:
+    from collections import namedtuple
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+class MyTuple(tuple):
+    pass
+
+
+N = namedtuple("N", ("a", "b"))
+
+
+class MyNamedTuple(N):
+    pass
+
+
+t = (1, 2)
+m = MyTuple((3, 4))
+n = N(5, 6)
+q = MyNamedTuple(7, 8)
+
+print(t + t)
+print(t + m)
+print(t + n)
+print(t + q)
+
+print(m + t)
+print(m + m)
+print(m + n)
+print(m + q)
+
+print(n + t)
+print(n + m)
+print(n + n)
+print(n + q)
+
+print(q + t)
+print(q + m)
+print(q + n)
+print(q + q)
+
+print(t < t)
+print(t < m)
+print(t < n)
+print(t < q)
+
+print(m < t)
+print(m < m)
+print(m < n)
+print(m < q)
+
+print(n < t)
+print(n < m)
+print(n < n)
+print(n < q)
+
+print(q < t)
+print(q < m)
+print(q < n)
+print(q < q)
diff --git a/tests/basics/weakref_callback_exception.py b/tests/basics/weakref_callback_exception.py
new file mode 100644
index 00000000000..df8e5129803
--- /dev/null
+++ b/tests/basics/weakref_callback_exception.py
@@ -0,0 +1,42 @@
+# Test weakref ref/finalize raising an exception within the callback.
+#
+# This test has different output to CPython due to the way that MicroPython
+# prints the exception.
+
+try:
+    import weakref
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+import gc
+
+
+class A:
+    def __str__(self):
+        return ""
+
+
+def callback(*args):
+    raise ValueError("weakref callback", args)
+
+
+def test():
+    print("test ref with exception in the callback")
+    a = A()
+    r = weakref.ref(a, callback)
+    a = None
+    clean_the_stack = [0, 0, 0, 0]
+    gc.collect()
+    print("collect done")
+
+    print("test finalize with exception in the callback")
+    a = A()
+    weakref.finalize(a, callback)
+    a = None
+    clean_the_stack = [0, 0, 0, 0]
+    gc.collect()
+    print("collect done")
+
+
+test()
diff --git a/tests/basics/weakref_callback_exception.py.exp b/tests/basics/weakref_callback_exception.py.exp
new file mode 100644
index 00000000000..c2f7796310c
--- /dev/null
+++ b/tests/basics/weakref_callback_exception.py.exp
@@ -0,0 +1,12 @@
+test ref with exception in the callback
+Unhandled exception in weakref callback:
+Traceback (most recent call last):
+  File "\.\+weakref_callback_exception.py", line 21, in callback
+ValueError: ('weakref callback', (,))
+collect done
+test finalize with exception in the callback
+Unhandled exception in weakref callback:
+Traceback (most recent call last):
+  File "\.\+weakref_callback_exception.py", line 21, in callback
+ValueError: ('weakref callback', ())
+collect done
diff --git a/tests/basics/weakref_callback_exception.py.native.exp b/tests/basics/weakref_callback_exception.py.native.exp
new file mode 100644
index 00000000000..a06a35c3b7e
--- /dev/null
+++ b/tests/basics/weakref_callback_exception.py.native.exp
@@ -0,0 +1,8 @@
+test ref with exception in the callback
+Unhandled exception in weakref callback:
+ValueError: ('weakref callback', (,))
+collect done
+test finalize with exception in the callback
+Unhandled exception in weakref callback:
+ValueError: ('weakref callback', ())
+collect done
diff --git a/tests/basics/weakref_finalize_basic.py b/tests/basics/weakref_finalize_basic.py
new file mode 100644
index 00000000000..792cffacb13
--- /dev/null
+++ b/tests/basics/weakref_finalize_basic.py
@@ -0,0 +1,58 @@
+# Test weakref.finalize() functionality that doesn't require gc.collect().
+
+try:
+    import weakref
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Cannot reference non-heap objects.
+for value in (None, False, True, Ellipsis, 0, "", ()):
+    try:
+        weakref.finalize(value, lambda: None)
+    except TypeError:
+        print(value, "TypeError")
+
+
+# Convert (obj, func, args, kwargs) so CPython and MicroPython have a chance to match.
+def convert_4_tuple(values):
+    if values is None:
+        return None
+    return (type(values[0]).__name__, type(values[1]), values[2], values[3])
+
+
+class A:
+    def __str__(self):
+        return ""
+
+
+print("test alive, peek, detach")
+a = A()
+f = weakref.finalize(a, lambda: None, 1, 2, kwarg=3)
+print("alive", f.alive)
+print("peek", convert_4_tuple(f.peek()))
+print("detach", convert_4_tuple(f.detach()))
+print("alive", f.alive)
+print("peek", convert_4_tuple(f.peek()))
+print("detach", convert_4_tuple(f.detach()))
+print("call", f())
+a = None
+
+print("test alive, peek, call")
+a = A()
+f = weakref.finalize(a, lambda *args, **kwargs: (args, kwargs), 1, 2, kwarg=3)
+print("alive", f.alive)
+print("peek", convert_4_tuple(f.peek()))
+print("call", f())
+print("alive", f.alive)
+print("peek", convert_4_tuple(f.peek()))
+print("call", f())
+print("detach", convert_4_tuple(f.detach()))
+
+print("test call which raises exception")
+a = A()
+f = weakref.finalize(a, lambda: 1 / 0)
+try:
+    f()
+except ZeroDivisionError as er:
+    print("call ZeroDivisionError")
diff --git a/tests/basics/weakref_finalize_collect.py b/tests/basics/weakref_finalize_collect.py
new file mode 100644
index 00000000000..f6e7c14843e
--- /dev/null
+++ b/tests/basics/weakref_finalize_collect.py
@@ -0,0 +1,76 @@
+# Test weakref.finalize() functionality requiring gc.collect().
+# Should be kept in sync with tests/ports/webassembly/weakref_finalize_collect.py.
+
+try:
+    import weakref
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# gc module must be available if weakref is.
+import gc
+
+
+class A:
+    def __str__(self):
+        return ""
+
+
+def callback(*args, **kwargs):
+    print("callback({}, {})".format(args, kwargs))
+    return 42
+
+
+def test():
+    print("test basic use of finalize() with a simple callback")
+    a = A()
+    f = weakref.finalize(a, callback)
+    a = None
+    clean_the_stack = [0, 0, 0, 0]
+    gc.collect()
+    print("alive", f.alive)
+    print("peek", f.peek())
+    print("detach", f.detach())
+    print("call", f())
+
+    print("test that a callback is passed the correct values")
+    a = A()
+    f = weakref.finalize(a, callback, 1, 2, kwarg=3)
+    a = None
+    clean_the_stack = [0, 0, 0, 0]
+    gc.collect()
+    print("alive", f.alive)
+    print("peek", f.peek())
+    print("detach", f.detach())
+    print("call", f())
+
+    print("test that calling the finalizer cancels the finalizer")
+    a = A()
+    f = weakref.finalize(a, callback)
+    print(f())
+    print(a)
+    a = None
+    clean_the_stack = [0, 0, 0, 0]
+    gc.collect()
+
+    print("test that calling detach cancels the finalizer")
+    a = A()
+    f = weakref.finalize(a, callback)
+    print(len(f.detach()))
+    print(a)
+    a = None
+    clean_the_stack = [0, 0, 0, 0]
+    gc.collect()
+
+    print("test that finalize does not get collected before its ref does")
+    a = A()
+    weakref.finalize(a, callback)
+    clean_the_stack = [0, 0, 0, 0]
+    gc.collect()
+    print("free a")
+    a = None
+    clean_the_stack = [0, 0, 0, 0]
+    gc.collect()
+
+
+test()
diff --git a/tests/basics/weakref_multiple_refs.py b/tests/basics/weakref_multiple_refs.py
new file mode 100644
index 00000000000..400e03a17c7
--- /dev/null
+++ b/tests/basics/weakref_multiple_refs.py
@@ -0,0 +1,36 @@
+# Test weakref when multiple weak references are active.
+#
+# This test has different output to CPython due to the order that MicroPython
+# executes weak reference callbacks.
+
+try:
+    import weakref
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# gc module must be available if weakref is.
+import gc
+
+
+class A:
+    def __str__(self):
+        return ""
+
+
+def test():
+    global r1, r2  # needed for webassembly port to retain references to them
+
+    print("test having multiple ref and finalize objects referencing the same thing")
+    a = A()
+    r1 = weakref.ref(a, lambda r: print("ref1", r()))
+    f1 = weakref.finalize(a, lambda: print("finalize1"))
+    r2 = weakref.ref(a, lambda r: print("ref2", r()))
+    f2 = weakref.finalize(a, lambda: print("finalize2"))
+    print(r1(), f1.alive, r2(), f2.alive)
+    a = None
+    clean_the_stack = [0, 0, 0, 0]
+    gc.collect()
+
+
+test()
diff --git a/tests/basics/weakref_multiple_refs.py.exp b/tests/basics/weakref_multiple_refs.py.exp
new file mode 100644
index 00000000000..1f2d366f776
--- /dev/null
+++ b/tests/basics/weakref_multiple_refs.py.exp
@@ -0,0 +1,6 @@
+test having multiple ref and finalize objects referencing the same thing
+ True  True
+finalize2
+finalize1
+ref2 None
+ref1 None
diff --git a/tests/basics/weakref_ref_basic.py b/tests/basics/weakref_ref_basic.py
new file mode 100644
index 00000000000..058045f6c33
--- /dev/null
+++ b/tests/basics/weakref_ref_basic.py
@@ -0,0 +1,14 @@
+# Test weakref.ref() functionality that doesn't require gc.collect().
+
+try:
+    import weakref
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Cannot reference non-heap objects.
+for value in (None, False, True, Ellipsis, 0, "", ()):
+    try:
+        weakref.ref(value)
+    except TypeError:
+        print(value, "TypeError")
diff --git a/tests/basics/weakref_ref_collect.py b/tests/basics/weakref_ref_collect.py
new file mode 100644
index 00000000000..0e8db977d77
--- /dev/null
+++ b/tests/basics/weakref_ref_collect.py
@@ -0,0 +1,69 @@
+# Test weakref.ref() functionality requiring gc.collect().
+# Should be kept in sync with tests/ports/webassembly/weakref_ref_collect.py.
+
+try:
+    import weakref
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# gc module must be available if weakref is.
+import gc
+
+# Cannot reference non-heap objects.
+for value in (None, False, True, Ellipsis, 0, "", ()):
+    try:
+        weakref.ref(value)
+    except TypeError:
+        print(value, "TypeError")
+
+
+class A:
+    def __str__(self):
+        return ""
+
+
+def callback(r):
+    print("callback", r())
+
+
+def test():
+    print("test basic use of ref() with only one argument")
+    a = A()
+    r = weakref.ref(a)
+    print(r())
+    a = None
+    clean_the_stack = [0, 0, 0, 0]
+    gc.collect()
+    print(r())
+
+    print("test use of ref() with a callback")
+    a = A()
+    r = weakref.ref(a, callback)
+    print(r())
+    a = None
+    clean_the_stack = [0, 0, 0, 0]
+    gc.collect()
+    print(r())
+
+    print("test when weakref gets collected before the object it refs")
+    a = A()
+    r = weakref.ref(a, callback)
+    print(r())
+    r = None
+    clean_the_stack = [0, 0, 0, 0]
+    gc.collect()
+    a = None
+
+    print("test a double reference")
+    a = A()
+    r1 = weakref.ref(a, callback)
+    r2 = weakref.ref(a, callback)
+    print(r1(), r2())
+    a = None
+    clean_the_stack = [0, 0, 0, 0]
+    gc.collect()
+    print(r1(), r2())
+
+
+test()
diff --git a/tests/cmdline/cmd_compile_only.py b/tests/cmdline/cmd_compile_only.py
new file mode 100644
index 00000000000..89964c1b5bd
--- /dev/null
+++ b/tests/cmdline/cmd_compile_only.py
@@ -0,0 +1,13 @@
+# cmdline: -X compile-only
+# test compile-only functionality
+print("This should not be printed")
+x = 1 + 2
+
+
+def hello():
+    return "world"
+
+
+class TestClass:
+    def __init__(self):
+        self.value = 42
diff --git a/tests/cmdline/cmd_compile_only.py.exp b/tests/cmdline/cmd_compile_only.py.exp
new file mode 100644
index 00000000000..8b137891791
--- /dev/null
+++ b/tests/cmdline/cmd_compile_only.py.exp
@@ -0,0 +1 @@
+
diff --git a/tests/cmdline/cmd_compile_only_error.py b/tests/cmdline/cmd_compile_only_error.py
new file mode 100644
index 00000000000..326937a5c07
--- /dev/null
+++ b/tests/cmdline/cmd_compile_only_error.py
@@ -0,0 +1,6 @@
+# cmdline: -X compile-only
+# test compile-only with syntax error
+print("This should not be printed")
+def broken_syntax(
+    # Missing closing parenthesis
+    return "error"
diff --git a/tests/cmdline/cmd_compile_only_error.py.exp b/tests/cmdline/cmd_compile_only_error.py.exp
new file mode 100644
index 00000000000..3911f71d624
--- /dev/null
+++ b/tests/cmdline/cmd_compile_only_error.py.exp
@@ -0,0 +1 @@
+CRASH
\ No newline at end of file
diff --git a/tests/cmdline/cmd_file_variable.py b/tests/cmdline/cmd_file_variable.py
new file mode 100644
index 00000000000..6cac6744d90
--- /dev/null
+++ b/tests/cmdline/cmd_file_variable.py
@@ -0,0 +1,5 @@
+# Test that __file__ is set correctly for script execution
+try:
+    print("__file__ =", __file__)
+except NameError:
+    print("__file__ not defined")
diff --git a/tests/cmdline/cmd_file_variable.py.exp b/tests/cmdline/cmd_file_variable.py.exp
new file mode 100644
index 00000000000..420f789ddf3
--- /dev/null
+++ b/tests/cmdline/cmd_file_variable.py.exp
@@ -0,0 +1 @@
+__file__ = \.\*cmdline/cmd_file_variable.py
diff --git a/tests/cmdline/cmd_module_atexit.py b/tests/cmdline/cmd_module_atexit.py
new file mode 100644
index 00000000000..100bc112777
--- /dev/null
+++ b/tests/cmdline/cmd_module_atexit.py
@@ -0,0 +1,16 @@
+# cmdline: -m cmdline.cmd_module_atexit
+#
+# Test running as a module and using sys.atexit.
+
+import sys
+
+if not hasattr(sys, "atexit"):
+    print("SKIP")
+    raise SystemExit
+
+# Verify we ran as a module.
+print(sys.argv)
+
+sys.atexit(lambda: print("done"))
+
+print("start")
diff --git a/tests/cmdline/cmd_module_atexit.py.exp b/tests/cmdline/cmd_module_atexit.py.exp
new file mode 100644
index 00000000000..334bf53bac9
--- /dev/null
+++ b/tests/cmdline/cmd_module_atexit.py.exp
@@ -0,0 +1,3 @@
+['cmdline.cmd_module_atexit', '\.\*cmdline/cmd_module_atexit.py']
+start
+done
diff --git a/tests/cmdline/cmd_module_atexit_exc.py b/tests/cmdline/cmd_module_atexit_exc.py
new file mode 100644
index 00000000000..88940a7741f
--- /dev/null
+++ b/tests/cmdline/cmd_module_atexit_exc.py
@@ -0,0 +1,19 @@
+# cmdline: -m cmdline.cmd_module_atexit_exc
+#
+# Test running as a module and using sys.atexit, with script completion via sys.exit.
+
+import sys
+
+if not hasattr(sys, "atexit"):
+    print("SKIP")
+    raise SystemExit
+
+# Verify we ran as a module.
+print(sys.argv)
+
+sys.atexit(lambda: print("done"))
+
+print("start")
+
+# This will raise SystemExit to finish the script, and atexit should still be run.
+sys.exit(0)
diff --git a/tests/cmdline/cmd_module_atexit_exc.py.exp b/tests/cmdline/cmd_module_atexit_exc.py.exp
new file mode 100644
index 00000000000..210748afdfe
--- /dev/null
+++ b/tests/cmdline/cmd_module_atexit_exc.py.exp
@@ -0,0 +1,3 @@
+['cmdline.cmd_module_atexit_exc', '\.\*cmdline/cmd_module_atexit_exc.py']
+start
+done
diff --git a/tests/cmdline/cmd_parsetree.py.exp b/tests/cmdline/cmd_parsetree.py.exp
index 3049267c0b9..672c212a965 100644
--- a/tests/cmdline/cmd_parsetree.py.exp
+++ b/tests/cmdline/cmd_parsetree.py.exp
@@ -1,6 +1,6 @@
 ----------------
-[   4] \(rule\|file_input_2\)(1) (n=10)
-         tok(6)
+[   1] file_input_2(1) (n=10)
+         tok(5)
 [   4]   \(rule\|for_stmt\)(22) (n=4)
            id(i)
 [   4]     \(rule\|atom_paren\)(45) (n=1)
@@ -9,7 +9,7 @@
            NULL
 [   6]   \(rule\|expr_stmt\)(5) (n=2)
            id(a)
-           tok(16)
+           tok(15)
 [   7]   \(rule\|expr_stmt\)(5) (n=2)
            id(b)
            str(str)
diff --git a/tests/cmdline/cmd_showbc.py.exp b/tests/cmdline/cmd_showbc.py.exp
index db06de92371..b839de6d1ed 100644
--- a/tests/cmdline/cmd_showbc.py.exp
+++ b/tests/cmdline/cmd_showbc.py.exp
@@ -1,4 +1,4 @@
-File cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 63 bytes)
+File \.\*cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 63 bytes)
 Raw bytecode (code_info_size=18, bytecode_size=45):
  10 20 01 60 20 84 7d 64 60 88 07 64 60 69 20 62
  64 20 32 00 16 05 32 01 16 05 81 2a 01 53 33 02
@@ -47,7 +47,7 @@ arg names:
 42 IMPORT_STAR
 43 LOAD_CONST_NONE
 44 RETURN_VALUE
-File cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 46\[68\] bytes)
+File \.\*cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 46\[68\] bytes)
 Raw bytecode (code_info_size=8\[46\], bytecode_size=382):
  a8 12 9\[bf\] 03 05 60 60 26 22 24 64 22 24 25 25 24
  26 23 63 22 22 25 23 23 2f 6c 25 65 25 25 69 68
@@ -185,7 +185,7 @@ arg names:
 58 UNARY_OP 1 __neg__
 59 STORE_FAST 9
 60 LOAD_FAST 0
-61 UNARY_OP 3 
+61 UNARY_OP 3 \$
 62 STORE_FAST 10
 63 LOAD_FAST 0
 64 LOAD_DEREF 14
@@ -206,7 +206,7 @@ arg names:
 84 LOAD_DEREF 14
 86 LOAD_FAST 1
 87 BINARY_OP 2 __eq__
-88 UNARY_OP 3 
+88 UNARY_OP 3 \$
 89 STORE_FAST 10
 90 LOAD_DEREF 14
 92 LOAD_ATTR c
@@ -411,7 +411,7 @@ arg names:
 379 RETURN_VALUE
 380 LOAD_CONST_NONE
 381 RETURN_VALUE
-File cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 59 bytes)
+File \.\*cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 59 bytes)
 Raw bytecode (code_info_size=8, bytecode_size=51):
  a8 10 0a 05 80 82 34 38 81 57 c0 57 c1 57 c2 57
  c3 57 c4 57 c5 57 c6 57 c7 57 c8 c9 82 57 ca 57
@@ -470,7 +470,7 @@ arg names:
 48 POP_TOP
 49 LOAD_CONST_NONE
 50 RETURN_VALUE
-File cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 20 bytes)
+File \.\*cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 20 bytes)
 Raw bytecode (code_info_size=9, bytecode_size=11):
  a1 01 0b 05 06 80 88 40 00 82 2a 01 53 b0 21 00
  01 c1 51 63
@@ -489,7 +489,7 @@ arg names: a
 08 STORE_FAST 1
 09 LOAD_CONST_NONE
 10 RETURN_VALUE
-File cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 21 bytes)
+File \.\*cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 21 bytes)
 Raw bytecode (code_info_size=8, bytecode_size=13):
  88 40 0a 05 80 8f 23 23 51 67 59 81 67 59 81 5e
  51 68 59 51 63
@@ -513,7 +513,7 @@ arg names:
 10 POP_TOP
 11 LOAD_CONST_NONE
 12 RETURN_VALUE
-File cmdline/cmd_showbc.py, code block 'Class' (descriptor: \.\+, bytecode @\.\+ 1\[56\] bytes)
+File \.\*cmdline/cmd_showbc.py, code block 'Class' (descriptor: \.\+, bytecode @\.\+ 1\[56\] bytes)
 Raw bytecode (code_info_size=\[56\], bytecode_size=10):
  00 \.\+ 11 0f 16 10 10 02 16 11 51 63
 arg names:
@@ -528,7 +528,7 @@ arg names:
 06 STORE_NAME __qualname__
 08 LOAD_CONST_NONE
 09 RETURN_VALUE
-File cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 18 bytes)
+File \.\*cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 18 bytes)
 Raw bytecode (code_info_size=6, bytecode_size=12):
  19 08 05 12 80 9c 12 13 12 14 b0 15 05 36 00 59
  51 63
@@ -545,7 +545,7 @@ arg names: self
 09 POP_TOP
 10 LOAD_CONST_NONE
 11 RETURN_VALUE
-File cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 28 bytes)
+File \.\*cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 28 bytes)
 Raw bytecode (code_info_size=9, bytecode_size=19):
  c3 40 0c 09 03 03 03 80 3b 53 b2 53 53 4b 0b c3
  25 01 44 39 25 00 67 59 42 33 51 63
@@ -568,7 +568,7 @@ arg names: * * *
 15 JUMP 4
 17 LOAD_CONST_NONE
 18 RETURN_VALUE
-File cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 26 bytes)
+File \.\*cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 26 bytes)
 Raw bytecode (code_info_size=8, bytecode_size=18):
  4b 0c 0a 03 03 03 80 3c 2b 00 b2 5f 4b 0b c3 25
  01 44 39 25 00 2f 14 42 33 63
@@ -588,7 +588,7 @@ arg names: * * *
 13 STORE_COMP 20
 15 JUMP 4
 17 RETURN_VALUE
-File cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 28 bytes)
+File \.\*cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 28 bytes)
 Raw bytecode (code_info_size=8, bytecode_size=20):
  53 0c 0b 03 03 03 80 3d 2c 00 b2 5f 4b 0d c3 25
  01 44 39 25 00 25 00 2f 19 42 31 63
@@ -609,7 +609,7 @@ arg names: * * *
 15 STORE_COMP 25
 17 JUMP 4
 19 RETURN_VALUE
-File cmdline/cmd_showbc.py, code block 'closure' (descriptor: \.\+, bytecode @\.\+ 20 bytes)
+File \.\*cmdline/cmd_showbc.py, code block 'closure' (descriptor: \.\+, bytecode @\.\+ 20 bytes)
 Raw bytecode (code_info_size=8, bytecode_size=12):
  19 0c 0c 03 80 6f 25 23 25 00 81 f2 c1 81 27 00
  29 00 51 63
@@ -629,7 +629,7 @@ arg names: *
 08 DELETE_DEREF 0
 10 LOAD_CONST_NONE
 11 RETURN_VALUE
-File cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 13 bytes)
+File \.\*cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 13 bytes)
 Raw bytecode (code_info_size=8, bytecode_size=5):
  9a 01 0a 05 03 08 80 8b b1 25 00 f2 63
 arg names: * b
diff --git a/tests/cmdline/cmd_showbc_const.py.exp b/tests/cmdline/cmd_showbc_const.py.exp
index 6cdc3e9c963..6faeb941d7d 100644
--- a/tests/cmdline/cmd_showbc_const.py.exp
+++ b/tests/cmdline/cmd_showbc_const.py.exp
@@ -1,4 +1,4 @@
-File cmdline/cmd_showbc_const.py, code block '' (descriptor: \.\+, bytecode @\.\+ 198 bytes)
+File \.\*cmdline/cmd_showbc_const.py, code block '' (descriptor: \.\+, bytecode @\.\+ 198 bytes)
 Raw bytecode (code_info_size=40, bytecode_size=158):
  2c 4c 01 60 2c 46 22 65 27 4a 83 0c 20 27 40 20
  27 20 27 40 60 20 27 24 40 60 40 24 27 47 24 27
@@ -76,7 +76,7 @@ arg names:
 34 RAISE_OBJ
 35 DUP_TOP
 36 LOAD_NAME AttributeError
-38 BINARY_OP 8 
+38 BINARY_OP 8 \$
 39 POP_JUMP_IF_FALSE 44
 41 POP_TOP
 42 POP_EXCEPT_JUMP 45
diff --git a/tests/cmdline/cmd_showbc_opt.py.exp b/tests/cmdline/cmd_showbc_opt.py.exp
index 9e4e4fae10c..c3047275a9c 100644
--- a/tests/cmdline/cmd_showbc_opt.py.exp
+++ b/tests/cmdline/cmd_showbc_opt.py.exp
@@ -1,4 +1,4 @@
-File cmdline/cmd_showbc_opt.py, code block '' (descriptor: \.\+, bytecode @\.\+ 35 bytes)
+File \.\*cmdline/cmd_showbc_opt.py, code block '' (descriptor: \.\+, bytecode @\.\+ 35 bytes)
 Raw bytecode (code_info_size=13, bytecode_size=22):
  00 16 01 60 20 64 40 84 07 64 40 84 07 32 00 16
  02 32 01 16 03 32 02 16 04 32 03 16 05 32 04 16
@@ -27,7 +27,7 @@ arg names:
 18 STORE_NAME f4
 20 LOAD_CONST_NONE
 21 RETURN_VALUE
-File cmdline/cmd_showbc_opt.py, code block 'f0' (descriptor: \.\+, bytecode @\.\+ 8 bytes)
+File \.\*cmdline/cmd_showbc_opt.py, code block 'f0' (descriptor: \.\+, bytecode @\.\+ 8 bytes)
 Raw bytecode (code_info_size=6, bytecode_size=2):
  08 08 02 60 40 22 80 63
 arg names:
@@ -39,7 +39,7 @@ arg names:
   bc=2 line=7
 00 LOAD_CONST_SMALL_INT 0
 01 RETURN_VALUE
-File cmdline/cmd_showbc_opt.py, code block 'f1' (descriptor: \.\+, bytecode @\.\+ 22 bytes)
+File \.\*cmdline/cmd_showbc_opt.py, code block 'f1' (descriptor: \.\+, bytecode @\.\+ 22 bytes)
 Raw bytecode (code_info_size=9, bytecode_size=13):
  11 0e 03 08 80 0a 23 22 20 b0 44 42 51 63 12 07
  82 34 01 59 51 63
@@ -61,7 +61,7 @@ arg names: x
 10 POP_TOP
 11 LOAD_CONST_NONE
 12 RETURN_VALUE
-File cmdline/cmd_showbc_opt.py, code block 'f2' (descriptor: \.\+, bytecode @\.\+ 10 bytes)
+File \.\*cmdline/cmd_showbc_opt.py, code block 'f2' (descriptor: \.\+, bytecode @\.\+ 10 bytes)
 Raw bytecode (code_info_size=7, bytecode_size=3):
  11 0a 04 08 80 11 23 12 09 65
 arg names: x
@@ -72,7 +72,7 @@ arg names: x
   bc=3 line=19
 00 LOAD_GLOBAL Exception
 02 RAISE_OBJ
-File cmdline/cmd_showbc_opt.py, code block 'f3' (descriptor: \.\+, bytecode @\.\+ 24 bytes)
+File \.\*cmdline/cmd_showbc_opt.py, code block 'f3' (descriptor: \.\+, bytecode @\.\+ 24 bytes)
 Raw bytecode (code_info_size=9, bytecode_size=15):
  11 0e 05 08 80 16 22 22 23 42 42 42 43 b0 43 3b
  12 07 82 34 01 59 51 63
@@ -94,7 +94,7 @@ arg names: x
 12 POP_TOP
 13 LOAD_CONST_NONE
 14 RETURN_VALUE
-File cmdline/cmd_showbc_opt.py, code block 'f4' (descriptor: \.\+, bytecode @\.\+ 24 bytes)
+File \.\*cmdline/cmd_showbc_opt.py, code block 'f4' (descriptor: \.\+, bytecode @\.\+ 24 bytes)
 Raw bytecode (code_info_size=9, bytecode_size=15):
  11 0e 06 08 80 1d 22 22 23 42 42 42 40 b0 43 3b
  12 07 82 34 01 59 51 63
diff --git a/tests/cmdline/cmd_sys_exit_0.py b/tests/cmdline/cmd_sys_exit_0.py
new file mode 100644
index 00000000000..1294b739e8f
--- /dev/null
+++ b/tests/cmdline/cmd_sys_exit_0.py
@@ -0,0 +1,5 @@
+# cmdline:
+# test sys.exit(0) - success exit code
+import sys
+
+sys.exit(0)
diff --git a/tests/cmdline/cmd_sys_exit_0.py.exp b/tests/cmdline/cmd_sys_exit_0.py.exp
new file mode 100644
index 00000000000..8b137891791
--- /dev/null
+++ b/tests/cmdline/cmd_sys_exit_0.py.exp
@@ -0,0 +1 @@
+
diff --git a/tests/cmdline/cmd_sys_exit_error.py b/tests/cmdline/cmd_sys_exit_error.py
new file mode 100644
index 00000000000..ecac15e94f1
--- /dev/null
+++ b/tests/cmdline/cmd_sys_exit_error.py
@@ -0,0 +1,5 @@
+# cmdline:
+# test sys.exit() functionality and exit codes
+import sys
+
+sys.exit(123)
diff --git a/tests/cmdline/cmd_sys_exit_error.py.exp b/tests/cmdline/cmd_sys_exit_error.py.exp
new file mode 100644
index 00000000000..3911f71d624
--- /dev/null
+++ b/tests/cmdline/cmd_sys_exit_error.py.exp
@@ -0,0 +1 @@
+CRASH
\ No newline at end of file
diff --git a/tests/cmdline/cmd_sys_exit_none.py b/tests/cmdline/cmd_sys_exit_none.py
new file mode 100644
index 00000000000..66e19666589
--- /dev/null
+++ b/tests/cmdline/cmd_sys_exit_none.py
@@ -0,0 +1,5 @@
+# cmdline:
+# test sys.exit(None) - should exit with code 0
+import sys
+
+sys.exit(None)
diff --git a/tests/cmdline/cmd_sys_exit_none.py.exp b/tests/cmdline/cmd_sys_exit_none.py.exp
new file mode 100644
index 00000000000..8b137891791
--- /dev/null
+++ b/tests/cmdline/cmd_sys_exit_none.py.exp
@@ -0,0 +1 @@
+
diff --git a/tests/cmdline/cmd_verbose.py.exp b/tests/cmdline/cmd_verbose.py.exp
index ae833dbec8d..1982f313cb7 100644
--- a/tests/cmdline/cmd_verbose.py.exp
+++ b/tests/cmdline/cmd_verbose.py.exp
@@ -1,4 +1,4 @@
-File cmdline/cmd_verbose.py, code block '' (descriptor: \.\+, bytecode @\.\+ 12 bytes)
+File \.\*cmdline/cmd_verbose.py, code block '' (descriptor: \.\+, bytecode @\.\+ 12 bytes)
 Raw bytecode (code_info_size=4, bytecode_size=8):
  08 04 01 40 11 02 81 34 01 59 51 63
 arg names:
diff --git a/tests/cmdline/repl_autocomplete.py.exp b/tests/cmdline/repl_autocomplete.py.exp
index 75002985e3c..56b754a31f4 100644
--- a/tests/cmdline/repl_autocomplete.py.exp
+++ b/tests/cmdline/repl_autocomplete.py.exp
@@ -1,5 +1,5 @@
 MicroPython \.\+ version
-Use \.\+
+Type "help()" for more information.
 >>> # tests for autocompletion
 >>> import sys
 >>> not_exist.
@@ -11,4 +11,4 @@ Use \.\+
 >>> i.lower('ABC')
 'abc'
 >>> None.
->>> 
+>>> \$
diff --git a/tests/cmdline/repl_autocomplete_underscore.py b/tests/cmdline/repl_autocomplete_underscore.py
new file mode 100644
index 00000000000..a0ad4aadf0f
--- /dev/null
+++ b/tests/cmdline/repl_autocomplete_underscore.py
@@ -0,0 +1,32 @@
+# Test REPL autocompletion filtering of underscore attributes
+
+# Start paste mode
+{\x05}
+class TestClass:
+    def __init__(self):
+        self.public_attr = 1
+        self._private_attr = 2
+        self.__very_private = 3
+
+    def public_method(self):
+        pass
+
+    def _private_method(self):
+        pass
+
+    @property
+    def public_property(self):
+        return 42
+
+    @property
+    def _private_property(self):
+        return 99
+{\x04}
+# Paste executed
+
+# Create an instance
+obj = TestClass()
+
+# Test tab completion on the instance
+# The tab character after `obj.` and 'a' below triggers the completions
+obj.{\x09}{\x09}a{\x09}
diff --git a/tests/cmdline/repl_autocomplete_underscore.py.exp b/tests/cmdline/repl_autocomplete_underscore.py.exp
new file mode 100644
index 00000000000..54882267d9b
--- /dev/null
+++ b/tests/cmdline/repl_autocomplete_underscore.py.exp
@@ -0,0 +1,41 @@
+MicroPython \.\+ version
+Type "help()" for more information.
+>>> # Test REPL autocompletion filtering of underscore attributes
+>>> \$
+>>> # Start paste mode
+>>> \$
+paste mode; Ctrl-C to cancel, Ctrl-D to finish
+=== \$
+=== class TestClass:
+===     def __init__(self):
+===         self.public_attr = 1
+===         self._private_attr = 2
+===         self.__very_private = 3
+=== \$
+===     def public_method(self):
+===         pass
+=== \$
+===     def _private_method(self):
+===         pass
+=== \$
+===     @property
+===     def public_property(self):
+===         return 42
+=== \$
+===     @property
+===     def _private_property(self):
+===         return 99
+=== \$
+>>> \$
+>>> # Paste executed
+>>> \$
+>>> # Create an instance
+>>> obj = TestClass()
+>>> \$
+>>> # Test tab completion on the instance
+>>> # The tab character after `obj.` and 'a' below triggers the completions
+>>> obj.public_
+public_attr     public_method   public_property
+>>> obj.public_attr
+1
+>>> \$
diff --git a/tests/cmdline/repl_autoindent.py.exp b/tests/cmdline/repl_autoindent.py.exp
index 9127a7d31d9..616ebe89989 100644
--- a/tests/cmdline/repl_autoindent.py.exp
+++ b/tests/cmdline/repl_autoindent.py.exp
@@ -1,22 +1,22 @@
 MicroPython \.\+ version
-Use \.\+
+Type "help()" for more information.
 >>> # tests for autoindent
 >>> if 1:
 ...     print(1)
-...     
-...     
-... 
+...     \$
+...     \$
+... \$
 1
 >>> if 0:
 ...      print(2)
 ... else:
 ...   print(3)
-... 
+... \$
 3
 >>> if 0:
 ...     print(4)
 ... else:
 ...     print(5)
-... 
+... \$
 5
->>> 
+>>> \$
diff --git a/tests/cmdline/repl_basic.py.exp b/tests/cmdline/repl_basic.py.exp
index 2b390ea98bb..ea013f641a5 100644
--- a/tests/cmdline/repl_basic.py.exp
+++ b/tests/cmdline/repl_basic.py.exp
@@ -1,5 +1,5 @@
 MicroPython \.\+ version
-Use \.\+
+Type "help()" for more information.
 >>> # basic REPL tests
 >>> print(1)
 1
@@ -7,4 +7,4 @@ Use \.\+
 1
 >>> 2
 2
->>> 
+>>> \$
diff --git a/tests/cmdline/repl_cont.py.exp b/tests/cmdline/repl_cont.py.exp
index 834c18a4d36..1083eedc73e 100644
--- a/tests/cmdline/repl_cont.py.exp
+++ b/tests/cmdline/repl_cont.py.exp
@@ -1,5 +1,5 @@
 MicroPython \.\+ version
-Use \.\+
+Type "help()" for more information.
 >>> # check REPL allows to continue input
 >>> 1 \\\\
 ... + 2
@@ -54,4 +54,4 @@ two
 >>> if1 = 2
 >>> print(if1)
 2
->>> 
+>>> \$
diff --git a/tests/cmdline/repl_ctrl_c_interrupt_execution.py b/tests/cmdline/repl_ctrl_c_interrupt_execution.py
new file mode 100644
index 00000000000..b125e2c16b4
--- /dev/null
+++ b/tests/cmdline/repl_ctrl_c_interrupt_execution.py
@@ -0,0 +1,8 @@
+# sigint: deliver via controlling terminal
+# Test that Ctrl-C (SIGINT) interrupts blocking code execution.
+# MicroPython restores original terminal mode (ISIG on) during
+# execution, so the PTY terminal driver generates SIGINT from \x03.
+import time
+time.sleep(10)
+{\x03}
+print('repl still responds')
diff --git a/tests/cmdline/repl_ctrl_c_interrupt_execution.py.exp b/tests/cmdline/repl_ctrl_c_interrupt_execution.py.exp
new file mode 100644
index 00000000000..48baad0193a
--- /dev/null
+++ b/tests/cmdline/repl_ctrl_c_interrupt_execution.py.exp
@@ -0,0 +1,15 @@
+MicroPython \.\+ version
+Type "help()" for more information.
+>>> # sigint: deliver via controlling terminal
+>>> # Test that Ctrl-C (SIGINT) interrupts blocking code execution.
+>>> # MicroPython restores original terminal mode (ISIG on) during
+>>> # execution, so the PTY terminal driver generates SIGINT from \x03.
+>>> import time
+>>> time.sleep(10)
+########
+\.\*Traceback (most recent call last):
+  File "", line 1, in 
+KeyboardInterrupt: \$
+>>> print('repl still responds')
+repl still responds
+>>> \$
diff --git a/tests/cmdline/repl_emacs_keys.py.exp b/tests/cmdline/repl_emacs_keys.py.exp
index 6102c19639a..52d6ac8bab0 100644
--- a/tests/cmdline/repl_emacs_keys.py.exp
+++ b/tests/cmdline/repl_emacs_keys.py.exp
@@ -1,5 +1,5 @@
 MicroPython \.\+ version
-Use \.\+
+Type "help()" for more information.
 >>> # REPL tests of GNU-ish readline navigation
 >>> # history buffer navigation
 >>> 1
@@ -16,4 +16,4 @@ Use \.\+
 >>> t = 121
 >>> \.\+
 'foobar'
->>> 
+>>> \$
diff --git a/tests/cmdline/repl_inspect.py.exp b/tests/cmdline/repl_inspect.py.exp
index 051acfd153a..66258a9faad 100644
--- a/tests/cmdline/repl_inspect.py.exp
+++ b/tests/cmdline/repl_inspect.py.exp
@@ -1,6 +1,6 @@
 test
 MicroPython \.\+ version
-Use \.\+
+Type "help()" for more information.
 >>> # cmdline: -i -c print("test")
 >>> # -c option combined with -i option results in REPL
->>> 
+>>> \$
diff --git a/tests/cmdline/repl_lock.py b/tests/cmdline/repl_lock.py
new file mode 100644
index 00000000000..77e2e932740
--- /dev/null
+++ b/tests/cmdline/repl_lock.py
@@ -0,0 +1,7 @@
+import micropython
+micropython.heap_lock()
+1+1
+micropython.heap_lock()
+None # Cause the repl's line storage to be enlarged ----------------
+micropython.heap_lock()
+raise SystemExit
diff --git a/tests/cmdline/repl_lock.py.exp b/tests/cmdline/repl_lock.py.exp
new file mode 100644
index 00000000000..d921fd6ae8e
--- /dev/null
+++ b/tests/cmdline/repl_lock.py.exp
@@ -0,0 +1,10 @@
+MicroPython \.\+ version
+Type "help()" for more information.
+>>> import micropython
+>>> micropython.heap_lock()
+>>> 1+1
+2
+>>> micropython.heap_lock()
+>>> None # Cause the repl's line storage to be enlarged ----------------
+>>> micropython.heap_lock()
+>>> raise SystemExit
diff --git a/tests/cmdline/repl_micropyinspect b/tests/cmdline/repl_micropyinspect
index 0710f1a75be..8dfa8810ea4 100644
--- a/tests/cmdline/repl_micropyinspect
+++ b/tests/cmdline/repl_micropyinspect
@@ -1,3 +1,3 @@
-import uos
+import os
 
-uos.putenv('MICROPYINSPECT', '1')
+os.putenv('MICROPYINSPECT', '1')
diff --git a/tests/cmdline/repl_micropyinspect.py.exp b/tests/cmdline/repl_micropyinspect.py.exp
index 93ff43546ea..2789df3f857 100644
--- a/tests/cmdline/repl_micropyinspect.py.exp
+++ b/tests/cmdline/repl_micropyinspect.py.exp
@@ -1,5 +1,5 @@
 MicroPython \.\+ version
-Use \.\+
+Type "help()" for more information.
 >>> # cmdline: cmdline/repl_micropyinspect
 >>> # setting MICROPYINSPECT environment variable before program exit triggers REPL
->>> 
+>>> \$
diff --git a/tests/cmdline/repl_paste.py b/tests/cmdline/repl_paste.py
new file mode 100644
index 00000000000..8c4f341701f
--- /dev/null
+++ b/tests/cmdline/repl_paste.py
@@ -0,0 +1,90 @@
+# Test REPL paste mode functionality
+
+# Basic paste mode with a simple function
+{\x05}
+def hello():
+    print('Hello from paste mode!')
+hello()
+{\x04}
+
+# Paste mode with multiple indentation levels
+{\x05}
+def calculate(n):
+    if n > 0:
+        for i in range(n):
+            if i % 2 == 0:
+                print(f'Even: {i}')
+            else:
+                print(f'Odd: {i}')
+    else:
+        print('n must be positive')
+
+calculate(5)
+{\x04}
+
+# Paste mode with blank lines
+{\x05}
+def function_with_blanks():
+    print('First line')
+{\x20}{\x20}{\x20}{\x20}
+    print('After blank line')
+{\x20}{\x20}{\x20}{\x20}
+{\x20}{\x20}{\x20}{\x20}
+    print('After two blank lines')
+
+function_with_blanks()
+{\x04}
+
+# Paste mode with class definition and multiple methods
+{\x05}
+class TestClass:
+    def __init__(self, value):
+        self.value = value
+{\x20}{\x20}{\x20}{\x20}
+    def display(self):
+        print(f'Value is: {self.value}')
+{\x20}{\x20}{\x20}{\x20}
+    def double(self):
+        self.value *= 2
+        return self.value
+
+obj = TestClass(21)
+obj.display()
+print(f'Doubled: {obj.double()}')
+obj.display()
+{\x04}
+
+# Paste mode with exception handling
+{\x05}
+try:
+    x = 1 / 0
+except ZeroDivisionError:
+    print('Caught division by zero')
+finally:
+    print('Finally block executed')
+{\x04}
+
+# Cancel paste mode with Ctrl-C
+{\x05}
+print('This should not execute')
+{\x03}
+
+# Normal REPL still works after cancelled paste
+print('Back to normal REPL')
+
+# Paste mode with syntax error
+{\x05}
+def bad_syntax(:
+    print('Missing parameter')
+{\x04}
+
+# Paste mode with runtime error
+{\x05}
+def will_error():
+    undefined_variable
+{\x20}{\x20}{\x20}{\x20}
+will_error()
+{\x04}
+
+# Final test to show REPL is still functioning
+1 + 2 + 3
diff --git a/tests/cmdline/repl_paste.py.exp b/tests/cmdline/repl_paste.py.exp
new file mode 100644
index 00000000000..a95d9fd2f7b
--- /dev/null
+++ b/tests/cmdline/repl_paste.py.exp
@@ -0,0 +1,140 @@
+MicroPython \.\+ version
+Type "help()" for more information.
+>>> # Test REPL paste mode functionality
+>>> \$
+>>> # Basic paste mode with a simple function
+>>> \$
+paste mode; Ctrl-C to cancel, Ctrl-D to finish
+=== \$
+=== def hello():
+===     print('Hello from paste mode!')
+=== hello()
+=== \$
+Hello from paste mode!
+>>> \$
+>>> \$
+>>> # Paste mode with multiple indentation levels
+>>> \$
+paste mode; Ctrl-C to cancel, Ctrl-D to finish
+=== \$
+=== def calculate(n):
+===     if n > 0:
+===         for i in range(n):
+===             if i % 2 == 0:
+===                 print(f'Even: {i}')
+===             else:
+===                 print(f'Odd: {i}')
+===     else:
+===         print('n must be positive')
+=== \$
+=== calculate(5)
+=== \$
+Even: 0
+Odd: 1
+Even: 2
+Odd: 3
+Even: 4
+>>> \$
+>>> \$
+>>> # Paste mode with blank lines
+>>> \$
+paste mode; Ctrl-C to cancel, Ctrl-D to finish
+=== \$
+=== def function_with_blanks():
+===     print('First line')
+===     \$
+===     print('After blank line')
+===     \$
+===     \$
+===     print('After two blank lines')
+=== \$
+=== function_with_blanks()
+=== \$
+First line
+After blank line
+After two blank lines
+>>> \$
+>>> \$
+>>> # Paste mode with class definition and multiple methods
+>>> \$
+paste mode; Ctrl-C to cancel, Ctrl-D to finish
+=== \$
+=== class TestClass:
+===     def __init__(self, value):
+===         self.value = value
+===     \$
+===     def display(self):
+===         print(f'Value is: {self.value}')
+===     \$
+===     def double(self):
+===         self.value *= 2
+===         return self.value
+=== \$
+=== obj = TestClass(21)
+=== obj.display()
+=== print(f'Doubled: {obj.double()}')
+=== obj.display()
+=== \$
+Value is: 21
+Doubled: 42
+Value is: 42
+>>> \$
+>>> \$
+>>> # Paste mode with exception handling
+>>> \$
+paste mode; Ctrl-C to cancel, Ctrl-D to finish
+=== \$
+=== try:
+===     x = 1 / 0
+=== except ZeroDivisionError:
+===     print('Caught division by zero')
+=== finally:
+===     print('Finally block executed')
+=== \$
+Caught division by zero
+Finally block executed
+>>> \$
+>>> \$
+>>> # Cancel paste mode with Ctrl-C
+>>> \$
+paste mode; Ctrl-C to cancel, Ctrl-D to finish
+=== \$
+=== print('This should not execute')
+=== \$
+>>> \$
+>>> \$
+>>> # Normal REPL still works after cancelled paste
+>>> print('Back to normal REPL')
+Back to normal REPL
+>>> \$
+>>> # Paste mode with syntax error
+>>> \$
+paste mode; Ctrl-C to cancel, Ctrl-D to finish
+=== \$
+=== def bad_syntax(:
+===     print('Missing parameter')
+=== \$
+Traceback (most recent call last):
+  File "", line 2
+SyntaxError: invalid syntax
+>>> \$
+>>> \$
+>>> # Paste mode with runtime error
+>>> \$
+paste mode; Ctrl-C to cancel, Ctrl-D to finish
+=== \$
+=== def will_error():
+===     undefined_variable
+===     \$
+=== will_error()
+=== \$
+Traceback (most recent call last):
+  File "", line 5, in 
+  File "", line 3, in will_error
+NameError: name 'undefined_variable' isn't defined
+>>> \$
+>>> \$
+>>> # Final test to show REPL is still functioning
+>>> 1 + 2 + 3
+6
+>>> \$
diff --git a/tests/cmdline/repl_sys_ps1_ps2.py b/tests/cmdline/repl_sys_ps1_ps2.py
index 4f96057c49b..cfefe804bf2 100644
--- a/tests/cmdline/repl_sys_ps1_ps2.py
+++ b/tests/cmdline/repl_sys_ps1_ps2.py
@@ -1,6 +1,6 @@
 # test changing ps1/ps2
-import usys
-usys.ps1 = "PS1"
-usys.ps2 = "PS2"
+import sys
+sys.ps1 = "PS1"
+sys.ps2 = "PS2"
 (1 +
 2)
diff --git a/tests/cmdline/repl_sys_ps1_ps2.py.exp b/tests/cmdline/repl_sys_ps1_ps2.py.exp
index e4a802d34d7..6781660bf33 100644
--- a/tests/cmdline/repl_sys_ps1_ps2.py.exp
+++ b/tests/cmdline/repl_sys_ps1_ps2.py.exp
@@ -1,9 +1,9 @@
 MicroPython \.\+ version
-Use \.\+
+Type "help()" for more information.
 >>> # test changing ps1/ps2
->>> import usys
->>> usys.ps1 = "PS1"
-PS1usys.ps2 = "PS2"
+>>> import sys
+>>> sys.ps1 = "PS1"
+PS1sys.ps2 = "PS2"
 PS1(1 +
 PS22)
 3
diff --git a/tests/cmdline/repl_words_move.py.exp b/tests/cmdline/repl_words_move.py.exp
index 86f6b778898..64d1e13d3e9 100644
--- a/tests/cmdline/repl_words_move.py.exp
+++ b/tests/cmdline/repl_words_move.py.exp
@@ -1,5 +1,5 @@
 MicroPython \.\+ version
-Use \.\+
+Type "help()" for more information.
 >>> # word movement
 >>> # backward-word, start in word
 >>> \.\+
@@ -19,7 +19,7 @@ Use \.\+
 >>> # forward-word on eol. if cursor is moved, this will result in a SyntaxError
 >>> \.\+
 6
->>> 
+>>> \$
 >>> # kill word
 >>> # backward-kill-word, start in word
 >>> \.\+
@@ -33,7 +33,7 @@ Use \.\+
 >>> # forward-kill-word, don't start in word
 >>> \.\+
 3
->>> 
+>>> \$
 >>> # extra move/kill shortcuts
 >>> # ctrl-left
 >>> \.\+
@@ -44,4 +44,4 @@ Use \.\+
 >>> # ctrl-w
 >>> \.\+
 1
->>> 
+>>> \$
diff --git a/tests/cpydiff/builtin_next_arg2.py b/tests/cpydiff/builtin_next_arg2.py
deleted file mode 100644
index 5df2d6e70f8..00000000000
--- a/tests/cpydiff/builtin_next_arg2.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""
-categories: Modules,builtins
-description: Second argument to next() is not implemented
-cause: MicroPython is optimised for code space.
-workaround: Instead of ``val = next(it, deflt)`` use::
-
-    try:
-        val = next(it)
-    except StopIteration:
-        val = deflt
-"""
-print(next(iter(range(0)), 42))
diff --git a/tests/cpydiff/core_class_delnotimpl.py b/tests/cpydiff/core_class_delnotimpl.py
index 18c176e9bb1..6fac764ac98 100644
--- a/tests/cpydiff/core_class_delnotimpl.py
+++ b/tests/cpydiff/core_class_delnotimpl.py
@@ -4,6 +4,7 @@
 cause: Unknown
 workaround: Unknown
 """
+
 import gc
 
 
diff --git a/tests/cpydiff/core_class_dir.py b/tests/cpydiff/core_class_dir.py
new file mode 100644
index 00000000000..e2b0e877cdc
--- /dev/null
+++ b/tests/cpydiff/core_class_dir.py
@@ -0,0 +1,14 @@
+"""
+categories: Core,Classes
+description: dir() does not convert __dir__ return value to a sorted list
+cause: MicroPython's dir() returns the value from __dir__ as-is, without iterating it into a list or sorting it.
+workaround: Have __dir__ return a sorted list directly.
+"""
+
+
+class C:
+    def __dir__(self):
+        return "cba"
+
+
+print(dir(C()))
diff --git a/tests/cpydiff/core_class_initsubclass.py b/tests/cpydiff/core_class_initsubclass.py
new file mode 100644
index 00000000000..8683271dcb0
--- /dev/null
+++ b/tests/cpydiff/core_class_initsubclass.py
@@ -0,0 +1,21 @@
+"""
+categories: Core,Classes
+description: ``__init_subclass__`` isn't automatically called.
+cause: MicroPython does not currently implement PEP 487.
+workaround: Manually call ``__init_subclass__`` after class creation if needed. e.g.::
+
+    class A(Base):
+        pass
+    A.__init_subclass__()
+
+"""
+
+
+class Base:
+    @classmethod
+    def __init_subclass__(cls):
+        print(f"Base.__init_subclass__({cls.__name__})")
+
+
+class A(Base):
+    pass
diff --git a/tests/cpydiff/core_class_initsubclass_autoclassmethod.py b/tests/cpydiff/core_class_initsubclass_autoclassmethod.py
new file mode 100644
index 00000000000..b2f7628976c
--- /dev/null
+++ b/tests/cpydiff/core_class_initsubclass_autoclassmethod.py
@@ -0,0 +1,31 @@
+"""
+categories: Core,Classes
+description: ``__init_subclass__`` isn't an implicit classmethod.
+cause: MicroPython does not currently implement PEP 487. ``__init_subclass__`` is not currently in the list of special-cased class/static methods.
+workaround: Decorate declarations of ``__init_subclass__`` with ``@classmethod``.
+"""
+
+
+def regularize_spelling(text, prefix="bound_"):
+    # for regularizing across the CPython "method" vs MicroPython "bound_method" spelling for the type of a bound classmethod
+    if text.startswith(prefix):
+        return text[len(prefix) :]
+    return text
+
+
+class A:
+    def __init_subclass__(cls):
+        pass
+
+    @classmethod
+    def manual_decorated(cls):
+        pass
+
+
+a = type(A.__init_subclass__).__name__
+b = type(A.manual_decorated).__name__
+
+print(regularize_spelling(a))
+print(regularize_spelling(b))
+if a != b:
+    print("FAIL")
diff --git a/tests/cpydiff/core_class_initsubclass_kwargs.py b/tests/cpydiff/core_class_initsubclass_kwargs.py
new file mode 100644
index 00000000000..ed5157afeae
--- /dev/null
+++ b/tests/cpydiff/core_class_initsubclass_kwargs.py
@@ -0,0 +1,22 @@
+"""
+categories: Core,Classes
+description: MicroPython doesn't support parameterized ``__init_subclass__`` class customization.
+cause: MicroPython does not currently implement PEP 487. The MicroPython syntax tree does not include a kwargs node after the class inheritance list.
+workaround: Use class variables or another mechanism to specify base-class customizations.
+"""
+
+
+class Base:
+    @classmethod
+    def __init_subclass__(cls, arg=None, **kwargs):
+        cls.init_subclass_was_called = True
+        print(f"Base.__init_subclass__({cls.__name__}, {arg=!r}, {kwargs=!r})")
+
+
+class A(Base, arg="arg"):
+    pass
+
+
+# Regularize across MicroPython not automatically calling __init_subclass__ either.
+if not getattr(A, "init_subclass_was_called", False):
+    A.__init_subclass__()
diff --git a/tests/cpydiff/core_class_initsubclass_super.py b/tests/cpydiff/core_class_initsubclass_super.py
new file mode 100644
index 00000000000..d18671a74bf
--- /dev/null
+++ b/tests/cpydiff/core_class_initsubclass_super.py
@@ -0,0 +1,22 @@
+"""
+categories: Core,Classes
+description: ``__init_subclass__`` can't be defined a cooperatively-recursive way.
+cause: MicroPython does not currently implement PEP 487. The base object type does not have an ``__init_subclass__`` implementation.
+workaround: Omit the recursive ``__init_subclass__`` call unless it's known that the grandparent also defines it.
+"""
+
+
+class Base:
+    @classmethod
+    def __init_subclass__(cls, **kwargs):
+        cls.init_subclass_was_called = True
+        super().__init_subclass__(**kwargs)
+
+
+class A(Base):
+    pass
+
+
+# Regularize across MicroPython not automatically calling __init_subclass__ either.
+if not getattr(A, "init_subclass_was_called", False):
+    A.__init_subclass__()
diff --git a/tests/cpydiff/core_class_name_mangling.py b/tests/cpydiff/core_class_name_mangling.py
new file mode 100644
index 00000000000..39153209deb
--- /dev/null
+++ b/tests/cpydiff/core_class_name_mangling.py
@@ -0,0 +1,26 @@
+"""
+categories: Core,Classes
+description: Private Class Members name mangling is not implemented
+cause: The MicroPython compiler does not implement name mangling for private class members.
+workaround: Avoid using or having a collision with global names, by adding a unique prefix to the private class member name manually.
+"""
+
+
+def __print_string(string):
+    print(string)
+
+
+class Foo:
+    def __init__(self, string):
+        self.string = string
+
+    def do_print(self):
+        __print_string(self.string)
+
+
+example_string = "Example String to print."
+
+class_item = Foo(example_string)
+print(class_item.string)
+
+class_item.do_print()
diff --git a/tests/cpydiff/core_class_strrettype.py b/tests/cpydiff/core_class_strrettype.py
new file mode 100644
index 00000000000..398b5bb1a3b
--- /dev/null
+++ b/tests/cpydiff/core_class_strrettype.py
@@ -0,0 +1,14 @@
+"""
+categories: Core,Classes
+description: ``__str__`` returning non-string type does not raise TypeError
+cause: MicroPython's instance_print does not validate that ``__str__`` or ``__repr__`` return a str or its subclass
+workaround: Ensure ``__str__`` and ``__repr__`` always return a str instance or its subclass
+"""
+
+
+class Foo:
+    def __str__(self):
+        return True
+
+
+print(str(Foo()))
diff --git a/tests/cpydiff/core_class_subclassret.py b/tests/cpydiff/core_class_subclassret.py
new file mode 100644
index 00000000000..681a16e879b
--- /dev/null
+++ b/tests/cpydiff/core_class_subclassret.py
@@ -0,0 +1,19 @@
+"""
+categories: Core,Classes
+description: str() does not preserve str subclass type from ``__str__`` or ``__repr__`` return value
+cause: Implementation discards str subclass type returned by ``__str__`` or ``__repr__`` and always returns a plain str instance from str().
+workaround: Do not rely on str() preserving str subclass types
+"""
+
+
+class MyStr(str):
+    pass
+
+
+class Foo:
+    def __str__(self):
+        return MyStr("abc")
+
+
+result = str(Foo())
+print(type(result))
diff --git a/tests/cpydiff/core_class_super_init.py b/tests/cpydiff/core_class_super_init.py
new file mode 100644
index 00000000000..1774f61dd82
--- /dev/null
+++ b/tests/cpydiff/core_class_super_init.py
@@ -0,0 +1,31 @@
+"""
+categories: Core,Classes
+description: When inheriting native types, calling a method in ``__init__(self, ...)`` before ``super().__init__()`` raises an ``AttributeError`` (or segfaults if ``MICROPY_BUILTIN_METHOD_CHECK_SELF_ARG`` is not enabled).
+cause: MicroPython does not have separate ``__new__`` and ``__init__`` methods in native types.
+workaround: Call ``super().__init__()`` first.
+"""
+
+
+class L1(list):
+    def __init__(self, a):
+        self.append(a)
+
+
+try:
+    L1(1)
+    print("OK")
+except AttributeError:
+    print("AttributeError")
+
+
+class L2(list):
+    def __init__(self, a):
+        super().__init__()
+        self.append(a)
+
+
+try:
+    L2(1)
+    print("OK")
+except AttributeError:
+    print("AttributeError")
diff --git a/tests/cpydiff/core_exception_construction.py b/tests/cpydiff/core_exception_construction.py
new file mode 100644
index 00000000000..56a358f4c45
--- /dev/null
+++ b/tests/cpydiff/core_exception_construction.py
@@ -0,0 +1,28 @@
+"""
+categories: Core,Exceptions
+description: Throwing a derived exception class instance in its `__init__` without first calling ``super().__init__`` is a TypeError
+cause: In MicroPython, an object is incompletely constructed if it does not call its superclass init function or return normally from its ``__init__``. This prevents its usage in some circumstances.
+workaround: Call the superclass `__init__` method before raising the exception.
+"""
+
+
+class C(Exception):
+    def __init__(self):
+        raise self
+
+
+class C1(Exception):
+    def __init__(self):
+        super().__init__()
+        raise self
+
+
+try:
+    C()
+except Exception as e:
+    print(type(e).__name__)
+
+try:
+    C1()
+except Exception as e:
+    print(type(e).__name__)
diff --git a/tests/cpydiff/core_fstring_concat.py b/tests/cpydiff/core_fstring_concat.py
index c2bdb4e666a..2fbe1b961a1 100644
--- a/tests/cpydiff/core_fstring_concat.py
+++ b/tests/cpydiff/core_fstring_concat.py
@@ -1,13 +1,14 @@
 """
-categories: Core
-description: f-strings don't support concatenation with adjacent literals if the adjacent literals contain braces or are f-strings
+categories: Core,f-strings
+description: f-strings don't support concatenation with adjacent literals if the adjacent literals contain braces
 cause: MicroPython is optimised for code space.
-workaround: Use the + operator between literal strings when either or both are f-strings
+workaround: Use the + operator between literal strings when they are not both f-strings
 """
 
 x, y = 1, 2
+# fmt: off
 print("aa" f"{x}")  # works
 print(f"{x}" "ab")  # works
 print("a{}a" f"{x}")  # fails
 print(f"{x}" "a{}b")  # fails
-print(f"{x}" f"{y}")  # fails
+# fmt: on
diff --git a/tests/cpydiff/core_fstring_parser.py b/tests/cpydiff/core_fstring_parser.py
index 22bbc5866ec..04b24cdfb69 100644
--- a/tests/cpydiff/core_fstring_parser.py
+++ b/tests/cpydiff/core_fstring_parser.py
@@ -1,9 +1,9 @@
 """
-categories: Core
+categories: Core,f-strings
 description: f-strings cannot support expressions that require parsing to resolve unbalanced nested braces and brackets
 cause: MicroPython is optimised for code space.
 workaround: Always use balanced braces and brackets in expressions inside f-strings
 """
 
-print(f'{"hello { world"}')
-print(f'{"hello ] world"}')
+print(f"{'hello { world'}")
+print(f"{'hello ] world'}")
diff --git a/tests/cpydiff/core_fstring_raw.py b/tests/cpydiff/core_fstring_raw.py
deleted file mode 100644
index 84e265f70fc..00000000000
--- a/tests/cpydiff/core_fstring_raw.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""
-categories: Core
-description: Raw f-strings are not supported
-cause: MicroPython is optimised for code space.
-workaround: Unknown
-"""
-
-rf"hello"
diff --git a/tests/cpydiff/core_fstring_repr.py b/tests/cpydiff/core_fstring_repr.py
index df80abf795b..2589a34b7e3 100644
--- a/tests/cpydiff/core_fstring_repr.py
+++ b/tests/cpydiff/core_fstring_repr.py
@@ -1,18 +1,8 @@
 """
-categories: Core
-description: f-strings don't support the !r, !s, and !a conversions
-cause: MicroPython is optimised for code space.
-workaround: Use repr(), str(), and ascii() explicitly.
+categories: Core,f-strings
+description: f-strings don't support !a conversions
+cause: MicropPython does not implement ascii()
+workaround: None
 """
 
-
-class X:
-    def __repr__(self):
-        return "repr"
-
-    def __str__(self):
-        return "str"
-
-
-print(f"{X()!r}")
-print(f"{X()!s}")
+f"{'unicode text'!a}"
diff --git a/tests/cpydiff/core_function_argcount.py b/tests/cpydiff/core_function_argcount.py
index 5f3dca4dcdb..c257def726a 100644
--- a/tests/cpydiff/core_function_argcount.py
+++ b/tests/cpydiff/core_function_argcount.py
@@ -4,6 +4,7 @@
 cause: MicroPython counts "self" as an argument.
 workaround: Interpret error messages with the information above in mind.
 """
+
 try:
     [].append()
 except Exception as e:
diff --git a/tests/cpydiff/core_function_star.py b/tests/cpydiff/core_function_star.py
new file mode 100644
index 00000000000..26f5effd4e3
--- /dev/null
+++ b/tests/cpydiff/core_function_star.py
@@ -0,0 +1,16 @@
+"""
+categories: Core,Functions
+description: ``*args`` cannot follow a keyword argument
+cause: MicroPython is optimised for code space. For more information see `this issue `_.
+workaround: Re-order the arguments
+"""
+
+
+def f(x, y):
+    return x + y
+
+
+try:
+    print(f(y=1, *(3,)))
+except Exception as e:
+    print(e)
diff --git a/tests/cpydiff/core_import_all.py b/tests/cpydiff/core_import_all.py
deleted file mode 100644
index 5adf9ae3eb1..00000000000
--- a/tests/cpydiff/core_import_all.py
+++ /dev/null
@@ -1,9 +0,0 @@
-"""
-categories: Core,import
-description: __all__ is unsupported in __init__.py in MicroPython.
-cause: Not implemented.
-workaround: Manually import the sub-modules directly in __init__.py using ``from . import foo, bar``.
-"""
-from modules3 import *
-
-foo.hello()
diff --git a/tests/cpydiff/core_import_path.py b/tests/cpydiff/core_import_path.py
index 959fd571f57..2028386be84 100644
--- a/tests/cpydiff/core_import_path.py
+++ b/tests/cpydiff/core_import_path.py
@@ -4,6 +4,7 @@
 cause: MicroPython doesn't support namespace packages split across filesystem. Beyond that, MicroPython's import system is highly optimized for minimal memory usage.
 workaround: Details of import handling is inherently implementation dependent. Don't rely on such details in portable applications.
 """
+
 import modules
 
 print(modules.__path__)
diff --git a/tests/cpydiff/core_import_prereg.py b/tests/cpydiff/core_import_prereg.py
deleted file mode 100644
index 3ce2340c68d..00000000000
--- a/tests/cpydiff/core_import_prereg.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""
-categories: Core,import
-description: Failed to load modules are still registered as loaded
-cause: To make module handling more efficient, it's not wrapped with exception handling.
-workaround: Test modules before production use; during development, use ``del sys.modules["name"]``, or just soft or hard reset the board.
-"""
-import sys
-
-try:
-    from modules import foo
-except NameError as e:
-    print(e)
-try:
-    from modules import foo
-
-    print("Should not get here")
-except NameError as e:
-    print(e)
diff --git a/tests/cpydiff/core_import_split_ns_pkgs.py b/tests/cpydiff/core_import_split_ns_pkgs.py
index 5c92b63124a..a1cfd84893d 100644
--- a/tests/cpydiff/core_import_split_ns_pkgs.py
+++ b/tests/cpydiff/core_import_split_ns_pkgs.py
@@ -4,6 +4,7 @@
 cause: MicroPython's import system is highly optimized for simplicity, minimal memory usage, and minimal filesystem search overhead.
 workaround: Don't install modules belonging to the same namespace package in different directories. For MicroPython, it's recommended to have at most 3-component module search paths: for your current application, per-user (writable), system-wide (non-writable).
 """
+
 import sys
 
 sys.path.append(sys.path[1] + "/modules")
diff --git a/tests/cpydiff/core_locals_eval.py b/tests/cpydiff/core_locals_eval.py
index 025d2263729..3f6ad2a1dbb 100644
--- a/tests/cpydiff/core_locals_eval.py
+++ b/tests/cpydiff/core_locals_eval.py
@@ -4,6 +4,7 @@
 cause: MicroPython doesn't maintain symbolic local environment, it is optimized to an array of slots. Thus, local variables can't be accessed by a name. Effectively, ``eval(expr)`` in MicroPython is equivalent to ``eval(expr, globals(), globals())``.
 workaround: Unknown
 """
+
 val = 1
 
 
diff --git a/tests/cpydiff/module_array_comparison.py b/tests/cpydiff/module_array_comparison.py
index a442af3f5bc..4929b1e590d 100644
--- a/tests/cpydiff/module_array_comparison.py
+++ b/tests/cpydiff/module_array_comparison.py
@@ -4,6 +4,7 @@
 cause: Code size
 workaround: Compare individual elements
 """
+
 import array
 
 array.array("b", [1, 2]) == array.array("i", [1, 2])
diff --git a/tests/cpydiff/module_array_constructor.py b/tests/cpydiff/module_array_constructor.py
index 08cf2ef2d1c..a53589d22ae 100644
--- a/tests/cpydiff/module_array_constructor.py
+++ b/tests/cpydiff/module_array_constructor.py
@@ -4,6 +4,7 @@
 cause: MicroPython implements implicit truncation in order to reduce code size and execution time
 workaround: If CPython compatibility is needed then mask the value explicitly
 """
+
 import array
 
 a = array.array("b", [257])
diff --git a/tests/cpydiff/modules3/__init__.py b/tests/cpydiff/modules3/__init__.py
deleted file mode 100644
index 27a2bf2ad90..00000000000
--- a/tests/cpydiff/modules3/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-__all__ = ["foo"]
diff --git a/tests/cpydiff/modules3/foo.py b/tests/cpydiff/modules3/foo.py
deleted file mode 100644
index dd9b9d4ddd4..00000000000
--- a/tests/cpydiff/modules3/foo.py
+++ /dev/null
@@ -1,2 +0,0 @@
-def hello():
-    print("hello")
diff --git a/tests/cpydiff/modules_array_containment.py b/tests/cpydiff/modules_array_containment.py
index 8f25b0d4914..b29895aa758 100644
--- a/tests/cpydiff/modules_array_containment.py
+++ b/tests/cpydiff/modules_array_containment.py
@@ -4,6 +4,7 @@
 cause: Unknown
 workaround: Unknown
 """
+
 import array
 
 print(1 in array.array("B", b"12"))
diff --git a/tests/cpydiff/modules_array_deletion.py b/tests/cpydiff/modules_array_deletion.py
index 3376527373e..2e7c31124b2 100644
--- a/tests/cpydiff/modules_array_deletion.py
+++ b/tests/cpydiff/modules_array_deletion.py
@@ -4,6 +4,7 @@
 cause: Unknown
 workaround: Unknown
 """
+
 import array
 
 a = array.array("b", (1, 2, 3))
diff --git a/tests/cpydiff/modules_array_subscrstep.py b/tests/cpydiff/modules_array_subscrstep.py
index 24308bd9042..65b12332f54 100644
--- a/tests/cpydiff/modules_array_subscrstep.py
+++ b/tests/cpydiff/modules_array_subscrstep.py
@@ -4,6 +4,7 @@
 cause: Unknown
 workaround: Unknown
 """
+
 import array
 
 a = array.array("b", (1, 2, 3))
diff --git a/tests/cpydiff/modules_deque.py b/tests/cpydiff/modules_deque.py
deleted file mode 100644
index 4d2746d1f86..00000000000
--- a/tests/cpydiff/modules_deque.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""
-categories: Modules,deque
-description: Deque not implemented
-cause: Unknown
-workaround: Use regular lists. micropython-lib has implementation of collections.deque.
-"""
-import collections
-
-D = collections.deque()
-print(D)
diff --git a/tests/cpydiff/modules_errno_enotsup.py b/tests/cpydiff/modules_errno_enotsup.py
new file mode 100644
index 00000000000..80e5ad9d032
--- /dev/null
+++ b/tests/cpydiff/modules_errno_enotsup.py
@@ -0,0 +1,10 @@
+"""
+categories: Modules,errno
+description: MicroPython does not include ``ENOTSUP`` as a name for errno 95.
+cause: MicroPython does not implement the ``ENOTSUP`` canonical alias for ``EOPNOTSUPP`` added in CPython 3.2.
+workaround: Use ``errno.EOPNOTSUPP`` in place of ``errno.ENOTSUP``.
+"""
+
+import errno
+
+print(f"{errno.errorcode[errno.EOPNOTSUPP]=!s}")
diff --git a/tests/cpydiff/modules_json_nonserializable.py b/tests/cpydiff/modules_json_nonserializable.py
index ffe523786f5..d83e7beca2d 100644
--- a/tests/cpydiff/modules_json_nonserializable.py
+++ b/tests/cpydiff/modules_json_nonserializable.py
@@ -4,12 +4,10 @@
 cause: Unknown
 workaround: Unknown
 """
+
 import json
 
-a = bytes(x for x in range(256))
 try:
-    z = json.dumps(a)
-    x = json.loads(z)
-    print("Should not get here")
+    print(json.dumps(b"shouldn't be able to serialise bytes"))
 except TypeError:
     print("TypeError")
diff --git a/tests/cpydiff/modules_os_environ.py b/tests/cpydiff/modules_os_environ.py
index 491d8a31016..4edde166567 100644
--- a/tests/cpydiff/modules_os_environ.py
+++ b/tests/cpydiff/modules_os_environ.py
@@ -4,6 +4,7 @@
 cause: Unknown
 workaround: Use ``getenv``, ``putenv`` and ``unsetenv``
 """
+
 import os
 
 try:
diff --git a/tests/cpydiff/modules_os_getenv.py b/tests/cpydiff/modules_os_getenv.py
index d1e828438e4..d5acd414eb3 100644
--- a/tests/cpydiff/modules_os_getenv.py
+++ b/tests/cpydiff/modules_os_getenv.py
@@ -4,6 +4,7 @@
 cause: The ``environ`` attribute is not implemented
 workaround: Unknown
 """
+
 import os
 
 print(os.getenv("NEW_VARIABLE"))
diff --git a/tests/cpydiff/modules_struct_fewargs.py b/tests/cpydiff/modules_struct_fewargs.py
index cb6b0fd874e..f6346a67938 100644
--- a/tests/cpydiff/modules_struct_fewargs.py
+++ b/tests/cpydiff/modules_struct_fewargs.py
@@ -1,9 +1,10 @@
 """
 categories: Modules,struct
-description: Struct pack with too few args, not checked by uPy
+description: Struct pack with too few args, not checked by MicroPython
 cause: Unknown
 workaround: Unknown
 """
+
 import struct
 
 try:
diff --git a/tests/cpydiff/modules_struct_manyargs.py b/tests/cpydiff/modules_struct_manyargs.py
index 03395baad3b..b2ea93b6c93 100644
--- a/tests/cpydiff/modules_struct_manyargs.py
+++ b/tests/cpydiff/modules_struct_manyargs.py
@@ -1,9 +1,10 @@
 """
 categories: Modules,struct
-description: Struct pack with too many args, not checked by uPy
+description: Struct pack with too many args, not checked by MicroPython
 cause: Unknown
 workaround: Unknown
 """
+
 import struct
 
 try:
diff --git a/tests/cpydiff/modules_struct_whitespace_in_format.py b/tests/cpydiff/modules_struct_whitespace_in_format.py
index a882b38569a..8b609425eb0 100644
--- a/tests/cpydiff/modules_struct_whitespace_in_format.py
+++ b/tests/cpydiff/modules_struct_whitespace_in_format.py
@@ -1,9 +1,10 @@
 """
 categories: Modules,struct
-description: Struct pack with whitespace in format, whitespace ignored by CPython, error on uPy
+description: Struct pack with whitespace in format, whitespace ignored by CPython, error on MicroPython
 cause: MicroPython is optimised for code size.
 workaround: Don't use spaces in format strings.
 """
+
 import struct
 
 try:
diff --git a/tests/cpydiff/modules_sys_stdassign.py b/tests/cpydiff/modules_sys_stdassign.py
index 7d086078a93..29afe1b1203 100644
--- a/tests/cpydiff/modules_sys_stdassign.py
+++ b/tests/cpydiff/modules_sys_stdassign.py
@@ -4,6 +4,7 @@
 cause: They are stored in read-only memory.
 workaround: Unknown
 """
+
 import sys
 
 sys.stdin = None
diff --git a/tests/cpydiff/syntax_annotation_expression.py b/tests/cpydiff/syntax_annotation_expression.py
new file mode 100644
index 00000000000..91814ec9522
--- /dev/null
+++ b/tests/cpydiff/syntax_annotation_expression.py
@@ -0,0 +1,24 @@
+"""
+categories: Syntax,Annotations
+description: MicroPython accepts type annotations on expressions where CPython forbids them.
+cause: To reduce code size, MicroPython does not check the form of expressions with type annotations applied.
+workaround: Always check for valid Python code using a linting tool.
+
+The expressions themselves are not evaluated.
+"""
+
+
+def test(expr):
+    code = f"def f():\n    {expr}: int"
+    print(code)
+    try:
+        exec(code)
+        print("OK")
+    except SyntaxError as e:
+        print("SyntaxError")
+    print()
+
+
+test("print('test')")
+test("[x,y]")
+test("x,y")
diff --git a/tests/cpydiff/syntax_arg_unpacking.py b/tests/cpydiff/syntax_arg_unpacking.py
index e54832ddb91..7133a8a2827 100644
--- a/tests/cpydiff/syntax_arg_unpacking.py
+++ b/tests/cpydiff/syntax_arg_unpacking.py
@@ -1,5 +1,5 @@
 """
-categories: Syntax
+categories: Syntax,Unpacking
 description: Argument unpacking does not work if the argument being unpacked is the nth or greater argument where n is the number of bits in an MP_SMALL_INT.
 cause: The implementation uses an MP_SMALL_INT to flag args that need to be unpacked.
 workaround: Use fewer arguments.
diff --git a/tests/cpydiff/syntax_assign_expr.py b/tests/cpydiff/syntax_assign_expr.py
index d4ed063b39a..704c5c3ecab 100644
--- a/tests/cpydiff/syntax_assign_expr.py
+++ b/tests/cpydiff/syntax_assign_expr.py
@@ -1,7 +1,8 @@
 """
 categories: Syntax,Operators
-description: MicroPython allows using := to assign to the variable of a comprehension, CPython raises a SyntaxError.
-cause: MicroPython is optimised for code size and doesn't check this case.
-workaround: Do not rely on this behaviour if writing CPython compatible code.
+description: MicroPython allows := to assign to the iteration variable in nested comprehensions, CPython does not.
+cause: MicroPython is optimised for code size. Although it is a syntax error to assign to the iteration variable in a standard comprehension (same as CPython), it doesn't check if an inner nested comprehension assigns to the iteration variable of the outer comprehension.
+workaround: Do not use := to assign to the iteration variable of a comprehension.
 """
-print([i := -1 for i in range(4)])
+
+print([[(j := i) for i in range(2)] for j in range(2)])
diff --git a/tests/cpydiff/syntax_literal_underscore.py b/tests/cpydiff/syntax_literal_underscore.py
new file mode 100644
index 00000000000..4b1406e9f3f
--- /dev/null
+++ b/tests/cpydiff/syntax_literal_underscore.py
@@ -0,0 +1,19 @@
+"""
+categories: Syntax,Literals
+description: MicroPython accepts underscores in numeric literals where CPython doesn't
+cause: Different parser implementation
+
+MicroPython's tokenizer ignores underscores in numeric literals, while CPython
+rejects multiple consecutive underscores and underscores after the last digit.
+
+workaround: Remove the underscores not accepted by CPython.
+"""
+
+try:
+    print(eval("1__1"))
+except SyntaxError:
+    print("Should not work")
+try:
+    print(eval("1_"))
+except SyntaxError:
+    print("Should not work")
diff --git a/tests/cpydiff/syntax_spaces.py b/tests/cpydiff/syntax_spaces.py
index c308240a78d..670cefdeac2 100644
--- a/tests/cpydiff/syntax_spaces.py
+++ b/tests/cpydiff/syntax_spaces.py
@@ -1,9 +1,17 @@
 """
-categories: Syntax,Spaces
-description: uPy requires spaces between literal numbers and keywords, CPy doesn't
-cause: Unknown
-workaround: Unknown
+categories: Syntax,Literals
+description: MicroPython requires spaces between literal numbers and keywords or ".", CPython doesn't
+cause: Different parser implementation
+
+MicroPython's tokenizer treats a sequence like ``1and`` as a single token, while CPython treats it as two tokens.
+
+Since CPython 3.11, when the literal number is followed by a token, this syntax causes a ``SyntaxWarning`` for an "invalid literal". When a literal number is followed by a "." denoting attribute access, CPython does not warn.
+
+workaround: Add a space between the integer literal and the intended next token.
+
+This also fixes the ``SyntaxWarning`` in CPython.
 """
+
 try:
     print(eval("1and 0"))
 except SyntaxError:
@@ -16,3 +24,7 @@
     print(eval("1if 1else 0"))
 except SyntaxError:
     print("Should have worked")
+try:
+    print(eval("0x1.to_bytes(1)"))
+except SyntaxError:
+    print("Should have worked")
diff --git a/tests/cpydiff/syntax_unicode_nameesc.py b/tests/cpydiff/syntax_unicode_nameesc.py
index 21628c974d8..838394b6ebd 100644
--- a/tests/cpydiff/syntax_unicode_nameesc.py
+++ b/tests/cpydiff/syntax_unicode_nameesc.py
@@ -4,4 +4,5 @@
 cause: Unknown
 workaround: Unknown
 """
+
 print("\N{LATIN SMALL LETTER A}")
diff --git a/tests/cpydiff/types_bytearray_sliceassign.py b/tests/cpydiff/types_bytearray_sliceassign.py
index e4068b04b98..353f61988fa 100644
--- a/tests/cpydiff/types_bytearray_sliceassign.py
+++ b/tests/cpydiff/types_bytearray_sliceassign.py
@@ -4,6 +4,7 @@
 cause: Unknown
 workaround: Unknown
 """
+
 b = bytearray(4)
 b[0:1] = [1, 2]
 print(b)
diff --git a/tests/cpydiff/types_bytes_decode_encoding.py b/tests/cpydiff/types_bytes_decode_encoding.py
new file mode 100644
index 00000000000..17a129a2af9
--- /dev/null
+++ b/tests/cpydiff/types_bytes_decode_encoding.py
@@ -0,0 +1,30 @@
+"""
+categories: Types,bytes
+description: bytes.decode() only supports encoding arguments 'utf8', 'utf-8' and 'ascii'. Other encodings like 'latin-1' are not supported. Other string forms such as 'UTF8' are not supported.
+cause: MicroPython is optimised for embedded systems and only includes UTF-8 and ASCII codec support with simple matching to save memory and code size. `The same restriction applies to str constructor `. See also `unicode_support`.
+workaround: Convert data to UTF-8 before processing, or implement custom encoding/decoding if needed. Ensure encoding argument is one of the accepted forms.
+"""
+
+# Both CPython and MicroPython support utf8 and ascii encodings
+print(b"caf\xc3\xa9".decode("utf8"))  # codespell:ignore caf
+print(b"cafe".decode("ascii"))
+
+# MicroPython does not support additional encodings
+try:
+    b"\xe9".decode("latin-1")  # 'é' in latin-1
+    print("latin-1 supported")
+except (ValueError, NotImplementedError, LookupError) as e:
+    print("latin-1 not supported:", type(e).__name__)
+
+try:
+    b"\x80".decode("cp1252")  # Euro sign in cp1252
+    print("cp1252 supported")
+except (ValueError, NotImplementedError, LookupError) as e:
+    print("cp1252 not supported:", type(e).__name__)
+
+# Encoding arguments must match exactly in MicroPython
+try:
+    b"hello".decode("ASCII")
+    print("Capital letters ASCII supported")
+except (ValueError, NotImplementedError, LookupError) as e:
+    print("Capital letters ASCII not supported:", type(e).__name__)
diff --git a/tests/cpydiff/types_bytes_decode_kwargs.py b/tests/cpydiff/types_bytes_decode_kwargs.py
new file mode 100644
index 00000000000..a1ca2b74c5c
--- /dev/null
+++ b/tests/cpydiff/types_bytes_decode_kwargs.py
@@ -0,0 +1,19 @@
+"""
+categories: Types,bytes
+description: bytes.decode() does not accept keyword arguments, only positional arguments
+cause: MicroPython optimizes for code size and does not implement keyword argument handling for bytes.decode()
+workaround: Use positional arguments instead of keyword arguments
+"""
+
+# CPython accepts keyword arguments, MicroPython only accepts positional
+b = b"hello\xffworld"
+
+try:
+    # Using keyword arguments
+    result = b.decode(encoding="utf-8", errors="ignore")
+    print("kwargs supported:", repr(result))
+except TypeError as e:
+    print("kwargs not supported: TypeError")
+    # Workaround: use positional arguments
+    result = b.decode("utf-8", "ignore")
+    print("positional args work:", repr(result))
diff --git a/tests/cpydiff/types_bytes_encoding.py b/tests/cpydiff/types_bytes_encoding.py
new file mode 100644
index 00000000000..0fe3c976589
--- /dev/null
+++ b/tests/cpydiff/types_bytes_encoding.py
@@ -0,0 +1,21 @@
+"""
+categories: Types,bytes
+description: bytes() constructor only supports encoding arguments 'utf8', 'utf-8' and 'ascii'. Other encodings like 'latin-1' are not supported. Other string forms such as 'UTF8' are not supported.
+cause: MicroPython is optimised for embedded systems and has limited codec support to save code size. `A similar restriction applies to str.encode() `. See also `unicode_support`.
+workaround: Implement other encoding conversions manually by parsing the result of str.encode() or bytes() constructor.
+"""
+
+# Both CPython and MicroPython can encode this emoji as UTF-8 bytes
+print(bytes("😀", "utf8"))
+
+# Both CPython and MicroPython will fail to encode this emoji as ASCII bytes
+try:
+    print(bytes("😀", "ascii"))
+except UnicodeError:
+    print("UnicodeError")
+
+# Other encodings or string formats aren't accepted by MicroPython
+try:
+    print(bytes("😀", "UTF-8"))
+except LookupError:
+    print("LookupError")
diff --git a/tests/cpydiff/types_bytes_format.py b/tests/cpydiff/types_bytes_format.py
index ad049877116..1a15e572c8a 100644
--- a/tests/cpydiff/types_bytes_format.py
+++ b/tests/cpydiff/types_bytes_format.py
@@ -4,4 +4,5 @@
 cause: MicroPython strives to be a more regular implementation, so if both `str` and `bytes` support ``__mod__()`` (the % operator), it makes sense to support ``format()`` for both too. Support for ``__mod__`` can also be compiled out, which leaves only ``format()`` for bytes formatting.
 workaround: If you are interested in CPython compatibility, don't use ``.format()`` on bytes objects.
 """
+
 print(b"{}".format(1))
diff --git a/tests/cpydiff/types_bytes_keywords.py b/tests/cpydiff/types_bytes_keywords.py
index ade83d0a709..a459c94b41e 100644
--- a/tests/cpydiff/types_bytes_keywords.py
+++ b/tests/cpydiff/types_bytes_keywords.py
@@ -4,4 +4,5 @@
 cause: Unknown
 workaround: Pass the encoding as a positional parameter, e.g. ``print(bytes('abc', 'utf-8'))``
 """
+
 print(bytes("abc", encoding="utf8"))
diff --git a/tests/cpydiff/types_bytes_subscrstep.py b/tests/cpydiff/types_bytes_subscrstep.py
index 51b94cb710f..c566cbdb630 100644
--- a/tests/cpydiff/types_bytes_subscrstep.py
+++ b/tests/cpydiff/types_bytes_subscrstep.py
@@ -4,4 +4,5 @@
 cause: MicroPython is highly optimized for memory usage.
 workaround: Use explicit loop for this very rare operation.
 """
+
 print(b"123"[0:3:2])
diff --git a/tests/cpydiff/types_complex_parser.py b/tests/cpydiff/types_complex_parser.py
new file mode 100644
index 00000000000..4a012987d9e
--- /dev/null
+++ b/tests/cpydiff/types_complex_parser.py
@@ -0,0 +1,14 @@
+"""
+categories: Types,complex
+description: MicroPython's complex() accepts certain incorrect values that CPython rejects
+cause: MicroPython is highly optimized for memory usage.
+workaround: Do not use non-standard complex literals as argument to complex()
+
+MicroPython's ``complex()`` function accepts literals that contain a space and
+no sign between the real and imaginary parts, and interprets it as a plus.
+"""
+
+try:
+    print(complex("1 1j"))
+except ValueError:
+    print("ValueError")
diff --git a/tests/cpydiff/types_dict_keys_set.py b/tests/cpydiff/types_dict_keys_set.py
index 3a0849a3556..a5f127962e6 100644
--- a/tests/cpydiff/types_dict_keys_set.py
+++ b/tests/cpydiff/types_dict_keys_set.py
@@ -4,4 +4,5 @@
 cause: Not implemented.
 workaround: Explicitly convert keys to a set before using set operations.
 """
+
 print({1: 2, 3: 4}.keys() & {1})
diff --git a/tests/cpydiff/types_exception_attrs.py b/tests/cpydiff/types_exception_attrs.py
index ad72b62a61a..5fed45451d2 100644
--- a/tests/cpydiff/types_exception_attrs.py
+++ b/tests/cpydiff/types_exception_attrs.py
@@ -4,6 +4,7 @@
 cause: MicroPython is optimised to reduce code size.
 workaround: Only use ``value`` on ``StopIteration`` exceptions, and ``errno`` on ``OSError`` exceptions.  Do not use or rely on these attributes on other exceptions.
 """
+
 e = Exception(1)
 print(e.value)
 print(e.errno)
diff --git a/tests/cpydiff/types_exception_chaining.py b/tests/cpydiff/types_exception_chaining.py
index 836c4eb3e73..cff68d4124a 100644
--- a/tests/cpydiff/types_exception_chaining.py
+++ b/tests/cpydiff/types_exception_chaining.py
@@ -4,6 +4,7 @@
 cause: Unknown
 workaround: Unknown
 """
+
 try:
     raise TypeError
 except TypeError:
diff --git a/tests/cpydiff/types_exception_instancevar.py b/tests/cpydiff/types_exception_instancevar.py
index adc353361f0..fb67771baf3 100644
--- a/tests/cpydiff/types_exception_instancevar.py
+++ b/tests/cpydiff/types_exception_instancevar.py
@@ -4,6 +4,7 @@
 cause: MicroPython is highly optimized for memory usage.
 workaround: Use user-defined exception subclasses.
 """
+
 e = Exception()
 e.x = 0
 print(e.x)
diff --git a/tests/cpydiff/types_exception_loops.py b/tests/cpydiff/types_exception_loops.py
index 8d326cbbbb0..549f1dd0a5b 100644
--- a/tests/cpydiff/types_exception_loops.py
+++ b/tests/cpydiff/types_exception_loops.py
@@ -4,6 +4,7 @@
 cause: Condition checks are optimized to happen at the end of loop body, and that line number is reported.
 workaround: Unknown
 """
+
 l = ["-foo", "-bar"]
 
 i = 0
diff --git a/tests/cpydiff/types_float_implicit_conversion.py b/tests/cpydiff/types_float_implicit_conversion.py
index 3726839fac6..764c9e4e6ed 100644
--- a/tests/cpydiff/types_float_implicit_conversion.py
+++ b/tests/cpydiff/types_float_implicit_conversion.py
@@ -1,6 +1,6 @@
 """
 categories: Types,float
-description: uPy allows implicit conversion of objects in maths operations while CPython does not.
+description: MicroPython allows implicit conversion of objects in maths operations while CPython does not.
 cause: Unknown
 workaround: Objects should be wrapped in ``float(obj)`` for compatibility with CPython.
 """
diff --git a/tests/cpydiff/types_float_rounding.py b/tests/cpydiff/types_float_rounding.py
deleted file mode 100644
index a5b591966b0..00000000000
--- a/tests/cpydiff/types_float_rounding.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-categories: Types,float
-description: uPy and CPython outputs formats may differ
-cause: Unknown
-workaround: Unknown
-"""
-print("%.1g" % -9.9)
diff --git a/tests/cpydiff/types_list_delete_subscrstep.py b/tests/cpydiff/types_list_delete_subscrstep.py
index 36e6f526b3d..3c801d84949 100644
--- a/tests/cpydiff/types_list_delete_subscrstep.py
+++ b/tests/cpydiff/types_list_delete_subscrstep.py
@@ -4,6 +4,7 @@
 cause: Unknown
 workaround: Use explicit loop for this rare operation.
 """
+
 l = [1, 2, 3, 4]
 del l[0:4:2]
 print(l)
diff --git a/tests/cpydiff/types_list_store_noniter.py b/tests/cpydiff/types_list_store_noniter.py
index 1d69b4a8231..581b1369801 100644
--- a/tests/cpydiff/types_list_store_noniter.py
+++ b/tests/cpydiff/types_list_store_noniter.py
@@ -4,6 +4,7 @@
 cause: RHS is restricted to be a tuple or list
 workaround: Use ``list()`` on RHS to convert the iterable to a list
 """
+
 l = [10, 20]
 l[0:1] = range(4)
 print(l)
diff --git a/tests/cpydiff/types_list_store_subscrstep.py b/tests/cpydiff/types_list_store_subscrstep.py
index 1460372bb17..97590267f4a 100644
--- a/tests/cpydiff/types_list_store_subscrstep.py
+++ b/tests/cpydiff/types_list_store_subscrstep.py
@@ -4,6 +4,7 @@
 cause: Unknown
 workaround: Use explicit loop for this rare operation.
 """
+
 l = [1, 2, 3, 4]
 l[0:4:2] = [5, 6]
 print(l)
diff --git a/tests/cpydiff/types_memoryview_invalid.py b/tests/cpydiff/types_memoryview_invalid.py
new file mode 100644
index 00000000000..f288c50ab57
--- /dev/null
+++ b/tests/cpydiff/types_memoryview_invalid.py
@@ -0,0 +1,13 @@
+"""
+categories: Types,memoryview
+description: memoryview can become invalid if its target is resized
+cause: CPython prevents a ``bytearray`` or ``io.bytesIO`` object from changing size while there is a ``memoryview`` object that references it. MicroPython requires the programmer to manually ensure that an object is not resized while any ``memoryview`` references it.
+
+In the worst case scenario, resizing an object which is the target of a memoryview can cause the memoryview(s) to reference invalid freed memory (a use-after-free bug) and corrupt the MicroPython runtime.
+workaround: Do not change the size of any ``bytearray`` or ``io.bytesIO`` object that has a ``memoryview`` assigned to it.
+"""
+
+b = bytearray(b"abcdefg")
+m = memoryview(b)
+b.extend(b"hijklmnop")
+print(b, bytes(m))
diff --git a/tests/cpydiff/types_oserror_errnomap.py b/tests/cpydiff/types_oserror_errnomap.py
new file mode 100644
index 00000000000..6627bd2af4a
--- /dev/null
+++ b/tests/cpydiff/types_oserror_errnomap.py
@@ -0,0 +1,48 @@
+"""
+categories: Types,OSError
+description: OSError constructor returns a plain OSError for all errno values, rather than a relevant subtype.
+cause: MicroPython does not include the CPython-standard OSError subclasses.
+workaround: Catch OSError and use its errno attribute to discriminate the cause.
+"""
+
+import errno
+
+errno_list = [  # i.e. the set implemented by micropython
+    errno.EPERM,
+    errno.ENOENT,
+    errno.EIO,
+    errno.EBADF,
+    errno.EAGAIN,
+    errno.ENOMEM,
+    errno.EACCES,
+    errno.EEXIST,
+    errno.ENODEV,
+    errno.EISDIR,
+    errno.EINVAL,
+    errno.EOPNOTSUPP,
+    errno.EADDRINUSE,
+    errno.ECONNABORTED,
+    errno.ECONNRESET,
+    errno.ENOBUFS,
+    errno.ENOTCONN,
+    errno.ETIMEDOUT,
+    errno.ECONNREFUSED,
+    errno.EHOSTUNREACH,
+    errno.EALREADY,
+    errno.EINPROGRESS,
+]
+
+
+def errno_output_type(n):
+    try:
+        raise OSError(n, "")
+    except OSError as e:
+        return f"{type(e).__name__}"
+    except Exception as e:
+        return f"non-OSError {type(e).__name__}"
+    else:
+        return "no error"
+
+
+for n in errno_list:
+    print(errno.errorcode[n], "=", errno_output_type(n))
diff --git a/tests/cpydiff/types_range_limits.py b/tests/cpydiff/types_range_limits.py
new file mode 100644
index 00000000000..dca830d3fbc
--- /dev/null
+++ b/tests/cpydiff/types_range_limits.py
@@ -0,0 +1,32 @@
+"""
+categories: Types,range
+description: Range arguments must fit in a machine word; large start or stop values misbehave.
+cause: Range stores its arguments as the C mp_int_t type, and intermediate calculations also use it.
+workaround: Avoid using such ranges
+"""
+
+from sys import maxsize
+
+# A range including `maxsize-1` cannot be created
+try:
+    print(range(-maxsize - 1, 0))
+except OverflowError:
+    print("OverflowError")
+
+# A range with start or stop outside [-maxsize, maxsize] cannot be created, even if the range itself would be small.
+try:
+    print(range(maxsize + 1, maxsize + 2))
+except OverflowError:
+    print("OverflowError")
+
+# A range with `stop-start` exceeding sys.maxsize has incorrect len(), while CPython cannot calculate len().
+try:
+    print(len(range(-maxsize, maxsize)))
+except OverflowError:
+    print("OverflowError")
+
+# A range with `stop-start` exceeding sys.maxsize has incorrect len()
+try:
+    print(len(range(-maxsize, maxsize, maxsize)))
+except OverflowError:
+    print("OverflowError")
diff --git a/tests/cpydiff/types_str_encode_encoding.py b/tests/cpydiff/types_str_encode_encoding.py
new file mode 100644
index 00000000000..12f6b947262
--- /dev/null
+++ b/tests/cpydiff/types_str_encode_encoding.py
@@ -0,0 +1,21 @@
+"""
+categories: Types,str
+description: str.encode() constructor only supports encoding arguments 'utf8', 'utf-8' and 'ascii'. Other encodings like 'latin-1' are not supported. Other string forms such as 'UTF8' are not supported.
+cause: MicroPython is optimised for embedded systems and has limited codec support to save code size. `A similar restriction applies to bytes constructor `. See also `unicode_support`.
+workaround: Implement encoding conversions manually by parsing the result of str.encode() or bytes() constructor.
+"""
+
+# Both CPython and MicroPython can encode this emoji as UTF-8 bytes
+print("😀".encode("utf8"))
+
+# Both CPython and MicroPython will fail to encode this emoji as ASCII bytes
+try:
+    print("😀".encode("ascii"))
+except UnicodeError:
+    print("UnicodeError")
+
+# Other encodings or string formats aren't accepted by MicroPython:
+try:
+    print("😀".encode("UTF-8"))
+except LookupError:
+    print("LookupError")
diff --git a/tests/cpydiff/types_str_encode_errors.py b/tests/cpydiff/types_str_encode_errors.py
new file mode 100644
index 00000000000..2e2550b0538
--- /dev/null
+++ b/tests/cpydiff/types_str_encode_errors.py
@@ -0,0 +1,19 @@
+"""
+categories: Types,str
+description: str.encode() and bytes() constructor ignore any ``errors`` argument specified. If the encoding is specified as ``'ascii'`` and a non-ASCII byte is found in the string then an exception is always raised.
+cause: MicroPython is optimised for embedded systems and has limited codec support to save code size. See also `unicode_support`.
+workaround: Handle encoding errors manually by parsing the result of str.encode() or bytes() constructor.
+"""
+
+# CPython will replace the emoji with an ASCII '?' but MicroPython will
+# raise an exception
+try:
+    print("😀".encode("ascii", "replace"))
+except UnicodeError:
+    print("UnicodeError")
+
+# CPython will ignore the emoji in the result but MicroPython will raise an exception
+try:
+    print("😀".encode("ascii", "ignore"))
+except UnicodeError:
+    print("UnicodeError")
diff --git a/tests/cpydiff/types_str_encoding.py b/tests/cpydiff/types_str_encoding.py
new file mode 100644
index 00000000000..b97b984c22e
--- /dev/null
+++ b/tests/cpydiff/types_str_encoding.py
@@ -0,0 +1,30 @@
+"""
+categories: Types,str
+description: str() constructor only supports encoding arguments 'utf8', 'utf-8' and 'ascii'. Other encodings like 'latin-1' are not supported. Other string forms such as 'UTF8' are not supported.
+cause: MicroPython is optimised for embedded systems and only includes UTF-8 and ASCII codec support with simple matching to save memory and code size. `The same restriction applies to bytes.decode() `. See also `unicode_support`.
+workaround: Convert data to UTF-8 before processing, or implement custom encoding/decoding if needed. Ensure encoding argument is one of the accepted forms.
+"""
+
+# Both CPython and MicroPython support utf8 and ascii encodings
+print(str(b"caf\xc3\xa9", "utf8"))  # codespell:ignore caf
+print(str(b"cafe", "ascii"))
+
+# MicroPython does not support additional encodings
+try:
+    str(b"\xe9", "latin-1")  # 'é' in latin-1
+    print("latin-1 supported")
+except (ValueError, NotImplementedError, LookupError) as e:
+    print("latin-1 not supported:", type(e).__name__)
+
+try:
+    str(b"\x80", "cp1252")  # Euro sign in cp1252
+    print("cp1252 supported")
+except (ValueError, NotImplementedError, LookupError) as e:
+    print("cp1252 not supported:", type(e).__name__)
+
+# Encoding arguments must match exactly in MicroPython
+try:
+    str(b"hello", "ASCII")
+    print("Capital letters ASCII supported")
+except (ValueError, NotImplementedError, LookupError) as e:
+    print("Capital letters ASCII not supported:", type(e).__name__)
diff --git a/tests/cpydiff/types_str_endswith.py b/tests/cpydiff/types_str_endswith.py
deleted file mode 100644
index f222ac1cd3a..00000000000
--- a/tests/cpydiff/types_str_endswith.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-categories: Types,str
-description: Start/end indices such as str.endswith(s, start) not implemented
-cause: Unknown
-workaround: Unknown
-"""
-print("abc".endswith("c", 1))
diff --git a/tests/cpydiff/types_str_formatsep.py b/tests/cpydiff/types_str_formatsep.py
new file mode 100644
index 00000000000..05d0b8d3d2c
--- /dev/null
+++ b/tests/cpydiff/types_str_formatsep.py
@@ -0,0 +1,19 @@
+"""
+categories: Types,str
+description: MicroPython accepts the "," grouping option with any radix, unlike CPython
+cause: To reduce code size, MicroPython does not issue an error for this combination
+workaround: Do not use a format string like ``{:,b}`` if CPython compatibility is required.
+"""
+
+try:
+    print("{:,b}".format(99))
+except ValueError:
+    print("ValueError")
+try:
+    print("{:,x}".format(99))
+except ValueError:
+    print("ValueError")
+try:
+    print("{:,o}".format(99))
+except ValueError:
+    print("ValueError")
diff --git a/tests/cpydiff/types_str_formatsep_float.py b/tests/cpydiff/types_str_formatsep_float.py
new file mode 100644
index 00000000000..b487cd3758e
--- /dev/null
+++ b/tests/cpydiff/types_str_formatsep_float.py
@@ -0,0 +1,11 @@
+"""
+categories: Types,str
+description: MicroPython accepts but does not properly implement the "," or "_" grouping character for float values
+cause: To reduce code size, MicroPython does not implement this combination. Grouping characters will not appear in the number's significant digits and will appear at incorrect locations in leading zeros.
+workaround: Do not use a format string like ``{:,f}`` if exact CPython compatibility is required.
+"""
+
+print("{:,f}".format(3141.159))
+print("{:_f}".format(3141.159))
+print("{:011,.2f}".format(3141.159))
+print("{:011_.2f}".format(3141.159))
diff --git a/tests/cpydiff/types_str_formatsubscr.py b/tests/cpydiff/types_str_formatsubscr.py
index 1b83cfff6cd..5c59a1d200a 100644
--- a/tests/cpydiff/types_str_formatsubscr.py
+++ b/tests/cpydiff/types_str_formatsubscr.py
@@ -4,4 +4,5 @@
 cause: Unknown
 workaround: Unknown
 """
+
 print("{a[0]}".format(a=[1, 2]))
diff --git a/tests/cpydiff/types_str_keywords.py b/tests/cpydiff/types_str_keywords.py
index 77a4eac1c1d..640dfa6c391 100644
--- a/tests/cpydiff/types_str_keywords.py
+++ b/tests/cpydiff/types_str_keywords.py
@@ -4,4 +4,5 @@
 cause: Unknown
 workaround: Input the encoding format directly. eg ``print(bytes('abc', 'utf-8'))``
 """
+
 print(str(b"abc", encoding="utf8"))
diff --git a/tests/cpydiff/types_str_ljust_rjust.py b/tests/cpydiff/types_str_ljust_rjust.py
index 72e5105e025..7f91c137e90 100644
--- a/tests/cpydiff/types_str_ljust_rjust.py
+++ b/tests/cpydiff/types_str_ljust_rjust.py
@@ -4,4 +4,5 @@
 cause: MicroPython is highly optimized for memory usage. Easy workarounds available.
 workaround: Instead of ``s.ljust(10)`` use ``"%-10s" % s``, instead of ``s.rjust(10)`` use ``"% 10s" % s``. Alternatively, ``"{:<10}".format(s)`` or ``"{:>10}".format(s)``.
 """
+
 print("abc".ljust(10))
diff --git a/tests/cpydiff/types_str_repr_nonprintable.py b/tests/cpydiff/types_str_repr_nonprintable.py
new file mode 100644
index 00000000000..6f9af65d8e1
--- /dev/null
+++ b/tests/cpydiff/types_str_repr_nonprintable.py
@@ -0,0 +1,11 @@
+"""
+categories: Types,str
+description: repr() may print some non-printable Unicode characters literally instead of as escape sequences
+cause: MicroPython uses a simplified heuristic to determine printable characters, avoiding the need for a full Unicode character database (saves memory). It prints characters >= U+0080 (excluding surrogates) as UTF-8. CPython uses the Unicode database to identify non-printable characters like noncharacters (U+FFFx in each plane).
+workaround: Accept the difference for embedded use cases, or use ascii() or manual escaping if exact control is needed.
+"""
+
+# These are noncharacters that CPython escapes but MicroPython prints
+# showing as hex to avoid display issues in documentation tables
+print("U+FFFF:", repr("\uffff").encode("utf-8").hex())
+print("U+1FFFF:", repr("\U0001ffff").encode("utf-8").hex())
diff --git a/tests/cpydiff/types_str_rsplitnone.py b/tests/cpydiff/types_str_rsplitnone.py
index 5d334fea2f8..ae524620a08 100644
--- a/tests/cpydiff/types_str_rsplitnone.py
+++ b/tests/cpydiff/types_str_rsplitnone.py
@@ -4,4 +4,5 @@
 cause: Unknown
 workaround: Unknown
 """
+
 print("a a a".rsplit(None, 1))
diff --git a/tests/cpydiff/types_str_subscrstep.py b/tests/cpydiff/types_str_subscrstep.py
index 2d3245e5582..cd57f18ccb3 100644
--- a/tests/cpydiff/types_str_subscrstep.py
+++ b/tests/cpydiff/types_str_subscrstep.py
@@ -4,4 +4,5 @@
 cause: Unknown
 workaround: Unknown
 """
+
 print("abcdefghi"[0:9:2])
diff --git a/tests/cpydiff/types_tuple_subscrstep.py b/tests/cpydiff/types_tuple_subscrstep.py
index f90f3c5bf4d..60ba31af4cf 100644
--- a/tests/cpydiff/types_tuple_subscrstep.py
+++ b/tests/cpydiff/types_tuple_subscrstep.py
@@ -4,4 +4,5 @@
 cause: Unknown
 workaround: Unknown
 """
+
 print((1, 2, 3, 4)[0:4:2])
diff --git a/tests/esp32/esp32_idf_heap_info.py b/tests/esp32/esp32_idf_heap_info.py
deleted file mode 100644
index fdd89161f43..00000000000
--- a/tests/esp32/esp32_idf_heap_info.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Test the esp32's esp32.idf_heap_info to return sane data
-try:
-    import esp32
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-
-# region tuple is: (size, free, largest free, min free)
-# we check that each region's size is > 0 and that the free amounts are smaller than the size
-def chk_heap(kind, regions):
-    chk = [True, True, True, True]
-    for r in regions:
-        chk = [
-            chk[0] and r[0] > 0,
-            chk[1] and r[1] <= r[0],
-            chk[2] and r[2] <= r[0],
-            chk[3] and r[3] <= r[0],
-        ]
-    print(kind, chk)
-
-
-# try getting heap regions
-regions = esp32.idf_heap_info(esp32.HEAP_DATA)
-print("HEAP_DATA >2:", len(regions) > 2)
-chk_heap("HEAP_DATA", regions)
-
-# try getting code regions
-regions = esp32.idf_heap_info(esp32.HEAP_EXEC)
-print("HEAP_EXEC >2:", len(regions) > 2)
-chk_heap("HEAP_EXEC", regions)
-
-# try invalid param
-print(esp32.idf_heap_info(-1))
diff --git a/tests/esp32/esp32_idf_heap_info.py.exp b/tests/esp32/esp32_idf_heap_info.py.exp
deleted file mode 100644
index 2b63bf3259f..00000000000
--- a/tests/esp32/esp32_idf_heap_info.py.exp
+++ /dev/null
@@ -1,5 +0,0 @@
-HEAP_DATA >2: True
-HEAP_DATA [True, True, True, True]
-HEAP_EXEC >2: True
-HEAP_EXEC [True, True, True, True]
-[]
diff --git a/tests/esp32/resolve_on_connect.py b/tests/esp32/resolve_on_connect.py
deleted file mode 100644
index 068757ab2ab..00000000000
--- a/tests/esp32/resolve_on_connect.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# Test that the esp32's socket module performs DNS resolutions on bind and connect
-import sys
-
-if sys.implementation.name == "micropython" and sys.platform != "esp32":
-    print("SKIP")
-    raise SystemExit
-
-try:
-    import usocket as socket, sys
-except:
-    import socket, sys
-
-
-def test_bind_resolves_0_0_0_0():
-    s = socket.socket()
-    try:
-        s.bind(("0.0.0.0", 31245))
-        print("bind actually bound")
-        s.close()
-    except Exception as e:
-        print("bind raised", e)
-
-
-def test_bind_resolves_localhost():
-    s = socket.socket()
-    try:
-        s.bind(("localhost", 31245))
-        print("bind actually bound")
-        s.close()
-    except Exception as e:
-        print("bind raised", e)
-
-
-def test_connect_resolves():
-    s = socket.socket()
-    try:
-        s.connect(("micropython.org", 80))
-        print("connect actually connected")
-        s.close()
-    except Exception as e:
-        print("connect raised", e)
-
-
-def test_connect_non_existent():
-    s = socket.socket()
-    try:
-        s.connect(("nonexistent.example.com", 80))
-        print("connect actually connected")
-        s.close()
-    except OSError as e:
-        print("connect raised OSError")
-    except Exception as e:
-        print("connect raised", e)
-
-
-test_funs = [n for n in dir() if n.startswith("test_")]
-for f in sorted(test_funs):
-    print("--", f, end=": ")
-    eval(f + "()")
diff --git a/tests/extmod/asyncio_as_uasyncio.py b/tests/extmod/asyncio_as_uasyncio.py
new file mode 100644
index 00000000000..b021980590c
--- /dev/null
+++ b/tests/extmod/asyncio_as_uasyncio.py
@@ -0,0 +1,33 @@
+try:
+    import uasyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+# Sample of public symbols we expect to see from `asyncio`. Verify they're all
+# available on `uasyncio`.
+expected = [
+    "CancelledError",
+    "create_task",
+    "current_task",
+    "Event",
+    "gather",
+    "get_event_loop",
+    "Lock",
+    "Loop",
+    "open_connection",
+    "run",
+    "run_until_complete",
+    "sleep",
+    "sleep_ms",
+    "start_server",
+    "StreamReader",
+    "StreamWriter",
+    "Task",
+    "ThreadSafeFlag",
+    "wait_for",
+]
+
+for e in expected:
+    getattr(uasyncio, e)
diff --git a/tests/feature_check/bytearray.py.exp b/tests/extmod/asyncio_as_uasyncio.py.exp
similarity index 100%
rename from tests/feature_check/bytearray.py.exp
rename to tests/extmod/asyncio_as_uasyncio.py.exp
diff --git a/tests/extmod/asyncio_await_return.py b/tests/extmod/asyncio_await_return.py
new file mode 100644
index 00000000000..35f214b99a9
--- /dev/null
+++ b/tests/extmod/asyncio_await_return.py
@@ -0,0 +1,23 @@
+# Test that tasks return their value correctly to the caller
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def foo():
+    return 42
+
+
+async def main():
+    # Call function directly via an await
+    print(await foo())
+
+    # Create a task and await on it
+    task = asyncio.create_task(foo())
+    print(await task)
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_basic.py b/tests/extmod/asyncio_basic.py
new file mode 100644
index 00000000000..ca37c7bcb02
--- /dev/null
+++ b/tests/extmod/asyncio_basic.py
@@ -0,0 +1,45 @@
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+import time
+
+if hasattr(time, "ticks_ms"):
+    ticks = time.ticks_ms
+    ticks_diff = time.ticks_diff
+else:
+    ticks = lambda: int(time.time() * 1000)
+    ticks_diff = lambda t1, t0: t1 - t0
+
+
+async def delay_print(t, s):
+    await asyncio.sleep(t)
+    print(s)
+
+
+async def main():
+    print("start")
+
+    await asyncio.sleep(0.001)
+    print("after sleep")
+
+    t0 = ticks()
+    await delay_print(0.2, "short")
+    t1 = ticks()
+    await delay_print(0.4, "long")
+    t2 = ticks()
+    await delay_print(-1, "negative")
+    t3 = ticks()
+
+    print(
+        "took {} {} {}".format(
+            round(ticks_diff(t1, t0), -2),
+            round(ticks_diff(t2, t1), -2),
+            round(ticks_diff(t3, t2), -2),
+        )
+    )
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_basic2.py b/tests/extmod/asyncio_basic2.py
new file mode 100644
index 00000000000..3040b6aa51b
--- /dev/null
+++ b/tests/extmod/asyncio_basic2.py
@@ -0,0 +1,21 @@
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def forever():
+    print("forever start")
+    await asyncio.sleep(10)
+
+
+async def main():
+    print("main start")
+    asyncio.create_task(forever())
+    await asyncio.sleep(0.001)
+    print("main done")
+    return 42
+
+
+print(asyncio.run(main()))
diff --git a/tests/extmod/asyncio_cancel_fair.py b/tests/extmod/asyncio_cancel_fair.py
new file mode 100644
index 00000000000..a1e35f82ba6
--- /dev/null
+++ b/tests/extmod/asyncio_cancel_fair.py
@@ -0,0 +1,34 @@
+# Test fairness of cancelling a task
+# That tasks which continuously cancel each other don't take over the scheduler
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task(id, other):
+    for i in range(3):
+        try:
+            print("start", id)
+            await asyncio.sleep(0)
+            print("done", id)
+        except asyncio.CancelledError as er:
+            print("cancelled", id)
+        if other is not None:
+            print(id, "cancels", other)
+            tasks[other].cancel()
+
+
+async def main():
+    global tasks
+    tasks = [
+        asyncio.create_task(task(0, 1)),
+        asyncio.create_task(task(1, 0)),
+        asyncio.create_task(task(2, None)),
+    ]
+    await tasks[2]
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_cancel_fair2.py b/tests/extmod/asyncio_cancel_fair2.py
new file mode 100644
index 00000000000..577823204fa
--- /dev/null
+++ b/tests/extmod/asyncio_cancel_fair2.py
@@ -0,0 +1,34 @@
+# Test fairness of cancelling a task
+# That tasks which keeps being cancelled by multiple other tasks gets a chance to run
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task_a():
+    try:
+        while True:
+            print("sleep a")
+            await asyncio.sleep(0)
+    except asyncio.CancelledError:
+        print("cancelled a")
+
+
+async def task_b(id, other):
+    while other.cancel():
+        print("sleep b", id)
+        await asyncio.sleep(0)
+    print("done b", id)
+
+
+async def main():
+    t = asyncio.create_task(task_a())
+    for i in range(3):
+        asyncio.create_task(task_b(i, t))
+    await t
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_cancel_self.py b/tests/extmod/asyncio_cancel_self.py
new file mode 100644
index 00000000000..3ed3ee88214
--- /dev/null
+++ b/tests/extmod/asyncio_cancel_self.py
@@ -0,0 +1,28 @@
+# Test a task cancelling itself (currently unsupported)
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task():
+    print("task start")
+    global_task.cancel()
+
+
+async def main():
+    global global_task
+    global_task = asyncio.create_task(task())
+    try:
+        await global_task
+    except asyncio.CancelledError:
+        print("main cancel")
+    print("main done")
+
+
+try:
+    asyncio.run(main())
+except RuntimeError as er:
+    print(str(er) or "can't cancel self")
diff --git a/tests/extmod/uasyncio_cancel_self.py.exp b/tests/extmod/asyncio_cancel_self.py.exp
similarity index 100%
rename from tests/extmod/uasyncio_cancel_self.py.exp
rename to tests/extmod/asyncio_cancel_self.py.exp
diff --git a/tests/extmod/asyncio_cancel_task.py b/tests/extmod/asyncio_cancel_task.py
new file mode 100644
index 00000000000..9250f46b51c
--- /dev/null
+++ b/tests/extmod/asyncio_cancel_task.py
@@ -0,0 +1,82 @@
+# Test cancelling a task
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task(s, allow_cancel):
+    try:
+        print("task start")
+        await asyncio.sleep(s)
+        print("task done")
+    except asyncio.CancelledError as er:
+        print("task cancel")
+        if allow_cancel:
+            raise er
+
+
+async def task2(allow_cancel):
+    print("task 2")
+    try:
+        await asyncio.create_task(task(0.05, allow_cancel))
+    except asyncio.CancelledError as er:
+        print("task 2 cancel")
+        raise er
+    print("task 2 done")
+
+
+async def main():
+    # Cancel task immediately
+    t = asyncio.create_task(task(2, True))
+    print(t.cancel())
+
+    # Cancel task after it has started
+    t = asyncio.create_task(task(2, True))
+    await asyncio.sleep(0.01)
+    print(t.cancel())
+    print("main sleep")
+    await asyncio.sleep(0.01)
+
+    # Cancel task multiple times after it has started
+    t = asyncio.create_task(task(2, True))
+    await asyncio.sleep(0.01)
+    for _ in range(4):
+        print(t.cancel())
+    print("main sleep")
+    await asyncio.sleep(0.01)
+
+    # Await on a cancelled task
+    print("main wait")
+    try:
+        await t
+    except asyncio.CancelledError:
+        print("main got CancelledError")
+
+    # Cancel task after it has finished
+    t = asyncio.create_task(task(0.01, False))
+    await asyncio.sleep(0.05)
+    print(t.cancel())
+
+    # Nested: task2 waits on task, task2 is cancelled (should cancel task then task2)
+    print("----")
+    t = asyncio.create_task(task2(True))
+    await asyncio.sleep(0.01)
+    print("main cancel")
+    t.cancel()
+    print("main sleep")
+    await asyncio.sleep(0.1)
+
+    # Nested: task2 waits on task, task2 is cancelled but task doesn't allow it (task2 should continue)
+    print("----")
+    t = asyncio.create_task(task2(False))
+    await asyncio.sleep(0.01)
+    print("main cancel")
+    t.cancel()
+    print("main sleep")
+    await asyncio.sleep(0.1)
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_cancel_wait_on_finished.py b/tests/extmod/asyncio_cancel_wait_on_finished.py
new file mode 100644
index 00000000000..c3529b8d7aa
--- /dev/null
+++ b/tests/extmod/asyncio_cancel_wait_on_finished.py
@@ -0,0 +1,38 @@
+# Test cancelling a task that is waiting on a task that just finishes.
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def sleep_task():
+    print("sleep_task sleep")
+    await asyncio.sleep(0)
+    print("sleep_task wake")
+
+
+async def wait_task(t):
+    print("wait_task wait")
+    await t
+    print("wait_task wake")
+
+
+async def main():
+    waiting_task = asyncio.create_task(wait_task(asyncio.create_task(sleep_task())))
+
+    print("main sleep")
+    await asyncio.sleep(0)
+    print("main sleep")
+    await asyncio.sleep(0)
+
+    waiting_task.cancel()
+    print("main wait")
+    try:
+        await waiting_task
+    except asyncio.CancelledError as er:
+        print(repr(er))
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_current_task.py b/tests/extmod/asyncio_current_task.py
new file mode 100644
index 00000000000..a25e543d0c5
--- /dev/null
+++ b/tests/extmod/asyncio_current_task.py
@@ -0,0 +1,33 @@
+# Test current_task() function
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task(result):
+    result[0] = asyncio.current_task()
+
+
+async def main():
+    result = [None]
+    t = asyncio.create_task(task(result))
+    await asyncio.sleep(0)
+    await asyncio.sleep(0)
+    print(t is result[0])
+
+
+try:
+    print(asyncio.current_task())
+except RuntimeError:
+    print("RuntimeError")
+
+
+asyncio.run(main())
+
+try:
+    print(asyncio.current_task())
+except RuntimeError:
+    print("RuntimeError")
diff --git a/tests/extmod/asyncio_event.py b/tests/extmod/asyncio_event.py
new file mode 100644
index 00000000000..290323a7de3
--- /dev/null
+++ b/tests/extmod/asyncio_event.py
@@ -0,0 +1,95 @@
+# Test Event class
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task(id, ev):
+    print("start", id)
+    print(await ev.wait())
+    print("end", id)
+
+
+async def task_delay_set(t, ev):
+    await asyncio.sleep(t)
+    print("set event")
+    ev.set()
+
+
+async def main():
+    ev = asyncio.Event()
+
+    # Set and clear without anything waiting, and test is_set()
+    print(ev.is_set())
+    ev.set()
+    print(ev.is_set())
+    ev.clear()
+    print(ev.is_set())
+
+    # Create 2 tasks waiting on the event
+    print("----")
+    asyncio.create_task(task(1, ev))
+    asyncio.create_task(task(2, ev))
+    print("yield")
+    await asyncio.sleep(0)
+    print("set event")
+    ev.set()
+    print("yield")
+    await asyncio.sleep(0)
+
+    # Create a task waiting on the already-set event
+    print("----")
+    asyncio.create_task(task(3, ev))
+    print("yield")
+    await asyncio.sleep(0)
+
+    # Clear event, start a task, then set event again
+    print("----")
+    print("clear event")
+    ev.clear()
+    asyncio.create_task(task(4, ev))
+    await asyncio.sleep(0)
+    print("set event")
+    ev.set()
+    await asyncio.sleep(0)
+
+    # Cancel a task waiting on an event (set event then cancel task)
+    print("----")
+    ev = asyncio.Event()
+    t = asyncio.create_task(task(5, ev))
+    await asyncio.sleep(0)
+    ev.set()
+    t.cancel()
+    await asyncio.sleep(0.1)
+
+    # Cancel a task waiting on an event (cancel task then set event)
+    print("----")
+    ev = asyncio.Event()
+    t = asyncio.create_task(task(6, ev))
+    await asyncio.sleep(0)
+    t.cancel()
+    ev.set()
+    await asyncio.sleep(0.1)
+
+    # Wait for an event that does get set in time
+    print("----")
+    ev.clear()
+    asyncio.create_task(task_delay_set(0.01, ev))
+    await asyncio.wait_for(ev.wait(), 0.1)
+    await asyncio.sleep(0)
+
+    # Wait for an event that doesn't get set in time
+    print("----")
+    ev.clear()
+    asyncio.create_task(task_delay_set(0.1, ev))
+    try:
+        await asyncio.wait_for(ev.wait(), 0.01)
+    except asyncio.TimeoutError:
+        print("TimeoutError")
+    await ev.wait()
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_event_fair.py b/tests/extmod/asyncio_event_fair.py
new file mode 100644
index 00000000000..ed34f3f0b5a
--- /dev/null
+++ b/tests/extmod/asyncio_event_fair.py
@@ -0,0 +1,37 @@
+# Test fairness of Event.set()
+# That tasks which continuously wait on events don't take over the scheduler
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task1(id):
+    for i in range(4):
+        print("sleep", id)
+        await asyncio.sleep(0)
+
+
+async def task2(id, ev):
+    for i in range(4):
+        ev.set()
+        ev.clear()
+        print("wait", id)
+        await ev.wait()
+
+
+async def main():
+    ev = asyncio.Event()
+    tasks = [
+        asyncio.create_task(task1(0)),
+        asyncio.create_task(task2(2, ev)),
+        asyncio.create_task(task1(1)),
+        asyncio.create_task(task2(3, ev)),
+    ]
+    await tasks[1]
+    ev.set()
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_event_queue.py b/tests/extmod/asyncio_event_queue.py
new file mode 100644
index 00000000000..e0125b1aefe
--- /dev/null
+++ b/tests/extmod/asyncio_event_queue.py
@@ -0,0 +1,64 @@
+# Ensure that an asyncio task can wait on an Event when the
+# _task_queue is empty
+# https://github.com/micropython/micropython/issues/16569
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# This test requires checking that the asyncio scheduler
+# remains active "indefinitely" when the task queue is empty.
+#
+# To check this, we need another independent scheduler that
+# can wait for a certain amount of time. So we have to
+# create one using micropython.schedule() and time.ticks_ms()
+#
+# Technically, this code breaks the rules, as it is clearly
+# documented that Event.set() should _NOT_ be called from a
+# schedule (soft IRQ) because in some cases, a race condition
+# can occur, resulting in a crash. However:
+# - since the risk of a race condition in that specific
+#   case has been analysed and excluded
+# - given that there is no other simple alternative to
+#   write this test case,
+# an exception to the rule was deemed acceptable. See
+# https://github.com/micropython/micropython/pull/16772
+
+import micropython, time
+
+try:
+    micropython.schedule
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+
+evt = asyncio.Event()
+
+
+def schedule_watchdog(end_ticks):
+    if time.ticks_diff(end_ticks, time.ticks_ms()) <= 0:
+        print("asyncio still pending, unlocking event")
+        # Caution: about to call Event.set() from a schedule
+        #          (see the note in the comment above)
+        evt.set()
+        return
+    micropython.schedule(schedule_watchdog, end_ticks)
+
+
+async def foo():
+    print("foo waiting")
+    schedule_watchdog(time.ticks_add(time.ticks_ms(), 100))
+    await evt.wait()
+    print("foo done")
+
+
+async def main():
+    print("main started")
+    await foo()
+    print("main done")
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_event_queue.py.exp b/tests/extmod/asyncio_event_queue.py.exp
new file mode 100644
index 00000000000..ee42c96d83e
--- /dev/null
+++ b/tests/extmod/asyncio_event_queue.py.exp
@@ -0,0 +1,5 @@
+main started
+foo waiting
+asyncio still pending, unlocking event
+foo done
+main done
diff --git a/tests/extmod/asyncio_exception.py b/tests/extmod/asyncio_exception.py
new file mode 100644
index 00000000000..6f19509bac9
--- /dev/null
+++ b/tests/extmod/asyncio_exception.py
@@ -0,0 +1,60 @@
+# Test general exception handling
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+# main task raising an exception
+async def main():
+    print("main start")
+    raise ValueError(1)
+    print("main done")
+
+
+try:
+    asyncio.run(main())
+except ValueError as er:
+    print("ValueError", er.args[0])
+
+
+# sub-task raising an exception
+async def task():
+    print("task start")
+    raise ValueError(2)
+    print("task done")
+
+
+async def main():
+    print("main start")
+    t = asyncio.create_task(task())
+    await t
+    print("main done")
+
+
+try:
+    asyncio.run(main())
+except ValueError as er:
+    print("ValueError", er.args[0])
+
+
+# main task raising an exception with sub-task not yet scheduled
+# TODO not currently working, task is never scheduled
+async def task():
+    # print('task run') uncomment this line when it works
+    pass
+
+
+async def main():
+    print("main start")
+    asyncio.create_task(task())
+    raise ValueError(3)
+    print("main done")
+
+
+try:
+    asyncio.run(main())
+except ValueError as er:
+    print("ValueError", er.args[0])
diff --git a/tests/extmod/asyncio_fair.py b/tests/extmod/asyncio_fair.py
new file mode 100644
index 00000000000..3e3ac047358
--- /dev/null
+++ b/tests/extmod/asyncio_fair.py
@@ -0,0 +1,31 @@
+# Test fairness of scheduler
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task(id, t):
+    print("task start", id)
+    while True:
+        if t > 0:
+            print("task work", id)
+        await asyncio.sleep(t)
+
+
+async def main():
+    t1 = asyncio.create_task(task(1, -0.01))
+    t2 = asyncio.create_task(task(2, 0.1))
+    t3 = asyncio.create_task(task(3, 0.18))
+    t4 = asyncio.create_task(task(4, -100))
+    await asyncio.sleep(0.45)  # t2 prints 5 times, t3 prints 3 times
+    t1.cancel()
+    t2.cancel()
+    t3.cancel()
+    t4.cancel()
+    print("finish")
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_gather.py b/tests/extmod/asyncio_gather.py
new file mode 100644
index 00000000000..78a40ba3aad
--- /dev/null
+++ b/tests/extmod/asyncio_gather.py
@@ -0,0 +1,111 @@
+# test asyncio.gather() function
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def factorial(name, number):
+    f = 1
+    for i in range(2, number + 1):
+        print("Task {}: Compute factorial({})...".format(name, i))
+        await asyncio.sleep(0.01)
+        f *= i
+    print("Task {}: factorial({}) = {}".format(name, number, f))
+    return f
+
+
+async def task(id, t=0.1):
+    print("start", id)
+    await asyncio.sleep(t)
+    print("end", id)
+    return id
+
+
+async def task_loop(id):
+    print("task_loop start", id)
+    while True:
+        await asyncio.sleep(0.1)
+        print("task_loop loop", id)
+
+
+async def task_raise(id, t=0.1):
+    print("task_raise start", id)
+    await asyncio.sleep(t)
+    print("task_raise raise", id)
+    raise ValueError(id)
+
+
+async def gather_task(t0, t1):
+    print("gather_task")
+    await asyncio.gather(t0, t1)
+    print("gather_task2")
+
+
+async def main():
+    # Simple gather with return values
+    print(await asyncio.gather(factorial("A", 2), factorial("B", 3), factorial("C", 4)))
+
+    print("====")
+
+    # Gather with no awaitables
+    print(await asyncio.gather())
+
+    print("====")
+
+    # Test return_exceptions, where one task is cancelled and the other finishes normally
+    tasks = [asyncio.create_task(task(1)), asyncio.create_task(task(2))]
+    tasks[0].cancel()
+    print(await asyncio.gather(*tasks, return_exceptions=True))
+
+    print("====")
+
+    # Test return_exceptions, where one task raises an exception and the other finishes normally.
+    tasks = [asyncio.create_task(task(1)), asyncio.create_task(task_raise(2))]
+    print(await asyncio.gather(*tasks, return_exceptions=True))
+
+    print("====")
+
+    # Test case where one task raises an exception and other task keeps running.
+    tasks = [asyncio.create_task(task_loop(1)), asyncio.create_task(task_raise(2))]
+    try:
+        await asyncio.gather(*tasks)
+    except ValueError as er:
+        print(repr(er))
+    print(tasks[0].done(), tasks[1].done())
+    for t in tasks:
+        t.cancel()
+    await asyncio.sleep(0.2)
+
+    print("====")
+
+    # Test case where both tasks raise an exception.
+    # Use t=0 so they raise one after the other, between the gather starting and finishing.
+    tasks = [asyncio.create_task(task_raise(1, t=0)), asyncio.create_task(task_raise(2, t=0))]
+    try:
+        await asyncio.gather(*tasks)
+    except ValueError as er:
+        print(repr(er))
+    print(tasks[0].done(), tasks[1].done())
+
+    print("====")
+
+    # Cancel a multi gather.
+    t = asyncio.create_task(gather_task(task(1), task(2)))
+    await asyncio.sleep(0.05)
+    t.cancel()
+    await asyncio.sleep(0.2)
+
+    # Test edge cases where the gather is cancelled just as tasks are created and ending.
+    for i in range(1, 4):
+        print("====")
+        t = asyncio.create_task(gather_task(task(1, t=0), task(2, t=0)))
+        for _ in range(i):
+            await asyncio.sleep(0)
+        t.cancel()
+        await asyncio.sleep(0.2)
+
+
+asyncio.run(main())
diff --git a/tests/extmod/uasyncio_gather.py.exp b/tests/extmod/asyncio_gather.py.exp
similarity index 100%
rename from tests/extmod/uasyncio_gather.py.exp
rename to tests/extmod/asyncio_gather.py.exp
diff --git a/tests/extmod/asyncio_gather_finished_early.py b/tests/extmod/asyncio_gather_finished_early.py
new file mode 100644
index 00000000000..030e79e3571
--- /dev/null
+++ b/tests/extmod/asyncio_gather_finished_early.py
@@ -0,0 +1,65 @@
+# Test asyncio.gather() when a task is already finished before the gather starts.
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+# CPython and MicroPython differ in when they signal (and print) that a task raised an
+# uncaught exception.  So define an empty custom_handler() to suppress this output.
+def custom_handler(loop, context):
+    pass
+
+
+async def task_that_finishes_early(id, event, fail):
+    print("task_that_finishes_early", id)
+    event.set()
+    if fail:
+        raise ValueError("intentional exception", id)
+
+
+async def task_that_runs():
+    for i in range(5):
+        print("task_that_runs", i)
+        await asyncio.sleep(0)
+
+
+async def main(start_task_that_runs, task_fail, return_exceptions):
+    print("== start", start_task_that_runs, task_fail, return_exceptions)
+
+    # Set exception handler to suppress exception output.
+    loop = asyncio.get_event_loop()
+    loop.set_exception_handler(custom_handler)
+
+    # Create tasks.
+    event_a = asyncio.Event()
+    event_b = asyncio.Event()
+    tasks = []
+    if start_task_that_runs:
+        tasks.append(asyncio.create_task(task_that_runs()))
+    tasks.append(asyncio.create_task(task_that_finishes_early("a", event_a, task_fail)))
+    tasks.append(asyncio.create_task(task_that_finishes_early("b", event_b, task_fail)))
+
+    # Make sure task_that_finishes_early() are both done, before calling gather().
+    await event_a.wait()
+    await event_b.wait()
+
+    # Gather the tasks.
+    try:
+        result = "complete", await asyncio.gather(*tasks, return_exceptions=return_exceptions)
+    except Exception as er:
+        result = "exception", er, start_task_that_runs and tasks[0].done()
+
+    # Wait for the final task to finish (if it was started).
+    if start_task_that_runs:
+        await tasks[0]
+
+    # Print results.
+    print(result)
+
+
+# Run the test in the 8 different combinations of its arguments.
+for i in range(8):
+    asyncio.run(main(bool(i & 4), bool(i & 2), bool(i & 1)))
diff --git a/tests/extmod/asyncio_gather_notimpl.py b/tests/extmod/asyncio_gather_notimpl.py
new file mode 100644
index 00000000000..98f9a9368f9
--- /dev/null
+++ b/tests/extmod/asyncio_gather_notimpl.py
@@ -0,0 +1,50 @@
+# Test asyncio.gather() function, features that are not implemented.
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def custom_handler(loop, context):
+    print(repr(context["exception"]))
+
+
+async def task(id):
+    print("task start", id)
+    await asyncio.sleep(0.01)
+    print("task end", id)
+    return id
+
+
+async def gather_task(t0, t1):
+    print("gather_task start")
+    await asyncio.gather(t0, t1)
+    print("gather_task end")
+
+
+async def main():
+    loop = asyncio.get_event_loop()
+    loop.set_exception_handler(custom_handler)
+
+    # Test case where can't wait on a task being gathered.
+    tasks = [asyncio.create_task(task(1)), asyncio.create_task(task(2))]
+    gt = asyncio.create_task(gather_task(tasks[0], tasks[1]))
+    await asyncio.sleep(0)  # let the gather start
+    try:
+        await tasks[0]  # can't await because this task is part of the gather
+    except RuntimeError as er:
+        print(repr(er))
+    await gt
+
+    print("====")
+
+    # Test case where can't gather on a task being waited.
+    tasks = [asyncio.create_task(task(1)), asyncio.create_task(task(2))]
+    asyncio.create_task(gather_task(tasks[0], tasks[1]))
+    await tasks[0]  # wait on this task before the gather starts
+    await tasks[1]
+
+
+asyncio.run(main())
diff --git a/tests/extmod/uasyncio_gather_notimpl.py.exp b/tests/extmod/asyncio_gather_notimpl.py.exp
similarity index 100%
rename from tests/extmod/uasyncio_gather_notimpl.py.exp
rename to tests/extmod/asyncio_gather_notimpl.py.exp
diff --git a/tests/extmod/asyncio_get_event_loop.py b/tests/extmod/asyncio_get_event_loop.py
new file mode 100644
index 00000000000..6ecbb13b57a
--- /dev/null
+++ b/tests/extmod/asyncio_get_event_loop.py
@@ -0,0 +1,22 @@
+# Test get_event_loop()
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# CPython 3.12 deprecated calling get_event_loop() when there is no current event
+# loop, so to make this test run on CPython requires setting the event loop.
+if hasattr(asyncio, "set_event_loop"):
+    asyncio.set_event_loop(asyncio.new_event_loop())
+
+
+async def main():
+    print("start")
+    await asyncio.sleep(0.01)
+    print("end")
+
+
+loop = asyncio.get_event_loop()
+loop.run_until_complete(main())
diff --git a/tests/extmod/asyncio_heaplock.py b/tests/extmod/asyncio_heaplock.py
new file mode 100644
index 00000000000..9e9908de1cb
--- /dev/null
+++ b/tests/extmod/asyncio_heaplock.py
@@ -0,0 +1,77 @@
+# test that the following do not use the heap:
+# - basic scheduling of tasks
+# - asyncio.sleep_ms
+# - StreamWriter.write, stream is blocked and data to write is a bytes object
+# - StreamWriter.write, when stream is not blocked
+
+try:
+    import asyncio, micropython
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# strict stackless builds can't call functions without allocating a frame on the heap
+try:
+    # force bytecode (in case we're running with emit=native) and verify
+    # that bytecode-calling-bytecode doesn't allocate
+    @micropython.bytecode
+    def f(x):
+        x and f(x - 1)
+
+    micropython.heap_lock()
+    f(1)
+    micropython.heap_unlock()
+except RuntimeError:
+    # RuntimeError (max recursion depth) not MemoryError because effectively
+    # the recursion depth is at the limit while the heap is locked with
+    # stackless
+    print("SKIP")
+    raise SystemExit
+
+
+class TestStream:
+    def __init__(self, blocked):
+        self.blocked = blocked
+
+    def write(self, data):
+        print("TestStream.write", data)
+        if self.blocked:
+            return None
+        return len(data)
+
+
+async def task(id, n, t):
+    for i in range(n):
+        print(id, i)
+        await asyncio.sleep_ms(t)
+
+
+async def main():
+    t1 = asyncio.create_task(task(1, 4, 100))
+    t2 = asyncio.create_task(task(2, 2, 250))
+
+    # test scheduling tasks, and calling sleep_ms
+    micropython.heap_lock()
+    print("start")
+    await asyncio.sleep_ms(5)
+    print("sleep")
+    await asyncio.sleep_ms(350)
+    print("finish")
+    micropython.heap_unlock()
+
+    # test writing to a stream, when the underlying stream is blocked
+    s = asyncio.StreamWriter(TestStream(True), None)
+    micropython.heap_lock()
+    s.write(b"12")
+    micropython.heap_unlock()
+
+    # test writing to a stream, when the underlying stream is not blocked
+    buf = bytearray(b"56")
+    s = asyncio.StreamWriter(TestStream(False), None)
+    micropython.heap_lock()
+    s.write(b"34")
+    s.write(buf)
+    micropython.heap_unlock()
+
+
+asyncio.run(main())
diff --git a/tests/extmod/uasyncio_heaplock.py.exp b/tests/extmod/asyncio_heaplock.py.exp
similarity index 100%
rename from tests/extmod/uasyncio_heaplock.py.exp
rename to tests/extmod/asyncio_heaplock.py.exp
diff --git a/tests/extmod/asyncio_iterator_event.py b/tests/extmod/asyncio_iterator_event.py
new file mode 100644
index 00000000000..f61fefcf051
--- /dev/null
+++ b/tests/extmod/asyncio_iterator_event.py
@@ -0,0 +1,86 @@
+# Ensure that an asyncio task can wait on an Event when the
+# _task_queue is empty, in the context of an async iterator
+# https://github.com/micropython/micropython/issues/16318
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# This test requires checking that the asyncio scheduler
+# remains active "indefinitely" when the task queue is empty.
+#
+# To check this, we need another independent scheduler that
+# can wait for a certain amount of time. So we have to
+# create one using micropython.schedule() and time.ticks_ms()
+#
+# Technically, this code breaks the rules, as it is clearly
+# documented that Event.set() should _NOT_ be called from a
+# schedule (soft IRQ) because in some cases, a race condition
+# can occur, resulting in a crash. However:
+# - since the risk of a race condition in that specific
+#   case has been analysed and excluded
+# - given that there is no other simple alternative to
+#   write this test case,
+# an exception to the rule was deemed acceptable. See
+# https://github.com/micropython/micropython/pull/16772
+
+import micropython, time
+
+try:
+    micropython.schedule
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+ai = None
+
+
+def schedule_watchdog(end_ticks):
+    if time.ticks_diff(end_ticks, time.ticks_ms()) <= 0:
+        print("good: asyncio iterator is still pending, exiting")
+        # Caution: ai.fetch_data() will invoke Event.set()
+        #          (see the note in the comment above)
+        ai.fetch_data(None)
+        return
+    micropython.schedule(schedule_watchdog, end_ticks)
+
+
+async def test(ai):
+    for x in range(3):
+        await asyncio.sleep(0.1)
+        ai.fetch_data("bar {}".format(x))
+
+
+class AsyncIterable:
+    def __init__(self):
+        self.message = None
+        self.evt = asyncio.Event()
+
+    def __aiter__(self):
+        return self
+
+    async def __anext__(self):
+        await self.evt.wait()
+        self.evt.clear()
+        if self.message is None:
+            raise StopAsyncIteration
+        return self.message
+
+    def fetch_data(self, message):
+        self.message = message
+        self.evt.set()
+
+
+async def main():
+    global ai
+    ai = AsyncIterable()
+    asyncio.create_task(test(ai))
+    schedule_watchdog(time.ticks_add(time.ticks_ms(), 500))
+    async for message in ai:
+        print(message)
+    print("end main")
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_iterator_event.py.exp b/tests/extmod/asyncio_iterator_event.py.exp
new file mode 100644
index 00000000000..a1893197d02
--- /dev/null
+++ b/tests/extmod/asyncio_iterator_event.py.exp
@@ -0,0 +1,5 @@
+bar 0
+bar 1
+bar 2
+good: asyncio iterator is still pending, exiting
+end main
diff --git a/tests/extmod/asyncio_lock.py b/tests/extmod/asyncio_lock.py
new file mode 100644
index 00000000000..10d3285be0f
--- /dev/null
+++ b/tests/extmod/asyncio_lock.py
@@ -0,0 +1,98 @@
+# Test Lock class
+#
+# This test has a corresponding .exp file because it tests a very specific ordering of
+# events when cancelling a task waiting on a lock, and that ordering should not change
+# (even though it does match CPython's output).
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task_loop(id, lock):
+    print("task start", id)
+    for i in range(3):
+        async with lock:
+            print("task have", id, i)
+    print("task end", id)
+
+
+async def task_sleep(lock):
+    async with lock:
+        print("task have", lock.locked())
+        await asyncio.sleep(0.2)
+    print("task release", lock.locked())
+    await lock.acquire()
+    print("task have again")
+    lock.release()
+
+
+async def task_cancel(id, lock, to_cancel=None):
+    try:
+        async with lock:
+            print("task got", id)
+            await asyncio.sleep(0.1)
+        print("task release", id)
+        if to_cancel:
+            to_cancel[0].cancel()
+    except asyncio.CancelledError:
+        print("task cancel", id)
+
+
+async def main():
+    lock = asyncio.Lock()
+
+    # Basic acquire/release
+    print(lock.locked())
+    await lock.acquire()
+    print(lock.locked())
+    await asyncio.sleep(0)
+    lock.release()
+    print(lock.locked())
+    await asyncio.sleep(0)
+
+    # Use with "async with"
+    async with lock:
+        print("have lock")
+
+    # 3 tasks wanting the lock
+    print("----")
+    asyncio.create_task(task_loop(1, lock))
+    asyncio.create_task(task_loop(2, lock))
+    t3 = asyncio.create_task(task_loop(3, lock))
+    await lock.acquire()
+    await asyncio.sleep(0)
+    lock.release()
+    await t3
+
+    # 2 sleeping tasks both wanting the lock
+    print("----")
+    asyncio.create_task(task_sleep(lock))
+    await asyncio.sleep(0.1)
+    await task_sleep(lock)
+
+    # 3 tasks, the first cancelling the second, the third should still run
+    print("----")
+    ts = [None]
+    asyncio.create_task(task_cancel(0, lock, ts))
+    ts[0] = asyncio.create_task(task_cancel(1, lock))
+    asyncio.create_task(task_cancel(2, lock))
+    await asyncio.sleep(0.3)
+    print(lock.locked())
+
+    # 3 tasks, the second and third being cancelled while waiting on the lock
+    print("----")
+    t0 = asyncio.create_task(task_cancel(0, lock))
+    t1 = asyncio.create_task(task_cancel(1, lock))
+    t2 = asyncio.create_task(task_cancel(2, lock))
+    await asyncio.sleep(0.05)
+    t1.cancel()
+    await asyncio.sleep(0.1)
+    t2.cancel()
+    await asyncio.sleep(0.1)
+    print(lock.locked())
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_lock_cancel.py b/tests/extmod/asyncio_lock_cancel.py
new file mode 100644
index 00000000000..c81bb3c7ba5
--- /dev/null
+++ b/tests/extmod/asyncio_lock_cancel.py
@@ -0,0 +1,52 @@
+# Test that locks work when cancelling multiple waiters on the lock
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task(i, lock, lock_flag):
+    print("task", i, "start")
+    try:
+        await lock.acquire()
+    except asyncio.CancelledError:
+        print("task", i, "cancel")
+        return
+    print("task", i, "lock_flag", lock_flag[0])
+    lock_flag[0] = True
+    await asyncio.sleep(0)
+    lock.release()
+    lock_flag[0] = False
+    print("task", i, "done")
+
+
+async def main():
+    # Create a lock and acquire it so the tasks below must wait
+    lock = asyncio.Lock()
+    await lock.acquire()
+    lock_flag = [True]
+
+    # Create 4 tasks and let them all run
+    t0 = asyncio.create_task(task(0, lock, lock_flag))
+    t1 = asyncio.create_task(task(1, lock, lock_flag))
+    t2 = asyncio.create_task(task(2, lock, lock_flag))
+    t3 = asyncio.create_task(task(3, lock, lock_flag))
+    await asyncio.sleep(0)
+
+    # Cancel 2 of the tasks (which are waiting on the lock) and release the lock
+    t1.cancel()
+    t2.cancel()
+    lock.release()
+    lock_flag[0] = False
+
+    # Let the tasks run to completion
+    for _ in range(4):
+        await asyncio.sleep(0)
+
+    # The locke should be unlocked
+    print(lock.locked())
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_loop_stop.py b/tests/extmod/asyncio_loop_stop.py
new file mode 100644
index 00000000000..e2a4cdc1af8
--- /dev/null
+++ b/tests/extmod/asyncio_loop_stop.py
@@ -0,0 +1,42 @@
+# Test Loop.stop() to stop the event loop
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task():
+    print("task")
+
+
+async def main():
+    print("start")
+
+    # Stop the loop after next yield
+    loop.stop()
+
+    # Check that calling stop() again doesn't do/break anything
+    loop.stop()
+
+    # This await should stop
+    print("sleep")
+    await asyncio.sleep(0)
+
+    # Schedule stop, then create a new task, then yield
+    loop.stop()
+    asyncio.create_task(task())
+    await asyncio.sleep(0)
+
+    # Final stop
+    print("end")
+    loop.stop()
+
+
+loop = asyncio.new_event_loop()
+loop.create_task(main())
+
+for i in range(3):
+    print("run", i)
+    loop.run_forever()
diff --git a/tests/extmod/asyncio_micropython.py b/tests/extmod/asyncio_micropython.py
new file mode 100644
index 00000000000..cc96d34be2e
--- /dev/null
+++ b/tests/extmod/asyncio_micropython.py
@@ -0,0 +1,43 @@
+# Test MicroPython extensions on CPython asyncio:
+# - sleep_ms
+# - wait_for_ms
+
+try:
+    import time, asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task(id, t):
+    print("task start", id)
+    await asyncio.sleep_ms(t)
+    print("task end", id)
+    return id * 2
+
+
+async def main():
+    # Simple sleep_ms
+    t0 = time.ticks_ms()
+    await asyncio.sleep_ms(1)
+    print(time.ticks_diff(time.ticks_ms(), t0) < 100)
+
+    try:
+        # Sleep 1ms beyond maximum allowed sleep value
+        await asyncio.sleep_ms(time.ticks_add(0, -1) // 2 + 1)
+    except OverflowError:
+        print("OverflowError")
+
+    # When task finished before the timeout
+    print(await asyncio.wait_for_ms(task(1, 5), 50))
+
+    # When timeout passes and task is cancelled
+    try:
+        print(await asyncio.wait_for_ms(task(2, 50), 5))
+    except asyncio.TimeoutError:
+        print("timeout")
+
+    print("finish")
+
+
+asyncio.run(main())
diff --git a/tests/extmod/uasyncio_micropython.py.exp b/tests/extmod/asyncio_micropython.py.exp
similarity index 100%
rename from tests/extmod/uasyncio_micropython.py.exp
rename to tests/extmod/asyncio_micropython.py.exp
diff --git a/tests/extmod/asyncio_new_event_loop.py b/tests/extmod/asyncio_new_event_loop.py
new file mode 100644
index 00000000000..3f05ffdd551
--- /dev/null
+++ b/tests/extmod/asyncio_new_event_loop.py
@@ -0,0 +1,52 @@
+# Test Loop.new_event_loop()
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# CPython 3.12 deprecated calling get_event_loop() when there is no current event
+# loop, so to make this test run on CPython requires setting the event loop.
+if hasattr(asyncio, "set_event_loop"):
+    asyncio.set_event_loop(asyncio.new_event_loop())
+
+
+def exception_handler(loop, context):
+    # This is a workaround for a difference between CPython and MicroPython: if
+    # a CPython event loop is closed while there are tasks pending (i.e. not finished)
+    # on it, then the task will log an error. MicroPython does not log this error.
+    if context.get("message", "") == "Task was destroyed but it is pending!":
+        pass
+    else:
+        loop.default_exception_handler(context)
+
+
+async def task():
+    for i in range(4):
+        print("task", i)
+        await asyncio.sleep(0)
+        await asyncio.sleep(0)
+
+
+async def main():
+    print("start")
+    loop.create_task(task())
+    await asyncio.sleep(0)  # yields, meaning new task will run once
+    print("stop")
+    loop.stop()
+
+
+# Use default event loop to run some tasks
+loop = asyncio.get_event_loop()
+loop.set_exception_handler(exception_handler)
+loop.create_task(main())
+loop.run_forever()
+loop.close()
+
+# Create new event loop, old one should not keep running
+loop = asyncio.new_event_loop()
+loop.set_exception_handler(exception_handler)
+loop.create_task(main())
+loop.run_forever()
+loop.close()
diff --git a/tests/extmod/asyncio_set_exception_handler.py b/tests/extmod/asyncio_set_exception_handler.py
new file mode 100644
index 00000000000..0ac4a624224
--- /dev/null
+++ b/tests/extmod/asyncio_set_exception_handler.py
@@ -0,0 +1,53 @@
+# Test that tasks return their value correctly to the caller
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def custom_handler(loop, context):
+    print("custom_handler", repr(context["exception"]))
+
+
+async def task(i):
+    # Raise with 2 args so exception prints the same in MicroPython and CPython
+    raise ValueError(i, i + 1)
+
+
+async def main():
+    loop = asyncio.get_event_loop()
+
+    # Check default exception handler, should be None
+    print(loop.get_exception_handler())
+
+    # Set exception handler and test it was set
+    loop.set_exception_handler(custom_handler)
+    print(loop.get_exception_handler() == custom_handler)
+
+    # Create a task that raises and uses the custom exception handler
+    asyncio.create_task(task(0))
+    print("sleep")
+    for _ in range(2):
+        await asyncio.sleep(0)
+
+    # Create 2 tasks to test order of printing exception
+    asyncio.create_task(task(1))
+    asyncio.create_task(task(2))
+    print("sleep")
+    for _ in range(2):
+        await asyncio.sleep(0)
+
+    # Create a task, let it run, then await it (no exception should be printed)
+    t = asyncio.create_task(task(3))
+    await asyncio.sleep(0)
+    try:
+        await t
+    except ValueError as er:
+        print(repr(er))
+
+    print("done")
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_task_done.py b/tests/extmod/asyncio_task_done.py
new file mode 100644
index 00000000000..7cc493a1227
--- /dev/null
+++ b/tests/extmod/asyncio_task_done.py
@@ -0,0 +1,63 @@
+# Test the Task.done() method
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task(t, exc=None):
+    print("task start")
+    if t >= 0:
+        await asyncio.sleep(t)
+    if exc:
+        raise exc
+    print("task done")
+
+
+async def main():
+    # Task that finishes immediately.
+    print("=" * 10)
+    t = asyncio.create_task(task(-1))
+    print(t.done())
+    await asyncio.sleep(0)
+    print(t.done())
+    await t
+    print(t.done())
+
+    # Task that starts, runs and finishes.
+    print("=" * 10)
+    t = asyncio.create_task(task(0.01))
+    print(t.done())
+    await asyncio.sleep(0)
+    print(t.done())
+    await t
+    print(t.done())
+
+    # Task that raises immediately.
+    print("=" * 10)
+    t = asyncio.create_task(task(-1, ValueError))
+    print(t.done())
+    await asyncio.sleep(0)
+    print(t.done())
+    try:
+        await t
+    except ValueError as er:
+        print(repr(er))
+    print(t.done())
+
+    # Task that raises after a delay.
+    print("=" * 10)
+    t = asyncio.create_task(task(0.01, ValueError))
+    print(t.done())
+    await asyncio.sleep(0)
+    print(t.done())
+    try:
+        await t
+    except ValueError as er:
+        print(repr(er))
+    print(t.done())
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_threadsafeflag.py b/tests/extmod/asyncio_threadsafeflag.py
new file mode 100644
index 00000000000..e8798afcf5f
--- /dev/null
+++ b/tests/extmod/asyncio_threadsafeflag.py
@@ -0,0 +1,86 @@
+# Test Event class
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+import micropython
+
+try:
+    micropython.schedule
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task(id, flag):
+    print("task", id)
+    await flag.wait()
+    print("task", id, "done")
+
+
+def set_from_schedule(flag):
+    flag.set()
+
+
+async def main():
+    flag = asyncio.ThreadSafeFlag()
+
+    # Set the flag from within the loop.
+    t = asyncio.create_task(task(1, flag))
+    print("yield")
+    await asyncio.sleep(0)
+    print("set event")
+    flag.set()
+    print("yield")
+    await asyncio.sleep(0)
+    print("wait task")
+    await t
+
+    # Set the flag from scheduler context.
+    print("----")
+    t = asyncio.create_task(task(2, flag))
+    print("yield")
+    await asyncio.sleep(0)
+    print("set event")
+    micropython.schedule(set_from_schedule, flag)
+    print("yield")
+    await asyncio.sleep(0)
+    print("wait task")
+    await t
+
+    # Flag already set.
+    print("----")
+    print("set event")
+    flag.set()
+    t = asyncio.create_task(task(3, flag))
+    print("yield")
+    await asyncio.sleep(0)
+    print("wait task")
+    await t
+
+    # Flag set, cleared, and set again.
+    print("----")
+    print("set event")
+    flag.set()
+    print("yield")
+    await asyncio.sleep(0)
+    print("clear event")
+    flag.clear()
+    print("yield")
+    await asyncio.sleep(0)
+    t = asyncio.create_task(task(4, flag))
+    print("yield")
+    await asyncio.sleep(0)
+    print("set event")
+    flag.set()
+    print("yield")
+    await asyncio.sleep(0)
+    print("wait task")
+    await t
+
+
+asyncio.run(main())
diff --git a/tests/extmod/asyncio_threadsafeflag.py.exp b/tests/extmod/asyncio_threadsafeflag.py.exp
new file mode 100644
index 00000000000..0c62eae9d09
--- /dev/null
+++ b/tests/extmod/asyncio_threadsafeflag.py.exp
@@ -0,0 +1,30 @@
+yield
+task 1
+set event
+yield
+wait task
+task 1 done
+----
+yield
+task 2
+set event
+yield
+wait task
+task 2 done
+----
+set event
+yield
+task 3
+task 3 done
+wait task
+----
+set event
+yield
+clear event
+yield
+yield
+task 4
+set event
+yield
+wait task
+task 4 done
diff --git a/tests/extmod/asyncio_wait_for.py b/tests/extmod/asyncio_wait_for.py
new file mode 100644
index 00000000000..f973e46f4cf
--- /dev/null
+++ b/tests/extmod/asyncio_wait_for.py
@@ -0,0 +1,129 @@
+# Test asyncio.wait_for
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task(id, t):
+    print("task start", id)
+    await asyncio.sleep(t)
+    print("task end", id)
+    return id * 2
+
+
+async def task_catch():
+    print("task_catch start")
+    try:
+        await asyncio.sleep(0.2)
+    except asyncio.CancelledError:
+        print("ignore cancel")
+    print("task_catch done")
+
+
+async def task_raise():
+    print("task start")
+    raise ValueError
+
+
+async def task_cancel_other(t, other):
+    print("task_cancel_other start")
+    await asyncio.sleep(t)
+    print("task_cancel_other cancel")
+    other.cancel()
+
+
+async def task_wait_for_cancel(id, t, t_wait):
+    print("task_wait_for_cancel start")
+    try:
+        await asyncio.wait_for(task(id, t), t_wait)
+    except asyncio.CancelledError as er:
+        print("task_wait_for_cancel cancelled")
+        raise er
+
+
+async def task_wait_for_cancel_ignore(t_wait):
+    print("task_wait_for_cancel_ignore start")
+    try:
+        await asyncio.wait_for(task_catch(), t_wait)
+    except asyncio.CancelledError as er:
+        print("task_wait_for_cancel_ignore cancelled")
+        raise er
+
+
+async def main():
+    sep = "-" * 10
+
+    # When task finished before the timeout
+    print(await asyncio.wait_for(task(1, 0.01), 10))
+    print(sep)
+
+    # When timeout passes and task is cancelled
+    try:
+        print(await asyncio.wait_for(task(2, 10), 0.01))
+    except asyncio.TimeoutError:
+        print("timeout")
+    print(sep)
+
+    # When timeout passes and task is cancelled, but task ignores the cancellation request
+    try:
+        print(await asyncio.wait_for(task_catch(), 0.1))
+    except asyncio.TimeoutError:
+        print("TimeoutError")
+    print(sep)
+
+    # When task raises an exception
+    try:
+        print(await asyncio.wait_for(task_raise(), 1))
+    except ValueError:
+        print("ValueError")
+    print(sep)
+
+    # Timeout of None means wait forever
+    print(await asyncio.wait_for(task(3, 0.1), None))
+    print(sep)
+
+    # When task is cancelled by another task
+    t = asyncio.create_task(task(4, 10))
+    asyncio.create_task(task_cancel_other(0.01, t))
+    try:
+        print(await asyncio.wait_for(t, 1))
+    except asyncio.CancelledError as er:
+        print(repr(er))
+    print(sep)
+
+    # When wait_for gets cancelled
+    t = asyncio.create_task(task_wait_for_cancel(4, 1, 2))
+    await asyncio.sleep(0.01)
+    t.cancel()
+    await asyncio.sleep(0.01)
+    print(sep)
+
+    # When wait_for gets cancelled and awaited task ignores the cancellation request
+    t = asyncio.create_task(task_wait_for_cancel_ignore(2))
+    await asyncio.sleep(0.01)
+    t.cancel()
+    await asyncio.sleep(0.01)
+    print(sep)
+
+    # When wait_for gets cancelled and the task it's waiting on finishes around the
+    # same time as the cancellation of the wait_for
+    for num_sleep in range(1, 5):
+        t = asyncio.create_task(task_wait_for_cancel(4 + num_sleep, 0, 2))
+        for _ in range(num_sleep):
+            await asyncio.sleep(0)
+        assert not t.done()
+        print("cancel wait_for")
+        t.cancel()
+        try:
+            await t
+        except asyncio.CancelledError as er:
+            print(repr(er))
+        print(sep)
+
+    print("finish")
+
+
+asyncio.run(main())
diff --git a/tests/extmod/uasyncio_wait_for.py.exp b/tests/extmod/asyncio_wait_for.py.exp
similarity index 100%
rename from tests/extmod/uasyncio_wait_for.py.exp
rename to tests/extmod/asyncio_wait_for.py.exp
diff --git a/tests/extmod/asyncio_wait_for_fwd.py b/tests/extmod/asyncio_wait_for_fwd.py
new file mode 100644
index 00000000000..9ec734fae66
--- /dev/null
+++ b/tests/extmod/asyncio_wait_for_fwd.py
@@ -0,0 +1,57 @@
+# Test asyncio.wait_for, with forwarding cancellation
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def awaiting(t, return_if_fail):
+    try:
+        print("awaiting started")
+        await asyncio.sleep(t)
+    except asyncio.CancelledError as er:
+        # CPython wait_for raises CancelledError inside task but TimeoutError in wait_for
+        print("awaiting canceled")
+        if return_if_fail:
+            return False  # return has no effect if Cancelled
+        else:
+            raise er
+    except Exception as er:
+        print("caught exception", er)
+        raise er
+
+
+async def test_cancellation_forwarded(catch, catch_inside):
+    print("----------")
+
+    async def wait():
+        try:
+            await asyncio.wait_for(awaiting(2, catch_inside), 1)
+        except asyncio.TimeoutError as er:
+            print("Got timeout error")
+            raise er
+        except asyncio.CancelledError as er:
+            print("Got canceled")
+            if not catch:
+                raise er
+
+    async def cancel(t):
+        print("cancel started")
+        await asyncio.sleep(0.01)
+        print("cancel wait()")
+        t.cancel()
+
+    t = asyncio.create_task(wait())
+    k = asyncio.create_task(cancel(t))
+    try:
+        await t
+    except asyncio.CancelledError:
+        print("waiting got cancelled")
+
+
+asyncio.run(test_cancellation_forwarded(False, False))
+asyncio.run(test_cancellation_forwarded(False, True))
+asyncio.run(test_cancellation_forwarded(True, True))
+asyncio.run(test_cancellation_forwarded(True, False))
diff --git a/tests/extmod/uasyncio_wait_for_fwd.py.exp b/tests/extmod/asyncio_wait_for_fwd.py.exp
similarity index 100%
rename from tests/extmod/uasyncio_wait_for_fwd.py.exp
rename to tests/extmod/asyncio_wait_for_fwd.py.exp
diff --git a/tests/extmod/asyncio_wait_for_linked_task.py b/tests/extmod/asyncio_wait_for_linked_task.py
new file mode 100644
index 00000000000..4dda62d5476
--- /dev/null
+++ b/tests/extmod/asyncio_wait_for_linked_task.py
@@ -0,0 +1,66 @@
+# Test asyncio.wait_for, with dependent tasks
+# https://github.com/micropython/micropython/issues/16759
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+# CPython 3.12 deprecated calling get_event_loop() when there is no current event
+# loop, so to make this test run on CPython requires setting the event loop.
+if hasattr(asyncio, "set_event_loop"):
+    asyncio.set_event_loop(asyncio.new_event_loop())
+
+
+class Worker:
+    def __init__(self):
+        self._eventLoop = None
+        self._tasks = []
+
+    def launchTask(self, asyncJob):
+        if self._eventLoop is None:
+            self._eventLoop = asyncio.get_event_loop()
+        return self._eventLoop.create_task(asyncJob)
+
+    async def job(self, prerequisite, taskName):
+        if prerequisite:
+            await prerequisite
+        await asyncio.sleep(0.1)
+        print(taskName, "work completed")
+
+    def planTasks(self):
+        self._tasks.append(self.launchTask(self.job(None, "task0")))
+        self._tasks.append(self.launchTask(self.job(self._tasks[0], "task1")))
+        self._tasks.append(self.launchTask(self.job(self._tasks[1], "task2")))
+
+    async def waitForTask(self, taskIdx):
+        return await self._tasks[taskIdx]
+
+    def syncWaitForTask(self, taskIdx):
+        return self._eventLoop.run_until_complete(self._tasks[taskIdx])
+
+
+async def async_test():
+    print("--- async test")
+    worker = Worker()
+    worker.planTasks()
+    await worker.waitForTask(0)
+    print("-> task0 done")
+    await worker.waitForTask(2)
+    print("-> task2 done")
+
+
+def sync_test():
+    print("--- sync test")
+    worker = Worker()
+    worker.planTasks()
+    worker.syncWaitForTask(0)
+    print("-> task0 done")
+    worker.syncWaitForTask(2)
+    print("-> task2 done")
+
+
+asyncio.get_event_loop().run_until_complete(async_test())
+sync_test()
diff --git a/tests/extmod/asyncio_wait_task.py b/tests/extmod/asyncio_wait_task.py
new file mode 100644
index 00000000000..7d79104f8cf
--- /dev/null
+++ b/tests/extmod/asyncio_wait_task.py
@@ -0,0 +1,83 @@
+# Test waiting on a task
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+import time
+
+if hasattr(time, "ticks_ms"):
+    ticks = time.ticks_ms
+    ticks_diff = time.ticks_diff
+else:
+    ticks = lambda: int(time.time() * 1000)
+    ticks_diff = lambda t1, t0: t1 - t0
+
+
+async def task(t):
+    print("task", t)
+
+
+async def delay_print(t, s):
+    await asyncio.sleep(t)
+    print(s)
+
+
+async def task_raise():
+    print("task_raise")
+    raise ValueError
+
+
+async def main():
+    print("start")
+
+    # Wait on a task
+    t = asyncio.create_task(task(1))
+    await t
+
+    # Wait on a task that's already done
+    t = asyncio.create_task(task(2))
+    await asyncio.sleep(0.001)
+    await t
+
+    # Wait again on same task
+    await t
+
+    print("----")
+
+    # Create 2 tasks
+    ts1 = asyncio.create_task(delay_print(0.2, "hello"))
+    ts2 = asyncio.create_task(delay_print(0.4, "world"))
+
+    # Time how long the tasks take to finish, they should execute in parallel
+    print("start")
+    t0 = ticks()
+    await ts1
+    t1 = ticks()
+    await ts2
+    t2 = ticks()
+    print("took {} {}".format(round(ticks_diff(t1, t0), -2), round(ticks_diff(t2, t1), -2)))
+
+    # Wait on a task that raises an exception
+    t = asyncio.create_task(task_raise())
+    try:
+        await t
+    except ValueError:
+        print("ValueError")
+
+    # Wait on a task that raises, but the waiting is done some time later.
+    # Need to suppress the "Task exception wasn't retrieved" message.
+    asyncio.get_event_loop().set_exception_handler(lambda loop, context: None)
+    t = asyncio.create_task(task_raise())
+    for _ in range(5):
+        await asyncio.sleep(0)
+    try:
+        await t
+    except ValueError:
+        print("ValueError")
+
+
+asyncio.run(main())
diff --git a/tests/extmod/binascii_a2b_base64.py b/tests/extmod/binascii_a2b_base64.py
new file mode 100644
index 00000000000..fd1ff75e1fc
--- /dev/null
+++ b/tests/extmod/binascii_a2b_base64.py
@@ -0,0 +1,43 @@
+try:
+    import binascii
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+print(binascii.a2b_base64(b""))
+print(binascii.a2b_base64(b"Zg=="))
+print(binascii.a2b_base64(b"Zm8="))
+print(binascii.a2b_base64(b"Zm9v"))
+print(binascii.a2b_base64(b"Zm9vYg=="))
+print(binascii.a2b_base64(b"Zm9vYmE="))
+print(binascii.a2b_base64(b"Zm9vYmFy"))
+
+print(binascii.a2b_base64(b"AAECAwQFBgc="))
+print(binascii.a2b_base64(b"CAkKCwwNDg8="))
+print(binascii.a2b_base64(b"f4D/"))
+print(binascii.a2b_base64(b"f4D+"))  # convert '+'
+print(binascii.a2b_base64(b"MTIzNEFCQ0RhYmNk"))
+
+# Ignore invalid characters and pad sequences
+print(binascii.a2b_base64(b"Zm9v\n"))
+print(binascii.a2b_base64(b"Zm\x009v\n"))
+print(binascii.a2b_base64(b"Zm9v=="))
+print(binascii.a2b_base64(b"Zm9v==="))
+print(binascii.a2b_base64(b"Zm9v===YmFy"))
+
+try:
+    print(binascii.a2b_base64(b"abc"))
+except ValueError:
+    print("ValueError")
+try:
+    print(binascii.a2b_base64(b"abcde="))
+except ValueError:
+    print("ValueError")
+try:
+    print(binascii.a2b_base64(b"ab*d"))
+except ValueError:
+    print("ValueError")
+try:
+    print(binascii.a2b_base64(b"ab=cdef="))
+except ValueError:
+    print("ValueError")
diff --git a/tests/extmod/binascii_b2a_base64.py b/tests/extmod/binascii_b2a_base64.py
new file mode 100644
index 00000000000..306af940048
--- /dev/null
+++ b/tests/extmod/binascii_b2a_base64.py
@@ -0,0 +1,22 @@
+try:
+    import binascii
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+print(binascii.b2a_base64(b""))
+print(binascii.b2a_base64(b"f"))
+print(binascii.b2a_base64(b"fo"))
+print(binascii.b2a_base64(b"foo"))
+print(binascii.b2a_base64(b"foob"))
+print(binascii.b2a_base64(b"fooba"))
+print(binascii.b2a_base64(b"foobar"))
+
+print(binascii.b2a_base64(b"\x00\x01\x02\x03\x04\x05\x06\x07"))
+print(binascii.b2a_base64(b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"))
+print(binascii.b2a_base64(b"\x7f\x80\xff"))
+print(binascii.b2a_base64(b"1234ABCDabcd"))
+print(binascii.b2a_base64(b"\x00\x00>"))  # convert into '+'
+
+print(binascii.b2a_base64(b"foobar", newline=True))
+print(binascii.b2a_base64(b"foobar", newline=False))
diff --git a/tests/extmod/binascii_crc32.py b/tests/extmod/binascii_crc32.py
new file mode 100644
index 00000000000..0e4a5b58f33
--- /dev/null
+++ b/tests/extmod/binascii_crc32.py
@@ -0,0 +1,21 @@
+try:
+    import binascii
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    binascii.crc32
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+print(hex(binascii.crc32(b"The quick brown fox jumps over the lazy dog")))
+print(hex(binascii.crc32(b"\x00" * 32)))
+print(hex(binascii.crc32(b"\xff" * 32)))
+print(hex(binascii.crc32(bytes(range(32)))))
+
+print(hex(binascii.crc32(b" over the lazy dog", binascii.crc32(b"The quick brown fox jumps"))))
+print(hex(binascii.crc32(b"\x00" * 16, binascii.crc32(b"\x00" * 16))))
+print(hex(binascii.crc32(b"\xff" * 16, binascii.crc32(b"\xff" * 16))))
+print(hex(binascii.crc32(bytes(range(16, 32)), binascii.crc32(bytes(range(16))))))
diff --git a/tests/extmod/binascii_hexlify.py b/tests/extmod/binascii_hexlify.py
new file mode 100644
index 00000000000..ae90b673365
--- /dev/null
+++ b/tests/extmod/binascii_hexlify.py
@@ -0,0 +1,19 @@
+try:
+    from binascii import hexlify
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+for x in (
+    b"\x00\x01\x02\x03\x04\x05\x06\x07",
+    b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
+    b"\x7f\x80\xff",
+    b"1234ABCDabcd",
+):
+    print(hexlify(x))
+
+# Two-argument version (now supported in CPython)
+print(hexlify(b"123", ":"))
+
+# zero length buffer
+print(hexlify(b"", b":"))
diff --git a/tests/extmod/binascii_unhexlify.py b/tests/extmod/binascii_unhexlify.py
new file mode 100644
index 00000000000..ec31fb2325a
--- /dev/null
+++ b/tests/extmod/binascii_unhexlify.py
@@ -0,0 +1,23 @@
+try:
+    from binascii import unhexlify
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+for x in (
+    b"0001020304050607",
+    b"08090a0b0c0d0e0f",
+    b"7f80ff",
+    b"313233344142434461626364",
+):
+    print(unhexlify(x))
+
+try:
+    a = unhexlify(b"0")  # odd buffer length
+except ValueError:
+    print("ValueError")
+
+try:
+    a = unhexlify(b"gg")  # digit not hex
+except ValueError:
+    print("ValueError")
diff --git a/tests/extmod/btree1.py b/tests/extmod/btree1.py
index 876bce4f875..d142a370f4d 100644
--- a/tests/extmod/btree1.py
+++ b/tests/extmod/btree1.py
@@ -1,13 +1,13 @@
 try:
     import btree
-    import uio
-    import uerrno
+    import io
+    import errno
 except ImportError:
     print("SKIP")
     raise SystemExit
 
 # f = open("_test.db", "w+b")
-f = uio.BytesIO()
+f = io.BytesIO()
 db = btree.open(f, pagesize=512)
 
 mv = memoryview(b"bar1foo1")
@@ -68,7 +68,7 @@
 try:
     db.seq(b"foo1")
 except OSError as e:
-    print(e.errno == uerrno.EINVAL)
+    print(e.errno == errno.EINVAL)
 
 print(list(db.keys()))
 print(list(db.values()))
diff --git a/tests/extmod/btree_closed.py b/tests/extmod/btree_closed.py
new file mode 100644
index 00000000000..ae61cd00836
--- /dev/null
+++ b/tests/extmod/btree_closed.py
@@ -0,0 +1,39 @@
+try:
+    import btree
+    import io
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+f = io.BytesIO()
+db = btree.open(f)
+
+db[b"foo"] = b"42"
+
+db.close()
+
+# Accessing an already-closed database should fail.
+try:
+    print(db[b"foo"])
+except ValueError:
+    print("ValueError")
+
+try:
+    db[b"bar"] = b"43"
+except ValueError:
+    print("ValueError")
+
+try:
+    db.flush()
+except ValueError:
+    print("ValueError")
+
+try:
+    for k, v in db.items():
+        pass
+except ValueError:
+    print("ValueError")
+
+# Closing and printing an already-closed database should not fail.
+db.close()
+print(db)
diff --git a/tests/extmod/btree_closed.py.exp b/tests/extmod/btree_closed.py.exp
new file mode 100644
index 00000000000..312edfd13db
--- /dev/null
+++ b/tests/extmod/btree_closed.py.exp
@@ -0,0 +1,5 @@
+ValueError
+ValueError
+ValueError
+ValueError
+
diff --git a/tests/extmod/btree_error.py b/tests/extmod/btree_error.py
index b64769e8840..2dd0d137a00 100644
--- a/tests/extmod/btree_error.py
+++ b/tests/extmod/btree_error.py
@@ -1,15 +1,15 @@
 # Test that errno's propagate correctly through btree module.
 
 try:
-    import btree, uio, uerrno
+    import btree, io, errno
 
-    uio.IOBase
+    io.IOBase
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
 
-class Device(uio.IOBase):
+class Device(io.IOBase):
     def __init__(self, read_ret=0, ioctl_ret=0):
         self.read_ret = read_ret
         self.ioctl_ret = ioctl_ret
@@ -25,18 +25,24 @@ def ioctl(self, cmd, arg):
 
 # Invalid pagesize; errno comes from btree library
 try:
+    import btree, io, errno
+
     db = btree.open(Device(), pagesize=511)
 except OSError as er:
-    print("OSError", er.errno == uerrno.EINVAL)
+    print("OSError", er.errno == errno.EINVAL)
 
 # Valid pagesize, device returns error on read; errno comes from Device.readinto
 try:
+    import btree, io, errno
+
     db = btree.open(Device(-1000), pagesize=512)
 except OSError as er:
     print(repr(er))
 
 # Valid pagesize, device returns error on seek; errno comes from Device.ioctl
 try:
+    import btree, io, errno
+
     db = btree.open(Device(0, -1001), pagesize=512)
 except OSError as er:
     print(repr(er))
diff --git a/tests/extmod/btree_gc.py b/tests/extmod/btree_gc.py
index 1845aa06409..c5274eb4896 100644
--- a/tests/extmod/btree_gc.py
+++ b/tests/extmod/btree_gc.py
@@ -1,7 +1,7 @@
 # Test btree interaction with the garbage collector.
 
 try:
-    import btree, uio, gc
+    import btree, io, gc
 except ImportError:
     print("SKIP")
     raise SystemExit
@@ -9,7 +9,7 @@
 N = 80
 
 # Create a BytesIO but don't keep a reference to it.
-db = btree.open(uio.BytesIO(), pagesize=512)
+db = btree.open(io.BytesIO(), pagesize=512)
 
 # Overwrite lots of the Python stack to make sure no reference to the BytesIO remains.
 x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
diff --git a/tests/extmod/cryptolib_aes128_cbc.py b/tests/extmod/cryptolib_aes128_cbc.py
new file mode 100644
index 00000000000..ef6d4536856
--- /dev/null
+++ b/tests/extmod/cryptolib_aes128_cbc.py
@@ -0,0 +1,16 @@
+try:
+    from Crypto.Cipher import AES
+
+    aes = AES.new
+except ImportError:
+    try:
+        from cryptolib import aes
+    except ImportError:
+        print("SKIP")
+        raise SystemExit
+
+crypto = aes(b"1234" * 4, 2, b"5678" * 4)
+enc = crypto.encrypt(bytes(range(32)))
+print(enc)
+crypto = aes(b"1234" * 4, 2, b"5678" * 4)
+print(crypto.decrypt(enc))
diff --git a/tests/extmod/ucryptolib_aes128_cbc.py.exp b/tests/extmod/cryptolib_aes128_cbc.py.exp
similarity index 100%
rename from tests/extmod/ucryptolib_aes128_cbc.py.exp
rename to tests/extmod/cryptolib_aes128_cbc.py.exp
diff --git a/tests/extmod/cryptolib_aes128_ctr.py b/tests/extmod/cryptolib_aes128_ctr.py
new file mode 100644
index 00000000000..ee9b503b066
--- /dev/null
+++ b/tests/extmod/cryptolib_aes128_ctr.py
@@ -0,0 +1,28 @@
+try:
+    from cryptolib import aes
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def _new(k, ctr_initial):
+    return aes(k, 6, ctr_initial)
+
+
+try:
+    _new(b"x" * 16, b"x" * 16)
+except ValueError as e:
+    # is CTR support disabled?
+    if len(e.args) == 0 or e.args[0] == "mode":
+        print("SKIP")
+        raise SystemExit
+    raise e
+
+crypto = _new(b"1234" * 4, b"5678" * 4)
+enc = crypto.encrypt(b"a")
+print(enc)
+enc += crypto.encrypt(b"b" * 1000)
+print(enc)
+
+crypto = _new(b"1234" * 4, b"5678" * 4)
+print(crypto.decrypt(enc))
diff --git a/tests/extmod/ucryptolib_aes128_ctr.py.exp b/tests/extmod/cryptolib_aes128_ctr.py.exp
similarity index 100%
rename from tests/extmod/ucryptolib_aes128_ctr.py.exp
rename to tests/extmod/cryptolib_aes128_ctr.py.exp
diff --git a/tests/extmod/cryptolib_aes128_ecb.py b/tests/extmod/cryptolib_aes128_ecb.py
new file mode 100644
index 00000000000..d1e77d326d0
--- /dev/null
+++ b/tests/extmod/cryptolib_aes128_ecb.py
@@ -0,0 +1,16 @@
+try:
+    from Crypto.Cipher import AES
+
+    aes = AES.new
+except ImportError:
+    try:
+        from cryptolib import aes
+    except ImportError:
+        print("SKIP")
+        raise SystemExit
+
+crypto = aes(b"1234" * 4, 1)
+enc = crypto.encrypt(bytes(range(32)))
+print(enc)
+crypto = aes(b"1234" * 4, 1)
+print(crypto.decrypt(enc))
diff --git a/tests/extmod/ucryptolib_aes128_ecb.py.exp b/tests/extmod/cryptolib_aes128_ecb.py.exp
similarity index 100%
rename from tests/extmod/ucryptolib_aes128_ecb.py.exp
rename to tests/extmod/cryptolib_aes128_ecb.py.exp
diff --git a/tests/extmod/cryptolib_aes128_ecb_enc.py b/tests/extmod/cryptolib_aes128_ecb_enc.py
new file mode 100644
index 00000000000..1bc973e22d6
--- /dev/null
+++ b/tests/extmod/cryptolib_aes128_ecb_enc.py
@@ -0,0 +1,17 @@
+# This tests minimal configuration of ucrypto module, which is
+# AES128 encryption (anything else, including AES128 decryption,
+# is optional).
+try:
+    from Crypto.Cipher import AES
+
+    aes = AES.new
+except ImportError:
+    try:
+        from cryptolib import aes
+    except ImportError:
+        print("SKIP")
+        raise SystemExit
+
+crypto = aes(b"1234" * 4, 1)
+enc = crypto.encrypt(bytes(range(32)))
+print(enc)
diff --git a/tests/extmod/ucryptolib_aes128_ecb_enc.py.exp b/tests/extmod/cryptolib_aes128_ecb_enc.py.exp
similarity index 100%
rename from tests/extmod/ucryptolib_aes128_ecb_enc.py.exp
rename to tests/extmod/cryptolib_aes128_ecb_enc.py.exp
diff --git a/tests/extmod/cryptolib_aes128_ecb_inpl.py b/tests/extmod/cryptolib_aes128_ecb_inpl.py
new file mode 100644
index 00000000000..826f95ced87
--- /dev/null
+++ b/tests/extmod/cryptolib_aes128_ecb_inpl.py
@@ -0,0 +1,15 @@
+# Inplace operations (input and output buffer is the same)
+try:
+    from cryptolib import aes
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+crypto = aes(b"1234" * 4, 1)
+buf = bytearray(bytes(range(32)))
+crypto.encrypt(buf, buf)
+print(buf)
+
+crypto = aes(b"1234" * 4, 1)
+crypto.decrypt(buf, buf)
+print(buf)
diff --git a/tests/extmod/ucryptolib_aes128_ecb_inpl.py.exp b/tests/extmod/cryptolib_aes128_ecb_inpl.py.exp
similarity index 100%
rename from tests/extmod/ucryptolib_aes128_ecb_inpl.py.exp
rename to tests/extmod/cryptolib_aes128_ecb_inpl.py.exp
diff --git a/tests/extmod/cryptolib_aes128_ecb_into.py b/tests/extmod/cryptolib_aes128_ecb_into.py
new file mode 100644
index 00000000000..f90dfcf2727
--- /dev/null
+++ b/tests/extmod/cryptolib_aes128_ecb_into.py
@@ -0,0 +1,16 @@
+# Operations with pre-allocated output buffer
+try:
+    from cryptolib import aes
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+crypto = aes(b"1234" * 4, 1)
+enc = bytearray(32)
+crypto.encrypt(bytes(range(32)), enc)
+print(enc)
+
+crypto = aes(b"1234" * 4, 1)
+dec = bytearray(32)
+crypto.decrypt(enc, dec)
+print(dec)
diff --git a/tests/extmod/ucryptolib_aes128_ecb_into.py.exp b/tests/extmod/cryptolib_aes128_ecb_into.py.exp
similarity index 100%
rename from tests/extmod/ucryptolib_aes128_ecb_into.py.exp
rename to tests/extmod/cryptolib_aes128_ecb_into.py.exp
diff --git a/tests/extmod/cryptolib_aes256_cbc.py b/tests/extmod/cryptolib_aes256_cbc.py
new file mode 100644
index 00000000000..91a9cd7609a
--- /dev/null
+++ b/tests/extmod/cryptolib_aes256_cbc.py
@@ -0,0 +1,16 @@
+try:
+    from Crypto.Cipher import AES
+
+    aes = AES.new
+except ImportError:
+    try:
+        from cryptolib import aes
+    except ImportError:
+        print("SKIP")
+        raise SystemExit
+
+crypto = aes(b"1234" * 8, 2, b"5678" * 4)
+enc = crypto.encrypt(bytes(range(32)))
+print(enc)
+crypto = aes(b"1234" * 8, 2, b"5678" * 4)
+print(crypto.decrypt(enc))
diff --git a/tests/extmod/ucryptolib_aes256_cbc.py.exp b/tests/extmod/cryptolib_aes256_cbc.py.exp
similarity index 100%
rename from tests/extmod/ucryptolib_aes256_cbc.py.exp
rename to tests/extmod/cryptolib_aes256_cbc.py.exp
diff --git a/tests/extmod/cryptolib_aes256_ecb.py b/tests/extmod/cryptolib_aes256_ecb.py
new file mode 100644
index 00000000000..ed040bc15ba
--- /dev/null
+++ b/tests/extmod/cryptolib_aes256_ecb.py
@@ -0,0 +1,16 @@
+try:
+    from Crypto.Cipher import AES
+
+    aes = AES.new
+except ImportError:
+    try:
+        from cryptolib import aes
+    except ImportError:
+        print("SKIP")
+        raise SystemExit
+
+crypto = aes(b"1234" * 8, 1)
+enc = crypto.encrypt(bytes(range(32)))
+print(enc)
+crypto = aes(b"1234" * 8, 1)
+print(crypto.decrypt(enc))
diff --git a/tests/extmod/ucryptolib_aes256_ecb.py.exp b/tests/extmod/cryptolib_aes256_ecb.py.exp
similarity index 100%
rename from tests/extmod/ucryptolib_aes256_ecb.py.exp
rename to tests/extmod/cryptolib_aes256_ecb.py.exp
diff --git a/tests/extmod/deflate_compress.py b/tests/extmod/deflate_compress.py
new file mode 100644
index 00000000000..981a986cbf5
--- /dev/null
+++ b/tests/extmod/deflate_compress.py
@@ -0,0 +1,154 @@
+try:
+    # Check if deflate is available.
+    import deflate
+    import io
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Check if compression is enabled.
+if not hasattr(deflate.DeflateIO, "write"):
+    print("SKIP")
+    raise SystemExit
+
+# Simple compression & decompression.
+b = io.BytesIO()
+g = deflate.DeflateIO(b, deflate.RAW)
+data = b"micropython"
+N = 10
+for i in range(N):
+    g.write(data)
+g.close()
+result_raw = b.getvalue()
+print(len(result_raw) < len(data) * N)
+b = io.BytesIO(result_raw)
+g = deflate.DeflateIO(b, deflate.RAW)
+print(g.read())
+
+# Same, but using a context manager.
+b = io.BytesIO()
+with deflate.DeflateIO(b, deflate.RAW) as g:
+    for i in range(N):
+        g.write(data)
+result_raw = b.getvalue()
+print(len(result_raw) < len(data) * N)
+b = io.BytesIO(result_raw)
+with deflate.DeflateIO(b, deflate.RAW) as g:
+    print(g.read())
+
+# Writing to a closed underlying stream.
+b = io.BytesIO()
+g = deflate.DeflateIO(b, deflate.RAW)
+g.write(b"micropython")
+b.close()
+try:
+    g.write(b"micropython")
+except ValueError:
+    print("ValueError")
+
+# Writing to a closed DeflateIO.
+b = io.BytesIO()
+g = deflate.DeflateIO(b, deflate.RAW)
+g.write(b"micropython")
+g.close()
+try:
+    g.write(b"micropython")
+except OSError:
+    print("OSError")
+
+
+def decompress(data, *args):
+    buf = io.BytesIO(data)
+    with deflate.DeflateIO(buf, *args) as g:
+        return g.read()
+
+
+def compress(data, *args):
+    b = io.BytesIO()
+    with deflate.DeflateIO(b, *args) as g:
+        g.write(data)
+    return b.getvalue()
+
+
+def compress_error(data, *args):
+    try:
+        compress(data, *args)
+    except OSError:
+        print("OSError")
+    except ValueError:
+        print("ValueError")
+
+
+# More test patterns.
+PATTERNS_RAW = (
+    (b"0", b"3\x00\x00"),
+    (b"a", b"K\x04\x00"),
+    (b"0" * 100, b"3\xa0\x03\x00\x00"),
+    (
+        bytes(range(64)),
+        b"c`dbfaec\xe7\xe0\xe4\xe2\xe6\xe1\xe5\xe3\x17\x10\x14\x12\x16\x11\x15\x13\x97\x90\x94\x92\x96\x91\x95\x93WPTRVQUS\xd7\xd0\xd4\xd2\xd6\xd1\xd5\xd370426153\xb7\xb0\xb4\xb2\xb6\xb1\xb5\xb3\x07\x00",
+    ),
+)
+for unpacked, packed in PATTERNS_RAW:
+    print(compress(unpacked) == packed)
+    print(compress(unpacked, deflate.RAW) == packed)
+
+# Verify header and checksum format.
+unpacked = b"hello"
+packed = b"\xcbH\xcd\xc9\xc9\x07\x00"
+
+
+def check_header(n, a, b):
+    if a == b:
+        print(n)
+    else:
+        print(n, a, b)
+
+
+check_header("RAW", compress(unpacked, deflate.RAW), packed)
+check_header(
+    "ZLIB(9)", compress(unpacked, deflate.ZLIB, 9), b"\x18\x95" + packed + b"\x06,\x02\x15"
+)
+check_header(
+    "ZLIB(15)", compress(unpacked, deflate.ZLIB, 15), b"\x78\x9c" + packed + b"\x06,\x02\x15"
+)
+check_header(
+    "GZIP",
+    compress(unpacked, deflate.GZIP, 9),
+    b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x04\x03" + packed + b"\x86\xa6\x106\x05\x00\x00\x00",
+)
+
+# Valid wbits values.
+compress_error(unpacked, deflate.RAW, -1)
+print(len(compress(unpacked, deflate.RAW, 0)))
+compress_error(unpacked, deflate.RAW, 1)
+compress_error(unpacked, deflate.RAW, 4)
+for i in range(5, 16):
+    print(len(compress(unpacked, deflate.RAW, i)))
+compress_error(unpacked, deflate.RAW, 16)
+
+# Invalid values for format.
+compress_error(unpacked, -1)
+compress_error(unpacked, 5)
+
+# Fill buf with a predictable pseudorandom sequence.
+buf = bytearray(1024)
+lfsr = 1 << 15 | 1
+for i in range(len(buf)):
+    bit = (lfsr ^ (lfsr >> 1) ^ (lfsr >> 3) ^ (lfsr >> 12)) & 1
+    lfsr = (lfsr >> 1) | (bit << 15)
+    buf[i] = lfsr & 0xFF
+
+# Verify that compression improves as the window size increases.
+prev_len = len(buf)
+for wbits in range(5, 10):
+    result = compress(buf, deflate.RAW, wbits)
+    next_len = len(result)
+    print(next_len < prev_len and decompress(result, deflate.RAW, wbits) == buf)
+    prev_len = next_len
+
+# Verify that compression is optimal: in the bytes below, the final "123" should be
+# compressed by referencing the "123" just before it, and not the one all the way back
+# at the start of the bytes.
+compressed = compress(b"1234567890abcdefghijklmnopqrstuvwxyz123123", deflate.RAW)
+print(len(compressed), compressed)
diff --git a/tests/extmod/deflate_compress.py.exp b/tests/extmod/deflate_compress.py.exp
new file mode 100644
index 00000000000..7b00fa3cb29
--- /dev/null
+++ b/tests/extmod/deflate_compress.py.exp
@@ -0,0 +1,42 @@
+True
+b'micropythonmicropythonmicropythonmicropythonmicropythonmicropythonmicropythonmicropythonmicropythonmicropython'
+True
+b'micropythonmicropythonmicropythonmicropythonmicropythonmicropythonmicropythonmicropythonmicropythonmicropython'
+ValueError
+OSError
+True
+True
+True
+True
+True
+True
+True
+True
+RAW
+ZLIB(9)
+ZLIB(15)
+GZIP
+ValueError
+7
+ValueError
+ValueError
+7
+7
+7
+7
+7
+7
+7
+7
+7
+7
+7
+ValueError
+ValueError
+ValueError
+False
+True
+True
+True
+True
+41 b'3426153\xb7\xb04HLJNIMK\xcf\xc8\xcc\xca\xce\xc9\xcd\xcb/(,*.)-+\xaf\xa8\xac\x02\xaa\x01"\x00'
diff --git a/tests/extmod/deflate_compress_memory_error.py b/tests/extmod/deflate_compress_memory_error.py
new file mode 100644
index 00000000000..19bef87bff3
--- /dev/null
+++ b/tests/extmod/deflate_compress_memory_error.py
@@ -0,0 +1,39 @@
+# Test deflate.DeflateIO compression, with out-of-memory errors.
+
+try:
+    # Check if deflate is available.
+    import deflate
+    import io
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Check if compression is enabled.
+if not hasattr(deflate.DeflateIO, "write"):
+    print("SKIP")
+    raise SystemExit
+
+# Create a compressor object.
+b = io.BytesIO()
+g = deflate.DeflateIO(b, deflate.RAW, 15)
+
+# Then, use up most of the heap.
+l = []
+while True:
+    try:
+        l.append(bytearray(1000))
+    except:
+        break
+l.pop()
+
+# Try to compress.  This will try to allocate a large window and fail.
+try:
+    g.write("test")
+except MemoryError:
+    print("MemoryError")
+
+# Should still be able to close the stream.
+g.close()
+
+# The underlying output stream should be unchanged.
+print(b.getvalue())
diff --git a/tests/extmod/deflate_compress_memory_error.py.exp b/tests/extmod/deflate_compress_memory_error.py.exp
new file mode 100644
index 00000000000..606315c1460
--- /dev/null
+++ b/tests/extmod/deflate_compress_memory_error.py.exp
@@ -0,0 +1,2 @@
+MemoryError
+b''
diff --git a/tests/extmod/deflate_decompress.py b/tests/extmod/deflate_decompress.py
new file mode 100644
index 00000000000..3ac8880af26
--- /dev/null
+++ b/tests/extmod/deflate_decompress.py
@@ -0,0 +1,185 @@
+try:
+    # Check if deflate is available.
+    import deflate
+    import io
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    # Check there's enough memory to deflate gzip streams.
+    # zlib.compress(b'', wbits=25)
+    empty_gzip = (
+        b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    )
+    deflate.DeflateIO(io.BytesIO(empty_gzip)).read()
+except MemoryError:
+    print("SKIP")
+    raise SystemExit
+
+# zlib.compress(b'micropython hello world hello world micropython', wbits=-9)
+data_raw = b'\xcb\xcdL.\xca/\xa8,\xc9\xc8\xcfS\xc8H\xcd\xc9\xc9W(\xcf/\xcaIAa\xe7"\xd4\x00\x00'
+# zlib.compress(b'micropython hello world hello world micropython', wbits=9)
+data_zlib = b'\x18\x95\xcb\xcdL.\xca/\xa8,\xc9\xc8\xcfS\xc8H\xcd\xc9\xc9W(\xcf/\xcaIAa\xe7"\xd4\x00\x00\xbc\xfa\x12\x91'
+# zlib.compress(b'micropython hello world hello world micropython', wbits=25)
+data_gzip = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\xcb\xcdL.\xca/\xa8,\xc9\xc8\xcfS\xc8H\xcd\xc9\xc9W(\xcf/\xcaIAa\xe7"\xd4\x00\x00"\xeb\xc4\x98/\x00\x00\x00'
+
+# compress(b'hello' + bytearray(300) + b'hello', format=deflate.RAW, 5)
+data_wbits_5 = b"\xcbH\xcd\xc9\xc9g\x18\xe9\x00\x08\x88\x95\xcfH\xcd\xc9\xc9\x07\x00"
+# compress(b'hello' + bytearray(300) + b'hello', format=deflate.RAW, 6)
+data_wbits_6 = b"\xcbH\xcd\xc9\xc9g\x18\xe9\x00\x08\x88\xd5\x9f\x91\x9a\x93\x93\x0f\x00"
+# compress(b'hello' + bytearray(300) + b'hello', format=deflate.RAW, 8)
+data_wbits_8 = b"\xcbH\xcd\xc9\xc9g\x18\xe9\x00\x08\x88\xf5\x7fFjNN>\x00"
+# compress(b'hello' + bytearray(2000) + b'hello', format=deflate.RAW, 10)
+data_wbits_10 = b"\xcbH\xcd\xc9\xc9g\x18\xe9\x00\x08Fz\x18\x00\xc3`\xa4'\x03`2\x18\xe99\x01\x98\x13Fz\xfe\x07\xe6\xff\x91\x9e\xff\x81\xf9\x7f\xa4\xe7\x7f`\xfe\x1f\xba\xf9?#5''\x1f\x00"
+
+
+def decompress(data, *args):
+    buf = io.BytesIO(data)
+    with deflate.DeflateIO(buf, *args) as g:
+        return g.read()
+
+
+def decompress_error(data, *args):
+    try:
+        decompress(data, *args)
+    except OSError:
+        print("OSError")
+    except EOFError:
+        print("EOFError")
+    except ValueError:
+        print("ValueError")
+
+
+# Basic handling of format and detection.
+print(decompress(data_raw, deflate.RAW))
+print(decompress(data_zlib, deflate.ZLIB))
+print(decompress(data_gzip, deflate.GZIP))
+print(decompress(data_zlib))  # detect zlib/gzip.
+print(decompress(data_gzip))  # detect zlib/gzip.
+
+decompress_error(data_raw)  # cannot detect zlib/gzip from raw stream
+decompress_error(data_raw, deflate.ZLIB)
+decompress_error(data_raw, deflate.GZIP)
+decompress_error(data_zlib, deflate.RAW)
+decompress_error(data_zlib, deflate.GZIP)
+decompress_error(data_gzip, deflate.RAW)
+decompress_error(data_gzip, deflate.ZLIB)
+
+# Invalid data stream.
+decompress_error(b"abcef", deflate.RAW)
+
+# Invalid block type. final-block, block-type=3.
+decompress_error(b"\x07", deflate.RAW)
+
+# Truncated stream.
+decompress_error(data_raw[:10], deflate.RAW)
+
+# Partial reads.
+buf = io.BytesIO(data_zlib)
+with deflate.DeflateIO(buf) as g:
+    print(buf.seek(0, 1))  # verify stream is not read until first read of the DeflateIO stream.
+    print(g.read(1))
+    print(buf.seek(0, 1))  # verify that only the minimal amount is read from the source
+    print(g.read(1))
+    print(buf.seek(0, 1))
+    print(g.read(2))
+    print(buf.seek(0, 1))
+    print(g.read())
+    print(buf.seek(0, 1))
+    print(g.read(1))
+    print(buf.seek(0, 1))
+    print(g.read())
+
+# Invalid zlib checksum (+ length for gzip). Note: only checksum errors are
+# currently detected, see the end of uzlib_uncompress_chksum().
+decompress_error(data_zlib[:-4] + b"\x00\x00\x00\x00")
+decompress_error(data_gzip[:-8] + b"\x00\x00\x00\x00\x00\x00\x00\x00")
+decompress_error(data_zlib[:-4] + b"\x00\x00\x00\x00", deflate.ZLIB)
+decompress_error(data_gzip[:-8] + b"\x00\x00\x00\x00\x00\x00\x00\x00", deflate.GZIP)
+
+# Reading from a closed underlying stream.
+b = io.BytesIO(data_raw)
+g = deflate.DeflateIO(b, deflate.RAW)
+g.read(4)
+b.close()
+try:
+    g.read(4)
+except ValueError:
+    print("ValueError")
+
+# Reading from a closed DeflateIO.
+b = io.BytesIO(data_raw)
+g = deflate.DeflateIO(b, deflate.RAW)
+g.read(4)
+g.close()
+try:
+    g.read(4)
+except OSError:
+    print("OSError")
+
+# Gzip header with extra flags (FCOMMENT FNAME FEXTRA FHCRC) enabled.
+data_gzip_header_extra = b"\x1f\x8b\x08\x1e}\x9a\x9bd\x02\x00\x00\x00\x00\x00\x00\xff\xcb\xcdL.\xca/\xa8,\xc9\xc8\xcf\x03\x00\xf2KF>\x0b\x00\x00\x00"
+print(decompress(data_gzip_header_extra))
+
+# Test patterns.
+PATTERNS_ZLIB = [
+    # Packed results produced by CPy's zlib.compress()
+    (b"0", b"x\x9c3\x00\x00\x001\x001"),
+    (b"a", b"x\x9cK\x04\x00\x00b\x00b"),
+    (b"0" * 100, b"x\x9c30\xa0=\x00\x00\xb3q\x12\xc1"),
+    (
+        bytes(range(64)),
+        b"x\x9cc`dbfaec\xe7\xe0\xe4\xe2\xe6\xe1\xe5\xe3\x17\x10\x14\x12\x16\x11\x15\x13\x97\x90\x94\x92\x96\x91\x95\x93WPTRVQUS\xd7\xd0\xd4\xd2\xd6\xd1\xd5\xd370426153\xb7\xb0\xb4\xb2\xb6\xb1\xb5\xb3\x07\x00\xaa\xe0\x07\xe1",
+    ),
+    (b"hello", b"x\x01\x01\x05\x00\xfa\xffhello\x06,\x02\x15"),  # compression level 0
+    # adaptive/dynamic huffman tree
+    (
+        b"13371813150|13764518736|12345678901",
+        b"x\x9c\x05\xc1\x81\x01\x000\x04\x04\xb1\x95\\\x1f\xcfn\x86o\x82d\x06Qq\xc8\x9d\xc5X}I}\x00\x951D>I}\x00\x951D>I}\x00\x951D>I}\x00\x951D",
+        b"x\x9c\x05\xc11\x01\x00\x00\x00\x010\x95\x14py\x84\x12C_\x9bR\x8cV\x8a\xd1J1Z)F\x1fw`\x089",
+    ),
+]
+for unpacked, packed in PATTERNS_ZLIB:
+    print(decompress(packed) == unpacked)
+    print(decompress(packed, deflate.ZLIB) == unpacked)
+
+# Older version's of CPython's zlib module still included the checksum and length (as if it were a zlib/gzip stream).
+# Make sure there're no problem decompressing this.
+data_raw_with_footer = data_raw + b"\x00\x00\x00\x00\x00\x00\x00\x00"
+print(decompress(data_raw_with_footer, deflate.RAW))
+
+# Valid wbits values.
+decompress_error(data_wbits_5, deflate.RAW, -1)
+print(len(decompress(data_wbits_5, deflate.RAW, 0)))
+decompress_error(data_wbits_5, deflate.RAW, 1)
+decompress_error(data_wbits_5, deflate.RAW, 4)
+for i in range(5, 16):
+    print(len(decompress(data_wbits_5, deflate.RAW, i)))
+decompress_error(data_wbits_5, deflate.RAW, 16)
+
+# Invalid values for format.
+decompress_error(data_raw, -1)
+decompress_error(data_raw, 5)
+
+# Data that requires a higher wbits value.
+decompress_error(data_wbits_6, deflate.RAW, 5)
+print(len(decompress(data_wbits_6, deflate.RAW, 6)))
+print(len(decompress(data_wbits_6, deflate.RAW, 7)))
+decompress_error(data_wbits_8, deflate.RAW, 7)
+print(len(decompress(data_wbits_8, deflate.RAW, 8)))
+print(len(decompress(data_wbits_8, deflate.RAW, 9)))
+decompress_error(data_wbits_10, deflate.RAW)
+decompress_error(data_wbits_10, deflate.RAW, 9)
+print(len(decompress(data_wbits_10, deflate.RAW, 10)))
+
+# zlib header sets the size, so works with wbits unset or wbits >= 10.
+data_wbits_10_zlib = b"(\x91\xcbH\xcd\xc9\xc9g\x18\xe9\x00\x08Fz\x18\x00\xc3`\xa4'\x03`2\x18\xe99\x01\x98\x13Fz\xfe\x07\xe6\xff\x91\x9e\xff\x81\xf9\x7f\xa4\xe7\x7f`\xfe\x1f\xba\xf9?#5''\x1f\x00[\xbc\x04)"
+print(len(decompress(data_wbits_10_zlib, deflate.ZLIB)))
+decompress_error(data_wbits_10_zlib, deflate.ZLIB, 9)
+print(len(decompress(data_wbits_10_zlib, deflate.ZLIB, 10)))
+print(len(decompress(data_wbits_10_zlib)))
diff --git a/tests/extmod/deflate_decompress.py.exp b/tests/extmod/deflate_decompress.py.exp
new file mode 100644
index 00000000000..381f2b06859
--- /dev/null
+++ b/tests/extmod/deflate_decompress.py.exp
@@ -0,0 +1,80 @@
+b'micropython hello world hello world micropython'
+b'micropython hello world hello world micropython'
+b'micropython hello world hello world micropython'
+b'micropython hello world hello world micropython'
+b'micropython hello world hello world micropython'
+OSError
+OSError
+OSError
+OSError
+OSError
+OSError
+OSError
+OSError
+OSError
+EOFError
+0
+b'm'
+4
+b'i'
+5
+b'cr'
+7
+b'opython hello world hello world micropython'
+36
+b''
+36
+b''
+OSError
+OSError
+OSError
+OSError
+ValueError
+OSError
+b'micropython'
+True
+True
+True
+True
+True
+True
+True
+True
+True
+True
+True
+True
+True
+True
+b'micropython hello world hello world micropython'
+ValueError
+310
+ValueError
+ValueError
+310
+310
+310
+310
+310
+310
+310
+310
+310
+310
+310
+ValueError
+ValueError
+ValueError
+OSError
+310
+310
+OSError
+310
+310
+OSError
+OSError
+2010
+2010
+OSError
+2010
+2010
diff --git a/tests/extmod/deflate_stream_error.py b/tests/extmod/deflate_stream_error.py
new file mode 100644
index 00000000000..aee6b280333
--- /dev/null
+++ b/tests/extmod/deflate_stream_error.py
@@ -0,0 +1,89 @@
+# Test deflate module with stream errors.
+
+try:
+    # Check if deflate & IOBase are available.
+    import deflate, io
+
+    io.IOBase
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+# Check if compression is enabled.
+if not hasattr(deflate.DeflateIO, "write"):
+    print("SKIP")
+    raise SystemExit
+
+formats = (deflate.RAW, deflate.ZLIB, deflate.GZIP)
+
+# Test error on read when decompressing.
+
+
+class Stream(io.IOBase):
+    def readinto(self, buf):
+        print("Stream.readinto", len(buf))
+        return -1
+
+
+try:
+    deflate.DeflateIO(Stream()).read()
+except OSError as er:
+    print(repr(er))
+
+# Test error on write when compressing.
+
+
+class Stream(io.IOBase):
+    def write(self, buf):
+        print("Stream.write", buf)
+        return -1
+
+
+for format in formats:
+    try:
+        deflate.DeflateIO(Stream(), format).write("a")
+    except OSError as er:
+        print(repr(er))
+
+# Test write after close.
+
+
+class Stream(io.IOBase):
+    def write(self, buf):
+        print("Stream.write", buf)
+        return -1
+
+    def ioctl(self, cmd, arg):
+        print("Stream.ioctl", cmd, arg)
+        return 0
+
+
+try:
+    d = deflate.DeflateIO(Stream(), deflate.RAW, 0, True)
+    d.close()
+    d.write("a")
+except OSError as er:
+    print(repr(er))
+
+# Test error on write when closing.
+
+
+class Stream(io.IOBase):
+    def __init__(self):
+        self.num_writes = 0
+
+    def write(self, buf):
+        print("Stream.write", buf)
+        if self.num_writes >= 4:
+            return -1
+        self.num_writes += 1
+        return len(buf)
+
+
+for format in formats:
+    d = deflate.DeflateIO(Stream(), format)
+    d.write("a")
+    try:
+        d.close()
+    except OSError as er:
+        print(repr(er))
diff --git a/tests/extmod/deflate_stream_error.py.exp b/tests/extmod/deflate_stream_error.py.exp
new file mode 100644
index 00000000000..4ec90d79977
--- /dev/null
+++ b/tests/extmod/deflate_stream_error.py.exp
@@ -0,0 +1,25 @@
+Stream.readinto 1
+OSError(1,)
+Stream.write bytearray(b'K')
+OSError(1,)
+Stream.write bytearray(b'\x18\x95')
+OSError(22,)
+Stream.write bytearray(b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x04\x03')
+OSError(22,)
+Stream.ioctl 4 0
+OSError(22,)
+Stream.write bytearray(b'K')
+Stream.write bytearray(b'\x04')
+Stream.write bytearray(b'\x00')
+Stream.write bytearray(b'\x18\x95')
+Stream.write bytearray(b'K')
+Stream.write bytearray(b'\x04')
+Stream.write bytearray(b'\x00')
+Stream.write bytearray(b'\x00b\x00b')
+OSError(1,)
+Stream.write bytearray(b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x04\x03')
+Stream.write bytearray(b'K')
+Stream.write bytearray(b'\x04')
+Stream.write bytearray(b'\x00')
+Stream.write bytearray(b'C\xbe\xb7\xe8\x01\x00\x00\x00')
+OSError(1,)
diff --git a/tests/extmod/framebuf1.py b/tests/extmod/framebuf1.py
index c8e0132265a..f5e92579f2f 100644
--- a/tests/extmod/framebuf1.py
+++ b/tests/extmod/framebuf1.py
@@ -6,7 +6,7 @@
 
 w = 5
 h = 16
-size = w * h // 8
+size = ((w + 7) & ~7) * ((h + 7) & ~7) // 8
 buf = bytearray(size)
 maps = {
     framebuf.MONO_VLSB: "MONO_VLSB",
@@ -106,6 +106,13 @@
     print("ValueError")
 
 # test legacy constructor
-fbuf = framebuf.FrameBuffer1(buf, w, h)
-fbuf = framebuf.FrameBuffer1(buf, w, h, w)
+if hasattr(framebuf, "FrameBuffer1"):
+    fbuf = framebuf.FrameBuffer1(buf, w, h)
+    fbuf = framebuf.FrameBuffer1(buf, w, h, w)
 print(framebuf.MVLSB == framebuf.MONO_VLSB)
+
+# test get-buffer (returns the original buffer)
+fbuf = framebuf.FrameBuffer(bytearray(2), 8, 1, framebuf.MONO_HLSB)
+fbuf.pixel(0, 0, 1)
+fbuf.pixel(4, 0, 1)
+print(bytearray(fbuf))
diff --git a/tests/extmod/framebuf1.py.exp b/tests/extmod/framebuf1.py.exp
index d954623dee3..4f18e48eca6 100644
--- a/tests/extmod/framebuf1.py.exp
+++ b/tests/extmod/framebuf1.py.exp
@@ -1,68 +1,69 @@
 MONO_VLSB
 0
-bytearray(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
-bytearray(b'\x01\x00\x00\x00\x01\x80\x00\x00\x00\x80')
-bytearray(b'\x01\x00\x00\x00\x01\x80\x00\x00\x00\x00')
+bytearray(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x01\x00\x00\x00\x01\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x01\x00\x00\x00\x01\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
 1 0
-hline bytearray(b'\x02\x02\x02\x02\x02\x00\x00\x00\x00\x00')
-vline bytearray(b'\x00\xff\x00\x00\x00\x00\xff\x00\x00\x00')
-rect bytearray(b'\x00\x0e\n\x0e\x00\x00\x00\x00\x00\x00')
-fill_rect bytearray(b'\x00\x0e\x0e\x0e\x00\x00\x00\x00\x00\x00')
-line bytearray(b'\x00\x02\x04\x08\x00\x00\x00\x00\x00\x00')
-line bytearray(b'\x00\x00\x06\x08\x00\x00\x00\x00\x00\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00')
-bytearray(b'\x00\x00@\x00\x00\x00\x00\x00\x00\x00')
-bytearray(b'\x00\x00\x00@\x00\x00\x00\x00\x00\x00')
-bytearray(b'\x00\x00@\x00\x00\x00\x00\x00\x00\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01')
-bytearray(b'\x00\x7f\x7f\x04\x04\x00\x00\x00\x00\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
-bytearray(b'\xaaU\xaaU\xaa\x00\x00\x00\x00\x00')
+hline bytearray(b'\x02\x02\x02\x02\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+vline bytearray(b'\x00\xff\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+rect bytearray(b'\x00\x0e\n\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+fill_rect bytearray(b'\x00\x0e\x0e\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+line bytearray(b'\x00\x02\x04\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+line bytearray(b'\x00\x00\x06\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x7f\x7f\x04\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\xaaU\xaaU\xaa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
 
 MONO_HLSB
 0
-bytearray(b'\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
-bytearray(b'\x88\x00\x00\x00\x00\x00\x00\x00\x00\x00')
-bytearray(b'\x88\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x88\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x88')
+bytearray(b'\x88\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80')
 1 0
-hline bytearray(b'\x00\xf8\x00\x00\x00\x00\x00\x00\x00\x00')
-vline bytearray(b'@@@@@@@@@@')
-rect bytearray(b'\x00pPp\x00\x00\x00\x00\x00\x00')
-fill_rect bytearray(b'\x00ppp\x00\x00\x00\x00\x00\x00')
-line bytearray(b'\x00@ \x10\x00\x00\x00\x00\x00\x00')
-line bytearray(b'\x00  \x10\x00\x00\x00\x00\x00\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00 \x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00 \x00\x00\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00 \x00\x00\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00')
-bytearray(b'``x````\x00\x00\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
-bytearray(b'P\xa8P\xa8P\xa8P\xa8\x00\x00')
+hline bytearray(b'\x00\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+vline bytearray(b'@@@@@@@@@@@@@@@@')
+rect bytearray(b'\x00pPp\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+fill_rect bytearray(b'\x00ppp\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+line bytearray(b'\x00@ \x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+line bytearray(b'\x00  \x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'``x````\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'P\xa8P\xa8P\xa8P\xa8\x00\x00\x00\x00\x00\x00\x00\x00')
 
 MONO_HMSB
 0
-bytearray(b'\x1f\x1f\x1f\x1f\x1f\x1f\x1f\x1f\x1f\x1f')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
-bytearray(b'\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00')
-bytearray(b'\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x1f\x1f\x1f\x1f\x1f\x1f\x1f\x1f\x1f\x1f\x1f\x1f\x1f\x1f\x1f\x1f')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11')
+bytearray(b'\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01')
 1 0
-hline bytearray(b'\x00\x1f\x00\x00\x00\x00\x00\x00\x00\x00')
-vline bytearray(b'\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02')
-rect bytearray(b'\x00\x0e\n\x0e\x00\x00\x00\x00\x00\x00')
-fill_rect bytearray(b'\x00\x0e\x0e\x0e\x00\x00\x00\x00\x00\x00')
-line bytearray(b'\x00\x02\x04\x08\x00\x00\x00\x00\x00\x00')
-line bytearray(b'\x00\x04\x04\x08\x00\x00\x00\x00\x00\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00')
-bytearray(b'\x06\x06\x1e\x06\x06\x06\x06\x00\x00\x00')
-bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
-bytearray(b'\n\x15\n\x15\n\x15\n\x15\x00\x00')
+hline bytearray(b'\x00\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+vline bytearray(b'\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02')
+rect bytearray(b'\x00\x0e\n\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+fill_rect bytearray(b'\x00\x0e\x0e\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+line bytearray(b'\x00\x02\x04\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+line bytearray(b'\x00\x04\x04\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x06\x06\x1e\x06\x06\x06\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+bytearray(b'\n\x15\n\x15\n\x15\n\x15\x00\x00\x00\x00\x00\x00\x00\x00')
 
 ValueError
 True
+bytearray(b'\x88\x00')
diff --git a/tests/extmod/framebuf16.py b/tests/extmod/framebuf16.py
index cd7f5ec0158..9f373c331e5 100644
--- a/tests/extmod/framebuf16.py
+++ b/tests/extmod/framebuf16.py
@@ -1,11 +1,11 @@
 try:
-    import framebuf, usys
+    import framebuf, sys
 except ImportError:
     print("SKIP")
     raise SystemExit
 
 # This test and its .exp file is based on a little-endian architecture.
-if usys.byteorder != "little":
+if sys.byteorder != "little":
     print("SKIP")
     raise SystemExit
 
diff --git a/tests/extmod/framebuf2.py b/tests/extmod/framebuf2.py
index 097057fe96f..408a483bef5 100644
--- a/tests/extmod/framebuf2.py
+++ b/tests/extmod/framebuf2.py
@@ -48,7 +48,7 @@ def printbuf():
 
 w2 = 2
 h2 = 3
-buf2 = bytearray(w2 * h2 // 4)
+buf2 = bytearray(h2 * ((w2 + 3) // 4))
 fbuf2 = framebuf.FrameBuffer(buf2, w2, h2, framebuf.GS2_HMSB)
 
 # blit
diff --git a/tests/extmod/framebuf_blit.py b/tests/extmod/framebuf_blit.py
new file mode 100644
index 00000000000..b1d98b330a8
--- /dev/null
+++ b/tests/extmod/framebuf_blit.py
@@ -0,0 +1,68 @@
+# Test FrameBuffer.blit method.
+
+try:
+    import framebuf
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def printbuf():
+    print("--8<--")
+    for y in range(h):
+        for x in range(w):
+            print("%02x" % buf[(x + y * w)], end="")
+        print()
+    print("-->8--")
+
+
+w = 5
+h = 4
+buf = bytearray(w * h)
+fbuf = framebuf.FrameBuffer(buf, w, h, framebuf.GS8)
+
+fbuf2 = framebuf.FrameBuffer(bytearray(4), 2, 2, framebuf.GS8)
+fbuf2.fill(0xFF)
+
+# Blit another FrameBuffer, at various locations.
+for x, y in ((-1, -1), (0, 0), (1, 1), (4, 3)):
+    fbuf.fill(0)
+    fbuf.blit(fbuf2, x, y)
+    printbuf()
+
+# Blit a bytes object.
+fbuf.fill(0)
+image = (b"\x10\x11\x12\x13", 2, 2, framebuf.GS8)
+fbuf.blit(image, 1, 1)
+printbuf()
+
+# Blit a bytes object that has a stride.
+fbuf.fill(0)
+image = (b"\x20\x21\xff\x22\x23\xff", 2, 2, framebuf.GS8, 3)
+fbuf.blit(image, 1, 1)
+printbuf()
+
+# Blit a bytes object with a bytes palette.
+fbuf.fill(0)
+image = (b"\x00\x01\x01\x00", 2, 2, framebuf.GS8)
+palette = (b"\xa1\xa2", 2, 1, framebuf.GS8)
+fbuf.blit(image, 1, 1, -1, palette)
+printbuf()
+
+# Not enough elements in the tuple.
+try:
+    fbuf.blit((0, 0, 0), 0, 0)
+except ValueError:
+    print("ValueError")
+
+# Too many elements in the tuple.
+try:
+    fbuf.blit((0, 0, 0, 0, 0, 0), 0, 0)
+except ValueError:
+    print("ValueError")
+
+# Bytes too small.
+try:
+    fbuf.blit((b"", 1, 1, framebuf.GS8), 0, 0)
+except ValueError:
+    print("ValueError")
diff --git a/tests/extmod/framebuf_blit.py.exp b/tests/extmod/framebuf_blit.py.exp
new file mode 100644
index 00000000000..e340f1990c7
--- /dev/null
+++ b/tests/extmod/framebuf_blit.py.exp
@@ -0,0 +1,45 @@
+--8<--
+ff00000000
+0000000000
+0000000000
+0000000000
+-->8--
+--8<--
+ffff000000
+ffff000000
+0000000000
+0000000000
+-->8--
+--8<--
+0000000000
+00ffff0000
+00ffff0000
+0000000000
+-->8--
+--8<--
+0000000000
+0000000000
+0000000000
+00000000ff
+-->8--
+--8<--
+0000000000
+0010110000
+0012130000
+0000000000
+-->8--
+--8<--
+0000000000
+0020210000
+0022230000
+0000000000
+-->8--
+--8<--
+0000000000
+00a1a20000
+00a2a10000
+0000000000
+-->8--
+ValueError
+ValueError
+ValueError
diff --git a/tests/extmod/framebuf_bounds.py b/tests/extmod/framebuf_bounds.py
new file mode 100644
index 00000000000..2a7647cfba4
--- /dev/null
+++ b/tests/extmod/framebuf_bounds.py
@@ -0,0 +1,90 @@
+try:
+    import framebuf
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def test_constructor(*args):
+    try:
+        framebuf.FrameBuffer(*args)
+        print("Valid")
+    except ValueError:
+        print("ValueError")
+
+
+print(framebuf.MONO_HLSB)
+test_constructor(bytearray(20), 10, 10, framebuf.MONO_HLSB)
+test_constructor(bytearray(20), -1, 10, framebuf.MONO_HLSB)
+test_constructor(bytearray(20), 10, -1, framebuf.MONO_HLSB)
+test_constructor(bytearray(20), 10, 10, framebuf.MONO_HLSB, 11)
+test_constructor(bytearray(20), 10, 10, framebuf.MONO_HLSB, 10)
+test_constructor(bytearray(20), 10, 10, framebuf.MONO_HLSB, 9)
+test_constructor(bytearray(20), 10, 10, framebuf.MONO_HLSB, -1)
+test_constructor(bytearray(20), 10, 10, framebuf.MONO_HLSB, 16)
+test_constructor(bytearray(20), 10, 10, framebuf.MONO_HLSB, 17)
+test_constructor(bytearray(19), 8, 10, framebuf.MONO_HLSB, 16)
+test_constructor(bytearray(18), 8, 10, framebuf.MONO_HLSB, 9)
+test_constructor(bytearray(19), 8, 10, framebuf.MONO_HLSB, 16)
+
+print(framebuf.MONO_VLSB)
+test_constructor(bytearray(8), 8, 1, framebuf.MONO_VLSB)
+test_constructor(bytearray(7), 8, 1, framebuf.MONO_VLSB)
+test_constructor(bytearray(8), 8, 8, framebuf.MONO_VLSB)
+test_constructor(bytearray(1), 1, 8, framebuf.MONO_VLSB)
+test_constructor(bytearray(1), 1, 9, framebuf.MONO_VLSB)
+test_constructor(bytearray(2), 1, 9, framebuf.MONO_VLSB)
+test_constructor(bytearray(1), 1, 8, framebuf.MONO_VLSB, 10)
+test_constructor(bytearray(8), 8, 8, framebuf.MONO_VLSB, 10)
+test_constructor(bytearray(8), 8, 8, framebuf.MONO_VLSB, 7)
+test_constructor(bytearray(11), 1, 9, framebuf.MONO_VLSB, 10)
+test_constructor(bytearray(10), 1, 9, framebuf.MONO_VLSB, 10)
+test_constructor(bytearray(11), 1, 16, framebuf.MONO_VLSB, 10)
+
+for f in (framebuf.MONO_HLSB, framebuf.MONO_HMSB):
+    print(f)
+    test_constructor(bytearray(1), 8, 1, f)
+    test_constructor(bytearray(0), 8, 1, f)
+    test_constructor(bytearray(8), 8, 8, f)
+    test_constructor(bytearray(9), 8, 9, f)
+    test_constructor(bytearray(9), 9, 8, f)
+
+print(framebuf.GS2_HMSB)
+test_constructor(bytearray(8), 4, 8, framebuf.GS2_HMSB)
+test_constructor(bytearray(15), 5, 8, framebuf.GS2_HMSB)
+test_constructor(bytearray(16), 5, 8, framebuf.GS2_HMSB)
+test_constructor(bytearray(9), 4, 9, framebuf.GS2_HMSB)
+test_constructor(bytearray(2), 7, 1, framebuf.GS2_HMSB, 9)
+test_constructor(bytearray(4), 7, 2, framebuf.GS2_HMSB, 9)
+test_constructor(bytearray(5), 7, 2, framebuf.GS2_HMSB, 9)
+
+print(framebuf.GS4_HMSB)
+test_constructor(bytearray(8), 2, 8, framebuf.GS4_HMSB)
+test_constructor(bytearray(15), 3, 8, framebuf.GS4_HMSB)
+test_constructor(bytearray(16), 3, 8, framebuf.GS4_HMSB)
+test_constructor(bytearray(9), 2, 9, framebuf.GS4_HMSB)
+test_constructor(bytearray(4), 7, 1, framebuf.GS4_HMSB, 9)
+test_constructor(bytearray(8), 7, 2, framebuf.GS4_HMSB, 9)
+test_constructor(bytearray(9), 7, 2, framebuf.GS4_HMSB, 9)
+
+print(framebuf.GS8)
+test_constructor(bytearray(63), 8, 8, framebuf.GS8)
+test_constructor(bytearray(64), 8, 8, framebuf.GS8)
+test_constructor(bytearray(64), 9, 8, framebuf.GS8)
+test_constructor(bytearray(64), 8, 9, framebuf.GS8)
+test_constructor(bytearray(64), 4, 8, framebuf.GS8, 8)
+test_constructor(bytearray(64), 4, 8, framebuf.GS8, 9)
+test_constructor(bytearray(8), 8, 1, framebuf.GS8, 10)
+test_constructor(bytearray(17), 8, 2, framebuf.GS8, 10)
+test_constructor(bytearray(18), 8, 2, framebuf.GS8, 10)
+
+print(framebuf.RGB565)
+test_constructor(bytearray(127), 8, 8, framebuf.RGB565)
+test_constructor(bytearray(128), 8, 8, framebuf.RGB565)
+test_constructor(bytearray(128), 9, 8, framebuf.RGB565)
+test_constructor(bytearray(128), 8, 9, framebuf.RGB565)
+test_constructor(bytearray(128), 4, 8, framebuf.RGB565, 8)
+test_constructor(bytearray(128), 4, 8, framebuf.RGB565, 9)
+test_constructor(bytearray(16), 8, 1, framebuf.RGB565, 10)
+test_constructor(bytearray(34), 8, 2, framebuf.RGB565, 10)
+test_constructor(bytearray(36), 8, 2, framebuf.RGB565, 10)
diff --git a/tests/extmod/framebuf_bounds.py.exp b/tests/extmod/framebuf_bounds.py.exp
new file mode 100644
index 00000000000..b9c221b42f8
--- /dev/null
+++ b/tests/extmod/framebuf_bounds.py.exp
@@ -0,0 +1,74 @@
+3
+Valid
+ValueError
+ValueError
+Valid
+Valid
+ValueError
+ValueError
+Valid
+ValueError
+Valid
+ValueError
+Valid
+0
+Valid
+ValueError
+Valid
+Valid
+ValueError
+Valid
+Valid
+Valid
+ValueError
+Valid
+ValueError
+Valid
+3
+Valid
+ValueError
+Valid
+Valid
+ValueError
+4
+Valid
+ValueError
+Valid
+Valid
+ValueError
+5
+Valid
+ValueError
+Valid
+Valid
+Valid
+ValueError
+Valid
+2
+Valid
+ValueError
+Valid
+Valid
+Valid
+ValueError
+Valid
+6
+ValueError
+Valid
+ValueError
+ValueError
+Valid
+ValueError
+Valid
+ValueError
+Valid
+1
+ValueError
+Valid
+ValueError
+ValueError
+Valid
+ValueError
+Valid
+ValueError
+Valid
diff --git a/tests/extmod/framebuf_ellipse.py b/tests/extmod/framebuf_ellipse.py
index a4c784aff87..ec0461e66ca 100644
--- a/tests/extmod/framebuf_ellipse.py
+++ b/tests/extmod/framebuf_ellipse.py
@@ -63,3 +63,18 @@ def printbuf():
     fbuf.fill(0)
     fbuf.ellipse(x, y, 6, 12, 0xAA, True)
     printbuf()
+
+# Draw an ellipse with both radius 0
+fbuf.fill(0)
+fbuf.ellipse(15, 15, 0, 0, 0xFF, True)
+printbuf()
+
+# Draw an ellipse with both radius 0 out of bounds
+fbuf.fill(0)
+fbuf.ellipse(45, 45, 0, 0, 0xFF, True)
+printbuf()
+
+# Draw an ellipse with radius 0 and all sectors masked out
+fbuf.fill(0)
+fbuf.ellipse(15, 15, 0, 0, 0xFF, True, 0)
+printbuf()
diff --git a/tests/extmod/framebuf_ellipse.py.exp b/tests/extmod/framebuf_ellipse.py.exp
index ae6ad1ee7e4..96a60c24da1 100644
--- a/tests/extmod/framebuf_ellipse.py.exp
+++ b/tests/extmod/framebuf_ellipse.py.exp
@@ -702,3 +702,99 @@ aaaaaaaaaaaaaaaaaaaaaa00000000000000000000000000000000000000
 aaaaaaaaaaaaaaaaaaaaaa00000000000000000000000000000000000000
 aaaaaaaaaaaaaaaaaaaaaa00000000000000000000000000000000000000
 -->8--
+--8<--
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000ff0000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+-->8--
+--8<--
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+-->8--
+--8<--
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000
+-->8--
diff --git a/tests/extmod/framebuf_palette.py b/tests/extmod/framebuf_palette.py
index f5b15fda739..ad1af2cf4c4 100644
--- a/tests/extmod/framebuf_palette.py
+++ b/tests/extmod/framebuf_palette.py
@@ -1,6 +1,6 @@
 # Test blit between different color spaces
 try:
-    import framebuf, usys
+    import framebuf, sys
 except ImportError:
     print("SKIP")
     raise SystemExit
diff --git a/tests/extmod/framebuf_polygon.py b/tests/extmod/framebuf_polygon.py
index 03130b3bf0f..91a64d74c30 100644
--- a/tests/extmod/framebuf_polygon.py
+++ b/tests/extmod/framebuf_polygon.py
@@ -1,5 +1,3 @@
-import sys
-
 try:
     import framebuf
     from array import array
@@ -13,16 +11,20 @@
     print("SKIP")
     raise SystemExit
 
+try:
+    buf = bytearray(70 * 70)
+except MemoryError:
+    print("SKIP")
+    raise SystemExit
+
 
 def print_buffer(buffer, width, height):
     for row in range(height):
         for col in range(width):
             val = buffer[(row * width) + col]
-            sys.stdout.write(" {:02x}".format(val) if val else " ··")
-        sys.stdout.write("\n")
-
+            print(" {:02x}".format(val) if val else " ··", end="")
+        print()
 
-buf = bytearray(70 * 70)
 
 w = 30
 h = 25
diff --git a/tests/extmod/framebuf_scroll.py b/tests/extmod/framebuf_scroll.py
index db9b6ea1e96..d7c07b1c657 100644
--- a/tests/extmod/framebuf_scroll.py
+++ b/tests/extmod/framebuf_scroll.py
@@ -42,4 +42,9 @@ def prepare_buffer():
 fbuf.scroll(15, 7)
 fbuf.scroll(10, -1)
 fbuf.scroll(1, -10)
+try:
+    fbuf.scroll(1000000000000, -1)
+except OverflowError:
+    # When mp_int_t is 32 bits, this throws OverflowError.
+    pass
 printbuf()
diff --git a/tests/extmod/framebuf_subclass.py b/tests/extmod/framebuf_subclass.py
index 4cd9ea4eb5f..162839e8bf6 100644
--- a/tests/extmod/framebuf_subclass.py
+++ b/tests/extmod/framebuf_subclass.py
@@ -1,13 +1,13 @@
 # test subclassing framebuf.FrameBuffer
 
 try:
-    import framebuf, usys
+    import framebuf, sys
 except ImportError:
     print("SKIP")
     raise SystemExit
 
 # This test and its .exp file is based on a little-endian architecture.
-if usys.byteorder != "little":
+if sys.byteorder != "little":
     print("SKIP")
     raise SystemExit
 
diff --git a/tests/extmod/hashlib_final.py b/tests/extmod/hashlib_final.py
new file mode 100644
index 00000000000..c8fdac92346
--- /dev/null
+++ b/tests/extmod/hashlib_final.py
@@ -0,0 +1,35 @@
+try:
+    import hashlib
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+for algo_name in ("md5", "sha1", "sha256"):
+    algo = getattr(hashlib, algo_name, None)
+    if not algo:
+        continue
+
+    # Running .digest() several times in row is not supported.
+    h = algo(b"123")
+    h.digest()
+    try:
+        h.digest()
+        print("fail")
+    except ValueError:
+        # Expected path, don't print anything so test output is the
+        # same even if the algorithm is not implemented on the port.
+        pass
+
+    # Partial digests are not supported.
+    h = algo(b"123")
+    h.digest()
+    try:
+        h.update(b"456")
+        print("fail")
+    except ValueError:
+        # Expected path, don't print anything so test output is the
+        # same even if the algorithm is not implemented on the port.
+        pass
+
+print("done")
diff --git a/tests/extmod/uhashlib_final.py.exp b/tests/extmod/hashlib_final.py.exp
similarity index 100%
rename from tests/extmod/uhashlib_final.py.exp
rename to tests/extmod/hashlib_final.py.exp
diff --git a/tests/extmod/hashlib_md5.py b/tests/extmod/hashlib_md5.py
new file mode 100644
index 00000000000..ebbe9155e04
--- /dev/null
+++ b/tests/extmod/hashlib_md5.py
@@ -0,0 +1,17 @@
+try:
+    import hashlib
+except ImportError:
+    # MicroPython with hashlib module disabled.
+    print("SKIP")
+    raise SystemExit
+
+try:
+    hashlib.md5
+except AttributeError:
+    # MD5 is only available on some ports
+    print("SKIP")
+    raise SystemExit
+
+md5 = hashlib.md5(b"hello")
+md5.update(b"world")
+print(md5.digest())
diff --git a/tests/extmod/hashlib_sha1.py b/tests/extmod/hashlib_sha1.py
new file mode 100644
index 00000000000..46ffb73fcbe
--- /dev/null
+++ b/tests/extmod/hashlib_sha1.py
@@ -0,0 +1,17 @@
+try:
+    import hashlib
+except ImportError:
+    # MicroPython with hashlib module disabled.
+    print("SKIP")
+    raise SystemExit
+
+try:
+    hashlib.sha1
+except AttributeError:
+    # SHA1 is only available on some ports
+    print("SKIP")
+    raise SystemExit
+
+sha1 = hashlib.sha1(b"hello")
+sha1.update(b"world")
+print(sha1.digest())
diff --git a/tests/extmod/hashlib_sha256.py b/tests/extmod/hashlib_sha256.py
new file mode 100644
index 00000000000..ffa0922a1cf
--- /dev/null
+++ b/tests/extmod/hashlib_sha256.py
@@ -0,0 +1,34 @@
+try:
+    import hashlib
+except ImportError:
+    # MicroPython with hashlib module disabled.
+    print("SKIP")
+    raise SystemExit
+
+
+h = hashlib.sha256()
+print(h.digest())
+
+h = hashlib.sha256()
+h.update(b"123")
+print(h.digest())
+
+h = hashlib.sha256()
+h.update(b"abcd" * 1000)
+print(h.digest())
+
+print(hashlib.sha256(b"\xff" * 64).digest())
+
+# 56 bytes is a boundary case in the algorithm
+print(hashlib.sha256(b"\xff" * 56).digest())
+
+# TODO: running .digest() several times in row is not supported()
+# h = hashlib.sha256(b'123')
+# print(h.digest())
+# print(h.digest())
+
+# TODO: partial digests are not supported
+# h = hashlib.sha256(b'123')
+# print(h.digest())
+# h.update(b'456')
+# print(h.digest())
diff --git a/tests/extmod/heapq1.py b/tests/extmod/heapq1.py
new file mode 100644
index 00000000000..efe208dac3e
--- /dev/null
+++ b/tests/extmod/heapq1.py
@@ -0,0 +1,39 @@
+try:
+    import heapq
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    heapq.heappop([])
+except IndexError:
+    print("IndexError")
+
+try:
+    heapq.heappush((), 1)
+except TypeError:
+    print("TypeError")
+
+
+def pop_and_print(h):
+    l = []
+    while h:
+        l.append(str(heapq.heappop(h)))
+    print(" ".join(l))
+
+
+h = []
+heapq.heappush(h, 3)
+heapq.heappush(h, 1)
+heapq.heappush(h, 2)
+print(h)
+pop_and_print(h)
+
+h = [4, 3, 8, 9, 10, 2, 7, 11, 5]
+heapq.heapify(h)
+print(h)
+heapq.heappush(h, 1)
+heapq.heappush(h, 6)
+heapq.heappush(h, 12)
+print(h)
+pop_and_print(h)
diff --git a/tests/extmod/json_dump.py b/tests/extmod/json_dump.py
new file mode 100644
index 00000000000..0beb4f5f856
--- /dev/null
+++ b/tests/extmod/json_dump.py
@@ -0,0 +1,26 @@
+try:
+    from io import StringIO
+    import json
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+s = StringIO()
+json.dump(False, s)
+print(s.getvalue())
+
+s = StringIO()
+json.dump({"a": (2, [3, None])}, s)
+print(s.getvalue())
+
+# dump to a small-int not allowed
+try:
+    json.dump(123, 1)
+except (AttributeError, OSError):  # CPython and MicroPython have different errors
+    print("Exception")
+
+# dump to an object not allowed
+try:
+    json.dump(123, {})
+except (AttributeError, OSError):  # CPython and MicroPython have different errors
+    print("Exception")
diff --git a/tests/extmod/json_dump_iobase.py b/tests/extmod/json_dump_iobase.py
new file mode 100644
index 00000000000..81105e36dcc
--- /dev/null
+++ b/tests/extmod/json_dump_iobase.py
@@ -0,0 +1,30 @@
+# test json.dump in combination with io.IOBase
+
+try:
+    import io, json
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(io, "IOBase"):
+    print("SKIP")
+    raise SystemExit
+
+
+# a user stream that only has the write method
+class S(io.IOBase):
+    def __init__(self):
+        self.buf = ""
+
+    def write(self, buf):
+        if type(buf) == bytearray:
+            # MicroPython passes a bytearray, CPython passes a str
+            buf = str(buf, "ascii")
+        self.buf += buf
+        return len(buf)
+
+
+# dump to the user stream
+s = S()
+json.dump([123, {}], s)
+print(s.buf)
diff --git a/tests/extmod/json_dump_separators.py b/tests/extmod/json_dump_separators.py
new file mode 100644
index 00000000000..ce39294820f
--- /dev/null
+++ b/tests/extmod/json_dump_separators.py
@@ -0,0 +1,58 @@
+try:
+    from io import StringIO
+    import json
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+for sep in [
+    None,
+    (", ", ": "),
+    (",", ": "),
+    (",", ":"),
+    [", ", ": "],
+    [",", ": "],
+    [",", ":"],
+]:
+    s = StringIO()
+    json.dump(False, s, separators=sep)
+    print(s.getvalue())
+
+    s = StringIO()
+    json.dump({"a": (2, [3, None])}, s, separators=sep)
+    print(s.getvalue())
+
+    # dump to a small-int not allowed
+    try:
+        json.dump(123, 1, separators=sep)
+    except (AttributeError, OSError):  # CPython and MicroPython have different errors
+        print("Exception")
+
+    # dump to an object not allowed
+    try:
+        json.dump(123, {}, separators=sep)
+    except (AttributeError, OSError):  # CPython and MicroPython have different errors
+        print("Exception")
+
+
+try:
+    s = StringIO()
+    json.dump(False, s, separators={"a": 1})
+except (TypeError, ValueError):  # CPython and MicroPython have different errors
+    print("Exception")
+
+# invalid separator types
+for sep in [1, object()]:
+    try:
+        s = StringIO()
+        json.dump(False, s, separators=sep)
+    except TypeError:
+        print("Exception")
+
+# too many/ not enough separators
+for sep in [(), (",", ":", "?"), (",",), []]:
+    try:
+        s = StringIO()
+        json.dump(False, s, separators=sep)
+    except ValueError:
+        print("Exception")
diff --git a/tests/extmod/json_dumps.py b/tests/extmod/json_dumps.py
new file mode 100644
index 00000000000..16f144e44fc
--- /dev/null
+++ b/tests/extmod/json_dumps.py
@@ -0,0 +1,29 @@
+try:
+    import json
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+print(json.dumps(False))
+print(json.dumps(True))
+print(json.dumps(None))
+print(json.dumps(1))
+print(json.dumps("abc"))
+print(json.dumps("\x00\x01\x7e"))
+print(json.dumps([]))
+print(json.dumps([1]))
+print(json.dumps([1, 2]))
+print(json.dumps([1, True]))
+print(json.dumps(()))
+print(json.dumps((1,)))
+print(json.dumps((1, 2)))
+print(json.dumps((1, (2, 3))))
+print(json.dumps({}))
+print(json.dumps({"a": 1}))
+print(json.dumps({"a": (2, [3, None])}))
+print(json.dumps('"quoted"'))
+print(json.dumps("space\n\r\tspace"))
+print(json.dumps({None: -1}))
+print(json.dumps({False: 0}))
+print(json.dumps({True: 1}))
+print(json.dumps({1: 2}))
diff --git a/tests/extmod/json_dumps_extra.py b/tests/extmod/json_dumps_extra.py
new file mode 100644
index 00000000000..9faf8eefabb
--- /dev/null
+++ b/tests/extmod/json_dumps_extra.py
@@ -0,0 +1,9 @@
+# test MicroPython json behaviour that's not valid in CPython
+
+try:
+    import json
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+print(json.dumps(b"1234"))
diff --git a/tests/extmod/ujson_dumps_extra.py.exp b/tests/extmod/json_dumps_extra.py.exp
similarity index 100%
rename from tests/extmod/ujson_dumps_extra.py.exp
rename to tests/extmod/json_dumps_extra.py.exp
diff --git a/tests/extmod/json_dumps_float.py b/tests/extmod/json_dumps_float.py
new file mode 100644
index 00000000000..25fbc5c5126
--- /dev/null
+++ b/tests/extmod/json_dumps_float.py
@@ -0,0 +1,8 @@
+try:
+    import json
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+print(json.dumps(1.2))
+print(json.dumps({1.5: "hi"}))
diff --git a/tests/extmod/json_dumps_ordereddict.py b/tests/extmod/json_dumps_ordereddict.py
new file mode 100644
index 00000000000..e1fa2a659cb
--- /dev/null
+++ b/tests/extmod/json_dumps_ordereddict.py
@@ -0,0 +1,8 @@
+try:
+    import json
+    from collections import OrderedDict
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+print(json.dumps(OrderedDict(((1, 2), (3, 4)))))
diff --git a/tests/extmod/json_dumps_separators.py b/tests/extmod/json_dumps_separators.py
new file mode 100644
index 00000000000..0a95f489a08
--- /dev/null
+++ b/tests/extmod/json_dumps_separators.py
@@ -0,0 +1,57 @@
+try:
+    import json
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+for sep in [
+    None,
+    (", ", ": "),
+    (",", ": "),
+    (",", ":"),
+    [", ", ": "],
+    [",", ": "],
+    [",", ":"],
+]:
+    print(json.dumps(False, separators=sep))
+    print(json.dumps(True, separators=sep))
+    print(json.dumps(None, separators=sep))
+    print(json.dumps(1, separators=sep))
+    print(json.dumps("abc", separators=sep))
+    print(json.dumps("\x00\x01\x7e", separators=sep))
+    print(json.dumps([], separators=sep))
+    print(json.dumps([1], separators=sep))
+    print(json.dumps([1, 2], separators=sep))
+    print(json.dumps([1, True], separators=sep))
+    print(json.dumps((), separators=sep))
+    print(json.dumps((1,), separators=sep))
+    print(json.dumps((1, 2), separators=sep))
+    print(json.dumps((1, (2, 3)), separators=sep))
+    print(json.dumps({}, separators=sep))
+    print(json.dumps({"a": 1}, separators=sep))
+    print(json.dumps({"a": (2, [3, None])}, separators=sep))
+    print(json.dumps('"quoted"', separators=sep))
+    print(json.dumps("space\n\r\tspace", separators=sep))
+    print(json.dumps({None: -1}, separators=sep))
+    print(json.dumps({False: 0}, separators=sep))
+    print(json.dumps({True: 1}, separators=sep))
+    print(json.dumps({1: 2}, separators=sep))
+
+try:
+    json.dumps(False, separators={"a": 1})
+except (TypeError, ValueError):  # CPython and MicroPython have different errors
+    print("Exception")
+
+# invalid separator types
+for sep in [1, object()]:
+    try:
+        json.dumps(False, separators=sep)
+    except TypeError:
+        print("Exception")
+
+# too many/ not enough separators
+for sep in [(), (",", ":", "?"), (",",), []]:
+    try:
+        json.dumps(False, separators=sep)
+    except ValueError:
+        print("Exception")
diff --git a/tests/extmod/json_load.py b/tests/extmod/json_load.py
new file mode 100644
index 00000000000..b2c7733282a
--- /dev/null
+++ b/tests/extmod/json_load.py
@@ -0,0 +1,11 @@
+try:
+    from io import StringIO
+    import json
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+print(json.load(StringIO("null")))
+print(json.load(StringIO('"abc\\u0064e"')))
+print(json.load(StringIO("[false, true, 1, -2]")))
+print(json.load(StringIO('{"a":true}')))
diff --git a/tests/extmod/json_loads.py b/tests/extmod/json_loads.py
new file mode 100644
index 00000000000..092402d715d
--- /dev/null
+++ b/tests/extmod/json_loads.py
@@ -0,0 +1,97 @@
+try:
+    import json
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def my_print(o):
+    if isinstance(o, dict):
+        print("sorted dict", sorted(o.items()))
+    else:
+        print(o)
+
+
+my_print(json.loads("null"))
+my_print(json.loads("false"))
+my_print(json.loads("true"))
+my_print(json.loads("1"))
+my_print(json.loads("-2"))
+my_print(json.loads('"abc\\u0064e"'))
+my_print(json.loads("[]"))
+my_print(json.loads("[null]"))
+my_print(json.loads("[null,false,true]"))
+my_print(json.loads(" [ null , false , true ] "))
+my_print(json.loads("{}"))
+my_print(json.loads('{"a":true}'))
+my_print(json.loads('{"a":null, "b":false, "c":true}'))
+my_print(json.loads('{"a":[], "b":[1], "c":{"3":4}}'))
+my_print(json.loads('"abc\\bdef"'))
+my_print(json.loads('"abc\\fdef"'))
+my_print(json.loads('"abc\\ndef"'))
+my_print(json.loads('"abc\\rdef"'))
+my_print(json.loads('"abc\\tdef"'))
+my_print(json.loads('"abc\\uabcd"'))
+
+# whitespace handling
+my_print(json.loads('{\n\t"a":[]\r\n, "b":[1], "c":{"3":4}     \n\r\t\r\r\r\n}'))
+
+# loading nothing should raise exception
+try:
+    json.loads("")
+except ValueError:
+    print("ValueError")
+
+# string which is not closed
+try:
+    my_print(json.loads('"abc'))
+except ValueError:
+    print("ValueError")
+
+# unaccompanied closing brace
+try:
+    my_print(json.loads("]"))
+except ValueError:
+    print("ValueError")
+
+# unspecified object type
+try:
+    my_print(json.loads("a"))
+except ValueError:
+    print("ValueError")
+
+# bad property name
+try:
+    my_print(json.loads('{{}:"abc"}'))
+except ValueError:
+    print("ValueError")
+
+# unexpected characters after white space
+try:
+    my_print(json.loads("[null]   a"))
+except ValueError:
+    print("ValueError")
+
+# incomplete object declaration
+try:
+    my_print(json.loads('{"a":0,'))
+except ValueError:
+    print("ValueError")
+
+# incomplete nested array declaration
+try:
+    my_print(json.loads('{"a":0, ['))
+except ValueError:
+    print("ValueError")
+
+# incomplete array declaration
+try:
+    my_print(json.loads("[0,"))
+except ValueError:
+    print("ValueError")
+
+# incomplete nested object declaration
+try:
+    my_print(json.loads('[0, {"a":0, '))
+except ValueError:
+    print("ValueError")
diff --git a/tests/extmod/json_loads_bytes.py b/tests/extmod/json_loads_bytes.py
new file mode 100644
index 00000000000..45cd0a35c5a
--- /dev/null
+++ b/tests/extmod/json_loads_bytes.py
@@ -0,0 +1,10 @@
+# test loading from bytes and bytearray (introduced in Python 3.6)
+
+try:
+    import json
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+print(json.loads(b"[1,2]"))
+print(json.loads(bytearray(b"[null]")))
diff --git a/tests/extmod/json_loads_float.py b/tests/extmod/json_loads_float.py
new file mode 100644
index 00000000000..2a8402dc6a2
--- /dev/null
+++ b/tests/extmod/json_loads_float.py
@@ -0,0 +1,17 @@
+try:
+    import json
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def my_print(o):
+    print("%.3f" % o)
+
+
+my_print(json.loads("1.2"))
+my_print(json.loads("1e2"))
+my_print(json.loads("-2.3"))
+my_print(json.loads("-2e3"))
+my_print(json.loads("-2e+3"))
+my_print(json.loads("-2e-3"))
diff --git a/tests/extmod/json_loads_int_64.py b/tests/extmod/json_loads_int_64.py
new file mode 100644
index 00000000000..f6236f1904a
--- /dev/null
+++ b/tests/extmod/json_loads_int_64.py
@@ -0,0 +1,16 @@
+# Parse 64-bit integers from JSON payloads.
+#
+# This also exercises parsing integers from strings
+# where the value may not be null terminated (last line)
+try:
+    import json
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+print(json.loads("9111222333444555666"))
+print(json.loads("-9111222333444555666"))
+print(json.loads("9111222333444555666"))
+print(json.loads("-9111222333444555666"))
+print(json.loads('["9111222333444555666777",9111222333444555666]'))
diff --git a/tests/extmod/machine1.py b/tests/extmod/machine1.py
index 0c7f8122f4a..2f1c0681f64 100644
--- a/tests/extmod/machine1.py
+++ b/tests/extmod/machine1.py
@@ -1,48 +1,43 @@
 # test machine module
 
 try:
-    try:
-        import umachine as machine
-    except ImportError:
-        import machine
+    import machine
+
     machine.mem8
 except:
     print("SKIP")
     raise SystemExit
 
-print(machine.mem8)
+import unittest
 
-try:
-    machine.mem16[1]
-except ValueError:
-    print("ValueError")
 
-try:
-    machine.mem16[1] = 1
-except ValueError:
-    print("ValueError")
+class Test(unittest.TestCase):
+    def test_mem8_print(self):
+        self.assertEqual(repr(machine.mem8), "<8-bit memory>")
 
-try:
-    del machine.mem8[0]
-except TypeError:
-    print("TypeError")
+    def test_alignment(self):
+        with self.assertRaises(ValueError):
+            machine.mem16[1]
 
-try:
-    machine.mem8[0:1]
-except TypeError:
-    print("TypeError")
+        with self.assertRaises(ValueError):
+            machine.mem16[1] = 1
 
-try:
-    machine.mem8[0:1] = 10
-except TypeError:
-    print("TypeError")
+    def test_operations(self):
+        with self.assertRaises(TypeError):
+            del machine.mem8[0]
 
-try:
-    machine.mem8["hello"]
-except TypeError:
-    print("TypeError")
+        with self.assertRaises(TypeError):
+            machine.mem8[0:1]
 
-try:
-    machine.mem8["hello"] = 10
-except TypeError:
-    print("TypeError")
+        with self.assertRaises(TypeError):
+            machine.mem8[0:1] = 10
+
+        with self.assertRaises(TypeError):
+            machine.mem8["hello"]
+
+        with self.assertRaises(TypeError):
+            machine.mem8["hello"] = 10
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/extmod/machine1.py.exp b/tests/extmod/machine1.py.exp
deleted file mode 100644
index 25048596908..00000000000
--- a/tests/extmod/machine1.py.exp
+++ /dev/null
@@ -1,8 +0,0 @@
-<8-bit memory>
-ValueError
-ValueError
-TypeError
-TypeError
-TypeError
-TypeError
-TypeError
diff --git a/tests/extmod/machine_disable_irq.py b/tests/extmod/machine_disable_irq.py
new file mode 100644
index 00000000000..31321287222
--- /dev/null
+++ b/tests/extmod/machine_disable_irq.py
@@ -0,0 +1,26 @@
+# Test executing Python code while IRQs disabled.
+try:
+    from machine import disable_irq, enable_irq
+    from time import ticks_us
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+# Structured to also nest disable_irq() calls
+def f(n):
+    st_irq = disable_irq()
+    if n:
+        f(n - 1)
+    else:
+        # busy-wait in a tight loop for 1ms, to simulate doing some work in a critical section
+        # (can't wait for time_us() to increment as not all ports will "tick" with interrupts off.)
+        for _ in range(100):
+            ticks_us()
+    enable_irq(st_irq)
+
+
+for nest in range(3):
+    print(nest)
+    for _ in range(5):
+        f(nest)
diff --git a/tests/extmod/machine_disable_irq.py.exp b/tests/extmod/machine_disable_irq.py.exp
new file mode 100644
index 00000000000..4539bbf2d22
--- /dev/null
+++ b/tests/extmod/machine_disable_irq.py.exp
@@ -0,0 +1,3 @@
+0
+1
+2
diff --git a/tests/extmod/machine_i2s_rate.py b/tests/extmod/machine_i2s_rate.py
index d2cd1ae105f..9262a42f491 100644
--- a/tests/extmod/machine_i2s_rate.py
+++ b/tests/extmod/machine_i2s_rate.py
@@ -8,29 +8,41 @@
 
 import time, sys
 
+MAX_DELTA_MS = 4
+
 # Configure pins based on the board.
+# Tuples of (i2s_id, sck_pin, ws_pin, sd_tx_pin, sd_rx_pin)
+# A board must have at least one instance to test, both TX and RX mode.
 if "pyboard" in sys.platform:
-    i2s_id = 2
-    sck_pin = Pin("Y6")
-    ws_pin = Pin("Y5")
-    sd_tx_pin = sd_rx_pin = Pin("Y8")
+    i2s_instances = ((2, Pin("Y6"), Pin("Y5"), Pin("Y8"), Pin("Y8")),)
 elif "rp2" in sys.platform:
-    i2s_id = 1
-    sck_pin = Pin(0)
-    ws_pin = Pin(1)
-    sd_tx_pin = sd_rx_pin = Pin(2)
+    i2s_instances = (
+        (0, Pin(0), Pin(1), Pin(2), Pin(2)),
+        (1, Pin(0), Pin(1), Pin(2), Pin(2)),
+    )
 elif "mimxrt" in sys.platform:
-    i2s_id = 1
-    sck_pin = Pin(26)
-    ws_pin = Pin(27)
-    sd_tx_pin = Pin(7)
-    sd_rx_pin = Pin(8)
+    i2s_instances = (
+        (1, Pin("D26"), Pin("D27"), Pin("D7"), Pin("D8")),
+        (2, Pin("D4"), Pin("D3"), Pin("D2"), None),
+    )
+elif "esp32" in sys.platform:
+    if "ESP32-C3" in sys.implementation._machine:
+        i2s_instances = ((0, Pin(6), Pin(7), Pin(10), Pin(11)),)
+    elif "ESP32-H2" in sys.implementation._machine:
+        i2s_instances = ((0, Pin(3), Pin(4), Pin(10), Pin(12)),)
+    else:
+        i2s_instances = ((0, Pin(18), Pin(19), Pin(21), Pin(14)),)
+    # Allow for small additional RTOS overhead
+    MAX_DELTA_MS = 8
 
 TEST_BYTES = b"01234567"
 RATE = 11025  # frames/sec
 
 
-def test(mode, sd_pin, bits_per_sample, frame_format):
+def test(i2s_id, sck_pin, ws_pin, sd_pin, mode, bits_per_sample, frame_format):
+    if sd_pin is None:
+        return
+
     i2s = I2S(
         i2s_id,
         sck=sck_pin,
@@ -48,16 +60,16 @@ def test(mode, sd_pin, bits_per_sample, frame_format):
     else:
         channels = 2
     bits_per_frame = bits_per_sample * channels
-    buf_len_250ms = bits_per_frame // 8 * RATE // 4
+    buf_len_200ms = bits_per_frame // 8 * RATE // 5
 
     # Create test data and preload I2S buffers.
     if mode == I2S.TX:
         mode_str = "TX"
-        data = TEST_BYTES * (buf_len_250ms // len(TEST_BYTES))
+        data = TEST_BYTES * (buf_len_200ms // len(TEST_BYTES))
         i2s.write(data)
     else:
         mode_str = "RX"
-        data = bytearray(len(TEST_BYTES) * (buf_len_250ms // len(TEST_BYTES)))
+        data = bytearray(len(TEST_BYTES) * (buf_len_200ms // len(TEST_BYTES)))
         i2s.readinto(data)
 
     # Time how long it takes to read/write 2 lots of data.
@@ -72,15 +84,28 @@ def test(mode, sd_pin, bits_per_sample, frame_format):
 
     i2s.deinit()
 
-    # Print out test result, time should be in range of 500ms.
-    print(mode_str, bits_per_sample, channels, abs(dt - 500) <= 4)
+    # Time should be in range of 400ms.
+    time_delta = abs(dt - 400)
+    time_in_range = time_delta <= MAX_DELTA_MS
+
+    # Print out test result if requested, or if time not in range.
+    if print_results or not time_in_range:
+        print(mode_str, bits_per_sample, channels, time_in_range or time_delta)
+
+
+print_results = True
 
+for i2s_id, sck_pin, ws_pin, sd_tx_pin, sd_rx_pin in i2s_instances:
+    test(i2s_id, sck_pin, ws_pin, sd_tx_pin, I2S.TX, 16, I2S.MONO)
+    test(i2s_id, sck_pin, ws_pin, sd_tx_pin, I2S.TX, 16, I2S.STEREO)
+    test(i2s_id, sck_pin, ws_pin, sd_tx_pin, I2S.TX, 32, I2S.MONO)
+    test(i2s_id, sck_pin, ws_pin, sd_tx_pin, I2S.TX, 32, I2S.STEREO)
+    test(i2s_id, sck_pin, ws_pin, sd_rx_pin, I2S.RX, 16, I2S.MONO)
+    test(i2s_id, sck_pin, ws_pin, sd_rx_pin, I2S.RX, 16, I2S.STEREO)
+    test(i2s_id, sck_pin, ws_pin, sd_rx_pin, I2S.RX, 32, I2S.MONO)
+    test(i2s_id, sck_pin, ws_pin, sd_rx_pin, I2S.RX, 32, I2S.STEREO)
 
-test(I2S.TX, sd_tx_pin, 16, I2S.MONO)
-test(I2S.TX, sd_tx_pin, 16, I2S.STEREO)
-test(I2S.TX, sd_tx_pin, 32, I2S.MONO)
-test(I2S.TX, sd_tx_pin, 32, I2S.STEREO)
-test(I2S.RX, sd_rx_pin, 16, I2S.MONO)
-test(I2S.RX, sd_rx_pin, 16, I2S.STEREO)
-test(I2S.RX, sd_rx_pin, 32, I2S.MONO)
-test(I2S.RX, sd_rx_pin, 32, I2S.STEREO)
+    # For any remaining tests, don't print the results if they pass.
+    # This is to have the same output on all boards regardless of
+    # how many I2S instances they test.
+    print_results = False
diff --git a/tests/extmod/machine_mem_backup.py b/tests/extmod/machine_mem_backup.py
new file mode 100644
index 00000000000..db2ff910a73
--- /dev/null
+++ b/tests/extmod/machine_mem_backup.py
@@ -0,0 +1,58 @@
+# Test machine.mem_backup() function.
+
+try:
+    import machine
+
+    mem = machine.mem_backup()
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+# Discovery: -1 returns a tuple of all regions.
+regions = machine.mem_backup(-1)
+print("regions is tuple:", isinstance(regions, tuple))
+print("at least one region:", len(regions) >= 1)
+print("all memoryviews:", all(isinstance(r, memoryview) for r in regions))
+print("all len > 0:", all(len(r) > 0 for r in regions))
+print("all valid itemsize:", all(r.itemsize in (1, 4) for r in regions))
+
+# Default region (index 0).
+print("memoryview:", isinstance(mem, memoryview))
+print("len > 0:", len(mem) > 0)
+print("itemsize ok:", mem.itemsize in (1, 4))
+
+# Small-value write/read.
+mem[0] = 42
+print(mem[0] == 42)
+
+if mem.itemsize == 4:
+    # Values >= 0x40000000 are big-ints on 32-bit targets; check that
+    # word-only backup registers receive them as aligned word writes.
+    for val in (0x40000000, 0x41020304, 0x7FFFFFFF, 0xDEADBEEF):
+        mem[0] = val
+        print(mem[0] == val)
+    mem[0] = -1
+    print(mem[0] == 0xFFFFFFFF)
+else:
+    for val in (0x80, 0xA5, 0xFF, 0xC3):
+        mem[0] = val
+        print(mem[0] == val)
+    mem[0] = 0
+    print(mem[0] == 0)
+
+last = len(mem) - 1
+mem[last] = 1
+print(mem[last] == 1)
+
+# Out-of-range region raises ValueError.
+try:
+    machine.mem_backup(len(regions))
+    print("no error")
+except ValueError:
+    print("ValueError")
+
+try:
+    machine.mem_backup(-2)
+    print("no error")
+except ValueError:
+    print("ValueError")
diff --git a/tests/extmod/machine_mem_backup.py.exp b/tests/extmod/machine_mem_backup.py.exp
new file mode 100644
index 00000000000..c3d858c86d6
--- /dev/null
+++ b/tests/extmod/machine_mem_backup.py.exp
@@ -0,0 +1,17 @@
+regions is tuple: True
+at least one region: True
+all memoryviews: True
+all len > 0: True
+all valid itemsize: True
+memoryview: True
+len > 0: True
+itemsize ok: True
+True
+True
+True
+True
+True
+True
+True
+ValueError
+ValueError
diff --git a/tests/extmod/machine_mem_backup_slice.py b/tests/extmod/machine_mem_backup_slice.py
new file mode 100644
index 00000000000..2b9c8048be3
--- /dev/null
+++ b/tests/extmod/machine_mem_backup_slice.py
@@ -0,0 +1,20 @@
+# Test machine.mem_backup() function.
+
+try:
+    import machine
+
+    mem = machine.mem_backup()
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+if mem.itemsize != 1:
+    print("SKIP")
+    raise SystemExit
+
+mem[0:1] = bytes(b"\x11")
+print(mem[0:1].hex())
+mem[0:3] = bytes(b"\x22\x33\x44")
+print(mem[0:3].hex())
+mem[1:5] = bytes(b"\x55\x66\x77\x88")
+print(mem[1:5].hex())
diff --git a/tests/extmod/machine_mem_backup_slice.py.exp b/tests/extmod/machine_mem_backup_slice.py.exp
new file mode 100644
index 00000000000..d517805fb5f
--- /dev/null
+++ b/tests/extmod/machine_mem_backup_slice.py.exp
@@ -0,0 +1,3 @@
+11
+223344
+55667788
diff --git a/tests/extmod/machine_pinbase.py b/tests/extmod/machine_pinbase.py
index 8bddd4bb706..4c467c2d26e 100644
--- a/tests/extmod/machine_pinbase.py
+++ b/tests/extmod/machine_pinbase.py
@@ -1,8 +1,6 @@
 try:
-    try:
-        import umachine as machine
-    except ImportError:
-        import machine
+    import machine
+
     machine.PinBase
 except:
     print("SKIP")
diff --git a/tests/extmod/machine_pulse.py b/tests/extmod/machine_pulse.py
index 65d15fb3513..c793134e197 100644
--- a/tests/extmod/machine_pulse.py
+++ b/tests/extmod/machine_pulse.py
@@ -1,8 +1,6 @@
 try:
-    try:
-        import umachine as machine
-    except ImportError:
-        import machine
+    import machine
+
     machine.PinBase
     machine.time_pulse_us
 except:
diff --git a/tests/extmod/machine_rtc.py b/tests/extmod/machine_rtc.py
new file mode 100644
index 00000000000..17518c23305
--- /dev/null
+++ b/tests/extmod/machine_rtc.py
@@ -0,0 +1,24 @@
+# Test basic behaviour of machine.RTC.
+
+try:
+    from machine import RTC
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+rtc = RTC()
+
+# Save datetime.
+orig_datetime = rtc.datetime()
+
+# Set datetime to a known value.
+rtc.datetime((2020, 1, 1, 0, 0, 0, 0, 0))
+
+# Check that getting and setting the datetime give expected values.
+for i in range(4):
+    now = rtc.datetime()
+    print(now[0], now[1], now[2], now[4], now[5])
+    rtc.datetime(now)
+
+# Restore datetime.
+rtc.datetime(orig_datetime)
diff --git a/tests/extmod/machine_rtc.py.exp b/tests/extmod/machine_rtc.py.exp
new file mode 100644
index 00000000000..8debc44693a
--- /dev/null
+++ b/tests/extmod/machine_rtc.py.exp
@@ -0,0 +1,4 @@
+2020 1 1 0 0
+2020 1 1 0 0
+2020 1 1 0 0
+2020 1 1 0 0
diff --git a/tests/extmod/machine_signal.py b/tests/extmod/machine_signal.py
index 1ffdb564360..2f6332748d5 100644
--- a/tests/extmod/machine_signal.py
+++ b/tests/extmod/machine_signal.py
@@ -1,10 +1,8 @@
 # test machine.Signal class
 
 try:
-    try:
-        import umachine as machine
-    except ImportError:
-        import machine
+    import machine
+
     machine.PinBase
     machine.Signal
 except:
diff --git a/tests/extmod/machine_spi_rate.py b/tests/extmod/machine_spi_rate.py
new file mode 100644
index 00000000000..27174bf95e7
--- /dev/null
+++ b/tests/extmod/machine_spi_rate.py
@@ -0,0 +1,112 @@
+# Test machine.SPI data transfer rates
+
+try:
+    from machine import Pin, SPI
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    wr_short = b"abcdefghijklmnop" * 10
+    rd_short = bytearray(len(wr_short))
+    wr_long = wr_short * 20
+    rd_long = bytearray(len(wr_long))
+except MemoryError:
+    print("SKIP")
+    raise SystemExit
+
+import time, sys
+from target_wiring import spi_standalone_args_list
+
+MAX_DELTA_MS = 8
+
+# Tune test parameters based on the target.
+if "alif" in sys.platform:
+    MAX_DELTA_MS = 20
+elif "esp8266" in sys.platform:
+    MAX_DELTA_MS = 50  # port requires much looser timing requirements
+
+
+def get_real_baudrate(spi):
+    # Return the 'real' baudrate for a SPI object, from parsing 'print' output
+    # i.e.
+    # SPI(id=1, baudrate=500000, polarity=0, phase=0, bits=8, firstbit=0, sck=14, mosi=13, miso=12)
+    #
+    # Note the 'real' rate may be quite different to the requested rate, i.e.
+    # on ports where the SPI hardware only supports power of 2 clock dividers.
+    #
+    # Implementation is somewhat fiddly and inefficient to avoid dependency on
+    # 're' module,
+    s = str(spi)
+    s = s.split("baudrate=")[1].split(",")[0]
+    return int(s)
+
+
+def test_instances():
+    print_results = True
+    for spi_args in spi_standalone_args_list:
+        for baudrate in (
+            100_000,
+            250_000,
+            800_000,
+            1_000_000,
+            2_000_000,
+            4_000_000,
+            8_000_000,
+        ):
+            test_spi(spi_args, baudrate, 0, 0, print_results)
+
+        for baudrate in (100_000, 4_000_000):
+            # Test the other polarity and phase settings
+            for polarity, phase in ((0, 1), (1, 0), (1, 1)):
+                test_spi(spi_args, baudrate, polarity, phase, print_results)
+
+        # Ensure the same test output regardless of how many SPI instances are tested
+        print_results = False
+
+
+def test_spi(spi_args, baudrate, polarity, phase, print_results):
+    s = SPI(*spi_args, baudrate=baudrate, polarity=polarity, phase=phase)
+
+    # to keep the test runtime down, use shorter buffer for lower baud rate
+    wr_buf = wr_long if baudrate > 500_000 else wr_short
+    rd_buf = rd_long if baudrate > 500_000 else rd_short
+
+    real_baudrate = get_real_baudrate(s)
+    assert real_baudrate
+    transfer_time_ms = len(wr_buf) * 8 * 1000 // real_baudrate
+
+    def test_write_readinto():
+        s.write_readinto(wr_buf, rd_buf)
+
+    def test_write():
+        s.write(wr_buf)
+
+    def test_readinto():
+        s.readinto(rd_buf)
+
+    for test_func, name in (
+        (test_write_readinto, "write_readinto"),
+        (test_write, "write"),
+        (test_readinto, "readinto"),
+    ):
+        t0 = time.ticks_ms()
+        test_func()
+        transfer_time = time.ticks_diff(time.ticks_ms(), t0)
+        t_delta = abs(transfer_time - transfer_time_ms)
+        t_ok = t_delta < MAX_DELTA_MS
+
+        if print_results or not t_ok:
+            print(
+                None if print_results else spi_args,
+                baudrate,
+                polarity,
+                phase,
+                name,
+                t_ok or t_delta,
+            )
+
+    s.deinit()
+
+
+test_instances()
diff --git a/tests/extmod/machine_spi_rate.py.exp b/tests/extmod/machine_spi_rate.py.exp
new file mode 100644
index 00000000000..6102016154d
--- /dev/null
+++ b/tests/extmod/machine_spi_rate.py.exp
@@ -0,0 +1,39 @@
+None 100000 0 0 write_readinto True
+None 100000 0 0 write True
+None 100000 0 0 readinto True
+None 250000 0 0 write_readinto True
+None 250000 0 0 write True
+None 250000 0 0 readinto True
+None 800000 0 0 write_readinto True
+None 800000 0 0 write True
+None 800000 0 0 readinto True
+None 1000000 0 0 write_readinto True
+None 1000000 0 0 write True
+None 1000000 0 0 readinto True
+None 2000000 0 0 write_readinto True
+None 2000000 0 0 write True
+None 2000000 0 0 readinto True
+None 4000000 0 0 write_readinto True
+None 4000000 0 0 write True
+None 4000000 0 0 readinto True
+None 8000000 0 0 write_readinto True
+None 8000000 0 0 write True
+None 8000000 0 0 readinto True
+None 100000 0 1 write_readinto True
+None 100000 0 1 write True
+None 100000 0 1 readinto True
+None 100000 1 0 write_readinto True
+None 100000 1 0 write True
+None 100000 1 0 readinto True
+None 100000 1 1 write_readinto True
+None 100000 1 1 write True
+None 100000 1 1 readinto True
+None 4000000 0 1 write_readinto True
+None 4000000 0 1 write True
+None 4000000 0 1 readinto True
+None 4000000 1 0 write_readinto True
+None 4000000 1 0 write True
+None 4000000 1 0 readinto True
+None 4000000 1 1 write_readinto True
+None 4000000 1 1 write True
+None 4000000 1 1 readinto True
diff --git a/tests/extmod/machine_timer.py b/tests/extmod/machine_timer.py
index c9a47c40258..cb5f3fa67e2 100644
--- a/tests/extmod/machine_timer.py
+++ b/tests/extmod/machine_timer.py
@@ -1,38 +1,149 @@
-# test machine.Timer
-
 try:
-    import utime, umachine as machine
-
-    machine.Timer
+    from machine import Timer
+    from time import sleep_ms
 except:
     print("SKIP")
     raise SystemExit
 
-# create and deinit
-t = machine.Timer(freq=1)
-t.deinit()
-
-# deinit again
-t.deinit()
-
-# create 2 and deinit
-t = machine.Timer(freq=1)
-t2 = machine.Timer(freq=1)
-t.deinit()
-t2.deinit()
-
-# create 2 and deinit in different order
-t = machine.Timer(freq=1)
-t2 = machine.Timer(freq=1)
-t2.deinit()
-t.deinit()
-
-# create one-shot timer with callback and wait for it to print (should be just once)
-t = machine.Timer(period=1, mode=machine.Timer.ONE_SHOT, callback=lambda t: print("one-shot"))
-utime.sleep_ms(5)
-t.deinit()
-
-# create periodic timer with callback and wait for it to print
-t = machine.Timer(period=4, mode=machine.Timer.PERIODIC, callback=lambda t: print("periodic"))
-utime.sleep_ms(14)
-t.deinit()
+import sys
+
+if sys.platform == "nrf":
+    # Note: The nrf port supports machine.Timer, but is not compatible: It lacks
+    # the .init() method and freq argument, the period argument is microseconds
+    # instead milliseconds, and the ONE_SHOT constant is named ONESHOT.
+    print("SKIP")
+    raise SystemExit
+
+import unittest
+
+# Hardware timers are only supported on the esp32 port
+NUM_HARDWARE_TIMERS = 0
+if sys.platform == "esp32":
+    if "ESP32-C2" in sys.implementation._machine:
+        # Only one hardware timer on ESP32-C2
+        NUM_HARDWARE_TIMERS = 1
+    else:
+        # at least two on other ESP32 SoCs
+        NUM_HARDWARE_TIMERS = 2
+
+# Hard IRQs are not supported on the esp32 port
+SUPPORTS_HARD_IRQ = sys.platform != "esp32"
+
+
+class Test(unittest.TestCase):
+    def test_virtual_create(self):
+        self._test_create(-1)
+        self._test_create_multiple(-1, -1)
+
+    @unittest.skipUnless(NUM_HARDWARE_TIMERS > 0, "no hardware timers")
+    def test_hardware_create(self):
+        self._test_create(0)
+
+    @unittest.skipUnless(NUM_HARDWARE_TIMERS >= 2, "less than 2 hardware timers")
+    def test_hardware_create_multiple(self):
+        self._test_create_multiple(0, 1)
+
+    def test_virtual_softirq(self):
+        self._test_all_freq_period(-1, Timer.ONE_SHOT, False)
+        self._test_all_freq_period(-1, Timer.PERIODIC, False)
+
+    @unittest.skipUnless(SUPPORTS_HARD_IRQ, "no hard-irq support")
+    def test_virtual_hardirq(self):
+        self._test_all_freq_period(-1, Timer.ONE_SHOT, True)
+        self._test_all_freq_period(-1, Timer.PERIODIC, True)
+
+    @unittest.skipUnless(NUM_HARDWARE_TIMERS > 0, "no hardware timers")
+    def test_hardware_softirq(self):
+        self._test_all_freq_period(0, Timer.ONE_SHOT, False)
+        self._test_all_freq_period(0, Timer.PERIODIC, False)
+
+    @unittest.skipUnless(NUM_HARDWARE_TIMERS > 0, "no hardware timers")
+    @unittest.skipUnless(SUPPORTS_HARD_IRQ, "no hard-irq support")
+    def test_hardware_hardirq(self):
+        self._test_all_freq_period(0, Timer.ONE_SHOT, True)
+        self._test_all_freq_period(0, Timer.PERIODIC, True)
+
+    def _test_create(self, id):
+        # create and deinit
+        t = Timer(id)
+        t.init(freq=1)
+        t.deinit()
+
+        # deinit again
+        t.deinit()
+
+        # init a large number of times to catch bugs like
+        # https://github.com/micropython/micropython/issues/19162
+        for _ in range(256):
+            t.init(freq=1)
+        t.deinit()
+
+    def _test_create_multiple(self, *ids):
+        # create and deinit
+        timers = []
+        for id in ids:
+            t = Timer(id)
+            self.assertFalse(t in timers)
+            t.init(freq=1)
+            timers.append(t)
+        for t in timers:
+            t.deinit()
+
+        # create and deinit in reverse order
+        timers = []
+        for id in ids:
+            t = Timer(id)
+            self.assertFalse(t in timers)
+            t.init(freq=1)
+            timers.append(t)
+        for t in reversed(timers):
+            t.deinit()
+
+    def _test_all_freq_period(self, id, mode, hard):
+        # test two different freq and period arguments
+        for period, freq_period_arg in (
+            (1000 // 50, {"freq": 50}),
+            (1000 // 25, {"freq": 25}),
+            (20, {"period": 20}),
+            (40, {"period": 40}),
+        ):
+            callback_results = []
+
+            def _callback(_):
+                try:
+                    allocate = bytearray(1)
+                    locked = False
+                except MemoryError:
+                    locked = True
+                callback_results.append(locked)
+
+            t = Timer(id)
+            t.init(
+                mode=mode,
+                callback=_callback,
+                hard=hard,
+                **freq_period_arg,
+            )
+
+            # Note: These sleep durations are such that there is always at least
+            # 10ms between when the callback fires and we perform our checks,
+            # thus this test also implicitly checks that timers and/or sleep_ms
+            # do not drift more than 10ms.
+            total = 0
+            for duration in (0, 10, 20, 40, 20):
+                sleep_ms(duration)
+                total += duration
+
+                # number of times the callback should have been called
+                n = total // period
+                if mode == Timer.ONE_SHOT:
+                    n = min(n, 1)
+
+                # callback reports whether memory was locked, which
+                # should equal whether the timer uses hard IRQs
+                self.assertEqual([hard] * n, callback_results)
+            t.deinit()
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/extmod/machine_timer.py.exp b/tests/extmod/machine_timer.py.exp
deleted file mode 100644
index 2dd85ba67d3..00000000000
--- a/tests/extmod/machine_timer.py.exp
+++ /dev/null
@@ -1,4 +0,0 @@
-one-shot
-periodic
-periodic
-periodic
diff --git a/tests/extmod/machine_uart_irq_txidle.py b/tests/extmod/machine_uart_irq_txidle.py
new file mode 100644
index 00000000000..6c20503d45f
--- /dev/null
+++ b/tests/extmod/machine_uart_irq_txidle.py
@@ -0,0 +1,47 @@
+# Test machine.UART.IRQ_TXIDLE firing after a transmission.
+# Does not require any external connections.
+
+try:
+    from machine import UART
+
+    UART.IRQ_TXIDLE
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+import time, sys
+from target_wiring import uart_loopback_args, uart_loopback_kwargs
+
+
+def irq(u):
+    print("IRQ_TXIDLE:", u.irq().flags() == u.IRQ_TXIDLE)
+
+
+text = "Hello World" * 20
+
+char_margin = 10  # number of characters to wait before and after the expected IRQ firing
+
+if "psoc-edge" in sys.platform:
+    char_margin = 100
+
+# Test that the IRQ is called after the write has completed.
+for bits_per_s in (2400, 9600, 115200):
+    uart = UART(*uart_loopback_args, baudrate=bits_per_s, **uart_loopback_kwargs)
+    uart.irq(irq, uart.IRQ_TXIDLE)
+
+    # The IRQ_TXIDLE shall trigger after the message has been sent. Thus
+    # the test marks a time window close to the expected of the sending
+    # and the time at which the IRQ should have been fired.
+    # It is just a rough estimation of 'char_margin' characters before and
+    # '2 x char_margin' characters after the data's end.
+
+    bits_per_char = 10  # 1(startbit) + 8(bits) + 1(stopbit) + 0(parity)
+    start_time_us = (len(text) - char_margin) * bits_per_char * 1_000_000 // bits_per_s
+    window_ms = (2 * char_margin) * bits_per_char * 1_000 // bits_per_s + 1
+
+    print("write", bits_per_s)
+    uart.write(text)
+    time.sleep_us(start_time_us)
+    print("ready")
+    time.sleep_ms(window_ms)
+    print("done")
diff --git a/tests/extmod/machine_uart_irq_txidle.py.exp b/tests/extmod/machine_uart_irq_txidle.py.exp
new file mode 100644
index 00000000000..2da51d10faa
--- /dev/null
+++ b/tests/extmod/machine_uart_irq_txidle.py.exp
@@ -0,0 +1,12 @@
+write 2400
+ready
+IRQ_TXIDLE: True
+done
+write 9600
+ready
+IRQ_TXIDLE: True
+done
+write 115200
+ready
+IRQ_TXIDLE: True
+done
diff --git a/tests/extmod/machine_uart_tx.py b/tests/extmod/machine_uart_tx.py
new file mode 100644
index 00000000000..b71f1ae339e
--- /dev/null
+++ b/tests/extmod/machine_uart_tx.py
@@ -0,0 +1,56 @@
+# Test machine.UART transmission.
+# Does not require any external connections.
+
+try:
+    from machine import UART
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+import time, sys
+from target_wiring import uart_loopback_args, uart_loopback_kwargs
+
+initial_delay_ms = 0
+bit_margin = 0
+timing_margin_us = 100
+
+# Tune test parameters based on the target.
+if "alif" in sys.platform:
+    bit_margin = 1
+elif "esp32" in sys.platform:
+    timing_margin_us = 400
+elif "esp8266" in sys.platform:
+    timing_margin_us = 4100
+elif "mimxrt" in sys.platform:
+    initial_delay_ms = 20  # UART sends idle frame after init, so wait for that
+    bit_margin = 1
+elif "nrf" in sys.platform:
+    timing_margin_us = 130
+elif "psoc-edge" in sys.platform:
+    timing_margin_us = 300
+elif "pyboard" in sys.platform:
+    initial_delay_ms = 50  # UART sends idle frame after init, so wait for that
+    bit_margin = 1  # first start-bit must wait to sync with the UART clock
+elif "rp2" in sys.platform:
+    timing_margin_us = 180
+elif "samd" in sys.platform:
+    timing_margin_us = 300
+    bit_margin = 1
+
+# Test that write+flush takes the expected amount of time to execute.
+for bits_per_s in (2400, 9600, 115200):
+    text = "Hello World from MicroPython"
+    uart = UART(*uart_loopback_args, baudrate=bits_per_s, **uart_loopback_kwargs)
+    time.sleep_ms(initial_delay_ms)
+
+    start_us = time.ticks_us()
+    uart.write(text)
+    uart.flush()
+    duration_us = time.ticks_diff(time.ticks_us(), start_us)
+
+    # 1(startbit) + 8(bits) + 1(stopbit) + 0(parity)
+    bits_per_char = 10
+    expect_us = (len(text)) * bits_per_char * 1_000_000 // bits_per_s
+    delta_us = abs(duration_us - expect_us)
+    margin_us = timing_margin_us + bit_margin * 1_000_000 // bits_per_s
+    print(bits_per_s, delta_us <= margin_us or delta_us)
diff --git a/tests/extmod/machine_uart_tx.py.exp b/tests/extmod/machine_uart_tx.py.exp
new file mode 100644
index 00000000000..0432ab94aee
--- /dev/null
+++ b/tests/extmod/machine_uart_tx.py.exp
@@ -0,0 +1,3 @@
+2400 True
+9600 True
+115200 True
diff --git a/tests/extmod/marshal_basic.py b/tests/extmod/marshal_basic.py
new file mode 100644
index 00000000000..9e7b70be482
--- /dev/null
+++ b/tests/extmod/marshal_basic.py
@@ -0,0 +1,38 @@
+# Test the marshal module, basic functionality.
+
+try:
+    import marshal
+
+    (lambda: 0).__code__
+except (AttributeError, ImportError):
+    print("SKIP")
+    raise SystemExit
+
+ftype = type(lambda: 0)
+
+# Test basic dumps and loads.
+print(ftype(marshal.loads(marshal.dumps((lambda: a).__code__)), {"a": 4})())
+
+# Test dumps of a result from compile().
+ftype(marshal.loads(marshal.dumps(compile("print(a)", "", "exec"))), {"print": print, "a": 5})()
+
+# Test marshalling a function with arguments.
+print(ftype(marshal.loads(marshal.dumps((lambda x, y: x + y).__code__)), {})(1, 2))
+
+# Test marshalling a function with default arguments.
+print(ftype(marshal.loads(marshal.dumps((lambda x=0: x).__code__)), {})("arg"))
+
+# Test marshalling a function containing constant objects (a tuple).
+print(ftype(marshal.loads(marshal.dumps((lambda: (None, ...)).__code__)), {})())
+
+# Test instantiating multiple code's with different globals dicts.
+code = marshal.loads(marshal.dumps((lambda: a).__code__))
+f1 = ftype(code, {"a": 1})
+f2 = ftype(code, {"a": 2})
+print(f1(), f2())
+
+# Test unmarshallable object.
+try:
+    marshal.dumps(type)
+except ValueError:
+    print("ValueError")
diff --git a/tests/extmod/marshal_fun_nested.py b/tests/extmod/marshal_fun_nested.py
new file mode 100644
index 00000000000..fcf8f9a0fa4
--- /dev/null
+++ b/tests/extmod/marshal_fun_nested.py
@@ -0,0 +1,79 @@
+# Test the marshal module, with functions that have children.
+
+try:
+    import marshal
+
+    (lambda: 0).__code__
+except (AttributeError, ImportError):
+    print("SKIP")
+    raise SystemExit
+
+
+def f_with_child():
+    def child():
+        return a
+
+    return child
+
+
+def f_with_child_defargs():
+    def child(a="default"):
+        return a
+
+    return child
+
+
+def f_with_child_closure():
+    a = "closure 1"
+
+    def child():
+        return a
+
+    a = "closure 2"
+    return child
+
+
+def f_with_child_closure_defargs():
+    a = "closure defargs 1"
+
+    def child(b="defargs default"):
+        return (a, b)
+
+    a = "closure defargs 1"
+    return child
+
+
+def f_with_list_comprehension(a):
+    return [i + a for i in range(4)]
+
+
+ftype = type(lambda: 0)
+
+# Test function with a child.
+f = ftype(marshal.loads(marshal.dumps(f_with_child.__code__)), {"a": "global"})
+print(f()())
+
+# Test function with a child that has default arguments.
+f = ftype(marshal.loads(marshal.dumps(f_with_child_defargs.__code__)), {})
+print(f()())
+print(f()("non-default"))
+
+# Test function with a child that is a closure.
+f = ftype(marshal.loads(marshal.dumps(f_with_child_closure.__code__)), {})
+print(f()())
+
+# Test function with a child that is a closure and has default arguments.
+f = ftype(marshal.loads(marshal.dumps(f_with_child_closure_defargs.__code__)), {})
+print(f()())
+print(f()("defargs non-default"))
+
+# Test function with a list comprehension (which will be an anonymous child).
+f = ftype(marshal.loads(marshal.dumps(f_with_list_comprehension.__code__)), {})
+print(f(10))
+
+# Test child within a module (the outer scope).
+code = compile("def child(a): return a", "", "exec")
+f = marshal.loads(marshal.dumps(code))
+ctx = {}
+exec(f, ctx)
+print(ctx["child"]("arg"))
diff --git a/tests/extmod/marshal_stress.py b/tests/extmod/marshal_stress.py
new file mode 100644
index 00000000000..b52475c0361
--- /dev/null
+++ b/tests/extmod/marshal_stress.py
@@ -0,0 +1,122 @@
+# Test the marshal module, stressing edge cases.
+
+try:
+    import marshal
+
+    (lambda: 0).__code__
+except (AttributeError, ImportError):
+    print("SKIP")
+    raise SystemExit
+
+ftype = type(lambda: 0)
+
+# Test a large function.
+
+
+def large_function(arg0, arg1, arg2, arg3):
+    # Arguments.
+    print(arg0, arg1, arg2, arg3)
+
+    # Positive medium-sized integer (still a small-int though).
+    print(1234)
+
+    # Negative small-ish integer.
+    print(-20)
+
+    # More than 64 constant objects.
+    x = (0,)
+    x = (1,)
+    x = (2,)
+    x = (3,)
+    x = (4,)
+    x = (5,)
+    x = (6,)
+    x = (7,)
+    x = (8,)
+    x = (9,)
+    x = (10,)
+    x = (11,)
+    x = (12,)
+    x = (13,)
+    x = (14,)
+    x = (15,)
+    x = (16,)
+    x = (17,)
+    x = (18,)
+    x = (19,)
+    x = (20,)
+    x = (21,)
+    x = (22,)
+    x = (23,)
+    x = (24,)
+    x = (25,)
+    x = (26,)
+    x = (27,)
+    x = (28,)
+    x = (29,)
+    x = (30,)
+    x = (31,)
+    x = (32,)
+    x = (33,)
+    x = (34,)
+    x = (35,)
+    x = (36,)
+    x = (37,)
+    x = (38,)
+    x = (39,)
+    x = (40,)
+    x = (41,)
+    x = (42,)
+    x = (43,)
+    x = (44,)
+    x = (45,)
+    x = (46,)
+    x = (47,)
+    x = (48,)
+    x = (49,)
+    x = (50,)
+    x = (51,)
+    x = (52,)
+    x = (53,)
+    x = (54,)
+    x = (55,)
+    x = (56,)
+    x = (57,)
+    x = (58,)
+    x = (59,)
+    x = (60,)
+    x = (61,)
+    x = (62,)
+    x = (63,)
+    x = (64,)
+
+    # Small jump.
+    x = 0
+    while x < 2:
+        print("loop", x)
+        x += 1
+
+    # Large jump.
+    x = 0
+    while x < 2:
+        try:
+            try:
+                try:
+                    print
+                except Exception as e:
+                    print
+                finally:
+                    print
+            except Exception as e:
+                print
+            finally:
+                print
+        except Exception as e:
+            print
+        finally:
+            print("loop", x)
+        x += 1
+
+
+code = marshal.dumps(large_function.__code__)
+ftype(marshal.loads(code), {"print": print})(0, 1, 2, 3)
diff --git a/tests/extmod/os_urandom.py b/tests/extmod/os_urandom.py
new file mode 100644
index 00000000000..982d6d04b96
--- /dev/null
+++ b/tests/extmod/os_urandom.py
@@ -0,0 +1,14 @@
+# Test os.urandom().
+
+try:
+    from os import urandom
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+for n in range(-2, 5, 1):
+    try:
+        r = urandom(n)
+        print(n, type(r), len(r))
+    except ValueError:
+        print(n, "ValueError")
diff --git a/tests/extmod/platform_basic.py b/tests/extmod/platform_basic.py
new file mode 100644
index 00000000000..eb6f2be13c1
--- /dev/null
+++ b/tests/extmod/platform_basic.py
@@ -0,0 +1,8 @@
+try:
+    import platform
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+print(type(platform.python_compiler()))
+print(type(platform.libc_ver()))
diff --git a/tests/extmod/random_basic.py b/tests/extmod/random_basic.py
new file mode 100644
index 00000000000..7cb992fede2
--- /dev/null
+++ b/tests/extmod/random_basic.py
@@ -0,0 +1,29 @@
+try:
+    import random
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# check getrandbits returns a value within the bit range
+for b in (1, 2, 3, 4, 16, 32):
+    for i in range(50):
+        assert random.getrandbits(b) < (1 << b)
+
+# check that seed(0) gives a non-zero value
+random.seed(0)
+print(random.getrandbits(16) != 0)
+
+# check that PRNG is repeatable
+random.seed(1)
+r = random.getrandbits(16)
+random.seed(1)
+print(random.getrandbits(16) == r)
+
+# check that zero bits works
+print(random.getrandbits(0))
+
+# check that it throws an error for negative bits
+try:
+    random.getrandbits(-1)
+except ValueError:
+    print("ValueError")
diff --git a/tests/extmod/urandom_basic.py.exp b/tests/extmod/random_basic.py.exp
similarity index 100%
rename from tests/extmod/urandom_basic.py.exp
rename to tests/extmod/random_basic.py.exp
diff --git a/tests/extmod/random_extra.py b/tests/extmod/random_extra.py
new file mode 100644
index 00000000000..aa05053377b
--- /dev/null
+++ b/tests/extmod/random_extra.py
@@ -0,0 +1,66 @@
+try:
+    import random
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    random.randint
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+print("randrange")
+for i in range(50):
+    assert 0 <= random.randrange(4) < 4
+    assert 2 <= random.randrange(2, 6) < 6
+    assert -2 <= random.randrange(-2, 2) < 2
+    assert random.randrange(1, 9, 2) in (1, 3, 5, 7)
+    assert random.randrange(2, 1, -1) in (1, 2)
+
+# empty range
+try:
+    random.randrange(0)
+except ValueError:
+    print("ValueError")
+
+# empty range
+try:
+    random.randrange(2, 1)
+except ValueError:
+    print("ValueError")
+
+# zero step
+try:
+    random.randrange(2, 1, 0)
+except ValueError:
+    print("ValueError")
+
+# empty range
+try:
+    random.randrange(2, 1, 1)
+except ValueError:
+    print("ValueError")
+
+print("randint")
+for i in range(50):
+    assert 0 <= random.randint(0, 4) <= 4
+    assert 2 <= random.randint(2, 6) <= 6
+    assert -2 <= random.randint(-2, 2) <= 2
+
+# empty range
+try:
+    random.randint(2, 1)
+except ValueError:
+    print("ValueError")
+
+print("choice")
+lst = [1, 2, 5, 6]
+for i in range(50):
+    assert random.choice(lst) in lst
+
+# empty sequence
+try:
+    random.choice([])
+except IndexError:
+    print("IndexError")
diff --git a/tests/extmod/random_extra_float.py b/tests/extmod/random_extra_float.py
new file mode 100644
index 00000000000..03973c58349
--- /dev/null
+++ b/tests/extmod/random_extra_float.py
@@ -0,0 +1,17 @@
+try:
+    import random
+
+    random.random
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+print("random")
+for i in range(50):
+    assert 0 <= random.random() < 1
+
+print("uniform")
+for i in range(50):
+    assert 0 <= random.uniform(0, 4) <= 4
+    assert 2 <= random.uniform(2, 6) <= 6
+    assert -2 <= random.uniform(-2, 2) <= 2
diff --git a/tests/extmod/random_seed_default.py b/tests/extmod/random_seed_default.py
new file mode 100644
index 00000000000..2fb16282ca3
--- /dev/null
+++ b/tests/extmod/random_seed_default.py
@@ -0,0 +1,27 @@
+# test random.seed() without any arguments
+
+try:
+    import random
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    random.seed()
+except ValueError:
+    # no default seed on this platform
+    print("SKIP")
+    raise SystemExit
+
+
+def rng_seq():
+    return [random.getrandbits(16) for _ in range(10)]
+
+
+# seed with default and check that doesn't produce the same RNG sequence
+random.seed()
+seq = rng_seq()
+random.seed()
+print(seq == rng_seq())
+random.seed(None)
+print(seq == rng_seq())
diff --git a/tests/extmod/re1.py b/tests/extmod/re1.py
new file mode 100644
index 00000000000..7e3839ae24f
--- /dev/null
+++ b/tests/extmod/re1.py
@@ -0,0 +1,135 @@
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+r = re.compile(".+")
+m = r.match("abc")
+print(m.group(0))
+try:
+    m.group(1)
+except IndexError:
+    print("IndexError")
+
+# conversion of re and match to string
+str(r)
+str(m)
+
+r = re.compile("(.+)1")
+m = r.match("xyz781")
+print(m.group(0))
+print(m.group(1))
+try:
+    m.group(2)
+except IndexError:
+    print("IndexError")
+
+r = re.compile("[a-cu-z]")
+m = r.match("a")
+print(m.group(0))
+m = r.match("z")
+print(m.group(0))
+m = r.match("d")
+print(m)
+m = r.match("A")
+print(m)
+print("===")
+
+r = re.compile("[^a-cu-z]")
+m = r.match("a")
+print(m)
+m = r.match("z")
+print(m)
+m = r.match("d")
+print(m.group(0))
+m = r.match("A")
+print(m.group(0))
+print("===")
+
+# '-' character within character class block
+print(re.match("[-a]+", "-a]d").group(0))
+print(re.match("[a-]+", "-a]d").group(0))
+print("===")
+
+r = re.compile("o+")
+m = r.search("foobar")
+print(m.group(0))
+try:
+    m.group(1)
+except IndexError:
+    print("IndexError")
+
+
+m = re.match(".*", "foo")
+print(m.group(0))
+
+m = re.search("w.r", "hello world")
+print(m.group(0))
+
+m = re.match("a+?", "ab")
+print(m.group(0))
+m = re.match("a*?", "ab")
+print(m.group(0))
+m = re.match("^ab$", "ab")
+print(m.group(0))
+m = re.match("a|b", "b")
+print(m.group(0))
+m = re.match("a|b|c", "c")
+print(m.group(0))
+
+# Case where anchors fail to match
+r = re.compile("^b|b$")
+m = r.search("abc")
+print(m)
+
+try:
+    re.compile("*")
+except:
+    print("Caught invalid regex")
+
+# bytes objects
+m = re.match(rb"a+?", b"ab")
+print(m.group(0))
+print("===")
+
+# escaping
+m = re.match(r"a\.c", "a.c")
+print(m.group(0) if m else "")
+m = re.match(r"a\.b", "abc")
+print(m is None)
+m = re.match(r"a\.b", "a\\bc")
+print(m is None)
+m = re.match(r"[a\-z]", "abc")
+print(m.group(0))
+m = re.match(r"[.\]]*", ".].]a")
+print(m.group(0))
+m = re.match(r"[.\]+]*", ".]+.]a")
+print(m.group(0))
+m = re.match(r"[a-f0-9x\-yz]*", "abxcd1-23")
+print(m.group(0))
+m = re.match(r"[a\\b]*", "a\\aa\\bb\\bbab")
+print(m.group(0))
+m = re.search(r"[a\-z]", "-")
+print(m.group(0))
+m = re.search(r"[a\-z]", "f")
+print(m is None)
+m = re.search(r"[a\]z]", "a")
+print(m.group(0))
+print(re.compile(r"[-a]").split("foo-bar"))
+print(re.compile(r"[a-]").split("foo-bar"))
+print(re.compile(r"[ax\-]").split("foo-bar"))
+print(re.compile(r"[a\-x]").split("foo-bar"))
+print(re.compile(r"[\-ax]").split("foo-bar"))
+print("===")
+
+# Module functions take str/bytes/re.
+for f in (re.match, re.search):
+    print(f(".", "foo").group(0))
+    print(f(b".", b"foo").group(0))
+    print(f(re.compile("."), "foo").group(0))
+    try:
+        f(123, "a")
+    except TypeError:
+        print("TypeError")
+print("===")
diff --git a/tests/extmod/re_debug.py b/tests/extmod/re_debug.py
new file mode 100644
index 00000000000..0f4c95551f9
--- /dev/null
+++ b/tests/extmod/re_debug.py
@@ -0,0 +1,10 @@
+# test printing debugging info when compiling
+try:
+    import re
+
+    re.DEBUG
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+re.compile("^a|b[0-9]\w$", re.DEBUG)
diff --git a/tests/extmod/ure_debug.py.exp b/tests/extmod/re_debug.py.exp
similarity index 100%
rename from tests/extmod/ure_debug.py.exp
rename to tests/extmod/re_debug.py.exp
diff --git a/tests/extmod/re_error.py b/tests/extmod/re_error.py
new file mode 100644
index 00000000000..bd678c9d251
--- /dev/null
+++ b/tests/extmod/re_error.py
@@ -0,0 +1,25 @@
+# test errors in regex
+
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def test_re(r):
+    try:
+        re.compile(r)
+        print("OK")
+    except:  # MPy and CPy use different errors, so just ignore the type
+        print("Error")
+
+
+test_re(r"?")
+test_re(r"*")
+test_re(r"+")
+test_re(r")")
+test_re(r"[")
+test_re(r"([")
+test_re(r"([)")
+test_re(r"[a\]")
diff --git a/tests/extmod/re_group.py b/tests/extmod/re_group.py
new file mode 100644
index 00000000000..06c9ccfb2de
--- /dev/null
+++ b/tests/extmod/re_group.py
@@ -0,0 +1,31 @@
+# test groups, and nested groups
+
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def print_groups(match):
+    print("----")
+    try:
+        i = 0
+        while True:
+            print(match.group(i))
+            i += 1
+    except IndexError:
+        pass
+
+
+m = re.match(r"(([0-9]*)([a-z]*)[0-9]*)", "1234hello567")
+print_groups(m)
+
+m = re.match(r"([0-9]*)(([a-z]*)([0-9]*))", "1234hello567")
+print_groups(m)
+
+# optional group that matches
+print_groups(re.match(r"(a)?b(c)", "abc"))
+
+# optional group that doesn't match
+print_groups(re.match(r"(a)?b(c)", "bc"))
diff --git a/tests/extmod/re_groups.py b/tests/extmod/re_groups.py
new file mode 100644
index 00000000000..840a4b1af13
--- /dev/null
+++ b/tests/extmod/re_groups.py
@@ -0,0 +1,30 @@
+# test match.groups()
+
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    m = re.match(".", "a")
+    m.groups
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+
+m = re.match(r"(([0-9]*)([a-z]*)[0-9]*)", "1234hello567")
+print(m.groups())
+
+m = re.match(r"([0-9]*)(([a-z]*)([0-9]*))", "1234hello567")
+print(m.groups())
+
+# optional group that matches
+print(re.match(r"(a)?b(c)", "abc").groups())
+
+# optional group that doesn't match
+print(re.match(r"(a)?b(c)", "bc").groups())
+
+# only a single match
+print(re.match(r"abc", "abc").groups())
diff --git a/tests/extmod/re_limit.py b/tests/extmod/re_limit.py
new file mode 100644
index 00000000000..e0531afbdc8
--- /dev/null
+++ b/tests/extmod/re_limit.py
@@ -0,0 +1,34 @@
+# Test overflow in re.compile output code.
+
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def test_re(r):
+    try:
+        re.compile(r)
+    except:
+        print("Error")
+
+
+# too many chars in []
+test_re("[" + "a" * 256 + "]")
+
+# too many groups
+test_re("(a)" * 256)
+
+# jump too big for ?
+test_re("(" + "a" * 62 + ")?")
+
+# jump too big for *
+test_re("(" + "a" * 60 + ".)*")
+test_re("(" + "a" * 60 + "..)*")
+
+# jump too big for +
+test_re("(" + "a" * 62 + ")+")
+
+# jump too big for |
+test_re("b" * 63 + "|a")
diff --git a/tests/extmod/ure_limit.py.exp b/tests/extmod/re_limit.py.exp
similarity index 100%
rename from tests/extmod/ure_limit.py.exp
rename to tests/extmod/re_limit.py.exp
diff --git a/tests/extmod/re_namedclass.py b/tests/extmod/re_namedclass.py
new file mode 100644
index 00000000000..e7125667192
--- /dev/null
+++ b/tests/extmod/re_namedclass.py
@@ -0,0 +1,36 @@
+# test named char classes
+
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def print_groups(match):
+    print("----")
+    try:
+        i = 0
+        while True:
+            print(match.group(i))
+            i += 1
+    except IndexError:
+        pass
+
+
+m = re.match(r"\w+", "1234hello567 abc")
+print_groups(m)
+
+m = re.match(r"(\w+)\s+(\w+)", "ABC \t1234hello567 abc")
+print_groups(m)
+
+m = re.match(r"(\S+)\s+(\D+)", "ABC \thello abc567 abc")
+print_groups(m)
+
+m = re.match(r"(([0-9]*)([a-z]*)\d*)", "1234hello567")
+print_groups(m)
+
+# named class within a class set
+print_groups(re.match(r"([^\s]+)\s*([^\s]+)", "1 23"))
+print_groups(re.match(r"([\s\d]+)([\W]+)", "1  2-+="))
+print_groups(re.match(r"([\W]+)([^\W]+)([^\S]+)([^\D]+)", " a_1 23"))
diff --git a/tests/extmod/re_span.py b/tests/extmod/re_span.py
new file mode 100644
index 00000000000..d00ef957a11
--- /dev/null
+++ b/tests/extmod/re_span.py
@@ -0,0 +1,38 @@
+# test match.span(), and nested spans
+
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    m = re.match(".", "a")
+    m.span
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+
+def print_spans(match):
+    print("----")
+    try:
+        i = 0
+        while True:
+            print(match.span(i), match.start(i), match.end(i))
+            i += 1
+    except IndexError:
+        pass
+
+
+m = re.match(r"(([0-9]*)([a-z]*)[0-9]*)", "1234hello567")
+print_spans(m)
+
+m = re.match(r"([0-9]*)(([a-z]*)([0-9]*))", "1234hello567")
+print_spans(m)
+
+# optional span that matches
+print_spans(re.match(r"(a)?b(c)", "abc"))
+
+# optional span that doesn't match
+print_spans(re.match(r"(a)?b(c)", "bc"))
diff --git a/tests/extmod/re_split.py b/tests/extmod/re_split.py
new file mode 100644
index 00000000000..7769e1a121d
--- /dev/null
+++ b/tests/extmod/re_split.py
@@ -0,0 +1,40 @@
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+r = re.compile(" ")
+s = r.split("a b c foobar")
+print(s)
+
+r = re.compile(" +")
+s = r.split("a b    c   foobar")
+print(s)
+
+r = re.compile(" +")
+s = r.split("a b    c   foobar", 1)
+print(s)
+
+r = re.compile(" +")
+s = r.split("a b    c   foobar", 2)
+print(s)
+
+r = re.compile("[a-f]+")
+s = r.split("0a3b9")
+print(s)
+
+# bytes objects
+r = re.compile(b"x")
+s = r.split(b"fooxbar")
+print(s)
+
+# using ^
+r = re.compile("^ab")
+s = r.split("abababcabab")
+print(s)
+
+# using ^ with |
+r = re.compile("^ab|cab")
+s = r.split("abababcabab")
+print(s)
diff --git a/tests/extmod/re_split_empty.py b/tests/extmod/re_split_empty.py
new file mode 100644
index 00000000000..bc17aa8d783
--- /dev/null
+++ b/tests/extmod/re_split_empty.py
@@ -0,0 +1,23 @@
+# test splitting with pattern matches that can be empty
+#
+# CPython 3.5 issues a FutureWarning for these tests because their
+# behaviour will change in a future version.  MicroPython just stops
+# splitting as soon as an empty match is found.
+
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+r = re.compile(" *")
+s = r.split("a b    c   foobar")
+print(s)
+
+r = re.compile("x*")
+s = r.split("foo")
+print(s)
+
+r = re.compile("x*")
+s = r.split("axbc")
+print(s)
diff --git a/tests/extmod/ure_split_empty.py.exp b/tests/extmod/re_split_empty.py.exp
similarity index 100%
rename from tests/extmod/ure_split_empty.py.exp
rename to tests/extmod/re_split_empty.py.exp
diff --git a/tests/extmod/re_split_notimpl.py b/tests/extmod/re_split_notimpl.py
new file mode 100644
index 00000000000..7ddec9fa03a
--- /dev/null
+++ b/tests/extmod/re_split_notimpl.py
@@ -0,0 +1,11 @@
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+r = re.compile("( )")
+try:
+    s = r.split("a b c foobar")
+except NotImplementedError:
+    print("NotImplementedError")
diff --git a/tests/extmod/ure_split_notimpl.py.exp b/tests/extmod/re_split_notimpl.py.exp
similarity index 100%
rename from tests/extmod/ure_split_notimpl.py.exp
rename to tests/extmod/re_split_notimpl.py.exp
diff --git a/tests/extmod/re_stack_overflow.py b/tests/extmod/re_stack_overflow.py
new file mode 100644
index 00000000000..90fb8296b12
--- /dev/null
+++ b/tests/extmod/re_stack_overflow.py
@@ -0,0 +1,10 @@
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    re.match("(a*)*", "aaa")
+except RuntimeError:
+    print("RuntimeError")
diff --git a/tests/extmod/ure_stack_overflow.py.exp b/tests/extmod/re_stack_overflow.py.exp
similarity index 100%
rename from tests/extmod/ure_stack_overflow.py.exp
rename to tests/extmod/re_stack_overflow.py.exp
diff --git a/tests/extmod/re_stack_overflow2.py b/tests/extmod/re_stack_overflow2.py
new file mode 100644
index 00000000000..c5ddd012b32
--- /dev/null
+++ b/tests/extmod/re_stack_overflow2.py
@@ -0,0 +1,25 @@
+# Test overflow in re.compile output code.
+
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def test_re(r):
+    try:
+        re.compile(r)
+    except:
+        print("Error")
+
+
+try:
+    r = "(" * 65536 + ")" * 65536
+except MemoryError:
+    print("SKIP")
+    raise SystemExit
+
+# This happens to trigger RecursionError on current versions of CPython
+# (tested with 3.13.5) as well, so no .exp file is needed.
+test_re(r)
diff --git a/tests/extmod/re_start_end_pos.py b/tests/extmod/re_start_end_pos.py
new file mode 100644
index 00000000000..bd16584374b
--- /dev/null
+++ b/tests/extmod/re_start_end_pos.py
@@ -0,0 +1,78 @@
+# test start and end pos specification
+
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def print_groups(match):
+    print("----")
+    try:
+        if match is not None:
+            i = 0
+            while True:
+                print(match.group(i))
+                i += 1
+    except IndexError:
+        pass
+
+
+p = re.compile(r"o")
+m = p.match("dog")
+print_groups(m)
+
+m = p.match("dog", 1)
+print_groups(m)
+
+m = p.match("dog", 2)
+print_groups(m)
+
+# No match past end of input
+m = p.match("dog", 5)
+print_groups(m)
+
+m = p.match("dog", 0, 1)
+print_groups(m)
+
+# Caret only matches the actual beginning
+p = re.compile(r"^o")
+m = p.match("dog", 1)
+print_groups(m)
+
+# End at beginning means searching empty string
+p = re.compile(r"o")
+m = p.match("dog", 1, 1)
+print_groups(m)
+
+# End before the beginning doesn't match anything
+m = p.match("dog", 2, 1)
+print_groups(m)
+
+# Negative starting values don't crash
+m = p.search("dog", -2)
+print_groups(m)
+
+m = p.search("dog", -2, -5)
+print_groups(m)
+
+# Search also works
+print("--search")
+
+p = re.compile(r"o")
+m = p.search("dog")
+print_groups(m)
+
+m = p.search("dog", 1)
+print_groups(m)
+
+m = p.search("dog", 2)
+print_groups(m)
+
+# Negative starting values don't crash
+m = p.search("dog", -2)
+print_groups(m)
+
+m = p.search("dog", -2, -5)
+print_groups(m)
diff --git a/tests/extmod/re_sub.py b/tests/extmod/re_sub.py
new file mode 100644
index 00000000000..95ba1d70d6e
--- /dev/null
+++ b/tests/extmod/re_sub.py
@@ -0,0 +1,84 @@
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    re.sub
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+import sys
+
+
+def multiply(m):
+    return str(int(m.group(0)) * 2)
+
+
+print(re.sub(r"\d+", multiply, "10 20 30 40 50"))
+
+print(re.sub(r"\d+", lambda m: str(int(m.group(0)) // 2), "10 20 30 40 50"))
+
+
+def A():
+    return "A"
+
+
+print(re.sub("a", A(), "aBCBABCDabcda."))
+
+print(
+    re.sub(
+        r"def\s+([a-zA-Z_][a-zA-Z_0-9]*)\s*\(\s*\):",
+        "static PyObject*\npy_\\1(void){\n  return;\n}\n",
+        "\n\ndef myfunc():\n\ndef myfunc1():\n\ndef myfunc2():",
+    )
+)
+
+print(
+    re.compile("(calzino) (blu|bianco|verde) e (scarpa) (blu|bianco|verde)").sub(
+        r"\g<1> colore \2 con \g<3> colore \4? ...", "calzino blu e scarpa verde"
+    )
+)
+
+# \g immediately followed by another \g
+print(re.sub("(abc)", r"\g<1>\g<1>", "abc"))
+
+# no matches at all
+print(re.sub("a", "b", "c"))
+
+# with maximum substitution count specified
+if sys.implementation.name != "micropython":
+    # On CPython 3.13 and later the substitution count must be a keyword argument.
+    print(re.sub("a", "b", "1a2a3a", count=2))
+else:
+    print(re.sub("a", "b", "1a2a3a", 2))
+
+# invalid group
+try:
+    re.sub("(a)", "b\\2", "a")
+except:
+    print("invalid group")
+
+# invalid group with very large number (to test overflow in MicroPython)
+try:
+    re.sub("(a)", "b\\199999999999999999999999999999999999999", "a")
+except:
+    print("invalid group")
+
+# Module function takes str/bytes/re.
+print(re.sub("a", "a", "a"))
+print(re.sub(b".", b"a", b"a"))
+print(re.sub(re.compile("a"), "a", "a"))
+try:
+    re.sub(123, "a", "a")
+except TypeError:
+    print("TypeError")
+
+# Include \ in the sub replacement
+print(re.sub("b", "\\\\b", "abc"))
+
+# Using ^, make sure it doesn't repeatedly match
+print(re.sub("^ab", "*", "abababcabab"))
+print(re.sub("^ab|cab", "*", "abababcabab"))
diff --git a/tests/extmod/re_sub_unmatched.py b/tests/extmod/re_sub_unmatched.py
new file mode 100644
index 00000000000..c238a7ed64b
--- /dev/null
+++ b/tests/extmod/re_sub_unmatched.py
@@ -0,0 +1,16 @@
+# test re.sub with unmatched groups, behaviour changed in CPython 3.5
+
+try:
+    import re
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    re.sub
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+# first group matches, second optional group doesn't so is replaced with a blank
+print(re.sub(r"(a)(b)?", r"\2-\1", "1a2"))
diff --git a/tests/extmod/select_ipoll.py b/tests/extmod/select_ipoll.py
new file mode 100644
index 00000000000..170bb22bcda
--- /dev/null
+++ b/tests/extmod/select_ipoll.py
@@ -0,0 +1,66 @@
+# Test select.ipoll().
+
+try:
+    import socket, select
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Use a new UDP socket for tests, which should be writable but not readable.
+# Some targets (eg PYBV10) have the socket module but are unable to create
+# UDP sockets without a registered NIC.
+try:
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    s.bind(socket.getaddrinfo("127.0.0.1", 8000)[0][-1])
+except OSError:
+    print("SKIP")
+    raise SystemExit
+
+import unittest
+
+
+class Test(unittest.TestCase):
+    def test_ipoll_single_socket(self):
+        poller = select.poll()
+
+        # Register socket.
+        poller.register(s)
+
+        # Basic polling.
+        self.assertEqual(list(poller.ipoll(0)), [(s, 4)])
+
+        # Pass in flags=1 for one-shot behaviour.
+        self.assertEqual(list(poller.ipoll(0, 1)), [(s, 4)])
+
+        # Socket should be deregistered and poll should return nothing.
+        self.assertEqual(list(poller.ipoll(0)), [])
+
+    def test_ipoll_multiple_sockets(self):
+        # Create a second socket.
+        s2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        s2.bind(socket.getaddrinfo("127.0.0.1", 8001)[0][-1])
+
+        poller = select.poll()
+
+        # Register both sockets.
+        poller.register(s)
+        poller.register(s2)
+
+        # Basic polling with two sockets.
+        self.assertEqual(list(poller.ipoll(0)), [(s2, 4), (s2, 4)])
+
+        # Unregister the first socket, to test polling the remaining one.
+        poller.unregister(s)
+        self.assertEqual(list(poller.ipoll(0)), [(s2, 4)])
+
+        # Unregister the second socket, to test polling none.
+        poller.unregister(s2)
+        self.assertEqual(list(poller.ipoll(0)), [])
+
+        s2.close()
+
+
+if __name__ == "__main__":
+    unittest.main()
+
+s.close()
diff --git a/tests/extmod/select_poll_basic.py b/tests/extmod/select_poll_basic.py
new file mode 100644
index 00000000000..0814d89ce93
--- /dev/null
+++ b/tests/extmod/select_poll_basic.py
@@ -0,0 +1,40 @@
+try:
+    import socket, select, errno
+
+    select.poll  # Raises AttributeError for CPython implementations without poll()
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+
+poller = select.poll()
+
+s = socket.socket()
+
+poller.register(s)
+# https://docs.python.org/3/library/select.html#select.poll.register
+# "Registering a file descriptor that’s already registered is not an error,
+# and has the same effect as registering the descriptor exactly once."
+poller.register(s)
+poller.register(s, select.POLLIN | select.POLLOUT)
+
+# 2 args are mandatory unlike register()
+try:
+    poller.modify(s)
+except TypeError:
+    print("modify:TypeError")
+
+poller.modify(s, select.POLLIN)
+
+poller.unregister(s)
+
+try:
+    poller.modify(s, select.POLLIN)
+except OSError as e:
+    assert e.errno == errno.ENOENT
+
+# poll after closing the socket, should return POLLNVAL
+poller.register(s)
+s.close()
+p = poller.poll(0)
+print(len(p), p[0][-1])
diff --git a/tests/extmod/select_poll_custom.py b/tests/extmod/select_poll_custom.py
new file mode 100644
index 00000000000..0d2ab07b232
--- /dev/null
+++ b/tests/extmod/select_poll_custom.py
@@ -0,0 +1,102 @@
+# Test custom pollable objects implemented in Python.
+
+try:
+    import socket, select, io
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+from micropython import const
+
+_MP_STREAM_POLL = const(3)
+_MP_STREAM_GET_FILENO = const(10)
+
+_MP_STREAM_POLL_RD = const(0x0001)
+_MP_STREAM_POLL_WR = const(0x0004)
+
+
+def print_poll_output(lst):
+    print([(type(obj), flags) for obj, flags in lst])
+
+
+class CustomPollable(io.IOBase):
+    def __init__(self):
+        self.poll_state = 0
+
+    def ioctl(self, cmd, arg):
+        if cmd == _MP_STREAM_GET_FILENO:
+            # Bare-metal ports don't call this ioctl, so don't print it.
+            return -1
+
+        print("CustomPollable.ioctl", cmd, arg)
+        if cmd == _MP_STREAM_POLL:
+            if self.poll_state == "delay_rd":
+                self.poll_state = _MP_STREAM_POLL_RD
+                return 0
+            elif self.poll_state < 0:
+                return self.poll_state
+            else:
+                return self.poll_state & arg
+
+
+poller = select.poll()
+
+# Use a new UDP socket for tests, which should be writable but not readable.
+try:
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    s.bind(socket.getaddrinfo("127.0.0.1", 8000)[0][-1])
+except OSError:
+    print("SKIP")
+    raise SystemExit
+
+x = CustomPollable()
+
+# Register both a file-descriptor-based object and a custom pure-Python object.
+poller.register(s)
+poller.register(x)
+
+# Modify the flags for the custom object.
+poller.modify(x, select.POLLIN)
+
+# Test polling.
+print_poll_output(poller.poll(0))
+x.poll_state = _MP_STREAM_POLL_WR
+print_poll_output(poller.poll(0))
+x.poll_state = _MP_STREAM_POLL_RD
+print_poll_output(poller.poll(0))
+
+# The custom object becomes readable only after being polled.
+poller.modify(s, select.POLLIN)
+x.poll_state = "delay_rd"
+print_poll_output(poller.poll())
+
+# The custom object returns an error.
+x.poll_state = -1000
+try:
+    poller.poll(0)
+except OSError as er:
+    print("OSError", er.errno)
+
+# Register then unregister a socket (a native stream), then test
+# that the Python object is still pollable.
+s2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+x.poll_state = _MP_STREAM_POLL_RD
+poller.register(s2)
+poller.unregister(s2)
+print_poll_output(poller.poll())
+
+# Test registering and unregistering multiple times.
+for _ in range(2):
+    poller.unregister(s)
+    poller.unregister(x)
+    poller.register(s2)
+    poller.register(s, select.POLLIN)
+    poller.register(x, select.POLLIN)
+    poller.unregister(s2)
+    print_poll_output(poller.poll())
+
+# Clean up.
+poller.unregister(x)
+poller.unregister(s)
+s2.close()
+s.close()
diff --git a/tests/extmod/select_poll_custom.py.exp b/tests/extmod/select_poll_custom.py.exp
new file mode 100644
index 00000000000..d85508bab24
--- /dev/null
+++ b/tests/extmod/select_poll_custom.py.exp
@@ -0,0 +1,17 @@
+CustomPollable.ioctl 3 1
+[(, 4)]
+CustomPollable.ioctl 3 1
+[(, 4)]
+CustomPollable.ioctl 3 1
+[(, 4), (, 1)]
+CustomPollable.ioctl 3 1
+CustomPollable.ioctl 3 1
+[(, 1)]
+CustomPollable.ioctl 3 1
+OSError 1000
+CustomPollable.ioctl 3 1
+[(, 1)]
+CustomPollable.ioctl 3 1
+[(, 1)]
+CustomPollable.ioctl 3 1
+[(, 1)]
diff --git a/tests/extmod/select_poll_eintr.py b/tests/extmod/select_poll_eintr.py
new file mode 100644
index 00000000000..5dfa5d4395f
--- /dev/null
+++ b/tests/extmod/select_poll_eintr.py
@@ -0,0 +1,69 @@
+# Test interruption of select.poll by EINTR signal, when
+# MICROPY_PY_SELECT_POSIX_OPTIMISATIONS is enabled.
+#
+# Note: this test should run and pass under CPython, but it has a .py.exp file
+# so that port 8000 does not need to be available to get the expected output.
+
+try:
+    import time, gc, select, socket, _thread
+
+    time.time_ns  # Check for time_ns on MicroPython
+    select.poll  # Raises AttributeError for CPython implementations without poll()
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+# Use a new UDP socket for tests, which should be writable but not readable.
+s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+localhost_addr_info = socket.getaddrinfo("127.0.0.1", 8000)
+try:
+    s.bind(localhost_addr_info[0][-1])
+except OSError:
+    # Target can't bind to localhost.
+    # Most likely it doesn't have a NIC and the test cannot be run.
+    s.close()
+    print("SKIP")
+    raise SystemExit
+
+
+def thread_main():
+    lock.acquire()
+    time.sleep(0.2)
+    print("thread gc start")
+    # The unix gc.collect() implementation will raise EINTR on other threads.
+    # Could possibly use _thread._interrupt_main() instead if MicroPython had it.
+    gc.collect()
+    print("thread gc end")
+
+
+# Pre-allocate global variables here so the global dict is not resized by the main
+# thread while the secondary thread runs.  This is a workaround for the bug described
+# in https://github.com/micropython/micropython/pull/11604
+poller = None
+t0 = None
+result = None
+dt_ms = None
+
+# Start a thread to interrupt the main thread during its call to poll.
+lock = _thread.allocate_lock()
+lock.acquire()
+_thread.start_new_thread(thread_main, ())
+
+# Create the poller object.
+poller = select.poll()
+poller.register(s, select.POLLIN)
+
+# Poll on the UDP socket for a set timeout, which should be reached.
+print("poll")
+lock.release()
+t0 = time.time_ns()
+result = poller.poll(400)
+dt_ms = (time.time_ns() - t0) / 1e6
+print("result:", result)
+if 380 <= dt_ms <= 600:
+    print("dt in range")
+else:
+    print("dt not in range:", dt_ms)
+
+# Clean up.
+s.close()
diff --git a/tests/extmod/select_poll_eintr.py.exp b/tests/extmod/select_poll_eintr.py.exp
new file mode 100644
index 00000000000..2b15f86649c
--- /dev/null
+++ b/tests/extmod/select_poll_eintr.py.exp
@@ -0,0 +1,5 @@
+poll
+thread gc start
+thread gc end
+result: []
+dt in range
diff --git a/tests/extmod/select_poll_fd.py b/tests/extmod/select_poll_fd.py
new file mode 100644
index 00000000000..8baf7a2ad10
--- /dev/null
+++ b/tests/extmod/select_poll_fd.py
@@ -0,0 +1,64 @@
+# Test select.poll in combination with file descriptors.
+
+try:
+    import sys, select, errno
+
+    select.poll  # Raises AttributeError for CPython implementations without poll()
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+# FreeBSD allows to have up to ~120k file descriptor by default, so this test
+# will fail on such a system.  Since there's no provision to query sysctl
+# values or an easy way to parse the output of `ulimit -n` the test is just
+# skipped on FreeBSD.
+if sys.platform == "freebsd":
+    print("SKIP")
+    raise SystemExit
+
+# Check that poll supports registering file descriptors (integers).
+try:
+    select.poll().register(0)
+except OSError:
+    print("SKIP")
+    raise SystemExit
+
+# Register invalid file descriptor.
+try:
+    select.poll().register(-1)
+except ValueError:
+    print("ValueError")
+
+# Test polling stdout, it should be writable.
+poller = select.poll()
+poller.register(1)
+poller.modify(1, select.POLLOUT)
+print(poller.poll())
+
+# Unregister then re-register.
+poller.unregister(1)
+poller.register(1, select.POLLIN)
+
+# Poll for input, should return an empty list.
+print(poller.poll(0))
+
+# Test registering a very large number of file descriptors (will trigger
+# EINVAL due to more than OPEN_MAX fds). Typically it's 1024 (and on GitHub CI
+# we force this via `ulimit -n 1024`).
+poller = select.poll()
+for fd in range(6000):
+    poller.register(fd)
+try:
+    poller.poll()
+    assert False
+except OSError as er:
+    print(er.errno == errno.EINVAL)
+
+# Register stdout/stderr, plus many extra ones to trigger the fd vector
+# resizing. Then unregister the excess ones and verify poll still works.
+poller = select.poll()
+for fd in range(1, 1000):
+    poller.register(fd)
+for i in range(3, 1000):
+    poller.unregister(i)
+print(sorted(poller.poll()))
diff --git a/tests/extmod/select_poll_udp.py b/tests/extmod/select_poll_udp.py
new file mode 100644
index 00000000000..44ea965efab
--- /dev/null
+++ b/tests/extmod/select_poll_udp.py
@@ -0,0 +1,47 @@
+# test select.poll on UDP sockets
+
+try:
+    import socket, select
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Some targets (eg PYBV10) have the socket module but are unable to create
+# UDP sockets without a registered NIC.
+try:
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    s.bind(socket.getaddrinfo("127.0.0.1", 8000)[0][-1])
+except OSError:
+    print("SKIP")
+    raise SystemExit
+
+import unittest
+
+
+class Test(unittest.TestCase):
+    def test_poll(self):
+        poll = select.poll()
+
+        # UDP socket should not be readable.
+        poll.register(s, select.POLLIN)
+        res = poll.poll(0)
+        self.assertEqual(len(res), 0)
+
+        # UDP socket should be writable.
+        poll.modify(s, select.POLLOUT)
+        res = poll.poll(0)
+        self.assertEqual(res[0][1], select.POLLOUT)
+
+    @unittest.skipUnless(hasattr(select, "select"), "no select")
+    def test_select(self):
+        # UDP socket should only be writable.
+        r, w, e = select.select([s], [s], [s], 0)
+        self.assertEqual(r, [])
+        self.assertEqual(w, [s])
+        self.assertEqual(e, [])
+
+
+if __name__ == "__main__":
+    unittest.main()
+
+s.close()
diff --git a/tests/extmod/socket_badconstructor.py b/tests/extmod/socket_badconstructor.py
new file mode 100644
index 00000000000..25d981f70a7
--- /dev/null
+++ b/tests/extmod/socket_badconstructor.py
@@ -0,0 +1,29 @@
+# Test passing in bad values to socket.socket constructor.
+
+try:
+    import socket
+except:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    s = socket.socket(None)
+except TypeError:
+    print("TypeError")
+
+try:
+    s = socket.socket(socket.AF_INET, None)
+except TypeError:
+    print("TypeError")
+
+# This may or may not raise an exception, depending on the socket implementation.
+# The test is here for coverage.
+try:
+    s = socket.socket(socket.AF_INET, 123456)
+except OSError:
+    pass
+
+try:
+    s = socket.socket(socket.AF_INET, socket.SOCK_RAW, None)
+except TypeError:
+    print("TypeError")
diff --git a/tests/extmod/socket_fileno.py b/tests/extmod/socket_fileno.py
new file mode 100644
index 00000000000..da15825e3d5
--- /dev/null
+++ b/tests/extmod/socket_fileno.py
@@ -0,0 +1,17 @@
+# Test socket.fileno() functionality
+
+try:
+    import socket
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(socket.socket, "fileno"):
+    print("SKIP")
+    raise SystemExit
+
+s = socket.socket()
+print(s.fileno() >= 0)
+
+s.close()
+print(s.fileno())  #  should print -1
diff --git a/tests/extmod/socket_tcp_basic.py b/tests/extmod/socket_tcp_basic.py
new file mode 100644
index 00000000000..ebd30f7862c
--- /dev/null
+++ b/tests/extmod/socket_tcp_basic.py
@@ -0,0 +1,14 @@
+# Test basic, stand-alone TCP socket functionality
+
+try:
+    import socket, errno
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# recv() on a fresh socket should raise ENOTCONN
+s = socket.socket()
+try:
+    s.recv(1)
+except OSError as er:
+    print("ENOTCONN:", er.errno == errno.ENOTCONN)
diff --git a/tests/extmod/socket_udp_nonblock.py b/tests/extmod/socket_udp_nonblock.py
new file mode 100644
index 00000000000..7a4aac4647e
--- /dev/null
+++ b/tests/extmod/socket_udp_nonblock.py
@@ -0,0 +1,33 @@
+# test non-blocking UDP sockets
+
+try:
+    import socket, errno
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Some targets (eg PYBV10) have the socket module but are unable to create
+# UDP sockets without a registered NIC.
+try:
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    s.bind(socket.getaddrinfo("127.0.0.1", 8000)[0][-1])
+except OSError:
+    print("SKIP")
+    raise SystemExit
+
+import unittest
+
+
+class Test(unittest.TestCase):
+    def test_nonblocking(self):
+        s.settimeout(0)
+
+        with self.assertRaises(OSError) as ctx:
+            s.recv(1)
+        self.assertEqual(ctx.exception.errno, errno.EAGAIN)
+
+
+if __name__ == "__main__":
+    unittest.main()
+
+s.close()
diff --git a/tests/extmod/ssl_basic.py b/tests/extmod/ssl_basic.py
new file mode 100644
index 00000000000..95e66e0cae6
--- /dev/null
+++ b/tests/extmod/ssl_basic.py
@@ -0,0 +1,69 @@
+# very basic test of ssl module, just to test the methods exist
+
+try:
+    import io
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+class TestSocket(io.IOBase):
+    def write(self, buf):
+        return len(buf)
+
+    def readinto(self, buf):
+        return 0
+
+    def ioctl(self, cmd, arg):
+        print("TestSocket.ioctl", cmd, arg)
+        return 0
+
+    def setblocking(self, value):
+        print("TestSocket.setblocking({})".format(value))
+
+
+# create in client mode
+try:
+    ss = ssl.wrap_socket(TestSocket(), server_hostname="test.example.com")
+except OSError as er:
+    print("OSError: client")
+
+# create in server mode (can use this object for further tests)
+ss = ssl.wrap_socket(TestSocket(), server_side=1, do_handshake=0)
+
+# print
+print(ss)
+
+# setblocking() propagates call to the underlying stream object
+ss.setblocking(False)
+ss.setblocking(True)
+
+# write
+try:
+    ss.write(b"aaaa")
+except OSError:
+    pass
+
+# read (underlying socket has no data)
+try:
+    ss.read(8)
+except OSError:
+    pass
+
+# close
+ss.close()
+# close 2nd time
+ss.close()
+
+# read on closed socket
+try:
+    ss.read(10)
+except OSError as er:
+    print("OSError: read")
+
+# write on closed socket
+try:
+    ss.write(b"aaaa")
+except OSError as er:
+    print("OSError: write")
diff --git a/tests/extmod/ssl_basic.py.exp b/tests/extmod/ssl_basic.py.exp
new file mode 100644
index 00000000000..1a10a186144
--- /dev/null
+++ b/tests/extmod/ssl_basic.py.exp
@@ -0,0 +1,7 @@
+OSError: client
+
+TestSocket.setblocking(False)
+TestSocket.setblocking(True)
+TestSocket.ioctl 4 0
+OSError: read
+OSError: write
diff --git a/tests/extmod/ssl_cadata.py b/tests/extmod/ssl_cadata.py
new file mode 100644
index 00000000000..2a0f18c57d1
--- /dev/null
+++ b/tests/extmod/ssl_cadata.py
@@ -0,0 +1,18 @@
+# Test ssl.wrap_socket() with cadata passed in.
+
+try:
+    import io
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Invalid cadata.
+try:
+    ssl.wrap_socket(io.BytesIO(), cadata=b"!")
+except AttributeError:
+    # "cadata" keyword argument is not supported by axtls.
+    print("SKIP")
+    raise SystemExit
+except ValueError as er:
+    print(str(er) or "invalid cert")
diff --git a/tests/extmod/ssl_cadata.py.exp b/tests/extmod/ssl_cadata.py.exp
new file mode 100644
index 00000000000..4a398036501
--- /dev/null
+++ b/tests/extmod/ssl_cadata.py.exp
@@ -0,0 +1 @@
+invalid cert
diff --git a/tests/extmod/ssl_ioctl.py b/tests/extmod/ssl_ioctl.py
new file mode 100644
index 00000000000..4db7c2df82a
--- /dev/null
+++ b/tests/extmod/ssl_ioctl.py
@@ -0,0 +1,31 @@
+# Test SSL ioctl method.
+# Direct access to this method is only available if MICROPY_UNIX_COVERAGE is enabled.
+
+try:
+    import io, ssl
+
+    io.BytesIO
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+_MP_STREAM_POLL = 3
+_MP_STREAM_CLOSE = 4
+_MP_STREAM_GET_FILENO = 10
+
+s = ssl.wrap_socket(io.BytesIO(), server_side=1, do_handshake=0)
+
+if not hasattr(s, "ioctl"):
+    print("SKIP")
+    raise SystemExit
+
+# These ioctl's should be unsupported.
+for request in (-1, 0, _MP_STREAM_GET_FILENO):
+    try:
+        s.ioctl(request, 0)
+    except OSError:
+        print(request, "OSError")
+
+# These ioctl's should be supported.
+for request in (_MP_STREAM_CLOSE, _MP_STREAM_POLL, _MP_STREAM_CLOSE):
+    print(request, s.ioctl(request, 0))
diff --git a/tests/extmod/ssl_ioctl.py.exp b/tests/extmod/ssl_ioctl.py.exp
new file mode 100644
index 00000000000..22208b00cc9
--- /dev/null
+++ b/tests/extmod/ssl_ioctl.py.exp
@@ -0,0 +1,6 @@
+-1 OSError
+0 OSError
+10 OSError
+4 0
+3 32
+4 0
diff --git a/tests/extmod/ssl_keycert.py b/tests/extmod/ssl_keycert.py
new file mode 100644
index 00000000000..0bb923dedac
--- /dev/null
+++ b/tests/extmod/ssl_keycert.py
@@ -0,0 +1,29 @@
+# Test ssl with key/cert passed in
+
+try:
+    import io
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+key = b"0\x82\x019\x02\x01\x00\x02A\x00\xf9\xe0}\xbd\xd7\x9cI\x18\x06\xc3\xcb\xb5\xec@r\xfbD\x18\x80\xaaWoZ{\xcc\xa3\xeb!\"\x0fY\x9e]-\xee\xe4\t!BY\x9f{7\xf3\xf2\x8f}}\r|.\xa8<\ta\xb2\xd7W\xb3\xc9\x19A\xc39\x02\x03\x01\x00\x01\x02@\x07:\x9fh\xa6\x9c6\xe1#\x10\xf7\x0b\xc4Q\xf9\x01\x9b\xee\xb9\x8a4\r\\\xa8\xc8:\xd5\xca\x97\x99\xaa\x16\x04)\xa8\xf9\x13\xdeq\x0ev`\xa7\x83\xc5\x8b`\xdb\xef \x9d\x93\xe8g\x84\x96\xfaV\\\xf4R\xda\xd0\xa1\x02!\x00\xfeR\xbf\n\x91Su\x87L\x98{\xeb%\xed\xfb\x06u)@\xfe\x1b\xde\xa0\xc6@\xab\xc5\xedg\x8e\x10[\x02!\x00\xfb\x86=\x85\xa4'\xde\x85\xb5L\xe0)\x99\xfaL\x8c3A\x02\xa8<\xdew\xad\x00\xe3\x1d\x05\xd8\xb4N\xfb\x02 \x08\xb0M\x04\x90hx\x88q\xcew\xd5U\xcbf\x9b\x16\xdf\x9c\xef\xd1\x85\xee\x9a7Ug\x02\xb0Z\x03'\x02 9\xa0D\xe2$|\xf9\xefz]5\x92rs\xb5+\xfd\xe6,\x1c\xadmn\xcf\xd5?3|\x0em)\x17\x02 5Z\xcc/\xa5?\n\x04%\x9b{N\x9dX\xddI\xbe\xd2\xb0\xa0\x03BQ\x02\x82\xc2\xe0u)\xbd\xb8\xaf"
+
+# Invalid key
+try:
+    ssl.wrap_socket(io.BytesIO(), key=b"!")
+except ValueError as er:
+    print(str(er) or "invalid key")
+
+# Valid key, no cert
+try:
+    ssl.wrap_socket(io.BytesIO(), key=key)
+except TypeError as er:
+    # Error message varies depending on the error detail level
+    print("TypeError")
+
+# Valid key, invalid cert
+try:
+    ssl.wrap_socket(io.BytesIO(), key=key, cert=b"!")
+except ValueError as er:
+    print(str(er) or "invalid cert")
diff --git a/tests/extmod/ssl_keycert.py.exp b/tests/extmod/ssl_keycert.py.exp
new file mode 100644
index 00000000000..075abfcde0e
--- /dev/null
+++ b/tests/extmod/ssl_keycert.py.exp
@@ -0,0 +1,3 @@
+invalid key
+TypeError
+invalid cert
diff --git a/tests/extmod/ssl_keycert_pkcs8.py b/tests/extmod/ssl_keycert_pkcs8.py
new file mode 100644
index 00000000000..05a594b34d7
--- /dev/null
+++ b/tests/extmod/ssl_keycert_pkcs8.py
@@ -0,0 +1,24 @@
+# Test ssl with key/cert passed in
+
+try:
+    import io
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Only mbedTLS supports PKCS8 keys.
+if not hasattr(ssl, "MBEDTLS_VERSION"):
+    print("SKIP")
+    raise SystemExit
+
+# This particular key was generated with:
+# $ openssl genrsa -out key.pem 512
+# $ openssl rsa -in key.pem -outform DER -out key.der
+keypkcs8 = b'0\x82\x01U\x02\x01\x000\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x04\x82\x01?0\x82\x01;\x02\x01\x00\x02A\x00\xae\x07\xbb\xbf \x8eL\\ G\x91\x1b\xc45\x89\x96\x0c\xca\x94\xa5\xc2%F\x84z\xea\xb2Y\xdd\x943\x1a\xe0\x1e\xfe%\xd3\xbc\x85\xf0\x05\x8eI\xca\x83\x8b\x86\xfd\x15P,\xa9t8\xc2s\x11\x7fk*[[w\xdd\x02\x03\x01\x00\x01\x02@?\x02-\xc7\x95Q\xef\xf3\xaa\xfc>]\x7f<\xa3\x03\xe0\xbd\xb7\xf4\x0b\xc6\xd31D\x0f\x95cT\x06D9\xed\xf0X\xa6\x18\xb4\xe7\x96\x8f\x91\xd6\xd7\xbfR\\V\xfd\x0e{\xd3\xa4e\xb4M\x01\xbb\xdf\xf1\xf0\xbcEA\x02!\x00\xd5\xd7f\xcc\x84\xf5Y&\xa7]\x00\x91\xc9\x8e\xb0\xf37\x108D\x7f&!JJ\x836\x83\x9f\n\xf2\x0f\x02!\x00\xd0W\x0ec\xa6\x86C\xf7\x8d\xbe\xf0\xbc2\xe3)|\xbb\xd6\xbb;\xb2\xafG\x05S\x0f-\x14X\n3S\x02 A\xf5\xb20\xcd\xa6<<\x8f\rA\xda\xb68<^\x99\x12x\xb8\xb0\x1b\x9b\xd3%\x8e\xb9\xa6\xf9\xcc\xcf\x83\x02!\x00\xa8.\xee\xdd\xc0\xeb8h\xda\x13^\xb0\xfe\x7fd\xd9]\xa5m\xc66k\xcap`\xe9\xaaav\xe4\xdd#\x02!\x00\xd1\xd0\x93\xe7wZ\xe0p\xd0M\x07\x9e\x13\xa7\xa7\x12\x95\x1e\x13h0O\xc0\x18\x1fa\x10")\x9f&U'
+
+# Valid key in PKCS8 format, invalid cert
+try:
+    ssl.wrap_socket(io.BytesIO(), key=keypkcs8, cert=b"!")
+except ValueError as er:
+    print(str(er) or "invalid cert")
diff --git a/tests/extmod/ssl_keycert_pkcs8.py.exp b/tests/extmod/ssl_keycert_pkcs8.py.exp
new file mode 100644
index 00000000000..4a398036501
--- /dev/null
+++ b/tests/extmod/ssl_keycert_pkcs8.py.exp
@@ -0,0 +1 @@
+invalid cert
diff --git a/tests/extmod/ssl_poll.py b/tests/extmod/ssl_poll.py
new file mode 100644
index 00000000000..42d7fb3f849
--- /dev/null
+++ b/tests/extmod/ssl_poll.py
@@ -0,0 +1,210 @@
+try:
+    import select
+    import ssl
+    import io
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+from micropython import const
+
+_MP_STREAM_POLL_RD = const(0x0001)
+_MP_STREAM_POLL_WR = const(0x0004)
+_MP_STREAM_POLL_NVAL = const(0x0020)
+_MP_STREAM_POLL = const(3)
+_MP_STREAM_CLOSE = const(4)
+
+
+# This self-signed key/cert pair is randomly generated and to be used for
+# testing/demonstration only.  You should always generate your own key/cert.
+# They has been generated using the following commands:
+# $ openssl req -x509 -newkey rsa:1024 -keyout rsa_key.pem -out rsa_cert.pem -days 3650 -nodes -subj '/CN=micropython.local/O=MicroPython/C=AU'
+# $ openssl pkey -in rsa_key.pem -out rsa_key.der -outform DER
+# $ openssl x509 -in rsa_cert.pem -out rsa_cert.der -outform DER
+key = bytes.fromhex(
+    "3082025d02010002818100eca28b2f8230237ae45e7a77ef495c05a786f423cc65caf6bc"
+    "1813d50eacf9d2d011a0e43a20fde947ff957075e4b3b6ded46c33f63af42597aac1c4cb"
+    "bb2d1a6aad91755707d8fc560e222b38c940a480da89ee849049613bd88d1ab283423aba"
+    "ba591c4fcc8ce89d19646e6b9f80de4efa7bbda68c569a5cbdb4235c3fc9990203010001"
+    "0281810089d4df978b90388a534c88af252ca8b20e7377ef0616609338da196b27422fbd"
+    "d03e04660b49be3bcc191dd5448632fb986d489d3795fd318c5704c879168c5cd0fa8551"
+    "f7f86073b95b092ffdb4f39c867a306a02f94cf3009df7055dab1f9277dd8914268d53a2"
+    "bd4de2cdf2ac90d8cd248b48868cb911781779750c344ae5024100f8c0d2cebfaccbdb1b"
+    "d8bc7519c84889cc6d5dff8fb994cf1a9492881289de66d689afb942d10dc0dc0f65464e"
+    "7cca7a53e451e6a8cfab0069d05065d56ba4bf024100f38757e2fc7f786e1a653c8a8b51"
+    "5b06a1d85db31998090fc4d52a88b8c5557e0a7bac10995e7e76cef6bfb59b67c01cbba7"
+    "edcfa7d3d623615a92459d07efa702407b149579fcf717caeb455b4229a6a2d5a3d3bd7d"
+    "d4f4833fd22c0f30cad372bab98e58e736bb4fadcc74c5ac7aeb5e1816e852e9e93f0fa2"
+    "5db8d7fefb118eeb024100caf0a0a6c1b02055f09d28c473b10a600e8356222853f04939"
+    "c84237c97278fa1e164d9f4f8fd56780b553b12d9e5a1417b5ab91fed3a381bc6153bfbb"
+    "4a9fc5024043eb4ff7e4dd6c12c6d6dc50977ee5d5f9730af4469d1f642321320fb4b969"
+    "90ed841e41bedda49ff89a0c28acf132e4af5f3ac406b1548d0f135aff6bf23ef1"
+)
+cert = bytes.fromhex(
+    "3082025a308201c3a003020102021475fd479918562f4c6cb08f63e8afbdfa3b884f8630"
+    "0d06092a864886f70d01010b0500303f311a301806035504030c116d6963726f70797468"
+    "6f6e2e6c6f63616c31143012060355040a0c0b4d6963726f507974686f6e310b30090603"
+    "55040613024155301e170d3234303730323030353931355a170d33343036333030303539"
+    "31355a303f311a301806035504030c116d6963726f707974686f6e2e6c6f63616c311430"
+    "12060355040a0c0b4d6963726f507974686f6e310b300906035504061302415530819f30"
+    "0d06092a864886f70d010101050003818d0030818902818100eca28b2f8230237ae45e7a"
+    "77ef495c05a786f423cc65caf6bc1813d50eacf9d2d011a0e43a20fde947ff957075e4b3"
+    "b6ded46c33f63af42597aac1c4cbbb2d1a6aad91755707d8fc560e222b38c940a480da89"
+    "ee849049613bd88d1ab283423ababa591c4fcc8ce89d19646e6b9f80de4efa7bbda68c56"
+    "9a5cbdb4235c3fc9990203010001a3533051301d0603551d0e04160414409545477a659a"
+    "16da174810ba9ad192ef962089301f0603551d23041830168014409545477a659a16da17"
+    "4810ba9ad192ef962089300f0603551d130101ff040530030101ff300d06092a864886f7"
+    "0d01010b0500038181007182e78cecceef00f98d0ee117cd9dc2f9fc84d581e7b1d9d43b"
+    "74db45e188368e264f79628e2bda89a545d50cd9925ad50f8e25decc9130164bdb9220c8"
+    "f49776d784511e9c4b94305cc2cb3eaf8204e42d31ba8aabd1d296b9ef51035b6df1ab75"
+    "89681f0026073ccac6bed5d8bd9235a4bb717b696ce518de4e35b751fa13"
+)
+
+
+class _Pipe(io.IOBase):
+    def __init__(self):
+        self._other = None
+        self.block_reads = False
+        self.block_writes = False
+
+        self.write_buffers = []
+        self.last_poll_arg = None
+
+    def readinto(self, buf):
+        if self.block_reads or len(self._other.write_buffers) == 0:
+            return None
+
+        read_buf = self._other.write_buffers[0]
+        l = min(len(buf), len(read_buf))
+        buf[:l] = read_buf[:l]
+        if l == len(read_buf):
+            self._other.write_buffers.pop(0)
+        else:
+            self._other.write_buffers[0] = read_buf[l:]
+        return l
+
+    def write(self, buf):
+        if self.block_writes:
+            return None
+
+        self.write_buffers.append(memoryview(bytes(buf)))
+        return len(buf)
+
+    def ioctl(self, request, arg):
+        if request == _MP_STREAM_POLL:
+            self.last_poll_arg = arg
+            ret = 0
+            if arg & _MP_STREAM_POLL_RD:
+                if not self.block_reads and self._other.write_buffers:
+                    ret |= _MP_STREAM_POLL_RD
+            if arg & _MP_STREAM_POLL_WR:
+                if not self.block_writes:
+                    ret |= _MP_STREAM_POLL_WR
+            return ret
+
+        elif request == _MP_STREAM_CLOSE:
+            return 0
+
+        raise NotImplementedError()
+
+    @classmethod
+    def new_pair(cls):
+        p1 = cls()
+        p2 = cls()
+        p1._other = p2
+        p2._other = p1
+        return p1, p2
+
+
+def assert_poll(s, i, arg, expected_arg, expected_ret):
+    ret = s.ioctl(_MP_STREAM_POLL, arg)
+    assert i.last_poll_arg == expected_arg
+    i.last_poll_arg = None
+    assert ret == expected_ret
+
+
+def assert_raises(cb, *args, **kwargs):
+    try:
+        cb(*args, **kwargs)
+        raise AssertionError("should have raised")
+    except Exception as exc:
+        pass
+
+
+client_io, server_io = _Pipe.new_pair()
+
+client_io.block_reads = True
+client_io.block_writes = True
+client_sock = ssl.wrap_socket(client_io, do_handshake=False)
+
+server_sock = ssl.wrap_socket(server_io, key=key, cert=cert, server_side=True, do_handshake=False)
+
+# Do a test read, at this point the TLS handshake wants to write,
+# so it returns None:
+assert client_sock.read(128) is None
+
+# Polling for either read or write actually check if the underlying socket can write:
+assert_poll(client_sock, client_io, _MP_STREAM_POLL_RD, _MP_STREAM_POLL_WR, 0)
+assert_poll(client_sock, client_io, _MP_STREAM_POLL_WR, _MP_STREAM_POLL_WR, 0)
+
+# Mark the socket as writable, and do another test read:
+client_io.block_writes = False
+assert client_sock.read(128) is None
+
+# The client wrote the CLIENT_HELLO message
+assert len(client_io.write_buffers) == 1
+
+# At this point the TLS handshake wants to read, but we don't know that yet:
+assert_poll(client_sock, client_io, _MP_STREAM_POLL_RD, _MP_STREAM_POLL_RD, 0)
+assert_poll(client_sock, client_io, _MP_STREAM_POLL_WR, _MP_STREAM_POLL_WR, _MP_STREAM_POLL_WR)
+
+# Do a test write
+client_sock.write(b"foo")
+
+# Now we know that we want to read:
+assert_poll(client_sock, client_io, _MP_STREAM_POLL_RD, _MP_STREAM_POLL_RD, 0)
+assert_poll(client_sock, client_io, _MP_STREAM_POLL_WR, _MP_STREAM_POLL_RD, 0)
+
+# Unblock reads and nudge the two sockets:
+client_io.block_reads = False
+while server_io.write_buffers or client_io.write_buffers:
+    if server_io.write_buffers:
+        assert client_sock.read(128) is None
+    if client_io.write_buffers:
+        assert server_sock.read(128) is None
+
+# At this point, the handshake is done, try writing data:
+client_sock.write(b"foo")
+assert server_sock.read(3) == b"foo"
+
+# Test reading partial data:
+client_sock.write(b"foobar")
+assert server_sock.read(3) == b"foo"
+server_io.block_reads = True
+assert_poll(
+    server_sock, server_io, _MP_STREAM_POLL_RD, None, _MP_STREAM_POLL_RD
+)  # Did not go to the socket, just consumed buffered data
+assert server_sock.read(3) == b"bar"
+
+
+# Polling on a closed socket errors out:
+client_io, _ = _Pipe.new_pair()
+client_sock = ssl.wrap_socket(client_io, do_handshake=False)
+client_sock.close()
+assert_poll(
+    client_sock, client_io, _MP_STREAM_POLL_RD, None, _MP_STREAM_POLL_NVAL
+)  # Did not go to the socket
+
+
+# Errors propagates to poll:
+client_io, server_io = _Pipe.new_pair()
+client_sock = ssl.wrap_socket(client_io, do_handshake=False)
+
+# The server returns garbage:
+server_io.write(b"fooba")  # Needs to be exactly 5 bytes
+
+assert_poll(client_sock, client_io, _MP_STREAM_POLL_RD, _MP_STREAM_POLL_RD, _MP_STREAM_POLL_RD)
+assert_raises(client_sock.read, 128)
+assert_poll(
+    client_sock, client_io, _MP_STREAM_POLL_RD, None, _MP_STREAM_POLL_NVAL
+)  # Did not go to the socket
diff --git a/tests/feature_check/byteorder.py.exp b/tests/extmod/ssl_poll.py.exp
similarity index 100%
rename from tests/feature_check/byteorder.py.exp
rename to tests/extmod/ssl_poll.py.exp
diff --git a/tests/extmod/ssl_sslcontext.py b/tests/extmod/ssl_sslcontext.py
new file mode 100644
index 00000000000..23ff9c29646
--- /dev/null
+++ b/tests/extmod/ssl_sslcontext.py
@@ -0,0 +1,25 @@
+# Very basic test of ssl.SSLContext class.
+
+try:
+    import socket, ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Test constructing with arguments.
+ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+
+# Test printing object.
+ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+print("SSLContext" in str(ctx))
+
+# Coverage test for destructor, and calling it twice.
+if hasattr(ctx, "__del__"):
+    ctx.__del__()
+    ctx.__del__()
+
+# Test calling .wrap_socket() method, multiple times.
+ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+ctx.wrap_socket(socket.socket(), do_handshake_on_connect=False)
+ctx.wrap_socket(socket.socket(), do_handshake_on_connect=False)
diff --git a/tests/extmod/ssl_sslcontext_verify_mode.py b/tests/extmod/ssl_sslcontext_verify_mode.py
new file mode 100644
index 00000000000..698ff65a897
--- /dev/null
+++ b/tests/extmod/ssl_sslcontext_verify_mode.py
@@ -0,0 +1,15 @@
+# Test ssl.SSLContext.verify_mode attribute.
+
+try:
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Test default verify_mode for server (client default is different in MicroPython).
+ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+print(ctx.verify_mode == ssl.CERT_NONE)
+
+# Test setting and getting verify_mode.
+ctx.verify_mode = ssl.CERT_NONE
+print(ctx.verify_mode == ssl.CERT_NONE)
diff --git a/tests/extmod/ssl_sslcontext_verify_mode2.py b/tests/extmod/ssl_sslcontext_verify_mode2.py
new file mode 100644
index 00000000000..1b33e6e8670
--- /dev/null
+++ b/tests/extmod/ssl_sslcontext_verify_mode2.py
@@ -0,0 +1,20 @@
+# Test ssl.SSLContext.verify_mode attribute, with additional CERT_xxx constants.
+# These aren't available in the axtls implementation.
+
+try:
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(ssl, "OPENSSL_VERSION") and not hasattr(ssl, "MBEDTLS_VERSION"):
+    print("SKIP")
+    raise SystemExit
+
+ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+
+# Test setting and getting verify_mode.
+ctx.verify_mode = ssl.CERT_OPTIONAL
+print(ctx.verify_mode == ssl.CERT_OPTIONAL)
+ctx.verify_mode = ssl.CERT_REQUIRED
+print(ctx.verify_mode == ssl.CERT_REQUIRED)
diff --git a/tests/extmod/ticks_add.py b/tests/extmod/ticks_add.py
index 2f1ba6c8109..4f465f3cfba 100644
--- a/tests/extmod/ticks_add.py
+++ b/tests/extmod/ticks_add.py
@@ -1,5 +1,5 @@
 try:
-    from utime import ticks_diff, ticks_add
+    from time import ticks_diff, ticks_add
 except ImportError:
     print("SKIP")
     raise SystemExit
diff --git a/tests/extmod/ticks_diff.py b/tests/extmod/ticks_diff.py
index de45a557afc..b2445f0d624 100644
--- a/tests/extmod/ticks_diff.py
+++ b/tests/extmod/ticks_diff.py
@@ -1,5 +1,5 @@
 try:
-    from utime import ticks_diff, ticks_add
+    from time import ticks_diff, ticks_add
 except ImportError:
     print("SKIP")
     raise SystemExit
diff --git a/tests/extmod/time_mktime.py b/tests/extmod/time_mktime.py
new file mode 100644
index 00000000000..7fc643dc3cb
--- /dev/null
+++ b/tests/extmod/time_mktime.py
@@ -0,0 +1,120 @@
+# test conversion from date tuple to timestamp and back
+
+try:
+    import time
+
+    time.localtime
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+# Range of date expected to work on all MicroPython platforms
+MIN_YEAR = 1970
+MAX_YEAR = 2099
+# CPython properly supported date range:
+# - on Windows: year 1970 to 3000+
+# - on Unix:    year 1583 to 3000+
+
+# Start test from Jan 1, 2001 13:00 (Feb 2000 might already be broken)
+SAFE_DATE = (2001, 1, 1, 13, 0, 0, 0, 0, -1)
+
+
+# mktime function that checks that the result is reversible
+def safe_mktime(date_tuple):
+    try:
+        res = time.mktime(date_tuple)
+        chk = time.localtime(res)
+    except OverflowError:
+        print("safe_mktime:", date_tuple, "overflow error")
+        return None
+    if chk[0:5] != date_tuple[0:5]:
+        print("safe_mktime:", date_tuple[0:5], " -> ", res, " -> ", chk[0:5])
+        return None
+    return res
+
+
+# localtime function that checks that the result is reversible
+def safe_localtime(timestamp):
+    try:
+        res = time.localtime(timestamp)
+        chk = time.mktime(res)
+    except OverflowError:
+        print("safe_localtime:", timestamp, "overflow error")
+        return None
+    if chk != timestamp:
+        print("safe_localtime:", timestamp, " -> ", res, " -> ", chk)
+        return None
+    return res
+
+
+# look for smallest valid timestamps by iterating backwards on tuple
+def test_bwd(date_tuple):
+    curr_stamp = safe_mktime(date_tuple)
+    year = date_tuple[0]
+    month = date_tuple[1] - 1
+    if month < 1:
+        year -= 1
+        month = 12
+    while year >= MIN_YEAR:
+        while month >= 1:
+            next_tuple = (year, month) + date_tuple[2:]
+            next_stamp = safe_mktime(next_tuple)
+            # at this stage, only test consistency and monotonicity
+            if next_stamp is None or next_stamp >= curr_stamp:
+                return date_tuple
+            date_tuple = next_tuple
+            curr_stamp = next_stamp
+            month -= 1
+        year -= 1
+        month = 12
+    return date_tuple
+
+
+# test day-by-day to ensure that every date is properly converted
+def test_fwd(start_date):
+    DAYS_PER_MONTH = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
+    curr_stamp = safe_mktime(start_date)
+    curr_date = safe_localtime(curr_stamp)
+    while curr_date[0] <= MAX_YEAR:
+        if curr_date[2] < 15:
+            skip_days = 13
+        else:
+            skip_days = 1
+        next_stamp = curr_stamp + skip_days * 86400
+        next_date = safe_localtime(next_stamp)
+        if next_date is None:
+            return curr_date
+        if next_date[2] != curr_date[2] + skip_days:
+            # next month
+            if next_date[2] != 1:
+                print("wrong day of month:", next_date)
+                return curr_date
+            # check the number of days in previous month
+            month_days = DAYS_PER_MONTH[curr_date[1]]
+            if month_days == 28 and curr_date[0] % 4 == 0:
+                if curr_date[0] % 100 != 0 or curr_date[0] % 400 == 0:
+                    month_days += 1
+            if curr_date[2] != month_days:
+                print("wrong day count in prev month:", curr_date[2], "vs", month_days)
+                return curr_date
+            if next_date[1] != curr_date[1] + 1:
+                # next year
+                if curr_date[1] != 12:
+                    print("wrong month count in prev year:", curr_date[1])
+                    return curr_date
+                if next_date[1] != 1:
+                    print("wrong month:", next_date)
+                    return curr_date
+                if next_date[0] != curr_date[0] + 1:
+                    print("wrong year:", next_date)
+                    return curr_date
+        curr_stamp = next_stamp
+        curr_date = next_date
+    return curr_date
+
+
+small_date = test_bwd(SAFE_DATE)
+large_date = test_fwd(small_date)
+print("tested from", small_date[0:3], "to", large_date[0:3])
+print(small_date[0:3], "wday is", small_date[6])
+print(large_date[0:3], "wday is", large_date[6])
diff --git a/tests/extmod/time_ms_us.py b/tests/extmod/time_ms_us.py
index ac2ed8be27e..e26c6e677a9 100644
--- a/tests/extmod/time_ms_us.py
+++ b/tests/extmod/time_ms_us.py
@@ -1,23 +1,23 @@
 try:
-    import utime
+    import time
 
-    utime.sleep_ms, utime.sleep_us, utime.ticks_diff, utime.ticks_ms, utime.ticks_us, utime.ticks_cpu
+    time.sleep_ms, time.sleep_us, time.ticks_diff, time.ticks_ms, time.ticks_us, time.ticks_cpu
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
-utime.sleep_ms(1)
-utime.sleep_us(1)
+time.sleep_ms(1)
+time.sleep_us(1)
 
-t0 = utime.ticks_ms()
-t1 = utime.ticks_ms()
-print(0 <= utime.ticks_diff(t1, t0) <= 1)
+t0 = time.ticks_ms()
+t1 = time.ticks_ms()
+print(0 <= time.ticks_diff(t1, t0) <= 1)
 
-t0 = utime.ticks_us()
-t1 = utime.ticks_us()
-print(0 <= utime.ticks_diff(t1, t0) <= 500)
+t0 = time.ticks_us()
+t1 = time.ticks_us()
+print(0 <= time.ticks_diff(t1, t0) <= 500)
 
 # ticks_cpu may not be implemented, at least make sure it doesn't decrease
-t0 = utime.ticks_cpu()
-t1 = utime.ticks_cpu()
-print(utime.ticks_diff(t1, t0) >= 0)
+t0 = time.ticks_cpu()
+t1 = time.ticks_cpu()
+print(time.ticks_diff(t1, t0) >= 0)
diff --git a/tests/extmod/time_res.py b/tests/extmod/time_res.py
new file mode 100644
index 00000000000..ef20050b914
--- /dev/null
+++ b/tests/extmod/time_res.py
@@ -0,0 +1,68 @@
+# test time resolutions
+
+try:
+    import time
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+def gmtime_time():
+    return time.gmtime(time.time())
+
+
+def localtime_time():
+    return time.localtime(time.time())
+
+
+def test():
+    TEST_TIME = 2500
+    EXPECTED_MAP = (
+        # (function name, min. number of results in 2.5 sec)
+        ("time", 3),
+        ("gmtime", 3),
+        ("localtime", 3),
+        ("gmtime_time", 3),
+        ("localtime_time", 3),
+        ("ticks_ms", 15),
+        ("ticks_us", 15),
+        ("ticks_ns", 15),
+        ("ticks_cpu", 15),
+    )
+
+    # call time functions
+    results_map = {}
+    end_time = time.ticks_ms() + TEST_TIME
+    while time.ticks_diff(end_time, time.ticks_ms()) > 0:
+        time.sleep_ms(100)
+        for func_name, _ in EXPECTED_MAP:
+            try:
+                if func_name.endswith("_time"):
+                    time_func = globals()[func_name]
+                else:
+                    time_func = getattr(time, func_name)
+                now = time_func()  # may raise AttributeError
+            except AttributeError:
+                continue
+            try:
+                results_map[func_name].add(now)
+            except KeyError:
+                results_map[func_name] = {now}
+
+    # check results
+    for func_name, min_len in EXPECTED_MAP:
+        print("Testing %s" % func_name)
+        results = results_map.get(func_name)
+        if results is None:
+            pass
+        elif func_name == "ticks_cpu" and results == {0}:
+            # ticks_cpu() returns 0 on some ports (e.g. unix)
+            pass
+        elif len(results) < min_len:
+            print(
+                "%s() returns %s result%s in %s ms, expecting >= %s"
+                % (func_name, len(results), "s"[: len(results) != 1], TEST_TIME, min_len)
+            )
+
+
+test()
diff --git a/tests/extmod/utime_res.py.exp b/tests/extmod/time_res.py.exp
similarity index 100%
rename from tests/extmod/utime_res.py.exp
rename to tests/extmod/time_res.py.exp
diff --git a/tests/extmod/time_time_ns.py b/tests/extmod/time_time_ns.py
new file mode 100644
index 00000000000..eab7b3a5d92
--- /dev/null
+++ b/tests/extmod/time_time_ns.py
@@ -0,0 +1,31 @@
+# test time.time_ns()
+
+try:
+    import time
+
+    time.sleep_us
+    time.time_ns
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+
+t0 = time.time_ns()
+time.sleep_us(5000)
+t1 = time.time_ns()
+
+# Check that time_ns increases.
+print(t0 < t1)
+
+# Check that time_ns counts correctly, but be very lenient with the bounds (2ms to 50ms).
+if 2000000 < t1 - t0 < 50000000:
+    print(True)
+else:
+    print(t0, t1, t1 - t0)
+
+# Check that time.time() and time.time_ns() are within a second of each other.
+# Note that time.time() may return an int or float.
+for _ in range(10):
+    t_s, t_ns = time.time(), time.time_ns()
+    print(abs(t_s * 1_000 - t_ns // 1_000_000) <= 1_000)
+    time.sleep_us(100_000)
diff --git a/tests/extmod/time_time_ns.py.exp b/tests/extmod/time_time_ns.py.exp
new file mode 100644
index 00000000000..e5307f2e4ed
--- /dev/null
+++ b/tests/extmod/time_time_ns.py.exp
@@ -0,0 +1,12 @@
+True
+True
+True
+True
+True
+True
+True
+True
+True
+True
+True
+True
diff --git a/tests/extmod/tls_dtls.py b/tests/extmod/tls_dtls.py
new file mode 100644
index 00000000000..753ab2fee4f
--- /dev/null
+++ b/tests/extmod/tls_dtls.py
@@ -0,0 +1,61 @@
+# Test DTLS functionality including timeout handling
+
+try:
+    from tls import PROTOCOL_DTLS_CLIENT, PROTOCOL_DTLS_SERVER, SSLContext, CERT_NONE
+    import io
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+class DummySocket(io.IOBase):
+    def __init__(self):
+        self.write_buffer = bytearray()
+        self.read_buffer = bytearray()
+
+    def write(self, data):
+        return len(data)
+
+    def readinto(self, buf):
+        # This is a placeholder socket that doesn't actually read anything
+        # so the read buffer is always empty.
+        return None
+
+    def ioctl(self, req, arg):
+        if req == 4:  # MP_STREAM_CLOSE
+            return 0
+        return -1
+
+
+# Create dummy sockets for testing
+server_socket = DummySocket()
+client_socket = DummySocket()
+
+# Wrap the DTLS Server
+dtls_server_ctx = SSLContext(PROTOCOL_DTLS_SERVER)
+dtls_server_ctx.verify_mode = CERT_NONE
+dtls_server = dtls_server_ctx.wrap_socket(
+    server_socket, do_handshake_on_connect=False, client_id=b"dummy_client_id"
+)
+print("Wrapped DTLS Server")
+
+# wrap DTLS server with invalid client_id
+try:
+    dtls_server = dtls_server_ctx.wrap_socket(
+        server_socket, do_handshake_on_connect=False, client_id=4
+    )
+except OSError:
+    print("Failed to wrap DTLS Server with invalid client_id")
+
+# Wrap the DTLS Client
+dtls_client_ctx = SSLContext(PROTOCOL_DTLS_CLIENT)
+dtls_client_ctx.verify_mode = CERT_NONE
+dtls_client = dtls_client_ctx.wrap_socket(client_socket, do_handshake_on_connect=False)
+print("Wrapped DTLS Client")
+
+# Trigger the timing check multiple times with different elapsed times
+for i in range(10):  # Try multiple iterations to hit the timing window
+    dtls_client.write(b"test")
+    data = dtls_server.read(1024)  # This should eventually hit the timing condition
+
+print("OK")
diff --git a/tests/extmod/tls_dtls.py.exp b/tests/extmod/tls_dtls.py.exp
new file mode 100644
index 00000000000..dbd005d0edf
--- /dev/null
+++ b/tests/extmod/tls_dtls.py.exp
@@ -0,0 +1,4 @@
+Wrapped DTLS Server
+Failed to wrap DTLS Server with invalid client_id
+Wrapped DTLS Client
+OK
diff --git a/tests/extmod/tls_noleak.py b/tests/extmod/tls_noleak.py
new file mode 100644
index 00000000000..870032d58e6
--- /dev/null
+++ b/tests/extmod/tls_noleak.py
@@ -0,0 +1,50 @@
+# Ensure that SSLSockets can be allocated sequentially
+# without running out of available memory.
+try:
+    import io
+    import tls
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+import unittest
+
+
+class TestSocket(io.IOBase):
+    def write(self, buf):
+        return len(buf)
+
+    def readinto(self, buf):
+        return 0
+
+    def ioctl(self, cmd, arg):
+        return 0
+
+    def setblocking(self, value):
+        pass
+
+
+ITERS = 128
+
+
+class TLSNoLeaks(unittest.TestCase):
+    def test_unique_context(self):
+        for n in range(ITERS):
+            print(n)
+            s = TestSocket()
+            ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+            ctx.verify_mode = tls.CERT_NONE
+            s = ctx.wrap_socket(s, do_handshake_on_connect=False)
+
+    def test_shared_context(self):
+        # Single SSLContext, multiple sockets
+        ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+        ctx.verify_mode = tls.CERT_NONE
+        for n in range(ITERS):
+            print(n)
+            s = TestSocket()
+            s = ctx.wrap_socket(s, do_handshake_on_connect=False)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/extmod/tls_psk.py b/tests/extmod/tls_psk.py
new file mode 100644
index 00000000000..cc49ecfc8ce
--- /dev/null
+++ b/tests/extmod/tls_psk.py
@@ -0,0 +1,42 @@
+# Test the tls.SSLContext PSK attributes (psk_identity, psk_key,
+# server_psk_keys) and basic validation, without needing a network connection.
+
+try:
+    import tls
+
+    # PSK support is optional; psk_identity only exists when it's enabled.
+    tls.SSLContext(tls.PROTOCOL_TLS_CLIENT).psk_identity
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+
+ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+
+# The PSK attributes default to None.
+print(ctx.psk_identity, ctx.psk_key, ctx.server_psk_keys)
+
+# They can be set and read back.
+ctx.psk_identity = b"micropython"
+ctx.psk_key = b"secret-shared-key"
+ctx.server_psk_keys = {b"micropython": b"secret-shared-key"}
+print(ctx.psk_identity, ctx.psk_key, ctx.server_psk_keys)
+
+# A PSK key longer than mbedTLS's maximum (MBEDTLS_PSK_MAX_LEN) is rejected
+# when it is set on the context (the identity is already set above).
+try:
+    ctx.psk_key = b"x" * 49
+    print("no error")
+except OSError:
+    print("OSError")
+
+# The key/identity are only applied once both are set, in either order.  Here
+# the over-length key is set first (a no-op while the identity is None), so the
+# rejection only surfaces when the identity is set and the pair is applied.
+ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+ctx.psk_key = b"x" * 49
+try:
+    ctx.psk_identity = b"micropython"
+    print("no error")
+except OSError:
+    print("OSError")
diff --git a/tests/extmod/tls_psk.py.exp b/tests/extmod/tls_psk.py.exp
new file mode 100644
index 00000000000..060d5120d85
--- /dev/null
+++ b/tests/extmod/tls_psk.py.exp
@@ -0,0 +1,4 @@
+None None None
+b'micropython' b'secret-shared-key' {b'micropython': b'secret-shared-key'}
+OSError
+OSError
diff --git a/tests/extmod/tls_sslcontext_ciphers.py b/tests/extmod/tls_sslcontext_ciphers.py
new file mode 100644
index 00000000000..9f650148b8e
--- /dev/null
+++ b/tests/extmod/tls_sslcontext_ciphers.py
@@ -0,0 +1,34 @@
+# Basic test of tls.SSLContext get_ciphers() and set_ciphers() methods.
+
+try:
+    import tls
+
+    tls.SSLContext.get_ciphers
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+
+ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+
+ciphers = ctx.get_ciphers()
+
+for ci in ciphers:
+    # Only print those ciphers know to exist on all ports.
+    if ("TLS-ECDHE-ECDSA-WITH-AES" in ci or "TLS-RSA-WITH-AES" in ci) and "CBC" in ci:
+        print(ci)
+
+ctx.set_ciphers(ciphers[:1])
+
+# Test error cases.
+
+try:
+    ctx.set_ciphers(ciphers[0])
+except TypeError as e:
+    # Error message varies depending on the error detail level
+    print("TypeError")
+
+try:
+    ctx.set_ciphers(["BAR"])
+except OSError as e:
+    print(e)
diff --git a/tests/extmod/tls_sslcontext_ciphers.py.exp b/tests/extmod/tls_sslcontext_ciphers.py.exp
new file mode 100644
index 00000000000..6d07dfe2587
--- /dev/null
+++ b/tests/extmod/tls_sslcontext_ciphers.py.exp
@@ -0,0 +1,10 @@
+TLS-ECDHE-ECDSA-WITH-AES-256-CBC-SHA384
+TLS-ECDHE-ECDSA-WITH-AES-256-CBC-SHA
+TLS-ECDHE-ECDSA-WITH-AES-128-CBC-SHA256
+TLS-ECDHE-ECDSA-WITH-AES-128-CBC-SHA
+TLS-RSA-WITH-AES-256-CBC-SHA256
+TLS-RSA-WITH-AES-256-CBC-SHA
+TLS-RSA-WITH-AES-128-CBC-SHA256
+TLS-RSA-WITH-AES-128-CBC-SHA
+TypeError
+(-24192, 'MBEDTLS_ERR_SSL_BAD_CONFIG')
diff --git a/tests/extmod/tls_sslcontext_micropython.py b/tests/extmod/tls_sslcontext_micropython.py
new file mode 100644
index 00000000000..e188f89b8d3
--- /dev/null
+++ b/tests/extmod/tls_sslcontext_micropython.py
@@ -0,0 +1,29 @@
+# Test MicroPython-specific behaviour of tls.SSLContext.
+
+try:
+    import tls
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Test constructing without any arguments (in CPython it's a DeprecationWarning).
+try:
+    tls.SSLContext()
+except TypeError:
+    print("TypeError")
+
+# Test attributes that don't exist (in CPython new attributes can be added).
+# This test is needed for coverage because SSLContext implements a custom attr handler.
+ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+try:
+    ctx.does_not_exist
+except AttributeError:
+    print("AttributeError on load")
+try:
+    ctx.does_not_exist = None
+except AttributeError:
+    print("AttributeError on store")
+try:
+    del ctx.does_not_exist
+except AttributeError:
+    print("AttributeError on delete")
diff --git a/tests/extmod/tls_sslcontext_micropython.py.exp b/tests/extmod/tls_sslcontext_micropython.py.exp
new file mode 100644
index 00000000000..21e65258ff2
--- /dev/null
+++ b/tests/extmod/tls_sslcontext_micropython.py.exp
@@ -0,0 +1,4 @@
+TypeError
+AttributeError on load
+AttributeError on store
+AttributeError on delete
diff --git a/tests/extmod/tls_threads.py b/tests/extmod/tls_threads.py
new file mode 100644
index 00000000000..1e0c3d23d2f
--- /dev/null
+++ b/tests/extmod/tls_threads.py
@@ -0,0 +1,58 @@
+# Ensure that SSL sockets can be allocated from multiple
+# threads without thread safety issues
+
+try:
+    import _thread
+    import io
+    import tls
+    import time
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+import unittest
+
+
+class TestSocket(io.IOBase):
+    def write(self, buf):
+        return len(buf)
+
+    def readinto(self, buf):
+        return 0
+
+    def ioctl(self, cmd, arg):
+        return 0
+
+    def setblocking(self, value):
+        pass
+
+
+ITERS = 256
+
+
+class TLSThreads(unittest.TestCase):
+    def test_sslsocket_threaded(self):
+        self.done = False
+        # only run in two threads: too much RAM demand otherwise, and rp2 only
+        # supports two anyhow
+        _thread.start_new_thread(self._alloc_many_sockets, (True,))
+        self._alloc_many_sockets(False)
+        while not self.done:
+            time.sleep(0.1)
+        print("done")
+
+    def _alloc_many_sockets(self, set_done_flag):
+        print("start", _thread.get_ident())
+        ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+        ctx.verify_mode = tls.CERT_NONE
+        for n in range(ITERS):
+            s = TestSocket()
+            s = ctx.wrap_socket(s, do_handshake_on_connect=False)
+            s.close()  # Free associated resources now from thread, not in a GC pass
+        print("done", _thread.get_ident())
+        if set_done_flag:
+            self.done = True
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/extmod/uasyncio_await_return.py b/tests/extmod/uasyncio_await_return.py
deleted file mode 100644
index d375c9ea97a..00000000000
--- a/tests/extmod/uasyncio_await_return.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Test that tasks return their value correctly to the caller
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def foo():
-    return 42
-
-
-async def main():
-    # Call function directly via an await
-    print(await foo())
-
-    # Create a task and await on it
-    task = asyncio.create_task(foo())
-    print(await task)
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_await_return.py.exp b/tests/extmod/uasyncio_await_return.py.exp
deleted file mode 100644
index daaac9e3030..00000000000
--- a/tests/extmod/uasyncio_await_return.py.exp
+++ /dev/null
@@ -1,2 +0,0 @@
-42
-42
diff --git a/tests/extmod/uasyncio_basic.py b/tests/extmod/uasyncio_basic.py
deleted file mode 100644
index 20c0a82e47a..00000000000
--- a/tests/extmod/uasyncio_basic.py
+++ /dev/null
@@ -1,51 +0,0 @@
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-try:
-    import utime
-
-    ticks = utime.ticks_ms
-    ticks_diff = utime.ticks_diff
-except:
-    import time
-
-    ticks = lambda: int(time.time() * 1000)
-    ticks_diff = lambda t1, t0: t1 - t0
-
-
-async def delay_print(t, s):
-    await asyncio.sleep(t)
-    print(s)
-
-
-async def main():
-    print("start")
-
-    await asyncio.sleep(0.001)
-    print("after sleep")
-
-    t0 = ticks()
-    await delay_print(0.2, "short")
-    t1 = ticks()
-    await delay_print(0.4, "long")
-    t2 = ticks()
-    await delay_print(-1, "negative")
-    t3 = ticks()
-
-    print(
-        "took {} {} {}".format(
-            round(ticks_diff(t1, t0), -2),
-            round(ticks_diff(t2, t1), -2),
-            round(ticks_diff(t3, t2), -2),
-        )
-    )
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_basic.py.exp b/tests/extmod/uasyncio_basic.py.exp
deleted file mode 100644
index 478e22abc8f..00000000000
--- a/tests/extmod/uasyncio_basic.py.exp
+++ /dev/null
@@ -1,6 +0,0 @@
-start
-after sleep
-short
-long
-negative
-took 200 400 0
diff --git a/tests/extmod/uasyncio_basic2.py b/tests/extmod/uasyncio_basic2.py
deleted file mode 100644
index a2167e48ee1..00000000000
--- a/tests/extmod/uasyncio_basic2.py
+++ /dev/null
@@ -1,24 +0,0 @@
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def forever():
-    print("forever start")
-    await asyncio.sleep(10)
-
-
-async def main():
-    print("main start")
-    asyncio.create_task(forever())
-    await asyncio.sleep(0.001)
-    print("main done")
-    return 42
-
-
-print(asyncio.run(main()))
diff --git a/tests/extmod/uasyncio_basic2.py.exp b/tests/extmod/uasyncio_basic2.py.exp
deleted file mode 100644
index 3ca3521728d..00000000000
--- a/tests/extmod/uasyncio_basic2.py.exp
+++ /dev/null
@@ -1,4 +0,0 @@
-main start
-forever start
-main done
-42
diff --git a/tests/extmod/uasyncio_cancel_fair.py b/tests/extmod/uasyncio_cancel_fair.py
deleted file mode 100644
index 9a7b35c1618..00000000000
--- a/tests/extmod/uasyncio_cancel_fair.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# Test fairness of cancelling a task
-# That tasks which continuously cancel each other don't take over the scheduler
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task(id, other):
-    for i in range(3):
-        try:
-            print("start", id)
-            await asyncio.sleep(0)
-            print("done", id)
-        except asyncio.CancelledError as er:
-            print("cancelled", id)
-        if other is not None:
-            print(id, "cancels", other)
-            tasks[other].cancel()
-
-
-async def main():
-    global tasks
-    tasks = [
-        asyncio.create_task(task(0, 1)),
-        asyncio.create_task(task(1, 0)),
-        asyncio.create_task(task(2, None)),
-    ]
-    await tasks[2]
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_cancel_fair.py.exp b/tests/extmod/uasyncio_cancel_fair.py.exp
deleted file mode 100644
index 8f5da08e4ce..00000000000
--- a/tests/extmod/uasyncio_cancel_fair.py.exp
+++ /dev/null
@@ -1,24 +0,0 @@
-start 0
-start 1
-start 2
-done 0
-0 cancels 1
-start 0
-cancelled 1
-1 cancels 0
-start 1
-done 2
-start 2
-cancelled 0
-0 cancels 1
-start 0
-cancelled 1
-1 cancels 0
-start 1
-done 2
-start 2
-cancelled 0
-0 cancels 1
-cancelled 1
-1 cancels 0
-done 2
diff --git a/tests/extmod/uasyncio_cancel_fair2.py b/tests/extmod/uasyncio_cancel_fair2.py
deleted file mode 100644
index 46e40f71b15..00000000000
--- a/tests/extmod/uasyncio_cancel_fair2.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# Test fairness of cancelling a task
-# That tasks which keeps being cancelled by multiple other tasks gets a chance to run
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task_a():
-    try:
-        while True:
-            print("sleep a")
-            await asyncio.sleep(0)
-    except asyncio.CancelledError:
-        print("cancelled a")
-
-
-async def task_b(id, other):
-    while other.cancel():
-        print("sleep b", id)
-        await asyncio.sleep(0)
-    print("done b", id)
-
-
-async def main():
-    t = asyncio.create_task(task_a())
-    for i in range(3):
-        asyncio.create_task(task_b(i, t))
-    await t
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_cancel_fair2.py.exp b/tests/extmod/uasyncio_cancel_fair2.py.exp
deleted file mode 100644
index e9dd649b453..00000000000
--- a/tests/extmod/uasyncio_cancel_fair2.py.exp
+++ /dev/null
@@ -1,8 +0,0 @@
-sleep a
-sleep b 0
-sleep b 1
-sleep b 2
-cancelled a
-done b 0
-done b 1
-done b 2
diff --git a/tests/extmod/uasyncio_cancel_self.py b/tests/extmod/uasyncio_cancel_self.py
deleted file mode 100644
index 660ae663893..00000000000
--- a/tests/extmod/uasyncio_cancel_self.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Test a task cancelling itself (currently unsupported)
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task():
-    print("task start")
-    global_task.cancel()
-
-
-async def main():
-    global global_task
-    global_task = asyncio.create_task(task())
-    try:
-        await global_task
-    except asyncio.CancelledError:
-        print("main cancel")
-    print("main done")
-
-
-try:
-    asyncio.run(main())
-except RuntimeError as er:
-    print(er)
diff --git a/tests/extmod/uasyncio_cancel_task.py b/tests/extmod/uasyncio_cancel_task.py
deleted file mode 100644
index ec60d855457..00000000000
--- a/tests/extmod/uasyncio_cancel_task.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# Test cancelling a task
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task(s, allow_cancel):
-    try:
-        print("task start")
-        await asyncio.sleep(s)
-        print("task done")
-    except asyncio.CancelledError as er:
-        print("task cancel")
-        if allow_cancel:
-            raise er
-
-
-async def task2(allow_cancel):
-    print("task 2")
-    try:
-        await asyncio.create_task(task(0.05, allow_cancel))
-    except asyncio.CancelledError as er:
-        print("task 2 cancel")
-        raise er
-    print("task 2 done")
-
-
-async def main():
-    # Cancel task immediately
-    t = asyncio.create_task(task(2, True))
-    print(t.cancel())
-
-    # Cancel task after it has started
-    t = asyncio.create_task(task(2, True))
-    await asyncio.sleep(0.01)
-    print(t.cancel())
-    print("main sleep")
-    await asyncio.sleep(0.01)
-
-    # Cancel task multiple times after it has started
-    t = asyncio.create_task(task(2, True))
-    await asyncio.sleep(0.01)
-    for _ in range(4):
-        print(t.cancel())
-    print("main sleep")
-    await asyncio.sleep(0.01)
-
-    # Await on a cancelled task
-    print("main wait")
-    try:
-        await t
-    except asyncio.CancelledError:
-        print("main got CancelledError")
-
-    # Cancel task after it has finished
-    t = asyncio.create_task(task(0.01, False))
-    await asyncio.sleep(0.05)
-    print(t.cancel())
-
-    # Nested: task2 waits on task, task2 is cancelled (should cancel task then task2)
-    print("----")
-    t = asyncio.create_task(task2(True))
-    await asyncio.sleep(0.01)
-    print("main cancel")
-    t.cancel()
-    print("main sleep")
-    await asyncio.sleep(0.1)
-
-    # Nested: task2 waits on task, task2 is cancelled but task doesn't allow it (task2 should continue)
-    print("----")
-    t = asyncio.create_task(task2(False))
-    await asyncio.sleep(0.01)
-    print("main cancel")
-    t.cancel()
-    print("main sleep")
-    await asyncio.sleep(0.1)
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_cancel_task.py.exp b/tests/extmod/uasyncio_cancel_task.py.exp
deleted file mode 100644
index 031b94023fd..00000000000
--- a/tests/extmod/uasyncio_cancel_task.py.exp
+++ /dev/null
@@ -1,31 +0,0 @@
-True
-task start
-True
-main sleep
-task cancel
-task start
-True
-True
-True
-True
-main sleep
-task cancel
-main wait
-main got CancelledError
-task start
-task done
-False
-----
-task 2
-task start
-main cancel
-main sleep
-task cancel
-task 2 cancel
-----
-task 2
-task start
-main cancel
-main sleep
-task cancel
-task 2 done
diff --git a/tests/extmod/uasyncio_cancel_wait_on_finished.py b/tests/extmod/uasyncio_cancel_wait_on_finished.py
deleted file mode 100644
index 66b36dd60d4..00000000000
--- a/tests/extmod/uasyncio_cancel_wait_on_finished.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# Test cancelling a task that is waiting on a task that just finishes.
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def sleep_task():
-    print("sleep_task sleep")
-    await asyncio.sleep(0)
-    print("sleep_task wake")
-
-
-async def wait_task(t):
-    print("wait_task wait")
-    await t
-    print("wait_task wake")
-
-
-async def main():
-    waiting_task = asyncio.create_task(wait_task(asyncio.create_task(sleep_task())))
-
-    print("main sleep")
-    await asyncio.sleep(0)
-    print("main sleep")
-    await asyncio.sleep(0)
-
-    waiting_task.cancel()
-    print("main wait")
-    try:
-        await waiting_task
-    except asyncio.CancelledError as er:
-        print(repr(er))
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_cancel_wait_on_finished.py.exp b/tests/extmod/uasyncio_cancel_wait_on_finished.py.exp
deleted file mode 100644
index 60e871bfe56..00000000000
--- a/tests/extmod/uasyncio_cancel_wait_on_finished.py.exp
+++ /dev/null
@@ -1,7 +0,0 @@
-main sleep
-sleep_task sleep
-wait_task wait
-main sleep
-sleep_task wake
-main wait
-CancelledError()
diff --git a/tests/extmod/uasyncio_current_task.py b/tests/extmod/uasyncio_current_task.py
deleted file mode 100644
index 3165a2cf16a..00000000000
--- a/tests/extmod/uasyncio_current_task.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Test current_task() function
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task(result):
-    result[0] = asyncio.current_task()
-
-
-async def main():
-    result = [None]
-    t = asyncio.create_task(task(result))
-    await asyncio.sleep(0)
-    await asyncio.sleep(0)
-    print(t is result[0])
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_current_task.py.exp b/tests/extmod/uasyncio_current_task.py.exp
deleted file mode 100644
index 0ca95142bb7..00000000000
--- a/tests/extmod/uasyncio_current_task.py.exp
+++ /dev/null
@@ -1 +0,0 @@
-True
diff --git a/tests/extmod/uasyncio_event.py b/tests/extmod/uasyncio_event.py
deleted file mode 100644
index fb8eb9ffa40..00000000000
--- a/tests/extmod/uasyncio_event.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# Test Event class
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task(id, ev):
-    print("start", id)
-    print(await ev.wait())
-    print("end", id)
-
-
-async def task_delay_set(t, ev):
-    await asyncio.sleep(t)
-    print("set event")
-    ev.set()
-
-
-async def main():
-    ev = asyncio.Event()
-
-    # Set and clear without anything waiting, and test is_set()
-    print(ev.is_set())
-    ev.set()
-    print(ev.is_set())
-    ev.clear()
-    print(ev.is_set())
-
-    # Create 2 tasks waiting on the event
-    print("----")
-    asyncio.create_task(task(1, ev))
-    asyncio.create_task(task(2, ev))
-    print("yield")
-    await asyncio.sleep(0)
-    print("set event")
-    ev.set()
-    print("yield")
-    await asyncio.sleep(0)
-
-    # Create a task waiting on the already-set event
-    print("----")
-    asyncio.create_task(task(3, ev))
-    print("yield")
-    await asyncio.sleep(0)
-
-    # Clear event, start a task, then set event again
-    print("----")
-    print("clear event")
-    ev.clear()
-    asyncio.create_task(task(4, ev))
-    await asyncio.sleep(0)
-    print("set event")
-    ev.set()
-    await asyncio.sleep(0)
-
-    # Cancel a task waiting on an event (set event then cancel task)
-    print("----")
-    ev = asyncio.Event()
-    t = asyncio.create_task(task(5, ev))
-    await asyncio.sleep(0)
-    ev.set()
-    t.cancel()
-    await asyncio.sleep(0.1)
-
-    # Cancel a task waiting on an event (cancel task then set event)
-    print("----")
-    ev = asyncio.Event()
-    t = asyncio.create_task(task(6, ev))
-    await asyncio.sleep(0)
-    t.cancel()
-    ev.set()
-    await asyncio.sleep(0.1)
-
-    # Wait for an event that does get set in time
-    print("----")
-    ev.clear()
-    asyncio.create_task(task_delay_set(0.01, ev))
-    await asyncio.wait_for(ev.wait(), 0.1)
-    await asyncio.sleep(0)
-
-    # Wait for an event that doesn't get set in time
-    print("----")
-    ev.clear()
-    asyncio.create_task(task_delay_set(0.1, ev))
-    try:
-        await asyncio.wait_for(ev.wait(), 0.01)
-    except asyncio.TimeoutError:
-        print("TimeoutError")
-    await ev.wait()
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_event.py.exp b/tests/extmod/uasyncio_event.py.exp
deleted file mode 100644
index 3188f291e58..00000000000
--- a/tests/extmod/uasyncio_event.py.exp
+++ /dev/null
@@ -1,33 +0,0 @@
-False
-True
-False
-----
-yield
-start 1
-start 2
-set event
-yield
-True
-end 1
-True
-end 2
-----
-yield
-start 3
-True
-end 3
-----
-clear event
-start 4
-set event
-True
-end 4
-----
-start 5
-----
-start 6
-----
-set event
-----
-TimeoutError
-set event
diff --git a/tests/extmod/uasyncio_event_fair.py b/tests/extmod/uasyncio_event_fair.py
deleted file mode 100644
index 37eca5faef1..00000000000
--- a/tests/extmod/uasyncio_event_fair.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# Test fairness of Event.set()
-# That tasks which continuously wait on events don't take over the scheduler
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task1(id):
-    for i in range(4):
-        print("sleep", id)
-        await asyncio.sleep(0)
-
-
-async def task2(id, ev):
-    for i in range(4):
-        ev.set()
-        ev.clear()
-        print("wait", id)
-        await ev.wait()
-
-
-async def main():
-    ev = asyncio.Event()
-    tasks = [
-        asyncio.create_task(task1(0)),
-        asyncio.create_task(task2(2, ev)),
-        asyncio.create_task(task1(1)),
-        asyncio.create_task(task2(3, ev)),
-    ]
-    await tasks[1]
-    ev.set()
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_event_fair.py.exp b/tests/extmod/uasyncio_event_fair.py.exp
deleted file mode 100644
index fe2e3d6e47a..00000000000
--- a/tests/extmod/uasyncio_event_fair.py.exp
+++ /dev/null
@@ -1,16 +0,0 @@
-sleep 0
-wait 2
-sleep 1
-wait 3
-sleep 0
-sleep 1
-wait 2
-sleep 0
-sleep 1
-wait 3
-sleep 0
-sleep 1
-wait 2
-wait 3
-wait 2
-wait 3
diff --git a/tests/extmod/uasyncio_exception.py b/tests/extmod/uasyncio_exception.py
deleted file mode 100644
index 4e4f978dc20..00000000000
--- a/tests/extmod/uasyncio_exception.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Test general exception handling
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-# main task raising an exception
-async def main():
-    print("main start")
-    raise ValueError(1)
-    print("main done")
-
-
-try:
-    asyncio.run(main())
-except ValueError as er:
-    print("ValueError", er.args[0])
-
-
-# sub-task raising an exception
-async def task():
-    print("task start")
-    raise ValueError(2)
-    print("task done")
-
-
-async def main():
-    print("main start")
-    t = asyncio.create_task(task())
-    await t
-    print("main done")
-
-
-try:
-    asyncio.run(main())
-except ValueError as er:
-    print("ValueError", er.args[0])
-
-
-# main task raising an exception with sub-task not yet scheduled
-# TODO not currently working, task is never scheduled
-async def task():
-    # print('task run') uncomment this line when it works
-    pass
-
-
-async def main():
-    print("main start")
-    asyncio.create_task(task())
-    raise ValueError(3)
-    print("main done")
-
-
-try:
-    asyncio.run(main())
-except ValueError as er:
-    print("ValueError", er.args[0])
diff --git a/tests/extmod/uasyncio_exception.py.exp b/tests/extmod/uasyncio_exception.py.exp
deleted file mode 100644
index b2ee860170e..00000000000
--- a/tests/extmod/uasyncio_exception.py.exp
+++ /dev/null
@@ -1,7 +0,0 @@
-main start
-ValueError 1
-main start
-task start
-ValueError 2
-main start
-ValueError 3
diff --git a/tests/extmod/uasyncio_fair.py b/tests/extmod/uasyncio_fair.py
deleted file mode 100644
index e2257060548..00000000000
--- a/tests/extmod/uasyncio_fair.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Test fairness of scheduler
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task(id, t):
-    print("task start", id)
-    while True:
-        if t > 0:
-            print("task work", id)
-        await asyncio.sleep(t)
-
-
-async def main():
-    t1 = asyncio.create_task(task(1, -0.01))
-    t2 = asyncio.create_task(task(2, 0.1))
-    t3 = asyncio.create_task(task(3, 0.18))
-    t4 = asyncio.create_task(task(4, -100))
-    await asyncio.sleep(0.5)
-    t1.cancel()
-    t2.cancel()
-    t3.cancel()
-    t4.cancel()
-    print("finish")
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_fair.py.exp b/tests/extmod/uasyncio_fair.py.exp
deleted file mode 100644
index b4b6481db01..00000000000
--- a/tests/extmod/uasyncio_fair.py.exp
+++ /dev/null
@@ -1,13 +0,0 @@
-task start 1
-task start 2
-task work 2
-task start 3
-task work 3
-task start 4
-task work 2
-task work 3
-task work 2
-task work 2
-task work 3
-task work 2
-finish
diff --git a/tests/extmod/uasyncio_gather.py b/tests/extmod/uasyncio_gather.py
deleted file mode 100644
index af6ea555ab1..00000000000
--- a/tests/extmod/uasyncio_gather.py
+++ /dev/null
@@ -1,114 +0,0 @@
-# test uasyncio.gather() function
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def factorial(name, number):
-    f = 1
-    for i in range(2, number + 1):
-        print("Task {}: Compute factorial({})...".format(name, i))
-        await asyncio.sleep(0.01)
-        f *= i
-    print("Task {}: factorial({}) = {}".format(name, number, f))
-    return f
-
-
-async def task(id, t=0.1):
-    print("start", id)
-    await asyncio.sleep(t)
-    print("end", id)
-    return id
-
-
-async def task_loop(id):
-    print("task_loop start", id)
-    while True:
-        await asyncio.sleep(0.1)
-        print("task_loop loop", id)
-
-
-async def task_raise(id, t=0.1):
-    print("task_raise start", id)
-    await asyncio.sleep(t)
-    print("task_raise raise", id)
-    raise ValueError(id)
-
-
-async def gather_task(t0, t1):
-    print("gather_task")
-    await asyncio.gather(t0, t1)
-    print("gather_task2")
-
-
-async def main():
-    # Simple gather with return values
-    print(await asyncio.gather(factorial("A", 2), factorial("B", 3), factorial("C", 4)))
-
-    print("====")
-
-    # Gather with no awaitables
-    print(await asyncio.gather())
-
-    print("====")
-
-    # Test return_exceptions, where one task is cancelled and the other finishes normally
-    tasks = [asyncio.create_task(task(1)), asyncio.create_task(task(2))]
-    tasks[0].cancel()
-    print(await asyncio.gather(*tasks, return_exceptions=True))
-
-    print("====")
-
-    # Test return_exceptions, where one task raises an exception and the other finishes normally.
-    tasks = [asyncio.create_task(task(1)), asyncio.create_task(task_raise(2))]
-    print(await asyncio.gather(*tasks, return_exceptions=True))
-
-    print("====")
-
-    # Test case where one task raises an exception and other task keeps running.
-    tasks = [asyncio.create_task(task_loop(1)), asyncio.create_task(task_raise(2))]
-    try:
-        await asyncio.gather(*tasks)
-    except ValueError as er:
-        print(repr(er))
-    print(tasks[0].done(), tasks[1].done())
-    for t in tasks:
-        t.cancel()
-    await asyncio.sleep(0.2)
-
-    print("====")
-
-    # Test case where both tasks raise an exception.
-    # Use t=0 so they raise one after the other, between the gather starting and finishing.
-    tasks = [asyncio.create_task(task_raise(1, t=0)), asyncio.create_task(task_raise(2, t=0))]
-    try:
-        await asyncio.gather(*tasks)
-    except ValueError as er:
-        print(repr(er))
-    print(tasks[0].done(), tasks[1].done())
-
-    print("====")
-
-    # Cancel a multi gather.
-    t = asyncio.create_task(gather_task(task(1), task(2)))
-    await asyncio.sleep(0.05)
-    t.cancel()
-    await asyncio.sleep(0.2)
-
-    # Test edge cases where the gather is cancelled just as tasks are created and ending.
-    for i in range(1, 4):
-        print("====")
-        t = asyncio.create_task(gather_task(task(1, t=0), task(2, t=0)))
-        for _ in range(i):
-            await asyncio.sleep(0)
-        t.cancel()
-        await asyncio.sleep(0.2)
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_gather_notimpl.py b/tests/extmod/uasyncio_gather_notimpl.py
deleted file mode 100644
index 3ebab9bad63..00000000000
--- a/tests/extmod/uasyncio_gather_notimpl.py
+++ /dev/null
@@ -1,53 +0,0 @@
-# Test uasyncio.gather() function, features that are not implemented.
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-def custom_handler(loop, context):
-    print(repr(context["exception"]))
-
-
-async def task(id):
-    print("task start", id)
-    await asyncio.sleep(0.01)
-    print("task end", id)
-    return id
-
-
-async def gather_task(t0, t1):
-    print("gather_task start")
-    await asyncio.gather(t0, t1)
-    print("gather_task end")
-
-
-async def main():
-    loop = asyncio.get_event_loop()
-    loop.set_exception_handler(custom_handler)
-
-    # Test case where can't wait on a task being gathered.
-    tasks = [asyncio.create_task(task(1)), asyncio.create_task(task(2))]
-    gt = asyncio.create_task(gather_task(tasks[0], tasks[1]))
-    await asyncio.sleep(0)  # let the gather start
-    try:
-        await tasks[0]  # can't await because this task is part of the gather
-    except RuntimeError as er:
-        print(repr(er))
-    await gt
-
-    print("====")
-
-    # Test case where can't gather on a task being waited.
-    tasks = [asyncio.create_task(task(1)), asyncio.create_task(task(2))]
-    asyncio.create_task(gather_task(tasks[0], tasks[1]))
-    await tasks[0]  # wait on this task before the gather starts
-    await tasks[1]
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_get_event_loop.py b/tests/extmod/uasyncio_get_event_loop.py
deleted file mode 100644
index 8ccbd6814e9..00000000000
--- a/tests/extmod/uasyncio_get_event_loop.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Test get_event_loop()
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def main():
-    print("start")
-    await asyncio.sleep(0.01)
-    print("end")
-
-
-loop = asyncio.get_event_loop()
-loop.run_until_complete(main())
diff --git a/tests/extmod/uasyncio_heaplock.py b/tests/extmod/uasyncio_heaplock.py
deleted file mode 100644
index 9c9487357a2..00000000000
--- a/tests/extmod/uasyncio_heaplock.py
+++ /dev/null
@@ -1,82 +0,0 @@
-# test that the following do not use the heap:
-# - basic scheduling of tasks
-# - uasyncio.sleep_ms
-# - StreamWriter.write, stream is blocked and data to write is a bytes object
-# - StreamWriter.write, when stream is not blocked
-
-import micropython
-
-# strict stackless builds can't call functions without allocating a frame on the heap
-try:
-    # force bytecode (in case we're running with emit=native) and verify
-    # that bytecode-calling-bytecode doesn't allocate
-    @micropython.bytecode
-    def f(x):
-        x and f(x - 1)
-
-    micropython.heap_lock()
-    f(1)
-    micropython.heap_unlock()
-except RuntimeError:
-    # RuntimeError (max recursion depth) not MemoryError because effectively
-    # the recursion depth is at the limit while the heap is locked with
-    # stackless
-    print("SKIP")
-    raise SystemExit
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-class TestStream:
-    def __init__(self, blocked):
-        self.blocked = blocked
-
-    def write(self, data):
-        print("TestStream.write", data)
-        if self.blocked:
-            return None
-        return len(data)
-
-
-async def task(id, n, t):
-    for i in range(n):
-        print(id, i)
-        await asyncio.sleep_ms(t)
-
-
-async def main():
-    t1 = asyncio.create_task(task(1, 4, 100))
-    t2 = asyncio.create_task(task(2, 2, 250))
-
-    # test scheduling tasks, and calling sleep_ms
-    micropython.heap_lock()
-    print("start")
-    await asyncio.sleep_ms(5)
-    print("sleep")
-    await asyncio.sleep_ms(350)
-    print("finish")
-    micropython.heap_unlock()
-
-    # test writing to a stream, when the underlying stream is blocked
-    s = asyncio.StreamWriter(TestStream(True), None)
-    micropython.heap_lock()
-    s.write(b"12")
-    micropython.heap_unlock()
-
-    # test writing to a stream, when the underlying stream is not blocked
-    buf = bytearray(b"56")
-    s = asyncio.StreamWriter(TestStream(False), None)
-    micropython.heap_lock()
-    s.write(b"34")
-    s.write(buf)
-    micropython.heap_unlock()
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_lock.py b/tests/extmod/uasyncio_lock.py
deleted file mode 100644
index 096a8c82be1..00000000000
--- a/tests/extmod/uasyncio_lock.py
+++ /dev/null
@@ -1,97 +0,0 @@
-# Test Lock class
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task_loop(id, lock):
-    print("task start", id)
-    for i in range(3):
-        async with lock:
-            print("task have", id, i)
-    print("task end", id)
-
-
-async def task_sleep(lock):
-    async with lock:
-        print("task have", lock.locked())
-        await asyncio.sleep(0.2)
-    print("task release", lock.locked())
-    await lock.acquire()
-    print("task have again")
-    lock.release()
-
-
-async def task_cancel(id, lock, to_cancel=None):
-    try:
-        async with lock:
-            print("task got", id)
-            await asyncio.sleep(0.1)
-        print("task release", id)
-        if to_cancel:
-            to_cancel[0].cancel()
-    except asyncio.CancelledError:
-        print("task cancel", id)
-
-
-async def main():
-    lock = asyncio.Lock()
-
-    # Basic acquire/release
-    print(lock.locked())
-    await lock.acquire()
-    print(lock.locked())
-    await asyncio.sleep(0)
-    lock.release()
-    print(lock.locked())
-    await asyncio.sleep(0)
-
-    # Use with "async with"
-    async with lock:
-        print("have lock")
-
-    # 3 tasks wanting the lock
-    print("----")
-    asyncio.create_task(task_loop(1, lock))
-    asyncio.create_task(task_loop(2, lock))
-    t3 = asyncio.create_task(task_loop(3, lock))
-    await lock.acquire()
-    await asyncio.sleep(0)
-    lock.release()
-    await t3
-
-    # 2 sleeping tasks both wanting the lock
-    print("----")
-    asyncio.create_task(task_sleep(lock))
-    await asyncio.sleep(0.1)
-    await task_sleep(lock)
-
-    # 3 tasks, the first cancelling the second, the third should still run
-    print("----")
-    ts = [None]
-    asyncio.create_task(task_cancel(0, lock, ts))
-    ts[0] = asyncio.create_task(task_cancel(1, lock))
-    asyncio.create_task(task_cancel(2, lock))
-    await asyncio.sleep(0.3)
-    print(lock.locked())
-
-    # 3 tasks, the second and third being cancelled while waiting on the lock
-    print("----")
-    t0 = asyncio.create_task(task_cancel(0, lock))
-    t1 = asyncio.create_task(task_cancel(1, lock))
-    t2 = asyncio.create_task(task_cancel(2, lock))
-    await asyncio.sleep(0.05)
-    t1.cancel()
-    await asyncio.sleep(0.1)
-    t2.cancel()
-    await asyncio.sleep(0.1)
-    print(lock.locked())
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_lock.py.exp b/tests/extmod/uasyncio_lock.py.exp
deleted file mode 100644
index a37dfcbd2e5..00000000000
--- a/tests/extmod/uasyncio_lock.py.exp
+++ /dev/null
@@ -1,41 +0,0 @@
-False
-True
-False
-have lock
-----
-task start 1
-task start 2
-task start 3
-task have 1 0
-task have 2 0
-task have 3 0
-task have 1 1
-task have 2 1
-task have 3 1
-task have 1 2
-task end 1
-task have 2 2
-task end 2
-task have 3 2
-task end 3
-----
-task have True
-task release False
-task have True
-task release False
-task have again
-task have again
-----
-task got 0
-task release 0
-task cancel 1
-task got 2
-task release 2
-False
-----
-task got 0
-task cancel 1
-task release 0
-task got 2
-task cancel 2
-False
diff --git a/tests/extmod/uasyncio_lock_cancel.py b/tests/extmod/uasyncio_lock_cancel.py
deleted file mode 100644
index 85b8df84830..00000000000
--- a/tests/extmod/uasyncio_lock_cancel.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# Test that locks work when cancelling multiple waiters on the lock
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task(i, lock, lock_flag):
-    print("task", i, "start")
-    try:
-        await lock.acquire()
-    except asyncio.CancelledError:
-        print("task", i, "cancel")
-        return
-    print("task", i, "lock_flag", lock_flag[0])
-    lock_flag[0] = True
-    await asyncio.sleep(0)
-    lock.release()
-    lock_flag[0] = False
-    print("task", i, "done")
-
-
-async def main():
-    # Create a lock and acquire it so the tasks below must wait
-    lock = asyncio.Lock()
-    await lock.acquire()
-    lock_flag = [True]
-
-    # Create 4 tasks and let them all run
-    t0 = asyncio.create_task(task(0, lock, lock_flag))
-    t1 = asyncio.create_task(task(1, lock, lock_flag))
-    t2 = asyncio.create_task(task(2, lock, lock_flag))
-    t3 = asyncio.create_task(task(3, lock, lock_flag))
-    await asyncio.sleep(0)
-
-    # Cancel 2 of the tasks (which are waiting on the lock) and release the lock
-    t1.cancel()
-    t2.cancel()
-    lock.release()
-    lock_flag[0] = False
-
-    # Let the tasks run to completion
-    for _ in range(4):
-        await asyncio.sleep(0)
-
-    # The locke should be unlocked
-    print(lock.locked())
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_lock_cancel.py.exp b/tests/extmod/uasyncio_lock_cancel.py.exp
deleted file mode 100644
index 49ae2538874..00000000000
--- a/tests/extmod/uasyncio_lock_cancel.py.exp
+++ /dev/null
@@ -1,11 +0,0 @@
-task 0 start
-task 1 start
-task 2 start
-task 3 start
-task 1 cancel
-task 2 cancel
-task 0 lock_flag False
-task 0 done
-task 3 lock_flag False
-task 3 done
-False
diff --git a/tests/extmod/uasyncio_loop_stop.py b/tests/extmod/uasyncio_loop_stop.py
deleted file mode 100644
index 23507f9a7ef..00000000000
--- a/tests/extmod/uasyncio_loop_stop.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Test Loop.stop() to stop the event loop
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task():
-    print("task")
-
-
-async def main():
-    print("start")
-
-    # Stop the loop after next yield
-    loop.stop()
-
-    # Check that calling stop() again doesn't do/break anything
-    loop.stop()
-
-    # This await should stop
-    print("sleep")
-    await asyncio.sleep(0)
-
-    # Schedule stop, then create a new task, then yield
-    loop.stop()
-    asyncio.create_task(task())
-    await asyncio.sleep(0)
-
-    # Final stop
-    print("end")
-    loop.stop()
-
-
-loop = asyncio.get_event_loop()
-loop.create_task(main())
-
-for i in range(3):
-    print("run", i)
-    loop.run_forever()
diff --git a/tests/extmod/uasyncio_loop_stop.py.exp b/tests/extmod/uasyncio_loop_stop.py.exp
deleted file mode 100644
index bada5f0d841..00000000000
--- a/tests/extmod/uasyncio_loop_stop.py.exp
+++ /dev/null
@@ -1,7 +0,0 @@
-run 0
-start
-sleep
-run 1
-run 2
-task
-end
diff --git a/tests/extmod/uasyncio_micropython.py b/tests/extmod/uasyncio_micropython.py
deleted file mode 100644
index a6b65bb2a84..00000000000
--- a/tests/extmod/uasyncio_micropython.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Test MicroPython extensions on CPython asyncio:
-# - sleep_ms
-# - wait_for_ms
-
-try:
-    import utime, uasyncio
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-
-async def task(id, t):
-    print("task start", id)
-    await uasyncio.sleep_ms(t)
-    print("task end", id)
-    return id * 2
-
-
-async def main():
-    # Simple sleep_ms
-    t0 = utime.ticks_ms()
-    await uasyncio.sleep_ms(1)
-    print(utime.ticks_diff(utime.ticks_ms(), t0) < 100)
-
-    try:
-        # Sleep 1ms beyond maximum allowed sleep value
-        await uasyncio.sleep_ms(utime.ticks_add(0, -1) // 2 + 1)
-    except OverflowError:
-        print("OverflowError")
-
-    # When task finished before the timeout
-    print(await uasyncio.wait_for_ms(task(1, 5), 50))
-
-    # When timeout passes and task is cancelled
-    try:
-        print(await uasyncio.wait_for_ms(task(2, 50), 5))
-    except uasyncio.TimeoutError:
-        print("timeout")
-
-    print("finish")
-
-
-uasyncio.run(main())
diff --git a/tests/extmod/uasyncio_new_event_loop.py b/tests/extmod/uasyncio_new_event_loop.py
deleted file mode 100644
index b711befba97..00000000000
--- a/tests/extmod/uasyncio_new_event_loop.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# Test Loop.new_event_loop()
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task():
-    for i in range(4):
-        print("task", i)
-        await asyncio.sleep(0)
-        await asyncio.sleep(0)
-
-
-async def main():
-    print("start")
-    loop.create_task(task())
-    await asyncio.sleep(0)
-    print("stop")
-    loop.stop()
-
-
-# Use default event loop to run some tasks
-loop = asyncio.get_event_loop()
-loop.create_task(main())
-loop.run_forever()
-
-# Create new event loop, old one should not keep running
-loop = asyncio.new_event_loop()
-loop.create_task(main())
-loop.run_forever()
diff --git a/tests/extmod/uasyncio_new_event_loop.py.exp b/tests/extmod/uasyncio_new_event_loop.py.exp
deleted file mode 100644
index 9e104fda39c..00000000000
--- a/tests/extmod/uasyncio_new_event_loop.py.exp
+++ /dev/null
@@ -1,6 +0,0 @@
-start
-task 0
-stop
-start
-task 0
-stop
diff --git a/tests/extmod/uasyncio_set_exception_handler.py b/tests/extmod/uasyncio_set_exception_handler.py
deleted file mode 100644
index fe7b83eb4d8..00000000000
--- a/tests/extmod/uasyncio_set_exception_handler.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# Test that tasks return their value correctly to the caller
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-def custom_handler(loop, context):
-    print("custom_handler", repr(context["exception"]))
-
-
-async def task(i):
-    # Raise with 2 args so exception prints the same in uPy and CPython
-    raise ValueError(i, i + 1)
-
-
-async def main():
-    loop = asyncio.get_event_loop()
-
-    # Check default exception handler, should be None
-    print(loop.get_exception_handler())
-
-    # Set exception handler and test it was set
-    loop.set_exception_handler(custom_handler)
-    print(loop.get_exception_handler() == custom_handler)
-
-    # Create a task that raises and uses the custom exception handler
-    asyncio.create_task(task(0))
-    print("sleep")
-    for _ in range(2):
-        await asyncio.sleep(0)
-
-    # Create 2 tasks to test order of printing exception
-    asyncio.create_task(task(1))
-    asyncio.create_task(task(2))
-    print("sleep")
-    for _ in range(2):
-        await asyncio.sleep(0)
-
-    # Create a task, let it run, then await it (no exception should be printed)
-    t = asyncio.create_task(task(3))
-    await asyncio.sleep(0)
-    try:
-        await t
-    except ValueError as er:
-        print(repr(er))
-
-    print("done")
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_set_exception_handler.py.exp b/tests/extmod/uasyncio_set_exception_handler.py.exp
deleted file mode 100644
index fb4711469dc..00000000000
--- a/tests/extmod/uasyncio_set_exception_handler.py.exp
+++ /dev/null
@@ -1,9 +0,0 @@
-None
-True
-sleep
-custom_handler ValueError(0, 1)
-sleep
-custom_handler ValueError(1, 2)
-custom_handler ValueError(2, 3)
-ValueError(3, 4)
-done
diff --git a/tests/extmod/uasyncio_task_done.py b/tests/extmod/uasyncio_task_done.py
deleted file mode 100644
index 2700da8c341..00000000000
--- a/tests/extmod/uasyncio_task_done.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# Test the Task.done() method
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task(t, exc=None):
-    print("task start")
-    if t >= 0:
-        await asyncio.sleep(t)
-    if exc:
-        raise exc
-    print("task done")
-
-
-async def main():
-    # Task that finishes immediately.
-    print("=" * 10)
-    t = asyncio.create_task(task(-1))
-    print(t.done())
-    await asyncio.sleep(0)
-    print(t.done())
-    await t
-    print(t.done())
-
-    # Task that starts, runs and finishes.
-    print("=" * 10)
-    t = asyncio.create_task(task(0.01))
-    print(t.done())
-    await asyncio.sleep(0)
-    print(t.done())
-    await t
-    print(t.done())
-
-    # Task that raises immediately.
-    print("=" * 10)
-    t = asyncio.create_task(task(-1, ValueError))
-    print(t.done())
-    await asyncio.sleep(0)
-    print(t.done())
-    try:
-        await t
-    except ValueError as er:
-        print(repr(er))
-    print(t.done())
-
-    # Task that raises after a delay.
-    print("=" * 10)
-    t = asyncio.create_task(task(0.01, ValueError))
-    print(t.done())
-    await asyncio.sleep(0)
-    print(t.done())
-    try:
-        await t
-    except ValueError as er:
-        print(repr(er))
-    print(t.done())
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_task_done.py.exp b/tests/extmod/uasyncio_task_done.py.exp
deleted file mode 100644
index ddda04c5ec4..00000000000
--- a/tests/extmod/uasyncio_task_done.py.exp
+++ /dev/null
@@ -1,24 +0,0 @@
-==========
-False
-task start
-task done
-True
-True
-==========
-False
-task start
-False
-task done
-True
-==========
-False
-task start
-True
-ValueError()
-True
-==========
-False
-task start
-False
-ValueError()
-True
diff --git a/tests/extmod/uasyncio_threadsafeflag.py b/tests/extmod/uasyncio_threadsafeflag.py
deleted file mode 100644
index a8a08d2e925..00000000000
--- a/tests/extmod/uasyncio_threadsafeflag.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# Test Event class
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-
-import micropython
-
-try:
-    micropython.schedule
-except AttributeError:
-    print("SKIP")
-    raise SystemExit
-
-
-try:
-    # Unix port can't select/poll on user-defined types.
-    import uselect as select
-
-    poller = select.poll()
-    poller.register(asyncio.ThreadSafeFlag())
-except TypeError:
-    print("SKIP")
-    raise SystemExit
-
-
-async def task(id, flag):
-    print("task", id)
-    await flag.wait()
-    print("task", id, "done")
-
-
-def set_from_schedule(flag):
-    print("schedule")
-    flag.set()
-    print("schedule done")
-
-
-async def main():
-    flag = asyncio.ThreadSafeFlag()
-
-    # Set the flag from within the loop.
-    t = asyncio.create_task(task(1, flag))
-    print("yield")
-    await asyncio.sleep(0)
-    print("set event")
-    flag.set()
-    print("yield")
-    await asyncio.sleep(0)
-    print("wait task")
-    await t
-
-    # Set the flag from scheduler context.
-    print("----")
-    t = asyncio.create_task(task(2, flag))
-    print("yield")
-    await asyncio.sleep(0)
-    print("set event")
-    micropython.schedule(set_from_schedule, flag)
-    print("yield")
-    await asyncio.sleep(0)
-    print("wait task")
-    await t
-
-    # Flag already set.
-    print("----")
-    print("set event")
-    flag.set()
-    t = asyncio.create_task(task(3, flag))
-    print("yield")
-    await asyncio.sleep(0)
-    print("wait task")
-    await t
-
-    # Flag set, cleared, and set again.
-    print("----")
-    print("set event")
-    flag.set()
-    print("yield")
-    await asyncio.sleep(0)
-    print("clear event")
-    flag.clear()
-    print("yield")
-    await asyncio.sleep(0)
-    t = asyncio.create_task(task(4, flag))
-    print("yield")
-    await asyncio.sleep(0)
-    print("set event")
-    flag.set()
-    print("yield")
-    await asyncio.sleep(0)
-    print("wait task")
-    await t
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_threadsafeflag.py.exp b/tests/extmod/uasyncio_threadsafeflag.py.exp
deleted file mode 100644
index 757115ac4bd..00000000000
--- a/tests/extmod/uasyncio_threadsafeflag.py.exp
+++ /dev/null
@@ -1,32 +0,0 @@
-yield
-task 1
-set event
-yield
-wait task
-task 1 done
-----
-yield
-task 2
-set event
-yield
-schedule
-schedule done
-wait task
-task 2 done
-----
-set event
-yield
-task 3
-task 3 done
-wait task
-----
-set event
-yield
-clear event
-yield
-yield
-task 4
-set event
-yield
-wait task
-task 4 done
diff --git a/tests/extmod/uasyncio_wait_for.py b/tests/extmod/uasyncio_wait_for.py
deleted file mode 100644
index c636c7dd747..00000000000
--- a/tests/extmod/uasyncio_wait_for.py
+++ /dev/null
@@ -1,132 +0,0 @@
-# Test asyncio.wait_for
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def task(id, t):
-    print("task start", id)
-    await asyncio.sleep(t)
-    print("task end", id)
-    return id * 2
-
-
-async def task_catch():
-    print("task_catch start")
-    try:
-        await asyncio.sleep(0.2)
-    except asyncio.CancelledError:
-        print("ignore cancel")
-    print("task_catch done")
-
-
-async def task_raise():
-    print("task start")
-    raise ValueError
-
-
-async def task_cancel_other(t, other):
-    print("task_cancel_other start")
-    await asyncio.sleep(t)
-    print("task_cancel_other cancel")
-    other.cancel()
-
-
-async def task_wait_for_cancel(id, t, t_wait):
-    print("task_wait_for_cancel start")
-    try:
-        await asyncio.wait_for(task(id, t), t_wait)
-    except asyncio.CancelledError as er:
-        print("task_wait_for_cancel cancelled")
-        raise er
-
-
-async def task_wait_for_cancel_ignore(t_wait):
-    print("task_wait_for_cancel_ignore start")
-    try:
-        await asyncio.wait_for(task_catch(), t_wait)
-    except asyncio.CancelledError as er:
-        print("task_wait_for_cancel_ignore cancelled")
-        raise er
-
-
-async def main():
-    sep = "-" * 10
-
-    # When task finished before the timeout
-    print(await asyncio.wait_for(task(1, 0.01), 10))
-    print(sep)
-
-    # When timeout passes and task is cancelled
-    try:
-        print(await asyncio.wait_for(task(2, 10), 0.01))
-    except asyncio.TimeoutError:
-        print("timeout")
-    print(sep)
-
-    # When timeout passes and task is cancelled, but task ignores the cancellation request
-    try:
-        print(await asyncio.wait_for(task_catch(), 0.1))
-    except asyncio.TimeoutError:
-        print("TimeoutError")
-    print(sep)
-
-    # When task raises an exception
-    try:
-        print(await asyncio.wait_for(task_raise(), 1))
-    except ValueError:
-        print("ValueError")
-    print(sep)
-
-    # Timeout of None means wait forever
-    print(await asyncio.wait_for(task(3, 0.1), None))
-    print(sep)
-
-    # When task is cancelled by another task
-    t = asyncio.create_task(task(4, 10))
-    asyncio.create_task(task_cancel_other(0.01, t))
-    try:
-        print(await asyncio.wait_for(t, 1))
-    except asyncio.CancelledError as er:
-        print(repr(er))
-    print(sep)
-
-    # When wait_for gets cancelled
-    t = asyncio.create_task(task_wait_for_cancel(4, 1, 2))
-    await asyncio.sleep(0.01)
-    t.cancel()
-    await asyncio.sleep(0.01)
-    print(sep)
-
-    # When wait_for gets cancelled and awaited task ignores the cancellation request
-    t = asyncio.create_task(task_wait_for_cancel_ignore(2))
-    await asyncio.sleep(0.01)
-    t.cancel()
-    await asyncio.sleep(0.01)
-    print(sep)
-
-    # When wait_for gets cancelled and the task it's waiting on finishes around the
-    # same time as the cancellation of the wait_for
-    for num_sleep in range(1, 5):
-        t = asyncio.create_task(task_wait_for_cancel(4 + num_sleep, 0, 2))
-        for _ in range(num_sleep):
-            await asyncio.sleep(0)
-        assert not t.done()
-        print("cancel wait_for")
-        t.cancel()
-        try:
-            await t
-        except asyncio.CancelledError as er:
-            print(repr(er))
-        print(sep)
-
-    print("finish")
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_wait_for_fwd.py b/tests/extmod/uasyncio_wait_for_fwd.py
deleted file mode 100644
index 33738085ce4..00000000000
--- a/tests/extmod/uasyncio_wait_for_fwd.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# Test asyncio.wait_for, with forwarding cancellation
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def awaiting(t, return_if_fail):
-    try:
-        print("awaiting started")
-        await asyncio.sleep(t)
-    except asyncio.CancelledError as er:
-        # CPython wait_for raises CancelledError inside task but TimeoutError in wait_for
-        print("awaiting canceled")
-        if return_if_fail:
-            return False  # return has no effect if Cancelled
-        else:
-            raise er
-    except Exception as er:
-        print("caught exception", er)
-        raise er
-
-
-async def test_cancellation_forwarded(catch, catch_inside):
-    print("----------")
-
-    async def wait():
-        try:
-            await asyncio.wait_for(awaiting(2, catch_inside), 1)
-        except asyncio.TimeoutError as er:
-            print("Got timeout error")
-            raise er
-        except asyncio.CancelledError as er:
-            print("Got canceled")
-            if not catch:
-                raise er
-
-    async def cancel(t):
-        print("cancel started")
-        await asyncio.sleep(0.01)
-        print("cancel wait()")
-        t.cancel()
-
-    t = asyncio.create_task(wait())
-    k = asyncio.create_task(cancel(t))
-    try:
-        await t
-    except asyncio.CancelledError:
-        print("waiting got cancelled")
-
-
-asyncio.run(test_cancellation_forwarded(False, False))
-asyncio.run(test_cancellation_forwarded(False, True))
-asyncio.run(test_cancellation_forwarded(True, True))
-asyncio.run(test_cancellation_forwarded(True, False))
diff --git a/tests/extmod/uasyncio_wait_task.py b/tests/extmod/uasyncio_wait_task.py
deleted file mode 100644
index e19e29903cc..00000000000
--- a/tests/extmod/uasyncio_wait_task.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# Test waiting on a task
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-try:
-    import utime
-
-    ticks = utime.ticks_ms
-    ticks_diff = utime.ticks_diff
-except:
-    import time
-
-    ticks = lambda: int(time.time() * 1000)
-    ticks_diff = lambda t1, t0: t1 - t0
-
-
-async def task(t):
-    print("task", t)
-
-
-async def delay_print(t, s):
-    await asyncio.sleep(t)
-    print(s)
-
-
-async def task_raise():
-    print("task_raise")
-    raise ValueError
-
-
-async def main():
-    print("start")
-
-    # Wait on a task
-    t = asyncio.create_task(task(1))
-    await t
-
-    # Wait on a task that's already done
-    t = asyncio.create_task(task(2))
-    await asyncio.sleep(0.001)
-    await t
-
-    # Wait again on same task
-    await t
-
-    print("----")
-
-    # Create 2 tasks
-    ts1 = asyncio.create_task(delay_print(0.2, "hello"))
-    ts2 = asyncio.create_task(delay_print(0.4, "world"))
-
-    # Time how long the tasks take to finish, they should execute in parallel
-    print("start")
-    t0 = ticks()
-    await ts1
-    t1 = ticks()
-    await ts2
-    t2 = ticks()
-    print("took {} {}".format(round(ticks_diff(t1, t0), -2), round(ticks_diff(t2, t1), -2)))
-
-    # Wait on a task that raises an exception
-    t = asyncio.create_task(task_raise())
-    try:
-        await t
-    except ValueError:
-        print("ValueError")
-
-
-asyncio.run(main())
diff --git a/tests/extmod/uasyncio_wait_task.py.exp b/tests/extmod/uasyncio_wait_task.py.exp
deleted file mode 100644
index 04be37f4840..00000000000
--- a/tests/extmod/uasyncio_wait_task.py.exp
+++ /dev/null
@@ -1,10 +0,0 @@
-start
-task 1
-task 2
-----
-start
-hello
-world
-took 200 200
-task_raise
-ValueError
diff --git a/tests/extmod/ubinascii_a2b_base64.py b/tests/extmod/ubinascii_a2b_base64.py
deleted file mode 100644
index 2630965e6a0..00000000000
--- a/tests/extmod/ubinascii_a2b_base64.py
+++ /dev/null
@@ -1,46 +0,0 @@
-try:
-    try:
-        import ubinascii as binascii
-    except ImportError:
-        import binascii
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-print(binascii.a2b_base64(b""))
-print(binascii.a2b_base64(b"Zg=="))
-print(binascii.a2b_base64(b"Zm8="))
-print(binascii.a2b_base64(b"Zm9v"))
-print(binascii.a2b_base64(b"Zm9vYg=="))
-print(binascii.a2b_base64(b"Zm9vYmE="))
-print(binascii.a2b_base64(b"Zm9vYmFy"))
-
-print(binascii.a2b_base64(b"AAECAwQFBgc="))
-print(binascii.a2b_base64(b"CAkKCwwNDg8="))
-print(binascii.a2b_base64(b"f4D/"))
-print(binascii.a2b_base64(b"f4D+"))  # convert '+'
-print(binascii.a2b_base64(b"MTIzNEFCQ0RhYmNk"))
-
-# Ignore invalid characters and pad sequences
-print(binascii.a2b_base64(b"Zm9v\n"))
-print(binascii.a2b_base64(b"Zm\x009v\n"))
-print(binascii.a2b_base64(b"Zm9v=="))
-print(binascii.a2b_base64(b"Zm9v==="))
-print(binascii.a2b_base64(b"Zm9v===YmFy"))
-
-try:
-    print(binascii.a2b_base64(b"abc"))
-except ValueError:
-    print("ValueError")
-try:
-    print(binascii.a2b_base64(b"abcde="))
-except ValueError:
-    print("ValueError")
-try:
-    print(binascii.a2b_base64(b"ab*d"))
-except ValueError:
-    print("ValueError")
-try:
-    print(binascii.a2b_base64(b"ab=cdef="))
-except ValueError:
-    print("ValueError")
diff --git a/tests/extmod/ubinascii_b2a_base64.py b/tests/extmod/ubinascii_b2a_base64.py
deleted file mode 100644
index 3f92488969e..00000000000
--- a/tests/extmod/ubinascii_b2a_base64.py
+++ /dev/null
@@ -1,25 +0,0 @@
-try:
-    try:
-        import ubinascii as binascii
-    except ImportError:
-        import binascii
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-print(binascii.b2a_base64(b""))
-print(binascii.b2a_base64(b"f"))
-print(binascii.b2a_base64(b"fo"))
-print(binascii.b2a_base64(b"foo"))
-print(binascii.b2a_base64(b"foob"))
-print(binascii.b2a_base64(b"fooba"))
-print(binascii.b2a_base64(b"foobar"))
-
-print(binascii.b2a_base64(b"\x00\x01\x02\x03\x04\x05\x06\x07"))
-print(binascii.b2a_base64(b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"))
-print(binascii.b2a_base64(b"\x7f\x80\xff"))
-print(binascii.b2a_base64(b"1234ABCDabcd"))
-print(binascii.b2a_base64(b"\x00\x00>"))  # convert into '+'
-
-print(binascii.b2a_base64(b"foobar", newline=True))
-print(binascii.b2a_base64(b"foobar", newline=False))
diff --git a/tests/extmod/ubinascii_crc32.py b/tests/extmod/ubinascii_crc32.py
deleted file mode 100644
index 8f5f4d9ba5e..00000000000
--- a/tests/extmod/ubinascii_crc32.py
+++ /dev/null
@@ -1,24 +0,0 @@
-try:
-    try:
-        import ubinascii as binascii
-    except ImportError:
-        import binascii
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-try:
-    binascii.crc32
-except AttributeError:
-    print("SKIP")
-    raise SystemExit
-
-print(hex(binascii.crc32(b"The quick brown fox jumps over the lazy dog")))
-print(hex(binascii.crc32(b"\x00" * 32)))
-print(hex(binascii.crc32(b"\xff" * 32)))
-print(hex(binascii.crc32(bytes(range(32)))))
-
-print(hex(binascii.crc32(b" over the lazy dog", binascii.crc32(b"The quick brown fox jumps"))))
-print(hex(binascii.crc32(b"\x00" * 16, binascii.crc32(b"\x00" * 16))))
-print(hex(binascii.crc32(b"\xff" * 16, binascii.crc32(b"\xff" * 16))))
-print(hex(binascii.crc32(bytes(range(16, 32)), binascii.crc32(bytes(range(16))))))
diff --git a/tests/extmod/ubinascii_hexlify.py b/tests/extmod/ubinascii_hexlify.py
deleted file mode 100644
index 3c266fb6cc0..00000000000
--- a/tests/extmod/ubinascii_hexlify.py
+++ /dev/null
@@ -1,22 +0,0 @@
-try:
-    try:
-        import ubinascii as binascii
-    except ImportError:
-        import binascii
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-for x in (
-    b"\x00\x01\x02\x03\x04\x05\x06\x07",
-    b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
-    b"\x7f\x80\xff",
-    b"1234ABCDabcd",
-):
-    print(binascii.hexlify(x))
-
-# Two-argument version (now supported in CPython)
-print(binascii.hexlify(b"123", ":"))
-
-# zero length buffer
-print(binascii.hexlify(b"", b":"))
diff --git a/tests/extmod/ubinascii_unhexlify.py b/tests/extmod/ubinascii_unhexlify.py
deleted file mode 100644
index 2c3598ab98e..00000000000
--- a/tests/extmod/ubinascii_unhexlify.py
+++ /dev/null
@@ -1,26 +0,0 @@
-try:
-    try:
-        import ubinascii as binascii
-    except ImportError:
-        import binascii
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-for x in (
-    b"0001020304050607",
-    b"08090a0b0c0d0e0f",
-    b"7f80ff",
-    b"313233344142434461626364",
-):
-    print(binascii.unhexlify(x))
-
-try:
-    a = binascii.unhexlify(b"0")  # odd buffer length
-except ValueError:
-    print("ValueError")
-
-try:
-    a = binascii.unhexlify(b"gg")  # digit not hex
-except ValueError:
-    print("ValueError")
diff --git a/tests/extmod/ucryptolib_aes128_cbc.py b/tests/extmod/ucryptolib_aes128_cbc.py
deleted file mode 100644
index d861d2c6bf4..00000000000
--- a/tests/extmod/ucryptolib_aes128_cbc.py
+++ /dev/null
@@ -1,16 +0,0 @@
-try:
-    from Crypto.Cipher import AES
-
-    aes = AES.new
-except ImportError:
-    try:
-        from ucryptolib import aes
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-crypto = aes(b"1234" * 4, 2, b"5678" * 4)
-enc = crypto.encrypt(bytes(range(32)))
-print(enc)
-crypto = aes(b"1234" * 4, 2, b"5678" * 4)
-print(crypto.decrypt(enc))
diff --git a/tests/extmod/ucryptolib_aes128_ctr.py b/tests/extmod/ucryptolib_aes128_ctr.py
deleted file mode 100644
index 538d9606e9e..00000000000
--- a/tests/extmod/ucryptolib_aes128_ctr.py
+++ /dev/null
@@ -1,28 +0,0 @@
-try:
-    from ucryptolib import aes
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-
-def _new(k, ctr_initial):
-    return aes(k, 6, ctr_initial)
-
-
-try:
-    _new(b"x" * 16, b"x" * 16)
-except ValueError as e:
-    # is CTR support disabled?
-    if e.args[0] == "mode":
-        print("SKIP")
-        raise SystemExit
-    raise e
-
-crypto = _new(b"1234" * 4, b"5678" * 4)
-enc = crypto.encrypt(b"a")
-print(enc)
-enc += crypto.encrypt(b"b" * 1000)
-print(enc)
-
-crypto = _new(b"1234" * 4, b"5678" * 4)
-print(crypto.decrypt(enc))
diff --git a/tests/extmod/ucryptolib_aes128_ecb.py b/tests/extmod/ucryptolib_aes128_ecb.py
deleted file mode 100644
index 5c0e1799868..00000000000
--- a/tests/extmod/ucryptolib_aes128_ecb.py
+++ /dev/null
@@ -1,16 +0,0 @@
-try:
-    from Crypto.Cipher import AES
-
-    aes = AES.new
-except ImportError:
-    try:
-        from ucryptolib import aes
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-crypto = aes(b"1234" * 4, 1)
-enc = crypto.encrypt(bytes(range(32)))
-print(enc)
-crypto = aes(b"1234" * 4, 1)
-print(crypto.decrypt(enc))
diff --git a/tests/extmod/ucryptolib_aes128_ecb_enc.py b/tests/extmod/ucryptolib_aes128_ecb_enc.py
deleted file mode 100644
index 1d4484b0bc1..00000000000
--- a/tests/extmod/ucryptolib_aes128_ecb_enc.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# This tests minimal configuration of ucrypto module, which is
-# AES128 encryption (anything else, including AES128 decryption,
-# is optional).
-try:
-    from Crypto.Cipher import AES
-
-    aes = AES.new
-except ImportError:
-    try:
-        from ucryptolib import aes
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-crypto = aes(b"1234" * 4, 1)
-enc = crypto.encrypt(bytes(range(32)))
-print(enc)
diff --git a/tests/extmod/ucryptolib_aes128_ecb_inpl.py b/tests/extmod/ucryptolib_aes128_ecb_inpl.py
deleted file mode 100644
index 88ccb02daf2..00000000000
--- a/tests/extmod/ucryptolib_aes128_ecb_inpl.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# Inplace operations (input and output buffer is the same)
-try:
-    from ucryptolib import aes
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-crypto = aes(b"1234" * 4, 1)
-buf = bytearray(bytes(range(32)))
-crypto.encrypt(buf, buf)
-print(buf)
-
-crypto = aes(b"1234" * 4, 1)
-crypto.decrypt(buf, buf)
-print(buf)
diff --git a/tests/extmod/ucryptolib_aes128_ecb_into.py b/tests/extmod/ucryptolib_aes128_ecb_into.py
deleted file mode 100644
index ff832d7ef3b..00000000000
--- a/tests/extmod/ucryptolib_aes128_ecb_into.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Operations with pre-allocated output buffer
-try:
-    from ucryptolib import aes
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-crypto = aes(b"1234" * 4, 1)
-enc = bytearray(32)
-crypto.encrypt(bytes(range(32)), enc)
-print(enc)
-
-crypto = aes(b"1234" * 4, 1)
-dec = bytearray(32)
-crypto.decrypt(enc, dec)
-print(dec)
diff --git a/tests/extmod/ucryptolib_aes256_cbc.py b/tests/extmod/ucryptolib_aes256_cbc.py
deleted file mode 100644
index c01846f199d..00000000000
--- a/tests/extmod/ucryptolib_aes256_cbc.py
+++ /dev/null
@@ -1,16 +0,0 @@
-try:
-    from Crypto.Cipher import AES
-
-    aes = AES.new
-except ImportError:
-    try:
-        from ucryptolib import aes
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-crypto = aes(b"1234" * 8, 2, b"5678" * 4)
-enc = crypto.encrypt(bytes(range(32)))
-print(enc)
-crypto = aes(b"1234" * 8, 2, b"5678" * 4)
-print(crypto.decrypt(enc))
diff --git a/tests/extmod/ucryptolib_aes256_ecb.py b/tests/extmod/ucryptolib_aes256_ecb.py
deleted file mode 100644
index 0760063c14c..00000000000
--- a/tests/extmod/ucryptolib_aes256_ecb.py
+++ /dev/null
@@ -1,16 +0,0 @@
-try:
-    from Crypto.Cipher import AES
-
-    aes = AES.new
-except ImportError:
-    try:
-        from ucryptolib import aes
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-crypto = aes(b"1234" * 8, 1)
-enc = crypto.encrypt(bytes(range(32)))
-print(enc)
-crypto = aes(b"1234" * 8, 1)
-print(crypto.decrypt(enc))
diff --git a/tests/extmod/uctypes_addressof.py b/tests/extmod/uctypes_addressof.py
new file mode 100644
index 00000000000..213fcc05eee
--- /dev/null
+++ b/tests/extmod/uctypes_addressof.py
@@ -0,0 +1,19 @@
+# Test uctypes.addressof().
+
+try:
+    from sys import maxsize
+    import uctypes
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Test small addresses.
+for i in range(8):
+    print(uctypes.addressof(uctypes.bytearray_at(1 << i, 8)))
+
+# Test address that is bigger than the greatest small-int but still within the address range.
+try:
+    large_addr = maxsize + 1
+    print(uctypes.addressof(uctypes.bytearray_at(large_addr, 8)) == large_addr)
+except OverflowError:
+    print(True)  # systems with 64-bit bigints will overflow on the above operation
diff --git a/tests/extmod/uctypes_addressof.py.exp b/tests/extmod/uctypes_addressof.py.exp
new file mode 100644
index 00000000000..5b48569d018
--- /dev/null
+++ b/tests/extmod/uctypes_addressof.py.exp
@@ -0,0 +1,9 @@
+1
+2
+4
+8
+16
+32
+64
+128
+True
diff --git a/tests/extmod/uctypes_array_assign_le.py b/tests/extmod/uctypes_array_assign_le.py
index d822faf7e8a..b7fcdfb49c0 100644
--- a/tests/extmod/uctypes_array_assign_le.py
+++ b/tests/extmod/uctypes_array_assign_le.py
@@ -18,7 +18,7 @@
     "arr8": (uctypes.ARRAY | 1, 1, {"l": uctypes.UINT32 | 0}),
 }
 
-data = bytearray(5)
+data = bytearray(6)
 
 S = uctypes.struct(uctypes.addressof(data), desc, uctypes.LITTLE_ENDIAN)
 
diff --git a/tests/extmod/uctypes_array_assign_native_le.py b/tests/extmod/uctypes_array_assign_native_le.py
index 5bddfcdf2f6..d4c27fc4b3f 100644
--- a/tests/extmod/uctypes_array_assign_native_le.py
+++ b/tests/extmod/uctypes_array_assign_native_le.py
@@ -1,4 +1,4 @@
-import usys
+import sys
 
 try:
     import uctypes
@@ -6,7 +6,7 @@
     print("SKIP")
     raise SystemExit
 
-if usys.byteorder != "little":
+if sys.byteorder != "little":
     print("SKIP")
     raise SystemExit
 
diff --git a/tests/extmod/uctypes_array_assign_native_le_intbig.py b/tests/extmod/uctypes_array_assign_native_le_intbig.py
index 42583b8afef..f33c63b4ef9 100644
--- a/tests/extmod/uctypes_array_assign_native_le_intbig.py
+++ b/tests/extmod/uctypes_array_assign_native_le_intbig.py
@@ -1,4 +1,4 @@
-import usys
+import sys
 
 try:
     import uctypes
@@ -6,7 +6,7 @@
     print("SKIP")
     raise SystemExit
 
-if usys.byteorder != "little":
+if sys.byteorder != "little":
     print("SKIP")
     raise SystemExit
 
diff --git a/tests/extmod/uctypes_array_load_store.py b/tests/extmod/uctypes_array_load_store.py
index 3b9bb6d7308..3de586e79ea 100644
--- a/tests/extmod/uctypes_array_load_store.py
+++ b/tests/extmod/uctypes_array_load_store.py
@@ -1,4 +1,5 @@
 # Test uctypes array, load and store, with array size > 1
+import sys
 
 try:
     import uctypes
@@ -6,14 +7,88 @@
     print("SKIP")
     raise SystemExit
 
+# 'int' needs to be able to represent UINT64 for this test
+try:
+    int("FF" * 8, 16)
+except OverflowError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    import unittest
+except MemoryError:
+    print("SKIP-TOO-LARGE")  # some small boards can't fit unittest in RAM
+    raise SystemExit
+
+# MicroPython V2.0 will enforce bounds on bytearray setters, V1.x truncates
+is_v2 = hasattr(sys.implementation, "_v2")
+
 N = 5
+PLACEHOLDER = 99
+
+
+class Test(unittest.TestCase):
+    def test_native_endian(self):
+        self._test_endian("NATIVE")
 
-for endian in ("NATIVE", "LITTLE_ENDIAN", "BIG_ENDIAN"):
-    for type_ in ("INT8", "UINT8", "INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64"):
-        desc = {"arr": (uctypes.ARRAY | 0, getattr(uctypes, type_) | N)}
+    def test_little_endian(self):
+        self._test_endian("LITTLE_ENDIAN")
+
+    def test_big_endian(self):
+        self._test_endian("BIG_ENDIAN")
+
+    def _test_endian(self, endian):
+        for item_type in (
+            "INT8",
+            "UINT8",
+            "INT16",
+            "UINT16",
+            "INT32",
+            "UINT32",
+            "INT64",
+            "UINT64",
+        ):
+            print(endian, item_type)
+            self._test_endian_type(endian, item_type)
+
+    def _test_endian_type(self, endian, item_type):
+        print("Testing array of", item_type, "with", endian, "endianness")
+        desc = {"arr": (uctypes.ARRAY | 0, getattr(uctypes, item_type) | N)}
+        print(repr(desc))
         sz = uctypes.sizeof(desc)
         data = bytearray(sz)
+        print(sz, repr((uctypes.addressof(data), desc, getattr(uctypes, endian))))
         s = uctypes.struct(uctypes.addressof(data), desc, getattr(uctypes, endian))
+        is_unsigned = item_type.startswith("U")
+        item_sz = uctypes.sizeof({"": getattr(uctypes, item_type)})
+
+        for i in range(N):
+            n = i - 2
+            print(i, n)
+            # uctypes returns a bytearray for arrays of type UINT8, MicroPython V2 will
+            # enforce bounds checks on these so we can't assign a negative value
+            if is_v2 and isinstance(s.arr, bytearray) and n < 0:
+                print("placeholder value", n)
+                with self.assertRaises(OverflowError):
+                    s.arr[i] = n
+                s.arr[i] = PLACEHOLDER
+                n = PLACEHOLDER
+            else:
+                s.arr[i] = n
+
+        print(endian, item_type, sz, *(s.arr[i] for i in range(N)))
+
         for i in range(N):
-            s.arr[i] = i - 2
-        print(endian, type_, sz, *(s.arr[i] for i in range(N)))
+            n = i - 2
+            if is_v2 and isinstance(s.arr, bytearray) and n < 0:
+                # The code above has swapped in PLACEHOLDER for this value
+                n = PLACEHOLDER
+            elif is_unsigned and n < 0:
+                # other types of unsigned uctypes arrays will "cast" negative values to unsigned
+                n = n & ((1 << (item_sz * 8)) - 1)
+
+            self.assertEqual(s.arr[i], n)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/extmod/uctypes_error.py b/tests/extmod/uctypes_error.py
index d4295626158..f8aea94a7f1 100644
--- a/tests/extmod/uctypes_error.py
+++ b/tests/extmod/uctypes_error.py
@@ -8,6 +8,12 @@
 
 data = bytearray(b"01234567")
 
+# first argument isn't an integer
+try:
+    uctypes.struct(data, {})
+except TypeError:
+    print("TypeError")
+
 # del subscr not supported
 S = uctypes.struct(uctypes.addressof(data), {})
 try:
diff --git a/tests/extmod/uctypes_error.py.exp b/tests/extmod/uctypes_error.py.exp
index f2e9c12f7f9..9910ced6c6a 100644
--- a/tests/extmod/uctypes_error.py.exp
+++ b/tests/extmod/uctypes_error.py.exp
@@ -3,3 +3,4 @@ TypeError
 TypeError
 TypeError
 TypeError
+TypeError
diff --git a/tests/extmod/uctypes_native_le.py b/tests/extmod/uctypes_native_le.py
index 9889b98e9d0..7958e5c22ad 100644
--- a/tests/extmod/uctypes_native_le.py
+++ b/tests/extmod/uctypes_native_le.py
@@ -1,7 +1,7 @@
 # This test is exactly like uctypes_le.py, but uses native structure layout.
 # Codepaths for packed vs native structures are different. This test only works
 # on little-endian machine (no matter if 32 or 64 bit).
-import usys
+import sys
 
 try:
     import uctypes
@@ -9,7 +9,7 @@
     print("SKIP")
     raise SystemExit
 
-if usys.byteorder != "little":
+if sys.byteorder != "little":
     print("SKIP")
     raise SystemExit
 
diff --git a/tests/extmod/uctypes_ptr_le.py b/tests/extmod/uctypes_ptr_le.py
index f475465ae80..5d8094ee48a 100644
--- a/tests/extmod/uctypes_ptr_le.py
+++ b/tests/extmod/uctypes_ptr_le.py
@@ -1,4 +1,4 @@
-import usys
+import sys
 
 try:
     import uctypes
@@ -6,7 +6,7 @@
     print("SKIP")
     raise SystemExit
 
-if usys.byteorder != "little":
+if sys.byteorder != "little":
     print("SKIP")
     raise SystemExit
 
diff --git a/tests/extmod/uctypes_ptr_native_le.py b/tests/extmod/uctypes_ptr_native_le.py
index ca2b316c54a..8ca4d2c55cf 100644
--- a/tests/extmod/uctypes_ptr_native_le.py
+++ b/tests/extmod/uctypes_ptr_native_le.py
@@ -1,4 +1,4 @@
-import usys
+import sys
 
 try:
     import uctypes
@@ -6,7 +6,7 @@
     print("SKIP")
     raise SystemExit
 
-if usys.byteorder != "little":
+if sys.byteorder != "little":
     print("SKIP")
     raise SystemExit
 
diff --git a/tests/extmod/uctypes_sizeof.py b/tests/extmod/uctypes_sizeof.py
index 6e52232e39e..d295cc85b68 100644
--- a/tests/extmod/uctypes_sizeof.py
+++ b/tests/extmod/uctypes_sizeof.py
@@ -43,8 +43,47 @@
 print(uctypes.sizeof(S.sub))
 assert uctypes.sizeof(S.sub) == 1
 
-# invalid descriptor
+# invalid descriptors
 try:
     print(uctypes.sizeof([]))
 except TypeError:
     print("TypeError")
+
+try:
+    print(uctypes.sizeof(()))
+except TypeError:
+    print("TypeError")
+
+try:
+    print(uctypes.sizeof(("garbage",)))
+except TypeError:
+    print("TypeError")
+
+try:
+    # PTR * 3 is intended to be an invalid agg_type (STRUCT, PTR, ARRAY are valid ones).
+    print(uctypes.sizeof((uctypes.PTR * 3,)))
+except TypeError:
+    print("TypeError")
+
+try:
+    print(uctypes.sizeof((0, {}, "garbage")))
+except TypeError:
+    print("TypeError")
+
+try:
+    print(uctypes.sizeof((uctypes.PTR | 0,)))
+except TypeError:
+    print("TypeError")
+
+try:
+    print(uctypes.sizeof((uctypes.ARRAY | 0,)))
+except TypeError:
+    print("TypeError")
+
+try:
+    print(uctypes.sizeof((uctypes.ARRAY | 0, 1, {}, "garbage")))
+except TypeError:
+    print("TypeError")
+
+# empty descriptor
+print(uctypes.sizeof({}))
diff --git a/tests/extmod/uctypes_sizeof.py.exp b/tests/extmod/uctypes_sizeof.py.exp
index b35b11aa0ce..edcc05a441f 100644
--- a/tests/extmod/uctypes_sizeof.py.exp
+++ b/tests/extmod/uctypes_sizeof.py.exp
@@ -5,3 +5,11 @@ TypeError
 6
 1
 TypeError
+TypeError
+TypeError
+TypeError
+TypeError
+TypeError
+TypeError
+TypeError
+0
diff --git a/tests/extmod/uctypes_sizeof_od.py b/tests/extmod/uctypes_sizeof_od.py
index 2f070095b55..8aff3636315 100644
--- a/tests/extmod/uctypes_sizeof_od.py
+++ b/tests/extmod/uctypes_sizeof_od.py
@@ -1,5 +1,5 @@
 try:
-    from ucollections import OrderedDict
+    from collections import OrderedDict
     import uctypes
 except ImportError:
     print("SKIP")
@@ -45,9 +45,3 @@
 
 print(uctypes.sizeof(S.sub))
 assert uctypes.sizeof(S.sub) == 1
-
-# invalid descriptor
-try:
-    print(uctypes.sizeof([]))
-except TypeError:
-    print("TypeError")
diff --git a/tests/extmod/uctypes_sizeof_od.py.exp b/tests/extmod/uctypes_sizeof_od.py.exp
index b35b11aa0ce..fb74def602b 100644
--- a/tests/extmod/uctypes_sizeof_od.py.exp
+++ b/tests/extmod/uctypes_sizeof_od.py.exp
@@ -4,4 +4,3 @@
 TypeError
 6
 1
-TypeError
diff --git a/tests/extmod/uhashlib_final.py b/tests/extmod/uhashlib_final.py
deleted file mode 100644
index f562cc17807..00000000000
--- a/tests/extmod/uhashlib_final.py
+++ /dev/null
@@ -1,35 +0,0 @@
-try:
-    import uhashlib
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-
-for algo_name in ("md5", "sha1", "sha256"):
-    algo = getattr(uhashlib, algo_name, None)
-    if not algo:
-        continue
-
-    # Running .digest() several times in row is not supported.
-    h = algo(b"123")
-    h.digest()
-    try:
-        h.digest()
-        print("fail")
-    except ValueError:
-        # Expected path, don't print anything so test output is the
-        # same even if the algorithm is not implemented on the port.
-        pass
-
-    # Partial digests are not supported.
-    h = algo(b"123")
-    h.digest()
-    try:
-        h.update(b"456")
-        print("fail")
-    except ValueError:
-        # Expected path, don't print anything so test output is the
-        # same even if the algorithm is not implemented on the port.
-        pass
-
-print("done")
diff --git a/tests/extmod/uhashlib_md5.py b/tests/extmod/uhashlib_md5.py
deleted file mode 100644
index 07d5f316920..00000000000
--- a/tests/extmod/uhashlib_md5.py
+++ /dev/null
@@ -1,21 +0,0 @@
-try:
-    import uhashlib as hashlib
-except ImportError:
-    try:
-        import hashlib
-    except ImportError:
-        # This is neither uPy, nor cPy, so must be uPy with
-        # uhashlib module disabled.
-        print("SKIP")
-        raise SystemExit
-
-try:
-    hashlib.md5
-except AttributeError:
-    # MD5 is only available on some ports
-    print("SKIP")
-    raise SystemExit
-
-md5 = hashlib.md5(b"hello")
-md5.update(b"world")
-print(md5.digest())
diff --git a/tests/extmod/uhashlib_sha1.py b/tests/extmod/uhashlib_sha1.py
deleted file mode 100644
index a5731213165..00000000000
--- a/tests/extmod/uhashlib_sha1.py
+++ /dev/null
@@ -1,21 +0,0 @@
-try:
-    import uhashlib as hashlib
-except ImportError:
-    try:
-        import hashlib
-    except ImportError:
-        # This is neither uPy, nor cPy, so must be uPy with
-        # uhashlib module disabled.
-        print("SKIP")
-        raise SystemExit
-
-try:
-    hashlib.sha1
-except AttributeError:
-    # SHA1 is only available on some ports
-    print("SKIP")
-    raise SystemExit
-
-sha1 = hashlib.sha1(b"hello")
-sha1.update(b"world")
-print(sha1.digest())
diff --git a/tests/extmod/uhashlib_sha256.py b/tests/extmod/uhashlib_sha256.py
deleted file mode 100644
index 2e6df7df132..00000000000
--- a/tests/extmod/uhashlib_sha256.py
+++ /dev/null
@@ -1,38 +0,0 @@
-try:
-    import uhashlib as hashlib
-except ImportError:
-    try:
-        import hashlib
-    except ImportError:
-        # This is neither uPy, nor cPy, so must be uPy with
-        # uhashlib module disabled.
-        print("SKIP")
-        raise SystemExit
-
-
-h = hashlib.sha256()
-print(h.digest())
-
-h = hashlib.sha256()
-h.update(b"123")
-print(h.digest())
-
-h = hashlib.sha256()
-h.update(b"abcd" * 1000)
-print(h.digest())
-
-print(hashlib.sha256(b"\xff" * 64).digest())
-
-# 56 bytes is a boundary case in the algorithm
-print(hashlib.sha256(b"\xff" * 56).digest())
-
-# TODO: running .digest() several times in row is not supported()
-# h = hashlib.sha256(b'123')
-# print(h.digest())
-# print(h.digest())
-
-# TODO: partial digests are not supported
-# h = hashlib.sha256(b'123')
-# print(h.digest())
-# h.update(b'456')
-# print(h.digest())
diff --git a/tests/extmod/uheapq1.py b/tests/extmod/uheapq1.py
deleted file mode 100644
index a470bb6f71b..00000000000
--- a/tests/extmod/uheapq1.py
+++ /dev/null
@@ -1,42 +0,0 @@
-try:
-    import uheapq as heapq
-except:
-    try:
-        import heapq
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-try:
-    heapq.heappop([])
-except IndexError:
-    print("IndexError")
-
-try:
-    heapq.heappush((), 1)
-except TypeError:
-    print("TypeError")
-
-
-def pop_and_print(h):
-    l = []
-    while h:
-        l.append(str(heapq.heappop(h)))
-    print(" ".join(l))
-
-
-h = []
-heapq.heappush(h, 3)
-heapq.heappush(h, 1)
-heapq.heappush(h, 2)
-print(h)
-pop_and_print(h)
-
-h = [4, 3, 8, 9, 10, 2, 7, 11, 5]
-heapq.heapify(h)
-print(h)
-heapq.heappush(h, 1)
-heapq.heappush(h, 6)
-heapq.heappush(h, 12)
-print(h)
-pop_and_print(h)
diff --git a/tests/extmod/ujson_dump.py b/tests/extmod/ujson_dump.py
deleted file mode 100644
index feda8a47dda..00000000000
--- a/tests/extmod/ujson_dump.py
+++ /dev/null
@@ -1,30 +0,0 @@
-try:
-    from uio import StringIO
-    import ujson as json
-except:
-    try:
-        from io import StringIO
-        import json
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-s = StringIO()
-json.dump(False, s)
-print(s.getvalue())
-
-s = StringIO()
-json.dump({"a": (2, [3, None])}, s)
-print(s.getvalue())
-
-# dump to a small-int not allowed
-try:
-    json.dump(123, 1)
-except (AttributeError, OSError):  # CPython and uPy have different errors
-    print("Exception")
-
-# dump to an object not allowed
-try:
-    json.dump(123, {})
-except (AttributeError, OSError):  # CPython and uPy have different errors
-    print("Exception")
diff --git a/tests/extmod/ujson_dump_iobase.py b/tests/extmod/ujson_dump_iobase.py
deleted file mode 100644
index 7ecf23afb65..00000000000
--- a/tests/extmod/ujson_dump_iobase.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# test ujson.dump in combination with uio.IOBase
-
-try:
-    import uio as io
-    import ujson as json
-except ImportError:
-    try:
-        import io, json
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-if not hasattr(io, "IOBase"):
-    print("SKIP")
-    raise SystemExit
-
-
-# a user stream that only has the write method
-class S(io.IOBase):
-    def __init__(self):
-        self.buf = ""
-
-    def write(self, buf):
-        if type(buf) == bytearray:
-            # uPy passes a bytearray, CPython passes a str
-            buf = str(buf, "ascii")
-        self.buf += buf
-        return len(buf)
-
-
-# dump to the user stream
-s = S()
-json.dump([123, {}], s)
-print(s.buf)
diff --git a/tests/extmod/ujson_dump_separators.py b/tests/extmod/ujson_dump_separators.py
deleted file mode 100644
index e19b99a26d7..00000000000
--- a/tests/extmod/ujson_dump_separators.py
+++ /dev/null
@@ -1,62 +0,0 @@
-try:
-    from uio import StringIO
-    import ujson as json
-except:
-    try:
-        from io import StringIO
-        import json
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-for sep in [
-    None,
-    (", ", ": "),
-    (",", ": "),
-    (",", ":"),
-    [", ", ": "],
-    [",", ": "],
-    [",", ":"],
-]:
-    s = StringIO()
-    json.dump(False, s, separators=sep)
-    print(s.getvalue())
-
-    s = StringIO()
-    json.dump({"a": (2, [3, None])}, s, separators=sep)
-    print(s.getvalue())
-
-    # dump to a small-int not allowed
-    try:
-        json.dump(123, 1, separators=sep)
-    except (AttributeError, OSError):  # CPython and uPy have different errors
-        print("Exception")
-
-    # dump to an object not allowed
-    try:
-        json.dump(123, {}, separators=sep)
-    except (AttributeError, OSError):  # CPython and uPy have different errors
-        print("Exception")
-
-
-try:
-    s = StringIO()
-    json.dump(False, s, separators={"a": 1})
-except (TypeError, ValueError):  # CPython and uPy have different errors
-    print("Exception")
-
-# invalid separator types
-for sep in [1, object()]:
-    try:
-        s = StringIO()
-        json.dump(False, s, separators=sep)
-    except TypeError:
-        print("Exception")
-
-# too many/ not enough separators
-for sep in [(), (",", ":", "?"), (",",), []]:
-    try:
-        s = StringIO()
-        json.dump(False, s, separators=sep)
-    except ValueError:
-        print("Exception")
diff --git a/tests/extmod/ujson_dumps.py b/tests/extmod/ujson_dumps.py
deleted file mode 100644
index 251b2687557..00000000000
--- a/tests/extmod/ujson_dumps.py
+++ /dev/null
@@ -1,32 +0,0 @@
-try:
-    import ujson as json
-except ImportError:
-    try:
-        import json
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-print(json.dumps(False))
-print(json.dumps(True))
-print(json.dumps(None))
-print(json.dumps(1))
-print(json.dumps("abc"))
-print(json.dumps("\x00\x01\x7e"))
-print(json.dumps([]))
-print(json.dumps([1]))
-print(json.dumps([1, 2]))
-print(json.dumps([1, True]))
-print(json.dumps(()))
-print(json.dumps((1,)))
-print(json.dumps((1, 2)))
-print(json.dumps((1, (2, 3))))
-print(json.dumps({}))
-print(json.dumps({"a": 1}))
-print(json.dumps({"a": (2, [3, None])}))
-print(json.dumps('"quoted"'))
-print(json.dumps("space\n\r\tspace"))
-print(json.dumps({None: -1}))
-print(json.dumps({False: 0}))
-print(json.dumps({True: 1}))
-print(json.dumps({1: 2}))
diff --git a/tests/extmod/ujson_dumps_extra.py b/tests/extmod/ujson_dumps_extra.py
deleted file mode 100644
index f2aa7f249fc..00000000000
--- a/tests/extmod/ujson_dumps_extra.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# test uPy ujson behaviour that's not valid in CPy
-
-try:
-    import ujson
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-print(ujson.dumps(b"1234"))
diff --git a/tests/extmod/ujson_dumps_float.py b/tests/extmod/ujson_dumps_float.py
deleted file mode 100644
index 25681d0c23c..00000000000
--- a/tests/extmod/ujson_dumps_float.py
+++ /dev/null
@@ -1,11 +0,0 @@
-try:
-    import ujson as json
-except ImportError:
-    try:
-        import json
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-print(json.dumps(1.2))
-print(json.dumps({1.5: "hi"}))
diff --git a/tests/extmod/ujson_dumps_ordereddict.py b/tests/extmod/ujson_dumps_ordereddict.py
deleted file mode 100644
index c6f4a8fcb79..00000000000
--- a/tests/extmod/ujson_dumps_ordereddict.py
+++ /dev/null
@@ -1,12 +0,0 @@
-try:
-    import ujson as json
-    from ucollections import OrderedDict
-except ImportError:
-    try:
-        import json
-        from collections import OrderedDict
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-print(json.dumps(OrderedDict(((1, 2), (3, 4)))))
diff --git a/tests/extmod/ujson_dumps_separators.py b/tests/extmod/ujson_dumps_separators.py
deleted file mode 100644
index efb541fe8bf..00000000000
--- a/tests/extmod/ujson_dumps_separators.py
+++ /dev/null
@@ -1,60 +0,0 @@
-try:
-    import ujson as json
-except ImportError:
-    try:
-        import json
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-for sep in [
-    None,
-    (", ", ": "),
-    (",", ": "),
-    (",", ":"),
-    [", ", ": "],
-    [",", ": "],
-    [",", ":"],
-]:
-    print(json.dumps(False, separators=sep))
-    print(json.dumps(True, separators=sep))
-    print(json.dumps(None, separators=sep))
-    print(json.dumps(1, separators=sep))
-    print(json.dumps("abc", separators=sep))
-    print(json.dumps("\x00\x01\x7e", separators=sep))
-    print(json.dumps([], separators=sep))
-    print(json.dumps([1], separators=sep))
-    print(json.dumps([1, 2], separators=sep))
-    print(json.dumps([1, True], separators=sep))
-    print(json.dumps((), separators=sep))
-    print(json.dumps((1,), separators=sep))
-    print(json.dumps((1, 2), separators=sep))
-    print(json.dumps((1, (2, 3)), separators=sep))
-    print(json.dumps({}, separators=sep))
-    print(json.dumps({"a": 1}, separators=sep))
-    print(json.dumps({"a": (2, [3, None])}, separators=sep))
-    print(json.dumps('"quoted"', separators=sep))
-    print(json.dumps("space\n\r\tspace", separators=sep))
-    print(json.dumps({None: -1}, separators=sep))
-    print(json.dumps({False: 0}, separators=sep))
-    print(json.dumps({True: 1}, separators=sep))
-    print(json.dumps({1: 2}, separators=sep))
-
-try:
-    json.dumps(False, separators={"a": 1})
-except (TypeError, ValueError):  # CPython and uPy have different errors
-    print("Exception")
-
-# invalid separator types
-for sep in [1, object()]:
-    try:
-        json.dumps(False, separators=sep)
-    except TypeError:
-        print("Exception")
-
-# too many/ not enough separators
-for sep in [(), (",", ":", "?"), (",",), []]:
-    try:
-        json.dumps(False, separators=sep)
-    except ValueError:
-        print("Exception")
diff --git a/tests/extmod/ujson_load.py b/tests/extmod/ujson_load.py
deleted file mode 100644
index 7cec9246b82..00000000000
--- a/tests/extmod/ujson_load.py
+++ /dev/null
@@ -1,15 +0,0 @@
-try:
-    from uio import StringIO
-    import ujson as json
-except:
-    try:
-        from io import StringIO
-        import json
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-print(json.load(StringIO("null")))
-print(json.load(StringIO('"abc\\u0064e"')))
-print(json.load(StringIO("[false, true, 1, -2]")))
-print(json.load(StringIO('{"a":true}')))
diff --git a/tests/extmod/ujson_loads.py b/tests/extmod/ujson_loads.py
deleted file mode 100644
index 2de9cdcbc1a..00000000000
--- a/tests/extmod/ujson_loads.py
+++ /dev/null
@@ -1,76 +0,0 @@
-try:
-    import ujson as json
-except ImportError:
-    try:
-        import json
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-def my_print(o):
-    if isinstance(o, dict):
-        print("sorted dict", sorted(o.items()))
-    else:
-        print(o)
-
-
-my_print(json.loads("null"))
-my_print(json.loads("false"))
-my_print(json.loads("true"))
-my_print(json.loads("1"))
-my_print(json.loads("-2"))
-my_print(json.loads('"abc\\u0064e"'))
-my_print(json.loads("[]"))
-my_print(json.loads("[null]"))
-my_print(json.loads("[null,false,true]"))
-my_print(json.loads(" [ null , false , true ] "))
-my_print(json.loads("{}"))
-my_print(json.loads('{"a":true}'))
-my_print(json.loads('{"a":null, "b":false, "c":true}'))
-my_print(json.loads('{"a":[], "b":[1], "c":{"3":4}}'))
-my_print(json.loads('"abc\\bdef"'))
-my_print(json.loads('"abc\\fdef"'))
-my_print(json.loads('"abc\\ndef"'))
-my_print(json.loads('"abc\\rdef"'))
-my_print(json.loads('"abc\\tdef"'))
-my_print(json.loads('"abc\\uabcd"'))
-
-# whitespace handling
-my_print(json.loads('{\n\t"a":[]\r\n, "b":[1], "c":{"3":4}     \n\r\t\r\r\r\n}'))
-
-# loading nothing should raise exception
-try:
-    json.loads("")
-except ValueError:
-    print("ValueError")
-
-# string which is not closed
-try:
-    my_print(json.loads('"abc'))
-except ValueError:
-    print("ValueError")
-
-# unaccompanied closing brace
-try:
-    my_print(json.loads("]"))
-except ValueError:
-    print("ValueError")
-
-# unspecified object type
-try:
-    my_print(json.loads("a"))
-except ValueError:
-    print("ValueError")
-
-# bad property name
-try:
-    my_print(json.loads('{{}:"abc"}'))
-except ValueError:
-    print("ValueError")
-
-# unexpected characters after white space
-try:
-    my_print(json.loads("[null]   a"))
-except ValueError:
-    print("ValueError")
diff --git a/tests/extmod/ujson_loads_bytes.py b/tests/extmod/ujson_loads_bytes.py
deleted file mode 100644
index 3ee87bbcd58..00000000000
--- a/tests/extmod/ujson_loads_bytes.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# test loading from bytes and bytearray (introduced in Python 3.6)
-
-try:
-    import ujson as json
-except ImportError:
-    try:
-        import json
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-print(json.loads(b"[1,2]"))
-print(json.loads(bytearray(b"[null]")))
diff --git a/tests/extmod/ujson_loads_bytes.py.exp b/tests/extmod/ujson_loads_bytes.py.exp
deleted file mode 100644
index c2735a99052..00000000000
--- a/tests/extmod/ujson_loads_bytes.py.exp
+++ /dev/null
@@ -1,2 +0,0 @@
-[1, 2]
-[None]
diff --git a/tests/extmod/ujson_loads_float.py b/tests/extmod/ujson_loads_float.py
deleted file mode 100644
index 842718f37de..00000000000
--- a/tests/extmod/ujson_loads_float.py
+++ /dev/null
@@ -1,20 +0,0 @@
-try:
-    import ujson as json
-except ImportError:
-    try:
-        import json
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-def my_print(o):
-    print("%.3f" % o)
-
-
-my_print(json.loads("1.2"))
-my_print(json.loads("1e2"))
-my_print(json.loads("-2.3"))
-my_print(json.loads("-2e3"))
-my_print(json.loads("-2e+3"))
-my_print(json.loads("-2e-3"))
diff --git a/tests/extmod/urandom_basic.py b/tests/extmod/urandom_basic.py
deleted file mode 100644
index f7f5a6d6911..00000000000
--- a/tests/extmod/urandom_basic.py
+++ /dev/null
@@ -1,32 +0,0 @@
-try:
-    import urandom as random
-except ImportError:
-    try:
-        import random
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-# check getrandbits returns a value within the bit range
-for b in (1, 2, 3, 4, 16, 32):
-    for i in range(50):
-        assert random.getrandbits(b) < (1 << b)
-
-# check that seed(0) gives a non-zero value
-random.seed(0)
-print(random.getrandbits(16) != 0)
-
-# check that PRNG is repeatable
-random.seed(1)
-r = random.getrandbits(16)
-random.seed(1)
-print(random.getrandbits(16) == r)
-
-# check that zero bits works
-print(random.getrandbits(0))
-
-# check that it throws an error for negative bits
-try:
-    random.getrandbits(-1)
-except ValueError:
-    print("ValueError")
diff --git a/tests/extmod/urandom_extra.py b/tests/extmod/urandom_extra.py
deleted file mode 100644
index 78e17379dc6..00000000000
--- a/tests/extmod/urandom_extra.py
+++ /dev/null
@@ -1,69 +0,0 @@
-try:
-    import urandom as random
-except ImportError:
-    try:
-        import random
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-try:
-    random.randint
-except AttributeError:
-    print("SKIP")
-    raise SystemExit
-
-print("randrange")
-for i in range(50):
-    assert 0 <= random.randrange(4) < 4
-    assert 2 <= random.randrange(2, 6) < 6
-    assert -2 <= random.randrange(-2, 2) < 2
-    assert random.randrange(1, 9, 2) in (1, 3, 5, 7)
-    assert random.randrange(2, 1, -1) in (1, 2)
-
-# empty range
-try:
-    random.randrange(0)
-except ValueError:
-    print("ValueError")
-
-# empty range
-try:
-    random.randrange(2, 1)
-except ValueError:
-    print("ValueError")
-
-# zero step
-try:
-    random.randrange(2, 1, 0)
-except ValueError:
-    print("ValueError")
-
-# empty range
-try:
-    random.randrange(2, 1, 1)
-except ValueError:
-    print("ValueError")
-
-print("randint")
-for i in range(50):
-    assert 0 <= random.randint(0, 4) <= 4
-    assert 2 <= random.randint(2, 6) <= 6
-    assert -2 <= random.randint(-2, 2) <= 2
-
-# empty range
-try:
-    random.randint(2, 1)
-except ValueError:
-    print("ValueError")
-
-print("choice")
-lst = [1, 2, 5, 6]
-for i in range(50):
-    assert random.choice(lst) in lst
-
-# empty sequence
-try:
-    random.choice([])
-except IndexError:
-    print("IndexError")
diff --git a/tests/extmod/urandom_extra_float.py b/tests/extmod/urandom_extra_float.py
deleted file mode 100644
index 65918da1369..00000000000
--- a/tests/extmod/urandom_extra_float.py
+++ /dev/null
@@ -1,24 +0,0 @@
-try:
-    import urandom as random
-except ImportError:
-    try:
-        import random
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-try:
-    random.randint
-except AttributeError:
-    print("SKIP")
-    raise SystemExit
-
-print("random")
-for i in range(50):
-    assert 0 <= random.random() < 1
-
-print("uniform")
-for i in range(50):
-    assert 0 <= random.uniform(0, 4) <= 4
-    assert 2 <= random.uniform(2, 6) <= 6
-    assert -2 <= random.uniform(-2, 2) <= 2
diff --git a/tests/extmod/urandom_seed_default.py b/tests/extmod/urandom_seed_default.py
deleted file mode 100644
index a032b9362b7..00000000000
--- a/tests/extmod/urandom_seed_default.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# test urandom.seed() without any arguments
-
-try:
-    import urandom as random
-except ImportError:
-    try:
-        import random
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-try:
-    random.seed()
-except ValueError:
-    # no default seed on this platform
-    print("SKIP")
-    raise SystemExit
-
-
-def rng_seq():
-    return [random.getrandbits(16) for _ in range(10)]
-
-
-# seed with default and check that doesn't produce the same RNG sequence
-random.seed()
-seq = rng_seq()
-random.seed()
-print(seq == rng_seq())
-random.seed(None)
-print(seq == rng_seq())
diff --git a/tests/extmod/ure1.py b/tests/extmod/ure1.py
deleted file mode 100644
index 2bdf2d0cfb4..00000000000
--- a/tests/extmod/ure1.py
+++ /dev/null
@@ -1,138 +0,0 @@
-try:
-    import ure as re
-except ImportError:
-    try:
-        import re
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-r = re.compile(".+")
-m = r.match("abc")
-print(m.group(0))
-try:
-    m.group(1)
-except IndexError:
-    print("IndexError")
-
-# conversion of re and match to string
-str(r)
-str(m)
-
-r = re.compile("(.+)1")
-m = r.match("xyz781")
-print(m.group(0))
-print(m.group(1))
-try:
-    m.group(2)
-except IndexError:
-    print("IndexError")
-
-r = re.compile("[a-cu-z]")
-m = r.match("a")
-print(m.group(0))
-m = r.match("z")
-print(m.group(0))
-m = r.match("d")
-print(m)
-m = r.match("A")
-print(m)
-print("===")
-
-r = re.compile("[^a-cu-z]")
-m = r.match("a")
-print(m)
-m = r.match("z")
-print(m)
-m = r.match("d")
-print(m.group(0))
-m = r.match("A")
-print(m.group(0))
-print("===")
-
-# '-' character within character class block
-print(re.match("[-a]+", "-a]d").group(0))
-print(re.match("[a-]+", "-a]d").group(0))
-print("===")
-
-r = re.compile("o+")
-m = r.search("foobar")
-print(m.group(0))
-try:
-    m.group(1)
-except IndexError:
-    print("IndexError")
-
-
-m = re.match(".*", "foo")
-print(m.group(0))
-
-m = re.search("w.r", "hello world")
-print(m.group(0))
-
-m = re.match("a+?", "ab")
-print(m.group(0))
-m = re.match("a*?", "ab")
-print(m.group(0))
-m = re.match("^ab$", "ab")
-print(m.group(0))
-m = re.match("a|b", "b")
-print(m.group(0))
-m = re.match("a|b|c", "c")
-print(m.group(0))
-
-# Case where anchors fail to match
-r = re.compile("^b|b$")
-m = r.search("abc")
-print(m)
-
-try:
-    re.compile("*")
-except:
-    print("Caught invalid regex")
-
-# bytes objects
-m = re.match(rb"a+?", b"ab")
-print(m.group(0))
-print("===")
-
-# escaping
-m = re.match(r"a\.c", "a.c")
-print(m.group(0) if m else "")
-m = re.match(r"a\.b", "abc")
-print(m is None)
-m = re.match(r"a\.b", "a\\bc")
-print(m is None)
-m = re.match(r"[a\-z]", "abc")
-print(m.group(0))
-m = re.match(r"[.\]]*", ".].]a")
-print(m.group(0))
-m = re.match(r"[.\]+]*", ".]+.]a")
-print(m.group(0))
-m = re.match(r"[a-f0-9x\-yz]*", "abxcd1-23")
-print(m.group(0))
-m = re.match(r"[a\\b]*", "a\\aa\\bb\\bbab")
-print(m.group(0))
-m = re.search(r"[a\-z]", "-")
-print(m.group(0))
-m = re.search(r"[a\-z]", "f")
-print(m is None)
-m = re.search(r"[a\]z]", "a")
-print(m.group(0))
-print(re.compile(r"[-a]").split("foo-bar"))
-print(re.compile(r"[a-]").split("foo-bar"))
-print(re.compile(r"[ax\-]").split("foo-bar"))
-print(re.compile(r"[a\-x]").split("foo-bar"))
-print(re.compile(r"[\-ax]").split("foo-bar"))
-print("===")
-
-# Module functions take str/bytes/re.
-for f in (re.match, re.search):
-    print(f(".", "foo").group(0))
-    print(f(b".", b"foo").group(0))
-    print(f(re.compile("."), "foo").group(0))
-    try:
-        f(123, "a")
-    except TypeError:
-        print("TypeError")
-print("===")
diff --git a/tests/extmod/ure_debug.py b/tests/extmod/ure_debug.py
deleted file mode 100644
index 7a07ede2d43..00000000000
--- a/tests/extmod/ure_debug.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# test printing debugging info when compiling
-try:
-    import ure
-
-    ure.DEBUG
-except (ImportError, AttributeError):
-    print("SKIP")
-    raise SystemExit
-
-ure.compile("^a|b[0-9]\w$", ure.DEBUG)
diff --git a/tests/extmod/ure_error.py b/tests/extmod/ure_error.py
deleted file mode 100644
index 52a96b7c038..00000000000
--- a/tests/extmod/ure_error.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# test errors in regex
-
-try:
-    import ure as re
-except ImportError:
-    try:
-        import re
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-def test_re(r):
-    try:
-        re.compile(r)
-        print("OK")
-    except:  # uPy and CPy use different errors, so just ignore the type
-        print("Error")
-
-
-test_re(r"?")
-test_re(r"*")
-test_re(r"+")
-test_re(r")")
-test_re(r"[")
-test_re(r"([")
-test_re(r"([)")
-test_re(r"[a\]")
diff --git a/tests/extmod/ure_group.py b/tests/extmod/ure_group.py
deleted file mode 100644
index 41425dd6234..00000000000
--- a/tests/extmod/ure_group.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# test groups, and nested groups
-
-try:
-    import ure as re
-except ImportError:
-    try:
-        import re
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-def print_groups(match):
-    print("----")
-    try:
-        i = 0
-        while True:
-            print(match.group(i))
-            i += 1
-    except IndexError:
-        pass
-
-
-m = re.match(r"(([0-9]*)([a-z]*)[0-9]*)", "1234hello567")
-print_groups(m)
-
-m = re.match(r"([0-9]*)(([a-z]*)([0-9]*))", "1234hello567")
-print_groups(m)
-
-# optional group that matches
-print_groups(re.match(r"(a)?b(c)", "abc"))
-
-# optional group that doesn't match
-print_groups(re.match(r"(a)?b(c)", "bc"))
diff --git a/tests/extmod/ure_groups.py b/tests/extmod/ure_groups.py
deleted file mode 100644
index 7da072a9206..00000000000
--- a/tests/extmod/ure_groups.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# test match.groups()
-
-try:
-    import ure as re
-except ImportError:
-    try:
-        import re
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-try:
-    m = re.match(".", "a")
-    m.groups
-except AttributeError:
-    print("SKIP")
-    raise SystemExit
-
-
-m = re.match(r"(([0-9]*)([a-z]*)[0-9]*)", "1234hello567")
-print(m.groups())
-
-m = re.match(r"([0-9]*)(([a-z]*)([0-9]*))", "1234hello567")
-print(m.groups())
-
-# optional group that matches
-print(re.match(r"(a)?b(c)", "abc").groups())
-
-# optional group that doesn't match
-print(re.match(r"(a)?b(c)", "bc").groups())
-
-# only a single match
-print(re.match(r"abc", "abc").groups())
diff --git a/tests/extmod/ure_limit.py b/tests/extmod/ure_limit.py
deleted file mode 100644
index 99c6a818e85..00000000000
--- a/tests/extmod/ure_limit.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Test overflow in ure.compile output code.
-
-try:
-    import ure as re
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-
-def test_re(r):
-    try:
-        re.compile(r)
-    except:
-        print("Error")
-
-
-# too many chars in []
-test_re("[" + "a" * 256 + "]")
-
-# too many groups
-test_re("(a)" * 256)
-
-# jump too big for ?
-test_re("(" + "a" * 62 + ")?")
-
-# jump too big for *
-test_re("(" + "a" * 60 + ".)*")
-test_re("(" + "a" * 60 + "..)*")
-
-# jump too big for +
-test_re("(" + "a" * 62 + ")+")
-
-# jump too big for |
-test_re("b" * 63 + "|a")
diff --git a/tests/extmod/ure_namedclass.py b/tests/extmod/ure_namedclass.py
deleted file mode 100644
index 4afc09dc0ab..00000000000
--- a/tests/extmod/ure_namedclass.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# test named char classes
-
-try:
-    import ure as re
-except ImportError:
-    try:
-        import re
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-def print_groups(match):
-    print("----")
-    try:
-        i = 0
-        while True:
-            print(match.group(i))
-            i += 1
-    except IndexError:
-        pass
-
-
-m = re.match(r"\w+", "1234hello567 abc")
-print_groups(m)
-
-m = re.match(r"(\w+)\s+(\w+)", "ABC \t1234hello567 abc")
-print_groups(m)
-
-m = re.match(r"(\S+)\s+(\D+)", "ABC \thello abc567 abc")
-print_groups(m)
-
-m = re.match(r"(([0-9]*)([a-z]*)\d*)", "1234hello567")
-print_groups(m)
-
-# named class within a class set
-print_groups(re.match("([^\s]+)\s*([^\s]+)", "1 23"))
-print_groups(re.match("([\s\d]+)([\W]+)", "1  2-+="))
-print_groups(re.match("([\W]+)([^\W]+)([^\S]+)([^\D]+)", " a_1 23"))
diff --git a/tests/extmod/ure_span.py b/tests/extmod/ure_span.py
deleted file mode 100644
index 03a3fef9f3d..00000000000
--- a/tests/extmod/ure_span.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# test match.span(), and nested spans
-
-try:
-    import ure as re
-except ImportError:
-    try:
-        import re
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-try:
-    m = re.match(".", "a")
-    m.span
-except AttributeError:
-    print("SKIP")
-    raise SystemExit
-
-
-def print_spans(match):
-    print("----")
-    try:
-        i = 0
-        while True:
-            print(match.span(i), match.start(i), match.end(i))
-            i += 1
-    except IndexError:
-        pass
-
-
-m = re.match(r"(([0-9]*)([a-z]*)[0-9]*)", "1234hello567")
-print_spans(m)
-
-m = re.match(r"([0-9]*)(([a-z]*)([0-9]*))", "1234hello567")
-print_spans(m)
-
-# optional span that matches
-print_spans(re.match(r"(a)?b(c)", "abc"))
-
-# optional span that doesn't match
-print_spans(re.match(r"(a)?b(c)", "bc"))
diff --git a/tests/extmod/ure_split.py b/tests/extmod/ure_split.py
deleted file mode 100644
index 7e6ef3990f4..00000000000
--- a/tests/extmod/ure_split.py
+++ /dev/null
@@ -1,43 +0,0 @@
-try:
-    import ure as re
-except ImportError:
-    try:
-        import re
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-r = re.compile(" ")
-s = r.split("a b c foobar")
-print(s)
-
-r = re.compile(" +")
-s = r.split("a b    c   foobar")
-print(s)
-
-r = re.compile(" +")
-s = r.split("a b    c   foobar", 1)
-print(s)
-
-r = re.compile(" +")
-s = r.split("a b    c   foobar", 2)
-print(s)
-
-r = re.compile("[a-f]+")
-s = r.split("0a3b9")
-print(s)
-
-# bytes objects
-r = re.compile(b"x")
-s = r.split(b"fooxbar")
-print(s)
-
-# using ^
-r = re.compile("^ab")
-s = r.split("abababcabab")
-print(s)
-
-# using ^ with |
-r = re.compile("^ab|cab")
-s = r.split("abababcabab")
-print(s)
diff --git a/tests/extmod/ure_split_empty.py b/tests/extmod/ure_split_empty.py
deleted file mode 100644
index 76ce97ea67d..00000000000
--- a/tests/extmod/ure_split_empty.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# test splitting with pattern matches that can be empty
-#
-# CPython 3.5 issues a FutureWarning for these tests because their
-# behaviour will change in a future version.  MicroPython just stops
-# splitting as soon as an empty match is found.
-
-try:
-    import ure as re
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-r = re.compile(" *")
-s = r.split("a b    c   foobar")
-print(s)
-
-r = re.compile("x*")
-s = r.split("foo")
-print(s)
-
-r = re.compile("x*")
-s = r.split("axbc")
-print(s)
diff --git a/tests/extmod/ure_split_notimpl.py b/tests/extmod/ure_split_notimpl.py
deleted file mode 100644
index 51bad791efb..00000000000
--- a/tests/extmod/ure_split_notimpl.py
+++ /dev/null
@@ -1,11 +0,0 @@
-try:
-    import ure as re
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-r = re.compile("( )")
-try:
-    s = r.split("a b c foobar")
-except NotImplementedError:
-    print("NotImplementedError")
diff --git a/tests/extmod/ure_stack_overflow.py b/tests/extmod/ure_stack_overflow.py
deleted file mode 100644
index d3ce0c5a7b1..00000000000
--- a/tests/extmod/ure_stack_overflow.py
+++ /dev/null
@@ -1,13 +0,0 @@
-try:
-    import ure as re
-except ImportError:
-    try:
-        import re
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-try:
-    re.match("(a*)*", "aaa")
-except RuntimeError:
-    print("RuntimeError")
diff --git a/tests/extmod/ure_sub.py b/tests/extmod/ure_sub.py
deleted file mode 100644
index 806c3895762..00000000000
--- a/tests/extmod/ure_sub.py
+++ /dev/null
@@ -1,81 +0,0 @@
-try:
-    import ure as re
-except ImportError:
-    try:
-        import re
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-try:
-    re.sub
-except AttributeError:
-    print("SKIP")
-    raise SystemExit
-
-
-def multiply(m):
-    return str(int(m.group(0)) * 2)
-
-
-print(re.sub("\d+", multiply, "10 20 30 40 50"))
-
-print(re.sub("\d+", lambda m: str(int(m.group(0)) // 2), "10 20 30 40 50"))
-
-
-def A():
-    return "A"
-
-
-print(re.sub("a", A(), "aBCBABCDabcda."))
-
-print(
-    re.sub(
-        r"def\s+([a-zA-Z_][a-zA-Z_0-9]*)\s*\(\s*\):",
-        "static PyObject*\npy_\\1(void){\n  return;\n}\n",
-        "\n\ndef myfunc():\n\ndef myfunc1():\n\ndef myfunc2():",
-    )
-)
-
-print(
-    re.compile("(calzino) (blu|bianco|verde) e (scarpa) (blu|bianco|verde)").sub(
-        r"\g<1> colore \2 con \g<3> colore \4? ...", "calzino blu e scarpa verde"
-    )
-)
-
-# \g immediately followed by another \g
-print(re.sub("(abc)", r"\g<1>\g<1>", "abc"))
-
-# no matches at all
-print(re.sub("a", "b", "c"))
-
-# with maximum substitution count specified
-print(re.sub("a", "b", "1a2a3a", 2))
-
-# invalid group
-try:
-    re.sub("(a)", "b\\2", "a")
-except:
-    print("invalid group")
-
-# invalid group with very large number (to test overflow in uPy)
-try:
-    re.sub("(a)", "b\\199999999999999999999999999999999999999", "a")
-except:
-    print("invalid group")
-
-# Module function takes str/bytes/re.
-print(re.sub("a", "a", "a"))
-print(re.sub(b".", b"a", b"a"))
-print(re.sub(re.compile("a"), "a", "a"))
-try:
-    re.sub(123, "a", "a")
-except TypeError:
-    print("TypeError")
-
-# Include \ in the sub replacement
-print(re.sub("b", "\\\\b", "abc"))
-
-# Using ^, make sure it doesn't repeatedly match
-print(re.sub("^ab", "*", "abababcabab"))
-print(re.sub("^ab|cab", "*", "abababcabab"))
diff --git a/tests/extmod/ure_sub_unmatched.py b/tests/extmod/ure_sub_unmatched.py
deleted file mode 100644
index d6312bfc2cb..00000000000
--- a/tests/extmod/ure_sub_unmatched.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# test re.sub with unmatched groups, behaviour changed in CPython 3.5
-
-try:
-    import ure as re
-except ImportError:
-    try:
-        import re
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-try:
-    re.sub
-except AttributeError:
-    print("SKIP")
-    raise SystemExit
-
-# first group matches, second optional group doesn't so is replaced with a blank
-print(re.sub(r"(a)(b)?", r"\2-\1", "1a2"))
diff --git a/tests/extmod/ure_sub_unmatched.py.exp b/tests/extmod/ure_sub_unmatched.py.exp
deleted file mode 100644
index 1e5f0fda055..00000000000
--- a/tests/extmod/ure_sub_unmatched.py.exp
+++ /dev/null
@@ -1 +0,0 @@
-1-a2
diff --git a/tests/extmod/uselect_poll_basic.py b/tests/extmod/uselect_poll_basic.py
deleted file mode 100644
index 97fbd6fd158..00000000000
--- a/tests/extmod/uselect_poll_basic.py
+++ /dev/null
@@ -1,42 +0,0 @@
-try:
-    import usocket as socket, uselect as select, uerrno as errno
-except ImportError:
-    try:
-        import socket, select, errno
-
-        select.poll  # Raises AttributeError for CPython implementations without poll()
-    except (ImportError, AttributeError):
-        print("SKIP")
-        raise SystemExit
-
-
-poller = select.poll()
-
-s = socket.socket()
-
-poller.register(s)
-# https://docs.python.org/3/library/select.html#select.poll.register
-# "Registering a file descriptor that’s already registered is not an error,
-# and has the same effect as registering the descriptor exactly once."
-poller.register(s)
-
-# 2 args are mandatory unlike register()
-try:
-    poller.modify(s)
-except TypeError:
-    print("modify:TypeError")
-
-poller.modify(s, select.POLLIN)
-
-poller.unregister(s)
-
-try:
-    poller.modify(s, select.POLLIN)
-except OSError as e:
-    assert e.errno == errno.ENOENT
-
-# poll after closing the socket, should return POLLNVAL
-poller.register(s)
-s.close()
-p = poller.poll(0)
-print(len(p), p[0][-1])
diff --git a/tests/extmod/uselect_poll_udp.py b/tests/extmod/uselect_poll_udp.py
deleted file mode 100644
index 2a56a122b5f..00000000000
--- a/tests/extmod/uselect_poll_udp.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# test select.poll on UDP sockets
-
-try:
-    import usocket as socket, uselect as select
-except ImportError:
-    try:
-        import socket, select
-
-        select.poll  # Raises AttributeError for CPython implementations without poll()
-    except (ImportError, AttributeError):
-        print("SKIP")
-        raise SystemExit
-
-
-s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-s.bind(socket.getaddrinfo("127.0.0.1", 8000)[0][-1])
-poll = select.poll()
-
-# UDP socket should not be readable
-poll.register(s, select.POLLIN)
-print(len(poll.poll(0)))
-
-# UDP socket should be writable
-poll.modify(s, select.POLLOUT)
-print(poll.poll(0)[0][1] == select.POLLOUT)
-
-# same test for select.select, but just skip it if the function isn't available
-if hasattr(select, "select"):
-    r, w, e = select.select([s], [], [], 0)
-    assert not r and not w and not e
diff --git a/tests/extmod/usocket_tcp_basic.py b/tests/extmod/usocket_tcp_basic.py
deleted file mode 100644
index c2fe8cd14cc..00000000000
--- a/tests/extmod/usocket_tcp_basic.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Test basic, stand-alone TCP socket functionality
-
-try:
-    import usocket as socket, uerrno as errno
-except ImportError:
-    try:
-        import socket, errno
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-# recv() on a fresh socket should raise ENOTCONN
-s = socket.socket()
-try:
-    s.recv(1)
-except OSError as er:
-    print("ENOTCONN:", er.errno == errno.ENOTCONN)
diff --git a/tests/extmod/usocket_udp_nonblock.py b/tests/extmod/usocket_udp_nonblock.py
deleted file mode 100644
index bc560de142e..00000000000
--- a/tests/extmod/usocket_udp_nonblock.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# test non-blocking UDP sockets
-
-try:
-    import usocket as socket, uerrno as errno
-except ImportError:
-    try:
-        import socket, errno
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-s.bind(socket.getaddrinfo("127.0.0.1", 8000)[0][-1])
-s.settimeout(0)
-
-try:
-    s.recv(1)
-except OSError as er:
-    print("EAGAIN:", er.errno == errno.EAGAIN)
diff --git a/tests/extmod/ussl_basic.py b/tests/extmod/ussl_basic.py
deleted file mode 100644
index dd3b6f0b91e..00000000000
--- a/tests/extmod/ussl_basic.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# very basic test of ssl module, just to test the methods exist
-
-try:
-    import uio as io
-    import ussl as ssl
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-
-class TestSocket(io.IOBase):
-    def write(self, buf):
-        return len(buf)
-
-    def readinto(self, buf):
-        return 0
-
-    def ioctl(self, cmd, arg):
-        print("TestSocket.ioctl", cmd, arg)
-        return 0
-
-    def setblocking(self, value):
-        print("TestSocket.setblocking({})".format(value))
-
-
-# create in client mode
-try:
-    ss = ssl.wrap_socket(TestSocket(), server_hostname="test.example.com")
-except OSError as er:
-    print("OSError: client")
-
-# create in server mode (can use this object for further tests)
-ss = ssl.wrap_socket(TestSocket(), server_side=1, do_handshake=0)
-
-# print
-print(repr(ss)[:12])
-
-# setblocking() propagates call to the underlying stream object
-ss.setblocking(False)
-ss.setblocking(True)
-
-# write
-try:
-    ss.write(b"aaaa")
-except OSError:
-    pass
-
-# read (underlying socket has no data)
-try:
-    ss.read(8)
-except OSError:
-    pass
-
-# close
-ss.close()
-# close 2nd time
-ss.close()
-
-# read on closed socket
-try:
-    ss.read(10)
-except OSError as er:
-    print("OSError: read")
-
-# write on closed socket
-try:
-    ss.write(b"aaaa")
-except OSError as er:
-    print("OSError: write")
diff --git a/tests/extmod/ussl_basic.py.exp b/tests/extmod/ussl_basic.py.exp
deleted file mode 100644
index 30bfd3a4367..00000000000
--- a/tests/extmod/ussl_basic.py.exp
+++ /dev/null
@@ -1,7 +0,0 @@
-OSError: client
-<_SSLSocket 
-TestSocket.setblocking(False)
-TestSocket.setblocking(True)
-TestSocket.ioctl 4 0
-OSError: read
-OSError: write
diff --git a/tests/extmod/ussl_keycert.py b/tests/extmod/ussl_keycert.py
deleted file mode 100644
index 87a40a1e552..00000000000
--- a/tests/extmod/ussl_keycert.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Test ussl with key/cert passed in
-
-try:
-    import uio as io
-    import ussl as ssl
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-key = b"0\x82\x019\x02\x01\x00\x02A\x00\xf9\xe0}\xbd\xd7\x9cI\x18\x06\xc3\xcb\xb5\xec@r\xfbD\x18\x80\xaaWoZ{\xcc\xa3\xeb!\"\x0fY\x9e]-\xee\xe4\t!BY\x9f{7\xf3\xf2\x8f}}\r|.\xa8<\ta\xb2\xd7W\xb3\xc9\x19A\xc39\x02\x03\x01\x00\x01\x02@\x07:\x9fh\xa6\x9c6\xe1#\x10\xf7\x0b\xc4Q\xf9\x01\x9b\xee\xb9\x8a4\r\\\xa8\xc8:\xd5\xca\x97\x99\xaa\x16\x04)\xa8\xf9\x13\xdeq\x0ev`\xa7\x83\xc5\x8b`\xdb\xef \x9d\x93\xe8g\x84\x96\xfaV\\\xf4R\xda\xd0\xa1\x02!\x00\xfeR\xbf\n\x91Su\x87L\x98{\xeb%\xed\xfb\x06u)@\xfe\x1b\xde\xa0\xc6@\xab\xc5\xedg\x8e\x10[\x02!\x00\xfb\x86=\x85\xa4'\xde\x85\xb5L\xe0)\x99\xfaL\x8c3A\x02\xa8<\xdew\xad\x00\xe3\x1d\x05\xd8\xb4N\xfb\x02 \x08\xb0M\x04\x90hx\x88q\xcew\xd5U\xcbf\x9b\x16\xdf\x9c\xef\xd1\x85\xee\x9a7Ug\x02\xb0Z\x03'\x02 9\xa0D\xe2$|\xf9\xefz]5\x92rs\xb5+\xfd\xe6,\x1c\xadmn\xcf\xd5?3|\x0em)\x17\x02 5Z\xcc/\xa5?\n\x04%\x9b{N\x9dX\xddI\xbe\xd2\xb0\xa0\x03BQ\x02\x82\xc2\xe0u)\xbd\xb8\xaf"
-
-# Invalid key
-try:
-    ssl.wrap_socket(io.BytesIO(), key=b"!")
-except ValueError as er:
-    print(repr(er))
-
-# Valid key, no cert
-try:
-    ssl.wrap_socket(io.BytesIO(), key=key)
-except TypeError as er:
-    print(repr(er))
-
-# Valid key, invalid cert
-try:
-    ssl.wrap_socket(io.BytesIO(), key=key, cert=b"!")
-except ValueError as er:
-    print(repr(er))
diff --git a/tests/extmod/ussl_keycert.py.exp b/tests/extmod/ussl_keycert.py.exp
deleted file mode 100644
index b72d319c6a8..00000000000
--- a/tests/extmod/ussl_keycert.py.exp
+++ /dev/null
@@ -1,3 +0,0 @@
-ValueError('invalid key',)
-TypeError("can't convert 'NoneType' object to str implicitly",)
-ValueError('invalid cert',)
diff --git a/tests/extmod/ussl_poll.py b/tests/extmod/ussl_poll.py
deleted file mode 100644
index 8080ec51122..00000000000
--- a/tests/extmod/ussl_poll.py
+++ /dev/null
@@ -1,196 +0,0 @@
-try:
-    import uselect
-    import ussl
-    import io
-    import ubinascii as binascii
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-from micropython import const
-
-_MP_STREAM_POLL_RD = const(0x0001)
-_MP_STREAM_POLL_WR = const(0x0004)
-_MP_STREAM_POLL_NVAL = const(0x0020)
-_MP_STREAM_POLL = const(3)
-_MP_STREAM_CLOSE = const(4)
-
-
-# This self-signed key/cert pair is randomly generated and to be used for
-# testing/demonstration only.  You should always generate your own key/cert.
-key = binascii.unhexlify(
-    b"3082013b020100024100cc20643fd3d9c21a0acba4f48f61aadd675f52175a9dcf07fbef"
-    b"610a6a6ba14abb891745cd18a1d4c056580d8ff1a639460f867013c8391cdc9f2e573b0f"
-    b"872d0203010001024100bb17a54aeb3dd7ae4edec05e775ca9632cf02d29c2a089b563b0"
-    b"d05cdf95aeca507de674553f28b4eadaca82d5549a86058f9996b07768686a5b02cb240d"
-    b"d9f1022100f4a63f5549e817547dca97b5c658038e8593cb78c5aba3c4642cc4cd031d86"
-    b"8f022100d598d870ffe4a34df8de57047a50b97b71f4d23e323f527837c9edae88c79483"
-    b"02210098560c89a70385c36eb07fd7083235c4c1184e525d838aedf7128958bedfdbb102"
-    b"2051c0dab7057a8176ca966f3feb81123d4974a733df0f958525f547dfd1c271f9022044"
-    b"6c2cafad455a671a8cf398e642e1be3b18a3d3aec2e67a9478f83c964c4f1f"
-)
-cert = binascii.unhexlify(
-    b"308201d53082017f020203e8300d06092a864886f70d01010505003075310b3009060355"
-    b"0406130258583114301206035504080c0b54686550726f76696e63653110300e06035504"
-    b"070c075468654369747931133011060355040a0c0a436f6d70616e7958595a3113301106"
-    b"0355040b0c0a436f6d70616e7958595a3114301206035504030c0b546865486f73744e61"
-    b"6d65301e170d3139313231383033333935355a170d3239313231353033333935355a3075"
-    b"310b30090603550406130258583114301206035504080c0b54686550726f76696e636531"
-    b"10300e06035504070c075468654369747931133011060355040a0c0a436f6d70616e7958"
-    b"595a31133011060355040b0c0a436f6d70616e7958595a3114301206035504030c0b5468"
-    b"65486f73744e616d65305c300d06092a864886f70d0101010500034b003048024100cc20"
-    b"643fd3d9c21a0acba4f48f61aadd675f52175a9dcf07fbef610a6a6ba14abb891745cd18"
-    b"a1d4c056580d8ff1a639460f867013c8391cdc9f2e573b0f872d0203010001300d06092a"
-    b"864886f70d0101050500034100b0513fe2829e9ecbe55b6dd14c0ede7502bde5d46153c8"
-    b"e960ae3ebc247371b525caeb41bbcf34686015a44c50d226e66aef0a97a63874ca5944ef"
-    b"979b57f0b3"
-)
-
-
-class _Pipe(io.IOBase):
-    def __init__(self):
-        self._other = None
-        self.block_reads = False
-        self.block_writes = False
-
-        self.write_buffers = []
-        self.last_poll_arg = None
-
-    def readinto(self, buf):
-        if self.block_reads or len(self._other.write_buffers) == 0:
-            return None
-
-        read_buf = self._other.write_buffers[0]
-        l = min(len(buf), len(read_buf))
-        buf[:l] = read_buf[:l]
-        if l == len(read_buf):
-            self._other.write_buffers.pop(0)
-        else:
-            self._other.write_buffers[0] = read_buf[l:]
-        return l
-
-    def write(self, buf):
-        if self.block_writes:
-            return None
-
-        self.write_buffers.append(memoryview(bytes(buf)))
-        return len(buf)
-
-    def ioctl(self, request, arg):
-        if request == _MP_STREAM_POLL:
-            self.last_poll_arg = arg
-            ret = 0
-            if arg & _MP_STREAM_POLL_RD:
-                if not self.block_reads and self._other.write_buffers:
-                    ret |= _MP_STREAM_POLL_RD
-            if arg & _MP_STREAM_POLL_WR:
-                if not self.block_writes:
-                    ret |= _MP_STREAM_POLL_WR
-            return ret
-
-        elif request == _MP_STREAM_CLOSE:
-            return 0
-
-        raise NotImplementedError()
-
-    @classmethod
-    def new_pair(cls):
-        p1 = cls()
-        p2 = cls()
-        p1._other = p2
-        p2._other = p1
-        return p1, p2
-
-
-def assert_poll(s, i, arg, expected_arg, expected_ret):
-    ret = s.ioctl(_MP_STREAM_POLL, arg)
-    assert i.last_poll_arg == expected_arg
-    i.last_poll_arg = None
-    assert ret == expected_ret
-
-
-def assert_raises(cb, *args, **kwargs):
-    try:
-        cb(*args, **kwargs)
-        raise AssertionError("should have raised")
-    except Exception as exc:
-        pass
-
-
-client_io, server_io = _Pipe.new_pair()
-
-client_io.block_reads = True
-client_io.block_writes = True
-client_sock = ussl.wrap_socket(client_io, do_handshake=False)
-
-server_sock = ussl.wrap_socket(server_io, key=key, cert=cert, server_side=True, do_handshake=False)
-
-# Do a test read, at this point the TLS handshake wants to write,
-# so it returns None:
-assert client_sock.read(128) is None
-
-# Polling for either read or write actually check if the underlying socket can write:
-assert_poll(client_sock, client_io, _MP_STREAM_POLL_RD, _MP_STREAM_POLL_WR, 0)
-assert_poll(client_sock, client_io, _MP_STREAM_POLL_WR, _MP_STREAM_POLL_WR, 0)
-
-# Mark the socket as writable, and do another test read:
-client_io.block_writes = False
-assert client_sock.read(128) is None
-
-# The client wrote the CLIENT_HELLO message
-assert len(client_io.write_buffers) == 1
-
-# At this point the TLS handshake wants to read, but we don't know that yet:
-assert_poll(client_sock, client_io, _MP_STREAM_POLL_RD, _MP_STREAM_POLL_RD, 0)
-assert_poll(client_sock, client_io, _MP_STREAM_POLL_WR, _MP_STREAM_POLL_WR, _MP_STREAM_POLL_WR)
-
-# Do a test write
-client_sock.write(b"foo")
-
-# Now we know that we want to read:
-assert_poll(client_sock, client_io, _MP_STREAM_POLL_RD, _MP_STREAM_POLL_RD, 0)
-assert_poll(client_sock, client_io, _MP_STREAM_POLL_WR, _MP_STREAM_POLL_RD, 0)
-
-# Unblock reads and nudge the two sockets:
-client_io.block_reads = False
-while server_io.write_buffers or client_io.write_buffers:
-    if server_io.write_buffers:
-        assert client_sock.read(128) is None
-    if client_io.write_buffers:
-        assert server_sock.read(128) is None
-
-# At this point, the handshake is done, try writing data:
-client_sock.write(b"foo")
-assert server_sock.read(3) == b"foo"
-
-# Test reading partial data:
-client_sock.write(b"foobar")
-assert server_sock.read(3) == b"foo"
-server_io.block_reads = True
-assert_poll(
-    server_sock, server_io, _MP_STREAM_POLL_RD, None, _MP_STREAM_POLL_RD
-)  # Did not go to the socket, just consumed buffered data
-assert server_sock.read(3) == b"bar"
-
-
-# Polling on a closed socket errors out:
-client_io, _ = _Pipe.new_pair()
-client_sock = ussl.wrap_socket(client_io, do_handshake=False)
-client_sock.close()
-assert_poll(
-    client_sock, client_io, _MP_STREAM_POLL_RD, None, _MP_STREAM_POLL_NVAL
-)  # Did not go to the socket
-
-
-# Errors propagates to poll:
-client_io, server_io = _Pipe.new_pair()
-client_sock = ussl.wrap_socket(client_io, do_handshake=False)
-
-# The server returns garbage:
-server_io.write(b"fooba")  # Needs to be exactly 5 bytes
-
-assert_poll(client_sock, client_io, _MP_STREAM_POLL_RD, _MP_STREAM_POLL_RD, _MP_STREAM_POLL_RD)
-assert_raises(client_sock.read, 128)
-assert_poll(
-    client_sock, client_io, _MP_STREAM_POLL_RD, None, _MP_STREAM_POLL_NVAL
-)  # Did not go to the socket
diff --git a/tests/extmod/utime_res.py b/tests/extmod/utime_res.py
deleted file mode 100644
index 4b624334836..00000000000
--- a/tests/extmod/utime_res.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# test utime resolutions
-
-try:
-    import utime
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-
-def gmtime_time():
-    return utime.gmtime(utime.time())
-
-
-def localtime_time():
-    return utime.localtime(utime.time())
-
-
-def test():
-    TEST_TIME = 2500
-    EXPECTED_MAP = (
-        # (function name, min. number of results in 2.5 sec)
-        ("time", 3),
-        ("gmtime", 3),
-        ("localtime", 3),
-        ("gmtime_time", 3),
-        ("localtime_time", 3),
-        ("ticks_ms", 15),
-        ("ticks_us", 15),
-        ("ticks_ns", 15),
-        ("ticks_cpu", 15),
-    )
-
-    # call time functions
-    results_map = {}
-    end_time = utime.ticks_ms() + TEST_TIME
-    while utime.ticks_diff(end_time, utime.ticks_ms()) > 0:
-        utime.sleep_ms(100)
-        for func_name, _ in EXPECTED_MAP:
-            try:
-                time_func = getattr(utime, func_name, None) or globals()[func_name]
-                now = time_func()  # may raise AttributeError
-            except (KeyError, AttributeError):
-                continue
-            try:
-                results_map[func_name].add(now)
-            except KeyError:
-                results_map[func_name] = {now}
-
-    # check results
-    for func_name, min_len in EXPECTED_MAP:
-        print("Testing %s" % func_name)
-        results = results_map.get(func_name)
-        if results is None:
-            pass
-        elif func_name == "ticks_cpu" and results == {0}:
-            # ticks_cpu() returns 0 on some ports (e.g. unix)
-            pass
-        elif len(results) < min_len:
-            print(
-                "%s() returns %s result%s in %s ms, expecting >= %s"
-                % (func_name, len(results), "s"[: len(results) != 1], TEST_TIME, min_len)
-            )
-
-
-test()
diff --git a/tests/extmod/utime_time_ns.py b/tests/extmod/utime_time_ns.py
deleted file mode 100644
index 0d13f839d4f..00000000000
--- a/tests/extmod/utime_time_ns.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# test utime.time_ns()
-
-try:
-    import utime
-
-    utime.sleep_us
-    utime.time_ns
-except (ImportError, AttributeError):
-    print("SKIP")
-    raise SystemExit
-
-
-t0 = utime.time_ns()
-utime.sleep_us(5000)
-t1 = utime.time_ns()
-
-# Check that time_ns increases.
-print(t0 < t1)
-
-# Check that time_ns counts correctly, but be very lenient with the bounds (2ms to 50ms).
-if 2000000 < t1 - t0 < 50000000:
-    print(True)
-else:
-    print(t0, t1, t1 - t0)
diff --git a/tests/extmod/utime_time_ns.py.exp b/tests/extmod/utime_time_ns.py.exp
deleted file mode 100644
index dbde422651c..00000000000
--- a/tests/extmod/utime_time_ns.py.exp
+++ /dev/null
@@ -1,2 +0,0 @@
-True
-True
diff --git a/tests/extmod/utimeq1.py b/tests/extmod/utimeq1.py
deleted file mode 100644
index 688e5b834ff..00000000000
--- a/tests/extmod/utimeq1.py
+++ /dev/null
@@ -1,146 +0,0 @@
-# Test for utimeq module which implements task queue with support for
-# wraparound time (utime.ticks_ms() style).
-try:
-    from utime import ticks_add, ticks_diff
-    from utimeq import utimeq
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-DEBUG = 0
-
-MAX = ticks_add(0, -1)
-MODULO_HALF = MAX // 2 + 1
-
-if DEBUG:
-
-    def dprint(*v):
-        print(*v)
-
-else:
-
-    def dprint(*v):
-        pass
-
-
-# Try not to crash on invalid data
-h = utimeq(10)
-try:
-    h.push(1)
-    assert False
-except TypeError:
-    pass
-
-try:
-    h.pop(1)
-    assert False
-except IndexError:
-    pass
-
-# unsupported unary op
-try:
-    ~h
-    assert False
-except TypeError:
-    pass
-
-# pushing on full queue
-h = utimeq(1)
-h.push(1, 0, 0)
-try:
-    h.push(2, 0, 0)
-    assert False
-except IndexError:
-    pass
-
-# popping into invalid type
-try:
-    h.pop([])
-    assert False
-except TypeError:
-    pass
-
-# length
-assert len(h) == 1
-
-# peektime
-assert h.peektime() == 1
-
-# peektime with empty queue
-try:
-    utimeq(1).peektime()
-    assert False
-except IndexError:
-    pass
-
-
-def pop_all(h):
-    l = []
-    while h:
-        item = [0, 0, 0]
-        h.pop(item)
-        # print("!", item)
-        l.append(tuple(item))
-    dprint(l)
-    return l
-
-
-def add(h, v):
-    h.push(v, 0, 0)
-    dprint("-----")
-    # h.dump()
-    dprint("-----")
-
-
-h = utimeq(10)
-add(h, 0)
-add(h, MAX)
-add(h, MAX - 1)
-add(h, 101)
-add(h, 100)
-add(h, MAX - 2)
-dprint(h)
-l = pop_all(h)
-for i in range(len(l) - 1):
-    diff = ticks_diff(l[i + 1][0], l[i][0])
-    assert diff > 0
-
-
-def edge_case(edge, offset):
-    h = utimeq(10)
-    add(h, ticks_add(0, offset))
-    add(h, ticks_add(edge, offset))
-    dprint(h)
-    l = pop_all(h)
-    diff = ticks_diff(l[1][0], l[0][0])
-    dprint(diff, diff > 0)
-    return diff
-
-
-dprint("===")
-diff = edge_case(MODULO_HALF - 1, 0)
-assert diff == MODULO_HALF - 1
-assert edge_case(MODULO_HALF - 1, 100) == diff
-assert edge_case(MODULO_HALF - 1, -100) == diff
-
-# We expect diff to be always positive, per the definition of heappop() which should return
-# the smallest value.
-# This is the edge case where this invariant breaks, due to asymmetry of two's-complement
-# range - there's one more negative integer than positive, so heappushing values like below
-# will then make ticks_diff() return the minimum negative value. We could make heappop
-# return them in a different order, but ticks_diff() result would be the same. Conclusion:
-# never add to a heap values where (a - b) == MODULO_HALF (and which are >= MODULO_HALF
-# ticks apart in real time of course).
-dprint("===")
-diff = edge_case(MODULO_HALF, 0)
-assert diff == -MODULO_HALF
-assert edge_case(MODULO_HALF, 100) == diff
-assert edge_case(MODULO_HALF, -100) == diff
-
-dprint("===")
-diff = edge_case(MODULO_HALF + 1, 0)
-assert diff == MODULO_HALF - 1
-assert edge_case(MODULO_HALF + 1, 100) == diff
-assert edge_case(MODULO_HALF + 1, -100) == diff
-
-print("OK")
diff --git a/tests/extmod/utimeq_stable.py b/tests/extmod/utimeq_stable.py
deleted file mode 100644
index 9fb522d5146..00000000000
--- a/tests/extmod/utimeq_stable.py
+++ /dev/null
@@ -1,22 +0,0 @@
-try:
-    from utimeq import utimeq
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-h = utimeq(10)
-
-# Check that for 2 same-key items, the queue is stable (pops items
-# in the same order they were pushed). Unfortunately, this no longer
-# holds for more same-key values, as the underlying heap structure
-# is not stable itself.
-h.push(100, 20, 0)
-h.push(100, 10, 0)
-
-res = [0, 0, 0]
-h.pop(res)
-assert res == [100, 20, 0]
-h.pop(res)
-assert res == [100, 10, 0]
-
-print("OK")
diff --git a/tests/extmod/uzlib_decompio.py b/tests/extmod/uzlib_decompio.py
deleted file mode 100644
index fae901aad0a..00000000000
--- a/tests/extmod/uzlib_decompio.py
+++ /dev/null
@@ -1,33 +0,0 @@
-try:
-    import uzlib as zlib
-    import uio as io
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-
-# Raw DEFLATE bitstream
-buf = io.BytesIO(b"\xcbH\xcd\xc9\xc9\x07\x00")
-inp = zlib.DecompIO(buf, -8)
-print(buf.seek(0, 1))
-print(inp.read(1))
-print(buf.seek(0, 1))
-print(inp.read(2))
-print(inp.read())
-print(buf.seek(0, 1))
-print(inp.read(1))
-print(inp.read())
-print(buf.seek(0, 1))
-
-
-# zlib bitstream (with 256 byte window size)
-inp = zlib.DecompIO(io.BytesIO(b"\x08\x9930\xa0=\x00\x00\xb3q\x12\xc1"))
-print(inp.read(10))
-print(inp.read())
-
-# zlib bitstream, wrong checksum
-inp = zlib.DecompIO(io.BytesIO(b"\x08\x9930\xa0=\x00\x00\xb3q\x12\xc0"))
-try:
-    print(inp.read())
-except OSError as e:
-    print(repr(e))
diff --git a/tests/extmod/uzlib_decompio.py.exp b/tests/extmod/uzlib_decompio.py.exp
deleted file mode 100644
index 3f5f360fa3e..00000000000
--- a/tests/extmod/uzlib_decompio.py.exp
+++ /dev/null
@@ -1,12 +0,0 @@
-0
-b'h'
-2
-b'el'
-b'lo'
-7
-b''
-b''
-7
-b'0000000000'
-b'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
-OSError(22,)
diff --git a/tests/extmod/uzlib_decompio_gz.py b/tests/extmod/uzlib_decompio_gz.py
deleted file mode 100644
index 1bc8ba885ba..00000000000
--- a/tests/extmod/uzlib_decompio_gz.py
+++ /dev/null
@@ -1,60 +0,0 @@
-try:
-    import uzlib as zlib
-    import uio as io
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-
-# gzip bitstream
-buf = io.BytesIO(
-    b"\x1f\x8b\x08\x08\x99\x0c\xe5W\x00\x03hello\x00\xcbH\xcd\xc9\xc9\x07\x00\x86\xa6\x106\x05\x00\x00\x00"
-)
-inp = zlib.DecompIO(buf, 16 + 8)
-print(buf.seek(0, 1))
-print(inp.read(1))
-print(buf.seek(0, 1))
-print(inp.read(2))
-print(inp.read())
-print(buf.seek(0, 1))
-print(inp.read(1))
-print(inp.read())
-print(buf.seek(0, 1))
-
-# Check FHCRC field
-buf = io.BytesIO(
-    b"\x1f\x8b\x08\x02\x99\x0c\xe5W\x00\x03\x00\x00\xcbH\xcd\xc9\xc9\x07\x00\x86\xa6\x106\x05\x00\x00\x00"
-)
-inp = zlib.DecompIO(buf, 16 + 8)
-print(inp.read())
-
-# Check FEXTRA field
-buf = io.BytesIO(
-    b"\x1f\x8b\x08\x04\x99\x0c\xe5W\x00\x03\x01\x00X\xcbH\xcd\xc9\xc9\x07\x00\x86\xa6\x106\x05\x00\x00\x00"
-)
-inp = zlib.DecompIO(buf, 16 + 8)
-print(inp.read())
-
-# broken header
-buf = io.BytesIO(
-    b"\x1f\x8c\x08\x08\x99\x0c\xe5W\x00\x03hello\x00\xcbH\xcd\xc9\xc9\x07\x00\x86\xa6\x106\x05\x00\x00\x00"
-)
-try:
-    inp = zlib.DecompIO(buf, 16 + 8)
-except ValueError:
-    print("ValueError")
-
-# broken crc32
-buf = io.BytesIO(
-    b"\x1f\x8b\x08\x08\x99\x0c\xe5W\x00\x03hello\x00\xcbH\xcd\xc9\xc9\x07\x00\x86\xa7\x106\x05\x00\x00\x00"
-)
-inp = zlib.DecompIO(buf, 16 + 8)
-try:
-    inp.read(6)
-except OSError as e:
-    print(repr(e))
-
-# broken uncompressed size - not checked so far
-# buf = io.BytesIO(b'\x1f\x8b\x08\x08\x99\x0c\xe5W\x00\x03hello\x00\xcbH\xcd\xc9\xc9\x07\x00\x86\xa6\x106\x06\x00\x00\x00')
-# inp = zlib.DecompIO(buf, 16 + 8)
-# inp.read(6)
diff --git a/tests/extmod/uzlib_decompio_gz.py.exp b/tests/extmod/uzlib_decompio_gz.py.exp
deleted file mode 100644
index 20a30c82a3d..00000000000
--- a/tests/extmod/uzlib_decompio_gz.py.exp
+++ /dev/null
@@ -1,13 +0,0 @@
-16
-b'h'
-18
-b'el'
-b'lo'
-31
-b''
-b''
-31
-b'hello'
-b'hello'
-ValueError
-OSError(22,)
diff --git a/tests/extmod/uzlib_decompress.py b/tests/extmod/uzlib_decompress.py
deleted file mode 100644
index 8d4a9640b47..00000000000
--- a/tests/extmod/uzlib_decompress.py
+++ /dev/null
@@ -1,60 +0,0 @@
-try:
-    import zlib
-except ImportError:
-    try:
-        import uzlib as zlib
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-PATTERNS = [
-    # Packed results produced by CPy's zlib.compress()
-    (b"0", b"x\x9c3\x00\x00\x001\x001"),
-    (b"a", b"x\x9cK\x04\x00\x00b\x00b"),
-    (b"0" * 100, b"x\x9c30\xa0=\x00\x00\xb3q\x12\xc1"),
-    (
-        bytes(range(64)),
-        b"x\x9cc`dbfaec\xe7\xe0\xe4\xe2\xe6\xe1\xe5\xe3\x17\x10\x14\x12\x16\x11\x15\x13\x97\x90\x94\x92\x96\x91\x95\x93WPTRVQUS\xd7\xd0\xd4\xd2\xd6\xd1\xd5\xd370426153\xb7\xb0\xb4\xb2\xb6\xb1\xb5\xb3\x07\x00\xaa\xe0\x07\xe1",
-    ),
-    (b"hello", b"x\x01\x01\x05\x00\xfa\xffhello\x06,\x02\x15"),  # compression level 0
-    # adaptive/dynamic huffman tree
-    (
-        b"13371813150|13764518736|12345678901",
-        b"x\x9c\x05\xc1\x81\x01\x000\x04\x04\xb1\x95\\\x1f\xcfn\x86o\x82d\x06Qq\xc8\x9d\xc5X}I}\x00\x951D>I}\x00\x951D>I}\x00\x951D>I}\x00\x951D",
-        b"x\x9c\x05\xc11\x01\x00\x00\x00\x010\x95\x14py\x84\x12C_\x9bR\x8cV\x8a\xd1J1Z)F\x1fw`\x089",
-    ),
-]
-
-for unpacked, packed in PATTERNS:
-    assert zlib.decompress(packed) == unpacked
-    print(unpacked)
-
-
-# Raw DEFLATE bitstream
-v = b"\xcbH\xcd\xc9\xc9\x07\x00"
-exp = b"hello"
-out = zlib.decompress(v, -15)
-assert out == exp
-print(exp)
-# Even when you ask CPython zlib.compress to produce Raw DEFLATE stream,
-# it returns it with adler2 and oriignal size appended, as if it was a
-# zlib stream. Make sure there're no random issues decompressing such.
-v = b"\xcbH\xcd\xc9\xc9\x07\x00\x86\xa6\x106\x05\x00\x00\x00"
-out = zlib.decompress(v, -15)
-assert out == exp
-
-# this should error
-try:
-    zlib.decompress(b"abc")
-except Exception:
-    print("Exception")
-
-# invalid block type
-try:
-    zlib.decompress(b"\x07", -15)  # final-block, block-type=3 (invalid)
-except Exception as er:
-    print("Exception")
diff --git a/tests/extmod/vfs_basic.py b/tests/extmod/vfs_basic.py
index 9a9ef2ca61b..e88df1a776d 100644
--- a/tests/extmod/vfs_basic.py
+++ b/tests/extmod/vfs_basic.py
@@ -1,10 +1,8 @@
 # test VFS functionality without any particular filesystem type
 
 try:
-    import uos
-
-    uos.mount
-except (ImportError, AttributeError):
+    import os, vfs
+except ImportError:
     print("SKIP")
     raise SystemExit
 
@@ -21,11 +19,11 @@ def umount(self):
         print(self.id, "umount")
 
     def ilistdir(self, dir):
-        print(self.id, "ilistdir", dir)
+        print(self.id, "ilistdir", repr(dir))
         return iter([("a%d" % self.id, 0, 0)])
 
     def chdir(self, dir):
-        print(self.id, "chdir", dir)
+        print(self.id, "chdir", repr(dir))
         if self.fail:
             raise OSError(self.fail)
 
@@ -34,23 +32,23 @@ def getcwd(self):
         return "dir%d" % self.id
 
     def mkdir(self, path):
-        print(self.id, "mkdir", path)
+        print(self.id, "mkdir", repr(path))
 
     def remove(self, path):
-        print(self.id, "remove", path)
+        print(self.id, "remove", repr(path))
 
     def rename(self, old_path, new_path):
-        print(self.id, "rename", old_path, new_path)
+        print(self.id, "rename", repr(old_path), repr(new_path))
 
     def rmdir(self, path):
-        print(self.id, "rmdir", path)
+        print(self.id, "rmdir", repr(path))
 
     def stat(self, path):
-        print(self.id, "stat", path)
-        return (self.id,)
+        print(self.id, "stat", repr(path))
+        return (self.id, 0, 0, 0, 0, 0, 0, 0, 0, 0)
 
     def statvfs(self, path):
-        print(self.id, "statvfs", path)
+        print(self.id, "statvfs", repr(path))
         return (self.id,)
 
     def open(self, file, mode):
@@ -59,35 +57,35 @@ def open(self, file, mode):
 
 # first we umount any existing mount points the target may have
 try:
-    uos.umount("/")
+    vfs.umount("/")
 except OSError:
     pass
-for path in uos.listdir("/"):
-    uos.umount("/" + path)
+for path in os.listdir("/"):
+    vfs.umount("/" + path)
 
 # stat root dir
-print(uos.stat("/"))
+print(tuple(os.stat("/")))
 
 # statvfs root dir; verify that f_namemax has a sensible size
-print(uos.statvfs("/")[9] >= 32)
+print(os.statvfs("/")[9] >= 32)
 
 # getcwd when in root dir
-print(uos.getcwd())
+print(os.getcwd())
 
 # test operations on the root directory with nothing mounted, they should all fail
 for func in ("chdir", "listdir", "mkdir", "remove", "rmdir", "stat"):
     for arg in ("x", "/x"):
         try:
-            getattr(uos, func)(arg)
+            getattr(os, func)(arg)
         except OSError:
             print(func, arg, "OSError")
 
 # basic mounting and listdir
-uos.mount(Filesystem(1), "/test_mnt")
-print(uos.listdir())
+vfs.mount(Filesystem(1), "/test_mnt")
+print(os.listdir())
 
 # ilistdir
-i = uos.ilistdir()
+i = os.ilistdir()
 print(next(i))
 try:
     next(i)
@@ -99,88 +97,88 @@ def open(self, file, mode):
     print("StopIteration")
 
 # referencing the mount point in different ways
-print(uos.listdir("test_mnt"))
-print(uos.listdir("/test_mnt"))
+print(os.listdir("test_mnt"))
+print(os.listdir("/test_mnt"))
 
 # mounting another filesystem
-uos.mount(Filesystem(2), "/test_mnt2", readonly=True)
-print(uos.listdir())
-print(uos.listdir("/test_mnt2"))
+vfs.mount(Filesystem(2), "/test_mnt2", readonly=True)
+print(os.listdir())
+print(os.listdir("/test_mnt2"))
 
 # mounting over an existing mount point
 try:
-    uos.mount(Filesystem(3), "/test_mnt2")
+    vfs.mount(Filesystem(3), "/test_mnt2")
 except OSError:
     print("OSError")
 
 # mkdir of a mount point
 try:
-    uos.mkdir("/test_mnt")
+    os.mkdir("/test_mnt")
 except OSError:
     print("OSError")
 
 # rename across a filesystem
 try:
-    uos.rename("/test_mnt/a", "/test_mnt2/b")
+    os.rename("/test_mnt/a", "/test_mnt2/b")
 except OSError:
     print("OSError")
 
 # delegating to mounted filesystem
-uos.chdir("test_mnt")
-print(uos.listdir())
-print(uos.getcwd())
-uos.mkdir("test_dir")
-uos.remove("test_file")
-uos.rename("test_file", "test_file2")
-uos.rmdir("test_dir")
-print(uos.stat("test_file"))
-print(uos.statvfs("/test_mnt"))
+os.chdir("test_mnt")
+print(os.listdir())
+print(os.getcwd())
+os.mkdir("test_dir")
+os.remove("test_file")
+os.rename("test_file", "test_file2")
+os.rmdir("test_dir")
+print(tuple(os.stat("test_file")))
+print(os.statvfs("/test_mnt"))
 open("test_file")
 open("test_file", "wb")
 
 # umount
-uos.umount("/test_mnt")
-uos.umount("/test_mnt2")
+vfs.umount("/test_mnt")
+vfs.umount("/test_mnt2")
 
 # umount a non-existent mount point
 try:
-    uos.umount("/test_mnt")
+    vfs.umount("/test_mnt")
 except OSError:
     print("OSError")
 
 # root dir
-uos.mount(Filesystem(3), "/")
-print(uos.stat("/"))
-print(uos.statvfs("/"))
-print(uos.listdir())
+vfs.mount(Filesystem(3), "/")
+print(tuple(os.stat("/")))
+print(os.statvfs("/"))
+print(os.listdir())
 open("test")
 
-uos.mount(Filesystem(4), "/mnt")
-print(uos.listdir())
-print(uos.listdir("/mnt"))
-uos.chdir("/mnt")
-print(uos.listdir())
+vfs.mount(Filesystem(4), "/mnt")
+print(os.listdir())
+print(os.listdir("/mnt"))
+os.chdir("/mnt")
+print(os.listdir())
 
 # chdir to a subdir within root-mounted vfs, and then listdir
-uos.chdir("/subdir")
-print(uos.listdir())
-uos.chdir("/")
+os.chdir("/subdir")
+print(os.listdir())
+os.chdir("/")
 
-uos.umount("/")
-print(uos.listdir("/"))
-uos.umount("/mnt")
+vfs.umount("/")
+print(os.listdir("/"))
+vfs.umount("/mnt")
 
 # chdir to a non-existent mount point (current directory should remain unchanged)
 try:
-    uos.chdir("/foo")
+    os.chdir("/foo")
 except OSError:
     print("OSError")
-print(uos.getcwd())
+print(os.getcwd())
 
 # chdir to a non-existent subdirectory in a mounted filesystem
-uos.mount(Filesystem(5, 1), "/mnt")
+vfs.mount(Filesystem(5, 1), "/mnt")
 try:
-    uos.chdir("/mnt/subdir")
+    os.chdir("/mnt/subdir")
 except OSError:
     print("OSError")
-print(uos.getcwd())
+print(os.getcwd())
diff --git a/tests/extmod/vfs_basic.py.exp b/tests/extmod/vfs_basic.py.exp
index 536bb4c805d..39a16c444ee 100644
--- a/tests/extmod/vfs_basic.py.exp
+++ b/tests/extmod/vfs_basic.py.exp
@@ -18,30 +18,30 @@ stat /x OSError
 ('test_mnt', 16384, 0)
 StopIteration
 StopIteration
-1 ilistdir /
+1 ilistdir '/'
 ['a1']
-1 ilistdir /
+1 ilistdir '/'
 ['a1']
 2 mount True False
 ['test_mnt', 'test_mnt2']
-2 ilistdir /
+2 ilistdir '/'
 ['a2']
 3 mount False False
 OSError
 OSError
 OSError
-1 chdir /
-1 ilistdir 
+1 chdir '/'
+1 ilistdir ''
 ['a1']
 1 getcwd
 /test_mntdir1
-1 mkdir test_dir
-1 remove test_file
-1 rename test_file test_file2
-1 rmdir test_dir
-1 stat test_file
-(1,)
-1 statvfs /
+1 mkdir 'test_dir'
+1 remove 'test_file'
+1 rename 'test_file' 'test_file2'
+1 rmdir 'test_dir'
+1 stat 'test_file'
+(1, 0, 0, 0, 0, 0, 0, 0, 0, 0)
+1 statvfs '/'
 (1,)
 1 open test_file r
 1 open test_file wb
@@ -50,29 +50,29 @@ OSError
 OSError
 3 mount False False
 (16384, 0, 0, 0, 0, 0, 0, 0, 0, 0)
-3 statvfs /
+3 statvfs '/'
 (3,)
-3 ilistdir /
+3 ilistdir '/'
 ['a3']
 3 open test r
 4 mount False False
-3 ilistdir /
+3 ilistdir '/'
 ['mnt', 'a3']
-4 ilistdir /
+4 ilistdir '/'
 ['a4']
-4 chdir /
-4 ilistdir 
+4 chdir '/'
+4 ilistdir ''
 ['a4']
-3 chdir /subdir
-3 ilistdir 
+3 chdir '/subdir'
+3 ilistdir ''
 ['a3']
-3 chdir /
+3 chdir '/'
 3 umount
 ['mnt']
 4 umount
 OSError
 /
 5 mount False False
-5 chdir /subdir
+5 chdir '/subdir'
 OSError
 /
diff --git a/tests/extmod/vfs_blockdev.py b/tests/extmod/vfs_blockdev.py
index e24169ba936..a1f07499627 100644
--- a/tests/extmod/vfs_blockdev.py
+++ b/tests/extmod/vfs_blockdev.py
@@ -1,10 +1,10 @@
 # Test for behaviour of combined standard and extended block device
 
 try:
-    import uos
+    import vfs
 
-    uos.VfsFat
-    uos.VfsLfs2
+    vfs.VfsFat
+    vfs.VfsLfs2
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
@@ -70,5 +70,5 @@ def test(bdev, vfs_class):
     print("SKIP")
     raise SystemExit
 
-test(bdev, uos.VfsFat)
-test(bdev, uos.VfsLfs2)
+test(bdev, vfs.VfsFat)
+test(bdev, vfs.VfsLfs2)
diff --git a/tests/extmod/vfs_blockdev_invalid.py b/tests/extmod/vfs_blockdev_invalid.py
new file mode 100644
index 00000000000..3cb97ac3144
--- /dev/null
+++ b/tests/extmod/vfs_blockdev_invalid.py
@@ -0,0 +1,133 @@
+# Tests where the block device returns invalid values
+
+try:
+    import vfs
+
+    vfs.VfsFat
+    vfs.VfsLfs2
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+
+class RAMBlockDevice:
+    ERASE_BLOCK_SIZE = 512
+
+    def __init__(self, blocks):
+        self.data = bytearray(blocks * self.ERASE_BLOCK_SIZE)
+        self.read_res = 0
+        self.write_res = 0
+
+    def readblocks(self, block, buf, off=0):
+        # print("readblocks", block, len(buf), off)
+        addr = block * self.ERASE_BLOCK_SIZE + off
+        for i in range(len(buf)):
+            buf[i] = self.data[addr + i]
+        return self.read_res
+
+    def writeblocks(self, block, buf, off=None):
+        if off is None:
+            # erase, then write
+            off = 0
+        addr = block * self.ERASE_BLOCK_SIZE + off
+        for i in range(len(buf)):
+            self.data[addr + i] = buf[i]
+        return self.write_res
+
+    def ioctl(self, op, arg):
+        if op == 4:  # block count
+            return len(self.data) // self.ERASE_BLOCK_SIZE
+        if op == 5:  # block size
+            return self.ERASE_BLOCK_SIZE
+        if op == 6:  # erase block
+            return 0
+
+
+try:
+    bdev = RAMBlockDevice(50)
+except MemoryError:
+    print("SKIP-TOO-LARGE")
+    raise SystemExit
+
+
+ERROR_EIO = (OSError, "[Errno 5] EIO")
+ERROR_EINVAL = (OSError, "[Errno 22] EINVAL")
+ERROR_TYPE = (TypeError, "can't convert str to int")
+ALL_ERROR_TYPES = (OSError, TypeError)
+
+
+def test(vfs_class, test_data):
+    bdev.read_res = 0  # reset function results
+    bdev.write_res = 0
+
+    vfs_class.mkfs(bdev)
+    fs = vfs_class(bdev)
+
+    with fs.open("test", "w") as f:
+        f.write("a" * 64)
+
+    for res, error_open, error_read in test_data:
+        # This variant will fail on open
+        bdev.read_res = res
+        try:
+            with fs.open("test", "r") as f:
+                assert error_open is None
+        except ALL_ERROR_TYPES as e:
+            assert error_open is not None
+            assert (type(e), str(e)) == error_open
+
+        # This variant should succeed on open, may fail on read
+        # unless the filesystem cached the contents already
+        bdev.read_res = 0
+        try:
+            with fs.open("test", "r") as f:
+                bdev.read_res = res
+                assert f.read(1) == "a"
+                assert f.read() == "a" * 63
+                assert error_read is None
+        except ALL_ERROR_TYPES as e:
+            assert error_read is not None
+            assert (type(e), str(e)) == error_read
+
+        # Try mounting this block device
+        #
+        # Failing mount operation will return EIO rather than EINVAL, but otherwise
+        # the result should match error_open
+        error_mount = ERROR_EIO if error_open == ERROR_EINVAL else error_open
+        try:
+            vfs.mount(bdev, "/test_ram")
+            assert error_mount is None
+        except ALL_ERROR_TYPES as e:
+            assert error_mount is not None
+            assert (type(e), str(e)) == error_mount
+        finally:
+            try:
+                vfs.umount("/test_ram")
+            except OSError:
+                pass
+
+
+try:
+    test(
+        vfs.VfsLfs2,
+        (
+            (0, None, None),
+            (-5, ERROR_EIO, None),
+            (5, ERROR_EINVAL, None),
+            (33, ERROR_EINVAL, None),
+            ("invalid", ERROR_TYPE, None),
+        ),
+    )
+    test(
+        vfs.VfsFat,
+        (
+            (0, None, None),
+            (-5, ERROR_EIO, ERROR_EIO),
+            (5, ERROR_EIO, ERROR_EIO),
+            (33, ERROR_EIO, ERROR_EIO),
+            ("invalid", ERROR_TYPE, ERROR_TYPE),
+        ),
+    )
+    print("OK")
+except MemoryError:
+    print("SKIP-TOO-LARGE")
diff --git a/tests/extmod/utimeq1.py.exp b/tests/extmod/vfs_blockdev_invalid.py.exp
similarity index 100%
rename from tests/extmod/utimeq1.py.exp
rename to tests/extmod/vfs_blockdev_invalid.py.exp
diff --git a/tests/extmod/vfs_blockdev_invalid2.py b/tests/extmod/vfs_blockdev_invalid2.py
new file mode 100644
index 00000000000..8905f833cf7
--- /dev/null
+++ b/tests/extmod/vfs_blockdev_invalid2.py
@@ -0,0 +1,37 @@
+# Tests where the block device returns invalid values
+
+try:
+    import vfs
+
+    vfs.VfsFat
+    memoryview
+except (NameError, ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+
+class BadDev:
+    SEC_SIZE = 512
+
+    def __init__(self, blocks):
+        self.blocks = blocks
+
+    def readblocks(self, n, buf):
+        assert len(buf) == self.SEC_SIZE
+        buf[:] = bytearray(self.SEC_SIZE + 1)  # Attempts to enlarge passed-in buf
+
+    def writeblocks(self, n, buf):
+        pass
+
+    def ioctl(self, op, arg):
+        if op == 4:  # MP_BLOCKDEV_IOCTL_BLOCK_COUNT
+            return self.blocks
+        if op == 5:  # MP_BLOCKDEV_IOCTL_BLOCK_SIZE
+            return self.SEC_SIZE
+
+
+bdev = BadDev(512)
+try:
+    vfs.VfsFat.mkfs(bdev)
+except ValueError as e:
+    print("ValueError")
diff --git a/tests/extmod/vfs_blockdev_invalid2.py.exp b/tests/extmod/vfs_blockdev_invalid2.py.exp
new file mode 100644
index 00000000000..94274de1bb3
--- /dev/null
+++ b/tests/extmod/vfs_blockdev_invalid2.py.exp
@@ -0,0 +1 @@
+ValueError
diff --git a/tests/extmod/vfs_fat_fileio1.py b/tests/extmod/vfs_fat_fileio1.py
index 55f399ff2ea..5221d405323 100644
--- a/tests/extmod/vfs_fat_fileio1.py
+++ b/tests/extmod/vfs_fat_fileio1.py
@@ -1,13 +1,8 @@
 try:
-    import uerrno
-    import uos
-except ImportError:
-    print("SKIP")
-    raise SystemExit
+    import errno, os, vfs
 
-try:
-    uos.VfsFat
-except AttributeError:
+    vfs.VfsFat
+except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
@@ -38,14 +33,14 @@ def ioctl(self, op, arg):
 
 try:
     bdev = RAMFS(50)
-    uos.VfsFat.mkfs(bdev)
+    vfs.VfsFat.mkfs(bdev)
 except MemoryError:
     print("SKIP")
     raise SystemExit
 
-vfs = uos.VfsFat(bdev)
-uos.mount(vfs, "/ramdisk")
-uos.chdir("/ramdisk")
+fs = vfs.VfsFat(bdev)
+vfs.mount(fs, "/ramdisk")
+os.chdir("/ramdisk")
 
 # file IO
 f = open("foo_file.txt", "w")
@@ -57,22 +52,22 @@ def ioctl(self, op, arg):
 try:
     f.write("world!")
 except OSError as e:
-    print(e.errno == uerrno.EINVAL)
+    print(e.errno == errno.EINVAL)
 
 try:
     f.read()
 except OSError as e:
-    print(e.errno == uerrno.EINVAL)
+    print(e.errno == errno.EINVAL)
 
 try:
     f.flush()
 except OSError as e:
-    print(e.errno == uerrno.EINVAL)
+    print(e.errno == errno.EINVAL)
 
 try:
     open("foo_file.txt", "x")
 except OSError as e:
-    print(e.errno == uerrno.EEXIST)
+    print(e.errno == errno.EEXIST)
 
 with open("foo_file.txt", "a") as f:
     f.write("world!")
@@ -99,12 +94,12 @@ def ioctl(self, op, arg):
 #    print(f.read())
 
 # dirs
-vfs.mkdir("foo_dir")
+fs.mkdir("foo_dir")
 
 try:
-    vfs.rmdir("foo_file.txt")
+    fs.rmdir("foo_file.txt")
 except OSError as e:
-    print(e.errno == 20)  # uerrno.ENOTDIR
+    print(e.errno == 20)  # errno.ENOTDIR
 
-vfs.remove("foo_file.txt")
-print(list(vfs.ilistdir()))
+fs.remove("foo_file.txt")
+print(list(fs.ilistdir()))
diff --git a/tests/extmod/vfs_fat_fileio2.py b/tests/extmod/vfs_fat_fileio2.py
index 9429f115f62..bbd6298c496 100644
--- a/tests/extmod/vfs_fat_fileio2.py
+++ b/tests/extmod/vfs_fat_fileio2.py
@@ -1,13 +1,8 @@
 try:
-    import uerrno
-    import uos
-except ImportError:
-    print("SKIP")
-    raise SystemExit
+    import errno, os, vfs
 
-try:
-    uos.VfsFat
-except AttributeError:
+    vfs.VfsFat
+except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
@@ -38,34 +33,34 @@ def ioctl(self, op, arg):
 
 try:
     bdev = RAMFS(50)
-    uos.VfsFat.mkfs(bdev)
+    vfs.VfsFat.mkfs(bdev)
 except MemoryError:
     print("SKIP")
     raise SystemExit
 
-vfs = uos.VfsFat(bdev)
-uos.mount(vfs, "/ramdisk")
-uos.chdir("/ramdisk")
+fs = vfs.VfsFat(bdev)
+vfs.mount(fs, "/ramdisk")
+os.chdir("/ramdisk")
 
 try:
-    vfs.mkdir("foo_dir")
+    fs.mkdir("foo_dir")
 except OSError as e:
-    print(e.errno == uerrno.EEXIST)
+    print(e.errno == errno.EEXIST)
 
 try:
-    vfs.remove("foo_dir")
+    fs.remove("foo_dir")
 except OSError as e:
-    print(e.errno == uerrno.EISDIR)
+    print(e.errno == errno.EISDIR)
 
 try:
-    vfs.remove("no_file.txt")
+    fs.remove("no_file.txt")
 except OSError as e:
-    print(e.errno == uerrno.ENOENT)
+    print(e.errno == errno.ENOENT)
 
 try:
-    vfs.rename("foo_dir", "/null/file")
+    fs.rename("foo_dir", "/null/file")
 except OSError as e:
-    print(e.errno == uerrno.ENOENT)
+    print(e.errno == errno.ENOENT)
 
 # file in dir
 with open("foo_dir/file-in-dir.txt", "w+t") as f:
@@ -79,36 +74,36 @@ def ioctl(self, op, arg):
 
 # directory not empty
 try:
-    vfs.rmdir("foo_dir")
+    fs.rmdir("foo_dir")
 except OSError as e:
-    print(e.errno == uerrno.EACCES)
+    print(e.errno == errno.EACCES)
 
 # trim full path
-vfs.rename("foo_dir/file-in-dir.txt", "foo_dir/file.txt")
-print(list(vfs.ilistdir("foo_dir")))
+fs.rename("foo_dir/file-in-dir.txt", "foo_dir/file.txt")
+print(list(fs.ilistdir("foo_dir")))
 
-vfs.rename("foo_dir/file.txt", "moved-to-root.txt")
-print(list(vfs.ilistdir()))
+fs.rename("foo_dir/file.txt", "moved-to-root.txt")
+print(list(fs.ilistdir()))
 
 # check that renaming to existing file will overwrite it
 with open("temp", "w") as f:
     f.write("new text")
-vfs.rename("temp", "moved-to-root.txt")
-print(list(vfs.ilistdir()))
+fs.rename("temp", "moved-to-root.txt")
+print(list(fs.ilistdir()))
 with open("moved-to-root.txt") as f:
     print(f.read())
 
 # valid removes
-vfs.remove("foo_dir/sub_file.txt")
-vfs.rmdir("foo_dir")
-print(list(vfs.ilistdir()))
+fs.remove("foo_dir/sub_file.txt")
+fs.rmdir("foo_dir")
+print(list(fs.ilistdir()))
 
 # disk full
 try:
-    bsize = vfs.statvfs("/ramdisk")[0]
-    free = vfs.statvfs("/ramdisk")[2] + 1
+    bsize = fs.statvfs("/ramdisk")[0]
+    free = fs.statvfs("/ramdisk")[2] + 1
     f = open("large_file.txt", "wb")
     f.write(bytearray(bsize * free))
 except OSError as e:
-    print("ENOSPC:", e.errno == 28)  # uerrno.ENOSPC
+    print("ENOSPC:", e.errno == 28)  # errno.ENOSPC
 f.close()
diff --git a/tests/extmod/vfs_fat_finaliser.py b/tests/extmod/vfs_fat_finaliser.py
index b38e640c73d..7be3cda1a35 100644
--- a/tests/extmod/vfs_fat_finaliser.py
+++ b/tests/extmod/vfs_fat_finaliser.py
@@ -1,9 +1,9 @@
 # Test VfsFat class and its finaliser
 
 try:
-    import uerrno, uos
+    import errno, os, vfs
 
-    uos.VfsFat
+    vfs.VfsFat
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
@@ -31,14 +31,16 @@ def ioctl(self, op, arg):
 
 # Create block device, and skip test if not enough RAM
 try:
+    import errno, os
+
     bdev = RAMBlockDevice(50)
 except MemoryError:
     print("SKIP")
     raise SystemExit
 
 # Format block device and create VFS object
-uos.VfsFat.mkfs(bdev)
-vfs = uos.VfsFat(bdev)
+vfs.VfsFat.mkfs(bdev)
+fs = vfs.VfsFat(bdev)
 
 # Here we test that opening a file with the heap locked fails correctly.  This
 # is a special case because file objects use a finaliser and allocating with a
@@ -48,7 +50,9 @@ def ioctl(self, op, arg):
 
 micropython.heap_lock()
 try:
-    vfs.open("x", "r")
+    import errno, os
+
+    fs.open("x", "r")
 except MemoryError:
     print("MemoryError")
 micropython.heap_unlock()
@@ -60,7 +64,7 @@ def ioctl(self, op, arg):
 # in turn allocate new qstrs and/or a new qstr pool).
 f = None
 n = None
-names = ["x%d" % i for i in range(4)]
+names = ["x%d" % i for i in range(5)]
 
 # Do a large number of single-block allocations to move the GC head forwards,
 # ensuring that the files are allocated from never-before-used blocks and
@@ -70,12 +74,13 @@ def ioctl(self, op, arg):
     []
 
 # Run the test: create files without closing them, run GC, then read back files.
+# Only read back N-1 files because the last one may not be finalised due to
+# references to it being left on the C stack.
 for n in names:
-    f = vfs.open(n, "w")
+    f = fs.open(n, "w")
     f.write(n)
     f = None  # release f without closing
-    sorted([0, 1, 2, 3], key=lambda x: x)  # use up Python and C stack so f is really gone
-gc.collect()  # should finalise all N files by closing them
-for n in names:
-    with vfs.open(n, "r") as f:
+gc.collect()  # should finalise at least the first N-1 files by closing them
+for n in names[:-1]:
+    with fs.open(n, "r") as f:
         print(f.read())
diff --git a/tests/extmod/vfs_fat_ilistdir_del.py b/tests/extmod/vfs_fat_ilistdir_del.py
index ccdacc57c25..964e6b12868 100644
--- a/tests/extmod/vfs_fat_ilistdir_del.py
+++ b/tests/extmod/vfs_fat_ilistdir_del.py
@@ -1,10 +1,9 @@
 # Test ilistdir __del__ for VfsFat using a RAM device.
-import gc
 
 try:
-    import uos
+    import gc, os, vfs
 
-    uos.VfsFat
+    vfs.VfsFat
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
@@ -37,29 +36,29 @@ def ioctl(self, op, arg):
 
 def test(bdev, vfs_class):
     vfs_class.mkfs(bdev)
-    vfs = vfs_class(bdev)
-    vfs.mkdir("/test_d1")
-    vfs.mkdir("/test_d2")
-    vfs.mkdir("/test_d3")
+    fs = vfs_class(bdev)
+    fs.mkdir("/test_d1")
+    fs.mkdir("/test_d2")
+    fs.mkdir("/test_d3")
 
     for i in range(10):
         print(i)
 
         # We want to partially iterate the ilistdir iterator to leave it in an
         # open state, which will then test the finaliser when it's garbage collected.
-        idir = vfs.ilistdir("/")
+        idir = fs.ilistdir("/")
         print(any(idir))
 
         # Alternate way of partially iterating the ilistdir object, modifying the
         # filesystem while it's open.
-        for dname, *_ in vfs.ilistdir("/"):
-            vfs.rmdir(dname)
+        for dname, *_ in fs.ilistdir("/"):
+            fs.rmdir(dname)
             break
-        vfs.mkdir(dname)
+        fs.mkdir(dname)
 
-        # Also create a fully drained iterator and ensure trying to re-use it
+        # Also create a fully drained iterator and ensure trying to reuse it
         # throws the correct exception.
-        idir_emptied = vfs.ilistdir("/")
+        idir_emptied = fs.ilistdir("/")
         l = list(idir_emptied)
         print(len(l))
         try:
@@ -68,7 +67,7 @@ def test(bdev, vfs_class):
             pass
 
         gc.collect()
-        vfs.open("/test", "w").close()
+        fs.open("/test", "w").close()
 
 
 try:
@@ -77,4 +76,4 @@ def test(bdev, vfs_class):
     print("SKIP")
     raise SystemExit
 
-test(bdev, uos.VfsFat)
+test(bdev, vfs.VfsFat)
diff --git a/tests/extmod/vfs_fat_more.py b/tests/extmod/vfs_fat_more.py
index 981c2524a6e..1d755f9c55c 100644
--- a/tests/extmod/vfs_fat_more.py
+++ b/tests/extmod/vfs_fat_more.py
@@ -1,12 +1,8 @@
 try:
-    import uos
-except ImportError:
-    print("SKIP")
-    raise SystemExit
+    import os, vfs
 
-try:
-    uos.VfsFat
-except AttributeError:
+    vfs.VfsFat
+except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
@@ -44,76 +40,76 @@ def ioctl(self, op, arg):
 
 # first we umount any existing mount points the target may have
 try:
-    uos.umount("/")
+    vfs.umount("/")
 except OSError:
     pass
-for path in uos.listdir("/"):
-    uos.umount("/" + path)
+for path in os.listdir("/"):
+    vfs.umount("/" + path)
 
-uos.VfsFat.mkfs(bdev)
-uos.mount(bdev, "/")
+vfs.VfsFat.mkfs(bdev)
+vfs.mount(bdev, "/")
 
-print(uos.getcwd())
+print(os.getcwd())
 
 f = open("test.txt", "w")
 f.write("hello")
 f.close()
 
-print(uos.listdir())
-print(uos.listdir("/"))
-print(uos.stat("")[:-3])
-print(uos.stat("/")[:-3])
-print(uos.stat("test.txt")[:-3])
-print(uos.stat("/test.txt")[:-3])
+print(os.listdir())
+print(os.listdir("/"))
+print(os.stat("")[:-3])
+print(os.stat("/")[:-3])
+print(os.stat("test.txt")[:-3])
+print(os.stat("/test.txt")[:-3])
 
 f = open("/test.txt")
 print(f.read())
 f.close()
 
-uos.rename("test.txt", "test2.txt")
-print(uos.listdir())
-uos.rename("test2.txt", "/test3.txt")
-print(uos.listdir())
-uos.rename("/test3.txt", "test4.txt")
-print(uos.listdir())
-uos.rename("/test4.txt", "/test5.txt")
-print(uos.listdir())
-
-uos.mkdir("dir")
-print(uos.listdir())
-uos.mkdir("/dir2")
-print(uos.listdir())
-uos.mkdir("dir/subdir")
-print(uos.listdir("dir"))
+os.rename("test.txt", "test2.txt")
+print(os.listdir())
+os.rename("test2.txt", "/test3.txt")
+print(os.listdir())
+os.rename("/test3.txt", "test4.txt")
+print(os.listdir())
+os.rename("/test4.txt", "/test5.txt")
+print(os.listdir())
+
+os.mkdir("dir")
+print(os.listdir())
+os.mkdir("/dir2")
+print(os.listdir())
+os.mkdir("dir/subdir")
+print(os.listdir("dir"))
 for exist in ("", "/", "dir", "/dir", "dir/subdir"):
     try:
-        uos.mkdir(exist)
+        os.mkdir(exist)
     except OSError as er:
         print("mkdir OSError", er.errno == 17)  # EEXIST
 
-uos.chdir("/")
-print(uos.stat("test5.txt")[:-3])
+os.chdir("/")
+print(os.stat("test5.txt")[:-3])
 
-uos.VfsFat.mkfs(bdev2)
-uos.mount(bdev2, "/sys")
-print(uos.listdir())
-print(uos.listdir("sys"))
-print(uos.listdir("/sys"))
+vfs.VfsFat.mkfs(bdev2)
+vfs.mount(bdev2, "/sys")
+print(os.listdir())
+print(os.listdir("sys"))
+print(os.listdir("/sys"))
 
-uos.rmdir("dir2")
-uos.remove("test5.txt")
-print(uos.listdir())
+os.rmdir("dir2")
+os.remove("test5.txt")
+print(os.listdir())
 
-uos.umount("/")
-print(uos.getcwd())
-print(uos.listdir())
-print(uos.listdir("sys"))
+vfs.umount("/")
+print(os.getcwd())
+print(os.listdir())
+print(os.listdir("sys"))
 
 # test importing a file from a mounted FS
-import usys
+import sys
 
-usys.path.clear()
-usys.path.append("/sys")
+sys.path.clear()
+sys.path.append("/sys")
 with open("sys/test_module.py", "w") as f:
     f.write('print("test_module!")')
 import test_module
diff --git a/tests/extmod/vfs_fat_mtime.py b/tests/extmod/vfs_fat_mtime.py
index d8fd66b75f7..0c7e10a5486 100644
--- a/tests/extmod/vfs_fat_mtime.py
+++ b/tests/extmod/vfs_fat_mtime.py
@@ -1,11 +1,11 @@
 # Test for VfsFat using a RAM device, mtime feature
 
 try:
-    import utime, uos
+    import time, os, vfs
 
-    utime.time
-    utime.sleep
-    uos.VfsFat
+    time.time
+    time.sleep
+    vfs.VfsFat
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
@@ -41,21 +41,21 @@ def test(bdev, vfs_class):
     vfs_class.mkfs(bdev)
 
     # construction
-    vfs = vfs_class(bdev)
+    fs = vfs_class(bdev)
 
     # Create an empty file, should have a timestamp.
-    current_time = int(utime.time())
-    vfs.open("test1", "wt").close()
+    current_time = int(time.time())
+    fs.open("test1", "wt").close()
 
     # Wait 2 seconds so mtime will increase (FAT has 2 second resolution).
-    utime.sleep(2)
+    time.sleep(2)
 
     # Create another empty file, should have a timestamp.
-    vfs.open("test2", "wt").close()
+    fs.open("test2", "wt").close()
 
     # Stat the files and check mtime is non-zero.
-    stat1 = vfs.stat("test1")
-    stat2 = vfs.stat("test2")
+    stat1 = fs.stat("test1")
+    stat2 = fs.stat("test2")
     print(stat1[8] != 0, stat2[8] != 0)
 
     # Check that test1 has mtime which matches time.time() at point of creation.
@@ -67,8 +67,8 @@ def test(bdev, vfs_class):
     print(stat1[8] < stat2[8])
 
     # Unmount.
-    vfs.umount()
+    fs.umount()
 
 
 bdev = RAMBlockDevice(50)
-test(bdev, uos.VfsFat)
+test(bdev, vfs.VfsFat)
diff --git a/tests/extmod/vfs_fat_oldproto.py b/tests/extmod/vfs_fat_oldproto.py
index b1556ba2c0e..2b5e4d00828 100644
--- a/tests/extmod/vfs_fat_oldproto.py
+++ b/tests/extmod/vfs_fat_oldproto.py
@@ -1,13 +1,8 @@
 try:
-    import uerrno
-    import uos
-except ImportError:
-    print("SKIP")
-    raise SystemExit
+    import errno, os, vfs
 
-try:
-    uos.VfsFat
-except AttributeError:
+    vfs.VfsFat
+except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
@@ -41,18 +36,18 @@ def count(self):
     print("SKIP")
     raise SystemExit
 
-uos.VfsFat.mkfs(bdev)
-vfs = uos.VfsFat(bdev)
-uos.mount(vfs, "/ramdisk")
+vfs.VfsFat.mkfs(bdev)
+fs = vfs.VfsFat(bdev)
+vfs.mount(fs, "/ramdisk")
 
 # file io
-with vfs.open("file.txt", "w") as f:
+with fs.open("file.txt", "w") as f:
     f.write("hello!")
 
-print(list(vfs.ilistdir()))
+print(list(fs.ilistdir()))
 
-with vfs.open("file.txt", "r") as f:
+with fs.open("file.txt", "r") as f:
     print(f.read())
 
-vfs.remove("file.txt")
-print(list(vfs.ilistdir()))
+fs.remove("file.txt")
+print(list(fs.ilistdir()))
diff --git a/tests/extmod/vfs_fat_ramdisk.py b/tests/extmod/vfs_fat_ramdisk.py
index 01235bc2664..05c45e96b06 100644
--- a/tests/extmod/vfs_fat_ramdisk.py
+++ b/tests/extmod/vfs_fat_ramdisk.py
@@ -1,13 +1,8 @@
 try:
-    import uerrno
-    import uos
-except ImportError:
-    print("SKIP")
-    raise SystemExit
+    import errno, os, vfs
 
-try:
-    uos.VfsFat
-except AttributeError:
+    vfs.VfsFat
+except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
@@ -38,7 +33,7 @@ def ioctl(self, op, arg):
 
 try:
     bdev = RAMFS(50)
-    uos.VfsFat.mkfs(bdev)
+    vfs.VfsFat.mkfs(bdev)
 except MemoryError:
     print("SKIP")
     raise SystemExit
@@ -46,50 +41,50 @@ def ioctl(self, op, arg):
 print(b"FOO_FILETXT" not in bdev.data)
 print(b"hello!" not in bdev.data)
 
-vfs = uos.VfsFat(bdev)
-uos.mount(vfs, "/ramdisk")
+fs = vfs.VfsFat(bdev)
+vfs.mount(fs, "/ramdisk")
 
-print("statvfs:", vfs.statvfs("/ramdisk"))
-print("getcwd:", vfs.getcwd())
+print("statvfs:", fs.statvfs("/ramdisk"))
+print("getcwd:", fs.getcwd())
 
 try:
-    vfs.stat("no_file.txt")
+    fs.stat("no_file.txt")
 except OSError as e:
-    print(e.errno == uerrno.ENOENT)
+    print(e.errno == errno.ENOENT)
 
-with vfs.open("foo_file.txt", "w") as f:
+with fs.open("foo_file.txt", "w") as f:
     f.write("hello!")
-print(list(vfs.ilistdir()))
+print(list(fs.ilistdir()))
 
-print("stat root:", vfs.stat("/")[:-3])  # timestamps differ across runs
-print("stat file:", vfs.stat("foo_file.txt")[:-3])  # timestamps differ across runs
+print("stat root:", fs.stat("/")[:-3])  # timestamps differ across runs
+print("stat file:", fs.stat("foo_file.txt")[:-3])  # timestamps differ across runs
 
 print(b"FOO_FILETXT" in bdev.data)
 print(b"hello!" in bdev.data)
 
-vfs.mkdir("foo_dir")
-vfs.chdir("foo_dir")
-print("getcwd:", vfs.getcwd())
-print(list(vfs.ilistdir()))
+fs.mkdir("foo_dir")
+fs.chdir("foo_dir")
+print("getcwd:", fs.getcwd())
+print(list(fs.ilistdir()))
 
-with vfs.open("sub_file.txt", "w") as f:
+with fs.open("sub_file.txt", "w") as f:
     f.write("subdir file")
 
 try:
-    vfs.chdir("sub_file.txt")
+    fs.chdir("sub_file.txt")
 except OSError as e:
-    print(e.errno == uerrno.ENOENT)
+    print(e.errno == errno.ENOENT)
 
-vfs.chdir("..")
-print("getcwd:", vfs.getcwd())
+fs.chdir("..")
+print("getcwd:", fs.getcwd())
 
-uos.umount(vfs)
+vfs.umount(fs)
 
-vfs = uos.VfsFat(bdev)
-print(list(vfs.ilistdir(b"")))
+fs = vfs.VfsFat(bdev)
+print(list(fs.ilistdir(b"")))
 
 # list a non-existent directory
 try:
-    vfs.ilistdir(b"no_exist")
+    fs.ilistdir(b"no_exist")
 except OSError as e:
-    print("ENOENT:", e.errno == uerrno.ENOENT)
+    print("ENOENT:", e.errno == errno.ENOENT)
diff --git a/tests/extmod/vfs_fat_ramdisklarge.py b/tests/extmod/vfs_fat_ramdisklarge.py
index 649a53db147..38ad772b443 100644
--- a/tests/extmod/vfs_fat_ramdisklarge.py
+++ b/tests/extmod/vfs_fat_ramdisklarge.py
@@ -1,14 +1,10 @@
 # test making a FAT filesystem on a very large block device
 
 try:
-    import uos
-except ImportError:
-    print("SKIP")
-    raise SystemExit
+    import os, vfs
 
-try:
-    uos.VfsFat
-except AttributeError:
+    vfs.VfsFat
+except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
@@ -46,24 +42,24 @@ def ioctl(self, op, arg):
 
 try:
     bdev = RAMBDevSparse(4 * 1024 * 1024 * 1024 // RAMBDevSparse.SEC_SIZE)
-    uos.VfsFat.mkfs(bdev)
+    vfs.VfsFat.mkfs(bdev)
 except MemoryError:
     print("SKIP")
     raise SystemExit
 
-vfs = uos.VfsFat(bdev)
-uos.mount(vfs, "/ramdisk")
+fs = vfs.VfsFat(bdev)
+vfs.mount(fs, "/ramdisk")
 
-print("statvfs:", vfs.statvfs("/ramdisk"))
+print("statvfs:", fs.statvfs("/ramdisk"))
 
 f = open("/ramdisk/test.txt", "w")
 f.write("test file")
 f.close()
 
-print("statvfs:", vfs.statvfs("/ramdisk"))
+print("statvfs:", fs.statvfs("/ramdisk"))
 
 f = open("/ramdisk/test.txt")
 print(f.read())
 f.close()
 
-uos.umount(vfs)
+vfs.umount(fs)
diff --git a/tests/extmod/vfs_lfs.py b/tests/extmod/vfs_lfs.py
index 8e56400df3c..40d58e9c9f7 100644
--- a/tests/extmod/vfs_lfs.py
+++ b/tests/extmod/vfs_lfs.py
@@ -1,10 +1,10 @@
 # Test for VfsLittle using a RAM device
 
 try:
-    import uos
+    import os, vfs
 
-    uos.VfsLfs1
-    uos.VfsLfs2
+    vfs.VfsLfs1
+    vfs.VfsLfs2
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
@@ -47,44 +47,44 @@ def test(bdev, vfs_class):
     vfs_class.mkfs(bdev)
 
     # construction
-    vfs = vfs_class(bdev)
+    fs = vfs_class(bdev)
 
     # statvfs
-    print(vfs.statvfs("/"))
+    print(fs.statvfs("/"))
 
     # open, write close
-    f = vfs.open("test", "w")
+    f = fs.open("test", "w")
     f.write("littlefs")
     f.close()
 
     # statvfs after creating a file
-    print(vfs.statvfs("/"))
+    print(fs.statvfs("/"))
 
     # ilistdir
-    print(list(vfs.ilistdir()))
-    print(list(vfs.ilistdir("/")))
-    print(list(vfs.ilistdir(b"/")))
+    print(list(fs.ilistdir()))
+    print(list(fs.ilistdir("/")))
+    print(list(fs.ilistdir(b"/")))
 
     # mkdir, rmdir
-    vfs.mkdir("testdir")
-    print(list(vfs.ilistdir()))
-    print(sorted(list(vfs.ilistdir("testdir"))))
-    vfs.rmdir("testdir")
-    print(list(vfs.ilistdir()))
-    vfs.mkdir("testdir")
+    fs.mkdir("testdir")
+    print(list(fs.ilistdir()))
+    print(sorted(list(fs.ilistdir("testdir"))))
+    fs.rmdir("testdir")
+    print(list(fs.ilistdir()))
+    fs.mkdir("testdir")
 
     # stat a file
-    print_stat(vfs.stat("test"))
+    print_stat(fs.stat("test"))
 
     # stat a dir (size seems to vary on LFS2 so don't print that)
-    print_stat(vfs.stat("testdir"), False)
+    print_stat(fs.stat("testdir"), False)
 
     # read
-    with vfs.open("test", "r") as f:
+    with fs.open("test", "r") as f:
         print(f.read())
 
     # create large file
-    with vfs.open("testbig", "w") as f:
+    with fs.open("testbig", "w") as f:
         data = "large012" * 32 * 16
         print("data length:", len(data))
         for i in range(4):
@@ -92,63 +92,63 @@ def test(bdev, vfs_class):
             f.write(data)
 
     # stat after creating large file
-    print(vfs.statvfs("/"))
+    print(fs.statvfs("/"))
 
     # rename
-    vfs.rename("testbig", "testbig2")
-    print(sorted(list(vfs.ilistdir())))
-    vfs.chdir("testdir")
-    vfs.rename("/testbig2", "testbig2")
-    print(sorted(list(vfs.ilistdir())))
-    vfs.rename("testbig2", "/testbig2")
-    vfs.chdir("/")
-    print(sorted(list(vfs.ilistdir())))
+    fs.rename("testbig", "testbig2")
+    print(sorted(list(fs.ilistdir())))
+    fs.chdir("testdir")
+    fs.rename("/testbig2", "testbig2")
+    print(sorted(list(fs.ilistdir())))
+    fs.rename("testbig2", "/testbig2")
+    fs.chdir("/")
+    print(sorted(list(fs.ilistdir())))
 
     # remove
-    vfs.remove("testbig2")
-    print(sorted(list(vfs.ilistdir())))
+    fs.remove("testbig2")
+    print(sorted(list(fs.ilistdir())))
 
     # getcwd, chdir
-    vfs.mkdir("/testdir2")
-    vfs.mkdir("/testdir/subdir")
-    print(vfs.getcwd())
-    vfs.chdir("/testdir")
-    print(vfs.getcwd())
+    fs.mkdir("/testdir2")
+    fs.mkdir("/testdir/subdir")
+    print(fs.getcwd())
+    fs.chdir("/testdir")
+    print(fs.getcwd())
 
     # create file in directory to make sure paths are relative
-    vfs.open("test2", "w").close()
-    print_stat(vfs.stat("test2"))
-    print_stat(vfs.stat("/testdir/test2"))
-    vfs.remove("test2")
+    fs.open("test2", "w").close()
+    print_stat(fs.stat("test2"))
+    print_stat(fs.stat("/testdir/test2"))
+    fs.remove("test2")
 
     # chdir back to root and remove testdir
-    vfs.chdir("/")
-    print(vfs.getcwd())
-    vfs.chdir("testdir")
-    print(vfs.getcwd())
-    vfs.chdir("..")
-    print(vfs.getcwd())
-    vfs.chdir("testdir/subdir")
-    print(vfs.getcwd())
-    vfs.chdir("../..")
-    print(vfs.getcwd())
-    vfs.chdir("/./testdir2")
-    print(vfs.getcwd())
-    vfs.chdir("../testdir")
-    print(vfs.getcwd())
-    vfs.chdir("../..")
-    print(vfs.getcwd())
-    vfs.chdir(".//testdir")
-    print(vfs.getcwd())
-    vfs.chdir("subdir/./")
-    print(vfs.getcwd())
-    vfs.chdir("/")
-    print(vfs.getcwd())
-    vfs.rmdir("testdir/subdir")
-    vfs.rmdir("testdir")
-    vfs.rmdir("testdir2")
+    fs.chdir("/")
+    print(fs.getcwd())
+    fs.chdir("testdir")
+    print(fs.getcwd())
+    fs.chdir("..")
+    print(fs.getcwd())
+    fs.chdir("testdir/subdir")
+    print(fs.getcwd())
+    fs.chdir("../..")
+    print(fs.getcwd())
+    fs.chdir("/./testdir2")
+    print(fs.getcwd())
+    fs.chdir("../testdir")
+    print(fs.getcwd())
+    fs.chdir("..")
+    print(fs.getcwd())
+    fs.chdir(".//testdir")
+    print(fs.getcwd())
+    fs.chdir("subdir/./")
+    print(fs.getcwd())
+    fs.chdir("/")
+    print(fs.getcwd())
+    fs.rmdir("testdir/subdir")
+    fs.rmdir("testdir")
+    fs.rmdir("testdir2")
 
 
 bdev = RAMBlockDevice(30)
-test(bdev, uos.VfsLfs1)
-test(bdev, uos.VfsLfs2)
+test(bdev, vfs.VfsLfs1)
+test(bdev, vfs.VfsLfs2)
diff --git a/tests/extmod/vfs_lfs_corrupt.py b/tests/extmod/vfs_lfs_corrupt.py
index 330458709a3..0365c0c23f3 100644
--- a/tests/extmod/vfs_lfs_corrupt.py
+++ b/tests/extmod/vfs_lfs_corrupt.py
@@ -1,10 +1,10 @@
 # Test for VfsLittle using a RAM device, testing error handling from corrupt block device
 
 try:
-    import uos
+    import vfs
 
-    uos.VfsLfs1
-    uos.VfsLfs2
+    vfs.VfsLfs1
+    vfs.VfsLfs2
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
@@ -47,28 +47,28 @@ def corrupt(bdev, block):
 def create_vfs(bdev, vfs_class):
     bdev.ret = 0
     vfs_class.mkfs(bdev)
-    vfs = vfs_class(bdev)
-    with vfs.open("f", "w") as f:
+    fs = vfs_class(bdev)
+    with fs.open("f", "w") as f:
         for i in range(100):
             f.write("test")
-    return vfs
+    return fs
 
 
 def test(bdev, vfs_class):
     print("test", vfs_class)
 
     # statvfs
-    vfs = create_vfs(bdev, vfs_class)
+    fs = create_vfs(bdev, vfs_class)
     corrupt(bdev, 0)
     corrupt(bdev, 1)
     try:
-        print(vfs.statvfs(""))
+        print(fs.statvfs(""))
     except OSError:
         print("statvfs OSError")
 
     # error during read
-    vfs = create_vfs(bdev, vfs_class)
-    f = vfs.open("f", "r")
+    fs = create_vfs(bdev, vfs_class)
+    f = fs.open("f", "r")
     bdev.ret = -5  # EIO
     try:
         f.read(10)
@@ -76,8 +76,8 @@ def test(bdev, vfs_class):
         print("read OSError")
 
     # error during write
-    vfs = create_vfs(bdev, vfs_class)
-    f = vfs.open("f", "a")
+    fs = create_vfs(bdev, vfs_class)
+    f = fs.open("f", "a")
     bdev.ret = -5  # EIO
     try:
         f.write("test")
@@ -85,8 +85,8 @@ def test(bdev, vfs_class):
         print("write OSError")
 
     # error during close
-    vfs = create_vfs(bdev, vfs_class)
-    f = vfs.open("f", "w")
+    fs = create_vfs(bdev, vfs_class)
+    f = fs.open("f", "w")
     f.write("test")
     bdev.ret = -5  # EIO
     try:
@@ -95,8 +95,8 @@ def test(bdev, vfs_class):
         print("close OSError")
 
     # error during flush
-    vfs = create_vfs(bdev, vfs_class)
-    f = vfs.open("f", "w")
+    fs = create_vfs(bdev, vfs_class)
+    f = fs.open("f", "w")
     f.write("test")
     bdev.ret = -5  # EIO
     try:
@@ -108,5 +108,5 @@ def test(bdev, vfs_class):
 
 
 bdev = RAMBlockDevice(30)
-test(bdev, uos.VfsLfs1)
-test(bdev, uos.VfsLfs2)
+test(bdev, vfs.VfsLfs1)
+test(bdev, vfs.VfsLfs2)
diff --git a/tests/extmod/vfs_lfs_error.py b/tests/extmod/vfs_lfs_error.py
index 717284ea017..73cdf343733 100644
--- a/tests/extmod/vfs_lfs_error.py
+++ b/tests/extmod/vfs_lfs_error.py
@@ -1,10 +1,10 @@
 # Test for VfsLittle using a RAM device, testing error handling
 
 try:
-    import uos
+    import errno, vfs
 
-    uos.VfsLfs1
-    uos.VfsLfs2
+    vfs.VfsLfs1
+    vfs.VfsLfs2
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
@@ -41,81 +41,81 @@ def test(bdev, vfs_class):
     # mkfs with too-small block device
     try:
         vfs_class.mkfs(RAMBlockDevice(1))
-    except OSError:
-        print("mkfs OSError")
+    except OSError as er:
+        print("mkfs OSError", er.errno > 0)
 
     # mount with invalid filesystem
     try:
         vfs_class(bdev)
-    except OSError:
-        print("mount OSError")
+    except OSError as er:
+        print("mount OSError", er.errno > 0)
 
     # set up for following tests
     vfs_class.mkfs(bdev)
-    vfs = vfs_class(bdev)
-    with vfs.open("testfile", "w") as f:
+    fs = vfs_class(bdev)
+    with fs.open("testfile", "w") as f:
         f.write("test")
-    vfs.mkdir("testdir")
+    fs.mkdir("testdir")
 
     # ilistdir
     try:
-        vfs.ilistdir("noexist")
-    except OSError:
-        print("ilistdir OSError")
+        fs.ilistdir("noexist")
+    except OSError as er:
+        print("ilistdir OSError", er)
 
     # remove
     try:
-        vfs.remove("noexist")
-    except OSError:
-        print("remove OSError")
+        fs.remove("noexist")
+    except OSError as er:
+        print("remove OSError", er)
 
     # rmdir
     try:
-        vfs.rmdir("noexist")
-    except OSError:
-        print("rmdir OSError")
+        fs.rmdir("noexist")
+    except OSError as er:
+        print("rmdir OSError", er)
 
     # rename
     try:
-        vfs.rename("noexist", "somethingelse")
-    except OSError:
-        print("rename OSError")
+        fs.rename("noexist", "somethingelse")
+    except OSError as er:
+        print("rename OSError", er)
 
     # mkdir
     try:
-        vfs.mkdir("testdir")
-    except OSError:
-        print("mkdir OSError")
+        fs.mkdir("testdir")
+    except OSError as er:
+        print("mkdir OSError", er)
 
     # chdir to nonexistent
     try:
-        vfs.chdir("noexist")
-    except OSError:
-        print("chdir OSError")
-    print(vfs.getcwd())  # check still at root
+        fs.chdir("noexist")
+    except OSError as er:
+        print("chdir OSError", er)
+    print(fs.getcwd())  # check still at root
 
     # chdir to file
     try:
-        vfs.chdir("testfile")
-    except OSError:
-        print("chdir OSError")
-    print(vfs.getcwd())  # check still at root
+        fs.chdir("testfile")
+    except OSError as er:
+        print("chdir OSError", er)
+    print(fs.getcwd())  # check still at root
 
     # stat
     try:
-        vfs.stat("noexist")
-    except OSError:
-        print("stat OSError")
+        fs.stat("noexist")
+    except OSError as er:
+        print("stat OSError", er)
 
     # error during seek
-    with vfs.open("testfile", "r") as f:
+    with fs.open("testfile", "r") as f:
         f.seek(1 << 30)  # SEEK_SET
         try:
             f.seek(1 << 30, 1)  # SEEK_CUR
-        except OSError:
-            print("seek OSError")
+        except OSError as er:
+            print("seek OSError", er)
 
 
 bdev = RAMBlockDevice(30)
-test(bdev, uos.VfsLfs1)
-test(bdev, uos.VfsLfs2)
+test(bdev, vfs.VfsLfs1)
+test(bdev, vfs.VfsLfs2)
diff --git a/tests/extmod/vfs_lfs_error.py.exp b/tests/extmod/vfs_lfs_error.py.exp
index f4327f6962e..440607ed84b 100644
--- a/tests/extmod/vfs_lfs_error.py.exp
+++ b/tests/extmod/vfs_lfs_error.py.exp
@@ -1,28 +1,28 @@
 test 
-mkfs OSError
-mount OSError
-ilistdir OSError
-remove OSError
-rmdir OSError
-rename OSError
-mkdir OSError
-chdir OSError
+mkfs OSError True
+mount OSError True
+ilistdir OSError [Errno 2] ENOENT
+remove OSError [Errno 2] ENOENT
+rmdir OSError [Errno 2] ENOENT
+rename OSError [Errno 2] ENOENT
+mkdir OSError [Errno 17] EEXIST
+chdir OSError [Errno 2] ENOENT
 /
-chdir OSError
+chdir OSError [Errno 2] ENOENT
 /
-stat OSError
-seek OSError
+stat OSError [Errno 2] ENOENT
+seek OSError [Errno 22] EINVAL
 test 
-mkfs OSError
-mount OSError
-ilistdir OSError
-remove OSError
-rmdir OSError
-rename OSError
-mkdir OSError
-chdir OSError
+mkfs OSError True
+mount OSError True
+ilistdir OSError [Errno 2] ENOENT
+remove OSError [Errno 2] ENOENT
+rmdir OSError [Errno 2] ENOENT
+rename OSError [Errno 2] ENOENT
+mkdir OSError [Errno 17] EEXIST
+chdir OSError [Errno 2] ENOENT
 /
-chdir OSError
+chdir OSError [Errno 2] ENOENT
 /
-stat OSError
-seek OSError
+stat OSError [Errno 2] ENOENT
+seek OSError [Errno 22] EINVAL
diff --git a/tests/extmod/vfs_lfs_file.py b/tests/extmod/vfs_lfs_file.py
index 774cca29643..0e1797714d3 100644
--- a/tests/extmod/vfs_lfs_file.py
+++ b/tests/extmod/vfs_lfs_file.py
@@ -1,10 +1,10 @@
 # Test for VfsLittle using a RAM device, file IO
 
 try:
-    import uos
+    import vfs
 
-    uos.VfsLfs1
-    uos.VfsLfs2
+    vfs.VfsLfs1
+    vfs.VfsLfs2
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
@@ -42,10 +42,10 @@ def test(bdev, vfs_class):
     vfs_class.mkfs(bdev)
 
     # construction
-    vfs = vfs_class(bdev)
+    fs = vfs_class(bdev)
 
     # create text, print, write, close
-    f = vfs.open("test.txt", "wt")
+    f = fs.open("test.txt", "wt")
     print(f)
     f.write("littlefs")
     f.close()
@@ -54,48 +54,48 @@ def test(bdev, vfs_class):
     f.close()
 
     # create binary, print, write, flush, close
-    f = vfs.open("test.bin", "wb")
+    f = fs.open("test.bin", "wb")
     print(f)
     f.write("littlefs")
     f.flush()
     f.close()
 
     # create for append
-    f = vfs.open("test.bin", "ab")
+    f = fs.open("test.bin", "ab")
     f.write("more")
     f.close()
 
     # create exclusive
-    f = vfs.open("test2.bin", "xb")
+    f = fs.open("test2.bin", "xb")
     f.close()
 
     # create exclusive with error
     try:
-        vfs.open("test2.bin", "x")
+        fs.open("test2.bin", "x")
     except OSError:
         print("open OSError")
 
     # read default
-    with vfs.open("test.txt", "") as f:
+    with fs.open("test.txt", "") as f:
         print(f.read())
 
     # read text
-    with vfs.open("test.txt", "rt") as f:
+    with fs.open("test.txt", "rt") as f:
         print(f.read())
 
     # read binary
-    with vfs.open("test.bin", "rb") as f:
+    with fs.open("test.bin", "rb") as f:
         print(f.read())
 
     # create read and write
-    with vfs.open("test.bin", "r+b") as f:
+    with fs.open("test.bin", "r+b") as f:
         print(f.read(8))
         f.write("MORE")
-    with vfs.open("test.bin", "rb") as f:
+    with fs.open("test.bin", "rb") as f:
         print(f.read())
 
     # seek and tell
-    f = vfs.open("test.txt", "r")
+    f = fs.open("test.txt", "r")
     print(f.tell())
     f.seek(3, 0)
     print(f.tell())
@@ -103,13 +103,13 @@ def test(bdev, vfs_class):
 
     # open nonexistent
     try:
-        vfs.open("noexist", "r")
+        fs.open("noexist", "r")
     except OSError:
         print("open OSError")
 
     # open multiple files at the same time
-    f1 = vfs.open("test.txt", "")
-    f2 = vfs.open("test.bin", "b")
+    f1 = fs.open("test.txt", "")
+    f2 = fs.open("test.bin", "b")
     print(f1.read())
     print(f2.read())
     f1.close()
@@ -117,5 +117,5 @@ def test(bdev, vfs_class):
 
 
 bdev = RAMBlockDevice(30)
-test(bdev, uos.VfsLfs1)
-test(bdev, uos.VfsLfs2)
+test(bdev, vfs.VfsLfs1)
+test(bdev, vfs.VfsLfs2)
diff --git a/tests/extmod/vfs_lfs_ilistdir_del.py b/tests/extmod/vfs_lfs_ilistdir_del.py
index 073576986d3..828c85a2588 100644
--- a/tests/extmod/vfs_lfs_ilistdir_del.py
+++ b/tests/extmod/vfs_lfs_ilistdir_del.py
@@ -1,10 +1,9 @@
 # Test ilistdir __del__ for VfsLittle using a RAM device.
-import gc
 
 try:
-    import uos
+    import gc, vfs
 
-    uos.VfsLfs2
+    vfs.VfsLfs2
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
@@ -37,29 +36,29 @@ def ioctl(self, op, arg):
 
 def test(bdev, vfs_class):
     vfs_class.mkfs(bdev)
-    vfs = vfs_class(bdev)
-    vfs.mkdir("/test_d1")
-    vfs.mkdir("/test_d2")
-    vfs.mkdir("/test_d3")
+    fs = vfs_class(bdev)
+    fs.mkdir("/test_d1")
+    fs.mkdir("/test_d2")
+    fs.mkdir("/test_d3")
 
     for i in range(10):
         print(i)
 
         # We want to partially iterate the ilistdir iterator to leave it in an
         # open state, which will then test the finaliser when it's garbage collected.
-        idir = vfs.ilistdir("/")
+        idir = fs.ilistdir("/")
         print(any(idir))
 
         # Alternate way of partially iterating the ilistdir object, modifying the
         # filesystem while it's open.
-        for dname, *_ in vfs.ilistdir("/"):
-            vfs.rmdir(dname)
+        for dname, *_ in fs.ilistdir("/"):
+            fs.rmdir(dname)
             break
-        vfs.mkdir(dname)
+        fs.mkdir(dname)
 
-        # Also create a fully drained iterator and ensure trying to re-use it
+        # Also create a fully drained iterator and ensure trying to reuse it
         # throws the correct exception.
-        idir_emptied = vfs.ilistdir("/")
+        idir_emptied = fs.ilistdir("/")
         l = list(idir_emptied)
         print(len(l))
         try:
@@ -68,8 +67,13 @@ def test(bdev, vfs_class):
             pass
 
         gc.collect()
-        vfs.open("/test", "w").close()
+        fs.open("/test", "w").close()
 
 
-bdev = RAMBlockDevice(30)
-test(bdev, uos.VfsLfs2)
+try:
+    bdev = RAMBlockDevice(30)
+except MemoryError:
+    print("SKIP")
+    raise SystemExit
+
+test(bdev, vfs.VfsLfs2)
diff --git a/tests/extmod/vfs_lfs_mount.py b/tests/extmod/vfs_lfs_mount.py
index bea8a2723ac..4887ddcec00 100644
--- a/tests/extmod/vfs_lfs_mount.py
+++ b/tests/extmod/vfs_lfs_mount.py
@@ -1,10 +1,10 @@
 # Test for VfsLittle using a RAM device, with mount/umount
 
 try:
-    import uos
+    import os, vfs
 
-    uos.VfsLfs1
-    uos.VfsLfs2
+    vfs.VfsLfs1
+    vfs.VfsLfs2
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
@@ -42,7 +42,7 @@ def test(vfs_class):
 
     # mount bdev unformatted
     try:
-        uos.mount(bdev, "/lfs")
+        vfs.mount(bdev, "/lfs")
     except Exception as er:
         print(repr(er))
 
@@ -50,10 +50,10 @@ def test(vfs_class):
     vfs_class.mkfs(bdev)
 
     # construction
-    vfs = vfs_class(bdev)
+    fs = vfs_class(bdev)
 
     # mount
-    uos.mount(vfs, "/lfs")
+    vfs.mount(fs, "/lfs")
 
     # import
     with open("/lfs/lfsmod.py", "w") as f:
@@ -61,23 +61,23 @@ def test(vfs_class):
     import lfsmod
 
     # import package
-    uos.mkdir("/lfs/lfspkg")
+    os.mkdir("/lfs/lfspkg")
     with open("/lfs/lfspkg/__init__.py", "w") as f:
         f.write('print("package")\n')
     import lfspkg
 
     # chdir and import module from current directory (needs "" in sys.path)
-    uos.mkdir("/lfs/subdir")
-    uos.chdir("/lfs/subdir")
-    uos.rename("/lfs/lfsmod.py", "/lfs/subdir/lfsmod2.py")
+    os.mkdir("/lfs/subdir")
+    os.chdir("/lfs/subdir")
+    os.rename("/lfs/lfsmod.py", "/lfs/subdir/lfsmod2.py")
     import lfsmod2
 
     # umount
-    uos.umount("/lfs")
+    vfs.umount("/lfs")
 
     # mount read-only
-    vfs = vfs_class(bdev)
-    uos.mount(vfs, "/lfs", readonly=True)
+    fs = vfs_class(bdev)
+    vfs.mount(fs, "/lfs", readonly=True)
 
     # test reading works
     with open("/lfs/subdir/lfsmod2.py") as f:
@@ -90,25 +90,25 @@ def test(vfs_class):
         print(repr(er))
 
     # umount
-    uos.umount("/lfs")
+    vfs.umount("/lfs")
 
     # mount bdev again
-    uos.mount(bdev, "/lfs")
+    vfs.mount(bdev, "/lfs")
 
     # umount
-    uos.umount("/lfs")
+    vfs.umount("/lfs")
 
     # clear imported modules
-    usys.modules.clear()
+    sys.modules.clear()
 
 
 # initialise path
-import usys
+import sys
 
-usys.path.clear()
-usys.path.append("/lfs")
-usys.path.append("")
+sys.path.clear()
+sys.path.append("/lfs")
+sys.path.append("")
 
 # run tests
-test(uos.VfsLfs1)
-test(uos.VfsLfs2)
+test(vfs.VfsLfs1)
+test(vfs.VfsLfs2)
diff --git a/tests/extmod/vfs_lfs_mtime.py b/tests/extmod/vfs_lfs_mtime.py
index a67e48dd80d..d4404de4dcb 100644
--- a/tests/extmod/vfs_lfs_mtime.py
+++ b/tests/extmod/vfs_lfs_mtime.py
@@ -1,11 +1,11 @@
 # Test for VfsLfs using a RAM device, mtime feature
 
 try:
-    import utime, uos
+    import time, vfs
 
-    utime.time
-    utime.sleep
-    uos.VfsLfs2
+    time.time_ns
+    time.sleep
+    vfs.VfsLfs2
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
@@ -44,61 +44,62 @@ def test(bdev, vfs_class):
 
     # construction
     print("mtime=True")
-    vfs = vfs_class(bdev, mtime=True)
+    fs = vfs_class(bdev, mtime=True)
 
     # Create an empty file, should have a timestamp.
-    current_time = int(utime.time())
-    vfs.open("test1", "wt").close()
+    # Use time_ns() for current time because that's what's used for VfsLfs2 time.
+    current_time = time.time_ns() // 1_000_000_000
+    fs.open("test1", "wt").close()
 
     # Wait 1 second so mtime will increase by at least 1.
-    utime.sleep(1)
+    time.sleep(1)
 
     # Create another empty file, should have a timestamp.
-    vfs.open("test2", "wt").close()
+    fs.open("test2", "wt").close()
 
     # Stat the files and check mtime is non-zero.
-    stat1 = vfs.stat("test1")
-    stat2 = vfs.stat("test2")
+    stat1 = fs.stat("test1")
+    stat2 = fs.stat("test2")
     print(stat1[8] != 0, stat2[8] != 0)
 
-    # Check that test1 has mtime which matches time.time() at point of creation.
+    # Check that test1 has mtime which matches time.time_ns() at point of creation.
     print(current_time <= stat1[8] <= current_time + 1)
 
     # Check that test1 is older than test2.
     print(stat1[8] < stat2[8])
 
     # Wait 1 second so mtime will increase by at least 1.
-    utime.sleep(1)
+    time.sleep(1)
 
     # Open test1 for reading and ensure mtime did not change.
-    vfs.open("test1", "rt").close()
-    print(vfs.stat("test1") == stat1)
+    fs.open("test1", "rt").close()
+    print(fs.stat("test1") == stat1)
 
     # Open test1 for writing and ensure mtime increased from the previous value.
-    vfs.open("test1", "wt").close()
+    fs.open("test1", "wt").close()
     stat1_old = stat1
-    stat1 = vfs.stat("test1")
+    stat1 = fs.stat("test1")
     print(stat1_old[8] < stat1[8])
 
     # Unmount.
-    vfs.umount()
+    fs.umount()
 
     # Check that remounting with mtime=False can read the timestamps.
     print("mtime=False")
-    vfs = vfs_class(bdev, mtime=False)
-    print(vfs.stat("test1") == stat1)
-    print(vfs.stat("test2") == stat2)
-    f = vfs.open("test1", "wt")
+    fs = vfs_class(bdev, mtime=False)
+    print(fs.stat("test1") == stat1)
+    print(fs.stat("test2") == stat2)
+    f = fs.open("test1", "wt")
     f.close()
-    print(vfs.stat("test1") == stat1)
-    vfs.umount()
+    print(fs.stat("test1") == stat1)
+    fs.umount()
 
     # Check that remounting with mtime=True still has the timestamps.
     print("mtime=True")
-    vfs = vfs_class(bdev, mtime=True)
-    print(vfs.stat("test1") == stat1)
-    print(vfs.stat("test2") == stat2)
-    vfs.umount()
+    fs = vfs_class(bdev, mtime=True)
+    print(fs.stat("test1") == stat1)
+    print(fs.stat("test2") == stat2)
+    fs.umount()
 
 
 try:
@@ -107,4 +108,4 @@ def test(bdev, vfs_class):
     print("SKIP")
     raise SystemExit
 
-test(bdev, uos.VfsLfs2)
+test(bdev, vfs.VfsLfs2)
diff --git a/tests/extmod/vfs_lfs_superblock.py b/tests/extmod/vfs_lfs_superblock.py
index 1ac5675554b..74b6004e991 100644
--- a/tests/extmod/vfs_lfs_superblock.py
+++ b/tests/extmod/vfs_lfs_superblock.py
@@ -1,9 +1,9 @@
 # Test for VfsLfs using a RAM device, when the first superblock does not exist
 
 try:
-    import uos
+    import os, vfs
 
-    uos.VfsLfs2
+    vfs.VfsLfs2
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
@@ -36,12 +36,12 @@ def ioctl(self, op, arg):
 bdev = RAMBlockDevice(64, lfs2_data)
 
 # Create the VFS explicitly, no auto-detection is needed for this.
-vfs = uos.VfsLfs2(bdev)
-print(list(vfs.ilistdir()))
+fs = vfs.VfsLfs2(bdev)
+print(list(fs.ilistdir()))
 
 # Mount the block device directly; this relies on auto-detection.
-uos.mount(bdev, "/userfs")
-print(uos.listdir("/userfs"))
+vfs.mount(bdev, "/userfs")
+print(os.listdir("/userfs"))
 
 # Clean up.
-uos.umount("/userfs")
+vfs.umount("/userfs")
diff --git a/tests/extmod/vfs_mountinfo.py b/tests/extmod/vfs_mountinfo.py
new file mode 100644
index 00000000000..b31dc60ce76
--- /dev/null
+++ b/tests/extmod/vfs_mountinfo.py
@@ -0,0 +1,65 @@
+# test VFS functionality without any particular filesystem type
+
+try:
+    import os, vfs
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+class Filesystem:
+    def __init__(self, id, paths=[]):
+        self.id = id
+        self.paths = paths
+
+    def mount(self, readonly, mksfs):
+        print("mount", self)
+
+    def umount(self):
+        print("umount", self)
+
+    def stat(self, path):
+        if path in self.paths:
+            return (0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
+        else:
+            raise OSError
+
+    statvfs = stat
+
+    def open(self, path, mode):
+        pass
+
+    def __repr__(self):
+        return "Filesystem(%d)" % self.id
+
+
+# first we umount any existing mount points the target may have
+try:
+    vfs.umount("/")
+except OSError:
+    pass
+for path in os.listdir("/"):
+    vfs.umount("/" + path)
+
+
+print(vfs.mount())
+
+vfs.mount(Filesystem(1), "/foo")
+
+print(vfs.mount())
+
+vfs.mount(Filesystem(2), "/bar/baz")
+
+print(vfs.mount())
+
+vfs.mount(Filesystem(3), "/bar")
+
+print(vfs.mount())
+
+vfs.umount("/bar/baz")
+
+print(vfs.mount())
+
+vfs.mount(Filesystem(4), "/")
+
+print(vfs.mount())
diff --git a/tests/extmod/vfs_mountinfo.py.exp b/tests/extmod/vfs_mountinfo.py.exp
new file mode 100644
index 00000000000..4ddf06c8c97
--- /dev/null
+++ b/tests/extmod/vfs_mountinfo.py.exp
@@ -0,0 +1,11 @@
+[]
+mount Filesystem(1)
+[(Filesystem(1), '/foo')]
+mount Filesystem(2)
+[(Filesystem(1), '/foo'), (Filesystem(2), '/bar/baz')]
+mount Filesystem(3)
+[(Filesystem(1), '/foo'), (Filesystem(2), '/bar/baz'), (Filesystem(3), '/bar')]
+umount Filesystem(2)
+[(Filesystem(1), '/foo'), (Filesystem(3), '/bar')]
+mount Filesystem(4)
+[(Filesystem(1), '/foo'), (Filesystem(3), '/bar'), (Filesystem(4), '/')]
diff --git a/tests/extmod/vfs_posix.py b/tests/extmod/vfs_posix.py
index 3c23140065a..22e6e07d3b6 100644
--- a/tests/extmod/vfs_posix.py
+++ b/tests/extmod/vfs_posix.py
@@ -1,10 +1,9 @@
 # Test for VfsPosix
 
 try:
-    import gc
-    import uos
+    import gc, os, vfs
 
-    uos.VfsPosix
+    vfs.VfsPosix
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
@@ -13,27 +12,41 @@
 # Skip the test if it does exist.
 temp_dir = "micropy_test_dir"
 try:
-    uos.stat(temp_dir)
+    os.stat(temp_dir)
     print("SKIP")
     raise SystemExit
 except OSError:
     pass
 
 # getcwd and chdir
-curdir = uos.getcwd()
-uos.chdir("/")
-print(uos.getcwd())
-uos.chdir(curdir)
-print(uos.getcwd() == curdir)
+curdir = os.getcwd()
+os.chdir("/")
+print(os.getcwd())
+os.chdir(curdir)
+print(os.getcwd() == curdir)
 
 # stat
-print(type(uos.stat("/")))
+print(type(os.stat("/")))
 
 # listdir and ilistdir
-print(type(uos.listdir("/")))
+target = "/"
+try:
+    import platform
+
+    # On Android non-root users are permitted full filesystem access only to
+    # selected directories.  To let this test pass on bionic, the internal
+    # user-accessible storage area root is enumerated instead of the
+    # filesystem root.  "/storage/emulated/0" should be there on pretty much
+    # any recent-ish device; querying the proper location requires a JNI
+    # round-trip, not really worth it.
+    if platform.platform().startswith("Android-"):
+        target = "/storage/emulated/0"
+except ImportError:
+    pass
+print(type(os.listdir(target)))
 
 # mkdir
-uos.mkdir(temp_dir)
+os.mkdir(temp_dir)
 
 # file create
 f = open(temp_dir + "/test", "w")
@@ -78,41 +91,73 @@ def write_files_without_closing():
     print("next_file_no <= base_file_no", next_file_no <= base_file_no)
 
 for n in names + [basefd, nextfd]:
-    uos.remove(n)
+    os.remove(n)
 
 # rename
-uos.rename(temp_dir + "/test", temp_dir + "/test2")
-print(uos.listdir(temp_dir))
+os.rename(temp_dir + "/test", temp_dir + "/test2")
+print(os.listdir(temp_dir))
+
+# construct new VfsPosix with absolute path
+fs = vfs.VfsPosix(os.getcwd() + os.sep + temp_dir)
+f = fs.open("/test", "w")
+f.close()
+print(sorted(os.listdir(temp_dir)))
+fs.rename("/test", "/_test")
+print(sorted(os.listdir(temp_dir)))
+fs.remove("/_test")
 
 # construct new VfsPosix with path argument
-vfs = uos.VfsPosix(temp_dir)
-print(list(i[0] for i in vfs.ilistdir(".")))
+fs = vfs.VfsPosix(temp_dir)
+# when VfsPosix is used the intended way via vfs.mount(), it can only be called
+# with relative paths when the CWD is inside or at its root, so simulate that
+os.chdir(temp_dir)
+print(list(i[0] for i in fs.ilistdir(".")))
 
 # stat, statvfs (statvfs may not exist)
-print(type(vfs.stat(".")))
-if hasattr(vfs, "statvfs"):
-    assert type(vfs.statvfs(".")) is tuple
+print(type(fs.stat(".")))
+if hasattr(fs, "statvfs"):
+    assert type(fs.statvfs(".")) is tuple
 
 # check types of ilistdir with str/bytes arguments
-print(type(list(vfs.ilistdir("."))[0][0]))
-print(type(list(vfs.ilistdir(b"."))[0][0]))
+print(type(list(fs.ilistdir("."))[0][0]))
+print(type(list(fs.ilistdir(b"."))[0][0]))
+
+# chdir should not affect absolute paths (regression test)
+fs.mkdir("/subdir")
+fs.mkdir("/subdir/micropy_test_dir")
+with fs.open("/subdir/micropy_test_dir/test2", "w") as f:
+    f.write("wrong")
+fs.chdir("/subdir")
+with fs.open("/test2", "r") as f:
+    print(f.read())
+os.chdir(curdir)
+fs.remove("/subdir/micropy_test_dir/test2")
+fs.rmdir("/subdir/micropy_test_dir")
+fs.rmdir("/subdir")
+
+# done with fs, restore CWD
+os.chdir(curdir)
 
 # remove
-uos.remove(temp_dir + "/test2")
-print(uos.listdir(temp_dir))
+os.remove(temp_dir + "/test2")
+print(os.listdir(temp_dir))
 
 # remove with error
 try:
-    uos.remove(temp_dir + "/test2")
+    import os
+
+    os.remove(temp_dir + "/test2")
 except OSError:
     print("remove OSError")
 
 # rmdir
-uos.rmdir(temp_dir)
-print(temp_dir in uos.listdir())
+os.rmdir(temp_dir)
+print(temp_dir in os.listdir())
 
 # rmdir with error
 try:
-    uos.rmdir(temp_dir)
+    import os
+
+    os.rmdir(temp_dir)
 except OSError:
     print("rmdir OSError")
diff --git a/tests/extmod/vfs_posix.py.exp b/tests/extmod/vfs_posix.py.exp
index 99922e621d4..28dee4c101a 100644
--- a/tests/extmod/vfs_posix.py.exp
+++ b/tests/extmod/vfs_posix.py.exp
@@ -6,10 +6,13 @@ True
 hello
 next_file_no <= base_file_no True
 ['test2']
+['test', 'test2']
+['_test', 'test2']
 ['test2']
 
 
 
+hello
 []
 remove OSError
 False
diff --git a/tests/extmod/vfs_posix_enoent.py b/tests/extmod/vfs_posix_enoent.py
new file mode 100644
index 00000000000..7b00388da53
--- /dev/null
+++ b/tests/extmod/vfs_posix_enoent.py
@@ -0,0 +1,41 @@
+# Test for VfsPosix error conditions
+
+try:
+    import os, sys, vfs
+
+    vfs.VfsPosix
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+if sys.platform == "win32":
+    # Windows doesn't let you delete the current directory, so this cannot be
+    # tested.
+    print("SKIP")
+    raise SystemExit
+
+# We need an empty directory for testing.
+# Skip the test if it already exists.
+temp_dir = "vfs_posix_enoent_test_dir"
+try:
+    os.stat(temp_dir)
+    print("SKIP")
+    raise SystemExit
+except OSError:
+    pass
+
+curdir = os.getcwd()
+os.mkdir(temp_dir)
+os.chdir(temp_dir)
+os.rmdir(curdir + "/" + temp_dir)
+try:
+    print("getcwd():", os.getcwd())
+except OSError as e:
+    # expecting ENOENT = 2
+    print("getcwd():", repr(e))
+
+try:
+    print("VfsPosix():", vfs.VfsPosix("something"))
+except OSError as e:
+    # expecting ENOENT = 2
+    print("VfsPosix():", repr(e))
diff --git a/tests/extmod/vfs_posix_enoent.py.exp b/tests/extmod/vfs_posix_enoent.py.exp
new file mode 100644
index 00000000000..f2d9a0d5510
--- /dev/null
+++ b/tests/extmod/vfs_posix_enoent.py.exp
@@ -0,0 +1,2 @@
+getcwd(): OSError(2,)
+VfsPosix(): OSError(2,)
diff --git a/tests/extmod/vfs_posix_ilistdir_del.py b/tests/extmod/vfs_posix_ilistdir_del.py
index edb50dfd622..8b5984cd81c 100644
--- a/tests/extmod/vfs_posix_ilistdir_del.py
+++ b/tests/extmod/vfs_posix_ilistdir_del.py
@@ -1,39 +1,44 @@
 # Test ilistdir __del__ for VfsPosix.
-import gc
 
 try:
-    import os
+    import gc, os, vfs
 
-    os.VfsPosix
+    vfs.VfsPosix
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
 
 def test(testdir):
-    vfs = os.VfsPosix(testdir)
-    vfs.mkdir("/test_d1")
-    vfs.mkdir("/test_d2")
-    vfs.mkdir("/test_d3")
+    curdir = os.getcwd()
+    fs = vfs.VfsPosix(testdir)
+    # When VfsPosix is used the intended way via vfs.mount(), it can only be called
+    # with relative paths when the CWD is inside or at its root, so simulate that.
+    # (Although perhaps calling with a relative path was an oversight in this case
+    # and the respective line below was meant to read `fs.rmdir("/" + dname)`.)
+    os.chdir(testdir)
+    fs.mkdir("/test_d1")
+    fs.mkdir("/test_d2")
+    fs.mkdir("/test_d3")
 
     for i in range(10):
         print(i)
 
         # We want to partially iterate the ilistdir iterator to leave it in an
         # open state, which will then test the finaliser when it's garbage collected.
-        idir = vfs.ilistdir("/")
+        idir = fs.ilistdir("/")
         print(any(idir))
 
         # Alternate way of partially iterating the ilistdir object, modifying the
         # filesystem while it's open.
-        for dname, *_ in vfs.ilistdir("/"):
-            vfs.rmdir(dname)
+        for dname, *_ in fs.ilistdir("/"):
+            fs.rmdir(dname)
             break
-        vfs.mkdir(dname)
+        fs.mkdir(dname)
 
-        # Also create a fully drained iterator and ensure trying to re-use it
+        # Also create a fully drained iterator and ensure trying to reuse it
         # throws the correct exception.
-        idir_emptied = vfs.ilistdir("/")
+        idir_emptied = fs.ilistdir("/")
         l = list(idir_emptied)
         print(len(l))
         try:
@@ -45,8 +50,11 @@ def test(testdir):
 
         # Create and delete a file, try to flush out any filesystem
         # corruption that may be caused over the loops.
-        vfs.open("/test", "w").close()
-        vfs.remove("/test")
+        fs.open("/test", "w").close()
+        fs.remove("/test")
+
+    # Done with fs, restore CWD.
+    os.chdir(curdir)
 
 
 # We need an empty directory for testing.
diff --git a/tests/extmod/vfs_posix_ilistdir_filter.py b/tests/extmod/vfs_posix_ilistdir_filter.py
index c32d1249710..6df95420459 100644
--- a/tests/extmod/vfs_posix_ilistdir_filter.py
+++ b/tests/extmod/vfs_posix_ilistdir_filter.py
@@ -1,29 +1,36 @@
 # Test ilistdir filter of . and .. for VfsPosix.
 
 try:
-    import os
+    import os, vfs
 
-    os.VfsPosix
+    vfs.VfsPosix
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
 
 def test(testdir):
-    vfs = os.VfsPosix(testdir)
+    curdir = os.getcwd()
+    fs = vfs.VfsPosix(testdir)
+    # When VfsPosix is used the intended way via vfs.mount(), it can only be called
+    # with relative paths when the CWD is inside or at its root, so simulate that.
+    os.chdir(testdir)
 
     dirs = [".a", "..a", "...a", "a.b", "a..b"]
 
     for dir in dirs:
-        vfs.mkdir(dir)
+        fs.mkdir(dir)
 
     dirs = []
-    for entry in vfs.ilistdir("/"):
+    for entry in fs.ilistdir("/"):
         dirs.append(entry[0])
     dirs.sort()
 
     print(dirs)
 
+    # Done with fs, restore CWD.
+    os.chdir(curdir)
+
 
 # We need an empty directory for testing.
 # Skip the test if it already exists.
diff --git a/tests/extmod/vfs_posix_paths.py b/tests/extmod/vfs_posix_paths.py
new file mode 100644
index 00000000000..c06318748a3
--- /dev/null
+++ b/tests/extmod/vfs_posix_paths.py
@@ -0,0 +1,90 @@
+# Test for VfsPosix with relative paths
+
+try:
+    import os, vfs
+
+    vfs.VfsPosix
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+# We need a directory for testing that doesn't already exist.
+# Skip the test if it does exist.
+temp_dir = "vfs_posix_paths_test_dir"
+try:
+    os.stat(temp_dir)
+    print("SKIP")
+    raise SystemExit
+except OSError:
+    pass
+
+curdir = os.getcwd()
+os.mkdir(temp_dir)
+
+# construct new VfsPosix with absolute path argument
+temp_dir_abs = os.getcwd() + os.sep + temp_dir
+fs = vfs.VfsPosix(temp_dir_abs)
+# when VfsPosix is used the intended way via vfs.mount(), it can only be called
+# with relative paths when the CWD is inside or at its root, so simulate that
+os.chdir(temp_dir_abs)
+fs.mkdir("subdir")
+fs.mkdir("subdir/one")
+print('listdir("/"):', sorted(i[0] for i in fs.ilistdir("/")))
+print('listdir("."):', sorted(i[0] for i in fs.ilistdir(".")))
+print('getcwd() in ("", "/"):', fs.getcwd() in ("", "/"))
+print('chdir("subdir"):', fs.chdir("subdir"))
+print("getcwd():", fs.getcwd())
+print('mkdir("two"):', fs.mkdir("two"))
+f = fs.open("file.py", "w")
+f.write("print('hello')")
+f.close()
+print('listdir("/"):', sorted(i[0] for i in fs.ilistdir("/")))
+print('listdir("/subdir"):', sorted(i[0] for i in fs.ilistdir("/subdir")))
+print('listdir("."):', sorted(i[0] for i in fs.ilistdir(".")))
+try:
+    f = fs.open("/subdir/file.py", "r")
+    print(f.read())
+    f.close()
+except Exception as e:
+    print(e)
+import sys
+
+sys.path.insert(0, "")
+try:
+    import file
+
+    print(file)
+except Exception as e:
+    print(e)
+del sys.path[0]
+fs.remove("file.py")
+fs.rmdir("two")
+fs.rmdir("/subdir/one")
+fs.chdir("/")
+fs.rmdir("/subdir")
+
+# done with fs, restore CWD
+os.chdir(curdir)
+
+# some integration tests with a mounted VFS
+vfs.mount(vfs.VfsPosix(temp_dir_abs), "/mnt")
+os.mkdir("/mnt/dir")
+print('chdir("/mnt/dir"):', os.chdir("/mnt/dir"))
+print("getcwd():", os.getcwd())
+print('chdir("/mnt"):', os.chdir("/mnt"))
+print("getcwd():", os.getcwd())
+print('chdir("/"):', os.chdir("/"))
+print("getcwd():", os.getcwd())
+print('chdir("/mnt/dir"):', os.chdir("/mnt/dir"))
+print("getcwd():", os.getcwd())
+print('chdir(".."):', os.chdir(".."))
+print("getcwd():", os.getcwd())
+os.rmdir("/mnt/dir")
+vfs.umount("/mnt")
+
+# restore CWD
+os.chdir(curdir)
+
+# rmdir
+os.rmdir(temp_dir)
+print(temp_dir in os.listdir())
diff --git a/tests/extmod/vfs_posix_paths.py.exp b/tests/extmod/vfs_posix_paths.py.exp
new file mode 100644
index 00000000000..d6a0960efd2
--- /dev/null
+++ b/tests/extmod/vfs_posix_paths.py.exp
@@ -0,0 +1,23 @@
+listdir("/"): ['subdir']
+listdir("."): ['subdir']
+getcwd() in ("", "/"): True
+chdir("subdir"): None
+getcwd(): /subdir
+mkdir("two"): None
+listdir("/"): ['subdir']
+listdir("/subdir"): ['file.py', 'one', 'two']
+listdir("."): ['file.py', 'one', 'two']
+print('hello')
+hello
+
+chdir("/mnt/dir"): None
+getcwd(): /mnt/dir
+chdir("/mnt"): None
+getcwd(): /mnt
+chdir("/"): None
+getcwd(): /
+chdir("/mnt/dir"): None
+getcwd(): /mnt/dir
+chdir(".."): None
+getcwd(): /mnt
+False
diff --git a/tests/extmod/vfs_posix_readonly.py b/tests/extmod/vfs_posix_readonly.py
new file mode 100644
index 00000000000..e7821381006
--- /dev/null
+++ b/tests/extmod/vfs_posix_readonly.py
@@ -0,0 +1,99 @@
+# Test for VfsPosix
+
+try:
+    import gc, os, vfs, errno
+
+    vfs.VfsPosix
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+# We need a directory for testing that doesn't already exist.
+# Skip the test if it does exist.
+temp_dir = "vfs_posix_readonly_test_dir"
+try:
+    os.stat(temp_dir)
+    raise SystemExit("Target directory {} exists".format(temp_dir))
+except OSError:
+    pass
+
+# mkdir (skip test if whole filesystem is readonly)
+try:
+    os.mkdir(temp_dir)
+except OSError as e:
+    if e.errno == errno.EROFS:
+        print("SKIP")
+        raise SystemExit
+
+fs_factory = lambda: vfs.VfsPosix(temp_dir)
+
+# mount
+fs = fs_factory()
+vfs.mount(fs, "/vfs")
+
+with open("/vfs/file", "w") as f:
+    f.write("content")
+
+# test reading works
+with open("/vfs/file") as f:
+    print("file:", f.read())
+
+os.mkdir("/vfs/emptydir")
+
+# umount
+vfs.umount("/vfs")
+
+# mount read-only
+fs = fs_factory()
+vfs.mount(fs, "/vfs", readonly=True)
+
+# test reading works
+with open("/vfs/file") as f:
+    print("file 2:", f.read())
+
+# test writing fails
+try:
+    with open("/vfs/test_write", "w"):
+        pass
+    print("opened")
+except OSError as er:
+    print(repr(er))
+
+# test removing fails
+try:
+    os.unlink("/vfs/file")
+    print("unlinked")
+except OSError as er:
+    print(repr(er))
+
+# test renaming fails
+try:
+    os.rename("/vfs/file2", "/vfs/renamed")
+    print("renamed")
+except OSError as er:
+    print(repr(er))
+
+# test removing directory fails
+try:
+    os.rmdir("/vfs/emptydir")
+    print("rmdir'd")
+except OSError as er:
+    print(repr(er))
+
+# test creating directory fails
+try:
+    os.mkdir("/vfs/emptydir2")
+    print("mkdir'd")
+except OSError as er:
+    print(repr(er))
+
+# umount
+vfs.umount("/vfs")
+
+fs = fs_factory()
+vfs.mount(fs, "/vfs")
+
+os.rmdir("/vfs/emptydir")
+os.unlink("/vfs/file")
+
+os.rmdir(temp_dir)
diff --git a/tests/extmod/vfs_posix_readonly.py.exp b/tests/extmod/vfs_posix_readonly.py.exp
new file mode 100644
index 00000000000..40e4316775f
--- /dev/null
+++ b/tests/extmod/vfs_posix_readonly.py.exp
@@ -0,0 +1,7 @@
+file: content
+file 2: content
+OSError(30,)
+OSError(30,)
+OSError(30,)
+OSError(30,)
+OSError(30,)
diff --git a/tests/extmod/vfs_rom.py b/tests/extmod/vfs_rom.py
new file mode 100644
index 00000000000..18ae1f5cf96
--- /dev/null
+++ b/tests/extmod/vfs_rom.py
@@ -0,0 +1,489 @@
+# Test VfsRom filesystem.
+
+try:
+    import errno, sys, struct, os, uctypes, vfs
+
+    vfs.VfsRom
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+try:
+    import select
+except ImportError:
+    select = None
+
+import unittest
+
+IFDIR = 0x4000
+IFREG = 0x8000
+
+SEEK_SET = 0
+SEEK_CUR = 1
+SEEK_END = 2
+
+# An mpy file with four constant objects: str, bytes, long-int, float.
+test_mpy = (
+    # header
+    b"M\x06\x00\x1e"  # mpy file header, -msmall-int-bits=30
+    b"\x06"  # n_qstr
+    b"\x05"  # n_obj
+    # qstrs
+    b"\x0etest.py\x00"  # qstr0 = "test.py"
+    b"\x0f"  # qstr1 = ""
+    b"\x0estr_obj\x00"  # qstr2 = "str_obj"
+    b"\x12bytes_obj\x00"  # qstr3 = "bytes_obj"
+    b"\x0eint_obj\x00"  # qstr4 = "int_obj"
+    b"\x12float_obj\x00"  # qstr5 = "float_obj"
+    # objects
+    b"\x05\x14this is a str object\x00"
+    b"\x06\x16this is a bytes object\x00"
+    b"\x07\x0a1234567890"  # long-int object
+    b"\x08\x041.23"  # float object
+    b"\x05\x07str_obj\x00"  # str object of existing qstr
+    # bytecode
+    b"\x81\x28"  # 21 bytes, no children, bytecode
+    b"\x00\x02"  # prelude
+    b"\x01"  # simple name ()
+    b"\x23\x00"  # LOAD_CONST_OBJ(0)
+    b"\x16\x02"  # STORE_NAME(str_obj)
+    b"\x23\x01"  # LOAD_CONST_OBJ(1)
+    b"\x16\x03"  # STORE_NAME(bytes_obj)
+    b"\x23\x02"  # LOAD_CONST_OBJ(2)
+    b"\x16\x04"  # STORE_NAME(int_obj)
+    b"\x23\x03"  # LOAD_CONST_OBJ(3)
+    b"\x16\x05"  # STORE_NAME(float_obj)
+    b"\x51"  # LOAD_CONST_NONE
+    b"\x63"  # RETURN_VALUE
+)
+
+
+class VfsRomWriter:
+    ROMFS_HEADER = b"\xd2\xcd\x31"
+
+    ROMFS_RECORD_KIND_UNUSED = 0
+    ROMFS_RECORD_KIND_PADDING = 1
+    ROMFS_RECORD_KIND_DATA_VERBATIM = 2
+    ROMFS_RECORD_KIND_DATA_POINTER = 3
+    ROMFS_RECORD_KIND_DIRECTORY = 4
+    ROMFS_RECORD_KIND_FILE = 5
+
+    def __init__(self):
+        self._dir_stack = [(None, bytearray())]
+
+    def _encode_uint(self, value):
+        encoded = [value & 0x7F]
+        value >>= 7
+        while value != 0:
+            encoded.insert(0, 0x80 | (value & 0x7F))
+            value >>= 7
+        return bytes(encoded)
+
+    def _pack(self, kind, payload):
+        return self._encode_uint(kind) + self._encode_uint(len(payload)) + payload
+
+    def _extend(self, data):
+        buf = self._dir_stack[-1][1]
+        buf.extend(data)
+        return len(buf)
+
+    def finalise(self):
+        _, data = self._dir_stack.pop()
+        encoded_kind = VfsRomWriter.ROMFS_HEADER
+        encoded_len = self._encode_uint(len(data))
+        if (len(encoded_kind) + len(encoded_len) + len(data)) % 2 == 1:
+            encoded_len = b"\x80" + encoded_len
+        data = encoded_kind + encoded_len + data
+        return data
+
+    def opendir(self, dirname):
+        self._dir_stack.append((dirname, bytearray()))
+
+    def closedir(self):
+        dirname, dirdata = self._dir_stack.pop()
+        dirdata = self._encode_uint(len(dirname)) + bytes(dirname, "ascii") + dirdata
+        self._extend(self._pack(VfsRomWriter.ROMFS_RECORD_KIND_DIRECTORY, dirdata))
+
+    def mkdata(self, data):
+        assert len(self._dir_stack) == 1
+        return self._extend(self._pack(VfsRomWriter.ROMFS_RECORD_KIND_DATA_VERBATIM, data)) - len(
+            data
+        )
+
+    def mkfile(self, filename, filedata, extra_payload=b""):
+        filename = bytes(filename, "ascii")
+        payload = self._encode_uint(len(filename))
+        payload += filename
+        payload += extra_payload
+        if isinstance(filedata, tuple):
+            sub_payload = self._encode_uint(filedata[0])
+            sub_payload += self._encode_uint(filedata[1])
+            payload += self._pack(VfsRomWriter.ROMFS_RECORD_KIND_DATA_POINTER, sub_payload)
+        else:
+            payload += self._pack(VfsRomWriter.ROMFS_RECORD_KIND_DATA_VERBATIM, filedata)
+        self._dir_stack[-1][1].extend(self._pack(VfsRomWriter.ROMFS_RECORD_KIND_FILE, payload))
+
+
+def _make_romfs(fs, files, data_map):
+    for filename, contents in files:
+        if isinstance(contents, tuple):
+            fs.opendir(filename)
+            _make_romfs(fs, contents, data_map)
+            fs.closedir()
+        elif isinstance(contents, int):
+            fs.mkfile(filename, data_map[contents])
+        else:
+            fs.mkfile(filename, contents)
+
+
+def make_romfs(files, data=None):
+    fs = VfsRomWriter()
+    data_map = {}
+    if data:
+        for k, v in data.items():
+            data_map[k] = len(v), fs.mkdata(v)
+    _make_romfs(fs, files, data_map)
+    return fs.finalise()
+
+
+# A class to test if a value is within a range, needed because MicroPython's range
+# doesn't support arbitrary objects.
+class Range:
+    def __init__(self, lower, upper):
+        self._lower = lower
+        self._upper = upper
+
+    def __repr__(self):
+        return "Range({}, {})".format(self._lower, self._upper)
+
+    def __contains__(self, value):
+        return self._lower <= value < self._upper
+
+
+class TestBase(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        fs_inner = make_romfs((("test_inner.txt", b"contents_inner"), ("c.py", b"")))
+        cls.romfs = make_romfs(
+            (
+                ("fs.romfs", 0),
+                ("test.txt", b"contents"),
+                (
+                    "dir",
+                    (
+                        ("a.py", b"x = 1"),
+                        ("b.py", b"x = 2"),
+                        ("test.mpy", test_mpy),
+                    ),
+                ),
+            ),
+            {0: fs_inner},
+        )
+        cls.romfs_ilistdir = [
+            ("fs.romfs", IFREG, 0, 46),
+            ("test.txt", IFREG, 0, 8),
+            ("dir", IFDIR, 0, 198),
+        ]
+        cls.romfs_listdir = [x[0] for x in cls.romfs_ilistdir]
+        cls.romfs_listdir_dir = ["a.py", "b.py", "test.mpy"]
+        cls.romfs_listdir_bytes = [bytes(x, "ascii") for x in cls.romfs_listdir]
+        cls.romfs_addr = uctypes.addressof(cls.romfs)
+        cls.romfs_addr_range = Range(cls.romfs_addr, cls.romfs_addr + len(cls.romfs))
+
+
+class TestEdgeCases(unittest.TestCase):
+    def test_empty_romfs(self):
+        empty_romfs = make_romfs(())
+        self.assertEqual(empty_romfs, bytes([0x80 | ord("R"), 0x80 | ord("M"), ord("1"), 0]))
+        fs = vfs.VfsRom(empty_romfs)
+        self.assertIsInstance(fs, vfs.VfsRom)
+        fs.mount(True, False)
+        self.assertEqual(list(fs.ilistdir("")), [])
+        self.assertEqual(fs.stat(""), (IFDIR, 0, 0, 0, 0, 0, 0, 0, 0, 0))
+        self.assertEqual(fs.statvfs(""), (1, 0, 0, 0, 0, 0, 0, 0, 0, 32767))
+
+    def test_error(self):
+        for bad_romfs in (b"", b"xxx", b"not a romfs"):
+            with self.assertRaises(OSError) as ctx:
+                vfs.VfsRom(bad_romfs)
+            self.assertEqual(ctx.exception.errno, errno.ENODEV)
+
+    def test_unknown_record(self):
+        fs = VfsRomWriter()
+        fs._extend(fs._pack(VfsRomWriter.ROMFS_RECORD_KIND_PADDING, b"payload"))
+        fs.mkfile(
+            "test",
+            b"contents",
+            extra_payload=fs._pack(VfsRomWriter.ROMFS_RECORD_KIND_PADDING, b"pad"),
+        )
+        romfs = fs.finalise()
+        fs = vfs.VfsRom(romfs)
+        self.assertEqual(list(fs.ilistdir("")), [("test", IFREG, 0, 8)])
+        with fs.open("test", "rb") as f:
+            self.assertEqual(f.read(), b"contents")
+
+
+class TestCorrupt(unittest.TestCase):
+    def test_corrupt_filesystem(self):
+        # Make the filesystem length bigger than the buffer.
+        romfs = bytearray(make_romfs(()))
+        romfs[3] = 0x01
+        with self.assertRaises(OSError):
+            vfs.VfsRom(romfs)
+
+        # Corrupt the filesystem length.
+        romfs = bytearray(make_romfs(()))
+        romfs[3] = 0xFF
+        with self.assertRaises(OSError):
+            vfs.VfsRom(romfs)
+
+        # Corrupt the contents of the filesystem.
+        romfs = bytearray(make_romfs(()))
+        romfs[3] = 0x01
+        romfs.extend(b"\xff\xff")
+        fs = vfs.VfsRom(romfs)
+        with self.assertRaises(OSError):
+            fs.stat("a")
+        self.assertEqual(list(fs.ilistdir("")), [])
+
+    def test_corrupt_file_entry(self):
+        romfs = make_romfs((("file", b"data"),))
+
+        # Corrupt the length of filename.
+        romfs_corrupt = bytearray(romfs)
+        romfs_corrupt[7:] = b"\xff" * (len(romfs) - 7)
+        fs = vfs.VfsRom(romfs_corrupt)
+        with self.assertRaises(OSError):
+            fs.stat("file")
+        self.assertEqual(list(fs.ilistdir("")), [])
+
+        # Erase the data record (change it to a padding record).
+        romfs_corrupt = bytearray(romfs)
+        romfs_corrupt[12] = VfsRomWriter.ROMFS_RECORD_KIND_PADDING
+        fs = vfs.VfsRom(romfs_corrupt)
+        with self.assertRaises(OSError):
+            fs.stat("file")
+        self.assertEqual(list(fs.ilistdir("")), [])
+
+        # Corrupt the header of the data record.
+        romfs_corrupt = bytearray(romfs)
+        romfs_corrupt[12:] = b"\xff" * (len(romfs) - 12)
+        fs = vfs.VfsRom(romfs_corrupt)
+        with self.assertRaises(OSError):
+            fs.stat("file")
+
+        # Corrupt the interior of the data record.
+        romfs_corrupt = bytearray(romfs)
+        romfs_corrupt[13:] = b"\xff" * (len(romfs) - 13)
+        fs = vfs.VfsRom(romfs_corrupt)
+        with self.assertRaises(OSError):
+            fs.stat("file")
+
+        # Change the data record to an indirect pointer and corrupt the length.
+        romfs_corrupt = bytearray(romfs)
+        romfs_corrupt[12] = VfsRomWriter.ROMFS_RECORD_KIND_DATA_POINTER
+        romfs_corrupt[14:18] = b"\xff\xff\xff\xff"
+        fs = vfs.VfsRom(romfs_corrupt)
+        with self.assertRaises(OSError):
+            fs.stat("file")
+
+        # Change the data record to an indirect pointer and corrupt the offset.
+        romfs_corrupt = bytearray(romfs)
+        romfs_corrupt[12] = VfsRomWriter.ROMFS_RECORD_KIND_DATA_POINTER
+        romfs_corrupt[14:18] = b"\x00\xff\xff\xff"
+        fs = vfs.VfsRom(romfs_corrupt)
+        with self.assertRaises(OSError):
+            fs.stat("file")
+
+
+class TestStandalone(TestBase):
+    def test_constructor(self):
+        self.assertIsInstance(vfs.VfsRom(self.romfs), vfs.VfsRom)
+        with self.assertRaises(TypeError):
+            vfs.VfsRom(self.romfs_addr)
+
+    def test_mount(self):
+        vfs.VfsRom(self.romfs).mount(True, False)
+        with self.assertRaises(OSError):
+            vfs.VfsRom(self.romfs).mount(True, True)
+
+    def test_ilistdir(self):
+        fs = vfs.VfsRom(self.romfs)
+        self.assertEqual(list(fs.ilistdir("")), self.romfs_ilistdir)
+        self.assertEqual(list(fs.ilistdir("/")), self.romfs_ilistdir)
+        with self.assertRaises(OSError):
+            fs.ilistdir("does not exist")
+
+    def test_stat(self):
+        fs = vfs.VfsRom(self.romfs)
+        self.assertEqual(fs.stat(""), (IFDIR, 0, 0, 0, 0, 0, 289, 0, 0, 0))
+        self.assertEqual(fs.stat("/"), (IFDIR, 0, 0, 0, 0, 0, 289, 0, 0, 0))
+        self.assertEqual(fs.stat("/test.txt"), (IFREG, 0, 0, 0, 0, 0, 8, 0, 0, 0))
+        self.assertEqual(fs.stat("/dir"), (IFDIR, 0, 0, 0, 0, 0, 198, 0, 0, 0))
+        with self.assertRaises(OSError):
+            fs.stat("/does-not-exist")
+
+    def test_statvfs(self):
+        fs = vfs.VfsRom(self.romfs)
+        self.assertEqual(fs.statvfs(""), (1, 0, 289, 0, 0, 0, 0, 0, 0, 32767))
+
+    def test_open(self):
+        fs = vfs.VfsRom(self.romfs)
+
+        with fs.open("/test.txt", "") as f:
+            self.assertEqual(f.read(), "contents")
+        with fs.open("/test.txt", "rt") as f:
+            self.assertEqual(f.read(), "contents")
+        with fs.open("/test.txt", "rb") as f:
+            self.assertEqual(f.read(), b"contents")
+
+        with self.assertRaises(OSError) as ctx:
+            fs.open("/file-does-not-exist", "")
+        self.assertEqual(ctx.exception.errno, errno.ENOENT)
+
+        with self.assertRaises(OSError) as ctx:
+            fs.open("/dir", "rb")
+        self.assertEqual(ctx.exception.errno, errno.EISDIR)
+
+        with self.assertRaises(OSError):
+            fs.open("/test.txt", "w")
+        with self.assertRaises(OSError):
+            fs.open("/test.txt", "a")
+        with self.assertRaises(OSError):
+            fs.open("/test.txt", "+")
+
+    def test_file_seek(self):
+        fs = vfs.VfsRom(self.romfs)
+        with fs.open("/test.txt", "") as f:
+            self.assertEqual(f.seek(0, SEEK_SET), 0)
+            self.assertEqual(f.seek(3, SEEK_SET), 3)
+            self.assertEqual(f.read(), "tents")
+            self.assertEqual(f.seek(0, SEEK_SET), 0)
+            self.assertEqual(f.seek(100, SEEK_CUR), 8)
+            self.assertEqual(f.seek(-1, SEEK_END), 7)
+            self.assertEqual(f.read(), "s")
+            self.assertEqual(f.seek(1, SEEK_END), 8)
+            with self.assertRaises(OSError):
+                f.seek(-1, SEEK_SET)
+            f.seek(0, SEEK_SET)
+            with self.assertRaises(OSError):
+                f.seek(-1, SEEK_CUR)
+            with self.assertRaises(OSError):
+                f.seek(-100, SEEK_END)
+
+    @unittest.skipIf(select is None, "no select module")
+    def test_file_ioctl_invalid(self):
+        fs = vfs.VfsRom(self.romfs)
+        with fs.open("/test.txt", "") as f:
+            p = select.poll()
+            p.register(f)
+            with self.assertRaises(OSError):
+                p.poll(0)
+
+    def test_memory_mapping(self):
+        fs = vfs.VfsRom(self.romfs)
+        with fs.open("/test.txt", "rb") as f:
+            addr = uctypes.addressof(f)
+            data = memoryview(f)
+            self.assertIn(addr, self.romfs_addr_range)
+            self.assertIn(addr + len(data), self.romfs_addr_range)
+            self.assertEqual(bytes(data), b"contents")
+
+
+class TestMounted(TestBase):
+    def setUp(self):
+        self.orig_sys_path = list(sys.path)
+        self.orig_cwd = os.getcwd()
+        sys.path = []
+        vfs.mount(vfs.VfsRom(self.romfs), "/test_rom")
+
+    def tearDown(self):
+        vfs.umount("/test_rom")
+        os.chdir(self.orig_cwd)
+        sys.path = self.orig_sys_path
+
+    def test_listdir(self):
+        self.assertEqual(os.listdir("/test_rom"), self.romfs_listdir)
+        self.assertEqual(os.listdir("/test_rom/dir"), self.romfs_listdir_dir)
+        self.assertEqual(os.listdir(b"/test_rom"), self.romfs_listdir_bytes)
+
+    def test_chdir(self):
+        os.chdir("/test_rom")
+        self.assertEqual(os.getcwd(), "/test_rom")
+        self.assertEqual(os.listdir(), self.romfs_listdir)
+
+        os.chdir("/test_rom/")
+        self.assertEqual(os.getcwd(), "/test_rom")
+        self.assertEqual(os.listdir(), self.romfs_listdir)
+
+        # chdir within the romfs is not implemented.
+        with self.assertRaises(OSError):
+            os.chdir("/test_rom/dir")
+
+    def test_stat(self):
+        self.assertEqual(os.stat("/test_rom"), (IFDIR, 0, 0, 0, 0, 0, 289, 0, 0, 0))
+        self.assertEqual(os.stat("/test_rom/"), (IFDIR, 0, 0, 0, 0, 0, 289, 0, 0, 0))
+        self.assertEqual(os.stat("/test_rom/test.txt"), (IFREG, 0, 0, 0, 0, 0, 8, 0, 0, 0))
+        self.assertEqual(os.stat("/test_rom/dir"), (IFDIR, 0, 0, 0, 0, 0, 198, 0, 0, 0))
+        with self.assertRaises(OSError):
+            os.stat("/test_rom/does-not-exist")
+
+    def test_open(self):
+        with open("/test_rom/test.txt") as f:
+            self.assertEqual(f.read(), "contents")
+        with open("/test_rom/test.txt", "b") as f:
+            self.assertEqual(f.read(), b"contents")
+
+        with self.assertRaises(OSError) as ctx:
+            open("/test_rom/file-does-not-exist")
+        self.assertEqual(ctx.exception.errno, errno.ENOENT)
+
+        with self.assertRaises(OSError) as ctx:
+            open("/test_rom/dir")
+        self.assertEqual(ctx.exception.errno, errno.EISDIR)
+
+    def test_import_py(self):
+        sys.path.append("/test_rom/dir")
+        a = __import__("a")
+        b = __import__("b")
+        self.assertEqual(a.__file__, "/test_rom/dir/a.py")
+        self.assertEqual(a.x, 1)
+        self.assertEqual(b.__file__, "/test_rom/dir/b.py")
+        self.assertEqual(b.x, 2)
+
+    def test_import_mpy(self):
+        sys.path.append("/test_rom/dir")
+        test = __import__("test")
+        self.assertEqual(test.__file__, "/test_rom/dir/test.mpy")
+        self.assertEqual(test.str_obj, "this is a str object")
+        self.assertEqual(test.bytes_obj, b"this is a bytes object")
+        self.assertEqual(test.int_obj, 1234567890)
+        self.assertEqual(test.float_obj, 1.23)
+        self.assertIn(uctypes.addressof(test.str_obj), self.romfs_addr_range)
+        self.assertIn(uctypes.addressof(test.bytes_obj), self.romfs_addr_range)
+
+    def test_romfs_inner(self):
+        with open("/test_rom/fs.romfs", "rb") as f:
+            romfs_inner = vfs.VfsRom(memoryview(f))
+
+        vfs.mount(romfs_inner, "/test_rom2")
+
+        self.assertEqual(os.listdir("/test_rom2"), ["test_inner.txt", "c.py"])
+
+        sys.path.append("/test_rom2")
+        self.assertEqual(__import__("c").__file__, "/test_rom2/c.py")
+
+        with open("/test_rom2/test_inner.txt") as f:
+            self.assertEqual(f.read(), "contents_inner")
+
+        with open("/test_rom2/test_inner.txt", "rb") as f:
+            addr = uctypes.addressof(f)
+            data = memoryview(f)
+            self.assertIn(addr, self.romfs_addr_range)
+            self.assertIn(addr + len(data), self.romfs_addr_range)
+
+        vfs.umount("/test_rom2")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/extmod/vfs_userfs.py b/tests/extmod/vfs_userfs.py
index 570b8335341..a7b87ec0b0d 100644
--- a/tests/extmod/vfs_userfs.py
+++ b/tests/extmod/vfs_userfs.py
@@ -1,21 +1,20 @@
 # test VFS functionality with a user-defined filesystem
-# also tests parts of uio.IOBase implementation
+# also tests parts of io.IOBase implementation
 
-import usys
+import sys
 
 try:
-    import uio
+    import io, vfs
 
-    uio.IOBase
-    import uos
-
-    uos.mount
+    io.IOBase
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
 
-class UserFile(uio.IOBase):
+class UserFile(io.IOBase):
+    buffer_size = 16
+
     def __init__(self, mode, data):
         assert isinstance(data, bytes)
         self.is_text = mode.find("b") == -1
@@ -39,7 +38,11 @@ def readinto(self, buf):
 
     def ioctl(self, req, arg):
         print("ioctl", req, arg)
-        return 0
+        if req == 4:  # MP_STREAM_CLOSE
+            return 0
+        if req == 11:  # MP_STREAM_GET_BUFFER_SIZE
+            return UserFile.buffer_size
+        return -1
 
 
 class UserFS:
@@ -68,17 +71,41 @@ def open(self, path, mode):
     "/data.txt": b"some data in a text file",
     "/usermod1.py": b"print('in usermod1')\nimport usermod2",
     "/usermod2.py": b"print('in usermod2')",
+    "/usermod3.py": b"syntax error",
+    "/usermod4.mpy": b"syntax error",
+    "/usermod5.py": b"print('in usermod5')",
+    "/usermod6.py": b"print('in usermod6')",
 }
-uos.mount(UserFS(user_files), "/userfs")
+vfs.mount(UserFS(user_files), "/userfs")
 
 # open and read a file
 f = open("/userfs/data.txt")
 print(f.read())
 
 # import files from the user filesystem
-usys.path.append("/userfs")
+sys.path.append("/userfs")
 import usermod1
 
+# import a .py file with a syntax error (file should be closed on error)
+try:
+    import usermod3
+except SyntaxError:
+    print("SyntaxError in usermod3")
+
+# import a .mpy file with a syntax error (file should be closed on error)
+try:
+    import usermod4
+except ValueError:
+    print("ValueError in usermod4")
+
+# Test an import with largest buffer size
+UserFile.buffer_size = 255
+import usermod5
+
+# Test an import with over-size buffer size (should be safely limited internally)
+UserFile.buffer_size = 1024
+import usermod6
+
 # unmount and undo path addition
-uos.umount("/userfs")
-usys.path.pop()
+vfs.umount("/userfs")
+sys.path.pop()
diff --git a/tests/extmod/vfs_userfs.py.exp b/tests/extmod/vfs_userfs.py.exp
index 00ddd95fca5..be8011d567e 100644
--- a/tests/extmod/vfs_userfs.py.exp
+++ b/tests/extmod/vfs_userfs.py.exp
@@ -3,10 +3,37 @@ some data in a text file
 stat /usermod1
 stat /usermod1.py
 open /usermod1.py rb
+ioctl 11 0
 ioctl 4 0
 in usermod1
 stat /usermod2
 stat /usermod2.py
 open /usermod2.py rb
+ioctl 11 0
 ioctl 4 0
 in usermod2
+stat /usermod3
+stat /usermod3.py
+open /usermod3.py rb
+ioctl 11 0
+ioctl 4 0
+SyntaxError in usermod3
+stat /usermod4
+stat /usermod4.py
+stat /usermod4.mpy
+open /usermod4.mpy rb
+ioctl 11 0
+ioctl 4 0
+ValueError in usermod4
+stat /usermod5
+stat /usermod5.py
+open /usermod5.py rb
+ioctl 11 0
+ioctl 4 0
+in usermod5
+stat /usermod6
+stat /usermod6.py
+open /usermod6.py rb
+ioctl 11 0
+ioctl 4 0
+in usermod6
diff --git a/tests/extmod/websocket_basic.py b/tests/extmod/websocket_basic.py
index dabdb76be1e..6ebd65d655f 100644
--- a/tests/extmod/websocket_basic.py
+++ b/tests/extmod/websocket_basic.py
@@ -1,7 +1,7 @@
 try:
-    import uio
-    import uerrno
-    import uwebsocket
+    import io
+    import errno
+    import websocket
 except ImportError:
     print("SKIP")
     raise SystemExit
@@ -9,14 +9,21 @@
 
 # put raw data in the stream and do a websocket read
 def ws_read(msg, sz):
-    ws = uwebsocket.websocket(uio.BytesIO(msg))
+    ws = websocket.websocket(io.BytesIO(msg))
     return ws.read(sz)
 
 
+# put raw data in the stream and do a series of websocket read
+def ws_readn(msg, *args):
+    ws = websocket.websocket(io.BytesIO(msg))
+    for sz in args:
+        yield ws.read(sz)
+
+
 # do a websocket write and then return the raw data from the stream
 def ws_write(msg, sz):
-    s = uio.BytesIO()
-    ws = uwebsocket.websocket(s)
+    s = io.BytesIO()
+    ws = websocket.websocket(s)
     ws.write(msg)
     s.seek(0)
     return s.read(sz)
@@ -24,22 +31,32 @@ def ws_write(msg, sz):
 
 # basic frame
 print(ws_read(b"\x81\x04ping", 4))
-print(ws_read(b"\x80\x04ping", 4))  # FRAME_CONT
 print(ws_write(b"pong", 6))
 
-# split frames are not supported
-# print(ws_read(b"\x01\x04ping", 4))
+# split frames and irregular size reads
+for s in ws_readn(b"\x01\x04ping\x00\x04Ping\x80\x04PING", 6, 4, 2, 2):
+    print(s)
 
 # extended payloads
 print(ws_read(b"\x81~\x00\x80" + b"ping" * 32, 128))
 print(ws_write(b"pong" * 32, 132))
 
-# mask (returned data will be 'mask' ^ 'mask')
-print(ws_read(b"\x81\x84maskmask", 4))
+# 64-bit payload size, unsupported by MicroPython implementation. Framing is lost.
+msg = b"\x81\x7f\x00\x00\x00\x00\x00\x00\x00\x80" + b"ping" * 32
+ws = websocket.websocket(io.BytesIO(msg))
+try:
+    print(ws.read(1))
+except OSError as e:
+    print("ioctl: EIO:", e.errno == errno.EIO)
+
+# mask (returned data will be 'maskmask' ^ 'maskMASK')
+print(ws_read(b"\x81\x88maskmaskMASK", 8))
+# mask w/2-byte payload len (returned data will be 'maskmask' ^ 'maskMASK')
+print(ws_read(b"\x81\xfe\x00\x08maskmaskMASK", 8))
 
 # close control frame
-s = uio.BytesIO(b"\x88\x00")  # FRAME_CLOSE
-ws = uwebsocket.websocket(s)
+s = io.BytesIO(b"\x88\x00")  # FRAME_CLOSE
+ws = websocket.websocket(s)
 print(ws.read(1))
 s.seek(2)
 print(s.read(4))
@@ -49,15 +66,15 @@ def ws_write(msg, sz):
 print(ws_read(b"\x8a\x00\x81\x04pong", 4))  # FRAME_PONG
 
 # close method
-ws = uwebsocket.websocket(uio.BytesIO())
+ws = websocket.websocket(io.BytesIO())
 ws.close()
 
 # ioctl
-ws = uwebsocket.websocket(uio.BytesIO())
+ws = websocket.websocket(io.BytesIO())
 print(ws.ioctl(8))  # GET_DATA_OPTS
 print(ws.ioctl(9, 2))  # SET_DATA_OPTS
 print(ws.ioctl(9))
 try:
     ws.ioctl(-1)
 except OSError as e:
-    print("ioctl: EINVAL:", e.errno == uerrno.EINVAL)
+    print("ioctl: EINVAL:", e.errno == errno.EINVAL)
diff --git a/tests/extmod/websocket_basic.py.exp b/tests/extmod/websocket_basic.py.exp
index 2d7657b5354..e4164ac69b1 100644
--- a/tests/extmod/websocket_basic.py.exp
+++ b/tests/extmod/websocket_basic.py.exp
@@ -1,11 +1,16 @@
 b'ping'
-b'ping'
 b'\x81\x04pong'
+b'pingPi'
+b'ngPI'
+b'NG'
+b''
 b'pingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingping'
 b'\x81~\x00\x80pongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpong'
-b'\x00\x00\x00\x00'
+ioctl: EIO: True
+b'\x00\x00\x00\x00    '
+b'\x00\x00\x00\x00    '
 b''
-b'\x81\x02\x88\x00'
+b'\x88\x00'
 b'ping'
 b'pong'
 0
diff --git a/tests/extmod/websocket_toobig.py b/tests/extmod/websocket_toobig.py
new file mode 100644
index 00000000000..f4c5a74bbce
--- /dev/null
+++ b/tests/extmod/websocket_toobig.py
@@ -0,0 +1,28 @@
+try:
+    import io
+    import errno
+    import websocket
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+try:
+    buf = "x" * 65536
+except MemoryError:
+    print("SKIP")
+    raise SystemExit
+
+
+# do a websocket write and then return the raw data from the stream
+def ws_write(msg, sz):
+    s = io.BytesIO()
+    ws = websocket.websocket(s)
+    ws.write(msg)
+    s.seek(0)
+    return s.read(sz)
+
+
+try:
+    print(ws_write(buf, 1))
+except OSError as e:
+    print("ioctl: ENOBUFS:", e.errno == errno.ENOBUFS)
diff --git a/tests/extmod/websocket_toobig.py.exp b/tests/extmod/websocket_toobig.py.exp
new file mode 100644
index 00000000000..3bbd95282fd
--- /dev/null
+++ b/tests/extmod/websocket_toobig.py.exp
@@ -0,0 +1 @@
+ioctl: ENOBUFS: True
diff --git a/tests/extmod_hardware/machine_can_instances.py b/tests/extmod_hardware/machine_can_instances.py
new file mode 100644
index 00000000000..180f4000c2b
--- /dev/null
+++ b/tests/extmod_hardware/machine_can_instances.py
@@ -0,0 +1,74 @@
+# Test multiple concurrent CAN instances using loopback.
+# Initialising in any order shouldn't break TX, RX or filtering.
+#
+# This test is ported from tests/ports/stm32/pyb_can_instances.py
+
+try:
+    from machine import CAN
+
+    CAN(2, 125_000)  # skip any board which doesn't have at least 2 CAN peripherals
+except (ImportError, ValueError):
+    print("SKIP")
+    raise SystemExit
+
+import time
+import unittest
+
+# Some boards have 3x CAN peripherals, test all three
+HAS_CAN3 = True
+try:
+    CAN(3, 125_000)
+except ValueError:
+    HAS_CAN3 = False
+
+
+class Test(unittest.TestCase):
+    def test_can12(self):
+        self._test_pairs([(1, 2), (2, 1)])
+
+    @unittest.skipUnless(HAS_CAN3, "no CAN3")
+    def test_can3(self):
+        self._test_pairs([(1, 3), (3, 1), (2, 3), (3, 2)])
+
+    def _test_pairs(self, seq):
+        for id_a, id_b in seq:
+            with self.subTest("Testing CAN pair", id_a=id_a, id_b=id_b):
+                self._test_controller_pair(id_a, id_b)
+
+    def _test_controller_pair(self, id_a, id_b):
+        # Setting up each CAN peripheral independently is deliberate here, to catch
+        # catch cases where initialising CAN2 breaks CAN1 or vice versa
+        can_a = CAN(id_a, 125_000, mode=CAN.MODE_SILENT_LOOPBACK)
+        can_a.set_filters([(0x100, 0x700, 0)])
+
+        can_b = CAN(id_b, 125_000, mode=CAN.MODE_SILENT_LOOPBACK)
+        can_b.set_filters([(0x000, 0x7F0, 0)])
+
+        try:
+            # Drain any old messages in RX FIFOs
+            for can in (can_a, can_b):
+                while can.recv():
+                    pass
+
+            for which, id, can in (("A", id_a, can_a), ("B", id_b, can_b)):
+                # print("testing config", which, "with controller", can)
+                # message1 should only receive on can_a, message2 on can_b
+                can.send(0x123, "message1", 0)
+                can.send(0x003, "message2", 0)
+                time.sleep_ms(10)
+                n_recv = 0
+                while res := can.recv():
+                    n_recv += 1
+                    # print(res)
+                    if can == can_a:
+                        self.assertEqual(res[1], b"message1", "can_a should receive message1 only")
+                    if can == can_b:
+                        self.assertEqual(res[1], b"message2", "can_b should receive message2 only")
+                self.assertEqual(n_recv, 1, "Each instance should receive exactly 1 message")
+        finally:
+            can_a.deinit()
+            can_b.deinit()
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/extmod_hardware/machine_can_timings.py b/tests/extmod_hardware/machine_can_timings.py
new file mode 100644
index 00000000000..441059f5da5
--- /dev/null
+++ b/tests/extmod_hardware/machine_can_timings.py
@@ -0,0 +1,60 @@
+# Test machine.CAN timings results
+#
+# Single device test, assumes no connections to the CAN pins
+
+try:
+    from machine import CAN
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+import unittest
+
+from target_wiring import can_args, can_kwargs
+
+
+class TestTimings(unittest.TestCase):
+    def test_bitrate(self):
+        for bitrate in (125_000, 250_000, 500_000, 1_000_000):
+            can = CAN(*can_args, bitrate=bitrate, **can_kwargs)
+            print(can)
+            timings = can.get_timings()
+            print(timings)
+            # Actual bitrate may not be exactly equal to requested rate
+            self.assertAlmostEqual(timings[0], bitrate, delta=1_000)
+            can.deinit()
+
+    def test_sample_point(self):
+        # Verify that tseg1 and tseg2 are set correctly from the sample_point argument
+        for sample_point in (66, 75, 95):
+            can = CAN(*can_args, bitrate=500_000, sample_point=sample_point, **can_kwargs)
+            _bitrate, _sjw, tseg1, tseg2, _fd, _port = can.get_timings()
+            print(f"sample_point={sample_point}, tseg1={tseg1}, tseg2={tseg2}")
+            self.assertAlmostEqual(sample_point / 100, tseg1 / (tseg1 + tseg2), delta=0.05)
+            can.deinit()
+
+    def test_tseg_args(self):
+        # Verify that tseg1 and tseg2 are set correctly and sample_point is ignored if these are provided
+        for tseg1, tseg2 in ((5, 2), (16, 8), (16, 5), (15, 5)):
+            print(f"tseg1={tseg1} tseg2={tseg2}")
+            can = CAN(
+                *can_args, bitrate=250_000, tseg1=tseg1, tseg2=tseg2, sample_point=99, **can_kwargs
+            )
+            bitrate, _sjw, ret_tseg1, ret_tseg2, _fd, _port = can.get_timings()
+            self.assertEqual(ret_tseg1, tseg1)
+            self.assertEqual(ret_tseg2, tseg2)
+
+    def test_invalid_timing_args(self):
+        # Test various kwargs out of their allowed value ranges
+        with self.assertRaises(ValueError):
+            CAN(*can_args, bitrate=250_000, tseg1=55, **can_kwargs)
+        with self.assertRaises(ValueError):
+            CAN(*can_args, bitrate=500_000, tseg2=9, **can_kwargs)
+        with self.assertRaises(ValueError):
+            CAN(*can_args, bitrate=-1, **can_kwargs)
+        with self.assertRaises(ValueError):
+            CAN(*can_args, bitrate=500_000, sample_point=101, **can_kwargs)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/extmod_hardware/machine_counter.py b/tests/extmod_hardware/machine_counter.py
new file mode 100644
index 00000000000..a91d262854e
--- /dev/null
+++ b/tests/extmod_hardware/machine_counter.py
@@ -0,0 +1,101 @@
+# Test machine.Counter implementation
+#
+# IMPORTANT: This test requires hardware connections: the out_pin and in_pin
+# must be wired together.
+
+try:
+    from machine import Counter
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+import sys
+from machine import Pin
+
+if "esp32" in sys.platform:
+    id = 0
+    out_pin = 4
+    in_pin = 5
+elif sys.platform == "mimxrt":
+    if "Teensy" in sys.implementation._machine:
+        id = 0
+        out_pin = "D2"
+        in_pin = "D3"
+else:
+    print("Please add support for this test on this platform.")
+    raise SystemExit
+
+import unittest
+
+out_pin = Pin(out_pin, mode=Pin.OUT)
+in_pin = Pin(in_pin, mode=Pin.IN)
+
+
+def toggle(times):
+    for _ in range(times):
+        out_pin(1)
+        out_pin(0)
+
+
+class TestConnections(unittest.TestCase):
+    def setUp(self):
+        in_pin.init(Pin.IN)
+
+    def test_connections(self):
+        # Test the hardware connections are correct. If this test fails, all tests will fail.
+        out_pin(1)
+        self.assertEqual(1, in_pin())
+        out_pin(0)
+        self.assertEqual(0, in_pin())
+
+
+class TestCounter(unittest.TestCase):
+    def setUp(self):
+        out_pin(0)
+        self.counter = Counter(id, in_pin)
+
+    def tearDown(self):
+        self.counter.deinit()
+
+    def assertCounter(self, value):
+        self.assertEqual(self.counter.value(), value)
+
+    def test_count_rising(self):
+        self.assertCounter(0)
+        toggle(100)
+        self.assertCounter(100)
+        out_pin(1)
+        self.assertEqual(self.counter.value(0), 101)
+        self.assertCounter(0)  # calling value(0) resets
+        out_pin(0)
+        self.assertCounter(0)  # no rising edge
+        out_pin(1)
+        self.assertCounter(1)
+
+    def test_change_directions(self):
+        self.assertCounter(0)
+        toggle(100)
+        self.assertCounter(100)
+        self.counter.init(in_pin, direction=Counter.DOWN)
+        self.assertCounter(0)  # calling init() zeroes the counter
+        self.counter.value(100)  # need to manually reset the value
+        self.assertCounter(100)
+        toggle(25)
+        self.assertCounter(75)
+
+    @unittest.skipIf(sys.platform == "mimxrt", "FALLING edge not supported")
+    def test_count_falling(self):
+        self.counter.init(in_pin, direction=Counter.UP, edge=Counter.FALLING)
+        toggle(20)
+        self.assertCounter(20)
+        out_pin(1)
+        self.assertCounter(20)  # no falling edge
+        out_pin(0)
+        self.assertCounter(21)
+        self.counter.value(-(2**24))
+        toggle(20)
+        self.assertCounter(-(2**24 - 20))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/extmod_hardware/machine_encoder.py b/tests/extmod_hardware/machine_encoder.py
new file mode 100644
index 00000000000..67d1b5f443e
--- /dev/null
+++ b/tests/extmod_hardware/machine_encoder.py
@@ -0,0 +1,158 @@
+# Test machine.Encoder implementation
+#
+# IMPORTANT: This test requires hardware connections:
+# - out0_pin and in0_pin must be wired together.
+# - out1_pin and in1_pin must be wired together.
+
+try:
+    from machine import Encoder
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+import sys
+import unittest
+from machine import Pin
+from target_wiring import encoder_loopback_id, encoder_loopback_out_pins, encoder_loopback_in_pins
+
+PRINT = False
+PIN_INIT_VALUE = 1
+
+id = encoder_loopback_id
+out0_pin, out1_pin = encoder_loopback_out_pins
+in0_pin, in1_pin = encoder_loopback_in_pins
+
+out0_pin = Pin(out0_pin, mode=Pin.OUT)
+in0_pin = Pin(in0_pin, mode=Pin.IN)
+out1_pin = Pin(out1_pin, mode=Pin.OUT)
+in1_pin = Pin(in1_pin, mode=Pin.IN)
+
+
+class TestConnections(unittest.TestCase):
+    def setUp(self):
+        in0_pin.init(Pin.IN)
+        in1_pin.init(Pin.IN)
+
+    def test_connections(self):
+        # Test the hardware connections are correct. If this test fails, all tests will fail.
+        for ch, outp, inp in ((0, out0_pin, in0_pin), (1, out1_pin, in1_pin)):
+            print("Testing channel ", ch)
+            outp(1)
+            self.assertEqual(1, inp())
+            outp(0)
+            self.assertEqual(0, inp())
+
+
+class TestEncoder(unittest.TestCase):
+    def setUp(self):
+        out0_pin(PIN_INIT_VALUE)
+        out1_pin(PIN_INIT_VALUE)
+        self.enc = Encoder(id, in0_pin, in1_pin, phases=1)
+        self.enc2 = Encoder(id + 1, in0_pin, in1_pin, phases=2)
+        self.enc4 = Encoder(id + 2, in0_pin, in1_pin, phases=4)
+        self.pulses = 0  # track the expected encoder position in software
+        if PRINT:
+            print(
+                "\nout0_pin() out1_pin() enc.value() enc2.value() enc4.value() |",
+                out0_pin(),
+                out1_pin(),
+                "|",
+                self.enc.value(),
+                self.enc2.value(),
+                self.enc4.value(),
+            )
+
+    def tearDown(self):
+        self.enc.deinit()
+        try:
+            self.enc2.deinit()
+        except:
+            pass
+        try:
+            self.enc4.deinit()
+        except:
+            pass
+
+    def rotate(self, pulses):
+        for _ in range(abs(pulses)):
+            self.pulses += 1 if (pulses > 0) else -1
+            if pulses > 0:
+                if self.pulses % 2:
+                    out0_pin(not out0_pin())
+                else:
+                    out1_pin(not out1_pin())
+            else:
+                if self.pulses % 2:
+                    out1_pin(not out1_pin())
+                else:
+                    out0_pin(not out0_pin())
+        if PRINT:
+            print(
+                "out0_pin() out1_pin() enc.value() enc2.value() enc4.value() pulses self.pulses |",
+                out0_pin(),
+                out1_pin(),
+                "|",
+                self.enc.value(),
+                self.enc2.value(),
+                self.enc4.value(),
+                "|",
+                pulses,
+                self.pulses,
+            )
+
+    def assertPosition(self, value, value2=None, value4=None):
+        self.assertEqual(self.enc.value(), value)
+        if not value2 is None:
+            self.assertEqual(self.enc2.value(), value2)
+        if not value4 is None:
+            self.assertEqual(self.enc4.value(), value4)
+        pass
+
+    def test_basics(self):
+        self.assertPosition(0)
+        self.rotate(100)
+        self.assertPosition(100 // 4, 100 // 2, 100)
+        self.rotate(-100)
+        self.assertPosition(0)
+
+    def test_partial(self):
+        # With phase=1 (default), need 4x pulses to count a rotation
+        self.assertPosition(0)
+        self.rotate(1)
+        self.assertPosition(1, 1, 1)
+        self.rotate(1)
+        self.assertPosition(1, 1, 2)
+        self.rotate(1)
+        self.assertPosition(1, 2, 3)
+        self.rotate(1)
+        self.assertPosition(1, 2, 4)  # +4
+        self.rotate(1)
+        self.assertPosition(2, 3, 5)
+        self.rotate(1)
+        self.assertPosition(2, 3, 6)
+        self.rotate(1)
+        self.assertPosition(2, 4, 7)
+        self.rotate(1)
+        self.assertPosition(2, 4, 8)  # +4
+        self.rotate(-1)
+        self.assertPosition(2, 4, 7)
+        self.rotate(-3)
+        self.assertPosition(1, 2, 4)  # -4
+        self.rotate(-4)
+        self.assertPosition(0, 0, 0)  # -4
+        self.rotate(-1)
+        self.assertPosition(0, 0, -1)
+        self.rotate(-1)
+        self.assertPosition(0, -1, -2)
+        self.rotate(-1)
+        self.assertPosition(0, -1, -3)
+        self.rotate(-1)
+        self.assertPosition(-1, -2, -4)  # -4
+        self.rotate(-1)
+        self.assertPosition(-1, -2, -5)
+        self.rotate(-3)
+        self.assertPosition(-2, -4, -8)  # -4
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/extmod_hardware/machine_i2c_target.py b/tests/extmod_hardware/machine_i2c_target.py
new file mode 100644
index 00000000000..763e6f4771e
--- /dev/null
+++ b/tests/extmod_hardware/machine_i2c_target.py
@@ -0,0 +1,307 @@
+# Test machine.I2CTarget.
+#
+# IMPORTANT: This test requires hardware connections: a SoftI2C instance must be
+# wired to a hardware I2C target.  See pin definitions below.
+
+import sys
+
+try:
+    from machine import Pin, SoftI2C, I2CTarget
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+import unittest
+
+ADDR = 67
+
+kwargs_target = {}
+
+# Configure pins based on the target.
+if sys.platform == "alif" and sys.implementation._build == "ALIF_ENSEMBLE":
+    args_controller = {"scl": "P1_1", "sda": "P1_0"}
+    args_target = (0,)  # on pins P0_3/P0_2
+elif sys.platform == "esp32":
+    args_controller = {"scl": 5, "sda": 6}
+    args_target = (0,)  # on pins 9/8 for C3 and S3, 18/19 for others
+    kwargs_target = {"scl": 9, "sda": 8}
+elif sys.platform == "rp2":
+    args_controller = {"scl": 5, "sda": 4}
+    args_target = (1,)
+elif sys.platform == "pyboard":
+    if sys.implementation._build == "NUCLEO_WB55":
+        args_controller = {"scl": "B8", "sda": "B9"}
+        args_target = (3,)
+    else:
+        args_controller = {"scl": "X1", "sda": "X2"}
+        args_target = ("X",)
+elif "zephyr-nucleo_wb55rg" in sys.implementation._machine:
+    # PB8=I2C1_SCL, PB9=I2C1_SDA (on Arduino header D15/D14)
+    # PC0=I2C3_SCL, PC1=I2C3_SDA (on Arduino header A0/A1)
+    args_controller = {"scl": Pin(("gpiob", 8)), "sda": Pin(("gpiob", 9))}
+    args_target = ("i2c3",)
+elif "zephyr-rpi_pico" in sys.implementation._machine:
+    args_controller = {"scl": Pin(("gpio0", 5)), "sda": Pin(("gpio0", 4))}
+    args_target = ("i2c1",)  # on gpio7/gpio6
+elif sys.platform == "mimxrt":
+    if "Teensy" in sys.implementation._machine:
+        args_controller = {"scl": "A6", "sda": "A3"}  # D20/D17
+    else:
+        args_controller = {"scl": "D0", "sda": "D1"}
+    args_target = (0,)  # pins 19/18 On Teensy 4.x
+elif sys.platform == "samd":
+    args_controller = {"scl": "D5", "sda": "D1"}
+    args_target = ()
+else:
+    print("Please add support for this test on this platform.")
+    raise SystemExit
+
+
+def config_pull_up():
+    Pin(args_controller["scl"], Pin.OPEN_DRAIN, Pin.PULL_UP)
+    Pin(args_controller["sda"], Pin.OPEN_DRAIN, Pin.PULL_UP)
+
+
+class TestMemory(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.mem = bytearray(8)
+        cls.i2c = SoftI2C(**args_controller)
+        cls.i2c_target = I2CTarget(*args_target, **kwargs_target, addr=ADDR, mem=cls.mem)
+        config_pull_up()
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.i2c_target.deinit()
+
+    def test_scan(self):
+        self.assertIn(ADDR, self.i2c.scan())
+
+    def test_write(self):
+        self.mem[:] = b"01234567"
+        self.i2c.writeto_mem(ADDR, 0, b"test")
+        self.assertEqual(self.mem, bytearray(b"test4567"))
+        self.i2c.writeto_mem(ADDR, 4, b"TEST")
+        self.assertEqual(self.mem, bytearray(b"testTEST"))
+
+    def test_write_wrap(self):
+        self.mem[:] = b"01234567"
+        self.i2c.writeto_mem(ADDR, 6, b"test")
+        self.assertEqual(self.mem, bytearray(b"st2345te"))
+
+    @unittest.skipIf(sys.platform == "esp32", "write lengths larger than buffer unsupported")
+    def test_write_wrap_large(self):
+        self.mem[:] = b"01234567"
+        self.i2c.writeto_mem(ADDR, 0, b"testTESTmore")
+        self.assertEqual(self.mem, bytearray(b"moreTEST"))
+
+    def test_read(self):
+        self.mem[:] = b"01234567"
+        self.assertEqual(self.i2c.readfrom_mem(ADDR, 0, 4), b"0123")
+        self.assertEqual(self.i2c.readfrom_mem(ADDR, 4, 4), b"4567")
+
+    def test_read_wrap(self):
+        self.mem[:] = b"01234567"
+        self.assertEqual(self.i2c.readfrom_mem(ADDR, 0, 4), b"0123")
+        self.assertEqual(self.i2c.readfrom_mem(ADDR, 2, 4), b"2345")
+        self.assertEqual(self.i2c.readfrom_mem(ADDR, 6, 4), b"6701")
+
+    @unittest.skipIf(sys.platform == "esp32", "read lengths larger than buffer unsupported")
+    def test_read_wrap_large(self):
+        self.mem[:] = b"01234567"
+        self.assertEqual(self.i2c.readfrom_mem(ADDR, 0, 12), b"012345670123")
+
+    def test_write_read(self):
+        self.mem[:] = b"01234567"
+        self.assertEqual(self.i2c.writeto(ADDR, b"\x02"), 1)
+        self.assertEqual(self.i2c.readfrom(ADDR, 4), b"2345")
+
+    @unittest.skipIf(sys.platform == "esp32", "read after read unsupported")
+    def test_write_read_read(self):
+        self.mem[:] = b"01234567"
+        self.assertEqual(self.i2c.writeto(ADDR, b"\x02"), 1)
+        self.assertEqual(self.i2c.readfrom(ADDR, 4), b"2345")
+        self.assertEqual(self.i2c.readfrom(ADDR, 4), b"7012")
+
+
+@unittest.skipUnless(hasattr(I2CTarget, "IRQ_END_READ"), "IRQ unsupported")
+class TestMemoryIRQ(unittest.TestCase):
+    @staticmethod
+    def irq_handler(i2c_target):
+        flags = i2c_target.irq().flags()
+        TestMemoryIRQ.events[TestMemoryIRQ.num_events] = flags
+        TestMemoryIRQ.events[TestMemoryIRQ.num_events + 1] = i2c_target.memaddr
+        TestMemoryIRQ.num_events += 2
+
+    @classmethod
+    def setUpClass(cls):
+        cls.mem = bytearray(8)
+        cls.events = [0] * 8
+        cls.num_events = 0
+        cls.i2c = SoftI2C(**args_controller)
+        cls.i2c_target = I2CTarget(*args_target, **kwargs_target, addr=ADDR, mem=cls.mem)
+        cls.i2c_target.irq(TestMemoryIRQ.irq_handler)
+        config_pull_up()
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.i2c_target.deinit()
+
+    @unittest.skipIf(sys.platform == "esp32", "scan doesn't trigger IRQ_END_WRITE")
+    def test_scan(self):
+        TestMemoryIRQ.num_events = 0
+        self.i2c.scan()
+        self.assertEqual(self.events[: self.num_events], [I2CTarget.IRQ_END_WRITE, 0])
+
+    def test_write(self):
+        TestMemoryIRQ.num_events = 0
+        self.mem[:] = b"01234567"
+        self.i2c.writeto_mem(ADDR, 2, b"test")
+        self.assertEqual(self.mem, bytearray(b"01test67"))
+        self.assertEqual(self.events[: self.num_events], [I2CTarget.IRQ_END_WRITE, 2])
+
+    def test_read(self):
+        TestMemoryIRQ.num_events = 0
+        self.mem[:] = b"01234567"
+        self.assertEqual(self.i2c.readfrom_mem(ADDR, 2, 4), b"2345")
+        self.assertEqual(self.events[: self.num_events], [I2CTarget.IRQ_END_READ, 2])
+
+
+@unittest.skipUnless(hasattr(I2CTarget, "IRQ_WRITE_REQ"), "IRQ unsupported")
+@unittest.skipIf(sys.platform == "mimxrt", "not working")
+@unittest.skipIf(sys.platform == "pyboard", "can't queue more than one byte")
+@unittest.skipIf(sys.platform == "samd", "not working")
+@unittest.skipIf(sys.platform == "zephyr", "must call readinto/write in IRQ handler")
+class TestPolling(unittest.TestCase):
+    @staticmethod
+    def irq_handler(i2c_target, buf=bytearray(1)):
+        flags = i2c_target.irq().flags()
+        if flags & I2CTarget.IRQ_READ_REQ:
+            i2c_target.write(b"0123")
+
+    @classmethod
+    def setUpClass(cls):
+        cls.i2c = SoftI2C(**args_controller)
+        cls.i2c_target = I2CTarget(*args_target, addr=ADDR)
+        cls.i2c_target.irq(
+            TestPolling.irq_handler,
+            I2CTarget.IRQ_WRITE_REQ | I2CTarget.IRQ_READ_REQ,
+            hard=True,
+        )
+        config_pull_up()
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.i2c_target.deinit()
+
+    def test_read(self):
+        # Can't write data up front, must wait until IRQ_READ_REQ.
+        # self.assertEqual(self.i2c_target.write(b"abcd"), 4)
+        self.assertEqual(self.i2c.readfrom(ADDR, 4), b"0123")
+
+    def test_write(self):
+        # Can do the read outside the IRQ, but requires IRQ_WRITE_REQ trigger to be set.
+        self.assertEqual(self.i2c.writeto(ADDR, b"0123"), 4)
+        buf = bytearray(8)
+        self.assertEqual(self.i2c_target.readinto(buf), 4)
+        self.assertEqual(buf, b"0123\x00\x00\x00\x00")
+
+
+@unittest.skipUnless(hasattr(I2CTarget, "IRQ_ADDR_MATCH_READ"), "IRQ unsupported")
+class TestIRQ(unittest.TestCase):
+    @staticmethod
+    def irq_handler(i2c_target, buf=bytearray(1)):
+        flags = i2c_target.irq().flags()
+        TestIRQ.events[TestIRQ.num_events] = flags
+        TestIRQ.num_events += 1
+        if flags & I2CTarget.IRQ_READ_REQ:
+            i2c_target.write(b"Y")
+        if flags & I2CTarget.IRQ_WRITE_REQ:
+            i2c_target.readinto(buf)
+            TestIRQ.events[TestIRQ.num_events] = buf[0]
+            TestIRQ.num_events += 1
+
+    @classmethod
+    def setUpClass(cls):
+        cls.events = [0] * 8
+        cls.num_events = 0
+        cls.i2c = SoftI2C(**args_controller)
+        cls.i2c_target = I2CTarget(*args_target, addr=ADDR)
+        cls.i2c_target.irq(
+            TestIRQ.irq_handler,
+            I2CTarget.IRQ_ADDR_MATCH_READ
+            | I2CTarget.IRQ_ADDR_MATCH_WRITE
+            | I2CTarget.IRQ_WRITE_REQ
+            | I2CTarget.IRQ_READ_REQ
+            | I2CTarget.IRQ_END_READ
+            | I2CTarget.IRQ_END_WRITE,
+            hard=True,
+        )
+        config_pull_up()
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.i2c_target.deinit()
+
+    def test_scan(self):
+        TestIRQ.num_events = 0
+        self.i2c.scan()
+        self.assertEqual(
+            self.events[: self.num_events],
+            [
+                I2CTarget.IRQ_ADDR_MATCH_WRITE,
+                I2CTarget.IRQ_END_WRITE,
+            ],
+        )
+
+    def test_write(self):
+        TestIRQ.num_events = 0
+        self.i2c.writeto(ADDR, b"XYZ")
+        self.assertEqual(
+            self.events[: self.num_events],
+            [
+                I2CTarget.IRQ_ADDR_MATCH_WRITE,
+                I2CTarget.IRQ_WRITE_REQ,
+                ord(b"X"),
+                I2CTarget.IRQ_WRITE_REQ,
+                ord(b"Y"),
+                I2CTarget.IRQ_WRITE_REQ,
+                ord(b"Z"),
+                I2CTarget.IRQ_END_WRITE,
+            ],
+        )
+
+    def test_read(self):
+        TestIRQ.num_events = 0
+        self.assertEqual(self.i2c.readfrom(ADDR, 1), b"Y")
+        self.assertEqual(
+            self.events[: self.num_events],
+            [
+                I2CTarget.IRQ_ADDR_MATCH_READ,
+                I2CTarget.IRQ_READ_REQ,
+                I2CTarget.IRQ_READ_REQ,
+                I2CTarget.IRQ_END_READ,
+            ],
+        )
+
+    def test_write_read(self):
+        TestIRQ.num_events = 0
+        self.i2c.writeto(ADDR, b"X", False)
+        self.assertEqual(self.i2c.readfrom(ADDR, 1), b"Y")
+        self.assertEqual(
+            self.events[: self.num_events],
+            [
+                I2CTarget.IRQ_ADDR_MATCH_WRITE,
+                I2CTarget.IRQ_WRITE_REQ,
+                ord(b"X"),
+                I2CTarget.IRQ_END_WRITE,
+                I2CTarget.IRQ_ADDR_MATCH_READ,
+                I2CTarget.IRQ_READ_REQ,
+                I2CTarget.IRQ_READ_REQ,
+                I2CTarget.IRQ_END_READ,
+            ],
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/extmod_hardware/machine_pwm.py b/tests/extmod_hardware/machine_pwm.py
new file mode 100644
index 00000000000..7d4f82fd2fe
--- /dev/null
+++ b/tests/extmod_hardware/machine_pwm.py
@@ -0,0 +1,153 @@
+# Test machine.PWM, frequency and duty cycle (using machine.time_pulse_us).
+#
+# IMPORTANT: This test requires hardware connections: the PWM-output and pulse-input
+# pins must be wired together (see the variable `pwm_loopback_pins`).
+
+try:
+    from machine import time_pulse_us, Pin, PWM
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+import machine, sys, time, unittest
+from target_wiring import pwm_loopback_pins
+
+pwm_freq_limit = 1000000
+freq_margin_per_thousand = 0
+duty_margin_per_thousand = 0
+timing_margin_us = 5
+
+# Slow MCUs cannot capture short pulses using `time_pulse_us` so limit the maximum PWM
+# frequency tested on such targets.
+if hasattr(machine, "freq"):
+    f = machine.freq()
+    if isinstance(f, tuple):
+        f = f[0]
+    if f <= 48_000_000:
+        pwm_freq_limit = 2_000
+    elif f <= 64_000_000:
+        pwm_freq_limit = 5_000
+
+# Tune test parameters based on the target.
+if "esp32" in sys.platform:
+    freq_margin_per_thousand = 2
+    duty_margin_per_thousand = 1
+    timing_margin_us = 20
+elif "esp8266" in sys.platform:
+    pwm_freq_limit = 1_000
+    duty_margin_per_thousand = 3
+    timing_margin_us = 50
+
+
+# Test a specific frequency and duty cycle.
+def _test_freq_duty(self, pulse_in, pwm, freq, duty_u16):
+    print("freq={:<5} duty_u16={:<5} :".format(freq, duty_u16), end="")
+
+    # Check configured freq/duty_u16 is within error bound.
+    freq_error = abs(pwm.freq() - freq) * 1000 // freq
+    duty_error = abs(pwm.duty_u16() - duty_u16) * 1000 // (duty_u16 or 1)
+    print(" freq={} freq_er={}".format(pwm.freq(), freq_error), end="")
+    print(" duty={} duty_er={}".format(pwm.duty_u16(), duty_error), end="")
+    print(" :", end="")
+    self.assertLessEqual(freq_error, freq_margin_per_thousand)
+    self.assertLessEqual(duty_error, duty_margin_per_thousand)
+
+    # Calculate expected timing.
+    expected_total_us = (1_000_000 + freq // 2) // freq
+    expected_high_us = (expected_total_us * duty_u16 + 65535 // 2) // 65535
+    expected_low_us = expected_total_us - expected_high_us
+    expected_us = (expected_low_us, expected_high_us)
+    timeout = 2 * expected_total_us
+
+    # Wait for output to settle.
+    time_pulse_us(pulse_in, 0, timeout)
+    time_pulse_us(pulse_in, 1, timeout)
+
+    if duty_u16 == 0 or duty_u16 == 65535:
+        # Expect a constant output level.
+        no_pulse = (
+            time_pulse_us(pulse_in, 0, timeout) < 0 and time_pulse_us(pulse_in, 1, timeout) < 0
+        )
+        self.assertTrue(no_pulse)
+        if expected_high_us == 0:
+            # Expect a constant low level.
+            self.assertEqual(pulse_in(), 0)
+        else:
+            # Expect a constant high level.
+            self.assertEqual(pulse_in(), 1)
+    else:
+        # Test timing of low and high pulse.
+        n_averaging = 10
+        for level in (0, 1):
+            t = 0
+            time_pulse_us(pulse_in, level, timeout)
+            for _ in range(n_averaging):
+                t += time_pulse_us(pulse_in, level, timeout)
+            t //= n_averaging
+            expected = expected_us[level]
+            print(" level={} timing_er={}".format(level, abs(t - expected)), end="")
+            self.assertLessEqual(abs(t - expected), timing_margin_us)
+
+    print()
+
+
+# Test a specific frequency with multiple duty cycles.
+def _test_freq(self, freq):
+    print()
+    self.pwm.freq(freq)
+    for duty in (0, 10, 25, 50, 75, 90, 100):
+        duty_u16 = duty * 65535 // 100
+        if sys.platform == "esp32":
+            # TODO why is this bit needed to get it working on esp32?
+            self.pwm.init(freq=freq, duty_u16=duty_u16)
+            time.sleep(0.1)
+        self.pwm.duty_u16(duty_u16)
+        _test_freq_duty(self, self.pulse_in, self.pwm, freq, duty_u16)
+
+
+# Given a set of pins, this test class will test multiple frequencies and duty cycles.
+class TestBase:
+    @classmethod
+    def setUpClass(cls):
+        print("set up pins:", cls.pwm_pin, cls.pulse_pin)
+        cls.pwm = PWM(cls.pwm_pin)
+        cls.pulse_in = Pin(cls.pulse_pin, Pin.IN)
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.pwm.deinit()
+
+    def test_freq_50(self):
+        _test_freq(self, 50)
+
+    def test_freq_100(self):
+        _test_freq(self, 100)
+
+    def test_freq_500(self):
+        _test_freq(self, 500)
+
+    def test_freq_1000(self):
+        _test_freq(self, 1000)
+
+    @unittest.skipIf(pwm_freq_limit < 2000, "frequency too high")
+    def test_freq_2000(self):
+        _test_freq(self, 2000)
+
+    @unittest.skipIf(pwm_freq_limit < 5000, "frequency too high")
+    def test_freq_5000(self):
+        _test_freq(self, 5000)
+
+    @unittest.skipIf(pwm_freq_limit < 10000, "frequency too high")
+    def test_freq_10000(self):
+        _test_freq(self, 10000)
+
+
+# Generate test classes, one for each set of pins to test.
+for pwm, pulse in pwm_loopback_pins:
+    cls_name = "Test_{}_{}".format(pwm, pulse)
+    globals()[cls_name] = type(
+        cls_name, (TestBase, unittest.TestCase), {"pwm_pin": pwm, "pulse_pin": pulse}
+    )
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/extmod_hardware/machine_rtc_sleep.py b/tests/extmod_hardware/machine_rtc_sleep.py
new file mode 100644
index 00000000000..380e19a9d50
--- /dev/null
+++ b/tests/extmod_hardware/machine_rtc_sleep.py
@@ -0,0 +1,74 @@
+# Test for RTC ticks during normal sleep and lightsleep.
+
+try:
+    from machine import RTC, lightsleep
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+import sys, time
+
+if "rp2" in sys.platform:
+    pass
+else:
+    # At time of writing most other ports with machine.RTC and
+    # machine.lightsleep() aren't capable of passing this test due to
+    # inconsistent or broken lightsleep behaviour... If you find another port
+    # which can pass the test, please add it above!
+    print("SKIP")
+    raise SystemExit
+
+
+_IDX_MINUTE = const(5)
+_IDX_SECOND = const(6)
+
+_SLEEP_SECS = const(2)
+
+rtc = RTC()
+
+
+def test_sleep(sleep_fn, label):
+    attempts = 0
+    while True:
+        attempts += 1
+        rtc0 = rtc.datetime()
+        t0 = time.ticks_ms()
+        sleep_fn()
+        rtc1 = rtc.datetime()
+        t1 = time.ticks_ms()
+        if attempts < 3 and rtc0[_IDX_SECOND] - rtc1[_IDX_SECOND] == _SLEEP_SECS + 1:
+            # allow the possibility that occasionally the sleep runs a few milliseconds long and
+            # the RTC ticks over an additional second in that small window.
+            continue
+        if rtc0[_IDX_MINUTE] == rtc1[_IDX_MINUTE]:
+            # To avoid having to make datetime calculations, re-run the sleep until it starts
+            # and ends in the same minute!
+            break
+
+    sec0 = rtc0[_IDX_SECOND]
+    sec1 = rtc1[_IDX_SECOND]
+    ticks_diff = time.ticks_diff(t1, t0)
+
+    if sec1 - sec0 == _SLEEP_SECS and abs(_SLEEP_SECS * 1000 - ticks_diff) < 100:
+        print(label, "OK")
+    else:
+        print(label, "Bad sleep time")
+        print("t0", t0)
+        print("t1", t1)
+        print("ticks_diff", ticks_diff)
+        print("rtc0", rtc0)
+        print("rtc1", rtc1)
+
+
+def force_lightsleep(target_ms):
+    # lightsleep for at least target_ms, not only until the next interrupt
+    t_end = time.ticks_add(time.ticks_ms(), target_ms)
+    remaining = time.ticks_diff(t_end, time.ticks_ms())
+    while remaining > 0:
+        lightsleep(remaining)
+        remaining = time.ticks_diff(t_end, time.ticks_ms())
+
+
+test_sleep(lambda: time.sleep(_SLEEP_SECS), "time.sleep")
+test_sleep(lambda: time.sleep_ms(_SLEEP_SECS * 1000), "time.sleep_ms")
+test_sleep(lambda: force_lightsleep(_SLEEP_SECS * 1000), "forced machine.lightsleep")
diff --git a/tests/extmod_hardware/machine_rtc_sleep.py.exp b/tests/extmod_hardware/machine_rtc_sleep.py.exp
new file mode 100644
index 00000000000..a9dd0c56351
--- /dev/null
+++ b/tests/extmod_hardware/machine_rtc_sleep.py.exp
@@ -0,0 +1,3 @@
+time.sleep OK
+time.sleep_ms OK
+forced machine.lightsleep OK
diff --git a/tests/extmod_hardware/machine_sdcard_dma_align.py b/tests/extmod_hardware/machine_sdcard_dma_align.py
new file mode 100644
index 00000000000..5ab7a501930
--- /dev/null
+++ b/tests/extmod_hardware/machine_sdcard_dma_align.py
@@ -0,0 +1,225 @@
+# Test DMA read operations when the buffer alignment in RAM varies.
+#
+# Runs on both pyb.SDCard and machine.SDCard with default arguments,
+# although some port-specific info is needed (see below)
+#
+# Test requirements:
+# A mostly empty FAT formatted SDCard installed in SD socket (a card with other
+# files may be much slower to start the test.)
+import errno
+import os
+import machine
+import micropython
+import sys
+import vfs
+import unittest
+
+from micropython import const
+
+# Use pyb classes on stm32, machine class otherwise
+try:
+    from pyb import SDCard, Timer
+except ImportError:
+    try:
+        from machine import SDCard, Timer
+    except ImportError:
+        print("SKIP")
+        raise SystemExit
+
+_BLOCK_SZ = const(512)
+_OFFS_WIDTH = const(64)  # Should be at least the cache line size plus the GC block size
+_TEST_BUF_SZ = const(_BLOCK_SZ + _OFFS_WIDTH)
+
+MOUNT_POINT = "/sd"
+FILE_PATH = "/sd/stm32_align.blk"
+
+# Skip the whole test if there isn't a mountable SDCard
+try:
+    sd = SDCard()
+    fs = vfs.VfsFat(sd)
+    vfs.mount(sd, MOUNT_POINT)
+    vfs.umount(MOUNT_POINT)
+    del sd
+except (OSError, ValueError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+
+# Set some port-specific parameters for test repeats & interrupt frequency
+# (ports which can issue interrupts at a high frequency don't need to run as many
+# repeats of the test in order to trigger a failure due to cache bugs.)
+if "pyboard" in sys.platform:
+
+    def make_timer(timer_cb):
+        # Pyboard SF6 can do at least this frequency and still run the test,
+        # possibly as high as 40kHz depending on the callback details.
+        return Timer(1, freq=35_000, callback=timer_cb, hard=True)
+
+    REPEATS = 8
+elif "mimxrt" in sys.platform:
+
+    def make_timer(timer_cb):
+        return Timer(-1, period=1, callback=timer_cb, hard=True)
+
+    # virtual timer limited to 1kHz so run more iterations (slow test!)
+    REPEATS = 64
+
+
+def verify_contents(buf, silent=False):
+    # Verify that each byte in 'buf' has the value of its index in the buffer
+    bad_pos = []
+    for i in range(_BLOCK_SZ):
+        bi = buf[i]
+        if bi != i & 0xFF:
+            if silent:
+                return False
+            bad_pos.append((i, bi))
+    if not bad_pos:
+        return True
+
+    assert not silent
+
+    print("{} bad readback values in sector:".format(len(bad_pos)), end="")
+    for i, v in bad_pos:
+        print(" {:#x}={:#x}".format(i, v), end="")
+    print()
+    return False
+
+
+class TestSDAlign(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.sd = SDCard()
+        vfs.mount(cls.sd, MOUNT_POINT)
+        buf = bytearray(_BLOCK_SZ)
+        try:
+            with open(FILE_PATH, "rb") as f:
+                rlen = f.readinto(buf)
+                if rlen != _BLOCK_SZ:
+                    raise RuntimeError("Unexpected length of temporary file ", FILE_PATH, rlen)
+                if not verify_contents(buf):
+                    raise RuntimeError("Corrupt test block in temporary file ", FILE_PATH)
+        except OSError as e:
+            if e.errno != errno.ENOENT:
+                raise e
+
+            print("Creating new temp file...")
+            with open(FILE_PATH, "wb") as f:
+                f.write(bytes(i & 0xFF for i in range(_BLOCK_SZ)))
+
+        # Now look for the sector which holds the temporary file
+        # (assume is in the first 20MB of the SD Card)
+        vfs.umount(MOUNT_POINT)
+        for b in range(1, 40960):
+            res = cls.sd.readblocks(b, buf)
+            if res != 0:
+                raise RuntimeError("Failed to call readblocks on SDCard:", res, "block:", b)
+            if verify_contents(buf, True):
+                print("Temporary file contents found in block {}".format(b))
+                cls.block = b
+                return
+
+        raise RuntimeError(
+            "Contents of temporary file not found near start of SDCard. Too many files?"
+        )
+
+    @classmethod
+    def tearDownClass(cls):
+        try:
+            os.unlink(FILE_PATH)
+            print("Deleted temp file")
+        except OSError:
+            pass
+        try:
+            vfs.umount(MOUNT_POINT)
+        except OSError:
+            pass
+        del cls.sd
+
+    def setUp(self):
+        self.offs = 0
+
+    @micropython.native
+    def _test_reads_inner(self, buf, repeats=REPEATS):
+        for offs in range(_OFFS_WIDTH):
+            with self.subTest(offs=offs):
+                self.offs = offs
+                slice = memoryview(buf)[offs : offs + _BLOCK_SZ]
+                assert len(slice) == _BLOCK_SZ
+                for r in range(repeats):
+                    self.assertEqual(
+                        self.sd.readblocks(self.block, slice),
+                        0,
+                        "Read failed for block {} offs {} repeat {}/{}".format(
+                            self.block, offs, r, repeats
+                        ),
+                    )
+                    self.assertTrue(
+                        verify_contents(slice),
+                        "Verify failed for block {} offs {} repeat {}/{}".format(
+                            self.block, offs, r, repeats
+                        ),
+                    )
+
+    def test_reads(self):
+        # Test reading at all available offsets in a buffer
+        buf = bytearray(_TEST_BUF_SZ)
+        # This test is the most random as any failure depends on speculative reads while
+        # the DMA operation is in progress. We run the test more times to increase the chance
+        # of hitting one of these cases, but even if the issue is present the test only fails
+        # once per ~250 iterations. The other tests inject explicit reads and writes so they fail
+        # more or less immediately.
+        self._test_reads_inner(buf, repeats=REPEATS * 4)
+
+    def test_interrupted_reads(self):
+        # Test reading at all available offsets in a buffer, while an interrupt is
+        # scanning through the whole buffer
+        buf = bytearray(_TEST_BUF_SZ)
+        t = None
+        self.scan = 0
+        self.val = None
+        try:
+
+            @micropython.native
+            def timer_cb(_):
+                # Arbitrary read from somewhere in the buffer
+                self.val = buf[self.scan % _TEST_BUF_SZ]
+                self.scan += 1
+
+            t = make_timer(timer_cb)
+            self._test_reads_inner(buf)
+        finally:
+            if t:
+                t.deinit()
+        # print("scan count", self.scan, self.val)
+
+    def test_interrupted_read_write(self):
+        # Test reading at all available offsets in a buffer, while an interrupt is
+        # writing before & after the DMA buffer
+        buf = bytearray(_TEST_BUF_SZ)
+        t = None
+        try:
+
+            @micropython.native
+            def timer_cb(t):
+                # Arbitrary CPU write just before and after the buffer, trying to dirty a DMA cache line
+                #
+                # Note: we never write into a word overlapping the DMA buffer, because if the buffer is not 4-byte aligned
+                # sdcard_read_blocks() will do a trick to align it temporarily, and this will race with that trick and
+                # corrupt the memory.
+                offs = self.offs
+                if offs > 3:
+                    buf[offs - 4] = 0x55
+                offs += _BLOCK_SZ
+                if offs < _TEST_BUF_SZ - 3:
+                    buf[offs] = 0x56
+
+            t = make_timer(timer_cb)
+            self._test_reads_inner(buf)
+        finally:
+            if t:
+                t.deinit()
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/extmod_hardware/machine_uart_irq_break.py b/tests/extmod_hardware/machine_uart_irq_break.py
new file mode 100644
index 00000000000..8906df36155
--- /dev/null
+++ b/tests/extmod_hardware/machine_uart_irq_break.py
@@ -0,0 +1,37 @@
+# Test machine.UART.IRQ_BREAK firing after a break is received.
+#
+# IMPORTANT: This test requires hardware connections: the UART TX and RX
+# pins must be wired together.
+
+try:
+    from machine import UART
+
+    UART.IRQ_BREAK
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+import time, sys
+from target_wiring import uart_loopback_args, uart_loopback_kwargs
+
+
+def irq(u):
+    print("IRQ_BREAK:", bool(u.irq().flags() & u.IRQ_BREAK), "data:", u.read(1))
+
+
+# Test that the IRQ is called for each break received.
+for bits_per_s in (2400, 9600, 57600):
+    uart = UART(*uart_loopback_args, baudrate=bits_per_s, **uart_loopback_kwargs)
+    uart.irq(irq, uart.IRQ_BREAK)
+
+    print("write", bits_per_s)
+    for i in range(3):
+        uart.write(str(i))
+        uart.flush()
+        time.sleep_ms(10)
+        uart.sendbreak()
+        time.sleep_ms(10)
+        if sys.platform in ("esp32", "psoc-edge"):
+            # On these platforms a read is needed to read in the break byte.
+            uart.read()
+    print("done")
diff --git a/tests/extmod_hardware/machine_uart_irq_break.py.exp b/tests/extmod_hardware/machine_uart_irq_break.py.exp
new file mode 100644
index 00000000000..ca8afe8f2b6
--- /dev/null
+++ b/tests/extmod_hardware/machine_uart_irq_break.py.exp
@@ -0,0 +1,15 @@
+write 2400
+IRQ_BREAK: True data: b'0'
+IRQ_BREAK: True data: b'1'
+IRQ_BREAK: True data: b'2'
+done
+write 9600
+IRQ_BREAK: True data: b'0'
+IRQ_BREAK: True data: b'1'
+IRQ_BREAK: True data: b'2'
+done
+write 57600
+IRQ_BREAK: True data: b'0'
+IRQ_BREAK: True data: b'1'
+IRQ_BREAK: True data: b'2'
+done
diff --git a/tests/extmod_hardware/machine_uart_irq_rx.py b/tests/extmod_hardware/machine_uart_irq_rx.py
new file mode 100644
index 00000000000..92fe218835d
--- /dev/null
+++ b/tests/extmod_hardware/machine_uart_irq_rx.py
@@ -0,0 +1,48 @@
+# Test machine.UART.IRQ_RX firing for each character received.
+#
+# IMPORTANT: This test requires hardware connections: the UART TX and RX
+# pins must be wired together.
+
+try:
+    from machine import UART
+
+    UART.IRQ_RX
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+import time, sys
+
+if "CC3200" in sys.implementation._machine:
+    # CC3200 doesn't work because it's too slow and has an allocation error in the handler.
+    print("SKIP")
+    raise SystemExit
+
+from target_wiring import uart_loopback_args, uart_loopback_kwargs
+
+byte_by_byte = "ItsyBitsy M0" in sys.implementation._machine
+
+
+def irq(u):
+    print("IRQ_RX:", bool(u.irq().flags() & u.IRQ_RX), "data:", u.read(1))
+
+
+text = "1234"
+
+# Test that the IRQ is called for each byte received.
+# Use slow baudrates so that the IRQ has time to run.
+for bits_per_s in (2400, 9600):
+    uart = UART(*uart_loopback_args, baudrate=bits_per_s, **uart_loopback_kwargs)
+    uart.irq(irq, uart.IRQ_RX)
+
+    print("write", bits_per_s)
+    if byte_by_byte:
+        # slow devices need data to be sent slow
+        for c in text:
+            uart.write(c)
+            uart.flush()
+    else:
+        uart.write(text)
+        uart.flush()
+    time.sleep_ms(100)
+    print("done")
diff --git a/tests/extmod_hardware/machine_uart_irq_rx.py.exp b/tests/extmod_hardware/machine_uart_irq_rx.py.exp
new file mode 100644
index 00000000000..945b1e508fa
--- /dev/null
+++ b/tests/extmod_hardware/machine_uart_irq_rx.py.exp
@@ -0,0 +1,12 @@
+write 2400
+IRQ_RX: True data: b'1'
+IRQ_RX: True data: b'2'
+IRQ_RX: True data: b'3'
+IRQ_RX: True data: b'4'
+done
+write 9600
+IRQ_RX: True data: b'1'
+IRQ_RX: True data: b'2'
+IRQ_RX: True data: b'3'
+IRQ_RX: True data: b'4'
+done
diff --git a/tests/extmod_hardware/machine_uart_irq_rxidle.py b/tests/extmod_hardware/machine_uart_irq_rxidle.py
new file mode 100644
index 00000000000..40f781d0788
--- /dev/null
+++ b/tests/extmod_hardware/machine_uart_irq_rxidle.py
@@ -0,0 +1,49 @@
+# Test machine.UART.IRQ_RXIDLE firing after a set of characters are received.
+#
+# IMPORTANT: This test requires hardware connections: the UART TX and RX
+# pins must be wired together.
+
+try:
+    from machine import UART
+
+    UART.IRQ_RXIDLE
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+import time, sys
+from target_wiring import uart_loopback_args, uart_loopback_kwargs
+
+# Target tuning options.
+tune_wait_initial_rxidle = sys.platform == "pyboard"
+
+
+def irq(u):
+    print("IRQ_RXIDLE:", bool(u.irq().flags() & u.IRQ_RXIDLE), "data:", u.read())
+
+
+text = ("12345678", "abcdefgh")
+
+# Test that the IRQ is called for each set of byte received.
+for bits_per_s in (2400, 9600, 115200):
+    print("========")
+    print("bits_per_s:", bits_per_s)
+
+    uart = UART(*uart_loopback_args, baudrate=bits_per_s, **uart_loopback_kwargs)
+
+    # Ignore a possible initial RXIDLE condition after creating UART.
+    if tune_wait_initial_rxidle:
+        uart.irq(lambda _: None, uart.IRQ_RXIDLE)
+        time.sleep_ms(10)
+
+    # Configure desired IRQ.
+    uart.irq(irq, uart.IRQ_RXIDLE)
+
+    for i in range(2):
+        # Write data and wait for IRQ.
+        print("write")
+        uart.write(text[i])
+        uart.flush()
+        print("ready")
+        time.sleep_ms(100)
+        print("done")
diff --git a/tests/extmod_hardware/machine_uart_irq_rxidle.py.exp b/tests/extmod_hardware/machine_uart_irq_rxidle.py.exp
new file mode 100644
index 00000000000..f3c7497e4ce
--- /dev/null
+++ b/tests/extmod_hardware/machine_uart_irq_rxidle.py.exp
@@ -0,0 +1,30 @@
+========
+bits_per_s: 2400
+write
+ready
+IRQ_RXIDLE: True data: b'12345678'
+done
+write
+ready
+IRQ_RXIDLE: True data: b'abcdefgh'
+done
+========
+bits_per_s: 9600
+write
+ready
+IRQ_RXIDLE: True data: b'12345678'
+done
+write
+ready
+IRQ_RXIDLE: True data: b'abcdefgh'
+done
+========
+bits_per_s: 115200
+write
+ready
+IRQ_RXIDLE: True data: b'12345678'
+done
+write
+ready
+IRQ_RXIDLE: True data: b'abcdefgh'
+done
diff --git a/tests/feature_check/byteorder.py b/tests/feature_check/byteorder.py
index 509bd8b1b51..c82a41a24b1 100644
--- a/tests/feature_check/byteorder.py
+++ b/tests/feature_check/byteorder.py
@@ -1,6 +1,3 @@
-try:
-    import usys as sys
-except ImportError:
-    import sys
+import sys
 
 print(sys.byteorder)
diff --git a/tests/feature_check/complex.py.exp b/tests/feature_check/complex.py.exp
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/tests/feature_check/const.py.exp b/tests/feature_check/const.py.exp
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/tests/feature_check/coverage.py.exp b/tests/feature_check/coverage.py.exp
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/tests/feature_check/float.py b/tests/feature_check/float.py
deleted file mode 100644
index d6d2a99d242..00000000000
--- a/tests/feature_check/float.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# detect how many bits of precision the floating point implementation has
-
-try:
-    float
-except NameError:
-    print(0)
-else:
-    if float("1.0000001") == float("1.0"):
-        print(30)
-    elif float("1e300") == float("inf"):
-        print(32)
-    else:
-        print(64)
diff --git a/tests/feature_check/float.py.exp b/tests/feature_check/float.py.exp
deleted file mode 100644
index 900731ffd51..00000000000
--- a/tests/feature_check/float.py.exp
+++ /dev/null
@@ -1 +0,0 @@
-64
diff --git a/tests/feature_check/fstring.py.exp b/tests/feature_check/fstring.py.exp
deleted file mode 100644
index 73cdb8bcc87..00000000000
--- a/tests/feature_check/fstring.py.exp
+++ /dev/null
@@ -1 +0,0 @@
-a=1
diff --git a/tests/feature_check/inlineasm.py b/tests/feature_check/inlineasm.py
new file mode 100644
index 00000000000..0a2858fa451
--- /dev/null
+++ b/tests/feature_check/inlineasm.py
@@ -0,0 +1,89 @@
+# check if the inline assembler is enabled and enumerate its capabilities.
+
+import sys
+
+fields = []
+
+mpy_arch = (getattr(sys.implementation, "_mpy", 0) >> 10) & 0x0F
+if 8 >= mpy_arch >= 3:
+    fields.append("thumb")
+    if mpy_arch >= 7:
+        fields.append("vfp")
+elif 10 >= mpy_arch >= 9:
+    fields.append("xtensa")
+    if mpy_arch == 10:
+        fields.append("windowed")
+elif mpy_arch == 11:
+    fields.append("rv32")
+else:
+    raise SystemExit
+
+arch = fields[0]
+has_asm = False
+if arch == "thumb":
+    try:
+        exec("""
+@micropython.asm_thumb
+def f():
+     add(r0, r0, r0)
+""")
+        has_asm = True
+    except SyntaxError:
+        pass
+elif arch == "xtensa":
+    try:
+        exec("""
+@micropython.asm_xtensa
+def f():
+    add(a0, a0, a0)
+""")
+        has_asm = True
+    except SyntaxError:
+        pass
+elif arch == "rv32":
+    try:
+        exec("""
+@micropython.asm_rv32
+def f():
+    add(a0, a0, a0)
+""")
+        has_asm = True
+    except SyntaxError:
+        pass
+if not has_asm:
+    raise SystemExit
+
+if arch == "thumb":
+    try:
+        exec("""
+@micropython.asm_thumb
+def f():
+    it(eq)
+    nop()
+""")
+        fields.append("thumb2")
+    except SyntaxError:
+        pass
+elif arch == "xtensa":
+    pass
+elif arch == "rv32":
+    try:
+        exec("""
+@micropython.asm_rv32
+def f():
+    sh1add(a0, a0, a0)
+""")
+        fields.append("zba")
+    except SyntaxError:
+        pass
+    try:
+        exec("""
+@micropython.asm_rv32
+def f():
+    cm_mva01s(s0, s1)
+""")
+        fields.append("zcmp")
+    except SyntaxError:
+        pass
+
+print(",".join(fields))
diff --git a/tests/feature_check/int_64.py b/tests/feature_check/int_64.py
new file mode 100644
index 00000000000..4d053782ca8
--- /dev/null
+++ b/tests/feature_check/int_64.py
@@ -0,0 +1,2 @@
+# Check whether 64-bit long integers are supported
+print(1 << 62)
diff --git a/tests/feature_check/int_big.py.exp b/tests/feature_check/int_big.py.exp
deleted file mode 100644
index 9dfe3354d59..00000000000
--- a/tests/feature_check/int_big.py.exp
+++ /dev/null
@@ -1 +0,0 @@
-1000000000000000000000000000000000000000000000
diff --git a/tests/feature_check/native_check.py.exp b/tests/feature_check/native_check.py.exp
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/tests/feature_check/repl_emacs_check.py.exp b/tests/feature_check/repl_emacs_check.py.exp
deleted file mode 100644
index 82a4e28ee4f..00000000000
--- a/tests/feature_check/repl_emacs_check.py.exp
+++ /dev/null
@@ -1,7 +0,0 @@
-MicroPython \.\+ version
-Use \.\+
->>> # Check for emacs keys in REPL
->>> t = \.\+
->>> t == 2
-True
->>> 
diff --git a/tests/feature_check/repl_words_move_check.py.exp b/tests/feature_check/repl_words_move_check.py.exp
deleted file mode 100644
index 82a4e28ee4f..00000000000
--- a/tests/feature_check/repl_words_move_check.py.exp
+++ /dev/null
@@ -1,7 +0,0 @@
-MicroPython \.\+ version
-Use \.\+
->>> # Check for emacs keys in REPL
->>> t = \.\+
->>> t == 2
-True
->>> 
diff --git a/tests/feature_check/reverse_ops.py.exp b/tests/feature_check/reverse_ops.py.exp
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/tests/feature_check/set_check.py.exp b/tests/feature_check/set_check.py.exp
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/tests/feature_check/slice.py.exp b/tests/feature_check/slice.py.exp
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/tests/feature_check/target_info.py b/tests/feature_check/target_info.py
new file mode 100644
index 00000000000..acd6a04d81c
--- /dev/null
+++ b/tests/feature_check/target_info.py
@@ -0,0 +1,45 @@
+# Retrieve the native architecture of the target.
+# See https://docs.micropython.org/en/latest/reference/mpyfiles.html#versioning-and-compatibility-of-mpy-files
+# for more details.
+
+import sys
+
+platform = getattr(sys, "platform", "minimal")
+sys_mpy = getattr(sys.implementation, "_mpy", 0)
+arch = [
+    None,
+    "x86",
+    "x64",
+    "armv6",
+    "armv6m",
+    "armv7m",
+    "armv7em",
+    "armv7emsp",
+    "armv7emdp",
+    "xtensa",
+    "xtensawin",
+    "rv32imc",
+    "rv64imc",
+][(sys_mpy >> 10) & 0x0F]
+arch_flags = sys_mpy >> 16
+build = getattr(sys.implementation, "_build", "unknown")
+thread = getattr(sys.implementation, "_thread", None)
+
+# Detect how many bits of precision the floating point implementation has.
+try:
+    if float("1.0000001") == float("1.0"):
+        float_prec = 30
+    elif float("1e300") == float("inf"):
+        float_prec = 32
+    else:
+        float_prec = 64
+except NameError:
+    float_prec = 0
+
+# Detect the error reporting level (based on the length of the raised exception message).
+try:
+    (lambda: 0)(0)
+except TypeError as er:
+    error_reporting = {0: "none", 27: "terse", 54: "normal", 56: "detailed"}[len(er.value or "")]
+
+print(platform, arch, arch_flags, build, thread, float_prec, len("α") == 1, error_reporting)
diff --git a/tests/feature_check/tstring.py b/tests/feature_check/tstring.py
new file mode 100644
index 00000000000..05322b2ae61
--- /dev/null
+++ b/tests/feature_check/tstring.py
@@ -0,0 +1,5 @@
+# check whether t-strings (PEP-750) are supported
+
+a = 1
+t = t"a={a}"
+print("tstring")
diff --git a/tests/feature_check/uio_module.py b/tests/feature_check/uio_module.py
deleted file mode 100644
index bad8d7c95be..00000000000
--- a/tests/feature_check/uio_module.py
+++ /dev/null
@@ -1,6 +0,0 @@
-try:
-    import uio
-
-    print("uio")
-except ImportError:
-    print("no")
diff --git a/tests/feature_check/uio_module.py.exp b/tests/feature_check/uio_module.py.exp
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/tests/float/array_construct.py b/tests/float/array_construct.py
index f6a3a9dc9d4..6b5c996312d 100644
--- a/tests/float/array_construct.py
+++ b/tests/float/array_construct.py
@@ -1,13 +1,10 @@
 # test construction of array from array with float type
 
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 print(array("f", array("h", [1, 2])))
 print(array("d", array("f", [1, 2])))
diff --git a/tests/float/bytearray_construct_endian.py b/tests/float/bytearray_construct_endian.py
index 47f2b793c06..a971d706e29 100644
--- a/tests/float/bytearray_construct_endian.py
+++ b/tests/float/bytearray_construct_endian.py
@@ -1,12 +1,9 @@
 # test construction of bytearray from array with float type
 
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 print(bytearray(array("f", [1, 2.5])))
diff --git a/tests/float/bytes_construct_endian.py b/tests/float/bytes_construct_endian.py
index 4e15acc8bc7..4dbcf390ea0 100644
--- a/tests/float/bytes_construct_endian.py
+++ b/tests/float/bytes_construct_endian.py
@@ -1,12 +1,9 @@
 # test construction of bytes from array with float type
 
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 print(bytes(array("f", [1, 2.5])))
diff --git a/tests/float/cmath_fun.py b/tests/float/cmath_fun.py
index 39011733b02..0037d7c6559 100644
--- a/tests/float/cmath_fun.py
+++ b/tests/float/cmath_fun.py
@@ -51,6 +51,9 @@
             print("%.5g" % ret)
         elif type(ret) == tuple:
             print("%.5g %.5g" % ret)
+        elif f_name == "exp":
+            # exp amplifies REPR_C inaccuracies, so we need to check one digit less
+            print("complex(%.4g, %.4g)" % (real, ret.imag))
         else:
             # some test (eg cmath.sqrt(-0.5)) disagree with CPython with tiny real part
             real = ret.real
diff --git a/tests/float/complex1.py b/tests/float/complex1.py
index f4107a1390f..4decea2ac6b 100644
--- a/tests/float/complex1.py
+++ b/tests/float/complex1.py
@@ -12,13 +12,14 @@
 print(complex("1+j"))
 print(complex("1+2j"))
 print(complex("-1-2j"))
+print(complex("-1+2j"))
 print(complex("+1-2j"))
 print(complex(" -1-2j "))
 print(complex(" +1-2j "))
+print(complex(" -1+2j "))
 print(complex("nanj"))
 print(complex("nan-infj"))
 print(complex(1, 2))
-print(complex(1j, 2j))
 
 # unary ops
 print(bool(1j))
diff --git a/tests/float/complex1_micropython.py b/tests/float/complex1_micropython.py
new file mode 100644
index 00000000000..6b92f593ef2
--- /dev/null
+++ b/tests/float/complex1_micropython.py
@@ -0,0 +1,6 @@
+# test basic complex number functionality
+
+# CPython 3.14 marks this constructor as deprecated, but it is still currently
+# supported by MicroPython.
+
+print(complex(1j, 2j))
diff --git a/tests/float/complex1_micropython.py.exp b/tests/float/complex1_micropython.py.exp
new file mode 100644
index 00000000000..1defdb822cd
--- /dev/null
+++ b/tests/float/complex1_micropython.py.exp
@@ -0,0 +1 @@
+(-2+1j)
diff --git a/tests/float/float2int_doubleprec_intbig.py b/tests/float/float2int_doubleprec_intbig.py
index adc76d4aeb2..565698d8770 100644
--- a/tests/float/float2int_doubleprec_intbig.py
+++ b/tests/float/float2int_doubleprec_intbig.py
@@ -1,11 +1,7 @@
 # check cases converting float to int, requiring double precision float
 
-try:
-    import ustruct as struct
-    import usys as sys
-except:
-    import struct
-    import sys
+import struct
+import sys
 
 maxsize_bits = 0
 maxsize = sys.maxsize
@@ -63,7 +59,7 @@
 def fp2int_test(num, name, should_fail):
     try:
         x = int(num)
-        passed = ~should_fail
+        passed = not should_fail
     except:
         passed = should_fail
     print("%s: %s" % (name, passed and "passed" or "failed"))
diff --git a/tests/float/float2int_fp30_intbig.py b/tests/float/float2int_fp30_intbig.py
index 4e3c1fa217b..eb65f895025 100644
--- a/tests/float/float2int_fp30_intbig.py
+++ b/tests/float/float2int_fp30_intbig.py
@@ -1,11 +1,7 @@
 # check cases converting float to int, relying only on single precision float
 
-try:
-    import ustruct as struct
-    import usys as sys
-except:
-    import struct
-    import sys
+import struct
+import sys
 
 maxsize_bits = 0
 maxsize = sys.maxsize
@@ -60,7 +56,7 @@
 def fp2int_test(num, name, should_fail):
     try:
         x = int(num)
-        passed = ~should_fail
+        passed = not should_fail
     except:
         passed = should_fail
     print("%s: %s" % (name, passed and "passed" or "failed"))
diff --git a/tests/float/float2int_intbig.py b/tests/float/float2int_intbig.py
index 739f98f804d..17414501c61 100644
--- a/tests/float/float2int_intbig.py
+++ b/tests/float/float2int_intbig.py
@@ -1,11 +1,7 @@
 # check cases converting float to int, relying only on single precision float
 
-try:
-    import ustruct as struct
-    import usys as sys
-except:
-    import struct
-    import sys
+import struct
+import sys
 
 maxsize_bits = 0
 maxsize = sys.maxsize
@@ -63,7 +59,7 @@
 def fp2int_test(num, name, should_fail):
     try:
         x = int(num)
-        passed = ~should_fail
+        passed = not should_fail
     except:
         passed = should_fail
     print("%s: %s" % (name, passed and "passed" or "failed"))
diff --git a/tests/float/float_array.py b/tests/float/float_array.py
index 219b6b86aef..cfff3b220c6 100644
--- a/tests/float/float_array.py
+++ b/tests/float/float_array.py
@@ -1,11 +1,8 @@
 try:
-    from uarray import array
+    from array import array
 except ImportError:
-    try:
-        from array import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 
 def test(a):
@@ -22,4 +19,10 @@ def test(a):
 test(array("f"))
 test(array("d"))
 
-print("{:.4f}".format(array("f", bytes(array("I", [0x3DCCCCCC])))[0]))
+# hand-crafted floats, including non-standard nan
+for float_hex in (0x3DCCCCCC, 0x7F800024, 0x7FC00004):
+    f = array("f", bytes(array("I", [float_hex])))[0]
+    if type(f) is float:
+        print("{:.4e}".format(f))
+    else:
+        print(f)
diff --git a/tests/float/float_format.py b/tests/float/float_format.py
index 98ed0eb096f..0eb8b232b06 100644
--- a/tests/float/float_format.py
+++ b/tests/float/float_format.py
@@ -2,14 +2,25 @@
 
 # general rounding
 for val in (116, 1111, 1234, 5010, 11111):
-    print("%.0f" % val)
-    print("%.1f" % val)
-    print("%.3f" % val)
+    print("Test on %d / 1000:" % val)
+    for fmt in ("%.5e", "%.3e", "%.1e", "%.0e", "%.3f", "%.1f", "%.0f", "%.3g", "%.1g", "%.0g"):
+        print(fmt, fmt % (val / 1000))
+
+# make sure round-up to the next unit is handled properly
+for val in range(4, 9):
+    divi = 10**val
+    print("Test on 99994 / (10 ** %d):" % val)
+    for fmt in ("%.5e", "%.3e", "%.1e", "%.0e", "%.3f", "%.1f", "%.0f", "%.3g", "%.1g", "%.0g"):
+        print(fmt, fmt % (99994 / divi))
 
 # make sure rounding is done at the correct precision
 for prec in range(8):
     print(("%%.%df" % prec) % 6e-5)
 
+# make sure trailing zeroes are added properly
+for prec in range(8):
+    print(("%%.%df" % prec) % 1e19)
+
 # check certain cases that had a digit value of 10 render as a ":" character
 print("%.2e" % float("9" * 51 + "e-39"))
 print("%.2e" % float("9" * 40 + "e-21"))
diff --git a/tests/float/float_format_accuracy.py b/tests/float/float_format_accuracy.py
new file mode 100644
index 00000000000..f9467f9c05d
--- /dev/null
+++ b/tests/float/float_format_accuracy.py
@@ -0,0 +1,73 @@
+# Test accuracy of `repr` conversions.
+# This test also increases code coverage for corner cases.
+
+try:
+    import array, math, random
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# The largest errors come from seldom used very small numbers, near the
+# limit of the representation. So we keep them out of this test to keep
+# the max relative error display useful.
+if float("1e-100") == 0.0:
+    # single-precision
+    float_type = "f"
+    float_size = 4
+    # testing range
+    min_expo = -96  # i.e. not smaller than 1.0e-29
+    # Expected results (given >=50'000 samples):
+    # - MICROPY_FLTCONV_IMPL_EXACT: 100% exact conversions
+    # - MICROPY_FLTCONV_IMPL_APPROX: >=98.53% exact conversions, max relative error <= 1.01e-7
+    min_success = 0.980  # with only 1200 samples, the success rate is lower
+    max_rel_err = 1.1e-7
+    # REPR_C is typically used with FORMAT_IMPL_BASIC, which has a larger error
+    is_REPR_C = float("1.0000001") == float("1.0")
+    if is_REPR_C:  # REPR_C
+        min_success = 0.83
+        max_rel_err = 5.75e-07
+else:
+    # double-precision
+    float_type = "d"
+    float_size = 8
+    # testing range
+    min_expo = -845  # i.e. not smaller than 1.0e-254
+    # Expected results (given >=200'000 samples):
+    # - MICROPY_FLTCONV_IMPL_EXACT: 100% exact conversions
+    # - MICROPY_FLTCONV_IMPL_APPROX: >=99.83% exact conversions, max relative error <= 2.7e-16
+    min_success = 0.997  # with only 1200 samples, the success rate is lower
+    max_rel_err = 2.7e-16
+
+
+# Deterministic pseudorandom generator. Designed to be uniform
+# on mantissa values and exponents, not on the represented number
+def pseudo_randfloat():
+    rnd_buff = bytearray(float_size)
+    for _ in range(float_size):
+        rnd_buff[_] = random.getrandbits(8)
+    return array.array(float_type, rnd_buff)[0]
+
+
+random.seed(42)
+stats = 0
+N = 1200
+max_err = 0
+for _ in range(N):
+    f = pseudo_randfloat()
+    while type(f) is not float or math.isinf(f) or math.isnan(f) or math.frexp(f)[1] <= min_expo:
+        f = pseudo_randfloat()
+
+    str_f = repr(f)
+    f2 = float(str_f)
+    if f2 == f:
+        stats += 1
+    else:
+        error = abs((f2 - f) / f)
+        if max_err < error:
+            max_err = error
+
+print(N, "values converted")
+if stats / N >= min_success and max_err <= max_rel_err:
+    print("float format accuracy OK")
+else:
+    print("FAILED: repr rate=%.3f%% max_err=%.3e" % (100 * stats / N, max_err))
diff --git a/tests/float/float_format_ints.py b/tests/float/float_format_ints.py
index 0bf4baf12d0..1f221bd5e79 100644
--- a/tests/float/float_format_ints.py
+++ b/tests/float/float_format_ints.py
@@ -12,20 +12,49 @@
             print(title, "with format", f_fmt, "gives", f_fmt.format(f))
             print(title, "with format", g_fmt, "gives", g_fmt.format(f))
 
-# Check that powers of 10 (that fit in float32) format correctly.
-for i in range(31):
-    # It works to 12 digits on all platforms *except* qemu-arm, where
-    # 10^11 comes out as 10000000820 or something.
-    print("{:.7g}".format(float("1e" + str(i))))
+# The tests below check border cases involving all mantissa bits.
+# In case of REPR_C, where the mantissa is missing two bits, the
+# the string representation for such numbers might not always be exactly
+# the same but nevertheless be correct, so we must allow a few exceptions.
+is_REPR_C = float("1.0000001") == float("1.0")
 
 # 16777215 is 2^24 - 1, the largest integer that can be completely held
 # in a float32.
-print("{:f}".format(16777215))
+val_str = "{:f}".format(16777215)
+
+# When using REPR_C, 16777215.0 is the same as 16777212.0 or 16777214.4
+# (depending on the implementation of pow() function, the result may differ)
+if is_REPR_C and (val_str == "16777212.000000" or val_str == "16777214.400000"):
+    val_str = "16777215.000000"
+
+print(val_str)
+
 # 4294967040 = 16777215 * 128 is the largest integer that is exactly
 # represented by a float32 and that will also fit within a (signed) int32.
 # The upper bound of our integer-handling code is actually double this,
 # but that constant might cause trouble on systems using 32 bit ints.
-print("{:f}".format(2147483520))
+val_str = "{:f}".format(2147483520)
+
+# When using FLOAT_IMPL_FLOAT, 2147483520.0 == 2147483500.0
+# Both representations are valid, the second being "simpler"
+is_float32 = float("1e300") == float("inf")
+if is_float32 and val_str == "2147483500.000000":
+    val_str = "2147483520.000000"
+
+# When using REPR_C, 2147483520.0 is the same as 2147483200.0
+# Both representations are valid, the second being "simpler"
+if is_REPR_C and val_str == "2147483200.000000":
+    val_str = "2147483520.000000"
+
+# When using REPR_C, x86 and clang, 2147483520.0 is the same
+# as 2147483100.0, the second being "simple" but rounded differently
+# due to x87 extra precision on intermediates.
+# Both representations are valid.
+if is_REPR_C and val_str == "2147483100.000000":
+    val_str = "2147483520.000000"
+
+print(val_str)
+
 # Very large positive integers can be a test for precision and resolution.
 # This is a weird way to represent 1e38 (largest power of 10 for float32).
 print("{:.6e}".format(float("9" * 30 + "e8")))
diff --git a/tests/float/float_format_ints_power10.py b/tests/float/float_format_ints_power10.py
new file mode 100644
index 00000000000..7ea200f18ef
--- /dev/null
+++ b/tests/float/float_format_ints_power10.py
@@ -0,0 +1,8 @@
+# Test that integers format to exact values.
+# This test requires at least 32-bit floats (won't work with 30-bit).
+
+# Check that powers of 10 (that fit in float32) format correctly.
+for i in range(31):
+    # It works to 12 digits on all platforms *except* qemu, where
+    # 10^11 comes out as 10000000820 or something.
+    print(i, "{:.7g}".format(float("1e" + str(i))))
diff --git a/tests/float/float_parse.py b/tests/float/float_parse.py
index de27c33e7be..6131da0a63a 100644
--- a/tests/float/float_parse.py
+++ b/tests/float/float_parse.py
@@ -31,6 +31,9 @@
 print(float("1e18446744073709551621"))
 print(float("1e-18446744073709551621"))
 
+# mantissa overflow while processing deferred trailing zeros
+print(float("10000000000000000000001"))
+
 # check small decimals are as close to their true value as possible
 for n in range(1, 10):
     print(float("0.%u" % n) == n / 10)
diff --git a/tests/float/float_parse_doubleprec.py b/tests/float/float_parse_doubleprec.py
index 81fcadcee88..c1b0b4823b0 100644
--- a/tests/float/float_parse_doubleprec.py
+++ b/tests/float/float_parse_doubleprec.py
@@ -19,3 +19,9 @@
 print(float("1.00000000000000000000e-307"))
 print(float("10.0000000000000000000e-308"))
 print(float("100.000000000000000000e-309"))
+
+# ensure repr() adds an extra digit when needed for accurate parsing
+print(float(repr(float("2.0") ** 100)) == float("2.0") ** 100)
+
+# ensure repr does not add meaningless extra digits (1.234999999999)
+print(repr(1.2345))
diff --git a/tests/float/float_struct.py b/tests/float/float_struct.py
index 18893af0e0a..6e282a6afe3 100644
--- a/tests/float/float_struct.py
+++ b/tests/float/float_struct.py
@@ -1,9 +1,6 @@
 # test struct package with floats
 try:
-    try:
-        import ustruct as struct
-    except:
-        import struct
+    import struct
 except ImportError:
     print("SKIP")
     raise SystemExit
@@ -11,7 +8,7 @@
 i = 1.0 + 1 / 2
 # TODO: it looks like '=' format modifier is not yet supported
 # for fmt in ('f', 'd', '>f', '>d', 'f", ">d", "e", ">f", ">d", "", "=", "^"):
                 for fill in ("", " ", "0", "@"):
                     for sign in ("", "+", "-", " "):
-                        # An empty precision defaults to 6, but when uPy is
+                        # An empty precision defaults to 6, but when MicroPython is
                         # configured to use a float, we can only use a
                         # precision of 6 with numbers less than 10 and still
                         # get results that compare to CPython (which uses
@@ -164,7 +164,7 @@ def test_fmt(conv, fill, alignment, sign, prefix, width, precision, type, arg):
         for alignment in ("", "<", ">", "=", "^"):
             for fill in ("", " ", "0", "@"):
                 for sign in ("", "+", "-", " "):
-                    # An empty precision defaults to 6, but when uPy is
+                    # An empty precision defaults to 6, but when MicroPython is
                     # configured to use a float, we can only use a
                     # precision of 6 with numbers less than 10 and still
                     # get results that compare to CPython (which uses
diff --git a/tests/float/string_format_fp30.py b/tests/float/string_format_fp30.py
deleted file mode 100644
index 5f0b213daa3..00000000000
--- a/tests/float/string_format_fp30.py
+++ /dev/null
@@ -1,42 +0,0 @@
-def test(fmt, *args):
-    print("{:8s}".format(fmt) + ">" + fmt.format(*args) + "<")
-
-
-test("{:10.4}", 123.456)
-test("{:10.4e}", 123.456)
-test("{:10.4e}", -123.456)
-# test("{:10.4f}", 123.456)
-# test("{:10.4f}", -123.456)
-test("{:10.4g}", 123.456)
-test("{:10.4g}", -123.456)
-test("{:10.4n}", 123.456)
-test("{:e}", 100)
-test("{:f}", 200)
-test("{:g}", 300)
-
-test("{:10.4E}", 123.456)
-test("{:10.4E}", -123.456)
-# test("{:10.4F}", 123.456)
-# test("{:10.4F}", -123.456)
-test("{:10.4G}", 123.456)
-test("{:10.4G}", -123.456)
-
-test("{:06e}", float("inf"))
-test("{:06e}", float("-inf"))
-test("{:06e}", float("nan"))
-
-# The following fails right now
-# test("{:10.1}", 0.0)
-
-print("%.0f" % (1.750000 % 0.08333333333))
-# Below isn't compatible with single-precision float
-# print("%.1f" % (1.750000 % 0.08333333333))
-# print("%.2f" % (1.750000 % 0.08333333333))
-# print("%.12f" % (1.750000 % 0.08333333333))
-
-# tests for errors in format string
-
-try:
-    "{:10.1b}".format(0.0)
-except ValueError:
-    print("ValueError")
diff --git a/tests/float/string_format_modulo.py b/tests/float/string_format_modulo.py
index 3c206b73935..55314643351 100644
--- a/tests/float/string_format_modulo.py
+++ b/tests/float/string_format_modulo.py
@@ -6,7 +6,7 @@
 print("%u" % 1.0)
 
 # these 3 have different behaviour in Python 3.x versions
-# uPy raises a TypeError, following Python 3.5 (earlier versions don't)
+# MicroPython raises a TypeError, following Python 3.5 (earlier versions don't)
 # print("%x" % 18.0)
 # print("%o" % 18.0)
 # print("%X" % 18.0)
diff --git a/tests/float/string_format_modulo3.py b/tests/float/string_format_modulo3.py
index f9d9c43cdf4..f8aeeda20f2 100644
--- a/tests/float/string_format_modulo3.py
+++ b/tests/float/string_format_modulo3.py
@@ -1,3 +1,3 @@
-# uPy and CPython outputs differ for the following
+# Test corner cases where MicroPython and CPython outputs used to differ in the past
 print("%.1g" % -9.9)  # round up 'g' with '-' sign
 print("%.2g" % 99.9)  # round up
diff --git a/tests/float/string_format_modulo3.py.exp b/tests/float/string_format_modulo3.py.exp
deleted file mode 100644
index 71432b34045..00000000000
--- a/tests/float/string_format_modulo3.py.exp
+++ /dev/null
@@ -1,2 +0,0 @@
--10
-100
diff --git a/tests/float/string_fstring.py b/tests/float/string_fstring.py
new file mode 100644
index 00000000000..3b4233559e3
--- /dev/null
+++ b/tests/float/string_fstring.py
@@ -0,0 +1,7 @@
+# Format specifiers with nested replacement fields
+space = 5
+prec = 2
+print(f"{3.14:{space}.{prec}}")
+
+space_prec = "5.2"
+print(f"{3.14:{space_prec}}")
diff --git a/tests/frozen/README.md b/tests/frozen/README.md
deleted file mode 100644
index bd786d5a3c4..00000000000
--- a/tests/frozen/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-This is a .mpy built against the current .mpy version that can be used to test
-freezing without a dependency on mpy-cross.
diff --git a/tests/frozen/frozentest.mpy b/tests/frozen/frozentest.mpy
deleted file mode 100644
index 99581617ac3..00000000000
Binary files a/tests/frozen/frozentest.mpy and /dev/null differ
diff --git a/tests/import/broken/pkg2_and_zerodiv.py b/tests/import/broken/pkg2_and_zerodiv.py
new file mode 100644
index 00000000000..3580628ff51
--- /dev/null
+++ b/tests/import/broken/pkg2_and_zerodiv.py
@@ -0,0 +1,2 @@
+import pkg2
+import broken.zerodiv
diff --git a/tests/import/broken/zerodiv.py b/tests/import/broken/zerodiv.py
new file mode 100644
index 00000000000..72dca4d5e47
--- /dev/null
+++ b/tests/import/broken/zerodiv.py
@@ -0,0 +1 @@
+1 / 0
diff --git a/tests/import/builtin_ext.py b/tests/import/builtin_ext.py
index dd8f95cbc13..aecbbb2639a 100644
--- a/tests/import/builtin_ext.py
+++ b/tests/import/builtin_ext.py
@@ -1,3 +1,10 @@
+try:
+    import uos, utime
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Verify that sys is a builtin.
 import sys
 
 print(sys, hasattr(sys, "__file__"))
@@ -7,6 +14,8 @@
 
 # All three should only get builtins, despite sys.py, usys.py, and
 # micropython.py being in the path.
+# usys isn't extensible, but has a special-cased alias for backwards
+# compatibility.
 import micropython
 
 print(micropython, hasattr(micropython, "__file__"))
@@ -31,6 +40,7 @@
 import uos
 
 print(uos, hasattr(uos, "__file__"), hasattr(uos, "extra"))
+
 import utime
 
 print(utime, hasattr(utime, "__file__"), hasattr(utime, "extra"))
diff --git a/tests/import/builtin_ext.py.exp b/tests/import/builtin_ext.py.exp
index 08496e14b71..e09af2843af 100644
--- a/tests/import/builtin_ext.py.exp
+++ b/tests/import/builtin_ext.py.exp
@@ -6,5 +6,5 @@ os from filesystem
  True / 1
 time from filesystem
  True  1
- False False
- False False
+ False False
+ False False
diff --git a/tests/import/builtin_import.py b/tests/import/builtin_import.py
index 734498d1be4..364b0bae912 100644
--- a/tests/import/builtin_import.py
+++ b/tests/import/builtin_import.py
@@ -20,3 +20,12 @@
     __import__("xyz", None, None, None, -1)
 except ValueError:
     print("ValueError")
+
+# globals is not checked for level=0
+__import__("builtins", "globals")
+
+# globals must be a dict (or None) for level>0
+try:
+    __import__("builtins", "globals", None, None, 1)
+except TypeError:
+    print("TypeError")
diff --git a/tests/import/circular/main.py b/tests/import/circular/main.py
new file mode 100644
index 00000000000..5d63d507c32
--- /dev/null
+++ b/tests/import/circular/main.py
@@ -0,0 +1,4 @@
+x = 1
+import circular.sub
+
+print(circular.sub.y)
diff --git a/tests/import/circular/sub.py b/tests/import/circular/sub.py
new file mode 100644
index 00000000000..50d7afe07bc
--- /dev/null
+++ b/tests/import/circular/sub.py
@@ -0,0 +1,3 @@
+from circular.main import x
+
+y = x + 20
diff --git a/tests/import/ext/micropython.py b/tests/import/ext/micropython.py
index 88c8b770e17..251e1e0aa91 100644
--- a/tests/import/ext/micropython.py
+++ b/tests/import/ext/micropython.py
@@ -1,2 +1,2 @@
-# micropython is always builtin and cannot be overriden by the filesystem.
+# micropython is always builtin and cannot be overridden by the filesystem.
 print("ERROR: micropython from filesystem")
diff --git a/tests/import/ext/sys.py b/tests/import/ext/sys.py
index 71ee633e119..a041c407f53 100644
--- a/tests/import/ext/sys.py
+++ b/tests/import/ext/sys.py
@@ -1,2 +1,2 @@
-# sys is always builtin and cannot be overriden by the filesystem.
+# sys is always builtin and cannot be overridden by the filesystem.
 print("ERROR: sys from filesystem")
diff --git a/tests/import/ext/time.py b/tests/import/ext/time.py
index 6f2d4a42c03..6b460b42355 100644
--- a/tests/import/ext/time.py
+++ b/tests/import/ext/time.py
@@ -4,11 +4,11 @@
 # import.
 import sys
 
-_path = sys.path[:]
-sys.path.clear()
+_path = sys.path
+sys.path = ()
 from time import *
 
-sys.path.extend(_path)
+sys.path = _path
 del _path
 
 extra = 1
diff --git a/tests/import/ext/usys.py b/tests/import/ext/usys.py
index d7629a4499d..302d1df338b 100644
--- a/tests/import/ext/usys.py
+++ b/tests/import/ext/usys.py
@@ -1,3 +1,3 @@
-# usys (and any u-prefix) is always builtin and cannot be overriden by the
+# usys (and any u-prefix) is always builtin and cannot be overridden by the
 # filesystem.
 print("ERROR: usys from filesystem")
diff --git a/tests/import/import_broken.py b/tests/import/import_broken.py
new file mode 100644
index 00000000000..440922157ef
--- /dev/null
+++ b/tests/import/import_broken.py
@@ -0,0 +1,38 @@
+try:
+    Exception.__class__
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+import sys, pkg
+
+# Modules we import are usually added to sys.modules.
+print("pkg" in sys.modules)
+
+try:
+    from broken.zerodiv import x
+except Exception as e:
+    print(e.__class__.__name__)
+
+# The broken module we tried to import should not be in sys.modules.
+print("broken.zerodiv" in sys.modules)
+
+# If we try to import the module again, the code should
+# run again and we should get the same error.
+try:
+    from broken.zerodiv import x
+except Exception as e:
+    print(e.__class__.__name__)
+
+# Import a module that successfully imports some other modules
+# before importing the problematic module.
+try:
+    import broken.pkg2_and_zerodiv
+except ZeroDivisionError:
+    pass
+
+print("pkg2" in sys.modules)
+print("pkg2.mod1" in sys.modules)
+print("pkg2.mod2" in sys.modules)
+print("broken.zerodiv" in sys.modules)
+print("broken.pkg2_and_zerodiv" in sys.modules)
diff --git a/tests/import/import_circular.py b/tests/import/import_circular.py
new file mode 100644
index 00000000000..388efdd1308
--- /dev/null
+++ b/tests/import/import_circular.py
@@ -0,0 +1 @@
+import circular.main
diff --git a/tests/import/import_file.py b/tests/import/import_file.py
index 90ec4e41e77..4cf307641c6 100644
--- a/tests/import/import_file.py
+++ b/tests/import/import_file.py
@@ -1,3 +1,7 @@
+if "__file__" not in globals():
+    print("SKIP")
+    raise SystemExit
+
 import import1b
 
 print(import1b.__file__)
diff --git a/tests/import/import_override.py b/tests/import/import_override.py
index 029ebe54c1f..0144e78cb9f 100644
--- a/tests/import/import_override.py
+++ b/tests/import/import_override.py
@@ -2,6 +2,10 @@
 
 
 def custom_import(name, globals, locals, fromlist, level):
+    # CPython always tries to import _io, so just let that through as-is.
+    if name == "_io":
+        return orig_import(name, globals, locals, fromlist, level)
+
     print("import", name, fromlist, level)
 
     class M:
diff --git a/tests/import/import_override.py.exp b/tests/import/import_override.py.exp
deleted file mode 100644
index 365248da6d8..00000000000
--- a/tests/import/import_override.py.exp
+++ /dev/null
@@ -1,2 +0,0 @@
-import import1b None 0
-456
diff --git a/tests/import/import_override2.py b/tests/import/import_override2.py
new file mode 100644
index 00000000000..25aac44fe98
--- /dev/null
+++ b/tests/import/import_override2.py
@@ -0,0 +1,18 @@
+# test overriding __import__ combined with importing from the filesystem
+
+
+def custom_import(name, globals, locals, fromlist, level):
+    if level > 0:
+        print("import", name, fromlist, level)
+    return orig_import(name, globals, locals, fromlist, level)
+
+
+orig_import = __import__
+try:
+    __import__("builtins").__import__ = custom_import
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+# import calls __import__ behind the scenes
+import pkg7.subpkg1.subpkg2.mod3
diff --git a/tests/import/import_pkg7.py.exp b/tests/import/import_pkg7.py.exp
deleted file mode 100644
index 8f21a615f6a..00000000000
--- a/tests/import/import_pkg7.py.exp
+++ /dev/null
@@ -1,8 +0,0 @@
-pkg __name__: pkg7
-pkg __name__: pkg7.subpkg1
-pkg __name__: pkg7.subpkg1.subpkg2
-mod1
-mod2
-mod1.foo
-mod2.bar
-ImportError
diff --git a/tests/import/import_pkg9.py b/tests/import/import_pkg9.py
index 4de028494f1..c2e0b618b69 100644
--- a/tests/import/import_pkg9.py
+++ b/tests/import/import_pkg9.py
@@ -13,4 +13,4 @@
 import pkg9.mod2
 
 pkg9.mod1()
-print(pkg9.mod2.__name__, type(pkg9.mod2).__name__)
+print(pkg9.mod2.__name__, type(pkg9.mod2))
diff --git a/tests/import/import_star.py b/tests/import/import_star.py
new file mode 100644
index 00000000000..dc34577d772
--- /dev/null
+++ b/tests/import/import_star.py
@@ -0,0 +1,71 @@
+# test `from package import *` conventions, including __all__ support
+#
+# This test requires MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_BASIC_FEATURES
+
+try:
+    next(iter([]), 42)
+except TypeError:
+    # 2-argument version of next() not supported
+    # we are probably not at MICROPY_CONFIG_ROM_LEVEL_BASIC_FEATURES
+    print("SKIP")
+    raise SystemExit
+
+# 1. test default visibility
+from pkgstar_default import *
+
+print("visibleFun" in globals())
+print("VisibleClass" in globals())
+print("_hiddenFun" in globals())
+print("_HiddenClass" in globals())
+print(visibleFun())
+
+# 2. test explicit visibility as defined by __all__ (as an array)
+from pkgstar_all_array import *
+
+print("publicFun" in globals())
+print("PublicClass" in globals())
+print("unlistedFun" in globals())
+print("UnlistedClass" in globals())
+print("_privateFun" in globals())
+print("_PrivateClass" in globals())
+print(publicFun())
+# test dynamic import as used in asyncio
+print("dynamicFun" in globals())
+print(dynamicFun())
+
+# 3. test explicit visibility as defined by __all__ (as an tuple)
+from pkgstar_all_tuple import *
+
+print("publicFun2" in globals())
+print("PublicClass2" in globals())
+print("unlistedFun2" in globals())
+print("UnlistedClass2" in globals())
+print(publicFun2())
+
+# 4. test reporting of missing entries in __all__
+try:
+    from pkgstar_all_miss import *
+
+    print("missed detection of incorrect __all__ definition")
+except AttributeError as er:
+    print("AttributeError triggered for bad __all__ definition")
+
+# 5. test reporting of invalid __all__ definition
+try:
+    from pkgstar_all_inval import *
+
+    print("missed detection of incorrect __all__ definition")
+except TypeError as er:
+    print("TypeError triggered for bad __all__ definition")
+
+# 6. test when package uses __getattr__ and raises AttributeError for __getattr__("__all__")
+from pkgstar_getattr_attr_er import *
+
+print("publicFun3" in globals())
+print(publicFun3())
+
+# 7. test when package uses __getattr__ and raises ValueError for __getattr__("__all__")
+try:
+    from pkgstar_getattr_value_er import *
+except ValueError as er:
+    print("ValueError triggered with args", er.args)
diff --git a/tests/import/import_star_error.py b/tests/import/import_star_error.py
index 9e1757b6ef5..73d9c863d6f 100644
--- a/tests/import/import_star_error.py
+++ b/tests/import/import_star_error.py
@@ -1,5 +1,10 @@
 # test errors with import *
 
+if not hasattr(object, "__init__"):
+    # target doesn't have MICROPY_CPYTHON_COMPAT enabled, so doesn't check for "import *"
+    print("SKIP")
+    raise SystemExit
+
 # 'import *' is not allowed in function scope
 try:
     exec("def foo(): from x import *")
diff --git a/tests/import/module_getattr.py.exp b/tests/import/module_getattr.py.exp
deleted file mode 100644
index bc59c12aa16..00000000000
--- a/tests/import/module_getattr.py.exp
+++ /dev/null
@@ -1 +0,0 @@
-False
diff --git a/tests/import/pkg7/subpkg1/subpkg2/mod3.py b/tests/import/pkg7/subpkg1/subpkg2/mod3.py
index b0f4279fcf5..f7f4c1e65f4 100644
--- a/tests/import/pkg7/subpkg1/subpkg2/mod3.py
+++ b/tests/import/pkg7/subpkg1/subpkg2/mod3.py
@@ -5,7 +5,9 @@
 print(bar)
 
 # attempted relative import beyond top-level package
+# On older versions of CPython (eg 3.8) this is a ValueError, but on
+# newer CPython (eg 3.11) and MicroPython it's an ImportError.
 try:
     from .... import mod1
-except ImportError:
+except (ImportError, ValueError):
     print("ImportError")
diff --git a/tests/import/pkgstar_all_array/__init__.py b/tests/import/pkgstar_all_array/__init__.py
new file mode 100644
index 00000000000..03b012123fe
--- /dev/null
+++ b/tests/import/pkgstar_all_array/__init__.py
@@ -0,0 +1,49 @@
+__all__ = ["publicFun", "PublicClass", "dynamicFun"]
+
+
+# Definitions below should always be imported by a star import
+def publicFun():
+    return 1
+
+
+class PublicClass:
+    def __init__(self):
+        self._val = 1
+
+
+# If __all__ support is enabled, definitions below
+# should not be imported by a star import
+def unlistedFun():
+    return 0
+
+
+class UnlistedClass:
+    def __init__(self):
+        self._val = 0
+
+
+# Definitions below should be not be imported by a star import
+# (they start with an underscore, and are not listed in __all__)
+def _privateFun():
+    return -1
+
+
+class _PrivateClass:
+    def __init__(self):
+        self._val = -1
+
+
+# Test lazy loaded function, as used by extmod/asyncio:
+# Works with a star import only if __all__ support is enabled
+_attrs = {
+    "dynamicFun": "funcs",
+}
+
+
+def __getattr__(attr):
+    mod = _attrs.get(attr, None)
+    if mod is None:
+        raise AttributeError(attr)
+    value = getattr(__import__(mod, globals(), locals(), True, 1), attr)
+    globals()[attr] = value
+    return value
diff --git a/tests/import/pkgstar_all_array/funcs.py b/tests/import/pkgstar_all_array/funcs.py
new file mode 100644
index 00000000000..7540d70f66b
--- /dev/null
+++ b/tests/import/pkgstar_all_array/funcs.py
@@ -0,0 +1,2 @@
+def dynamicFun():
+    return 777
diff --git a/tests/import/pkgstar_all_inval/__init__.py b/tests/import/pkgstar_all_inval/__init__.py
new file mode 100644
index 00000000000..7022476c19b
--- /dev/null
+++ b/tests/import/pkgstar_all_inval/__init__.py
@@ -0,0 +1 @@
+__all__ = 42
diff --git a/tests/import/pkgstar_all_miss/__init__.py b/tests/import/pkgstar_all_miss/__init__.py
new file mode 100644
index 00000000000..f9bbb538072
--- /dev/null
+++ b/tests/import/pkgstar_all_miss/__init__.py
@@ -0,0 +1,8 @@
+__all__ = ("existingFun", "missingFun")
+
+
+def existingFun():
+    return None
+
+
+# missingFun is not defined, should raise an error on import
diff --git a/tests/import/pkgstar_all_tuple/__init__.py b/tests/import/pkgstar_all_tuple/__init__.py
new file mode 100644
index 00000000000..433ddc8e976
--- /dev/null
+++ b/tests/import/pkgstar_all_tuple/__init__.py
@@ -0,0 +1,22 @@
+__all__ = ("publicFun2", "PublicClass2")
+
+
+# Definitions below should always be imported by a star import
+def publicFun2():
+    return 2
+
+
+class PublicClass2:
+    def __init__(self):
+        self._val = 2
+
+
+# If __all__ support is enabled, definitions below
+# should not be imported by a star import
+def unlistedFun2():
+    return 0
+
+
+class UnlistedClass2:
+    def __init__(self):
+        self._val = 0
diff --git a/tests/import/pkgstar_default/__init__.py b/tests/import/pkgstar_default/__init__.py
new file mode 100644
index 00000000000..4947e4ce7f1
--- /dev/null
+++ b/tests/import/pkgstar_default/__init__.py
@@ -0,0 +1,20 @@
+# When __all__ is undefined, star import should only
+# show objects that do not start with an underscore
+
+
+def visibleFun():
+    return 42
+
+
+class VisibleClass:
+    def __init__(self):
+        self._val = 42
+
+
+def _hiddenFun():
+    return -1
+
+
+class _HiddenClass:
+    def __init__(self):
+        self._val = -1
diff --git a/tests/import/pkgstar_getattr_attr_er/__init__.py b/tests/import/pkgstar_getattr_attr_er/__init__.py
new file mode 100644
index 00000000000..123463d3cd6
--- /dev/null
+++ b/tests/import/pkgstar_getattr_attr_er/__init__.py
@@ -0,0 +1,15 @@
+_print_msg = True
+
+
+def publicFun3():
+    return 3
+
+
+def __getattr__(attr):
+    global _print_msg
+    if _print_msg:
+        # CPython calls __getattr__("__all__") twice, but MicroPython only once.
+        # To make the output match, only print the message once.
+        print("__getattr__", attr)
+        _print_msg = False
+    raise AttributeError(attr)
diff --git a/tests/import/pkgstar_getattr_value_er/__init__.py b/tests/import/pkgstar_getattr_value_er/__init__.py
new file mode 100644
index 00000000000..43d6cdf250f
--- /dev/null
+++ b/tests/import/pkgstar_getattr_value_er/__init__.py
@@ -0,0 +1,3 @@
+def __getattr__(attr):
+    print("__getattr__", attr)
+    raise ValueError(attr)
diff --git a/tests/inlineasm/asmbcc.py b/tests/inlineasm/asmbcc.py
deleted file mode 100644
index 08967d48c74..00000000000
--- a/tests/inlineasm/asmbcc.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# test bcc instructions
-# at the moment only tests beq, narrow and wide versions
-
-
-@micropython.asm_thumb
-def f(r0):
-    mov(r1, r0)
-
-    mov(r0, 10)
-    cmp(r1, 1)
-    beq(end)
-
-    mov(r0, 20)
-    cmp(r1, 2)
-    beq_n(end)
-
-    mov(r0, 30)
-    cmp(r1, 3)
-    beq_w(end)
-
-    mov(r0, 0)
-
-    label(end)
-
-
-print(f(0))
-print(f(1))
-print(f(2))
-print(f(3))
diff --git a/tests/inlineasm/asmbcc.py.exp b/tests/inlineasm/asmbcc.py.exp
deleted file mode 100644
index 39da7d1a99e..00000000000
--- a/tests/inlineasm/asmbcc.py.exp
+++ /dev/null
@@ -1,4 +0,0 @@
-0
-10
-20
-30
diff --git a/tests/inlineasm/asmfpldrstr.py b/tests/inlineasm/asmfpldrstr.py
deleted file mode 100644
index 96cd0c23eb5..00000000000
--- a/tests/inlineasm/asmfpldrstr.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import uarray as array
-
-
-@micropython.asm_thumb  # test vldr, vstr
-def arrayadd(r0):
-    vldr(s0, [r0, 0])
-    vldr(s1, [r0, 4])
-    vadd(s2, s0, s1)
-    vstr(s2, [r0, 8])
-
-
-z = array.array("f", [2, 4, 10])
-arrayadd(z)
-print(z[2])
diff --git a/tests/inlineasm/asmsum.py b/tests/inlineasm/asmsum.py
deleted file mode 100644
index 208709a25f7..00000000000
--- a/tests/inlineasm/asmsum.py
+++ /dev/null
@@ -1,61 +0,0 @@
-@micropython.asm_thumb
-def asm_sum_words(r0, r1):
-    # r0 = len
-    # r1 = ptr
-    # r2 = sum
-    # r3 = dummy
-    mov(r2, 0)
-
-    b(loop_entry)
-
-    label(loop1)
-    ldr(r3, [r1, 0])
-    add(r2, r2, r3)
-
-    add(r1, r1, 4)
-    sub(r0, r0, 1)
-
-    label(loop_entry)
-    cmp(r0, 0)
-    bgt(loop1)
-
-    mov(r0, r2)
-
-
-@micropython.asm_thumb
-def asm_sum_bytes(r0, r1):
-    # r0 = len
-    # r1 = ptr
-    # r2 = sum
-    # r3 = dummy
-    mov(r2, 0)
-
-    b(loop_entry)
-
-    label(loop1)
-    ldrb(r3, [r1, 0])
-    add(r2, r2, r3)
-
-    add(r1, r1, 1)
-    sub(r0, r0, 1)
-
-    label(loop_entry)
-    cmp(r0, 0)
-    bgt(loop1)
-
-    mov(r0, r2)
-
-
-import uarray as array
-
-b = array.array("l", (100, 200, 300, 400))
-n = asm_sum_words(len(b), b)
-print(b, n)
-
-b = array.array("b", (10, 20, 30, 40, 50, 60, 70, 80))
-n = asm_sum_bytes(len(b), b)
-print(b, n)
-
-b = b"\x01\x02\x03\x04"
-n = asm_sum_bytes(len(b), b)
-print(b, n)
diff --git a/tests/inlineasm/rv32/asm_ext_zba.py b/tests/inlineasm/rv32/asm_ext_zba.py
new file mode 100644
index 00000000000..75f3573c864
--- /dev/null
+++ b/tests/inlineasm/rv32/asm_ext_zba.py
@@ -0,0 +1,18 @@
+@micropython.asm_rv32
+def test_sh1add(a0, a1):
+    sh1add(a0, a0, a1)
+
+
+@micropython.asm_rv32
+def test_sh2add(a0, a1):
+    sh2add(a0, a0, a1)
+
+
+@micropython.asm_rv32
+def test_sh3add(a0, a1):
+    sh3add(a0, a0, a1)
+
+
+print(hex(test_sh1add(10, 20)))
+print(hex(test_sh2add(10, 20)))
+print(hex(test_sh3add(10, 20)))
diff --git a/tests/inlineasm/rv32/asm_ext_zba.py.exp b/tests/inlineasm/rv32/asm_ext_zba.py.exp
new file mode 100644
index 00000000000..5f56bd95642
--- /dev/null
+++ b/tests/inlineasm/rv32/asm_ext_zba.py.exp
@@ -0,0 +1,3 @@
+0x28
+0x3c
+0x64
diff --git a/tests/inlineasm/rv32/asm_ext_zcmp.py b/tests/inlineasm/rv32/asm_ext_zcmp.py
new file mode 100644
index 00000000000..619478def7f
--- /dev/null
+++ b/tests/inlineasm/rv32/asm_ext_zcmp.py
@@ -0,0 +1,264 @@
+CMMV_TEMPLATE = """
+@micropython.asm_rv32
+def t():
+    cm_mv{}({}, {})
+"""
+
+CMMV_TESTS = (
+    ("s0", "s9", False),
+    ("s0", "s0", False),
+    ("s1", "s1", False),
+    ("s9", "s10", False),
+    ("s0", "s1", True),
+    ("s1", "s2", True),
+    ("s2", "s3", True),
+    ("s3", "s4", True),
+)
+
+
+def cmmv_test(op, tests):
+    passed = True
+    for lhs, rhs, success in tests:
+        try:
+            exec(CMMV_TEMPLATE.format(op, lhs, rhs))
+            if success is False:
+                print("cm.mv{} {} {} syntax fail".format(op, lhs, rhs))
+                passed = False
+        except SyntaxError:
+            if success is True:
+                print("cm.mv{} {} {} syntax fail".format(op, lhs, rhs))
+                passed = False
+    if passed:
+        print("cm.mv{} syntax pass".format(op))
+
+
+cmmv_test("a01s", CMMV_TESTS)
+cmmv_test("sa01", CMMV_TESTS)
+
+
+CMPP_TEMPLATE = """
+@micropython.asm_rv32
+def t():
+    cm_push({}, {})
+    cm_pop{}({}, {})
+"""
+
+
+CMPP_TESTS = (
+    ("", 0, 0, False),
+    ("ra", 0, 0, False),
+    ("ra, s0", 0, 0, False),
+    ("ra, s0-s1", 0, 0, False),
+    ("ra, s0-s10", 0, 0, False),
+    ("{}", 0, 0, False),
+    ("{t0}", 0, 0, False),
+    ("{s0}", 0, 0, False),
+    ("{s0-s1}", 0, 0, False),
+    ("{ra, s1-s0}", 0, 0, False),
+    ("{ra, s0-s10}", 0, 0, False),
+    ("{ra}", 16, -16, False),
+    ("{ra, s0}", -20, 20, False),
+    ("{ra}", 0, 0, True),
+    ("{ra, s0}", 0, 0, True),
+    ("{ra, s0-s1}", 0, 0, True),
+    ("{ra, s0-s5}", 0, 0, True),
+    ("{ra, s0-s11}", 0, 0, True),
+    ("{ra}", -16, 16, True),
+    ("{ra, s0}", -16, 16, True),
+    ("{ra, s0-s1}", -16, 16, True),
+    ("{ra}", -32, 32, True),
+    ("{ra, s0}", -32, 32, True),
+    ("{ra, s0-s1}", -32, 32, True),
+    ("{ra}", -48, 48, True),
+    ("{ra, s0}", -48, 48, True),
+    ("{ra, s0-s1}", -48, 48, True),
+)
+
+
+def cmpp_test(op, tests):
+    passed = True
+    for lhs, rhs_in, rhs_out, success in tests:
+        try:
+            exec(CMPP_TEMPLATE.format(lhs, rhs_in, op, lhs, rhs_out))
+            if success is False:
+                print("cm.push/cm.pop{} {} {} {} syntax fail".format(op, lhs, rhs_in, rhs_out))
+                passed = False
+        except SyntaxError:
+            if success is True:
+                print("cm.push/cm.pop{} {} {} {} syntax fail".format(op, lhs, rhs_in, rhs_out))
+                passed = False
+    if passed:
+        print("cm.push/cm.pop{} syntax pass".format(op))
+
+
+cmpp_test("", CMPP_TESTS)
+cmpp_test("ret", CMPP_TESTS)
+cmpp_test("retz", CMPP_TESTS)
+
+
+CM_POP_TEMPLATE = """
+@micropython.asm_rv32
+def t(a0):
+    mv(t0, s0)
+    mv(a1, a0)
+    mv(s0, a0)
+    cm_push({{ra, s0}}, {})
+    add(s0, a1, a1)
+    cm_pop({{ra, s0}}, {})
+    bne(s0, a0, fail)
+    li(a0, 1)
+    c_j(end)
+    label(fail)
+    li(a0, 0)
+    label(end)
+    mv(s0, t0)
+print("cm.push/cm.pop {}", t(0x1234) == 1)
+"""
+
+for stack_adj in 0, 16, 32, 48:
+    exec(CM_POP_TEMPLATE.format(-stack_adj, stack_adj, stack_adj))
+
+
+CM_POPRET_TEMPLATE = """
+@micropython.asm_rv32
+def t(a0):
+    mv(t0, ra)
+    mv(a1, s0)
+    jal(ra, clobber)
+    mv(ra, t0)
+    bne(s0, a1, fail)
+    li(a0, 1)
+    c_j(end)
+    label(fail)
+    li(a0, 0)
+    label(end)
+    c_jr(ra)
+    label(clobber)
+    cm_push({{ra, s0}}, {})
+    mv(s0, a0)
+    cm_popret({{ra, s0}}, {})
+print("cm.push/cm.popret {}", t(0x1234) == 1)
+"""
+
+for stack_adj in 0, 16, 32, 48:
+    exec(CM_POPRET_TEMPLATE.format(-stack_adj, stack_adj, stack_adj))
+
+
+CM_POPRETZ_TEMPLATE = """
+@micropython.asm_rv32
+def t(a0):
+    mv(t0, ra)
+    mv(a1, s0)
+    jal(ra, clobber)
+    mv(ra, t0)
+    bne(a0, zero, fail)
+    bne(s0, a1, fail)
+    li(a0, 1)
+    c_j(end)
+    label(fail)
+    li(a0, 0)
+    label(end)
+    c_jr(ra)
+    label(clobber)
+    cm_push({{ra, s0}}, {})
+    mv(s0, a0)
+    cm_popretz({{ra, s0}}, {})
+print("cm.push/cm.popretz {}", t(0x1234) == 1)
+"""
+
+
+for stack_adj in 0, 16, 32, 48:
+    exec(CM_POPRETZ_TEMPLATE.format(-stack_adj, stack_adj, stack_adj))
+
+
+REGLIST_TEMPLATE = """
+@micropython.asm_rv32
+def t():
+    li(t6, 0)
+{save}
+    cm_push({reglist}, 0)
+{trash}
+    cm_pop({reglist}, 0)
+{compare}
+    c_j(restore)
+    label(fail)
+    li(t6, 1)
+    label(restore)
+{restore}
+    mv(a0, t6)
+print("reglist {reglist}", t() == 0)
+"""
+
+REG_MAP = [
+    ("ra", "a0", "{ra}"),
+    ("s0", "a1", "{ra,s0}"),
+    ("s1", "a2", "{ra,s0-s1}"),
+    ("s2", "a3", "{ra,s0-s2}"),
+    ("s3", "a4", "{ra,s0-s3}"),
+    ("s4", "a5", "{ra,s0-s4}"),
+    ("s5", "a6", "{ra,s0-s5}"),
+    ("s6", "a7", "{ra,s0-s6}"),
+    ("s7", "t0", "{ra,s0-s7}"),
+    ("s8", "t1", "{ra,s0-s8}"),
+    ("s9", "t2", "{ra,s0-s9}"),
+    ("s10", "t3", "{ra,s0-s11}"),
+    ("s11", "t4", "{ra,s0-s11}"),
+]
+
+for i in range(len(REG_MAP)):
+    save = ""
+    trash = ""
+    compare = ""
+    restore = ""
+    _, _, reglist = REG_MAP[i]
+    for j in range(i + 1):
+        r1, r2, _ = REG_MAP[j]
+        save += "    mv({}, {})\n".format(r2, r1)
+        trash += "    li({}, {})\n".format(r1, (0x1111_1111 * (j + 1)) & 0xFFFF_FFFF)
+        compare += "    bne({}, {}, fail)\n".format(r2, r1)
+        restore += "    mv({}, {})\n".format(r1, r2)
+    exec(
+        REGLIST_TEMPLATE.format(
+            save=save, trash=trash, compare=compare, restore=restore, reglist=reglist
+        )
+    )
+
+
+@micropython.asm_rv32
+def test_cm_mva01s(a0, a1):
+    cm_push({ra, s0 - s11}, 0)
+    mv(s4, a0)
+    mv(s5, a1)
+    li(a0, 0)
+    li(a1, 0)
+    cm_mva01s(s4, s5)
+    bne(a0, s4, fail)
+    bne(a1, s5, fail)
+    li(a0, 1)
+    c_j(end)
+    label(fail)
+    li(a0, 0)
+    label(end)
+    cm_pop({ra, s0 - s11}, 0)
+
+
+print("cm.mva01s", test_cm_mva01s(100, 200) == 1)
+
+
+@micropython.asm_rv32
+def test_cm_mvsa01(a0, a1):
+    cm_push({ra, s0 - s11}, 0)
+    li(s6, 0x12345678)
+    li(s7, 0x87654321)
+    cm_mvsa01(s6, s7)
+    bne(a0, s6, fail)
+    bne(a1, s7, fail)
+    li(a0, 1)
+    c_j(end)
+    label(fail)
+    li(a0, 0)
+    label(end)
+    cm_pop({ra, s0 - s11}, 0)
+
+
+print("cm.mvsa01", test_cm_mvsa01(100, 200) == 1)
diff --git a/tests/inlineasm/rv32/asm_ext_zcmp.py.exp b/tests/inlineasm/rv32/asm_ext_zcmp.py.exp
new file mode 100644
index 00000000000..62850dfff4a
--- /dev/null
+++ b/tests/inlineasm/rv32/asm_ext_zcmp.py.exp
@@ -0,0 +1,32 @@
+cm.mva01s syntax pass
+cm.mvsa01 syntax pass
+cm.push/cm.pop syntax pass
+cm.push/cm.popret syntax pass
+cm.push/cm.popretz syntax pass
+cm.push/cm.pop 0 True
+cm.push/cm.pop 16 True
+cm.push/cm.pop 32 True
+cm.push/cm.pop 48 True
+cm.push/cm.popret 0 True
+cm.push/cm.popret 16 True
+cm.push/cm.popret 32 True
+cm.push/cm.popret 48 True
+cm.push/cm.popretz 0 True
+cm.push/cm.popretz 16 True
+cm.push/cm.popretz 32 True
+cm.push/cm.popretz 48 True
+reglist {ra} True
+reglist {ra,s0} True
+reglist {ra,s0-s1} True
+reglist {ra,s0-s2} True
+reglist {ra,s0-s3} True
+reglist {ra,s0-s4} True
+reglist {ra,s0-s5} True
+reglist {ra,s0-s6} True
+reglist {ra,s0-s7} True
+reglist {ra,s0-s8} True
+reglist {ra,s0-s9} True
+reglist {ra,s0-s11} True
+reglist {ra,s0-s11} True
+cm.mva01s True
+cm.mvsa01 True
diff --git a/tests/inlineasm/rv32/asmargs.py b/tests/inlineasm/rv32/asmargs.py
new file mode 100644
index 00000000000..78afd511150
--- /dev/null
+++ b/tests/inlineasm/rv32/asmargs.py
@@ -0,0 +1,44 @@
+# test passing arguments
+
+
+@micropython.asm_rv32
+def arg0():
+    c_li(a0, 1)
+
+
+print(arg0())
+
+
+@micropython.asm_rv32
+def arg1(a0):
+    addi(a0, a0, 1)
+
+
+print(arg1(1))
+
+
+@micropython.asm_rv32
+def arg2(a0, a1):
+    add(a0, a0, a1)
+
+
+print(arg2(1, 2))
+
+
+@micropython.asm_rv32
+def arg3(a0, a1, a2):
+    add(a0, a0, a1)
+    add(a0, a0, a2)
+
+
+print(arg3(1, 2, 3))
+
+
+@micropython.asm_rv32
+def arg4(a0, a1, a2, a3):
+    add(a0, a0, a1)
+    add(a0, a0, a2)
+    add(a0, a0, a3)
+
+
+print(arg4(1, 2, 3, 4))
diff --git a/tests/inlineasm/asmargs.py.exp b/tests/inlineasm/rv32/asmargs.py.exp
similarity index 100%
rename from tests/inlineasm/asmargs.py.exp
rename to tests/inlineasm/rv32/asmargs.py.exp
diff --git a/tests/inlineasm/rv32/asmarith.py b/tests/inlineasm/rv32/asmarith.py
new file mode 100644
index 00000000000..8b864c0b3b2
--- /dev/null
+++ b/tests/inlineasm/rv32/asmarith.py
@@ -0,0 +1,79 @@
+# test arithmetic opcodes
+
+
+@micropython.asm_rv32
+def f1():
+    li(a0, 0x100)
+    li(a1, 1)
+    add(a0, a0, a1)
+    addi(a0, a0, 1)
+    addi(a0, a0, -2)
+    sub(a0, a0, a1)
+    c_add(a0, a1)
+    c_addi(a0, -1)
+    c_sub(a0, a1)
+
+
+print(hex(f1()))
+
+
+@micropython.asm_rv32
+def f2():
+    li(a0, 0x10FF)
+    li(a1, 1)
+    and_(a2, a0, a1)
+    andi(a3, a0, 0x10)
+    or_(a2, a2, a3)
+    ori(a2, a2, 8)
+    li(a1, 0x200)
+    c_or(a2, a1)
+    li(a1, 0xF0)
+    mv(a0, a2)
+    c_and(a0, a1)
+    li(a1, 0x101)
+    xor(a0, a0, a1)
+    xori(a0, a0, 0x101)
+    c_xor(a0, a1)
+
+
+print(hex(f2()))
+
+
+@micropython.asm_rv32
+def f3(a0, a1):
+    slt(a0, a0, a1)
+
+
+print(f3(0xFFFFFFF0, 0xFFFFFFF1))
+print(f3(0x0, 0xFFFFFFF1))
+print(f3(0xFFFFFFF1, 0xFFFFFFF1))
+print(f3(0xFFFFFFF1, 0xFFFFFFF0))
+
+
+@micropython.asm_rv32
+def f4(a0, a1):
+    sltu(a0, a0, a1)
+
+
+print(f3(0xFFFFFFF0, 0xFFFFFFF1))
+print(f3(0x0, 0xFFFFFFF1))
+print(f3(0xFFFFFFF1, 0xFFFFFFF1))
+print(f3(0xFFFFFFF1, 0xFFFFFFF0))
+
+
+@micropython.asm_rv32
+def f5(a0):
+    slti(a0, a0, -2)
+
+
+print(f5(-1))
+print(f5(-3))
+
+
+@micropython.asm_rv32
+def f6(a0):
+    sltiu(a0, a0, -2)
+
+
+print(f6(-1))
+print(f6(-3))
diff --git a/tests/inlineasm/rv32/asmarith.py.exp b/tests/inlineasm/rv32/asmarith.py.exp
new file mode 100644
index 00000000000..7da4dd5c93c
--- /dev/null
+++ b/tests/inlineasm/rv32/asmarith.py.exp
@@ -0,0 +1,14 @@
+0xfe
+0x111
+1
+0
+0
+0
+1
+0
+0
+0
+0
+1
+0
+1
diff --git a/tests/inlineasm/rv32/asmbranch.py b/tests/inlineasm/rv32/asmbranch.py
new file mode 100644
index 00000000000..d7d059d4067
--- /dev/null
+++ b/tests/inlineasm/rv32/asmbranch.py
@@ -0,0 +1,161 @@
+# test branch instructions
+
+
+@micropython.asm_rv32
+def tbeq(a0):
+    mv(a1, a0)
+
+    li(a0, 10)
+    li(a2, 1)
+    beq(a1, a2, end)
+
+    li(a0, 20)
+    li(a2, 2)
+    beq(a1, a2, end)
+
+    li(a0, 30)
+    li(a2, 3)
+    beq(a1, a2, end)
+
+    li(a0, 0)
+
+    label(end)
+
+
+print(tbeq(0))
+print(tbeq(1))
+print(tbeq(2))
+print(tbeq(3))
+
+
+@micropython.asm_rv32
+def tbne(a0):
+    mv(a1, a0)
+
+    li(a0, 10)
+    li(a2, 1)
+    bne(a1, a2, end)
+
+    li(a0, 20)
+    li(a2, 2)
+    bne(a1, a2, end)
+
+    li(a0, 30)
+    li(a2, 3)
+    bne(a1, a2, end)
+
+    li(a0, 0)
+
+    label(end)
+
+
+print(tbne(0))
+print(tbne(1))
+print(tbne(2))
+print(tbne(3))
+
+
+@micropython.asm_rv32
+def tbgeu(a0):
+    mv(a1, a0)
+
+    li(a0, 1)
+    li(a2, 2)
+    bgeu(a1, a2, end)
+    li(a0, 0)
+
+    label(end)
+
+
+print(tbgeu(0))
+print(tbgeu(1))
+print(tbgeu(2))
+print(tbgeu(3))
+
+
+@micropython.asm_rv32
+def tbltu(a0):
+    mv(a1, a0)
+
+    li(a0, 1)
+    li(a2, 2)
+    bltu(a1, a2, end)
+    li(a0, 0)
+
+    label(end)
+
+
+print(tbltu(0))
+print(tbltu(1))
+print(tbltu(2))
+print(tbltu(3))
+
+
+@micropython.asm_rv32
+def tbge(a0):
+    mv(a1, a0)
+
+    li(a0, 1)
+    li(a2, -2)
+    bge(a1, a2, end)
+    li(a0, 0)
+
+    label(end)
+
+
+print(tbge(-3))
+print(tbge(-2))
+print(tbge(-1))
+print(tbge(0))
+
+
+@micropython.asm_rv32
+def tblt(a0):
+    mv(a1, a0)
+
+    li(a0, 1)
+    li(a2, -2)
+    blt(a1, a2, end)
+    li(a0, 0)
+
+    label(end)
+
+
+print(tblt(-3))
+print(tblt(-2))
+print(tblt(-1))
+print(tblt(0))
+
+
+@micropython.asm_rv32
+def tcbeqz(a0):
+    mv(a1, a0)
+
+    li(a0, 1)
+    c_beqz(a1, end)
+    li(a0, 0)
+
+    label(end)
+
+
+print(tcbeqz(0))
+print(tcbeqz(1))
+print(tcbeqz(2))
+print(tcbeqz(3))
+
+
+@micropython.asm_rv32
+def tcbnez(a0):
+    mv(a1, a0)
+
+    li(a0, 1)
+    c_bnez(a1, end)
+    li(a0, 0)
+
+    label(end)
+
+
+print(tcbnez(0))
+print(tcbnez(1))
+print(tcbnez(2))
+print(tcbnez(3))
diff --git a/tests/inlineasm/rv32/asmbranch.py.exp b/tests/inlineasm/rv32/asmbranch.py.exp
new file mode 100644
index 00000000000..baae6914953
--- /dev/null
+++ b/tests/inlineasm/rv32/asmbranch.py.exp
@@ -0,0 +1,32 @@
+0
+10
+20
+30
+10
+20
+10
+10
+0
+0
+1
+1
+1
+1
+0
+0
+0
+1
+1
+1
+1
+0
+0
+0
+1
+0
+0
+0
+0
+1
+1
+1
diff --git a/tests/inlineasm/rv32/asmconst.py b/tests/inlineasm/rv32/asmconst.py
new file mode 100644
index 00000000000..2b6363a43db
--- /dev/null
+++ b/tests/inlineasm/rv32/asmconst.py
@@ -0,0 +1,49 @@
+# test constants in assembler
+
+
+@micropython.asm_rv32
+def c1():
+    li(a0, 0xFFFFFFFF)
+    li(a1, 0xF0000000)
+    sub(a0, a0, a1)
+
+
+print(hex(c1()))
+
+
+@micropython.asm_rv32
+def c2():
+    lui(a0, 0x12345)
+    li(a1, 0x678)
+    add(a0, a0, a1)
+
+
+print(hex(c2()))
+
+
+@micropython.asm_rv32
+def c3() -> uint:
+    lui(a0, 0)
+    addi(a0, a0, 0x7FF)
+
+
+print(hex(c3()))
+
+
+@micropython.asm_rv32
+def c4() -> uint:
+    lui(a0, 0)
+    addi(a0, a0, -1)
+
+
+print(hex(c4()))
+
+
+@micropython.asm_rv32
+def c5():
+    c_lui(a0, 1)
+    c_li(a1, 1)
+    c_add(a0, a1)
+
+
+print(hex(c5()))
diff --git a/tests/inlineasm/rv32/asmconst.py.exp b/tests/inlineasm/rv32/asmconst.py.exp
new file mode 100644
index 00000000000..0c713a84148
--- /dev/null
+++ b/tests/inlineasm/rv32/asmconst.py.exp
@@ -0,0 +1,5 @@
+0xfffffff
+0x12345678
+0x7ff
+0xffffffff
+0x1001
diff --git a/tests/inlineasm/rv32/asmcsr.py b/tests/inlineasm/rv32/asmcsr.py
new file mode 100644
index 00000000000..f27e2aa5e34
--- /dev/null
+++ b/tests/inlineasm/rv32/asmcsr.py
@@ -0,0 +1,65 @@
+# test csr instructions
+
+# CSR 0x340 is `mscratch`.  This test suite is only safe to run on a system
+# where it is known that there is no other code that can read from or write
+# to that register.  The qemu port is one such system, as the CSR is only
+# accessed when a machine exception occurs, and at that point it doesn't matter
+# anymore whether these tests are running or not.
+
+
+@micropython.asm_rv32
+def csr():
+    li(a0, 0)
+    csrrw(zero, zero, 0x340)  # All zeroes
+    csrrs(a1, zero, 0x340)  # Read zeroes
+    c_bnez(a1, end)
+    addi(a0, a0, 1)
+    li(a1, 0xA5A5A5A5)
+    li(a2, 0x5A5A5A5A)
+    csrrs(a2, a1, 0x340)  # Read zeroes, set 0xA5A5A5A5
+    c_bnez(a2, end)
+    addi(a0, a0, 1)
+    csrrs(a3, zero, 0x340)  # Read 0xA5A5A5A5
+    bne(a3, a1, end)
+    addi(a0, a0, 1)
+    li(a2, 0xF0F0F0F0)
+    csrrc(zero, a2, 0x340)  # Clear upper half
+    csrrs(a3, zero, 0x340)  # Read 0x05050505
+    xori(a2, a2, -1)
+    and_(a2, a1, a2)
+    bne(a2, a3, end)
+    addi(a0, a0, 1)
+    label(end)
+
+
+print(csr())
+
+
+@micropython.asm_rv32
+def csri():
+    li(a0, 0)
+    csrrwi(zero, 0x340, 15)  # Write 0xF
+    csrrs(a1, zero, 0x340)  # Read 0xF
+    csrrsi(a2, 0x340, 0)  # Read
+    bne(a1, a2, end)
+    addi(a0, a0, 1)
+    csrrci(a2, 0x340, 0)  # Read
+    bne(a1, a2, end)
+    addi(a0, a0, 1)
+    li(a2, 15)
+    bne(a1, a2, end)
+    addi(a0, a0, 1)
+    csrrci(zero, 0x340, 1)  # Clear bit 1
+    csrrs(a1, zero, 0x340)  # Read 0xE
+    li(a2, 14)
+    bne(a1, a2, end)
+    addi(a0, a0, 1)
+    csrrsi(zero, 0x340, 1)  # Set bit 1
+    csrrs(a1, zero, 0x340)  # Read 0xF
+    li(a2, 15)
+    bne(a1, a2, end)
+    addi(a0, a0, 1)
+    label(end)
+
+
+print(csri())
diff --git a/tests/inlineasm/rv32/asmcsr.py.exp b/tests/inlineasm/rv32/asmcsr.py.exp
new file mode 100644
index 00000000000..61c83cba41c
--- /dev/null
+++ b/tests/inlineasm/rv32/asmcsr.py.exp
@@ -0,0 +1,2 @@
+4
+5
diff --git a/tests/inlineasm/rv32/asmdata.py b/tests/inlineasm/rv32/asmdata.py
new file mode 100644
index 00000000000..5e555ef4bf4
--- /dev/null
+++ b/tests/inlineasm/rv32/asmdata.py
@@ -0,0 +1,33 @@
+# test the "data" directive
+
+
+@micropython.asm_rv32
+def ret_num(a0) -> uint:
+    slli(a0, a0, 2)
+    addi(a0, a0, 16)
+    auipc(a1, 0)
+    add(a1, a1, a0)
+    lw(a0, 0(a1))
+    jal(zero, HERE)
+    data(4, 0x12345678, 0x20000000, 0x40000000, 0x7FFFFFFF + 1, (1 << 32) - 2)
+    label(HERE)
+
+
+for i in range(5):
+    print(hex(ret_num(i)))
+
+
+@micropython.asm_rv32
+def ret_num_la(a0) -> uint:
+    slli(a0, a0, 2)
+    la(a1, DATA)
+    add(a1, a1, a0)
+    lw(a0, 0(a1))
+    jal(zero, HERE)
+    label(DATA)
+    data(4, 0x12345678, 0x20000000, 0x40000000, 0x7FFFFFFF + 1, (1 << 32) - 2)
+    label(HERE)
+
+
+for i in range(5):
+    print(hex(ret_num_la(i)))
diff --git a/tests/inlineasm/rv32/asmdata.py.exp b/tests/inlineasm/rv32/asmdata.py.exp
new file mode 100644
index 00000000000..79e92bdfa5d
--- /dev/null
+++ b/tests/inlineasm/rv32/asmdata.py.exp
@@ -0,0 +1,10 @@
+0x12345678
+0x20000000
+0x40000000
+0x80000000
+0xfffffffe
+0x12345678
+0x20000000
+0x40000000
+0x80000000
+0xfffffffe
diff --git a/tests/inlineasm/rv32/asmdivmul.py b/tests/inlineasm/rv32/asmdivmul.py
new file mode 100644
index 00000000000..e1120c6f63c
--- /dev/null
+++ b/tests/inlineasm/rv32/asmdivmul.py
@@ -0,0 +1,63 @@
+@micropython.asm_rv32
+def sdiv(a0, a1):
+    div(a0, a0, a1)
+
+
+@micropython.asm_rv32
+def udiv(a0, a1):
+    divu(a0, a0, a1)
+
+
+@micropython.asm_rv32
+def srem(a0, a1):
+    rem(a0, a0, a1)
+
+
+@micropython.asm_rv32
+def urem(a0, a1):
+    remu(a0, a0, a1)
+
+
+print(sdiv(1234, 3))
+print(sdiv(-1234, 3))
+print(sdiv(1234, -3))
+print(sdiv(-1234, -3))
+
+print(udiv(1234, 3))
+print(udiv(0xFFFFFFFF, 0x7FFFFFFF))
+print(udiv(0xFFFFFFFF, 0xFFFFFFFF))
+
+print(srem(1234, 3))
+print(srem(-1234, 3))
+print(srem(1234, -3))
+print(srem(-1234, -3))
+
+print(urem(1234, 3))
+print(urem(0xFFFFFFFF, 0x7FFFFFFF))
+print(urem(0xFFFFFFFF, 0xFFFFFFFF))
+
+
+@micropython.asm_rv32
+def m1(a0, a1):
+    mul(a0, a0, a1)
+
+
+@micropython.asm_rv32
+def m2(a0, a1):
+    mulh(a0, a0, a1)
+
+
+@micropython.asm_rv32
+def m3(a0, a1):
+    mulhu(a0, a0, a1)
+
+
+@micropython.asm_rv32
+def m4(a0, a1):
+    mulhsu(a0, a0, a1)
+
+
+print(m1(0xFFFFFFFF, 2))
+print(m2(0xFFFFFFFF, 0xFFFFFFF0))
+print(m3(0xFFFFFFFF, 0xFFFFFFF0))
+print(m4(0xFFFFFFFF, 0xFFFFFFF0))
diff --git a/tests/inlineasm/rv32/asmdivmul.py.exp b/tests/inlineasm/rv32/asmdivmul.py.exp
new file mode 100644
index 00000000000..60d28635f79
--- /dev/null
+++ b/tests/inlineasm/rv32/asmdivmul.py.exp
@@ -0,0 +1,18 @@
+411
+-411
+-411
+411
+411
+2
+1
+1
+-1
+1
+-1
+1
+1
+0
+-2
+0
+-17
+-1
diff --git a/tests/inlineasm/rv32/asmjump.py b/tests/inlineasm/rv32/asmjump.py
new file mode 100644
index 00000000000..fe87d3f968b
--- /dev/null
+++ b/tests/inlineasm/rv32/asmjump.py
@@ -0,0 +1,115 @@
+@micropython.asm_rv32
+def f1():
+    li(a0, 0)
+    la(a1, END)
+    c_jr(a1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    label(END)
+
+
+print(f1())
+
+
+@micropython.asm_rv32
+def f2():
+    addi(sp, sp, -4)
+    c_swsp(ra, 0)
+    li(ra, 0)
+    li(a0, 0)
+    c_jal(END)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    label(END)
+    bne(ra, zero, SUCCESS)
+    c_addi(a0, 2)
+    label(SUCCESS)
+    c_lwsp(ra, 0)
+    addi(sp, sp, 4)
+
+
+print(f2())
+
+
+@micropython.asm_rv32
+def f3():
+    li(a0, 0)
+    c_j(END)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    label(END)
+
+
+print(f3())
+
+
+@micropython.asm_rv32
+def f4():
+    addi(sp, sp, -4)
+    c_swsp(ra, 0)
+    li(ra, 0)
+    li(a0, 0)
+    la(a1, END)
+    c_jalr(a1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    label(END)
+    bne(ra, zero, SUCCESS)
+    c_addi(a0, 2)
+    label(SUCCESS)
+    c_lwsp(ra, 0)
+    addi(sp, sp, 4)
+
+
+print(f4())
+
+
+@micropython.asm_rv32
+def f5():
+    li(a0, 0)
+    li(a1, 0)
+    jal(a1, END)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    label(END)
+    bne(a1, zero, SUCCESS)
+    c_addi(a0, 2)
+    label(SUCCESS)
+
+
+print(f5())
+
+
+@micropython.asm_rv32
+def f6():
+    li(a0, 0)
+    la(a1, JUMP)
+    li(a2, 0)
+    jalr(a2, a1, 10)
+    label(JUMP)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    c_addi(a0, 1)
+    bne(a2, zero, SUCCESS)
+    c_addi(a0, 2)
+    label(SUCCESS)
+
+
+print(f6())
diff --git a/tests/inlineasm/rv32/asmjump.py.exp b/tests/inlineasm/rv32/asmjump.py.exp
new file mode 100644
index 00000000000..f7eb44d66e0
--- /dev/null
+++ b/tests/inlineasm/rv32/asmjump.py.exp
@@ -0,0 +1,6 @@
+0
+0
+0
+0
+0
+0
diff --git a/tests/inlineasm/rv32/asmloadstore.py b/tests/inlineasm/rv32/asmloadstore.py
new file mode 100644
index 00000000000..2c49e07b41a
--- /dev/null
+++ b/tests/inlineasm/rv32/asmloadstore.py
@@ -0,0 +1,86 @@
+# test load/store opcodes
+
+
+@micropython.asm_rv32
+def l():
+    li(a5, 4)
+    addi(sp, sp, -12)
+    li(a0, 0x123)
+    c_swsp(a0, 0)
+    addi(a1, a0, 0x111)
+    c_swsp(a1, 4)
+    addi(a2, a1, 0x111)
+    c_swsp(a2, 8)
+    mv(a4, sp)
+    c_lw(a3, 0(a4))
+    bne(a3, a0, END)
+    addi(a5, a5, -1)
+    lw(a3, 4(a4))
+    bne(a3, a1, END)
+    addi(a5, a5, -1)
+    lhu(a3, 8(a4))
+    bne(a3, a2, END)
+    addi(a5, a5, -1)
+    lbu(a0, 8(a4))
+    addi(a0, a0, 0x300)
+    bne(a0, a2, END)
+    addi(a5, a5, -1)
+    label(END)
+    addi(sp, sp, 12)
+    mv(a0, a5)
+
+
+print(l())
+
+
+@micropython.asm_rv32
+def s():
+    li(a5, 4)
+    addi(sp, sp, -12)
+    c_swsp(zero, 0)
+    c_swsp(zero, 4)
+    c_swsp(zero, 8)
+    li(a0, 0x12345)
+    mv(a4, sp)
+    c_sw(a0, 0(a4))
+    sh(a0, 4(a4))
+    sb(a0, 8(a4))
+    li(a1, 0xFFFF)
+    and_(a1, a0, a1)
+    andi(a2, a0, 0xFF)
+    lw(a3, 0(sp))
+    bne(a3, a0, END)
+    addi(a5, a5, -1)
+    lw(a3, 4(sp))
+    bne(a3, a1, END)
+    addi(a5, a5, -1)
+    lw(a3, 8(sp))
+    bne(a3, a2, END)
+    addi(a5, a5, -1)
+    label(END)
+    addi(sp, sp, 12)
+    mv(a0, a5)
+
+
+print(s())
+
+
+@micropython.asm_rv32
+def lu():
+    li(a5, 4)
+    addi(sp, sp, -8)
+    li(a0, 0xF1234567)
+    c_swsp(a0, 0)
+    c_swsp(a0, 4)
+    lh(a1, 0(sp))
+    blt(a1, zero, END)
+    addi(a5, a5, -1)
+    lb(a2, 4(sp))
+    blt(a2, zero, END)
+    addi(a5, a5, -1)
+    label(END)
+    addi(sp, sp, 8)
+    mv(a0, a5)
+
+
+print(lu())
diff --git a/tests/inlineasm/rv32/asmloadstore.py.exp b/tests/inlineasm/rv32/asmloadstore.py.exp
new file mode 100644
index 00000000000..4539bbf2d22
--- /dev/null
+++ b/tests/inlineasm/rv32/asmloadstore.py.exp
@@ -0,0 +1,3 @@
+0
+1
+2
diff --git a/tests/inlineasm/rv32/asmrettype.py b/tests/inlineasm/rv32/asmrettype.py
new file mode 100644
index 00000000000..fc7ae61d152
--- /dev/null
+++ b/tests/inlineasm/rv32/asmrettype.py
@@ -0,0 +1,33 @@
+# test return type of inline asm
+
+
+@micropython.asm_rv32
+def ret_obj(a0) -> object:
+    pass
+
+
+ret_obj(print)(1)
+
+
+@micropython.asm_rv32
+def ret_bool(a0) -> bool:
+    pass
+
+
+print(ret_bool(0), ret_bool(1))
+
+
+@micropython.asm_rv32
+def ret_int(a0) -> int:
+    slli(a0, a0, 29)
+
+
+print(ret_int(0), hex(ret_int(1)), hex(ret_int(2)), hex(ret_int(4)))
+
+
+@micropython.asm_rv32
+def ret_uint(a0) -> uint:
+    slli(a0, a0, 29)
+
+
+print(ret_uint(0), hex(ret_uint(1)), hex(ret_uint(2)), hex(ret_uint(4)))
diff --git a/tests/inlineasm/asmrettype.py.exp b/tests/inlineasm/rv32/asmrettype.py.exp
similarity index 100%
rename from tests/inlineasm/asmrettype.py.exp
rename to tests/inlineasm/rv32/asmrettype.py.exp
diff --git a/tests/inlineasm/rv32/asmsanity.py b/tests/inlineasm/rv32/asmsanity.py
new file mode 100644
index 00000000000..1a16d3504db
--- /dev/null
+++ b/tests/inlineasm/rv32/asmsanity.py
@@ -0,0 +1,204 @@
+TEMPLATE3 = """
+@micropython.asm_rv32
+def f():
+    {}({}, {}, {})
+"""
+
+TEMPLATE2 = """
+@micropython.asm_rv32
+def f():
+    {}({}, {})
+"""
+
+TEMPLATE1 = """
+@micropython.asm_rv32
+def f():
+    {}({})
+"""
+
+
+REGISTERS = [
+    "zero",
+    "s0",
+    "s1",
+    "s2",
+    "s3",
+    "s4",
+    "s5",
+    "s6",
+    "s7",
+    "s8",
+    "s9",
+    "s10",
+    "s11",
+    "a0",
+    "a1",
+    "a2",
+    "a3",
+    "a4",
+    "a5",
+    "a6",
+    "a7",
+    "tp",
+    "gp",
+    "sp",
+    "ra",
+    "t0",
+    "t1",
+    "t2",
+    "t3",
+    "t4",
+    "t5",
+    "t6",
+    "x0",
+    "x1",
+    "x2",
+    "x3",
+    "x4",
+    "x5",
+    "x6",
+    "x7",
+    "x8",
+    "x9",
+    "x10",
+    "x11",
+    "x12",
+    "x13",
+    "x14",
+    "x15",
+    "x16",
+    "x17",
+    "x18",
+    "x19",
+    "x20",
+    "x21",
+    "x22",
+    "x23",
+    "x24",
+    "x25",
+    "x26",
+    "x27",
+    "x28",
+    "x29",
+    "x30",
+    "x31",
+]
+
+
+def harness(opcode, fragment, tag):
+    try:
+        exec(fragment)
+    except SyntaxError:
+        print(tag, opcode)
+
+
+for opcode in ("slli", "srli", "srai"):
+    harness(opcode, TEMPLATE3.format(opcode, "a0", "a0", -1), "-")
+    harness(opcode, TEMPLATE3.format(opcode, "a0", "a0", 33), "+")
+
+for opcode in ("c_slli", "c_srli", "c_srai"):
+    harness(opcode, TEMPLATE2.format(opcode, "a0", -1), "-")
+    harness(opcode, TEMPLATE2.format(opcode, "a0", 33), "+")
+
+harness("c_slli", TEMPLATE2.format("c_slli", "zero", 0), "0")
+harness("c_slli", TEMPLATE2.format("c_slli", "x0", 0), "0")
+
+for opcode in ("c_srli", "c_srai"):
+    for register in REGISTERS:
+        harness(opcode, TEMPLATE2.format(opcode, register, 0), register)
+
+for opcode in ("c_mv", "c_add"):
+    harness(opcode, TEMPLATE2.format(opcode, "a0", "zero"), "0l")
+    harness(opcode, TEMPLATE2.format(opcode, "zero", "a0"), "0r")
+    harness(opcode, TEMPLATE2.format(opcode, "zero", "zero"), "0b")
+
+harness("c_jr", TEMPLATE1.format("c_jr", "zero"), "0")
+
+for opcode in ("addi", "andi", "ori", "slti", "sltiu", "xori"):
+    harness(opcode, TEMPLATE3.format(opcode, "a0", "a0", 0x7FF), ">=s")
+    harness(opcode, TEMPLATE3.format(opcode, "a0", "a0", 0x800), ">s")
+    harness(opcode, TEMPLATE3.format(opcode, "a0", "a0", -2048), "<=s")
+    harness(opcode, TEMPLATE3.format(opcode, "a0", "a0", -2049), "=s")
+    harness(opcode, TEMPLATE.format(opcode, 0x800), ">s")
+    harness(opcode, TEMPLATE.format(opcode, -2048), "<=s")
+    harness(opcode, TEMPLATE.format(opcode, -2049), "0")
+harness("c_addi", TEMPLATE2.format("c_andi", "zero", -512), "<0")
+harness("c_addi", TEMPLATE2.format("c_andi", "s0", 0), "s0")
+harness("c_addi", TEMPLATE2.format("c_andi", "s0", -100), "s")
+
+harness("c_andi", TEMPLATE2.format("c_andi", "zero", 0), "00")
+harness("c_andi", TEMPLATE2.format("c_andi", "zero", 512), ">0")
+harness("c_andi", TEMPLATE2.format("c_andi", "zero", -512), "<0")
+harness("c_andi", TEMPLATE2.format("c_andi", "s0", 0), "s0")
+harness("c_andi", TEMPLATE2.format("c_andi", "s0", -100), "s")
+
+C_REGISTERS = (
+    "a0",
+    "a1",
+    "a2",
+    "a3",
+    "a4",
+    "a5",
+    "s0",
+    "s1",
+    "x8",
+    "x9",
+    "x10",
+    "x11",
+    "x12",
+    "x13",
+    "x14",
+    "x15",
+)
+
+for opcode in ("c_and", "c_or", "c_xor"):
+    for source in REGISTERS:
+        for destination in REGISTERS:
+            if source in C_REGISTERS and destination in C_REGISTERS:
+                try:
+                    exec(
+                        """
+@micropython.asm_rv32
+def f():
+    {}({}, {})
+""".format(opcode, source, destination)
+                    )
+                except SyntaxError:
+                    print(source, destination, opcode)
+            else:
+                try:
+                    exec(
+                        """
+@micropython.asm_rv32
+def f():
+    {}({}, {})
+""".format(opcode, source, destination)
+                    )
+                    print(source, destination, opcode)
+                except SyntaxError:
+                    pass
+    print(opcode)
+
+for opcode in ("c_lw", "c_sw"):
+    TEMPLATE = """
+@micropython.asm_rv32
+def f():
+    {}(a0, {}(a0))
+"""
+    harness(opcode, TEMPLATE.format(opcode, 60), ">=s")
+    harness(opcode, TEMPLATE.format(opcode, 61), ">s")
+    harness(opcode, TEMPLATE.format(opcode, -60), "<=s")
+    harness(opcode, TEMPLATE.format(opcode, -61), "s addi
+s andi
+s ori
+s slti
+s sltiu
+s xori
+s lb
+s lbu
+s lh
+s lhu
+s lw
+s sb
+s sh
+s sw
+0 c_addi
+<0 c_addi
+s c_addi
+00 c_andi
+>0 c_andi
+<0 c_andi
+s c_andi
+c_and
+c_or
+c_xor
+>s c_lw
+s c_sw
+ int:
+    addmi(a2, a2, {})
+print(f1(0))
+"""
+
+for value in (-32768, -32767, 32512, 32513, 0):
+    try:
+        exec(ADDMI_TEMPLATE.format(value))
+    except SyntaxError as error:
+        print(error)
+
+
+@micropython.asm_xtensa
+def a2(a2, a3) -> int:
+    addx2(a2, a2, a3)
+
+
+@micropython.asm_xtensa
+def a4(a2, a3) -> int:
+    addx4(a2, a2, a3)
+
+
+@micropython.asm_xtensa
+def a8(a2, a3) -> int:
+    addx8(a2, a2, a3)
+
+
+@micropython.asm_xtensa
+def s2(a2, a3) -> int:
+    subx2(a2, a2, a3)
+
+
+@micropython.asm_xtensa
+def s4(a2, a3) -> int:
+    subx4(a2, a2, a3)
+
+
+@micropython.asm_xtensa
+def s8(a2, a3) -> int:
+    subx8(a2, a2, a3)
+
+
+for first, second in ((100, 100), (-100, 100), (-100, -100), (100, -100)):
+    print("a2", a2(first, second))
+    print("a4", a4(first, second))
+    print("a8", a8(first, second))
+    print("s2", s2(first, second))
+    print("s4", s4(first, second))
+    print("s8", s8(first, second))
+
+
+@micropython.asm_xtensa
+def f5(a2) -> int:
+    neg(a2, a2)
+
+
+for value in (0, -100, 100):
+    print(f5(value))
+
+
+@micropython.asm_xtensa
+def f6():
+    movi(a2, 0x100)
+    movi(a3, 1)
+    add(a2, a2, a3)
+    addi(a2, a2, 1)
+    addi(a2, a2, -2)
+    sub(a2, a2, a3)
+
+
+print(hex(f6()))
+
+
+@micropython.asm_xtensa
+def f7():
+    movi(a2, 0x10FF)
+    movi(a3, 1)
+    and_(a4, a2, a3)
+    or_(a4, a4, a3)
+    movi(a3, 0x200)
+    xor(a2, a4, a3)
+
+
+print(hex(f7()))
+
+
+@micropython.asm_xtensa
+def f8(a2, a3):
+    add_n(a2, a2, a3)
+
+
+print(f8(100, 200))
+
+
+@micropython.asm_xtensa
+def f9(a2):
+    addi_n(a2, a2, 1)
+
+
+print(f9(100))
+
+
+@micropython.asm_xtensa
+def f10(a2, a3) -> uint:
+    mull(a2, a2, a3)
+
+
+print(hex(f10(0xC0000000, 2)))
diff --git a/tests/inlineasm/xtensa/asmarith.py.exp b/tests/inlineasm/xtensa/asmarith.py.exp
new file mode 100644
index 00000000000..7aba46a27d9
--- /dev/null
+++ b/tests/inlineasm/xtensa/asmarith.py.exp
@@ -0,0 +1,40 @@
+10
+10
+0
+-32768
+-32767 is not a multiple of 256
+32512
+'addmi' integer 32513 isn't within range -32768..32512
+0
+a2 300
+a4 500
+a8 900
+s2 100
+s4 300
+s8 700
+a2 -100
+a4 -300
+a8 -700
+s2 -300
+s4 -500
+s8 -900
+a2 -300
+a4 -500
+a8 -900
+s2 -100
+s4 -300
+s8 -700
+a2 100
+a4 300
+a8 700
+s2 300
+s4 500
+s8 900
+0
+100
+-100
+0xff
+0x201
+300
+101
+0x80000000
diff --git a/tests/inlineasm/xtensa/asmbranch.py b/tests/inlineasm/xtensa/asmbranch.py
new file mode 100644
index 00000000000..22bcd5a7c71
--- /dev/null
+++ b/tests/inlineasm/xtensa/asmbranch.py
@@ -0,0 +1,299 @@
+# test branch instructions
+
+
+@micropython.asm_xtensa
+def tball(a2, a3) -> int:
+    mov(a4, a2)
+    movi(a2, 0)
+    ball(a4, a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tball(0xFFFFFFFF, 0xFFFFFFFF))
+print(tball(0xFFFEFFFF, 0xFFFFFFFF))
+print(tball(0x00000000, 0xFFFFFFFF))
+print(tball(0xFFFFFFFF, 0x01010101))
+
+
+@micropython.asm_xtensa
+def tbany(a2, a3) -> int:
+    mov(a4, a2)
+    movi(a2, 0)
+    bany(a4, a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbany(0xFFFFFFFF, 0xFFFFFFFF))
+print(tbany(0xFFFEFFFF, 0xFFFFFFFF))
+print(tbany(0x00000000, 0xFFFFFFFF))
+print(tbany(0xFFFFFFFF, 0x01010101))
+
+
+@micropython.asm_xtensa
+def tbbc(a2, a3) -> int:
+    mov(a4, a2)
+    movi(a2, 0)
+    bbc(a4, a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbbc(0xFFFFFFFF, 4))
+print(tbbc(0xFFFEFFFF, 16))
+print(tbbc(0x00000000, 1))
+
+
+BBCI_TEMPLATE = """
+@micropython.asm_xtensa
+def tbbci(a2) -> int:
+    mov(a3, a2)
+    movi(a2, 0)
+    bbci(a3, {}, end)
+    movi(a2, -1)
+    label(end)
+
+print(tbbci({}))
+"""
+
+
+for value, bit in ((0xFFFFFFFF, 4), (0xFFFEFFFF, 16), (0x00000000, 1)):
+    try:
+        exec(BBCI_TEMPLATE.format(bit, value))
+    except SyntaxError as error:
+        print(error)
+
+
+@micropython.asm_xtensa
+def tbbs(a2, a3) -> int:
+    mov(a4, a2)
+    movi(a2, 0)
+    bbs(a4, a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbbs(0x00000000, 4))
+print(tbbs(0x00010000, 16))
+print(tbbs(0xFFFFFFFF, 1))
+
+
+BBSI_TEMPLATE = """
+@micropython.asm_xtensa
+def tbbsi(a2) -> int:
+    mov(a3, a2)
+    movi(a2, 0)
+    bbsi(a3, {}, end)
+    movi(a2, -1)
+    label(end)
+
+print(tbbsi({}))
+"""
+
+
+for value, bit in ((0x00000000, 4), (0x00010000, 16), (0xFFFFFFFF, 1)):
+    try:
+        exec(BBSI_TEMPLATE.format(bit, value))
+    except SyntaxError as error:
+        print(error)
+
+
+@micropython.asm_xtensa
+def tbeq(a2, a3) -> int:
+    mov(a4, a2)
+    movi(a2, 0)
+    beq(a4, a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbeq(0x00000000, 0x00000000))
+print(tbeq(0x00010000, 0x00000000))
+print(tbeq(0xFFFFFFFF, 0xFFFFFFFF))
+
+
+@micropython.asm_xtensa
+def tbeqz(a2) -> int:
+    mov(a3, a2)
+    movi(a2, 0)
+    beqz(a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbeqz(0))
+print(tbeqz(0x12345678))
+print(tbeqz(-1))
+
+
+@micropython.asm_xtensa
+def tbge(a2, a3) -> int:
+    mov(a4, a2)
+    movi(a2, 0)
+    bge(a4, a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbge(0x00000000, 0x00000000))
+print(tbge(0x00010000, 0x00000000))
+print(tbge(0xF0000000, 0xFFFFFFFF))
+
+
+@micropython.asm_xtensa
+def tbgeu(a2, a3) -> int:
+    mov(a4, a2)
+    movi(a2, 0)
+    bgeu(a4, a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbgeu(0x00000000, 0x00000000))
+print(tbgeu(0x00010000, 0x00000000))
+print(tbgeu(0xF0000000, 0xFFFFFFFF))
+print(tbgeu(0xFFFFFFFF, 0xF0000000))
+
+
+@micropython.asm_xtensa
+def tbgez(a2) -> int:
+    mov(a3, a2)
+    movi(a2, 0)
+    bgez(a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbgez(0))
+print(tbgez(0x12345678))
+print(tbgez(-1))
+
+
+@micropython.asm_xtensa
+def tblt(a2, a3) -> int:
+    mov(a4, a2)
+    movi(a2, 0)
+    blt(a4, a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tblt(0x00000000, 0x00000000))
+print(tblt(0x00010000, 0x00000000))
+print(tblt(0xF0000000, 0xFFFFFFFF))
+
+
+@micropython.asm_xtensa
+def tbltu(a2, a3) -> int:
+    mov(a4, a2)
+    movi(a2, 0)
+    bltu(a4, a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbltu(0x00000000, 0x00000000))
+print(tbltu(0x00010000, 0x00000000))
+print(tbltu(0xF0000000, 0xFFFFFFFF))
+print(tbltu(0xFFFFFFFF, 0xF0000000))
+
+
+@micropython.asm_xtensa
+def tbltz(a2) -> int:
+    mov(a3, a2)
+    movi(a2, 0)
+    bltz(a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbltz(0))
+print(tbltz(0x12345678))
+print(tbltz(-1))
+
+
+@micropython.asm_xtensa
+def tbnall(a2, a3) -> int:
+    mov(a4, a2)
+    movi(a2, 0)
+    bnall(a4, a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbnall(0xFFFFFFFF, 0xFFFFFFFF))
+print(tbnall(0xFFFEFFFF, 0xFFFFFFFF))
+print(tbnall(0x00000000, 0xFFFFFFFF))
+print(tbnall(0xFFFFFFFF, 0x01010101))
+
+
+@micropython.asm_xtensa
+def tbne(a2, a3) -> int:
+    mov(a4, a2)
+    movi(a2, 0)
+    bne(a4, a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbne(0x00000000, 0x00000000))
+print(tbne(0x00010000, 0x00000000))
+print(tbne(0xFFFFFFFF, 0xFFFFFFFF))
+
+
+@micropython.asm_xtensa
+def tbnez(a2) -> int:
+    mov(a3, a2)
+    movi(a2, 0)
+    bnez(a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbnez(0))
+print(tbnez(0x12345678))
+print(tbnez(-1))
+
+
+@micropython.asm_xtensa
+def tbnone(a2, a3) -> int:
+    mov(a4, a2)
+    movi(a2, 0)
+    bnone(a4, a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbnone(0xFFFFFFFF, 0xFFFFFFFF))
+print(tbnone(0xFFFEFFFF, 0xFFFFFFFF))
+print(tbnone(0x00000000, 0xFFFFFFFF))
+print(tbnone(0x10101010, 0x01010101))
+
+
+@micropython.asm_xtensa
+def tbeqz_n(a2) -> int:
+    mov(a3, a2)
+    movi(a2, 0)
+    beqz_n(a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbeqz_n(0))
+print(tbeqz_n(0x12345678))
+print(tbeqz_n(-1))
+
+
+@micropython.asm_xtensa
+def tbnez_n(a2) -> int:
+    mov(a3, a2)
+    movi(a2, 0)
+    bnez(a3, end)
+    movi(a2, -1)
+    label(end)
+
+
+print(tbnez_n(0))
+print(tbnez_n(0x12345678))
+print(tbnez_n(-1))
diff --git a/tests/inlineasm/xtensa/asmbranch.py.exp b/tests/inlineasm/xtensa/asmbranch.py.exp
new file mode 100644
index 00000000000..319a4b435ef
--- /dev/null
+++ b/tests/inlineasm/xtensa/asmbranch.py.exp
@@ -0,0 +1,66 @@
+0
+-1
+-1
+0
+0
+0
+-1
+0
+-1
+0
+0
+-1
+0
+0
+-1
+0
+0
+-1
+0
+0
+0
+-1
+0
+0
+-1
+-1
+0
+0
+-1
+0
+0
+-1
+0
+0
+0
+-1
+-1
+-1
+0
+-1
+-1
+0
+-1
+-1
+-1
+0
+-1
+0
+0
+-1
+-1
+0
+-1
+-1
+0
+0
+-1
+-1
+0
+0
+0
+-1
+-1
+-1
+0
+0
diff --git a/tests/inlineasm/xtensa/asmjump.py b/tests/inlineasm/xtensa/asmjump.py
new file mode 100644
index 00000000000..f41c9482319
--- /dev/null
+++ b/tests/inlineasm/xtensa/asmjump.py
@@ -0,0 +1,26 @@
+@micropython.asm_xtensa
+def jump() -> int:
+    movi(a2, 0)
+    j(NEXT)
+    addi(a2, a2, 1)
+    j(DONE)
+    label(NEXT)
+    addi(a2, a2, 2)
+    label(DONE)
+
+
+print(jump())
+
+
+@micropython.asm_xtensa
+def jumpx() -> int:
+    call0(ENTRY)
+    label(ENTRY)
+    movi(a2, 0)
+    addi(a3, a0, 12)
+    jx(a3)
+    movi(a2, 1)
+    movi(a2, 2)
+
+
+print(jumpx())
diff --git a/tests/inlineasm/xtensa/asmjump.py.exp b/tests/inlineasm/xtensa/asmjump.py.exp
new file mode 100644
index 00000000000..51993f072d5
--- /dev/null
+++ b/tests/inlineasm/xtensa/asmjump.py.exp
@@ -0,0 +1,2 @@
+2
+2
diff --git a/tests/inlineasm/xtensa/asmloadstore.py b/tests/inlineasm/xtensa/asmloadstore.py
new file mode 100644
index 00000000000..85f1f8a561b
--- /dev/null
+++ b/tests/inlineasm/xtensa/asmloadstore.py
@@ -0,0 +1,101 @@
+import array
+
+# On the ESP8266 the generated code gets put into the IRAM segment, which is
+# only word-addressable.  Therefore, to test byte and halfword load/store
+# opcodes some memory must be reserved in the DRAM segment.  This also happens
+# to work on the ESP32 too.
+
+BYTE_DATA = array.array("B", (0x11, 0x22, 0x33, 0x44))
+WORD_DATA = array.array("h", (100, 200, -100, -200))
+DWORD_DATA = array.array("i", (100_000, -200_000, 300_000, -400_000))
+
+
+@micropython.asm_xtensa
+def tl32r() -> int:
+    nop()
+    j(CODE)
+    align(4)
+    label(DATA)
+    data(1, 1, 2, 3, 4, 5, 6, 7)
+    align(4)
+    label(CODE)
+    nop_n()
+    nop_n()
+    l32r(a2, DATA)
+
+
+print(hex(tl32r()))
+
+
+@micropython.asm_xtensa
+def tl32i() -> uint:
+    call0(ENTRY)
+    align(4)
+    label(ENTRY)
+    l32i(a2, a0, 0)
+    nop()
+
+
+print(hex(tl32i()))
+
+
+@micropython.asm_xtensa
+def tl8ui(a2) -> uint:
+    mov(a3, a2)
+    l8ui(a2, a3, 1)
+
+
+print(hex(tl8ui(BYTE_DATA)))
+
+
+@micropython.asm_xtensa
+def tl16ui(a2) -> uint:
+    mov(a3, a2)
+    l16ui(a2, a3, 2)
+
+
+print(tl16ui(WORD_DATA))
+
+
+@micropython.asm_xtensa
+def tl16si(a2) -> int:
+    mov(a3, a2)
+    l16si(a2, a3, 6)
+
+
+print(tl16si(WORD_DATA))
+
+
+@micropython.asm_xtensa
+def ts8i(a2, a3):
+    s8i(a3, a2, 1)
+
+
+ts8i(BYTE_DATA, 0xFF)
+print(BYTE_DATA)
+
+
+@micropython.asm_xtensa
+def ts16i(a2, a3):
+    s16i(a3, a2, 2)
+
+
+ts16i(WORD_DATA, -123)
+print(WORD_DATA)
+
+
+@micropython.asm_xtensa
+def ts32i(a2, a3) -> uint:
+    s32i(a3, a2, 4)
+
+
+ts32i(DWORD_DATA, -123456)
+print(DWORD_DATA)
+
+
+@micropython.asm_xtensa
+def tl32i_n(a2) -> uint:
+    l32i_n(a2, a2, 8)
+
+
+print(tl32i_n(DWORD_DATA))
diff --git a/tests/inlineasm/xtensa/asmloadstore.py.exp b/tests/inlineasm/xtensa/asmloadstore.py.exp
new file mode 100644
index 00000000000..ec453a1cad6
--- /dev/null
+++ b/tests/inlineasm/xtensa/asmloadstore.py.exp
@@ -0,0 +1,9 @@
+0x4030201
+0xf0002022
+0x22
+200
+-200
+array('B', [17, 255, 51, 68])
+array('h', [100, -123, -100, -200])
+array('i', [100000, -123456, 300000, -400000])
+300000
diff --git a/tests/inlineasm/xtensa/asmmisc.py b/tests/inlineasm/xtensa/asmmisc.py
new file mode 100644
index 00000000000..271ab836625
--- /dev/null
+++ b/tests/inlineasm/xtensa/asmmisc.py
@@ -0,0 +1,25 @@
+@micropython.asm_xtensa
+def tnop(a2, a3, a4, a5):
+    nop()
+
+
+out2 = tnop(0x100, 0x200, 0x300, 0x400)
+print(out2 == 0x100)
+
+
+@micropython.asm_xtensa
+def tnop_n(a2, a3, a4, a5):
+    nop_n()
+
+
+out2 = tnop_n(0x100, 0x200, 0x300, 0x400)
+print(out2 == 0x100)
+
+
+@micropython.asm_xtensa
+def tmov_n(a2, a3):
+    mov_n(a4, a3)
+    add(a2, a4, a3)
+
+
+print(tmov_n(0, 1))
diff --git a/tests/inlineasm/xtensa/asmmisc.py.exp b/tests/inlineasm/xtensa/asmmisc.py.exp
new file mode 100644
index 00000000000..eefaa35daf0
--- /dev/null
+++ b/tests/inlineasm/xtensa/asmmisc.py.exp
@@ -0,0 +1,3 @@
+True
+True
+2
diff --git a/tests/inlineasm/xtensa/asmshift.py b/tests/inlineasm/xtensa/asmshift.py
new file mode 100644
index 00000000000..271ca1ccd49
--- /dev/null
+++ b/tests/inlineasm/xtensa/asmshift.py
@@ -0,0 +1,137 @@
+@micropython.asm_xtensa
+def lsl1(a2):
+    slli(a2, a2, 1)
+
+
+print(hex(lsl1(0x123)))
+
+
+@micropython.asm_xtensa
+def lsl23(a2):
+    slli(a2, a2, 23)
+
+
+print(hex(lsl23(1)))
+
+
+@micropython.asm_xtensa
+def lsr1(a2):
+    srli(a2, a2, 1)
+
+
+print(hex(lsr1(0x123)))
+
+
+@micropython.asm_xtensa
+def lsr15(a2):
+    srli(a2, a2, 15)
+
+
+print(hex(lsr15(0x80000000)))
+
+
+@micropython.asm_xtensa
+def asr1(a2):
+    srai(a2, a2, 1)
+
+
+print(hex(asr1(0x123)))
+
+
+@micropython.asm_xtensa
+def asr31(a2):
+    srai(a2, a2, 31)
+
+
+print(hex(asr31(0x80000000)))
+
+
+@micropython.asm_xtensa
+def lsl1r(a2):
+    movi(a3, 1)
+    ssl(a3)
+    sll(a2, a2)
+
+
+print(hex(lsl1r(0x123)))
+
+
+@micropython.asm_xtensa
+def lsr1r(a2):
+    movi(a3, 1)
+    ssr(a3)
+    srl(a2, a2)
+
+
+print(hex(lsr1r(0x123)))
+
+
+@micropython.asm_xtensa
+def asr1r(a2):
+    movi(a3, 1)
+    ssr(a3)
+    sra(a2, a2)
+
+
+print(hex(asr1r(0x123)))
+
+
+@micropython.asm_xtensa
+def sll9(a2):
+    ssai(9)
+    sll(a2, a2)
+
+
+print(hex(sll9(1)))
+
+
+@micropython.asm_xtensa
+def srlr(a2, a3):
+    ssa8l(a3)
+    srl(a2, a2)
+
+
+print(hex(srlr(0x12340000, 2)))
+
+
+@micropython.asm_xtensa
+def sllr(a2, a3):
+    ssa8b(a3)
+    sll(a2, a2)
+
+
+print(hex(sllr(0x1234, 2)))
+
+
+@micropython.asm_xtensa
+def srcr(a2, a3, a4):
+    ssr(a4)
+    src(a2, a2, a3)
+
+
+print(hex(srcr(0x00000001, 0x80000000, 2)))
+
+
+@micropython.asm_xtensa
+def srai24(a2):
+    srai(a2, a2, 24)
+
+
+print(hex(srai24(0x12345678)))
+
+
+@micropython.asm_xtensa
+def nsar(a2, a3):
+    nsa(a2, a3)
+
+
+print(nsar(0x12345678, 0))
+print(nsar(0x12345678, -1))
+
+
+@micropython.asm_xtensa
+def nsaur(a2, a3):
+    nsau(a2, a3)
+
+
+print(nsaur(0x12345678, 0))
diff --git a/tests/inlineasm/xtensa/asmshift.py.exp b/tests/inlineasm/xtensa/asmshift.py.exp
new file mode 100644
index 00000000000..3e2bb3b4aef
--- /dev/null
+++ b/tests/inlineasm/xtensa/asmshift.py.exp
@@ -0,0 +1,17 @@
+0x246
+0x800000
+0x91
+0x10000
+0x91
+-0x1
+0x246
+0x91
+0x91
+0x800000
+0x1234
+0x12340000
+0x60000000
+0x12
+31
+31
+32
diff --git a/tests/internal_bench/class_create-0-empty.py b/tests/internal_bench/class_create-0-empty.py
new file mode 100644
index 00000000000..1fd8ccd9257
--- /dev/null
+++ b/tests/internal_bench/class_create-0-empty.py
@@ -0,0 +1,11 @@
+import bench
+
+
+def test(num):
+    for i in range(num // 40):
+
+        class X:
+            pass
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-1-slots.py b/tests/internal_bench/class_create-1-slots.py
new file mode 100644
index 00000000000..9b3e4b9570d
--- /dev/null
+++ b/tests/internal_bench/class_create-1-slots.py
@@ -0,0 +1,12 @@
+import bench
+
+
+def test(num):
+    l = ["x"]
+    for i in range(num // 40):
+
+        class X:
+            __slots__ = l
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-1.1-slots5.py b/tests/internal_bench/class_create-1.1-slots5.py
new file mode 100644
index 00000000000..ccac77dec9d
--- /dev/null
+++ b/tests/internal_bench/class_create-1.1-slots5.py
@@ -0,0 +1,12 @@
+import bench
+
+
+def test(num):
+    l = ["a", "b", "c", "d", "x"]
+    for i in range(num // 40):
+
+        class X:
+            __slots__ = l
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-2-classattr.py b/tests/internal_bench/class_create-2-classattr.py
new file mode 100644
index 00000000000..049a7dab170
--- /dev/null
+++ b/tests/internal_bench/class_create-2-classattr.py
@@ -0,0 +1,11 @@
+import bench
+
+
+def test(num):
+    for i in range(num // 40):
+
+        class X:
+            x = 1
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-2.1-classattr5.py b/tests/internal_bench/class_create-2.1-classattr5.py
new file mode 100644
index 00000000000..5051e7dcca7
--- /dev/null
+++ b/tests/internal_bench/class_create-2.1-classattr5.py
@@ -0,0 +1,15 @@
+import bench
+
+
+def test(num):
+    for i in range(num // 40):
+
+        class X:
+            a = 0
+            b = 0
+            c = 0
+            d = 0
+            x = 1
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-2.3-classattr5objs.py b/tests/internal_bench/class_create-2.3-classattr5objs.py
new file mode 100644
index 00000000000..74540865dcd
--- /dev/null
+++ b/tests/internal_bench/class_create-2.3-classattr5objs.py
@@ -0,0 +1,20 @@
+import bench
+
+
+class Class:
+    pass
+
+
+def test(num):
+    instance = Class()
+    for i in range(num // 40):
+
+        class X:
+            a = instance
+            b = instance
+            c = instance
+            d = instance
+            x = instance
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-3-instancemethod.py b/tests/internal_bench/class_create-3-instancemethod.py
new file mode 100644
index 00000000000..e8c201cb2c3
--- /dev/null
+++ b/tests/internal_bench/class_create-3-instancemethod.py
@@ -0,0 +1,12 @@
+import bench
+
+
+def test(num):
+    for i in range(num // 40):
+
+        class X:
+            def x(self):
+                pass
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-4-classmethod.py b/tests/internal_bench/class_create-4-classmethod.py
new file mode 100644
index 00000000000..f34962bc671
--- /dev/null
+++ b/tests/internal_bench/class_create-4-classmethod.py
@@ -0,0 +1,13 @@
+import bench
+
+
+def test(num):
+    for i in range(num // 40):
+
+        class X:
+            @classmethod
+            def x(cls):
+                pass
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-4.1-classmethod_implicit.py b/tests/internal_bench/class_create-4.1-classmethod_implicit.py
new file mode 100644
index 00000000000..f2d1fcfd188
--- /dev/null
+++ b/tests/internal_bench/class_create-4.1-classmethod_implicit.py
@@ -0,0 +1,12 @@
+import bench
+
+
+def test(num):
+    for i in range(num // 40):
+
+        class X:
+            def __new__(cls):
+                pass
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-5-staticmethod.py b/tests/internal_bench/class_create-5-staticmethod.py
new file mode 100644
index 00000000000..06335566675
--- /dev/null
+++ b/tests/internal_bench/class_create-5-staticmethod.py
@@ -0,0 +1,13 @@
+import bench
+
+
+def test(num):
+    for i in range(num // 40):
+
+        class X:
+            @staticmethod
+            def x():
+                pass
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-6-getattribute.py b/tests/internal_bench/class_create-6-getattribute.py
new file mode 100644
index 00000000000..10a4fe7ce8d
--- /dev/null
+++ b/tests/internal_bench/class_create-6-getattribute.py
@@ -0,0 +1,12 @@
+import bench
+
+
+def test(num):
+    for i in range(num // 40):
+
+        class X:
+            def __getattribute__(self, name):
+                pass
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-6.1-getattr.py b/tests/internal_bench/class_create-6.1-getattr.py
new file mode 100644
index 00000000000..b4b9ba2f552
--- /dev/null
+++ b/tests/internal_bench/class_create-6.1-getattr.py
@@ -0,0 +1,12 @@
+import bench
+
+
+def test(num):
+    for i in range(num // 40):
+
+        class X:
+            def __getattr__(self, name):
+                pass
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-6.2-property.py b/tests/internal_bench/class_create-6.2-property.py
new file mode 100644
index 00000000000..cf847b6dc9c
--- /dev/null
+++ b/tests/internal_bench/class_create-6.2-property.py
@@ -0,0 +1,13 @@
+import bench
+
+
+def test(num):
+    for i in range(num // 40):
+
+        class X:
+            @property
+            def x(self):
+                pass
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-6.3-descriptor.py b/tests/internal_bench/class_create-6.3-descriptor.py
new file mode 100644
index 00000000000..7b0a6357263
--- /dev/null
+++ b/tests/internal_bench/class_create-6.3-descriptor.py
@@ -0,0 +1,17 @@
+import bench
+
+
+class D:
+    def __get__(self, instance, owner=None):
+        pass
+
+
+def test(num):
+    descriptor = D()
+    for i in range(num // 40):
+
+        class X:
+            x = descriptor
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-7-inherit.py b/tests/internal_bench/class_create-7-inherit.py
new file mode 100644
index 00000000000..f48fb215e0a
--- /dev/null
+++ b/tests/internal_bench/class_create-7-inherit.py
@@ -0,0 +1,14 @@
+import bench
+
+
+def test(num):
+    class B:
+        pass
+
+    for i in range(num // 40):
+
+        class X(B):
+            pass
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-7.1-inherit_initsubclass.py b/tests/internal_bench/class_create-7.1-inherit_initsubclass.py
new file mode 100644
index 00000000000..0660fa86258
--- /dev/null
+++ b/tests/internal_bench/class_create-7.1-inherit_initsubclass.py
@@ -0,0 +1,16 @@
+import bench
+
+
+def test(num):
+    class B:
+        @classmethod
+        def __init_subclass__(cls):
+            pass
+
+    for i in range(num // 40):
+
+        class X(B):
+            pass
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-8-metaclass_setname.py b/tests/internal_bench/class_create-8-metaclass_setname.py
new file mode 100644
index 00000000000..e4515b54279
--- /dev/null
+++ b/tests/internal_bench/class_create-8-metaclass_setname.py
@@ -0,0 +1,17 @@
+import bench
+
+
+class D:
+    def __set_name__(self, owner, name):
+        pass
+
+
+def test(num):
+    descriptor = D()
+    for i in range(num // 40):
+
+        class X:
+            x = descriptor
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_create-8.1-metaclass_setname5.py b/tests/internal_bench/class_create-8.1-metaclass_setname5.py
new file mode 100644
index 00000000000..5daa3f8471b
--- /dev/null
+++ b/tests/internal_bench/class_create-8.1-metaclass_setname5.py
@@ -0,0 +1,21 @@
+import bench
+
+
+class D:
+    def __set_name__(self, owner, name):
+        pass
+
+
+def test(num):
+    descriptor = D()
+    for i in range(num // 40):
+
+        class X:
+            a = descriptor
+            b = descriptor
+            c = descriptor
+            d = descriptor
+            x = descriptor
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_instance-0-object.py b/tests/internal_bench/class_instance-0-object.py
new file mode 100644
index 00000000000..401c8ea7e3c
--- /dev/null
+++ b/tests/internal_bench/class_instance-0-object.py
@@ -0,0 +1,11 @@
+import bench
+
+X = object
+
+
+def test(num):
+    for i in range(num // 5):
+        x = X()
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_instance-0.1-object-gc.py b/tests/internal_bench/class_instance-0.1-object-gc.py
new file mode 100644
index 00000000000..7c475963a8e
--- /dev/null
+++ b/tests/internal_bench/class_instance-0.1-object-gc.py
@@ -0,0 +1,13 @@
+import bench
+import gc
+
+X = object
+
+
+def test(num):
+    for i in range(num // 5):
+        x = X()
+    gc.collect()
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_instance-1-empty.py b/tests/internal_bench/class_instance-1-empty.py
new file mode 100644
index 00000000000..617d47a86e9
--- /dev/null
+++ b/tests/internal_bench/class_instance-1-empty.py
@@ -0,0 +1,13 @@
+import bench
+
+
+class X:
+    pass
+
+
+def test(num):
+    for i in range(num // 5):
+        x = X()
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_instance-1.1-classattr.py b/tests/internal_bench/class_instance-1.1-classattr.py
new file mode 100644
index 00000000000..4e667533d4a
--- /dev/null
+++ b/tests/internal_bench/class_instance-1.1-classattr.py
@@ -0,0 +1,13 @@
+import bench
+
+
+class X:
+    x = 0
+
+
+def test(num):
+    for i in range(num // 5):
+        x = X()
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_instance-1.2-func.py b/tests/internal_bench/class_instance-1.2-func.py
new file mode 100644
index 00000000000..21bf7a1ac48
--- /dev/null
+++ b/tests/internal_bench/class_instance-1.2-func.py
@@ -0,0 +1,14 @@
+import bench
+
+
+class X:
+    def f(self):
+        pass
+
+
+def test(num):
+    for i in range(num // 5):
+        x = X()
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_instance-1.3-empty-gc.py b/tests/internal_bench/class_instance-1.3-empty-gc.py
new file mode 100644
index 00000000000..a5108ef8e81
--- /dev/null
+++ b/tests/internal_bench/class_instance-1.3-empty-gc.py
@@ -0,0 +1,15 @@
+import bench
+import gc
+
+
+class X:
+    pass
+
+
+def test(num):
+    for i in range(num // 5):
+        x = X()
+    gc.collect()
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_instance-2-init.py b/tests/internal_bench/class_instance-2-init.py
new file mode 100644
index 00000000000..86619d31548
--- /dev/null
+++ b/tests/internal_bench/class_instance-2-init.py
@@ -0,0 +1,14 @@
+import bench
+
+
+class X:
+    def __init__(self):
+        pass
+
+
+def test(num):
+    for i in range(num // 5):
+        x = X()
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_instance-2.1-init_super.py b/tests/internal_bench/class_instance-2.1-init_super.py
new file mode 100644
index 00000000000..38bca5fef87
--- /dev/null
+++ b/tests/internal_bench/class_instance-2.1-init_super.py
@@ -0,0 +1,14 @@
+import bench
+
+
+class X:
+    def __init__(self):
+        return super().__init__()
+
+
+def test(num):
+    for i in range(num // 5):
+        x = X()
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_instance-2.2-new.py b/tests/internal_bench/class_instance-2.2-new.py
new file mode 100644
index 00000000000..dc5e78ea5ef
--- /dev/null
+++ b/tests/internal_bench/class_instance-2.2-new.py
@@ -0,0 +1,14 @@
+import bench
+
+
+class X:
+    def __new__(cls):
+        return super().__new__(cls)
+
+
+def test(num):
+    for i in range(num // 5):
+        x = X()
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_instance-3-del.py b/tests/internal_bench/class_instance-3-del.py
new file mode 100644
index 00000000000..af700f72a94
--- /dev/null
+++ b/tests/internal_bench/class_instance-3-del.py
@@ -0,0 +1,14 @@
+import bench
+
+
+class X:
+    def __del__(self):
+        pass
+
+
+def test(num):
+    for i in range(num // 5):
+        x = X()
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_instance-3.1-del-gc.py b/tests/internal_bench/class_instance-3.1-del-gc.py
new file mode 100644
index 00000000000..311c71c3571
--- /dev/null
+++ b/tests/internal_bench/class_instance-3.1-del-gc.py
@@ -0,0 +1,16 @@
+import bench
+import gc
+
+
+class X:
+    def __del__(self):
+        pass
+
+
+def test(num):
+    for i in range(num // 5):
+        x = X()
+    gc.collect()
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_instance-4-slots.py b/tests/internal_bench/class_instance-4-slots.py
new file mode 100644
index 00000000000..51b067fedf0
--- /dev/null
+++ b/tests/internal_bench/class_instance-4-slots.py
@@ -0,0 +1,13 @@
+import bench
+
+
+class X:
+    __slots__ = ["x"]
+
+
+def test(num):
+    for i in range(num // 5):
+        x = X()
+
+
+bench.run(test)
diff --git a/tests/internal_bench/class_instance-4.1-slots5.py b/tests/internal_bench/class_instance-4.1-slots5.py
new file mode 100644
index 00000000000..8f5c2ecb456
--- /dev/null
+++ b/tests/internal_bench/class_instance-4.1-slots5.py
@@ -0,0 +1,13 @@
+import bench
+
+
+class X:
+    __slots__ = ["a", "b", "c", "d", "x"]
+
+
+def test(num):
+    for i in range(num // 5):
+        x = X()
+
+
+bench.run(test)
diff --git a/tests/internal_bench/var-6.2-instance-speciallookup.py b/tests/internal_bench/var-6.2-instance-speciallookup.py
new file mode 100644
index 00000000000..fee12b2f930
--- /dev/null
+++ b/tests/internal_bench/var-6.2-instance-speciallookup.py
@@ -0,0 +1,19 @@
+import bench
+
+
+class Foo:
+    def __init__(self):
+        self.num = 20000000
+
+    def __delattr__(self, name):  # just trigger the 'special lookups' flag on the class
+        pass
+
+
+def test(num):
+    o = Foo()
+    i = 0
+    while i < o.num:
+        i += 1
+
+
+bench.run(test)
diff --git a/tests/internal_bench/var-6.3-instance-property.py b/tests/internal_bench/var-6.3-instance-property.py
new file mode 100644
index 00000000000..b4426ef7928
--- /dev/null
+++ b/tests/internal_bench/var-6.3-instance-property.py
@@ -0,0 +1,17 @@
+import bench
+
+
+class Foo:
+    @property
+    def num(self):
+        return 20000000
+
+
+def test(num):
+    o = Foo()
+    i = 0
+    while i < o.num:
+        i += 1
+
+
+bench.run(test)
diff --git a/tests/internal_bench/var-6.4-instance-descriptor.py b/tests/internal_bench/var-6.4-instance-descriptor.py
new file mode 100644
index 00000000000..b4df69f878f
--- /dev/null
+++ b/tests/internal_bench/var-6.4-instance-descriptor.py
@@ -0,0 +1,20 @@
+import bench
+
+
+class Descriptor:
+    def __get__(self, instance, owner=None):
+        return 20000000
+
+
+class Foo:
+    num = Descriptor()
+
+
+def test(num):
+    o = Foo()
+    i = 0
+    while i < o.num:
+        i += 1
+
+
+bench.run(test)
diff --git a/tests/internal_bench/var-6.5-instance-getattr.py b/tests/internal_bench/var-6.5-instance-getattr.py
new file mode 100644
index 00000000000..3b2ef672110
--- /dev/null
+++ b/tests/internal_bench/var-6.5-instance-getattr.py
@@ -0,0 +1,16 @@
+import bench
+
+
+class Foo:
+    def __getattr__(self, name):
+        return 20000000
+
+
+def test(num):
+    o = Foo()
+    i = 0
+    while i < o.num:
+        i += 1
+
+
+bench.run(test)
diff --git a/tests/internal_bench/var-6.6-instance-builtin_ordered.py b/tests/internal_bench/var-6.6-instance-builtin_ordered.py
new file mode 100644
index 00000000000..02d75252301
--- /dev/null
+++ b/tests/internal_bench/var-6.6-instance-builtin_ordered.py
@@ -0,0 +1,12 @@
+import bench
+
+
+def test(num):
+    i = 0
+    o = set()  # object with largest rom-frozen ordered locals_dict
+    n = "__contains__"  # last element in that dict for longest lookup
+    while i < num:
+        i += hasattr(o, n)  # True, converts to 1
+
+
+bench.run(test)
diff --git a/tests/internal_bench/var-8-namedtuple-1st.py b/tests/internal_bench/var-8-namedtuple-1st.py
index 1a6daa6cddc..e3a2fa19cd3 100644
--- a/tests/internal_bench/var-8-namedtuple-1st.py
+++ b/tests/internal_bench/var-8-namedtuple-1st.py
@@ -1,5 +1,5 @@
 import bench
-from ucollections import namedtuple
+from collections import namedtuple
 
 T = namedtuple("Tup", ["num", "bar"])
 
diff --git a/tests/internal_bench/var-8.1-namedtuple-5th.py b/tests/internal_bench/var-8.1-namedtuple-5th.py
index 568ece80671..3e521217466 100644
--- a/tests/internal_bench/var-8.1-namedtuple-5th.py
+++ b/tests/internal_bench/var-8.1-namedtuple-5th.py
@@ -1,5 +1,5 @@
 import bench
-from ucollections import namedtuple
+from collections import namedtuple
 
 T = namedtuple("Tup", ["foo1", "foo2", "foo3", "foo4", "num"])
 
diff --git a/tests/internal_bench/var-9-getattr.py b/tests/internal_bench/var-9-getattr.py
new file mode 100644
index 00000000000..69d2bfed2e0
--- /dev/null
+++ b/tests/internal_bench/var-9-getattr.py
@@ -0,0 +1,16 @@
+import bench
+
+
+class Foo:
+    pass
+
+
+def test(num):
+    o = Foo()
+    o.num = num
+    i = 0
+    while i < getattr(o, "num", num):
+        i += 1
+
+
+bench.run(test)
diff --git a/tests/internal_bench/var-9.1-getattr_default.py b/tests/internal_bench/var-9.1-getattr_default.py
new file mode 100644
index 00000000000..e803d39b326
--- /dev/null
+++ b/tests/internal_bench/var-9.1-getattr_default.py
@@ -0,0 +1,15 @@
+import bench
+
+
+class Foo:
+    pass
+
+
+def test(num):
+    o = Foo()
+    i = 0
+    while i < getattr(o, "num", num):
+        i += 1
+
+
+bench.run(test)
diff --git a/tests/internal_bench/var-9.2-getattr_default_special.py b/tests/internal_bench/var-9.2-getattr_default_special.py
new file mode 100644
index 00000000000..c48ec0742cf
--- /dev/null
+++ b/tests/internal_bench/var-9.2-getattr_default_special.py
@@ -0,0 +1,16 @@
+import bench
+
+
+class Foo:
+    def __delattr__(self, name):  # just trigger the 'special lookups' flag on the class
+        pass
+
+
+def test(num):
+    o = Foo()
+    i = 0
+    while i < getattr(o, "num", num):
+        i += 1
+
+
+bench.run(test)
diff --git a/tests/internal_bench/var-9.3-except_ok.py b/tests/internal_bench/var-9.3-except_ok.py
new file mode 100644
index 00000000000..efc1a8f858c
--- /dev/null
+++ b/tests/internal_bench/var-9.3-except_ok.py
@@ -0,0 +1,23 @@
+import bench
+
+
+class Foo:
+    pass
+
+
+def test(num):
+    o = Foo()
+    o.num = num
+
+    def get():
+        try:
+            return o.num
+        except AttributeError:
+            return num
+
+    i = 0
+    while i < get():
+        i += 1
+
+
+bench.run(test)
diff --git a/tests/internal_bench/var-9.4-except_selfinduced.py b/tests/internal_bench/var-9.4-except_selfinduced.py
new file mode 100644
index 00000000000..544609ca4b6
--- /dev/null
+++ b/tests/internal_bench/var-9.4-except_selfinduced.py
@@ -0,0 +1,22 @@
+import bench
+
+
+class Foo:
+    pass
+
+
+def test(num):
+    o = Foo()
+
+    def get():
+        try:
+            raise AttributeError
+        except AttributeError:
+            return num
+
+    i = 0
+    while i < get():
+        i += 1
+
+
+bench.run(test)
diff --git a/tests/internal_bench/var-9.5-except_error.py b/tests/internal_bench/var-9.5-except_error.py
new file mode 100644
index 00000000000..caf83fa46ab
--- /dev/null
+++ b/tests/internal_bench/var-9.5-except_error.py
@@ -0,0 +1,22 @@
+import bench
+
+
+class Foo:
+    pass
+
+
+def test(num):
+    o = Foo()
+
+    def get():
+        try:
+            return o.num
+        except AttributeError:
+            return num
+
+    i = 0
+    while i < get():
+        i += 1
+
+
+bench.run(test)
diff --git a/tests/internal_bench/var-9.6-except_error_special.py b/tests/internal_bench/var-9.6-except_error_special.py
new file mode 100644
index 00000000000..8bc395b4d7e
--- /dev/null
+++ b/tests/internal_bench/var-9.6-except_error_special.py
@@ -0,0 +1,23 @@
+import bench
+
+
+class Foo:
+    def __delattr__(self, name):  # just trigger the 'special lookups' flag on the class
+        pass
+
+
+def test(num):
+    o = Foo()
+
+    def get():
+        try:
+            return o.num
+        except AttributeError:
+            return num
+
+    i = 0
+    while i < get():
+        i += 1
+
+
+bench.run(test)
diff --git a/tests/io/argv.py b/tests/io/argv.py
index 834292504d0..53254da1190 100644
--- a/tests/io/argv.py
+++ b/tests/io/argv.py
@@ -1,6 +1,3 @@
-try:
-    import usys as sys
-except ImportError:
-    import sys
+import sys
 
 print(sys.argv)
diff --git a/tests/io/builtin_print_file.py b/tests/io/builtin_print_file.py
index e5c20b64e4b..e00f58635a9 100644
--- a/tests/io/builtin_print_file.py
+++ b/tests/io/builtin_print_file.py
@@ -1,9 +1,6 @@
 # test builtin print function, using file= argument
 
-try:
-    import usys as sys
-except ImportError:
-    import sys
+import sys
 
 try:
     sys.stdout
@@ -16,5 +13,5 @@
 
 try:
     print(file=1)
-except (AttributeError, OSError):  # CPython and uPy differ in error message
+except (AttributeError, OSError):  # CPython and MicroPython differ in error message
     print("Error")
diff --git a/tests/io/data/bigfile1 b/tests/io/data/bigfile1
index 5afbf01a6f4..777c436a21f 100644
--- a/tests/io/data/bigfile1
+++ b/tests/io/data/bigfile1
@@ -1,147 +1,147 @@
 {
     "info": {
-        "maintainer": null, 
-        "docs_url": "", 
-        "requires_python": null, 
-        "maintainer_email": null, 
-        "cheesecake_code_kwalitee_id": null, 
-        "keywords": null, 
-        "package_url": "http://pypi.python.org/pypi/micropython-uasyncio", 
-        "author": "MicroPython Developers", 
-        "author_email": "micro-python@googlegroups.com", 
-        "download_url": "UNKNOWN", 
-        "platform": "UNKNOWN", 
-        "version": "0.8.1", 
-        "cheesecake_documentation_id": null, 
-        "_pypi_hidden": false, 
-        "description": "Lightweight asyncio-like library built around native Python coroutines, not around un-Python devices like callback mess.", 
-        "release_url": "http://pypi.python.org/pypi/micropython-uasyncio/0.8.1", 
+        "maintainer": null,
+        "docs_url": "",
+        "requires_python": null,
+        "maintainer_email": null,
+        "cheesecake_code_kwalitee_id": null,
+        "keywords": null,
+        "package_url": "http://pypi.python.org/pypi/micropython-uasyncio",
+        "author": "MicroPython Developers",
+        "author_email": "micro-python@googlegroups.com",
+        "download_url": "UNKNOWN",
+        "platform": "UNKNOWN",
+        "version": "0.8.1",
+        "cheesecake_documentation_id": null,
+        "_pypi_hidden": false,
+        "description": "Lightweight asyncio-like library built around native Python coroutines, not around un-Python devices like callback mess.",
+        "release_url": "http://pypi.python.org/pypi/micropython-uasyncio/0.8.1",
         "downloads": {
-            "last_month": 942, 
-            "last_week": 173, 
+            "last_month": 942,
+            "last_week": 173,
             "last_day": 29
-        }, 
-        "_pypi_ordering": 6, 
-        "classifiers": [], 
-        "name": "micropython-uasyncio", 
-        "bugtrack_url": null, 
-        "license": "MIT", 
-        "summary": "uasyncio module for MicroPython", 
-        "home_page": "https://github.com/micropython/micropython/issues/405", 
-        "stable_version": null, 
+        },
+        "_pypi_ordering": 6,
+        "classifiers": [],
+        "name": "micropython-uasyncio",
+        "bugtrack_url": null,
+        "license": "MIT",
+        "summary": "uasyncio module for MicroPython",
+        "home_page": "https://github.com/micropython/micropython/issues/405",
+        "stable_version": null,
         "cheesecake_installability_id": null
-    }, 
+    },
     "releases": {
         "0.8": [
             {
-                "has_sig": false, 
-                "upload_time": "2015-01-01T23:52:41", 
-                "comment_text": "", 
-                "python_version": "source", 
-                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.8.tar.gz", 
-                "md5_digest": "5df4d0d6b5fdb7c05fc418e5785e1336", 
-                "downloads": 352, 
-                "filename": "micropython-uasyncio-0.8.tar.gz", 
-                "packagetype": "sdist", 
+                "has_sig": false,
+                "upload_time": "2015-01-01T23:52:41",
+                "comment_text": "",
+                "python_version": "source",
+                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.8.tar.gz",
+                "md5_digest": "5df4d0d6b5fdb7c05fc418e5785e1336",
+                "downloads": 352,
+                "filename": "micropython-uasyncio-0.8.tar.gz",
+                "packagetype": "sdist",
                 "size": 2476
             }
-        ], 
+        ],
         "0.6.2": [
             {
-                "has_sig": false, 
-                "upload_time": "2014-10-18T11:26:52", 
-                "comment_text": "", 
-                "python_version": "source", 
-                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.6.2.tar.gz", 
-                "md5_digest": "c85fa7c11ef437f4e73c1fcd639db066", 
-                "downloads": 475, 
-                "filename": "micropython-uasyncio-0.6.2.tar.gz", 
-                "packagetype": "sdist", 
+                "has_sig": false,
+                "upload_time": "2014-10-18T11:26:52",
+                "comment_text": "",
+                "python_version": "source",
+                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.6.2.tar.gz",
+                "md5_digest": "c85fa7c11ef437f4e73c1fcd639db066",
+                "downloads": 475,
+                "filename": "micropython-uasyncio-0.6.2.tar.gz",
+                "packagetype": "sdist",
                 "size": 3262
             }
-        ], 
+        ],
         "0.6.1": [
             {
-                "has_sig": false, 
-                "upload_time": "2014-10-11T02:21:17", 
-                "comment_text": "", 
-                "python_version": "source", 
-                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.6.1.tar.gz", 
-                "md5_digest": "48cb0db7d8249d5f4a86db9c4b302d03", 
-                "downloads": 507, 
-                "filename": "micropython-uasyncio-0.6.1.tar.gz", 
-                "packagetype": "sdist", 
+                "has_sig": false,
+                "upload_time": "2014-10-11T02:21:17",
+                "comment_text": "",
+                "python_version": "source",
+                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.6.1.tar.gz",
+                "md5_digest": "48cb0db7d8249d5f4a86db9c4b302d03",
+                "downloads": 507,
+                "filename": "micropython-uasyncio-0.6.1.tar.gz",
+                "packagetype": "sdist",
                 "size": 3237
             }
-        ], 
+        ],
         "0.8.1": [
             {
-                "has_sig": false, 
-                "upload_time": "2015-01-04T20:02:03", 
-                "comment_text": "", 
-                "python_version": "source", 
-                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.8.1.tar.gz", 
-                "md5_digest": "940d2647b8355289d54de543ff710b05", 
-                "downloads": 249, 
-                "filename": "micropython-uasyncio-0.8.1.tar.gz", 
-                "packagetype": "sdist", 
+                "has_sig": false,
+                "upload_time": "2015-01-04T20:02:03",
+                "comment_text": "",
+                "python_version": "source",
+                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.8.1.tar.gz",
+                "md5_digest": "940d2647b8355289d54de543ff710b05",
+                "downloads": 249,
+                "filename": "micropython-uasyncio-0.8.1.tar.gz",
+                "packagetype": "sdist",
                 "size": 2484
             }
-        ], 
+        ],
         "0.7": [
             {
-                "has_sig": false, 
-                "upload_time": "2014-10-23T22:02:11", 
-                "comment_text": "", 
-                "python_version": "source", 
-                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.7.tar.gz", 
-                "md5_digest": "81250a0ee6649b5117878d5788ba96d3", 
-                "downloads": 457, 
-                "filename": "micropython-uasyncio-0.7.tar.gz", 
-                "packagetype": "sdist", 
+                "has_sig": false,
+                "upload_time": "2014-10-23T22:02:11",
+                "comment_text": "",
+                "python_version": "source",
+                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.7.tar.gz",
+                "md5_digest": "81250a0ee6649b5117878d5788ba96d3",
+                "downloads": 457,
+                "filename": "micropython-uasyncio-0.7.tar.gz",
+                "packagetype": "sdist",
                 "size": 2277
             }
-        ], 
+        ],
         "0.7.1": [
             {
-                "has_sig": false, 
-                "upload_time": "2014-11-04T00:56:16", 
-                "comment_text": "", 
-                "python_version": "source", 
-                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.7.1.tar.gz", 
-                "md5_digest": "21eda0501142830730cd82e1b0fa1a33", 
-                "downloads": 412, 
-                "filename": "micropython-uasyncio-0.7.1.tar.gz", 
-                "packagetype": "sdist", 
+                "has_sig": false,
+                "upload_time": "2014-11-04T00:56:16",
+                "comment_text": "",
+                "python_version": "source",
+                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.7.1.tar.gz",
+                "md5_digest": "21eda0501142830730cd82e1b0fa1a33",
+                "downloads": 412,
+                "filename": "micropython-uasyncio-0.7.1.tar.gz",
+                "packagetype": "sdist",
                 "size": 2474
             }
-        ], 
+        ],
         "0.6": [
             {
-                "has_sig": false, 
-                "upload_time": "2014-08-27T00:17:45", 
-                "comment_text": "", 
-                "python_version": "source", 
-                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.6.tar.gz", 
-                "md5_digest": "9d0b15108c5ade3a6902c9370c9dacf1", 
-                "downloads": 668, 
-                "filename": "micropython-uasyncio-0.6.tar.gz", 
-                "packagetype": "sdist", 
+                "has_sig": false,
+                "upload_time": "2014-08-27T00:17:45",
+                "comment_text": "",
+                "python_version": "source",
+                "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.6.tar.gz",
+                "md5_digest": "9d0b15108c5ade3a6902c9370c9dacf1",
+                "downloads": 668,
+                "filename": "micropython-uasyncio-0.6.tar.gz",
+                "packagetype": "sdist",
                 "size": 3246
             }
         ]
-    }, 
+    },
     "urls": [
         {
-            "has_sig": false, 
-            "upload_time": "2015-01-04T20:02:03", 
-            "comment_text": "", 
-            "python_version": "source", 
-            "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.8.1.tar.gz", 
-            "md5_digest": "940d2647b8355289d54de543ff710b05", 
-            "downloads": 249, 
-            "filename": "micropython-uasyncio-0.8.1.tar.gz", 
-            "packagetype": "sdist", 
+            "has_sig": false,
+            "upload_time": "2015-01-04T20:02:03",
+            "comment_text": "",
+            "python_version": "source",
+            "url": "https://pypi.python.org/packages/source/m/micropython-uasyncio/micropython-uasyncio-0.8.1.tar.gz",
+            "md5_digest": "940d2647b8355289d54de543ff710b05",
+            "downloads": 249,
+            "filename": "micropython-uasyncio-0.8.1.tar.gz",
+            "packagetype": "sdist",
             "size": 2484
         }
     ]
diff --git a/tests/io/file_seek.py b/tests/io/file_seek.py
index 3990df84090..b9f9593786f 100644
--- a/tests/io/file_seek.py
+++ b/tests/io/file_seek.py
@@ -30,5 +30,5 @@
 try:
     f.seek(1)
 except (OSError, ValueError):
-    # CPy raises ValueError, uPy raises OSError
+    # CPy raises ValueError, MPy raises OSError
     print("OSError or ValueError")
diff --git a/tests/io/file_stdio.py b/tests/io/file_stdio.py
index 6c08f35d787..d714bffd4df 100644
--- a/tests/io/file_stdio.py
+++ b/tests/io/file_stdio.py
@@ -1,7 +1,5 @@
-try:
-    import usys as sys
-except ImportError:
-    import sys
+import sys
 
 print(sys.stdin.fileno())
 print(sys.stdout.fileno())
+print(sys.stderr.fileno())
diff --git a/tests/io/file_stdio2.py b/tests/io/file_stdio2.py
new file mode 100644
index 00000000000..5b8a5a76921
--- /dev/null
+++ b/tests/io/file_stdio2.py
@@ -0,0 +1,65 @@
+# Test sys.std*.buffer objects.
+
+import sys
+
+try:
+    sys.stdout.buffer
+    sys.stdin.buffer
+    sys.stderr.buffer
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+
+# force cpython to flush after every print
+# this is to sequence stdout and stderr
+def print_flush(*args, **kwargs):
+    try:
+        print(*args, **kwargs, flush=True)
+    except TypeError:
+        print(*args, **kwargs)
+
+
+print_flush("==stdin==")
+print_flush(sys.stdin.buffer.fileno())
+
+
+print_flush("==stdout==")
+print_flush(sys.stdout.buffer.fileno())
+n_text = sys.stdout.write("The quick brown fox jumps over the lazy dog\n")
+sys.stdout.flush()
+n_binary = sys.stdout.buffer.write("The quick brown fox jumps over the lazy dog\n".encode("utf-8"))
+sys.stdout.buffer.flush()
+print_flush("n_text:{} n_binary:{}".format(n_text, n_binary))
+
+# temporarily disabling unicode tests until future PR which fixes unicode write character count
+# n_text = sys.stdout.write("🚀")
+# sys.stdout.flush()
+# n_binary = sys.stdout.buffer.write("🚀".encode("utf-8"))
+# sys.stdout.buffer.flush()
+# print_flush("")
+# print_flush("n_text:{} n_binary:{}".format(n_text, n_binary))
+# n_text = sys.stdout.write("1🚀2a3α4b5β6c7γ8d9δ0ぁ1🙐")
+# sys.stdout.flush()
+# n_binary = sys.stdout.buffer.write("1🚀2a3α4b5β6c7γ8d9δ0ぁ1🙐".encode("utf-8"))
+# sys.stdout.buffer.flush()
+# print_flush("")
+# print_flush("n_text:{} n_binary:{}".format(n_text, n_binary))
+
+
+print_flush("==stderr==")
+print_flush(sys.stderr.buffer.fileno())
+n_text = sys.stderr.write("The quick brown fox jumps over the lazy dog\n")
+sys.stderr.flush()
+n_binary = sys.stderr.buffer.write("The quick brown fox jumps over the lazy dog\n".encode("utf-8"))
+sys.stderr.buffer.flush()
+print_flush("n_text:{} n_binary:{}".format(n_text, n_binary))
+
+# temporarily disabling unicode tests until future PR which fixes unicode write character count
+# n_text = sys.stderr.write("🚀")
+# sys.stderr.flush()
+# n_binary = sys.stderr.buffer.write("🚀".encode("utf-8"))
+# sys.stderr.buffer.flush()
+# print_flush("")
+# print_flush("n_text:{} n_binary:{}".format(n_text, n_binary))
+# print_flush("")
diff --git a/tests/io/open_append.py b/tests/io/open_append.py
index 49cdd094b3d..80a9422d815 100644
--- a/tests/io/open_append.py
+++ b/tests/io/open_append.py
@@ -1,7 +1,4 @@
-try:
-    import uos as os
-except ImportError:
-    import os
+import os
 
 if not hasattr(os, "remove"):
     print("SKIP")
diff --git a/tests/io/open_plus.py b/tests/io/open_plus.py
index 3cb2330eed4..5199861a4e3 100644
--- a/tests/io/open_plus.py
+++ b/tests/io/open_plus.py
@@ -1,7 +1,4 @@
-try:
-    import uos as os
-except ImportError:
-    import os
+import os
 
 if not hasattr(os, "remove"):
     print("SKIP")
diff --git a/tests/micropython/builtin_execfile.py b/tests/micropython/builtin_execfile.py
index 8a8ce79f784..4fd4d66d4ee 100644
--- a/tests/micropython/builtin_execfile.py
+++ b/tests/micropython/builtin_execfile.py
@@ -1,23 +1,24 @@
 # Test builtin execfile function using VFS.
 
 try:
-    import uio, uos
+    import io, os, vfs
 
     execfile
-    uio.IOBase
-    uos.mount
+    io.IOBase
 except (ImportError, NameError, AttributeError):
     print("SKIP")
     raise SystemExit
 
 
-class File(uio.IOBase):
+class File(io.IOBase):
     def __init__(self, data):
         self.data = data
         self.off = 0
 
     def ioctl(self, request, arg):
-        return 0
+        if request == 4:  # MP_STREAM_CLOSE
+            return 0
+        return -1
 
     def readinto(self, buf):
         buf[:] = memoryview(self.data)[self.off : self.off + len(buf)]
@@ -44,18 +45,18 @@ def open(self, file, mode):
 
 # First umount any existing mount points the target may have.
 try:
-    uos.umount("/")
+    vfs.umount("/")
 except OSError:
     pass
-for path in uos.listdir("/"):
-    uos.umount("/" + path)
+for path in os.listdir("/"):
+    vfs.umount("/" + path)
 
 # Create and mount the VFS object.
 files = {
     "/test.py": "print(123)",
 }
 fs = Filesystem(files)
-uos.mount(fs, "/test_mnt")
+vfs.mount(fs, "/test_mnt")
 
 # Test execfile with a file that doesn't exist.
 try:
@@ -66,5 +67,32 @@ def open(self, file, mode):
 # Test execfile with a file that does exist.
 execfile("/test_mnt/test.py")
 
+# Test that it only works with string arguments.
+try:
+    execfile(b"aaa")
+except TypeError:
+    print("TypeError")
+
 # Unmount the VFS object.
-uos.umount(fs)
+vfs.umount(fs)
+
+
+class EvilFilesystem:
+    def mount(self, readonly, mkfs):
+        print("mount", readonly, mkfs)
+
+    def umount(self):
+        print("umount")
+
+    def open(self, file, mode):
+        return None
+
+
+fs = EvilFilesystem()
+vfs.mount(fs, "/test_mnt")
+try:
+    execfile("/test_mnt/test.py")
+    print("ExecFile succeeded")
+except OSError:
+    print("OSError")
+vfs.umount(fs)
diff --git a/tests/micropython/builtin_execfile.py.exp b/tests/micropython/builtin_execfile.py.exp
index 1d5d8ee571e..d93dee547b6 100644
--- a/tests/micropython/builtin_execfile.py.exp
+++ b/tests/micropython/builtin_execfile.py.exp
@@ -3,4 +3,8 @@ open /noexist.py rb
 OSError
 open /test.py rb
 123
+TypeError
+umount
+mount False False
+OSError
 umount
diff --git a/tests/micropython/builtin_tstring.py b/tests/micropython/builtin_tstring.py
new file mode 100644
index 00000000000..8de0acfd98b
--- /dev/null
+++ b/tests/micropython/builtin_tstring.py
@@ -0,0 +1,24 @@
+# This tests the built-in __template__ (which is MicroPython specific).
+
+# Test a varying number of arguments.
+print(__template__(()))
+print(__template__((), ()))
+print(__template__((), None, None))
+print(__template__((), None, None, None))
+print(__template__((), None, None, None, None))
+print(__template__((), None, None, None, None, None))
+
+# Test two strings and one interpolation.
+print(__template__(("Hello ", "!"), (42, "x", None, "")))
+
+# Test not enough arguments.
+try:
+    print(__template__())
+except TypeError as er:
+    print(repr(er))
+
+# Test two arguments with second not being a tuple/list.
+try:
+    print(__template__((), None))
+except TypeError as er:
+    print(repr(er))
diff --git a/tests/micropython/builtin_tstring.py.exp b/tests/micropython/builtin_tstring.py.exp
new file mode 100644
index 00000000000..b6b47306124
--- /dev/null
+++ b/tests/micropython/builtin_tstring.py.exp
@@ -0,0 +1,9 @@
+Template(strings=(), interpolations=())
+Template(strings=(), interpolations=())
+Template(strings=(), interpolations=())
+Template(strings=(), interpolations=())
+Template(strings=(), interpolations=(Interpolation(None, None, None, None),))
+Template(strings=(), interpolations=(Interpolation(None, None, None, None),))
+Template(strings=('Hello ', '!'), interpolations=(Interpolation(42, 'x', None, ''),))
+TypeError('function missing 1 required positional arguments',)
+TypeError("object 'NoneType' isn't a tuple or list",)
diff --git a/tests/micropython/const_annotated.py b/tests/micropython/const_annotated.py
new file mode 100644
index 00000000000..4d01389e43a
--- /dev/null
+++ b/tests/micropython/const_annotated.py
@@ -0,0 +1,18 @@
+# Test type annotations in combination with const.
+# This test will only work when MICROPY_COMP_CONST and MICROPY_COMP_CONST_TUPLE are enabled.
+
+from micropython import const
+
+_X0: bool = const(True)
+_X1: int = const(123)
+_X2: str = const("test")
+_X3: tuple = const((1, 2))
+_X4: tuple[bool, int] = const((True, 4))
+_X5: bytes = b"\x01\x02\x03"
+
+print(_X0)
+print(_X1)
+print(_X2)
+print(_X3)
+print(_X4)
+print(_X5)
diff --git a/tests/micropython/const_annotated.py.exp b/tests/micropython/const_annotated.py.exp
new file mode 100644
index 00000000000..65edb58e72e
--- /dev/null
+++ b/tests/micropython/const_annotated.py.exp
@@ -0,0 +1,6 @@
+True
+123
+test
+(1, 2)
+(True, 4)
+b'\x01\x02\x03'
diff --git a/tests/micropython/const_error.py b/tests/micropython/const_error.py
index d35be530a7c..950360e4dc7 100644
--- a/tests/micropython/const_error.py
+++ b/tests/micropython/const_error.py
@@ -18,8 +18,6 @@ def test_syntax(code):
 
 # these operations are not supported within const
 test_syntax("A = const(1 @ 2)")
-test_syntax("A = const(1 / 2)")
-test_syntax("A = const(1 ** -2)")
 test_syntax("A = const(1 << -2)")
 test_syntax("A = const(1 >> -2)")
 test_syntax("A = const(1 % 0)")
diff --git a/tests/micropython/const_error.py.exp b/tests/micropython/const_error.py.exp
index 3edc3efe9c3..bef69eb32ea 100644
--- a/tests/micropython/const_error.py.exp
+++ b/tests/micropython/const_error.py.exp
@@ -5,5 +5,3 @@ SyntaxError
 SyntaxError
 SyntaxError
 SyntaxError
-SyntaxError
-SyntaxError
diff --git a/tests/micropython/const_float.py b/tests/micropython/const_float.py
new file mode 100644
index 00000000000..c3a0df0276b
--- /dev/null
+++ b/tests/micropython/const_float.py
@@ -0,0 +1,23 @@
+# test constant optimisation, with consts that are floats
+
+from micropython import const
+
+# check we can make consts from floats
+F1 = const(2.5)
+F2 = const(-0.3)
+print(type(F1), F1)
+print(type(F2), F2)
+
+# check arithmetic with floats
+F3 = const(F1 + F2)
+F4 = const(F1**2)
+print(F3, F4)
+
+# check int operations with float results
+F5 = const(1 / 2)
+F6 = const(2**-2)
+print(F5, F6)
+
+# note: we also test float expression folding when
+#       we're compiling test cases in tests/float, as
+#       many expressions are resolved at compile time.
diff --git a/tests/micropython/const_float.py.exp b/tests/micropython/const_float.py.exp
new file mode 100644
index 00000000000..17a86a6d936
--- /dev/null
+++ b/tests/micropython/const_float.py.exp
@@ -0,0 +1,4 @@
+ 2.5
+ -0.3
+2.2 6.25
+0.5 0.25
diff --git a/tests/micropython/const_math.py b/tests/micropython/const_math.py
new file mode 100644
index 00000000000..7ee5edc6d32
--- /dev/null
+++ b/tests/micropython/const_math.py
@@ -0,0 +1,18 @@
+# Test expressions based on math module constants
+try:
+    import math
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+from micropython import const
+
+# check that we can make consts from math constants
+# (skip if the target has MICROPY_COMP_MODULE_CONST disabled)
+try:
+    exec("two_pi = const(2.0 * math.pi)")
+except SyntaxError:
+    print("SKIP")
+    raise SystemExit
+
+print(math.cos(two_pi))
diff --git a/tests/micropython/const_math.py.exp b/tests/micropython/const_math.py.exp
new file mode 100644
index 00000000000..d3827e75a5c
--- /dev/null
+++ b/tests/micropython/const_math.py.exp
@@ -0,0 +1 @@
+1.0
diff --git a/tests/micropython/decorator_error.py b/tests/micropython/decorator_error.py
index 94772ac1e5f..b5eae65b292 100644
--- a/tests/micropython/decorator_error.py
+++ b/tests/micropython/decorator_error.py
@@ -1,4 +1,4 @@
-# test syntax errors for uPy-specific decorators
+# test syntax errors for MicroPython-specific decorators
 
 
 def test_syntax(code):
diff --git a/tests/micropython/emg_exc.py b/tests/micropython/emg_exc.py
index b8df94b071a..42b709375ce 100644
--- a/tests/micropython/emg_exc.py
+++ b/tests/micropython/emg_exc.py
@@ -1,10 +1,9 @@
 # test that emergency exceptions work
 
-import micropython
-import usys
+import sys
 
 try:
-    import uio
+    import io, micropython
 except ImportError:
     print("SKIP")
     raise SystemExit
@@ -25,8 +24,8 @@ def f():
     micropython.heap_unlock()
 
     # print the exception
-    buf = uio.StringIO()
-    usys.print_exception(exc, buf)
+    buf = io.StringIO()
+    sys.print_exception(exc, buf)
     for l in buf.getvalue().split("\n"):
         if l.startswith("  File "):
             print(l.split('"')[2])
diff --git a/tests/micropython/emg_exc.py.exp b/tests/micropython/emg_exc.py.exp
index 0d4b80ce2bc..d18012d691a 100644
--- a/tests/micropython/emg_exc.py.exp
+++ b/tests/micropython/emg_exc.py.exp
@@ -1,4 +1,4 @@
 Traceback (most recent call last):
-, line 22, in f
+, line 21, in f
 ValueError: 1
 
diff --git a/tests/micropython/emg_exc.py.native.exp b/tests/micropython/emg_exc.py.native.exp
new file mode 100644
index 00000000000..9677c526a9c
--- /dev/null
+++ b/tests/micropython/emg_exc.py.native.exp
@@ -0,0 +1,2 @@
+ValueError: 1
+
diff --git a/tests/micropython/extreme_exc.py b/tests/micropython/extreme_exc.py
index ad819e408fd..17323b566d3 100644
--- a/tests/micropython/extreme_exc.py
+++ b/tests/micropython/extreme_exc.py
@@ -1,6 +1,10 @@
 # test some extreme cases of allocating exceptions and tracebacks
 
-import micropython
+try:
+    import micropython
+except ImportError:
+    print("SKIP")
+    raise SystemExit
 
 # Check for stackless build, which can't call functions without
 # allocating a frame on the heap.
diff --git a/tests/micropython/heap_lock.py b/tests/micropython/heap_lock.py
index f2892a6dc58..209a3143461 100644
--- a/tests/micropython/heap_lock.py
+++ b/tests/micropython/heap_lock.py
@@ -1,6 +1,10 @@
 # check that heap_lock/heap_unlock work as expected
 
-import micropython
+try:
+    import micropython
+except ImportError:
+    print("SKIP")
+    raise SystemExit
 
 l = []
 l2 = list(range(100))
diff --git a/tests/micropython/heap_locked.py b/tests/micropython/heap_locked.py
index d9e5b5d4090..d9d99493dd6 100644
--- a/tests/micropython/heap_locked.py
+++ b/tests/micropython/heap_locked.py
@@ -1,8 +1,10 @@
 # test micropython.heap_locked()
 
-import micropython
+try:
+    import micropython
 
-if not hasattr(micropython, "heap_locked"):
+    micropython.heap_locked
+except (AttributeError, ImportError):
     print("SKIP")
     raise SystemExit
 
diff --git a/tests/micropython/heapalloc.py b/tests/micropython/heapalloc.py
index e19f8d0255d..010bf878a07 100644
--- a/tests/micropython/heapalloc.py
+++ b/tests/micropython/heapalloc.py
@@ -1,6 +1,10 @@
 # check that we can do certain things without allocating heap memory
 
-import micropython
+try:
+    import micropython
+except ImportError:
+    print("SKIP")
+    raise SystemExit
 
 # Check for stackless build, which can't call functions without
 # allocating a frame on heap.
diff --git a/tests/micropython/heapalloc_bytesio.py b/tests/micropython/heapalloc_bytesio.py
index 4aae2abf063..0ac8c92555a 100644
--- a/tests/micropython/heapalloc_bytesio.py
+++ b/tests/micropython/heapalloc_bytesio.py
@@ -1,5 +1,5 @@
 try:
-    import uio
+    import io
 except ImportError:
     print("SKIP")
     raise SystemExit
@@ -7,7 +7,7 @@
 import micropython
 
 data = b"1234" * 16
-buf = uio.BytesIO(64)
+buf = io.BytesIO(64)
 
 micropython.heap_lock()
 
diff --git a/tests/micropython/heapalloc_bytesio2.py b/tests/micropython/heapalloc_bytesio2.py
index 3b9f141270d..05c384a516d 100644
--- a/tests/micropython/heapalloc_bytesio2.py
+++ b/tests/micropython/heapalloc_bytesio2.py
@@ -1,7 +1,7 @@
 # Creating BytesIO from immutable object should not immediately
 # copy its content.
 try:
-    import uio
+    import io
     import micropython
 
     micropython.mem_total
@@ -14,7 +14,7 @@
 
 before = micropython.mem_total()
 
-buf = uio.BytesIO(data)
+buf = io.BytesIO(data)
 
 after = micropython.mem_total()
 
diff --git a/tests/micropython/heapalloc_exc_compressed.py b/tests/micropython/heapalloc_exc_compressed.py
index aa071d641c2..96fe3ca4f7e 100644
--- a/tests/micropython/heapalloc_exc_compressed.py
+++ b/tests/micropython/heapalloc_exc_compressed.py
@@ -1,4 +1,10 @@
-import micropython
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 # Tests both code paths for built-in exception raising.
 # mp_obj_new_exception_msg_varg (exception requires decompression at raise-time to format)
diff --git a/tests/micropython/heapalloc_exc_compressed_emg_exc.py b/tests/micropython/heapalloc_exc_compressed_emg_exc.py
index 48ce9dd69e6..31d937b8f93 100644
--- a/tests/micropython/heapalloc_exc_compressed_emg_exc.py
+++ b/tests/micropython/heapalloc_exc_compressed_emg_exc.py
@@ -1,4 +1,10 @@
-import micropython
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 # Does the full test from heapalloc_exc_compressed.py but while the heap is
 # locked (this can only work when the emergency exception buf is enabled).
diff --git a/tests/micropython/heapalloc_exc_raise.py b/tests/micropython/heapalloc_exc_raise.py
index 99810e00750..917ccf42f1e 100644
--- a/tests/micropython/heapalloc_exc_raise.py
+++ b/tests/micropython/heapalloc_exc_raise.py
@@ -1,6 +1,12 @@
 # Test that we can raise and catch (preallocated) exception
 # without memory allocation.
-import micropython
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 e = ValueError("error")
 
diff --git a/tests/micropython/heapalloc_fail_bytearray.py b/tests/micropython/heapalloc_fail_bytearray.py
index 1bf7ddd600b..9ee73d1c583 100644
--- a/tests/micropython/heapalloc_fail_bytearray.py
+++ b/tests/micropython/heapalloc_fail_bytearray.py
@@ -1,6 +1,12 @@
 # test handling of failed heap allocation with bytearray
 
-import micropython
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 
 class GetSlice:
diff --git a/tests/micropython/heapalloc_fail_dict.py b/tests/micropython/heapalloc_fail_dict.py
index ce2d158bd09..04c79183578 100644
--- a/tests/micropython/heapalloc_fail_dict.py
+++ b/tests/micropython/heapalloc_fail_dict.py
@@ -1,6 +1,12 @@
 # test handling of failed heap allocation with dict
 
-import micropython
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 # create dict
 x = 1
diff --git a/tests/micropython/heapalloc_fail_list.py b/tests/micropython/heapalloc_fail_list.py
index 9a2e9a555f3..7afb6dc1019 100644
--- a/tests/micropython/heapalloc_fail_list.py
+++ b/tests/micropython/heapalloc_fail_list.py
@@ -1,6 +1,12 @@
 # test handling of failed heap allocation with list
 
-import micropython
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 
 class GetSlice:
diff --git a/tests/micropython/heapalloc_fail_memoryview.py b/tests/micropython/heapalloc_fail_memoryview.py
index da2d1abffa6..17e3e126247 100644
--- a/tests/micropython/heapalloc_fail_memoryview.py
+++ b/tests/micropython/heapalloc_fail_memoryview.py
@@ -1,6 +1,12 @@
 # test handling of failed heap allocation with memoryview
 
-import micropython
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 
 class GetSlice:
diff --git a/tests/micropython/heapalloc_fail_set.py b/tests/micropython/heapalloc_fail_set.py
index 3c347660ad7..0c4d85eef62 100644
--- a/tests/micropython/heapalloc_fail_set.py
+++ b/tests/micropython/heapalloc_fail_set.py
@@ -1,6 +1,12 @@
 # test handling of failed heap allocation with set
 
-import micropython
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 # create set
 x = 1
diff --git a/tests/micropython/heapalloc_fail_tstring.py b/tests/micropython/heapalloc_fail_tstring.py
new file mode 100644
index 00000000000..94687d57bfb
--- /dev/null
+++ b/tests/micropython/heapalloc_fail_tstring.py
@@ -0,0 +1,475 @@
+# Test template string (t-string) operations with heap allocation failure
+
+import micropython
+from string.templatelib import Template, Interpolation
+
+i1 = Interpolation(1, "1")
+i2 = Interpolation(2, "2")
+
+micropython.heap_lock()
+try:
+    args = []
+    for i in range(9):
+        args.append("x" * 100)
+        args.append(Interpolation(i, "x" + str(i)))
+    args.append("x" * 100)
+    Template(*args)
+    print("FAIL: Template creation")
+except MemoryError:
+    print("OK: Template creation")
+micropython.heap_unlock()
+
+# Multiple string concatenation
+micropython.heap_lock()
+try:
+    Template("first", "second", "third", "fourth")
+    print("FAIL: Multi string concat")
+except MemoryError:
+    print("OK: Multi string concat")
+micropython.heap_unlock()
+
+# Mixed constructor
+micropython.heap_lock()
+try:
+    Template("a", i1, "b", i2, "c", "d", "e")
+    print("FAIL: Mixed constructor")
+except MemoryError:
+    print("OK: Mixed constructor")
+micropython.heap_unlock()
+
+# Template.__str__()
+t = t"Hello {42} world {99}"
+micropython.heap_lock()
+try:
+    str(t)
+    print("FAIL: Template.__str__()")
+except MemoryError:
+    print("OK: Template.__str__()")
+micropython.heap_unlock()
+
+# Template.values property
+vals = list(range(10))
+t_many = t"{vals[0]}{vals[1]}{vals[2]}{vals[3]}{vals[4]}"
+micropython.heap_lock()
+try:
+    t_many.values
+    print("FAIL: Template.values")
+except MemoryError:
+    print("OK: Template.values")
+micropython.heap_unlock()
+
+n = 20
+args_large = []
+for i in range(n):
+    args_large.append("")
+    args_large.append(Interpolation(i, "x" + str(i)))
+args_large.append("")
+t_large = Template(*args_large)
+micropython.heap_lock()
+try:
+    t_large.values
+    print("FAIL: Large values array")
+except MemoryError:
+    print("OK: Large values array")
+micropython.heap_unlock()
+
+# Template concatenation
+t1 = t"Hello"
+t2 = t"World"
+micropython.heap_lock()
+try:
+    t1 + t2
+    print("FAIL: Template concatenation")
+except MemoryError:
+    print("OK: Template concatenation")
+micropython.heap_unlock()
+
+# Template iterator
+t_iter = t"a{1}b{2}c"
+micropython.heap_lock()
+try:
+    iter(t_iter)
+    print("FAIL: Template iterator")
+except MemoryError:
+    print("OK: Template iterator")
+micropython.heap_unlock()
+
+# Iterator next
+t_iter2 = t"a{1}b{2}c{3}d"
+it = iter(t_iter2)
+micropython.heap_lock()
+try:
+    list(it)
+    print("FAIL: Iterator next")
+except MemoryError:
+    print("OK: Iterator next")
+micropython.heap_unlock()
+
+# __template__ builtin
+strings2 = ("test",) * 5
+interps2 = ((42, "x", None, ""),) * 4
+micropython.heap_lock()
+try:
+    __template__(strings2, interps2)
+    print("FAIL: __template__ builtin")
+except MemoryError:
+    print("OK: __template__ builtin")
+micropython.heap_unlock()
+
+# Format spec interpolation
+width = 10
+t_fmt = t"{42:{width}d}"
+micropython.heap_lock()
+try:
+    str(t_fmt)
+    print("FAIL: Format spec interpolation")
+except MemoryError:
+    print("OK: Format spec interpolation")
+micropython.heap_unlock()
+
+# Debug format
+x = 42
+t_debug = t"{x=}"
+micropython.heap_lock()
+try:
+    str(t_debug)
+    print("FAIL: Debug format")
+except MemoryError:
+    print("OK: Debug format")
+micropython.heap_unlock()
+
+# Conversion with format
+obj = "test"
+t_conv = t"{obj!r:>10}"
+micropython.heap_lock()
+try:
+    str(t_conv)
+    print("FAIL: Conversion + format")
+except MemoryError:
+    print("OK: Conversion + format")
+micropython.heap_unlock()
+
+# String conversion (s)
+t_s = t"{'test'!s}"
+micropython.heap_lock()
+try:
+    str(t_s)
+    print("FAIL: s conversion")
+except MemoryError:
+    print("OK: s conversion")
+micropython.heap_unlock()
+
+# ASCII conversion (a)
+# t_a = t"{'test'!a}"
+# micropython.heap_lock()
+# try:
+#     str(t_a)
+#     print("FAIL: a conversion")
+# except MemoryError:
+#     print("OK: a conversion")
+# micropython.heap_unlock()
+
+# Complex format spec
+fill = "*"
+align = ">"
+width = 10
+precision = 2
+t_complex = t"{3.14159:{fill}{align}{width}.{precision}f}"
+micropython.heap_lock()
+try:
+    str(t_complex)
+    print("FAIL: Complex format spec")
+except MemoryError:
+    print("OK: Complex format spec")
+micropython.heap_unlock()
+
+# Simple expression
+x = 42
+t_simple = t"{x}"
+micropython.heap_lock()
+try:
+    str(t_simple)
+    print("FAIL: Simple expression")
+except MemoryError:
+    print("OK: Simple expression")
+micropython.heap_unlock()
+
+# Interpolation creation
+micropython.heap_lock()
+try:
+    Interpolation("value", "expr", "r", ".2f")
+    print("FAIL: Interpolation creation")
+except MemoryError:
+    print("OK: Interpolation creation")
+micropython.heap_unlock()
+
+# Template repr
+t = t"test"
+micropython.heap_lock()
+try:
+    repr(t)
+    print("FAIL: Template repr")
+except MemoryError:
+    print("OK: Template repr")
+micropython.heap_unlock()
+
+# Interpolation repr
+i = Interpolation(42, "x")
+micropython.heap_lock()
+try:
+    repr(i)
+    print("FAIL: Interpolation repr")
+except MemoryError:
+    print("OK: Interpolation repr")
+micropython.heap_unlock()
+
+def exhaust_heap(limit):
+    allocations = []
+    try:
+        for _ in range(limit):
+            allocations.append("x" * 1024)
+    except MemoryError:
+        pass
+    return allocations
+
+def test_many_interpolations_heap():
+    # Pre-create variables before exhausting heap
+    x1, x2, x3, x4, x5, x6, x7, x8, x9 = 1, 2, 3, 4, 5, 6, 7, 8, 9
+    micropython.heap_lock()
+    try:
+        t = t"{x1}{x2}{x3}{x4}{x5}{x6}{x7}{x8}{x9}"
+        print("FAIL: Many interpolations heap test")
+    except MemoryError:
+        print("OK: Many interpolations heap test")
+    micropython.heap_unlock()
+
+def test_template_str_heap():
+    t = t"x{1}x{2}x{3}x{4}x"
+    micropython.heap_lock()
+    try:
+        s = str(t)
+        print("FAIL: Template str heap test")
+    except MemoryError:
+        print("OK: Template str heap test")
+    micropython.heap_unlock()
+
+def test_template_iter_heap():
+    t = t"a{1}b{2}c"
+    micropython.heap_lock()
+    try:
+        parts = list(iter(t))
+        print("FAIL: Template iter heap test")
+    except MemoryError:
+        print("OK: Template iter heap test")
+    micropython.heap_unlock()
+
+def test_template_concat_heap():
+    t1 = t"first"
+    t2 = t"second"
+    micropython.heap_lock()
+    try:
+        t3 = t1 + t2
+        print("FAIL: Template concat heap test")
+    except MemoryError:
+        print("OK: Template concat heap test")
+    micropython.heap_unlock()
+
+def test_format_spec_heap():
+    width = 10
+    t = t"{42:{width}d}"
+    micropython.heap_lock()
+    try:
+        s = str(t)
+        print("FAIL: Format spec heap test")
+    except MemoryError:
+        print("OK: Format spec heap test")
+    micropython.heap_unlock()
+
+def test_debug_format_heap():
+    value = 42
+    t = t"{value}"
+    micropython.heap_lock()
+    try:
+        s = str(t)
+        print("FAIL: Debug format heap test")
+    except MemoryError:
+        print("OK: Debug format heap test")
+    micropython.heap_unlock()
+
+print("\n=== Heap allocation failure tests ===")
+test_many_interpolations_heap()
+test_template_str_heap()
+test_template_iter_heap()
+test_template_concat_heap()
+test_format_spec_heap()
+test_debug_format_heap()
+
+# Additional tests from coverage.c
+print("\n=== Coverage.c heap tests ===")
+
+# Test creating interpolation with heap locked (from coverage.c)
+micropython.heap_lock()
+try:
+    i = Interpolation(42, "x", None, None)
+    print("FAIL: Interpolation with heap locked")
+except MemoryError:
+    print("OK: Interpolation creation with heap locked")
+micropython.heap_unlock()
+
+# Test parsing expression with heap locked (from coverage.c)
+micropython.heap_lock()
+try:
+    t = eval('t"{x + 1}"')
+    print("FAIL: Parse with heap locked")
+except Exception as e:
+    if type(e).__name__ == "MemoryError":
+        print("OK: Parse with heap locked")
+    else:
+        print(f"Parse with heap locked: {type(e).__name__}")
+micropython.heap_unlock()
+
+# Test lexer allocation failure for t-string expression parsing
+print("\n=== Lexer/Parser allocation tests ===")
+
+# Wrap all tests in a try-catch to handle any unexpected errors
+try:
+    # Test 1: Empty expression (tests tstring_expr_parser.c empty check)
+    try:
+        micropython.heap_lock()
+        try:
+            # This tests the empty expression path
+            exec("t'{}'")
+            print("FAIL: Empty expression with heap locked")
+        except Exception as e:
+            print(f"OK: Empty expression - {type(e).__name__}")
+        finally:
+            micropython.heap_unlock()
+    except:
+        pass
+
+    # Test 2: Whitespace-only expression
+    try:
+        micropython.heap_lock()
+        try:
+            exec("t'{   }'")
+            print("FAIL: Whitespace expression with heap locked")
+        except Exception as e:
+            print(f"OK: Whitespace expression - {type(e).__name__}")
+        finally:
+            micropython.heap_unlock()
+    except:
+        pass
+
+    # Test 3: Very complex expression to stress parser allocation
+    # Pre-create variables
+    x, y, z = 1, 2, 3
+    try:
+        micropython.heap_lock()
+        try:
+            # Complex nested expression that requires many parse nodes
+            result = t'{[x*y for x in range(10) for y in range(10) if x+y > 5]}'
+            print("FAIL: Complex expression with heap locked")
+        except MemoryError:
+            print("OK: Complex expression parser allocation")
+        except Exception as e:
+            print(f"Complex expression: {type(e).__name__}")
+        finally:
+            micropython.heap_unlock()
+    except:
+        pass
+
+    # Test 4: Expression parser lexer failure (simulates NULL lexer)
+    # This happens when memory is exhausted during lexer creation
+    try:
+        micropython.heap_lock()
+        try:
+            # Try to create t-string with expression during low memory
+            long_expr = "x" * 100  # Use a moderately long expression
+            exec(f"{long_expr} = 42; result = t'{{{long_expr}}}'")
+            print("FAIL: Lexer creation with heap locked")
+        except MemoryError:
+            print("OK: Lexer creation failure")
+        except Exception as e:
+            print(f"Lexer creation: {type(e).__name__}")
+        finally:
+            micropython.heap_unlock()
+    except:
+        pass
+
+    # Test 5: Format spec and conversion parsing under memory pressure
+    val = 42
+    try:
+        micropython.heap_lock()
+        try:
+            # This tests format spec node creation
+            result = t'{val!r:>10.2f}'
+            print("FAIL: Format spec with heap locked")
+        except MemoryError:
+            print("OK: Format spec allocation")
+        except Exception as e:
+            print(f"Format spec: {type(e).__name__}")
+        finally:
+            micropython.heap_unlock()
+    except:
+        pass
+
+    # Test 6: Debug format with memory pressure
+    x = 100
+    try:
+        micropython.heap_lock()
+        try:
+            result = t'{x=:.2f}'
+            print("FAIL: Debug format with heap locked")
+        except MemoryError:
+            print("OK: Debug format allocation")
+        except Exception as e:
+            print(f"Debug format: {type(e).__name__}")
+        finally:
+            micropython.heap_unlock()
+    except:
+        pass
+
+    # Test 7: Multiple interpolations causing allocation failure
+    vals = list(range(20))
+    try:
+        micropython.heap_lock()
+        try:
+            # Many interpolations to stress allocation
+            result = t'{vals[0]}{vals[1]}{vals[2]}{vals[3]}{vals[4]}{vals[5]}{vals[6]}{vals[7]}{vals[8]}{vals[9]}'
+            print("FAIL: Many interpolations with heap locked")
+        except MemoryError:
+            print("OK: Many interpolations allocation")
+        except Exception as e:
+            print(f"Many interpolations: {type(e).__name__}")
+        finally:
+            micropython.heap_unlock()
+    except:
+        pass
+
+    # Test 8: Template string size limit (tests overflow check)
+    # Note: We can't directly test integer overflow, but we can test large templates
+    print("Template size limit: Tested via exec in coverage tests")
+
+    # Test 9: Compile-time parsing under allocation failure
+    try:
+        micropython.heap_lock()
+        try:
+            compile("t'{x}'", "", "exec")
+            print("FAIL: Compile-time parsing under heap lock")
+        except MemoryError:
+            print("OK: Compile-time parsing under heap lock")
+        finally:
+            micropython.heap_unlock()
+    except:
+        pass
+
+except Exception as e:
+    # Catch any unexpected errors from the tests
+    print(f"\nTest error: {type(e).__name__}: {e}")
+    # Ensure heap is unlocked
+    try:
+        micropython.heap_unlock()
+    except:
+        pass
+
+print("\n=== Tests completed ===")
diff --git a/tests/micropython/heapalloc_fail_tstring.py.exp b/tests/micropython/heapalloc_fail_tstring.py.exp
new file mode 100644
index 00000000000..3bc90ef418a
--- /dev/null
+++ b/tests/micropython/heapalloc_fail_tstring.py.exp
@@ -0,0 +1,42 @@
+OK: Template creation
+OK: Multi string concat
+OK: Mixed constructor
+OK: Template.__str__()
+OK: Template.values
+OK: Large values array
+OK: Template concatenation
+OK: Template iterator
+OK: Iterator next
+OK: __template__ builtin
+OK: Format spec interpolation
+OK: Debug format
+OK: Conversion + format
+OK: s conversion
+OK: Complex format spec
+OK: Simple expression
+OK: Interpolation creation
+OK: Template repr
+OK: Interpolation repr
+
+=== Heap allocation failure tests ===
+OK: Many interpolations heap test
+OK: Template str heap test
+OK: Template iter heap test
+OK: Template concat heap test
+OK: Format spec heap test
+OK: Debug format heap test
+
+=== Coverage.c heap tests ===
+OK: Interpolation creation with heap locked
+OK: Parse with heap locked
+
+=== Lexer/Parser allocation tests ===
+OK: Complex expression parser allocation
+OK: Lexer creation failure
+OK: Format spec allocation
+OK: Debug format allocation
+OK: Many interpolations allocation
+Template size limit: Tested via exec in coverage tests
+OK: Compile-time parsing under heap lock
+
+=== Tests completed ===
diff --git a/tests/micropython/heapalloc_fail_tuple.py b/tests/micropython/heapalloc_fail_tuple.py
index de79385e3e3..11718a8107b 100644
--- a/tests/micropython/heapalloc_fail_tuple.py
+++ b/tests/micropython/heapalloc_fail_tuple.py
@@ -1,6 +1,12 @@
 # test handling of failed heap allocation with tuple
 
-import micropython
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 # create tuple
 x = 1
diff --git a/tests/micropython/heapalloc_inst_call.py b/tests/micropython/heapalloc_inst_call.py
index 14d8826bf06..f78aa3cf877 100644
--- a/tests/micropython/heapalloc_inst_call.py
+++ b/tests/micropython/heapalloc_inst_call.py
@@ -1,6 +1,13 @@
 # Test that calling clazz.__call__() with up to at least 3 arguments
 # doesn't require heap allocation.
-import micropython
+
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 
 class Foo0:
diff --git a/tests/micropython/heapalloc_int_from_bytes.py b/tests/micropython/heapalloc_int_from_bytes.py
index 5fe50443ae3..3310ea95d14 100644
--- a/tests/micropython/heapalloc_int_from_bytes.py
+++ b/tests/micropython/heapalloc_int_from_bytes.py
@@ -1,6 +1,13 @@
 # Test that int.from_bytes() for small number of bytes generates
 # small int.
-import micropython
+
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 micropython.heap_lock()
 print(int.from_bytes(b"1", "little"))
diff --git a/tests/micropython/heapalloc_iter.py b/tests/micropython/heapalloc_iter.py
index 18f5322ee15..bd1ba4db79f 100644
--- a/tests/micropython/heapalloc_iter.py
+++ b/tests/micropython/heapalloc_iter.py
@@ -5,13 +5,10 @@
     print("SKIP")
     raise SystemExit
 try:
-    import uarray as array
+    import array
 except ImportError:
-    try:
-        import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
+    print("SKIP")
+    raise SystemExit
 
 try:
     from micropython import heap_lock, heap_unlock
diff --git a/tests/micropython/heapalloc_slice.py b/tests/micropython/heapalloc_slice.py
new file mode 100644
index 00000000000..62d96595c71
--- /dev/null
+++ b/tests/micropython/heapalloc_slice.py
@@ -0,0 +1,18 @@
+# slice operations that don't require allocation
+try:
+    from micropython import heap_lock, heap_unlock
+except (ImportError, AttributeError):
+    heap_lock = heap_unlock = lambda: 0
+
+b = bytearray(range(10))
+
+m = memoryview(b)
+
+heap_lock()
+
+b[3:5] = b"aa"
+m[5:7] = b"bb"
+
+heap_unlock()
+
+print(b)
diff --git a/tests/micropython/heapalloc_str.py b/tests/micropython/heapalloc_str.py
index 39aa56ccd78..6372df5d37b 100644
--- a/tests/micropython/heapalloc_str.py
+++ b/tests/micropython/heapalloc_str.py
@@ -1,5 +1,12 @@
 # String operations which don't require allocation
-import micropython
+
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 micropython.heap_lock()
 
diff --git a/tests/micropython/heapalloc_super.py b/tests/micropython/heapalloc_super.py
index 51afae3d83d..6839eee330a 100644
--- a/tests/micropython/heapalloc_super.py
+++ b/tests/micropython/heapalloc_super.py
@@ -1,5 +1,12 @@
 # test super() operations which don't require allocation
-import micropython
+
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 # Check for stackless build, which can't call functions without
 # allocating a frame on heap.
diff --git a/tests/micropython/heapalloc_traceback.py b/tests/micropython/heapalloc_traceback.py
index 09a2ad2c14d..89fa37a48cc 100644
--- a/tests/micropython/heapalloc_traceback.py
+++ b/tests/micropython/heapalloc_traceback.py
@@ -1,10 +1,9 @@
 # test that we can generate a traceback without allocating
 
-import micropython
-import usys
+import sys
 
 try:
-    import uio
+    import io, micropython
 except ImportError:
     print("SKIP")
     raise SystemExit
@@ -31,13 +30,13 @@ def test():
 # call test() with heap allocation disabled
 test()
 
-# print the exception that was raised
-buf = uio.StringIO()
-usys.print_exception(global_exc, buf)
+# print the exception that was raised. Use repr() to make the whitespace visible.
+buf = io.StringIO()
+sys.print_exception(global_exc, buf)
 for l in buf.getvalue().split("\n"):
-    # uPy on pyboard prints  as file, so remove filename.
+    # MicroPython on pyboard prints  as file, so remove filename.
     if l.startswith("  File "):
         l = l.split('"')
-        print(l[0], l[2])
+        print(repr(l[0]), repr(l[2]))
     else:
-        print(l)
+        print(repr(l))
diff --git a/tests/micropython/heapalloc_traceback.py.exp b/tests/micropython/heapalloc_traceback.py.exp
index 71929db93d5..fa30fa22080 100644
--- a/tests/micropython/heapalloc_traceback.py.exp
+++ b/tests/micropython/heapalloc_traceback.py.exp
@@ -1,5 +1,5 @@
 StopIteration
-Traceback (most recent call last):
-  File  , line 25, in test
-StopIteration: 
-
+'Traceback (most recent call last):'
+'  File ' ', line 24, in test'
+'StopIteration: '
+''
diff --git a/tests/micropython/heapalloc_traceback.py.native.exp b/tests/micropython/heapalloc_traceback.py.native.exp
new file mode 100644
index 00000000000..851eb5c7806
--- /dev/null
+++ b/tests/micropython/heapalloc_traceback.py.native.exp
@@ -0,0 +1,3 @@
+StopIteration
+'StopIteration: '
+''
diff --git a/tests/micropython/heapalloc_yield_from.py b/tests/micropython/heapalloc_yield_from.py
index 95071718908..9d4f8c6940f 100644
--- a/tests/micropython/heapalloc_yield_from.py
+++ b/tests/micropython/heapalloc_yield_from.py
@@ -1,6 +1,12 @@
 # Check that yield-from can work without heap allocation
 
-import micropython
+try:
+    import micropython
+
+    micropython.heap_lock
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
 
 
 # Yielding from a function generator
diff --git a/tests/micropython/import_mpy_invalid.py b/tests/micropython/import_mpy_invalid.py
index b02312a7a8d..5d7df4fb19c 100644
--- a/tests/micropython/import_mpy_invalid.py
+++ b/tests/micropython/import_mpy_invalid.py
@@ -1,16 +1,16 @@
 # test importing of invalid .mpy files
 
 try:
-    import usys, uio, uos
+    import sys, io, vfs
 
-    uio.IOBase
-    uos.mount
+    sys.implementation._mpy
+    io.IOBase
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
 
-class UserFile(uio.IOBase):
+class UserFile(io.IOBase):
     def __init__(self, data):
         self.data = memoryview(data)
         self.pos = 0
@@ -22,7 +22,9 @@ def readinto(self, buf):
         return n
 
     def ioctl(self, req, arg):
-        return 0
+        if req == 4:  # MP_STREAM_CLOSE
+            return 0
+        return -1
 
 
 class UserFS:
@@ -52,8 +54,8 @@ def open(self, path, mode):
 }
 
 # create and mount a user filesystem
-uos.mount(UserFS(user_files), "/userfs")
-usys.path.append("/userfs")
+vfs.mount(UserFS(user_files), "/userfs")
+sys.path.append("/userfs")
 
 # import .mpy files from the user filesystem
 for i in range(len(user_files)):
@@ -61,8 +63,8 @@ def open(self, path, mode):
     try:
         __import__(mod)
     except ValueError as er:
-        print(mod, "ValueError", er)
+        print(mod, "ValueError", str(er) or "incompatible .mpy file")
 
 # unmount and undo path addition
-uos.umount("/userfs")
-usys.path.pop()
+vfs.umount("/userfs")
+sys.path.pop()
diff --git a/tests/micropython/import_mpy_native.py b/tests/micropython/import_mpy_native.py
index 73e20694cc2..0d47a20fc32 100644
--- a/tests/micropython/import_mpy_native.py
+++ b/tests/micropython/import_mpy_native.py
@@ -1,23 +1,22 @@
 # test importing of .mpy files with native code
 
 try:
-    import usys, uio, uos
+    import sys, io, vfs
 
-    usys.implementation._mpy
-    uio.IOBase
-    uos.mount
+    sys.implementation._mpy
+    io.IOBase
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
-mpy_arch = usys.implementation._mpy >> 8
+mpy_arch = sys.implementation._mpy >> 8
 if mpy_arch >> 2 == 0:
     # This system does not support .mpy files containing native code
     print("SKIP")
     raise SystemExit
 
 
-class UserFile(uio.IOBase):
+class UserFile(io.IOBase):
     def __init__(self, data):
         self.data = memoryview(data)
         self.pos = 0
@@ -29,7 +28,9 @@ def readinto(self, buf):
         return n
 
     def ioctl(self, req, arg):
-        return 0
+        if req == 4:  # MP_STREAM_CLOSE
+            return 0
+        return -1
 
 
 class UserFS:
@@ -52,11 +53,12 @@ def open(self, path, mode):
 
 
 # these are the test .mpy files
-valid_header = bytes([77, 6, mpy_arch, 31])
+small_int_bits = 30
+valid_header = bytes([77, 6, (mpy_arch & 0x3F), small_int_bits])
 # fmt: off
 user_files = {
     # bad architecture (mpy_arch needed for sub-version)
-    '/mod0.mpy': bytes([77, 6, 0xfc | mpy_arch, 31]),
+    '/mod0.mpy': bytes([77, 6, 0xfc | (mpy_arch & 3), small_int_bits]),
 
     # test loading of viper and asm
     '/mod1.mpy': valid_header + (
@@ -110,8 +112,8 @@ def open(self, path, mode):
 # fmt: on
 
 # create and mount a user filesystem
-uos.mount(UserFS(user_files), "/userfs")
-usys.path.append("/userfs")
+vfs.mount(UserFS(user_files), "/userfs")
+sys.path.append("/userfs")
 
 # import .mpy files from the user filesystem
 for i in range(len(user_files)):
@@ -120,8 +122,8 @@ def open(self, path, mode):
         __import__(mod)
         print(mod, "OK")
     except ValueError as er:
-        print(mod, "ValueError", er)
+        print(mod, "ValueError", str(er) or "incompatible .mpy arch")
 
 # unmount and undo path addition
-uos.umount("/userfs")
-usys.path.pop()
+vfs.umount("/userfs")
+sys.path.pop()
diff --git a/tests/micropython/import_mpy_native_gc.py b/tests/micropython/import_mpy_native_gc.py
index e18720fb314..bfc4e1c107d 100644
--- a/tests/micropython/import_mpy_native_gc.py
+++ b/tests/micropython/import_mpy_native_gc.py
@@ -1,17 +1,16 @@
-# Test that native code loaded from a .mpy file is retained after a GC.
+# Test that native text/BSS/rodata loaded from a .mpy file is retained after a GC.
 
 try:
-    import gc, sys, uio, uos
+    import gc, sys, io, vfs
 
     sys.implementation._mpy
-    uio.IOBase
-    uos.mount
+    io.IOBase
 except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
 
-class UserFile(uio.IOBase):
+class UserFile(io.IOBase):
     def __init__(self, data):
         self.data = memoryview(data)
         self.pos = 0
@@ -23,7 +22,9 @@ def readinto(self, buf):
         return n
 
     def ioctl(self, req, arg):
-        return 0
+        if req == 4:  # MP_STREAM_CLOSE
+            return 0
+        return -1
 
 
 class UserFS:
@@ -45,13 +46,22 @@ def open(self, path, mode):
         return UserFile(self.files[path])
 
 
-# Pre-compiled examples/natmod/features0 example for various architectures, keyed
+# Pre-compiled import_mpy_native_gc_module example for various architectures, keyed
 # by the required value of sys.implementation._mpy (without sub-version).
+# To rebuild:
+#   $ cd import_mpy_native_gc_module
+#   $ make clean
+#   $ make ARCH=x64 # or ARCH=armv6m or ARCH=xtensawin
+# Then copy the bytes object printed on the last line.
 features0_file_contents = {
     # -march=x64
-    0x806: b'M\x06\x09\x1f\x02\x004build/features0.native.mpy\x00\x12factorial\x00\x8a\x02\xe9/\x00\x00\x00SH\x8b\x1d\x83\x00\x00\x00\xbe\x02\x00\x00\x00\xffS\x18\xbf\x01\x00\x00\x00H\x85\xc0u\x0cH\x8bC \xbe\x02\x00\x00\x00[\xff\xe0H\x0f\xaf\xf8H\xff\xc8\xeb\xe6ATUSH\x8b\x1dQ\x00\x00\x00H\x8bG\x08L\x8bc(H\x8bx\x08A\xff\xd4H\x8d5+\x00\x00\x00H\x89\xc5H\x8b\x059\x00\x00\x00\x0f\xb7x\x02\xffShH\x89\xefA\xff\xd4H\x8b\x03[]A\\\xc3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x11$\r&\xa3 \x01"\xff',
+    0x806: b"M\x06\x0b\x1f\x03\x002build/test_x64.native.mpy\x00\x08add1\x00\x0cunused\x00\x91B\xe9I\x00\x00\x00H\x8b\x05\xf4\x00\x00\x00H\x8b\x00\xc3H\x8b\x05\xf9\x00\x00\x00\xbe\x02\x00\x00\x00\x8b8H\x8b\x05\xdb\x00\x00\x00H\x8b@ \xff\xe0H\x8b\x05\xce\x00\x00\x00S\xbe\x02\x00\x00\x00H\x8bX \xffP\x18\xbe\x02\x00\x00\x00H\x8dx\x01H\x89\xd8[\xff\xe0AVAUATUSH\x8b\x1d\xa3\x00\x00\x00H\x8bG\x08L\x8bk(H\x8bx\x08A\xff\xd5L\x8b5\x95\x00\x00\x00L\x8bchH\x8d5r\x00\x00\x00H\x89\xc5H\x8b\x05\x88\x00\x00\x00A\x0f\xb7~\x04\xc7\x00@\xe2\x01\x00A\xff\xd4H\x8d5C\x00\x00\x00\xbfV\x00\x00\x00A\xff\xd4A\x0f\xb7~\x02H\x8d5\x1f\x00\x00\x00A\xff\xd4H\x89\xefA\xff\xd5H\x8b\x03[]A\\A]A^\xc3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00P\x04\x11@\rB\tD\xaf4\x016\xad8\x01:\xaf<\x01>\xff",
     # -march=armv6m
-    0x1006: b"M\x06\x11\x1f\x02\x004build/features0.native.mpy\x00\x12factorial\x00\x88\x02\x18\xe0\x00\x00\x10\xb5\tK\tJ{D\x9cX\x02!\xe3h\x98G\x03\x00\x01 \x00+\x02\xd0XC\x01;\xfa\xe7\x02!#i\x98G\x10\xbd\xc0Fj\x00\x00\x00\x00\x00\x00\x00\xf8\xb5\nN\nK~D\xf4XChgiXh\xb8G\x05\x00\x07K\x08I\xf3XyDX\x88ck\x98G(\x00\xb8G h\xf8\xbd\xc0F:\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x11<\r>\xa38\x01:\xff",
+    0x1006: b"M\x06\x13\x1f\x03\x008build/test_armv6m.native.mpy\x00\x08add1\x00\x0cunused\x00\x8eb0\xe0\x00\x00\x00\x00\x00\x00\x02K\x03J{D\x9bX\x18hpG\xd0\x00\x00\x00\x00\x00\x00\x00\x10\xb5\x05K\x05I\x06J{D\x9aX[X\x10h\x02!\x1bi\x98G\x10\xbd\xb8\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x10\xb5\x06K\x06J{D\x9bX\x02!\x1ci\xdbh\x98G\x02!\x010\xa0G\x10\xbd\xc0F\x96\x00\x00\x00\x00\x00\x00\x00\xf7\xb5\x12O\x12K\x7fD\xfdX\x12Lki|D\x00\x93ChXh\x00\x9b\x98G\x0fK\x01\x90\x0fJ\xfbXnk\x1a`\x0eK!\x00\xffX\xb8\x88\xb0G!\x00V \x081\xb0G!\x00x\x88\x101\xb0G\x01\x98\x00\x9b\x98G(h\xfe\xbd\xc0Fr\x00\x00\x00\x00\x00\x00\x00R\x00\x00\x00\x08\x00\x00\x00@\xe2\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\x00\x00\x00\x1d\x00\x00\x00\x00\x00\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00P\x04\x11p\rr\tt\xafd\x01f\xadh\x01j\xafl\x01n\xff",
+    # -march=xtensawin
+    0x2806: b"M\x06+\x1f\x03\x00>build/test_xtensawin.native.mpy\x00\x08add1\x00\x0cunused\x00\x8a\x12\x06\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x006A\x00\x81\xf9\xff(\x08\x1d\xf0\x00\x006A\x00\x91\xfb\xff\x81\xf5\xff\xa8\t\x88H\x0c+\xe0\x08\x00-\n\x1d\xf0\x00\x006A\x00\x81\xf0\xff\xad\x02xH\x888\x0c+\xe0\x08\x00\x0c+\x1b\xaa\xe0\x07\x00-\n\x1d\xf06A\x00a\xe9\xff\x88\x122&\x05\xa2(\x01\xe0\x03\x00q\xe6\xff\x81\xea\xff\x92\xa7\x89\xa0\x99\x11H\xd6]\n\xb1\xe3\xff\xa2\x17\x02\x99\x08\xe0\x04\x00\xb1\xe2\xff\\j\xe0\x04\x00\xb1\xe1\xff\xa2\x17\x01\xe0\x04\x00\xad\x05\xe0\x03\x00(\x06\x1d\xf0p\x18\x04\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x11\x02\r\x04\x07\x06\x03\t\x0c\xaf\x01\x01\x03\xad\x05\x01\x07\xaf\t\x01\x0b\xff",
+    # -march=rv32imc
+    0x2C06: b'M\x06/\x1f\x03\x00:build/test_rv32imc.native.mpy\x00\x08add1\x00\x0cunused\x00\x8fb\x97\x0f\x00\x00g\x80\x0f\x05\x97\x07\x00\x00\x83\xa7\x07\x0e\x88C\x82\x80\x97\x07\x00\x00\x83\xa7G\r\x17\x07\x00\x00\x03\'\xc7\r\x9cK\x08C\x89E\x82\x87A\x11\x97\x07\x00\x00\x83\xa7\xa7\x0b"\xc4\x80K\xdcG\x06\xc6\x89E\x82\x97\xa2\x87"D\xb2@\x89E\x05\x05A\x01\x82\x87\\A\x01\x11"\xcc\x17\x04\x00\x00\x03$$\tN\xc6\xc8C\x83)D\x01\x06\xce&\xcaJ\xc8R\xc4\x82\x99\x17\n\x00\x00\x03*\xca\x07\x03)D\x03\xaa\x84\xf9g\x03UJ\x00\x93\x87\x07$\x17\x07\x00\x00\x03\'\x07\x07\x1c\xc3\x97\x05\x00\x00\x93\x85\xe5\x03\x02\x99\x97\x05\x00\x00\x93\x85\xc5\x03\x13\x05`\x05\x02\x99\x03U*\x00\x97\x05\x00\x00\x93\x85%\x03\x02\x99&\x85\x82\x99\x08@\xf2@bD\xd2DBI\xb2I"J\x05a\x82\x80\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x00\x00\x00\x00,\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00P\x04\x11t\rv\xafx\xadz\t|\xafh\x01j\xadl\x01n\xafp\x01r\xff',
 }
 
 # Populate armv7m-derived archs based on armv6m.
@@ -59,7 +69,7 @@ def open(self, path, mode):
     features0_file_contents[arch] = features0_file_contents[0x1006]
 
 # Check that a .mpy exists for the target (ignore sub-version in lookup).
-sys_implementation_mpy = sys.implementation._mpy & ~(3 << 8)
+sys_implementation_mpy = (sys.implementation._mpy & ~(3 << 8)) & 0xFFFF
 if sys_implementation_mpy not in features0_file_contents:
     print("SKIP")
     raise SystemExit
@@ -68,16 +78,29 @@ def open(self, path, mode):
 user_files = {"/features0.mpy": features0_file_contents[sys_implementation_mpy]}
 
 # Create and mount a user filesystem.
-uos.mount(UserFS(user_files), "/userfs")
+vfs.mount(UserFS(user_files), "/userfs")
 sys.path.append("/userfs")
 
 # Import the native function.
 gc.collect()
-from features0 import factorial
+from features0 import get, add1
+
+# Test that the native functions work to begin with.
+print(get())
+print(add1(12))
 
 # Free the module that contained the function.
 del sys.modules["features0"]
 
+
+# Sweep the stack to remove any stray pointers that we are aiming to reclaim.
+def recurse(n):
+    if n:
+        recurse(n - 1)
+
+
+recurse(10)
+
 # Run a GC cycle which should reclaim the module but not the function.
 gc.collect()
 
@@ -85,9 +108,10 @@ def open(self, path, mode):
 for i in range(1000):
     []
 
-# Run the native function, it should not have been freed or overwritten.
-print(factorial(10))
+# Run the native function, its text/BSS/rodata should not have been freed or overwritten.
+print(get())
+print(add1(12))
 
 # Unmount and undo path addition.
-uos.umount("/userfs")
+vfs.umount("/userfs")
 sys.path.pop()
diff --git a/tests/micropython/import_mpy_native_gc.py.exp b/tests/micropython/import_mpy_native_gc.py.exp
index 3fbd4a8698f..4250a13c726 100644
--- a/tests/micropython/import_mpy_native_gc.py.exp
+++ b/tests/micropython/import_mpy_native_gc.py.exp
@@ -1 +1,4 @@
-3628800
+123456
+13
+123456
+13
diff --git a/tests/micropython/import_mpy_native_gc_module/Makefile b/tests/micropython/import_mpy_native_gc_module/Makefile
new file mode 100644
index 00000000000..feccfc11762
--- /dev/null
+++ b/tests/micropython/import_mpy_native_gc_module/Makefile
@@ -0,0 +1,15 @@
+MPY_DIR = ../../..
+
+MOD = test_$(ARCH)
+SRC = test.c
+ARCH = x64
+PYTHON := $(command -v python3 2> /dev/null)
+ifndef PYTHON
+PYTHON = python
+endif
+
+.PHONY: main
+main: all
+	$(Q)cat $(MOD).mpy | $(PYTHON) -c 'import sys; print(sys.stdin.buffer.read())'
+
+include $(MPY_DIR)/py/dynruntime.mk
diff --git a/tests/micropython/import_mpy_native_gc_module/test.c b/tests/micropython/import_mpy_native_gc_module/test.c
new file mode 100644
index 00000000000..7ec7db42dae
--- /dev/null
+++ b/tests/micropython/import_mpy_native_gc_module/test.c
@@ -0,0 +1,38 @@
+// This test native module is used by import_mpy_native_gc.py.
+// It has:
+//  - A variable in the BSS, to check that the BSS is not reclaimed by the GC.
+//  - An unused native function at the start so that subsequent native functions
+//    don't start at the beginning of the native function data.  This tests that the
+//    GC doesn't reclaim the native function data even when the only pointer to that
+//    data is pointing inside the allocated memory.
+
+#include "py/dynruntime.h"
+
+uint32_t bss_variable;
+
+static mp_obj_t unused(mp_obj_t x_obj) {
+    return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(unused_obj, unused);
+
+static mp_obj_t get(void) {
+    return mp_obj_new_int(bss_variable);
+}
+static MP_DEFINE_CONST_FUN_OBJ_0(get_obj, get);
+
+static mp_obj_t add1(mp_obj_t x_obj) {
+    return mp_obj_new_int(mp_obj_get_int(x_obj) + 1);
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(add1_obj, add1);
+
+mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) {
+    MP_DYNRUNTIME_INIT_ENTRY
+
+    bss_variable = 123456;
+
+    mp_store_global(MP_QSTR_unused, MP_OBJ_FROM_PTR(&unused_obj));
+    mp_store_global(MP_QSTR_get, MP_OBJ_FROM_PTR(&get_obj));
+    mp_store_global(MP_QSTR_add1, MP_OBJ_FROM_PTR(&add1_obj));
+
+    MP_DYNRUNTIME_INIT_EXIT
+}
diff --git a/tests/micropython/incomplete_exc.py b/tests/micropython/incomplete_exc.py
new file mode 100644
index 00000000000..2aec9a47b3a
--- /dev/null
+++ b/tests/micropython/incomplete_exc.py
@@ -0,0 +1,40 @@
+# Test raising an incomplete exception.
+
+
+class C(Exception):
+    def __init__(self):
+        raise self
+
+
+class C1(C):
+    pass
+
+
+class B:
+    pass
+
+
+class C2(B, Exception):
+    def __init__(self):
+        raise self
+
+
+class C3(Exception, B):
+    def __init__(self):
+        raise self
+
+
+class D(Exception):
+    pass
+
+
+class C4(D):
+    def __init__(self):
+        raise self
+
+
+for cls in C, C1, C2, C3, C4:
+    try:
+        cls()
+    except TypeError as e:
+        print("TypeError")
diff --git a/tests/micropython/incomplete_exc.py.exp b/tests/micropython/incomplete_exc.py.exp
new file mode 100644
index 00000000000..f2e9c12f7f9
--- /dev/null
+++ b/tests/micropython/incomplete_exc.py.exp
@@ -0,0 +1,5 @@
+TypeError
+TypeError
+TypeError
+TypeError
+TypeError
diff --git a/tests/micropython/io_badlength.py b/tests/micropython/io_badlength.py
new file mode 100644
index 00000000000..646a2cd9241
--- /dev/null
+++ b/tests/micropython/io_badlength.py
@@ -0,0 +1,35 @@
+# Test when a use IOBase class has write/readinto which returns more data than
+# requested (https://github.com/micropython/micropython/issues/18845)
+
+try:
+    import io, json
+except:
+    print("SKIP")
+    raise SystemExit
+
+
+class S(io.IOBase):
+    def write(self, buf):
+        assert len(buf) >= 0
+        return 2
+
+    def ioctl(self, cmd, arg):
+        return 0
+
+    def readinto(self, buf):
+        assert len(buf) >= 0
+        return 3
+
+
+try:
+    print("abc", file=S())
+    print("write OK")
+except OSError as e:
+    print("write failed, errno", e.errno)
+
+buf = bytearray(1)
+try:
+    json.load(S())
+    print("readinto OK")
+except OSError as e:
+    print("read failed, errno", e.errno)
diff --git a/tests/micropython/io_badlength.py.exp b/tests/micropython/io_badlength.py.exp
new file mode 100644
index 00000000000..bba660dd3c8
--- /dev/null
+++ b/tests/micropython/io_badlength.py.exp
@@ -0,0 +1,2 @@
+write failed, errno 5
+read failed, errno 5
diff --git a/tests/micropython/kbd_intr.py b/tests/micropython/kbd_intr.py
index 81977aaa52f..e1ed7185ef0 100644
--- a/tests/micropython/kbd_intr.py
+++ b/tests/micropython/kbd_intr.py
@@ -1,10 +1,10 @@
 # test the micropython.kbd_intr() function
 
-import micropython
-
 try:
+    import micropython
+
     micropython.kbd_intr
-except AttributeError:
+except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
diff --git a/tests/micropython/meminfo.py b/tests/micropython/meminfo.py
index 9df341fbb83..f4dd8fdb604 100644
--- a/tests/micropython/meminfo.py
+++ b/tests/micropython/meminfo.py
@@ -1,12 +1,14 @@
 # tests meminfo functions in micropython module
 
-import micropython
+try:
+    import micropython
 
-# these functions are not always available
-if not hasattr(micropython, "mem_info"):
+    micropython.mem_info
+except (ImportError, AttributeError):
     print("SKIP")
-else:
-    micropython.mem_info()
-    micropython.mem_info(1)
-    micropython.qstr_info()
-    micropython.qstr_info(1)
+    raise SystemExit
+
+micropython.mem_info()
+micropython.mem_info(1)
+micropython.qstr_info()
+micropython.qstr_info(1)
diff --git a/tests/micropython/memstats.py b/tests/micropython/memstats.py
index dee3a4ce2f2..0e2e7b1c0b3 100644
--- a/tests/micropython/memstats.py
+++ b/tests/micropython/memstats.py
@@ -1,17 +1,19 @@
 # tests meminfo functions in micropython module
 
-import micropython
+try:
+    import micropython
 
-# these functions are not always available
-if not hasattr(micropython, "mem_total"):
+    micropython.mem_total
+except (ImportError, AttributeError):
     print("SKIP")
-else:
-    t = micropython.mem_total()
-    c = micropython.mem_current()
-    p = micropython.mem_peak()
+    raise SystemExit
 
-    l = list(range(10000))
+t = micropython.mem_total()
+c = micropython.mem_current()
+p = micropython.mem_peak()
 
-    print(micropython.mem_total() > t)
-    print(micropython.mem_current() > c)
-    print(micropython.mem_peak() > p)
+l = list(range(10000))
+
+print(micropython.mem_total() > t)
+print(micropython.mem_current() > c)
+print(micropython.mem_peak() > p)
diff --git a/tests/micropython/native_fun_attrs_code.py b/tests/micropython/native_fun_attrs_code.py
new file mode 100644
index 00000000000..d277a7b9b26
--- /dev/null
+++ b/tests/micropython/native_fun_attrs_code.py
@@ -0,0 +1,21 @@
+# Test MicroPython-specific restrictions of function.__code__ attribute.
+
+try:
+    (lambda: 0).__code__
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+import micropython
+
+
+@micropython.native
+def f_native():
+    pass
+
+
+# Can't access __code__ when function is native code.
+try:
+    f_native.__code__
+except AttributeError:
+    print("AttributeError")
diff --git a/tests/micropython/native_fun_attrs_code.py.exp b/tests/micropython/native_fun_attrs_code.py.exp
new file mode 100644
index 00000000000..d169edffb4c
--- /dev/null
+++ b/tests/micropython/native_fun_attrs_code.py.exp
@@ -0,0 +1 @@
+AttributeError
diff --git a/tests/micropython/native_marshal.py b/tests/micropython/native_marshal.py
new file mode 100644
index 00000000000..09a27a374b8
--- /dev/null
+++ b/tests/micropython/native_marshal.py
@@ -0,0 +1,45 @@
+# Test the marshal module in combination with native/viper functions.
+
+try:
+    import marshal
+
+    (lambda: 0).__code__
+except (AttributeError, ImportError):
+    print("SKIP")
+    raise SystemExit
+
+import unittest
+
+
+def f_native():
+    @micropython.native
+    def g():
+        pass
+
+    return g
+
+
+def f_viper():
+    @micropython.viper
+    def g():
+        pass
+
+    return g
+
+
+class Test(unittest.TestCase):
+    def test_native_function(self):
+        # Can't marshal a function with native code.
+        code = f_native.__code__
+        with self.assertRaises(ValueError):
+            marshal.dumps(code)
+
+    def test_viper_function(self):
+        # Can't marshal a function with viper code.
+        code = f_viper.__code__
+        with self.assertRaises(ValueError):
+            marshal.dumps(code)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/micropython/native_with.py b/tests/micropython/native_with.py
index 9c0b98af903..37c385a42f9 100644
--- a/tests/micropython/native_with.py
+++ b/tests/micropython/native_with.py
@@ -9,6 +9,7 @@ def __enter__(self):
         print("__enter__")
 
     def __exit__(self, a, b, c):
+        b = repr(b)[:10]  # shorten exception to "NameError(" prefix for target compatibility
         print("__exit__", a, b, c)
 
 
diff --git a/tests/micropython/native_with.py.exp b/tests/micropython/native_with.py.exp
index 7e28663f6fc..b8083a4c66b 100644
--- a/tests/micropython/native_with.py.exp
+++ b/tests/micropython/native_with.py.exp
@@ -5,5 +5,5 @@ __exit__ None None None
 __init__
 __enter__
 1
-__exit__  name 'fail' isn't defined None
+__exit__  NameError( None
 NameError
diff --git a/tests/micropython/opt_level.py b/tests/micropython/opt_level.py
index dd5493a7a3c..789197d8825 100644
--- a/tests/micropython/opt_level.py
+++ b/tests/micropython/opt_level.py
@@ -1,4 +1,10 @@
-import micropython as micropython
+# test micropython.opt_level()
+
+try:
+    import micropython
+except ImportError:
+    print("SKIP")
+    raise SystemExit
 
 # check we can get and set the level
 micropython.opt_level(0)
diff --git a/tests/micropython/opt_level_lineno.py b/tests/micropython/opt_level_lineno.py
index 1cbf2fb1a85..fcfbe8643da 100644
--- a/tests/micropython/opt_level_lineno.py
+++ b/tests/micropython/opt_level_lineno.py
@@ -1,6 +1,23 @@
-import micropython as micropython
+# test micropython.opt_level() and line numbers
+
+try:
+    import micropython
+except ImportError:
+    print("SKIP")
+    raise SystemExit
 
 # check that level 3 doesn't store line numbers
 # the expected output is that any line is printed as "line 1"
 micropython.opt_level(3)
-exec("try:\n xyz\nexcept NameError as er:\n import usys\n usys.print_exception(er)")
+
+# force bytecode emitter, because native emitter doesn't store line numbers
+exec("""
+@micropython.bytecode
+def f():
+    try:
+        xyz
+    except NameError as er:
+        import sys
+        sys.print_exception(er)
+f()
+""")
diff --git a/tests/micropython/opt_level_lineno.py.exp b/tests/micropython/opt_level_lineno.py.exp
index 469b90ba793..b50f0628c81 100644
--- a/tests/micropython/opt_level_lineno.py.exp
+++ b/tests/micropython/opt_level_lineno.py.exp
@@ -1,3 +1,3 @@
 Traceback (most recent call last):
-  File "", line 1, in 
+  File "", line 1, in f
 NameError: name 'xyz' isn't defined
diff --git a/tests/micropython/ringio.py b/tests/micropython/ringio.py
new file mode 100644
index 00000000000..d82c04565bd
--- /dev/null
+++ b/tests/micropython/ringio.py
@@ -0,0 +1,66 @@
+# Check that micropython.RingIO works correctly.
+
+try:
+    import micropython
+
+    micropython.RingIO
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+rb = micropython.RingIO(16)
+print(rb)
+
+print(rb.any())
+
+rb.write(b"\x00")
+print(rb.any())
+
+rb.write(b"\x00")
+print(rb.any())
+
+print(rb.read(2))
+print(rb.any())
+
+
+rb.write(b"\x00\x01")
+print(rb.read())
+
+print(rb.read(1))
+
+# Try to write more data than can fit at one go.
+print(rb.write(b"\x00\x01" * 10))
+print(rb.write(b"\x00"))
+print(rb.read())
+
+
+ba = bytearray(17)
+rb = micropython.RingIO(ba)
+print(rb)
+print(rb.write(b"\x00\x01" * 10))
+print(rb.write(b"\x00"))
+print(rb.read())
+
+try:
+    # Size must be int.
+    micropython.RingIO(None)
+except TypeError as ex:
+    print(type(ex))
+
+try:
+    # Buffer may not be empty
+    micropython.RingIO(bytearray(0))
+except ValueError as ex:
+    print(type(ex))
+
+try:
+    # Buffer may not be too small
+    micropython.RingIO(bytearray(1))
+except ValueError as ex:
+    print(type(ex))
+
+try:
+    # Size may not be too small
+    micropython.RingIO(0)
+except ValueError as ex:
+    print(type(ex))
diff --git a/tests/micropython/ringio.py.exp b/tests/micropython/ringio.py.exp
new file mode 100644
index 00000000000..c391529a41e
--- /dev/null
+++ b/tests/micropython/ringio.py.exp
@@ -0,0 +1,19 @@
+
+0
+1
+2
+b'\x00\x00'
+0
+b'\x00\x01'
+b''
+16
+0
+b'\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01'
+
+16
+0
+b'\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01'
+
+
+
+
diff --git a/tests/micropython/ringio_async.py b/tests/micropython/ringio_async.py
new file mode 100644
index 00000000000..0db146451c9
--- /dev/null
+++ b/tests/micropython/ringio_async.py
@@ -0,0 +1,34 @@
+# Check that micropython.RingIO works correctly with asyncio.Stream.
+
+try:
+    import asyncio, micropython
+
+    asyncio.StreamWriter
+    micropython.RingIO
+except (AttributeError, ImportError):
+    print("SKIP")
+    raise SystemExit
+
+rb = micropython.RingIO(16)
+rba = asyncio.StreamWriter(rb)
+
+data = b"ABC123" * 20
+print("w", len(data))
+
+
+async def data_writer():
+    global data
+    rba.write(data)
+    await rba.drain()
+
+
+async def main():
+    task = asyncio.create_task(data_writer())
+    await asyncio.sleep_ms(10)
+    read = await rba.readexactly(len(data))
+    print(read)
+    print("r", len(read))
+    print(read == data)
+
+
+asyncio.run(main())
diff --git a/tests/micropython/ringio_async.py.exp b/tests/micropython/ringio_async.py.exp
new file mode 100644
index 00000000000..dfeb71d890f
--- /dev/null
+++ b/tests/micropython/ringio_async.py.exp
@@ -0,0 +1,4 @@
+w 120
+b'ABC123ABC123ABC123ABC123ABC123ABC123ABC123ABC123ABC123ABC123ABC123ABC123ABC123ABC123ABC123ABC123ABC123ABC123ABC123ABC123'
+r 120
+True
diff --git a/tests/micropython/ringio_big.py b/tests/micropython/ringio_big.py
new file mode 100644
index 00000000000..88d5d2cc165
--- /dev/null
+++ b/tests/micropython/ringio_big.py
@@ -0,0 +1,40 @@
+# Check that micropython.RingIO works correctly.
+
+try:
+    import micropython
+
+    micropython.RingIO
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+results = []
+
+try:
+    # The maximum possible size passed as an integer.
+    micropython.RingIO(65534)
+
+    try:
+        # Size may not be too big
+        micropython.RingIO(65535)
+    except ValueError as ex:
+        results.append(type(ex))
+
+    # Allocate a buffer for use below.
+    buf_64k = memoryview(bytearray(65536))
+
+    # The maximum possible size passed as a buffer.
+    micropython.RingIO(buf_64k[:-1])
+
+    try:
+        # Buffer may not be too big
+        micropython.RingIO(buf_64k)
+    except ValueError as ex:
+        results.append(type(ex))
+
+except MemoryError:
+    print("SKIP")
+    raise SystemExit
+
+for result in results:
+    print(result)
diff --git a/tests/micropython/ringio_big.py.exp b/tests/micropython/ringio_big.py.exp
new file mode 100644
index 00000000000..72af34b3838
--- /dev/null
+++ b/tests/micropython/ringio_big.py.exp
@@ -0,0 +1,2 @@
+
+
diff --git a/tests/micropython/schedule.py b/tests/micropython/schedule.py
index 6a91459ea3d..e629edb3eb3 100644
--- a/tests/micropython/schedule.py
+++ b/tests/micropython/schedule.py
@@ -1,10 +1,12 @@
 # test micropython.schedule() function
-
-import micropython
+# this test should be manually kept in synch with
+# tests/micrpython/schedule_sleep.py.
 
 try:
+    import micropython
+
     micropython.schedule
-except AttributeError:
+except (ImportError, AttributeError):
     print("SKIP")
     raise SystemExit
 
diff --git a/tests/micropython/schedule_kbd_intr.py b/tests/micropython/schedule_kbd_intr.py
new file mode 100644
index 00000000000..ec7ef5a5d07
--- /dev/null
+++ b/tests/micropython/schedule_kbd_intr.py
@@ -0,0 +1,72 @@
+# Test using micropython.schedule() to schedule a KeyboardInterrupt.
+# Note: this test cannot use unittest because the unittest overhead
+# may trigger the scheduler when we don't want it to.
+
+try:
+    from time import sleep
+    from micropython import schedule, kbd_intr
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+########################################################################
+# Test basic scheduling.
+
+try:
+    schedule(kbd_intr, None)
+except ValueError:
+    # Scheduling `kbd_intr` is not supported on this target.
+    print("SKIP")
+    raise SystemExit
+
+try:
+    sleep(0)
+    sleep(0)
+    print("fail")
+except KeyboardInterrupt:
+    print("KeyboardInterrupt")
+
+########################################################################
+# Should be able to schedule it many times without error, and that
+# should only trigger one KeyboardInterrupt.
+
+schedule(kbd_intr, None)
+schedule(kbd_intr, None)
+schedule(kbd_intr, None)
+schedule(kbd_intr, None)
+schedule(kbd_intr, None)
+schedule(kbd_intr, None)
+schedule(kbd_intr, None)
+schedule(kbd_intr, None)
+schedule(kbd_intr, None)
+schedule(kbd_intr, None)
+try:
+    sleep(0)
+    sleep(0)
+    print("fail")
+except KeyboardInterrupt:
+    print("KeyboardInterrupt")
+
+# This should not raise.
+for _ in range(100):
+    sleep(0)
+
+########################################################################
+# Test nested scheduling.
+
+
+def callback(_):
+    schedule(kbd_intr, None)
+    # This should not raise because we are in schedule context.
+    for _ in range(100):
+        sleep(0)
+
+
+schedule(callback, None)
+sleep(0)
+try:
+    sleep(0)
+    sleep(0)
+    print("fail")
+except KeyboardInterrupt:
+    print("KeyboardInterrupt")
diff --git a/tests/micropython/schedule_kbd_intr.py.exp b/tests/micropython/schedule_kbd_intr.py.exp
new file mode 100644
index 00000000000..c4ba7730c52
--- /dev/null
+++ b/tests/micropython/schedule_kbd_intr.py.exp
@@ -0,0 +1,3 @@
+KeyboardInterrupt
+KeyboardInterrupt
+KeyboardInterrupt
diff --git a/tests/micropython/schedule_sleep.py b/tests/micropython/schedule_sleep.py
new file mode 100644
index 00000000000..9aadde7b084
--- /dev/null
+++ b/tests/micropython/schedule_sleep.py
@@ -0,0 +1,72 @@
+# test micropython.schedule() function
+# this is the same as tests/micropython/schedule.py but the busy loops are
+# replaced with sleep/sleep_ms which allows the test to succeed when run under
+# the native emitter.
+
+try:
+    import micropython
+    import time
+
+    micropython.schedule
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+
+# Basic test of scheduling a function.
+
+
+def callback(arg):
+    global done
+    print(arg)
+    done = True
+
+
+done = False
+micropython.schedule(callback, 1)
+while not done:
+    time.sleep(0)
+
+# Test that callbacks can be scheduled from within a callback, but
+# that they don't execute until the outer callback is finished.
+
+
+def callback_inner(arg):
+    global done
+    print("inner")
+    done += 1
+
+
+def callback_outer(arg):
+    global done
+    micropython.schedule(callback_inner, 0)
+    # need a loop so that the VM can check for pending events
+    for i in range(2):
+        pass
+    print("outer")
+    done += 1
+
+
+done = 0
+micropython.schedule(callback_outer, 0)
+while done != 2:
+    time.sleep(0)
+
+# Test that scheduling too many callbacks leads to an exception.  To do this we
+# must schedule from within a callback to guarantee that the scheduler is locked.
+
+
+def callback(arg):
+    global done
+    try:
+        for i in range(100):
+            micropython.schedule(lambda x: x, None)
+    except RuntimeError:
+        print("RuntimeError")
+    done = True
+
+
+done = False
+micropython.schedule(callback, None)
+while not done:
+    time.sleep_ms(0)
diff --git a/tests/micropython/schedule_sleep.py.exp b/tests/micropython/schedule_sleep.py.exp
new file mode 100644
index 00000000000..c4a3e1227e2
--- /dev/null
+++ b/tests/micropython/schedule_sleep.py.exp
@@ -0,0 +1,4 @@
+1
+outer
+inner
+RuntimeError
diff --git a/tests/micropython/stack_use.py b/tests/micropython/stack_use.py
index 266885d9d18..5d36fdca2fe 100644
--- a/tests/micropython/stack_use.py
+++ b/tests/micropython/stack_use.py
@@ -1,7 +1,11 @@
 # tests stack_use function in micropython module
-import micropython
 
-if not hasattr(micropython, "stack_use"):
+try:
+    import micropython
+
+    micropython.stack_use
+except (ImportError, AttributeError):
     print("SKIP")
-else:
-    print(type(micropython.stack_use()))  # output varies
+    raise SystemExit
+
+print(type(micropython.stack_use()))  # output varies
diff --git a/tests/micropython/test_normalize_newlines.py b/tests/micropython/test_normalize_newlines.py
new file mode 100644
index 00000000000..f19aaa69a3f
--- /dev/null
+++ b/tests/micropython/test_normalize_newlines.py
@@ -0,0 +1,14 @@
+# Test for normalize_newlines functionality
+# This test verifies that test framework handles various newline combinations correctly
+
+# Note: This is more of an integration test since normalize_newlines is in the test framework
+# The actual testing happens when this test is run through run-tests.py
+
+print("Testing newline handling")
+print("Line 1\r\nLine 2")  # Windows-style line ending - should be normalized
+print("Line 3")            # Normal line
+print("Line 4")            # Normal line
+print("Line 5\nLine 6")    # Unix-style line ending - already normalized
+
+# Test that literal \r in strings is preserved
+print(repr("test\rstring"))  # Should show 'test\rstring' not 'test\nstring'
diff --git a/tests/micropython/test_normalize_newlines.py.exp b/tests/micropython/test_normalize_newlines.py.exp
new file mode 100644
index 00000000000..c4395468cf1
--- /dev/null
+++ b/tests/micropython/test_normalize_newlines.py.exp
@@ -0,0 +1,8 @@
+Testing newline handling
+Line 1
+Line 2
+Line 3
+Line 4
+Line 5
+Line 6
+'test\rstring'
diff --git a/tests/micropython/viper_error.py b/tests/micropython/viper_error.py
index 80617af0c1f..e58a3c59fa0 100644
--- a/tests/micropython/viper_error.py
+++ b/tests/micropython/viper_error.py
@@ -1,19 +1,19 @@
 # test syntax and type errors specific to viper code generation
 
 
-def test(code):
+def test(code, msg):
     try:
         exec(code)
     except (SyntaxError, ViperTypeError, NotImplementedError) as e:
-        print(repr(e))
+        print(type(e), str(e) or msg)
 
 
 # viper: annotations must be identifiers
-test("@micropython.viper\ndef f(a:1): pass")
-test("@micropython.viper\ndef f() -> 1: pass")
+test("@micropython.viper\ndef f(a:1): pass", "annotation must be an identifier")
+test("@micropython.viper\ndef f() -> 1: pass", "annotation must be an identifier")
 
 # unknown type
-test("@micropython.viper\ndef f(x:unknown_type): pass")
+test("@micropython.viper\ndef f(x:unknown_type): pass", "unknown type 'unknown_type'")
 
 # local used before type known
 test(
@@ -22,7 +22,8 @@ def test(code):
 def f():
     print(x)
     x = 1
-"""
+""",
+    "local 'x' used before type known",
 )
 
 # type mismatch storing to local
@@ -33,7 +34,8 @@ def f():
     x = 1
     y = []
     x = y
-"""
+""",
+    "local 'x' has type 'int' but source is 'object'",
 )
 
 # can't implicitly convert type to bool
@@ -44,46 +46,52 @@ def f():
     x = ptr(0)
     if x:
         pass
-"""
+""",
+    "can't implicitly convert 'ptr' to 'bool'",
 )
 
 # incorrect return type
-test("@micropython.viper\ndef f() -> int: return []")
+test("@micropython.viper\ndef f() -> int: return []", "return expected 'int' but got 'object'")
+
+# can't do unary op of incompatible type
+test("@micropython.viper\ndef f(x:ptr): -x", "can't do unary op of 'ptr'")
 
 # can't do binary op between incompatible types
-test("@micropython.viper\ndef f(): 1 + []")
-test("@micropython.viper\ndef f(x:int, y:uint): x < y")
+test("@micropython.viper\ndef f(): 1 + []", "can't do binary op between 'int' and 'object'")
+test("@micropython.viper\ndef f(x:int, y:uint): x < y", "comparison of int and uint")
 
 # can't load
-test("@micropython.viper\ndef f(): 1[0]")
-test("@micropython.viper\ndef f(): 1[x]")
+test("@micropython.viper\ndef f(): 1[0]", "can't load from 'int'")
+test("@micropython.viper\ndef f(): 1[x]", "can't load from 'int'")
 
 # can't store
-test("@micropython.viper\ndef f(): 1[0] = 1")
-test("@micropython.viper\ndef f(): 1[x] = 1")
-test("@micropython.viper\ndef f(x:int): x[0] = x")
-test("@micropython.viper\ndef f(x:ptr32): x[0] = None")
-test("@micropython.viper\ndef f(x:ptr32): x[x] = None")
+test("@micropython.viper\ndef f(): 1[0] = 1", "can't store to 'int'")
+test("@micropython.viper\ndef f(): 1[x] = 1", "can't store to 'int'")
+test("@micropython.viper\ndef f(x:int): x[0] = x", "can't store to 'int'")
+test("@micropython.viper\ndef f(x:ptr32): x[0] = None", "can't store 'None'")
+test("@micropython.viper\ndef f(x:ptr32): x[x] = None", "can't store 'None'")
 
 # must raise an object
-test("@micropython.viper\ndef f(): raise 1")
+test("@micropython.viper\ndef f(): raise 1", "must raise an object")
 
 # unary ops not implemented
-test("@micropython.viper\ndef f(x:int): +x")
-test("@micropython.viper\ndef f(x:int): -x")
-test("@micropython.viper\ndef f(x:int): ~x")
+test("@micropython.viper\ndef f(x:int): not x", "'not' not implemented")
 
 # binary op not implemented
-test("@micropython.viper\ndef f(x:uint, y:uint): res = x // y")
-test("@micropython.viper\ndef f(x:uint, y:uint): res = x % y")
-test("@micropython.viper\ndef f(x:int): res = x in x")
+test("@micropython.viper\ndef f(x:uint, y:uint): res = x // y", "div/mod not implemented for uint")
+test("@micropython.viper\ndef f(x:uint, y:uint): res = x % y", "div/mod not implemented for uint")
+test("@micropython.viper\ndef f(x:int): res = x in x", "binary op  not implemented")
+
+# raise with 0 or 2 args not implemented
+test("@micropython.viper\ndef f():\n try:\n  x\n except:\n  raise\n", "native raise")
+test("@micropython.viper\ndef f(): raise Exception from Exception", "native raise")
 
 # yield (from) not implemented
-test("@micropython.viper\ndef f(): yield")
-test("@micropython.viper\ndef f(): yield from f")
+test("@micropython.viper\ndef f(): yield", "native yield")
+test("@micropython.viper\ndef f(): yield from f", "native yield")
 
 # passing a ptr to a Python function not implemented
-test("@micropython.viper\ndef f(): print(ptr(1))")
+test("@micropython.viper\ndef f(): print(ptr(1))", "conversion to object")
 
 # cast of a casting identifier not implemented
-test("@micropython.viper\ndef f(): int(int)")
+test("@micropython.viper\ndef f(): int(int)", "casting")
diff --git a/tests/micropython/viper_error.py.exp b/tests/micropython/viper_error.py.exp
index 31c85b1d872..9b663b8508a 100644
--- a/tests/micropython/viper_error.py.exp
+++ b/tests/micropython/viper_error.py.exp
@@ -1,27 +1,28 @@
-SyntaxError('annotation must be an identifier',)
-SyntaxError('annotation must be an identifier',)
-ViperTypeError("unknown type 'unknown_type'",)
-ViperTypeError("local 'x' used before type known",)
-ViperTypeError("local 'x' has type 'int' but source is 'object'",)
-ViperTypeError("can't implicitly convert 'ptr' to 'bool'",)
-ViperTypeError("return expected 'int' but got 'object'",)
-ViperTypeError("can't do binary op between 'int' and 'object'",)
-ViperTypeError('comparison of int and uint',)
-ViperTypeError("can't load from 'int'",)
-ViperTypeError("can't load from 'int'",)
-ViperTypeError("can't store to 'int'",)
-ViperTypeError("can't store to 'int'",)
-ViperTypeError("can't store to 'int'",)
-ViperTypeError("can't store 'None'",)
-ViperTypeError("can't store 'None'",)
-ViperTypeError('must raise an object',)
-ViperTypeError('unary op __pos__ not implemented',)
-ViperTypeError('unary op __neg__ not implemented',)
-ViperTypeError('unary op __invert__ not implemented',)
-ViperTypeError('div/mod not implemented for uint',)
-ViperTypeError('div/mod not implemented for uint',)
-ViperTypeError('binary op  not implemented',)
-NotImplementedError('native yield',)
-NotImplementedError('native yield',)
-NotImplementedError('conversion to object',)
-NotImplementedError('casting',)
+ annotation must be an identifier
+ annotation must be an identifier
+ unknown type 'unknown_type'
+ local 'x' used before type known
+ local 'x' has type 'int' but source is 'object'
+ can't implicitly convert 'ptr' to 'bool'
+ return expected 'int' but got 'object'
+ can't do unary op of 'ptr'
+ can't do binary op between 'int' and 'object'
+ comparison of int and uint
+ can't load from 'int'
+ can't load from 'int'
+ can't store to 'int'
+ can't store to 'int'
+ can't store to 'int'
+ can't store 'None'
+ can't store 'None'
+ must raise an object
+ 'not' not implemented
+ div/mod not implemented for uint
+ div/mod not implemented for uint
+ binary op  not implemented
+ native raise
+ native raise
+ native yield
+ native yield
+ conversion to object
+ casting
diff --git a/tests/micropython/viper_large_jump.py b/tests/micropython/viper_large_jump.py
new file mode 100644
index 00000000000..1c5913dec1e
--- /dev/null
+++ b/tests/micropython/viper_large_jump.py
@@ -0,0 +1,20 @@
+COUNT = 600
+
+
+try:
+    code = """
+@micropython.viper
+def f() -> int:
+    x = 0
+    while x < 10:
+"""
+    for i in range(COUNT):
+        code += "        x += 1\n"
+    code += "    return x"
+    exec(code)
+except MemoryError:
+    print("SKIP-TOO-LARGE")
+    raise SystemExit
+
+
+print(f())
diff --git a/tests/micropython/viper_large_jump.py.exp b/tests/micropython/viper_large_jump.py.exp
new file mode 100644
index 00000000000..e9f960cf4ac
--- /dev/null
+++ b/tests/micropython/viper_large_jump.py.exp
@@ -0,0 +1 @@
+600
diff --git a/tests/micropython/viper_misc_intbig.py b/tests/micropython/viper_misc_intbig.py
index ac09f578572..91673f2c108 100644
--- a/tests/micropython/viper_misc_intbig.py
+++ b/tests/micropython/viper_misc_intbig.py
@@ -7,6 +7,6 @@ def viper_uint() -> uint:
     return uint(-1)
 
 
-import usys
+import sys
 
-print(viper_uint() == (usys.maxsize << 1 | 1))
+print(viper_uint() == (sys.maxsize << 1 | 1))
diff --git a/tests/micropython/viper_preserve_invariants.py b/tests/micropython/viper_preserve_invariants.py
new file mode 100644
index 00000000000..26f6003aba7
--- /dev/null
+++ b/tests/micropython/viper_preserve_invariants.py
@@ -0,0 +1,39 @@
+RESULTS = []
+
+LOAD_TEMPLATE_REG = """
+BUFFER = bytearray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
+OFFSET = 1
+@micropython.viper
+def test_invariants_load{}_reg(buf: ptr{}, offset: int):
+    offset_copy1: int = offset
+    temporary1 = buf[offset]
+    offset_copy2: int = offset
+    temporary2 = buf[offset]
+    RESULTS.append(["LOAD{}", temporary1 == temporary2, offset == offset_copy1 == offset_copy2])
+test_invariants_load{}_reg(BUFFER, OFFSET)
+"""
+
+
+STORE_TEMPLATE_REG = """
+BUFFER = bytearray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
+OFFSET = 1
+@micropython.viper
+def test_invariants_store{}_reg(buf: ptr{}, offset: int):
+    offset_copy: int = offset
+    value: uint = {}
+    buf[offset] = value
+    temporary = buf[offset]
+    RESULTS.append(["STORE{}", temporary == value, offset == offset_copy])
+test_invariants_store{}_reg(BUFFER, OFFSET)
+"""
+
+try:
+    for width, value in ((8, 0x11), (16, 0x1111), (32, 0x11111111)):
+        exec(LOAD_TEMPLATE_REG.format(width, width, width, width, width))
+        exec(STORE_TEMPLATE_REG.format(width, width, value, width, width, width))
+except MemoryError:
+    print("SKIP-TOO-LARGE")
+    raise SystemExit
+
+for line in RESULTS:
+    print(" ".join([str(i) for i in line]))
diff --git a/tests/micropython/viper_preserve_invariants.py.exp b/tests/micropython/viper_preserve_invariants.py.exp
new file mode 100644
index 00000000000..21c66252430
--- /dev/null
+++ b/tests/micropython/viper_preserve_invariants.py.exp
@@ -0,0 +1,6 @@
+LOAD8 True True
+STORE8 True True
+LOAD16 True True
+STORE16 True True
+LOAD32 True True
+STORE32 True True
diff --git a/tests/micropython/viper_ptr16_load_boundary.py b/tests/micropython/viper_ptr16_load_boundary.py
new file mode 100644
index 00000000000..0d4c3105b68
--- /dev/null
+++ b/tests/micropython/viper_ptr16_load_boundary.py
@@ -0,0 +1,39 @@
+# Test boundary conditions for various architectures
+
+GET_TEMPLATE = """
+@micropython.viper
+def get{off}(src: ptr16) -> uint:
+    return uint(src[{off}])
+print(hex(get{off}(buffer)))
+"""
+
+
+BIT_THRESHOLDS = (5, 8, 11, 12)
+SIZE = 2
+
+
+@micropython.viper
+def get_index(src: ptr16, i: int) -> int:
+    return src[i]
+
+
+def data(start, len):
+    output = bytearray(len)
+    for idx in range(len):
+        output[idx] = (start + idx) & 0xFF
+    return output
+
+
+buffer = bytearray((((1 << max(BIT_THRESHOLDS)) + 1) // 1024) * 1024)
+val = 0
+for bit in BIT_THRESHOLDS:
+    print("---", bit)
+    pre, idx, post = ((1 << bit) - (2 * SIZE), (1 << bit) - (1 * SIZE), 1 << bit)
+    buffer[pre:post] = data(val, 3 * SIZE)
+    val = val + (3 * SIZE)
+
+    pre, idx, post = pre // SIZE, idx // SIZE, post // SIZE
+    print(hex(get_index(buffer, pre)), hex(get_index(buffer, idx)), hex(get_index(buffer, post)))
+    exec(GET_TEMPLATE.format(off=pre))
+    exec(GET_TEMPLATE.format(off=idx))
+    exec(GET_TEMPLATE.format(off=post))
diff --git a/tests/micropython/viper_ptr16_load_boundary.py.exp b/tests/micropython/viper_ptr16_load_boundary.py.exp
new file mode 100644
index 00000000000..56f1d322904
--- /dev/null
+++ b/tests/micropython/viper_ptr16_load_boundary.py.exp
@@ -0,0 +1,20 @@
+--- 5
+0x100 0x302 0x504
+0x100
+0x302
+0x504
+--- 8
+0x706 0x908 0xb0a
+0x706
+0x908
+0xb0a
+--- 11
+0xd0c 0xf0e 0x1110
+0xd0c
+0xf0e
+0x1110
+--- 12
+0x1312 0x1514 0x1716
+0x1312
+0x1514
+0x1716
diff --git a/tests/micropython/viper_ptr16_store_boundary.py b/tests/micropython/viper_ptr16_store_boundary.py
new file mode 100644
index 00000000000..7c774d4d1ca
--- /dev/null
+++ b/tests/micropython/viper_ptr16_store_boundary.py
@@ -0,0 +1,63 @@
+# Test boundary conditions for various architectures
+
+SET_TEMPLATE = """
+@micropython.viper
+def set{off}(dest: ptr16):
+    saved = dest
+    dest[{off}] = {val}
+    assert int(saved) == int(dest)
+"""
+
+BIT_THRESHOLDS = (5, 8, 11, 12)
+SIZE = 2
+MASK = (1 << (8 * SIZE)) - 1
+
+next_int = 1
+test_buffer = bytearray(SIZE)
+
+
+def next_value() -> uint:
+    global next_int
+    global test_buffer
+    for index in range(1, SIZE):
+        test_buffer[index - 1] = test_buffer[index]
+    test_buffer[SIZE - 1] = next_int
+    next_int += 1
+    output = 0
+    for byte in test_buffer:
+        output = (output << 8) | byte
+    return output & MASK
+
+
+def get_index(src, i):
+    return src[i * SIZE] + (src[(i * SIZE) + 1] << 8)
+
+
+@micropython.viper
+def set_index(dest: ptr16, i: int, val: uint):
+    saved = dest
+    dest[i] = val
+    assert int(saved) == int(dest)
+
+
+try:
+    buffer = bytearray((((1 << max(BIT_THRESHOLDS)) // 1024) + 1) * 1024)
+
+    for bit in BIT_THRESHOLDS:
+        offset = (1 << bit) - (2 * SIZE)
+        for index in range(0, 3 * SIZE, SIZE):
+            exec(SET_TEMPLATE.format(off=(offset + index) // SIZE, val=next_value()))
+except MemoryError:
+    print("SKIP-TOO-LARGE")
+    raise SystemExit
+
+
+for bit in BIT_THRESHOLDS:
+    print("---", bit)
+    offset = (1 << bit) - (2 * SIZE)
+    for index in range(0, 3 * SIZE, SIZE):
+        globals()["set{}".format((offset + index) // SIZE)](buffer)
+        print(hex(get_index(buffer, (offset + index) // SIZE)))
+    for index in range(0, 3 * SIZE, SIZE):
+        set_index(buffer, (offset + index) // SIZE, next_value())
+        print(hex(get_index(buffer, (offset + index) // SIZE)))
diff --git a/tests/micropython/viper_ptr16_store_boundary.py.exp b/tests/micropython/viper_ptr16_store_boundary.py.exp
new file mode 100644
index 00000000000..007a50b3eda
--- /dev/null
+++ b/tests/micropython/viper_ptr16_store_boundary.py.exp
@@ -0,0 +1,28 @@
+--- 5
+0x1
+0x102
+0x203
+0xc0d
+0xd0e
+0xe0f
+--- 8
+0x304
+0x405
+0x506
+0xf10
+0x1011
+0x1112
+--- 11
+0x607
+0x708
+0x809
+0x1213
+0x1314
+0x1415
+--- 12
+0x90a
+0xa0b
+0xb0c
+0x1516
+0x1617
+0x1718
diff --git a/tests/micropython/viper_ptr32_load_boundary.py b/tests/micropython/viper_ptr32_load_boundary.py
new file mode 100644
index 00000000000..971d1113c49
--- /dev/null
+++ b/tests/micropython/viper_ptr32_load_boundary.py
@@ -0,0 +1,39 @@
+# Test boundary conditions for various architectures
+
+GET_TEMPLATE = """
+@micropython.viper
+def get{off}(src: ptr32) -> uint:
+    return uint(src[{off}])
+print(hex(get{off}(buffer)))
+"""
+
+
+BIT_THRESHOLDS = (5, 8, 11, 12)
+SIZE = 4
+
+
+@micropython.viper
+def get_index(src: ptr32, i: int) -> int:
+    return src[i]
+
+
+def data(start, len):
+    output = bytearray(len)
+    for idx in range(len):
+        output[idx] = (start + idx) & 0xFF
+    return output
+
+
+buffer = bytearray((((1 << max(BIT_THRESHOLDS)) + 1) // 1024) * 1024)
+val = 0
+for bit in BIT_THRESHOLDS:
+    print("---", bit)
+    pre, idx, post = (((1 << bit) - (2 * SIZE)), ((1 << bit) - (1 * SIZE)), (1 << bit))
+    buffer[pre:post] = data(val, 3 * SIZE)
+    val = val + (3 * SIZE)
+
+    pre, idx, post = pre // SIZE, idx // SIZE, post // SIZE
+    print(hex(get_index(buffer, pre)), hex(get_index(buffer, idx)), hex(get_index(buffer, post)))
+    exec(GET_TEMPLATE.format(off=pre))
+    exec(GET_TEMPLATE.format(off=idx))
+    exec(GET_TEMPLATE.format(off=post))
diff --git a/tests/micropython/viper_ptr32_load_boundary.py.exp b/tests/micropython/viper_ptr32_load_boundary.py.exp
new file mode 100644
index 00000000000..1e22a8b3613
--- /dev/null
+++ b/tests/micropython/viper_ptr32_load_boundary.py.exp
@@ -0,0 +1,20 @@
+--- 5
+0x3020100 0x7060504 0xb0a0908
+0x3020100
+0x7060504
+0xb0a0908
+--- 8
+0xf0e0d0c 0x13121110 0x17161514
+0xf0e0d0c
+0x13121110
+0x17161514
+--- 11
+0x1b1a1918 0x1f1e1d1c 0x23222120
+0x1b1a1918
+0x1f1e1d1c
+0x23222120
+--- 12
+0x27262524 0x2b2a2928 0x2f2e2d2c
+0x27262524
+0x2b2a2928
+0x2f2e2d2c
diff --git a/tests/micropython/viper_ptr32_store_boundary.py b/tests/micropython/viper_ptr32_store_boundary.py
new file mode 100644
index 00000000000..96ca74ad3ca
--- /dev/null
+++ b/tests/micropython/viper_ptr32_store_boundary.py
@@ -0,0 +1,68 @@
+# Test boundary conditions for various architectures
+
+SET_TEMPLATE = """
+@micropython.viper
+def set{off}(dest: ptr32):
+    saved = dest
+    dest[{off}] = {val}
+    assert int(saved) == int(dest)
+"""
+
+BIT_THRESHOLDS = (5, 8, 11, 12)
+SIZE = 4
+MASK = (1 << (8 * SIZE)) - 1
+
+next_int = 1
+test_buffer = bytearray(SIZE)
+
+
+def next_value() -> uint:
+    global next_int
+    global test_buffer
+    for index in range(1, SIZE):
+        test_buffer[index - 1] = test_buffer[index]
+    test_buffer[SIZE - 1] = next_int
+    next_int += 1
+    output = 0
+    for byte in test_buffer:
+        output = (output << 8) | byte
+    return output & MASK
+
+
+def get_index(src, i):
+    return (
+        src[i * SIZE]
+        + (src[(i * SIZE) + 1] << 8)
+        + (src[(i * SIZE) + 2] << 16)
+        + (src[(i * SIZE) + 3] << 24)
+    )
+
+
+@micropython.viper
+def set_index(dest: ptr32, i: int, val: uint):
+    saved = dest
+    dest[i] = val
+    assert int(dest) == int(saved)
+
+
+try:
+    buffer = bytearray((((1 << max(BIT_THRESHOLDS)) // 1024) + 1) * 1024)
+
+    for bit in BIT_THRESHOLDS:
+        offset = (1 << bit) - (2 * SIZE)
+        for index in range(0, 3 * SIZE, SIZE):
+            exec(SET_TEMPLATE.format(off=(offset + index) // SIZE, val=next_value()))
+except MemoryError:
+    print("SKIP-TOO-LARGE")
+    raise SystemExit
+
+
+for bit in BIT_THRESHOLDS:
+    print("---", bit)
+    offset = (1 << bit) - (2 * SIZE)
+    for index in range(0, 3 * SIZE, SIZE):
+        globals()["set{}".format((offset + index) // SIZE)](buffer)
+        print(hex(get_index(buffer, (offset + index) // SIZE)))
+    for index in range(0, 3 * SIZE, SIZE):
+        set_index(buffer, (offset + index) // SIZE, next_value())
+        print(hex(get_index(buffer, (offset + index) // SIZE)))
diff --git a/tests/micropython/viper_ptr32_store_boundary.py.exp b/tests/micropython/viper_ptr32_store_boundary.py.exp
new file mode 100644
index 00000000000..7a9a5162474
--- /dev/null
+++ b/tests/micropython/viper_ptr32_store_boundary.py.exp
@@ -0,0 +1,28 @@
+--- 5
+0x1
+0x102
+0x10203
+0xa0b0c0d
+0xb0c0d0e
+0xc0d0e0f
+--- 8
+0x1020304
+0x2030405
+0x3040506
+0xd0e0f10
+0xe0f1011
+0xf101112
+--- 11
+0x4050607
+0x5060708
+0x6070809
+0x10111213
+0x11121314
+0x12131415
+--- 12
+0x708090a
+0x8090a0b
+0x90a0b0c
+0x13141516
+0x14151617
+0x15161718
diff --git a/tests/micropython/viper_ptr8_load_boundary.py b/tests/micropython/viper_ptr8_load_boundary.py
new file mode 100644
index 00000000000..57e06da5709
--- /dev/null
+++ b/tests/micropython/viper_ptr8_load_boundary.py
@@ -0,0 +1,38 @@
+# Test boundary conditions for various architectures
+
+GET_TEMPLATE = """
+@micropython.viper
+def get{off}(src: ptr8) -> uint:
+    return uint(src[{off}])
+print(hex(get{off}(buffer)))
+"""
+
+
+BIT_THRESHOLDS = (5, 8, 11, 12)
+SIZE = 1
+
+
+@micropython.viper
+def get_index(src: ptr8, i: int) -> int:
+    return src[i]
+
+
+def data(start, len):
+    output = bytearray(len)
+    for idx in range(len):
+        output[idx] = (start + idx) & 0xFF
+    return output
+
+
+buffer = bytearray((((1 << max(BIT_THRESHOLDS)) + 1) // 1024) * 1024)
+val = 0
+for bit in BIT_THRESHOLDS:
+    print("---", bit)
+    pre, idx, post = (((1 << bit) - (2 * SIZE)), ((1 << bit) - (1 * SIZE)), (1 << bit))
+    buffer[pre:post] = data(val, 3 * SIZE)
+    val = val + (3 * SIZE)
+
+    print(hex(get_index(buffer, pre)), hex(get_index(buffer, idx)), hex(get_index(buffer, post)))
+    exec(GET_TEMPLATE.format(off=pre))
+    exec(GET_TEMPLATE.format(off=idx))
+    exec(GET_TEMPLATE.format(off=post))
diff --git a/tests/micropython/viper_ptr8_load_boundary.py.exp b/tests/micropython/viper_ptr8_load_boundary.py.exp
new file mode 100644
index 00000000000..a0e423686b8
--- /dev/null
+++ b/tests/micropython/viper_ptr8_load_boundary.py.exp
@@ -0,0 +1,20 @@
+--- 5
+0x0 0x1 0x2
+0x0
+0x1
+0x2
+--- 8
+0x3 0x4 0x5
+0x3
+0x4
+0x5
+--- 11
+0x6 0x7 0x8
+0x6
+0x7
+0x8
+--- 12
+0x9 0xa 0xb
+0x9
+0xa
+0xb
diff --git a/tests/micropython/viper_ptr8_store_boundary.py b/tests/micropython/viper_ptr8_store_boundary.py
new file mode 100644
index 00000000000..68b76fd598b
--- /dev/null
+++ b/tests/micropython/viper_ptr8_store_boundary.py
@@ -0,0 +1,63 @@
+# Test boundary conditions for various architectures
+
+SET_TEMPLATE = """
+@micropython.viper
+def set{off}(dest: ptr8):
+    saved = dest
+    dest[{off}] = {val}
+    assert int(saved) == int(dest)
+"""
+
+BIT_THRESHOLDS = (5, 8, 11, 12)
+SIZE = 1
+MASK = (1 << (8 * SIZE)) - 1
+
+next_int = 1
+test_buffer = bytearray(SIZE)
+
+
+def next_value() -> uint:
+    global next_int
+    global test_buffer
+    for index in range(1, SIZE):
+        test_buffer[index - 1] = test_buffer[index]
+    test_buffer[SIZE - 1] = next_int
+    next_int += 1
+    output = 0
+    for byte in test_buffer:
+        output = (output << 8) | byte
+    return output & MASK
+
+
+def get_index(src: ptr8, i: int):
+    return src[i]
+
+
+@micropython.viper
+def set_index(dest: ptr8, i: int, val: uint):
+    saved = dest
+    dest[i] = val
+    assert int(dest) == int(saved)
+
+
+try:
+    buffer = bytearray((((1 << max(BIT_THRESHOLDS)) // 1024) + 1) * 1024)
+
+    for bit in BIT_THRESHOLDS:
+        offset = (1 << bit) - (2 * SIZE)
+        for index in range(0, 3 * SIZE, SIZE):
+            exec(SET_TEMPLATE.format(off=(offset + index) // SIZE, val=next_value()))
+except MemoryError:
+    print("SKIP-TOO-LARGE")
+    raise SystemExit
+
+
+for bit in BIT_THRESHOLDS:
+    print("---", bit)
+    offset = (1 << bit) - (2 * SIZE)
+    for index in range(0, 3 * SIZE, SIZE):
+        globals()["set{}".format((offset + index) // SIZE)](buffer)
+        print(hex(get_index(buffer, (offset + index) // SIZE)))
+    for index in range(0, 3 * SIZE, SIZE):
+        set_index(buffer, (offset + index) // SIZE, next_value())
+        print(hex(get_index(buffer, (offset + index) // SIZE)))
diff --git a/tests/micropython/viper_ptr8_store_boundary.py.exp b/tests/micropython/viper_ptr8_store_boundary.py.exp
new file mode 100644
index 00000000000..621295d81a8
--- /dev/null
+++ b/tests/micropython/viper_ptr8_store_boundary.py.exp
@@ -0,0 +1,28 @@
+--- 5
+0x1
+0x2
+0x3
+0xd
+0xe
+0xf
+--- 8
+0x4
+0x5
+0x6
+0x10
+0x11
+0x12
+--- 11
+0x7
+0x8
+0x9
+0x13
+0x14
+0x15
+--- 12
+0xa
+0xb
+0xc
+0x16
+0x17
+0x18
diff --git a/tests/micropython/viper_unop.py b/tests/micropython/viper_unop.py
new file mode 100644
index 00000000000..80c247a581e
--- /dev/null
+++ b/tests/micropython/viper_unop.py
@@ -0,0 +1,64 @@
+# test unary operators
+
+
+@micropython.viper
+def pos(x: int) -> int:
+    return +x
+
+
+print(pos(0))
+print(pos(1))
+print(pos(-2))
+
+
+@micropython.viper
+def neg(x: int) -> int:
+    return -x
+
+
+print(neg(0))
+print(neg(1))
+print(neg(-2))
+
+
+@micropython.viper
+def inv(x: int) -> int:
+    return ~x
+
+
+print(inv(0))
+print(inv(1))
+print(inv(-2))
+
+
+@micropython.viper
+def posc() -> int:
+    x = -1
+    x1 = +x
+    assert x == -1
+    return x1
+
+
+print(posc())
+
+
+@micropython.viper
+def negc() -> int:
+    x = -1
+    x1 = -x
+    assert x == -1
+    return x1
+
+
+print(negc())
+
+
+@micropython.viper
+def invc() -> int:
+    x = -1
+    x1 = ~x
+    assert x == -1
+    return x1
+
+
+print(invc())
diff --git a/tests/micropython/viper_unop.py.exp b/tests/micropython/viper_unop.py.exp
new file mode 100644
index 00000000000..9e9a5f4715f
--- /dev/null
+++ b/tests/micropython/viper_unop.py.exp
@@ -0,0 +1,12 @@
+0
+1
+-2
+0
+-1
+2
+-1
+-2
+1
+-1
+1
+0
diff --git a/tests/micropython/viper_with.py b/tests/micropython/viper_with.py
index 40fbf6fb315..833a6babe7d 100644
--- a/tests/micropython/viper_with.py
+++ b/tests/micropython/viper_with.py
@@ -9,6 +9,7 @@ def __enter__(self):
         print("__enter__")
 
     def __exit__(self, a, b, c):
+        b = repr(b)[:10]  # shorten exception to "NameError(" prefix for target compatibility
         print("__exit__", a, b, c)
 
 
diff --git a/tests/micropython/viper_with.py.exp b/tests/micropython/viper_with.py.exp
index 7e28663f6fc..b8083a4c66b 100644
--- a/tests/micropython/viper_with.py.exp
+++ b/tests/micropython/viper_with.py.exp
@@ -5,5 +5,5 @@ __exit__ None None None
 __init__
 __enter__
 1
-__exit__  name 'fail' isn't defined None
+__exit__  NameError( None
 NameError
diff --git a/tests/misc/cexample_class.py b/tests/misc/cexample_class.py
index 6b8718ad8cc..06d741922d2 100644
--- a/tests/misc/cexample_class.py
+++ b/tests/misc/cexample_class.py
@@ -22,3 +22,20 @@
 print(timer)
 print(0 <= t_start <= TOLERANCE_MS)
 print(SLEEP_MS - TOLERANCE_MS <= t_end <= SLEEP_MS + TOLERANCE_MS)
+
+advanced_timer = cexample.AdvancedTimer()
+
+time.sleep_ms(100)
+
+print(repr(advanced_timer))
+print(str(advanced_timer))
+
+print(advanced_timer.seconds)
+advanced_timer.seconds = 123
+print(advanced_timer.seconds)
+print(advanced_timer.time() < 123000 + TOLERANCE_MS)
+
+try:
+    advanced_timer.seconds = "bad input"
+except TypeError:
+    print("TypeError")
diff --git a/tests/misc/cexample_class.py.exp b/tests/misc/cexample_class.py.exp
index b9a06602a31..a86d4d14f78 100644
--- a/tests/misc/cexample_class.py.exp
+++ b/tests/misc/cexample_class.py.exp
@@ -1,3 +1,9 @@
 
 True
 True
+AdvancedTimer()
+AdvancedTimer()  # created 0 seconds ago
+0
+123
+True
+TypeError
diff --git a/tests/misc/cexample_module.py b/tests/misc/cexample_module.py
index c1da2ecf7ab..979c1fa24b3 100644
--- a/tests/misc/cexample_module.py
+++ b/tests/misc/cexample_module.py
@@ -12,5 +12,6 @@
 d = dir(cexample)
 d.index("add_ints")
 d.index("Timer")
+d.index("AdvancedTimer")
 
 print(cexample.add_ints(1, 3))
diff --git a/tests/misc/cexample_subclass.py b/tests/misc/cexample_subclass.py
new file mode 100644
index 00000000000..6857788e5dc
--- /dev/null
+++ b/tests/misc/cexample_subclass.py
@@ -0,0 +1,38 @@
+# test subclassing custom native class
+
+try:
+    from cexample import AdvancedTimer
+    import time  # used to skip this test on minimal unix variant
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+class SubTimer(AdvancedTimer):
+    def __init__(self):
+        # At this point, self does not yet represent a AdvancedTimer instance.
+        print(self)
+
+        # So lookups via type.attr handler will fail.
+        try:
+            self.seconds
+        except AttributeError:
+            print("AttributeError")
+
+        # Also applies to builtin methods.
+        try:
+            self.time()
+        except AttributeError:
+            print("AttributeError")
+
+        # Initialize base class.
+        super().__init__(self)
+
+        # Now you can access methods and attributes normally.
+        self.time()
+        print(self.seconds)
+        self.seconds = 123
+        print(self.seconds)
+
+
+watch = SubTimer()
diff --git a/tests/misc/cexample_subclass.py.exp b/tests/misc/cexample_subclass.py.exp
new file mode 100644
index 00000000000..a035649e475
--- /dev/null
+++ b/tests/misc/cexample_subclass.py.exp
@@ -0,0 +1,5 @@
+
+AttributeError
+AttributeError
+0
+123
diff --git a/tests/misc/features.py b/tests/misc/features.py
index 455b44fb1ef..3db88118144 100644
--- a/tests/misc/features.py
+++ b/tests/misc/features.py
@@ -103,7 +103,7 @@ class c25:
 
         x = c25()
         print(x.x)
-        raise
+        raise Exception
     except:
         print(26)
     print(27 + zero)
diff --git a/tests/misc/non_compliant.py b/tests/misc/non_compliant.py
index cc63185b5d2..54ad2960e49 100644
--- a/tests/misc/non_compliant.py
+++ b/tests/misc/non_compliant.py
@@ -1,8 +1,8 @@
 # tests for things that are not implemented, or have non-compliant behaviour
 
 try:
-    import uarray as array
-    import ustruct
+    import array
+    import struct
 except ImportError:
     print("SKIP")
     raise SystemExit
@@ -39,7 +39,7 @@
 except NotImplementedError:
     print("NotImplementedError")
 
-# uPy raises TypeError, should be ValueError
+# MicroPython raises TypeError, should be ValueError
 try:
     "%c" % b"\x01\x02"
 except (TypeError, ValueError):
@@ -63,12 +63,6 @@
 except NotImplementedError:
     print("NotImplementedError")
 
-# str.endswith(s, start) not implemented
-try:
-    "abc".endswith("c", 1)
-except NotImplementedError:
-    print("NotImplementedError")
-
 # str subscr with step!=1 not implemented
 try:
     print("abc"[1:2:3])
@@ -105,11 +99,11 @@
 except NotImplementedError:
     print("NotImplementedError")
 
-# struct pack with too many args, not checked by uPy
-print(ustruct.pack("bb", 1, 2, 3))
+# struct pack with too many args, not checked by MicroPython
+print(struct.pack("bb", 1, 2, 3))
 
-# struct pack with too few args, not checked by uPy
-print(ustruct.pack("bb", 1))
+# struct pack with too few args, not checked by MicroPython
+print(struct.pack("bb", 1))
 
 # array slice assignment with unsupported RHS
 try:
diff --git a/tests/misc/non_compliant.py.exp b/tests/misc/non_compliant.py.exp
index 3f15a144060..deefe78a712 100644
--- a/tests/misc/non_compliant.py.exp
+++ b/tests/misc/non_compliant.py.exp
@@ -13,7 +13,6 @@ NotImplementedError
 NotImplementedError
 NotImplementedError
 NotImplementedError
-NotImplementedError
 b'\x01\x02'
 b'\x01\x00'
 NotImplementedError
diff --git a/tests/misc/non_compliant_lexer.py b/tests/misc/non_compliant_lexer.py
index e1c21f3d713..04c605953e7 100644
--- a/tests/misc/non_compliant_lexer.py
+++ b/tests/misc/non_compliant_lexer.py
@@ -11,7 +11,7 @@ def test(code):
         print("NotImplementedError")
 
 
-# uPy requires spaces between literal numbers and keywords, CPy doesn't
+# MPy requires spaces between literal numbers and keywords, CPy doesn't
 try:
     eval("1and 0")
 except SyntaxError:
diff --git a/tests/misc/print_exception.py b/tests/misc/print_exception.py
index 06f4023349a..fdf5711c578 100644
--- a/tests/misc/print_exception.py
+++ b/tests/misc/print_exception.py
@@ -1,10 +1,8 @@
+# Test sys.print_exception (MicroPython) / traceback.print_exception (CPython).
+
 try:
-    try:
-        import uio as io
-        import usys as sys
-    except ImportError:
-        import io
-        import sys
+    import io
+    import sys
 except ImportError:
     print("SKIP")
     raise SystemExit
@@ -22,11 +20,11 @@ def print_exc(e):
     print_exception(e, buf)
     s = buf.getvalue()
     for l in s.split("\n"):
-        # uPy on pyboard prints  as file, so remove filename.
+        # MPy on pyboard prints  as file, so remove filename.
         if l.startswith("  File "):
             l = l.split('"')
             print(l[0], l[2])
-        # uPy and CPy tracebacks differ in that CPy prints a source line for
+        # MPy and CPy tracebacks differ in that CPy prints a source line for
         # each traceback entry. In this case, we know that offending line
         # has 4-space indent, so filter it out.
         elif not l.startswith("    "):
@@ -73,7 +71,7 @@ def g():
     except Exception as e:
         print("reraise")
         print_exc(e)
-        raise
+        raise e
 except Exception as e:
     print("caught")
     print_exc(e)
@@ -91,7 +89,7 @@ def f():
 except Exception as e:
     print_exc(e)
 
-# Test non-stream object passed as output object, only valid for uPy
+# Test non-stream object passed as output object, only valid for MicroPython
 if hasattr(sys, "print_exception"):
     try:
         sys.print_exception(Exception, 1)
diff --git a/tests/misc/print_exception.py.native.exp b/tests/misc/print_exception.py.native.exp
new file mode 100644
index 00000000000..59e856ae3c4
--- /dev/null
+++ b/tests/misc/print_exception.py.native.exp
@@ -0,0 +1,18 @@
+caught
+Exception: msg
+
+caught
+Exception: fail
+
+finally
+caught
+Exception: fail
+
+reraise
+Exception: fail
+
+caught
+Exception: fail
+
+AttributeError: 'function' object has no attribute 'X'
+
diff --git a/tests/misc/rge_sm.py b/tests/misc/rge_sm.py
index 00b0a7a0216..9e74c3b7a79 100644
--- a/tests/misc/rge_sm.py
+++ b/tests/misc/rge_sm.py
@@ -39,14 +39,6 @@ def solve(self, finishtime):
             if not self.iterate():
                 break
 
-    def solveNSteps(self, nSteps):
-        for i in range(nSteps):
-            if not self.iterate():
-                break
-
-    def series(self):
-        return zip(*self.Trajectory)
-
 
 # 1-loop RGES for the main parameters of the SM
 # couplings are: g1, g2, g3 of U(1), SU(2), SU(3); yt (top Yukawa), lambda (Higgs quartic)
@@ -79,63 +71,32 @@ def series(self):
 )
 
 
-def drange(start, stop, step):
-    r = start
-    while r < stop:
-        yield r
-        r += step
-
-
-def phaseDiagram(system, trajStart, trajPlot, h=0.1, tend=1.0, range=1.0):
-    tstart = 0.0
-    for i in drange(0, range, 0.1 * range):
-        for j in drange(0, range, 0.1 * range):
-            rk = RungeKutta(system, trajStart(i, j), tstart, h)
-            rk.solve(tend)
-            # draw the line
-            for tr in rk.Trajectory:
-                x, y = trajPlot(tr)
-                print(x, y)
-            print()
-            # draw the arrow
-            continue
-            l = (len(rk.Trajectory) - 1) / 3
-            if l > 0 and 2 * l < len(rk.Trajectory):
-                p1 = rk.Trajectory[l]
-                p2 = rk.Trajectory[2 * l]
-                x1, y1 = trajPlot(p1)
-                x2, y2 = trajPlot(p2)
-                dx = -0.5 * (y2 - y1)  # orthogonal to line
-                dy = 0.5 * (x2 - x1)  # orthogonal to line
-                # l = math.sqrt(dx*dx + dy*dy)
-                # if abs(l) > 1e-3:
-                #    l = 0.1 / l
-                #    dx *= l
-                #    dy *= l
-                print(x1 + dx, y1 + dy)
-                print(x2, y2)
-                print(x1 - dx, y1 - dy)
-                print()
-
-
 def singleTraj(system, trajStart, h=0.02, tend=1.0):
+    is_REPR_C = float("1.0000001") == float("1.0")
     tstart = 0.0
 
     # compute the trajectory
 
     rk = RungeKutta(system, trajStart, tstart, h)
-    rk.solve(tend)
+    try:
+        rk.solve(tend)
+    except MemoryError:
+        print("SKIP")
+        raise SystemExit
 
     # print out trajectory
 
     for i in range(len(rk.Trajectory)):
         tr = rk.Trajectory[i]
-        print(" ".join(["{:.4f}".format(t) for t in tr]))
-
+        tr_str = " ".join(["{:.4f}".format(t) for t in tr])
+        if is_REPR_C:
+            # allow two small deviations for REPR_C
+            if tr_str == "1.0000 0.3559 0.6485 1.1944 0.9271 0.1083":
+                tr_str = "1.0000 0.3559 0.6485 1.1944 0.9272 0.1083"
+            if tr_str == "16.0000 0.3894 0.5793 0.7017 0.5686 -0.0168":
+                tr_str = "16.0000 0.3894 0.5793 0.7017 0.5686 -0.0167"
+        print(tr_str)
 
-# phaseDiagram(sysSM, (lambda i, j: [0.354, 0.654, 1.278, 0.8 + 0.2 * i, 0.1 + 0.1 * j]), (lambda a: (a[4], a[5])), h=0.1, tend=math.log(10**17))
 
 # initial conditions at M_Z
-singleTraj(
-    sysSM, [0.354, 0.654, 1.278, 0.983, 0.131], h=0.5, tend=math.log(10**17)
-)  # true values
+singleTraj(sysSM, [0.354, 0.654, 1.278, 0.983, 0.131], h=0.5, tend=math.log(10**17))  # true values
diff --git a/tests/misc/sys_atexit.py b/tests/misc/sys_atexit.py
index 141b24cc9f6..e9c5693f975 100644
--- a/tests/misc/sys_atexit.py
+++ b/tests/misc/sys_atexit.py
@@ -1,9 +1,9 @@
 # test sys.atexit() function
 
-import usys
+import sys
 
 try:
-    usys.atexit
+    sys.atexit
 except AttributeError:
     print("SKIP")
     raise SystemExit
@@ -15,7 +15,7 @@ def do_at_exit():
     print("done at exit:", some_var)
 
 
-usys.atexit(do_at_exit)
+sys.atexit(do_at_exit)
 
 some_var = "ok"
 print("done before exit")
diff --git a/tests/misc/sys_exc_info.py b/tests/misc/sys_exc_info.py
index 3a8c4a6c8d3..c076dd572b0 100644
--- a/tests/misc/sys_exc_info.py
+++ b/tests/misc/sys_exc_info.py
@@ -1,7 +1,4 @@
-try:
-    import usys as sys
-except ImportError:
-    import sys
+import sys
 
 try:
     sys.exc_info
@@ -11,13 +8,14 @@
 
 
 def f():
-    print(sys.exc_info()[0:2])
+    e = sys.exc_info()
+    print(e[0], e[1])
 
 
 try:
     raise ValueError("value", 123)
 except:
-    print(sys.exc_info()[0:2])
+    print(sys.exc_info()[0], sys.exc_info()[1])
     f()
 
 # Outside except block, sys.exc_info() should be back to None's
diff --git a/tests/misc/sys_settrace_cov.py b/tests/misc/sys_settrace_cov.py
new file mode 100644
index 00000000000..579c8a4a25e
--- /dev/null
+++ b/tests/misc/sys_settrace_cov.py
@@ -0,0 +1,23 @@
+import sys
+
+try:
+    sys.settrace
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+
+def trace_tick_handler(frame, event, arg):
+    print("FRAME", frame)
+    print("LASTI", frame.f_lasti)
+    return None
+
+
+def f():
+    x = 3
+    return x
+
+
+sys.settrace(trace_tick_handler)
+f()
+sys.settrace(None)
diff --git a/tests/misc/sys_settrace_cov.py.exp b/tests/misc/sys_settrace_cov.py.exp
new file mode 100644
index 00000000000..423d78ec42b
--- /dev/null
+++ b/tests/misc/sys_settrace_cov.py.exp
@@ -0,0 +1,2 @@
+FRAME 
+LASTI \\d\+
diff --git a/tests/misc/sys_settrace_features.py b/tests/misc/sys_settrace_features.py
index 8a38b7534f7..6eeb2b900f1 100644
--- a/tests/misc/sys_settrace_features.py
+++ b/tests/misc/sys_settrace_features.py
@@ -6,6 +6,10 @@
     print("SKIP")
     raise SystemExit
 
+if sys.version.startswith("3.12"):
+    # There is a CPython change in settrace that is reverted in 3.13!
+    print("WARNING: this test will fail when compared to CPython 3.12.x behaviour")
+
 
 def print_stacktrace(frame, level=0):
     # Ignore CPython specific helpers.
@@ -22,7 +26,9 @@ def print_stacktrace(frame, level=0):
             frame.f_code.co_name,
             # Keep just the filename.
             "sys_settrace_" + frame.f_code.co_filename.split("sys_settrace_")[-1],
-            frame.f_lineno,
+            max(
+                1, frame.f_lineno
+            ),  # CPython 3.11 emits `0` where CPython 3.6 emits `1` for the "call" event corresponding to import.
         )
     )
 
@@ -65,6 +71,11 @@ def trace_tick_handler(frame, event, arg):
     if any(name in frame_name for name in to_ignore):
         return
 
+    # Lines 4,5,7 create the `const` lambda, and line `15` is a `_X = const()` which
+    # MicroPython will not see as it's optimised out.
+    if "sys_settrace_importme" in frame.f_code.co_filename and frame.f_lineno in (4, 5, 7, 15):
+        return trace_tick_handler
+
     print("### trace_handler::main event:", event)
     __prof__.trace_tick(frame, event, arg)
 
diff --git a/tests/misc/sys_settrace_subdir/sys_settrace_importme.py b/tests/misc/sys_settrace_subdir/sys_settrace_importme.py
index de561ef2176..fdfa06134e4 100644
--- a/tests/misc/sys_settrace_subdir/sys_settrace_importme.py
+++ b/tests/misc/sys_settrace_subdir/sys_settrace_importme.py
@@ -3,12 +3,18 @@
 try:
     x = const(1)
 except NameError:
-    print("const not defined")
+    # Either running on CPython or MICROPY_COMP_CONST disabled.
+    const = lambda x: x
 
-const = lambda x: x
 
+# No const optimisation.
 _CNT01 = "CONST01"
+
+# Const assigned to an underscore name. Invisible to MicroPython with
+# MICROPY_COMP_CONST enabled.
 _CNT02 = const(123)
+
+# Consts assigned to regular name, executed normally.
 A123 = const(123)
 a123 = const(123)
 
diff --git a/tests/multi_bluetooth/ble_deepsleep.py b/tests/multi_bluetooth/ble_deepsleep.py
deleted file mode 100644
index 7c34c036067..00000000000
--- a/tests/multi_bluetooth/ble_deepsleep.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# Test BLE re-connect after peripheral deepsleep.
-# instance0 needs to be a device capable of deepsleep without disconnecting the
-# REPL, eg. STM32WB55 Nucleo board with REPL on UART port.
-
-from micropython import const
-import time, machine, bluetooth
-
-TIMEOUT_MS = 4000
-
-_IRQ_CENTRAL_CONNECT = const(1)
-_IRQ_CENTRAL_DISCONNECT = const(2)
-_IRQ_PERIPHERAL_CONNECT = const(7)
-_IRQ_PERIPHERAL_DISCONNECT = const(8)
-
-waiting_events = {}
-
-
-def irq(event, data):
-    if event == _IRQ_CENTRAL_CONNECT:
-        print("_IRQ_CENTRAL_CONNECT")
-        waiting_events[event] = data[0]
-    elif event == _IRQ_CENTRAL_DISCONNECT:
-        print("_IRQ_CENTRAL_DISCONNECT")
-    elif event == _IRQ_PERIPHERAL_CONNECT:
-        print("_IRQ_PERIPHERAL_CONNECT")
-        waiting_events[event] = data[0]
-    elif event == _IRQ_PERIPHERAL_DISCONNECT:
-        print("_IRQ_PERIPHERAL_DISCONNECT")
-
-    if event not in waiting_events:
-        waiting_events[event] = None
-
-
-def wait_for_event(event, timeout_ms):
-    t0 = time.ticks_ms()
-    while time.ticks_diff(time.ticks_ms(), t0) < timeout_ms:
-        if event in waiting_events:
-            return waiting_events.pop(event)
-        machine.idle()
-    raise ValueError("Timeout waiting for {}".format(event))
-
-
-# Acting in peripheral role.
-def instance0():
-    multitest.globals(BDADDR=ble.config("mac"))
-    print("gap_advertise")
-    ble.gap_advertise(20_000, b"\x02\x01\x06\x04\tMPY")
-    multitest.next()
-    # Wait for central to connect, then wait for it to disconnect.
-    wait_for_event(_IRQ_CENTRAL_CONNECT, TIMEOUT_MS)
-    wait_for_event(_IRQ_CENTRAL_DISCONNECT, TIMEOUT_MS)
-
-    multitest.expect_reboot(resume="instance0_resume", delay_ms=100)
-    machine.deepsleep(100)
-
-
-# Acting in peripheral role.
-def instance0_resume():
-    # continue here after reboot
-    print("restarted")
-    print("gap_advertise")
-    ble.gap_advertise(20_000, b"\x02\x01\x06\x04\tMPY")
-    try:
-        # Wait for central to connect, then disconnect it.
-        conn_handle = wait_for_event(_IRQ_CENTRAL_CONNECT, TIMEOUT_MS)
-        print("gap_disconnect:", ble.gap_disconnect(conn_handle))
-        wait_for_event(_IRQ_CENTRAL_DISCONNECT, TIMEOUT_MS)
-    finally:
-        ble.active(0)
-
-
-# Acting in central role.
-def instance1():
-    multitest.next()
-    try:
-        # Connect to peripheral and then disconnect.
-        print("gap_connect")
-        ble.gap_connect(*BDADDR)
-        conn_handle = wait_for_event(_IRQ_PERIPHERAL_CONNECT, TIMEOUT_MS)
-        print("gap_disconnect:", ble.gap_disconnect(conn_handle))
-        wait_for_event(_IRQ_PERIPHERAL_DISCONNECT, TIMEOUT_MS)
-
-        # Connect to peripheral and then let the peripheral disconnect us.
-        # Extra scan timeout allows for the peripheral to receive the disconnect
-        # event and start advertising again.
-        print("gap_connect")
-        ble.gap_connect(BDADDR[0], BDADDR[1], 5000)
-        wait_for_event(_IRQ_PERIPHERAL_CONNECT, TIMEOUT_MS)
-        wait_for_event(_IRQ_PERIPHERAL_DISCONNECT, TIMEOUT_MS)
-    finally:
-        ble.active(0)
-
-
-ble = bluetooth.BLE()
-ble.active(1)
-ble.irq(irq)
diff --git a/tests/multi_bluetooth/ble_irq_calls.py b/tests/multi_bluetooth/ble_irq_calls.py
new file mode 100644
index 00000000000..2717bd5f3da
--- /dev/null
+++ b/tests/multi_bluetooth/ble_irq_calls.py
@@ -0,0 +1,224 @@
+# Test calling BLE methods from within the BLE.irq event handler.
+
+from micropython import const
+import struct
+import time
+import bluetooth
+
+_IRQ_CENTRAL_CONNECT = const(1)
+_IRQ_CENTRAL_DISCONNECT = const(2)
+_IRQ_PERIPHERAL_CONNECT = const(7)
+_IRQ_PERIPHERAL_DISCONNECT = const(8)
+_IRQ_GATTC_SERVICE_RESULT = const(9)
+_IRQ_GATTC_SERVICE_DONE = const(10)
+_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
+_IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
+_IRQ_GATTC_DESCRIPTOR_RESULT = const(13)
+_IRQ_GATTC_DESCRIPTOR_DONE = const(14)
+_IRQ_GATTC_READ_RESULT = const(15)
+_IRQ_GATTC_READ_DONE = const(16)
+_IRQ_GATTC_WRITE_DONE = const(17)
+_IRQ_MTU_EXCHANGED = const(21)
+_IRQ_GET_SECRET = const(29)
+_IRQ_SET_SECRET = const(30)
+
+EVENT_NAMES = {
+    1: "_IRQ_CENTRAL_CONNECT",
+    2: "_IRQ_CENTRAL_DISCONNECT",
+    3: "_IRQ_GATTS_WRITE",
+    4: "_IRQ_GATTS_READ_REQUEST",
+    7: "_IRQ_PERIPHERAL_CONNECT",
+    8: "_IRQ_PERIPHERAL_DISCONNECT",
+    9: "_IRQ_GATTC_SERVICE_RESULT",
+    10: "_IRQ_GATTC_SERVICE_DONE",
+    11: "_IRQ_GATTC_CHARACTERISTIC_RESULT",
+    12: "_IRQ_GATTC_CHARACTERISTIC_DONE",
+    13: "_IRQ_GATTC_DESCRIPTOR_RESULT",
+    14: "_IRQ_GATTC_DESCRIPTOR_DONE",
+    15: "_IRQ_GATTC_READ_RESULT",
+    16: "_IRQ_GATTC_READ_DONE",
+    17: "_IRQ_GATTC_WRITE_DONE",
+    18: "_IRQ_GATTC_NOTIFY",
+    21: "_IRQ_MTU_EXCHANGED",
+}
+
+_ADV_TYPE_FLAGS = const(0x01)
+_ADV_TYPE_NAME = const(0x09)
+_ADV_TYPE_UUID128_COMPLETE = const(0x7)
+
+_NOTIFY_ENABLE = const(1)
+
+ACCESSORY_UUID = bluetooth.UUID("a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a")
+STATE_UUID = bluetooth.UUID("a5a5a5a5-eeee-9999-1111-5a5a5a5a5a5a")
+CCC_UUID = bluetooth.UUID(0x2902)
+
+STATE_CHARACTERISTIC = (
+    STATE_UUID,
+    bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,
+)
+
+ACCESSORY_SERVICE = (ACCESSORY_UUID, (STATE_CHARACTERISTIC,))
+
+
+def recursive_function():
+    recursive_function()
+
+
+def test_deep_calls(n):
+    if n > 0:
+        # Test that a few nested calls will succeed.
+        test_deep_calls(n - 1)
+    else:
+        # Test that a Python stack overflow is detected.
+        try:
+            recursive_function()
+        except RuntimeError:
+            print("test_deep_calls finished")
+
+
+class Central:
+    def __init__(self):
+        self.done = False
+        self._conn_handle = None
+        self._service = None
+        self._characteristic_handle = None
+        self._cccd_handle = None
+        self._reads_remaining = None
+        ble.active(1)
+        ble.irq(self._ble_event_handler)
+        ble.gap_connect(*BDADDR)
+
+    def _ble_event_handler(self, event, data):
+        print(EVENT_NAMES[event])
+
+        if event == _IRQ_PERIPHERAL_CONNECT:
+            conn_handle, _, _ = data
+            self._conn_handle = conn_handle
+            ble.gattc_discover_services(self._conn_handle, ACCESSORY_UUID)
+            # Test deep Python calls from within this BLE IRQ handler.
+            test_deep_calls(5)
+
+        elif event == _IRQ_PERIPHERAL_DISCONNECT:
+            conn_handle, _, addr = data
+            assert self._conn_handle == conn_handle
+            self._conn_handle = None
+            print("connection closed")
+
+        elif event == _IRQ_GATTC_SERVICE_RESULT:
+            _, first_handle, last_handle, uuid = data
+            print("service found:", last_handle - first_handle, uuid)
+            if uuid == ACCESSORY_UUID:
+                assert self._service is None
+                self._service = (first_handle, last_handle)
+
+        elif event == _IRQ_GATTC_SERVICE_DONE:
+            print("service handle range:", self._service[1] - self._service[0])
+            start_handle, end_handle = self._service
+            ble.gattc_discover_characteristics(self._conn_handle, start_handle, end_handle)
+
+        elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT:
+            _, end_handle, value_handle, properties, uuid = data
+            assert uuid == STATE_UUID
+            print("characteristic found:", uuid)
+            assert self._characteristic_handle is None
+            self._characteristic_handle = value_handle
+
+        elif event == _IRQ_GATTC_CHARACTERISTIC_DONE:
+            start_handle, end_handle = self._service
+            ble.gattc_discover_descriptors(self._conn_handle, start_handle, end_handle)
+
+        elif event == _IRQ_GATTC_DESCRIPTOR_RESULT:
+            _, dsc_handle, uuid = data
+            if uuid == CCC_UUID:
+                print("CCCD found:", uuid)
+                assert self._cccd_handle is None
+                self._cccd_handle = dsc_handle
+
+        elif event == _IRQ_GATTC_DESCRIPTOR_DONE:
+            # Discovery complete, proceed to MTU exchange.
+            ble.gattc_exchange_mtu(self._conn_handle)
+
+        elif event == _IRQ_MTU_EXCHANGED:
+            # MTU exchanged, proceed to enable CCCD.
+            print("CCCD write")
+            ble.gattc_write(
+                self._conn_handle, self._cccd_handle, struct.pack(" 0:
+                ble.gattc_read(self._conn_handle, self._characteristic_handle)
+            else:
+                self.done = True
+                ble.gap_disconnect(self._conn_handle)
+
+
+class Peripheral:
+    def __init__(self):
+        self.done = False
+        ble.active(1)
+        ble.irq(self._ble_event_handler)
+        ble.gatts_register_services((ACCESSORY_SERVICE,))
+        add_payload = self.advertising_payload("acc", (ACCESSORY_UUID,))
+        ble.gap_advertise(500000, add_payload)
+
+    def advertising_payload(self, name, services):
+        payload = bytearray()
+
+        def _append(adv_type, value):
+            nonlocal payload
+            payload.extend(struct.pack("BB", len(value) + 1, adv_type) + value)
+
+        _append(_ADV_TYPE_FLAGS, struct.pack("B", 0x02 + 0x04))
+        _append(_ADV_TYPE_NAME, name)
+
+        for uuid in services:
+            b = bytes(uuid)
+            assert len(b) == 16
+            _append(_ADV_TYPE_UUID128_COMPLETE, b)
+
+        return payload
+
+    def _ble_event_handler(self, event, data):
+        if event not in (_IRQ_GET_SECRET, _IRQ_SET_SECRET):
+            print(EVENT_NAMES[event])
+        if event == _IRQ_CENTRAL_DISCONNECT:
+            self.done = True
+
+
+# Acting in peripheral role.
+def instance0():
+    print("peripheral start")
+    peripheral = Peripheral()
+    multitest.globals(BDADDR=ble.config("mac"))
+    multitest.next()
+    while not peripheral.done:
+        time.sleep_ms(100)
+    multitest.broadcast("finished")
+    ble.active(0)
+
+
+# Acting in central role.
+def instance1():
+    print("central start")
+    multitest.next()
+    central = Central()
+    while not central.done:
+        time.sleep_ms(100)
+    multitest.wait("finished")
+    ble.active(0)
+
+
+ble = bluetooth.BLE()
diff --git a/tests/multi_bluetooth/ble_irq_calls.py.exp b/tests/multi_bluetooth/ble_irq_calls.py.exp
new file mode 100644
index 00000000000..993e6f79e8a
--- /dev/null
+++ b/tests/multi_bluetooth/ble_irq_calls.py.exp
@@ -0,0 +1,36 @@
+--- instance0 ---
+peripheral start
+_IRQ_CENTRAL_CONNECT
+_IRQ_MTU_EXCHANGED
+_IRQ_GATTS_READ_REQUEST
+_IRQ_GATTS_READ_REQUEST
+_IRQ_CENTRAL_DISCONNECT
+--- instance1 ---
+central start
+_IRQ_PERIPHERAL_CONNECT
+test_deep_calls finished
+_IRQ_GATTC_SERVICE_RESULT
+service found: 3 UUID('a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a')
+_IRQ_GATTC_SERVICE_DONE
+service handle range: 3
+_IRQ_GATTC_CHARACTERISTIC_RESULT
+characteristic found: UUID('a5a5a5a5-eeee-9999-1111-5a5a5a5a5a5a')
+_IRQ_GATTC_CHARACTERISTIC_DONE
+_IRQ_GATTC_DESCRIPTOR_RESULT
+_IRQ_GATTC_DESCRIPTOR_RESULT
+_IRQ_GATTC_DESCRIPTOR_RESULT
+CCCD found: UUID(0x2902)
+_IRQ_GATTC_DESCRIPTOR_DONE
+_IRQ_MTU_EXCHANGED
+CCCD write
+_IRQ_GATTC_WRITE_DONE
+CCCD write result: 0
+issue gattc_read
+_IRQ_GATTC_READ_RESULT
+gattc_read result: b''
+_IRQ_GATTC_READ_DONE
+_IRQ_GATTC_READ_RESULT
+gattc_read result: b''
+_IRQ_GATTC_READ_DONE
+_IRQ_PERIPHERAL_DISCONNECT
+connection closed
diff --git a/tests/multi_bluetooth/ble_mtu.py b/tests/multi_bluetooth/ble_mtu.py
index 5f00b270cc0..92a40fbb4bc 100644
--- a/tests/multi_bluetooth/ble_mtu.py
+++ b/tests/multi_bluetooth/ble_mtu.py
@@ -106,6 +106,9 @@ def instance0():
             # Wait for central to connect to us.
             conn_handle = wait_for_event(_IRQ_CENTRAL_CONNECT, TIMEOUT_MS)
 
+            # Inform the central that we have been connected to.
+            multitest.broadcast("peripheral is connected")
+
             mtu = wait_for_event(_IRQ_MTU_EXCHANGED, TIMEOUT_MS)
 
             multitest.wait(f"client:discovery:{i}")
@@ -138,6 +141,13 @@ def instance1():
             ble.gap_connect(BDADDR[0], BDADDR[1], TIMEOUT_MS)
             conn_handle = wait_for_event(_IRQ_PERIPHERAL_CONNECT, TIMEOUT_MS)
 
+            # Wait for peripheral to be ready for MTU exchange.
+            # On ESP32 with NimBLE and IDF 5.4.2 (at least), if the MTU exchange occurs
+            # immediately after the peripheral connect event, the peripheral may see its
+            # _IRQ_MTU_EXCHANGED event before its _IRQ_CENTRAL_CONNECT event.  The wait
+            # here ensures correct event ordering on the peripheral.
+            multitest.wait("peripheral is connected")
+
             # Central-initiated mtu exchange.
             print("gattc_exchange_mtu")
             ble.gattc_exchange_mtu(conn_handle)
diff --git a/tests/multi_bluetooth/ble_mtu_peripheral.py b/tests/multi_bluetooth/ble_mtu_peripheral.py
index 1c0de40a0c4..50882bcd79c 100644
--- a/tests/multi_bluetooth/ble_mtu_peripheral.py
+++ b/tests/multi_bluetooth/ble_mtu_peripheral.py
@@ -101,6 +101,13 @@ def instance0():
             # Wait for central to connect to us.
             conn_handle = wait_for_event(_IRQ_CENTRAL_CONNECT, TIMEOUT_MS)
 
+            # Wait for central to be ready for MTU exchange.
+            # On ESP32 with NimBLE and IDF 5.4.2 (at least), if the MTU exchange occurs
+            # immediately after the central connect event, the central may see its
+            # _IRQ_MTU_EXCHANGED event before its _IRQ_PERIPHERAL_CONNECT event.  The
+            # wait here ensures correct event ordering on the central.
+            multitest.wait("central is connected")
+
             # Peripheral-initiated mtu exchange.
             print("gattc_exchange_mtu")
             ble.gattc_exchange_mtu(conn_handle)
@@ -142,6 +149,9 @@ def instance1():
             ble.gap_connect(BDADDR[0], BDADDR[1], 5000)
             conn_handle = wait_for_event(_IRQ_PERIPHERAL_CONNECT, TIMEOUT_MS)
 
+            # Inform the peripheral that we have been connected to.
+            multitest.broadcast("central is connected")
+
             mtu = wait_for_event(_IRQ_MTU_EXCHANGED, TIMEOUT_MS)
 
             print("gattc_discover_characteristics")
diff --git a/tests/multi_bluetooth/perf_gatt_notify.py b/tests/multi_bluetooth/perf_gatt_notify.py
index 8acccc6470d..ac791a3ebfa 100644
--- a/tests/multi_bluetooth/perf_gatt_notify.py
+++ b/tests/multi_bluetooth/perf_gatt_notify.py
@@ -110,9 +110,9 @@ def instance1():
     ((char_handle,),) = ble.gatts_register_services(SERVICES)
     multitest.next()
     try:
-        # Connect to peripheral and then disconnect.
+        # Connect to peripheral, with a short connection interval to reduce notify latency.
         print("gap_connect")
-        ble.gap_connect(*BDADDR)
+        ble.gap_connect(BDADDR[0], BDADDR[1], 2000, 12500, 12500)
         conn_handle = wait_for_event(_IRQ_PERIPHERAL_CONNECT, TIMEOUT_MS)
 
         # Discover characteristics.
diff --git a/tests/multi_bluetooth/stress_deepsleep_reconnect.py b/tests/multi_bluetooth/stress_deepsleep_reconnect.py
new file mode 100644
index 00000000000..b588b4000b4
--- /dev/null
+++ b/tests/multi_bluetooth/stress_deepsleep_reconnect.py
@@ -0,0 +1,98 @@
+# Test BLE re-connect after peripheral deepsleep.
+# instance0 needs to be a device capable of deepsleep without disconnecting the
+# REPL, eg. STM32WB55 Nucleo board with REPL on UART port.
+
+from micropython import const
+import time, machine, bluetooth
+
+# Note: This value can be much lower most of the time, but an ESP32 with a boot.py
+# that connects to Wi-Fi may take an extra 5 seconds after reboot.
+TIMEOUT_MS = 8000
+
+_IRQ_CENTRAL_CONNECT = const(1)
+_IRQ_CENTRAL_DISCONNECT = const(2)
+_IRQ_PERIPHERAL_CONNECT = const(7)
+_IRQ_PERIPHERAL_DISCONNECT = const(8)
+
+waiting_events = {}
+
+
+def irq(event, data):
+    if event == _IRQ_CENTRAL_CONNECT:
+        print("_IRQ_CENTRAL_CONNECT")
+        waiting_events[event] = data[0]
+    elif event == _IRQ_CENTRAL_DISCONNECT:
+        print("_IRQ_CENTRAL_DISCONNECT")
+    elif event == _IRQ_PERIPHERAL_CONNECT:
+        print("_IRQ_PERIPHERAL_CONNECT")
+        waiting_events[event] = data[0]
+    elif event == _IRQ_PERIPHERAL_DISCONNECT:
+        print("_IRQ_PERIPHERAL_DISCONNECT")
+
+    if event not in waiting_events:
+        waiting_events[event] = None
+
+
+def wait_for_event(event, timeout_ms):
+    t0 = time.ticks_ms()
+    while time.ticks_diff(time.ticks_ms(), t0) < timeout_ms:
+        if event in waiting_events:
+            return waiting_events.pop(event)
+        machine.idle()
+    raise ValueError("Timeout waiting for {}".format(event))
+
+
+# Acting in peripheral role.
+def instance0():
+    multitest.globals(BDADDR=ble.config("mac"))
+    print("gap_advertise")
+    ble.gap_advertise(20_000, b"\x02\x01\x06\x04\tMPY")
+    multitest.next()
+    # Wait for central to connect, then wait for it to disconnect.
+    wait_for_event(_IRQ_CENTRAL_CONNECT, TIMEOUT_MS)
+    wait_for_event(_IRQ_CENTRAL_DISCONNECT, TIMEOUT_MS)
+
+    multitest.expect_reboot(resume="instance0_resume", delay_ms=100)
+    machine.deepsleep(100)
+
+
+# Acting in peripheral role.
+def instance0_resume():
+    # continue here after reboot
+    print("restarted")
+    print("gap_advertise")
+    ble.gap_advertise(20_000, b"\x02\x01\x06\x04\tMPY")
+    try:
+        # Wait for central to connect, then disconnect it.
+        conn_handle = wait_for_event(_IRQ_CENTRAL_CONNECT, TIMEOUT_MS)
+        print("gap_disconnect:", ble.gap_disconnect(conn_handle))
+        wait_for_event(_IRQ_CENTRAL_DISCONNECT, TIMEOUT_MS)
+    finally:
+        ble.active(0)
+
+
+# Acting in central role.
+def instance1():
+    multitest.next()
+    try:
+        # Connect to peripheral and then disconnect.
+        print("gap_connect")
+        ble.gap_connect(*BDADDR)
+        conn_handle = wait_for_event(_IRQ_PERIPHERAL_CONNECT, TIMEOUT_MS)
+        print("gap_disconnect:", ble.gap_disconnect(conn_handle))
+        wait_for_event(_IRQ_PERIPHERAL_DISCONNECT, TIMEOUT_MS)
+
+        # Connect to peripheral and then let the peripheral disconnect us.
+        # Extra scan timeout allows for the peripheral to receive the disconnect
+        # event and start advertising again.
+        print("gap_connect")
+        ble.gap_connect(BDADDR[0], BDADDR[1], 5000)
+        wait_for_event(_IRQ_PERIPHERAL_CONNECT, TIMEOUT_MS)
+        wait_for_event(_IRQ_PERIPHERAL_DISCONNECT, TIMEOUT_MS)
+    finally:
+        ble.active(0)
+
+
+ble = bluetooth.BLE()
+ble.active(1)
+ble.irq(irq)
diff --git a/tests/multi_bluetooth/ble_deepsleep.py.exp b/tests/multi_bluetooth/stress_deepsleep_reconnect.py.exp
similarity index 100%
rename from tests/multi_bluetooth/ble_deepsleep.py.exp
rename to tests/multi_bluetooth/stress_deepsleep_reconnect.py.exp
diff --git a/tests/multi_espnow/10_simple_data.py b/tests/multi_espnow/10_simple_data.py
index 1d218fe9813..00d69f30d91 100644
--- a/tests/multi_espnow/10_simple_data.py
+++ b/tests/multi_espnow/10_simple_data.py
@@ -14,7 +14,7 @@
 
 
 def init(sta_active=True, ap_active=False):
-    wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
+    wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]]
     e = espnow.ESPNow()
     e.active(True)
     e.set_pmk(default_pmk)
diff --git a/tests/multi_espnow/20_send_echo.py b/tests/multi_espnow/20_send_echo.py
index 4a1d1624d84..49cfcc19c17 100644
--- a/tests/multi_espnow/20_send_echo.py
+++ b/tests/multi_espnow/20_send_echo.py
@@ -27,7 +27,7 @@ def echo_server(e):
 
         #  Echo the MAC and message back to the sender
         if not e.send(peer, msg, sync):
-            print("ERROR: send() failed to", peer)
+            print("ERROR: send() failed to", peer.hex())
             return
 
         if msg == b"!done":
@@ -35,7 +35,12 @@ def echo_server(e):
 
 
 def echo_test(e, peer, msg, sync):
-    print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
+    # to get the same log output regardless of ESP-NOW V1 or V2,
+    # log the exact length unless it's overlong
+    msg_len = len(msg)
+    if msg_len > espnow.MAX_DATA_LEN:
+        msg_len = "max+{}".format(msg_len - espnow.MAX_DATA_LEN)
+    print("TEST: send/recv(msglen=", msg_len, ",sync=", sync, "): ", end="", sep="")
     try:
         if not e.send(peer, msg, sync):
             print("ERROR: Send failed.")
@@ -61,7 +66,7 @@ def echo_client(e, peer, msglens):
 
 
 def init(sta_active=True, ap_active=False):
-    wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
+    wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]]
     e = espnow.ESPNow()
     e.active(True)
     e.set_pmk(default_pmk)
@@ -88,6 +93,6 @@ def instance1():
     multitest.next()
     peer = PEERS[0]
     e.add_peer(peer)
-    echo_client(e, peer, [1, 2, 8, 100, 249, 250, 251, 0])
+    echo_client(e, peer, [1, 2, 8, 100, 249, 250, espnow.MAX_DATA_LEN + 1, 0])
     echo_test(e, peer, b"!done", True)
     e.active(False)
diff --git a/tests/multi_espnow/20_send_echo.py.exp b/tests/multi_espnow/20_send_echo.py.exp
index e43900bcf64..3703593523d 100644
--- a/tests/multi_espnow/20_send_echo.py.exp
+++ b/tests/multi_espnow/20_send_echo.py.exp
@@ -8,7 +8,7 @@ TEST: send/recv(msglen=8,sync=True): OK
 TEST: send/recv(msglen=100,sync=True): OK
 TEST: send/recv(msglen=249,sync=True): OK
 TEST: send/recv(msglen=250,sync=True): OK
-TEST: send/recv(msglen=251,sync=True): ERROR: OSError:
+TEST: send/recv(msglen=max+1,sync=True): ERROR: OSError:
 TEST: send/recv(msglen=0,sync=True): ERROR: OSError:
 TEST: send/recv(msglen=1,sync=False): OK
 TEST: send/recv(msglen=2,sync=False): OK
@@ -16,6 +16,6 @@ TEST: send/recv(msglen=8,sync=False): OK
 TEST: send/recv(msglen=100,sync=False): OK
 TEST: send/recv(msglen=249,sync=False): OK
 TEST: send/recv(msglen=250,sync=False): OK
-TEST: send/recv(msglen=251,sync=False): ERROR: OSError:
+TEST: send/recv(msglen=max+1,sync=False): ERROR: OSError:
 TEST: send/recv(msglen=0,sync=False): ERROR: OSError:
 TEST: send/recv(msglen=5,sync=True): OK
diff --git a/tests/multi_espnow/30_lmk_echo.py b/tests/multi_espnow/30_lmk_echo.py
index ac890804925..eca558c7793 100644
--- a/tests/multi_espnow/30_lmk_echo.py
+++ b/tests/multi_espnow/30_lmk_echo.py
@@ -45,7 +45,7 @@ def echo_server(e):
 
         #  Echo the message back to the sender
         if not e.send(peer, msg, sync):
-            print("ERROR: send() failed to", peer)
+            print("ERROR: send() failed to", peer.hex())
             return
 
         if peer not in peers:
@@ -92,7 +92,7 @@ def echo_client(e, peer, msglens):
 
 # Initialise the wifi and espnow hardware and software
 def init(sta_active=True, ap_active=False):
-    wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
+    wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]]
     e = espnow.ESPNow()
     e.active(True)
     e.set_pmk(default_pmk)
diff --git a/tests/multi_espnow/40_recv_test.py b/tests/multi_espnow/40_recv_test.py
index 46f4f78df48..2c6854851d5 100644
--- a/tests/multi_espnow/40_recv_test.py
+++ b/tests/multi_espnow/40_recv_test.py
@@ -28,7 +28,7 @@ def echo_server(e):
 
         #  Echo the MAC and message back to the sender
         if not e.send(peer, msg, sync):
-            print("ERROR: send() failed to", peer)
+            print("ERROR: send() failed to", peer.hex())
             return
 
         if msg == b"!done":
@@ -48,7 +48,7 @@ def client_send(e, peer, msg, sync):
 
 
 def init(sta_active=True, ap_active=False):
-    wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
+    wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]]
     e = espnow.ESPNow()
     e.active(True)
     e.set_pmk(default_pmk)
diff --git a/tests/multi_espnow/45_recv_espnow_v2.py b/tests/multi_espnow/45_recv_espnow_v2.py
new file mode 100644
index 00000000000..07a23ad1363
--- /dev/null
+++ b/tests/multi_espnow/45_recv_espnow_v2.py
@@ -0,0 +1,136 @@
+# Test of a ESP-NOW V2 echo server and client transferring data, as a variant of
+# 40_recv_test.py.
+#
+# This test requires both instances to be ESP32s built with ESP-IDF V5.5 or
+# newer for ESP-NOW V2 support.
+#
+# Explicitly tests the irecv(), rev() and recvinto() methods.
+
+try:
+    import network, os
+    import random
+    import espnow
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if espnow.MAX_DATA_LEN < 1470:  # Check for V2 support
+    print("SKIP")
+    raise SystemExit
+
+# Set read timeout to 8 seconds
+timeout_ms = 8000
+default_pmk = b"VerySecretValue!"
+sync = True
+
+
+def echo_server(e):
+    peers = []
+    while True:
+        peer, msg = e.irecv(timeout_ms)
+        if peer is None:
+            return
+        if peer not in peers:
+            peers.append(peer)
+            e.add_peer(peer)
+
+        #  Echo the MAC and message back to the sender
+        if not e.send(peer, msg, sync):
+            print("ERROR: send() failed to", peer.hex())
+            return
+
+        if msg == b"!done":
+            return
+
+
+def client_send(e, peer, msg, sync):
+    import time
+
+    time.sleep_ms(100)
+    print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
+    try:
+        if not e.send(peer, msg, sync):
+            print("ERROR: Send failed.")
+            return
+    except OSError as exc:
+        # Don't print exc as it is differs for esp32 and esp8266
+        print("ERROR: OSError:", exc)  # FIXME
+        return
+
+
+def init(sta_active=True, ap_active=False):
+    wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]]
+    e = espnow.ESPNow()
+    e.active(True)
+    e.set_pmk(default_pmk)
+    wlans[0].active(sta_active)
+    wlans[1].active(ap_active)
+    wlans[0].disconnect()  # Force esp8266 STA interface to disconnect from AP
+    return e
+
+
+# Server
+def instance0():
+    e = init(True, False)
+    multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
+    multitest.next()
+    print("Server Start")
+    echo_server(e)
+    print("Server Done")
+    e.active(False)
+
+
+def check_recv(s, r):
+    if s == r:
+        print("OK")
+    else:
+        print("ERROR: Received != Sent")
+        print("Sent", len(s), "bytes")
+        if r is None:
+            print("Receive timed out")
+        else:
+            print("Received", len(r), "bytes")
+
+
+# Client
+def instance1():
+    # Instance 1 (the client)
+    e = init(True, False)
+    e.config(timeout_ms=timeout_ms)
+    multitest.next()
+    peer = PEERS[0]
+    e.add_peer(peer)
+
+    print("RECVINTO() test...")
+    msg = os.urandom(768)
+    client_send(e, peer, msg, True)
+    data = [bytearray(espnow.ADDR_LEN), bytearray(espnow.MAX_DATA_LEN)]
+    n = e.recvinto(data)
+    check_recv(msg, data[1])
+
+    print("IRECV() test...")
+    msg = os.urandom(768)
+    client_send(e, peer, msg, True)
+    p2, msg2 = e.irecv()
+    check_recv(msg, msg2)
+
+    print("RECV() test...")
+    msg = os.urandom(1024)
+    client_send(e, peer, msg, True)
+    p2, msg2 = e.recv()
+    check_recv(msg, msg2)
+
+    print("ITERATOR() test...")
+    msg = os.urandom(espnow.MAX_DATA_LEN)
+    client_send(e, peer, msg, True)
+    p2, msg2 = next(e)
+    check_recv(msg, msg2)
+
+    # Tell the server to stop
+    print("DONE")
+    msg = b"!done"
+    client_send(e, peer, msg, True)
+    p2, msg2 = e.irecv()
+    check_recv(msg, msg2)
+
+    e.active(False)
diff --git a/tests/multi_espnow/45_recv_espnow_v2.py.exp b/tests/multi_espnow/45_recv_espnow_v2.py.exp
new file mode 100644
index 00000000000..3638cc40640
--- /dev/null
+++ b/tests/multi_espnow/45_recv_espnow_v2.py.exp
@@ -0,0 +1,14 @@
+--- instance0 ---
+Server Start
+Server Done
+--- instance1 ---
+RECVINTO() test...
+TEST: send/recv(msglen=768,sync=True): OK
+IRECV() test...
+TEST: send/recv(msglen=768,sync=True): OK
+RECV() test...
+TEST: send/recv(msglen=1024,sync=True): OK
+ITERATOR() test...
+TEST: send/recv(msglen=1470,sync=True): OK
+DONE
+TEST: send/recv(msglen=5,sync=True): OK
diff --git a/tests/multi_espnow/50_esp32_rssi_test.py b/tests/multi_espnow/50_esp32_rssi_test.py
index 6a47b540d35..7209ad341ef 100644
--- a/tests/multi_espnow/50_esp32_rssi_test.py
+++ b/tests/multi_espnow/50_esp32_rssi_test.py
@@ -29,7 +29,7 @@ def echo_server(e):
 
         #  Echo the MAC and message back to the sender
         if not e.send(peer, msg, sync):
-            print("ERROR: send() failed to", peer)
+            print("ERROR: send() failed to", peer.hex())
             return
 
         if msg == b"!done":
@@ -49,7 +49,7 @@ def client_send(e, peer, msg, sync):
 
 
 def init(sta_active=True, ap_active=False):
-    wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
+    wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]]
     e = espnow.ESPNow()
     e.active(True)
     e.set_pmk(default_pmk)
diff --git a/tests/multi_espnow/60_irq_test.py b/tests/multi_espnow/60_irq_test.py
index 37fc57ce4ae..d39f8bccc4c 100644
--- a/tests/multi_espnow/60_irq_test.py
+++ b/tests/multi_espnow/60_irq_test.py
@@ -29,7 +29,7 @@ def echo_server(e):
 
         #  Echo the MAC and message back to the sender
         if not e.send(peer, msg, sync):
-            print("ERROR: send() failed to", peer)
+            print("ERROR: send() failed to", peer.hex())
             return
 
         if msg == b"!done":
@@ -49,7 +49,7 @@ def client_send(e, peer, msg, sync):
 
 
 def init(sta_active=True, ap_active=False):
-    wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
+    wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]]
     e = espnow.ESPNow()
     e.active(True)
     e.set_pmk(default_pmk)
diff --git a/tests/multi_espnow/70_channel.py b/tests/multi_espnow/70_channel.py
new file mode 100644
index 00000000000..90b963a7a4c
--- /dev/null
+++ b/tests/multi_espnow/70_channel.py
@@ -0,0 +1,94 @@
+# Test that ESP-NOW picks up the channel configuration for STA
+# mode on ESP32.
+#
+# Note that setting the channel on a peer in ESP-NOW on modern ESP-IDF only
+# checks it against the configured channel, it doesn't ever change the radio
+# channel
+import sys
+import time
+
+try:
+    import network
+    import espnow
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# ESP8266 doesn't support config('channel') on the STA interface,
+# and the channel parameter to add_peer doesn't appear to set the
+# channel either.
+if sys.platform == "esp8266":
+    print("SKIP")
+    raise SystemExit
+
+# Workaround for a bug in ESP32-C6 where it doesn't select channels cleanly
+# (probably an ESP-IDF v5.5.x bug, needs further investigation.)
+if "ESP32-C6" in sys.implementation._machine:
+    print("SKIP")
+    raise SystemExit
+
+timeout_ms = 1000
+default_pmk = b"MicroPyth0nRules"
+
+CHANNEL = 3
+WRONG_CHANNEL = 8
+
+
+def init_sta():
+    sta = network.WLAN(network.WLAN.IF_STA)
+    e = espnow.ESPNow()
+    e.active(True)
+    sta.active(True)
+    sta.disconnect()  # Force AP disconnect for any saved config, important so the channel doesn't change
+    sta.config(channel=CHANNEL)
+    e.set_pmk(default_pmk)
+    return sta, e
+
+
+# Receiver
+def instance0():
+    sta, e = init_sta()
+    multitest.globals(PEER=sta.config("mac"))
+    multitest.next()
+    print(sta.config("channel"))
+    while True:
+        peer, msg = e.recv(timeout_ms)
+        if peer is None:
+            print("Timeout")
+            break
+        print(msg)
+    e.active(False)
+
+
+# Sender
+def instance1():
+    sta, e = init_sta()
+    multitest.next()
+    peer = PEER
+
+    # both instances set channel via sta.config(), above
+    msg = b"sent to right channel 1"
+    e.add_peer(peer, channel=CHANNEL)
+    for _ in range(3):
+        e.send(peer, msg)
+    e.del_peer(peer)
+    print(sta.config("channel"))
+
+    sta.config(channel=WRONG_CHANNEL)
+    msg = b"sent to wrong channel"
+    e.add_peer(peer, channel=WRONG_CHANNEL)
+    for _ in range(3):
+        e.send(peer, msg)
+    e.del_peer(peer)
+    print(sta.config("channel"))
+
+    # switching back to the correct channel should also work
+    sta.config(channel=CHANNEL)
+    msg = b"sent to right channel 2"
+    e.add_peer(peer, channel=CHANNEL)
+    for _ in range(3):
+        e.send(peer, msg)
+    e.del_peer(peer)
+    print(sta.config("channel"))
+
+    e.active(False)
diff --git a/tests/multi_espnow/70_channel.py.exp b/tests/multi_espnow/70_channel.py.exp
new file mode 100644
index 00000000000..d8553966032
--- /dev/null
+++ b/tests/multi_espnow/70_channel.py.exp
@@ -0,0 +1,13 @@
+--- instance0 ---
+3
+b'sent to right channel 1'
+b'sent to right channel 1'
+b'sent to right channel 1'
+b'sent to right channel 2'
+b'sent to right channel 2'
+b'sent to right channel 2'
+Timeout
+--- instance1 ---
+3
+8
+3
diff --git a/tests/multi_espnow/75_rate.py b/tests/multi_espnow/75_rate.py
new file mode 100644
index 00000000000..f676ab1d616
--- /dev/null
+++ b/tests/multi_espnow/75_rate.py
@@ -0,0 +1,97 @@
+# Test configuring transmit rates for ESP-NOW on ESP32
+
+import sys
+import time
+
+try:
+    import network
+    import espnow
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# ESP8266 doesn't support multiple ESP-NOW data rates
+if sys.platform == "esp8266":
+    print("SKIP")
+    raise SystemExit
+
+# Currently the config(rate=...) implementation is not compatible with ESP32-C6
+# (this test passes when C6 is receiver, but not if C6 is sender.)
+if "ESP32-C6" in sys.implementation._machine:
+    print("SKIP")
+    raise SystemExit
+
+# ESP32-C2 doesn't support Long Range mode. This test is currently written assuming
+# LR mode can be enabled.
+if "ESP32-C2" in sys.implementation._machine:
+    print("SKIP")
+    raise SystemExit
+
+
+timeout_ms = 1000
+default_pmk = b"MicroPyth0nRules"
+CHANNEL = 9
+
+
+def init_sta():
+    sta = network.WLAN(network.WLAN.IF_STA)
+    e = espnow.ESPNow()
+    e.active(True)
+    sta.active(True)
+    sta.disconnect()  # Force AP disconnect for any saved config, important so the channel doesn't change
+    sta.config(channel=CHANNEL)
+    e.set_pmk(default_pmk)
+    # Enable both default 802.11 modes and Long Range modes
+    sta.config(protocol=network.WLAN.PROTOCOL_LR | network.WLAN.PROTOCOL_DEFAULT)
+    return sta, e
+
+
+# Receiver
+def instance0():
+    sta, e = init_sta()
+    try:
+        multitest.globals(PEER=sta.config("mac"))
+        multitest.next()
+        while True:
+            peer, msg = e.recv(timeout_ms)
+            if peer is None:
+                print("Timeout")
+                break
+            # Note that we don't have any way in Python to tell what data rate this message
+            # was received with, so we're assuming the rate was correct.
+            print(msg)
+    finally:
+        # Important to stop both even if the test is skipped due to the other instance
+        sta.active(False)
+        e.active(False)
+
+
+# Sender
+def instance1():
+    sta, e = init_sta()
+    multitest.next()
+    peer = PEER
+    try:
+        e.add_peer(peer)
+        # Test normal, non-LR rates
+        for msg, rate in (
+            (b"default rate", None),
+            (b"5Mbit", espnow.RATE_5M),
+            (b"11Mbit", espnow.RATE_11M),
+            (b"24Mbit", espnow.RATE_24M),
+            (b"54Mbit", espnow.RATE_54M),
+            (b"250K LR", espnow.RATE_LORA_250K),
+            (b"500K LR", espnow.RATE_LORA_500K),
+            # switch back to non-LR rates to check it's all OK
+            (b"1Mbit again", espnow.RATE_1M),
+            (b"11Mbit again", espnow.RATE_11M),
+        ):
+            if rate is not None:
+                e.config(rate=rate)
+            for _ in range(3):
+                e.send(peer, msg)
+            time.sleep_ms(50)  # give messages some time to be received before continuing
+        e.del_peer(peer)
+    finally:
+        sta.active(False)
+        e.active(False)
diff --git a/tests/multi_espnow/75_rate.py.exp b/tests/multi_espnow/75_rate.py.exp
new file mode 100644
index 00000000000..5a275ee6b28
--- /dev/null
+++ b/tests/multi_espnow/75_rate.py.exp
@@ -0,0 +1,31 @@
+--- instance0 ---
+b'default rate'
+b'default rate'
+b'default rate'
+b'5Mbit'
+b'5Mbit'
+b'5Mbit'
+b'11Mbit'
+b'11Mbit'
+b'11Mbit'
+b'24Mbit'
+b'24Mbit'
+b'24Mbit'
+b'54Mbit'
+b'54Mbit'
+b'54Mbit'
+b'250K LR'
+b'250K LR'
+b'250K LR'
+b'500K LR'
+b'500K LR'
+b'500K LR'
+b'1Mbit again'
+b'1Mbit again'
+b'1Mbit again'
+b'11Mbit again'
+b'11Mbit again'
+b'11Mbit again'
+Timeout
+--- instance1 ---
+
diff --git a/tests/multi_espnow/80_asyncio_client.py b/tests/multi_espnow/80_asyncio_client.py
new file mode 100644
index 00000000000..b77ab54a202
--- /dev/null
+++ b/tests/multi_espnow/80_asyncio_client.py
@@ -0,0 +1,113 @@
+# Test of a ESPnow echo server and asyncio client transferring data.
+# Test will SKIP if instance1 (asyncio client) does not support asyncio.
+# - eg. ESP8266 with 1MB flash.
+# Instance0 is not required to support asyncio.
+
+try:
+    import network
+    import random
+    import espnow
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Set read timeout to 5 seconds
+timeout_ms = 5000
+default_pmk = b"MicroPyth0nRules"
+sync = True
+
+
+def echo_server(e):
+    peers = []
+    while True:
+        peer, msg = e.irecv(timeout_ms)
+        if peer is None:
+            return
+        if peer not in peers:
+            peers.append(peer)
+            e.add_peer(peer)
+
+        #  Echo the MAC and message back to the sender
+        if not e.send(peer, msg, sync):
+            print("ERROR: send() failed to", peer.hex())
+            return
+
+        if msg == b"!done":
+            return
+
+
+def client_send(e, peer, msg, sync):
+    print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
+    try:
+        if not e.send(peer, msg, sync):
+            print("ERROR: Send failed.")
+            return
+    except OSError as exc:
+        # Don't print exc as it is differs for esp32 and esp8266
+        print("ERROR: OSError:")
+        return
+    print("OK")
+
+
+def init(e, sta_active=True, ap_active=False):
+    wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]]
+    e.active(True)
+    e.set_pmk(default_pmk)
+    wlans[0].active(sta_active)
+    wlans[1].active(ap_active)
+    wlans[0].disconnect()  # Force esp8266 STA interface to disconnect from AP
+    return e
+
+
+async def client(e):
+    init(e, True, False)
+    e.config(timeout_ms=timeout_ms)
+    peer = PEERS[0]
+    e.add_peer(peer)
+    multitest.next()
+
+    multitest.wait("server ready")
+
+    print("airecv() test...")
+    msgs = []
+    for i in range(5):
+        # Send messages to the peer who will echo it back
+        msgs.append(bytes([random.getrandbits(8) for _ in range(12)]))
+        client_send(e, peer, msgs[i], True)
+
+    for i in range(5):
+        mac, reply = await e.airecv()
+        print("OK" if reply == msgs[i] else "ERROR: Received != Sent")
+
+    # Tell the server to stop
+    print("DONE")
+    msg = b"!done"
+    client_send(e, peer, msg, True)
+    mac, reply = await e.airecv()
+    print("OK" if reply == msg else "ERROR: Received != Sent")
+
+    e.active(False)
+
+
+# Server
+def instance0():
+    e = espnow.ESPNow()
+    init(e, True, False)
+    multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
+    multitest.next()
+    multitest.broadcast("server ready")
+    print("Server Start")
+    echo_server(e)
+    print("Server Done")
+    e.active(False)
+
+
+# Client
+def instance1():
+    try:
+        import asyncio
+        from aioespnow import AIOESPNow
+    except ImportError:
+        print("SKIP")
+        raise SystemExit
+    asyncio.run(client(AIOESPNow()))
diff --git a/tests/multi_espnow/80_uasyncio_client.py.exp b/tests/multi_espnow/80_asyncio_client.py.exp
similarity index 100%
rename from tests/multi_espnow/80_uasyncio_client.py.exp
rename to tests/multi_espnow/80_asyncio_client.py.exp
diff --git a/tests/multi_espnow/80_uasyncio_client.py b/tests/multi_espnow/80_uasyncio_client.py
deleted file mode 100644
index fa2918cc031..00000000000
--- a/tests/multi_espnow/80_uasyncio_client.py
+++ /dev/null
@@ -1,110 +0,0 @@
-# Test of a ESPnow echo server and asyncio client transferring data.
-# Test will SKIP if instance1 (asyncio client) does not support asyncio.
-# - eg. ESP8266 with 1MB flash.
-# Instance0 is not required to support asyncio.
-
-try:
-    import network
-    import random
-    import espnow
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-# Set read timeout to 5 seconds
-timeout_ms = 5000
-default_pmk = b"MicroPyth0nRules"
-sync = True
-
-
-def echo_server(e):
-    peers = []
-    while True:
-        peer, msg = e.irecv(timeout_ms)
-        if peer is None:
-            return
-        if peer not in peers:
-            peers.append(peer)
-            e.add_peer(peer)
-
-        #  Echo the MAC and message back to the sender
-        if not e.send(peer, msg, sync):
-            print("ERROR: send() failed to", peer)
-            return
-
-        if msg == b"!done":
-            return
-
-
-def client_send(e, peer, msg, sync):
-    print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
-    try:
-        if not e.send(peer, msg, sync):
-            print("ERROR: Send failed.")
-            return
-    except OSError as exc:
-        # Don't print exc as it is differs for esp32 and esp8266
-        print("ERROR: OSError:")
-        return
-    print("OK")
-
-
-def init(e, sta_active=True, ap_active=False):
-    wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
-    e.active(True)
-    e.set_pmk(default_pmk)
-    wlans[0].active(sta_active)
-    wlans[1].active(ap_active)
-    wlans[0].disconnect()  # Force esp8266 STA interface to disconnect from AP
-    return e
-
-
-async def client(e):
-    init(e, True, False)
-    e.config(timeout_ms=timeout_ms)
-    peer = PEERS[0]
-    e.add_peer(peer)
-    multitest.next()
-
-    print("airecv() test...")
-    msgs = []
-    for i in range(5):
-        # Send messages to the peer who will echo it back
-        msgs.append(bytes([random.getrandbits(8) for _ in range(12)]))
-        client_send(e, peer, msgs[i], True)
-
-    for i in range(5):
-        mac, reply = await e.airecv()
-        print("OK" if reply == msgs[i] else "ERROR: Received != Sent")
-
-    # Tell the server to stop
-    print("DONE")
-    msg = b"!done"
-    client_send(e, peer, msg, True)
-    mac, reply = await e.airecv()
-    print("OK" if reply == msg else "ERROR: Received != Sent")
-
-    e.active(False)
-
-
-# Server
-def instance0():
-    e = espnow.ESPNow()
-    init(e, True, False)
-    multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
-    multitest.next()
-    print("Server Start")
-    echo_server(e)
-    print("Server Done")
-    e.active(False)
-
-
-# Client
-def instance1():
-    try:
-        import uasyncio as asyncio
-        from aioespnow import AIOESPNow
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-    asyncio.run(client(AIOESPNow()))
diff --git a/tests/multi_espnow/81_asyncio_server.py b/tests/multi_espnow/81_asyncio_server.py
new file mode 100644
index 00000000000..e6002582c08
--- /dev/null
+++ b/tests/multi_espnow/81_asyncio_server.py
@@ -0,0 +1,96 @@
+# Test of a ESPnow asyncio echo server and client transferring data.
+# Test will SKIP if instance0 (asyncio echo server) does not support asyncio.
+# - eg. ESP8266 with 1MB flash.
+# Instance1 is not required to support asyncio.
+
+try:
+    import network
+    import random
+    import espnow
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# Set read timeout to 5 seconds
+timeout_ms = 5000
+default_pmk = b"MicroPyth0nRules"
+sync = True
+
+
+def client_send(e, peer, msg, sync):
+    print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
+    try:
+        if not e.send(peer, msg, sync):
+            print("ERROR: Send failed.")
+            return
+    except OSError as exc:
+        # Don't print exc as it is differs for esp32 and esp8266
+        print("ERROR: OSError:")
+        return
+
+
+def init(e, sta_active=True, ap_active=False):
+    wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]]
+    e.active(True)
+    e.set_pmk(default_pmk)
+    wlans[0].active(sta_active)
+    wlans[1].active(ap_active)
+    wlans[0].disconnect()  # Force esp8266 STA interface to disconnect from AP
+    return e
+
+
+async def echo_server(e):
+    peers = []
+    async for peer, msg in e:
+        if peer not in peers:
+            peers.append(peer)
+            e.add_peer(peer)
+
+        #  Echo the message back to the sender
+        if not await e.asend(peer, msg, sync):
+            print("ERROR: asend() failed to", peer)
+            return
+
+        if msg == b"!done":
+            return
+
+
+# Server
+def instance0():
+    try:
+        import asyncio
+        from aioespnow import AIOESPNow
+    except ImportError:
+        print("SKIP")
+        raise SystemExit
+    e = AIOESPNow()
+    init(e, True, False)
+    multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
+    multitest.next()
+    print("Server Start")
+    asyncio.run(echo_server(e))
+    print("Server Done")
+    e.active(False)
+
+
+def instance1():
+    e = espnow.ESPNow()
+    init(e, True, False)
+    peer = PEERS[0]
+    e.add_peer(peer)
+    multitest.next()
+
+    for i in range(5):
+        msg = bytes([random.getrandbits(8) for _ in range(12)])
+        client_send(e, peer, msg, True)
+        p2, msg2 = e.irecv(timeout_ms)
+        print("OK" if msg2 == msg else "ERROR: Received != Sent")
+
+    # Tell the server to stop
+    print("DONE")
+    msg = b"!done"
+    client_send(e, peer, msg, True)
+    p2, msg2 = e.irecv(timeout_ms)
+    print("OK" if msg2 == msg else "ERROR: Received != Sent")
+
+    e.active(False)
diff --git a/tests/multi_espnow/81_uasyncio_server.py.exp b/tests/multi_espnow/81_asyncio_server.py.exp
similarity index 100%
rename from tests/multi_espnow/81_uasyncio_server.py.exp
rename to tests/multi_espnow/81_asyncio_server.py.exp
diff --git a/tests/multi_espnow/81_uasyncio_server.py b/tests/multi_espnow/81_uasyncio_server.py
deleted file mode 100644
index ee098b7f3be..00000000000
--- a/tests/multi_espnow/81_uasyncio_server.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# Test of a ESPnow asyncio echo server and client transferring data.
-# Test will SKIP if instance0 (asyncio echo server) does not support asyncio.
-# - eg. ESP8266 with 1MB flash.
-# Instance1 is not required to support asyncio.
-
-try:
-    import network
-    import random
-    import espnow
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-# Set read timeout to 5 seconds
-timeout_ms = 5000
-default_pmk = b"MicroPyth0nRules"
-sync = True
-
-
-def client_send(e, peer, msg, sync):
-    print("TEST: send/recv(msglen=", len(msg), ",sync=", sync, "): ", end="", sep="")
-    try:
-        if not e.send(peer, msg, sync):
-            print("ERROR: Send failed.")
-            return
-    except OSError as exc:
-        # Don't print exc as it is differs for esp32 and esp8266
-        print("ERROR: OSError:")
-        return
-
-
-def init(e, sta_active=True, ap_active=False):
-    wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
-    e.active(True)
-    e.set_pmk(default_pmk)
-    wlans[0].active(sta_active)
-    wlans[1].active(ap_active)
-    wlans[0].disconnect()  # Force esp8266 STA interface to disconnect from AP
-    return e
-
-
-async def echo_server(e):
-    peers = []
-    async for peer, msg in e:
-        if peer not in peers:
-            peers.append(peer)
-            e.add_peer(peer)
-
-        #  Echo the message back to the sender
-        if not await e.asend(peer, msg, sync):
-            print("ERROR: asend() failed to", peer)
-            return
-
-        if msg == b"!done":
-            return
-
-
-# Server
-def instance0():
-    try:
-        import uasyncio as asyncio
-        from aioespnow import AIOESPNow
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-    e = AIOESPNow()
-    init(e, True, False)
-    multitest.globals(PEERS=[network.WLAN(i).config("mac") for i in (0, 1)])
-    multitest.next()
-    print("Server Start")
-    asyncio.run(echo_server(e))
-    print("Server Done")
-    e.active(False)
-
-
-def instance1():
-    e = espnow.ESPNow()
-    init(e, True, False)
-    peer = PEERS[0]
-    e.add_peer(peer)
-    multitest.next()
-
-    for i in range(5):
-        msg = bytes([random.getrandbits(8) for _ in range(12)])
-        client_send(e, peer, msg, True)
-        p2, msg2 = e.irecv(timeout_ms)
-        print("OK" if msg2 == msg else "ERROR: Received != Sent")
-
-    # Tell the server to stop
-    print("DONE")
-    msg = b"!done"
-    client_send(e, peer, msg, True)
-    p2, msg2 = e.irecv(timeout_ms)
-    print("OK" if msg2 == msg else "ERROR: Received != Sent")
-
-    e.active(False)
diff --git a/tests/multi_espnow/90_memory_test.py b/tests/multi_espnow/90_memory_test.py
index 5e80eb0fdf6..fe30740e3ea 100644
--- a/tests/multi_espnow/90_memory_test.py
+++ b/tests/multi_espnow/90_memory_test.py
@@ -31,7 +31,7 @@ def echo_server(e):
 
         #  Echo the MAC and message back to the sender
         if not e.send(peer, msg, sync):
-            print("ERROR: send() failed to", peer)
+            print("ERROR: send() failed to", peer.hex())
             return
 
         if msg == b"!done":
@@ -65,7 +65,7 @@ def echo_client(e, peer, msglens):
 
 
 def init(sta_active=True, ap_active=False):
-    wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]]
+    wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]]
     e = espnow.ESPNow()
     e.active(True)
     e.set_pmk(default_pmk)
diff --git a/tests/multi_extmod/machine_can_01_rxtx_simple.py b/tests/multi_extmod/machine_can_01_rxtx_simple.py
new file mode 100644
index 00000000000..fbc7692926d
--- /dev/null
+++ b/tests/multi_extmod/machine_can_01_rxtx_simple.py
@@ -0,0 +1,35 @@
+from machine import CAN
+import time
+
+ID_0 = 0x50
+ID_1 = 0x50333
+FLAGS_1 = CAN.FLAG_EXT_ID  # ID_1 is an extended CAN ID
+
+can = CAN(1, 500_000)
+can.set_filters(None)  # receive all
+
+
+def print_rx(can_id, data, flags, errors):
+    print(hex(can_id), bytes(data), hex(flags), hex(errors))
+
+
+def instance0():
+    multitest.next()
+
+    # receive from instance1 first
+    while not (rx := can.recv()):
+        time.sleep(0)
+    print_rx(*rx)
+
+    # now send one
+    can.send(ID_0, b"1234")
+
+
+def instance1():
+    multitest.next()
+
+    can.send(ID_1, b"ABCD", FLAGS_1)
+
+    while not (rx := can.recv()):
+        time.sleep(0)
+    print_rx(*rx)
diff --git a/tests/multi_extmod/machine_can_01_rxtx_simple.py.exp b/tests/multi_extmod/machine_can_01_rxtx_simple.py.exp
new file mode 100644
index 00000000000..479b6809490
--- /dev/null
+++ b/tests/multi_extmod/machine_can_01_rxtx_simple.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+0x50333 b'ABCD' 0x2 0x0
+--- instance1 ---
+0x50 b'1234' 0x0 0x0
diff --git a/tests/multi_extmod/machine_can_02_rx_callback.py b/tests/multi_extmod/machine_can_02_rx_callback.py
new file mode 100644
index 00000000000..80b13dcd622
--- /dev/null
+++ b/tests/multi_extmod/machine_can_02_rx_callback.py
@@ -0,0 +1,122 @@
+from machine import CAN
+import time
+
+# Test the CAN.IRQ_RX irq handler, including overflow
+
+rx_overflow = False
+rx_full = False
+received = []
+
+# CAN IDs
+ID_SPAM = 0x345  # messages spammed into the receive FIFO
+ID_ACK_OFLOW = 0x055  # message the receiver sends after it's seen an overflow
+ID_AFTER = 0x100  # message the sender sends after the ACK
+
+can = CAN(1, 500_000)
+
+
+# A very basic "soft" receiver handler that stores received messages into a global list
+def receiver_irq_recv(can):
+    global rx_overflow, rx_full
+
+    assert can.irq().flags() & can.IRQ_RX  # the only enabled IRQ
+
+    can_id, data, _flags, errors = can.recv()
+
+    received.append((can_id, None))
+
+    # The FIFO is expected not to overflow by itself, wait until 40 messages
+    # have been received and then block the receive handler to induce an overflow
+    if len(received) == 40:
+        assert not rx_overflow  # shouldn't have already happened, either
+        time.sleep_ms(500)
+
+    if not rx_overflow and (errors & CAN.RECV_ERR_OVERRUN):
+        # expected this should happen on the very next message after
+        # the one where we slept for 500ms
+        print("irq_recv overrun", len(received))
+        received.clear()  # check we still get some messages, see rx_spam print line below
+        rx_overflow = True
+
+    # also expect the FIFO to be FULL again immediately after overrunning and rx_overflow event
+    if rx_overflow and (errors & CAN.RECV_ERR_OVERRUN | CAN.RECV_ERR_FULL) == CAN.RECV_ERR_FULL:
+        rx_full = True
+
+
+# Receiver
+def instance0():
+    can.irq(receiver_irq_recv, trigger=can.IRQ_RX, hard=False)
+
+    can.set_filters(None)  # receive all
+
+    multitest.next()
+
+    while not rx_overflow:
+        pass  # Resume ASAP after FIFO0 overflows
+
+    can.send(ID_ACK_OFLOW, b"overflow")
+
+    # at least one ID_SPAM message should have been received
+    # *after* we overflowed and 'received' was clear in the irq handler
+    print("rx_spam", any(r[0] == ID_SPAM for r in received))
+
+    # wait until the "after" message is received
+    for n in range(100):
+        if any(r[0] == ID_AFTER for r in received):
+            break
+        time.sleep_ms(10)
+
+    can.irq(None)  # disable the IRQ
+    received.clear()
+
+    # at some point while waiting for ID_AFTER the FIFO should have gotten
+    # full again
+    print("rx_full", rx_full)
+
+    # now IRQ is disabled, no new messages should be received
+    time.sleep_ms(250)
+    print("len", len(received))
+
+
+received_ack = False
+
+# reusing the result buffer so sender_irq_recv can be 'hard'
+sender_irq_result = [None, memoryview(bytearray(64)), None, None]
+
+
+def sender_irq_recv(can):
+    global received_ack
+
+    assert can.irq().flags() & can.IRQ_RX  # the only enabled IRQ
+
+    can_id, data, _flags, _errors = can.recv(sender_irq_result)
+    print("sender_irq_recv", can_id, len(data))  # should be ID_ACK_OFLOW and "overflow" payload
+    received_ack = True
+
+
+# Sender
+def instance1():
+    can.irq(sender_irq_recv, CAN.IRQ_RX, hard=True)
+
+    can.set_filters(None)
+
+    multitest.next()
+
+    # Spam out messages until the receiver tells us its RX FIFO is full.
+    #
+    # The RX FIFO on the receiver can vary from 3 deep (BXCAN) to 25 deep (STM32H7),
+    # so we keep sending to it until we see a CAN message on ID_ACK_OFLOW indicating
+    # the receiver's FIFO has overflowed
+    while not received_ack:
+        for i in range(255):
+            while can.send(ID_SPAM, bytes([i] * 8)) is None and not received_ack:
+                # Don't overflow the TX FIFO
+                time.sleep_ms(1)
+            if received_ack:
+                break
+
+    # give the receiver some time to make space in the FIFO
+    time.sleep_ms(200)
+
+    # send the final message, the receiver should get this one
+    can.send(ID_AFTER, b"aaaaa")
diff --git a/tests/multi_extmod/machine_can_02_rx_callback.py.exp b/tests/multi_extmod/machine_can_02_rx_callback.py.exp
new file mode 100644
index 00000000000..8e4e5dc6475
--- /dev/null
+++ b/tests/multi_extmod/machine_can_02_rx_callback.py.exp
@@ -0,0 +1,7 @@
+--- instance0 ---
+irq_recv overrun 41
+rx_spam True
+rx_full True
+len 0
+--- instance1 ---
+sender_irq_recv 85 8
diff --git a/tests/multi_extmod/machine_can_03_rx_filters.py b/tests/multi_extmod/machine_can_03_rx_filters.py
new file mode 100644
index 00000000000..a56acbbe91e
--- /dev/null
+++ b/tests/multi_extmod/machine_can_03_rx_filters.py
@@ -0,0 +1,103 @@
+from machine import CAN
+import time
+
+# Test for filtering capabilities
+
+can = CAN(1, 500_000)
+
+# IDs and filter phases used for the 'single id' part of the test
+SINGLE_EXT_ID = (0x1234_5678, 0x1FFF_FFFF, CAN.FLAG_EXT_ID)
+SINGLE_STD_ID = (0x505, 0x7FF, 0)
+SINGLE_ID_PHASES = [
+    ("single ext id", [SINGLE_EXT_ID]),
+    ("single std id", [SINGLE_STD_ID]),
+    ("ext+std ids", [SINGLE_EXT_ID, SINGLE_STD_ID]),
+    ("std+ext ids", [SINGLE_STD_ID, SINGLE_EXT_ID]),  # these two should be equivalent
+    ("accept none", []),
+    ("accept all", None),
+    ("accept none again", ()),
+]
+
+
+# Receiver
+def receiver_irq_recv(can):
+    assert can.irq().flags() & can.IRQ_RX  # the only enabled IRQ
+    can_id, data, _flags, _errors = can.recv()
+    print("recv", hex(can_id), data.hex())
+
+
+def instance0():
+    can.irq(receiver_irq_recv, trigger=can.IRQ_RX, hard=False)
+
+    multitest.next()
+
+    # Configure to receive standard frames (in a range), and
+    # extended frames (in a range).
+    can.set_filters([(0x300, 0x300, 0), (0x3000, 0x3000, CAN.FLAG_EXT_ID)])
+    multitest.broadcast("ready id ranges")
+
+    # Run through the phases of filtering for individual IDs
+    for phase, filters in SINGLE_ID_PHASES:
+        multitest.wait("configure " + phase)
+        if filters and len(filters) > CAN.FILTERS_MAX:
+            # this check really exists to add test coverage for the FILTERS_MAX constant
+            print("Warning: Too many filters for hardware!")
+        can.set_filters(filters)
+        print("receiver configured " + phase)
+        multitest.broadcast("ready " + phase)
+
+    multitest.wait("Sender done")
+
+
+def send_messages(messages):
+    for can_id, payload, flags in messages:
+        r = can.send(can_id, payload, flags)
+        if r is None:
+            print("Failed to send:", hex(can_id), payload.hex())
+        time.sleep_ms(5)  # avoid flooding either our or the receiver's FIFO
+
+
+# Sender
+def instance1():
+    multitest.next()
+    multitest.wait("ready id ranges")
+
+    print("Sending ID ranges...")
+    for i in range(3):
+        send_messages(
+            [
+                (0x345, bytes([i, 0xFF] * (i + 1)), 0),
+                (0x3700 + i, bytes([0xEE] * (i + 1)), CAN.FLAG_EXT_ID),
+                (0x123, b"abcdef", 0),  # matches no filter, expect ACKed but not received
+            ]
+        )
+
+    # Now move on to single ID filtering
+
+    single_id_messages = [
+        (0x1234_5678, b"\x01\x02\x03\x04\x05", CAN.FLAG_EXT_ID),  # matches ext id
+        (0x0234_5678, b"\x00\x00", CAN.FLAG_EXT_ID),  # no match
+        (0x678, b"\x00\x01", 0),  # no match
+        (0x505, b"\x06\x07\x08\x09\x0a\x0b", 0),  # matches standard id
+        (0x345, b"\x00\x02", 0),  # no match (in prev filter)
+        (0x1234_5679, b"\x00\x03", CAN.FLAG_EXT_ID),  # no match
+        (0x3705, b"\x00\x04", CAN.FLAG_EXT_ID),  # no match (in prev filter)
+        (0x1234_5678, b"\x01\x02\x03", CAN.FLAG_EXT_ID),  # matches ext id
+        (0x505, b"\x04\x05\x06", 0),  # matches standard id
+        (0x505, b"\x00\x05", CAN.FLAG_EXT_ID),  # no match (is ext id)
+        (0x507, b"\x00\x06", 0),  # no match
+        (0x1334_5678, b"\x00\x07", CAN.FLAG_EXT_ID),  # no match
+        (0x1234_5670, b"\x00\x08", CAN.FLAG_EXT_ID),  # no match
+    ]
+
+    # Send the same list of messages for each phase of the test.
+    # The receiver will have configured different filters, and the .exp
+    # file is what selects which messages should be received or not.
+    for phase, _ in SINGLE_ID_PHASES:
+        multitest.broadcast("configure " + phase)
+        multitest.wait("ready " + phase)
+        print("Sending for " + phase + "...")
+        send_messages(single_id_messages)
+
+    print("Sender done")
+    multitest.broadcast("Sender done")
diff --git a/tests/multi_extmod/machine_can_03_rx_filters.py.exp b/tests/multi_extmod/machine_can_03_rx_filters.py.exp
new file mode 100644
index 00000000000..d55c0c97d15
--- /dev/null
+++ b/tests/multi_extmod/machine_can_03_rx_filters.py.exp
@@ -0,0 +1,49 @@
+--- instance0 ---
+recv 0x345 00ff
+recv 0x3700 ee
+recv 0x345 01ff01ff
+recv 0x3701 eeee
+recv 0x345 02ff02ff02ff
+recv 0x3702 eeeeee
+receiver configured single ext id
+recv 0x12345678 0102030405
+recv 0x12345678 010203
+receiver configured single std id
+recv 0x505 060708090a0b
+recv 0x505 040506
+receiver configured ext+std ids
+recv 0x12345678 0102030405
+recv 0x505 060708090a0b
+recv 0x12345678 010203
+recv 0x505 040506
+receiver configured std+ext ids
+recv 0x12345678 0102030405
+recv 0x505 060708090a0b
+recv 0x12345678 010203
+recv 0x505 040506
+receiver configured accept none
+receiver configured accept all
+recv 0x12345678 0102030405
+recv 0x2345678 0000
+recv 0x678 0001
+recv 0x505 060708090a0b
+recv 0x345 0002
+recv 0x12345679 0003
+recv 0x3705 0004
+recv 0x12345678 010203
+recv 0x505 040506
+recv 0x505 0005
+recv 0x507 0006
+recv 0x13345678 0007
+recv 0x12345670 0008
+receiver configured accept none again
+--- instance1 ---
+Sending ID ranges...
+Sending for single ext id...
+Sending for single std id...
+Sending for ext+std ids...
+Sending for std+ext ids...
+Sending for accept none...
+Sending for accept all...
+Sending for accept none again...
+Sender done
diff --git a/tests/multi_extmod/machine_can_04_tx_order.py b/tests/multi_extmod/machine_can_04_tx_order.py
new file mode 100644
index 00000000000..32fa905f99e
--- /dev/null
+++ b/tests/multi_extmod/machine_can_04_tx_order.py
@@ -0,0 +1,174 @@
+from machine import CAN
+import time
+from random import seed, randrange
+import sys
+
+import micropython
+
+micropython.alloc_emergency_exception_buf(256)
+seed(0)
+
+# Testing that transmit order obeys the priority ordering
+
+ID_LOW = 0x500
+ID_HIGH = 0x200
+
+NUM_MSGS = 255
+
+MSG_LEN = 4
+
+can = CAN(1, 500_000)
+
+
+def check_sequence(items, label):
+    # The full range of NUM_MSGS values should have been received or sent, in
+    # order, without duplicates
+    if len(items) != NUM_MSGS:
+        print(label, "wrong count", len(items), "vs", NUM_MSGS)
+    if items == list(range(NUM_MSGS)):
+        print(label, "OK")
+    else:
+        print(label, "error:")
+        print(items)
+
+
+# Receiver
+
+# lists of received messages, one list per ID
+received_low = []
+received_high = []
+
+
+def irq_recv(can):
+    while can.irq().flags() & can.IRQ_RX:
+        can_id, data, flags, _errors = can.recv()
+
+        if can_id == ID_LOW and len(data) == MSG_LEN:
+            received_low.append(data[0])
+        elif can_id == ID_HIGH and len(data) == MSG_LEN:
+            received_high.append(data[0])
+        else:
+            print("unexpected recv", can_id, data, flags)
+
+
+def instance0():
+    can.irq(irq_recv, trigger=can.IRQ_RX, hard=False)
+    can.set_filters(None)  # receive all
+
+    multitest.next()
+
+    multitest.wait("sender done")
+    check_sequence(received_low, "Low prio received")
+    check_sequence(received_high, "High prio received")
+
+
+# Sender
+
+## Messages pending to send
+pending_low = list(range(NUM_MSGS))
+pending_high = list(range(NUM_MSGS))
+
+# List of the messages currently queued to send
+tx_queue = [None] * CAN.TX_QUEUE_LEN
+
+# Messages sent, recorded in order as [high_prio, val]
+sent = []
+for _ in range(NUM_MSGS * 2):
+    sent.append([None, None])
+num_sent = 0
+
+
+def irq_send(can):
+    global num_sent
+
+    while flags := can.irq().flags():
+        assert flags & can.IRQ_TX  # the only enabled IRQ
+
+        idx = (flags >> can.IRQ_TX_IDX_SHIFT) & can.IRQ_TX_IDX_MASK
+        success = not (flags & can.IRQ_TX_FAILED)
+
+        if not success:
+            return  # We don't worry about failures here
+
+        if not tx_queue[idx]:
+            print("bad done", idx, success)
+            return
+
+        was_high, val = tx_queue[idx]
+        tx_queue[idx] = None
+        sent[num_sent][0] = was_high
+        sent[num_sent][1] = val
+        num_sent += 1
+
+
+def instance1():
+    # note: this test can pass with hard=True, but in a debug build
+    # the completion IRQ may race ahead of setting tx_queue[idx], below
+    if "mimxrt" in sys.platform:
+        can.irq(irq_send, trigger=can.IRQ_TX, hard=True)
+    else:
+        can.irq(irq_send, trigger=can.IRQ_TX, hard=False)
+    data = bytearray(MSG_LEN)
+
+    multitest.next()
+
+    while pending_low or pending_high:
+        if pending_high:
+            val = pending_high.pop(0)
+            data[0] = val
+            data[1] = 1
+            while True:
+                idx = can.send(ID_HIGH, data)
+                if idx is None:
+                    continue  # keep trying until a queue spot opens up
+                old = tx_queue[idx]
+                tx_queue[idx] = (True, val)
+                if old:
+                    print("error high priority queue race", idx, val, old)
+                break
+
+        for _ in range(randrange(4)):
+            # Try and queue many low priority messages (expecting most will fail)
+            if pending_low:
+                val = pending_low[0]
+                data[0] = val
+                data[1] = 0
+                idx = can.send(ID_LOW, data)
+                if idx is None:
+                    # don't retry indefinitely for low priority messages
+                    continue
+
+                old = tx_queue[idx]
+                tx_queue[idx] = (False, val)
+                pending_low.pop(0)
+                if old is not None:
+                    print("error low priority queue race", idx, val, old)
+
+    print("waiting for tx queue to empty...")
+    while any(x is not None for x in tx_queue):
+        pass
+
+    multitest.broadcast("sender done")
+
+    # Check we sent the right number of messages
+    if num_sent != 2 * NUM_MSGS:
+        print("Sent %d expected %d" % (num_sent, 2 * NUM_MSGS))
+    else:
+        print("Sent right number of messages")
+
+    # Check the low and high priority messages all arrived in order
+    sent_low = [val for (prio, val) in sent[:num_sent] if prio == False]
+    sent_high = [val for (prio, val) in sent[:num_sent] if prio == True]
+    check_sequence(sent_low, "Low prio sent")
+    check_sequence(sent_high, "High prio sent")
+
+    # check that high priority message queue items always stayed ahead of the low priority
+    high_val = -1
+    for idx, (prio, val) in enumerate(sent):
+        if prio:
+            high_val = val
+        elif high_val <= val and val < NUM_MSGS - 1:
+            print(
+                "Low priority message %d overtook high priority %d at index %d"
+                % (val, high_val, idx)
+            )
diff --git a/tests/multi_extmod/machine_can_04_tx_order.py.exp b/tests/multi_extmod/machine_can_04_tx_order.py.exp
new file mode 100644
index 00000000000..a2de6ee2706
--- /dev/null
+++ b/tests/multi_extmod/machine_can_04_tx_order.py.exp
@@ -0,0 +1,8 @@
+--- instance0 ---
+Low prio received OK
+High prio received OK
+--- instance1 ---
+waiting for tx queue to empty...
+Sent right number of messages
+Low prio sent OK
+High prio sent OK
diff --git a/tests/multi_extmod/machine_can_05_tx_prio_cancel.py b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py
new file mode 100644
index 00000000000..3ed35edefda
--- /dev/null
+++ b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py
@@ -0,0 +1,142 @@
+from machine import CAN
+import time
+import sys
+
+# Check that cancelling a low priority outgoing message and replacing it with a
+# high priority message causes it to be transmitted successfully onto a busy bus
+
+recv = []
+
+ITERS = 5
+
+can = CAN(1, 500_000)
+
+
+def irq_recv(can):
+    global recv_std_id
+    while can.irq().flags() & can.IRQ_RX:
+        can_id, data, flags, _errors = can.recv()
+        assert flags & CAN.FLAG_EXT_ID  # test uses all extended IDs
+        if len(recv) < ITERS:
+            recv.append(can_id)
+
+
+def instance0():
+    can.irq(irq_recv, trigger=can.IRQ_RX, hard=False)
+    can.set_filters(None)  # receive all
+
+    multitest.next()
+
+    # don't babble until we know instance1 is ready to receive, or this instance
+    # may go to Error Passive while instance1 is still initialising CAN (meaning
+    # the "babble" won't saturate the bus, due to the Suspend Transmission
+    # requirement)
+    multitest.wait("instance1 ready")
+
+    bcast_countdown = 5
+
+    # "Babble" medium priority messages onto the bus to prevent
+    # instance1() from sending anything lower priority than this
+    while len(recv) < ITERS:
+        for id in range(0x5000, 0x6000):
+            while can.send(id, b"BABBLE", CAN.FLAG_EXT_ID) is None:
+                pass
+            if len(recv) >= ITERS:
+                break
+            if bcast_countdown > 0:
+                # queue some "babble" messages onto the bus before signalling to
+                # instance1 that it can start trying to send
+                bcast_countdown -= 1
+                if not bcast_countdown:
+                    multitest.broadcast("instance0 babbling")
+
+    print("received", ITERS, "messages")
+    for can_id in recv:
+        print(hex(can_id))  # should be the high priority messages from instance1, only
+
+    multitest.wait("sender done")
+    print("done")
+
+
+last_idx = 0
+total_cancels = 0
+total_sent = 0
+
+
+def irq_send(can):
+    global total_cancels, total_sent
+
+    while flags := can.irq().flags():
+        assert flags & can.IRQ_TX  # the only enabled IRQ
+
+        idx = (flags >> can.IRQ_TX_IDX_SHIFT) & can.IRQ_TX_IDX_MASK
+
+        if flags & can.IRQ_TX_FAILED:
+            # we should only see failed transmits due to cancels in buffer 'last_idx'
+            assert idx == last_idx
+            total_cancels += 1
+        else:
+            # this includes the messages we explicitly send, plus queued low
+            # priority messages once the receiver stops 'babbling' on the bus
+            total_sent += 1
+
+
+def instance1():
+    global last_idx, total_cancels
+    can.irq(irq_send, trigger=can.IRQ_TX, hard=True)
+
+    multitest.next()
+    multitest.broadcast("instance1 ready")
+
+    # make sure instance0 can queue outgoing medium-priority
+    # babble before we start trying to send, so we're trying to
+    # send onto an already busy bus
+    multitest.wait("instance0 babbling")
+
+    for i in range(ITERS):
+        # Fill the transmit queue with low priority messages (all extended IDs)
+        last_idx = 0
+        if i < 3:
+            # For the first 3 iterations, send unique message IDs
+            id_range = range(0x7000, 0x7FFF)
+            flags = CAN.FLAG_EXT_ID
+        else:
+            # For the last iterations, repeat the same ID but tell controller to ignore
+            # ordering (allows it to queue more than one despite hardware limitations)
+            id_range = [0x50000 + i] * CAN.TX_QUEUE_LEN
+            flags = CAN.FLAG_EXT_ID | CAN.FLAG_UNORDERED
+
+        for id in id_range:
+            idx = can.send(id, b"LOWPRIO", flags)
+            if idx is None:
+                break  # send queue is full, stop trying to send
+            last_idx = idx
+
+        time.sleep_ms(50)  # the send queue shouldn't empty as instance0 is "babbling"
+
+        # try and cancel the last message we queued
+        res = can.cancel_send(last_idx)
+        print(i, "cancel result", res)
+        if ("mimxrt" in sys.platform) and res:
+            total_cancels += 1
+
+        # send a high priority message, that we expect to go out
+        idx = can.send(0x500 + i, b"HIPRIO", CAN.FLAG_EXT_ID)
+        print(i, "send result", idx is not None)
+
+        # make sure this message is sent onto the bus
+        time.sleep_ms(1)
+
+    multitest.broadcast("sender done")
+
+    # let the entire transmit queue drain, now instance0 should have gone quiet
+    time.sleep_ms(50)
+
+    print("total cancels", total_cancels)  # should equal ITERS
+
+    if total_sent == CAN.TX_QUEUE_LEN - 1 + ITERS:
+        # expect we send one message for each of ITERS, plus all low priority
+        # queued messages once instance0 stops babbling on the bus
+        print("total sent OK")
+    else:
+        print("total sent", total_sent, CAN.TX_QUEUE_LEN - 1 + ITERS)
diff --git a/tests/multi_extmod/machine_can_05_tx_prio_cancel.py.exp b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py.exp
new file mode 100644
index 00000000000..26bab097c44
--- /dev/null
+++ b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py.exp
@@ -0,0 +1,21 @@
+--- instance0 ---
+received 5 messages
+0x500
+0x501
+0x502
+0x503
+0x504
+done
+--- instance1 ---
+0 cancel result True
+0 send result True
+1 cancel result True
+1 send result True
+2 cancel result True
+2 send result True
+3 cancel result True
+3 send result True
+4 cancel result True
+4 send result True
+total cancels 5
+total sent OK
diff --git a/tests/multi_extmod/machine_can_06_remote_req.py b/tests/multi_extmod/machine_can_06_remote_req.py
new file mode 100644
index 00000000000..4a1414c0946
--- /dev/null
+++ b/tests/multi_extmod/machine_can_06_remote_req.py
@@ -0,0 +1,70 @@
+from machine import CAN
+import time
+
+# Test CAN remote transmission requests
+
+ID = 0x750
+
+FILTER_ID = 0x101
+FILTER_MASK = 0x7FF
+
+can = CAN(1, 500_000)
+
+
+def receiver_irq_recv(can):
+    assert can.irq().flags() & can.IRQ_RX  # the only enabled IRQ
+    can_id, data, flags, _errors = can.recv()
+    is_rtr = flags == CAN.FLAG_RTR
+    print(
+        "recv",
+        hex(can_id),
+        is_rtr,
+        len(data) if is_rtr else bytes(data),
+    )
+    if is_rtr:
+        # The 'data' response of a remote request should be all zeroes
+        assert bytes(data) == b"\x00" * len(data)
+
+
+# Receiver
+def instance0():
+    can.irq(receiver_irq_recv, trigger=can.IRQ_RX, hard=False)
+    can.set_filters(None)  # receive all
+
+    multitest.next()
+
+    multitest.wait("enable filter")
+    can.set_filters([(FILTER_ID, FILTER_MASK, 0)])
+    multitest.broadcast("filter set")
+
+    multitest.wait("done")
+
+
+# Sender
+def instance1():
+    multitest.next()
+
+    can.send(ID, b"abc", CAN.FLAG_RTR)  # length==3 remote request
+    time.sleep_ms(5)
+    can.send(ID, b"abc", 0)  # regular message using the same ID
+    time.sleep_ms(5)
+    can.send(ID, b"abcde", CAN.FLAG_RTR)  # length==5 remote request
+    time.sleep_ms(5)
+
+    multitest.broadcast("enable filter")
+    multitest.wait("filter set")
+
+    # these two messages should be filtered out
+    can.send(ID, b"abc", CAN.FLAG_RTR)  # length==3 remote request
+    time.sleep_ms(5)
+    can.send(ID, b"abc", 0)  # regular message using the same ID
+    time.sleep_ms(5)
+
+    # these messages should be filtered in
+    can.send(FILTER_ID, b"def", CAN.FLAG_RTR)  # length==3 remote request
+    time.sleep_ms(5)
+    can.send(FILTER_ID, b"hij", 0)  # regular message using the same ID
+    time.sleep_ms(5)
+
+    multitest.broadcast("done")
+    print("done")
diff --git a/tests/multi_extmod/machine_can_06_remote_req.py.exp b/tests/multi_extmod/machine_can_06_remote_req.py.exp
new file mode 100644
index 00000000000..fe94508ba3d
--- /dev/null
+++ b/tests/multi_extmod/machine_can_06_remote_req.py.exp
@@ -0,0 +1,8 @@
+--- instance0 ---
+recv 0x750 True 3
+recv 0x750 False b'abc'
+recv 0x750 True 5
+recv 0x101 True 3
+recv 0x101 False b'hij'
+--- instance1 ---
+done
diff --git a/tests/multi_extmod/machine_can_07_error_states.py b/tests/multi_extmod/machine_can_07_error_states.py
new file mode 100644
index 00000000000..f7071a8bd77
--- /dev/null
+++ b/tests/multi_extmod/machine_can_07_error_states.py
@@ -0,0 +1,211 @@
+from machine import CAN
+from micropython import const
+import time
+
+# Test that without a second CAN node on the network the controller will
+# correctly go into the correct error states, and can then recover.
+#
+# Note this test depends on no other CAN node being active on the network apart
+# from the two test instances. (Although it's OK for extra nodes to be in a
+# "listen only" mode where they won't ACK messages.)
+
+rx_overflow = False
+rx_full = False
+received = []
+
+# CAN IDs
+_ID = const(0x100)
+
+# can.state() result list indexes, as constants
+_IDX_TEC = const(0)
+_IDX_REC = const(1)
+_IDX_NUM_WARNING = const(2)
+_IDX_NUM_PASSIVE = const(3)
+_IDX_NUM_BUS_OFF = const(4)
+_IDX_PEND_TX = const(5)
+
+can = CAN(1, 500_000)
+
+
+def state_name(state):
+    for name in dir(CAN):
+        if name.startswith("STATE_") and state == getattr(CAN, name):
+            return name
+    return f"UNKNOWN-{state}"
+
+
+def irq_recv(can):
+    while can.irq().flags() & can.IRQ_RX:
+        can_id, data, _flags, errors = can.recv()
+        print("recv", hex(can_id), data.hex())
+
+
+# Receiver
+def instance0():
+    can.irq(irq_recv, trigger=can.IRQ_RX, hard=False)
+
+    can.set_filters(None)  # receive all
+
+    multitest.next()
+
+    # Receive at least one CAN message before sender asks us to disable the controller
+
+    multitest.wait("disable receiver")
+    can.deinit()
+    print("can deinit()")
+    multitest.broadcast("receiver disabled")
+
+    # Wait for the sender to tell us to re-enable
+
+    multitest.wait("enable receiver")
+    # note the irq is no longer active after deinit()
+    can.init(500_000)
+    print("can init()")
+    multitest.broadcast("receiver enabled")
+
+    # Receive CAN messages until the sender asks us to switch to an invalid baud rate
+
+    multitest.wait("switch baud")
+    can.init(125_000)
+    print("can switch baud")
+    multitest.broadcast("switched baud")
+
+    time.sleep_ms(1)
+
+    print("sending bad msg")
+    # trying to send this frame should introduce more bus errors
+    idx_bad = can.send(_ID, b"BADBAUD")
+
+    multitest.wait("fix baud")
+    print("sending cancelling")
+    print("cancelled", can.cancel_send(idx_bad))
+    print("re-init")
+    can.init(500_000)
+    multitest.broadcast("fixed baud")
+
+    # Should be receiving CAN messages OK again
+
+    print("done")
+
+
+def irq_sender(can):
+    while flags := can.irq().flags():
+        if flags & can.IRQ_STATE:
+            print("irq state", can.state())
+        if flags & can.IRQ_TX:
+            print("irq sent", not (flags & can.IRQ_TX_FAILED))
+
+
+# Sender
+def instance1():
+    can.irq(irq_sender, CAN.IRQ_STATE | CAN.IRQ_TX, hard=False)
+
+    can.set_filters(None)
+
+    multitest.next()
+
+    print("started", state_name(can.state()))  # should be ERROR_ACTIVE
+
+    # Send a single message to the receiver, to verify it's working
+    can.send(_ID, b"PAYLOAD")
+    time.sleep_ms(50)  # irq_sender should fire during this window
+
+    active_counters = can.get_counters()
+    # print(active_counters)  # DEBUG
+
+    multitest.broadcast("disable receiver")
+    multitest.wait("receiver disabled")
+
+    # Now the receiver shouldn't be ACKing our frames, queue will stay full
+    # ... we will get the ISR for ERROR_WARNING but it'll go from ERROR_WARNING to ERROR_PASSIVE
+    # very quickly as all messages are failing
+    can.send(_ID, b"MORE")
+    while can.state() in (CAN.STATE_ACTIVE, CAN.STATE_WARNING):
+        pass
+
+    print(state_name(can.state()))  # should be ERROR_PASSIVE now
+    passive_counters = can.get_counters()
+    # print(passive_counters)  # DEBUG
+    print("tec increased", passive_counters[_IDX_TEC] > active_counters[_IDX_TEC])
+    print("tec over thresh", passive_counters[_IDX_TEC] >= 128)
+    # we should have counted exactly one ERROR_WARNING and ERROR_PASSIVE transition
+    print(
+        "counted warning",
+        passive_counters[_IDX_NUM_WARNING] == active_counters[_IDX_NUM_WARNING] + 1,
+    )
+    print(
+        "counted passive",
+        passive_counters[_IDX_NUM_PASSIVE] == active_counters[_IDX_NUM_PASSIVE] + 1,
+    )
+    print("some pending tx", passive_counters[_IDX_PEND_TX] > 0)
+
+    # Re-enable the receiver which should allow us to go from ERROR_PASSIVE to ERROR_WARNING
+    multitest.broadcast("enable receiver")
+    multitest.wait("receiver enabled")
+
+    can.send(_ID, b"MORE")
+    while can.state() == CAN.STATE_PASSIVE:
+        pass
+
+    print(state_name(can.state()))  # should be ERROR_WARNING now
+    warning_counters = can.get_counters()
+    # print(warning_counters)  # DEBUG
+    print("tec decreased", warning_counters[_IDX_TEC] < passive_counters[_IDX_TEC])
+    print(
+        "tec below thresh", warning_counters[_IDX_TEC] < 128
+    )  # and should be more than error passive threshold
+    # error warning count should stay the same, as we went "down" in severity not up
+    print(
+        "no new warning", warning_counters[_IDX_NUM_WARNING] == passive_counters[_IDX_NUM_WARNING]
+    )
+    print(
+        "no new passive", warning_counters[_IDX_NUM_PASSIVE] == passive_counters[_IDX_NUM_PASSIVE]
+    )
+
+    # Tell the receiver to change to the wrong baud rate, which should create both RX and TX errorxs
+    multitest.broadcast("switch baud")
+    multitest.wait("switched baud")
+
+    # queue another message. This will keep trying to send until we revert back to ERROR_PASSIVE
+    idx = can.send(_ID, b"YETMORE")
+    print("queued yetmore", idx is not None)
+    while can.state() != CAN.STATE_PASSIVE:
+        pass
+
+    print(state_name(can.state()))  # should be ERROR_PASSIVE again
+    passive_counters = can.get_counters()
+    # print(passive_counters)  # DEBUG
+    # we can't say for sure which error counter will hit the ERROR_PASSIVE threshold first
+    print(
+        "one over thresh", passive_counters[_IDX_TEC] >= 128 or passive_counters[_IDX_REC] >= 128
+    )
+    print(
+        "no new warning", passive_counters[_IDX_NUM_WARNING] == warning_counters[_IDX_NUM_WARNING]
+    )
+    print(
+        "counted passive",
+        passive_counters[_IDX_NUM_PASSIVE] == warning_counters[_IDX_NUM_PASSIVE] + 1,
+    )
+
+    # Note that we can't get all the way to the most severe BUS_OFF error state
+    # with this test setup, as Bus Off requires more than just "normal" frame
+    # transmit errors.
+
+    # tell the receiver to go back to a valid baud rate, causing the pending tx
+    # to be sent.
+    multitest.broadcast("fix baud")
+    multitest.wait("fixed baud")
+
+    # Restarting the controller may cause it to leave its error state, or not, depending
+    # on the implementation - but it shouldn't cause any recovery issues. Also cancels all pending TX.
+    # If restart() clears the TEC and REC error counters, resetting the error state to ACTIVE,
+    # and if that is done while instance0 is still trying to send at the wrong baud rate,
+    # the error state rushes up again pretty fast. Therefore the baud rate is fixed before
+    # calling restart().
+    can.restart()
+
+    idx_more = can.send(_ID, b"MOREMORE")
+    time.sleep_ms(50)  # irq_sender should fire during this window
+    print("queued moremore", idx_more is not None)
+
+    print("done")
diff --git a/tests/multi_extmod/machine_can_07_error_states.py.exp b/tests/multi_extmod/machine_can_07_error_states.py.exp
new file mode 100644
index 00000000000..c2731f20905
--- /dev/null
+++ b/tests/multi_extmod/machine_can_07_error_states.py.exp
@@ -0,0 +1,38 @@
+--- instance0 ---
+recv 0x100 5041594c4f4144
+can deinit()
+can init()
+can switch baud
+sending bad msg
+sending cancelling
+cancelled True
+re-init
+done
+--- instance1 ---
+started STATE_ACTIVE
+irq sent True
+irq state 2
+irq state 3
+STATE_PASSIVE
+tec increased True
+tec over thresh True
+counted warning True
+counted passive True
+some pending tx True
+irq sent True
+irq sent True
+STATE_WARNING
+tec decreased True
+tec below thresh True
+no new warning True
+no new passive True
+irq state 3
+queued yetmore True
+STATE_PASSIVE
+one over thresh True
+no new warning True
+counted passive True
+irq sent True
+irq sent True
+queued moremore True
+done
diff --git a/tests/multi_extmod/machine_can_08_init_mode.py b/tests/multi_extmod/machine_can_08_init_mode.py
new file mode 100644
index 00000000000..37768030a26
--- /dev/null
+++ b/tests/multi_extmod/machine_can_08_init_mode.py
@@ -0,0 +1,112 @@
+from machine import CAN
+from micropython import const
+import time
+import sys
+
+# instance0 transitions through various modes, instance1
+# listens for various messages (or not)
+#
+# Note this test assumes no other CAN nodes are connected apart from the test
+# instances (or if they are connected they must be in silent mode.)
+#
+# TODO: This test needs to eventually support the case where modes aren't supported
+# on a controller, maybe by printing fake output if the mode switch fails?
+
+# MODE_NORMAL, MODE_SLEEP, MODE_LOOPBACK, MODE_SILENT, MODE_SILENT_LOOPBACK
+can = CAN(1, 500_000, mode=CAN.MODE_NORMAL)
+
+# While instance0 is in Silent mode, instance1 sends a message with this ID
+# that will be retried for 100ms (as instance0 won't ACK). So don't print every one.
+_SILENT_RX_ID = const(0x53)
+silent_rx_count = 0
+
+
+def irq_print(can):
+    global silent_rx_count
+    while flags := can.irq().flags():
+        if flags & can.IRQ_RX:
+            can_id, data, _flags, errors = can.recv()
+            if can_id != _SILENT_RX_ID:
+                print("recv", hex(can_id), bytes(data))
+            else:
+                silent_rx_count += 1
+        elif flags & can.IRQ_TX:  # note: only enabled on instance1 to avoid race conditions
+            print("send", "failed" if flags & can.IRQ_TX_FAILED else "ok")
+
+
+def reinit_with_mode(mode):
+    can.deinit()
+    can.init(bitrate=500_000, mode=mode)
+    can.irq(irq_print, trigger=can.IRQ_RX, hard=False)
+    can.set_filters(None)  # receive all
+
+
+def instance0():
+    multitest.next()
+    multitest.wait("instance1 ready")
+
+    reinit_with_mode(can.MODE_NORMAL)
+    print("Normal", "MODE_NORMAL" in str(can))
+    can.send(0x50, b"Normal")
+    time.sleep_ms(100)
+
+    # Skipping MODE_SLEEP as means different things on different hardware
+
+    reinit_with_mode(can.MODE_LOOPBACK)
+    print("Loopback", "MODE_LOOPBACK" in str(can))
+
+    # This message should go out to the bus, but will also be received by instance0 itself
+    can.send(0x51, b"Loopback")
+    time.sleep_ms(100)
+
+    reinit_with_mode(can.MODE_SILENT)
+    print("Silent", "MODE_SLIENT" in str(can))
+
+    # This message shouldn't go out onto the bus
+    idx = can.send(0x52, b"Silent")
+    multitest.broadcast("silent")
+    multitest.wait("silent done")
+    # we should have received the message from instance1 many times, as instance0 won't have ACKed it
+    # create a dummy "OK" for MIMXRT, since it on receives ACKed messages in SILENT mode.
+    if ("mimxrt" in sys.platform) or ("alif" in sys.platform):
+        print("silent_rx_count True")
+    else:
+        print("silent_rx_count", silent_rx_count > 5)
+    # Cancel the sent message only if it was accpeted for sending.
+    if idx is not None:
+        can.cancel_send(idx)
+
+    reinit_with_mode(can.MODE_SILENT_LOOPBACK)
+    print("Silent Loopback", "MODE_SILENT_LOOPBACK" in str(can))
+
+    # This message should be received by instance0 only
+    idx = can.send(0x54, b"SiLoop")
+    time.sleep_ms(50)
+
+    reinit_with_mode(can.MODE_NORMAL)
+    print("Normal again", "MODE_NORMAL" in str(can))
+    can.send(0x55, b"Normal2")  # should be received by instance1 only, again
+    multitest.broadcast("normal done")
+
+
+# Receiver
+def instance1():
+    can.irq(irq_print, trigger=can.IRQ_RX | can.IRQ_TX, hard=False)
+    can.set_filters(None)  # receive all
+    multitest.next()
+
+    multitest.broadcast("instance1 ready")
+
+    # The IRQ does most of the work on this instance
+
+    multitest.wait("silent")
+    # Sending this message back, it should fail to send as Silent mode won't ACK it
+    idx = can.send(0x53, b"Silent2")
+    time.sleep_ms(20)
+    can.cancel_send(idx)
+    # mimxrt does not give TX failed interrupts on cancel_send.
+    if "mimxrt" in sys.platform:
+        print("send failed")
+    multitest.broadcast("silent done")
+
+    multitest.wait("normal done")
diff --git a/tests/multi_extmod/machine_can_08_init_mode.py.exp b/tests/multi_extmod/machine_can_08_init_mode.py.exp
new file mode 100644
index 00000000000..b9f0f11ae10
--- /dev/null
+++ b/tests/multi_extmod/machine_can_08_init_mode.py.exp
@@ -0,0 +1,14 @@
+--- instance0 ---
+Normal True
+Loopback True
+recv 0x51 b'Loopback'
+Silent False
+silent_rx_count True
+Silent Loopback True
+recv 0x54 b'SiLoop'
+Normal again True
+--- instance1 ---
+recv 0x50 b'Normal'
+recv 0x51 b'Loopback'
+send failed
+recv 0x55 b'Normal2'
diff --git a/tests/multi_extmod/machine_i2c_target_irq.py b/tests/multi_extmod/machine_i2c_target_irq.py
new file mode 100644
index 00000000000..eafd9dfdca8
--- /dev/null
+++ b/tests/multi_extmod/machine_i2c_target_irq.py
@@ -0,0 +1,137 @@
+# Test I2CTarget IRQs and clock stretching.
+#
+# Requires two instances with their SCL and SDA lines connected together.
+# Any combination of the below supported boards can be used.
+#
+# Notes:
+# - pull-up resistors may be needed
+# - alif use 1.8V signalling
+
+import sys
+import time
+from machine import I2C, I2CTarget
+
+if not hasattr(I2CTarget, "IRQ_ADDR_MATCH_READ"):
+    print("SKIP")
+    raise SystemExit
+
+ADDR = 67
+clock_stretch_us = 200
+
+# Configure pins based on the target.
+if sys.platform == "alif":
+    i2c_args = (1,)  # pins P3_7/P3_6
+    i2c_kwargs = {}
+elif sys.platform == "mimxrt":
+    i2c_args = (0,)  # pins 19/18 on Teensy 4.x
+    i2c_kwargs = {}
+    clock_stretch_us = 50  # mimxrt cannot delay too long in the IRQ handler
+elif sys.platform == "rp2":
+    i2c_args = (0,)
+    i2c_kwargs = {"scl": 9, "sda": 8}
+elif sys.platform == "pyboard":
+    i2c_args = ("Y",)
+    i2c_kwargs = {}
+elif sys.platform == "samd":
+    i2c_args = ()  # pins SCL/SDA
+    i2c_kwargs = {}
+elif "zephyr-rpi_pico" in sys.implementation._machine:
+    i2c_args = ("i2c1",)  # on gpio7/gpio6
+    i2c_kwargs = {}
+else:
+    print("Please add support for this test on this platform.")
+    raise SystemExit
+
+
+def simple_irq(i2c_target):
+    flags = i2c_target.irq().flags()
+    if flags & I2CTarget.IRQ_ADDR_MATCH_READ:
+        print("IRQ_ADDR_MATCH_READ")
+    if flags & I2CTarget.IRQ_ADDR_MATCH_WRITE:
+        print("IRQ_ADDR_MATCH_WRITE")
+
+    # Force clock stretching.
+    time.sleep_us(clock_stretch_us)
+
+
+class I2CTargetMemory:
+    def __init__(self, i2c_target, mem):
+        self.buf1 = bytearray(1)
+        self.mem = mem
+        self.memaddr = 0
+        self.state = 0
+        i2c_target.irq(
+            self.irq,
+            I2CTarget.IRQ_ADDR_MATCH_WRITE | I2CTarget.IRQ_READ_REQ | I2CTarget.IRQ_WRITE_REQ,
+            hard=True,
+        )
+
+    def irq(self, i2c_target):
+        # Force clock stretching.
+        time.sleep_us(clock_stretch_us)
+
+        flags = i2c_target.irq().flags()
+        if flags & I2CTarget.IRQ_ADDR_MATCH_WRITE:
+            self.state = 0
+        if flags & I2CTarget.IRQ_READ_REQ:
+            self.buf1[0] = self.mem[self.memaddr]
+            self.memaddr += 1
+            i2c_target.write(self.buf1)
+        if flags & I2CTarget.IRQ_WRITE_REQ:
+            i2c_target.readinto(self.buf1)
+            if self.state == 0:
+                self.state = 1
+                self.memaddr = self.buf1[0]
+            else:
+                self.mem[self.memaddr] = self.buf1[0]
+                self.memaddr += 1
+        self.memaddr %= len(self.mem)
+
+        # Force clock stretching.
+        time.sleep_us(clock_stretch_us)
+
+
+# I2C controller
+def instance0():
+    i2c = I2C(*i2c_args, **i2c_kwargs)
+    multitest.next()
+    for iteration in range(2):
+        print("controller iteration", iteration)
+        multitest.wait("target stage 1")
+        i2c.writeto_mem(ADDR, 2, "0123")
+        multitest.broadcast("controller stage 2")
+        multitest.wait("target stage 3")
+        print(i2c.readfrom_mem(ADDR, 2, 4))
+        multitest.broadcast("controller stage 4")
+    print("done")
+
+
+# I2C target
+def instance1():
+    multitest.next()
+
+    for iteration in range(2):
+        print("target iteration", iteration)
+        buf = bytearray(b"--------")
+        if iteration == 0:
+            # Use built-in memory capability of I2CTarget.
+            i2c_target = I2CTarget(*i2c_args, **i2c_kwargs, addr=ADDR, mem=buf)
+            i2c_target.irq(
+                simple_irq,
+                I2CTarget.IRQ_ADDR_MATCH_READ | I2CTarget.IRQ_ADDR_MATCH_WRITE,
+                hard=True,
+            )
+        else:
+            # Implement a memory device by hand.
+            i2c_target = I2CTarget(*i2c_args, **i2c_kwargs, addr=ADDR)
+            I2CTargetMemory(i2c_target, buf)
+
+        multitest.broadcast("target stage 1")
+        multitest.wait("controller stage 2")
+        print(buf)
+        multitest.broadcast("target stage 3")
+        multitest.wait("controller stage 4")
+
+        i2c_target.deinit()
+
+    print("done")
diff --git a/tests/multi_extmod/machine_i2c_target_irq.py.exp b/tests/multi_extmod/machine_i2c_target_irq.py.exp
new file mode 100644
index 00000000000..a17c8f43858
--- /dev/null
+++ b/tests/multi_extmod/machine_i2c_target_irq.py.exp
@@ -0,0 +1,15 @@
+--- instance0 ---
+controller iteration 0
+b'0123'
+controller iteration 1
+b'0123'
+done
+--- instance1 ---
+target iteration 0
+IRQ_ADDR_MATCH_WRITE
+bytearray(b'--0123--')
+IRQ_ADDR_MATCH_WRITE
+IRQ_ADDR_MATCH_READ
+target iteration 1
+bytearray(b'--0123--')
+done
diff --git a/tests/multi_extmod/machine_i2c_target_memory.py b/tests/multi_extmod/machine_i2c_target_memory.py
new file mode 100644
index 00000000000..6b3f0d03eb7
--- /dev/null
+++ b/tests/multi_extmod/machine_i2c_target_memory.py
@@ -0,0 +1,79 @@
+# Test basic use of I2CTarget and a memory buffer.
+#
+# Requires two instances with their SCL and SDA lines connected together.
+# Any combination of the below supported boards can be used.
+#
+# Notes:
+# - pull-up resistors may be needed
+# - alif use 1.8V signalling
+
+import sys
+from machine import I2C, I2CTarget
+
+ADDR = 67
+
+# Configure pins based on the target.
+if sys.platform == "alif":
+    i2c_args = (1,)  # pins P3_7/P3_6
+    i2c_kwargs = {}
+elif sys.platform == "esp32":
+    i2c_args = (1,)  # on pins 9/8
+    i2c_kwargs = {}
+elif sys.platform == "mimxrt":
+    i2c_args = (0,)  # pins 19/18 on Teensy 4.x
+    i2c_kwargs = {}
+elif sys.platform == "rp2":
+    i2c_args = (0,)
+    i2c_kwargs = {"scl": 9, "sda": 8}
+elif sys.platform == "pyboard":
+    i2c_args = ("Y",)
+    i2c_kwargs = {}
+elif sys.platform == "samd":
+    i2c_args = ()  # pins SCL/SDA
+    i2c_kwargs = {}
+elif "zephyr-rpi_pico" in sys.implementation._machine:
+    i2c_args = ("i2c1",)  # on gpio7/gpio6
+    i2c_kwargs = {}
+else:
+    print("Please add support for this test on this platform.")
+    raise SystemExit
+
+
+def simple_irq(i2c_target):
+    flags = i2c_target.irq().flags()
+    if flags & I2CTarget.IRQ_END_READ:
+        print("IRQ_END_READ", i2c_target.memaddr)
+    if flags & I2CTarget.IRQ_END_WRITE:
+        print("IRQ_END_WRITE", i2c_target.memaddr)
+
+
+# I2C controller
+def instance0():
+    i2c = I2C(*i2c_args, **i2c_kwargs)
+    multitest.next()
+    for iteration in range(2):
+        print("controller iteration", iteration)
+        multitest.wait("target stage 1")
+        i2c.writeto_mem(ADDR, 2 + iteration, "0123")
+        multitest.broadcast("controller stage 2")
+        multitest.wait("target stage 3")
+        print(i2c.readfrom_mem(ADDR, 2 + iteration, 4))
+        multitest.broadcast("controller stage 4")
+    print("done")
+
+
+# I2C target
+def instance1():
+    buf = bytearray(b"--------")
+    i2c_target = I2CTarget(*i2c_args, **i2c_kwargs, addr=ADDR, mem=buf)
+    i2c_target.irq(simple_irq)
+    multitest.next()
+    for iteration in range(2):
+        print("target iteration", iteration)
+        multitest.broadcast("target stage 1")
+        multitest.wait("controller stage 2")
+        print(buf)
+        multitest.broadcast("target stage 3")
+        multitest.wait("controller stage 4")
+    i2c_target.deinit()
+    print("done")
diff --git a/tests/multi_extmod/machine_i2c_target_memory.py.exp b/tests/multi_extmod/machine_i2c_target_memory.py.exp
new file mode 100644
index 00000000000..71386cfe769
--- /dev/null
+++ b/tests/multi_extmod/machine_i2c_target_memory.py.exp
@@ -0,0 +1,16 @@
+--- instance0 ---
+controller iteration 0
+b'0123'
+controller iteration 1
+b'0123'
+done
+--- instance1 ---
+target iteration 0
+IRQ_END_WRITE 2
+bytearray(b'--0123--')
+IRQ_END_READ 2
+target iteration 1
+IRQ_END_WRITE 3
+bytearray(b'--00123-')
+IRQ_END_READ 3
+done
diff --git a/tests/multi_net/asyncio_tcp_client_rst.py b/tests/multi_net/asyncio_tcp_client_rst.py
new file mode 100644
index 00000000000..af70c2e22ee
--- /dev/null
+++ b/tests/multi_net/asyncio_tcp_client_rst.py
@@ -0,0 +1,53 @@
+# Test TCP server with client issuing TCP RST part way through read
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+import struct, time, socket
+
+PORT = 8000
+
+
+async def handle_connection(reader, writer):
+    data = await reader.read(10)  # should succeed
+    print(data)
+    await asyncio.sleep(0.2)  # wait for client to drop connection
+    try:
+        data = await reader.read(100)
+        print(data)
+        writer.close()
+        await writer.wait_closed()
+    except OSError as er:
+        print("OSError", er.errno)
+    ev.set()
+
+
+async def main():
+    global ev
+    ev = asyncio.Event()
+    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT)
+    multitest.next()
+    async with server:
+        await asyncio.wait_for(ev.wait(), 10)
+
+
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    asyncio.run(main())
+
+
+def instance1():
+    if not hasattr(socket, "SO_LINGER"):
+        multitest.skip()
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    lgr_onoff = 1
+    lgr_linger = 0
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", lgr_onoff, lgr_linger))
+    s.send(b"GET / HTTP/1.0\r\n\r\n")
+    time.sleep(0.1)
+    s.close()  # This issues a TCP RST since we've set the linger option
diff --git a/tests/multi_net/uasyncio_tcp_client_rst.py.exp b/tests/multi_net/asyncio_tcp_client_rst.py.exp
similarity index 100%
rename from tests/multi_net/uasyncio_tcp_client_rst.py.exp
rename to tests/multi_net/asyncio_tcp_client_rst.py.exp
diff --git a/tests/multi_net/asyncio_tcp_close_write.py b/tests/multi_net/asyncio_tcp_close_write.py
new file mode 100644
index 00000000000..ea206c9219a
--- /dev/null
+++ b/tests/multi_net/asyncio_tcp_close_write.py
@@ -0,0 +1,66 @@
+# Test asyncio TCP stream closing then writing
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+
+async def handle_connection(reader, writer):
+    # Write data to ensure connection
+    writer.write(b"x")
+    await writer.drain()
+
+    # Read, should return nothing
+    print("read:", await reader.read(100))
+
+    # Close connection
+    print("close")
+    writer.close()
+    await writer.wait_closed()
+
+    print("done")
+    ev.set()
+
+
+async def tcp_server():
+    global ev
+    ev = asyncio.Event()
+    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT)
+    print("server running")
+    multitest.next()
+    async with server:
+        await asyncio.wait_for(ev.wait(), 10)
+
+
+async def tcp_client():
+    reader, writer = await asyncio.open_connection(IP, PORT)
+
+    # Read data to ensure connection
+    print("read:", await reader.read(1))
+
+    # Close connection
+    print("close")
+    writer.close()
+    await writer.wait_closed()
+
+    # Try writing data to the closed connection
+    print("write")
+    try:
+        writer.write(b"x")
+        await writer.drain()
+    except OSError:
+        print("OSError")
+
+
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    asyncio.run(tcp_server())
+
+
+def instance1():
+    multitest.next()
+    asyncio.run(tcp_client())
diff --git a/tests/multi_net/uasyncio_tcp_close_write.py.exp b/tests/multi_net/asyncio_tcp_close_write.py.exp
similarity index 100%
rename from tests/multi_net/uasyncio_tcp_close_write.py.exp
rename to tests/multi_net/asyncio_tcp_close_write.py.exp
diff --git a/tests/multi_net/asyncio_tcp_readall.py b/tests/multi_net/asyncio_tcp_readall.py
new file mode 100644
index 00000000000..45eac229d23
--- /dev/null
+++ b/tests/multi_net/asyncio_tcp_readall.py
@@ -0,0 +1,57 @@
+# Test asyncio stream read(-1) method using TCP server/client
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+
+async def handle_connection(reader, writer):
+    writer.write(b"a")
+    await writer.drain()
+
+    # Split the first 2 bytes up so the client must wait for the second one
+    await asyncio.sleep(1)
+
+    writer.write(b"b")
+    await writer.drain()
+
+    writer.write(b"c")
+    await writer.drain()
+
+    print("close")
+    writer.close()
+    await writer.wait_closed()
+
+    print("done")
+    ev.set()
+
+
+async def tcp_server():
+    global ev
+    ev = asyncio.Event()
+    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT)
+    print("server running")
+    multitest.next()
+    async with server:
+        await asyncio.wait_for(ev.wait(), 10)
+
+
+async def tcp_client():
+    reader, writer = await asyncio.open_connection(IP, PORT)
+    print(await reader.read())
+    print(await reader.read())  # should be empty
+    print(await reader.read(1))  # should be empty
+
+
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    asyncio.run(tcp_server())
+
+
+def instance1():
+    multitest.next()
+    asyncio.run(tcp_client())
diff --git a/tests/multi_net/uasyncio_tcp_readall.py.exp b/tests/multi_net/asyncio_tcp_readall.py.exp
similarity index 100%
rename from tests/multi_net/uasyncio_tcp_readall.py.exp
rename to tests/multi_net/asyncio_tcp_readall.py.exp
diff --git a/tests/multi_net/asyncio_tcp_readexactly.py b/tests/multi_net/asyncio_tcp_readexactly.py
new file mode 100644
index 00000000000..4024e23fedb
--- /dev/null
+++ b/tests/multi_net/asyncio_tcp_readexactly.py
@@ -0,0 +1,65 @@
+# Test asyncio stream readexactly() method using TCP server/client
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+
+async def handle_connection(reader, writer):
+    writer.write(b"a")
+    await writer.drain()
+
+    # Split the first 2 bytes up so the client must wait for the second one
+    await asyncio.sleep(1)
+
+    writer.write(b"b")
+    await writer.drain()
+
+    writer.write(b"c")
+    await writer.drain()
+
+    writer.write(b"d")
+    await writer.drain()
+
+    print("close")
+    writer.close()
+    await writer.wait_closed()
+
+    print("done")
+    ev.set()
+
+
+async def tcp_server():
+    global ev
+    ev = asyncio.Event()
+    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT)
+    print("server running")
+    multitest.next()
+    async with server:
+        await asyncio.wait_for(ev.wait(), 10)
+
+
+async def tcp_client():
+    reader, writer = await asyncio.open_connection(IP, PORT)
+    print(await reader.readexactly(2))
+    print(await reader.readexactly(0))
+    print(await reader.readexactly(1))
+    try:
+        print(await reader.readexactly(2))
+    except EOFError as er:
+        print("EOFError")
+    print(await reader.readexactly(0))
+
+
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    asyncio.run(tcp_server())
+
+
+def instance1():
+    multitest.next()
+    asyncio.run(tcp_client())
diff --git a/tests/multi_net/uasyncio_tcp_readexactly.py.exp b/tests/multi_net/asyncio_tcp_readexactly.py.exp
similarity index 100%
rename from tests/multi_net/uasyncio_tcp_readexactly.py.exp
rename to tests/multi_net/asyncio_tcp_readexactly.py.exp
diff --git a/tests/multi_net/asyncio_tcp_readinto.py b/tests/multi_net/asyncio_tcp_readinto.py
new file mode 100644
index 00000000000..972195f96dc
--- /dev/null
+++ b/tests/multi_net/asyncio_tcp_readinto.py
@@ -0,0 +1,69 @@
+# Test asyncio stream readinto() method using TCP server/client
+
+try:
+    import array
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+
+async def handle_connection(reader, writer):
+    writer.write(b"ab")
+    await writer.drain()
+
+    writer.write(b"c")
+    await writer.drain()
+
+    print("close")
+    writer.close()
+    await writer.wait_closed()
+
+    print("done")
+    ev.set()
+
+
+async def tcp_server():
+    global ev
+    ev = asyncio.Event()
+    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT)
+    print("server running")
+    multitest.next()
+    async with server:
+        await asyncio.wait_for(ev.wait(), 10)
+
+
+async def tcp_client():
+    reader, writer = await asyncio.open_connection(IP, PORT)
+
+    ba = bytearray(2)
+    n = await reader.readinto(ba)
+    print(n)
+    print(ba[:n])
+
+    a = array.array("b", [0, 0])
+    n = await reader.readinto(a)
+    print(n)
+    print(a[:n])
+
+    try:
+        n = await reader.readinto(5)
+    except TypeError as er:
+        print("TypeError")
+
+    try:
+        n = await reader.readinto()
+    except TypeError as er:
+        print("TypeError")
+
+
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    asyncio.run(tcp_server())
+
+
+def instance1():
+    multitest.next()
+    asyncio.run(tcp_client())
diff --git a/tests/multi_net/uasyncio_tcp_readinto.py.exp b/tests/multi_net/asyncio_tcp_readinto.py.exp
similarity index 100%
rename from tests/multi_net/uasyncio_tcp_readinto.py.exp
rename to tests/multi_net/asyncio_tcp_readinto.py.exp
diff --git a/tests/multi_net/asyncio_tcp_server_client.py b/tests/multi_net/asyncio_tcp_server_client.py
new file mode 100644
index 00000000000..6c4aaf2b7ef
--- /dev/null
+++ b/tests/multi_net/asyncio_tcp_server_client.py
@@ -0,0 +1,55 @@
+# Test asyncio TCP server and client using start_server() and open_connection()
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+
+async def handle_connection(reader, writer):
+    # Test that peername exists (but don't check its value, it changes)
+    writer.get_extra_info("peername")
+
+    data = await reader.read(100)
+    print("echo:", data)
+    writer.write(data)
+    await writer.drain()
+
+    print("close")
+    writer.close()
+    await writer.wait_closed()
+
+    print("done")
+    ev.set()
+
+
+async def tcp_server():
+    global ev
+    ev = asyncio.Event()
+    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT)
+    print("server running")
+    multitest.next()
+    async with server:
+        await asyncio.wait_for(ev.wait(), 10)
+
+
+async def tcp_client(message):
+    reader, writer = await asyncio.open_connection(IP, PORT)
+    print("write:", message)
+    writer.write(message)
+    await writer.drain()
+    data = await reader.read(100)
+    print("read:", data)
+
+
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    asyncio.run(tcp_server())
+
+
+def instance1():
+    multitest.next()
+    asyncio.run(tcp_client(b"client data"))
diff --git a/tests/multi_net/uasyncio_tcp_server_client.py.exp b/tests/multi_net/asyncio_tcp_server_client.py.exp
similarity index 100%
rename from tests/multi_net/uasyncio_tcp_server_client.py.exp
rename to tests/multi_net/asyncio_tcp_server_client.py.exp
diff --git a/tests/multi_net/asyncio_tls_server_client.py b/tests/multi_net/asyncio_tls_server_client.py
new file mode 100644
index 00000000000..959dcfe5994
--- /dev/null
+++ b/tests/multi_net/asyncio_tls_server_client.py
@@ -0,0 +1,70 @@
+# Test asyncio TCP server and client with TLS, transferring some data.
+
+try:
+    import os
+    import asyncio
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+cert = cafile = "ec_cert.der"
+key = "ec_key.der"
+
+
+async def handle_connection(reader, writer):
+    data = await reader.read(100)
+    print("echo:", data)
+    writer.write(data)
+    await writer.drain()
+
+    print("close")
+    writer.close()
+    await writer.wait_closed()
+
+    print("done")
+    ev.set()
+
+
+async def tcp_server():
+    global ev
+
+    server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cert, key)
+    ev = asyncio.Event()
+    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT, ssl=server_ctx)
+    print("server running")
+    multitest.next()
+    async with server:
+        await asyncio.wait_for(ev.wait(), 10)
+
+
+async def tcp_client(message):
+    client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = ssl.CERT_REQUIRED
+    client_ctx.load_verify_locations(cafile=cafile)
+    reader, writer = await asyncio.open_connection(
+        IP, PORT, ssl=client_ctx, server_hostname="micropython.local"
+    )
+    print("write:", message)
+    writer.write(message)
+    await writer.drain()
+    data = await reader.read(100)
+    print("read:", data)
+
+
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    asyncio.run(tcp_server())
+
+
+def instance1():
+    multitest.next()
+    asyncio.run(tcp_client(b"client data"))
diff --git a/tests/multi_net/asyncio_tls_server_client.py.exp b/tests/multi_net/asyncio_tls_server_client.py.exp
new file mode 100644
index 00000000000..6dc6a9bbc7e
--- /dev/null
+++ b/tests/multi_net/asyncio_tls_server_client.py.exp
@@ -0,0 +1,8 @@
+--- instance0 ---
+server running
+echo: b'client data'
+close
+done
+--- instance1 ---
+write: b'client data'
+read: b'client data'
diff --git a/tests/multi_net/asyncio_tls_server_client_cert_required_error.py b/tests/multi_net/asyncio_tls_server_client_cert_required_error.py
new file mode 100644
index 00000000000..5d66d9c1bd9
--- /dev/null
+++ b/tests/multi_net/asyncio_tls_server_client_cert_required_error.py
@@ -0,0 +1,73 @@
+# Test asyncio TCP server and client with TLS, giving a cert required error.
+
+try:
+    import os
+    import asyncio
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+cert = cafile = "ec_cert.der"
+key = "ec_key.der"
+
+
+async def handle_connection(reader, writer):
+    print("handle connection")
+    try:
+        data = await reader.read(100)
+    except Exception as e:
+        print(e)
+    ev.set()
+
+
+async def tcp_server():
+    global ev
+
+    server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cert, key)
+    server_ctx.verify_mode = ssl.CERT_REQUIRED
+    server_ctx.load_verify_locations(cafile=cert)
+    ev = asyncio.Event()
+    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT, ssl=server_ctx)
+    print("server running")
+    multitest.next()
+    async with server:
+        await asyncio.wait_for(ev.wait(), 10)
+        multitest.wait("finished")
+        print("server done")
+
+
+async def tcp_client(message):
+    client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = ssl.CERT_REQUIRED
+    client_ctx.load_verify_locations(cafile=cafile)
+    reader, writer = await asyncio.open_connection(
+        IP, PORT, ssl=client_ctx, server_hostname="micropython.local"
+    )
+    try:
+        print("write:", message)
+        writer.write(message)
+        print("drain")
+        await writer.drain()
+    except Exception as e:
+        print(e)
+    print("client done")
+    multitest.broadcast("finished")
+
+
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    asyncio.run(tcp_server())
+
+
+def instance1():
+    multitest.next()
+    asyncio.run(tcp_client(b"client data"))
diff --git a/tests/multi_net/asyncio_tls_server_client_cert_required_error.py.exp b/tests/multi_net/asyncio_tls_server_client_cert_required_error.py.exp
new file mode 100644
index 00000000000..0f905d0d23b
--- /dev/null
+++ b/tests/multi_net/asyncio_tls_server_client_cert_required_error.py.exp
@@ -0,0 +1,10 @@
+--- instance0 ---
+server running
+handle connection
+(-29824, 'MBEDTLS_ERR_SSL_NO_CLIENT_CERTIFICATE')
+server done
+--- instance1 ---
+write: b'client data'
+drain
+(-30592, 'MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE')
+client done
diff --git a/tests/multi_net/asyncio_tls_server_client_readline.py b/tests/multi_net/asyncio_tls_server_client_readline.py
new file mode 100644
index 00000000000..539eae23d5c
--- /dev/null
+++ b/tests/multi_net/asyncio_tls_server_client_readline.py
@@ -0,0 +1,74 @@
+# Test asyncio TCP server and client with TLS, using readline() to read data.
+
+try:
+    import os
+    import asyncio
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+cert = cafile = "ec_cert.der"
+key = "ec_key.der"
+
+
+async def handle_connection(reader, writer):
+    data = await reader.readline()
+    print("echo:", data)
+    data2 = await reader.readline()
+    print("echo:", data2)
+    writer.write(data + data2)
+    await writer.drain()
+
+    print("close")
+    writer.close()
+    await writer.wait_closed()
+
+    print("done")
+    ev.set()
+
+
+async def tcp_server():
+    global ev
+
+    server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cert, key)
+    ev = asyncio.Event()
+    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT, ssl=server_ctx)
+    print("server running")
+    multitest.next()
+    async with server:
+        await asyncio.wait_for(ev.wait(), 10)
+
+
+async def tcp_client(message):
+    client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = ssl.CERT_REQUIRED
+    client_ctx.load_verify_locations(cafile=cafile)
+    reader, writer = await asyncio.open_connection(
+        IP, PORT, ssl=client_ctx, server_hostname="micropython.local"
+    )
+    print("write:", message)
+    writer.write(message)
+    await writer.drain()
+    data = await reader.readline()
+    print("read:", data)
+    data2 = await reader.readline()
+    print("read:", data2)
+
+
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    asyncio.run(tcp_server())
+
+
+def instance1():
+    multitest.next()
+    asyncio.run(tcp_client(b"client data\nclient data2\n"))
diff --git a/tests/multi_net/asyncio_tls_server_client_readline.py.exp b/tests/multi_net/asyncio_tls_server_client_readline.py.exp
new file mode 100644
index 00000000000..4c93c5729aa
--- /dev/null
+++ b/tests/multi_net/asyncio_tls_server_client_readline.py.exp
@@ -0,0 +1,10 @@
+--- instance0 ---
+server running
+echo: b'client data\n'
+echo: b'client data2\n'
+close
+done
+--- instance1 ---
+write: b'client data\nclient data2\n'
+read: b'client data\n'
+read: b'client data2\n'
diff --git a/tests/multi_net/asyncio_tls_server_client_verify_error.py b/tests/multi_net/asyncio_tls_server_client_verify_error.py
new file mode 100644
index 00000000000..50169dd679a
--- /dev/null
+++ b/tests/multi_net/asyncio_tls_server_client_verify_error.py
@@ -0,0 +1,74 @@
+# Test asyncio TCP server and client with TLS, and an incorrect server_hostname.
+
+try:
+    import os
+    import asyncio
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+cert = cafile = "ec_cert.der"
+key = "ec_key.der"
+
+
+async def handle_connection(reader, writer):
+    print("handle connection")
+    try:
+        data = await reader.read(100)
+    except Exception as e:
+        print(e)
+    ev.set()
+
+
+async def tcp_server():
+    global ev
+
+    server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cert, key)
+    ev = asyncio.Event()
+    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT, ssl=server_ctx)
+    print("server running")
+    multitest.next()
+    async with server:
+        await asyncio.wait_for(ev.wait(), 10)
+    print("server done")
+    multitest.broadcast("finished")
+
+
+async def tcp_client(message):
+    client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = ssl.CERT_REQUIRED
+    client_ctx.load_verify_locations(cafile=cafile)
+    reader, writer = await asyncio.open_connection(
+        IP,
+        PORT,
+        ssl=client_ctx,
+        server_hostname="foobar.local",  # incorrect server_hostname
+    )
+    try:
+        print("write:", message)
+        writer.write(message)
+        print("drain")
+        await writer.drain()
+    except Exception as e:
+        print(e)
+    multitest.wait("finished")
+    print("client done")
+
+
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    asyncio.run(tcp_server())
+
+
+def instance1():
+    multitest.next()
+    asyncio.run(tcp_client(b"client data"))
diff --git a/tests/multi_net/asyncio_tls_server_client_verify_error.py.exp b/tests/multi_net/asyncio_tls_server_client_verify_error.py.exp
new file mode 100644
index 00000000000..36d0ab00f48
--- /dev/null
+++ b/tests/multi_net/asyncio_tls_server_client_verify_error.py.exp
@@ -0,0 +1,12 @@
+--- instance0 ---
+server running
+handle connection
+(-30592, 'MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE')
+server done
+--- instance1 ---
+write: b'client data'
+drain
+
+The certificate Common Name (CN) does not match with the expected CN
+
+client done
diff --git a/tests/multi_net/ec_cert.der b/tests/multi_net/ec_cert.der
new file mode 100644
index 00000000000..a13f1c984a6
Binary files /dev/null and b/tests/multi_net/ec_cert.der differ
diff --git a/tests/multi_net/ec_key.der b/tests/multi_net/ec_key.der
new file mode 100644
index 00000000000..f136665755c
Binary files /dev/null and b/tests/multi_net/ec_key.der differ
diff --git a/tests/multi_net/expired_cert.der b/tests/multi_net/expired_cert.der
new file mode 100644
index 00000000000..3801fca1776
Binary files /dev/null and b/tests/multi_net/expired_cert.der differ
diff --git a/tests/multi_net/rsa_cert.der b/tests/multi_net/rsa_cert.der
new file mode 100644
index 00000000000..fc15ef9dae9
Binary files /dev/null and b/tests/multi_net/rsa_cert.der differ
diff --git a/tests/multi_net/rsa_key.der b/tests/multi_net/rsa_key.der
new file mode 100644
index 00000000000..658af9bd899
Binary files /dev/null and b/tests/multi_net/rsa_key.der differ
diff --git a/tests/multi_net/ssl_cert_ec.py b/tests/multi_net/ssl_cert_ec.py
new file mode 100644
index 00000000000..cb4e36f78cc
--- /dev/null
+++ b/tests/multi_net/ssl_cert_ec.py
@@ -0,0 +1,53 @@
+# Simple test creating an SSL connection and transferring some data
+# This test won't run under CPython because CPython doesn't have key/cert
+
+try:
+    import binascii, os, socket, ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+certfile = "ec_cert.der"
+keyfile = "ec_key.der"
+
+with open(certfile, "rb") as cf:
+    cert = cadata = cf.read()
+
+with open(keyfile, "rb") as kf:
+    key = kf.read()
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    s2 = ssl.wrap_socket(s2, server_side=True, key=key, cert=cert)
+    print(s2.read(16))
+    s2.write(b"server to client")
+    s2.close()
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    s = ssl.wrap_socket(
+        s, cert_reqs=ssl.CERT_REQUIRED, server_hostname="micropython.local", cadata=cadata
+    )
+    s.write(b"client to server")
+    print(s.read(16))
+    s.close()
diff --git a/tests/multi_net/ssl_data.py.exp b/tests/multi_net/ssl_cert_ec.py.exp
similarity index 100%
rename from tests/multi_net/ssl_data.py.exp
rename to tests/multi_net/ssl_cert_ec.py.exp
diff --git a/tests/multi_net/ssl_cert_rsa.py b/tests/multi_net/ssl_cert_rsa.py
index 7c718ee40a5..473a5b16f8d 100644
--- a/tests/multi_net/ssl_cert_rsa.py
+++ b/tests/multi_net/ssl_cert_rsa.py
@@ -2,128 +2,26 @@
 # This test won't run under CPython because CPython doesn't have key/cert
 
 try:
-    import ubinascii as binascii, usocket as socket, ussl as ssl
+    import binascii, os, socket, ssl
 except ImportError:
     print("SKIP")
     raise SystemExit
 
-PORT = 8000
-
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
 
-# This self-signed key/cert pair is randomly generated and to be used for
-# testing/demonstration only.  You should always generate your own key/cert.
+PORT = 8000
 
-# To generate a new self-signed key/cert pair with openssl do:
-# $ openssl req -x509 -newkey rsa:4096 -keyout rsa_key.pem -out rsa_cert.pem -days 365 -node
-#
-# Convert them to DER format:
-# $ openssl rsa -in rsa_key.pem -out rsa_key.der -outform DER
-# $ openssl x509 -in rsa_cert.pem -out rsa_key.der -outform DER
-#
-# Then convert to hex format, eg using binascii.hexlify(data).
+# These are test certificates. See tests/README.md for details.
+certfile = "rsa_cert.der"
+keyfile = "rsa_key.der"
 
-cert = binascii.unhexlify(
-    b"308205d7308203bfa003020102020900bc63b48a700c3d49300d06092a864886f70d01010b050030"
-    b"8181310b3009060355040613024155310c300a06035504080c03466f6f310c300a06035504070c03"
-    b"42617231143012060355040a0c0b4d6963726f507974686f6e310c300a060355040b0c03666f6f31"
-    b"16301406035504030c0d657370686f6d652e6c6f63616c311a301806092a864886f70d010901160b"
-    b"666f6f406261722e636f6d301e170d3232303731323138303031335a170d32333037313231383030"
-    b"31335a308181310b3009060355040613024155310c300a06035504080c03466f6f310c300a060355"
-    b"04070c0342617231143012060355040a0c0b4d6963726f507974686f6e310c300a060355040b0c03"
-    b"666f6f3116301406035504030c0d657370686f6d652e6c6f63616c311a301806092a864886f70d01"
-    b"0901160b666f6f406261722e636f6d30820222300d06092a864886f70d01010105000382020f0030"
-    b"82020a0282020100ce3c0f730ab34432ce605ab44d4ac0aafd8a6243133eab0dcc9d444ab7d9ff66"
-    b"a6815a101d2d3cbd72140afc34f8c3caedce16e9528350f3e0e56343f248507d82e41b51abb515cb"
-    b"f60e5a619f2dbca8684d174c3b0951e2c7ba576c7fb06453a3597755810a6a4c45eb0925c855ab53"
-    b"7785df46bf29145871330ff0641a101a24f0830c20bae865ba8bb32606caac4555812acf19f59553"
-    b"349ce70fb7ff63512f0444f8f41b973183eabf9679903087c6cd69dc3adcbe754dd0207ea57c50e9"
-    b"2d800bce6258d1618bb749d3fc01239b6d1af6d3f9cada3acbb312a1d85a59cfabd28b2e572c56a4"
-    b"818ce170ca2b781a04749c6239206c64ad9e057484143a4c52bdef6189c46405c1a9642489cb640a"
-    b"937adfc2687578dfa2b40ebafa05213642a1ccbc265557cd40de53324cff1bfba6f5c215f657b8f9"
-    b"f2260ab6293625d0e203bba975bc7ac6dff3e604c9b0d2a2a4ba5941c0dc8d2e0e9439c56447b404"
-    b"8c0e6cfb03517742ff6f7c2140a05954aa1e29247d1ae8bfd7db0db8dd45d095710fb78284ede285"
-    b"0fc0c21235406af83e6044addf9385316403e2a25442b9ffbfc7b01c6c9292e5a3531e6a48496c01"
-    b"6de1373334a52f01b7c6a0ece1261936788d2161c53a8985a0946d6d319225b230d96d055ea4692f"
-    b"eb71fdaf4b775ac9fbc38e1b943e6617cf61d33e930ab288a3ea4730b4f2784a8018e0dfc8a11e73"
-    b"0203010001a350304e301d0603551d0e04160414bc6048fe3cd278257e8b7c90dedbbce8369b20b8"
-    b"301f0603551d23041830168014bc6048fe3cd278257e8b7c90dedbbce8369b20b8300c0603551d13"
-    b"040530030101ff300d06092a864886f70d01010b0500038202010009238354b43379a3d2b56e928c"
-    b"ac8ea28e2c01cf8148e54c0bbd4055e2e57d578697d1e2c392f1fe3bc9211d4f27ed1be631e7547a"
-    b"6390d7f121a9e20a195fdda73f755188b16cf39714924a9686dd7cc749421335038c0640c2c6b15d"
-    b"f44d74d94a97285ee2a7b075ccc9d9d632e2a5906030cf59bde14ab10660b7cf47ec9d7ae2f35963"
-    b"454f76735a3dac12a4a4c907183e9ccf3e07d59484c182e67edc7c35ce15c7e1072fae8c9965a126"
-    b"1a1f31147d4af8d1ebf8ee7c142badfe67e31fb324a79a29bc94e89370b70d8cf7cd2b2aa427a49f"
-    b"77849891e7c4d5911f6fda52733a3c169b0188c2d9918f296dd8e234f8962f0db5e47c6159448045"
-    b"4e2d9a5850d4c696a0fb3b66534a4591c49dda8cc6f1b0008c625aa5e0091ecfbd51d9715c60b85e"
-    b"4e89d4a6cfabb2acdf81518eb61403b8f8767c5c00216f730e08f22959dff695a081cc726c4ab35a"
-    b"e3f6538a231f831a6e91206f3b691a94bdf95343ec02ef7aac42da2a70846cd5f13dd2955a5f1737"
-    b"a4c3c6c03b041d334c1dadd1e305f07c83b4b4e0509ec1d23e95f820290942eaaf8bea304cd5a505"
-    b"8fc0d4624ff1ffe1348e7bc54c756a12acb258eb5e7426fb062a82b88ec274c9c13b3eff8b010947"
-    b"62e166f490cd25b14e762db708785859a337d8fd0008fe602a90e2933cded3359e98ce3fbc041208"
-    b"66bd4d96d6b6f7f53def854d40021196b7a06b"
-)
+with open(certfile, "rb") as cf:
+    cert = cadata = cf.read()
 
-key = binascii.unhexlify(
-    b"308209290201000282020100ce3c0f730ab34432ce605ab44d4ac0aafd8a6243133eab0dcc9d444a"
-    b"b7d9ff66a6815a101d2d3cbd72140afc34f8c3caedce16e9528350f3e0e56343f248507d82e41b51"
-    b"abb515cbf60e5a619f2dbca8684d174c3b0951e2c7ba576c7fb06453a3597755810a6a4c45eb0925"
-    b"c855ab537785df46bf29145871330ff0641a101a24f0830c20bae865ba8bb32606caac4555812acf"
-    b"19f59553349ce70fb7ff63512f0444f8f41b973183eabf9679903087c6cd69dc3adcbe754dd0207e"
-    b"a57c50e92d800bce6258d1618bb749d3fc01239b6d1af6d3f9cada3acbb312a1d85a59cfabd28b2e"
-    b"572c56a4818ce170ca2b781a04749c6239206c64ad9e057484143a4c52bdef6189c46405c1a96424"
-    b"89cb640a937adfc2687578dfa2b40ebafa05213642a1ccbc265557cd40de53324cff1bfba6f5c215"
-    b"f657b8f9f2260ab6293625d0e203bba975bc7ac6dff3e604c9b0d2a2a4ba5941c0dc8d2e0e9439c5"
-    b"6447b4048c0e6cfb03517742ff6f7c2140a05954aa1e29247d1ae8bfd7db0db8dd45d095710fb782"
-    b"84ede2850fc0c21235406af83e6044addf9385316403e2a25442b9ffbfc7b01c6c9292e5a3531e6a"
-    b"48496c016de1373334a52f01b7c6a0ece1261936788d2161c53a8985a0946d6d319225b230d96d05"
-    b"5ea4692feb71fdaf4b775ac9fbc38e1b943e6617cf61d33e930ab288a3ea4730b4f2784a8018e0df"
-    b"c8a11e73020301000102820201008efa0e8fe81c2e2cb6ed10152dfca4242750581d3e6b54f56524"
-    b"a6a2d2613cf2727efcec6cfddebd4c285f1148bc2a2936c28919cb0da502dea8c92fe2f9856bee61"
-    b"ac1aebdac838b5e66f7c7c799df07716f30ef362dbb5485884a180c8ce5539cb1db35699dce5f217"
-    b"27295d811f1ce7a115111c1823b5c90ce880f5352872a7a76282f6f1fd8a015136ab274c3d30783d"
-    b"eb6ad7096e33d826eafdf7c70398d5eab4d28f91cd3913c69c7a7ade9ef692b9f8292959be64dec4"
-    b"6ab2c291b41a6464004b5ddd4b93bfe41b37eedeef4ba2d16dcbb9c28b96f57fb96c20ed4a9471ff"
-    b"ae643b254f100f8c9702b5f67af6369e8d887f285e5d520c5aa5d3a79e5de96432e6d2e3dea68e58"
-    b"208c075fb119c6d3d4149b7e1247208d6b337c70272befc41d57f278618f1a82de337173346dc135"
-    b"4d80a7c9075af99dbb2a14733c06b71600c6677a6bb28c0e4fc63db622228047a2cb7474dc8141c3"
-    b"5f3a597c3e2bca9911d28eb9fd1a0c915e9f9c1cfd643d4fd8cac867f215380168ec37b8cfa28564"
-    b"e6288ab04a7d67ca44b4c8375214a7ffaa1e6be92c4b138fcfd6beaba251b31a50a6e2ef241c9554"
-    b"a1dc710b4acb63e749f5849e53d3f4915c6eb2a9a009bab04e932841ab34ae29eb000a08777d6399"
-    b"169c2dc3d7952df5bc2d06e90a32139c6a2793d3817e4feadac2ccac554d383a8d41569140c29168"
-    b"89220d3a5e410282010100fd603d18feef7aac61bda3b674a57ab38748bcde5c3efdba2279638f8e"
-    b"a413cc26b9dda0375c116a8798a295b2c283aaaad7cca0dbd9bb3322a9a815f6d0aa5fc4f9aff8fb"
-    b"da8ff914091ede7aefdb07a119c9b2e2b2bda776ac497060b8e88a82eb20c62f26f343566697726e"
-    b"71aa46fd4efad6f42fc8a478856324d72cbf5eb3918317162d6fc2cfd775969a2077759fa2c8220d"
-    b"acdc2ebb03ec39feed3f2b415449cbf40a7126bcf01d1068e3a45ec01181f2c68d7e05b4720bfe4a"
-    b"308e1648123c91214a5f8dfce58727c4cd9396a8b403b733a717449b2f1970db97a3b8467271ffa6"
-    b"e8c7cc9e2e1c0f789284ae9efe77eaac01131463c9c1329a1ba3530282010100d05ed6ab9b9fbdf7"
-    b"a0f5f91f68dc3bac5789332d6ece46103fb1ef109fc972fdc99edf3107a23d66d1cdfe6bdddfd1bb"
-    b"3952ccd10b5c20ad1b3e0aa6a51271ecf3a7ef2a65e029f5d77f238d1235b52a9dca3451c165d70a"
-    b"99cbaea5c610e5455979696db769191e7cf2db21f641959e4ba1c5c0aae260c724962b6ac2621d92"
-    b"e9df7adeb82b522d37b42cb454003bbe60d9915bf7737aeccf88c7ed1263a22f431a734e61fe7173"
-    b"a937ddf76ad2a79994c05238defc15f6846858e9edf27ae2a567c7c5c735ea5d2fbef65a2195bc05"
-    b"d82cbf06a477b29c84c92e8054c2bb25d8c6f19d43ef5fd1fce13c2cdbc361c39baec37b399200b3"
-    b"2d4a6798ba0b546102820101008e41492c4e7daff7368d1d6c64034067a94dca5461a0301e201add"
-    b"2e0d5ccb8cb435685bfa98e362572cf8236a10d191b187a568aee688b6c60050d1bc181d7fd57c86"
-    b"33195bf5b7576b637c6fb358dae8b52ccc15815affb99e334137dcb91a833475db2f4004164b5d20"
-    b"2c6c1bbf094a50dc7e70ec9f0ed067bb6944b1e7e3c897aaecfc53984add1c4ff5b525034cf3ca95"
-    b"e8a09aeba804f1c7e02be391b2bc641166c3e654eef5e72dba37d98f406f3fa520e41f2ea10f5574"
-    b"ac5984f75145378fefbfac1d07fff3f234fec698d55e746b1da18f6f7de24ec84ed7cb446d428820"
-    b"bef33c00693e6a0ef114b5d66e9fefa8ee059238df1ac37c87e7841ae7028201000721a7d139c34e"
-    b"da21cd295894db2cc3aa3f4cdc1a35bf1a2143f2bdabea56202f7d5b802f15b36a4875f76633b2cc"
-    b"57cf0f71691a2d6e04deb0d1e68031d06a5eb079b406c6944910b60e3e6ec81dca369a4c0e1c4363"
-    b"07bed9c4c171b4f453da4b187ba3d25a04bc1c07b9f2d6adcb3c256e4238d7049eec36a387c4dd5c"
-    b"cbc16b5fa62dc175cf8c5f83442cb7d153a3b6ee8daa3b6e929a4bc123f1042df1d6271a992d2b6b"
-    b"309d33074ac7822c304a72069e61ab590915e10862013dd24cdd825ec8fb17724cfc2c59fc1db825"
-    b"3641fece0ee9241b9dd5c198f0d575d0b7ebe26b3489b5b09edc3bcd366fd3110e83ce886c383d31"
-    b"feefe6e302cc2345210282010008f77a33d0081e9be3c1b1ac8b8e0eebb72df2eb69b95d2ed74935"
-    b"b9dab8e17023cc38465354023c5183b51a6a20288fbb2181172be1c2fdb8b444419454e5b37f7f3b"
-    b"df11e28cf4746b25534eb62f7e87bbbf28eda37024368b3897fbc661b40a93e04a183db9219c04a8"
-    b"7643edf5d8b5dbfe3d424e91d558d5e3e2fa02ce1984ee69fb8518470eee2e7db0e1df5ac4571f78"
-    b"a7a2529bc1fef5e32d46994869a8d8cc47869e174d84e7976be8ebb88f2ccb71a603a8bdb06af3eb"
-    b"2ddbd62082f40d7987e47f2e321eb5eb2a28fefab263409f89dc97ebc723a1b751418cdd3ea684ba"
-    b"8b17a330a306a6fbcf51ba83563aed85a4f886fff1a22423748d83798c"
-)
+with open(keyfile, "rb") as kf:
+    key = kf.read()
 
 
 # Server
@@ -147,7 +45,9 @@ def instance1():
     multitest.next()
     s = socket.socket()
     s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
-    s = ssl.wrap_socket(s, cert_reqs=ssl.CERT_REQUIRED, cadata=cert)
+    s = ssl.wrap_socket(
+        s, cert_reqs=ssl.CERT_REQUIRED, server_hostname="micropython.local", cadata=cadata
+    )
     s.write(b"client to server")
     print(s.read(16))
     s.close()
diff --git a/tests/multi_net/ssl_data.py b/tests/multi_net/ssl_data.py
deleted file mode 100644
index aef85b83ab3..00000000000
--- a/tests/multi_net/ssl_data.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# Simple test creating an SSL connection and transferring some data
-# This test won't run under CPython because it requires key/cert
-
-try:
-    import ubinascii as binascii, usocket as socket, ussl as ssl
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-PORT = 8000
-
-# This self-signed key/cert pair is randomly generated and to be used for
-# testing/demonstration only.  You should always generate your own key/cert.
-key = binascii.unhexlify(
-    b"3082013b020100024100cc20643fd3d9c21a0acba4f48f61aadd675f52175a9dcf07fbef"
-    b"610a6a6ba14abb891745cd18a1d4c056580d8ff1a639460f867013c8391cdc9f2e573b0f"
-    b"872d0203010001024100bb17a54aeb3dd7ae4edec05e775ca9632cf02d29c2a089b563b0"
-    b"d05cdf95aeca507de674553f28b4eadaca82d5549a86058f9996b07768686a5b02cb240d"
-    b"d9f1022100f4a63f5549e817547dca97b5c658038e8593cb78c5aba3c4642cc4cd031d86"
-    b"8f022100d598d870ffe4a34df8de57047a50b97b71f4d23e323f527837c9edae88c79483"
-    b"02210098560c89a70385c36eb07fd7083235c4c1184e525d838aedf7128958bedfdbb102"
-    b"2051c0dab7057a8176ca966f3feb81123d4974a733df0f958525f547dfd1c271f9022044"
-    b"6c2cafad455a671a8cf398e642e1be3b18a3d3aec2e67a9478f83c964c4f1f"
-)
-cert = binascii.unhexlify(
-    b"308201d53082017f020203e8300d06092a864886f70d01010505003075310b3009060355"
-    b"0406130258583114301206035504080c0b54686550726f76696e63653110300e06035504"
-    b"070c075468654369747931133011060355040a0c0a436f6d70616e7958595a3113301106"
-    b"0355040b0c0a436f6d70616e7958595a3114301206035504030c0b546865486f73744e61"
-    b"6d65301e170d3139313231383033333935355a170d3239313231353033333935355a3075"
-    b"310b30090603550406130258583114301206035504080c0b54686550726f76696e636531"
-    b"10300e06035504070c075468654369747931133011060355040a0c0a436f6d70616e7958"
-    b"595a31133011060355040b0c0a436f6d70616e7958595a3114301206035504030c0b5468"
-    b"65486f73744e616d65305c300d06092a864886f70d0101010500034b003048024100cc20"
-    b"643fd3d9c21a0acba4f48f61aadd675f52175a9dcf07fbef610a6a6ba14abb891745cd18"
-    b"a1d4c056580d8ff1a639460f867013c8391cdc9f2e573b0f872d0203010001300d06092a"
-    b"864886f70d0101050500034100b0513fe2829e9ecbe55b6dd14c0ede7502bde5d46153c8"
-    b"e960ae3ebc247371b525caeb41bbcf34686015a44c50d226e66aef0a97a63874ca5944ef"
-    b"979b57f0b3"
-)
-
-
-# Server
-def instance0():
-    multitest.globals(IP=multitest.get_network_ip())
-    s = socket.socket()
-    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
-    s.listen(1)
-    multitest.next()
-    s2, _ = s.accept()
-    s2 = ssl.wrap_socket(s2, server_side=True, key=key, cert=cert)
-    print(s2.read(16))
-    s2.write(b"server to client")
-    s2.close()
-    s.close()
-
-
-# Client
-def instance1():
-    multitest.next()
-    s = socket.socket()
-    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
-    s = ssl.wrap_socket(s)
-    s.write(b"client to server")
-    print(s.read(16))
-    s.close()
diff --git a/tests/multi_net/sslcontext_check_hostname_error.py b/tests/multi_net/sslcontext_check_hostname_error.py
new file mode 100644
index 00000000000..65dff998e1f
--- /dev/null
+++ b/tests/multi_net/sslcontext_check_hostname_error.py
@@ -0,0 +1,56 @@
+# Test creating an SSL connection when server_hostname is required but not specified.
+
+try:
+    import os
+    import socket
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+cert = cafile = "ec_cert.der"
+key = "ec_key.der"
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cert, key)
+    multitest.broadcast("ready")
+    try:
+        s2 = server_ctx.wrap_socket(s2, server_side=True)
+    except Exception as e:
+        print(e)
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = ssl.CERT_REQUIRED
+    client_ctx.load_verify_locations(cafile=cafile)
+    # The client will reject the SSL connection so wait until the server is ready
+    # so that it can see the rejection (otherwise it sees a closed socket).
+    multitest.wait("ready")
+    try:
+        s = client_ctx.wrap_socket(s)
+    except Exception as e:
+        print(e)
+    s.close()
diff --git a/tests/multi_net/sslcontext_check_hostname_error.py.exp b/tests/multi_net/sslcontext_check_hostname_error.py.exp
new file mode 100644
index 00000000000..d29279f8600
--- /dev/null
+++ b/tests/multi_net/sslcontext_check_hostname_error.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+(-29312, 'MBEDTLS_ERR_SSL_CONN_EOF')
+--- instance1 ---
+CERT_REQUIRED requires server_hostname
diff --git a/tests/multi_net/sslcontext_getpeercert.py b/tests/multi_net/sslcontext_getpeercert.py
new file mode 100644
index 00000000000..89a12753bb4
--- /dev/null
+++ b/tests/multi_net/sslcontext_getpeercert.py
@@ -0,0 +1,59 @@
+# Test creating an SSL connection and getting the peer certificate.
+
+try:
+    import io
+    import os
+    import socket
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+cert = cafile = "ec_cert.der"
+key = "ec_key.der"
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cert, key)
+    s2 = server_ctx.wrap_socket(s2, server_side=True)
+    print(s2.read(16))
+    s2.write(b"server to client")
+    s2.close()
+    s.close()
+
+
+# Client
+def instance1():
+    s_test = ssl.wrap_socket(io.BytesIO(), server_side=True, do_handshake=False)
+    s_test.close()
+    if not hasattr(s_test, "getpeercert"):
+        print("SKIP")
+        raise SystemExit
+
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = ssl.CERT_REQUIRED
+    client_ctx.load_verify_locations(cafile=cafile)
+    s = client_ctx.wrap_socket(s, server_hostname="micropython.local")
+    print(s.getpeercert(True).hex())
+    s.write(b"client to server")
+    print(s.read(16))
+    s.close()
diff --git a/tests/multi_net/sslcontext_getpeercert.py.exp b/tests/multi_net/sslcontext_getpeercert.py.exp
new file mode 100644
index 00000000000..a0dfaa4f567
--- /dev/null
+++ b/tests/multi_net/sslcontext_getpeercert.py.exp
@@ -0,0 +1,5 @@
+--- instance0 ---
+b'client to server'
+--- instance1 ---
+308201d230820179a003020102021403abf0266b125f8ec8dab57c34d655e94ae4d9bb300a06082a8648ce3d040302303f311a301806035504030c116d6963726f707974686f6e2e6c6f63616c31143012060355040a0c0b4d6963726f507974686f6e310b3009060355040613024155301e170d3235303131343033333531375a170d3335303131323033333531375a303f311a301806035504030c116d6963726f707974686f6e2e6c6f63616c31143012060355040a0c0b4d6963726f507974686f6e310b30090603550406130241553059301306072a8648ce3d020106082a8648ce3d030107034200042882eb6055f540ddef989ec7ead84281461436745d1b9174a2730f2bc74cf91e1ce62d4ada67129e891ab70e73a34b644b000d8d1e4c43189a3390210f447169a3533051301d0603551d0e0416041410ced497be3800e7b19008504efd0adc39bfe02a301f0603551d2304183016801410ced497be3800e7b19008504efd0adc39bfe02a300f0603551d130101ff040530030101ff300a06082a8648ce3d040302034700304402204b153d2851e7846351383beac1d5b39791810156b0f9672040d3499e14041969022013e32fe207d3bccb7f9859b3140e73074f656c034679135e517b1e9cb05ae5b2
+b'server to client'
diff --git a/tests/multi_net/sslcontext_server_client.py b/tests/multi_net/sslcontext_server_client.py
new file mode 100644
index 00000000000..1b6a0996688
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client.py
@@ -0,0 +1,56 @@
+# Test creating an SSL connection with certificates as bytes objects.
+
+try:
+    import os
+    import socket
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+certfile = "ec_cert.der"
+keyfile = "ec_key.der"
+
+with open(certfile, "rb") as cf:
+    cert = cadata = cf.read()
+with open(keyfile, "rb") as kf:
+    key = kf.read()
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cert, key)
+    s2 = server_ctx.wrap_socket(s2, server_side=True)
+    print(s2.read(16))
+    s2.write(b"server to client")
+    s2.close()
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = ssl.CERT_REQUIRED
+    client_ctx.load_verify_locations(cadata=cadata)
+    s = client_ctx.wrap_socket(s, server_hostname="micropython.local")
+    s.write(b"client to server")
+    print(s.read(16))
+    s.close()
diff --git a/tests/multi_net/sslcontext_server_client.py.exp b/tests/multi_net/sslcontext_server_client.py.exp
new file mode 100644
index 00000000000..909c496d019
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+b'client to server'
+--- instance1 ---
+b'server to client'
diff --git a/tests/multi_net/sslcontext_server_client_ciphers.py b/tests/multi_net/sslcontext_server_client_ciphers.py
new file mode 100644
index 00000000000..cd3fe919a34
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_ciphers.py
@@ -0,0 +1,60 @@
+# Test creating an SSL connection with specified ciphers.
+
+try:
+    import os
+    import socket
+    import tls
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(tls, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+cert = cafile = "ec_cert.der"
+key = "ec_key.der"
+
+with open(cafile, "rb") as f:
+    cadata = f.read()
+with open(key, "rb") as f:
+    keydata = f.read()
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = tls.SSLContext(tls.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cadata, keydata)
+    s2 = server_ctx.wrap_socket(s2, server_side=True)
+    assert isinstance(s2.cipher(), tuple)
+    print(s2.read(16))
+    s2.write(b"server to client")
+    s2.close()
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    ciphers = client_ctx.get_ciphers()
+    assert "TLS-ECDHE-ECDSA-WITH-AES-128-CBC-SHA256" in ciphers
+    client_ctx.set_ciphers(["TLS-ECDHE-ECDSA-WITH-AES-128-CBC-SHA256"])
+    client_ctx.verify_mode = tls.CERT_REQUIRED
+    client_ctx.load_verify_locations(cadata)
+    s = client_ctx.wrap_socket(s, server_hostname="micropython.local")
+    s.write(b"client to server")
+    print(s.read(16))
+    s.close()
diff --git a/tests/multi_net/sslcontext_server_client_ciphers.py.exp b/tests/multi_net/sslcontext_server_client_ciphers.py.exp
new file mode 100644
index 00000000000..909c496d019
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_ciphers.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+b'client to server'
+--- instance1 ---
+b'server to client'
diff --git a/tests/multi_net/sslcontext_server_client_files.py b/tests/multi_net/sslcontext_server_client_files.py
new file mode 100644
index 00000000000..e42b9ea033a
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_files.py
@@ -0,0 +1,51 @@
+# Test creating an SSL connection with certificates from a file.
+
+try:
+    import os
+    import socket
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+cert = cafile = "ec_cert.der"
+key = "ec_key.der"
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cert, key)
+    s2 = server_ctx.wrap_socket(s2, server_side=True)
+    print(s2.read(16))
+    s2.write(b"server to client")
+    s2.close()
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = ssl.CERT_REQUIRED
+    client_ctx.load_verify_locations(cafile=cafile)
+    s = client_ctx.wrap_socket(s, server_hostname="micropython.local")
+    s.write(b"client to server")
+    print(s.read(16))
+    s.close()
diff --git a/tests/multi_net/sslcontext_server_client_files.py.exp b/tests/multi_net/sslcontext_server_client_files.py.exp
new file mode 100644
index 00000000000..909c496d019
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_files.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+b'client to server'
+--- instance1 ---
+b'server to client'
diff --git a/tests/multi_net/sslcontext_server_client_psk.py b/tests/multi_net/sslcontext_server_client_psk.py
new file mode 100644
index 00000000000..db189ac834f
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk.py
@@ -0,0 +1,57 @@
+# Test TCP server and client using TLS-PSK (pre-shared key).  The client
+# presents an identity/key via the psk_identity/psk_key attributes, and the
+# server looks the identity up in its server_psk_keys mapping.
+
+try:
+    import socket
+    import tls
+
+    # PSK support is optional; psk_identity only exists when it's enabled.
+    tls.SSLContext(tls.PROTOCOL_TLS_CLIENT).psk_identity
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# The identity the client presents and the key shared by both ends.  Both are
+# byte strings (the identity is an opaque octet string on the wire); readable
+# ones are used here for clarity.
+PSK_IDENTITY = b"micropython"
+PSK_KEY = b"secret-shared-key"
+
+# The server maps identities to keys.  A plain dict covers the common case;
+# any object with a get() method can be used to look keys up on demand.
+SERVER_KEYS = {PSK_IDENTITY: PSK_KEY}
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = tls.SSLContext(tls.PROTOCOL_TLS_SERVER)
+    server_ctx.server_psk_keys = SERVER_KEYS
+    s2 = server_ctx.wrap_socket(s2, server_side=True)
+    print(s2.read(16))
+    s2.write(b"server to client")
+    s2.close()
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    client_ctx.psk_identity = PSK_IDENTITY
+    client_ctx.psk_key = PSK_KEY
+    s = client_ctx.wrap_socket(s, server_hostname="micropython.local")
+    s.write(b"client to server")
+    print(s.read(16))
+    s.close()
diff --git a/tests/multi_net/sslcontext_server_client_psk.py.exp b/tests/multi_net/sslcontext_server_client_psk.py.exp
new file mode 100644
index 00000000000..909c496d019
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+b'client to server'
+--- instance1 ---
+b'server to client'
diff --git a/tests/multi_net/sslcontext_server_client_psk_bad_keys_error.py b/tests/multi_net/sslcontext_server_client_psk_bad_keys_error.py
new file mode 100644
index 00000000000..f061dd7b212
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_bad_keys_error.py
@@ -0,0 +1,60 @@
+# Adversarial test: if the server's server_psk_keys lookup misbehaves (here, by
+# returning a non-bytes value), the server must reject the handshake cleanly
+# rather than crashing or hanging.
+
+try:
+    import socket
+    import tls
+
+    # PSK support is optional; psk_identity only exists when it's enabled.
+    tls.SSLContext(tls.PROTOCOL_TLS_CLIENT).psk_identity
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+
+# A misbehaving mapping whose get() returns a non-bytes (invalid) key.
+class BadKeys:
+    def get(self, identity):
+        return 12345
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = tls.SSLContext(tls.PROTOCOL_TLS_SERVER)
+    server_ctx.server_psk_keys = BadKeys()
+    try:
+        server_ctx.wrap_socket(s2, server_side=True)
+        print("server: connected")
+    except OSError:
+        print("server: handshake failed")
+    multitest.broadcast("finished")
+    s2.close()
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    client_ctx.psk_identity = b"micropython"
+    client_ctx.psk_key = b"secret-shared-key"
+    try:
+        client_ctx.wrap_socket(s, server_hostname="micropython.local")
+        print("client: connected")
+    except OSError:
+        print("client: handshake failed")
+    # Don't close until the server has seen the failed handshake.
+    multitest.wait("finished")
+    s.close()
diff --git a/tests/multi_net/sslcontext_server_client_psk_bad_keys_error.py.exp b/tests/multi_net/sslcontext_server_client_psk_bad_keys_error.py.exp
new file mode 100644
index 00000000000..feff9d9884e
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_bad_keys_error.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+server: handshake failed
+--- instance1 ---
+client: handshake failed
diff --git a/tests/multi_net/sslcontext_server_client_psk_binary_identity.py b/tests/multi_net/sslcontext_server_client_psk_binary_identity.py
new file mode 100644
index 00000000000..90ff03ece0a
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_binary_identity.py
@@ -0,0 +1,52 @@
+# Test TLS-PSK with a non-ASCII (binary, non-UTF-8) identity, to confirm the
+# identity is handled as opaque bytes end to end (not assumed to be a UTF-8
+# string).  The client presents a binary identity that the server has a key for,
+# and the handshake succeeds.
+
+try:
+    import socket
+    import tls
+
+    # PSK support is optional; psk_identity only exists when it's enabled.
+    tls.SSLContext(tls.PROTOCOL_TLS_CLIENT).psk_identity
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+PSK_IDENTITY = b"\xff\xfe\xfd"
+PSK_KEY = b"secret-shared-key"
+SERVER_KEYS = {PSK_IDENTITY: PSK_KEY}
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = tls.SSLContext(tls.PROTOCOL_TLS_SERVER)
+    server_ctx.server_psk_keys = SERVER_KEYS
+    s2 = server_ctx.wrap_socket(s2, server_side=True)
+    print(s2.read(16))
+    s2.write(b"server to client")
+    s2.close()
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    client_ctx.psk_identity = PSK_IDENTITY
+    client_ctx.psk_key = PSK_KEY
+    s = client_ctx.wrap_socket(s, server_hostname="micropython.local")
+    s.write(b"client to server")
+    print(s.read(16))
+    s.close()
diff --git a/tests/multi_net/sslcontext_server_client_psk_binary_identity.py.exp b/tests/multi_net/sslcontext_server_client_psk_binary_identity.py.exp
new file mode 100644
index 00000000000..909c496d019
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_binary_identity.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+b'client to server'
+--- instance1 ---
+b'server to client'
diff --git a/tests/multi_net/sslcontext_server_client_psk_client_only_error.py b/tests/multi_net/sslcontext_server_client_psk_client_only_error.py
new file mode 100644
index 00000000000..fd6557d928c
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_client_only_error.py
@@ -0,0 +1,60 @@
+# Adversarial test: a PSK client must not connect to a non-PSK (certificate)
+# server.  Configuring PSK restricts the client to PSK cipher suites, so it has
+# no cipher suite in common with a certificate-only server and the handshake
+# fails on both ends (rather than silently falling back to a non-PSK suite).
+
+try:
+    import socket
+    import tls
+
+    # PSK support is optional; psk_identity only exists when it's enabled.
+    tls.SSLContext(tls.PROTOCOL_TLS_CLIENT).psk_identity
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+with open("ec_cert.der", "rb") as f:
+    cert = f.read()
+with open("ec_key.der", "rb") as f:
+    key = f.read()
+
+
+# Server: ordinary certificate-based server, no PSK.
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = tls.SSLContext(tls.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cert, key)
+    try:
+        server_ctx.wrap_socket(s2, server_side=True)
+        print("server: connected")
+    except OSError:
+        print("server: handshake failed")
+    multitest.broadcast("finished")
+    s2.close()
+    s.close()
+
+
+# Client: PSK configured, so only PSK cipher suites are offered.
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    client_ctx.psk_identity = b"micropython"
+    client_ctx.psk_key = b"secret-shared-key"
+    try:
+        client_ctx.wrap_socket(s, server_hostname="micropython.local")
+        print("client: connected")
+    except OSError:
+        print("client: handshake failed")
+    # Don't close until the server has seen the failed handshake.
+    multitest.wait("finished")
+    s.close()
diff --git a/tests/multi_net/sslcontext_server_client_psk_client_only_error.py.exp b/tests/multi_net/sslcontext_server_client_psk_client_only_error.py.exp
new file mode 100644
index 00000000000..feff9d9884e
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_client_only_error.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+server: handshake failed
+--- instance1 ---
+client: handshake failed
diff --git a/tests/multi_net/sslcontext_server_client_psk_no_fallback_error.py b/tests/multi_net/sslcontext_server_client_psk_no_fallback_error.py
new file mode 100644
index 00000000000..d81e6cabe5d
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_no_fallback_error.py
@@ -0,0 +1,70 @@
+# Adversarial test: a PSK client must not fall back to a non-PSK (certificate)
+# cipher suite, even when it would otherwise trust the server's certificate.
+# Configuring PSK restricts the client to PSK cipher suites, so it has no cipher
+# suite in common with a certificate-only server and the handshake fails.
+#
+# Unlike sslcontext_server_client_psk_client_only_error.py (where the client
+# rejects the server's certificate), here the client is given the CA so the
+# certificate would verify.  The only thing preventing a connection is the PSK
+# cipher-suite restriction, so this test fails if that restriction is not
+# applied (i.e. the client would negotiate a certificate suite and connect).
+
+try:
+    import socket
+    import tls
+
+    # PSK support is optional; psk_identity only exists when it's enabled.
+    tls.SSLContext(tls.PROTOCOL_TLS_CLIENT).psk_identity
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+with open("ec_cert.der", "rb") as f:
+    cert = cadata = f.read()
+with open("ec_key.der", "rb") as f:
+    key = f.read()
+
+
+# Server: ordinary certificate-based server, no PSK.
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = tls.SSLContext(tls.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cert, key)
+    try:
+        server_ctx.wrap_socket(s2, server_side=True)
+        print("server: connected")
+    except OSError:
+        print("server: handshake failed")
+    multitest.broadcast("finished")
+    s2.close()
+    s.close()
+
+
+# Client: PSK configured (so only PSK suites are offered) but also trusts the
+# server's certificate, so the only reason the handshake fails is the PSK
+# cipher-suite restriction.
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = tls.CERT_REQUIRED
+    client_ctx.load_verify_locations(cadata)
+    client_ctx.psk_identity = b"micropython"
+    client_ctx.psk_key = b"secret-shared-key"
+    try:
+        client_ctx.wrap_socket(s, server_hostname="micropython.local")
+        print("client: connected")
+    except OSError:
+        print("client: handshake failed")
+    # Don't close until the server has seen the failed handshake.
+    multitest.wait("finished")
+    s.close()
diff --git a/tests/multi_net/sslcontext_server_client_psk_no_fallback_error.py.exp b/tests/multi_net/sslcontext_server_client_psk_no_fallback_error.py.exp
new file mode 100644
index 00000000000..feff9d9884e
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_no_fallback_error.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+server: handshake failed
+--- instance1 ---
+client: handshake failed
diff --git a/tests/multi_net/sslcontext_server_client_psk_object.py b/tests/multi_net/sslcontext_server_client_psk_object.py
new file mode 100644
index 00000000000..26889794f02
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_object.py
@@ -0,0 +1,60 @@
+# Test TLS-PSK where the server's server_psk_keys is a user object with a get()
+# method (rather than a dict), so the key lookup is a callback into Python
+# during the handshake.
+
+try:
+    import socket
+    import tls
+
+    # PSK support is optional; psk_identity only exists when it's enabled.
+    tls.SSLContext(tls.PROTOCOL_TLS_CLIENT).psk_identity
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+PSK_IDENTITY = b"micropython"
+PSK_KEY = b"secret-shared-key"
+
+
+# A mapping-like object whose get() is called with the client's identity during
+# the server handshake; it returns the key, or None to reject the identity.
+class PSKStore:
+    def get(self, identity):
+        print("server: lookup", identity)
+        if identity == PSK_IDENTITY:
+            return PSK_KEY
+        return None
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = tls.SSLContext(tls.PROTOCOL_TLS_SERVER)
+    server_ctx.server_psk_keys = PSKStore()
+    s2 = server_ctx.wrap_socket(s2, server_side=True)
+    print(s2.read(16))
+    s2.write(b"server to client")
+    s2.close()
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    client_ctx.psk_identity = PSK_IDENTITY
+    client_ctx.psk_key = PSK_KEY
+    s = client_ctx.wrap_socket(s, server_hostname="micropython.local")
+    s.write(b"client to server")
+    print(s.read(16))
+    s.close()
diff --git a/tests/multi_net/sslcontext_server_client_psk_object.py.exp b/tests/multi_net/sslcontext_server_client_psk_object.py.exp
new file mode 100644
index 00000000000..e22c94395f4
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_object.py.exp
@@ -0,0 +1,5 @@
+--- instance0 ---
+server: lookup b'micropython'
+b'client to server'
+--- instance1 ---
+b'server to client'
diff --git a/tests/multi_net/sslcontext_server_client_psk_server_no_fallback_error.py b/tests/multi_net/sslcontext_server_client_psk_server_no_fallback_error.py
new file mode 100644
index 00000000000..dfbca8affb8
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_server_no_fallback_error.py
@@ -0,0 +1,72 @@
+# Adversarial test: a PSK server must not fall back to a non-PSK (certificate)
+# cipher suite, even when it has a certificate loaded that the client trusts.
+# Configuring server_psk_keys restricts the server to PSK cipher suites, so it
+# has no cipher suite in common with a certificate-only client and the handshake
+# fails.
+#
+# This is the server-side counterpart to
+# sslcontext_server_client_psk_no_fallback_error.py: the server is given both a
+# certificate (which the client trusts) and PSK keys, so the only thing
+# preventing a connection is the PSK cipher-suite restriction.  This test fails
+# if that restriction is not applied (i.e. the server would negotiate a
+# certificate suite and connect).
+
+try:
+    import socket
+    import tls
+
+    # PSK support is optional; psk_identity only exists when it's enabled.
+    tls.SSLContext(tls.PROTOCOL_TLS_CLIENT).psk_identity
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+with open("ec_cert.der", "rb") as f:
+    cert = cadata = f.read()
+with open("ec_key.der", "rb") as f:
+    key = f.read()
+
+SERVER_KEYS = {b"micropython": b"secret-shared-key"}
+
+
+# Server: has a certificate the client trusts, but also configures PSK, which
+# restricts it to PSK suites only.
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = tls.SSLContext(tls.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cert, key)
+    server_ctx.server_psk_keys = SERVER_KEYS
+    try:
+        server_ctx.wrap_socket(s2, server_side=True)
+        print("server: connected")
+    except OSError:
+        print("server: handshake failed")
+    multitest.broadcast("finished")
+    s2.close()
+    s.close()
+
+
+# Client: ordinary certificate client with no PSK, trusting the server's cert.
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = tls.CERT_REQUIRED
+    client_ctx.load_verify_locations(cadata)
+    try:
+        client_ctx.wrap_socket(s, server_hostname="micropython.local")
+        print("client: connected")
+    except OSError:
+        print("client: handshake failed")
+    # Don't close until the server has seen the failed handshake.
+    multitest.wait("finished")
+    s.close()
diff --git a/tests/multi_net/sslcontext_server_client_psk_server_no_fallback_error.py.exp b/tests/multi_net/sslcontext_server_client_psk_server_no_fallback_error.py.exp
new file mode 100644
index 00000000000..feff9d9884e
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_server_no_fallback_error.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+server: handshake failed
+--- instance1 ---
+client: handshake failed
diff --git a/tests/multi_net/sslcontext_server_client_psk_server_only_error.py b/tests/multi_net/sslcontext_server_client_psk_server_only_error.py
new file mode 100644
index 00000000000..39a77e06c44
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_server_only_error.py
@@ -0,0 +1,55 @@
+# Adversarial test: a non-PSK client must not connect to a PSK server.  A PSK
+# server offers only PSK cipher suites, so an ordinary client has no cipher
+# suite in common with it and the handshake fails on both ends.
+
+try:
+    import socket
+    import tls
+
+    # PSK support is optional; psk_identity only exists when it's enabled.
+    tls.SSLContext(tls.PROTOCOL_TLS_CLIENT).psk_identity
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+SERVER_KEYS = {b"micropython": b"secret-shared-key"}
+
+
+# Server: PSK configured, so only PSK cipher suites are offered.
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = tls.SSLContext(tls.PROTOCOL_TLS_SERVER)
+    server_ctx.server_psk_keys = SERVER_KEYS
+    try:
+        server_ctx.wrap_socket(s2, server_side=True)
+        print("server: connected")
+    except OSError:
+        print("server: handshake failed")
+    multitest.broadcast("finished")
+    s2.close()
+    s.close()
+
+
+# Client: ordinary client with no PSK configured.
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = tls.CERT_NONE
+    try:
+        client_ctx.wrap_socket(s, server_hostname="micropython.local")
+        print("client: connected")
+    except OSError:
+        print("client: handshake failed")
+    # Don't close until the server has seen the failed handshake.
+    multitest.wait("finished")
+    s.close()
diff --git a/tests/multi_net/sslcontext_server_client_psk_server_only_error.py.exp b/tests/multi_net/sslcontext_server_client_psk_server_only_error.py.exp
new file mode 100644
index 00000000000..feff9d9884e
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_server_only_error.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+server: handshake failed
+--- instance1 ---
+client: handshake failed
diff --git a/tests/multi_net/sslcontext_server_client_psk_unknown_identity_error.py b/tests/multi_net/sslcontext_server_client_psk_unknown_identity_error.py
new file mode 100644
index 00000000000..734a588f25b
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_unknown_identity_error.py
@@ -0,0 +1,56 @@
+# Adversarial test: a PSK client presenting an identity the server does not
+# know must not connect.  The server's lookup (server_psk_keys.get(identity))
+# returns None, which rejects the handshake.
+
+try:
+    import socket
+    import tls
+
+    # PSK support is optional; psk_identity only exists when it's enabled.
+    tls.SSLContext(tls.PROTOCOL_TLS_CLIENT).psk_identity
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+SERVER_KEYS = {b"micropython": b"secret-shared-key"}
+
+
+# Server: only knows the "micropython" identity.
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = tls.SSLContext(tls.PROTOCOL_TLS_SERVER)
+    server_ctx.server_psk_keys = SERVER_KEYS
+    try:
+        server_ctx.wrap_socket(s2, server_side=True)
+        print("server: connected")
+    except OSError:
+        print("server: handshake failed")
+    multitest.broadcast("finished")
+    s2.close()
+    s.close()
+
+
+# Client: presents an identity the server has no key for.
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    client_ctx.psk_identity = b"stranger"
+    client_ctx.psk_key = b"secret-shared-key"
+    try:
+        client_ctx.wrap_socket(s, server_hostname="micropython.local")
+        print("client: connected")
+    except OSError:
+        print("client: handshake failed")
+    # Don't close until the server has seen the failed handshake.
+    multitest.wait("finished")
+    s.close()
diff --git a/tests/multi_net/sslcontext_server_client_psk_unknown_identity_error.py.exp b/tests/multi_net/sslcontext_server_client_psk_unknown_identity_error.py.exp
new file mode 100644
index 00000000000..feff9d9884e
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_unknown_identity_error.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+server: handshake failed
+--- instance1 ---
+client: handshake failed
diff --git a/tests/multi_net/sslcontext_server_client_psk_wrong_key_error.py b/tests/multi_net/sslcontext_server_client_psk_wrong_key_error.py
new file mode 100644
index 00000000000..3483c6b2803
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_wrong_key_error.py
@@ -0,0 +1,57 @@
+# Adversarial test: a PSK client presenting the wrong key for a known identity
+# must not connect.  Both ends negotiate a PSK cipher suite, but the mismatched
+# keys cause the handshake to fail on both ends.
+
+try:
+    import socket
+    import tls
+
+    # PSK support is optional; psk_identity only exists when it's enabled.
+    tls.SSLContext(tls.PROTOCOL_TLS_CLIENT).psk_identity
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+PSK_IDENTITY = b"micropython"
+SERVER_KEYS = {PSK_IDENTITY: b"secret-shared-key"}
+
+
+# Server: knows the correct key for the identity.
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = tls.SSLContext(tls.PROTOCOL_TLS_SERVER)
+    server_ctx.server_psk_keys = SERVER_KEYS
+    try:
+        server_ctx.wrap_socket(s2, server_side=True)
+        print("server: connected")
+    except OSError:
+        print("server: handshake failed")
+    multitest.broadcast("finished")
+    s2.close()
+    s.close()
+
+
+# Client: presents the right identity but the wrong key.
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    client_ctx.psk_identity = PSK_IDENTITY
+    client_ctx.psk_key = b"wrong-key"
+    try:
+        client_ctx.wrap_socket(s, server_hostname="micropython.local")
+        print("client: connected")
+    except OSError:
+        print("client: handshake failed")
+    # Don't close until the server has seen the failed handshake.
+    multitest.wait("finished")
+    s.close()
diff --git a/tests/multi_net/sslcontext_server_client_psk_wrong_key_error.py.exp b/tests/multi_net/sslcontext_server_client_psk_wrong_key_error.py.exp
new file mode 100644
index 00000000000..feff9d9884e
--- /dev/null
+++ b/tests/multi_net/sslcontext_server_client_psk_wrong_key_error.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+server: handshake failed
+--- instance1 ---
+client: handshake failed
diff --git a/tests/multi_net/sslcontext_verify_callback.py b/tests/multi_net/sslcontext_verify_callback.py
new file mode 100644
index 00000000000..b762b95e6f0
--- /dev/null
+++ b/tests/multi_net/sslcontext_verify_callback.py
@@ -0,0 +1,68 @@
+# Test creating an SSL connection and getting the peer certificate.
+
+try:
+    import io
+    import os
+    import socket
+    import tls
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(tls, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+cert = cafile = "ec_cert.der"
+key = "ec_key.der"
+
+with open(cafile, "rb") as f:
+    cadata = f.read()
+with open(key, "rb") as f:
+    key = f.read()
+
+
+def verify_callback(cert, depth):
+    print(cert.hex())
+    return 0
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = tls.SSLContext(tls.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cadata, key)
+    s2 = server_ctx.wrap_socket(s2, server_side=True)
+    print(s2.read(16))
+    s2.write(b"server to client")
+    s2.close()
+    s.close()
+
+
+# Client
+def instance1():
+    s_test = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    if not hasattr(s_test, "verify_callback"):
+        print("SKIP")
+        raise SystemExit
+
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = tls.CERT_REQUIRED
+    client_ctx.verify_callback = verify_callback
+    client_ctx.load_verify_locations(cadata)
+    s = client_ctx.wrap_socket(s, server_hostname="micropython.local")
+    s.write(b"client to server")
+    print(s.read(16))
+    s.close()
diff --git a/tests/multi_net/sslcontext_verify_callback.py.exp b/tests/multi_net/sslcontext_verify_callback.py.exp
new file mode 100644
index 00000000000..a0dfaa4f567
--- /dev/null
+++ b/tests/multi_net/sslcontext_verify_callback.py.exp
@@ -0,0 +1,5 @@
+--- instance0 ---
+b'client to server'
+--- instance1 ---
+308201d230820179a003020102021403abf0266b125f8ec8dab57c34d655e94ae4d9bb300a06082a8648ce3d040302303f311a301806035504030c116d6963726f707974686f6e2e6c6f63616c31143012060355040a0c0b4d6963726f507974686f6e310b3009060355040613024155301e170d3235303131343033333531375a170d3335303131323033333531375a303f311a301806035504030c116d6963726f707974686f6e2e6c6f63616c31143012060355040a0c0b4d6963726f507974686f6e310b30090603550406130241553059301306072a8648ce3d020106082a8648ce3d030107034200042882eb6055f540ddef989ec7ead84281461436745d1b9174a2730f2bc74cf91e1ce62d4ada67129e891ab70e73a34b644b000d8d1e4c43189a3390210f447169a3533051301d0603551d0e0416041410ced497be3800e7b19008504efd0adc39bfe02a301f0603551d2304183016801410ced497be3800e7b19008504efd0adc39bfe02a300f0603551d130101ff040530030101ff300a06082a8648ce3d040302034700304402204b153d2851e7846351383beac1d5b39791810156b0f9672040d3499e14041969022013e32fe207d3bccb7f9859b3140e73074f656c034679135e517b1e9cb05ae5b2
+b'server to client'
diff --git a/tests/multi_net/sslcontext_verify_error.py b/tests/multi_net/sslcontext_verify_error.py
new file mode 100644
index 00000000000..f6e1477bfed
--- /dev/null
+++ b/tests/multi_net/sslcontext_verify_error.py
@@ -0,0 +1,55 @@
+# Test creating an SSL connection with an invalid certificate.
+
+try:
+    import os
+    import socket
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+cert = cafile = "ec_cert.der"
+key = "ec_key.der"
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cert, key)
+    try:
+        s2 = server_ctx.wrap_socket(s2, server_side=True)
+    except Exception as e:
+        print(e)
+    multitest.broadcast("finished")
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = ssl.CERT_REQUIRED
+    client_ctx.load_verify_locations(cafile=cafile)
+    try:
+        s = client_ctx.wrap_socket(s, server_hostname="foobar.local")
+    except Exception as e:
+        print(e)
+    # Don't close the socket until the server has seen our SSL rejection.
+    multitest.wait("finished")
+    s.close()
diff --git a/tests/multi_net/sslcontext_verify_error.py.exp b/tests/multi_net/sslcontext_verify_error.py.exp
new file mode 100644
index 00000000000..900c7c23089
--- /dev/null
+++ b/tests/multi_net/sslcontext_verify_error.py.exp
@@ -0,0 +1,6 @@
+--- instance0 ---
+(-30592, 'MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE')
+--- instance1 ---
+
+The certificate Common Name (CN) does not match with the expected CN
+
diff --git a/tests/multi_net/sslcontext_verify_time_error.py b/tests/multi_net/sslcontext_verify_time_error.py
new file mode 100644
index 00000000000..274d99d4bf6
--- /dev/null
+++ b/tests/multi_net/sslcontext_verify_time_error.py
@@ -0,0 +1,55 @@
+# Test creating an SSL connection with an expired certificate.
+
+try:
+    import os
+    import socket
+    import ssl
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+cert = cafile = "expired_cert.der"
+key = "ec_key.der"
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen(1)
+    multitest.next()
+    s2, _ = s.accept()
+    server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    server_ctx.load_cert_chain(cert, key)
+    try:
+        s2 = server_ctx.wrap_socket(s2, server_side=True)
+    except Exception as e:
+        print(e)
+    multitest.broadcast("finished")
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    client_ctx.verify_mode = ssl.CERT_REQUIRED
+    client_ctx.load_verify_locations(cafile=cafile)
+    try:
+        s = client_ctx.wrap_socket(s, server_hostname="micropython.local")
+    except Exception as e:
+        print(e)
+    # Don't close the socket until the server has seen our SSL rejection.
+    multitest.wait("finished")
+    s.close()
diff --git a/tests/multi_net/sslcontext_verify_time_error.py.exp b/tests/multi_net/sslcontext_verify_time_error.py.exp
new file mode 100644
index 00000000000..25f754c97e5
--- /dev/null
+++ b/tests/multi_net/sslcontext_verify_time_error.py.exp
@@ -0,0 +1,6 @@
+--- instance0 ---
+(-30592, 'MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE')
+--- instance1 ---
+
+The certificate validity has expired
+
diff --git a/tests/multi_net/tcp_accept_recv.py b/tests/multi_net/tcp_accept_recv.py
index dee14e3b977..4108a6f8a3e 100644
--- a/tests/multi_net/tcp_accept_recv.py
+++ b/tests/multi_net/tcp_accept_recv.py
@@ -1,30 +1,73 @@
-# Test recv on socket that just accepted a connection
+# Test recv on listening socket after accept(), with various listen() arguments
 
 import socket
 
 PORT = 8000
 
+# Test cases for listen() function
+LISTEN_ARGS = [None, 0, 1, 2]  # None means no argument
+
 
 # Server
 def instance0():
     multitest.globals(IP=multitest.get_network_ip())
-    s = socket.socket()
-    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
-    s.listen(1)
     multitest.next()
-    s.accept()
-    try:
-        print("recv", s.recv(10))  # should raise Errno 107 ENOTCONN
-    except OSError as er:
-        print(er.errno in (107, 128))
-    s.close()
+
+    test_num = 0
+    for blocking_mode in [True, False]:
+        for listen_arg in LISTEN_ARGS:
+            test_num += 1
+            s = socket.socket()
+            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+            s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+
+            # Call listen with or without argument based on test case
+            if listen_arg is None:
+                print(f"Test case {test_num}/8: listen() blocking={blocking_mode}")
+                s.listen()
+            else:
+                print(f"Test case {test_num}/8: listen({listen_arg}) blocking={blocking_mode}")
+                s.listen(listen_arg)
+
+            # Signal client that server is ready
+            multitest.broadcast(f"server_ready_{test_num}")
+
+            # Wait for client connection
+            c, _ = s.accept()
+
+            # Set blocking mode after accept
+            s.setblocking(blocking_mode)
+
+            try:
+                print("recv", s.recv(10))  # should raise Errno 107 ENOTCONN
+            except OSError as er:
+                # Verify the error code is either 107 (ENOTCONN) or 128 (ENOTCONN on Windows)
+                print(er.errno in (107, 128))
+
+            # Cleanup
+            c.close()
+            s.close()
+
+            # Signal client we're done with this test case
+            multitest.broadcast(f"server_done_{test_num}")
 
 
 # Client
 def instance1():
     multitest.next()
-    s = socket.socket()
-    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
-    s.send(b"GET / HTTP/1.0\r\n\r\n")
-    s.close()
+
+    test_num = 0
+    for blocking_mode in [True, False]:
+        for _ in LISTEN_ARGS:
+            test_num += 1
+            # Wait for server to be ready
+            multitest.wait(f"server_ready_{test_num}")
+
+            # Connect to server
+            s = socket.socket()
+            s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+            s.send(b"GET / HTTP/1.0\r\n\r\n")
+            s.close()
+
+            # Wait for server to finish this test case
+            multitest.wait(f"server_done_{test_num}")
diff --git a/tests/multi_net/tcp_client_rst.py b/tests/multi_net/tcp_client_rst.py
index 1fe994f36ce..6e74edc194d 100644
--- a/tests/multi_net/tcp_client_rst.py
+++ b/tests/multi_net/tcp_client_rst.py
@@ -2,6 +2,12 @@
 
 import struct, time, socket, select
 
+try:
+    import errno
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
 PORT = 8000
 
 
@@ -18,27 +24,49 @@ def instance0():
     s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
     s.listen(1)
     multitest.next()
-    s2, _ = s.accept()
-    s2.setblocking(False)
-    poll = select.poll()
-    poll.register(s2, select.POLLIN)
-    time.sleep(0.4)
-    print(convert_poll_list(poll.poll(1000)))
-    # TODO: the following recv don't work with lwip, it abandons data upon TCP RST
-    try:
-        print(s2.recv(10))
+
+    for iteration in range(2):
+        print("server iteration", iteration)
+
+        # Accept a connection from the client.
+        s2, _ = s.accept()
+        s2.setblocking(False)
+
+        # Create a poller for the connected socket.
+        poll = select.poll()
+        poll.register(s2, select.POLLIN)
+
+        # Wait for the client to send data and issue a TCP RST.
+        for _ in range(10):
+            if select.POLLIN | select.POLLERR | select.POLLHUP in convert_poll_list(
+                poll.poll(1000)
+            ):
+                break
+            time.sleep(0.1)
         print(convert_poll_list(poll.poll(1000)))
-        print(s2.recv(10))
+
+        # On the second test, drain the incoming data.
+        if iteration == 1:
+            for _ in range(5):
+                try:
+                    print(s2.recv(10))
+                except OSError as er:
+                    # This error should only happen on the 3rd recv.
+                    print("ECONNRESET:", er.errno == errno.ECONNRESET)
+                print(convert_poll_list(poll.poll(1000)))
+
+        # Close the connection to the client.
+        s2.close()
         print(convert_poll_list(poll.poll(1000)))
-        print(s2.recv(10))
+
+        # Try to read more data from the closed socket, to make sure the error code is correct.
+        try:
+            print(s2.recv(10))
+        except OSError as er:
+            print("EBADF:", er.errno == errno.EBADF)
         print(convert_poll_list(poll.poll(1000)))
-    except OSError as er:
-        print(er.errno)
-    print(convert_poll_list(poll.poll(1000)))
-    # TODO lwip raises here but apparently it shouldn't
-    print(s2.recv(10))
-    print(convert_poll_list(poll.poll(1000)))
-    s2.close()
+
+    # Close the listening socket.
     s.close()
 
 
@@ -47,11 +75,13 @@ def instance1():
     if not hasattr(socket, "SO_LINGER"):
         multitest.skip()
     multitest.next()
-    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
-    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
-    lgr_onoff = 1
-    lgr_linger = 0
-    s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", lgr_onoff, lgr_linger))
-    s.send(b"GET / HTTP/1.0\r\n\r\n")
-    time.sleep(0.2)
-    s.close()  # This issues a TCP RST since we've set the linger option
+    for iteration in range(2):
+        print("client iteration", iteration)
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+        s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+        lgr_onoff = 1
+        lgr_linger = 0
+        s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", lgr_onoff, lgr_linger))
+        s.send(b"GET / HTTP/1.0\r\n\r\n")
+        time.sleep(0.2)
+        s.close()  # This issues a TCP RST since we've set the linger option
diff --git a/tests/multi_net/tcp_recv_peek.py b/tests/multi_net/tcp_recv_peek.py
new file mode 100644
index 00000000000..4b8dd6768e4
--- /dev/null
+++ b/tests/multi_net/tcp_recv_peek.py
@@ -0,0 +1,46 @@
+# Test TCP recv with MSG_PEEK
+#
+# Note that bare metal LWIP only returns at most one packet's worth of TCP data
+# in any recv() call - including when peeking - so can't be too clever with
+# different recv() combinations
+import socket
+import random
+
+
+# Server
+def instance0():
+    PORT = 10000 + random.getrandbits(15)
+    multitest.globals(IP=multitest.get_network_ip(), PORT=PORT)
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen()
+    multitest.next()
+    s2, _ = s.accept()
+    print(s2.recv(8, socket.MSG_PEEK))
+    print(s2.recv(8))
+    s2.send(b"1234567890")
+    multitest.broadcast("0-sent")
+    multitest.wait("1-sent")
+    print(s2.recv(5, socket.MSG_PEEK))
+    print(s2.recv(5, socket.MSG_PEEK))
+    multitest.broadcast("0-recved")
+    multitest.wait("1-recved")  # sync here necessary as MP sends RST if closing TCP early
+    s2.close()
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    s.send(b"abcdefgh")
+    multitest.broadcast("1-sent")
+    multitest.wait("0-sent")
+    s.send(b"klmnopqr")
+    print(s.recv(5, socket.MSG_PEEK))
+    print(s.recv(10))
+    multitest.broadcast("1-recved")
+    multitest.wait("0-recved")
+    s.close()
diff --git a/tests/multi_net/tcp_sendall.py b/tests/multi_net/tcp_sendall.py
new file mode 100644
index 00000000000..ee9b780edff
--- /dev/null
+++ b/tests/multi_net/tcp_sendall.py
@@ -0,0 +1,71 @@
+# Simple test of a TCP server and client transferring data using sendall.
+
+import socket
+
+if not hasattr(socket.socket, "sendall"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# This is the smallest send buffer size supported by Linux, see socket(7).
+SEND_SIZE = 1024
+BUFFERS = 8
+
+
+# G. D. Nguyen, "Fast CRCs", vol. 18, no. 10, pp. 1321-1331, Oct. 2009
+# https://dl.acm.org/doi/abs/10.1109/TC.2009.83
+def calculate_checksum(buffer):
+    checksum = 0
+    for byte in buffer:
+        checksum = (checksum ^ byte) & 0xFFFF
+        for step in range(16):
+            if (checksum & 0x8000) != 0:
+                checksum = ((checksum << 1) ^ 0x8005) & 0xFFFF
+            else:
+                checksum = (checksum << 1) & 0xFFFF
+    return checksum
+
+
+# Server
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+    s = socket.socket()
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    s.listen()
+    multitest.next()
+    s2, _ = s.accept()
+    buffer = b""
+    while len(buffer) < SEND_SIZE * BUFFERS:
+        buffer += s2.recv(SEND_SIZE * BUFFERS)
+    s2.send(b"%08x" % len(buffer))
+    s2.send(b"%04x" % calculate_checksum(buffer))
+    s2.close()
+    s.close()
+    print("received {} bytes".format(len(buffer)))
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket()
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    # Try our best to trigger a partial send in socket.sendall
+    if hasattr(socket, "SO_SNDBUF"):
+        s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, SEND_SIZE)
+    buffer = bytearray(SEND_SIZE * BUFFERS)
+    for index in range(len(buffer)):
+        buffer[index] = index & 0xFF
+    expected_checksum = calculate_checksum(buffer)
+    s.sendall(buffer)
+    print("sent {} bytes".format(len(buffer)))
+    length = int(s.recv(8), 16)
+    if length != len(buffer):
+        raise Exception("sendall len fail (expected %d, got %d)" % (len(buffer), length))
+    checksum = int(s.recv(4), 16)
+    if checksum != expected_checksum:
+        raise Exception(
+            "sendall checksum fail (expected %04x, got %04x)" % (expected_checksum, checksum)
+        )
+    s.close()
diff --git a/tests/multi_net/tls_dtls_server_client.py b/tests/multi_net/tls_dtls_server_client.py
new file mode 100644
index 00000000000..10fac3c6f42
--- /dev/null
+++ b/tests/multi_net/tls_dtls_server_client.py
@@ -0,0 +1,94 @@
+# Test DTLS server and client, sending a small amount of data between them.
+
+try:
+    import socket
+    import tls
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(tls, "PROTOCOL_DTLS_SERVER"):
+    print("SKIP")
+    raise SystemExit
+
+PORT = 8000
+
+# These are test certificates. See tests/README.md for details.
+certfile = "ec_cert.der"
+keyfile = "ec_key.der"
+
+with open(certfile, "rb") as cf:
+    cert = cadata = cf.read()
+with open(keyfile, "rb") as kf:
+    key = kf.read()
+
+
+# DTLS server.
+def instance0():
+    multitest.globals(IP=multitest.get_network_ip())
+
+    # Create a UDP socket and bind it to accept incoming connections.
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+
+    multitest.next()
+
+    ctx = tls.SSLContext(tls.PROTOCOL_DTLS_SERVER)
+    ctx.load_cert_chain(cert, key)
+
+    # Because of "hello verify required", we expect the peer
+    # to connect twice: once to set the cookie, then second time
+    # successfully.
+    #
+    # As this isn't a real server, we hard-code two connection attempts
+    for _ in range(2):
+        print("waiting")
+        # Wait for the client to connect so we know their address
+        _, client_addr = s.recvfrom(1, socket.MSG_PEEK)
+        print("incoming connection")
+        s.connect(client_addr)  # Connect back to the client
+
+        # Wrap the UDP socket in server mode.
+        try:
+            s = ctx.wrap_socket(s, server_side=1, client_id=repr(client_addr).encode())
+        except OSError as e:
+            print(e)
+            continue  # wait for second connection
+
+        # Transfer some data.
+        for i in range(4):
+            print(s.recv(32))
+            s.send(b"server to client " + str(i).encode())
+
+        # Close the DTLS and UDP connection.
+        s.close()
+        break
+
+
+# DTLS client.
+def instance1():
+    multitest.next()
+
+    # Create a UDP socket and connect to the server.
+    addr = socket.getaddrinfo(IP, PORT)[0][-1]
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    print("connect")
+    s.connect(addr)
+
+    # Create a DTLS context and load the certificate.
+    ctx = tls.SSLContext(tls.PROTOCOL_DTLS_CLIENT)
+    ctx.verify_mode = tls.CERT_REQUIRED
+    ctx.load_verify_locations(cadata)
+
+    # Wrap the UDP socket.
+    print("wrap socket")
+    s = ctx.wrap_socket(s, server_hostname="micropython.local")
+
+    # Transfer some data.
+    for i in range(4):
+        s.send(b"client to server " + str(i).encode())
+        print(s.recv(32))
+
+    # Close the DTLS and UDP connection.
+    s.close()
diff --git a/tests/multi_net/tls_dtls_server_client.py.exp b/tests/multi_net/tls_dtls_server_client.py.exp
new file mode 100644
index 00000000000..3de03056740
--- /dev/null
+++ b/tests/multi_net/tls_dtls_server_client.py.exp
@@ -0,0 +1,17 @@
+--- instance0 ---
+waiting
+incoming connection
+(-27264, 'MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED')
+waiting
+incoming connection
+b'client to server 0'
+b'client to server 1'
+b'client to server 2'
+b'client to server 3'
+--- instance1 ---
+connect
+wrap socket
+b'server to client 0'
+b'server to client 1'
+b'server to client 2'
+b'server to client 3'
diff --git a/tests/multi_net/uasyncio_tcp_client_rst.py b/tests/multi_net/uasyncio_tcp_client_rst.py
deleted file mode 100644
index d81f22748e4..00000000000
--- a/tests/multi_net/uasyncio_tcp_client_rst.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# Test TCP server with client issuing TCP RST part way through read
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-import struct, time, socket
-
-PORT = 8000
-
-
-async def handle_connection(reader, writer):
-    data = await reader.read(10)  # should succeed
-    print(data)
-    await asyncio.sleep(0.2)  # wait for client to drop connection
-    try:
-        data = await reader.read(100)
-        print(data)
-        writer.close()
-        await writer.wait_closed()
-    except OSError as er:
-        print("OSError", er.errno)
-    ev.set()
-
-
-async def main():
-    global ev
-    ev = asyncio.Event()
-    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT)
-    multitest.next()
-    async with server:
-        await asyncio.wait_for(ev.wait(), 10)
-
-
-def instance0():
-    multitest.globals(IP=multitest.get_network_ip())
-    asyncio.run(main())
-
-
-def instance1():
-    if not hasattr(socket, "SO_LINGER"):
-        multitest.skip()
-    multitest.next()
-    s = socket.socket()
-    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
-    lgr_onoff = 1
-    lgr_linger = 0
-    s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", lgr_onoff, lgr_linger))
-    s.send(b"GET / HTTP/1.0\r\n\r\n")
-    time.sleep(0.1)
-    s.close()  # This issues a TCP RST since we've set the linger option
diff --git a/tests/multi_net/uasyncio_tcp_close_write.py b/tests/multi_net/uasyncio_tcp_close_write.py
deleted file mode 100644
index 5698ed8b169..00000000000
--- a/tests/multi_net/uasyncio_tcp_close_write.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# Test uasyncio TCP stream closing then writing
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-PORT = 8000
-
-
-async def handle_connection(reader, writer):
-    # Write data to ensure connection
-    writer.write(b"x")
-    await writer.drain()
-
-    # Read, should return nothing
-    print("read:", await reader.read(100))
-
-    # Close connection
-    print("close")
-    writer.close()
-    await writer.wait_closed()
-
-    print("done")
-    ev.set()
-
-
-async def tcp_server():
-    global ev
-    ev = asyncio.Event()
-    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT)
-    print("server running")
-    multitest.next()
-    async with server:
-        await asyncio.wait_for(ev.wait(), 5)
-
-
-async def tcp_client():
-    reader, writer = await asyncio.open_connection(IP, PORT)
-
-    # Read data to ensure connection
-    print("read:", await reader.read(1))
-
-    # Close connection
-    print("close")
-    writer.close()
-    await writer.wait_closed()
-
-    # Try writing data to the closed connection
-    print("write")
-    try:
-        writer.write(b"x")
-        await writer.drain()
-    except OSError:
-        print("OSError")
-
-
-def instance0():
-    multitest.globals(IP=multitest.get_network_ip())
-    asyncio.run(tcp_server())
-
-
-def instance1():
-    multitest.next()
-    asyncio.run(tcp_client())
diff --git a/tests/multi_net/uasyncio_tcp_readall.py b/tests/multi_net/uasyncio_tcp_readall.py
deleted file mode 100644
index c411f1c69d6..00000000000
--- a/tests/multi_net/uasyncio_tcp_readall.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# Test uasyncio stream read(-1) method using TCP server/client
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-PORT = 8000
-
-
-async def handle_connection(reader, writer):
-    writer.write(b"a")
-    await writer.drain()
-
-    # Split the first 2 bytes up so the client must wait for the second one
-    await asyncio.sleep(0.1)
-
-    writer.write(b"b")
-    await writer.drain()
-
-    writer.write(b"c")
-    await writer.drain()
-
-    print("close")
-    writer.close()
-    await writer.wait_closed()
-
-    print("done")
-    ev.set()
-
-
-async def tcp_server():
-    global ev
-    ev = asyncio.Event()
-    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT)
-    print("server running")
-    multitest.next()
-    async with server:
-        await asyncio.wait_for(ev.wait(), 2)
-
-
-async def tcp_client():
-    reader, writer = await asyncio.open_connection(IP, PORT)
-    print(await reader.read())
-    print(await reader.read())  # should be empty
-    print(await reader.read(1))  # should be empty
-
-
-def instance0():
-    multitest.globals(IP=multitest.get_network_ip())
-    asyncio.run(tcp_server())
-
-
-def instance1():
-    multitest.next()
-    asyncio.run(tcp_client())
diff --git a/tests/multi_net/uasyncio_tcp_readexactly.py b/tests/multi_net/uasyncio_tcp_readexactly.py
deleted file mode 100644
index 71d8c6d0e93..00000000000
--- a/tests/multi_net/uasyncio_tcp_readexactly.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# Test uasyncio stream readexactly() method using TCP server/client
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-PORT = 8000
-
-
-async def handle_connection(reader, writer):
-    writer.write(b"a")
-    await writer.drain()
-
-    # Split the first 2 bytes up so the client must wait for the second one
-    await asyncio.sleep(0.1)
-
-    writer.write(b"b")
-    await writer.drain()
-
-    writer.write(b"c")
-    await writer.drain()
-
-    writer.write(b"d")
-    await writer.drain()
-
-    print("close")
-    writer.close()
-    await writer.wait_closed()
-
-    print("done")
-    ev.set()
-
-
-async def tcp_server():
-    global ev
-    ev = asyncio.Event()
-    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT)
-    print("server running")
-    multitest.next()
-    async with server:
-        await asyncio.wait_for(ev.wait(), 10)
-
-
-async def tcp_client():
-    reader, writer = await asyncio.open_connection(IP, PORT)
-    print(await reader.readexactly(2))
-    print(await reader.readexactly(0))
-    print(await reader.readexactly(1))
-    try:
-        print(await reader.readexactly(2))
-    except EOFError as er:
-        print("EOFError")
-    print(await reader.readexactly(0))
-
-
-def instance0():
-    multitest.globals(IP=multitest.get_network_ip())
-    asyncio.run(tcp_server())
-
-
-def instance1():
-    multitest.next()
-    asyncio.run(tcp_client())
diff --git a/tests/multi_net/uasyncio_tcp_readinto.py b/tests/multi_net/uasyncio_tcp_readinto.py
deleted file mode 100644
index 631997652aa..00000000000
--- a/tests/multi_net/uasyncio_tcp_readinto.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# Test uasyncio stream readinto() method using TCP server/client
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-try:
-    import uarray as array
-except ImportError:
-    try:
-        import array
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-PORT = 8000
-
-
-async def handle_connection(reader, writer):
-    writer.write(b"ab")
-    await writer.drain()
-
-    writer.write(b"c")
-    await writer.drain()
-
-    print("close")
-    writer.close()
-    await writer.wait_closed()
-
-    print("done")
-    ev.set()
-
-
-async def tcp_server():
-    global ev
-    ev = asyncio.Event()
-    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT)
-    print("server running")
-    multitest.next()
-    async with server:
-        await asyncio.wait_for(ev.wait(), 10)
-
-
-async def tcp_client():
-    reader, writer = await asyncio.open_connection(IP, PORT)
-
-    ba = bytearray(2)
-    n = await reader.readinto(ba)
-    print(n)
-    print(ba[:n])
-
-    a = array.array("b", [0, 0])
-    n = await reader.readinto(a)
-    print(n)
-    print(a[:n])
-
-    try:
-        n = await reader.readinto(5)
-    except TypeError as er:
-        print("TypeError")
-
-    try:
-        n = await reader.readinto()
-    except TypeError as er:
-        print("TypeError")
-
-
-def instance0():
-    multitest.globals(IP=multitest.get_network_ip())
-    asyncio.run(tcp_server())
-
-
-def instance1():
-    multitest.next()
-    asyncio.run(tcp_client())
diff --git a/tests/multi_net/uasyncio_tcp_server_client.py b/tests/multi_net/uasyncio_tcp_server_client.py
deleted file mode 100644
index 6a8cb58de53..00000000000
--- a/tests/multi_net/uasyncio_tcp_server_client.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# Test uasyncio TCP server and client using start_server() and open_connection()
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-PORT = 8000
-
-
-async def handle_connection(reader, writer):
-    # Test that peername exists (but don't check its value, it changes)
-    writer.get_extra_info("peername")
-
-    data = await reader.read(100)
-    print("echo:", data)
-    writer.write(data)
-    await writer.drain()
-
-    print("close")
-    writer.close()
-    await writer.wait_closed()
-
-    print("done")
-    ev.set()
-
-
-async def tcp_server():
-    global ev
-    ev = asyncio.Event()
-    server = await asyncio.start_server(handle_connection, "0.0.0.0", PORT)
-    print("server running")
-    multitest.next()
-    async with server:
-        await asyncio.wait_for(ev.wait(), 10)
-
-
-async def tcp_client(message):
-    reader, writer = await asyncio.open_connection(IP, PORT)
-    print("write:", message)
-    writer.write(message)
-    await writer.drain()
-    data = await reader.read(100)
-    print("read:", data)
-
-
-def instance0():
-    multitest.globals(IP=multitest.get_network_ip())
-    asyncio.run(tcp_server())
-
-
-def instance1():
-    multitest.next()
-    asyncio.run(tcp_client(b"client data"))
diff --git a/tests/multi_net/udp_data_multi.py b/tests/multi_net/udp_data_multi.py
new file mode 100644
index 00000000000..a2ed33d292d
--- /dev/null
+++ b/tests/multi_net/udp_data_multi.py
@@ -0,0 +1,69 @@
+# Test UDP reception when there are multiple incoming UDP packets that need to be
+# queued internally in the TCP/IP stack.
+
+import socket
+
+NUM_NEW_SOCKETS = 4
+NUM_PACKET_BURSTS = 6
+NUM_PACKET_GROUPS = 4
+TOTAL_PACKET_BURSTS = NUM_NEW_SOCKETS * NUM_PACKET_BURSTS
+# The tast passes if more than 75% of packets are received in each group.
+PACKET_RECV_THRESH = 0.75 * TOTAL_PACKET_BURSTS
+PORT = 8000
+
+
+# Server
+def instance0():
+    recv_count = {i: 0 for i in range(NUM_PACKET_GROUPS)}
+    multitest.globals(IP=multitest.get_network_ip())
+    multitest.next()
+    for i in range(NUM_NEW_SOCKETS):
+        print("test socket", i)
+        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        s.bind(socket.getaddrinfo("0.0.0.0", PORT + i)[0][-1])
+        s.settimeout(0.250)
+        multitest.broadcast("server ready")
+        for burst in range(NUM_PACKET_BURSTS):
+            # Wait for all packets to be sent, without receiving any yet.
+            multitest.wait("data sent burst={}".format(burst))
+            # Try to receive all packets (they should be waiting in the queue).
+            for group in range(NUM_PACKET_GROUPS):
+                try:
+                    data, addr = s.recvfrom(1000)
+                except:
+                    continue
+                recv_burst, recv_group = data.split(b":")
+                recv_burst = int(recv_burst)
+                recv_group = int(recv_group)
+                if recv_burst == burst:
+                    recv_count[recv_group] += 1
+            # Inform the client that all data was received.
+            multitest.broadcast("data received burst={}".format(burst))
+        s.close()
+
+    # Check how many packets were received.
+    for group, count in recv_count.items():
+        if count >= PACKET_RECV_THRESH:
+            print("pass group={}".format(group))
+        else:
+            print("fail group={} received={}%".format(group, 100 * count // TOTAL_PACKET_BURSTS))
+
+
+# Client
+def instance1():
+    multitest.next()
+    for i in range(NUM_NEW_SOCKETS):
+        print("test socket", i)
+        ai = socket.getaddrinfo(IP, PORT + i)[0][-1]
+        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        multitest.wait("server ready")
+        for burst in range(NUM_PACKET_BURSTS):
+            # Send a bunch of packets all in a row.
+            for group in range(NUM_PACKET_GROUPS):
+                s.sendto(b"%d:%d" % (burst, group), ai)
+            # Inform the server that the data has been sent.
+            multitest.broadcast("data sent burst={}".format(burst))
+            # Wait for the server to finish receiving.
+            multitest.wait("data received burst={}".format(burst))
+        s.close()
diff --git a/tests/multi_net/udp_data_multi.py.exp b/tests/multi_net/udp_data_multi.py.exp
new file mode 100644
index 00000000000..a74ef42b730
--- /dev/null
+++ b/tests/multi_net/udp_data_multi.py.exp
@@ -0,0 +1,14 @@
+--- instance0 ---
+test socket 0
+test socket 1
+test socket 2
+test socket 3
+pass group=0
+pass group=1
+pass group=2
+pass group=3
+--- instance1 ---
+test socket 0
+test socket 1
+test socket 2
+test socket 3
diff --git a/tests/multi_net/udp_recv_dontwait.py b/tests/multi_net/udp_recv_dontwait.py
new file mode 100644
index 00000000000..2d2ee572d34
--- /dev/null
+++ b/tests/multi_net/udp_recv_dontwait.py
@@ -0,0 +1,59 @@
+# Test UDP recv and recvfrom with MSG_DONTWAIT
+import random
+import socket
+
+try:
+    import errno, time
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+# Server
+def instance0():
+    PORT = 10000 + random.getrandbits(15)
+    multitest.globals(IP=multitest.get_network_ip(), PORT=PORT)
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    multitest.next()
+    begin = time.ticks_ms()
+
+    # do some recvs before instance1 starts, when we know no packet is waiting
+    try:
+        print(s.recvfrom(8, socket.MSG_DONTWAIT))
+    except OSError as e:
+        print(e.errno == errno.EAGAIN)
+    try:
+        print(s.recv(8, socket.MSG_DONTWAIT))
+    except OSError as e:
+        print(e.errno == errno.EAGAIN)
+
+    # the above steps should not have taken any substantial time
+    elapsed = time.ticks_diff(time.ticks_ms(), begin)
+    print(True if elapsed < 50 else elapsed)
+
+    # Now instance1 will send us a UDP packet
+    multitest.broadcast("0-ready")
+    multitest.wait("1-sent")
+
+    for _ in range(10):  # retry if necessary, to allow for network delay
+        time.sleep_ms(100)
+        try:
+            print(s.recv(8, socket.MSG_DONTWAIT))
+            break
+        except OSError as er:
+            if er.errno != errno.EAGAIN:
+                raise er
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    multitest.wait("0-ready")
+    print(s.send(b"abcdefgh"))
+    multitest.broadcast("1-sent")
+    s.close()
diff --git a/tests/multi_net/udp_recv_dontwait.py.exp b/tests/multi_net/udp_recv_dontwait.py.exp
new file mode 100644
index 00000000000..f61fd4bbe2e
--- /dev/null
+++ b/tests/multi_net/udp_recv_dontwait.py.exp
@@ -0,0 +1,7 @@
+--- instance0 ---
+True
+True
+True
+b'abcdefgh'
+--- instance1 ---
+8
diff --git a/tests/multi_net/udp_recv_peek.py b/tests/multi_net/udp_recv_peek.py
new file mode 100644
index 00000000000..e8a5f112919
--- /dev/null
+++ b/tests/multi_net/udp_recv_peek.py
@@ -0,0 +1,36 @@
+# Test UDP recv and recvfrom with MSG_PEEK
+import random
+import socket
+import time
+
+
+# Server
+def instance0():
+    PORT = 10000 + random.getrandbits(15)
+    multitest.globals(IP=multitest.get_network_ip(), PORT=PORT)
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1])
+    multitest.next()
+    peek_bytes, peek_addr = s.recvfrom(8, socket.MSG_PEEK)
+    print(peek_bytes)
+    real_bytes, real_addr = s.recvfrom(8)
+    print(real_bytes)
+    print(peek_addr == real_addr)  # source addr should be the same for each
+    res = s.sendto(b"1234567890", peek_addr)
+    print(res)
+    print(s.recv(5, socket.MSG_PEEK))
+    print(s.recv(5, socket.MSG_PEEK))
+    s.close()
+
+
+# Client
+def instance1():
+    multitest.next()
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    s.connect(socket.getaddrinfo(IP, PORT)[0][-1])
+    s.send(b"abcdefgh")
+    s.send(b"klmnopqr")
+    print(s.recv(5, socket.MSG_PEEK))
+    print(s.recv(10))
+    s.close()
diff --git a/tests/multi_pyb_can/rx_callback.py b/tests/multi_pyb_can/rx_callback.py
new file mode 100644
index 00000000000..960a225c93f
--- /dev/null
+++ b/tests/multi_pyb_can/rx_callback.py
@@ -0,0 +1,98 @@
+from pyb import CAN
+import time
+import errno
+
+# Test the various receive IRQs, including overflow
+
+rx_overflow = False
+
+REASONS = ["received", "full", "overflow"]
+
+# CAN IDs
+ID_SPAM = 0x345  # messages spammed into the receive FIFO
+ID_ACK_OFLOW = 0x055  # message the receiver sends after it's seen an overflow
+ID_AFTER = 0x100  # message the sender sends after the ACK
+
+
+def cb0(bus, reason):
+    global rx_overflow
+    if reason != 0 and not rx_overflow:
+        # exact timing of 'received' callbacks depends on controller type,
+        # so only log the other two
+        print("rx0 reason", REASONS[reason])
+    if reason == 2:
+        rx_overflow = True
+
+
+# Accept all standard IDs on FIFO 0
+def _enable_accept_all():
+    if hasattr(CAN, "MASK"):  # FD-CAN controller
+        can.setfilter(0, CAN.RANGE, 0, (0x0, 0x7FF), extframe=False)
+    else:
+        can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0), extframe=False)
+
+
+# Receiver
+def instance0():
+    _enable_accept_all()
+    can.rxcallback(0, cb0)
+
+    multitest.next()
+    multitest.wait("sender ready")
+    multitest.broadcast("receiver ready")
+
+    while not rx_overflow:
+        pass  # Resume ASAP after FIFO0 overflows
+
+    can.send(b"overflow", ID_ACK_OFLOW)
+
+    # drain the receive FIFO, making sure we read at least on ID_SPAM message
+    rxed_spam = False
+    while can.any(0):
+        msg = can.recv(0, timeout=0)
+        assert msg[0] == ID_SPAM
+        rxed_spam = True
+    print("rxed_spam", rxed_spam)
+
+    # This should be the one message with ID_AFTER, there may be one or two spam messages as well
+    for _ in range(10):
+        msg = can.recv(0, timeout=500)
+        if msg[0] == ID_AFTER:
+            print(msg)
+            break
+
+    # RX FIFO should be empty now
+    print("any", can.any(0))
+
+
+# Sender
+def instance1():
+    _enable_accept_all()
+    multitest.next()
+    multitest.broadcast("sender ready")
+    multitest.wait("receiver ready")
+
+    # Spam out messages until the receiver tells us its RX FIFO is full.
+    #
+    # The RX FIFO on the receiver can vary from 3 deep (BXCAN) to 25 deep (STM32H7),
+    # so we keep sending to it until we see a CAN message on ID_ACK_OFLOW indicating
+    # the receiver's FIFO has overflowed
+    for i in range(255):
+        can.send(bytes([i] * 8), ID_SPAM, timeout=25)
+        if can.any(0):
+            print(can.recv(0))  # should be ID_ACK_OFLOW
+            break
+        # on boards like STM32H7 the TX FIFO is really deep, so don't fill it too quickly...
+        time.sleep_ms(1)
+
+    # give the receiver some time to make space in the FIFO
+    time.sleep_ms(200)
+
+    # send the final message, the receiver should get this one
+    can.send(b"aaaaa", ID_AFTER)
+
+    # Sender's RX FIFO should also be empty at this point
+    print("any", can.any(0))
+
+
+can = CAN(1, CAN.NORMAL, baudrate=500_000, sample_point=75)
diff --git a/tests/multi_pyb_can/rx_callback.py.exp b/tests/multi_pyb_can/rx_callback.py.exp
new file mode 100644
index 00000000000..2126a3fccb0
--- /dev/null
+++ b/tests/multi_pyb_can/rx_callback.py.exp
@@ -0,0 +1,9 @@
+--- instance0 ---
+rx0 reason full
+rx0 reason overflow
+rxed_spam True
+[256, False, False, 0, b'aaaaa']
+any False
+--- instance1 ---
+[85, False, False, 0, b'overflow']
+any False
diff --git a/tests/multi_pyb_can/rx_filters.py b/tests/multi_pyb_can/rx_filters.py
new file mode 100644
index 00000000000..22e5eb03647
--- /dev/null
+++ b/tests/multi_pyb_can/rx_filters.py
@@ -0,0 +1,50 @@
+from pyb import CAN
+import time
+import errno
+
+# Test for the filtering capabilities for RX FIFO 0 and 1.
+
+
+# Receiver
+def instance0():
+    # Configure to receive standard frames (in a range) on FIFO 0
+    # and extended frames (in a range) on FIFO 1.
+    if hasattr(CAN, "MASK"):
+        # FD-CAN has independent filter banks for standard and extended IDs
+        can.setfilter(0, CAN.MASK, 0, (0x300, 0x700), extframe=False)
+        can.setfilter(0, CAN.MASK, 1, (0x3000, 0x7000), extframe=True)
+    else:
+        # pyb.CAN only supports MASK32 for extended ids
+        can.setfilter(0, CAN.MASK16, 0, (0x300, 0x700, 0x300, 0x700), extframe=False)
+        can.setfilter(1, CAN.MASK32, 1, (0x3000, 0x7000), extframe=True)
+
+    multitest.next()
+    multitest.wait("sender ready")
+    multitest.broadcast("receiver ready")
+    for i in range(3):
+        print(i)
+        print("fifo0", can.recv(0, timeout=200))
+        print("fifo1", can.recv(1, timeout=200))
+
+    try:
+        can.recv(0, timeout=100)  # should time out
+    except OSError as e:
+        assert e.errno == errno.ETIMEDOUT
+        print("Timed out as expected")
+
+
+# Sender
+def instance1():
+    multitest.next()
+    multitest.broadcast("sender ready")
+    multitest.wait("receiver ready")
+
+    for i in range(3):
+        print(i)
+        can.send(bytes([i, 3] * i), 0x345)
+        can.send(bytes([0xEE] * i), 0x3700 + i, extframe=True)
+        can.send(b"abcdef", 0x123)  # matches no filter, expect ACKed but not received
+        time.sleep_ms(5)  # avoid flooding either our or the receiver's FIFO
+
+
+can = CAN(1, CAN.NORMAL, baudrate=500_000, sample_point=75)
diff --git a/tests/multi_pyb_can/rx_filters.py.exp b/tests/multi_pyb_can/rx_filters.py.exp
new file mode 100644
index 00000000000..8016f641524
--- /dev/null
+++ b/tests/multi_pyb_can/rx_filters.py.exp
@@ -0,0 +1,15 @@
+--- instance0 ---
+0
+fifo0 [837, False, False, 0, b'']
+fifo1 [14080, True, False, 0, b'']
+1
+fifo0 [837, False, False, 0, b'\x01\x03']
+fifo1 [14081, True, False, 0, b'\xee']
+2
+fifo0 [837, False, False, 0, b'\x02\x03\x02\x03']
+fifo1 [14082, True, False, 0, b'\xee\xee']
+Timed out as expected
+--- instance1 ---
+0
+1
+2
diff --git a/tests/multi_wlan/01_ap_sta.py b/tests/multi_wlan/01_ap_sta.py
new file mode 100644
index 00000000000..368addf1393
--- /dev/null
+++ b/tests/multi_wlan/01_ap_sta.py
@@ -0,0 +1,109 @@
+# Basic Wi-Fi MAC layer test where one device creates an Access Point and the
+# other device connects to it as a Station. Also tests channel assignment (where
+# possible) and disconnection.
+
+try:
+    import network
+
+    network.WLAN
+except (ImportError, NameError):
+    print("SKIP")
+    raise SystemExit
+
+import os
+import sys
+import time
+
+CHANNEL = 8
+
+# Note that on slower Wi-Fi stacks this bumps up against the run-multitests.py
+# timeout which expects <10s between lines of output. We work around this by
+# logging something half way through the wait_for loop...
+CONNECT_TIMEOUT = 15000
+
+
+def wait_for(test_func):
+    has_printed = False
+    start = time.ticks_ms()
+    while not test_func():
+        time.sleep(0.1)
+        delta = time.ticks_diff(time.ticks_ms(), start)
+        if not has_printed and delta > CONNECT_TIMEOUT / 2:
+            print("...")
+            has_printed = True
+        elif delta > CONNECT_TIMEOUT:
+            break
+
+    if not has_printed:
+        print("...")  # keep the output consistent
+
+    return test_func()
+
+
+# AP
+def instance0():
+    ap = network.WLAN(network.WLAN.IF_AP)
+    ssid = "MP-test-" + os.urandom(6).hex()
+    psk = "Secret-" + os.urandom(6).hex()
+
+    # stop any previous activity
+    network.WLAN(network.WLAN.IF_STA).active(False)
+    ap.active(False)
+
+    ap.active(True)
+    ap.config(ssid=ssid, key=psk, channel=CHANNEL, security=network.WLAN.SEC_WPA_WPA2)
+
+    # print("AP setup", ssid, psk)
+    print("AP started")
+
+    multitest.globals(SSID=ssid, PSK=psk)
+    multitest.next()
+
+    # Wait for station
+    if not wait_for(ap.isconnected):
+        raise RuntimeError("Timed out waiting for station, status ", ap.status())
+
+    print("AP got station")
+    time.sleep(
+        3
+    )  # depending on port, may still need to negotiate DHCP lease for STA to see connection
+
+    print("AP disabling...")
+    ap.active(False)
+
+
+# STA
+def instance1():
+    sta = network.WLAN(network.WLAN.IF_STA)
+
+    # stop any previous activity
+    network.WLAN(network.WLAN.IF_AP).active(False)
+    sta.active(False)
+
+    multitest.next()
+    ssid = SSID
+    psk = PSK
+
+    # print("STA setup", ssid, psk)
+
+    sta.active(True)
+    sta.connect(ssid, psk)
+
+    print("STA connecting...")
+
+    if not wait_for(sta.isconnected):
+        raise RuntimeError("Timed out waiting to connect, status ", sta.status())
+
+    print("STA connected")
+
+    print("channel", sta.config("channel"))
+
+    print("STA waiting for disconnect...")
+
+    # Expect the AP to disconnect us immediately
+    if not wait_for(lambda: not sta.isconnected()):
+        raise RuntimeError("Timed out waiting for AP to disconnect us, status ", sta.status())
+
+    print("STA disconnected")
+
+    sta.active(False)
diff --git a/tests/multi_wlan/01_ap_sta.py.exp b/tests/multi_wlan/01_ap_sta.py.exp
new file mode 100644
index 00000000000..8fc023a3980
--- /dev/null
+++ b/tests/multi_wlan/01_ap_sta.py.exp
@@ -0,0 +1,13 @@
+--- instance0 ---
+AP started
+...
+AP got station
+AP disabling...
+--- instance1 ---
+STA connecting...
+...
+STA connected
+channel 8
+STA waiting for disconnect...
+...
+STA disconnected
diff --git a/tests/multi_wlan/getaddrinfo.py b/tests/multi_wlan/getaddrinfo.py
new file mode 100644
index 00000000000..8ad6155ed0e
--- /dev/null
+++ b/tests/multi_wlan/getaddrinfo.py
@@ -0,0 +1,42 @@
+# This is a regression test to ensure getaddrinfo() fails (after a timeout)
+# when Wi-Fi is disconnected.
+#
+# It doesn't require multiple instances, but it does require Wi-Fi to be present
+# but not active, and for no other network interface to be configured. The only
+# tests which already meet these conditions is here in multi_wlan tests.
+try:
+    from network import WLAN
+except (ImportError, NameError):
+    print("SKIP")
+    raise SystemExit
+
+import socket
+
+
+def instance0():
+    WLAN(WLAN.IF_AP).active(0)
+    wlan = WLAN(WLAN.IF_STA)
+    wlan.active(0)
+
+    multitest.next()
+
+    # Note: Lookup domain for this test doesn't need to exist, as the board
+    # isn't internet connected. It also shouldn't exist, so a cached result is
+    # never returned!
+    try:
+        socket.getaddrinfo("doesnotexist.example.com", 80)
+    except OSError as er:
+        print(
+            "active(0) failed"
+        )  # This may fail with code -6 or -2 depending on whether WLAN has been active since reset
+
+    wlan.active(1)
+
+    try:
+        socket.getaddrinfo("doesnotexist.example.com", 80)
+    except OSError as er:
+        print(
+            "active(1) failed", er.errno in (-2, -202)
+        )  # This one should always be -2 or -202 depending on port
+
+    wlan.active(0)
diff --git a/tests/multi_wlan/getaddrinfo.py.exp b/tests/multi_wlan/getaddrinfo.py.exp
new file mode 100644
index 00000000000..5616a23c728
--- /dev/null
+++ b/tests/multi_wlan/getaddrinfo.py.exp
@@ -0,0 +1,3 @@
+--- instance0 ---
+active(0) failed
+active(1) failed True
diff --git a/tests/net_hosted/accept_nonblock.py b/tests/net_hosted/accept_nonblock.py
index d17e287498c..30d2033e652 100644
--- a/tests/net_hosted/accept_nonblock.py
+++ b/tests/net_hosted/accept_nonblock.py
@@ -1,9 +1,6 @@
 # test that socket.accept() on a non-blocking socket raises EAGAIN
 
-try:
-    import usocket as socket
-except:
-    import socket
+import socket
 
 s = socket.socket()
 s.bind(socket.getaddrinfo("127.0.0.1", 8123)[0][-1])
diff --git a/tests/net_hosted/accept_timeout.py b/tests/net_hosted/accept_timeout.py
index 734fe217cad..865d2aad26f 100644
--- a/tests/net_hosted/accept_timeout.py
+++ b/tests/net_hosted/accept_timeout.py
@@ -1,9 +1,6 @@
 # test that socket.accept() on a socket with timeout raises ETIMEDOUT
 
-try:
-    import uerrno as errno, usocket as socket
-except:
-    import errno, socket
+import errno, socket
 
 try:
     socket.socket.settimeout
diff --git a/tests/net_hosted/asyncio_loopback.py b/tests/net_hosted/asyncio_loopback.py
new file mode 100644
index 00000000000..03513ae6244
--- /dev/null
+++ b/tests/net_hosted/asyncio_loopback.py
@@ -0,0 +1,71 @@
+# Test network loopback behaviour
+
+import sys
+
+# Only certain platforms can do TCP/IP loopback.
+if sys.platform not in ("darwin", "esp32", "linux"):
+    print("SKIP")
+    raise SystemExit
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def client(host, port):
+    print(f"client open_connection to {host}:{port}")
+    reader, writer = await asyncio.open_connection(host, port)
+
+    data_in = b"A" * 100
+
+    print("client writing")
+    writer.write(data_in)
+    await writer.drain()
+
+    await asyncio.sleep(0.1)
+
+    print("client reading")
+    data = await reader.readexactly(100)
+    print(f"client got {len(data)} bytes")
+
+    assert data_in == data
+
+    print("client closing")
+
+    writer.close()
+    await writer.wait_closed()
+
+    print("client closed")
+
+
+async def echo_handler(reader, writer):
+    print("handler reading")
+    await asyncio.sleep(0.1)
+    data = await reader.readexactly(100)
+    print(f"handler got {len(data)} bytes")
+
+    print("handler writing")
+    writer.write(data)
+    await writer.drain()
+
+    print("handler closing")
+
+    writer.close()
+    await writer.wait_closed()
+
+    print("handler closed")
+
+
+async def test(host, port):
+    print(f"create server on {host}:{port}")
+    server = await asyncio.start_server(echo_handler, host, port)
+
+    async with server:
+        print("server started")
+        await client("127.0.0.1", 8080)
+    print("server closed")
+
+
+asyncio.run(test("0.0.0.0", 8080))
diff --git a/tests/net_hosted/asyncio_loopback.py.exp b/tests/net_hosted/asyncio_loopback.py.exp
new file mode 100644
index 00000000000..7110b35e12c
--- /dev/null
+++ b/tests/net_hosted/asyncio_loopback.py.exp
@@ -0,0 +1,14 @@
+create server on 0.0.0.0:8080
+server started
+client open_connection to 127.0.0.1:8080
+client writing
+handler reading
+client reading
+handler got 100 bytes
+handler writing
+handler closing
+handler closed
+client got 100 bytes
+client closing
+client closed
+server closed
diff --git a/tests/net_hosted/asyncio_start_server.py b/tests/net_hosted/asyncio_start_server.py
new file mode 100644
index 00000000000..e76faf7edb6
--- /dev/null
+++ b/tests/net_hosted/asyncio_start_server.py
@@ -0,0 +1,66 @@
+# Test basic behaviour of asyncio.start_server()
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def test():
+    # Test creating 2 servers using the same address
+    print("create server1")
+    server1 = await asyncio.start_server(None, "0.0.0.0", 8000)
+    try:
+        print("create server2")
+        await asyncio.start_server(None, "0.0.0.0", 8000)
+    except OSError as er:
+        print("OSError")
+
+    # Wait for server to close.
+    async with server1:
+        print("sleep")
+        await asyncio.sleep(0)
+
+    # Test that cancellation works before the server starts if
+    # the subsequent code raises.
+    print("create server3")
+    server3 = await asyncio.start_server(None, "0.0.0.0", 8000)
+    try:
+        async with server3:
+            raise OSError
+    except OSError as er:
+        print("OSError")
+
+    # Test that closing doesn't raise CancelledError.
+    print("create server4")
+    server4 = await asyncio.start_server(None, "0.0.0.0", 8000)
+    server4.close()
+    await server4.wait_closed()
+    print("server4 closed")
+
+    # Test that cancelling the task will still raise CancelledError, checking
+    # edge cases around how many times the tasks have been re-scheduled by
+    # sleep.
+    async def task(n):
+        print("create task server", n)
+        srv = await asyncio.start_server(None, "0.0.0.0", 8000)
+        await srv.wait_closed()
+        # This should be unreachable.
+        print("task finished")
+
+    for num_sleep in range(0, 5):
+        print("sleep", num_sleep)
+        t = asyncio.create_task(task(num_sleep))
+        for _ in range(num_sleep):
+            await asyncio.sleep(0)
+        t.cancel()
+        try:
+            await t
+        except asyncio.CancelledError:
+            print("CancelledError")
+
+    print("done")
+
+
+asyncio.run(test())
diff --git a/tests/net_hosted/asyncio_start_server.py.exp b/tests/net_hosted/asyncio_start_server.py.exp
new file mode 100644
index 00000000000..58982a108ce
--- /dev/null
+++ b/tests/net_hosted/asyncio_start_server.py.exp
@@ -0,0 +1,23 @@
+create server1
+create server2
+OSError
+sleep
+create server3
+OSError
+create server4
+server4 closed
+sleep 0
+CancelledError
+sleep 1
+create task server 1
+CancelledError
+sleep 2
+create task server 2
+CancelledError
+sleep 3
+create task server 3
+CancelledError
+sleep 4
+create task server 4
+CancelledError
+done
diff --git a/tests/net_hosted/connect_nonblock.py b/tests/net_hosted/connect_nonblock.py
index 4b8055c1615..781f1a4ee2c 100644
--- a/tests/net_hosted/connect_nonblock.py
+++ b/tests/net_hosted/connect_nonblock.py
@@ -1,10 +1,6 @@
 # test that socket.connect() on a non-blocking socket raises EINPROGRESS
 
-try:
-    import usocket as socket
-    import uerrno as errno
-except:
-    import socket, errno
+import socket, errno
 
 
 def test(peer_addr):
diff --git a/tests/net_hosted/connect_nonblock_xfer.py b/tests/net_hosted/connect_nonblock_xfer.py
index 1a0b2422765..2c8e12473fe 100644
--- a/tests/net_hosted/connect_nonblock_xfer.py
+++ b/tests/net_hosted/connect_nonblock_xfer.py
@@ -1,147 +1,135 @@
 # test that socket.connect() on a non-blocking socket raises EINPROGRESS
 # and that an immediate write/send/read/recv does the right thing
 
-try:
-    import sys, time
-    import uerrno as errno, usocket as socket, ussl as ssl
-except:
-    import socket, errno, ssl
-isMP = sys.implementation.name == "micropython"
+import unittest
+import errno
+import select
+import socket
+import ssl
 
+# only mbedTLS supports non-blocking mode
+ssl_supports_nonblocking = hasattr(ssl, "MBEDTLS_VERSION")
 
-def dp(e):
-    # uncomment next line for development and testing, to print the actual exceptions
-    # print(repr(e))
-    pass
+
+# get the name of an errno error code
+def errno_name(er):
+    if er == errno.EAGAIN:
+        return "EAGAIN"
+    if er == errno.EINPROGRESS:
+        return "EINPROGRESS"
+    return er
 
 
 # do_connect establishes the socket and wraps it if tls is True.
 # If handshake is true, the initial connect (and TLS handshake) is
 # allowed to be performed before returning.
-def do_connect(peer_addr, tls, handshake):
+def do_connect(self, peer_addr, tls, handshake):
     s = socket.socket()
     s.setblocking(False)
     try:
-        # print("Connecting to", peer_addr)
+        print("Connecting to", peer_addr)
         s.connect(peer_addr)
+        self.fail()
     except OSError as er:
-        print("connect:", er.errno == errno.EINPROGRESS)
-        if er.errno != errno.EINPROGRESS:
-            print("  got", er.errno)
+        print("connect:", errno_name(er.errno))
+        self.assertEqual(er.errno, errno.EINPROGRESS)
+
     # wrap with ssl/tls if desired
     if tls:
-        try:
-            if sys.implementation.name == "micropython":
-                s = ssl.wrap_socket(s, do_handshake=handshake)
-            else:
-                s = ssl.wrap_socket(s, do_handshake_on_connect=handshake)
-            print("wrap: True")
-        except Exception as e:
-            dp(e)
-            print("wrap:", e)
-    elif handshake:
-        # just sleep a little bit, this allows any connect() errors to happen
-        time.sleep(0.2)
+        print("wrap socket")
+        ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+        s = ssl_context.wrap_socket(s, do_handshake_on_connect=handshake)
+
     return s
 
 
-# test runs the test against a specific peer address.
-def test(peer_addr, tls=False, handshake=False):
-    # MicroPython plain sockets have read/write, but CPython's don't
-    # MicroPython TLS sockets and CPython's have read/write
-    # hasRW captures this wonderful state of affairs
-    hasRW = isMP or tls
+# poll a socket and check the result
+def poll(self, s, expect_writable):
+    poller = select.poll()
+    poller.register(s)
+    result = poller.poll(0)
+    print("poll:", result)
+    if expect_writable:
+        self.assertEqual(len(result), 1)
+        self.assertEqual(result[0][1], select.POLLOUT)
+    else:
+        self.assertEqual(result, [])
+
 
-    # MicroPython plain sockets and CPython's have send/recv
-    # MicroPython TLS sockets don't have send/recv, but CPython's do
-    # hasSR captures this wonderful state of affairs
-    hasSR = not (isMP and tls)
+# do_test runs the test against a specific peer address.
+def do_test(self, peer_addr, tls, handshake):
+    print()
+
+    # MicroPython plain and TLS sockets have read/write
+    hasRW = True
+
+    # MicroPython plain sockets have send/recv
+    # MicroPython TLS sockets don't have send/recv
+    hasSR = not tls
 
     # connect + send
+    # non-blocking send should raise EAGAIN
     if hasSR:
-        s = do_connect(peer_addr, tls, handshake)
-        # send -> 4 or EAGAIN
-        try:
+        s = do_connect(self, peer_addr, tls, handshake)
+        poll(self, s, False)
+        with self.assertRaises(OSError) as ctx:
             ret = s.send(b"1234")
-            print("send:", handshake and ret == 4)
-        except OSError as er:
-            #
-            dp(er)
-            print("send:", er.errno in (errno.EAGAIN, errno.EINPROGRESS))
+        print("send error:", errno_name(ctx.exception.errno))
+        self.assertEqual(ctx.exception.errno, errno.EAGAIN)
         s.close()
-    else:  # fake it...
-        print("connect:", True)
-        if tls:
-            print("wrap:", True)
-        print("send:", True)
 
     # connect + write
+    # non-blocking write should return None
     if hasRW:
-        s = do_connect(peer_addr, tls, handshake)
-        # write -> None
-        try:
-            ret = s.write(b"1234")
-            print("write:", ret in (4, None))  # SSL may accept 4 into buffer
-        except OSError as er:
-            dp(er)
-            print("write:", False)  # should not raise
-        except ValueError as er:  # CPython
-            dp(er)
-            print("write:", er.args[0] == "Write on closed or unwrapped SSL socket.")
+        s = do_connect(self, peer_addr, tls, handshake)
+        poll(self, s, tls and handshake)
+        ret = s.write(b"1234")
+        print("write:", ret)
+        if tls and handshake:
+            self.assertEqual(ret, 4)
+        else:
+            self.assertIsNone(ret)
         s.close()
-    else:  # fake it...
-        print("connect:", True)
-        if tls:
-            print("wrap:", True)
-        print("write:", True)
 
+    # connect + recv
+    # non-blocking recv should raise EAGAIN
     if hasSR:
-        # connect + recv
-        s = do_connect(peer_addr, tls, handshake)
-        # recv -> EAGAIN
-        try:
-            print("recv:", s.recv(10))
-        except OSError as er:
-            dp(er)
-            print("recv:", er.errno == errno.EAGAIN)
+        s = do_connect(self, peer_addr, tls, handshake)
+        poll(self, s, False)
+        with self.assertRaises(OSError) as ctx:
+            ret = s.recv(10)
+        print("recv error:", errno_name(ctx.exception.errno))
+        self.assertEqual(ctx.exception.errno, errno.EAGAIN)
         s.close()
-    else:  # fake it...
-        print("connect:", True)
-        if tls:
-            print("wrap:", True)
-        print("recv:", True)
 
     # connect + read
+    # non-blocking read should return None
     if hasRW:
-        s = do_connect(peer_addr, tls, handshake)
-        # read -> None
-        try:
-            ret = s.read(10)
-            print("read:", ret is None)
-        except OSError as er:
-            dp(er)
-            print("read:", False)  # should not raise
-        except ValueError as er:  # CPython
-            dp(er)
-            print("read:", er.args[0] == "Read on closed or unwrapped SSL socket.")
+        s = do_connect(self, peer_addr, tls, handshake)
+        poll(self, s, tls and handshake)
+        ret = s.read(10)
+        print("read:", ret)
+        self.assertIsNone(ret)
         s.close()
-    else:  # fake it...
-        print("connect:", True)
-        if tls:
-            print("wrap:", True)
-        print("read:", True)
 
 
-if __name__ == "__main__":
+class Test(unittest.TestCase):
     # these tests use a non-existent test IP address, this way the connect takes forever and
     # we can see EAGAIN/None (https://tools.ietf.org/html/rfc5737)
-    print("--- Plain sockets to nowhere ---")
-    test(socket.getaddrinfo("192.0.2.1", 80)[0][-1], False, False)
-    print("--- SSL sockets to nowhere ---")
-    # this test fails with AXTLS because do_handshake=False blocks on first read/write and
-    # there it times out until the connect is aborted
-    test(socket.getaddrinfo("192.0.2.1", 443)[0][-1], True, False)
-    print("--- Plain sockets ---")
-    test(socket.getaddrinfo("micropython.org", 80)[0][-1], False, True)
-    print("--- SSL sockets ---")
-    test(socket.getaddrinfo("micropython.org", 443)[0][-1], True, True)
+    def test_plain_sockets_to_nowhere(self):
+        do_test(self, socket.getaddrinfo("192.0.2.1", 80)[0][-1], False, False)
+
+    @unittest.skipIf(not ssl_supports_nonblocking, "SSL doesn't support non-blocking")
+    def test_ssl_sockets_to_nowhere(self):
+        do_test(self, socket.getaddrinfo("192.0.2.1", 443)[0][-1], True, False)
+
+    def test_plain_sockets(self):
+        do_test(self, socket.getaddrinfo("micropython.org", 80)[0][-1], False, False)
+
+    @unittest.skipIf(not ssl_supports_nonblocking, "SSL doesn't support non-blocking")
+    def test_ssl_sockets(self):
+        do_test(self, socket.getaddrinfo("micropython.org", 443)[0][-1], True, True)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/net_hosted/connect_poll.py b/tests/net_hosted/connect_poll.py
index b2739e36e9d..a3232bd334e 100644
--- a/tests/net_hosted/connect_poll.py
+++ b/tests/net_hosted/connect_poll.py
@@ -1,9 +1,6 @@
 # test that socket.connect() has correct polling behaviour before, during and after
 
-try:
-    import usocket as socket, uselect as select
-except:
-    import socket, select
+import socket, select
 
 
 def test(peer_addr):
diff --git a/tests/net_hosted/connect_timeout.py b/tests/net_hosted/connect_timeout.py
new file mode 100644
index 00000000000..5f35047c8c8
--- /dev/null
+++ b/tests/net_hosted/connect_timeout.py
@@ -0,0 +1,24 @@
+# Test that socket.connect() on a socket with timeout raises EINPROGRESS or ETIMEDOUT appropriately.
+
+import errno
+import socket
+
+
+def test(peer_addr, timeout, expected_exc):
+    s = socket.socket()
+    s.settimeout(timeout)
+    try:
+        s.connect(peer_addr)
+        print("OK")
+    except OSError as er:
+        print(er.args[0] in expected_exc)
+    s.close()
+
+
+if __name__ == "__main__":
+    # This test needs an address that doesn't respond to TCP connections.
+    # 1.1.1.1:8000 seem to reliably timeout, so use that.
+    addr = socket.getaddrinfo("1.1.1.1", 8000)[0][-1]
+
+    test(addr, 0, (errno.EINPROGRESS,))
+    test(addr, 1, (errno.ETIMEDOUT, "timed out"))  # CPython uses a string instead of errno
diff --git a/tests/net_hosted/ssl_getpeercert.py b/tests/net_hosted/ssl_getpeercert.py
index dee5fcfd899..e0a1350fa73 100644
--- a/tests/net_hosted/ssl_getpeercert.py
+++ b/tests/net_hosted/ssl_getpeercert.py
@@ -1,11 +1,14 @@
 # test ssl.getpeercert() method
 
-try:
-    import usocket as socket
-    import ussl as ssl
-except:
-    import socket
-    import ssl
+import io
+import socket
+import ssl
+
+s_test = ssl.wrap_socket(io.BytesIO(), server_side=True, do_handshake=False)
+s_test.close()
+if not hasattr(s_test, "getpeercert"):
+    print("SKIP")
+    raise SystemExit
 
 
 def test(peer_addr):
diff --git a/tests/net_hosted/ssl_verify_callback.py b/tests/net_hosted/ssl_verify_callback.py
new file mode 100644
index 00000000000..cd03ff49d77
--- /dev/null
+++ b/tests/net_hosted/ssl_verify_callback.py
@@ -0,0 +1,42 @@
+# test ssl verify_callback
+
+import io
+import socket
+import tls
+
+context = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+
+if not hasattr(context, "verify_callback"):
+    print("SKIP")
+    raise SystemExit
+
+
+def verify_callback(cert, depth):
+    print("verify_callback:", type(cert), len(cert) > 100, depth)
+    return 0
+
+
+def verify_callback_fail(cert, depth):
+    print("verify_callback_fail:", type(cert), len(cert) > 100, depth)
+    return 1
+
+
+def test(peer_addr):
+    context.verify_mode = tls.CERT_OPTIONAL
+    context.verify_callback = verify_callback
+    s = socket.socket()
+    s.connect(peer_addr)
+    s = context.wrap_socket(s)
+    s.close()
+
+    context.verify_callback = verify_callback_fail
+    s = socket.socket()
+    s.connect(peer_addr)
+    try:
+        s = context.wrap_socket(s)
+    except OSError as e:
+        print(e.args[1])
+
+
+if __name__ == "__main__":
+    test(socket.getaddrinfo("micropython.org", 443)[0][-1])
diff --git a/tests/net_hosted/ssl_verify_callback.py.exp b/tests/net_hosted/ssl_verify_callback.py.exp
new file mode 100644
index 00000000000..e4564ce5aba
--- /dev/null
+++ b/tests/net_hosted/ssl_verify_callback.py.exp
@@ -0,0 +1,4 @@
+verify_callback:  True 1
+verify_callback:  True 0
+verify_callback_fail:  True 1
+MBEDTLS_ERR_ERROR_GENERIC_ERROR
diff --git a/tests/net_hosted/uasyncio_start_server.py b/tests/net_hosted/uasyncio_start_server.py
deleted file mode 100644
index f13402979f8..00000000000
--- a/tests/net_hosted/uasyncio_start_server.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Test basic behaviour of uasyncio.start_server()
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def test():
-    # Test creating 2 servers using the same address
-    print("create server1")
-    server1 = await asyncio.start_server(None, "0.0.0.0", 8000)
-    try:
-        print("create server2")
-        await asyncio.start_server(None, "0.0.0.0", 8000)
-    except OSError as er:
-        print("OSError")
-
-    # Wait for server to close.
-    async with server1:
-        print("sleep")
-        await asyncio.sleep(0)
-
-    print("done")
-
-
-asyncio.run(test())
diff --git a/tests/net_hosted/uasyncio_start_server.py.exp b/tests/net_hosted/uasyncio_start_server.py.exp
deleted file mode 100644
index 0fb8e6a63b0..00000000000
--- a/tests/net_hosted/uasyncio_start_server.py.exp
+++ /dev/null
@@ -1,5 +0,0 @@
-create server1
-create server2
-OSError
-sleep
-done
diff --git a/tests/net_inet/asyncio_cancel_stream.py b/tests/net_inet/asyncio_cancel_stream.py
new file mode 100644
index 00000000000..a192736576a
--- /dev/null
+++ b/tests/net_inet/asyncio_cancel_stream.py
@@ -0,0 +1,32 @@
+# Test cancelling a task waiting on stream IO
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def get(reader):
+    print("start")
+    try:
+        await reader.read(10)
+        print("fail")
+    except asyncio.CancelledError:
+        print("cancelled")
+
+
+async def main(url):
+    reader, writer = await asyncio.open_connection(url, 80)
+    task = asyncio.create_task(get(reader))
+    await asyncio.sleep(0)
+    print("cancelling")
+    task.cancel()
+    print("waiting")
+    await task
+    print("done")
+    writer.close()
+    await writer.wait_closed()
+
+
+asyncio.run(main("micropython.org"))
diff --git a/tests/net_inet/uasyncio_cancel_stream.py.exp b/tests/net_inet/asyncio_cancel_stream.py.exp
similarity index 100%
rename from tests/net_inet/uasyncio_cancel_stream.py.exp
rename to tests/net_inet/asyncio_cancel_stream.py.exp
diff --git a/tests/net_inet/asyncio_open_connection.py b/tests/net_inet/asyncio_open_connection.py
new file mode 100644
index 00000000000..505f3c16bfa
--- /dev/null
+++ b/tests/net_inet/asyncio_open_connection.py
@@ -0,0 +1,27 @@
+# Test simple HTTP request with asyncio.open_connection()
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def http_get(url):
+    reader, writer = await asyncio.open_connection(url, 80)
+
+    print("write GET")
+    writer.write(b"GET / HTTP/1.0\r\n\r\n")
+    await writer.drain()
+
+    print("read response")
+    data = await reader.read(100)
+    print("read:", data.split(b"\r\n")[0])
+
+    print("close")
+    writer.close()
+    await writer.wait_closed()
+    print("done")
+
+
+asyncio.run(http_get("micropython.org"))
diff --git a/tests/net_inet/uasyncio_open_connection.py.exp b/tests/net_inet/asyncio_open_connection.py.exp
similarity index 100%
rename from tests/net_inet/uasyncio_open_connection.py.exp
rename to tests/net_inet/asyncio_open_connection.py.exp
diff --git a/tests/net_inet/asyncio_tcp_read_headers.py b/tests/net_inet/asyncio_tcp_read_headers.py
new file mode 100644
index 00000000000..ec334d8bf48
--- /dev/null
+++ b/tests/net_inet/asyncio_tcp_read_headers.py
@@ -0,0 +1,35 @@
+# Test asyncio.open_connection() and stream readline()
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def http_get_headers(url):
+    reader, writer = await asyncio.open_connection(url, 80)
+
+    print("write GET")
+    writer.write(b"GET / HTTP/1.0\r\n\r\n")
+    await writer.drain()
+
+    while True:
+        line = await reader.readline()
+        line = line.strip()
+        if not line:
+            break
+        if (
+            line.find(b"Date") == -1
+            and line.find(b"Modified") == -1
+            and line.find(b"Server") == -1
+        ):
+            print(line)
+
+    print("close")
+    writer.close()
+    await writer.wait_closed()
+    print("done")
+
+
+asyncio.run(http_get_headers("micropython.org"))
diff --git a/tests/net_inet/uasyncio_tcp_read_headers.py.exp b/tests/net_inet/asyncio_tcp_read_headers.py.exp
similarity index 100%
rename from tests/net_inet/uasyncio_tcp_read_headers.py.exp
rename to tests/net_inet/asyncio_tcp_read_headers.py.exp
diff --git a/tests/net_inet/asyncio_tls_open_connection_readline.py b/tests/net_inet/asyncio_tls_open_connection_readline.py
new file mode 100644
index 00000000000..53b7705d27a
--- /dev/null
+++ b/tests/net_inet/asyncio_tls_open_connection_readline.py
@@ -0,0 +1,42 @@
+import ssl
+import os
+import asyncio
+
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+# For details about this cert, see comment in test_sslcontext_client.py
+root_cert_path = "isrgrootx1.der"
+
+with open(root_cert_path, "rb") as ca:
+    cadata = ca.read()
+
+client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+client_ctx.verify_mode = ssl.CERT_REQUIRED
+client_ctx.load_verify_locations(cadata=cadata)
+
+
+async def http_get(url, port, sslctx):
+    reader, writer = await asyncio.open_connection(url, port, ssl=sslctx)
+
+    print("write GET")
+    writer.write(b"GET / HTTP/1.0\r\n\r\n")
+    await writer.drain()
+
+    print("read response")
+    while True:
+        data = await reader.readline()
+        # avoid printing datetime which makes the test fail
+        if b"GMT" not in data:
+            print("read:", data)
+        if not data:
+            break
+
+    print("close")
+    writer.close()
+    await writer.wait_closed()
+    print("done")
+
+
+asyncio.run(http_get("micropython.org", 443, client_ctx))
diff --git a/tests/net_inet/getaddrinfo.py b/tests/net_inet/getaddrinfo.py
index 765723ae73f..e26060ad212 100644
--- a/tests/net_inet/getaddrinfo.py
+++ b/tests/net_inet/getaddrinfo.py
@@ -1,7 +1,4 @@
-try:
-    import usocket as socket, sys
-except:
-    import socket, sys
+import socket, sys
 
 
 def test_non_existent():
diff --git a/tests/net_inet/isrgrootx1.der b/tests/net_inet/isrgrootx1.der
new file mode 100644
index 00000000000..9d2132e7f1e
Binary files /dev/null and b/tests/net_inet/isrgrootx1.der differ
diff --git a/tests/net_inet/isrgrootx1.pem b/tests/net_inet/isrgrootx1.pem
new file mode 100644
index 00000000000..b85c8037f6b
--- /dev/null
+++ b/tests/net_inet/isrgrootx1.pem
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
+TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
+cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
+WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
+ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
+MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
+h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
+A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
+T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
+B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
+B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
+KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
+OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
+jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
+qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
+rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
+hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
+ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
+3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
+NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
+ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
+TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
+jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
+oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
+4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
+mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
+emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
+-----END CERTIFICATE-----
diff --git a/tests/net_inet/resolve_on_connect.py b/tests/net_inet/resolve_on_connect.py
new file mode 100644
index 00000000000..ac36e0f7fcd
--- /dev/null
+++ b/tests/net_inet/resolve_on_connect.py
@@ -0,0 +1,38 @@
+# Test that the socket module performs DNS resolutions on bind and connect.
+# Currenty only the esp32 port does this, so the test is restricted to that port.
+
+import sys
+
+if sys.implementation.name == "micropython" and sys.platform != "esp32":
+    print("SKIP")
+    raise SystemExit
+
+import socket
+import unittest
+
+
+class Test(unittest.TestCase):
+    def test_bind_resolves_0_0_0_0(self):
+        s = socket.socket()
+        self.assertEqual(s.bind(("0.0.0.0", 31245)), None)
+        s.close()
+
+    def test_bind_resolves_localhost(self):
+        s = socket.socket()
+        self.assertEqual(s.bind(("localhost", 31245)), None)
+        s.close()
+
+    def test_connect_resolves(self):
+        s = socket.socket()
+        self.assertEqual(s.connect(("micropython.org", 80)), None)
+        s.close()
+
+    def test_connect_non_existent(self):
+        s = socket.socket()
+        with self.assertRaises(OSError):
+            s.connect(("nonexistent.example.com", 80))
+        s.close()
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/net_inet/ssl_cert.py b/tests/net_inet/ssl_cert.py
index d2d437e381b..ad285dc7702 100644
--- a/tests/net_inet/ssl_cert.py
+++ b/tests/net_inet/ssl_cert.py
@@ -1,64 +1,23 @@
-import ubinascii as binascii
-import usocket as socket
-import ussl as ssl
+import socket
+import ssl
 
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
 
-# This certificate was obtained from micropython.org using openssl:
-# $ openssl s_client -showcerts -connect micropython.org:443 /dev/null
-# The certificate is from Let's Encrypt:
-# 1 s:/C=US/O=Let's Encrypt/CN=R3
-#   i:/C=US/O=Internet Security Research Group/CN=ISRG Root X1
-# Validity
-#            Not Before: Sep  4 00:00:00 2020 GMT
-#            Not After : Sep 15 16:00:00 2025 GMT
-# Copy PEM content to a file (certmpy.pem) and convert to DER e.g.
-# $ openssl x509 -in certmpy.pem -out certmpy.der -outform DER
-# Then convert to hex format, eg using binascii.hexlify(data).
-
-ca_cert_chain = binascii.unhexlify(
-    b"30820516308202fea003020102021100912b084acf0c18a753f6d62e25a75f5a300d06092a864886"
-    b"f70d01010b0500304f310b300906035504061302555331293027060355040a1320496e7465726e65"
-    b"742053656375726974792052657365617263682047726f7570311530130603550403130c49535247"
-    b"20526f6f74205831301e170d3230303930343030303030305a170d3235303931353136303030305a"
-    b"3032310b300906035504061302555331163014060355040a130d4c6574277320456e637279707431"
-    b"0b300906035504031302523330820122300d06092a864886f70d01010105000382010f003082010a"
-    b"0282010100bb021528ccf6a094d30f12ec8d5592c3f882f199a67a4288a75d26aab52bb9c54cb1af"
-    b"8e6bf975c8a3d70f4794145535578c9ea8a23919f5823c42a94e6ef53bc32edb8dc0b05cf35938e7"
-    b"edcf69f05a0b1bbec094242587fa3771b313e71cace19befdbe43b45524596a9c153ce34c852eeb5"
-    b"aeed8fde6070e2a554abb66d0e97a540346b2bd3bc66eb66347cfa6b8b8f572999f830175dba726f"
-    b"fb81c5add286583d17c7e709bbf12bf786dcc1da715dd446e3ccad25c188bc60677566b3f118f7a2"
-    b"5ce653ff3a88b647a5ff1318ea9809773f9d53f9cf01e5f5a6701714af63a4ff99b3939ddc53a706"
-    b"fe48851da169ae2575bb13cc5203f5ed51a18bdb150203010001a382010830820104300e0603551d"
-    b"0f0101ff040403020186301d0603551d250416301406082b0601050507030206082b060105050703"
-    b"0130120603551d130101ff040830060101ff020100301d0603551d0e04160414142eb317b75856cb"
-    b"ae500940e61faf9d8b14c2c6301f0603551d2304183016801479b459e67bb6e5e40173800888c81a"
-    b"58f6e99b6e303206082b0601050507010104263024302206082b060105050730028616687474703a"
-    b"2f2f78312e692e6c656e63722e6f72672f30270603551d1f0420301e301ca01aa018861668747470"
-    b"3a2f2f78312e632e6c656e63722e6f72672f30220603551d20041b30193008060667810c01020130"
-    b"0d060b2b0601040182df13010101300d06092a864886f70d01010b0500038202010085ca4e473ea3"
-    b"f7854485bcd56778b29863ad754d1e963d336572542d81a0eac3edf820bf5fccb77000b76e3bf65e"
-    b"94dee4209fa6ef8bb203e7a2b5163c91ceb4ed3902e77c258a47e6656e3f46f4d9f0ce942bee54ce"
-    b"12bc8c274bb8c1982fa2afcd71914a08b7c8b8237b042d08f908573e83d904330a472178098227c3"
-    b"2ac89bb9ce5cf264c8c0be79c04f8e6d440c5e92bb2ef78b10e1e81d4429db5920ed63b921f81226"
-    b"949357a01d6504c10a22ae100d4397a1181f7ee0e08637b55ab1bd30bf876e2b2aff214e1b05c3f5"
-    b"1897f05eacc3a5b86af02ebc3b33b9ee4bdeccfce4af840b863fc0554336f668e136176a8e99d1ff"
-    b"a540a734b7c0d063393539756ef2ba76c89302e9a94b6c17ce0c02d9bd81fb9fb768d40665b3823d"
-    b"7753f88e7903ad0a3107752a43d8559772c4290ef7c45d4ec8ae468430d7f2855f18a179bbe75e70"
-    b"8b07e18693c3b98fdc6171252aafdfed255052688b92dce5d6b5e3da7dd0876c842131ae82f5fbb9"
-    b"abc889173de14ce5380ef6bd2bbd968114ebd5db3d20a77e59d3e2f858f95bb848cdfe5c4f1629fe"
-    b"1e5523afc811b08dea7c9390172ffdaca20947463ff0e9b0b7ff284d6832d6675e1e69a393b8f59d"
-    b"8b2f0bd25243a66f3257654d3281df3853855d7e5d6629eab8dde495b5cdb5561242cdc44ec62538"
-    b"44506decce005518fee94964d44eca979cb45bc073a8abb847c2"
-)
+# For details about this cert, see comment in test_sslcontext_client.py
+root_cert_path = "isrgrootx1.der"
 
 
 def main(use_stream=True):
+    with open(root_cert_path, "rb") as f:
+        cadata = f.read()
     s = socket.socket()
     ai = socket.getaddrinfo("micropython.org", 443)
     addr = ai[0][-1]
     s.connect(addr)
     s = ssl.wrap_socket(
-        s, cert_reqs=ssl.CERT_REQUIRED, cadata=ca_cert_chain, server_hostname="micropython.org"
+        s, cert_reqs=ssl.CERT_REQUIRED, cadata=cadata, server_hostname="micropython.org"
     )
     s.write(b"GET / HTTP/1.0\r\n\r\n")
     print(s.read(17))
diff --git a/tests/net_inet/ssl_errors.py b/tests/net_inet/ssl_errors.py
index ece1f6e253b..76785c4a0d7 100644
--- a/tests/net_inet/ssl_errors.py
+++ b/tests/net_inet/ssl_errors.py
@@ -1,31 +1,34 @@
 # test that socket.connect() on a non-blocking socket raises EINPROGRESS
 # and that an immediate write/send/read/recv does the right thing
 
-import sys
+import sys, errno, select, socket, ssl
 
-try:
-    import uerrno as errno, usocket as socket, ussl as ssl
-except:
-    import errno, socket, ssl
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
 
 
 def test(addr, hostname, block=True):
-    print("---", hostname or addr)
-    s = socket.socket()
+    print("---", hostname)
+    s = socket.socket(socket.AF_INET)
     s.setblocking(block)
     try:
         s.connect(addr)
         print("connected")
     except OSError as e:
         if e.errno != errno.EINPROGRESS:
-            raise
+            raise e
         print("EINPROGRESS")
 
+    if sys.implementation.name != "micropython":
+        # in CPython we have to wait, otherwise wrap_socket is not happy
+        select.select([], [s], [])
+
+    ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    ssl_context.verify_mode = ssl.CERT_REQUIRED
+
     try:
-        if sys.implementation.name == "micropython":
-            s = ssl.wrap_socket(s, do_handshake=block)
-        else:
-            s = ssl.wrap_socket(s, do_handshake_on_connect=block)
+        s = ssl_context.wrap_socket(s, do_handshake_on_connect=block, server_hostname=hostname)
         print("wrap: True")
     except OSError:
         print("wrap: error")
@@ -41,11 +44,11 @@ def test(addr, hostname, block=True):
 
 if __name__ == "__main__":
     # connect to plain HTTP port, oops!
-    addr = socket.getaddrinfo("micropython.org", 80)[0][-1]
-    test(addr, None)
+    addr = socket.getaddrinfo("micropython.org", 80, socket.AF_INET)[0][-1]
+    test(addr, "micropython.org")
     # connect to plain HTTP port, oops!
-    addr = socket.getaddrinfo("micropython.org", 80)[0][-1]
-    test(addr, None, False)
+    addr = socket.getaddrinfo("micropython.org", 80, socket.AF_INET)[0][-1]
+    test(addr, "micropython.org", False)
     # connect to server with self-signed cert, oops!
-    addr = socket.getaddrinfo("test.mosquitto.org", 8883)[0][-1]
+    addr = socket.getaddrinfo("test.mosquitto.org", 8883, socket.AF_INET)[0][-1]
     test(addr, "test.mosquitto.org")
diff --git a/tests/net_inet/test_sslcontext_client.py b/tests/net_inet/test_sslcontext_client.py
new file mode 100644
index 00000000000..5aa00edf9d3
--- /dev/null
+++ b/tests/net_inet/test_sslcontext_client.py
@@ -0,0 +1,42 @@
+import os
+import socket
+import ssl
+
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+# This ISRG Root X1 certificate was downloaded from https://letsencrypt.org/certificates/#root-cas
+# This cert is used to sign the intermediate cert in use by micropython.org
+# To check the current intermediate cert, can run:
+# $ openssl s_client -showcerts -connect micropython.org:443 /dev/null
+root_cert_path = "isrgrootx1.der"
+
+
+def main(use_stream=True):
+    context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+
+    context.verify_mode = ssl.CERT_REQUIRED
+    assert context.verify_mode == ssl.CERT_REQUIRED
+
+    context.load_verify_locations(cafile=root_cert_path)
+
+    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    addr = socket.getaddrinfo("micropython.org", 443)[0][-1]
+
+    # CPython can wrap the socket even if not connected yet.
+    # ssl_sock = context.wrap_socket(s, server_hostname='micropython.org')
+    # ssl_sock.connect(addr)
+
+    # MicroPython needs to connect first, CPython can do this too.
+    s.connect(addr)
+    # server_hostname must match CN (Common Name) in the certificate
+    # presented by the server
+    ssl_sock = context.wrap_socket(s, server_hostname="micropython.org")
+    ssl_sock.write(b"GET / HTTP/1.0\r\n\r\n")
+    print(ssl_sock.read(17))
+    assert isinstance(ssl_sock.cipher(), tuple)
+    ssl_sock.close()
+
+
+main()
diff --git a/tests/net_inet/test_sslcontext_client.py.exp b/tests/net_inet/test_sslcontext_client.py.exp
new file mode 100644
index 00000000000..fb69ecf1a66
--- /dev/null
+++ b/tests/net_inet/test_sslcontext_client.py.exp
@@ -0,0 +1 @@
+b'HTTP/1.1 200 OK\r\n'
diff --git a/tests/net_inet/test_tls_nonblock.py b/tests/net_inet/test_tls_nonblock.py
index b4f2ef4ed27..1f2b3a899fa 100644
--- a/tests/net_inet/test_tls_nonblock.py
+++ b/tests/net_inet/test_tls_nonblock.py
@@ -1,38 +1,47 @@
-try:
-    import usocket as socket, ussl as ssl, uerrno as errno, sys
-except:
-    import socket, ssl, errno, sys, time, select
+import socket, ssl, errno, sys, time, select
+
+# Although this test doesn't need ssl.CERT_REQUIRED, it does require the ssl module
+# to support modern ciphers.  So exclude the test on axTLS which doesn't have
+# CERT_REQUIRED.
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
 
 
 def test_one(site, opts):
-    ai = socket.getaddrinfo(site, 443)
+    ai = socket.getaddrinfo(site, 443, socket.AF_INET)
     addr = ai[0][-1]
-    print(addr)
+    print(site)
 
     # Connect the raw socket
-    s = socket.socket()
+    s = socket.socket(socket.AF_INET)
     s.setblocking(False)
     try:
         s.connect(addr)
         raise OSError(-1, "connect blocks")
     except OSError as e:
         if e.errno != errno.EINPROGRESS:
-            raise
+            raise e
+
+    # Create SSLContext.
+    ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
 
+    # CPython compatibility:
+    # - disable check_hostname
+    # - load default system certificate chain
+    # - must wait for socket to be writable before calling wrap_socket
     if sys.implementation.name != "micropython":
-        # in CPython we have to wait, otherwise wrap_socket is not happy
+        ssl_context.check_hostname = False
+        ssl_context.load_default_certs()
         select.select([], [s], [])
 
     try:
         # Wrap with SSL
         try:
-            if sys.implementation.name == "micropython":
-                s = ssl.wrap_socket(s, do_handshake=False)
-            else:
-                s = ssl.wrap_socket(s, do_handshake_on_connect=False)
+            s = ssl_context.wrap_socket(s, do_handshake_on_connect=False)
         except OSError as e:
             if e.errno != errno.EINPROGRESS:
-                raise
+                raise e
         print("wrapped")
 
         # CPython needs to be told to do the handshake
@@ -47,12 +56,12 @@ def test_one(site, opts):
                     elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE:
                         select.select([], [s], [])
                     else:
-                        raise
+                        raise err
                 time.sleep(0.1)
             # print("shook hands")
 
         # Write HTTP request
-        out = b"GET / HTTP/1.0\r\nHost: %s\r\n\r\n" % bytes(site, "latin")
+        out = b"GET / HTTP/1.0\r\nHost: %s\r\n\r\n" % bytes(site, "ascii")
         while len(out) > 0:
             n = s.write(out)
             if n is None:
@@ -71,7 +80,7 @@ def test_one(site, opts):
             except OSError as err:
                 if err.errno == 2:  # 2=ssl.SSL_ERROR_WANT_READ:
                     continue
-                raise
+                raise err
             if b is None:
                 continue
             if len(b) > 0:
@@ -90,8 +99,7 @@ def test_one(site, opts):
 
 
 SITES = [
-    "google.com",
-    {"host": "www.google.com"},
+    "www.github.com",
     "micropython.org",
     "pypi.org",
     {"host": "api.pushbullet.com", "sni": True},
@@ -108,7 +116,7 @@ def main():
             test_one(site, opts)
             print(site, "ok")
         except Exception as e:
-            print(site, "error")
+            print(site, "error", e)
     print("DONE")
 
 
diff --git a/tests/net_inet/test_tls_sites.py b/tests/net_inet/test_tls_sites.py
index fabe4b43c9c..2f4ea3ce079 100644
--- a/tests/net_inet/test_tls_sites.py
+++ b/tests/net_inet/test_tls_sites.py
@@ -1,31 +1,43 @@
-try:
-    import usocket as _socket
-except:
-    import _socket
-try:
-    import ussl as ssl
-except:
-    import ssl
+# Test making HTTPS requests to sites that may require advanced ciphers.
 
-    # CPython only supports server_hostname with SSLContext
-    ssl = ssl.SSLContext()
+import sys
+import select
+import socket
+import ssl
+
+# Don't run if ssl doesn't support required certificates (eg axtls).
+if not hasattr(ssl, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
 
 
 def test_one(site, opts):
-    ai = _socket.getaddrinfo(site, 443)
+    ai = socket.getaddrinfo(site, 443, socket.AF_INET)
     addr = ai[0][-1]
 
-    s = _socket.socket()
+    s = socket.socket(socket.AF_INET)
+
+    # Create SSLContext.
+    ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+
+    # CPython compatibility:
+    # - disable check_hostname
+    # - load default system certificate chain
+    # - must wait for socket to be writable before calling wrap_socket
+    if sys.implementation.name != "micropython":
+        ssl_context.check_hostname = False
+        ssl_context.load_default_certs()
+        select.select([], [s], [])
 
     try:
         s.connect(addr)
 
         if "sni" in opts:
-            s = ssl.wrap_socket(s, server_hostname=opts["host"])
+            s = ssl_context.wrap_socket(s, server_hostname=opts["host"])
         else:
-            s = ssl.wrap_socket(s)
+            s = ssl_context.wrap_socket(s)
 
-        s.write(b"GET / HTTP/1.0\r\nHost: %s\r\n\r\n" % bytes(site, "latin"))
+        s.write(b"GET / HTTP/1.0\r\nHost: %s\r\n\r\n" % bytes(site, "ascii"))
         resp = s.read(4096)
         if resp[:7] != b"HTTP/1.":
             raise ValueError("response doesn't start with HTTP/1.")
@@ -36,8 +48,7 @@ def test_one(site, opts):
 
 
 SITES = [
-    "google.com",
-    "www.google.com",
+    "www.github.com",
     "micropython.org",
     "pypi.org",
     {"host": "api.pushbullet.com", "sni": True},
diff --git a/tests/net_inet/test_tls_sites.py.exp b/tests/net_inet/test_tls_sites.py.exp
deleted file mode 100644
index 09faa336a71..00000000000
--- a/tests/net_inet/test_tls_sites.py.exp
+++ /dev/null
@@ -1,5 +0,0 @@
-google.com ok
-www.google.com ok
-micropython.org ok
-pypi.org ok
-api.pushbullet.com ok
diff --git a/tests/net_inet/test_tls_sites_simple.py b/tests/net_inet/test_tls_sites_simple.py
new file mode 100644
index 00000000000..9dece9d99c7
--- /dev/null
+++ b/tests/net_inet/test_tls_sites_simple.py
@@ -0,0 +1,43 @@
+# Test making HTTPS requests to sites that allow simple ciphers.
+
+import sys
+import socket
+import ssl
+
+SITES = (
+    ("micropython.org", "/ks/test.html"),
+    ("pypi.org", "/"),
+)
+
+
+def test_one(site, path):
+    ai = socket.getaddrinfo(site, 443, socket.AF_INET)
+    addr = ai[0][-1]
+
+    # Create SSLContext.
+    ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    if sys.implementation.name != "micropython":
+        # CPython compatibility: disable check_hostname
+        ssl_context.check_hostname = False
+    ssl_context.verify_mode = ssl.CERT_NONE
+
+    s = socket.socket(socket.AF_INET)
+    s.connect(addr)
+    s = ssl_context.wrap_socket(s)
+
+    s.write(b"GET %s HTTP/1.0\r\nHost: %s\r\n\r\n" % (bytes(path, "ascii"), bytes(site, "ascii")))
+    resp = s.read(4096)
+    s.close()
+
+    if resp.startswith(b"HTTP/1."):
+        print(site, "ok")
+    else:
+        print(site, "response doesn't start with HTTP/1.")
+
+
+def main():
+    for site, path in SITES:
+        test_one(site, path)
+
+
+main()
diff --git a/tests/net_inet/tls_cert_pem.py b/tests/net_inet/tls_cert_pem.py
new file mode 100644
index 00000000000..5f333c565a0
--- /dev/null
+++ b/tests/net_inet/tls_cert_pem.py
@@ -0,0 +1,27 @@
+# Test tls.load_verify_locations loading a .pem file.
+
+import socket
+import tls
+
+if not hasattr(tls, "CERT_REQUIRED"):
+    print("SKIP")
+    raise SystemExit
+
+# For details about this cert, see comment in test_sslcontext_client.py
+root_cert_path = "isrgrootx1.pem"
+
+with open(root_cert_path) as ca:
+    cadata = ca.read()
+
+context = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+context.verify_mode = tls.CERT_REQUIRED
+context.load_verify_locations(cadata)
+
+s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+addr = socket.getaddrinfo("micropython.org", 443)[0][-1]
+
+s.connect(addr)
+ssl_sock = context.wrap_socket(s, server_hostname="micropython.org")
+ssl_sock.write(b"GET / HTTP/1.0\r\n\r\n")
+print(ssl_sock.read(17))
+ssl_sock.close()
diff --git a/tests/net_inet/tls_cert_pem.py.exp b/tests/net_inet/tls_cert_pem.py.exp
new file mode 100644
index 00000000000..fb69ecf1a66
--- /dev/null
+++ b/tests/net_inet/tls_cert_pem.py.exp
@@ -0,0 +1 @@
+b'HTTP/1.1 200 OK\r\n'
diff --git a/tests/net_inet/tls_num_errors.py b/tests/net_inet/tls_num_errors.py
index dd7f714e6ef..79678e07274 100644
--- a/tests/net_inet/tls_num_errors.py
+++ b/tests/net_inet/tls_num_errors.py
@@ -1,24 +1,35 @@
 # test that modtls produces a numerical error message when out of heap
 
+import socket, tls
+
+# This test is specific to the mbedTLS implementation, so require that.
+if not hasattr(tls, "MBEDTLS_VERSION"):
+    print("SKIP")
+    raise SystemExit
+
 try:
-    import usocket as socket, ussl as ssl, sys
-except:
-    import socket, ssl, sys
-try:
-    from micropython import alloc_emergency_exception_buf, heap_lock, heap_unlock
+    from micropython import heap_lock, heap_unlock
 except:
     print("SKIP")
     raise SystemExit
 
+try:
+    from micropython import alloc_emergency_exception_buf
+
+    alloc_emergency_exception_buf(256)
+except:
+    pass
+
 
 # test with heap locked to see it switch to number-only error message
 def test(addr):
-    alloc_emergency_exception_buf(256)
+    ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+    ctx.verify_mode = tls.CERT_NONE
     s = socket.socket()
     s.connect(addr)
     try:
         s.setblocking(False)
-        s = ssl.wrap_socket(s, do_handshake=False)
+        s = ctx.wrap_socket(s, do_handshake_on_connect=False)
         heap_lock()
         print("heap is locked")
         while True:
diff --git a/tests/net_inet/tls_text_errors.py b/tests/net_inet/tls_text_errors.py
index 9e8ccfaf9ec..f7c8014d75c 100644
--- a/tests/net_inet/tls_text_errors.py
+++ b/tests/net_inet/tls_text_errors.py
@@ -1,25 +1,28 @@
 # test that modtls produces a text error message
 
-try:
-    import usocket as socket, ussl as ssl, sys
-except:
-    import socket, ssl, sys
+import socket, ssl
 
 
 def test(addr):
     s = socket.socket()
     s.connect(addr)
     try:
-        s = ssl.wrap_socket(s)
+        ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+        if hasattr(ssl_context, "check_hostname"):
+            # Disable hostname check on CPython.
+            ssl_context.check_hostname = False
+        s = ssl_context.wrap_socket(s)
         print("wrap: no exception")
     except OSError as e:
         # mbedtls produces "mbedtls -0x7200: SSL - An invalid SSL record was received"
         # axtls produces "RECORD_OVERFLOW" but also prints "TLS buffer overflow,..."
         # CPython produces "[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1108)"
+        # CPython newer version produces "[SSL] record layer failure (_ssl.c:1000)"
         ok = (
             "SSL_INVALID_RECORD" in str(e)
             or "RECORD_OVERFLOW" in str(e)
             or "wrong version" in str(e)
+            or "record layer failure" in str(e)
         )
         print("wrap:", ok)
         if not ok:
diff --git a/tests/net_inet/tls_text_errors.py.exp b/tests/net_inet/tls_text_errors.py.exp
new file mode 100644
index 00000000000..2cedeb7c9a3
--- /dev/null
+++ b/tests/net_inet/tls_text_errors.py.exp
@@ -0,0 +1,2 @@
+########
+wrap: True
diff --git a/tests/net_inet/uasyncio_cancel_stream.py b/tests/net_inet/uasyncio_cancel_stream.py
deleted file mode 100644
index 6b6b845b0f7..00000000000
--- a/tests/net_inet/uasyncio_cancel_stream.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Test cancelling a task waiting on stream IO
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def get(reader):
-    print("start")
-    try:
-        await reader.read(10)
-        print("fail")
-    except asyncio.CancelledError:
-        print("cancelled")
-
-
-async def main(url):
-    reader, writer = await asyncio.open_connection(url, 80)
-    task = asyncio.create_task(get(reader))
-    await asyncio.sleep(0)
-    print("cancelling")
-    task.cancel()
-    print("waiting")
-    await task
-    print("done")
-    writer.close()
-    await writer.wait_closed()
-
-
-asyncio.run(main("micropython.org"))
diff --git a/tests/net_inet/uasyncio_open_connection.py b/tests/net_inet/uasyncio_open_connection.py
deleted file mode 100644
index 68285a2437d..00000000000
--- a/tests/net_inet/uasyncio_open_connection.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Test simple HTTP request with uasyncio.open_connection()
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def http_get(url):
-    reader, writer = await asyncio.open_connection(url, 80)
-
-    print("write GET")
-    writer.write(b"GET / HTTP/1.0\r\n\r\n")
-    await writer.drain()
-
-    print("read response")
-    data = await reader.read(100)
-    print("read:", data.split(b"\r\n")[0])
-
-    print("close")
-    writer.close()
-    await writer.wait_closed()
-    print("done")
-
-
-asyncio.run(http_get("micropython.org"))
diff --git a/tests/net_inet/uasyncio_tcp_read_headers.py b/tests/net_inet/uasyncio_tcp_read_headers.py
deleted file mode 100644
index e8087aeedf7..00000000000
--- a/tests/net_inet/uasyncio_tcp_read_headers.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Test uasyncio.open_connection() and stream readline()
-
-try:
-    import uasyncio as asyncio
-except ImportError:
-    try:
-        import asyncio
-    except ImportError:
-        print("SKIP")
-        raise SystemExit
-
-
-async def http_get_headers(url):
-    reader, writer = await asyncio.open_connection(url, 80)
-
-    print("write GET")
-    writer.write(b"GET / HTTP/1.0\r\n\r\n")
-    await writer.drain()
-
-    while True:
-        line = await reader.readline()
-        line = line.strip()
-        if not line:
-            break
-        if (
-            line.find(b"Date") == -1
-            and line.find(b"Modified") == -1
-            and line.find(b"Server") == -1
-        ):
-            print(line)
-
-    print("close")
-    writer.close()
-    await writer.wait_closed()
-    print("done")
-
-
-asyncio.run(http_get_headers("micropython.org"))
diff --git a/tests/perf_bench/benchrun.py b/tests/perf_bench/benchrun.py
index ed43297d15f..4029c8ac8aa 100644
--- a/tests/perf_bench/benchrun.py
+++ b/tests/perf_bench/benchrun.py
@@ -1,10 +1,10 @@
 def bm_run(N, M):
     try:
-        from utime import ticks_us, ticks_diff
+        from time import ticks_us, ticks_diff
     except ImportError:
-        import time
+        from time import perf_counter
 
-        ticks_us = lambda: int(time.perf_counter() * 1000000)
+        ticks_us = lambda: int(perf_counter() * 1000000)
         ticks_diff = lambda a, b: a - b
 
     # Pick sensible parameters given N, M
diff --git a/tests/perf_bench/bm_fft.py b/tests/perf_bench/bm_fft.py
index 9a2d03d11b9..e35c1216c13 100644
--- a/tests/perf_bench/bm_fft.py
+++ b/tests/perf_bench/bm_fft.py
@@ -15,7 +15,7 @@ def reverse(x, bits):
 
     # Initialization
     n = len(vector)
-    levels = int(math.log(n) / math.log(2))
+    levels = int(round(math.log(n) / math.log(2)))
     coef = (2 if inverse else -2) * cmath.pi / n
     exptable = [cmath.rect(1, i * coef) for i in range(n // 2)]
     vector = [vector[reverse(i, levels)] for i in range(n)]  # Copy with bit-reversed permutation
diff --git a/tests/perf_bench/bm_hexiom.py b/tests/perf_bench/bm_hexiom.py
index 84eda9a9096..e36fc234cd7 100644
--- a/tests/perf_bench/bm_hexiom.py
+++ b/tests/perf_bench/bm_hexiom.py
@@ -632,10 +632,7 @@ def solve_file(file, strategy, order, output):
 
 
 def bm_setup(params):
-    try:
-        import uio as io
-    except ImportError:
-        import io
+    import io
 
     loops, level, order, strategy = params
 
diff --git a/tests/perf_bench/bm_pidigits.py b/tests/perf_bench/bm_pidigits.py
index bdaa73cec7e..c935f103c5b 100644
--- a/tests/perf_bench/bm_pidigits.py
+++ b/tests/perf_bench/bm_pidigits.py
@@ -5,6 +5,12 @@
 # This benchmark stresses big integer arithmetic.
 # Adapted from code on: http://benchmarksgame.alioth.debian.org/
 
+try:
+    int("0x10000000000000000", 16)
+except:
+    print("SKIP")  # No support for >64-bit integers
+    raise SystemExit
+
 
 def compose(a, b):
     aq, ar, as_, at = a
diff --git a/tests/perf_bench/bm_wordcount.py b/tests/perf_bench/bm_wordcount.py
new file mode 100644
index 00000000000..ef55046956a
--- /dev/null
+++ b/tests/perf_bench/bm_wordcount.py
@@ -0,0 +1,45 @@
+# This tests using string as dictionary keys when they are not qstrs
+
+ZEN = "the zen of python beautiful is better than ugly explicit is better than implicit simple is better than complex complex is better than complicated flat is better than nested sparse is better than dense readability counts special cases arent special enough to break the rules although practicality beats purity errors should never pass silently unless explicitly silenced in the face of ambiguity refuse the temptation to guess there should be one and preferably only one obvious way to do it although that way may not be obvious at first unless youre dutch now is better than never although never is often better than right now if the implementation is hard to explain its a bad idea if the implementation is easy to explain it may be a good idea namespaces are one honking great idea  lets do more of those"
+
+
+def test(niter):
+    words = ZEN.split(" ")
+    for _ in range(niter):
+        counts = {}
+        for _ in range(niter):
+            for word in words:
+                counts[word] = counts.get(word, 0) + 1
+
+    return (
+        counts["python"],
+        counts["is"],
+        counts["than"],
+    )
+
+
+###########################################################################
+# Benchmark interface
+
+bm_params = {
+    (32, 10): (2,),
+    (50, 10): (4,),
+    (100, 10): (8,),
+    (500, 10): (40,),
+    (1000, 10): (80,),
+    (5000, 10): (400,),
+}
+
+
+def bm_setup(params):
+    (niter,) = params
+    state = None
+
+    def run():
+        nonlocal state
+        state = test(niter)
+
+    def result():
+        return niter, state
+
+    return run, result
diff --git a/tests/perf_bench/core_import_mpy_multi.py b/tests/perf_bench/core_import_mpy_multi.py
index 99c4721d29d..55aa379e2bf 100644
--- a/tests/perf_bench/core_import_mpy_multi.py
+++ b/tests/perf_bench/core_import_mpy_multi.py
@@ -1,12 +1,13 @@
 # Test performance of importing an .mpy file many times.
 
-import usys, uio, uos
-
-if not (hasattr(uio, "IOBase") and hasattr(uos, "mount")):
+try:
+    import sys, vfs
+    from io import IOBase
+except ImportError:
     print("SKIP")
     raise SystemExit
 
-# This is the test.py file that is compiled to test.mpy below.
+# This is the test.py file that is compiled to test.mpy below. mpy-cross must be invoked with `-msmall-int-bits=30`.
 """
 class A:
     def __init__(self, arg):
@@ -23,15 +24,17 @@ def f():
     x = ("const tuple", None, False, True, 1, 2, 3)
 result = 123
 """
-file_data = b'M\x06\x00\x1f\x14\x03\x0etest.py\x00\x0f\x02A\x00\x02f\x00\x0cresult\x00/-5#\x82I\x81{\x81w\x82/\x81\x05\x81\x17Iom\x82\x13\x06arg\x00\x05\x1cthis will be a string object\x00\x06\x1bthis will be a bytes object\x00\n\x07\x05\x0bconst tuple\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\x81\\\x10\n\x01\x89\x07d`T2\x00\x10\x024\x02\x16\x022\x01\x16\x03"\x80{\x16\x04Qc\x02\x81d\x00\x08\x02(DD\x11\x05\x16\x06\x10\x02\x16\x072\x00\x16\x082\x01\x16\t2\x02\x16\nQc\x03`\x1a\x08\x08\x12\x13@\xb1\xb0\x18\x13Qc@\t\x08\t\x12` Qc@\t\x08\n\x12``Qc\x82@ \x0e\x03\x80\x08+)##\x12\x0b\x12\x0c\x12\r\x12\x0e*\x04Y\x12\x0f\x12\x10\x12\x11*\x03Y#\x00\xc0#\x01\xc0#\x02\xc0Qc'
+file_data = b'M\x06\x00\x1e\x14\x03\x0etest.py\x00\x0f\x02A\x00\x02f\x00\x0cresult\x00/-5#\x82I\x81{\x81w\x82/\x81\x05\x81\x17Iom\x82\x13\x06arg\x00\x05\x1cthis will be a string object\x00\x06\x1bthis will be a bytes object\x00\n\x07\x05\x0bconst tuple\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\x81\\\x10\n\x01\x89\x07d`T2\x00\x10\x024\x02\x16\x022\x01\x16\x03"\x80{\x16\x04Qc\x02\x81d\x00\x08\x02(DD\x11\x05\x16\x06\x10\x02\x16\x072\x00\x16\x082\x01\x16\t2\x02\x16\nQc\x03`\x1a\x08\x08\x12\x13@\xb1\xb0\x18\x13Qc@\t\x08\t\x12` Qc@\t\x08\n\x12``Qc\x82@ \x0e\x03\x80\x08+)##\x12\x0b\x12\x0c\x12\r\x12\x0e*\x04Y\x12\x0f\x12\x10\x12\x11*\x03Y#\x00\xc0#\x01\xc0#\x02\xc0Qc'
 
 
-class File(uio.IOBase):
+class File(IOBase):
     def __init__(self):
         self.off = 0
 
     def ioctl(self, request, arg):
-        return 0
+        if request == 4:  # MP_STREAM_CLOSE
+            return 0
+        return -1
 
     def readinto(self, buf):
         buf[:] = memoryview(file_data)[self.off : self.off + len(buf)]
@@ -47,7 +50,7 @@ def chdir(self, path):
         pass
 
     def stat(self, path):
-        if path == "__injected.mpy":
+        if path == "/__injected.mpy":
             return tuple(0 for _ in range(10))
         else:
             raise OSError(-2)  # ENOENT
@@ -57,14 +60,14 @@ def open(self, path, mode):
 
 
 def mount():
-    uos.mount(FS(), "/__remote")
-    uos.chdir("/__remote")
+    vfs.mount(FS(), "/__remote")
+    sys.path.insert(0, "/__remote")
 
 
 def test(r):
     global result
     for _ in r:
-        usys.modules.clear()
+        sys.modules.clear()
         module = __import__("__injected")
     result = module.result
 
diff --git a/tests/perf_bench/core_import_mpy_single.py b/tests/perf_bench/core_import_mpy_single.py
index af3f4a29b25..755e4159487 100644
--- a/tests/perf_bench/core_import_mpy_single.py
+++ b/tests/perf_bench/core_import_mpy_single.py
@@ -2,13 +2,14 @@
 # The first import of a module will intern strings that don't already exist, and
 # this test should be representative of what happens in a real application.
 
-import uio, uos
-
-if not (hasattr(uio, "IOBase") and hasattr(uos, "mount")):
+try:
+    import sys, vfs
+    from io import IOBase
+except ImportError:
     print("SKIP")
     raise SystemExit
 
-# This is the test.py file that is compiled to test.mpy below.
+# This is the test.py file that is compiled to test.mpy below. mpy-cross must be invoked with `-msmall-int-bits=30`.
 # Many known and unknown names/strings are included to test the linking process.
 """
 class A0:
@@ -78,15 +79,17 @@ def f1():
     x = ("const tuple 9", None, False, True, 1, 2, 3)
 result = 123
 """
-file_data = b"M\x06\x00\x1f\x81=\x1e\x0etest.py\x00\x0f\x04A0\x00\x04A1\x00\x04f0\x00\x04f1\x00\x0cresult\x00/-5\x04a0\x00\x04a1\x00\x04a2\x00\x04a3\x00\x13\x15\x17\x19\x1b\x1d\x1f!#%')+1379;=?ACEGIKMOQSUWY[]_acegikmoqsuwy{}\x7f\x81\x01\x81\x03\x81\x05\x81\x07\x81\t\x81\x0b\x81\r\x81\x0f\x81\x11\x81\x13\x81\x15\x81\x17\x81\x19\x81\x1b\x81\x1d\x81\x1f\x81!\x81#\x81%\x81'\x81)\x81+\x81-\x81/\x811\x813\x815\x817\x819\x81;\x81=\x81?\x81A\x81C\x81E\x81G\x81I\x81K\x81M\x81O\x81Q\x81S\x81U\x81W\x81Y\x81[\x81]\x81_\x81a\x81c\x81e\x81g\x81i\x81k\x81m\x81o\x81q\x81s\x81u\x81w\x81y\x81{\x81}\x81\x7f\x82\x01\x82\x03\x82\x05\x82\x07\x82\t\x82\x0b\x82\r\x82\x0f\x82\x11\x82\x13\x82\x15\x82\x17\x82\x19\x82\x1b\x82\x1d\x82\x1f\x82!\x82#\x82%\x82'\x82)\x82+\x82-\x82/\x821\x823\x825\x827\x829\x82;\x82=\x82?\x82A\x82E\x82G\x82I\x82K\nname0\x00\nname1\x00\nname2\x00\nname3\x00\nname4\x00\nname5\x00\nname6\x00\nname7\x00\nname8\x00\nname9\x00$quite_a_long_name0\x00$quite_a_long_name1\x00$quite_a_long_name2\x00$quite_a_long_name3\x00$quite_a_long_name4\x00$quite_a_long_name5\x00$quite_a_long_name6\x00$quite_a_long_name7\x00$quite_a_long_name8\x00$quite_a_long_name9\x00&quite_a_long_name10\x00&quite_a_long_name11\x00\x05\x1ethis will be a string object 0\x00\x05\x1ethis will be a string object 1\x00\x05\x1ethis will be a string object 2\x00\x05\x1ethis will be a string object 3\x00\x05\x1ethis will be a string object 4\x00\x05\x1ethis will be a string object 5\x00\x05\x1ethis will be a string object 6\x00\x05\x1ethis will be a string object 7\x00\x05\x1ethis will be a string object 8\x00\x05\x1ethis will be a string object 9\x00\x06\x1dthis will be a bytes object 0\x00\x06\x1dthis will be a bytes object 1\x00\x06\x1dthis will be a bytes object 2\x00\x06\x1dthis will be a bytes object 3\x00\x06\x1dthis will be a bytes object 4\x00\x06\x1dthis will be a bytes object 5\x00\x06\x1dthis will be a bytes object 6\x00\x06\x1dthis will be a bytes object 7\x00\x06\x1dthis will be a bytes object 8\x00\x06\x1dthis will be a bytes object 9\x00\n\x07\x05\rconst tuple 0\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 1\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 2\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 3\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 4\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 5\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 6\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 7\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 8\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 9\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\x82d\x10\x12\x01i@i@\x84\x18\x84\x1fT2\x00\x10\x024\x02\x16\x02T2\x01\x10\x034\x02\x16\x032\x02\x16\x042\x03\x16\x05\"\x80{\x16\x06Qc\x04\x82\x0c\x00\n\x02($$$\x11\x07\x16\x08\x10\x02\x16\t2\x00\x16\n2\x01\x16\x0b2\x02\x16\x0c2\x03\x16\rQc\x04@\t\x08\n\x81\x0b Qc@\t\x08\x0b\x81\x0b@Qc@\t\x08\x0c\x81\x0b`QcH\t\n\r\x81\x0b` Qc\x82\x14\x00\x0c\x03h`$$$\x11\x07\x16\x08\x10\x03\x16\t2\x00\x16\n2\x01\x16\x0b2\x02\x16\x0c2\x03\x16\rQc\x04H\t\n\n\x81\x0b``QcH\t\n\x0b\x81\x0b\x80\x07QcH\t\n\x0c\x81\x0b\x80\x08QcH\t\n\r\x81\x0b\x80\tQc\xa08P:\x04\x80\x0b13///---997799<\x1f%\x1f\"\x1f%)\x1f\"//\x12\x0e\x12\x0f\x12\x10\x12\x11\x12\x12\x12\x13\x12\x14*\x07Y\x12\x15\x12\x16\x12\x17\x12\x18\x12\x19\x12\x1a\x12\x08\x12\x07*\x08Y\x12\x1b\x12\x1c\x12\t\x12\x1d\x12\x1e\x12\x1f*\x06Y\x12 \x12!\x12\"\x12#\x12$\x12%*\x06Y\x12&\x12'\x12(\x12)\x12*\x12+*\x06Y\x12,\x12-\x12.\x12/\x120*\x05Y\x121\x122\x123\x124\x125*\x05Y\x126\x127\x128\x129\x12:*\x05Y\x12;\x12<\x12=\x12>\x12?\x12@\x12A\x12B\x12C\x12D\x12E*\x0bY\x12F\x12G\x12H\x12I\x12J\x12K\x12L\x12M\x12N\x12O\x12P*\x0bY\x12Q\x12R\x12S\x12T\x12U\x12V\x12W\x12X\x12Y\x12Z*\nY\x12[\x12\\\x12]\x12^\x12_\x12`\x12a\x12b\x12c\x12d*\nY\x12e\x12f\x12g\x12h\x12i\x12j\x12k\x12l\x12m\x12n\x12o*\x0bY\x12p\x12q\x12r\x12s\x12t\x12u\x12v\x12w\x12x\x12y\x12z*\x0bY\x12{\x12|\x12}\x12~\x12\x7f\x12\x81\x00\x12\x81\x01\x12\x81\x02\x12\x81\x03\x12\x81\x04*\nY\x12\x81\x05\x12\x81\x06\x12\x81\x07\x12\x81\x08\x12\x81\t\x12\x81\n\x12\x81\x0b\x12\x81\x0c\x12\x81\r\x12\x81\x0e\x12\x81\x0f*\x0bY\x12\x81\x10\x12\x81\x11\x12\x81\x12\x12\x81\x13\x12\x81\x14\x12\x81\x15\x12\x81\x16\x12\x81\x17\x12\x81\x18\x12\x81\x19*\nY\x12\x81\x1a\x12\x81\x1b\x12\x81\x1c\x12\x81\x1d\x12\x81\x1e\x12\x81\x1f\x12\x81 \x12\x81!\x12\x81\"\x12\x81#\x12\x81$*\x0bY\x12\x81%\x12\x81&*\x02Y\x12\x81'\x12\x81(\x12\x81)\x12\x81*\x12\x81+\x12\x81,\x12\x81-\x12\x81.\x12\x81/\x12\x810*\nY\x12\x811\x12\x812\x12\x813\x12\x814*\x04Y\x12\x815\x12\x816\x12\x817\x12\x818*\x04Y\x12\x819\x12\x81:\x12\x81;\x12\x81<*\x04YQc\x87p\x08@\x05\x80###############################\x00\xc0#\x01\xc0#\x02\xc0#\x03\xc0#\x04\xc0#\x05\xc0#\x06\xc0#\x07\xc0#\x08\xc0#\t\xc0#\n\xc0#\x0b\xc0#\x0c\xc0#\r\xc0#\x0e\xc0#\x0f\xc0#\x10\xc0#\x11\xc0#\x12\xc0#\x13\xc0#\x14\xc0#\x15\xc0#\x16\xc0#\x17\xc0#\x18\xc0#\x19\xc0#\x1a\xc0#\x1b\xc0#\x1c\xc0#\x1d\xc0Qc"
+file_data = b"M\x06\x00\x1e\x81=\x1e\x0etest.py\x00\x0f\x04A0\x00\x04A1\x00\x04f0\x00\x04f1\x00\x0cresult\x00/-5\x04a0\x00\x04a1\x00\x04a2\x00\x04a3\x00\x13\x15\x17\x19\x1b\x1d\x1f!#%')+1379;=?ACEGIKMOQSUWY[]_acegikmoqsuwy{}\x7f\x81\x01\x81\x03\x81\x05\x81\x07\x81\t\x81\x0b\x81\r\x81\x0f\x81\x11\x81\x13\x81\x15\x81\x17\x81\x19\x81\x1b\x81\x1d\x81\x1f\x81!\x81#\x81%\x81'\x81)\x81+\x81-\x81/\x811\x813\x815\x817\x819\x81;\x81=\x81?\x81A\x81C\x81E\x81G\x81I\x81K\x81M\x81O\x81Q\x81S\x81U\x81W\x81Y\x81[\x81]\x81_\x81a\x81c\x81e\x81g\x81i\x81k\x81m\x81o\x81q\x81s\x81u\x81w\x81y\x81{\x81}\x81\x7f\x82\x01\x82\x03\x82\x05\x82\x07\x82\t\x82\x0b\x82\r\x82\x0f\x82\x11\x82\x13\x82\x15\x82\x17\x82\x19\x82\x1b\x82\x1d\x82\x1f\x82!\x82#\x82%\x82'\x82)\x82+\x82-\x82/\x821\x823\x825\x827\x829\x82;\x82=\x82?\x82A\x82E\x82G\x82I\x82K\nname0\x00\nname1\x00\nname2\x00\nname3\x00\nname4\x00\nname5\x00\nname6\x00\nname7\x00\nname8\x00\nname9\x00$quite_a_long_name0\x00$quite_a_long_name1\x00$quite_a_long_name2\x00$quite_a_long_name3\x00$quite_a_long_name4\x00$quite_a_long_name5\x00$quite_a_long_name6\x00$quite_a_long_name7\x00$quite_a_long_name8\x00$quite_a_long_name9\x00&quite_a_long_name10\x00&quite_a_long_name11\x00\x05\x1ethis will be a string object 0\x00\x05\x1ethis will be a string object 1\x00\x05\x1ethis will be a string object 2\x00\x05\x1ethis will be a string object 3\x00\x05\x1ethis will be a string object 4\x00\x05\x1ethis will be a string object 5\x00\x05\x1ethis will be a string object 6\x00\x05\x1ethis will be a string object 7\x00\x05\x1ethis will be a string object 8\x00\x05\x1ethis will be a string object 9\x00\x06\x1dthis will be a bytes object 0\x00\x06\x1dthis will be a bytes object 1\x00\x06\x1dthis will be a bytes object 2\x00\x06\x1dthis will be a bytes object 3\x00\x06\x1dthis will be a bytes object 4\x00\x06\x1dthis will be a bytes object 5\x00\x06\x1dthis will be a bytes object 6\x00\x06\x1dthis will be a bytes object 7\x00\x06\x1dthis will be a bytes object 8\x00\x06\x1dthis will be a bytes object 9\x00\n\x07\x05\rconst tuple 0\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 1\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 2\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 3\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 4\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 5\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 6\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 7\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 8\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 9\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\x82d\x10\x12\x01i@i@\x84\x18\x84\x1fT2\x00\x10\x024\x02\x16\x02T2\x01\x10\x034\x02\x16\x032\x02\x16\x042\x03\x16\x05\"\x80{\x16\x06Qc\x04\x82\x0c\x00\n\x02($$$\x11\x07\x16\x08\x10\x02\x16\t2\x00\x16\n2\x01\x16\x0b2\x02\x16\x0c2\x03\x16\rQc\x04@\t\x08\n\x81\x0b Qc@\t\x08\x0b\x81\x0b@Qc@\t\x08\x0c\x81\x0b`QcH\t\n\r\x81\x0b` Qc\x82\x14\x00\x0c\x03h`$$$\x11\x07\x16\x08\x10\x03\x16\t2\x00\x16\n2\x01\x16\x0b2\x02\x16\x0c2\x03\x16\rQc\x04H\t\n\n\x81\x0b``QcH\t\n\x0b\x81\x0b\x80\x07QcH\t\n\x0c\x81\x0b\x80\x08QcH\t\n\r\x81\x0b\x80\tQc\xa08P:\x04\x80\x0b13///---997799<\x1f%\x1f\"\x1f%)\x1f\"//\x12\x0e\x12\x0f\x12\x10\x12\x11\x12\x12\x12\x13\x12\x14*\x07Y\x12\x15\x12\x16\x12\x17\x12\x18\x12\x19\x12\x1a\x12\x08\x12\x07*\x08Y\x12\x1b\x12\x1c\x12\t\x12\x1d\x12\x1e\x12\x1f*\x06Y\x12 \x12!\x12\"\x12#\x12$\x12%*\x06Y\x12&\x12'\x12(\x12)\x12*\x12+*\x06Y\x12,\x12-\x12.\x12/\x120*\x05Y\x121\x122\x123\x124\x125*\x05Y\x126\x127\x128\x129\x12:*\x05Y\x12;\x12<\x12=\x12>\x12?\x12@\x12A\x12B\x12C\x12D\x12E*\x0bY\x12F\x12G\x12H\x12I\x12J\x12K\x12L\x12M\x12N\x12O\x12P*\x0bY\x12Q\x12R\x12S\x12T\x12U\x12V\x12W\x12X\x12Y\x12Z*\nY\x12[\x12\\\x12]\x12^\x12_\x12`\x12a\x12b\x12c\x12d*\nY\x12e\x12f\x12g\x12h\x12i\x12j\x12k\x12l\x12m\x12n\x12o*\x0bY\x12p\x12q\x12r\x12s\x12t\x12u\x12v\x12w\x12x\x12y\x12z*\x0bY\x12{\x12|\x12}\x12~\x12\x7f\x12\x81\x00\x12\x81\x01\x12\x81\x02\x12\x81\x03\x12\x81\x04*\nY\x12\x81\x05\x12\x81\x06\x12\x81\x07\x12\x81\x08\x12\x81\t\x12\x81\n\x12\x81\x0b\x12\x81\x0c\x12\x81\r\x12\x81\x0e\x12\x81\x0f*\x0bY\x12\x81\x10\x12\x81\x11\x12\x81\x12\x12\x81\x13\x12\x81\x14\x12\x81\x15\x12\x81\x16\x12\x81\x17\x12\x81\x18\x12\x81\x19*\nY\x12\x81\x1a\x12\x81\x1b\x12\x81\x1c\x12\x81\x1d\x12\x81\x1e\x12\x81\x1f\x12\x81 \x12\x81!\x12\x81\"\x12\x81#\x12\x81$*\x0bY\x12\x81%\x12\x81&*\x02Y\x12\x81'\x12\x81(\x12\x81)\x12\x81*\x12\x81+\x12\x81,\x12\x81-\x12\x81.\x12\x81/\x12\x810*\nY\x12\x811\x12\x812\x12\x813\x12\x814*\x04Y\x12\x815\x12\x816\x12\x817\x12\x818*\x04Y\x12\x819\x12\x81:\x12\x81;\x12\x81<*\x04YQc\x87p\x08@\x05\x80###############################\x00\xc0#\x01\xc0#\x02\xc0#\x03\xc0#\x04\xc0#\x05\xc0#\x06\xc0#\x07\xc0#\x08\xc0#\t\xc0#\n\xc0#\x0b\xc0#\x0c\xc0#\r\xc0#\x0e\xc0#\x0f\xc0#\x10\xc0#\x11\xc0#\x12\xc0#\x13\xc0#\x14\xc0#\x15\xc0#\x16\xc0#\x17\xc0#\x18\xc0#\x19\xc0#\x1a\xc0#\x1b\xc0#\x1c\xc0#\x1d\xc0Qc"
 
 
-class File(uio.IOBase):
+class File(IOBase):
     def __init__(self):
         self.off = 0
 
     def ioctl(self, request, arg):
-        return 0
+        if request == 4:  # MP_STREAM_CLOSE
+            return 0
+        return -1
 
     def readinto(self, buf):
         buf[:] = memoryview(file_data)[self.off : self.off + len(buf)]
@@ -102,7 +105,7 @@ def chdir(self, path):
         pass
 
     def stat(self, path):
-        if path == "__injected.mpy":
+        if path == "/__injected.mpy":
             return tuple(0 for _ in range(10))
         else:
             raise OSError(-2)  # ENOENT
@@ -112,8 +115,8 @@ def open(self, path, mode):
 
 
 def mount():
-    uos.mount(FS(), "/__remote")
-    uos.chdir("/__remote")
+    vfs.mount(FS(), "/__remote")
+    sys.path.insert(0, "/__remote")
 
 
 def test():
diff --git a/tests/perf_bench/core_locals.py b/tests/perf_bench/core_locals.py
new file mode 100644
index 00000000000..d28e078b26e
--- /dev/null
+++ b/tests/perf_bench/core_locals.py
@@ -0,0 +1,191 @@
+# This tests the performance of an instance class locals dict (importantly, that has all keys as qstrs)
+
+# These are all shorter than 10 characters, so will be interned by the parser.
+ZEN = [
+    "the",
+    "zen",
+    "of",
+    "python",
+    "beautiful",
+    "is",
+    "better",
+    "than",
+    "ugly",
+    "explicit",
+    "is",
+    "better",
+    "than",
+    "implicit",
+    "simple",
+    "is",
+    "better",
+    "than",
+    "complex",
+    "complex",
+    "is",
+    "better",
+    "than",
+    "complicate",
+    "flat",
+    "is",
+    "better",
+    "than",
+    "nested",
+    "sparse",
+    "is",
+    "better",
+    "than",
+    "dense",
+    "readabilit",
+    "counts",
+    "special",
+    "cases",
+    "arent",
+    "special",
+    "enough",
+    "to",
+    "break",
+    "the",
+    "rules",
+    "although",
+    "practicali",
+    "beats",
+    "purity",
+    "errors",
+    "should",
+    "never",
+    "pass",
+    "silently",
+    "unless",
+    "explicitly",
+    "silenced",
+    "in",
+    "the",
+    "face",
+    "of",
+    "ambiguity",
+    "refuse",
+    "the",
+    "temptation",
+    "to",
+    "guess",
+    "there",
+    "should",
+    "be",
+    "one",
+    "and",
+    "preferably",
+    "only",
+    "one",
+    "obvious",
+    "way",
+    "to",
+    "do",
+    "it",
+    "although",
+    "that",
+    "way",
+    "may",
+    "not",
+    "be",
+    "obvious",
+    "at",
+    "first",
+    "unless",
+    "youre",
+    "dutch",
+    "now",
+    "is",
+    "better",
+    "than",
+    "never",
+    "although",
+    "never",
+    "is",
+    "often",
+    "better",
+    "than",
+    "right",
+    "now",
+    "if",
+    "the",
+    "implementa",
+    "is",
+    "hard",
+    "to",
+    "explain",
+    "its",
+    "a",
+    "bad",
+    "idea",
+    "if",
+    "the",
+    "implementa",
+    "is",
+    "easy",
+    "to",
+    "explain",
+    "it",
+    "may",
+    "be",
+    "a",
+    "good",
+    "idea",
+    "namespaces",
+    "are",
+    "one",
+    "honking",
+    "great",
+    "idea",
+    "",
+    "lets",
+    "do",
+    "more",
+    "of",
+    "those",
+]
+
+
+class A:
+    pass
+
+
+def test(niter):
+    for _ in range(niter):
+        a = A()
+        for _ in range(niter):
+            for word in ZEN:
+                setattr(a, word, getattr(a, word, 0) + 1)
+
+    return (
+        getattr(a, "python"),
+        getattr(a, "is"),
+        getattr(a, "than"),
+    )
+
+
+###########################################################################
+# Benchmark interface
+
+bm_params = {
+    (32, 10): (2,),
+    (50, 10): (4,),
+    (100, 10): (8,),
+    (500, 10): (40,),
+    (1000, 10): (80,),
+    (5000, 10): (400,),
+}
+
+
+def bm_setup(params):
+    (niter,) = params
+    state = None
+
+    def run():
+        nonlocal state
+        state = test(niter)
+
+    def result():
+        return niter, state
+
+    return run, result
diff --git a/tests/perf_bench/core_str.py b/tests/perf_bench/core_str.py
new file mode 100644
index 00000000000..ca827194d6f
--- /dev/null
+++ b/tests/perf_bench/core_str.py
@@ -0,0 +1,65 @@
+# This tests string handling operations
+
+ZEN = """
+The Zen of Python
+Beautiful is better than ugly.
+Explicit is better than implicit.
+Simple is better than complex.
+Complex is better than complicated.
+Flat is better than nested.
+Sparse is better than dense.
+Readability counts.
+Special cases aren't special enough to break the rules.
+Although practicality beats purity.
+Errors should never pass silently.
+Unless explicitly silenced.
+In the face of ambiguity, refuse the temptation to guess.
+There should be one-- and preferably only one --obvious way to do it.
+Although that way may not be obvious at first unless you're Dutch.
+Now is better than never.
+Although never is often better than *right* now.
+If the implementation is hard to explain, it's a bad idea.
+If the implementation is easy to explain, it may be a good idea.
+Namespaces are one honking great idea -- let's do more of those!
+"""
+
+
+def test(niter):
+    counts = {}
+    for _ in range(niter):
+        x = ZEN.replace("\n", " ").split(" ")
+        y = " ".join(x)
+        for i in range(50):
+            a = ZEN[i : i * 2]
+            b = a + "hello world"
+        for c in ZEN:
+            i = ord(c)
+            c = chr(i)
+    return (x[0], a)
+
+
+###########################################################################
+# Benchmark interface
+
+bm_params = {
+    (32, 10): (2,),
+    (50, 10): (3,),
+    (100, 10): (6,),
+    (500, 10): (30,),
+    (1000, 10): (60,),
+    (5000, 10): (300,),
+}
+
+
+def bm_setup(params):
+    (niter,) = params
+    state = None
+
+    def run():
+        nonlocal state
+        state = test(niter)
+
+    def result():
+        return niter, state
+
+    return run, result
diff --git a/tests/ports/alif_hardware/flash_test.py b/tests/ports/alif_hardware/flash_test.py
new file mode 100644
index 00000000000..5be3fc2d02f
--- /dev/null
+++ b/tests/ports/alif_hardware/flash_test.py
@@ -0,0 +1,149 @@
+# MIT license; Copyright (c) 2024 OpenMV LLC.
+#
+# Note: this test completely erases the filesystem!
+# It is intended to be run manually.
+
+import alif
+import hashlib
+import os
+import sys
+import time
+import vfs
+
+hash_algo = "sha256"
+
+
+def flash_make_filesystem():
+    try:
+        vfs.umount("/flash")
+    except:
+        pass
+    bdev = alif.Flash()
+    vfs.VfsFat.mkfs(bdev)
+    vfs.mount(vfs.VfsFat(bdev), "/flash")
+    sys.path.append("/flash")
+    sys.path.append("/flash/lib")
+    os.chdir("/flash")
+
+
+def flash_block_test():
+    try:
+        vfs.umount("/flash")
+    except:
+        pass
+    dev = alif.Flash()
+
+    data512 = os.urandom(512)
+    buf512 = bytearray(512)
+    block_numbers = tuple(range(32)) + tuple(range(250, 266))
+
+    print("Block read/write integrity: ", end="")
+    ok = True
+    for block_n in block_numbers:
+        dev.writeblocks(block_n, data512)
+        dev.readblocks(block_n, buf512)
+        if buf512 != data512:
+            ok = False
+    print(ok)
+
+    print("Block read back integrity: ", end="")
+    ok = True
+    for block_n in block_numbers:
+        dev.readblocks(block_n, buf512)
+        if buf512 != data512:
+            ok = False
+    print(ok)
+
+    N = 16 * 1024
+    data_big = os.urandom(N)
+    t0 = time.ticks_us()
+    dev.writeblocks(0, data_big)
+    dt = time.ticks_diff(time.ticks_us(), t0)
+    print(f"Block write speed: {len(data_big) / 1024 / dt * 1_000_000} KiB/sec")
+
+    buf_big = bytearray(N)
+    t0 = time.ticks_us()
+    dev.readblocks(0, buf_big)
+    dt = time.ticks_diff(time.ticks_us(), t0)
+    print(f"Block read speed:  {len(buf_big) / 1024 / dt * 1_000_000} KiB/sec")
+
+    if buf_big != data_big:
+        raise RuntimeError("big block read-back failed")
+
+    try:
+        import uctypes
+
+        xip = memoryview(dev)
+    except:
+        xip = None
+    if xip is not None:
+        t0 = time.ticks_us()
+        buf_big[:] = xip[: len(buf_big)]
+        dt = time.ticks_diff(time.ticks_us(), t0)
+        print(f"XIP read speed:    {len(buf_big) / 1024 / dt * 1_000_000} KiB/sec")
+
+        if buf_big != data_big:
+            raise RuntimeError("XIP read-back failed")
+        for i in range(len(buf_big)):
+            if xip[i] != data_big[i]:
+                raise RuntimeError("XIP byte-wise read-back failed")
+
+
+def flash_write_verify(path, size=1024 * 1024, block_size=1024):
+    hash_sum = getattr(hashlib, hash_algo)()
+    block_count = size // block_size
+
+    print("-" * 16)
+    print(f"Writing file {size=} {block_size=}")
+    t0 = time.ticks_ms()
+    total_size = 0
+    with open(path, "wb") as file:
+        for i in range(block_count):
+            buf = os.urandom(block_size)
+            # Update digest
+            hash_sum.update(buf)
+            total_size += file.write(buf)
+            if i % (block_count // 16) == 0:
+                print(f"{i}/{block_count}", end="\r")
+    dt = time.ticks_diff(time.ticks_ms(), t0)
+    print(f"Flash write finished:       {total_size / 1024 / dt * 1000} KiB/sec")
+    digest = hash_sum.digest()
+
+    print("Reading file... ", end="")
+    hash_sum = getattr(hashlib, hash_algo)()
+    buf = bytearray(block_size)
+    t0 = time.ticks_ms()
+    total_size = 0
+    with open(path, "rb") as file:
+        for i in range(block_count):
+            total_size += file.readinto(buf)
+    dt = time.ticks_diff(time.ticks_ms(), t0)
+    print(f"finished:   {total_size / 1024 / dt * 1000} KiB/sec")
+
+    print("Verifying file... ", end="")
+    hash_sum = getattr(hashlib, hash_algo)()
+    t0 = time.ticks_ms()
+    total_size = 0
+    with open(path, "rb") as file:
+        for i in range(block_count):
+            buf = file.read(block_size)
+            total_size += len(buf)
+            # Update digest
+            hash_sum.update(buf)
+    dt = time.ticks_diff(time.ticks_ms(), t0)
+    print(f"finished: {total_size / 1024 / dt * 1000} KiB/sec; ", end="")
+
+    if digest != hash_sum.digest():
+        raise RuntimeError(f"{hash_algo} checksum verify failed")
+
+    print(f"{hash_algo} checksum verified")
+
+
+if __name__ == "__main__":
+    flash_block_test()
+    flash_make_filesystem()
+    flash_write_verify("test0.bin", size=64 * 1024, block_size=1024)
+    flash_write_verify("test1.bin", size=128 * 1024, block_size=1024)
+    flash_write_verify("test2.bin", size=64 * 1024, block_size=2048)
+    flash_write_verify("test4.bin", size=256 * 1024, block_size=4096)
+    flash_write_verify("test4.bin", size=512 * 1024, block_size=16384)
diff --git a/tests/wipy/adc.py b/tests/ports/cc3200/adc.py
similarity index 100%
rename from tests/wipy/adc.py
rename to tests/ports/cc3200/adc.py
diff --git a/tests/wipy/adc.py.exp b/tests/ports/cc3200/adc.py.exp
similarity index 100%
rename from tests/wipy/adc.py.exp
rename to tests/ports/cc3200/adc.py.exp
diff --git a/tests/wipy/i2c.py b/tests/ports/cc3200/i2c.py
similarity index 100%
rename from tests/wipy/i2c.py
rename to tests/ports/cc3200/i2c.py
diff --git a/tests/wipy/i2c.py.exp b/tests/ports/cc3200/i2c.py.exp
similarity index 100%
rename from tests/wipy/i2c.py.exp
rename to tests/ports/cc3200/i2c.py.exp
diff --git a/tests/wipy/modwipy.py b/tests/ports/cc3200/modwipy.py
similarity index 100%
rename from tests/wipy/modwipy.py
rename to tests/ports/cc3200/modwipy.py
diff --git a/tests/wipy/modwipy.py.exp b/tests/ports/cc3200/modwipy.py.exp
similarity index 100%
rename from tests/wipy/modwipy.py.exp
rename to tests/ports/cc3200/modwipy.py.exp
diff --git a/tests/wipy/os.py b/tests/ports/cc3200/os.py
similarity index 95%
rename from tests/wipy/os.py
rename to tests/ports/cc3200/os.py
index 1f4debcade5..c126dcffc4e 100644
--- a/tests/wipy/os.py
+++ b/tests/ports/cc3200/os.py
@@ -3,7 +3,7 @@
 """
 
 from machine import SD
-import os
+import os, vfs
 
 mch = os.uname().machine
 if "LaunchPad" in mch:
@@ -15,7 +15,7 @@
 
 sd = SD(pins=sd_pins)
 
-os.mount(sd, "/sd")
+vfs.mount(sd, "/sd")
 os.mkfs("/sd")
 os.chdir("/flash")
 print(os.listdir())
@@ -88,7 +88,7 @@
 os.unmount("/sd")
 print(os.listdir("/"))
 os.mkfs(sd)
-os.mount(sd, "/sd")
+vfs.mount(sd, "/sd")
 print(os.listdir("/"))
 os.chdir("/flash")
 
@@ -104,12 +104,12 @@
 print(os.listdir("/sd"))
 
 try:
-    os.mount(sd, "/sd")
+    vfs.mount(sd, "/sd")
 except:
     print("Exception")
 
 try:
-    os.mount(sd, "/sd2")
+    vfs.mount(sd, "/sd2")
 except:
     print("Exception")
 
@@ -159,6 +159,6 @@
 except:
     print("Exception")
 
-os.mount(sd, "/sd")
+vfs.mount(sd, "/sd")
 print(os.listdir("/"))
 os.unmount("/sd")
diff --git a/tests/wipy/os.py.exp b/tests/ports/cc3200/os.py.exp
similarity index 100%
rename from tests/wipy/os.py.exp
rename to tests/ports/cc3200/os.py.exp
diff --git a/tests/ports/cc3200/pin.py b/tests/ports/cc3200/pin.py
new file mode 100644
index 00000000000..d7b0fa976cd
--- /dev/null
+++ b/tests/ports/cc3200/pin.py
@@ -0,0 +1,218 @@
+"""
+This test need a set of pins which can be set as inputs and have no external
+pull up or pull down connected.
+GP12 and GP17 must be connected together
+"""
+
+from machine import Pin
+import os
+
+mch = os.uname().machine
+if "LaunchPad" in mch:
+    pin_map = [
+        "GP24",
+        "GP12",
+        "GP14",
+        "GP15",
+        "GP16",
+        "GP17",
+        "GP28",
+        "GP8",
+        "GP6",
+        "GP30",
+        "GP31",
+        "GP3",
+        "GP0",
+        "GP4",
+        "GP5",
+    ]
+    max_af_idx = 15
+elif "WiPy" in mch:
+    pin_map = [
+        "GP23",
+        "GP24",
+        "GP12",
+        "GP13",
+        "GP14",
+        "GP9",
+        "GP17",
+        "GP28",
+        "GP22",
+        "GP8",
+        "GP30",
+        "GP31",
+        "GP0",
+        "GP4",
+        "GP5",
+    ]
+    max_af_idx = 15
+else:
+    raise Exception("Board not supported!")
+
+# test initial value
+p = Pin("GP12", Pin.IN)
+Pin("GP17", Pin.OUT, value=1)
+print(p() == 1)
+Pin("GP17", Pin.OUT, value=0)
+print(p() == 0)
+
+
+def test_noinit():
+    for p in pin_map:
+        pin = Pin(p)
+        pin.value()
+
+
+def test_pin_read(pull):
+    # enable the pull resistor on all pins, then read the value
+    for p in pin_map:
+        pin = Pin(p, mode=Pin.IN, pull=pull)
+    for p in pin_map:
+        print(pin())
+
+
+def test_pin_af():
+    for p in pin_map:
+        for af in Pin(p).alt_list():
+            if af[1] <= max_af_idx:
+                Pin(p, mode=Pin.ALT, alt=af[1])
+                Pin(p, mode=Pin.ALT_OPEN_DRAIN, alt=af[1])
+
+
+# test un-initialized pins
+test_noinit()
+# test with pull-up and pull-down
+test_pin_read(Pin.PULL_UP)
+test_pin_read(Pin.PULL_DOWN)
+
+# test all constructor combinations
+pin = Pin(pin_map[0])
+pin = Pin(pin_map[0], mode=Pin.IN)
+pin = Pin(pin_map[0], mode=Pin.OUT)
+pin = Pin(pin_map[0], mode=Pin.IN, pull=Pin.PULL_DOWN)
+pin = Pin(pin_map[0], mode=Pin.IN, pull=Pin.PULL_UP)
+pin = Pin(pin_map[0], mode=Pin.OPEN_DRAIN, pull=Pin.PULL_UP)
+pin = Pin(pin_map[0], mode=Pin.OUT, pull=Pin.PULL_DOWN)
+pin = Pin(pin_map[0], mode=Pin.OUT, pull=None)
+pin = Pin(pin_map[0], mode=Pin.OUT, pull=Pin.PULL_UP)
+pin = Pin(pin_map[0], mode=Pin.OUT, pull=Pin.PULL_UP, drive=pin.LOW_POWER)
+pin = Pin(pin_map[0], mode=Pin.OUT, pull=Pin.PULL_UP, drive=pin.MED_POWER)
+pin = Pin(pin_map[0], mode=Pin.OUT, pull=Pin.PULL_UP, drive=pin.HIGH_POWER)
+pin = Pin(pin_map[0], mode=Pin.OUT, drive=pin.LOW_POWER)
+pin = Pin(pin_map[0], Pin.OUT, Pin.PULL_DOWN)
+pin = Pin(pin_map[0], Pin.ALT, Pin.PULL_UP)
+pin = Pin(pin_map[0], Pin.ALT_OPEN_DRAIN, Pin.PULL_UP)
+test_pin_af()  # try the entire af range on all pins
+
+# test pin init and printing
+pin = Pin(pin_map[0])
+pin.init(mode=Pin.IN)
+print(pin)
+pin.init(Pin.IN, Pin.PULL_DOWN)
+print(pin)
+pin.init(mode=Pin.OUT, pull=Pin.PULL_UP, drive=pin.LOW_POWER)
+print(pin)
+pin.init(mode=Pin.OUT, pull=Pin.PULL_UP, drive=pin.HIGH_POWER)
+print(pin)
+
+# test value in OUT mode
+pin = Pin(pin_map[0], mode=Pin.OUT)
+pin.value(0)
+pin.toggle()  # test toggle
+print(pin())
+pin.toggle()  # test toggle again
+print(pin())
+# test different value settings
+pin(1)
+print(pin.value())
+pin(0)
+print(pin.value())
+pin.value(1)
+print(pin())
+pin.value(0)
+print(pin())
+
+# test all getters and setters
+pin = Pin(pin_map[0], mode=Pin.OUT)
+# mode
+print(pin.mode() == Pin.OUT)
+pin.mode(Pin.IN)
+print(pin.mode() == Pin.IN)
+# pull
+pin.pull(None)
+print(pin.pull() == None)
+pin.pull(Pin.PULL_DOWN)
+print(pin.pull() == Pin.PULL_DOWN)
+# drive
+pin.drive(Pin.MED_POWER)
+print(pin.drive() == Pin.MED_POWER)
+pin.drive(Pin.HIGH_POWER)
+print(pin.drive() == Pin.HIGH_POWER)
+# id
+print(pin.id() == pin_map[0])
+
+# all the next ones MUST raise
+try:
+    pin = Pin(pin_map[0], mode=Pin.OUT, pull=Pin.PULL_UP, drive=pin.IN)  # incorrect drive value
+except Exception:
+    print("Exception")
+
+try:
+    pin = Pin(pin_map[0], mode=Pin.LOW_POWER, pull=Pin.PULL_UP)  # incorrect mode value
+except Exception:
+    print("Exception")
+
+try:
+    pin = Pin(pin_map[0], mode=Pin.IN, pull=Pin.HIGH_POWER)  # incorrect pull value
+except Exception:
+    print("Exception")
+
+try:
+    pin = Pin("A0", Pin.OUT, Pin.PULL_DOWN)  # incorrect pin id
+except Exception:
+    print("Exception")
+
+try:
+    pin = Pin(pin_map[0], Pin.IN, Pin.PULL_UP, alt=0)  # af specified in GPIO mode
+except Exception:
+    print("Exception")
+
+try:
+    pin = Pin(pin_map[0], Pin.OUT, Pin.PULL_UP, alt=7)  # af specified in GPIO mode
+except Exception:
+    print("Exception")
+
+try:
+    pin = Pin(pin_map[0], Pin.ALT, Pin.PULL_UP, alt=0)  # incorrect af
+except Exception:
+    print("Exception")
+
+try:
+    pin = Pin(pin_map[0], Pin.ALT_OPEN_DRAIN, Pin.PULL_UP, alt=-1)  # incorrect af
+except Exception:
+    print("Exception")
+
+try:
+    pin = Pin(pin_map[0], Pin.ALT_OPEN_DRAIN, Pin.PULL_UP, alt=16)  # incorrect af
+except Exception:
+    print("Exception")
+
+try:
+    pin.mode(Pin.PULL_UP)  # incorrect pin mode
+except Exception:
+    print("Exception")
+
+try:
+    pin.pull(Pin.OUT)  # incorrect pull
+except Exception:
+    print("Exception")
+
+try:
+    pin.drive(Pin.IN)  # incorrect drive strength
+except Exception:
+    print("Exception")
+
+try:
+    pin.id("ABC")  # id cannot be set
+except Exception:
+    print("Exception")
diff --git a/tests/wipy/pin.py.exp b/tests/ports/cc3200/pin.py.exp
similarity index 100%
rename from tests/wipy/pin.py.exp
rename to tests/ports/cc3200/pin.py.exp
diff --git a/tests/wipy/pin_irq.py b/tests/ports/cc3200/pin_irq.py
similarity index 100%
rename from tests/wipy/pin_irq.py
rename to tests/ports/cc3200/pin_irq.py
diff --git a/tests/wipy/pin_irq.py.exp b/tests/ports/cc3200/pin_irq.py.exp
similarity index 100%
rename from tests/wipy/pin_irq.py.exp
rename to tests/ports/cc3200/pin_irq.py.exp
diff --git a/tests/wipy/reset/reset.py b/tests/ports/cc3200/reset/reset.py
similarity index 100%
rename from tests/wipy/reset/reset.py
rename to tests/ports/cc3200/reset/reset.py
diff --git a/tests/wipy/reset/reset.py.exp b/tests/ports/cc3200/reset/reset.py.exp
similarity index 100%
rename from tests/wipy/reset/reset.py.exp
rename to tests/ports/cc3200/reset/reset.py.exp
diff --git a/tests/wipy/rtc.py b/tests/ports/cc3200/rtc.py
similarity index 100%
rename from tests/wipy/rtc.py
rename to tests/ports/cc3200/rtc.py
diff --git a/tests/wipy/rtc.py.exp b/tests/ports/cc3200/rtc.py.exp
similarity index 100%
rename from tests/wipy/rtc.py.exp
rename to tests/ports/cc3200/rtc.py.exp
diff --git a/tests/wipy/sd.py b/tests/ports/cc3200/sd.py
similarity index 100%
rename from tests/wipy/sd.py
rename to tests/ports/cc3200/sd.py
diff --git a/tests/wipy/sd.py.exp b/tests/ports/cc3200/sd.py.exp
similarity index 100%
rename from tests/wipy/sd.py.exp
rename to tests/ports/cc3200/sd.py.exp
diff --git a/tests/wipy/skipped/rtc_irq.py b/tests/ports/cc3200/skipped/rtc_irq.py
similarity index 100%
rename from tests/wipy/skipped/rtc_irq.py
rename to tests/ports/cc3200/skipped/rtc_irq.py
diff --git a/tests/wipy/skipped/rtc_irq.py.exp b/tests/ports/cc3200/skipped/rtc_irq.py.exp
similarity index 100%
rename from tests/wipy/skipped/rtc_irq.py.exp
rename to tests/ports/cc3200/skipped/rtc_irq.py.exp
diff --git a/tests/wipy/spi.py b/tests/ports/cc3200/spi.py
similarity index 100%
rename from tests/wipy/spi.py
rename to tests/ports/cc3200/spi.py
diff --git a/tests/wipy/spi.py.exp b/tests/ports/cc3200/spi.py.exp
similarity index 100%
rename from tests/wipy/spi.py.exp
rename to tests/ports/cc3200/spi.py.exp
diff --git a/tests/wipy/time.py b/tests/ports/cc3200/time.py
similarity index 100%
rename from tests/wipy/time.py
rename to tests/ports/cc3200/time.py
diff --git a/tests/wipy/time.py.exp b/tests/ports/cc3200/time.py.exp
similarity index 100%
rename from tests/wipy/time.py.exp
rename to tests/ports/cc3200/time.py.exp
diff --git a/tests/wipy/timer.py b/tests/ports/cc3200/timer.py
similarity index 100%
rename from tests/wipy/timer.py
rename to tests/ports/cc3200/timer.py
diff --git a/tests/wipy/timer.py.exp b/tests/ports/cc3200/timer.py.exp
similarity index 100%
rename from tests/wipy/timer.py.exp
rename to tests/ports/cc3200/timer.py.exp
diff --git a/tests/ports/cc3200/uart.py b/tests/ports/cc3200/uart.py
new file mode 100644
index 00000000000..c92c8041fc0
--- /dev/null
+++ b/tests/ports/cc3200/uart.py
@@ -0,0 +1,164 @@
+"""
+UART test for the CC3200 based boards.
+UART0 and UART1 must be connected together for this test to pass.
+"""
+
+from machine import UART
+from machine import Pin
+import os
+import time
+
+mch = os.uname().machine
+if "LaunchPad" in mch:
+    uart_id_range = range(0, 2)
+    uart_pins = [
+        [("GP12", "GP13"), ("GP12", "GP13", "GP7", "GP6")],
+        [("GP16", "GP17"), ("GP16", "GP17", "GP7", "GP6")],
+    ]
+elif "WiPy" in mch:
+    uart_id_range = range(0, 2)
+    uart_pins = [
+        [("GP12", "GP13"), ("GP12", "GP13", "GP7", "GP6")],
+        [("GP16", "GP17"), ("GP16", "GP17", "GP7", "GP6")],
+    ]
+else:
+    raise Exception("Board not supported!")
+
+# just in case we have the repl duplicated on any of the uarts
+os.dupterm(None)
+
+for uart_id in uart_id_range:
+    uart = UART(uart_id, 38400)
+    print(uart)
+    uart.init(57600, 8, None, 1, pins=uart_pins[uart_id][0])
+    uart.init(baudrate=9600, stop=2, parity=UART.EVEN, pins=uart_pins[uart_id][1])
+    uart.init(baudrate=115200, parity=UART.ODD, stop=0, pins=uart_pins[uart_id][0])
+    uart = UART(baudrate=1000000)
+    uart.sendbreak()
+
+uart = UART(baudrate=1000000)
+uart = UART()
+print(uart)
+uart = UART(baudrate=38400, pins=("GP12", "GP13"))
+print(uart)
+uart = UART(pins=("GP12", "GP13"))
+print(uart)
+uart = UART(pins=(None, "GP17"))
+print(uart)
+uart = UART(baudrate=57600, pins=("GP16", "GP17"))
+print(uart)
+
+# now it's time for some loopback tests between the uarts
+uart0 = UART(0, 1000000, pins=uart_pins[0][0])
+print(uart0)
+uart1 = UART(1, 1000000, pins=uart_pins[1][0])
+print(uart1)
+
+print(uart0.write(b"123456") == 6)
+print(uart1.read() == b"123456")
+
+print(uart1.write(b"123") == 3)
+print(uart0.read(1) == b"1")
+print(uart0.read(2) == b"23")
+print(uart0.read() == None)
+
+uart0.write(b"123")
+buf = bytearray(3)
+print(uart1.readinto(buf, 1) == 1)
+print(buf)
+print(uart1.readinto(buf) == 2)
+print(buf)
+
+# try initializing without the id
+uart0 = UART(baudrate=1000000, pins=uart_pins[0][0])
+uart0.write(b"1234567890")
+time.sleep_ms(2)  # because of the fifo interrupt levels
+print(uart1.any() == 10)
+print(uart1.readline() == b"1234567890")
+print(uart1.any() == 0)
+
+uart0.write(b"1234567890")
+print(uart1.read() == b"1234567890")
+
+# tx only mode
+uart0 = UART(0, 1000000, pins=("GP12", None))
+print(uart0.write(b"123456") == 6)
+print(uart1.read() == b"123456")
+print(uart1.write(b"123") == 3)
+print(uart0.read() == None)
+
+# rx only mode
+uart0 = UART(0, 1000000, pins=(None, "GP13"))
+print(uart0.write(b"123456") == 6)
+print(uart1.read() == None)
+print(uart1.write(b"123") == 3)
+print(uart0.read() == b"123")
+
+# leave pins as they were (rx only mode)
+uart0 = UART(0, 1000000, pins=None)
+print(uart0.write(b"123456") == 6)
+print(uart1.read() == None)
+print(uart1.write(b"123") == 3)
+print(uart0.read() == b"123")
+
+# no pin assignment
+uart0 = UART(0, 1000000, pins=(None, None))
+print(uart0.write(b"123456789") == 9)
+print(uart1.read() == None)
+print(uart1.write(b"123456789") == 9)
+print(uart0.read() == None)
+print(Pin.board.GP12)
+print(Pin.board.GP13)
+
+# check for memory leaks...
+for i in range(0, 1000):
+    uart0 = UART(0, 1000000)
+    uart1 = UART(1, 1000000)
+
+# next ones must raise
+try:
+    UART(0, 9600, parity=None, pins=("GP12", "GP13", "GP7"))
+except Exception:
+    print("Exception")
+
+try:
+    UART(0, 9600, parity=UART.ODD, pins=("GP12", "GP7"))
+except Exception:
+    print("Exception")
+
+uart0 = UART(0, 1000000)
+uart0.deinit()
+try:
+    uart0.any()
+except Exception:
+    print("Exception")
+
+try:
+    uart0.read()
+except Exception:
+    print("Exception")
+
+try:
+    uart0.write("abc")
+except Exception:
+    print("Exception")
+
+try:
+    uart0.sendbreak("abc")
+except Exception:
+    print("Exception")
+
+try:
+    UART(2, 9600)
+except Exception:
+    print("Exception")
+
+for uart_id in uart_id_range:
+    uart = UART(uart_id, 1000000)
+    uart.deinit()
+    # test printing an uninitialized uart
+    print(uart)
+    # initialize it back and check that it works again
+    uart.init(115200)
+    print(uart)
+    uart.read()
diff --git a/tests/wipy/uart.py.exp b/tests/ports/cc3200/uart.py.exp
similarity index 100%
rename from tests/wipy/uart.py.exp
rename to tests/ports/cc3200/uart.py.exp
diff --git a/tests/wipy/uart_irq.py b/tests/ports/cc3200/uart_irq.py
similarity index 100%
rename from tests/wipy/uart_irq.py
rename to tests/ports/cc3200/uart_irq.py
diff --git a/tests/wipy/uart_irq.py.exp b/tests/ports/cc3200/uart_irq.py.exp
similarity index 100%
rename from tests/wipy/uart_irq.py.exp
rename to tests/ports/cc3200/uart_irq.py.exp
diff --git a/tests/wipy/wdt.py b/tests/ports/cc3200/wdt.py
similarity index 100%
rename from tests/wipy/wdt.py
rename to tests/ports/cc3200/wdt.py
diff --git a/tests/wipy/wdt.py.exp b/tests/ports/cc3200/wdt.py.exp
similarity index 100%
rename from tests/wipy/wdt.py.exp
rename to tests/ports/cc3200/wdt.py.exp
diff --git a/tests/wipy/wlan/machine.py b/tests/ports/cc3200/wlan/machine.py
similarity index 100%
rename from tests/wipy/wlan/machine.py
rename to tests/ports/cc3200/wlan/machine.py
diff --git a/tests/ports/cc3200/wlan/machine.py.exp b/tests/ports/cc3200/wlan/machine.py.exp
new file mode 100644
index 00000000000..303a0633a65
--- /dev/null
+++ b/tests/ports/cc3200/wlan/machine.py.exp
@@ -0,0 +1,7 @@
+
+True
+True
+Active
+True
+True
+Exception
diff --git a/tests/wipy/wlan/server.py b/tests/ports/cc3200/wlan/server.py
similarity index 100%
rename from tests/wipy/wlan/server.py
rename to tests/ports/cc3200/wlan/server.py
diff --git a/tests/wipy/wlan/server.py.exp b/tests/ports/cc3200/wlan/server.py.exp
similarity index 100%
rename from tests/wipy/wlan/server.py.exp
rename to tests/ports/cc3200/wlan/server.py.exp
diff --git a/tests/wipy/wlan/wlan.py b/tests/ports/cc3200/wlan/wlan.py
similarity index 100%
rename from tests/wipy/wlan/wlan.py
rename to tests/ports/cc3200/wlan/wlan.py
diff --git a/tests/wipy/wlan/wlan.py.exp b/tests/ports/cc3200/wlan/wlan.py.exp
similarity index 100%
rename from tests/wipy/wlan/wlan.py.exp
rename to tests/ports/cc3200/wlan/wlan.py.exp
diff --git a/tests/esp32/check_err_str.py b/tests/ports/esp32/check_err_str.py
similarity index 79%
rename from tests/esp32/check_err_str.py
rename to tests/ports/esp32/check_err_str.py
index 055eecf7476..598db4e1e11 100644
--- a/tests/esp32/check_err_str.py
+++ b/tests/ports/esp32/check_err_str.py
@@ -1,3 +1,5 @@
+# This tests checks the behaviour of the `check_esp_err`/`check_esp_err_` C function.
+
 try:
     from esp32 import Partition as p
     import micropython
@@ -23,17 +25,19 @@
 
 # same but with out of memory condition by locking the heap
 exc = "FAILED TO RAISE"
+e = None  # preallocate entry in globals dict
 micropython.heap_lock()
 try:
     fun(part)
 except OSError as e:
     exc = e
 micropython.heap_unlock()
-print("exc:", exc)  # exc empty due to no memory
+print("exc:", repr(exc))  # exc empty due to no memory
 
 # same again but having an emergency buffer
 micropython.alloc_emergency_exception_buf(256)
 exc = "FAILED TO RAISE"
+e = None  # preallocate entry in globals dict
 micropython.heap_lock()
 try:
     fun(part)
diff --git a/tests/esp32/check_err_str.py.exp b/tests/ports/esp32/check_err_str.py.exp
similarity index 80%
rename from tests/esp32/check_err_str.py.exp
rename to tests/ports/esp32/check_err_str.py.exp
index d4162d1be00..db45d492551 100644
--- a/tests/esp32/check_err_str.py.exp
+++ b/tests/ports/esp32/check_err_str.py.exp
@@ -1,4 +1,4 @@
 [Errno 2] ENOENT
 (-5379, 'ESP_ERR_OTA_VALIDATE_FAILED')
-exc: 
+exc: OSError()
 -5379
diff --git a/tests/ports/esp32/esp32_idf_heap_info.py b/tests/ports/esp32/esp32_idf_heap_info.py
new file mode 100644
index 00000000000..91ac6cd7bc0
--- /dev/null
+++ b/tests/ports/esp32/esp32_idf_heap_info.py
@@ -0,0 +1,47 @@
+# Test the esp32's esp32.idf_heap_info to return sane data
+try:
+    import esp32
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+import sys
+
+# idf_heap_info() is expected to return at least this many
+# regions for HEAP_DATA and HEAP_EXEC, respectively.
+MIN_DATA = 3
+MIN_EXEC = 3
+
+impl = str(sys.implementation)
+if "ESP32-C2" in impl:
+    # ESP32-C2 is less fragmented (yay!) and only has two memory regions
+    MIN_DATA = 2
+    MIN_EXEC = 2
+
+
+# region tuple is: (size, free, largest free, min free)
+# we check that each region's size is > 0 and that the free amounts are smaller than the size
+def chk_heap(kind, regions):
+    chk = [True, True, True, True]
+    for r in regions:
+        chk = [
+            chk[0] and r[0] > 0,
+            chk[1] and r[1] <= r[0],
+            chk[2] and r[2] <= r[0],
+            chk[3] and r[3] <= r[0],
+        ]
+    print(kind, chk)
+
+
+# try getting heap regions
+regions = esp32.idf_heap_info(esp32.HEAP_DATA)
+print("HEAP_DATA >=MIN:", len(regions) >= MIN_DATA)
+chk_heap("HEAP_DATA", regions)
+
+# try getting code regions
+regions = esp32.idf_heap_info(esp32.HEAP_EXEC)
+print("HEAP_EXEC >=MIN:", len(regions) >= MIN_EXEC)
+chk_heap("HEAP_EXEC", regions)
+
+# try invalid param
+print(esp32.idf_heap_info(-1))
diff --git a/tests/ports/esp32/esp32_idf_heap_info.py.exp b/tests/ports/esp32/esp32_idf_heap_info.py.exp
new file mode 100644
index 00000000000..7fc50fc1624
--- /dev/null
+++ b/tests/ports/esp32/esp32_idf_heap_info.py.exp
@@ -0,0 +1,5 @@
+HEAP_DATA >=MIN: True
+HEAP_DATA [True, True, True, True]
+HEAP_EXEC >=MIN: True
+HEAP_EXEC [True, True, True, True]
+[]
diff --git a/tests/esp32/esp32_nvs.py b/tests/ports/esp32/esp32_nvs.py
similarity index 97%
rename from tests/esp32/esp32_nvs.py
rename to tests/ports/esp32/esp32_nvs.py
index fd8b152ca71..e6c308cde2f 100644
--- a/tests/esp32/esp32_nvs.py
+++ b/tests/ports/esp32/esp32_nvs.py
@@ -4,7 +4,7 @@
 
 nvs = NVS("mp-test")
 
-# test setting and gettin an integer kv
+# test setting and getting an integer kv
 nvs.set_i32("key1", 1234)
 print(nvs.get_i32("key1"))
 nvs.set_i32("key2", -503)
diff --git a/tests/esp32/esp32_nvs.py.exp b/tests/ports/esp32/esp32_nvs.py.exp
similarity index 100%
rename from tests/esp32/esp32_nvs.py.exp
rename to tests/ports/esp32/esp32_nvs.py.exp
diff --git a/tests/esp32/partition_ota.py b/tests/ports/esp32/partition_ota.py
similarity index 95%
rename from tests/esp32/partition_ota.py
rename to tests/ports/esp32/partition_ota.py
index 212fcf03385..65e2742ebb2 100644
--- a/tests/esp32/partition_ota.py
+++ b/tests/ports/esp32/partition_ota.py
@@ -22,10 +22,10 @@ def log(*args):
 
 
 # replace boot.py with the test code that will run on each reboot
-import uos
+import os
 
 try:
-    uos.rename("boot.py", "boot-orig.py")
+    os.rename("boot.py", "boot-orig.py")
 except:
     pass
 with open("boot.py", "w") as f:
@@ -74,10 +74,10 @@ def log(*args):
 elif STEP == 4:
     log("Confirming boot ok and DONE!")
     Partition.mark_app_valid_cancel_rollback()
-    import uos
-    uos.remove("step.py")
-    uos.remove("boot.py")
-    uos.rename("boot-orig.py", "boot.py")
+    import os
+    os.remove("step.py")
+    os.remove("boot.py")
+    os.rename("boot-orig.py", "boot.py")
     print("\\nSUCCESS!\\n\\x04\\x04")
 
 """
diff --git a/tests/esp32/partition_ota.py.exp b/tests/ports/esp32/partition_ota.py.exp
similarity index 100%
rename from tests/esp32/partition_ota.py.exp
rename to tests/ports/esp32/partition_ota.py.exp
diff --git a/tests/ports/psoc-edge/signal.py b/tests/ports/psoc-edge/signal.py
new file mode 100644
index 00000000000..7c62a1dd7e6
--- /dev/null
+++ b/tests/ports/psoc-edge/signal.py
@@ -0,0 +1,61 @@
+from machine import Pin, Signal
+
+# P16_0 (out) and P16_1 (in) must be connected together on the board
+
+pin_out = Pin("P16_0", mode=Pin.OUT, value=0)
+pin_in = Pin("P16_1", mode=Pin.IN)
+
+# --- Non-inverted signal wrapping an existing Pin object ---
+print("non-inverted")
+
+signal = Signal(pin_out, invert=False)
+
+signal.on()
+print("on() drives high:  ", pin_in.value() == 1)
+
+signal.off()
+print("off() drives low:  ", pin_in.value() == 0)
+
+signal.value(1)
+print("value(1) drives high: ", pin_in.value() == 1)
+
+signal.value(0)
+print("value(0) drives low:  ", pin_in.value() == 0)
+
+# value() getter returns the logical signal level
+signal.on()
+print("value() getter high:  ", signal.value() == 1)
+signal.off()
+print("value() getter low:   ", signal.value() == 0)
+
+# --- Inverted signal wrapping an existing Pin object ---
+print("inverted")
+
+signal = Signal(pin_out, invert=True)
+
+signal.on()
+print("on() drives low:   ", pin_in.value() == 0)
+
+signal.off()
+print("off() drives high: ", pin_in.value() == 1)
+
+signal.value(1)
+print("value(1) drives low:  ", pin_in.value() == 0)
+
+signal.value(0)
+print("value(0) drives high: ", pin_in.value() == 1)
+
+# value() getter returns the logical (inverted) signal level
+signal.on()
+print("value() getter high:  ", signal.value() == 1)
+signal.off()
+print("value() getter low:   ", signal.value() == 0)
+
+# --- Signal constructed directly from pin ID string ---
+print("construct from id")
+
+signal = Signal("P16_0", Pin.OUT, invert=False)
+signal.on()
+print("id construct on():  ", pin_in.value() == 1)
+signal.off()
+print("id construct off(): ", pin_in.value() == 0)
diff --git a/tests/ports/psoc-edge/signal.py.exp b/tests/ports/psoc-edge/signal.py.exp
new file mode 100644
index 00000000000..de2cae38d6a
--- /dev/null
+++ b/tests/ports/psoc-edge/signal.py.exp
@@ -0,0 +1,17 @@
+non-inverted
+on() drives high:   True
+off() drives low:   True
+value(1) drives high:  True
+value(0) drives low:   True
+value() getter high:   True
+value() getter low:    True
+inverted
+on() drives low:    True
+off() drives high:  True
+value(1) drives low:   True
+value(0) drives high:  True
+value() getter high:   True
+value() getter low:    True
+construct from id
+id construct on():   True
+id construct off():  True
diff --git a/tests/ports/psoc-edge/unique_id.py b/tests/ports/psoc-edge/unique_id.py
new file mode 100644
index 00000000000..880eebec23e
--- /dev/null
+++ b/tests/ports/psoc-edge/unique_id.py
@@ -0,0 +1,27 @@
+import machine
+
+# ---------------------------------------------------------------------------
+# machine.unique_id() contract test.
+# Verifies that unique_id is bytes, has expected 64-bit length (8 bytes),
+# is non-zero, and remains stable across repeated calls on the same boot.
+# ---------------------------------------------------------------------------
+
+print("***** Test 1: machine.unique_id() contract *****")
+uid0 = machine.unique_id()
+
+ok = True
+if not isinstance(uid0, bytes):
+    ok = False
+
+if len(uid0) != 8:
+    ok = False
+
+if not any(b != 0 for b in uid0):
+    ok = False
+
+for _ in range(5):
+    if machine.unique_id() != uid0:
+        ok = False
+        break
+
+print("PASS" if ok else "FAIL")
diff --git a/tests/ports/psoc-edge/unique_id.py.exp b/tests/ports/psoc-edge/unique_id.py.exp
new file mode 100644
index 00000000000..dea1c56b073
--- /dev/null
+++ b/tests/ports/psoc-edge/unique_id.py.exp
@@ -0,0 +1,2 @@
+***** Test 1: machine.unique_id() contract *****
+PASS
diff --git a/tests/ports/qemu/asm_test.py b/tests/ports/qemu/asm_test.py
new file mode 100644
index 00000000000..ab5ce6905fd
--- /dev/null
+++ b/tests/ports/qemu/asm_test.py
@@ -0,0 +1,13 @@
+try:
+    import frozen_asm_thumb as frozen_asm
+except ImportError:
+    try:
+        import frozen_asm_rv32 as frozen_asm
+    except ImportError:
+        raise ImportError
+
+print(frozen_asm.asm_add(1, 2))
+print(frozen_asm.asm_add1(3))
+print(frozen_asm.asm_cast_bool(0), frozen_asm.asm_cast_bool(3))
+print(frozen_asm.asm_shift_int(4))
+print(frozen_asm.asm_shift_uint(4))
diff --git a/tests/qemu-arm/asm_test.py.exp b/tests/ports/qemu/asm_test.py.exp
similarity index 100%
rename from tests/qemu-arm/asm_test.py.exp
rename to tests/ports/qemu/asm_test.py.exp
diff --git a/tests/ports/qemu/mpy_arch_flags_test.py b/tests/ports/qemu/mpy_arch_flags_test.py
new file mode 100644
index 00000000000..c07d92d533d
--- /dev/null
+++ b/tests/ports/qemu/mpy_arch_flags_test.py
@@ -0,0 +1,128 @@
+try:
+    import sys, io, vfs
+
+    sys.implementation._mpy
+    io.IOBase
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+sys_implementation_mpy = sys.implementation._mpy
+arch = (sys_implementation_mpy >> 10) & 0x0F
+
+# RV32-only for now.
+if arch != 11:
+    print("SKIP")
+    raise SystemExit
+
+
+class UserFile(io.IOBase):
+    def __init__(self, data):
+        self.data = memoryview(data)
+        self.pos = 0
+
+    def readinto(self, buf):
+        n = min(len(buf), len(self.data) - self.pos)
+        buf[:n] = self.data[self.pos : self.pos + n]
+        self.pos += n
+        return n
+
+    def ioctl(self, req, arg):
+        if req == 4:  # MP_STREAM_CLOSE
+            return 0
+        return -1
+
+
+class UserFS:
+    def __init__(self, files):
+        self.files = files
+
+    def mount(self, readonly, mksfs):
+        pass
+
+    def umount(self):
+        pass
+
+    def stat(self, path):
+        if path in self.files:
+            return (32768, 0, 0, 0, 0, 0, 0, 0, 0, 0)
+        raise OSError
+
+    def open(self, path, mode):
+        return UserFile(self.files[path])
+
+
+def mp_encode_uint(val):
+    encoded = [val & 0x7F]
+    val >>= 7
+    while val != 0:
+        encoded.insert(0, 0x80 | (val & 0x7F))
+        val >>= 7
+    return bytes(encoded)
+
+
+def add_flags(module, flags):
+    output = bytearray()
+    output.extend(module[:4])
+    output[2] |= 0x40
+    output.extend(mp_encode_uint(flags))
+    output.extend(module[4:])
+    return output
+
+
+arch_flags = sys_implementation_mpy >> 16
+can_test_partial_flags = arch_flags not in [1 << i for i in range(16)]
+
+# To recreate this string run:
+# echo "hello=1" | mpy-cross -s module.py - | python -c 'import sys; print(sys.stdin.buffer.read())'
+MPY_FILE_BASE = b"M\x06\x00\x1f\x03\x00\x12module.py\x00\x0f\nhello\x00@\x00\x02\x01\x81\x16\x02Qc"
+
+user_files = {
+    "/matching_flags.mpy": add_flags(MPY_FILE_BASE, arch_flags),
+    "/invalid_flags.mpy": add_flags(MPY_FILE_BASE, (~arch_flags & 0xFFFF)),
+    "/explicit_no_flags.mpy": add_flags(MPY_FILE_BASE, 0),
+}
+if can_test_partial_flags:
+    user_files["/partial_flags_more.mpy"] = add_flags(
+        MPY_FILE_BASE, (arch_flags | (arch_flags << 1)) & 0xFFFF
+    )
+    user_files["/partial_flags_less.mpy"] = add_flags(
+        MPY_FILE_BASE, (arch_flags >> 1) & arch_flags
+    )
+
+vfs.mount(UserFS(user_files), "/userfs")
+sys.path.append("/userfs")
+
+import matching_flags
+
+if not hasattr(matching_flags, "hello"):
+    print("Improper matching_flags import")
+
+import explicit_no_flags
+
+if not hasattr(explicit_no_flags, "hello"):
+    print("Improper explicit_no_flags import")
+
+try:
+    import invalid_flags
+
+    print("Unexpected invalid_flags import")
+except ValueError as e:
+    if str(e) != "incompatible .mpy file":
+        print("Unexpected invalid_flags import error:", str(e))
+
+if can_test_partial_flags:
+    import partial_flags_less
+
+    if not hasattr(partial_flags_less, "hello"):
+        print("Improper partial_flags_less import")
+
+    try:
+        import partial_flags_more
+
+        print("Unexpected partial_flags_more import")
+    except ValueError as e:
+        if str(e) != "incompatible .mpy file":
+            print("Unexpected partial_flags_more import error:", str(e))
+
+print("OK")
diff --git a/tests/extmod/utimeq_stable.py.exp b/tests/ports/qemu/mpy_arch_flags_test.py.exp
similarity index 100%
rename from tests/extmod/utimeq_stable.py.exp
rename to tests/ports/qemu/mpy_arch_flags_test.py.exp
diff --git a/tests/qemu-arm/native_test.py b/tests/ports/qemu/native_test.py
similarity index 100%
rename from tests/qemu-arm/native_test.py
rename to tests/ports/qemu/native_test.py
diff --git a/tests/qemu-arm/native_test.py.exp b/tests/ports/qemu/native_test.py.exp
similarity index 100%
rename from tests/qemu-arm/native_test.py.exp
rename to tests/ports/qemu/native_test.py.exp
diff --git a/tests/ports/qemu/romfs_test.py b/tests/ports/qemu/romfs_test.py
new file mode 100644
index 00000000000..dce847d844f
--- /dev/null
+++ b/tests/ports/qemu/romfs_test.py
@@ -0,0 +1,55 @@
+try:
+    import binascii, os
+
+    binascii.crc32
+    os.listdir
+except (ImportError, AttributeError):
+    print("SKIP")
+    raise SystemExit
+
+TEST_FILES = [
+    "0x30d83fe5.bin",
+    "0x37bef0eb.bin",
+    "0x442f3b5f.bin",
+    "0x648793fb.bin",
+    "0x913837b6.bin",
+    "0xdb14aac7.bin",
+]
+
+
+def is_test_partition():
+    # Make sure a ROMFS partition exists first and that it is the one used for
+    # CI tests, otherwise skip the test.
+
+    try:
+        romfs_files = sorted(os.listdir("/rom"))
+    except OSError as e:
+        if "ENODEV" in str(e):
+            return False, None
+        raise OSError(e)
+
+    # os.path.exists isn't available so we have to be a bit more creative.
+    if "romfs_sentinel.txt" not in romfs_files:
+        return False, None
+
+    try:
+        with open("/rom/romfs_sentinel.txt", "rt") as s:
+            data = s.read(128)
+            return data.startswith("*MPY-ROMFS-TEST-PARTITION*\n"), romfs_files
+    except OSError as e:
+        if "ENOENT" in str(e):
+            return False, None
+        raise OSError(e)
+
+
+valid_partition, romfs_files = is_test_partition()
+if not valid_partition:
+    print("SKIP")
+    raise SystemExit
+
+# The last entry is the CI romfs partition tag.
+
+print(TEST_FILES == romfs_files[:-1])
+for f in TEST_FILES:
+    with open("/rom/" + f, "rb") as h:
+        print(hex(binascii.crc32(h.read())) == f[:-4])
diff --git a/tests/ports/qemu/romfs_test.py.exp b/tests/ports/qemu/romfs_test.py.exp
new file mode 100644
index 00000000000..672e08f95cc
--- /dev/null
+++ b/tests/ports/qemu/romfs_test.py.exp
@@ -0,0 +1,7 @@
+True
+True
+True
+True
+True
+True
+True
diff --git a/tests/qemu-arm/viper_test.py b/tests/ports/qemu/viper_test.py
similarity index 100%
rename from tests/qemu-arm/viper_test.py
rename to tests/ports/qemu/viper_test.py
diff --git a/tests/qemu-arm/viper_test.py.exp b/tests/ports/qemu/viper_test.py.exp
similarity index 100%
rename from tests/qemu-arm/viper_test.py.exp
rename to tests/ports/qemu/viper_test.py.exp
diff --git a/tests/renesas-ra/freq.py b/tests/ports/renesas-ra/freq.py
similarity index 98%
rename from tests/renesas-ra/freq.py
rename to tests/ports/renesas-ra/freq.py
index 6ce1871f8c1..97fe946b72a 100644
--- a/tests/renesas-ra/freq.py
+++ b/tests/ports/renesas-ra/freq.py
@@ -20,7 +20,7 @@
 
 try:
     import machine
-except:
+except ImportError:
     print("machine module is not found")
     raise SystemExit
 
diff --git a/tests/renesas-ra/freq.py.exp b/tests/ports/renesas-ra/freq.py.exp
similarity index 100%
rename from tests/renesas-ra/freq.py.exp
rename to tests/ports/renesas-ra/freq.py.exp
diff --git a/tests/renesas-ra/i2c.py b/tests/ports/renesas-ra/i2c.py
similarity index 100%
rename from tests/renesas-ra/i2c.py
rename to tests/ports/renesas-ra/i2c.py
diff --git a/tests/renesas-ra/i2c.py.exp b/tests/ports/renesas-ra/i2c.py.exp
similarity index 100%
rename from tests/renesas-ra/i2c.py.exp
rename to tests/ports/renesas-ra/i2c.py.exp
diff --git a/tests/pyb/modtime.py b/tests/ports/renesas-ra/modtime.py
similarity index 100%
rename from tests/pyb/modtime.py
rename to tests/ports/renesas-ra/modtime.py
diff --git a/tests/pyb/modtime.py.exp b/tests/ports/renesas-ra/modtime.py.exp
similarity index 100%
rename from tests/pyb/modtime.py.exp
rename to tests/ports/renesas-ra/modtime.py.exp
diff --git a/tests/renesas-ra/pin.py b/tests/ports/renesas-ra/pin.py
similarity index 100%
rename from tests/renesas-ra/pin.py
rename to tests/ports/renesas-ra/pin.py
diff --git a/tests/renesas-ra/pin.py.exp b/tests/ports/renesas-ra/pin.py.exp
similarity index 100%
rename from tests/renesas-ra/pin.py.exp
rename to tests/ports/renesas-ra/pin.py.exp
diff --git a/tests/renesas-ra/rtc_init.py b/tests/ports/renesas-ra/rtc_init.py
similarity index 100%
rename from tests/renesas-ra/rtc_init.py
rename to tests/ports/renesas-ra/rtc_init.py
diff --git a/tests/renesas-ra/rtc_init.py.exp b/tests/ports/renesas-ra/rtc_init.py.exp
similarity index 100%
rename from tests/renesas-ra/rtc_init.py.exp
rename to tests/ports/renesas-ra/rtc_init.py.exp
diff --git a/tests/renesas-ra/spi.py b/tests/ports/renesas-ra/spi.py
similarity index 100%
rename from tests/renesas-ra/spi.py
rename to tests/ports/renesas-ra/spi.py
diff --git a/tests/renesas-ra/spi.py.exp b/tests/ports/renesas-ra/spi.py.exp
similarity index 100%
rename from tests/renesas-ra/spi.py.exp
rename to tests/ports/renesas-ra/spi.py.exp
diff --git a/tests/renesas-ra/uart1.py b/tests/ports/renesas-ra/uart1.py
similarity index 100%
rename from tests/renesas-ra/uart1.py
rename to tests/ports/renesas-ra/uart1.py
diff --git a/tests/renesas-ra/uart1.py.exp b/tests/ports/renesas-ra/uart1.py.exp
similarity index 100%
rename from tests/renesas-ra/uart1.py.exp
rename to tests/ports/renesas-ra/uart1.py.exp
diff --git a/tests/ports/rp2/rp2_dma.py b/tests/ports/rp2/rp2_dma.py
new file mode 100644
index 00000000000..436e5ee48ec
--- /dev/null
+++ b/tests/ports/rp2/rp2_dma.py
@@ -0,0 +1,152 @@
+# Test rp2.DMA functionality.
+
+import sys
+import time
+import machine
+import rp2
+import unittest
+
+is_rp2350 = "RP2350" in sys.implementation._machine
+
+SRC = bytes(i & 0xFF for i in range(16 * 1024))
+
+
+class Test(unittest.TestCase):
+    def setUp(self):
+        self.dma = rp2.DMA()
+
+    def tearDown(self):
+        self.dma.close()
+
+    def test_printing(self):
+        dma = self.dma
+        self.assertEqual(str(dma), "DMA({})".format(dma.channel))
+
+    def test_pack_unpack_ctrl(self):
+        dma = self.dma
+        ctrl_dict = rp2.DMA.unpack_ctrl(dma.pack_ctrl())
+        if is_rp2350:
+            self.assertEqual(len(ctrl_dict), 18)
+            self.assertTrue("inc_read_rev" in ctrl_dict)
+            self.assertTrue("inc_write_rev" in ctrl_dict)
+        else:
+            self.assertEqual(len(ctrl_dict), 16)
+        self.assertEqual(ctrl_dict["ahb_err"], 0)
+        self.assertEqual(ctrl_dict["bswap"], 0)
+        self.assertEqual(ctrl_dict["busy"], 0)
+        self.assertEqual(ctrl_dict["chain_to"], dma.channel)
+        self.assertEqual(ctrl_dict["enable"], 1)
+        self.assertEqual(ctrl_dict["high_pri"], 0)
+        self.assertEqual(ctrl_dict["inc_read"], 1)
+        self.assertEqual(ctrl_dict["inc_write"], 1)
+        self.assertEqual(ctrl_dict["irq_quiet"], 1)
+        self.assertEqual(ctrl_dict["read_err"], 0)
+        self.assertEqual(ctrl_dict["ring_sel"], 0)
+        self.assertEqual(ctrl_dict["ring_size"], 0)
+        self.assertEqual(ctrl_dict["size"], 2)
+        self.assertEqual(ctrl_dict["sniff_en"], 0)
+        self.assertEqual(ctrl_dict["treq_sel"], 63)
+        self.assertEqual(ctrl_dict["write_err"], 0)
+
+    def test_register_access(self):
+        dma = self.dma
+        dma.read = 0
+        dma.write = 0
+        dma.count = 0
+        dma.ctrl = dma.pack_ctrl()
+        self.assertEqual(dma.read, 0)
+        self.assertEqual(dma.write, 0)
+        self.assertEqual(dma.count, 0)
+        self.assertEqual(dma.ctrl & 0x01F, 25)
+        self.assertIn(dma.channel, range(16))
+        self.assertIsInstance(dma.registers, memoryview)
+
+    def test_close(self):
+        dma = self.dma
+        dma.close()
+
+        # Test closing when already closed.
+        dma.close()
+
+        # Test using when closed.
+        with self.assertRaises(ValueError):
+            dma.active()
+
+    def test_simple_memory_copy(self):
+        dma = self.dma
+        dest = bytearray(1024)
+        dma.config(read=SRC, write=dest, count=len(dest) // 4, ctrl=dma.pack_ctrl(), trigger=False)
+        self.assertFalse(any(dest))
+        dma.active(True)
+        while dma.active():
+            pass
+        self.assertEqual(dest[:8], SRC[:8])
+        self.assertEqual(dest[-8:], SRC[-8:])
+
+    def test_time_taken_for_large_memory_copy(self):
+        def run_and_time_dma(dma):
+            ticks_us = time.ticks_us
+            active = dma.active
+            irq_state = machine.disable_irq()
+            t0 = ticks_us()
+            active(True)
+            while active():
+                pass
+            t1 = ticks_us()
+            machine.enable_irq(irq_state)
+            return time.ticks_diff(t1, t0)
+
+        num_average = 10
+        dt_sum = 0
+        for _ in range(num_average):
+            dma = self.dma
+            dest = bytearray(16 * 1024)
+            dma.read = SRC
+            dma.write = dest
+            dma.count = len(dest) // 4
+            dma.ctrl = dma.pack_ctrl()
+            dt_sum += run_and_time_dma(dma)
+            self.assertEqual(dest[:8], SRC[:8])
+            self.assertEqual(dest[-8:], SRC[-8:])
+            self.tearDown()
+            self.setUp()
+        dt = dt_sum // num_average
+        self.assertIn(dt, range(30, 80))
+
+    def test_config_trigger(self):
+        # Test using .config(trigger=True) to start DMA immediately.
+        dma = self.dma
+        dest = bytearray(1024)
+        dma.config(read=SRC, write=dest, count=len(dest) // 4, ctrl=dma.pack_ctrl(), trigger=True)
+        while dma.active():
+            pass
+        self.assertEqual(dest[:8], SRC[:8])
+        self.assertEqual(dest[-8:], SRC[-8:])
+
+    def test_irq(self):
+        def callback(dma):
+            nonlocal irq_flags
+            print("irq fired")
+            irq_flags = dma.irq().flags()
+
+        dma = self.dma
+        irq_flags = None
+        dest = bytearray(1024)
+        dma.irq(callback)
+        dma.config(
+            read=SRC,
+            write=dest,
+            count=len(dest) // 4,
+            ctrl=dma.pack_ctrl(irq_quiet=0),
+            trigger=True,
+        )
+        while dma.active():
+            pass
+        time.sleep_ms(1)  # when running as native code, give the scheduler a chance to run
+        self.assertEqual(irq_flags, 1)
+        self.assertEqual(dest[:8], SRC[:8])
+        self.assertEqual(dest[-8:], SRC[-8:])
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/ports/rp2/rp2_dma_timer.py b/tests/ports/rp2/rp2_dma_timer.py
new file mode 100644
index 00000000000..f80981795c1
--- /dev/null
+++ b/tests/ports/rp2/rp2_dma_timer.py
@@ -0,0 +1,84 @@
+# Test rp2.DMATimer functionality.
+
+import time
+import machine
+import rp2
+import unittest
+
+K = 16
+
+SRC = bytes(i & 0xFF for i in range(K << 10))
+
+
+class Test(unittest.TestCase):
+    def setUp(self):
+        self.timer = rp2.DMATimer(0)
+        self.dma = rp2.DMA()
+
+    def tearDown(self):
+        self.dma.close()
+        self.timer.close()
+
+    def test_printing(self):
+        timer = self.timer
+        self.assertEqual(str(timer), "DMATimer(0)")
+
+    def test_set_ratio_unity(self):
+        timer = self.timer
+        timer.ratio = (1, 1)
+        self.assertEqual(timer.ratio, (1, 1))
+
+    def test_set_ratio(self):
+        timer = self.timer
+        timer.ratio = (1, 1)
+        base_clock = timer.freq
+        num, den = (47, 97)
+        target = base_clock * num // den
+        timer.ratio = (num, den)
+        self.assertEqual(timer.freq, target)
+
+    def test_set_freq(self):
+        timer = self.timer
+        timer.ratio = (1, 1)
+        base_clock = timer.freq
+        num, den = (53, 101)
+        target = base_clock * num // den
+        timer.freq = target
+        self.assertEqual(timer.ratio, (num, den))
+
+    def test_speed_throttle(self):
+        def time_dma(d):
+            start = time.ticks_us()
+            d.active(1)
+            while d.active():
+                pass
+            end = time.ticks_us()
+            return time.ticks_diff(end, start)
+
+        speeds = [5000 * (10**i) for i in range(4)]
+        dma = self.dma
+        timer = self.timer
+        ctrl = dma.pack_ctrl(treq_sel=timer)
+        dest = bytearray(2048)
+
+        times = []
+        deltas = []
+
+        for i, speed in enumerate(speeds):
+            timer.freq = speed
+            dma.config(read=SRC, write=dest, count=len(dest) // 4, ctrl=ctrl)
+            t = time_dma(dma)
+            times.append(t)
+            if i > 0:
+                deltas.append(times[i - 1] - t)
+
+        # The ratio between the first two times should be very close to 10
+        self.assertIn(times[0] * 10 // times[1], range(95, 105))
+
+        # The ratios between successive deltas (which removes the overhead) should also be about 10
+        for i in range(len(deltas) - 1):
+            self.assertIn(deltas[i] * 10 // deltas[i + 1], range(95, 105))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/ports/rp2/rp2_lightsleep.py b/tests/ports/rp2/rp2_lightsleep.py
new file mode 100644
index 00000000000..d11757cb671
--- /dev/null
+++ b/tests/ports/rp2/rp2_lightsleep.py
@@ -0,0 +1,41 @@
+# This test is mostly intended for ensuring USB serial stays stable over
+# lightsleep, but can double as a general lightsleep test.
+#
+# In theory this should run on any port, but native USB REPL doesn't currently
+# recover automatically on all ports. On pyboard and ESP32-S3 the host needs to
+# send something to the port before it responds again. Possibly the same for other
+# ports with native USB.
+#
+# A range of sleep periods (1 to 512ms) are tested. The total nominal sleep time
+# is 10.23 seconds, but on most ports this will finish much earlier as interrupts
+# happen before each timeout expires.
+import sys
+
+try:
+    from machine import lightsleep, Pin
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+try:
+    led = Pin(Pin.board.LED, Pin.OUT)
+except AttributeError:
+    led = None
+
+for n in range(100):
+    if led:
+        led.toggle()
+    sys.stdout.write(chr(ord("a") + (n % 26)))
+    lightsleep(2 ** (n % 10))
+
+old_freq = machine.freq()
+machine.freq(50_000_000)
+print("\n{}".format(machine.freq()))
+lightsleep(500)
+print(machine.freq())
+machine.freq(51_000_000, 48_000_000)
+print(machine.freq())
+lightsleep(500)
+print(machine.freq())
+machine.freq(old_freq)
+
+print("DONE")
diff --git a/tests/ports/rp2/rp2_lightsleep.py.exp b/tests/ports/rp2/rp2_lightsleep.py.exp
new file mode 100644
index 00000000000..a7626179e0a
--- /dev/null
+++ b/tests/ports/rp2/rp2_lightsleep.py.exp
@@ -0,0 +1,6 @@
+abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv
+50000000
+50000000
+51000000
+51000000
+DONE
diff --git a/tests/ports/rp2/rp2_lightsleep_regs.py b/tests/ports/rp2/rp2_lightsleep_regs.py
new file mode 100644
index 00000000000..4a833d0a9c3
--- /dev/null
+++ b/tests/ports/rp2/rp2_lightsleep_regs.py
@@ -0,0 +1,58 @@
+# Test that SLEEP_ENx registers are preserved over a call to machine.lightsleep().
+
+import sys
+from machine import mem32, lightsleep
+import unittest
+
+is_rp2350 = "RP2350" in sys.implementation._machine
+
+if is_rp2350:
+    CLOCK_BASE = 0x40010000
+    SLEEP_EN0 = CLOCK_BASE + 0xB4
+    SLEEP_EN1 = CLOCK_BASE + 0xB8
+    TO_DISABLE_EN0 = 1 << 30  # SHA256
+    TO_DISABLE_EN1 = 1 << 4  # SRAM0
+else:
+    CLOCK_BASE = 0x40008000
+    SLEEP_EN0 = CLOCK_BASE + 0xA8
+    SLEEP_EN1 = CLOCK_BASE + 0xAC
+    TO_DISABLE_EN0 = 1 << 28  # SRAM0
+    TO_DISABLE_EN1 = 1 << 0  # SRAM4
+
+
+class Test(unittest.TestCase):
+    def setUp(self):
+        self.orig_sleep_en0 = mem32[SLEEP_EN0]
+        self.orig_sleep_en1 = mem32[SLEEP_EN1]
+
+    def tearDown(self):
+        mem32[SLEEP_EN0] = self.orig_sleep_en0
+        mem32[SLEEP_EN1] = self.orig_sleep_en1
+
+    def test_sleep_en_regs(self):
+        print()
+
+        # Disable some bits so the registers aren't just 0xffff.
+        mem32[SLEEP_EN0] &= ~TO_DISABLE_EN0
+        mem32[SLEEP_EN1] &= ~TO_DISABLE_EN1
+
+        # Get the registers before the lightsleep.
+        sleep_en0_before = mem32[SLEEP_EN0] & 0xFFFFFFFF
+        sleep_en1_before = mem32[SLEEP_EN1] & 0xFFFFFFFF
+        print(hex(sleep_en0_before), hex(sleep_en1_before))
+
+        # Do a lightsleep.
+        lightsleep(100)
+
+        # Get the registers after a lightsleep.
+        sleep_en0_after = mem32[SLEEP_EN0] & 0xFFFFFFFF
+        sleep_en1_after = mem32[SLEEP_EN1] & 0xFFFFFFFF
+        print(hex(sleep_en0_after), hex(sleep_en1_after))
+
+        # Check the registers have not changed.
+        self.assertEqual(sleep_en0_before, sleep_en0_after)
+        self.assertEqual(sleep_en1_before, sleep_en1_after)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/ports/rp2/rp2_lightsleep_thread.py b/tests/ports/rp2/rp2_lightsleep_thread.py
new file mode 100644
index 00000000000..90db6c4a5f8
--- /dev/null
+++ b/tests/ports/rp2/rp2_lightsleep_thread.py
@@ -0,0 +1,71 @@
+# Verify that a thread running on CPU1 can go to lightsleep
+# and wake up in the expected timeframe
+import _thread
+import time
+import unittest
+from machine import lightsleep
+
+N_SLEEPS = 5
+SLEEP_MS = 250
+
+IDEAL_RUNTIME = N_SLEEPS * SLEEP_MS
+MIN_RUNTIME = 2 * SLEEP_MS
+MAX_RUNTIME = (N_SLEEPS + 1) * SLEEP_MS
+MAX_DELTA = 20
+
+
+class LightSleepInThread(unittest.TestCase):
+    def thread_entry(self, is_thread=True):
+        for _ in range(N_SLEEPS):
+            lightsleep(SLEEP_MS)
+        if is_thread:
+            self.thread_done = True
+
+    def elapsed_ms(self):
+        return time.ticks_diff(time.ticks_ms(), self.t0)
+
+    def setUp(self):
+        self.thread_done = False
+        self.t0 = time.ticks_ms()
+
+    def test_cpu0_busy(self):
+        _thread.start_new_thread(self.thread_entry, ())
+        # CPU0 is busy-waiting not asleep itself
+        while not self.thread_done:
+            self.assertLessEqual(self.elapsed_ms(), MAX_RUNTIME)
+        self.assertAlmostEqual(self.elapsed_ms(), IDEAL_RUNTIME, delta=MAX_DELTA)
+
+    def test_cpu0_sleeping(self):
+        _thread.start_new_thread(self.thread_entry, ())
+        time.sleep_ms(MAX_RUNTIME)
+        self.assertTrue(self.thread_done)
+        self.assertAlmostEqual(self.elapsed_ms(), MAX_RUNTIME, delta=MAX_DELTA)
+
+    def test_cpu0_also_lightsleep(self):
+        _thread.start_new_thread(self.thread_entry, ())
+        time.sleep_ms(50)  # account for any delay in starting the thread
+        self.thread_entry(False)  # does the same lightsleep loop, doesn't set the done flag
+        while not self.thread_done:
+            time.sleep_ms(10)
+        #
+        # Only one thread can actually be in lightsleep at a time to avoid
+        # races, but otherwise the behaviour when both threads call lightsleep()
+        # is unspecified.
+        #
+        # Currently, the other thread will return immediately if one is already
+        # in lightsleep doing set up, or waking up.  When a thread is actually in the
+        # sleep section of lightsleep, the CPU clock is stopped and that also stops
+        # the other thread.  It's possible for the total sleep time of this test to
+        # be less than IDEAL_RUNTIME due to the order in which each thread gets to go
+        # to sleep first and whether the other thread is paused before or after it
+        # tries to enter lightsleep itself.
+        #
+        # Note this test case is really only here to ensure that the rp2 hasn't
+        # hung or failed to sleep at all - not to verify any correct behaviour
+        # when there's a race to call lightsleep().
+        self.assertGreaterEqual(self.elapsed_ms(), MIN_RUNTIME - MAX_DELTA)
+        self.assertLessEqual(self.elapsed_ms(), IDEAL_RUNTIME * 2 + MAX_DELTA)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/ports/rp2/rp2_machine_idle.py b/tests/ports/rp2/rp2_machine_idle.py
new file mode 100644
index 00000000000..f9c28284782
--- /dev/null
+++ b/tests/ports/rp2/rp2_machine_idle.py
@@ -0,0 +1,36 @@
+import machine
+import time
+
+# Verify that machine.idle() resumes execution within 0.1 and 1.1ms (should be
+# 1ms max but allowing for some overhead).
+#
+# (A minimum sleep time for machine.idle() isn't specified but in a system like
+# this with no active interrupts then we should expect some idle time before
+# resuming. If it's consistently resuming immediately then that indicates a bug
+# is preventing proper idle.)
+#
+# This test doesn't contain any rp2-specific code, but rp2 is currently the only
+# tickless port - which is what led to the bug this is a regression test for.
+# Some other ports (unix, esp32) have idle behaviour that resumes immediately on
+# a quiet system, so this test is also less useful for those.
+#
+# Verification uses the average idle time, as individual iterations will always
+# have outliers due to interrupts, scheduler, etc.
+
+ITERATIONS = 500
+total = 0
+
+for _ in range(ITERATIONS):
+    before = time.ticks_us()
+    machine.idle()
+    total += time.ticks_diff(time.ticks_us(), before)
+
+total /= 1000  # us to ms
+average = total / ITERATIONS
+
+# print(f"Total {total}ms average {average}ms")  # uncomment for debug
+
+if 0.1 < average < 1.1:
+    print("PASS")
+else:
+    print(f"Total {total}ms average {average}ms, out of spec")
diff --git a/tests/ports/rp2/rp2_machine_idle.py.exp b/tests/ports/rp2/rp2_machine_idle.py.exp
new file mode 100644
index 00000000000..7ef22e9a431
--- /dev/null
+++ b/tests/ports/rp2/rp2_machine_idle.py.exp
@@ -0,0 +1 @@
+PASS
diff --git a/tests/ports/rp2/rp2_thread_reset_part1.py b/tests/ports/rp2/rp2_thread_reset_part1.py
new file mode 100644
index 00000000000..d43868113f5
--- /dev/null
+++ b/tests/ports/rp2/rp2_thread_reset_part1.py
@@ -0,0 +1,18 @@
+# This is a regression test for https://github.com/micropython/micropython/issues/16619
+# it runs in two parts by necessity:
+#
+# - This "part1" creates a non-terminating thread.
+# - The test runner issues a soft reset, which will terminate that thread.
+# - "part2" is the actual test, which is whether flash access works correctly
+#   after the thread was terminated by soft reset.
+
+import _thread
+
+
+def infinite():
+    while True:
+        pass
+
+
+_thread.start_new_thread(infinite, ())
+print("Part 1 complete...")
diff --git a/tests/ports/rp2/rp2_thread_reset_part1.py.exp b/tests/ports/rp2/rp2_thread_reset_part1.py.exp
new file mode 100644
index 00000000000..48ea7efad97
--- /dev/null
+++ b/tests/ports/rp2/rp2_thread_reset_part1.py.exp
@@ -0,0 +1 @@
+Part 1 complete...
diff --git a/tests/ports/rp2/rp2_thread_reset_part2.py b/tests/ports/rp2/rp2_thread_reset_part2.py
new file mode 100644
index 00000000000..15f0eaab8f8
--- /dev/null
+++ b/tests/ports/rp2/rp2_thread_reset_part2.py
@@ -0,0 +1,12 @@
+# This is part2 of a two-part regression test, see part1
+# for details of what's expected.
+import os
+
+FILENAME = "/rp2_thread_reset_test.txt"
+
+print("Starting")
+with open(FILENAME, "w") as f:
+    f.write("test")
+print("Written")
+os.unlink(FILENAME)
+print("Removed")
diff --git a/tests/ports/rp2/rp2_thread_reset_part2.py.exp b/tests/ports/rp2/rp2_thread_reset_part2.py.exp
new file mode 100644
index 00000000000..d7581c7d260
--- /dev/null
+++ b/tests/ports/rp2/rp2_thread_reset_part2.py.exp
@@ -0,0 +1,3 @@
+Starting
+Written
+Removed
diff --git a/tests/ports/rp2/rp2_uart.py b/tests/ports/rp2/rp2_uart.py
new file mode 100644
index 00000000000..da885993447
--- /dev/null
+++ b/tests/ports/rp2/rp2_uart.py
@@ -0,0 +1,10 @@
+# Test construction of machine.UART objects.
+
+import sys
+from machine import UART
+
+print(UART(0, tx=0, rx=1))
+
+if "RP2350" in sys.implementation._machine:
+    # Test that UART can be constructed using other tx/rx pins.
+    UART(0, tx=2, rx=3)
diff --git a/tests/ports/rp2/rp2_uart.py.exp b/tests/ports/rp2/rp2_uart.py.exp
new file mode 100644
index 00000000000..72cd5fb00f6
--- /dev/null
+++ b/tests/ports/rp2/rp2_uart.py.exp
@@ -0,0 +1 @@
+UART(0, baudrate=115200, bits=8, parity=None, stop=1, tx=0, rx=1, txbuf=256, rxbuf=256, timeout=0, timeout_char=1, invert=None, irq=0)
diff --git a/tests/pyb/accel.py b/tests/ports/stm32/accel.py
similarity index 100%
rename from tests/pyb/accel.py
rename to tests/ports/stm32/accel.py
diff --git a/tests/pyb/accel.py.exp b/tests/ports/stm32/accel.py.exp
similarity index 100%
rename from tests/pyb/accel.py.exp
rename to tests/ports/stm32/accel.py.exp
diff --git a/tests/pyb/adc.py b/tests/ports/stm32/adc.py
similarity index 84%
rename from tests/pyb/adc.py
rename to tests/ports/stm32/adc.py
index 875d31d732c..327938c97e8 100644
--- a/tests/pyb/adc.py
+++ b/tests/ports/stm32/adc.py
@@ -1,5 +1,12 @@
+import sys
 from pyb import ADC, Timer
 
+# This test assumes ADC(16) is the internal temperature sensor and ADC(17) is
+# the internal voltage referenc.  That's only true on certain MCUs.
+if not ("STM32F4" in sys.implementation._machine or "STM32F7" in sys.implementation._machine):
+    print("SKIP")
+    raise SystemExit
+
 adct = ADC(16)  # Temperature 930 -> 20C
 print(str(adct)[:19])
 adcv = ADC(17)  # Voltage 1500 -> 3.3V
diff --git a/tests/pyb/adc.py.exp b/tests/ports/stm32/adc.py.exp
similarity index 100%
rename from tests/pyb/adc.py.exp
rename to tests/ports/stm32/adc.py.exp
diff --git a/tests/ports/stm32/adcall.py b/tests/ports/stm32/adcall.py
new file mode 100644
index 00000000000..9e9811db3ef
--- /dev/null
+++ b/tests/ports/stm32/adcall.py
@@ -0,0 +1,56 @@
+import sys
+from pyb import Pin, ADCAll
+
+# This test requires PA0-PA3 with ADC, so can't run on:
+# - boards with UART REPL on PA2/PA3
+# - STM32N6 which does not have ADC on PA3
+if (
+    sys.implementation._build in ("NUCLEO_G0B1RE", "NUCLEO_L152RE")
+    or "STM32N6" in sys.implementation._machine
+):
+    print("SKIP")
+    raise SystemExit
+
+pins = [Pin.cpu.A0, Pin.cpu.A1, Pin.cpu.A2, Pin.cpu.A3]
+pa0_adc_channel = 0
+skip_temp_test = False
+
+if "STM32WB" in sys.implementation._machine:
+    pa0_adc_channel = 5
+    skip_temp_test = True  # temperature fails on WB55
+elif "STM32H7" in sys.implementation._machine:
+    # ADCAll is connected to ADC3 with the following pins.
+    pins = [Pin.cpu.F9, Pin.cpu.F7, Pin.cpu.F5, Pin.cpu.F3]
+    pa0_adc_channel = 2
+
+# set pins to IN mode, init ADCAll, then check pins are ANALOG
+for p in pins:
+    p.init(p.IN)
+adc = ADCAll(12)
+for p in pins:
+    print(p.mode())
+
+# set pins to IN mode, init ADCAll with mask, then check some pins are ANALOG
+for p in pins:
+    p.init(p.IN)
+adc = ADCAll(12, 0x70000 | 3 << pa0_adc_channel)
+for p in pins:
+    print(p.mode())
+
+# init all pins to ANALOG
+adc = ADCAll(12)
+print(adc)
+
+# read all channels
+for c in range(19):
+    print(type(adc.read_channel(c)))
+
+# call special reading functions
+print(skip_temp_test or 0 < adc.read_core_temp() < 100)
+print(0 < adc.read_core_vbat() < 4)
+print(0 < adc.read_core_vref() < 2)
+print(0 < adc.read_vref() < 4)
+
+if sys.implementation._build == "NUCLEO_WB55":
+    # Restore button pin settings.
+    Pin("SW", Pin.IN, Pin.PULL_UP)
diff --git a/tests/ports/stm32/adcall.py.exp b/tests/ports/stm32/adcall.py.exp
new file mode 100644
index 00000000000..b31d947a48a
--- /dev/null
+++ b/tests/ports/stm32/adcall.py.exp
@@ -0,0 +1,32 @@
+3
+3
+3
+3
+3
+3
+0
+0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+True
+True
+True
+True
diff --git a/tests/ports/stm32/board_pybx.py b/tests/ports/stm32/board_pybx.py
new file mode 100644
index 00000000000..340201c6f81
--- /dev/null
+++ b/tests/ports/stm32/board_pybx.py
@@ -0,0 +1,62 @@
+# Test board-specific items on PYBv1.x, PYBLITEv1.0 and PYBD-SFx boards.
+
+import sys
+
+build = sys.implementation._build
+
+if not build.startswith("PYB"):
+    print("SKIP")
+    raise SystemExit
+
+import machine
+import pyb
+import unittest
+
+if build.startswith("PYBD_"):
+    # PYBD-SFx boards.
+    expected_uarts = [1, 2, 3, 4, 6, "XA", "YA", "YB"]
+    expected_spis = [1, 2, 3, "X", "Y"]
+    expected_i2cs = [1, 2, "X", "Y"]
+    expected_cans = [1, "X"]
+    if build == "PYBD_SF6":
+        expected_uarts += [7]
+        expected_cans += [2, "Y"]
+else:
+    # PYBv1.x and PYBLITEv1.0 boards.
+    expected_uarts = [1, 2, 6, "XA", "XB", "YA"]
+    expected_spis = [1, 2, "X", "Y"]
+    expected_i2cs = [1, "X", "Y"]
+    expected_cans = []
+    if build.startswith("PYBV1"):
+        expected_uarts += [3, 4, "YB"]
+        expected_i2cs += [2]
+        expected_cans += [1, 2, "YA", "YB"]
+    elif build.startswith("PYBLITEV1"):
+        expected_i2cs += [3]
+
+
+class TestPeripheralConstruction(unittest.TestCase):
+    def _test_peripheral(self, periph, args, expected):
+        for bus in (0, 1, 2, 3, 4, 5, 6, 7, 8, "X", "Y", "Z", "XA", "XB", "YA", "YB"):
+            try:
+                periph(bus, *args)
+                assert bus in expected
+            except ValueError:
+                assert bus not in expected
+
+    def test_uart(self):
+        self._test_peripheral(machine.UART, (9600,), expected_uarts)
+
+    def test_spi(self):
+        self._test_peripheral(machine.SPI, (), expected_spis)
+
+    def test_i2c(self):
+        self._test_peripheral(machine.I2C, (), expected_i2cs)
+
+    @unittest.skipUnless(expected_cans, "no CAN")
+    def test_can(self):
+        self._test_peripheral(pyb.CAN, (pyb.CAN.LOOPBACK,), expected_cans)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/pyb/dac.py b/tests/ports/stm32/dac.py
similarity index 88%
rename from tests/pyb/dac.py
rename to tests/ports/stm32/dac.py
index 506cf272b31..e5709afc741 100644
--- a/tests/pyb/dac.py
+++ b/tests/ports/stm32/dac.py
@@ -1,4 +1,4 @@
-import pyb
+import time, pyb
 
 if not hasattr(pyb, "DAC"):
     print("SKIP")
@@ -10,7 +10,7 @@
 dac.triangle(100)
 dac.write(0)
 dac.write_timed(bytearray(10), 100, mode=pyb.DAC.NORMAL)
-pyb.delay(20)
+time.sleep_ms(20)
 dac.write(0)
 
 # test buffering arg
diff --git a/tests/pyb/dac.py.exp b/tests/ports/stm32/dac.py.exp
similarity index 100%
rename from tests/pyb/dac.py.exp
rename to tests/ports/stm32/dac.py.exp
diff --git a/tests/ports/stm32/extint.py b/tests/ports/stm32/extint.py
new file mode 100644
index 00000000000..d3161f7cc76
--- /dev/null
+++ b/tests/ports/stm32/extint.py
@@ -0,0 +1,18 @@
+import pyb
+
+# test basic functionality
+pin = pyb.Pin.cpu.A4
+ext = pyb.ExtInt(pin, pyb.ExtInt.IRQ_RISING, pyb.Pin.PULL_DOWN, lambda l: print("line:", l))
+ext.disable()
+ext.enable()
+print(ext.line())
+ext.swint()
+
+# test swint while disabled, then again after re-enabled
+ext.disable()
+ext.swint()
+ext.enable()
+ext.swint()
+
+# disable now that the test is finished
+ext.disable()
diff --git a/tests/pyb/extint.py.exp b/tests/ports/stm32/extint.py.exp
similarity index 100%
rename from tests/pyb/extint.py.exp
rename to tests/ports/stm32/extint.py.exp
diff --git a/tests/ports/stm32/i2c.py b/tests/ports/stm32/i2c.py
new file mode 100644
index 00000000000..7e7fd250408
--- /dev/null
+++ b/tests/ports/stm32/i2c.py
@@ -0,0 +1,19 @@
+try:
+    from pyb import I2C
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+# test we can correctly create by id
+for bus in (-1, 0, 1):
+    try:
+        I2C(bus)
+        print("I2C", bus)
+    except ValueError:
+        print("ValueError", bus)
+
+i2c = I2C(1)
+
+i2c.init(I2C.CONTROLLER, baudrate=400000)
+print(i2c.scan())
+i2c.deinit()
diff --git a/tests/pyb/i2c.py.exp b/tests/ports/stm32/i2c.py.exp
similarity index 100%
rename from tests/pyb/i2c.py.exp
rename to tests/ports/stm32/i2c.py.exp
diff --git a/tests/pyb/i2c_accel.py b/tests/ports/stm32/i2c_accel.py
similarity index 76%
rename from tests/pyb/i2c_accel.py
rename to tests/ports/stm32/i2c_accel.py
index 8b87d406d06..11ff1392ba4 100644
--- a/tests/pyb/i2c_accel.py
+++ b/tests/ports/stm32/i2c_accel.py
@@ -1,15 +1,14 @@
 # use accelerometer to test i2c bus
 
-import pyb
-from pyb import I2C
-
-if not hasattr(pyb, "Accel"):
+try:
+    from pyb import Accel, I2C
+except ImportError:
     print("SKIP")
     raise SystemExit
 
 accel_addr = 76
 
-pyb.Accel()  # this will init the MMA for us
+Accel()  # this will init the MMA for us
 
 i2c = I2C(1, I2C.CONTROLLER, baudrate=400000)
 
diff --git a/tests/pyb/i2c_accel.py.exp b/tests/ports/stm32/i2c_accel.py.exp
similarity index 100%
rename from tests/pyb/i2c_accel.py.exp
rename to tests/ports/stm32/i2c_accel.py.exp
diff --git a/tests/pyb/i2c_error.py b/tests/ports/stm32/i2c_error.py
similarity index 99%
rename from tests/pyb/i2c_error.py
rename to tests/ports/stm32/i2c_error.py
index 1228962f5f4..de6e1ca6fec 100644
--- a/tests/pyb/i2c_error.py
+++ b/tests/ports/stm32/i2c_error.py
@@ -1,12 +1,13 @@
 # test I2C errors, with polling (disabled irqs) and DMA
 
 import pyb
-from pyb import I2C
 
 if not hasattr(pyb, "Accel"):
     print("SKIP")
     raise SystemExit
 
+from pyb import I2C
+
 # init accelerometer
 pyb.Accel()
 
diff --git a/tests/pyb/i2c_error.py.exp b/tests/ports/stm32/i2c_error.py.exp
similarity index 100%
rename from tests/pyb/i2c_error.py.exp
rename to tests/ports/stm32/i2c_error.py.exp
diff --git a/tests/pyb/irq.py b/tests/ports/stm32/irq.py
similarity index 88%
rename from tests/pyb/irq.py
rename to tests/ports/stm32/irq.py
index 04e70a7b792..fd8742d3eaf 100644
--- a/tests/pyb/irq.py
+++ b/tests/ports/stm32/irq.py
@@ -1,3 +1,4 @@
+import time
 import pyb
 
 
@@ -8,7 +9,7 @@ def test_irq():
     pyb.enable_irq()  # by default should enable IRQ
 
     # check that interrupts are enabled by waiting for ticks
-    pyb.delay(10)
+    time.sleep_ms(10)
 
     # check nested disable/enable
     i1 = pyb.disable_irq()
@@ -18,7 +19,7 @@ def test_irq():
     pyb.enable_irq(i1)
 
     # check that interrupts are enabled by waiting for ticks
-    pyb.delay(10)
+    time.sleep_ms(10)
 
 
 test_irq()
diff --git a/tests/pyb/irq.py.exp b/tests/ports/stm32/irq.py.exp
similarity index 100%
rename from tests/pyb/irq.py.exp
rename to tests/ports/stm32/irq.py.exp
diff --git a/tests/pyb/led.py b/tests/ports/stm32/led.py
similarity index 100%
rename from tests/pyb/led.py
rename to tests/ports/stm32/led.py
diff --git a/tests/pyb/led.py.exp b/tests/ports/stm32/led.py.exp
similarity index 100%
rename from tests/pyb/led.py.exp
rename to tests/ports/stm32/led.py.exp
diff --git a/tests/pyb/modstm.py b/tests/ports/stm32/modstm.py
similarity index 91%
rename from tests/pyb/modstm.py
rename to tests/ports/stm32/modstm.py
index f1e147c0529..1459ee2a9e3 100644
--- a/tests/pyb/modstm.py
+++ b/tests/ports/stm32/modstm.py
@@ -1,13 +1,13 @@
 # test stm module
 
 import stm
-import pyb
+import time
 
 # test storing a full 32-bit number
 # turn on then off the A15(=yellow) LED
 BSRR = 0x18
 stm.mem32[stm.GPIOA + BSRR] = 0x00008000
-pyb.delay(100)
+time.sleep_ms(100)
 print(hex(stm.mem32[stm.GPIOA + stm.GPIO_ODR] & 0x00008000))
 stm.mem32[stm.GPIOA + BSRR] = 0x80000000
 print(hex(stm.mem32[stm.GPIOA + stm.GPIO_ODR] & 0x00008000))
diff --git a/tests/pyb/modstm.py.exp b/tests/ports/stm32/modstm.py.exp
similarity index 100%
rename from tests/pyb/modstm.py.exp
rename to tests/ports/stm32/modstm.py.exp
diff --git a/tests/renesas-ra/modtime.py b/tests/ports/stm32/modtime.py
similarity index 100%
rename from tests/renesas-ra/modtime.py
rename to tests/ports/stm32/modtime.py
diff --git a/tests/renesas-ra/modtime.py.exp b/tests/ports/stm32/modtime.py.exp
similarity index 100%
rename from tests/renesas-ra/modtime.py.exp
rename to tests/ports/stm32/modtime.py.exp
diff --git a/tests/ports/stm32/pin.py b/tests/ports/stm32/pin.py
new file mode 100644
index 00000000000..fa6a475bf3d
--- /dev/null
+++ b/tests/ports/stm32/pin.py
@@ -0,0 +1,51 @@
+import sys
+from pyb import Pin
+
+# Select a pin which is unconnected.
+if sys.implementation._build == "NUCLEO_H723ZG":
+    test_pin_name = "F3"
+else:
+    test_pin_name = "A7"
+
+test_pin = getattr(Pin.cpu, test_pin_name)
+
+
+# Print Pin without id.
+# This ensures an output that's consistent across MCUs.
+def print_pin(pin):
+    s = str(pin).split(", ")
+    s[0] = s[0][0:4]
+    print(", ".join(s))
+
+
+p = Pin(test_pin, Pin.IN)
+print_pin(p)
+print(p.name() == test_pin_name)
+print(p.pin() == int(test_pin_name[1]))
+print(p.port() == ord(test_pin_name[0]) - ord("A"))
+
+p = Pin(test_pin, Pin.IN, Pin.PULL_UP)
+p = Pin(test_pin, Pin.IN, pull=Pin.PULL_UP)
+p = Pin(test_pin, mode=Pin.IN, pull=Pin.PULL_UP)
+print_pin(p)
+print(p.value())
+
+p.init(p.IN, p.PULL_DOWN)
+p.init(p.IN, pull=p.PULL_DOWN)
+p.init(mode=p.IN, pull=p.PULL_DOWN)
+print_pin(p)
+print(p.value())
+
+p.init(p.OUT_PP)
+p.low()
+print(p.value())
+p.high()
+print(p.value())
+p.value(0)
+print(p.value())
+p.value(1)
+print(p.value())
+p.value(False)
+print(p.value())
+p.value(True)
+print(p.value())
diff --git a/tests/ports/stm32/pin.py.exp b/tests/ports/stm32/pin.py.exp
new file mode 100644
index 00000000000..0b00eee460b
--- /dev/null
+++ b/tests/ports/stm32/pin.py.exp
@@ -0,0 +1,14 @@
+Pin(, mode=Pin.IN)
+True
+True
+True
+Pin(, mode=Pin.IN, pull=Pin.PULL_UP)
+1
+Pin(, mode=Pin.IN, pull=Pin.PULL_DOWN)
+0
+0
+1
+0
+1
+0
+1
diff --git a/tests/pyb/pyb1.py b/tests/ports/stm32/pyb1.py
similarity index 76%
rename from tests/pyb/pyb1.py
rename to tests/ports/stm32/pyb1.py
index e9626ecf4e7..97823b38213 100644
--- a/tests/pyb/pyb1.py
+++ b/tests/ports/stm32/pyb1.py
@@ -2,6 +2,10 @@
 
 import pyb
 
+if not hasattr(pyb, "delay"):
+    print("SKIP")
+    raise SystemExit
+
 # test delay
 
 pyb.delay(-1)
@@ -27,7 +31,10 @@
 pyb.disable_irq()
 pyb.enable_irq()
 
-print(pyb.have_cdc())
+if hasattr(pyb, "have_cdc"):
+    print(pyb.have_cdc())
+else:
+    print(True)
 
 pyb.sync()
 
diff --git a/tests/pyb/pyb1.py.exp b/tests/ports/stm32/pyb1.py.exp
similarity index 100%
rename from tests/pyb/pyb1.py.exp
rename to tests/ports/stm32/pyb1.py.exp
diff --git a/tests/ports/stm32/pyb_can.py b/tests/ports/stm32/pyb_can.py
new file mode 100644
index 00000000000..e8214d8e47b
--- /dev/null
+++ b/tests/ports/stm32/pyb_can.py
@@ -0,0 +1,310 @@
+try:
+    from pyb import CAN
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+from array import array
+import micropython
+import pyb
+import sys
+import time
+
+# Classic CAN (aka bxCAN) hardware has a different filter API
+# and some different behaviours to newer FDCAN hardware
+IS_CLASSIC = hasattr(CAN, "MASK16")
+
+# STM32H7/STM32N6 series have a gold-plated FDCAN peripheral with much deeper TX Queue
+# than all other parts
+sys_impl = str(sys.implementation)
+HAS_DEEP_TXQ = (not IS_CLASSIC) and ("STM32H7" in sys_impl or "STM32N6" in sys_impl)
+
+# test we can correctly create by id (2 handled in can2.py test)
+for bus in (-1, 0, 1, 4):
+    try:
+        CAN(bus, CAN.LOOPBACK)
+        print("CAN", bus)
+    except ValueError:
+        print("ValueError", bus)
+CAN(1).deinit()
+
+can = CAN(1)
+print(can)
+
+# Test state when de-init'd
+print(can.state() == can.STOPPED)
+
+can.init(CAN.LOOPBACK, num_filter_banks=14)
+print(can)
+print("any", can.any(0))
+
+# Test state when freshly created
+print("error_active", can.state() == can.ERROR_ACTIVE)
+
+# Test that restart can be called
+can.restart()
+
+# Test info returns a sensible value
+print("info", can.info())
+
+# Catch all filter (standard IDs)
+if IS_CLASSIC:
+    can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0))
+else:
+    can.setfilter(0, CAN.MASK, 0, (0, 0), extframe=False)
+
+can.send("abcd", 123, timeout=5000)
+time.sleep_ms(10)  # For FDCAN, needs some time to send
+print("any+info", can.any(0), can.info())
+print(can.recv(0))
+
+can.send("abcd", -1, timeout=5000)
+print(can.recv(0))
+
+can.send("abcd", 0x7FF + 1, timeout=5000)
+print(can.recv(0))
+
+# Test too long message
+try:
+    if IS_CLASSIC:
+        payload = "abcdefghi"  # 9 bytes long
+    else:
+        # the pyb.CAN API for FDCAN always accepts messages up to 64 bytes and sends as FDCAN
+        payload = b"x" * 65
+    can.send(payload, 0x7FF, timeout=5000)
+except ValueError:
+    print("overlong passed")
+else:
+    print("overlong failed")
+
+# Test that recv can work without allocating memory on the heap
+
+buf = bytearray(10)
+l = [0, 0, 0, 0, memoryview(buf)]
+l2 = None
+
+micropython.heap_lock()
+
+can.send("", 42)
+l2 = can.recv(0, l)
+assert l is l2
+print(l, len(l[4]), buf)
+
+can.send("1234", 42)
+l2 = can.recv(0, l)
+assert l is l2
+print(l, len(l[4]), buf)
+
+can.send("01234567", 42)
+l2 = can.recv(0, l)
+assert l is l2
+print(l, len(l[4]), buf)
+
+can.send("abc", 42)
+l2 = can.recv(0, l)
+assert l is l2
+print(l, len(l[4]), buf)
+
+micropython.heap_unlock()
+
+# Test that recv can work with different arrays behind the memoryview
+can.send("abc", 1)
+print(bytes(can.recv(0, [0, 0, 0, 0, memoryview(array("B", range(8)))])[4]))
+can.send("def", 1)
+print(bytes(can.recv(0, [0, 0, 0, 0, memoryview(array("b", range(8)))])[4]))
+
+# Test for non-list passed as second arg to recv
+can.send("abc", 1)
+try:
+    can.recv(0, 1)
+except TypeError:
+    print("TypeError")
+
+# Test for too-short-list passed as second arg to recv
+can.send("abc", 1)
+try:
+    can.recv(0, [0, 0, 0])
+except ValueError:
+    print("ValueError")
+
+# Test for non-memoryview passed as 4th element to recv
+can.send("abc", 1)
+try:
+    can.recv(0, [0, 0, 0, 0, 0])
+except TypeError:
+    print("TypeError")
+
+# Test for read-only-memoryview passed as 4th element to recv
+can.send("abc", 1)
+try:
+    can.recv(0, [0, 0, 0, memoryview(bytes(8))])
+except ValueError:
+    print("ValueError")
+
+# Test for bad-typecode-memoryview passed as 4th element to recv
+can.send("abc", 1)
+try:
+    can.recv(0, [0, 0, 0, memoryview(array("i", range(8)))])
+except ValueError:
+    print("ValueError")
+
+del can
+
+# Testing extended IDs
+print("==== TEST extframe=True ====")
+
+can = CAN(1, CAN.LOOPBACK)
+# Catch all filter, but only for extframe's
+if IS_CLASSIC:
+    can.setfilter(0, CAN.MASK32, 0, (0, 0), extframe=True)
+else:
+    # FDCAN manages standard and extframe IDs independently, so need to
+    # clear the standard ID filter and then set the extended ID filter
+    can.clearfilter(0, extframe=False)
+    can.setfilter(0, CAN.MASK, 0, (0, 0), extframe=True)
+
+print(can)
+
+try:
+    can.send("abcde", 0x7FF + 1, timeout=5000, extframe=True)
+except ValueError:
+    print("failed")
+else:
+    time.sleep_ms(10)
+    r = can.recv(0)
+    if r[0] == 0x7FF + 1 and r[4] == b"abcde":
+        print("extframe passed")
+    else:
+        print("failed, wrong data received", r)
+
+# Test filters
+print("==== TEST filters ====")
+for n in [0, 8, 16, 24]:
+    filter_id = 0b00001000 << n
+    filter_mask = 0b00011100 << n
+    id_ok = 0b00001010 << n
+    id_fail = 0b00011010 << n
+
+    can.clearfilter(0, extframe=True)
+    if IS_CLASSIC:
+        can.setfilter(0, CAN.MASK32, 0, (filter_id, filter_mask), extframe=True)
+    else:
+        can.setfilter(0, CAN.MASK, 0, (filter_id, filter_mask), extframe=True)
+
+    can.send("ok", id_ok, timeout=5, extframe=True)
+    time.sleep_ms(10)
+    if can.any(0):
+        msg = can.recv(0)
+        print((hex(filter_id), hex(filter_mask), hex(msg[0]), msg[1], msg[4]))
+
+    can.send("fail", id_fail, timeout=5, extframe=True)
+    time.sleep_ms(10)
+    if can.any(0):
+        msg = can.recv(0)
+        print((hex(filter_id), hex(filter_mask), hex(msg[0]), msg[1], msg[4]))
+
+del can
+
+# Testing asynchronous send
+print("==== TEST async send ====")
+
+can = CAN(1, CAN.LOOPBACK)
+# Catch all filter (standard IDs)
+if IS_CLASSIC:
+    can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0))
+else:
+    can.setfilter(0, CAN.MASK, 0, (0, 0), extframe=False)
+
+while can.any(0):
+    can.recv(0)
+
+can.send("abcde", 1, timeout=0)
+print("any", can.any(0))
+while not can.any(0):
+    pass
+
+print(can.recv(0))
+
+try:
+    can.send("abcde", 2, timeout=0)
+    can.send("abcde", 3, timeout=0)
+    can.send("abcde", 4, timeout=0)
+    if not HAS_DEEP_TXQ:
+        can.send("abcde", 5, timeout=0)
+    else:
+        # Hack around a deep transmit queue by pretending this call failed
+        # (STM32G4 will fail here, using otherwise the same code, so there is still some test coverage.)
+        print("send fail ok")
+except OSError as e:
+    # When send() fails Classic CAN raises OSError(MP_EBUSY) (16), FDCAN raises OSError(MP_ETIMEDOUT) (110)
+    if e.errno == (16 if IS_CLASSIC else 110):
+        print("send fail ok")
+    else:
+        print("send fail not ok", e)
+
+time.sleep_ms(500)
+while can.any(0):
+    print(can.recv(0))
+
+del can
+
+# Testing rtr messages
+print("==== TEST rtr messages ====")
+
+bus1 = CAN(1, CAN.LOOPBACK)
+while bus1.any(0):
+    bus1.recv(0)
+
+if IS_CLASSIC:
+    # pyb.CAN Classic API allows distinguishing between RTR in the filter
+    bus1.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4))
+    bus1.setfilter(1, CAN.LIST16, 0, (5, 6, 7, 8), rtr=(True, True, True, True))
+    bus1.setfilter(2, CAN.MASK16, 0, (64, 64, 32, 32), rtr=(False, True))
+else:
+    # pyb.CAN FDCAN API does not allow distinguishing RTR in filter args, so
+    # instead we'll only filter the message IDs where Classic CAN equivalent is
+    # setting the RTR flag (meaning we're verifying RTR is received correctly, but
+    # not verifying the missing filter behaviour.)
+    bus1.setfilter(0, CAN.RANGE, 0, (5, 8))
+    bus1.setfilter(1, CAN.MASK, 0, (32, 32))
+
+
+def print_rtr(msg):
+    if msg:
+        # Skip printing msg[3] as this is the filter match index, and the value
+        # is different between Classic and FDCAN implementations
+        print(msg[0], msg[1], msg[2], msg[4])
+    else:
+        print(msg)
+
+
+bus1.send("", 1, rtr=True)
+print("any", bus1.any(0))
+bus1.send("", 5, rtr=True)
+print_rtr(bus1.recv(0))
+bus1.send("", 6, rtr=True)
+print_rtr(bus1.recv(0))
+bus1.send("", 7, rtr=True)
+print_rtr(bus1.recv(0))
+bus1.send("", 16, rtr=True)
+print("any", bus1.any(0))
+bus1.send("", 32, rtr=True)
+print_rtr(bus1.recv(0))
+
+del bus1
+
+# test HAL error, timeout
+print("==== TEST errors ====")
+
+# Note: this test requires no other CAN node is attached to the CAN peripheral
+can = pyb.CAN(1, pyb.CAN.NORMAL)
+try:
+    if IS_CLASSIC:
+        can.send("1", 1, timeout=50)
+    else:
+        # Difference between pyb.CAN on Classic vs FDCAN - Classic waits until the message is sent to the bus,
+        # FDCAN only times out if the TX queue is full
+        while True:
+            can.send("1", 1, timeout=50)
+except OSError as e:
+    print("timeout", repr(e))
diff --git a/tests/ports/stm32/pyb_can.py.exp b/tests/ports/stm32/pyb_can.py.exp
new file mode 100644
index 00000000000..3c37074f0fa
--- /dev/null
+++ b/tests/ports/stm32/pyb_can.py.exp
@@ -0,0 +1,50 @@
+ValueError -1
+ValueError 0
+CAN 1
+ValueError 4
+CAN(1)
+True
+CAN(1, CAN.LOOPBACK, auto_restart=False)
+any False
+error_active True
+info [0, 0, 0, 0, 0, 0, 0, 0]
+any+info True [0, 0, 0, 0, 0, 0, 1, 0]
+[123, False, False, 0, b'abcd']
+[2047, False, False, 0, b'abcd']
+[0, False, False, 0, b'abcd']
+overlong passed
+[42, False, False, 0, ] 0 bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+[42, False, False, 0, ] 4 bytearray(b'1234\x00\x00\x00\x00\x00\x00')
+[42, False, False, 0, ] 8 bytearray(b'01234567\x00\x00')
+[42, False, False, 0, ] 3 bytearray(b'abc34567\x00\x00')
+b'abc'
+b'def'
+TypeError
+ValueError
+TypeError
+ValueError
+ValueError
+==== TEST extframe=True ====
+CAN(1, CAN.LOOPBACK, auto_restart=False)
+extframe passed
+==== TEST filters ====
+('0x8', '0x1c', '0xa', True, b'ok')
+('0x800', '0x1c00', '0xa00', True, b'ok')
+('0x80000', '0x1c0000', '0xa0000', True, b'ok')
+('0x8000000', '0x1c000000', '0xa000000', True, b'ok')
+==== TEST async send ====
+any False
+[1, False, False, 0, b'abcde']
+send fail ok
+[2, False, False, 0, b'abcde']
+[3, False, False, 0, b'abcde']
+[4, False, False, 0, b'abcde']
+==== TEST rtr messages ====
+any False
+5 False True b''
+6 False True b''
+7 False True b''
+any False
+32 False True b''
+==== TEST errors ====
+timeout OSError(110,)
diff --git a/tests/ports/stm32/pyb_can_classic_rtr_filter.py b/tests/ports/stm32/pyb_can_classic_rtr_filter.py
new file mode 100644
index 00000000000..90ae28ca9be
--- /dev/null
+++ b/tests/ports/stm32/pyb_can_classic_rtr_filter.py
@@ -0,0 +1,30 @@
+try:
+    from pyb import CAN
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(CAN, "LIST16"):
+    # This test relies on the RTR-aware filters of Classic CAN,
+    # so needs to be skipped on FDCAN hardware
+    print("SKIP")
+    raise SystemExit
+
+can = CAN(1, CAN.LOOPBACK)
+while can.any(0):
+    can.recv(0)
+can.setfilter(0, CAN.LIST32, 0, (1, 2), rtr=(True, True), extframe=True)
+can.setfilter(1, CAN.LIST32, 0, (3, 4), rtr=(True, False), extframe=True)
+can.setfilter(2, CAN.MASK32, 0, (16, 16), rtr=(False,), extframe=True)
+can.setfilter(2, CAN.MASK32, 0, (32, 32), rtr=(True,), extframe=True)
+
+can.send("", 1, rtr=True, extframe=True)
+print(can.recv(0))
+can.send("", 2, rtr=True, extframe=True)
+print(can.recv(0))
+can.send("", 3, rtr=True, extframe=True)
+print(can.recv(0))
+can.send("", 4, rtr=True, extframe=True)
+print(can.any(0))
+
+del can
diff --git a/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp b/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp
new file mode 100644
index 00000000000..b970a0509fe
--- /dev/null
+++ b/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp
@@ -0,0 +1,4 @@
+[1, True, True, 0, b'']
+[2, True, True, 1, b'']
+[3, True, True, 2, b'']
+False
diff --git a/tests/ports/stm32/pyb_can_classic_rx.py b/tests/ports/stm32/pyb_can_classic_rx.py
new file mode 100644
index 00000000000..39a5eadca54
--- /dev/null
+++ b/tests/ports/stm32/pyb_can_classic_rx.py
@@ -0,0 +1,89 @@
+try:
+    from pyb import CAN
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+if not hasattr(CAN, "LIST16"):
+    # This test relies on some specific behaviours of the Classic CAN RX FIFO
+    # interrupt, so needs to be skipped on FDCAN hardware
+    print("SKIP")
+    raise SystemExit
+
+# Test RxCallbacks
+print("==== TEST rx callbacks ====")
+
+can = CAN(1, CAN.LOOPBACK)
+can.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4))
+can.setfilter(1, CAN.LIST16, 1, (5, 6, 7, 8))
+
+
+def cb0(bus, reason):
+    print("cb0")
+    if reason == 0:
+        print("pending")
+    if reason == 1:
+        print("full")
+    if reason == 2:
+        print("overflow")
+
+
+def cb1(bus, reason):
+    print("cb1")
+    if reason == 0:
+        print("pending")
+    if reason == 1:
+        print("full")
+    if reason == 2:
+        print("overflow")
+
+
+def cb0a(bus, reason):
+    print("cb0a")
+    if reason == 0:
+        print("pending")
+    if reason == 1:
+        print("full")
+    if reason == 2:
+        print("overflow")
+
+
+def cb1a(bus, reason):
+    print("cb1a")
+    if reason == 0:
+        print("pending")
+    if reason == 1:
+        print("full")
+    if reason == 2:
+        print("overflow")
+
+
+can.rxcallback(0, cb0)
+can.rxcallback(1, cb1)
+
+can.send("11111111", 1, timeout=5000)
+can.send("22222222", 2, timeout=5000)
+can.send("33333333", 3, timeout=5000)
+can.rxcallback(0, cb0a)
+can.send("44444444", 4, timeout=5000)
+
+can.send("55555555", 5, timeout=5000)
+can.send("66666666", 6, timeout=5000)
+can.send("77777777", 7, timeout=5000)
+can.rxcallback(1, cb1a)
+can.send("88888888", 8, timeout=5000)
+
+print(can.recv(0))
+print(can.recv(0))
+print(can.recv(0))
+print(can.recv(1))
+print(can.recv(1))
+print(can.recv(1))
+
+can.send("11111111", 1, timeout=5000)
+can.send("55555555", 5, timeout=5000)
+
+print(can.recv(0))
+print(can.recv(1))
+
+del can
diff --git a/tests/ports/stm32/pyb_can_classic_rx.py.exp b/tests/ports/stm32/pyb_can_classic_rx.py.exp
new file mode 100644
index 00000000000..c1c743c42b5
--- /dev/null
+++ b/tests/ports/stm32/pyb_can_classic_rx.py.exp
@@ -0,0 +1,25 @@
+==== TEST rx callbacks ====
+cb0
+pending
+cb0
+full
+cb0a
+overflow
+cb1
+pending
+cb1
+full
+cb1a
+overflow
+[1, False, False, 0, b'11111111']
+[2, False, False, 1, b'22222222']
+[4, False, False, 3, b'44444444']
+[5, False, False, 0, b'55555555']
+[6, False, False, 1, b'66666666']
+[8, False, False, 3, b'88888888']
+cb0a
+pending
+cb1a
+pending
+[1, False, False, 0, b'11111111']
+[5, False, False, 0, b'55555555']
diff --git a/tests/ports/stm32/pyb_can_instances.py b/tests/ports/stm32/pyb_can_instances.py
new file mode 100644
index 00000000000..f2100783904
--- /dev/null
+++ b/tests/ports/stm32/pyb_can_instances.py
@@ -0,0 +1,90 @@
+# Test that initialising CAN instances in any order doesn't break
+# TX, RX or filtering
+try:
+    from pyb import CAN
+
+    CAN(2)  # skip any board which doesn't have at least 2 CAN peripherals
+except (ImportError, ValueError):
+    print("SKIP")
+    raise SystemExit
+
+import unittest
+import errno
+
+# Classic CAN (aka bxCAN) hardware has a different filter API
+# and some different behaviours to newer FDCAN hardware
+IS_CLASSIC = hasattr(CAN, "MASK16")
+
+# Some boards have 3x CAN peripherals, test all three
+HAS_CAN3 = True
+try:
+    CAN(3)
+except ValueError:
+    HAS_CAN3 = False
+
+
+class Test(unittest.TestCase):
+    def test_can12(self):
+        self._test_pairs([(1, 2), (2, 1)])
+
+    @unittest.skipUnless(HAS_CAN3, "no CAN3")
+    def test_can3(self):
+        self._test_pairs([(1, 3), (3, 1), (2, 3), (3, 2)])
+
+    def _test_pairs(self, seq):
+        for id_a, id_b in seq:
+            with self.subTest("Testing CAN pair", id_a=id_a, id_b=id_b):
+                self._test_controller_pair(id_a, id_b)
+
+    def _test_controller_pair(self, id_a, id_b):
+        # Setting up each CAN peripheral independently is deliberate here, to catch
+        # catch cases where initialising CAN2 breaks CAN1 or vice versa
+        try:
+            can_a = CAN(id_a, CAN.LOOPBACK)
+            if IS_CLASSIC:
+                can_a.setfilter(0, CAN.LIST16, 0, (123, 124, 125, 126))
+            else:
+                can_a.setfilter(0, CAN.RANGE, 0, (123, 126))
+
+            can_b = CAN(id_b, CAN.LOOPBACK)
+            if IS_CLASSIC:
+                can_b.setfilter(0, CAN.LIST16, 0, (3, 4, 5, 6))
+            else:
+                can_b.setfilter(0, CAN.RANGE, 0, (3, 6))
+
+            # Drain any old messages in RX FIFOs
+            for can in (can_a, can_b):
+                while can.any(0):
+                    can.recv(0)
+
+            for which, id, can in (("A", id_a, can_a), ("B", id_b, can_b)):
+                print("testing config", which, "with controller", can)
+                # message1 should only receive on can_a, message2 on can_b
+                can.send("message1", 123)
+                can.send("message2", 3)
+                n_recv = 0
+                try:
+                    while True:
+                        res = can.recv(0, timeout=50)
+                        n_recv += 1
+                        print("received", res)
+                        if can == can_a:
+                            self.assertEqual(
+                                res[4], b"message1", "can_a should receive message1 only"
+                            )
+                        if can == can_b:
+                            self.assertEqual(
+                                res[4], b"message2", "can_b should receive message2 only"
+                            )
+                except OSError as e:
+                    if e.errno != errno.ETIMEDOUT:
+                        raise e
+                    print("recv timed out")
+                self.assertEqual(n_recv, 1, "Each instance should receive exactly 1 message")
+        finally:
+            can_a.deinit()
+            can_b.deinit()
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/pyb/pyb_f405.py b/tests/ports/stm32/pyb_f405.py
similarity index 100%
rename from tests/pyb/pyb_f405.py
rename to tests/ports/stm32/pyb_f405.py
diff --git a/tests/pyb/pyb_f405.py.exp b/tests/ports/stm32/pyb_f405.py.exp
similarity index 100%
rename from tests/pyb/pyb_f405.py.exp
rename to tests/ports/stm32/pyb_f405.py.exp
diff --git a/tests/pyb/pyb_f411.py b/tests/ports/stm32/pyb_f411.py
similarity index 100%
rename from tests/pyb/pyb_f411.py
rename to tests/ports/stm32/pyb_f411.py
diff --git a/tests/pyb/pyb_f411.py.exp b/tests/ports/stm32/pyb_f411.py.exp
similarity index 100%
rename from tests/pyb/pyb_f411.py.exp
rename to tests/ports/stm32/pyb_f411.py.exp
diff --git a/tests/pyb/rtc.py b/tests/ports/stm32/rtc.py
similarity index 83%
rename from tests/pyb/rtc.py
rename to tests/ports/stm32/rtc.py
index 013b2f33142..f513d1fcd1d 100644
--- a/tests/pyb/rtc.py
+++ b/tests/ports/stm32/rtc.py
@@ -1,13 +1,19 @@
-import pyb, stm
+import time, stm
 from pyb import RTC
 
+prediv_a = stm.mem32[stm.RTC + stm.RTC_PRER] >> 16
+
 rtc = RTC()
+
+# save the current datetime
+cur_datetime = rtc.datetime()
+
 rtc.init()
 print(rtc)
 
 # make sure that 1 second passes correctly
 rtc.datetime((2014, 1, 1, 1, 0, 0, 0, 0))
-pyb.delay(1002)
+time.sleep_ms(1050)
 print(rtc.datetime()[:7])
 
 
@@ -38,8 +44,12 @@ def set_and_print(datetime):
 
 
 def set_and_print_calib(cal):
-    rtc.calibration(cal)
-    print(rtc.calibration())
+    if cal > 0 and prediv_a < 3:
+        # can't set positive calibration if prediv_a<3, so just make test pass
+        print(cal)
+    else:
+        rtc.calibration(cal)
+        print(rtc.calibration())
 
 
 set_and_print_calib(512)
@@ -83,3 +93,6 @@ def set_and_print_wakeup(ms):
 set_and_print_wakeup(0x1FFFF * 1000)
 set_and_print_wakeup(0x20000 * 1000)
 set_and_print_wakeup(0x20001 * 1000)  # exception
+
+# restore the current datetime
+rtc.datetime(cur_datetime)
diff --git a/tests/pyb/rtc.py.exp b/tests/ports/stm32/rtc.py.exp
similarity index 100%
rename from tests/pyb/rtc.py.exp
rename to tests/ports/stm32/rtc.py.exp
diff --git a/tests/ports/stm32/servo.py b/tests/ports/stm32/servo.py
new file mode 100644
index 00000000000..0784f64d336
--- /dev/null
+++ b/tests/ports/stm32/servo.py
@@ -0,0 +1,20 @@
+try:
+    from pyb import Servo
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+servo = Servo(1)
+print(servo)
+
+servo.angle(0)
+servo.angle(10, 100)
+
+servo.speed(-10)
+servo.speed(10, 100)
+
+servo.pulse_width(1500)
+print(servo.pulse_width())
+
+servo.calibration(630, 2410, 1490, 2460, 2190)
+print(servo.calibration())
diff --git a/tests/pyb/servo.py.exp b/tests/ports/stm32/servo.py.exp
similarity index 100%
rename from tests/pyb/servo.py.exp
rename to tests/ports/stm32/servo.py.exp
diff --git a/tests/ports/stm32/spi.py b/tests/ports/stm32/spi.py
new file mode 100644
index 00000000000..0036a124195
--- /dev/null
+++ b/tests/ports/stm32/spi.py
@@ -0,0 +1,72 @@
+# Test pyb.SPI basic behaviour.
+
+from pyb import SPI
+
+# Test invalid SPI identifiers.
+for bus in (-1, 0, 100, "doesnotexist"):
+    try:
+        SPI(bus)
+    except ValueError:
+        print("ValueError", bus)
+
+# Find the first available SPI (assume there is one).
+spi_id = None
+for bus in (1, 2, 3):
+    try:
+        SPI(bus)
+        spi_id = bus
+        break
+    except ValueError:
+        pass
+
+
+# Print SPI without id, baudrate or prescaler.
+def print_spi(spi):
+    s = str(spi)
+    assert s.startswith("SPI(") and s.endswith(")")
+    s = s[4:-1].split(", ")
+    assert s[0].isdigit()
+    del s[0]
+    if len(s) > 1 and s[0] == "SPI.CONTROLLER":
+        assert s[1].startswith("baudrate=")
+        assert s[2].startswith("prescaler=")
+        del s[1:3]
+    print("SPI(", ", ".join(s), ")", sep="")
+
+
+# Test constructing SPI with various parameters.
+spi = SPI(spi_id)
+print_spi(spi)
+spi = SPI(spi_id, SPI.CONTROLLER)
+print_spi(spi)
+spi = SPI(spi_id, SPI.CONTROLLER, baudrate=500000)
+print_spi(spi)
+spi = SPI(
+    spi_id,
+    SPI.CONTROLLER,
+    500000,
+    polarity=1,
+    phase=0,
+    bits=8,
+    firstbit=SPI.MSB,
+    ti=False,
+    crc=None,
+)
+print_spi(spi)
+
+# Test SPI.PERIPHERAL mode.
+spi.init(SPI.PERIPHERAL, phase=1)
+print_spi(spi)
+try:
+    # need to flush input before we get an error (error is what we want to test)
+    for i in range(10):
+        spi.recv(1, timeout=100)
+except OSError:
+    print("OSError")
+
+spi.init(SPI.CONTROLLER)
+spi.send(1, timeout=100)
+print(spi.recv(1, timeout=100))
+print(spi.send_recv(1, timeout=100))
+
+spi.deinit()
diff --git a/tests/ports/stm32/spi.py.exp b/tests/ports/stm32/spi.py.exp
new file mode 100644
index 00000000000..2fc7a076c10
--- /dev/null
+++ b/tests/ports/stm32/spi.py.exp
@@ -0,0 +1,12 @@
+ValueError -1
+ValueError 0
+ValueError 100
+ValueError doesnotexist
+SPI()
+SPI(SPI.CONTROLLER, polarity=1, phase=0, bits=8)
+SPI(SPI.CONTROLLER, polarity=1, phase=0, bits=8)
+SPI(SPI.CONTROLLER, polarity=1, phase=0, bits=8)
+SPI(SPI.PERIPHERAL, polarity=1, phase=1, bits=8)
+OSError
+b'\xff'
+b'\xff'
diff --git a/tests/ports/stm32/switch.py b/tests/ports/stm32/switch.py
new file mode 100644
index 00000000000..d3ead55d820
--- /dev/null
+++ b/tests/ports/stm32/switch.py
@@ -0,0 +1,12 @@
+# Test pyb.Switch() basic behaviour.
+
+try:
+    from pyb import Switch
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+sw = Switch()
+print(sw())
+sw.callback(print)
+sw.callback(None)
diff --git a/tests/pyb/switch.py.exp b/tests/ports/stm32/switch.py.exp
similarity index 100%
rename from tests/pyb/switch.py.exp
rename to tests/ports/stm32/switch.py.exp
diff --git a/tests/ports/stm32/timer.py b/tests/ports/stm32/timer.py
new file mode 100644
index 00000000000..f8e8da977ab
--- /dev/null
+++ b/tests/ports/stm32/timer.py
@@ -0,0 +1,24 @@
+# check basic functionality of the timer class
+
+import sys
+from pyb import Timer
+
+if "STM32WB" in sys.implementation._machine:
+    tim_id = 16
+else:
+    tim_id = 4
+
+tim = Timer(tim_id)
+tim = Timer(tim_id, prescaler=100, period=200)
+print(tim.prescaler())
+print(tim.period())
+tim.prescaler(300)
+print(tim.prescaler())
+tim.period(400)
+print(tim.period())
+
+# Setting and printing frequency
+tim = Timer(2, freq=100)
+print(tim.freq())
+tim.freq(0.01)
+print("{:.2f}".format(tim.freq()))
diff --git a/tests/pyb/timer.py.exp b/tests/ports/stm32/timer.py.exp
similarity index 76%
rename from tests/pyb/timer.py.exp
rename to tests/ports/stm32/timer.py.exp
index 7602bbd70db..8de378667e7 100644
--- a/tests/pyb/timer.py.exp
+++ b/tests/ports/stm32/timer.py.exp
@@ -3,4 +3,4 @@
 300
 400
 100
-0.001
+0.01
diff --git a/tests/ports/stm32/timer_callback.py b/tests/ports/stm32/timer_callback.py
new file mode 100644
index 00000000000..e370d49a12d
--- /dev/null
+++ b/tests/ports/stm32/timer_callback.py
@@ -0,0 +1,65 @@
+# check callback feature of the timer class
+
+import sys
+import time
+from pyb import Timer
+
+tim_first = 1
+tim_second = 2
+tim_third = 4
+
+if "STM32L1" in sys.implementation._machine:
+    tim_first = 5
+elif "STM32WB" in sys.implementation._machine:
+    tim_third = 16
+
+
+# callback function that disables the callback when called
+def cb1(t):
+    print("cb1")
+    t.callback(None)
+
+
+# callback function that disables the timer when called
+def cb2(t):
+    print("cb2")
+    t.deinit()
+
+
+# callback where cb4 closes over cb3.y
+def cb3(x):
+    y = x
+
+    def cb4(t):
+        print("cb4", y)
+        t.callback(None)
+
+    return cb4
+
+
+# create a timer with a callback, using callback(None) to stop
+tim = Timer(tim_first, freq=100, callback=cb1)
+time.sleep_ms(5)
+print("before cb1")
+time.sleep_ms(15)
+
+# create a timer with a callback, using deinit to stop
+tim = Timer(tim_second, freq=100, callback=cb2)
+time.sleep_ms(5)
+print("before cb2")
+time.sleep_ms(15)
+
+# create a timer, then set the freq, then set the callback
+tim = Timer(tim_third)
+tim.init(freq=100)
+tim.callback(cb1)
+time.sleep_ms(5)
+print("before cb1")
+time.sleep_ms(15)
+
+# test callback with a closure
+tim.init(freq=100)
+tim.callback(cb3(3))
+time.sleep_ms(5)
+print("before cb4")
+time.sleep_ms(15)
diff --git a/tests/pyb/timer_callback.py.exp b/tests/ports/stm32/timer_callback.py.exp
similarity index 100%
rename from tests/pyb/timer_callback.py.exp
rename to tests/ports/stm32/timer_callback.py.exp
diff --git a/tests/ports/stm32/uart.py b/tests/ports/stm32/uart.py
new file mode 100644
index 00000000000..50be202ab06
--- /dev/null
+++ b/tests/ports/stm32/uart.py
@@ -0,0 +1,79 @@
+# Test pyb.UART basic behaviour.
+
+import sys
+from pyb import UART
+
+if "STM32WB" in sys.implementation._machine:
+    # UART(1) is usually connected to the REPL on these MCUs.
+    print("SKIP")
+    raise SystemExit
+
+# Test invalid UART identifiers.
+for bus in (-1, 0, 100, "doesnotexist"):
+    try:
+        UART(bus, 9600)
+    except ValueError:
+        print("ValueError", bus)
+
+# Find the first available UART (assume there is one).
+uart_id = None
+for bus in (1, 2, 3):
+    try:
+        UART(bus)
+        uart_id = bus
+        break
+    except ValueError:
+        pass
+
+
+# Print UART without id or invert, and round baudrate to multiple of 2.
+# This ensures an output that's consistent across MCUs.
+def print_uart(uart):
+    s = str(uart).split(", ")
+    s[0] = s[0][0:5]
+    br_key, br_value = s[1].split("=")
+    br_value = (int(br_value) + 1) // 2 * 2
+    s[1] = br_key + "=" + str(br_value)
+    if s[-1] == "invert=0)":
+        del s[-1]
+        s[-1] += ")"
+    print(", ".join(s))
+
+
+uart = UART(uart_id)
+uart = UART(uart_id, 9600)
+uart = UART(uart_id, 9600, bits=8, parity=None, stop=1)
+print_uart(uart)
+
+uart.init(2400)
+print_uart(uart)
+
+print(uart.any())
+print(uart.write("123"))
+print(uart.write(b"abcd"))
+print(uart.writechar(1))
+
+# make sure this method exists
+uart.sendbreak()
+
+# non-blocking mode
+uart = UART(uart_id, 9600, timeout=0)
+print(uart.write(b"1"))
+print(uart.write(b"abcd"))
+print(uart.writechar(1))
+if uart.any():
+    # discard data in case UART is connected externally in loopback
+    uart.read(8)
+print(uart.read(100))
+
+# set rxbuf
+uart.init(9600, rxbuf=8)
+print_uart(uart)
+uart.init(9600, rxbuf=0)
+print_uart(uart)
+
+# set read_buf_len (legacy, use rxbuf instead)
+uart.init(9600, read_buf_len=4)
+print_uart(uart)
+uart.init(9600, read_buf_len=0)
+print_uart(uart)
diff --git a/tests/ports/stm32/uart.py.exp b/tests/ports/stm32/uart.py.exp
new file mode 100644
index 00000000000..249a3024ce2
--- /dev/null
+++ b/tests/ports/stm32/uart.py.exp
@@ -0,0 +1,18 @@
+ValueError -1
+ValueError 0
+ValueError 100
+ValueError doesnotexist
+UART(, baudrate=9600, bits=8, parity=None, stop=1, flow=0, timeout=0, timeout_char=3, rxbuf=64)
+UART(, baudrate=2400, bits=8, parity=None, stop=1, flow=0, timeout=0, timeout_char=7, rxbuf=64)
+0
+3
+4
+None
+1
+4
+None
+None
+UART(, baudrate=9600, bits=8, parity=None, stop=1, flow=0, timeout=0, timeout_char=3, rxbuf=8)
+UART(, baudrate=9600, bits=8, parity=None, stop=1, flow=0, timeout=0, timeout_char=3, rxbuf=0)
+UART(, baudrate=9600, bits=8, parity=None, stop=1, flow=0, timeout=0, timeout_char=3, rxbuf=4)
+UART(, baudrate=9600, bits=8, parity=None, stop=1, flow=0, timeout=0, timeout_char=3, rxbuf=0)
diff --git a/tests/ports/stm32_hardware/spi_dma_align.py b/tests/ports/stm32_hardware/spi_dma_align.py
new file mode 100644
index 00000000000..1c271d48e72
--- /dev/null
+++ b/tests/ports/stm32_hardware/spi_dma_align.py
@@ -0,0 +1,59 @@
+# Regression test for DMA for DCache coherency bugs with cache line
+# written originally for https://github.com/micropython/micropython/issues/13471
+
+# IMPORTANT: This test requires SPI2 MISO (pin Y8 on Pyboard D) to be connected to GND
+
+import unittest
+from machine import SPI
+
+SPI_NUM = 2
+BAUDRATE = 5_000_000
+
+
+class Test(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.spi = SPI(SPI_NUM, baudrate=BAUDRATE)
+        cls.skip_slow_test = False
+
+    def test_variable_offset_fixed_length(self):
+        buf = bytearray(1024)
+        for offs in range(0, len(buf)):
+            v = memoryview(buf)[offs : offs + 128]
+            self.spi.readinto(v, 0xFF)
+            ok = all(b == 0x00 for b in v)
+            if not ok:
+                print(offs, v.hex())
+                self.skip_slow_test = True
+            self.assertTrue(ok)
+
+    def test_variable_offset_and_lengths(self):
+        # this takes around 30s to run, so skipped if already failing
+        if self.skip_slow_test:
+            self.skipTest("already failing")
+
+        buf = bytearray(1024)
+        for op_len in range(1, 66):
+            print(op_len)
+            wr = b"\xff" * op_len
+            for offs in range(1, len(buf) - op_len - 1):
+                # Place some "sentinel" values before and after the DMA buffer
+                before = offs & 0xFF
+                after = (~offs) & 0xFF
+                buf[offs - 1] = before
+                buf[offs + op_len] = after
+                v = memoryview(buf)[offs : offs + op_len]
+                self.spi.write_readinto(wr, v)
+                ok = (
+                    all(b == 0x00 for b in v)
+                    and buf[offs - 1] == before
+                    and buf[offs + op_len] == after
+                )
+                if not ok:
+                    print(v.hex())
+                    print(hex(op_len), hex(offs), hex(buf[offs - 1]), hex(buf[offs + op_len]))
+                self.assertTrue(ok)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/ports/unix/extra_coverage.py b/tests/ports/unix/extra_coverage.py
new file mode 100644
index 00000000000..2087e7ea2ad
--- /dev/null
+++ b/tests/ports/unix/extra_coverage.py
@@ -0,0 +1,151 @@
+try:
+    extra_coverage
+except NameError:
+    print("SKIP")
+    raise SystemExit
+
+import errno
+import io
+import uctypes
+
+# create an int-like variable used for coverage of `mp_obj_get_ll`
+buf = bytearray(b"\xde\xad\xbe\xef")
+struct = uctypes.struct(
+    uctypes.addressof(buf),
+    {"f32": uctypes.UINT32 | 0},
+    uctypes.BIG_ENDIAN,
+)
+deadbeef = struct.f32
+
+data = extra_coverage()
+
+# test hashing of str/bytes that have an invalid hash
+print(data[0], data[1])
+print(hash(data[0]))
+print(hash(data[1]))
+print(hash(bytes(data[0], "utf8")))
+print(hash(str(data[1], "utf8")))
+
+# test streams
+stream = data[2]  # has set_error and set_buf. Write always returns error
+stream.set_error(errno.EAGAIN)  # non-blocking error
+print(stream.read())  # read all encounters non-blocking error
+print(stream.read(1))  # read 1 byte encounters non-blocking error
+print(stream.readline())  # readline encounters non-blocking error
+print(stream.readinto(bytearray(10)))  # readinto encounters non-blocking error
+print(stream.readinto1(bytearray(10)))  # readinto1 encounters non-blocking error
+print(stream.write(b"1"))  # write encounters non-blocking error
+print(stream.write1(b"1"))  # write1 encounters non-blocking error
+stream.set_buf(b"123")
+print(stream.read(4))  # read encounters non-blocking error after successful reads
+stream.set_buf(b"123")
+print(stream.read1(4))  # read1 encounters non-blocking error after successful reads
+stream.set_buf(b"123")
+print(stream.readline(4))  # readline encounters non-blocking error after successful reads
+try:
+    print(stream.ioctl(0, 0))  # ioctl encounters non-blocking error; raises OSError
+except OSError:
+    print("OSError")
+stream.set_error(0)
+print(stream.ioctl(0, bytearray(10)))  # successful ioctl call
+
+print("# stream.readinto")
+
+# stream.readinto will read 3 bytes then try to read more to fill the buffer,
+# but on the second attempt will encounter EIO and should raise that error.
+stream.set_error(errno.EIO)
+stream.set_buf(b"123")
+buf = bytearray(4)
+try:
+    stream.readinto(buf)
+except OSError as er:
+    print("OSError", er.errno == errno.EIO)
+print(buf)
+
+# stream.readinto1 will read 3 bytes then should return them immediately, and
+# not encounter the EIO.
+stream.set_error(errno.EIO)
+stream.set_buf(b"123")
+buf = bytearray(4)
+print(stream.readinto1(buf), buf)
+
+print("# stream textio")
+
+stream2 = data[3]  # is textio
+print(stream2.read(1))  # read 1 byte encounters non-blocking error with textio stream
+
+# test BufferedWriter with stream errors
+stream.set_error(errno.EAGAIN)
+buf = io.BufferedWriter(stream, 8)
+print(buf.write(bytearray(16)))
+
+# function defined in C++ code
+print("cpp", extra_cpp_coverage())
+
+# test user C module mixed with C++ code
+import cppexample
+
+print(cppexample.cppfunc(1, 2))
+
+# test basic import of frozen scripts
+import frzstr1
+
+print(frzstr1.__file__)
+import frzmpy1
+
+print(frzmpy1.__file__)
+
+# test import of frozen packages with __init__.py
+import frzstr_pkg1
+
+print(frzstr_pkg1.__file__, frzstr_pkg1.x)
+import frzmpy_pkg1
+
+print(frzmpy_pkg1.__file__, frzmpy_pkg1.x)
+
+# test import of frozen packages without __init__.py
+from frzstr_pkg2.mod import Foo
+
+print(Foo.x)
+from frzmpy_pkg2.mod import Foo
+
+print(Foo.x)
+
+# test raising exception in frozen script
+try:
+    import frzmpy2
+except ZeroDivisionError:
+    print("ZeroDivisionError")
+
+# test importing various objects
+import frzmpy3
+
+# test importing other things
+import frzmpy4
+
+# test for MP_QSTR_NULL regression
+from frzqstr import returns_NULL
+
+print(returns_NULL())
+
+# test for freeze_mpy (importing prints several lines)
+import frozentest
+
+print(frozentest.__file__)
+
+# test for builtin sub-packages
+from example_package.foo import bar
+
+print(bar)
+bar.f()
+import example_package
+
+print(example_package, example_package.foo, example_package.foo.bar)
+example_package.f()
+example_package.foo.f()
+example_package.foo.bar.f()
+print(bar == example_package.foo.bar)
+from example_package.foo import f as foo_f
+
+foo_f()
+print(foo_f == example_package.foo.f)
diff --git a/tests/ports/unix/extra_coverage.py.exp b/tests/ports/unix/extra_coverage.py.exp
new file mode 100644
index 00000000000..a89457f5161
--- /dev/null
+++ b/tests/ports/unix/extra_coverage.py.exp
@@ -0,0 +1,286 @@
+# mp_printf
+-123 +123  123
+-0123
+123
+123f
+123F
+7fffffffffffffff
+7FFFFFFFFFFFFFFF
+18446744073709551615
+789f
+789F
+ab abc ' abc' ' True' 'Tru'
+
+false true
+(null)
+-2147483648
+2147483648
+8000000f
+8000000F
+abc
+%
+.a  .
+<%>
+
+
+<43690>
+<43690>
+<43690>
+
+<1000.000000>
+
+<9223372036854775807>
+# GC
+0
+0
+1
+1
+# GC part 2
+pass
+# tracked allocation
+m_tracked_head = 0
+0 1
+1 1
+2 1
+3 1
+4 1
+5 1
+6 1
+7 1
+1
+0 1
+1 1
+2 1
+3 1
+4 1
+5 1
+6 1
+7 1
+m_tracked_head = 0
+# tracked realloc
+grow preserves data: 1
+shrink preserves data: 1
+realloc gc stable: 1
+realloc(NULL) ok: 1
+realloc(ptr, 0) returns NULL: 1
+m_tracked_head after cleanup: 0
+# vstr
+tests
+sts
+
+test
+tes
+RuntimeError: \$
+RuntimeError: \$
+# repl
+ame__
+port \$
+
+builtins        micropython     array           binascii
+btree           cexample        cmath           collections
+cppexample      cryptolib       deflate         errno
+example_package                 ffi             framebuf
+gc              hashlib         heapq           io
+json            machine         marshal         math
+os              platform        random          re
+select          socket          string          struct
+sys             termios         time            tls
+uctypes         vfs             weakref         websocket
+me
+
+micropython     machine         marshal         math
+
+argv            atexit          byteorder       exc_info
+executable      exit            getsizeof       implementation
+intern          maxsize         modules         path
+platform        print_exception                 ps1
+ps2             settrace        stderr          stdin
+stdout          tracebacklimit  version         version_info
+ementation
+# attrtuple
+(start=1, stop=2, step=3)
+# str
+1
+# bytearray
+data
+# mpz
+1
+12345678
+0
+0
+0
+0
+0
+1
+12345
+6
+-1
+0
+1
+0
+0.000000
+deadbeef
+c0ffee777c0ffee
+deadbeef
+0deadbeef
+c0ffee
+000c0ffee
+# list argument helpers
+TypeError: \$
+ValueError: \$
+mp_obj_list_ensure same list? 1
+mp_obj_list_optional_arg same list? 1
+mp_obj_list_optional_arg new list len 3
+mp_obj_list_optional_arg new list from NULL len 3
+# runtime utils
+TypeError: unsupported type for __abs__: 'str'
+TypeError: unsupported types for __divmod__: 'str', 'str'
+0
+1
+2
+OverflowError: overflow converting long int to machine word
+OverflowError: overflow converting long int to machine word
+TypeError: can't convert NoneType to int
+TypeError: can't convert NoneType to int
+ValueError: \$
+Warning: test
+# VM
+2 1
+# scheduler
+sched(0)=1
+sched(1)=1
+sched(2)=1
+sched(3)=1
+sched(4)=0
+unlocked
+0
+1
+2
+3
+KeyboardInterrupt: \$
+KeyboardInterrupt: \$
+10
+loop
+scheduled function
+loop
+scheduled function
+loop
+scheduled function
+scheduled function
+# ringbuf
+99 0
+98 1
+22
+99 0
+97 2
+aa55
+99 0
+0 99
+-1
+1 98
+-1
+2 97
+0
+cc99
+99 0
+0
+11bb
+0
+22ff
+-1
+-1
+0
+0
+abc123
+-1
+-2
+# pairheap
+create: 0 0 0 0
+pop all: 0 1 2 3
+create: 7 6 5 4 3 2 1 0
+pop all: 0 1 2 3 4 5 6 7
+create: 1 - - 1 1 1 1 1 1
+pop all: 1 2
+create: 1 1 1 1 2 2
+pop all: 2 4
+create: 1 1 1 1 1
+pop all: 1 3 4
+create: 3 3 3 1 1 1
+pop all: 1 2 4 5
+# mp_obj_is_type
+1 1
+0 0
+1 1
+1 1
+0 0
+1 1
+# stackctrl
+1 1
+# end coverage.c
+0123456789 b'0123456789'
+7300
+7300
+7300
+7300
+None
+None
+None
+None
+None
+None
+None
+b'123'
+b'123'
+b'123'
+OSError
+0
+# stream.readinto
+OSError True
+bytearray(b'123\x00')
+3 bytearray(b'123\x00')
+# stream textio
+None
+None
+cpp None
+(3, 'hellocpp')
+frzstr1
+frzstr1.py
+frzmpy1
+frzmpy1.py
+frzstr_pkg1.__init__
+frzstr_pkg1/__init__.py 1
+frzmpy_pkg1.__init__
+frzmpy_pkg1/__init__.py 1
+frzstr_pkg2.mod
+1
+frzmpy_pkg2.mod
+1
+ZeroDivisionError
+\
+
+X
+'\x1b'
+b'\x00\xff'
+frzmpy4 1
+frzmpy4 2
+NULL
+interned
+a long string that is not interned
+a string that has unicode αβγ chars
+b'bytes 1234\x01'
+123456789
+0
+1
+2
+3
+frozentest.py
+example_package.__init__
+
+example_package.foo.bar.f
+  
+example_package.f
+example_package.foo.f
+example_package.foo.bar.f
+True
+example_package.foo.f
+True
diff --git a/tests/unix/ffi_callback.py b/tests/ports/unix/ffi_callback.py
similarity index 87%
rename from tests/unix/ffi_callback.py
rename to tests/ports/unix/ffi_callback.py
index 21bfccf251e..b5dfe28b644 100644
--- a/tests/unix/ffi_callback.py
+++ b/tests/ports/unix/ffi_callback.py
@@ -16,7 +16,7 @@ def ffi_open(names):
     raise err
 
 
-libc = ffi_open(("libc.so", "libc.so.0", "libc.so.6", "libc.dylib"))
+libc = ffi_open(("libc.so", "libc.so.0", "libc.so.6", "libc.so.7", "libc.dylib"))
 
 qsort = libc.func("v", "qsort", "piip")
 
diff --git a/tests/unix/ffi_callback.py.exp b/tests/ports/unix/ffi_callback.py.exp
similarity index 100%
rename from tests/unix/ffi_callback.py.exp
rename to tests/ports/unix/ffi_callback.py.exp
diff --git a/tests/unix/ffi_float.py b/tests/ports/unix/ffi_float.py
similarity index 86%
rename from tests/unix/ffi_float.py
rename to tests/ports/unix/ffi_float.py
index 03bd9f7f17b..117db69dd6c 100644
--- a/tests/unix/ffi_float.py
+++ b/tests/ports/unix/ffi_float.py
@@ -17,7 +17,7 @@ def ffi_open(names):
     raise err
 
 
-libc = ffi_open(("libc.so", "libc.so.0", "libc.so.6", "libc.dylib"))
+libc = ffi_open(("libc.so", "libc.so.0", "libc.so.6", "libc.so.7", "libc.dylib"))
 
 try:
     strtof = libc.func("f", "strtof", "sp")
@@ -33,7 +33,9 @@ def ffi_open(names):
 print("%.6f" % strtod("1.23", None))
 
 # test passing double and float args
-libm = ffi_open(("libm.so", "libm.so.6", "libc.so.0", "libc.so.6", "libc.dylib"))
+libm = ffi_open(
+    ("libm.so", "libm.so.5", "libm.so.6", "libc.so.0", "libc.so.6", "libc.so.7", "libc.dylib")
+)
 tgamma = libm.func("d", "tgamma", "d")
 for fun_name in ("tgamma",):
     fun = globals()[fun_name]
diff --git a/tests/unix/ffi_float.py.exp b/tests/ports/unix/ffi_float.py.exp
similarity index 100%
rename from tests/unix/ffi_float.py.exp
rename to tests/ports/unix/ffi_float.py.exp
diff --git a/tests/unix/ffi_float2.py b/tests/ports/unix/ffi_float2.py
similarity index 84%
rename from tests/unix/ffi_float2.py
rename to tests/ports/unix/ffi_float2.py
index bbed6966627..eac6cd106cf 100644
--- a/tests/unix/ffi_float2.py
+++ b/tests/ports/unix/ffi_float2.py
@@ -29,4 +29,5 @@ def ffi_open(names):
 
 for fun in (tgammaf,):
     for val in (0.5, 1, 1.0, 1.5, 4, 4.0):
-        print("%.6f" % fun(val))
+        # limit to 5 decimals in order to pass with REPR_C with FORMAT_IMPL_BASIC
+        print("%.5f" % fun(val))
diff --git a/tests/ports/unix/ffi_float2.py.exp b/tests/ports/unix/ffi_float2.py.exp
new file mode 100644
index 00000000000..4c750e223a3
--- /dev/null
+++ b/tests/ports/unix/ffi_float2.py.exp
@@ -0,0 +1,6 @@
+1.77245
+1.00000
+1.00000
+0.88623
+6.00000
+6.00000
diff --git a/tests/ports/unix/ffi_int_base.py b/tests/ports/unix/ffi_int_base.py
new file mode 100644
index 00000000000..906bc3c1e14
--- /dev/null
+++ b/tests/ports/unix/ffi_int_base.py
@@ -0,0 +1,49 @@
+# common tests for ffi_int_types/long32/long64
+# requires ffi_lib.c to be compiled as: $(CC) -shared -o ffi_lib.so ffi_lib.c
+
+import os, sys
+
+try:
+    import ffi
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+ffi_lib_filename = sys.argv[0].rsplit("/", 1)[0] + "/ffi_lib.so"
+try:
+    os.stat(ffi_lib_filename)
+except OSError:
+    print("SKIP")
+    raise SystemExit
+
+ffi_lib = ffi.open(ffi_lib_filename)
+
+
+def test(funcs):
+    for type, name in funcs:
+        func = ffi_lib.func(type, name, type)
+        for val in (
+            0,
+            0x7F,
+            0x80,
+            0xFF,
+            0x100,
+            0x7FFF,
+            0x8000,
+            0xFFFF,
+            0x10000,
+            0x7FFFFFFF,
+            0x80000000,
+            0xFFFFFFFF,
+            0x100000000,
+            0x7FFF_FFFF_FFFF_FFFF,
+            0x8000_0000_0000_0000,
+            0xFFFF_FFFF_FFFF_FFFF,
+            0x1_0000_0000_0000_0000,
+        ):
+            print("{}({:x}) = {:x}".format(name, val, func(val)))
+
+
+if __name__ == "__main__":
+    print("SKIP")
+    raise SystemExit
diff --git a/tests/ports/unix/ffi_int_long32.py b/tests/ports/unix/ffi_int_long32.py
new file mode 100644
index 00000000000..32fdb8e0ec7
--- /dev/null
+++ b/tests/ports/unix/ffi_int_long32.py
@@ -0,0 +1,16 @@
+# test 32-bit long arguments and return types for ffi functions
+
+import struct
+
+import ffi_int_base
+
+if struct.calcsize("l") != 4:
+    print("SKIP")
+    raise SystemExit
+
+ffi_int_base.test(
+    [
+        ("l", "fli"),
+        ("L", "flu"),
+    ]
+)
diff --git a/tests/ports/unix/ffi_int_long32.py.exp b/tests/ports/unix/ffi_int_long32.py.exp
new file mode 100644
index 00000000000..e0f1338a48e
--- /dev/null
+++ b/tests/ports/unix/ffi_int_long32.py.exp
@@ -0,0 +1,34 @@
+fli(0) = 1
+fli(7f) = 7e
+fli(80) = 81
+fli(ff) = fe
+fli(100) = 101
+fli(7fff) = 7ffe
+fli(8000) = 8001
+fli(ffff) = fffe
+fli(10000) = 10001
+fli(7fffffff) = 7ffffffe
+fli(80000000) = -7fffffff
+fli(ffffffff) = -2
+fli(100000000) = 1
+fli(7fffffffffffffff) = -2
+fli(8000000000000000) = 1
+fli(ffffffffffffffff) = -2
+fli(10000000000000000) = 1
+flu(0) = 1
+flu(7f) = 7e
+flu(80) = 81
+flu(ff) = fe
+flu(100) = 101
+flu(7fff) = 7ffe
+flu(8000) = 8001
+flu(ffff) = fffe
+flu(10000) = 10001
+flu(7fffffff) = 7ffffffe
+flu(80000000) = 80000001
+flu(ffffffff) = fffffffe
+flu(100000000) = 1
+flu(7fffffffffffffff) = fffffffe
+flu(8000000000000000) = 1
+flu(ffffffffffffffff) = fffffffe
+flu(10000000000000000) = 1
diff --git a/tests/ports/unix/ffi_int_long64.py b/tests/ports/unix/ffi_int_long64.py
new file mode 100644
index 00000000000..55bf6605232
--- /dev/null
+++ b/tests/ports/unix/ffi_int_long64.py
@@ -0,0 +1,16 @@
+# test 64-bit long arguments and return types for ffi functions
+
+import struct
+
+import ffi_int_base
+
+if struct.calcsize("l") != 8:
+    print("SKIP")
+    raise SystemExit
+
+ffi_int_base.test(
+    [
+        ("l", "fli"),
+        ("L", "flu"),
+    ]
+)
diff --git a/tests/ports/unix/ffi_int_long64.py.exp b/tests/ports/unix/ffi_int_long64.py.exp
new file mode 100644
index 00000000000..904e6c3ed44
--- /dev/null
+++ b/tests/ports/unix/ffi_int_long64.py.exp
@@ -0,0 +1,34 @@
+fli(0) = 1
+fli(7f) = 7e
+fli(80) = 81
+fli(ff) = fe
+fli(100) = 101
+fli(7fff) = 7ffe
+fli(8000) = 8001
+fli(ffff) = fffe
+fli(10000) = 10001
+fli(7fffffff) = 7ffffffe
+fli(80000000) = 80000001
+fli(ffffffff) = fffffffe
+fli(100000000) = 100000001
+fli(7fffffffffffffff) = 7ffffffffffffffe
+fli(8000000000000000) = -7fffffffffffffff
+fli(ffffffffffffffff) = -2
+fli(10000000000000000) = 1
+flu(0) = 1
+flu(7f) = 7e
+flu(80) = 81
+flu(ff) = fe
+flu(100) = 101
+flu(7fff) = 7ffe
+flu(8000) = 8001
+flu(ffff) = fffe
+flu(10000) = 10001
+flu(7fffffff) = 7ffffffe
+flu(80000000) = 80000001
+flu(ffffffff) = fffffffe
+flu(100000000) = 100000001
+flu(7fffffffffffffff) = 7ffffffffffffffe
+flu(8000000000000000) = 8000000000000001
+flu(ffffffffffffffff) = fffffffffffffffe
+flu(10000000000000000) = 1
diff --git a/tests/ports/unix/ffi_int_types.py b/tests/ports/unix/ffi_int_types.py
new file mode 100644
index 00000000000..24fa2a721b2
--- /dev/null
+++ b/tests/ports/unix/ffi_int_types.py
@@ -0,0 +1,16 @@
+# test 8/16/32/64 bit signed/unsigned integer arguments and return types for ffi functions
+
+import ffi_int_base
+
+ffi_int_base.test(
+    [
+        ("b", "f8i"),
+        ("B", "f8u"),
+        ("h", "f16i"),
+        ("H", "f16u"),
+        ("i", "f32i"),
+        ("I", "f32u"),
+        ("q", "f64i"),
+        ("Q", "f64u"),
+    ]
+)
diff --git a/tests/unix/ffi_types.py.exp b/tests/ports/unix/ffi_int_types.py.exp
similarity index 100%
rename from tests/unix/ffi_types.py.exp
rename to tests/ports/unix/ffi_int_types.py.exp
diff --git a/tests/unix/ffi_lib.c b/tests/ports/unix/ffi_lib.c
similarity index 80%
rename from tests/unix/ffi_lib.c
rename to tests/ports/unix/ffi_lib.c
index 35340536aee..dae4e04bc44 100644
--- a/tests/unix/ffi_lib.c
+++ b/tests/ports/unix/ffi_lib.c
@@ -31,3 +31,11 @@ int64_t f64i(int64_t x) {
 uint64_t f64u(uint64_t x) {
     return x ^ 1;
 }
+
+long fli(long x) {
+    return x ^ 1;
+}
+
+unsigned long flu(unsigned long x) {
+    return x ^ 1;
+}
diff --git a/tests/unix/mod_os.py b/tests/ports/unix/mod_os.py
similarity index 81%
rename from tests/unix/mod_os.py
rename to tests/ports/unix/mod_os.py
index 17554d9375a..468f6badd1e 100644
--- a/tests/unix/mod_os.py
+++ b/tests/ports/unix/mod_os.py
@@ -1,6 +1,9 @@
 # This module is not entirely compatible with CPython
 import os
 
+if not hasattr(os, "getenv"):
+    print("SKIP")
+    raise SystemExit
 
 os.putenv("TEST_VARIABLE", "TEST_VALUE")
 
@@ -12,7 +15,7 @@
 print(os.getenv("TEST_VARIABLE"))
 print(os.getenv("TEST_VARIABLE", "TEST_DEFAULT_VALUE"))
 
-print(os.system("true"))
+print(os.system("exit 0"))
 
 rand = os.urandom(4)
 print(type(rand) is bytes, len(rand))
diff --git a/tests/unix/mod_os.py.exp b/tests/ports/unix/mod_os.py.exp
similarity index 100%
rename from tests/unix/mod_os.py.exp
rename to tests/ports/unix/mod_os.py.exp
diff --git a/tests/ports/unix/time_mktime_localtime.py b/tests/ports/unix/time_mktime_localtime.py
new file mode 100644
index 00000000000..df5d6cda690
--- /dev/null
+++ b/tests/ports/unix/time_mktime_localtime.py
@@ -0,0 +1,60 @@
+try:
+    import time
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+DAYS_PER_MONTH = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
+
+tzseconds = -time.mktime((1970, 1, 1, 14, 0, 0, 0, 0, 0))
+
+
+def is_leap(year):
+    return (year % 4) == 0
+
+
+def test():
+    seconds = 0
+    wday = 3  # Jan 1, 1970 was a Thursday
+    for year in range(1970, 2038):
+        print("Testing %d" % year)
+        yday = 1
+        for month in range(1, 13):
+            if month == 2 and is_leap(year):
+                DAYS_PER_MONTH[2] = 29
+            else:
+                DAYS_PER_MONTH[2] = 28
+            for day in range(1, DAYS_PER_MONTH[month] + 1):
+                secs = time.mktime((year, month, day, 14, 0, 0, 0, 0, 0)) + tzseconds
+                if secs != seconds:
+                    print(
+                        "mktime failed for %d-%02d-%02d got %d expected %d"
+                        % (year, month, day, secs, seconds)
+                    )
+                    return
+                tuple = time.localtime(seconds)
+                secs = time.mktime(tuple)
+                if secs != seconds:
+                    print(
+                        "localtime failed for %d-%02d-%02d got %d expected %d"
+                        % (year, month, day, secs, seconds)
+                    )
+                    return
+                seconds += 86400
+                if yday != tuple[7]:
+                    print(
+                        "locatime for %d-%02d-%02d got yday %d, expecting %d"
+                        % (year, month, day, tuple[7], yday)
+                    )
+                    return
+                if wday != tuple[6]:
+                    print(
+                        "locatime for %d-%02d-%02d got wday %d, expecting %d"
+                        % (year, month, day, tuple[6], wday)
+                    )
+                    return
+                yday += 1
+                wday = (wday + 1) % 7
+
+
+test()
diff --git a/tests/ports/webassembly/asyncio_await_resolve_no_arg.mjs b/tests/ports/webassembly/asyncio_await_resolve_no_arg.mjs
new file mode 100644
index 00000000000..0334a14f118
--- /dev/null
+++ b/tests/ports/webassembly/asyncio_await_resolve_no_arg.mjs
@@ -0,0 +1,21 @@
+// Test an asyncio task await'ing on a Promise that's resolved without an argument.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+globalThis.foo = new Promise((resolve) => {
+    console.log(1);
+    resolve(); // resolve without an argument
+    console.log(2);
+});
+
+mp.runPython(`
+import asyncio
+import js
+
+async def task():
+    print(3)
+    print(await js.foo)
+    print(4)
+
+asyncio.create_task(task())
+`);
diff --git a/tests/ports/webassembly/asyncio_await_resolve_no_arg.mjs.exp b/tests/ports/webassembly/asyncio_await_resolve_no_arg.mjs.exp
new file mode 100644
index 00000000000..f3ca0f268af
--- /dev/null
+++ b/tests/ports/webassembly/asyncio_await_resolve_no_arg.mjs.exp
@@ -0,0 +1,5 @@
+1
+2
+3
+None
+4
diff --git a/tests/ports/webassembly/asyncio_create_task.mjs b/tests/ports/webassembly/asyncio_create_task.mjs
new file mode 100644
index 00000000000..e388ade4f12
--- /dev/null
+++ b/tests/ports/webassembly/asyncio_create_task.mjs
@@ -0,0 +1,44 @@
+// Test asyncio.create_task(), and tasks waiting on a Promise.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+globalThis.p0 = new Promise((resolve, reject) => {
+    resolve(123);
+});
+
+globalThis.p1 = new Promise((resolve, reject) => {
+    setTimeout(() => {
+        console.log("setTimeout resolved");
+        resolve(456);
+    }, 200);
+});
+
+mp.runPython(`
+import js
+import asyncio
+
+async def task(id, promise):
+    print("task start", id)
+    print("task await", id, await promise)
+    print("task await", id, await promise)
+    print("task end", id)
+
+print("start")
+t1 = asyncio.create_task(task(1, js.p0))
+t2 = asyncio.create_task(task(2, js.p1))
+print("t1", t1.done(), t2.done())
+print("end")
+`);
+
+// Wait for p1 to fulfill so t2 can continue.
+await globalThis.p1;
+
+// Wait a little longer so t2 can complete.
+await new Promise((resolve, reject) => {
+    setTimeout(resolve, 10);
+});
+
+mp.runPython(`
+print("restart")
+print("t1", t1.done(), t2.done())
+`);
diff --git a/tests/ports/webassembly/asyncio_create_task.mjs.exp b/tests/ports/webassembly/asyncio_create_task.mjs.exp
new file mode 100644
index 00000000000..c1958bba593
--- /dev/null
+++ b/tests/ports/webassembly/asyncio_create_task.mjs.exp
@@ -0,0 +1,14 @@
+start
+t1 False False
+end
+task start 1
+task start 2
+task await 1 123
+task await 1 123
+task end 1
+setTimeout resolved
+task await 2 456
+task await 2 456
+task end 2
+restart
+t1 True True
diff --git a/tests/ports/webassembly/asyncio_sleep.mjs b/tests/ports/webassembly/asyncio_sleep.mjs
new file mode 100644
index 00000000000..74d22ee1f85
--- /dev/null
+++ b/tests/ports/webassembly/asyncio_sleep.mjs
@@ -0,0 +1,25 @@
+// Test asyncio.sleep(), both at the top level and within a task.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+await mp.runPythonAsync(`
+import time
+import asyncio
+
+print("main start")
+t0 = time.time()
+await asyncio.sleep(0.25)
+dt = time.time() - t0
+print(0.2 <= dt <= 0.3)
+
+async def task():
+    print("task start")
+    t0 = time.time()
+    await asyncio.sleep(0.25)
+    dt = time.time() - t0
+    print(0.2 <= dt <= 0.3)
+    print("task end")
+
+asyncio.create_task(task())
+print("main end")
+`);
diff --git a/tests/ports/webassembly/asyncio_sleep.mjs.exp b/tests/ports/webassembly/asyncio_sleep.mjs.exp
new file mode 100644
index 00000000000..619ba175f6c
--- /dev/null
+++ b/tests/ports/webassembly/asyncio_sleep.mjs.exp
@@ -0,0 +1,6 @@
+main start
+True
+main end
+task start
+True
+task end
diff --git a/tests/ports/webassembly/asyncio_top_level_await.mjs b/tests/ports/webassembly/asyncio_top_level_await.mjs
new file mode 100644
index 00000000000..fedf10d9c5d
--- /dev/null
+++ b/tests/ports/webassembly/asyncio_top_level_await.mjs
@@ -0,0 +1,126 @@
+// Test top-level await on asyncio primitives: Task, Event.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+/**********************************************************/
+// Top-level await for an Event which is set by a JavaScript
+// callback.
+
+console.log("= TEST 1 ==========");
+
+await mp.runPythonAsync(`
+import asyncio
+import js
+
+event = asyncio.Event()
+
+def callback():
+    print("callback set event")
+    event.set()
+
+js.setTimeout(callback, 100)
+
+print("top-level wait event")
+await event.wait()
+print("top-level end")
+`);
+
+console.log("finished");
+
+/**********************************************************/
+// Top-level await for a Task which is cancelled by a
+// JavaScript callback.
+
+console.log("= TEST 2 ==========");
+
+await mp.runPythonAsync(`
+import asyncio
+import js
+import time
+
+async def task():
+    print("task start")
+    await asyncio.sleep(5)
+    print("task end")
+
+def callback():
+    print("callback cancel task")
+    t.cancel()
+
+t  = asyncio.create_task(task())
+js.setTimeout(callback, 100)
+
+print("top-level wait task")
+try:
+    t0 = time.time()
+    await t
+except asyncio.CancelledError:
+    dt = time.time() - t0
+    print("top-level task CancelledError", dt < 1)
+`);
+
+console.log("finished");
+
+/**********************************************************/
+// Top-level await for an Event and a Task, with the task
+// setting the event.
+
+console.log("= TEST 3 ==========");
+
+await mp.runPythonAsync(`
+import asyncio
+
+async def task(event):
+    print("task set event")
+    event.set()
+    print("task sleep")
+    await asyncio.sleep(0.1)
+    print("task end")
+
+event = asyncio.Event()
+t = asyncio.create_task(task(event))
+
+print("top-level wait event")
+await event.wait()
+print("top-level wait task")
+await t
+print("top-level end")
+`);
+
+console.log("finished");
+
+/**********************************************************/
+// Top-level await's on a JavaScript function that throws.
+
+console.log("= TEST 4 ==========");
+
+globalThis.jsFail = async () => {
+    console.log("jsFail");
+    throw new Error("jsFail");
+};
+
+await mp.runPythonAsync(`
+import asyncio
+import js
+
+# Test top-level catching from a failed JS await.
+try:
+    await js.jsFail()
+except Exception as er:
+    print("caught exception:", type(er), type(er.args[0]), er.args[1:])
+
+async def main():
+    try:
+        await js.jsFail()
+    except Exception as er:
+        print("caught exception:", type(er), type(er.args[0]), er.args[1:])
+
+# Test top-level waiting on a coro that catches.
+await main()
+
+# Test top-level waiting on a task that catches.
+t = asyncio.create_task(main())
+await t
+`);
+
+console.log("finished");
diff --git a/tests/ports/webassembly/asyncio_top_level_await.mjs.exp b/tests/ports/webassembly/asyncio_top_level_await.mjs.exp
new file mode 100644
index 00000000000..f6720b5aba7
--- /dev/null
+++ b/tests/ports/webassembly/asyncio_top_level_await.mjs.exp
@@ -0,0 +1,27 @@
+= TEST 1 ==========
+top-level wait event
+callback set event
+top-level end
+finished
+= TEST 2 ==========
+top-level wait task
+task start
+callback cancel task
+top-level task CancelledError True
+finished
+= TEST 3 ==========
+top-level wait event
+task set event
+task sleep
+top-level wait task
+task end
+top-level end
+finished
+= TEST 4 ==========
+jsFail
+caught exception:   ('Error', 'jsFail')
+jsFail
+caught exception:   ('Error', 'jsFail')
+jsFail
+caught exception:   ('Error', 'jsFail')
+finished
diff --git a/tests/ports/webassembly/await_error_handling.mjs b/tests/ports/webassembly/await_error_handling.mjs
new file mode 100644
index 00000000000..067bc251381
--- /dev/null
+++ b/tests/ports/webassembly/await_error_handling.mjs
@@ -0,0 +1,22 @@
+// Test await'ing on a JavaScript async function that throws a JavaScript Error.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+globalThis.foo = async () => {
+    console.log(2);
+    throw Error("test");
+};
+
+await mp.runPythonAsync(`
+import js, jsffi
+print(1)
+try:
+    await js.foo()
+except jsffi.JsException as er:
+    error = er
+print(error)
+print(3)
+`);
+
+const error = mp.globals.get("error");
+console.log(error instanceof Error, error.name, error.message);
diff --git a/tests/ports/webassembly/await_error_handling.mjs.exp b/tests/ports/webassembly/await_error_handling.mjs.exp
new file mode 100644
index 00000000000..0c8eb52f20f
--- /dev/null
+++ b/tests/ports/webassembly/await_error_handling.mjs.exp
@@ -0,0 +1,5 @@
+1
+2
+(, 'Error', 'test')
+3
+true Error test
diff --git a/tests/ports/webassembly/await_js_async_py.mjs b/tests/ports/webassembly/await_js_async_py.mjs
new file mode 100644
index 00000000000..953a7b783d5
--- /dev/null
+++ b/tests/ports/webassembly/await_js_async_py.mjs
@@ -0,0 +1,35 @@
+// Test JavaScript await'ing on Python async functions.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+globalThis.asyncTimeout = (ms) =>
+    new Promise((resolve) => setTimeout(resolve, ms));
+
+mp.runPython(`
+import js
+
+def f0():
+    print("f0 run")
+    return 1
+
+async def f1():
+    print("f1 run")
+    return 2
+
+async def f2():
+    print("f2 start")
+    await js.asyncTimeout(0)
+    print("f2 end")
+    return 3
+
+async def f3():
+    print("f3 start")
+    ret = await f2()
+    print("f3 end")
+    return ret + 1
+`);
+
+console.log("f0 return:", await mp.globals.get("f0")());
+console.log("f1 return:", await mp.globals.get("f1")());
+console.log("f2 return:", await mp.globals.get("f2")());
+console.log("f3 return:", await mp.globals.get("f3")());
diff --git a/tests/ports/webassembly/await_js_async_py.mjs.exp b/tests/ports/webassembly/await_js_async_py.mjs.exp
new file mode 100644
index 00000000000..ab8cbabebb3
--- /dev/null
+++ b/tests/ports/webassembly/await_js_async_py.mjs.exp
@@ -0,0 +1,12 @@
+f0 run
+f0 return: 1
+f1 run
+f1 return: 2
+f2 start
+f2 end
+f2 return: 3
+f3 start
+f2 start
+f2 end
+f3 end
+f3 return: 4
diff --git a/tests/ports/webassembly/basic.js b/tests/ports/webassembly/basic.js
new file mode 100644
index 00000000000..19b1881faf5
--- /dev/null
+++ b/tests/ports/webassembly/basic.js
@@ -0,0 +1,8 @@
+import(process.argv[2]).then((mp) => {
+    mp.loadMicroPython().then((py) => {
+        py.runPython("1");
+        py.runPython("print('hello')");
+        py.runPython("import sys; print(f'hello from {sys.platform}')");
+        py.runPython("import collections; print(collections.OrderedDict)");
+    });
+});
diff --git a/tests/ports/webassembly/basic.js.exp b/tests/ports/webassembly/basic.js.exp
new file mode 100644
index 00000000000..6459b2492cf
--- /dev/null
+++ b/tests/ports/webassembly/basic.js.exp
@@ -0,0 +1,3 @@
+hello
+hello from webassembly
+
diff --git a/tests/ports/webassembly/filesystem.mjs b/tests/ports/webassembly/filesystem.mjs
new file mode 100644
index 00000000000..e9e2920a161
--- /dev/null
+++ b/tests/ports/webassembly/filesystem.mjs
@@ -0,0 +1,9 @@
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.FS.mkdir("/lib/");
+mp.FS.writeFile("/lib/testmod.py", "x = 1; print(__name__, x)");
+mp.runPython("import testmod");
+
+mp.runPython("import sys; sys.modules.clear()");
+const testmod = mp.pyimport("testmod");
+console.log("testmod:", testmod, testmod.x);
diff --git a/tests/ports/webassembly/filesystem.mjs.exp b/tests/ports/webassembly/filesystem.mjs.exp
new file mode 100644
index 00000000000..25a48b10832
--- /dev/null
+++ b/tests/ports/webassembly/filesystem.mjs.exp
@@ -0,0 +1,3 @@
+testmod 1
+testmod 1
+testmod: PyProxy { _ref: 3 } 1
diff --git a/tests/ports/webassembly/float.mjs b/tests/ports/webassembly/float.mjs
new file mode 100644
index 00000000000..53bb8b1c4d0
--- /dev/null
+++ b/tests/ports/webassembly/float.mjs
@@ -0,0 +1,43 @@
+// Test passing floats between JavaScript and Python.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+globalThis.a = 1 / 2;
+globalThis.b = Infinity;
+globalThis.c = NaN;
+
+mp.runPython(`
+import js
+
+# Test retrieving floats from JS.
+print(js.a)
+print(js.b)
+print(js.c)
+
+# Test calling JS which returns a float.
+r = js.Math.random()
+print(type(r), 0 < r < 1)
+
+x = 1 / 2
+y = float("inf")
+z = float("nan")
+
+# Test passing floats to a JS function.
+js.console.log(x)
+js.console.log(x, y)
+js.console.log(x, y, z)
+`);
+
+// Test retrieving floats from Python.
+console.log(mp.globals.get("x"));
+console.log(mp.globals.get("y"));
+console.log(mp.globals.get("z"));
+
+// Test passing floats to a Python function.
+const mp_print = mp.pyimport("builtins").print;
+mp_print(globalThis.a);
+mp_print(globalThis.a, globalThis.b);
+mp_print(globalThis.a, globalThis.b, globalThis.c);
+
+// Test calling Python which returns a float.
+console.log(mp.pyimport("math").sqrt(0.16));
diff --git a/tests/ports/webassembly/float.mjs.exp b/tests/ports/webassembly/float.mjs.exp
new file mode 100644
index 00000000000..57eff74acd8
--- /dev/null
+++ b/tests/ports/webassembly/float.mjs.exp
@@ -0,0 +1,14 @@
+0.5
+inf
+nan
+ True
+0.5
+0.5 Infinity
+0.5 Infinity NaN
+0.5
+Infinity
+NaN
+0.5
+0.5 inf
+0.5 inf nan
+0.4
diff --git a/tests/ports/webassembly/fun_call.mjs b/tests/ports/webassembly/fun_call.mjs
new file mode 100644
index 00000000000..3a24d61bb93
--- /dev/null
+++ b/tests/ports/webassembly/fun_call.mjs
@@ -0,0 +1,44 @@
+// Test calling JavaScript functions from Python.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+globalThis.f = (a, b, c, d, e) => {
+    console.log(a, b, c, d, e);
+};
+mp.runPython(`
+import js
+js.f()
+js.f(1)
+js.f(1, 2)
+js.f(1, 2, 3)
+js.f(1, 2, 3, 4)
+js.f(1, 2, 3, 4, 5)
+js.f(1, 2, 3, 4, 5, 6)
+`);
+
+globalThis.g = (...args) => {
+    console.log(args);
+};
+mp.runPython(`
+import js
+js.g()
+js.g(a=1)
+js.g(a=1, b=2)
+js.g(a=1, b=2, c=3)
+js.g(a=1, b=2, c=3, d=4)
+js.g(a=1, b=2, c=3, d=4, e=5)
+js.g(1)
+js.g(1, b=2)
+js.g(1, b=2, c=3)
+js.g(1, b=2, c=3, d=4)
+js.g(1, b=2, c=3, d=4, e=5)
+js.g(1, 2)
+js.g(1, 2, c=3)
+js.g(1, 2, c=3, d=4)
+js.g(1, 2, c=3, d=4, e=5)
+js.g(1, 2, 3)
+js.g(1, 2, 3, d=4)
+js.g(1, 2, 3, d=4, e=5)
+js.g(1, 2, 3, 4)
+js.g(1, 2, 3, 4, e=5)
+`);
diff --git a/tests/ports/webassembly/fun_call.mjs.exp b/tests/ports/webassembly/fun_call.mjs.exp
new file mode 100644
index 00000000000..57edb39a47c
--- /dev/null
+++ b/tests/ports/webassembly/fun_call.mjs.exp
@@ -0,0 +1,27 @@
+undefined undefined undefined undefined undefined
+1 undefined undefined undefined undefined
+1 2 undefined undefined undefined
+1 2 3 undefined undefined
+1 2 3 4 undefined
+1 2 3 4 5
+1 2 3 4 5
+[]
+[ { a: 1 } ]
+[ { a: 1, b: 2 } ]
+[ { a: 1, b: 2, c: 3 } ]
+[ { a: 1, b: 2, c: 3, d: 4 } ]
+[ { a: 1, b: 2, c: 3, d: 4, e: 5 } ]
+[ 1 ]
+[ 1, { b: 2 } ]
+[ 1, { b: 2, c: 3 } ]
+[ 1, { b: 2, c: 3, d: 4 } ]
+[ 1, { b: 2, c: 3, d: 4, e: 5 } ]
+[ 1, 2 ]
+[ 1, 2, { c: 3 } ]
+[ 1, 2, { c: 3, d: 4 } ]
+[ 1, 2, { c: 3, d: 4, e: 5 } ]
+[ 1, 2, 3 ]
+[ 1, 2, 3, { d: 4 } ]
+[ 1, 2, 3, { d: 4, e: 5 } ]
+[ 1, 2, 3, 4 ]
+[ 1, 2, 3, 4, { e: 5 } ]
diff --git a/tests/ports/webassembly/fun_proxy.mjs b/tests/ports/webassembly/fun_proxy.mjs
new file mode 100644
index 00000000000..87cce44f2bc
--- /dev/null
+++ b/tests/ports/webassembly/fun_proxy.mjs
@@ -0,0 +1,52 @@
+// Test proxying of functions between Python and JavaScript.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+// Create JS functions living on the JS and Py sides.
+globalThis.jsFunAdd = (x, y) => x + y;
+mp.globals.set("js_fun_sub", (x, y) => x - y);
+
+console.log("== JavaScript side ==");
+
+// JS function living on the JS side, should be a function.
+console.log(globalThis.jsFunAdd);
+console.log(globalThis.jsFunAdd(1, 2));
+
+// JS function living on the Py side, should be a function.
+console.log(mp.globals.get("js_fun_sub"));
+console.log(mp.globals.get("js_fun_sub")(1, 2));
+
+mp.runPython(`
+import js
+
+print("== Python side ==")
+
+py_fun_mul = lambda x, y: x * y
+js.pyFunDiv = lambda x, y: x / y
+
+# JS function living on the JS side, should be a JsProxy.
+print(type(js.jsFunAdd))
+print(js.jsFunAdd(1, 2))
+
+# JS function living on the Py side, should be a JsProxy.
+print(type(js_fun_sub))
+print(js_fun_sub(1, 2))
+
+# Py function living on the Py side, should be a function.
+print(type(py_fun_mul))
+print(py_fun_mul(2, 3))
+
+# Py function living on the JS side, should be a function.
+print(type(js.pyFunDiv))
+print(js.pyFunDiv(6, 2))
+`);
+
+console.log("== JavaScript side ==");
+
+// Py function living on the Py side, should be a proxy function.
+console.log(mp.globals.get("py_fun_mul"));
+console.log(mp.globals.get("py_fun_mul")(2, 3));
+
+// Py function living on the JS side, should be a proxy function.
+console.log(globalThis.pyFunDiv);
+console.log(globalThis.pyFunDiv(6, 2));
diff --git a/tests/ports/webassembly/fun_proxy.mjs.exp b/tests/ports/webassembly/fun_proxy.mjs.exp
new file mode 100644
index 00000000000..4aeaa47b225
--- /dev/null
+++ b/tests/ports/webassembly/fun_proxy.mjs.exp
@@ -0,0 +1,19 @@
+== JavaScript side ==
+[Function (anonymous)]
+3
+[Function (anonymous)]
+-1
+== Python side ==
+
+3
+
+-1
+
+6
+
+3.0
+== JavaScript side ==
+[Function: obj] { _ref: 4 }
+6
+[Function: obj] { _ref: 3 }
+3
diff --git a/tests/ports/webassembly/fun_py_callback_js.mjs b/tests/ports/webassembly/fun_py_callback_js.mjs
new file mode 100644
index 00000000000..5541e7a4617
--- /dev/null
+++ b/tests/ports/webassembly/fun_py_callback_js.mjs
@@ -0,0 +1,31 @@
+// Test using Python functions as JS callbacks.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+globalThis.asyncTimeout = (ms) =>
+    new Promise((resolve) => setTimeout(resolve, ms));
+
+mp.runPython(`
+import js
+
+def f0():
+    print("f0 run")
+
+async def f1():
+    print("f1 run")
+
+async def f2():
+    print("f2 start")
+    await js.asyncTimeout(0)
+    print("f2 end")
+
+async def f3():
+    print("f3 start")
+    await f2()
+    print("f3 end")
+
+js.setTimeout(f0, 0)
+js.setTimeout(f1, 50)
+js.setTimeout(f2, 100)
+js.setTimeout(f3, 150)
+`);
diff --git a/tests/ports/webassembly/fun_py_callback_js.mjs.exp b/tests/ports/webassembly/fun_py_callback_js.mjs.exp
new file mode 100644
index 00000000000..732062c840e
--- /dev/null
+++ b/tests/ports/webassembly/fun_py_callback_js.mjs.exp
@@ -0,0 +1,8 @@
+f0 run
+f1 run
+f2 start
+f2 end
+f3 start
+f2 start
+f2 end
+f3 end
diff --git a/tests/ports/webassembly/gc_behaviour.mjs b/tests/ports/webassembly/gc_behaviour.mjs
new file mode 100644
index 00000000000..65e69ffa95d
--- /dev/null
+++ b/tests/ports/webassembly/gc_behaviour.mjs
@@ -0,0 +1,31 @@
+// Test GC behaviour, that objects are not prematurely collected.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+// This test converts a Python list/dict to the corresponding JavaScript object and
+// returns it to JavaScript.  Internally it will be wrapped in a `JsProxy`, and the
+// call to `gc.collect()` will force a GC collection upon exit of the `make_xxx()`
+// function call but before the `JsProxy` is passed to JavaScript.  The test ensures
+// that the `JsProxy` doesn't get prematurely collected before JavaScript has a chance
+// to receive the underlying JavaScript object.
+
+mp.runPython(`
+import gc, js, jsffi
+
+def make_list():
+    obj = jsffi.to_js([1, 2, 3])
+    print(obj)
+    js.console.log(obj)
+    gc.collect()
+    return obj
+
+def make_dict():
+    obj = jsffi.to_js({"key": "value"})
+    print(obj)
+    js.console.log(obj)
+    gc.collect()
+    return obj
+`);
+
+console.log(mp.globals.get("make_list")());
+console.log(mp.globals.get("make_dict")());
diff --git a/tests/ports/webassembly/gc_behaviour.mjs.exp b/tests/ports/webassembly/gc_behaviour.mjs.exp
new file mode 100644
index 00000000000..a1bb658e3df
--- /dev/null
+++ b/tests/ports/webassembly/gc_behaviour.mjs.exp
@@ -0,0 +1,6 @@
+
+[ 1, 2, 3 ]
+[ 1, 2, 3 ]
+
+{ key: 'value' }
+{ key: 'value' }
diff --git a/tests/ports/webassembly/globals.mjs b/tests/ports/webassembly/globals.mjs
new file mode 100644
index 00000000000..3d5ecb41678
--- /dev/null
+++ b/tests/ports/webassembly/globals.mjs
@@ -0,0 +1,13 @@
+// Test accessing Python globals dict via mp.globals.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython("x = 1");
+console.log(mp.globals.get("x"));
+
+mp.globals.set("y", 2);
+mp.runPython("print(y)");
+
+mp.runPython("print('y' in globals())");
+mp.globals.delete("y");
+mp.runPython("print('y' in globals())");
diff --git a/tests/ports/webassembly/globals.mjs.exp b/tests/ports/webassembly/globals.mjs.exp
new file mode 100644
index 00000000000..a118c13fecb
--- /dev/null
+++ b/tests/ports/webassembly/globals.mjs.exp
@@ -0,0 +1,4 @@
+1
+2
+True
+False
diff --git a/tests/ports/webassembly/heap_expand.mjs b/tests/ports/webassembly/heap_expand.mjs
new file mode 100644
index 00000000000..2cf7c07b0b8
--- /dev/null
+++ b/tests/ports/webassembly/heap_expand.mjs
@@ -0,0 +1,15 @@
+// Test expanding the MicroPython GC heap.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+import gc
+bs = []
+for i in range(24):
+    b = bytearray(1 << i)
+    bs.append(b)
+    gc.collect()
+    print(gc.mem_free())
+for b in bs:
+    print(len(b))
+`);
diff --git a/tests/ports/webassembly/heap_expand.mjs.exp b/tests/ports/webassembly/heap_expand.mjs.exp
new file mode 100644
index 00000000000..4161fc7eaed
--- /dev/null
+++ b/tests/ports/webassembly/heap_expand.mjs.exp
@@ -0,0 +1,48 @@
+135233568
+135233536
+135233504
+135233472
+135233424
+135233376
+135233296
+135233152
+135232848
+135232320
+135231280
+135229216
+135225104
+135216896
+135200496
+135167712
+135102096
+134971008
+134708848
+135201312
+136186256
+138156160
+142095984
+149975648
+1
+2
+4
+8
+16
+32
+64
+128
+256
+512
+1024
+2048
+4096
+8192
+16384
+32768
+65536
+131072
+262144
+524288
+1048576
+2097152
+4194304
+8388608
diff --git a/tests/ports/webassembly/iterator.mjs b/tests/ports/webassembly/iterator.mjs
new file mode 100644
index 00000000000..91415d530f9
--- /dev/null
+++ b/tests/ports/webassembly/iterator.mjs
@@ -0,0 +1,21 @@
+// Test accessing Python iterables from JavaScript via the JavaScript iterator protocol.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+    s = "abc"
+    l = [1, 2, 3]
+`);
+
+// Iterate a Python string.
+for (const value of mp.globals.get("s")) {
+    console.log(value);
+}
+
+// Iterate a Python list.
+for (const value of mp.globals.get("l")) {
+    console.log(value);
+}
+
+// Iterate a Python list from a built-in JavaScript constructor.
+mp.runPython("import js; print(js.Set.new([1, 2, 3]).has(3))");
diff --git a/tests/ports/webassembly/iterator.mjs.exp b/tests/ports/webassembly/iterator.mjs.exp
new file mode 100644
index 00000000000..9d1aaac117d
--- /dev/null
+++ b/tests/ports/webassembly/iterator.mjs.exp
@@ -0,0 +1,7 @@
+a
+b
+c
+1
+2
+3
+True
diff --git a/tests/ports/webassembly/js_proxy_attribute.mjs b/tests/ports/webassembly/js_proxy_attribute.mjs
new file mode 100644
index 00000000000..5d950122778
--- /dev/null
+++ b/tests/ports/webassembly/js_proxy_attribute.mjs
@@ -0,0 +1,34 @@
+// Test lookup of attributes on JsProxy objects.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+// Simple attribute names and values.
+globalThis.obj1 = { a: 1, b: 2 };
+
+// Unconventional attribute names and values.
+globalThis.obj2 = { undefined: "undefined", undef: undefined };
+
+// Dynamically created attribute names and values.
+globalThis.obj3 = new Proxy(new Map(), {
+    get(map, name) {
+        if (!map.has(name)) {
+            console.log("creating attribute", name);
+            map.set(name, name);
+        }
+        return map.get(name);
+    },
+});
+
+mp.runPython(`
+import js
+
+print(js.obj1.a, js.obj1.b)
+print(js.obj1["a"], js.obj1["b"])
+
+print(js.obj2.undefined, js.obj2.undef)
+
+print(js.obj3.c)
+print(js.obj3["c"])
+print(hasattr(js.obj3, "d"))
+print(js.obj3.d)
+`);
diff --git a/tests/ports/webassembly/js_proxy_attribute.mjs.exp b/tests/ports/webassembly/js_proxy_attribute.mjs.exp
new file mode 100644
index 00000000000..7de85a56648
--- /dev/null
+++ b/tests/ports/webassembly/js_proxy_attribute.mjs.exp
@@ -0,0 +1,9 @@
+1 2
+1 2
+undefined 
+creating attribute c
+c
+c
+creating attribute d
+True
+d
diff --git a/tests/ports/webassembly/js_proxy_identity.mjs b/tests/ports/webassembly/js_proxy_identity.mjs
new file mode 100644
index 00000000000..ca2f3980af4
--- /dev/null
+++ b/tests/ports/webassembly/js_proxy_identity.mjs
@@ -0,0 +1,25 @@
+// Test identity of JsProxy when they are the same JavaScript object.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+import js
+
+print(js.Object)
+
+print("Object equality")
+print(js.Object == js.Object)
+print(js.Object.assign == js.Object.assign)
+
+print("Object identity")
+print(js.Object is js.Object)
+
+print("Array equality")
+print(js.Array == js.Array)
+print(js.Array.prototype == js.Array.prototype)
+print(js.Array.prototype.push == js.Array.prototype.push)
+
+print("Array identity")
+print(js.Array is js.Array)
+print(js.Array.prototype is js.Array.prototype)
+`);
diff --git a/tests/ports/webassembly/js_proxy_identity.mjs.exp b/tests/ports/webassembly/js_proxy_identity.mjs.exp
new file mode 100644
index 00000000000..d8f1ae89139
--- /dev/null
+++ b/tests/ports/webassembly/js_proxy_identity.mjs.exp
@@ -0,0 +1,13 @@
+
+Object equality
+True
+True
+Object identity
+True
+Array equality
+True
+True
+True
+Array identity
+True
+True
diff --git a/tests/ports/webassembly/js_proxy_iterator.mjs b/tests/ports/webassembly/js_proxy_iterator.mjs
new file mode 100644
index 00000000000..34b54b4837a
--- /dev/null
+++ b/tests/ports/webassembly/js_proxy_iterator.mjs
@@ -0,0 +1,14 @@
+// Test accessing JavaScript iterables (objects with Symbol.iterator) from Python.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+import js
+
+for v in js.Set.new([1, 2]):
+    print(v)
+
+url_search_params = js.URLSearchParams.new("one=1&two=2")
+for key in url_search_params.keys():
+    print(key, list(url_search_params.getAll(key)))
+`);
diff --git a/tests/ports/webassembly/js_proxy_iterator.mjs.exp b/tests/ports/webassembly/js_proxy_iterator.mjs.exp
new file mode 100644
index 00000000000..e8d11286c60
--- /dev/null
+++ b/tests/ports/webassembly/js_proxy_iterator.mjs.exp
@@ -0,0 +1,4 @@
+1
+2
+one ['1']
+two ['2']
diff --git a/tests/ports/webassembly/js_proxy_reuse_free.mjs b/tests/ports/webassembly/js_proxy_reuse_free.mjs
new file mode 100644
index 00000000000..ebca86f0bad
--- /dev/null
+++ b/tests/ports/webassembly/js_proxy_reuse_free.mjs
@@ -0,0 +1,48 @@
+// Test reuse of JsProxy references and freeing of JsProxy objects.
+// This ensures that a Python-side JsProxy that refers to a JavaScript object retains
+// the correct JavaScript object in the case that another JsProxy that refers to the
+// same JavaScript object is freed.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+globalThis.obj = [1, 2];
+globalThis.obj2 = [3, 4];
+
+console.log("JS obj:", globalThis.obj);
+
+mp.runPython(`
+import gc
+import js
+
+# Create 2 proxies of the same JS object.
+# They should refer to the same underlying JS-side reference.
+obj = js.obj
+obj_copy = js.obj
+print(obj, obj_copy, obj == obj_copy)
+
+# Print out the object.
+js.console.log("Py obj:", obj)
+
+# Forget obj_copy and trigger a GC when the Python code finishes.
+obj_copy = None
+gc.collect()
+`);
+
+console.log("JS obj:", globalThis.obj);
+
+mp.runPython(`
+# Create a new proxy of a different object.
+# It should not clobber the existing obj proxy reference.
+obj2 = js.obj2
+
+# Create a copy of the existing obj proxy.
+obj_copy = js.obj
+
+# Print the JS proxy, it should be the same reference as before.
+print(obj, obj_copy, obj == obj_copy)
+
+# Print out the object.
+js.console.log("Py obj:", obj)
+`);
+
+console.log("JS obj:", globalThis.obj);
diff --git a/tests/ports/webassembly/js_proxy_reuse_free.mjs.exp b/tests/ports/webassembly/js_proxy_reuse_free.mjs.exp
new file mode 100644
index 00000000000..c74e4f49ee4
--- /dev/null
+++ b/tests/ports/webassembly/js_proxy_reuse_free.mjs.exp
@@ -0,0 +1,7 @@
+JS obj: [ 1, 2 ]
+  True
+Py obj: [ 1, 2 ]
+JS obj: [ 1, 2 ]
+  True
+Py obj: [ 1, 2 ]
+JS obj: [ 1, 2 ]
diff --git a/tests/ports/webassembly/jsffi_create_proxy.mjs b/tests/ports/webassembly/jsffi_create_proxy.mjs
new file mode 100644
index 00000000000..5f04bf44d7e
--- /dev/null
+++ b/tests/ports/webassembly/jsffi_create_proxy.mjs
@@ -0,0 +1,15 @@
+// Test jsffi.create_proxy().
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+import jsffi
+x = jsffi.create_proxy(1)
+print(x)
+y = jsffi.create_proxy([2])
+print(y)
+`);
+console.log(mp.globals.get("x"));
+console.log(mp.PyProxy.toJs(mp.globals.get("x")));
+console.log(mp.globals.get("y"));
+console.log(mp.PyProxy.toJs(mp.globals.get("y")));
diff --git a/tests/ports/webassembly/jsffi_create_proxy.mjs.exp b/tests/ports/webassembly/jsffi_create_proxy.mjs.exp
new file mode 100644
index 00000000000..5e541f0b0d8
--- /dev/null
+++ b/tests/ports/webassembly/jsffi_create_proxy.mjs.exp
@@ -0,0 +1,6 @@
+1
+
+1
+1
+PyProxy { _ref: 3 }
+[ 2 ]
diff --git a/tests/ports/webassembly/jsffi_to_js.mjs b/tests/ports/webassembly/jsffi_to_js.mjs
new file mode 100644
index 00000000000..241e67fb284
--- /dev/null
+++ b/tests/ports/webassembly/jsffi_to_js.mjs
@@ -0,0 +1,47 @@
+// Test jsffi.to_js().
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+import jsffi
+x = jsffi.to_js(1)
+print(x)
+y = jsffi.to_js([2])
+print(y)
+z = jsffi.to_js({"three":3})
+print(z)
+`);
+
+const x = mp.globals.get("x");
+const y = mp.globals.get("y");
+const z = mp.globals.get("z");
+
+console.log(Array.isArray(x));
+console.log(x);
+
+console.log(Array.isArray(y));
+console.log(y);
+console.log(Reflect.ownKeys(y));
+
+console.log(Array.isArray(z));
+console.log(z);
+console.log(Reflect.ownKeys(z));
+
+/**********************************************************/
+// Test using to_js on objects with the buffer protocol.
+
+console.log("= TEST BUFFER =====");
+
+mp.runPython(`
+import jsffi
+buf1 = jsffi.to_js(b"1234")
+print(buf1)
+buf2 = jsffi.to_js(bytearray(b"5678"))
+print(buf2)
+buf3 = jsffi.to_js(memoryview(b"abcd"))
+print(buf3)
+`);
+
+console.log(mp.globals.get("buf1"));
+console.log(mp.globals.get("buf2"));
+console.log(mp.globals.get("buf3"));
diff --git a/tests/ports/webassembly/jsffi_to_js.mjs.exp b/tests/ports/webassembly/jsffi_to_js.mjs.exp
new file mode 100644
index 00000000000..4e7d0e99f86
--- /dev/null
+++ b/tests/ports/webassembly/jsffi_to_js.mjs.exp
@@ -0,0 +1,18 @@
+1
+
+
+false
+1
+true
+[ 2 ]
+[ '0', 'length' ]
+false
+{ three: 3 }
+[ 'three' ]
+= TEST BUFFER =====
+
+
+
+Uint8Array(4) [ 49, 50, 51, 52 ]
+Uint8Array(4) [ 53, 54, 55, 56 ]
+Uint8Array(4) [ 97, 98, 99, 100 ]
diff --git a/tests/ports/webassembly/method_bind_behaviour.mjs b/tests/ports/webassembly/method_bind_behaviour.mjs
new file mode 100644
index 00000000000..24de0fa3bb1
--- /dev/null
+++ b/tests/ports/webassembly/method_bind_behaviour.mjs
@@ -0,0 +1,43 @@
+// Test how JavaScript binds self/this when methods are called from Python.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+// Test accessing and calling JavaScript methods from Python.
+mp.runPython(`
+import js
+
+# Get the push method to call later on.
+push = js.Array.prototype.push
+
+# Create initial array.
+ar = js.Array(1, 2)
+js.console.log(ar)
+
+# Add an element using a method (should implicitly supply "ar" as context).
+print(ar.push(3))
+js.console.log(ar)
+
+# Add an element using prototype function, need to explicitly provide "ar" as context.
+print(push.call(ar, 4))
+js.console.log(ar)
+
+# Add an element using a method with call and explicit context.
+print(ar.push.call(ar, 5))
+js.console.log(ar)
+
+# Add an element using a different instances method with call and explicit context.
+print(js.Array().push.call(ar, 6))
+js.console.log(ar)
+`);
+
+// Test assigning Python functions to JavaScript objects, and using them like a method.
+mp.runPython(`
+import js
+
+a = js.Object()
+a.meth1 = lambda *x: print("meth1", x)
+a.meth1(1, 2)
+
+js.Object.prototype.meth2 = lambda *x: print("meth2", x)
+a.meth2(3, 4)
+`);
diff --git a/tests/ports/webassembly/method_bind_behaviour.mjs.exp b/tests/ports/webassembly/method_bind_behaviour.mjs.exp
new file mode 100644
index 00000000000..ab3743f6672
--- /dev/null
+++ b/tests/ports/webassembly/method_bind_behaviour.mjs.exp
@@ -0,0 +1,11 @@
+[ 1, 2 ]
+3
+[ 1, 2, 3 ]
+4
+[ 1, 2, 3, 4 ]
+5
+[ 1, 2, 3, 4, 5 ]
+6
+[ 1, 2, 3, 4, 5, 6 ]
+meth1 (1, 2)
+meth2 (3, 4)
diff --git a/tests/ports/webassembly/override_new.mjs b/tests/ports/webassembly/override_new.mjs
new file mode 100644
index 00000000000..f5d64d7d113
--- /dev/null
+++ b/tests/ports/webassembly/override_new.mjs
@@ -0,0 +1,33 @@
+// Test overriding .new() on a JavaScript class.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+globalThis.MyClass1 = class {
+    new() {
+        console.log("MyClass1 new");
+        return 1;
+    }
+};
+
+globalThis.MyClass2 = class {
+    static new() {
+        console.log("MyClass2 static new");
+        return 2;
+    }
+    new() {
+        console.log("MyClass2 new");
+        return 3;
+    }
+};
+
+globalThis.myClass2Instance = new globalThis.MyClass2();
+
+mp.runPython(`
+    import js
+
+    print(type(js.MyClass1.new()))
+    print(js.MyClass1.new().new())
+
+    print(js.MyClass2.new())
+    print(js.myClass2Instance.new())
+`);
diff --git a/tests/ports/webassembly/override_new.mjs.exp b/tests/ports/webassembly/override_new.mjs.exp
new file mode 100644
index 00000000000..2efb669714e
--- /dev/null
+++ b/tests/ports/webassembly/override_new.mjs.exp
@@ -0,0 +1,7 @@
+
+MyClass1 new
+1
+MyClass2 static new
+2
+MyClass2 new
+3
diff --git a/tests/ports/webassembly/promise_with_resolvers.mjs b/tests/ports/webassembly/promise_with_resolvers.mjs
new file mode 100644
index 00000000000..a2c6d509a60
--- /dev/null
+++ b/tests/ports/webassembly/promise_with_resolvers.mjs
@@ -0,0 +1,23 @@
+// Test polyfill of a method on a built-in.
+
+// Implement Promise.withResolvers, and make sure it has a unique name so
+// the test below is guaranteed to use this version.
+Promise.withResolversCustom = function withResolversCustom() {
+    let a;
+    let b;
+    const c = new this((resolve, reject) => {
+        a = resolve;
+        b = reject;
+    });
+    return { resolve: a, reject: b, promise: c };
+};
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+    from js import Promise
+
+    deferred = Promise.withResolversCustom()
+    deferred.promise.then(print)
+    deferred.resolve('OK')
+`);
diff --git a/tests/ports/webassembly/promise_with_resolvers.mjs.exp b/tests/ports/webassembly/promise_with_resolvers.mjs.exp
new file mode 100644
index 00000000000..d86bac9de59
--- /dev/null
+++ b/tests/ports/webassembly/promise_with_resolvers.mjs.exp
@@ -0,0 +1 @@
+OK
diff --git a/tests/ports/webassembly/py_proxy_delete.mjs b/tests/ports/webassembly/py_proxy_delete.mjs
new file mode 100644
index 00000000000..2afea0b9f22
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_delete.mjs
@@ -0,0 +1,30 @@
+// Test `delete .` on the JavaScript side, which tests PyProxy.deleteProperty.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+class A:
+    pass
+x = A()
+x.foo = 1
+y = []
+`);
+
+const x = mp.globals.get("x");
+const y = mp.globals.get("y");
+
+// Should pass.
+// biome-ignore lint/performance/noDelete: test delete statement
+delete x.foo;
+
+mp.runPython(`
+print(hasattr(x, "foo"))
+`);
+
+// Should fail, can't delete attributes on MicroPython lists.
+try {
+    // biome-ignore lint/performance/noDelete: test delete statement
+    delete y.sort;
+} catch (error) {
+    console.log(error.message);
+}
diff --git a/tests/ports/webassembly/py_proxy_delete.mjs.exp b/tests/ports/webassembly/py_proxy_delete.mjs.exp
new file mode 100644
index 00000000000..8eb9ad15014
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_delete.mjs.exp
@@ -0,0 +1,2 @@
+False
+'deleteProperty' on proxy: trap returned falsish for property 'sort'
diff --git a/tests/ports/webassembly/py_proxy_dict.mjs b/tests/ports/webassembly/py_proxy_dict.mjs
new file mode 100644
index 00000000000..201f179ef37
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_dict.mjs
@@ -0,0 +1,34 @@
+// Test passing a Python dict into JavaScript, it should act like a JS object.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+x = {"a": 1, "b": 2}
+`);
+
+const x = mp.globals.get("x");
+
+// Test has, get, keys/iteration.
+console.log("a" in x, "b" in x, "c" in x);
+console.log(x.a, x.b);
+for (const k in x) {
+    console.log(k, x[k]);
+}
+console.log(Object.keys(x));
+console.log(Reflect.ownKeys(x));
+
+// Test set.
+x.c = 3;
+console.log(Object.keys(x));
+
+// Test delete.
+// biome-ignore lint/performance/noDelete: test delete statement
+delete x.b;
+console.log(Object.keys(x));
+
+// Make sure changes on the JavaScript side are reflected in Python.
+mp.runPython(`
+print(x["a"])
+print("b" in x)
+print(x["c"])
+`);
diff --git a/tests/ports/webassembly/py_proxy_dict.mjs.exp b/tests/ports/webassembly/py_proxy_dict.mjs.exp
new file mode 100644
index 00000000000..f0e15034bd7
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_dict.mjs.exp
@@ -0,0 +1,11 @@
+true true false
+1 2
+a 1
+b 2
+[ 'a', 'b' ]
+[ 'a', 'b' ]
+[ 'a', 'c', 'b' ]
+[ 'a', 'c' ]
+1
+False
+3
diff --git a/tests/ports/webassembly/py_proxy_dict_undefined.mjs b/tests/ports/webassembly/py_proxy_dict_undefined.mjs
new file mode 100644
index 00000000000..d47a6a028b6
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_dict_undefined.mjs
@@ -0,0 +1,34 @@
+// Test passing a Python dict into JavaScript, how it behaves with undefined keys.
+// If JavaScript accesses a key that does not exist, `undefined` should be returned.
+// This is different to Python-side behaviour, where `KeyError` is raised.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+// Create a JavaScript function with default arguments.
+// When `value` is `undefined` it will receive its default.
+function withDefault({ value = "OK" } = {}) {
+    console.log(value);
+}
+
+globalThis.withDefault = withDefault;
+
+// Call the function from JavaScript with various arguments.
+withDefault();
+withDefault({});
+withDefault({ value: null });
+withDefault({ value: undefined });
+withDefault({ value: () => {} });
+
+console.log("====");
+
+// Call the function from Python with the same arguments as above.
+// The results should be the same.
+mp.runPython(`
+import js
+
+js.withDefault()
+js.withDefault({})
+js.withDefault({"value": None})
+js.withDefault({"value": js.undefined})
+js.withDefault({"value": (lambda: {})})
+`);
diff --git a/tests/ports/webassembly/py_proxy_dict_undefined.mjs.exp b/tests/ports/webassembly/py_proxy_dict_undefined.mjs.exp
new file mode 100644
index 00000000000..6cf5b9fa128
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_dict_undefined.mjs.exp
@@ -0,0 +1,11 @@
+OK
+OK
+null
+OK
+[Function: value]
+====
+OK
+OK
+null
+OK
+[Function: obj] { _ref: 7 }
diff --git a/tests/ports/webassembly/py_proxy_get.mjs b/tests/ports/webassembly/py_proxy_get.mjs
new file mode 100644
index 00000000000..825de7cabeb
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_get.mjs
@@ -0,0 +1,14 @@
+// Test ` get ` on the JavaScript side, which tests PyProxy.get.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+x = {"a": 1}
+`);
+
+const x = mp.globals.get("x");
+console.log(x.a === 1);
+console.log(x.b === undefined);
+console.log(typeof x[Symbol.iterator] === "function");
+console.log(x[Symbol.toStringTag] === undefined);
+console.log(x.then === undefined);
diff --git a/tests/ports/webassembly/py_proxy_get.mjs.exp b/tests/ports/webassembly/py_proxy_get.mjs.exp
new file mode 100644
index 00000000000..36c7afad66a
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_get.mjs.exp
@@ -0,0 +1,5 @@
+true
+true
+true
+true
+true
diff --git a/tests/ports/webassembly/py_proxy_has.mjs b/tests/ports/webassembly/py_proxy_has.mjs
new file mode 100644
index 00000000000..37df0ae1794
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_has.mjs
@@ -0,0 +1,13 @@
+// Test ` in ` on the JavaScript side, which tests PyProxy.has.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+x = []
+`);
+
+const x = mp.globals.get("x");
+console.log("no_exist" in x);
+console.log("sort" in x);
+console.log(Symbol.toStringTag in x);
+console.log(Symbol.iterator in x);
diff --git a/tests/ports/webassembly/py_proxy_has.mjs.exp b/tests/ports/webassembly/py_proxy_has.mjs.exp
new file mode 100644
index 00000000000..7565230c01f
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_has.mjs.exp
@@ -0,0 +1,4 @@
+false
+true
+false
+true
diff --git a/tests/ports/webassembly/py_proxy_identity.mjs b/tests/ports/webassembly/py_proxy_identity.mjs
new file mode 100644
index 00000000000..d4a720b738a
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_identity.mjs
@@ -0,0 +1,26 @@
+// Test identity of PyProxy when they are the same Python object.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+l = []
+`);
+
+const l1 = mp.globals.get("l");
+const l2 = mp.globals.get("l");
+console.log(l1, l2);
+console.log(l1 === l2);
+
+globalThis.eventTarget = new EventTarget();
+globalThis.event = new Event("event");
+
+mp.runPython(`
+import js
+
+def callback(ev):
+    print("callback", ev)
+js.eventTarget.addEventListener("event", callback)
+js.eventTarget.dispatchEvent(js.event)
+js.eventTarget.removeEventListener("event", callback)
+js.eventTarget.dispatchEvent(js.event)
+`);
diff --git a/tests/ports/webassembly/py_proxy_identity.mjs.exp b/tests/ports/webassembly/py_proxy_identity.mjs.exp
new file mode 100644
index 00000000000..b5b8b210bb6
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_identity.mjs.exp
@@ -0,0 +1,3 @@
+PyProxy { _ref: 3 } PyProxy { _ref: 3 }
+true
+callback 
diff --git a/tests/ports/webassembly/py_proxy_own_keys.mjs b/tests/ports/webassembly/py_proxy_own_keys.mjs
new file mode 100644
index 00000000000..bbf5b4f01c0
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_own_keys.mjs
@@ -0,0 +1,11 @@
+// Test `Reflect.ownKeys()` on the JavaScript side, which tests PyProxy.ownKeys.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+x = []
+y = {"a": 1}
+`);
+
+console.log(Reflect.ownKeys(mp.globals.get("x")));
+console.log(Reflect.ownKeys(mp.globals.get("y")));
diff --git a/tests/ports/webassembly/py_proxy_own_keys.mjs.exp b/tests/ports/webassembly/py_proxy_own_keys.mjs.exp
new file mode 100644
index 00000000000..313d06d4d75
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_own_keys.mjs.exp
@@ -0,0 +1,9 @@
+[
+  'append', 'clear',
+  'copy',   'count',
+  'extend', 'index',
+  'insert', 'pop',
+  'remove', 'reverse',
+  'sort'
+]
+[ 'a' ]
diff --git a/tests/ports/webassembly/py_proxy_set.mjs b/tests/ports/webassembly/py_proxy_set.mjs
new file mode 100644
index 00000000000..30360a847d8
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_set.mjs
@@ -0,0 +1,27 @@
+// Test `. = ` on the JavaScript side, which tests PyProxy.set.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+class A:
+    pass
+x = A()
+y = []
+`);
+
+const x = mp.globals.get("x");
+const y = mp.globals.get("y");
+
+// Should pass.
+x.foo = 1;
+
+mp.runPython(`
+print(x.foo)
+`);
+
+// Should fail, can't set attributes on MicroPython lists.
+try {
+    y.bar = 1;
+} catch (error) {
+    console.log(error.message);
+}
diff --git a/tests/ports/webassembly/py_proxy_set.mjs.exp b/tests/ports/webassembly/py_proxy_set.mjs.exp
new file mode 100644
index 00000000000..e1d99515634
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_set.mjs.exp
@@ -0,0 +1,2 @@
+1
+'set' on proxy: trap returned falsish for property 'bar'
diff --git a/tests/ports/webassembly/py_proxy_to_js.mjs b/tests/ports/webassembly/py_proxy_to_js.mjs
new file mode 100644
index 00000000000..f9fce606f1f
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_to_js.mjs
@@ -0,0 +1,20 @@
+// Test PyProxy.toJs().
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+a = 1
+b = (1, 2, 3)
+c = [None, True, 1.2]
+d = {"one": 1, "tuple": b, "list": c}
+`);
+
+const py_a = mp.globals.get("a");
+const py_b = mp.globals.get("b");
+const py_c = mp.globals.get("c");
+const py_d = mp.globals.get("d");
+
+console.log(py_a instanceof mp.PyProxy, mp.PyProxy.toJs(py_a));
+console.log(py_b instanceof mp.PyProxy, mp.PyProxy.toJs(py_b));
+console.log(py_c instanceof mp.PyProxy, mp.PyProxy.toJs(py_c));
+console.log(py_d instanceof mp.PyProxy, mp.PyProxy.toJs(py_d));
diff --git a/tests/ports/webassembly/py_proxy_to_js.mjs.exp b/tests/ports/webassembly/py_proxy_to_js.mjs.exp
new file mode 100644
index 00000000000..279df7bdfbb
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_to_js.mjs.exp
@@ -0,0 +1,4 @@
+false 1
+true [ 1, 2, 3 ]
+true [ null, true, 1.2 ]
+true { tuple: [ 1, 2, 3 ], one: 1, list: [ null, true, 1.2 ] }
diff --git a/tests/ports/webassembly/register_js_module.js b/tests/ports/webassembly/register_js_module.js
new file mode 100644
index 00000000000..f58d12e50d3
--- /dev/null
+++ b/tests/ports/webassembly/register_js_module.js
@@ -0,0 +1,45 @@
+// Test the registerJsModule() public API method.
+
+import(process.argv[2]).then((mp) => {
+    mp.loadMicroPython().then((py) => {
+        // Simple module.
+        py.registerJsModule("js_module", { y: 2 });
+        py.runPython("import js_module; print(js_module); print(js_module.y)");
+
+        // Module with functions.
+        // In particular test how "this" behaves.
+        py.registerJsModule("js_module2", {
+            yes: true,
+            add1(x) {
+                return x + 1;
+            },
+            getThis() {
+                return this;
+            },
+        });
+
+        console.log("====");
+
+        // Test using simple import.
+        py.runPython(`
+import js_module2
+
+print(js_module2.yes)
+print(js_module2.add1(1))
+print(js_module2.getThis())
+print(js_module2.getThis().yes)
+`);
+
+        console.log("====");
+
+        // Test using "from ... import".
+        py.runPython(`
+from js_module2 import yes, add1, getThis
+
+print(yes)
+print(add1(2))
+print(getThis())
+print(getThis().yes)
+`);
+    });
+});
diff --git a/tests/ports/webassembly/register_js_module.js.exp b/tests/ports/webassembly/register_js_module.js.exp
new file mode 100644
index 00000000000..34bfd345d17
--- /dev/null
+++ b/tests/ports/webassembly/register_js_module.js.exp
@@ -0,0 +1,12 @@
+
+2
+====
+True
+2
+
+True
+====
+True
+3
+
+True
diff --git a/tests/ports/webassembly/run_python_async.mjs b/tests/ports/webassembly/run_python_async.mjs
new file mode 100644
index 00000000000..44f2a90ab17
--- /dev/null
+++ b/tests/ports/webassembly/run_python_async.mjs
@@ -0,0 +1,115 @@
+// Test runPythonAsync() and top-level await in Python.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+/**********************************************************/
+// Using only promise objects, no await's.
+
+console.log("= TEST 1 ==========");
+
+globalThis.p = new Promise((resolve, reject) => {
+    resolve(123);
+});
+
+console.log(1);
+
+mp.runPython(`
+import js
+print(js.p)
+print("py 1")
+print(js.p.then(lambda x: print("resolved", x)))
+print("py 2")
+`);
+
+console.log(2);
+
+// Let the promise resolve.
+await globalThis.p;
+
+console.log(3);
+
+/**********************************************************/
+// Using setTimeout to resolve the promise.
+
+console.log("= TEST 2 ==========");
+
+globalThis.p = new Promise((resolve, reject) => {
+    setTimeout(() => {
+        resolve(123);
+        console.log("setTimeout resolved");
+    }, 100);
+});
+
+console.log(1);
+
+mp.runPython(`
+import js
+print(js.p)
+print("py 1")
+print(js.p.then(lambda x: print("resolved", x)))
+print("py 2")
+`);
+
+console.log(2);
+
+// Let the promise resolve.
+await globalThis.p;
+
+console.log(3);
+
+/**********************************************************/
+// Using setTimeout and await within Python.
+
+console.log("= TEST 3 ==========");
+
+globalThis.p = new Promise((resolve, reject) => {
+    setTimeout(() => {
+        resolve(123);
+        console.log("setTimeout resolved");
+    }, 100);
+});
+
+console.log(1);
+
+const ret3 = await mp.runPythonAsync(`
+import js
+print("py 1")
+print("resolved value:", await js.p)
+print("py 2")
+`);
+
+console.log(2, ret3);
+
+/**********************************************************/
+// Multiple setTimeout's and await's within Python.
+
+console.log("= TEST 4 ==========");
+
+globalThis.p1 = new Promise((resolve, reject) => {
+    setTimeout(() => {
+        resolve(123);
+        console.log("setTimeout A resolved");
+    }, 100);
+});
+
+globalThis.p2 = new Promise((resolve, reject) => {
+    setTimeout(() => {
+        resolve(456);
+        console.log("setTimeout B resolved");
+    }, 200);
+});
+
+console.log(1);
+
+const ret4 = await mp.runPythonAsync(`
+import js
+print("py 1")
+print("resolved value:", await js.p1)
+print("py 2")
+print("resolved value:", await js.p1)
+print("py 3")
+print("resolved value:", await js.p2)
+print("py 4")
+`);
+
+console.log(2, ret4);
diff --git a/tests/ports/webassembly/run_python_async.mjs.exp b/tests/ports/webassembly/run_python_async.mjs.exp
new file mode 100644
index 00000000000..4dff64a6053
--- /dev/null
+++ b/tests/ports/webassembly/run_python_async.mjs.exp
@@ -0,0 +1,38 @@
+= TEST 1 ==========
+1
+
+py 1
+
+py 2
+2
+resolved 123
+3
+= TEST 2 ==========
+1
+
+py 1
+
+py 2
+2
+setTimeout resolved
+resolved 123
+3
+= TEST 3 ==========
+1
+py 1
+setTimeout resolved
+resolved value: 123
+py 2
+2 null
+= TEST 4 ==========
+1
+py 1
+setTimeout A resolved
+resolved value: 123
+py 2
+resolved value: 123
+py 3
+setTimeout B resolved
+resolved value: 456
+py 4
+2 null
diff --git a/tests/ports/webassembly/run_python_async2.mjs b/tests/ports/webassembly/run_python_async2.mjs
new file mode 100644
index 00000000000..87067e6e8c4
--- /dev/null
+++ b/tests/ports/webassembly/run_python_async2.mjs
@@ -0,0 +1,42 @@
+// Test runPythonAsync() and top-level await in Python, with multi-level awaits.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+// simulate a 2-step resolution of the string "OK".
+await mp.runPythonAsync(`
+import js
+
+def _timeout(resolve, _):
+    js.setTimeout(resolve, 100)
+
+def _fetch():
+    return js.Promise.new(_timeout)
+
+async def _text(promise):
+    if not promise._response:
+        print("_text await start")
+        await promise
+        print("_text awaited end")
+    ret = await promise._response.text()
+    return ret
+
+class _Response:
+    async def text(self):
+        print('_Response.text start')
+        await js.Promise.new(_timeout)
+        print('_Response.text end')
+        return "OK"
+
+def _response(promise):
+    promise._response = _Response()
+    return promise._response
+
+def fetch(url):
+    promise = _fetch().then(lambda *_: _response(promise))
+    promise._response = None
+    promise.text = lambda: _text(promise)
+    return promise
+
+print(await fetch("config.json").text())
+print(await (await fetch("config.json")).text())
+`);
diff --git a/tests/ports/webassembly/run_python_async2.mjs.exp b/tests/ports/webassembly/run_python_async2.mjs.exp
new file mode 100644
index 00000000000..60d68c5d3b5
--- /dev/null
+++ b/tests/ports/webassembly/run_python_async2.mjs.exp
@@ -0,0 +1,8 @@
+_text await start
+_text awaited end
+_Response.text start
+_Response.text end
+OK
+_Response.text start
+_Response.text end
+OK
diff --git a/tests/ports/webassembly/run_python_async_error.mjs b/tests/ports/webassembly/run_python_async_error.mjs
new file mode 100644
index 00000000000..ce4db0ff078
--- /dev/null
+++ b/tests/ports/webassembly/run_python_async_error.mjs
@@ -0,0 +1,11 @@
+// Test raising an exception in async Python code running in runPythonAsync,
+// that the JavaScript-level promise is rejected with a PythonError.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+try {
+    await mp.runPythonAsync("await fail");
+} catch (error) {
+    console.log(error.name, error.type);
+    console.log(error.message);
+}
diff --git a/tests/ports/webassembly/run_python_async_error.mjs.exp b/tests/ports/webassembly/run_python_async_error.mjs.exp
new file mode 100644
index 00000000000..c77de4bb345
--- /dev/null
+++ b/tests/ports/webassembly/run_python_async_error.mjs.exp
@@ -0,0 +1,5 @@
+PythonError NameError
+Traceback (most recent call last):
+  File "", line 1, in 
+NameError: name 'fail' isn't defined
+
diff --git a/tests/ports/webassembly/run_python_async_no_await.mjs b/tests/ports/webassembly/run_python_async_no_await.mjs
new file mode 100644
index 00000000000..19462d6e7f6
--- /dev/null
+++ b/tests/ports/webassembly/run_python_async_no_await.mjs
@@ -0,0 +1,24 @@
+// Test runPythonAsync() without await'ing it.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+globalThis.p = new Promise((resolve, reject) => {
+    setTimeout(() => {
+        resolve(123);
+        console.log("setTimeout resolved");
+    }, 100);
+});
+
+console.log(1);
+
+const ret = mp.runPythonAsync(`
+import js
+print("py 1")
+print("resolved value:", await js.p)
+print("py 2")
+`);
+
+// `ret` should be a Promise.
+console.log(2, ret);
+
+// Here, the Python async code should continue to run until completed.
diff --git a/tests/ports/webassembly/run_python_async_no_await.mjs.exp b/tests/ports/webassembly/run_python_async_no_await.mjs.exp
new file mode 100644
index 00000000000..1dbf7f95f4d
--- /dev/null
+++ b/tests/ports/webassembly/run_python_async_no_await.mjs.exp
@@ -0,0 +1,6 @@
+1
+2 Promise {  }
+py 1
+setTimeout resolved
+resolved value: 123
+py 2
diff --git a/tests/ports/webassembly/this_behaviour.mjs b/tests/ports/webassembly/this_behaviour.mjs
new file mode 100644
index 00000000000..6411b6ce634
--- /dev/null
+++ b/tests/ports/webassembly/this_behaviour.mjs
@@ -0,0 +1,24 @@
+// Test "this" behaviour.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+// "this" should be undefined.
+globalThis.func0 = function () {
+    console.log("func0", this);
+};
+mp.runPython("import js; js.func0()");
+
+globalThis.func1 = function (a) {
+    console.log("func1", a, this);
+};
+mp.runPython("import js; js.func1(123)");
+
+globalThis.func2 = function (a, b) {
+    console.log("func2", a, b, this);
+};
+mp.runPython("import js; js.func2(123, 456)");
+
+globalThis.func3 = function (a, b, c) {
+    console.log("func3", a, b, c, this);
+};
+mp.runPython("import js; js.func3(123, 456, 789)");
diff --git a/tests/ports/webassembly/this_behaviour.mjs.exp b/tests/ports/webassembly/this_behaviour.mjs.exp
new file mode 100644
index 00000000000..4762026ab20
--- /dev/null
+++ b/tests/ports/webassembly/this_behaviour.mjs.exp
@@ -0,0 +1,4 @@
+func0 undefined
+func1 123 undefined
+func2 123 456 undefined
+func3 123 456 789 undefined
diff --git a/tests/ports/webassembly/various.js b/tests/ports/webassembly/various.js
new file mode 100644
index 00000000000..e2fa9362c4f
--- /dev/null
+++ b/tests/ports/webassembly/various.js
@@ -0,0 +1,38 @@
+import(process.argv[2]).then((mp) => {
+    mp.loadMicroPython().then((py) => {
+        globalThis.jsadd = (x, y) => {
+            return x + y;
+        };
+        py.runPython("import js; print(js); print(js.jsadd(4, 9))");
+
+        py.runPython(
+            "def set_timeout_callback():\n print('set_timeout_callback')",
+        );
+        py.runPython("import js; js.setTimeout(set_timeout_callback, 100)");
+
+        py.runPython("obj = js.Object(a=1)");
+        console.log("main", py.pyimport("__main__").obj);
+
+        console.log("=======");
+        py.runPython(`
+            from js import Array, Promise, Reflect
+
+            def callback(resolve, reject):
+                resolve('OK1')
+
+            p = Reflect.construct(Promise, Array(callback))
+            p.then(print)
+        `);
+
+        console.log("=======");
+        py.runPython(`
+            from js import Promise
+
+            def callback(resolve, reject):
+                resolve('OK2')
+
+            p = Promise.new(callback)
+            p.then(print)
+        `);
+    });
+});
diff --git a/tests/ports/webassembly/various.js.exp b/tests/ports/webassembly/various.js.exp
new file mode 100644
index 00000000000..502ab2cccf0
--- /dev/null
+++ b/tests/ports/webassembly/various.js.exp
@@ -0,0 +1,8 @@
+
+13
+main { a: 1 }
+=======
+=======
+OK1
+OK2
+set_timeout_callback
diff --git a/tests/ports/webassembly/weakref_finalize_collect.mjs b/tests/ports/webassembly/weakref_finalize_collect.mjs
new file mode 100644
index 00000000000..1e0bc951350
--- /dev/null
+++ b/tests/ports/webassembly/weakref_finalize_collect.mjs
@@ -0,0 +1,86 @@
+// Test weakref.finalize() functionality requiring gc.collect().
+// Should be kept in sync with tests/basics/weakref_finalize_collect.py.
+//
+// This needs custom testing on the webassembly port since the GC can only
+// run when Python code returns to JavaScript.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+// Set up.
+mp.runPython(`
+import gc, weakref
+
+class A:
+    def __str__(self):
+        return ""
+
+def callback(*args, **kwargs):
+    print("callback({}, {})".format(args, kwargs))
+    return 42
+`);
+
+console.log("test basic use of finalize() with a simple callback");
+mp.runPython(`
+    a = A()
+    f = weakref.finalize(a, callback)
+    a = None
+    gc.collect()
+`);
+console.log("(outside Python)");
+mp.runPython(`
+    print("alive", f.alive)
+    print("peek", f.peek())
+    print("detach", f.detach())
+    print("call", f())
+`);
+
+console.log("test that a callback is passed the correct values");
+mp.runPython(`
+    a = A()
+    f = weakref.finalize(a, callback, 1, 2, kwarg=3)
+    a = None
+    gc.collect()
+`);
+console.log("(outside Python)");
+mp.runPython(`
+    print("alive", f.alive)
+    print("peek", f.peek())
+    print("detach", f.detach())
+    print("call", f())
+`);
+
+console.log("test that calling the finalizer cancels the finalizer");
+mp.runPython(`
+    a = A()
+    f = weakref.finalize(a, callback)
+    print(f())
+    print(a)
+    a = None
+    gc.collect()
+`);
+console.log("(outside Python)");
+
+console.log("test that calling detach cancels the finalizer");
+mp.runPython(`
+    a = A()
+    f = weakref.finalize(a, callback)
+    print(len(f.detach()))
+    print(a)
+    a = None
+    gc.collect()
+`);
+console.log("(outside Python)");
+
+console.log("test that finalize does not get collected before its ref does");
+mp.runPython(`
+    a = A()
+    weakref.finalize(a, callback)
+    gc.collect()
+`);
+console.log("(outside Python)");
+mp.runPython(`
+    print("free a")
+    a = None
+    gc.collect()
+`);
+console.log("(outside Python)");
diff --git a/tests/ports/webassembly/weakref_finalize_collect.mjs.exp b/tests/ports/webassembly/weakref_finalize_collect.mjs.exp
new file mode 100644
index 00000000000..e8087a4ae9b
--- /dev/null
+++ b/tests/ports/webassembly/weakref_finalize_collect.mjs.exp
@@ -0,0 +1,28 @@
+test basic use of finalize() with a simple callback
+callback((), {})
+(outside Python)
+alive False
+peek None
+detach None
+call None
+test that a callback is passed the correct values
+callback((1, 2), {'kwarg': 3})
+(outside Python)
+alive False
+peek None
+detach None
+call None
+test that calling the finalizer cancels the finalizer
+callback((), {})
+42
+
+(outside Python)
+test that calling detach cancels the finalizer
+4
+
+(outside Python)
+test that finalize does not get collected before its ref does
+(outside Python)
+free a
+callback((), {})
+(outside Python)
diff --git a/tests/ports/webassembly/weakref_ref_collect.mjs b/tests/ports/webassembly/weakref_ref_collect.mjs
new file mode 100644
index 00000000000..546a851f0ac
--- /dev/null
+++ b/tests/ports/webassembly/weakref_ref_collect.mjs
@@ -0,0 +1,69 @@
+// Test weakref.ref() functionality requiring gc.collect().
+// Should be kept in sync with tests/basics/weakref_ref_collect.py.
+//
+// This needs custom testing on the webassembly port since the GC can only
+// run when Python code returns to JavaScript.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+// Set up.
+mp.runPython(`
+import gc, weakref
+
+class A:
+    def __str__(self):
+        return ""
+
+def callback(r):
+    print("callback", r())
+`);
+
+console.log("test basic use of ref() with only one argument");
+mp.runPython(`
+    a = A()
+    r = weakref.ref(a)
+    print(r())
+    gc.collect()
+`);
+console.log("(outside Python)");
+mp.runPython(`
+    print(r())
+    a = None
+    gc.collect()
+`);
+console.log("(outside Python)");
+mp.runPython(`
+    print(r())
+`);
+
+console.log("test use of ref() with a callback");
+mp.runPython(`
+    a = A()
+    r = weakref.ref(a, callback)
+    print(r())
+    gc.collect()
+`);
+console.log("(outside Python)");
+mp.runPython(`
+    print(r())
+    a = None
+    gc.collect()
+`);
+console.log("(outside Python)");
+mp.runPython(`
+    print(r())
+`);
+
+console.log("test when weakref gets collected before the object it refs");
+mp.runPython(`
+    a = A()
+    r = weakref.ref(a, callback)
+    print(r())
+    r = None
+    gc.collect()
+`);
+console.log("(outside Python)");
+mp.runPython(`
+    a = None
+    gc.collect()
+`);
diff --git a/tests/ports/webassembly/weakref_ref_collect.mjs.exp b/tests/ports/webassembly/weakref_ref_collect.mjs.exp
new file mode 100644
index 00000000000..f903d417028
--- /dev/null
+++ b/tests/ports/webassembly/weakref_ref_collect.mjs.exp
@@ -0,0 +1,16 @@
+test basic use of ref() with only one argument
+
+(outside Python)
+
+(outside Python)
+None
+test use of ref() with a callback
+
+(outside Python)
+
+callback None
+(outside Python)
+None
+test when weakref gets collected before the object it refs
+
+(outside Python)
diff --git a/tests/pyb/adcall.py b/tests/pyb/adcall.py
deleted file mode 100644
index cfe179a97b2..00000000000
--- a/tests/pyb/adcall.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from pyb import Pin, ADCAll
-
-pins = [Pin.cpu.A0, Pin.cpu.A1, Pin.cpu.A2, Pin.cpu.A3]
-
-# set pins to IN mode, init ADCAll, then check pins are ANALOG
-for p in pins:
-    p.init(p.IN)
-adc = ADCAll(12)
-for p in pins:
-    print(p)
-
-# set pins to IN mode, init ADCAll with mask, then check some pins are ANALOG
-for p in pins:
-    p.init(p.IN)
-adc = ADCAll(12, 0x70003)
-for p in pins:
-    print(p)
-
-# init all pins to ANALOG
-adc = ADCAll(12)
-print(adc)
-
-# read all channels
-for c in range(19):
-    print(type(adc.read_channel(c)))
-
-# call special reading functions
-print(0 < adc.read_core_temp() < 100)
-print(0 < adc.read_core_vbat() < 4)
-print(0 < adc.read_core_vref() < 2)
-print(0 < adc.read_vref() < 4)
diff --git a/tests/pyb/adcall.py.exp b/tests/pyb/adcall.py.exp
deleted file mode 100644
index 5a85ba770e8..00000000000
--- a/tests/pyb/adcall.py.exp
+++ /dev/null
@@ -1,32 +0,0 @@
-Pin(Pin.cpu.A0, mode=Pin.ANALOG)
-Pin(Pin.cpu.A1, mode=Pin.ANALOG)
-Pin(Pin.cpu.A2, mode=Pin.ANALOG)
-Pin(Pin.cpu.A3, mode=Pin.ANALOG)
-Pin(Pin.cpu.A0, mode=Pin.ANALOG)
-Pin(Pin.cpu.A1, mode=Pin.ANALOG)
-Pin(Pin.cpu.A2, mode=Pin.IN)
-Pin(Pin.cpu.A3, mode=Pin.IN)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-True
-True
-True
-True
diff --git a/tests/pyb/board_pybv1x.py b/tests/pyb/board_pybv1x.py
deleted file mode 100644
index ea66270b297..00000000000
--- a/tests/pyb/board_pybv1x.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Test board-specific items on PYBv1.x
-
-import os, pyb
-
-if not "PYBv1." in os.uname().machine:
-    print("SKIP")
-    raise SystemExit
-
-# test creating UART by id/name
-for bus in (1, 2, 3, 4, 5, 6, 7, "XA", "XB", "YA", "YB", "Z"):
-    try:
-        pyb.UART(bus, 9600)
-        print("UART", bus)
-    except ValueError:
-        print("ValueError", bus)
-
-# test creating SPI by id/name
-for bus in (1, 2, 3, "X", "Y", "Z"):
-    try:
-        pyb.SPI(bus)
-        print("SPI", bus)
-    except ValueError:
-        print("ValueError", bus)
-
-# test creating I2C by id/name
-for bus in (2, 3, "X", "Y", "Z"):
-    try:
-        pyb.I2C(bus)
-        print("I2C", bus)
-    except ValueError:
-        print("ValueError", bus)
-
-# test creating CAN by id/name
-for bus in (1, 2, 3, "YA", "YB", "YC"):
-    try:
-        pyb.CAN(bus, pyb.CAN.LOOPBACK)
-        print("CAN", bus)
-    except ValueError:
-        print("ValueError", bus)
diff --git a/tests/pyb/board_pybv1x.py.exp b/tests/pyb/board_pybv1x.py.exp
deleted file mode 100644
index 6d4a6f63c74..00000000000
--- a/tests/pyb/board_pybv1x.py.exp
+++ /dev/null
@@ -1,29 +0,0 @@
-UART 1
-UART 2
-UART 3
-UART 4
-ValueError 5
-UART 6
-ValueError 7
-UART XA
-UART XB
-UART YA
-UART YB
-ValueError Z
-SPI 1
-SPI 2
-ValueError 3
-SPI X
-SPI Y
-ValueError Z
-I2C 2
-ValueError 3
-I2C X
-I2C Y
-ValueError Z
-CAN 1
-CAN 2
-ValueError 3
-CAN YA
-CAN YB
-ValueError YC
diff --git a/tests/pyb/can.py b/tests/pyb/can.py
deleted file mode 100644
index 020efae0531..00000000000
--- a/tests/pyb/can.py
+++ /dev/null
@@ -1,314 +0,0 @@
-try:
-    from pyb import CAN
-except ImportError:
-    print("SKIP")
-    raise SystemExit
-
-from array import array
-import micropython
-import pyb
-
-# test we can correctly create by id (2 handled in can2.py test)
-for bus in (-1, 0, 1, 3):
-    try:
-        CAN(bus, CAN.LOOPBACK)
-        print("CAN", bus)
-    except ValueError:
-        print("ValueError", bus)
-CAN(1).deinit()
-
-can = CAN(1)
-print(can)
-
-# Test state when de-init'd
-print(can.state() == can.STOPPED)
-
-can.init(CAN.LOOPBACK, num_filter_banks=14)
-print(can)
-print(can.any(0))
-
-# Test state when freshly created
-print(can.state() == can.ERROR_ACTIVE)
-
-# Test that restart can be called
-can.restart()
-
-# Test info returns a sensible value
-print(can.info())
-
-# Catch all filter
-can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0))
-
-can.send("abcd", 123, timeout=5000)
-print(can.any(0), can.info())
-print(can.recv(0))
-
-can.send("abcd", -1, timeout=5000)
-print(can.recv(0))
-
-can.send("abcd", 0x7FF + 1, timeout=5000)
-print(can.recv(0))
-
-# Test too long message
-try:
-    can.send("abcdefghi", 0x7FF, timeout=5000)
-except ValueError:
-    print("passed")
-else:
-    print("failed")
-
-# Test that recv can work without allocating memory on the heap
-
-buf = bytearray(10)
-l = [0, 0, 0, 0, memoryview(buf)]
-l2 = None
-
-micropython.heap_lock()
-
-can.send("", 42)
-l2 = can.recv(0, l)
-assert l is l2
-print(l, len(l[4]), buf)
-
-can.send("1234", 42)
-l2 = can.recv(0, l)
-assert l is l2
-print(l, len(l[4]), buf)
-
-can.send("01234567", 42)
-l2 = can.recv(0, l)
-assert l is l2
-print(l, len(l[4]), buf)
-
-can.send("abc", 42)
-l2 = can.recv(0, l)
-assert l is l2
-print(l, len(l[4]), buf)
-
-micropython.heap_unlock()
-
-# Test that recv can work with different arrays behind the memoryview
-can.send("abc", 1)
-print(bytes(can.recv(0, [0, 0, 0, 0, memoryview(array("B", range(8)))])[4]))
-can.send("def", 1)
-print(bytes(can.recv(0, [0, 0, 0, 0, memoryview(array("b", range(8)))])[4]))
-
-# Test for non-list passed as second arg to recv
-can.send("abc", 1)
-try:
-    can.recv(0, 1)
-except TypeError:
-    print("TypeError")
-
-# Test for too-short-list passed as second arg to recv
-can.send("abc", 1)
-try:
-    can.recv(0, [0, 0, 0])
-except ValueError:
-    print("ValueError")
-
-# Test for non-memoryview passed as 4th element to recv
-can.send("abc", 1)
-try:
-    can.recv(0, [0, 0, 0, 0, 0])
-except TypeError:
-    print("TypeError")
-
-# Test for read-only-memoryview passed as 4th element to recv
-can.send("abc", 1)
-try:
-    can.recv(0, [0, 0, 0, memoryview(bytes(8))])
-except ValueError:
-    print("ValueError")
-
-# Test for bad-typecode-memoryview passed as 4th element to recv
-can.send("abc", 1)
-try:
-    can.recv(0, [0, 0, 0, memoryview(array("i", range(8)))])
-except ValueError:
-    print("ValueError")
-
-del can
-
-# Testing extended IDs
-print("==== TEST extframe=True ====")
-
-can = CAN(1, CAN.LOOPBACK)
-# Catch all filter, but only for extframe's
-can.setfilter(0, CAN.MASK32, 0, (0, 0), extframe=True)
-
-print(can)
-
-try:
-    can.send("abcde", 0x7FF + 1, timeout=5000, extframe=True)
-except ValueError:
-    print("failed")
-else:
-    r = can.recv(0)
-    if r[0] == 0x7FF + 1 and r[4] == b"abcde":
-        print("passed")
-    else:
-        print("failed, wrong data received")
-
-# Test filters
-for n in [0, 8, 16, 24]:
-    filter_id = 0b00001000 << n
-    filter_mask = 0b00011100 << n
-    id_ok = 0b00001010 << n
-    id_fail = 0b00011010 << n
-
-    can.clearfilter(0, extframe=True)
-    can.setfilter(0, pyb.CAN.MASK32, 0, (filter_id, filter_mask), extframe=True)
-
-    can.send("ok", id_ok, timeout=3, extframe=True)
-    if can.any(0):
-        msg = can.recv(0)
-        print((hex(filter_id), hex(filter_mask), hex(msg[0]), msg[1], msg[4]))
-
-    can.send("fail", id_fail, timeout=3, extframe=True)
-    if can.any(0):
-        msg = can.recv(0)
-        print((hex(filter_id), hex(filter_mask), hex(msg[0]), msg[1], msg[4]))
-
-del can
-
-# Test RxCallbacks
-print("==== TEST rx callbacks ====")
-
-can = CAN(1, CAN.LOOPBACK)
-can.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4))
-can.setfilter(1, CAN.LIST16, 1, (5, 6, 7, 8))
-
-
-def cb0(bus, reason):
-    print("cb0")
-    if reason == 0:
-        print("pending")
-    if reason == 1:
-        print("full")
-    if reason == 2:
-        print("overflow")
-
-
-def cb1(bus, reason):
-    print("cb1")
-    if reason == 0:
-        print("pending")
-    if reason == 1:
-        print("full")
-    if reason == 2:
-        print("overflow")
-
-
-def cb0a(bus, reason):
-    print("cb0a")
-    if reason == 0:
-        print("pending")
-    if reason == 1:
-        print("full")
-    if reason == 2:
-        print("overflow")
-
-
-def cb1a(bus, reason):
-    print("cb1a")
-    if reason == 0:
-        print("pending")
-    if reason == 1:
-        print("full")
-    if reason == 2:
-        print("overflow")
-
-
-can.rxcallback(0, cb0)
-can.rxcallback(1, cb1)
-
-can.send("11111111", 1, timeout=5000)
-can.send("22222222", 2, timeout=5000)
-can.send("33333333", 3, timeout=5000)
-can.rxcallback(0, cb0a)
-can.send("44444444", 4, timeout=5000)
-
-can.send("55555555", 5, timeout=5000)
-can.send("66666666", 6, timeout=5000)
-can.send("77777777", 7, timeout=5000)
-can.rxcallback(1, cb1a)
-can.send("88888888", 8, timeout=5000)
-
-print(can.recv(0))
-print(can.recv(0))
-print(can.recv(0))
-print(can.recv(1))
-print(can.recv(1))
-print(can.recv(1))
-
-can.send("11111111", 1, timeout=5000)
-can.send("55555555", 5, timeout=5000)
-
-print(can.recv(0))
-print(can.recv(1))
-
-del can
-
-# Testing asynchronous send
-print("==== TEST async send ====")
-
-can = CAN(1, CAN.LOOPBACK)
-can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0))
-
-while can.any(0):
-    can.recv(0)
-
-can.send("abcde", 1, timeout=0)
-print(can.any(0))
-while not can.any(0):
-    pass
-
-print(can.recv(0))
-
-try:
-    can.send("abcde", 2, timeout=0)
-    can.send("abcde", 3, timeout=0)
-    can.send("abcde", 4, timeout=0)
-    can.send("abcde", 5, timeout=0)
-except OSError as e:
-    if str(e) == "16":
-        print("passed")
-    else:
-        print("failed")
-
-pyb.delay(500)
-while can.any(0):
-    print(can.recv(0))
-
-# Testing rtr messages
-print("==== TEST rtr messages ====")
-
-bus1 = CAN(1, CAN.LOOPBACK)
-while bus1.any(0):
-    bus1.recv(0)
-bus1.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4))
-bus1.setfilter(1, CAN.LIST16, 0, (5, 6, 7, 8), rtr=(True, True, True, True))
-bus1.setfilter(2, CAN.MASK16, 0, (64, 64, 32, 32), rtr=(False, True))
-
-bus1.send("", 1, rtr=True)
-print(bus1.any(0))
-bus1.send("", 5, rtr=True)
-print(bus1.recv(0))
-bus1.send("", 6, rtr=True)
-print(bus1.recv(0))
-bus1.send("", 7, rtr=True)
-print(bus1.recv(0))
-bus1.send("", 16, rtr=True)
-print(bus1.any(0))
-bus1.send("", 32, rtr=True)
-print(bus1.recv(0))
-
-# test HAL error, timeout
-print("==== TEST errors ====")
-
-can = pyb.CAN(1, pyb.CAN.NORMAL)
-try:
-    can.send("1", 1, timeout=50)
-except OSError as e:
-    print(repr(e))
diff --git a/tests/pyb/can.py.exp b/tests/pyb/can.py.exp
deleted file mode 100644
index bd8f6d60b60..00000000000
--- a/tests/pyb/can.py.exp
+++ /dev/null
@@ -1,74 +0,0 @@
-ValueError -1
-ValueError 0
-CAN 1
-ValueError 3
-CAN(1)
-True
-CAN(1, CAN.LOOPBACK, auto_restart=False)
-False
-True
-[0, 0, 0, 0, 0, 0, 0, 0]
-True [0, 0, 0, 0, 0, 0, 1, 0]
-(123, False, False, 0, b'abcd')
-(2047, False, False, 0, b'abcd')
-(0, False, False, 0, b'abcd')
-passed
-[42, False, False, 0, ] 0 bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
-[42, False, False, 0, ] 4 bytearray(b'1234\x00\x00\x00\x00\x00\x00')
-[42, False, False, 0, ] 8 bytearray(b'01234567\x00\x00')
-[42, False, False, 0, ] 3 bytearray(b'abc34567\x00\x00')
-b'abc'
-b'def'
-TypeError
-ValueError
-TypeError
-ValueError
-ValueError
-==== TEST extframe=True ====
-CAN(1, CAN.LOOPBACK, auto_restart=False)
-passed
-('0x8', '0x1c', '0xa', True, b'ok')
-('0x800', '0x1c00', '0xa00', True, b'ok')
-('0x80000', '0x1c0000', '0xa0000', True, b'ok')
-('0x8000000', '0x1c000000', '0xa000000', True, b'ok')
-==== TEST rx callbacks ====
-cb0
-pending
-cb0
-full
-cb0a
-overflow
-cb1
-pending
-cb1
-full
-cb1a
-overflow
-(1, False, False, 0, b'11111111')
-(2, False, False, 1, b'22222222')
-(4, False, False, 3, b'44444444')
-(5, False, False, 0, b'55555555')
-(6, False, False, 1, b'66666666')
-(8, False, False, 3, b'88888888')
-cb0a
-pending
-cb1a
-pending
-(1, False, False, 0, b'11111111')
-(5, False, False, 0, b'55555555')
-==== TEST async send ====
-False
-(1, False, False, 0, b'abcde')
-passed
-(2, False, False, 0, b'abcde')
-(3, False, False, 0, b'abcde')
-(4, False, False, 0, b'abcde')
-==== TEST rtr messages ====
-False
-(5, False, True, 4, b'')
-(6, False, True, 5, b'')
-(7, False, True, 6, b'')
-False
-(32, False, True, 9, b'')
-==== TEST errors ====
-OSError(110,)
diff --git a/tests/pyb/can2.py b/tests/pyb/can2.py
deleted file mode 100644
index 2ce438f1af9..00000000000
--- a/tests/pyb/can2.py
+++ /dev/null
@@ -1,25 +0,0 @@
-try:
-    from pyb import CAN
-
-    CAN(2)
-except (ImportError, ValueError):
-    print("SKIP")
-    raise SystemExit
-
-# Testing rtr messages
-bus2 = CAN(2, CAN.LOOPBACK)
-while bus2.any(0):
-    bus2.recv(0)
-bus2.setfilter(0, CAN.LIST32, 0, (1, 2), rtr=(True, True), extframe=True)
-bus2.setfilter(1, CAN.LIST32, 0, (3, 4), rtr=(True, False), extframe=True)
-bus2.setfilter(2, CAN.MASK32, 0, (16, 16), rtr=(False,), extframe=True)
-bus2.setfilter(2, CAN.MASK32, 0, (32, 32), rtr=(True,), extframe=True)
-
-bus2.send("", 1, rtr=True, extframe=True)
-print(bus2.recv(0))
-bus2.send("", 2, rtr=True, extframe=True)
-print(bus2.recv(0))
-bus2.send("", 3, rtr=True, extframe=True)
-print(bus2.recv(0))
-bus2.send("", 4, rtr=True, extframe=True)
-print(bus2.any(0))
diff --git a/tests/pyb/can2.py.exp b/tests/pyb/can2.py.exp
deleted file mode 100644
index 3339e5cbeca..00000000000
--- a/tests/pyb/can2.py.exp
+++ /dev/null
@@ -1,4 +0,0 @@
-(1, True, True, 0, b'')
-(2, True, True, 1, b'')
-(3, True, True, 2, b'')
-False
diff --git a/tests/pyb/extint.py b/tests/pyb/extint.py
deleted file mode 100644
index 5510600020e..00000000000
--- a/tests/pyb/extint.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import pyb
-
-# test basic functionality
-ext = pyb.ExtInt("X5", pyb.ExtInt.IRQ_RISING, pyb.Pin.PULL_DOWN, lambda l: print("line:", l))
-ext.disable()
-ext.enable()
-print(ext.line())
-ext.swint()
-
-# test swint while disabled, then again after re-enabled
-ext.disable()
-ext.swint()
-ext.enable()
-ext.swint()
-
-# disable now that the test is finished
-ext.disable()
diff --git a/tests/pyb/i2c.py b/tests/pyb/i2c.py
deleted file mode 100644
index c968843273a..00000000000
--- a/tests/pyb/i2c.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import pyb
-from pyb import I2C
-
-# test we can correctly create by id
-for bus in (-1, 0, 1):
-    try:
-        I2C(bus)
-        print("I2C", bus)
-    except ValueError:
-        print("ValueError", bus)
-
-i2c = I2C(1)
-
-i2c.init(I2C.CONTROLLER, baudrate=400000)
-print(i2c.scan())
-i2c.deinit()
diff --git a/tests/pyb/pin.py b/tests/pyb/pin.py
deleted file mode 100644
index 3d2bef97e34..00000000000
--- a/tests/pyb/pin.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from pyb import Pin
-
-p = Pin("X8", Pin.IN)
-print(p)
-print(p.name())
-print(p.pin())
-print(p.port())
-
-p = Pin("X8", Pin.IN, Pin.PULL_UP)
-p = Pin("X8", Pin.IN, pull=Pin.PULL_UP)
-p = Pin("X8", mode=Pin.IN, pull=Pin.PULL_UP)
-print(p)
-print(p.value())
-
-p.init(p.IN, p.PULL_DOWN)
-p.init(p.IN, pull=p.PULL_DOWN)
-p.init(mode=p.IN, pull=p.PULL_DOWN)
-print(p)
-print(p.value())
-
-p.init(p.OUT_PP)
-p.low()
-print(p.value())
-p.high()
-print(p.value())
-p.value(0)
-print(p.value())
-p.value(1)
-print(p.value())
-p.value(False)
-print(p.value())
-p.value(True)
-print(p.value())
diff --git a/tests/pyb/pin.py.exp b/tests/pyb/pin.py.exp
deleted file mode 100644
index a2a7e42d96d..00000000000
--- a/tests/pyb/pin.py.exp
+++ /dev/null
@@ -1,14 +0,0 @@
-Pin(Pin.cpu.A7, mode=Pin.IN)
-A7
-7
-0
-Pin(Pin.cpu.A7, mode=Pin.IN, pull=Pin.PULL_UP)
-1
-Pin(Pin.cpu.A7, mode=Pin.IN, pull=Pin.PULL_DOWN)
-0
-0
-1
-0
-1
-0
-1
diff --git a/tests/pyb/servo.py b/tests/pyb/servo.py
deleted file mode 100644
index d15cafe483e..00000000000
--- a/tests/pyb/servo.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from pyb import Servo
-
-servo = Servo(1)
-print(servo)
-
-servo.angle(0)
-servo.angle(10, 100)
-
-servo.speed(-10)
-servo.speed(10, 100)
-
-servo.pulse_width(1500)
-print(servo.pulse_width())
-
-servo.calibration(630, 2410, 1490, 2460, 2190)
-print(servo.calibration())
diff --git a/tests/pyb/spi.py b/tests/pyb/spi.py
deleted file mode 100644
index 7df6aeb4577..00000000000
--- a/tests/pyb/spi.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from pyb import SPI
-
-# test we can correctly create by id
-for bus in (-1, 0, 1, 2):
-    try:
-        SPI(bus)
-        print("SPI", bus)
-    except ValueError:
-        print("ValueError", bus)
-
-spi = SPI(1)
-print(spi)
-
-spi = SPI(1, SPI.CONTROLLER)
-spi = SPI(1, SPI.CONTROLLER, baudrate=500000)
-spi = SPI(
-    1, SPI.CONTROLLER, 500000, polarity=1, phase=0, bits=8, firstbit=SPI.MSB, ti=False, crc=None
-)
-print(str(spi)[:32], str(spi)[53:])  # don't print baudrate/prescaler
-
-spi.init(SPI.PERIPHERAL, phase=1)
-print(spi)
-try:
-    # need to flush input before we get an error (error is what we want to test)
-    for i in range(10):
-        spi.recv(1, timeout=100)
-except OSError:
-    print("OSError")
-
-spi.init(SPI.CONTROLLER)
-spi.send(1, timeout=100)
-print(spi.recv(1, timeout=100))
-print(spi.send_recv(1, timeout=100))
-
-spi.deinit()
diff --git a/tests/pyb/spi.py.exp b/tests/pyb/spi.py.exp
deleted file mode 100644
index 661bace9008..00000000000
--- a/tests/pyb/spi.py.exp
+++ /dev/null
@@ -1,10 +0,0 @@
-ValueError -1
-ValueError 0
-SPI 1
-SPI 2
-SPI(1)
-SPI(1, SPI.CONTROLLER, baudrate= , polarity=1, phase=0, bits=8)
-SPI(1, SPI.PERIPHERAL, polarity=1, phase=1, bits=8)
-OSError
-b'\xff'
-b'\xff'
diff --git a/tests/pyb/switch.py b/tests/pyb/switch.py
deleted file mode 100644
index 7c44adb136c..00000000000
--- a/tests/pyb/switch.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from pyb import Switch
-
-sw = Switch()
-print(sw())
-sw.callback(print)
-sw.callback(None)
diff --git a/tests/pyb/timer.py b/tests/pyb/timer.py
deleted file mode 100644
index 251a06c081b..00000000000
--- a/tests/pyb/timer.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# check basic functionality of the timer class
-
-import pyb
-from pyb import Timer
-
-tim = Timer(4)
-tim = Timer(4, prescaler=100, period=200)
-print(tim.prescaler())
-print(tim.period())
-tim.prescaler(300)
-print(tim.prescaler())
-tim.period(400)
-print(tim.period())
-
-# Setting and printing frequency
-tim = Timer(2, freq=100)
-print(tim.freq())
-tim.freq(0.001)
-print("{:.3f}".format(tim.freq()))
diff --git a/tests/pyb/timer_callback.py b/tests/pyb/timer_callback.py
deleted file mode 100644
index 5f170ccde11..00000000000
--- a/tests/pyb/timer_callback.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# check callback feature of the timer class
-
-import pyb
-from pyb import Timer
-
-
-# callback function that disables the callback when called
-def cb1(t):
-    print("cb1")
-    t.callback(None)
-
-
-# callback function that disables the timer when called
-def cb2(t):
-    print("cb2")
-    t.deinit()
-
-
-# callback where cb4 closes over cb3.y
-def cb3(x):
-    y = x
-
-    def cb4(t):
-        print("cb4", y)
-        t.callback(None)
-
-    return cb4
-
-
-# create a timer with a callback, using callback(None) to stop
-tim = Timer(1, freq=100, callback=cb1)
-pyb.delay(5)
-print("before cb1")
-pyb.delay(15)
-
-# create a timer with a callback, using deinit to stop
-tim = Timer(2, freq=100, callback=cb2)
-pyb.delay(5)
-print("before cb2")
-pyb.delay(15)
-
-# create a timer, then set the freq, then set the callback
-tim = Timer(4)
-tim.init(freq=100)
-tim.callback(cb1)
-pyb.delay(5)
-print("before cb1")
-pyb.delay(15)
-
-# test callback with a closure
-tim.init(freq=100)
-tim.callback(cb3(3))
-pyb.delay(5)
-print("before cb4")
-pyb.delay(15)
diff --git a/tests/pyb/uart.py b/tests/pyb/uart.py
deleted file mode 100644
index 53b0ea6ade4..00000000000
--- a/tests/pyb/uart.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from pyb import UART
-
-# test we can correctly create by id
-for bus in (-1, 0, 1, 2, 5, 6):
-    try:
-        UART(bus, 9600)
-        print("UART", bus)
-    except ValueError:
-        print("ValueError", bus)
-
-uart = UART(1)
-uart = UART(1, 9600)
-uart = UART(1, 9600, bits=8, parity=None, stop=1)
-print(uart)
-
-uart.init(2400)
-print(uart)
-
-print(uart.any())
-print(uart.write("123"))
-print(uart.write(b"abcd"))
-print(uart.writechar(1))
-
-# make sure this method exists
-uart.sendbreak()
-
-# non-blocking mode
-uart = UART(1, 9600, timeout=0)
-print(uart.write(b"1"))
-print(uart.write(b"abcd"))
-print(uart.writechar(1))
-print(uart.read(100))
-
-# set rxbuf
-uart.init(9600, rxbuf=8)
-print(uart)
-uart.init(9600, rxbuf=0)
-print(uart)
-
-# set read_buf_len (legacy, use rxbuf instead)
-uart.init(9600, read_buf_len=4)
-print(uart)
-uart.init(9600, read_buf_len=0)
-print(uart)
diff --git a/tests/pyb/uart.py.exp b/tests/pyb/uart.py.exp
deleted file mode 100644
index 70e4ab8509f..00000000000
--- a/tests/pyb/uart.py.exp
+++ /dev/null
@@ -1,20 +0,0 @@
-ValueError -1
-ValueError 0
-UART 1
-UART 2
-ValueError 5
-UART 6
-UART(1, baudrate=9600, bits=8, parity=None, stop=1, flow=0, timeout=0, timeout_char=3, rxbuf=64)
-UART(1, baudrate=2400, bits=8, parity=None, stop=1, flow=0, timeout=0, timeout_char=7, rxbuf=64)
-0
-3
-4
-None
-1
-4
-None
-None
-UART(1, baudrate=9600, bits=8, parity=None, stop=1, flow=0, timeout=0, timeout_char=3, rxbuf=8)
-UART(1, baudrate=9600, bits=8, parity=None, stop=1, flow=0, timeout=0, timeout_char=3, rxbuf=0)
-UART(1, baudrate=9600, bits=8, parity=None, stop=1, flow=0, timeout=0, timeout_char=3, rxbuf=4)
-UART(1, baudrate=9600, bits=8, parity=None, stop=1, flow=0, timeout=0, timeout_char=3, rxbuf=0)
diff --git a/tests/qemu-arm/asm_test.py b/tests/qemu-arm/asm_test.py
deleted file mode 100644
index 26c7efe4273..00000000000
--- a/tests/qemu-arm/asm_test.py
+++ /dev/null
@@ -1,7 +0,0 @@
-import frozen_asm
-
-print(frozen_asm.asm_add(1, 2))
-print(frozen_asm.asm_add1(3))
-print(frozen_asm.asm_cast_bool(0), frozen_asm.asm_cast_bool(3))
-print(frozen_asm.asm_shift_int(4))
-print(frozen_asm.asm_shift_uint(4))
diff --git a/tests/run-internalbench.py b/tests/run-internalbench.py
index 2cf7f3443bb..715b0ffc9ed 100755
--- a/tests/run-internalbench.py
+++ b/tests/run-internalbench.py
@@ -8,53 +8,92 @@
 from glob import glob
 from collections import defaultdict
 
-# Tests require at least CPython 3.3. If your default python3 executable
-# is of lower version, you can point MICROPY_CPYTHON3 environment var
-# to the correct executable.
+from test_utils import (
+    base_path,
+    pyboard,
+    test_instance_description,
+    test_instance_epilog,
+    test_directory_description,
+    get_test_instance,
+)
+
 if os.name == "nt":
-    CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3.exe")
     MICROPYTHON = os.getenv(
         "MICROPY_MICROPYTHON", "../ports/windows/build-standard/micropython.exe"
     )
-else:
     CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3")
+else:
     MICROPYTHON = os.getenv("MICROPY_MICROPYTHON", "../ports/unix/build-standard/micropython")
+    CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3")
+
+MICROPYTHON_CMD = [MICROPYTHON, "-X", "emit=bytecode"]
+CPYTHON3_CMD = [CPYTHON3, "-BS"]
+
+
+injected_bench_code = b"""
+import time
 
+class bench_class:
+    ITERS = 20000000
 
-def run_tests(pyb, test_dict):
+    @staticmethod
+    def run(test):
+        t = time.ticks_us()
+        test(bench_class.ITERS)
+        t = time.ticks_diff(time.ticks_us(), t)
+        s, us = divmod(t, 1_000_000)
+        print("{}.{:06}".format(s, us))
+
+import sys
+sys.modules['bench'] = bench_class
+"""
+
+
+def execbench(test_instance, filename, iters):
+    with open(filename, "rb") as f:
+        pyfile = f.read()
+    code = (injected_bench_code + pyfile).replace(b"20000000", str(iters).encode("utf-8"))
+    return test_instance.exec(code).replace(b"\r\n", b"\n")
+
+
+def run_tests(test_instance, test_dict, iters):
     test_count = 0
     testcase_count = 0
 
     for base_test, tests in sorted(test_dict.items()):
         print(base_test + ":")
+        baseline = None
         for test_file in tests:
             # run MicroPython
-            if pyb is None:
+            if isinstance(test_instance, list):
                 # run on PC
                 try:
-                    output_mupy = subprocess.check_output(
-                        [MICROPYTHON, "-X", "emit=bytecode", test_file[0]]
-                    )
+                    output_mupy = subprocess.check_output(test_instance + [test_file[0]])
                 except subprocess.CalledProcessError:
                     output_mupy = b"CRASH"
             else:
                 # run on pyboard
-                pyb.enter_raw_repl()
+                test_instance.enter_raw_repl()
                 try:
-                    output_mupy = pyb.execfile(test_file).replace(b"\r\n", b"\n")
+                    output_mupy = execbench(test_instance, test_file[0], iters)
                 except pyboard.PyboardError:
                     output_mupy = b"CRASH"
 
-            output_mupy = float(output_mupy.strip())
+            try:
+                output_mupy = float(output_mupy.strip())
+            except ValueError:
+                output_mupy = -1
             test_file[1] = output_mupy
             testcase_count += 1
 
-        test_count += 1
-        baseline = None
-        for t in tests:
             if baseline is None:
-                baseline = t[1]
-            print("    %.3fs (%+06.2f%%) %s" % (t[1], (t[1] * 100 / baseline) - 100, t[0]))
+                baseline = test_file[1]
+            print(
+                "    %.3fs (%+06.2f%%) %s"
+                % (test_file[1], (test_file[1] * 100 / baseline) - 100, test_file[0])
+            )
+
+        test_count += 1
 
     print("{} tests performed ({} individual testcases)".format(test_count, testcase_count))
 
@@ -63,27 +102,52 @@ def run_tests(pyb, test_dict):
 
 
 def main():
-    cmd_parser = argparse.ArgumentParser(description="Run tests for MicroPython.")
-    cmd_parser.add_argument("--pyboard", action="store_true", help="run the tests on the pyboard")
+    cmd_parser = argparse.ArgumentParser(
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        description=f"""Run and manage tests for MicroPython.
+
+{test_instance_description}
+{test_directory_description}
+""",
+        epilog=f"""{test_instance_epilog}- cpython - use CPython to run the benchmarks instead\n""",
+    )
+    cmd_parser.add_argument(
+        "-t", "--test-instance", default="unix", help="the MicroPython instance to test"
+    )
+    cmd_parser.add_argument(
+        "-b", "--baudrate", default=115200, help="the baud rate of the serial device"
+    )
+    cmd_parser.add_argument("-u", "--user", default="micro", help="the telnet login username")
+    cmd_parser.add_argument("-p", "--password", default="python", help="the telnet login password")
+    cmd_parser.add_argument(
+        "-d", "--test-dirs", nargs="*", help="input test directories (if no files given)"
+    )
+    cmd_parser.add_argument(
+        "-I",
+        "--iters",
+        type=int,
+        default=200_000,
+        help="number of test iterations, only for remote instances (default 200,000)",
+    )
     cmd_parser.add_argument("files", nargs="*", help="input test files")
     args = cmd_parser.parse_args()
 
-    # Note pyboard support is copied over from run-tests.py, not tests, and likely needs revamping
-    if args.pyboard:
-        import pyboard
-
-        pyb = pyboard.Pyboard("/dev/ttyACM0")
-        pyb.enter_raw_repl()
+    if args.test_instance == "cpython":
+        test_instance = CPYTHON3_CMD
     else:
-        pyb = None
+        # Note pyboard support is copied over from run-tests.py, not tests, and likely needs revamping
+        test_instance = get_test_instance(
+            args.test_instance, args.baudrate, args.user, args.password
+        )
+        if test_instance is None:
+            test_instance = MICROPYTHON_CMD
 
     if len(args.files) == 0:
-        if pyb is None:
-            # run PC tests
-            test_dirs = ("internal_bench",)
+        if args.test_dirs:
+            test_dirs = tuple(args.test_dirs)
         else:
-            # run pyboard tests
-            test_dirs = ("basics", "float", "pyb")
+            test_dirs = ("internal_bench",)
+
         tests = sorted(
             test_file
             for test_files in (glob("{}/*.py".format(dir)) for dir in test_dirs)
@@ -100,7 +164,7 @@ def main():
             continue
         test_dict[m.group(1)].append([t, None])
 
-    if not run_tests(pyb, test_dict):
+    if not run_tests(test_instance, test_dict, args.iters):
         sys.exit(1)
 
 
diff --git a/tests/run-multitests.py b/tests/run-multitests.py
index 81db31ea94e..de59db2dc38 100755
--- a/tests/run-multitests.py
+++ b/tests/run-multitests.py
@@ -15,6 +15,14 @@
 import subprocess
 import tempfile
 
+from test_utils import (
+    base_path,
+    pyboard,
+    test_instance_epilog,
+    convert_device_shortcut_to_real_device,
+    create_test_report,
+)
+
 test_dir = os.path.abspath(os.path.dirname(__file__))
 
 if os.path.abspath(sys.path[0]) == test_dir:
@@ -22,18 +30,17 @@
     # accidentally importing tests like micropython/const.py
     sys.path.pop(0)
 
-sys.path.insert(0, test_dir + "/../tools")
-import pyboard
-
 if os.name == "nt":
     CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3.exe")
-    MICROPYTHON = os.getenv(
-        "MICROPY_MICROPYTHON", test_dir + "/../ports/windows/build-standard/micropython.exe"
+    MICROPYTHON = os.path.abspath(
+        os.getenv(
+            "MICROPY_MICROPYTHON", test_dir + "/../ports/windows/build-standard/micropython.exe"
+        )
     )
 else:
     CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3")
-    MICROPYTHON = os.getenv(
-        "MICROPY_MICROPYTHON", test_dir + "/../ports/unix/build-standard/micropython"
+    MICROPYTHON = os.path.abspath(
+        os.getenv("MICROPY_MICROPYTHON", test_dir + "/../ports/unix/build-standard/micropython")
     )
 
 # For diff'ing test output
@@ -103,15 +110,14 @@ def output_metric(data):
 multitest.flush()
 """
 
-# The btstack implementation on Unix generates some spurious output that we
-# can't control.  Also other platforms may output certain warnings/errors that
-# can be safely ignored.
+# Some ports generate output we can't control, and that can be safely ignored.
 IGNORE_OUTPUT_MATCHES = (
-    "libusb: error ",  # It tries to open devices that it doesn't have access to (libusb prints unconditionally).
+    "libusb: error ",  # unix btstack tries to open devices that it doesn't have access to (libusb prints unconditionally).
     "hci_transport_h2_libusb.c",  # Same issue. We enable LOG_ERROR in btstack.
-    "USB Path: ",  # Hardcoded in btstack's libusb transport.
-    "hci_number_completed_packet",  # Warning from btstack.
+    "USB Path: ",  # Hardcoded in unix btstack's libusb transport.
+    "hci_number_completed_packet",  # Warning from unix btstack.
     "lld_pdu_get_tx_flush_nb HCI packet count mismatch (",  # From ESP-IDF, see https://github.com/espressif/esp-idf/issues/5105
+    " ets_task(",  # ESP8266 port debug output
 )
 
 
@@ -129,6 +135,11 @@ def get_host_ip(_ip_cache=[]):
     return _ip_cache[0]
 
 
+def decode(output):
+    # Convenience function to convert raw process or serial output to ASCII
+    return str(output, "ascii", "backslashreplace")
+
+
 class PyInstance:
     def __init__(self):
         pass
@@ -155,6 +166,7 @@ def start_file(self, filename, prepend="", append=""):
 class PyInstanceSubProcess(PyInstance):
     def __init__(self, argv, env=None):
         self.argv = argv
+        self.cwd = None
         self.env = {n: v for n, v in (i.split("=") for i in env)} if env else None
         self.popen = None
         self.finished = True
@@ -162,6 +174,15 @@ def __init__(self, argv, env=None):
     def __str__(self):
         return self.argv[0].rsplit("/")[-1]
 
+    def prepare_script_from_file(self, filename, prepend, append):
+        # Make tests run in the directory of the test file, and in an isolated environment
+        # (i.e. `import io` would otherwise get the `tests/io` directory).
+        self.cwd = os.path.dirname(filename)
+        remove_cwd_from_sys_path = b"import sys\nsys.path.remove('')\n\n"
+        return remove_cwd_from_sys_path + super().prepare_script_from_file(
+            filename, prepend, append
+        )
+
     def run_script(self, script):
         output = b""
         err = None
@@ -171,12 +192,13 @@ def run_script(self, script):
                 stdout=subprocess.PIPE,
                 stderr=subprocess.STDOUT,
                 input=script,
+                cwd=self.cwd,
                 env=self.env,
             )
             output = p.stdout
         except subprocess.CalledProcessError as er:
             err = er
-        return str(output.strip(), "ascii"), err
+        return decode(output.strip()), err
 
     def start_script(self, script):
         self.popen = subprocess.Popen(
@@ -184,6 +206,7 @@ def start_script(self, script):
             stdin=subprocess.PIPE,
             stdout=subprocess.PIPE,
             stderr=subprocess.STDOUT,
+            cwd=self.cwd,
             env=self.env,
         )
         self.finished = False
@@ -202,7 +225,7 @@ def readline(self):
             self.finished = self.popen.poll() is not None
             return None, None
         else:
-            return str(out.rstrip(), "ascii"), None
+            return decode(out.rstrip()), None
 
     def write(self, data):
         self.popen.stdin.write(data)
@@ -214,21 +237,12 @@ def is_finished(self):
     def wait_finished(self):
         self.popen.wait()
         out = self.popen.stdout.read()
-        return str(out, "ascii"), ""
+        return decode(out), ""
 
 
 class PyInstancePyboard(PyInstance):
-    @staticmethod
-    def map_device_shortcut(device):
-        if device[0] == "a" and device[1:].isdigit():
-            return "/dev/ttyACM" + device[1:]
-        elif device[0] == "u" and device[1:].isdigit():
-            return "/dev/ttyUSB" + device[1:]
-        else:
-            return device
-
     def __init__(self, device):
-        device = self.map_device_shortcut(device)
+        device = device
         self.device = device
         self.pyb = pyboard.Pyboard(device)
         self.pyb.enter_raw_repl()
@@ -249,7 +263,7 @@ def run_script(self, script):
             output = self.pyb.exec_(script)
         except pyboard.PyboardError as er:
             err = er
-        return str(output.strip(), "ascii"), err
+        return decode(output.strip()), err
 
     def start_script(self, script):
         self.pyb.enter_raw_repl()
@@ -262,19 +276,22 @@ def stop(self):
     def readline(self):
         if self.finished:
             return None, None
-        if self.pyb.serial.inWaiting() == 0:
-            return None, None
-        out = self.pyb.read_until(1, (b"\r\n", b"\x04"))
-        if out.endswith(b"\x04"):
-            self.finished = True
-            out = out[:-1]
-            err = str(self.pyb.read_until(1, b"\x04"), "ascii")
-            err = err[:-1]
-            if not out and not err:
+        try:
+            if self.pyb.serial.inWaiting() == 0:
                 return None, None
-        else:
-            err = None
-        return str(out.rstrip(), "ascii"), err
+            out = self.pyb.read_until(1, (b"\r\n", b"\x04"))
+            if out.endswith(b"\x04"):
+                self.finished = True
+                out = out[:-1]
+                err = decode(self.pyb.read_until(1, b"\x04"))
+                err = err[:-1]
+                if not out and not err:
+                    return None, None
+            else:
+                err = None
+            return decode(out.rstrip()), err
+        except OSError as e:
+            return None, "Failed to read from instance: {}".format(e)
 
     def write(self, data):
         self.pyb.serial.write(data)
@@ -284,7 +301,7 @@ def is_finished(self):
 
     def wait_finished(self):
         out, err = self.pyb.follow(10, None)
-        return str(out, "ascii"), str(err, "ascii")
+        return decode(out), decode(err)
 
 
 def prepare_test_file_list(test_files):
@@ -420,7 +437,10 @@ def run_test_on_instances(test_file, num_instances, instances):
 
     # Stop all instances
     for idx in range(num_instances):
-        instances[idx].stop()
+        try:
+            instances[idx].stop()
+        except OSError as e:
+            output[idx].append("Runner failed to stop instance: {}".format(e))
 
     output_str = ""
     for idx, lines in enumerate(output):
@@ -475,9 +495,7 @@ def print_diff(a, b):
 
 
 def run_tests(test_files, instances_truth, instances_test):
-    skipped_tests = []
-    passed_tests = []
-    failed_tests = []
+    test_results = []
 
     for test_file, num_instances in test_files:
         instances_str = "|".join(str(instances_test[i]) for i in range(num_instances))
@@ -513,13 +531,13 @@ def run_tests(test_files, instances_truth, instances_test):
         # Print result of test
         if skip:
             print("skip")
-            skipped_tests.append(test_file)
+            test_results.append((test_file, "skip", ""))
         elif output_test == output_truth:
             print("pass")
-            passed_tests.append(test_file)
+            test_results.append((test_file, "pass", ""))
         else:
             print("FAIL")
-            failed_tests.append(test_file)
+            test_results.append((test_file, "fail", ""))
             if not cmd_args.show_output:
                 print("### TEST ###")
                 print(output_test, end="")
@@ -536,15 +554,7 @@ def run_tests(test_files, instances_truth, instances_test):
         if cmd_args.show_output:
             print()
 
-    print("{} tests performed".format(len(skipped_tests) + len(passed_tests) + len(failed_tests)))
-    print("{} tests passed".format(len(passed_tests)))
-
-    if skipped_tests:
-        print("{} tests skipped: {}".format(len(skipped_tests), " ".join(skipped_tests)))
-    if failed_tests:
-        print("{} tests failed: {}".format(len(failed_tests), " ".join(failed_tests)))
-
-    return not failed_tests
+    return test_results
 
 
 def main():
@@ -552,16 +562,24 @@ def main():
 
     cmd_parser = argparse.ArgumentParser(
         description="Run network tests for MicroPython",
+        epilog=(
+            test_instance_epilog
+            + "Each instance arg can optionally have custom env provided, eg. ,ENV=VAR,ENV=VAR...\n"
+        ),
         formatter_class=argparse.RawTextHelpFormatter,
     )
     cmd_parser.add_argument(
         "-s", "--show-output", action="store_true", help="show test output after running"
     )
     cmd_parser.add_argument(
-        "-t", "--trace-output", action="store_true", help="trace test output while running"
+        "-c", "--trace-output", action="store_true", help="trace test output while running"
     )
     cmd_parser.add_argument(
-        "-i", "--instance", action="append", default=[], help="instance(s) to run the tests on"
+        "-t",
+        "--test-instance",
+        action="append",
+        default=[],
+        help="instance(s) to run the tests on",
     )
     cmd_parser.add_argument(
         "-p",
@@ -570,19 +588,17 @@ def main():
         default=1,
         help="repeat the test with this many permutations of the instance order",
     )
-    cmd_parser.epilog = (
-        "Supported instance types:\r\n"
-        " -i pyb:   physical device (eg. pyboard) on provided repl port.\n"
-        " -i micropython  unix micropython instance, path customised with MICROPY_MICROPYTHON env.\n"
-        " -i cpython      desktop python3 instance, path customised with MICROPY_CPYTHON3 env.\n"
-        " -i exec:  custom program run on provided path.\n"
-        "Each instance arg can optionally have custom env provided, eg. ,ENV=VAR,ENV=VAR...\n"
+    cmd_parser.add_argument(
+        "-r",
+        "--result-dir",
+        default=base_path("results"),
+        help="directory for test results",
     )
     cmd_parser.add_argument("files", nargs="+", help="input test files")
     cmd_args = cmd_parser.parse_args()
 
     # clear search path to make sure tests use only builtin modules and those in extmod
-    os.environ["MICROPYPATH"] = os.pathsep.join(("", ".frozen", "../extmod"))
+    os.environ["MICROPYPATH"] = os.pathsep.join((".frozen", base_path("../extmod")))
 
     test_files = prepare_test_file_list(cmd_args.files)
     max_instances = max(t[1] for t in test_files)
@@ -590,33 +606,36 @@ def main():
     instances_truth = [PyInstanceSubProcess([PYTHON_TRUTH]) for _ in range(max_instances)]
 
     instances_test = []
-    for i in cmd_args.instance:
+    for i in cmd_args.test_instance:
         # Each instance arg is ,ENV=VAR,ENV=VAR...
         i = i.split(",")
         cmd = i[0]
         env = i[1:]
         if cmd.startswith("exec:"):
             instances_test.append(PyInstanceSubProcess([cmd[len("exec:") :]], env))
-        elif cmd == "micropython":
+        elif cmd == "unix":
             instances_test.append(PyInstanceSubProcess([MICROPYTHON], env))
         elif cmd == "cpython":
             instances_test.append(PyInstanceSubProcess([CPYTHON3], env))
-        elif cmd.startswith("pyb:"):
-            instances_test.append(PyInstancePyboard(cmd[len("pyb:") :]))
+        elif cmd == "webassembly" or cmd.startswith("execpty:"):
+            print("unsupported instance string: {}".format(cmd), file=sys.stderr)
+            sys.exit(2)
         else:
-            print("unknown instance string: {}".format(cmd), file=sys.stderr)
-            sys.exit(1)
+            device = convert_device_shortcut_to_real_device(cmd)
+            instances_test.append(PyInstancePyboard(device))
 
     for _ in range(max_instances - len(instances_test)):
         instances_test.append(PyInstanceSubProcess([MICROPYTHON]))
 
+    os.makedirs(cmd_args.result_dir, exist_ok=True)
     all_pass = True
     try:
         for i, instances_test_permutation in enumerate(itertools.permutations(instances_test)):
             if i >= cmd_args.permutations:
                 break
 
-            all_pass &= run_tests(test_files, instances_truth, instances_test_permutation)
+            test_results = run_tests(test_files, instances_truth, instances_test_permutation)
+            all_pass &= create_test_report(cmd_args, test_results)
 
     finally:
         for i in instances_truth:
diff --git a/tests/run-natmodtests.py b/tests/run-natmodtests.py
index 16bb4692225..83974a6aa09 100755
--- a/tests/run-natmodtests.py
+++ b/tests/run-natmodtests.py
@@ -9,8 +9,15 @@
 import sys
 import argparse
 
-sys.path.append("../tools")
-import pyboard
+from test_utils import (
+    base_path,
+    pyboard,
+    TEST_ENTER_RAW_REPL_TIMEOUT,
+    TEST_MAXIMUM_RAW_REPL_FAILURES,
+    test_instance_epilog,
+    get_test_instance,
+    create_test_report,
+)
 
 # Paths for host executables
 CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3")
@@ -21,21 +28,41 @@
 # Supported tests and their corresponding mpy module
 TEST_MAPPINGS = {
     "btree": "btree/btree_$(ARCH).mpy",
+    "deflate": "deflate/deflate_$(ARCH).mpy",
     "framebuf": "framebuf/framebuf_$(ARCH).mpy",
-    "uheapq": "uheapq/uheapq_$(ARCH).mpy",
-    "urandom": "urandom/urandom_$(ARCH).mpy",
-    "ure": "ure/ure_$(ARCH).mpy",
-    "uzlib": "uzlib/uzlib_$(ARCH).mpy",
+    "heapq": "heapq/heapq_$(ARCH).mpy",
+    "random": "random/random_$(ARCH).mpy",
+    "re": "re/re_$(ARCH).mpy",
 }
 
+# Supported architectures for native mpy modules
+AVAILABLE_ARCHS = (
+    "x86",
+    "x64",
+    "armv6",
+    "armv6m",
+    "armv7m",
+    "armv7em",
+    "armv7emsp",
+    "armv7emdp",
+    "xtensa",
+    "xtensawin",
+    "rv32imc",
+    "rv64imc",
+)
+
+ARCH_MAPPINGS = {"armv7em": "armv7m"}
+
 # Code to allow a target MicroPython to import an .mpy from RAM
 injected_import_hook_code = """\
-import usys, uos, uio
-class __File(uio.IOBase):
+import sys, io, vfs
+class __File(io.IOBase):
   def __init__(self):
     self.off = 0
   def ioctl(self, request, arg):
-    return 0
+    if request == 4: # MP_STREAM_CLOSE
+      return 0
+    return -1
   def readinto(self, buf):
     buf[:] = memoryview(__buf)[self.off:self.off + len(buf)]
     self.off += len(buf)
@@ -46,15 +73,16 @@ def mount(self, readonly, mkfs):
   def chdir(self, path):
     pass
   def stat(self, path):
-    if path == '__injected.mpy':
+    if path == '/__injected.mpy':
       return tuple(0 for _ in range(10))
     else:
       raise OSError(-2) # ENOENT
   def open(self, path, mode):
     return __File()
-uos.mount(__FS(), '/__remote')
-uos.chdir('/__remote')
-usys.modules['{}'] = __import__('__injected')
+vfs.mount(__FS(), '/__remote')
+sys.path.insert(0, '/__remote')
+{import_prelude}
+sys.modules['{}'] = __import__('__injected')
 """
 
 
@@ -86,7 +114,7 @@ def close(self):
 
     def run_script(self, script):
         try:
-            self.pyb.enter_raw_repl()
+            self.pyb.enter_raw_repl(timeout_overall=TEST_ENTER_RAW_REPL_TIMEOUT)
             output = self.pyb.exec_(script)
             output = output.replace(b"\r\n", b"\n")
             return output, None
@@ -94,13 +122,35 @@ def run_script(self, script):
             return b"", er
 
 
-def run_tests(target_truth, target, args, stats):
+def detect_architecture(target):
+    with open("./feature_check/target_info.py", "rb") as f:
+        target_info_data = f.read()
+        result_out, error = target.run_script(target_info_data)
+        if error is not None:
+            return None, None, error
+        info = result_out.split(b" ")
+        if len(info) < 2:
+            return None, None, "unexpected target info: {}".format(info)
+        platform = info[0].strip().decode()
+        arch = info[1].strip().decode()
+        if arch not in AVAILABLE_ARCHS:
+            if arch == "None":
+                return None, None, "the target does not support dynamic modules"
+            else:
+                return None, None, "{} is not a supported architecture".format(arch)
+        return platform, arch, None
+
+
+def run_tests(target_truth, target, args, resolved_arch):
+    raw_repl_failure_count = 0
+    test_results = []
     for test_file in args.files:
         # Find supported test
+        test_file_basename = os.path.basename(test_file)
         for k, v in TEST_MAPPINGS.items():
-            if test_file.find(k) != -1:
+            if test_file_basename.startswith(k):
                 test_module = k
-                test_mpy = v.replace("$(ARCH)", args.arch)
+                test_mpy = v.replace("$(ARCH)", resolved_arch)
                 break
         else:
             print("----  {} - no matching mpy".format(test_file))
@@ -111,13 +161,16 @@ def run_tests(target_truth, target, args, stats):
             test_file_data = f.read()
 
         # Create full test with embedded .mpy
+        test_script = b"import sys\nsys.path.remove('')\n\n"
         try:
             with open(NATMOD_EXAMPLE_DIR + test_mpy, "rb") as f:
-                test_script = b"__buf=" + bytes(repr(f.read()), "ascii") + b"\n"
+                test_script += b"__buf=" + bytes(repr(f.read()), "ascii") + b"\n"
         except OSError:
-            print("----  {} - mpy file not compiled".format(test_file))
+            test_results.append((test_file, "skip", "mpy file not compiled"))
+            print("skip  {} - mpy file not compiled".format(test_file))
             continue
         test_script += bytes(injected_import_hook_code.format(test_module), "ascii")
+        test_script += b"print('START TEST')\n"
         test_script += test_file_data
 
         # Run test under MicroPython
@@ -125,11 +178,23 @@ def run_tests(target_truth, target, args, stats):
 
         # Work out result of test
         extra = ""
+        result_out = result_out.removeprefix(b"START TEST\n")
         if error is None and result_out == b"SKIP\n":
             result = "SKIP"
+        elif (
+            error is not None
+            and error.args[0] == "exception"
+            and error.args[1] == b""
+            and b"MemoryError" in error.args[2]
+        ):
+            # Test had a MemoryError before anything (should be at least "START TEST")
+            # was printed, so the test is too big for the target.
+            result = "LRGE"
         elif error is not None:
             result = "FAIL"
             extra = " - " + str(error)
+            if str(error).startswith("could not enter raw repl"):
+                raw_repl_failure_count += 1
         else:
             # Check result against truth
             try:
@@ -147,55 +212,96 @@ def run_tests(target_truth, target, args, stats):
                 result = "pass"
 
         # Accumulate statistics
-        stats["total"] += 1
         if result == "pass":
-            stats["pass"] += 1
+            test_results.append((test_file, "pass", ""))
         elif result == "SKIP":
-            stats["skip"] += 1
+            test_results.append((test_file, "skip", ""))
+        elif result == "LRGE":
+            test_results.append((test_file, "skip", "too large"))
         else:
-            stats["fail"] += 1
+            test_results.append((test_file, "fail", ""))
 
         # Print result
         print("{:4}  {}{}".format(result, test_file, extra))
 
+        if raw_repl_failure_count > TEST_MAXIMUM_RAW_REPL_FAILURES:
+            print("Too many raw REPL failures, aborting test run")
+            break
+
+    return test_results
+
 
 def main():
+    global injected_import_hook_code
+
     cmd_parser = argparse.ArgumentParser(
-        description="Run dynamic-native-module tests under MicroPython"
+        description="Run dynamic-native-module tests under MicroPython",
+        epilog=test_instance_epilog,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    cmd_parser.add_argument(
+        "-t", "--test-instance", default="unix", help="the MicroPython instance to test"
     )
+    cmd_parser.add_argument("--baudrate", default=115200, help="baud rate of the serial device")
+    cmd_parser.add_argument("--user", default="micro", help="telnet login username")
+    cmd_parser.add_argument("--password", default="python", help="telnet login password")
     cmd_parser.add_argument(
-        "-p", "--pyboard", action="store_true", help="run tests via pyboard.py"
+        "-a", "--arch", choices=AVAILABLE_ARCHS, help="override native architecture of the target"
     )
     cmd_parser.add_argument(
-        "-d", "--device", default="/dev/ttyACM0", help="the device for pyboard.py"
+        "-b",
+        "--begin",
+        metavar="PROLOGUE",
+        default=None,
+        help="prologue python file to execute before module import",
     )
     cmd_parser.add_argument(
-        "-a", "--arch", default="x64", help="native architecture of the target"
+        "-r",
+        "--result-dir",
+        default=base_path("results"),
+        help="directory for test results",
     )
     cmd_parser.add_argument("files", nargs="*", help="input test files")
     args = cmd_parser.parse_args()
 
+    prologue = ""
+    if args.begin:
+        with open(args.begin, "rt") as source:
+            prologue = source.read()
+    injected_import_hook_code = injected_import_hook_code.replace("{import_prelude}", prologue)
+
     target_truth = TargetSubprocess([CPYTHON3])
 
-    if args.pyboard:
-        target = TargetPyboard(pyboard.Pyboard(args.device))
-    else:
+    target = get_test_instance(args.test_instance, args.baudrate, args.user, args.password)
+    if target is None:
+        # Use the unix port of MicroPython.
         target = TargetSubprocess([MICROPYTHON])
+    else:
+        # Use a remote target.
+        target = TargetPyboard(target)
 
-    stats = {"total": 0, "pass": 0, "fail": 0, "skip": 0}
-    run_tests(target_truth, target, args, stats)
+    if hasattr(args, "arch") and args.arch is not None:
+        target_arch = args.arch
+        target_platform = None
+    else:
+        target_platform, target_arch, error = detect_architecture(target)
+        if error:
+            print("Cannot run tests: {}".format(error))
+            sys.exit(2)
+    target_arch = ARCH_MAPPINGS.get(target_arch, target_arch)
+
+    if target_platform:
+        print("platform={} ".format(target_platform), end="")
+    print("arch={}".format(target_arch))
+
+    os.makedirs(args.result_dir, exist_ok=True)
+    test_results = run_tests(target_truth, target, args, target_arch)
+    res = create_test_report(args, test_results)
 
     target.close()
     target_truth.close()
 
-    print("{} tests performed".format(stats["total"]))
-    print("{} tests passed".format(stats["pass"]))
-    if stats["fail"]:
-        print("{} tests failed".format(stats["fail"]))
-    if stats["skip"]:
-        print("{} tests skipped".format(stats["skip"]))
-
-    if stats["fail"]:
+    if not res:
         sys.exit(1)
 
 
diff --git a/tests/run-perfbench.py b/tests/run-perfbench.py
index 578f975bb84..f9641b6fade 100755
--- a/tests/run-perfbench.py
+++ b/tests/run-perfbench.py
@@ -8,12 +8,17 @@
 import subprocess
 import sys
 import argparse
+import re
 from glob import glob
 
-sys.path.append("../tools")
-import pyboard
-
-prepare_script_for_target = __import__("run-tests").prepare_script_for_target
+from test_utils import (
+    base_path,
+    pyboard,
+    set_injected_prologue,
+    get_test_instance,
+    prepare_script_for_target,
+    create_test_report,
+)
 
 # Paths for host executables
 if os.name == "nt":
@@ -45,7 +50,7 @@ def run_script_on_target(target, script):
     output = b""
     err = None
 
-    if isinstance(target, pyboard.Pyboard):
+    if hasattr(target, "enter_raw_repl"):
         # Run via pyboard interface
         try:
             target.enter_raw_repl()
@@ -90,40 +95,55 @@ def run_benchmark_on_target(target, script):
 
 
 def run_benchmarks(args, target, param_n, param_m, n_average, test_list):
+    test_results = []
     skip_complex = run_feature_test(target, "complex") != "complex"
     skip_native = run_feature_test(target, "native_check") != "native"
-    target_had_error = False
 
     for test_file in sorted(test_list):
         print(test_file + ": ", end="")
 
         # Check if test should be skipped
-        skip = (
-            skip_complex
-            and test_file.find("bm_fft") != -1
-            or skip_native
-            and test_file.find("viper_") != -1
-        )
-        if skip:
-            print("SKIP")
+        skip_reason = None
+        if skip_complex and test_file.endswith(("bm_fft.py", "misc_mandel.py")):
+            skip_reason = "complex not supported"
+        elif skip_native and test_file.find("viper_") != -1:
+            skip_reason = "native not supported"
+        if skip_reason:
+            test_results.append((test_file, "skip", skip_reason))
+            print("SKIP:", skip_reason)
             continue
 
         # Create test script
+        test_script = b"import sys\nsys.path.remove('')\n\n"
         with open(test_file, "rb") as f:
-            test_script = f.read()
+            test_script += f.read()
         with open(BENCH_SCRIPT_DIR + "benchrun.py", "rb") as f:
             test_script += f.read()
         test_script += b"bm_run(%u, %u)\n" % (param_n, param_m)
 
+        # Search for the bm_params dict, to extract the minimum memory required.
+        m = re.search(rb"bm_params = {\s+\((\d+), (\d+)\):", test_script)
+        if not m:
+            print(f"Test file '{test_file}' doesn't contain valid 'bm_params'")
+            sys.exit(2)
+        min_m = int(m.group(2))
+
+        # Skip the test if the target doesn't have enough memory.
+        if param_m < min_m:
+            test_results.append((test_file, "skip", "too large"))
+            print("SKIP: too large")
+            continue
+
         # Write full test script if needed
         if 0:
             with open("%s.full" % test_file, "wb") as f:
                 f.write(test_script)
 
         # Process script through mpy-cross if needed
-        if isinstance(target, pyboard.Pyboard) or args.via_mpy:
-            crash, test_script_target = prepare_script_for_target(args, script_text=test_script)
+        if hasattr(target, "enter_raw_repl") or args.via_mpy:
+            crash, test_script_target = prepare_script_for_target(args, test_script, test_file)
             if crash:
+                test_results.append((test_file, "fail", "preparation"))
                 print("CRASH:", test_script_target)
                 continue
         else:
@@ -161,10 +181,13 @@ def run_benchmarks(args, target, param_n, param_m, n_average, test_list):
                 error = "FAIL truth"
 
         if error is not None:
-            if not error.startswith("SKIP"):
-                target_had_error = True
+            if error.startswith("SKIP"):
+                test_results.append((test_file, "skip", error))
+            else:
+                test_results.append((test_file, "fail", error))
             print(error)
         else:
+            test_results.append((test_file, "pass", ""))
             t_avg, t_sd = compute_stats(times)
             s_avg, s_sd = compute_stats(scores)
             print(
@@ -178,7 +201,7 @@ def run_benchmarks(args, target, param_n, param_m, n_average, test_list):
 
         sys.stdout.flush()
 
-    return target_had_error
+    return test_results
 
 
 def parse_output(filename):
@@ -189,7 +212,13 @@ def parse_output(filename):
         m = int(m.split("=")[1])
         data = []
         for l in f:
-            if ": " in l and ": SKIP" not in l and "CRASH: " not in l:
+            if (
+                ": " in l
+                and ": SKIP" not in l
+                and "CRASH: " not in l
+                and re.search("skipped.*: ", l) is None
+                and "failed: " not in l
+            ):
                 name, values = l.strip().split(": ")
                 values = tuple(float(v) for v in values.split())
                 data.append((name,) + values)
@@ -245,17 +274,17 @@ def compute_diff(file1, file2, diff_score):
 def main():
     cmd_parser = argparse.ArgumentParser(description="Run benchmarks for MicroPython")
     cmd_parser.add_argument(
-        "-t", "--diff-time", action="store_true", help="diff time outputs from a previous run"
+        "-m", "--diff-time", action="store_true", help="diff time outputs from a previous run"
     )
     cmd_parser.add_argument(
         "-s", "--diff-score", action="store_true", help="diff score outputs from a previous run"
     )
     cmd_parser.add_argument(
-        "-p", "--pyboard", action="store_true", help="run tests via pyboard.py"
-    )
-    cmd_parser.add_argument(
-        "-d", "--device", default="/dev/ttyACM0", help="the device for pyboard.py"
+        "-t", "--test-instance", default="unix", help="the MicroPython instance to test"
     )
+    cmd_parser.add_argument("--baudrate", default=115200, help="baud rate of the serial device")
+    cmd_parser.add_argument("--user", default="micro", help="telnet login username")
+    cmd_parser.add_argument("--password", default="python", help="telnet login password")
     cmd_parser.add_argument("-a", "--average", default="8", help="averaging number")
     cmd_parser.add_argument(
         "--emit", default="bytecode", help="MicroPython emitter to use (bytecode or native)"
@@ -263,6 +292,18 @@ def main():
     cmd_parser.add_argument("--heapsize", help="heapsize to use (use default if not specified)")
     cmd_parser.add_argument("--via-mpy", action="store_true", help="compile code to .mpy first")
     cmd_parser.add_argument("--mpy-cross-flags", default="", help="flags to pass to mpy-cross")
+    cmd_parser.add_argument(
+        "--begin",
+        metavar="PROLOGUE",
+        default=None,
+        help="prologue python file to execute before module import",
+    )
+    cmd_parser.add_argument(
+        "-r",
+        "--result-dir",
+        default=base_path("results"),
+        help="directory for test results",
+    )
     cmd_parser.add_argument(
         "N", nargs=1, help="N parameter (approximate target CPU frequency in MHz)"
     )
@@ -274,6 +315,12 @@ def main():
         compute_diff(args.N[0], args.M[0], args.diff_score)
         sys.exit(0)
 
+    prologue = ""
+    if args.begin:
+        with open(args.begin, "rt") as source:
+            prologue = source.read()
+    set_injected_prologue(prologue)
+
     # N, M = 50, 25 # esp8266
     # N, M = 100, 100 # pyboard, esp32
     # N, M = 1000, 1000 # PC
@@ -281,38 +328,37 @@ def main():
     M = int(args.M[0])
     n_average = int(args.average)
 
-    if args.pyboard:
-        if not args.mpy_cross_flags:
-            args.mpy_cross_flags = "-march=armv7m"
-        target = pyboard.Pyboard(args.device)
-        target.enter_raw_repl()
-    else:
+    target = get_test_instance(args.test_instance, args.baudrate, args.user, args.password)
+    if target is None:
+        # Use the unix port of MicroPython.
         target = [MICROPYTHON, "-X", "emit=" + args.emit]
         if args.heapsize is not None:
             target.extend(["-X", "heapsize=" + args.heapsize])
+    else:
+        # Use a remote target.
+        if not args.mpy_cross_flags:
+            args.mpy_cross_flags = "-march=armv7m"
 
     if len(args.files) == 0:
-        tests_skip = ("benchrun.py",)
-        if M <= 25:
-            # These scripts are too big to be compiled by the target
-            tests_skip += ("bm_chaos.py", "bm_hexiom.py", "misc_raytrace.py")
         tests = sorted(
             BENCH_SCRIPT_DIR + test_file
             for test_file in os.listdir(BENCH_SCRIPT_DIR)
-            if test_file.endswith(".py") and test_file not in tests_skip
+            if test_file.endswith(".py") and test_file != "benchrun.py"
         )
     else:
         tests = sorted(args.files)
 
     print("N={} M={} n_average={}".format(N, M, n_average))
 
-    target_had_error = run_benchmarks(args, target, N, M, n_average, tests)
+    os.makedirs(args.result_dir, exist_ok=True)
+    test_results = run_benchmarks(args, target, N, M, n_average, tests)
+    res = create_test_report(args, test_results)
 
-    if isinstance(target, pyboard.Pyboard):
+    if hasattr(target, "exit_raw_repl"):
         target.exit_raw_repl()
         target.close()
 
-    if target_had_error:
+    if not res:
         sys.exit(1)
 
 
diff --git a/tests/run-tests-exp.py b/tests/run-tests-exp.py
deleted file mode 100644
index 21b6645336f..00000000000
--- a/tests/run-tests-exp.py
+++ /dev/null
@@ -1,94 +0,0 @@
-#
-# This is minimal MicroPython variant of run-tests.py script, which uses
-# .exp files as generated by run-tests.py --write-exp. It is useful to run
-# testsuite on systems which have neither CPython3 nor unix shell.
-# This script is intended to be run by the same interpreter executable
-# which is to be tested, so should use minimal language functionality.
-#
-import usys as sys
-import uos as os
-
-
-tests = ["basics", "micropython", "float", "import", "io", " misc", "unicode", "extmod", "unix"]
-
-if sys.platform == "win32":
-    MICROPYTHON = "micropython.exe"
-else:
-    MICROPYTHON = "micropython"
-
-
-def should_skip(test):
-    if test.startswith("native"):
-        return True
-    if test.startswith("viper"):
-        return True
-
-
-test_count = 0
-passed_count = 0
-skip_count = 0
-
-for suite in tests:
-    # print("Running in: %s" % suite)
-    if sys.platform == "win32":
-        # dir /b prints only contained filenames, one on a line
-        # http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/dir.mspx
-        r = os.system("dir /b %s/*.py >tests.lst" % suite)
-    else:
-        r = os.system("ls %s/*.py | xargs -n1 basename >tests.lst" % suite)
-    assert r == 0
-
-    with open("tests.lst") as f:
-        testcases = f.readlines()
-        testcases = [l[:-1] for l in testcases]
-    assert testcases, "No tests found in dir '%s', which is implausible" % suite
-    # print(testcases)
-    for t in testcases:
-        if t == "native_check.py":
-            continue
-
-        qtest = "%s/%s" % (suite, t)
-
-        if should_skip(t):
-            print("skip " + qtest)
-            skip_count += 1
-            continue
-
-        exp = None
-        try:
-            f = open(qtest + ".exp")
-            exp = f.read()
-            f.close()
-        except OSError:
-            pass
-
-        if exp is not None:
-            # print("run " + qtest)
-            r = os.system(MICROPYTHON + " %s >.tst.out" % qtest)
-            if r == 0:
-                f = open(".tst.out")
-                out = f.read()
-                f.close()
-            else:
-                out = "CRASH"
-
-            if out == "SKIP\n":
-                print("skip " + qtest)
-                skip_count += 1
-            else:
-                if out == exp:
-                    print("pass " + qtest)
-                    passed_count += 1
-                else:
-                    print("FAIL " + qtest)
-
-                test_count += 1
-        else:
-            skip_count += 1
-
-print("%s tests performed" % test_count)
-print("%s tests passed" % passed_count)
-if test_count != passed_count:
-    print("%s tests failed" % (test_count - passed_count))
-if skip_count:
-    print("%s tests skipped" % skip_count)
diff --git a/tests/run-tests-exp.sh b/tests/run-tests-exp.sh
deleted file mode 100755
index 177090cd8db..00000000000
--- a/tests/run-tests-exp.sh
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/bin/sh
-#
-# This is plain shell variant of run-tests.py script, which uses .exp files
-# as generated by run-tests.py --write-exp. It is useful to run testsuite
-# on embedded systems which don't have CPython3.
-#
-
-RM="rm -f"
-MP_PY=micropython
-
-numtests=0
-numtestcases=0
-numpassed=0
-numskipped=0
-numfailed=0
-nameskipped=
-namefailed=
-
-if [ $# -eq 0 ]
-then
-    tests="basics/*.py micropython/*.py float/*.py import/*.py io/*.py misc/*.py unicode/*.py extmod/*.py unix/*.py"
-else
-    tests="$@"
-fi
-
-for infile in $tests
-do
-    basename=`basename $infile .py`
-    outfile=${basename}.py.out
-    expfile=$infile.exp
-
-    $MP_PY $infile > $outfile
-    numtestcases=$(expr $numtestcases + $(cat $expfile | wc -l))
-
-    if grep -q "SKIP\|SyntaxError: invalid micropython decorator" $outfile
-    then
-        # we don't count tests that explicitly ask to be skipped
-        # we don't count tests that fail due to unsupported decorator
-        echo "skip  $infile"
-        $RM $outfile
-        numskipped=$(expr $numskipped + 1)
-        nameskipped="$nameskipped $basename"
-    else
-        diff --brief $expfile $outfile > /dev/null
-
-        if [ $? -eq 0 ]
-        then
-            echo "pass  $infile"
-            $RM $outfile
-            numpassed=$(expr $numpassed + 1)
-        else
-            echo "FAIL  $infile"
-            numfailed=$(expr $numfailed + 1)
-            namefailed="$namefailed $basename"
-        fi
-    fi
-
-    numtests=$(expr $numtests + 1)
-done
-
-echo "$numtests tests performed ($numtestcases individual testcases)"
-echo "$numpassed tests passed"
-if [ $numskipped != 0 ]
-then
-    echo "$numskipped tests skipped -$nameskipped"
-fi
-if [ $numfailed != 0 ]
-then
-    echo "$numfailed tests failed -$namefailed"
-    exit 1
-else
-    exit 0
-fi
diff --git a/tests/run-tests.py b/tests/run-tests.py
index 498db8b4041..593f82c0b6e 100755
--- a/tests/run-tests.py
+++ b/tests/run-tests.py
@@ -3,9 +3,10 @@
 import os
 import subprocess
 import sys
+import sysconfig
 import platform
 import argparse
-import inspect
+import json
 import re
 from glob import glob
 import multiprocessing
@@ -13,17 +14,29 @@
 import threading
 import tempfile
 
-# Maximum time to run a PC-based test, in seconds.
-TEST_TIMEOUT = 30
-
-# See stackoverflow.com/questions/2632199: __file__ nor sys.argv[0]
-# are guaranteed to always work, this one should though.
-BASEPATH = os.path.dirname(os.path.abspath(inspect.getsourcefile(lambda: None)))
-
-
-def base_path(*p):
-    return os.path.abspath(os.path.join(BASEPATH, *p)).replace("\\", "/")
-
+from test_utils import (
+    base_path,
+    pyboard,
+    TEST_TIMEOUT,
+    TEST_MAXIMUM_RAW_REPL_FAILURES,
+    MPYCROSS,
+    test_instance_description,
+    test_instance_epilog,
+    test_directory_description,
+    rm_f,
+    normalize_newlines,
+    set_injected_prologue,
+    get_results_filename,
+    convert_device_shortcut_to_real_device,
+    get_test_instance,
+    create_test_report,
+    FLAKY_REASON_PREFIX,
+)
+
+RV32_ARCH_FLAGS = {
+    "zba": 1 << 0,
+    "zcmp": 1 << 1,
+}
 
 # Tests require at least CPython 3.3. If your default python3 executable
 # is of lower version, you can point MICROPY_CPYTHON3 environment var
@@ -33,62 +46,280 @@ def base_path(*p):
     MICROPYTHON = os.getenv(
         "MICROPY_MICROPYTHON", base_path("../ports/windows/build-standard/micropython.exe")
     )
-    # mpy-cross is only needed if --via-mpy command-line arg is passed
-    MPYCROSS = os.getenv("MICROPY_MPYCROSS", base_path("../mpy-cross/build/mpy-cross.exe"))
 else:
     CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3")
     MICROPYTHON = os.getenv(
         "MICROPY_MICROPYTHON", base_path("../ports/unix/build-standard/micropython")
     )
-    # mpy-cross is only needed if --via-mpy command-line arg is passed
-    MPYCROSS = os.getenv("MICROPY_MPYCROSS", base_path("../mpy-cross/build/mpy-cross"))
 
 # Use CPython options to not save .pyc files, to only access the core standard library
 # (not site packages which may clash with u-module names), and improve start up time.
 CPYTHON3_CMD = [CPYTHON3, "-BS"]
 
-
 # For diff'ing test output
 DIFF = os.getenv("MICROPY_DIFF", "diff -u")
 
 # Set PYTHONIOENCODING so that CPython will use utf-8 on systems which set another encoding in the locale
 os.environ["PYTHONIOENCODING"] = "utf-8"
 
-# Code to allow a target MicroPython to import an .mpy from RAM
-injected_import_hook_code = """\
-import usys, uos, uio
-class __File(uio.IOBase):
-  def __init__(self):
-    self.off = 0
-  def ioctl(self, request, arg):
-    return 0
-  def readinto(self, buf):
-    buf[:] = memoryview(__buf)[self.off:self.off + len(buf)]
-    self.off += len(buf)
-    return len(buf)
-class __FS:
-  def mount(self, readonly, mkfs):
-    pass
-  def umount(self):
-    pass
-  def chdir(self, path):
-    pass
-  def stat(self, path):
-    if path == '__injected_test.mpy':
-      return tuple(0 for _ in range(10))
-    else:
-      raise OSError(-2) # ENOENT
-  def open(self, path, mode):
-    return __File()
-uos.mount(__FS(), '/__vfstest')
-uos.chdir('/__vfstest')
-__import__('__injected_test')
-"""
-
-
-def rm_f(fname):
-    if os.path.exists(fname):
-        os.remove(fname)
+# Platforms associated with the unix port, values of `sys.platform`.
+PC_PLATFORMS = ("darwin", "linux", "win32")
+
+# Mapping from `sys.platform` to the port name, for special cases.
+# See `platform_to_port()` function.
+platform_to_port_map = {"pyboard": "stm32", "WiPy": "cc3200"}
+platform_to_port_map.update({p: "unix" for p in PC_PLATFORMS})
+
+# Tests to skip for values of the `--via-mpy` argument.
+via_mpy_tests_to_skip = {
+    # Skip the following when mpy is enabled.
+    True: (
+        # These print out the filename and that's expected to match the .py name.
+        "import/import_file.py",
+        "io/argv.py",
+        "misc/sys_settrace_features.py",
+        "misc/sys_settrace_generator.py",
+        "misc/sys_settrace_loop.py",
+    ),
+}
+
+# Tests to skip for specific emitters.
+emitter_tests_to_skip = {
+    # Some tests are known to fail with native emitter.
+    # Remove them from the below when they work.
+    "native": (
+        # These require raise_varargs.
+        "basics/try_finally_return2.py",
+        "basics/try_reraise.py",
+        "basics/try_reraise2.py",
+        # These require checking for unbound local.
+        "basics/annotate_var.py",
+        "basics/del_deref.py",
+        "basics/del_local.py",
+        "basics/scope_implicit.py",
+        "basics/unboundlocal.py",
+        # These require "raise from".
+        "basics/exception_chain.py",
+        # These require stack-allocated slice optimisation.
+        "micropython/heapalloc_slice.py",
+        # These require implicitly running the scheduler between bytecodes.
+        "micropython/schedule.py",
+        # These require sys.exc_info().
+        "misc/sys_exc_info.py",
+        # These require sys.settrace().
+        "misc/sys_settrace_cov.py",
+        "misc/sys_settrace_features.py",
+        "misc/sys_settrace_generator.py",
+        "misc/sys_settrace_loop.py",
+        # These are bytecode-specific tests.
+        "stress/bytecode_limit.py",
+    ),
+}
+
+# Tests to skip on specific targets.
+# These are tests that are difficult to detect that they should not be run on the given target.
+platform_tests_to_skip = {
+    "esp8266": (
+        "stress/list_sort.py",  # watchdog kicks in because it takes too long
+    ),
+    "minimal": (
+        "basics/class_inplace_op.py",  # all special methods not supported
+        "basics/subclass_native_init.py",  # native subclassing corner cases not support
+        "micropython/opt_level.py",  # don't assume line numbers are stored
+    ),
+    "nrf": (
+        "basics/io_buffered_writer.py",
+        "basics/io_bytesio_cow.py",
+        "basics/io_bytesio_ext.py",
+        "basics/io_bytesio_ext2.py",
+        "basics/io_iobase.py",
+        "basics/io_stringio1.py",
+        "basics/io_stringio_base.py",
+        "basics/io_stringio_with.py",
+        "basics/io_write_ext.py",
+        "basics/memoryview1.py",  # no item assignment for memoryview
+        "extmod/random_basic.py",  # unimplemented: random.seed
+        "micropython/opt_level.py",  # no support for line numbers
+        "misc/non_compliant.py",  # no item assignment for bytearray
+    ),
+    "renesas-ra": (
+        "extmod/time_time_ns.py",  # RA fsp rtc function doesn't support nano sec info
+    ),
+    "rp2": (
+        # Skip thread tests that require more that 2 threads.
+        "thread/stress_heap.py",
+        "thread/thread_lock2.py",
+        "thread/thread_lock3.py",
+        "thread/thread_shared2.py",
+    ),
+    "webassembly": (
+        "basics/string_format_modulo.py",  # can't print nulls to stdout
+        "basics/string_strip.py",  # can't print nulls to stdout
+        "basics/weakref_callback_exception.py",  # has different exception printing output
+        "basics/weakref_ref_collect.py",  # requires custom test due to GC behaviour
+        "basics/weakref_finalize_collect.py",  # requires custom test due to GC behaviour
+        "extmod/asyncio_basic2.py",
+        "extmod/asyncio_cancel_self.py",
+        "extmod/asyncio_current_task.py",
+        "extmod/asyncio_exception.py",
+        "extmod/asyncio_gather_finished_early.py",
+        "extmod/asyncio_get_event_loop.py",
+        "extmod/asyncio_heaplock.py",
+        "extmod/asyncio_loop_stop.py",
+        "extmod/asyncio_new_event_loop.py",
+        "extmod/asyncio_threadsafeflag.py",
+        "extmod/asyncio_wait_for_fwd.py",
+        "extmod/asyncio_event_queue.py",
+        "extmod/asyncio_iterator_event.py",
+        "extmod/asyncio_wait_for_linked_task.py",
+        "extmod/binascii_a2b_base64.py",
+        "extmod/deflate_compress_memory_error.py",  # tries to allocate unlimited memory
+        "extmod/re_stack_overflow.py",
+        "extmod/re_stack_overflow2.py",
+        "extmod/time_res.py",
+        "extmod/vfs_posix.py",
+        "extmod/vfs_posix_enoent.py",
+        "extmod/vfs_posix_paths.py",
+        "extmod/vfs_userfs.py",
+        "micropython/emg_exc.py",
+        "micropython/extreme_exc.py",
+        "micropython/heapalloc_exc_compressed_emg_exc.py",
+    ),
+    "WiPy": (
+        "misc/print_exception.py",  # requires error reporting full
+    ),
+    "zephyr": (
+        # Skip thread tests that require more than 4 threads.
+        "thread/stress_heap.py",
+        "thread/thread_lock3.py",
+    ),
+}
+
+# Tests to skip when MICROPY_ERROR_REPORTING is at a certain level.
+error_reporting_tests_to_skip = {
+    # Skip at level MICROPY_ERROR_REPORTING_TERSE.
+    "terse": (
+        "cmdline/repl_paste.py",
+        # This test needs updates before being removed from this list.
+        "extmod/vfs_blockdev_invalid.py",
+        "micropython/heapalloc_exc_compressed.py",
+        "micropython/heapalloc_exc_compressed_emg_exc.py",
+        "micropython/opt_level_lineno.py",
+        "misc/print_exception.py",
+        "misc/sys_settrace_features.py",
+    ),
+}
+# Skip at level MICROPY_ERROR_REPORTING_NONE.
+error_reporting_tests_to_skip["none"] = error_reporting_tests_to_skip["terse"] + (
+    "extmod/asyncio_gather_notimpl.py",
+)
+
+# Tests with known intermittent failures. These tests still run, but failures
+# are reclassified as "ignored" instead of "fail" so they don't affect the CI
+# exit code. Paths are relative to the tests/ directory (must match test_file
+# format used by run_one_test, which normalises backslashes to forward slashes).
+#
+# Values are (reason, platforms) tuples where platforms is None (all platforms)
+# or a tuple of sys.platform strings to restrict ignoring to those platforms.
+flaky_tests_to_ignore = {
+    "thread/thread_gc1.py": ("GC race condition", None),
+    "thread/stress_schedule.py": ("intermittent crash under QEMU", None),
+    "thread/stress_recurse.py": ("stack overflow under emulation", None),
+    "thread/stress_heap.py": ("flaky on macOS", ("darwin",)),
+    "cmdline/repl_lock.py": ("REPL timing under QEMU", None),
+    "cmdline/repl_cont.py": ("REPL escaping on macOS", ("darwin",)),
+    "extmod/time_time_ns.py": ("CI runner clock precision", None),
+}
+
+# These tests don't test float explicitly but rather use it to perform the test.
+tests_requiring_float = (
+    "extmod/asyncio_basic.py",
+    "extmod/asyncio_basic2.py",
+    "extmod/asyncio_cancel_task.py",
+    "extmod/asyncio_event.py",
+    "extmod/asyncio_fair.py",
+    "extmod/asyncio_gather.py",
+    "extmod/asyncio_gather_notimpl.py",
+    "extmod/asyncio_get_event_loop.py",
+    "extmod/asyncio_iterator_event.py",
+    "extmod/asyncio_lock.py",
+    "extmod/asyncio_task_done.py",
+    "extmod/asyncio_wait_for.py",
+    "extmod/asyncio_wait_for_fwd.py",
+    "extmod/asyncio_wait_for_linked_task.py",
+    "extmod/asyncio_wait_task.py",
+    "extmod/json_dumps_float.py",
+    "extmod/json_loads_float.py",
+    "extmod/random_extra_float.py",
+    "extmod/select_poll_eintr.py",
+    "extmod/tls_threads.py",
+    "extmod/uctypes_le_float.py",
+    "extmod/uctypes_native_float.py",
+    "extmod/uctypes_sizeof_float.py",
+    "extmod/vfs_rom.py",
+    "micropython/const_float.py",
+    "misc/rge_sm.py",
+    "ports/unix/ffi_float.py",
+    "ports/unix/ffi_float2.py",
+)
+
+# These tests don't test slice explicitly but rather use it to perform the test.
+tests_requiring_slice = (
+    "basics/builtin_range.py",
+    "basics/bytearray1.py",
+    "basics/class_super.py",
+    "basics/containment.py",
+    "basics/errno1.py",
+    "basics/fun_str.py",
+    "basics/generator1.py",
+    "basics/globals_del.py",
+    "basics/memoryview1.py",
+    "basics/memoryview_gc.py",
+    "basics/object1.py",
+    "basics/python34.py",
+    "basics/struct_endian.py",
+    "extmod/btree1.py",
+    "extmod/deflate_decompress.py",
+    "extmod/framebuf16.py",
+    "extmod/framebuf4.py",
+    "extmod/machine1.py",
+    "extmod/time_mktime.py",
+    "extmod/time_res.py",
+    "extmod/tls_sslcontext_ciphers.py",
+    "extmod/vfs_blockdev_invalid2.py",
+    "extmod/vfs_fat_fileio1.py",
+    "extmod/vfs_fat_finaliser.py",
+    "extmod/vfs_fat_more.py",
+    "extmod/vfs_fat_ramdisk.py",
+    "extmod/vfs_fat_ramdisklarge.py",
+    "extmod/vfs_lfs.py",
+    "extmod/vfs_rom.py",
+    "float/string_format_modulo.py",
+    "micropython/builtin_execfile.py",
+    "micropython/extreme_exc.py",
+    "micropython/heapalloc_fail_bytearray.py",
+    "micropython/heapalloc_fail_list.py",
+    "micropython/heapalloc_fail_memoryview.py",
+    "micropython/import_mpy_invalid.py",
+    "micropython/import_mpy_native.py",
+    "micropython/import_mpy_native_gc.py",
+    "micropython/ringio_big.py",
+    "misc/non_compliant.py",
+    "misc/rge_sm.py",
+)
+
+# Tests that require `import target_wiring` to work.
+tests_requiring_target_wiring = (
+    "extmod/machine_spi_rate.py",
+    "extmod/machine_uart_irq_txidle.py",
+    "extmod/machine_uart_tx.py",
+    "extmod_hardware/machine_can_timings.py",
+    "extmod_hardware/machine_encoder.py",
+    "extmod_hardware/machine_pwm.py",
+    "extmod_hardware/machine_uart_irq_break.py",
+    "extmod_hardware/machine_uart_irq_rx.py",
+    "extmod_hardware/machine_uart_irq_rxidle.py",
+)
 
 
 # unescape wanted regex chars and escape unwanted ones
@@ -111,98 +342,158 @@ def convert_regex_escapes(line):
     return bytes("".join(cs), "utf8")
 
 
-def prepare_script_for_target(args, *, script_filename=None, script_text=None, force_plain=False):
-    if force_plain or (not args.via_mpy and args.emit == "bytecode"):
-        if script_filename is not None:
-            with open(script_filename, "rb") as f:
-                script_text = f.read()
-    elif args.via_mpy:
-        tempname = tempfile.mktemp(dir="")
-        mpy_filename = tempname + ".mpy"
-
-        if script_filename is None:
-            script_filename = tempname + ".py"
-            cleanup_script_filename = True
-            with open(script_filename, "wb") as f:
-                f.write(script_text)
-        else:
-            cleanup_script_filename = False
-
-        try:
-            subprocess.check_output(
-                [MPYCROSS]
-                + args.mpy_cross_flags.split()
-                + ["-o", mpy_filename, "-X", "emit=" + args.emit, script_filename],
-                stderr=subprocess.STDOUT,
-            )
-        except subprocess.CalledProcessError as er:
-            return True, b"mpy-cross crash\n" + er.output
+def platform_to_port(platform):
+    return platform_to_port_map.get(platform, platform)
 
-        with open(mpy_filename, "rb") as f:
-            script_text = b"__buf=" + bytes(repr(f.read()), "ascii") + b"\n"
 
-        rm_f(mpy_filename)
-        if cleanup_script_filename:
-            rm_f(script_filename)
+def detect_inline_asm_arch(pyb, args):
+    output = run_feature_check(pyb, args, "inlineasm.py").decode().strip()
+    arch, *features = output.split(",")
+    return arch, features
 
-        script_text += bytes(injected_import_hook_code, "ascii")
-    else:
-        print("error: using emit={} must go via .mpy".format(args.emit))
-        sys.exit(1)
 
-    return False, script_text
+def map_rv32_arch_flags(flags):
+    mapped_flags = []
+    for extension, bit in RV32_ARCH_FLAGS.items():
+        if flags & bit:
+            mapped_flags.append(extension)
+        flags &= ~bit
+    if flags:
+        raise Exception("Unexpected flag bits set in value {}".format(flags))
+    return mapped_flags
 
 
-def run_script_on_remote_target(pyb, args, test_file, is_special):
-    had_crash, script = prepare_script_for_target(
-        args, script_filename=test_file, force_plain=is_special
+def detect_test_platform(pyb, args):
+    # Run a script to detect various bits of information about the target test instance.
+    output = run_feature_check(pyb, args, "target_info.py")
+    if output.endswith(b"CRASH"):
+        raise ValueError("cannot detect platform: {}".format(output))
+    platform, arch, arch_flags, build, thread, float_prec, unicode, error_reporting = (
+        str(output, "ascii").strip().split()
     )
-    if had_crash:
-        return True, script
-
-    try:
-        had_crash = False
-        pyb.enter_raw_repl()
-        output_mupy = pyb.exec_(script)
-    except pyboard.PyboardError as e:
-        had_crash = True
-        if not is_special and e.args[0] == "exception":
-            output_mupy = e.args[1] + e.args[2] + b"CRASH"
-        else:
-            output_mupy = bytes(e.args[0], "ascii") + b"\nCRASH"
-    return had_crash, output_mupy
-
-
-def run_micropython(pyb, args, test_file, is_special=False):
-    special_tests = (
+    if arch == "None":
+        arch = None
+    inlineasm_arch, inlineasm_features = detect_inline_asm_arch(pyb, args)
+    if thread == "None":
+        thread = None
+    float_prec = int(float_prec)
+    unicode = unicode == "True"
+    if arch == "rv32imc":
+        arch_flags = map_rv32_arch_flags(int(arch_flags))
+    else:
+        arch_flags = None
+
+    args.platform = platform
+    args.arch = arch
+    args.arch_flags = arch_flags
+    if arch and not args.mpy_cross_flags:
+        args.mpy_cross_flags = "-march=" + arch
+        if arch_flags:
+            args.mpy_cross_flags += " -march-flags=" + ",".join(arch_flags)
+    args.inlineasm_arch = inlineasm_arch
+    args.inlineasm_features = inlineasm_features
+    args.build = build
+    args.thread = thread
+    args.float_prec = float_prec
+    args.unicode = unicode
+    args.error_reporting = error_reporting
+
+    # Print the detected information about the target.
+    print("platform={}".format(platform), end="")
+    if arch:
+        print(" arch={}".format(arch), end="")
+        if arch_flags:
+            print(" arch_flags={}".format(",".join(arch_flags)), end="")
+    if inlineasm_arch:
+        print(" inlineasm={}".format(inlineasm_arch), end="")
+    if thread:
+        print(" thread={}".format(thread), end="")
+    if float_prec:
+        print(" float={}-bit".format(float_prec), end="")
+    if unicode:
+        print(" unicode", end="")
+
+
+def detect_target_wiring_script(pyb, args):
+    tw_data = b""
+    tw_source = None
+    if args.target_wiring:
+        # A target_wiring path is explicitly provided, so use that.
+        tw_source = args.target_wiring
+        with open(tw_source, "rb") as f:
+            tw_data = f.read()
+    elif hasattr(pyb, "exec_raw") and pyb.exec_raw("import target_wiring") == (b"", b""):
+        # The board already has a target_wiring module available, so use that.
+        tw_source = "on-device"
+    else:
+        port = platform_to_port(args.platform)
+        build = args.build
+        tw_board_exact = None
+        tw_board_partial = None
+        tw_port = None
+        for file in os.listdir("target_wiring"):
+            file_base = file.removesuffix(".py")
+            if file_base == build:
+                # A file matching the target's board/build name.
+                tw_board_exact = file
+            elif file_base.endswith("x") and build.startswith(file_base.removesuffix("x")):
+                # A file with a partial match to the target's board/build name.
+                tw_board_partial = file
+            elif file_base == port:
+                # A file matching the target's port.
+                tw_port = file
+        tw_source = tw_board_exact or tw_board_partial or tw_port
+        if tw_source:
+            with open("target_wiring/" + tw_source, "rb") as f:
+                tw_data = f.read()
+    if tw_source:
+        print(" target_wiring={}".format(tw_source), end="")
+    pyb.target_wiring_script = tw_data
+
+
+tests_with_regex_output = [
+    base_path(file)
+    for file in (
         "micropython/meminfo.py",
         "basics/bytes_compare3.py",
         "basics/builtin_help.py",
+        "basics/weakref_callback_exception.py",
+        "misc/sys_settrace_cov.py",
+        "net_inet/tls_text_errors.py",
+        "ports/unix/extra_coverage.py",
         "thread/thread_exc2.py",
-        "esp32/partition_ota.py",
+        "ports/esp32/partition_ota.py",
     )
+]
+
+
+def run_micropython(
+    pyb, args, test_file, test_file_abspath, is_special=False, is_feature_check=False
+):
     had_crash = False
     if pyb is None:
         # run on PC
-        if (
-            test_file.startswith(("cmdline/", base_path("feature_check/")))
-            or test_file in special_tests
-        ):
+        if test_file_abspath.startswith((base_path("cmdline/"), base_path("feature_check/"))):
             # special handling for tests of the unix cmdline program
             is_special = True
 
         if is_special:
             # check for any cmdline options needed for this test
-            args = [MICROPYTHON]
+            cmdlist = [MICROPYTHON]
+            send_sigint = False
             with open(test_file, "rb") as f:
-                line = f.readline()
-                if line.startswith(b"# cmdline:"):
-                    # subprocess.check_output on Windows only accepts strings, not bytes
-                    args += [str(c, "utf-8") for c in line[10:].strip().split()]
+                for line in f:
+                    if line.startswith(b"# cmdline:"):
+                        # subprocess.check_output on Windows only accepts strings, not bytes
+                        cmdlist += [str(c, "utf-8") for c in line[10:].strip().split()]
+                    elif line.startswith(b"# sigint:"):
+                        send_sigint = True
+                    elif not line.startswith(b"#"):
+                        break
 
             # run the test, possibly with redirected input
             try:
-                if "repl_" in test_file:
+                if os.path.basename(test_file).startswith("repl_"):
                     # Need to use a PTY to test command line editing
                     try:
                         import pty
@@ -226,33 +517,86 @@ def get(required=False):
                                     return rv
 
                     def send_get(what):
+                        # Detect hex {\x00} pattern and convert to ctrl-key codes.
+                        ctrl_code = lambda m: bytes([int(m.group(1), 16)])
+                        what = re.sub(rb"{\\x(\d\d)}", ctrl_code, what)
+
                         os.write(master, what)
                         return get()
 
+                    def send_ctrl_c():
+                        # Send \x03 without trailing newline and wait for
+                        # the full response (traceback + new prompt).
+                        os.write(master, b"\x03")
+                        return get(True)
+
                     with open(test_file, "rb") as f:
-                        # instead of: output_mupy = subprocess.check_output(args, stdin=f)
+                        # instead of: output_mupy = subprocess.check_output(cmdlist, stdin=f)
                         master, slave = pty.openpty()
-                        p = subprocess.Popen(
-                            args, stdin=slave, stdout=slave, stderr=subprocess.STDOUT, bufsize=0
-                        )
-                        banner = get(True)
-                        output_mupy = banner + b"".join(send_get(line) for line in f)
-                        send_get(b"\x04")  # exit the REPL, so coverage info is saved
-                        # At this point the process might have exited already, but trying to
-                        # kill it 'again' normally doesn't result in exceptions as Python and/or
-                        # the OS seem to try to handle this nicely. When running Linux on WSL
-                        # though, the situation differs and calling Popen.kill after the process
-                        # terminated results in a ProcessLookupError. Just catch that one here
-                        # since we just want the process to be gone and that's the case.
                         try:
-                            p.kill()
-                        except ProcessLookupError:
-                            pass
-                        os.close(master)
-                        os.close(slave)
+                            preexec_fn = None
+                            use_sigint_kill = False
+                            # Tests with "# sigint:" need Ctrl-C (\x03) to
+                            # generate SIGINT. MicroPython restores original
+                            # terminal mode (ISIG on) during code execution,
+                            # so on Linux we set up the PTY as a controlling
+                            # terminal for proper signal delivery. On macOS,
+                            # setsid/TIOCSCTTY breaks PTY I/O, so we fall
+                            # back to os.kill().
+                            if send_sigint:
+                                if sys.platform == "darwin":
+                                    use_sigint_kill = True
+                                else:
+                                    import fcntl
+                                    import termios
+
+                                    def preexec_fn():
+                                        os.setsid()
+                                        fcntl.ioctl(0, termios.TIOCSCTTY, 0)
+                                        os.tcsetpgrp(0, os.getpid())
+
+                            p = subprocess.Popen(
+                                cmdlist,
+                                stdin=slave,
+                                stdout=slave,
+                                stderr=subprocess.STDOUT,
+                                bufsize=0,
+                                preexec_fn=preexec_fn,
+                            )
+                            banner = get(True)
+                            if send_sigint:
+                                import signal
+
+                                parts = []
+                                for line in f:
+                                    if b"{\\x03}" in line:
+                                        if use_sigint_kill:
+                                            os.kill(p.pid, signal.SIGINT)
+                                            parts.append(get(True))
+                                        else:
+                                            parts.append(send_ctrl_c())
+                                    else:
+                                        parts.append(send_get(line))
+                                output_mupy = banner + b"".join(parts)
+                            else:
+                                output_mupy = banner + b"".join(send_get(line) for line in f)
+                            send_get(b"\x04")  # exit the REPL, so coverage info is saved
+                            # At this point the process might have exited already, but trying to
+                            # kill it 'again' normally doesn't result in exceptions as Python and/or
+                            # the OS seem to try to handle this nicely. When running Linux on WSL
+                            # though, the situation differs and calling Popen.kill after the process
+                            # terminated results in a ProcessLookupError. Just catch that one here
+                            # since we just want the process to be gone and that's the case.
+                            try:
+                                p.kill()
+                            except ProcessLookupError:
+                                pass
+                        finally:
+                            os.close(master)
+                            os.close(slave)
                 else:
                     output_mupy = subprocess.check_output(
-                        args + [test_file], stderr=subprocess.STDOUT
+                        cmdlist + [test_file], stderr=subprocess.STDOUT
                     )
             except subprocess.CalledProcessError:
                 return b"CRASH"
@@ -280,7 +624,7 @@ def send_get(what):
                 mpy_modname = os.path.splitext(os.path.basename(mpy_filename))[0]
                 cmdlist.extend(["-m", mpy_modname])
             else:
-                cmdlist.append(os.path.abspath(test_file))
+                cmdlist.append(test_file_abspath)
 
             # run the actual test
             try:
@@ -300,22 +644,34 @@ def send_get(what):
 
     else:
         # run via pyboard interface
-        had_crash, output_mupy = run_script_on_remote_target(pyb, args, test_file, is_special)
+        requires_target_wiring = test_file.endswith(tests_requiring_target_wiring)
+        had_crash, output_mupy = pyb.run_script_on_remote_target(
+            args, test_file_abspath, is_special, requires_target_wiring
+        )
 
     # canonical form for all ports/platforms is to use \n for end-of-line
-    output_mupy = output_mupy.replace(b"\r\n", b"\n")
+    output_mupy = normalize_newlines(output_mupy)
+
+    # for feature-check tests, return the output as-is
+    if is_feature_check:
+        return output_mupy
 
     # don't try to convert the output if we should skip this test
-    if had_crash or output_mupy in (b"SKIP\n", b"CRASH"):
+    if had_crash or output_mupy in (b"SKIP\n", b"SKIP-TOO-LARGE\n", b"CRASH"):
         return output_mupy
 
     # skipped special tests will output "SKIP" surrounded by other interpreter debug output
     if is_special and not had_crash and b"\nSKIP\n" in output_mupy:
         return b"SKIP\n"
 
-    if is_special or test_file in special_tests:
+    if is_special or test_file_abspath in tests_with_regex_output:
         # convert parts of the output that are not stable across runs
-        with open(test_file + ".exp", "rb") as f:
+        # Prefer emitter-specific expected output.
+        exp_file = test_file + "." + args.emit + ".exp"
+        if not os.path.isfile(exp_file):
+            # Fall back to generic expected output.
+            exp_file = test_file + ".exp"
+        with open(exp_file, "rb") as f:
             lines_exp = []
             for line in f.readlines():
                 if line == b"########\n":
@@ -357,11 +713,18 @@ def send_get(what):
     return output_mupy
 
 
-def run_feature_check(pyb, args, base_path, test_file):
+def run_feature_check(pyb, args, test_file):
     if pyb is not None and test_file.startswith("repl_"):
         # REPL feature tests will not run via pyboard because they require prompt interactivity
         return b""
-    return run_micropython(pyb, args, base_path("feature_check", test_file), is_special=True)
+    test_file_path = base_path("feature_check", test_file)
+    return run_micropython(
+        pyb, args, test_file_path, test_file_path, is_special=True, is_feature_check=True
+    )
+
+
+class TestError(Exception):
+    pass
 
 
 class ThreadSafeCounter:
@@ -385,155 +748,155 @@ def value(self):
 
 
 def run_tests(pyb, tests, args, result_dir, num_threads=1):
-    test_count = ThreadSafeCounter()
     testcase_count = ThreadSafeCounter()
-    passed_count = ThreadSafeCounter()
-    failed_tests = ThreadSafeCounter([])
-    skipped_tests = ThreadSafeCounter([])
+    raw_repl_failure_count = ThreadSafeCounter()
+    test_results = ThreadSafeCounter([])
 
     skip_tests = set()
     skip_native = False
     skip_int_big = False
+    skip_int_64 = False
     skip_bytearray = False
     skip_set_type = False
     skip_slice = False
     skip_async = False
     skip_const = False
     skip_revops = False
-    skip_io_module = False
     skip_fstring = False
+    skip_tstring = False
     skip_endian = False
+    skip_inlineasm = False
     has_complex = True
     has_coverage = False
 
-    upy_float_precision = 32
-
-    # If we're asked to --list-tests, we can't assume that there's a
-    # connection to target, so we can't run feature checks usefully.
-    if not (args.list_tests or args.write_exp):
+    if True:
         # Even if we run completely different tests in a different directory,
         # we need to access feature_checks from the same directory as the
         # run-tests.py script itself so use base_path.
 
         # Check if micropython.native is supported, and skip such tests if it's not
-        output = run_feature_check(pyb, args, base_path, "native_check.py")
+        output = run_feature_check(pyb, args, "native_check.py")
         if output != b"native\n":
             skip_native = True
 
         # Check if arbitrary-precision integers are supported, and skip such tests if it's not
-        output = run_feature_check(pyb, args, base_path, "int_big.py")
+        output = run_feature_check(pyb, args, "int_big.py")
         if output != b"1000000000000000000000000000000000000000000000\n":
             skip_int_big = True
 
+        # Check if 'long long' precision integers are supported, even if arbitrary precision is not
+        output = run_feature_check(pyb, args, "int_64.py")
+        if output != b"4611686018427387904\n":
+            skip_int_64 = True
+
         # Check if bytearray is supported, and skip such tests if it's not
-        output = run_feature_check(pyb, args, base_path, "bytearray.py")
+        output = run_feature_check(pyb, args, "bytearray.py")
         if output != b"bytearray\n":
             skip_bytearray = True
 
         # Check if set type (and set literals) is supported, and skip such tests if it's not
-        output = run_feature_check(pyb, args, base_path, "set_check.py")
+        output = run_feature_check(pyb, args, "set_check.py")
         if output != b"{1}\n":
             skip_set_type = True
 
         # Check if slice is supported, and skip such tests if it's not
-        output = run_feature_check(pyb, args, base_path, "slice.py")
+        output = run_feature_check(pyb, args, "slice.py")
         if output != b"slice\n":
             skip_slice = True
 
         # Check if async/await keywords are supported, and skip such tests if it's not
-        output = run_feature_check(pyb, args, base_path, "async_check.py")
+        output = run_feature_check(pyb, args, "async_check.py")
         if output != b"async\n":
             skip_async = True
 
         # Check if const keyword (MicroPython extension) is supported, and skip such tests if it's not
-        output = run_feature_check(pyb, args, base_path, "const.py")
+        output = run_feature_check(pyb, args, "const.py")
         if output != b"1\n":
             skip_const = True
 
         # Check if __rOP__ special methods are supported, and skip such tests if it's not
-        output = run_feature_check(pyb, args, base_path, "reverse_ops.py")
+        output = run_feature_check(pyb, args, "reverse_ops.py")
         if output == b"TypeError\n":
             skip_revops = True
 
-        # Check if uio module exists, and skip such tests if it doesn't
-        output = run_feature_check(pyb, args, base_path, "uio_module.py")
-        if output != b"uio\n":
-            skip_io_module = True
-
         # Check if fstring feature is enabled, and skip such tests if it doesn't
-        output = run_feature_check(pyb, args, base_path, "fstring.py")
+        output = run_feature_check(pyb, args, "fstring.py")
         if output != b"a=1\n":
             skip_fstring = True
 
+        # Check if tstring feature is enabled, and skip such tests if it doesn't
+        output = run_feature_check(pyb, args, "tstring.py")
+        if output != b"tstring\n":
+            skip_tstring = True
+
+        if args.inlineasm_arch == "thumb":
+            for feature in ("thumb2", "vfp"):
+                if feature not in args.inlineasm_features:
+                    for test in glob(f"inlineasm/thumb/asm_{feature}_*.py"):
+                        skip_tests.add(test)
+
+        if args.inlineasm_arch == "rv32":
+            for extension in RV32_ARCH_FLAGS:
+                if extension not in args.inlineasm_features:
+                    skip_tests.add(f"inlineasm/rv32/asm_ext_{extension}.py")
+
         # Check if emacs repl is supported, and skip such tests if it's not
-        t = run_feature_check(pyb, args, base_path, "repl_emacs_check.py")
+        t = run_feature_check(pyb, args, "repl_emacs_check.py")
         if "True" not in str(t, "ascii"):
             skip_tests.add("cmdline/repl_emacs_keys.py")
 
         # Check if words movement in repl is supported, and skip such tests if it's not
-        t = run_feature_check(pyb, args, base_path, "repl_words_move_check.py")
+        t = run_feature_check(pyb, args, "repl_words_move_check.py")
         if "True" not in str(t, "ascii"):
             skip_tests.add("cmdline/repl_words_move.py")
 
-        upy_byteorder = run_feature_check(pyb, args, base_path, "byteorder.py")
-        upy_float_precision = run_feature_check(pyb, args, base_path, "float.py")
-        try:
-            upy_float_precision = int(upy_float_precision)
-        except ValueError:
-            upy_float_precision = 0
-        has_complex = run_feature_check(pyb, args, base_path, "complex.py") == b"complex\n"
-        has_coverage = run_feature_check(pyb, args, base_path, "coverage.py") == b"coverage\n"
+        upy_byteorder = run_feature_check(pyb, args, "byteorder.py")
+        has_complex = run_feature_check(pyb, args, "complex.py") == b"complex\n"
+        has_coverage = run_feature_check(pyb, args, "coverage.py") == b"coverage\n"
         cpy_byteorder = subprocess.check_output(
             CPYTHON3_CMD + [base_path("feature_check/byteorder.py")]
         )
         skip_endian = upy_byteorder != cpy_byteorder
 
-    # These tests don't test slice explicitly but rather use it to perform the test
-    misc_slice_tests = (
-        "builtin_range",
-        "class_super",
-        "containment",
-        "errno1",
-        "fun_str",
-        "generator1",
-        "globals_del",
-        "memoryview1",
-        "memoryview_gc",
-        "object1",
-        "python34",
-        "struct_endian",
-    )
+        skip_inlineasm = not args.inlineasm_arch
 
     # Some tests shouldn't be run on GitHub Actions
     if os.getenv("GITHUB_ACTIONS") == "true":
-        skip_tests.add("thread/stress_schedule.py")  # has reliability issues
-
-    if upy_float_precision == 0:
-        skip_tests.add("extmod/uctypes_le_float.py")
-        skip_tests.add("extmod/uctypes_native_float.py")
-        skip_tests.add("extmod/uctypes_sizeof_float.py")
-        skip_tests.add("extmod/ujson_dumps_float.py")
-        skip_tests.add("extmod/ujson_loads_float.py")
-        skip_tests.add("extmod/urandom_extra_float.py")
-        skip_tests.add("misc/rge_sm.py")
-    if upy_float_precision < 32:
+        if os.getenv("RUNNER_OS") == "Windows" and os.getenv("CI_BUILD_CONFIGURATION") == "Debug":
+            # fails with stack overflow on Debug builds
+            skip_tests.add("misc/sys_settrace_features.py")
+
+    if args.float_prec == 0:
+        skip_tests.update(tests_requiring_float)
+    if args.float_prec < 32:
         skip_tests.add(
             "float/float2int_intbig.py"
         )  # requires fp32, there's float2int_fp30_intbig.py instead
         skip_tests.add(
-            "float/string_format.py"
-        )  # requires fp32, there's string_format_fp30.py instead
+            "float/float_struct_e.py"
+        )  # requires fp32, there's float_struct_e_fp30.py instead
         skip_tests.add("float/bytes_construct.py")  # requires fp32
         skip_tests.add("float/bytearray_construct.py")  # requires fp32
-    if upy_float_precision < 64:
+        skip_tests.add("float/float_format_ints_power10.py")  # requires fp32
+    if args.float_prec < 64:
         skip_tests.add("float/float_divmod.py")  # tested by float/float_divmod_relaxed.py instead
         skip_tests.add("float/float2int_doubleprec_intbig.py")
+        skip_tests.add("float/float_struct_e_doubleprec.py")
         skip_tests.add("float/float_format_ints_doubleprec.py")
         skip_tests.add("float/float_parse_doubleprec.py")
 
+    if not args.unicode:
+        if args.via_mpy:
+            skip_tests.add("basics/string_escape.py")  # stores a utf-8 character in the mpy file
+        skip_tests.add("extmod/json_loads.py")  # tests loading a utf-8 character
+
+    if skip_slice:
+        skip_tests.update(tests_requiring_slice)
+
     if not has_complex:
         skip_tests.add("float/complex1.py")
         skip_tests.add("float/complex1_intbig.py")
+        skip_tests.add("float/complex1_micropython.py")
         skip_tests.add("float/complex_reverse_op.py")
         skip_tests.add("float/complex_special_methods.py")
         skip_tests.add("float/int_big_float.py")
@@ -544,56 +907,33 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1):
     if not has_coverage:
         skip_tests.add("cmdline/cmd_parsetree.py")
         skip_tests.add("cmdline/repl_sys_ps1_ps2.py")
-        skip_tests.add("extmod/ussl_poll.py")
+        skip_tests.add("extmod/ssl_poll.py")
 
-    # Some tests shouldn't be run on a PC
-    if args.target == "unix":
-        # unix build does not have the GIL so can't run thread mutation tests
+    # Skip thread mutation tests on targets that have unsafe threading behaviour.
+    if args.thread == "unsafe":
         for t in tests:
             if t.startswith("thread/mutate_"):
                 skip_tests.add(t)
 
     # Some tests shouldn't be run on pyboard
-    if args.target != "unix":
+    if args.platform not in PC_PLATFORMS:
         skip_tests.add("basics/exception_chain.py")  # warning is not printed
         skip_tests.add("micropython/meminfo.py")  # output is very different to PC output
-        skip_tests.add("extmod/machine_mem.py")  # raw memory access not supported
-
-        if args.target == "wipy":
-            skip_tests.add("misc/print_exception.py")  # requires error reporting full
-            skip_tests.update(
-                {
-                    "extmod/uctypes_%s.py" % t
-                    for t in "bytearray le native_le ptr_le ptr_native_le sizeof sizeof_native array_assign_le array_assign_native_le".split()
-                }
-            )  # requires uctypes
-            skip_tests.add("extmod/zlibd_decompress.py")  # requires zlib
-            skip_tests.add("extmod/uheapq1.py")  # uheapq not supported by WiPy
-            skip_tests.add("extmod/urandom_basic.py")  # requires urandom
-            skip_tests.add("extmod/urandom_extra.py")  # requires urandom
-        elif args.target == "esp8266":
-            skip_tests.add("misc/rge_sm.py")  # too large
-        elif args.target == "minimal":
-            skip_tests.add("basics/class_inplace_op.py")  # all special methods not supported
-            skip_tests.add(
-                "basics/subclass_native_init.py"
-            )  # native subclassing corner cases not support
-            skip_tests.add("misc/rge_sm.py")  # too large
-            skip_tests.add("micropython/opt_level.py")  # don't assume line numbers are stored
-        elif args.target == "nrf":
-            skip_tests.add("basics/memoryview1.py")  # no item assignment for memoryview
-            skip_tests.add("extmod/urandom_basic.py")  # unimplemented: urandom.seed
-            skip_tests.add("micropython/opt_level.py")  # no support for line numbers
-            skip_tests.add("misc/non_compliant.py")  # no item assignment for bytearray
-            for t in tests:
-                if t.startswith("basics/io_"):
-                    skip_tests.add(t)
-        elif args.target == "renesas-ra":
-            skip_tests.add(
-                "extmod/utime_time_ns.py"
-            )  # RA fsp rtc function doesn't support nano sec info
-        elif args.target == "qemu-arm":
-            skip_tests.add("misc/print_exception.py")  # requires sys stdfiles
+        skip_tests.add("unicode/file1.py")  # requires local file access
+        skip_tests.add("unicode/file2.py")  # requires local file access
+        skip_tests.add("unicode/file_invalid.py")  # requires local file access
+
+    # Skip certain tests when going via a .mpy file.
+    skip_tests.update(via_mpy_tests_to_skip.get(args.via_mpy, ()))
+
+    # Skip emitter-specific tests.
+    skip_tests.update(emitter_tests_to_skip.get(args.emit, ()))
+
+    # Skip platform-specific tests.
+    skip_tests.update(platform_tests_to_skip.get(args.platform, ()))
+
+    # Skip error-reporting-specific tests.
+    skip_tests.update(error_reporting_tests_to_skip.get(args.error_reporting, ()))
 
     # Some tests are known to fail on 64-bit machines
     if pyb is None and platform.architecture()[0] == "64bit":
@@ -601,58 +941,30 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1):
 
     # Some tests use unsupported features on Windows
     if os.name == "nt":
-        skip_tests.add("import/import_file.py")  # works but CPython prints forward slashes
-
-    # Some tests are known to fail with native emitter
-    # Remove them from the below when they work
-    if args.emit == "native":
-        skip_tests.update(
-            {"basics/%s.py" % t for t in "gen_yield_from_close generator_name".split()}
-        )  # require raise_varargs, generator name
-        skip_tests.update(
-            {"basics/async_%s.py" % t for t in "with with2 with_break with_return".split()}
-        )  # require async_with
-        skip_tests.update(
-            {"basics/%s.py" % t for t in "try_reraise try_reraise2".split()}
-        )  # require raise_varargs
-        skip_tests.add("basics/annotate_var.py")  # requires checking for unbound local
-        skip_tests.add("basics/del_deref.py")  # requires checking for unbound local
-        skip_tests.add("basics/del_local.py")  # requires checking for unbound local
-        skip_tests.add("basics/exception_chain.py")  # raise from is not supported
-        skip_tests.add("basics/fun_name.py")  # requires proper names for native functions
-        skip_tests.add("basics/scope_implicit.py")  # requires checking for unbound local
-        skip_tests.add("basics/sys_tracebacklimit.py")  # requires traceback info
-        skip_tests.add("basics/try_finally_return2.py")  # requires raise_varargs
-        skip_tests.add("basics/unboundlocal.py")  # requires checking for unbound local
-        skip_tests.add("extmod/uasyncio_event.py")  # unknown issue
-        skip_tests.add("extmod/uasyncio_lock.py")  # requires async with
-        skip_tests.add("extmod/uasyncio_micropython.py")  # unknown issue
-        skip_tests.add("extmod/uasyncio_wait_for.py")  # unknown issue
-        skip_tests.add("misc/features.py")  # requires raise_varargs
-        skip_tests.add(
-            "misc/print_exception.py"
-        )  # because native doesn't have proper traceback info
-        skip_tests.add("misc/sys_exc_info.py")  # sys.exc_info() is not supported for native
-        skip_tests.add("misc/sys_settrace_features.py")  # sys.settrace() not supported
-        skip_tests.add("misc/sys_settrace_generator.py")  # sys.settrace() not supported
-        skip_tests.add("misc/sys_settrace_loop.py")  # sys.settrace() not supported
-        skip_tests.add(
-            "micropython/emg_exc.py"
-        )  # because native doesn't have proper traceback info
-        skip_tests.add(
-            "micropython/heapalloc_traceback.py"
-        )  # because native doesn't have proper traceback info
-        skip_tests.add(
-            "micropython/opt_level_lineno.py"
-        )  # native doesn't have proper traceback info
-        skip_tests.add("micropython/schedule.py")  # native code doesn't check pending events
-        skip_tests.add("stress/bytecode_limit.py")  # bytecode specific test
+        if not sysconfig.get_platform().startswith("mingw"):
+            # Works but CPython uses '\' path separator
+            skip_tests.add("import/import_file.py")
+
+    skip_tests = [os.path.realpath(base_path(skip_test)) for skip_test in skip_tests]
 
     def run_one_test(test_file):
+        test_file_abspath = os.path.abspath(test_file).replace("\\", "/")
+        # If test_file is one of our own tests always make it relative to our tests/ dir and
+        # otherwise use the absolute path, regardless of actual path passed,
+        # such that display and result output is always the same.
+        try:
+            test_file_relpath = os.path.relpath(test_file, start=base_path())
+            if not test_file_relpath.startswith(".."):
+                test_file = test_file_relpath
+            else:
+                test_file = test_file_abspath
+        except ValueError:
+            # Path on different drive on Windows.
+            test_file = test_file_abspath
         test_file = test_file.replace("\\", "/")
 
         if args.filters:
-            # Default verdict is the opposit of the first action
+            # Default verdict is the opposite of the first action
             verdict = "include" if args.filters[0][0] == "exclude" else "exclude"
             for action, pat in args.filters:
                 if pat.search(test_file):
@@ -662,126 +974,207 @@ def run_one_test(test_file):
 
         test_basename = test_file.replace("..", "_").replace("./", "").replace("/", "_")
         test_name = os.path.splitext(os.path.basename(test_file))[0]
-        is_native = (
-            test_name.startswith("native_")
-            or test_name.startswith("viper_")
-            or args.emit == "native"
-        )
+        is_native = test_name.startswith("native_") or test_name.startswith("viper_")
         is_endian = test_name.endswith("_endian")
-        is_int_big = test_name.startswith("int_big") or test_name.endswith("_intbig")
+        is_int_big = (
+            test_name.startswith("int_big")
+            or test_name.endswith("_intbig")
+            or test_name.startswith("ffi_int")  # these tests contain large integer literals
+        )
+        is_int_64 = test_name.startswith("int_64") or test_name.endswith("_int64")
         is_bytearray = test_name.startswith("bytearray") or test_name.endswith("_bytearray")
         is_set_type = test_name.startswith(("set_", "frozenset")) or test_name.endswith("_set")
-        is_slice = test_name.find("slice") != -1 or test_name in misc_slice_tests
-        is_async = test_name.startswith(("async_", "uasyncio_"))
+        is_slice = test_name.find("slice") != -1
+        is_async = test_name.startswith(("async_", "asyncio_")) or test_name.endswith("_async")
         is_const = test_name.startswith("const")
-        is_io_module = test_name.startswith("io_")
-        is_fstring = test_name.startswith("string_fstring")
+        is_fstring = test_name.startswith("string_fstring") or test_name.endswith("_fstring")
+        is_tstring = test_name.startswith("string_tstring") or test_name.endswith("_tstring")
+        is_inlineasm = test_name.startswith("asm")
 
-        skip_it = test_file in skip_tests
+        skip_it = os.path.realpath(test_file) in skip_tests
         skip_it |= skip_native and is_native
         skip_it |= skip_endian and is_endian
         skip_it |= skip_int_big and is_int_big
+        skip_it |= skip_int_64 and is_int_64
         skip_it |= skip_bytearray and is_bytearray
         skip_it |= skip_set_type and is_set_type
         skip_it |= skip_slice and is_slice
         skip_it |= skip_async and is_async
         skip_it |= skip_const and is_const
         skip_it |= skip_revops and "reverse_op" in test_name
-        skip_it |= skip_io_module and is_io_module
         skip_it |= skip_fstring and is_fstring
-
-        if args.list_tests:
-            if not skip_it:
-                print(test_file)
-            return
+        skip_it |= skip_tstring and is_tstring
+        skip_it |= skip_inlineasm and is_inlineasm
 
         if skip_it:
             print("skip ", test_file)
-            skipped_tests.append(test_name)
+            test_results.append((test_file, "skip", ""))
             return
-
-        # get expected output
-        test_file_expected = test_file + ".exp"
-        if os.path.isfile(test_file_expected):
-            # expected output given by a file, so read that in
-            with open(test_file_expected, "rb") as f:
-                output_expected = f.read()
-        else:
-            # run CPython to work out expected output
-            try:
-                output_expected = subprocess.check_output(
-                    CPYTHON3_CMD + [os.path.abspath(test_file)], cwd=os.path.dirname(test_file)
-                )
-                if args.write_exp:
-                    with open(test_file_expected, "wb") as f:
-                        f.write(output_expected)
-            except subprocess.CalledProcessError:
-                output_expected = b"CPYTHON3 CRASH"
-
-        # canonical form for all host platforms is to use \n for end-of-line
-        output_expected = output_expected.replace(b"\r\n", b"\n")
-
-        if args.write_exp:
+        elif args.dry_run:
+            print("found", test_file)
+            test_results.append((test_file, "found", ""))
             return
 
-        # run MicroPython
-        output_mupy = run_micropython(pyb, args, test_file)
+        # Run the test on the MicroPython target.
+        output_mupy = run_micropython(pyb, args, test_file, test_file_abspath)
 
+        # Check if the target requested to skip this test.
         if output_mupy == b"SKIP\n":
+            if pyb is not None and hasattr(pyb, "read_until"):
+                # Running on a target over a serial connection, and the target requested
+                # to skip the test.  It does this via a SystemExit which triggers a soft
+                # reset.  Wait for the soft reset to finish, so we don't interrupt the
+                # start-up code (eg boot.py) when preparing to run the next test.
+                pyb.read_until(1, b"raw REPL; CTRL-B to exit\r\n")
             print("skip ", test_file)
-            skipped_tests.append(test_name)
+            test_results.append((test_file, "skip", ""))
             return
+        elif output_mupy == b"SKIP-TOO-LARGE\n":
+            print("lrge ", test_file)
+            test_results.append((test_file, "skip", "too large"))
+            return
+
+        # Look at the output of the test to see if unittest was used.
+        uses_unittest = False
+        output_mupy_lines = output_mupy.splitlines()
+        if any(
+            line == b"ImportError: no module named 'unittest'" for line in output_mupy_lines[-3:]
+        ):
+            raise TestError(
+                (
+                    "error: test {} requires unittest".format(test_file),
+                    "(eg run `mpremote mip install unittest` to install it)",
+                )
+            )
+        elif (
+            len(output_mupy_lines) > 4
+            and output_mupy_lines[-4] == b"-" * 70
+            and output_mupy_lines[-2] == b""
+        ):
+            # look for unittest summary
+            unittest_ran_match = re.match(rb"Ran (\d+) tests$", output_mupy_lines[-3])
+            unittest_result_match = re.match(
+                b"("
+                rb"(OK)( \(skipped=(\d+)\))?"
+                b"|"
+                rb"(FAILED) \(failures=(\d+), errors=(\d+)\)"
+                b")$",
+                output_mupy_lines[-1],
+            )
+            uses_unittest = unittest_ran_match and unittest_result_match
 
-        testcase_count.add(len(output_expected.splitlines()))
+        # Determine the expected output.
+        if uses_unittest:
+            # Expected output is result of running unittest.
+            output_expected = None
+        else:
+            # Prefer emitter-specific expected output.
+            test_file_expected = test_file + "." + args.emit + ".exp"
+            if not os.path.isfile(test_file_expected):
+                # Fall back to generic expected output.
+                test_file_expected = test_file + ".exp"
+            if os.path.isfile(test_file_expected):
+                # Expected output given by a file, so read that in.
+                with open(test_file_expected, "rb") as f:
+                    output_expected = f.read()
+            else:
+                # Run CPython to work out expected output.
+                try:
+                    output_expected = subprocess.check_output(
+                        CPYTHON3_CMD + [test_file_abspath],
+                        cwd=os.path.dirname(test_file),
+                        stderr=subprocess.STDOUT,
+                    )
+                except subprocess.CalledProcessError as er:
+                    output_expected = b"CPYTHON3 CRASH:\n" + er.output
+
+            # Canonical form for all host platforms is to use \n for end-of-line.
+            output_expected = output_expected.replace(b"\r\n", b"\n")
+
+        # Work out if test passed or not.
+        test_passed = False
+        extra_info = ""
+        if uses_unittest:
+            test_passed = unittest_result_match.group(2) == b"OK"
+            num_test_cases = int(unittest_ran_match.group(1))
+            extra_info = "unittest: {} ran".format(num_test_cases)
+            if test_passed and unittest_result_match.group(4) is not None:
+                num_skipped = int(unittest_result_match.group(4))
+                num_test_cases -= num_skipped
+                extra_info += ", {} skipped".format(num_skipped)
+            elif not test_passed:
+                num_failures = int(unittest_result_match.group(6))
+                num_errors = int(unittest_result_match.group(7))
+                extra_info += ", {} failures, {} errors".format(num_failures, num_errors)
+            extra_info = "(" + extra_info + ")"
+            testcase_count.add(num_test_cases)
+        else:
+            testcase_count.add(len(output_expected.splitlines()))
+            test_passed = output_expected == output_mupy
 
         filename_expected = os.path.join(result_dir, test_basename + ".exp")
         filename_mupy = os.path.join(result_dir, test_basename + ".out")
 
-        if output_expected == output_mupy:
-            print("pass ", test_file)
-            passed_count.increment()
+        # Print test summary, update counters, and save .exp/.out files if needed.
+        if test_passed:
+            print("pass ", test_file, extra_info)
+            test_results.append((test_file, "pass", ""))
             rm_f(filename_expected)
             rm_f(filename_mupy)
         else:
-            with open(filename_expected, "wb") as f:
-                f.write(output_expected)
+            if output_mupy.startswith(b"could not enter raw repl"):
+                extra_info = "raw REPL failed"
+                raw_repl_failure_count.increment()
+            print("FAIL ", test_file, extra_info)
+            if output_expected is not None:
+                with open(filename_expected, "wb") as f:
+                    f.write(output_expected)
+            else:
+                rm_f(filename_expected)  # in case left over from previous failed run
             with open(filename_mupy, "wb") as f:
                 f.write(output_mupy)
-            print("FAIL ", test_file)
-            failed_tests.append(test_name)
-
-        test_count.increment()
+            test_results.append((test_file, "fail", ""))
+
+        # Print a note if this looks like it might have been a misfired unittest
+        if not uses_unittest and not test_passed:
+            with open(test_file, "r", encoding="utf-8") as f:
+                if any(re.match("^import.+unittest", l) for l in f.readlines()):
+                    print(
+                        "NOTE: {} may be a unittest that doesn't run unittest.main()".format(
+                            test_file
+                        )
+                    )
 
-    if pyb or args.list_tests:
+    if pyb:
         num_threads = 1
 
-    if num_threads > 1:
-        pool = ThreadPool(num_threads)
-        pool.map(run_one_test, tests)
-    else:
-        for test in tests:
-            run_one_test(test)
-
-    if args.list_tests:
-        return True
-
-    print(
-        "{} tests performed ({} individual testcases)".format(
-            test_count.value, testcase_count.value
-        )
-    )
-    print("{} tests passed".format(passed_count.value))
+    try:
+        if num_threads > 1:
+            pool = ThreadPool(num_threads)
+            pool.map(run_one_test, tests)
+        else:
+            for test in tests:
+                run_one_test(test)
+                if raw_repl_failure_count.value > TEST_MAXIMUM_RAW_REPL_FAILURES:
+                    print("Too many raw REPL failures, aborting test run")
+                    break
+    except TestError as er:
+        for line in er.args[0]:
+            print(line)
+        sys.exit(2)
 
-    skipped_tests = sorted(skipped_tests.value)
-    if len(skipped_tests) > 0:
-        print("{} tests skipped: {}".format(len(skipped_tests), " ".join(skipped_tests)))
-    failed_tests = sorted(failed_tests.value)
-    if len(failed_tests) > 0:
-        print("{} tests failed: {}".format(len(failed_tests), " ".join(failed_tests)))
-        return False
+    # Reclassify known-flaky test failures as ignored.
+    # Safe to mutate: thread pool has joined.
+    results = test_results.value
+    for i, r in enumerate(results):
+        if r[1] == "fail":
+            reason, platforms = flaky_tests_to_ignore.get(r[0], (None, None))
+            if reason is not None:
+                if platforms is None or sys.platform in platforms:
+                    results[i] = (r[0], "ignored", "{}: {}".format(FLAKY_REASON_PREFIX, reason))
 
-    # all tests succeeded
-    return True
+    # Return test results.
+    return test_results.value, testcase_count.value
 
 
 class append_filter(argparse.Action):
@@ -801,19 +1194,20 @@ def __call__(self, parser, args, value, option):
 def main():
     cmd_parser = argparse.ArgumentParser(
         formatter_class=argparse.RawDescriptionHelpFormatter,
-        description="""Run and manage tests for MicroPython.
+        description=f"""Run and manage tests for MicroPython.
+
+{test_instance_description}
+{test_directory_description}
 
-Tests are discovered by scanning test directories for .py files or using the
-specified test files. If test files nor directories are specified, the script
-expects to be ran in the tests directory (where this file is located) and the
-builtin tests suitable for the target platform are ran.
 When running tests, run-tests.py compares the MicroPython output of the test with the output
-produced by running the test through CPython unless a .exp file is found, in which
-case it is used as comparison.
+produced by running the test through CPython unless a .exp file is found (or a
+.native.exp file when using the native emitter), in which case it is used as comparison.
+
 If a test fails, run-tests.py produces a pair of .out and .exp files in the result
 directory with the MicroPython output and the expectations, respectively.
 """,
-        epilog="""\
+        epilog=f"""\
+{test_instance_epilog}
 Options -i and -e can be multiple and processed in the order given. Regex
 "search" (vs "match") operation is used. An action (include/exclude) of
 the last matching regex is used:
@@ -822,11 +1216,11 @@ def main():
   run-tests.py -e async -i async_foo - include all, exclude async, yet still include async_foo
 """,
     )
-    cmd_parser.add_argument("--target", default="unix", help="the target platform")
     cmd_parser.add_argument(
-        "--device",
-        default="/dev/ttyACM0",
-        help="the serial device or the IP address of the pyboard",
+        "-c", "--trace-output", action="store_true", help="trace test output while running"
+    )
+    cmd_parser.add_argument(
+        "-t", "--test-instance", default="unix", help="the MicroPython instance to test"
     )
     cmd_parser.add_argument(
         "-b", "--baudrate", default=115200, help="the baud rate of the serial device"
@@ -856,12 +1250,9 @@ def main():
         help="include test by regex on path/name.py",
     )
     cmd_parser.add_argument(
-        "--write-exp",
+        "--dry-run",
         action="store_true",
-        help="use CPython to generate .exp files to run tests w/o CPython",
-    )
-    cmd_parser.add_argument(
-        "--list-tests", action="store_true", help="list tests instead of running them"
+        help="Show tests which would run (though might still be skipped at runtime)",
     )
     cmd_parser.add_argument(
         "--emit", default="bytecode", help="MicroPython emitter to use (bytecode or native)"
@@ -893,14 +1284,43 @@ def main():
         action="store_true",
         help="delete the .exp and .out files from failed tests and exit",
     )
+    cmd_parser.add_argument(
+        "--run-failures",
+        action="store_true",
+        help="re-run only the failed tests",
+    )
+    cmd_parser.add_argument(
+        "--begin",
+        metavar="PROLOGUE",
+        default=None,
+        help="prologue python file to execute before module import",
+    )
+    cmd_parser.add_argument(
+        "--target-wiring",
+        default=None,
+        help="force the given script to be used as target_wiring.py",
+    )
     args = cmd_parser.parse_args()
 
+    prologue = ""
+    if args.begin:
+        with open(args.begin, "rt") as source:
+            prologue = source.read()
+    set_injected_prologue(prologue)
+
     if args.print_failures:
-        for exp in glob(os.path.join(args.result_dir, "*.exp")):
-            testbase = exp[:-4]
+        for out in glob(os.path.join(args.result_dir, "*.out")):
+            testbase = out[:-4]
             print()
             print("FAILURE {0}".format(testbase))
-            os.system("{0} {1}.exp {1}.out".format(DIFF, testbase))
+            if os.path.exists(testbase + ".exp"):
+                # Show diff of expected and actual output.
+                os.system("{0} {1}.exp {1}.out".format(DIFF, testbase))
+            else:
+                # No expected output, just show the actual output (eg from a unittest).
+                with open(out) as f:
+                    for line in f:
+                        print(line, end="")
 
         sys.exit(0)
 
@@ -909,114 +1329,110 @@ def main():
             os.path.join(args.result_dir, "*.out")
         ):
             os.remove(f)
+        rm_f(get_results_filename(args))
 
         sys.exit(0)
 
-    LOCAL_TARGETS = (
-        "unix",
-        "qemu-arm",
-    )
-    EXTERNAL_TARGETS = (
-        "pyboard",
-        "wipy",
-        "esp8266",
-        "esp32",
-        "minimal",
-        "nrf",
-        "renesas-ra",
-        "rp2",
-    )
-    if args.list_tests:
-        pyb = None
-    elif args.target in LOCAL_TARGETS:
-        pyb = None
-        if not args.mpy_cross_flags:
-            if args.target == "unix":
-                args.mpy_cross_flags = "-march=host"
-            elif args.target == "qemu-arm":
-                args.mpy_cross_flags = "-march=armv7m"
-    elif args.target in EXTERNAL_TARGETS:
-        global pyboard
-        sys.path.append(base_path("../tools"))
-        import pyboard
-
-        if not args.mpy_cross_flags:
-            if args.target == "esp8266":
-                args.mpy_cross_flags = "-march=xtensa"
-            elif args.target == "esp32":
-                args.mpy_cross_flags = "-march=xtensawin"
-            elif args.target == "rp2":
-                args.mpy_cross_flags = "-march=armv6m"
-            elif args.target == "pyboard":
-                args.mpy_cross_flags = "-march=armv7emsp"
-            else:
-                args.mpy_cross_flags = "-march=armv7m"
+    # Get the test instance to run on.
+    pyb = get_test_instance(args.test_instance, args.baudrate, args.user, args.password)
 
-        pyb = pyboard.Pyboard(args.device, args.baudrate, args.user, args.password)
-        pyb.enter_raw_repl()
-    else:
-        raise ValueError("target must be one of %s" % ", ".join(LOCAL_TARGETS + EXTERNAL_TARGETS))
+    # Automatically detect the platform.
+    detect_test_platform(pyb, args)
+
+    if args.run_failures and (any(args.files) or args.test_dirs is not None):
+        raise ValueError(
+            "--run-failures cannot be used together with files or --test-dirs arguments"
+        )
 
-    if len(args.files) == 0:
-        if args.test_dirs is None:
+    if args.run_failures:
+        results_file = get_results_filename(args)
+        if os.path.exists(results_file):
+            with open(results_file, "r") as f:
+                tests = list(test[0] for test in json.load(f)["results"] if test[1] == "fail")
+        else:
+            tests = []
+    elif len(args.files) == 0:
+        test_extensions = ("*.py",)
+        if args.platform == "webassembly":
+            test_extensions += ("*.js", "*.mjs")
+
+        all_test_dirs = []
+        main_tests_dir_in_args = None
+        if args.test_dirs is not None:
+            # Run tests from given directories though if user explicitly passes this directory as argument
+            # still do the normal test discovery to be consistent with running from within this directory.
+            main_tests_dir = os.path.realpath(base_path())
+            for test_dir in args.test_dirs:
+                if os.path.realpath(test_dir) == main_tests_dir:
+                    main_tests_dir_in_args = test_dir
+                else:
+                    all_test_dirs.append(test_dir)
+
+        if args.test_dirs is None or main_tests_dir_in_args is not None:
             test_dirs = (
                 "basics",
                 "micropython",
                 "misc",
                 "extmod",
+                "stress",
             )
-            if args.target == "pyboard":
-                # run pyboard tests
-                test_dirs += ("float", "stress", "pyb", "inlineasm")
-            elif args.target in ("renesas-ra"):
-                test_dirs += ("float", "inlineasm", "renesas-ra")
-            elif args.target == "rp2":
-                test_dirs += ("float", "stress", "inlineasm")
-            elif args.target in ("esp8266", "esp32", "minimal", "nrf"):
+            if args.inlineasm_arch is not None:
+                test_dirs += ("inlineasm/{}".format(args.inlineasm_arch),)
+            if args.thread is not None:
+                test_dirs += ("thread",)
+            if args.float_prec > 0:
                 test_dirs += ("float",)
-            elif args.target == "wipy":
-                # run WiPy tests
-                test_dirs += ("wipy",)
-            elif args.target == "unix":
+            if args.unicode:
+                test_dirs += ("unicode",)
+            port_specific_test_dir = "ports/{}".format(platform_to_port(args.platform))
+            if os.path.isdir(port_specific_test_dir):
+                test_dirs += (port_specific_test_dir,)
+            if args.platform in PC_PLATFORMS:
                 # run PC tests
-                test_dirs += (
-                    "float",
-                    "import",
-                    "io",
-                    "stress",
-                    "unicode",
-                    "unix",
-                    "cmdline",
-                )
-            elif args.target == "qemu-arm":
-                if not args.write_exp:
-                    raise ValueError("--target=qemu-arm must be used with --write-exp")
-                # Generate expected output files for qemu run.
-                # This list should match the test_dirs tuple in tinytest-codegen.py.
-                test_dirs += (
-                    "float",
-                    "inlineasm",
-                    "qemu-arm",
-                )
-        else:
-            # run tests from these directories
-            test_dirs = args.test_dirs
+                test_dirs += ("import",)
+                if args.build != "minimal":
+                    test_dirs += ("cmdline", "io")
+
+            all_test_dirs.extend(
+                test_dir
+                if main_tests_dir_in_args is None
+                else os.path.join(main_tests_dir_in_args, test_dir)
+                for test_dir in test_dirs
+            )
+
         tests = sorted(
             test_file
-            for test_files in (glob("{}/*.py".format(dir)) for dir in test_dirs)
+            for test_files in (
+                glob(os.path.join(dir, ext)) for dir in all_test_dirs for ext in test_extensions
+            )
             for test_file in test_files
         )
     else:
         # tests explicitly given
         tests = args.files
 
+    # If any tests need it, prepare the target_wiring script for the target.
+    if pyb and any(test.endswith(tests_requiring_target_wiring) for test in tests):
+        detect_target_wiring_script(pyb, args)
+
+    # End the target information line.
+    print()
+
     if not args.keep_path:
-        # clear search path to make sure tests use only builtin modules and those in extmod
-        os.environ["MICROPYPATH"] = ".frozen" + os.pathsep + base_path("../extmod")
+        # Clear search path to make sure tests use only builtin modules, those in
+        # extmod, and a path to unittest in case it's needed.
+        os.environ["MICROPYPATH"] = (
+            ".frozen"
+            + os.pathsep
+            + base_path("../extmod")
+            + os.pathsep
+            + base_path("../lib/micropython-lib/python-stdlib/unittest")
+        )
 
     try:
         os.makedirs(args.result_dir, exist_ok=True)
-        res = run_tests(pyb, tests, args, args.result_dir, args.jobs)
+        test_results, testcase_count = run_tests(pyb, tests, args, args.result_dir, args.jobs)
+        res = create_test_report(args, test_results, testcase_count)
     finally:
         if pyb:
             pyb.close()
diff --git a/tests/serial_test.py b/tests/serial_test.py
new file mode 100755
index 00000000000..d04c8d0f9fc
--- /dev/null
+++ b/tests/serial_test.py
@@ -0,0 +1,361 @@
+#!/usr/bin/env python
+#
+# Performance and reliability test for serial port communication.
+#
+# Basic usage:
+#   serial_test.py [-t serial-device]
+#
+# The `serial-device` will default to /dev/ttyACM0.
+
+import argparse
+import random
+import serial
+import sys
+import time
+
+from test_utils import test_instance_epilog, convert_device_shortcut_to_real_device
+
+TEST_READ_TIMEOUT = 1
+
+# For ESP32 boards with both native/USJ USB and a UART serial port, soft
+# reset may flush the UART buffer before it completes - this can be quite large
+# if it's backed up a lot of data during the previous test.
+SOFT_RESET_TIMEOUT = 5
+
+echo_test_script = """
+import sys
+bytes_min=%u
+bytes_max=%u
+repeat=%u
+b=memoryview(bytearray(bytes_max))
+for n in range(bytes_min,bytes_max+1):
+    for _ in range(repeat):
+        n2 = sys.stdin.readinto(b[:n])
+        sys.stdout.write(b[:n2])
+"""
+
+read_test_script = """
+bin = True
+try:
+    wr=__import__("pyb").USB_VCP(0).send
+except:
+    import sys
+    if hasattr(sys.stdout,'buffer'):
+        wr=sys.stdout.buffer.write
+    else:
+        wr=sys.stdout.write
+        bin = False
+b=bytearray(%u)
+if bin:
+    wr('BIN')
+    for i in range(len(b)):
+        b[i] = i & 0xff
+else:
+    wr('TXT')
+    for i in range(len(b)):
+        b[i] = 0x20 + (i & 0x3f)
+for _ in range(%d):
+    wr(b)
+"""
+
+
+write_test_script_verified = """
+import sys
+try:
+    rd=__import__("pyb").USB_VCP(0).recv
+except:
+    rd=sys.stdin.readinto
+b=bytearray(%u)
+for _ in range(%u):
+    n = rd(b)
+    fail = 0
+    for i in range(n):
+        if b[i] != 32 + (i & 0x3f):
+            fail += 1
+    if fail:
+        sys.stdout.write(b'ER%%05u' %% fail)
+    else:
+        sys.stdout.write(b'OK%%05u' %% n)
+"""
+
+write_test_script_unverified = """
+import sys
+try:
+    rd=__import__("pyb").USB_VCP(0).recv
+except:
+    rd=sys.stdin.readinto
+b=bytearray(%u)
+for _ in range(%u):
+    n = rd(b)
+    if n != len(b):
+        sys.stdout.write(b'ER%%05u' %% n)
+    else:
+        sys.stdout.write(b'OK%%05u' %% n)
+"""
+
+
+class TestError(Exception):
+    pass
+
+
+def drain_input(ser):
+    time.sleep(0.1)
+    while ser.inWaiting() > 0:
+        data = ser.read(ser.inWaiting())
+        time.sleep(0.1)
+
+
+def reset_into_raw_repl(ser):
+    try:
+        ser.timeout = SOFT_RESET_TIMEOUT
+        ser.flushInput()
+        ser.write(b"\x03\x01\x04")  # break, raw-repl, soft-reboot
+        EXPECTED1 = b"MPY: soft reboot\r\n"
+        EXPECTED2 = b"raw REPL; CTRL-B to exit\r\n>"
+        r1 = ser.read_until(EXPECTED1)
+        r2 = b""
+        if r1.endswith(EXPECTED1):
+            r2 = ser.read_until(EXPECTED2)
+        if not r2.endswith(EXPECTED2):
+            raise TestError("could not reset to raw REPL", r1 + r2)
+    finally:
+        ser.timeout = TEST_READ_TIMEOUT
+
+
+def send_script(ser, script):
+    reset_into_raw_repl(ser)
+    chunk_size = 32
+    for i in range(0, len(script), chunk_size):
+        ser.write(script[i : i + chunk_size])
+        time.sleep(0.01)
+    ser.write(b"\x04")  # eof
+    ser.flush()
+    response = ser.read(2)
+    if response != b"OK":
+        response += ser.read(ser.inWaiting())
+        raise TestError("could not send script", response)
+
+
+def echo_test(ser_repl, ser_data):
+    global test_passed
+
+    # Make the test data deterministic.
+    random.seed(0)
+
+    # Set parameters for the test.
+    # Go just a bit above the size of a USB high-speed packet.
+    bytes_min = 1
+    bytes_max = 520
+    num_repeat = 1
+
+    # Load and run the write_test_script.
+    script = bytes(echo_test_script % (bytes_min, bytes_max, num_repeat), "ascii")
+    send_script(ser_repl, script)
+
+    # A selection of printable bytes for echo data.
+    printable_bytes = list(range(48, 58)) + list(range(65, 91)) + list(range(97, 123))
+
+    # Write data to the device and record the echo'd data.
+    # Use a different selection of random printable characters for each
+    # echo, to make it easier to debug when the echo doesn't match.
+    num_errors = 0
+    echo_results = []
+    for num_bytes in range(bytes_min, bytes_max + 1):
+        print(f"DATA ECHO: {num_bytes} / {bytes_max}", end="\r")
+        for repeat in range(num_repeat):
+            rand_bytes = list(random.choice(printable_bytes) for _ in range(8))
+            buf = bytes(random.choice(rand_bytes) for _ in range(num_bytes))
+            ser_data.write(buf)
+            buf2 = ser_data.read(len(buf))
+            match = buf == buf2
+            num_errors += not match
+            echo_results.append((match, buf, buf2))
+        if num_errors > 8:
+            # Stop early if there are too many errors.
+            break
+    ser_repl.write(b"\x03")
+
+    # Print results.
+    if all(match for match, _, _ in echo_results):
+        print("DATA ECHO: OK for {}-{} bytes at a time".format(bytes_min, bytes_max))
+    else:
+        test_passed = False
+        print("DATA ECHO: FAIL     ")
+        for match, buf, buf2 in echo_results:
+            print("  sent", len(buf), buf)
+            if match:
+                print("  echo match")
+            else:
+                print("  echo", len(buf), buf2)
+
+
+def read_test(ser_repl, ser_data, bufsize, nbuf):
+    global test_passed
+
+    assert bufsize % 256 == 0  # for verify to work
+
+    # how long to wait for data from device
+    # (if UART TX is also enabled then it can take 1.4s to send
+    # out a 16KB butter at 115200bps)
+    READ_TIMEOUT_S = 2
+
+    # Load and run the read_test_script.
+    script = bytes(read_test_script % (bufsize, nbuf), "ascii")
+    send_script(ser_repl, script)
+
+    # Read from the device the type of data that it will send (BIN or TXT).
+    data_type = ser_data.read(3)
+
+    # Read data from the device, check it is correct, and measure throughput.
+    n = 0
+    last_byte = None
+    t_start = time.time()
+    remain = nbuf * bufsize
+    total_data = bytearray(remain)
+    while remain:
+        t0 = time.monotonic_ns()
+        while ser_data.inWaiting() == 0:
+            if time.monotonic_ns() - t0 > READ_TIMEOUT_S * 1e9:
+                # timeout waiting for data from device
+                break
+            time.sleep(0.0001)
+        if not ser_data.inWaiting():
+            test_passed = False
+            print("ERROR: timeout waiting for data")
+            print(total_data[:n])
+            return 0
+        to_read = min(ser_data.inWaiting(), remain)
+        data = ser_data.read(to_read)
+        remain -= len(data)
+        print(f"{n} / {nbuf * bufsize}", end="\r")
+        total_data[n : n + len(data)] = data
+        n += len(data)
+    t_end = time.time()
+    for i in range(0, len(total_data)):
+        if data_type == b"BIN":
+            wanted = i & 0xFF
+        else:
+            wanted = 0x20 + (i & 0x3F)
+        if total_data[i] != wanted:
+            test_passed = False
+            print("ERROR: data mismatch:", i, wanted, total_data[i])
+    ser_repl.write(b"\x03")  # break
+    t = t_end - t_start
+
+    # Print results.
+    print(
+        "DATA IN: bufsize=%u, read %u bytes in %.2f msec = %.2f kibytes/sec = %.2f MBits/sec"
+        % (bufsize, n, t * 1000, n / 1024 / t, n * 8 / 1000000 / t)
+    )
+
+    return n / t
+
+
+def write_test(ser_repl, ser_data, bufsize, nbuf, verified):
+    global test_passed
+
+    # Load and run the write_test_script.
+    if verified:
+        script = write_test_script_verified
+    else:
+        script = write_test_script_unverified
+    script = bytes(script % (bufsize, nbuf), "ascii")
+    send_script(ser_repl, script)
+    drain_input(ser_repl)
+
+    # Write data to the device, check it is correct, and measure throughput.
+    n = 0
+    t_start = time.time()
+    buf = bytearray(bufsize)
+    for i in range(len(buf)):
+        buf[i] = 32 + (i & 0x3F)  # don't want to send ctrl chars!
+    for i in range(nbuf):
+        ser_data.write(buf)
+        n += len(buf)
+        print(f"{n} / {nbuf * bufsize}", end="\r")
+        response = ser_repl.read(7)
+        if response != b"OK%05u" % bufsize:
+            test_passed = False
+            print("ERROR: bad response, expecting OK%05u, got %r" % (bufsize, response))
+    t_end = time.time()
+    ser_repl.write(b"\x03")  # break
+    t = t_end - t_start
+
+    # Print results.
+    print(
+        "DATA OUT: verify=%d, bufsize=%u, wrote %u bytes in %.2f msec = %.2f kibytes/sec = %.2f MBits/sec"
+        % (verified, bufsize, n, t * 1000, n / 1024 / t, n * 8 / 1000000 / t)
+    )
+
+    return n / t
+
+
+def do_test(dev_repl, dev_data=None, time_per_subtest=1):
+    if dev_data is None:
+        print("REPL and data on", dev_repl)
+        ser_repl = serial.Serial(dev_repl, baudrate=115200, timeout=TEST_READ_TIMEOUT)
+        ser_data = ser_repl
+    else:
+        print("REPL on", dev_repl)
+        print("data on", dev_data)
+        ser_repl = serial.Serial(dev_repl, baudrate=115200, timeout=TEST_READ_TIMEOUT)
+        ser_data = serial.Serial(dev_data, baudrate=115200, timeout=TEST_READ_TIMEOUT)
+
+    # Do echo test first, and abort if it doesn't pass.
+    echo_test(ser_repl, ser_data)
+    if not test_passed:
+        return
+
+    # Do read and write throughput test.
+    for test_func, test_args, bufsize in (
+        (read_test, (), 256),
+        (write_test, (True,), 128),
+        (write_test, (False,), 128),
+    ):
+        nbuf = 128
+        while bufsize <= 16384:
+            rate = test_func(ser_repl, ser_data, bufsize, nbuf, *test_args)
+            bufsize *= 2
+            if rate:
+                # Adjust the amount of data based on the rate, to keep each subtest
+                # at around time_per_subtest seconds long.
+                nbuf = max(min(128, int(rate * time_per_subtest / bufsize)), 1)
+
+    ser_repl.close()
+    ser_data.close()
+
+
+def main():
+    global test_passed
+
+    cmd_parser = argparse.ArgumentParser(
+        description="Test performance and reliability of serial port communication.",
+        epilog=test_instance_epilog,
+        formatter_class=argparse.RawTextHelpFormatter,
+    )
+    cmd_parser.add_argument(
+        "-t",
+        "--test-instance",
+        default="a0",
+        help="MicroPython instance to test",
+    )
+    cmd_parser.add_argument(
+        "--time-per-subtest", default="1", help="approximate time to take per subtest (in seconds)"
+    )
+    args = cmd_parser.parse_args()
+
+    dev_repl = convert_device_shortcut_to_real_device(args.test_instance)
+
+    test_passed = True
+    try:
+        do_test(dev_repl, None, float(args.time_per_subtest))
+    except TestError as er:
+        test_passed = False
+        print("ERROR:", er)
+
+    if not test_passed:
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/stress/bytecode_limit.py b/tests/stress/bytecode_limit.py
index 8cca413cf27..7e77cdd5e5a 100644
--- a/tests/stress/bytecode_limit.py
+++ b/tests/stress/bytecode_limit.py
@@ -1,16 +1,30 @@
 # Test the limits of bytecode generation.
 
+import sys
+
+# Tune the test parameters based on the target's bytecode generator.
+if hasattr(sys.implementation, "_mpy"):
+    # Target can load .mpy files so generated bytecode uses 1 byte per qstr.
+    number_of_body_copies = (433, 432, 431, 399)
+else:
+    # Target can't load .mpy files so generated bytecode uses 2 bytes per qstr.
+    number_of_body_copies = (401, 400, 399, 398)
+
 body = " with f()()() as a:\n  try:\n   f()()()\n  except Exception:\n   pass\n"
 
 # Test overflow of jump offset.
-for n in (430, 431, 432, 433):
+# Print results at the end in case an intermediate value of n fails with MemoryError.
+results = []
+for n in number_of_body_copies:
     try:
         exec("cond = 0\nif cond:\n" + body * n + "else:\n print('cond false')\n")
+        results.append("ok")
     except MemoryError:
         print("SKIP")
         raise SystemExit
-    except RuntimeError:
-        print("RuntimeError")
+    except RuntimeError as er:
+        results.append("RuntimeError('{}')".format(str(er) or "bytecode overflow"))
+print(results)
 
 # Test changing size of code info (source line/bytecode mapping) due to changing
 # bytecode size in the final passes.  This test is very specific to how the
diff --git a/tests/stress/bytecode_limit.py.exp b/tests/stress/bytecode_limit.py.exp
index 74ab06c09b6..3f1eb0db4c4 100644
--- a/tests/stress/bytecode_limit.py.exp
+++ b/tests/stress/bytecode_limit.py.exp
@@ -1,5 +1,4 @@
 cond false
 cond false
-RuntimeError
-RuntimeError
+["RuntimeError('bytecode overflow')", "RuntimeError('bytecode overflow')", 'ok', 'ok']
 [123]
diff --git a/tests/stress/dict_copy.py b/tests/stress/dict_copy.py
index 73d3a5b51d6..f9b742e20f7 100644
--- a/tests/stress/dict_copy.py
+++ b/tests/stress/dict_copy.py
@@ -1,6 +1,11 @@
 # copying a large dictionary
 
-a = {i: 2 * i for i in range(1000)}
+try:
+    a = {i: 2 * i for i in range(1000)}
+except MemoryError:
+    print("SKIP")
+    raise SystemExit
+
 b = a.copy()
 for i in range(1000):
     print(i, b[i])
diff --git a/tests/stress/dict_create.py b/tests/stress/dict_create.py
index e9db40a8e6c..91a83a12f9d 100644
--- a/tests/stress/dict_create.py
+++ b/tests/stress/dict_create.py
@@ -3,6 +3,10 @@
 d = {}
 x = 1
 while x < 1000:
-    d[x] = x
+    try:
+        d[x] = x
+    except MemoryError:
+        print("SKIP")
+        raise SystemExit
     x += 1
 print(d[500])
diff --git a/tests/stress/fun_call_limit.py b/tests/stress/fun_call_limit.py
index b802aadd558..69f8aa5aec4 100644
--- a/tests/stress/fun_call_limit.py
+++ b/tests/stress/fun_call_limit.py
@@ -16,14 +16,16 @@ def test(n):
 
 
 # If the port has at least 32-bits then this test should pass.
-print(test(29))
+print(test(28))
 
 # This test should fail on all ports (overflows a small int).
 print(test(70))
 
-# Check that there is a correct transition to the limit of too many args before *args.
+# 28 is the biggest number that will pass on a 32-bit port using object
+# representation B, which has 1<<28 still fitting in a positive small int.
 reached_limit = False
-for i in range(30, 70):
+any_test_succeeded = False
+for i in range(28, 70):
     result = test(i)
     if reached_limit:
         if result != "SyntaxError":
@@ -34,3 +36,5 @@ def test(n):
         else:
             if result != i + 4:
                 print("FAIL")
+            any_test_succeeded = True
+assert any_test_succeeded  # At least one iteration must have passed
diff --git a/tests/stress/fun_call_limit.py.exp b/tests/stress/fun_call_limit.py.exp
index 53d2b280430..491abbfa8be 100644
--- a/tests/stress/fun_call_limit.py.exp
+++ b/tests/stress/fun_call_limit.py.exp
@@ -1,2 +1,2 @@
-33
+32
 SyntaxError
diff --git a/tests/stress/qstr_limit.py b/tests/stress/qstr_limit.py
index 08b10a039f5..55b87075df9 100644
--- a/tests/stress/qstr_limit.py
+++ b/tests/stress/qstr_limit.py
@@ -12,8 +12,8 @@ def make_id(n, base="a"):
     var = make_id(l)
     try:
         exec(var + "=1", g)
-    except RuntimeError:
-        print("RuntimeError", l)
+    except RuntimeError as er:
+        print(str(er) or "name too long", l)
         continue
     print(var in g)
 
@@ -26,16 +26,16 @@ def f(**k):
 for l in range(254, 259):
     try:
         exec("f({}=1)".format(make_id(l)))
-    except RuntimeError:
-        print("RuntimeError", l)
+    except RuntimeError as er:
+        print(str(er) or "name too long", l)
 
 # type construction
 for l in range(254, 259):
     id = make_id(l)
     try:
-        print(type(id, (), {}).__name__)
-    except RuntimeError:
-        print("RuntimeError", l)
+        print(type(id, (), {}))
+    except RuntimeError as er:
+        print(str(er) or "name too long", l)
 
 
 # hasattr, setattr, getattr
@@ -48,28 +48,20 @@ class A:
     a = A()
     try:
         setattr(a, id, 123)
-    except RuntimeError:
-        print("RuntimeError", l)
+    except RuntimeError as er:
+        print(str(er) or "name too long", l)
     try:
         print(hasattr(a, id), getattr(a, id))
-    except RuntimeError:
-        print("RuntimeError", l)
+    except RuntimeError as er:
+        print(str(er) or "name too long", l)
 
 # format with keys
 for l in range(254, 259):
     id = make_id(l)
     try:
         print(("{" + id + "}").format(**{id: l}))
-    except RuntimeError:
-        print("RuntimeError", l)
-
-# modulo format with keys
-for l in range(254, 259):
-    id = make_id(l)
-    try:
-        print(("%(" + id + ")d") % {id: l})
-    except RuntimeError:
-        print("RuntimeError", l)
+    except RuntimeError as er:
+        print(str(er) or "name too long", l)
 
 # import module
 # (different OS's have different results so only run those that are consistent)
@@ -78,8 +70,8 @@ class A:
         __import__(make_id(l))
     except ImportError:
         print("ok", l)
-    except RuntimeError:
-        print("RuntimeError", l)
+    except RuntimeError as er:
+        print(str(er) or "name too long", l)
 
 # import package
 for l in (100, 101, 102, 128, 129):
@@ -87,5 +79,5 @@ class A:
         exec("import " + make_id(l) + "." + make_id(l, "A"))
     except ImportError:
         print("ok", l)
-    except RuntimeError:
-        print("RuntimeError", l)
+    except RuntimeError as er:
+        print(str(er) or "name too long", l)
diff --git a/tests/stress/qstr_limit.py.exp b/tests/stress/qstr_limit.py.exp
index 455761bc71e..5596b54bebd 100644
--- a/tests/stress/qstr_limit.py.exp
+++ b/tests/stress/qstr_limit.py.exp
@@ -1,43 +1,38 @@
 True
 True
-RuntimeError 256
-RuntimeError 257
-RuntimeError 258
+name too long 256
+name too long 257
+name too long 258
 {'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrst': 1}
 {'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstu': 1}
-RuntimeError 256
-RuntimeError 257
-RuntimeError 258
-abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrst
-abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstu
-RuntimeError 256
-RuntimeError 257
-RuntimeError 258
+name too long 256
+name too long 257
+name too long 258
+
+
+name too long 256
+name too long 257
+name too long 258
 True 123
 True 123
-RuntimeError 256
-RuntimeError 256
-RuntimeError 257
-RuntimeError 257
-RuntimeError 258
-RuntimeError 258
+name too long 256
+name too long 256
+name too long 257
+name too long 257
+name too long 258
+name too long 258
 254
 255
-RuntimeError 256
-RuntimeError 257
-RuntimeError 258
-254
-255
-RuntimeError 256
-RuntimeError 257
-RuntimeError 258
+name too long 256
+name too long 257
+name too long 258
 ok 100
 ok 101
-RuntimeError 256
-RuntimeError 257
-RuntimeError 258
+name too long 256
+name too long 257
+name too long 258
 ok 100
 ok 101
 ok 102
-RuntimeError 128
-RuntimeError 129
+name too long 128
+name too long 129
diff --git a/tests/stress/qstr_limit_str_modulo.py b/tests/stress/qstr_limit_str_modulo.py
new file mode 100644
index 00000000000..c56f23b0a1d
--- /dev/null
+++ b/tests/stress/qstr_limit_str_modulo.py
@@ -0,0 +1,21 @@
+# Test interning qstrs that go over the qstr length limit (255 bytes in default configuration).
+# The tests here are specifically for str formatting with %.
+
+try:
+    "" % ()
+except TypeError:
+    print("SKIP")
+    raise SystemExit
+
+
+def make_id(n, base="a"):
+    return "".join(chr(ord(base) + i % 26) for i in range(n))
+
+
+# modulo format with keys
+for l in range(254, 259):
+    id = make_id(l)
+    try:
+        print(("%(" + id + ")d") % {id: l})
+    except RuntimeError as er:
+        print("RuntimeError", str(er) or "name too long", l)
diff --git a/tests/stress/qstr_limit_str_modulo.py.exp b/tests/stress/qstr_limit_str_modulo.py.exp
new file mode 100644
index 00000000000..3632c85bffe
--- /dev/null
+++ b/tests/stress/qstr_limit_str_modulo.py.exp
@@ -0,0 +1,5 @@
+254
+255
+RuntimeError name too long 256
+RuntimeError name too long 257
+RuntimeError name too long 258
diff --git a/tests/stress/recursive_data.py b/tests/stress/recursive_data.py
index 3b7fa509525..6e01319ed3a 100644
--- a/tests/stress/recursive_data.py
+++ b/tests/stress/recursive_data.py
@@ -1,6 +1,6 @@
 # This tests that printing recursive data structure doesn't lead to segfault.
 try:
-    import uio as io
+    import io
 except ImportError:
     print("SKIP")
     raise SystemExit
diff --git a/tests/stress/recursive_iternext.py b/tests/stress/recursive_iternext.py
index bbc389e7262..c737f1e36d7 100644
--- a/tests/stress/recursive_iternext.py
+++ b/tests/stress/recursive_iternext.py
@@ -1,4 +1,8 @@
 # This tests that recursion with iternext doesn't lead to segfault.
+#
+# This test segfaults CPython, but that's not a bug as CPython doesn't enforce
+# limits on C recursion - see
+# https://github.com/python/cpython/issues/58218#issuecomment-1093570209
 try:
     enumerate
     filter
@@ -9,49 +13,25 @@
     print("SKIP")
     raise SystemExit
 
-# We need to pick an N that is large enough to hit the recursion
-# limit, but not too large that we run out of heap memory.
-try:
-    # large stack/heap, eg unix
-    [0] * 80000
-    N = 5000
-except:
-    try:
-        # medium, eg pyboard
-        [0] * 10000
-        N = 1000
-    except:
-        # small, eg esp8266
-        N = 100
-
-try:
-    x = (1, 2)
-    for i in range(N):
-        x = enumerate(x)
-    tuple(x)
-except RuntimeError:
-    print("RuntimeError")
 
-try:
+# Progressively build a bigger nested iterator structure (10 at a time for speed),
+# and then try to evaluate it via tuple(x) which makes deep recursive function calls.
+#
+# Eventually this should raise a RuntimeError as MicroPython runs out of stack.
+# It shouldn't ever raise a MemoryError, if it does then somehow MicroPython has
+# run out of heap (for the nested structure) before running out of stack.
+def recurse_iternext(nested_fn):
     x = (1, 2)
-    for i in range(N):
-        x = filter(None, x)
-    tuple(x)
-except RuntimeError:
-    print("RuntimeError")
+    while True:
+        for _ in range(10):
+            x = nested_fn(x)
+        try:
+            tuple(x)
+        except RuntimeError:
+            print("RuntimeError")
+            break
 
-try:
-    x = (1, 2)
-    for i in range(N):
-        x = map(max, x, ())
-    tuple(x)
-except RuntimeError:
-    print("RuntimeError")
 
-try:
-    x = (1, 2)
-    for i in range(N):
-        x = zip(x)
-    tuple(x)
-except RuntimeError:
-    print("RuntimeError")
+# Test on various nested iterator structures
+for nested_fn in [enumerate, lambda x: filter(None, x), lambda x: map(max, x, ()), zip]:
+    recurse_iternext(nested_fn)
diff --git a/tests/target_wiring/EK_RA6M2.py b/tests/target_wiring/EK_RA6M2.py
new file mode 100644
index 00000000000..7d4a8cbbd64
--- /dev/null
+++ b/tests/target_wiring/EK_RA6M2.py
@@ -0,0 +1,8 @@
+# Target wiring for EK_RA6M2.
+#
+# Connect:
+# - P601 to P602
+
+# UART(9) is on P602/P601.
+uart_loopback_args = (9,)
+uart_loopback_kwargs = {}
diff --git a/tests/target_wiring/KIT_PSE84_AI.py b/tests/target_wiring/KIT_PSE84_AI.py
new file mode 100644
index 00000000000..8ef3f08385d
--- /dev/null
+++ b/tests/target_wiring/KIT_PSE84_AI.py
@@ -0,0 +1,5 @@
+# Target wiring for KIT_PSE84_AI.
+
+# UART(5) is on P17_1/P17_0.
+uart_loopback_args = ()
+uart_loopback_kwargs = {"tx": "P17_1", "rx": "P17_0"}
diff --git a/tests/target_wiring/NUCLEO_WB55.py b/tests/target_wiring/NUCLEO_WB55.py
new file mode 100644
index 00000000000..e40d35e225b
--- /dev/null
+++ b/tests/target_wiring/NUCLEO_WB55.py
@@ -0,0 +1,12 @@
+# Target wiring for NUCLEO_WB55.
+#
+# Connect:
+# - PA2 to PA3
+
+# LPUART(1) is on PA2/PA3.
+uart_loopback_args = ("LP1",)
+uart_loopback_kwargs = {}
+
+spi_standalone_args_list = [(1,), (2,)]
+
+pwm_loopback_pins = [("D1", "D0")]
diff --git a/tests/target_wiring/PYBx.py b/tests/target_wiring/PYBx.py
new file mode 100644
index 00000000000..b36e342e944
--- /dev/null
+++ b/tests/target_wiring/PYBx.py
@@ -0,0 +1,16 @@
+# Target wiring for PYBV10, PYBV11, PYBLITEV10, PYBD_SF2, PYBD_SF3, PYBD_SF6.
+#
+# Connect:
+# - X1 to X2
+
+# UART("XA") is on X1/X2  (usually UART(4) on PA0/PA1).
+uart_loopback_args = ("XA",)
+uart_loopback_kwargs = {}
+
+spi_standalone_args_list = [(1,), (2,)]
+
+# CAN args assume no connection for single device tests
+can_args = (1,)
+can_kwargs = {}
+
+pwm_loopback_pins = [("X1", "X2")]
diff --git a/tests/target_wiring/README.md b/tests/target_wiring/README.md
new file mode 100644
index 00000000000..c3a7038bb91
--- /dev/null
+++ b/tests/target_wiring/README.md
@@ -0,0 +1,81 @@
+# Target wiring
+
+Some tests require hardware configuration and/or external connections, for example
+bridging a pair of GPIO pins.  Each board that such tests run on needs to be configured
+individually.  That is achieved by providing a target wiring configuration script that
+defines the necessary hardware parameters for each test.
+
+## Selecting the target wiring
+
+There are three ways to provide the target wiring configuration:
+
+1. Specify it explicitly when running the test: `./run-tests.py --target-wiring