Commit 939d749a authored by Chen-Yu Tsai's avatar Chen-Yu Tsai Committed by Maxime Ripard

drm/sun4i: hdmi: Add support for controller hardware variants

The HDMI controller found in earlier Allwinner SoCs have slight
differences between the A10, A10s, and the A31:

  - Need different initial values for the PLL related registers

  - Different behavior of the DDC and TMDS clocks

  - Different register layout for the DDC portion

  - Separate DDC parent clock on the A31

  - Explicit reset control

For the A31, the HDMI TMDS clock has a different value offset for
the divider. The HDMI DDC block is different from the one in the
other SoCs. As far as the DDC clock goes, it has no pre-divider,
as it is clocked from a slower parent clock, not the TMDS clock.
The divider offset from the register value is different. And the
clock control register is at a different offset.

A new variant data structure is created to store pointers to the
above functions, structures, and the different initial values.
Another flag notates whether there is a separate DDC parent clock.
If not, the TMDS clock is passed to the DDC clock create function,
as before.

Regmap fields are used to deal with the different register layout
of the DDC block.
Signed-off-by: default avatarChen-Yu Tsai <wens@csie.org>
Acked-by: default avatarMaxime Ripard <maxime.ripard@free-electrons.com>
Signed-off-by: default avatarMaxime Ripard <maxime.ripard@free-electrons.com>
Link: https://patchwork.freedesktop.org/patch/msgid/20171010032008.682-8-wens@csie.org
parent 68a48afa
......@@ -14,6 +14,7 @@
#include <drm/drm_connector.h>
#include <drm/drm_encoder.h>
#include <linux/regmap.h>
#include <media/cec-pin.h>
......@@ -157,6 +158,55 @@ enum sun4i_hdmi_pkt_type {
SUN4I_HDMI_PKT_END = 15,
};
struct sun4i_hdmi_variant {
bool has_ddc_parent_clk;
bool has_reset_control;
u32 pad_ctrl0_init_val;
u32 pad_ctrl1_init_val;
u32 pll_ctrl_init_val;
struct reg_field ddc_clk_reg;
u8 ddc_clk_pre_divider;
u8 ddc_clk_m_offset;
u8 tmds_clk_div_offset;
/* Register fields for I2C adapter */
struct reg_field field_ddc_en;
struct reg_field field_ddc_start;
struct reg_field field_ddc_reset;
struct reg_field field_ddc_addr_reg;
struct reg_field field_ddc_slave_addr;
struct reg_field field_ddc_int_mask;
struct reg_field field_ddc_int_status;
struct reg_field field_ddc_fifo_clear;
struct reg_field field_ddc_fifo_rx_thres;
struct reg_field field_ddc_fifo_tx_thres;
struct reg_field field_ddc_byte_count;
struct reg_field field_ddc_cmd;
struct reg_field field_ddc_sda_en;
struct reg_field field_ddc_sck_en;
/* DDC FIFO register offset */
u32 ddc_fifo_reg;
/*
* DDC FIFO threshold boundary conditions
*
* This is used to cope with the threshold boundary condition
* being slightly different on sun5i and sun6i.
*
* On sun5i the threshold is exclusive, i.e. does not include,
* the value of the threshold. ( > for RX; < for TX )
* On sun6i the threshold is inclusive, i.e. includes, the
* value of the threshold. ( >= for RX; <= for TX )
*/
bool ddc_fifo_thres_incl;
bool ddc_fifo_has_dir;
};
struct sun4i_hdmi {
struct drm_connector connector;
struct drm_encoder encoder;
......@@ -165,9 +215,13 @@ struct sun4i_hdmi {
void __iomem *base;
struct regmap *regmap;
/* Reset control */
struct reset_control *reset;
/* Parent clocks */
struct clk *bus_clk;
struct clk *mod_clk;
struct clk *ddc_parent_clk;
struct clk *pll0_clk;
struct clk *pll1_clk;
......@@ -177,10 +231,28 @@ struct sun4i_hdmi {
struct i2c_adapter *i2c;
/* Regmap fields for I2C adapter */
struct regmap_field *field_ddc_en;
struct regmap_field *field_ddc_start;
struct regmap_field *field_ddc_reset;
struct regmap_field *field_ddc_addr_reg;
struct regmap_field *field_ddc_slave_addr;
struct regmap_field *field_ddc_int_mask;
struct regmap_field *field_ddc_int_status;
struct regmap_field *field_ddc_fifo_clear;
struct regmap_field *field_ddc_fifo_rx_thres;
struct regmap_field *field_ddc_fifo_tx_thres;
struct regmap_field *field_ddc_byte_count;
struct regmap_field *field_ddc_cmd;
struct regmap_field *field_ddc_sda_en;
struct regmap_field *field_ddc_sck_en;
struct sun4i_drv *drv;
bool hdmi_monitor;
struct cec_adapter *cec_adap;
const struct sun4i_hdmi_variant *variant;
};
int sun4i_ddc_create(struct sun4i_hdmi *hdmi, struct clk *clk);
......
......@@ -11,6 +11,7 @@
*/
#include <linux/clk-provider.h>
#include <linux/regmap.h>
#include "sun4i_tcon.h"
#include "sun4i_hdmi.h"
......@@ -18,6 +19,9 @@
struct sun4i_ddc {
struct clk_hw hw;
struct sun4i_hdmi *hdmi;
struct regmap_field *reg;
u8 pre_div;
u8 m_offset;
};
static inline struct sun4i_ddc *hw_to_ddc(struct clk_hw *hw)
......@@ -27,6 +31,8 @@ static inline struct sun4i_ddc *hw_to_ddc(struct clk_hw *hw)
static unsigned long sun4i_ddc_calc_divider(unsigned long rate,
unsigned long parent_rate,
const u8 pre_div,
const u8 m_offset,
u8 *m, u8 *n)
{
unsigned long best_rate = 0;
......@@ -36,7 +42,8 @@ static unsigned long sun4i_ddc_calc_divider(unsigned long rate,
for (_n = 0; _n < 8; _n++) {
unsigned long tmp_rate;
tmp_rate = (((parent_rate / 2) / 10) >> _n) / (_m + 1);
tmp_rate = (((parent_rate / pre_div) / 10) >> _n) /
(_m + m_offset);
if (tmp_rate > rate)
continue;
......@@ -60,21 +67,25 @@ static unsigned long sun4i_ddc_calc_divider(unsigned long rate,
static long sun4i_ddc_round_rate(struct clk_hw *hw, unsigned long rate,
unsigned long *prate)
{
return sun4i_ddc_calc_divider(rate, *prate, NULL, NULL);
struct sun4i_ddc *ddc = hw_to_ddc(hw);
return sun4i_ddc_calc_divider(rate, *prate, ddc->pre_div,
ddc->m_offset, NULL, NULL);
}
static unsigned long sun4i_ddc_recalc_rate(struct clk_hw *hw,
unsigned long parent_rate)
{
struct sun4i_ddc *ddc = hw_to_ddc(hw);
u32 reg;
unsigned int reg;
u8 m, n;
reg = readl(ddc->hdmi->base + SUN4I_HDMI_DDC_CLK_REG);
m = (reg >> 3) & 0x7;
regmap_field_read(ddc->reg, &reg);
m = (reg >> 3) & 0xf;
n = reg & 0x7;
return (((parent_rate / 2) / 10) >> n) / (m + 1);
return (((parent_rate / ddc->pre_div) / 10) >> n) /
(m + ddc->m_offset);
}
static int sun4i_ddc_set_rate(struct clk_hw *hw, unsigned long rate,
......@@ -83,10 +94,12 @@ static int sun4i_ddc_set_rate(struct clk_hw *hw, unsigned long rate,
struct sun4i_ddc *ddc = hw_to_ddc(hw);
u8 div_m, div_n;
sun4i_ddc_calc_divider(rate, parent_rate, &div_m, &div_n);
sun4i_ddc_calc_divider(rate, parent_rate, ddc->pre_div,
ddc->m_offset, &div_m, &div_n);
writel(SUN4I_HDMI_DDC_CLK_M(div_m) | SUN4I_HDMI_DDC_CLK_N(div_n),
ddc->hdmi->base + SUN4I_HDMI_DDC_CLK_REG);
regmap_field_write(ddc->reg,
SUN4I_HDMI_DDC_CLK_M(div_m) |
SUN4I_HDMI_DDC_CLK_N(div_n));
return 0;
}
......@@ -111,6 +124,11 @@ int sun4i_ddc_create(struct sun4i_hdmi *hdmi, struct clk *parent)
if (!ddc)
return -ENOMEM;
ddc->reg = devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->ddc_clk_reg);
if (IS_ERR(ddc->reg))
return PTR_ERR(ddc->reg);
init.name = "hdmi-ddc";
init.ops = &sun4i_ddc_ops;
init.parent_names = &parent_name;
......@@ -118,6 +136,8 @@ int sun4i_ddc_create(struct sun4i_hdmi *hdmi, struct clk *parent)
ddc->hdmi = hdmi;
ddc->hw.init = &init;
ddc->pre_div = hdmi->variant->ddc_clk_pre_divider;
ddc->m_offset = hdmi->variant->ddc_clk_m_offset;
hdmi->ddc_clk = devm_clk_register(hdmi->dev, &ddc->hw);
if (IS_ERR(hdmi->ddc_clk))
......
......@@ -20,9 +20,11 @@
#include <linux/clk.h>
#include <linux/component.h>
#include <linux/iopoll.h>
#include <linux/of_device.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/regmap.h>
#include <linux/reset.h>
#include "sun4i_backend.h"
#include "sun4i_crtc.h"
......@@ -268,6 +270,60 @@ static const struct cec_pin_ops sun4i_hdmi_cec_pin_ops = {
};
#endif
#define SUN4I_HDMI_PAD_CTRL1_MASK (GENMASK(24, 7) | GENMASK(5, 0))
#define SUN4I_HDMI_PLL_CTRL_MASK (GENMASK(31, 8) | GENMASK(3, 0))
static const struct sun4i_hdmi_variant sun5i_variant = {
.pad_ctrl0_init_val = SUN4I_HDMI_PAD_CTRL0_TXEN |
SUN4I_HDMI_PAD_CTRL0_CKEN |
SUN4I_HDMI_PAD_CTRL0_PWENG |
SUN4I_HDMI_PAD_CTRL0_PWEND |
SUN4I_HDMI_PAD_CTRL0_PWENC |
SUN4I_HDMI_PAD_CTRL0_LDODEN |
SUN4I_HDMI_PAD_CTRL0_LDOCEN |
SUN4I_HDMI_PAD_CTRL0_BIASEN,
.pad_ctrl1_init_val = SUN4I_HDMI_PAD_CTRL1_REG_AMP(6) |
SUN4I_HDMI_PAD_CTRL1_REG_EMP(2) |
SUN4I_HDMI_PAD_CTRL1_REG_DENCK |
SUN4I_HDMI_PAD_CTRL1_REG_DEN |
SUN4I_HDMI_PAD_CTRL1_EMPCK_OPT |
SUN4I_HDMI_PAD_CTRL1_EMP_OPT |
SUN4I_HDMI_PAD_CTRL1_AMPCK_OPT |
SUN4I_HDMI_PAD_CTRL1_AMP_OPT,
.pll_ctrl_init_val = SUN4I_HDMI_PLL_CTRL_VCO_S(8) |
SUN4I_HDMI_PLL_CTRL_CS(7) |
SUN4I_HDMI_PLL_CTRL_CP_S(15) |
SUN4I_HDMI_PLL_CTRL_S(7) |
SUN4I_HDMI_PLL_CTRL_VCO_GAIN(4) |
SUN4I_HDMI_PLL_CTRL_SDIV2 |
SUN4I_HDMI_PLL_CTRL_LDO2_EN |
SUN4I_HDMI_PLL_CTRL_LDO1_EN |
SUN4I_HDMI_PLL_CTRL_HV_IS_33 |
SUN4I_HDMI_PLL_CTRL_BWS |
SUN4I_HDMI_PLL_CTRL_PLL_EN,
.ddc_clk_reg = REG_FIELD(SUN4I_HDMI_DDC_CLK_REG, 0, 6),
.ddc_clk_pre_divider = 2,
.ddc_clk_m_offset = 1,
.field_ddc_en = REG_FIELD(SUN4I_HDMI_DDC_CTRL_REG, 31, 31),
.field_ddc_start = REG_FIELD(SUN4I_HDMI_DDC_CTRL_REG, 30, 30),
.field_ddc_reset = REG_FIELD(SUN4I_HDMI_DDC_CTRL_REG, 0, 0),
.field_ddc_addr_reg = REG_FIELD(SUN4I_HDMI_DDC_ADDR_REG, 0, 31),
.field_ddc_slave_addr = REG_FIELD(SUN4I_HDMI_DDC_ADDR_REG, 0, 6),
.field_ddc_int_status = REG_FIELD(SUN4I_HDMI_DDC_INT_STATUS_REG, 0, 8),
.field_ddc_fifo_clear = REG_FIELD(SUN4I_HDMI_DDC_FIFO_CTRL_REG, 31, 31),
.field_ddc_fifo_rx_thres = REG_FIELD(SUN4I_HDMI_DDC_FIFO_CTRL_REG, 4, 7),
.field_ddc_fifo_tx_thres = REG_FIELD(SUN4I_HDMI_DDC_FIFO_CTRL_REG, 0, 3),
.field_ddc_byte_count = REG_FIELD(SUN4I_HDMI_DDC_BYTE_COUNT_REG, 0, 9),
.field_ddc_cmd = REG_FIELD(SUN4I_HDMI_DDC_CMD_REG, 0, 2),
.field_ddc_sda_en = REG_FIELD(SUN4I_HDMI_DDC_LINE_CTRL_REG, 9, 9),
.field_ddc_sck_en = REG_FIELD(SUN4I_HDMI_DDC_LINE_CTRL_REG, 8, 8),
.ddc_fifo_reg = SUN4I_HDMI_DDC_FIFO_DATA_REG,
.ddc_fifo_has_dir = true,
};
static const struct regmap_config sun4i_hdmi_regmap_config = {
.reg_bits = 32,
.val_bits = 32,
......@@ -293,6 +349,10 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master,
hdmi->dev = dev;
hdmi->drv = drv;
hdmi->variant = of_device_get_match_data(dev);
if (!hdmi->variant)
return -EINVAL;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
hdmi->base = devm_ioremap_resource(dev, res);
if (IS_ERR(hdmi->base)) {
......@@ -300,10 +360,25 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master,
return PTR_ERR(hdmi->base);
}
if (hdmi->variant->has_reset_control) {
hdmi->reset = devm_reset_control_get(dev, NULL);
if (IS_ERR(hdmi->reset)) {
dev_err(dev, "Couldn't get the HDMI reset control\n");
return PTR_ERR(hdmi->reset);
}
ret = reset_control_deassert(hdmi->reset);
if (ret) {
dev_err(dev, "Couldn't deassert HDMI reset\n");
return ret;
}
}
hdmi->bus_clk = devm_clk_get(dev, "ahb");
if (IS_ERR(hdmi->bus_clk)) {
dev_err(dev, "Couldn't get the HDMI bus clock\n");
return PTR_ERR(hdmi->bus_clk);
ret = PTR_ERR(hdmi->bus_clk);
goto err_assert_reset;
}
clk_prepare_enable(hdmi->bus_clk);
......@@ -342,12 +417,19 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master,
goto err_disable_mod_clk;
}
if (hdmi->variant->has_ddc_parent_clk) {
hdmi->ddc_parent_clk = devm_clk_get(dev, "ddc");
if (IS_ERR(hdmi->ddc_parent_clk)) {
dev_err(dev, "Couldn't get the HDMI DDC clock\n");
return PTR_ERR(hdmi->ddc_parent_clk);
}
} else {
hdmi->ddc_parent_clk = hdmi->tmds_clk;
}
writel(SUN4I_HDMI_CTRL_ENABLE, hdmi->base + SUN4I_HDMI_CTRL_REG);
writel(SUN4I_HDMI_PAD_CTRL0_TXEN | SUN4I_HDMI_PAD_CTRL0_CKEN |
SUN4I_HDMI_PAD_CTRL0_PWENG | SUN4I_HDMI_PAD_CTRL0_PWEND |
SUN4I_HDMI_PAD_CTRL0_PWENC | SUN4I_HDMI_PAD_CTRL0_LDODEN |
SUN4I_HDMI_PAD_CTRL0_LDOCEN | SUN4I_HDMI_PAD_CTRL0_BIASEN,
writel(hdmi->variant->pad_ctrl0_init_val,
hdmi->base + SUN4I_HDMI_PAD_CTRL0_REG);
/*
......@@ -357,24 +439,12 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master,
*/
reg = readl(hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
reg &= SUN4I_HDMI_PAD_CTRL1_HALVE_CLK;
reg |= SUN4I_HDMI_PAD_CTRL1_REG_AMP(6) |
SUN4I_HDMI_PAD_CTRL1_REG_EMP(2) |
SUN4I_HDMI_PAD_CTRL1_REG_DENCK |
SUN4I_HDMI_PAD_CTRL1_REG_DEN |
SUN4I_HDMI_PAD_CTRL1_EMPCK_OPT |
SUN4I_HDMI_PAD_CTRL1_EMP_OPT |
SUN4I_HDMI_PAD_CTRL1_AMPCK_OPT |
SUN4I_HDMI_PAD_CTRL1_AMP_OPT;
reg |= hdmi->variant->pad_ctrl1_init_val;
writel(reg, hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
reg = readl(hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
reg &= SUN4I_HDMI_PLL_CTRL_DIV_MASK;
reg |= SUN4I_HDMI_PLL_CTRL_VCO_S(8) | SUN4I_HDMI_PLL_CTRL_CS(7) |
SUN4I_HDMI_PLL_CTRL_CP_S(15) | SUN4I_HDMI_PLL_CTRL_S(7) |
SUN4I_HDMI_PLL_CTRL_VCO_GAIN(4) | SUN4I_HDMI_PLL_CTRL_SDIV2 |
SUN4I_HDMI_PLL_CTRL_LDO2_EN | SUN4I_HDMI_PLL_CTRL_LDO1_EN |
SUN4I_HDMI_PLL_CTRL_HV_IS_33 | SUN4I_HDMI_PLL_CTRL_BWS |
SUN4I_HDMI_PLL_CTRL_PLL_EN;
reg |= hdmi->variant->pll_ctrl_init_val;
writel(reg, hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
ret = sun4i_hdmi_i2c_create(dev, hdmi);
......@@ -444,6 +514,8 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master,
clk_disable_unprepare(hdmi->mod_clk);
err_disable_bus_clk:
clk_disable_unprepare(hdmi->bus_clk);
err_assert_reset:
reset_control_assert(hdmi->reset);
return ret;
}
......@@ -478,7 +550,7 @@ static int sun4i_hdmi_remove(struct platform_device *pdev)
}
static const struct of_device_id sun4i_hdmi_of_table[] = {
{ .compatible = "allwinner,sun5i-a10s-hdmi" },
{ .compatible = "allwinner,sun5i-a10s-hdmi", .data = &sun5i_variant, },
{ }
};
MODULE_DEVICE_TABLE(of, sun4i_hdmi_of_table);
......
This diff is collapsed.
......@@ -18,6 +18,8 @@
struct sun4i_tmds {
struct clk_hw hw;
struct sun4i_hdmi *hdmi;
u8 div_offset;
};
static inline struct sun4i_tmds *hw_to_tmds(struct clk_hw *hw)
......@@ -28,6 +30,7 @@ static inline struct sun4i_tmds *hw_to_tmds(struct clk_hw *hw)
static unsigned long sun4i_tmds_calc_divider(unsigned long rate,
unsigned long parent_rate,
u8 div_offset,
u8 *div,
bool *half)
{
......@@ -35,7 +38,7 @@ static unsigned long sun4i_tmds_calc_divider(unsigned long rate,
u8 best_m = 0, m;
bool is_double;
for (m = 1; m < 16; m++) {
for (m = div_offset ?: 1; m < (16 + div_offset); m++) {
u8 d;
for (d = 1; d < 3; d++) {
......@@ -67,6 +70,7 @@ static unsigned long sun4i_tmds_calc_divider(unsigned long rate,
static int sun4i_tmds_determine_rate(struct clk_hw *hw,
struct clk_rate_request *req)
{
struct sun4i_tmds *tmds = hw_to_tmds(hw);
struct clk_hw *parent = NULL;
unsigned long best_parent = 0;
unsigned long rate = req->rate;
......@@ -85,7 +89,8 @@ static int sun4i_tmds_determine_rate(struct clk_hw *hw,
continue;
for (i = 1; i < 3; i++) {
for (j = 1; j < 16; j++) {
for (j = tmds->div_offset ?: 1;
j < (16 + tmds->div_offset); j++) {
unsigned long ideal = rate * i * j;
unsigned long rounded;
......@@ -129,7 +134,7 @@ static unsigned long sun4i_tmds_recalc_rate(struct clk_hw *hw,
parent_rate /= 2;
reg = readl(tmds->hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
reg = (reg >> 4) & 0xf;
reg = ((reg >> 4) & 0xf) + tmds->div_offset;
if (!reg)
reg = 1;
......@@ -144,7 +149,8 @@ static int sun4i_tmds_set_rate(struct clk_hw *hw, unsigned long rate,
u32 reg;
u8 div;
sun4i_tmds_calc_divider(rate, parent_rate, &div, &half);
sun4i_tmds_calc_divider(rate, parent_rate, tmds->div_offset,
&div, &half);
reg = readl(tmds->hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
reg &= ~SUN4I_HDMI_PAD_CTRL1_HALVE_CLK;
......@@ -154,7 +160,7 @@ static int sun4i_tmds_set_rate(struct clk_hw *hw, unsigned long rate,
reg = readl(tmds->hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
reg &= ~SUN4I_HDMI_PLL_CTRL_DIV_MASK;
writel(reg | SUN4I_HDMI_PLL_CTRL_DIV(div),
writel(reg | SUN4I_HDMI_PLL_CTRL_DIV(div - tmds->div_offset),
tmds->hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
return 0;
......@@ -221,6 +227,7 @@ int sun4i_tmds_create(struct sun4i_hdmi *hdmi)
tmds->hdmi = hdmi;
tmds->hw.init = &init;
tmds->div_offset = hdmi->variant->tmds_clk_div_offset;
hdmi->tmds_clk = devm_clk_register(hdmi->dev, &tmds->hw);
if (IS_ERR(hdmi->tmds_clk))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment