forked from Minki/linux
clk: imx6: Fix procedure to switch the parent of LDB_DI_CLK
Due to incorrect placement of the clock gate cell in the ldb_di[x]_clk tree, the glitchy parent mux of ldb_di[x]_clk can cause a glitch to enter the ldb_di_ipu_div divider. If the divider gets locked up, no ldb_di[x]_clk is generated, and the LVDS display will hang when the ipu_di_clk is sourced from ldb_di_clk. To fix the problem, both the new and current parent of the ldb_di_clk should be disabled before the switch. This patch ensures that correct steps are followed when ldb_di_clk parent is switched in the beginning of boot. The glitchy muxes are then registered as read-only. The clock parent can be selected using the assigned-clocks and assigned-clock-parents properties of the ccm device tree node: &clks { assigned-clocks = <&clks IMX6QDL_CLK_LDB_DI0_SEL>, <&clks IMX6QDL_CLK_LDB_DI1_SEL>; assigned-clock-parents = <&clks IMX6QDL_CLK_MMDC_CH1_AXI>, <&clks IMX6QDL_CLK_PLL5_VIDEO_DIV>; }; The issue is explained in detail in EB821 ("LDB Clock Switch Procedure & i.MX6 Asynchronous Clock Switching Guidelines") [1]. [1] http://www.nxp.com/files/32bit/doc/eng_bulletin/EB821.pdf Signed-off-by: Ranjani Vaidyanathan <Ranjani.Vaidyanathan@nxp.com> Signed-off-by: Fabio Estevam <fabio.estevam@nxp.com> Signed-off-by: Philipp Zabel <p.zabel@pengutronix.de> Reviewed-by: Akshay Bhat <akshay.bhat@timesys.com> Tested-by Joshua Clayton <stillcompiling@gmail.com> Tested-by: Charles Kang <Charles.Kang@advantech.com.tw> Signed-off-by: Shawn Guo <shawnguo@kernel.org>
This commit is contained in:
parent
03d576f202
commit
5d283b0838
@ -156,9 +156,90 @@ static struct clk ** const uart_clks[] __initconst = {
|
||||
NULL
|
||||
};
|
||||
|
||||
#define CCM_CCDR 0x04
|
||||
static int ldb_di_sel_by_clock_id(int clock_id)
|
||||
{
|
||||
switch (clock_id) {
|
||||
case IMX6QDL_CLK_PLL5_VIDEO_DIV:
|
||||
if (clk_on_imx6q() &&
|
||||
imx_get_soc_revision() == IMX_CHIP_REVISION_1_0)
|
||||
return -ENOENT;
|
||||
return 0;
|
||||
case IMX6QDL_CLK_PLL2_PFD0_352M:
|
||||
return 1;
|
||||
case IMX6QDL_CLK_PLL2_PFD2_396M:
|
||||
return 2;
|
||||
case IMX6QDL_CLK_MMDC_CH1_AXI:
|
||||
return 3;
|
||||
case IMX6QDL_CLK_PLL3_USB_OTG:
|
||||
return 4;
|
||||
default:
|
||||
return -ENOENT;
|
||||
}
|
||||
}
|
||||
|
||||
#define CCDR_MMDC_CH1_MASK BIT(16)
|
||||
static void of_assigned_ldb_sels(struct device_node *node,
|
||||
unsigned int *ldb_di0_sel,
|
||||
unsigned int *ldb_di1_sel)
|
||||
{
|
||||
struct of_phandle_args clkspec;
|
||||
int index, rc, num_parents;
|
||||
int parent, child, sel;
|
||||
|
||||
num_parents = of_count_phandle_with_args(node, "assigned-clock-parents",
|
||||
"#clock-cells");
|
||||
for (index = 0; index < num_parents; index++) {
|
||||
rc = of_parse_phandle_with_args(node, "assigned-clock-parents",
|
||||
"#clock-cells", index, &clkspec);
|
||||
if (rc < 0) {
|
||||
/* skip empty (null) phandles */
|
||||
if (rc == -ENOENT)
|
||||
continue;
|
||||
else
|
||||
return;
|
||||
}
|
||||
if (clkspec.np != node || clkspec.args[0] >= IMX6QDL_CLK_END) {
|
||||
pr_err("ccm: parent clock %d not in ccm\n", index);
|
||||
return;
|
||||
}
|
||||
parent = clkspec.args[0];
|
||||
|
||||
rc = of_parse_phandle_with_args(node, "assigned-clocks",
|
||||
"#clock-cells", index, &clkspec);
|
||||
if (rc < 0)
|
||||
return;
|
||||
if (clkspec.np != node || clkspec.args[0] >= IMX6QDL_CLK_END) {
|
||||
pr_err("ccm: child clock %d not in ccm\n", index);
|
||||
return;
|
||||
}
|
||||
child = clkspec.args[0];
|
||||
|
||||
if (child != IMX6QDL_CLK_LDB_DI0_SEL &&
|
||||
child != IMX6QDL_CLK_LDB_DI1_SEL)
|
||||
continue;
|
||||
|
||||
sel = ldb_di_sel_by_clock_id(parent);
|
||||
if (sel < 0) {
|
||||
pr_err("ccm: invalid ldb_di%d parent clock: %d\n",
|
||||
child == IMX6QDL_CLK_LDB_DI1_SEL, parent);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child == IMX6QDL_CLK_LDB_DI0_SEL)
|
||||
*ldb_di0_sel = sel;
|
||||
if (child == IMX6QDL_CLK_LDB_DI1_SEL)
|
||||
*ldb_di1_sel = sel;
|
||||
}
|
||||
}
|
||||
|
||||
#define CCM_CCDR 0x04
|
||||
#define CCM_CCSR 0x0c
|
||||
#define CCM_CS2CDR 0x2c
|
||||
|
||||
#define CCDR_MMDC_CH1_MASK BIT(16)
|
||||
#define CCSR_PLL3_SW_CLK_SEL BIT(0)
|
||||
|
||||
#define CS2CDR_LDB_DI0_CLK_SEL_SHIFT 9
|
||||
#define CS2CDR_LDB_DI1_CLK_SEL_SHIFT 12
|
||||
|
||||
static void __init imx6q_mmdc_ch1_mask_handshake(void __iomem *ccm_base)
|
||||
{
|
||||
@ -169,10 +250,173 @@ static void __init imx6q_mmdc_ch1_mask_handshake(void __iomem *ccm_base)
|
||||
writel_relaxed(reg, ccm_base + CCM_CCDR);
|
||||
}
|
||||
|
||||
/*
|
||||
* The only way to disable the MMDC_CH1 clock is to move it to pll3_sw_clk
|
||||
* via periph2_clk2_sel and then to disable pll3_sw_clk by selecting the
|
||||
* bypass clock source, since there is no CG bit for mmdc_ch1.
|
||||
*/
|
||||
static void mmdc_ch1_disable(void __iomem *ccm_base)
|
||||
{
|
||||
unsigned int reg;
|
||||
|
||||
clk_set_parent(clk[IMX6QDL_CLK_PERIPH2_CLK2_SEL],
|
||||
clk[IMX6QDL_CLK_PLL3_USB_OTG]);
|
||||
|
||||
/*
|
||||
* Handshake with mmdc_ch1 module must be masked when changing
|
||||
* periph2_clk_sel.
|
||||
*/
|
||||
clk_set_parent(clk[IMX6QDL_CLK_PERIPH2], clk[IMX6QDL_CLK_PERIPH2_CLK2]);
|
||||
|
||||
/* Disable pll3_sw_clk by selecting the bypass clock source */
|
||||
reg = readl_relaxed(ccm_base + CCM_CCSR);
|
||||
reg |= CCSR_PLL3_SW_CLK_SEL;
|
||||
writel_relaxed(reg, ccm_base + CCM_CCSR);
|
||||
}
|
||||
|
||||
static void mmdc_ch1_reenable(void __iomem *ccm_base)
|
||||
{
|
||||
unsigned int reg;
|
||||
|
||||
/* Enable pll3_sw_clk by disabling the bypass */
|
||||
reg = readl_relaxed(ccm_base + CCM_CCSR);
|
||||
reg &= ~CCSR_PLL3_SW_CLK_SEL;
|
||||
writel_relaxed(reg, ccm_base + CCM_CCSR);
|
||||
|
||||
clk_set_parent(clk[IMX6QDL_CLK_PERIPH2], clk[IMX6QDL_CLK_PERIPH2_PRE]);
|
||||
}
|
||||
|
||||
/*
|
||||
* We have to follow a strict procedure when changing the LDB clock source,
|
||||
* otherwise we risk introducing a glitch that can lock up the LDB divider.
|
||||
* Things to keep in mind:
|
||||
*
|
||||
* 1. The current and new parent clock inputs to the mux must be disabled.
|
||||
* 2. The default clock input for ldb_di0/1_clk_sel is mmdc_ch1_axi, which
|
||||
* has no CG bit.
|
||||
* 3. pll2_pfd2_396m can not be gated if it is used as memory clock.
|
||||
* 4. In the RTL implementation of the LDB_DI_CLK_SEL muxes the top four
|
||||
* options are in one mux and the PLL3 option along with three unused
|
||||
* inputs is in a second mux. There is a third mux with two inputs used
|
||||
* to decide between the first and second 4-port mux:
|
||||
*
|
||||
* pll5_video_div 0 --|\
|
||||
* pll2_pfd0_352m 1 --| |_
|
||||
* pll2_pfd2_396m 2 --| | `-|\
|
||||
* mmdc_ch1_axi 3 --|/ | |
|
||||
* | |--
|
||||
* pll3_usb_otg 4 --|\ | |
|
||||
* 5 --| |_,-|/
|
||||
* 6 --| |
|
||||
* 7 --|/
|
||||
*
|
||||
* The ldb_di0/1_clk_sel[1:0] bits control both 4-port muxes at the same time.
|
||||
* The ldb_di0/1_clk_sel[2] bit controls the 2-port mux. The code below
|
||||
* switches the parent to the bottom mux first and then manipulates the top
|
||||
* mux to ensure that no glitch will enter the divider.
|
||||
*/
|
||||
static void init_ldb_clks(struct device_node *np, void __iomem *ccm_base)
|
||||
{
|
||||
unsigned int reg;
|
||||
unsigned int sel[2][4];
|
||||
int i;
|
||||
|
||||
reg = readl_relaxed(ccm_base + CCM_CS2CDR);
|
||||
sel[0][0] = (reg >> CS2CDR_LDB_DI0_CLK_SEL_SHIFT) & 7;
|
||||
sel[1][0] = (reg >> CS2CDR_LDB_DI1_CLK_SEL_SHIFT) & 7;
|
||||
|
||||
sel[0][3] = sel[0][2] = sel[0][1] = sel[0][0];
|
||||
sel[1][3] = sel[1][2] = sel[1][1] = sel[1][0];
|
||||
|
||||
of_assigned_ldb_sels(np, &sel[0][3], &sel[1][3]);
|
||||
|
||||
for (i = 0; i < 2; i++) {
|
||||
/* Warn if a glitch might have been introduced already */
|
||||
if (sel[i][0] != 3) {
|
||||
pr_warn("ccm: ldb_di%d_sel already changed from reset value: %d\n",
|
||||
i, sel[i][0]);
|
||||
}
|
||||
|
||||
if (sel[i][0] == sel[i][3])
|
||||
continue;
|
||||
|
||||
/* Only switch to or from pll2_pfd2_396m if it is disabled */
|
||||
if ((sel[i][0] == 2 || sel[i][3] == 2) &&
|
||||
(clk_get_parent(clk[IMX6QDL_CLK_PERIPH_PRE]) ==
|
||||
clk[IMX6QDL_CLK_PLL2_PFD2_396M])) {
|
||||
pr_err("ccm: ldb_di%d_sel: couldn't disable pll2_pfd2_396m\n",
|
||||
i);
|
||||
sel[i][3] = sel[i][2] = sel[i][1] = sel[i][0];
|
||||
continue;
|
||||
}
|
||||
|
||||
/* First switch to the bottom mux */
|
||||
sel[i][1] = sel[i][0] | 4;
|
||||
|
||||
/* Then configure the top mux before switching back to it */
|
||||
sel[i][2] = sel[i][3] | 4;
|
||||
|
||||
pr_debug("ccm: switching ldb_di%d_sel: %d->%d->%d->%d\n", i,
|
||||
sel[i][0], sel[i][1], sel[i][2], sel[i][3]);
|
||||
}
|
||||
|
||||
if (sel[0][0] == sel[0][3] && sel[1][0] == sel[1][3])
|
||||
return;
|
||||
|
||||
mmdc_ch1_disable(ccm_base);
|
||||
|
||||
for (i = 1; i < 4; i++) {
|
||||
reg = readl_relaxed(ccm_base + CCM_CS2CDR);
|
||||
reg &= ~((7 << CS2CDR_LDB_DI0_CLK_SEL_SHIFT) |
|
||||
(7 << CS2CDR_LDB_DI1_CLK_SEL_SHIFT));
|
||||
reg |= ((sel[0][i] << CS2CDR_LDB_DI0_CLK_SEL_SHIFT) |
|
||||
(sel[1][i] << CS2CDR_LDB_DI1_CLK_SEL_SHIFT));
|
||||
writel_relaxed(reg, ccm_base + CCM_CS2CDR);
|
||||
}
|
||||
|
||||
mmdc_ch1_reenable(ccm_base);
|
||||
}
|
||||
|
||||
#define CCM_ANALOG_PLL_VIDEO 0xa0
|
||||
#define CCM_ANALOG_PFD_480 0xf0
|
||||
#define CCM_ANALOG_PFD_528 0x100
|
||||
|
||||
#define PLL_ENABLE BIT(13)
|
||||
|
||||
#define PFD0_CLKGATE BIT(7)
|
||||
#define PFD1_CLKGATE BIT(15)
|
||||
#define PFD2_CLKGATE BIT(23)
|
||||
#define PFD3_CLKGATE BIT(31)
|
||||
|
||||
static void disable_anatop_clocks(void __iomem *anatop_base)
|
||||
{
|
||||
unsigned int reg;
|
||||
|
||||
/* Make sure PLL2 PFDs 0-2 are gated */
|
||||
reg = readl_relaxed(anatop_base + CCM_ANALOG_PFD_528);
|
||||
/* Cannot gate PFD2 if pll2_pfd2_396m is the parent of MMDC clock */
|
||||
if (clk_get_parent(clk[IMX6QDL_CLK_PERIPH_PRE]) ==
|
||||
clk[IMX6QDL_CLK_PLL2_PFD2_396M])
|
||||
reg |= PFD0_CLKGATE | PFD1_CLKGATE;
|
||||
else
|
||||
reg |= PFD0_CLKGATE | PFD1_CLKGATE | PFD2_CLKGATE;
|
||||
writel_relaxed(reg, anatop_base + CCM_ANALOG_PFD_528);
|
||||
|
||||
/* Make sure PLL3 PFDs 0-3 are gated */
|
||||
reg = readl_relaxed(anatop_base + CCM_ANALOG_PFD_480);
|
||||
reg |= PFD0_CLKGATE | PFD1_CLKGATE | PFD2_CLKGATE | PFD3_CLKGATE;
|
||||
writel_relaxed(reg, anatop_base + CCM_ANALOG_PFD_480);
|
||||
|
||||
/* Make sure PLL5 is disabled */
|
||||
reg = readl_relaxed(anatop_base + CCM_ANALOG_PLL_VIDEO);
|
||||
reg &= ~PLL_ENABLE;
|
||||
writel_relaxed(reg, anatop_base + CCM_ANALOG_PLL_VIDEO);
|
||||
}
|
||||
|
||||
static void __init imx6q_clocks_init(struct device_node *ccm_node)
|
||||
{
|
||||
struct device_node *np;
|
||||
void __iomem *base;
|
||||
void __iomem *anatop_base, *base;
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
@ -185,7 +429,7 @@ static void __init imx6q_clocks_init(struct device_node *ccm_node)
|
||||
clk[IMX6QDL_CLK_ANACLK2] = imx_obtain_fixed_clock("anaclk2", 0);
|
||||
|
||||
np = of_find_compatible_node(NULL, NULL, "fsl,imx6q-anatop");
|
||||
base = of_iomap(np, 0);
|
||||
anatop_base = base = of_iomap(np, 0);
|
||||
WARN_ON(!base);
|
||||
|
||||
/* Audio/video PLL post dividers do not work on i.MX6q revision 1.0 */
|
||||
@ -310,8 +554,6 @@ static void __init imx6q_clocks_init(struct device_node *ccm_node)
|
||||
base = of_iomap(np, 0);
|
||||
WARN_ON(!base);
|
||||
|
||||
imx6q_mmdc_ch1_mask_handshake(base);
|
||||
|
||||
/* name reg shift width parent_names num_parents */
|
||||
clk[IMX6QDL_CLK_STEP] = imx_clk_mux("step", base + 0xc, 8, 1, step_sels, ARRAY_SIZE(step_sels));
|
||||
clk[IMX6QDL_CLK_PLL1_SW] = imx_clk_mux("pll1_sw", base + 0xc, 2, 1, pll1_sw_sels, ARRAY_SIZE(pll1_sw_sels));
|
||||
@ -345,6 +587,18 @@ static void __init imx6q_clocks_init(struct device_node *ccm_node)
|
||||
clk[IMX6QDL_CLK_GPU3D_SHADER_SEL] = imx_clk_mux("gpu3d_shader_sel", base + 0x18, 8, 2, gpu3d_shader_sels, ARRAY_SIZE(gpu3d_shader_sels));
|
||||
clk[IMX6QDL_CLK_IPU1_SEL] = imx_clk_mux("ipu1_sel", base + 0x3c, 9, 2, ipu_sels, ARRAY_SIZE(ipu_sels));
|
||||
clk[IMX6QDL_CLK_IPU2_SEL] = imx_clk_mux("ipu2_sel", base + 0x3c, 14, 2, ipu_sels, ARRAY_SIZE(ipu_sels));
|
||||
|
||||
disable_anatop_clocks(anatop_base);
|
||||
|
||||
imx6q_mmdc_ch1_mask_handshake(base);
|
||||
|
||||
/*
|
||||
* The LDB_DI0/1_SEL muxes are registered read-only due to a hardware
|
||||
* bug. Set the muxes to the requested values before registering the
|
||||
* ldb_di_sel clocks.
|
||||
*/
|
||||
init_ldb_clks(np, base);
|
||||
|
||||
clk[IMX6QDL_CLK_LDB_DI0_SEL] = imx_clk_mux_ldb("ldb_di0_sel", base + 0x2c, 9, 3, ldb_di_sels, ARRAY_SIZE(ldb_di_sels));
|
||||
clk[IMX6QDL_CLK_LDB_DI1_SEL] = imx_clk_mux_ldb("ldb_di1_sel", base + 0x2c, 12, 3, ldb_di_sels, ARRAY_SIZE(ldb_di_sels));
|
||||
clk[IMX6QDL_CLK_IPU1_DI0_PRE_SEL] = imx_clk_mux_flags("ipu1_di0_pre_sel", base + 0x34, 6, 3, ipu_di_pre_sels, ARRAY_SIZE(ipu_di_pre_sels), CLK_SET_RATE_PARENT);
|
||||
|
Loading…
Reference in New Issue
Block a user