firmware: add firmware

This commit is contained in:
daylily
2025-04-07 19:57:23 -04:00
parent fc2bd71702
commit 0ea0231f42
15 changed files with 1562 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"rust-analyzer.check.allTargets": false,
"rust-analyzer.check.targets": "thumbv7em-none-eabihf"
}
+8
View File
@@ -0,0 +1,8 @@
[target.thumbv7em-none-eabihf]
runner = 'probe-rs run --chip STM32F411CEUx'
[build]
target = "thumbv7em-none-eabihf"
[env]
DEFMT_LOG = "debug"
+1
View File
@@ -0,0 +1 @@
/target
+1057
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
[package]
name = "inkclip-firmware"
version = "0.1.0"
edition = "2024"
[dependencies]
embassy-stm32 = { version = "0.2", features = [
"defmt",
"stm32f411ce",
"memory-x",
"time-driver-any",
"exti",
] }
embassy-sync = { version = "0.6", features = ["defmt"] }
embassy-executor = { version = "0.7", features = [
"nightly",
"arch-cortex-m",
"executor-thread",
"executor-interrupt",
"defmt",
] }
embassy-time = { version = "0.4", features = [
"defmt",
"defmt-timestamp-uptime",
"tick-hz-32_768",
] }
embassy-usb = { version = "0.4", features = ["defmt"] }
embassy-futures = { version = "0.1", features = ["defmt"] }
defmt = "1.0"
defmt-rtt = "1.0"
cortex-m = { version = "0.7", features = [
"inline-asm",
"critical-section-single-core",
] }
cortex-m-rt = "0.7"
embedded-hal-bus = { version = "0.3", features = ["async"] }
panic-probe = { version = "1.0", features = ["print-defmt"] }
critical-section = "1.1"
epd-waveshare = "0.6"
embedded-graphics = "0.8"
usbd-hid = "0.8"
base64 = { version = "0.22", default-features = false }
[profile.release]
opt-level = "z"
+5
View File
@@ -0,0 +1,5 @@
fn main() {
println!("cargo:rustc-link-arg-bins=--nmagic");
println!("cargo:rustc-link-arg-bins=-Tlink.x");
println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
}
+11
View File
@@ -0,0 +1,11 @@
MEMORY
{
/* NOTE K = KiBi = 1024 bytes */
FLASH : ORIGIN = 0x08000000, LENGTH = 512K
RAM : ORIGIN = 0x20000000, LENGTH = 128K
}
/* This is where the call stack will be allocated. */
/* The stack is of the full descending type. */
/* NOTE Do NOT modify `_stack_start` unless you know what you are doing */
_stack_start = ORIGIN(RAM) + LENGTH(RAM);
+23
View File
@@ -0,0 +1,23 @@
use usbd_hid::descriptor::generator_prelude::*;
// Watch out the terminology!
// - An "output" report is a report from the host to the device (as the device is the one doing the output);
// - An "input" report is a report from the device to the host (as it is presumably triggered by user input).
#[gen_hid_descriptor(
(usage_page = VENDOR_DEFINED_END, usage = 0x01, collection = APPLICATION) = {
(report_id = 0x01, usage = 0x02) = {
write_pattern = output;
};
(report_id = 0x02, usage = 0x03) = {
serial_number_request = output;
};
(report_id = 0x02, usage = 0x03) = {
serial_number_response = input;
};
}
)]
pub struct HIDReports {
write_pattern: [u8; 5000],
serial_number_request: u8,
serial_number_response: [u8; 16],
}
+86
View File
@@ -0,0 +1,86 @@
use defmt::*;
use embedded_graphics::prelude::*;
use epd_waveshare::epd1in54_v2::Display1in54;
use epd_waveshare::prelude::*;
use crate::device::epd::Epd;
use crate::device::usb_hid::Hid;
use crate::util::{uid_base64, uid_base64_bytes};
pub mod descriptor;
pub const USB_READ_BUFFER_SIZE: usize = 8192;
pub const USB_WRITE_BUFFER_SIZE: usize = 32;
const WRITE_PATTERN_REPORT_ID: u8 = 0x01;
const SERIAL_NUMBER_REPORT_ID: u8 = 0x02;
fn color_at(byte: u8, ix: i32) -> Color {
defmt::debug_assert!(0 <= ix && ix < 8);
if byte & (1u8 << ix) == 0 {
Color::White
} else {
Color::Black
}
}
pub struct MainLoop<'a> {
pub hid: Hid<'a, USB_READ_BUFFER_SIZE, USB_WRITE_BUFFER_SIZE>,
pub epd: Epd,
}
impl<'a> MainLoop<'a> {
fn draw_pattern(&mut self, buf: &[u8]) {
debug!("Updating display");
let mut buf_size = buf.len();
if buf_size < 5000 {
warn!("WRITE_PATTERN buffer too short (buf_size = {})", buf_size);
} else if buf_size > 5000 {
warn!(
"WRITE_PATTERN buffer too long; truncating (buf_size = {})",
buf_size
);
buf_size = 5000;
}
let mut display = Display1in54::default();
display.set_rotation(DisplayRotation::Rotate180);
let width = display.size().width as i32;
for (stride, &byte) in buf[..buf_size].iter().enumerate() {
for stroll in 0..8 {
let ix = stride as i32 * 8 + stroll;
let x = ix % width;
let y = ix / width;
display.set_pixel(Pixel(Point { x, y }, color_at(byte, stroll)));
}
}
self.epd.update_display(&display);
}
async fn send_serial_number(&mut self) {
debug!("Sending serial number {}", uid_base64());
self.hid
.write(SERIAL_NUMBER_REPORT_ID, uid_base64_bytes())
.await;
}
pub async fn run(&mut self) -> ! {
let mut read_buf = [0u8; USB_READ_BUFFER_SIZE];
loop {
let (id, buf) = self.hid.read(&mut read_buf).await;
match id {
WRITE_PATTERN_REPORT_ID => self.draw_pattern(buf),
SERIAL_NUMBER_REPORT_ID => self.send_serial_number().await,
_ => warn!("Unknown report ID: {}", buf[0]),
}
}
}
}
+63
View File
@@ -0,0 +1,63 @@
use embassy_stm32::gpio::{self, Level, Pull, Speed};
use embassy_stm32::spi::{self, MosiPin, SckPin, Spi};
use embassy_stm32::time::Hertz;
use embassy_time::Delay;
use embedded_hal_bus::spi::ExclusiveDevice;
use epd_waveshare::epd1in54::Display1in54;
use epd_waveshare::epd1in54_v2::Epd1in54;
use epd_waveshare::prelude::*;
type EpdSpiDevice =
ExclusiveDevice<Spi<'static, embassy_stm32::mode::Blocking>, gpio::Output<'static>, Delay>;
pub struct Epd {
spi_device: EpdSpiDevice,
epd: Epd1in54<
EpdSpiDevice,
gpio::Input<'static>,
gpio::Output<'static>,
gpio::Output<'static>,
Delay,
>,
}
impl Epd {
pub fn new<SPI: spi::Instance>(
spi: SPI,
cs: impl gpio::Pin,
sck: impl SckPin<SPI>,
mosi: impl MosiPin<SPI>,
busy: impl gpio::Pin,
dc: impl gpio::Pin,
rst: impl gpio::Pin,
) -> Epd {
let cs_pin = gpio::Output::new(cs, Level::High, Speed::VeryHigh);
let busy_pin = gpio::Input::new(busy, Pull::Down);
let dc_pin = gpio::Output::new(dc, Level::High, Speed::VeryHigh);
let rst_pin = gpio::Output::new(rst, Level::High, Speed::VeryHigh);
let mut spi_config = spi::Config::default();
spi_config.frequency = Hertz(20_000_000);
let spi_bus = Spi::new_blocking_txonly(spi, sck, mosi, spi_config);
let mut spi_device = ExclusiveDevice::new(spi_bus, cs_pin, Delay {}).unwrap();
let epd = Epd1in54::new(
&mut spi_device,
busy_pin,
dc_pin,
rst_pin,
&mut Delay {},
Some(0),
)
.unwrap();
return Epd { spi_device, epd };
}
pub fn update_display(&mut self, display: &Display1in54) {
self.epd
.update_and_display_frame(&mut self.spi_device, display.buffer(), &mut Delay {})
.unwrap_or(())
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod epd;
pub mod usb_hid;
+120
View File
@@ -0,0 +1,120 @@
use defmt::*;
use embassy_stm32::peripherals::USB_OTG_FS;
use embassy_stm32::usb::{self, DmPin, DpPin};
use embassy_usb::class::hid::{self, HidReader, HidReaderWriter, HidWriter};
use embassy_usb::driver::EndpointError;
use crate::init::Irqs;
pub type UsbBuilder<'d> = embassy_usb::Builder<'d, usb::Driver<'d, USB_OTG_FS>>;
pub fn get_usb_builder<'d>(
usb_peri: USB_OTG_FS,
dp: impl DpPin<USB_OTG_FS>,
dm: impl DmPin<USB_OTG_FS>,
ep_out_buffer: &'d mut [u8],
driver_config: usb::Config,
builder_config: embassy_usb::Config<'d>,
config_descriptor: &'d mut [u8],
bos_descriptor: &'d mut [u8],
msos_descriptor: &'d mut [u8],
control_buf: &'d mut [u8],
) -> UsbBuilder<'d> {
let driver = usb::Driver::new_fs(usb_peri, Irqs, dp, dm, ep_out_buffer, driver_config);
return embassy_usb::Builder::new(
driver,
builder_config,
config_descriptor,
bos_descriptor,
msos_descriptor,
control_buf,
);
}
pub struct Hid<'d, const READ_N: usize, const WRITE_N: usize> {
reader: HidReader<'d, usb::Driver<'d, USB_OTG_FS>, READ_N>,
writer: HidWriter<'d, usb::Driver<'d, USB_OTG_FS>, WRITE_N>,
}
impl<'d, const READ_N: usize, const WRITE_N: usize> Hid<'d, READ_N, WRITE_N> {
pub fn new(
usb_builder: &mut UsbBuilder<'d>,
report_descriptor: &'d [u8],
state: &'d mut hid::State<'d>,
) -> Hid<'d, READ_N, WRITE_N> {
let hid_config = hid::Config {
report_descriptor,
request_handler: None,
poll_ms: 10,
max_packet_size: 64,
};
let (reader, writer) = HidReaderWriter::new(usb_builder, state, hid_config).split();
return Hid { reader, writer };
}
async fn internal_read<'a, const USE_ID: bool>(
&mut self,
buf: &'a mut [u8; READ_N],
) -> (u8, &'a [u8]) {
loop {
match self.reader.read(buf).await {
Ok(len) => {
if USE_ID {
if len < 1 {
warn!("Received no report ID")
} else {
return (buf[0], &buf[1..len]);
}
} else {
return (0, &buf[..len]);
}
}
Err(hid::ReadError::BufferOverflow) => {
warn!("Buffer overflow when reading from host")
}
Err(hid::ReadError::Disabled) => {
info!("Endpoint disabled");
self.reader.ready().await
}
Err(hid::ReadError::Sync(_)) => defmt::unreachable!(),
}
}
}
pub async fn read<'a>(&mut self, buf: &'a mut [u8; READ_N]) -> (u8, &'a [u8]) {
self.internal_read::<true>(buf).await
}
#[allow(dead_code)]
pub async fn read_no_id<'a>(&mut self, buf: &'a mut [u8; READ_N]) -> &'a [u8] {
self.internal_read::<false>(buf).await.1
}
pub async fn write(&mut self, id: u8, report: &[u8]) {
let payload_size = report.len() + 1;
if payload_size > WRITE_N {
error!(
"Buffer overflow when writing to host (report_size = {})",
payload_size
);
return;
}
let mut payload = [id; WRITE_N];
payload[1..payload_size].copy_from_slice(report);
self.writer
.write(&payload[..payload_size])
.await
.unwrap_or_else(|err| match err {
EndpointError::BufferOverflow => error!("Buffer overflow when writing to host"),
EndpointError::Disabled => warn!("Endpoint disabled"),
});
info!("Written report #{}: {}", id, report);
}
}
+35
View File
@@ -0,0 +1,35 @@
use embassy_stm32::bind_interrupts;
use embassy_stm32::peripherals::USB_OTG_FS;
use embassy_stm32::time::Hertz;
use embassy_stm32::usb;
use embassy_stm32::{Config, Peripherals};
bind_interrupts!(pub struct Irqs {
OTG_FS => usb::InterruptHandler<USB_OTG_FS>;
});
pub fn init_peripherals() -> Peripherals {
let mut config = Config::default();
{
use embassy_stm32::rcc::*;
config.rcc.hse = Some(Hse {
freq: Hertz(12_000_000),
mode: HseMode::Oscillator,
});
config.rcc.pll_src = PllSource::HSE;
config.rcc.pll = Some(Pll {
prediv: PllPreDiv::DIV12, // 12 / 12 = 1 MHz
mul: PllMul::MUL192, // 1 * 192 = 192 MHz
divp: Some(PllPDiv::DIV2), // 192 / 2 = 96 MHz <= 100 MHz max freq
divq: Some(PllQDiv::DIV4), // 192 / 4 = 48 MHz necessary for USB
divr: None,
});
config.rcc.ahb_pre = AHBPrescaler::DIV1;
config.rcc.apb1_pre = APBPrescaler::DIV2;
config.rcc.apb2_pre = APBPrescaler::DIV4;
config.rcc.sys = Sysclk::PLL1_P;
config.rcc.mux.clk48sel = mux::Clk48sel::PLL1_Q;
}
return embassy_stm32::init(config);
}
+79
View File
@@ -0,0 +1,79 @@
#![no_main]
#![no_std]
#![feature(impl_trait_in_assoc_type)]
use defmt::*;
use embassy_executor::Spawner;
use embassy_futures::join::join;
use device::epd::Epd;
use device::usb_hid::Hid;
use device::usb_hid::get_usb_builder;
use embassy_stm32::usb;
use embassy_usb::UsbVersion;
use embassy_usb::class::hid;
use usbd_hid::descriptor::generator_prelude::*;
use init::init_peripherals;
use util::uid_base64;
use app::MainLoop;
use app::descriptor::HIDReports;
use {defmt_rtt as _, panic_probe as _};
mod app;
mod device;
mod init;
mod util;
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let peri = init_peripherals();
info!("Initialized peripherals");
let epd = Epd::new(
peri.SPI1, peri.PA4, peri.PA5, peri.PA7, peri.PB10, peri.PB0, peri.PB1,
);
info!("Established SPI link to e-paper display");
let mut ep_out_buffer = [0u8; 256];
let mut config_descriptor = [0; 256];
let mut control_buf = [0; 256];
let mut state = hid::State::new();
let mut builder_config = embassy_usb::Config::new(0x1209, 0xc9c9);
builder_config.manufacturer = Some("daylily");
builder_config.product = Some("“Inkclip” 1.54″ ePaper Accessory");
builder_config.serial_number = Some(uid_base64());
// Caution: past USB 2.0 (including USB 2.1 aka 2.0 LPM), BOS descriptors become necessary
builder_config.bcd_usb = UsbVersion::Two;
let mut builder = get_usb_builder(
peri.USB_OTG_FS,
peri.PA12,
peri.PA11,
&mut ep_out_buffer,
usb::Config::default(),
builder_config,
&mut config_descriptor,
&mut [], // USB 2.0 does not require BOS descriptors
&mut [], // Non vendor-specific devices/interfaces do not require MS OS descriptors
&mut control_buf,
);
let usb_hid = Hid::new(&mut builder, HIDReports::desc(), &mut state);
let mut main_loop = MainLoop { epd, hid: usb_hid };
let main_fut = main_loop.run();
let mut usb_device = builder.build();
let usb_fut = usb_device.run();
info!("Initialized USB HID device");
join(main_fut, usb_fut).await;
}
+20
View File
@@ -0,0 +1,20 @@
use base64::prelude::*;
use embassy_stm32::uid::uid;
pub fn uid_base64() -> &'static str {
unsafe { core::str::from_utf8_unchecked(uid_base64_bytes()) }
}
#[allow(static_mut_refs)]
pub fn uid_base64_bytes() -> &'static [u8] {
static mut UID_BASE64: [u8; 16] = [0; 16];
static mut LOADED: bool = false;
critical_section::with(|_| unsafe {
if !LOADED {
let uid = uid();
BASE64_STANDARD.encode_slice(uid, &mut UID_BASE64).unwrap();
LOADED = true;
}
});
unsafe { &*core::ptr::addr_of!(UID_BASE64) }
}