383 lines
9.5 KiB
C
383 lines
9.5 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
//
|
|
// Copyright (c) 2021 Samuel Holland <samuel@sholland.org>
|
|
//
|
|
|
|
#include <linux/clk.h>
|
|
#include <linux/clk-provider.h>
|
|
#include <linux/io.h>
|
|
#include <linux/module.h>
|
|
#include <linux/of_device.h>
|
|
|
|
#include <linux/clk/sunxi-ng.h>
|
|
|
|
#include "ccu_common.h"
|
|
|
|
#include "ccu_div.h"
|
|
#include "ccu_gate.h"
|
|
#include "ccu_mux.h"
|
|
|
|
#include "ccu-sun6i-rtc.h"
|
|
|
|
#define IOSC_ACCURACY 300000000 /* 30% */
|
|
#define IOSC_RATE 16000000
|
|
|
|
#define LOSC_RATE 32768
|
|
#define LOSC_RATE_SHIFT 15
|
|
|
|
#define LOSC_CTRL_REG 0x0
|
|
#define LOSC_CTRL_KEY 0x16aa0000
|
|
|
|
#define IOSC_32K_CLK_DIV_REG 0x8
|
|
#define IOSC_32K_CLK_DIV GENMASK(4, 0)
|
|
#define IOSC_32K_PRE_DIV 32
|
|
|
|
#define IOSC_CLK_CALI_REG 0xc
|
|
#define IOSC_CLK_CALI_DIV_ONES 22
|
|
#define IOSC_CLK_CALI_EN BIT(1)
|
|
#define IOSC_CLK_CALI_SRC_SEL BIT(0)
|
|
|
|
#define LOSC_OUT_GATING_REG 0x60
|
|
|
|
#define DCXO_CTRL_REG 0x160
|
|
#define DCXO_CTRL_CLK16M_RC_EN BIT(0)
|
|
|
|
struct sun6i_rtc_match_data {
|
|
bool have_ext_osc32k : 1;
|
|
bool have_iosc_calibration : 1;
|
|
bool rtc_32k_single_parent : 1;
|
|
const struct clk_parent_data *osc32k_fanout_parents;
|
|
u8 osc32k_fanout_nparents;
|
|
};
|
|
|
|
static bool have_iosc_calibration;
|
|
|
|
static int ccu_iosc_enable(struct clk_hw *hw)
|
|
{
|
|
struct ccu_common *cm = hw_to_ccu_common(hw);
|
|
|
|
return ccu_gate_helper_enable(cm, DCXO_CTRL_CLK16M_RC_EN);
|
|
}
|
|
|
|
static void ccu_iosc_disable(struct clk_hw *hw)
|
|
{
|
|
struct ccu_common *cm = hw_to_ccu_common(hw);
|
|
|
|
return ccu_gate_helper_disable(cm, DCXO_CTRL_CLK16M_RC_EN);
|
|
}
|
|
|
|
static int ccu_iosc_is_enabled(struct clk_hw *hw)
|
|
{
|
|
struct ccu_common *cm = hw_to_ccu_common(hw);
|
|
|
|
return ccu_gate_helper_is_enabled(cm, DCXO_CTRL_CLK16M_RC_EN);
|
|
}
|
|
|
|
static unsigned long ccu_iosc_recalc_rate(struct clk_hw *hw,
|
|
unsigned long parent_rate)
|
|
{
|
|
struct ccu_common *cm = hw_to_ccu_common(hw);
|
|
|
|
if (have_iosc_calibration) {
|
|
u32 reg = readl(cm->base + IOSC_CLK_CALI_REG);
|
|
|
|
/*
|
|
* Recover the IOSC frequency by shifting the ones place of
|
|
* (fixed-point divider * 32768) into bit zero.
|
|
*/
|
|
if (reg & IOSC_CLK_CALI_EN)
|
|
return reg >> (IOSC_CLK_CALI_DIV_ONES - LOSC_RATE_SHIFT);
|
|
}
|
|
|
|
return IOSC_RATE;
|
|
}
|
|
|
|
static unsigned long ccu_iosc_recalc_accuracy(struct clk_hw *hw,
|
|
unsigned long parent_accuracy)
|
|
{
|
|
return IOSC_ACCURACY;
|
|
}
|
|
|
|
static const struct clk_ops ccu_iosc_ops = {
|
|
.enable = ccu_iosc_enable,
|
|
.disable = ccu_iosc_disable,
|
|
.is_enabled = ccu_iosc_is_enabled,
|
|
.recalc_rate = ccu_iosc_recalc_rate,
|
|
.recalc_accuracy = ccu_iosc_recalc_accuracy,
|
|
};
|
|
|
|
static struct ccu_common iosc_clk = {
|
|
.reg = DCXO_CTRL_REG,
|
|
.hw.init = CLK_HW_INIT_NO_PARENT("iosc", &ccu_iosc_ops,
|
|
CLK_GET_RATE_NOCACHE),
|
|
};
|
|
|
|
static int ccu_iosc_32k_prepare(struct clk_hw *hw)
|
|
{
|
|
struct ccu_common *cm = hw_to_ccu_common(hw);
|
|
u32 val;
|
|
|
|
if (!have_iosc_calibration)
|
|
return 0;
|
|
|
|
val = readl(cm->base + IOSC_CLK_CALI_REG);
|
|
writel(val | IOSC_CLK_CALI_EN | IOSC_CLK_CALI_SRC_SEL,
|
|
cm->base + IOSC_CLK_CALI_REG);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void ccu_iosc_32k_unprepare(struct clk_hw *hw)
|
|
{
|
|
struct ccu_common *cm = hw_to_ccu_common(hw);
|
|
u32 val;
|
|
|
|
if (!have_iosc_calibration)
|
|
return;
|
|
|
|
val = readl(cm->base + IOSC_CLK_CALI_REG);
|
|
writel(val & ~(IOSC_CLK_CALI_EN | IOSC_CLK_CALI_SRC_SEL),
|
|
cm->base + IOSC_CLK_CALI_REG);
|
|
}
|
|
|
|
static unsigned long ccu_iosc_32k_recalc_rate(struct clk_hw *hw,
|
|
unsigned long parent_rate)
|
|
{
|
|
struct ccu_common *cm = hw_to_ccu_common(hw);
|
|
u32 val;
|
|
|
|
if (have_iosc_calibration) {
|
|
val = readl(cm->base + IOSC_CLK_CALI_REG);
|
|
|
|
/* Assume the calibrated 32k clock is accurate. */
|
|
if (val & IOSC_CLK_CALI_SRC_SEL)
|
|
return LOSC_RATE;
|
|
}
|
|
|
|
val = readl(cm->base + IOSC_32K_CLK_DIV_REG) & IOSC_32K_CLK_DIV;
|
|
|
|
return parent_rate / IOSC_32K_PRE_DIV / (val + 1);
|
|
}
|
|
|
|
static unsigned long ccu_iosc_32k_recalc_accuracy(struct clk_hw *hw,
|
|
unsigned long parent_accuracy)
|
|
{
|
|
struct ccu_common *cm = hw_to_ccu_common(hw);
|
|
u32 val;
|
|
|
|
if (have_iosc_calibration) {
|
|
val = readl(cm->base + IOSC_CLK_CALI_REG);
|
|
|
|
/* Assume the calibrated 32k clock is accurate. */
|
|
if (val & IOSC_CLK_CALI_SRC_SEL)
|
|
return 0;
|
|
}
|
|
|
|
return parent_accuracy;
|
|
}
|
|
|
|
static const struct clk_ops ccu_iosc_32k_ops = {
|
|
.prepare = ccu_iosc_32k_prepare,
|
|
.unprepare = ccu_iosc_32k_unprepare,
|
|
.recalc_rate = ccu_iosc_32k_recalc_rate,
|
|
.recalc_accuracy = ccu_iosc_32k_recalc_accuracy,
|
|
};
|
|
|
|
static struct ccu_common iosc_32k_clk = {
|
|
.hw.init = CLK_HW_INIT_HW("iosc-32k", &iosc_clk.hw,
|
|
&ccu_iosc_32k_ops,
|
|
CLK_GET_RATE_NOCACHE),
|
|
};
|
|
|
|
static const struct clk_hw *ext_osc32k[] = { NULL }; /* updated during probe */
|
|
|
|
static SUNXI_CCU_GATE_HWS(ext_osc32k_gate_clk, "ext-osc32k-gate",
|
|
ext_osc32k, 0x0, BIT(4), 0);
|
|
|
|
static const struct clk_hw *osc32k_parents[] = {
|
|
&iosc_32k_clk.hw,
|
|
&ext_osc32k_gate_clk.common.hw
|
|
};
|
|
|
|
static struct clk_init_data osc32k_init_data = {
|
|
.name = "osc32k",
|
|
.ops = &ccu_mux_ops,
|
|
.parent_hws = osc32k_parents,
|
|
.num_parents = ARRAY_SIZE(osc32k_parents), /* updated during probe */
|
|
};
|
|
|
|
static struct ccu_mux osc32k_clk = {
|
|
.mux = _SUNXI_CCU_MUX(0, 1),
|
|
.common = {
|
|
.reg = LOSC_CTRL_REG,
|
|
.features = CCU_FEATURE_KEY_FIELD,
|
|
.hw.init = &osc32k_init_data,
|
|
},
|
|
};
|
|
|
|
/* This falls back to the global name for fwnodes without a named reference. */
|
|
static const struct clk_parent_data osc24M[] = {
|
|
{ .fw_name = "hosc", .name = "osc24M" }
|
|
};
|
|
|
|
static struct ccu_gate osc24M_32k_clk = {
|
|
.enable = BIT(16),
|
|
.common = {
|
|
.reg = LOSC_OUT_GATING_REG,
|
|
.prediv = 750,
|
|
.features = CCU_FEATURE_ALL_PREDIV,
|
|
.hw.init = CLK_HW_INIT_PARENTS_DATA("osc24M-32k", osc24M,
|
|
&ccu_gate_ops, 0),
|
|
},
|
|
};
|
|
|
|
static const struct clk_hw *rtc_32k_parents[] = {
|
|
&osc32k_clk.common.hw,
|
|
&osc24M_32k_clk.common.hw
|
|
};
|
|
|
|
static struct clk_init_data rtc_32k_init_data = {
|
|
.name = "rtc-32k",
|
|
.ops = &ccu_mux_ops,
|
|
.parent_hws = rtc_32k_parents,
|
|
.num_parents = ARRAY_SIZE(rtc_32k_parents), /* updated during probe */
|
|
.flags = CLK_IS_CRITICAL,
|
|
};
|
|
|
|
static struct ccu_mux rtc_32k_clk = {
|
|
.mux = _SUNXI_CCU_MUX(1, 1),
|
|
.common = {
|
|
.reg = LOSC_CTRL_REG,
|
|
.features = CCU_FEATURE_KEY_FIELD,
|
|
.hw.init = &rtc_32k_init_data,
|
|
},
|
|
};
|
|
|
|
static struct clk_init_data osc32k_fanout_init_data = {
|
|
.name = "osc32k-fanout",
|
|
.ops = &ccu_mux_ops,
|
|
/* parents are set during probe */
|
|
};
|
|
|
|
static struct ccu_mux osc32k_fanout_clk = {
|
|
.enable = BIT(0),
|
|
.mux = _SUNXI_CCU_MUX(1, 2),
|
|
.common = {
|
|
.reg = LOSC_OUT_GATING_REG,
|
|
.hw.init = &osc32k_fanout_init_data,
|
|
},
|
|
};
|
|
|
|
static struct ccu_common *sun6i_rtc_ccu_clks[] = {
|
|
&iosc_clk,
|
|
&iosc_32k_clk,
|
|
&ext_osc32k_gate_clk.common,
|
|
&osc32k_clk.common,
|
|
&osc24M_32k_clk.common,
|
|
&rtc_32k_clk.common,
|
|
&osc32k_fanout_clk.common,
|
|
};
|
|
|
|
static struct clk_hw_onecell_data sun6i_rtc_ccu_hw_clks = {
|
|
.num = CLK_NUMBER,
|
|
.hws = {
|
|
[CLK_OSC32K] = &osc32k_clk.common.hw,
|
|
[CLK_OSC32K_FANOUT] = &osc32k_fanout_clk.common.hw,
|
|
[CLK_IOSC] = &iosc_clk.hw,
|
|
[CLK_IOSC_32K] = &iosc_32k_clk.hw,
|
|
[CLK_EXT_OSC32K_GATE] = &ext_osc32k_gate_clk.common.hw,
|
|
[CLK_OSC24M_32K] = &osc24M_32k_clk.common.hw,
|
|
[CLK_RTC_32K] = &rtc_32k_clk.common.hw,
|
|
},
|
|
};
|
|
|
|
static const struct sunxi_ccu_desc sun6i_rtc_ccu_desc = {
|
|
.ccu_clks = sun6i_rtc_ccu_clks,
|
|
.num_ccu_clks = ARRAY_SIZE(sun6i_rtc_ccu_clks),
|
|
|
|
.hw_clks = &sun6i_rtc_ccu_hw_clks,
|
|
};
|
|
|
|
static const struct clk_parent_data sun50i_h616_osc32k_fanout_parents[] = {
|
|
{ .hw = &osc32k_clk.common.hw },
|
|
{ .fw_name = "pll-32k" },
|
|
{ .hw = &osc24M_32k_clk.common.hw }
|
|
};
|
|
|
|
static const struct clk_parent_data sun50i_r329_osc32k_fanout_parents[] = {
|
|
{ .hw = &osc32k_clk.common.hw },
|
|
{ .hw = &ext_osc32k_gate_clk.common.hw },
|
|
{ .hw = &osc24M_32k_clk.common.hw }
|
|
};
|
|
|
|
static const struct sun6i_rtc_match_data sun50i_h616_rtc_ccu_data = {
|
|
.have_iosc_calibration = true,
|
|
.rtc_32k_single_parent = true,
|
|
.osc32k_fanout_parents = sun50i_h616_osc32k_fanout_parents,
|
|
.osc32k_fanout_nparents = ARRAY_SIZE(sun50i_h616_osc32k_fanout_parents),
|
|
};
|
|
|
|
static const struct sun6i_rtc_match_data sun50i_r329_rtc_ccu_data = {
|
|
.have_ext_osc32k = true,
|
|
.osc32k_fanout_parents = sun50i_r329_osc32k_fanout_parents,
|
|
.osc32k_fanout_nparents = ARRAY_SIZE(sun50i_r329_osc32k_fanout_parents),
|
|
};
|
|
|
|
static const struct of_device_id sun6i_rtc_ccu_match[] = {
|
|
{
|
|
.compatible = "allwinner,sun50i-h616-rtc",
|
|
.data = &sun50i_h616_rtc_ccu_data,
|
|
},
|
|
{
|
|
.compatible = "allwinner,sun50i-r329-rtc",
|
|
.data = &sun50i_r329_rtc_ccu_data,
|
|
},
|
|
{},
|
|
};
|
|
|
|
int sun6i_rtc_ccu_probe(struct device *dev, void __iomem *reg)
|
|
{
|
|
const struct sun6i_rtc_match_data *data;
|
|
struct clk *ext_osc32k_clk = NULL;
|
|
const struct of_device_id *match;
|
|
|
|
/* This driver is only used for newer variants of the hardware. */
|
|
match = of_match_device(sun6i_rtc_ccu_match, dev);
|
|
if (!match)
|
|
return 0;
|
|
|
|
data = match->data;
|
|
have_iosc_calibration = data->have_iosc_calibration;
|
|
|
|
if (data->have_ext_osc32k) {
|
|
const char *fw_name;
|
|
|
|
/* ext-osc32k was the only input clock in the old binding. */
|
|
fw_name = of_property_read_bool(dev->of_node, "clock-names")
|
|
? "ext-osc32k" : NULL;
|
|
ext_osc32k_clk = devm_clk_get_optional(dev, fw_name);
|
|
if (IS_ERR(ext_osc32k_clk))
|
|
return PTR_ERR(ext_osc32k_clk);
|
|
}
|
|
|
|
if (ext_osc32k_clk) {
|
|
/* Link ext-osc32k-gate to its parent. */
|
|
*ext_osc32k = __clk_get_hw(ext_osc32k_clk);
|
|
} else {
|
|
/* ext-osc32k-gate is an orphan, so do not register it. */
|
|
sun6i_rtc_ccu_hw_clks.hws[CLK_EXT_OSC32K_GATE] = NULL;
|
|
osc32k_init_data.num_parents = 1;
|
|
}
|
|
|
|
if (data->rtc_32k_single_parent)
|
|
rtc_32k_init_data.num_parents = 1;
|
|
|
|
osc32k_fanout_init_data.parent_data = data->osc32k_fanout_parents;
|
|
osc32k_fanout_init_data.num_parents = data->osc32k_fanout_nparents;
|
|
|
|
return devm_sunxi_ccu_probe(dev, reg, &sun6i_rtc_ccu_desc);
|
|
}
|
|
|
|
MODULE_IMPORT_NS(SUNXI_CCU);
|
|
MODULE_LICENSE("GPL");
|