Skip to main content

ws63_hal/
clock_init.rs

1//! Clock initialization for WS63.
2//!
3//! Based on the fbb_ws63 C SDK boot sequence analysis:
4//!
5//! ## What the boot ROM / bootloader does
6//!
7//! The flashboot bootloader (`flashboot_ws63/startup/main.c`) runs before
8//! the application and performs:
9//!
10//! 1. `boot_clock_adapt()` — detects TCXO (24/40MHz), configures UART/WDT tick rates
11//! 2. `switch_flash_clock_to_pll()` — sets CLDO_CRG_CLK_SEL bit 18 to switch
12//!    the **flash controller** clock source from TCXO to PLL. Does NOT switch
13//!    CPU, UART, or other peripheral clocks.
14//! 3. Initializes watchdog, eFuse, SPI flash, partition table
15//! 4. Loads and jumps to the application image
16//!
17//! The **CPU PLL** (240MHz) is configured by the boot ROM before the bootloader
18//! runs. The bootloader inherits this configuration.
19//!
20//! ## What the application must do
21//!
22//! The application-level `clock_init.c` in the LiteOS SDK performs:
23//!
24//! 1. `switch_clock()` — switches peripheral clocks from TCXO to PLL:
25//!    - UART0/1/2: CLDO_CRG_CLK_SEL bits 1,2,3
26//!    - WiFi MAC: bit 20, WiFi PHY: bit 19
27//!    - RF_CTL: bit 0
28//!    - SPI: bit 6 (spi_porting.c)
29//! 2. `set_uart_tcxo_clock_period()` — configures UART baud base, timer tick,
30//!    watchdog period, I2C clock based on detected TCXO frequency
31//!
32//! For a bare-metal Rust application (no LiteOS), we provide:
33//! - `probe_clocks()` — non-invasive: detect TCXO and PLL status
34//! - `init_clocks()` — full init: switch flash to PLL + switch UART/SPI to PLL
35//!
36//! # CLDO_CRG_CLK_SEL bit map (from fbb_ws63 clock_init.c)
37//!
38//! | Bit | Peripheral | Description |
39//! |-----|-----------|-------------|
40//! | 0 | RF_CTL | RF control clock → PLL |
41//! | 1 | UART0 | UART0 clock → PLL |
42//! | 2 | UART1 | UART1 clock → PLL |
43//! | 3 | UART2 | UART2 clock → PLL |
44//! | 6 | SPI | SPI clock → PLL |
45//! | 18 | FLASH | Flash/SFC controller → PLL |
46//! | 19 | WiFi PHY | WiFi PHY clock → PLL |
47//! | 20 | WiFi MAC | WiFi MAC clock → PLL |
48//!
49//! # Register map (from fbb_ws63)
50//!
51//! | Register | Address | Description |
52//! |----------|---------|-------------|
53//! | HW_CTL | 0x4000_0014 | TCXO frequency detect (bit[0]: 0=24MHz, 1=40MHz) |
54//! | REG_EXCEP_RO_RG | 0x4000_319C | PLL lock status (bit 12) |
55//! | CMU_NEW_CFG1 | 0x4000_34A4 | Flash clock control |
56//! | CLDO_CRG_CLK_SEL | 0x4400_1134 | Clock source select |
57//! | CLDO_SUB_CRG_CKEN_CTL1 | 0x4400_1104 | UART clock gate control |
58//!
59//! # Clock tree (from ws63-guide ch2_system.md)
60//!
61//! | Domain | Frequency | Clock Source |
62//! |--------|-----------|-------------|
63//! | CPU | 240 MHz | PLL |
64//! | CPU Bus | 240 MHz | PLL |
65//! | GPIO | 120 MHz | PLL / 2 |
66//! | UART | 160 MHz | PLL-derived |
67//! | SPI | 160 MHz | PLL-derived |
68//! | I2C | 80 MHz | PLL-derived |
69//! | QSPI | 64 MHz | PLL-derived |
70//! | Timer | 32 kHz | Crystal |
71//! | WDT | 32 kHz | Crystal |
72//! | RTC | 32 kHz | Crystal |
73//! | Crystal | 40/24 MHz | TCXO |
74
75use crate::peripherals::{CldoCrg, SysCtl0};
76use crate::soc::ws63::SYSTEM_CLOCK_HZ;
77
78// ── Register addresses (from fbb_ws63 soc_porting.c / pm_porting.c) ──
79
80/// Hardware control register — TCXO frequency detect.
81const HW_CTL: *mut u32 = 0x4000_0014 as *mut u32;
82/// Exception RO register — PLL lock status (bit 12).
83const REG_EXCEP_RO_RG: *mut u32 = 0x4000_319C as *mut u32;
84/// CMU PLL signal register — PLL power-down control (bit 15).
85#[allow(dead_code)]
86const REG_CMU_FNPLL_SIG: *mut u32 = 0x4000_342C as *mut u32;
87/// Flash clock control register.
88const CMU_NEW_CFG1: *mut u32 = 0x4000_34A4 as *mut u32;
89
90// ── TCXO frequency ────────────────────────────────────────────────
91
92/// TCXO crystal frequency in Hz.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum TcxoFreq {
95    /// 24 MHz crystal.
96    MHz24 = 24_000_000,
97    /// 40 MHz crystal.
98    MHz40 = 40_000_000,
99}
100
101impl TcxoFreq {
102    /// Detect the TCXO frequency by reading the HW_CTL register.
103    pub fn detect() -> Self {
104        // SAFETY: HW_CTL (0x4000_0014) is a valid physical MMIO register per fbb_ws63
105        let hw_ctl = unsafe { HW_CTL.read_volatile() };
106        if hw_ctl & 0x01 == 0 { TcxoFreq::MHz24 } else { TcxoFreq::MHz40 }
107    }
108
109    /// Return the frequency in Hz.
110    pub const fn hz(&self) -> u32 {
111        *self as u32
112    }
113}
114
115// ── PLL status ────────────────────────────────────────────────────
116
117/// Result of PLL lock check.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum PllStatus {
120    /// PLL is locked and stable.
121    Locked,
122    /// PLL is unlocked — system runs from TCXO.
123    Unlocked,
124}
125
126/// Check if the PLL is locked by reading REG_EXCEP_RO_RG bit 12.
127fn pll_is_locked() -> bool {
128    // fbb_ws63: cmu_is_fnpll_locked() reads REG_EXCEP_RO_RG bit 12
129    (unsafe { REG_EXCEP_RO_RG.read_volatile() } >> 12) & 1 == 1
130}
131
132/// Poll the PLL lock status with retries.
133///
134/// * `retry_count` — Number of polling attempts (default 30).
135/// * `retry_delay_us` — Delay between attempts in µs (default 1000 = 1ms).
136fn wait_pll_lock(retry_count: u32, retry_delay_us: u32) -> PllStatus {
137    for _ in 0..retry_count {
138        if pll_is_locked() {
139            return PllStatus::Locked;
140        }
141        // Busy-wait delay (approximate at TCXO speed before PLL is on)
142        let cycles = TcxoFreq::detect().hz() as u64 * retry_delay_us as u64 / 1_000_000;
143        for _ in 0..cycles / 3 {
144            core::hint::spin_loop();
145        }
146    }
147    PllStatus::Unlocked
148}
149
150// ── Clock initialization ──────────────────────────────────────────
151
152/// System clock configuration after initialization.
153#[derive(Debug, Clone, Copy)]
154pub struct SystemClocks {
155    /// CPU clock (PLL output, 240 MHz).
156    pub cpu_clk: u32,
157    /// Peripheral bus clock (PCLK, 240 MHz default).
158    pub pclk: u32,
159    /// TCXO crystal frequency.
160    pub tcxo_freq: TcxoFreq,
161    /// Whether the PLL is locked.
162    pub pll_locked: bool,
163}
164
165impl SystemClocks {
166    /// Default clocks (assumes boot ROM has configured PLL).
167    pub const fn assumed() -> Self {
168        Self { cpu_clk: SYSTEM_CLOCK_HZ, pclk: SYSTEM_CLOCK_HZ, tcxo_freq: TcxoFreq::MHz40, pll_locked: true }
169    }
170}
171
172/// Initialize the system clock tree.
173///
174/// Performs the full clock initialization sequence from fbb_ws63:
175///
176/// 1. Detect TCXO frequency (24 or 40 MHz) via HW_CTL register
177/// 2. Switch flash clock to PLL (bootloader already did this, but we re-apply)
178///    — CMU_NEW_CFG1 sequence + CLDO_CRG_CLK_SEL bit 18
179/// 3. Switch UART0/1/2 clocks from TCXO to PLL
180///    — Disable UART clock gates → set CLDO_CRG_CLK_SEL bits 1,2,3 → re-enable gates
181/// 4. Switch SPI clock to PLL — set CLDO_CRG_CLK_SEL bit 6
182/// 5. Verify PLL lock via REG_EXCEP_RO_RG bit 12
183///
184/// # Arguments
185///
186/// * `_sys_ctl0` — SYS_CTL0 peripheral (reserved for future PLL config).
187/// * `_cldo_crg` — CLDO_CRG peripheral (reserved for future divider config).
188///
189/// # Returns
190///
191/// The resolved [`SystemClocks`] configuration.
192///
193/// # Safety
194///
195/// This writes to raw MMIO registers. Should only be called once at boot,
196/// before any peripheral drivers are initialized.
197pub fn init_clocks(_sys_ctl0: &SysCtl0<'_>, _cldo_crg: &CldoCrg<'_>) -> SystemClocks {
198    let tcxo_freq = TcxoFreq::detect();
199    let clk_sel_ptr = 0x4400_1134 as *mut u32;
200    let clk_gate_ptr = 0x4400_1104 as *mut u32; // CLDO_SUB_CRG_CKEN_CTL1
201
202    // ── Step 1: Switch flash clock to PLL ────────────────────
203    // (fbb_ws63: switch_flash_clock_to_pll in soc_porting.c)
204    // SAFETY: CMU_NEW_CFG1 (0x4000_34A4), CLDO_CRG_CLK_SEL (0x4400_1134)
205    // are valid physical MMIO addresses per fbb_ws63 register map.
206    unsafe { CMU_NEW_CFG1.write_volatile(0x1) }; // CPU_DIV_FLASH_RSTN_SYNC
207    for _ in 0..tcxo_freq.hz() / 1_000_000 / 3 {
208        core::hint::spin_loop(); // delay 1µs
209    }
210    unsafe { CMU_NEW_CFG1.write_volatile(0x3) }; // CPU_DIV_FLASH_RSTN
211    unsafe {
212        let val = clk_sel_ptr.read_volatile();
213        clk_sel_ptr.write_volatile(val | (1 << 18)); // bit 18: flash → PLL
214    }
215
216    // ── Step 2: Switch UART clocks to PLL ───────────────────
217    // (fbb_ws63: switch_clock in clock_init.c)
218    unsafe {
219        // Disable UART clock gates (bits 18,19,20 in CLDO_SUB_CRG_CKEN_CTL1)
220        let mut gate = clk_gate_ptr.read_volatile();
221        gate &= !((1 << 18) | (1 << 19) | (1 << 20));
222        clk_gate_ptr.write_volatile(gate);
223
224        // Set CLDO_CRG_CLK_SEL bits 1,2,3: UART0/1/2 → PLL
225        let mut sel = clk_sel_ptr.read_volatile();
226        sel |= (1 << 1) | (1 << 2) | (1 << 3);
227        clk_sel_ptr.write_volatile(sel);
228
229        // Re-enable UART clock gates
230        gate |= (1 << 18) | (1 << 19) | (1 << 20);
231        clk_gate_ptr.write_volatile(gate);
232    }
233
234    // ── Step 3: Switch SPI clock to PLL ─────────────────────
235    // (fbb_ws63: spi_porting.c sets CLDO_CRG_CLK_SEL bit 6)
236    unsafe {
237        let val = clk_sel_ptr.read_volatile();
238        clk_sel_ptr.write_volatile(val | (1 << 6)); // bit 6: SPI → PLL
239    }
240
241    // ── Step 4: Verify PLL lock ─────────────────────────────
242    let pll_locked = match wait_pll_lock(30, 1000) {
243        PllStatus::Locked => true,
244        PllStatus::Unlocked => false,
245    };
246
247    SystemClocks {
248        cpu_clk: if pll_locked { SYSTEM_CLOCK_HZ } else { tcxo_freq.hz() },
249        pclk: if pll_locked { SYSTEM_CLOCK_HZ } else { tcxo_freq.hz() },
250        tcxo_freq,
251        pll_locked,
252    }
253}
254
255/// Simple version: detect clocks without modifying them.
256///
257/// Reads TCXO frequency and checks PLL status. Does NOT reconfigure
258/// the clock tree. Safe to call when boot ROM has already configured
259/// the PLL (which is the default on WS63).
260pub fn probe_clocks() -> SystemClocks {
261    let tcxo_freq = TcxoFreq::detect();
262    let pll_locked = pll_is_locked();
263
264    SystemClocks {
265        cpu_clk: if pll_locked { SYSTEM_CLOCK_HZ } else { tcxo_freq.hz() },
266        pclk: if pll_locked { SYSTEM_CLOCK_HZ } else { tcxo_freq.hz() },
267        tcxo_freq,
268        pll_locked,
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_tcxo_freq_values() {
278        assert_eq!(TcxoFreq::MHz24.hz(), 24_000_000);
279        assert_eq!(TcxoFreq::MHz40.hz(), 40_000_000);
280    }
281
282    #[test]
283    fn test_default_clocks() {
284        let c = SystemClocks::assumed();
285        assert_eq!(c.cpu_clk, 240_000_000);
286        assert_eq!(c.pclk, 240_000_000);
287        assert!(c.pll_locked);
288    }
289}