Compare commits

...

10 Commits

Author SHA1 Message Date
8643aa6fcc Update demonstration 2025-09-15 14:17:10 +03:00
ad1941c798 Add a dice-game with 10-sides and cat-prize for 10 2025-09-13 20:17:39 +03:00
0be133e40a Visualize all inputs 2025-09-13 19:37:06 +03:00
5c54d2d48c Create a proto-game 2025-09-13 19:20:30 +03:00
df146d7c0a Add formatting for numbers too 2025-09-13 19:10:45 +03:00
c78db0c81a Store alphabet in progmem data 2025-09-13 18:43:57 +03:00
9bb7af612c Improve font rendering 2025-09-13 15:12:10 +03:00
0806f9803e Format overflowing words to go to next line 2025-09-13 15:06:02 +03:00
3a827cfde7 Add simple text rendering 2025-09-13 15:00:42 +03:00
549ebdc37c Add alphabet 2025-09-13 14:46:14 +03:00
7 changed files with 511 additions and 40 deletions

View File

@ -1,10 +1,16 @@
# Slideshow # Dice game
This project is a relatively simple slideshow running on the Adafruit Feather This project is a simple dice-game running on the Adafruit Feather 328P (an
328P (an ATmega328p-microcontroller) using a simple button and a 240x240 LCD ATmega328p-microcontroller) using a simple button and a 240x240 LCD display with
display with the ST7789-driver over the ST7789-driver over
[SPI](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface). [SPI](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface).
In the game you roll a number between 1 and 10, and if you get a 10, you are
awarded a picture of a high-quality cat stored in program-memory!
This project is a direct continuation of my earlier
[slideshow](https://git.teascade.net/teascade/slideshow)
This project is written on Rust and uses a very minimal amount of libraries with This project is written on Rust and uses a very minimal amount of libraries with
the most significant being [`atmega-hal`](https://github.com/Rahix/avr-hal), a the most significant being [`atmega-hal`](https://github.com/Rahix/avr-hal), a
hardware abstraction layer for ATmega-microcontrollers. Images are stored in the hardware abstraction layer for ATmega-microcontrollers. Images are stored in the

Binary file not shown.

View File

@ -43,6 +43,14 @@ impl Rgb565 {
Rgb565(0, 255, 255) Rgb565(0, 255, 255)
} }
pub fn black() -> Rgb565 {
Rgb565(0, 0, 0)
}
pub fn white() -> Rgb565 {
Rgb565(255, 255, 255)
}
pub fn qoi_hash(&self) -> usize { pub fn qoi_hash(&self) -> usize {
((self.0 as u32 * 3 + self.1 as u32 * 5 + self.2 as u32 * 7 + 255 * 11) % 64) as usize ((self.0 as u32 * 3 + self.1 as u32 * 5 + self.2 as u32 * 7 + 255 * 11) % 64) as usize
} }
@ -66,6 +74,17 @@ pub struct Vec2 {
pub y: u16, pub y: u16,
} }
impl Mul<u16> for Vec2 {
type Output = Vec2;
fn mul(self, rhs: u16) -> Self::Output {
Vec2 {
x: self.x * rhs,
y: self.y * rhs,
}
}
}
#[derive(Default, Clone, Copy)] #[derive(Default, Clone, Copy)]
pub struct Color { pub struct Color {
pub bytes: [u8; 2], pub bytes: [u8; 2],

View File

@ -1,9 +1,375 @@
fn letter(character: char) -> [u8; 8] { use core::{
arch::asm,
ptr::{addr_of, null},
};
use atmega_hal::port::PinOps;
use embedded_hal::delay::DelayNs;
use crate::{
display::{Display, Rgb565, Vec2},
graphics::draw_stream,
};
#[unsafe(link_section = ".progmem.data")]
pub static A: [u8; 8] = [
0b01111110, 0b01000010, 0b10000001, 0b11111111, 0b10000001, 0b10000001, 0b10000001, 0b10000001,
];
#[unsafe(link_section = ".progmem.data")]
pub static B: [u8; 8] = [
0b11111100, 0b10000010, 0b10000010, 0b10001100, 0b10000010, 0b10000001, 0b10000001, 0b11111110,
];
#[unsafe(link_section = ".progmem.data")]
pub static C: [u8; 8] = [
0b11111110, 0b10000001, 0b10000000, 0b10000000, 0b10000000, 0b10000000, 0b10000001, 0b11111110,
];
#[unsafe(link_section = ".progmem.data")]
pub static D: [u8; 8] = [
0b11111100, 0b10000010, 0b10000010, 0b10000001, 0b10000001, 0b10000010, 0b10000010, 0b11111100,
];
#[unsafe(link_section = ".progmem.data")]
pub static E: [u8; 8] = [
0b11111111, 0b10000000, 0b10000000, 0b11111000, 0b10000000, 0b10000000, 0b10000000, 0b11111111,
];
#[unsafe(link_section = ".progmem.data")]
pub static F: [u8; 8] = [
0b11111111, 0b10000000, 0b10000000, 0b11111000, 0b10000000, 0b10000000, 0b10000000, 0b10000000,
];
#[unsafe(link_section = ".progmem.data")]
pub static G: [u8; 8] = [
0b11111110, 0b10000001, 0b10000000, 0b10000000, 0b10011111, 0b10010001, 0b01000010, 0b00111100,
];
#[unsafe(link_section = ".progmem.data")]
pub static H: [u8; 8] = [
0b10000001, 0b10000001, 0b10000001, 0b11111111, 0b10000001, 0b10000001, 0b10000001, 0b10000001,
];
#[unsafe(link_section = ".progmem.data")]
pub static I: [u8; 8] = [
0b00111100, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00111100,
];
#[unsafe(link_section = ".progmem.data")]
pub static J: [u8; 8] = [
0b00001110, 0b00000010, 0b00000010, 0b00000010, 0b00000010, 0b10000010, 0b10000010, 0b01111100,
];
#[unsafe(link_section = ".progmem.data")]
pub static K: [u8; 8] = [
0b01000010, 0b01000100, 0b01001000, 0b01110000, 0b01001000, 0b01000100, 0b01000010, 0b01000001,
];
#[unsafe(link_section = ".progmem.data")]
pub static L: [u8; 8] = [
0b10000000, 0b10000000, 0b10000000, 0b10000000, 0b10000000, 0b10000000, 0b10000000, 0b11111111,
];
#[unsafe(link_section = ".progmem.data")]
pub static M: [u8; 8] = [
0b00100100, 0b11011011, 0b10011001, 0b10011001, 0b10011001, 0b10000001, 0b10000001, 0b10000001,
];
#[unsafe(link_section = ".progmem.data")]
pub static N: [u8; 8] = [
0b11000001, 0b10100001, 0b10010001, 0b10001001, 0b10001001, 0b10000101, 0b10000111, 0b10000011,
];
#[unsafe(link_section = ".progmem.data")]
pub static O: [u8; 8] = [
0b11111111, 0b10000001, 0b10000001, 0b10000001, 0b10000001, 0b10000001, 0b10000001, 0b11111111,
];
#[unsafe(link_section = ".progmem.data")]
pub static P: [u8; 8] = [
0b11111111, 0b10000001, 0b10000001, 0b11111111, 0b10000000, 0b10000000, 0b10000000, 0b00000000,
];
#[unsafe(link_section = ".progmem.data")]
pub static Q: [u8; 8] = [
0b01111110, 0b10000001, 0b10000001, 0b10000001, 0b10001001, 0b10000101, 0b10000011, 0b01111111,
];
#[unsafe(link_section = ".progmem.data")]
pub static R: [u8; 8] = [
0b11111111, 0b10000001, 0b10000001, 0b11111111, 0b11111100, 0b10000010, 0b10000001, 0b10000001,
];
#[unsafe(link_section = ".progmem.data")]
pub static S: [u8; 8] = [
0b01111111, 0b10000000, 0b10000000, 0b01111110, 0b00000001, 0b00000001, 0b00000001, 0b11111110,
];
#[unsafe(link_section = ".progmem.data")]
pub static T: [u8; 8] = [
0b11111111, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000,
];
#[unsafe(link_section = ".progmem.data")]
pub static U: [u8; 8] = [
0b10000001, 0b10000001, 0b10000001, 0b10000001, 0b10000001, 0b10000001, 0b10000001, 0b01111110,
];
#[unsafe(link_section = ".progmem.data")]
pub static V: [u8; 8] = [
0b10000001, 0b10000001, 0b01000010, 0b01000010, 0b00100100, 0b00100100, 0b00100100, 0b00011000,
];
#[unsafe(link_section = ".progmem.data")]
pub static W: [u8; 8] = [
0b10000001, 0b10000001, 0b10000001, 0b10000001, 0b10011001, 0b01011010, 0b00100100, 0b00100100,
];
#[unsafe(link_section = ".progmem.data")]
pub static X: [u8; 8] = [
0b10000001, 0b01000010, 0b00100100, 0b00011000, 0b00011000, 0b00100100, 0b01000010, 0b10000001,
];
#[unsafe(link_section = ".progmem.data")]
pub static Y: [u8; 8] = [
0b10000001, 0b01000010, 0b00100100, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000,
];
#[unsafe(link_section = ".progmem.data")]
pub static Z: [u8; 8] = [
0b11111111, 0b00000111, 0b00001100, 0b00011000, 0b00110000, 0b01100000, 0b11000000, 0b11111111,
];
#[unsafe(link_section = ".progmem.data")]
pub static EXCLAMATION: [u8; 8] = [
0b01000000, 0b01000000, 0b01000000, 0b01000000, 0b01000000, 0b01000000, 0b00000000, 0b01000000,
];
#[unsafe(link_section = ".progmem.data")]
pub static COMMA: [u8; 8] = [
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b01000000, 0b10000000,
];
#[unsafe(link_section = ".progmem.data")]
pub static COLON: [u8; 8] = [
0b01100000, 0b01100000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b01100000, 0b01100000,
];
#[unsafe(link_section = ".progmem.data")]
pub static EMPTY: [u8; 8] = [
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
];
#[unsafe(link_section = ".progmem.data")]
pub static NUM_1: [u8; 8] = [
0b00011000, 0b00111000, 0b01111000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000,
];
#[unsafe(link_section = ".progmem.data")]
pub static NUM_2: [u8; 8] = [
0b11111110, 0b11110110, 0b00001100, 0b00011000, 0b00110000, 0b01100000, 0b11111111, 0b11111111,
];
#[unsafe(link_section = ".progmem.data")]
pub static NUM_3: [u8; 8] = [
0b01111110, 0b10000001, 0b00011110, 0b00011110, 0b00000001, 0b10000001, 0b11111111, 0b01111110,
];
#[unsafe(link_section = ".progmem.data")]
pub static NUM_4: [u8; 8] = [
0b11000110, 0b11000110, 0b11000110, 0b11111111, 0b11111110, 0b00000110, 0b00000110, 0b00000110,
];
#[unsafe(link_section = ".progmem.data")]
pub static NUM_5: [u8; 8] = [
0b11111111, 0b11111111, 0b11000000, 0b11111100, 0b00000110, 0b11000011, 0b11111111, 0b01111110,
];
#[unsafe(link_section = ".progmem.data")]
pub static NUM_6: [u8; 8] = [
0b01111110, 0b11000011, 0b11000000, 0b11111110, 0b11000011, 0b11000011, 0b11000011, 0b01111110,
];
#[unsafe(link_section = ".progmem.data")]
pub static NUM_7: [u8; 8] = [
0b11111111, 0b11111111, 0b00000110, 0b00001100, 0b00011000, 0b00110000, 0b01100000, 0b11000000,
];
#[unsafe(link_section = ".progmem.data")]
pub static NUM_8: [u8; 8] = [
0b01111110, 0b11111111, 0b11000011, 0b01111110, 0b11000011, 0b11000011, 0b11111111, 0b01111110,
];
#[unsafe(link_section = ".progmem.data")]
pub static NUM_9: [u8; 8] = [
0b01111110, 0b11000011, 0b11000011, 0b01111111, 0b00000011, 0b11000011, 0b11000011, 0b01111110,
];
#[unsafe(link_section = ".progmem.data")]
pub static NUM_0: [u8; 8] = [
0b01111110, 0b11000111, 0b11001011, 0b11001011, 0b11010011, 0b11010011, 0b11100011, 0b01111110,
];
fn letter(character: char) -> *const [u8; 8] {
match character { match character {
'a' => [ 'a' => addr_of!(A),
0b01111110, 0b01000010, 0b10000001, 0b11111111, 0b10000001, 0b10000001, 0b10000001, 'b' => addr_of!(B),
0b10000001, 'c' => addr_of!(C),
], 'd' => addr_of!(D),
_ => [0; 8], 'e' => addr_of!(E),
'f' => addr_of!(F),
'g' => addr_of!(G),
'h' => addr_of!(H),
'i' => addr_of!(I),
'j' => addr_of!(J),
'k' => addr_of!(K),
'l' => addr_of!(L),
'm' => addr_of!(M),
'n' => addr_of!(N),
'o' => addr_of!(O),
'p' => addr_of!(P),
'q' => addr_of!(Q),
'r' => addr_of!(R),
's' => addr_of!(S),
't' => addr_of!(T),
'u' => addr_of!(U),
'v' => addr_of!(V),
'w' => addr_of!(W),
'x' => addr_of!(X),
'y' => addr_of!(Y),
'z' => addr_of!(Z),
'0' => addr_of!(NUM_0),
'1' => addr_of!(NUM_1),
'2' => addr_of!(NUM_2),
'3' => addr_of!(NUM_3),
'4' => addr_of!(NUM_4),
'5' => addr_of!(NUM_5),
'6' => addr_of!(NUM_6),
'7' => addr_of!(NUM_7),
'8' => addr_of!(NUM_8),
'9' => addr_of!(NUM_9),
'!' => addr_of!(EXCLAMATION),
',' => addr_of!(COMMA),
':' => addr_of!(COLON),
_ => addr_of!(EMPTY),
}
}
pub fn draw_text<T: DelayNs, DCPin: PinOps, RSTPin: PinOps>(
display: &mut Display<T, DCPin, RSTPin>,
text: &str,
fg: Rgb565,
bg: Rgb565,
position: &mut Vec2,
scale: u16,
) {
let kerning = 9 * scale;
let original_x = position.x;
for line in text.split('\n') {
for word in line.split(' ') {
let word_length = word.len() as u16 * kerning;
if position.x + word_length > 240 {
position.x = original_x;
position.y += kerning;
}
for c in word.chars() {
draw_character(display, c, fg, bg, original_x, position, scale);
}
draw_character(display, ' ', fg, bg, original_x, position, scale);
}
position.y += kerning;
position.x = original_x;
}
}
pub fn draw_character<T: DelayNs, DCPin: PinOps, RSTPin: PinOps>(
display: &mut Display<T, DCPin, RSTPin>,
character: char,
fg: Rgb565,
bg: Rgb565,
original_x: u16,
position: &mut Vec2,
scale: u16,
) {
let kerning = 9 * scale;
if position.x + kerning > 240 {
position.x = original_x;
position.y += kerning;
}
if position.y + kerning > 240 {
position.y = 0;
}
Letter::from(character, fg, bg)
.iter()
.draw(display, *position, scale);
position.x += kerning;
}
pub fn draw_number<T: DelayNs, DCPin: PinOps, RSTPin: PinOps>(
display: &mut Display<T, DCPin, RSTPin>,
number: u32,
fg: Rgb565,
bg: Rgb565,
position: &mut Vec2,
scale: u16,
) {
if number >= 10 {
draw_number(display, number / 10, fg, bg, position, scale);
}
let character = match number % 10 {
0 => '0',
1 => '1',
2 => '2',
3 => '3',
4 => '4',
5 => '5',
6 => '6',
7 => '7',
8 => '8',
9 => '9',
_ => 'X',
};
draw_character(display, character, fg, bg, position.x, position, scale);
}
pub struct Letter {
base: [u8; 8],
pub fg: Rgb565,
pub bg: Rgb565,
}
impl Letter {
pub fn from(character: char, fg: Rgb565, bg: Rgb565) -> Letter {
let mut out = [0u8; 8];
let out_ptr = out.as_mut_ptr();
let ptr_addr = letter(character);
unsafe {
asm!(
// Load value of Z to temporary register $1 and post-increment Z
"lpm {1}, Z+",
// Store value from register $1 to X and post-increment X
"st X+, {1}",
// Subtract loop counter at register $0
"subi {0}, 1",
// If equality failed, jump back 8 bytes (or 4 instructions)
"brne -8",
inout(reg) 8u8 => _,
out(reg) _,
inout("Z") ptr_addr => _,
inout("X") out_ptr => _
)
}
Letter { base: out, fg, bg }
}
pub fn iter<'a>(&'a self) -> LetterIter<'a> {
LetterIter {
letter: &self,
idx: 0,
}
}
}
pub struct LetterIter<'a> {
letter: &'a Letter,
idx: usize,
}
impl<'a> LetterIter<'a> {
pub fn draw<T: DelayNs, DCPin: PinOps, RSTPin: PinOps>(
mut self,
display: &mut Display<T, DCPin, RSTPin>,
position: Vec2,
scale: u16,
) {
draw_stream(&mut self, display, position, Vec2 { x: 8, y: 8 }, scale);
}
}
impl<'a> Iterator for LetterIter<'a> {
type Item = Rgb565;
fn next(&mut self) -> Option<Self::Item> {
let byte_idx = self.idx / 8;
let bit_idx = 7 - (self.idx % 8);
if byte_idx >= 8 {
None
} else {
self.idx += 1;
let flag = (self.letter.base[byte_idx] & (1 << bit_idx)) >> bit_idx;
if flag == 1 {
Some(self.letter.fg)
} else {
Some(self.letter.bg)
}
}
} }
} }

View File

@ -108,7 +108,8 @@ pub fn draw_image<T: DelayNs, DCPin: PinOps, RSTPin: PinOps>(
y: height, y: height,
}, },
scale_factor, scale_factor,
) );
Ok(())
} else { } else {
Err(QoiErr::InvalidMagicNumber) Err(QoiErr::InvalidMagicNumber)
} }
@ -120,7 +121,7 @@ pub fn draw_stream<T: DelayNs, DCPin: PinOps, RSTPin: PinOps>(
position: Vec2, position: Vec2,
scale: Vec2, scale: Vec2,
scale_factor: u16, scale_factor: u16,
) -> Result<(), QoiErr> { ) {
let scale_iter = ScaleIterator { let scale_iter = ScaleIterator {
iter: stream, iter: stream,
last_row: [Rgb565::yellow(); 120], last_row: [Rgb565::yellow(); 120],
@ -142,8 +143,6 @@ pub fn draw_stream<T: DelayNs, DCPin: PinOps, RSTPin: PinOps>(
let [c1, c2] = pixel.as_color().bytes; let [c1, c2] = pixel.as_color().bytes;
display.write(Writeable::Data(&[c1, c2])); display.write(Writeable::Data(&[c1, c2]));
} }
Ok(())
} }
struct ScaleIterator<'a> { struct ScaleIterator<'a> {
iter: &'a mut dyn Iterator<Item = Rgb565>, iter: &'a mut dyn Iterator<Item = Rgb565>,

View File

@ -11,16 +11,21 @@
use core::ptr::addr_of; use core::ptr::addr_of;
use atmega_hal::{ use atmega_hal::{
Usart, Adc, Usart, Wdt,
adc::AdcSettings,
delay,
spi::{self, Settings}, spi::{self, Settings},
usart::Baudrate, usart::{Baudrate, UsartOps},
wdt::WdtOps,
}; };
use embedded_hal::delay::DelayNs;
use panic_halt as _; use panic_halt as _;
use crate::{ use crate::{
display::{Display, Vec2}, display::{Display, Rgb565, Vec2},
graphics::{Image, LARGE_CAT_UNSAFE, PRESS_BTN_UNSAFE, draw_image}, font::{draw_number, draw_text},
peripherals::Button, graphics::{Image, LARGE_CAT_UNSAFE, draw_image},
peripherals::{Button, Knob},
}; };
mod display; mod display;
@ -75,35 +80,108 @@ fn main() -> ! {
display.init(); display.init();
let mut adc = Adc::new(dp.ADC, AdcSettings::default());
let mut button = Button::from(pins.pd5.into_pull_up_input()); let mut button = Button::from(pins.pd5.into_pull_up_input());
let mut knob = Knob {
pin: pins.pc1.into_analog_input(&mut adc),
adc,
};
let mut idx = 0;
let images = [Image::from(addr_of!(LARGE_CAT_UNSAFE))]; let images = [Image::from(addr_of!(LARGE_CAT_UNSAFE))];
let len = images.len();
match draw_image( let mut delay = atmega_hal::delay::Delay::<CoreClock>::new();
&mut serial,
&mut Image::from(addr_of!(PRESS_BTN_UNSAFE)).iter(), let mut position = Vec2 { x: 10, y: 10 };
let original_position = position.clone();
draw_text(
&mut display, &mut display,
Vec2 { x: 0, y: 0 }, "dice:\n",
) { Rgb565::white(),
Ok(_) => ufmt::uwriteln!(serial, "Successfully read QOI").unwrap(), Rgb565::black(),
Err(e) => ufmt::uwriteln!(serial, "Error: {:?}", e).unwrap(), &mut position,
} 3,
);
let number_pos = position.clone();
draw_number(
&mut display,
0,
Rgb565::white(),
Rgb565::black(),
&mut number_pos.clone(),
3,
);
let mut clock = 0u32;
let mut animation = 0;
let mut animation_reached = true;
let mut cat_received = false;
let max_number = 10;
loop { loop {
clock += 1;
if button.poll() { if button.poll() {
match draw_image( animation = 50;
&mut serial, animation_reached = false;
&mut images[idx].iter(), if cat_received {
&mut display, display.draw_rect(
Vec2 { x: 0, y: 0 }, Vec2 { x: 0, y: 0 },
) { Vec2 { x: 240, y: 240 },
Ok(_) => ufmt::uwriteln!(serial, "Successfully read QOI").unwrap(), Rgb565::black().as_color(),
Err(e) => ufmt::uwriteln!(serial, "Error: {:?}", e).unwrap(), );
draw_text(
&mut display,
"dice:\n",
Rgb565::white(),
Rgb565::black(),
&mut original_position.clone(),
3,
);
cat_received = false;
} }
}
if animation > 0 {
let modulo = match animation {
50..100 => 5,
20..50 => 10,
0..20 => 20,
_ => 10,
};
animation -= 1;
if animation % modulo == 0 {
let random = (((clock * 543_128) ^ 7_643_125) & 0b11111111) as f32 / 255f32;
let dice = (random * max_number as f32) as u32;
draw_number(
&mut display,
dice,
Rgb565::white(),
Rgb565::black(),
&mut number_pos.clone(),
3,
);
}
} else if !animation_reached {
animation_reached = true;
let random = (((clock * 543_128) ^ 7_643_125) & 0b11111111) as f32 / 255f32;
let dice = (random * max_number as f32 + 1f32) as u32;
draw_number(
&mut display,
dice,
Rgb565::white(),
Rgb565::black(),
&mut number_pos.clone(),
3,
);
idx = (idx + 1) % len; if dice == max_number {
delay.delay_ms(1000);
draw_image(
&mut serial,
&mut Image::from(addr_of!(LARGE_CAT_UNSAFE)).iter(),
&mut display,
Vec2 { x: 0, y: 0 },
)
.unwrap();
cat_received = true;
}
} }
} }
} }

View File

@ -51,7 +51,10 @@ where
Pin<mode::Analog, T>: AdcChannel<Atmega, ADC>, Pin<mode::Analog, T>: AdcChannel<Atmega, ADC>,
{ {
pub fn poll(&mut self) -> f32 { pub fn poll(&mut self) -> f32 {
let read = self.pin.analog_read(&mut self.adc); self.raw() as f32 / 1024f32
read as f32 / 1024f32 }
pub fn raw(&mut self) -> u16 {
self.pin.analog_read(&mut self.adc)
} }
} }