|
| 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 | +} |
0 commit comments