blob: ddcc7893c7a79bbcfc2f3fd7b707e1cb7990081b [file]
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0
//! Timing configuration for I2C
//!
//! The AST1060 I2C controller uses a hierarchical clock system:
//! 1. HPLL (1 `GHz`) → APB clock (typically 50 `MHz`)
//! 2. APB clock → 4 base clocks (configured in I2CG10)
//! 3. Base clock → SCL frequency via per-controller divider
//!
//! This module calculates the optimal divider chain to achieve
//! the requested I2C bus speed while meeting timing specifications.
//!
//! # Design: Dependency Injection
//!
//! Clock frequencies are provided via `ClockConfig` in `I2cConfig`, rather than
//! being read directly from SCU/I2C global registers. This provides:
//! - **Decoupling**: I2C module has no hidden dependencies on SCU
//! - **Testability**: Unit tests can inject known clock values
//! - **Portability**: Different board configurations are easily supported
//! - **Clear ownership**: System init owns clock detection, I2C just uses values
//!
//! # I2C Timing Requirements
//!
//! | Mode | Speed | tLOW (min) | tHIGH (min) | tBuf (min) |
//! |------------|----------|------------|-------------|------------|
//! | Standard | 100 kHz | 4.7 µs | 4.0 µs | 4.7 µs |
//! | Fast | 400 kHz | 1.3 µs | 0.6 µs | 1.3 µs |
//! | Fast-plus | 1 `MHz` | 0.5 µs | 0.26 µs | 0.5 µs |
//!
//! # Clock Derivation
//!
//! The I2C global register I2CG10 configures four base clock dividers:
//! ```text
//! I2CG10[7:0] = base_clk1 divisor → Fast-plus (1 MHz) source
//! I2CG10[15:8] = base_clk2 divisor → Fast (400 kHz) source
//! I2CG10[23:16] = base_clk3 divisor → Standard (100 kHz) source
//! I2CG10[31:24] = base_clk4 divisor → Recovery timeout source
//! ```
//!
//! Default I2CG10 value: `0x6222_0803`
//! With 50 `MHz` APB clock:
//! - `base_clk1` (0x03): 50M / ((3+2)/2) = 20 `MHz` → 1 `MHz` with div=20
//! - `base_clk2` (0x08): 50M / ((8+2)/2) = 10 `MHz` → 400 kHz with div=25
//! - `base_clk3` (0x22): 50M / ((34+2)/2) = 2.77 `MHz` → 100 kHz with div=28
use super::error::I2cError;
use super::types::{ClockConfig, I2cConfig, I2cSpeed};
use ast1060_pac::i2c::RegisterBlock;
use core::cmp::min;
/// Maximum divider ratio per base clock
const MAX_DIVIDER_RATIO: u32 = 32;
/// I2C speed frequencies in Hz
const SPEED_STANDARD: u32 = 100_000;
const SPEED_FAST: u32 = 400_000;
const SPEED_FAST_PLUS: u32 = 1_000_000;
/// Configure I2C timing based on speed and injected clock configuration
///
/// Uses the `ClockConfig` from `I2cConfig` to calculate proper SCL timing
/// that meets I2C specifications. No direct hardware register access for
/// clock detection - all clock values come from the injected config.
///
/// # Arguments
///
/// * `regs` - Reference to the I2C controller register block
/// * `config` - I2C configuration containing speed, timeout, and clock settings
///
/// # Returns
///
/// * `Ok(())` on successful configuration
/// * `Err(I2cError::Invalid)` if timing cannot be achieved
pub fn configure_timing(regs: &RegisterBlock, config: &I2cConfig) -> Result<(), I2cError> {
let clocks = &config.clock_config;
// Step 1: Get target speed
let target_speed = match config.speed {
I2cSpeed::Standard => SPEED_STANDARD,
I2cSpeed::Fast => SPEED_FAST,
I2cSpeed::FastPlus => SPEED_FAST_PLUS,
};
// Step 2: Find best base clock and divider using injected clock config
let (div, divider_ratio) = calculate_divider(clocks, target_speed)?;
// Step 3: Calculate SCL high/low times
// I2C spec: tLOW should be slightly longer than tHIGH
// Default ratio: 9/16 low, 7/16 high (asymmetric for spec compliance)
let scl_low = calculate_scl_low(divider_ratio);
let scl_high = calculate_scl_high(divider_ratio, scl_low);
let scl_high_min = scl_high.saturating_sub(1);
// Step 4: Build timeout configuration if enabled
let (timeout_divisor, timeout_timer) = if config.smbus_timeout {
// SMBus timeout: 25-35ms per specification
// With base_clk_divisor=2, timer=8 gives approximately 35ms timeout
(2u8, 8u8)
} else {
(0u8, 0u8)
};
// Step 5: Write AC timing register with all fields in a single write
// This avoids corrupting fields with multiple separate writes
regs.i2cc04().write(|w| unsafe {
w.base_clk_divisor_tbase_clk()
.bits((div & 0xf) as u8)
.cycles_of_master_sclclklow_pulse_width_tcklow()
.bits(scl_low)
.cycles_of_master_sclclkhigh_pulse_width_tckhigh()
.bits(scl_high)
.cycles_of_master_sclclkhigh_minimum_pulse_width_tckhigh_min()
.bits(scl_high_min)
.timeout_base_clk_divisor_tout_base_clk()
.bits(timeout_divisor)
.timeout_timer()
.bits(timeout_timer)
});
Ok(())
}
/// Calculate optimal base clock selection and divider ratio
///
/// Tries each base clock in order (APB, clk1, clk2, clk3, clk4) and finds
/// the first one that can achieve the target speed with a divider ≤ 32.
///
/// # Arguments
///
/// * `clocks` - Injected clock configuration
/// * `target_speed` - Desired I2C bus speed in Hz
///
/// # Returns
///
/// * `(div, ratio)` - Base clock selector (0-4+) and divider ratio
fn calculate_divider(clocks: &ClockConfig, target_speed: u32) -> Result<(u32, u32), I2cError> {
// Avoid division by zero
if target_speed == 0 {
return Err(I2cError::Invalid);
}
// Try each base clock in order, finding first that works with divider <= 32
let candidates = [
(0u32, clocks.apb_clock_hz),
(1u32, clocks.base_clk1_hz),
(2u32, clocks.base_clk2_hz),
(3u32, clocks.base_clk3_hz),
];
for (div, base_clk) in candidates {
if base_clk == 0 {
continue; // Skip unconfigured clocks
}
if base_clk / target_speed <= MAX_DIVIDER_RATIO {
let mut ratio = base_clk / target_speed;
// Round up if we'd exceed target speed
if ratio > 0 && base_clk / ratio > target_speed {
ratio = ratio.saturating_add(1);
}
// Ensure ratio is at least 1
if ratio == 0 {
ratio = 1;
}
return Ok((div, ratio));
}
}
// Fall back to base_clk4 with extended divider for very slow speeds
if clocks.base_clk4_hz == 0 {
return Err(I2cError::Invalid);
}
let mut div = 4u32;
let mut ratio = clocks.base_clk4_hz / target_speed;
let mut inc = 0u32;
// Shift ratio down until it fits in MAX_DIVIDER_RATIO
while ratio.saturating_add(inc) > MAX_DIVIDER_RATIO {
inc |= ratio & 1; // Track if we need to round up
ratio >>= 1;
div = div.saturating_add(1);
}
ratio = ratio.saturating_add(inc);
// Round up if needed
if ratio > 0 && clocks.base_clk4_hz / ratio > target_speed {
ratio = ratio.saturating_add(1);
}
// Clamp to valid range
ratio = min(ratio, MAX_DIVIDER_RATIO);
if ratio == 0 {
ratio = 1;
}
div &= 0xf;
Ok((div, ratio))
}
/// Calculate SCL low time cycles
///
/// I2C specification requires tLOW to be longer than tHIGH.
/// We use 9/16 of the period for low time (56.25%).
fn calculate_scl_low(divider_ratio: u32) -> u8 {
// scl_low = (ratio * 9/16) - 1, clamped to 4 bits
let scl_low = (divider_ratio * 9 / 16).saturating_sub(1);
#[allow(clippy::cast_possible_truncation)]
let result = min(scl_low, 0xf) as u8;
result
}
/// Calculate SCL high time cycles
///
/// High time is the remainder after low time and 2 cycles for edges.
fn calculate_scl_high(divider_ratio: u32, scl_low: u8) -> u8 {
// scl_high = ratio - scl_low - 2 (2 cycles for rise/fall edges)
let scl_high = divider_ratio
.saturating_sub(u32::from(scl_low))
.saturating_sub(2);
#[allow(clippy::cast_possible_truncation)]
let result = min(scl_high, 0xf) as u8;
result
}