Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ui-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ version = "0.1.0"
edition = "2024"

[dependencies]
bitmap-font = "0.3.0"
defmt = { version = "1.0.1", default-features = false }
embedded-graphics = "0.8.1"
embedded-graphics-core = { version = "0.4.0", default-features = false }
heapless = "0.9.1"
132 changes: 84 additions & 48 deletions ui-app/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
#![no_std]

use bitmap_font::{TextStyle, tamzen::FONT_14x26};
use defmt::Format;
use embedded_graphics::{
draw_target::DrawTarget,
geometry::Dimensions,
pixelcolor::{BinaryColor, Rgb565},
prelude::*,
primitives::{Circle, PrimitiveStyle, Rectangle},
text::Text,
};

#[derive(Copy, Clone, Debug, PartialEq, Format)]
pub enum Key {
Expand Down Expand Up @@ -130,16 +139,49 @@ pub trait Led {
}
}

fn rgb565(r: u8, g: u8, b: u8) -> u16 {
// Discard the low-order bits of the colors
(((r as u16) >> 3) << 11) | (((g as u16) >> 2) << 5) | ((b as u16) >> 3)
struct BinaryDisplay<'a, C, D> {
foreground: C,
background: C,
display: &'a mut D,
}

pub trait Screen {
fn width(&self) -> usize;
fn height(&self) -> usize;
fn fill(&mut self, color: u16);
fn draw(&mut self, left: usize, right: usize, top: usize, bottom: usize, data: &[u16]);
impl<'a, C, D> BinaryDisplay<'a, C, D> {
fn new(foreground: C, background: C, display: &'a mut D) -> Self {
Self {
foreground,
background,
display,
}
}
}

impl<'a, C, D> Dimensions for BinaryDisplay<'a, C, D>
where
D: Dimensions,
{
fn bounding_box(&self) -> Rectangle {
self.display.bounding_box()
}
}

impl<'a, C, D> DrawTarget for BinaryDisplay<'a, C, D>
where
C: PixelColor,
D: DrawTarget<Color = C>,
{
type Color = BinaryColor;
type Error = D::Error;

fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
let rgb = pixels.into_iter().map(|Pixel(point, color)| match color {
BinaryColor::On => Pixel(point, self.foreground),
BinaryColor::Off => Pixel(point, self.background),
});
self.display.draw_iter(rgb)
}
}

pub trait NetTx {
Expand All @@ -148,7 +190,7 @@ pub trait NetTx {

pub trait Outputs {
fn status_led(&mut self) -> &mut impl Led;
fn screen(&mut self) -> &mut impl Screen;
fn screen(&mut self) -> &mut impl DrawTarget<Color = Rgb565>;
fn net_tx(&mut self) -> &mut impl NetTx;
fn log(&mut self, message: &str);
}
Expand All @@ -173,47 +215,41 @@ impl App {
out.status_led().set_color(Color::Black);

// Draw a test pattern to the screen
const SIZE: usize = 10;
let screen = out.screen();
let mut data = [0_u16; SIZE * SIZE];

// Background
screen.fill(rgb565(0x88, 0x88, 0x88));

// Upper left = R
let (x0, x1, y0, y1) = (SIZE, SIZE + SIZE, SIZE, SIZE + SIZE);
data.fill(rgb565(0xFF, 0x00, 0x00));
screen.draw(x0, x1, y0, y1, &data);

// Upper right = G
let (x0, x1, y0, y1) = (
screen.width() - SIZE - SIZE,
screen.width() - SIZE,
SIZE,
SIZE + SIZE,
);
data.fill(rgb565(0x00, 0xFF, 0x00));
screen.draw(x0, x1, y0, y1, &data);

// Lower left = B
let (x0, x1, y0, y1) = (
SIZE,
SIZE + SIZE,
screen.height() - SIZE - SIZE,
screen.height() - SIZE,
let rect = out.screen().bounding_box();

rect.into_styled(PrimitiveStyle::with_fill(Rgb565::new(0x88, 0x88, 0x88)))
.draw(out.screen())
.unwrap_or_else(|_| panic!("graphics error"));

let pad: u32 = 10;
let diameter: u32 = 20;
let width = rect.size.width;
let height = rect.size.height;

let mut dot = |left, top, color| {
Circle::new(Point::new(left as i32, top as i32), diameter)
.into_styled(PrimitiveStyle::with_fill(color))
.draw(out.screen())
.unwrap_or_else(|_| panic!("graphics error"));
};

dot(pad, pad, Rgb565::RED);
dot(width - pad - diameter, pad, Rgb565::GREEN);
dot(pad, height - pad - diameter, Rgb565::BLUE);
dot(
width - pad - diameter,
height - pad - diameter,
Rgb565::YELLOW,
);
data.fill(rgb565(0x00, 0x00, 0xFF));
screen.draw(x0, x1, y0, y1, &data);

// Lower right = Y
let (x0, x1, y0, y1) = (
screen.width() - SIZE - SIZE,
screen.width() - SIZE,
screen.height() - SIZE - SIZE,
screen.height() - SIZE,

let text = Text::new(
"Hello World!",
Point { x: 10, y: 30 },
TextStyle::new(&FONT_14x26, BinaryColor::On),
);
data.fill(rgb565(0xFF, 0xFF, 0x00));
screen.draw(x0, x1, y0, y1, &data);
let mut binary_display = BinaryDisplay::new(Rgb565::WHITE, Rgb565::BLACK, out.screen());
text.draw(&mut binary_display)
.unwrap_or_else(|_| panic!("graphics error"));
}

pub fn handle(&mut self, event: Event, out: &mut impl Outputs) {
Expand Down
31 changes: 18 additions & 13 deletions ui-app/tests/integration.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use ui_app::*;

use embedded_graphics::{
draw_target::DrawTarget, pixelcolor::Rgb565, prelude::*, primitives::Rectangle,
};

#[derive(Default)]
struct MockLed {
color: Option<Color>,
Expand All @@ -15,21 +19,22 @@ impl Led for MockLed {
#[derive(Default)]
struct MockScreen;

impl Screen for MockScreen {
fn width(&self) -> usize {
240
}

fn height(&self) -> usize {
320
impl Dimensions for MockScreen {
fn bounding_box(&self) -> Rectangle {
Rectangle::new(Point::new(0, 0), Size::new(240, 320))
}
}

fn fill(&mut self, _color: u16) {
// TODO: Store pixels
}
impl DrawTarget for MockScreen {
type Color = Rgb565;
type Error = String;

fn draw(&mut self, _left: usize, _right: usize, _top: usize, _bottom: usize, _data: &[u16]) {
// TODO: Store pixels
fn draw_iter<I>(&mut self, _pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
// TODO(RLB) Store pixels
Ok(())
}
}

Expand Down Expand Up @@ -57,7 +62,7 @@ impl Outputs for MockOutputs {
&mut self.status_led
}

fn screen(&mut self) -> &mut impl Screen {
fn screen(&mut self) -> &mut impl DrawTarget<Color = Rgb565> {
&mut self.screen
}

Expand Down
5 changes: 5 additions & 0 deletions ui-stm32/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ hex = { version = "0.4.3", default-features = false }
heapless = { version = "0.9.1", features = ["defmt"] }
embedded-io-async = "0.6.1"
embedded-io = { version = "0.6.1", default-features = false }
ili9341 = "0.6.0"
display-interface = "0.5.0"
embedded-graphics-core = { version = "0.4.0", default-features = false }
bitmap-font = "0.3.0"
embedded-graphics = "0.8.1"

[profile.release]
debug = 2
Expand Down
110 changes: 103 additions & 7 deletions ui-stm32/src/board/ev12.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,106 @@
use super::{Button, Keyboard, NetTx, Screen, StatusLed};
use super::{Button, Keyboard, NetTx, StatusLed};
use display_interface::{DataFormat, DisplayError, WriteOnlyDataCommand};
use embassy_stm32::{
bind_interrupts,
exti::ExtiInput,
gpio::{Input, Level, Output, Pull, Speed},
mode::Async,
mode::{Async, Blocking},
peripherals,
spi::Spi,
spi::{Spi, Word},
usart::{self, UartRx, UartTx},
};
use embassy_time::Delay;
use embedded_graphics::{pixelcolor::Rgb565, prelude::*};
use ili9341::{Ili9341, Orientation};
use ui_app::{Led, Outputs};

struct DisplayData {
// These fields are not used, but we need to keep them alive so that the chip select output is
// held low and the backlight output is held high.
_chip_select: Output<'static>,
_backlight: Output<'static>,

data_command: Output<'static>,
spi: Spi<'static, Blocking>,
}

impl DisplayData {
fn new(
mut backlight: Output<'static>,
mut chip_select: Output<'static>,
data_command: Output<'static>,
spi: Spi<'static, Blocking>,
) -> Self {
backlight.set_high();
chip_select.set_low();

Self {
_backlight: backlight,
_chip_select: chip_select,
data_command,
spi,
}
}

fn write(&mut self, data: DataFormat<'_>) -> Result<(), DisplayError> {
use DataFormat::*;
match data {
U8(slice) => self.write_slice(slice),
U16(slice) => self.write_slice(slice),
U16BE(slice) => self.write_slice(slice),
U16LE(slice) => self.write_slice(slice),
U8Iter(iter) => self.write_iter(iter),
U16BEIter(iter) => self.write_iter(iter),
U16LEIter(iter) => self.write_iter(iter),
_ => unreachable!(),
}
}

fn write_slice<W: Word>(&mut self, data: &[W]) -> Result<(), DisplayError> {
self.spi.blocking_write(data).unwrap();
Ok(())
}

fn write_iter<W: Word>(
&mut self,
iter: &mut dyn Iterator<Item = W>,
) -> Result<(), DisplayError> {
const CHUNK_SIZE: usize = 128;

// XXX(RLB) Very C-style iteration, could probably write this in a way that would optimize
// better.
let mut data = [W::default(); CHUNK_SIZE];
let mut n = 0;
for (i, x) in iter.enumerate() {
data[i % CHUNK_SIZE] = x;
n = i + 1;

if n > 0 && n % CHUNK_SIZE == 0 {
self.spi.blocking_write(&data).unwrap();
n = 0;
}
}

self.spi.blocking_write(&data[..n]).unwrap();
Ok(())
}
}

impl WriteOnlyDataCommand for DisplayData {
fn send_commands(&mut self, cmd: DataFormat<'_>) -> Result<(), DisplayError> {
self.data_command.set_low();
self.write(cmd)
}

fn send_data(&mut self, buf: DataFormat<'_>) -> Result<(), DisplayError> {
self.data_command.set_high();
self.write(buf)
}
}

pub struct Board {
status_led: StatusLed,
screen: Screen,
screen: Ili9341<DisplayData, Output<'static>>,
net_tx: NetTx<UartTx<'static, Async>>,
pub button_a: Option<Button>,
pub button_b: Option<Button>,
Expand Down Expand Up @@ -101,8 +189,16 @@ impl Board {
config.bit_order = BitOrder::MsbFirst;
config
};
let spi1 = Spi::new_blocking_txonly(p.SPI1, p.PA5, p.PA7, config);
let screen = Screen::new(chip_select, data_command, reset, backlight, spi1).await;
let spi = Spi::new_blocking_txonly(p.SPI1, p.PA5, p.PA7, config);

let screen = Ili9341::new(
DisplayData::new(backlight, chip_select, data_command, spi),
reset,
&mut Delay,
Orientation::Portrait,
ili9341::DisplaySize240x320,
)
.unwrap();

// NET UART
let net_uart = {
Expand Down Expand Up @@ -136,7 +232,7 @@ impl Outputs for Board {
&mut self.status_led
}

fn screen(&mut self) -> &mut impl ui_app::Screen {
fn screen(&mut self) -> &mut impl DrawTarget<Color = Rgb565> {
&mut self.screen
}

Expand Down
Loading