diff --git a/machine/cortex-m/build.rs b/machine/cortex-m/build.rs index da1c593..5a161a3 100644 --- a/machine/cortex-m/build.rs +++ b/machine/cortex-m/build.rs @@ -338,6 +338,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 4e51b59..9ce74cc 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; @@ -11,6 +9,7 @@ pub mod i2c; pub mod panic; pub mod sched; pub mod spi; +pub mod uart; pub mod system; mod crit; @@ -46,7 +45,9 @@ 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(); } } @@ -75,13 +76,10 @@ impl hal_api::Machinelike for ArmMachine { fn print(s: &str) -> Result<()> { // Mask PendSV only — a full cpsid_i across a polled-UART line at // 115200 baud (~13 ms) overruns the bxCAN FIFO at 1 Mbit/s. + // (bxCAN fix from main, carried onto the generic-UART console path.) let state = asm::disable_pendsv_save(); - - let ok = - unsafe { bindings::write_debug_uart(s.as_ptr() as *const c_char, s.len() as i32) } != 0; - + let ok = uart::console_write(s.as_bytes()).is_ok(); asm::enable_pendsv_restr(state); - if ok { Ok(()) } else { diff --git a/machine/cortex-m/src/native/uart.rs b/machine/cortex-m/src/native/uart.rs new file mode 100644 index 0000000..ed8e365 --- /dev/null +++ b/machine/cortex-m/src/native/uart.rs @@ -0,0 +1,251 @@ +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(()) +} + +/// Length-proportional console TX timeout: slack over the line-rate +/// transmit time, capped so the PendSV-masked window stays bounded +/// (caller masks PendSV across this). Beyond the cap, lines may truncate. +const CONSOLE_TX_SLACK_MS: u32 = 50; +const CONSOLE_TX_MAX_MS: u32 = 100; + +pub(crate) fn console_write(buf: &[u8]) -> Result<()> { + let Some(entry) = console_entry() else { + return Ok(()); + }; + if buf.is_empty() { + return Ok(()); + } + + // 10 bits/byte (8N1). + let per_byte_us = 10_000_000 / entry.baud.max(1); + let budget_ms = (buf.len() as u32).saturating_mul(per_byte_us) / 1000; + let timeout_ms = CONSOLE_TX_SLACK_MS + .saturating_add(budget_ms) + .min(CONSOLE_TX_MAX_MS); + let rc = unsafe { + bindings::uart_transmit_blocking( + entry.instance, + buf.as_ptr(), + buf.len() as i32, + timeout_ms, + ) + }; + 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 f89334b..f8a4e7f 100644 --- a/machine/cortex-m/src/stub.rs +++ b/machine/cortex-m/src/stub.rs @@ -8,6 +8,7 @@ pub mod gpio; pub mod i2c; pub mod sched; pub mod spi; +pub mod uart; pub mod system; pub type Machine = StubMachine; 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 0d97dd2..c5857d6 100644 --- a/machine/cortex-m/st/stm32l4/interface/export.h +++ b/machine/cortex-m/st/stm32l4/interface/export.h @@ -8,8 +8,7 @@ void init_hal(void); __attribute__((noreturn)) void system_reset(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..2ddcf0b 100644 --- a/machine/cortex-m/st/stm32l4/interface/uart.c +++ b/machine/cortex-m/st/stm32l4/interface/uart.c @@ -1,95 +1,581 @@ +#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." +/* RX coalescing: HAL_UARTEx_ReceiveToIdle_IT lands bytes here and flushes + * on the hardware IDLE line (~1 char time after the last byte) or when + * full. 32 bounds worst-case tail latency (~2.8 ms @ 115200) for a + * gapless >32-byte stream; record-oriented traffic flushes at the + * inter-record gap. Must be <= UART_RX_RING_SZ - 1 (in-ISR copy target). */ +#define UART_RX_SCRATCH_SZ 32 +/* RX threshold must be 1/8: the HAL's ReceiveToIdle IDLE handler reports + * only RxXferSize-RxXferCount and never drains the RXFIFO, so any bytes + * left below a larger threshold are stranded in hardware and corrupt the + * next frame. At 1/8 the FIFO ISR drains every byte (RXFT re-fires while + * RXFNE), so the FIFO is empty at IDLE. TXFIFO threshold batches TX IT. */ +#define UART_RX_FIFO_THRESH UART_RXFIFO_THRESHOLD_1_8 +#define UART_TX_FIFO_THRESH UART_TXFIFO_THRESHOLD_1_2 + +typedef struct +{ + uint8_t in_use; + uint8_t console_owned; + uart_bus_cfg_t bus_cfg; + UART_HandleTypeDef huart; + + /* ReceiveToIdle landing buffer; drained into rx_ring in + * HAL_UARTEx_RxEventCallback on IDLE/threshold, then re-armed. */ + uint8_t rx_scratch[UART_RX_SCRATCH_SZ]; + 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; + + /* FIFO mode for the interrupt-driven (non-console) path only. The + * console is blocking and stays register-pristine. Thresholds must + * be set while FIFO is still disabled (they program CR3 RX/TXFTCFG); + * EnableFifoMode then sets CR1 FIFOEN and recomputes the HAL's + * Nb{Rx,Tx}DataToProcess from those thresholds. All three require + * post-HAL_UART_Init state. */ + if (!slot->console_owned) + { + if (HAL_UARTEx_SetTxFifoThreshold(&slot->huart, UART_TX_FIFO_THRESH) != HAL_OK) + return UART_ERR_IO; + if (HAL_UARTEx_SetRxFifoThreshold(&slot->huart, UART_RX_FIFO_THRESH) != HAL_OK) + return UART_ERR_IO; + if (HAL_UARTEx_EnableFifoMode(&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; + + if (!console_owned) + { + IRQn_Type irqn = (IRQn_Type)cfg->irqn; + HAL_NVIC_SetPriority(irqn, cfg->priority, 0); + HAL_NVIC_EnableIRQ(irqn); + /* Arm idle-line RX: the HAL fires HAL_UARTEx_RxEventCallback with + * the bytes received so far on the IDLE line or when rx_scratch + * fills, instead of one RxCplt per byte. */ + if (HAL_UARTEx_ReceiveToIdle_IT(&slot->huart, slot->rx_scratch, + UART_RX_SCRATCH_SZ) != HAL_OK) + return UART_ERR_IO; + } + + /* in_use is the last write: a failure above leaves the slot free, so + * uart_find_slot/uart_alloc_slot never hand out a half-initialized + * slot (fixes the failed-init-leaves-slot-stuck-in-use bug). */ + slot->in_use = 1; + + 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; + } +} + +/* Fires once per RX event (IDLE line, RXFIFO threshold, or rx_scratch + * full) with the number of bytes landed in rx_scratch — not per byte. + * IDLE and buffer-full are handled identically (no half-transfer split, + * so HAL_UARTEx_GetRxEventType is unnecessary). */ +void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) +{ + for (int i = 0; i < UART_SLOT_COUNT; ++i) + { + uart_slot_t *slot = &uart_slots[i]; + if (!slot->in_use || &slot->huart != huart) + continue; + + /* HAL bounds Size to the armed length; clamp defensively. */ + if (Size > UART_RX_SCRATCH_SZ) + Size = UART_RX_SCRATCH_SZ; + + for (uint16_t j = 0; j < Size; ++j) + { + uint16_t next = (uint16_t)((slot->rx_head + 1) % UART_RX_RING_SZ); + if (next == slot->rx_tail) + break; /* ring full ⇒ drop rest; consumer resyncs. */ + slot->rx_ring[slot->rx_head] = slot->rx_scratch[j]; + slot->rx_head = next; + } + + /* One wake per event, not per byte — the interrupt-load win. */ + if (Size > 0 && slot->cb) + slot->cb(UART_IRQ_RX, slot->cb_ctx); + + /* RxState was set READY by the HAL before this callback. */ + HAL_UARTEx_ReceiveToIdle_IT(&slot->huart, slot->rx_scratch, + UART_RX_SCRATCH_SZ); + return; + } +} + +/* On error the HAL aborts the hit transfer to READY without its + * completion callback, so that side stays dead (RX: no RxEvents; TX: + * tx_busy stuck). Recover only the side(s) actually torn down (state == + * READY); the other keeps running. Lost bytes: peer reframes. */ +void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) +{ + for (int i = 0; i < UART_SLOT_COUNT; ++i) + { + uart_slot_t *slot = &uart_slots[i]; + if (!slot->in_use || slot->console_owned || &slot->huart != huart) + continue; + + if (slot->tx_busy && slot->huart.gState == HAL_UART_STATE_READY) + { + /* TxCpltCallback won't fire: drop the in-flight chunk, + * re-arm queued bytes, wake a blocked writer. */ + slot->tx_tail = (uint16_t)((slot->tx_tail + slot->tx_in_flight) % UART_TX_RING_SZ); + slot->tx_in_flight = 0; + slot->tx_busy = 0; + uart_arm_tx(slot); + if (!slot->tx_busy && slot->cb) + slot->cb(UART_IRQ_TX_DONE, slot->cb_ctx); + } + + if (slot->huart.RxState == HAL_UART_STATE_READY) + HAL_UARTEx_ReceiveToIdle_IT(&slot->huart, slot->rx_scratch, + UART_RX_SCRATCH_SZ); + 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..4092a17 --- /dev/null +++ b/machine/cortex-m/st/stm32l4/interface/uart.h @@ -0,0 +1,85 @@ +#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_UARTEx_RxEventCallback + * once per RX event — a burst of bytes pushed to the RX ring on the IDLE + * line or RXFIFO threshold (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 04b8885..b1ddc57 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" OSIRIS_METRICS = "false" diff --git a/src/drivers.rs b/src/drivers.rs index 6d15f2c..816718e 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -3,6 +3,7 @@ pub mod i2c; pub mod key; pub mod led; pub mod spi; +pub mod uart; pub fn init() { i2c::init(); diff --git a/src/drivers/uart.rs b/src/drivers/uart.rs new file mode 100644 index 0000000..5add2ef --- /dev/null +++ b/src/drivers/uart.rs @@ -0,0 +1,351 @@ +pub use crate::hal::uart::{Error, Overrides}; + +use core::sync::atomic::{AtomicBool, Ordering}; +use core::time::Duration; + +use crate::sched; +use crate::sync::waiter::ParkedWaiter; +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, + /// `read_blocking` default; `None` blocks forever. + pub read_timeout: Option, + /// `write_blocking` default; `None` blocks forever. + pub write_timeout: Option, +} + +pub const UART_SLOT_COUNT: usize = 6; + +/// Latched once the process-global vector→`vector_dispatch` mapping is +/// live (it outlives every `Device`); set only after `register_irq` +/// succeeds. Per-Device `slot->cb` is re-installed each `open` instead. +static VECTOR_REGISTERED: [AtomicBool; UART_SLOT_COUNT] = + [const { AtomicBool::new(false) }; UART_SLOT_COUNT]; + +struct UartSlotWaiters { + rx: ParkedWaiter, + tx: ParkedWaiter, +} + +impl UartSlotWaiters { + const fn new() -> Self { + Self { + rx: ParkedWaiter::new(), + tx: ParkedWaiter::new(), + } + } +} + +// One reader + one writer thread per slot; a second is rejected with `Error::Busy`. +static UART_WAITERS: [UartSlotWaiters; UART_SLOT_COUNT] = + [const { UartSlotWaiters::new() }; UART_SLOT_COUNT]; + +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) = ensure_registered(&desc) { + if let Err(de) = crate::hal::uart::deinit(&desc) { + warn!("uart: deinit during open cleanup failed: {:?}", de); + } + 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), + } + } + + pub fn read_blocking(&self, buf: &mut [u8]) -> Result { + self.read_with_timeout(buf, self.read_timeout) + } + + /// `None` blocks forever. `Busy` if another thread already reads this UART. + 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(time::duration_to_ticks(d))); + register_rx_waiter(self.slot(), uid)?; + 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(None, d, now); + false + } + None => { + let _ = s.sleep_until(None, u64::MAX, now); + false + } + } + } + _ => true, + } + }); + if exit { + break got; + } + }; + unregister_rx_waiter(self.slot()); + result + } + + pub fn write_blocking(&self, buf: &[u8]) -> Result<(), Error> { + self.write_with_timeout(buf, self.write_timeout) + } + + /// Returns once the whole buffer is enqueued. `None` blocks forever; + /// on `TimedOut`, partial progress is not reported (use `write_nb`). + /// `Busy` if another thread already writes this UART. + 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(time::duration_to_ticks(d))); + register_tx_waiter(self.slot(), uid)?; + 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(None, d, now); + false + } + None => { + let _ = s.sleep_until(None, 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), + } + } + }; + unregister_tx_waiter(self.slot()); + result + } +} + +impl Drop for Device { + fn drop(&mut self) { + if let Err(e) = crate::hal::uart::deinit(&self.desc) { + warn!("uart: deinit on drop failed: {:?}", e); + } + } +} + +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 register_rx_waiter(slot: u8, uid: u32) -> Result<(), Error> { + UART_WAITERS[slot as usize] + .rx + .arm(uid as usize) + .map_err(|_| Error::Busy) +} + +fn unregister_rx_waiter(slot: u8) { + UART_WAITERS[slot as usize].rx.disarm(); +} + +fn register_tx_waiter(slot: u8, uid: u32) -> Result<(), Error> { + UART_WAITERS[slot as usize] + .tx + .arm(uid as usize) + .map_err(|_| Error::Busy) +} + +fn unregister_tx_waiter(slot: u8) { + UART_WAITERS[slot as usize].tx.disarm(); +} + +extern "C" fn kernel_dispatch(kind: crate::hal::uart::Irq, ctx: *mut ()) { + if ctx.is_null() { + return; + } + // SAFETY: `ctx` is the `&'static UartSlotWaiters` installed by + // `ensure_registered` and round-tripped by the HAL; `ParkedWaiter` + // is atomic, so concurrent ISR/thread access has no aliasing `&mut`. + let w = unsafe { &*(ctx as *const UartSlotWaiters) }; + match kind { + crate::hal::uart::Irq::Rx => w.rx.wake(), + crate::hal::uart::Irq::TxDone => w.tx.wake(), + } +} + +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<(), Error> { + let slot = dev.index(); + if (slot as usize) >= UART_SLOT_COUNT { + return Err(Error::InvalidArgument); + } + + // Claim the once-only vector install via CAS; release on failure so + // a later `open` can retry. Latch true only after success. + if VECTOR_REGISTERED[slot as usize] + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + // IPSR = NVIC line + 16: kernel `HANDLERS` is IPSR-indexed. + let vector = dev.irqn() as usize + 16; + if let Err(e) = + unsafe { crate::irq::register_irq(vector, vector_dispatch, Some(slot as usize)) } + { + VECTOR_REGISTERED[slot as usize].store(false, Ordering::Release); + warn!("uart: irq vector registration failed: {:?}", e); + return Err(Error::Io); + } + } + + // `deinit` clears the C slot's `cb`, so re-install it on every open + let ctx = &UART_WAITERS[slot as usize] as *const _ as *mut (); + crate::hal::uart::register_irq_handler(dev, Some(kernel_dispatch), ctx) +} diff --git a/src/time.rs b/src/time.rs index e5379b2..7de94da 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,3 +1,5 @@ +use core::time::Duration; + use crate::hal::{self, Machinelike}; use crate::{sched, sync}; @@ -24,6 +26,13 @@ pub fn to_secs(cnt: u64, hz: u32, digits: u8) -> (u64, u64) { (secs, frac) } +pub fn duration_to_ticks(d: Duration) -> u64 { + let freq = 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) +} + /// cbindgen:ignore /// cbindgen:no-export #[unsafe(no_mangle)] diff --git a/src/uapi.rs b/src/uapi.rs index 86feecf..bf7b5e6 100644 --- a/src/uapi.rs +++ b/src/uapi.rs @@ -7,3 +7,4 @@ pub mod sched; pub mod spi; pub mod system; 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 6cd303b..c24625b 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; @@ -7,6 +7,7 @@ mod i2c; mod key; mod led; mod spi; +mod uart; pub fn generate_rust(dt: &DeviceTree) -> String { enforce_unique_gpio_pins(dt); @@ -22,6 +23,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(), can::emit_registry(dt), can::emit_query_api(), led::emit_registry(dt), @@ -431,6 +434,15 @@ macro_rules! match_compatible { pub(crate) use 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") { diff --git a/xtasks/crates/dtgen/src/codegen/uart.rs b/xtasks/crates/dtgen/src/codegen/uart.rs new file mode 100644 index 0000000..9d72ef1 --- /dev/null +++ b/xtasks/crates/dtgen/src/codegen/uart.rs @@ -0,0 +1,348 @@ +//! UART specific code generation bus and console registry. + +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(|(base, _)| usize::try_from(base).ok()) + .unwrap_or_else(|| { + panic!( + "Pin controller node {} is missing a valid reg base", + pin_ctrl.name + ) + }); + 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")) +} + +/// Node index of the `chosen.osiris,console` UART, if any. +fn console_node_idx(dt: &DeviceTree) -> Option { + let chosen_idx = dt.by_name.get("chosen").and_then(|v| v.first()).copied()?; + let path = match dt.nodes[chosen_idx].extra.get("osiris,console")? { + PropValue::Str(s) => s.as_str(), + _ => return None, + }; + resolve_path(dt, path) +} + +fn collect(dt: &DeviceTree, console_idx: Option) -> 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 { + panic!("dtgen: UART node `{}` is missing a `reg` base", node.name); + }; + let Ok(instance) = usize::try_from(base) else { + panic!( + "dtgen: UART node `{}` has an out-of-range `reg` base {base:#x}", + node.name + ); + }; + + 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, + }; + + // IT mode needs `interrupts = `; the console is + // blocking, so keep it even without one (else: no boot console). + let (irqn, priority) = match node.interrupts.as_slice() { + [irqn, priority, ..] => (*irqn as u8, *priority as u8), + _ if Some(idx) == console_idx => (0, 0), + _ => panic!( + "dtgen: UART node `{}` has no `interrupts` property (required for \ + the interrupt-driven path; only the chosen console may omit it)", + node.name + ), + }; + + 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 { + panic!( + "dtgen: UART node `{}` has no `tx` pin in any pinctrl-* state", + node.name + ); + }; + let Some(rx) = rx else { + panic!( + "dtgen: UART node `{}` has no `rx` pin in any pinctrl-* state", + node.name + ); + }; + if flow_control == 1 && (rts.is_none() || cts.is_none()) { + panic!( + "dtgen: UART node `{}` sets `hw-flow-control` but is missing an `rts`/`cts` pin", + node.name + ); + } + + 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 +} + +pub fn emit_registry(dt: &DeviceTree) -> TokenStream { + let console_idx = console_node_idx(dt); + let buses = collect(dt, console_idx); + let console_const = match console_idx.and_then(|n| buses.iter().position(|b| b.node == n)) { + 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 + } + } +}