diff --git a/machine/cortex-m/src/native/gpio.rs b/machine/cortex-m/src/native/gpio.rs index 7f4659b..9609836 100644 --- a/machine/cortex-m/src/native/gpio.rs +++ b/machine/cortex-m/src/native/gpio.rs @@ -76,9 +76,19 @@ pub fn configure_input(pin: Pin, pull: Pull) -> Result<()> { ok_or_err(rc, ()) } -pub fn configure_output(pin: Pin, initial: Level) -> Result<()> { +pub fn configure_output(pin: Pin, initial: Level, pull: Pull) -> Result<()> { let mask = pin_mask(pin)?; - let rc = unsafe { bindings::gpio_configure_output_pp(port_ptr(pin), mask, initial as u8) }; + let rc = unsafe { + bindings::gpio_configure_output_pp(port_ptr(pin), mask, initial as u8, pull as u8) + }; + ok_or_err(rc, ()) +} + +pub fn configure_output_od(pin: Pin, initial: Level, pull: Pull) -> Result<()> { + let mask = pin_mask(pin)?; + let rc = unsafe { + bindings::gpio_configure_output_od(port_ptr(pin), mask, initial as u8, pull as u8) + }; ok_or_err(rc, ()) } @@ -88,6 +98,13 @@ pub fn write(pin: Pin, level: Level) -> Result<()> { ok_or_err(rc, ()) } +/// Ungate the port's clock without touching mode/pull — needed +/// before `read_odr` on a still-analog pin. +pub fn enable_port_clock(pin: Pin) -> Result<()> { + let rc = unsafe { bindings::gpio_clock_enable(port_ptr(pin)) }; + ok_or_err(rc, ()) +} + pub fn read(pin: Pin) -> Result { let mask = pin_mask(pin)?; let rc = unsafe { bindings::gpio_read(port_ptr(pin), mask) }; @@ -99,6 +116,20 @@ pub fn read(pin: Pin) -> Result { } } +/// Read ODR (latched output bit). Use instead of `read` on a pin +/// that may still be in analog mode — IDR is forced to 0 there +/// (RM0432 §8.3.12). Clock must be ungated. +pub fn read_odr(pin: Pin) -> Result { + let mask = pin_mask(pin)?; + let rc = unsafe { bindings::gpio_read_odr(port_ptr(pin), mask) }; + match rc { + 0 => Ok(Level::Low), + 1 => Ok(Level::High), + other if other < 0 => Err(PosixError::from_errno(-other)), + _ => Err(PosixError::EIO), + } +} + pub fn toggle(pin: Pin) -> Result<()> { let mask = pin_mask(pin)?; let rc = unsafe { bindings::gpio_toggle(port_ptr(pin), mask) }; diff --git a/machine/cortex-m/src/stub/device_tree.rs b/machine/cortex-m/src/stub/device_tree.rs index 7fbedfb..fa54f72 100644 --- a/machine/cortex-m/src/stub/device_tree.rs +++ b/machine/cortex-m/src/stub/device_tree.rs @@ -86,6 +86,29 @@ pub fn can_by_compatible(_compatible: &str, _ord: usize) -> Option<&'static CanR None } +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LedDefaultState { + Off, + On, + Keep, +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LedOutputMode { + PushPull, + OpenDrain, +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LedPull { + None, + Up, + Down, +} + #[derive(Debug, Clone, Copy)] #[repr(C)] pub struct LedRegistryEntry { @@ -94,6 +117,9 @@ pub struct LedRegistryEntry { pub line: u8, pub active_low: u8, pub label: &'static str, + pub default_state: LedDefaultState, + pub output_mode: LedOutputMode, + pub pull: LedPull, } pub const LED_REGISTRY: &[LedRegistryEntry] = &[]; diff --git a/machine/cortex-m/src/stub/gpio.rs b/machine/cortex-m/src/stub/gpio.rs index 78809eb..c83fe6b 100644 --- a/machine/cortex-m/src/stub/gpio.rs +++ b/machine/cortex-m/src/stub/gpio.rs @@ -51,7 +51,11 @@ pub fn configure_input(_pin: Pin, _pull: Pull) -> Result<()> { Err(PosixError::EOPNOTSUPP) } -pub fn configure_output(_pin: Pin, _initial: Level) -> Result<()> { +pub fn configure_output(_pin: Pin, _initial: Level, _pull: Pull) -> Result<()> { + Err(PosixError::EOPNOTSUPP) +} + +pub fn configure_output_od(_pin: Pin, _initial: Level, _pull: Pull) -> Result<()> { Err(PosixError::EOPNOTSUPP) } @@ -59,10 +63,18 @@ pub fn write(_pin: Pin, _level: Level) -> Result<()> { Err(PosixError::EOPNOTSUPP) } +pub fn enable_port_clock(_pin: Pin) -> Result<()> { + Err(PosixError::EOPNOTSUPP) +} + pub fn read(_pin: Pin) -> Result { Err(PosixError::EOPNOTSUPP) } +pub fn read_odr(_pin: Pin) -> Result { + Err(PosixError::EOPNOTSUPP) +} + pub fn toggle(_pin: Pin) -> Result<()> { Err(PosixError::EOPNOTSUPP) } diff --git a/machine/cortex-m/st/stm32l4/interface/export.h b/machine/cortex-m/st/stm32l4/interface/export.h index 0d97dd2..f8c4102 100644 --- a/machine/cortex-m/st/stm32l4/interface/export.h +++ b/machine/cortex-m/st/stm32l4/interface/export.h @@ -218,10 +218,15 @@ void can_isr(uint8_t index); #define GPIO_PULL_UP 1 #define GPIO_PULL_DOWN 2 int gpio_configure_input(void *port, uint16_t pin_mask, uint8_t pull); -int gpio_configure_output_pp(void *port, uint16_t pin_mask, uint8_t initial); +int gpio_configure_output_pp(void *port, uint16_t pin_mask, uint8_t initial, + uint8_t pull); +int gpio_configure_output_od(void *port, uint16_t pin_mask, uint8_t initial, + uint8_t pull); int gpio_write(void *port, uint16_t pin_mask, uint8_t level); int gpio_read(void *port, uint16_t pin_mask); +int gpio_read_odr(void *port, uint16_t pin_mask); int gpio_toggle(void *port, uint16_t pin_mask); +int gpio_clock_enable(void *port); // exti.c // edge_mask bitfield: 0x1 = rising, 0x2 = falling (see exti.h). diff --git a/machine/cortex-m/st/stm32l4/interface/gpio.c b/machine/cortex-m/st/stm32l4/interface/gpio.c index 81a42db..af19d48 100644 --- a/machine/cortex-m/st/stm32l4/interface/gpio.c +++ b/machine/cortex-m/st/stm32l4/interface/gpio.c @@ -139,11 +139,14 @@ int gpio_configure_input(void *port, uint16_t pin_mask, uint8_t pull) return 0; } -int gpio_configure_output_pp(void *port, uint16_t pin_mask, uint8_t initial) +int gpio_configure_output_pp(void *port, uint16_t pin_mask, uint8_t initial, + uint8_t pull) { GPIO_TypeDef *p = (GPIO_TypeDef *)port; if (!port_is_known(p) || pin_mask == 0) return -PosixError_EINVAL; + if (pull > GPIO_PULL_DOWN) + return -PosixError_EINVAL; gpio_enable_clock(p); /* Drive the initial level before switching the pin to output mode so the @@ -151,7 +154,28 @@ int gpio_configure_output_pp(void *port, uint16_t pin_mask, uint8_t initial) HAL_GPIO_WritePin(p, pin_mask, initial ? GPIO_PIN_SET : GPIO_PIN_RESET); GPIO_InitTypeDef gpio = {0}; gpio.Mode = GPIO_MODE_OUTPUT_PP; - gpio.Pull = GPIO_NOPULL; + gpio.Pull = pull_to_hal(pull); + gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + gpio.Pin = pin_mask; + HAL_GPIO_Init(p, &gpio); + return 0; +} + +int gpio_configure_output_od(void *port, uint16_t pin_mask, uint8_t initial, + uint8_t pull) +{ + GPIO_TypeDef *p = (GPIO_TypeDef *)port; + if (!port_is_known(p) || pin_mask == 0) + return -PosixError_EINVAL; + if (pull > GPIO_PULL_DOWN) + return -PosixError_EINVAL; + + gpio_enable_clock(p); + /* Pre-drive ODR before switching mode so the line never glitches. */ + HAL_GPIO_WritePin(p, pin_mask, initial ? GPIO_PIN_SET : GPIO_PIN_RESET); + GPIO_InitTypeDef gpio = {0}; + gpio.Mode = GPIO_MODE_OUTPUT_OD; + gpio.Pull = pull_to_hal(pull); gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio.Pin = pin_mask; HAL_GPIO_Init(p, &gpio); @@ -175,6 +199,19 @@ int gpio_read(void *port, uint16_t pin_mask) return HAL_GPIO_ReadPin(p, pin_mask) == GPIO_PIN_SET ? 1 : 0; } +/* Read ODR directly; gpio_read goes via IDR which is forced to 0 in + * analog mode (RM0432 §8.3.12). Single-bit mask only — multi-bit + * would OR several ODR bits into one 0/1. Clock must be ungated. */ +int gpio_read_odr(void *port, uint16_t pin_mask) +{ + GPIO_TypeDef *p = (GPIO_TypeDef *)port; + if (!port_is_known(p) || pin_mask == 0) + return -PosixError_EINVAL; + if (pin_mask & (uint16_t)(pin_mask - 1)) + return -PosixError_EINVAL; + return (p->ODR & pin_mask) ? 1 : 0; +} + int gpio_toggle(void *port, uint16_t pin_mask) { GPIO_TypeDef *p = (GPIO_TypeDef *)port; @@ -183,3 +220,12 @@ int gpio_toggle(void *port, uint16_t pin_mask) HAL_GPIO_TogglePin(p, pin_mask); return 0; } + +int gpio_clock_enable(void *port) +{ + GPIO_TypeDef *p = (GPIO_TypeDef *)port; + if (!port_is_known(p)) + return -PosixError_EINVAL; + gpio_enable_clock(p); + return 0; +} diff --git a/machine/cortex-m/st/stm32l4/interface/gpio.h b/machine/cortex-m/st/stm32l4/interface/gpio.h index 46ab9b0..31090e7 100644 --- a/machine/cortex-m/st/stm32l4/interface/gpio.h +++ b/machine/cortex-m/st/stm32l4/interface/gpio.h @@ -19,8 +19,13 @@ void gpio_init_output_od(GPIO_TypeDef *port, uint16_t pin_mask); * All return 0 on success or a negative PosixError code on failure. */ int gpio_configure_input(void *port, uint16_t pin_mask, uint8_t pull); -int gpio_configure_output_pp(void *port, uint16_t pin_mask, uint8_t initial); +int gpio_configure_output_pp(void *port, uint16_t pin_mask, uint8_t initial, + uint8_t pull); +int gpio_configure_output_od(void *port, uint16_t pin_mask, uint8_t initial, + uint8_t pull); int gpio_write(void *port, uint16_t pin_mask, uint8_t level); /* gpio_read returns 0 or 1 (level) on success, or negative PosixError. */ int gpio_read(void *port, uint16_t pin_mask); +int gpio_read_odr(void *port, uint16_t pin_mask); int gpio_toggle(void *port, uint16_t pin_mask); +int gpio_clock_enable(void *port); diff --git a/src/drivers/led.rs b/src/drivers/led.rs index c07508a..b4bf745 100644 --- a/src/drivers/led.rs +++ b/src/drivers/led.rs @@ -6,8 +6,8 @@ use crate::error::Result; use crate::hal; use crate::sync::once::OnceCell; -use hal::device_tree::LedRegistryEntry; -use hal::gpio::{Level, Pin}; +use hal::device_tree::{LedDefaultState, LedOutputMode, LedPull, LedRegistryEntry}; +use hal::gpio::{Level, Pin, Pull}; struct LedState { entry: &'static LedRegistryEntry, @@ -87,6 +87,36 @@ fn level_for(entry: &LedRegistryEntry, on: bool) -> Level { } } +/// `default-state = "keep"`. Reads ODR (set by whatever ran before us, +/// e.g. a bootloader) instead of IDR — IDR is forced to 0 on an +/// analog-mode pin (RM0432 §8.3.12). Only ODR=High is preserved; Low +/// falls back to logical-Off because ODR's reset value is also 0 +/// (RM0432 §8.4.6), so Low is ambiguous and would illuminate +/// active-low LEDs on cold boot. +fn keep_initial_level(entry: &LedRegistryEntry) -> Level { + let pin = pin_of(entry); + if let Err(e) = hal::gpio::enable_port_clock(pin) { + kprintln!( + " LED {}: keep: enable_port_clock failed ({:?}); falling back to Off", + entry.label, + e, + ); + return level_for(entry, false); + } + match hal::gpio::read_odr(pin) { + Ok(Level::High) => Level::High, + Ok(Level::Low) => level_for(entry, false), + Err(e) => { + kprintln!( + " LED {}: keep: read_odr failed ({:?}); falling back to Off", + entry.label, + e, + ); + level_for(entry, false) + } + } +} + pub fn init() { let entries = hal::device_tree::LED_REGISTRY; kprintln!("Found {} gpio-led entries", entries.len()); @@ -98,17 +128,33 @@ pub fn init() { }; let state_ref: &'static LedState = SLOTS[i].set_or_get(state); - // Drive the off level before switching to output so the line - // never glitches the active polarity on boot. - let off = level_for(entry, false); - match hal::gpio::configure_output(pin_of(entry), off) { + let initial = match entry.default_state { + LedDefaultState::Off => level_for(entry, false), + LedDefaultState::On => level_for(entry, true), + LedDefaultState::Keep => keep_initial_level(entry), + }; + let pull = match entry.pull { + LedPull::None => Pull::None, + LedPull::Up => Pull::Up, + LedPull::Down => Pull::Down, + }; + let res = match entry.output_mode { + LedOutputMode::OpenDrain => { + hal::gpio::configure_output_od(pin_of(entry), initial, pull) + } + LedOutputMode::PushPull => hal::gpio::configure_output(pin_of(entry), initial, pull), + }; + match res { Ok(()) => { state_ref.initialized.store(true, Ordering::Release); kprintln!( - " Initialized LED {} on port 0x{:x} line {}", + " Initialized LED {} on port 0x{:x} line {} ({:?}, {:?}, {:?})", entry.label, entry.port, - entry.line + entry.line, + entry.default_state, + entry.output_mode, + entry.pull, ); } Err(e) => kprintln!(" LED {}: configure_output failed: {:?}", entry.label, e), diff --git a/xtasks/crates/dtgen/src/codegen/led.rs b/xtasks/crates/dtgen/src/codegen/led.rs index b044720..8012d8e 100644 --- a/xtasks/crates/dtgen/src/codegen/led.rs +++ b/xtasks/crates/dtgen/src/codegen/led.rs @@ -4,6 +4,120 @@ use super::*; +#[derive(Clone, Copy)] +enum DefaultState { + Off, + On, + Keep, +} + +impl DefaultState { + fn from_node(node: &crate::ir::Node) -> Self { + match node.extra.get("default-state") { + Some(PropValue::Str(s)) => match s.as_str() { + "on" => DefaultState::On, + "keep" => DefaultState::Keep, + "off" => DefaultState::Off, + other => panic!( + "gpio-leds child {}: unknown `default-state` value {:?} \ + (expected \"on\", \"off\", or \"keep\")", + node.name, other + ), + }, + // Flag-form (`default-state;`) → Off, matching the Linux binding. + Some(PropValue::Empty) | None => DefaultState::Off, + Some(other) => panic!( + "gpio-leds child {}: `default-state` must be a string \ + (\"on\" / \"off\" / \"keep\"), got {:?}", + node.name, other + ), + } + } + + fn tokens(self) -> TokenStream { + match self { + DefaultState::Off => quote! { LedDefaultState::Off }, + DefaultState::On => quote! { LedDefaultState::On }, + DefaultState::Keep => quote! { LedDefaultState::Keep }, + } + } +} + +#[derive(Clone, Copy)] +enum OutputMode { + PushPull, + OpenDrain, +} + +impl OutputMode { + fn from_node(node: &crate::ir::Node) -> Self { + match node.extra.get("osiris,output-mode") { + Some(PropValue::Str(s)) => match s.as_str() { + "push-pull" => OutputMode::PushPull, + "open-drain" => OutputMode::OpenDrain, + other => panic!( + "gpio-leds child {}: unknown `osiris,output-mode` value {:?} \ + (expected \"push-pull\" or \"open-drain\")", + node.name, other + ), + }, + // Flag-form / absent → push-pull (the pre-existing default). + Some(PropValue::Empty) | None => OutputMode::PushPull, + Some(other) => panic!( + "gpio-leds child {}: `osiris,output-mode` must be a string \ + (\"push-pull\" / \"open-drain\"), got {:?}", + node.name, other + ), + } + } + + fn tokens(self) -> TokenStream { + match self { + OutputMode::PushPull => quote! { LedOutputMode::PushPull }, + OutputMode::OpenDrain => quote! { LedOutputMode::OpenDrain }, + } + } +} + +/// Linux/Zephyr `bias-*` triple (empty flag props). Absent → no pull. +#[derive(Clone, Copy)] +enum Pull { + None, + Up, + Down, +} + +impl Pull { + fn from_node(node: &crate::ir::Node) -> Self { + let up = node.extra.contains_key("bias-pull-up"); + let down = node.extra.contains_key("bias-pull-down"); + let disable = node.extra.contains_key("bias-disable"); + let count = up as u8 + down as u8 + disable as u8; + if count > 1 { + panic!( + "gpio-leds child {}: only one of `bias-pull-up`, `bias-pull-down`, \ + `bias-disable` may be set", + node.name + ); + } + if up { + Pull::Up + } else if down { + Pull::Down + } else { + Pull::None + } + } + + fn tokens(self) -> TokenStream { + match self { + Pull::None => quote! { LedPull::None }, + Pull::Up => quote! { LedPull::Up }, + Pull::Down => quote! { LedPull::Down }, + } + } +} + #[derive(Clone)] struct Led { node: usize, @@ -11,6 +125,9 @@ struct Led { line: u8, active_low: u8, label: String, + default_state: DefaultState, + output_mode: OutputMode, + pull: Pull, } fn collect_leds(dt: &DeviceTree) -> Vec { @@ -22,6 +139,9 @@ fn collect_leds(dt: &DeviceTree) -> Vec { line: c.line, active_low: c.active_low, label: c.label, + default_state: DefaultState::from_node(c.child), + output_mode: OutputMode::from_node(c.child), + pull: Pull::from_node(c.child), }) .collect(); @@ -53,6 +173,9 @@ pub fn emit_registry(dt: &DeviceTree) -> TokenStream { let line = l.line; let active_low = l.active_low; let label = l.label.as_str(); + let default_state = l.default_state.tokens(); + let output_mode = l.output_mode.tokens(); + let pull = l.pull.tokens(); quote! { LedRegistryEntry { node: #node, @@ -60,11 +183,38 @@ pub fn emit_registry(dt: &DeviceTree) -> TokenStream { line: #line, active_low: #active_low, label: #label, + default_state: #default_state, + output_mode: #output_mode, + pull: #pull, }, } }); quote! { + #[repr(u8)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum LedDefaultState { + Off, + On, + /// Preserve current ODR (warm restart); cold boot reads ODR → 0. + Keep, + } + + #[repr(u8)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum LedOutputMode { + PushPull, + OpenDrain, + } + + #[repr(u8)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum LedPull { + None, + Up, + Down, + } + #[derive(Debug, Clone, Copy)] #[repr(C)] pub struct LedRegistryEntry { @@ -73,6 +223,9 @@ pub fn emit_registry(dt: &DeviceTree) -> TokenStream { pub line: u8, pub active_low: u8, pub label: &'static str, + pub default_state: LedDefaultState, + pub output_mode: LedOutputMode, + pub pull: LedPull, } pub const LED_REGISTRY: &[LedRegistryEntry] = &[