From 5f366957f31d87030182f80dd2d39dc8a8496883 Mon Sep 17 00:00:00 2001 From: James Munns Date: Fri, 12 Dec 2025 20:45:15 +0100 Subject: Add SOSC support --- embassy-mcxa/src/clkout.rs | 8 +++ embassy-mcxa/src/clocks/config.rs | 23 +++++++ embassy-mcxa/src/clocks/mod.rs | 128 +++++++++++++++++++++++++++++++++++++- embassy-mcxa/src/gpio.rs | 20 +++--- embassy-mcxa/src/i2c/mod.rs | 7 ++- embassy-mcxa/src/lib.rs | 24 ++++++- examples/mcxa/src/bin/clkout.rs | 85 +++++++++++++------------ 7 files changed, 240 insertions(+), 55 deletions(-) diff --git a/embassy-mcxa/src/clkout.rs b/embassy-mcxa/src/clkout.rs index 5b21f24b0..3495eb886 100644 --- a/embassy-mcxa/src/clkout.rs +++ b/embassy-mcxa/src/clkout.rs @@ -20,6 +20,7 @@ pub struct ClockOut<'a> { } /// Selected clock source to output +#[derive(Copy, Clone)] pub enum ClockOutSel { /// 12MHz Internal Oscillator Fro12M, @@ -36,6 +37,7 @@ pub enum ClockOutSel { } /// Configuration for the ClockOut +#[derive(Copy, Clone)] pub struct Config { /// Selected Source Clock pub sel: ClockOutSel, @@ -157,6 +159,12 @@ mod sealed { fn mux(&self) { self.set_function(crate::pac::port0::pcr0::Mux::$func); self.set_pull(Pull::Disabled); + + // TODO: we may want to expose these as options to allow the slew rate + // and drive strength for clocks if they are particularly high speed. + // + // self.set_drive_strength(crate::pac::port0::pcr0::Dse::Dse1); + // self.set_slew_rate(crate::pac::port0::pcr0::Sre::Sre0); } } }; diff --git a/embassy-mcxa/src/clocks/config.rs b/embassy-mcxa/src/clocks/config.rs index 0563b8917..9f97160ff 100644 --- a/embassy-mcxa/src/clocks/config.rs +++ b/embassy-mcxa/src/clocks/config.rs @@ -119,6 +119,28 @@ pub struct ClocksConfig { pub sirc: SircConfig, /// FRO16K clock source pub fro16k: Option, + /// SOSC, clk_in clock source + pub sosc: Option, +} + +/// The mode of the external reference clock +#[derive(Copy, Clone)] +pub enum SoscMode { + /// Passive crystal oscillators + CrystalOscillator, + /// Active external reference clock + ActiveClock, +} + +// SOSC/clk_in configuration +#[derive(Copy, Clone)] +pub struct SoscConfig { + /// Mode of the external reference clock + pub mode: SoscMode, + /// Specific frequency of the external reference clock + pub frequency: u32, + /// Power state of the external reference clock + pub power: PoweredClock, } // FIRC/FRO180M @@ -199,6 +221,7 @@ impl Default for ClocksConfig { vsys_domain_active: true, vdd_core_domain_active: true, }), + sosc: None, } } } diff --git a/embassy-mcxa/src/clocks/mod.rs b/embassy-mcxa/src/clocks/mod.rs index 667d79536..ceb1e2a50 100644 --- a/embassy-mcxa/src/clocks/mod.rs +++ b/embassy-mcxa/src/clocks/mod.rs @@ -87,6 +87,7 @@ pub fn init(settings: ClocksConfig) -> Result<(), ClockError> { operator.configure_firc_clocks()?; operator.configure_sirc_clocks()?; operator.configure_fro16k_clocks()?; + operator.configure_sosc()?; // For now, just use FIRC as the main/cpu clock, which should already be // the case on reset @@ -136,6 +137,7 @@ pub fn with_clocks R>(f: F) -> Option { #[non_exhaustive] pub struct Clocks { /// The `clk_in` is a clock provided by an external oscillator + /// AKA SOSC pub clk_in: Option, // FRO180M stuff @@ -485,8 +487,20 @@ impl Clocks { } /// Ensure the `clk_in` clock is active and valid at the given power state. - pub fn ensure_clk_in_active(&self, _at_level: &PoweredClock) -> Result { - Err(ClockError::NotImplemented { clock: "clk_in" }) + pub fn ensure_clk_in_active(&self, at_level: &PoweredClock) -> Result { + let Some(clk) = self.clk_in.as_ref() else { + return Err(ClockError::BadConfig { + clock: "clk_in", + reason: "required but not active", + }); + }; + if !clk.power.meets_requirement_of(at_level) { + return Err(ClockError::BadConfig { + clock: "clk_in", + reason: "not low power active", + }); + } + Ok(clk.frequency) } /// Ensure the `clk_16k_vsys` clock is active and valid at the given power state. @@ -851,6 +865,116 @@ impl ClockOperator<'_> { Ok(()) } + + /// Configure the SOSC/clk_in oscillator + fn configure_sosc(&mut self) -> Result<(), ClockError> { + let Some(parts) = self.config.sosc.as_ref() else { + return Ok(()); + }; + + let scg0 = unsafe { pac::Scg0::steal() }; + + // TODO: Config for the LDO? For now, if we have Sosc, just enable + // using the default settings: + // LDOBYPASS: 0/not bypassed + // VOUT_SEL: 0b100: 1.1v + // LDOEN: 0/Disabled + scg0.ldocsr().modify(|_r, w| w.ldoen().enabled()); + while scg0.ldocsr().read().vout_ok().is_disabled() {} + + // TODO: something something pins? This seems to work when the pins are + // not enabled, even if GPIO hasn't been initialized at all yet. + let eref = match parts.mode { + config::SoscMode::CrystalOscillator => pac::scg0::sosccfg::Erefs::Internal, + config::SoscMode::ActiveClock => pac::scg0::sosccfg::Erefs::External, + }; + let freq = parts.frequency; + + // TODO: Fix PAC names here + // + // #[doc = "0: Frequency range select of 8-16 MHz."] + // Freq16to20mhz = 0, + // #[doc = "1: Frequency range select of 16-25 MHz."] + // LowFreq = 1, + // #[doc = "2: Frequency range select of 25-40 MHz."] + // MediumFreq = 2, + // #[doc = "3: Frequency range select of 40-50 MHz."] + // HighFreq = 3, + let range = match freq { + 0..8_000_000 => { + return Err(ClockError::BadConfig { + clock: "clk_in", + reason: "freq too low", + }); + } + 8_000_000..16_000_000 => pac::scg0::sosccfg::Range::Freq16to20mhz, + 16_000_000..25_000_000 => pac::scg0::sosccfg::Range::LowFreq, + 25_000_000..40_000_000 => pac::scg0::sosccfg::Range::MediumFreq, + 40_000_000..50_000_001 => pac::scg0::sosccfg::Range::HighFreq, + 50_000_001.. => { + return Err(ClockError::BadConfig { + clock: "clk_in", + reason: "freq too high", + }); + } + }; + + // Set source/erefs and range + scg0.sosccfg().modify(|_r, w| { + w.erefs().variant(eref); + w.range().variant(range); + w + }); + + // Disable lock + scg0.sosccsr().modify(|_r, w| w.lk().clear_bit()); + + // TODO: We could enable the SOSC clock monitor. There are some things to + // figure out first: + // + // * This requires SIRC to be enabled, not sure which branch. Maybe fro12m_root? + // * If SOSC needs to work in deep sleep, AND the monitor is enabled: + // * SIRC also need needs to be low power + // * We need to decide if we need an interrupt or a reset if the monitor trips + + // Apply remaining config + scg0.sosccsr().modify(|_r, w| { + // For now, just disable the monitor. See above. + w.sosccm().disabled(); + + // Set deep sleep mode + match parts.power { + PoweredClock::NormalEnabledDeepSleepDisabled => { + w.soscsten().clear_bit(); + } + PoweredClock::AlwaysEnabled => { + w.soscsten().set_bit(); + } + } + + // Enable SOSC + w.soscen().enabled() + }); + + // Wait for SOSC to be valid, check for errors + while !scg0.sosccsr().read().soscvld().bit_is_set() {} + if scg0.sosccsr().read().soscerr().is_enabled_and_error() { + return Err(ClockError::BadConfig { + clock: "clk_in", + reason: "soscerr is set", + }); + } + + // Re-lock the sosc + scg0.sosccsr().modify(|_r, w| w.lk().set_bit()); + + self.clocks.clk_in = Some(Clock { + frequency: freq, + power: parts.power, + }); + + Ok(()) + } } // diff --git a/embassy-mcxa/src/gpio.rs b/embassy-mcxa/src/gpio.rs index 65f8df985..29d66656d 100644 --- a/embassy-mcxa/src/gpio.rs +++ b/embassy-mcxa/src/gpio.rs @@ -81,7 +81,7 @@ fn GPIO4() { irq_handler(4, crate::pac::Gpio4::ptr()); } -pub(crate) unsafe fn init() { +pub(crate) unsafe fn interrupt_init() { use embassy_hal_internal::interrupt::InterruptExt; crate::pac::interrupt::GPIO0.enable(); @@ -320,8 +320,12 @@ impl GpioPin for AnyPin {} macro_rules! impl_pin { ($peri:ident, $port:expr, $pin:expr, $block:ident) => { + impl_pin!(crate::peripherals, $peri, $port, $pin, $block); + }; + + ($perip:path, $peri:ident, $port:expr, $pin:expr, $block:ident) => { paste! { - impl SealedPin for crate::peripherals::$peri { + impl SealedPin for $perip::$peri { fn pin_port(&self) -> usize { $port * 32 + $pin } @@ -372,15 +376,15 @@ macro_rules! impl_pin { } } - impl GpioPin for crate::peripherals::$peri {} + impl GpioPin for $perip::$peri {} - impl From for AnyPin { - fn from(value: crate::peripherals::$peri) -> Self { + impl From<$perip::$peri> for AnyPin { + fn from(value: $perip::$peri) -> Self { value.degrade() } } - impl crate::peripherals::$peri { + impl $perip::$peri { /// Convenience helper to obtain a type-erased handle to this pin. pub fn degrade(&self) -> AnyPin { AnyPin::new(self.port(), self.pin(), self.gpio(), self.port_reg(), self.pcr_reg()) @@ -453,8 +457,8 @@ impl_pin!(P1_26, 1, 26, Gpio1); impl_pin!(P1_27, 1, 27, Gpio1); impl_pin!(P1_28, 1, 28, Gpio1); impl_pin!(P1_29, 1, 29, Gpio1); -impl_pin!(P1_30, 1, 30, Gpio1); -impl_pin!(P1_31, 1, 31, Gpio1); +impl_pin!(crate::internal_peripherals, P1_30, 1, 30, Gpio1); +impl_pin!(crate::internal_peripherals, P1_31, 1, 31, Gpio1); impl_pin!(P2_0, 2, 0, Gpio2); impl_pin!(P2_1, 2, 1, Gpio2); diff --git a/embassy-mcxa/src/i2c/mod.rs b/embassy-mcxa/src/i2c/mod.rs index 9a014224a..55c933f71 100644 --- a/embassy-mcxa/src/i2c/mod.rs +++ b/embassy-mcxa/src/i2c/mod.rs @@ -180,8 +180,11 @@ impl_pin!(P1_12, LPI2C1, Mux2, SdaPin); impl_pin!(P1_13, LPI2C1, Mux2, SclPin); impl_pin!(P1_14, LPI2C1, Mux2, SclPin); impl_pin!(P1_15, LPI2C1, Mux2, SdaPin); -impl_pin!(P1_30, LPI2C0, Mux3, SdaPin); -impl_pin!(P1_31, LPI2C0, Mux3, SclPin); +// NOTE: P1_30 and P1_31 are typically used for the external oscillator +// For now, we just don't give users these pins. +// +// impl_pin!(P1_30, LPI2C0, Mux3, SdaPin); +// impl_pin!(P1_31, LPI2C0, Mux3, SclPin); impl_pin!(P3_27, LPI2C3, Mux2, SclPin); impl_pin!(P3_28, LPI2C3, Mux2, SdaPin); // impl_pin!(P3_29, LPI2C3, Mux2, HreqPin); What is this HREQ pin? diff --git a/embassy-mcxa/src/lib.rs b/embassy-mcxa/src/lib.rs index 22bc40e35..8d39728a7 100644 --- a/embassy-mcxa/src/lib.rs +++ b/embassy-mcxa/src/lib.rs @@ -174,8 +174,18 @@ embassy_hal_internal::peripherals!( P1_27, P1_28, P1_29, - P1_30, - P1_31, + // TODO: These pins are optionally used as the clock sources for SOSC. + // Ideally, we'd want to have a custom version of the `peripheral!` macro + // that presented these as `Option>` instead of `Peri<'_, P1_30>` + // when the user DOES enable the external SOSC. For now, I'm guessing MOST designs + // will have an external clock sitting on these pins anyway, so we just notch them + // out from the `Peripherals` struct given to users. + // + // If you find this and want your extra two pins to be available: please open an + // embassy issue to discuss how we could do this. + // + // P1_30, + // P1_31, P2_0, P2_1, @@ -336,6 +346,14 @@ embassy_hal_internal::peripherals!( WWDT0, ); +// See commented out items above to understand why we create the instances +// here but don't give them to the user. +pub(crate) mod internal_peripherals { + embassy_hal_internal::peripherals_definition!(P1_30, P1_31,); + + pub(crate) use peripherals::*; +} + // Use cortex-m-rt's #[interrupt] attribute directly; PAC does not re-export it. // Re-export interrupt traits and types @@ -368,7 +386,7 @@ pub fn init(cfg: crate::config::Config) -> Peripherals { crate::clocks::init(cfg.clock_cfg).unwrap(); unsafe { - crate::gpio::init(); + crate::gpio::interrupt_init(); } // Initialize DMA controller (clock, reset, configuration) diff --git a/examples/mcxa/src/bin/clkout.rs b/examples/mcxa/src/bin/clkout.rs index 1e52912d3..ba7c8185e 100644 --- a/examples/mcxa/src/bin/clkout.rs +++ b/examples/mcxa/src/bin/clkout.rs @@ -4,6 +4,7 @@ use embassy_executor::Spawner; use embassy_mcxa::clkout::{ClockOut, ClockOutSel, Config, Div4}; use embassy_mcxa::clocks::PoweredClock; +use embassy_mcxa::clocks::config::{SoscConfig, SoscMode, SoscRange}; use embassy_mcxa::gpio::{DriveStrength, Level, Output, SlewRate}; use embassy_time::Timer; use {defmt_rtt as _, embassy_mcxa as hal, panic_probe as _}; @@ -11,58 +12,62 @@ use {defmt_rtt as _, embassy_mcxa as hal, panic_probe as _}; /// Demonstrate CLKOUT, using Pin P4.2 #[embassy_executor::main] async fn main(_spawner: Spawner) { - let p = hal::init(hal::config::Config::default()); + let mut cfg = hal::config::Config::default(); + cfg.clock_cfg.sosc = Some(SoscConfig { + mode: SoscMode::CrystalOscillator, + frequency: 8_000_000, + range: SoscRange::Mhz8To16, + power: PoweredClock::NormalEnabledDeepSleepDisabled, + }); + + let p = hal::init(cfg); + let mut pin = p.P4_2; let mut clkout = p.CLKOUT; - loop { - defmt::info!("Set Low..."); - let mut output = Output::new(pin.reborrow(), Level::Low, DriveStrength::Normal, SlewRate::Slow); - Timer::after_millis(500).await; + const K16_CONFIG: Config = Config { + sel: ClockOutSel::Clk16K, + div: Div4::no_div(), + level: PoweredClock::NormalEnabledDeepSleepDisabled, + }; + const M4_CONFIG: Config = Config { + sel: ClockOutSel::Fro12M, + div: const { Div4::from_divisor(3).unwrap() }, + level: PoweredClock::NormalEnabledDeepSleepDisabled, + }; + const K512_CONFIG: Config = Config { + sel: ClockOutSel::ClkIn, + div: const { Div4::from_divisor(16).unwrap() }, + level: PoweredClock::NormalEnabledDeepSleepDisabled, + }; + let configs = [ + ("16K -> /1 = 16K", K16_CONFIG), + ("12M -> /3 = 4M", M4_CONFIG), + ("8M -> /16 = 512K", K512_CONFIG), + ]; + + loop { defmt::info!("Set High..."); - output.set_high(); - Timer::after_millis(400).await; + let mut output = Output::new(pin.reborrow(), Level::High, DriveStrength::Normal, SlewRate::Slow); + Timer::after_millis(250).await; defmt::info!("Set Low..."); output.set_low(); - Timer::after_millis(500).await; + Timer::after_millis(750).await; - defmt::info!("16k..."); - // Run Clock Out with the 16K clock - let _clock_out = ClockOut::new( - clkout.reborrow(), - pin.reborrow(), - Config { - sel: ClockOutSel::Clk16K, - div: Div4::no_div(), - level: PoweredClock::NormalEnabledDeepSleepDisabled, - }, - ) - .unwrap(); + for (name, conf) in configs.iter() { + defmt::info!("Running {=str}", name); - Timer::after_millis(3000).await; - - defmt::info!("Set Low..."); - drop(_clock_out); + let _clock_out = ClockOut::new(clkout.reborrow(), pin.reborrow(), *conf).unwrap(); - let _output = Output::new(pin.reborrow(), Level::Low, DriveStrength::Normal, SlewRate::Slow); - Timer::after_millis(500).await; + Timer::after_millis(3000).await; - // Run Clock Out with the 12M clock, divided by 3 - defmt::info!("4M..."); - let _clock_out = ClockOut::new( - clkout.reborrow(), - pin.reborrow(), - Config { - sel: ClockOutSel::Fro12M, - div: const { Div4::from_divisor(3).unwrap() }, - level: PoweredClock::NormalEnabledDeepSleepDisabled, - }, - ) - .unwrap(); + defmt::info!("Set Low..."); + drop(_clock_out); - // Let it run for 3 seconds... - Timer::after_millis(3000).await; + let _output = Output::new(pin.reborrow(), Level::Low, DriveStrength::Normal, SlewRate::Slow); + Timer::after_millis(500).await; + } } } -- cgit From d6c65cd0e4b651b1b07e1583562dfccfd5db22b1 Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 15 Dec 2025 14:33:47 +0100 Subject: Update example to not explicitly configure range --- examples/mcxa/src/bin/clkout.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/mcxa/src/bin/clkout.rs b/examples/mcxa/src/bin/clkout.rs index ba7c8185e..e6e6a2d3d 100644 --- a/examples/mcxa/src/bin/clkout.rs +++ b/examples/mcxa/src/bin/clkout.rs @@ -4,7 +4,7 @@ use embassy_executor::Spawner; use embassy_mcxa::clkout::{ClockOut, ClockOutSel, Config, Div4}; use embassy_mcxa::clocks::PoweredClock; -use embassy_mcxa::clocks::config::{SoscConfig, SoscMode, SoscRange}; +use embassy_mcxa::clocks::config::{SoscConfig, SoscMode}; use embassy_mcxa::gpio::{DriveStrength, Level, Output, SlewRate}; use embassy_time::Timer; use {defmt_rtt as _, embassy_mcxa as hal, panic_probe as _}; @@ -16,7 +16,6 @@ async fn main(_spawner: Spawner) { cfg.clock_cfg.sosc = Some(SoscConfig { mode: SoscMode::CrystalOscillator, frequency: 8_000_000, - range: SoscRange::Mhz8To16, power: PoweredClock::NormalEnabledDeepSleepDisabled, }); -- cgit