From 24edf1dbd1ea0935d425333ab1e1041de2b4874c Mon Sep 17 00:00:00 2001
From: Jernej Skrabec <jernej.skrabec@gmail.com>
Date: Mon, 11 Oct 2021 20:13:41 +0200
Subject: [PATCH 23/23] HACK: SW CEC implementation for H3

Many H3 boards lack 32768 Hz external oscillator, so internal RC is used
instead. However, it's too unstable for CEC. Use SW implementation
instead. That makes it usable, albeit sensitive to cpufreq changes.

Signed-off-by: Jernej Skrabec <jernej.skrabec@gmail.com>
---
 drivers/gpu/drm/sun4i/Kconfig          |  2 +
 drivers/gpu/drm/sun4i/sun8i_dw_hdmi.c  |  4 ++
 drivers/gpu/drm/sun4i/sun8i_dw_hdmi.h  | 14 ++++
 drivers/gpu/drm/sun4i/sun8i_hdmi_phy.c | 98 +++++++++++++++++++++++++-
 4 files changed, 116 insertions(+), 2 deletions(-)

diff --git a/drivers/gpu/drm/sun4i/Kconfig b/drivers/gpu/drm/sun4i/Kconfig
index 4741d9f6544c..4dac2dddf707 100644
--- a/drivers/gpu/drm/sun4i/Kconfig
+++ b/drivers/gpu/drm/sun4i/Kconfig
@@ -59,6 +59,8 @@ config DRM_SUN8I_DW_HDMI
 	depends on DRM_SUN4I
 	default DRM_SUN4I
 	select DRM_DW_HDMI
+	select CEC_CORE
+	select CEC_PIN
 	help
 	  Choose this option if you have an Allwinner SoC with the
 	  DesignWare HDMI controller. SoCs that support HDMI and
diff --git a/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.c b/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.c
index 7309590feb56..bde6b81def55 100644
--- a/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.c
+++ b/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.c
@@ -273,6 +273,10 @@ static int sun8i_dw_hdmi_bind(struct device *dev, struct device *master,
 		return dev_err_probe(dev, PTR_ERR(phy),
 				     "Couldn't get the HDMI PHY\n");
 
+	ret = sun8i_hdmi_phy_register_cec(phy, dev);
+	if (ret)
+		return ret;
+
 	hdmi = drmm_kzalloc(drm, sizeof(*hdmi), GFP_KERNEL);
 	if (!hdmi)
 		return -ENOMEM;
diff --git a/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.h b/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.h
index 5383d9267a4d..9d1158f24ddb 100644
--- a/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.h
+++ b/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.h
@@ -13,6 +13,8 @@
 #include <linux/regmap.h>
 #include <linux/regulator/consumer.h>
 #include <linux/reset.h>
+#include <media/cec-notifier.h>
+#include <media/cec-pin.h>
 
 #define SUN8I_HDMI_PHY_DBG_CTRL_REG	0x0000
 #define SUN8I_HDMI_PHY_DBG_CTRL_PX_LOCK		BIT(0)
@@ -145,6 +147,13 @@
 #define SUN8I_HDMI_PHY_ANA_STS_RCAL_MASK	GENMASK(5, 0)
 
 #define SUN8I_HDMI_PHY_CEC_REG		0x003c
+#define SUN8I_HDMI_PHY_CEC_PIN_CTRL		BIT(7)
+/*
+ * Documentation says that this bit is output enable. However,
+ * it seems that this bit is actually output disable.
+ */
+#define SUN8I_HDMI_PHY_CEC_OUT_DIS		BIT(2)
+#define SUN8I_HDMI_PHY_CEC_IN_DATA		BIT(1)
 
 struct sun8i_hdmi_phy;
 
@@ -159,6 +168,8 @@ struct sun8i_hdmi_phy_variant {
 };
 
 struct sun8i_hdmi_phy {
+	struct cec_adapter		*cec_adapter;
+	struct cec_notifier		*cec_notifier;
 	struct clk			*clk_bus;
 	struct clk			*clk_mod;
 	struct clk			*clk_phy;
@@ -169,6 +180,8 @@ struct sun8i_hdmi_phy {
 	struct regmap			*regs;
 	struct reset_control		*rst_phy;
 	const struct sun8i_hdmi_phy_variant *variant;
+	unsigned int 			disable_cec : 1;
+	unsigned int 			bit_bang_cec : 1;
 };
 
 struct sun8i_dw_hdmi_quirks {
@@ -211,5 +224,6 @@ void sun8i_hdmi_phy_set_ops(struct sun8i_hdmi_phy *phy,
 
 int sun8i_phy_clk_create(struct sun8i_hdmi_phy *phy, struct device *dev,
 			 bool second_parent);
+int sun8i_hdmi_phy_register_cec(struct sun8i_hdmi_phy *phy, struct device *dev);
 
 #endif /* _SUN8I_DW_HDMI_H_ */
diff --git a/drivers/gpu/drm/sun4i/sun8i_hdmi_phy.c b/drivers/gpu/drm/sun4i/sun8i_hdmi_phy.c
index 581233d6eaf2..a5771b5d9b67 100644
--- a/drivers/gpu/drm/sun4i/sun8i_hdmi_phy.c
+++ b/drivers/gpu/drm/sun4i/sun8i_hdmi_phy.c
@@ -507,8 +507,9 @@ static void sun8i_hdmi_phy_init_h3(struct sun8i_hdmi_phy *phy)
 	regmap_update_bits(phy->regs, SUN8I_HDMI_PHY_PLL_CFG1_REG,
 			   SUN8I_HDMI_PHY_PLL_CFG1_CKIN_SEL_MSK, 0);
 
-	/* set HW control of CEC pins */
-	regmap_write(phy->regs, SUN8I_HDMI_PHY_CEC_REG, 0);
+	/* manual control of CEC pins */
+	regmap_write(phy->regs, SUN8I_HDMI_PHY_CEC_REG,
+		     phy->bit_bang_cec ? SUN8I_HDMI_PHY_CEC_PIN_CTRL : 0);
 
 	/* read calibration data */
 	regmap_read(phy->regs, SUN8I_HDMI_PHY_ANA_STS_REG, &val);
@@ -585,8 +586,47 @@ void sun8i_hdmi_phy_set_ops(struct sun8i_hdmi_phy *phy,
 		plat_data->cur_ctr = variant->cur_ctr;
 		plat_data->phy_config = variant->phy_cfg;
 	}
+	plat_data->disable_cec = phy->disable_cec;
 }
 
+static int sun8i_hdmi_phy_cec_pin_read(struct cec_adapter *adap)
+{
+	struct sun8i_hdmi_phy *phy = cec_get_drvdata(adap);
+	unsigned int val;
+
+	regmap_read(phy->regs, SUN8I_HDMI_PHY_CEC_REG, &val);
+
+	return val & SUN8I_HDMI_PHY_CEC_IN_DATA;
+}
+
+static void sun8i_hdmi_phy_cec_pin_low(struct cec_adapter *adap)
+{
+	struct sun8i_hdmi_phy *phy = cec_get_drvdata(adap);
+
+	/* Start driving the CEC pin low */
+	regmap_write(phy->regs, SUN8I_HDMI_PHY_CEC_REG,
+		     SUN8I_HDMI_PHY_CEC_PIN_CTRL);
+}
+
+static void sun8i_hdmi_phy_cec_pin_high(struct cec_adapter *adap)
+{
+	struct sun8i_hdmi_phy *phy = cec_get_drvdata(adap);
+
+	/*
+	 * Stop driving the CEC pin, the pull up will take over
+	 * unless another CEC device is driving the pin low.
+	 */
+	regmap_write(phy->regs, SUN8I_HDMI_PHY_CEC_REG,
+		     SUN8I_HDMI_PHY_CEC_PIN_CTRL |
+		     SUN8I_HDMI_PHY_CEC_OUT_DIS);
+}
+
+static const struct cec_pin_ops sun8i_hdmi_phy_cec_pin_ops = {
+	.read = sun8i_hdmi_phy_cec_pin_read,
+	.low = sun8i_hdmi_phy_cec_pin_low,
+	.high = sun8i_hdmi_phy_cec_pin_high,
+};
+
 static const struct regmap_config sun8i_hdmi_phy_regmap_config = {
 	.reg_bits	= 32,
 	.val_bits	= 32,
@@ -669,6 +709,41 @@ struct sun8i_hdmi_phy *sun8i_hdmi_phy_get(struct device_node *node)
 	return phy;
 }
 
+int sun8i_hdmi_phy_register_cec(struct sun8i_hdmi_phy *phy, struct device *dev)
+{
+	int ret;
+
+	if (!phy->bit_bang_cec)
+		return 0;
+
+	phy->cec_adapter =
+		cec_pin_allocate_adapter(&sun8i_hdmi_phy_cec_pin_ops,
+						phy, "sun8i-cec",
+						CEC_CAP_DEFAULTS);
+	if (IS_ERR(phy->cec_adapter))
+		return PTR_ERR(phy->cec_adapter);
+
+	phy->cec_notifier = cec_notifier_cec_adap_register(dev, NULL,
+							   phy->cec_adapter);
+	if (!phy->cec_notifier) {
+		ret = -ENOMEM;
+		goto err_delete_cec_adapter;
+	}
+
+	ret = cec_register_adapter(phy->cec_adapter, dev);
+	if (ret < 0)
+		goto err_put_cec_notifier;
+
+	return 0;
+
+err_put_cec_notifier:
+	cec_notifier_cec_adap_unregister(phy->cec_notifier, phy->cec_adapter);
+err_delete_cec_adapter:
+	cec_delete_adapter(phy->cec_adapter);
+
+	return ret;
+}
+
 static int sun8i_hdmi_phy_probe(struct platform_device *pdev)
 {
 	struct device *dev = &pdev->dev;
@@ -681,6 +756,15 @@ static int sun8i_hdmi_phy_probe(struct platform_device *pdev)
 
 	phy->variant = of_device_get_match_data(dev);
 	phy->dev = dev;
+	phy->disable_cec = of_machine_is_compatible("roofull,beelink-x2") ||
+			   of_machine_is_compatible("friendlyarm,nanopi-m1") ||
+			   of_machine_is_compatible("xunlong,orangepi-lite") ||
+			   of_machine_is_compatible("xunlong,orangepi-one") ||
+			   of_machine_is_compatible("xunlong,orangepi-pc") ||
+			   of_machine_is_compatible("xunlong,orangepi-pc-plus") ||
+			   of_machine_is_compatible("xunlong,orangepi-plus2e");
+	phy->bit_bang_cec = phy->disable_cec &&
+			    !of_machine_is_compatible("roofull,beelink-x2");
 
 	regs = devm_platform_ioremap_resource(pdev, 0);
 	if (IS_ERR(regs))
@@ -727,8 +810,17 @@ static int sun8i_hdmi_phy_probe(struct platform_device *pdev)
 	return 0;
 }
 
+static void sun8i_hdmi_phy_remove(struct platform_device *pdev)
+{
+	struct sun8i_hdmi_phy *phy = platform_get_drvdata(pdev);
+
+	cec_notifier_cec_adap_unregister(phy->cec_notifier, phy->cec_adapter);
+	cec_unregister_adapter(phy->cec_adapter);
+}
+
 struct platform_driver sun8i_hdmi_phy_driver = {
 	.probe  = sun8i_hdmi_phy_probe,
+	.remove = sun8i_hdmi_phy_remove,
 	.driver = {
 		.name = "sun8i-hdmi-phy",
 		.of_match_table = sun8i_hdmi_phy_of_table,
-- 
2.42.0

