hisi-hal 0.7.0-alpha.2

Hardware Abstraction Layer for HiSilicon WS63 (RISC-V RV32IMFC_Zicsr)
//! TCXO 64-bit Free-Running Counter driver for WS63.
//!
//! The WS63 TCXO provides a 64-bit free-running counter that can be used
//! for precise timing measurements. The counter value must be latched
//! before reading by triggering a refresh.
//!
//! # Counter read procedure
//!
//! 1. Write `refresh = 1` to latch the current count value
//! 2. Wait for `valid` flag to be set
//! 3. Read `count0..count3` to get the full 64-bit value

use crate::peripherals::Tcxo;

// `tcxo_status` control/status bits (the PAC exposes this register only as raw
// bits). Named here so the driver and its tests pin the SAME values — a change
// to one is caught by the property tests rather than silently shipping.
/// Counter enable (bit 2).
pub(crate) const TCXO_EN_BIT: u32 = 1 << 2;
/// Counter clear/reset (bit 1).
pub(crate) const TCXO_CLEAR_BIT: u32 = 1 << 1;
/// Latch/refresh request (bit 0).
pub(crate) const TCXO_REFRESH_BIT: u32 = 1 << 0;
/// Latched-value-valid flag (bit 4), set by HW after a refresh.
pub(crate) const TCXO_VALID_BIT: u32 = 1 << 4;

/// TCXO 64-bit counter driver.
pub struct TcxoDriver<'d> {
    _tcxo: Tcxo<'d>,
}

impl<'d> TcxoDriver<'d> {
    /// Create a new TCXO driver.
    pub fn new(tcxo: Tcxo<'d>) -> Self {
        Self { _tcxo: tcxo }
    }

    fn regs(&self) -> &'static crate::soc::pac::tcxo::RegisterBlock {
        // SAFETY: PAC peripheral pointer is a static physical MMIO address, always valid
        unsafe { &*Tcxo::ptr() }
    }

    /// Enable the TCXO counter.
    pub fn enable(&mut self) {
        let status = self.regs().tcxo_status().read().bits();
        unsafe {
            self.regs().tcxo_status().write(|w| w.bits(status | TCXO_EN_BIT));
        }
    }

    /// Disable the TCXO counter.
    pub fn disable(&mut self) {
        let status = self.regs().tcxo_status().read().bits();
        unsafe {
            self.regs().tcxo_status().write(|w| w.bits(status & !TCXO_EN_BIT));
        }
    }

    /// Clear the TCXO counter (reset to zero).
    pub fn clear(&mut self) {
        let status = self.regs().tcxo_status().read().bits();
        unsafe {
            self.regs().tcxo_status().write(|w| w.bits(status | TCXO_CLEAR_BIT));
        }
    }

    /// Latch the current counter value by triggering a refresh.
    ///
    /// Returns `Ok(())` if the refresh completed, or `Err(())` on timeout.
    fn refresh(&self) -> Result<(), ()> {
        let status = self.regs().tcxo_status().read().bits();
        unsafe {
            self.regs().tcxo_status().write(|w| w.bits(status | TCXO_REFRESH_BIT));
        }
        // Wait for valid flag with timeout (~1ms at 32kHz TCXO clock)
        for _ in 0..100 {
            if self.regs().tcxo_status().read().bits() & TCXO_VALID_BIT != 0 {
                return Ok(());
            }
            core::hint::spin_loop();
        }
        Err(())
    }

    /// Returns true if the counter was successfully refreshed.
    /// Callers should check this after reading before relying on the counter value.
    pub fn refresh_ok(&self) -> bool {
        self.regs().tcxo_status().read().bits() & TCXO_VALID_BIT != 0
    }

    /// Read the full 64-bit counter value.
    ///
    /// Triggers a refresh to latch the current value, then reads all four
    /// 16-bit count registers. Returns `None` if the refresh timed out.
    pub fn read_counter(&self) -> Option<u64> {
        self.refresh().ok()?;
        let c0 = self.regs().tcxo_count0().read().bits() as u64;
        let c1 = self.regs().tcxo_count1().read().bits() as u64;
        let c2 = self.regs().tcxo_count2().read().bits() as u64;
        let c3 = self.regs().tcxo_count3().read().bits() as u64;
        Some((c3 << 48) | (c2 << 32) | (c1 << 16) | c0)
    }

    /// Read the lower 32 bits of the counter (faster, triggers refresh).
    ///
    /// Returns `None` if the refresh timed out.
    pub fn read_counter32(&self) -> Option<u32> {
        self.refresh().ok()?;
        let c0 = self.regs().tcxo_count0().read().bits();
        let c1 = self.regs().tcxo_count1().read().bits();
        Some((c1 << 16) | c0)
    }

    /// Check if the counter value is valid after a refresh.
    pub fn is_valid(&self) -> bool {
        self.regs().tcxo_status().read().bits() & TCXO_VALID_BIT != 0
    }
}

// ── Tests ──────────────────────────────────────────────────────

#[cfg(all(test, not(target_arch = "riscv32")))]
mod tests {
    #[test]
    fn test_tcxo_refresh_timeout_logic() {
        // refresh() loops max 100 times, checking valid bit (0x10)
        let max_retries = 100;
        let valid = false;
        let mut attempts = 0;
        let result = loop {
            if valid {
                break Ok(());
            }
            if attempts >= max_retries {
                break Err(());
            }
            attempts += 1;
        };
        assert_eq!(result, Err(()));
        assert_eq!(attempts, 100);
    }

    #[test]
    fn test_tcxo_refresh_success_on_valid() {
        let valid = true;
        let result = if valid { Ok(()) } else { Err(()) };
        assert!(result.is_ok());
    }

    // (The four tautological status-bit unit tests — `assert_eq!(0x04, 1<<2)` etc.
    // — were removed: they asserted literals against themselves and could not
    // catch a driver-bit change. The proptests below bind to the driver's own
    // `TCXO_*_BIT` consts and verify the real read-modify-write properties.)

    #[test]
    fn test_tcxo_counter_64bit_assembly() {
        // read_counter assembles c3:c2:c1:c0 as a 64-bit value
        let c0: u64 = 0x1234;
        let c1: u64 = 0x5678;
        let c2: u64 = 0x9ABC;
        let c3: u64 = 0xDEF0;
        let counter: u64 = (c3 << 48) | (c2 << 32) | (c1 << 16) | c0;
        assert_eq!(counter, 0xDEF0_9ABC_5678_1234);
    }

    #[test]
    fn test_tcxo_counter_32bit_assembly() {
        // read_counter32 assembles c1:c0 as a 32-bit value
        let c0: u32 = 0x1234;
        let c1: u32 = 0x5678;
        let counter: u32 = (c1 << 16) | c0;
        assert_eq!(counter, 0x5678_1234);
    }
}

// ── Property-based fuzz tests ──────────────────────────────────

#[cfg(all(test, not(target_arch = "riscv32")))]
mod proptests {
    use proptest::prelude::*;

    // Bind to the SAME consts the driver writes (not private copies), so a change
    // to a driver bit value actually fails these property tests.
    use super::{
        TCXO_CLEAR_BIT as CLEAR_BIT, TCXO_EN_BIT as ENABLE_BIT, TCXO_REFRESH_BIT as REFRESH_BIT,
        TCXO_VALID_BIT as VALID_BIT,
    };

    proptest! {
        /// Fuzz: read_counter() 64-bit assembly is a perfect round-trip of the
        /// four 16-bit count registers. Each lane occupies its own 16-bit slot
        /// with no overlap, regardless of input. Mirrors:
        ///   (c3 << 48) | (c2 << 32) | (c1 << 16) | c0
        #[test]
        fn read_counter64_roundtrip(
            c0 in any::<u16>(),
            c1 in any::<u16>(),
            c2 in any::<u16>(),
            c3 in any::<u16>(),
        ) {
            // Re-derive exactly as the driver does (each reg read widened to u64).
            let counter: u64 = ((c3 as u64) << 48)
                | ((c2 as u64) << 32)
                | ((c1 as u64) << 16)
                | (c0 as u64);

            // Each 16-bit lane must round-trip back unchanged.
            prop_assert_eq!((counter & 0xFFFF) as u16, c0);
            prop_assert_eq!(((counter >> 16) & 0xFFFF) as u16, c1);
            prop_assert_eq!(((counter >> 32) & 0xFFFF) as u16, c2);
            prop_assert_eq!(((counter >> 48) & 0xFFFF) as u16, c3);
        }

        /// Fuzz: the 64-bit assembly never sets a stray bit beyond the four
        /// documented 16-bit lanes (it occupies the full 64-bit width with no
        /// gaps and no overflow). Equivalent to a byte/word LE pack.
        #[test]
        fn read_counter64_no_stray_bits(
            c0 in any::<u16>(),
            c1 in any::<u16>(),
            c2 in any::<u16>(),
            c3 in any::<u16>(),
        ) {
            let counter: u64 = ((c3 as u64) << 48)
                | ((c2 as u64) << 32)
                | ((c1 as u64) << 16)
                | (c0 as u64);

            // Reconstruct from the four masked lanes; must equal the original.
            let rebuilt = (counter & 0x0000_0000_0000_FFFF)
                | (counter & 0x0000_0000_FFFF_0000)
                | (counter & 0x0000_FFFF_0000_0000)
                | (counter & 0xFFFF_0000_0000_0000);
            prop_assert_eq!(rebuilt, counter);

            // And the same as packing the equivalent little-endian byte array.
            let bytes = [
                c0.to_le_bytes()[0], c0.to_le_bytes()[1],
                c1.to_le_bytes()[0], c1.to_le_bytes()[1],
                c2.to_le_bytes()[0], c2.to_le_bytes()[1],
                c3.to_le_bytes()[0], c3.to_le_bytes()[1],
            ];
            prop_assert_eq!(u64::from_le_bytes(bytes), counter);
        }

        /// Fuzz: read_counter32() low-half assembly round-trips the two low
        /// 16-bit registers. Mirrors the driver, which keeps the regs as u32:
        ///   (c1 << 16) | c0
        #[test]
        fn read_counter32_roundtrip(c0 in any::<u16>(), c1 in any::<u16>()) {
            // Driver reads these as u32; widen exactly the same way.
            let c0u = c0 as u32;
            let c1u = c1 as u32;
            let counter: u32 = (c1u << 16) | c0u;

            prop_assert_eq!((counter & 0xFFFF) as u16, c0);
            prop_assert_eq!((counter >> 16) as u16, c1);

            // Consistent with a little-endian u16 pair pack.
            let bytes = [
                c0.to_le_bytes()[0], c0.to_le_bytes()[1],
                c1.to_le_bytes()[0], c1.to_le_bytes()[1],
            ];
            prop_assert_eq!(u32::from_le_bytes(bytes), counter);
        }

        /// Fuzz: the low 32 bits of the full 64-bit read agree with the fast
        /// 32-bit read for the same two low registers.
        #[test]
        fn read_counter32_matches_low_of_64(
            c0 in any::<u16>(),
            c1 in any::<u16>(),
            c2 in any::<u16>(),
            c3 in any::<u16>(),
        ) {
            let counter64: u64 = ((c3 as u64) << 48)
                | ((c2 as u64) << 32)
                | ((c1 as u64) << 16)
                | (c0 as u64);
            let counter32: u32 = ((c1 as u32) << 16) | (c0 as u32);
            prop_assert_eq!((counter64 & 0xFFFF_FFFF) as u32, counter32);
        }

        /// Fuzz: enable() sets bit 2 (status | ENABLE_BIT) without disturbing
        /// any other bit, and the result always reads back enabled. Mirrors:
        ///   write(status | 0x04)
        #[test]
        fn enable_sets_only_enable_bit(status in any::<u32>()) {
            let out = status | ENABLE_BIT;
            prop_assert_ne!(out & ENABLE_BIT, 0);
            // Only bit 2 may change; every other bit is preserved.
            prop_assert_eq!(out & !ENABLE_BIT, status & !ENABLE_BIT);
        }

        /// Fuzz: disable() clears exactly bit 2 (status & !ENABLE_BIT) and
        /// nothing else. Mirrors: write(status & !0x04)
        #[test]
        fn disable_clears_only_enable_bit(status in any::<u32>()) {
            let out = status & !ENABLE_BIT;
            prop_assert_eq!(out & ENABLE_BIT, 0);
            prop_assert_eq!(out & !ENABLE_BIT, status & !ENABLE_BIT);
        }

        /// Fuzz: enable() followed by disable() restores all bits except that
        /// the enable bit ends up cleared (idempotent toggle round-trip).
        #[test]
        fn enable_then_disable_roundtrip(status in any::<u32>()) {
            let after = (status | ENABLE_BIT) & !ENABLE_BIT;
            prop_assert_eq!(after, status & !ENABLE_BIT);
        }

        /// Fuzz: clear() sets exactly bit 1 (status | CLEAR_BIT) and refresh()
        /// sets exactly bit 0 (status | REFRESH_BIT); neither touches other bits.
        #[test]
        fn clear_and_refresh_set_only_their_bit(status in any::<u32>()) {
            let cleared = status | CLEAR_BIT;
            prop_assert_ne!(cleared & CLEAR_BIT, 0);
            prop_assert_eq!(cleared & !CLEAR_BIT, status & !CLEAR_BIT);

            let refreshed = status | REFRESH_BIT;
            prop_assert_ne!(refreshed & REFRESH_BIT, 0);
            prop_assert_eq!(refreshed & !REFRESH_BIT, status & !REFRESH_BIT);
        }

        /// Fuzz: the four control/status masks are single, distinct,
        /// non-overlapping bits (no aliasing between refresh/clear/enable/valid).
        #[test]
        fn status_masks_are_disjoint_single_bits(_ in any::<u8>()) {
            for m in [REFRESH_BIT, CLEAR_BIT, ENABLE_BIT, VALID_BIT] {
                prop_assert_eq!(m.count_ones(), 1);
            }
            let all = REFRESH_BIT | CLEAR_BIT | ENABLE_BIT | VALID_BIT;
            prop_assert_eq!(all.count_ones(), 4);
        }

        /// Fuzz: is_valid()/refresh_ok() decode the valid flag purely from
        /// bit 4 — exactly when (status & 0x10) != 0 — independent of any other
        /// bit pattern in the status word.
        #[test]
        fn valid_flag_decodes_only_bit4(status in any::<u32>()) {
            let valid = (status & VALID_BIT) != 0;
            // Toggling any non-valid bit must never change the decoded result.
            let mutated = status ^ (REFRESH_BIT | CLEAR_BIT | ENABLE_BIT);
            prop_assert_eq!((mutated & VALID_BIT) != 0, valid);
            // Forcing bit 4 on/off deterministically flips the decode.
            prop_assert!((status | VALID_BIT) & VALID_BIT != 0);
            prop_assert_eq!((status & !VALID_BIT) & VALID_BIT, 0);
        }
    }
}