diff options
author | Eric Anholt <eric@anholt.net> | 2017-02-27 23:28:02 +0300 |
---|---|---|
committer | Eric Anholt <eric@anholt.net> | 2017-03-16 20:33:30 +0300 |
commit | bb7d78568814a31a11fa14f1479a9fe51f1582ad (patch) | |
tree | 007a090c8601fa512d50ec6487d22da059eff505 | |
parent | 199a76946d4303ce047e6ac9a49eefa0105f6e78 (diff) | |
download | linux-bb7d78568814a31a11fa14f1479a9fe51f1582ad.tar.xz |
drm/vc4: Add HDMI audio support
The HDMI encoder IP embeds all needed blocks to output audio, with a
custom DAI called MAI moving audio between the two parts of the HDMI
core. This driver now exposes a sound card to let users stream audio
to their display.
Using the hdmi-codec driver has been considered here, but MAI meant
having to significantly rework hdmi-codec, and it would have left
little shared code with the I2S mode anyway.
The encoder requires that the audio be SPDIF-formatted frames only,
which alsalib will format-convert for us.
This patch is the combined work of Eric Anholt (initial register setup
with a separate dmaengine driver and using simple-audio-card) and
Boris Brezillon (moving it all into HDMI, massive debug to get it
actually working), and which Eric has the permission to release.
v2: Drop "-audio" from sound card name, since that's already implied
(suggestion by Boris)
Signed-off-by: Eric Anholt <eric@anholt.net>
Acked-by: Boris Brezillon <boris.brezillon@free-electrons.com>
Link: http://patchwork.freedesktop.org/patch/msgid/20170227202803.12855-2-eric@anholt.net
-rw-r--r-- | drivers/gpu/drm/vc4/Kconfig | 4 | ||||
-rw-r--r-- | drivers/gpu/drm/vc4/vc4_hdmi.c | 494 | ||||
-rw-r--r-- | drivers/gpu/drm/vc4/vc4_regs.h | 107 |
3 files changed, 603 insertions, 2 deletions
diff --git a/drivers/gpu/drm/vc4/Kconfig b/drivers/gpu/drm/vc4/Kconfig index e1517d07cb7d..973b4203c0b2 100644 --- a/drivers/gpu/drm/vc4/Kconfig +++ b/drivers/gpu/drm/vc4/Kconfig @@ -2,11 +2,15 @@ config DRM_VC4 tristate "Broadcom VC4 Graphics" depends on ARCH_BCM2835 || COMPILE_TEST depends on DRM + depends on SND && SND_SOC depends on COMMON_CLK select DRM_KMS_HELPER select DRM_KMS_CMA_HELPER select DRM_GEM_CMA_HELPER select DRM_PANEL + select SND_PCM + select SND_PCM_ELD + select SND_SOC_GENERIC_DMAENGINE_PCM select DRM_MIPI_DSI help Choose this option if you have a system that has a Broadcom diff --git a/drivers/gpu/drm/vc4/vc4_hdmi.c b/drivers/gpu/drm/vc4/vc4_hdmi.c index 1be1e8304720..e9cbe269710b 100644 --- a/drivers/gpu/drm/vc4/vc4_hdmi.c +++ b/drivers/gpu/drm/vc4/vc4_hdmi.c @@ -48,11 +48,27 @@ #include "linux/clk.h" #include "linux/component.h" #include "linux/i2c.h" +#include "linux/of_address.h" #include "linux/of_gpio.h" #include "linux/of_platform.h" +#include "linux/rational.h" +#include "sound/dmaengine_pcm.h" +#include "sound/pcm_drm_eld.h" +#include "sound/pcm_params.h" +#include "sound/soc.h" #include "vc4_drv.h" #include "vc4_regs.h" +/* HDMI audio information */ +struct vc4_hdmi_audio { + struct snd_soc_card card; + struct snd_soc_dai_link link; + int samplerate; + int channels; + struct snd_dmaengine_dai_dma_data dma_data; + struct snd_pcm_substream *substream; +}; + /* General HDMI hardware state. */ struct vc4_hdmi { struct platform_device *pdev; @@ -60,6 +76,8 @@ struct vc4_hdmi { struct drm_encoder *encoder; struct drm_connector *connector; + struct vc4_hdmi_audio audio; + struct i2c_adapter *ddc; void __iomem *hdmicore_regs; void __iomem *hd_regs; @@ -115,6 +133,10 @@ static const struct { HDMI_REG(VC4_HDMI_SW_RESET_CONTROL), HDMI_REG(VC4_HDMI_HOTPLUG_INT), HDMI_REG(VC4_HDMI_HOTPLUG), + HDMI_REG(VC4_HDMI_MAI_CHANNEL_MAP), + HDMI_REG(VC4_HDMI_MAI_CONFIG), + HDMI_REG(VC4_HDMI_MAI_FORMAT), + HDMI_REG(VC4_HDMI_AUDIO_PACKET_CONFIG), HDMI_REG(VC4_HDMI_RAM_PACKET_CONFIG), HDMI_REG(VC4_HDMI_HORZA), HDMI_REG(VC4_HDMI_HORZB), @@ -125,6 +147,7 @@ static const struct { HDMI_REG(VC4_HDMI_VERTB0), HDMI_REG(VC4_HDMI_VERTB1), HDMI_REG(VC4_HDMI_TX_PHY_RESET_CTL), + HDMI_REG(VC4_HDMI_TX_PHY_CTL0), }; static const struct { @@ -133,6 +156,9 @@ static const struct { } hd_regs[] = { HDMI_REG(VC4_HD_M_CTL), HDMI_REG(VC4_HD_MAI_CTL), + HDMI_REG(VC4_HD_MAI_THR), + HDMI_REG(VC4_HD_MAI_FMT), + HDMI_REG(VC4_HD_MAI_SMP), HDMI_REG(VC4_HD_VID_CTL), HDMI_REG(VC4_HD_CSC_CTL), HDMI_REG(VC4_HD_FRAME_COUNT), @@ -232,6 +258,7 @@ static int vc4_hdmi_connector_get_modes(struct drm_connector *connector) drm_mode_connector_update_edid_property(connector, edid); ret = drm_add_edid_modes(connector, edid); + drm_edid_to_eld(connector, edid); return ret; } @@ -317,7 +344,7 @@ static void vc4_hdmi_write_infoframe(struct drm_encoder *encoder, struct drm_device *dev = encoder->dev; struct vc4_dev *vc4 = to_vc4_dev(dev); u32 packet_id = frame->any.type - 0x80; - u32 packet_reg = VC4_HDMI_GCP_0 + VC4_HDMI_PACKET_STRIDE * packet_id; + u32 packet_reg = VC4_HDMI_RAM_PACKET(packet_id); uint8_t buffer[VC4_HDMI_PACKET_STRIDE]; ssize_t len, i; int ret; @@ -398,6 +425,24 @@ static void vc4_hdmi_set_spd_infoframe(struct drm_encoder *encoder) vc4_hdmi_write_infoframe(encoder, &frame); } +static void vc4_hdmi_set_audio_infoframe(struct drm_encoder *encoder) +{ + struct drm_device *drm = encoder->dev; + struct vc4_dev *vc4 = drm->dev_private; + struct vc4_hdmi *hdmi = vc4->hdmi; + union hdmi_infoframe frame; + int ret; + + ret = hdmi_audio_infoframe_init(&frame.audio); + + frame.audio.coding_type = HDMI_AUDIO_CODING_TYPE_STREAM; + frame.audio.sample_frequency = HDMI_AUDIO_SAMPLE_FREQUENCY_STREAM; + frame.audio.sample_size = HDMI_AUDIO_SAMPLE_SIZE_STREAM; + frame.audio.channels = hdmi->audio.channels; + + vc4_hdmi_write_infoframe(encoder, &frame); +} + static void vc4_hdmi_set_infoframes(struct drm_encoder *encoder) { vc4_hdmi_set_avi_infoframe(encoder); @@ -606,6 +651,447 @@ static const struct drm_encoder_helper_funcs vc4_hdmi_encoder_helper_funcs = { .enable = vc4_hdmi_encoder_enable, }; +/* HDMI audio codec callbacks */ +static void vc4_hdmi_audio_set_mai_clock(struct vc4_hdmi *hdmi) +{ + struct drm_device *drm = hdmi->encoder->dev; + struct vc4_dev *vc4 = to_vc4_dev(drm); + u32 hsm_clock = clk_get_rate(hdmi->hsm_clock); + unsigned long n, m; + + rational_best_approximation(hsm_clock, hdmi->audio.samplerate, + VC4_HD_MAI_SMP_N_MASK >> + VC4_HD_MAI_SMP_N_SHIFT, + (VC4_HD_MAI_SMP_M_MASK >> + VC4_HD_MAI_SMP_M_SHIFT) + 1, + &n, &m); + + HD_WRITE(VC4_HD_MAI_SMP, + VC4_SET_FIELD(n, VC4_HD_MAI_SMP_N) | + VC4_SET_FIELD(m - 1, VC4_HD_MAI_SMP_M)); +} + +static void vc4_hdmi_set_n_cts(struct vc4_hdmi *hdmi) +{ + struct drm_encoder *encoder = hdmi->encoder; + struct drm_crtc *crtc = encoder->crtc; + struct drm_device *drm = encoder->dev; + struct vc4_dev *vc4 = to_vc4_dev(drm); + const struct drm_display_mode *mode = &crtc->state->adjusted_mode; + u32 samplerate = hdmi->audio.samplerate; + u32 n, cts; + u64 tmp; + + n = 128 * samplerate / 1000; + tmp = (u64)(mode->clock * 1000) * n; + do_div(tmp, 128 * samplerate); + cts = tmp; + + HDMI_WRITE(VC4_HDMI_CRP_CFG, + VC4_HDMI_CRP_CFG_EXTERNAL_CTS_EN | + VC4_SET_FIELD(n, VC4_HDMI_CRP_CFG_N)); + + /* + * We could get slightly more accurate clocks in some cases by + * providing a CTS_1 value. The two CTS values are alternated + * between based on the period fields + */ + HDMI_WRITE(VC4_HDMI_CTS_0, cts); + HDMI_WRITE(VC4_HDMI_CTS_1, cts); +} + +static inline struct vc4_hdmi *dai_to_hdmi(struct snd_soc_dai *dai) +{ + struct snd_soc_card *card = snd_soc_dai_get_drvdata(dai); + + return snd_soc_card_get_drvdata(card); +} + +static int vc4_hdmi_audio_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct vc4_hdmi *hdmi = dai_to_hdmi(dai); + struct drm_encoder *encoder = hdmi->encoder; + struct vc4_dev *vc4 = to_vc4_dev(encoder->dev); + int ret; + + if (hdmi->audio.substream && hdmi->audio.substream != substream) + return -EINVAL; + + hdmi->audio.substream = substream; + + /* + * If the HDMI encoder hasn't probed, or the encoder is + * currently in DVI mode, treat the codec dai as missing. + */ + if (!encoder->crtc || !(HDMI_READ(VC4_HDMI_RAM_PACKET_CONFIG) & + VC4_HDMI_RAM_PACKET_ENABLE)) + return -ENODEV; + + ret = snd_pcm_hw_constraint_eld(substream->runtime, + hdmi->connector->eld); + if (ret) + return ret; + + return 0; +} + +static int vc4_hdmi_audio_set_fmt(struct snd_soc_dai *dai, unsigned int fmt) +{ + return 0; +} + +static void vc4_hdmi_audio_reset(struct vc4_hdmi *hdmi) +{ + struct drm_encoder *encoder = hdmi->encoder; + struct drm_device *drm = encoder->dev; + struct device *dev = &hdmi->pdev->dev; + struct vc4_dev *vc4 = to_vc4_dev(drm); + int ret; + + ret = vc4_hdmi_stop_packet(encoder, HDMI_INFOFRAME_TYPE_AUDIO); + if (ret) + dev_err(dev, "Failed to stop audio infoframe: %d\n", ret); + + HD_WRITE(VC4_HD_MAI_CTL, VC4_HD_MAI_CTL_RESET); + HD_WRITE(VC4_HD_MAI_CTL, VC4_HD_MAI_CTL_ERRORF); + HD_WRITE(VC4_HD_MAI_CTL, VC4_HD_MAI_CTL_FLUSH); +} + +static void vc4_hdmi_audio_shutdown(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct vc4_hdmi *hdmi = dai_to_hdmi(dai); + + if (substream != hdmi->audio.substream) + return; + + vc4_hdmi_audio_reset(hdmi); + + hdmi->audio.substream = NULL; +} + +/* HDMI audio codec callbacks */ +static int vc4_hdmi_audio_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct vc4_hdmi *hdmi = dai_to_hdmi(dai); + struct drm_encoder *encoder = hdmi->encoder; + struct drm_device *drm = encoder->dev; + struct device *dev = &hdmi->pdev->dev; + struct vc4_dev *vc4 = to_vc4_dev(drm); + u32 audio_packet_config, channel_mask; + u32 channel_map, i; + + if (substream != hdmi->audio.substream) + return -EINVAL; + + dev_dbg(dev, "%s: %u Hz, %d bit, %d channels\n", __func__, + params_rate(params), params_width(params), + params_channels(params)); + + hdmi->audio.channels = params_channels(params); + hdmi->audio.samplerate = params_rate(params); + + HD_WRITE(VC4_HD_MAI_CTL, + VC4_HD_MAI_CTL_RESET | + VC4_HD_MAI_CTL_FLUSH | + VC4_HD_MAI_CTL_DLATE | + VC4_HD_MAI_CTL_ERRORE | + VC4_HD_MAI_CTL_ERRORF); + + vc4_hdmi_audio_set_mai_clock(hdmi); + + audio_packet_config = + VC4_HDMI_AUDIO_PACKET_ZERO_DATA_ON_SAMPLE_FLAT | + VC4_HDMI_AUDIO_PACKET_ZERO_DATA_ON_INACTIVE_CHANNELS | + VC4_SET_FIELD(0xf, VC4_HDMI_AUDIO_PACKET_B_FRAME_IDENTIFIER); + + channel_mask = GENMASK(hdmi->audio.channels - 1, 0); + audio_packet_config |= VC4_SET_FIELD(channel_mask, + VC4_HDMI_AUDIO_PACKET_CEA_MASK); + + /* Set the MAI threshold. This logic mimics the firmware's. */ + if (hdmi->audio.samplerate > 96000) { + HD_WRITE(VC4_HD_MAI_THR, + VC4_SET_FIELD(0x12, VC4_HD_MAI_THR_DREQHIGH) | + VC4_SET_FIELD(0x12, VC4_HD_MAI_THR_DREQLOW)); + } else if (hdmi->audio.samplerate > 48000) { + HD_WRITE(VC4_HD_MAI_THR, + VC4_SET_FIELD(0x14, VC4_HD_MAI_THR_DREQHIGH) | + VC4_SET_FIELD(0x12, VC4_HD_MAI_THR_DREQLOW)); + } else { + HD_WRITE(VC4_HD_MAI_THR, + VC4_SET_FIELD(0x10, VC4_HD_MAI_THR_PANICHIGH) | + VC4_SET_FIELD(0x10, VC4_HD_MAI_THR_PANICLOW) | + VC4_SET_FIELD(0x10, VC4_HD_MAI_THR_DREQHIGH) | + VC4_SET_FIELD(0x10, VC4_HD_MAI_THR_DREQLOW)); + } + + HDMI_WRITE(VC4_HDMI_MAI_CONFIG, + VC4_HDMI_MAI_CONFIG_BIT_REVERSE | + VC4_SET_FIELD(channel_mask, VC4_HDMI_MAI_CHANNEL_MASK)); + + channel_map = 0; + for (i = 0; i < 8; i++) { + if (channel_mask & BIT(i)) + channel_map |= i << (3 * i); + } + + HDMI_WRITE(VC4_HDMI_MAI_CHANNEL_MAP, channel_map); + HDMI_WRITE(VC4_HDMI_AUDIO_PACKET_CONFIG, audio_packet_config); + vc4_hdmi_set_n_cts(hdmi); + + return 0; +} + +static int vc4_hdmi_audio_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct vc4_hdmi *hdmi = dai_to_hdmi(dai); + struct drm_encoder *encoder = hdmi->encoder; + struct drm_device *drm = encoder->dev; + struct vc4_dev *vc4 = to_vc4_dev(drm); + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + vc4_hdmi_set_audio_infoframe(encoder); + HDMI_WRITE(VC4_HDMI_TX_PHY_CTL0, + HDMI_READ(VC4_HDMI_TX_PHY_CTL0) & + ~VC4_HDMI_TX_PHY_RNG_PWRDN); + HD_WRITE(VC4_HD_MAI_CTL, + VC4_SET_FIELD(hdmi->audio.channels, + VC4_HD_MAI_CTL_CHNUM) | + VC4_HD_MAI_CTL_ENABLE); + break; + case SNDRV_PCM_TRIGGER_STOP: + HD_WRITE(VC4_HD_MAI_CTL, + VC4_HD_MAI_CTL_DLATE | + VC4_HD_MAI_CTL_ERRORE | + VC4_HD_MAI_CTL_ERRORF); + HDMI_WRITE(VC4_HDMI_TX_PHY_CTL0, + HDMI_READ(VC4_HDMI_TX_PHY_CTL0) | + VC4_HDMI_TX_PHY_RNG_PWRDN); + break; + default: + break; + } + + return 0; +} + +static inline struct vc4_hdmi * +snd_component_to_hdmi(struct snd_soc_component *component) +{ + struct snd_soc_card *card = snd_soc_component_get_drvdata(component); + + return snd_soc_card_get_drvdata(card); +} + +static int vc4_hdmi_audio_eld_ctl_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); + struct vc4_hdmi *hdmi = snd_component_to_hdmi(component); + + uinfo->type = SNDRV_CTL_ELEM_TYPE_BYTES; + uinfo->count = sizeof(hdmi->connector->eld); + + return 0; +} + +static int vc4_hdmi_audio_eld_ctl_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); + struct vc4_hdmi *hdmi = snd_component_to_hdmi(component); + + memcpy(ucontrol->value.bytes.data, hdmi->connector->eld, + sizeof(hdmi->connector->eld)); + + return 0; +} + +static const struct snd_kcontrol_new vc4_hdmi_audio_controls[] = { + { + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .iface = SNDRV_CTL_ELEM_IFACE_PCM, + .name = "ELD", + .info = vc4_hdmi_audio_eld_ctl_info, + .get = vc4_hdmi_audio_eld_ctl_get, + }, +}; + +static const struct snd_soc_dapm_widget vc4_hdmi_audio_widgets[] = { + SND_SOC_DAPM_OUTPUT("TX"), +}; + +static const struct snd_soc_dapm_route vc4_hdmi_audio_routes[] = { + { "TX", NULL, "Playback" }, +}; + +static const struct snd_soc_codec_driver vc4_hdmi_audio_codec_drv = { + .component_driver = { + .controls = vc4_hdmi_audio_controls, + .num_controls = ARRAY_SIZE(vc4_hdmi_audio_controls), + .dapm_widgets = vc4_hdmi_audio_widgets, + .num_dapm_widgets = ARRAY_SIZE(vc4_hdmi_audio_widgets), + .dapm_routes = vc4_hdmi_audio_routes, + .num_dapm_routes = ARRAY_SIZE(vc4_hdmi_audio_routes), + }, +}; + +static const struct snd_soc_dai_ops vc4_hdmi_audio_dai_ops = { + .startup = vc4_hdmi_audio_startup, + .shutdown = vc4_hdmi_audio_shutdown, + .hw_params = vc4_hdmi_audio_hw_params, + .set_fmt = vc4_hdmi_audio_set_fmt, + .trigger = vc4_hdmi_audio_trigger, +}; + +static struct snd_soc_dai_driver vc4_hdmi_audio_codec_dai_drv = { + .name = "vc4-hdmi-hifi", + .playback = { + .stream_name = "Playback", + .channels_min = 2, + .channels_max = 8, + .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, + .formats = SNDRV_PCM_FMTBIT_IEC958_SUBFRAME_LE, + }, +}; + +static const struct snd_soc_component_driver vc4_hdmi_audio_cpu_dai_comp = { + .name = "vc4-hdmi-cpu-dai-component", +}; + +static int vc4_hdmi_audio_cpu_dai_probe(struct snd_soc_dai *dai) +{ + struct vc4_hdmi *hdmi = dai_to_hdmi(dai); + + snd_soc_dai_init_dma_data(dai, &hdmi->audio.dma_data, NULL); + + return 0; +} + +static struct snd_soc_dai_driver vc4_hdmi_audio_cpu_dai_drv = { + .name = "vc4-hdmi-cpu-dai", + .probe = vc4_hdmi_audio_cpu_dai_probe, + .playback = { + .stream_name = "Playback", + .channels_min = 1, + .channels_max = 8, + .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, + .formats = SNDRV_PCM_FMTBIT_IEC958_SUBFRAME_LE, + }, + .ops = &vc4_hdmi_audio_dai_ops, +}; + +static const struct snd_dmaengine_pcm_config pcm_conf = { + .chan_names[SNDRV_PCM_STREAM_PLAYBACK] = "audio-rx", + .prepare_slave_config = snd_dmaengine_pcm_prepare_slave_config, +}; + +static int vc4_hdmi_audio_init(struct vc4_hdmi *hdmi) +{ + struct snd_soc_dai_link *dai_link = &hdmi->audio.link; + struct snd_soc_card *card = &hdmi->audio.card; + struct device *dev = &hdmi->pdev->dev; + const __be32 *addr; + int ret; + + if (!of_find_property(dev->of_node, "dmas", NULL)) { + dev_warn(dev, + "'dmas' DT property is missing, no HDMI audio\n"); + return 0; + } + + /* + * Get the physical address of VC4_HD_MAI_DATA. We need to retrieve + * the bus address specified in the DT, because the physical address + * (the one returned by platform_get_resource()) is not appropriate + * for DMA transfers. + * This VC/MMU should probably be exposed to avoid this kind of hacks. + */ + addr = of_get_address(dev->of_node, 1, NULL, NULL); + hdmi->audio.dma_data.addr = be32_to_cpup(addr) + VC4_HD_MAI_DATA; + hdmi->audio.dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; + hdmi->audio.dma_data.maxburst = 2; + + ret = devm_snd_dmaengine_pcm_register(dev, &pcm_conf, 0); + if (ret) { + dev_err(dev, "Could not register PCM component: %d\n", ret); + return ret; + } + + ret = devm_snd_soc_register_component(dev, &vc4_hdmi_audio_cpu_dai_comp, + &vc4_hdmi_audio_cpu_dai_drv, 1); + if (ret) { + dev_err(dev, "Could not register CPU DAI: %d\n", ret); + return ret; + } + + /* register codec and codec dai */ + ret = snd_soc_register_codec(dev, &vc4_hdmi_audio_codec_drv, + &vc4_hdmi_audio_codec_dai_drv, 1); + if (ret) { + dev_err(dev, "Could not register codec: %d\n", ret); + return ret; + } + + dai_link->name = "MAI"; + dai_link->stream_name = "MAI PCM"; + dai_link->codec_dai_name = vc4_hdmi_audio_codec_dai_drv.name; + dai_link->cpu_dai_name = dev_name(dev); + dai_link->codec_name = dev_name(dev); + dai_link->platform_name = dev_name(dev); + + card->dai_link = dai_link; + card->num_links = 1; + card->name = "vc4-hdmi"; + card->dev = dev; + + /* + * Be careful, snd_soc_register_card() calls dev_set_drvdata() and + * stores a pointer to the snd card object in dev->driver_data. This + * means we cannot use it for something else. The hdmi back-pointer is + * now stored in card->drvdata and should be retrieved with + * snd_soc_card_get_drvdata() if needed. + */ + snd_soc_card_set_drvdata(card, hdmi); + ret = devm_snd_soc_register_card(dev, card); + if (ret) { + dev_err(dev, "Could not register sound card: %d\n", ret); + goto unregister_codec; + } + + return 0; + +unregister_codec: + snd_soc_unregister_codec(dev); + + return ret; +} + +static void vc4_hdmi_audio_cleanup(struct vc4_hdmi *hdmi) +{ + struct device *dev = &hdmi->pdev->dev; + + /* + * If drvdata is not set this means the audio card was not + * registered, just skip codec unregistration in this case. + */ + if (dev_get_drvdata(dev)) + snd_soc_unregister_codec(dev); +} + static int vc4_hdmi_bind(struct device *dev, struct device *master, void *data) { struct platform_device *pdev = to_platform_device(dev); @@ -737,6 +1223,10 @@ static int vc4_hdmi_bind(struct device *dev, struct device *master, void *data) goto err_destroy_encoder; } + ret = vc4_hdmi_audio_init(hdmi); + if (ret) + goto err_destroy_encoder; + return 0; err_destroy_encoder: @@ -758,6 +1248,8 @@ static void vc4_hdmi_unbind(struct device *dev, struct device *master, struct vc4_dev *vc4 = drm->dev_private; struct vc4_hdmi *hdmi = vc4->hdmi; + vc4_hdmi_audio_cleanup(hdmi); + vc4_hdmi_connector_destroy(hdmi->connector); vc4_hdmi_encoder_destroy(hdmi->encoder); diff --git a/drivers/gpu/drm/vc4/vc4_regs.h b/drivers/gpu/drm/vc4/vc4_regs.h index 385405a2df05..932093936178 100644 --- a/drivers/gpu/drm/vc4/vc4_regs.h +++ b/drivers/gpu/drm/vc4/vc4_regs.h @@ -446,11 +446,62 @@ #define VC4_HDMI_HOTPLUG 0x00c # define VC4_HDMI_HOTPLUG_CONNECTED BIT(0) +/* 3 bits per field, where each field maps from that corresponding MAI + * bus channel to the given HDMI channel. + */ +#define VC4_HDMI_MAI_CHANNEL_MAP 0x090 + +#define VC4_HDMI_MAI_CONFIG 0x094 +# define VC4_HDMI_MAI_CONFIG_FORMAT_REVERSE BIT(27) +# define VC4_HDMI_MAI_CONFIG_BIT_REVERSE BIT(26) +# define VC4_HDMI_MAI_CHANNEL_MASK_MASK VC4_MASK(15, 0) +# define VC4_HDMI_MAI_CHANNEL_MASK_SHIFT 0 + +/* Last received format word on the MAI bus. */ +#define VC4_HDMI_MAI_FORMAT 0x098 + +#define VC4_HDMI_AUDIO_PACKET_CONFIG 0x09c +# define VC4_HDMI_AUDIO_PACKET_ZERO_DATA_ON_SAMPLE_FLAT BIT(29) +# define VC4_HDMI_AUDIO_PACKET_ZERO_DATA_ON_INACTIVE_CHANNELS BIT(24) +# define VC4_HDMI_AUDIO_PACKET_FORCE_SAMPLE_PRESENT BIT(19) +# define VC4_HDMI_AUDIO_PACKET_FORCE_B_FRAME BIT(18) +# define VC4_HDMI_AUDIO_PACKET_B_FRAME_IDENTIFIER_MASK VC4_MASK(13, 10) +# define VC4_HDMI_AUDIO_PACKET_B_FRAME_IDENTIFIER_SHIFT 10 +/* If set, then multichannel, otherwise 2 channel. */ +# define VC4_HDMI_AUDIO_PACKET_AUDIO_LAYOUT BIT(9) +/* If set, then AUDIO_LAYOUT overrides audio_cea_mask */ +# define VC4_HDMI_AUDIO_PACKET_FORCE_AUDIO_LAYOUT BIT(8) +# define VC4_HDMI_AUDIO_PACKET_CEA_MASK_MASK VC4_MASK(7, 0) +# define VC4_HDMI_AUDIO_PACKET_CEA_MASK_SHIFT 0 + #define VC4_HDMI_RAM_PACKET_CONFIG 0x0a0 # define VC4_HDMI_RAM_PACKET_ENABLE BIT(16) #define VC4_HDMI_RAM_PACKET_STATUS 0x0a4 +#define VC4_HDMI_CRP_CFG 0x0a8 +/* When set, the CTS_PERIOD counts based on MAI bus sync pulse instead + * of pixel clock. + */ +# define VC4_HDMI_CRP_USE_MAI_BUS_SYNC_FOR_CTS BIT(26) +/* When set, no CRP packets will be sent. */ +# define VC4_HDMI_CRP_CFG_DISABLE BIT(25) +/* If set, generates CTS values based on N, audio clock, and video + * clock. N must be divisible by 128. + */ +# define VC4_HDMI_CRP_CFG_EXTERNAL_CTS_EN BIT(24) +# define VC4_HDMI_CRP_CFG_N_MASK VC4_MASK(19, 0) +# define VC4_HDMI_CRP_CFG_N_SHIFT 0 + +/* 20-bit fields containing CTS values to be transmitted if !EXTERNAL_CTS_EN */ +#define VC4_HDMI_CTS_0 0x0ac +#define VC4_HDMI_CTS_1 0x0b0 +/* 20-bit fields containing number of clocks to send CTS0/1 before + * switching to the other one. + */ +#define VC4_HDMI_CTS_PERIOD_0 0x0b4 +#define VC4_HDMI_CTS_PERIOD_1 0x0b8 + #define VC4_HDMI_HORZA 0x0c4 # define VC4_HDMI_HORZA_VPOS BIT(14) # define VC4_HDMI_HORZA_HPOS BIT(13) @@ -512,7 +563,11 @@ #define VC4_HDMI_TX_PHY_RESET_CTL 0x2c0 -#define VC4_HDMI_GCP_0 0x400 +#define VC4_HDMI_TX_PHY_CTL0 0x2c4 +# define VC4_HDMI_TX_PHY_RNG_PWRDN BIT(25) + +#define VC4_HDMI_GCP(x) (0x400 + ((x) * 0x4)) +#define VC4_HDMI_RAM_PACKET(x) (0x400 + ((x) * 0x24)) #define VC4_HDMI_PACKET_STRIDE 0x24 #define VC4_HD_M_CTL 0x00c @@ -522,6 +577,56 @@ # define VC4_HD_M_ENABLE BIT(0) #define VC4_HD_MAI_CTL 0x014 +/* Set when audio stream is received at a slower rate than the + * sampling period, so MAI fifo goes empty. Write 1 to clear. + */ +# define VC4_HD_MAI_CTL_DLATE BIT(15) +# define VC4_HD_MAI_CTL_BUSY BIT(14) +# define VC4_HD_MAI_CTL_CHALIGN BIT(13) +# define VC4_HD_MAI_CTL_WHOLSMP BIT(12) +# define VC4_HD_MAI_CTL_FULL BIT(11) +# define VC4_HD_MAI_CTL_EMPTY BIT(10) +# define VC4_HD_MAI_CTL_FLUSH BIT(9) +/* If set, MAI bus generates SPDIF (bit 31) parity instead of passing + * through. + */ +# define VC4_HD_MAI_CTL_PAREN BIT(8) +# define VC4_HD_MAI_CTL_CHNUM_MASK VC4_MASK(7, 4) +# define VC4_HD_MAI_CTL_CHNUM_SHIFT 4 +# define VC4_HD_MAI_CTL_ENABLE BIT(3) +/* Underflow error status bit, write 1 to clear. */ +# define VC4_HD_MAI_CTL_ERRORE BIT(2) +/* Overflow error status bit, write 1 to clear. */ +# define VC4_HD_MAI_CTL_ERRORF BIT(1) +/* Single-shot reset bit. Read value is undefined. */ +# define VC4_HD_MAI_CTL_RESET BIT(0) + +#define VC4_HD_MAI_THR 0x018 +# define VC4_HD_MAI_THR_PANICHIGH_MASK VC4_MASK(29, 24) +# define VC4_HD_MAI_THR_PANICHIGH_SHIFT 24 +# define VC4_HD_MAI_THR_PANICLOW_MASK VC4_MASK(21, 16) +# define VC4_HD_MAI_THR_PANICLOW_SHIFT 16 +# define VC4_HD_MAI_THR_DREQHIGH_MASK VC4_MASK(13, 8) +# define VC4_HD_MAI_THR_DREQHIGH_SHIFT 8 +# define VC4_HD_MAI_THR_DREQLOW_MASK VC4_MASK(5, 0) +# define VC4_HD_MAI_THR_DREQLOW_SHIFT 0 + +/* Format header to be placed on the MAI data. Unused. */ +#define VC4_HD_MAI_FMT 0x01c + +/* Register for DMAing in audio data to be transported over the MAI + * bus to the Falcon core. + */ +#define VC4_HD_MAI_DATA 0x020 + +/* Divider from HDMI HSM clock to MAI serial clock. Sampling period + * converges to N / (M + 1) cycles. + */ +#define VC4_HD_MAI_SMP 0x02c +# define VC4_HD_MAI_SMP_N_MASK VC4_MASK(31, 8) +# define VC4_HD_MAI_SMP_N_SHIFT 8 +# define VC4_HD_MAI_SMP_M_MASK VC4_MASK(7, 0) +# define VC4_HD_MAI_SMP_M_SHIFT 0 #define VC4_HD_VID_CTL 0x038 # define VC4_HD_VID_CTL_ENABLE BIT(31) |