From a5e8891fe315e2ee84992d94bd7f7d5b7710cce6 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Fri, 25 Jul 2025 18:57:27 -0700 Subject: Added support for PLL as a clock source on STM32WBA - PLL multiplier and dividers work - Added timer example --- embassy-stm32/Cargo.toml | 4 +- embassy-stm32/src/rcc/wba.rs | 247 +++++++++++++++++++++++++++++++++++++-- examples/stm32wba/src/bin/pwm.rs | 65 +++++++++++ 3 files changed, 304 insertions(+), 12 deletions(-) create mode 100644 examples/stm32wba/src/bin/pwm.rs diff --git a/embassy-stm32/Cargo.toml b/embassy-stm32/Cargo.toml index 02e75733e..520443466 100644 --- a/embassy-stm32/Cargo.toml +++ b/embassy-stm32/Cargo.toml @@ -81,7 +81,7 @@ futures-util = { version = "0.3.30", default-features = false } sdio-host = "0.9.0" critical-section = "1.1" #stm32-metapac = { version = "16" } -stm32-metapac = { git = "https://github.com/embassy-rs/stm32-data-generated", tag = "stm32-data-9fc86ca7b3a8bc05182bf1ce3045602df1f5dce3" } +stm32-metapac = { git = "https://github.com/embassy-rs/stm32-data-generated", tag = "stm32-data-df3f5212f2dd70955a6b3d0137e7b4457c6047bf" } vcell = "0.1.3" nb = "1.0.0" @@ -110,7 +110,7 @@ proc-macro2 = "1.0.36" quote = "1.0.15" #stm32-metapac = { version = "16", default-features = false, features = ["metadata"]} -stm32-metapac = { git = "https://github.com/embassy-rs/stm32-data-generated", tag = "stm32-data-9fc86ca7b3a8bc05182bf1ce3045602df1f5dce3", default-features = false, features = ["metadata"] } +stm32-metapac = { git = "https://github.com/embassy-rs/stm32-data-generated", tag = "stm32-data-df3f5212f2dd70955a6b3d0137e7b4457c6047bf", default-features = false, features = ["metadata"] } [features] default = ["rt"] diff --git a/embassy-stm32/src/rcc/wba.rs b/embassy-stm32/src/rcc/wba.rs index b494997b3..0025d2a51 100644 --- a/embassy-stm32/src/rcc/wba.rs +++ b/embassy-stm32/src/rcc/wba.rs @@ -1,19 +1,81 @@ pub use crate::pac::pwr::vals::Vos as VoltageScale; use crate::pac::rcc::regs::Cfgr1; -pub use crate::pac::rcc::vals::{Hpre as AHBPrescaler, Hsepre as HsePrescaler, Ppre as APBPrescaler, Sw as Sysclk}; +use core::ops::Div; +pub use crate::pac::rcc::vals::{ + Hpre as AHBPrescaler, Hsepre as HsePrescaler, Ppre as APBPrescaler, Sw as Sysclk, Pllsrc as PllSource, + Plldiv as PllDiv, Pllm as PllPreDiv, Plln as PllMul, Hpre5 as AHB5Prescaler, Hdiv5, +}; +use crate::pac::rcc::vals::Pllrge; use crate::pac::{FLASH, RCC}; +use crate::rcc::LSI_FREQ; use crate::time::Hertz; +#[cfg(all(peri_usb_otg_hs))] +pub use crate::pac::rcc::vals::Otghssel; + +#[cfg(all(peri_usb_otg_hs))] +pub use crate::pac::{syscfg::vals::Usbrefcksel, SYSCFG}; + /// HSI speed pub const HSI_FREQ: Hertz = Hertz(16_000_000); // HSE speed pub const HSE_FREQ: Hertz = Hertz(32_000_000); +// Allow dividing a Hertz value by an AHB5 prescaler directly +impl Div for Hertz { + type Output = Hertz; + fn div(self, rhs: AHB5Prescaler) -> Hertz { + // Map the prescaler enum to its integer divisor + let divisor = match rhs { + AHB5Prescaler::DIV1 => 1, + AHB5Prescaler::DIV2 => 2, + AHB5Prescaler::DIV3 => 3, + AHB5Prescaler::DIV4 => 4, + AHB5Prescaler::DIV6 => 6, + _ => unreachable!("Invalid AHB5 prescaler: {:?}", rhs), + }; + Hertz(self.0 / divisor) + } +} + #[derive(Clone, Copy, Eq, PartialEq)] pub struct Hse { pub prescaler: HsePrescaler, } +#[derive(Clone, Copy)] +pub struct Pll { + /// The clock source for the PLL. + pub source: PllSource, + /// The PLL pre-divider. + /// + /// The clock speed of the `source` divided by `m` must be between 4 and 16 MHz. + pub prediv: PllPreDiv, + /// The PLL multiplier. + /// + /// The multiplied clock – `source` divided by `m` times `n` – must be between 128 and 544 + /// MHz. The upper limit may be lower depending on the `Config { voltage_range }`. + pub mul: PllMul, + /// The divider for the P output. + /// + /// The P output is one of several options + /// that can be used to feed the SAI/MDF/ADF Clock mux's. + pub divp: Option, + /// The divider for the Q output. + /// + /// The Q ouput is one of severals options that can be used to feed the 48MHz clocks + /// and the OCTOSPI clock. It may also be used on the MDF/ADF clock mux's. + pub divq: Option, + /// The divider for the R output. + /// + /// When used to drive the system clock, `source` divided by `m` times `n` divided by `r` + /// must not exceed 160 MHz. System clocks above 55 MHz require a non-default + /// `Config { voltage_range }`. + pub divr: Option, + + pub frac: Option, +} + /// Clocks configuration #[derive(Clone, Copy)] pub struct Config { @@ -21,15 +83,20 @@ pub struct Config { pub hsi: bool, pub hse: Option, + // pll + pub pll1: Option, + // sysclk, buses. pub sys: Sysclk, pub ahb_pre: AHBPrescaler, + pub ahb5_pre: AHB5Prescaler, pub apb1_pre: APBPrescaler, pub apb2_pre: APBPrescaler, pub apb7_pre: APBPrescaler, // low speed LSI/LSE/RTC - pub ls: super::LsConfig, + pub lsi: super::LsConfig, + // pub lsi2: super::LsConfig, pub voltage_scale: VoltageScale, @@ -40,14 +107,17 @@ pub struct Config { impl Config { pub const fn new() -> Self { Config { - hse: None, hsi: true, + hse: None, + pll1: None, sys: Sysclk::HSI, ahb_pre: AHBPrescaler::DIV1, + ahb5_pre: AHB5Prescaler::DIV1, apb1_pre: APBPrescaler::DIV1, apb2_pre: APBPrescaler::DIV1, apb7_pre: APBPrescaler::DIV1, - ls: crate::rcc::LsConfig::new(), + lsi: crate::rcc::LsConfig::new(), + // lsi2: crate::rcc::LsConfig::new(), voltage_scale: VoltageScale::RANGE2, mux: super::mux::ClockMux::default(), } @@ -81,7 +151,7 @@ pub(crate) unsafe fn init(config: Config) { crate::pac::PWR.vosr().write(|w| w.set_vos(config.voltage_scale)); while !crate::pac::PWR.vosr().read().vosrdy() {} - let rtc = config.ls.init(); + let rtc = config.lsi.init(); let hsi = config.hsi.then(|| { hsi_enable(); @@ -99,11 +169,15 @@ pub(crate) unsafe fn init(config: Config) { HSE_FREQ }); + let pll_input = PllInput {hse, hsi }; + + let pll1 = init_pll(config.pll1, &pll_input, config.voltage_scale); + let sys_clk = match config.sys { Sysclk::HSE => hse.unwrap(), Sysclk::HSI => hsi.unwrap(), Sysclk::_RESERVED_1 => unreachable!(), - Sysclk::PLL1_R => todo!(), + Sysclk::PLL1_R => pll1.r.unwrap(), }; assert!(sys_clk.0 <= 100_000_000); @@ -111,7 +185,6 @@ pub(crate) unsafe fn init(config: Config) { let hclk1 = sys_clk / config.ahb_pre; let hclk2 = hclk1; let hclk4 = hclk1; - // TODO: hclk5 let (pclk1, pclk1_tim) = super::util::calc_pclk(hclk1, config.apb1_pre); let (pclk2, pclk2_tim) = super::util::calc_pclk(hclk1, config.apb2_pre); let (pclk7, _) = super::util::calc_pclk(hclk1, config.apb7_pre); @@ -157,6 +230,54 @@ pub(crate) unsafe fn init(config: Config) { w.set_ppre2(config.apb2_pre); }); + // Set AHB5 prescaler depending on sysclk source + RCC.cfgr4().modify(|w| match config.sys { + // When using HSI or HSE, use HDIV5 bit (0 = div1, 1 = div2) + Sysclk::HSI | Sysclk::HSE => { + // Only Div1 and Div2 are valid for HDIV5, enforce this + match config.ahb5_pre { + AHB5Prescaler::DIV1 => w.set_hdiv5(Hdiv5::DIV1), + AHB5Prescaler::DIV2 => w.set_hdiv5(Hdiv5::DIV2), + _ => panic!("Invalid ahb5_pre for HSI/HSE sysclk: only DIV1 and DIV2 are allowed"), + }; + } + // When using PLL1, use HPRE5 bits [2:0] + Sysclk::PLL1_R => { + w.set_hpre5(config.ahb5_pre); + } + _ => {} + }); + + let hclk5 = sys_clk / config.ahb5_pre; + + + #[cfg(all(stm32wba, peri_usb_otg_hs))] + let usb_refck = match config.mux.otghssel { + Otghssel::HSE => hse, + Otghssel::HSE_DIV_2 => hse.map(|hse_val| hse_val / 2u8), + Otghssel::PLL1_P => pll1.p, + Otghssel::PLL1_P_DIV_2 => pll1.p.map(|pll1p_val| pll1p_val / 2u8), + }; + #[cfg(all(stm32wba, peri_usb_otg_hs))] + let usb_refck_sel = match usb_refck { + Some(clk_val) => match clk_val { + Hertz(16_000_000) => Usbrefcksel::MHZ16, + Hertz(19_200_000) => Usbrefcksel::MHZ19_2, + Hertz(20_000_000) => Usbrefcksel::MHZ20, + Hertz(24_000_000) => Usbrefcksel::MHZ24, + Hertz(26_000_000) => Usbrefcksel::MHZ26, + Hertz(32_000_000) => Usbrefcksel::MHZ32, + _ => panic!("cannot select OTG_HS reference clock with source frequency of {}, must be one of 16, 19.2, 20, 24, 26, 32 MHz", clk_val), + }, + None => Usbrefcksel::MHZ24, + }; + #[cfg(all(stm32wba, peri_usb_otg_hs))] + SYSCFG.otghsphycr().modify(|w| { + w.set_clksel(usb_refck_sel); + }); + + let lsi = config.lsi.lsi.then_some(LSI_FREQ); + config.mux.init(); set_clocks!( @@ -164,6 +285,7 @@ pub(crate) unsafe fn init(config: Config) { hclk1: Some(hclk1), hclk2: Some(hclk2), hclk4: Some(hclk4), + hclk5: Some(hclk5), pclk1: Some(pclk1), pclk2: Some(pclk2), pclk7: Some(pclk7), @@ -171,12 +293,117 @@ pub(crate) unsafe fn init(config: Config) { pclk2_tim: Some(pclk2_tim), rtc: rtc, hse: hse, + lsi: lsi, hsi: hsi, + pll1_p: pll1.p, + pll1_q: pll1.q, + pll1_r: pll1.r, // TODO lse: None, - lsi: None, - pll1_p: None, - pll1_q: None, ); } + +pub(super) struct PllInput { + pub hsi: Option, + pub hse: Option, +} + +#[allow(unused)] +#[derive(Default)] +pub(super) struct PllOutput { + pub p: Option, + pub q: Option, + pub r: Option, +} + +fn pll_enable(enabled: bool) { + RCC.cr().modify(|w| w.set_pllon(enabled)); + while RCC.cr().read().pllrdy() != enabled {} +} + +fn init_pll(config: Option, input: &PllInput, voltage_range: VoltageScale) -> PllOutput { + // Disable PLL + pll_enable(false); + + let Some(pll) = config else { return PllOutput::default() }; + + let pre_src_freq = match pll.source { + PllSource::DISABLE => panic!("must not select PLL source as DISABLE"), + PllSource::HSE => unwrap!(input.hse), + PllSource::HSI => unwrap!(input.hsi), + PllSource::_RESERVED_1 => panic!("must not select RESERVED_1 source as DISABLE"), + }; + + // Only divide by the HSE prescaler when the PLL source is HSE + let src_freq = match pll.source { + PllSource::HSE => { + // read the prescaler bits and divide + let hsepre = RCC.cr().read().hsepre(); + pre_src_freq / hsepre + } + _ => pre_src_freq, + }; + + // Calculate the reference clock, which is the source divided by m + let ref_freq = src_freq / pll.prediv; + // Check limits per RM0515 § 12.4.3 + assert!(Hertz::mhz(4) <= ref_freq && ref_freq <= Hertz::mhz(16)); + + // Check PLL clocks per RM0515 § 12.4.5 + let (vco_min, vco_max, out_max) = match voltage_range { + VoltageScale::RANGE1 => (Hertz::mhz(128), Hertz::mhz(544), Hertz::mhz(100)), + VoltageScale::RANGE2 => panic!("PLL is unavailable in voltage range 2"), + }; + + // Calculate the PLL VCO clock + // let vco_freq = ref_freq * pll.mul; + // Calculate VCO frequency including fractional part: FVCO = Fref_ck × (N + FRAC/2^13) + let numerator = (ref_freq.0 as u64) * (((pll.mul as u64) + 1 << 13) + pll.frac.unwrap_or(0) as u64); + let vco_hz = (numerator >> 13) as u32; + let vco_freq = Hertz(vco_hz); + assert!(vco_freq >= vco_min && vco_freq <= vco_max); + + // Calculate output clocks. + let p = pll.divp.map(|div| vco_freq / div); + let q = pll.divq.map(|div| vco_freq / div); + let r = pll.divr.map(|div| vco_freq / div); + for freq in [p, q, r] { + if let Some(freq) = freq { + assert!(freq <= out_max); + } + } + + let divr = RCC.pll1divr(); + divr.write(|w| { + w.set_plln(pll.mul); + w.set_pllp(pll.divp.unwrap_or(PllDiv::DIV1)); + w.set_pllq(pll.divq.unwrap_or(PllDiv::DIV1)); + w.set_pllr(pll.divr.unwrap_or(PllDiv::DIV1)); + }); + RCC.pll1fracr().write(|w| {w.set_pllfracn(pll.frac.unwrap_or(0));}); + + let input_range = match ref_freq.0 { + ..=8_000_000 => Pllrge::FREQ_4TO8MHZ, + _ => Pllrge::FREQ_8TO16MHZ, + }; + + macro_rules! write_fields { + ($w:ident) => { + $w.set_pllpen(pll.divp.is_some()); + $w.set_pllqen(pll.divq.is_some()); + $w.set_pllren(pll.divr.is_some()); + $w.set_pllfracen(pll.frac.is_some()); + $w.set_pllm(pll.prediv); + $w.set_pllsrc(pll.source); + $w.set_pllrge(input_range); + }; + } + + RCC.pll1cfgr().write(|w| {write_fields!(w);}); + + // Enable PLL + pll_enable(true); + + PllOutput{ p, q, r } +} \ No newline at end of file diff --git a/examples/stm32wba/src/bin/pwm.rs b/examples/stm32wba/src/bin/pwm.rs new file mode 100644 index 000000000..54d223d34 --- /dev/null +++ b/examples/stm32wba/src/bin/pwm.rs @@ -0,0 +1,65 @@ +#![no_std] +#![no_main] + +use defmt::*; +use defmt_rtt as _; // global logger +use embassy_executor::Spawner; +use embassy_stm32::gpio::OutputType; +use embassy_stm32::rcc::{mux, AHB5Prescaler, AHBPrescaler, APBPrescaler, Sysclk, VoltageScale}; +use embassy_stm32::rcc::{PllDiv, PllMul, PllPreDiv, PllSource}; +use embassy_stm32::time::khz; +use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; +use embassy_stm32::Config; +use embassy_time::Timer; +use panic_probe as _; + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + info!("Hello World!"); + + let mut config = Config::default(); + // Fine-tune PLL1 dividers/multipliers + config.rcc.pll1 = Some(embassy_stm32::rcc::Pll { + source: PllSource::HSI, + prediv: PllPreDiv::DIV1, // PLLM = 1 → HSI / 1 = 16 MHz + mul: PllMul::MUL30, // PLLN = 30 → 16 MHz * 30 = 480 MHz VCO + divr: Some(PllDiv::DIV5), // PLLR = 5 → 96 MHz (Sysclk) + // divq: Some(PllDiv::DIV10), // PLLQ = 10 → 48 MHz (NOT USED) + divq: None, + divp: Some(PllDiv::DIV30), // PLLP = 30 → 16 MHz (USBOTG) + frac: Some(0), // Fractional part (enabled) + }); + + config.rcc.ahb_pre = AHBPrescaler::DIV1; + config.rcc.apb1_pre = APBPrescaler::DIV1; + config.rcc.apb2_pre = APBPrescaler::DIV1; + config.rcc.apb7_pre = APBPrescaler::DIV1; + config.rcc.ahb5_pre = AHB5Prescaler::DIV4; + + // voltage scale for max performance + config.rcc.voltage_scale = VoltageScale::RANGE1; + // route PLL1_P into the USB‐OTG‐HS block + config.rcc.mux.otghssel = mux::Otghssel::PLL1_P; + config.rcc.sys = Sysclk::PLL1_R; + + let p = embassy_stm32::init(config); + + let ch1_pin = PwmPin::new(p.PA2, OutputType::PushPull); + let mut pwm = SimplePwm::new(p.TIM3, Some(ch1_pin), None, None, None, khz(10), Default::default()); + let mut ch1 = pwm.ch1(); + ch1.enable(); + + info!("PWM initialized"); + info!("PWM max duty {}", ch1.max_duty_cycle()); + + loop { + ch1.set_duty_cycle_fully_off(); + Timer::after_millis(300).await; + ch1.set_duty_cycle_fraction(1, 4); + Timer::after_millis(300).await; + ch1.set_duty_cycle_fraction(1, 2); + Timer::after_millis(300).await; + ch1.set_duty_cycle(ch1.max_duty_cycle() - 1); + Timer::after_millis(300).await; + } +} -- cgit