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 @@ ...@@ -14,6 +14,7 @@
#include <drm/drm_connector.h> #include <drm/drm_connector.h>
#include <drm/drm_encoder.h> #include <drm/drm_encoder.h>
#include <linux/regmap.h>
#include <media/cec-pin.h> #include <media/cec-pin.h>
...@@ -157,6 +158,55 @@ enum sun4i_hdmi_pkt_type { ...@@ -157,6 +158,55 @@ enum sun4i_hdmi_pkt_type {
SUN4I_HDMI_PKT_END = 15, 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 sun4i_hdmi {
struct drm_connector connector; struct drm_connector connector;
struct drm_encoder encoder; struct drm_encoder encoder;
...@@ -165,9 +215,13 @@ struct sun4i_hdmi { ...@@ -165,9 +215,13 @@ struct sun4i_hdmi {
void __iomem *base; void __iomem *base;
struct regmap *regmap; struct regmap *regmap;
/* Reset control */
struct reset_control *reset;
/* Parent clocks */ /* Parent clocks */
struct clk *bus_clk; struct clk *bus_clk;
struct clk *mod_clk; struct clk *mod_clk;
struct clk *ddc_parent_clk;
struct clk *pll0_clk; struct clk *pll0_clk;
struct clk *pll1_clk; struct clk *pll1_clk;
...@@ -177,10 +231,28 @@ struct sun4i_hdmi { ...@@ -177,10 +231,28 @@ struct sun4i_hdmi {
struct i2c_adapter *i2c; 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; struct sun4i_drv *drv;
bool hdmi_monitor; bool hdmi_monitor;
struct cec_adapter *cec_adap; struct cec_adapter *cec_adap;
const struct sun4i_hdmi_variant *variant;
}; };
int sun4i_ddc_create(struct sun4i_hdmi *hdmi, struct clk *clk); int sun4i_ddc_create(struct sun4i_hdmi *hdmi, struct clk *clk);
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
*/ */
#include <linux/clk-provider.h> #include <linux/clk-provider.h>
#include <linux/regmap.h>
#include "sun4i_tcon.h" #include "sun4i_tcon.h"
#include "sun4i_hdmi.h" #include "sun4i_hdmi.h"
...@@ -18,6 +19,9 @@ ...@@ -18,6 +19,9 @@
struct sun4i_ddc { struct sun4i_ddc {
struct clk_hw hw; struct clk_hw hw;
struct sun4i_hdmi *hdmi; 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) 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) ...@@ -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, static unsigned long sun4i_ddc_calc_divider(unsigned long rate,
unsigned long parent_rate, unsigned long parent_rate,
const u8 pre_div,
const u8 m_offset,
u8 *m, u8 *n) u8 *m, u8 *n)
{ {
unsigned long best_rate = 0; unsigned long best_rate = 0;
...@@ -36,7 +42,8 @@ static unsigned long sun4i_ddc_calc_divider(unsigned long rate, ...@@ -36,7 +42,8 @@ static unsigned long sun4i_ddc_calc_divider(unsigned long rate,
for (_n = 0; _n < 8; _n++) { for (_n = 0; _n < 8; _n++) {
unsigned long tmp_rate; 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) if (tmp_rate > rate)
continue; continue;
...@@ -60,21 +67,25 @@ static unsigned long sun4i_ddc_calc_divider(unsigned long rate, ...@@ -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, static long sun4i_ddc_round_rate(struct clk_hw *hw, unsigned long rate,
unsigned long *prate) 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, static unsigned long sun4i_ddc_recalc_rate(struct clk_hw *hw,
unsigned long parent_rate) unsigned long parent_rate)
{ {
struct sun4i_ddc *ddc = hw_to_ddc(hw); struct sun4i_ddc *ddc = hw_to_ddc(hw);
u32 reg; unsigned int reg;
u8 m, n; u8 m, n;
reg = readl(ddc->hdmi->base + SUN4I_HDMI_DDC_CLK_REG); regmap_field_read(ddc->reg, &reg);
m = (reg >> 3) & 0x7; m = (reg >> 3) & 0xf;
n = reg & 0x7; 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, 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, ...@@ -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); struct sun4i_ddc *ddc = hw_to_ddc(hw);
u8 div_m, div_n; 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), regmap_field_write(ddc->reg,
ddc->hdmi->base + SUN4I_HDMI_DDC_CLK_REG); SUN4I_HDMI_DDC_CLK_M(div_m) |
SUN4I_HDMI_DDC_CLK_N(div_n));
return 0; return 0;
} }
...@@ -111,6 +124,11 @@ int sun4i_ddc_create(struct sun4i_hdmi *hdmi, struct clk *parent) ...@@ -111,6 +124,11 @@ int sun4i_ddc_create(struct sun4i_hdmi *hdmi, struct clk *parent)
if (!ddc) if (!ddc)
return -ENOMEM; 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.name = "hdmi-ddc";
init.ops = &sun4i_ddc_ops; init.ops = &sun4i_ddc_ops;
init.parent_names = &parent_name; init.parent_names = &parent_name;
...@@ -118,6 +136,8 @@ int sun4i_ddc_create(struct sun4i_hdmi *hdmi, struct clk *parent) ...@@ -118,6 +136,8 @@ int sun4i_ddc_create(struct sun4i_hdmi *hdmi, struct clk *parent)
ddc->hdmi = hdmi; ddc->hdmi = hdmi;
ddc->hw.init = &init; 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); hdmi->ddc_clk = devm_clk_register(hdmi->dev, &ddc->hw);
if (IS_ERR(hdmi->ddc_clk)) if (IS_ERR(hdmi->ddc_clk))
......
...@@ -20,9 +20,11 @@ ...@@ -20,9 +20,11 @@
#include <linux/clk.h> #include <linux/clk.h>
#include <linux/component.h> #include <linux/component.h>
#include <linux/iopoll.h> #include <linux/iopoll.h>
#include <linux/of_device.h>
#include <linux/platform_device.h> #include <linux/platform_device.h>
#include <linux/pm_runtime.h> #include <linux/pm_runtime.h>
#include <linux/regmap.h> #include <linux/regmap.h>
#include <linux/reset.h>
#include "sun4i_backend.h" #include "sun4i_backend.h"
#include "sun4i_crtc.h" #include "sun4i_crtc.h"
...@@ -268,6 +270,60 @@ static const struct cec_pin_ops sun4i_hdmi_cec_pin_ops = { ...@@ -268,6 +270,60 @@ static const struct cec_pin_ops sun4i_hdmi_cec_pin_ops = {
}; };
#endif #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 = { static const struct regmap_config sun4i_hdmi_regmap_config = {
.reg_bits = 32, .reg_bits = 32,
.val_bits = 32, .val_bits = 32,
...@@ -293,6 +349,10 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master, ...@@ -293,6 +349,10 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master,
hdmi->dev = dev; hdmi->dev = dev;
hdmi->drv = drv; hdmi->drv = drv;
hdmi->variant = of_device_get_match_data(dev);
if (!hdmi->variant)
return -EINVAL;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
hdmi->base = devm_ioremap_resource(dev, res); hdmi->base = devm_ioremap_resource(dev, res);
if (IS_ERR(hdmi->base)) { if (IS_ERR(hdmi->base)) {
...@@ -300,10 +360,25 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master, ...@@ -300,10 +360,25 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master,
return PTR_ERR(hdmi->base); 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"); hdmi->bus_clk = devm_clk_get(dev, "ahb");
if (IS_ERR(hdmi->bus_clk)) { if (IS_ERR(hdmi->bus_clk)) {
dev_err(dev, "Couldn't get the HDMI bus clock\n"); 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); clk_prepare_enable(hdmi->bus_clk);
...@@ -342,12 +417,19 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master, ...@@ -342,12 +417,19 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master,
goto err_disable_mod_clk; 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_CTRL_ENABLE, hdmi->base + SUN4I_HDMI_CTRL_REG);
writel(SUN4I_HDMI_PAD_CTRL0_TXEN | SUN4I_HDMI_PAD_CTRL0_CKEN | writel(hdmi->variant->pad_ctrl0_init_val,
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,
hdmi->base + SUN4I_HDMI_PAD_CTRL0_REG); hdmi->base + SUN4I_HDMI_PAD_CTRL0_REG);
/* /*
...@@ -357,24 +439,12 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master, ...@@ -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 = readl(hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
reg &= SUN4I_HDMI_PAD_CTRL1_HALVE_CLK; reg &= SUN4I_HDMI_PAD_CTRL1_HALVE_CLK;
reg |= SUN4I_HDMI_PAD_CTRL1_REG_AMP(6) | reg |= hdmi->variant->pad_ctrl1_init_val;
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;
writel(reg, hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG); writel(reg, hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
reg = readl(hdmi->base + SUN4I_HDMI_PLL_CTRL_REG); reg = readl(hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
reg &= SUN4I_HDMI_PLL_CTRL_DIV_MASK; reg &= SUN4I_HDMI_PLL_CTRL_DIV_MASK;
reg |= SUN4I_HDMI_PLL_CTRL_VCO_S(8) | SUN4I_HDMI_PLL_CTRL_CS(7) | reg |= hdmi->variant->pll_ctrl_init_val;
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;
writel(reg, hdmi->base + SUN4I_HDMI_PLL_CTRL_REG); writel(reg, hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
ret = sun4i_hdmi_i2c_create(dev, hdmi); ret = sun4i_hdmi_i2c_create(dev, hdmi);
...@@ -444,6 +514,8 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master, ...@@ -444,6 +514,8 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master,
clk_disable_unprepare(hdmi->mod_clk); clk_disable_unprepare(hdmi->mod_clk);
err_disable_bus_clk: err_disable_bus_clk:
clk_disable_unprepare(hdmi->bus_clk); clk_disable_unprepare(hdmi->bus_clk);
err_assert_reset:
reset_control_assert(hdmi->reset);
return ret; return ret;
} }
...@@ -478,7 +550,7 @@ static int sun4i_hdmi_remove(struct platform_device *pdev) ...@@ -478,7 +550,7 @@ static int sun4i_hdmi_remove(struct platform_device *pdev)
} }
static const struct of_device_id sun4i_hdmi_of_table[] = { 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); MODULE_DEVICE_TABLE(of, sun4i_hdmi_of_table);
......
...@@ -25,8 +25,6 @@ ...@@ -25,8 +25,6 @@
/* FIFO request bit is set when FIFO level is above RX_THRESHOLD during read */ /* FIFO request bit is set when FIFO level is above RX_THRESHOLD during read */
#define RX_THRESHOLD SUN4I_HDMI_DDC_FIFO_CTRL_RX_THRES_MAX #define RX_THRESHOLD SUN4I_HDMI_DDC_FIFO_CTRL_RX_THRES_MAX
/* FIFO request bit is set when FIFO level is below TX_THRESHOLD during write */
#define TX_THRESHOLD 1
static int fifo_transfer(struct sun4i_hdmi *hdmi, u8 *buf, int len, bool read) static int fifo_transfer(struct sun4i_hdmi *hdmi, u8 *buf, int len, bool read)
{ {
...@@ -39,27 +37,36 @@ static int fifo_transfer(struct sun4i_hdmi *hdmi, u8 *buf, int len, bool read) ...@@ -39,27 +37,36 @@ static int fifo_transfer(struct sun4i_hdmi *hdmi, u8 *buf, int len, bool read)
SUN4I_HDMI_DDC_INT_STATUS_FIFO_REQUEST | SUN4I_HDMI_DDC_INT_STATUS_FIFO_REQUEST |
SUN4I_HDMI_DDC_INT_STATUS_TRANSFER_COMPLETE; SUN4I_HDMI_DDC_INT_STATUS_TRANSFER_COMPLETE;
u32 reg; u32 reg;
/*
* If threshold is inclusive, then the FIFO may only have
* RX_THRESHOLD number of bytes, instead of RX_THRESHOLD + 1.
*/
int read_len = RX_THRESHOLD +
(hdmi->variant->ddc_fifo_thres_incl ? 0 : 1);
/* Limit transfer length by FIFO threshold */ /*
len = min_t(int, len, read ? (RX_THRESHOLD + 1) : * Limit transfer length by FIFO threshold or FIFO size.
(SUN4I_HDMI_DDC_FIFO_SIZE - TX_THRESHOLD + 1)); * For TX the threshold is for an empty FIFO.
*/
len = min_t(int, len, read ? read_len : SUN4I_HDMI_DDC_FIFO_SIZE);
/* Wait until error, FIFO request bit set or transfer complete */ /* Wait until error, FIFO request bit set or transfer complete */
if (readl_poll_timeout(hdmi->base + SUN4I_HDMI_DDC_INT_STATUS_REG, reg, if (regmap_field_read_poll_timeout(hdmi->field_ddc_int_status, reg,
reg & mask, len * byte_time_ns, 100000)) reg & mask, len * byte_time_ns,
100000))
return -ETIMEDOUT; return -ETIMEDOUT;
if (reg & SUN4I_HDMI_DDC_INT_STATUS_ERROR_MASK) if (reg & SUN4I_HDMI_DDC_INT_STATUS_ERROR_MASK)
return -EIO; return -EIO;
if (read) if (read)
readsb(hdmi->base + SUN4I_HDMI_DDC_FIFO_DATA_REG, buf, len); readsb(hdmi->base + hdmi->variant->ddc_fifo_reg, buf, len);
else else
writesb(hdmi->base + SUN4I_HDMI_DDC_FIFO_DATA_REG, buf, len); writesb(hdmi->base + hdmi->variant->ddc_fifo_reg, buf, len);
/* Clear FIFO request bit */ /* Clear FIFO request bit by forcing a write to that bit */
writel(SUN4I_HDMI_DDC_INT_STATUS_FIFO_REQUEST, regmap_field_force_write(hdmi->field_ddc_int_status,
hdmi->base + SUN4I_HDMI_DDC_INT_STATUS_REG); SUN4I_HDMI_DDC_INT_STATUS_FIFO_REQUEST);
return len; return len;
} }
...@@ -70,50 +77,52 @@ static int xfer_msg(struct sun4i_hdmi *hdmi, struct i2c_msg *msg) ...@@ -70,50 +77,52 @@ static int xfer_msg(struct sun4i_hdmi *hdmi, struct i2c_msg *msg)
u32 reg; u32 reg;
/* Set FIFO direction */ /* Set FIFO direction */
if (hdmi->variant->ddc_fifo_has_dir) {
reg = readl(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG); reg = readl(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG);
reg &= ~SUN4I_HDMI_DDC_CTRL_FIFO_DIR_MASK; reg &= ~SUN4I_HDMI_DDC_CTRL_FIFO_DIR_MASK;
reg |= (msg->flags & I2C_M_RD) ? reg |= (msg->flags & I2C_M_RD) ?
SUN4I_HDMI_DDC_CTRL_FIFO_DIR_READ : SUN4I_HDMI_DDC_CTRL_FIFO_DIR_READ :
SUN4I_HDMI_DDC_CTRL_FIFO_DIR_WRITE; SUN4I_HDMI_DDC_CTRL_FIFO_DIR_WRITE;
writel(reg, hdmi->base + SUN4I_HDMI_DDC_CTRL_REG); writel(reg, hdmi->base + SUN4I_HDMI_DDC_CTRL_REG);
}
/* Clear address register (not cleared by soft reset) */
regmap_field_write(hdmi->field_ddc_addr_reg, 0);
/* Set I2C address */ /* Set I2C address */
writel(SUN4I_HDMI_DDC_ADDR_SLAVE(msg->addr), regmap_field_write(hdmi->field_ddc_slave_addr, msg->addr);
hdmi->base + SUN4I_HDMI_DDC_ADDR_REG);
/*
/* Set FIFO RX/TX thresholds and clear FIFO */ * Set FIFO RX/TX thresholds and clear FIFO
reg = readl(hdmi->base + SUN4I_HDMI_DDC_FIFO_CTRL_REG); *
reg |= SUN4I_HDMI_DDC_FIFO_CTRL_CLEAR; * If threshold is inclusive, we can set the TX threshold to
reg &= ~SUN4I_HDMI_DDC_FIFO_CTRL_RX_THRES_MASK; * 0 instead of 1.
reg |= SUN4I_HDMI_DDC_FIFO_CTRL_RX_THRES(RX_THRESHOLD); */
reg &= ~SUN4I_HDMI_DDC_FIFO_CTRL_TX_THRES_MASK; regmap_field_write(hdmi->field_ddc_fifo_tx_thres,
reg |= SUN4I_HDMI_DDC_FIFO_CTRL_TX_THRES(TX_THRESHOLD); hdmi->variant->ddc_fifo_thres_incl ? 0 : 1);
writel(reg, hdmi->base + SUN4I_HDMI_DDC_FIFO_CTRL_REG); regmap_field_write(hdmi->field_ddc_fifo_rx_thres, RX_THRESHOLD);
if (readl_poll_timeout(hdmi->base + SUN4I_HDMI_DDC_FIFO_CTRL_REG, regmap_field_write(hdmi->field_ddc_fifo_clear, 1);
reg, if (regmap_field_read_poll_timeout(hdmi->field_ddc_fifo_clear,
!(reg & SUN4I_HDMI_DDC_FIFO_CTRL_CLEAR), reg, !reg, 100, 2000))
100, 2000))
return -EIO; return -EIO;
/* Set transfer length */ /* Set transfer length */
writel(msg->len, hdmi->base + SUN4I_HDMI_DDC_BYTE_COUNT_REG); regmap_field_write(hdmi->field_ddc_byte_count, msg->len);
/* Set command */ /* Set command */
writel(msg->flags & I2C_M_RD ? regmap_field_write(hdmi->field_ddc_cmd,
msg->flags & I2C_M_RD ?
SUN4I_HDMI_DDC_CMD_IMPLICIT_READ : SUN4I_HDMI_DDC_CMD_IMPLICIT_READ :
SUN4I_HDMI_DDC_CMD_IMPLICIT_WRITE, SUN4I_HDMI_DDC_CMD_IMPLICIT_WRITE);
hdmi->base + SUN4I_HDMI_DDC_CMD_REG);
/* Clear interrupt status bits */ /* Clear interrupt status bits by forcing a write */
writel(SUN4I_HDMI_DDC_INT_STATUS_ERROR_MASK | regmap_field_force_write(hdmi->field_ddc_int_status,
SUN4I_HDMI_DDC_INT_STATUS_ERROR_MASK |
SUN4I_HDMI_DDC_INT_STATUS_FIFO_REQUEST | SUN4I_HDMI_DDC_INT_STATUS_FIFO_REQUEST |
SUN4I_HDMI_DDC_INT_STATUS_TRANSFER_COMPLETE, SUN4I_HDMI_DDC_INT_STATUS_TRANSFER_COMPLETE);
hdmi->base + SUN4I_HDMI_DDC_INT_STATUS_REG);
/* Start command */ /* Start command */
reg = readl(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG); regmap_field_write(hdmi->field_ddc_start, 1);
writel(reg | SUN4I_HDMI_DDC_CTRL_START_CMD,
hdmi->base + SUN4I_HDMI_DDC_CTRL_REG);
/* Transfer bytes */ /* Transfer bytes */
for (i = 0; i < msg->len; i += len) { for (i = 0; i < msg->len; i += len) {
...@@ -124,14 +133,12 @@ static int xfer_msg(struct sun4i_hdmi *hdmi, struct i2c_msg *msg) ...@@ -124,14 +133,12 @@ static int xfer_msg(struct sun4i_hdmi *hdmi, struct i2c_msg *msg)
} }
/* Wait for command to finish */ /* Wait for command to finish */
if (readl_poll_timeout(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG, if (regmap_field_read_poll_timeout(hdmi->field_ddc_start,
reg, reg, !reg, 100, 100000))
!(reg & SUN4I_HDMI_DDC_CTRL_START_CMD),
100, 100000))
return -EIO; return -EIO;
/* Check for errors */ /* Check for errors */
reg = readl(hdmi->base + SUN4I_HDMI_DDC_INT_STATUS_REG); regmap_field_read(hdmi->field_ddc_int_status, &reg);
if ((reg & SUN4I_HDMI_DDC_INT_STATUS_ERROR_MASK) || if ((reg & SUN4I_HDMI_DDC_INT_STATUS_ERROR_MASK) ||
!(reg & SUN4I_HDMI_DDC_INT_STATUS_TRANSFER_COMPLETE)) { !(reg & SUN4I_HDMI_DDC_INT_STATUS_TRANSFER_COMPLETE)) {
return -EIO; return -EIO;
...@@ -154,20 +161,21 @@ static int sun4i_hdmi_i2c_xfer(struct i2c_adapter *adap, ...@@ -154,20 +161,21 @@ static int sun4i_hdmi_i2c_xfer(struct i2c_adapter *adap,
return -EINVAL; return -EINVAL;
} }
/* DDC clock needs to be enabled for the module to work */
clk_prepare_enable(hdmi->ddc_clk);
clk_set_rate(hdmi->ddc_clk, 100000);
/* Reset I2C controller */ /* Reset I2C controller */
writel(SUN4I_HDMI_DDC_CTRL_ENABLE | SUN4I_HDMI_DDC_CTRL_RESET, regmap_field_write(hdmi->field_ddc_en, 1);
hdmi->base + SUN4I_HDMI_DDC_CTRL_REG); regmap_field_write(hdmi->field_ddc_reset, 1);
if (readl_poll_timeout(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG, reg, if (regmap_field_read_poll_timeout(hdmi->field_ddc_reset,
!(reg & SUN4I_HDMI_DDC_CTRL_RESET), reg, !reg, 100, 2000)) {
100, 2000)) clk_disable_unprepare(hdmi->ddc_clk);
return -EIO; return -EIO;
}
writel(SUN4I_HDMI_DDC_LINE_CTRL_SDA_ENABLE | regmap_field_write(hdmi->field_ddc_sck_en, 1);
SUN4I_HDMI_DDC_LINE_CTRL_SCL_ENABLE, regmap_field_write(hdmi->field_ddc_sda_en, 1);
hdmi->base + SUN4I_HDMI_DDC_LINE_CTRL_REG);
clk_prepare_enable(hdmi->ddc_clk);
clk_set_rate(hdmi->ddc_clk, 100000);
for (i = 0; i < num; i++) { for (i = 0; i < num; i++) {
err = xfer_msg(hdmi, &msgs[i]); err = xfer_msg(hdmi, &msgs[i]);
...@@ -191,12 +199,105 @@ static const struct i2c_algorithm sun4i_hdmi_i2c_algorithm = { ...@@ -191,12 +199,105 @@ static const struct i2c_algorithm sun4i_hdmi_i2c_algorithm = {
.functionality = sun4i_hdmi_i2c_func, .functionality = sun4i_hdmi_i2c_func,
}; };
static int sun4i_hdmi_init_regmap_fields(struct sun4i_hdmi *hdmi)
{
hdmi->field_ddc_en =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_en);
if (IS_ERR(hdmi->field_ddc_en))
return PTR_ERR(hdmi->field_ddc_en);
hdmi->field_ddc_start =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_start);
if (IS_ERR(hdmi->field_ddc_start))
return PTR_ERR(hdmi->field_ddc_start);
hdmi->field_ddc_reset =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_reset);
if (IS_ERR(hdmi->field_ddc_reset))
return PTR_ERR(hdmi->field_ddc_reset);
hdmi->field_ddc_addr_reg =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_addr_reg);
if (IS_ERR(hdmi->field_ddc_addr_reg))
return PTR_ERR(hdmi->field_ddc_addr_reg);
hdmi->field_ddc_slave_addr =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_slave_addr);
if (IS_ERR(hdmi->field_ddc_slave_addr))
return PTR_ERR(hdmi->field_ddc_slave_addr);
hdmi->field_ddc_int_mask =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_int_mask);
if (IS_ERR(hdmi->field_ddc_int_mask))
return PTR_ERR(hdmi->field_ddc_int_mask);
hdmi->field_ddc_int_status =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_int_status);
if (IS_ERR(hdmi->field_ddc_int_status))
return PTR_ERR(hdmi->field_ddc_int_status);
hdmi->field_ddc_fifo_clear =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_fifo_clear);
if (IS_ERR(hdmi->field_ddc_fifo_clear))
return PTR_ERR(hdmi->field_ddc_fifo_clear);
hdmi->field_ddc_fifo_rx_thres =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_fifo_rx_thres);
if (IS_ERR(hdmi->field_ddc_fifo_rx_thres))
return PTR_ERR(hdmi->field_ddc_fifo_rx_thres);
hdmi->field_ddc_fifo_tx_thres =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_fifo_tx_thres);
if (IS_ERR(hdmi->field_ddc_fifo_tx_thres))
return PTR_ERR(hdmi->field_ddc_fifo_tx_thres);
hdmi->field_ddc_byte_count =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_byte_count);
if (IS_ERR(hdmi->field_ddc_byte_count))
return PTR_ERR(hdmi->field_ddc_byte_count);
hdmi->field_ddc_cmd =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_cmd);
if (IS_ERR(hdmi->field_ddc_cmd))
return PTR_ERR(hdmi->field_ddc_cmd);
hdmi->field_ddc_sda_en =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_sda_en);
if (IS_ERR(hdmi->field_ddc_sda_en))
return PTR_ERR(hdmi->field_ddc_sda_en);
hdmi->field_ddc_sck_en =
devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
hdmi->variant->field_ddc_sck_en);
if (IS_ERR(hdmi->field_ddc_sck_en))
return PTR_ERR(hdmi->field_ddc_sck_en);
return 0;
}
int sun4i_hdmi_i2c_create(struct device *dev, struct sun4i_hdmi *hdmi) int sun4i_hdmi_i2c_create(struct device *dev, struct sun4i_hdmi *hdmi)
{ {
struct i2c_adapter *adap; struct i2c_adapter *adap;
int ret = 0; int ret = 0;
ret = sun4i_ddc_create(hdmi, hdmi->tmds_clk); ret = sun4i_ddc_create(hdmi, hdmi->ddc_parent_clk);
if (ret)
return ret;
ret = sun4i_hdmi_init_regmap_fields(hdmi);
if (ret) if (ret)
return ret; return ret;
......
...@@ -18,6 +18,8 @@ ...@@ -18,6 +18,8 @@
struct sun4i_tmds { struct sun4i_tmds {
struct clk_hw hw; struct clk_hw hw;
struct sun4i_hdmi *hdmi; struct sun4i_hdmi *hdmi;
u8 div_offset;
}; };
static inline struct sun4i_tmds *hw_to_tmds(struct clk_hw *hw) 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) ...@@ -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, static unsigned long sun4i_tmds_calc_divider(unsigned long rate,
unsigned long parent_rate, unsigned long parent_rate,
u8 div_offset,
u8 *div, u8 *div,
bool *half) bool *half)
{ {
...@@ -35,7 +38,7 @@ static unsigned long sun4i_tmds_calc_divider(unsigned long rate, ...@@ -35,7 +38,7 @@ static unsigned long sun4i_tmds_calc_divider(unsigned long rate,
u8 best_m = 0, m; u8 best_m = 0, m;
bool is_double; bool is_double;
for (m = 1; m < 16; m++) { for (m = div_offset ?: 1; m < (16 + div_offset); m++) {
u8 d; u8 d;
for (d = 1; d < 3; d++) { for (d = 1; d < 3; d++) {
...@@ -67,6 +70,7 @@ static unsigned long sun4i_tmds_calc_divider(unsigned long rate, ...@@ -67,6 +70,7 @@ static unsigned long sun4i_tmds_calc_divider(unsigned long rate,
static int sun4i_tmds_determine_rate(struct clk_hw *hw, static int sun4i_tmds_determine_rate(struct clk_hw *hw,
struct clk_rate_request *req) struct clk_rate_request *req)
{ {
struct sun4i_tmds *tmds = hw_to_tmds(hw);
struct clk_hw *parent = NULL; struct clk_hw *parent = NULL;
unsigned long best_parent = 0; unsigned long best_parent = 0;
unsigned long rate = req->rate; unsigned long rate = req->rate;
...@@ -85,7 +89,8 @@ static int sun4i_tmds_determine_rate(struct clk_hw *hw, ...@@ -85,7 +89,8 @@ static int sun4i_tmds_determine_rate(struct clk_hw *hw,
continue; continue;
for (i = 1; i < 3; i++) { 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 ideal = rate * i * j;
unsigned long rounded; unsigned long rounded;
...@@ -129,7 +134,7 @@ static unsigned long sun4i_tmds_recalc_rate(struct clk_hw *hw, ...@@ -129,7 +134,7 @@ static unsigned long sun4i_tmds_recalc_rate(struct clk_hw *hw,
parent_rate /= 2; parent_rate /= 2;
reg = readl(tmds->hdmi->base + SUN4I_HDMI_PLL_CTRL_REG); reg = readl(tmds->hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
reg = (reg >> 4) & 0xf; reg = ((reg >> 4) & 0xf) + tmds->div_offset;
if (!reg) if (!reg)
reg = 1; reg = 1;
...@@ -144,7 +149,8 @@ static int sun4i_tmds_set_rate(struct clk_hw *hw, unsigned long rate, ...@@ -144,7 +149,8 @@ static int sun4i_tmds_set_rate(struct clk_hw *hw, unsigned long rate,
u32 reg; u32 reg;
u8 div; 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 = readl(tmds->hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
reg &= ~SUN4I_HDMI_PAD_CTRL1_HALVE_CLK; reg &= ~SUN4I_HDMI_PAD_CTRL1_HALVE_CLK;
...@@ -154,7 +160,7 @@ static int sun4i_tmds_set_rate(struct clk_hw *hw, unsigned long rate, ...@@ -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 = readl(tmds->hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
reg &= ~SUN4I_HDMI_PLL_CTRL_DIV_MASK; 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); tmds->hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
return 0; return 0;
...@@ -221,6 +227,7 @@ int sun4i_tmds_create(struct sun4i_hdmi *hdmi) ...@@ -221,6 +227,7 @@ int sun4i_tmds_create(struct sun4i_hdmi *hdmi)
tmds->hdmi = hdmi; tmds->hdmi = hdmi;
tmds->hw.init = &init; tmds->hw.init = &init;
tmds->div_offset = hdmi->variant->tmds_clk_div_offset;
hdmi->tmds_clk = devm_clk_register(hdmi->dev, &tmds->hw); hdmi->tmds_clk = devm_clk_register(hdmi->dev, &tmds->hw);
if (IS_ERR(hdmi->tmds_clk)) 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