diff --git a/Cargo.toml b/Cargo.toml index 3a957ba..7df2ae2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,8 @@ cortex-m = "0.7.3" cortex-m-rt = "0.6.10" embedded-graphics = "0.7.1" panic-semihosting = "0.5.2" +cortex-m-rtic = "0.5.6" +tinybmp = "0.3.1" [dev-dependencies.stm32f1xx-hal] version = "0.7.0" diff --git a/examples/dvd.bmp b/examples/dvd.bmp new file mode 100644 index 0000000..9c13c84 Binary files /dev/null and b/examples/dvd.bmp differ diff --git a/examples/rtic_dvd.rs b/examples/rtic_dvd.rs new file mode 100644 index 0000000..c7311d4 --- /dev/null +++ b/examples/rtic_dvd.rs @@ -0,0 +1,180 @@ +//! Bounce a DVD player logo around the screen +//! +//! Like this, but with no color changing: https://bouncingdvdlogo.com/ +//! +//! This example is for the STM32F103 "Blue Pill" board using I2C1. +//! +//! Wiring connections are as follows for a CRIUS-branded display: +//! +//! ``` +//! Display -> Blue Pill +//! (black) GND -> GND +//! (red) +5V -> VCC +//! (yellow) SDA -> PB9 +//! (green) SCL -> PB8 +//! ``` +//! +//! Run on a Blue Pill with `cargo run --example rtic_dvd`. + +#![no_std] +#![no_main] + +use embedded_graphics::{ + geometry::Point, + image::Image, + pixelcolor::{BinaryColor, Rgb565}, + prelude::*, + primitives::{PrimitiveStyle, Rectangle}, +}; +use panic_semihosting as _; +use rtic::app; +use sh1106::{prelude::*, Builder}; +use stm32f1xx_hal::{ + gpio, + i2c::{BlockingI2c, DutyCycle, Mode}, + pac::{self, I2C1}, + prelude::*, + timer::{CountDownTimer, Event, Timer}, +}; +use tinybmp::Bmp; + +type Display = GraphicsMode< + I2cInterface< + BlockingI2c< + I2C1, + ( + gpio::gpiob::PB8>, + gpio::gpiob::PB9>, + ), + >, + >, +>; + +#[app(device = stm32f1xx_hal::pac, peripherals = true)] +const APP: () = { + struct Resources { + display: Display, + timer: CountDownTimer, + top_left: Point, + velocity: Point, + bmp: Bmp, + } + + #[init] + fn init(cx: init::Context) -> init::LateResources { + let dp = cx.device; + + let mut flash = dp.FLASH.constrain(); + let mut rcc = dp.RCC.constrain(); + + let clocks = rcc + .cfgr + .use_hse(8.mhz()) + .sysclk(72.mhz()) + .pclk1(36.mhz()) + .freeze(&mut flash.acr); + + let mut afio = dp.AFIO.constrain(&mut rcc.apb2); + + let mut gpiob = dp.GPIOB.split(&mut rcc.apb2); + + let scl = gpiob.pb8.into_alternate_open_drain(&mut gpiob.crh); + let sda = gpiob.pb9.into_alternate_open_drain(&mut gpiob.crh); + + let i2c = BlockingI2c::i2c1( + dp.I2C1, + (scl, sda), + &mut afio.mapr, + Mode::Fast { + frequency: 400_000.hz(), + duty_cycle: DutyCycle::Ratio2to1, + }, + clocks, + &mut rcc.apb1, + 1000, + 10, + 1000, + 1000, + ); + + let mut display: GraphicsMode<_> = Builder::new().connect_i2c(i2c).into(); + + display.init().unwrap(); + display.flush().unwrap(); + + // Update framerate + let fps = 20; + + let mut timer = Timer::tim1(dp.TIM1, &clocks, &mut rcc.apb2).start_count_down(fps.hz()); + + timer.listen(Event::Update); + + let bmp = Bmp::from_slice(include_bytes!("dvd.bmp")).unwrap(); + + // Init the static resources to use them later through RTIC + init::LateResources { + timer, + display, + top_left: Point::new(5, 3), + velocity: Point::new(1, 1), + bmp, + } + } + + #[idle()] + fn idle(_: idle::Context) -> ! { + loop { + // Fix default wfi() behaviour breaking debug probe + core::hint::spin_loop(); + } + } + + #[task(binds = TIM1_UP, resources = [display, top_left, velocity, timer, bmp])] + fn update(cx: update::Context) { + let update::Resources { + display, + top_left, + velocity, + timer, + bmp, + .. + } = cx.resources; + + let bottom_right = *top_left + bmp.bounding_box().size; + + // Erase previous image position with a filled black rectangle + Rectangle::with_corners(*top_left, bottom_right) + .into_styled(PrimitiveStyle::with_fill(BinaryColor::Off)) + .draw(display) + .unwrap(); + + // Check if the image collided with a screen edge + { + if bottom_right.x > display.size().width as i32 || top_left.x < 0 { + velocity.x = -velocity.x; + } + + if bottom_right.y > display.size().height as i32 || top_left.y < 0 { + velocity.y = -velocity.y; + } + } + + // Move the image + *top_left += *velocity; + + // Draw image at new position + Image::new(bmp, *top_left) + .draw(&mut display.color_converted()) + .unwrap(); + + // Write changes to the display + display.flush().unwrap(); + + // Clears the update flag + timer.clear_update_interrupt_flag(); + } + + extern "C" { + fn EXTI0(); + } +}; diff --git a/src/mode/graphics.rs b/src/mode/graphics.rs index 15642fb..ed61d3f 100644 --- a/src/mode/graphics.rs +++ b/src/mode/graphics.rs @@ -42,12 +42,12 @@ //! display.flush().unwrap(); //! ``` -use hal::{blocking::delay::DelayMs, digital::v2::OutputPin}; - use crate::{ displayrotation::DisplayRotation, interface::DisplayInterface, mode::displaymode::DisplayModeTrait, properties::DisplayProperties, Error, }; +use embedded_graphics_core::{prelude::Point, primitives::Rectangle}; +use hal::{blocking::delay::DelayMs, digital::v2::OutputPin}; const BUFFER_SIZE: usize = 132 * 64 / 8; @@ -226,6 +226,81 @@ where Ok(()) } + + fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> { + let Rectangle { + top_left: Point { x, y }, + size: Size { width, height }, + } = area.intersection(&self.bounding_box()); + // swap coordinates if rotated + let (x, y, width, height) = match self.properties.get_rotation() { + DisplayRotation::Rotate0 | DisplayRotation::Rotate180 => (x, y, width, height), + DisplayRotation::Rotate90 | DisplayRotation::Rotate270 => (y, x, height, width), + }; + + let color = if color.is_on() { 0xff } else { 0x00 }; + + let display_width = self.properties.get_size().dimensions().0 as u32; + + let mut remaining = height; + + let mut block = y as u32 / 8; + + // Top partial row + if y % 8 > 0 { + // Row (aka bit) in this block + let row = y as u32 % 8; + + let mask = (2u8.pow(height) - 1) << row; + + let start = (x as u32 + (block * display_width)) as usize; + + let slice = &mut self.buffer[start..(start + width as usize)]; + + // Write fill color without clobbering existing data in the other bits of the block + slice.iter_mut().for_each(|column| { + *column = (*column & !mask) | (color & mask); + }); + + remaining = remaining.saturating_sub((8 - row).min(height)); + block += 1; + } + + // Full height rows to fill + while remaining > 8 { + let start = (x as u32 + (block * display_width)) as usize; + + let slice = &mut self.buffer[start..(start + width as usize)]; + + slice.fill(color); + + block += 1; + remaining = remaining.saturating_sub(8); + } + + // Bottom partial row + if remaining > 0 { + // Row (aka bit) in this block + let row = 8 - (remaining % 8); + + let mask = (2u8.pow(height) - 1) >> row; + + let start = (x as u32 + (block * display_width)) as usize; + + let slice = &mut self.buffer[start..(start + width as usize)]; + + // Write fill color without clobbering existing data in the other bits of the block + slice.iter_mut().for_each(|column| { + *column = (*column & !mask) | (color & mask); + }); + + remaining = remaining.saturating_sub(8); + } + + debug_assert_eq!(remaining, 0); + + Ok(()) + } } #[cfg(feature = "graphics")]