forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
288 lines
7.0 KiB
288 lines
7.0 KiB
// SPDX-License-Identifier: GPL-2.0 |
|
/* |
|
* Copyright 2019 NXP |
|
* |
|
* Clock driver for LS1028A Display output interfaces(LCD, DPHY). |
|
*/ |
|
|
|
#include <linux/clk-provider.h> |
|
#include <linux/device.h> |
|
#include <linux/module.h> |
|
#include <linux/err.h> |
|
#include <linux/io.h> |
|
#include <linux/iopoll.h> |
|
#include <linux/of.h> |
|
#include <linux/of_address.h> |
|
#include <linux/of_device.h> |
|
#include <linux/platform_device.h> |
|
#include <linux/slab.h> |
|
#include <linux/bitfield.h> |
|
|
|
/* PLLDIG register offsets and bit masks */ |
|
#define PLLDIG_REG_PLLSR 0x24 |
|
#define PLLDIG_LOCK_MASK BIT(2) |
|
#define PLLDIG_REG_PLLDV 0x28 |
|
#define PLLDIG_MFD_MASK GENMASK(7, 0) |
|
#define PLLDIG_RFDPHI1_MASK GENMASK(30, 25) |
|
#define PLLDIG_REG_PLLFM 0x2c |
|
#define PLLDIG_SSCGBYP_ENABLE BIT(30) |
|
#define PLLDIG_REG_PLLFD 0x30 |
|
#define PLLDIG_FDEN BIT(30) |
|
#define PLLDIG_FRAC_MASK GENMASK(15, 0) |
|
#define PLLDIG_REG_PLLCAL1 0x38 |
|
#define PLLDIG_REG_PLLCAL2 0x3c |
|
|
|
/* Range of the VCO frequencies, in Hz */ |
|
#define PLLDIG_MIN_VCO_FREQ 650000000 |
|
#define PLLDIG_MAX_VCO_FREQ 1300000000 |
|
|
|
/* Range of the output frequencies, in Hz */ |
|
#define PHI1_MIN_FREQ 27000000UL |
|
#define PHI1_MAX_FREQ 600000000UL |
|
|
|
/* Maximum value of the reduced frequency divider */ |
|
#define MAX_RFDPHI1 63UL |
|
|
|
/* Best value of multiplication factor divider */ |
|
#define PLLDIG_DEFAULT_MFD 44 |
|
|
|
/* |
|
* Denominator part of the fractional part of the |
|
* loop multiplication factor. |
|
*/ |
|
#define MFDEN 20480 |
|
|
|
static const struct clk_parent_data parent_data[] = { |
|
{ .index = 0 }, |
|
}; |
|
|
|
struct clk_plldig { |
|
struct clk_hw hw; |
|
void __iomem *regs; |
|
unsigned int vco_freq; |
|
}; |
|
|
|
#define to_clk_plldig(_hw) container_of(_hw, struct clk_plldig, hw) |
|
|
|
static int plldig_enable(struct clk_hw *hw) |
|
{ |
|
struct clk_plldig *data = to_clk_plldig(hw); |
|
u32 val; |
|
|
|
val = readl(data->regs + PLLDIG_REG_PLLFM); |
|
/* |
|
* Use Bypass mode with PLL off by default, the frequency overshoot |
|
* detector output was disable. SSCG Bypass mode should be enable. |
|
*/ |
|
val |= PLLDIG_SSCGBYP_ENABLE; |
|
writel(val, data->regs + PLLDIG_REG_PLLFM); |
|
|
|
return 0; |
|
} |
|
|
|
static void plldig_disable(struct clk_hw *hw) |
|
{ |
|
struct clk_plldig *data = to_clk_plldig(hw); |
|
u32 val; |
|
|
|
val = readl(data->regs + PLLDIG_REG_PLLFM); |
|
|
|
val &= ~PLLDIG_SSCGBYP_ENABLE; |
|
val |= FIELD_PREP(PLLDIG_SSCGBYP_ENABLE, 0x0); |
|
|
|
writel(val, data->regs + PLLDIG_REG_PLLFM); |
|
} |
|
|
|
static int plldig_is_enabled(struct clk_hw *hw) |
|
{ |
|
struct clk_plldig *data = to_clk_plldig(hw); |
|
|
|
return readl(data->regs + PLLDIG_REG_PLLFM) & |
|
PLLDIG_SSCGBYP_ENABLE; |
|
} |
|
|
|
static unsigned long plldig_recalc_rate(struct clk_hw *hw, |
|
unsigned long parent_rate) |
|
{ |
|
struct clk_plldig *data = to_clk_plldig(hw); |
|
u32 val, rfdphi1; |
|
|
|
val = readl(data->regs + PLLDIG_REG_PLLDV); |
|
|
|
/* Check if PLL is bypassed */ |
|
if (val & PLLDIG_SSCGBYP_ENABLE) |
|
return parent_rate; |
|
|
|
rfdphi1 = FIELD_GET(PLLDIG_RFDPHI1_MASK, val); |
|
|
|
/* |
|
* If RFDPHI1 has a value of 1 the VCO frequency is also divided by |
|
* one. |
|
*/ |
|
if (!rfdphi1) |
|
rfdphi1 = 1; |
|
|
|
return DIV_ROUND_UP(data->vco_freq, rfdphi1); |
|
} |
|
|
|
static unsigned long plldig_calc_target_div(unsigned long vco_freq, |
|
unsigned long target_rate) |
|
{ |
|
unsigned long div; |
|
|
|
div = DIV_ROUND_CLOSEST(vco_freq, target_rate); |
|
div = clamp(div, 1UL, MAX_RFDPHI1); |
|
|
|
return div; |
|
} |
|
|
|
static int plldig_determine_rate(struct clk_hw *hw, |
|
struct clk_rate_request *req) |
|
{ |
|
struct clk_plldig *data = to_clk_plldig(hw); |
|
unsigned int div; |
|
|
|
req->rate = clamp(req->rate, PHI1_MIN_FREQ, PHI1_MAX_FREQ); |
|
div = plldig_calc_target_div(data->vco_freq, req->rate); |
|
req->rate = DIV_ROUND_UP(data->vco_freq, div); |
|
|
|
return 0; |
|
} |
|
|
|
static int plldig_set_rate(struct clk_hw *hw, unsigned long rate, |
|
unsigned long parent_rate) |
|
{ |
|
struct clk_plldig *data = to_clk_plldig(hw); |
|
unsigned int val, cond; |
|
unsigned int rfdphi1; |
|
|
|
rate = clamp(rate, PHI1_MIN_FREQ, PHI1_MAX_FREQ); |
|
rfdphi1 = plldig_calc_target_div(data->vco_freq, rate); |
|
|
|
/* update the divider value */ |
|
val = readl(data->regs + PLLDIG_REG_PLLDV); |
|
val &= ~PLLDIG_RFDPHI1_MASK; |
|
val |= FIELD_PREP(PLLDIG_RFDPHI1_MASK, rfdphi1); |
|
writel(val, data->regs + PLLDIG_REG_PLLDV); |
|
|
|
/* waiting for old lock state to clear */ |
|
udelay(200); |
|
|
|
/* Wait until PLL is locked or timeout */ |
|
return readl_poll_timeout_atomic(data->regs + PLLDIG_REG_PLLSR, cond, |
|
cond & PLLDIG_LOCK_MASK, 0, |
|
USEC_PER_MSEC); |
|
} |
|
|
|
static const struct clk_ops plldig_clk_ops = { |
|
.enable = plldig_enable, |
|
.disable = plldig_disable, |
|
.is_enabled = plldig_is_enabled, |
|
.recalc_rate = plldig_recalc_rate, |
|
.determine_rate = plldig_determine_rate, |
|
.set_rate = plldig_set_rate, |
|
}; |
|
|
|
static int plldig_init(struct clk_hw *hw) |
|
{ |
|
struct clk_plldig *data = to_clk_plldig(hw); |
|
struct clk_hw *parent = clk_hw_get_parent(hw); |
|
unsigned long parent_rate; |
|
unsigned long val; |
|
unsigned long long lltmp; |
|
unsigned int mfd, fracdiv = 0; |
|
|
|
if (!parent) |
|
return -EINVAL; |
|
|
|
parent_rate = clk_hw_get_rate(parent); |
|
|
|
if (data->vco_freq) { |
|
mfd = data->vco_freq / parent_rate; |
|
lltmp = data->vco_freq % parent_rate; |
|
lltmp *= MFDEN; |
|
do_div(lltmp, parent_rate); |
|
fracdiv = lltmp; |
|
} else { |
|
mfd = PLLDIG_DEFAULT_MFD; |
|
data->vco_freq = parent_rate * mfd; |
|
} |
|
|
|
val = FIELD_PREP(PLLDIG_MFD_MASK, mfd); |
|
writel(val, data->regs + PLLDIG_REG_PLLDV); |
|
|
|
/* Enable fractional divider */ |
|
if (fracdiv) { |
|
val = FIELD_PREP(PLLDIG_FRAC_MASK, fracdiv); |
|
val |= PLLDIG_FDEN; |
|
writel(val, data->regs + PLLDIG_REG_PLLFD); |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static int plldig_clk_probe(struct platform_device *pdev) |
|
{ |
|
struct clk_plldig *data; |
|
struct device *dev = &pdev->dev; |
|
int ret; |
|
|
|
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); |
|
if (!data) |
|
return -ENOMEM; |
|
|
|
data->regs = devm_platform_ioremap_resource(pdev, 0); |
|
if (IS_ERR(data->regs)) |
|
return PTR_ERR(data->regs); |
|
|
|
data->hw.init = CLK_HW_INIT_PARENTS_DATA("dpclk", |
|
parent_data, |
|
&plldig_clk_ops, |
|
0); |
|
|
|
ret = devm_clk_hw_register(dev, &data->hw); |
|
if (ret) { |
|
dev_err(dev, "failed to register %s clock\n", |
|
dev->of_node->name); |
|
return ret; |
|
} |
|
|
|
ret = devm_of_clk_add_hw_provider(dev, of_clk_hw_simple_get, |
|
&data->hw); |
|
if (ret) { |
|
dev_err(dev, "unable to add clk provider\n"); |
|
return ret; |
|
} |
|
|
|
/* |
|
* The frequency of the VCO cannot be changed during runtime. |
|
* Therefore, let the user specify a desired frequency. |
|
*/ |
|
if (!of_property_read_u32(dev->of_node, "fsl,vco-hz", |
|
&data->vco_freq)) { |
|
if (data->vco_freq < PLLDIG_MIN_VCO_FREQ || |
|
data->vco_freq > PLLDIG_MAX_VCO_FREQ) |
|
return -EINVAL; |
|
} |
|
|
|
return plldig_init(&data->hw); |
|
} |
|
|
|
static const struct of_device_id plldig_clk_id[] = { |
|
{ .compatible = "fsl,ls1028a-plldig" }, |
|
{ } |
|
}; |
|
MODULE_DEVICE_TABLE(of, plldig_clk_id); |
|
|
|
static struct platform_driver plldig_clk_driver = { |
|
.driver = { |
|
.name = "plldig-clock", |
|
.of_match_table = plldig_clk_id, |
|
}, |
|
.probe = plldig_clk_probe, |
|
}; |
|
module_platform_driver(plldig_clk_driver); |
|
|
|
MODULE_LICENSE("GPL v2"); |
|
MODULE_AUTHOR("Wen He <[email protected]>"); |
|
MODULE_DESCRIPTION("LS1028A Display output interface pixel clock driver");
|
|
|