7f2e9ab5a2
Similarly like the previous patch, drop the global variable to keep the ELD copy. It can be embedded in hdmi_lpe_audio_ctx as well. And this makes easier to code, it's just a memcpy(), after all. Signed-off-by: Takashi Iwai <tiwai@suse.de>
478 lines
12 KiB
C
478 lines
12 KiB
C
/*
|
|
* intel_hdmi_lpe_audio.c - Intel HDMI LPE audio driver for Atom platforms
|
|
*
|
|
* Copyright (C) 2016 Intel Corp
|
|
* Authors:
|
|
* Jerome Anand <jerome.anand@intel.com>
|
|
* Aravind Siddappaji <aravindx.siddappaji@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.
|
|
*
|
|
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
*/
|
|
|
|
#include <linux/platform_device.h>
|
|
#include <linux/irqreturn.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/io.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/module.h>
|
|
#include <linux/pci.h>
|
|
#include <sound/core.h>
|
|
#include <sound/pcm.h>
|
|
#include <sound/pcm_params.h>
|
|
#include <sound/initval.h>
|
|
#include <sound/control.h>
|
|
#include <sound/initval.h>
|
|
#include <drm/intel_lpe_audio.h>
|
|
#include "intel_hdmi_lpe_audio.h"
|
|
#include "intel_hdmi_audio.h"
|
|
|
|
struct hdmi_lpe_audio_ctx {
|
|
struct platform_device *pdev;
|
|
int irq;
|
|
void __iomem *mmio_start;
|
|
struct snd_intelhad *had;
|
|
int tmds_clock_speed;
|
|
bool dp_output;
|
|
int link_rate;
|
|
unsigned int had_config_offset;
|
|
int hdmi_audio_interrupt_mask;
|
|
struct work_struct hdmi_audio_wq;
|
|
int state; /* connection state */
|
|
union otm_hdmi_eld_t eld; /* ELD copy */
|
|
};
|
|
|
|
static void mid_hdmi_audio_signal_event(struct platform_device *pdev,
|
|
enum had_event_type event)
|
|
{
|
|
struct hdmi_lpe_audio_ctx *ctx = platform_get_drvdata(pdev);
|
|
|
|
dev_dbg(&pdev->dev, "%s: Enter\n", __func__);
|
|
|
|
/* The handler is protected in the respective
|
|
* event handlers to avoid races
|
|
*/
|
|
had_event_handler(event, ctx->had);
|
|
}
|
|
|
|
/*
|
|
* used to write into display controller HDMI audio registers.
|
|
*/
|
|
int mid_hdmi_audio_write(struct platform_device *pdev, u32 reg, u32 val)
|
|
{
|
|
struct hdmi_lpe_audio_ctx *ctx = platform_get_drvdata(pdev);
|
|
|
|
dev_dbg(&pdev->dev, "%s: reg[0x%x] = 0x%x\n", __func__, reg, val);
|
|
|
|
if (ctx->dp_output) {
|
|
if (reg == AUD_CONFIG && (val & AUD_CONFIG_VALID_BIT))
|
|
val |= AUD_CONFIG_DP_MODE | AUD_CONFIG_BLOCK_BIT;
|
|
}
|
|
iowrite32(val, ctx->mmio_start + ctx->had_config_offset + reg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* used to get the register value read from
|
|
* display controller HDMI audio registers.
|
|
*/
|
|
int mid_hdmi_audio_read(struct platform_device *pdev, u32 reg, u32 *val)
|
|
{
|
|
struct hdmi_lpe_audio_ctx *ctx = platform_get_drvdata(pdev);
|
|
|
|
*val = ioread32(ctx->mmio_start + ctx->had_config_offset + reg);
|
|
dev_dbg(&pdev->dev, "%s: reg[0x%x] = 0x%x\n", __func__, reg, *val);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* used to update the masked bits in display controller HDMI
|
|
* audio registers.
|
|
*/
|
|
int mid_hdmi_audio_rmw(struct platform_device *pdev,
|
|
u32 reg, u32 val, u32 mask)
|
|
{
|
|
struct hdmi_lpe_audio_ctx *ctx = platform_get_drvdata(pdev);
|
|
u32 val_tmp = 0;
|
|
|
|
val_tmp = ioread32(ctx->mmio_start + ctx->had_config_offset + reg);
|
|
val_tmp &= ~mask;
|
|
val_tmp |= (val & mask);
|
|
|
|
if (ctx->dp_output) {
|
|
if (reg == AUD_CONFIG && (val_tmp & AUD_CONFIG_VALID_BIT))
|
|
val_tmp |= AUD_CONFIG_DP_MODE | AUD_CONFIG_BLOCK_BIT;
|
|
}
|
|
|
|
iowrite32(val_tmp, ctx->mmio_start + ctx->had_config_offset + reg);
|
|
dev_dbg(&pdev->dev, "%s: reg[0x%x] = 0x%x\n", __func__,
|
|
reg, val_tmp);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* used to return the HDMI audio capabilities.
|
|
* e.g. resolution, frame rate.
|
|
*/
|
|
int mid_hdmi_audio_get_caps(struct platform_device *pdev,
|
|
enum had_caps_list get_element,
|
|
void *capabilities)
|
|
{
|
|
struct hdmi_lpe_audio_ctx *ctx = platform_get_drvdata(pdev);
|
|
int ret = 0;
|
|
|
|
dev_dbg(&pdev->dev, "%s: Enter\n", __func__);
|
|
|
|
switch (get_element) {
|
|
case HAD_GET_ELD:
|
|
memcpy(capabilities, &ctx->eld, sizeof(ctx->eld));
|
|
print_hex_dump_bytes("eld: ", DUMP_PREFIX_NONE,
|
|
(u8 *)&ctx->eld, sizeof(ctx->eld));
|
|
break;
|
|
case HAD_GET_DISPLAY_RATE:
|
|
/* ToDo: Verify if sampling freq logic is correct */
|
|
*(u32 *)capabilities = ctx->tmds_clock_speed;
|
|
dev_dbg(&pdev->dev, "%s: tmds_clock_speed = 0x%x\n",
|
|
__func__, ctx->tmds_clock_speed);
|
|
break;
|
|
case HAD_GET_LINK_RATE:
|
|
/* ToDo: Verify if sampling freq logic is correct */
|
|
*(u32 *)capabilities = ctx->link_rate;
|
|
dev_dbg(&pdev->dev, "%s: link rate = 0x%x\n",
|
|
__func__, ctx->link_rate);
|
|
break;
|
|
case HAD_GET_DP_OUTPUT:
|
|
*(u32 *)capabilities = ctx->dp_output;
|
|
dev_dbg(&pdev->dev, "%s: dp_output = %d\n",
|
|
__func__, ctx->dp_output);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* used to set the HDMI audio capabilities.
|
|
* e.g. Audio INT.
|
|
*/
|
|
int mid_hdmi_audio_set_caps(struct platform_device *pdev,
|
|
enum had_caps_list set_element,
|
|
void *capabilties)
|
|
{
|
|
dev_dbg(&pdev->dev, "%s: cap_id = 0x%x\n", __func__, set_element);
|
|
|
|
switch (set_element) {
|
|
case HAD_SET_ENABLE_AUDIO_INT:
|
|
{
|
|
u32 status_reg;
|
|
|
|
mid_hdmi_audio_read(pdev, AUD_HDMI_STATUS_v2,
|
|
&status_reg);
|
|
status_reg |=
|
|
HDMI_AUDIO_BUFFER_DONE | HDMI_AUDIO_UNDERRUN;
|
|
mid_hdmi_audio_write(pdev, AUD_HDMI_STATUS_v2,
|
|
status_reg);
|
|
mid_hdmi_audio_read(pdev, AUD_HDMI_STATUS_v2,
|
|
&status_reg);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void _had_wq(struct work_struct *work)
|
|
{
|
|
struct hdmi_lpe_audio_ctx *ctx =
|
|
container_of(work, struct hdmi_lpe_audio_ctx, hdmi_audio_wq);
|
|
|
|
mid_hdmi_audio_signal_event(ctx->pdev, HAD_EVENT_HOT_PLUG);
|
|
}
|
|
|
|
static irqreturn_t display_pipe_interrupt_handler(int irq, void *dev_id)
|
|
{
|
|
struct platform_device *pdev = dev_id;
|
|
u32 audio_stat, audio_reg;
|
|
struct hdmi_lpe_audio_ctx *ctx;
|
|
|
|
dev_dbg(&pdev->dev, "%s: Enter\n", __func__);
|
|
|
|
ctx = platform_get_drvdata(pdev);
|
|
|
|
audio_reg = AUD_HDMI_STATUS_v2;
|
|
mid_hdmi_audio_read(pdev, audio_reg, &audio_stat);
|
|
|
|
if (audio_stat & HDMI_AUDIO_UNDERRUN) {
|
|
mid_hdmi_audio_write(pdev, audio_reg, HDMI_AUDIO_UNDERRUN);
|
|
mid_hdmi_audio_signal_event(pdev,
|
|
HAD_EVENT_AUDIO_BUFFER_UNDERRUN);
|
|
}
|
|
|
|
if (audio_stat & HDMI_AUDIO_BUFFER_DONE) {
|
|
mid_hdmi_audio_write(pdev, audio_reg, HDMI_AUDIO_BUFFER_DONE);
|
|
mid_hdmi_audio_signal_event(pdev,
|
|
HAD_EVENT_AUDIO_BUFFER_DONE);
|
|
}
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static void notify_audio_lpe(struct platform_device *pdev)
|
|
{
|
|
struct hdmi_lpe_audio_ctx *ctx = platform_get_drvdata(pdev);
|
|
struct intel_hdmi_lpe_audio_pdata *pdata = pdev->dev.platform_data;
|
|
|
|
if (pdata->hdmi_connected != true) {
|
|
|
|
dev_dbg(&pdev->dev, "%s: Event: HAD_NOTIFY_HOT_UNPLUG\n",
|
|
__func__);
|
|
|
|
if (ctx->state == hdmi_connector_status_connected) {
|
|
|
|
ctx->state = hdmi_connector_status_disconnected;
|
|
|
|
mid_hdmi_audio_signal_event(pdev,
|
|
HAD_EVENT_HOT_UNPLUG);
|
|
} else
|
|
dev_dbg(&pdev->dev, "%s: Already Unplugged!\n",
|
|
__func__);
|
|
|
|
} else {
|
|
struct intel_hdmi_lpe_audio_eld *eld = &pdata->eld;
|
|
|
|
switch (eld->pipe_id) {
|
|
case 0:
|
|
ctx->had_config_offset = AUDIO_HDMI_CONFIG_A;
|
|
break;
|
|
case 1:
|
|
ctx->had_config_offset = AUDIO_HDMI_CONFIG_B;
|
|
break;
|
|
case 2:
|
|
ctx->had_config_offset = AUDIO_HDMI_CONFIG_C;
|
|
break;
|
|
default:
|
|
dev_dbg(&pdev->dev, "Invalid pipe %d\n",
|
|
eld->pipe_id);
|
|
break;
|
|
}
|
|
|
|
memcpy(&ctx->eld, eld->eld_data, sizeof(ctx->eld));
|
|
|
|
mid_hdmi_audio_signal_event(pdev, HAD_EVENT_HOT_PLUG);
|
|
|
|
ctx->state = hdmi_connector_status_connected;
|
|
|
|
dev_dbg(&pdev->dev, "%s: HAD_NOTIFY_ELD : port = %d, tmds = %d\n",
|
|
__func__, eld->port_id, pdata->tmds_clock_speed);
|
|
|
|
if (pdata->tmds_clock_speed) {
|
|
ctx->tmds_clock_speed = pdata->tmds_clock_speed;
|
|
ctx->dp_output = pdata->dp_output;
|
|
ctx->link_rate = pdata->link_rate;
|
|
mid_hdmi_audio_signal_event(pdev,
|
|
HAD_EVENT_MODE_CHANGING);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* hdmi_lpe_audio_probe - start bridge with i915
|
|
*
|
|
* This function is called when the i915 driver creates the
|
|
* hdmi-lpe-audio platform device. Card creation is deferred until a
|
|
* hot plug event is received
|
|
*/
|
|
static int hdmi_lpe_audio_probe(struct platform_device *pdev)
|
|
{
|
|
struct hdmi_lpe_audio_ctx *ctx;
|
|
struct intel_hdmi_lpe_audio_pdata *pdata;
|
|
int irq;
|
|
struct resource *res_mmio;
|
|
void __iomem *mmio_start;
|
|
int ret = 0;
|
|
unsigned long flag_irq;
|
|
static const struct pci_device_id cherryview_ids[] = {
|
|
{PCI_DEVICE(0x8086, 0x22b0)},
|
|
{PCI_DEVICE(0x8086, 0x22b1)},
|
|
{PCI_DEVICE(0x8086, 0x22b2)},
|
|
{PCI_DEVICE(0x8086, 0x22b3)},
|
|
{}
|
|
};
|
|
|
|
dev_dbg(&pdev->dev, "Enter %s\n", __func__);
|
|
|
|
/* get resources */
|
|
irq = platform_get_irq(pdev, 0);
|
|
if (irq < 0) {
|
|
dev_err(&pdev->dev, "Could not get irq resource\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
res_mmio = platform_get_resource(pdev, IORESOURCE_MEM, 0);
|
|
if (!res_mmio) {
|
|
dev_err(&pdev->dev, "Could not get IO_MEM resources\n");
|
|
return -ENXIO;
|
|
}
|
|
|
|
dev_dbg(&pdev->dev, "%s: mmio_start = 0x%x, mmio_end = 0x%x\n",
|
|
__func__, (unsigned int)res_mmio->start,
|
|
(unsigned int)res_mmio->end);
|
|
|
|
mmio_start = ioremap_nocache(res_mmio->start,
|
|
(size_t)(resource_size(res_mmio)));
|
|
if (!mmio_start) {
|
|
dev_err(&pdev->dev, "Could not get ioremap\n");
|
|
return -EACCES;
|
|
}
|
|
|
|
/* alloc and save context */
|
|
ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
|
|
if (ctx == NULL) {
|
|
ret = -ENOMEM;
|
|
goto error_ctx;
|
|
}
|
|
|
|
ctx->pdev = pdev;
|
|
ctx->irq = irq;
|
|
dev_dbg(&pdev->dev, "hdmi lpe audio: irq num = %d\n", irq);
|
|
ctx->mmio_start = mmio_start;
|
|
ctx->tmds_clock_speed = DIS_SAMPLE_RATE_148_5;
|
|
INIT_WORK(&ctx->hdmi_audio_wq, _had_wq);
|
|
ctx->state = hdmi_connector_status_disconnected;
|
|
|
|
if (pci_dev_present(cherryview_ids))
|
|
dev_dbg(&pdev->dev, "%s: Cherrytrail LPE - Detected\n",
|
|
__func__);
|
|
else
|
|
dev_dbg(&pdev->dev, "%s: Baytrail LPE - Assume\n",
|
|
__func__);
|
|
|
|
/* assume pipe A as default */
|
|
ctx->had_config_offset = AUDIO_HDMI_CONFIG_A;
|
|
|
|
pdata = pdev->dev.platform_data;
|
|
|
|
if (pdata == NULL) {
|
|
dev_err(&pdev->dev, "%s: quit: pdata not allocated by i915!!\n", __func__);
|
|
ret = -ENOMEM;
|
|
goto error_irq;
|
|
}
|
|
|
|
platform_set_drvdata(pdev, ctx);
|
|
|
|
/* setup interrupt handler */
|
|
ret = request_irq(irq, display_pipe_interrupt_handler, 0,
|
|
pdev->name, pdev);
|
|
|
|
if (ret < 0) {
|
|
dev_err(&pdev->dev, "request_irq failed\n");
|
|
goto error_irq;
|
|
}
|
|
|
|
ret = hdmi_audio_probe(pdev, &ctx->had);
|
|
if (ret < 0)
|
|
goto error_probe;
|
|
|
|
dev_dbg(&pdev->dev, "hdmi lpe audio: setting pin eld notify callback\n");
|
|
|
|
/* The Audio driver is loading now and we need to notify
|
|
* it if there is an HDMI device attached
|
|
*/
|
|
dev_dbg(&pdev->dev, "%s: Scheduling HDMI audio work queue\n",
|
|
__func__);
|
|
schedule_work(&ctx->hdmi_audio_wq);
|
|
|
|
spin_lock_irqsave(&pdata->lpe_audio_slock, flag_irq);
|
|
pdata->notify_audio_lpe = notify_audio_lpe;
|
|
if (pdata->notify_pending) {
|
|
|
|
dev_dbg(&pdev->dev, "%s: handle pending notification\n", __func__);
|
|
notify_audio_lpe(pdev);
|
|
pdata->notify_pending = false;
|
|
}
|
|
spin_unlock_irqrestore(&pdata->lpe_audio_slock, flag_irq);
|
|
|
|
return ret;
|
|
|
|
error_probe:
|
|
free_irq(irq, pdev);
|
|
error_irq:
|
|
kfree(ctx);
|
|
error_ctx:
|
|
iounmap(mmio_start);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* hdmi_lpe_audio_remove - stop bridge with i915
|
|
*
|
|
* This function is called when the platform device is destroyed. The sound
|
|
* card should have been removed on hot plug event.
|
|
*/
|
|
static int hdmi_lpe_audio_remove(struct platform_device *pdev)
|
|
{
|
|
struct hdmi_lpe_audio_ctx *ctx = platform_get_drvdata(pdev);
|
|
|
|
dev_dbg(&pdev->dev, "Enter %s\n", __func__);
|
|
|
|
hdmi_audio_remove(ctx->had);
|
|
|
|
/* release resources */
|
|
iounmap(ctx->mmio_start);
|
|
free_irq(ctx->irq, pdev);
|
|
kfree(ctx);
|
|
return 0;
|
|
}
|
|
|
|
static int hdmi_lpe_audio_suspend(struct platform_device *pdev,
|
|
pm_message_t state)
|
|
{
|
|
struct hdmi_lpe_audio_ctx *ctx = platform_get_drvdata(pdev);
|
|
|
|
dev_dbg(&pdev->dev, "%s: state %d", __func__, ctx->state);
|
|
/* HDMI is not connected, assuming audio device is suspended already */
|
|
if (ctx->state != hdmi_connector_status_disconnected)
|
|
hdmi_audio_suspend(ctx->had);
|
|
return 0;
|
|
}
|
|
|
|
static int hdmi_lpe_audio_resume(struct platform_device *pdev)
|
|
{
|
|
struct hdmi_lpe_audio_ctx *ctx = platform_get_drvdata(pdev);
|
|
|
|
dev_dbg(&pdev->dev, "%s: state %d", __func__, ctx->state);
|
|
/* HDMI is not connected, there is no need to resume audio device */
|
|
if (ctx->state != hdmi_connector_status_disconnected)
|
|
hdmi_audio_resume(ctx->had);
|
|
return 0;
|
|
}
|
|
|
|
static struct platform_driver hdmi_lpe_audio_driver = {
|
|
.driver = {
|
|
.name = "hdmi-lpe-audio",
|
|
},
|
|
.probe = hdmi_lpe_audio_probe,
|
|
.remove = hdmi_lpe_audio_remove,
|
|
.suspend = hdmi_lpe_audio_suspend,
|
|
.resume = hdmi_lpe_audio_resume
|
|
};
|
|
|
|
module_platform_driver(hdmi_lpe_audio_driver);
|
|
MODULE_LICENSE("GPL v2");
|
|
MODULE_ALIAS("platform:hdmi_lpe_audio");
|