diff options
Diffstat (limited to 'sound/x86/intel_hdmi_lpe_audio.c')
-rw-r--r-- | sound/x86/intel_hdmi_lpe_audio.c | 616 |
1 files changed, 616 insertions, 0 deletions
diff --git a/sound/x86/intel_hdmi_lpe_audio.c b/sound/x86/intel_hdmi_lpe_audio.c new file mode 100644 index 0000000..ead2d3a --- /dev/null +++ b/sound/x86/intel_hdmi_lpe_audio.c @@ -0,0 +1,616 @@ +/* + * 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" + +/* globals*/ +static struct platform_device *hlpe_pdev; +static int hlpe_state; +static union otm_hdmi_eld_t hlpe_eld; + +struct hdmi_lpe_audio_ctx { + int irq; + void __iomem *mmio_start; + had_event_call_back had_event_callbacks; + struct snd_intel_had_interface *had_interface; + void *had_pvt_data; + int tmds_clock_speed; + unsigned int had_config_offset; + int hdmi_audio_interrupt_mask; + struct work_struct hdmi_audio_wq; +}; + +static void hdmi_set_eld(void *eld) +{ + int size; + + BUILD_BUG_ON(sizeof(hlpe_eld) > HDMI_MAX_ELD_BYTES); + + size = sizeof(hlpe_eld); + memcpy((void *)&hlpe_eld, eld, size); +} + +static int hdmi_get_eld(void *eld) +{ + u8 *eld_data = (u8 *)&hlpe_eld; + + memcpy(eld, (void *)&hlpe_eld, sizeof(hlpe_eld)); + + print_hex_dump_bytes("eld: ", DUMP_PREFIX_NONE, eld_data, + sizeof(hlpe_eld)); + return 0; +} + + +static struct hdmi_lpe_audio_ctx *get_hdmi_context(void) +{ + struct hdmi_lpe_audio_ctx *ctx; + + ctx = platform_get_drvdata(hlpe_pdev); + return ctx; +} + +/* + * return whether HDMI audio device is busy. + */ +bool mid_hdmi_audio_is_busy(void *ddev) +{ + struct hdmi_lpe_audio_ctx *ctx; + int hdmi_audio_busy = 0; + struct hdmi_audio_event hdmi_audio_event; + + dev_dbg(&hlpe_pdev->dev, "%s: Enter", __func__); + + ctx = platform_get_drvdata(hlpe_pdev); + + if (hlpe_state == hdmi_connector_status_disconnected) { + /* HDMI is not connected, assuming audio device is idle. */ + return false; + } + + if (ctx->had_interface) { + hdmi_audio_event.type = HAD_EVENT_QUERY_IS_AUDIO_BUSY; + hdmi_audio_busy = ctx->had_interface->query( + ctx->had_pvt_data, + hdmi_audio_event); + return hdmi_audio_busy != 0; + } + return false; +} + +/* + * return true if HDMI audio device is suspended/ disconnected + */ +bool mid_hdmi_audio_suspend(void *ddev) +{ + struct hdmi_lpe_audio_ctx *ctx; + struct hdmi_audio_event hdmi_audio_event; + int ret = 0; + + ctx = platform_get_drvdata(hlpe_pdev); + + if (hlpe_state == hdmi_connector_status_disconnected) { + /* HDMI is not connected, assuming audio device + * is suspended already. + */ + return true; + } + + dev_dbg(&hlpe_pdev->dev, "%s: hlpe_state %d", __func__, + hlpe_state); + + if (ctx->had_interface) { + hdmi_audio_event.type = 0; + ret = ctx->had_interface->suspend(ctx->had_pvt_data, + hdmi_audio_event); + return (ret == 0) ? true : false; + } + return true; +} + +void mid_hdmi_audio_resume(void *ddev) +{ + struct hdmi_lpe_audio_ctx *ctx; + + ctx = platform_get_drvdata(hlpe_pdev); + + if (hlpe_state == hdmi_connector_status_disconnected) { + /* HDMI is not connected, there is no need + * to resume audio device. + */ + return; + } + + dev_dbg(&hlpe_pdev->dev, "%s: hlpe_state %d", __func__, hlpe_state); + + if (ctx->had_interface) + ctx->had_interface->resume(ctx->had_pvt_data); +} + +void mid_hdmi_audio_signal_event(enum had_event_type event) +{ + struct hdmi_lpe_audio_ctx *ctx; + + dev_dbg(&hlpe_pdev->dev, "%s: Enter\n", __func__); + + ctx = platform_get_drvdata(hlpe_pdev); + + /* The handler is protected in the respective + * event handlers to avoid races + */ + if (ctx->had_event_callbacks) + (*ctx->had_event_callbacks)(event, + ctx->had_pvt_data); +} + +/** + * used to write into display controller HDMI audio registers. + */ +static int hdmi_audio_write(u32 reg, u32 val) +{ + struct hdmi_lpe_audio_ctx *ctx; + + ctx = platform_get_drvdata(hlpe_pdev); + + dev_dbg(&hlpe_pdev->dev, "%s: reg[0x%x] = 0x%x\n", __func__, reg, val); + + iowrite32(val, (ctx->mmio_start+reg)); + + return 0; +} + +/** + * used to get the register value read from + * display controller HDMI audio registers. + */ +static int hdmi_audio_read(u32 reg, u32 *val) +{ + struct hdmi_lpe_audio_ctx *ctx; + + ctx = platform_get_drvdata(hlpe_pdev); + *val = ioread32(ctx->mmio_start+reg); + dev_dbg(&hlpe_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. + */ +static int hdmi_audio_rmw(u32 reg, u32 val, u32 mask) +{ + struct hdmi_lpe_audio_ctx *ctx; + u32 val_tmp = 0; + + ctx = platform_get_drvdata(hlpe_pdev); + + val_tmp = (val & mask) | + ((ioread32(ctx->mmio_start + reg)) & ~mask); + + iowrite32(val_tmp, (ctx->mmio_start+reg)); + dev_dbg(&hlpe_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. + */ +static int hdmi_audio_get_caps(enum had_caps_list get_element, + void *capabilities) +{ + struct hdmi_lpe_audio_ctx *ctx; + int ret = 0; + + ctx = get_hdmi_context(); + + dev_dbg(&hlpe_pdev->dev, "%s: Enter\n", __func__); + + switch (get_element) { + case HAD_GET_ELD: + ret = hdmi_get_eld(capabilities); + break; + case HAD_GET_DISPLAY_RATE: + /* ToDo: Verify if sampling freq logic is correct */ + *(u32 *)capabilities = ctx->tmds_clock_speed; + dev_dbg(&hlpe_pdev->dev, "%s: tmds_clock_speed = 0x%x\n", + __func__, ctx->tmds_clock_speed); + break; + default: + break; + } + + return ret; +} + +/** + * used to get the current hdmi base address + */ +int hdmi_audio_get_register_base(u32 **reg_base, + u32 *config_offset) +{ + struct hdmi_lpe_audio_ctx *ctx; + + ctx = platform_get_drvdata(hlpe_pdev); + *reg_base = (u32 *)(ctx->mmio_start); + *config_offset = ctx->had_config_offset; + dev_dbg(&hlpe_pdev->dev, "%s: reg_base = 0x%p, cfg_off = 0x%x\n", + __func__, *reg_base, *config_offset); + return 0; +} + +/** + * used to set the HDMI audio capabilities. + * e.g. Audio INT. + */ +int hdmi_audio_set_caps(enum had_caps_list set_element, + void *capabilties) +{ + struct hdmi_lpe_audio_ctx *ctx; + + ctx = platform_get_drvdata(hlpe_pdev); + + dev_dbg(&hlpe_pdev->dev, "%s: cap_id = 0x%x\n", __func__, set_element); + + switch (set_element) { + case HAD_SET_ENABLE_AUDIO_INT: + { + u32 status_reg; + + hdmi_audio_read(AUD_HDMI_STATUS_v2 + + ctx->had_config_offset, &status_reg); + status_reg |= + HDMI_AUDIO_BUFFER_DONE | HDMI_AUDIO_UNDERRUN; + hdmi_audio_write(AUD_HDMI_STATUS_v2 + + ctx->had_config_offset, status_reg); + hdmi_audio_read(AUD_HDMI_STATUS_v2 + + ctx->had_config_offset, &status_reg); + + } + break; + default: + break; + } + + return 0; +} + +static struct hdmi_audio_registers_ops hdmi_audio_reg_ops = { + .hdmi_audio_get_register_base = hdmi_audio_get_register_base, + .hdmi_audio_read_register = hdmi_audio_read, + .hdmi_audio_write_register = hdmi_audio_write, + .hdmi_audio_read_modify = hdmi_audio_rmw, +}; + +static struct hdmi_audio_query_set_ops hdmi_audio_get_set_ops = { + .hdmi_audio_get_caps = hdmi_audio_get_caps, + .hdmi_audio_set_caps = hdmi_audio_set_caps, +}; + +int mid_hdmi_audio_setup( + had_event_call_back audio_callbacks, + struct hdmi_audio_registers_ops *reg_ops, + struct hdmi_audio_query_set_ops *query_ops) +{ + struct hdmi_lpe_audio_ctx *ctx; + + ctx = platform_get_drvdata(hlpe_pdev); + + dev_dbg(&hlpe_pdev->dev, "%s: called\n", __func__); + + reg_ops->hdmi_audio_get_register_base = + (hdmi_audio_reg_ops.hdmi_audio_get_register_base); + reg_ops->hdmi_audio_read_register = + (hdmi_audio_reg_ops.hdmi_audio_read_register); + reg_ops->hdmi_audio_write_register = + (hdmi_audio_reg_ops.hdmi_audio_write_register); + reg_ops->hdmi_audio_read_modify = + (hdmi_audio_reg_ops.hdmi_audio_read_modify); + query_ops->hdmi_audio_get_caps = + hdmi_audio_get_set_ops.hdmi_audio_get_caps; + query_ops->hdmi_audio_set_caps = + hdmi_audio_get_set_ops.hdmi_audio_set_caps; + + ctx->had_event_callbacks = audio_callbacks; + + return 0; +} + +static void _had_wq(struct work_struct *work) +{ + mid_hdmi_audio_signal_event(HAD_EVENT_HOT_PLUG); +} + +int mid_hdmi_audio_register(struct snd_intel_had_interface *driver, + void *had_data) +{ + struct hdmi_lpe_audio_ctx *ctx; + + ctx = platform_get_drvdata(hlpe_pdev); + + dev_dbg(&hlpe_pdev->dev, "%s: called\n", __func__); + + ctx->had_pvt_data = had_data; + ctx->had_interface = driver; + + /* The Audio driver is loading now and we need to notify + * it if there is an HDMI device attached + */ + INIT_WORK(&ctx->hdmi_audio_wq, _had_wq); + dev_dbg(&hlpe_pdev->dev, "%s: Scheduling HDMI audio work queue\n", + __func__); + schedule_work(&ctx->hdmi_audio_wq); + + return 0; +} + +static irqreturn_t display_pipe_interrupt_handler(int irq, void *dev_id) +{ + u32 audio_stat, audio_reg; + + struct hdmi_lpe_audio_ctx *ctx; + + dev_dbg(&hlpe_pdev->dev, "%s: Enter\n", __func__); + + ctx = platform_get_drvdata(hlpe_pdev); + + audio_reg = ctx->had_config_offset + AUD_HDMI_STATUS_v2; + hdmi_audio_read(audio_reg, &audio_stat); + + if (audio_stat & HDMI_AUDIO_UNDERRUN) { + hdmi_audio_write(audio_reg, HDMI_AUDIO_UNDERRUN); + mid_hdmi_audio_signal_event( + HAD_EVENT_AUDIO_BUFFER_UNDERRUN); + } + + if (audio_stat & HDMI_AUDIO_BUFFER_DONE) { + hdmi_audio_write(audio_reg, HDMI_AUDIO_BUFFER_DONE); + mid_hdmi_audio_signal_event( + HAD_EVENT_AUDIO_BUFFER_DONE); + } + + return IRQ_HANDLED; +} + +static void notify_audio_lpe(void *audio_ptr) +{ + struct hdmi_lpe_audio_ctx *ctx = get_hdmi_context(); + struct intel_hdmi_lpe_audio_pdata *pdata = hlpe_pdev->dev.platform_data; + struct intel_hdmi_lpe_audio_eld *eld = audio_ptr; + + if (pdata->hdmi_connected != true) { + + dev_dbg(&hlpe_pdev->dev, "%s: Event: HAD_NOTIFY_HOT_UNPLUG\n", + __func__); + + if (hlpe_state == hdmi_connector_status_connected) { + + hlpe_state = + hdmi_connector_status_disconnected; + + mid_hdmi_audio_signal_event( + HAD_EVENT_HOT_UNPLUG); + } else + dev_dbg(&hlpe_pdev->dev, "%s: Already Unplugged!\n", + __func__); + + } else if (eld != NULL) { + + hdmi_set_eld(eld->eld_data); + + mid_hdmi_audio_signal_event(HAD_EVENT_HOT_PLUG); + + hlpe_state = hdmi_connector_status_connected; + + dev_dbg(&hlpe_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; + mid_hdmi_audio_signal_event(HAD_EVENT_MODE_CHANGING); + } + } else + dev_dbg(&hlpe_pdev->dev, "%s: Event: NULL EDID!!\n", __func__); +} + +/** + * 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(&hlpe_pdev->dev, "Enter %s\n", __func__); + + /*TBD:remove globals*/ + hlpe_pdev = pdev; + hlpe_state = hdmi_connector_status_disconnected; + + /* get resources */ + irq = platform_get_irq(pdev, 0); + if (irq < 0) { + dev_err(&hlpe_pdev->dev, "Could not get irq resource\n"); + return -ENODEV; + } + + res_mmio = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res_mmio) { + dev_err(&hlpe_pdev->dev, "Could not get IO_MEM resources\n"); + return -ENXIO; + } + + dev_dbg(&hlpe_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(&hlpe_pdev->dev, "Could not get ioremap\n"); + return -EACCES; + } + + /* setup interrupt handler */ + ret = request_irq(irq, display_pipe_interrupt_handler, + 0, + pdev->name, + NULL); + if (ret < 0) { + dev_err(&hlpe_pdev->dev, "request_irq failed\n"); + iounmap(mmio_start); + return -ENODEV; + } + + /* alloc and save context */ + ctx = kzalloc(sizeof(*ctx), GFP_KERNEL); + if (ctx == NULL) { + free_irq(irq, NULL); + iounmap(mmio_start); + return -ENOMEM; + } + + ctx->irq = irq; + dev_dbg(&hlpe_pdev->dev, "hdmi lpe audio: irq num = %d\n", irq); + ctx->mmio_start = mmio_start; + ctx->tmds_clock_speed = DIS_SAMPLE_RATE_148_5; + + if (pci_dev_present(cherryview_ids)) { + dev_dbg(&hlpe_pdev->dev, "%s: Cherrytrail LPE - Detected\n", + __func__); + ctx->had_config_offset = AUDIO_HDMI_CONFIG_C; + } else { + dev_dbg(&hlpe_pdev->dev, "%s: Baytrail LPE - Assume\n", + __func__); + ctx->had_config_offset = AUDIO_HDMI_CONFIG_A; + } + + pdata = pdev->dev.platform_data; + + if (pdata == NULL) { + dev_err(&hlpe_pdev->dev, "%s: quit: pdata not allocated by i915!!\n", __func__); + kfree(ctx); + free_irq(irq, NULL); + iounmap(mmio_start); + return -ENOMEM; + } + + platform_set_drvdata(pdev, ctx); + + ret = hdmi_audio_probe((void *)pdev); + dev_dbg(&hlpe_pdev->dev, "hdmi lpe audio: setting pin eld notify callback\n"); + + spin_lock_irqsave(&pdata->lpe_audio_slock, flag_irq); + pdata->notify_audio_lpe = notify_audio_lpe; + if (pdata->notify_pending) { + + dev_dbg(&hlpe_pdev->dev, "%s: handle pending notification\n", __func__); + notify_audio_lpe(&pdata->eld); + pdata->notify_pending = false; + } + spin_unlock_irqrestore(&pdata->lpe_audio_slock, flag_irq); + + 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; + + dev_dbg(&hlpe_pdev->dev, "Enter %s\n", __func__); + + hdmi_audio_remove(pdev); + + /* get context, release resources */ + ctx = platform_get_drvdata(pdev); + iounmap(ctx->mmio_start); + free_irq(ctx->irq, NULL); + kfree(ctx); + return 0; +} + +static int hdmi_lpe_audio_suspend(struct platform_device *pt_dev, + pm_message_t state) +{ + dev_dbg(&hlpe_pdev->dev, "Enter %s\n", __func__); + mid_hdmi_audio_suspend(NULL); + return 0; +} + +static int hdmi_lpe_audio_resume(struct platform_device *pt_dev) +{ + dev_dbg(&hlpe_pdev->dev, "Enter %s\n", __func__); + mid_hdmi_audio_resume(NULL); + 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"); |