Commit bb3b6fcb authored by Maxime Ripard's avatar Maxime Ripard

sun6i: dsi: Convert to generic phy handling

Now that we have everything in place in the PHY framework to deal in a
generic way with MIPI D-PHY phys, let's convert our PHY driver and its
associated DSI driver to that new API.
Reviewed-by: default avatarPaul Kocialkowski <paul.kocialkowski@bootlin.com>
Signed-off-by: default avatarMaxime Ripard <maxime.ripard@bootlin.com>
Link: https://patchwork.freedesktop.org/patch/msgid/dc6450e2978b6dafcc464595ad06204d22d2658f.1548085432.git-series.maxime.ripard@bootlin.com
parent 1eb6ea4a
...@@ -45,10 +45,19 @@ config DRM_SUN6I_DSI ...@@ -45,10 +45,19 @@ config DRM_SUN6I_DSI
default MACH_SUN8I default MACH_SUN8I
select CRC_CCITT select CRC_CCITT
select DRM_MIPI_DSI select DRM_MIPI_DSI
select DRM_SUN6I_DPHY
help help
Choose this option if you want have an Allwinner SoC with Choose this option if you want have an Allwinner SoC with
MIPI-DSI support. If M is selected the module will be called MIPI-DSI support. If M is selected the module will be called
sun6i-dsi sun6i_mipi_dsi.
config DRM_SUN6I_DPHY
tristate "Allwinner A31 MIPI D-PHY Support"
select GENERIC_PHY_MIPI_DPHY
help
Choose this option if you have an Allwinner SoC with
MIPI-DSI support. If M is selected, the module will be
called sun6i_mipi_dphy.
config DRM_SUN8I_DW_HDMI config DRM_SUN8I_DW_HDMI
tristate "Support for Allwinner version of DesignWare HDMI" tristate "Support for Allwinner version of DesignWare HDMI"
......
...@@ -24,9 +24,6 @@ sun4i-tcon-y += sun4i_lvds.o ...@@ -24,9 +24,6 @@ sun4i-tcon-y += sun4i_lvds.o
sun4i-tcon-y += sun4i_tcon.o sun4i-tcon-y += sun4i_tcon.o
sun4i-tcon-y += sun4i_rgb.o sun4i-tcon-y += sun4i_rgb.o
sun6i-dsi-y += sun6i_mipi_dphy.o
sun6i-dsi-y += sun6i_mipi_dsi.o
obj-$(CONFIG_DRM_SUN4I) += sun4i-drm.o obj-$(CONFIG_DRM_SUN4I) += sun4i-drm.o
obj-$(CONFIG_DRM_SUN4I) += sun4i-tcon.o obj-$(CONFIG_DRM_SUN4I) += sun4i-tcon.o
obj-$(CONFIG_DRM_SUN4I) += sun4i_tv.o obj-$(CONFIG_DRM_SUN4I) += sun4i_tv.o
...@@ -37,7 +34,8 @@ ifdef CONFIG_DRM_SUN4I_BACKEND ...@@ -37,7 +34,8 @@ ifdef CONFIG_DRM_SUN4I_BACKEND
obj-$(CONFIG_DRM_SUN4I) += sun4i-frontend.o obj-$(CONFIG_DRM_SUN4I) += sun4i-frontend.o
endif endif
obj-$(CONFIG_DRM_SUN4I_HDMI) += sun4i-drm-hdmi.o obj-$(CONFIG_DRM_SUN4I_HDMI) += sun4i-drm-hdmi.o
obj-$(CONFIG_DRM_SUN6I_DSI) += sun6i-dsi.o obj-$(CONFIG_DRM_SUN6I_DPHY) += sun6i_mipi_dphy.o
obj-$(CONFIG_DRM_SUN6I_DSI) += sun6i_mipi_dsi.o
obj-$(CONFIG_DRM_SUN8I_DW_HDMI) += sun8i-drm-hdmi.o obj-$(CONFIG_DRM_SUN8I_DW_HDMI) += sun8i-drm-hdmi.o
obj-$(CONFIG_DRM_SUN8I_MIXER) += sun8i-mixer.o obj-$(CONFIG_DRM_SUN8I_MIXER) += sun8i-mixer.o
obj-$(CONFIG_DRM_SUN8I_TCON_TOP) += sun8i_tcon_top.o obj-$(CONFIG_DRM_SUN8I_TCON_TOP) += sun8i_tcon_top.o
...@@ -8,11 +8,14 @@ ...@@ -8,11 +8,14 @@
#include <linux/bitops.h> #include <linux/bitops.h>
#include <linux/clk.h> #include <linux/clk.h>
#include <linux/module.h>
#include <linux/of_address.h> #include <linux/of_address.h>
#include <linux/platform_device.h>
#include <linux/regmap.h> #include <linux/regmap.h>
#include <linux/reset.h> #include <linux/reset.h>
#include "sun6i_mipi_dsi.h" #include <linux/phy/phy.h>
#include <linux/phy/phy-mipi-dphy.h>
#define SUN6I_DPHY_GCTL_REG 0x00 #define SUN6I_DPHY_GCTL_REG 0x00
#define SUN6I_DPHY_GCTL_LANE_NUM(n) ((((n) - 1) & 3) << 4) #define SUN6I_DPHY_GCTL_LANE_NUM(n) ((((n) - 1) & 3) << 4)
...@@ -81,12 +84,46 @@ ...@@ -81,12 +84,46 @@
#define SUN6I_DPHY_DBG5_REG 0xf4 #define SUN6I_DPHY_DBG5_REG 0xf4
int sun6i_dphy_init(struct sun6i_dphy *dphy, unsigned int lanes) struct sun6i_dphy {
struct clk *bus_clk;
struct clk *mod_clk;
struct regmap *regs;
struct reset_control *reset;
struct phy *phy;
struct phy_configure_opts_mipi_dphy config;
};
static int sun6i_dphy_init(struct phy *phy)
{ {
struct sun6i_dphy *dphy = phy_get_drvdata(phy);
reset_control_deassert(dphy->reset); reset_control_deassert(dphy->reset);
clk_prepare_enable(dphy->mod_clk); clk_prepare_enable(dphy->mod_clk);
clk_set_rate_exclusive(dphy->mod_clk, 150000000); clk_set_rate_exclusive(dphy->mod_clk, 150000000);
return 0;
}
static int sun6i_dphy_configure(struct phy *phy, union phy_configure_opts *opts)
{
struct sun6i_dphy *dphy = phy_get_drvdata(phy);
int ret;
ret = phy_mipi_dphy_config_validate(&opts->mipi_dphy);
if (ret)
return ret;
memcpy(&dphy->config, opts, sizeof(dphy->config));
return 0;
}
static int sun6i_dphy_power_on(struct phy *phy)
{
struct sun6i_dphy *dphy = phy_get_drvdata(phy);
u8 lanes_mask = GENMASK(dphy->config.lanes - 1, 0);
regmap_write(dphy->regs, SUN6I_DPHY_TX_CTL_REG, regmap_write(dphy->regs, SUN6I_DPHY_TX_CTL_REG,
SUN6I_DPHY_TX_CTL_HS_TX_CLK_CONT); SUN6I_DPHY_TX_CTL_HS_TX_CLK_CONT);
...@@ -111,16 +148,9 @@ int sun6i_dphy_init(struct sun6i_dphy *dphy, unsigned int lanes) ...@@ -111,16 +148,9 @@ int sun6i_dphy_init(struct sun6i_dphy *dphy, unsigned int lanes)
SUN6I_DPHY_TX_TIME4_HS_TX_ANA1(3)); SUN6I_DPHY_TX_TIME4_HS_TX_ANA1(3));
regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG, regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG,
SUN6I_DPHY_GCTL_LANE_NUM(lanes) | SUN6I_DPHY_GCTL_LANE_NUM(dphy->config.lanes) |
SUN6I_DPHY_GCTL_EN); SUN6I_DPHY_GCTL_EN);
return 0;
}
int sun6i_dphy_power_on(struct sun6i_dphy *dphy, unsigned int lanes)
{
u8 lanes_mask = GENMASK(lanes - 1, 0);
regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG, regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG,
SUN6I_DPHY_ANA0_REG_PWS | SUN6I_DPHY_ANA0_REG_PWS |
SUN6I_DPHY_ANA0_REG_DMPC | SUN6I_DPHY_ANA0_REG_DMPC |
...@@ -181,16 +211,20 @@ int sun6i_dphy_power_on(struct sun6i_dphy *dphy, unsigned int lanes) ...@@ -181,16 +211,20 @@ int sun6i_dphy_power_on(struct sun6i_dphy *dphy, unsigned int lanes)
return 0; return 0;
} }
int sun6i_dphy_power_off(struct sun6i_dphy *dphy) static int sun6i_dphy_power_off(struct phy *phy)
{ {
struct sun6i_dphy *dphy = phy_get_drvdata(phy);
regmap_update_bits(dphy->regs, SUN6I_DPHY_ANA1_REG, regmap_update_bits(dphy->regs, SUN6I_DPHY_ANA1_REG,
SUN6I_DPHY_ANA1_REG_VTTMODE, 0); SUN6I_DPHY_ANA1_REG_VTTMODE, 0);
return 0; return 0;
} }
int sun6i_dphy_exit(struct sun6i_dphy *dphy) static int sun6i_dphy_exit(struct phy *phy)
{ {
struct sun6i_dphy *dphy = phy_get_drvdata(phy);
clk_rate_exclusive_put(dphy->mod_clk); clk_rate_exclusive_put(dphy->mod_clk);
clk_disable_unprepare(dphy->mod_clk); clk_disable_unprepare(dphy->mod_clk);
reset_control_assert(dphy->reset); reset_control_assert(dphy->reset);
...@@ -198,6 +232,15 @@ int sun6i_dphy_exit(struct sun6i_dphy *dphy) ...@@ -198,6 +232,15 @@ int sun6i_dphy_exit(struct sun6i_dphy *dphy)
return 0; return 0;
} }
static struct phy_ops sun6i_dphy_ops = {
.configure = sun6i_dphy_configure,
.power_on = sun6i_dphy_power_on,
.power_off = sun6i_dphy_power_off,
.init = sun6i_dphy_init,
.exit = sun6i_dphy_exit,
};
static struct regmap_config sun6i_dphy_regmap_config = { static struct regmap_config sun6i_dphy_regmap_config = {
.reg_bits = 32, .reg_bits = 32,
.val_bits = 32, .val_bits = 32,
...@@ -206,87 +249,70 @@ static struct regmap_config sun6i_dphy_regmap_config = { ...@@ -206,87 +249,70 @@ static struct regmap_config sun6i_dphy_regmap_config = {
.name = "mipi-dphy", .name = "mipi-dphy",
}; };
static const struct of_device_id sun6i_dphy_of_table[] = { static int sun6i_dphy_probe(struct platform_device *pdev)
{ .compatible = "allwinner,sun6i-a31-mipi-dphy" },
{ }
};
int sun6i_dphy_probe(struct sun6i_dsi *dsi, struct device_node *node)
{ {
struct phy_provider *phy_provider;
struct sun6i_dphy *dphy; struct sun6i_dphy *dphy;
struct resource res; struct resource *res;
void __iomem *regs; void __iomem *regs;
int ret;
if (!of_match_node(sun6i_dphy_of_table, node)) {
dev_err(dsi->dev, "Incompatible D-PHY\n");
return -EINVAL;
}
dphy = devm_kzalloc(dsi->dev, sizeof(*dphy), GFP_KERNEL); dphy = devm_kzalloc(&pdev->dev, sizeof(*dphy), GFP_KERNEL);
if (!dphy) if (!dphy)
return -ENOMEM; return -ENOMEM;
ret = of_address_to_resource(node, 0, &res); res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (ret) { regs = devm_ioremap_resource(&pdev->dev, res);
dev_err(dsi->dev, "phy: Couldn't get our resources\n");
return ret;
}
regs = devm_ioremap_resource(dsi->dev, &res);
if (IS_ERR(regs)) { if (IS_ERR(regs)) {
dev_err(dsi->dev, "Couldn't map the DPHY encoder registers\n"); dev_err(&pdev->dev, "Couldn't map the DPHY encoder registers\n");
return PTR_ERR(regs); return PTR_ERR(regs);
} }
dphy->regs = devm_regmap_init_mmio(dsi->dev, regs, dphy->regs = devm_regmap_init_mmio_clk(&pdev->dev, "bus",
&sun6i_dphy_regmap_config); regs, &sun6i_dphy_regmap_config);
if (IS_ERR(dphy->regs)) { if (IS_ERR(dphy->regs)) {
dev_err(dsi->dev, "Couldn't create the DPHY encoder regmap\n"); dev_err(&pdev->dev, "Couldn't create the DPHY encoder regmap\n");
return PTR_ERR(dphy->regs); return PTR_ERR(dphy->regs);
} }
dphy->reset = of_reset_control_get_shared(node, NULL); dphy->reset = devm_reset_control_get_shared(&pdev->dev, NULL);
if (IS_ERR(dphy->reset)) { if (IS_ERR(dphy->reset)) {
dev_err(dsi->dev, "Couldn't get our reset line\n"); dev_err(&pdev->dev, "Couldn't get our reset line\n");
return PTR_ERR(dphy->reset); return PTR_ERR(dphy->reset);
} }
dphy->bus_clk = of_clk_get_by_name(node, "bus"); dphy->mod_clk = devm_clk_get(&pdev->dev, "mod");
if (IS_ERR(dphy->bus_clk)) {
dev_err(dsi->dev, "Couldn't get the DPHY bus clock\n");
ret = PTR_ERR(dphy->bus_clk);
goto err_free_reset;
}
regmap_mmio_attach_clk(dphy->regs, dphy->bus_clk);
dphy->mod_clk = of_clk_get_by_name(node, "mod");
if (IS_ERR(dphy->mod_clk)) { if (IS_ERR(dphy->mod_clk)) {
dev_err(dsi->dev, "Couldn't get the DPHY mod clock\n"); dev_err(&pdev->dev, "Couldn't get the DPHY mod clock\n");
ret = PTR_ERR(dphy->mod_clk); return PTR_ERR(dphy->mod_clk);
goto err_free_bus;
} }
dsi->dphy = dphy; dphy->phy = devm_phy_create(&pdev->dev, NULL, &sun6i_dphy_ops);
if (IS_ERR(dphy->phy)) {
dev_err(&pdev->dev, "failed to create PHY\n");
return PTR_ERR(dphy->phy);
}
return 0; phy_set_drvdata(dphy->phy, dphy);
phy_provider = devm_of_phy_provider_register(&pdev->dev, of_phy_simple_xlate);
err_free_bus: return PTR_ERR_OR_ZERO(phy_provider);
regmap_mmio_detach_clk(dphy->regs);
clk_put(dphy->bus_clk);
err_free_reset:
reset_control_put(dphy->reset);
return ret;
} }
int sun6i_dphy_remove(struct sun6i_dsi *dsi) static const struct of_device_id sun6i_dphy_of_table[] = {
{ { .compatible = "allwinner,sun6i-a31-mipi-dphy" },
struct sun6i_dphy *dphy = dsi->dphy; { }
};
regmap_mmio_detach_clk(dphy->regs); MODULE_DEVICE_TABLE(of, sun6i_dphy_of_table);
clk_put(dphy->mod_clk);
clk_put(dphy->bus_clk); static struct platform_driver sun6i_dphy_platform_driver = {
reset_control_put(dphy->reset); .probe = sun6i_dphy_probe,
.driver = {
.name = "sun6i-mipi-dphy",
.of_match_table = sun6i_dphy_of_table,
},
};
module_platform_driver(sun6i_dphy_platform_driver);
return 0; MODULE_AUTHOR("Maxime Ripard <maxime.ripard@bootlin>");
} MODULE_DESCRIPTION("Allwinner A31 MIPI D-PHY Driver");
MODULE_LICENSE("GPL");
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
#include <linux/slab.h> #include <linux/slab.h>
#include <linux/phy/phy.h> #include <linux/phy/phy.h>
#include <linux/phy/phy-mipi-dphy.h>
#include <drm/drmP.h> #include <drm/drmP.h>
#include <drm/drm_atomic_helper.h> #include <drm/drm_atomic_helper.h>
...@@ -616,6 +617,8 @@ static void sun6i_dsi_encoder_enable(struct drm_encoder *encoder) ...@@ -616,6 +617,8 @@ static void sun6i_dsi_encoder_enable(struct drm_encoder *encoder)
struct drm_display_mode *mode = &encoder->crtc->state->adjusted_mode; struct drm_display_mode *mode = &encoder->crtc->state->adjusted_mode;
struct sun6i_dsi *dsi = encoder_to_sun6i_dsi(encoder); struct sun6i_dsi *dsi = encoder_to_sun6i_dsi(encoder);
struct mipi_dsi_device *device = dsi->device; struct mipi_dsi_device *device = dsi->device;
union phy_configure_opts opts = { 0 };
struct phy_configure_opts_mipi_dphy *cfg = &opts.mipi_dphy;
u16 delay; u16 delay;
DRM_DEBUG_DRIVER("Enabling DSI output\n"); DRM_DEBUG_DRIVER("Enabling DSI output\n");
...@@ -634,8 +637,15 @@ static void sun6i_dsi_encoder_enable(struct drm_encoder *encoder) ...@@ -634,8 +637,15 @@ static void sun6i_dsi_encoder_enable(struct drm_encoder *encoder)
sun6i_dsi_setup_format(dsi, mode); sun6i_dsi_setup_format(dsi, mode);
sun6i_dsi_setup_timings(dsi, mode); sun6i_dsi_setup_timings(dsi, mode);
sun6i_dphy_init(dsi->dphy, device->lanes); phy_init(dsi->dphy);
sun6i_dphy_power_on(dsi->dphy, device->lanes);
phy_mipi_dphy_get_default_config(mode->clock * 1000,
mipi_dsi_pixel_format_to_bpp(device->format),
device->lanes, cfg);
phy_set_mode(dsi->dphy, PHY_MODE_MIPI_DPHY);
phy_configure(dsi->dphy, &opts);
phy_power_on(dsi->dphy);
if (!IS_ERR(dsi->panel)) if (!IS_ERR(dsi->panel))
drm_panel_prepare(dsi->panel); drm_panel_prepare(dsi->panel);
...@@ -673,8 +683,8 @@ static void sun6i_dsi_encoder_disable(struct drm_encoder *encoder) ...@@ -673,8 +683,8 @@ static void sun6i_dsi_encoder_disable(struct drm_encoder *encoder)
drm_panel_unprepare(dsi->panel); drm_panel_unprepare(dsi->panel);
} }
sun6i_dphy_power_off(dsi->dphy); phy_power_off(dsi->dphy);
sun6i_dphy_exit(dsi->dphy); phy_exit(dsi->dphy);
pm_runtime_put(dsi->dev); pm_runtime_put(dsi->dev);
} }
...@@ -967,7 +977,6 @@ static const struct component_ops sun6i_dsi_ops = { ...@@ -967,7 +977,6 @@ static const struct component_ops sun6i_dsi_ops = {
static int sun6i_dsi_probe(struct platform_device *pdev) static int sun6i_dsi_probe(struct platform_device *pdev)
{ {
struct device *dev = &pdev->dev; struct device *dev = &pdev->dev;
struct device_node *dphy_node;
struct sun6i_dsi *dsi; struct sun6i_dsi *dsi;
struct resource *res; struct resource *res;
void __iomem *base; void __iomem *base;
...@@ -1013,10 +1022,8 @@ static int sun6i_dsi_probe(struct platform_device *pdev) ...@@ -1013,10 +1022,8 @@ static int sun6i_dsi_probe(struct platform_device *pdev)
*/ */
clk_set_rate_exclusive(dsi->mod_clk, 297000000); clk_set_rate_exclusive(dsi->mod_clk, 297000000);
dphy_node = of_parse_phandle(dev->of_node, "phys", 0); dsi->dphy = devm_phy_get(dev, "dphy");
ret = sun6i_dphy_probe(dsi, dphy_node); if (IS_ERR(dsi->dphy)) {
of_node_put(dphy_node);
if (ret) {
dev_err(dev, "Couldn't get the MIPI D-PHY\n"); dev_err(dev, "Couldn't get the MIPI D-PHY\n");
goto err_unprotect_clk; goto err_unprotect_clk;
} }
...@@ -1026,7 +1033,7 @@ static int sun6i_dsi_probe(struct platform_device *pdev) ...@@ -1026,7 +1033,7 @@ static int sun6i_dsi_probe(struct platform_device *pdev)
ret = mipi_dsi_host_register(&dsi->host); ret = mipi_dsi_host_register(&dsi->host);
if (ret) { if (ret) {
dev_err(dev, "Couldn't register MIPI-DSI host\n"); dev_err(dev, "Couldn't register MIPI-DSI host\n");
goto err_remove_phy; goto err_pm_disable;
} }
ret = component_add(&pdev->dev, &sun6i_dsi_ops); ret = component_add(&pdev->dev, &sun6i_dsi_ops);
...@@ -1039,9 +1046,8 @@ static int sun6i_dsi_probe(struct platform_device *pdev) ...@@ -1039,9 +1046,8 @@ static int sun6i_dsi_probe(struct platform_device *pdev)
err_remove_dsi_host: err_remove_dsi_host:
mipi_dsi_host_unregister(&dsi->host); mipi_dsi_host_unregister(&dsi->host);
err_remove_phy: err_pm_disable:
pm_runtime_disable(dev); pm_runtime_disable(dev);
sun6i_dphy_remove(dsi);
err_unprotect_clk: err_unprotect_clk:
clk_rate_exclusive_put(dsi->mod_clk); clk_rate_exclusive_put(dsi->mod_clk);
return ret; return ret;
...@@ -1055,7 +1061,6 @@ static int sun6i_dsi_remove(struct platform_device *pdev) ...@@ -1055,7 +1061,6 @@ static int sun6i_dsi_remove(struct platform_device *pdev)
component_del(&pdev->dev, &sun6i_dsi_ops); component_del(&pdev->dev, &sun6i_dsi_ops);
mipi_dsi_host_unregister(&dsi->host); mipi_dsi_host_unregister(&dsi->host);
pm_runtime_disable(dev); pm_runtime_disable(dev);
sun6i_dphy_remove(dsi);
clk_rate_exclusive_put(dsi->mod_clk); clk_rate_exclusive_put(dsi->mod_clk);
return 0; return 0;
......
...@@ -13,13 +13,6 @@ ...@@ -13,13 +13,6 @@
#include <drm/drm_encoder.h> #include <drm/drm_encoder.h>
#include <drm/drm_mipi_dsi.h> #include <drm/drm_mipi_dsi.h>
struct sun6i_dphy {
struct clk *bus_clk;
struct clk *mod_clk;
struct regmap *regs;
struct reset_control *reset;
};
struct sun6i_dsi { struct sun6i_dsi {
struct drm_connector connector; struct drm_connector connector;
struct drm_encoder encoder; struct drm_encoder encoder;
...@@ -29,7 +22,7 @@ struct sun6i_dsi { ...@@ -29,7 +22,7 @@ struct sun6i_dsi {
struct clk *mod_clk; struct clk *mod_clk;
struct regmap *regs; struct regmap *regs;
struct reset_control *reset; struct reset_control *reset;
struct sun6i_dphy *dphy; struct phy *dphy;
struct device *dev; struct device *dev;
struct sun4i_drv *drv; struct sun4i_drv *drv;
...@@ -52,12 +45,4 @@ static inline struct sun6i_dsi *encoder_to_sun6i_dsi(const struct drm_encoder *e ...@@ -52,12 +45,4 @@ static inline struct sun6i_dsi *encoder_to_sun6i_dsi(const struct drm_encoder *e
return container_of(encoder, struct sun6i_dsi, encoder); return container_of(encoder, struct sun6i_dsi, encoder);
}; };
int sun6i_dphy_probe(struct sun6i_dsi *dsi, struct device_node *node);
int sun6i_dphy_remove(struct sun6i_dsi *dsi);
int sun6i_dphy_init(struct sun6i_dphy *dphy, unsigned int lanes);
int sun6i_dphy_power_on(struct sun6i_dphy *dphy, unsigned int lanes);
int sun6i_dphy_power_off(struct sun6i_dphy *dphy);
int sun6i_dphy_exit(struct sun6i_dphy *dphy);
#endif /* _SUN6I_MIPI_DSI_H_ */ #endif /* _SUN6I_MIPI_DSI_H_ */
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