aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorIan McIntyre <me@mciantyre.dev>2026-08-03 07:46:10 -0400
committerIan McIntyre <me@mciantyre.dev>2026-08-03 07:46:10 -0400
commit7ffc62ab8613b097485b2c664093e649907dc2a1 (patch)
tree428e6e958af00e1022e663676fffe9c19f0174a6 /crates
First commit
Diffstat (limited to 'crates')
-rw-r--r--crates/cortex-m/Cargo.toml7
-rw-r--r--crates/cortex-m/build.rs14
-rw-r--r--crates/cortex-m/src/lib.rs1
-rw-r--r--crates/imxrt1170evk/Cargo.toml17
-rw-r--r--crates/imxrt1170evk/build.rs17
-rw-r--r--crates/imxrt1170evk/src/lib.rs117
-rw-r--r--crates/imxrt1170evk/src/main.rs10
-rw-r--r--crates/qemu/Cargo.toml15
-rw-r--r--crates/qemu/src/lib.rs37
-rw-r--r--crates/qemu/src/main.rs72
-rw-r--r--crates/stdio-imxrt-usbd/Cargo.toml13
-rw-r--r--crates/stdio-imxrt-usbd/src/lib.rs297
-rw-r--r--crates/stdio-semihosting/.gitignore1
-rw-r--r--crates/stdio-semihosting/Cargo.toml8
-rw-r--r--crates/stdio-semihosting/src/lib.rs52
-rw-r--r--crates/teensy4/Cargo.toml12
-rw-r--r--crates/teensy4/src/lib.rs67
-rw-r--r--crates/teensy4/src/main.rs13
18 files changed, 770 insertions, 0 deletions
diff --git a/crates/cortex-m/Cargo.toml b/crates/cortex-m/Cargo.toml
new file mode 100644
index 0000000..9f18dd2
--- /dev/null
+++ b/crates/cortex-m/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "rust-threadx-cortex-m"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[dependencies]
diff --git a/crates/cortex-m/build.rs b/crates/cortex-m/build.rs
new file mode 100644
index 0000000..286e738
--- /dev/null
+++ b/crates/cortex-m/build.rs
@@ -0,0 +1,14 @@
+use std::{env, path};
+
+fn main() {
+ let out_dir = path::PathBuf::from(env::var("OUT_DIR").unwrap());
+ std::fs::write(
+ out_dir.join("weak-symbols.x"),
+ r#"
+PROVIDE(PendSV = PendSV_Handler);
+PROVIDE(SysTick = _tx_timer_interrupt);
+"#,
+ )
+ .unwrap();
+ println!("cargo::rustc-link-search={}", out_dir.display());
+}
diff --git a/crates/cortex-m/src/lib.rs b/crates/cortex-m/src/lib.rs
new file mode 100644
index 0000000..0c9ac1a
--- /dev/null
+++ b/crates/cortex-m/src/lib.rs
@@ -0,0 +1 @@
+#![no_std]
diff --git a/crates/imxrt1170evk/Cargo.toml b/crates/imxrt1170evk/Cargo.toml
new file mode 100644
index 0000000..995eff0
--- /dev/null
+++ b/crates/imxrt1170evk/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "rust-threadx-imxrt1170evk"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[dependencies]
+imxrt-ral = { workspace = true, features = ["imxrt1176_cm7"] }
+imxrt-rt = { workspace = true }
+imxrt1170evk-fcb = { version = "0.2" }
+
+cortex-m = { workspace = true }
+rust-threadx-cortex-m = { workspace = true }
+rust-threadx-stdio-semihosting = { workspace = true }
+
+[build-dependencies]
+imxrt-rt = { workspace = true }
diff --git a/crates/imxrt1170evk/build.rs b/crates/imxrt1170evk/build.rs
new file mode 100644
index 0000000..6d4b58e
--- /dev/null
+++ b/crates/imxrt1170evk/build.rs
@@ -0,0 +1,17 @@
+use imxrt_rt::{Family, Memory, RuntimeBuilder};
+
+fn main() {
+ RuntimeBuilder::from_flexspi(Family::Imxrt1170, 16 * 1024 * 1024)
+ .text(Memory::Itcm)
+ .vectors(Memory::Itcm)
+ .data(Memory::Dtcm)
+ .bss(Memory::Dtcm)
+ .uninit(Memory::Ocram)
+ .rodata(Memory::Ocram)
+ .stack(Memory::Dtcm)
+ .heap(Memory::Ocram)
+ .heap_size(8 * 1024)
+ .stack_size(4 * 1024)
+ .build()
+ .unwrap();
+}
diff --git a/crates/imxrt1170evk/src/lib.rs b/crates/imxrt1170evk/src/lib.rs
new file mode 100644
index 0000000..b626843
--- /dev/null
+++ b/crates/imxrt1170evk/src/lib.rs
@@ -0,0 +1,117 @@
+#![no_std]
+#![feature(rustc_private)]
+extern crate fusible;
+
+use core::pin::Pin;
+
+use imxrt_ral as ral;
+use imxrt_rt as _;
+use imxrt1170evk_fcb as _;
+use rust_threadx_cortex_m as _;
+use rust_threadx_stdio_semihosting as _;
+
+#[doc(hidden)]
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn _tx_initialize_low_level() {
+ use cortex_m::{
+ Peripherals,
+ peripheral::{scb::SystemHandler, syst::SystClkSource},
+ };
+
+ cortex_m::interrupt::disable();
+
+ unsafe {
+ let Peripherals {
+ mut SYST, mut SCB, ..
+ } = Peripherals::steal();
+
+ SCB.set_priority(SystemHandler::SysTick, 0x40);
+ SCB.set_priority(SystemHandler::PendSV, 0xff);
+ SCB.set_priority(SystemHandler::SVCall, 0xff);
+
+ SYST.set_clock_source(SystClkSource::External);
+ SYST.set_reload(100_000 / 100 - 1);
+ SYST.clear_current();
+ SYST.enable_interrupt();
+ SYST.enable_counter();
+ }
+
+ unsafe {
+ use ral::ccm;
+
+ let ccm = ccm::CCM::instance();
+ let clock_root = &ccm.CLOCK_ROOT[8];
+ ral::modify_reg!(ccm::clockroot, clock_root, CLOCK_ROOT_CONTROL, MUX: 1, DIV: 240 - 1);
+ }
+
+ unsafe {
+ use ral::iomuxc;
+ let iomuxc = iomuxc::IOMUXC::instance();
+
+ ral::write_reg!(iomuxc, iomuxc, SW_MUX_CTL_PAD_GPIO_AD_04, MUX_MODE: 5, SION: 0);
+
+ ral::write_reg!(iomuxc, iomuxc, SW_PAD_CTL_PAD_GPIO_AD_04, PUE: 0);
+ }
+
+ unsafe {
+ use ral::gpio;
+ let gpio3 = gpio::GPIO3::instance();
+
+ const GPIO3_OUTPUTS: u32 = led::MASK;
+
+ ral::write_reg!(gpio, gpio3, GDIR, GPIO3_OUTPUTS);
+ }
+}
+
+/// Controls the LED.
+pub mod led {
+ use crate::ral::gpio;
+
+ pub(crate) const MASK: u32 = 1 << 3;
+
+ /// Turn on the LED.
+ #[inline]
+ pub fn set() {
+ unsafe { crate::ral::write_reg!(gpio, gpio::GPIO3, DR_SET, MASK) };
+ }
+
+ /// Turn off the LED.
+ #[inline]
+ pub fn clear() {
+ unsafe { crate::ral::write_reg!(gpio, gpio::GPIO3, DR_CLEAR, MASK) };
+ }
+
+ /// Toggle the LED.
+ ///
+ /// This is achieved in hardware, without reading the current state
+ /// of the LED. It's usually more efficient.
+ #[inline]
+ pub fn toggle() {
+ unsafe { crate::ral::write_reg!(gpio, gpio::GPIO3, DR_TOGGLE, MASK) };
+ }
+
+ /// Drive the LED on or off.
+ #[inline]
+ pub fn drive(value: bool) {
+ if value { set() } else { clear() }
+ }
+}
+
+use fusible::thread::{StaticStack, Thread, ThreadContext};
+static STACK: StaticStack<512> = StaticStack::new();
+static THREAD: ThreadContext = Thread::context();
+
+#[unsafe(no_mangle)]
+#[doc(hidden)]
+pub extern "C" fn __rust_threadx_app_define() {
+ Thread::create(
+ Pin::static_ref(&THREAD),
+ STACK.take().unwrap(),
+ &Default::default(),
+ || loop {
+ fusible::thread::sleep(25);
+ led::toggle();
+ },
+ )
+ .unwrap();
+}
diff --git a/crates/imxrt1170evk/src/main.rs b/crates/imxrt1170evk/src/main.rs
new file mode 100644
index 0000000..737a6cc
--- /dev/null
+++ b/crates/imxrt1170evk/src/main.rs
@@ -0,0 +1,10 @@
+use rust_threadx_imxrt1170evk as _;
+
+fn main() {
+ let mut count = 0_usize;
+ loop {
+ println!("Hello world! The count is {count}");
+ count = count.wrapping_add(1);
+ std::thread::sleep(std::time::Duration::from_millis(500))
+ }
+}
diff --git a/crates/qemu/Cargo.toml b/crates/qemu/Cargo.toml
new file mode 100644
index 0000000..904f696
--- /dev/null
+++ b/crates/qemu/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "rust-threadx-qemu"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[dependencies]
+cortex-m = { workspace = true }
+cortex-m-rt = { workspace = true }
+cortex-m-semihosting = { workspace = true }
+lm3s6965 = { workspace = true }
+panic-semihosting = { version = "0.6", features = ["exit"] }
+
+rust-threadx-cortex-m = { workspace = true }
+rust-threadx-stdio-semihosting = { workspace = true }
diff --git a/crates/qemu/src/lib.rs b/crates/qemu/src/lib.rs
new file mode 100644
index 0000000..dc91465
--- /dev/null
+++ b/crates/qemu/src/lib.rs
@@ -0,0 +1,37 @@
+#![no_std]
+
+use cortex_m_rt as _;
+use lm3s6965 as _;
+
+use rust_threadx_cortex_m as _;
+use rust_threadx_stdio_semihosting as _;
+
+pub fn exit_success() -> ! {
+ use cortex_m_semihosting::debug::*;
+ exit(EXIT_SUCCESS);
+ unreachable!()
+}
+
+// Designed for ARMv6-M. Just enough to get the example
+// running in QEMU with a meaningless system tick.
+#[unsafe(no_mangle)]
+pub extern "C" fn _tx_initialize_low_level() {
+ use cortex_m::{Peripherals, peripheral::scb::SystemHandler};
+
+ cortex_m::interrupt::disable();
+
+ let Peripherals {
+ mut SYST, mut SCB, ..
+ } = Peripherals::take().unwrap();
+
+ SYST.set_reload(80000 - 1);
+ SYST.clear_current();
+ SYST.enable_interrupt();
+ SYST.enable_counter();
+
+ unsafe {
+ SCB.set_priority(SystemHandler::SysTick, 0x40);
+ SCB.set_priority(SystemHandler::PendSV, 0xff);
+ SCB.set_priority(SystemHandler::SVCall, 0xff);
+ }
+}
diff --git a/crates/qemu/src/main.rs b/crates/qemu/src/main.rs
new file mode 100644
index 0000000..3a3df38
--- /dev/null
+++ b/crates/qemu/src/main.rs
@@ -0,0 +1,72 @@
+use rust_threadx_qemu::exit_success;
+use std::{
+ sync::{Arc, Condvar, Mutex},
+ time::Duration,
+};
+
+fn main() {
+ println!("Hello, world!");
+ println!("Testing allocations in 0.25 seconds...");
+ std::thread::sleep(Duration::from_millis(250));
+ test_heap_allocation();
+
+ let mtx = Arc::new(Mutex::new(0_u32));
+ let cv = Arc::new(Condvar::new());
+
+ std::thread::scope(|s| {
+ s.spawn(|| {
+ println!("Hello from one thread!");
+ let mut guard = mtx.lock().unwrap();
+ *guard += 1;
+ cv.notify_all();
+ });
+ });
+
+ let (m, c) = (mtx.clone(), cv.clone());
+ std::thread::spawn(move || {
+ println!("Hello from another thread!");
+
+ let guard = m.lock().unwrap();
+ let mut guard = c.wait_while(guard, |c| *c != 1).unwrap();
+ *guard += 1;
+ c.notify_all();
+ })
+ .join()
+ .unwrap();
+
+ let guard = mtx.lock().unwrap();
+ let guard = cv.wait_while(guard, |c| *c != 2).unwrap();
+ assert_eq!(*guard, 2);
+
+ echo_stdin();
+ exit_success();
+}
+
+fn echo_stdin() {
+ use std::io::{self, Write};
+
+ print!("Enter a message: ");
+ // `print!` does not flush, so make sure the prompt reaches the host before
+ // we block waiting on input.
+ io::stdout().flush().ok();
+
+ let mut message = String::new();
+ match io::stdin().read_line(&mut message) {
+ Ok(0) => println!("No message received (EOF)"),
+ Ok(_) => println!("You said: {}", message.trim_end()),
+ Err(e) => println!("Failed to read message: {e}"),
+ }
+}
+
+fn test_heap_allocation() {
+ use core::hint::black_box;
+
+ for len in [32usize, 64, 128] {
+ let mut v: Vec<usize> = Vec::with_capacity(len);
+ for i in 0..len {
+ v.push(black_box(i));
+ }
+ let v = black_box(v);
+ println!("Allocated vector with {} elements", v.len());
+ }
+}
diff --git a/crates/stdio-imxrt-usbd/Cargo.toml b/crates/stdio-imxrt-usbd/Cargo.toml
new file mode 100644
index 0000000..ce3012e
--- /dev/null
+++ b/crates/stdio-imxrt-usbd/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "rust-threadx-stdio-imxrt-usbd"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[dependencies]
+imxrt-usbd = "0.4"
+usb-device = "0.3"
+usbd-serial = "0.2"
+
+cortex-m = { workspace = true }
+imxrt-ral = { workspace = true }
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.
+}
diff --git a/crates/stdio-semihosting/.gitignore b/crates/stdio-semihosting/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/crates/stdio-semihosting/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/crates/stdio-semihosting/Cargo.toml b/crates/stdio-semihosting/Cargo.toml
new file mode 100644
index 0000000..3178f61
--- /dev/null
+++ b/crates/stdio-semihosting/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "rust-threadx-stdio-semihosting"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[dependencies]
+cortex-m-semihosting = { workspace = true }
diff --git a/crates/stdio-semihosting/src/lib.rs b/crates/stdio-semihosting/src/lib.rs
new file mode 100644
index 0000000..1a18b84
--- /dev/null
+++ b/crates/stdio-semihosting/src/lib.rs
@@ -0,0 +1,52 @@
+//! ThreadX stdio implemented over semihosting
+
+#![no_std]
+
+#[unsafe(no_mangle)]
+fn __rust_threadx_stdout_write(buf: &[u8]) -> usize {
+ use cortex_m_semihosting::hio;
+ // `write_all` loops until the whole buffer is sent or the host reports an
+ // error, so a success means every byte was written.
+ let Ok(mut out) = hio::hstdout() else {
+ return 0;
+ };
+ match out.write_all(buf) {
+ Ok(()) => buf.len(),
+ Err(()) => 0,
+ }
+}
+
+#[unsafe(no_mangle)]
+fn __rust_threadx_stderr_write(buf: &[u8]) -> usize {
+ use cortex_m_semihosting::hio;
+ let Ok(mut out) = hio::hstderr() else {
+ return 0;
+ };
+ match out.write_all(buf) {
+ Ok(()) => buf.len(),
+ Err(()) => 0,
+ }
+}
+
+#[unsafe(no_mangle)]
+fn __rust_threadx_stdin_read(buf: &mut [u8]) -> usize {
+ use cortex_m_semihosting::{nr, syscall};
+
+ if buf.is_empty() {
+ return 0;
+ }
+
+ // `hio` only opens the host console for writing, so open it for reading
+ // directly. `:tt` is the special semihosting path for the console.
+ let name = b":tt\0";
+ let fd = match unsafe { syscall!(OPEN, name.as_ptr(), nr::open::R, name.len() - 1) } as isize {
+ -1 => return 0,
+ fd => fd as usize,
+ };
+
+ // SYS_READ returns the number of bytes that were *not* read; on error it
+ // returns a value larger than the request, which `saturating_sub` maps to 0.
+ let not_read = unsafe { syscall!(READ, fd, buf.as_mut_ptr(), buf.len()) };
+ let _ = unsafe { syscall!(CLOSE, fd) };
+ buf.len().saturating_sub(not_read)
+}
diff --git a/crates/teensy4/Cargo.toml b/crates/teensy4/Cargo.toml
new file mode 100644
index 0000000..4b196d5
--- /dev/null
+++ b/crates/teensy4/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "rust-threadx-teensy4"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[dependencies]
+cortex-m = { workspace = true }
+teensy4-bsp = { version = "0.6", features = ["rt"] }
+
+rust-threadx-cortex-m = { workspace = true }
+rust-threadx-stdio-imxrt-usbd = { workspace = true }
diff --git a/crates/teensy4/src/lib.rs b/crates/teensy4/src/lib.rs
new file mode 100644
index 0000000..35afb31
--- /dev/null
+++ b/crates/teensy4/src/lib.rs
@@ -0,0 +1,67 @@
+#![no_std]
+#![feature(rustc_private)]
+extern crate fusible;
+
+use core::pin::Pin;
+
+use rust_threadx_cortex_m as _;
+use rust_threadx_stdio_imxrt_usbd as stdio;
+
+#[doc(hidden)]
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn _tx_initialize_low_level() {
+ use cortex_m::{
+ Peripherals,
+ peripheral::{scb::SystemHandler, syst::SystClkSource},
+ };
+
+ cortex_m::interrupt::disable();
+
+ unsafe {
+ let Peripherals {
+ mut SYST, mut SCB, ..
+ } = Peripherals::steal();
+
+ SCB.set_priority(SystemHandler::SysTick, 0x40);
+ SCB.set_priority(SystemHandler::PendSV, 0xff);
+ SCB.set_priority(SystemHandler::SVCall, 0xff);
+
+ SYST.set_clock_source(SystClkSource::External);
+ SYST.set_reload(100_000 / 100 - 1);
+ SYST.clear_current();
+ SYST.enable_interrupt();
+ SYST.enable_counter();
+ }
+}
+
+use fusible::thread::{StaticStack, Thread, ThreadContext};
+static STACK: StaticStack<512> = StaticStack::new();
+static THREAD: ThreadContext = Thread::context();
+
+#[unsafe(no_mangle)]
+#[doc(hidden)]
+pub extern "C" fn __rust_threadx_app_define() {
+ let instances = teensy4_bsp::board::instances();
+ let mut resources = teensy4_bsp::board::t40(instances);
+
+ let led = teensy4_bsp::board::led(&mut resources.gpio2, resources.pins.p13);
+ Thread::create(
+ Pin::static_ref(&THREAD),
+ STACK.take().unwrap(),
+ &Default::default(),
+ move || loop {
+ fusible::thread::sleep(50);
+ led.toggle();
+ },
+ )
+ .unwrap();
+
+ stdio::start(resources.usb, interrupt::USB_OTG1);
+}
+
+use teensy4_bsp::{ral::interrupt, rt::interrupt};
+
+#[interrupt]
+fn USB_OTG1() {
+ stdio::on_interrupt(interrupt::USB_OTG1);
+}
diff --git a/crates/teensy4/src/main.rs b/crates/teensy4/src/main.rs
new file mode 100644
index 0000000..16a1a1b
--- /dev/null
+++ b/crates/teensy4/src/main.rs
@@ -0,0 +1,13 @@
+use std::thread;
+use std::time::Duration;
+
+use rust_threadx_teensy4 as _;
+
+fn main() {
+ let mut count = 0_usize;
+ loop {
+ thread::sleep(Duration::from_millis(500));
+ println!("Hello world! The count is {count}\r");
+ count = count.wrapping_add(1);
+ }
+}