Skip to content

Commit ae7f304

Browse files
committed
use separate implementation for Bevy based version for Waveshare
1 parent 699170c commit ae7f304

File tree

14 files changed

+395
-3
lines changed

14 files changed

+395
-3
lines changed

docs/README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
1-
# ESP32 Conways Game of Life in Rust
1+
# ESP32 Conway's Game of Life in Rust
22

3-
Implementation for ESP32-S3-BOX-3 Rust Bare Metal.
3+
Implementation of Conway's Game of Life Rust Bare Metal.
44

55
[![Wokwi](https://img.shields.io/endpoint?url=https%3A%2F%2Fwokwi.com%2Fbadge%2Fclick-to-simulate.json)](https://wokwi.com/projects/380370193649185793)
66

77
![ESP32 Conways Game of Life in Rust](esp32-conways-game-of-life-rs.png)
88

9-
## Build and run
9+
## Supported boards
10+
11+
### ESP32-S3-BOX-3
12+
13+
- https://github.com/espressif/esp-box
14+
15+
The implementation is based on Rust no\_std, using mipidsi crate.
16+
17+
```
18+
cargo run --release
19+
```
20+
21+
### ESP32-C6-1.47 Waveshare
22+
23+
- https://www.waveshare.com/esp32-c6-lcd-1.47.htm
24+
25+
The implementation is based on Rust no\_std and Bevy 0.15 no\_std, plus mipidsi crate
1026

1127
```
1228
cargo run --release
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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"]

esp32-c6-waveshare-1_47/Cargo.toml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[package]
2+
name = "esp32-conways-game-of-life-rs"
3+
version = "0.5.0"
4+
authors = ["Juraj Michálek <[email protected]>"]
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+
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/esp32s3", "esp-backtrace/esp32s3", "esp-println/esp32s3", "esp32-s3-box-3" ]
33+
default = [ "esp-hal/esp32c6", "esp-backtrace/esp32c6", "esp-println/esp32c6" ]
34+
35+
# esp32-s3-box-3 = [ "esp-bsp/esp32-s3-box-3", "esp-hal/psram" ]
File renamed without changes.
File renamed without changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[toolchain]
2+
channel = "stable"
3+
components = ["rust-src"]
4+
targets = ["riscv32imac-unknown-none-elf"]

esp32-c6-waveshare-1_47/src/main.rs

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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 esp_hal::delay::Delay;
18+
use esp_hal::{
19+
gpio::{Level, Output, OutputConfig},
20+
rng::Rng,
21+
spi::master::Spi,
22+
Blocking,
23+
main,
24+
time::Rate,
25+
};
26+
use embedded_hal_bus::spi::ExclusiveDevice;
27+
use esp_println::{logger::init_logger_from_env, println};
28+
use log::info;
29+
use mipidsi::{interface::SpiInterface, options::ColorInversion};
30+
31+
use mipidsi::{models::ST7789, Builder};
32+
// Bevy ECS (no_std) imports:
33+
use bevy_ecs::prelude::*;
34+
35+
// --- Type Alias for the Concrete Display ---
36+
// Now we specify that the SPI interface uses Delay.
37+
type MyDisplay = mipidsi::Display<
38+
SpiInterface<
39+
'static,
40+
ExclusiveDevice<Spi<'static, Blocking>, Output<'static>, Delay>,
41+
Output<'static>
42+
>,
43+
ST7789,
44+
Output<'static>
45+
>;
46+
47+
#[panic_handler]
48+
fn panic(_info: &core::panic::PanicInfo) -> ! {
49+
println!("Panic: {}", _info);
50+
loop {}
51+
}
52+
53+
#[allow(unused_imports)]
54+
use esp_alloc::EspHeap;
55+
56+
// --- Game of Life Definitions ---
57+
58+
const WIDTH: usize = 64;
59+
const HEIGHT: usize = 48;
60+
const RESET_AFTER_GENERATIONS: usize = 500;
61+
62+
fn randomize_grid(rng: &mut Rng, grid: &mut [[bool; WIDTH]; HEIGHT]) {
63+
for row in grid.iter_mut() {
64+
for cell in row.iter_mut() {
65+
let mut buf = [0u8; 1];
66+
rng.read(&mut buf);
67+
*cell = buf[0] & 1 != 0;
68+
}
69+
}
70+
}
71+
72+
fn update_game_of_life(grid: &mut [[bool; WIDTH]; HEIGHT]) {
73+
let mut new_grid = [[false; WIDTH]; HEIGHT];
74+
for y in 0..HEIGHT {
75+
for x in 0..WIDTH {
76+
let alive_neighbors = count_alive_neighbors(x, y, grid);
77+
new_grid[y][x] = matches!(
78+
(grid[y][x], alive_neighbors),
79+
(true, 2) | (true, 3) | (false, 3)
80+
);
81+
}
82+
}
83+
*grid = new_grid;
84+
}
85+
86+
fn count_alive_neighbors(x: usize, y: usize, grid: &[[bool; WIDTH]; HEIGHT]) -> u8 {
87+
let mut count = 0;
88+
for i in 0..3 {
89+
for j in 0..3 {
90+
if i == 1 && j == 1 { continue; }
91+
let neighbor_x = (x + i + WIDTH - 1) % WIDTH;
92+
let neighbor_y = (y + j + HEIGHT - 1) % HEIGHT;
93+
if grid[neighbor_y][neighbor_x] {
94+
count += 1;
95+
}
96+
}
97+
}
98+
count
99+
}
100+
101+
fn draw_grid<D: DrawTarget<Color = Rgb565>>(
102+
display: &mut D,
103+
grid: &[[bool; WIDTH]; HEIGHT],
104+
) -> Result<(), D::Error> {
105+
let border_color = Rgb565::new(230, 230, 230);
106+
107+
for (y, row) in grid.iter().enumerate() {
108+
for (x, &cell) in row.iter().enumerate() {
109+
if cell {
110+
let cell_size = Size::new(5, 5);
111+
let border_size = Size::new(7, 7);
112+
Rectangle::new(Point::new(x as i32 * 7, y as i32 * 7), border_size)
113+
.into_styled(PrimitiveStyle::with_fill(border_color))
114+
.draw(display)?;
115+
Rectangle::new(Point::new(x as i32 * 7 + 1, y as i32 * 7 + 1), cell_size)
116+
.into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE))
117+
.draw(display)?;
118+
} else {
119+
Rectangle::new(Point::new(x as i32 * 7, y as i32 * 7), Size::new(7, 7))
120+
.into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK))
121+
.draw(display)?;
122+
}
123+
}
124+
}
125+
Ok(())
126+
}
127+
128+
fn write_generation<D: DrawTarget<Color = Rgb565>>(
129+
display: &mut D,
130+
generation: usize,
131+
) -> Result<(), D::Error> {
132+
let mut num_str = heapless::String::<20>::new();
133+
write!(num_str, "{}", generation).unwrap();
134+
Text::new(
135+
num_str.as_str(),
136+
Point::new(8, 13),
137+
MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE),
138+
)
139+
.draw(display)?;
140+
Ok(())
141+
}
142+
143+
// --- ECS Resources and Systems ---
144+
145+
#[derive(Resource)]
146+
struct GameOfLifeResource {
147+
grid: [[bool; WIDTH]; HEIGHT],
148+
generation: usize,
149+
}
150+
151+
impl Default for GameOfLifeResource {
152+
fn default() -> Self {
153+
Self {
154+
grid: [[false; WIDTH]; HEIGHT],
155+
generation: 0,
156+
}
157+
}
158+
}
159+
160+
#[derive(Resource)]
161+
struct RngResource(Rng);
162+
163+
// Use the concrete display type in our resource.
164+
#[derive(Resource)]
165+
struct DisplayResource {
166+
display: MyDisplay,
167+
}
168+
169+
fn update_game_of_life_system(
170+
mut game: ResMut<GameOfLifeResource>,
171+
mut rng_res: ResMut<RngResource>,
172+
) {
173+
update_game_of_life(&mut game.grid);
174+
game.generation += 1;
175+
if game.generation >= RESET_AFTER_GENERATIONS {
176+
randomize_grid(&mut rng_res.0, &mut game.grid);
177+
game.generation = 0;
178+
}
179+
}
180+
181+
fn render_system(mut display_res: ResMut<DisplayResource>, game: Res<GameOfLifeResource>) {
182+
display_res.display.clear(Rgb565::BLACK).unwrap();
183+
draw_grid(&mut display_res.display, &game.grid).unwrap();
184+
write_generation(&mut display_res.display, game.generation).unwrap();
185+
}
186+
187+
// --- Main Function ---
188+
189+
#[main]
190+
fn main() -> ! {
191+
let peripherals = esp_hal::init(esp_hal::Config::default());
192+
esp_alloc::heap_allocator!(size: 72 * 1024);
193+
init_logger_from_env();
194+
195+
// --- Display Setup using BSP values ---
196+
// SPI: SCK = GPIO7, MOSI = GPIO6, CS = GPIO14.
197+
let spi = Spi::<Blocking>::new(
198+
peripherals.SPI2,
199+
esp_hal::spi::master::Config::default()
200+
.with_frequency(Rate::from_mhz(40))
201+
.with_mode(esp_hal::spi::Mode::_0),
202+
)
203+
.unwrap()
204+
.with_sck(peripherals.GPIO7)
205+
.with_mosi(peripherals.GPIO6);
206+
let cs_output = Output::new(peripherals.GPIO14, Level::High, OutputConfig::default());
207+
// Create a proper SPI device with a Delay instance.
208+
let spi_delay = Delay::new();
209+
let spi_device = ExclusiveDevice::new(spi, cs_output, spi_delay).unwrap();
210+
211+
// LCD interface: DC = GPIO15.
212+
let lcd_dc = Output::new(peripherals.GPIO15, Level::Low, OutputConfig::default());
213+
// Leak a Box to obtain a 'static mutable buffer.
214+
let buffer: &'static mut [u8; 512] = Box::leak(Box::new([0_u8; 512]));
215+
let di = SpiInterface::new(spi_device, lcd_dc, buffer);
216+
217+
// Create a separate Delay for display initialization.
218+
let mut display_delay = Delay::new();
219+
display_delay.delay_ns(500_000u32);
220+
221+
// Reset pin: GPIO21. Per BSP, reset is active low.
222+
let reset = Output::new(
223+
peripherals.GPIO21,
224+
Level::Low, // match BSP: lcd_reset_pin! creates with Level::Low.
225+
OutputConfig::default(),
226+
);
227+
// Initialize the display using mipidsi's builder.
228+
let mut display: MyDisplay = Builder::new(ST7789, di)
229+
.reset_pin(reset)
230+
.display_size(206, 320)
231+
.invert_colors(ColorInversion::Inverted)
232+
.init(&mut display_delay)
233+
.unwrap();
234+
235+
display.clear(Rgb565::BLUE).unwrap();
236+
237+
// Backlight on GPIO22: create pin with initial low then set high.
238+
let mut backlight = Output::new(peripherals.GPIO22, Level::Low, OutputConfig::default());
239+
backlight.set_high();
240+
241+
info!("Display initialized");
242+
243+
// --- Initialize Game Resources ---
244+
let mut game = GameOfLifeResource::default();
245+
let mut rng_instance = Rng::new(peripherals.RNG);
246+
randomize_grid(&mut rng_instance, &mut game.grid);
247+
let glider = [(1, 0), (2, 1), (0, 2), (1, 2), (2, 2)];
248+
for (x, y) in glider.iter() {
249+
game.grid[*y][*x] = true;
250+
}
251+
252+
let mut world = World::default();
253+
world.insert_resource(game);
254+
world.insert_resource(RngResource(rng_instance));
255+
world.insert_resource(DisplayResource { display });
256+
257+
let mut schedule = Schedule::default();
258+
schedule.add_systems(update_game_of_life_system);
259+
schedule.add_systems(render_system);
260+
261+
// Create a separate Delay for game-loop timing.
262+
let mut loop_delay = Delay::new();
263+
264+
loop {
265+
schedule.run(&mut world);
266+
loop_delay.delay_ms(100u32);
267+
}
268+
}
File renamed without changes.
File renamed without changes.

esp32-s3-box-3/build.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
println!("cargo:rustc-link-arg=-Tlinkall.x");
3+
}

0 commit comments

Comments
 (0)