root/drivers/gpu/drm/bridge/synopsys/dw-hdmi-gp-audio.c
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
 * dw-hdmi-gp-audio.c
 *
 * Copyright 2020-2022 NXP
 */
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
#include <drm/bridge/dw_hdmi.h>
#include <drm/drm_edid.h>
#include <drm/drm_connector.h>

#include <sound/hdmi-codec.h>
#include <sound/asoundef.h>
#include <sound/core.h>
#include <sound/initval.h>
#include <sound/pcm.h>
#include <sound/pcm_drm_eld.h>
#include <sound/pcm_iec958.h>
#include <sound/dmaengine_pcm.h>

#include "dw-hdmi-audio.h"

#define DRIVER_NAME "dw-hdmi-gp-audio"
#define DRV_NAME    "hdmi-gp-audio"

struct snd_dw_hdmi {
        struct dw_hdmi_audio_data data;
        struct platform_device  *audio_pdev;
        unsigned int pos;
};

struct dw_hdmi_channel_conf {
        u8 conf1;
        u8 ca;
};

/*
 * The default mapping of ALSA channels to HDMI channels and speaker
 * allocation bits.  Note that we can't do channel remapping here -
 * channels must be in the same order.
 *
 * Mappings for alsa-lib pcm/surround*.conf files:
 *
 *              Front   Sur4.0  Sur4.1  Sur5.0  Sur5.1  Sur7.1
 * Channels     2       4       6       6       6       8
 *
 * Our mapping from ALSA channel to CEA686D speaker name and HDMI channel:
 *
 *                              Number of ALSA channels
 * ALSA Channel 2       3       4       5       6       7       8
 * 0            FL:0    =       =       =       =       =       =
 * 1            FR:1    =       =       =       =       =       =
 * 2                    FC:3    RL:4    LFE:2   =       =       =
 * 3                            RR:5    RL:4    FC:3    =       =
 * 4                                    RR:5    RL:4    =       =
 * 5                                            RR:5    =       =
 * 6                                                    RC:6    =
 * 7                                                    RLC/FRC RLC/FRC
 */
static struct dw_hdmi_channel_conf default_hdmi_channel_config[7] = {
        { 0x03, 0x00 }, /* FL,FR */
        { 0x0b, 0x02 }, /* FL,FR,FC */
        { 0x33, 0x08 }, /* FL,FR,RL,RR */
        { 0x37, 0x09 }, /* FL,FR,LFE,RL,RR */
        { 0x3f, 0x0b }, /* FL,FR,LFE,FC,RL,RR */
        { 0x7f, 0x0f }, /* FL,FR,LFE,FC,RL,RR,RC */
        { 0xff, 0x13 }, /* FL,FR,LFE,FC,RL,RR,[FR]RC,[FR]LC */
};

static int audio_hw_params(struct device *dev,  void *data,
                           struct hdmi_codec_daifmt *daifmt,
                           struct hdmi_codec_params *params)
{
        struct snd_dw_hdmi *dw = dev_get_drvdata(dev);
        u8 ca;

        dw_hdmi_set_sample_rate(dw->data.hdmi, params->sample_rate);

        ca = default_hdmi_channel_config[params->channels - 2].ca;

        dw_hdmi_set_channel_count(dw->data.hdmi, params->channels);
        dw_hdmi_set_channel_allocation(dw->data.hdmi, ca);

        dw_hdmi_set_sample_non_pcm(dw->data.hdmi,
                                   params->iec.status[0] & IEC958_AES0_NONAUDIO);
        dw_hdmi_set_sample_width(dw->data.hdmi, params->sample_width);

        if (daifmt->bit_fmt == SNDRV_PCM_FORMAT_IEC958_SUBFRAME_LE)
                dw_hdmi_set_sample_iec958(dw->data.hdmi, 1);
        else
                dw_hdmi_set_sample_iec958(dw->data.hdmi, 0);

        return 0;
}

static void audio_shutdown(struct device *dev, void *data)
{
}

static int audio_mute_stream(struct device *dev, void *data,
                             bool enable, int direction)
{
        struct snd_dw_hdmi *dw = dev_get_drvdata(dev);

        if (!enable)
                dw_hdmi_audio_enable(dw->data.hdmi);
        else
                dw_hdmi_audio_disable(dw->data.hdmi);

        return 0;
}

static int audio_get_eld(struct device *dev, void *data,
                         u8 *buf, size_t len)
{
        struct dw_hdmi_audio_data *audio = data;
        u8 *eld;

        eld = audio->get_eld(audio->hdmi);
        if (eld)
                memcpy(buf, eld, min_t(size_t, MAX_ELD_BYTES, len));
        else
                /* Pass en empty ELD if connector not available */
                memset(buf, 0, len);

        return 0;
}

static int audio_hook_plugged_cb(struct device *dev, void *data,
                                 hdmi_codec_plugged_cb fn,
                                 struct device *codec_dev)
{
        struct snd_dw_hdmi *dw = dev_get_drvdata(dev);

        return dw_hdmi_set_plugged_cb(dw->data.hdmi, fn, codec_dev);
}

static const struct hdmi_codec_ops audio_codec_ops = {
        .hw_params = audio_hw_params,
        .audio_shutdown = audio_shutdown,
        .mute_stream = audio_mute_stream,
        .get_eld = audio_get_eld,
        .hook_plugged_cb = audio_hook_plugged_cb,
};

static int snd_dw_hdmi_probe(struct platform_device *pdev)
{
        struct dw_hdmi_audio_data *data = pdev->dev.platform_data;
        struct snd_dw_hdmi *dw;

        const struct hdmi_codec_pdata codec_data = {
                .i2s = 1,
                .spdif = 0,
                .ops = &audio_codec_ops,
                .max_i2s_channels = 8,
                .data = data,
        };

        dw = devm_kzalloc(&pdev->dev, sizeof(*dw), GFP_KERNEL);
        if (!dw)
                return -ENOMEM;

        dw->data = *data;

        platform_set_drvdata(pdev, dw);

        dw->audio_pdev = platform_device_register_data(&pdev->dev,
                                                       HDMI_CODEC_DRV_NAME, 1,
                                                       &codec_data,
                                                       sizeof(codec_data));

        return PTR_ERR_OR_ZERO(dw->audio_pdev);
}

static void snd_dw_hdmi_remove(struct platform_device *pdev)
{
        struct snd_dw_hdmi *dw = platform_get_drvdata(pdev);

        platform_device_unregister(dw->audio_pdev);
}

static struct platform_driver snd_dw_hdmi_driver = {
        .probe  = snd_dw_hdmi_probe,
        .remove = snd_dw_hdmi_remove,
        .driver = {
                .name = DRIVER_NAME,
        },
};

module_platform_driver(snd_dw_hdmi_driver);

MODULE_AUTHOR("Shengjiu Wang <shengjiu.wang@nxp.com>");
MODULE_DESCRIPTION("Synopsys Designware HDMI GPA ALSA interface");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:" DRIVER_NAME);