| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * Copyright (c) 2021-2022 Rockchip Electronics Co., Ltd. |
| * Copyright (c) 2024 Collabora Ltd. |
| * |
| * Author: Algea Cao <algea.cao@rock-chips.com> |
| * Author: Cristian Ciocaltea <cristian.ciocaltea@collabora.com> |
| */ |
| #include <linux/completion.h> |
| #include <linux/hdmi.h> |
| #include <linux/export.h> |
| #include <linux/i2c.h> |
| #include <linux/irq.h> |
| #include <linux/module.h> |
| #include <linux/mutex.h> |
| #include <linux/of.h> |
| #include <linux/workqueue.h> |
| |
| #include <drm/bridge/dw_hdmi_qp.h> |
| #include <drm/display/drm_hdmi_helper.h> |
| #include <drm/display/drm_hdmi_state_helper.h> |
| #include <drm/drm_atomic.h> |
| #include <drm/drm_atomic_helper.h> |
| #include <drm/drm_bridge.h> |
| #include <drm/drm_connector.h> |
| #include <drm/drm_edid.h> |
| #include <drm/drm_modes.h> |
| |
| #include <sound/hdmi-codec.h> |
| |
| #include "dw-hdmi-qp.h" |
| |
| #define DDC_CI_ADDR 0x37 |
| #define DDC_SEGMENT_ADDR 0x30 |
| |
| #define HDMI14_MAX_TMDSCLK 340000000 |
| |
| #define SCRAMB_POLL_DELAY_MS 3000 |
| |
| /* |
| * Unless otherwise noted, entries in this table are 100% optimization. |
| * Values can be obtained from dw_hdmi_qp_compute_n() but that function is |
| * slow so we pre-compute values we expect to see. |
| * |
| * The values for TMDS 25175, 25200, 27000, 54000, 74250 and 148500 kHz are |
| * the recommended N values specified in the Audio chapter of the HDMI |
| * specification. |
| */ |
| static const struct dw_hdmi_audio_tmds_n { |
| unsigned long tmds; |
| unsigned int n_32k; |
| unsigned int n_44k1; |
| unsigned int n_48k; |
| } common_tmds_n_table[] = { |
| { .tmds = 25175000, .n_32k = 4576, .n_44k1 = 7007, .n_48k = 6864, }, |
| { .tmds = 25200000, .n_32k = 4096, .n_44k1 = 6272, .n_48k = 6144, }, |
| { .tmds = 27000000, .n_32k = 4096, .n_44k1 = 6272, .n_48k = 6144, }, |
| { .tmds = 28320000, .n_32k = 4096, .n_44k1 = 5586, .n_48k = 6144, }, |
| { .tmds = 30240000, .n_32k = 4096, .n_44k1 = 5642, .n_48k = 6144, }, |
| { .tmds = 31500000, .n_32k = 4096, .n_44k1 = 5600, .n_48k = 6144, }, |
| { .tmds = 32000000, .n_32k = 4096, .n_44k1 = 5733, .n_48k = 6144, }, |
| { .tmds = 33750000, .n_32k = 4096, .n_44k1 = 6272, .n_48k = 6144, }, |
| { .tmds = 36000000, .n_32k = 4096, .n_44k1 = 5684, .n_48k = 6144, }, |
| { .tmds = 40000000, .n_32k = 4096, .n_44k1 = 5733, .n_48k = 6144, }, |
| { .tmds = 49500000, .n_32k = 4096, .n_44k1 = 5488, .n_48k = 6144, }, |
| { .tmds = 50000000, .n_32k = 4096, .n_44k1 = 5292, .n_48k = 6144, }, |
| { .tmds = 54000000, .n_32k = 4096, .n_44k1 = 6272, .n_48k = 6144, }, |
| { .tmds = 65000000, .n_32k = 4096, .n_44k1 = 7056, .n_48k = 6144, }, |
| { .tmds = 68250000, .n_32k = 4096, .n_44k1 = 5376, .n_48k = 6144, }, |
| { .tmds = 71000000, .n_32k = 4096, .n_44k1 = 7056, .n_48k = 6144, }, |
| { .tmds = 72000000, .n_32k = 4096, .n_44k1 = 5635, .n_48k = 6144, }, |
| { .tmds = 73250000, .n_32k = 11648, .n_44k1 = 14112, .n_48k = 6144, }, |
| { .tmds = 74250000, .n_32k = 4096, .n_44k1 = 6272, .n_48k = 6144, }, |
| { .tmds = 75000000, .n_32k = 4096, .n_44k1 = 5880, .n_48k = 6144, }, |
| { .tmds = 78750000, .n_32k = 4096, .n_44k1 = 5600, .n_48k = 6144, }, |
| { .tmds = 78800000, .n_32k = 4096, .n_44k1 = 5292, .n_48k = 6144, }, |
| { .tmds = 79500000, .n_32k = 4096, .n_44k1 = 4704, .n_48k = 6144, }, |
| { .tmds = 83500000, .n_32k = 4096, .n_44k1 = 7056, .n_48k = 6144, }, |
| { .tmds = 85500000, .n_32k = 4096, .n_44k1 = 5488, .n_48k = 6144, }, |
| { .tmds = 88750000, .n_32k = 4096, .n_44k1 = 14112, .n_48k = 6144, }, |
| { .tmds = 97750000, .n_32k = 4096, .n_44k1 = 14112, .n_48k = 6144, }, |
| { .tmds = 101000000, .n_32k = 4096, .n_44k1 = 7056, .n_48k = 6144, }, |
| { .tmds = 106500000, .n_32k = 4096, .n_44k1 = 4704, .n_48k = 6144, }, |
| { .tmds = 108000000, .n_32k = 4096, .n_44k1 = 5684, .n_48k = 6144, }, |
| { .tmds = 115500000, .n_32k = 4096, .n_44k1 = 5712, .n_48k = 6144, }, |
| { .tmds = 119000000, .n_32k = 4096, .n_44k1 = 5544, .n_48k = 6144, }, |
| { .tmds = 135000000, .n_32k = 4096, .n_44k1 = 5488, .n_48k = 6144, }, |
| { .tmds = 146250000, .n_32k = 11648, .n_44k1 = 6272, .n_48k = 6144, }, |
| { .tmds = 148500000, .n_32k = 4096, .n_44k1 = 6272, .n_48k = 6144, }, |
| { .tmds = 154000000, .n_32k = 4096, .n_44k1 = 5544, .n_48k = 6144, }, |
| { .tmds = 162000000, .n_32k = 4096, .n_44k1 = 5684, .n_48k = 6144, }, |
| |
| /* For 297 MHz+ HDMI spec have some other rule for setting N */ |
| { .tmds = 297000000, .n_32k = 3073, .n_44k1 = 4704, .n_48k = 5120, }, |
| { .tmds = 594000000, .n_32k = 3073, .n_44k1 = 9408, .n_48k = 10240,}, |
| |
| /* End of table */ |
| { .tmds = 0, .n_32k = 0, .n_44k1 = 0, .n_48k = 0, }, |
| }; |
| |
| /* |
| * These are the CTS values as recommended in the Audio chapter of the HDMI |
| * specification. |
| */ |
| static const struct dw_hdmi_audio_tmds_cts { |
| unsigned long tmds; |
| unsigned int cts_32k; |
| unsigned int cts_44k1; |
| unsigned int cts_48k; |
| } common_tmds_cts_table[] = { |
| { .tmds = 25175000, .cts_32k = 28125, .cts_44k1 = 31250, .cts_48k = 28125, }, |
| { .tmds = 25200000, .cts_32k = 25200, .cts_44k1 = 28000, .cts_48k = 25200, }, |
| { .tmds = 27000000, .cts_32k = 27000, .cts_44k1 = 30000, .cts_48k = 27000, }, |
| { .tmds = 54000000, .cts_32k = 54000, .cts_44k1 = 60000, .cts_48k = 54000, }, |
| { .tmds = 74250000, .cts_32k = 74250, .cts_44k1 = 82500, .cts_48k = 74250, }, |
| { .tmds = 148500000, .cts_32k = 148500, .cts_44k1 = 165000, .cts_48k = 148500, }, |
| |
| /* End of table */ |
| { .tmds = 0, .cts_32k = 0, .cts_44k1 = 0, .cts_48k = 0, }, |
| }; |
| |
| struct dw_hdmi_qp_i2c { |
| struct i2c_adapter adap; |
| |
| struct mutex lock; /* used to serialize data transfers */ |
| struct completion cmp; |
| u8 stat; |
| |
| u8 slave_reg; |
| bool is_regaddr; |
| bool is_segment; |
| }; |
| |
| struct dw_hdmi_qp { |
| struct drm_bridge bridge; |
| |
| struct device *dev; |
| struct dw_hdmi_qp_i2c *i2c; |
| |
| struct { |
| const struct dw_hdmi_qp_phy_ops *ops; |
| void *data; |
| } phy; |
| |
| struct regmap *regm; |
| |
| unsigned long tmds_char_rate; |
| }; |
| |
| static void dw_hdmi_qp_write(struct dw_hdmi_qp *hdmi, unsigned int val, |
| int offset) |
| { |
| regmap_write(hdmi->regm, offset, val); |
| } |
| |
| static unsigned int dw_hdmi_qp_read(struct dw_hdmi_qp *hdmi, int offset) |
| { |
| unsigned int val = 0; |
| |
| regmap_read(hdmi->regm, offset, &val); |
| |
| return val; |
| } |
| |
| static void dw_hdmi_qp_mod(struct dw_hdmi_qp *hdmi, unsigned int data, |
| unsigned int mask, unsigned int reg) |
| { |
| regmap_update_bits(hdmi->regm, reg, mask, data); |
| } |
| |
| static struct dw_hdmi_qp *dw_hdmi_qp_from_bridge(struct drm_bridge *bridge) |
| { |
| return container_of(bridge, struct dw_hdmi_qp, bridge); |
| } |
| |
| static void dw_hdmi_qp_set_cts_n(struct dw_hdmi_qp *hdmi, unsigned int cts, |
| unsigned int n) |
| { |
| /* Set N */ |
| dw_hdmi_qp_mod(hdmi, n, AUDPKT_ACR_N_VALUE, AUDPKT_ACR_CONTROL0); |
| |
| /* Set CTS */ |
| if (cts) |
| dw_hdmi_qp_mod(hdmi, AUDPKT_ACR_CTS_OVR_EN, AUDPKT_ACR_CTS_OVR_EN_MSK, |
| AUDPKT_ACR_CONTROL1); |
| else |
| dw_hdmi_qp_mod(hdmi, 0, AUDPKT_ACR_CTS_OVR_EN_MSK, |
| AUDPKT_ACR_CONTROL1); |
| |
| dw_hdmi_qp_mod(hdmi, AUDPKT_ACR_CTS_OVR_VAL(cts), AUDPKT_ACR_CTS_OVR_VAL_MSK, |
| AUDPKT_ACR_CONTROL1); |
| } |
| |
| static int dw_hdmi_qp_match_tmds_n_table(struct dw_hdmi_qp *hdmi, |
| unsigned long pixel_clk, |
| unsigned long freq) |
| { |
| const struct dw_hdmi_audio_tmds_n *tmds_n = NULL; |
| int i; |
| |
| for (i = 0; common_tmds_n_table[i].tmds != 0; i++) { |
| if (pixel_clk == common_tmds_n_table[i].tmds) { |
| tmds_n = &common_tmds_n_table[i]; |
| break; |
| } |
| } |
| |
| if (!tmds_n) |
| return -ENOENT; |
| |
| switch (freq) { |
| case 32000: |
| return tmds_n->n_32k; |
| case 44100: |
| case 88200: |
| case 176400: |
| return (freq / 44100) * tmds_n->n_44k1; |
| case 48000: |
| case 96000: |
| case 192000: |
| return (freq / 48000) * tmds_n->n_48k; |
| default: |
| return -ENOENT; |
| } |
| } |
| |
| static u32 dw_hdmi_qp_audio_math_diff(unsigned int freq, unsigned int n, |
| unsigned int pixel_clk) |
| { |
| u64 cts = mul_u32_u32(pixel_clk, n); |
| |
| return do_div(cts, 128 * freq); |
| } |
| |
| static unsigned int dw_hdmi_qp_compute_n(struct dw_hdmi_qp *hdmi, |
| unsigned long pixel_clk, |
| unsigned long freq) |
| { |
| unsigned int min_n = DIV_ROUND_UP((128 * freq), 1500); |
| unsigned int max_n = (128 * freq) / 300; |
| unsigned int ideal_n = (128 * freq) / 1000; |
| unsigned int best_n_distance = ideal_n; |
| unsigned int best_n = 0; |
| u64 best_diff = U64_MAX; |
| int n; |
| |
| /* If the ideal N could satisfy the audio math, then just take it */ |
| if (dw_hdmi_qp_audio_math_diff(freq, ideal_n, pixel_clk) == 0) |
| return ideal_n; |
| |
| for (n = min_n; n <= max_n; n++) { |
| u64 diff = dw_hdmi_qp_audio_math_diff(freq, n, pixel_clk); |
| |
| if (diff < best_diff || |
| (diff == best_diff && abs(n - ideal_n) < best_n_distance)) { |
| best_n = n; |
| best_diff = diff; |
| best_n_distance = abs(best_n - ideal_n); |
| } |
| |
| /* |
| * The best N already satisfy the audio math, and also be |
| * the closest value to ideal N, so just cut the loop. |
| */ |
| if (best_diff == 0 && (abs(n - ideal_n) > best_n_distance)) |
| break; |
| } |
| |
| return best_n; |
| } |
| |
| static unsigned int dw_hdmi_qp_find_n(struct dw_hdmi_qp *hdmi, unsigned long pixel_clk, |
| unsigned long sample_rate) |
| { |
| int n = dw_hdmi_qp_match_tmds_n_table(hdmi, pixel_clk, sample_rate); |
| |
| if (n > 0) |
| return n; |
| |
| dev_warn(hdmi->dev, "Rate %lu missing; compute N dynamically\n", |
| pixel_clk); |
| |
| return dw_hdmi_qp_compute_n(hdmi, pixel_clk, sample_rate); |
| } |
| |
| static unsigned int dw_hdmi_qp_find_cts(struct dw_hdmi_qp *hdmi, unsigned long pixel_clk, |
| unsigned long sample_rate) |
| { |
| const struct dw_hdmi_audio_tmds_cts *tmds_cts = NULL; |
| int i; |
| |
| for (i = 0; common_tmds_cts_table[i].tmds != 0; i++) { |
| if (pixel_clk == common_tmds_cts_table[i].tmds) { |
| tmds_cts = &common_tmds_cts_table[i]; |
| break; |
| } |
| } |
| |
| if (!tmds_cts) |
| return 0; |
| |
| switch (sample_rate) { |
| case 32000: |
| return tmds_cts->cts_32k; |
| case 44100: |
| case 88200: |
| case 176400: |
| return tmds_cts->cts_44k1; |
| case 48000: |
| case 96000: |
| case 192000: |
| return tmds_cts->cts_48k; |
| default: |
| return -ENOENT; |
| } |
| } |
| |
| static void dw_hdmi_qp_set_audio_interface(struct dw_hdmi_qp *hdmi, |
| struct hdmi_codec_daifmt *fmt, |
| struct hdmi_codec_params *hparms) |
| { |
| u32 conf0 = 0; |
| |
| /* Reset the audio data path of the AVP */ |
| dw_hdmi_qp_write(hdmi, AVP_DATAPATH_PACKET_AUDIO_SWINIT_P, GLOBAL_SWRESET_REQUEST); |
| |
| /* Disable AUDS, ACR, AUDI */ |
| dw_hdmi_qp_mod(hdmi, 0, |
| PKTSCHED_ACR_TX_EN | PKTSCHED_AUDS_TX_EN | PKTSCHED_AUDI_TX_EN, |
| PKTSCHED_PKT_EN); |
| |
| /* Clear the audio FIFO */ |
| dw_hdmi_qp_write(hdmi, AUDIO_FIFO_CLR_P, AUDIO_INTERFACE_CONTROL0); |
| |
| /* Select I2S interface as the audio source */ |
| dw_hdmi_qp_mod(hdmi, AUD_IF_I2S, AUD_IF_SEL_MSK, AUDIO_INTERFACE_CONFIG0); |
| |
| /* Enable the active i2s lanes */ |
| switch (hparms->channels) { |
| case 7 ... 8: |
| conf0 |= I2S_LINES_EN(3); |
| fallthrough; |
| case 5 ... 6: |
| conf0 |= I2S_LINES_EN(2); |
| fallthrough; |
| case 3 ... 4: |
| conf0 |= I2S_LINES_EN(1); |
| fallthrough; |
| default: |
| conf0 |= I2S_LINES_EN(0); |
| break; |
| } |
| |
| dw_hdmi_qp_mod(hdmi, conf0, I2S_LINES_EN_MSK, AUDIO_INTERFACE_CONFIG0); |
| |
| /* |
| * Enable bpcuv generated internally for L-PCM, or received |
| * from stream for NLPCM/HBR. |
| */ |
| switch (fmt->bit_fmt) { |
| case SNDRV_PCM_FORMAT_IEC958_SUBFRAME_LE: |
| conf0 = (hparms->channels == 8) ? AUD_HBR : AUD_ASP; |
| conf0 |= I2S_BPCUV_RCV_EN; |
| break; |
| default: |
| conf0 = AUD_ASP | I2S_BPCUV_RCV_DIS; |
| break; |
| } |
| |
| dw_hdmi_qp_mod(hdmi, conf0, I2S_BPCUV_RCV_MSK | AUD_FORMAT_MSK, |
| AUDIO_INTERFACE_CONFIG0); |
| |
| /* Enable audio FIFO auto clear when overflow */ |
| dw_hdmi_qp_mod(hdmi, AUD_FIFO_INIT_ON_OVF_EN, AUD_FIFO_INIT_ON_OVF_MSK, |
| AUDIO_INTERFACE_CONFIG0); |
| } |
| |
| /* |
| * When transmitting IEC60958 linear PCM audio, these registers allow to |
| * configure the channel status information of all the channel status |
| * bits in the IEC60958 frame. For the moment this configuration is only |
| * used when the I2S audio interface, General Purpose Audio (GPA), |
| * or AHB audio DMA (AHBAUDDMA) interface is active |
| * (for S/PDIF interface this information comes from the stream). |
| */ |
| static void dw_hdmi_qp_set_channel_status(struct dw_hdmi_qp *hdmi, |
| u8 *channel_status, bool ref2stream) |
| { |
| /* |
| * AUDPKT_CHSTATUS_OVR0: { RSV, RSV, CS1, CS0 } |
| * AUDPKT_CHSTATUS_OVR1: { CS6, CS5, CS4, CS3 } |
| * |
| * | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
| * CS0: | Mode | d | c | b | a | |
| * CS1: | Category Code | |
| * CS2: | Channel Number | Source Number | |
| * CS3: | Clock Accuracy | Sample Freq | |
| * CS4: | Ori Sample Freq | Word Length | |
| * CS5: | | CGMS-A | |
| * CS6~CS23: Reserved |
| * |
| * a: use of channel status block |
| * b: linear PCM identification: 0 for lpcm, 1 for nlpcm |
| * c: copyright information |
| * d: additional format information |
| */ |
| |
| if (ref2stream) |
| channel_status[0] |= IEC958_AES0_NONAUDIO; |
| |
| if ((dw_hdmi_qp_read(hdmi, AUDIO_INTERFACE_CONFIG0) & GENMASK(25, 24)) == AUD_HBR) { |
| /* fixup cs for HBR */ |
| channel_status[3] = (channel_status[3] & 0xf0) | IEC958_AES3_CON_FS_768000; |
| channel_status[4] = (channel_status[4] & 0x0f) | IEC958_AES4_CON_ORIGFS_NOTID; |
| } |
| |
| dw_hdmi_qp_write(hdmi, channel_status[0] | (channel_status[1] << 8), |
| AUDPKT_CHSTATUS_OVR0); |
| |
| regmap_bulk_write(hdmi->regm, AUDPKT_CHSTATUS_OVR1, &channel_status[3], 1); |
| |
| if (ref2stream) |
| dw_hdmi_qp_mod(hdmi, 0, |
| AUDPKT_PBIT_FORCE_EN_MASK | AUDPKT_CHSTATUS_OVR_EN_MASK, |
| AUDPKT_CONTROL0); |
| else |
| dw_hdmi_qp_mod(hdmi, AUDPKT_PBIT_FORCE_EN | AUDPKT_CHSTATUS_OVR_EN, |
| AUDPKT_PBIT_FORCE_EN_MASK | AUDPKT_CHSTATUS_OVR_EN_MASK, |
| AUDPKT_CONTROL0); |
| } |
| |
| static void dw_hdmi_qp_set_sample_rate(struct dw_hdmi_qp *hdmi, unsigned long long tmds_char_rate, |
| unsigned int sample_rate) |
| { |
| unsigned int n, cts; |
| |
| n = dw_hdmi_qp_find_n(hdmi, tmds_char_rate, sample_rate); |
| cts = dw_hdmi_qp_find_cts(hdmi, tmds_char_rate, sample_rate); |
| |
| dw_hdmi_qp_set_cts_n(hdmi, cts, n); |
| } |
| |
| static int dw_hdmi_qp_audio_enable(struct drm_bridge *bridge, |
| struct drm_connector *connector) |
| { |
| struct dw_hdmi_qp *hdmi = dw_hdmi_qp_from_bridge(bridge); |
| |
| if (hdmi->tmds_char_rate) |
| dw_hdmi_qp_mod(hdmi, 0, AVP_DATAPATH_PACKET_AUDIO_SWDISABLE, GLOBAL_SWDISABLE); |
| |
| return 0; |
| } |
| |
| static int dw_hdmi_qp_audio_prepare(struct drm_bridge *bridge, |
| struct drm_connector *connector, |
| struct hdmi_codec_daifmt *fmt, |
| struct hdmi_codec_params *hparms) |
| { |
| struct dw_hdmi_qp *hdmi = dw_hdmi_qp_from_bridge(bridge); |
| bool ref2stream = false; |
| |
| if (!hdmi->tmds_char_rate) |
| return -ENODEV; |
| |
| if (fmt->bit_clk_provider | fmt->frame_clk_provider) { |
| dev_err(hdmi->dev, "unsupported clock settings\n"); |
| return -EINVAL; |
| } |
| |
| if (fmt->bit_fmt == SNDRV_PCM_FORMAT_IEC958_SUBFRAME_LE) |
| ref2stream = true; |
| |
| dw_hdmi_qp_set_audio_interface(hdmi, fmt, hparms); |
| dw_hdmi_qp_set_sample_rate(hdmi, hdmi->tmds_char_rate, hparms->sample_rate); |
| dw_hdmi_qp_set_channel_status(hdmi, hparms->iec.status, ref2stream); |
| drm_atomic_helper_connector_hdmi_update_audio_infoframe(connector, &hparms->cea); |
| |
| return 0; |
| } |
| |
| static void dw_hdmi_qp_audio_disable_regs(struct dw_hdmi_qp *hdmi) |
| { |
| /* |
| * Keep ACR, AUDI, AUDS packet always on to make SINK device |
| * active for better compatibility and user experience. |
| * |
| * This also fix POP sound on some SINK devices which wakeup |
| * from suspend to active. |
| */ |
| dw_hdmi_qp_mod(hdmi, I2S_BPCUV_RCV_DIS, I2S_BPCUV_RCV_MSK, |
| AUDIO_INTERFACE_CONFIG0); |
| dw_hdmi_qp_mod(hdmi, AUDPKT_PBIT_FORCE_EN | AUDPKT_CHSTATUS_OVR_EN, |
| AUDPKT_PBIT_FORCE_EN_MASK | AUDPKT_CHSTATUS_OVR_EN_MASK, |
| AUDPKT_CONTROL0); |
| |
| dw_hdmi_qp_mod(hdmi, AVP_DATAPATH_PACKET_AUDIO_SWDISABLE, |
| AVP_DATAPATH_PACKET_AUDIO_SWDISABLE, GLOBAL_SWDISABLE); |
| } |
| |
| static void dw_hdmi_qp_audio_disable(struct drm_bridge *bridge, |
| struct drm_connector *connector) |
| { |
| struct dw_hdmi_qp *hdmi = dw_hdmi_qp_from_bridge(bridge); |
| |
| drm_atomic_helper_connector_hdmi_clear_audio_infoframe(connector); |
| |
| if (hdmi->tmds_char_rate) |
| dw_hdmi_qp_audio_disable_regs(hdmi); |
| } |
| |
| static int dw_hdmi_qp_i2c_read(struct dw_hdmi_qp *hdmi, |
| unsigned char *buf, unsigned int length) |
| { |
| struct dw_hdmi_qp_i2c *i2c = hdmi->i2c; |
| int stat; |
| |
| if (!i2c->is_regaddr) { |
| dev_dbg(hdmi->dev, "set read register address to 0\n"); |
| i2c->slave_reg = 0x00; |
| i2c->is_regaddr = true; |
| } |
| |
| while (length--) { |
| reinit_completion(&i2c->cmp); |
| |
| dw_hdmi_qp_mod(hdmi, i2c->slave_reg++ << 12, I2CM_ADDR, |
| I2CM_INTERFACE_CONTROL0); |
| |
| if (i2c->is_segment) |
| dw_hdmi_qp_mod(hdmi, I2CM_EXT_READ, I2CM_WR_MASK, |
| I2CM_INTERFACE_CONTROL0); |
| else |
| dw_hdmi_qp_mod(hdmi, I2CM_FM_READ, I2CM_WR_MASK, |
| I2CM_INTERFACE_CONTROL0); |
| |
| stat = wait_for_completion_timeout(&i2c->cmp, HZ / 10); |
| if (!stat) { |
| dev_err(hdmi->dev, "i2c read timed out\n"); |
| dw_hdmi_qp_write(hdmi, 0x01, I2CM_CONTROL0); |
| return -EAGAIN; |
| } |
| |
| /* Check for error condition on the bus */ |
| if (i2c->stat & I2CM_NACK_RCVD_IRQ) { |
| dev_err(hdmi->dev, "i2c read error\n"); |
| dw_hdmi_qp_write(hdmi, 0x01, I2CM_CONTROL0); |
| return -EIO; |
| } |
| |
| *buf++ = dw_hdmi_qp_read(hdmi, I2CM_INTERFACE_RDDATA_0_3) & 0xff; |
| dw_hdmi_qp_mod(hdmi, 0, I2CM_WR_MASK, I2CM_INTERFACE_CONTROL0); |
| } |
| |
| i2c->is_segment = false; |
| |
| return 0; |
| } |
| |
| static int dw_hdmi_qp_i2c_write(struct dw_hdmi_qp *hdmi, |
| unsigned char *buf, unsigned int length) |
| { |
| struct dw_hdmi_qp_i2c *i2c = hdmi->i2c; |
| int stat; |
| |
| if (!i2c->is_regaddr) { |
| /* Use the first write byte as register address */ |
| i2c->slave_reg = buf[0]; |
| length--; |
| buf++; |
| i2c->is_regaddr = true; |
| } |
| |
| while (length--) { |
| reinit_completion(&i2c->cmp); |
| |
| dw_hdmi_qp_write(hdmi, *buf++, I2CM_INTERFACE_WRDATA_0_3); |
| dw_hdmi_qp_mod(hdmi, i2c->slave_reg++ << 12, I2CM_ADDR, |
| I2CM_INTERFACE_CONTROL0); |
| dw_hdmi_qp_mod(hdmi, I2CM_FM_WRITE, I2CM_WR_MASK, |
| I2CM_INTERFACE_CONTROL0); |
| |
| stat = wait_for_completion_timeout(&i2c->cmp, HZ / 10); |
| if (!stat) { |
| dev_err(hdmi->dev, "i2c write time out!\n"); |
| dw_hdmi_qp_write(hdmi, 0x01, I2CM_CONTROL0); |
| return -EAGAIN; |
| } |
| |
| /* Check for error condition on the bus */ |
| if (i2c->stat & I2CM_NACK_RCVD_IRQ) { |
| dev_err(hdmi->dev, "i2c write nack!\n"); |
| dw_hdmi_qp_write(hdmi, 0x01, I2CM_CONTROL0); |
| return -EIO; |
| } |
| |
| dw_hdmi_qp_mod(hdmi, 0, I2CM_WR_MASK, I2CM_INTERFACE_CONTROL0); |
| } |
| |
| return 0; |
| } |
| |
| static int dw_hdmi_qp_i2c_xfer(struct i2c_adapter *adap, |
| struct i2c_msg *msgs, int num) |
| { |
| struct dw_hdmi_qp *hdmi = i2c_get_adapdata(adap); |
| struct dw_hdmi_qp_i2c *i2c = hdmi->i2c; |
| u8 addr = msgs[0].addr; |
| int i, ret = 0; |
| |
| if (addr == DDC_CI_ADDR) |
| /* |
| * The internal I2C controller does not support the multi-byte |
| * read and write operations needed for DDC/CI. |
| * FIXME: Blacklist the DDC/CI address until we filter out |
| * unsupported I2C operations. |
| */ |
| return -EOPNOTSUPP; |
| |
| for (i = 0; i < num; i++) { |
| if (msgs[i].len == 0) { |
| dev_err(hdmi->dev, |
| "unsupported transfer %d/%d, no data\n", |
| i + 1, num); |
| return -EOPNOTSUPP; |
| } |
| } |
| |
| guard(mutex)(&i2c->lock); |
| |
| /* Unmute DONE and ERROR interrupts */ |
| dw_hdmi_qp_mod(hdmi, I2CM_NACK_RCVD_MASK_N | I2CM_OP_DONE_MASK_N, |
| I2CM_NACK_RCVD_MASK_N | I2CM_OP_DONE_MASK_N, |
| MAINUNIT_1_INT_MASK_N); |
| |
| /* Set slave device address taken from the first I2C message */ |
| if (addr == DDC_SEGMENT_ADDR && msgs[0].len == 1) |
| addr = DDC_ADDR; |
| |
| dw_hdmi_qp_mod(hdmi, addr << 5, I2CM_SLVADDR, I2CM_INTERFACE_CONTROL0); |
| |
| /* Set slave device register address on transfer */ |
| i2c->is_regaddr = false; |
| |
| /* Set segment pointer for I2C extended read mode operation */ |
| i2c->is_segment = false; |
| |
| for (i = 0; i < num; i++) { |
| if (msgs[i].addr == DDC_SEGMENT_ADDR && msgs[i].len == 1) { |
| i2c->is_segment = true; |
| dw_hdmi_qp_mod(hdmi, DDC_SEGMENT_ADDR, I2CM_SEG_ADDR, |
| I2CM_INTERFACE_CONTROL1); |
| dw_hdmi_qp_mod(hdmi, *msgs[i].buf << 7, I2CM_SEG_PTR, |
| I2CM_INTERFACE_CONTROL1); |
| } else { |
| if (msgs[i].flags & I2C_M_RD) |
| ret = dw_hdmi_qp_i2c_read(hdmi, msgs[i].buf, |
| msgs[i].len); |
| else |
| ret = dw_hdmi_qp_i2c_write(hdmi, msgs[i].buf, |
| msgs[i].len); |
| } |
| if (ret < 0) |
| break; |
| } |
| |
| if (!ret) |
| ret = num; |
| |
| /* Mute DONE and ERROR interrupts */ |
| dw_hdmi_qp_mod(hdmi, 0, I2CM_OP_DONE_MASK_N | I2CM_NACK_RCVD_MASK_N, |
| MAINUNIT_1_INT_MASK_N); |
| |
| return ret; |
| } |
| |
| static u32 dw_hdmi_qp_i2c_func(struct i2c_adapter *adapter) |
| { |
| return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL; |
| } |
| |
| static const struct i2c_algorithm dw_hdmi_qp_algorithm = { |
| .master_xfer = dw_hdmi_qp_i2c_xfer, |
| .functionality = dw_hdmi_qp_i2c_func, |
| }; |
| |
| static struct i2c_adapter *dw_hdmi_qp_i2c_adapter(struct dw_hdmi_qp *hdmi) |
| { |
| struct dw_hdmi_qp_i2c *i2c; |
| struct i2c_adapter *adap; |
| int ret; |
| |
| i2c = devm_kzalloc(hdmi->dev, sizeof(*i2c), GFP_KERNEL); |
| if (!i2c) |
| return ERR_PTR(-ENOMEM); |
| |
| mutex_init(&i2c->lock); |
| init_completion(&i2c->cmp); |
| |
| adap = &i2c->adap; |
| adap->owner = THIS_MODULE; |
| adap->dev.parent = hdmi->dev; |
| adap->algo = &dw_hdmi_qp_algorithm; |
| strscpy(adap->name, "DesignWare HDMI QP", sizeof(adap->name)); |
| |
| i2c_set_adapdata(adap, hdmi); |
| |
| ret = devm_i2c_add_adapter(hdmi->dev, adap); |
| if (ret) { |
| dev_warn(hdmi->dev, "cannot add %s I2C adapter\n", adap->name); |
| devm_kfree(hdmi->dev, i2c); |
| return ERR_PTR(ret); |
| } |
| |
| hdmi->i2c = i2c; |
| dev_info(hdmi->dev, "registered %s I2C bus driver\n", adap->name); |
| |
| return adap; |
| } |
| |
| static int dw_hdmi_qp_config_avi_infoframe(struct dw_hdmi_qp *hdmi, |
| const u8 *buffer, size_t len) |
| { |
| u32 val, i, j; |
| |
| if (len != HDMI_INFOFRAME_SIZE(AVI)) { |
| dev_err(hdmi->dev, "failed to configure avi infoframe\n"); |
| return -EINVAL; |
| } |
| |
| /* |
| * DW HDMI QP IP uses a different byte format from standard AVI info |
| * frames, though generally the bits are in the correct bytes. |
| */ |
| val = buffer[1] << 8 | buffer[2] << 16; |
| dw_hdmi_qp_write(hdmi, val, PKT_AVI_CONTENTS0); |
| |
| for (i = 0; i < 4; i++) { |
| for (j = 0; j < 4; j++) { |
| if (i * 4 + j >= 14) |
| break; |
| if (!j) |
| val = buffer[i * 4 + j + 3]; |
| val |= buffer[i * 4 + j + 3] << (8 * j); |
| } |
| |
| dw_hdmi_qp_write(hdmi, val, PKT_AVI_CONTENTS1 + i * 4); |
| } |
| |
| dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_AVI_FIELDRATE, PKTSCHED_PKT_CONFIG1); |
| |
| dw_hdmi_qp_mod(hdmi, PKTSCHED_AVI_TX_EN | PKTSCHED_GCP_TX_EN, |
| PKTSCHED_AVI_TX_EN | PKTSCHED_GCP_TX_EN, PKTSCHED_PKT_EN); |
| |
| return 0; |
| } |
| |
| static int dw_hdmi_qp_config_drm_infoframe(struct dw_hdmi_qp *hdmi, |
| const u8 *buffer, size_t len) |
| { |
| u32 val, i; |
| |
| if (len != HDMI_INFOFRAME_SIZE(DRM)) { |
| dev_err(hdmi->dev, "failed to configure drm infoframe\n"); |
| return -EINVAL; |
| } |
| |
| dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_DRMI_TX_EN, PKTSCHED_PKT_EN); |
| |
| val = buffer[1] << 8 | buffer[2] << 16; |
| dw_hdmi_qp_write(hdmi, val, PKT_DRMI_CONTENTS0); |
| |
| for (i = 0; i <= buffer[2]; i++) { |
| if (i % 4 == 0) |
| val = buffer[3 + i]; |
| val |= buffer[3 + i] << ((i % 4) * 8); |
| |
| if ((i % 4 == 3) || i == buffer[2]) |
| dw_hdmi_qp_write(hdmi, val, |
| PKT_DRMI_CONTENTS1 + ((i / 4) * 4)); |
| } |
| |
| dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_DRMI_FIELDRATE, PKTSCHED_PKT_CONFIG1); |
| dw_hdmi_qp_mod(hdmi, PKTSCHED_DRMI_TX_EN, PKTSCHED_DRMI_TX_EN, |
| PKTSCHED_PKT_EN); |
| |
| return 0; |
| } |
| |
| /* |
| * Static values documented in the TRM |
| * Different values are only used for debug purposes |
| */ |
| #define DW_HDMI_QP_AUDIO_INFOFRAME_HB1 0x1 |
| #define DW_HDMI_QP_AUDIO_INFOFRAME_HB2 0xa |
| |
| static int dw_hdmi_qp_config_audio_infoframe(struct dw_hdmi_qp *hdmi, |
| const u8 *buffer, size_t len) |
| { |
| /* |
| * AUDI_CONTENTS0: { RSV, HB2, HB1, RSV } |
| * AUDI_CONTENTS1: { PB3, PB2, PB1, PB0 } |
| * AUDI_CONTENTS2: { PB7, PB6, PB5, PB4 } |
| * |
| * PB0: CheckSum |
| * PB1: | CT3 | CT2 | CT1 | CT0 | F13 | CC2 | CC1 | CC0 | |
| * PB2: | F27 | F26 | F25 | SF2 | SF1 | SF0 | SS1 | SS0 | |
| * PB3: | F37 | F36 | F35 | F34 | F33 | F32 | F31 | F30 | |
| * PB4: | CA7 | CA6 | CA5 | CA4 | CA3 | CA2 | CA1 | CA0 | |
| * PB5: | DM_INH | LSV3 | LSV2 | LSV1 | LSV0 | F52 | F51 | F50 | |
| * PB6~PB10: Reserved |
| * |
| * AUDI_CONTENTS0 default value defined by HDMI specification, |
| * and shall only be changed for debug purposes. |
| */ |
| u32 header_bytes = (DW_HDMI_QP_AUDIO_INFOFRAME_HB1 << 8) | |
| (DW_HDMI_QP_AUDIO_INFOFRAME_HB2 << 16); |
| |
| regmap_bulk_write(hdmi->regm, PKT_AUDI_CONTENTS0, &header_bytes, 1); |
| regmap_bulk_write(hdmi->regm, PKT_AUDI_CONTENTS1, &buffer[3], 1); |
| regmap_bulk_write(hdmi->regm, PKT_AUDI_CONTENTS2, &buffer[4], 1); |
| |
| /* Enable ACR, AUDI, AMD */ |
| dw_hdmi_qp_mod(hdmi, |
| PKTSCHED_ACR_TX_EN | PKTSCHED_AUDI_TX_EN | PKTSCHED_AMD_TX_EN, |
| PKTSCHED_ACR_TX_EN | PKTSCHED_AUDI_TX_EN | PKTSCHED_AMD_TX_EN, |
| PKTSCHED_PKT_EN); |
| |
| /* Enable AUDS */ |
| dw_hdmi_qp_mod(hdmi, PKTSCHED_AUDS_TX_EN, PKTSCHED_AUDS_TX_EN, PKTSCHED_PKT_EN); |
| |
| return 0; |
| } |
| |
| static void dw_hdmi_qp_bridge_atomic_enable(struct drm_bridge *bridge, |
| struct drm_atomic_state *state) |
| { |
| struct dw_hdmi_qp *hdmi = bridge->driver_private; |
| struct drm_connector_state *conn_state; |
| struct drm_connector *connector; |
| unsigned int op_mode; |
| |
| connector = drm_atomic_get_new_connector_for_encoder(state, bridge->encoder); |
| if (WARN_ON(!connector)) |
| return; |
| |
| conn_state = drm_atomic_get_new_connector_state(state, connector); |
| if (WARN_ON(!conn_state)) |
| return; |
| |
| if (connector->display_info.is_hdmi) { |
| dev_dbg(hdmi->dev, "%s mode=HDMI rate=%llu\n", |
| __func__, conn_state->hdmi.tmds_char_rate); |
| op_mode = 0; |
| hdmi->tmds_char_rate = conn_state->hdmi.tmds_char_rate; |
| } else { |
| dev_dbg(hdmi->dev, "%s mode=DVI\n", __func__); |
| op_mode = OPMODE_DVI; |
| } |
| |
| hdmi->phy.ops->init(hdmi, hdmi->phy.data); |
| |
| dw_hdmi_qp_mod(hdmi, HDCP2_BYPASS, HDCP2_BYPASS, HDCP2LOGIC_CONFIG0); |
| dw_hdmi_qp_mod(hdmi, op_mode, OPMODE_DVI, LINK_CONFIG0); |
| |
| drm_atomic_helper_connector_hdmi_update_infoframes(connector, state); |
| } |
| |
| static void dw_hdmi_qp_bridge_atomic_disable(struct drm_bridge *bridge, |
| struct drm_atomic_state *state) |
| { |
| struct dw_hdmi_qp *hdmi = bridge->driver_private; |
| |
| hdmi->tmds_char_rate = 0; |
| |
| hdmi->phy.ops->disable(hdmi, hdmi->phy.data); |
| } |
| |
| static enum drm_connector_status |
| dw_hdmi_qp_bridge_detect(struct drm_bridge *bridge, struct drm_connector *connector) |
| { |
| struct dw_hdmi_qp *hdmi = bridge->driver_private; |
| |
| return hdmi->phy.ops->read_hpd(hdmi, hdmi->phy.data); |
| } |
| |
| static const struct drm_edid * |
| dw_hdmi_qp_bridge_edid_read(struct drm_bridge *bridge, |
| struct drm_connector *connector) |
| { |
| struct dw_hdmi_qp *hdmi = bridge->driver_private; |
| const struct drm_edid *drm_edid; |
| |
| drm_edid = drm_edid_read_ddc(connector, bridge->ddc); |
| if (!drm_edid) |
| dev_dbg(hdmi->dev, "failed to get edid\n"); |
| |
| return drm_edid; |
| } |
| |
| static enum drm_mode_status |
| dw_hdmi_qp_bridge_tmds_char_rate_valid(const struct drm_bridge *bridge, |
| const struct drm_display_mode *mode, |
| unsigned long long rate) |
| { |
| struct dw_hdmi_qp *hdmi = bridge->driver_private; |
| |
| if (rate > HDMI14_MAX_TMDSCLK) { |
| dev_dbg(hdmi->dev, "Unsupported TMDS char rate: %lld\n", rate); |
| return MODE_CLOCK_HIGH; |
| } |
| |
| return MODE_OK; |
| } |
| |
| static int dw_hdmi_qp_bridge_clear_infoframe(struct drm_bridge *bridge, |
| enum hdmi_infoframe_type type) |
| { |
| struct dw_hdmi_qp *hdmi = bridge->driver_private; |
| |
| switch (type) { |
| case HDMI_INFOFRAME_TYPE_AVI: |
| dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_AVI_TX_EN | PKTSCHED_GCP_TX_EN, |
| PKTSCHED_PKT_EN); |
| break; |
| |
| case HDMI_INFOFRAME_TYPE_DRM: |
| dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_DRMI_TX_EN, PKTSCHED_PKT_EN); |
| break; |
| |
| case HDMI_INFOFRAME_TYPE_AUDIO: |
| dw_hdmi_qp_mod(hdmi, 0, |
| PKTSCHED_ACR_TX_EN | |
| PKTSCHED_AUDS_TX_EN | |
| PKTSCHED_AUDI_TX_EN, |
| PKTSCHED_PKT_EN); |
| break; |
| default: |
| dev_dbg(hdmi->dev, "Unsupported infoframe type %x\n", type); |
| } |
| |
| return 0; |
| } |
| |
| static int dw_hdmi_qp_bridge_write_infoframe(struct drm_bridge *bridge, |
| enum hdmi_infoframe_type type, |
| const u8 *buffer, size_t len) |
| { |
| struct dw_hdmi_qp *hdmi = bridge->driver_private; |
| |
| dw_hdmi_qp_bridge_clear_infoframe(bridge, type); |
| |
| switch (type) { |
| case HDMI_INFOFRAME_TYPE_AVI: |
| return dw_hdmi_qp_config_avi_infoframe(hdmi, buffer, len); |
| |
| case HDMI_INFOFRAME_TYPE_DRM: |
| return dw_hdmi_qp_config_drm_infoframe(hdmi, buffer, len); |
| |
| case HDMI_INFOFRAME_TYPE_AUDIO: |
| return dw_hdmi_qp_config_audio_infoframe(hdmi, buffer, len); |
| |
| default: |
| dev_dbg(hdmi->dev, "Unsupported infoframe type %x\n", type); |
| return 0; |
| } |
| } |
| |
| static const struct drm_bridge_funcs dw_hdmi_qp_bridge_funcs = { |
| .atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state, |
| .atomic_destroy_state = drm_atomic_helper_bridge_destroy_state, |
| .atomic_reset = drm_atomic_helper_bridge_reset, |
| .atomic_enable = dw_hdmi_qp_bridge_atomic_enable, |
| .atomic_disable = dw_hdmi_qp_bridge_atomic_disable, |
| .detect = dw_hdmi_qp_bridge_detect, |
| .edid_read = dw_hdmi_qp_bridge_edid_read, |
| .hdmi_tmds_char_rate_valid = dw_hdmi_qp_bridge_tmds_char_rate_valid, |
| .hdmi_clear_infoframe = dw_hdmi_qp_bridge_clear_infoframe, |
| .hdmi_write_infoframe = dw_hdmi_qp_bridge_write_infoframe, |
| .hdmi_audio_startup = dw_hdmi_qp_audio_enable, |
| .hdmi_audio_shutdown = dw_hdmi_qp_audio_disable, |
| .hdmi_audio_prepare = dw_hdmi_qp_audio_prepare, |
| }; |
| |
| static irqreturn_t dw_hdmi_qp_main_hardirq(int irq, void *dev_id) |
| { |
| struct dw_hdmi_qp *hdmi = dev_id; |
| struct dw_hdmi_qp_i2c *i2c = hdmi->i2c; |
| u32 stat; |
| |
| stat = dw_hdmi_qp_read(hdmi, MAINUNIT_1_INT_STATUS); |
| |
| i2c->stat = stat & (I2CM_OP_DONE_IRQ | I2CM_READ_REQUEST_IRQ | |
| I2CM_NACK_RCVD_IRQ); |
| |
| if (i2c->stat) { |
| dw_hdmi_qp_write(hdmi, i2c->stat, MAINUNIT_1_INT_CLEAR); |
| complete(&i2c->cmp); |
| } |
| |
| if (stat) |
| return IRQ_HANDLED; |
| |
| return IRQ_NONE; |
| } |
| |
| static const struct regmap_config dw_hdmi_qp_regmap_config = { |
| .reg_bits = 32, |
| .val_bits = 32, |
| .reg_stride = 4, |
| .max_register = EARCRX_1_INT_FORCE, |
| }; |
| |
| static void dw_hdmi_qp_init_hw(struct dw_hdmi_qp *hdmi) |
| { |
| dw_hdmi_qp_write(hdmi, 0, MAINUNIT_0_INT_MASK_N); |
| dw_hdmi_qp_write(hdmi, 0, MAINUNIT_1_INT_MASK_N); |
| dw_hdmi_qp_write(hdmi, 428571429, TIMER_BASE_CONFIG0); |
| |
| /* Software reset */ |
| dw_hdmi_qp_write(hdmi, 0x01, I2CM_CONTROL0); |
| |
| dw_hdmi_qp_write(hdmi, 0x085c085c, I2CM_FM_SCL_CONFIG0); |
| |
| dw_hdmi_qp_mod(hdmi, 0, I2CM_FM_EN, I2CM_INTERFACE_CONTROL0); |
| |
| /* Clear DONE and ERROR interrupts */ |
| dw_hdmi_qp_write(hdmi, I2CM_OP_DONE_CLEAR | I2CM_NACK_RCVD_CLEAR, |
| MAINUNIT_1_INT_CLEAR); |
| |
| if (hdmi->phy.ops->setup_hpd) |
| hdmi->phy.ops->setup_hpd(hdmi, hdmi->phy.data); |
| } |
| |
| struct dw_hdmi_qp *dw_hdmi_qp_bind(struct platform_device *pdev, |
| struct drm_encoder *encoder, |
| const struct dw_hdmi_qp_plat_data *plat_data) |
| { |
| struct device *dev = &pdev->dev; |
| struct dw_hdmi_qp *hdmi; |
| void __iomem *regs; |
| int ret; |
| |
| if (!plat_data->phy_ops || !plat_data->phy_ops->init || |
| !plat_data->phy_ops->disable || !plat_data->phy_ops->read_hpd) { |
| dev_err(dev, "Missing platform PHY ops\n"); |
| return ERR_PTR(-ENODEV); |
| } |
| |
| hdmi = devm_drm_bridge_alloc(dev, struct dw_hdmi_qp, bridge, |
| &dw_hdmi_qp_bridge_funcs); |
| if (IS_ERR(hdmi)) |
| return ERR_CAST(hdmi); |
| |
| hdmi->dev = dev; |
| |
| regs = devm_platform_ioremap_resource(pdev, 0); |
| if (IS_ERR(regs)) |
| return ERR_CAST(regs); |
| |
| hdmi->regm = devm_regmap_init_mmio(dev, regs, &dw_hdmi_qp_regmap_config); |
| if (IS_ERR(hdmi->regm)) { |
| dev_err(dev, "Failed to configure regmap\n"); |
| return ERR_CAST(hdmi->regm); |
| } |
| |
| hdmi->phy.ops = plat_data->phy_ops; |
| hdmi->phy.data = plat_data->phy_data; |
| |
| dw_hdmi_qp_init_hw(hdmi); |
| |
| ret = devm_request_threaded_irq(dev, plat_data->main_irq, |
| dw_hdmi_qp_main_hardirq, NULL, |
| IRQF_SHARED, dev_name(dev), hdmi); |
| if (ret) |
| return ERR_PTR(ret); |
| |
| hdmi->bridge.driver_private = hdmi; |
| hdmi->bridge.ops = DRM_BRIDGE_OP_DETECT | |
| DRM_BRIDGE_OP_EDID | |
| DRM_BRIDGE_OP_HDMI | |
| DRM_BRIDGE_OP_HDMI_AUDIO | |
| DRM_BRIDGE_OP_HPD; |
| hdmi->bridge.of_node = pdev->dev.of_node; |
| hdmi->bridge.type = DRM_MODE_CONNECTOR_HDMIA; |
| hdmi->bridge.vendor = "Synopsys"; |
| hdmi->bridge.product = "DW HDMI QP TX"; |
| |
| hdmi->bridge.ddc = dw_hdmi_qp_i2c_adapter(hdmi); |
| if (IS_ERR(hdmi->bridge.ddc)) |
| return ERR_CAST(hdmi->bridge.ddc); |
| |
| hdmi->bridge.hdmi_audio_max_i2s_playback_channels = 8; |
| hdmi->bridge.hdmi_audio_dev = dev; |
| hdmi->bridge.hdmi_audio_dai_port = 1; |
| |
| ret = devm_drm_bridge_add(dev, &hdmi->bridge); |
| if (ret) |
| return ERR_PTR(ret); |
| |
| ret = drm_bridge_attach(encoder, &hdmi->bridge, NULL, |
| DRM_BRIDGE_ATTACH_NO_CONNECTOR); |
| if (ret) |
| return ERR_PTR(ret); |
| |
| return hdmi; |
| } |
| EXPORT_SYMBOL_GPL(dw_hdmi_qp_bind); |
| |
| void dw_hdmi_qp_resume(struct device *dev, struct dw_hdmi_qp *hdmi) |
| { |
| dw_hdmi_qp_init_hw(hdmi); |
| } |
| EXPORT_SYMBOL_GPL(dw_hdmi_qp_resume); |
| |
| MODULE_AUTHOR("Algea Cao <algea.cao@rock-chips.com>"); |
| MODULE_AUTHOR("Cristian Ciocaltea <cristian.ciocaltea@collabora.com>"); |
| MODULE_DESCRIPTION("DW HDMI QP transmitter library"); |
| MODULE_LICENSE("GPL"); |