From f94ab604db9db1a8a0cf0827f5573bfd2d2b89aa Mon Sep 17 00:00:00 2001 From: Russell King Date: Tue, 21 Jul 2015 10:00:15 +0100 Subject: [PATCH 01/13] gpu: imx: simplify sync polarity setting Use a function to convert the sync pin to a bit mask for the DI_GENERAL register, and move this out of the interlace/non-interlace path to the common path. Tested-by: Philipp Zabel Reviewed-by: Fabio Estevam Signed-off-by: Russell King --- drivers/gpu/ipu-v3/ipu-di.c | 50 +++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/drivers/gpu/ipu-v3/ipu-di.c b/drivers/gpu/ipu-v3/ipu-di.c index 2970c6bb668c..a96991c5c15f 100644 --- a/drivers/gpu/ipu-v3/ipu-di.c +++ b/drivers/gpu/ipu-v3/ipu-di.c @@ -543,6 +543,29 @@ int ipu_di_adjust_videomode(struct ipu_di *di, struct videomode *mode) } EXPORT_SYMBOL_GPL(ipu_di_adjust_videomode); +static u32 ipu_di_gen_polarity(int pin) +{ + switch (pin) { + case 1: + return DI_GEN_POLARITY_1; + case 2: + return DI_GEN_POLARITY_2; + case 3: + return DI_GEN_POLARITY_3; + case 4: + return DI_GEN_POLARITY_4; + case 5: + return DI_GEN_POLARITY_5; + case 6: + return DI_GEN_POLARITY_6; + case 7: + return DI_GEN_POLARITY_7; + case 8: + return DI_GEN_POLARITY_8; + } + return 0; +} + int ipu_di_init_sync_panel(struct ipu_di *di, struct ipu_di_signal_cfg *sig) { u32 reg; @@ -586,11 +609,6 @@ int ipu_di_init_sync_panel(struct ipu_di *di, struct ipu_di_signal_cfg *sig) di_gen |= DI_GEN_POLARITY_8; vsync_cnt = 7; - - if (sig->mode.flags & DISPLAY_FLAGS_HSYNC_HIGH) - di_gen |= DI_GEN_POLARITY_3; - if (sig->mode.flags & DISPLAY_FLAGS_VSYNC_HIGH) - di_gen |= DI_GEN_POLARITY_2; } else { ipu_di_sync_config_noninterlaced(di, sig, div); @@ -602,25 +620,13 @@ int ipu_di_init_sync_panel(struct ipu_di *di, struct ipu_di_signal_cfg *sig) */ if (!(sig->hsync_pin == 2 && sig->vsync_pin == 3)) vsync_cnt = 6; - - if (sig->mode.flags & DISPLAY_FLAGS_HSYNC_HIGH) { - if (sig->hsync_pin == 2) - di_gen |= DI_GEN_POLARITY_2; - else if (sig->hsync_pin == 4) - di_gen |= DI_GEN_POLARITY_4; - else if (sig->hsync_pin == 7) - di_gen |= DI_GEN_POLARITY_7; - } - if (sig->mode.flags & DISPLAY_FLAGS_VSYNC_HIGH) { - if (sig->vsync_pin == 3) - di_gen |= DI_GEN_POLARITY_3; - else if (sig->vsync_pin == 6) - di_gen |= DI_GEN_POLARITY_6; - else if (sig->vsync_pin == 8) - di_gen |= DI_GEN_POLARITY_8; - } } + if (sig->mode.flags & DISPLAY_FLAGS_HSYNC_HIGH) + di_gen |= ipu_di_gen_polarity(sig->hsync_pin); + if (sig->mode.flags & DISPLAY_FLAGS_VSYNC_HIGH) + di_gen |= ipu_di_gen_polarity(sig->vsync_pin); + if (sig->clk_pol) di_gen |= DI_GEN_POLARITY_DISP_CLK; From aefa627fa3f231c5d86aeb229f15e8478cb6e910 Mon Sep 17 00:00:00 2001 From: Russell King Date: Tue, 21 Jul 2015 10:22:29 +0100 Subject: [PATCH 02/13] gpu: imx: fix support for interlaced modes The support for interlaced video modes seems to be broken; we don't use anything other than the vtotal/htotal from the timing information to define the various sync counters. Freescale patches for interlaced video support contain an alternative sync counter setup, which we include here. This setup produces the hsync and vsync via the normal counter 2 and 3, but moves the display enable signal from counter 5 to counter 6. Therefore, we need to change the display controller setup as well. The corresponding Freescale patches for this change are: iMX6-HDMI-support-interlaced-display-mode.patch IPU-fine-tuning-the-interlace-display-timing-for-CEA.patch This produces a working interlace format output from the IPU. Tested-by: Philipp Zabel Reviewed-by: Fabio Estevam Signed-off-by: Russell King --- drivers/gpu/ipu-v3/ipu-dc.c | 15 ++++-- drivers/gpu/ipu-v3/ipu-di.c | 95 ++++++++++++++++++------------------- 2 files changed, 56 insertions(+), 54 deletions(-) diff --git a/drivers/gpu/ipu-v3/ipu-dc.c b/drivers/gpu/ipu-v3/ipu-dc.c index 9ef2e1f54ca4..d3ad5347342c 100644 --- a/drivers/gpu/ipu-v3/ipu-dc.c +++ b/drivers/gpu/ipu-v3/ipu-dc.c @@ -183,12 +183,19 @@ int ipu_dc_init_sync(struct ipu_dc *dc, struct ipu_di *di, bool interlaced, } if (interlaced) { - dc_link_event(dc, DC_EVT_NL, 0, 3); - dc_link_event(dc, DC_EVT_EOL, 0, 2); - dc_link_event(dc, DC_EVT_NEW_DATA, 0, 1); + int addr; + + if (dc->di) + addr = 1; + else + addr = 0; + + dc_link_event(dc, DC_EVT_NL, addr, 3); + dc_link_event(dc, DC_EVT_EOL, addr, 2); + dc_link_event(dc, DC_EVT_NEW_DATA, addr, 1); /* Init template microcode */ - dc_write_tmpl(dc, 0, WROD(0), 0, map, SYNC_WAVE, 0, 8, 1); + dc_write_tmpl(dc, addr, WROD(0), 0, map, SYNC_WAVE, 0, 6, 1); } else { if (dc->di) { dc_link_event(dc, DC_EVT_NL, 2, 3); diff --git a/drivers/gpu/ipu-v3/ipu-di.c b/drivers/gpu/ipu-v3/ipu-di.c index a96991c5c15f..359268e3a166 100644 --- a/drivers/gpu/ipu-v3/ipu-di.c +++ b/drivers/gpu/ipu-v3/ipu-di.c @@ -71,6 +71,10 @@ enum di_sync_wave { DI_SYNC_HSYNC = 3, DI_SYNC_VSYNC = 4, DI_SYNC_DE = 6, + + DI_SYNC_CNT1 = 2, /* counter >= 2 only */ + DI_SYNC_CNT4 = 5, /* counter >= 5 only */ + DI_SYNC_CNT5 = 6, /* counter >= 6 only */ }; #define SYNC_WAVE 0 @@ -211,66 +215,59 @@ static void ipu_di_sync_config_interlaced(struct ipu_di *di, sig->mode.hback_porch + sig->mode.hfront_porch; u32 v_total = sig->mode.vactive + sig->mode.vsync_len + sig->mode.vback_porch + sig->mode.vfront_porch; - u32 reg; struct di_sync_config cfg[] = { { - .run_count = h_total / 2 - 1, - .run_src = DI_SYNC_CLK, - }, { - .run_count = h_total - 11, - .run_src = DI_SYNC_CLK, - .cnt_down = 4, - }, { + /* 1: internal VSYNC for each frame */ .run_count = v_total * 2 - 1, - .run_src = DI_SYNC_INT_HSYNC, - .offset_count = 1, - .offset_src = DI_SYNC_INT_HSYNC, - .cnt_down = 4, - }, { - .run_count = v_total / 2 - 1, - .run_src = DI_SYNC_HSYNC, - .offset_count = sig->mode.vback_porch, - .offset_src = DI_SYNC_HSYNC, - .repeat_count = 2, - .cnt_clr_src = DI_SYNC_VSYNC, - }, { - .run_src = DI_SYNC_HSYNC, - .repeat_count = sig->mode.vactive / 2, - .cnt_clr_src = 4, - }, { - .run_count = v_total - 1, - .run_src = DI_SYNC_HSYNC, - }, { - .run_count = v_total / 2 - 1, - .run_src = DI_SYNC_HSYNC, - .offset_count = 9, - .offset_src = DI_SYNC_HSYNC, - .repeat_count = 2, - .cnt_clr_src = DI_SYNC_VSYNC, + .run_src = 3, /* == counter 7 */ }, { + /* PIN2: HSYNC waveform */ + .run_count = h_total - 1, .run_src = DI_SYNC_CLK, - .offset_count = sig->mode.hback_porch, + .cnt_polarity_gen_en = 1, + .cnt_polarity_trigger_src = DI_SYNC_CLK, + .cnt_down = sig->mode.hsync_len * 2, + }, { + /* PIN3: VSYNC waveform */ + .run_count = v_total - 1, + .run_src = 4, /* == counter 7 */ + .cnt_polarity_gen_en = 1, + .cnt_polarity_trigger_src = 4, /* == counter 7 */ + .cnt_down = sig->mode.vsync_len * 2, + .cnt_clr_src = DI_SYNC_CNT1, + }, { + /* 4: Field */ + .run_count = v_total / 2, + .run_src = DI_SYNC_HSYNC, + .offset_count = h_total / 2, + .offset_src = DI_SYNC_CLK, + .repeat_count = 2, + .cnt_clr_src = DI_SYNC_CNT1, + }, { + /* 5: Active lines */ + .run_src = DI_SYNC_HSYNC, + .offset_count = (sig->mode.vsync_len + + sig->mode.vback_porch) / 2, + .offset_src = DI_SYNC_HSYNC, + .repeat_count = sig->mode.vactive / 2, + .cnt_clr_src = DI_SYNC_CNT4, + }, { + /* 6: Active pixel, referenced by DC */ + .run_src = DI_SYNC_CLK, + .offset_count = sig->mode.hsync_len + + sig->mode.hback_porch, .offset_src = DI_SYNC_CLK, .repeat_count = sig->mode.hactive, - .cnt_clr_src = 5, + .cnt_clr_src = DI_SYNC_CNT5, }, { - .run_count = v_total - 1, - .run_src = DI_SYNC_INT_HSYNC, - .offset_count = v_total / 2, - .offset_src = DI_SYNC_INT_HSYNC, - .cnt_clr_src = DI_SYNC_HSYNC, - .cnt_down = 4, + /* 7: Half line HSYNC */ + .run_count = h_total / 2 - 1, + .run_src = DI_SYNC_CLK, } }; ipu_di_sync_config(di, cfg, 0, ARRAY_SIZE(cfg)); - /* set gentime select and tag sel */ - reg = ipu_di_read(di, DI_SW_GEN1(9)); - reg &= 0x1FFFFFFF; - reg |= (3 - 1) << 29 | 0x00008000; - ipu_di_write(di, reg, DI_SW_GEN1(9)); - ipu_di_write(di, v_total / 2 - 1, DI_SCR_CONF); } @@ -605,10 +602,8 @@ int ipu_di_init_sync_panel(struct ipu_di *di, struct ipu_di_signal_cfg *sig) /* set y_sel = 1 */ di_gen |= 0x10000000; - di_gen |= DI_GEN_POLARITY_5; - di_gen |= DI_GEN_POLARITY_8; - vsync_cnt = 7; + vsync_cnt = 3; } else { ipu_di_sync_config_noninterlaced(di, sig, div); From e80b9f4eea2649f67edc05d4f2e7ddc250f91453 Mon Sep 17 00:00:00 2001 From: Russell King Date: Tue, 21 Jul 2015 11:08:25 +0100 Subject: [PATCH 03/13] drm: bridge/dw_hdmi: add support for interlaced video modes Add support for interlaced video modes to the dw_hdmi bridge. This mainly involves halving the vertical parameters to be programmed into the bridge registers, and setting the interlace_allowed connector flag. This brings working 1080i support. However, 480i and 576i fail to work due to the lack of proper pixel repetition support, which is not trivial to add due to the tabular PLL parameterisation. Hence, we filter out these modes in our mode_valid() method. Tested-by: Philipp Zabel Reviewed-by: Fabio Estevam Signed-off-by: Russell King --- drivers/gpu/drm/bridge/dw_hdmi.c | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/bridge/dw_hdmi.c b/drivers/gpu/drm/bridge/dw_hdmi.c index 0083d4e7e7e2..dc1a382a9666 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi.c +++ b/drivers/gpu/drm/bridge/dw_hdmi.c @@ -1063,6 +1063,7 @@ static void hdmi_av_composer(struct dw_hdmi *hdmi, u8 inv_val; struct hdmi_vmode *vmode = &hdmi->hdmi_data.video_mode; int hblank, vblank, h_de_hs, v_de_vs, hsync_len, vsync_len; + unsigned int vdisplay; vmode->mpixelclock = mode->clock * 1000; @@ -1102,13 +1103,29 @@ static void hdmi_av_composer(struct dw_hdmi *hdmi, hdmi_writeb(hdmi, inv_val, HDMI_FC_INVIDCONF); + vdisplay = mode->vdisplay; + vblank = mode->vtotal - mode->vdisplay; + v_de_vs = mode->vsync_start - mode->vdisplay; + vsync_len = mode->vsync_end - mode->vsync_start; + + /* + * When we're setting an interlaced mode, we need + * to adjust the vertical timing to suit. + */ + if (mode->flags & DRM_MODE_FLAG_INTERLACE) { + vdisplay /= 2; + vblank /= 2; + v_de_vs /= 2; + vsync_len /= 2; + } + /* Set up horizontal active pixel width */ hdmi_writeb(hdmi, mode->hdisplay >> 8, HDMI_FC_INHACTV1); hdmi_writeb(hdmi, mode->hdisplay, HDMI_FC_INHACTV0); /* Set up vertical active lines */ - hdmi_writeb(hdmi, mode->vdisplay >> 8, HDMI_FC_INVACTV1); - hdmi_writeb(hdmi, mode->vdisplay, HDMI_FC_INVACTV0); + hdmi_writeb(hdmi, vdisplay >> 8, HDMI_FC_INVACTV1); + hdmi_writeb(hdmi, vdisplay, HDMI_FC_INVACTV0); /* Set up horizontal blanking pixel region width */ hblank = mode->htotal - mode->hdisplay; @@ -1116,7 +1133,6 @@ static void hdmi_av_composer(struct dw_hdmi *hdmi, hdmi_writeb(hdmi, hblank, HDMI_FC_INHBLANK0); /* Set up vertical blanking pixel region width */ - vblank = mode->vtotal - mode->vdisplay; hdmi_writeb(hdmi, vblank, HDMI_FC_INVBLANK); /* Set up HSYNC active edge delay width (in pixel clks) */ @@ -1125,7 +1141,6 @@ static void hdmi_av_composer(struct dw_hdmi *hdmi, hdmi_writeb(hdmi, h_de_hs, HDMI_FC_HSYNCINDELAY0); /* Set up VSYNC active edge delay (in lines) */ - v_de_vs = mode->vsync_start - mode->vdisplay; hdmi_writeb(hdmi, v_de_vs, HDMI_FC_VSYNCINDELAY); /* Set up HSYNC active pulse width (in pixel clks) */ @@ -1134,7 +1149,6 @@ static void hdmi_av_composer(struct dw_hdmi *hdmi, hdmi_writeb(hdmi, hsync_len, HDMI_FC_HSYNCINWIDTH0); /* Set up VSYNC active edge delay (in lines) */ - vsync_len = mode->vsync_end - mode->vsync_start; hdmi_writeb(hdmi, vsync_len, HDMI_FC_VSYNCINWIDTH); } @@ -1608,6 +1622,8 @@ int dw_hdmi_bind(struct device *dev, struct device *master, if (!hdmi) return -ENOMEM; + hdmi->connector.interlace_allowed = 1; + hdmi->plat_data = plat_data; hdmi->dev = dev; hdmi->dev_type = plat_data->dev_type; From 381f05a7a8428c84483d6bbd0aa3f4ced463c278 Mon Sep 17 00:00:00 2001 From: Russell King Date: Fri, 5 Jun 2015 15:25:08 +0100 Subject: [PATCH 04/13] drm: bridge/dw_hdmi: add connector mode forcing When connected to HDMI sources, some DVI monitors de-assert their HPD signal and TDMS loads for one seconds every four seconds when there is no signal present on the connection. Unfortunately, this behaviour is indistinguishable from a proper HDMI setup with an AV receiver in the path to the display: the HDMI spec requires us to detect HPD deassertions as short as 100ms, which indicate that the EDID has changed. Since it is possible to connect a DVI monitor to an AV receiver and then to a HDMI source, merely working around this by detecting the lack of HDMI vendor block in the EDID is insufficient - the AV receiver is at liberty to modify the EDID as it sees fit, and it will place its own parameters into the EDID including the HDMI vendor block. DRM has support for forcing the state of a connector, which we should implement to allow us to work around these broken DVI monitors - we can tell DRM to force the connection state to indicate that there is always a device connected to work around this problem. Although this requires manual configuration, it is better than nothing at all. When a forced connection state has been set, there is no point handling our RXSENSE interrupts, so disable them in this circumstance. Tested-by: Philipp Zabel Reviewed-by: Fabio Estevam Signed-off-by: Russell King --- drivers/gpu/drm/bridge/dw_hdmi.c | 51 +++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/bridge/dw_hdmi.c b/drivers/gpu/drm/bridge/dw_hdmi.c index dc1a382a9666..7dcae9df509b 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi.c +++ b/drivers/gpu/drm/bridge/dw_hdmi.c @@ -126,7 +126,9 @@ struct dw_hdmi { bool sink_has_audio; struct mutex mutex; /* for state below and previous_mode */ + enum drm_connector_force force; /* mutex-protected force state */ bool disabled; /* DRM has disabled our bridge */ + bool bridge_is_on; /* indicates the bridge is on */ spinlock_t audio_lock; struct mutex audio_mutex; @@ -1378,12 +1380,36 @@ static void initialize_hdmi_ih_mutes(struct dw_hdmi *hdmi) static void dw_hdmi_poweron(struct dw_hdmi *hdmi) { + hdmi->bridge_is_on = true; dw_hdmi_setup(hdmi, &hdmi->previous_mode); } static void dw_hdmi_poweroff(struct dw_hdmi *hdmi) { dw_hdmi_phy_disable(hdmi); + hdmi->bridge_is_on = false; +} + +static void dw_hdmi_update_power(struct dw_hdmi *hdmi) +{ + int force = hdmi->force; + + if (hdmi->disabled) { + force = DRM_FORCE_OFF; + } else if (force == DRM_FORCE_UNSPECIFIED) { + if (hdmi_readb(hdmi, HDMI_PHY_STAT0) & HDMI_PHY_HPD) + force = DRM_FORCE_ON; + else + force = DRM_FORCE_OFF; + } + + if (force == DRM_FORCE_OFF) { + if (hdmi->bridge_is_on) + dw_hdmi_poweroff(hdmi); + } else { + if (!hdmi->bridge_is_on) + dw_hdmi_poweron(hdmi); + } } static void dw_hdmi_bridge_mode_set(struct drm_bridge *bridge, @@ -1413,7 +1439,7 @@ static void dw_hdmi_bridge_disable(struct drm_bridge *bridge) mutex_lock(&hdmi->mutex); hdmi->disabled = true; - dw_hdmi_poweroff(hdmi); + dw_hdmi_update_power(hdmi); mutex_unlock(&hdmi->mutex); } @@ -1422,8 +1448,8 @@ static void dw_hdmi_bridge_enable(struct drm_bridge *bridge) struct dw_hdmi *hdmi = bridge->driver_private; mutex_lock(&hdmi->mutex); - dw_hdmi_poweron(hdmi); hdmi->disabled = false; + dw_hdmi_update_power(hdmi); mutex_unlock(&hdmi->mutex); } @@ -1438,6 +1464,11 @@ dw_hdmi_connector_detect(struct drm_connector *connector, bool force) struct dw_hdmi *hdmi = container_of(connector, struct dw_hdmi, connector); + mutex_lock(&hdmi->mutex); + hdmi->force = DRM_FORCE_UNSPECIFIED; + dw_hdmi_update_power(hdmi); + mutex_unlock(&hdmi->mutex); + return hdmi_readb(hdmi, HDMI_PHY_STAT0) & HDMI_PHY_HPD ? connector_status_connected : connector_status_disconnected; } @@ -1502,11 +1533,23 @@ static void dw_hdmi_connector_destroy(struct drm_connector *connector) drm_connector_cleanup(connector); } +static void dw_hdmi_connector_force(struct drm_connector *connector) +{ + struct dw_hdmi *hdmi = container_of(connector, struct dw_hdmi, + connector); + + mutex_lock(&hdmi->mutex); + hdmi->force = connector->force; + dw_hdmi_update_power(hdmi); + mutex_unlock(&hdmi->mutex); +} + static struct drm_connector_funcs dw_hdmi_connector_funcs = { .dpms = drm_helper_connector_dpms, .fill_modes = drm_helper_probe_single_connector_modes, .detect = dw_hdmi_connector_detect, .destroy = dw_hdmi_connector_destroy, + .force = dw_hdmi_connector_force, }; static struct drm_connector_helper_funcs dw_hdmi_connector_helper_funcs = { @@ -1552,12 +1595,12 @@ static irqreturn_t dw_hdmi_irq(int irq, void *dev_id) if (phy_int_pol & HDMI_PHY_HPD) { dev_dbg(hdmi->dev, "EVENT=plugin\n"); - if (!hdmi->disabled) + if (!hdmi->disabled && !hdmi->force) dw_hdmi_poweron(hdmi); } else { dev_dbg(hdmi->dev, "EVENT=plugout\n"); - if (!hdmi->disabled) + if (!hdmi->disabled && !hdmi->force) dw_hdmi_poweroff(hdmi); } mutex_unlock(&hdmi->mutex); From aeac23bda87fd98049352d3bc27a1cb80445e7a0 Mon Sep 17 00:00:00 2001 From: Russell King Date: Fri, 5 Jun 2015 13:46:22 +0100 Subject: [PATCH 05/13] drm: bridge/dw_hdmi: improve HDMI enable/disable handling HDMI sinks are permitted to de-assert and re-assert the HPD signal to indicate that their EDID has been updated, which may not involve a change of video information. An example of where such a situation can arise is when an AV receiver is connected between the source and the display device. Events which can cause the HPD to be deasserted include: * turning on or switching to standby the AV receiver. * turning on or switching to standby the display device. Each of these can change the entire EDID data, or just a part of the EDID data - it's up to the connected HDMI sink to do what they desire here. For example - with the AV receiver and display device both in standby, a source connected to the AV receiver may provide its own EDID to the source. - turning on the display device causes the display device's EDID to be made available in an unmodified form to the source. - subsequently turning on the AV receiver then provides a modified version of the display device's EDID. Moreover, HPD doesn't tell us whether something is actually listening on the HDMI TDMS signals. The phy gives us a set of RXSENSE indications which tell us whether there is a sink connected to the TMDS signals. Currently, we use the HPD signal to enable or disable the HDMI block, which is questionable when HPD is used in this manner. Using the RXSENSE would be more appropriate, but there is some bad behaviour which needs to be coped with. The iMX6 implementation lets the TMDS signals float when the phy is "powered down", which cause spurious interrupts. Rather than just using RXSENSE, use RXSENSE and HPD becoming both active to signal the presence of a device, but loss of RXSENSE to indicate that the device has been unplugged. The side effect of this change is that a sink deasserting the HPD signal to cause a re-read of the EDID data will not cause the bridge to immediately disable the video signal. Tested-by: Philipp Zabel Reviewed-by: Fabio Estevam Signed-off-by: Russell King --- drivers/gpu/drm/bridge/dw_hdmi.c | 120 +++++++++++++++++++++++++------ 1 file changed, 100 insertions(+), 20 deletions(-) diff --git a/drivers/gpu/drm/bridge/dw_hdmi.c b/drivers/gpu/drm/bridge/dw_hdmi.c index 7dcae9df509b..7a130f6fdb3d 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi.c +++ b/drivers/gpu/drm/bridge/dw_hdmi.c @@ -129,6 +129,8 @@ struct dw_hdmi { enum drm_connector_force force; /* mutex-protected force state */ bool disabled; /* DRM has disabled our bridge */ bool bridge_is_on; /* indicates the bridge is on */ + bool rxsense; /* rxsense state */ + u8 phy_mask; /* desired phy int mask settings */ spinlock_t audio_lock; struct mutex audio_mutex; @@ -142,6 +144,14 @@ struct dw_hdmi { u8 (*read)(struct dw_hdmi *hdmi, int offset); }; +#define HDMI_IH_PHY_STAT0_RX_SENSE \ + (HDMI_IH_PHY_STAT0_RX_SENSE0 | HDMI_IH_PHY_STAT0_RX_SENSE1 | \ + HDMI_IH_PHY_STAT0_RX_SENSE2 | HDMI_IH_PHY_STAT0_RX_SENSE3) + +#define HDMI_PHY_RX_SENSE \ + (HDMI_PHY_RX_SENSE0 | HDMI_PHY_RX_SENSE1 | \ + HDMI_PHY_RX_SENSE2 | HDMI_PHY_RX_SENSE3) + static void dw_hdmi_writel(struct dw_hdmi *hdmi, u8 val, int offset) { writel(val, hdmi->regs + (offset << 2)); @@ -1318,10 +1328,11 @@ static int dw_hdmi_fb_registered(struct dw_hdmi *hdmi) HDMI_PHY_I2CM_CTLINT_ADDR); /* enable cable hot plug irq */ - hdmi_writeb(hdmi, (u8)~HDMI_PHY_HPD, HDMI_PHY_MASK0); + hdmi_writeb(hdmi, hdmi->phy_mask, HDMI_PHY_MASK0); /* Clear Hotplug interrupts */ - hdmi_writeb(hdmi, HDMI_IH_PHY_STAT0_HPD, HDMI_IH_PHY_STAT0); + hdmi_writeb(hdmi, HDMI_IH_PHY_STAT0_HPD | HDMI_IH_PHY_STAT0_RX_SENSE, + HDMI_IH_PHY_STAT0); return 0; } @@ -1397,7 +1408,7 @@ static void dw_hdmi_update_power(struct dw_hdmi *hdmi) if (hdmi->disabled) { force = DRM_FORCE_OFF; } else if (force == DRM_FORCE_UNSPECIFIED) { - if (hdmi_readb(hdmi, HDMI_PHY_STAT0) & HDMI_PHY_HPD) + if (hdmi->rxsense) force = DRM_FORCE_ON; else force = DRM_FORCE_OFF; @@ -1412,6 +1423,31 @@ static void dw_hdmi_update_power(struct dw_hdmi *hdmi) } } +/* + * Adjust the detection of RXSENSE according to whether we have a forced + * connection mode enabled, or whether we have been disabled. There is + * no point processing RXSENSE interrupts if we have a forced connection + * state, or DRM has us disabled. + * + * We also disable rxsense interrupts when we think we're disconnected + * to avoid floating TDMS signals giving false rxsense interrupts. + * + * Note: we still need to listen for HPD interrupts even when DRM has us + * disabled so that we can detect a connect event. + */ +static void dw_hdmi_update_phy_mask(struct dw_hdmi *hdmi) +{ + u8 old_mask = hdmi->phy_mask; + + if (hdmi->force || hdmi->disabled || !hdmi->rxsense) + hdmi->phy_mask |= HDMI_PHY_RX_SENSE; + else + hdmi->phy_mask &= ~HDMI_PHY_RX_SENSE; + + if (old_mask != hdmi->phy_mask) + hdmi_writeb(hdmi, hdmi->phy_mask, HDMI_PHY_MASK0); +} + static void dw_hdmi_bridge_mode_set(struct drm_bridge *bridge, struct drm_display_mode *orig_mode, struct drm_display_mode *mode) @@ -1440,6 +1476,7 @@ static void dw_hdmi_bridge_disable(struct drm_bridge *bridge) mutex_lock(&hdmi->mutex); hdmi->disabled = true; dw_hdmi_update_power(hdmi); + dw_hdmi_update_phy_mask(hdmi); mutex_unlock(&hdmi->mutex); } @@ -1450,6 +1487,7 @@ static void dw_hdmi_bridge_enable(struct drm_bridge *bridge) mutex_lock(&hdmi->mutex); hdmi->disabled = false; dw_hdmi_update_power(hdmi); + dw_hdmi_update_phy_mask(hdmi); mutex_unlock(&hdmi->mutex); } @@ -1467,6 +1505,7 @@ dw_hdmi_connector_detect(struct drm_connector *connector, bool force) mutex_lock(&hdmi->mutex); hdmi->force = DRM_FORCE_UNSPECIFIED; dw_hdmi_update_power(hdmi); + dw_hdmi_update_phy_mask(hdmi); mutex_unlock(&hdmi->mutex); return hdmi_readb(hdmi, HDMI_PHY_STAT0) & HDMI_PHY_HPD ? @@ -1541,6 +1580,7 @@ static void dw_hdmi_connector_force(struct drm_connector *connector) mutex_lock(&hdmi->mutex); hdmi->force = connector->force; dw_hdmi_update_power(hdmi); + dw_hdmi_update_phy_mask(hdmi); mutex_unlock(&hdmi->mutex); } @@ -1582,33 +1622,69 @@ static irqreturn_t dw_hdmi_hardirq(int irq, void *dev_id) static irqreturn_t dw_hdmi_irq(int irq, void *dev_id) { struct dw_hdmi *hdmi = dev_id; - u8 intr_stat; - u8 phy_int_pol; + u8 intr_stat, phy_int_pol, phy_pol_mask, phy_stat; intr_stat = hdmi_readb(hdmi, HDMI_IH_PHY_STAT0); - phy_int_pol = hdmi_readb(hdmi, HDMI_PHY_POL0); + phy_stat = hdmi_readb(hdmi, HDMI_PHY_STAT0); - if (intr_stat & HDMI_IH_PHY_STAT0_HPD) { - hdmi_modb(hdmi, ~phy_int_pol, HDMI_PHY_HPD, HDMI_PHY_POL0); + phy_pol_mask = 0; + if (intr_stat & HDMI_IH_PHY_STAT0_HPD) + phy_pol_mask |= HDMI_PHY_HPD; + if (intr_stat & HDMI_IH_PHY_STAT0_RX_SENSE0) + phy_pol_mask |= HDMI_PHY_RX_SENSE0; + if (intr_stat & HDMI_IH_PHY_STAT0_RX_SENSE1) + phy_pol_mask |= HDMI_PHY_RX_SENSE1; + if (intr_stat & HDMI_IH_PHY_STAT0_RX_SENSE2) + phy_pol_mask |= HDMI_PHY_RX_SENSE2; + if (intr_stat & HDMI_IH_PHY_STAT0_RX_SENSE3) + phy_pol_mask |= HDMI_PHY_RX_SENSE3; + + if (phy_pol_mask) + hdmi_modb(hdmi, ~phy_int_pol, phy_pol_mask, HDMI_PHY_POL0); + + /* + * RX sense tells us whether the TDMS transmitters are detecting + * load - in other words, there's something listening on the + * other end of the link. Use this to decide whether we should + * power on the phy as HPD may be toggled by the sink to merely + * ask the source to re-read the EDID. + */ + if (intr_stat & + (HDMI_IH_PHY_STAT0_RX_SENSE | HDMI_IH_PHY_STAT0_HPD)) { mutex_lock(&hdmi->mutex); - if (phy_int_pol & HDMI_PHY_HPD) { - dev_dbg(hdmi->dev, "EVENT=plugin\n"); + if (!hdmi->disabled && !hdmi->force) { + /* + * If the RX sense status indicates we're disconnected, + * clear the software rxsense status. + */ + if (!(phy_stat & HDMI_PHY_RX_SENSE)) + hdmi->rxsense = false; - if (!hdmi->disabled && !hdmi->force) - dw_hdmi_poweron(hdmi); - } else { - dev_dbg(hdmi->dev, "EVENT=plugout\n"); + /* + * Only set the software rxsense status when both + * rxsense and hpd indicates we're connected. + * This avoids what seems to be bad behaviour in + * at least iMX6S versions of the phy. + */ + if (phy_stat & HDMI_PHY_HPD) + hdmi->rxsense = true; - if (!hdmi->disabled && !hdmi->force) - dw_hdmi_poweroff(hdmi); + dw_hdmi_update_power(hdmi); + dw_hdmi_update_phy_mask(hdmi); } mutex_unlock(&hdmi->mutex); + } + + if (intr_stat & HDMI_IH_PHY_STAT0_HPD) { + dev_dbg(hdmi->dev, "EVENT=%s\n", + phy_int_pol & HDMI_PHY_HPD ? "plugin" : "plugout"); drm_helper_hpd_irq_event(hdmi->bridge->dev); } hdmi_writeb(hdmi, intr_stat, HDMI_IH_PHY_STAT0); - hdmi_writeb(hdmi, ~HDMI_IH_PHY_STAT0_HPD, HDMI_IH_MUTE_PHY_STAT0); + hdmi_writeb(hdmi, ~(HDMI_IH_PHY_STAT0_HPD | HDMI_IH_PHY_STAT0_RX_SENSE), + HDMI_IH_MUTE_PHY_STAT0); return IRQ_HANDLED; } @@ -1674,6 +1750,8 @@ int dw_hdmi_bind(struct device *dev, struct device *master, hdmi->ratio = 100; hdmi->encoder = encoder; hdmi->disabled = true; + hdmi->rxsense = true; + hdmi->phy_mask = (u8)~(HDMI_PHY_HPD | HDMI_PHY_RX_SENSE); mutex_init(&hdmi->mutex); mutex_init(&hdmi->audio_mutex); @@ -1764,10 +1842,11 @@ int dw_hdmi_bind(struct device *dev, struct device *master, * Configure registers related to HDMI interrupt * generation before registering IRQ. */ - hdmi_writeb(hdmi, HDMI_PHY_HPD, HDMI_PHY_POL0); + hdmi_writeb(hdmi, HDMI_PHY_HPD | HDMI_PHY_RX_SENSE, HDMI_PHY_POL0); /* Clear Hotplug interrupts */ - hdmi_writeb(hdmi, HDMI_IH_PHY_STAT0_HPD, HDMI_IH_PHY_STAT0); + hdmi_writeb(hdmi, HDMI_IH_PHY_STAT0_HPD | HDMI_IH_PHY_STAT0_RX_SENSE, + HDMI_IH_PHY_STAT0); ret = dw_hdmi_fb_registered(hdmi); if (ret) @@ -1778,7 +1857,8 @@ int dw_hdmi_bind(struct device *dev, struct device *master, goto err_iahb; /* Unmute interrupts */ - hdmi_writeb(hdmi, ~HDMI_IH_PHY_STAT0_HPD, HDMI_IH_MUTE_PHY_STAT0); + hdmi_writeb(hdmi, ~(HDMI_IH_PHY_STAT0_HPD | HDMI_IH_PHY_STAT0_RX_SENSE), + HDMI_IH_MUTE_PHY_STAT0); dev_set_drvdata(dev, hdmi); From 7ed6c665e19d4c0456d01c01147e82563c482c4d Mon Sep 17 00:00:00 2001 From: Russell King Date: Thu, 7 Nov 2013 16:01:45 +0000 Subject: [PATCH 06/13] drm: bridge/dw_hdmi-ahb-audio: add audio driver Add ALSA based HDMI AHB audio driver for dw_hdmi. The only buffer format supported by the hardware is its own special IEC958 based format, which is not compatible with any ALSA format. To avoid doing too much data manipulation within the driver, we support only ALSAs IEC958 LE and 24-bit PCM formats for 2 to 6 channels, which we convert to its hardware format. A more desirable solution would be to have this conversion in userspace, but ALSA does not appear to allow such transformations outside of libasound itself. Reviewed-by: Takashi Iwai Tested-by: Fabio Estevam Signed-off-by: Russell King --- drivers/gpu/drm/bridge/Kconfig | 11 + drivers/gpu/drm/bridge/Makefile | 1 + drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c | 579 +++++++++++++++++++++ drivers/gpu/drm/bridge/dw_hdmi-audio.h | 13 + drivers/gpu/drm/bridge/dw_hdmi.c | 24 + drivers/gpu/drm/bridge/dw_hdmi.h | 3 + 6 files changed, 631 insertions(+) create mode 100644 drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c create mode 100644 drivers/gpu/drm/bridge/dw_hdmi-audio.h diff --git a/drivers/gpu/drm/bridge/Kconfig b/drivers/gpu/drm/bridge/Kconfig index 2de52a53a803..0d0a4f7f4f5c 100644 --- a/drivers/gpu/drm/bridge/Kconfig +++ b/drivers/gpu/drm/bridge/Kconfig @@ -11,6 +11,17 @@ config DRM_DW_HDMI tristate select DRM_KMS_HELPER +config DRM_DW_HDMI_AHB_AUDIO + tristate "Synopsis Designware AHB Audio interface" + depends on DRM_DW_HDMI && SND + select SND_PCM + select SND_PCM_IEC958 + help + Support the AHB Audio interface which is part of the Synopsis + Designware HDMI block. This is used in conjunction with + the i.MX6 HDMI driver. + + config DRM_NXP_PTN3460 tristate "NXP PTN3460 DP/LVDS bridge" depends on OF diff --git a/drivers/gpu/drm/bridge/Makefile b/drivers/gpu/drm/bridge/Makefile index e2eef1c2f4c3..d4e28beec30e 100644 --- a/drivers/gpu/drm/bridge/Makefile +++ b/drivers/gpu/drm/bridge/Makefile @@ -1,5 +1,6 @@ ccflags-y := -Iinclude/drm obj-$(CONFIG_DRM_DW_HDMI) += dw_hdmi.o +obj-$(CONFIG_DRM_DW_HDMI_AHB_AUDIO) += dw_hdmi-ahb-audio.o obj-$(CONFIG_DRM_NXP_PTN3460) += nxp-ptn3460.o obj-$(CONFIG_DRM_PARADE_PS8622) += parade-ps8622.o diff --git a/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c b/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c new file mode 100644 index 000000000000..bf379310008a --- /dev/null +++ b/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c @@ -0,0 +1,579 @@ +/* + * DesignWare HDMI audio driver + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * Written and tested against the Designware HDMI Tx found in iMX6. + */ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "dw_hdmi-audio.h" + +#define DRIVER_NAME "dw-hdmi-ahb-audio" + +/* Provide some bits rather than bit offsets */ +enum { + HDMI_AHB_DMA_CONF0_SW_FIFO_RST = BIT(7), + HDMI_AHB_DMA_CONF0_EN_HLOCK = BIT(3), + HDMI_AHB_DMA_START_START = BIT(0), + HDMI_AHB_DMA_STOP_STOP = BIT(0), + HDMI_IH_MUTE_AHBDMAAUD_STAT0_ERROR = BIT(5), + HDMI_IH_MUTE_AHBDMAAUD_STAT0_LOST = BIT(4), + HDMI_IH_MUTE_AHBDMAAUD_STAT0_RETRY = BIT(3), + HDMI_IH_MUTE_AHBDMAAUD_STAT0_DONE = BIT(2), + HDMI_IH_MUTE_AHBDMAAUD_STAT0_BUFFFULL = BIT(1), + HDMI_IH_MUTE_AHBDMAAUD_STAT0_BUFFEMPTY = BIT(0), + HDMI_IH_MUTE_AHBDMAAUD_STAT0_ALL = + HDMI_IH_MUTE_AHBDMAAUD_STAT0_ERROR | + HDMI_IH_MUTE_AHBDMAAUD_STAT0_LOST | + HDMI_IH_MUTE_AHBDMAAUD_STAT0_RETRY | + HDMI_IH_MUTE_AHBDMAAUD_STAT0_DONE | + HDMI_IH_MUTE_AHBDMAAUD_STAT0_BUFFFULL | + HDMI_IH_MUTE_AHBDMAAUD_STAT0_BUFFEMPTY, + HDMI_IH_AHBDMAAUD_STAT0_ERROR = BIT(5), + HDMI_IH_AHBDMAAUD_STAT0_LOST = BIT(4), + HDMI_IH_AHBDMAAUD_STAT0_RETRY = BIT(3), + HDMI_IH_AHBDMAAUD_STAT0_DONE = BIT(2), + HDMI_IH_AHBDMAAUD_STAT0_BUFFFULL = BIT(1), + HDMI_IH_AHBDMAAUD_STAT0_BUFFEMPTY = BIT(0), + HDMI_IH_AHBDMAAUD_STAT0_ALL = + HDMI_IH_AHBDMAAUD_STAT0_ERROR | + HDMI_IH_AHBDMAAUD_STAT0_LOST | + HDMI_IH_AHBDMAAUD_STAT0_RETRY | + HDMI_IH_AHBDMAAUD_STAT0_DONE | + HDMI_IH_AHBDMAAUD_STAT0_BUFFFULL | + HDMI_IH_AHBDMAAUD_STAT0_BUFFEMPTY, + HDMI_AHB_DMA_CONF0_INCR16 = 2 << 1, + HDMI_AHB_DMA_CONF0_INCR8 = 1 << 1, + HDMI_AHB_DMA_CONF0_INCR4 = 0, + HDMI_AHB_DMA_CONF0_BURST_MODE = BIT(0), + HDMI_AHB_DMA_MASK_DONE = BIT(7), + HDMI_REVISION_ID = 0x0001, + HDMI_IH_AHBDMAAUD_STAT0 = 0x0109, + HDMI_IH_MUTE_AHBDMAAUD_STAT0 = 0x0189, + HDMI_AHB_DMA_CONF0 = 0x3600, + HDMI_AHB_DMA_START = 0x3601, + HDMI_AHB_DMA_STOP = 0x3602, + HDMI_AHB_DMA_THRSLD = 0x3603, + HDMI_AHB_DMA_STRADDR0 = 0x3604, + HDMI_AHB_DMA_STPADDR0 = 0x3608, + HDMI_AHB_DMA_MASK = 0x3614, + HDMI_AHB_DMA_POL = 0x3615, + HDMI_AHB_DMA_CONF1 = 0x3616, + HDMI_AHB_DMA_BUFFPOL = 0x361a, +}; + +struct snd_dw_hdmi { + struct snd_card *card; + struct snd_pcm *pcm; + spinlock_t lock; + struct dw_hdmi_audio_data data; + struct snd_pcm_substream *substream; + void (*reformat)(struct snd_dw_hdmi *, size_t, size_t); + void *buf_src; + void *buf_dst; + dma_addr_t buf_addr; + unsigned buf_offset; + unsigned buf_period; + unsigned buf_size; + unsigned channels; + u8 revision; + u8 iec_offset; + u8 cs[192][8]; +}; + +static void dw_hdmi_writel(u32 val, void __iomem *ptr) +{ + writeb_relaxed(val, ptr); + writeb_relaxed(val >> 8, ptr + 1); + writeb_relaxed(val >> 16, ptr + 2); + writeb_relaxed(val >> 24, ptr + 3); +} + +/* + * Convert to hardware format: The userspace buffer contains IEC958 samples, + * with the PCUV bits in bits 31..28 and audio samples in bits 27..4. We + * need these to be in bits 27..24, with the IEC B bit in bit 28, and audio + * samples in 23..0. + * + * Default preamble in bits 3..0: 8 = block start, 4 = even 2 = odd + * + * Ideally, we could do with having the data properly formatted in userspace. + */ +static void dw_hdmi_reformat_iec958(struct snd_dw_hdmi *dw, + size_t offset, size_t bytes) +{ + u32 *src = dw->buf_src + offset; + u32 *dst = dw->buf_dst + offset; + u32 *end = dw->buf_src + offset + bytes; + + do { + u32 b, sample = *src++; + + b = (sample & 8) << (28 - 3); + + sample >>= 4; + + *dst++ = sample | b; + } while (src < end); +} + +static u32 parity(u32 sample) +{ + sample ^= sample >> 16; + sample ^= sample >> 8; + sample ^= sample >> 4; + sample ^= sample >> 2; + sample ^= sample >> 1; + return (sample & 1) << 27; +} + +static void dw_hdmi_reformat_s24(struct snd_dw_hdmi *dw, + size_t offset, size_t bytes) +{ + u32 *src = dw->buf_src + offset; + u32 *dst = dw->buf_dst + offset; + u32 *end = dw->buf_src + offset + bytes; + + do { + unsigned i; + u8 *cs; + + cs = dw->cs[dw->iec_offset++]; + if (dw->iec_offset >= 192) + dw->iec_offset = 0; + + i = dw->channels; + do { + u32 sample = *src++; + + sample &= ~0xff000000; + sample |= *cs++ << 24; + sample |= parity(sample & ~0xf8000000); + + *dst++ = sample; + } while (--i); + } while (src < end); +} + +static void dw_hdmi_create_cs(struct snd_dw_hdmi *dw, + struct snd_pcm_runtime *runtime) +{ + u8 cs[4]; + unsigned ch, i, j; + + snd_pcm_create_iec958_consumer(runtime, cs, sizeof(cs)); + + memset(dw->cs, 0, sizeof(dw->cs)); + + for (ch = 0; ch < 8; ch++) { + cs[2] &= ~IEC958_AES2_CON_CHANNEL; + cs[2] |= (ch + 1) << 4; + + for (i = 0; i < ARRAY_SIZE(cs); i++) { + unsigned c = cs[i]; + + for (j = 0; j < 8; j++, c >>= 1) + dw->cs[i * 8 + j][ch] = (c & 1) << 2; + } + } + dw->cs[0][0] |= BIT(4); +} + +static void dw_hdmi_start_dma(struct snd_dw_hdmi *dw) +{ + void __iomem *base = dw->data.base; + unsigned offset = dw->buf_offset; + unsigned period = dw->buf_period; + u32 start, stop; + + dw->reformat(dw, offset, period); + + /* Clear all irqs before enabling irqs and starting DMA */ + writeb_relaxed(HDMI_IH_AHBDMAAUD_STAT0_ALL, + base + HDMI_IH_AHBDMAAUD_STAT0); + + start = dw->buf_addr + offset; + stop = start + period - 1; + + /* Setup the hardware start/stop addresses */ + dw_hdmi_writel(start, base + HDMI_AHB_DMA_STRADDR0); + dw_hdmi_writel(stop, base + HDMI_AHB_DMA_STPADDR0); + + writeb_relaxed((u8)~HDMI_AHB_DMA_MASK_DONE, base + HDMI_AHB_DMA_MASK); + writeb(HDMI_AHB_DMA_START_START, base + HDMI_AHB_DMA_START); + + offset += period; + if (offset >= dw->buf_size) + offset = 0; + dw->buf_offset = offset; +} + +static void dw_hdmi_stop_dma(struct snd_dw_hdmi *dw) +{ + /* Disable interrupts before disabling DMA */ + writeb_relaxed(~0, dw->data.base + HDMI_AHB_DMA_MASK); + writeb_relaxed(HDMI_AHB_DMA_STOP_STOP, dw->data.base + HDMI_AHB_DMA_STOP); +} + +static irqreturn_t snd_dw_hdmi_irq(int irq, void *data) +{ + struct snd_dw_hdmi *dw = data; + struct snd_pcm_substream *substream; + unsigned stat; + + stat = readb_relaxed(dw->data.base + HDMI_IH_AHBDMAAUD_STAT0); + if (!stat) + return IRQ_NONE; + + writeb_relaxed(stat, dw->data.base + HDMI_IH_AHBDMAAUD_STAT0); + + substream = dw->substream; + if (stat & HDMI_IH_AHBDMAAUD_STAT0_DONE && substream) { + snd_pcm_period_elapsed(substream); + + spin_lock(&dw->lock); + if (dw->substream) + dw_hdmi_start_dma(dw); + spin_unlock(&dw->lock); + } + + return IRQ_HANDLED; +} + +static struct snd_pcm_hardware dw_hdmi_hw = { + .info = SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_BLOCK_TRANSFER | + SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_MMAP_VALID, + .formats = SNDRV_PCM_FMTBIT_IEC958_SUBFRAME_LE | + SNDRV_PCM_FMTBIT_S24_LE, + .rates = SNDRV_PCM_RATE_32000 | + SNDRV_PCM_RATE_44100 | + SNDRV_PCM_RATE_48000 | + SNDRV_PCM_RATE_88200 | + SNDRV_PCM_RATE_96000 | + SNDRV_PCM_RATE_176400 | + SNDRV_PCM_RATE_192000, + .channels_min = 2, + .channels_max = 8, + .buffer_bytes_max = 64 * 1024, + .period_bytes_min = 256, + .period_bytes_max = 8192, /* ERR004323: must limit to 8k */ + .periods_min = 2, + .periods_max = 16, + .fifo_size = 0, +}; + +static int dw_hdmi_open(struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct snd_dw_hdmi *dw = substream->private_data; + void __iomem *base = dw->data.base; + int ret; + + runtime->hw = dw_hdmi_hw; + + ret = snd_pcm_limit_hw_rates(runtime); + if (ret < 0) + return ret; + + ret = snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS); + if (ret < 0) + return ret; + + /* Clear FIFO */ + writeb_relaxed(HDMI_AHB_DMA_CONF0_SW_FIFO_RST, + base + HDMI_AHB_DMA_CONF0); + + /* Configure interrupt polarities */ + writeb_relaxed(~0, base + HDMI_AHB_DMA_POL); + writeb_relaxed(~0, base + HDMI_AHB_DMA_BUFFPOL); + + /* Keep interrupts masked, and clear any pending */ + writeb_relaxed(~0, base + HDMI_AHB_DMA_MASK); + writeb_relaxed(~0, base + HDMI_IH_AHBDMAAUD_STAT0); + + ret = request_irq(dw->data.irq, snd_dw_hdmi_irq, IRQF_SHARED, + "dw-hdmi-audio", dw); + if (ret) + return ret; + + /* Un-mute done interrupt */ + writeb_relaxed(HDMI_IH_MUTE_AHBDMAAUD_STAT0_ALL & + ~HDMI_IH_MUTE_AHBDMAAUD_STAT0_DONE, + base + HDMI_IH_MUTE_AHBDMAAUD_STAT0); + + return 0; +} + +static int dw_hdmi_close(struct snd_pcm_substream *substream) +{ + struct snd_dw_hdmi *dw = substream->private_data; + + /* Mute all interrupts */ + writeb_relaxed(HDMI_IH_MUTE_AHBDMAAUD_STAT0_ALL, + dw->data.base + HDMI_IH_MUTE_AHBDMAAUD_STAT0); + + free_irq(dw->data.irq, dw); + + return 0; +} + +static int dw_hdmi_hw_free(struct snd_pcm_substream *substream) +{ + return snd_pcm_lib_free_vmalloc_buffer(substream); +} + +static int dw_hdmi_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + return snd_pcm_lib_alloc_vmalloc_buffer(substream, + params_buffer_bytes(params)); +} + +static int dw_hdmi_prepare(struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct snd_dw_hdmi *dw = substream->private_data; + u8 threshold, conf0, conf1; + + /* Setup as per 3.0.5 FSL 4.1.0 BSP */ + switch (dw->revision) { + case 0x0a: + conf0 = HDMI_AHB_DMA_CONF0_BURST_MODE | + HDMI_AHB_DMA_CONF0_INCR4; + if (runtime->channels == 2) + threshold = 126; + else + threshold = 124; + break; + case 0x1a: + conf0 = HDMI_AHB_DMA_CONF0_BURST_MODE | + HDMI_AHB_DMA_CONF0_INCR8; + threshold = 128; + break; + default: + /* NOTREACHED */ + return -EINVAL; + } + + dw_hdmi_set_sample_rate(dw->data.hdmi, runtime->rate); + + /* Minimum number of bytes in the fifo. */ + runtime->hw.fifo_size = threshold * 32; + + conf0 |= HDMI_AHB_DMA_CONF0_EN_HLOCK; + conf1 = (1 << runtime->channels) - 1; + + writeb_relaxed(threshold, dw->data.base + HDMI_AHB_DMA_THRSLD); + writeb_relaxed(conf0, dw->data.base + HDMI_AHB_DMA_CONF0); + writeb_relaxed(conf1, dw->data.base + HDMI_AHB_DMA_CONF1); + + switch (runtime->format) { + case SNDRV_PCM_FORMAT_IEC958_SUBFRAME_LE: + dw->reformat = dw_hdmi_reformat_iec958; + break; + case SNDRV_PCM_FORMAT_S24_LE: + dw_hdmi_create_cs(dw, runtime); + dw->reformat = dw_hdmi_reformat_s24; + break; + } + dw->iec_offset = 0; + dw->channels = runtime->channels; + dw->buf_src = runtime->dma_area; + dw->buf_dst = substream->dma_buffer.area; + dw->buf_addr = substream->dma_buffer.addr; + dw->buf_period = snd_pcm_lib_period_bytes(substream); + dw->buf_size = snd_pcm_lib_buffer_bytes(substream); + + return 0; +} + +static int dw_hdmi_trigger(struct snd_pcm_substream *substream, int cmd) +{ + struct snd_dw_hdmi *dw = substream->private_data; + unsigned long flags; + int ret = 0; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + spin_lock_irqsave(&dw->lock, flags); + dw->buf_offset = 0; + dw->substream = substream; + dw_hdmi_start_dma(dw); + dw_hdmi_audio_enable(dw->data.hdmi); + spin_unlock_irqrestore(&dw->lock, flags); + substream->runtime->delay = substream->runtime->period_size; + break; + + case SNDRV_PCM_TRIGGER_STOP: + spin_lock_irqsave(&dw->lock, flags); + dw->substream = NULL; + dw_hdmi_stop_dma(dw); + dw_hdmi_audio_disable(dw->data.hdmi); + spin_unlock_irqrestore(&dw->lock, flags); + break; + + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static snd_pcm_uframes_t dw_hdmi_pointer(struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct snd_dw_hdmi *dw = substream->private_data; + + /* + * We are unable to report the exact hardware position as + * reading the 32-bit DMA position using 8-bit reads is racy. + */ + return bytes_to_frames(runtime, dw->buf_offset); +} + +static struct snd_pcm_ops snd_dw_hdmi_ops = { + .open = dw_hdmi_open, + .close = dw_hdmi_close, + .ioctl = snd_pcm_lib_ioctl, + .hw_params = dw_hdmi_hw_params, + .hw_free = dw_hdmi_hw_free, + .prepare = dw_hdmi_prepare, + .trigger = dw_hdmi_trigger, + .pointer = dw_hdmi_pointer, + .page = snd_pcm_lib_get_vmalloc_page, +}; + +static int snd_dw_hdmi_probe(struct platform_device *pdev) +{ + const struct dw_hdmi_audio_data *data = pdev->dev.platform_data; + struct device *dev = pdev->dev.parent; + struct snd_dw_hdmi *dw; + struct snd_card *card; + struct snd_pcm *pcm; + unsigned revision; + int ret; + + writeb_relaxed(HDMI_IH_MUTE_AHBDMAAUD_STAT0_ALL, + data->base + HDMI_IH_MUTE_AHBDMAAUD_STAT0); + revision = readb_relaxed(data->base + HDMI_REVISION_ID); + if (revision != 0x0a && revision != 0x1a) { + dev_err(dev, "dw-hdmi-audio: unknown revision 0x%02x\n", + revision); + return -ENXIO; + } + + ret = snd_card_new(dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1, + THIS_MODULE, sizeof(struct snd_dw_hdmi), &card); + if (ret < 0) + return ret; + + strlcpy(card->driver, DRIVER_NAME, sizeof(card->driver)); + strlcpy(card->shortname, "DW-HDMI", sizeof(card->shortname)); + snprintf(card->longname, sizeof(card->longname), + "%s rev 0x%02x, irq %d", card->shortname, revision, + data->irq); + + dw = card->private_data; + dw->card = card; + dw->data = *data; + dw->revision = revision; + + spin_lock_init(&dw->lock); + + ret = snd_pcm_new(card, "DW HDMI", 0, 1, 0, &pcm); + if (ret < 0) + goto err; + + dw->pcm = pcm; + pcm->private_data = dw; + strlcpy(pcm->name, DRIVER_NAME, sizeof(pcm->name)); + snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &snd_dw_hdmi_ops); + + snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_DEV, + dev, 64 * 1024, 64 * 1024); + + ret = snd_card_register(card); + if (ret < 0) + goto err; + + platform_set_drvdata(pdev, dw); + + return 0; + +err: + snd_card_free(card); + return ret; +} + +static int snd_dw_hdmi_remove(struct platform_device *pdev) +{ + struct snd_dw_hdmi *dw = platform_get_drvdata(pdev); + + snd_card_free(dw->card); + + return 0; +} + +#if defined(CONFIG_PM_SLEEP) && defined(IS_NOT_BROKEN) +/* + * This code is fine, but requires implementation in the dw_hdmi_trigger() + * method which is currently missing as I have no way to test this. + */ +static int snd_dw_hdmi_suspend(struct device *dev) +{ + struct snd_dw_hdmi *dw = dev_get_drvdata(dev); + + snd_power_change_state(dw->card, SNDRV_CTL_POWER_D3cold); + snd_pcm_suspend_all(dw->pcm); + + return 0; +} + +static int snd_dw_hdmi_resume(struct device *dev) +{ + struct snd_dw_hdmi *dw = dev_get_drvdata(dev); + + snd_power_change_state(dw->card, SNDRV_CTL_POWER_D0); + + return 0; +} + +static SIMPLE_DEV_PM_OPS(snd_dw_hdmi_pm, snd_dw_hdmi_suspend, + snd_dw_hdmi_resume); +#define PM_OPS &snd_dw_hdmi_pm +#else +#define PM_OPS NULL +#endif + +static struct platform_driver snd_dw_hdmi_driver = { + .probe = snd_dw_hdmi_probe, + .remove = snd_dw_hdmi_remove, + .driver = { + .name = DRIVER_NAME, + .owner = THIS_MODULE, + .pm = PM_OPS, + }, +}; + +module_platform_driver(snd_dw_hdmi_driver); + +MODULE_AUTHOR("Russell King "); +MODULE_DESCRIPTION("Synopsis Designware HDMI AHB ALSA interface"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:" DRIVER_NAME); diff --git a/drivers/gpu/drm/bridge/dw_hdmi-audio.h b/drivers/gpu/drm/bridge/dw_hdmi-audio.h new file mode 100644 index 000000000000..1e840118d90a --- /dev/null +++ b/drivers/gpu/drm/bridge/dw_hdmi-audio.h @@ -0,0 +1,13 @@ +#ifndef DW_HDMI_AUDIO_H +#define DW_HDMI_AUDIO_H + +struct dw_hdmi; + +struct dw_hdmi_audio_data { + phys_addr_t phys; + void __iomem *base; + int irq; + struct dw_hdmi *hdmi; +}; + +#endif diff --git a/drivers/gpu/drm/bridge/dw_hdmi.c b/drivers/gpu/drm/bridge/dw_hdmi.c index 7a130f6fdb3d..d61a9fb46c6d 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi.c +++ b/drivers/gpu/drm/bridge/dw_hdmi.c @@ -28,6 +28,7 @@ #include #include "dw_hdmi.h" +#include "dw_hdmi-audio.h" #define HDMI_EDID_LEN 512 @@ -104,6 +105,7 @@ struct dw_hdmi { struct drm_encoder *encoder; struct drm_bridge *bridge; + struct platform_device *audio; enum dw_hdmi_devtype dev_type; struct device *dev; struct clk *isfr_clk; @@ -1732,7 +1734,9 @@ int dw_hdmi_bind(struct device *dev, struct device *master, { struct drm_device *drm = data; struct device_node *np = dev->of_node; + struct platform_device_info pdevinfo; struct device_node *ddc_node; + struct dw_hdmi_audio_data audio; struct dw_hdmi *hdmi; int ret; u32 val = 1; @@ -1860,6 +1864,23 @@ int dw_hdmi_bind(struct device *dev, struct device *master, hdmi_writeb(hdmi, ~(HDMI_IH_PHY_STAT0_HPD | HDMI_IH_PHY_STAT0_RX_SENSE), HDMI_IH_MUTE_PHY_STAT0); + memset(&pdevinfo, 0, sizeof(pdevinfo)); + pdevinfo.parent = dev; + pdevinfo.id = PLATFORM_DEVID_AUTO; + + if (hdmi_readb(hdmi, HDMI_CONFIG1_ID) & HDMI_CONFIG1_AHB) { + audio.phys = iores->start; + audio.base = hdmi->regs; + audio.irq = irq; + audio.hdmi = hdmi; + + pdevinfo.name = "dw-hdmi-ahb-audio"; + pdevinfo.data = &audio; + pdevinfo.size_data = sizeof(audio); + pdevinfo.dma_mask = DMA_BIT_MASK(32); + hdmi->audio = platform_device_register_full(&pdevinfo); + } + dev_set_drvdata(dev, hdmi); return 0; @@ -1877,6 +1898,9 @@ void dw_hdmi_unbind(struct device *dev, struct device *master, void *data) { struct dw_hdmi *hdmi = dev_get_drvdata(dev); + if (hdmi->audio && !IS_ERR(hdmi->audio)) + platform_device_unregister(hdmi->audio); + /* Disable all interrupts */ hdmi_writeb(hdmi, ~0, HDMI_IH_MUTE_PHY_STAT0); diff --git a/drivers/gpu/drm/bridge/dw_hdmi.h b/drivers/gpu/drm/bridge/dw_hdmi.h index ee7f7ed2ab12..fc9a560429d6 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi.h +++ b/drivers/gpu/drm/bridge/dw_hdmi.h @@ -545,6 +545,9 @@ #define HDMI_I2CM_FS_SCL_LCNT_0_ADDR 0x7E12 enum { +/* CONFIG1_ID field values */ + HDMI_CONFIG1_AHB = 0x01, + /* IH_FC_INT2 field values */ HDMI_IH_FC_INT2_OVERFLOW_MASK = 0x03, HDMI_IH_FC_INT2_LOW_PRIORITY_OVERFLOW = 0x02, From f5ce405722e410a69da4604285ae5e9210011ffe Mon Sep 17 00:00:00 2001 From: Russell King Date: Thu, 7 Nov 2013 16:06:01 +0000 Subject: [PATCH 07/13] drm: bridge/dw_hdmi-ahb-audio: parse ELD from HDMI driver Parse the ELD (EDID like data) stored from the HDMI driver to restrict the sample rates and channels which are available to ALSA. This causes the ALSA device to reflect the capabilities of the overall audio path, not just what is supported at the HDMI source interface level. Tested-by: Fabio Estevam Signed-off-by: Russell King --- drivers/gpu/drm/bridge/Kconfig | 1 + drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c | 6 ++++++ drivers/gpu/drm/bridge/dw_hdmi-audio.h | 1 + drivers/gpu/drm/bridge/dw_hdmi.c | 3 +++ 4 files changed, 11 insertions(+) diff --git a/drivers/gpu/drm/bridge/Kconfig b/drivers/gpu/drm/bridge/Kconfig index 0d0a4f7f4f5c..6dddd392aa42 100644 --- a/drivers/gpu/drm/bridge/Kconfig +++ b/drivers/gpu/drm/bridge/Kconfig @@ -15,6 +15,7 @@ config DRM_DW_HDMI_AHB_AUDIO tristate "Synopsis Designware AHB Audio interface" depends on DRM_DW_HDMI && SND select SND_PCM + select SND_PCM_ELD select SND_PCM_IEC958 help Support the AHB Audio interface which is part of the Synopsis diff --git a/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c b/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c index bf379310008a..fbcef2695c93 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c +++ b/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c @@ -12,11 +12,13 @@ #include #include #include +#include #include #include #include #include +#include #include #include "dw_hdmi-audio.h" @@ -286,6 +288,10 @@ static int dw_hdmi_open(struct snd_pcm_substream *substream) runtime->hw = dw_hdmi_hw; + ret = snd_pcm_hw_constraint_eld(runtime, dw->data.eld); + if (ret < 0) + return ret; + ret = snd_pcm_limit_hw_rates(runtime); if (ret < 0) return ret; diff --git a/drivers/gpu/drm/bridge/dw_hdmi-audio.h b/drivers/gpu/drm/bridge/dw_hdmi-audio.h index 1e840118d90a..91f631beecc7 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi-audio.h +++ b/drivers/gpu/drm/bridge/dw_hdmi-audio.h @@ -8,6 +8,7 @@ struct dw_hdmi_audio_data { void __iomem *base; int irq; struct dw_hdmi *hdmi; + u8 *eld; }; #endif diff --git a/drivers/gpu/drm/bridge/dw_hdmi.c b/drivers/gpu/drm/bridge/dw_hdmi.c index d61a9fb46c6d..70e7a938a37b 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi.c +++ b/drivers/gpu/drm/bridge/dw_hdmi.c @@ -1533,6 +1533,8 @@ static int dw_hdmi_connector_get_modes(struct drm_connector *connector) hdmi->sink_has_audio = drm_detect_monitor_audio(edid); drm_mode_connector_update_edid_property(connector, edid); ret = drm_add_edid_modes(connector, edid); + /* Store the ELD */ + drm_edid_to_eld(connector, edid); kfree(edid); } else { dev_dbg(hdmi->dev, "failed to get edid\n"); @@ -1873,6 +1875,7 @@ int dw_hdmi_bind(struct device *dev, struct device *master, audio.base = hdmi->regs; audio.irq = irq; audio.hdmi = hdmi; + audio.eld = hdmi->connector.eld; pdevinfo.name = "dw-hdmi-ahb-audio"; pdevinfo.data = &audio; From 9dc515f8dfc8c2b5d4e8355933e6ac71abeb7b1e Mon Sep 17 00:00:00 2001 From: Russell King Date: Thu, 4 Jun 2015 10:13:28 +0100 Subject: [PATCH 08/13] drm: bridge/dw_hdmi-ahb-audio: basic support for multi-channel PCM audio Add basic support for multi-channel PCM audio, with fixed speaker mappings. This has been tested with an AV receiver, and appears to work for low sample rates up to 8 channels. It should be noted that multi-channel mode using the IEC958 alsa-lib conversion plugin requires correct AES channel status for the AV receiver to recognise the stream, especially the sample rate bits. "Not identified" does not work there. Tested-by: Fabio Estevam Signed-off-by: Russell King --- drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c | 59 +++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c b/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c index fbcef2695c93..e779c16f1e67 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c +++ b/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c @@ -62,9 +62,14 @@ enum { HDMI_AHB_DMA_CONF0_INCR4 = 0, HDMI_AHB_DMA_CONF0_BURST_MODE = BIT(0), HDMI_AHB_DMA_MASK_DONE = BIT(7), + HDMI_REVISION_ID = 0x0001, HDMI_IH_AHBDMAAUD_STAT0 = 0x0109, HDMI_IH_MUTE_AHBDMAAUD_STAT0 = 0x0189, + HDMI_FC_AUDICONF2 = 0x1027, + HDMI_FC_AUDSCONF = 0x1063, + HDMI_FC_AUDSCONF_LAYOUT1 = 1 << 0, + HDMI_FC_AUDSCONF_LAYOUT0 = 0 << 0, HDMI_AHB_DMA_CONF0 = 0x3600, HDMI_AHB_DMA_START = 0x3601, HDMI_AHB_DMA_STOP = 0x3602, @@ -77,6 +82,44 @@ enum { HDMI_AHB_DMA_BUFFPOL = 0x361a, }; +struct dw_hdmi_channel_conf { + u8 conf1; + u8 ca; +}; + +/* + * The default mapping of ALSA channels to HDMI channels and speaker + * allocation bits. Note that we can't do channel remapping here - + * channels must be in the same order. + * + * Mappings for alsa-lib pcm/surround*.conf files: + * + * Front Sur4.0 Sur4.1 Sur5.0 Sur5.1 Sur7.1 + * Channels 2 4 6 6 6 8 + * + * Our mapping from ALSA channel to CEA686D speaker name and HDMI channel: + * + * Number of ALSA channels + * ALSA Channel 2 3 4 5 6 7 8 + * 0 FL:0 = = = = = = + * 1 FR:1 = = = = = = + * 2 FC:3 RL:4 LFE:2 = = = + * 3 RR:5 RL:4 FC:3 = = + * 4 RR:5 RL:4 = = + * 5 RR:5 = = + * 6 RC:6 = + * 7 RLC/FRC RLC/FRC + */ +static struct dw_hdmi_channel_conf default_hdmi_channel_config[7] = { + { 0x03, 0x00 }, /* FL,FR */ + { 0x0b, 0x02 }, /* FL,FR,FC */ + { 0x33, 0x08 }, /* FL,FR,RL,RR */ + { 0x37, 0x09 }, /* FL,FR,LFE,RL,RR */ + { 0x3f, 0x0b }, /* FL,FR,LFE,FC,RL,RR */ + { 0x7f, 0x0f }, /* FL,FR,LFE,FC,RL,RR,RC */ + { 0xff, 0x13 }, /* FL,FR,LFE,FC,RL,RR,[FR]RC,[FR]LC */ +}; + struct snd_dw_hdmi { struct snd_card *card; struct snd_pcm *pcm; @@ -354,7 +397,7 @@ static int dw_hdmi_prepare(struct snd_pcm_substream *substream) { struct snd_pcm_runtime *runtime = substream->runtime; struct snd_dw_hdmi *dw = substream->private_data; - u8 threshold, conf0, conf1; + u8 threshold, conf0, conf1, layout, ca; /* Setup as per 3.0.5 FSL 4.1.0 BSP */ switch (dw->revision) { @@ -382,11 +425,23 @@ static int dw_hdmi_prepare(struct snd_pcm_substream *substream) runtime->hw.fifo_size = threshold * 32; conf0 |= HDMI_AHB_DMA_CONF0_EN_HLOCK; - conf1 = (1 << runtime->channels) - 1; + conf1 = default_hdmi_channel_config[runtime->channels - 2].conf1; + ca = default_hdmi_channel_config[runtime->channels - 2].ca; + + /* + * For >2 channel PCM audio, we need to select layout 1 + * and set an appropriate channel map. + */ + if (runtime->channels > 2) + layout = HDMI_FC_AUDSCONF_LAYOUT1; + else + layout = HDMI_FC_AUDSCONF_LAYOUT0; writeb_relaxed(threshold, dw->data.base + HDMI_AHB_DMA_THRSLD); writeb_relaxed(conf0, dw->data.base + HDMI_AHB_DMA_CONF0); writeb_relaxed(conf1, dw->data.base + HDMI_AHB_DMA_CONF1); + writeb_relaxed(layout, dw->data.base + HDMI_FC_AUDSCONF); + writeb_relaxed(ca, dw->data.base + HDMI_FC_AUDICONF2); switch (runtime->format) { case SNDRV_PCM_FORMAT_IEC958_SUBFRAME_LE: From 91cd69088ecc375d057eb3721bfbcf927e9f2dd2 Mon Sep 17 00:00:00 2001 From: Russell King Date: Thu, 4 Jun 2015 10:24:33 +0100 Subject: [PATCH 09/13] drm: bridge/dw_hdmi-ahb-audio: allow larger buffer sizes With multichannel audio, we need to allow larger buffer sizes to avoid XRUNs during playback. Push the buffer size up to 1024K, but as we maintain two buffers, ensure that the vmalloc buffer does not exceed the userspace buffer size. Tested-by: Fabio Estevam Signed-off-by: Russell King --- drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c b/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c index e779c16f1e67..59f630f1c61a 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c +++ b/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c @@ -314,7 +314,7 @@ static struct snd_pcm_hardware dw_hdmi_hw = { SNDRV_PCM_RATE_192000, .channels_min = 2, .channels_max = 8, - .buffer_bytes_max = 64 * 1024, + .buffer_bytes_max = 1024 * 1024, .period_bytes_min = 256, .period_bytes_max = 8192, /* ERR004323: must limit to 8k */ .periods_min = 2, @@ -339,7 +339,15 @@ static int dw_hdmi_open(struct snd_pcm_substream *substream) if (ret < 0) return ret; - ret = snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS); + ret = snd_pcm_hw_constraint_integer(runtime, + SNDRV_PCM_HW_PARAM_PERIODS); + if (ret < 0) + return ret; + + /* Limit the buffer size to the size of the preallocated buffer */ + ret = snd_pcm_hw_constraint_minmax(runtime, + SNDRV_PCM_HW_PARAM_BUFFER_SIZE, + 0, substream->dma_buffer.bytes); if (ret < 0) return ret; @@ -389,6 +397,7 @@ static int dw_hdmi_hw_free(struct snd_pcm_substream *substream) static int dw_hdmi_hw_params(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params) { + /* Allocate the PCM runtime buffer, which is exposed to userspace. */ return snd_pcm_lib_alloc_vmalloc_buffer(substream, params_buffer_bytes(params)); } @@ -566,8 +575,12 @@ static int snd_dw_hdmi_probe(struct platform_device *pdev) strlcpy(pcm->name, DRIVER_NAME, sizeof(pcm->name)); snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &snd_dw_hdmi_ops); + /* + * To support 8-channel 96kHz audio reliably, we need 512k + * to satisfy alsa with our restricted period (ERR004323). + */ snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_DEV, - dev, 64 * 1024, 64 * 1024); + dev, 128 * 1024, 1024 * 1024); ret = snd_card_register(card); if (ret < 0) From d0c96d168021a60555c692d860334814cc694d8b Mon Sep 17 00:00:00 2001 From: Russell King Date: Wed, 22 Jul 2015 10:35:41 +0100 Subject: [PATCH 10/13] drm: bridge/dw_hdmi: avoid being recursive in N calculation There's no need to be recursive when computing the N value for the ACR packet - we can instead calculate the multiplier prior to our switch() based lookup, and multiply the N value appropriately afterwards. Tested-by: Fabio Estevam Signed-off-by: Russell King --- drivers/gpu/drm/bridge/dw_hdmi.c | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/drivers/gpu/drm/bridge/dw_hdmi.c b/drivers/gpu/drm/bridge/dw_hdmi.c index 70e7a938a37b..e5ee92370f49 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi.c +++ b/drivers/gpu/drm/bridge/dw_hdmi.c @@ -221,6 +221,12 @@ static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk, unsigned int ratio) { unsigned int n = (128 * freq) / 1000; + unsigned int mult = 1; + + while (freq > 48000) { + mult *= 2; + freq /= 2; + } switch (freq) { case 32000: @@ -232,6 +238,7 @@ static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk, n = 11648; else n = 4096; + n *= mult; break; case 44100: @@ -243,6 +250,7 @@ static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk, n = (ratio == 150) ? 17836 : 8918; else n = 6272; + n *= mult; break; case 48000: @@ -256,22 +264,7 @@ static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk, n = (ratio == 150) ? 11648 : 5824; else n = 6144; - break; - - case 88200: - n = hdmi_compute_n(44100, pixel_clk, ratio) * 2; - break; - - case 96000: - n = hdmi_compute_n(48000, pixel_clk, ratio) * 2; - break; - - case 176400: - n = hdmi_compute_n(44100, pixel_clk, ratio) * 4; - break; - - case 192000: - n = hdmi_compute_n(48000, pixel_clk, ratio) * 4; + n *= mult; break; default: From 426701d0ccc13a2819fd99938ec50a888817e77d Mon Sep 17 00:00:00 2001 From: Russell King Date: Wed, 22 Jul 2015 10:39:27 +0100 Subject: [PATCH 11/13] drm: bridge/dw_hdmi: adjust pixel clock values in N calculation Adjust the pixel clock values in the N calculation to match the more accurate clock values we're given by the DRM subsystem, which are the kHz pixel rate, with any fractional kHz rounded down in the case of the non-240, non-480 line modes, or rounded up for the others. So, 25.20 / 1.001 => 25175 27.00 * 1.001 => 27027 74.25 / 1.001 => 74176 148.50 / 1.001 => 148352 DRM derives these rates from the EDID CEA mode identifiers, which are looked up in the tables in drivers/gpu/drm/drm_edid.c. The values on the right are the clock values found in these tables, and are currently expected to be passed to the HDMI driver unchanged. Tested-by: Fabio Estevam Signed-off-by: Russell King --- drivers/gpu/drm/bridge/dw_hdmi.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/drivers/gpu/drm/bridge/dw_hdmi.c b/drivers/gpu/drm/bridge/dw_hdmi.c index e5ee92370f49..2ef13d92eaae 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi.c +++ b/drivers/gpu/drm/bridge/dw_hdmi.c @@ -230,11 +230,11 @@ static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk, switch (freq) { case 32000: - if (pixel_clk == 25170000) + if (pixel_clk == 25175000) n = (ratio == 150) ? 9152 : 4576; - else if (pixel_clk == 27020000) + else if (pixel_clk == 27027000) n = (ratio == 150) ? 8192 : 4096; - else if (pixel_clk == 74170000 || pixel_clk == 148350000) + else if (pixel_clk == 74176000 || pixel_clk == 148352000) n = 11648; else n = 4096; @@ -242,11 +242,11 @@ static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk, break; case 44100: - if (pixel_clk == 25170000) + if (pixel_clk == 25175000) n = 7007; - else if (pixel_clk == 74170000) + else if (pixel_clk == 74176000) n = 17836; - else if (pixel_clk == 148350000) + else if (pixel_clk == 148352000) n = (ratio == 150) ? 17836 : 8918; else n = 6272; @@ -254,13 +254,13 @@ static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk, break; case 48000: - if (pixel_clk == 25170000) + if (pixel_clk == 25175000) n = (ratio == 150) ? 9152 : 6864; - else if (pixel_clk == 27020000) + else if (pixel_clk == 27027000) n = (ratio == 150) ? 8192 : 6144; - else if (pixel_clk == 74170000) + else if (pixel_clk == 74176000) n = 11648; - else if (pixel_clk == 148350000) + else if (pixel_clk == 148352000) n = (ratio == 150) ? 11648 : 5824; else n = 6144; From b195fbdbdf5f6712beec09bb9009f99ff9ecd912 Mon Sep 17 00:00:00 2001 From: Russell King Date: Wed, 22 Jul 2015 11:28:16 +0100 Subject: [PATCH 12/13] drm: bridge/dw_hdmi: remove ratio support from ACR code We never set the ratio for CTS/N calculation for the audio clock regenerator (ACR) to anything but 100, so this adds pointless complexity. Should we support pixel repetition, we should update the CTS/N calculation code to use those parameters or the actual TMDS clock rate instead of a ratio. Tested-by: Fabio Estevam Signed-off-by: Russell King --- drivers/gpu/drm/bridge/dw_hdmi.c | 44 +++++++++++++------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/drivers/gpu/drm/bridge/dw_hdmi.c b/drivers/gpu/drm/bridge/dw_hdmi.c index 2ef13d92eaae..62ee3027d08c 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi.c +++ b/drivers/gpu/drm/bridge/dw_hdmi.c @@ -140,7 +140,6 @@ struct dw_hdmi { unsigned int audio_cts; unsigned int audio_n; bool audio_enable; - int ratio; void (*write)(struct dw_hdmi *hdmi, u8 val, int offset); u8 (*read)(struct dw_hdmi *hdmi, int offset); @@ -217,8 +216,7 @@ static void hdmi_set_cts_n(struct dw_hdmi *hdmi, unsigned int cts, hdmi_writeb(hdmi, n & 0xff, HDMI_AUD_N1); } -static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk, - unsigned int ratio) +static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk) { unsigned int n = (128 * freq) / 1000; unsigned int mult = 1; @@ -231,9 +229,9 @@ static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk, switch (freq) { case 32000: if (pixel_clk == 25175000) - n = (ratio == 150) ? 9152 : 4576; + n = 4576; else if (pixel_clk == 27027000) - n = (ratio == 150) ? 8192 : 4096; + n = 4096; else if (pixel_clk == 74176000 || pixel_clk == 148352000) n = 11648; else @@ -247,7 +245,7 @@ static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk, else if (pixel_clk == 74176000) n = 17836; else if (pixel_clk == 148352000) - n = (ratio == 150) ? 17836 : 8918; + n = 8918; else n = 6272; n *= mult; @@ -255,13 +253,13 @@ static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk, case 48000: if (pixel_clk == 25175000) - n = (ratio == 150) ? 9152 : 6864; + n = 6864; else if (pixel_clk == 27027000) - n = (ratio == 150) ? 8192 : 6144; + n = 6144; else if (pixel_clk == 74176000) n = 11648; else if (pixel_clk == 148352000) - n = (ratio == 150) ? 11648 : 5824; + n = 5824; else n = 6144; n *= mult; @@ -274,13 +272,11 @@ static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk, return n; } -static unsigned int hdmi_compute_cts(unsigned int freq, unsigned long pixel_clk, - unsigned int ratio) +static unsigned int hdmi_compute_cts(unsigned int freq, unsigned long pixel_clk) { unsigned int cts = 0; - pr_debug("%s: freq: %d pixel_clk: %ld ratio: %d\n", __func__, freq, - pixel_clk, ratio); + pr_debug("%s: freq: %d pixel_clk: %ld\n", __func__, freq, pixel_clk); switch (freq) { case 32000: @@ -341,26 +337,24 @@ static unsigned int hdmi_compute_cts(unsigned int freq, unsigned long pixel_clk, default: break; } - if (ratio == 100) - return cts; - return (cts * ratio) / 100; + return cts; } static void hdmi_set_clk_regenerator(struct dw_hdmi *hdmi, - unsigned long pixel_clk, unsigned int sample_rate, unsigned int ratio) + unsigned long pixel_clk, unsigned int sample_rate) { unsigned int n, cts; - n = hdmi_compute_n(sample_rate, pixel_clk, ratio); - cts = hdmi_compute_cts(sample_rate, pixel_clk, ratio); + n = hdmi_compute_n(sample_rate, pixel_clk); + cts = hdmi_compute_cts(sample_rate, pixel_clk); if (!cts) { dev_err(hdmi->dev, "%s: pixel clock/sample rate not supported: %luMHz / %ukHz\n", __func__, pixel_clk, sample_rate); } - dev_dbg(hdmi->dev, "%s: samplerate=%ukHz ratio=%d pixelclk=%luMHz N=%d cts=%d\n", - __func__, sample_rate, ratio, pixel_clk, n, cts); + dev_dbg(hdmi->dev, "%s: samplerate=%ukHz pixelclk=%luMHz N=%d cts=%d\n", + __func__, sample_rate, pixel_clk, n, cts); spin_lock_irq(&hdmi->audio_lock); hdmi->audio_n = n; @@ -372,8 +366,7 @@ static void hdmi_set_clk_regenerator(struct dw_hdmi *hdmi, static void hdmi_init_clk_regenerator(struct dw_hdmi *hdmi) { mutex_lock(&hdmi->audio_mutex); - hdmi_set_clk_regenerator(hdmi, 74250000, hdmi->sample_rate, - hdmi->ratio); + hdmi_set_clk_regenerator(hdmi, 74250000, hdmi->sample_rate); mutex_unlock(&hdmi->audio_mutex); } @@ -381,7 +374,7 @@ static void hdmi_clk_regenerator_update_pixel_clock(struct dw_hdmi *hdmi) { mutex_lock(&hdmi->audio_mutex); hdmi_set_clk_regenerator(hdmi, hdmi->hdmi_data.video_mode.mpixelclock, - hdmi->sample_rate, hdmi->ratio); + hdmi->sample_rate); mutex_unlock(&hdmi->audio_mutex); } @@ -390,7 +383,7 @@ void dw_hdmi_set_sample_rate(struct dw_hdmi *hdmi, unsigned int rate) mutex_lock(&hdmi->audio_mutex); hdmi->sample_rate = rate; hdmi_set_clk_regenerator(hdmi, hdmi->hdmi_data.video_mode.mpixelclock, - hdmi->sample_rate, hdmi->ratio); + hdmi->sample_rate); mutex_unlock(&hdmi->audio_mutex); } EXPORT_SYMBOL_GPL(dw_hdmi_set_sample_rate); @@ -1746,7 +1739,6 @@ int dw_hdmi_bind(struct device *dev, struct device *master, hdmi->dev = dev; hdmi->dev_type = plat_data->dev_type; hdmi->sample_rate = 48000; - hdmi->ratio = 100; hdmi->encoder = encoder; hdmi->disabled = true; hdmi->rxsense = true; From dfbdaf50460479446a258ef781683e7d7d6349d7 Mon Sep 17 00:00:00 2001 From: Russell King Date: Wed, 22 Jul 2015 16:54:37 +0100 Subject: [PATCH 13/13] drm: bridge/dw_hdmi: replace CTS calculation for the ACR Given the TDMS clock, audio sample rate, and the N parameter, we can calculate the CTS value for the audio clock regenerator (ACR) using the following calculation given in the HDMI specification: CTS = ftdms * N / (128 * fs) The specification says that the CTS value is an average value, which is true if the source hardware measures it. Where source hardware needs it to be programmed, it is particularly difficult to alternate between two values correctly to ensure that we achieve a correct "average" fractional value at the sink. Also, there's the problem that our "ftdms" is not a fully accurate value; it is rounded to a kHz value. This introduces an unnecessary (and harmless) fractional value into the above equation for combinations like 148.5MHz/1.001 for 44100Hz - we still calculate the correct CTS value. Tested-by: Fabio Estevam Signed-off-by: Russell King --- drivers/gpu/drm/bridge/dw_hdmi.c | 92 ++++++-------------------------- 1 file changed, 16 insertions(+), 76 deletions(-) diff --git a/drivers/gpu/drm/bridge/dw_hdmi.c b/drivers/gpu/drm/bridge/dw_hdmi.c index 62ee3027d08c..56de9f1c95fc 100644 --- a/drivers/gpu/drm/bridge/dw_hdmi.c +++ b/drivers/gpu/drm/bridge/dw_hdmi.c @@ -272,89 +272,29 @@ static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk) return n; } -static unsigned int hdmi_compute_cts(unsigned int freq, unsigned long pixel_clk) -{ - unsigned int cts = 0; - - pr_debug("%s: freq: %d pixel_clk: %ld\n", __func__, freq, pixel_clk); - - switch (freq) { - case 32000: - if (pixel_clk == 297000000) { - cts = 222750; - break; - } - case 48000: - case 96000: - case 192000: - switch (pixel_clk) { - case 25200000: - case 27000000: - case 54000000: - case 74250000: - case 148500000: - cts = pixel_clk / 1000; - break; - case 297000000: - cts = 247500; - break; - /* - * All other TMDS clocks are not supported by - * DWC_hdmi_tx. The TMDS clocks divided or - * multiplied by 1,001 coefficients are not - * supported. - */ - default: - break; - } - break; - case 44100: - case 88200: - case 176400: - switch (pixel_clk) { - case 25200000: - cts = 28000; - break; - case 27000000: - cts = 30000; - break; - case 54000000: - cts = 60000; - break; - case 74250000: - cts = 82500; - break; - case 148500000: - cts = 165000; - break; - case 297000000: - cts = 247500; - break; - default: - break; - } - break; - default: - break; - } - return cts; -} - static void hdmi_set_clk_regenerator(struct dw_hdmi *hdmi, unsigned long pixel_clk, unsigned int sample_rate) { + unsigned long ftdms = pixel_clk; unsigned int n, cts; + u64 tmp; n = hdmi_compute_n(sample_rate, pixel_clk); - cts = hdmi_compute_cts(sample_rate, pixel_clk); - if (!cts) { - dev_err(hdmi->dev, - "%s: pixel clock/sample rate not supported: %luMHz / %ukHz\n", - __func__, pixel_clk, sample_rate); - } - dev_dbg(hdmi->dev, "%s: samplerate=%ukHz pixelclk=%luMHz N=%d cts=%d\n", - __func__, sample_rate, pixel_clk, n, cts); + /* + * Compute the CTS value from the N value. Note that CTS and N + * can be up to 20 bits in total, so we need 64-bit math. Also + * note that our TDMS clock is not fully accurate; it is accurate + * to kHz. This can introduce an unnecessary remainder in the + * calculation below, so we don't try to warn about that. + */ + tmp = (u64)ftdms * n; + do_div(tmp, 128 * sample_rate); + cts = tmp; + + dev_dbg(hdmi->dev, "%s: fs=%uHz ftdms=%lu.%03luMHz N=%d cts=%d\n", + __func__, sample_rate, ftdms / 1000000, (ftdms / 1000) % 1000, + n, cts); spin_lock_irq(&hdmi->audio_lock); hdmi->audio_n = n;