diff --git a/machine/cortex-m/build.rs b/machine/cortex-m/build.rs index c33f61c..58ad60f 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"); 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..dd166cc --- /dev/null +++ b/machine/cortex-m/src/native/uart.rs @@ -0,0 +1,234 @@ +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]) +} + +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(()) +} + +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(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + InvalidArgument, + NoSuchDevice, + NotInitialized, + OutOfMemory, + Busy, + WouldBlock, + TimedOut, + Io, +} + +pub type Result = core::result::Result; + +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 + } + + pub fn irqn(&self) -> u8 { + self.0.irqn + } +} + +#[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) +} + +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<()> { + 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<()> { + 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<()> { + 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 { + 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 { + 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<()> { + // 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.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..f380096 --- /dev/null +++ b/machine/cortex-m/src/stub/uart.rs @@ -0,0 +1,98 @@ +#[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 irqn(&self) -> u8 { + 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) +} + +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 ()> { + 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..c4a7461 100644 --- a/machine/cortex-m/st/stm32l4/interface/uart.c +++ b/machine/cortex-m/st/stm32l4/interface/uart.c @@ -1,95 +1,500 @@ +#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; + } + + 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; + } +} -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); - } +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); } 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..46b1602 --- /dev/null +++ b/src/drivers/uart/mod.rs @@ -0,0 +1,285 @@ +pub mod wait; + +pub use crate::hal::uart::{Error, 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) +} + +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)?; + 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, + write_timeout: cfg.write_timeout, + }) + } + + 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); + } +} + +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 new file mode 100644 index 0000000..22f7ebc --- /dev/null +++ b/src/drivers/uart/wait.rs @@ -0,0 +1,205 @@ +use core::cell::Cell; +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, + 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(); +} + +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); +} + +/// 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(()); + } + + // 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/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..173bd95 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -1,4 +1,4 @@ -use crate::ir::{DeviceTree, PropValue}; +use crate::ir::{DeviceTree, Node, PropValue}; use proc_macro2::TokenStream; use quote::quote; @@ -14,6 +14,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 +417,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 +906,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 +1377,361 @@ 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 + } + } + } +} + fn resolve_path(dt: &DeviceTree, path: &str) -> Option { if path == "/" { return Some(dt.root);