From f7d3c4c63352405e4330cb5bdd6e98c380be5aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=A4nner?= Date: Sun, 16 Nov 2025 20:57:31 +0100 Subject: [PATCH] simple picorom interface --- Cargo.lock | 69 +++++++++++-- Cargo.toml | 41 +++++++- src/main.rs | 137 ++++++++++++++++++++++-- src/serial.rs | 280 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 511 insertions(+), 16 deletions(-) create mode 100644 src/serial.rs diff --git a/Cargo.lock b/Cargo.lock index d09a8d1..59624d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,6 +155,15 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.0.1", +] + [[package]] name = "defmt" version = "1.0.1" @@ -194,7 +203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93d5a25c99d89c40f5676bec8cefe0614f17f0f40e916f98e345dae941807f9e" dependencies = [ "critical-section", - "defmt", + "defmt 1.0.1", ] [[package]] @@ -391,6 +400,16 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heapless" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -522,7 +541,17 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" dependencies = [ - "num_enum_derive", + "num_enum_derive 0.5.11", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive 0.7.5", + "rustversion", ] [[package]] @@ -536,6 +565,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "panic-probe" version = "1.0.0" @@ -543,7 +583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd402d00b0fb94c5aee000029204a46884b1262e0c443f166d86d2c0747e1a1a" dependencies = [ "cortex-m", - "defmt", + "defmt 1.0.1", ] [[package]] @@ -600,13 +640,17 @@ version = "0.1.0" dependencies = [ "cortex-m", "cortex-m-rt", - "defmt", + "defmt 1.0.1", "defmt-rtt", "embedded-hal 1.0.0", + "heapless 0.9.2", + "num_enum 0.7.5", "panic-probe", "pio", "pio-proc", "rp-pico", + "usb-device", + "usbd-serial", ] [[package]] @@ -616,7 +660,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76e09694b50f89f302ed531c1f2a7569f0be5867aee4ab4f8f729bbeec0078e3" dependencies = [ "arrayvec", - "num_enum", + "num_enum 0.5.11", "paste", ] @@ -1042,10 +1086,23 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98816b1accafbb09085168b90f27e93d790b4bfa19d883466b5e53315b5f06a6" dependencies = [ - "heapless", + "defmt 0.3.100", + "heapless 0.8.0", "portable-atomic", ] +[[package]] +name = "usbd-serial" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065e4eaf93db81d5adac82d9cef8f8da314cb640fa7f89534b972383f1cf80fc" +dependencies = [ + "embedded-hal 0.2.7", + "embedded-io", + "nb 1.1.0", + "usb-device", +] + [[package]] name = "vcell" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index db09e12..a7c3817 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,13 +9,52 @@ test = false bench = false [dependencies] -cortex-m = "0.7" +cortex-m = { version = "0.7", features = ["inline-asm"] } cortex-m-rt = "0.7" panic-probe = { version = "1.0", features = ["print-defmt", "defmt-error"] } rp-pico = "0.9" embedded-hal = "1.0" pio = "0.2" pio-proc = "0.2" +usb-device = { version = "0.3", features = ["defmt"] } +usbd-serial = "0.2" defmt = "1.0" defmt-rtt = "1.1" + +num_enum = { version = "0.7", default-features = false } +heapless = "0.9" + +# cargo build/run +[profile.dev] +codegen-units = 1 +debug = 2 +debug-assertions = true +incremental = false +opt-level = 3 +overflow-checks = true + +# cargo build/run --release +[profile.release] +codegen-units = 1 +debug = 2 +debug-assertions = false +incremental = false +lto = 'fat' +opt-level = 3 +overflow-checks = false + +# do not optimize proc-macro crates = faster builds from scratch +[profile.dev.build-override] +codegen-units = 8 +debug = false +debug-assertions = false +opt-level = 0 +overflow-checks = false + +[profile.release.build-override] +codegen-units = 8 +debug = false +debug-assertions = false +opt-level = 0 +overflow-checks = false diff --git a/src/main.rs b/src/main.rs index e6add7f..a2d760f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,9 +8,14 @@ //! GP26 => CE //! GP27 => OE -use core::cell::Cell; +mod serial; -use cortex_m::{delay::Delay, interrupt::Mutex}; +use core::{ + cell::Cell, + sync::atomic::{AtomicU8, Ordering}, +}; + +use cortex_m::{delay::Delay, interrupt::Mutex, singleton}; use defmt_rtt as _; use embedded_hal::digital::OutputPin; use panic_probe as _; @@ -21,11 +26,16 @@ use rp_pico::{ Sio, Watchdog, clocks::init_clocks_and_plls, gpio::{FunctionPio0, PullNone}, - pio::{PIO0SM0, PIO0SM3, PIOBuilder, PioIRQ, Rx, ShiftDirection, Tx}, + pio::{PIO0SM0, PIO0SM3, PIOBuilder, PioIRQ, Rx, Tx}, prelude::*, + usb::UsbBus, }, pac::{self, CorePeripherals, Peripherals, interrupt}, }; +use usb_device::{class_prelude::*, prelude::*}; +use usbd_serial::SerialPort; + +use crate::serial::{Link, PacketBuilder}; const ADDRESS_BASE: u8 = 0; const ADDRESS_PINS: u8 = 15; @@ -35,13 +45,19 @@ const CE: u8 = 26; #[allow(unused)] const OE: u8 = 27; +const ROM_SIZE: usize = 1024 * 32; + static GLOBAL_ADDRESS_RX: Mutex>>> = Mutex::new(Cell::new(None)); static GLOBAL_DATA_TX: Mutex>>> = Mutex::new(Cell::new(None)); +static ROM_DATA: [AtomicU8; ROM_SIZE] = [const { AtomicU8::new(0) }; ROM_SIZE]; + +static GLOBAL_USB_DEVICE: Mutex>>> = Mutex::new(Cell::new(None)); +static GLOBAL_USB_SERIAL: Mutex>>> = Mutex::new(Cell::new(None)); #[entry] fn main() -> ! { let mut pac = Peripherals::take().unwrap(); - let core = CorePeripherals::take().unwrap(); + let mut core = CorePeripherals::take().unwrap(); let mut watchdog = Watchdog::new(pac.WATCHDOG); @@ -195,8 +211,37 @@ fn main() -> ! { address_rx.enable_rx_not_empty_interrupt(PioIRQ::Irq0); cortex_m::interrupt::free(|cs| GLOBAL_ADDRESS_RX.borrow(cs).set(Some(address_rx))); + let usbctrl_regs = pac.USBCTRL_REGS; + let usbctrl_dpram = pac.USBCTRL_DPRAM; + let resets = &mut pac.RESETS; + let usb_bus = singleton!(: UsbBusAllocator = UsbBusAllocator::new(UsbBus::new( + usbctrl_regs, + usbctrl_dpram, + clocks.usb_clock, + true, + resets, + ))) + .unwrap(); + let serial = SerialPort::new(usb_bus); + cortex_m::interrupt::free(|cs| GLOBAL_USB_SERIAL.borrow(cs).set(Some(serial))); + + let usb_dev = UsbDeviceBuilder::new(usb_bus, UsbVidPid(0x2e8a, 0x000a)) + .strings(&[StringDescriptors::default() + .manufacturer("Max Känner") + .product("PicoROM.rs") + .serial_number("1")]) + .unwrap() + .device_class(2) + .build(); + cortex_m::interrupt::free(|cs| GLOBAL_USB_DEVICE.borrow(cs).set(Some(usb_dev))); + unsafe { + // Highest priority for pio + core.NVIC.set_priority(pac::interrupt::PIO0_IRQ_0, 0x00); + // lowest priority for usb + core.NVIC.set_priority(pac::interrupt::USBCTRL_IRQ, 0xFF); pac::NVIC::unmask(pac::interrupt::PIO0_IRQ_0); + pac::NVIC::unmask(pac::interrupt::USBCTRL_IRQ); } loop { @@ -220,12 +265,86 @@ fn PIO0_IRQ_0() { } if let Some(address) = ADDRESS { - while let Some(address) = address.read() { - let address = (address & 0x7fff) as u16; - if let Some(data) = DATA { - defmt::trace!("replying with {:#04x} @ {:#06x}", address as u8, address); - data.write(u32::from(address)); + while let Some(rx) = address.read() { + let address = (rx & 0x7fff) as u16; + if let Some(tx) = DATA { + let data = ROM_DATA[usize::from(address)].load(Ordering::Relaxed); + defmt::trace!("replying with {:#04x} @ {:#06x}", data, address); + tx.write(u32::from(data)); } } } } + +#[interrupt] +fn USBCTRL_IRQ() { + static mut USB_DEVICE: Option> = None; + static mut USB_SERIAL: Option> = None; + static mut DO_PREAMBLE: bool = true; + static mut PACKET_BUILDER: PacketBuilder = PacketBuilder::new(); + static mut LINK: Link = Link::new(); + const PREAMBLE: &[u8] = b"PicoROM Hello"; + + if USB_DEVICE.is_none() { + *USB_DEVICE = cortex_m::interrupt::free(|cs| GLOBAL_USB_DEVICE.borrow(cs).take()); + } + + if USB_SERIAL.is_none() { + *USB_SERIAL = cortex_m::interrupt::free(|cs| GLOBAL_USB_SERIAL.borrow(cs).take()); + } + + let Some(usb_dev) = USB_DEVICE else { return }; + let Some(serial) = USB_SERIAL else { return }; + + if !serial.rts() { + *DO_PREAMBLE = true; + } + if *DO_PREAMBLE && serial.rts() { + match serial.write(PREAMBLE) { + Ok(count) if count == PREAMBLE.len() => { + *DO_PREAMBLE = false; + defmt::info!("Send Preamble") + } + Ok(count) => { + defmt::warn!( + "Unable to write the whole preamble. Only wrote {} Bytes", + count + ); + } + Err(UsbError::WouldBlock) => (), + Err(e) => { + defmt::error!("Unable to write preamble: {}", e) + } + } + } + + if usb_dev.poll(&mut [serial]) { + let mut buf = [0u8; 64]; + match serial.read(&mut buf) { + Ok(0) => {} + Ok(count) => { + for byte in buf.into_iter().take(count) { + if let Some(packet) = PACKET_BUILDER.push(byte) { + defmt::debug!("Got packet: {}", packet); + if let Some(response) = LINK.handle_packet(&packet) { + defmt::info!("Sending Response: {}", response); + if let Err(e) = response.send(serial) + && e != UsbError::WouldBlock + { + defmt::error!("Unable to send response: {}", e); + } + } + } + } + } + Err(UsbError::WouldBlock) => (), + Err(e) => { + defmt::error!("USB Error: {}", e); + } + } + } + + if usb_dev.state() == UsbDeviceState::Default { + *DO_PREAMBLE = true; + } +} diff --git a/src/serial.rs b/src/serial.rs new file mode 100644 index 0000000..13ea13d --- /dev/null +++ b/src/serial.rs @@ -0,0 +1,280 @@ +use core::{borrow::BorrowMut, sync::atomic::Ordering}; + +use defmt::Format; +use heapless::{String, Vec}; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use usb_device::{UsbError, bus::UsbBus}; +use usbd_serial::SerialPort; + +use crate::ROM_DATA; + +const MAX_PAYLOAD: u8 = 30; + +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Format, TryFromPrimitive, IntoPrimitive, +)] +#[repr(u8)] +pub enum Kind { + PointerSet = 3, + PointerGet = 4, + PointerCur = 5, + Write = 6, + Read = 7, + ReadData = 8, + + CommitFlash = 12, + CommitDone = 13, + + ParameterSet = 20, + ParameterGet = 21, + Parameter = 22, + ParameterError = 23, + ParameterQuery = 24, + + CommsStart = 80, + CommsEnd = 81, + CommsData = 82, + + Identify = 0xf8, + Bootsel = 0xf9, + Error = 0xfe, + Debug = 0xff, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +#[repr(C)] +pub struct Packet { + pub kind: Kind, + pub payload: Vec, +} + +impl Format for Packet { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!( + fmt, + "Packet {{ kind: {}, payload: {:x}}}", + self.kind, + &self.payload[..] + ); + } +} + +impl Packet { + pub fn send( + &self, + serial: &mut SerialPort, impl BorrowMut<[u8]>>, + ) -> Result<(), UsbError> { + serial.write(&[ + self.kind.into(), + u8::try_from(self.payload.len()).unwrap_or(MAX_PAYLOAD), + ])?; + serial.write(&self.payload)?; + Ok(()) + } + + pub fn error(reason: &[u8; N]) -> Self { + Self { + kind: Kind::Error, + payload: Vec::from_array(*reason), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum PacketBuilder { + Idle, + Kind { + kind: Kind, + }, + Payload { + kind: Kind, + target_size: u8, + payload: Vec, + }, +} + +impl PacketBuilder { + pub const fn new() -> Self { + Self::Idle + } + + pub fn push(&mut self, byte: u8) -> Option { + match self { + Self::Idle => { + let kind = Kind::try_from(byte).ok()?; + *self = Self::Kind { kind }; + } + Self::Kind { kind } => { + let target_size = byte; + let kind = *kind; + match target_size { + 0 => { + *self = Self::Idle; + return Some(Packet { + kind, + payload: Vec::new(), + }); + } + 1..=MAX_PAYLOAD => { + *self = Self::Payload { + kind, + target_size, + payload: Vec::new(), + }; + } + _ => { + *self = Self::Idle; + } + } + } + Self::Payload { + kind, + target_size, + payload, + } => { + payload.push(byte).unwrap(); + if payload.len() >= usize::from(*target_size) { + let kind = *kind; + let payload = payload.clone(); + *self = Self::Idle; + return Some(Packet { kind, payload }); + } + } + } + None + } +} + +pub struct Link { + pointer: u32, + name: String<{ MAX_PAYLOAD as usize }>, + rom_name: String<{ MAX_PAYLOAD as usize }>, +} + +impl Link { + pub const fn new() -> Self { + Self { + pointer: 0, + name: String::new(), + rom_name: String::new(), + } + } + + pub fn handle_packet(&mut self, packet: &Packet) -> Option { + if self.name.is_empty() { + let _ = self.name.push_str("rom"); + } + match packet.kind { + Kind::PointerSet => { + if packet.payload.len() != 4 { + return Some(Packet::error(b"Invalid pointer length")); + } + self.pointer = u32::from_le_bytes(packet.payload[..].try_into().unwrap()); + defmt::info!("Set pointer to {:#010x}", self.pointer); + None + } + Kind::PointerGet => Some(Packet { + kind: Kind::PointerCur, + payload: Vec::from_array(self.pointer.to_le_bytes()), + }), + Kind::Write => { + for byte in &packet.payload { + let Some(target) = ROM_DATA.get(self.pointer as usize) else { + self.pointer += 1; + // return Some(Packet::error(b"Write out of range")); + continue; + }; + target.store(*byte, Ordering::Relaxed); + self.pointer += 1; + } + None + } + Kind::ParameterGet => match &packet.payload[..] { + b"name\0" => Some(Packet { + kind: Kind::Parameter, + payload: self.name.clone().into_bytes(), + }), + b"addr_mask\0" => Some(Packet { + kind: Kind::Parameter, + payload: Vec::from_array(*b"0x7fff"), + }), + b"rom_name\0" => Some(Packet { + kind: Kind::Parameter, + payload: self.rom_name.clone().into_bytes(), + }), + payload => { + if let Ok(name) = str::from_utf8(payload) { + defmt::warn!("Got unknonwn parameter request: {}", name); + } else { + defmt::warn!("Got unknonw parameter request: {}", payload); + } + Some(Packet { + kind: Kind::ParameterError, + payload: Vec::new(), + }) + } + }, + Kind::ParameterSet => { + let mut parts = packet.payload.split(|b| *b == b','); + let Some(name) = parts.next() else { + return Some(Packet { + kind: Kind::ParameterError, + payload: Vec::new(), + }); + }; + let Some(value) = parts.next() else { + return Some(Packet { + kind: Kind::ParameterError, + payload: Vec::new(), + }); + }; + match name { + b"name" => { + let Ok(value) = str::from_utf8(value) else { + return Some(Packet { + kind: Kind::ParameterError, + payload: Vec::new(), + }); + }; + self.name.clear(); + let _ = self.name.push_str(value); + Some(Packet { + kind: Kind::Parameter, + payload: self.name.clone().into_bytes(), + }) + } + b"addr_mask" => Some(Packet { + kind: Kind::Parameter, + payload: Vec::from_array(*b"0x7fff"), + }), + b"rom_name" => { + let Ok(value) = str::from_utf8(value) else { + return Some(Packet { + kind: Kind::ParameterError, + payload: Vec::new(), + }); + }; + self.rom_name.clear(); + let _ = self.rom_name.push_str(value); + Some(Packet { + kind: Kind::Parameter, + payload: self.rom_name.clone().into_bytes(), + }) + } + payload => { + if let Ok(name) = str::from_utf8(payload) { + defmt::warn!("Got unknonwn parameter request: {}", name); + } else { + defmt::warn!("Got unknonw parameter request: {}", payload); + } + Some(Packet { + kind: Kind::ParameterError, + payload: Vec::new(), + }) + } + } + } + _ => Some(Packet::error(b"Unrecognized packet")), + } + } +}