aboutsummaryrefslogtreecommitdiff
path: root/crates/stdio-imxrt-usbd/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/stdio-imxrt-usbd/src/lib.rs')
-rw-r--r--crates/stdio-imxrt-usbd/src/lib.rs297
1 files changed, 297 insertions, 0 deletions
diff --git a/crates/stdio-imxrt-usbd/src/lib.rs b/crates/stdio-imxrt-usbd/src/lib.rs
new file mode 100644
index 0000000..dbd4f66
--- /dev/null
+++ b/crates/stdio-imxrt-usbd/src/lib.rs
@@ -0,0 +1,297 @@
+//! Route stdout through a USB serial interface
+//!
+//! You'll notice this looks pretty similar to imxrt-log's usbd backend.
+//! I replace the bbqueue with a fusible Queue and BlockPool, and I run
+//! the "backend" on a fusible Thread.
+//!
+//! You're responsible for registering the USB interrupt and calling
+//! `on_interrupt` in that interrupt. You'll also need to call `start`
+//! to set up the driver.
+//!
+//! This only supports stdout; I haven't gotten stdin working yet.
+
+#![no_std]
+#![feature(rustc_private)]
+extern crate fusible;
+
+use usb_device::device::UsbDeviceState;
+
+const VID_PID: usb_device::device::UsbVidPid = usb_device::device::UsbVidPid(0x5824, 0x27dd);
+const PRODUCT: &str = "rust-threadx-usbd";
+
+/// Provide some extra overhead for the interrupt endpoint.
+///
+/// If you start noticing panics, check to make sure that this buffer
+/// is large enough for all the max packet sizes for all the endpoints.
+const ENDPOINT_BYTES: usize = MAX_PACKET_SIZE * 2 + EP0_CONTROL_PACKET_SIZE * 2 + 128;
+static ENDPOINT_MEMORY: imxrt_usbd::EndpointMemory<ENDPOINT_BYTES> =
+ imxrt_usbd::EndpointMemory::new();
+static ENDPOINT_STATE: imxrt_usbd::EndpointState<6> = imxrt_usbd::EndpointState::new();
+
+type Bus = imxrt_usbd::BusAdapter;
+type Class<'a> = usbd_serial::CdcAcmClass<'a, Bus>;
+type Device<'a> = usb_device::device::UsbDevice<'a, Bus>;
+
+/// High-speed bulk endpoint limit.
+const MAX_PACKET_SIZE: usize = 512;
+/// Size for control transfers on endpoint 0.
+const EP0_CONTROL_PACKET_SIZE: usize = 64;
+/// The USB GPT timer we use to (infrequently) check for data.
+const GPT_INSTANCE: imxrt_usbd::gpt::Instance = imxrt_usbd::gpt::Instance::Gpt0;
+
+struct Backend<'a> {
+ class: Class<'a>,
+ device: Device<'a>,
+ configured: bool,
+ stdout_queue: &'static BlockQueue,
+}
+
+impl Backend<'_> {
+ fn poll(&mut self) {
+ // Is there a CDC class event, like a completed transfer? If so, check
+ // the consumer immediately, even if a timer hasn't expired.
+ //
+ // Checking the consumer on class traffic lets the driver burst out data.
+ // Suppose the user wants to use the USB GPT timer, and they configure a very
+ // long interval. That interval expires, and we see tons of data in the consumer.
+ // We should write that out as fast as possible, even if the timer hasn't elapsed.
+ // That's the behavior provided by the class_event flag.
+ let class_event = self.device.poll(&mut [&mut self.class]);
+ let timer_event = self.device.bus().gpt_mut(GPT_INSTANCE, |gpt| {
+ let mut elapsed = false;
+ while gpt.is_elapsed() {
+ gpt.clear_elapsed();
+ elapsed = true;
+ }
+ // Simulate a timer event if the timer is not running.
+ //
+ // If the timer is not running, its because the user disabled interrupts,
+ // and they're using their own timer / polling loop. There might not always
+ // be a class traffic (transfer complete) event when the user polls, so
+ // signaling true allows the poll to check the consumer for new data and
+ // send it.
+ //
+ // If the timer is running, checking the consumer depends on the elapsed
+ // timer.
+ elapsed || !gpt.is_running()
+ });
+ let check_consumer = class_event || timer_event;
+
+ if self.device.state() != UsbDeviceState::Configured {
+ if self.configured {
+ // Turn off the timer, but only if we were previously configured.
+ self.device.bus().gpt_mut(GPT_INSTANCE, |gpt| gpt.stop());
+ }
+ self.configured = false;
+ // We can't use the class if we're not configured,
+ // so bail out here.
+ return;
+ }
+
+ // We're now configured. Are we newly configured?
+ if !self.configured {
+ // Must call this when we transition into configured.
+ self.device.bus().configure();
+ self.device.bus().gpt_mut(GPT_INSTANCE, |gpt| {
+ // There's no need for a timer if interrupts are disabled.
+ // If the user disabled USB interrupts and decided to poll this
+ // from another timer, this USB timer could unnecessarily block
+ // that timer from checking the consumer queue.
+ if gpt.is_interrupt_enabled() {
+ gpt.run()
+ }
+ });
+ self.configured = true;
+ }
+
+ self.class.read_packet(&mut []).ok();
+
+ if check_consumer && let Some(block) = self.stdout_queue.try_receive() {
+ // Ignoring errors here... and we're not really utilizing the full
+ // width of the endpoint buffer. Let's see how long we can get
+ // away with this...
+ let _ = self.class.write_packet(&block.byte_buffer[..block.len]);
+ }
+ }
+}
+
+fn run<const N: u8>(
+ peripherals: imxrt_usbd::Instances<N>,
+ interrupt: imxrt_ral::Interrupt,
+ stdout_queue: &'static BlockQueue,
+) -> ! {
+ let events = Semaphore::create(Pin::static_ref(&EVENTS), &Default::default()).unwrap();
+
+ let bus = {
+ // Safety: we ensure that the bus, class, and all other related USB objects
+ // are accessed in poll(). poll() is not reentrant, so there's no racing
+ // occuring across executing contexts.
+ let bus = unsafe {
+ imxrt_usbd::BusAdapter::without_critical_sections(
+ peripherals,
+ &ENDPOINT_MEMORY,
+ &ENDPOINT_STATE,
+ imxrt_usbd::Speed::High,
+ )
+ };
+ bus.set_interrupts(true);
+ bus.gpt_mut(GPT_INSTANCE, |gpt| {
+ gpt.stop();
+ gpt.clear_elapsed();
+ gpt.set_interrupt_enabled(true);
+ gpt.set_mode(imxrt_usbd::gpt::Mode::Repeat);
+ gpt.set_load(4_000);
+ gpt.reset();
+ });
+ usb_device::bus::UsbBusAllocator::new(bus)
+ };
+ let class = usbd_serial::CdcAcmClass::new(&bus, MAX_PACKET_SIZE as u16);
+
+ let device = usb_device::device::UsbDeviceBuilder::new(&bus, VID_PID)
+ .strings(&[usb_device::device::StringDescriptors::default().product(PRODUCT)])
+ .unwrap()
+ .device_class(usbd_serial::USB_CLASS_CDC)
+ .max_packet_size_0(EP0_CONTROL_PACKET_SIZE as u8)
+ .unwrap()
+ .build();
+
+ // Not sure which endpoints the CDC ACM class will pick,
+ // so enable the setting for all non-zero endpoints.
+ for idx in 1..8 {
+ for dir in &[usb_device::UsbDirection::In, usb_device::UsbDirection::Out] {
+ let ep_addr = usb_device::endpoint::EndpointAddress::from_parts(idx, *dir);
+ // CDC class requires that we send the ZLP.
+ // Let the hardware do that for us.
+ device.bus().enable_zlt(ep_addr);
+ }
+ }
+
+ let mut backend = Backend {
+ class,
+ device,
+ configured: false,
+ stdout_queue,
+ };
+
+ loop {
+ backend.poll();
+ unsafe { cortex_m::peripheral::NVIC::unmask(interrupt) };
+ events.get().unwrap();
+ }
+}
+
+use core::pin::Pin;
+use fusible::semaphore::{Semaphore, SemaphoreContext};
+
+static EVENTS: SemaphoreContext = Semaphore::context();
+
+#[inline]
+pub fn on_interrupt(intr: imxrt_ral::Interrupt) {
+ cortex_m::peripheral::NVIC::mask(intr);
+ if let Some(events) = Pin::static_ref(&EVENTS).try_created() {
+ events.put();
+ }
+}
+
+use fusible::thread::{StaticStack, Thread, ThreadContext};
+
+static STACK: StaticStack<2048> = StaticStack::new();
+static THREAD: ThreadContext = Thread::context();
+
+pub fn start<const N: u8>(peripherals: imxrt_usbd::Instances<N>, interrupt: imxrt_ral::Interrupt) {
+ BlockPool::create(
+ Pin::static_ref(&BLOCK_POOL),
+ BLOCK_STORAGE.take().unwrap(),
+ &Default::default(),
+ )
+ .unwrap();
+
+ let stdout_queue = Queue::create(
+ Pin::static_ref(&STDOUT_QUEUE),
+ STDOUT_STORAGE.take().unwrap(),
+ &Default::default(),
+ )
+ .unwrap();
+
+ Thread::create(
+ Pin::static_ref(&THREAD),
+ STACK.take().unwrap(),
+ &Default::default(),
+ move || run(peripherals, interrupt, stdout_queue),
+ )
+ .unwrap();
+}
+
+use fusible::block_pool::{Block, BlockPool, BlockPoolContext, StaticBlocks};
+
+type ByteBuffer = [u8; MAX_PACKET_SIZE];
+
+struct BlockWithLen {
+ len: usize,
+ byte_buffer: ByteBuffer,
+}
+
+impl BlockWithLen {
+ fn copy_of(buf: &[u8]) -> Self {
+ let mut byte_buffer: ByteBuffer = [0_u8; _];
+ let len = byte_buffer.len().min(buf.len());
+ byte_buffer[..len].copy_from_slice(&buf[..len]);
+ Self { len, byte_buffer }
+ }
+}
+
+const BLOCK_COUNT: usize = 4;
+static BLOCK_STORAGE: StaticBlocks<BlockWithLen, BLOCK_COUNT> = StaticBlocks::new();
+static BLOCK_POOL: BlockPoolContext<BlockWithLen> = BlockPool::context();
+
+type ByteBufferBlock = Block<'static, BlockWithLen>;
+
+use fusible::queue::{Queue, QueueContext, StaticQueueSlots};
+
+static STDOUT_STORAGE: StaticQueueSlots<ByteBufferBlock, BLOCK_COUNT> = StaticQueueSlots::new();
+static STDOUT_QUEUE: QueueContext<ByteBufferBlock> = Queue::context();
+
+type BlockQueue = Queue<ByteBufferBlock>;
+
+fn stdout_write(buf: &[u8]) -> usize {
+ let mut written = 0;
+
+ let Some(block_pool) = Pin::static_ref(&BLOCK_POOL).try_created() else {
+ return written;
+ };
+
+ let Some(stdout_queue) = Pin::static_ref(&STDOUT_QUEUE).try_created() else {
+ return written;
+ };
+
+ for chunk in buf.chunks(MAX_PACKET_SIZE) {
+ let Some(block) = block_pool.try_allocate(|| {
+ let block = BlockWithLen::copy_of(&chunk);
+ written += block.len;
+ block
+ }) else {
+ break;
+ };
+
+ if stdout_queue.try_send(block).is_some() {
+ break;
+ }
+ }
+
+ written
+}
+
+#[unsafe(no_mangle)]
+fn __rust_threadx_stdout_write(buf: &[u8]) -> usize {
+ stdout_write(buf)
+}
+
+#[unsafe(no_mangle)]
+fn __rust_threadx_stderr_write(buf: &[u8]) -> usize {
+ stdout_write(buf)
+}
+
+#[unsafe(no_mangle)]
+fn __rust_threadx_stdin_read(_: &mut [u8]) -> usize {
+ 0 // Haven't gotten this working yet.
+}