├── .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 | [](https://wokwi.com/projects/380370193649185793)
6 |
7 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------