From 2804340a8afaeb76076e993b7cb9cc4dc6a550c0 Mon Sep 17 00:00:00 2001 From: xarantolus Date: Sun, 10 May 2026 20:57:24 +0000 Subject: [PATCH 1/3] UART: device-tree-driven driver --- Cargo.lock | 1 + machine/cortex-m/Cargo.toml | 1 + machine/cortex-m/build.rs | 15 + machine/cortex-m/src/native.rs | 15 +- machine/cortex-m/src/native/uart.rs | 296 +++++++++ machine/cortex-m/src/stub.rs | 1 + machine/cortex-m/src/stub/uart.rs | 93 +++ .../cortex-m/st/stm32l4/interface/export.h | 3 +- machine/cortex-m/st/stm32l4/interface/uart.c | 573 +++++++++++++++--- machine/cortex-m/st/stm32l4/interface/uart.h | 84 +++ presets/stm32l4r5zi_def.toml | 1 - src/drivers.rs | 1 + src/drivers/uart/mod.rs | 284 +++++++++ src/drivers/uart/wait.rs | 193 ++++++ src/lib.rs | 2 + src/sched.rs | 30 + src/uapi.rs | 1 + src/uapi/uart.rs | 5 + xtasks/crates/dtgen/src/codegen.rs | 396 +++++++++++- xtasks/crates/dtgen/src/lib.rs | 6 + 20 files changed, 1898 insertions(+), 103 deletions(-) create mode 100644 machine/cortex-m/src/native/uart.rs create mode 100644 machine/cortex-m/src/stub/uart.rs create mode 100644 machine/cortex-m/st/stm32l4/interface/uart.h create mode 100644 src/drivers/uart/mod.rs create mode 100644 src/drivers/uart/wait.rs create mode 100644 src/uapi/uart.rs diff --git a/Cargo.lock b/Cargo.lock index cc23789..da6b8e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -778,6 +778,7 @@ dependencies = [ "cbindgen", "cmake", "critical-section", + "dtgen", "hal-api", "hal-builder", "quote", diff --git a/machine/cortex-m/Cargo.toml b/machine/cortex-m/Cargo.toml index 5871c24..ada797a 100644 --- a/machine/cortex-m/Cargo.toml +++ b/machine/cortex-m/Cargo.toml @@ -14,6 +14,7 @@ critical-section = { version = "1.2", features = ["restore-state-usize"] } [build-dependencies] hal-builder = { path = "../builder" } +dtgen = { workspace = true } cbindgen = "0.28.0" bindgen = "0.72.0" cmake = "0.1.54" diff --git a/machine/cortex-m/build.rs b/machine/cortex-m/build.rs index c33f61c..95890c0 100644 --- a/machine/cortex-m/build.rs +++ b/machine/cortex-m/build.rs @@ -309,6 +309,14 @@ fn main() { return; } + println!("cargo::rerun-if-env-changed=OSIRIS_DEBUG_UART"); + if env::var_os("OSIRIS_DEBUG_UART").is_some() { + println!( + "cargo::error=OSIRIS_DEBUG_UART is removed; the console UART is now driven by `chosen.osiris,console` in the device tree. Drop the env var from your preset / .cargo/config.toml and pick the console in DTS." + ); + std::process::exit(1); + } + let dts = hal_builder::dt::check_dts() .expect("No DeviceTree specified. Set OSIRIS_DTS_PATH to specify."); let out = hal_builder::read_path_env("OUT_DIR"); @@ -322,6 +330,13 @@ fn main() { panic!("Failed to generate device tree scripts: {e}"); } + if let Err(e) = fs::write( + out.join("uart_trampolines.h"), + dtgen::generate_uart_trampolines_h(&dt), + ) { + panic!("Failed to write uart_trampolines.h: {e}"); + } + for (vendor, name) in hal_builder::dt::soc(&dt) { let hal = Path::new(vendor).join(name); diff --git a/machine/cortex-m/src/native.rs b/machine/cortex-m/src/native.rs index 436b665..5eb3c74 100644 --- a/machine/cortex-m/src/native.rs +++ b/machine/cortex-m/src/native.rs @@ -1,5 +1,3 @@ -use core::ffi::c_char; - pub use hal_api::*; pub mod asm; @@ -9,6 +7,7 @@ pub mod i2c; pub mod panic; pub mod sched; pub mod spi; +pub mod uart; mod crit; mod print; @@ -39,20 +38,20 @@ impl hal_api::Machinelike for ArmMachine { fn init() { unsafe { bindings::init_hal(); - bindings::init_debug_uart(); + } + let _ = uart::init_console_from_dt(); + unsafe { bindings::dwt_init(); } } fn print(s: &str) -> Result<()> { let state = asm::disable_irq_save(); - - if (unsafe { bindings::write_debug_uart(s.as_ptr() as *const c_char, s.len() as i32) } != 0) - { - asm::enable_irq_restr(state); + let ok = uart::console_write(s.as_bytes()).is_ok(); + asm::enable_irq_restr(state); + if ok { Ok(()) } else { - asm::enable_irq_restr(state); Err(hal_api::PosixError::EIO) } } diff --git a/machine/cortex-m/src/native/uart.rs b/machine/cortex-m/src/native/uart.rs new file mode 100644 index 0000000..4c357dc --- /dev/null +++ b/machine/cortex-m/src/native/uart.rs @@ -0,0 +1,296 @@ +use super::device_tree; +#[cfg(not(feature = "host"))] +use super::bindings; + +pub(crate) fn console_entry() -> Option<&'static device_tree::UartRegistryEntry> { + device_tree::CONSOLE_UART.map(|i| &device_tree::UART_REGISTRY[i]) +} + +#[cfg(not(feature = "host"))] +pub(crate) fn init_console_from_dt() -> Result<()> { + let Some(entry) = console_entry() else { + return Ok(()); + }; + let cfg = cfg_from_entry(entry, &Overrides::default()); + let rc = unsafe { bindings::uart_init_console(&cfg as *const _) }; + if rc < 0 { + return Err(from_c_rc(rc)); + } + Ok(()) +} + +#[cfg(feature = "host")] +pub(crate) fn init_console_from_dt() -> Result<()> { + Ok(()) +} + +#[cfg(not(feature = "host"))] +pub(crate) fn console_write(buf: &[u8]) -> Result<()> { + let Some(entry) = console_entry() else { + return Ok(()); + }; + if buf.is_empty() { + return Ok(()); + } + // HAL_MAX_DELAY: console writes never give up. + let rc = unsafe { + bindings::uart_transmit_blocking( + entry.instance, + buf.as_ptr(), + buf.len() as i32, + u32::MAX, + ) + }; + if rc < 0 { + return Err(from_c_rc(rc)); + } + Ok(()) +} + +#[cfg(feature = "host")] +pub(crate) fn console_write(_buf: &[u8]) -> Result<()> { + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + InvalidArgument, + NoSuchDevice, + NotInitialized, + OutOfMemory, + Busy, + WouldBlock, + TimedOut, + Io, +} + +pub type Result = core::result::Result; + +#[cfg(not(feature = "host"))] +fn from_c_rc(rc: i32) -> Error { + match rc { + -1 => Error::InvalidArgument, + -2 => Error::OutOfMemory, + -3 => Error::Busy, + -4 => Error::WouldBlock, + -5 => Error::Io, + _ => Error::Io, + } +} + +#[derive(Clone, Copy)] +pub struct Device(&'static device_tree::UartRegistryEntry); + +impl Device { + pub fn entry(&self) -> &'static device_tree::UartRegistryEntry { + self.0 + } + + pub fn index(&self) -> u8 { + self.0.index + } + + pub fn instance(&self) -> usize { + self.0.instance + } +} + +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Irq { + Rx = 0, + TxDone = 1, +} + +pub type IrqHandler = extern "C" fn(Irq, *mut ()); + +pub fn get_by_index(idx: u8) -> Result { + device_tree::uart_by_index(idx) + .map(Device) + .ok_or(Error::NoSuchDevice) +} + +pub fn get(compatible: &str, ordinal: usize) -> Result { + device_tree::uart_by_compatible(compatible, ordinal) + .map(Device) + .ok_or(Error::NoSuchDevice) +} + +#[cfg(not(feature = "host"))] +fn cfg_from_entry( + entry: &device_tree::UartRegistryEntry, + overrides: &Overrides, +) -> bindings::uart_bus_cfg_t { + let mk_pin = |port: usize, line: u8, af: u8| bindings::uart_pin_cfg_t { + port, + pin: line, + af, + reserved: 0, + }; + let zero_pin = bindings::uart_pin_cfg_t { + port: 0, + pin: 0, + af: 0, + reserved: 0, + }; + let rts = entry + .rts + .map(|p| mk_pin(p.port, p.line, p.af)) + .unwrap_or(zero_pin); + let cts = entry + .cts + .map(|p| mk_pin(p.port, p.line, p.af)) + .unwrap_or(zero_pin); + + bindings::uart_bus_cfg_t { + instance: entry.instance, + tx: mk_pin(entry.tx.port, entry.tx.line, entry.tx.af), + rx: mk_pin(entry.rx.port, entry.rx.line, entry.rx.af), + rts, + cts, + baud: overrides.baud.unwrap_or(entry.baud), + data_bits: overrides.data_bits.unwrap_or(entry.data_bits), + stop_bits: overrides.stop_bits.unwrap_or(entry.stop_bits), + parity: overrides.parity.unwrap_or(entry.parity), + flow_control: overrides.flow_control.unwrap_or(entry.flow_control), + irqn: entry.irqn, + priority: entry.priority, + } +} + +#[derive(Clone, Copy, Default)] +pub struct Overrides { + pub baud: Option, + pub data_bits: Option, + pub stop_bits: Option, + pub parity: Option, + pub flow_control: Option, +} + +pub fn init(dev: &Device, overrides: &Overrides) -> Result<()> { + #[cfg(feature = "host")] + { + let _ = (dev, overrides); + return Ok(()); + } + #[cfg(not(feature = "host"))] + { + let cfg = cfg_from_entry(dev.0, overrides); + let rc = unsafe { bindings::uart_init(&cfg as *const _) }; + if rc < 0 { + return Err(from_c_rc(rc)); + } + Ok(()) + } +} + +pub fn deinit(dev: &Device) -> Result<()> { + #[cfg(feature = "host")] + { + let _ = dev; + return Ok(()); + } + #[cfg(not(feature = "host"))] + { + let rc = unsafe { bindings::uart_deinit(dev.0.instance) }; + if rc < 0 { + return Err(from_c_rc(rc)); + } + Ok(()) + } +} + +pub fn transmit_blocking(dev: &Device, buf: &[u8], timeout_ms: u32) -> Result<()> { + #[cfg(feature = "host")] + { + let _ = (dev, buf, timeout_ms); + return Ok(()); + } + #[cfg(not(feature = "host"))] + { + if buf.is_empty() { + return Ok(()); + } + let rc = unsafe { + bindings::uart_transmit_blocking( + dev.0.instance, + buf.as_ptr(), + buf.len() as i32, + timeout_ms, + ) + }; + if rc < 0 { + return Err(from_c_rc(rc)); + } + Ok(()) + } +} + +pub fn transmit_nb(dev: &Device, buf: &[u8]) -> Result { + #[cfg(feature = "host")] + { + let _ = (dev, buf); + return Ok(0); + } + #[cfg(not(feature = "host"))] + { + if buf.is_empty() { + return Ok(0); + } + let rc = unsafe { + bindings::uart_transmit_nb(dev.0.instance, buf.as_ptr(), buf.len() as i32) + }; + if rc < 0 { + return Err(from_c_rc(rc)); + } + Ok(rc as usize) + } +} + +pub fn receive_nb(dev: &Device, buf: &mut [u8]) -> Result { + #[cfg(feature = "host")] + { + let _ = (dev, buf); + return Ok(0); + } + #[cfg(not(feature = "host"))] + { + if buf.is_empty() { + return Ok(0); + } + let rc = unsafe { + bindings::uart_receive_nb(dev.0.instance, buf.as_mut_ptr(), buf.len() as i32) + }; + if rc < 0 { + return Err(from_c_rc(rc)); + } + Ok(rc as usize) + } +} + +pub fn register_irq_handler( + dev: &Device, + handler: Option, + ctx: *mut (), +) -> Result<()> { + #[cfg(feature = "host")] + { + let _ = (dev, handler, ctx); + return Ok(()); + } + #[cfg(not(feature = "host"))] + { + // SAFETY: `IrqHandler = extern "C" fn(Irq, *mut ())` is ABI-compatible + // with `uart_irq_handler_fn` because `Irq` is `#[repr(u32)]` and `ctx` + // is `*mut () ↔ void *`. + let raw: bindings::uart_irq_handler_fn = unsafe { core::mem::transmute(handler) }; + let rc = unsafe { + bindings::uart_set_irq_handler(dev.0.instance, raw, ctx as *mut core::ffi::c_void) + }; + if rc < 0 { + return Err(from_c_rc(rc)); + } + Ok(()) + } +} + diff --git a/machine/cortex-m/src/stub.rs b/machine/cortex-m/src/stub.rs index 5a700a7..da56821 100644 --- a/machine/cortex-m/src/stub.rs +++ b/machine/cortex-m/src/stub.rs @@ -6,6 +6,7 @@ pub mod device_tree; pub mod i2c; pub mod sched; pub mod spi; +pub mod uart; pub type Machine = StubMachine; pub type Stack = sched::StubStack; diff --git a/machine/cortex-m/src/stub/uart.rs b/machine/cortex-m/src/stub/uart.rs new file mode 100644 index 0000000..ed68179 --- /dev/null +++ b/machine/cortex-m/src/stub/uart.rs @@ -0,0 +1,93 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + InvalidArgument, + NoSuchDevice, + NotInitialized, + OutOfMemory, + Busy, + WouldBlock, + TimedOut, + Io, +} + +pub type Result = core::result::Result; + +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Irq { + Rx = 0, + TxDone = 1, +} + +pub type IrqHandler = extern "C" fn(Irq, *mut ()); + +#[derive(Clone, Copy, Default)] +pub struct Overrides { + pub baud: Option, + pub data_bits: Option, + pub stop_bits: Option, + pub parity: Option, + pub flow_control: Option, +} + +#[derive(Clone, Copy)] +pub struct Device; + +impl Device { + pub fn index(&self) -> u8 { + 0 + } + pub fn instance(&self) -> usize { + 0 + } +} + +pub fn get_by_index(_idx: u8) -> Result { + Err(Error::NoSuchDevice) +} + +pub fn get(_compatible: &str, _ordinal: usize) -> Result { + Err(Error::NoSuchDevice) +} + +pub fn init(_dev: &Device, _overrides: &Overrides) -> Result<()> { + Err(Error::NotInitialized) +} + +pub fn deinit(_dev: &Device) -> Result<()> { + Err(Error::NotInitialized) +} + +pub fn transmit_blocking(_dev: &Device, _buf: &[u8], _timeout_ms: u32) -> Result<()> { + Err(Error::NotInitialized) +} + +pub fn transmit_nb(_dev: &Device, _buf: &[u8]) -> Result { + Err(Error::NotInitialized) +} + +pub fn receive_nb(_dev: &Device, _buf: &mut [u8]) -> Result { + Err(Error::NotInitialized) +} + +pub fn register_irq_handler( + _dev: &Device, + _handler: Option, + _ctx: *mut (), +) -> Result<()> { + Err(Error::NotInitialized) +} + +/// Mirror of the `hal_arm::uart::console_entry` API; testing has no +/// device-tree-driven console, so always returns `None`. +pub fn console_entry() -> Option<&'static ()> { + None +} + +pub fn init_console_from_dt() -> Result<()> { + Ok(()) +} + +pub fn console_write(_buf: &[u8]) -> Result<()> { + Ok(()) +} diff --git a/machine/cortex-m/st/stm32l4/interface/export.h b/machine/cortex-m/st/stm32l4/interface/export.h index 7f71638..3f90feb 100644 --- a/machine/cortex-m/st/stm32l4/interface/export.h +++ b/machine/cortex-m/st/stm32l4/interface/export.h @@ -7,8 +7,7 @@ unsigned long long systick_freq(void); void init_hal(void); // uart.c -int init_debug_uart(void); -int write_debug_uart(const char *buf, int len); +#include "uart.h" // spi.c typedef struct diff --git a/machine/cortex-m/st/stm32l4/interface/uart.c b/machine/cortex-m/st/stm32l4/interface/uart.c index 90d0edd..84af7be 100644 --- a/machine/cortex-m/st/stm32l4/interface/uart.c +++ b/machine/cortex-m/st/stm32l4/interface/uart.c @@ -1,95 +1,502 @@ +#include "uart.h" #include "lib.h" #include "gpio.h" #include "stm32l4xx.h" +#include "stm32l4xx_hal.h" +#include "stm32l4xx_hal_gpio.h" +#include "stm32l4xx_hal_pwr_ex.h" #include "stm32l4xx_hal_rcc.h" #include "stm32l4xx_hal_rcc_ex.h" +#include "stm32l4xx_hal_uart.h" -#include +#define UART_SLOT_COUNT 6 -static UART_HandleTypeDef HDBG_UART; +#define UART_ERR_OK 0 +#define UART_ERR_INVAL (-1) +#define UART_ERR_NOMEM (-2) +#define UART_ERR_BUSY (-3) +#define UART_ERR_AGAIN (-4) +#define UART_ERR_IO (-5) -#ifndef OSIRIS_DEBUG_UART - #error "OSIRIS_DEBUG_UART not defined." +typedef struct +{ + uint8_t in_use; + uint8_t console_owned; + uart_bus_cfg_t bus_cfg; + UART_HandleTypeDef huart; + + /* 1-byte landing pad re-armed in RxCpltCallback. */ + volatile uint8_t rx_byte; + uint8_t rx_ring[UART_RX_RING_SZ]; + volatile uint16_t rx_head; + volatile uint16_t rx_tail; + + uint8_t tx_ring[UART_TX_RING_SZ]; + volatile uint16_t tx_head; + volatile uint16_t tx_tail; + volatile uint16_t tx_in_flight; + volatile uint8_t tx_busy; + + uart_irq_handler_fn cb; + void *cb_ctx; +} uart_slot_t; + +static uart_slot_t uart_slots[UART_SLOT_COUNT]; + +static uart_slot_t *uart_find_slot(uintptr_t instance) +{ + for (int i = 0; i < UART_SLOT_COUNT; ++i) + { + if (uart_slots[i].in_use && uart_slots[i].bus_cfg.instance == instance) + { + return &uart_slots[i]; + } + } + return NULL; +} + +static int uart_slot_index(const uart_slot_t *s) +{ + return (int)(s - uart_slots); +} + +static uart_slot_t *uart_alloc_slot(void) +{ + for (int i = 0; i < UART_SLOT_COUNT; ++i) + { + if (!uart_slots[i].in_use) + { + return &uart_slots[i]; + } + } + return NULL; +} + +static int uart_periph_clock_enable(USART_TypeDef *u) +{ + RCC_PeriphCLKInitTypeDef init = {0}; +#if defined(USART1) + if (u == USART1) + { + init.PeriphClockSelection = RCC_PERIPHCLK_USART1; + init.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; + if (HAL_RCCEx_PeriphCLKConfig(&init) != HAL_OK) + return UART_ERR_IO; + __HAL_RCC_USART1_CLK_ENABLE(); + return UART_ERR_OK; + } +#endif +#if defined(USART2) + if (u == USART2) + { + init.PeriphClockSelection = RCC_PERIPHCLK_USART2; + init.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; + if (HAL_RCCEx_PeriphCLKConfig(&init) != HAL_OK) + return UART_ERR_IO; + __HAL_RCC_USART2_CLK_ENABLE(); + return UART_ERR_OK; + } #endif +#if defined(USART3) + if (u == USART3) + { + init.PeriphClockSelection = RCC_PERIPHCLK_USART3; + init.Usart3ClockSelection = RCC_USART3CLKSOURCE_PCLK1; + if (HAL_RCCEx_PeriphCLKConfig(&init) != HAL_OK) + return UART_ERR_IO; + __HAL_RCC_USART3_CLK_ENABLE(); + return UART_ERR_OK; + } +#endif +#if defined(UART4) + if (u == UART4) + { + init.PeriphClockSelection = RCC_PERIPHCLK_UART4; + init.Uart4ClockSelection = RCC_UART4CLKSOURCE_PCLK1; + if (HAL_RCCEx_PeriphCLKConfig(&init) != HAL_OK) + return UART_ERR_IO; + __HAL_RCC_UART4_CLK_ENABLE(); + return UART_ERR_OK; + } +#endif +#if defined(UART5) + if (u == UART5) + { + init.PeriphClockSelection = RCC_PERIPHCLK_UART5; + init.Uart5ClockSelection = RCC_UART5CLKSOURCE_PCLK1; + if (HAL_RCCEx_PeriphCLKConfig(&init) != HAL_OK) + return UART_ERR_IO; + __HAL_RCC_UART5_CLK_ENABLE(); + return UART_ERR_OK; + } +#endif +#if defined(LPUART1) + if (u == LPUART1) + { + init.PeriphClockSelection = RCC_PERIPHCLK_LPUART1; + init.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_PCLK1; + if (HAL_RCCEx_PeriphCLKConfig(&init) != HAL_OK) + return UART_ERR_IO; + __HAL_RCC_LPUART1_CLK_ENABLE(); + /* L4Rxxx LPUART1 pins (PG7/PG8) are on VddIO2; without this they + * float and TX never leaves the pad. */ + HAL_PWREx_EnableVddIO2(); + return UART_ERR_OK; + } +#endif + return UART_ERR_INVAL; +} + +static uint32_t uart_word_length(uint8_t data_bits, uint8_t parity) +{ + /* STM32 word length includes the parity bit. */ + uint8_t total = data_bits + (parity == 0 ? 0 : 1); + switch (total) + { + case 7: + return UART_WORDLENGTH_7B; + case 8: + return UART_WORDLENGTH_8B; + case 9: + return UART_WORDLENGTH_9B; + default: + return UART_WORDLENGTH_8B; + } +} + +static uint32_t uart_stop_bits(uint8_t bits) +{ + return bits == 2 ? UART_STOPBITS_2 : UART_STOPBITS_1; +} + +static uint32_t uart_parity_mode(uint8_t p) +{ + switch (p) + { + case 1: + return UART_PARITY_ODD; + case 2: + return UART_PARITY_EVEN; + default: + return UART_PARITY_NONE; + } +} + +static uint32_t uart_hw_flow(uint8_t f) +{ + return f == 1 ? UART_HWCONTROL_RTS_CTS : UART_HWCONTROL_NONE; +} + +static int uart_apply_pin(const uart_pin_cfg_t *pin) +{ + if (pin->port == 0) + return UART_ERR_OK; + GPIO_TypeDef *port = (GPIO_TypeDef *)pin->port; + gpio_enable_clock(port); + gpio_init_af(port, (uint16_t)(1u << pin->pin), pin->af); + return UART_ERR_OK; +} + +static int uart_hw_init(uart_slot_t *slot) +{ + USART_TypeDef *u = (USART_TypeDef *)slot->bus_cfg.instance; + + if (uart_periph_clock_enable(u) != UART_ERR_OK) + return UART_ERR_INVAL; + + uart_apply_pin(&slot->bus_cfg.tx); + uart_apply_pin(&slot->bus_cfg.rx); + if (slot->bus_cfg.flow_control == 1) + { + uart_apply_pin(&slot->bus_cfg.rts); + uart_apply_pin(&slot->bus_cfg.cts); + } + + slot->huart.Instance = u; + slot->huart.Init.BaudRate = slot->bus_cfg.baud ? slot->bus_cfg.baud : 115200u; + slot->huart.Init.WordLength = uart_word_length( + slot->bus_cfg.data_bits ? slot->bus_cfg.data_bits : 8, + slot->bus_cfg.parity); + slot->huart.Init.StopBits = uart_stop_bits(slot->bus_cfg.stop_bits); + slot->huart.Init.Parity = uart_parity_mode(slot->bus_cfg.parity); + slot->huart.Init.Mode = UART_MODE_TX_RX; + slot->huart.Init.HwFlowCtl = uart_hw_flow(slot->bus_cfg.flow_control); + slot->huart.Init.OverSampling = UART_OVERSAMPLING_16; + slot->huart.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; + slot->huart.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; + + if (HAL_UART_Init(&slot->huart) != HAL_OK) + return UART_ERR_IO; + + return UART_ERR_OK; +} + +/* Override the HAL's __weak MspInit: pins/clock are configured in + * uart_hw_init before HAL_UART_Init, so MspInit has nothing to do. */ +void HAL_UART_MspInit(UART_HandleTypeDef *huart) +{ + (void)huart; +} + +int uart_slot_of(uintptr_t instance) +{ + uart_slot_t *s = uart_find_slot(instance); + return s ? uart_slot_index(s) : -1; +} + +static int uart_init_common(const uart_bus_cfg_t *cfg, uint8_t console_owned) +{ + if (cfg == NULL || cfg->instance == 0) + return UART_ERR_INVAL; + + uart_slot_t *existing = uart_find_slot(cfg->instance); + if (existing != NULL) + { + /* Idempotent re-open, but the console slot can't be promoted + * to IT mode (and vice versa) — they have different invariants. */ + if (existing->console_owned != console_owned) + return UART_ERR_BUSY; + return uart_slot_index(existing); + } + + uart_slot_t *slot = uart_alloc_slot(); + if (slot == NULL) + return UART_ERR_NOMEM; + + *slot = (uart_slot_t){0}; + slot->bus_cfg = *cfg; + slot->console_owned = console_owned; + + if (uart_hw_init(slot) != UART_ERR_OK) + return UART_ERR_IO; + + slot->in_use = 1; + + if (!console_owned) + { + IRQn_Type irqn = (IRQn_Type)cfg->irqn; + HAL_NVIC_SetPriority(irqn, cfg->priority, 0); + HAL_NVIC_EnableIRQ(irqn); + if (HAL_UART_Receive_IT(&slot->huart, (uint8_t *)&slot->rx_byte, 1) != HAL_OK) + return UART_ERR_IO; + } -int init_debug_uart(void) { - HDBG_UART.Instance = OSIRIS_DEBUG_UART; - HDBG_UART.Init.BaudRate = 115200; - HDBG_UART.Init.Mode = UART_MODE_TX_RX; - - if (HAL_UART_Init(&HDBG_UART) != HAL_OK) { - return -1; - } - - return 0; -} - -int write_debug_uart(const char *buf, int len) { - if (HAL_UART_Transmit(&HDBG_UART, (uint8_t *)buf, len, 100) != HAL_OK) { - return -1; - } - return len; // Return number of bytes written -} - -void HAL_UART_MspInit(UART_HandleTypeDef *huart) { - RCC_PeriphCLKInitTypeDef PeriphClkInit = {0}; - - if (huart->Instance == USART1) { - // TX: PA9 (AF7), RX: PA10 (AF7) - PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1; - PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; - if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) return; - __HAL_RCC_USART1_CLK_ENABLE(); - gpio_enable_clock(GPIOA); - gpio_init_af(GPIOA, GPIO_PIN_9 | GPIO_PIN_10, GPIO_AF7_USART1); - - } else if (huart->Instance == USART2) { - // TX: PA2 (AF7), RX: PA3 (AF7) - PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART2; - PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; - if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) return; - __HAL_RCC_USART2_CLK_ENABLE(); - gpio_enable_clock(GPIOA); - gpio_init_af(GPIOA, GPIO_PIN_2 | GPIO_PIN_3, GPIO_AF7_USART2); - - } else if (huart->Instance == USART3) { - // TX: PC10 (AF7), RX: PC11 (AF7) - PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART3; - PeriphClkInit.Usart3ClockSelection = RCC_USART3CLKSOURCE_PCLK1; - if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) return; - __HAL_RCC_USART3_CLK_ENABLE(); - gpio_enable_clock(GPIOC); - gpio_init_af(GPIOC, GPIO_PIN_10 | GPIO_PIN_11, GPIO_AF7_USART3); - - } else if (huart->Instance == UART4) { - // TX: PA0 (AF8), RX: PA1 (AF8) - PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_UART4; - PeriphClkInit.Uart4ClockSelection = RCC_UART4CLKSOURCE_PCLK1; - if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) return; - __HAL_RCC_UART4_CLK_ENABLE(); - gpio_enable_clock(GPIOA); - gpio_init_af(GPIOA, GPIO_PIN_0 | GPIO_PIN_1, GPIO_AF8_UART4); - - } else if (huart->Instance == UART5) { - // TX: PC12 (AF8), RX: PD2 (AF8) - PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_UART5; - PeriphClkInit.Uart5ClockSelection = RCC_UART5CLKSOURCE_PCLK1; - if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) return; - __HAL_RCC_UART5_CLK_ENABLE(); - gpio_enable_clock(GPIOC); - gpio_enable_clock(GPIOD); - gpio_init_af(GPIOC, GPIO_PIN_12, GPIO_AF8_UART5); - gpio_init_af(GPIOD, GPIO_PIN_2, GPIO_AF8_UART5); - - } else if (huart->Instance == LPUART1) { - // TX: PG7 (AF8), RX: PG8 (AF8) - PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_LPUART1; - PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_PCLK1; - if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) return; - __HAL_RCC_LPUART1_CLK_ENABLE(); - HAL_PWREx_EnableVddIO2(); - gpio_enable_clock(GPIOG); - gpio_init_af(GPIOG, GPIO_PIN_7 | GPIO_PIN_8, GPIO_AF8_LPUART1); - } + return uart_slot_index(slot); } + +int uart_init(const uart_bus_cfg_t *cfg) +{ + return uart_init_common(cfg, 0); +} + +int uart_init_console(const uart_bus_cfg_t *cfg) +{ + return uart_init_common(cfg, 1); +} + +int uart_deinit(uintptr_t instance) +{ + uart_slot_t *slot = uart_find_slot(instance); + if (slot == NULL) + return UART_ERR_OK; + + if (!slot->console_owned) + HAL_NVIC_DisableIRQ((IRQn_Type)slot->bus_cfg.irqn); + HAL_UART_DeInit(&slot->huart); + slot->in_use = 0; + slot->console_owned = 0; + return UART_ERR_OK; +} + +int uart_transmit_blocking(uintptr_t instance, + const uint8_t *buf, + int len, + uint32_t timeout_ms) +{ + if (buf == NULL || len < 0) + return UART_ERR_INVAL; + uart_slot_t *slot = uart_find_slot(instance); + if (slot == NULL) + return UART_ERR_INVAL; + if (len == 0) + return 0; + + HAL_StatusTypeDef rc = HAL_UART_Transmit(&slot->huart, (uint8_t *)buf, (uint16_t)len, timeout_ms); + if (rc != HAL_OK) + return UART_ERR_IO; + return len; +} + +/* head == tail is empty; (head + 1) % cap == tail is full. One slot + * reserved as the empty/full sentinel. */ +static uint16_t ring_occ(uint16_t head, uint16_t tail, uint16_t cap) +{ + return (uint16_t)((head + cap - tail) % cap); +} + +static uint16_t ring_free(uint16_t head, uint16_t tail, uint16_t cap) +{ + return (uint16_t)(cap - 1u - ring_occ(head, tail, cap)); +} + +static void uart_arm_tx(uart_slot_t *slot); + +int uart_transmit_nb(uintptr_t instance, const uint8_t *buf, int len) +{ + if (buf == NULL || len < 0) + return UART_ERR_INVAL; + uart_slot_t *slot = uart_find_slot(instance); + if (slot == NULL || slot->console_owned) + return UART_ERR_INVAL; + if (len == 0) + return 0; + + /* IRQ-disable: head/tail are shared with TxCpltCallback. */ + uint32_t pri = __get_PRIMASK(); + __disable_irq(); + + uint16_t free = ring_free(slot->tx_head, slot->tx_tail, UART_TX_RING_SZ); + if (free == 0) + { + if (!pri) + __enable_irq(); + return UART_ERR_AGAIN; + } + + int n = (int)free < len ? (int)free : len; + for (int i = 0; i < n; ++i) + { + slot->tx_ring[slot->tx_head] = buf[i]; + slot->tx_head = (uint16_t)((slot->tx_head + 1) % UART_TX_RING_SZ); + } + + if (!slot->tx_busy) + uart_arm_tx(slot); + + if (!pri) + __enable_irq(); + return n; +} + +int uart_receive_nb(uintptr_t instance, uint8_t *buf, int len) +{ + if (buf == NULL || len < 0) + return UART_ERR_INVAL; + uart_slot_t *slot = uart_find_slot(instance); + if (slot == NULL || slot->console_owned) + return UART_ERR_INVAL; + if (len == 0) + return 0; + + uint32_t pri = __get_PRIMASK(); + __disable_irq(); + + int n = 0; + while (n < len && slot->rx_tail != slot->rx_head) + { + buf[n++] = slot->rx_ring[slot->rx_tail]; + slot->rx_tail = (uint16_t)((slot->rx_tail + 1) % UART_RX_RING_SZ); + } + + if (!pri) + __enable_irq(); + return n; +} + +int uart_set_irq_handler(uintptr_t instance, uart_irq_handler_fn fn, void *ctx) +{ + uart_slot_t *slot = uart_find_slot(instance); + if (slot == NULL) + return UART_ERR_INVAL; + if (slot->console_owned) + return UART_ERR_BUSY; + uint32_t pri = __get_PRIMASK(); + __disable_irq(); + slot->cb = fn; + slot->cb_ctx = ctx; + if (!pri) + __enable_irq(); + return UART_ERR_OK; +} + +/* Arms the next TX-IT chunk if the ring has data. Caller must hold IRQs off. + * Sends the largest contiguous span up to end-of-ring; TxCpltCallback re-arms + * for any wrap-around. */ +static void uart_arm_tx(uart_slot_t *slot) +{ + if (slot->tx_head == slot->tx_tail) + { + slot->tx_busy = 0; + slot->tx_in_flight = 0; + return; + } + uint16_t span; + if (slot->tx_tail < slot->tx_head) + span = (uint16_t)(slot->tx_head - slot->tx_tail); + else + span = (uint16_t)(UART_TX_RING_SZ - slot->tx_tail); + + slot->tx_busy = 1; + slot->tx_in_flight = span; + if (HAL_UART_Transmit_IT(&slot->huart, &slot->tx_ring[slot->tx_tail], span) != HAL_OK) + { + slot->tx_busy = 0; + slot->tx_in_flight = 0; + } +} + +void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) +{ + for (int i = 0; i < UART_SLOT_COUNT; ++i) + { + uart_slot_t *slot = &uart_slots[i]; + if (!slot->in_use || &slot->huart != huart) + continue; + + uint16_t next = (uint16_t)((slot->rx_head + 1) % UART_RX_RING_SZ); + if (next != slot->rx_tail) + { + slot->rx_ring[slot->rx_head] = slot->rx_byte; + slot->rx_head = next; + } + // ring full ⇒ drop. Consumer recovers from the next read. + + if (slot->cb) + slot->cb(UART_IRQ_RX, slot->cb_ctx); + + HAL_UART_Receive_IT(&slot->huart, (uint8_t *)&slot->rx_byte, 1); + return; + } +} + +void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) +{ + for (int i = 0; i < UART_SLOT_COUNT; ++i) + { + uart_slot_t *slot = &uart_slots[i]; + if (!slot->in_use || &slot->huart != huart) + continue; + + slot->tx_tail = (uint16_t)((slot->tx_tail + slot->tx_in_flight) % UART_TX_RING_SZ); + slot->tx_in_flight = 0; + + uart_arm_tx(slot); + + if (!slot->tx_busy && slot->cb) + slot->cb(UART_IRQ_TX_DONE, slot->cb_ctx); + return; + } +} + +void uart_dispatch_by_slot(uint8_t slot_index) +{ + if (slot_index >= UART_SLOT_COUNT) + return; + uart_slot_t *slot = &uart_slots[slot_index]; + if (!slot->in_use) + return; + HAL_UART_IRQHandler(&slot->huart); +} + +#include "uart_trampolines.h" diff --git a/machine/cortex-m/st/stm32l4/interface/uart.h b/machine/cortex-m/st/stm32l4/interface/uart.h new file mode 100644 index 0000000..44e919a --- /dev/null +++ b/machine/cortex-m/st/stm32l4/interface/uart.h @@ -0,0 +1,84 @@ +#pragma once + +#include + +#define UART_RX_RING_SZ 128 +#define UART_TX_RING_SZ 256 + +typedef struct +{ + uintptr_t port; + uint8_t pin; + uint8_t af; + uint16_t reserved; +} uart_pin_cfg_t; + +typedef struct +{ + uintptr_t instance; + /* tx and rx are required. rts/cts are optional; .port == 0 means unused. */ + uart_pin_cfg_t tx; + uart_pin_cfg_t rx; + uart_pin_cfg_t rts; + uart_pin_cfg_t cts; + uint32_t baud; + uint8_t data_bits; /* 7, 8, 9 */ + uint8_t stop_bits; /* 1, 2 */ + uint8_t parity; /* 0=none, 1=odd, 2=even */ + uint8_t flow_control; /* 0=none, 1=rts/cts */ + uint8_t irqn; /* NVIC line; sourced from DT `interrupts` */ + uint8_t priority; /* NVIC priority; sourced from DT `interrupts` */ +} uart_bus_cfg_t; + +typedef enum +{ + UART_IRQ_RX = 0, + UART_IRQ_TX_DONE = 1, +} uart_irq_kind; + +typedef void (*uart_irq_handler_fn)(uart_irq_kind kind, void *ctx); + +/* Open a UART for IT-driven RX + IT-driven TX with software rings. Returns + * 0..UART_SLOT_COUNT-1 on success or negative errno on failure (-EBUSY if + * the instance is already initialised, -ENOMEM if no slot is free, -EINVAL + * for malformed cfg). Idempotent: calling twice on the same instance with + * an already-open slot returns the existing slot index. */ +int uart_init(const uart_bus_cfg_t *cfg); + +/* Open a UART in console (blocking-only, no IT, no rings) mode. The console + * slot is reserved for `uprintln!` and panic output and refuses to be + * promoted to IT mode by a later uart_init() on the same instance. */ +int uart_init_console(const uart_bus_cfg_t *cfg); + +/* Tear down a previously initialised slot. */ +int uart_deinit(uintptr_t instance); + +/* Blocking transmit. Bypasses the TX ring; calls HAL_UART_Transmit. Safe + * for the console slot. timeout_ms == 0xFFFFFFFF means HAL_MAX_DELAY. */ +int uart_transmit_blocking(uintptr_t instance, + const uint8_t *buf, + int len, + uint32_t timeout_ms); + +/* Non-blocking transmit. Enqueues into the TX ring and arms HAL_UART_Transmit_IT. + * Returns bytes enqueued (0..len) or -EAGAIN if the ring is full. Not valid + * on a console-owned slot. */ +int uart_transmit_nb(uintptr_t instance, const uint8_t *buf, int len); + +/* Non-blocking receive. Drains up to len bytes from the RX ring. Returns + * bytes drained (0..len). Not valid on a console-owned slot. */ +int uart_receive_nb(uintptr_t instance, uint8_t *buf, int len); + +/* Install an ISR-context callback. Called from HAL_UART_RxCpltCallback after + * each byte is pushed to the RX ring (kind=RX) and from HAL_UART_TxCpltCallback + * when the TX ring drains (kind=TX_DONE). Pass NULL fn to clear. */ +int uart_set_irq_handler(uintptr_t instance, uart_irq_handler_fn fn, void *ctx); + +/* Returns the slot index for `instance` or -1 if not open. */ +int uart_slot_of(uintptr_t instance); + +/* Drives `HAL_UART_IRQHandler` for the slot at `slot_index`. Called from + * the DT-generated `__irq__handler` trampolines in `uart_trampolines.h`. + * Bounds-checks the slot index and ignores idle slots, so the trampolines + * are safe to leave in place even when the slot was never opened. */ +void uart_dispatch_by_slot(uint8_t slot_index); diff --git a/presets/stm32l4r5zi_def.toml b/presets/stm32l4r5zi_def.toml index ee3bf89..5b83ef5 100644 --- a/presets/stm32l4r5zi_def.toml +++ b/presets/stm32l4r5zi_def.toml @@ -6,7 +6,6 @@ DTS_PATH = { value = "../boards/nucleo_l4r5zi.dts", relative = true } OSIRIS_MACHINE = "cortex-m" # Debugging configuration -OSIRIS_DEBUG_UART = "LPUART1" OSIRIS_DEBUG_RUNTIMESYMBOLS = "false" # Tuning parameters diff --git a/src/drivers.rs b/src/drivers.rs index 86f7bc4..2497495 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -1,5 +1,6 @@ pub mod i2c; pub mod spi; +pub mod uart; pub fn init() { i2c::init(); diff --git a/src/drivers/uart/mod.rs b/src/drivers/uart/mod.rs new file mode 100644 index 0000000..4bf1934 --- /dev/null +++ b/src/drivers/uart/mod.rs @@ -0,0 +1,284 @@ +pub mod wait; + +pub use crate::hal::uart::{Error, Irq, IrqHandler, Overrides}; + +use core::time::Duration; + +use crate::sched; +use crate::time; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum DataBits { + Seven, + #[default] + Eight, + Nine, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum StopBits { + #[default] + One, + Two, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Parity { + #[default] + None, + Odd, + Even, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum FlowControl { + #[default] + None, + RtsCts, +} + +#[derive(Clone, Copy, Default)] +pub struct Config { + pub baud: Option, + pub data_bits: Option, + pub stop_bits: Option, + pub parity: Option, + pub flow_control: Option, + /// Default timeout for [`Device::read_blocking`]. `None` blocks + /// forever; per-call timeouts override. + pub read_timeout: Option, + /// Default timeout for [`Device::write_blocking`]. `None` blocks + /// forever; per-call timeouts override. + pub write_timeout: Option, +} + +fn data_bits_to_u8(d: DataBits) -> u8 { + match d { + DataBits::Seven => 7, + DataBits::Eight => 8, + DataBits::Nine => 9, + } +} + +fn stop_bits_to_u8(s: StopBits) -> u8 { + match s { + StopBits::One => 1, + StopBits::Two => 2, + } +} + +fn parity_to_u8(p: Parity) -> u8 { + match p { + Parity::None => 0, + Parity::Odd => 1, + Parity::Even => 2, + } +} + +fn flow_to_u8(f: FlowControl) -> u8 { + match f { + FlowControl::None => 0, + FlowControl::RtsCts => 1, + } +} + +fn duration_to_ticks(d: Duration) -> u64 { + use crate::hal::Machinelike; + let freq = crate::hal::Machine::systick_freq(); + let secs = d.as_secs().saturating_mul(freq); + let sub = (d.subsec_micros() as u64).saturating_mul(freq) / 1_000_000; + secs.saturating_add(sub) +} + +#[derive(Clone, Copy)] +pub struct Device { + desc: crate::hal::uart::Device, + read_timeout: Option, + write_timeout: Option, +} + +impl Device { + pub fn open(compatible: &str, ordinal: usize, cfg: Config) -> Result { + let desc = crate::hal::uart::get(compatible, ordinal)?; + let overrides = Overrides { + baud: cfg.baud, + data_bits: cfg.data_bits.map(data_bits_to_u8), + stop_bits: cfg.stop_bits.map(stop_bits_to_u8), + parity: cfg.parity.map(parity_to_u8), + flow_control: cfg.flow_control.map(flow_to_u8), + }; + crate::hal::uart::init(&desc, &overrides)?; + Ok(Self { + desc, + read_timeout: cfg.read_timeout, + write_timeout: cfg.write_timeout, + }) + } + + pub fn close(self) -> Result<(), Error> { + crate::hal::uart::deinit(&self.desc) + } + + pub fn slot(&self) -> u8 { + self.desc.index() + } + + pub fn write_nb(&self, buf: &[u8]) -> Result { + match crate::hal::uart::transmit_nb(&self.desc, buf) { + Ok(n) => Ok(n), + Err(Error::WouldBlock) => Ok(0), + Err(e) => Err(e), + } + } + + pub fn read_nb(&self, buf: &mut [u8]) -> Result { + match crate::hal::uart::receive_nb(&self.desc, buf) { + Ok(n) => Ok(n), + Err(Error::WouldBlock) => Ok(0), + Err(e) => Err(e), + } + } + + /// Block until at least one byte is available or the device's + /// configured `read_timeout` expires. + pub fn read_blocking(&self, buf: &mut [u8]) -> Result { + self.read_with_timeout(buf, self.read_timeout) + } + + /// `None` blocks forever; `Some(d)` returns `Err(TimedOut)` if no + /// byte arrives within `d`. + pub fn read_with_timeout( + &self, + buf: &mut [u8], + timeout: Option, + ) -> Result { + if buf.is_empty() { + return Ok(0); + } + let uid = match sched::with(|s| s.current_uid()) { + Some(u) => u as u32, + None => return Err(Error::Io), + }; + let deadline = timeout.map(|d| time::tick().saturating_add(duration_to_ticks(d))); + let waiter = wait::Waiter::new(uid); + wait::register_rx_waiter(self.slot(), &waiter); + let result = loop { + let mut got: Result = Ok(0); + let exit = sched::with(|s| { + got = self.read_nb(buf); + match got { + Ok(0) => { + let now = time::tick(); + match deadline { + Some(d) if now >= d => { + got = Err(Error::TimedOut); + true + } + Some(d) => { + let _ = s.sleep_until(d, now); + false + } + None => { + let _ = s.sleep_until(u64::MAX, now); + false + } + } + } + _ => true, + } + }); + if exit { + break got; + } + }; + wait::unregister_rx_waiter(self.slot(), &waiter); + result + } + + /// Block until the entire buffer is enqueued or the device's + /// configured `write_timeout` expires. + pub fn write_blocking(&self, buf: &[u8]) -> Result<(), Error> { + self.write_with_timeout(buf, self.write_timeout) + } + + /// `None` blocks forever; `Some(d)` returns `Err(TimedOut)` if the + /// buffer can't be fully enqueued within `d`. Partial progress is + /// not surfaced; use [`Self::write_nb`] if you need that. + pub fn write_with_timeout(&self, buf: &[u8], timeout: Option) -> Result<(), Error> { + if buf.is_empty() { + return Ok(()); + } + let uid = match sched::with(|s| s.current_uid()) { + Some(u) => u as u32, + None => return Err(Error::Io), + }; + let deadline = timeout.map(|d| time::tick().saturating_add(duration_to_ticks(d))); + let waiter = wait::Waiter::new(uid); + wait::register_tx_waiter(self.slot(), &waiter); + let mut sent = 0usize; + let result = loop { + let mut step: Result = Ok(0); + let exit = sched::with(|s| { + step = self.write_nb(&buf[sent..]); + match step { + Ok(0) => { + let now = time::tick(); + match deadline { + Some(d) if now >= d => { + step = Err(Error::TimedOut); + true + } + Some(d) => { + let _ = s.sleep_until(d, now); + false + } + None => { + let _ = s.sleep_until(u64::MAX, now); + false + } + } + } + Ok(_) => true, + Err(_) => true, + } + }); + if exit { + match step { + Ok(n) => { + sent += n; + if sent >= buf.len() { + break Ok(()); + } + } + Err(e) => break Err(e), + } + } + }; + wait::unregister_tx_waiter(self.slot(), &waiter); + result + } + + pub fn register_rx_waiter(&self, w: &wait::Waiter) { + wait::register_rx_waiter(self.slot(), w); + } + pub fn unregister_rx_waiter(&self, w: &wait::Waiter) { + wait::unregister_rx_waiter(self.slot(), w); + } + pub fn register_tx_waiter(&self, w: &wait::Waiter) { + wait::register_tx_waiter(self.slot(), w); + } + pub fn unregister_tx_waiter(&self, w: &wait::Waiter) { + wait::unregister_tx_waiter(self.slot(), w); + } + + pub fn set_rx_callback(&self, cb: Option) { + wait::set_rx_callback(self.slot(), cb); + } + pub fn set_tx_callback(&self, cb: Option) { + wait::set_tx_callback(self.slot(), cb); + } + + pub fn set_irq_handler(&self, h: Option, ctx: *mut ()) -> Result<(), Error> { + crate::hal::uart::register_irq_handler(&self.desc, h, ctx) + } +} diff --git a/src/drivers/uart/wait.rs b/src/drivers/uart/wait.rs new file mode 100644 index 0000000..d80dd9f --- /dev/null +++ b/src/drivers/uart/wait.rs @@ -0,0 +1,193 @@ +//! Per-UART-slot wait lists and ISR-context callbacks. Mirrors +//! `drivers/can/wait.rs` with separate RX and TX channels per slot. + +use core::cell::Cell; +use core::sync::atomic::{AtomicPtr, AtomicUsize, Ordering}; + +pub const UART_SLOT_COUNT: usize = 6; + +pub struct UartWaitDevice { + rx_waiters: AtomicPtr, + tx_waiters: AtomicPtr, + rx_callback: AtomicUsize, + tx_callback: AtomicUsize, +} + +impl UartWaitDevice { + pub const fn new() -> Self { + Self { + rx_waiters: AtomicPtr::new(core::ptr::null_mut()), + tx_waiters: AtomicPtr::new(core::ptr::null_mut()), + rx_callback: AtomicUsize::new(0), + tx_callback: AtomicUsize::new(0), + } + } +} + +static UART_DEVICES: [UartWaitDevice; UART_SLOT_COUNT] = [ + UartWaitDevice::new(), + UartWaitDevice::new(), + UartWaitDevice::new(), + UartWaitDevice::new(), + UartWaitDevice::new(), + UartWaitDevice::new(), +]; + +/// Caller-owned wait-queue node. Must stay live and at a fixed address +/// from `register_*_waiter` until `unregister_*_waiter` returns; the +/// ISR dereferences it. Killing the parked thread mid-wait leaves a +/// dangling pointer in the list — caller must unregister before exit. +#[repr(C)] +pub struct Waiter { + uid: u32, + next: Cell<*mut Waiter>, +} + +impl Waiter { + pub const fn new(uid: u32) -> Self { + Self { + uid, + next: Cell::new(core::ptr::null_mut()), + } + } + + pub fn uid(&self) -> u32 { + self.uid + } +} + +// SAFETY: `Waiter` is mutated only with IRQs disabled (register/unregister) +// and read in IRQ context. The IRQ-disable discipline substitutes for +// the locking the type system would otherwise enforce on `Cell`. +unsafe impl Sync for Waiter {} + +#[derive(Clone, Copy)] +pub enum Channel { + Rx, + Tx, +} + +fn waiter_head(dev: &UartWaitDevice, ch: Channel) -> &AtomicPtr { + match ch { + Channel::Rx => &dev.rx_waiters, + Channel::Tx => &dev.tx_waiters, + } +} + +fn callback_slot(dev: &UartWaitDevice, ch: Channel) -> &AtomicUsize { + match ch { + Channel::Rx => &dev.rx_callback, + Channel::Tx => &dev.tx_callback, + } +} + +pub fn register_waiter(slot: u8, ch: Channel, w: &Waiter) { + let dev = &UART_DEVICES[slot as usize]; + let head = waiter_head(dev, ch); + let state = crate::hal::asm::disable_irq_save(); + let cur = head.load(Ordering::Relaxed); + w.next.set(cur); + head.store(w as *const _ as *mut _, Ordering::Relaxed); + crate::hal::asm::enable_irq_restr(state); +} + +pub fn unregister_waiter(slot: u8, ch: Channel, w: &Waiter) { + let dev = &UART_DEVICES[slot as usize]; + let head = waiter_head(dev, ch); + let state = crate::hal::asm::disable_irq_save(); + let target = w as *const _ as *mut Waiter; + let mut prev_next: *const Cell<*mut Waiter> = core::ptr::null(); + let mut cur = head.load(Ordering::Relaxed); + while !cur.is_null() { + if cur == target { + let next = unsafe { (*cur).next.get() }; + if prev_next.is_null() { + head.store(next, Ordering::Relaxed); + } else { + unsafe { (*prev_next).set(next) }; + } + w.next.set(core::ptr::null_mut()); + break; + } + prev_next = unsafe { &(*cur).next }; + cur = unsafe { (*cur).next.get() }; + } + crate::hal::asm::enable_irq_restr(state); +} + +pub fn register_rx_waiter(slot: u8, w: &Waiter) { + register_waiter(slot, Channel::Rx, w); +} +pub fn unregister_rx_waiter(slot: u8, w: &Waiter) { + unregister_waiter(slot, Channel::Rx, w); +} +pub fn register_tx_waiter(slot: u8, w: &Waiter) { + register_waiter(slot, Channel::Tx, w); +} +pub fn unregister_tx_waiter(slot: u8, w: &Waiter) { + unregister_waiter(slot, Channel::Tx, w); +} + +pub type ChannelCallback = extern "C" fn(slot: u8); + +pub fn set_rx_callback(slot: u8, cb: Option) { + let raw = cb.map(|f| f as usize).unwrap_or(0); + UART_DEVICES[slot as usize] + .rx_callback + .store(raw, Ordering::Release); +} + +pub fn set_tx_callback(slot: u8, cb: Option) { + let raw = cb.map(|f| f as usize).unwrap_or(0); + UART_DEVICES[slot as usize] + .tx_callback + .store(raw, Ordering::Release); +} + +extern "C" fn kernel_dispatch(kind: crate::hal::uart::Irq, ctx: *mut ()) { + let dev = unsafe { &*(ctx as *const UartWaitDevice) }; + + let ch = match kind { + crate::hal::uart::Irq::Rx => Channel::Rx, + crate::hal::uart::Irq::TxDone => Channel::Tx, + }; + + let head = waiter_head(dev, ch); + let mut cur = head.load(Ordering::Relaxed); + while !cur.is_null() { + let uid = unsafe { (*cur).uid }; + crate::sched::with(|s| { + let _ = s.kick_by_uid(uid as usize); + }); + cur = unsafe { (*cur).next.get() }; + } + + let raw = callback_slot(dev, ch).load(Ordering::Acquire); + if raw != 0 { + // SAFETY: `raw` came from `set_*_callback` as `f as usize` and the + // `raw != 0` guard excludes the unset sentinel. + let cb: ChannelCallback = unsafe { core::mem::transmute(raw) }; + let base = &UART_DEVICES[0] as *const _ as usize; + let slot = (ctx as usize - base) / core::mem::size_of::(); + cb(slot as u8); + } + + crate::sched::reschedule(); +} + +/// Register the kernel dispatcher with the HAL for every populated slot. +/// Call once during osiris boot, after the console has been initialised. +pub fn init() { + for slot in 0..UART_SLOT_COUNT as u8 { + let Ok(dev) = crate::hal::uart::get_by_index(slot) else { + continue; + }; + let ctx = &UART_DEVICES[slot as usize] as *const _ as *mut (); + match crate::hal::uart::register_irq_handler(&dev, Some(kernel_dispatch), ctx) { + Ok(()) => {} + // Console-owned slot — IT mode disabled there by design. + Err(crate::hal::uart::Error::Busy) => {} + Err(e) => panic!("UART wait dispatcher: HAL rejected slot {} ({:?})", slot, e), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index b643953..8fd97ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,6 +53,8 @@ pub unsafe extern "C" fn kernel_init() -> ! { sched::init(kaddr_space); kprintln!("Scheduler initialized."); + drivers::uart::wait::init(); + idle::init(); kprintln!("Idle thread initialized."); diff --git a/src/sched.rs b/src/sched.rs index 30dd534..7011197 100644 --- a/src/sched.rs +++ b/src/sched.rs @@ -291,6 +291,19 @@ impl Scheduler { }) } + /// Wake a thread by raw `UId::as_usize()`. Returns the same errors as + /// [`kick`](Self::kick) — notably "not in wakeup tree" if the target + /// is currently runnable. The lookup uses only the `uid` field, so + /// the synthetic `tid` is a placeholder. + pub fn kick_by_uid(&mut self, uid: usize) -> Result<()> { + let lookup_uid = thread::UId::new(uid, thread::Id::new(0, crate::sched::task::UId::new(0))); + self.kick(lookup_uid) + } + + pub fn current_uid(&self) -> Option { + self.current.map(|uid| uid.as_usize()) + } + /// This will just remove the thread from the scheduler, but it will not trigger a reschedule, even if the thread is currently running. /// /// Returns an error if the thread does not exist, or if the thread is not currently enqueued in any scheduler. @@ -443,6 +456,23 @@ pub fn reschedule() { hal::Machine::trigger_reschedule(); } +/// Wake a thread by raw `uid` from ISR context. `sched::with` disables +/// IRQs internally, and the trailing `reschedule()` arms PendSV so the +/// woken thread is picked on IRQ-exit. Errors are swallowed — a +/// not-yet-sleeping target is the common case and the consumer's bounded +/// sleep loop covers it. +/// +/// Exported as a C-FFI symbol so consumers that run in interrupt context +/// can avoid the SVC path, which would HardFault from handler mode on +/// Cortex-M. +#[unsafe(no_mangle)] +pub extern "C" fn kick_thread(uid: u32) { + with(|sched| { + let _ = sched.kick_by_uid(uid as usize); + }); + reschedule(); +} + /// This will be called by the architecture-specific code to enter the scheduler. It will land the current thread, pick the next thread to run, and return its context and task. #[unsafe(no_mangle)] pub extern "C" fn sched_enter(mut ctx: *mut c_void) -> *mut c_void { diff --git a/src/uapi.rs b/src/uapi.rs index 5b4c3df..e91dd4c 100644 --- a/src/uapi.rs +++ b/src/uapi.rs @@ -3,3 +3,4 @@ pub mod print; pub mod sched; pub mod spi; pub mod time; +pub mod uart; diff --git a/src/uapi/uart.rs b/src/uapi/uart.rs new file mode 100644 index 0000000..b853a40 --- /dev/null +++ b/src/uapi/uart.rs @@ -0,0 +1,5 @@ +pub use crate::drivers::uart::*; + +pub fn open(compatible: &str, ordinal: usize, cfg: Config) -> Result { + Device::open(compatible, ordinal, cfg) +} diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index ab65bb0..853b7da 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -1,7 +1,11 @@ -use crate::ir::{DeviceTree, PropValue}; +use crate::ir::{DeviceTree, Node, PropValue}; use proc_macro2::TokenStream; use quote::quote; +pub fn generate_uart_trampolines_h(dt: &DeviceTree) -> String { + uart::generate_trampolines_h(dt) +} + pub fn generate_rust(dt: &DeviceTree) -> String { let segments: &[TokenStream] = &[ emit_prop_value_type(), @@ -14,6 +18,8 @@ pub fn generate_rust(dt: &DeviceTree) -> String { i2c::emit_query_api(), spi::emit_registry(dt), spi::emit_query_api(), + uart::emit_registry(dt), + uart::emit_query_api(), emit_aliases_module(dt), emit_memory_module(dt), emit_chosen_module(dt), @@ -415,6 +421,15 @@ macro_rules! match_compatible { }}; } +/// Decode the STM32_PINMUX macro encoding. Shared by every peripheral +/// codegen module that talks to `st,stm32-pinctrl` (SPI, UART, ...). +fn decode_stm32_pinmux(pinmux: u32) -> (usize, u8, u8) { + let port_idx = ((pinmux >> 9) & 0x1f) as usize; + let line = ((pinmux >> 5) & 0x0f) as u8; + let mode = (pinmux & 0x1f) as u8; + (port_idx, line, mode) +} + /// Returns true if the "status" prop is absent or set to okay. fn is_enabled(node: &crate::ir::Node) -> bool { match node.extra.get("status") { @@ -895,14 +910,6 @@ mod spi { compatible: String, } - /// Decodes the STM32_PINMUX macro encoding. - fn decode_stm32_pinmux(pinmux: u32) -> (usize, u8, u8) { - let port_idx = ((pinmux >> 9) & 0x1f) as usize; - let line = ((pinmux >> 5) & 0x0f) as u8; - let mode = (pinmux & 0x1f) as u8; - (port_idx, line, mode) - } - /// Decodes pinctrl phandles to extract port, line, and alternate function for SPI pins. fn decode_pinctrl<'a>(dt: &'a DeviceTree, pinctrl: &[u32]) -> Vec<(&'a str, BusPin)> { /// Parses a node name like "spi1_sck_pa5" to extract the SPI signal role (sck, miso, mosi). @@ -1374,6 +1381,377 @@ mod spi { } } +// ------------------------------------------------------------------------------------------------ +// UART registry +// ------------------------------------------------------------------------------------------------ + +mod uart { + use super::*; + + #[derive(Clone, Copy)] + struct Pin { + port: usize, + line: u8, + af: u8, + } + + #[derive(Clone)] + struct Bus { + node: usize, + instance: usize, + baud: u32, + data_bits: u8, + stop_bits: u8, + parity: u8, + flow_control: u8, + irqn: u8, + priority: u8, + tx: Pin, + rx: Pin, + rts: Option, + cts: Option, + compatible: String, + } + + /// Parse a pinctrl node name like `usart1_tx_pa9` or `lpuart1_rts_pg6` + /// into the signal role. Returns `None` for unrecognised roles + /// (don't panic — keeps DT extension cheap and avoids the SPI NSS trap). + fn parse_uart_role(name: &str) -> Option<&'static str> { + let mut parts = name.split('_'); + let periph = parts.next()?; + let signal = parts.next()?; + let is_uart = periph.starts_with("usart") + || periph.starts_with("uart") + || periph.starts_with("lpuart"); + if !is_uart { + return None; + } + match signal { + "tx" => Some("tx"), + "rx" => Some("rx"), + "rts" => Some("rts"), + "cts" => Some("cts"), + _ => None, + } + } + + fn decode_pinctrl<'a>(dt: &'a DeviceTree, pinctrl: &[u32]) -> Vec<(&'static str, Pin)> { + let mut pins = Vec::new(); + for ph in pinctrl { + let Some(idx) = dt.resolve_phandle_idx(*ph) else { + continue; + }; + let pin = &dt.nodes[idx]; + let Some(role) = parse_uart_role(&pin.name) else { + continue; + }; + let Some(pin_ctrl_idx) = pin.parent else { + continue; + }; + let pin_ctrl = &dt.nodes[pin_ctrl_idx]; + + let decoded = match_compatible!(&pin_ctrl.compatible, { + "st,stm32-pinctrl" => { + let pinmux = match pin.extra.get("pinmux") { + Some(PropValue::U32Array(v)) if !v.is_empty() => v[0], + Some(PropValue::U32(v)) => *v, + _ => continue, + }; + let (port_idx, line, mode) = decode_stm32_pinmux(pinmux); + let base = pin_ctrl + .reg + .and_then(|(b, _)| usize::try_from(b).ok()) + .unwrap_or(0); + Pin { port: base + (port_idx * 0x400), line, af: mode } + } + }); + let Some(pin) = decoded else { continue }; + pins.push((role, pin)); + } + pins + } + + fn is_uart_node(node: &Node) -> bool { + node.compatible + .iter() + .any(|c| c.contains("usart") || c.contains("uart") || c.contains("lpuart")) + } + + fn collect(dt: &DeviceTree) -> Vec { + let mut out: Vec = Vec::new(); + for (idx, node) in dt.nodes.iter().enumerate() { + if !is_enabled(node) { + continue; + } + if !is_uart_node(node) { + continue; + } + let Some((base, _)) = node.reg else { + continue; + }; + let Ok(instance) = usize::try_from(base) else { + continue; + }; + + let baud = match node.extra.get("current-speed") { + Some(PropValue::U32(v)) => *v, + _ => 115200, + }; + let data_bits = match node.extra.get("data-bits") { + Some(PropValue::U32(v)) => *v as u8, + _ => 8, + }; + let stop_bits = match node.extra.get("stop-bits") { + Some(PropValue::U32(v)) => *v as u8, + _ => 1, + }; + let parity = match node.extra.get("parity") { + Some(PropValue::Str(s)) => match s.as_str() { + "odd" => 1, + "even" => 2, + _ => 0, + }, + Some(PropValue::U32(v)) => *v as u8, + _ => 0, + }; + let flow_control = match node.extra.get("hw-flow-control") { + Some(PropValue::Empty) => 1, + Some(PropValue::U32(v)) if *v != 0 => 1, + _ => 0, + }; + + // STM32 UART nodes carry a 2-cell `interrupts = `. + // Skip nodes that don't (we can't wire IT mode without an IRQ). + let (irqn, priority) = match node.interrupts.as_slice() { + [irqn, priority, ..] => (*irqn as u8, *priority as u8), + _ => { + eprintln!( + "cargo::warning=dtgen: skipping UART node `{}` — no `interrupts` property", + node.name + ); + continue; + } + }; + + let (mut tx, mut rx, mut rts, mut cts) = (None, None, None, None); + for (key, value) in &node.extra { + if !key.starts_with("pinctrl-") { + continue; + } + let PropValue::U32Array(pinctrl) = value else { + continue; + }; + for (role, pin) in decode_pinctrl(dt, pinctrl) { + let dst = match role { + "tx" => &mut tx, + "rx" => &mut rx, + "rts" => &mut rts, + "cts" => &mut cts, + _ => continue, + }; + *dst = Some(pin); + } + } + + let Some(tx) = tx else { + eprintln!( + "cargo::warning=dtgen: skipping UART node `{}` — no `tx` pin in pinctrl-0", + node.name + ); + continue; + }; + let Some(rx) = rx else { + eprintln!( + "cargo::warning=dtgen: skipping UART node `{}` — no `rx` pin in pinctrl-0", + node.name + ); + continue; + }; + if flow_control == 1 && (rts.is_none() || cts.is_none()) { + eprintln!( + "cargo::warning=dtgen: skipping UART node `{}` — `hw-flow-control` set but `rts`/`cts` pin missing", + node.name + ); + continue; + } + + let compatible = node + .compatible + .first() + .cloned() + .unwrap_or_else(|| "st,stm32-uart".to_string()); + + out.push(Bus { + node: idx, + instance, + baud, + data_bits, + stop_bits, + parity, + flow_control, + irqn, + priority, + tx, + rx, + rts, + cts, + compatible, + }); + } + out + } + + /// Resolve the `chosen.osiris,console` property to an index into + /// `UART_REGISTRY`. The DTC turns `osiris,console = &foo;` into a + /// path string `/soc/foo@..`, which we walk back to a node index + /// and then match against `buses`. Returns `None` if the property + /// is missing or doesn't point at a UART node we collected. + fn resolve_console(dt: &DeviceTree, buses: &[Bus]) -> Option { + let chosen_idx = dt.by_name.get("chosen").and_then(|v| v.first()).copied()?; + let chosen = &dt.nodes[chosen_idx]; + let path = match chosen.extra.get("osiris,console")? { + PropValue::Str(s) => s.as_str(), + _ => return None, + }; + let node_idx = resolve_path(dt, path)?; + buses.iter().position(|b| b.node == node_idx) + } + + pub fn emit_registry(dt: &DeviceTree) -> TokenStream { + let buses = collect(dt); + let console_const = match resolve_console(dt, &buses) { + Some(i) => quote! { Some(#i) }, + None => quote! { None }, + }; + + let pin_tokens = |p: Pin| { + let port = p.port; + let line = p.line; + let af = p.af; + quote! { UartPin { port: #port, line: #line, af: #af } } + }; + + let opt_pin = |p: Option| match p { + Some(p) => { + let pin = pin_tokens(p); + quote! { Some(#pin) } + } + None => quote! { None }, + }; + + let entries = buses.iter().enumerate().map(|(i, b)| { + let index = i as u8; + let node = b.node; + let instance = b.instance; + let baud = b.baud; + let data_bits = b.data_bits; + let stop_bits = b.stop_bits; + let parity = b.parity; + let flow_control = b.flow_control; + let irqn = b.irqn; + let priority = b.priority; + let tx = pin_tokens(b.tx); + let rx = pin_tokens(b.rx); + let rts = opt_pin(b.rts); + let cts = opt_pin(b.cts); + let compatible = b.compatible.as_str(); + quote! { + UartRegistryEntry { + index: #index, + node: #node, + instance: #instance, + compatible: #compatible, + tx: #tx, + rx: #rx, + rts: #rts, + cts: #cts, + baud: #baud, + data_bits: #data_bits, + stop_bits: #stop_bits, + parity: #parity, + flow_control: #flow_control, + irqn: #irqn, + priority: #priority, + }, + } + }); + + quote! { + #[derive(Debug, Clone, Copy)] + #[repr(C)] + pub struct UartPin { + pub port: usize, + pub line: u8, + pub af: u8, + } + + #[derive(Debug, Clone, Copy)] + #[repr(C)] + pub struct UartRegistryEntry { + pub index: u8, + pub node: usize, + pub instance: usize, + pub compatible: &'static str, + pub tx: UartPin, + pub rx: UartPin, + pub rts: Option, + pub cts: Option, + pub baud: u32, + pub data_bits: u8, + pub stop_bits: u8, + pub parity: u8, + pub flow_control: u8, + pub irqn: u8, + pub priority: u8, + } + + pub const UART_REGISTRY: &[UartRegistryEntry] = &[ + #(#entries)* + ]; + + #[doc = "index into UART_REGISTRY of the chosen.osiris,console node, resolved at codegen time"] + pub const CONSOLE_UART: Option = #console_const; + } + } + + pub fn emit_query_api() -> TokenStream { + quote! { + pub fn uart_by_index(idx: u8) -> Option<&'static UartRegistryEntry> { + UART_REGISTRY.iter().find(|e| e.index == idx) + } + + pub fn uart_by_compatible(compatible: &str, ord: usize) -> Option<&'static UartRegistryEntry> { + let mut matches = 0usize; + for e in UART_REGISTRY { + if e.compatible == compatible { + if matches == ord { + return Some(e); + } + matches += 1; + } + } + None + } + } + } + + /// Emit a small C header that overrides the weak `__irq__handler` + /// symbols for every DT-declared UART, dispatching into the slot + /// position that the same registry assigns. Mirrors CAN's + /// `can_trampolines.h`. + pub fn generate_trampolines_h(dt: &DeviceTree) -> String { + let buses = collect(dt); + let mut out = String::from("#pragma once\n\n/* Generated by dtgen. */\n"); + for (i, b) in buses.iter().enumerate() { + out.push_str(&format!( + "void __irq_{}_handler(void) {{ uart_dispatch_by_slot({}); }}\n", + b.irqn, i + )); + } + out + } +} + fn resolve_path(dt: &DeviceTree, path: &str) -> Option { if path == "/" { return Some(dt.root); diff --git a/xtasks/crates/dtgen/src/lib.rs b/xtasks/crates/dtgen/src/lib.rs index 026b763..6f03797 100644 --- a/xtasks/crates/dtgen/src/lib.rs +++ b/xtasks/crates/dtgen/src/lib.rs @@ -22,3 +22,9 @@ pub fn generate_rust(dt: &DeviceTree) -> String { pub fn generate_ld(dt: &DeviceTree) -> Result { ldgen::generate_ld(dt) } + +/// Codegen a C header overriding `__irq__handler` for each DT-declared +/// UART node. Consumed by `interface/uart.c` via `#include`. +pub fn generate_uart_trampolines_h(dt: &DeviceTree) -> String { + codegen::generate_uart_trampolines_h(dt) +} From 5c95e694088a565952a8122688cc8dc06d9b272e Mon Sep 17 00:00:00 2001 From: xarantolus Date: Sun, 10 May 2026 20:57:27 +0000 Subject: [PATCH 2/3] UART: route IRQs through kernel IRQ registry --- Cargo.lock | 1 - machine/cortex-m/Cargo.toml | 1 - machine/cortex-m/build.rs | 7 - machine/cortex-m/src/native/uart.rs | 168 ++++++------------- machine/cortex-m/src/stub/uart.rs | 5 + machine/cortex-m/st/stm32l4/interface/uart.c | 2 - src/drivers/uart/wait.rs | 21 ++- xtasks/crates/dtgen/src/codegen.rs | 20 --- xtasks/crates/dtgen/src/lib.rs | 6 - 9 files changed, 78 insertions(+), 153 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da6b8e0..cc23789 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -778,7 +778,6 @@ dependencies = [ "cbindgen", "cmake", "critical-section", - "dtgen", "hal-api", "hal-builder", "quote", diff --git a/machine/cortex-m/Cargo.toml b/machine/cortex-m/Cargo.toml index ada797a..5871c24 100644 --- a/machine/cortex-m/Cargo.toml +++ b/machine/cortex-m/Cargo.toml @@ -14,7 +14,6 @@ critical-section = { version = "1.2", features = ["restore-state-usize"] } [build-dependencies] hal-builder = { path = "../builder" } -dtgen = { workspace = true } cbindgen = "0.28.0" bindgen = "0.72.0" cmake = "0.1.54" diff --git a/machine/cortex-m/build.rs b/machine/cortex-m/build.rs index 95890c0..58ad60f 100644 --- a/machine/cortex-m/build.rs +++ b/machine/cortex-m/build.rs @@ -330,13 +330,6 @@ fn main() { panic!("Failed to generate device tree scripts: {e}"); } - if let Err(e) = fs::write( - out.join("uart_trampolines.h"), - dtgen::generate_uart_trampolines_h(&dt), - ) { - panic!("Failed to write uart_trampolines.h: {e}"); - } - for (vendor, name) in hal_builder::dt::soc(&dt) { let hal = Path::new(vendor).join(name); diff --git a/machine/cortex-m/src/native/uart.rs b/machine/cortex-m/src/native/uart.rs index 4c357dc..dd166cc 100644 --- a/machine/cortex-m/src/native/uart.rs +++ b/machine/cortex-m/src/native/uart.rs @@ -1,12 +1,10 @@ -use super::device_tree; -#[cfg(not(feature = "host"))] use super::bindings; +use super::device_tree; pub(crate) fn console_entry() -> Option<&'static device_tree::UartRegistryEntry> { device_tree::CONSOLE_UART.map(|i| &device_tree::UART_REGISTRY[i]) } -#[cfg(not(feature = "host"))] pub(crate) fn init_console_from_dt() -> Result<()> { let Some(entry) = console_entry() else { return Ok(()); @@ -19,12 +17,6 @@ pub(crate) fn init_console_from_dt() -> Result<()> { Ok(()) } -#[cfg(feature = "host")] -pub(crate) fn init_console_from_dt() -> Result<()> { - Ok(()) -} - -#[cfg(not(feature = "host"))] pub(crate) fn console_write(buf: &[u8]) -> Result<()> { let Some(entry) = console_entry() else { return Ok(()); @@ -34,12 +26,7 @@ pub(crate) fn console_write(buf: &[u8]) -> Result<()> { } // HAL_MAX_DELAY: console writes never give up. let rc = unsafe { - bindings::uart_transmit_blocking( - entry.instance, - buf.as_ptr(), - buf.len() as i32, - u32::MAX, - ) + bindings::uart_transmit_blocking(entry.instance, buf.as_ptr(), buf.len() as i32, u32::MAX) }; if rc < 0 { return Err(from_c_rc(rc)); @@ -47,11 +34,6 @@ pub(crate) fn console_write(buf: &[u8]) -> Result<()> { Ok(()) } -#[cfg(feature = "host")] -pub(crate) fn console_write(_buf: &[u8]) -> Result<()> { - Ok(()) -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Error { InvalidArgument, @@ -66,7 +48,6 @@ pub enum Error { pub type Result = core::result::Result; -#[cfg(not(feature = "host"))] fn from_c_rc(rc: i32) -> Error { match rc { -1 => Error::InvalidArgument, @@ -93,6 +74,10 @@ impl Device { pub fn instance(&self) -> usize { self.0.instance } + + pub fn irqn(&self) -> u8 { + self.0.irqn + } } #[repr(u32)] @@ -116,7 +101,6 @@ pub fn get(compatible: &str, ordinal: usize) -> Result { .ok_or(Error::NoSuchDevice) } -#[cfg(not(feature = "host"))] fn cfg_from_entry( entry: &device_tree::UartRegistryEntry, overrides: &Overrides, @@ -168,104 +152,62 @@ pub struct Overrides { } pub fn init(dev: &Device, overrides: &Overrides) -> Result<()> { - #[cfg(feature = "host")] - { - let _ = (dev, overrides); - return Ok(()); - } - #[cfg(not(feature = "host"))] - { - let cfg = cfg_from_entry(dev.0, overrides); - let rc = unsafe { bindings::uart_init(&cfg as *const _) }; - if rc < 0 { - return Err(from_c_rc(rc)); - } - Ok(()) + let cfg = cfg_from_entry(dev.0, overrides); + let rc = unsafe { bindings::uart_init(&cfg as *const _) }; + if rc < 0 { + return Err(from_c_rc(rc)); } + Ok(()) } pub fn deinit(dev: &Device) -> Result<()> { - #[cfg(feature = "host")] - { - let _ = dev; - return Ok(()); - } - #[cfg(not(feature = "host"))] - { - let rc = unsafe { bindings::uart_deinit(dev.0.instance) }; - if rc < 0 { - return Err(from_c_rc(rc)); - } - Ok(()) + let rc = unsafe { bindings::uart_deinit(dev.0.instance) }; + if rc < 0 { + return Err(from_c_rc(rc)); } + Ok(()) } pub fn transmit_blocking(dev: &Device, buf: &[u8], timeout_ms: u32) -> Result<()> { - #[cfg(feature = "host")] - { - let _ = (dev, buf, timeout_ms); + if buf.is_empty() { return Ok(()); } - #[cfg(not(feature = "host"))] - { - if buf.is_empty() { - return Ok(()); - } - let rc = unsafe { - bindings::uart_transmit_blocking( - dev.0.instance, - buf.as_ptr(), - buf.len() as i32, - timeout_ms, - ) - }; - if rc < 0 { - return Err(from_c_rc(rc)); - } - Ok(()) + let rc = unsafe { + bindings::uart_transmit_blocking( + dev.0.instance, + buf.as_ptr(), + buf.len() as i32, + timeout_ms, + ) + }; + if rc < 0 { + return Err(from_c_rc(rc)); } + Ok(()) } pub fn transmit_nb(dev: &Device, buf: &[u8]) -> Result { - #[cfg(feature = "host")] - { - let _ = (dev, buf); + if buf.is_empty() { return Ok(0); } - #[cfg(not(feature = "host"))] - { - if buf.is_empty() { - return Ok(0); - } - let rc = unsafe { - bindings::uart_transmit_nb(dev.0.instance, buf.as_ptr(), buf.len() as i32) - }; - if rc < 0 { - return Err(from_c_rc(rc)); - } - Ok(rc as usize) + let rc = + unsafe { bindings::uart_transmit_nb(dev.0.instance, buf.as_ptr(), buf.len() as i32) }; + if rc < 0 { + return Err(from_c_rc(rc)); } + Ok(rc as usize) } pub fn receive_nb(dev: &Device, buf: &mut [u8]) -> Result { - #[cfg(feature = "host")] - { - let _ = (dev, buf); + if buf.is_empty() { return Ok(0); } - #[cfg(not(feature = "host"))] - { - if buf.is_empty() { - return Ok(0); - } - let rc = unsafe { - bindings::uart_receive_nb(dev.0.instance, buf.as_mut_ptr(), buf.len() as i32) - }; - if rc < 0 { - return Err(from_c_rc(rc)); - } - Ok(rc as usize) + let rc = + unsafe { bindings::uart_receive_nb(dev.0.instance, buf.as_mut_ptr(), buf.len() as i32) }; + if rc < 0 { + return Err(from_c_rc(rc)); } + Ok(rc as usize) } pub fn register_irq_handler( @@ -273,24 +215,20 @@ pub fn register_irq_handler( handler: Option, ctx: *mut (), ) -> Result<()> { - #[cfg(feature = "host")] - { - let _ = (dev, handler, ctx); - return Ok(()); - } - #[cfg(not(feature = "host"))] - { - // SAFETY: `IrqHandler = extern "C" fn(Irq, *mut ())` is ABI-compatible - // with `uart_irq_handler_fn` because `Irq` is `#[repr(u32)]` and `ctx` - // is `*mut () ↔ void *`. - let raw: bindings::uart_irq_handler_fn = unsafe { core::mem::transmute(handler) }; - let rc = unsafe { - bindings::uart_set_irq_handler(dev.0.instance, raw, ctx as *mut core::ffi::c_void) - }; - if rc < 0 { - return Err(from_c_rc(rc)); - } - Ok(()) + // SAFETY: `IrqHandler = extern "C" fn(Irq, *mut ())` is ABI-compatible + // with `uart_irq_handler_fn` because `Irq` is `#[repr(u32)]` and `ctx` + // is `*mut () ↔ void *`. + let raw: bindings::uart_irq_handler_fn = unsafe { core::mem::transmute(handler) }; + let rc = + unsafe { bindings::uart_set_irq_handler(dev.0.instance, raw, ctx as *mut core::ffi::c_void) }; + if rc < 0 { + return Err(from_c_rc(rc)); } + Ok(()) } +pub fn dispatch_by_slot(slot: u8) { + unsafe { + bindings::uart_dispatch_by_slot(slot); + } +} diff --git a/machine/cortex-m/src/stub/uart.rs b/machine/cortex-m/src/stub/uart.rs index ed68179..f380096 100644 --- a/machine/cortex-m/src/stub/uart.rs +++ b/machine/cortex-m/src/stub/uart.rs @@ -40,6 +40,9 @@ impl Device { pub fn instance(&self) -> usize { 0 } + pub fn irqn(&self) -> u8 { + 0 + } } pub fn get_by_index(_idx: u8) -> Result { @@ -78,6 +81,8 @@ pub fn register_irq_handler( Err(Error::NotInitialized) } +pub fn dispatch_by_slot(_slot: u8) {} + /// Mirror of the `hal_arm::uart::console_entry` API; testing has no /// device-tree-driven console, so always returns `None`. pub fn console_entry() -> Option<&'static ()> { diff --git a/machine/cortex-m/st/stm32l4/interface/uart.c b/machine/cortex-m/st/stm32l4/interface/uart.c index 84af7be..c4a7461 100644 --- a/machine/cortex-m/st/stm32l4/interface/uart.c +++ b/machine/cortex-m/st/stm32l4/interface/uart.c @@ -498,5 +498,3 @@ void uart_dispatch_by_slot(uint8_t slot_index) return; HAL_UART_IRQHandler(&slot->huart); } - -#include "uart_trampolines.h" diff --git a/src/drivers/uart/wait.rs b/src/drivers/uart/wait.rs index d80dd9f..1e7ee94 100644 --- a/src/drivers/uart/wait.rs +++ b/src/drivers/uart/wait.rs @@ -175,13 +175,32 @@ extern "C" fn kernel_dispatch(kind: crate::hal::uart::Irq, ctx: *mut ()) { crate::sched::reschedule(); } -/// Register the kernel dispatcher with the HAL for every populated slot. +fn vector_dispatch(_ctx: *mut u8, _vector: usize, userdata: Option) { + let Some(slot) = userdata else { + return; + }; + crate::hal::uart::dispatch_by_slot(slot as u8); +} + /// Call once during osiris boot, after the console has been initialised. pub fn init() { for slot in 0..UART_SLOT_COUNT as u8 { let Ok(dev) = crate::hal::uart::get_by_index(slot) else { continue; }; + + // IPSR = NVIC line + 16 on Cortex-M. + let vector = dev.irqn() as usize + 16; + unsafe { + if let Err(e) = crate::irq::register_irq(vector, vector_dispatch, Some(slot as usize)) + { + panic!( + "UART wait dispatcher: failed to register IRQ vector {} for slot {} ({:?})", + vector, slot, e + ); + } + } + let ctx = &UART_DEVICES[slot as usize] as *const _ as *mut (); match crate::hal::uart::register_irq_handler(&dev, Some(kernel_dispatch), ctx) { Ok(()) => {} diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index 853b7da..173bd95 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -2,10 +2,6 @@ use crate::ir::{DeviceTree, Node, PropValue}; use proc_macro2::TokenStream; use quote::quote; -pub fn generate_uart_trampolines_h(dt: &DeviceTree) -> String { - uart::generate_trampolines_h(dt) -} - pub fn generate_rust(dt: &DeviceTree) -> String { let segments: &[TokenStream] = &[ emit_prop_value_type(), @@ -1734,22 +1730,6 @@ mod uart { } } } - - /// Emit a small C header that overrides the weak `__irq__handler` - /// symbols for every DT-declared UART, dispatching into the slot - /// position that the same registry assigns. Mirrors CAN's - /// `can_trampolines.h`. - pub fn generate_trampolines_h(dt: &DeviceTree) -> String { - let buses = collect(dt); - let mut out = String::from("#pragma once\n\n/* Generated by dtgen. */\n"); - for (i, b) in buses.iter().enumerate() { - out.push_str(&format!( - "void __irq_{}_handler(void) {{ uart_dispatch_by_slot({}); }}\n", - b.irqn, i - )); - } - out - } } fn resolve_path(dt: &DeviceTree, path: &str) -> Option { diff --git a/xtasks/crates/dtgen/src/lib.rs b/xtasks/crates/dtgen/src/lib.rs index 6f03797..026b763 100644 --- a/xtasks/crates/dtgen/src/lib.rs +++ b/xtasks/crates/dtgen/src/lib.rs @@ -22,9 +22,3 @@ pub fn generate_rust(dt: &DeviceTree) -> String { pub fn generate_ld(dt: &DeviceTree) -> Result { ldgen::generate_ld(dt) } - -/// Codegen a C header overriding `__irq__handler` for each DT-declared -/// UART node. Consumed by `interface/uart.c` via `#include`. -pub fn generate_uart_trampolines_h(dt: &DeviceTree) -> String { - codegen::generate_uart_trampolines_h(dt) -} From 9ea59102745e994d53b66cddc2c99a4f95e82e0b Mon Sep 17 00:00:00 2001 From: xarantolus Date: Sun, 10 May 2026 20:57:30 +0000 Subject: [PATCH 3/3] UART: open-time IRQ wiring, Drop, drop set_irq_handler --- src/drivers/uart/mod.rs | 17 +++++++------- src/drivers/uart/wait.rs | 51 +++++++++++++++++----------------------- src/lib.rs | 2 -- 3 files changed, 31 insertions(+), 39 deletions(-) diff --git a/src/drivers/uart/mod.rs b/src/drivers/uart/mod.rs index 4bf1934..46b1602 100644 --- a/src/drivers/uart/mod.rs +++ b/src/drivers/uart/mod.rs @@ -1,6 +1,6 @@ pub mod wait; -pub use crate::hal::uart::{Error, Irq, IrqHandler, Overrides}; +pub use crate::hal::uart::{Error, Overrides}; use core::time::Duration; @@ -90,7 +90,6 @@ fn duration_to_ticks(d: Duration) -> u64 { secs.saturating_add(sub) } -#[derive(Clone, Copy)] pub struct Device { desc: crate::hal::uart::Device, read_timeout: Option, @@ -108,6 +107,10 @@ impl Device { flow_control: cfg.flow_control.map(flow_to_u8), }; crate::hal::uart::init(&desc, &overrides)?; + if let Err(e) = wait::ensure_registered(&desc) { + let _ = crate::hal::uart::deinit(&desc); + return Err(e); + } Ok(Self { desc, read_timeout: cfg.read_timeout, @@ -115,10 +118,6 @@ impl Device { }) } - pub fn close(self) -> Result<(), Error> { - crate::hal::uart::deinit(&self.desc) - } - pub fn slot(&self) -> u8 { self.desc.index() } @@ -277,8 +276,10 @@ impl Device { pub fn set_tx_callback(&self, cb: Option) { wait::set_tx_callback(self.slot(), cb); } +} - pub fn set_irq_handler(&self, h: Option, ctx: *mut ()) -> Result<(), Error> { - crate::hal::uart::register_irq_handler(&self.desc, h, ctx) +impl Drop for Device { + fn drop(&mut self) { + let _ = crate::hal::uart::deinit(&self.desc); } } diff --git a/src/drivers/uart/wait.rs b/src/drivers/uart/wait.rs index 1e7ee94..22f7ebc 100644 --- a/src/drivers/uart/wait.rs +++ b/src/drivers/uart/wait.rs @@ -1,11 +1,11 @@ -//! Per-UART-slot wait lists and ISR-context callbacks. Mirrors -//! `drivers/can/wait.rs` with separate RX and TX channels per slot. - use core::cell::Cell; -use core::sync::atomic::{AtomicPtr, AtomicUsize, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering}; pub const UART_SLOT_COUNT: usize = 6; +static REGISTERED: [AtomicBool; UART_SLOT_COUNT] = + [const { AtomicBool::new(false) }; UART_SLOT_COUNT]; + pub struct UartWaitDevice { rx_waiters: AtomicPtr, tx_waiters: AtomicPtr, @@ -182,31 +182,24 @@ fn vector_dispatch(_ctx: *mut u8, _vector: usize, userdata: Option) { crate::hal::uart::dispatch_by_slot(slot as u8); } -/// Call once during osiris boot, after the console has been initialised. -pub fn init() { - for slot in 0..UART_SLOT_COUNT as u8 { - let Ok(dev) = crate::hal::uart::get_by_index(slot) else { - continue; - }; - - // IPSR = NVIC line + 16 on Cortex-M. - let vector = dev.irqn() as usize + 16; - unsafe { - if let Err(e) = crate::irq::register_irq(vector, vector_dispatch, Some(slot as usize)) - { - panic!( - "UART wait dispatcher: failed to register IRQ vector {} for slot {} ({:?})", - vector, slot, e - ); - } - } +/// Must be called *after* `hal::uart::init` — `uart_set_irq_handler` looks up +/// the slot by `in_use`, which only `uart_init` sets. +pub fn ensure_registered(dev: &crate::hal::uart::Device) -> Result<(), crate::hal::uart::Error> { + let slot = dev.index(); + if (slot as usize) >= UART_SLOT_COUNT { + return Err(crate::hal::uart::Error::InvalidArgument); + } + if REGISTERED[slot as usize].swap(true, Ordering::AcqRel) { + return Ok(()); + } - let ctx = &UART_DEVICES[slot as usize] as *const _ as *mut (); - match crate::hal::uart::register_irq_handler(&dev, Some(kernel_dispatch), ctx) { - Ok(()) => {} - // Console-owned slot — IT mode disabled there by design. - Err(crate::hal::uart::Error::Busy) => {} - Err(e) => panic!("UART wait dispatcher: HAL rejected slot {} ({:?})", slot, e), - } + // IPSR = NVIC line + 16 on Cortex-M. + let vector = dev.irqn() as usize + 16; + unsafe { + crate::irq::register_irq(vector, vector_dispatch, Some(slot as usize)) + .map_err(|_| crate::hal::uart::Error::Io)?; } + + let ctx = &UART_DEVICES[slot as usize] as *const _ as *mut (); + crate::hal::uart::register_irq_handler(dev, Some(kernel_dispatch), ctx) } diff --git a/src/lib.rs b/src/lib.rs index 8fd97ab..b643953 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,8 +53,6 @@ pub unsafe extern "C" fn kernel_init() -> ! { sched::init(kaddr_space); kprintln!("Scheduler initialized."); - drivers::uart::wait::init(); - idle::init(); kprintln!("Idle thread initialized.");