psoc-drivers 0.1.0

Hardware driver implementations for psoc-rs
// Copyright (c) 2026, Infineon Technologies AG or an affiliate of Infineon Technologies AG.
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
// in compliance with the License. You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the
// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing permissions and
// limitations under the License.

use crate::{regs, sys};

use super::{Bypass, Multiplier};

/// A frequency-locked loop.
///
/// An FLL consists of a current-controlled oscillator (CCO) and counters that measure the CCO
/// frequency against a reference clock. The counters are used to adjust the trim of the CCO.
///
/// An FLL consumes less current than a PLL, but does not lock the phase.
#[derive(Debug)]
#[non_exhaustive]
pub struct Fll;

/// Configuration for an FLL.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FllConfig {
    /// Multiplier to determine CCO frequency in multiples of the frequency of the selected reference
    /// clock (Fref).
    ///
    /// Ffll = multiplier * (Fref / reference_divider) / (if output_divider { 2 } else { 1 })
    pub multiplier: u32,

    /// Whether to divide the FLL output frequency by 2.
    pub output_divider: bool,

    /// Divider for the reference clock. Must be in 1..=8191.
    pub reference_divider: u16,

    #[cfg(mxs40ssrss)]
    /// Error threshold for updating the CCO frequency setting. The update tolerance is the
    /// allowed difference between the count value for the ideal formula and the measured value.
    /// Should be less than lock_tolerance.
    pub update_tolerance: u8,

    #[cfg(mxs40ssrss)]
    /// Lock tolerance for the CCO. If the count value is outside of this tolerance, the FLL will
    /// be considered unlocked. A high tolerance can be used to lock more quickly.
    pub lock_tolerance: u8,

    #[cfg(mxs40srss)]
    /// Lock tolerance for the CCO. If the count value is outside of this tolerance, the FLL will
    /// be considered unlocked. A high tolerance can be used to lock more quickly.
    ///
    /// Must be in 0..=511.
    pub lock_tolerance: u16,

    /// Proportional gain for the FLL's feedback loop.
    ///
    /// The actual gain is 2^(p_gain-8), and the value of p_gain must be in 0..=11,
    /// resulting in a gain between 1/256 and 8.
    pub p_gain: u8,

    /// Integral gain for the FLL's feedback loop.
    ///
    /// The actual gain is 2^(i_gain-8), and the value of i_gain must be in 0..=11,
    /// resulting in a gain between 1/256 and 8.
    pub i_gain: u8,

    /// Number of undivided reference clock cycles to wait after changing the CCO trim until the loop
    /// measurement restarts. A delay allows the CCO output to settle and gives a more accurate mea-
    /// surement. The default is tuned to an 8MHz reference clock since the IMO is expected to be the
    /// most common use case.
    pub settling_count: u16,

    /// Whether to disable hardware update of CCO frequency and use the CCO in open-loop mode.
    pub open_loop: bool,

    /// Target frequency for current-controlled oscillator.
    pub cco_frequency: CcoConfig,

    /// Maximum offset that can be applied to the CCO frequency.
    pub cco_limit: u8,

    /// Configures the bypass multiplexer located after the FLL's output.
    pub bypass_mode: Bypass,
}

/// Current-controlled oscillator frequency configuration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CcoConfig {
    /// The range selector (CLK_FLL_CONFIG4.CCO_RANGE).
    pub range: u8,

    /// The frequency selector within the range (CLK_FLL_CONFIG4.CCO_FREQ).
    pub trim: u16,
}

impl FllConfig {
    /// Computes an FLL configuration from a reference frequency and target frequency.
    pub const fn from_frequency(reference: u32, target: u32) -> Self {
        debug_assert!(target >= 24_000_000 && target <= 100_000_000);

        let cco_freq = target * 2;
        let cco_config = CcoConfig::from_frequency(cco_freq);
        let ref_div = ((reference as u64) * 250 / (target as u64)) as u16;
        let multiplier = (cco_freq as u64 * ref_div as u64 / reference as u64) as u32;

        // Compute the lock tolerance.
        // Formula is lock tolerance = 1.5 * fllMult * (((1 + CCO accuracy) / (1 - source clock accuracy)) - 1)
        // We assume CCO accuracy is 0.25%.
        // We assume the source clock accuracy = 1%. This is the accuracy of the IMO.
        // Therefore the formula is lock tolerance = 1.5 * fllMult * 0.012626 = 0.018939 * fllMult */
        #[expect(clippy::zero_prefixed_literal)]
        let lock_tolerance = multiplier * 0_018_939 / 1_000_000;

        // Compute the gains.
        // K_cco = base_freq * (trim_step - 1)
        let (_, base_freq, trim_step) = CcoConfig::cco_constants(cco_freq);
        let k_cco = base_freq as f32 * (trim_step - 1.);

        // gain = 0.85 / (K_cco * N / f_ref)
        let gain = 0.85 / (k_cco * ref_div as f32 / reference as f32);

        // choose largest i_gain < gain
        let scaled_gain = gain * 256.;
        let i_gain = (scaled_gain.min(8. * 256.) as u16).checked_ilog2();
        let i_gain = if let Some(i_gain) = i_gain {
            i_gain as u8
        } else {
            0
        };

        // choose largest p_gain < (gain - i_gain)
        let scaled_p_gain = scaled_gain - (1 << i_gain) as f32;
        let p_gain = (scaled_p_gain.min(8. * 256.) as u16).checked_ilog2();
        let p_gain = if let Some(p_gain) = p_gain {
            p_gain as u8
        } else {
            0
        };

        // Compute the settling count, using a 1 usec settling time.
        let settling_count = (reference / 1_000_000) as u16;

        FllConfig {
            multiplier,
            output_divider: true,
            reference_divider: ref_div,
            #[cfg(mxs40ssrss)]
            update_tolerance: 0,
            lock_tolerance: lock_tolerance as _,
            p_gain,
            i_gain,
            settling_count,
            open_loop: false,
            cco_frequency: cco_config,
            cco_limit: 0xFF,
            bypass_mode: Bypass::Auto,
        }
    }
}

impl Multiplier for Fll {
    type Config = FllConfig;
    unsafe fn steal() -> &'static mut Self {
        // SAFETY: Fll is a zero-sized type
        unsafe { &mut *core::ptr::dangling_mut() }
    }

    /// Enables or disables the FLL with the given configuration.
    fn configure(&mut self, config: Option<&FllConfig>) {
        unsafe {
            if let Some(config) = config {
                if regs::SRSS.clk_fll_config().read().fll_enable().get() {
                    // FLL is already enabled, disable it before applying the new configuration.
                    self.configure(None);
                }

                // 1. Configure FLL and CCO settings, but don't enable the FLL yet.
                debug_assert!(config.multiplier <= 0x3FFFF);
                let fll_config = regs::srss::ClkFllConfig::default()
                    .fll_output_div()
                    .set(config.output_divider)
                    .fll_mult()
                    .set(config.multiplier)
                    .fll_enable()
                    .set(false);
                regs::SRSS.clk_fll_config().write(fll_config);
                debug_assert!(matches!(config.reference_divider, 1..=8191));
                #[cfg(mxs40srss)]
                core::debug_assert!(matches!(config.lock_tolerance, 0..=511));
                regs::SRSS.clk_fll_config2().init(|mut r| {
                    r = r
                        .fll_ref_div()
                        .set(config.reference_divider)
                        .lock_tol()
                        .set(config.lock_tolerance);
                    #[cfg(mxs40ssrss)]
                    {
                        r = r.update_tol().set(config.update_tolerance);
                    }
                    r
                });

                debug_assert!(config.p_gain <= 11);
                debug_assert!(config.i_gain <= 11);
                regs::SRSS.clk_fll_config3().init(|r| {
                    r.fll_lf_pgain()
                        .set(config.p_gain)
                        .fll_lf_igain()
                        .set(config.i_gain)
                        .settling_count()
                        .set(config.settling_count)
                        .bypass_sel()
                        .set(
                            (match config.bypass_mode {
                                Bypass::MultiplierOutputOnly => Bypass::Auto,
                                m => m,
                            } as u8)
                                .into(),
                        )
                });

                debug_assert!(config.cco_frequency.range <= 4);
                debug_assert!(config.cco_frequency.trim <= 0x1FF);
                regs::SRSS.clk_fll_config4().init(|r| {
                    r.cco_hw_update_dis()
                        .set(config.open_loop)
                        .cco_range()
                        .set(regs::srss::clk_fll_config4::CcoRange::new(
                            config.cco_frequency.range,
                        ))
                        .cco_freq()
                        .set(config.cco_frequency.trim)
                        .cco_enable()
                        .set(true)
                });

                // 2. Wait for CCO to become ready.
                while !regs::SRSS.clk_fll_status().read().cco_ready().get() {}

                // 3. Enable the FLL.
                regs::SRSS
                    .clk_fll_config()
                    .write(fll_config.fll_enable().set(true));

                if config.bypass_mode == Bypass::MultiplierOutputOnly {
                    // If the bypass mode is FLL output only, we need to switch it once the FLL is
                    // locked.
                    while !regs::SRSS.clk_fll_status().read().locked().get() {}
                    regs::SRSS.clk_fll_config3().modify(|r| {
                        r.bypass_sel()
                            .set((Bypass::MultiplierOutputOnly as u8).into())
                    });
                }
            } else {
                // Disable the FLL.

                // 1. Bypass the FLL output.
                regs::SRSS.clk_fll_config3().modify(|r| {
                    r.bypass_sel()
                        .set((Bypass::ReferenceInputOnly as u8).into())
                });
                // Dummy read to ensure the write is posted.
                _ = regs::SRSS.clk_fll_config3().read();

                // 2. Wait at least 6 reference clock cycles.
                sys::delay_microseconds(2);

                // 3. Disable the CCO.
                regs::SRSS
                    .clk_fll_config4()
                    .modify(|r| r.cco_enable().set(false));
            }
        }
    }
}

impl CcoConfig {
    const fn cco_constants(freq: u32) -> (u8, u32, f32) {
        #[allow(clippy::excessive_precision)]
        match freq {
            48_000_000..63_885_600 => (0, 43_600_000, 1.00110340),
            63_885_600..84_948_700 => (1, 58_100_000, 1.00110200),
            84_948_700..113_009_380 => (2, 77_200_000, 1.00110000),
            113_009_380..150_339_200 => (3, 103_000_000, 1.00110000),
            150_339_200..=200_000_000 => (4, 132_000_000, 1.00117062),
            _ => panic!("CCO frequency out of range"),
        }
    }

    /// Returns a CCO configuration for a target frequency.
    pub const fn from_frequency(freq: u32) -> Self {
        let (range, base_freq, trim_step) = Self::cco_constants(freq);

        let target_multiplier = freq as f32 / base_freq as f32;
        let mut trim = 0;
        let mut current_multiplier = 1.;
        while current_multiplier < target_multiplier {
            trim += 1;
            current_multiplier *= trim_step;
        }

        CcoConfig { range, trim }
    }
}