| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (C) 2018 BayLibre, SAS |
| * Author: Jorge Ramirez-Ortiz <jramirez@baylibre.com> |
| */ |
| |
| #include <display.h> |
| #include <dm.h> |
| #include <edid.h> |
| #include <log.h> |
| #include <asm/io.h> |
| #include <dw_hdmi.h> |
| #include <dm/device-internal.h> |
| #include <dm/uclass-internal.h> |
| #include <linux/bitops.h> |
| #include <linux/printk.h> |
| #include <power/regulator.h> |
| #include <clk.h> |
| #include <linux/delay.h> |
| #include <reset.h> |
| #include <media_bus_format.h> |
| #include "meson_dw_hdmi.h" |
| #include "meson_vpu.h" |
| |
| /* TOP Block Communication Channel */ |
| #define HDMITX_TOP_ADDR_REG 0x0 |
| #define HDMITX_TOP_DATA_REG 0x4 |
| #define HDMITX_TOP_CTRL_REG 0x8 |
| #define HDMITX_TOP_G12A_OFFSET 0x8000 |
| |
| /* Controller Communication Channel */ |
| #define HDMITX_DWC_ADDR_REG 0x10 |
| #define HDMITX_DWC_DATA_REG 0x14 |
| #define HDMITX_DWC_CTRL_REG 0x18 |
| |
| /* HHI Registers */ |
| #define HHI_MEM_PD_REG0 0x100 /* 0x40 */ |
| #define HHI_HDMI_CLK_CNTL 0x1cc /* 0x73 */ |
| #define HHI_HDMI_PHY_CNTL0 0x3a0 /* 0xe8 */ |
| #define HHI_HDMI_PHY_CNTL1 0x3a4 /* 0xe9 */ |
| #define HHI_HDMI_PHY_CNTL2 0x3a8 /* 0xea */ |
| #define HHI_HDMI_PHY_CNTL3 0x3ac /* 0xeb */ |
| #define HHI_HDMI_PHY_CNTL4 0x3b0 /* 0xec */ |
| #define HHI_HDMI_PHY_CNTL5 0x3b4 /* 0xed */ |
| |
| struct meson_dw_hdmi { |
| struct udevice *dev; |
| struct dw_hdmi hdmi; |
| void __iomem *hhi_base; |
| }; |
| |
| enum hdmi_compatible { |
| HDMI_COMPATIBLE_GXBB = 0, |
| HDMI_COMPATIBLE_GXL = 1, |
| HDMI_COMPATIBLE_GXM = 2, |
| HDMI_COMPATIBLE_G12A = 3, |
| }; |
| |
| static inline bool meson_hdmi_is_compatible(struct meson_dw_hdmi *priv, |
| enum hdmi_compatible family) |
| { |
| enum hdmi_compatible compat = dev_get_driver_data(priv->dev); |
| |
| return compat == family; |
| } |
| |
| static unsigned int dw_hdmi_top_read(struct dw_hdmi *hdmi, unsigned int addr) |
| { |
| struct meson_dw_hdmi *priv = container_of(hdmi, struct meson_dw_hdmi, |
| hdmi); |
| unsigned int data; |
| |
| if (meson_hdmi_is_compatible(priv, HDMI_COMPATIBLE_G12A)) |
| return readl(hdmi->ioaddr + |
| HDMITX_TOP_G12A_OFFSET + (addr << 2)); |
| |
| /* ADDR must be written twice */ |
| writel(addr & 0xffff, hdmi->ioaddr + HDMITX_TOP_ADDR_REG); |
| writel(addr & 0xffff, hdmi->ioaddr + HDMITX_TOP_ADDR_REG); |
| |
| /* Read needs a second DATA read */ |
| data = readl(hdmi->ioaddr + HDMITX_TOP_DATA_REG); |
| data = readl(hdmi->ioaddr + HDMITX_TOP_DATA_REG); |
| |
| return data; |
| } |
| |
| static inline void dw_hdmi_top_write(struct dw_hdmi *hdmi, |
| unsigned int addr, unsigned int data) |
| { |
| struct meson_dw_hdmi *priv = container_of(hdmi, struct meson_dw_hdmi, |
| hdmi); |
| |
| if (meson_hdmi_is_compatible(priv, HDMI_COMPATIBLE_G12A)) { |
| writel(data, hdmi->ioaddr + |
| HDMITX_TOP_G12A_OFFSET + (addr << 2)); |
| return; |
| } |
| |
| /* ADDR must be written twice */ |
| writel(addr & 0xffff, hdmi->ioaddr + HDMITX_TOP_ADDR_REG); |
| writel(addr & 0xffff, hdmi->ioaddr + HDMITX_TOP_ADDR_REG); |
| |
| /* Write needs single DATA write */ |
| writel(data, hdmi->ioaddr + HDMITX_TOP_DATA_REG); |
| } |
| |
| static inline void dw_hdmi_top_write_bits(struct dw_hdmi *hdmi, |
| unsigned int addr, |
| unsigned int mask, |
| unsigned int val) |
| { |
| unsigned int data = dw_hdmi_top_read(hdmi, addr); |
| |
| data &= ~mask; |
| data |= val; |
| dw_hdmi_top_write(hdmi, addr, data); |
| } |
| |
| static u8 dw_hdmi_dwc_read(struct dw_hdmi *hdmi, int addr) |
| { |
| unsigned int data; |
| |
| /* ADDR must be written twice */ |
| writel(addr & 0xffff, hdmi->ioaddr + HDMITX_DWC_ADDR_REG); |
| writel(addr & 0xffff, hdmi->ioaddr + HDMITX_DWC_ADDR_REG); |
| |
| /* Read needs a second DATA read */ |
| data = readl(hdmi->ioaddr + HDMITX_DWC_DATA_REG); |
| data = readl(hdmi->ioaddr + HDMITX_DWC_DATA_REG); |
| |
| return data; |
| } |
| |
| static inline void dw_hdmi_dwc_write(struct dw_hdmi *hdmi, u8 data, int addr) |
| { |
| /* ADDR must be written twice */ |
| writel(addr & 0xffff, hdmi->ioaddr + HDMITX_DWC_ADDR_REG); |
| writel(addr & 0xffff, hdmi->ioaddr + HDMITX_DWC_ADDR_REG); |
| |
| /* Write needs single DATA write */ |
| writel(data, hdmi->ioaddr + HDMITX_DWC_DATA_REG); |
| } |
| |
| static inline void dw_hdmi_dwc_write_bits(struct dw_hdmi *hdmi, |
| unsigned int addr, |
| unsigned int mask, |
| unsigned int val) |
| { |
| u8 data = dw_hdmi_dwc_read(hdmi, addr); |
| |
| data &= ~mask; |
| data |= val; |
| |
| dw_hdmi_dwc_write(hdmi, data, addr); |
| } |
| |
| static inline void dw_hdmi_hhi_write(struct meson_dw_hdmi *priv, |
| unsigned int addr, unsigned int data) |
| { |
| hhi_write(addr, data); |
| } |
| |
| __attribute__((unused)) |
| static unsigned int dw_hdmi_hhi_read(struct meson_dw_hdmi *priv, |
| unsigned int addr) |
| { |
| return hhi_read(addr); |
| } |
| |
| static inline void dw_hdmi_hhi_update_bits(struct meson_dw_hdmi *priv, |
| unsigned int addr, |
| unsigned int mask, |
| unsigned int val) |
| { |
| hhi_update_bits(addr, mask, val); |
| } |
| |
| static int meson_dw_hdmi_read_edid(struct udevice *dev, u8 *buf, int buf_size) |
| { |
| #if defined DEBUG |
| struct display_timing timing; |
| int panel_bits_per_colour; |
| #endif |
| struct meson_dw_hdmi *priv = dev_get_priv(dev); |
| int ret; |
| |
| ret = dw_hdmi_read_edid(&priv->hdmi, buf, buf_size); |
| |
| #if defined DEBUG |
| if (!ret) |
| return ret; |
| |
| edid_print_info((struct edid1_info *)buf); |
| edid_get_timing(buf, ret, &timing, &panel_bits_per_colour); |
| debug("Display timing:\n"); |
| debug(" hactive %04d, hfrontp %04d, hbackp %04d hsync %04d\n" |
| " vactive %04d, vfrontp %04d, vbackp %04d vsync %04d\n", |
| timing.hactive.typ, timing.hfront_porch.typ, |
| timing.hback_porch.typ, timing.hsync_len.typ, |
| timing.vactive.typ, timing.vfront_porch.typ, |
| timing.vback_porch.typ, timing.vsync_len.typ); |
| debug(" flags: "); |
| if (timing.flags & DISPLAY_FLAGS_INTERLACED) |
| debug("interlaced "); |
| if (timing.flags & DISPLAY_FLAGS_DOUBLESCAN) |
| debug("doublescan "); |
| if (timing.flags & DISPLAY_FLAGS_DOUBLECLK) |
| debug("doubleclk "); |
| if (timing.flags & DISPLAY_FLAGS_HSYNC_LOW) |
| debug("hsync_low "); |
| if (timing.flags & DISPLAY_FLAGS_HSYNC_HIGH) |
| debug("hsync_high "); |
| if (timing.flags & DISPLAY_FLAGS_VSYNC_LOW) |
| debug("vsync_low "); |
| if (timing.flags & DISPLAY_FLAGS_VSYNC_HIGH) |
| debug("vsync_high "); |
| debug("\n"); |
| #endif |
| |
| return ret; |
| } |
| |
| static inline void meson_dw_hdmi_phy_reset(struct meson_dw_hdmi *priv) |
| { |
| /* Enable and software reset */ |
| dw_hdmi_hhi_update_bits(priv, HHI_HDMI_PHY_CNTL1, 0xf, 0xf); |
| |
| mdelay(2); |
| |
| /* Enable and unreset */ |
| dw_hdmi_hhi_update_bits(priv, HHI_HDMI_PHY_CNTL1, 0xf, 0xe); |
| |
| mdelay(2); |
| } |
| |
| static void meson_dw_hdmi_phy_setup_mode(struct meson_dw_hdmi *priv, |
| uint pixel_clock) |
| { |
| pixel_clock = pixel_clock / 1000; |
| |
| if (meson_hdmi_is_compatible(priv, HDMI_COMPATIBLE_GXL) || |
| meson_hdmi_is_compatible(priv, HDMI_COMPATIBLE_GXM)) { |
| if (pixel_clock >= 371250) { |
| /* 5.94Gbps, 3.7125Gbps */ |
| hhi_write(HHI_HDMI_PHY_CNTL0, 0x333d3282); |
| hhi_write(HHI_HDMI_PHY_CNTL3, 0x2136315b); |
| } else if (pixel_clock >= 297000) { |
| /* 2.97Gbps */ |
| hhi_write(HHI_HDMI_PHY_CNTL0, 0x33303382); |
| hhi_write(HHI_HDMI_PHY_CNTL3, 0x2036315b); |
| } else if (pixel_clock >= 148500) { |
| /* 1.485Gbps */ |
| hhi_write(HHI_HDMI_PHY_CNTL0, 0x33303362); |
| hhi_write(HHI_HDMI_PHY_CNTL3, 0x2016315b); |
| } else { |
| /* 742.5Mbps, and below */ |
| hhi_write(HHI_HDMI_PHY_CNTL0, 0x33604142); |
| hhi_write(HHI_HDMI_PHY_CNTL3, 0x0016315b); |
| } |
| } else if (meson_hdmi_is_compatible(priv, HDMI_COMPATIBLE_GXBB)) { |
| if (pixel_clock >= 371250) { |
| /* 5.94Gbps, 3.7125Gbps */ |
| hhi_write(HHI_HDMI_PHY_CNTL0, 0x33353245); |
| hhi_write(HHI_HDMI_PHY_CNTL3, 0x2100115b); |
| } else if (pixel_clock >= 297000) { |
| /* 2.97Gbps */ |
| hhi_write(HHI_HDMI_PHY_CNTL0, 0x33634283); |
| hhi_write(HHI_HDMI_PHY_CNTL3, 0xb000115b); |
| } else { |
| /* 1.485Gbps, and below */ |
| hhi_write(HHI_HDMI_PHY_CNTL0, 0x33632122); |
| hhi_write(HHI_HDMI_PHY_CNTL3, 0x2000115b); |
| } |
| } else if (meson_hdmi_is_compatible(priv, HDMI_COMPATIBLE_G12A)) { |
| if (pixel_clock >= 371250) { |
| /* 5.94Gbps, 3.7125Gbps */ |
| hhi_write(HHI_HDMI_PHY_CNTL0, 0x37eb65c4); |
| hhi_write(HHI_HDMI_PHY_CNTL3, 0x2ab0ff3b); |
| hhi_write(HHI_HDMI_PHY_CNTL5, 0x0000080b); |
| } else if (pixel_clock >= 297000) { |
| /* 2.97Gbps */ |
| hhi_write(HHI_HDMI_PHY_CNTL0, 0x33eb6262); |
| hhi_write(HHI_HDMI_PHY_CNTL3, 0x2ab0ff3b); |
| hhi_write(HHI_HDMI_PHY_CNTL5, 0x00000003); |
| } else { |
| /* 1.485Gbps, and below */ |
| hhi_write(HHI_HDMI_PHY_CNTL0, 0x33eb4242); |
| hhi_write(HHI_HDMI_PHY_CNTL3, 0x2ab0ff3b); |
| hhi_write(HHI_HDMI_PHY_CNTL5, 0x00000003); |
| } |
| } |
| } |
| |
| static int meson_dw_hdmi_phy_init(struct dw_hdmi *hdmi, uint pixel_clock) |
| { |
| struct meson_dw_hdmi *priv = container_of(hdmi, struct meson_dw_hdmi, |
| hdmi); |
| /* Enable clocks */ |
| dw_hdmi_hhi_update_bits(priv, HHI_HDMI_CLK_CNTL, 0xffff, 0x100); |
| |
| /* Bring HDMITX MEM output of power down */ |
| dw_hdmi_hhi_update_bits(priv, HHI_MEM_PD_REG0, 0xff << 8, 0); |
| |
| /* Bring out of reset */ |
| dw_hdmi_top_write(hdmi, HDMITX_TOP_SW_RESET, 0); |
| |
| /* Enable internal pixclk, tmds_clk, spdif_clk, i2s_clk, cecclk */ |
| dw_hdmi_top_write_bits(hdmi, HDMITX_TOP_CLK_CNTL, 0x3, 0x3); |
| dw_hdmi_top_write_bits(hdmi, HDMITX_TOP_CLK_CNTL, 0x3 << 4, 0x3 << 4); |
| |
| /* Enable normal output to PHY */ |
| dw_hdmi_top_write(hdmi, HDMITX_TOP_BIST_CNTL, BIT(12)); |
| |
| /* TMDS pattern setup (TOFIX pattern for 4k2k scrambling) */ |
| dw_hdmi_top_write(hdmi, HDMITX_TOP_TMDS_CLK_PTTN_01, 0x001f001f); |
| dw_hdmi_top_write(hdmi, HDMITX_TOP_TMDS_CLK_PTTN_23, 0x001f001f); |
| |
| /* Load TMDS pattern */ |
| dw_hdmi_top_write(hdmi, HDMITX_TOP_TMDS_CLK_PTTN_CNTL, 0x1); |
| mdelay(20); |
| dw_hdmi_top_write(hdmi, HDMITX_TOP_TMDS_CLK_PTTN_CNTL, 0x2); |
| |
| /* Setup PHY parameters */ |
| meson_dw_hdmi_phy_setup_mode(priv, pixel_clock); |
| |
| /* Setup PHY */ |
| dw_hdmi_hhi_update_bits(priv, HHI_HDMI_PHY_CNTL1, |
| 0xffff << 16, 0x0390 << 16); |
| |
| /* BIT_INVERT */ |
| if (meson_hdmi_is_compatible(priv, HDMI_COMPATIBLE_GXL) || |
| meson_hdmi_is_compatible(priv, HDMI_COMPATIBLE_GXM) || |
| meson_hdmi_is_compatible(priv, HDMI_COMPATIBLE_G12A)) |
| dw_hdmi_hhi_update_bits(priv, HHI_HDMI_PHY_CNTL1, BIT(17), 0); |
| else |
| dw_hdmi_hhi_update_bits(priv, HHI_HDMI_PHY_CNTL1, |
| BIT(17), BIT(17)); |
| |
| /* Disable clock, fifo, fifo_wr */ |
| dw_hdmi_hhi_update_bits(priv, HHI_HDMI_PHY_CNTL1, 0xf, 0); |
| |
| mdelay(100); |
| |
| /* Reset PHY 3 times in a row */ |
| meson_dw_hdmi_phy_reset(priv); |
| meson_dw_hdmi_phy_reset(priv); |
| meson_dw_hdmi_phy_reset(priv); |
| |
| return 0; |
| } |
| |
| static int meson_dw_hdmi_enable(struct udevice *dev, int panel_bpp, |
| const struct display_timing *edid) |
| { |
| struct meson_dw_hdmi *priv = dev_get_priv(dev); |
| |
| /* will back into meson_dw_hdmi_phy_init */ |
| return dw_hdmi_enable(&priv->hdmi, edid); |
| } |
| |
| static int meson_dw_hdmi_wait_hpd(struct dw_hdmi *hdmi) |
| { |
| int i; |
| |
| /* Poll 1 second for HPD signal */ |
| for (i = 0; i < 10; ++i) { |
| if (dw_hdmi_top_read(hdmi, HDMITX_TOP_STAT0)) |
| return 0; |
| |
| mdelay(100); |
| } |
| |
| return -ETIMEDOUT; |
| } |
| |
| static const struct dw_hdmi_phy_ops dw_hdmi_meson_phy_ops = { |
| .phy_set = meson_dw_hdmi_phy_init, |
| }; |
| |
| static int meson_dw_hdmi_probe(struct udevice *dev) |
| { |
| struct meson_dw_hdmi *priv = dev_get_priv(dev); |
| struct reset_ctl_bulk resets; |
| struct clk_bulk clocks; |
| #if CONFIG_IS_ENABLED(DM_REGULATOR) |
| struct udevice *supply; |
| #endif |
| int ret; |
| |
| priv->dev = dev; |
| |
| priv->hdmi.ioaddr = (ulong)dev_remap_addr_index(dev, 0); |
| if (!priv->hdmi.ioaddr) |
| return -EINVAL; |
| |
| priv->hhi_base = dev_remap_addr_index(dev, 1); |
| if (!priv->hhi_base) |
| return -EINVAL; |
| |
| priv->hdmi.hdmi_data.enc_out_bus_format = MEDIA_BUS_FMT_RGB888_1X24; |
| priv->hdmi.hdmi_data.enc_in_bus_format = MEDIA_BUS_FMT_YUV8_1X24; |
| priv->hdmi.ops = &dw_hdmi_meson_phy_ops; |
| if (meson_hdmi_is_compatible(priv, HDMI_COMPATIBLE_G12A)) |
| priv->hdmi.reg_io_width = 1; |
| else { |
| priv->hdmi.write_reg = dw_hdmi_dwc_write; |
| priv->hdmi.read_reg = dw_hdmi_dwc_read; |
| } |
| priv->hdmi.i2c_clk_high = 0x67; |
| priv->hdmi.i2c_clk_low = 0x78; |
| |
| #if CONFIG_IS_ENABLED(DM_REGULATOR) |
| ret = device_get_supply_regulator(dev, "hdmi-supply", &supply); |
| if (ret && ret != -ENOENT) { |
| pr_err("Failed to get HDMI regulator\n"); |
| return ret; |
| } |
| |
| if (!ret) { |
| ret = regulator_set_enable_if_allowed(supply, true); |
| if (ret && ret != -ENOSYS) |
| return ret; |
| } |
| #endif |
| |
| uclass_get_device_by_phandle(UCLASS_I2C, dev, "ddc-i2c-bus", |
| &priv->hdmi.ddc_bus); |
| |
| ret = reset_get_bulk(dev, &resets); |
| if (ret) |
| return ret; |
| |
| ret = clk_get_bulk(dev, &clocks); |
| if (ret) |
| return ret; |
| |
| ret = clk_enable_bulk(&clocks); |
| if (ret) |
| return ret; |
| |
| /* Enable clocks */ |
| dw_hdmi_hhi_update_bits(priv, HHI_HDMI_CLK_CNTL, 0xffff, 0x100); |
| |
| /* Bring HDMITX MEM output of power down */ |
| dw_hdmi_hhi_update_bits(priv, HHI_MEM_PD_REG0, 0xff << 8, 0); |
| |
| /* Reset HDMITX APB & TX & PHY: cycle needed for EDID */ |
| ret = reset_deassert_bulk(&resets); |
| if (ret) |
| return ret; |
| |
| ret = reset_assert_bulk(&resets); |
| if (ret) |
| return ret; |
| |
| ret = reset_deassert_bulk(&resets); |
| if (ret) |
| return ret; |
| |
| if (!meson_hdmi_is_compatible(priv, HDMI_COMPATIBLE_G12A)) { |
| /* Enable APB3 fail on error */ |
| writel_bits(BIT(15), BIT(15), |
| priv->hdmi.ioaddr + HDMITX_TOP_CTRL_REG); |
| writel_bits(BIT(15), BIT(15), |
| priv->hdmi.ioaddr + HDMITX_DWC_CTRL_REG); |
| } |
| |
| /* Bring out of reset */ |
| dw_hdmi_top_write(&priv->hdmi, HDMITX_TOP_SW_RESET, 0); |
| mdelay(20); |
| dw_hdmi_top_write(&priv->hdmi, HDMITX_TOP_CLK_CNTL, 0xff); |
| |
| dw_hdmi_init(&priv->hdmi); |
| dw_hdmi_phy_init(&priv->hdmi); |
| |
| /* wait for connector */ |
| ret = meson_dw_hdmi_wait_hpd(&priv->hdmi); |
| if (ret) |
| debug("hdmi can not get hpd signal\n"); |
| |
| return ret; |
| } |
| |
| static bool meson_dw_hdmi_mode_valid(struct udevice *dev, |
| const struct display_timing *timing) |
| { |
| return meson_venc_hdmi_supported_mode(timing); |
| } |
| |
| static const struct dm_display_ops meson_dw_hdmi_ops = { |
| .read_edid = meson_dw_hdmi_read_edid, |
| .enable = meson_dw_hdmi_enable, |
| .mode_valid = meson_dw_hdmi_mode_valid, |
| }; |
| |
| static const struct udevice_id meson_dw_hdmi_ids[] = { |
| { .compatible = "amlogic,meson-gxbb-dw-hdmi", |
| .data = HDMI_COMPATIBLE_GXBB }, |
| { .compatible = "amlogic,meson-gxl-dw-hdmi", |
| .data = HDMI_COMPATIBLE_GXL }, |
| { .compatible = "amlogic,meson-gxm-dw-hdmi", |
| .data = HDMI_COMPATIBLE_GXM }, |
| { .compatible = "amlogic,meson-g12a-dw-hdmi", |
| .data = HDMI_COMPATIBLE_G12A }, |
| { } |
| }; |
| |
| U_BOOT_DRIVER(meson_dw_hdmi) = { |
| .name = "meson_dw_hdmi", |
| .id = UCLASS_DISPLAY, |
| .of_match = meson_dw_hdmi_ids, |
| .ops = &meson_dw_hdmi_ops, |
| .probe = meson_dw_hdmi_probe, |
| .priv_auto = sizeof(struct meson_dw_hdmi), |
| }; |