feat: add display UI with splash screen, dashboard, and RSSI sparkline

Implement full SSD1306 OLED display support:
- Splash screen on boot (1.5s) with project name, version, board
- Dashboard showing radio state, config, counters, RSSI/SNR
- RSSI history sparkline (64-sample ring buffer, 2px-wide bars)
- DisplayOn/DisplayOff host commands routed via dedicated channel
- Pure render functions against generic DrawTarget (no hardware coupling)
- Display on/off control bypasses radio task entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+127
View File
@@ -0,0 +1,127 @@
//! Draw a 1 bit per pixel black and white rust logo to 128x64 SSD1306 displays over both I2C
//! and SPI. This uses async an approach to transfer to i2c and spi simultaniously using DMA
//!
//! This example is for the STM32F103 "Blue Pill" board using I2C1 and SPI1.
//!
//! Wiring connections are as follows for a CRIUS-branded display:
//!
//! ```
//! I2c Display -> Blue Pill
//! GND -> GND
//! +5V -> VCC
//! SDA -> PB7
//! SCL -> PB6
//!
//! SPI display -> Blue Pill
//! GND -> GND
//! 3V3 -> VCC
//! PA5 -> SCL (D0)
//! PA7 -> SDA (D1)
//! PB0 -> RST
//! PB1 -> D/C
//! PB10 -> CS
//! ```
//!
//! Run on a Blue Pill with `cargo run --example async_i2c_spi`.
#![no_std]
#![no_main]
use defmt_rtt as _;
use embassy_executor::Spawner;
use embassy_futures::join::join;
use embassy_stm32::{bind_interrupts, gpio, i2c, peripherals, spi::Spi, time::Hertz, Config};
use embedded_graphics::{
image::{Image, ImageRaw},
pixelcolor::BinaryColor,
prelude::*,
};
use panic_probe as _;
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306Async};
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::EventInterruptHandler<peripherals::I2C1>;
I2C1_ER => i2c::ErrorInterruptHandler<peripherals::I2C1>;
});
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let mut config: Config = Default::default();
config.rcc.hse = Some(embassy_stm32::rcc::Hse {
freq: Hertz::mhz(8),
mode: embassy_stm32::rcc::HseMode::Oscillator,
});
config.rcc.sys = embassy_stm32::rcc::Sysclk::PLL1_P;
config.rcc.pll = Some(embassy_stm32::rcc::Pll {
src: embassy_stm32::rcc::PllSource::HSE,
prediv: embassy_stm32::rcc::PllPreDiv::DIV1,
mul: embassy_stm32::rcc::PllMul::MUL9, // 8 * 9 = 72Mhz
});
// Scale down to 36Mhz (maximum allowed)
config.rcc.apb1_pre = embassy_stm32::rcc::APBPrescaler::DIV2;
let p = embassy_stm32::init(config);
// I2C
let i2c = embassy_stm32::i2c::I2c::new(
p.I2C1,
p.PB6,
p.PB7,
Irqs,
p.DMA1_CH6,
p.DMA1_CH7,
// According to the datasheet the stm32f1xx only supports up to 400khz, but 1mhz seems to
// work just fine. WHen having issues try changing this to Hertz::khz(400).
Hertz::mhz(1),
Default::default(),
);
let interface = I2CDisplayInterface::new(i2c);
let mut display_i2c = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
// SPI
let spi = Spi::new_txonly(p.SPI1, p.PA5, p.PA7, p.DMA1_CH3, Default::default());
let mut rst = gpio::Output::new(p.PB0, gpio::Level::Low, gpio::Speed::Low);
let dc = gpio::Output::new(p.PB1, gpio::Level::Low, gpio::Speed::Low);
let cs = gpio::Output::new(p.PB10, gpio::Level::Low, gpio::Speed::Low);
let spi = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, cs).unwrap();
let interface = SPIInterface::new(spi, dc);
let mut display_spi = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
// Init and reset both displays as needed
join(
async {
display_i2c.init().await.unwrap();
},
async {
display_spi
.reset(&mut rst, &mut embassy_time::Delay {})
.await
.unwrap();
display_spi.init().await.unwrap();
},
)
.await;
let raw: ImageRaw<BinaryColor> = ImageRaw::new(include_bytes!("./rust.raw"), 64);
for i in (0..=64).chain((0..64).rev()).cycle() {
let top_left = Point::new(i, 0);
let im = Image::new(&raw, top_left);
im.draw(&mut display_i2c).unwrap();
im.draw(&mut display_spi).unwrap();
join(async { display_i2c.flush().await.unwrap() }, async {
display_spi.flush().await.unwrap()
})
.await;
display_i2c.clear(BinaryColor::Off).unwrap();
display_spi.clear(BinaryColor::Off).unwrap();
}
}
+64
View File
@@ -0,0 +1,64 @@
//! Endlessly fill the screen with characters from the alphabet
//!
//! 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 -> PB7
//! (green) SCL -> PB6
//! ```
//!
//! Run on a Blue Pill with `cargo run --example async_terminal_i2c`.
#![no_std]
#![no_main]
use defmt_rtt as _;
use embassy_executor::Spawner;
use embassy_stm32::{bind_interrupts, i2c, peripherals, time::Hertz};
use panic_probe as _;
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306Async};
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::EventInterruptHandler<peripherals::I2C1>;
I2C1_ER => i2c::ErrorInterruptHandler<peripherals::I2C1>;
});
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
let i2c = embassy_stm32::i2c::I2c::new(
p.I2C1,
p.PB6,
p.PB7,
Irqs,
p.DMA1_CH6,
p.DMA1_CH7,
Hertz::khz(400),
Default::default(),
);
let interface = I2CDisplayInterface::new(i2c);
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_terminal_mode();
display.init().await.unwrap();
let _ = display.clear().await;
/* Endless loop */
loop {
for c in 97..123 {
let _ = display
.write_str(unsafe { core::str::from_utf8_unchecked(&[c]) })
.await;
}
for c in 65..91 {
let _ = display
.write_str(unsafe { core::str::from_utf8_unchecked(&[c]) })
.await;
}
}
}
+86
View File
@@ -0,0 +1,86 @@
//! Draw an RGB565 BMP image onto the display by converting the `Rgb565` pixel color type to
//! `BinaryColor` using a simple threshold where any pixel with a value greater than zero is treated
//! as "on".
//!
//! Note that the `bmp` feature for `embedded-graphics` must be turned on.
//!
//! 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 -> PB7
//! (green) SCL -> PB6
//! ```
//!
//! Run on a Blue Pill with `cargo run --example image_i2c`.
#![no_std]
#![no_main]
use cortex_m::asm::nop;
use cortex_m_rt::entry;
use defmt_rtt as _;
use embassy_stm32::time::Hertz;
#[cfg(feature = "async")]
use embassy_stm32::{bind_interrupts, i2c, peripherals};
use embedded_graphics::{image::Image, pixelcolor::Rgb565, prelude::*};
use panic_probe as _;
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
use tinybmp::Bmp;
#[entry]
fn main() -> ! {
let p: embassy_stm32::Peripherals = embassy_stm32::init(Default::default());
#[cfg(feature = "async")]
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::EventInterruptHandler<peripherals::I2C1>;
I2C1_ER => i2c::ErrorInterruptHandler<peripherals::I2C1>;
});
#[cfg(feature = "async")]
let i2c = embassy_stm32::i2c::I2c::new(
p.I2C1,
p.PB6,
p.PB7,
Irqs,
p.DMA1_CH6,
p.DMA1_CH7,
Hertz::khz(400),
Default::default(),
);
#[cfg(not(feature = "async"))]
let i2c = embassy_stm32::i2c::I2c::new_blocking(
p.I2C1,
p.PB6,
p.PB7,
Hertz::khz(400),
Default::default(),
);
let interface = I2CDisplayInterface::new(i2c);
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
display.init().unwrap();
let bmp = Bmp::from_slice(include_bytes!("./rust.bmp")).expect("Failed to load BMP image");
// The image is an RGB565 encoded BMP, so specifying the type as `Image<Bmp<Rgb565>>` will read
// the pixels correctly
let im: Image<Bmp<Rgb565>> = Image::new(&bmp, Point::new(32, 0));
// We use the `color_converted` method here to automatically convert the RGB565 image data into
// BinaryColor values.
im.draw(&mut display.color_converted()).unwrap();
display.flush().unwrap();
loop {
nop()
}
}
+103
View File
@@ -0,0 +1,103 @@
//! Draw a square, circle and triangle on the screen using the embedded_graphics library over a 4
//! wire SPI interface.
//!
//! This example is for the STM32F103 "Blue Pill" board using a 4 wire interface to the display on
//! SPI1.
//!
//! Wiring connections are as follows
//!
//! ```
//! GND -> GND
//! 3V3 -> VCC
//! PA5 -> SCL (D0)
//! PA7 -> SDA (D1)
//! PB0 -> RST
//! PB1 -> D/C
//! PB10 -> CS
//! ```
//!
//! Run on a Blue Pill with `cargo run --example graphics`.
#![no_std]
#![no_main]
use cortex_m::asm::nop;
use cortex_m_rt::entry;
use defmt_rtt as _;
use embassy_stm32::{
gpio,
spi::{self, Spi},
time::Hertz,
};
use embedded_graphics::{
pixelcolor::BinaryColor,
prelude::*,
primitives::{Circle, PrimitiveStyleBuilder, Rectangle, Triangle},
};
use panic_probe as _;
use ssd1306::{prelude::*, Ssd1306};
#[entry]
fn main() -> ! {
let p = embassy_stm32::init(Default::default());
let mut config = spi::Config::default();
config.frequency = Hertz::mhz(8);
let spi = Spi::new_blocking_txonly(p.SPI1, p.PA5, p.PA7, config);
let mut rst = gpio::Output::new(p.PB0, gpio::Level::Low, gpio::Speed::Low);
let dc = gpio::Output::new(p.PB1, gpio::Level::Low, gpio::Speed::Low);
let cs = gpio::Output::new(p.PB10, gpio::Level::Low, gpio::Speed::Low);
let spi = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, cs).unwrap();
let interface = display_interface_spi::SPIInterface::new(spi, dc);
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
display
.reset(&mut rst, &mut embassy_time::Delay {})
.unwrap();
display.init().unwrap();
let yoffset = 20;
let style = PrimitiveStyleBuilder::new()
.stroke_width(1)
.stroke_color(BinaryColor::On)
.build();
// screen outline
// default display size is 128x64 if you don't pass a _DisplaySize_
// enum to the _Builder_ struct
Rectangle::new(Point::new(0, 0), Size::new(127, 63))
.into_styled(style)
.draw(&mut display)
.unwrap();
// triangle
Triangle::new(
Point::new(16, 16 + yoffset),
Point::new(16 + 16, 16 + yoffset),
Point::new(16 + 8, yoffset),
)
.into_styled(style)
.draw(&mut display)
.unwrap();
// square
Rectangle::new(Point::new(52, yoffset), Size::new_equal(16))
.into_styled(style)
.draw(&mut display)
.unwrap();
// circle
Circle::new(Point::new(88, yoffset), 16)
.into_styled(style)
.draw(&mut display)
.unwrap();
display.flush().unwrap();
loop {
nop()
}
}
+111
View File
@@ -0,0 +1,111 @@
//! Draw a square, circle and triangle on the screen using the `embedded_graphics` crate.
//!
//! 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 -> PB7
//! (green) SCL -> PB6
//! ```
//!
//! Run on a Blue Pill with `cargo run --example graphics_i2c`.
#![no_std]
#![no_main]
use cortex_m::asm::nop;
use cortex_m_rt::entry;
use defmt_rtt as _;
use embassy_stm32::time::Hertz;
#[cfg(feature = "async")]
use embassy_stm32::{bind_interrupts, i2c, peripherals};
use embedded_graphics::{
pixelcolor::BinaryColor,
prelude::*,
primitives::{Circle, PrimitiveStyleBuilder, Rectangle, Triangle},
};
use panic_probe as _;
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
#[entry]
fn main() -> ! {
let p = embassy_stm32::init(Default::default());
#[cfg(feature = "async")]
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::EventInterruptHandler<peripherals::I2C1>;
I2C1_ER => i2c::ErrorInterruptHandler<peripherals::I2C1>;
});
#[cfg(feature = "async")]
let i2c = embassy_stm32::i2c::I2c::new(
p.I2C1,
p.PB6,
p.PB7,
Irqs,
p.DMA1_CH6,
p.DMA1_CH7,
Hertz::khz(400),
Default::default(),
);
#[cfg(not(feature = "async"))]
let i2c = embassy_stm32::i2c::I2c::new_blocking(
p.I2C1,
p.PB6,
p.PB7,
Hertz::khz(400),
Default::default(),
);
let interface = I2CDisplayInterface::new(i2c);
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
display.init().unwrap();
let yoffset = 20;
let style = PrimitiveStyleBuilder::new()
.stroke_width(1)
.stroke_color(BinaryColor::On)
.build();
// screen outline
// default display size is 128x64 if you don't pass a _DisplaySize_
// enum to the _Builder_ struct
Rectangle::new(Point::new(0, 0), Size::new(127, 63))
.into_styled(style)
.draw(&mut display)
.unwrap();
// triangle
Triangle::new(
Point::new(16, 16 + yoffset),
Point::new(16 + 16, 16 + yoffset),
Point::new(16 + 8, yoffset),
)
.into_styled(style)
.draw(&mut display)
.unwrap();
// square
Rectangle::new(Point::new(52, yoffset), Size::new_equal(16))
.into_styled(style)
.draw(&mut display)
.unwrap();
// circle
Circle::new(Point::new(88, yoffset), 16)
.into_styled(style)
.draw(&mut display)
.unwrap();
display.flush().unwrap();
loop {
nop()
}
}
+111
View File
@@ -0,0 +1,111 @@
//! Draw a square, circle and triangle on a 128x32px display.
//!
//! 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 -> PB7
//! (green) SCL -> PB6
//! ```
//!
//! Run on a Blue Pill with `cargo run --example graphics_i2c_128x32`.
#![no_std]
#![no_main]
use cortex_m::asm::nop;
use cortex_m_rt::entry;
use defmt_rtt as _;
use embassy_stm32::time::Hertz;
#[cfg(feature = "async")]
use embassy_stm32::{bind_interrupts, i2c, peripherals};
use embedded_graphics::{
pixelcolor::BinaryColor,
prelude::*,
primitives::{Circle, PrimitiveStyleBuilder, Rectangle, Triangle},
};
use panic_probe as _;
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
#[entry]
fn main() -> ! {
let p = embassy_stm32::init(Default::default());
#[cfg(feature = "async")]
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::EventInterruptHandler<peripherals::I2C1>;
I2C1_ER => i2c::ErrorInterruptHandler<peripherals::I2C1>;
});
#[cfg(feature = "async")]
let i2c = embassy_stm32::i2c::I2c::new(
p.I2C1,
p.PB6,
p.PB7,
Irqs,
p.DMA1_CH6,
p.DMA1_CH7,
Hertz::khz(400),
Default::default(),
);
#[cfg(not(feature = "async"))]
let i2c = embassy_stm32::i2c::I2c::new_blocking(
p.I2C1,
p.PB6,
p.PB7,
Hertz::khz(400),
Default::default(),
);
let interface = I2CDisplayInterface::new(i2c);
let mut display = Ssd1306::new(interface, DisplaySize128x32, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
display.init().unwrap();
let yoffset = 8;
let style = PrimitiveStyleBuilder::new()
.stroke_width(1)
.stroke_color(BinaryColor::On)
.build();
// screen outline
// default display size is 128x64 if you don't pass a _DisplaySize_
// enum to the _Builder_ struct
Rectangle::new(Point::new(0, 0), Size::new(127, 31))
.into_styled(style)
.draw(&mut display)
.unwrap();
// triangle
Triangle::new(
Point::new(16, 16 + yoffset),
Point::new(16 + 16, 16 + yoffset),
Point::new(16 + 8, yoffset),
)
.into_styled(style)
.draw(&mut display)
.unwrap();
// square
Rectangle::new(Point::new(52, yoffset), Size::new_equal(16))
.into_styled(style)
.draw(&mut display)
.unwrap();
// circle
Circle::new(Point::new(88, yoffset), 16)
.into_styled(style)
.draw(&mut display)
.unwrap();
display.flush().unwrap();
loop {
nop()
}
}
+122
View File
@@ -0,0 +1,122 @@
//! Draw a square, circle and triangle on the screen using the `embedded_graphics` crate.
//!
//! 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 -> PB7
//! (green) SCL -> PB6
//! ```
//!
//! Run on a Blue Pill with `cargo run --example graphics_i2c`.
#![no_std]
#![no_main]
use cortex_m::asm::nop;
use cortex_m_rt::entry;
use defmt_rtt as _;
use embassy_stm32::time::Hertz;
#[cfg(feature = "async")]
use embassy_stm32::{bind_interrupts, i2c, peripherals};
use embedded_graphics::{
pixelcolor::BinaryColor,
prelude::*,
primitives::{Circle, PrimitiveStyleBuilder, Rectangle, Triangle},
};
use panic_probe as _;
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
#[entry]
fn main() -> ! {
let p = embassy_stm32::init(Default::default());
#[cfg(feature = "async")]
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::EventInterruptHandler<peripherals::I2C1>;
I2C1_ER => i2c::ErrorInterruptHandler<peripherals::I2C1>;
});
#[cfg(feature = "async")]
let i2c = embassy_stm32::i2c::I2c::new(
p.I2C1,
p.PB6,
p.PB7,
Irqs,
p.DMA1_CH6,
p.DMA1_CH7,
Hertz::khz(400),
Default::default(),
);
#[cfg(not(feature = "async"))]
let i2c = embassy_stm32::i2c::I2c::new_blocking(
p.I2C1,
p.PB6,
p.PB7,
Hertz::khz(400),
Default::default(),
);
let interface = I2CDisplayInterface::new(i2c);
let mut display = Ssd1306::new(interface, DisplaySize72x40, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
display.init().unwrap();
let size = 10;
let offset = Point::new(10, (42 / 2) - (size / 2) - 1);
let spacing = size + 10;
let style = PrimitiveStyleBuilder::new()
.stroke_width(1)
.stroke_color(BinaryColor::On)
.build();
// screen outline
// default display size is 128x64 if you don't pass a _DisplaySize_
// enum to the _Builder_ struct
Rectangle::with_corners(Point::new(0, 0), Point::new(71, 39))
.into_styled(style)
.draw(&mut display)
.unwrap();
// Triangle
Triangle::new(
Point::new(0, size),
Point::new(size / 2, 0),
Point::new(size, size),
)
.translate(offset)
.into_styled(style)
.draw(&mut display)
.unwrap();
// Move over to next position
let offset = offset + Point::new(spacing, 0);
// Draw a square
Rectangle::new(Point::new(0, 0), Size::new_equal(size as u32))
.translate(offset)
.into_styled(style)
.draw(&mut display)
.unwrap();
// Move over a bit more
let offset = offset + Point::new(spacing, 0);
// Circle
Circle::new(Point::zero(), size as u32)
.translate(offset)
.into_styled(style)
.draw(&mut display)
.unwrap();
display.flush().unwrap();
loop {
nop()
}
}
+87
View File
@@ -0,0 +1,87 @@
//! Draw a 1 bit per pixel black and white image. On a 128x64 SSD1306 display over I2C.
//!
//! Image was created with ImageMagick:
//!
//! ```bash
//! convert rust.png -depth 1 gray:rust.raw
//! ```
//!
//! 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 -> PB7
//! (green) SCL -> PB6
//! ```
//!
//! Run on a Blue Pill with `cargo run --example image_i2c`.
#![no_std]
#![no_main]
use cortex_m::asm::nop;
use cortex_m_rt::entry;
use defmt_rtt as _;
#[cfg(feature = "async")]
use embassy_stm32::{bind_interrupts, i2c, peripherals};
use embassy_stm32::time::Hertz;
use embedded_graphics::{
image::{Image, ImageRaw},
pixelcolor::BinaryColor,
prelude::*,
};
use panic_probe as _;
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
#[entry]
fn main() -> ! {
let p = embassy_stm32::init(Default::default());
#[cfg(feature = "async")]
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::EventInterruptHandler<peripherals::I2C1>;
I2C1_ER => i2c::ErrorInterruptHandler<peripherals::I2C1>;
});
#[cfg(feature = "async")]
let i2c = embassy_stm32::i2c::I2c::new(
p.I2C1,
p.PB6,
p.PB7,
Irqs,
p.DMA1_CH6,
p.DMA1_CH7,
Hertz::khz(400),
Default::default(),
);
#[cfg(not(feature = "async"))]
let i2c = embassy_stm32::i2c::I2c::new_blocking(
p.I2C1,
p.PB6,
p.PB7,
Hertz::khz(400),
Default::default(),
);
let interface = I2CDisplayInterface::new(i2c);
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
display.init().unwrap();
let raw: ImageRaw<BinaryColor> = ImageRaw::new(include_bytes!("./rust.raw"), 64);
let im = Image::new(&raw, Point::new(32, 0));
im.draw(&mut display).unwrap();
display.flush().unwrap();
loop {
nop()
}
}
+75
View File
@@ -0,0 +1,75 @@
//! Send random raw data to the display, emulating an old untuned TV. This example retrieves the
//! underlying display properties struct and allows calling of the low-level `draw()` method,
//! sending a 1024 byte buffer straight to the display.
//!
//! 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 -> PB7
//! (green) SCL -> PB6
//! ```
//!
//! Run on a Blue Pill with `cargo run --example noise_i2c`. Best results when using `--release`.
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use defmt_rtt as _;
#[cfg(feature = "async")]
use embassy_stm32::{bind_interrupts, i2c, peripherals};
use embassy_stm32::time::Hertz;
use panic_probe as _;
use rand::prelude::*;
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
#[entry]
fn main() -> ! {
let p = embassy_stm32::init(Default::default());
#[cfg(feature = "async")]
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::EventInterruptHandler<peripherals::I2C1>;
I2C1_ER => i2c::ErrorInterruptHandler<peripherals::I2C1>;
});
#[cfg(feature = "async")]
let i2c = embassy_stm32::i2c::I2c::new(
p.I2C1,
p.PB6,
p.PB7,
Irqs,
p.DMA1_CH6,
p.DMA1_CH7,
Hertz::khz(400),
Default::default(),
);
#[cfg(not(feature = "async"))]
let i2c = embassy_stm32::i2c::I2c::new_blocking(
p.I2C1,
p.PB6,
p.PB7,
Hertz::khz(400),
Default::default(),
);
let interface = I2CDisplayInterface::new(i2c);
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0);
display.init().unwrap();
let mut buf = [0x00u8; 1024];
let mut rng = SmallRng::seed_from_u64(0xdead_beef_cafe_d00d);
loop {
rng.fill_bytes(&mut buf);
display.draw(&buf).unwrap();
}
}
+86
View File
@@ -0,0 +1,86 @@
//! This example draws a small square one pixel at a time in the top left corner of the display
//!
//! You will probably want to use the [`embedded_graphics`](https://crates.io/crates/embedded-graphics) crate to do more complex drawing.
//!
//! This example is for the STM32F103 "Blue Pill" board using a 4 wire interface to the display on
//! SPI1.
//!
//! Wiring connections are as follows
//!
//! ```
//! GND -> GND
//! 3V3 -> VCC
//! PA5 -> SCL (D0)
//! PA7 -> SDA (D1)
//! PB0 -> RST
//! PB1 -> D/C
//! PB10 -> CS
//! ```
//!
//! Run on a Blue Pill with `cargo run --example pixelsquare`.
#![no_std]
#![no_main]
use cortex_m::asm::nop;
use cortex_m_rt::entry;
use defmt_rtt as _;
use embassy_stm32::{
gpio,
spi::{self, Spi},
time::Hertz,
};
use panic_probe as _;
use ssd1306::{prelude::*, Ssd1306};
#[entry]
fn main() -> ! {
let p = embassy_stm32::init(Default::default());
let mut config = spi::Config::default();
config.frequency = Hertz::mhz(8);
let spi = Spi::new_blocking_txonly(p.SPI1, p.PA5, p.PA7, config);
let mut rst = gpio::Output::new(p.PB0, gpio::Level::Low, gpio::Speed::Low);
let dc = gpio::Output::new(p.PB1, gpio::Level::Low, gpio::Speed::Low);
let cs = gpio::Output::new(p.PB10, gpio::Level::Low, gpio::Speed::Low);
let spi = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, cs).unwrap();
let interface = display_interface_spi::SPIInterface::new(spi, dc);
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
display
.reset(&mut rst, &mut embassy_time::Delay {})
.unwrap();
display.init().unwrap();
// Top side
display.set_pixel(0, 0, true);
display.set_pixel(1, 0, true);
display.set_pixel(2, 0, true);
display.set_pixel(3, 0, true);
// Right side
display.set_pixel(3, 0, true);
display.set_pixel(3, 1, true);
display.set_pixel(3, 2, true);
display.set_pixel(3, 3, true);
// Bottom side
display.set_pixel(0, 3, true);
display.set_pixel(1, 3, true);
display.set_pixel(2, 3, true);
display.set_pixel(3, 3, true);
// Left side
display.set_pixel(0, 0, true);
display.set_pixel(0, 1, true);
display.set_pixel(0, 2, true);
display.set_pixel(0, 3, true);
display.flush().unwrap();
loop {
nop()
}
}
+95
View File
@@ -0,0 +1,95 @@
//! Draw the Rust logo centered on a 90 degree rotated 128x64px display
//!
//! Image was created with ImageMagick:
//!
//! ```bash
//! convert rust.png -depth 1 gray:rust.raw
//! ```
//!
//! 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 -> PB7
//! (green) SCL -> PB6
//! ```
//!
//! Run on a Blue Pill with `cargo run --example rotation_i2c`.
#![no_std]
#![no_main]
use cortex_m::asm::nop;
use cortex_m_rt::entry;
use defmt_rtt as _;
use embassy_stm32::time::Hertz;
#[cfg(feature = "async")]
use embassy_stm32::{bind_interrupts, i2c, peripherals};
use embedded_graphics::{
image::{Image, ImageRaw},
pixelcolor::BinaryColor,
prelude::*,
};
use panic_probe as _;
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
#[entry]
fn main() -> ! {
let p = embassy_stm32::init(Default::default());
#[cfg(feature = "async")]
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::EventInterruptHandler<peripherals::I2C1>;
I2C1_ER => i2c::ErrorInterruptHandler<peripherals::I2C1>;
});
#[cfg(feature = "async")]
let i2c = embassy_stm32::i2c::I2c::new(
p.I2C1,
p.PB6,
p.PB7,
Irqs,
p.DMA1_CH6,
p.DMA1_CH7,
Hertz::khz(400),
Default::default(),
);
#[cfg(not(feature = "async"))]
let i2c = embassy_stm32::i2c::I2c::new_blocking(
p.I2C1,
p.PB6,
p.PB7,
Hertz::khz(400),
Default::default(),
);
let interface = I2CDisplayInterface::new(i2c);
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate90)
.into_buffered_graphics_mode();
display.init().unwrap();
// Contrived example to test builder and instance methods. Sets rotation to 270 degress
// or 90 degress counterclockwise
display.set_rotation(DisplayRotation::Rotate270).unwrap();
let (w, h) = display.dimensions();
let raw: ImageRaw<BinaryColor> = ImageRaw::new(include_bytes!("./rust.raw"), 64);
let im = Image::new(
&raw,
Point::new(w as i32 / 2 - 64 / 2, h as i32 / 2 - 64 / 2),
);
im.draw(&mut display).unwrap();
display.flush().unwrap();
loop {
nop()
}
}
+186
View File
@@ -0,0 +1,186 @@
//! Cycle the screen brightness through 5 predefined brightness levels when the "DVD" logo hits one
//! of the sides of the display.
//!
//! For best results, run with the `--release` flag.
#![no_std]
#![no_main]
pub mod pac {
pub use embassy_stm32::pac::Interrupt as interrupt;
pub use embassy_stm32::pac::*;
}
#[rtic::app(device = crate::pac, peripherals= false, dispatchers = [EXTI0])]
mod app {
use defmt_rtt as _;
use display_interface_spi::SPIInterface;
use embassy_stm32::{
gpio,
mode::Blocking,
spi::{self, Spi},
time::Hertz,
timer::low_level::Timer,
Config,
};
use embedded_graphics::{
geometry::Point,
image::Image,
pixelcolor::{BinaryColor, Rgb565},
prelude::*,
primitives::{PrimitiveStyle, Rectangle},
};
use panic_probe as _;
use ssd1306::{mode::BufferedGraphicsMode, prelude::*, Ssd1306};
use tinybmp::Bmp;
type Display = Ssd1306<
SPIInterface<
embedded_hal_bus::spi::ExclusiveDevice<
Spi<'static, Blocking>,
gpio::Output<'static>,
embedded_hal_bus::spi::NoDelay,
>,
gpio::Output<'static>,
>,
DisplaySize128x64,
BufferedGraphicsMode<DisplaySize128x64>,
>;
#[shared]
struct SharedResources {}
#[local]
struct Resources {
display: Display,
timer: Timer<'static, embassy_stm32::peripherals::TIM1>,
top_left: Point,
velocity: Point,
bmp: Bmp<Rgb565, 'static>,
brightness: Brightness,
}
#[init]
fn init(_cx: init::Context) -> (SharedResources, Resources, init::Monotonics) {
let mut config: Config = Default::default();
config.rcc.hse = Some(embassy_stm32::rcc::Hse {
freq: Hertz::mhz(8),
mode: embassy_stm32::rcc::HseMode::Oscillator,
});
config.rcc.sys = embassy_stm32::rcc::Sysclk::PLL1_P;
config.rcc.pll = Some(embassy_stm32::rcc::Pll {
src: embassy_stm32::rcc::PllSource::HSE,
prediv: embassy_stm32::rcc::PllPreDiv::DIV1,
mul: embassy_stm32::rcc::PllMul::MUL9, // 8 * 9 = 72Mhz
});
// Scale down to 36Mhz (maximum allowed)
config.rcc.apb1_pre = embassy_stm32::rcc::APBPrescaler::DIV2;
let p = embassy_stm32::init(config);
let mut config = spi::Config::default();
config.frequency = Hertz::mhz(8);
let spi = Spi::new_blocking_txonly(p.SPI1, p.PA5, p.PA7, config);
let mut rst = gpio::Output::new(p.PB0, gpio::Level::Low, gpio::Speed::Low);
let dc = gpio::Output::new(p.PB1, gpio::Level::Low, gpio::Speed::Low);
let cs = gpio::Output::new(p.PB10, gpio::Level::Low, gpio::Speed::Low);
let spi = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, cs).unwrap();
let interface = display_interface_spi::SPIInterface::new(spi, dc);
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate180)
.into_buffered_graphics_mode();
display
.reset(&mut rst, &mut embassy_time::Delay {})
.unwrap();
display.init().unwrap();
// Forget the RST pin to keep the display out of reset
core::mem::forget(rst);
// Update framerate
let timer = Timer::new(p.TIM1);
timer.set_frequency(Hertz(20)); // 20 FPS
timer.enable_update_interrupt(true);
timer.start();
let bmp = Bmp::from_slice(include_bytes!("dvd.bmp")).unwrap();
// Init the static resources to use them later through RTIC
(
SharedResources {},
Resources {
timer,
display,
top_left: Point::new(5, 3),
velocity: Point::new(1, 1),
bmp,
brightness: Brightness::default(),
},
init::Monotonics(),
)
}
#[task(binds = TIM1_UP, local = [display, top_left, velocity, timer, bmp, brightness])]
fn update(cx: update::Context) {
let update::LocalResources {
display,
top_left,
velocity,
timer,
bmp,
brightness,
..
} = cx.local;
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
{
let mut collision = false;
if bottom_right.x > display.size().width as i32 || top_left.x < 0 {
velocity.x = -velocity.x;
collision = true;
}
if bottom_right.y > display.size().height as i32 || top_left.y < 0 {
velocity.y = -velocity.y;
collision = true;
}
if collision {
// Change the brightness
*brightness = match *brightness {
Brightness::DIMMEST => Brightness::DIM,
Brightness::DIM => Brightness::NORMAL,
Brightness::NORMAL => Brightness::BRIGHT,
Brightness::BRIGHT => Brightness::BRIGHTEST,
Brightness::BRIGHTEST => Brightness::DIMMEST, // restart cycle
_ => Brightness::NORMAL, // Brightness is not an enum, cover rest of patterns
};
// Send the new brightness value to the display
display.set_brightness(*brightness).unwrap();
}
}
// 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();
}
}
+167
View File
@@ -0,0 +1,167 @@
//! Bounce a DVD player logo around the screen
//!
//! Like this, but with no color changing: https://bouncingdvdlogo.com/
//!
//! For best results, run with the `--release` flag.
#![no_std]
#![no_main]
pub mod pac {
pub use embassy_stm32::pac::Interrupt as interrupt;
pub use embassy_stm32::pac::*;
}
#[rtic::app(device = crate::pac, peripherals= false, dispatchers = [EXTI0])]
mod app {
use defmt_rtt as _;
use display_interface_spi::SPIInterface;
use embassy_stm32::{
gpio,
mode::Blocking,
spi::{self, Spi},
time::Hertz,
timer::low_level::Timer,
Config,
};
use embedded_graphics::{
geometry::Point,
image::Image,
pixelcolor::{BinaryColor, Rgb565},
prelude::*,
primitives::{PrimitiveStyle, Rectangle},
};
use panic_probe as _;
use ssd1306::{mode::BufferedGraphicsMode, prelude::*, Ssd1306};
use tinybmp::Bmp;
type Display = Ssd1306<
SPIInterface<
embedded_hal_bus::spi::ExclusiveDevice<
Spi<'static, Blocking>,
gpio::Output<'static>,
embedded_hal_bus::spi::NoDelay,
>,
gpio::Output<'static>,
>,
DisplaySize128x64,
BufferedGraphicsMode<DisplaySize128x64>,
>;
#[shared]
struct SharedResources {}
#[local]
struct Resources {
display: Display,
timer: Timer<'static, embassy_stm32::peripherals::TIM1>,
top_left: Point,
velocity: Point,
bmp: Bmp<Rgb565, 'static>,
}
#[init]
fn init(_cx: init::Context) -> (SharedResources, Resources, init::Monotonics) {
let mut config: Config = Default::default();
config.rcc.hse = Some(embassy_stm32::rcc::Hse {
freq: Hertz::mhz(8),
mode: embassy_stm32::rcc::HseMode::Oscillator,
});
config.rcc.sys = embassy_stm32::rcc::Sysclk::PLL1_P;
config.rcc.pll = Some(embassy_stm32::rcc::Pll {
src: embassy_stm32::rcc::PllSource::HSE,
prediv: embassy_stm32::rcc::PllPreDiv::DIV1,
mul: embassy_stm32::rcc::PllMul::MUL9, // 8 * 9 = 72Mhz
});
// Scale down to 36Mhz (maximum allowed)
config.rcc.apb1_pre = embassy_stm32::rcc::APBPrescaler::DIV2;
let p = embassy_stm32::init(config);
let mut config = spi::Config::default();
config.frequency = Hertz::mhz(8);
let spi = Spi::new_blocking_txonly(p.SPI1, p.PA5, p.PA7, config);
let mut rst = gpio::Output::new(p.PB0, gpio::Level::Low, gpio::Speed::Low);
let dc = gpio::Output::new(p.PB1, gpio::Level::Low, gpio::Speed::Low);
let cs = gpio::Output::new(p.PB10, gpio::Level::Low, gpio::Speed::Low);
let spi = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, cs).unwrap();
let interface = display_interface_spi::SPIInterface::new(spi, dc);
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate180)
.into_buffered_graphics_mode();
display
.reset(&mut rst, &mut embassy_time::Delay {})
.unwrap();
display.init().unwrap();
// Forget the RST pin to keep the display out of reset
core::mem::forget(rst);
// Update framerate
let timer = Timer::new(p.TIM1);
timer.set_frequency(Hertz(20)); // 20 FPS
timer.enable_update_interrupt(true);
timer.start();
let bmp = Bmp::from_slice(include_bytes!("dvd.bmp")).unwrap();
// Init the static resources to use them later through RTIC
(
SharedResources {},
Resources {
timer,
display,
top_left: Point::new(5, 3),
velocity: Point::new(1, 1),
bmp,
},
init::Monotonics(),
)
}
#[task(binds = TIM1_UP, local = [display, top_left, velocity, timer, bmp ])]
fn update(cx: update::Context) {
let update::LocalResources {
display,
top_left,
velocity,
timer,
bmp,
..
} = cx.local;
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();
}
}
Binary file not shown.
+74
View File
@@ -0,0 +1,74 @@
//! Endlessly fill the screen with characters from the alphabet
//!
//! 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 -> PB7
//! (green) SCL -> PB6
//! ```
//!
//! Run on a Blue Pill with `cargo run --example terminal_i2c`.
#![no_std]
#![no_main]
use core::fmt::Write;
use cortex_m_rt::entry;
use defmt_rtt as _;
use embassy_stm32::time::Hertz;
#[cfg(feature = "async")]
use embassy_stm32::{bind_interrupts, i2c, peripherals};
use panic_probe as _;
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
#[entry]
fn main() -> ! {
let p = embassy_stm32::init(Default::default());
#[cfg(feature = "async")]
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::EventInterruptHandler<peripherals::I2C1>;
I2C1_ER => i2c::ErrorInterruptHandler<peripherals::I2C1>;
});
#[cfg(feature = "async")]
let i2c = embassy_stm32::i2c::I2c::new(
p.I2C1,
p.PB6,
p.PB7,
Irqs,
p.DMA1_CH6,
p.DMA1_CH7,
Hertz::khz(400),
Default::default(),
);
#[cfg(not(feature = "async"))]
let i2c = embassy_stm32::i2c::I2c::new_blocking(
p.I2C1,
p.PB6,
p.PB7,
Hertz::khz(400),
Default::default(),
);
let interface = I2CDisplayInterface::new(i2c);
let mut display =
Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0).into_terminal_mode();
display.init().unwrap();
let _ = display.clear();
/* Endless loop */
loop {
for c in 97..123 {
let _ = display.write_str(unsafe { core::str::from_utf8_unchecked(&[c]) });
}
for c in 65..91 {
let _ = display.write_str(unsafe { core::str::from_utf8_unchecked(&[c]) });
}
}
}
+88
View File
@@ -0,0 +1,88 @@
//! Print "Hello world!" with "Hello rust!" underneath. Uses the `embedded_graphics` crate to draw
//! the text with a 6x10 pixel font.
//!
//! 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 -> PB7
//! (green) SCL -> PB6
//! ```
//!
//! Run on a Blue Pill with `cargo run --example text_i2c`.
#![no_std]
#![no_main]
use cortex_m::asm::nop;
use cortex_m_rt::entry;
use defmt_rtt as _;
use embassy_stm32::time::Hertz;
#[cfg(feature = "async")]
use embassy_stm32::{bind_interrupts, i2c, peripherals};
use embedded_graphics::{
mono_font::{ascii::FONT_6X10, MonoTextStyleBuilder},
pixelcolor::BinaryColor,
prelude::*,
text::{Baseline, Text},
};
use panic_probe as _;
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
#[entry]
fn main() -> ! {
let p = embassy_stm32::init(Default::default());
#[cfg(feature = "async")]
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::EventInterruptHandler<peripherals::I2C1>;
I2C1_ER => i2c::ErrorInterruptHandler<peripherals::I2C1>;
});
#[cfg(feature = "async")]
let i2c = embassy_stm32::i2c::I2c::new(
p.I2C1,
p.PB6,
p.PB7,
Irqs,
p.DMA1_CH6,
p.DMA1_CH7,
Hertz::khz(400),
Default::default(),
);
#[cfg(not(feature = "async"))]
let i2c = embassy_stm32::i2c::I2c::new_blocking(
p.I2C1,
p.PB6,
p.PB7,
Hertz::khz(400),
Default::default(),
);
let interface = I2CDisplayInterface::new(i2c);
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
display.init().unwrap();
let text_style = MonoTextStyleBuilder::new()
.font(&FONT_6X10)
.text_color(BinaryColor::On)
.build();
Text::with_baseline("Hello world!", Point::zero(), text_style, Baseline::Top)
.draw(&mut display)
.unwrap();
Text::with_baseline("Hello Rust!", Point::new(0, 16), text_style, Baseline::Top)
.draw(&mut display)
.unwrap();
display.flush().unwrap();
loop {
nop()
}
}