Add core voltage scaling options and PLL parameter finder for RP2040

This commit is contained in:
1-rafael-1 2025-04-26 21:54:40 +02:00
parent 572e788b2e
commit 4ce3bdb370

View File

@ -69,6 +69,48 @@ pub enum PeriClkSrc {
// Gpin1 = ClkPeriCtrlAuxsrc::CLKSRC_GPIN1 as _ ,
}
/// Core voltage scaling options for RP2040.
/// See RP2040 Datasheet, Table 18.
#[cfg(feature = "rp2040")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum VoltageScale {
/// 0.85V
V0_85 = 0b1000,
/// 0.90V
V0_90 = 0b1001,
/// 0.95V
V0_95 = 0b1010,
/// 1.00V
V1_00 = 0b1011,
/// 1.05V
V1_05 = 0b1100,
/// 1.10V (Default)
V1_10 = 0b1101,
/// 1.15V
V1_15 = 0b1110,
/// 1.20V
V1_20 = 0b1111,
}
#[cfg(feature = "rp2040")]
impl VoltageScale {
/// Get the recommended Brown-Out Detection (BOD) setting for this voltage.
/// See RP2040 Datasheet, Table 19.
fn recommended_bod(self) -> u8 {
match self {
VoltageScale::V0_85 => 0b1000, // BOD recommends VSEL + 1
VoltageScale::V0_90 => 0b1001,
VoltageScale::V0_95 => 0b1010,
VoltageScale::V1_00 => 0b1011,
VoltageScale::V1_05 => 0b1100,
VoltageScale::V1_10 => 0b1101, // Default
VoltageScale::V1_15 => 0b1110,
VoltageScale::V1_20 => 0b1111,
}
}
}
/// CLock configuration.
#[non_exhaustive]
pub struct ClockConfig {
@ -89,6 +131,9 @@ pub struct ClockConfig {
/// RTC clock configuration.
#[cfg(feature = "rp2040")]
pub rtc_clk: Option<RtcClkConfig>,
/// Core voltage scaling (RP2040 only). Defaults to 1.10V if None.
#[cfg(feature = "rp2040")]
pub voltage_scale: Option<VoltageScale>,
// gpin0: Option<(u32, Gpin<'static, AnyPin>)>,
// gpin1: Option<(u32, Gpin<'static, AnyPin>)>,
}
@ -152,6 +197,93 @@ impl ClockConfig {
div_frac: 0,
phase: 0,
}),
#[cfg(feature = "rp2040")]
voltage_scale: None, // Use hardware default (1.10V)
// gpin0: None,
// gpin1: None,
}
}
/// Clock configuration derived from external crystal, targeting a specific SYS clock frequency for RP2040.
///
/// This function calculates the required PLL settings and core voltage based on the target frequency.
///
/// # Arguments
///
/// * `crystal_hz`: The frequency of the external crystal (e.g., 12_000_000 for 12MHz).
/// * `sys_freq_hz`: The target system clock frequency.
///
/// # Panics
///
/// Panics if the requested frequency is impossible to achieve with the given crystal,
/// or if the required voltage for the frequency is not supported or known.
///
/// # Safety Notes (RP2040)
///
/// * Frequencies > 133MHz require increased core voltage.
/// * This function automatically selects `VoltageScale::V1_15` for frequencies > 133MHz and <= 200MHz.
/// * Frequencies > 200MHz might require `VoltageScale::V1_20` or higher and are considered overclocking beyond datasheet recommendations.
/// This function will select `VoltageScale::V1_20` for frequencies > 200MHz, use with caution.
/// * Ensure your hardware supports the selected voltage and frequency.
#[cfg(feature = "rp2040")]
pub fn crystal_freq(crystal_hz: u32, sys_freq_hz: u32) -> Self {
// Determine required voltage based on target frequency
let voltage_scale = match sys_freq_hz {
0..=133_000_000 => VoltageScale::V1_10, // Default voltage is sufficient
133_000_001..=200_000_000 => VoltageScale::V1_15, // Requires 1.15V
_ => VoltageScale::V1_20, // Frequencies > 200MHz require at least 1.20V (Overclocking)
};
// Find suitable PLL parameters
let pll_params = find_pll_params(crystal_hz, sys_freq_hz)
.expect("Could not find valid PLL parameters for the requested frequency");
Self {
rosc: Some(RoscConfig {
hz: 6_500_000,
range: RoscRange::Medium,
drive_strength: [0; 8],
div: 16,
}),
xosc: Some(XoscConfig {
hz: crystal_hz,
sys_pll: Some(pll_params),
// Keep USB PLL at 48MHz for compatibility
usb_pll: Some(PllConfig {
refdiv: 1,
fbdiv: 120,
post_div1: 6,
post_div2: 5,
}),
delay_multiplier: 128,
}),
ref_clk: RefClkConfig {
src: RefClkSrc::Xosc,
div: 1,
},
sys_clk: SysClkConfig {
src: SysClkSrc::PllSys,
div_int: 1,
div_frac: 0,
},
peri_clk_src: Some(PeriClkSrc::Sys),
usb_clk: Some(UsbClkConfig {
src: UsbClkSrc::PllUsb,
div: 1,
phase: 0,
}),
adc_clk: Some(AdcClkConfig {
src: AdcClkSrc::PllUsb,
div: 1,
phase: 0,
}),
rtc_clk: Some(RtcClkConfig {
src: RtcClkSrc::PllUsb,
div_int: 1024,
div_frac: 0,
phase: 0,
}),
voltage_scale: Some(voltage_scale),
// gpin0: None,
// gpin1: None,
}
@ -192,8 +324,10 @@ impl ClockConfig {
div_frac: 171,
phase: 0,
}),
// gpin0: None,
// gpin1: None,
#[cfg(feature = "rp2040")]
voltage_scale: None, // Use hardware default (1.10V)
// gpin0: None,
// gpin1: None,
}
}
@ -405,6 +539,72 @@ pub struct RtcClkConfig {
pub phase: u8,
}
/// Find valid PLL parameters (refdiv, fbdiv, post_div1, post_div2) for a target output frequency
/// based on the input frequency.
///
/// See RP2040 Datasheet section 2.16.3. Reference Clock (ref) and 2.18.3. PLL
#[cfg(feature = "rp2040")]
fn find_pll_params(input_hz: u32, target_hz: u32) -> Option<PllConfig> {
// Constraints from datasheet:
// REFDIV: 1..=63
// FBDIV: 16..=320
// POSTDIV1: 1..=7
// POSTDIV2: 1..=7 (must be <= POSTDIV1)
// VCO frequency (input_hz / refdiv * fbdiv): 400MHz ..= 1600MHz
for refdiv in 1..=63 {
let ref_clk = input_hz / refdiv;
// Reference clock must be >= 5MHz (implied by VCO min / FBDIV max)
if ref_clk < 5_000_000 {
continue;
}
for fbdiv in (16..=320).rev() {
// Iterate high fbdiv first for better VCO stability
let vco_freq = ref_clk * fbdiv;
if !(400_000_000..=1_600_000_000).contains(&vco_freq) {
continue;
}
// We want vco_freq / (post_div1 * post_div2) = target_hz
// So, post_div1 * post_div2 = vco_freq / target_hz
let target_post_div_product = vco_freq as f64 / target_hz as f64;
if target_post_div_product < 1.0 || target_post_div_product > 49.0 {
// 7*7 = 49
continue;
}
// Manual rounding: floor(x + 0.5)
let target_post_div_product_int = (target_post_div_product + 0.5) as u32;
if target_post_div_product_int == 0 {
continue;
}
// Check if the rounded product gives the target frequency
if vco_freq / target_post_div_product_int != target_hz {
continue;
}
for post_div1 in (1..=7).rev() {
// Iterate high post_div1 first
if target_post_div_product_int % post_div1 == 0 {
let post_div2 = target_post_div_product_int / post_div1;
if (1..=7).contains(&post_div2) && post_div2 <= post_div1 {
// Found a valid combination
return Some(PllConfig {
refdiv: refdiv as u8, // Cast u32 to u8 (safe: 1..=63)
fbdiv: fbdiv as u16, // Cast u32 to u16 (safe: 16..=320)
post_div1: post_div1 as u8,
post_div2: post_div2 as u8,
});
}
}
}
}
}
None // No valid parameters found
}
/// safety: must be called exactly once at bootup
pub(crate) unsafe fn init(config: ClockConfig) {
// Reset everything except:
@ -447,23 +647,14 @@ pub(crate) unsafe fn init(config: ClockConfig) {
reset::reset(peris);
reset::unreset_wait(peris);
// let gpin0_freq = config.gpin0.map_or(0, |p| {
// core::mem::forget(p.1);
// p.0
// });
// CLOCKS.gpin0.store(gpin0_freq, Ordering::Relaxed);
// let gpin1_freq = config.gpin1.map_or(0, |p| {
// core::mem::forget(p.1);
// p.0
// });
// CLOCKS.gpin1.store(gpin1_freq, Ordering::Relaxed);
// Configure ROSC first if present
let rosc_freq = match config.rosc {
Some(config) => configure_rosc(config),
None => 0,
};
CLOCKS.rosc.store(rosc_freq, Ordering::Relaxed);
// Configure XOSC and PLLs if present
let (xosc_freq, pll_sys_freq, pll_usb_freq) = match config.xosc {
Some(config) => {
// start XOSC
@ -488,6 +679,7 @@ pub(crate) unsafe fn init(config: ClockConfig) {
CLOCKS.pll_sys.store(pll_sys_freq, Ordering::Relaxed);
CLOCKS.pll_usb.store(pll_usb_freq, Ordering::Relaxed);
// Configure REF clock source and divider
let (ref_src, ref_aux, clk_ref_freq) = {
use {ClkRefCtrlAuxsrc as Aux, ClkRefCtrlSrc as Src};
let div = config.ref_clk.div as u32;
@ -514,7 +706,7 @@ pub(crate) unsafe fn init(config: ClockConfig) {
w.set_int(config.ref_clk.div);
});
// Configure tick generation on the 2040.
// Configure tick generation using REF clock
#[cfg(feature = "rp2040")]
pac::WATCHDOG.tick().write(|w| {
w.set_cycles((clk_ref_freq / 1_000_000) as u16);
@ -532,6 +724,28 @@ pub(crate) unsafe fn init(config: ClockConfig) {
pac::TICKS.watchdog_ctrl().write(|w| w.set_enable(true));
}
// Set Core Voltage (RP2040 only) BEFORE switching SYS clock to high speed PLL
#[cfg(feature = "rp2040")]
if let Some(voltage) = config.voltage_scale {
let vreg = pac::VREG_AND_CHIP_RESET;
let current_vsel = vreg.vreg().read().vsel();
let target_vsel = voltage as u8;
if target_vsel != current_vsel {
// Set voltage and recommended BOD level
vreg.bod().write(|w| w.set_vsel(voltage.recommended_bod()));
vreg.vreg().write(|w| w.set_vsel(target_vsel));
// Wait 10us for regulator to settle. Delay calculation uses REF clock
// as it's guaranteed to be stable here, before SYS potentially switches.
// 10 us = 1/100_000 s. cycles = freq * time.
let delay_cycles = clk_ref_freq / 100_000;
// delay(N) waits N+1 cycles.
cortex_m::asm::delay(delay_cycles.saturating_sub(1));
}
}
// Configure SYS clock source and divider
let (sys_src, sys_aux, clk_sys_freq) = {
use {ClkSysCtrlAuxsrc as Aux, ClkSysCtrlSrc as Src};
let (src, aux, freq) = match config.sys_clk.src {