summaryrefslogtreecommitdiff
path: root/sound/x86/intel_hdmi_audio_if.c
diff options
context:
space:
mode:
Diffstat (limited to 'sound/x86/intel_hdmi_audio_if.c')
-rw-r--r--sound/x86/intel_hdmi_audio_if.c548
1 files changed, 548 insertions, 0 deletions
diff --git a/sound/x86/intel_hdmi_audio_if.c b/sound/x86/intel_hdmi_audio_if.c
new file mode 100644
index 000000000000..9ae242d62eb2
--- /dev/null
+++ b/sound/x86/intel_hdmi_audio_if.c
@@ -0,0 +1,548 @@
+/*
+ * intel_hdmi_audio_if.c - Intel HDMI audio driver for MID
+ *
+ * Copyright (C) 2016 Intel Corp
+ * Authors: Sailaja Bandarupalli <sailaja.bandarupalli@intel.com>
+ * Ramesh Babu K V <ramesh.babu@intel.com>
+ * Vaibhav Agarwal <vaibhav.agarwal@intel.com>
+ * Jerome Anand <jerome.anand@intel.com>
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ * ALSA driver for Intel MID HDMI audio controller. This file contains
+ * interface functions exposed to HDMI Display driver and code to register
+ * with ALSA framework..
+ */
+
+#define pr_fmt(fmt) "had: " fmt
+
+#include <linux/io.h>
+#include <linux/jiffies.h>
+#include <linux/slab.h>
+#include <sound/pcm.h>
+#include <sound/core.h>
+#include "intel_hdmi_audio.h"
+#include "intel_hdmi_lpe_audio.h"
+
+/**
+ * hdmi_audio_query - hdmi audio query function
+ *
+ *@haddata: pointer to HAD private data
+ *@event: audio event for which this method is invoked
+ *
+ * This function is called by client driver to query the
+ * hdmi audio.
+ */
+int hdmi_audio_query(void *haddata, struct hdmi_audio_event event)
+{
+ struct snd_pcm_substream *substream = NULL;
+ struct had_pvt_data *had_stream;
+ unsigned long flag_irqs;
+ struct snd_intelhad *intelhaddata = (struct snd_intelhad *)haddata;
+
+ if (intelhaddata->stream_info.had_substream)
+ substream = intelhaddata->stream_info.had_substream;
+ had_stream = intelhaddata->private_data;
+ switch (event.type) {
+ case HAD_EVENT_QUERY_IS_AUDIO_BUSY:
+ spin_lock_irqsave(&intelhaddata->had_spinlock, flag_irqs);
+
+ if ((had_stream->stream_type == HAD_RUNNING_STREAM) ||
+ substream) {
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock,
+ flag_irqs);
+ pr_debug("Audio stream active\n");
+ return -EBUSY;
+ }
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ break;
+
+ case HAD_EVENT_QUERY_IS_AUDIO_SUSPENDED:
+ spin_lock_irqsave(&intelhaddata->had_spinlock, flag_irqs);
+ if (intelhaddata->drv_status == HAD_DRV_SUSPENDED) {
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock,
+ flag_irqs);
+ pr_debug("Audio is suspended\n");
+ return 1;
+ }
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ break;
+
+ default:
+ pr_debug("error un-handled event !!\n");
+ return -EINVAL;
+ break;
+
+ }
+
+ return 0;
+}
+
+/**
+ * hdmi_audio_suspend - power management suspend function
+ *
+ *@haddata: pointer to HAD private data
+ *@event: pm event for which this method is invoked
+ *
+ * This function is called by client driver to suspend the
+ * hdmi audio.
+ */
+int hdmi_audio_suspend(void *haddata, struct hdmi_audio_event event)
+{
+ int caps, retval = 0;
+ struct had_pvt_data *had_stream;
+ unsigned long flag_irqs;
+ struct snd_pcm_substream *substream;
+ struct snd_intelhad *intelhaddata = (struct snd_intelhad *)haddata;
+
+ pr_debug("Enter:%s\n", __func__);
+
+ had_stream = intelhaddata->private_data;
+ substream = intelhaddata->stream_info.had_substream;
+
+ if (intelhaddata->dev->power.runtime_status != RPM_SUSPENDED) {
+ pr_err("audio stream is active\n");
+ return -EAGAIN;
+ }
+
+
+ spin_lock_irqsave(&intelhaddata->had_spinlock, flag_irqs);
+ if (intelhaddata->drv_status == HAD_DRV_DISCONNECTED) {
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ pr_debug("had not connected\n");
+ return retval;
+ }
+
+ if (intelhaddata->drv_status == HAD_DRV_SUSPENDED) {
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ pr_debug("had already suspended\n");
+ return retval;
+ }
+
+ intelhaddata->drv_status = HAD_DRV_SUSPENDED;
+ pr_debug("%s @ %d:DEBUG PLUG/UNPLUG : HAD_DRV_SUSPENDED\n",
+ __func__, __LINE__);
+
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ /*
+ * ToDo: Need to disable UNDERRUN interrupts as well
+ * caps = HDMI_AUDIO_UNDERRUN | HDMI_AUDIO_BUFFER_DONE;
+ */
+ caps = HDMI_AUDIO_BUFFER_DONE;
+ had_set_caps(HAD_SET_DISABLE_AUDIO_INT, &caps);
+ had_set_caps(HAD_SET_DISABLE_AUDIO, NULL);
+ pr_debug("Exit:%s", __func__);
+ return retval;
+}
+
+/**
+ * hdmi_audio_resume - power management resume function
+ *
+ *@haddata: pointer to HAD private data
+ *
+ * This function is called by client driver to resume the
+ * hdmi audio.
+ */
+int hdmi_audio_resume(void *haddata)
+{
+ int caps, retval = 0;
+ struct snd_intelhad *intelhaddata = (struct snd_intelhad *)haddata;
+ unsigned long flag_irqs;
+
+ pr_debug("Enter:%s\n", __func__);
+
+ spin_lock_irqsave(&intelhaddata->had_spinlock, flag_irqs);
+ if (intelhaddata->drv_status == HAD_DRV_DISCONNECTED) {
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ pr_debug("had not connected\n");
+ return 0;
+ }
+
+ if (intelhaddata->drv_status != HAD_DRV_SUSPENDED) {
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ pr_err("had is not in suspended state\n");
+ return 0;
+ }
+
+ if (had_get_hwstate(intelhaddata)) {
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ pr_err("Failed to resume. Device not accessible\n");
+ return -ENODEV;
+ }
+
+ intelhaddata->drv_status = HAD_DRV_CONNECTED;
+ pr_debug("%s @ %d:DEBUG PLUG/UNPLUG : HAD_DRV_DISCONNECTED\n",
+ __func__, __LINE__);
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ /*
+ * ToDo: Need to enable UNDERRUN interrupts as well
+ * caps = HDMI_AUDIO_UNDERRUN | HDMI_AUDIO_BUFFER_DONE;
+ */
+ caps = HDMI_AUDIO_BUFFER_DONE;
+ retval = had_set_caps(HAD_SET_ENABLE_AUDIO_INT, &caps);
+ retval = had_set_caps(HAD_SET_ENABLE_AUDIO, NULL);
+ pr_debug("Exit:%s", __func__);
+ return retval;
+}
+
+static inline int had_chk_intrmiss(struct snd_intelhad *intelhaddata,
+ enum intel_had_aud_buf_type buf_id)
+{
+ int i, intr_count = 0;
+ enum intel_had_aud_buf_type buff_done;
+ u32 buf_size, buf_addr;
+ struct had_pvt_data *had_stream;
+ unsigned long flag_irqs;
+
+ had_stream = intelhaddata->private_data;
+
+ buff_done = buf_id;
+
+ intr_count = snd_intelhad_read_len(intelhaddata);
+ if (intr_count > 1) {
+ /* In case of active playback */
+ pr_err("Driver detected %d missed buffer done interrupt(s)!!!!\n",
+ (intr_count - 1));
+ if (intr_count > 3)
+ return intr_count;
+
+ buf_id += (intr_count - 1);
+ /* Reprogram registers*/
+ for (i = buff_done; i < buf_id; i++) {
+ int j = i % 4;
+
+ buf_size = intelhaddata->buf_info[j].buf_size;
+ buf_addr = intelhaddata->buf_info[j].buf_addr;
+ had_write_register(AUD_BUF_A_LENGTH +
+ (j * HAD_REG_WIDTH), buf_size);
+ had_write_register(
+ AUD_BUF_A_ADDR+(j * HAD_REG_WIDTH),
+ (buf_addr | BIT(0) | BIT(1)));
+ }
+ buf_id = buf_id % 4;
+ spin_lock_irqsave(&intelhaddata->had_spinlock, flag_irqs);
+ intelhaddata->buff_done = buf_id;
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ }
+
+ return intr_count;
+}
+
+int had_process_buffer_done(struct snd_intelhad *intelhaddata)
+{
+ u32 len = 1;
+ enum intel_had_aud_buf_type buf_id;
+ enum intel_had_aud_buf_type buff_done;
+ struct pcm_stream_info *stream;
+ u32 buf_size;
+ struct had_pvt_data *had_stream;
+ int intr_count;
+ enum had_status_stream stream_type;
+ unsigned long flag_irqs;
+
+ had_stream = intelhaddata->private_data;
+ stream = &intelhaddata->stream_info;
+ intr_count = 1;
+
+ spin_lock_irqsave(&intelhaddata->had_spinlock, flag_irqs);
+ if (intelhaddata->drv_status == HAD_DRV_DISCONNECTED) {
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ pr_err("%s:Device already disconnected\n", __func__);
+ return 0;
+ }
+ buf_id = intelhaddata->curr_buf;
+ intelhaddata->buff_done = buf_id;
+ buff_done = intelhaddata->buff_done;
+ buf_size = intelhaddata->buf_info[buf_id].buf_size;
+ stream_type = had_stream->stream_type;
+
+ pr_debug("Enter:%s buf_id=%d\n", __func__, buf_id);
+
+ /* Every debug statement has an implication
+ * of ~5msec. Thus, avoid having >3 debug statements
+ * for each buffer_done handling.
+ */
+
+ /* Check for any intr_miss in case of active playback */
+ if (had_stream->stream_type == HAD_RUNNING_STREAM) {
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ intr_count = had_chk_intrmiss(intelhaddata, buf_id);
+ if (!intr_count || (intr_count > 3)) {
+ pr_err("HAD SW state in non-recoverable!!! mode\n");
+ pr_err("Already played stale data\n");
+ return 0;
+ }
+ buf_id += (intr_count - 1);
+ buf_id = buf_id % 4;
+ spin_lock_irqsave(&intelhaddata->had_spinlock, flag_irqs);
+ }
+
+ intelhaddata->buf_info[buf_id].is_valid = true;
+ if (intelhaddata->valid_buf_cnt-1 == buf_id) {
+ if (had_stream->stream_type >= HAD_RUNNING_STREAM)
+ intelhaddata->curr_buf = HAD_BUF_TYPE_A;
+ } else
+ intelhaddata->curr_buf = buf_id + 1;
+
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+
+ if (had_get_hwstate(intelhaddata)) {
+ pr_err("HDMI cable plugged-out\n");
+ return 0;
+ }
+
+ /*Reprogram the registers with addr and length*/
+ had_write_register(AUD_BUF_A_LENGTH +
+ (buf_id * HAD_REG_WIDTH), buf_size);
+ had_write_register(AUD_BUF_A_ADDR+(buf_id * HAD_REG_WIDTH),
+ intelhaddata->buf_info[buf_id].buf_addr|
+ BIT(0) | BIT(1));
+
+ had_read_register(AUD_BUF_A_LENGTH + (buf_id * HAD_REG_WIDTH),
+ &len);
+ pr_debug("%s:Enabled buf[%d]\n", __func__, buf_id);
+
+ /* In case of actual data,
+ * report buffer_done to above ALSA layer
+ */
+ buf_size = intelhaddata->buf_info[buf_id].buf_size;
+ if (stream_type >= HAD_RUNNING_STREAM) {
+ intelhaddata->stream_info.buffer_rendered +=
+ (intr_count * buf_size);
+ stream->period_elapsed(stream->had_substream);
+ }
+
+ return 0;
+}
+
+int had_process_buffer_underrun(struct snd_intelhad *intelhaddata)
+{
+ enum intel_had_aud_buf_type buf_id;
+ struct pcm_stream_info *stream;
+ struct had_pvt_data *had_stream;
+ enum had_status_stream stream_type;
+ unsigned long flag_irqs;
+ int drv_status;
+
+ had_stream = intelhaddata->private_data;
+ stream = &intelhaddata->stream_info;
+
+ spin_lock_irqsave(&intelhaddata->had_spinlock, flag_irqs);
+ buf_id = intelhaddata->curr_buf;
+ stream_type = had_stream->stream_type;
+ intelhaddata->buff_done = buf_id;
+ drv_status = intelhaddata->drv_status;
+ if (stream_type == HAD_RUNNING_STREAM)
+ intelhaddata->curr_buf = HAD_BUF_TYPE_A;
+
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+
+ pr_debug("Enter:%s buf_id=%d, stream_type=%d\n",
+ __func__, buf_id, stream_type);
+
+ intelhaddata->ops->handle_underrun(intelhaddata);
+
+ if (drv_status == HAD_DRV_DISCONNECTED) {
+ pr_err("%s:Device already disconnected\n", __func__);
+ return 0;
+ }
+
+ if (stream_type == HAD_RUNNING_STREAM) {
+ /* Report UNDERRUN error to above layers */
+ intelhaddata->flag_underrun = 1;
+ stream->period_elapsed(stream->had_substream);
+ }
+
+ return 0;
+}
+
+int had_process_hot_plug(struct snd_intelhad *intelhaddata)
+{
+ enum intel_had_aud_buf_type buf_id;
+ struct snd_pcm_substream *substream;
+ struct had_pvt_data *had_stream;
+ unsigned long flag_irqs;
+
+ pr_debug("Enter:%s\n", __func__);
+
+ substream = intelhaddata->stream_info.had_substream;
+ had_stream = intelhaddata->private_data;
+
+ spin_lock_irqsave(&intelhaddata->had_spinlock, flag_irqs);
+ if (intelhaddata->drv_status == HAD_DRV_CONNECTED) {
+ pr_debug("Device already connected\n");
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ return 0;
+ }
+ buf_id = intelhaddata->curr_buf;
+ intelhaddata->buff_done = buf_id;
+ intelhaddata->drv_status = HAD_DRV_CONNECTED;
+ pr_debug("%s @ %d:DEBUG PLUG/UNPLUG : HAD_DRV_CONNECTED\n",
+ __func__, __LINE__);
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+
+ pr_debug("Processing HOT_PLUG, buf_id = %d\n", buf_id);
+
+ /* Query display driver for audio register base */
+ if (intelhaddata->reg_ops.hdmi_audio_get_register_base(
+ &intelhaddata->audio_reg_base,
+ &intelhaddata->audio_cfg_offset)) {
+ pr_err("Unable to get audio reg base from Display driver\n");
+ goto err;
+ }
+
+ if (intelhaddata->audio_reg_base == NULL) {
+ pr_err("audio reg base value is NULL\n");
+ goto err;
+ }
+
+ pr_debug("%s audio_reg_base = 0x%p\n", __func__,
+ intelhaddata->audio_reg_base);
+
+ /* Safety check */
+ if (substream) {
+ pr_debug("There should not be active PB from ALSA\n");
+ pr_debug("Signifies, cable is plugged-in even before\n");
+ pr_debug("processing snd_pcm_disconnect\n");
+ /* Set runtime->state to hw_params done */
+ snd_pcm_stop(substream, SNDRV_PCM_STATE_SETUP);
+ }
+
+ had_build_channel_allocation_map(intelhaddata);
+
+ return 0;
+
+err:
+ pm_runtime_disable(intelhaddata->dev);
+ intelhaddata->dev = NULL;
+ return 0;
+}
+
+int had_process_hot_unplug(struct snd_intelhad *intelhaddata)
+{
+ int caps, retval = 0;
+ enum intel_had_aud_buf_type buf_id;
+ struct had_pvt_data *had_stream;
+ unsigned long flag_irqs;
+
+ pr_debug("Enter:%s\n", __func__);
+
+ had_stream = intelhaddata->private_data;
+ buf_id = intelhaddata->curr_buf;
+
+ spin_lock_irqsave(&intelhaddata->had_spinlock, flag_irqs);
+
+ if (intelhaddata->drv_status == HAD_DRV_DISCONNECTED) {
+ pr_debug("Device already disconnected\n");
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ return retval;
+
+ } else {
+ /* Disable Audio */
+ caps = HDMI_AUDIO_BUFFER_DONE;
+ retval = had_set_caps(HAD_SET_DISABLE_AUDIO_INT, &caps);
+ retval = had_set_caps(HAD_SET_DISABLE_AUDIO, NULL);
+ intelhaddata->ops->enable_audio(
+ intelhaddata->stream_info.had_substream, 0);
+ }
+
+ intelhaddata->drv_status = HAD_DRV_DISCONNECTED;
+ pr_debug("%s @ %d:DEBUG PLUG/UNPLUG : HAD_DRV_DISCONNECTED\n",
+ __func__, __LINE__);
+
+ /* Report to above ALSA layer */
+ if (intelhaddata->stream_info.had_substream != NULL) {
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ pr_debug("%s: unlock -> sending pcm_stop -> lock\n", __func__);
+ snd_pcm_stop(intelhaddata->stream_info.had_substream,
+ SNDRV_PCM_STATE_DISCONNECTED);
+ spin_lock_irqsave(&intelhaddata->had_spinlock, flag_irqs);
+ }
+
+ had_stream->stream_type = HAD_INIT;
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ kfree(intelhaddata->chmap->chmap);
+ intelhaddata->chmap->chmap = NULL;
+ intelhaddata->audio_reg_base = NULL;
+ pr_debug("%s: unlocked -> returned\n", __func__);
+
+ return retval;
+}
+
+/**
+ * had_event_handler - Call back function to handle events
+ *
+ * @event_type: Event type to handle
+ * @data: data related to the event_type
+ *
+ * This function is invoked to handle HDMI events from client driver.
+ */
+int had_event_handler(enum had_event_type event_type, void *data)
+{
+ int retval = 0;
+ struct snd_intelhad *intelhaddata = data;
+ enum intel_had_aud_buf_type buf_id;
+ struct snd_pcm_substream *substream;
+ struct had_pvt_data *had_stream;
+ unsigned long flag_irqs;
+
+ buf_id = intelhaddata->curr_buf;
+ had_stream = intelhaddata->private_data;
+
+ /* Switching to a function can drop atomicity even in INTR context.
+ * Thus, a big lock is acquired to maintain atomicity.
+ * This can be optimized later.
+ * Currently, only buffer_done/_underrun executes in INTR context.
+ * Also, locking is implemented separately to avoid real contention
+ * of data(struct intelhaddata) between IRQ/SOFT_IRQ/PROCESS context.
+ */
+ substream = intelhaddata->stream_info.had_substream;
+ switch (event_type) {
+ case HAD_EVENT_AUDIO_BUFFER_DONE:
+ retval = had_process_buffer_done(intelhaddata);
+ break;
+
+ case HAD_EVENT_AUDIO_BUFFER_UNDERRUN:
+ retval = had_process_buffer_underrun(intelhaddata);
+ break;
+
+ case HAD_EVENT_HOT_PLUG:
+ retval = had_process_hot_plug(intelhaddata);
+ break;
+
+ case HAD_EVENT_HOT_UNPLUG:
+ retval = had_process_hot_unplug(intelhaddata);
+ break;
+
+ case HAD_EVENT_MODE_CHANGING:
+ pr_debug(" called _event_handler with _MODE_CHANGE event\n");
+ /* Process only if stream is active & cable Plugged-in */
+ spin_lock_irqsave(&intelhaddata->had_spinlock, flag_irqs);
+ if (intelhaddata->drv_status >= HAD_DRV_DISCONNECTED) {
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock,
+ flag_irqs);
+ break;
+ }
+ spin_unlock_irqrestore(&intelhaddata->had_spinlock, flag_irqs);
+ if ((had_stream->stream_type == HAD_RUNNING_STREAM)
+ && substream)
+ retval = hdmi_audio_mode_change(substream);
+ break;
+
+ default:
+ pr_debug("error un-handled event !!\n");
+ retval = -EINVAL;
+ break;
+
+ }
+ return retval;
+}