├── .devcontainer └── devcontainer.json ├── .dockerignore ├── .github └── workflows │ ├── build-rustzx-esp32-on-x86_64-unknown-linux-gnu-dispatch.yaml │ └── build-rustzx-esp32-on-x86_64-unknown-linux-gnu-ubuntu-20-dispatch.yaml ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .vscode ├── launch.json └── tasks.json ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── data ├── hello.tap └── sample.bas ├── docs ├── rustzx-esp32-diagram.json └── rustzx-esp32-ili9341.png ├── emulator ├── Cargo.toml └── src │ ├── host.rs │ ├── io.rs │ ├── lib.rs │ └── stopwatch.rs ├── esp-now-keyboard ├── Cargo.toml └── src │ └── lib.rs ├── esp32-c3-devkit-rust ├── .cargo │ └── config.toml ├── .vscode │ └── launch.json ├── Cargo.toml ├── diagram.json ├── rust-toolchain.toml ├── src │ ├── host.rs │ ├── io.rs │ ├── main.rs │ ├── spritebuf.rs │ └── stopwatch.rs └── wokwi.toml ├── esp32-c6 ├── .cargo │ └── config.toml ├── Cargo.toml ├── diagram.json ├── rust-toolchain.toml ├── src │ └── main.rs └── wokwi.toml ├── esp32-s3-box ├── .cargo │ └── config.toml ├── Cargo.toml ├── rust-toolchain.toml └── src │ └── main.rs ├── esp32-s3-usb-otg-keyboard ├── CMakeLists.txt ├── README.md ├── main │ ├── CMakeLists.txt │ ├── hid_host_example.c │ └── idf_component.yml └── sdkconfig ├── graphics ├── Cargo.toml └── src │ └── lib.rs ├── keyboard-pipe ├── Cargo.toml └── src │ └── lib.rs ├── m5stack-cores3-ps2-keyboard ├── .cargo │ └── config.toml ├── Cargo.toml └── src │ ├── host.rs │ ├── io.rs │ ├── main.rs │ ├── pc_zxkey.rs │ ├── stopwatch.rs │ └── zx_event.rs ├── m5stack-cores3 ├── .cargo │ └── config.toml ├── Cargo.toml ├── diagram.json ├── rust-toolchain.toml ├── src │ └── main.rs └── wokwi.toml ├── uart-keyboard ├── Cargo.toml └── src │ └── lib.rs └── usb-zx ├── Cargo.toml ├── README.md └── src ├── lib.rs ├── uart_usb_key.rs ├── usb_zx_key.rs └── zx_event.rs /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rustzx-esp32", 3 | //"image": "docker.io/sergiogasquez/esp-rs-env:espidf_v4.4", 4 | "build": { 5 | "dockerfile": "../Containerfile", 6 | "args": { 7 | "CONTAINER_USER": "esp", 8 | "CONTAINER_GROUP": "esp", 9 | //"TOOLCHAIN_VERSION": "1.60.0.1", 10 | //"ESP_IDF_VERSION": "release/v4.4", 11 | "ESP_BOARD": "esp32" 12 | } 13 | }, 14 | "settings": { 15 | "editor.formatOnPaste": true, 16 | "editor.formatOnSave": true, 17 | "editor.formatOnSaveMode": "modifications", 18 | "editor.formatOnType": true, 19 | "lldb.executable": "/usr/bin/lldb", 20 | "files.watcherExclude": { 21 | "**/target/**": true 22 | }, 23 | "rust-analyzer.checkOnSave.command": "clippy", 24 | "[rust]": { 25 | "editor.defaultFormatter": "rust-lang.rust-analyzer" 26 | } 27 | }, 28 | "extensions": [ 29 | "rust-lang.rust-analyzer", 30 | "tamasfe.even-better-toml", 31 | "serayuzgur.crates", 32 | "mutantdino.resourcemonitor", 33 | "yzhang.markdown-all-in-one", 34 | "webfreak.debug", 35 | "actboy168.tasks" 36 | ], 37 | "forwardPorts": [ 38 | 9012, 39 | 9333, 40 | 8000 41 | ], 42 | "workspaceMount": "source=${localWorkspaceFolder},target=/home/esp/workspace,type=bind,consistency=cached", 43 | "workspaceFolder": "/home/esp/workspace/" 44 | } 45 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/build-rustzx-esp32-on-x86_64-unknown-linux-gnu-dispatch.yaml: -------------------------------------------------------------------------------- 1 | name: Build RustZX-ESP32 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | rust_build_branch: 7 | description: 'Branch with rust-build scripts' 8 | required: true 9 | default: 'main' 10 | toolchain_version: 11 | description: 'Version of Rust IDF toolchain' 12 | required: true 13 | default: '1.56.0.1' 14 | esp_idf_version: 15 | description: 'ESP-IDF version for embuild' 16 | required: true 17 | default: 'branch:master' 18 | target: 19 | required: true 20 | default: 'xtensa-esp32s3-espidf' 21 | board: 22 | required: true 23 | default: 'esp32s3_usb_otg' 24 | 25 | jobs: 26 | get_release: 27 | # https://github.com/octokit/request-action 28 | name: Get release 29 | runs-on: ubuntu-latest 30 | outputs: 31 | upload_url: ${{ steps.get_upload_url.outputs.url }} 32 | steps: 33 | - uses: octokit/request-action@v2.x 34 | id: get_latest_release 35 | with: 36 | route: GET /repos/{owner}/{repo}/releases/latest 37 | owner: georgik 38 | repo: rustzx-esp32 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | - name: get upload url 42 | id: get_upload_url 43 | run: | 44 | url=$(echo "$response" | jq -r '.upload_url') 45 | echo "::set-output name=url::$url" 46 | env: 47 | response: ${{ steps.get_latest_release.outputs.data }} 48 | build-rustzx-esp32: 49 | name: Build RustZX using ${{ matrix.os }} 50 | needs: get_release 51 | runs-on: ${{ matrix.os }} 52 | strategy: 53 | matrix: 54 | os: [ubuntu-18.04] 55 | include: 56 | - os: ubuntu-18.04 57 | ARCH: '' 58 | steps: 59 | - name: Check out Rust installation scripts 60 | uses: actions/checkout@v2 61 | with: 62 | repository: esp-rs/rust-build 63 | path: rust-build 64 | ref: ${{ github.event.inputs.rust_build_branch }} 65 | - name: Set up Python 66 | if: ${{ matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-18.04' || matrix.os == 'windows-latest' }} 67 | uses: actions/setup-python@v2 68 | with: 69 | python-version: '3.8' 70 | - name: Setup Ninja 71 | if: ${{ matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-18.04' || matrix.os == 'windows-latest' }} 72 | uses: seanmiddleditch/gha-setup-ninja@master 73 | - name: Installing Rust toolchain 74 | run: | 75 | ./rust-build/install-rust-toolchain.sh --toolchain-version ${{ github.event.inputs.toolchain_version }} --export-file "export-esp-${{ github.event.inputs.toolchain_version }}.sh" --extra-crates "ldproxy" --toolchain-destination "${HOME}/.rustup/toolchains/esp-${{ github.event.inputs.toolchain_version }}" 76 | - name: Check out RustZX-ESP32 77 | uses: actions/checkout@v2 78 | with: 79 | path: rustzx-esp32 80 | - name: Build RustZX for ${{ github.event.inputs.target }} 81 | working-directory: rustzx-esp32 82 | run: | 83 | source "../export-esp-${{ github.event.inputs.toolchain_version }}.sh" 84 | export ESP_IDF_VERSION=${{ github.event.inputs.esp_idf_version }} 85 | cargo +esp-${{ github.event.inputs.toolchain_version }} build --target ${{ github.event.inputs.target }} --release --features "${{ github.event.inputs.board }} native" 86 | - name: Upload Release Asset 87 | id: upload-release-asset 88 | uses: actions/upload-release-asset@v1 89 | env: 90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | with: 92 | upload_url: ${{ needs.get_release.outputs.upload_url }} 93 | asset_path: rustzx-esp32/target/${{ github.event.inputs.target }}/release/rustzx-esp32 94 | asset_name: rustzx-esp32-${{ github.event.inputs.board }} 95 | asset_content_type: 'application/octet-stream' 96 | -------------------------------------------------------------------------------- /.github/workflows/build-rustzx-esp32-on-x86_64-unknown-linux-gnu-ubuntu-20-dispatch.yaml: -------------------------------------------------------------------------------- 1 | name: Build RustZX-ESP32 on Ubuntu 20 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | rust_build_branch: 7 | description: 'Branch with rust-build scripts' 8 | required: true 9 | default: 'main' 10 | toolchain_version: 11 | description: 'Version of Rust IDF toolchain' 12 | required: true 13 | default: '1.56.0.1' 14 | esp_idf_version: 15 | description: 'ESP-IDF version for embuild' 16 | required: true 17 | default: 'branch:master' 18 | target: 19 | required: true 20 | default: 'xtensa-esp32s3-espidf' 21 | board: 22 | required: true 23 | default: 'esp32s3_usb_otg' 24 | 25 | jobs: 26 | get_release: 27 | # https://github.com/octokit/request-action 28 | name: Get release 29 | runs-on: ubuntu-latest 30 | outputs: 31 | upload_url: ${{ steps.get_upload_url.outputs.url }} 32 | steps: 33 | - uses: octokit/request-action@v2.x 34 | id: get_latest_release 35 | with: 36 | route: GET /repos/{owner}/{repo}/releases/latest 37 | owner: georgik 38 | repo: rustzx-esp32 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | - name: get upload url 42 | id: get_upload_url 43 | run: | 44 | url=$(echo "$response" | jq -r '.upload_url') 45 | echo "::set-output name=url::$url" 46 | env: 47 | response: ${{ steps.get_latest_release.outputs.data }} 48 | build-rustzx-esp32: 49 | name: Build RustZX using ${{ matrix.os }} 50 | needs: get_release 51 | runs-on: ${{ matrix.os }} 52 | strategy: 53 | matrix: 54 | os: [ubuntu-20.04] 55 | include: 56 | - os: ubuntu-20.04 57 | ARCH: '' 58 | steps: 59 | - name: Check out Rust installation scripts 60 | uses: actions/checkout@v2 61 | with: 62 | repository: esp-rs/rust-build 63 | path: rust-build 64 | ref: ${{ github.event.inputs.rust_build_branch }} 65 | - name: Set up Python 66 | if: ${{ matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-18.04' || matrix.os == 'windows-latest' }} 67 | uses: actions/setup-python@v2 68 | with: 69 | python-version: '3.8' 70 | - name: Setup Ninja 71 | if: ${{ matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-18.04' || matrix.os == 'windows-latest' }} 72 | uses: seanmiddleditch/gha-setup-ninja@master 73 | - name: Installing Rust toolchain 74 | run: | 75 | ./rust-build/install-rust-toolchain.sh --toolchain-version ${{ github.event.inputs.toolchain_version }} --export-file "export-esp-${{ github.event.inputs.toolchain_version }}.sh" --extra-crates "ldproxy" --toolchain-destination "${HOME}/.rustup/toolchains/esp-${{ github.event.inputs.toolchain_version }}" 76 | - name: Check out RustZX-ESP32 77 | uses: actions/checkout@v2 78 | with: 79 | path: rustzx-esp32 80 | - name: Build RustZX for ${{ github.event.inputs.target }} 81 | working-directory: rustzx-esp32 82 | run: | 83 | source "../export-esp-${{ github.event.inputs.toolchain_version }}.sh" 84 | export ESP_IDF_VERSION=${{ github.event.inputs.esp_idf_version }} 85 | cargo +esp-${{ github.event.inputs.toolchain_version }} build --target ${{ github.event.inputs.target }} --release --features "${{ github.event.inputs.board }} native" 86 | - name: Upload Release Asset 87 | id: upload-release-asset 88 | uses: actions/upload-release-asset@v1 89 | env: 90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | with: 92 | upload_url: ${{ needs.get_release.outputs.upload_url }} 93 | asset_path: rustzx-esp32/target/${{ github.event.inputs.target }}/release/rustzx-esp32 94 | asset_name: rustzx-esp32-${{ github.event.inputs.board }} 95 | asset_content_type: 'application/octet-stream' 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | !.vscode/task.json 3 | !.vscode/launch.json 4 | /.embuild 5 | target/ 6 | build/ 7 | managed_components/ 8 | Cargo.lock 9 | cfg.toml 10 | -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | # Note: gitpod/workspace-base image references older version of CMake, it's necessary to install newer one 2 | FROM gitpod/workspace-base 3 | ENV LC_ALL=C.UTF-8 4 | ENV LANG=C.UTF-8 5 | 6 | # Set users 7 | ARG CONTAINER_USER=gitpod 8 | ARG CONTAINER_GROUP=gitpod 9 | ARG TOOLCHAIN_VERSION=1.62.0.0 10 | 11 | # Install dependencies 12 | RUN sudo install-packages git curl gcc ninja-build libudev-dev libpython2.7 \ 13 | python3 python3-pip libusb-1.0-0 libssl-dev pkg-config libtinfo5 clang 14 | 15 | USER ${CONTAINER_USER} 16 | WORKDIR /home/${CONTAINER_USER} 17 | 18 | # Install toolchain with extra crates 19 | ARG INSTALL_RUST_TOOLCHAIN=install-rust-toolchain.sh 20 | ENV PATH=${PATH}:/home/${CONTAINER_USER}/.cargo/bin:/home/${CONTAINER_USER}/opt/bin 21 | 22 | # Use LLVM installer 23 | ADD --chown=${CONTAINER_USER}:${CONTAINER_GROUP} \ 24 | https://github.com/esp-rs/rust-build/releases/download/v${TOOLCHAIN_VERSION}/${INSTALL_RUST_TOOLCHAIN} \ 25 | /home/${CONTAINER_USER}/${INSTALL_RUST_TOOLCHAIN} 26 | # Install Rust toolchain, extra crates and esp-idf 27 | RUN chmod a+x ${INSTALL_RUST_TOOLCHAIN} \ 28 | && ./${INSTALL_RUST_TOOLCHAIN} \ 29 | --extra-crates "cargo-espflash ldproxy" \ 30 | --clear-cache "YES" --export-file /home/${CONTAINER_USER}/export-rust.sh \ 31 | --esp-idf-version "release/v4.4" \ 32 | --minified-esp-idf "YES" \ 33 | --build-target "esp32" 34 | # Install web-flash and wokwi-server 35 | RUN cargo install web-flash --git https://github.com/bjoernQ/esp-web-flash-server \ 36 | && RUSTFLAGS="--cfg tokio_unstable" cargo install wokwi-server --git https://github.com/MabezDev/wokwi-server --locked 37 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # Select the proper tag: refer to https://github.com/SergioGasquez/esp-rs-container 2 | # for more information about tags 3 | image: 4 | file: .gitpod.Dockerfile 5 | tasks: 6 | - name: Setup environment variables for Rust and ESP-IDF 7 | command: | 8 | source /home/gitpod/export-rust.sh 9 | 10 | vscode: 11 | extensions: 12 | - matklad.rust-analyzer 13 | - tamasfe.even-better-toml 14 | - anwar.resourcemonitor 15 | - yzhang.markdown-all-in-one 16 | - webfreak.debug 17 | - actboy168.tasks 18 | - serayuzgur.crates 19 | ports: 20 | - port: 9012 21 | visibility: public 22 | - port: 9333 23 | visibility: public 24 | - port: 8000 25 | visibility: public 26 | onOpen: open-browser 27 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "gdb", 6 | "request": "attach", 7 | "name": "Wokwi Debug", 8 | "executable": "${workspaceFolder}/target/xtensa-esp32-espidf/debug/rustzx-esp32", 9 | "target": "localhost:9333", 10 | "remote": true, 11 | "gdbpath": "/home/${input:user}/.espressif/tools/xtensa-esp32-elf/esp-2021r2-patch3-8.4.0/xtensa-esp32-elf/bin/xtensa-esp32-elf-gdb", 12 | "cwd": "${workspaceRoot}", 13 | "stopAtConnect": true, 14 | "valuesFormatting": "parseText" 15 | } 16 | ], 17 | "inputs": [ 18 | { 19 | "type": "pickString", 20 | "id": "user", 21 | "description": "Select the user: esp for VsCode and Codespaces and gitpod for Gitpod:", 22 | "options": [ 23 | "esp", 24 | "gitpod" 25 | ], 26 | "default": "esp" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build", 8 | "type": "shell", 9 | "command": "./build-rustzx.sh ${input:buildMode}", 10 | "options": { 11 | "cwd": "${workspaceFolder}" 12 | }, 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | }, 18 | { 19 | "label": "Build & Flash", 20 | "type": "shell", 21 | "command": "./flash.sh ${input:buildMode}", 22 | "options": { 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | "group": { 26 | "kind": "test", 27 | "isDefault": true 28 | } 29 | }, 30 | { 31 | "label": "Build & Run Wokwi", 32 | "type": "shell", 33 | "command": "./run-wokwi.sh ${input:buildMode}", 34 | "options": { 35 | "cwd": "${workspaceFolder}" 36 | }, 37 | "group": { 38 | "kind": "test", 39 | "isDefault": true 40 | } 41 | }, 42 | ], 43 | "inputs": [ 44 | { 45 | "type": "pickString", 46 | "id": "buildMode", 47 | "description": "Select the build mode:", 48 | "options": [ 49 | "release", 50 | "debug" 51 | ], 52 | "default": "release" 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2019-2020 Contributors to xtensa-lx6-rt 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RustZX for ESP32 2 | 3 | Rust Bare Metal implementation of ZX Spectrum for ESP32. 4 | The project is still work in progress. 5 | 6 | Hardware (working): 7 | - ZX Spectrum with USB keyboard over ESP-NOW (wireless) 8 | - [ESP32-S3-BOX](https://github.com/espressif/esp-box), [M5Stack CoreS3](https://shop.m5stack.com/products/) or [ESP32-C6-DevKitC-1](https://docs.espressif.com/projects/espressif-esp-dev-kits/en/latest/esp32c6/esp32-c6-devkitc-1/index.html) as main emulator unit with display 9 | - [ESP32-S3-USB-OTG](https://github.com/espressif/esp-bsp/tree/master/bsp/esp32_s3_usb_otg) as USB keyboard to ESP-NOW converter (wireless) (ESP-IDF) 10 | - ZX Spectrum with UART keyboard over `espflash monitor` 11 | - [ESP32-C6-DevKitC-1](https://docs.espressif.com/projects/espressif-esp-dev-kits/en/latest/esp32c6/esp32-c6-devkitc-1/index.html) as main emulator unit with display 12 | - ZX Spectrum PS/2 keyboard over UART (wired) 13 | - [M5Stack CoreS3](https://shop.m5stack.com/products/m5stack-cores3-esp32s3-lotdevelopment-kit) as main emulator unit with display 14 | - [ESP32-C3 DevKit](https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/hw-reference/esp32c3/user-guide-devkitm-1.html) as PS/2 or USB COMBO keyboard converter to serial 15 | 16 | Hardware (work-in-progress): 17 | - [ESP32 C3 DevKit RUST](https://github.com/esp-rs/esp-rust-board) - app is not yet optimized for low memory 18 | 19 | ## ZX Spectrum with USB keyboard over ESP-NOW (wireless) 20 | 21 | ### Assembly 22 | 23 | #### Assembly of the keyboard 24 | 25 | - plug USB keyboard to ESP32-S3-USB-OTG USB HOST connector 26 | - plug USB power supply to ESP32-S3-USB-OTG USB DEV connector 27 | - plug mini-USB connector to port for flashing 28 | 29 | #### Flashing keyboard 30 | 31 | - use ESP-IDF 5.2 32 | ``` 33 | cd esp32-s3-usb-otg-keyboard 34 | idf.py build flash monitor 35 | ``` 36 | - code is based on [ESP-IDF USB HID example](https://github.com/espressif/esp-idf/tree/master/examples/peripherals/usb/host/hid) 37 | 38 | ### Assembly of the main part 39 | 40 | - connect ESP32-S3-BOX or M5Stack CoreS3 with USB-C to computer and flash the application 41 | 42 | ## Software setup 43 | 44 | - use [espup](https://github.com/esp-rs/espup) to install Rust toolchain for Xtensa (ESP32-S3) 45 | ``` 46 | espup install 47 | ``` 48 | - use [espflash](https://github.com/esp-rs/espflash) to flash binaries 49 | ``` 50 | cargo install espflash 51 | ``` 52 | - download a `.tap` file from Speccy archives and store it to `data/hello.tap` 53 | 54 | ## Run 55 | 56 | Flash and monitor the application. 57 | 58 | ESP32-S3-BOX: 59 | ``` 60 | cd esp32-s3-box 61 | cargo run --release 62 | ``` 63 | 64 | M5Stack CoreS3: 65 | ``` 66 | cd m5stack-cores3 67 | cargo run --release 68 | ``` 69 | 70 | ESP32-C6-DevKitC-1: 71 | ``` 72 | cd esp32-c6 73 | cargo run --release 74 | ``` 75 | 76 | ## ZX Spectrum with UART keyboard over `espflash monitor` 77 | 78 | ## Software setup 79 | 80 | - use [espup](https://github.com/esp-rs/espup) to install Rust toolchain for Xtensa (ESP32-S3) 81 | ``` 82 | espup install 83 | ``` 84 | - use [espflash](https://github.com/esp-rs/espflash) to flash binaries 85 | ``` 86 | cargo install espflash 87 | ``` 88 | - download a `.tap` file from Speccy archives and store it to `data/hello.tap` 89 | 90 | ## Run 91 | 92 | Flash and monitor the application. 93 | 94 | ESP32-C6-DevKitC-1: 95 | ``` 96 | cd esp32-c6 97 | cargo run --release 98 | ``` 99 | 100 | Command `cargo run --release` will turn on `espflash monitor` after the flashing. 101 | You can use the monitor console as kkeyboard output. 102 | 103 | 104 | ## ZX Spectrum PS/2 keyboard over UART (wired) 105 | 106 | ### Assembly 107 | 108 | #### Assmebly of the keyboard 109 | 110 | - flash ESP32-C3 which should serve as keyboard converter from [PS/2 or USB to serial](https://georgik.rocks/how-to-connect-usb-and-ps-2-keyboards-to-esp32-with-rust/) 111 | ``` 112 | git clone https://github.com/georgik/ps2keyboard-esp32c3.git --branch feature/serial-converter 113 | cd ps2keyboard-esp32c3 114 | cargo run --release 115 | ``` 116 | - take PS/2 keyboard and wire it to ESP32-C3 according to [PS/2 ESP32-C3 circuit](https://github.com/georgik/ps2keyboard-esp32c3/tree/feature/serial-converter?tab=readme-ov-file#circuit) 117 | - in case of USB keyboard you can skip PS/2 connector and wire[PS/2 ESP32-C3 circuit](https://github.com/georgik/ps2keyboard-esp32c3/tree/feature/serial-converter?tab=readme-ov-file#circuit) to USB connector using the schematics from [USB to PS2 convertor](https://www.instructables.com/USB-to-PS2-convertor/) 118 | - recommendation: use [wire wrapping](https://youtu.be/L-463vchW0o?si=MtQrXpbTJznikXSJ) to connect parts 119 | 120 | ### Assembly of the main part 121 | 122 | - connect ESP32-C3 keyboard converter and M5Stack CoreS3 123 | ``` 124 | GPIO4 RX (ESP32-C3 KB) - GPIO17 TX or T at Grove Port C (M5Stack CoreS3) 125 | GPIO5 TX (ESP32-C3 KB) - GPIO18 RX or R at Grove Port C (M5Stack CoreS3) 126 | GND (ESP32-C3 KB) - GND (M5Stack CoreS3) 127 | ``` 128 | 129 | ## Software setup 130 | 131 | - use [espup](https://github.com/esp-rs/espup) to install Rust toolchain for Xtensa (ESP32-S3) 132 | ``` 133 | espup install 134 | ``` 135 | - use [espflash](https://github.com/esp-rs/espflash) to flash binaries 136 | ``` 137 | cargo install espflash 138 | ``` 139 | - download a `.tap` file from Speccy archives and store it to `data/hello.tap` 140 | 141 | ## Run 142 | 143 | Flash and monitor the application: 144 | ``` 145 | cd m5stack-cores3-ps2-keyboard 146 | cargo run --release 147 | ``` 148 | 149 | Hit enter to load the tape included in the memory. 150 | 151 | ## References 152 | 153 | - RustZX wrapper code reused from https://github.com/pacmancoder/rustzx 154 | - [ESP-IDF USB HID example](https://github.com/espressif/esp-idf/tree/master/examples/peripherals/usb/host/hid) 155 | - [Rust ESP-NOW](https://github.com/esp-rs/esp-wifi) 156 | - older RustZX-ESP32 based with std on [v1.0.0-archive](https://github.com/georgik/rustzx-esp32/tree/v1.0.0-archive) 157 | -------------------------------------------------------------------------------- /data/hello.tap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgik/rustzx-esp32/88e1e466b71eb83e304bb7d1c9a77ecd855410a7/data/hello.tap -------------------------------------------------------------------------------- /data/sample.bas: -------------------------------------------------------------------------------- 1 | 10p"Hello RustZX" 2 | 40q10,10 3 | 50w100,0 4 | 60w0,100 5 | 70w-100,0 6 | 80w0,-100 7 | 90w100,100 8 | 100w-50,50 9 | 110w-50,-50 10 | 120w100,-100 11 | r 12 | 13 | -------------------------------------------------------------------------------- /docs/rustzx-esp32-diagram.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "author": "Juraj Michálek", 4 | "editor": "wokwi", 5 | "parts": [ 6 | { "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": -134.67, "left": -295.33, "attrs": {} }, 7 | { 8 | "type": "wokwi-ili9341", 9 | "id": "lcd1", 10 | "top": -167.1, 11 | "left": -54.33, 12 | "rotate": 90, 13 | "attrs": { "flipVertical": 1 } 14 | } 15 | ], 16 | "connections": [ 17 | [ "esp:TX0", "$serialMonitor:RX", "", [] ], 18 | [ "esp:RX0", "$serialMonitor:TX", "", [] ], 19 | [ "lcd1:VCC", "esp:3V3", "red", [ "h-10", "*", "v10" ] ], 20 | [ "lcd1:GND", "esp:GND.1", "black", [ "h-20", "*", "v0" ] ], 21 | [ "lcd1:CS", "esp:D15", "blue", [ "h-30", "*", "v0" ] ], 22 | [ "lcd1:RST", "esp:D4", "orange", [ "h-40", "*", "v0" ] ], 23 | [ "lcd1:D/C", "esp:D2", "green", [ "h-50", "*", "v0" ] ], 24 | [ "lcd1:SCK", "esp:D18", "gold", [ "h-60", "*", "v0"] ], 25 | [ "lcd1:LED", "esp:3V3", "green", [ "h-15", "*", "v0" ] ], 26 | [ "lcd1:MOSI", "esp:D23", "purple", [ "h-70", "*", "v0" ] ] 27 | ] 28 | } -------------------------------------------------------------------------------- /docs/rustzx-esp32-ili9341.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgik/rustzx-esp32/88e1e466b71eb83e304bb7d1c9a77ecd855410a7/docs/rustzx-esp32-ili9341.png -------------------------------------------------------------------------------- /emulator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "emulator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | esp32-hal = { version = "0.18.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 8 | esp32s2-hal = { version = "0.15.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 9 | esp32s3-hal = { version = "0.15.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 10 | esp32c3-hal = { version = "0.15.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 11 | esp32c6-hal = { version = "0.8.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 12 | #esp32h2-hal = { version = "0.5.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 13 | 14 | embassy-executor = { version = "0.5.0", package = "embassy-executor", features = ["nightly", "integrated-timers"] } 15 | embedded-graphics = "0.8.0" 16 | embedded-io-async = "0.6.1" 17 | embassy-time = { version = "0.3.0" } 18 | # hal = { package = "esp32c6-hal", version = "0.7.0" , features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 19 | log = "0.4" 20 | mipidsi = "0.7.1" 21 | rustzx-core = { git = "https://github.com/georgik/rustzx.git", branch = "feature/performance-bounding-box", features = ["embedded-roms"] } 22 | esp-display-interface-spi-dma = { version = "0.1.0" } 23 | 24 | esp-bsp = { version = "0.2.0" } 25 | graphics = { path = "../graphics" } 26 | keyboard-pipe = { path = "../keyboard-pipe" } 27 | usb-zx = { path = "../usb-zx" } 28 | 29 | [features] 30 | # default = [ "esp32" ] 31 | esp32 = [ "esp32-hal", "esp-display-interface-spi-dma/esp32" ] 32 | esp32s2 = [ "esp32s2-hal", "esp-display-interface-spi-dma/esp32s2" ] 33 | esp32s3 = [ "esp32s3-hal", "esp-display-interface-spi-dma/esp32s3" ] 34 | esp32c3 = [ "esp32c3-hal", "esp-display-interface-spi-dma/esp32c3" ] 35 | esp32c6 = [ "esp32c6-hal", "esp-display-interface-spi-dma/esp32c6" ] 36 | #esp32h2 = [ "esp32h2-hal", "esp-display-interface-spi-dma/esp32h2" ] 37 | 38 | esp32_c6_devkit_c1 = [ "esp32c6" ] 39 | esp32_s3_box = [ "esp32s3", "esp32s3-hal/opsram-8m" ] 40 | m5stack_cores3 = [ "esp32s3", "esp32s3-hal/psram-8m" ] 41 | -------------------------------------------------------------------------------- /emulator/src/host.rs: -------------------------------------------------------------------------------- 1 | extern crate alloc; 2 | use alloc::{vec, vec::Vec}; 3 | use embedded_graphics::pixelcolor::RgbColor; 4 | use log::*; 5 | 6 | use rustzx_core::host::FrameBuffer; 7 | use rustzx_core::host::FrameBufferSource; 8 | use rustzx_core::host::Host; 9 | use rustzx_core::host::HostContext; 10 | use rustzx_core::host::StubIoExtender; 11 | use rustzx_core::zx::video::colors::ZXBrightness; 12 | use rustzx_core::zx::video::colors::ZXColor; 13 | const LCD_H_RES: usize = 256; 14 | const LCD_PIXELS: usize = LCD_H_RES * 192; 15 | use crate::io::FileAsset; 16 | use crate::stopwatch::InstantStopwatch; 17 | use embedded_graphics::pixelcolor::Rgb565; 18 | 19 | use graphics::color_conv; 20 | 21 | pub(crate) struct Esp32Host {} 22 | 23 | impl Host for Esp32Host { 24 | type Context = Esp32HostContext; 25 | type EmulationStopwatch = InstantStopwatch; 26 | type FrameBuffer = EmbeddedGraphicsFrameBuffer; 27 | type TapeAsset = FileAsset; // TODO 28 | type IoExtender = StubIoExtender; 29 | // type DebugInterface = StubDebugInterface; // TODO 30 | } 31 | 32 | pub(crate) struct Esp32HostContext; 33 | 34 | impl HostContext for Esp32HostContext { 35 | fn frame_buffer_context(&self) -> <::FrameBuffer as FrameBuffer>::Context { 36 | () 37 | } 38 | } 39 | 40 | pub(crate) struct EmbeddedGraphicsFrameBuffer { 41 | buffer: Vec, 42 | buffer_width: usize, 43 | pub bounding_box_top_left: Option<(usize, usize)>, 44 | pub bounding_box_bottom_right: Option<(usize, usize)>, 45 | } 46 | 47 | impl EmbeddedGraphicsFrameBuffer { 48 | // pub fn get_pixel_iter(&self) -> impl Iterator + '_ { 49 | // self.buffer.iter().copied() 50 | // } 51 | 52 | fn mark_dirty(&mut self, x: usize, y: usize) { 53 | let (min_x, min_y) = self.bounding_box_top_left.unwrap_or((x, y)); 54 | let (max_x, max_y) = self.bounding_box_bottom_right.unwrap_or((x, y)); 55 | 56 | self.bounding_box_top_left = Some((min_x.min(x), min_y.min(y))); 57 | self.bounding_box_bottom_right = Some((max_x.max(x), max_y.max(y))); 58 | } 59 | 60 | pub fn get_region_pixel_iter( 61 | &self, 62 | top_left: (usize, usize), 63 | bottom_right: (usize, usize), 64 | ) -> impl Iterator + '_ { 65 | let start_x = top_left.0; 66 | let end_x = bottom_right.0 + 1; // Include the pixel at bottom_right coordinates 67 | let start_y = top_left.1; 68 | let end_y = bottom_right.1 + 1; // Include the pixel at bottom_right coordinates 69 | 70 | (start_y..end_y).flat_map(move |y| { 71 | (start_x..end_x).map(move |x| self.buffer[y * self.buffer_width + x]) 72 | }) 73 | } 74 | } 75 | 76 | impl FrameBuffer for EmbeddedGraphicsFrameBuffer { 77 | type Context = (); 78 | 79 | fn new( 80 | width: usize, 81 | height: usize, 82 | source: FrameBufferSource, 83 | _context: Self::Context, 84 | ) -> Self { 85 | info!("Allocation"); 86 | match source { 87 | FrameBufferSource::Screen => { 88 | info!("Allocating frame buffer width={}, height={}", width, height); 89 | 90 | Self { 91 | buffer: vec![Rgb565::RED; LCD_PIXELS], 92 | buffer_width: LCD_H_RES as usize, 93 | bounding_box_bottom_right: None, 94 | bounding_box_top_left: None, 95 | } 96 | } 97 | FrameBufferSource::Border => Self { 98 | buffer: vec![Rgb565::WHITE; 1], 99 | buffer_width: 1, 100 | bounding_box_bottom_right: None, 101 | bounding_box_top_left: None, 102 | }, 103 | } 104 | } 105 | 106 | fn set_color(&mut self, x: usize, y: usize, zx_color: ZXColor, zx_brightness: ZXBrightness) { 107 | let index = y * self.buffer_width + x; 108 | let new_color = color_conv(&zx_color, zx_brightness); 109 | if self.buffer[index] != new_color { 110 | self.buffer[index] = new_color; 111 | self.mark_dirty(x, y); // Update the bounding box 112 | } 113 | } 114 | 115 | fn set_colors(&mut self, x: usize, y: usize, colors: [ZXColor; 8], brightness: ZXBrightness) { 116 | for (i, &color) in colors.iter().enumerate() { 117 | self.set_color(x + i, y, color, brightness); 118 | } 119 | } 120 | 121 | fn reset_bounding_box(&mut self) { 122 | self.bounding_box_bottom_right = None; 123 | self.bounding_box_top_left = None; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /emulator/src/io.rs: -------------------------------------------------------------------------------- 1 | // no_std implementation of File concept using memory buffer 2 | use rustzx_core::{ 3 | error::IoError, 4 | host::{LoadableAsset, SeekFrom, SeekableAsset}, 5 | }; 6 | 7 | #[derive(Debug)] 8 | pub enum FileAssetError { 9 | ReadError, 10 | SeekError, 11 | } 12 | 13 | pub struct FileAsset { 14 | data: &'static [u8], 15 | position: usize, 16 | } 17 | 18 | impl FileAsset { 19 | pub fn new(data: &'static [u8]) -> Self { 20 | FileAsset { data, position: 0 } 21 | } 22 | 23 | // Helper methods to convert FileAssetError to IoError if necessary 24 | fn convert_error(error: FileAssetError) -> IoError { 25 | match error { 26 | FileAssetError::ReadError => IoError::HostAssetImplFailed, 27 | FileAssetError::SeekError => IoError::SeekBeforeStart, 28 | } 29 | } 30 | } 31 | 32 | impl SeekableAsset for FileAsset { 33 | fn seek(&mut self, _pos: SeekFrom) -> Result { 34 | todo!() 35 | } 36 | } 37 | 38 | impl LoadableAsset for FileAsset { 39 | fn read(&mut self, buf: &mut [u8]) -> Result { 40 | let available = self.data.len() - self.position; // Bytes remaining 41 | let to_read = available.min(buf.len()); // Number of bytes to read 42 | 43 | if to_read == 0 { 44 | return Err(FileAsset::convert_error(FileAssetError::ReadError)); 45 | } 46 | 47 | buf[..to_read].copy_from_slice(&self.data[self.position..self.position + to_read]); 48 | self.position += to_read; // Update the position 49 | 50 | Ok(to_read) // Return the number of bytes read 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /emulator/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![feature(type_alias_impl_trait)] 3 | 4 | #[cfg(feature = "esp32")] 5 | pub use esp32_hal as hal; 6 | #[cfg(feature = "esp32c2")] 7 | pub use esp32c2_hal as hal; 8 | #[cfg(feature = "esp32c3")] 9 | pub use esp32c3_hal as hal; 10 | #[cfg(feature = "esp32c6")] 11 | pub use esp32c6_hal as hal; 12 | #[cfg(feature = "esp32h2")] 13 | pub use esp32h2_hal as hal; 14 | #[cfg(feature = "esp32s2")] 15 | pub use esp32s2_hal as hal; 16 | #[cfg(feature = "esp32s3")] 17 | pub use esp32s3_hal as hal; 18 | 19 | pub mod host; 20 | pub mod io; 21 | pub mod stopwatch; 22 | 23 | use rustzx_core::{host::Host, zx::machine::ZXMachine, EmulationMode, Emulator, RustzxSettings}; 24 | 25 | use log::{debug, error, info}; 26 | 27 | use keyboard_pipe::PIPE; 28 | 29 | use embassy_time::{Duration, Timer}; 30 | 31 | use esp_bsp::{define_display_type, BoardType}; 32 | 33 | use embedded_graphics::{ 34 | mono_font::{ascii::FONT_8X13, MonoTextStyle}, 35 | prelude::{Point, RgbColor}, 36 | text::Text, 37 | Drawable, 38 | }; 39 | 40 | use esp_display_interface_spi_dma::display_interface_spi_dma; 41 | use hal::gpio::{GpioPin, Output}; 42 | use hal::spi::FullDuplexMode; 43 | 44 | use usb_zx::{usb_zx_key::usb_code_to_zxkey, zx_event::Event}; 45 | 46 | use crate::io::FileAsset; 47 | 48 | #[cfg(feature = "esp32_c6_devkit_c1")] 49 | type AppDisplay = define_display_type!(BoardType::ESP32C6DevKitC1); 50 | #[cfg(feature = "m5stack_cores3")] 51 | type AppDisplay = define_display_type!(BoardType::M5StackCoreS3); 52 | #[cfg(feature = "esp32_s3_box")] 53 | type AppDisplay = define_display_type!(BoardType::ESP32S3Box); 54 | 55 | const SCREEN_OFFSET_X: u16 = (320 - 256) / 2; 56 | const SCREEN_OFFSET_Y: u16 = (240 - 192) / 2; 57 | 58 | fn handle_key_event( 59 | key_state: u8, 60 | modifier: u8, 61 | key_code: u8, 62 | emulator: &mut Emulator, 63 | ) { 64 | let is_pressed = key_state == 0; 65 | if let Some(mapped_key) = usb_code_to_zxkey(is_pressed, modifier, key_code) { 66 | match mapped_key { 67 | Event::ZXKey(k, p) => { 68 | debug!("-> ZXKey"); 69 | emulator.send_key(k, p); 70 | } 71 | Event::NoEvent => { 72 | error!("Key not implemented"); 73 | } 74 | Event::ZXKeyWithModifier(k, k2, p) => { 75 | debug!("-> ZXKeyWithModifier"); 76 | emulator.send_key(k, p); 77 | emulator.send_key(k2, p); 78 | } 79 | } 80 | } else { 81 | info!("Mapped key: NoEvent"); 82 | } 83 | } 84 | 85 | #[embassy_executor::task] 86 | pub async fn app_loop(mut display: AppDisplay) 87 | //-> Result<(), core::fmt::Error> 88 | { 89 | // let _ = lcd_backlight.set_high(); 90 | 91 | Timer::after(Duration::from_millis(500)).await; 92 | 93 | info!("Initializing..."); 94 | Text::new( 95 | "Initializing...", 96 | Point::new(80, 110), 97 | MonoTextStyle::new(&FONT_8X13, RgbColor::WHITE), 98 | ) 99 | .draw(&mut display) 100 | .unwrap(); 101 | 102 | info!("Initialized"); 103 | 104 | // display 105 | // .clear(color_conv(ZXColor::Blue, ZXBrightness::Normal)) 106 | // .map_err(|err| error!("{:?}", err)) 107 | // .ok(); 108 | 109 | info!("Creating emulator"); 110 | 111 | let settings = RustzxSettings { 112 | machine: ZXMachine::Sinclair128K, 113 | // machine: ZXMachine::Sinclair48K, 114 | emulation_mode: EmulationMode::FrameCount(1), 115 | tape_fastload_enabled: true, 116 | kempston_enabled: false, 117 | mouse_enabled: false, 118 | load_default_rom: true, 119 | }; 120 | 121 | info!("Initialize emulator"); 122 | const MAX_FRAME_DURATION: core::time::Duration = core::time::Duration::from_millis(0); 123 | 124 | let mut emulator: Emulator = 125 | match Emulator::new(settings, host::Esp32HostContext {}) { 126 | Ok(emulator) => emulator, 127 | Err(err) => { 128 | error!("Error creating emulator: {:?}", err); 129 | return; 130 | } 131 | }; 132 | 133 | info!("Loading tape"); 134 | let tape_bytes = include_bytes!("../../data/hello.tap"); 135 | let tape_asset = FileAsset::new(tape_bytes); 136 | let _ = emulator.load_tape(rustzx_core::host::Tape::Tap(tape_asset)); 137 | 138 | info!("Entering emulator loop"); 139 | let mut last_modifier: u8 = 0; 140 | 141 | loop { 142 | match emulator.emulate_frames(MAX_FRAME_DURATION) { 143 | Ok(_) => { 144 | let framebuffer = emulator.screen_buffer(); 145 | if let (Some(top_left), Some(bottom_right)) = ( 146 | framebuffer.bounding_box_top_left, 147 | framebuffer.bounding_box_bottom_right, 148 | ) { 149 | // let width = bottom_right.0 - top_left.0 + 1; // Calculate width 150 | // let height = bottom_right.1 - top_left.1 + 1; // Calculate height 151 | // debug!("Bounding box: {:?} {:?}", top_left, bottom_right); 152 | // debug!("Bounding box size: {}", width * height); 153 | let pixel_iterator = framebuffer.get_region_pixel_iter(top_left, bottom_right); 154 | let _ = display.set_pixels( 155 | top_left.0 as u16 + SCREEN_OFFSET_X, 156 | top_left.1 as u16 + SCREEN_OFFSET_Y, 157 | bottom_right.0 as u16 + SCREEN_OFFSET_X, 158 | bottom_right.1 as u16 + SCREEN_OFFSET_Y, 159 | pixel_iterator, 160 | ); 161 | } 162 | emulator.reset_bounding_box(); 163 | } 164 | _ => { 165 | error!("Emulation of frame failed"); 166 | } 167 | } 168 | 169 | // Read 3 bytes from PIPE if available 170 | if PIPE.len() >= 3 { 171 | let mut bytes = [0u8; 3]; 172 | let bytes_read = PIPE.read(&mut bytes).await; 173 | info!("Bytes read from pipe: {}", bytes_read); 174 | let (key_state, modifier, key_code) = (bytes[0], bytes[1], bytes[2]); 175 | 176 | // USB Keyaboards send a key up event with modifier 0 when a modifier key is released 177 | // We need to keep track of the last modifier key pressed to know if we should send a key up event 178 | if (key_state == 1) && (modifier == 0) { 179 | handle_key_event(key_state, last_modifier, key_code, &mut emulator); 180 | } else { 181 | handle_key_event(key_state, modifier, key_code, &mut emulator); 182 | last_modifier = modifier; 183 | } 184 | } 185 | 186 | Timer::after(Duration::from_millis(5)).await; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /emulator/src/stopwatch.rs: -------------------------------------------------------------------------------- 1 | use rustzx_core::host::Stopwatch; 2 | // use std::time::{Duration, Instant}; 3 | use core::time::Duration; 4 | 5 | pub struct InstantStopwatch { 6 | // timestamp: Instant, 7 | } 8 | 9 | impl Default for InstantStopwatch { 10 | fn default() -> Self { 11 | Self { 12 | // timestamp: Instant::now(), 13 | } 14 | } 15 | } 16 | 17 | impl Stopwatch for InstantStopwatch { 18 | fn new() -> Self { 19 | Self::default() 20 | } 21 | 22 | fn measure(&self) -> Duration { 23 | // self.timestamp.elapsed() 24 | Duration::from_millis(100) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /esp-now-keyboard/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esp-now-keyboard" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | esp32-hal = { version = "0.18.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 8 | esp32s2-hal = { version = "0.15.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 9 | esp32s3-hal = { version = "0.15.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread" ] } 10 | esp32c3-hal = { version = "0.15.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 11 | esp32c6-hal = { version = "0.8.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 12 | esp32h2-hal = { version = "0.6.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 13 | 14 | embassy-executor = { version = "0.5.0", package = "embassy-executor", features = ["nightly", "integrated-timers"] } 15 | embedded-io-async = "0.6.1" 16 | embassy-time = { version = "0.3.0" } 17 | esp-wifi = { version = "0.3.0", features = [ "wifi", "utils", "tcp", "smoltcp", "esp-now"] } 18 | # hal = { package = "esp32c6-hal", version = "0.7.0" , features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 19 | keyboard-pipe = { path = "../keyboard-pipe" } 20 | log = "0.4" 21 | usb-zx = { path = "../usb-zx" } 22 | 23 | [features] 24 | # default = [ "esp32" ] 25 | esp32 = [ "esp32-hal", "esp-wifi/esp32" ] 26 | esp32s2 = [ "esp32s2-hal", "esp-wifi/esp32s2" ] 27 | esp32s3 = [ "esp32s3-hal", "esp-wifi/esp32s3" ] 28 | esp32c3 = [ "esp32c3-hal", "esp-wifi/esp32c3" ] 29 | esp32c6 = [ "esp32c6-hal", "esp-wifi/esp32c6" ] 30 | esp32h2 = [ "esp32h2-hal", "esp-wifi/esp32h2" ] 31 | 32 | #[patch.crates-io] 33 | #embassy-executor = { git = "https://github.com/embassy-rs/embassy.git", package = "embassy-executor", rev = "14f41a71b6ea9dedb4ee5b9c741fe10575772c7d"} 34 | #embassy-executor-macros = { git = "https://github.com/embassy-rs/embassy.git", package = "embassy-executor-macros", rev = "14f41a71b6ea9dedb4ee5b9c741fe10575772c7d"} 35 | #embassy-time = { git = "https://github.com/embassy-rs/embassy.git", package = "embassy-time", rev = "14f41a71b6ea9dedb4ee5b9c741fe10575772c7d"} 36 | 37 | -------------------------------------------------------------------------------- /esp-now-keyboard/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![feature(type_alias_impl_trait)] 3 | 4 | #[cfg(feature = "esp32")] 5 | pub use esp32_hal as hal; 6 | #[cfg(feature = "esp32c2")] 7 | pub use esp32c2_hal as hal; 8 | #[cfg(feature = "esp32c3")] 9 | pub use esp32c3_hal as hal; 10 | #[cfg(feature = "esp32c6")] 11 | pub use esp32c6_hal as hal; 12 | #[cfg(feature = "esp32h2")] 13 | pub use esp32h2_hal as hal; 14 | #[cfg(feature = "esp32s2")] 15 | pub use esp32s2_hal as hal; 16 | #[cfg(feature = "esp32s3")] 17 | pub use esp32s3_hal as hal; 18 | 19 | use embassy_time::{Duration, Ticker, Timer}; 20 | use esp_wifi::esp_now::{EspNow, EspNowError, PeerInfo}; 21 | use hal::embassy; 22 | use keyboard_pipe::PIPE; 23 | use log::{error, info}; 24 | use usb_zx::{ 25 | uart_usb_key::{uart_code_to_usb_key, uart_composite_code_to_usb_key}, 26 | usb_zx_key::usb_code_to_zxkey, 27 | zx_event::Event, 28 | }; 29 | 30 | const ESP_NOW_PAYLOAD_INDEX: usize = 20; 31 | 32 | #[embassy_executor::task] 33 | pub async fn esp_now_receiver(esp_now: EspNow<'static>) { 34 | info!("ESP-NOW receiver task"); 35 | let peer_info = PeerInfo { 36 | // Specify a unique peer address here (replace with actual address) 37 | peer_address: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], 38 | lmk: None, 39 | channel: Some(1), // Specify the channel if known 40 | encrypt: false, // Set to true if encryption is needed 41 | }; 42 | 43 | // Check if the peer already exists 44 | if !esp_now.peer_exists(&peer_info.peer_address) { 45 | info!("Adding peer"); 46 | match esp_now.add_peer(peer_info) { 47 | Ok(_) => info!("Peer added"), 48 | Err(e) => error!("Peer add error: {:?}", e), 49 | } 50 | } else { 51 | info!("Peer already exists, not adding"); 52 | } 53 | 54 | loop { 55 | let received_data = esp_now.receive(); 56 | match received_data { 57 | Some(data) => { 58 | let bytes = data.data; 59 | info!( 60 | "Key code received over ESP-NOW: state = {:?}, modifier = {:?}, key = {:?}", 61 | bytes[ESP_NOW_PAYLOAD_INDEX], 62 | bytes[ESP_NOW_PAYLOAD_INDEX + 1], 63 | bytes[ESP_NOW_PAYLOAD_INDEX + 2] 64 | ); 65 | let bytes_written = PIPE 66 | .write(&[ 67 | bytes[ESP_NOW_PAYLOAD_INDEX], 68 | bytes[ESP_NOW_PAYLOAD_INDEX + 1], 69 | bytes[ESP_NOW_PAYLOAD_INDEX + 2], 70 | ]) 71 | .await; 72 | if bytes_written != 3 { 73 | error!("Failed to write to pipe"); 74 | break; 75 | } 76 | } 77 | None => { 78 | //error!("ESP-NOW receive error"); 79 | } 80 | } 81 | Timer::after(Duration::from_millis(5)).await; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /esp32-c3-devkit-rust/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.riscv32imac-unknown-none-elf] 2 | runner = "espflash flash --monitor" 3 | #runner = "probe-rs run --chip esp32c3 --format idf" 4 | 5 | rustflags = [ 6 | "-C", "link-arg=-Tlinkall.x", 7 | # Required to obtain backtraces (e.g. when using the "esp-backtrace" crate.) 8 | # NOTE: May negatively impact performance of produced code 9 | "-C", "force-frame-pointers", 10 | 11 | # comment the cfgs below if you do _not_ wish to emulate atomics. 12 | # enable the atomic codegen option for RISCV 13 | "-C", "target-feature=+a", 14 | # tell the core library have atomics even though it's not specified in the target definition 15 | "--cfg", "target_has_atomic_load_store", 16 | "--cfg", 'target_has_atomic_load_store="8"', 17 | "--cfg", 'target_has_atomic_load_store="16"', 18 | "--cfg", 'target_has_atomic_load_store="32"', 19 | "--cfg", 'target_has_atomic_load_store="ptr"', 20 | # enable cas 21 | "--cfg", "target_has_atomic", 22 | "--cfg", 'target_has_atomic="8"', 23 | "--cfg", 'target_has_atomic="16"', 24 | "--cfg", 'target_has_atomic="32"', 25 | "--cfg", 'target_has_atomic="ptr"', 26 | ] 27 | 28 | [env] 29 | # Use clean build after changing ESP_LOGLEVEL 30 | ESP_LOGLEVEL="DEBUG" 31 | 32 | [build] 33 | target = "riscv32imac-unknown-none-elf" 34 | 35 | [unstable] 36 | build-std = [ "core", "alloc" ] 37 | -------------------------------------------------------------------------------- /esp32-c3-devkit-rust/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "probe-rs-debug", 6 | "request": "launch", 7 | "name": "Flash & Debug with probe-rs", 8 | "cwd": "${workspaceFolder}", 9 | "chip": "esp32c3", 10 | "flashingConfig": { 11 | "flashingEnabled": true, 12 | "resetAfterFlashing": true, 13 | "haltAfterReset": true, 14 | "formatOptions": { 15 | "format": "idf" 16 | } 17 | }, 18 | "coreConfigs": [ 19 | { 20 | "coreIndex": 0, 21 | "programBinary": "${workspaceFolder}/target/riscv32imac-unknown-none-elf/debug/rustzx-esp32-c3-devkit-rust", 22 | "rttEnabled": true, 23 | "rttChannelFormats": [ 24 | { 25 | "channelNumer": "0", 26 | "dataFormat": "String", 27 | "showTimestamp": true 28 | } 29 | ] 30 | } 31 | ] 32 | }, 33 | { 34 | "type": "probe-rs-debug", 35 | "request": "attach", 36 | "name": "Attach to probe-rs debugger", 37 | "cwd": "${workspaceFolder}", 38 | "chip": "esp32c3", 39 | "coreConfigs": [ 40 | { 41 | "coreIndex": 0, 42 | "programBinary": "${workspaceFolder}/target/riscv32imac-unknown-none-elf/debug/spooky-esp32-c3", 43 | "rttEnabled": true, 44 | "rttChannelFormats": [ 45 | { 46 | "channelNumer": "0", 47 | "dataFormat": "String", 48 | "showTimestamp": true, 49 | } 50 | ] 51 | } 52 | ] 53 | }, 54 | { 55 | // more info at: https://github.com/Marus/cortex-debug/blob/master/package.json 56 | "name": "Attach to OpenOCD", 57 | "type": "cortex-debug", 58 | "request": "attach", 59 | "cwd": "${workspaceRoot}", 60 | "executable": "${workspaceFolder}/target/riscv32imac-unknown-none-elf/debug/rustzx-esp32-c3-devkit-rust", 61 | "servertype": "openocd", 62 | "interface": "jtag", 63 | "toolchainPrefix": "riscv32-esp-elf", 64 | "openOCDPreConfigLaunchCommands": ["set ESP_RTOS none"], 65 | "serverpath": "openocd", 66 | "configFiles": ["board/esp32c3-builtin.cfg"], 67 | "gdbPath": "${userHome}/.espressif/tools/riscv32-esp-elf-gdb/12.1_20221002/riscv32-esp-elf-gdb/bin/riscv32-esp-elf-gdb", 68 | "overrideAttachCommands": [ 69 | "set remote hardware-watchpoint-limit 2", 70 | "mon halt", 71 | "flushregs" 72 | ], 73 | "overrideRestartCommands": ["mon reset halt", "flushregs", "c"] 74 | }, 75 | { 76 | "name": "Attach to Wokwi GDB", 77 | "type": "cppdbg", 78 | "request": "launch", 79 | "program": "${workspaceFolder}/target/riscv32imac-unknown-none-elf/debug/rustzx-esp32-c3-devkit-rust", 80 | "cwd": "${workspaceFolder}", 81 | "MIMode": "gdb", 82 | "miDebuggerPath": "${userHome}/.espressif/tools/riscv32-esp-elf-gdb/12.1_20221002/riscv32-esp-elf-gdb/bin/riscv32-esp-elf-gdb", 83 | "miDebuggerServerAddress": "localhost:3333" 84 | } 85 | ] 86 | } 87 | 88 | -------------------------------------------------------------------------------- /esp32-c3-devkit-rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustzx-esp32-c3-devkit-rust" 3 | version = "0.3.0" 4 | authors = ["Juraj Michálek "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | [target.riscv32imac-unknown-none-elf.dependencies] 9 | hal = { package = "esp32c3-hal", version = "0.13.0" } 10 | esp-backtrace = { version = "0.9.0", features = [ 11 | "esp32c3", 12 | "panic-handler", 13 | "print-uart", 14 | ] } 15 | esp-println = { version = "0.7.0", default-features = false, features = [ "esp32c3", "log" ] } 16 | 17 | [dependencies] 18 | critical-section = { version = "1.1.2" } 19 | display-interface = "0.4" 20 | esp-alloc = "0.3.0" 21 | embedded-graphics = "0.8.0" 22 | embedded-hal = "0.2" 23 | embedded-graphics-framebuf = { version = "0.3.0", git = "https://github.com/georgik/embedded-graphics-framebuf.git", branch = "feature/embedded-graphics-0.8" } 24 | icm42670 = { git = "https://github.com/jessebraham/icm42670/" } 25 | log = "0.4" 26 | mipidsi = "0.7.1" 27 | panic-halt = "0.2" 28 | shared-bus = { version = "0.3.0" } 29 | spooky-core = { git = "https://github.com/georgik/esp32-spooky-maze-game.git", rev = "ad47396", default-features = false, features = ["static_maze"]} 30 | spooky-embedded = { git = "https://github.com/georgik/esp32-spooky-maze-game.git", rev = "ad47396", default-features = false, features = [ "esp32c3", "static_maze", "resolution_320x240" ] } 31 | spi-dma-displayinterface = { git = "https://github.com/georgik/esp32-spooky-maze-game.git", rev = "ad47396", features = ["esp32c3"] } 32 | rustzx-utils = { version = "0.16.0" } 33 | rustzx-core = { version = "0.16.0", features = ["embedded-roms"] } 34 | -------------------------------------------------------------------------------- /esp32-c3-devkit-rust/diagram.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "author": "Juraj Michálek", 4 | "editor": "wokwi", 5 | "parts": [ 6 | { 7 | "type": "board-esp32-c3-rust-1", 8 | "id": "esp", 9 | "top": -494.32, 10 | "left": -455.03, 11 | "attrs": { "builder": "rust-nostd-esp" } 12 | }, 13 | { 14 | "type": "wokwi-ili9341", 15 | "id": "lcd1", 16 | "top": -541.8, 17 | "left": -132.7, 18 | "rotate": 90, 19 | "attrs": { "flipVertical": "1" } 20 | }, 21 | { "type": "chip-icm42670p", "id": "imu1", "top": -248.58, "left": -302.4, "attrs": {} } 22 | ], 23 | "connections": [ 24 | [ "esp:21", "$serialMonitor:RX", "", [] ], 25 | [ "esp:20", "$serialMonitor:TX", "", [] ], 26 | [ "esp:3V3", "lcd1:VCC", "green", [] ], 27 | [ "esp:GND.1", "lcd1:GND", "black", [] ], 28 | [ "esp:0", "lcd1:SCK", "blue", [] ], 29 | [ "esp:6", "lcd1:MOSI", "orange", [] ], 30 | [ "esp:5", "lcd1:CS", "red", [] ], 31 | [ "esp:4", "lcd1:D/C", "magenta", [] ], 32 | [ "esp:3", "lcd1:RST", "yellow", [] ], 33 | [ "lcd1:LED", "esp:3V3", "white", [] ], 34 | [ "imu1:SDA", "esp:10", "green", [ "v0" ] ], 35 | [ "imu1:SCL", "esp:8.2", "green", [ "v0" ] ], 36 | [ "imu1:VCC", "esp:3V3", "red", [ "v0" ] ], 37 | [ "imu1:GND", "esp:GND", "black", [ "v-192", "h-211.12" ] ] 38 | ], 39 | "serialMonitor": { "display": "terminal", "newline": "lf", "convertEol": true }, 40 | "dependencies": { 41 | "chip-icm42670p": "github:SergioGasquez/wokwi-icm42670p@0.0.4" 42 | } 43 | } -------------------------------------------------------------------------------- /esp32-c3-devkit-rust/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | components = ["rustfmt", "rustc-dev"] 4 | # targets = ["xtensa-esp32s3-none-elf"] 5 | # targets = ["xtensa-esp32-none-elf", "xtensa-esp32s2-none-elf","xtensa-esp32s3-none-elf"] 6 | -------------------------------------------------------------------------------- /esp32-c3-devkit-rust/src/host.rs: -------------------------------------------------------------------------------- 1 | use embedded_hal::can::Frame; 2 | use log::*; 3 | 4 | use embedded_graphics::prelude::*; 5 | use embedded_graphics::primitives::Rectangle; 6 | 7 | use rustzx_core::host::FrameBuffer; 8 | use rustzx_core::host::FrameBufferSource; 9 | use rustzx_core::host::Host; 10 | use rustzx_core::host::HostContext; 11 | use rustzx_core::host::StubIoExtender; 12 | use rustzx_core::zx::video::colors::ZXBrightness; 13 | use rustzx_core::zx::video::colors::ZXColor; 14 | // use spooky_embedded::embedded_display::LCD_H_RES; 15 | // use spooky_embedded::embedded_display::LCD_PIXELS; 16 | const LCD_H_RES: usize = 256; 17 | const LCD_PIXELS: usize = LCD_H_RES*1; 18 | // use rustzx_utils::io::FileAsset; 19 | // use rustzx_utils::stopwatch::InstantStopwatch; 20 | use crate::stopwatch::InstantStopwatch; 21 | use crate::io::FileAsset; 22 | use crate::spritebuf::SpriteBuf; 23 | 24 | use embedded_graphics_framebuf::FrameBuf; 25 | use embedded_graphics_framebuf::backends::{DMACapableFrameBufferBackend, FrameBufferBackend}; 26 | use embedded_graphics::pixelcolor::Rgb565; 27 | 28 | use display_interface::WriteOnlyDataCommand; 29 | use embedded_hal::digital::v2::OutputPin; 30 | 31 | pub(crate) struct Esp32Host 32 | { 33 | } 34 | 35 | impl Host for Esp32Host 36 | { 37 | type Context = Esp32HostContext; 38 | type EmulationStopwatch = InstantStopwatch; 39 | type FrameBuffer = EmbeddedGraphicsFrameBuffer; 40 | type TapeAsset = FileAsset; // TODO 41 | type IoExtender = StubIoExtender; 42 | type DebugInterface = StubDebugInterface; // TODO 43 | } 44 | 45 | pub(crate) struct Esp32HostContext; 46 | 47 | impl HostContext for Esp32HostContext 48 | { 49 | fn frame_buffer_context(&self) -> <::FrameBuffer as FrameBuffer>::Context { 50 | () 51 | } 52 | } 53 | 54 | pub(crate) struct EmbeddedGraphicsFrameBuffer { 55 | buffer: [ZXColor; LCD_PIXELS], 56 | buffer_width: usize, 57 | // changed: RefCell>, 58 | } 59 | 60 | use crate::color_conv; 61 | impl EmbeddedGraphicsFrameBuffer { 62 | pub fn get_pixel_iter(&self) -> impl Iterator + '_ { 63 | self.buffer.into_iter().map(|zh_color| color_conv(zh_color, ZXBrightness::Normal)) 64 | } 65 | } 66 | 67 | // impl EmbeddedGraphicsFrameBuffer { 68 | // pub(crate) fn blit( 69 | // &self, 70 | // display: &mut D, 71 | // color_conv: fn(ZXColor, ZXBrightness) -> D::Color, 72 | // ) -> Result<(), D::Error> { 73 | 74 | // let mut changed = self.changed.borrow_mut(); 75 | 76 | // let mut y = 0_usize; 77 | // while y < changed.len() { 78 | // let mut yoff = y; 79 | // while yoff < changed.len() && changed[yoff] { 80 | // changed[yoff] = false; 81 | // yoff += 1; 82 | 83 | // break; // TODO: Seems there is a bug with multiple rows 84 | // } 85 | 86 | // if y < yoff { 87 | // display.fill_contiguous( 88 | // &Rectangle::new( 89 | // Point::new(0, y as i32), 90 | // Size::new(self.buffer_width as u32, (yoff - y) as u32), 91 | // ), 92 | // self.buffer[y * self.buffer_width..yoff * self.buffer_width] 93 | // .iter() 94 | // .map(|zh_color| color_conv(*zh_color, ZXBrightness::Normal)), 95 | // )?; 96 | 97 | // y = yoff; 98 | // } else { 99 | // y += 1; 100 | // } 101 | // } 102 | 103 | // Ok(()) 104 | // } 105 | // } 106 | 107 | impl FrameBuffer for EmbeddedGraphicsFrameBuffer { 108 | type Context = (); 109 | 110 | fn new( 111 | width: usize, 112 | height: usize, 113 | source: FrameBufferSource, 114 | _context: Self::Context, 115 | ) -> Self { 116 | info!("Allocation"); 117 | match source { 118 | FrameBufferSource::Screen => { 119 | info!("Allocating frame buffer width={}, height={}", width, height); 120 | 121 | Self { 122 | buffer: [ZXColor::Red; LCD_PIXELS], 123 | buffer_width: LCD_H_RES as usize, 124 | // changed: RefCell::new(vec![true; height]), 125 | } 126 | } 127 | // FrameBufferSource::Border => todo!("Border frame buffer not implemented"), 128 | FrameBufferSource::Border => Self { 129 | buffer: [ZXColor::White; LCD_PIXELS], 130 | buffer_width: LCD_H_RES as usize, 131 | // changed: RefCell::new(Vec::new()), 132 | }, 133 | } 134 | } 135 | 136 | fn set_color( 137 | &mut self, 138 | x: usize, 139 | y: usize, 140 | color: ZXColor, 141 | _brightness: ZXBrightness, /*TODO*/ 142 | ) { 143 | if self.buffer_width > 0 { 144 | let pixel = &mut self.buffer[y * self.buffer_width + x]; 145 | if *pixel as u8 != color as u8 { 146 | *pixel = color; 147 | // self.changed.borrow_mut()[y] = true; 148 | } 149 | } 150 | } 151 | } 152 | 153 | pub struct StubDebugInterface; 154 | use rustzx_core::host::DebugInterface; 155 | 156 | impl DebugInterface for StubDebugInterface { 157 | fn check_pc_breakpoint(&mut self, _addr: u16) -> bool { 158 | // In a stub implementation, you can simply return false 159 | // to indicate that no breakpoints are set. 160 | false 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /esp32-c3-devkit-rust/src/io.rs: -------------------------------------------------------------------------------- 1 | // no_std implementation of File concept using memory buffer 2 | use rustzx_core::{ 3 | error::IoError, 4 | host::{SeekableAsset, LoadableAsset, SeekFrom} 5 | }; 6 | 7 | #[derive(Debug)] 8 | pub enum FileAssetError { 9 | ReadError, 10 | SeekError, 11 | } 12 | 13 | pub struct FileAsset { 14 | data: &'static [u8], 15 | } 16 | 17 | impl FileAsset { 18 | pub fn new(data: &'static [u8]) -> Self { 19 | FileAsset { data } 20 | } 21 | 22 | // Helper methods to convert FileAssetError to IoError if necessary 23 | fn convert_error(error: FileAssetError) -> IoError { 24 | match error { 25 | FileAssetError::ReadError => IoError::HostAssetImplFailed, 26 | FileAssetError::SeekError => IoError::SeekBeforeStart, 27 | } 28 | } 29 | } 30 | 31 | impl SeekableAsset for FileAsset { 32 | fn seek(&mut self, pos: SeekFrom) -> Result { 33 | todo!() 34 | } 35 | } 36 | 37 | impl LoadableAsset for FileAsset { 38 | fn read(&mut self, buf: &mut [u8]) -> Result { 39 | todo!() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /esp32-c3-devkit-rust/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use spi_dma_displayinterface::spi_dma_displayinterface; 5 | 6 | use embedded_graphics::{ 7 | mono_font::{ascii::FONT_8X13, MonoTextStyle}, 8 | prelude::{Point, RgbColor}, 9 | text::Text, 10 | Drawable, 11 | }; 12 | 13 | use esp_println::println; 14 | use core::cell::RefCell; 15 | 16 | use hal::{ 17 | assist_debug::DebugAssist, 18 | clock::{ClockControl, CpuClock}, 19 | dma::DmaPriority, 20 | gdma::Gdma, 21 | i2c, 22 | interrupt, 23 | peripherals::{ 24 | Peripherals, 25 | Interrupt 26 | }, 27 | prelude::*, 28 | spi::{ 29 | master::{prelude::*, Spi}, 30 | SpiMode, 31 | }, 32 | Delay, 33 | Rng, 34 | IO 35 | }; 36 | 37 | // use spooky_embedded::app::app_loop; 38 | 39 | use spooky_embedded::{ 40 | embedded_display::{LCD_H_RES, LCD_V_RES, LCD_MEMORY_SIZE}, 41 | controllers::{accel::AccelMovementController, composites::accel_composite::AccelCompositeController} 42 | }; 43 | 44 | use esp_backtrace as _; 45 | 46 | use icm42670::{Address, Icm42670}; 47 | use shared_bus::BusManagerSimple; 48 | 49 | use rustzx_core::zx::video::colors::ZXBrightness; 50 | use rustzx_core::zx::video::colors::ZXColor; 51 | use rustzx_core::{zx::machine::ZXMachine, EmulationMode, Emulator, RustzxSettings}; 52 | 53 | use log::{info, error}; 54 | 55 | use core::time::Duration; 56 | use embedded_graphics::{ 57 | prelude::*, 58 | pixelcolor::Rgb565 59 | }; 60 | 61 | use display_interface::WriteOnlyDataCommand; 62 | use mipidsi::models::Model; 63 | use embedded_hal::digital::v2::OutputPin; 64 | 65 | use core::mem::MaybeUninit; 66 | use critical_section::Mutex; 67 | 68 | #[global_allocator] 69 | static ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty(); 70 | 71 | fn init_heap() { 72 | const HEAP_SIZE: usize = 300 * 1024; 73 | static mut HEAP: MaybeUninit<[u8; HEAP_SIZE]> = MaybeUninit::uninit(); 74 | 75 | unsafe { 76 | ALLOCATOR.init(HEAP.as_mut_ptr() as *mut u8, HEAP_SIZE); 77 | } 78 | } 79 | 80 | // Static variable to hold DebugAssist 81 | static DA: Mutex>>> = Mutex::new(RefCell::new(None)); 82 | 83 | fn install_stack_guard(mut da: DebugAssist<'static>, safe_area_size: u32) { 84 | extern "C" { 85 | static mut _stack_end: u32; 86 | static mut _stack_start: u32; 87 | } 88 | let stack_low = unsafe { &mut _stack_end as *mut _ as u32 }; 89 | let stack_high = unsafe { &mut _stack_start as *mut _ as u32 }; 90 | info!("Safe stack {} bytes", stack_high - stack_low - safe_area_size); 91 | da.enable_region0_monitor(stack_low, stack_low + safe_area_size, true, true); 92 | 93 | critical_section::with(|cs| DA.borrow_ref_mut(cs).replace(da)); 94 | interrupt::enable(Interrupt::ASSIST_DEBUG, interrupt::Priority::Priority1).unwrap(); 95 | } 96 | 97 | #[interrupt] 98 | fn ASSIST_DEBUG() { 99 | critical_section::with(|cs| { 100 | error!("\n\nPossible Stack Overflow Detected"); 101 | let mut da = DA.borrow_ref_mut(cs); 102 | let da = da.as_mut().unwrap(); 103 | if da.is_region0_monitor_interrupt_set() { 104 | let pc = da.get_region_monitor_pc(); 105 | info!("PC = 0x{:x}", pc); 106 | da.clear_region0_monitor_interrupt(); 107 | da.disable_region0_monitor(); 108 | loop {} 109 | } 110 | }); 111 | } 112 | 113 | #[entry] 114 | fn main() -> ! { 115 | init_heap(); 116 | let peripherals = Peripherals::take(); 117 | let system = peripherals.SYSTEM.split(); 118 | 119 | // With DMA we have sufficient throughput, so we can clock down the CPU to 80MHz 120 | let clocks = ClockControl::configure(system.clock_control, CpuClock::Clock160MHz).freeze(); 121 | 122 | esp_println::logger::init_logger_from_env(); 123 | 124 | info!("Enabling DebugAssist 4 KB stack guard"); 125 | // get the debug assist driver 126 | let da = DebugAssist::new(peripherals.ASSIST_DEBUG); 127 | 128 | install_stack_guard(da, 4096); // 4 KB safe area 129 | 130 | let mut delay = Delay::new(&clocks); 131 | 132 | info!("About to initialize the SPI LED driver"); 133 | let io = IO::new(peripherals.GPIO, peripherals.IO_MUX); 134 | 135 | let lcd_sclk = io.pins.gpio0; 136 | let lcd_mosi = io.pins.gpio6; 137 | let lcd_miso = io.pins.gpio11; // random unused pin 138 | let lcd_cs = io.pins.gpio5; 139 | let lcd_dc = io.pins.gpio4.into_push_pull_output(); 140 | let _lcd_backlight = io.pins.gpio1.into_push_pull_output(); 141 | let lcd_reset = io.pins.gpio3.into_push_pull_output(); 142 | 143 | let i2c_sda = io.pins.gpio10; 144 | let i2c_scl = io.pins.gpio8; 145 | 146 | let dma = Gdma::new(peripherals.DMA); 147 | let dma_channel = dma.channel0; 148 | 149 | let mut descriptors = [0u32; 8 * 3]; 150 | let mut rx_descriptors = [0u32; 8 * 3]; 151 | 152 | let spi = Spi::new( 153 | peripherals.SPI2, 154 | lcd_sclk, 155 | lcd_mosi, 156 | lcd_miso, 157 | lcd_cs, 158 | 60u32.MHz(), 159 | SpiMode::Mode0, 160 | &clocks, 161 | ).with_dma(dma_channel.configure( 162 | false, 163 | &mut descriptors, 164 | &mut rx_descriptors, 165 | DmaPriority::Priority0, 166 | )); 167 | 168 | info!("SPI ready"); 169 | 170 | let di = spi_dma_displayinterface::new_no_cs(LCD_MEMORY_SIZE, spi, lcd_dc); 171 | 172 | // ESP32-S3-BOX display initialization workaround: Wait for the display to power up. 173 | // If delay is 250ms, picture will be fuzzy. 174 | // If there is no delay, display is blank 175 | delay.delay_ms(500u32); 176 | 177 | let mut display = match mipidsi::Builder::st7789(di) 178 | .with_display_size(LCD_H_RES, LCD_V_RES) 179 | .with_orientation(mipidsi::Orientation::Landscape(true)) 180 | .with_color_order(mipidsi::ColorOrder::Rgb) 181 | .init(&mut delay, Some(lcd_reset)) { 182 | Ok(display) => display, 183 | Err(_e) => { 184 | // Handle the error and possibly exit the application 185 | panic!("Display initialization failed"); 186 | } 187 | }; 188 | 189 | info!("Initializing..."); 190 | Text::new( 191 | "Initializing...", 192 | Point::new(80, 110), 193 | MonoTextStyle::new(&FONT_8X13, RgbColor::GREEN), 194 | ) 195 | .draw(&mut display) 196 | .unwrap(); 197 | 198 | info!("Initialized"); 199 | 200 | let i2c = i2c::I2C::new( 201 | peripherals.I2C0, 202 | i2c_sda, 203 | i2c_scl, 204 | 2u32.kHz(), // Set just to 2 kHz, it seems that there is an interference on Rust board 205 | &clocks, 206 | ); 207 | 208 | info!("I2C ready"); 209 | 210 | // let bus = BusManagerSimple::new(i2c); 211 | let icm = Icm42670::new(i2c, Address::Primary).unwrap(); 212 | 213 | // let mut rng = Rng::new(peripherals.RNG); 214 | // let mut seed_buffer = [0u8; 32]; 215 | // rng.read(&mut seed_buffer).unwrap(); 216 | 217 | // let accel_movement_controller = AccelMovementController::new(icm, 0.2); 218 | // let demo_movement_controller = spooky_core::demo_movement_controller::DemoMovementController::new(seed_buffer); 219 | // let movement_controller = AccelCompositeController::new(demo_movement_controller, accel_movement_controller); 220 | 221 | // app_loop( &mut display, seed_buffer, movement_controller); 222 | let _ = app_loop(&mut display, color_conv); 223 | loop {} 224 | 225 | } 226 | 227 | 228 | fn color_conv(color: ZXColor, _brightness: ZXBrightness) -> Rgb565 { 229 | match color { 230 | ZXColor::Black => Rgb565::BLACK, 231 | ZXColor::Blue => Rgb565::BLUE, 232 | ZXColor::Red => Rgb565::RED, 233 | ZXColor::Purple => Rgb565::MAGENTA, 234 | ZXColor::Green => Rgb565::GREEN, 235 | ZXColor::Cyan => Rgb565::CYAN, 236 | ZXColor::Yellow => Rgb565::YELLOW, 237 | ZXColor::White => Rgb565::WHITE, 238 | } 239 | } 240 | 241 | mod host; 242 | mod stopwatch; 243 | mod io; 244 | mod spritebuf; 245 | fn app_loop( 246 | display: &mut mipidsi::Display, 247 | color_conv: fn(ZXColor, ZXBrightness) -> Rgb565) //-> Result<(), core::fmt::Error> 248 | where 249 | DI: WriteOnlyDataCommand, 250 | M: Model, 251 | RST: OutputPin, 252 | { 253 | // display 254 | // .clear(color_conv(ZXColor::Blue, ZXBrightness::Normal)) 255 | // .map_err(|err| error!("{:?}", err)) 256 | // .ok(); 257 | 258 | info!("Creating emulator"); 259 | 260 | let settings = RustzxSettings { 261 | machine: ZXMachine::Sinclair48K, 262 | emulation_mode: EmulationMode::FrameCount(1), 263 | tape_fastload_enabled: true, 264 | kempston_enabled: false, 265 | mouse_enabled: false, 266 | load_default_rom: true, 267 | }; 268 | 269 | info!("Initialize emulator"); 270 | const MAX_FRAME_DURATION: Duration = Duration::from_millis(0); 271 | 272 | let mut emulator: Emulator = 273 | match Emulator::new(settings, host::Esp32HostContext {}) { 274 | Ok(emulator) => emulator, 275 | Err(err) => { 276 | error!("Error creating emulator: {:?}", err); 277 | return; 278 | } 279 | }; 280 | 281 | 282 | // info!("Binding keyboard"); 283 | 284 | // #[cfg(feature = "tcpstream_keyboard")] 285 | // let rx = bind_keyboard(80); 286 | 287 | // #[cfg(feature = "tcpstream_keyboard")] 288 | // let stage = 0; 289 | // #[cfg(feature = "tcpstream_keyboard")] 290 | // if let Status( 291 | // ClientStatus::Started(ClientConnectionStatus::Connected(ClientIpStatus::Done(config))), 292 | // _, 293 | // ) = wifi_interface.get_status() 294 | // { 295 | // match stage { 296 | // 0 => { 297 | // let message = format!("Keyboard: {}:80", config.ip); 298 | // println!("{}", message); 299 | // Text::new( 300 | // message.as_str(), 301 | // Point::new(10, 210), 302 | // MonoTextStyle::new(&FONT_8X13, color_conv(ZXColor::White, ZXBrightness::Normal)), 303 | // ) 304 | // .draw(&mut display).unwrap(); 305 | 306 | // } 307 | // _ => { 308 | // println!("WiFi unknown"); 309 | // } 310 | // } 311 | // } 312 | 313 | // #[cfg(feature = "tcpstream_keyboard")] 314 | // let mut key_emulation_delay = 0; 315 | // #[cfg(feature = "tcpstream_keyboard")] 316 | // let mut last_key:u8 = 0; 317 | 318 | info!("Entering emulator loop"); 319 | 320 | loop { 321 | info!("Emulating frame"); 322 | 323 | // emulator.emulate_frames(MAX_FRAME_DURATION); 324 | match emulator.emulate_frames(MAX_FRAME_DURATION) { 325 | Ok(_) => { 326 | info!("Emulation of frame succeeded"); 327 | let pixel_iterator = emulator.screen_buffer().get_pixel_iter(); 328 | info!("Drawing frame"); 329 | let _ = display.set_pixels(0, 0, 256 - 1, 192, pixel_iterator); 330 | // .blit(&mut display, color_conv) 331 | // .map_err(|err| error!("{:?}", err)) 332 | // .ok(); 333 | } 334 | _ => { 335 | error!("Emulation of frame failed"); 336 | } 337 | } 338 | 339 | // #[cfg(feature = "tcpstream_keyboard")] 340 | // if key_emulation_delay > 0 { 341 | // key_emulation_delay -= 1; 342 | // } 343 | 344 | // #[cfg(feature = "tcpstream_keyboard")] 345 | // match rx.try_recv() { 346 | // Ok(key) => { 347 | // if key_emulation_delay > 0 { 348 | // // It's not possible to process same keys which were entered shortly after each other 349 | // for frame in 0..key_emulation_delay { 350 | // debug!("Keys received too fast. Running extra emulation frame: {}", frame); 351 | // emulator.emulate_frames(MAX_FRAME_DURATION).map_err(|err| error!("{:?}", err)) 352 | // .map_err(|err| error!("{:?}", err)) 353 | // .ok(); 354 | // } 355 | // emulator.screen_buffer() 356 | // .blit(&mut display, color_conv) 357 | // .map_err(|err| error!("{:?}", err)) 358 | // .ok(); 359 | // } 360 | 361 | // if key == last_key { 362 | // // Same key requires bigger delay 363 | // key_emulation_delay = 6; 364 | // } else { 365 | // key_emulation_delay = 4; 366 | // } 367 | 368 | // last_key = key; 369 | 370 | // info!("Key: {} - {}", key, true); 371 | // let mapped_key_down_option = ascii_code_to_zxkey(key, true) 372 | // .or_else(|| ascii_code_to_modifier(key, true)); 373 | 374 | // let mapped_key_down = match mapped_key_down_option { 375 | // Some(x) => { x }, 376 | // _ => { Event::NoEvent } 377 | // }; 378 | 379 | // let mapped_key_up_option = ascii_code_to_zxkey(key, false) 380 | // .or_else(|| ascii_code_to_modifier(key, false)); 381 | 382 | // let mapped_key_up = match mapped_key_up_option { 383 | // Some(x) => { x }, 384 | // _ => { Event::NoEvent } 385 | // }; 386 | 387 | // debug!("-> key down"); 388 | // match mapped_key_down { 389 | // Event::ZXKey(k,p) => { 390 | // debug!("-> ZXKey"); 391 | // emulator.send_key(k, p); 392 | // }, 393 | // Event::ZXKeyWithModifier(k, k2, p) => { 394 | // debug!("-> ZXKeyWithModifier"); 395 | // emulator.send_key(k, p); 396 | // emulator.send_key(k2, p); 397 | // } 398 | // _ => { 399 | // debug!("Unknown key."); 400 | // } 401 | // } 402 | 403 | // debug!("-> emulating frame"); 404 | // match emulator.emulate_frames(MAX_FRAME_DURATION) { 405 | // Ok(_) => { 406 | // emulator.screen_buffer() 407 | // .blit(&mut display, color_conv) 408 | // .map_err(|err| error!("{:?}", err)) 409 | // .ok(); 410 | // } 411 | // _ => { 412 | // error!("Emulation of frame failed"); 413 | // } 414 | // } 415 | 416 | // debug!("-> key up"); 417 | // match mapped_key_up { 418 | // Event::ZXKey(k,p) => { 419 | // emulator.send_key(k, p); 420 | // }, 421 | // Event::ZXKeyWithModifier(k, k2, p) => { 422 | // emulator.send_key(k, p); 423 | // emulator.send_key(k2, p); 424 | // } 425 | // _ => {} 426 | // } 427 | 428 | // }, 429 | // _ => { 430 | // } 431 | // } 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /esp32-c3-devkit-rust/src/spritebuf.rs: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/bernii/embedded-graphics-framebuf 2 | 3 | use embedded_graphics::pixelcolor::Rgb565; 4 | use embedded_graphics::{ 5 | geometry::OriginDimensions, 6 | prelude::RgbColor, 7 | prelude::{DrawTarget, Point, Size}, 8 | Pixel, 9 | }; 10 | use embedded_graphics_framebuf::{backends::FrameBufferBackend, FrameBuf, PixelIterator}; 11 | 12 | pub struct SpriteBuf> { 13 | pub fbuf: FrameBuf, 14 | } 15 | 16 | impl> OriginDimensions for SpriteBuf { 17 | fn size(&self) -> Size { 18 | self.fbuf.size() 19 | } 20 | } 21 | 22 | impl> SpriteBuf { 23 | pub fn new(fbuf: FrameBuf) -> Self { 24 | Self { fbuf } 25 | } 26 | 27 | /// Get the framebuffers width. 28 | pub fn width(&self) -> usize { 29 | self.fbuf.width() 30 | } 31 | 32 | /// Get the framebuffers height. 33 | pub fn height(&self) -> usize { 34 | self.fbuf.height() 35 | } 36 | 37 | /// Set a pixel's color. 38 | pub fn set_color_at(&mut self, p: Point, color: Rgb565) { 39 | self.fbuf.set_color_at(p, color) 40 | } 41 | 42 | /// Get a pixel's color. 43 | pub fn get_color_at(&self, p: Point) -> Rgb565 { 44 | self.fbuf.get_color_at(p) 45 | } 46 | } 47 | 48 | impl<'a, B: FrameBufferBackend> IntoIterator for &'a SpriteBuf { 49 | type Item = Pixel; 50 | type IntoIter = PixelIterator<'a, Rgb565, B>; 51 | 52 | fn into_iter(self) -> Self::IntoIter { 53 | self.fbuf.into_iter() 54 | } 55 | } 56 | 57 | impl> DrawTarget for SpriteBuf { 58 | type Color = Rgb565; 59 | type Error = core::convert::Infallible; 60 | 61 | fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> 62 | where 63 | I: IntoIterator>, 64 | { 65 | for Pixel(coord, color) in pixels.into_iter() { 66 | if color.g() == 0 && color.b() == 31 && color.r() == 31 { 67 | continue; 68 | } 69 | if coord.x >= 0 70 | && coord.x < self.width() as i32 71 | && coord.y >= 0 72 | && coord.y < self.height() as i32 73 | { 74 | self.fbuf.set_color_at(coord, color); 75 | } 76 | } 77 | Ok(()) 78 | } 79 | } 80 | 81 | // impl SpriteBuf 82 | // where 83 | // B: FrameBufferBackend, 84 | // { 85 | // pub fn get_pixel_iter(&self) -> impl Iterator> + '_ { 86 | // self.fbuf.into_iter() 87 | // } 88 | // } 89 | 90 | impl SpriteBuf 91 | where 92 | B: FrameBufferBackend, 93 | { 94 | pub fn get_pixel_iter(&self) -> impl Iterator + '_ { 95 | self.fbuf.into_iter().map(|pixel| pixel.1) 96 | } 97 | } -------------------------------------------------------------------------------- /esp32-c3-devkit-rust/src/stopwatch.rs: -------------------------------------------------------------------------------- 1 | use rustzx_core::host::Stopwatch; 2 | // use std::time::{Duration, Instant}; 3 | use core::time::Duration; 4 | 5 | pub struct InstantStopwatch { 6 | // timestamp: Instant, 7 | } 8 | 9 | impl Default for InstantStopwatch { 10 | fn default() -> Self { 11 | Self { 12 | // timestamp: Instant::now(), 13 | } 14 | } 15 | } 16 | 17 | impl Stopwatch for InstantStopwatch { 18 | fn new() -> Self { 19 | Self::default() 20 | } 21 | 22 | fn measure(&self) -> Duration { 23 | // self.timestamp.elapsed() 24 | Duration::from_millis(100) 25 | } 26 | } -------------------------------------------------------------------------------- /esp32-c3-devkit-rust/wokwi.toml: -------------------------------------------------------------------------------- 1 | 2 | 3 | [wokwi] 4 | version = 1 5 | #elf = "target/riscv32imac-unknown-none-elf/debug/rustzx-esp32-c3-devkit-rust" 6 | #firmware = "target/riscv32imac-unknown-none-elf/debug/rustzx-esp32-c3-devkit-rust" 7 | gdbServerPort = 3333 8 | 9 | elf = "target/riscv32imac-unknown-none-elf/release/rustzx-esp32-c3-devkit-rust" 10 | firmware = "target/riscv32imac-unknown-none-elf/release/rustzx-esp32-c3-devkit-rust" 11 | -------------------------------------------------------------------------------- /esp32-c6/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.riscv32imac-unknown-none-elf] 2 | runner = "espflash flash --monitor" 3 | rustflags = [ 4 | "-C", "link-arg=-Tlinkall.x", 5 | "-C", "link-arg=-Trom_functions.x", 6 | "-C", "force-frame-pointers", 7 | ] 8 | 9 | [build] 10 | # Uncomment the target if you'd like to use automatic code hinting in your IDE 11 | # target = "xtensa-esp32-none-elf" 12 | # target = "xtensa-esp32s2-none-elf" 13 | #target = "xtensa-esp32s3-none-elf" 14 | target = "riscv32imac-unknown-none-elf" 15 | 16 | [unstable] 17 | build-std = [ "core", "alloc" ] 18 | 19 | [env] 20 | # Use clean build after changing ESP_LOGLEVEL 21 | #ESP_LOGLEVEL="TRACE" 22 | ESP_LOGLEVEL="DEBUG" 23 | 24 | -------------------------------------------------------------------------------- /esp32-c6/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustzx-esp32-c6" 3 | version = "2.0.0" 4 | authors = ["Juraj Michálek "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | hal = { package = "esp32c6-hal", version = "0.8.0", features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 10 | esp-backtrace = { version = "0.10.0", features = [ 11 | "esp32c6", 12 | "panic-handler", 13 | "print-uart", 14 | ] } 15 | esp-println = { version = "0.8.0", features = ["esp32c6", "log"] } 16 | 17 | critical-section = { version = "1.1.2" } 18 | display-interface = "0.4" 19 | esp-alloc = "0.3.0" 20 | embassy-sync = { version = "0.5.0" } 21 | embassy-futures = { version = "0.1.0" } 22 | embassy-executor = { version = "0.5.0", package = "embassy-executor", features = ["nightly", "integrated-timers"] } 23 | embassy-time = { version = "0.3.0" } 24 | embedded-graphics = "0.8.0" 25 | embedded-hal = "1.0.0" 26 | embedded-graphics-framebuf = { version = "0.3.0", git = "https://github.com/georgik/embedded-graphics-framebuf.git", branch = "feature/embedded-graphics-0.8" } 27 | icm42670 = { git = "https://github.com/jessebraham/icm42670/" } 28 | log = "0.4" 29 | mipidsi = "0.7.1" 30 | #panic-halt = "0.2" 31 | shared-bus = { version = "0.3.0" } 32 | esp-display-interface-spi-dma = { version = "0.1.0", features = ["esp32c6"] } 33 | display-interface-spi = "0.4" 34 | #rustzx-utils = { version = "0.16.0" } 35 | #rustzx-core = { version = "0.16.0", features = ["embedded-roms"] } 36 | #rustzx-utils = { path = "../../rustzx/rustzx-utils" } 37 | #rustzx-core = { path = "../../rustzx/rustzx-core" , features = ["embedded-roms"] } 38 | rustzx-utils = { git = "https://github.com/georgik/rustzx.git", branch = "feature/performance-bounding-box" } 39 | rustzx-core = { git = "https://github.com/georgik/rustzx.git", branch = "feature/performance-bounding-box", features = ["embedded-roms"] } 40 | esp-wifi = { version = "0.3.0", features = [ "esp32c6", "wifi", "utils", "tcp", "smoltcp", "dhcpv4", "phy-enable-usb"] } 41 | 42 | usb-zx = { path = "../usb-zx" } 43 | static_cell = { version = "2.0.0", features = ["nightly"] } 44 | esp-bsp = { version = "0.2.0" } 45 | graphics = { path = "../graphics" } 46 | keyboard-pipe = { path = "../keyboard-pipe" } 47 | uart-keyboard = { path = "../uart-keyboard", features = [ "esp32c6" ] } 48 | esp-now-keyboard = { path = "../esp-now-keyboard", features = [ "esp32c6" ] } 49 | emulator = { path = "../emulator", features = [ "esp32_c6_devkit_c1" ] } 50 | -------------------------------------------------------------------------------- /esp32-c6/diagram.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "author": "Juraj Michálek", 4 | "editor": "wokwi", 5 | "parts": [ 6 | { 7 | "type": "board-esp32-c6-devkitc-1", 8 | "id": "esp", 9 | "top": -494.32, 10 | "left": -455.03, 11 | "attrs": { "builder": "rust-std-esp32" } 12 | }, 13 | { 14 | "type": "wokwi-ili9341", 15 | "id": "lcd1", 16 | "top": -546.22, 17 | "left": -134.92, 18 | "rotate": 90, 19 | "attrs": { "flipVertical": "1" } 20 | } 21 | ], 22 | "connections": [ 23 | [ "esp:TX0", "$serialMonitor:RX", "", [] ], 24 | [ "esp:RX0", "$serialMonitor:TX", "", [] ], 25 | [ "esp:3V3", "lcd1:VCC", "green", [] ], 26 | [ "esp:GND.1", "lcd1:GND", "black", [] ], 27 | [ "esp:6", "lcd1:SCK", "blue", [] ], 28 | [ "esp:7", "lcd1:MOSI", "orange", [] ], 29 | [ "esp:20", "lcd1:CS", "red", [] ], 30 | [ "esp:21", "lcd1:D/C", "magenta", [] ], 31 | [ "esp:3", "lcd1:RST", "yellow", [] ], 32 | [ "lcd1:LED", "esp:3V3", "white", [] ] 33 | ], 34 | "serialMonitor": { "display": "terminal" }, 35 | "dependencies": {} 36 | } 37 | -------------------------------------------------------------------------------- /esp32-c6/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | components = ["rustfmt", "rustc-dev"] 4 | -------------------------------------------------------------------------------- /esp32-c6/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(type_alias_impl_trait)] 4 | 5 | use esp_display_interface_spi_dma::display_interface_spi_dma; 6 | use static_cell::make_static; 7 | 8 | use hal::{ 9 | clock::{ClockControl, CpuClock}, 10 | dma::DmaPriority, 11 | embassy, 12 | gdma::Gdma, 13 | peripherals::Peripherals, 14 | prelude::*, 15 | spi::{ 16 | master::{prelude::*, Spi}, 17 | SpiMode, 18 | }, 19 | systimer::SystemTimer, 20 | Delay, Rng, Uart, IO, 21 | }; 22 | 23 | use esp_backtrace as _; 24 | 25 | use esp_wifi::{initialize, EspWifiInitFor}; 26 | 27 | use log::{error, info}; 28 | 29 | use embassy_executor::Spawner; 30 | use esp_wifi::esp_now::{EspNow, EspNowError}; 31 | 32 | use core::mem::MaybeUninit; 33 | 34 | use emulator::app_loop; 35 | use esp_now_keyboard::esp_now_receiver; 36 | use uart_keyboard::uart_receiver; 37 | 38 | #[global_allocator] 39 | static ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty(); 40 | 41 | fn init_heap() { 42 | const HEAP_SIZE: usize = 280 * 1024; 43 | static mut HEAP: MaybeUninit<[u8; HEAP_SIZE]> = MaybeUninit::uninit(); 44 | 45 | unsafe { 46 | ALLOCATOR.init(HEAP.as_mut_ptr() as *mut u8, HEAP_SIZE); 47 | } 48 | } 49 | 50 | use embassy_time::{Duration, Ticker}; 51 | 52 | use esp_bsp::{lcd_gpios, BoardType, DisplayConfig}; 53 | 54 | #[main] 55 | async fn main(spawner: Spawner) -> ! { 56 | init_heap(); 57 | 58 | let peripherals = Peripherals::take(); 59 | 60 | let system = peripherals.SYSTEM.split(); 61 | let clocks = ClockControl::configure(system.clock_control, CpuClock::Clock160MHz).freeze(); 62 | 63 | esp_println::logger::init_logger_from_env(); 64 | 65 | info!("Starting up"); 66 | let timer_group0 = hal::timer::TimerGroup::new(peripherals.TIMG0, &clocks); 67 | embassy::init(&clocks, timer_group0); 68 | 69 | // ESP-NOW keyboard receiver 70 | let wifi_timer = SystemTimer::new(peripherals.SYSTIMER).alarm0; 71 | let rng = Rng::new(peripherals.RNG); 72 | let radio_clock_control = system.radio_clock_control; 73 | 74 | let wifi = peripherals.WIFI; 75 | 76 | let esp_now_init = initialize( 77 | EspWifiInitFor::Wifi, 78 | wifi_timer, 79 | rng, 80 | radio_clock_control, 81 | &clocks, 82 | ); 83 | 84 | match esp_now_init { 85 | Ok(init) => { 86 | info!("ESP-NOW init"); 87 | let esp_now: Result = EspNow::new(&init, wifi); 88 | match esp_now { 89 | Ok(esp_now) => { 90 | spawner.spawn(esp_now_receiver(esp_now)).unwrap(); 91 | } 92 | _ => { 93 | error!("ESP-NOW startup error"); 94 | } 95 | } 96 | } 97 | _ => { 98 | error!("ESP-NOW init error"); 99 | } 100 | } 101 | 102 | // UART Keyboard receiver 103 | let uart0 = Uart::new(peripherals.UART0, &clocks); 104 | spawner.spawn(uart_receiver(uart0)).unwrap(); 105 | 106 | let io = IO::new(peripherals.GPIO, peripherals.IO_MUX); 107 | let (lcd_sclk, lcd_mosi, lcd_cs, lcd_miso, lcd_dc, _lcd_backlight, lcd_reset) = 108 | lcd_gpios!(BoardType::ESP32C6DevKitC1, io); 109 | 110 | let dma = Gdma::new(peripherals.DMA); 111 | let dma_channel = dma.channel0; 112 | 113 | let mut delay = Delay::new(&clocks); 114 | 115 | let descriptors = make_static!([0u32; 8 * 3]); 116 | let rx_descriptors = make_static!([0u32; 8 * 3]); 117 | info!("About to initialize the SPI LED driver"); 118 | 119 | let spi = Spi::new(peripherals.SPI2, 40u32.MHz(), SpiMode::Mode0, &clocks) 120 | .with_pins(Some(lcd_sclk), Some(lcd_mosi), Some(lcd_miso), Some(lcd_cs)) 121 | .with_dma(dma_channel.configure( 122 | false, 123 | &mut *descriptors, 124 | &mut *rx_descriptors, 125 | DmaPriority::Priority0, 126 | )); 127 | 128 | let di = display_interface_spi_dma::new_no_cs(2 * 256 * 192, spi, lcd_dc); 129 | 130 | let display_config = DisplayConfig::for_board(BoardType::ESP32C6DevKitC1); 131 | let display = match mipidsi::Builder::ili9341_rgb565(di) 132 | .with_display_size(display_config.h_res, display_config.v_res) 133 | .with_orientation(mipidsi::Orientation::Landscape(true)) 134 | .with_color_order(mipidsi::ColorOrder::Rgb) 135 | .init(&mut delay, Some(lcd_reset)) 136 | { 137 | Ok(display) => display, 138 | Err(_e) => { 139 | // Handle the error and possibly exit the application 140 | panic!("Display initialization failed"); 141 | } 142 | }; 143 | 144 | // Main Emulator loop 145 | spawner.spawn(app_loop(display)).unwrap(); 146 | 147 | let mut ticker = Ticker::every(Duration::from_secs(1)); 148 | loop { 149 | info!("Tick"); 150 | ticker.next().await; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /esp32-c6/wokwi.toml: -------------------------------------------------------------------------------- 1 | [wokwi] 2 | version = 1 3 | elf = "target/riscv32imac-unknown-none-elf/release/rustzx-esp32-c6" 4 | firmware = "target/riscv32imac-unknown-none-elf/release/rustzx-esp32-c6" -------------------------------------------------------------------------------- /esp32-s3-box/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(target_arch = "xtensa")'] 2 | runner = "espflash flash --monitor" 3 | rustflags = [ 4 | "-C", "link-arg=-Tlinkall.x", 5 | "-C", "link-arg=-Trom_functions.x", 6 | "-C", "link-arg=-nostartfiles", 7 | ] 8 | 9 | [build] 10 | # Uncomment the target if you'd like to use automatic code hinting in your IDE 11 | # target = "xtensa-esp32-none-elf" 12 | # target = "xtensa-esp32s2-none-elf" 13 | target = "xtensa-esp32s3-none-elf" 14 | # target = "riscv32imac-unknown-none-elf" 15 | 16 | [unstable] 17 | build-std = [ "core", "alloc" ] 18 | 19 | [env] 20 | # Use clean build after changing ESP_LOGLEVEL 21 | #ESP_LOGLEVEL="TRACE" 22 | ESP_LOGLEVEL="DEBUG" 23 | 24 | -------------------------------------------------------------------------------- /esp32-s3-box/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustzx-esp32-s3-box" 3 | version = "2.0.0" 4 | authors = ["Juraj Michálek "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | [target.xtensa-esp32s3-none-elf.dependencies] 9 | hal = { package = "esp32s3-hal", version = "0.15.0", features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread", "opsram-8m"] } 10 | esp-backtrace = { version = "0.10.0", features = [ 11 | "esp32s3", 12 | "panic-handler", 13 | "print-uart", 14 | ] } 15 | esp-println = { version = "0.8.0", features = ["esp32s3", "log"] } 16 | 17 | [dependencies] 18 | critical-section = { version = "1.1.2" } 19 | display-interface = "0.4" 20 | esp-alloc = "0.3.0" 21 | embassy-sync = { version = "0.5.0" } 22 | embassy-futures = { version = "0.1.0" } 23 | embassy-executor = { version = "0.5.0", package = "embassy-executor", features = ["nightly", "integrated-timers"] } 24 | embassy-time = { version = "0.3.0" } 25 | embedded-graphics = "0.8.0" 26 | embedded-hal = "1.0.0" 27 | embedded-graphics-framebuf = { version = "0.3.0", git = "https://github.com/georgik/embedded-graphics-framebuf.git", branch = "feature/embedded-graphics-0.8" } 28 | icm42670 = { git = "https://github.com/jessebraham/icm42670/" } 29 | log = "0.4" 30 | mipidsi = "0.7.1" 31 | #panic-halt = "0.2" 32 | shared-bus = { version = "0.3.0" } 33 | #spooky-core = { git = "https://github.com/georgik/esp32-spooky-maze-game.git", rev = "fb5f755", default-features = false, features = ["static_maze"]} 34 | #spooky-embedded = { git = "https://github.com/georgik/esp32-spooky-maze-game.git", rev = "fb5f755", default-features = false, features = [ "esp32s3", "static_maze", "resolution_320x240" ] } 35 | esp-display-interface-spi-dma = { version = "0.1.0", features = ["esp32s3"] } 36 | #rustzx-utils = { version = "0.16.0" } 37 | #rustzx-core = { version = "0.16.0", features = ["embedded-roms"] } 38 | #rustzx-utils = { path = "../../rustzx/rustzx-utils" } 39 | #rustzx-core = { path = "../../rustzx/rustzx-core" , features = ["embedded-roms"] } 40 | rustzx-utils = { git = "https://github.com/georgik/rustzx.git", branch = "feature/performance-bounding-box" } 41 | rustzx-core = { git = "https://github.com/georgik/rustzx.git", branch = "feature/performance-bounding-box", features = ["embedded-roms"] } 42 | esp-wifi = { version = "0.3.0", features = [ "wifi", "utils", "tcp", "smoltcp", "dhcpv4", "phy-enable-usb"] } 43 | 44 | usb-zx = { path = "../usb-zx" } 45 | static_cell = { version = "2.0.0", features = ["nightly"] } 46 | #esp-bsp = { version = "0.1.0" } 47 | esp-bsp = { git = "https://github.com/georgik/esp-bsp-rs", branch = "feature/display_interface_spi_dma" } 48 | graphics = { path = "../graphics" } 49 | keyboard-pipe = { path = "../keyboard-pipe" } 50 | uart-keyboard = { path = "../uart-keyboard", features = [ "esp32s3" ] } 51 | esp-now-keyboard = { path = "../esp-now-keyboard", features = [ "esp32s3" ] } 52 | emulator = { path = "../emulator", features = [ "esp32_s3_box" ] } 53 | -------------------------------------------------------------------------------- /esp32-s3-box/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "esp" 3 | components = ["rustfmt", "rustc-dev"] 4 | targets = ["xtensa-esp32s3-none-elf"] 5 | # targets = ["xtensa-esp32-none-elf", "xtensa-esp32s2-none-elf","xtensa-esp32s3-none-elf"] 6 | -------------------------------------------------------------------------------- /esp32-s3-box/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(type_alias_impl_trait)] 4 | 5 | use esp_display_interface_spi_dma::display_interface_spi_dma; 6 | use static_cell::make_static; 7 | 8 | use hal::{ 9 | clock::{ClockControl, CpuClock}, 10 | dma::DmaPriority, 11 | embassy, 12 | gdma::Gdma, 13 | peripherals::Peripherals, 14 | prelude::*, 15 | psram, 16 | spi::{ 17 | master::{prelude::*, Spi}, 18 | SpiMode, 19 | }, 20 | Delay, 21 | Rng, 22 | IO, 23 | systimer::SystemTimer, 24 | Uart 25 | }; 26 | 27 | use esp_backtrace as _; 28 | 29 | use esp_wifi::{initialize, EspWifiInitFor}; 30 | 31 | use log::{info, error}; 32 | 33 | use embassy_executor::Spawner; 34 | use esp_wifi::esp_now::{EspNow, EspNowError}; 35 | 36 | use core::mem::MaybeUninit; 37 | 38 | use uart_keyboard::uart_receiver; 39 | use esp_now_keyboard::esp_now_receiver; 40 | use emulator::app_loop; 41 | 42 | #[global_allocator] 43 | static ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty(); 44 | 45 | fn init_psram_heap() { 46 | unsafe { 47 | ALLOCATOR.init(psram::psram_vaddr_start() as *mut u8, psram::PSRAM_BYTES); 48 | } 49 | } 50 | 51 | use embassy_time::{Duration, Ticker}; 52 | 53 | use esp_bsp::{BoardType, lcd_gpios, DisplayConfig}; 54 | 55 | #[main] 56 | async fn main(spawner: Spawner) -> ! { 57 | let peripherals = Peripherals::take(); 58 | 59 | psram::init_psram(peripherals.PSRAM); 60 | init_psram_heap(); 61 | 62 | let system = peripherals.SYSTEM.split(); 63 | let clocks = ClockControl::configure(system.clock_control, CpuClock::Clock240MHz).freeze(); 64 | 65 | esp_println::logger::init_logger_from_env(); 66 | 67 | info!("Starting up"); 68 | let timer_group0 = hal::timer::TimerGroup::new(peripherals.TIMG0, &clocks); 69 | embassy::init(&clocks, timer_group0); 70 | 71 | // ESP-NOW keyboard receiver 72 | let wifi_timer = hal::timer::TimerGroup::new(peripherals.TIMG1, &clocks).timer0; 73 | let rng = Rng::new(peripherals.RNG); 74 | let radio_clock_control = system.radio_clock_control; 75 | 76 | let wifi = peripherals.WIFI; 77 | 78 | let esp_now_init = initialize( 79 | EspWifiInitFor::Wifi, 80 | wifi_timer, 81 | rng, 82 | radio_clock_control, 83 | &clocks, 84 | ); 85 | 86 | match esp_now_init { 87 | Ok(init) => { 88 | info!("ESP-NOW init"); 89 | let esp_now: Result = EspNow::new(&init, wifi); 90 | match esp_now { 91 | Ok(esp_now) => { 92 | spawner.spawn(esp_now_receiver(esp_now)).unwrap(); 93 | } 94 | _ => { 95 | error!("ESP-NOW startup error"); 96 | } 97 | } 98 | } 99 | _ => { 100 | error!("ESP-NOW init error"); 101 | } 102 | } 103 | 104 | // UART Keyboard receiver 105 | let uart0 = Uart::new(peripherals.UART0, &clocks); 106 | spawner.spawn(uart_receiver(uart0)).unwrap(); 107 | 108 | let io = IO::new(peripherals.GPIO, peripherals.IO_MUX); 109 | let (lcd_sclk, lcd_mosi, lcd_cs, lcd_miso, lcd_dc, mut lcd_backlight, lcd_reset) = lcd_gpios!(BoardType::ESP32S3Box, io); 110 | 111 | let dma = Gdma::new(peripherals.DMA); 112 | let dma_channel = dma.channel0; 113 | 114 | let mut delay = Delay::new(&clocks); 115 | 116 | let descriptors = make_static!([0u32; 8 * 3]); 117 | let rx_descriptors = make_static!([0u32; 8 * 3]); 118 | info!("About to initialize the SPI LED driver"); 119 | 120 | let spi = Spi::new( 121 | peripherals.SPI2, 122 | 40u32.MHz(), 123 | SpiMode::Mode0, 124 | &clocks 125 | ).with_pins( 126 | Some(lcd_sclk), 127 | Some(lcd_mosi), 128 | Some(lcd_miso), 129 | Some(lcd_cs), 130 | ).with_dma( 131 | dma_channel.configure( 132 | false, 133 | &mut *descriptors, 134 | &mut *rx_descriptors, 135 | DmaPriority::Priority0, 136 | ) 137 | ); 138 | 139 | let _ = lcd_backlight.set_high(); 140 | 141 | let di = display_interface_spi_dma::new_no_cs(2 * 256 * 192, spi, lcd_dc); 142 | 143 | let display_config = DisplayConfig::for_board(BoardType::ESP32S3Box); 144 | let mut display = match mipidsi::Builder::ili9342c_rgb565(di) 145 | .with_display_size(display_config.h_res, display_config.v_res) 146 | .with_orientation(mipidsi::Orientation::PortraitInverted(false)) 147 | .with_color_order(mipidsi::ColorOrder::Bgr) 148 | .init(&mut delay, Some(lcd_reset)) 149 | { 150 | Ok(display) => display, 151 | Err(_e) => { 152 | // Handle the error and possibly exit the application 153 | panic!("Display initialization failed"); 154 | } 155 | }; 156 | 157 | // Main Emulator loop 158 | spawner.spawn(app_loop(display)).unwrap(); 159 | 160 | let mut ticker = Ticker::every(Duration::from_secs(1)); 161 | loop { 162 | info!("Tick"); 163 | ticker.next().await; 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /esp32-s3-usb-otg-keyboard/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # The following lines of boilerplate have to be in your project's 2 | # CMakeLists in this exact order for cmake to work correctly 3 | cmake_minimum_required(VERSION 3.5) 4 | 5 | # "Trim" the build. Include the minimal set of components, main, and anything it depends on. 6 | set(COMPONENTS main) 7 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 8 | get_filename_component(ProjectId ${CMAKE_CURRENT_LIST_DIR} NAME) 9 | string(REPLACE " " "_" ProjectId ${ProjectId}) 10 | project(${ProjectId}) 11 | -------------------------------------------------------------------------------- /esp32-s3-usb-otg-keyboard/README.md: -------------------------------------------------------------------------------- 1 | | Supported Targets | ESP32-S2 | ESP32-S3 | 2 | | ----------------- | -------- | -------- | 3 | 4 | # USB HID Class example 5 | This example implements a basic USB Host HID Class Driver, and demonstrates how to use the driver to communicate with USB HID devices (such as Keyboard and Mouse or both) on the ESP32-S2/S3. Currently, the example only supports the HID boot protocol which should be present on most USB Mouse and Keyboards. The example will continuously scan for the connection of any HID Mouse or Keyboard, and attempt to fetch HID reports from those devices once connected. To quit the example (and shut down the HID driver), users can GPIO0 to low (i.e., pressing the "Boot" button on most ESP dev kits). 6 | 7 | 8 | ### Hardware Required 9 | * Development board with USB capable ESP SoC (ESP32-S2/ESP32-S3) 10 | * A USB cable for Power supply and programming 11 | * USB OTG Cable 12 | 13 | ### Common Pin Assignments 14 | 15 | If your board doesn't have a USB A connector connected to the dedicated GPIOs, 16 | you may have to DIY a cable and connect **D+** and **D-** to the pins listed below. 17 | 18 | ``` 19 | ESP BOARD USB CONNECTOR (type A) 20 | -- 21 | | || VCC 22 | [GPIO19] ------> | || D- 23 | [GPIO20] ------> | || D+ 24 | | || GND 25 | -- 26 | ``` 27 | 28 | ### Build and Flash 29 | 30 | Build the project and flash it to the board, then run monitor tool to view serial output: 31 | 32 | ``` 33 | idf.py -p PORT flash monitor 34 | ``` 35 | 36 | The example serial output will be the following: 37 | 38 | ``` 39 | I (198) example: HID HOST example 40 | I (598) example: Interface number 0, protocol Mouse 41 | I (598) example: Interface number 1, protocol Keyboard 42 | 43 | Mouse 44 | X: 000883 Y: 000058 |o| | 45 | Keyboard 46 | qwertyuiop[]\asdfghjkl;'zxcvbnm,./ 47 | Mouse 48 | X: 000883 Y: 000058 | |o| 49 | ``` 50 | 51 | Where every keyboard key printed as char symbol if it is possible and a Hex value for any other key. 52 | 53 | #### Keyboard input data 54 | Keyboard input data starts with the word "Keyboard" and every pressed key is printed to the serial debug. 55 | Left or right Shift modifier is also supported. 56 | 57 | ``` 58 | Keyboard 59 | Hello, ESP32 USB HID Keyboard is here! 60 | ``` 61 | 62 | #### Mouse input data 63 | Mouse input data starts with the word "Mouse" and has the following structure. 64 | ``` 65 | Mouse 66 | X: -00343 Y: 000183 | |o| 67 | | | | | 68 | | | | +- Right mouse button pressed status ("o" - pressed, " " - not pressed) 69 | | | +--- Left mouse button pressed status ("o" - pressed, " " - not pressed) 70 | | +---------- Y relative coordinate of the cursor 71 | +----------------------- X relative coordinate of the cursor 72 | ``` 73 | -------------------------------------------------------------------------------- /esp32-s3-usb-otg-keyboard/main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | idf_component_register(SRCS "hid_host_example.c" 2 | INCLUDE_DIRS "." 3 | PRIV_REQUIRES usb driver 4 | ) 5 | -------------------------------------------------------------------------------- /esp32-s3-usb-otg-keyboard/main/hid_host_example.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD 3 | * 4 | * SPDX-License-Identifier: Unlicense OR CC0-1.0 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "freertos/FreeRTOS.h" 12 | #include "freertos/task.h" 13 | #include "freertos/event_groups.h" 14 | #include "freertos/queue.h" 15 | #include "esp_err.h" 16 | #include "esp_log.h" 17 | #include "usb/usb_host.h" 18 | #include "errno.h" 19 | #include "driver/gpio.h" 20 | 21 | #include "usb/hid_host.h" 22 | #include "usb/hid_usage_keyboard.h" 23 | #include "usb/hid_usage_mouse.h" 24 | 25 | #include "esp_wifi.h" 26 | #include "nvs_flash.h" 27 | 28 | #include "espnow.h" 29 | #include "espnow_storage.h" 30 | #include "espnow_utils.h" 31 | 32 | #include "esp_log.h" 33 | #include "bsp/esp-bsp.h" 34 | #include "lvgl.h" 35 | #include "esp_lvgl_port.h" 36 | 37 | static lv_disp_t *display; 38 | static lv_obj_t *log_console = NULL; 39 | 40 | 41 | /* GPIO Pin number for quit from example logic */ 42 | #define APP_QUIT_PIN GPIO_NUM_0 43 | 44 | static const char *TAG = "example"; 45 | 46 | QueueHandle_t app_event_queue = NULL; 47 | 48 | 49 | espnow_frame_head_t frame_head = { 50 | .retransmit_count = 1, 51 | .broadcast = true, 52 | }; 53 | 54 | /** 55 | * @brief APP event group 56 | * 57 | * Application logic can be different. There is a one among other ways to distingiush the 58 | * event by application event group. 59 | * In this example we have two event groups: 60 | * APP_EVENT - General event, which is APP_QUIT_PIN press event (Generally, it is IO0). 61 | * APP_EVENT_HID_HOST - HID Host Driver event, such as device connection/disconnection or input report. 62 | */ 63 | typedef enum { 64 | APP_EVENT = 0, 65 | APP_EVENT_HID_HOST 66 | } app_event_group_t; 67 | 68 | /** 69 | * @brief APP event queue 70 | * 71 | * This event is used for delivering the HID Host event from callback to a task. 72 | */ 73 | typedef struct { 74 | app_event_group_t event_group; 75 | /* HID Host - Device related info */ 76 | struct { 77 | hid_host_device_handle_t handle; 78 | hid_host_driver_event_t event; 79 | void *arg; 80 | } hid_host_device; 81 | } app_event_queue_t; 82 | 83 | /** 84 | * @brief HID Protocol string names 85 | */ 86 | static const char *hid_proto_name_str[] = { 87 | "NONE", 88 | "KEYBOARD", 89 | "MOUSE" 90 | }; 91 | 92 | /** 93 | * @brief Key event 94 | */ 95 | typedef struct { 96 | enum key_state { 97 | KEY_STATE_PRESSED = 0x00, 98 | KEY_STATE_RELEASED = 0x01 99 | } state; 100 | uint8_t modifier; 101 | uint8_t key_code; 102 | } key_event_t; 103 | 104 | /* Main char symbol for ENTER key */ 105 | #define KEYBOARD_ENTER_MAIN_CHAR '\r' 106 | /* When set to 1 pressing ENTER will be extending with LineFeed during serial debug output */ 107 | #define KEYBOARD_ENTER_LF_EXTEND 1 108 | 109 | /** 110 | * @brief Scancode to ascii table 111 | */ 112 | const uint8_t keycode2ascii [57][2] = { 113 | {0, 0}, /* HID_KEY_NO_PRESS */ 114 | {0, 0}, /* HID_KEY_ROLLOVER */ 115 | {0, 0}, /* HID_KEY_POST_FAIL */ 116 | {0, 0}, /* HID_KEY_ERROR_UNDEFINED */ 117 | {'a', 'A'}, /* HID_KEY_A */ 118 | {'b', 'B'}, /* HID_KEY_B */ 119 | {'c', 'C'}, /* HID_KEY_C */ 120 | {'d', 'D'}, /* HID_KEY_D */ 121 | {'e', 'E'}, /* HID_KEY_E */ 122 | {'f', 'F'}, /* HID_KEY_F */ 123 | {'g', 'G'}, /* HID_KEY_G */ 124 | {'h', 'H'}, /* HID_KEY_H */ 125 | {'i', 'I'}, /* HID_KEY_I */ 126 | {'j', 'J'}, /* HID_KEY_J */ 127 | {'k', 'K'}, /* HID_KEY_K */ 128 | {'l', 'L'}, /* HID_KEY_L */ 129 | {'m', 'M'}, /* HID_KEY_M */ 130 | {'n', 'N'}, /* HID_KEY_N */ 131 | {'o', 'O'}, /* HID_KEY_O */ 132 | {'p', 'P'}, /* HID_KEY_P */ 133 | {'q', 'Q'}, /* HID_KEY_Q */ 134 | {'r', 'R'}, /* HID_KEY_R */ 135 | {'s', 'S'}, /* HID_KEY_S */ 136 | {'t', 'T'}, /* HID_KEY_T */ 137 | {'u', 'U'}, /* HID_KEY_U */ 138 | {'v', 'V'}, /* HID_KEY_V */ 139 | {'w', 'W'}, /* HID_KEY_W */ 140 | {'x', 'X'}, /* HID_KEY_X */ 141 | {'y', 'Y'}, /* HID_KEY_Y */ 142 | {'z', 'Z'}, /* HID_KEY_Z */ 143 | {'1', '!'}, /* HID_KEY_1 */ 144 | {'2', '@'}, /* HID_KEY_2 */ 145 | {'3', '#'}, /* HID_KEY_3 */ 146 | {'4', '$'}, /* HID_KEY_4 */ 147 | {'5', '%'}, /* HID_KEY_5 */ 148 | {'6', '^'}, /* HID_KEY_6 */ 149 | {'7', '&'}, /* HID_KEY_7 */ 150 | {'8', '*'}, /* HID_KEY_8 */ 151 | {'9', '('}, /* HID_KEY_9 */ 152 | {'0', ')'}, /* HID_KEY_0 */ 153 | {KEYBOARD_ENTER_MAIN_CHAR, KEYBOARD_ENTER_MAIN_CHAR}, /* HID_KEY_ENTER */ 154 | {0, 0}, /* HID_KEY_ESC */ 155 | {'\b', 0}, /* HID_KEY_DEL */ 156 | {0, 0}, /* HID_KEY_TAB */ 157 | {' ', ' '}, /* HID_KEY_SPACE */ 158 | {'-', '_'}, /* HID_KEY_MINUS */ 159 | {'=', '+'}, /* HID_KEY_EQUAL */ 160 | {'[', '{'}, /* HID_KEY_OPEN_BRACKET */ 161 | {']', '}'}, /* HID_KEY_CLOSE_BRACKET */ 162 | {'\\', '|'}, /* HID_KEY_BACK_SLASH */ 163 | {'\\', '|'}, /* HID_KEY_SHARP */ // HOTFIX: for NonUS Keyboards repeat HID_KEY_BACK_SLASH 164 | {';', ':'}, /* HID_KEY_COLON */ 165 | {'\'', '"'}, /* HID_KEY_QUOTE */ 166 | {'`', '~'}, /* HID_KEY_TILDE */ 167 | {',', '<'}, /* HID_KEY_LESS */ 168 | {'.', '>'}, /* HID_KEY_GREATER */ 169 | {'/', '?'} /* HID_KEY_SLASH */ 170 | }; 171 | 172 | /** 173 | * @brief Makes new line depending on report output protocol type 174 | * 175 | * @param[in] proto Current protocol to output 176 | */ 177 | static void hid_print_new_device_report_header(hid_protocol_t proto) 178 | { 179 | static hid_protocol_t prev_proto_output = -1; 180 | 181 | if (prev_proto_output != proto) { 182 | prev_proto_output = proto; 183 | printf("\r\n"); 184 | if (proto == HID_PROTOCOL_MOUSE) { 185 | printf("Mouse\r\n"); 186 | } else if (proto == HID_PROTOCOL_KEYBOARD) { 187 | printf("Keyboard\r\n"); 188 | } else { 189 | printf("Generic\r\n"); 190 | } 191 | fflush(stdout); 192 | } 193 | } 194 | 195 | /** 196 | * @brief HID Keyboard modifier verification for capitalization application (right or left shift) 197 | * 198 | * @param[in] modifier 199 | * @return true Modifier was pressed (left or right shift) 200 | * @return false Modifier was not pressed (left or right shift) 201 | * 202 | */ 203 | static inline bool hid_keyboard_is_modifier_shift(uint8_t modifier) 204 | { 205 | if (((modifier & HID_LEFT_SHIFT) == HID_LEFT_SHIFT) || 206 | ((modifier & HID_RIGHT_SHIFT) == HID_RIGHT_SHIFT)) { 207 | return true; 208 | } 209 | return false; 210 | } 211 | 212 | void set_console_text(const char *text) 213 | { 214 | bsp_display_lock(0); 215 | lv_textarea_set_text(log_console, text); 216 | bsp_display_unlock(); 217 | } 218 | 219 | #define ESP_NOW_ETH_ALEN 6 220 | // const uint8_t broadcast_address[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; 221 | 222 | // Broadcast to address [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF] 223 | // const uint8_t broadcast_address[ESP_NOW_ETH_ALEN] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; 224 | 225 | // set broadcast address to 34:85:18:00:bc:38 226 | // const uint8_t broadcast_address[ESP_NOW_ETH_ALEN] = {0x34, 0x85, 0x18, 0x00, 0xbc, 0x38}; 227 | 228 | /** 229 | * @brief HID Keyboard get char symbol from key code 230 | * 231 | * @param[in] modifier Keyboard modifier data 232 | * @param[in] key_code Keyboard key code 233 | * @param[in] key_char Pointer to key char data 234 | * 235 | * @return true Key scancode converted successfully 236 | * @return false Key scancode unknown 237 | */ 238 | static inline bool hid_keyboard_get_char(uint8_t modifier, 239 | uint8_t key_code, 240 | unsigned char *key_char) 241 | { 242 | uint8_t mod = (hid_keyboard_is_modifier_shift(modifier)) ? 1 : 0; 243 | 244 | 245 | // Array with one keycode 246 | // uint8_t key_code_array[1] = {key_code}; 247 | 248 | if ((key_code >= HID_KEY_A) && (key_code <= HID_KEY_SLASH)) { 249 | *key_char = keycode2ascii[key_code][mod]; 250 | } else { 251 | // All other key pressed 252 | return false; 253 | } 254 | 255 | return true; 256 | } 257 | 258 | /** 259 | * @brief HID Keyboard print char symbol 260 | * 261 | * @param[in] key_char Keyboard char to stdout 262 | */ 263 | static inline void hid_keyboard_print_char(unsigned int key_char) 264 | { 265 | if (!!key_char) { 266 | putchar(key_char); 267 | #if (KEYBOARD_ENTER_LF_EXTEND) 268 | if (KEYBOARD_ENTER_MAIN_CHAR == key_char) { 269 | putchar('\n'); 270 | } 271 | #endif // KEYBOARD_ENTER_LF_EXTEND 272 | fflush(stdout); 273 | } 274 | } 275 | 276 | /** 277 | * @brief Send key_event over esp-now 278 | * 279 | * Information is sent in three bytes. 280 | * The first byte contains keypress state. 281 | * The second byte contains modifier state. 282 | * The third byte contains key code. 283 | */ 284 | static inline void hid_keyboard_send_key_event(key_event_t *key_event) 285 | { 286 | uint8_t key_event_array[3] = {key_event->state, key_event->modifier, key_event->key_code}; 287 | char key_event_str[50]; 288 | snprintf(key_event_str, sizeof(key_event_str), "State: 0x%x\nModifier: 0x%x\nKey: 0x%x", key_event->state, key_event->modifier, key_event->key_code); 289 | 290 | ESP_LOGI(TAG, "%s", key_event_str); 291 | 292 | esp_err_t ret = ESP_OK; 293 | ret = espnow_send(ESPNOW_DATA_TYPE_DATA, ESPNOW_ADDR_BROADCAST, key_event_array, 3, &frame_head, portMAX_DELAY); 294 | 295 | if (ret == ESP_OK) { 296 | set_console_text(key_event_str); 297 | } else { 298 | ESP_LOGE(TAG, "Error sending data: %s", esp_err_to_name(ret)); 299 | set_console_text("Error sending data"); 300 | } 301 | 302 | 303 | } 304 | 305 | /** 306 | * @brief Key Event. Key event with the key code, state and modifier. 307 | * 308 | * @param[in] key_event Pointer to Key Event structure 309 | * 310 | */ 311 | static void key_event_callback(key_event_t *key_event) 312 | { 313 | unsigned char key_char; 314 | 315 | hid_print_new_device_report_header(HID_PROTOCOL_KEYBOARD); 316 | hid_keyboard_send_key_event(key_event); 317 | 318 | 319 | // if (KEY_STATE_PRESSED == key_event->state) { 320 | // if (hid_keyboard_get_char(key_event->modifier, 321 | // key_event->key_code, &key_char)) { 322 | 323 | // hid_keyboard_print_char(key_char); 324 | 325 | // } 326 | // } 327 | } 328 | 329 | /** 330 | * @brief Key buffer scan code search. 331 | * 332 | * @param[in] src Pointer to source buffer where to search 333 | * @param[in] key Key scancode to search 334 | * @param[in] length Size of the source buffer 335 | */ 336 | static inline bool key_found(const uint8_t *const src, 337 | uint8_t key, 338 | unsigned int length) 339 | { 340 | for (unsigned int i = 0; i < length; i++) { 341 | if (src[i] == key) { 342 | return true; 343 | } 344 | } 345 | return false; 346 | } 347 | 348 | /** 349 | * @brief USB HID Host Keyboard Interface report callback handler 350 | * 351 | * @param[in] data Pointer to input report data buffer 352 | * @param[in] length Length of input report data buffer 353 | */ 354 | static void hid_host_keyboard_report_callback(const uint8_t *const data, const int length) 355 | { 356 | hid_keyboard_input_report_boot_t *kb_report = (hid_keyboard_input_report_boot_t *)data; 357 | 358 | if (length < sizeof(hid_keyboard_input_report_boot_t)) { 359 | return; 360 | } 361 | 362 | static uint8_t prev_keys[HID_KEYBOARD_KEY_MAX] = { 0 }; 363 | key_event_t key_event; 364 | 365 | for (int i = 0; i < HID_KEYBOARD_KEY_MAX; i++) { 366 | 367 | // key has been released verification 368 | if (prev_keys[i] > HID_KEY_ERROR_UNDEFINED && 369 | !key_found(kb_report->key, prev_keys[i], HID_KEYBOARD_KEY_MAX)) { 370 | key_event.key_code = prev_keys[i]; 371 | key_event.modifier = 0; 372 | key_event.state = KEY_STATE_RELEASED; 373 | key_event_callback(&key_event); 374 | } 375 | 376 | // key has been pressed verification 377 | if (kb_report->key[i] > HID_KEY_ERROR_UNDEFINED && 378 | !key_found(prev_keys, kb_report->key[i], HID_KEYBOARD_KEY_MAX)) { 379 | key_event.key_code = kb_report->key[i]; 380 | key_event.modifier = kb_report->modifier.val; 381 | key_event.state = KEY_STATE_PRESSED; 382 | key_event_callback(&key_event); 383 | } 384 | } 385 | 386 | memcpy(prev_keys, &kb_report->key, HID_KEYBOARD_KEY_MAX); 387 | } 388 | 389 | /** 390 | * @brief USB HID Host Mouse Interface report callback handler 391 | * 392 | * @param[in] data Pointer to input report data buffer 393 | * @param[in] length Length of input report data buffer 394 | */ 395 | static void hid_host_mouse_report_callback(const uint8_t *const data, const int length) 396 | { 397 | hid_mouse_input_report_boot_t *mouse_report = (hid_mouse_input_report_boot_t *)data; 398 | 399 | if (length < sizeof(hid_mouse_input_report_boot_t)) { 400 | return; 401 | } 402 | 403 | static int x_pos = 0; 404 | static int y_pos = 0; 405 | 406 | // Calculate absolute position from displacement 407 | x_pos += mouse_report->x_displacement; 408 | y_pos += mouse_report->y_displacement; 409 | 410 | hid_print_new_device_report_header(HID_PROTOCOL_MOUSE); 411 | 412 | printf("X: %06d\tY: %06d\t|%c|%c|\r", 413 | x_pos, y_pos, 414 | (mouse_report->buttons.button1 ? 'o' : ' '), 415 | (mouse_report->buttons.button2 ? 'o' : ' ')); 416 | fflush(stdout); 417 | } 418 | 419 | /** 420 | * @brief USB HID Host Generic Interface report callback handler 421 | * 422 | * 'generic' means anything else than mouse or keyboard 423 | * 424 | * @param[in] data Pointer to input report data buffer 425 | * @param[in] length Length of input report data buffer 426 | */ 427 | static void hid_host_generic_report_callback(const uint8_t *const data, const int length) 428 | { 429 | hid_print_new_device_report_header(HID_PROTOCOL_NONE); 430 | for (int i = 0; i < length; i++) { 431 | printf("%02X", data[i]); 432 | } 433 | putchar('\r'); 434 | } 435 | 436 | /** 437 | * @brief USB HID Host interface callback 438 | * 439 | * @param[in] hid_device_handle HID Device handle 440 | * @param[in] event HID Host interface event 441 | * @param[in] arg Pointer to arguments, does not used 442 | */ 443 | void hid_host_interface_callback(hid_host_device_handle_t hid_device_handle, 444 | const hid_host_interface_event_t event, 445 | void *arg) 446 | { 447 | uint8_t data[64] = { 0 }; 448 | size_t data_length = 0; 449 | hid_host_dev_params_t dev_params; 450 | ESP_ERROR_CHECK(hid_host_device_get_params(hid_device_handle, &dev_params)); 451 | 452 | switch (event) { 453 | case HID_HOST_INTERFACE_EVENT_INPUT_REPORT: 454 | ESP_ERROR_CHECK(hid_host_device_get_raw_input_report_data(hid_device_handle, 455 | data, 456 | 64, 457 | &data_length)); 458 | 459 | if (HID_SUBCLASS_BOOT_INTERFACE == dev_params.sub_class) { 460 | if (HID_PROTOCOL_KEYBOARD == dev_params.proto) { 461 | hid_host_keyboard_report_callback(data, data_length); 462 | } else if (HID_PROTOCOL_MOUSE == dev_params.proto) { 463 | hid_host_mouse_report_callback(data, data_length); 464 | } 465 | } else { 466 | hid_host_generic_report_callback(data, data_length); 467 | } 468 | 469 | break; 470 | case HID_HOST_INTERFACE_EVENT_DISCONNECTED: 471 | ESP_LOGI(TAG, "HID Device, protocol '%s' DISCONNECTED", 472 | hid_proto_name_str[dev_params.proto]); 473 | ESP_ERROR_CHECK(hid_host_device_close(hid_device_handle)); 474 | break; 475 | case HID_HOST_INTERFACE_EVENT_TRANSFER_ERROR: 476 | ESP_LOGI(TAG, "HID Device, protocol '%s' TRANSFER_ERROR", 477 | hid_proto_name_str[dev_params.proto]); 478 | break; 479 | default: 480 | ESP_LOGE(TAG, "HID Device, protocol '%s' Unhandled event", 481 | hid_proto_name_str[dev_params.proto]); 482 | break; 483 | } 484 | } 485 | 486 | /** 487 | * @brief USB HID Host Device event 488 | * 489 | * @param[in] hid_device_handle HID Device handle 490 | * @param[in] event HID Host Device event 491 | * @param[in] arg Pointer to arguments, does not used 492 | */ 493 | void hid_host_device_event(hid_host_device_handle_t hid_device_handle, 494 | const hid_host_driver_event_t event, 495 | void *arg) 496 | { 497 | hid_host_dev_params_t dev_params; 498 | ESP_ERROR_CHECK(hid_host_device_get_params(hid_device_handle, &dev_params)); 499 | 500 | switch (event) { 501 | case HID_HOST_DRIVER_EVENT_CONNECTED: 502 | ESP_LOGI(TAG, "HID Device, protocol '%s' CONNECTED", 503 | hid_proto_name_str[dev_params.proto]); 504 | 505 | const hid_host_device_config_t dev_config = { 506 | .callback = hid_host_interface_callback, 507 | .callback_arg = NULL 508 | }; 509 | 510 | ESP_ERROR_CHECK(hid_host_device_open(hid_device_handle, &dev_config)); 511 | if (HID_SUBCLASS_BOOT_INTERFACE == dev_params.sub_class) { 512 | ESP_ERROR_CHECK(hid_class_request_set_protocol(hid_device_handle, HID_REPORT_PROTOCOL_BOOT)); 513 | if (HID_PROTOCOL_KEYBOARD == dev_params.proto) { 514 | ESP_ERROR_CHECK(hid_class_request_set_idle(hid_device_handle, 0, 0)); 515 | } 516 | } 517 | ESP_ERROR_CHECK(hid_host_device_start(hid_device_handle)); 518 | break; 519 | default: 520 | break; 521 | } 522 | } 523 | 524 | /** 525 | * @brief Start USB Host install and handle common USB host library events while app pin not low 526 | * 527 | * @param[in] arg Not used 528 | */ 529 | static void usb_lib_task(void *arg) 530 | { 531 | const usb_host_config_t host_config = { 532 | .skip_phy_setup = false, 533 | .intr_flags = ESP_INTR_FLAG_LEVEL1, 534 | }; 535 | 536 | ESP_ERROR_CHECK(usb_host_install(&host_config)); 537 | xTaskNotifyGive(arg); 538 | 539 | while (true) { 540 | uint32_t event_flags; 541 | usb_host_lib_handle_events(portMAX_DELAY, &event_flags); 542 | // In this example, there is only one client registered 543 | // So, once we deregister the client, this call must succeed with ESP_OK 544 | if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) { 545 | ESP_ERROR_CHECK(usb_host_device_free_all()); 546 | break; 547 | } 548 | } 549 | 550 | ESP_LOGI(TAG, "USB shutdown"); 551 | // Clean up USB Host 552 | vTaskDelay(10); // Short delay to allow clients clean-up 553 | ESP_ERROR_CHECK(usb_host_uninstall()); 554 | vTaskDelete(NULL); 555 | } 556 | 557 | /** 558 | * @brief BOOT button pressed callback 559 | * 560 | * Signal application to exit the HID Host task 561 | * 562 | * @param[in] arg Unused 563 | */ 564 | static void gpio_isr_cb(void *arg) 565 | { 566 | BaseType_t xTaskWoken = pdFALSE; 567 | const app_event_queue_t evt_queue = { 568 | .event_group = APP_EVENT, 569 | }; 570 | 571 | if (app_event_queue) { 572 | xQueueSendFromISR(app_event_queue, &evt_queue, &xTaskWoken); 573 | } 574 | 575 | if (xTaskWoken == pdTRUE) { 576 | portYIELD_FROM_ISR(); 577 | } 578 | } 579 | 580 | // Global variable to store the current rotation state 581 | static lv_disp_rot_t current_rotation = LV_DISP_ROT_NONE; 582 | 583 | // Rotate the screen to the left 584 | void rotate_screen_left() { 585 | bsp_display_lock(0); 586 | lv_disp_t *disp = lv_disp_get_default(); 587 | 588 | // Rotate left (counter-clockwise) 589 | if (current_rotation == LV_DISP_ROT_NONE) { 590 | current_rotation = LV_DISP_ROT_270; 591 | } else if (current_rotation == LV_DISP_ROT_90) { 592 | current_rotation = LV_DISP_ROT_NONE; 593 | } else if (current_rotation == LV_DISP_ROT_180) { 594 | current_rotation = LV_DISP_ROT_90; 595 | } else if (current_rotation == LV_DISP_ROT_270) { 596 | current_rotation = LV_DISP_ROT_180; 597 | } 598 | 599 | lv_disp_set_rotation(disp, current_rotation); 600 | bsp_display_unlock(); 601 | } 602 | 603 | // Rotate the screen to the right 604 | void rotate_screen_right() { 605 | bsp_display_lock(0); 606 | lv_disp_t *disp = lv_disp_get_default(); 607 | 608 | // Rotate right (clockwise) 609 | if (current_rotation == LV_DISP_ROT_NONE) { 610 | current_rotation = LV_DISP_ROT_90; 611 | } else if (current_rotation == LV_DISP_ROT_90) { 612 | current_rotation = LV_DISP_ROT_180; 613 | } else if (current_rotation == LV_DISP_ROT_180) { 614 | current_rotation = LV_DISP_ROT_270; 615 | } else if (current_rotation == LV_DISP_ROT_270) { 616 | current_rotation = LV_DISP_ROT_NONE; 617 | } 618 | 619 | lv_disp_set_rotation(disp, current_rotation); 620 | bsp_display_unlock(); 621 | } 622 | 623 | // ISR Handler for buttons 624 | static void IRAM_ATTR button_isr_handler(void* arg) { 625 | int gpio_num = (int)arg; 626 | if (gpio_num == 10) { 627 | // Handle left button press (rotate screen left) 628 | rotate_screen_left(); 629 | } else if (gpio_num == 11) { 630 | // Handle right button press (rotate screen right) 631 | rotate_screen_right(); 632 | } 633 | } 634 | 635 | void init_button_gpios() { 636 | gpio_config_t button_config = { 637 | .pin_bit_mask = (1ULL << 10) | (1ULL << 11), 638 | .mode = GPIO_MODE_INPUT, 639 | .pull_up_en = GPIO_PULLUP_ENABLE, 640 | .intr_type = GPIO_INTR_NEGEDGE, 641 | }; 642 | gpio_config(&button_config); 643 | 644 | // Attach the interrupt service routines 645 | gpio_isr_handler_add(10, button_isr_handler, (void*) 10); 646 | gpio_isr_handler_add(11, button_isr_handler, (void*) 11); 647 | } 648 | 649 | /** 650 | * @brief HID Host Device callback 651 | * 652 | * Puts new HID Device event to the queue 653 | * 654 | * @param[in] hid_device_handle HID Device handle 655 | * @param[in] event HID Device event 656 | * @param[in] arg Not used 657 | */ 658 | void hid_host_device_callback(hid_host_device_handle_t hid_device_handle, 659 | const hid_host_driver_event_t event, 660 | void *arg) 661 | { 662 | const app_event_queue_t evt_queue = { 663 | .event_group = APP_EVENT_HID_HOST, 664 | // HID Host Device related info 665 | .hid_host_device.handle = hid_device_handle, 666 | .hid_host_device.event = event, 667 | .hid_host_device.arg = arg 668 | }; 669 | 670 | if (app_event_queue) { 671 | xQueueSend(app_event_queue, &evt_queue, 0); 672 | } 673 | } 674 | 675 | #define GPIO_DEV_VBUS_EN GPIO_NUM_12 676 | #define GPIO_BOOST_EN GPIO_NUM_13 677 | #define GPIO_IDEV_LIMIT_EN GPIO_NUM_17 678 | #define GPIO_USB_SEL GPIO_NUM_18 679 | 680 | 681 | static void app_wifi_init() 682 | { 683 | esp_event_loop_create_default(); 684 | 685 | wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); 686 | 687 | ESP_ERROR_CHECK(esp_wifi_init(&cfg)); 688 | ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); 689 | ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); 690 | ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); 691 | ESP_ERROR_CHECK(esp_wifi_start()); 692 | } 693 | 694 | 695 | void app_main(void) 696 | { 697 | BaseType_t task_created; 698 | app_event_queue_t evt_queue; 699 | ESP_LOGI(TAG, "HID Host example"); 700 | 701 | // // Initialize NVS 702 | ESP_LOGI(TAG, "Initializing NVS"); 703 | esp_err_t ret = nvs_flash_init(); 704 | if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { 705 | ESP_ERROR_CHECK(nvs_flash_erase()); 706 | ret = nvs_flash_init(); 707 | } 708 | ESP_ERROR_CHECK(ret); 709 | 710 | 711 | // // Initialize esp-now 712 | // ESP_LOGI(TAG, "Initializing ESP-NOW"); 713 | // ESP_ERROR_CHECK(esp_netif_init()); 714 | // ESP_ERROR_CHECK(esp_event_loop_create_default()); 715 | // esp_netif_create_default_wifi_sta(); 716 | // wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); 717 | // ESP_ERROR_CHECK(esp_wifi_init(&cfg)); 718 | // ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); 719 | // ESP_ERROR_CHECK(esp_wifi_start()); 720 | // // Initialize ESP-NOW 721 | // esp_log_level_set("esp_now", ESP_LOG_VERBOSE); 722 | // ESP_ERROR_CHECK(esp_now_init()); 723 | 724 | // // Adding peer configuration 725 | // esp_now_peer_info_t peer_info = { 726 | // .peer_addr = {0x25, 0x0A, 0xC4, 0x00, 0x1C, 0x1C}, 727 | // .channel = 1, 728 | // .ifidx = ESP_IF_WIFI_STA, 729 | // .encrypt = false, 730 | // }; 731 | // ESP_ERROR_CHECK(esp_now_add_peer(&peer_info)); 732 | 733 | // bsp_usb_host_start(BSP_USB_HOST_POWER_MODE_USB_DEV, true); 734 | 735 | 736 | 737 | app_wifi_init(); 738 | espnow_config_t espnow_config = ESPNOW_INIT_CONFIG_DEFAULT(); 739 | espnow_config.forward_enable = 0; 740 | espnow_init(&espnow_config); 741 | 742 | // espnow_set_config_for_data_type(ESPNOW_DATA_TYPE_DATA, true, app_uart_write_handle); 743 | ESP_LOGI(TAG, "ESP-NOW initialized"); 744 | 745 | // Initialize Power configuration for ESP32-S3-USB-OTG 746 | // Configure GPIO12 to high (DEV_VBUS_EN) 747 | ESP_LOGI(TAG, "Initializing Power configuration for ESP32-S3-USB-OTG"); 748 | gpio_reset_pin(GPIO_DEV_VBUS_EN); 749 | gpio_set_direction(GPIO_DEV_VBUS_EN, GPIO_MODE_OUTPUT); 750 | gpio_set_level(GPIO_DEV_VBUS_EN, 1); 751 | 752 | // Configure GPIO13 to low (BOOST_EN) 753 | gpio_reset_pin(GPIO_BOOST_EN); 754 | gpio_set_direction(GPIO_BOOST_EN, GPIO_MODE_OUTPUT); 755 | gpio_set_level(GPIO_BOOST_EN, 0); 756 | 757 | // Set GPIO17 high to enable current limiting IC (IDEV_LIMIT_EN) 758 | gpio_reset_pin(GPIO_IDEV_LIMIT_EN); 759 | gpio_set_direction(GPIO_IDEV_LIMIT_EN, GPIO_MODE_OUTPUT); 760 | gpio_set_level(GPIO_IDEV_LIMIT_EN, 1); 761 | 762 | // Set GPIO18 high to connect USB D+/D- to USB_DEV D+ D- (USB_SEL) 763 | gpio_reset_pin(GPIO_USB_SEL); 764 | gpio_set_direction(GPIO_USB_SEL, GPIO_MODE_OUTPUT); 765 | gpio_set_level(GPIO_USB_SEL, 1); 766 | 767 | // Init BOOT button: Pressing the button simulates app request to exit 768 | // It will disconnect the USB device and uninstall the HID driver and USB Host Lib 769 | const gpio_config_t input_pin = { 770 | .pin_bit_mask = BIT64(APP_QUIT_PIN), 771 | .mode = GPIO_MODE_INPUT, 772 | .pull_up_en = GPIO_PULLUP_ENABLE, 773 | .intr_type = GPIO_INTR_NEGEDGE, 774 | }; 775 | ESP_ERROR_CHECK(gpio_config(&input_pin)); 776 | ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1)); 777 | ESP_ERROR_CHECK(gpio_isr_handler_add(APP_QUIT_PIN, gpio_isr_cb, NULL)); 778 | 779 | /* 780 | * Create usb_lib_task to: 781 | * - initialize USB Host library 782 | * - Handle USB Host events while APP pin in in HIGH state 783 | */ 784 | task_created = xTaskCreatePinnedToCore(usb_lib_task, 785 | "usb_events", 786 | 4096, 787 | xTaskGetCurrentTaskHandle(), 788 | 2, NULL, 0); 789 | assert(task_created == pdTRUE); 790 | 791 | // Wait for notification from usb_lib_task to proceed 792 | ulTaskNotifyTake(false, 1000); 793 | 794 | /* 795 | * HID host driver configuration 796 | * - create background task for handling low level event inside the HID driver 797 | * - provide the device callback to get new HID Device connection event 798 | */ 799 | const hid_host_driver_config_t hid_host_driver_config = { 800 | .create_background_task = true, 801 | .task_priority = 5, 802 | .stack_size = 4096, 803 | .core_id = 0, 804 | .callback = hid_host_device_callback, 805 | .callback_arg = NULL 806 | }; 807 | 808 | ESP_ERROR_CHECK(hid_host_install(&hid_host_driver_config)); 809 | 810 | // Create queue 811 | app_event_queue = xQueueCreate(10, sizeof(app_event_queue_t)); 812 | 813 | ESP_LOGI(TAG, "Waiting for HID Device to be connected"); 814 | ESP_LOGI(TAG, "LVGL init"); 815 | /* Initialize display and LVGL */ 816 | display = bsp_display_start(); 817 | ESP_LOGI(TAG, "LVGL init done"); 818 | // esp_err_t ret = ESP_OK; 819 | // uint8_t key_code = 42; 820 | // espnow_send(ESPNOW_DATA_TYPE_DATA, ESPNOW_ADDR_BROADCAST, &key_code, sizeof(key_code), &frame_head, portMAX_DELAY); 821 | 822 | bsp_display_backlight_on(); 823 | 824 | bsp_display_lock(0); 825 | log_console = lv_textarea_create(lv_scr_act()); 826 | lv_obj_set_size(log_console, 240, 240); 827 | bsp_display_unlock(); 828 | 829 | set_console_text("USB Keyboard to ESP-NOW\n"); 830 | 831 | // Add reaction on Up and Down buttons - rotate the screen 832 | init_button_gpios(); 833 | // lv_textarea_set_readonly(log_console, true); 834 | // lv_textarea_set_scrollbar_mode(log_console, LV_SCROLLBAR_MODE_AUTO); 835 | 836 | while (1) { 837 | // Wait queue 838 | if (xQueueReceive(app_event_queue, &evt_queue, portMAX_DELAY)) { 839 | if (APP_EVENT == evt_queue.event_group) { 840 | // User pressed button 841 | usb_host_lib_info_t lib_info; 842 | ESP_ERROR_CHECK(usb_host_lib_info(&lib_info)); 843 | if (lib_info.num_devices == 0) { 844 | // End while cycle 845 | break; 846 | } else { 847 | ESP_LOGW(TAG, "To shutdown example, remove all USB devices and press button again."); 848 | // Keep polling 849 | } 850 | } 851 | 852 | if (APP_EVENT_HID_HOST == evt_queue.event_group) { 853 | hid_host_device_event(evt_queue.hid_host_device.handle, 854 | evt_queue.hid_host_device.event, 855 | evt_queue.hid_host_device.arg); 856 | } 857 | } 858 | } 859 | 860 | ESP_LOGI(TAG, "HID Driver uninstall"); 861 | ESP_ERROR_CHECK(hid_host_uninstall()); 862 | gpio_isr_handler_remove(APP_QUIT_PIN); 863 | xQueueReset(app_event_queue); 864 | vQueueDelete(app_event_queue); 865 | } 866 | -------------------------------------------------------------------------------- /esp32-s3-usb-otg-keyboard/main/idf_component.yml: -------------------------------------------------------------------------------- 1 | ## IDF Component Manager Manifest File 2 | dependencies: 3 | espressif/esp32_s3_usb_otg: "^1.5.0~1" 4 | espressif/esp-now: "^2.4.0" 5 | idf: ">=4.4" 6 | usb_host_hid: "^1.0.1" 7 | -------------------------------------------------------------------------------- /graphics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphics" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | embedded-graphics = "0.8.0" 8 | rustzx-core = { git = "https://github.com/georgik/rustzx.git", branch = "feature/performance-bounding-box", features = ["embedded-roms"] } 9 | -------------------------------------------------------------------------------- /graphics/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | use embedded_graphics::{pixelcolor::Rgb565, prelude::RgbColor}; 4 | 5 | use rustzx_core::zx::video::colors::{ZXBrightness, ZXColor}; 6 | 7 | const ZX_BLACK: Rgb565 = Rgb565::BLACK; 8 | const ZX_BRIGHT_BLUE: Rgb565 = Rgb565::new(0, 0, Rgb565::MAX_B); 9 | const ZX_BRIGHT_RED: Rgb565 = Rgb565::new(Rgb565::MAX_R, 0, 0); 10 | const ZX_BRIGHT_PURPLE: Rgb565 = Rgb565::new(Rgb565::MAX_R, 0, Rgb565::MAX_B); 11 | const ZX_BRIGHT_GREEN: Rgb565 = Rgb565::new(0, Rgb565::MAX_G, 0); 12 | const ZX_BRIGHT_CYAN: Rgb565 = Rgb565::new(0, Rgb565::MAX_G, Rgb565::MAX_B); 13 | const ZX_BRIGHT_YELLOW: Rgb565 = Rgb565::new(Rgb565::MAX_R, Rgb565::MAX_G, 0); 14 | const ZX_BRIGHT_WHITE: Rgb565 = Rgb565::WHITE; 15 | 16 | const ZX_NORMAL_BLUE: Rgb565 = Rgb565::new(0, 0, Rgb565::MAX_B / 2); 17 | const ZX_NORMAL_RED: Rgb565 = Rgb565::new(Rgb565::MAX_R / 2, 0, 0); 18 | const ZX_NORMAL_PURPLE: Rgb565 = Rgb565::new(Rgb565::MAX_R / 2, 0, Rgb565::MAX_B / 2); 19 | const ZX_NORMAL_GREEN: Rgb565 = Rgb565::new(0, Rgb565::MAX_G / 2, 0); 20 | const ZX_NORMAL_CYAN: Rgb565 = Rgb565::new(0, Rgb565::MAX_G / 2, Rgb565::MAX_B / 2); 21 | const ZX_NORMAL_YELLOW: Rgb565 = Rgb565::new(Rgb565::MAX_R / 2, Rgb565::MAX_G / 2, 0); 22 | const ZX_NORMAL_WHITE: Rgb565 = 23 | Rgb565::new(Rgb565::MAX_R / 2, Rgb565::MAX_G / 2, Rgb565::MAX_B / 2); 24 | 25 | pub fn color_conv(color: &ZXColor, brightness: ZXBrightness) -> Rgb565 { 26 | match (color, brightness) { 27 | (ZXColor::Black, _) => ZX_BLACK, 28 | 29 | // Bright Colors 30 | (ZXColor::Blue, ZXBrightness::Bright) => ZX_BRIGHT_BLUE, 31 | (ZXColor::Red, ZXBrightness::Bright) => ZX_BRIGHT_RED, 32 | (ZXColor::Purple, ZXBrightness::Bright) => ZX_BRIGHT_PURPLE, 33 | (ZXColor::Green, ZXBrightness::Bright) => ZX_BRIGHT_GREEN, 34 | (ZXColor::Cyan, ZXBrightness::Bright) => ZX_BRIGHT_CYAN, 35 | (ZXColor::Yellow, ZXBrightness::Bright) => ZX_BRIGHT_YELLOW, 36 | (ZXColor::White, ZXBrightness::Bright) => ZX_BRIGHT_WHITE, 37 | 38 | // Normal Colors 39 | (ZXColor::Blue, ZXBrightness::Normal) => ZX_NORMAL_BLUE, 40 | (ZXColor::Red, ZXBrightness::Normal) => ZX_NORMAL_RED, 41 | (ZXColor::Purple, ZXBrightness::Normal) => ZX_NORMAL_PURPLE, 42 | (ZXColor::Green, ZXBrightness::Normal) => ZX_NORMAL_GREEN, 43 | (ZXColor::Cyan, ZXBrightness::Normal) => ZX_NORMAL_CYAN, 44 | (ZXColor::Yellow, ZXBrightness::Normal) => ZX_NORMAL_YELLOW, 45 | (ZXColor::White, ZXBrightness::Normal) => ZX_NORMAL_WHITE, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /keyboard-pipe/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "keyboard-pipe" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | embassy-sync = { version = "0.5.0" } 8 | 9 | [patch.crates-io] 10 | embassy-sync = { git = "https://github.com/embassy-rs/embassy.git", package = "embassy-sync", rev = "14f41a71b6ea9dedb4ee5b9c741fe10575772c7d"} 11 | -------------------------------------------------------------------------------- /keyboard-pipe/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; 4 | use embassy_sync::pipe::Pipe; 5 | 6 | // Pipe for transporting keystrokes from ESP-NOW to emulator core 7 | const PIPE_BUF_SIZE: usize = 15; 8 | pub static PIPE: Pipe = Pipe::new(); 9 | -------------------------------------------------------------------------------- /m5stack-cores3-ps2-keyboard/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(target_arch = "xtensa")'] 2 | runner = "espflash flash --monitor" 3 | rustflags = [ 4 | "-C", "link-arg=-Tlinkall.x", 5 | #"-C", "link-arg=-Trom_functions.x", 6 | "-C", "link-arg=-nostartfiles", 7 | ] 8 | 9 | [build] 10 | # Uncomment the target if you'd like to use automatic code hinting in your IDE 11 | # target = "xtensa-esp32-none-elf" 12 | # target = "xtensa-esp32s2-none-elf" 13 | target = "xtensa-esp32s3-none-elf" 14 | # target = "riscv32imac-unknown-none-elf" 15 | 16 | [unstable] 17 | build-std = [ "core", "alloc" ] 18 | 19 | [env] 20 | # Use clean build after changing ESP_LOGLEVEL 21 | ESP_LOGLEVEL="TRACE" 22 | #ESP_LOGLEVEL="DEBUG" 23 | 24 | -------------------------------------------------------------------------------- /m5stack-cores3-ps2-keyboard/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustzx-m5stack-cores3" 3 | version = "2.0.0" 4 | authors = ["Juraj Michálek "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | [target.xtensa-esp32s3-none-elf.dependencies] 9 | hal = { package = "esp32s3-hal", version = "0.14.0" , optional = true } 10 | esp-backtrace = { version = "0.9.0", features = [ 11 | "esp32s3", 12 | "panic-handler", 13 | "print-uart", 14 | ] } 15 | esp-println = { version = "0.8.0", features = ["esp32s3", "log"] } 16 | 17 | [dependencies] 18 | critical-section = { version = "1.1.2" } 19 | display-interface = "0.4" 20 | esp-alloc = "0.3.0" 21 | embedded-graphics = "0.8.0" 22 | embedded-hal = "0.2" 23 | embedded-graphics-framebuf = { version = "0.3.0", git = "https://github.com/georgik/embedded-graphics-framebuf.git", branch = "feature/embedded-graphics-0.8" } 24 | icm42670 = { git = "https://github.com/jessebraham/icm42670/" } 25 | log = "0.4" 26 | mipidsi = "0.7.1" 27 | #panic-halt = "0.2" 28 | shared-bus = { version = "0.3.0" } 29 | spooky-core = { git = "https://github.com/georgik/esp32-spooky-maze-game.git", rev = "fb5f755", default-features = false, features = ["static_maze"]} 30 | spooky-embedded = { git = "https://github.com/georgik/esp32-spooky-maze-game.git", rev = "fb5f755", default-features = false, features = [ "esp32s3", "static_maze", "resolution_320x240" ] } 31 | spi-dma-displayinterface = { git = "https://github.com/georgik/esp32-spooky-maze-game.git", rev = "fb5f755", features = ["esp32s3"] } 32 | #rustzx-utils = { version = "0.16.0" } 33 | #rustzx-core = { version = "0.16.0", features = ["embedded-roms"] } 34 | #rustzx-utils = { path = "../../rustzx/rustzx-utils" } 35 | #rustzx-core = { path = "../../rustzx/rustzx-core" , features = ["embedded-roms"] } 36 | rustzx-utils = { git = "https://github.com/georgik/rustzx.git", branch = "feature/performance-bounding-box" } 37 | rustzx-core = { git = "https://github.com/georgik/rustzx.git", branch = "feature/performance-bounding-box", features = ["embedded-roms"] } 38 | axp2101 = { git = "https://github.com/georgik/axp2101-rs.git", rev = "60db0e1" } 39 | aw9523 = { git = "https://github.com/georgik/aw9523-rs.git", rev = "af49728" } 40 | pc-keyboard = "0.7.0" 41 | 42 | 43 | # Async dependencies 44 | embedded-hal-async = { version = "=1.0.0-rc.2", optional = true } 45 | embassy-sync = { version = "0.5.0",optional = true } 46 | embassy-futures = { version = "0.1.0", optional = true } 47 | embassy-executor = { version = "0.4.0", package = "embassy-executor", features = ["nightly", "integrated-timers"], optional = true } 48 | embassy-time = { version = "0.2.0", optional = true } 49 | 50 | # WiFi dependencies 51 | #esp-wifi = { git = "https://github.com/esp-rs/esp-wifi.git", rev = "a69545d", optional = true, features = [ "esp32c3", "esp-now", "async" ] } 52 | 53 | [features] 54 | #default = [ "hal", "embassy", "wifi"] 55 | default = [ "hal/psram-8m" ] 56 | embassy = [ "hal/psram-8m", "hal/embassy", "hal/async", "hal/embassy-time-timg0", "hal/rt", "hal/embassy-executor-thread", "embedded-hal-async", "embassy-sync", "embassy-futures", "embassy-executor", "embassy-time" ] 57 | #wifi = [ "esp-wifi" ] 58 | -------------------------------------------------------------------------------- /m5stack-cores3-ps2-keyboard/src/host.rs: -------------------------------------------------------------------------------- 1 | 2 | extern crate alloc; 3 | use alloc::{vec, vec::Vec}; 4 | use embedded_graphics::pixelcolor::RgbColor; 5 | use log::*; 6 | 7 | use rustzx_core::host::FrameBuffer; 8 | use rustzx_core::host::FrameBufferSource; 9 | use rustzx_core::host::Host; 10 | use rustzx_core::host::HostContext; 11 | use rustzx_core::host::StubIoExtender; 12 | use rustzx_core::zx::video::colors::ZXBrightness; 13 | use rustzx_core::zx::video::colors::ZXColor; 14 | const LCD_H_RES: usize = 256; 15 | const LCD_PIXELS: usize = LCD_H_RES*192; 16 | use crate::stopwatch::InstantStopwatch; 17 | use crate::io::FileAsset; 18 | use embedded_graphics::pixelcolor::Rgb565; 19 | 20 | 21 | pub(crate) struct Esp32Host 22 | { 23 | } 24 | 25 | impl Host for Esp32Host 26 | { 27 | type Context = Esp32HostContext; 28 | type EmulationStopwatch = InstantStopwatch; 29 | type FrameBuffer = EmbeddedGraphicsFrameBuffer; 30 | type TapeAsset = FileAsset; // TODO 31 | type IoExtender = StubIoExtender; 32 | // type DebugInterface = StubDebugInterface; // TODO 33 | } 34 | 35 | pub(crate) struct Esp32HostContext; 36 | 37 | impl HostContext for Esp32HostContext 38 | { 39 | fn frame_buffer_context(&self) -> <::FrameBuffer as FrameBuffer>::Context { 40 | () 41 | } 42 | } 43 | 44 | pub(crate) struct EmbeddedGraphicsFrameBuffer { 45 | buffer: Vec, 46 | buffer_width: usize, 47 | pub bounding_box_top_left: Option<(usize, usize)>, 48 | pub bounding_box_bottom_right: Option<(usize, usize)>, 49 | } 50 | 51 | use crate::color_conv; 52 | impl EmbeddedGraphicsFrameBuffer { 53 | // pub fn get_pixel_iter(&self) -> impl Iterator + '_ { 54 | // self.buffer.iter().copied() 55 | // } 56 | 57 | fn mark_dirty(&mut self, x: usize, y: usize) { 58 | let (min_x, min_y) = self.bounding_box_top_left.unwrap_or((x, y)); 59 | let (max_x, max_y) = self.bounding_box_bottom_right.unwrap_or((x, y)); 60 | 61 | self.bounding_box_top_left = Some((min_x.min(x), min_y.min(y))); 62 | self.bounding_box_bottom_right = Some((max_x.max(x), max_y.max(y))); 63 | } 64 | 65 | pub fn get_region_pixel_iter(&self, top_left: (usize, usize), bottom_right: (usize, usize)) -> impl Iterator + '_ { 66 | let start_x = top_left.0; 67 | let end_x = bottom_right.0 + 1; // Include the pixel at bottom_right coordinates 68 | let start_y = top_left.1; 69 | let end_y = bottom_right.1 + 1; // Include the pixel at bottom_right coordinates 70 | 71 | (start_y..end_y).flat_map(move |y| { 72 | (start_x..end_x).map(move |x| { 73 | self.buffer[y * self.buffer_width + x] 74 | }) 75 | }) 76 | } 77 | } 78 | 79 | 80 | impl FrameBuffer for EmbeddedGraphicsFrameBuffer { 81 | type Context = (); 82 | 83 | fn new( 84 | width: usize, 85 | height: usize, 86 | source: FrameBufferSource, 87 | _context: Self::Context, 88 | ) -> Self { 89 | info!("Allocation"); 90 | match source { 91 | FrameBufferSource::Screen => { 92 | info!("Allocating frame buffer width={}, height={}", width, height); 93 | 94 | Self { 95 | buffer: vec![Rgb565::RED; LCD_PIXELS], 96 | buffer_width: LCD_H_RES as usize, 97 | bounding_box_bottom_right: None, 98 | bounding_box_top_left: None, 99 | } 100 | } 101 | FrameBufferSource::Border => Self { 102 | buffer: vec![Rgb565::WHITE; 1], 103 | buffer_width: 1, 104 | bounding_box_bottom_right: None, 105 | bounding_box_top_left: None, 106 | }, 107 | } 108 | } 109 | 110 | 111 | fn set_color(&mut self, x: usize, y: usize, zx_color: ZXColor, zx_brightness: ZXBrightness) { 112 | let index = y * self.buffer_width + x; 113 | let new_color = color_conv(&zx_color, zx_brightness); 114 | if self.buffer[index] != new_color { 115 | self.buffer[index] = new_color; 116 | self.mark_dirty(x, y); // Update the bounding box 117 | } 118 | } 119 | 120 | fn set_colors(&mut self, x: usize, y: usize, colors: [ZXColor; 8], brightness: ZXBrightness) { 121 | for (i, &color) in colors.iter().enumerate() { 122 | self.set_color(x + i, y, color, brightness); 123 | } 124 | } 125 | 126 | fn reset_bounding_box(&mut self) { 127 | self.bounding_box_bottom_right = None; 128 | self.bounding_box_top_left = None; 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /m5stack-cores3-ps2-keyboard/src/io.rs: -------------------------------------------------------------------------------- 1 | // no_std implementation of File concept using memory buffer 2 | use rustzx_core::{ 3 | error::IoError, 4 | host::{SeekableAsset, LoadableAsset, SeekFrom} 5 | }; 6 | 7 | #[derive(Debug)] 8 | pub enum FileAssetError { 9 | ReadError, 10 | SeekError, 11 | } 12 | 13 | pub struct FileAsset { 14 | data: &'static [u8], 15 | position: usize, 16 | } 17 | 18 | impl FileAsset { 19 | pub fn new(data: &'static [u8]) -> Self { 20 | FileAsset { data, position: 0 } 21 | } 22 | 23 | // Helper methods to convert FileAssetError to IoError if necessary 24 | fn convert_error(error: FileAssetError) -> IoError { 25 | match error { 26 | FileAssetError::ReadError => IoError::HostAssetImplFailed, 27 | FileAssetError::SeekError => IoError::SeekBeforeStart, 28 | } 29 | } 30 | } 31 | 32 | impl SeekableAsset for FileAsset { 33 | fn seek(&mut self, _pos: SeekFrom) -> Result { 34 | todo!() 35 | } 36 | } 37 | 38 | impl LoadableAsset for FileAsset { 39 | fn read(&mut self, buf: &mut [u8]) -> Result { 40 | let available = self.data.len() - self.position; // Bytes remaining 41 | let to_read = available.min(buf.len()); // Number of bytes to read 42 | 43 | if to_read == 0 { 44 | return Err(FileAsset::convert_error(FileAssetError::ReadError)); 45 | } 46 | 47 | buf[..to_read].copy_from_slice(&self.data[self.position..self.position + to_read]); 48 | self.position += to_read; // Update the position 49 | 50 | Ok(to_read) // Return the number of bytes read 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /m5stack-cores3-ps2-keyboard/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use spi_dma_displayinterface::spi_dma_displayinterface; 5 | 6 | use embedded_graphics::{ 7 | mono_font::{ascii::FONT_8X13, MonoTextStyle}, 8 | prelude::{Point, RgbColor}, 9 | text::Text, 10 | Drawable, 11 | }; 12 | 13 | use hal::{ 14 | clock::{ClockControl, CpuClock}, 15 | dma::DmaPriority, 16 | gdma::Gdma, 17 | i2c, 18 | // interrupt, 19 | peripherals::{ 20 | Peripherals, 21 | UART1 22 | }, 23 | prelude::*, 24 | psram, 25 | spi::{ 26 | master::{prelude::*, Spi}, 27 | SpiMode, 28 | }, 29 | Delay, 30 | // Rng, 31 | IO, 32 | uart::{ 33 | config::{Config, DataBits, Parity, StopBits}, 34 | TxRxPins, 35 | }, 36 | Uart 37 | }; 38 | 39 | // use spooky_embedded::app::app_loop; 40 | 41 | // use spooky_embedded::{ 42 | // embedded_display::LCD_MEMORY_SIZE, 43 | // controllers::{accel::AccelMovementController, composites::accel_composite::AccelCompositeController} 44 | // }; 45 | 46 | use esp_backtrace as _; 47 | 48 | // use icm42670::{Address, Icm42670}; 49 | use shared_bus::BusManagerSimple; 50 | 51 | use rustzx_core::zx::video::colors::ZXBrightness; 52 | use rustzx_core::zx::video::colors::ZXColor; 53 | use rustzx_core::{zx::machine::ZXMachine, EmulationMode, Emulator, RustzxSettings, host::Host}; 54 | 55 | use log::{info, error, debug}; 56 | 57 | use core::time::Duration; 58 | use embedded_graphics::pixelcolor::Rgb565; 59 | 60 | use display_interface::WriteOnlyDataCommand; 61 | use mipidsi::models::Model; 62 | use embedded_hal::digital::v2::OutputPin; 63 | 64 | use axp2101::{ I2CPowerManagementInterface, Axp2101 }; 65 | use aw9523::I2CGpioExpanderInterface; 66 | 67 | use pc_keyboard::{layouts, HandleControl, ScancodeSet2}; 68 | 69 | mod host; 70 | mod stopwatch; 71 | mod io; 72 | mod pc_zxkey; 73 | use pc_zxkey::{ pc_code_to_zxkey, pc_code_to_modifier }; 74 | mod zx_event; 75 | use zx_event::Event; 76 | 77 | use crate::io::FileAsset; 78 | 79 | #[global_allocator] 80 | static ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty(); 81 | 82 | const SCREEN_OFFSET_X: u16 = (320 - 256) / 2; 83 | const SCREEN_OFFSET_Y: u16 = (240 - 192) / 2; 84 | 85 | fn init_psram_heap() { 86 | unsafe { 87 | ALLOCATOR.init(psram::psram_vaddr_start() as *mut u8, psram::PSRAM_BYTES); 88 | } 89 | } 90 | 91 | #[entry] 92 | fn main() -> ! { 93 | let peripherals = Peripherals::take(); 94 | 95 | psram::init_psram(peripherals.PSRAM); 96 | init_psram_heap(); 97 | 98 | let system = peripherals.SYSTEM.split(); 99 | 100 | let clocks = ClockControl::configure(system.clock_control, CpuClock::Clock240MHz).freeze(); 101 | 102 | esp_println::logger::init_logger_from_env(); 103 | 104 | let mut delay = Delay::new(&clocks); 105 | 106 | info!("About to initialize the SPI LED driver"); 107 | let io = IO::new(peripherals.GPIO, peripherals.IO_MUX); 108 | 109 | let lcd_sclk = io.pins.gpio36; 110 | let lcd_mosi = io.pins.gpio37; 111 | let lcd_cs = io.pins.gpio3; 112 | let lcd_miso = io.pins.gpio6; 113 | let lcd_dc = io.pins.gpio35.into_push_pull_output(); 114 | let lcd_reset = io.pins.gpio15.into_push_pull_output(); 115 | 116 | let serial_tx = io.pins.gpio17.into_push_pull_output(); 117 | let serial_rx = io.pins.gpio18.into_floating_input(); 118 | 119 | // I2C 120 | let sda = io.pins.gpio12; 121 | let scl = io.pins.gpio11; 122 | 123 | let dma = Gdma::new(peripherals.DMA); 124 | let dma_channel = dma.channel0; 125 | 126 | let mut descriptors = [0u32; 8 * 3]; 127 | let mut rx_descriptors = [0u32; 8 * 3]; 128 | 129 | let i2c_bus = i2c::I2C::new( 130 | peripherals.I2C0, 131 | sda, 132 | scl, 133 | 400u32.kHz(), 134 | &clocks, 135 | ); 136 | 137 | let bus = BusManagerSimple::new(i2c_bus); 138 | 139 | info!("Initializing AXP2101"); 140 | let axp_interface = I2CPowerManagementInterface::new(bus.acquire_i2c()); 141 | let mut axp = Axp2101::new(axp_interface); 142 | axp.init().unwrap(); 143 | 144 | info!("Initializing GPIO Expander"); 145 | let aw_interface = I2CGpioExpanderInterface::new(bus.acquire_i2c()); 146 | let mut aw = aw9523::Aw9523::new(aw_interface); 147 | aw.init().unwrap(); 148 | 149 | // M5Stack CORE 2 - https://docs.m5stack.com/en/core/core2 150 | // let mut backlight = io.pins.gpio3.into_push_pull_output(); 151 | delay.delay_ms(500u32); 152 | info!("About to initialize the SPI LED driver"); 153 | 154 | let spi = Spi::new( 155 | peripherals.SPI3, 156 | 60u32.MHz(), 157 | SpiMode::Mode0, 158 | &clocks, 159 | ).with_pins( 160 | Some(lcd_sclk), 161 | Some(lcd_mosi), 162 | Some(lcd_miso), 163 | Some(lcd_cs), 164 | ).with_dma(dma_channel.configure( 165 | false, 166 | &mut descriptors, 167 | &mut rx_descriptors, 168 | DmaPriority::Priority0, 169 | )); 170 | 171 | delay.delay_ms(500u32); 172 | 173 | //https://github.com/m5stack/M5CoreS3/blob/main/src/utility/Config.h#L8 174 | let di = spi_dma_displayinterface::new_no_cs(2*256*192, spi, lcd_dc); 175 | 176 | let mut display = mipidsi::Builder::ili9342c_rgb565(di) 177 | .with_display_size(320, 240) 178 | .with_color_order(mipidsi::ColorOrder::Bgr) 179 | .with_invert_colors(mipidsi::ColorInversion::Inverted) 180 | .init(&mut delay, Some(lcd_reset)) 181 | .unwrap(); 182 | delay.delay_ms(500u32); 183 | info!("Initializing..."); 184 | Text::new( 185 | "Initializing...", 186 | Point::new(80, 110), 187 | MonoTextStyle::new(&FONT_8X13, RgbColor::WHITE), 188 | ) 189 | .draw(&mut display) 190 | .unwrap(); 191 | 192 | info!("Initialized"); 193 | 194 | // let mut rng = Rng::new(peripherals.RNG); 195 | // let mut seed_buffer = [0u8; 32]; 196 | // rng.read(&mut seed_buffer).unwrap(); 197 | 198 | // let accel_movement_controller = AccelMovementController::new(icm, 0.2); 199 | // let demo_movement_controller = spooky_core::demo_movement_controller::DemoMovementController::new(seed_buffer); 200 | // let movement_controller = AccelCompositeController::new(demo_movement_controller, accel_movement_controller); 201 | // use esp_wifi::{initialize, EspWifiInitFor}; 202 | // app_loop( &mut display, seed_buffer, movement_controller); 203 | // info!("Initializing WiFi"); 204 | // use hal::Rng; 205 | // #[cfg(target_arch = "xtensa")] 206 | // let timer = hal::timer::TimerGroup::new(peripherals.TIMG1, &clocks).timer0; 207 | // #[cfg(target_arch = "riscv32")] 208 | // let timer = hal::systimer::SystemTimer::new(peripherals.SYSTIMER).alarm0; 209 | // match initialize( 210 | // EspWifiInitFor::Wifi, 211 | // timer, 212 | // Rng::new(peripherals.RNG), 213 | // system.radio_clock_control, 214 | // &clocks, 215 | // ) { 216 | // Ok(_) => { 217 | // info!("WiFi initialized"); 218 | // }, 219 | // Err(err) => { 220 | // error!("Error initializing WiFi: {:?}", err); 221 | // } 222 | // } 223 | 224 | 225 | let config = Config { 226 | baudrate: 115200, 227 | data_bits: DataBits::DataBits8, 228 | parity: Parity::ParityNone, 229 | stop_bits: StopBits::STOP1, 230 | }; 231 | 232 | let pins = TxRxPins::new_tx_rx( 233 | serial_tx, 234 | serial_rx, 235 | ); 236 | 237 | let serial = Uart::new_with_config(peripherals.UART1, config, Some(pins), &clocks); 238 | 239 | let _ = app_loop(&mut display, color_conv, serial); 240 | loop {} 241 | 242 | } 243 | 244 | const ZX_BLACK: Rgb565 = Rgb565::BLACK; 245 | const ZX_BRIGHT_BLUE: Rgb565 = Rgb565::new(0, 0, Rgb565::MAX_B); 246 | const ZX_BRIGHT_RED: Rgb565 = Rgb565::new(Rgb565::MAX_R, 0, 0); 247 | const ZX_BRIGHT_PURPLE: Rgb565 = Rgb565::new(Rgb565::MAX_R, 0, Rgb565::MAX_B); 248 | const ZX_BRIGHT_GREEN: Rgb565 = Rgb565::new(0, Rgb565::MAX_G, 0); 249 | const ZX_BRIGHT_CYAN: Rgb565 = Rgb565::new(0, Rgb565::MAX_G, Rgb565::MAX_B); 250 | const ZX_BRIGHT_YELLOW: Rgb565 = Rgb565::new(Rgb565::MAX_R, Rgb565::MAX_G, 0); 251 | const ZX_BRIGHT_WHITE: Rgb565 = Rgb565::WHITE; 252 | 253 | const ZX_NORMAL_BLUE: Rgb565 = Rgb565::new(0, 0, Rgb565::MAX_B / 2); 254 | const ZX_NORMAL_RED: Rgb565 = Rgb565::new(Rgb565::MAX_R / 2, 0, 0); 255 | const ZX_NORMAL_PURPLE: Rgb565 = Rgb565::new(Rgb565::MAX_R / 2, 0, Rgb565::MAX_B / 2); 256 | const ZX_NORMAL_GREEN: Rgb565 = Rgb565::new(0, Rgb565::MAX_G / 2, 0); 257 | const ZX_NORMAL_CYAN: Rgb565 = Rgb565::new(0, Rgb565::MAX_G / 2, Rgb565::MAX_B / 2); 258 | const ZX_NORMAL_YELLOW: Rgb565 = Rgb565::new(Rgb565::MAX_R / 2, Rgb565::MAX_G / 2, 0); 259 | const ZX_NORMAL_WHITE: Rgb565 = Rgb565::new(Rgb565::MAX_R / 2, Rgb565::MAX_G / 2, Rgb565::MAX_B / 2); 260 | 261 | fn color_conv(color: &ZXColor, brightness: ZXBrightness) -> Rgb565 { 262 | match (color, brightness) { 263 | (ZXColor::Black, _) => ZX_BLACK, 264 | 265 | // Bright Colors 266 | (ZXColor::Blue, ZXBrightness::Bright) => ZX_BRIGHT_BLUE, 267 | (ZXColor::Red, ZXBrightness::Bright) => ZX_BRIGHT_RED, 268 | (ZXColor::Purple, ZXBrightness::Bright) => ZX_BRIGHT_PURPLE, 269 | (ZXColor::Green, ZXBrightness::Bright) => ZX_BRIGHT_GREEN, 270 | (ZXColor::Cyan, ZXBrightness::Bright) => ZX_BRIGHT_CYAN, 271 | (ZXColor::Yellow, ZXBrightness::Bright) => ZX_BRIGHT_YELLOW, 272 | (ZXColor::White, ZXBrightness::Bright) => ZX_BRIGHT_WHITE, 273 | 274 | // Normal Colors 275 | (ZXColor::Blue, ZXBrightness::Normal) => ZX_NORMAL_BLUE, 276 | (ZXColor::Red, ZXBrightness::Normal) => ZX_NORMAL_RED, 277 | (ZXColor::Purple, ZXBrightness::Normal) => ZX_NORMAL_PURPLE, 278 | (ZXColor::Green, ZXBrightness::Normal) => ZX_NORMAL_GREEN, 279 | (ZXColor::Cyan, ZXBrightness::Normal) => ZX_NORMAL_CYAN, 280 | (ZXColor::Yellow, ZXBrightness::Normal) => ZX_NORMAL_YELLOW, 281 | (ZXColor::White, ZXBrightness::Normal) => ZX_NORMAL_WHITE, 282 | } 283 | } 284 | 285 | fn handle_key_event(key: pc_keyboard::KeyCode, state: pc_keyboard::KeyState, emulator: &mut Emulator) { 286 | let is_pressed = matches!(state, pc_keyboard::KeyState::Down); 287 | if let Some(mapped_key) = pc_code_to_zxkey(key, is_pressed).or_else(|| pc_code_to_modifier(key, is_pressed)) { 288 | match mapped_key { 289 | Event::ZXKey(k, p) => { 290 | debug!("-> ZXKey"); 291 | emulator.send_key(k, p); 292 | }, 293 | Event::NoEvent => { 294 | error!("Key not implemented"); 295 | }, 296 | Event::ZXKeyWithModifier(k, k2, p) => { 297 | debug!("-> ZXKeyWithModifier"); 298 | emulator.send_key(k, p); 299 | emulator.send_key(k2, p); 300 | } 301 | } 302 | } else { 303 | info!("Mapped key: NoEvent"); 304 | } 305 | } 306 | 307 | fn app_loop( 308 | display: &mut mipidsi::Display, 309 | _color_conv: fn(&ZXColor, ZXBrightness) -> Rgb565, 310 | mut serial: Uart, 311 | ) //-> Result<(), core::fmt::Error> 312 | where 313 | DI: WriteOnlyDataCommand, 314 | M: Model, 315 | RST: OutputPin, 316 | { 317 | // display 318 | // .clear(color_conv(ZXColor::Blue, ZXBrightness::Normal)) 319 | // .map_err(|err| error!("{:?}", err)) 320 | // .ok(); 321 | 322 | info!("Creating emulator"); 323 | 324 | let settings = RustzxSettings { 325 | machine: ZXMachine::Sinclair128K, 326 | // machine: ZXMachine::Sinclair48K, 327 | emulation_mode: EmulationMode::FrameCount(1), 328 | tape_fastload_enabled: true, 329 | kempston_enabled: false, 330 | mouse_enabled: false, 331 | load_default_rom: true, 332 | }; 333 | 334 | info!("Initialize emulator"); 335 | const MAX_FRAME_DURATION: Duration = Duration::from_millis(0); 336 | 337 | let mut emulator: Emulator = 338 | match Emulator::new(settings, host::Esp32HostContext {}) { 339 | Ok(emulator) => emulator, 340 | Err(err) => { 341 | error!("Error creating emulator: {:?}", err); 342 | return; 343 | } 344 | }; 345 | 346 | 347 | 348 | info!("Loading tape"); 349 | let tape_bytes = include_bytes!("../../data/hello.tap"); 350 | let tape_asset = FileAsset::new(tape_bytes); 351 | let _ = emulator.load_tape(rustzx_core::host::Tape::Tap(tape_asset)); 352 | 353 | info!("Entering emulator loop"); 354 | let mut kb = pc_keyboard::Keyboard::new( 355 | ScancodeSet2::new(), 356 | layouts::Us104Key, 357 | HandleControl::MapLettersToUnicode, 358 | ); 359 | 360 | loop { 361 | // info!("Emulating frame"); 362 | let read_result = serial.read(); 363 | match read_result { 364 | Ok(byte) => { 365 | match kb.add_byte(byte) { 366 | Ok(Some(event)) => { 367 | info!("Event {:?}", event); 368 | handle_key_event(event.code, event.state, &mut emulator); 369 | }, 370 | Ok(None) => {}, 371 | Err(_) => {}, 372 | } 373 | } 374 | Err(_) => {}, 375 | } 376 | 377 | match emulator.emulate_frames(MAX_FRAME_DURATION) { 378 | Ok(_) => { 379 | let framebuffer = emulator.screen_buffer(); 380 | if let (Some(top_left), Some(bottom_right)) = (framebuffer.bounding_box_top_left, framebuffer.bounding_box_bottom_right) { 381 | // let width = bottom_right.0 - top_left.0 + 1; // Calculate width 382 | // let height = bottom_right.1 - top_left.1 + 1; // Calculate height 383 | // debug!("Bounding box: {:?} {:?}", top_left, bottom_right); 384 | // debug!("Bounding box size: {}", width * height); 385 | let pixel_iterator = framebuffer.get_region_pixel_iter(top_left, bottom_right); 386 | let _ = display.set_pixels( 387 | top_left.0 as u16 + SCREEN_OFFSET_X, 388 | top_left.1 as u16 + SCREEN_OFFSET_Y, 389 | bottom_right.0 as u16 + SCREEN_OFFSET_X, 390 | bottom_right.1 as u16 + SCREEN_OFFSET_Y, 391 | pixel_iterator); 392 | } 393 | emulator.reset_bounding_box(); 394 | 395 | } 396 | _ => { 397 | error!("Emulation of frame failed"); 398 | } 399 | } } 400 | } 401 | -------------------------------------------------------------------------------- /m5stack-cores3-ps2-keyboard/src/pc_zxkey.rs: -------------------------------------------------------------------------------- 1 | 2 | use rustzx_core::zx::keys::ZXKey; 3 | 4 | use crate::zx_event::Event; 5 | 6 | use pc_keyboard::KeyCode; 7 | 8 | pub(crate) fn pc_code_to_zxkey(keycode: KeyCode, pressed:bool) -> Option { 9 | let zxkey_event:Option = match keycode { 10 | KeyCode::Spacebar => Some(ZXKey::Space), 11 | KeyCode::Key1 => Some(ZXKey::N1), 12 | KeyCode::Key2 => Some(ZXKey::N2), 13 | KeyCode::Key3 => Some(ZXKey::N3), 14 | KeyCode::Key4 => Some(ZXKey::N4), 15 | KeyCode::Key5 => Some(ZXKey::N5), 16 | KeyCode::Key6 => Some(ZXKey::N6), 17 | KeyCode::Key7 => Some(ZXKey::N7), 18 | KeyCode::Key8 => Some(ZXKey::N8), 19 | KeyCode::Key9 => Some(ZXKey::N9), 20 | KeyCode::Key0 => Some(ZXKey::N0), 21 | KeyCode::Q => Some(ZXKey::Q), 22 | KeyCode::W => Some(ZXKey::W), 23 | KeyCode::E => Some(ZXKey::E), 24 | KeyCode::R => Some(ZXKey::R), 25 | KeyCode::T => Some(ZXKey::T), 26 | KeyCode::Y => Some(ZXKey::Y), 27 | KeyCode::U => Some(ZXKey::U), 28 | KeyCode::I => Some(ZXKey::I), 29 | KeyCode::O => Some(ZXKey::O), 30 | KeyCode::P => Some(ZXKey::P), 31 | KeyCode::A => Some(ZXKey::A), 32 | KeyCode::S => Some(ZXKey::S), 33 | KeyCode::D => Some(ZXKey::D), 34 | KeyCode::F => Some(ZXKey::F), 35 | KeyCode::G => Some(ZXKey::G), 36 | KeyCode::H => Some(ZXKey::H), 37 | KeyCode::J => Some(ZXKey::J), 38 | KeyCode::K => Some(ZXKey::K), 39 | KeyCode::L => Some(ZXKey::L), 40 | KeyCode::Z => Some(ZXKey::Z), 41 | KeyCode::X => Some(ZXKey::X), 42 | KeyCode::C => Some(ZXKey::C), 43 | KeyCode::V => Some(ZXKey::V), 44 | KeyCode::B => Some(ZXKey::B), 45 | KeyCode::N => Some(ZXKey::N), 46 | KeyCode::M => Some(ZXKey::M), 47 | 48 | KeyCode::LShift => Some(ZXKey::Shift), 49 | KeyCode::RShift => Some(ZXKey::Shift), 50 | 51 | KeyCode::LAlt => Some(ZXKey::SymShift), 52 | KeyCode::RAlt2 => Some(ZXKey::SymShift), 53 | KeyCode::LControl => Some(ZXKey::SymShift), 54 | KeyCode::RControl => Some(ZXKey::SymShift), 55 | 56 | KeyCode::Return => Some(ZXKey::Enter), 57 | 58 | _ => None, 59 | }; 60 | 61 | return zxkey_event.map(|k| Event::ZXKey(k, pressed)) 62 | } 63 | 64 | pub (crate) fn pc_code_to_modifier(keycode: KeyCode, pressed: bool) -> Option { 65 | let zxkey_event:Option<(ZXKey, ZXKey)> = match keycode { 66 | KeyCode::Backspace => Some((ZXKey::Shift, ZXKey::N0)), 67 | 68 | KeyCode::ArrowLeft => Some((ZXKey::Shift, ZXKey::N5)), 69 | KeyCode::ArrowDown => Some((ZXKey::Shift, ZXKey::N6)), 70 | KeyCode::ArrowUp => Some((ZXKey::Shift, ZXKey::N7)), 71 | KeyCode::ArrowRight => Some((ZXKey::Shift, ZXKey::N8)), 72 | 73 | // ========= Row 2 (the numbers) ========= 74 | KeyCode::OemMinus => Some((ZXKey::SymShift, ZXKey::J)), // - 75 | KeyCode::OemPlus => Some((ZXKey::SymShift, ZXKey::K)), // + 76 | 77 | // ========= Row 4 (ASDF) ========= 78 | KeyCode::Oem1 => Some((ZXKey::SymShift, ZXKey::O)), // ; 79 | // KeyCode::Oem3 => Some((ZXKey::SymShift, ZXKey::Z)), // : 80 | KeyCode::Oem3 => Some((ZXKey::SymShift, ZXKey::N7)), // ' 81 | 82 | // ========= Row 5 (ZXCV) ========= 83 | KeyCode::OemComma => Some((ZXKey::SymShift, ZXKey::N)), // , 84 | KeyCode::OemPeriod => Some((ZXKey::SymShift, ZXKey::M)), // . 85 | KeyCode::Oem2 => Some((ZXKey::SymShift, ZXKey::V)), // / 86 | _ => None, 87 | }; 88 | zxkey_event.map(|(k, k2)| Event::ZXKeyWithModifier(k, k2, pressed)) 89 | } -------------------------------------------------------------------------------- /m5stack-cores3-ps2-keyboard/src/stopwatch.rs: -------------------------------------------------------------------------------- 1 | use rustzx_core::host::Stopwatch; 2 | // use std::time::{Duration, Instant}; 3 | use core::time::Duration; 4 | 5 | pub struct InstantStopwatch { 6 | // timestamp: Instant, 7 | } 8 | 9 | impl Default for InstantStopwatch { 10 | fn default() -> Self { 11 | Self { 12 | // timestamp: Instant::now(), 13 | } 14 | } 15 | } 16 | 17 | impl Stopwatch for InstantStopwatch { 18 | fn new() -> Self { 19 | Self::default() 20 | } 21 | 22 | fn measure(&self) -> Duration { 23 | // self.timestamp.elapsed() 24 | Duration::from_millis(100) 25 | } 26 | } -------------------------------------------------------------------------------- /m5stack-cores3-ps2-keyboard/src/zx_event.rs: -------------------------------------------------------------------------------- 1 | use rustzx_core::{zx::keys::ZXKey}; 2 | 3 | pub enum Event { 4 | NoEvent, 5 | ZXKey(ZXKey, bool), 6 | ZXKeyWithModifier(ZXKey, ZXKey, bool), 7 | // CompoundKey(CompoundKey, bool), 8 | // Kempston(KempstonKey, bool), 9 | // Sinclair(SinclairJoyNum, SinclairKey, bool), 10 | // MouseMove { x: i8, y: i8 }, 11 | // MouseButton(KempstonMouseButton, bool), 12 | // MouseWheel(KempstonMouseWheelDirection), 13 | // SwitchFrameTrace, 14 | // ChangeJoyKeyboardLayer(bool), 15 | // ChangeSpeed(EmulationMode), 16 | // InsertTape, 17 | // StopTape, 18 | // QuickSave, 19 | // QuickLoad, 20 | // OpenFile(PathBuf), 21 | // Exit, 22 | } -------------------------------------------------------------------------------- /m5stack-cores3/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(target_arch = "xtensa")'] 2 | runner = "espflash flash --monitor" 3 | rustflags = [ 4 | "-C", "link-arg=-Tlinkall.x", 5 | "-C", "link-arg=-Trom_functions.x", 6 | "-C", "link-arg=-nostartfiles", 7 | ] 8 | 9 | [build] 10 | # Uncomment the target if you'd like to use automatic code hinting in your IDE 11 | # target = "xtensa-esp32-none-elf" 12 | # target = "xtensa-esp32s2-none-elf" 13 | target = "xtensa-esp32s3-none-elf" 14 | # target = "riscv32imac-unknown-none-elf" 15 | 16 | [unstable] 17 | build-std = [ "core", "alloc" ] 18 | 19 | [env] 20 | # Use clean build after changing ESP_LOGLEVEL 21 | #ESP_LOGLEVEL="TRACE" 22 | ESP_LOGLEVEL="DEBUG" 23 | 24 | -------------------------------------------------------------------------------- /m5stack-cores3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustzx-m5stack-cores3" 3 | version = "2.0.1" 4 | authors = ["Juraj Michálek "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | [target.xtensa-esp32s3-none-elf.dependencies] 9 | hal = { package = "esp32s3-hal", version = "0.15.0", features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread", "psram-8m"] } 10 | esp-backtrace = { version = "0.10.0", features = [ 11 | "esp32s3", 12 | "panic-handler", 13 | "print-uart", 14 | ] } 15 | esp-println = { version = "0.8.0", features = ["esp32s3", "log"] } 16 | 17 | [dependencies] 18 | # critical-section = { version = "1.1.2" } 19 | display-interface = "0.4" 20 | esp-alloc = "0.3.0" 21 | 22 | embassy-sync = { version = "0.5.0" } 23 | embassy-futures = { version = "0.1.0" } 24 | embassy-executor = { version = "0.5.0", package = "embassy-executor", features = ["nightly", "integrated-timers"] } 25 | embassy-time = { version = "0.3.0" } 26 | 27 | embedded-graphics = "0.8.0" 28 | embedded-hal = "1.0.0" 29 | embedded-graphics-framebuf = { version = "0.3.0", git = "https://github.com/georgik/embedded-graphics-framebuf.git", branch = "feature/embedded-graphics-0.8" } 30 | log = "0.4" 31 | mipidsi = "0.7.1" 32 | #panic-halt = "0.2" 33 | shared-bus = { version = "0.3.0" } 34 | esp-display-interface-spi-dma = { version = "0.1.0", features = ["esp32s3"] } 35 | #rustzx-utils = { version = "0.16.0" } 36 | #rustzx-core = { version = "0.16.0", features = ["embedded-roms"] } 37 | #rustzx-utils = { path = "../../rustzx/rustzx-utils" } 38 | #rustzx-core = { path = "../../rustzx/rustzx-core" , features = ["embedded-roms"] } 39 | rustzx-utils = { git = "https://github.com/georgik/rustzx.git", branch = "feature/performance-bounding-box" } 40 | rustzx-core = { git = "https://github.com/georgik/rustzx.git", branch = "feature/performance-bounding-box", features = ["embedded-roms"] } 41 | axp2101 = { git = "https://github.com/georgik/axp2101-rs.git", rev = "60db0e1" } 42 | aw9523 = { git = "https://github.com/georgik/aw9523-rs.git", rev = "af49728" } 43 | esp-wifi = { version = "0.3.0", features = [ "wifi", "utils", "tcp", "smoltcp", "dhcpv4", "phy-enable-usb"] } 44 | 45 | usb-zx = { path = "../usb-zx" } 46 | static_cell = { version = "2.0.0", features = ["nightly"] } 47 | esp-bsp = { version = "0.2.0" } 48 | graphics = { path = "../graphics" } 49 | keyboard-pipe = { path = "../keyboard-pipe" } 50 | uart-keyboard = { path = "../uart-keyboard", features = [ "esp32s3" ] } 51 | esp-now-keyboard = { path = "../esp-now-keyboard", features = [ "esp32s3" ] } 52 | emulator = { path = "../emulator", features = [ "m5stack_cores3" ] } 53 | -------------------------------------------------------------------------------- /m5stack-cores3/diagram.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "author": "Juraj Michálek", 4 | "editor": "wokwi", 5 | "parts": [ 6 | { 7 | "type": "board-esp32-c3-rust-1", 8 | "id": "esp", 9 | "top": -494.32, 10 | "left": -455.03, 11 | "attrs": { "builder": "rust-nostd-esp" } 12 | }, 13 | { 14 | "type": "wokwi-ili9341", 15 | "id": "lcd1", 16 | "top": -541.8, 17 | "left": -132.7, 18 | "rotate": 90, 19 | "attrs": { "flipVertical": "1" } 20 | }, 21 | { "type": "chip-icm42670p", "id": "imu1", "top": -248.58, "left": -302.4, "attrs": {} } 22 | ], 23 | "connections": [ 24 | [ "esp:21", "$serialMonitor:RX", "", [] ], 25 | [ "esp:20", "$serialMonitor:TX", "", [] ], 26 | [ "esp:3V3", "lcd1:VCC", "green", [] ], 27 | [ "esp:GND.1", "lcd1:GND", "black", [] ], 28 | [ "esp:0", "lcd1:SCK", "blue", [] ], 29 | [ "esp:6", "lcd1:MOSI", "orange", [] ], 30 | [ "esp:5", "lcd1:CS", "red", [] ], 31 | [ "esp:4", "lcd1:D/C", "magenta", [] ], 32 | [ "esp:3", "lcd1:RST", "yellow", [] ], 33 | [ "lcd1:LED", "esp:3V3", "white", [] ], 34 | [ "imu1:SDA", "esp:10", "green", [ "v0" ] ], 35 | [ "imu1:SCL", "esp:8.2", "green", [ "v0" ] ], 36 | [ "imu1:VCC", "esp:3V3", "red", [ "v0" ] ], 37 | [ "imu1:GND", "esp:GND", "black", [ "v-192", "h-211.12" ] ] 38 | ], 39 | "serialMonitor": { "display": "terminal", "newline": "lf", "convertEol": true }, 40 | "dependencies": { 41 | "chip-icm42670p": "github:SergioGasquez/wokwi-icm42670p@0.0.4" 42 | } 43 | } -------------------------------------------------------------------------------- /m5stack-cores3/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "esp" 3 | components = ["rustfmt", "rustc-dev"] 4 | targets = ["xtensa-esp32s3-none-elf"] 5 | # targets = ["xtensa-esp32-none-elf", "xtensa-esp32s2-none-elf","xtensa-esp32s3-none-elf"] 6 | -------------------------------------------------------------------------------- /m5stack-cores3/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(type_alias_impl_trait)] 4 | 5 | use esp_display_interface_spi_dma::display_interface_spi_dma; 6 | 7 | use static_cell::make_static; 8 | 9 | use hal::{ 10 | clock::{ClockControl, CpuClock}, 11 | dma::DmaPriority, 12 | embassy, 13 | gdma::Gdma, 14 | i2c, 15 | // interrupt, 16 | peripherals::Peripherals, 17 | prelude::*, 18 | psram, 19 | spi::{ 20 | master::{prelude::*, Spi}, 21 | SpiMode, 22 | }, 23 | Delay, 24 | Rng, 25 | IO, 26 | // Uart 27 | }; 28 | 29 | use embassy_executor::Spawner; 30 | use esp_wifi::esp_now::{EspNow, EspNowError}; 31 | use esp_wifi::{initialize, EspWifiInitFor}; 32 | 33 | use embassy_time::{Duration, Ticker}; 34 | 35 | use esp_backtrace as _; 36 | 37 | // use icm42670::{Address, Icm42670}; 38 | use shared_bus::BusManagerSimple; 39 | 40 | use log::{info, error}; 41 | 42 | use mipidsi::models::Model; 43 | 44 | use axp2101::{ I2CPowerManagementInterface, Axp2101 }; 45 | use aw9523::I2CGpioExpanderInterface; 46 | 47 | use esp_bsp::{lcd_gpios, BoardType, DisplayConfig}; 48 | 49 | use uart_keyboard::uart_receiver; 50 | use esp_now_keyboard::esp_now_receiver; 51 | use emulator::app_loop; 52 | 53 | #[global_allocator] 54 | static ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty(); 55 | 56 | const SCREEN_OFFSET_X: u16 = (320 - 256) / 2; 57 | const SCREEN_OFFSET_Y: u16 = (240 - 192) / 2; 58 | 59 | fn init_psram_heap() { 60 | unsafe { 61 | ALLOCATOR.init(psram::psram_vaddr_start() as *mut u8, psram::PSRAM_BYTES); 62 | } 63 | } 64 | 65 | #[main] 66 | async fn main(spawner: Spawner) -> ! { 67 | let peripherals = Peripherals::take(); 68 | 69 | psram::init_psram(peripherals.PSRAM); 70 | init_psram_heap(); 71 | 72 | let system = peripherals.SYSTEM.split(); 73 | 74 | esp_println::logger::init_logger_from_env(); 75 | 76 | info!("Starting up"); 77 | let clocks = ClockControl::configure(system.clock_control, CpuClock::Clock240MHz).freeze(); 78 | 79 | let io = IO::new(peripherals.GPIO, peripherals.IO_MUX); 80 | let (lcd_sclk, lcd_mosi, lcd_cs, lcd_miso, lcd_dc, _lcd_backlight, lcd_reset) = lcd_gpios!(BoardType::M5StackCoreS3, io); 81 | 82 | // I2C 83 | let sda = io.pins.gpio12; 84 | let scl = io.pins.gpio11; 85 | 86 | let i2c_bus = i2c::I2C::new( 87 | peripherals.I2C0, 88 | sda, 89 | scl, 90 | 400u32.kHz(), 91 | &clocks, 92 | ); 93 | 94 | let bus = BusManagerSimple::new(i2c_bus); 95 | 96 | info!("Initializing AXP2101"); 97 | let axp_interface = I2CPowerManagementInterface::new(bus.acquire_i2c()); 98 | let mut axp = Axp2101::new(axp_interface); 99 | axp.init().unwrap(); 100 | 101 | info!("Initializing GPIO Expander"); 102 | let aw_interface = I2CGpioExpanderInterface::new(bus.acquire_i2c()); 103 | let mut aw = aw9523::Aw9523::new(aw_interface); 104 | aw.init().unwrap(); 105 | 106 | // M5Stack CORE 2 - https://docs.m5stack.com/en/core/core2 107 | // let mut backlight = io.pins.gpio3.into_push_pull_output(); 108 | let mut delay = Delay::new(&clocks); 109 | delay.delay_ms(500u32); 110 | info!("About to initialize the SPI LED driver"); 111 | 112 | let timer_group0 = hal::timer::TimerGroup::new(peripherals.TIMG0, &clocks); 113 | embassy::init(&clocks, timer_group0); 114 | 115 | // ESP-NOW keyboard receiver 116 | let wifi_timer = hal::timer::TimerGroup::new(peripherals.TIMG1, &clocks).timer0; 117 | let rng = Rng::new(peripherals.RNG); 118 | let radio_clock_control = system.radio_clock_control; 119 | 120 | let wifi = peripherals.WIFI; 121 | 122 | let esp_now_init = initialize( 123 | EspWifiInitFor::Wifi, 124 | wifi_timer, 125 | rng, 126 | radio_clock_control, 127 | &clocks, 128 | ); 129 | 130 | match esp_now_init { 131 | Ok(init) => { 132 | info!("ESP-NOW init"); 133 | let esp_now: Result = EspNow::new(&init, wifi); 134 | match esp_now { 135 | Ok(esp_now) => { 136 | spawner.spawn(esp_now_receiver(esp_now)).unwrap(); 137 | } 138 | _ => { 139 | error!("ESP-NOW startup error"); 140 | } 141 | } 142 | } 143 | _ => { 144 | error!("ESP-NOW init error"); 145 | } 146 | } 147 | 148 | // UART Keyboard receiver 149 | // let uart0 = Uart::new(peripherals.UART0, &clocks); 150 | // spawner.spawn(uart_receiver(uart0)).unwrap(); 151 | 152 | let dma = Gdma::new(peripherals.DMA); 153 | let dma_channel = dma.channel0; 154 | 155 | let mut delay = Delay::new(&clocks); 156 | 157 | let descriptors = make_static!([0u32; 8 * 3]); 158 | let rx_descriptors = make_static!([0u32; 8 * 3]); 159 | info!("About to initialize the SPI LED driver"); 160 | 161 | let spi = Spi::new( 162 | peripherals.SPI2, 163 | 40u32.MHz(), 164 | SpiMode::Mode0, 165 | &clocks 166 | ).with_pins( 167 | Some(lcd_sclk), 168 | Some(lcd_mosi), 169 | Some(lcd_miso), 170 | Some(lcd_cs), 171 | ).with_dma( 172 | dma_channel.configure( 173 | false, 174 | &mut *descriptors, 175 | &mut *rx_descriptors, 176 | DmaPriority::Priority0, 177 | ) 178 | ); 179 | 180 | let di = display_interface_spi_dma::new_no_cs(2 * 256 * 192, spi, lcd_dc); 181 | 182 | let display_config = DisplayConfig::for_board(BoardType::M5StackCoreS3); 183 | let mut display = match mipidsi::Builder::ili9342c_rgb565(di) 184 | .with_display_size(display_config.h_res, display_config.v_res) 185 | .with_color_order(mipidsi::ColorOrder::Bgr) 186 | .with_invert_colors(mipidsi::ColorInversion::Inverted) 187 | .init(&mut delay, Some(lcd_reset)) 188 | { 189 | Ok(display) => display, 190 | Err(_e) => { 191 | // Handle the error and possibly exit the application 192 | panic!("Display initialization failed"); 193 | } 194 | }; 195 | 196 | // Main Emulator loop 197 | spawner.spawn(app_loop(display)).unwrap(); 198 | 199 | let mut ticker = Ticker::every(Duration::from_secs(1)); 200 | loop { 201 | info!("Tick"); 202 | ticker.next().await; 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /m5stack-cores3/wokwi.toml: -------------------------------------------------------------------------------- 1 | 2 | 3 | [wokwi] 4 | version = 1 5 | #elf = "target/riscv32imac-unknown-none-elf/debug/rustzx-esp32-c3-devkit-rust" 6 | #firmware = "target/riscv32imac-unknown-none-elf/debug/rustzx-esp32-c3-devkit-rust" 7 | gdbServerPort = 3333 8 | 9 | elf = "target/riscv32imac-unknown-none-elf/release/rustzx-esp32-c3-devkit-rust" 10 | firmware = "target/riscv32imac-unknown-none-elf/release/rustzx-esp32-c3-devkit-rust" 11 | -------------------------------------------------------------------------------- /uart-keyboard/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uart-keyboard" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | esp32-hal = { version = "0.18.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 8 | esp32s2-hal = { version = "0.15.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 9 | esp32s3-hal = { version = "0.15.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 10 | esp32c3-hal = { version = "0.15.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 11 | esp32c6-hal = { version = "0.8.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 12 | esp32h2-hal = { version = "0.6.0", optional = true, default-features = false, features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 13 | 14 | embassy-executor = { version = "0.5.0", package = "embassy-executor", features = ["nightly", "integrated-timers"] } 15 | embedded-io-async = "0.6.1" 16 | embassy-time = { version = "0.3.0" } 17 | # hal = { package = "esp32c6-hal", version = "0.7.0" , features = ["embassy", "async", "embassy-time-timg0", "rt", "embassy-executor-thread"] } 18 | keyboard-pipe = { path = "../keyboard-pipe" } 19 | log = "0.4" 20 | usb-zx = { path = "../usb-zx" } 21 | 22 | [features] 23 | # default = [ "esp32" ] 24 | esp32 = [ "esp32-hal" ] 25 | esp32s2 = [ "esp32s2-hal" ] 26 | esp32s3 = [ "esp32s3-hal" ] 27 | esp32c3 = [ "esp32c3-hal" ] 28 | esp32c6 = [ "esp32c6-hal" ] 29 | esp32h2 = [ "esp32h2-hal" ] 30 | 31 | #[patch.crates-io] 32 | #embassy-executor = { git = "https://github.com/embassy-rs/embassy.git", package = "embassy-executor", rev = "14f41a71b6ea9dedb4ee5b9c741fe10575772c7d"} 33 | #embassy-executor-macros = { git = "https://github.com/embassy-rs/embassy.git", package = "embassy-executor-macros", rev = "14f41a71b6ea9dedb4ee5b9c741fe10575772c7d"} 34 | #embassy-time = { git = "https://github.com/embassy-rs/embassy.git", package = "embassy-time", rev = "14f41a71b6ea9dedb4ee5b9c741fe10575772c7d"} 35 | 36 | -------------------------------------------------------------------------------- /uart-keyboard/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![feature(type_alias_impl_trait)] 3 | 4 | #[cfg(feature = "esp32")] 5 | pub use esp32_hal as hal; 6 | #[cfg(feature = "esp32c2")] 7 | pub use esp32c2_hal as hal; 8 | #[cfg(feature = "esp32c3")] 9 | pub use esp32c3_hal as hal; 10 | #[cfg(feature = "esp32c6")] 11 | pub use esp32c6_hal as hal; 12 | #[cfg(feature = "esp32h2")] 13 | pub use esp32h2_hal as hal; 14 | #[cfg(feature = "esp32s2")] 15 | pub use esp32s2_hal as hal; 16 | #[cfg(feature = "esp32s3")] 17 | pub use esp32s3_hal as hal; 18 | 19 | use embassy_time::{Duration, Ticker, Timer}; 20 | use hal::{embassy, peripherals::UART0, Uart}; 21 | use keyboard_pipe::PIPE; 22 | use log::{debug, error, info}; 23 | use usb_zx::{ 24 | uart_usb_key::{uart_code_to_usb_key, uart_composite_code_to_usb_key}, 25 | usb_zx_key::usb_code_to_zxkey, 26 | zx_event::Event, 27 | }; 28 | 29 | async fn usb_write_key(key_state: u8, modifier: u8, key_code: u8) { 30 | let mut bytes = [0u8; 3]; 31 | bytes[0] = key_state; 32 | bytes[1] = modifier; 33 | bytes[2] = key_code; 34 | let bytes_written = PIPE.write(&bytes).await; 35 | if bytes_written != 3 { 36 | error!("Failed to write to pipe"); 37 | } 38 | } 39 | 40 | async fn usb_press_key(modifier: u8, key_code: u8) { 41 | usb_write_key(0, modifier, key_code).await; 42 | usb_write_key(1, modifier, key_code).await; 43 | } 44 | 45 | /// Read from UART and send to emulator 46 | #[embassy_executor::task] 47 | pub async fn uart_receiver(uart0: Uart<'static, UART0>) { 48 | info!("UART receiver task"); 49 | 50 | let (_, mut rx) = uart0.split(); 51 | const MAX_BUFFER_SIZE: usize = 16; 52 | let mut rbuf: [u8; MAX_BUFFER_SIZE] = [0u8; MAX_BUFFER_SIZE]; 53 | 54 | loop { 55 | let result = embedded_io_async::Read::read(&mut rx, &mut rbuf).await; 56 | match result { 57 | Ok(bytes_read) => { 58 | info!("UART read: {} bytes", bytes_read); 59 | if bytes_read == 1 { 60 | info!("UART read: {:x}", rbuf[0]); 61 | match uart_code_to_usb_key(rbuf[0]) { 62 | Some((modifier, key_code)) => { 63 | usb_press_key(modifier, key_code).await; 64 | } 65 | None => { 66 | error!("Unknown key code"); 67 | } 68 | } 69 | } else if bytes_read == 3 { 70 | info!("UART read: {:x} {:x} {:x}", rbuf[0], rbuf[1], rbuf[2]); 71 | match uart_composite_code_to_usb_key(rbuf[0], rbuf[1], rbuf[2]) { 72 | Some((modifier, key_code)) => { 73 | usb_press_key(modifier, key_code).await; 74 | } 75 | None => { 76 | error!("Unknown key code"); 77 | } 78 | } 79 | } 80 | } 81 | Err(e) => { 82 | error!("UART read error: {:?}", e); 83 | } 84 | } 85 | 86 | Timer::after(Duration::from_millis(5)).await; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /usb-zx/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "usb-zx" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | 7 | [dependencies] 8 | rustzx-core = { git = "https://github.com/georgik/rustzx.git", branch = "feature/performance-bounding-box", features = ["embedded-roms"] } 9 | -------------------------------------------------------------------------------- /usb-zx/README.md: -------------------------------------------------------------------------------- 1 | # USB ZX 2 | 3 | Key conversion from USB key codes to ZX key codes. 4 | 5 | -------------------------------------------------------------------------------- /usb-zx/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | pub mod uart_usb_key; 3 | pub mod usb_zx_key; 4 | pub mod zx_event; 5 | -------------------------------------------------------------------------------- /usb-zx/src/uart_usb_key.rs: -------------------------------------------------------------------------------- 1 | pub fn uart_code_to_usb_key(keycode: u8) -> Option<(u8, u8)> { 2 | match keycode { 3 | 0x08 => Some((0x00, 0x2a)), // Backspace 4 | 0x0d => Some((0x00, 0x28)), // Enter 5 | 0x20 => Some((0x00, 0x2c)), // Space 6 | 0x31 => Some((0x00, 0x1e)), // 1 7 | 0x32 => Some((0x00, 0x1f)), // 2 8 | 0x33 => Some((0x00, 0x20)), // 3 9 | 0x34 => Some((0x00, 0x21)), // 4 10 | 0x35 => Some((0x00, 0x22)), // 5 11 | 0x36 => Some((0x00, 0x23)), // 6 12 | 0x37 => Some((0x00, 0x24)), // 7 13 | 0x38 => Some((0x00, 0x25)), // 8 14 | 0x39 => Some((0x00, 0x26)), // 9 15 | 0x30 => Some((0x00, 0x27)), // 0 16 | 0x71 => Some((0x00, 0x14)), // q 17 | 0x77 => Some((0x00, 0x1a)), // w 18 | 0x65 => Some((0x00, 0x08)), // e 19 | 0x72 => Some((0x00, 0x15)), // r 20 | 0x74 => Some((0x00, 0x17)), // t 21 | 0x79 => Some((0x00, 0x1c)), // y 22 | 0x75 => Some((0x00, 0x18)), // u 23 | 0x69 => Some((0x00, 0x0c)), // i 24 | 0x6f => Some((0x00, 0x12)), // o 25 | 0x70 => Some((0x00, 0x13)), // p 26 | 0x61 => Some((0x00, 0x04)), // a 27 | 0x73 => Some((0x00, 0x16)), // s 28 | 0x64 => Some((0x00, 0x07)), // d 29 | 0x66 => Some((0x00, 0x09)), // f 30 | 0x67 => Some((0x00, 0x0a)), // g 31 | 0x68 => Some((0x00, 0x0b)), // h 32 | 0x6a => Some((0x00, 0x0d)), // j 33 | 0x6b => Some((0x00, 0x0e)), // k 34 | 0x6c => Some((0x00, 0x0f)), // l 35 | 0x7a => Some((0x00, 0x1d)), // z 36 | 0x78 => Some((0x00, 0x1b)), // x 37 | 0x63 => Some((0x00, 0x06)), // c 38 | 0x76 => Some((0x00, 0x19)), // v 39 | 0x62 => Some((0x00, 0x05)), // b 40 | 0x6e => Some((0x00, 0x11)), // n 41 | 0x6d => Some((0x00, 0x10)), // m 42 | 0x2c => Some((0x00, 0x33)), // , 43 | 0x2e => Some((0x00, 0x34)), // . 44 | 0x2f => Some((0x00, 0x35)), // / 45 | 0x3b => Some((0x00, 0x2b)), // ; 46 | 0x27 => Some((0x00, 0x2f)), // ' 47 | 0x5b => Some((0x00, 0x2f)), // [ 48 | 0x5d => Some((0x00, 0x30)), // ] 49 | 0x5c => Some((0x00, 0x31)), // \ 50 | 0x2d => Some((0x00, 0x2d)), // - 51 | 0x3d => Some((0x00, 0x2e)), // = 52 | 0x60 => Some((0x00, 0x35)), // ` 53 | 0x21 => Some((0x02, 0x1e)), // ! 54 | 0x40 => Some((0x02, 0x1f)), // @ 55 | 0x23 => Some((0x02, 0x20)), // # 56 | 0x24 => Some((0x02, 0x21)), // $ 57 | 0x25 => Some((0x02, 0x22)), // % 58 | 0x5e => Some((0x02, 0x23)), // ^ 59 | 0x26 => Some((0x02, 0x24)), // & 60 | 0x2a => Some((0x02, 0x25)), // * 61 | 0x28 => Some((0x02, 0x26)), // ( 62 | 0x29 => Some((0x02, 0x27)), // ) 63 | 0x5f => Some((0x02, 0x2d)), // _ 64 | 0x2b => Some((0x02, 0x2e)), // + 65 | 0x7e => Some((0x02, 0x35)), // ~ 66 | 0x51 => Some((0x02, 0x14)), // Q 67 | 0x57 => Some((0x02, 0x1a)), // W 68 | 0x45 => Some((0x02, 0x08)), // E 69 | 0x52 => Some((0x02, 0x15)), // R 70 | 0x54 => Some((0x02, 0x17)), // T 71 | 0x59 => Some((0x02, 0x1c)), // Y 72 | 0x55 => Some((0x02, 0x18)), // U 73 | 0x49 => Some((0x02, 0x0c)), // I 74 | 0x4f => Some((0x02, 0x12)), // O 75 | 0x50 => Some((0x02, 0x13)), // P 76 | 0x41 => Some((0x02, 0x04)), // A 77 | 0x53 => Some((0x02, 0x16)), // S 78 | 0x44 => Some((0x02, 0x07)), // D 79 | 0x46 => Some((0x02, 0x09)), // F 80 | 0x47 => Some((0x02, 0x0a)), // G 81 | 0x48 => Some((0x02, 0x0b)), // H 82 | 0x4a => Some((0x02, 0x0d)), // J 83 | 0x4b => Some((0x02, 0x0e)), // K 84 | 0x4c => Some((0x02, 0x0f)), // L 85 | 0x5a => Some((0x02, 0x1d)), // Z 86 | 0x58 => Some((0x02, 0x1b)), // X 87 | 0x43 => Some((0x02, 0x06)), // C 88 | 0x56 => Some((0x02, 0x19)), // V 89 | 0x42 => Some((0x02, 0x05)), // B 90 | 0x4e => Some((0x02, 0x11)), // N 91 | 0x4d => Some((0x02, 0x10)), // M 92 | 0x3c => Some((0x02, 0x33)), // < 93 | 0x3e => Some((0x02, 0x34)), // > 94 | 0x3f => Some((0x02, 0x35)), // ? 95 | 0x3a => Some((0x02, 0x2b)), // : 96 | 0x22 => Some((0x02, 0x34)), // " 97 | 0x7b => Some((0x02, 0x2f)), // { 98 | 0x7d => Some((0x02, 0x30)), // } 99 | 0x7c => Some((0x02, 0x31)), // | 100 | 101 | _ => None, 102 | } 103 | } 104 | 105 | pub fn uart_composite_code_to_usb_key(code1: u8, code2: u8, code3: u8) -> Option<(u8, u8)> { 106 | match (code1, code2, code3) { 107 | (0x1b, 0x5b, 0x41) => Some((0x0, 0x52)), // Up 108 | (0x1b, 0x5b, 0x42) => Some((0x0, 0x51)), // Down 109 | (0x1b, 0x5b, 0x43) => Some((0x0, 0x4f)), // Right 110 | (0x1b, 0x5b, 0x44) => Some((0x0, 0x50)), // Left 111 | 112 | _ => None, 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /usb-zx/src/usb_zx_key.rs: -------------------------------------------------------------------------------- 1 | use rustzx_core::zx::keys::ZXKey; 2 | 3 | use crate::zx_event::Event; 4 | 5 | pub fn usb_code_to_zxkey(pressed: bool, modifier: u8, keycode: u8) -> Option { 6 | if modifier == 0 { 7 | let zxkey_event: Option = match keycode { 8 | 44 => Some(ZXKey::Space), 9 | 10 | 30 => Some(ZXKey::N1), 11 | 31 => Some(ZXKey::N2), 12 | 32 => Some(ZXKey::N3), 13 | 33 => Some(ZXKey::N4), 14 | 34 => Some(ZXKey::N5), // 5 15 | 35 => Some(ZXKey::N6), 16 | 36 => Some(ZXKey::N7), 17 | 37 => Some(ZXKey::N8), 18 | 38 => Some(ZXKey::N9), 19 | 39 => Some(ZXKey::N0), 20 | 21 | 20 => Some(ZXKey::Q), 22 | 26 => Some(ZXKey::W), 23 | 8 => Some(ZXKey::E), 24 | 21 => Some(ZXKey::R), 25 | 23 => Some(ZXKey::T), 26 | 28 => Some(ZXKey::Y), 27 | 24 => Some(ZXKey::U), 28 | 12 => Some(ZXKey::I), 29 | 18 => Some(ZXKey::O), 30 | 19 => Some(ZXKey::P), 31 | 32 | 4 => Some(ZXKey::A), 33 | 22 => Some(ZXKey::S), 34 | 7 => Some(ZXKey::D), 35 | 9 => Some(ZXKey::F), 36 | 10 => Some(ZXKey::G), 37 | 11 => Some(ZXKey::H), 38 | 13 => Some(ZXKey::J), 39 | 14 => Some(ZXKey::K), 40 | 15 => Some(ZXKey::L), 41 | 42 | 29 => Some(ZXKey::Z), 43 | 27 => Some(ZXKey::X), 44 | 6 => Some(ZXKey::C), 45 | 25 => Some(ZXKey::V), 46 | 5 => Some(ZXKey::B), 47 | 17 => Some(ZXKey::N), 48 | 16 => Some(ZXKey::M), 49 | 50 | // KeyCode::LShift => Some(ZXKey::Shift), 51 | // KeyCode::RShift => Some(ZXKey::Shift), 52 | 53 | // KeyCode::LAlt => Some(ZXKey::SymShift), 54 | // KeyCode::RAlt2 => Some(ZXKey::SymShift), 55 | // KeyCode::LControl => Some(ZXKey::SymShift), 56 | // KeyCode::RControl => Some(ZXKey::SymShift), 57 | 40 => Some(ZXKey::Enter), // Enter 58 | 59 | _ => None, 60 | }; 61 | 62 | if zxkey_event.is_some() { 63 | return zxkey_event.map(|k| Event::ZXKey(k, pressed)); 64 | } 65 | } 66 | 67 | let zxkey_event: Option<(ZXKey, ZXKey)> = match (modifier, keycode) { 68 | (0, 42) => Some((ZXKey::Shift, ZXKey::N0)), // Backspace 69 | 70 | (0, 80) => Some((ZXKey::Shift, ZXKey::N5)), // Left 71 | (0, 81) => Some((ZXKey::Shift, ZXKey::N6)), // Down 72 | (0, 82) => Some((ZXKey::Shift, ZXKey::N7)), // Up 73 | (0, 79) => Some((ZXKey::Shift, ZXKey::N8)), // Right 74 | 75 | (2, 20) => Some((ZXKey::Shift, ZXKey::Q)), 76 | (2, 26) => Some((ZXKey::Shift, ZXKey::W)), 77 | (2, 8) => Some((ZXKey::Shift, ZXKey::E)), 78 | (2, 21) => Some((ZXKey::Shift, ZXKey::R)), 79 | (2, 23) => Some((ZXKey::Shift, ZXKey::T)), 80 | (2, 28) => Some((ZXKey::Shift, ZXKey::Y)), 81 | (2, 24) => Some((ZXKey::Shift, ZXKey::U)), 82 | (2, 12) => Some((ZXKey::Shift, ZXKey::I)), 83 | (2, 18) => Some((ZXKey::Shift, ZXKey::O)), 84 | (2, 19) => Some((ZXKey::Shift, ZXKey::P)), 85 | 86 | (2, 4) => Some((ZXKey::Shift, ZXKey::A)), 87 | (2, 22) => Some((ZXKey::Shift, ZXKey::S)), 88 | (2, 7) => Some((ZXKey::Shift, ZXKey::D)), 89 | (2, 9) => Some((ZXKey::Shift, ZXKey::F)), 90 | (2, 10) => Some((ZXKey::Shift, ZXKey::G)), 91 | (2, 11) => Some((ZXKey::Shift, ZXKey::H)), 92 | (2, 13) => Some((ZXKey::Shift, ZXKey::J)), 93 | (2, 14) => Some((ZXKey::Shift, ZXKey::K)), 94 | (2, 15) => Some((ZXKey::Shift, ZXKey::L)), 95 | 96 | (2, 29) => Some((ZXKey::Shift, ZXKey::Z)), 97 | (2, 27) => Some((ZXKey::Shift, ZXKey::X)), 98 | (2, 6) => Some((ZXKey::Shift, ZXKey::C)), 99 | (2, 25) => Some((ZXKey::Shift, ZXKey::V)), 100 | (2, 5) => Some((ZXKey::Shift, ZXKey::B)), 101 | (2, 17) => Some((ZXKey::Shift, ZXKey::N)), 102 | (2, 16) => Some((ZXKey::Shift, ZXKey::M)), 103 | 104 | // // ========= Row 2 (the numbers) ========= 105 | (0, 45) => Some((ZXKey::SymShift, ZXKey::J)), // - 106 | (0, 86) => Some((ZXKey::SymShift, ZXKey::J)), // - NumPad 107 | (0, 87) => Some((ZXKey::SymShift, ZXKey::K)), // + NumPad 108 | 109 | // // ========= Row 4 (ASDF) ========= 110 | (0, 51) => Some((ZXKey::SymShift, ZXKey::O)), // ; 111 | (2, 52) => Some((ZXKey::SymShift, ZXKey::P)), // " 112 | // // KeyCode::Oem3 => Some((ZXKey::SymShift, ZXKey::Z)), // : 113 | // KeyCode::Oem3 => Some((ZXKey::SymShift, ZXKey::N7)), // ' 114 | 115 | // // ========= Row 5 (ZXCV) ========= 116 | (0, 54) => Some((ZXKey::SymShift, ZXKey::N)), // , 117 | (0, 55) => Some((ZXKey::SymShift, ZXKey::M)), // . 118 | (0, 56) => Some((ZXKey::SymShift, ZXKey::V)), // / 119 | _ => None, 120 | }; 121 | zxkey_event.map(|(k, k2)| Event::ZXKeyWithModifier(k, k2, pressed)) 122 | } 123 | -------------------------------------------------------------------------------- /usb-zx/src/zx_event.rs: -------------------------------------------------------------------------------- 1 | use rustzx_core::zx::keys::ZXKey; 2 | 3 | pub enum Event { 4 | NoEvent, 5 | ZXKey(ZXKey, bool), 6 | ZXKeyWithModifier(ZXKey, ZXKey, bool), 7 | // CompoundKey(CompoundKey, bool), 8 | // Kempston(KempstonKey, bool), 9 | // Sinclair(SinclairJoyNum, SinclairKey, bool), 10 | // MouseMove { x: i8, y: i8 }, 11 | // MouseButton(KempstonMouseButton, bool), 12 | // MouseWheel(KempstonMouseWheelDirection), 13 | // SwitchFrameTrace, 14 | // ChangeJoyKeyboardLayer(bool), 15 | // ChangeSpeed(EmulationMode), 16 | // InsertTape, 17 | // StopTape, 18 | // QuickSave, 19 | // QuickLoad, 20 | // OpenFile(PathBuf), 21 | // Exit, 22 | } 23 | --------------------------------------------------------------------------------