├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── ci-risc-v-imac.yml │ ├── ci-risc-v-imc.yml │ ├── ci-xtensa.yml │ ├── release-risc-v-imac.yml │ ├── release-risc-v-imc.yml │ └── release-xtensa.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE-APACHE ├── LICENSE-MIT ├── docs ├── README.md ├── esope-sld-c-w-s3.jpg ├── esp32-c3-lcdkit-conway.jpg ├── esp32-conways-game-of-life-rs.png ├── esp32-s3-box-3-conway.jpg ├── esp32-s3-lcd-ev-board-conway.jpg ├── esp32-wrover-kit.jpg ├── m5stack-atom-s3.jpg ├── m5stack-cores3-conway.jpg └── waveshare-esp32-s3-touch-lcd-1_28.jpg ├── esope-sld-c-w-s3 ├── .cargo │ └── config.toml ├── Cargo.toml ├── build.rs ├── rust-toolchain.toml └── src │ ├── bin │ └── main.rs │ └── lib.rs ├── esp32-c3-lcdkit ├── .cargo │ └── config.toml ├── Cargo.toml ├── build.rs ├── rust-toolchain.toml └── src │ └── main.rs ├── esp32-s3-box-3-minimal ├── .cargo │ └── config.toml ├── Cargo.toml ├── build.rs ├── diagram.json ├── rust-toolchain.toml ├── src │ └── main.rs └── wokwi.toml ├── esp32-s3-box-3 ├── .cargo │ └── config.toml ├── Cargo.toml ├── build.rs ├── diagram.json ├── rust-toolchain.toml ├── src │ └── main.rs └── wokwi.toml ├── esp32-s3-lcd-ev-board ├── .cargo │ └── config.toml ├── Cargo.toml ├── build.rs ├── rust-toolchain.toml └── src │ └── main.rs ├── esp32-wrover-kit ├── .cargo │ └── config.toml ├── Cargo.toml ├── build.rs ├── rust-toolchain.toml └── src │ └── main.rs ├── m5stack-atom-s3 ├── .cargo │ └── config.toml ├── Cargo.toml ├── build.rs ├── rust-toolchain.toml └── src │ └── main.rs ├── m5stack-cores3 ├── .cargo │ └── config.toml ├── Cargo.toml ├── build.rs ├── rust-toolchain.toml └── src │ └── main.rs ├── scripts └── fix-code.sh ├── wasm ├── Cargo.toml ├── pkg │ └── index.html └── src │ └── lib.rs ├── waveshare-esp32-c6-lcd-1_47 ├── .cargo │ └── config.toml ├── Cargo.toml ├── build.rs ├── diagram.json ├── rust-toolchain.toml ├── src │ └── main.rs └── wokwi.toml └── waveshare-esp32-s3-touch-lcd-1_28 ├── .cargo └── config.toml ├── Cargo.toml ├── build.rs ├── rust-toolchain.toml └── src └── main.rs /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image 2 | ARG VARIANT=bullseye-slim 3 | FROM debian:${VARIANT} 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | ENV LC_ALL=C.UTF-8 6 | ENV LANG=C.UTF-8 7 | 8 | # Arguments 9 | ARG CONTAINER_USER=esp 10 | ARG CONTAINER_GROUP=esp 11 | ARG ESP_BOARD=all 12 | ARG GITHUB_TOKEN 13 | 14 | # Install dependencies 15 | RUN apt-get update \ 16 | && apt-get install -y git curl llvm-dev libclang-dev clang unzip \ 17 | libusb-1.0-0 libssl-dev libudev-dev pkg-config libpython2.7 \ 18 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts 19 | 20 | # Set users 21 | RUN adduser --disabled-password --gecos "" ${CONTAINER_USER} 22 | USER ${CONTAINER_USER} 23 | WORKDIR /home/${CONTAINER_USER} 24 | 25 | # Install rustup 26 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- \ 27 | --default-toolchain none -y --profile minimal 28 | 29 | # Update envs 30 | ENV PATH=${PATH}:/home/${CONTAINER_USER}/.cargo/bin 31 | 32 | # Install extra crates 33 | RUN ARCH=$($HOME/.cargo/bin/rustup show | grep "Default host" | sed -e 's/.* //') && \ 34 | curl -L "https://github.com/esp-rs/espup/releases/latest/download/espup-${ARCH}" -o "${HOME}/.cargo/bin/espup" && \ 35 | chmod u+x "${HOME}/.cargo/bin/espup" && \ 36 | curl -L "https://github.com/esp-rs/espflash/releases/latest/download/cargo-espflash-${ARCH}.zip" -o "${HOME}/.cargo/bin/cargo-espflash.zip" && \ 37 | unzip "${HOME}/.cargo/bin/cargo-espflash.zip" -d "${HOME}/.cargo/bin/" && \ 38 | rm "${HOME}/.cargo/bin/cargo-espflash.zip" && \ 39 | chmod u+x "${HOME}/.cargo/bin/cargo-espflash" && \ 40 | curl -L "https://github.com/bjoernQ/esp-web-flash-server/releases/latest/download/web-flash-${ARCH}.zip" -o "${HOME}/.cargo/bin/web-flash.zip" && \ 41 | unzip "${HOME}/.cargo/bin/web-flash.zip" -d "${HOME}/.cargo/bin/" && \ 42 | rm "${HOME}/.cargo/bin/web-flash.zip" && \ 43 | chmod u+x "${HOME}/.cargo/bin/web-flash" 44 | 45 | # Install Xtensa Rust 46 | RUN if [ -n "${GITHUB_TOKEN}" ]; then export GITHUB_TOKEN=${GITHUB_TOKEN}; fi \ 47 | && ${HOME}/.cargo/bin/espup install\ 48 | --targets "${ESP_BOARD}" \ 49 | --log-level debug \ 50 | --export-file /home/${CONTAINER_USER}/export-esp.sh 51 | 52 | # Set default toolchain 53 | RUN rustup default esp 54 | 55 | # Activate ESP environment 56 | RUN echo "source /home/${CONTAINER_USER}/export-esp.sh" >> ~/.bashrc 57 | 58 | CMD [ "/bin/bash" ] 59 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esp32_conways_game_of_life_rs", 3 | // Select between image and build properties to pull or build the image. 4 | // "image": "docker.io/espressif/idf-rust:esp32s3_latest", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | "CONTAINER_USER": "esp", 9 | "CONTAINER_GROUP": "esp", 10 | "ESP_BOARD": "esp32s3" 11 | } 12 | }, 13 | "customizations": { 14 | "vscode": { 15 | "settings": { 16 | "editor.formatOnPaste": true, 17 | "editor.formatOnSave": true, 18 | "editor.formatOnSaveMode": "file", 19 | "editor.formatOnType": true, 20 | "lldb.executable": "/usr/bin/lldb", 21 | "files.watcherExclude": { 22 | "**/target/**": true 23 | }, 24 | "rust-analyzer.checkOnSave.command": "clippy", 25 | "rust-analyzer.checkOnSave.allTargets": false, 26 | "[rust]": { 27 | "editor.defaultFormatter": "rust-lang.rust-analyzer" 28 | } 29 | }, 30 | "extensions": [ 31 | "rust-lang.rust-analyzer", 32 | "tamasfe.even-better-toml", 33 | "serayuzgur.crates", 34 | "mutantdino.resourcemonitor", 35 | "yzhang.markdown-all-in-one", 36 | "ms-vscode.cpptools", 37 | "actboy168.tasks", 38 | "Wokwi.wokwi-vscode" 39 | ] 40 | } 41 | }, 42 | "forwardPorts": [ 43 | 8000, 44 | 3333 45 | ], 46 | "workspaceMount": "source=${localWorkspaceFolder},target=/home/esp/esp32_conways_game_of_life_rs,type=bind,consistency=cached", 47 | "workspaceFolder": "/home/esp/esp32_conways_game_of_life_rs" 48 | } 49 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci-risc-v-imac.yml: -------------------------------------------------------------------------------- 1 | name: CI RISC-V IMAC 2 | 3 | on: 4 | push: 5 | paths: 6 | - "esp32-c6-waveshare-1_47/**" 7 | workflow_dispatch: 8 | inputs: 9 | release_name: 10 | description: 'Name of the GitHub Release' 11 | required: true 12 | default: 'v0.5.0' 13 | release_tag: 14 | description: 'Tag for the GitHub Release' 15 | required: true 16 | default: 'v0.5.0' 17 | prefix: 18 | description: 'Prefix for binary name' 19 | required: true 20 | default: 'esp32-conways-game-of-life' 21 | board: 22 | description: 'Target directory for the ESP32-C6 project (e.g. esp32-c6-waveshare-1_47)' 23 | required: true 24 | default: 'esp32-c6-waveshare-1_47' 25 | 26 | env: 27 | CARGO_TERM_COLOR: always 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | jobs: 31 | riscv-imc-release: 32 | name: RISC-V IMAC CI (ESP32-C6 Projects) 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout Repository 36 | uses: actions/checkout@v4 37 | 38 | - name: Setup Rust for RISC-V IMAC 39 | uses: actions-rs/toolchain@v1 40 | with: 41 | toolchain: stable 42 | target: riscv32imac-unknown-none-elf 43 | 44 | - name: Build ESP32-C6 Project and Collect Assets 45 | run: | 46 | cd "${{ github.event.inputs.board }}" 47 | cargo build --release 48 | cargo fmt --all -- --check --color always 49 | cargo clippy --all-features --workspace -- -D warnings 50 | -------------------------------------------------------------------------------- /.github/workflows/ci-risc-v-imc.yml: -------------------------------------------------------------------------------- 1 | name: CI RISC-V IMC 2 | 3 | on: 4 | push: 5 | paths: 6 | - "esp32-c3-lcdkit/**" 7 | workflow_dispatch: 8 | inputs: 9 | release_name: 10 | description: 'Name of the GitHub Release' 11 | required: true 12 | default: 'v0.5.0' 13 | release_tag: 14 | description: 'Tag for the GitHub Release' 15 | required: true 16 | default: 'v0.5.0' 17 | prefix: 18 | description: 'Prefix for binary name' 19 | required: true 20 | default: 'esp32-conways-game-of-life' 21 | 22 | env: 23 | CARGO_TERM_COLOR: always 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | jobs: 27 | riscv-imc-release: 28 | name: RISC-V IMC CI (ESP32-C3 Projects) 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout Repository 32 | uses: actions/checkout@v4 33 | 34 | - name: Setup Rust for RISC-V IMC 35 | uses: actions-rs/toolchain@v1 36 | with: 37 | toolchain: stable 38 | target: riscv32imc-unknown-none-elf 39 | 40 | - name: Build ESP32-C3 Project and Collect Assets 41 | run: | 42 | cd "esp32-c3-lcdkit" 43 | cargo build --release 44 | cargo fmt --all -- --check --color always 45 | cargo clippy --all-features --workspace -- -D warnings 46 | -------------------------------------------------------------------------------- /.github/workflows/ci-xtensa.yml: -------------------------------------------------------------------------------- 1 | name: CI Xtensa 2 | 3 | on: 4 | push: 5 | paths: 6 | - "esp32-s3-box-3/**" 7 | workflow_dispatch: 8 | inputs: 9 | release_name: 10 | description: 'Name of the GitHub Release' 11 | required: true 12 | default: 'v0.5.0' 13 | release_tag: 14 | description: 'Tag for the GitHub Release' 15 | required: true 16 | default: 'v0.5.0' 17 | prefix: 18 | description: 'Prefix for binary name' 19 | required: true 20 | default: 'esp32-conways-game-of-life' 21 | 22 | env: 23 | CARGO_TERM_COLOR: always 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | jobs: 27 | xtensa-release: 28 | name: Xtensa Release (ESP32-S3 Projects) 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout Repository 32 | uses: actions/checkout@v4 33 | 34 | - name: Install espup and set toolchain 1.85.0.0 35 | run: | 36 | curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash 37 | cargo binstall espflash espup 38 | espup install --toolchain-version 1.85.0.0 39 | source ~/export-esp.sh 40 | 41 | - name: Setup Rust for Xtensa (ESP32-S3) 42 | uses: esp-rs/xtensa-toolchain@v1.5 43 | with: 44 | default: true 45 | buildtargets: esp32s3 46 | ldproxy: false 47 | version: "1.85.0" 48 | 49 | - name: Build Xtensa Projects, Create Flashable Images, and Collect Assets 50 | run: | 51 | # Build the box project. 52 | cd "esp32-s3-box-3" 53 | cargo build --release 54 | cargo fmt --all -- --check --color always 55 | cargo clippy --all-features --workspace -- -D warnings 56 | -------------------------------------------------------------------------------- /.github/workflows/release-risc-v-imac.yml: -------------------------------------------------------------------------------- 1 | name: Release RISC-V IMAC 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_name: 7 | description: 'Name of the GitHub Release' 8 | required: true 9 | default: 'v0.5.0' 10 | release_tag: 11 | description: 'Tag for the GitHub Release' 12 | required: true 13 | default: 'v0.5.0' 14 | prefix: 15 | description: 'Prefix for binary name' 16 | required: true 17 | default: 'esp32-conways-game-of-life' 18 | board: 19 | description: 'Target directory for the ESP32-C3 project (e.g. esp32-c3-lcdkit)' 20 | required: true 21 | default: 'waveshare-esp32-c6-1_47' 22 | 23 | 24 | env: 25 | CARGO_TERM_COLOR: always 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | jobs: 29 | 30 | riscv-imac-release: 31 | name: RISC-V IMAC Release (ESP32-C6 Projects) 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@v4 36 | 37 | - name: Setup Rust for RISC-V IMAC 38 | uses: actions-rs/toolchain@v1 39 | with: 40 | toolchain: stable 41 | target: riscv32imac-unknown-none-elf 42 | 43 | - name: Build ESP32-C6 Project and collect assets 44 | run: | 45 | curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash 46 | cargo binstall espflash 47 | cd "${{ github.event.inputs.board }}" && cargo build --release && cd .. 48 | mkdir -p release_riscv_imac 49 | espflash save-image --chip esp32c3 "${{ github.event.inputs.board }}/target/riscv32imac-unknown-none-elf/release/esp32-conways-game-of-life-rs" release_riscv_imac/esp32-conways-game-of-life-rs 50 | 51 | - name: Check if Release Exists 52 | id: check_release 53 | run: | 54 | set +e 55 | gh release view "${{ github.event.inputs.release_tag }}" > /dev/null 2>&1 56 | if [ $? -eq 0 ]; then 57 | echo "Release already exists." 58 | echo "release_exists=true" >> $GITHUB_ENV 59 | else 60 | echo "Release does not exist." 61 | echo "release_exists=false" >> $GITHUB_ENV 62 | fi 63 | set -e 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | 67 | - name: Create Release if Needed 68 | if: env.release_exists == 'false' 69 | run: | 70 | gh release create "${{ github.event.inputs.release_tag }}" --title "${{ github.event.inputs.release_name }}" --prerelease 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | 74 | - name: Upload RISC-V IMAC Assets 75 | run: | 76 | for file in $(find release_riscv_imac -type f); do 77 | base=$(basename "$file") 78 | asset_name="${{ github.event.inputs.prefix }}-${{ github.event.inputs.release_tag }}-${{ github.event.inputs.board }}.bin" 79 | echo "Uploading $file as $asset_name" 80 | ls -l $file 81 | gh release upload "${{ github.event.inputs.release_tag }}" "$file#${asset_name}" --clobber 82 | done 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | -------------------------------------------------------------------------------- /.github/workflows/release-risc-v-imc.yml: -------------------------------------------------------------------------------- 1 | name: Release RISC-V IMC 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_name: 7 | description: 'Name of the GitHub Release' 8 | required: true 9 | default: 'v0.5.0' 10 | release_tag: 11 | description: 'Tag for the GitHub Release' 12 | required: true 13 | default: 'v0.5.0' 14 | prefix: 15 | description: 'Prefix for binary name' 16 | required: true 17 | default: 'esp32-conways-game-of-life' 18 | board: 19 | description: 'Target directory for the ESP32-C3 project (e.g. esp32-c3-lcdkit)' 20 | required: true 21 | default: 'esp32-c3-lcdkit' 22 | 23 | env: 24 | CARGO_TERM_COLOR: always 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | jobs: 28 | riscv-imc-release: 29 | name: RISC-V IMC Release (ESP32-C3 Projects) 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout Repository 33 | uses: actions/checkout@v4 34 | 35 | - name: Setup Rust for RISC-V IMC 36 | uses: actions-rs/toolchain@v1 37 | with: 38 | toolchain: stable 39 | target: riscv32imc-unknown-none-elf 40 | 41 | - name: Build ESP32-C3 Project and Collect Assets 42 | run: | 43 | curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash 44 | cargo binstall espflash 45 | cd "${{ github.event.inputs.board }}" 46 | cargo build --release 47 | cd .. 48 | mkdir -p release_riscv_imc 49 | espflash save-image --chip esp32c3 "${{ github.event.inputs.board }}/target/riscv32imc-unknown-none-elf/release/esp32-conways-game-of-life-rs" release_riscv_imc/esp32-conways-game-of-life-rs 50 | 51 | - name: Check if Release Exists 52 | id: check_release 53 | run: | 54 | set +e 55 | gh release view "${{ github.event.inputs.release_tag }}" > /dev/null 2>&1 56 | if [ $? -eq 0 ]; then 57 | echo "Release already exists." 58 | echo "release_exists=true" >> $GITHUB_ENV 59 | else 60 | echo "Release does not exist." 61 | echo "release_exists=false" >> $GITHUB_ENV 62 | fi 63 | set -e 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | 67 | - name: Create Release if Needed 68 | if: env.release_exists == 'false' 69 | run: | 70 | gh release create "${{ github.event.inputs.release_tag }}" --title "${{ github.event.inputs.release_name }}" --prerelease 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | 74 | - name: Upload RISC-V IMC Assets 75 | run: | 76 | for file in $(find release_riscv_imc -type f); do 77 | base=$(basename "$file") 78 | asset_name="${{ github.event.inputs.prefix }}-${{ github.event.inputs.release_tag }}-${{ github.event.inputs.board }}.bin" 79 | echo "Uploading $file as $asset_name" 80 | ls -l $file 81 | gh release upload "${{ github.event.inputs.release_tag }}" "$file#${asset_name}" --clobber 82 | done 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | -------------------------------------------------------------------------------- /.github/workflows/release-xtensa.yml: -------------------------------------------------------------------------------- 1 | name: Release Xtensa 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_name: 7 | description: 'Name of the GitHub Release' 8 | required: true 9 | default: 'v0.5.0' 10 | release_tag: 11 | description: 'Tag for the GitHub Release' 12 | required: true 13 | default: 'v0.5.0' 14 | prefix: 15 | description: 'Prefix for binary name' 16 | required: true 17 | default: 'esp32-conways-game-of-life' 18 | target_box: 19 | description: 'Target directory for the box project (e.g. esp32-s3-box-3)' 20 | required: true 21 | default: 'esp32-s3-box-3' 22 | target_minimal: 23 | description: 'Target directory for the minimal project (e.g. esp32-s3-box-3-minimal)' 24 | required: true 25 | default: 'esp32-s3-box-3-minimal' 26 | 27 | env: 28 | CARGO_TERM_COLOR: always 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | jobs: 32 | xtensa-release: 33 | name: Xtensa Release (ESP32-S3 Projects) 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout Repository 37 | uses: actions/checkout@v4 38 | 39 | - name: Install espup and set toolchain 1.85.0.0 40 | run: | 41 | cargo install espup --locked 42 | espup install --toolchain-version 1.85.0.0 43 | source ~/export-esp.sh 44 | 45 | - name: Install espflash CLI 46 | run: cargo install espflash --locked 47 | 48 | - name: Setup Rust for Xtensa (ESP32-S3) 49 | uses: esp-rs/xtensa-toolchain@v1.5 50 | with: 51 | default: true 52 | buildtargets: esp32s3 53 | ldproxy: false 54 | version: "1.85.0" 55 | 56 | - name: Build Xtensa Projects, Create Flashable Images, and Collect Assets 57 | run: | 58 | mkdir -p release_xtensa 59 | 60 | # Build the box project. 61 | cd "${{ github.event.inputs.target_box }}" 62 | cargo build --release 63 | # Create flashable image for the box project. 64 | cd .. 65 | # Build the minimal project. 66 | cd "${{ github.event.inputs.target_minimal }}" 67 | cargo build --release 68 | # Create flashable image for the minimal project. 69 | cd .. 70 | espflash save-image "${{ github.event.inputs.target_box }}/target/xtensa-esp32s3-none-elf/release/esp32-conways-game-of-life-rs" release_xtensa/esp32-conways-game-of-life-rs-${{ github.event.inputs.target_box }} 71 | espflash save-image "${{ github.event.inputs.target_minimal }}/target/xtensa-esp32s3-none-elf/release/esp32-conways-game-of-life-rs-minimal" release_xtensa/esp32-conways-game-of-life-rs-${{ github.event.inputs.target_minimal }} 72 | 73 | - name: Check if Release Exists 74 | id: check_release 75 | run: | 76 | set +e 77 | gh release view "${{ github.event.inputs.release_tag }}" > /dev/null 2>&1 78 | if [ $? -eq 0 ]; then 79 | echo "Release already exists." 80 | echo "release_exists=true" >> $GITHUB_ENV 81 | else 82 | echo "Release does not exist." 83 | echo "release_exists=false" >> $GITHUB_ENV 84 | fi 85 | set -e 86 | 87 | - name: Create Release if Needed 88 | if: env.release_exists == 'false' 89 | run: | 90 | gh release create "${{ github.event.inputs.release_tag }}" --title "${{ github.event.inputs.release_name }}" --prerelease 91 | 92 | - name: Upload Xtensa Assets 93 | run: | 94 | for file in $(find release_xtensa -type f); do 95 | base=$(basename "$file") 96 | asset_name="${{ github.event.inputs.prefix }}-${{ github.event.inputs.release_tag }}-${base}" 97 | echo "Uploading $file as $asset_name" 98 | gh release upload "${{ github.event.inputs.release_tag }}" "$file#${asset_name}" --clobber 99 | done 100 | env: 101 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | Cargo.lock 13 | .idea/ 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Wokwi GDB", 6 | "type": "cppdbg", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/target/xtensa-esp32s3-none-elf/debug/esp32_conways_game_of_life_rs", 9 | "cwd": "${workspaceFolder}", 10 | "MIMode": "gdb", 11 | "miDebuggerPath": "${userHome}/.espressif/tools/xtensa-esp32s3-elf/esp-2021r2-patch3-8.4.0/xtensa-esp32s3-elf/bin/xtensa-esp32s3-elf-gdb", 12 | "miDebuggerServerAddress": "localhost:3333" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.checkOnSave.allTargets": false, 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build", 6 | "type": "shell", 7 | "command": "scripts/build.sh ${input:buildMode}", 8 | "options": { 9 | "cwd": "${workspaceFolder}" 10 | }, 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | } 15 | }, 16 | { 17 | "label": "Build & Flash", 18 | "type": "shell", 19 | "command": "scripts/flash.sh ${input:buildMode}", 20 | "options": { 21 | "cwd": "${workspaceFolder}" 22 | }, 23 | "group": { 24 | "kind": "test", 25 | "isDefault": true 26 | } 27 | }, 28 | ], 29 | "inputs": [ 30 | { 31 | "type": "pickString", 32 | "id": "buildMode", 33 | "description": "Select the build mode:", 34 | "options": [ 35 | "release", 36 | "debug" 37 | ], 38 | "default": "release" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /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. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright [year] [fullname] 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. 26 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # ESP32 Conway's Game of Life in Rust 2 | 3 | Implementation of Conway's Game of Life Rust Bare Metal. 4 | 5 | [![Wokwi](https://img.shields.io/endpoint?url=https%3A%2F%2Fwokwi.com%2Fbadge%2Fclick-to-simulate.json)](https://wokwi.com/projects/380370193649185793) 6 | 7 | ![ESP32 Conways Game of Life in Rust](esp32-conways-game-of-life-rs.png) 8 | 9 | ## Supported boards 10 | 11 | ### ESP32-S3-BOX-3 Minimal Implementation 12 | 13 | - https://github.com/espressif/esp-box 14 | 15 | The implementation is based on Rust no\_std, using mipidsi crate. 16 | 17 | ``` 18 | cd esp32-s3-box-3-minimal 19 | cargo run --release 20 | ``` 21 | 22 | ### Waveshare ESP32-C6-LCD 1.47 23 | 24 | [Rust Bare Metal no_std](https://developer.espressif.com/blog/2025/02/rust-esp-hal-beta/) with [Bevy ECS no_std](https://github.com/bevyengine/bevy/issues/15460) on 1.47 inch [ESP32-C6 LCD Waheshare](https://www.waveshare.com/esp32-c6-lcd-1.47.htm) with DMA and framebuffer - [Conway's Game of Life](https://github.com/georgik/esp32-conways-game-of-life-rs/tree/main/esp32-c6-waveshare-1_47): 25 | 26 | 29 | 30 | The implementation is based on Rust no\_std and Bevy 0.16 no\_std, plus mipidsi crate. 31 | 32 | ``` 33 | cd waveshare-esp32-c6-lcd-1_28 34 | cargo run --release 35 | ``` 36 | 37 | ### Waveshare ESP32-S3-Touch-LCD 1.28 38 | 39 | ![ESP32 Conways Game of Life in Rust - Waveshare ESP32-S3 Touch LCD with Bevy ECS](waveshare-esp32-s3-touch-lcd-1_28.jpg) 40 | 41 | [Rust Bare Metal no_std](https://developer.espressif.com/blog/2025/02/rust-esp-hal-beta/) with [Bevy ECS no_std](https://github.com/bevyengine/bevy/issues/15460) on [Waheshare ESP32-S3 LCD Touch 1.28 inch](https://www.waveshare.com/esp32-c6-lcd-1.47.htm) with DMA and framebuffer: 42 | 43 | The implementation is based on Rust no\_std and Bevy 0.16 no\_std, plus mipidsi crate. 44 | 45 | ``` 46 | cd waveshare-esp32-s3-touch-lcd-1_28 47 | cargo run --release 48 | ``` 49 | 50 | ### M5Stack Atom-S3 51 | 52 | ![ESP32 Conways Game of Life in Rust - M5Stack Atom-S3 with Bevy ECS](m5stack-atom-s3.jpg) 53 | 54 | - https://docs.m5stack.com/en/core/AtomS3 55 | 56 | Controls: Press button under display to reset the game state (GPIO 41). 57 | 58 | The implementation is based on Rust no\_std, using mipidsi crate and Bevy ECS. 59 | It requires es-rs toolchain for ESP32-S3 version at [least 1.85](https://github.com/esp-rs/rust-build/releases/tag/v1.85.0.0), because of edition 2024. 60 | 61 | Installation of the toolchain: 62 | 63 | ``` 64 | cargo install espup 65 | espup install --toolchain-version 1.85.0.0 66 | source ~/export-esp.sh 67 | ``` 68 | 69 | Build: 70 | 71 | ``` 72 | cd m5stack-atom-s3 73 | cargo run --release 74 | ``` 75 | 76 | ### M5Stack CoreS3 77 | 78 | ![ESP32 Conways Game of Life in Rust - M5Stack CoreS3 with Bevy ECS](m5stack-cores3-conway.jpg) 79 | 80 | - https://shop.m5stack.com/products/m5stack-cores3-esp32s3-lotdevelopment-kit 81 | 82 | Controls: Press the button under display to reset the game state. 83 | 84 | Note: Press Boot button and reset to enter download mode. 85 | 86 | The implementation is based on Rust no\_std, using mipidsi crate and Bevy ECS. 87 | 88 | Installation of the toolchain: 89 | 90 | ``` 91 | espup install --toolchain-version 1.85.0.0 92 | source ~/export-esp.sh 93 | ``` 94 | 95 | Build: 96 | 97 | ``` 98 | cd m5stack-cores3 99 | cargo run --release 100 | ``` 101 | 102 | ### ESP32-S3-BOX-3 103 | 104 | ![ESP32 Conways Game of Life in Rust - ESP32-S3-BOX-3 with Bevy ECS](esp32-s3-box-3-conway.jpg) 105 | 106 | - https://github.com/espressif/esp-box 107 | 108 | The implementation is based on Rust no\_std, using mipidsi crate and Bevy ECS. 109 | It requires es-rs toolchain for ESP32-S3 version at [least 1.85](https://github.com/esp-rs/rust-build/releases/tag/v1.85.0.0), because of edition 2024. 110 | 111 | Installation of the toolchain: 112 | 113 | ``` 114 | cargo install espup 115 | espup install --toolchain-version 1.85.0.0 116 | source ~/export-esp.sh 117 | ``` 118 | 119 | Build: 120 | 121 | ``` 122 | cd esp32-s3-box-3 123 | cargo run --release 124 | ``` 125 | 126 | ### ESP32-S3-LCD-Ev-Board 127 | 128 | ![ESP32 Conways Game of Life in Rust - ESP32-S3-LCD-Ev-Board with Bevy ECS](esp32-s3-lcd-ev-board-conway.jpg) 129 | 130 | [ESP32-S3-LCD-Ev-Board](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s3/esp32-s3-lcd-ev-board/index.html) 131 | is more complex when it comes to the display. Initialization sequence for the display is: 132 | - initialize I2C 133 | - tunnel SPI commands via I2C bus 134 | - configure 16 GPIOs to transfer data 135 | - all data must be transferred in one transaction (requires PSRAM) 136 | 137 | The timing of the display must be precise, otherwise incorrect data will be displayed. 138 | 139 | Working configuration of timing: 140 | 141 | ```rust 142 | // Configure the RGB display 143 | let config = Config::default() 144 | .with_clock_mode(ClockMode { 145 | polarity: Polarity::IdleLow, 146 | phase: Phase::ShiftLow, 147 | }) 148 | .with_frequency(Rate::from_mhz(10)) 149 | .with_format(Format { 150 | enable_2byte_mode: true, 151 | ..Default::default() 152 | }) 153 | .with_timing(FrameTiming { 154 | // active region 155 | horizontal_active_width: 480, 156 | vertical_active_height: 480, 157 | // extend total timings for larger porch intervals 158 | horizontal_total_width: 600, // allow long back/front porch 159 | horizontal_blank_front_porch: 80, 160 | vertical_total_height: 600, // allow longer vertical blank 161 | vertical_blank_front_porch: 80, 162 | // maintain sync widths 163 | hsync_width: 10, 164 | vsync_width: 4, 165 | // place HSYNC pulse well before active data 166 | hsync_position: 10, 167 | }) 168 | .with_vsync_idle_level(Level::High) 169 | .with_hsync_idle_level(Level::High) 170 | .with_de_idle_level(Level::Low) 171 | .with_disable_black_region(false); 172 | ``` 173 | 174 | This is only bare metal implementation, does not contain Bevy ECS in this version. 175 | 176 | ``` 177 | cargo install espup 178 | espup install --toolchain-version 1.85.0.0 179 | source ~/export-esp.sh 180 | ``` 181 | 182 | Build: 183 | 184 | ``` 185 | cd esp32-s3-lcd-ev-board 186 | cargo run --release 187 | ``` 188 | 189 | 190 | ### ESP32-C3-LCDKit 191 | 192 | ![ESP32 Conways Game of Life in Rust - ESP32-C3-LCDkit with Bevy ECS](esp32-c3-lcdkit-conway.jpg) 193 | 194 | Controls: Press button rotary button to reset the game state (GPIO 9). 195 | 196 | ``` 197 | cd esp32-c3-lcdkit 198 | cargo run --release 199 | ``` 200 | 201 | ### ESoPe SLD\_C\_W\_S3 202 | 203 | ![ESP32 Conways Game of Life in Rust - ESoPe SLD_C_W_S3](esope-sld-c-w-s3.jpg) 204 | 205 | Board: [SDL\_C\_W\_S3](https://esope.de/en/products) 206 | Display: RGB [Schukat Smartwin display-concept](https://shop.schukat.com/de/de/EUR/c/ESOP) 207 | 208 | The implementation is based on Embassy Async Rust no\_std with RGB interface. 209 | Both cores of ESP32-S3 are used. One core is handling DMA transfers to the display, 210 | while the other core is running the game logic. 211 | 212 | RGB displays are very time-sensitive, so the timing of the display must be precise, that's also why 213 | one core is dedicated to the display. 214 | 215 | The display configuration is stored in EEPROM for this specific display type. 216 | 217 | Run: 218 | ``` 219 | esope-sld-c-w-s3 220 | cargo r -r 221 | ``` 222 | 223 | The board requires connection using ESP-Prog. You need to switch the board into boot mode. 224 | Press and hold the BOOT button, then press the RESET button, then release the BOOT button. 225 | Press the RESET button again to start the program. 226 | 227 | ### WASM 228 | 229 | This is experimental implementation for WASM. 230 | 231 | ``` 232 | cd wasm 233 | wasm-pack build --target web 234 | wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/conways_wasm.wasm 235 | python3 -m http.server 236 | ``` 237 | 238 | ### ESP32-WROVER-KIT 239 | 240 | This board is no longer in production, yet it's still used by many developers. 241 | 242 | ![ESP32 Conways Game of Life in Rust - ESP-WROVER-KIT with Bevy ECS](esp32-wrover-kit.jpg) 243 | 244 | The implementation is based on Rust no\_std, using mipidsi crate and Bevy ECS. 245 | It requires es-rs toolchain for ESP32-S3 version at [least 1.85](https://github.com/esp-rs/rust-build/releases/tag/v1.85.0.0), because of edition 2024. 246 | 247 | Installation of the toolchain: 248 | 249 | ``` 250 | cargo install espup 251 | espup install --toolchain-version 1.85.0.0 252 | source ~/export-esp.sh 253 | ``` 254 | 255 | Build: 256 | 257 | ``` 258 | cd esp32-wrover-kit 259 | cargo run --release 260 | ``` 261 | 262 | 263 | -------------------------------------------------------------------------------- /docs/esope-sld-c-w-s3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgik/esp32-conways-game-of-life-rs/ef5ba9a6fdd6319c85df30b5fdd5f75ffb24b557/docs/esope-sld-c-w-s3.jpg -------------------------------------------------------------------------------- /docs/esp32-c3-lcdkit-conway.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgik/esp32-conways-game-of-life-rs/ef5ba9a6fdd6319c85df30b5fdd5f75ffb24b557/docs/esp32-c3-lcdkit-conway.jpg -------------------------------------------------------------------------------- /docs/esp32-conways-game-of-life-rs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgik/esp32-conways-game-of-life-rs/ef5ba9a6fdd6319c85df30b5fdd5f75ffb24b557/docs/esp32-conways-game-of-life-rs.png -------------------------------------------------------------------------------- /docs/esp32-s3-box-3-conway.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgik/esp32-conways-game-of-life-rs/ef5ba9a6fdd6319c85df30b5fdd5f75ffb24b557/docs/esp32-s3-box-3-conway.jpg -------------------------------------------------------------------------------- /docs/esp32-s3-lcd-ev-board-conway.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgik/esp32-conways-game-of-life-rs/ef5ba9a6fdd6319c85df30b5fdd5f75ffb24b557/docs/esp32-s3-lcd-ev-board-conway.jpg -------------------------------------------------------------------------------- /docs/esp32-wrover-kit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgik/esp32-conways-game-of-life-rs/ef5ba9a6fdd6319c85df30b5fdd5f75ffb24b557/docs/esp32-wrover-kit.jpg -------------------------------------------------------------------------------- /docs/m5stack-atom-s3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgik/esp32-conways-game-of-life-rs/ef5ba9a6fdd6319c85df30b5fdd5f75ffb24b557/docs/m5stack-atom-s3.jpg -------------------------------------------------------------------------------- /docs/m5stack-cores3-conway.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgik/esp32-conways-game-of-life-rs/ef5ba9a6fdd6319c85df30b5fdd5f75ffb24b557/docs/m5stack-cores3-conway.jpg -------------------------------------------------------------------------------- /docs/waveshare-esp32-s3-touch-lcd-1_28.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgik/esp32-conways-game-of-life-rs/ef5ba9a6fdd6319c85df30b5fdd5f75ffb24b557/docs/waveshare-esp32-s3-touch-lcd-1_28.jpg -------------------------------------------------------------------------------- /esope-sld-c-w-s3/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.xtensa-esp32s3-none-elf] 2 | runner = "espflash flash --monitor --chip esp32s3 --before default-reset --after hard-reset" 3 | 4 | [env] 5 | ESP_LOG="INFO" 6 | ESP_WIFI_CONFIG_PHY_ENABLE_USB="true" 7 | ESP_HAL_CONFIG_PSRAM_MODE = "octal" 8 | 9 | [build] 10 | rustflags = [ 11 | "-C", "link-arg=-nostartfiles", 12 | ] 13 | 14 | target = "xtensa-esp32s3-none-elf" 15 | 16 | [unstable] 17 | build-std = ["alloc", "core"] 18 | -------------------------------------------------------------------------------- /esope-sld-c-w-s3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "esp32-conways-game-of-life-rs" 4 | version = "0.7.0" 5 | 6 | [[bin]] 7 | name = "esp32-conways-game-of-life-rs" 8 | path = "./src/bin/main.rs" 9 | 10 | [dependencies] 11 | esp-alloc = "0.8.0" 12 | esp-hal = { version = "1.0.0-beta.1", features = ["esp32s3", "unstable", "psram"] } 13 | esp-println = { version = "0.14.0", features = ["esp32s3", "log-04"] } 14 | log = "0.4.27" 15 | 16 | embedded-graphics = "0.8.1" 17 | embedded-graphics-framebuf = "0.5.0" 18 | eeprom24x = "0.7.2" 19 | embassy-executor = { version = "0.7.0", features = ["task-arena-size-20480"] } 20 | embassy-sync = "0.7.0" 21 | embassy-time = "0.4.0" 22 | esp-hal-embassy = { version = "0.8.1", features = ["esp32s3"] } 23 | static_cell = { version = "2.1.0", features = ["nightly"] } 24 | heapless = "0.8.0" 25 | 26 | [profile.dev] 27 | # Rust debug is too slow. 28 | # For debug builds always builds with some optimization 29 | opt-level = "s" 30 | 31 | [profile.release] 32 | codegen-units = 1 # LLVM can perform better optimizations using a single thread 33 | debug = 2 34 | debug-assertions = false 35 | incremental = false 36 | lto = 'fat' 37 | opt-level = 's' 38 | overflow-checks = false 39 | -------------------------------------------------------------------------------- /esope-sld-c-w-s3/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | linker_be_nice(); 3 | // make sure linkall.x is the last linker script (otherwise might cause problems with flip-link) 4 | println!("cargo:rustc-link-arg=-Tlinkall.x"); 5 | } 6 | 7 | fn linker_be_nice() { 8 | let args: Vec = std::env::args().collect(); 9 | if args.len() > 1 { 10 | let kind = &args[1]; 11 | let what = &args[2]; 12 | 13 | match kind.as_str() { 14 | "undefined-symbol" => match what.as_str() { 15 | "_defmt_timestamp" => { 16 | eprintln!(); 17 | eprintln!("💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`"); 18 | eprintln!(); 19 | } 20 | "_stack_start" => { 21 | eprintln!(); 22 | eprintln!("💡 Is the linker script `linkall.x` missing?"); 23 | eprintln!(); 24 | } 25 | _ => (), 26 | }, 27 | // we don't have anything helpful for "missing-lib" yet 28 | _ => { 29 | std::process::exit(1); 30 | } 31 | } 32 | 33 | std::process::exit(0); 34 | } 35 | 36 | println!( 37 | "cargo:rustc-link-arg=-Wl,--error-handling-script={}", 38 | std::env::current_exe().unwrap().display() 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /esope-sld-c-w-s3/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "esp" 3 | -------------------------------------------------------------------------------- /esope-sld-c-w-s3/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | -------------------------------------------------------------------------------- /esp32-c3-lcdkit/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.xtensa-esp32s3-none-elf] 2 | runner = "espflash flash --monitor" 3 | 4 | [target.riscv32imc-unknown-none-elf] 5 | runner = "espflash flash --monitor --chip esp32c3" 6 | 7 | [target.riscv32imac-unknown-none-elf] 8 | runner = "espflash flash --monitor --chip esp32c6" 9 | 10 | [env] 11 | ESP_LOG="INFO" 12 | 13 | [build] 14 | rustflags = [ 15 | "-C", "force-frame-pointers", 16 | ] 17 | 18 | target = "riscv32imc-unknown-none-elf" 19 | 20 | [unstable] 21 | build-std = ["alloc", "core"] 22 | -------------------------------------------------------------------------------- /esp32-c3-lcdkit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esp32-conways-game-of-life-rs" 3 | version = "0.6.0" 4 | authors = ["Juraj Michálek "] 5 | edition = "2024" 6 | license = "MIT OR Apache-2.0" 7 | 8 | 9 | [dependencies] 10 | esp-hal = { version = "1.0.0-beta.0", features = ["esp32c3", "unstable"] } 11 | esp-backtrace = { version = "0.15.1", features = [ 12 | "panic-handler", 13 | "println" 14 | ] } 15 | esp-println = { version = "0.13", features = [ "log" ] } 16 | log = { version = "0.4.26" } 17 | 18 | esp-alloc = "0.7.0" 19 | embedded-graphics = "0.8.1" 20 | embedded-hal = "1.0.0" 21 | mipidsi = "0.9.0" 22 | embedded-graphics-framebuf = "0.5.0" 23 | heapless = "0.8.0" 24 | embedded-hal-bus = "0.3.0" 25 | bevy_ecs = { git = "https://github.com/bevyengine/bevy.git", rev = "adbb53b8", default-features = false } 26 | #bevy_ecs = { git = "https://github.com/bevyengine/bevy.git", default-features = false } 27 | 28 | 29 | [features] 30 | default = [ "esp-hal/esp32c3", "esp-backtrace/esp32c3", "esp-println/esp32c3" ] 31 | 32 | 33 | [profile.dev] 34 | # Rust debug is too slow. 35 | # For debug builds always builds with some optimization 36 | opt-level = "s" 37 | 38 | [profile.release] 39 | codegen-units = 1 # LLVM can perform better optimizations using a single thread 40 | debug = 2 41 | debug-assertions = false 42 | incremental = false 43 | lto = 'fat' 44 | opt-level = 's' 45 | overflow-checks = false 46 | 47 | -------------------------------------------------------------------------------- /esp32-c3-lcdkit/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg-bins=-Tlinkall.x"); 3 | } 4 | -------------------------------------------------------------------------------- /esp32-c3-lcdkit/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["rust-src"] 4 | targets = ["riscv32imc-unknown-none-elf"] 5 | -------------------------------------------------------------------------------- /esp32-c3-lcdkit/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | extern crate alloc; 5 | use alloc::boxed::Box; 6 | 7 | use bevy_ecs::prelude::*; 8 | use core::fmt::Write; 9 | use embedded_graphics::{ 10 | Drawable, 11 | mono_font::{MonoTextStyle, ascii::FONT_8X13}, 12 | pixelcolor::Rgb565, 13 | prelude::*, 14 | primitives::{PrimitiveStyle, Rectangle}, 15 | text::Text, 16 | }; 17 | use embedded_graphics_framebuf::FrameBuf; 18 | use embedded_graphics_framebuf::backends::FrameBufferBackend; 19 | use embedded_hal::delay::DelayNs; 20 | use embedded_hal_bus::spi::ExclusiveDevice; 21 | use esp_hal::delay::Delay; 22 | use esp_hal::dma::{DmaRxBuf, DmaTxBuf}; 23 | use esp_hal::dma_buffers; 24 | use esp_hal::{ 25 | Blocking, 26 | gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull}, 27 | main, 28 | rng::Rng, 29 | spi::master::{Spi, SpiDmaBus}, 30 | time::Rate, 31 | }; 32 | use esp_println::{logger::init_logger_from_env, println}; 33 | use log::info; 34 | use mipidsi::options::ColorOrder; 35 | use mipidsi::{Builder, models::GC9A01}; 36 | use mipidsi::{interface::SpiInterface, options::ColorInversion}; 37 | 38 | #[panic_handler] 39 | fn panic(_info: &core::panic::PanicInfo) -> ! { 40 | println!("Panic: {}", _info); 41 | loop {} 42 | } 43 | 44 | /// A wrapper around a boxed array that implements FrameBufferBackend. 45 | /// This allows the framebuffer to be allocated on the heap. 46 | pub struct HeapBuffer(Box<[C; N]>); 47 | 48 | impl HeapBuffer { 49 | pub fn new(data: Box<[C; N]>) -> Self { 50 | Self(data) 51 | } 52 | } 53 | 54 | impl core::ops::Deref for HeapBuffer { 55 | type Target = [C; N]; 56 | fn deref(&self) -> &Self::Target { 57 | &*self.0 58 | } 59 | } 60 | 61 | impl core::ops::DerefMut for HeapBuffer { 62 | fn deref_mut(&mut self) -> &mut Self::Target { 63 | &mut *self.0 64 | } 65 | } 66 | 67 | impl FrameBufferBackend for HeapBuffer { 68 | type Color = C; 69 | fn set(&mut self, index: usize, color: Self::Color) { 70 | self.0[index] = color; 71 | } 72 | fn get(&self, index: usize) -> Self::Color { 73 | self.0[index] 74 | } 75 | fn nr_elements(&self) -> usize { 76 | N 77 | } 78 | } 79 | 80 | // --- Type Alias for the Concrete Display --- 81 | // Use the DMA-enabled SPI bus type. 82 | type MyDisplay = mipidsi::Display< 83 | SpiInterface< 84 | 'static, 85 | ExclusiveDevice, Output<'static>, Delay>, 86 | Output<'static>, 87 | >, 88 | GC9A01, 89 | Output<'static>, 90 | >; 91 | 92 | // --- LCD Resolution and FrameBuffer Type Aliases --- 93 | const LCD_H_RES: usize = 240; 94 | const LCD_V_RES: usize = 240; 95 | const LCD_BUFFER_SIZE: usize = LCD_H_RES * LCD_V_RES; 96 | 97 | // We want our pixels stored as Rgb565. 98 | type FbBuffer = HeapBuffer; 99 | // Define a type alias for the complete FrameBuf. 100 | type MyFrameBuf = FrameBuf; 101 | 102 | #[derive(Resource)] 103 | struct FrameBufferResource { 104 | frame_buf: MyFrameBuf, 105 | } 106 | 107 | impl FrameBufferResource { 108 | fn new() -> Self { 109 | // Allocate the framebuffer data on the heap. 110 | let fb_data: Box<[Rgb565; LCD_BUFFER_SIZE]> = Box::new([Rgb565::BLACK; LCD_BUFFER_SIZE]); 111 | let heap_buffer = HeapBuffer::new(fb_data); 112 | let frame_buf = MyFrameBuf::new(heap_buffer, LCD_H_RES, LCD_V_RES); 113 | Self { frame_buf } 114 | } 115 | } 116 | 117 | // --- Game of Life Definitions --- 118 | // Now each cell is a u8 (0 means dead; >0 indicates age) 119 | const GRID_WIDTH: usize = 35; 120 | const GRID_HEIGHT: usize = 35; 121 | const RESET_AFTER_GENERATIONS: usize = 500; 122 | 123 | fn randomize_grid(rng: &mut Rng, grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 124 | for row in grid.iter_mut() { 125 | for cell in row.iter_mut() { 126 | let mut buf = [0u8; 1]; 127 | rng.read(&mut buf); 128 | // Randomly set cell to 1 (alive) or 0 (dead) 129 | *cell = if buf[0] & 1 != 0 { 1 } else { 0 }; 130 | } 131 | } 132 | } 133 | 134 | fn update_game_of_life(grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 135 | let mut new_grid = [[0u8; GRID_WIDTH]; GRID_HEIGHT]; 136 | for y in 0..GRID_HEIGHT { 137 | for x in 0..GRID_WIDTH { 138 | // Count neighbors: consider a cell alive if its age is >0. 139 | let mut alive_neighbors = 0; 140 | for i in 0..3 { 141 | for j in 0..3 { 142 | if i == 1 && j == 1 { 143 | continue; 144 | } 145 | let nx = (x + i + GRID_WIDTH - 1) % GRID_WIDTH; 146 | let ny = (y + j + GRID_HEIGHT - 1) % GRID_HEIGHT; 147 | if grid[ny][nx] > 0 { 148 | alive_neighbors += 1; 149 | } 150 | } 151 | } 152 | if grid[y][x] > 0 { 153 | // Live cell survives if 2 or 3 neighbors; increment age. 154 | if alive_neighbors == 2 || alive_neighbors == 3 { 155 | new_grid[y][x] = grid[y][x].saturating_add(1); 156 | } else { 157 | new_grid[y][x] = 0; 158 | } 159 | } else { 160 | // Dead cell becomes alive if exactly 3 neighbors. 161 | if alive_neighbors == 3 { 162 | new_grid[y][x] = 1; 163 | } else { 164 | new_grid[y][x] = 0; 165 | } 166 | } 167 | } 168 | } 169 | *grid = new_grid; 170 | } 171 | 172 | /// Maps cell age (1..=max_age) to a color. Newborn cells are dark blue and older cells become brighter (toward white). 173 | fn age_to_color(age: u8) -> Rgb565 { 174 | if age == 0 { 175 | Rgb565::BLACK 176 | } else { 177 | let max_age = 10; 178 | let a = age.min(max_age) as u32; // clamp age and use u32 for intermediate math 179 | let r = ((31 * a) + 5) / max_age as u32; 180 | let g = ((63 * a) + 5) / max_age as u32; 181 | let b = 31; // Keep blue channel constant 182 | // Convert back to u8 and return the color. 183 | Rgb565::new(r as u8, g as u8, b) 184 | } 185 | } 186 | 187 | /// Draws the game grid using the cell age for color. 188 | fn draw_grid>( 189 | display: &mut D, 190 | grid: &[[u8; GRID_WIDTH]; GRID_HEIGHT], 191 | ) -> Result<(), D::Error> { 192 | let border_color = Rgb565::new(230, 230, 230); 193 | for (y, row) in grid.iter().enumerate() { 194 | for (x, &age) in row.iter().enumerate() { 195 | let point = Point::new(x as i32 * 7, y as i32 * 7); 196 | if age > 0 { 197 | // Draw a border then fill with color based on age. 198 | Rectangle::new(point, Size::new(7, 7)) 199 | .into_styled(PrimitiveStyle::with_fill(border_color)) 200 | .draw(display)?; 201 | // Draw an inner cell with color according to age. 202 | Rectangle::new(point + Point::new(1, 1), Size::new(5, 5)) 203 | .into_styled(PrimitiveStyle::with_fill(age_to_color(age))) 204 | .draw(display)?; 205 | } else { 206 | // Draw a dead cell as black. 207 | Rectangle::new(point, Size::new(7, 7)) 208 | .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) 209 | .draw(display)?; 210 | } 211 | } 212 | } 213 | Ok(()) 214 | } 215 | 216 | fn write_generation>( 217 | display: &mut D, 218 | generation: usize, 219 | ) -> Result<(), D::Error> { 220 | let x = 70; 221 | let y = 140; 222 | 223 | let mut num_str = heapless::String::<20>::new(); 224 | write!(num_str, "Generation: {}", generation).unwrap(); 225 | Text::new( 226 | num_str.as_str(), 227 | Point::new(x, y), 228 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 229 | ) 230 | .draw(display)?; 231 | Ok(()) 232 | } 233 | 234 | // --- ECS Resources and Systems --- 235 | 236 | #[derive(Resource)] 237 | struct GameOfLifeResource { 238 | grid: [[u8; GRID_WIDTH]; GRID_HEIGHT], 239 | generation: usize, 240 | } 241 | 242 | impl Default for GameOfLifeResource { 243 | fn default() -> Self { 244 | Self { 245 | grid: [[0; GRID_WIDTH]; GRID_HEIGHT], 246 | generation: 0, 247 | } 248 | } 249 | } 250 | 251 | #[derive(Resource)] 252 | struct RngResource(Rng); 253 | 254 | // Because our display type contains DMA descriptors and raw pointers, it isn’t Sync. 255 | // We wrap it as a NonSend resource so that Bevy doesn’t require Sync. 256 | struct DisplayResource { 257 | display: MyDisplay, 258 | } 259 | 260 | struct ButtonResource { 261 | button: Input<'static>, 262 | } 263 | 264 | /// Resource to track the previous state of the button (for edge detection). 265 | #[derive(Default, Resource)] 266 | struct ButtonState { 267 | was_pressed: bool, 268 | } 269 | 270 | fn update_game_of_life_system( 271 | mut game: ResMut, 272 | mut rng_res: ResMut, 273 | ) { 274 | update_game_of_life(&mut game.grid); 275 | game.generation += 1; 276 | if game.generation >= RESET_AFTER_GENERATIONS { 277 | randomize_grid(&mut rng_res.0, &mut game.grid); 278 | game.generation = 0; 279 | } 280 | } 281 | 282 | /// System to check the button and reset the simulation when pressed. 283 | fn button_reset_system( 284 | mut game: ResMut, 285 | mut rng_res: ResMut, 286 | mut btn_state: ResMut, 287 | button_res: NonSend, 288 | ) { 289 | // Check if the button is pressed (active low) 290 | if button_res.button.is_low() { 291 | if !btn_state.was_pressed { 292 | // Button press detected: reset simulation. 293 | randomize_grid(&mut rng_res.0, &mut game.grid); 294 | game.generation = 0; 295 | btn_state.was_pressed = true; 296 | } 297 | } else { 298 | btn_state.was_pressed = false; 299 | } 300 | } 301 | 302 | /// Render the game state by drawing into the offscreen framebuffer and then flushing 303 | /// it to the display via DMA. After drawing the game grid and generation number, 304 | /// we overlay centered text. 305 | fn render_system( 306 | mut display_res: NonSendMut, 307 | game: Res, 308 | mut fb_res: ResMut, 309 | ) { 310 | // Clear the framebuffer. 311 | fb_res.frame_buf.clear(Rgb565::BLACK).unwrap(); 312 | // Draw the game grid (using the age-based color) and generation number. 313 | draw_grid(&mut fb_res.frame_buf, &game.grid).unwrap(); 314 | write_generation(&mut fb_res.frame_buf, game.generation).unwrap(); 315 | 316 | // --- Overlay centered text --- 317 | let line1 = "Rust no_std ESP32-C3"; 318 | let line2 = "Bevy ECS 0.16 no_std"; 319 | // Estimate text width: assume ~8 pixels per character. 320 | let line1_width = line1.len() as i32 * 8; 321 | let line2_width = line2.len() as i32 * 8; 322 | let x1 = (LCD_H_RES as i32 - line1_width) / 2 + 14; 323 | let x2 = (LCD_H_RES as i32 - line2_width) / 2 + 14; 324 | // For vertical centering, assume 26 pixels total text height. 325 | let y = (LCD_V_RES as i32 - 26) / 2; 326 | Text::new( 327 | line1, 328 | Point::new(x1, y), 329 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 330 | ) 331 | .draw(&mut fb_res.frame_buf) 332 | .unwrap(); 333 | Text::new( 334 | line2, 335 | Point::new(x2, y + 14), 336 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 337 | ) 338 | .draw(&mut fb_res.frame_buf) 339 | .unwrap(); 340 | 341 | // Define the area covering the entire framebuffer. 342 | let area = Rectangle::new(Point::zero(), fb_res.frame_buf.size()); 343 | // Flush the framebuffer to the physical display. 344 | display_res 345 | .display 346 | .fill_contiguous(&area, fb_res.frame_buf.data.iter().copied()) 347 | .unwrap(); 348 | } 349 | 350 | #[main] 351 | fn main() -> ! { 352 | let peripherals = esp_hal::init(esp_hal::Config::default()); 353 | // Increase heap size as needed. 354 | esp_alloc::heap_allocator!(size: 150000); 355 | init_logger_from_env(); 356 | 357 | // --- DMA Buffers for SPI --- 358 | let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(1024); 359 | let dma_rx_buf = DmaRxBuf::new(rx_descriptors, rx_buffer).unwrap(); 360 | let dma_tx_buf = DmaTxBuf::new(tx_descriptors, tx_buffer).unwrap(); 361 | 362 | // --- Display Setup using BSP values --- 363 | let spi = Spi::::new( 364 | peripherals.SPI2, 365 | esp_hal::spi::master::Config::default() 366 | .with_frequency(Rate::from_mhz(40)) 367 | .with_mode(esp_hal::spi::Mode::_0), 368 | ) 369 | .unwrap() 370 | .with_sck(peripherals.GPIO1) 371 | .with_mosi(peripherals.GPIO0) 372 | .with_dma(peripherals.DMA_CH0) 373 | .with_buffers(dma_rx_buf, dma_tx_buf); 374 | let cs_output = Output::new(peripherals.GPIO7, Level::High, OutputConfig::default()); 375 | let spi_delay = Delay::new(); 376 | let spi_device = ExclusiveDevice::new(spi, cs_output, spi_delay).unwrap(); 377 | 378 | // LCD interface 379 | let lcd_dc = Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default()); 380 | // Leak a Box to obtain a 'static mutable buffer. 381 | let buffer: &'static mut [u8; 512] = Box::leak(Box::new([0_u8; 512])); 382 | let di = SpiInterface::new(spi_device, lcd_dc, buffer); 383 | 384 | let mut display_delay = Delay::new(); 385 | display_delay.delay_ns(500_000u32); 386 | 387 | // Reset pin 388 | let reset = Output::new(peripherals.GPIO10, Level::Low, OutputConfig::default()); 389 | // Initialize the display using mipidsi's builder. 390 | let mut display: MyDisplay = Builder::new(GC9A01, di) 391 | .reset_pin(reset) 392 | .display_size(240, 240) 393 | // .orientation(Orientation::new().flip_horizontal()) 394 | .color_order(ColorOrder::Bgr) 395 | .invert_colors(ColorInversion::Inverted) 396 | .init(&mut display_delay) 397 | .unwrap(); 398 | 399 | display.clear(Rgb565::BLACK).unwrap(); 400 | 401 | // Backlight 402 | let mut backlight = Output::new(peripherals.GPIO5, Level::Low, OutputConfig::default()); 403 | backlight.set_high(); 404 | 405 | info!("Display initialized"); 406 | 407 | // --- Initialize Game Resources --- 408 | let mut game = GameOfLifeResource::default(); 409 | let mut rng_instance = Rng::new(peripherals.RNG); 410 | randomize_grid(&mut rng_instance, &mut game.grid); 411 | let glider = [(1, 0), (2, 1), (0, 2), (1, 2), (2, 2)]; 412 | for (x, y) in glider.iter() { 413 | game.grid[*y][*x] = 1; // alive with age 1 414 | } 415 | 416 | // Create the framebuffer resource. 417 | let fb_res = FrameBufferResource::new(); 418 | 419 | let mut world = World::default(); 420 | world.insert_resource(game); 421 | world.insert_resource(RngResource(rng_instance)); 422 | // Insert the display as a non-send resource because its DMA pointers are not Sync. 423 | world.insert_non_send_resource(DisplayResource { display }); 424 | // Insert the framebuffer resource as a normal resource. 425 | world.insert_resource(fb_res); 426 | 427 | // --- Initialize Button Resource --- 428 | // Configure the button as an input with an internal pull-up (active low). 429 | let button = Input::new( 430 | peripherals.GPIO9, 431 | InputConfig::default().with_pull(Pull::Up), 432 | ); 433 | world.insert_non_send_resource(ButtonResource { button }); 434 | // Insert a resource to track button state for debouncing. 435 | world.insert_resource(ButtonState::default()); 436 | 437 | let mut schedule = Schedule::default(); 438 | schedule.add_systems(button_reset_system); 439 | schedule.add_systems(update_game_of_life_system); 440 | schedule.add_systems(render_system); 441 | 442 | let mut loop_delay = Delay::new(); 443 | 444 | loop { 445 | schedule.run(&mut world); 446 | loop_delay.delay_ms(50u32); 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /esp32-s3-box-3-minimal/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.xtensa-esp32s3-none-elf] 2 | runner = "espflash flash --monitor" 3 | 4 | [env] 5 | ESP_LOG="INFO" 6 | ESP_HAL_CONFIG_PSRAM_MODE = "octal" 7 | 8 | [build] 9 | rustflags = [ 10 | "-C", "link-arg=-nostartfiles", 11 | ] 12 | 13 | target = "xtensa-esp32s3-none-elf" 14 | 15 | [unstable] 16 | build-std = ["alloc", "core"] 17 | -------------------------------------------------------------------------------- /esp32-s3-box-3-minimal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esp32-conways-game-of-life-rs" 3 | version = "0.6.0" 4 | authors = ["Juraj Michálek "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | 8 | 9 | [dependencies] 10 | esp-hal = { version = "1.0.0-beta.1", features = ["esp32s3", "unstable"] } 11 | esp-backtrace = { version = "0.16.0", features = [ 12 | "panic-handler", 13 | "println" 14 | ] } 15 | esp-println = { version = "0.14.0", features = [ "log-04" ] } 16 | log = { version = "0.4.26" } 17 | 18 | embedded-graphics = "0.8.1" 19 | embedded-hal = "1.0.0" 20 | mipidsi = "0.9.0" 21 | #esp-display-interface-spi-dma = "0.3.0" 22 | # esp-display-interface-spi-dma = { path = "../esp-display-interface-spi-dma"} 23 | esp-bsp = "0.4.1" 24 | embedded-graphics-framebuf = { version = "0.3.0", git = "https://github.com/georgik/embedded-graphics-framebuf.git", branch = "feature/embedded-graphics-0.8" } 25 | heapless = "0.8.0" 26 | embedded-hal-bus = "0.3.0" 27 | 28 | 29 | [features] 30 | default = [ "esp-hal/esp32s3", "esp-backtrace/esp32s3", "esp-println/esp32s3", "esp32-s3-box-3" ] 31 | 32 | esp32-s3-box-3 = [ "esp-bsp/esp32-s3-box-3", "esp-hal/psram" ] -------------------------------------------------------------------------------- /esp32-s3-box-3-minimal/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg=-Tlinkall.x"); 3 | } 4 | -------------------------------------------------------------------------------- /esp32-s3-box-3-minimal/diagram.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "author": "Juraj Michálek ", 4 | "editor": "wokwi", 5 | "parts": [ 6 | { 7 | "type": "board-esp32-s3-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": {} 20 | } 21 | ], 22 | "connections": [ 23 | [ "esp:TX", "$serialMonitor:RX", "", [] ], 24 | [ "esp:RX", "$serialMonitor:TX", "", [] ], 25 | [ "esp:3V3", "lcd1:VCC", "green", [] ], 26 | [ "esp:GND.1", "lcd1:GND", "black", [] ], 27 | [ "esp:7", "lcd1:SCK", "blue", [] ], 28 | [ "esp:6", "lcd1:MOSI", "orange", [] ], 29 | [ "esp:GND.1", "lcd1:CS", "red", [] ], 30 | [ "esp:4", "lcd1:D/C", "magenta", [] ], 31 | [ "esp:48", "lcd1:RST", "yellow", [] ], 32 | [ "lcd1:LED", "esp:3V3", "white", [] ] 33 | ], 34 | "serialMonitor": { "display": "terminal" }, 35 | "dependencies": {} 36 | } -------------------------------------------------------------------------------- /esp32-s3-box-3-minimal/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "esp" 3 | -------------------------------------------------------------------------------- /esp32-s3-box-3-minimal/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | // use esp_bsp::prelude::*; 5 | // use esp_display_interface_spi_dma::display_interface_spi_dma; 6 | use embedded_graphics::{ 7 | mono_font::{ascii::FONT_8X13, MonoTextStyle}, 8 | pixelcolor::Rgb565, 9 | prelude::*, 10 | prelude::{DrawTarget, Point, RgbColor}, 11 | primitives::{PrimitiveStyle, Rectangle}, 12 | text::Text, 13 | Drawable, 14 | }; 15 | use embedded_hal_bus::spi::ExclusiveDevice; 16 | #[allow(unused_imports)] 17 | use esp_backtrace as _; 18 | // use esp_hal::gpio::OutputOpenDrain; 19 | use esp_hal::rng::Rng; 20 | use esp_hal::{ 21 | delay::Delay, 22 | gpio::{DriveMode, Level, Output, OutputConfig}, 23 | main, 24 | spi::master::Spi, 25 | time::Rate, 26 | }; 27 | use mipidsi::interface::SpiInterface; 28 | // use embedded_graphics_framebuf::FrameBuf; 29 | use embedded_hal::delay::DelayNs; 30 | use log::info; 31 | 32 | // Define grid size 33 | const WIDTH: usize = 64; 34 | const HEIGHT: usize = 48; 35 | 36 | const RESET_AFTER_GENERATIONS: usize = 500; 37 | 38 | use core::fmt::Write; 39 | // use esp_hal::dma::Owner::Dma; 40 | use esp_hal::spi::Mode; 41 | use heapless::String; 42 | use mipidsi::{models::ILI9486Rgb565, Builder}; 43 | 44 | fn write_generation>( 45 | display: &mut D, 46 | generation: usize, 47 | ) -> Result<(), D::Error> { 48 | // Create a String with a fixed capacity of 20 bytes 49 | let mut num_str = String::<20>::new(); 50 | // Write the generation number into the string 51 | // unwrap is safe here since we know the number is at most 20 characters 52 | write!(num_str, "{}", generation).unwrap(); 53 | // Create the text drawable with the generation number 54 | Text::new( 55 | num_str.as_str(), 56 | Point::new(8, 13), 57 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 58 | ) 59 | // Draw the text to the display 60 | .draw(display)?; 61 | 62 | Ok(()) 63 | } 64 | 65 | fn randomize_grid(rng: &mut Rng, grid: &mut [[bool; WIDTH]; HEIGHT]) { 66 | for row in grid.iter_mut() { 67 | for cell in row.iter_mut() { 68 | // Read a single byte from the RNG 69 | let mut buf = [0u8; 1]; 70 | rng.read(&mut buf); 71 | 72 | // Set the cell to be alive or dead based on the random byte 73 | *cell = buf[0] & 1 != 0; 74 | } 75 | } 76 | } 77 | 78 | // Apply the Game of Life rules: 79 | // 1. Any live cell with fewer than two live neighbors dies, as if by underpopulation. 80 | // 2. Any live cell with two or three live neighbors lives on to the next generation. 81 | // 3. Any live cell with more than three live neighbors dies, as if by overpopulation. 82 | // 4. Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction. 83 | fn update_game_of_life(grid: &mut [[bool; WIDTH]; HEIGHT]) { 84 | let mut new_grid = [[false; WIDTH]; HEIGHT]; 85 | 86 | for y in 0..HEIGHT { 87 | for x in 0..WIDTH { 88 | let alive_neighbors = count_alive_neighbors(x, y, grid); 89 | 90 | new_grid[y][x] = matches!( 91 | (grid[y][x], alive_neighbors), 92 | (true, 2) | (true, 3) | (false, 3) 93 | ); 94 | } 95 | } 96 | 97 | // Copy the new state back into the original grid 98 | *grid = new_grid; 99 | } 100 | 101 | fn count_alive_neighbors(x: usize, y: usize, grid: &[[bool; WIDTH]; HEIGHT]) -> u8 { 102 | let mut count = 0; 103 | 104 | for i in 0..3 { 105 | for j in 0..3 { 106 | if i == 1 && j == 1 { 107 | continue; // Skip the current cell itself 108 | } 109 | 110 | // Calculate the neighbor's coordinates with wrapping 111 | let neighbor_x = (x + i + WIDTH - 1) % WIDTH; 112 | let neighbor_y = (y + j + HEIGHT - 1) % HEIGHT; 113 | 114 | // Increase count if the neighbor is alive 115 | if grid[neighbor_y][neighbor_x] { 116 | count += 1; 117 | } 118 | } 119 | } 120 | 121 | count 122 | } 123 | 124 | fn draw_grid>( 125 | display: &mut D, 126 | grid: &[[bool; WIDTH]; HEIGHT], 127 | ) -> Result<(), D::Error> { 128 | // Define the border color 129 | let border_color = Rgb565::new(230, 230, 230); // Gray color 130 | 131 | for (y, row) in grid.iter().enumerate() { 132 | for (x, &cell) in row.iter().enumerate() { 133 | if cell { 134 | // Live cell with border 135 | // Define the size of the cells and borders 136 | let cell_size = Size::new(5, 5); 137 | let border_size = Size::new(7, 7); // Slightly larger for the border 138 | 139 | // Draw the border rectangle 140 | Rectangle::new(Point::new(x as i32 * 7, y as i32 * 7), border_size) 141 | .into_styled(PrimitiveStyle::with_fill(border_color)) 142 | .draw(display)?; 143 | 144 | // Draw the inner cell rectangle (white) 145 | Rectangle::new(Point::new(x as i32 * 7 + 1, y as i32 * 7 + 1), cell_size) 146 | .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE)) 147 | .draw(display)?; 148 | } else { 149 | // Dead cell without border (black) 150 | Rectangle::new(Point::new(x as i32 * 7, y as i32 * 7), Size::new(7, 7)) 151 | .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) 152 | .draw(display)?; 153 | } 154 | } 155 | } 156 | Ok(()) 157 | } 158 | 159 | #[main] 160 | fn main() -> ! { 161 | let peripherals = esp_hal::init(esp_hal::Config::default()); 162 | esp_println::logger::init_logger_from_env(); 163 | 164 | // let spi = lcd_spi!(peripherals); 165 | let spi = Spi::new( 166 | peripherals.SPI2, 167 | esp_hal::spi::master::Config::default() 168 | .with_frequency(Rate::from_mhz(40)) 169 | .with_mode(Mode::_0), 170 | ) 171 | .unwrap() 172 | .with_sck(peripherals.GPIO7) 173 | .with_mosi(peripherals.GPIO6) 174 | // .with_cs((peripherals.GPIO5)) 175 | ; 176 | // .with_dma((peripherals.DMA_CH0)); 177 | // let mut spi = Spi::new( 178 | // peripherals.SPI2, 179 | // esp_hal::spi::master::Config::default() 180 | // .with_frequency(100.kHz()) 181 | // .with_mode(Mode::_0), 182 | // ) 183 | // .unwrap() 184 | // .with_sck(sclk) 185 | // .with_miso(miso) 186 | // .with_mosi(mosi) 187 | // .with_cs(cs) 188 | // .with_dma(peripherals.DMA_CH0); 189 | 190 | // let di = lcd_display_interface!(peripherals, spi); 191 | // let lcd_dc = Output::new($dc_pin, Level::Low); 192 | let mut buffer = [0_u8; 512]; 193 | 194 | let lcd_dc = Output::new(peripherals.GPIO4, Level::Low, OutputConfig::default()); 195 | // Define the display interface with no chip select 196 | let cs_output = Output::new(peripherals.GPIO5, Level::High, OutputConfig::default()); 197 | let spi_device = ExclusiveDevice::new_no_delay(spi, cs_output).unwrap(); 198 | let di = SpiInterface::new(spi_device, lcd_dc, &mut buffer); 199 | 200 | // let di = display_interface_spi_dma::new_no_cs(crate::LCD_MEMORY_SIZE, spi, lcd_dc); 201 | // let di = SpiInterface::new(spi_device, dc, &mut buffer); 202 | // } 203 | let mut delay = Delay::new(); 204 | delay.delay_ns(500_000u32); 205 | 206 | // let mut display = lcd_display!(peripherals, di).init(&mut delay).unwrap(); 207 | // let mut display = mipidsi::Builder::new(mipidsi::models::ILI9342CRgb565, di) 208 | // .display_size((320 as u16), (240 as u16)) 209 | // .orientation((mipidsi::options::Orientation::new() 210 | // .flip_vertical() 211 | // .flip_horizontal() 212 | // )) 213 | // .color_order(mipidsi::options::ColorOrder::Bgr) 214 | // .reset_pin(OutputOpenDrain::new(peripherals.GPIO48, Level::High, Pull::Up) 215 | // ); 216 | 217 | let reset = Output::new( 218 | peripherals.GPIO48, 219 | Level::High, 220 | OutputConfig::default().with_drive_mode(DriveMode::OpenDrain), 221 | ); 222 | 223 | let mut display = Builder::new(ILI9486Rgb565, di) 224 | .reset_pin(reset) 225 | .init(&mut delay) 226 | .unwrap(); 227 | 228 | // Use the `lcd_backlight_init` macro to turn on the backlight 229 | let mut backlight = Output::new(peripherals.GPIO47, Level::High, OutputConfig::default()); 230 | backlight.set_high(); 231 | // lcd_backlight_init!(peripherals); 232 | 233 | info!("Hello Conway!"); 234 | 235 | let mut grid: [[bool; WIDTH]; HEIGHT] = [[false; WIDTH]; HEIGHT]; 236 | let mut rng = Rng::new(peripherals.RNG); 237 | randomize_grid(&mut rng, &mut grid); 238 | let glider = [(1, 0), (2, 1), (0, 2), (1, 2), (2, 2)]; 239 | for (x, y) in glider.iter() { 240 | grid[*y][*x] = true; 241 | } 242 | let mut generation_count = 0; 243 | 244 | // let mut data = [Rgb565::BLACK; 320 * 240]; 245 | // let mut fbuf = FrameBuf::new(&mut data, 320, 240); 246 | display.clear(Rgb565::BLACK).unwrap(); 247 | 248 | loop { 249 | // Update the game state 250 | update_game_of_life(&mut grid); 251 | 252 | // Draw the updated grid on the display 253 | // draw_grid(&mut fbuf, &grid).unwrap(); 254 | draw_grid(&mut display, &grid).unwrap(); 255 | 256 | generation_count += 1; 257 | 258 | if generation_count >= RESET_AFTER_GENERATIONS { 259 | randomize_grid(&mut rng, &mut grid); 260 | generation_count = 0; // Reset the generation counter 261 | } 262 | 263 | // write_generation(&mut fbuf, generation_count).unwrap(); 264 | write_generation(&mut display, generation_count).unwrap(); 265 | 266 | // let pixel_iterator = fbuf.into_iter().map(|p| p.1); 267 | // // let _ = display.set_pixels(0, 0, 319, 240, pixel_iterator); 268 | // use mipidsi::interface::InterfacePixelFormat; 269 | 270 | // let pixel_iterator = fbuf.into_iter().map(|p| p.1); 271 | 272 | // // Send the pixels to the display 273 | // Rgb565::send_pixels(&mut display, pixel_iterator).unwrap(); 274 | 275 | // Add a delay to control the simulation speed 276 | delay.delay_ms(100u32); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /esp32-s3-box-3-minimal/wokwi.toml: -------------------------------------------------------------------------------- 1 | [wokwi] 2 | version = 1 3 | 4 | elf = "target/xtensa-esp32s3-none-elf/release/esp32-conways-game-of-life-rs" 5 | firmware = "target/xtensa-esp32s3-none-elf/release/esp32-conways-game-of-life-rs" 6 | 7 | #gdbServerPort = 3333 8 | #elf = "target/xtensa-esp32s3-none-elf/debug/esp32-conways-game-of-life-rs" 9 | #firmware = "target/xtensa-esp32s3-none-elf/debug/esp32-conways-game-of-life-rs" 10 | 11 | 12 | -------------------------------------------------------------------------------- /esp32-s3-box-3/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.xtensa-esp32s3-none-elf] 2 | runner = "espflash flash --monitor" 3 | 4 | [env] 5 | ESP_LOG="INFO" 6 | ESP_HAL_CONFIG_PSRAM_MODE = "octal" 7 | 8 | [build] 9 | rustflags = [ 10 | "-C", "link-arg=-nostartfiles", 11 | ] 12 | 13 | target = "xtensa-esp32s3-none-elf" 14 | 15 | [unstable] 16 | build-std = ["alloc", "core"] 17 | -------------------------------------------------------------------------------- /esp32-s3-box-3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esp32-conways-game-of-life-rs" 3 | version = "0.6.0" 4 | authors = ["Juraj Michálek "] 5 | edition = "2024" 6 | license = "MIT OR Apache-2.0" 7 | 8 | 9 | [dependencies] 10 | esp-hal = { version = "1.0.0-beta.1", features = ["esp32s3", "unstable"] } 11 | esp-backtrace = { version = "0.16.0", features = [ 12 | "panic-handler", 13 | "println" 14 | ] } 15 | esp-println = { version = "0.14.0", features = [ "log-04" ] } 16 | log = { version = "0.4.26" } 17 | 18 | esp-alloc = "0.8.0" 19 | embedded-graphics = "0.8.1" 20 | embedded-hal = "1.0.0" 21 | mipidsi = "0.9.0" 22 | #esp-display-interface-spi-dma = "0.3.0" 23 | # esp-display-interface-spi-dma = { path = "../esp-display-interface-spi-dma"} 24 | # esp-bsp = "0.4.1" 25 | embedded-graphics-framebuf = "0.5.0" 26 | heapless = "0.8.0" 27 | embedded-hal-bus = "0.3.0" 28 | #bevy_ecs = { git = "https://github.com/bevyengine/bevy.git", rev = "301f618", default-features = false } 29 | bevy_ecs = { version = "0.16.1", default-features = false } 30 | 31 | [features] 32 | default = [ "esp-hal/esp32s3", "esp-backtrace/esp32s3", "esp-println/esp32s3", "esp32-s3-box-3" ] 33 | 34 | # esp32-s3-box-3 = [ "esp-bsp/esp32-s3-box-3", "esp-hal/psram" ] 35 | esp32-s3-box-3 = [ "esp-hal/psram" ] 36 | -------------------------------------------------------------------------------- /esp32-s3-box-3/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg=-Tlinkall.x"); 3 | } 4 | -------------------------------------------------------------------------------- /esp32-s3-box-3/diagram.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "author": "Juraj Michálek ", 4 | "editor": "wokwi", 5 | "parts": [ 6 | { 7 | "type": "board-esp32-s3-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": {} 20 | } 21 | ], 22 | "connections": [ 23 | [ "esp:TX", "$serialMonitor:RX", "", [] ], 24 | [ "esp:RX", "$serialMonitor:TX", "", [] ], 25 | [ "esp:3V3", "lcd1:VCC", "green", [] ], 26 | [ "esp:GND.1", "lcd1:GND", "black", [] ], 27 | [ "esp:7", "lcd1:SCK", "blue", [] ], 28 | [ "esp:6", "lcd1:MOSI", "orange", [] ], 29 | [ "esp:GND.1", "lcd1:CS", "red", [] ], 30 | [ "esp:4", "lcd1:D/C", "magenta", [] ], 31 | [ "esp:48", "lcd1:RST", "yellow", [] ], 32 | [ "lcd1:LED", "esp:3V3", "white", [] ] 33 | ], 34 | "serialMonitor": { "display": "terminal" }, 35 | "dependencies": {} 36 | } -------------------------------------------------------------------------------- /esp32-s3-box-3/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "esp" 3 | -------------------------------------------------------------------------------- /esp32-s3-box-3/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | extern crate alloc; 5 | use alloc::boxed::Box; 6 | use embedded_graphics_framebuf::backends::FrameBufferBackend; 7 | 8 | use bevy_ecs::prelude::*; 9 | use core::fmt::Write; 10 | use embedded_graphics::{ 11 | Drawable, 12 | mono_font::{MonoTextStyle, ascii::FONT_8X13}, 13 | pixelcolor::Rgb565, 14 | prelude::*, 15 | primitives::{PrimitiveStyle, Rectangle}, 16 | text::Text, 17 | }; 18 | use embedded_graphics_framebuf::FrameBuf; 19 | use embedded_hal::delay::DelayNs; 20 | use embedded_hal_bus::spi::ExclusiveDevice; 21 | use esp_hal::delay::Delay; 22 | use esp_hal::dma::{DmaRxBuf, DmaTxBuf}; 23 | use esp_hal::dma_buffers; 24 | use esp_hal::{ 25 | Blocking, 26 | gpio::{DriveMode, Level, Output, OutputConfig}, 27 | main, 28 | rng::Rng, 29 | spi::master::{Spi, SpiDmaBus}, 30 | time::Rate, 31 | }; 32 | use esp_println::{logger::init_logger_from_env, println}; 33 | use log::info; 34 | use mipidsi::{Builder, models::ILI9486Rgb565}; 35 | use mipidsi::{ 36 | interface::SpiInterface, 37 | options::{ColorInversion, ColorOrder, Orientation}, 38 | }; // includes NonSend and NonSendMut 39 | 40 | #[panic_handler] 41 | fn panic(_info: &core::panic::PanicInfo) -> ! { 42 | println!("Panic: {}", _info); 43 | loop {} 44 | } 45 | 46 | /// A wrapper around a boxed array that implements FrameBufferBackend. 47 | /// This allows the framebuffer to be allocated on the heap. 48 | pub struct HeapBuffer(Box<[C; N]>); 49 | 50 | impl HeapBuffer { 51 | pub fn new(data: Box<[C; N]>) -> Self { 52 | Self(data) 53 | } 54 | } 55 | 56 | impl core::ops::Deref for HeapBuffer { 57 | type Target = [C; N]; 58 | fn deref(&self) -> &Self::Target { 59 | &*self.0 60 | } 61 | } 62 | 63 | impl core::ops::DerefMut for HeapBuffer { 64 | fn deref_mut(&mut self) -> &mut Self::Target { 65 | &mut *self.0 66 | } 67 | } 68 | 69 | impl FrameBufferBackend for HeapBuffer { 70 | type Color = C; 71 | fn set(&mut self, index: usize, color: Self::Color) { 72 | self.0[index] = color; 73 | } 74 | fn get(&self, index: usize) -> Self::Color { 75 | self.0[index] 76 | } 77 | fn nr_elements(&self) -> usize { 78 | N 79 | } 80 | } 81 | 82 | // --- Type Alias for the Concrete Display --- 83 | // Use the DMA-enabled SPI bus type. 84 | type MyDisplay = mipidsi::Display< 85 | SpiInterface< 86 | 'static, 87 | ExclusiveDevice, Output<'static>, Delay>, 88 | Output<'static>, 89 | >, 90 | ILI9486Rgb565, 91 | Output<'static>, 92 | >; 93 | 94 | // --- LCD Resolution and FrameBuffer Type Aliases --- 95 | const LCD_H_RES: usize = 320; 96 | const LCD_V_RES: usize = 240; 97 | const LCD_BUFFER_SIZE: usize = LCD_H_RES * LCD_V_RES; 98 | 99 | // We want our pixels stored as Rgb565. 100 | type FbBuffer = HeapBuffer; 101 | // Define a type alias for the complete FrameBuf. 102 | type MyFrameBuf = FrameBuf; 103 | 104 | #[derive(Resource)] 105 | struct FrameBufferResource { 106 | frame_buf: MyFrameBuf, 107 | } 108 | 109 | impl FrameBufferResource { 110 | fn new() -> Self { 111 | // Allocate the framebuffer data on the heap. 112 | let fb_data: Box<[Rgb565; LCD_BUFFER_SIZE]> = Box::new([Rgb565::BLACK; LCD_BUFFER_SIZE]); 113 | let heap_buffer = HeapBuffer::new(fb_data); 114 | let frame_buf = MyFrameBuf::new(heap_buffer, LCD_H_RES, LCD_V_RES); 115 | Self { frame_buf } 116 | } 117 | } 118 | 119 | // --- Game of Life Definitions --- 120 | // Now each cell is a u8 (0 means dead; >0 indicates age) 121 | const GRID_WIDTH: usize = 64; 122 | const GRID_HEIGHT: usize = 48; 123 | const RESET_AFTER_GENERATIONS: usize = 500; 124 | 125 | fn randomize_grid(rng: &mut Rng, grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 126 | for row in grid.iter_mut() { 127 | for cell in row.iter_mut() { 128 | let mut buf = [0u8; 1]; 129 | rng.read(&mut buf); 130 | // Randomly set cell to 1 (alive) or 0 (dead) 131 | *cell = if buf[0] & 1 != 0 { 1 } else { 0 }; 132 | } 133 | } 134 | } 135 | 136 | fn update_game_of_life(grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 137 | let mut new_grid = [[0u8; GRID_WIDTH]; GRID_HEIGHT]; 138 | for y in 0..GRID_HEIGHT { 139 | for x in 0..GRID_WIDTH { 140 | // Count neighbors: consider a cell alive if its age is >0. 141 | let mut alive_neighbors = 0; 142 | for i in 0..3 { 143 | for j in 0..3 { 144 | if i == 1 && j == 1 { 145 | continue; 146 | } 147 | let nx = (x + i + GRID_WIDTH - 1) % GRID_WIDTH; 148 | let ny = (y + j + GRID_HEIGHT - 1) % GRID_HEIGHT; 149 | if grid[ny][nx] > 0 { 150 | alive_neighbors += 1; 151 | } 152 | } 153 | } 154 | if grid[y][x] > 0 { 155 | // Live cell survives if 2 or 3 neighbors; increment age. 156 | if alive_neighbors == 2 || alive_neighbors == 3 { 157 | new_grid[y][x] = grid[y][x].saturating_add(1); 158 | } else { 159 | new_grid[y][x] = 0; 160 | } 161 | } else { 162 | // Dead cell becomes alive if exactly 3 neighbors. 163 | if alive_neighbors == 3 { 164 | new_grid[y][x] = 1; 165 | } else { 166 | new_grid[y][x] = 0; 167 | } 168 | } 169 | } 170 | } 171 | *grid = new_grid; 172 | } 173 | 174 | /// Maps cell age (1...=max_age) to a color. Newborn cells are dark blue and older cells become brighter (toward white). 175 | fn age_to_color(age: u8) -> Rgb565 { 176 | if age == 0 { 177 | Rgb565::BLACK 178 | } else { 179 | let max_age = 10; 180 | let a = age.min(max_age) as u32; // clamp age and use u32 for intermediate math 181 | let r = ((31 * a) + 5) / max_age as u32; 182 | let g = ((63 * a) + 5) / max_age as u32; 183 | let b = 31; // Keep blue channel constant 184 | // Convert back to u8 and return the color. 185 | Rgb565::new(r as u8, g as u8, b) 186 | } 187 | } 188 | 189 | /// Draws the game grid using the cell age for color. 190 | fn draw_grid>( 191 | display: &mut D, 192 | grid: &[[u8; GRID_WIDTH]; GRID_HEIGHT], 193 | ) -> Result<(), D::Error> { 194 | let border_color = Rgb565::new(230, 230, 230); 195 | for (y, row) in grid.iter().enumerate() { 196 | for (x, &age) in row.iter().enumerate() { 197 | let point = Point::new(x as i32 * 7, y as i32 * 7); 198 | if age > 0 { 199 | // Draw a border then fill with color based on age. 200 | Rectangle::new(point, Size::new(7, 7)) 201 | .into_styled(PrimitiveStyle::with_fill(border_color)) 202 | .draw(display)?; 203 | // Draw an inner cell with color according to age. 204 | Rectangle::new(point + Point::new(1, 1), Size::new(5, 5)) 205 | .into_styled(PrimitiveStyle::with_fill(age_to_color(age))) 206 | .draw(display)?; 207 | } else { 208 | // Draw a dead cell as black. 209 | Rectangle::new(point, Size::new(7, 7)) 210 | .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) 211 | .draw(display)?; 212 | } 213 | } 214 | } 215 | Ok(()) 216 | } 217 | 218 | fn write_generation>( 219 | display: &mut D, 220 | generation: usize, 221 | ) -> Result<(), D::Error> { 222 | let mut num_str = heapless::String::<20>::new(); 223 | write!(num_str, "{}", generation).unwrap(); 224 | Text::new( 225 | num_str.as_str(), 226 | Point::new(8, 13), 227 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 228 | ) 229 | .draw(display)?; 230 | Ok(()) 231 | } 232 | 233 | // --- ECS Resources and Systems --- 234 | 235 | #[derive(Resource)] 236 | struct GameOfLifeResource { 237 | grid: [[u8; GRID_WIDTH]; GRID_HEIGHT], 238 | generation: usize, 239 | } 240 | 241 | impl Default for GameOfLifeResource { 242 | fn default() -> Self { 243 | Self { 244 | grid: [[0; GRID_WIDTH]; GRID_HEIGHT], 245 | generation: 0, 246 | } 247 | } 248 | } 249 | 250 | #[derive(Resource)] 251 | struct RngResource(Rng); 252 | 253 | // Because our display type contains DMA descriptors and raw pointers, it isn’t Sync. 254 | // We wrap it as a NonSend resource so that Bevy doesn’t require Sync. 255 | struct DisplayResource { 256 | display: MyDisplay, 257 | } 258 | 259 | fn update_game_of_life_system( 260 | mut game: ResMut, 261 | mut rng_res: ResMut, 262 | ) { 263 | update_game_of_life(&mut game.grid); 264 | game.generation += 1; 265 | if game.generation >= RESET_AFTER_GENERATIONS { 266 | randomize_grid(&mut rng_res.0, &mut game.grid); 267 | game.generation = 0; 268 | } 269 | } 270 | 271 | /// Render the game state by drawing into the offscreen framebuffer and then flushing 272 | /// it to the display via DMA. After drawing the game grid and generation number, 273 | /// we overlay centered text. 274 | fn render_system( 275 | mut display_res: NonSendMut, 276 | game: Res, 277 | mut fb_res: ResMut, 278 | ) { 279 | // Clear the framebuffer. 280 | fb_res.frame_buf.clear(Rgb565::BLACK).unwrap(); 281 | // Draw the game grid (using the age-based color) and generation number. 282 | draw_grid(&mut fb_res.frame_buf, &game.grid).unwrap(); 283 | write_generation(&mut fb_res.frame_buf, game.generation).unwrap(); 284 | 285 | // --- Overlay centered text --- 286 | let line1 = "Rust no_std ESP32-S3"; 287 | let line2 = "Bevy ECS 0.16 no_std"; 288 | // Estimate text width: assume ~8 pixels per character. 289 | let line1_width = line1.len() as i32 * 8; 290 | let line2_width = line2.len() as i32 * 8; 291 | let x1 = (LCD_H_RES as i32 - line1_width) / 2 + 14; 292 | let x2 = (LCD_H_RES as i32 - line2_width) / 2 + 14; 293 | // For vertical centering, assume 26 pixels total text height. 294 | let y = (LCD_V_RES as i32 - 26) / 2; 295 | Text::new( 296 | line1, 297 | Point::new(x1, y), 298 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 299 | ) 300 | .draw(&mut fb_res.frame_buf) 301 | .unwrap(); 302 | Text::new( 303 | line2, 304 | Point::new(x2, y + 14), 305 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 306 | ) 307 | .draw(&mut fb_res.frame_buf) 308 | .unwrap(); 309 | 310 | // Define the area covering the entire framebuffer. 311 | let area = Rectangle::new(Point::zero(), fb_res.frame_buf.size()); 312 | // Flush the framebuffer to the physical display. 313 | display_res 314 | .display 315 | .fill_contiguous(&area, fb_res.frame_buf.data.iter().copied()) 316 | .unwrap(); 317 | } 318 | 319 | #[main] 320 | fn main() -> ! { 321 | let peripherals = esp_hal::init(esp_hal::Config::default()); 322 | 323 | // PSRAM allocator for heap memory. 324 | // Note: Placing framebuffer into PSRAM might result into slower redraw. 325 | esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram); 326 | // esp_alloc::heap_allocator!(size: 150 * 1024); 327 | 328 | init_logger_from_env(); 329 | 330 | // --- DMA Buffers for SPI --- 331 | let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(8912); 332 | let dma_rx_buf = DmaRxBuf::new(rx_descriptors, rx_buffer).unwrap(); 333 | let dma_tx_buf = DmaTxBuf::new(tx_descriptors, tx_buffer).unwrap(); 334 | 335 | // --- Display Setup using BSP values --- 336 | let spi = Spi::::new( 337 | peripherals.SPI2, 338 | esp_hal::spi::master::Config::default() 339 | .with_frequency(Rate::from_mhz(40)) 340 | .with_mode(esp_hal::spi::Mode::_0), 341 | ) 342 | .unwrap() 343 | .with_sck(peripherals.GPIO7) 344 | .with_mosi(peripherals.GPIO6) 345 | .with_dma(peripherals.DMA_CH0) 346 | .with_buffers(dma_rx_buf, dma_tx_buf); 347 | let cs_output = Output::new(peripherals.GPIO5, Level::High, OutputConfig::default()); 348 | let spi_delay = Delay::new(); 349 | let spi_device = ExclusiveDevice::new(spi, cs_output, spi_delay).unwrap(); 350 | 351 | // LCD interface: DC = GPIO4. 352 | let lcd_dc = Output::new(peripherals.GPIO4, Level::Low, OutputConfig::default()); 353 | // Leak a Box to obtain a 'static mutable buffer. 354 | let buffer: &'static mut [u8; 512] = Box::leak(Box::new([0_u8; 512])); 355 | let di = SpiInterface::new(spi_device, lcd_dc, buffer); 356 | 357 | let mut display_delay = Delay::new(); 358 | display_delay.delay_ns(500_000u32); 359 | 360 | // Reset pin: OpenDrain required for ESP32-S3-BOX! Tricky setting. 361 | let reset = Output::new( 362 | peripherals.GPIO48, 363 | Level::High, 364 | OutputConfig::default().with_drive_mode(DriveMode::OpenDrain), 365 | ); 366 | // Initialize the display using mipidsi's builder. 367 | let mut display: MyDisplay = Builder::new(ILI9486Rgb565, di) 368 | .reset_pin(reset) 369 | .display_size(320, 240) 370 | // .orientation(Orientation::new().flip_horizontal()) 371 | .color_order(ColorOrder::Bgr) 372 | // .invert_colors(ColorInversion::Inverted) 373 | .init(&mut display_delay) 374 | .unwrap(); 375 | 376 | display.clear(Rgb565::BLUE).unwrap(); 377 | 378 | // Backlight. 379 | let mut backlight = Output::new(peripherals.GPIO47, Level::Low, OutputConfig::default()); 380 | backlight.set_high(); 381 | 382 | info!("Display initialized"); 383 | 384 | // --- Initialize Game Resources --- 385 | let mut game = GameOfLifeResource::default(); 386 | let mut rng_instance = Rng::new(peripherals.RNG); 387 | randomize_grid(&mut rng_instance, &mut game.grid); 388 | let glider = [(1, 0), (2, 1), (0, 2), (1, 2), (2, 2)]; 389 | for (x, y) in glider.iter() { 390 | game.grid[*y][*x] = 1; // alive with age 1 391 | } 392 | 393 | // Create the framebuffer resource. 394 | let fb_res = FrameBufferResource::new(); 395 | 396 | let mut world = World::default(); 397 | world.insert_resource(game); 398 | world.insert_resource(RngResource(rng_instance)); 399 | // Insert the display as a non-send resource because its DMA pointers are not Sync. 400 | world.insert_non_send_resource(DisplayResource { display }); 401 | // Insert the framebuffer resource as a normal resource. 402 | world.insert_resource(fb_res); 403 | 404 | let mut schedule = Schedule::default(); 405 | schedule.add_systems(update_game_of_life_system); 406 | schedule.add_systems(render_system); 407 | 408 | let mut loop_delay = Delay::new(); 409 | 410 | loop { 411 | schedule.run(&mut world); 412 | loop_delay.delay_ms(50u32); 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /esp32-s3-box-3/wokwi.toml: -------------------------------------------------------------------------------- 1 | [wokwi] 2 | version = 1 3 | 4 | elf = "target/xtensa-esp32s3-none-elf/release/esp32-conways-game-of-life-rs" 5 | firmware = "target/xtensa-esp32s3-none-elf/release/esp32-conways-game-of-life-rs" 6 | 7 | #gdbServerPort = 3333 8 | #elf = "target/xtensa-esp32s3-none-elf/debug/esp32-conways-game-of-life-rs" 9 | #firmware = "target/xtensa-esp32s3-none-elf/debug/esp32-conways-game-of-life-rs" 10 | 11 | 12 | -------------------------------------------------------------------------------- /esp32-s3-lcd-ev-board/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.xtensa-esp32s3-none-elf] 2 | runner = "espflash flash --monitor" 3 | 4 | [env] 5 | ESP_LOG="INFO" 6 | ESP_HAL_CONFIG_PSRAM_MODE = "octal" 7 | 8 | [build] 9 | rustflags = [ 10 | "-C", "link-arg=-nostartfiles", 11 | ] 12 | 13 | target = "xtensa-esp32s3-none-elf" 14 | 15 | [unstable] 16 | build-std = ["alloc", "core"] 17 | -------------------------------------------------------------------------------- /esp32-s3-lcd-ev-board/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esp32-conways-game-of-life-rs" 3 | version = "0.6.0" 4 | authors = ["Juraj Michálek "] 5 | edition = "2024" 6 | license = "MIT OR Apache-2.0" 7 | 8 | 9 | [dependencies] 10 | esp-hal = { version = "1.0.0-beta.1", features = ["esp32s3", "unstable"] } 11 | #esp-hal = { git = "https://github.com/esp-rs/esp-hal.git", rev = "611bdc6", features = ["esp32s3", "unstable"] } 12 | esp-backtrace = { version = "0.16.0", features = [ 13 | "panic-handler", 14 | "println" 15 | ] } 16 | esp-println = { version = "0.14.0", features = ["log-04"] } 17 | log = { version = "0.4.26" } 18 | 19 | esp-alloc = "0.8.0" 20 | embedded-graphics = "0.8.1" 21 | embedded-hal = "1.0.0" 22 | mipidsi = "0.9.0" 23 | embedded-graphics-framebuf = "0.5.0" 24 | heapless = "0.8.0" 25 | embedded-hal-bus = "0.3.0" 26 | bevy_ecs = { git = "https://github.com/bevyengine/bevy.git", rev = "301f618", default-features = false } 27 | 28 | 29 | [features] 30 | default = [ "esp-hal/esp32s3", "esp-backtrace/esp32s3", "esp-println/esp32s3", "esp32-s3-box-3" ] 31 | 32 | # esp32-s3-box-3 = [ "esp-bsp/esp32-s3-box-3", "esp-hal/psram" ] 33 | esp32-s3-box-3 = [ "esp-hal/psram" ] 34 | -------------------------------------------------------------------------------- /esp32-s3-lcd-ev-board/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg=-Tlinkall.x"); 3 | } 4 | -------------------------------------------------------------------------------- /esp32-s3-lcd-ev-board/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "esp" 3 | -------------------------------------------------------------------------------- /esp32-wrover-kit/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.xtensa-esp32-none-elf] 2 | runner = "espflash flash --monitor" 3 | 4 | [env] 5 | ESP_LOG="INFO" 6 | ESP_HAL_CONFIG_PSRAM_MODE = "quad" 7 | 8 | [build] 9 | rustflags = [ 10 | "-C", "link-arg=-nostartfiles", 11 | ] 12 | 13 | target = "xtensa-esp32-none-elf" 14 | 15 | [unstable] 16 | build-std = ["alloc", "core"] 17 | -------------------------------------------------------------------------------- /esp32-wrover-kit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esp32-conways-game-of-life-rs" 3 | version = "0.5.0" 4 | authors = ["Juraj Michálek "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | 8 | 9 | [dependencies] 10 | esp-hal = { version = "1.0.0-beta.0", features = ["esp32", "unstable", "psram"] } 11 | esp-backtrace = { version = "0.15.1", features = [ 12 | "panic-handler", 13 | "println" 14 | ] } 15 | esp-println = { version = "0.13", features = [ "log" ] } 16 | log = { version = "0.4.26" } 17 | 18 | esp-alloc = "0.7.0" 19 | embedded-graphics = "0.8.1" 20 | embedded-hal = "1.0.0" 21 | mipidsi = "0.9.0" 22 | #esp-display-interface-spi-dma = "0.3.0" 23 | # esp-display-interface-spi-dma = { path = "../esp-display-interface-spi-dma"} 24 | # esp-bsp = "0.4.1" 25 | embedded-graphics-framebuf = "0.5.0" 26 | heapless = "0.8.0" 27 | embedded-hal-bus = "0.3.0" 28 | bevy_ecs = { git = "https://github.com/bevyengine/bevy.git", rev = "06cb5c5", default-features = false } 29 | 30 | 31 | [features] 32 | default = [ "esp-hal/esp32", "esp-backtrace/esp32", "esp-println/esp32", "psram" ] 33 | psram = [] 34 | -------------------------------------------------------------------------------- /esp32-wrover-kit/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg=-Tlinkall.x"); 3 | } 4 | -------------------------------------------------------------------------------- /esp32-wrover-kit/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "esp" 3 | -------------------------------------------------------------------------------- /esp32-wrover-kit/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | extern crate alloc; 5 | use alloc::boxed::Box; 6 | 7 | use core::fmt::Write; 8 | use embedded_hal::delay::DelayNs; 9 | use embedded_graphics::{ 10 | mono_font::{ascii::FONT_8X13, MonoTextStyle}, 11 | pixelcolor::Rgb565, 12 | prelude::*, 13 | primitives::{PrimitiveStyle, Rectangle}, 14 | text::Text, 15 | Drawable, 16 | }; 17 | use embedded_graphics_framebuf::FrameBuf; 18 | use esp_hal::delay::Delay; 19 | use esp_hal::{ 20 | gpio::{Level, Output, OutputConfig, DriveMode}, 21 | rng::Rng, 22 | spi::master::{Spi, SpiDmaBus}, 23 | Blocking, 24 | main, 25 | time::Rate, 26 | }; 27 | use esp_hal::dma::{DmaRxBuf, DmaTxBuf}; 28 | use esp_hal::dma_buffers; 29 | use embedded_hal_bus::spi::ExclusiveDevice; 30 | use esp_println::{logger::init_logger_from_env, println}; 31 | use log::info; 32 | use mipidsi::{interface::SpiInterface, options::{ColorInversion, Orientation, ColorOrder}}; 33 | use mipidsi::{models::ILI9341Rgb565, Builder}; 34 | use bevy_ecs::prelude::*; 35 | use mipidsi::options::Rotation; 36 | // includes NonSend and NonSendMut 37 | 38 | #[panic_handler] 39 | fn panic(_info: &core::panic::PanicInfo) -> ! { 40 | println!("Panic: {}", _info); 41 | loop {} 42 | } 43 | 44 | // --- Type Alias for the Concrete Display --- 45 | // Use the DMA-enabled SPI bus type. 46 | type MyDisplay = mipidsi::Display< 47 | SpiInterface< 48 | 'static, 49 | ExclusiveDevice, Output<'static>, Delay>, 50 | Output<'static> 51 | >, 52 | ILI9341Rgb565, 53 | Output<'static> 54 | >; 55 | 56 | // --- LCD Resolution and FrameBuffer Type Aliases --- 57 | const LCD_H_RES: usize = 320; 58 | const LCD_V_RES: usize = 240; 59 | const LCD_BUFFER_SIZE: usize = LCD_H_RES * LCD_V_RES; 60 | 61 | use embedded_graphics::pixelcolor::PixelColor; 62 | use embedded_graphics_framebuf::backends::FrameBufferBackend; 63 | 64 | /// A wrapper around a boxed array that implements FrameBufferBackend. 65 | /// This allows the framebuffer to be allocated on the heap. 66 | pub struct HeapBuffer(Box<[C; N]>); 67 | 68 | impl HeapBuffer { 69 | pub fn new(data: Box<[C; N]>) -> Self { 70 | Self(data) 71 | } 72 | } 73 | 74 | impl core::ops::Deref for HeapBuffer { 75 | type Target = [C; N]; 76 | fn deref(&self) -> &Self::Target { 77 | &*self.0 78 | } 79 | } 80 | 81 | impl core::ops::DerefMut for HeapBuffer { 82 | fn deref_mut(&mut self) -> &mut Self::Target { 83 | &mut *self.0 84 | } 85 | } 86 | 87 | impl FrameBufferBackend for HeapBuffer { 88 | type Color = C; 89 | fn set(&mut self, index: usize, color: Self::Color) { 90 | self.0[index] = color; 91 | } 92 | fn get(&self, index: usize) -> Self::Color { 93 | self.0[index] 94 | } 95 | fn nr_elements(&self) -> usize { 96 | N 97 | } 98 | } 99 | 100 | 101 | // We want our pixels stored as Rgb565. 102 | type FbBuffer = HeapBuffer; 103 | // Define a type alias for the complete FrameBuf. 104 | type MyFrameBuf = FrameBuf; 105 | 106 | #[derive(Resource)] 107 | struct FrameBufferResource { 108 | frame_buf: MyFrameBuf, 109 | } 110 | 111 | impl FrameBufferResource { 112 | fn new() -> Self { 113 | // Allocate the framebuffer data on the heap. 114 | let fb_data: Box<[Rgb565; LCD_BUFFER_SIZE]> = Box::new([Rgb565::BLACK; LCD_BUFFER_SIZE]); 115 | let heap_buffer = HeapBuffer::new(fb_data); 116 | let frame_buf = MyFrameBuf::new(heap_buffer, LCD_H_RES, LCD_V_RES); 117 | Self { frame_buf } 118 | } 119 | } 120 | 121 | 122 | // --- Game of Life Definitions --- 123 | // Now each cell is a u8 (0 means dead; >0 indicates age) 124 | const GRID_WIDTH: usize = 64; 125 | const GRID_HEIGHT: usize = 48; 126 | const RESET_AFTER_GENERATIONS: usize = 500; 127 | 128 | fn randomize_grid(rng: &mut Rng, grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 129 | for row in grid.iter_mut() { 130 | for cell in row.iter_mut() { 131 | let mut buf = [0u8; 1]; 132 | rng.read(&mut buf); 133 | // Randomly set cell to 1 (alive) or 0 (dead) 134 | *cell = if buf[0] & 1 != 0 { 1 } else { 0 }; 135 | } 136 | } 137 | } 138 | 139 | fn update_game_of_life(grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 140 | let mut new_grid = [[0u8; GRID_WIDTH]; GRID_HEIGHT]; 141 | for y in 0..GRID_HEIGHT { 142 | for x in 0..GRID_WIDTH { 143 | // Count neighbors: consider a cell alive if its age is >0. 144 | let mut alive_neighbors = 0; 145 | for i in 0..3 { 146 | for j in 0..3 { 147 | if i == 1 && j == 1 { continue; } 148 | let nx = (x + i + GRID_WIDTH - 1) % GRID_WIDTH; 149 | let ny = (y + j + GRID_HEIGHT - 1) % GRID_HEIGHT; 150 | if grid[ny][nx] > 0 { 151 | alive_neighbors += 1; 152 | } 153 | } 154 | } 155 | if grid[y][x] > 0 { 156 | // Live cell survives if 2 or 3 neighbors; increment age. 157 | if alive_neighbors == 2 || alive_neighbors == 3 { 158 | new_grid[y][x] = grid[y][x].saturating_add(1); 159 | } else { 160 | new_grid[y][x] = 0; 161 | } 162 | } else { 163 | // Dead cell becomes alive if exactly 3 neighbors. 164 | if alive_neighbors == 3 { 165 | new_grid[y][x] = 1; 166 | } else { 167 | new_grid[y][x] = 0; 168 | } 169 | } 170 | } 171 | } 172 | *grid = new_grid; 173 | } 174 | 175 | /// Maps cell age (1..=max_age) to a color. Newborn cells are dark blue and older cells become brighter (toward white). 176 | fn age_to_color(age: u8) -> Rgb565 { 177 | if age == 0 { 178 | Rgb565::BLACK 179 | } else { 180 | let max_age = 10; 181 | let a = age.min(max_age) as u32; // clamp age and use u32 for intermediate math 182 | let r = ((31 * a) + 5) / max_age as u32; 183 | let g = ((63 * a) + 5) / max_age as u32; 184 | let b = 31; // Keep blue channel constant 185 | // Convert back to u8 and return the color. 186 | Rgb565::new(r as u8, g as u8, b) 187 | } 188 | } 189 | 190 | /// Draws the game grid using the cell age for color. 191 | fn draw_grid>( 192 | display: &mut D, 193 | grid: &[[u8; GRID_WIDTH]; GRID_HEIGHT], 194 | ) -> Result<(), D::Error> { 195 | let border_color = Rgb565::new(230, 230, 230); 196 | for (y, row) in grid.iter().enumerate() { 197 | for (x, &age) in row.iter().enumerate() { 198 | let point = Point::new(x as i32 * 7, y as i32 * 7); 199 | if age > 0 { 200 | // Draw a border then fill with color based on age. 201 | Rectangle::new(point, Size::new(7, 7)) 202 | .into_styled(PrimitiveStyle::with_fill(border_color)) 203 | .draw(display)?; 204 | // Draw an inner cell with color according to age. 205 | Rectangle::new(point + Point::new(1, 1), Size::new(5, 5)) 206 | .into_styled(PrimitiveStyle::with_fill(age_to_color(age))) 207 | .draw(display)?; 208 | } else { 209 | // Draw a dead cell as black. 210 | Rectangle::new(point, Size::new(7, 7)) 211 | .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) 212 | .draw(display)?; 213 | } 214 | } 215 | } 216 | Ok(()) 217 | } 218 | 219 | fn write_generation>( 220 | display: &mut D, 221 | generation: usize, 222 | ) -> Result<(), D::Error> { 223 | let mut num_str = heapless::String::<20>::new(); 224 | write!(num_str, "{}", generation).unwrap(); 225 | Text::new( 226 | num_str.as_str(), 227 | Point::new(8, 13), 228 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 229 | ) 230 | .draw(display)?; 231 | Ok(()) 232 | } 233 | 234 | // --- ECS Resources and Systems --- 235 | 236 | #[derive(Resource)] 237 | struct GameOfLifeResource { 238 | grid: [[u8; GRID_WIDTH]; GRID_HEIGHT], 239 | generation: usize, 240 | } 241 | 242 | impl Default for GameOfLifeResource { 243 | fn default() -> Self { 244 | Self { 245 | grid: [[0; GRID_WIDTH]; GRID_HEIGHT], 246 | generation: 0, 247 | } 248 | } 249 | } 250 | 251 | #[derive(Resource)] 252 | struct RngResource(Rng); 253 | 254 | // Because our display type contains DMA descriptors and raw pointers, it isn’t Sync. 255 | // We wrap it as a NonSend resource so that Bevy doesn’t require Sync. 256 | struct DisplayResource { 257 | display: MyDisplay, 258 | } 259 | 260 | fn update_game_of_life_system( 261 | mut game: ResMut, 262 | mut rng_res: ResMut, 263 | ) { 264 | update_game_of_life(&mut game.grid); 265 | game.generation += 1; 266 | if game.generation >= RESET_AFTER_GENERATIONS { 267 | randomize_grid(&mut rng_res.0, &mut game.grid); 268 | game.generation = 0; 269 | } 270 | } 271 | 272 | /// Render the game state by drawing into the offscreen framebuffer and then flushing 273 | /// it to the display via DMA. After drawing the game grid and generation number, 274 | /// we overlay centered text. 275 | fn render_system( 276 | mut display_res: NonSendMut, 277 | game: Res, 278 | mut fb_res: ResMut, 279 | ) { 280 | // Clear the framebuffer. 281 | fb_res.frame_buf.clear(Rgb565::BLACK).unwrap(); 282 | // Draw the game grid (using the age-based color) and generation number. 283 | draw_grid(&mut fb_res.frame_buf, &game.grid).unwrap(); 284 | write_generation(&mut fb_res.frame_buf, game.generation).unwrap(); 285 | 286 | // --- Overlay centered text --- 287 | let line1 = "Rust no_std ESP32"; 288 | let line2 = "Bevy ECS 0.15 no_std"; 289 | // Estimate text width: assume ~8 pixels per character. 290 | let line1_width = line1.len() as i32 * 8; 291 | let line2_width = line2.len() as i32 * 8; 292 | let x1 = (LCD_H_RES as i32 - line1_width) / 2 + 14; 293 | let x2 = (LCD_H_RES as i32 - line2_width) / 2 + 14; 294 | // For vertical centering, assume 26 pixels total text height. 295 | let y = (LCD_V_RES as i32 - 26) / 2; 296 | Text::new( 297 | line1, 298 | Point::new(x1, y), 299 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 300 | ) 301 | .draw(&mut fb_res.frame_buf) 302 | .unwrap(); 303 | Text::new( 304 | line2, 305 | Point::new(x2, y + 14), 306 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 307 | ) 308 | .draw(&mut fb_res.frame_buf) 309 | .unwrap(); 310 | 311 | // Define the area covering the entire framebuffer. 312 | let area = Rectangle::new(Point::zero(), fb_res.frame_buf.size()); 313 | // Flush the framebuffer to the physical display. 314 | display_res 315 | .display 316 | .fill_contiguous(&area, fb_res.frame_buf.data.iter().copied()) 317 | .unwrap(); 318 | } 319 | 320 | #[main] 321 | fn main() -> ! { 322 | let peripherals = esp_hal::init(esp_hal::Config::default()); 323 | 324 | // PSRAM allocator for heap memory. 325 | // Note: Placing framebuffer into PSRAM might result into slower redraw. 326 | esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram); 327 | // esp_alloc::heap_allocator!(size: 130 * 1024); 328 | 329 | init_logger_from_env(); 330 | 331 | // --- DMA Buffers for SPI --- 332 | let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(8912); 333 | let dma_rx_buf = DmaRxBuf::new(rx_descriptors, rx_buffer).unwrap(); 334 | let dma_tx_buf = DmaTxBuf::new(tx_descriptors, tx_buffer).unwrap(); 335 | 336 | // --- Display Setup using BSP values --- 337 | let spi = Spi::::new( 338 | peripherals.SPI3, 339 | esp_hal::spi::master::Config::default() 340 | .with_frequency(Rate::from_mhz(40)) 341 | .with_mode(esp_hal::spi::Mode::_0), 342 | ) 343 | .unwrap() 344 | .with_sck(peripherals.GPIO19) 345 | .with_mosi(peripherals.GPIO23) 346 | .with_dma(peripherals.DMA_SPI3) 347 | .with_buffers(dma_rx_buf, dma_tx_buf); 348 | let cs_output = Output::new(peripherals.GPIO22, Level::High, OutputConfig::default()); 349 | let spi_delay = Delay::new(); 350 | let spi_device = ExclusiveDevice::new(spi, cs_output, spi_delay).unwrap(); 351 | 352 | // LCD interface 353 | let lcd_dc = Output::new(peripherals.GPIO21, Level::Low, OutputConfig::default()); 354 | // Leak a Box to obtain a 'static mutable buffer. 355 | let buffer: &'static mut [u8; 512] = Box::leak(Box::new([0_u8; 512])); 356 | let di = SpiInterface::new(spi_device, lcd_dc, buffer); 357 | 358 | let mut display_delay = Delay::new(); 359 | display_delay.delay_ns(500_000u32); 360 | 361 | // Reset pin: OpenDrain required for ESP32-S3-BOX! Tricky setting. 362 | // For some Wrover-Kit boards the reset pin must be pulsed low. 363 | let mut reset = Output::new( 364 | peripherals.GPIO18, 365 | Level::Low, 366 | OutputConfig::default() 367 | ); 368 | // Pulse the reset pin: drive low for 100 ms then high. 369 | reset.set_low(); 370 | Delay::new().delay_ms(100u32); 371 | reset.set_high(); 372 | 373 | // Initialize the display using mipidsi's builder. 374 | let mut display: MyDisplay = Builder::new(ILI9341Rgb565, di) 375 | .reset_pin(reset) 376 | .display_size(240, 320) 377 | .orientation(Orientation::new().rotate(Rotation::Deg90).flip_horizontal()) 378 | 379 | .color_order(ColorOrder::Bgr) 380 | // .invert_colors(ColorInversion::Inverted) 381 | .init(&mut display_delay) 382 | .unwrap(); 383 | 384 | display.clear(Rgb565::BLUE).unwrap(); 385 | 386 | // Backlight. 387 | let mut backlight = Output::new(peripherals.GPIO5, Level::High, OutputConfig::default()); 388 | backlight.set_low(); 389 | 390 | info!("Display initialized"); 391 | 392 | // --- Initialize Game Resources --- 393 | let mut game = GameOfLifeResource::default(); 394 | let mut rng_instance = Rng::new(peripherals.RNG); 395 | randomize_grid(&mut rng_instance, &mut game.grid); 396 | let glider = [(1, 0), (2, 1), (0, 2), (1, 2), (2, 2)]; 397 | for (x, y) in glider.iter() { 398 | game.grid[*y][*x] = 1; // alive with age 1 399 | } 400 | 401 | // Create the framebuffer resource. 402 | let fb_res = FrameBufferResource::new(); 403 | 404 | let mut world = World::default(); 405 | world.insert_resource(game); 406 | world.insert_resource(RngResource(rng_instance)); 407 | // Insert the display as a non-send resource because its DMA pointers are not Sync. 408 | world.insert_non_send_resource(DisplayResource { display }); 409 | // Insert the framebuffer resource as a normal resource. 410 | world.insert_resource(fb_res); 411 | 412 | let mut schedule = Schedule::default(); 413 | schedule.add_systems(update_game_of_life_system); 414 | schedule.add_systems(render_system); 415 | 416 | let mut loop_delay = Delay::new(); 417 | 418 | loop { 419 | schedule.run(&mut world); 420 | loop_delay.delay_ms(50u32); 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /m5stack-atom-s3/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.xtensa-esp32s3-none-elf] 2 | runner = "espflash flash --monitor" 3 | 4 | [env] 5 | ESP_LOG="INFO" 6 | #ESP_HAL_CONFIG_PSRAM_MODE = "octal" 7 | 8 | [build] 9 | rustflags = [ 10 | "-C", "link-arg=-nostartfiles", 11 | ] 12 | 13 | target = "xtensa-esp32s3-none-elf" 14 | 15 | [unstable] 16 | build-std = ["alloc", "core"] 17 | -------------------------------------------------------------------------------- /m5stack-atom-s3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esp32-conways-game-of-life-rs" 3 | version = "0.6.0" 4 | authors = ["Juraj Michálek "] 5 | edition = "2024" 6 | license = "MIT OR Apache-2.0" 7 | 8 | 9 | [dependencies] 10 | esp-hal = { version = "1.0.0-beta.0", features = ["esp32s3", "unstable"] } 11 | esp-backtrace = { version = "0.15.1", features = [ 12 | "panic-handler", 13 | "println" 14 | ] } 15 | esp-println = { version = "0.13", features = [ "log" ] } 16 | log = { version = "0.4.26" } 17 | 18 | esp-alloc = "0.7.0" 19 | embedded-graphics = "0.8.1" 20 | embedded-hal = "1.0.0" 21 | mipidsi = "0.9.0" 22 | embedded-graphics-framebuf = "0.5.0" 23 | heapless = "0.8.0" 24 | embedded-hal-bus = "0.3.0" 25 | bevy_ecs = { git = "https://github.com/bevyengine/bevy.git", rev = "06cb5c5", default-features = false } 26 | 27 | 28 | [features] 29 | default = [ "esp-hal/esp32s3", "esp-backtrace/esp32s3", "esp-println/esp32s3" ] 30 | 31 | # esp32-s3-box-3 = [ "esp-bsp/esp32-s3-box-3", "esp-hal/psram" ] 32 | #esp32-s3-box-3 = [ "esp-hal/psram" ] -------------------------------------------------------------------------------- /m5stack-atom-s3/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg=-Tlinkall.x"); 3 | } 4 | -------------------------------------------------------------------------------- /m5stack-atom-s3/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "esp" 3 | -------------------------------------------------------------------------------- /m5stack-atom-s3/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | extern crate alloc; 5 | use alloc::boxed::Box; 6 | use embedded_graphics_framebuf::backends::FrameBufferBackend; 7 | 8 | use bevy_ecs::prelude::*; 9 | use core::fmt::Write; 10 | use embedded_graphics::{ 11 | Drawable, 12 | mono_font::{MonoTextStyle, ascii::FONT_8X13}, 13 | pixelcolor::Rgb565, 14 | prelude::*, 15 | primitives::{PrimitiveStyle, Rectangle}, 16 | text::Text, 17 | }; 18 | use embedded_graphics_framebuf::FrameBuf; 19 | use embedded_hal::delay::DelayNs; 20 | use embedded_hal_bus::spi::ExclusiveDevice; 21 | use esp_hal::delay::Delay; 22 | use esp_hal::dma::{DmaRxBuf, DmaTxBuf}; 23 | use esp_hal::dma_buffers; 24 | use esp_hal::{ 25 | Blocking, 26 | gpio::{DriveMode, Input, InputConfig, Level, Output, OutputConfig, Pull}, 27 | main, 28 | rng::Rng, 29 | spi::master::{Spi, SpiDmaBus}, 30 | time::Rate, 31 | }; 32 | use esp_println::{logger::init_logger_from_env, println}; 33 | use log::info; 34 | use mipidsi::{Builder, models::GC9A01}; 35 | use mipidsi::{ 36 | interface::SpiInterface, 37 | options::{ColorInversion, ColorOrder, Orientation}, 38 | }; // includes NonSend and NonSendMut 39 | 40 | #[panic_handler] 41 | fn panic(_info: &core::panic::PanicInfo) -> ! { 42 | println!("Panic: {}", _info); 43 | loop {} 44 | } 45 | 46 | /// A wrapper around a boxed array that implements FrameBufferBackend. 47 | /// This allows the framebuffer to be allocated on the heap. 48 | pub struct HeapBuffer(Box<[C; N]>); 49 | 50 | impl HeapBuffer { 51 | pub fn new(data: Box<[C; N]>) -> Self { 52 | Self(data) 53 | } 54 | } 55 | 56 | impl core::ops::Deref for HeapBuffer { 57 | type Target = [C; N]; 58 | fn deref(&self) -> &Self::Target { 59 | &*self.0 60 | } 61 | } 62 | 63 | impl core::ops::DerefMut for HeapBuffer { 64 | fn deref_mut(&mut self) -> &mut Self::Target { 65 | &mut *self.0 66 | } 67 | } 68 | 69 | impl FrameBufferBackend for HeapBuffer { 70 | type Color = C; 71 | fn set(&mut self, index: usize, color: Self::Color) { 72 | self.0[index] = color; 73 | } 74 | fn get(&self, index: usize) -> Self::Color { 75 | self.0[index] 76 | } 77 | fn nr_elements(&self) -> usize { 78 | N 79 | } 80 | } 81 | 82 | // --- Type Alias for the Concrete Display --- 83 | // Use the DMA-enabled SPI bus type. 84 | type MyDisplay = mipidsi::Display< 85 | SpiInterface< 86 | 'static, 87 | ExclusiveDevice, Output<'static>, Delay>, 88 | Output<'static>, 89 | >, 90 | GC9A01, 91 | Output<'static>, 92 | >; 93 | 94 | // --- LCD Resolution and FrameBuffer Type Aliases --- 95 | const LCD_H_RES: usize = 130; 96 | const LCD_V_RES: usize = 129; 97 | const LCD_BUFFER_SIZE: usize = LCD_H_RES * LCD_V_RES; 98 | 99 | // We want our pixels stored as Rgb565. 100 | type FbBuffer = HeapBuffer; 101 | // Define a type alias for the complete FrameBuf. 102 | type MyFrameBuf = FrameBuf; 103 | 104 | #[derive(Resource)] 105 | struct FrameBufferResource { 106 | frame_buf: MyFrameBuf, 107 | } 108 | 109 | impl FrameBufferResource { 110 | fn new() -> Self { 111 | // Allocate the framebuffer data on the heap. 112 | let fb_data: Box<[Rgb565; LCD_BUFFER_SIZE]> = Box::new([Rgb565::BLACK; LCD_BUFFER_SIZE]); 113 | let heap_buffer = HeapBuffer::new(fb_data); 114 | let frame_buf = MyFrameBuf::new(heap_buffer, LCD_H_RES, LCD_V_RES); 115 | Self { frame_buf } 116 | } 117 | } 118 | 119 | // --- Game of Life Definitions --- 120 | // Now each cell is a u8 (0 means dead; >0 indicates age) 121 | const GRID_WIDTH: usize = 48; 122 | const GRID_HEIGHT: usize = 48; 123 | const RESET_AFTER_GENERATIONS: usize = 500; 124 | 125 | fn randomize_grid(rng: &mut Rng, grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 126 | for row in grid.iter_mut() { 127 | for cell in row.iter_mut() { 128 | let mut buf = [0u8; 1]; 129 | rng.read(&mut buf); 130 | // Randomly set cell to 1 (alive) or 0 (dead) 131 | *cell = if buf[0] & 1 != 0 { 1 } else { 0 }; 132 | } 133 | } 134 | } 135 | 136 | fn update_game_of_life(grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 137 | let mut new_grid = [[0u8; GRID_WIDTH]; GRID_HEIGHT]; 138 | for y in 0..GRID_HEIGHT { 139 | for x in 0..GRID_WIDTH { 140 | // Count neighbors: consider a cell alive if its age is >0. 141 | let mut alive_neighbors = 0; 142 | for i in 0..3 { 143 | for j in 0..3 { 144 | if i == 1 && j == 1 { 145 | continue; 146 | } 147 | let nx = (x + i + GRID_WIDTH - 1) % GRID_WIDTH; 148 | let ny = (y + j + GRID_HEIGHT - 1) % GRID_HEIGHT; 149 | if grid[ny][nx] > 0 { 150 | alive_neighbors += 1; 151 | } 152 | } 153 | } 154 | if grid[y][x] > 0 { 155 | // Live cell survives if 2 or 3 neighbors; increment age. 156 | if alive_neighbors == 2 || alive_neighbors == 3 { 157 | new_grid[y][x] = grid[y][x].saturating_add(1); 158 | } else { 159 | new_grid[y][x] = 0; 160 | } 161 | } else { 162 | // Dead cell becomes alive if exactly 3 neighbors. 163 | if alive_neighbors == 3 { 164 | new_grid[y][x] = 1; 165 | } else { 166 | new_grid[y][x] = 0; 167 | } 168 | } 169 | } 170 | } 171 | *grid = new_grid; 172 | } 173 | 174 | /// Maps cell age (1...=max_age) to a color. Newborn cells are dark blue and older cells become brighter (toward white). 175 | fn age_to_color(age: u8) -> Rgb565 { 176 | if age == 0 { 177 | Rgb565::BLACK 178 | } else { 179 | let max_age = 10; 180 | let a = age.min(max_age) as u32; // clamp age and use u32 for intermediate math 181 | let r = ((31 * a) + 5) / max_age as u32; 182 | let g = ((63 * a) + 5) / max_age as u32; 183 | let b = 31; // Keep blue channel constant 184 | // Convert back to u8 and return the color. 185 | Rgb565::new(r as u8, g as u8, b) 186 | } 187 | } 188 | 189 | /// Draws the game grid using the cell age for color. 190 | fn draw_grid>( 191 | display: &mut D, 192 | grid: &[[u8; GRID_WIDTH]; GRID_HEIGHT], 193 | ) -> Result<(), D::Error> { 194 | let border_color = Rgb565::new(230, 230, 230); 195 | for (y, row) in grid.iter().enumerate() { 196 | for (x, &age) in row.iter().enumerate() { 197 | let point = Point::new(x as i32 * 7, y as i32 * 7); 198 | if age > 0 { 199 | // Draw a border then fill with color based on age. 200 | Rectangle::new(point, Size::new(7, 7)) 201 | .into_styled(PrimitiveStyle::with_fill(border_color)) 202 | .draw(display)?; 203 | // Draw an inner cell with color according to age. 204 | Rectangle::new(point + Point::new(1, 1), Size::new(5, 5)) 205 | .into_styled(PrimitiveStyle::with_fill(age_to_color(age))) 206 | .draw(display)?; 207 | } else { 208 | // Draw a dead cell as black. 209 | Rectangle::new(point, Size::new(7, 7)) 210 | .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) 211 | .draw(display)?; 212 | } 213 | } 214 | } 215 | Ok(()) 216 | } 217 | 218 | fn write_generation>( 219 | display: &mut D, 220 | generation: usize, 221 | ) -> Result<(), D::Error> { 222 | let mut num_str = heapless::String::<20>::new(); 223 | write!(num_str, "{}", generation).unwrap(); 224 | Text::new( 225 | num_str.as_str(), 226 | Point::new(8, 13), 227 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 228 | ) 229 | .draw(display)?; 230 | Ok(()) 231 | } 232 | 233 | // --- ECS Resources and Systems --- 234 | 235 | #[derive(Resource)] 236 | struct GameOfLifeResource { 237 | grid: [[u8; GRID_WIDTH]; GRID_HEIGHT], 238 | generation: usize, 239 | } 240 | 241 | impl Default for GameOfLifeResource { 242 | fn default() -> Self { 243 | Self { 244 | grid: [[0; GRID_WIDTH]; GRID_HEIGHT], 245 | generation: 0, 246 | } 247 | } 248 | } 249 | 250 | #[derive(Resource)] 251 | struct RngResource(Rng); 252 | 253 | // Because our display type contains DMA descriptors and raw pointers, it isn’t Sync. 254 | // We wrap it as a NonSend resource so that Bevy doesn’t require Sync. 255 | struct DisplayResource { 256 | display: MyDisplay, 257 | } 258 | 259 | struct ButtonResource { 260 | button: Input<'static>, 261 | } 262 | 263 | /// Resource to track the previous state of the button (for edge detection). 264 | #[derive(Default, Resource)] 265 | struct ButtonState { 266 | was_pressed: bool, 267 | } 268 | 269 | fn update_game_of_life_system( 270 | mut game: ResMut, 271 | mut rng_res: ResMut, 272 | ) { 273 | update_game_of_life(&mut game.grid); 274 | game.generation += 1; 275 | if game.generation >= RESET_AFTER_GENERATIONS { 276 | randomize_grid(&mut rng_res.0, &mut game.grid); 277 | game.generation = 0; 278 | } 279 | } 280 | 281 | /// System to check the button and reset the simulation when pressed. 282 | fn button_reset_system( 283 | mut game: ResMut, 284 | mut rng_res: ResMut, 285 | mut btn_state: ResMut, 286 | button_res: NonSend, 287 | ) { 288 | // Check if the button is pressed (active low) 289 | if button_res.button.is_low() { 290 | if !btn_state.was_pressed { 291 | // Button press detected: reset simulation. 292 | randomize_grid(&mut rng_res.0, &mut game.grid); 293 | game.generation = 0; 294 | btn_state.was_pressed = true; 295 | } 296 | } else { 297 | btn_state.was_pressed = false; 298 | } 299 | } 300 | 301 | /// Render the game state by drawing into the offscreen framebuffer and then flushing 302 | /// it to the display via DMA. After drawing the game grid and generation number, 303 | /// we overlay centered text. 304 | fn render_system( 305 | mut display_res: NonSendMut, 306 | game: Res, 307 | mut fb_res: ResMut, 308 | ) { 309 | // Clear the framebuffer. 310 | fb_res.frame_buf.clear(Rgb565::BLACK).unwrap(); 311 | // Draw the game grid (using the age-based color) and generation number. 312 | draw_grid(&mut fb_res.frame_buf, &game.grid).unwrap(); 313 | write_generation(&mut fb_res.frame_buf, game.generation).unwrap(); 314 | 315 | // --- Overlay centered text --- 316 | let line1 = "Rust - ATOM-S3"; 317 | let line2 = "Bevy ECS no_std"; 318 | // Estimate text width: assume ~8 pixels per character. 319 | let line1_width = line1.len() as i32 * 8; 320 | let line2_width = line2.len() as i32 * 8; 321 | let x1 = (LCD_H_RES as i32 - line1_width) / 2; 322 | let x2 = (LCD_H_RES as i32 - line2_width) / 2; 323 | // For vertical centering, assume 26 pixels total text height. 324 | let y = (LCD_V_RES as i32 - 26) / 2; 325 | Text::new( 326 | line1, 327 | Point::new(x1, y), 328 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 329 | ) 330 | .draw(&mut fb_res.frame_buf) 331 | .unwrap(); 332 | Text::new( 333 | line2, 334 | Point::new(x2, y + 14), 335 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 336 | ) 337 | .draw(&mut fb_res.frame_buf) 338 | .unwrap(); 339 | 340 | // Define the area covering the entire framebuffer. 341 | let area = Rectangle::new(Point::zero(), fb_res.frame_buf.size()); 342 | // Flush the framebuffer to the physical display. 343 | display_res 344 | .display 345 | .fill_contiguous(&area, fb_res.frame_buf.data.iter().copied()) 346 | .unwrap(); 347 | } 348 | 349 | #[main] 350 | fn main() -> ! { 351 | let peripherals = esp_hal::init(esp_hal::Config::default()); 352 | 353 | // esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram); 354 | esp_alloc::heap_allocator!(size: 150 * 1024); 355 | 356 | init_logger_from_env(); 357 | 358 | // --- DMA Buffers for SPI --- 359 | let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(8912); 360 | let dma_rx_buf = DmaRxBuf::new(rx_descriptors, rx_buffer).unwrap(); 361 | let dma_tx_buf = DmaTxBuf::new(tx_descriptors, tx_buffer).unwrap(); 362 | 363 | // --- Display Setup using BSP values --- 364 | let spi = Spi::::new( 365 | peripherals.SPI2, 366 | esp_hal::spi::master::Config::default() 367 | .with_frequency(Rate::from_mhz(40)) 368 | .with_mode(esp_hal::spi::Mode::_0), 369 | ) 370 | .unwrap() 371 | .with_sck(peripherals.GPIO17) 372 | .with_mosi(peripherals.GPIO21) 373 | .with_dma(peripherals.DMA_CH0) 374 | .with_buffers(dma_rx_buf, dma_tx_buf); 375 | let cs_output = Output::new(peripherals.GPIO15, Level::High, OutputConfig::default()); 376 | let spi_delay = Delay::new(); 377 | let spi_device = ExclusiveDevice::new(spi, cs_output, spi_delay).unwrap(); 378 | 379 | // LCD interface: DC. 380 | let lcd_dc = Output::new(peripherals.GPIO33, Level::Low, OutputConfig::default()); 381 | // Leak a Box to obtain a 'static mutable buffer. 382 | let buffer: &'static mut [u8; 512] = Box::leak(Box::new([0_u8; 512])); 383 | let di = SpiInterface::new(spi_device, lcd_dc, buffer); 384 | 385 | let mut display_delay = Delay::new(); 386 | display_delay.delay_ns(500_000u32); 387 | 388 | let reset = Output::new(peripherals.GPIO34, Level::High, OutputConfig::default()); 389 | // Initialize the display using mipidsi's builder. 390 | let mut display: MyDisplay = Builder::new(GC9A01, di) 391 | .reset_pin(reset) 392 | .display_size(130, 129) 393 | // .orientation(Orientation::new().flip_horizontal()) 394 | .color_order(ColorOrder::Bgr) 395 | .invert_colors(ColorInversion::Inverted) 396 | .init(&mut display_delay) 397 | .unwrap(); 398 | 399 | display.clear(Rgb565::BLUE).unwrap(); 400 | 401 | // Backlight. 402 | let mut backlight = Output::new(peripherals.GPIO16, Level::Low, OutputConfig::default()); 403 | backlight.set_high(); 404 | 405 | info!("Display initialized"); 406 | 407 | // --- Initialize Game Resources --- 408 | let mut game = GameOfLifeResource::default(); 409 | let mut rng_instance = Rng::new(peripherals.RNG); 410 | randomize_grid(&mut rng_instance, &mut game.grid); 411 | let glider = [(1, 0), (2, 1), (0, 2), (1, 2), (2, 2)]; 412 | for (x, y) in glider.iter() { 413 | game.grid[*y][*x] = 1; // alive with age 1 414 | } 415 | 416 | // Create the framebuffer resource. 417 | let fb_res = FrameBufferResource::new(); 418 | 419 | let mut world = World::default(); 420 | world.insert_resource(game); 421 | world.insert_resource(RngResource(rng_instance)); 422 | // Insert the display as a non-send resource because its DMA pointers are not Sync. 423 | world.insert_non_send_resource(DisplayResource { display }); 424 | // Insert the framebuffer resource as a normal resource. 425 | world.insert_resource(fb_res); 426 | 427 | // --- Initialize Button Resource --- 428 | // Configure the button as an input with an internal pull-up (active low). 429 | let button = Input::new( 430 | peripherals.GPIO41, 431 | InputConfig::default().with_pull(Pull::Up), 432 | ); 433 | world.insert_non_send_resource(ButtonResource { button }); 434 | // Insert a resource to track button state for debouncing. 435 | world.insert_resource(ButtonState::default()); 436 | 437 | // --- Build ECS Schedule --- 438 | // We add the button system so that a button press resets the simulation. 439 | let mut schedule = Schedule::default(); 440 | schedule.add_systems(button_reset_system); 441 | schedule.add_systems(update_game_of_life_system); 442 | schedule.add_systems(render_system); 443 | 444 | let mut loop_delay = Delay::new(); 445 | 446 | loop { 447 | schedule.run(&mut world); 448 | loop_delay.delay_ms(50u32); 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /m5stack-cores3/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.xtensa-esp32s3-none-elf] 2 | runner = "espflash flash --monitor" 3 | 4 | [env] 5 | ESP_LOG="INFO" 6 | #ESP_HAL_CONFIG_PSRAM_MODE = "octal" 7 | 8 | [build] 9 | rustflags = [ 10 | "-C", "link-arg=-nostartfiles", 11 | ] 12 | 13 | target = "xtensa-esp32s3-none-elf" 14 | 15 | [unstable] 16 | build-std = ["alloc", "core"] 17 | -------------------------------------------------------------------------------- /m5stack-cores3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esp32-conways-game-of-life-rs" 3 | version = "0.6.0" 4 | authors = ["Juraj Michálek "] 5 | edition = "2024" 6 | license = "MIT OR Apache-2.0" 7 | 8 | 9 | [dependencies] 10 | esp-hal = { version = "1.0.0-beta.0", features = ["esp32s3", "unstable"] } 11 | esp-backtrace = { version = "0.15.1", features = [ 12 | "panic-handler", 13 | "println" 14 | ] } 15 | esp-println = { version = "0.13", features = [ "log" ] } 16 | log = { version = "0.4.26" } 17 | 18 | esp-alloc = "0.7.0" 19 | embedded-graphics = "0.8.1" 20 | embedded-hal = "1.0.0" 21 | mipidsi = "0.9.0" 22 | embedded-graphics-framebuf = "0.5.0" 23 | heapless = "0.8.0" 24 | embedded-hal-bus = "0.3.0" 25 | 26 | # M5Stack specific 27 | # Power Management IC 28 | axp2101 = { git = "https://github.com/georgik/axp2101-rs.git", tag = "v0.2.0" } 29 | # GPIO Expander 30 | aw9523 = { git = "https://github.com/georgik/aw9523-rs.git", tag = "v0.2.0" } 31 | 32 | bevy_ecs = { git = "https://github.com/bevyengine/bevy.git", rev = "301f618", default-features = false } 33 | 34 | 35 | [features] 36 | default = [ "esp-hal/esp32s3", "esp-backtrace/esp32s3", "esp-println/esp32s3", "esp32-s3-box-3" ] 37 | 38 | # esp32-s3-box-3 = [ "esp-bsp/esp32-s3-box-3", "esp-hal/psram" ] 39 | esp32-s3-box-3 = [ "esp-hal/psram" ] 40 | -------------------------------------------------------------------------------- /m5stack-cores3/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg=-Tlinkall.x"); 3 | } 4 | -------------------------------------------------------------------------------- /m5stack-cores3/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "esp" 3 | -------------------------------------------------------------------------------- /m5stack-cores3/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | extern crate alloc; 5 | use alloc::boxed::Box; 6 | use core::fmt::Write; 7 | use embedded_graphics::{ 8 | mono_font::{MonoTextStyle, ascii::FONT_8X13}, 9 | pixelcolor::Rgb565, 10 | prelude::*, 11 | primitives::{PrimitiveStyle, Rectangle}, 12 | text::Text, 13 | }; 14 | use embedded_graphics_framebuf::{FrameBuf, backends::FrameBufferBackend}; 15 | use embedded_hal::delay::DelayNs; 16 | use embedded_hal_bus::spi::ExclusiveDevice; 17 | use esp_hal::Blocking; 18 | use esp_hal::{ 19 | delay::Delay, 20 | dma::{DmaRxBuf, DmaTxBuf}, 21 | dma_buffers, 22 | gpio::{Level, Output, OutputConfig}, 23 | i2c::master::I2c, 24 | main, 25 | rng::Rng, 26 | spi::master::{Spi, SpiDmaBus}, 27 | time::Rate, 28 | }; 29 | use esp_println::{logger::init_logger_from_env, println}; 30 | use log::info; 31 | use mipidsi::{ 32 | Builder, 33 | interface::SpiInterface, 34 | options::{ColorInversion, ColorOrder}, 35 | }; 36 | 37 | use aw9523::I2CGpioExpanderInterface; 38 | use axp2101::{Axp2101, I2CPowerManagementInterface}; 39 | 40 | use bevy_ecs::prelude::*; 41 | use mipidsi::models::ILI9342CRgb565; 42 | // use shared_bus::BusManagerSimple; 43 | 44 | #[panic_handler] 45 | fn panic(_info: &core::panic::PanicInfo) -> ! { 46 | println!("Panic: {}", _info); 47 | loop {} 48 | } 49 | 50 | // --- FrameBuffer Backend Definition --- 51 | pub struct HeapBuffer(Box<[C; N]>); 52 | 53 | impl HeapBuffer { 54 | pub fn new(data: Box<[C; N]>) -> Self { 55 | Self(data) 56 | } 57 | } 58 | 59 | impl core::ops::Deref for HeapBuffer { 60 | type Target = [C; N]; 61 | fn deref(&self) -> &Self::Target { 62 | &*self.0 63 | } 64 | } 65 | 66 | impl core::ops::DerefMut for HeapBuffer { 67 | fn deref_mut(&mut self) -> &mut Self::Target { 68 | &mut *self.0 69 | } 70 | } 71 | 72 | impl FrameBufferBackend for HeapBuffer { 73 | type Color = C; 74 | fn set(&mut self, index: usize, color: Self::Color) { 75 | self.0[index] = color; 76 | } 77 | fn get(&self, index: usize) -> Self::Color { 78 | self.0[index] 79 | } 80 | fn nr_elements(&self) -> usize { 81 | N 82 | } 83 | } 84 | 85 | // --- Display and FrameBuffer Type Aliases --- 86 | type MyDisplay = mipidsi::Display< 87 | SpiInterface< 88 | 'static, 89 | ExclusiveDevice, Output<'static>, Delay>, 90 | Output<'static>, 91 | >, 92 | ILI9342CRgb565, 93 | Output<'static>, 94 | >; 95 | 96 | const LCD_H_RES: usize = 320; 97 | const LCD_V_RES: usize = 240; 98 | const LCD_BUFFER_SIZE: usize = LCD_H_RES * LCD_V_RES; 99 | type FbBuffer = HeapBuffer; 100 | type MyFrameBuf = FrameBuf; 101 | 102 | #[derive(Resource)] 103 | struct FrameBufferResource { 104 | frame_buf: MyFrameBuf, 105 | } 106 | 107 | impl FrameBufferResource { 108 | fn new() -> Self { 109 | let fb_data: Box<[Rgb565; LCD_BUFFER_SIZE]> = Box::new([Rgb565::BLACK; LCD_BUFFER_SIZE]); 110 | let heap_buffer = HeapBuffer::new(fb_data); 111 | let frame_buf = MyFrameBuf::new(heap_buffer, LCD_H_RES, LCD_V_RES); 112 | Self { frame_buf } 113 | } 114 | } 115 | 116 | // --- ECS Resources and Systems --- 117 | 118 | #[derive(Resource)] 119 | struct GameOfLifeResource { 120 | grid: [[u8; GRID_WIDTH]; GRID_HEIGHT], 121 | generation: usize, 122 | } 123 | 124 | impl Default for GameOfLifeResource { 125 | fn default() -> Self { 126 | Self { 127 | grid: [[0; GRID_WIDTH]; GRID_HEIGHT], 128 | generation: 0, 129 | } 130 | } 131 | } 132 | 133 | #[derive(Resource)] 134 | struct RngResource(Rng); 135 | 136 | /// A wrapper for the display as a non-send resource. 137 | struct DisplayResource { 138 | display: MyDisplay, 139 | } 140 | 141 | fn update_game_of_life_system( 142 | mut game: ResMut, 143 | mut rng_res: ResMut, 144 | ) { 145 | update_game_of_life(&mut game.grid); 146 | game.generation += 1; 147 | if game.generation >= RESET_AFTER_GENERATIONS { 148 | randomize_grid(&mut rng_res.0, &mut game.grid); 149 | game.generation = 0; 150 | } 151 | } 152 | 153 | // --- Game of Life Definitions --- 154 | // Now each cell is a u8 (0 means dead; >0 indicates age) 155 | const GRID_WIDTH: usize = 64; 156 | const GRID_HEIGHT: usize = 48; 157 | const RESET_AFTER_GENERATIONS: usize = 500; 158 | 159 | fn randomize_grid(rng: &mut Rng, grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 160 | for row in grid.iter_mut() { 161 | for cell in row.iter_mut() { 162 | let mut buf = [0u8; 1]; 163 | rng.read(&mut buf); 164 | // Randomly set cell to 1 (alive) or 0 (dead) 165 | *cell = if buf[0] & 1 != 0 { 1 } else { 0 }; 166 | } 167 | } 168 | } 169 | 170 | fn update_game_of_life(grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 171 | let mut new_grid = [[0u8; GRID_WIDTH]; GRID_HEIGHT]; 172 | for y in 0..GRID_HEIGHT { 173 | for x in 0..GRID_WIDTH { 174 | // Count neighbors: consider a cell alive if its age is >0. 175 | let mut alive_neighbors = 0; 176 | for i in 0..3 { 177 | for j in 0..3 { 178 | if i == 1 && j == 1 { 179 | continue; 180 | } 181 | let nx = (x + i + GRID_WIDTH - 1) % GRID_WIDTH; 182 | let ny = (y + j + GRID_HEIGHT - 1) % GRID_HEIGHT; 183 | if grid[ny][nx] > 0 { 184 | alive_neighbors += 1; 185 | } 186 | } 187 | } 188 | if grid[y][x] > 0 { 189 | // Live cell survives if 2 or 3 neighbors; increment age. 190 | if alive_neighbors == 2 || alive_neighbors == 3 { 191 | new_grid[y][x] = grid[y][x].saturating_add(1); 192 | } else { 193 | new_grid[y][x] = 0; 194 | } 195 | } else { 196 | // Dead cell becomes alive if exactly 3 neighbors. 197 | if alive_neighbors == 3 { 198 | new_grid[y][x] = 1; 199 | } else { 200 | new_grid[y][x] = 0; 201 | } 202 | } 203 | } 204 | } 205 | *grid = new_grid; 206 | } 207 | 208 | /// Maps cell age (1...=max_age) to a color. Newborn cells are dark blue and older cells become brighter (toward white). 209 | fn age_to_color(age: u8) -> Rgb565 { 210 | if age == 0 { 211 | Rgb565::BLACK 212 | } else { 213 | let max_age = 10; 214 | let a = age.min(max_age) as u32; // clamp age and use u32 for intermediate math 215 | let r = ((31 * a) + 5) / max_age as u32; 216 | let g = ((63 * a) + 5) / max_age as u32; 217 | let b = 31; // Keep blue channel constant 218 | // Convert back to u8 and return the color. 219 | Rgb565::new(r as u8, g as u8, b) 220 | } 221 | } 222 | 223 | /// Draws the game grid using the cell age for color. 224 | fn draw_grid>( 225 | display: &mut D, 226 | grid: &[[u8; GRID_WIDTH]; GRID_HEIGHT], 227 | ) -> Result<(), D::Error> { 228 | let border_color = Rgb565::new(230, 230, 230); 229 | for (y, row) in grid.iter().enumerate() { 230 | for (x, &age) in row.iter().enumerate() { 231 | let point = Point::new(x as i32 * 7, y as i32 * 7); 232 | if age > 0 { 233 | // Draw a border then fill with color based on age. 234 | Rectangle::new(point, Size::new(7, 7)) 235 | .into_styled(PrimitiveStyle::with_fill(border_color)) 236 | .draw(display)?; 237 | // Draw an inner cell with color according to age. 238 | Rectangle::new(point + Point::new(1, 1), Size::new(5, 5)) 239 | .into_styled(PrimitiveStyle::with_fill(age_to_color(age))) 240 | .draw(display)?; 241 | } else { 242 | // Draw a dead cell as black. 243 | Rectangle::new(point, Size::new(7, 7)) 244 | .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) 245 | .draw(display)?; 246 | } 247 | } 248 | } 249 | Ok(()) 250 | } 251 | 252 | fn write_generation>( 253 | display: &mut D, 254 | generation: usize, 255 | ) -> Result<(), D::Error> { 256 | let mut num_str = heapless::String::<20>::new(); 257 | write!(num_str, "{}", generation).unwrap(); 258 | Text::new( 259 | num_str.as_str(), 260 | Point::new(8, 13), 261 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 262 | ) 263 | .draw(display)?; 264 | Ok(()) 265 | } 266 | 267 | /// Render the game state by drawing into the offscreen framebuffer and then flushing 268 | /// it to the display via DMA. After drawing the game grid and generation number, 269 | /// we overlay centered text. 270 | fn render_system( 271 | mut display_res: NonSendMut, 272 | game: Res, 273 | mut fb_res: ResMut, 274 | ) { 275 | // Clear the framebuffer. 276 | fb_res.frame_buf.clear(Rgb565::BLACK).unwrap(); 277 | // Draw the game grid (using the age-based color) and generation number. 278 | draw_grid(&mut fb_res.frame_buf, &game.grid).unwrap(); 279 | write_generation(&mut fb_res.frame_buf, game.generation).unwrap(); 280 | 281 | // --- Overlay centered text --- 282 | let line1 = "Rust no_std M5Stack-CoreS3"; 283 | let line2 = "Bevy ECS 0.16 no_std"; 284 | // Estimate text width: assume ~8 pixels per character. 285 | let line1_width = line1.len() as i32 * 8; 286 | let line2_width = line2.len() as i32 * 8; 287 | let x1 = (LCD_H_RES as i32 - line1_width) / 2 + 14; 288 | let x2 = (LCD_H_RES as i32 - line2_width) / 2 + 14; 289 | // For vertical centering, assume 26 pixels total text height. 290 | let y = (LCD_V_RES as i32 - 26) / 2; 291 | Text::new( 292 | line1, 293 | Point::new(x1, y), 294 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 295 | ) 296 | .draw(&mut fb_res.frame_buf) 297 | .unwrap(); 298 | Text::new( 299 | line2, 300 | Point::new(x2, y + 14), 301 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 302 | ) 303 | .draw(&mut fb_res.frame_buf) 304 | .unwrap(); 305 | 306 | // Define the area covering the entire framebuffer. 307 | let area = Rectangle::new(Point::zero(), fb_res.frame_buf.size()); 308 | // Flush the framebuffer to the physical display. 309 | display_res 310 | .display 311 | .fill_contiguous(&area, fb_res.frame_buf.data.iter().copied()) 312 | .unwrap(); 313 | } 314 | 315 | // --- Main Application --- 316 | #[main] 317 | fn main() -> ! { 318 | let peripherals = esp_hal::init(esp_hal::Config::default()); 319 | 320 | // PSRAM allocator for heap memory. 321 | esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram); 322 | 323 | init_logger_from_env(); 324 | 325 | // --- DMA Buffers for SPI --- 326 | let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(8912); 327 | let dma_rx_buf = DmaRxBuf::new(rx_descriptors, rx_buffer).unwrap(); 328 | let dma_tx_buf = DmaTxBuf::new(tx_descriptors, tx_buffer).unwrap(); 329 | 330 | // Create the I2C instance 331 | let i2c_bus = I2c::new(peripherals.I2C0, esp_hal::i2c::master::Config::default()) 332 | .unwrap() 333 | .with_sda(peripherals.GPIO12) 334 | .with_scl(peripherals.GPIO11); 335 | 336 | // let bus = BusManagerSimple::new(i2c_bus); 337 | 338 | info!("Initializing AXP2101"); 339 | // Initialize AXP2101 directly 340 | let axp_interface = I2CPowerManagementInterface::new(i2c_bus); 341 | let mut axp = Axp2101::new(axp_interface); 342 | axp.init().unwrap(); 343 | 344 | info!("Initializing GPIO Expander"); 345 | // Get the I2C interface back by consuming the AXP2101 346 | let i2c_bus = axp.release_i2c(); 347 | 348 | // Initialize AW9523 with the I2C interface 349 | let aw_interface = I2CGpioExpanderInterface::new(i2c_bus); 350 | let mut aw = aw9523::Aw9523::new(aw_interface); 351 | aw.init().unwrap(); 352 | 353 | // --- Display Setup --- 354 | let spi = Spi::::new( 355 | peripherals.SPI2, 356 | esp_hal::spi::master::Config::default() 357 | .with_frequency(Rate::from_mhz(40)) 358 | .with_mode(esp_hal::spi::Mode::_0), 359 | ) 360 | .unwrap() 361 | .with_sck(peripherals.GPIO36) 362 | .with_mosi(peripherals.GPIO37) 363 | .with_dma(peripherals.DMA_CH0) 364 | .with_buffers(dma_rx_buf, dma_tx_buf); 365 | let cs_output = Output::new(peripherals.GPIO3, Level::High, OutputConfig::default()); 366 | let spi_delay = Delay::new(); 367 | let spi_device = ExclusiveDevice::new(spi, cs_output, spi_delay).unwrap(); 368 | 369 | // LCD interface: DC = GPIO4. 370 | let lcd_dc = Output::new(peripherals.GPIO35, Level::Low, OutputConfig::default()); 371 | // Leak a Box to obtain a 'static mutable buffer. 372 | let buffer: &'static mut [u8; 512] = Box::leak(Box::new([0_u8; 512])); 373 | let di = SpiInterface::new(spi_device, lcd_dc, buffer); 374 | 375 | let mut display_delay = Delay::new(); 376 | display_delay.delay_ns(500_000u32); 377 | 378 | // Reset pin: Use open-drain (required for ESP32-S3-BOX). 379 | let reset = Output::new(peripherals.GPIO15, Level::High, OutputConfig::default()); 380 | let mut display: MyDisplay = Builder::new(ILI9342CRgb565, di) 381 | .reset_pin(reset) 382 | .display_size(320, 240) 383 | .color_order(ColorOrder::Bgr) 384 | .invert_colors(ColorInversion::Inverted) 385 | .init(&mut display_delay) 386 | .unwrap(); 387 | 388 | display.clear(Rgb565::BLUE).unwrap(); 389 | 390 | info!("Display initialized"); 391 | 392 | // --- Initialize Game Resources --- 393 | let mut game = GameOfLifeResource::default(); 394 | let mut rng_instance = Rng::new(peripherals.RNG); 395 | randomize_grid(&mut rng_instance, &mut game.grid); 396 | let glider = [(1, 0), (2, 1), (0, 2), (1, 2), (2, 2)]; 397 | for (x, y) in glider.iter() { 398 | game.grid[*y][*x] = 1; // alive with age 1 399 | } 400 | 401 | // Create the framebuffer resource. 402 | let fb_res = FrameBufferResource::new(); 403 | 404 | let mut world = World::default(); 405 | world.insert_resource(game); 406 | world.insert_resource(RngResource(rng_instance)); 407 | // Insert the display as a non-send resource because its DMA pointers are not Sync. 408 | world.insert_non_send_resource(DisplayResource { display }); 409 | // Insert the framebuffer resource as a normal resource. 410 | world.insert_resource(fb_res); 411 | 412 | let mut schedule = Schedule::default(); 413 | schedule.add_systems(update_game_of_life_system); 414 | schedule.add_systems(render_system); 415 | 416 | let mut loop_delay = Delay::new(); 417 | 418 | loop { 419 | schedule.run(&mut world); 420 | loop_delay.delay_ms(50u32); 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /scripts/fix-code.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # This script formats the code using cargo fmt, runs cargo clippy with auto-fix, 5 | # and if any files were modified, stages and commits them. 6 | 7 | echo "Running cargo fmt to format code..." 8 | cargo fmt --all 9 | 10 | echo "Running cargo clippy with auto-fix (requires nightly)..." 11 | cargo clippy --all-features --workspace --fix --allow-dirty 12 | 13 | echo "Running tests..." 14 | cargo test --all 15 | 16 | -------------------------------------------------------------------------------- /wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "conways-wasm" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | wasm-bindgen = "0.2.100" 11 | web-sys = { version = "0.3", features = ["Window", "Document", "HtmlCanvasElement", "CanvasRenderingContext2d", "ImageData", "Performance"] } 12 | console_error_panic_hook = "0.1" 13 | log = "0.4" 14 | embedded-graphics = "0.8.1" 15 | embedded-graphics-framebuf = "0.5.0" 16 | embedded-graphics-web-simulator = "0.4.0" 17 | bevy_ecs = { git = "https://github.com/bevyengine/bevy.git", rev = "06cb5c5", default-features = false } 18 | heapless = "0.8.0" 19 | console = "0.15.11" 20 | getrandom = { version = "0.2.8", features = ["js"] } 21 | rand_chacha = { version = "0.3.1", default-features = false } 22 | rand_core = "0.6.4" 23 | 24 | #[package.metadata.wasm-pack] 25 | #wasm-opt = false 26 | #[package.metadata.wasm-pack.profile.release] 27 | #wasm-opt = false -------------------------------------------------------------------------------- /wasm/pkg/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Conway's Game of Life - WASM 6 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | extern crate alloc; 5 | use alloc::boxed::Box; 6 | use alloc::rc::Rc; 7 | use alloc::string::{String, ToString}; 8 | use alloc::vec::Vec; 9 | use core::cell::RefCell; 10 | use core::fmt::Write; 11 | 12 | use alloc::format; 13 | use bevy_ecs::prelude::*; // includes Resource, NonSendMut, etc. 14 | use embedded_graphics::{ 15 | Drawable, 16 | mono_font::{MonoTextStyle, ascii::FONT_8X13}, 17 | pixelcolor::Rgb565, 18 | prelude::*, 19 | primitives::{PrimitiveStyle, Rectangle}, 20 | text::Text, 21 | }; 22 | use embedded_graphics_framebuf::FrameBuf; 23 | use getrandom; 24 | use log::info; 25 | use wasm_bindgen::JsCast; 26 | use wasm_bindgen::prelude::*; 27 | use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData, window}; 28 | 29 | // === LCD & Framebuffer settings === 30 | const LCD_H_RES: usize = 320; 31 | const LCD_V_RES: usize = 240; 32 | const LCD_BUFFER_SIZE: usize = LCD_H_RES * LCD_V_RES; 33 | 34 | // We store pixels as Rgb565. 35 | pub type FbBuffer = [Rgb565; LCD_BUFFER_SIZE]; 36 | // Type alias for our framebuffer. 37 | pub type MyFrameBuf = FrameBuf; 38 | 39 | // === FrameBuffer Resource === 40 | #[derive(Resource)] 41 | struct FrameBufferResource { 42 | frame_buf: MyFrameBuf, 43 | } 44 | 45 | impl FrameBufferResource { 46 | fn new() -> Self { 47 | // Allocate framebuffer data on the heap. 48 | let fb_data: FbBuffer = *Box::new([Rgb565::BLACK; LCD_BUFFER_SIZE]); 49 | let frame_buf = MyFrameBuf::new(fb_data, LCD_H_RES, LCD_V_RES); 50 | Self { frame_buf } 51 | } 52 | } 53 | 54 | // === Game of Life Definitions === 55 | // Our simulation grid is 64x48; each cell is a u8 (0 = dead, >0 = alive/age). 56 | const GRID_WIDTH: usize = 64; 57 | const GRID_HEIGHT: usize = 48; 58 | const RESET_AFTER_GENERATIONS: usize = 500; 59 | 60 | #[derive(Resource)] 61 | struct GameOfLifeResource { 62 | grid: [[u8; GRID_WIDTH]; GRID_HEIGHT], 63 | generation: usize, 64 | } 65 | 66 | impl Default for GameOfLifeResource { 67 | fn default() -> Self { 68 | // Create an empty grid. 69 | let mut grid = [[0u8; GRID_WIDTH]; GRID_HEIGHT]; 70 | // Seed the RNG using your helper. 71 | let mut rng = ChaCha8Rng::seed_from_u64(get_seed() as u64); 72 | // Fill the grid with random values. 73 | randomize_grid(&mut rng, &mut grid); 74 | Self { 75 | grid, 76 | generation: 0, 77 | } 78 | } 79 | } 80 | 81 | // Helper function that returns a u32 seed using getrandom. 82 | fn get_seed() -> u32 { 83 | let mut buf = [0u8; 4]; 84 | getrandom::getrandom(&mut buf).expect("failed to get random seed"); 85 | u32::from_le_bytes(buf) 86 | } 87 | 88 | use rand_chacha::ChaCha8Rng; 89 | use rand_core::{RngCore, SeedableRng}; 90 | #[derive(Resource)] 91 | struct RngResource(ChaCha8Rng); 92 | 93 | impl Default for RngResource { 94 | fn default() -> Self { 95 | // Get an 8-byte seed from the browser. 96 | let mut seed_buf = [0u8; 8]; 97 | getrandom::getrandom(&mut seed_buf).expect("failed to get random seed"); 98 | let seed = u64::from_le_bytes(seed_buf); 99 | Self(ChaCha8Rng::seed_from_u64(seed)) 100 | } 101 | } 102 | // === Display Resource for WASM === 103 | // For the WASM version, our display is simulated by updating an HTML canvas. 104 | struct DisplayResource { 105 | ctx: CanvasRenderingContext2d, 106 | } 107 | 108 | impl DisplayResource { 109 | fn new(ctx: CanvasRenderingContext2d) -> Self { 110 | Self { ctx } 111 | } 112 | } 113 | 114 | // === Simulation Functions === 115 | 116 | fn randomize_grid(rng: &mut impl RngCore, grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 117 | for row in grid.iter_mut() { 118 | for cell in row.iter_mut() { 119 | // Use next_u32() to generate a random number. 120 | *cell = if (rng.next_u32() & 1) != 0 { 1 } else { 0 }; 121 | } 122 | } 123 | } 124 | 125 | fn update_game_of_life(grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 126 | let mut new_grid = [[0u8; GRID_WIDTH]; GRID_HEIGHT]; 127 | for y in 0..GRID_HEIGHT { 128 | for x in 0..GRID_WIDTH { 129 | let mut alive_neighbors = 0; 130 | for i in 0..3 { 131 | for j in 0..3 { 132 | if i == 1 && j == 1 { 133 | continue; 134 | } 135 | let nx = (x + i + GRID_WIDTH - 1) % GRID_WIDTH; 136 | let ny = (y + j + GRID_HEIGHT - 1) % GRID_HEIGHT; 137 | if grid[ny][nx] > 0 { 138 | alive_neighbors += 1; 139 | } 140 | } 141 | } 142 | if grid[y][x] > 0 { 143 | if alive_neighbors == 2 || alive_neighbors == 3 { 144 | new_grid[y][x] = grid[y][x].saturating_add(1); 145 | } else { 146 | new_grid[y][x] = 0; 147 | } 148 | } else if alive_neighbors == 3 { 149 | new_grid[y][x] = 1; 150 | } 151 | } 152 | } 153 | *grid = new_grid; 154 | } 155 | 156 | /// Maps cell age to a color. Young cells are dark blue and older cells brighten. 157 | fn age_to_color(age: u8) -> Rgb565 { 158 | if age == 0 { 159 | Rgb565::BLACK 160 | } else { 161 | let max_age = 10; 162 | let a = age.min(max_age) as u32; 163 | let r = ((31 * a) + 5) / (max_age as u32); 164 | let g = ((63 * a) + 5) / (max_age as u32); 165 | let b = 31; 166 | Rgb565::new(r as u8, g as u8, b) 167 | } 168 | } 169 | 170 | fn draw_grid>( 171 | display: &mut D, 172 | grid: &[[u8; GRID_WIDTH]; GRID_HEIGHT], 173 | ) -> Result<(), D::Error> { 174 | let border_color = Rgb565::new(230, 230, 230); 175 | for (y, row) in grid.iter().enumerate() { 176 | for (x, &age) in row.iter().enumerate() { 177 | let point = Point::new(x as i32 * 7, y as i32 * 7); 178 | if age > 0 { 179 | Rectangle::new(point, Size::new(7, 7)) 180 | .into_styled(PrimitiveStyle::with_fill(border_color)) 181 | .draw(display)?; 182 | Rectangle::new(point + Point::new(1, 1), Size::new(5, 5)) 183 | .into_styled(PrimitiveStyle::with_fill(age_to_color(age))) 184 | .draw(display)?; 185 | } else { 186 | Rectangle::new(point, Size::new(7, 7)) 187 | .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) 188 | .draw(display)?; 189 | } 190 | } 191 | } 192 | Ok(()) 193 | } 194 | 195 | fn write_generation>( 196 | display: &mut D, 197 | generation: usize, 198 | ) -> Result<(), D::Error> { 199 | let mut num_str = String::new(); 200 | write!(&mut num_str, "{}", generation).unwrap(); 201 | Text::new( 202 | &num_str, 203 | Point::new(8, 13), 204 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 205 | ) 206 | .draw(display)?; 207 | Ok(()) 208 | } 209 | 210 | // === ECS Systems === 211 | fn update_game_of_life_system( 212 | mut game: ResMut, 213 | mut rng_res: ResMut, 214 | ) { 215 | update_game_of_life(&mut game.grid); 216 | game.generation += 1; 217 | if game.generation >= RESET_AFTER_GENERATIONS { 218 | randomize_grid(&mut rng_res.0, &mut game.grid); 219 | game.generation = 0; 220 | } 221 | } 222 | 223 | fn render_system( 224 | mut display_res: NonSendMut, 225 | game: Res, 226 | mut fb_res: ResMut, 227 | ) { 228 | fb_res.frame_buf.clear(Rgb565::BLACK).unwrap(); 229 | draw_grid(&mut fb_res.frame_buf, &game.grid).unwrap(); 230 | write_generation(&mut fb_res.frame_buf, game.generation).unwrap(); 231 | 232 | // Overlay centered text. 233 | let line1 = "Rust no_std WASM"; 234 | let line2 = "Bevy ECS 0.15"; 235 | let line1_width = line1.len() as i32 * 8; 236 | let line2_width = line2.len() as i32 * 8; 237 | let x1 = (LCD_H_RES as i32 - line1_width) / 2; 238 | let x2 = (LCD_H_RES as i32 - line2_width) / 2; 239 | let y = (LCD_V_RES as i32 - 26) / 2; 240 | Text::new( 241 | line1, 242 | Point::new(x1, y), 243 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 244 | ) 245 | .draw(&mut fb_res.frame_buf) 246 | .unwrap(); 247 | Text::new( 248 | line2, 249 | Point::new(x2, y + 14), 250 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 251 | ) 252 | .draw(&mut fb_res.frame_buf) 253 | .unwrap(); 254 | 255 | // Flush the framebuffer to the canvas. 256 | update_canvas(&display_res.ctx, &fb_res.frame_buf); 257 | } 258 | 259 | // === WASM Helper: Update the HTML canvas from the framebuffer === 260 | fn update_canvas(ctx: &CanvasRenderingContext2d, fb: &MyFrameBuf) { 261 | let mut out = Vec::with_capacity(LCD_H_RES * LCD_V_RES * 4); 262 | for pixel in fb.data.iter().copied() { 263 | let raw = pixel.into_storage(); 264 | let r = ((raw >> 11) & 0x1F) * 255 / 31; 265 | let g = ((raw >> 5) & 0x3F) * 255 / 63; 266 | let b = (raw & 0x1F) * 255 / 31; 267 | out.push(r as u8); 268 | out.push(g as u8); 269 | out.push(b as u8); 270 | out.push(255); 271 | } 272 | let clamped = wasm_bindgen::Clamped(&out[..]); 273 | let image_data = 274 | ImageData::new_with_u8_clamped_array_and_sh(clamped, LCD_H_RES as u32, LCD_V_RES as u32) 275 | .unwrap(); 276 | ctx.put_image_data(&image_data, 0.0, 0.0).unwrap(); 277 | } 278 | 279 | #[wasm_bindgen(start)] 280 | pub fn start() -> Result<(), JsValue> { 281 | console_error_panic_hook::set_once(); 282 | info!("Starting WASM Conway’s Game of Life"); 283 | 284 | // Set up the canvas. 285 | let document = window().unwrap().document().unwrap(); 286 | let canvas = document.create_element("canvas")?; 287 | canvas.set_attribute("width", &LCD_H_RES.to_string())?; 288 | canvas.set_attribute("height", &LCD_V_RES.to_string())?; 289 | document.body().unwrap().append_child(&canvas)?; 290 | let canvas: HtmlCanvasElement = canvas.dyn_into()?; 291 | let ctx = canvas 292 | .get_context("2d")? 293 | .unwrap() 294 | .dyn_into::()?; 295 | 296 | // Create ECS world and insert resources. 297 | let mut world = World::default(); 298 | world.insert_resource(GameOfLifeResource::default()); 299 | world.insert_resource(RngResource::default()); 300 | world.insert_resource(FrameBufferResource::new()); 301 | // Our DisplayResource is non-send because it contains non-Sync data. 302 | world.insert_non_send_resource(DisplayResource::new(ctx)); 303 | 304 | let mut schedule = Schedule::default(); 305 | schedule.add_systems(update_game_of_life_system); 306 | schedule.add_systems(render_system); 307 | 308 | // --- Frame Rate Limiter Setup --- 309 | let frame_interval_ms = 100.0; 310 | let last_frame = Rc::new(RefCell::new(0.0)); 311 | 312 | // Create an Rc>>> to store the animation loop closure. 313 | let f: Rc>>> = Rc::new(RefCell::new(None)); 314 | // Clone the Rc so we can move the clone into the closure. 315 | let f_clone = f.clone(); 316 | 317 | let win = window().unwrap(); 318 | let win_clone = win.clone(); 319 | 320 | // Set up our animation closure. 321 | *f.borrow_mut() = Some(Closure::wrap(Box::new({ 322 | let last_frame = last_frame.clone(); 323 | move || { 324 | // Get the current time in milliseconds. 325 | let now = win_clone.performance().unwrap().now(); 326 | { 327 | let mut last = last_frame.borrow_mut(); 328 | // Only update if enough time has passed. 329 | if now - *last >= frame_interval_ms { 330 | schedule.run(&mut world); 331 | *last = now; 332 | } 333 | } 334 | // Schedule the next frame using the clone. 335 | win_clone 336 | .request_animation_frame( 337 | f_clone.borrow().as_ref().unwrap().as_ref().unchecked_ref(), 338 | ) 339 | .unwrap(); 340 | } 341 | }) as Box)); 342 | 343 | // Kick off the animation loop. 344 | win.request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref())?; 345 | Ok(()) 346 | } 347 | -------------------------------------------------------------------------------- /waveshare-esp32-c6-lcd-1_47/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.xtensa-esp32s3-none-elf] 2 | runner = "espflash flash --monitor" 3 | 4 | [target.riscv32imac-unknown-none-elf] 5 | runner = "espflash flash --monitor --chip esp32c6" 6 | 7 | [env] 8 | ESP_LOG="INFO" 9 | # ESP_HAL_CONFIG_PSRAM_MODE = "octal" 10 | 11 | [build] 12 | rustflags = [ 13 | "-C", "force-frame-pointers", 14 | ] 15 | 16 | target = "riscv32imac-unknown-none-elf" 17 | 18 | [unstable] 19 | build-std = ["alloc", "core"] 20 | -------------------------------------------------------------------------------- /waveshare-esp32-c6-lcd-1_47/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esp32-conways-game-of-life-rs" 3 | version = "0.5.0" 4 | authors = ["Juraj Michálek "] 5 | edition = "2024" 6 | license = "MIT OR Apache-2.0" 7 | 8 | 9 | [dependencies] 10 | esp-hal = { version = "1.0.0-beta.0", features = ["esp32c6", "unstable"] } 11 | esp-backtrace = { version = "0.15.1", features = [ 12 | "panic-handler", 13 | "println" 14 | ] } 15 | esp-println = { version = "0.13", features = [ "log" ] } 16 | log = { version = "0.4.26" } 17 | 18 | esp-alloc = "0.7.0" 19 | embedded-graphics = "0.8.1" 20 | embedded-hal = "1.0.0" 21 | mipidsi = "0.9.0" 22 | #esp-display-interface-spi-dma = "0.3.0" 23 | # esp-display-interface-spi-dma = { path = "../esp-display-interface-spi-dma"} 24 | esp-bsp = "0.4.1" 25 | # embedded-graphics-framebuf = { version = "0.3.0", git = "https://github.com/georgik/embedded-graphics-framebuf.git", branch = "feature/embedded-graphics-0.8" } 26 | embedded-graphics-framebuf = "0.5.0" 27 | heapless = "0.8.0" 28 | embedded-hal-bus = "0.3.0" 29 | bevy_ecs = { git = "https://github.com/bevyengine/bevy.git", rev = "06cb5c5", default-features = false } 30 | 31 | 32 | [features] 33 | # default = [ "esp-hal/esp32s3", "esp-backtrace/esp32s3", "esp-println/esp32s3", "esp32-s3-box-3" ] 34 | default = [ "esp-hal/esp32c6", "esp-backtrace/esp32c6", "esp-println/esp32c6" ] 35 | 36 | # esp32-s3-box-3 = [ "esp-bsp/esp32-s3-box-3", "esp-hal/psram" ] -------------------------------------------------------------------------------- /waveshare-esp32-c6-lcd-1_47/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg=-Tlinkall.x"); 3 | } 4 | -------------------------------------------------------------------------------- /waveshare-esp32-c6-lcd-1_47/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", "lcd1:GND", "black", [] ], 27 | [ "esp:7", "lcd1:SCK", "blue", [] ], 28 | [ "esp:6", "lcd1:MOSI", "orange", [] ], 29 | [ "esp:14", "lcd1:CS", "red", [] ], 30 | [ "esp:15", "lcd1:D/C", "magenta", [] ], 31 | [ "esp:21", "lcd1:RST", "yellow", [] ], 32 | [ "esp:22", "lcd1:LED", "white", [] ] 33 | ], 34 | "serialMonitor": { "display": "terminal" }, 35 | "dependencies": {} 36 | } 37 | -------------------------------------------------------------------------------- /waveshare-esp32-c6-lcd-1_47/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["rust-src"] 4 | targets = ["riscv32imac-unknown-none-elf"] 5 | -------------------------------------------------------------------------------- /waveshare-esp32-c6-lcd-1_47/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | extern crate alloc; 5 | use alloc::boxed::Box; 6 | 7 | use bevy_ecs::prelude::*; 8 | use core::fmt::Write; 9 | use embedded_graphics::{ 10 | Drawable, 11 | mono_font::{MonoTextStyle, ascii::FONT_8X13}, 12 | pixelcolor::Rgb565, 13 | prelude::*, 14 | primitives::{PrimitiveStyle, Rectangle}, 15 | text::Text, 16 | }; 17 | use embedded_graphics_framebuf::FrameBuf; 18 | use embedded_hal::delay::DelayNs; 19 | use embedded_hal_bus::spi::ExclusiveDevice; 20 | use esp_hal::delay::Delay; 21 | use esp_hal::dma::{DmaRxBuf, DmaTxBuf}; 22 | use esp_hal::dma_buffers; 23 | use esp_hal::{ 24 | Blocking, 25 | gpio::{Level, Output, OutputConfig}, 26 | main, 27 | rng::Rng, 28 | spi::master::{Spi, SpiDmaBus}, 29 | time::Rate, 30 | }; 31 | use esp_println::{logger::init_logger_from_env, println}; 32 | use log::info; 33 | use mipidsi::{Builder, models::ST7789}; 34 | use mipidsi::{interface::SpiInterface, options::ColorInversion}; // includes NonSend and NonSendMut 35 | 36 | #[panic_handler] 37 | fn panic(_info: &core::panic::PanicInfo) -> ! { 38 | println!("Panic: {}", _info); 39 | loop {} 40 | } 41 | 42 | // --- Type Alias for the Concrete Display --- 43 | // Use the DMA-enabled SPI bus type. 44 | type MyDisplay = mipidsi::Display< 45 | SpiInterface< 46 | 'static, 47 | ExclusiveDevice, Output<'static>, Delay>, 48 | Output<'static>, 49 | >, 50 | ST7789, 51 | Output<'static>, 52 | >; 53 | 54 | // --- LCD Resolution and FrameBuffer Type Aliases --- 55 | const LCD_H_RES: usize = 206; 56 | const LCD_V_RES: usize = 320; 57 | const LCD_BUFFER_SIZE: usize = LCD_H_RES * LCD_V_RES; 58 | 59 | use embedded_graphics::pixelcolor::PixelColor; 60 | use embedded_graphics_framebuf::backends::FrameBufferBackend; 61 | 62 | /// A wrapper around a boxed array that implements FrameBufferBackend. 63 | /// This allows the framebuffer to be allocated on the heap. 64 | pub struct HeapBuffer(Box<[C; N]>); 65 | 66 | impl HeapBuffer { 67 | pub fn new(data: Box<[C; N]>) -> Self { 68 | Self(data) 69 | } 70 | } 71 | 72 | impl core::ops::Deref for HeapBuffer { 73 | type Target = [C; N]; 74 | fn deref(&self) -> &Self::Target { 75 | &*self.0 76 | } 77 | } 78 | 79 | impl core::ops::DerefMut for HeapBuffer { 80 | fn deref_mut(&mut self) -> &mut Self::Target { 81 | &mut *self.0 82 | } 83 | } 84 | 85 | impl FrameBufferBackend for HeapBuffer { 86 | type Color = C; 87 | fn set(&mut self, index: usize, color: Self::Color) { 88 | self.0[index] = color; 89 | } 90 | fn get(&self, index: usize) -> Self::Color { 91 | self.0[index] 92 | } 93 | fn nr_elements(&self) -> usize { 94 | N 95 | } 96 | } 97 | 98 | 99 | // We want our pixels stored as Rgb565. 100 | type FbBuffer = HeapBuffer; 101 | // Define a type alias for the complete FrameBuf. 102 | type MyFrameBuf = FrameBuf; 103 | 104 | #[derive(Resource)] 105 | struct FrameBufferResource { 106 | frame_buf: MyFrameBuf, 107 | } 108 | 109 | impl FrameBufferResource { 110 | fn new() -> Self { 111 | // Allocate the framebuffer data on the heap. 112 | let fb_data: Box<[Rgb565; LCD_BUFFER_SIZE]> = Box::new([Rgb565::BLACK; LCD_BUFFER_SIZE]); 113 | let heap_buffer = HeapBuffer::new(fb_data); 114 | let frame_buf = MyFrameBuf::new(heap_buffer, LCD_H_RES, LCD_V_RES); 115 | Self { frame_buf } 116 | } 117 | } 118 | 119 | // --- Game of Life Definitions --- 120 | // Now each cell is a u8 (0 means dead; >0 indicates age) 121 | const GRID_WIDTH: usize = 64; 122 | const GRID_HEIGHT: usize = 48; 123 | const RESET_AFTER_GENERATIONS: usize = 500; 124 | 125 | fn randomize_grid(rng: &mut Rng, grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 126 | for row in grid.iter_mut() { 127 | for cell in row.iter_mut() { 128 | let mut buf = [0u8; 1]; 129 | rng.read(&mut buf); 130 | // Randomly set cell to 1 (alive) or 0 (dead) 131 | *cell = if buf[0] & 1 != 0 { 1 } else { 0 }; 132 | } 133 | } 134 | } 135 | 136 | fn update_game_of_life(grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 137 | let mut new_grid = [[0u8; GRID_WIDTH]; GRID_HEIGHT]; 138 | for y in 0..GRID_HEIGHT { 139 | for x in 0..GRID_WIDTH { 140 | // Count neighbors: consider a cell alive if its age is >0. 141 | let mut alive_neighbors = 0; 142 | for i in 0..3 { 143 | for j in 0..3 { 144 | if i == 1 && j == 1 { 145 | continue; 146 | } 147 | let nx = (x + i + GRID_WIDTH - 1) % GRID_WIDTH; 148 | let ny = (y + j + GRID_HEIGHT - 1) % GRID_HEIGHT; 149 | if grid[ny][nx] > 0 { 150 | alive_neighbors += 1; 151 | } 152 | } 153 | } 154 | if grid[y][x] > 0 { 155 | // Live cell survives if 2 or 3 neighbors; increment age. 156 | if alive_neighbors == 2 || alive_neighbors == 3 { 157 | new_grid[y][x] = grid[y][x].saturating_add(1); 158 | } else { 159 | new_grid[y][x] = 0; 160 | } 161 | } else { 162 | // Dead cell becomes alive if exactly 3 neighbors. 163 | if alive_neighbors == 3 { 164 | new_grid[y][x] = 1; 165 | } else { 166 | new_grid[y][x] = 0; 167 | } 168 | } 169 | } 170 | } 171 | *grid = new_grid; 172 | } 173 | 174 | /// Maps cell age (1...=max_age) to a color. Newborn cells are dark blue and older cells become brighter (toward white). 175 | fn age_to_color(age: u8) -> Rgb565 { 176 | if age == 0 { 177 | Rgb565::BLACK 178 | } else { 179 | let max_age = 10; 180 | let a = age.min(max_age) as u32; // clamp age and use u32 for intermediate math 181 | let r = ((31 * a) + 5) / max_age as u32; 182 | let g = ((63 * a) + 5) / max_age as u32; 183 | let b = 31; // Keep blue channel constant 184 | // Convert back to u8 and return the color. 185 | Rgb565::new(r as u8, g as u8, b) 186 | } 187 | } 188 | 189 | /// Draws the game grid using the cell age for color. 190 | fn draw_grid>( 191 | display: &mut D, 192 | grid: &[[u8; GRID_WIDTH]; GRID_HEIGHT], 193 | ) -> Result<(), D::Error> { 194 | let border_color = Rgb565::new(230, 230, 230); 195 | for (y, row) in grid.iter().enumerate() { 196 | for (x, &age) in row.iter().enumerate() { 197 | let point = Point::new(x as i32 * 7, y as i32 * 7); 198 | if age > 0 { 199 | // Draw a border then fill with color based on age. 200 | Rectangle::new(point, Size::new(7, 7)) 201 | .into_styled(PrimitiveStyle::with_fill(border_color)) 202 | .draw(display)?; 203 | // Draw an inner cell with color according to age. 204 | Rectangle::new(point + Point::new(1, 1), Size::new(5, 5)) 205 | .into_styled(PrimitiveStyle::with_fill(age_to_color(age))) 206 | .draw(display)?; 207 | } else { 208 | // Draw a dead cell as black. 209 | Rectangle::new(point, Size::new(7, 7)) 210 | .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) 211 | .draw(display)?; 212 | } 213 | } 214 | } 215 | Ok(()) 216 | } 217 | 218 | fn write_generation>( 219 | display: &mut D, 220 | generation: usize, 221 | ) -> Result<(), D::Error> { 222 | let mut num_str = heapless::String::<20>::new(); 223 | write!(num_str, "{}", generation).unwrap(); 224 | Text::new( 225 | num_str.as_str(), 226 | Point::new(8, 13), 227 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 228 | ) 229 | .draw(display)?; 230 | Ok(()) 231 | } 232 | 233 | // --- ECS Resources and Systems --- 234 | 235 | #[derive(Resource)] 236 | struct GameOfLifeResource { 237 | grid: [[u8; GRID_WIDTH]; GRID_HEIGHT], 238 | generation: usize, 239 | } 240 | 241 | impl Default for GameOfLifeResource { 242 | fn default() -> Self { 243 | Self { 244 | grid: [[0; GRID_WIDTH]; GRID_HEIGHT], 245 | generation: 0, 246 | } 247 | } 248 | } 249 | 250 | #[derive(Resource)] 251 | struct RngResource(Rng); 252 | 253 | // Because our display type contains DMA descriptors and raw pointers, it isn’t Sync. 254 | // We wrap it as a NonSend resource so that Bevy doesn’t require Sync. 255 | struct DisplayResource { 256 | display: MyDisplay, 257 | } 258 | 259 | fn update_game_of_life_system( 260 | mut game: ResMut, 261 | mut rng_res: ResMut, 262 | ) { 263 | update_game_of_life(&mut game.grid); 264 | game.generation += 1; 265 | if game.generation >= RESET_AFTER_GENERATIONS { 266 | randomize_grid(&mut rng_res.0, &mut game.grid); 267 | game.generation = 0; 268 | } 269 | } 270 | 271 | /// Render the game state by drawing into the offscreen framebuffer and then flushing 272 | /// it to the display via DMA. After drawing the game grid and generation number, 273 | /// we overlay centered text. 274 | fn render_system( 275 | mut display_res: NonSendMut, 276 | game: Res, 277 | mut fb_res: ResMut, 278 | ) { 279 | // Clear the framebuffer. 280 | fb_res.frame_buf.clear(Rgb565::BLACK).unwrap(); 281 | // Draw the game grid (using the age-based color) and generation number. 282 | draw_grid(&mut fb_res.frame_buf, &game.grid).unwrap(); 283 | write_generation(&mut fb_res.frame_buf, game.generation).unwrap(); 284 | 285 | // --- Overlay centered text --- 286 | let line1 = "Rust no_std ESP32-C6"; 287 | let line2 = "Bevy ECS 0.15 no_std"; 288 | // Estimate text width: assume ~8 pixels per character. 289 | let line1_width = line1.len() as i32 * 8; 290 | let line2_width = line2.len() as i32 * 8; 291 | let x1 = (LCD_H_RES as i32 - line1_width) / 2 + 14; 292 | let x2 = (LCD_H_RES as i32 - line2_width) / 2 + 14; 293 | // For vertical centering, assume 26 pixels total text height. 294 | let y = (LCD_V_RES as i32 - 26) / 2; 295 | Text::new( 296 | line1, 297 | Point::new(x1, y), 298 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 299 | ) 300 | .draw(&mut fb_res.frame_buf) 301 | .unwrap(); 302 | Text::new( 303 | line2, 304 | Point::new(x2, y + 14), 305 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 306 | ) 307 | .draw(&mut fb_res.frame_buf) 308 | .unwrap(); 309 | 310 | // Define the area covering the entire framebuffer. 311 | let area = Rectangle::new(Point::zero(), fb_res.frame_buf.size()); 312 | // Flush the framebuffer to the physical display. 313 | display_res 314 | .display 315 | .fill_contiguous(&area, fb_res.frame_buf.data.iter().copied()) 316 | .unwrap(); 317 | } 318 | 319 | #[main] 320 | fn main() -> ! { 321 | let peripherals = esp_hal::init(esp_hal::Config::default()); 322 | // Increase heap size as needed. 323 | esp_alloc::heap_allocator!(size: 150 * 1024); 324 | init_logger_from_env(); 325 | 326 | // --- DMA Buffers for SPI --- 327 | let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(8912); 328 | let dma_rx_buf = DmaRxBuf::new(rx_descriptors, rx_buffer).unwrap(); 329 | let dma_tx_buf = DmaTxBuf::new(tx_descriptors, tx_buffer).unwrap(); 330 | 331 | // --- Display Setup using BSP values --- 332 | // SPI: SCK = GPIO7, MOSI = GPIO6, CS = GPIO14. 333 | let spi = Spi::::new( 334 | peripherals.SPI2, 335 | esp_hal::spi::master::Config::default() 336 | .with_frequency(Rate::from_mhz(40)) 337 | .with_mode(esp_hal::spi::Mode::_0), 338 | ) 339 | .unwrap() 340 | .with_sck(peripherals.GPIO7) 341 | .with_mosi(peripherals.GPIO6) 342 | .with_dma(peripherals.DMA_CH0) 343 | .with_buffers(dma_rx_buf, dma_tx_buf); 344 | let cs_output = Output::new(peripherals.GPIO14, Level::High, OutputConfig::default()); 345 | let spi_delay = Delay::new(); 346 | let spi_device = ExclusiveDevice::new(spi, cs_output, spi_delay).unwrap(); 347 | 348 | // LCD interface: DC = GPIO15. 349 | let lcd_dc = Output::new(peripherals.GPIO15, Level::Low, OutputConfig::default()); 350 | // Leak a Box to obtain a 'static mutable buffer. 351 | let buffer: &'static mut [u8; 512] = Box::leak(Box::new([0_u8; 512])); 352 | let di = SpiInterface::new(spi_device, lcd_dc, buffer); 353 | 354 | let mut display_delay = Delay::new(); 355 | display_delay.delay_ns(500_000u32); 356 | 357 | // Reset pin: GPIO21 (active low per BSP). 358 | let reset = Output::new(peripherals.GPIO21, Level::Low, OutputConfig::default()); 359 | // Initialize the display using mipidsi's builder. 360 | let mut display: MyDisplay = Builder::new(ST7789, di) 361 | .reset_pin(reset) 362 | .display_size(206, 320) 363 | .invert_colors(ColorInversion::Inverted) 364 | .init(&mut display_delay) 365 | .unwrap(); 366 | 367 | display.clear(Rgb565::BLUE).unwrap(); 368 | 369 | // Backlight on GPIO22. 370 | let mut backlight = Output::new(peripherals.GPIO22, Level::Low, OutputConfig::default()); 371 | backlight.set_high(); 372 | 373 | info!("Display initialized"); 374 | 375 | // --- Initialize Game Resources --- 376 | let mut game = GameOfLifeResource::default(); 377 | let mut rng_instance = Rng::new(peripherals.RNG); 378 | randomize_grid(&mut rng_instance, &mut game.grid); 379 | let glider = [(1, 0), (2, 1), (0, 2), (1, 2), (2, 2)]; 380 | for (x, y) in glider.iter() { 381 | game.grid[*y][*x] = 1; // alive with age 1 382 | } 383 | 384 | // Create the framebuffer resource. 385 | let fb_res = FrameBufferResource::new(); 386 | 387 | let mut world = World::default(); 388 | world.insert_resource(game); 389 | world.insert_resource(RngResource(rng_instance)); 390 | // Insert the display as a non-send resource because its DMA pointers are not Sync. 391 | world.insert_non_send_resource(DisplayResource { display }); 392 | // Insert the framebuffer resource as a normal resource. 393 | world.insert_resource(fb_res); 394 | 395 | let mut schedule = Schedule::default(); 396 | schedule.add_systems(update_game_of_life_system); 397 | schedule.add_systems(render_system); 398 | 399 | let mut loop_delay = Delay::new(); 400 | 401 | loop { 402 | schedule.run(&mut world); 403 | loop_delay.delay_ms(50u32); 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /waveshare-esp32-c6-lcd-1_47/wokwi.toml: -------------------------------------------------------------------------------- 1 | [wokwi] 2 | version = 1 3 | 4 | elf = "target/riscv32imac-unknown-none-elf/release/esp32-conways-game-of-life-rs" 5 | firmware = "target/riscv32imac-unknown-none-elf/release/esp32-conways-game-of-life-rs" 6 | 7 | #gdbServerPort = 3333 8 | #elf = "target/xtensa-esp32s3-none-elf/debug/esp32-conways-game-of-life-rs" 9 | #firmware = "target/xtensa-esp32s3-none-elf/debug/esp32-conways-game-of-life-rs" 10 | 11 | 12 | -------------------------------------------------------------------------------- /waveshare-esp32-s3-touch-lcd-1_28/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.xtensa-esp32s3-none-elf] 2 | runner = "espflash flash --monitor" 3 | 4 | [env] 5 | ESP_LOG="INFO" 6 | ESP_HAL_CONFIG_PSRAM_MODE = "octal" 7 | 8 | [build] 9 | rustflags = [ 10 | "-C", "link-arg=-nostartfiles", 11 | ] 12 | 13 | target = "xtensa-esp32s3-none-elf" 14 | 15 | [unstable] 16 | build-std = ["alloc", "core"] 17 | -------------------------------------------------------------------------------- /waveshare-esp32-s3-touch-lcd-1_28/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esp32-conways-game-of-life-rs" 3 | version = "0.6.0" 4 | authors = ["Juraj Michálek "] 5 | edition = "2024" 6 | license = "MIT OR Apache-2.0" 7 | 8 | 9 | [dependencies] 10 | esp-hal = { version = "1.0.0-beta.1", features = ["esp32s3", "unstable"] } 11 | esp-backtrace = { version = "0.16.0", features = [ 12 | "panic-handler", 13 | "println" 14 | ] } 15 | esp-println = { version = "0.14.0", features = [ "log-04" ] } 16 | log = { version = "0.4.26" } 17 | 18 | esp-alloc = "0.8.0" 19 | embedded-graphics = "0.8.1" 20 | embedded-hal = "1.0.0" 21 | mipidsi = "0.9.0" 22 | #esp-display-interface-spi-dma = "0.3.0" 23 | # esp-display-interface-spi-dma = { path = "../esp-display-interface-spi-dma"} 24 | # esp-bsp = "0.4.1" 25 | embedded-graphics-framebuf = "0.5.0" 26 | heapless = "0.8.0" 27 | embedded-hal-bus = "0.3.0" 28 | #bevy_ecs = { git = "https://github.com/bevyengine/bevy.git", rev = "301f618", default-features = false } 29 | bevy_ecs = { version = "0.16.1", default-features = false } 30 | 31 | [features] 32 | default = [ "esp-hal/esp32s3", "esp-backtrace/esp32s3", "esp-println/esp32s3", "esp-hal/psram" ] 33 | -------------------------------------------------------------------------------- /waveshare-esp32-s3-touch-lcd-1_28/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg=-Tlinkall.x"); 3 | } 4 | -------------------------------------------------------------------------------- /waveshare-esp32-s3-touch-lcd-1_28/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "esp" 3 | -------------------------------------------------------------------------------- /waveshare-esp32-s3-touch-lcd-1_28/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | extern crate alloc; 5 | use alloc::boxed::Box; 6 | 7 | use bevy_ecs::prelude::*; 8 | use core::fmt::Write; 9 | use embedded_graphics::{ 10 | Drawable, 11 | mono_font::{MonoTextStyle, ascii::FONT_8X13}, 12 | pixelcolor::Rgb565, 13 | prelude::*, 14 | primitives::{PrimitiveStyle, Rectangle}, 15 | text::Text, 16 | }; 17 | use embedded_graphics_framebuf::FrameBuf; 18 | use embedded_graphics_framebuf::backends::FrameBufferBackend; 19 | use embedded_hal::delay::DelayNs; 20 | use embedded_hal_bus::spi::ExclusiveDevice; 21 | use esp_hal::delay::Delay; 22 | use esp_hal::dma::{DmaRxBuf, DmaTxBuf}; 23 | use esp_hal::dma_buffers; 24 | use esp_hal::{ 25 | Blocking, 26 | gpio::{Input, Level, Output, OutputConfig}, 27 | main, 28 | rng::Rng, 29 | spi::master::{Spi, SpiDmaBus}, 30 | time::Rate, 31 | }; 32 | use esp_println::{logger::init_logger_from_env, println}; 33 | use log::info; 34 | use mipidsi::options::ColorOrder; 35 | use mipidsi::{Builder, models::GC9A01}; 36 | use mipidsi::{interface::SpiInterface, options::ColorInversion}; 37 | 38 | #[panic_handler] 39 | fn panic(_info: &core::panic::PanicInfo) -> ! { 40 | println!("Panic: {}", _info); 41 | loop {} 42 | } 43 | 44 | /// A wrapper around a boxed array that implements FrameBufferBackend. 45 | /// This allows the framebuffer to be allocated on the heap. 46 | pub struct HeapBuffer(Box<[C; N]>); 47 | 48 | impl HeapBuffer { 49 | pub fn new(data: Box<[C; N]>) -> Self { 50 | Self(data) 51 | } 52 | } 53 | 54 | impl core::ops::Deref for HeapBuffer { 55 | type Target = [C; N]; 56 | fn deref(&self) -> &Self::Target { 57 | &*self.0 58 | } 59 | } 60 | 61 | impl core::ops::DerefMut for HeapBuffer { 62 | fn deref_mut(&mut self) -> &mut Self::Target { 63 | &mut *self.0 64 | } 65 | } 66 | 67 | impl FrameBufferBackend for HeapBuffer { 68 | type Color = C; 69 | fn set(&mut self, index: usize, color: Self::Color) { 70 | self.0[index] = color; 71 | } 72 | fn get(&self, index: usize) -> Self::Color { 73 | self.0[index] 74 | } 75 | fn nr_elements(&self) -> usize { 76 | N 77 | } 78 | } 79 | 80 | // --- Type Alias for the Concrete Display --- 81 | // Use the DMA-enabled SPI bus type. 82 | type MyDisplay = mipidsi::Display< 83 | SpiInterface< 84 | 'static, 85 | ExclusiveDevice, Output<'static>, Delay>, 86 | Output<'static>, 87 | >, 88 | GC9A01, 89 | Output<'static>, 90 | >; 91 | 92 | // --- LCD Resolution and FrameBuffer Type Aliases --- 93 | const LCD_H_RES: usize = 240; 94 | const LCD_V_RES: usize = 240; 95 | const LCD_BUFFER_SIZE: usize = LCD_H_RES * LCD_V_RES; 96 | 97 | // We want our pixels stored as Rgb565. 98 | type FbBuffer = HeapBuffer; 99 | // Define a type alias for the complete FrameBuf. 100 | type MyFrameBuf = FrameBuf; 101 | 102 | #[derive(Resource)] 103 | struct FrameBufferResource { 104 | frame_buf: MyFrameBuf, 105 | } 106 | 107 | impl FrameBufferResource { 108 | fn new() -> Self { 109 | // Allocate the framebuffer data on the heap. 110 | let fb_data: Box<[Rgb565; LCD_BUFFER_SIZE]> = Box::new([Rgb565::BLACK; LCD_BUFFER_SIZE]); 111 | let heap_buffer = HeapBuffer::new(fb_data); 112 | let frame_buf = MyFrameBuf::new(heap_buffer, LCD_H_RES, LCD_V_RES); 113 | Self { frame_buf } 114 | } 115 | } 116 | 117 | // --- Game of Life Definitions --- 118 | // Now each cell is a u8 (0 means dead; >0 indicates age) 119 | const GRID_WIDTH: usize = 35; 120 | const GRID_HEIGHT: usize = 35; 121 | const RESET_AFTER_GENERATIONS: usize = 500; 122 | 123 | fn randomize_grid(rng: &mut Rng, grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 124 | for row in grid.iter_mut() { 125 | for cell in row.iter_mut() { 126 | let mut buf = [0u8; 1]; 127 | rng.read(&mut buf); 128 | // Randomly set cell to 1 (alive) or 0 (dead) 129 | *cell = if buf[0] & 1 != 0 { 1 } else { 0 }; 130 | } 131 | } 132 | } 133 | 134 | fn update_game_of_life(grid: &mut [[u8; GRID_WIDTH]; GRID_HEIGHT]) { 135 | let mut new_grid = [[0u8; GRID_WIDTH]; GRID_HEIGHT]; 136 | for y in 0..GRID_HEIGHT { 137 | for x in 0..GRID_WIDTH { 138 | // Count neighbors: consider a cell alive if its age is >0. 139 | let mut alive_neighbors = 0; 140 | for i in 0..3 { 141 | for j in 0..3 { 142 | if i == 1 && j == 1 { 143 | continue; 144 | } 145 | let nx = (x + i + GRID_WIDTH - 1) % GRID_WIDTH; 146 | let ny = (y + j + GRID_HEIGHT - 1) % GRID_HEIGHT; 147 | if grid[ny][nx] > 0 { 148 | alive_neighbors += 1; 149 | } 150 | } 151 | } 152 | if grid[y][x] > 0 { 153 | // Live cell survives if 2 or 3 neighbors; increment age. 154 | if alive_neighbors == 2 || alive_neighbors == 3 { 155 | new_grid[y][x] = grid[y][x].saturating_add(1); 156 | } else { 157 | new_grid[y][x] = 0; 158 | } 159 | } else { 160 | // Dead cell becomes alive if exactly 3 neighbors. 161 | if alive_neighbors == 3 { 162 | new_grid[y][x] = 1; 163 | } else { 164 | new_grid[y][x] = 0; 165 | } 166 | } 167 | } 168 | } 169 | *grid = new_grid; 170 | } 171 | 172 | /// Maps cell age (1..=max_age) to a color. Newborn cells are dark blue and older cells become brighter (toward white). 173 | fn age_to_color(age: u8) -> Rgb565 { 174 | if age == 0 { 175 | Rgb565::BLACK 176 | } else { 177 | let max_age = 10; 178 | let a = age.min(max_age) as u32; // clamp age and use u32 for intermediate math 179 | let r = ((31 * a) + 5) / max_age as u32; 180 | let g = ((63 * a) + 5) / max_age as u32; 181 | let b = 31; // Keep blue channel constant 182 | // Convert back to u8 and return the color. 183 | Rgb565::new(r as u8, g as u8, b) 184 | } 185 | } 186 | 187 | /// Draws the game grid using the cell age for color. 188 | fn draw_grid>( 189 | display: &mut D, 190 | grid: &[[u8; GRID_WIDTH]; GRID_HEIGHT], 191 | ) -> Result<(), D::Error> { 192 | let border_color = Rgb565::new(230, 230, 230); 193 | for (y, row) in grid.iter().enumerate() { 194 | for (x, &age) in row.iter().enumerate() { 195 | let point = Point::new(x as i32 * 7, y as i32 * 7); 196 | if age > 0 { 197 | // Draw a border then fill with color based on age. 198 | Rectangle::new(point, Size::new(7, 7)) 199 | .into_styled(PrimitiveStyle::with_fill(border_color)) 200 | .draw(display)?; 201 | // Draw an inner cell with color according to age. 202 | Rectangle::new(point + Point::new(1, 1), Size::new(5, 5)) 203 | .into_styled(PrimitiveStyle::with_fill(age_to_color(age))) 204 | .draw(display)?; 205 | } else { 206 | // Draw a dead cell as black. 207 | Rectangle::new(point, Size::new(7, 7)) 208 | .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) 209 | .draw(display)?; 210 | } 211 | } 212 | } 213 | Ok(()) 214 | } 215 | 216 | fn write_generation>( 217 | display: &mut D, 218 | generation: usize, 219 | ) -> Result<(), D::Error> { 220 | let x = 70; 221 | let y = 140; 222 | 223 | let mut num_str = heapless::String::<20>::new(); 224 | write!(num_str, "Generation: {}", generation).unwrap(); 225 | Text::new( 226 | num_str.as_str(), 227 | Point::new(x, y), 228 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 229 | ) 230 | .draw(display)?; 231 | Ok(()) 232 | } 233 | 234 | // --- ECS Resources and Systems --- 235 | 236 | #[derive(Resource)] 237 | struct GameOfLifeResource { 238 | grid: [[u8; GRID_WIDTH]; GRID_HEIGHT], 239 | generation: usize, 240 | } 241 | 242 | impl Default for GameOfLifeResource { 243 | fn default() -> Self { 244 | Self { 245 | grid: [[0; GRID_WIDTH]; GRID_HEIGHT], 246 | generation: 0, 247 | } 248 | } 249 | } 250 | 251 | #[derive(Resource)] 252 | struct RngResource(Rng); 253 | 254 | // Because our display type contains DMA descriptors and raw pointers, it isn’t Sync. 255 | // We wrap it as a NonSend resource so that Bevy doesn’t require Sync. 256 | struct DisplayResource { 257 | display: MyDisplay, 258 | } 259 | 260 | struct ButtonResource { 261 | button: Input<'static>, 262 | } 263 | 264 | /// Resource to track the previous state of the button (for edge detection). 265 | #[derive(Default, Resource)] 266 | struct ButtonState { 267 | was_pressed: bool, 268 | } 269 | 270 | fn update_game_of_life_system( 271 | mut game: ResMut, 272 | mut rng_res: ResMut, 273 | ) { 274 | update_game_of_life(&mut game.grid); 275 | game.generation += 1; 276 | if game.generation >= RESET_AFTER_GENERATIONS { 277 | randomize_grid(&mut rng_res.0, &mut game.grid); 278 | game.generation = 0; 279 | } 280 | } 281 | 282 | /// System to check the button and reset the simulation when pressed. 283 | fn button_reset_system( 284 | mut game: ResMut, 285 | mut rng_res: ResMut, 286 | mut btn_state: ResMut, 287 | button_res: NonSend, 288 | ) { 289 | // Check if the button is pressed (active low) 290 | if button_res.button.is_low() { 291 | if !btn_state.was_pressed { 292 | // Button press detected: reset simulation. 293 | randomize_grid(&mut rng_res.0, &mut game.grid); 294 | game.generation = 0; 295 | btn_state.was_pressed = true; 296 | } 297 | } else { 298 | btn_state.was_pressed = false; 299 | } 300 | } 301 | 302 | /// Render the game state by drawing into the offscreen framebuffer and then flushing 303 | /// it to the display via DMA. After drawing the game grid and generation number, 304 | /// we overlay centered text. 305 | fn render_system( 306 | mut display_res: NonSendMut, 307 | game: Res, 308 | mut fb_res: ResMut, 309 | ) { 310 | // Clear the framebuffer. 311 | fb_res.frame_buf.clear(Rgb565::BLACK).unwrap(); 312 | // Draw the game grid (using the age-based color) and generation number. 313 | draw_grid(&mut fb_res.frame_buf, &game.grid).unwrap(); 314 | write_generation(&mut fb_res.frame_buf, game.generation).unwrap(); 315 | 316 | // --- Overlay centered text --- 317 | let line1 = "Rust no_std ESP32-S3"; 318 | let line2 = "Bevy ECS 0.16 no_std"; 319 | // Estimate text width: assume ~8 pixels per character. 320 | let line1_width = line1.len() as i32 * 8; 321 | let line2_width = line2.len() as i32 * 8; 322 | let x1 = (LCD_H_RES as i32 - line1_width) / 2 + 14; 323 | let x2 = (LCD_H_RES as i32 - line2_width) / 2 + 14; 324 | // For vertical centering, assume 26 pixels total text height. 325 | let y = (LCD_V_RES as i32 - 26) / 2; 326 | Text::new( 327 | line1, 328 | Point::new(x1, y), 329 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 330 | ) 331 | .draw(&mut fb_res.frame_buf) 332 | .unwrap(); 333 | Text::new( 334 | line2, 335 | Point::new(x2, y + 14), 336 | MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE), 337 | ) 338 | .draw(&mut fb_res.frame_buf) 339 | .unwrap(); 340 | 341 | // Define the area covering the entire framebuffer. 342 | let area = Rectangle::new(Point::zero(), fb_res.frame_buf.size()); 343 | // Flush the framebuffer to the physical display. 344 | display_res 345 | .display 346 | .fill_contiguous(&area, fb_res.frame_buf.data.iter().copied()) 347 | .unwrap(); 348 | } 349 | 350 | #[main] 351 | fn main() -> ! { 352 | let peripherals = esp_hal::init(esp_hal::Config::default()); 353 | // Increase heap size as needed. 354 | esp_alloc::heap_allocator!(size: 150000); 355 | init_logger_from_env(); 356 | 357 | // --- DMA Buffers for SPI --- 358 | let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(1024); 359 | let dma_rx_buf = DmaRxBuf::new(rx_descriptors, rx_buffer).unwrap(); 360 | let dma_tx_buf = DmaTxBuf::new(tx_descriptors, tx_buffer).unwrap(); 361 | 362 | // --- Display Setup using BSP values --- 363 | let spi = Spi::::new( 364 | peripherals.SPI2, 365 | esp_hal::spi::master::Config::default() 366 | .with_frequency(Rate::from_mhz(80)) 367 | .with_mode(esp_hal::spi::Mode::_0), 368 | ) 369 | .unwrap() 370 | .with_sck(peripherals.GPIO10) 371 | .with_mosi(peripherals.GPIO11) 372 | .with_dma(peripherals.DMA_CH0) 373 | // .with_miso(peripherals.GPIO14) 374 | .with_buffers(dma_rx_buf, dma_tx_buf); 375 | let cs_output = Output::new(peripherals.GPIO9, Level::High, OutputConfig::default()); 376 | let spi_delay = Delay::new(); 377 | let spi_device = ExclusiveDevice::new(spi, cs_output, spi_delay).unwrap(); 378 | 379 | // LCD interface 380 | let lcd_dc = Output::new(peripherals.GPIO8, Level::Low, OutputConfig::default()); 381 | // Leak a Box to obtain a 'static mutable buffer. 382 | let buffer: &'static mut [u8; 512] = Box::leak(Box::new([0_u8; 512])); 383 | let di = SpiInterface::new(spi_device, lcd_dc, buffer); 384 | 385 | let mut display_delay = Delay::new(); 386 | display_delay.delay_ns(500_000u32); 387 | 388 | // Reset pin 389 | let reset = Output::new(peripherals.GPIO14, Level::Low, OutputConfig::default()); 390 | // Initialize the display using mipidsi's builder. 391 | let mut display: MyDisplay = Builder::new(GC9A01, di) 392 | .reset_pin(reset) 393 | .display_size(240, 240) 394 | // .orientation(Orientation::new().flip_horizontal()) 395 | .color_order(ColorOrder::Bgr) 396 | .invert_colors(ColorInversion::Inverted) 397 | .init(&mut display_delay) 398 | .unwrap(); 399 | 400 | display.clear(Rgb565::BLACK).unwrap(); 401 | 402 | // Backlight 403 | let mut backlight = Output::new(peripherals.GPIO2, Level::High, OutputConfig::default()); 404 | backlight.set_high(); 405 | 406 | info!("Display initialized"); 407 | 408 | // --- Initialize Game Resources --- 409 | let mut game = GameOfLifeResource::default(); 410 | let mut rng_instance = Rng::new(peripherals.RNG); 411 | randomize_grid(&mut rng_instance, &mut game.grid); 412 | let glider = [(1, 0), (2, 1), (0, 2), (1, 2), (2, 2)]; 413 | for (x, y) in glider.iter() { 414 | game.grid[*y][*x] = 1; // alive with age 1 415 | } 416 | 417 | // Create the framebuffer resource. 418 | let fb_res = FrameBufferResource::new(); 419 | 420 | let mut world = World::default(); 421 | world.insert_resource(game); 422 | world.insert_resource(RngResource(rng_instance)); 423 | // Insert the display as a non-send resource because its DMA pointers are not Sync. 424 | world.insert_non_send_resource(DisplayResource { display }); 425 | // Insert the framebuffer resource as a normal resource. 426 | world.insert_resource(fb_res); 427 | 428 | // --- Initialize Button Resource --- 429 | // Configure the button as an input with an internal pull-up (active low). 430 | // let button = Input::new( 431 | // peripherals.GPIO9, 432 | // InputConfig::default().with_pull(Pull::Up), 433 | // ); 434 | // world.insert_non_send_resource(ButtonResource { button }); 435 | // Insert a resource to track button state for debouncing. 436 | // world.insert_resource(ButtonState::default()); 437 | 438 | let mut schedule = Schedule::default(); 439 | // schedule.add_systems(button_reset_system); 440 | schedule.add_systems(update_game_of_life_system); 441 | schedule.add_systems(render_system); 442 | 443 | let mut loop_delay = Delay::new(); 444 | 445 | loop { 446 | schedule.run(&mut world); 447 | loop_delay.delay_ms(50u32); 448 | } 449 | } 450 | --------------------------------------------------------------------------------