| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * Legacy Nvidia HDMI codec support |
| */ |
| |
| #include <linux/init.h> |
| #include <linux/slab.h> |
| #include <linux/module.h> |
| #include <sound/core.h> |
| #include <sound/hdaudio.h> |
| #include <sound/hda_codec.h> |
| #include "hda_local.h" |
| #include "hdmi_local.h" |
| |
| enum { MODEL_2CH, MODEL_8CH }; |
| |
| #define Nv_VERB_SET_Channel_Allocation 0xF79 |
| #define Nv_VERB_SET_Info_Frame_Checksum 0xF7A |
| #define Nv_VERB_SET_Audio_Protection_On 0xF98 |
| #define Nv_VERB_SET_Audio_Protection_Off 0xF99 |
| |
| #define nvhdmi_master_con_nid_7x 0x04 |
| #define nvhdmi_master_pin_nid_7x 0x05 |
| |
| static const hda_nid_t nvhdmi_con_nids_7x[4] = { |
| /*front, rear, clfe, rear_surr */ |
| 0x6, 0x8, 0xa, 0xc, |
| }; |
| |
| static const struct hda_verb nvhdmi_basic_init_7x_2ch[] = { |
| /* set audio protect on */ |
| { 0x1, Nv_VERB_SET_Audio_Protection_On, 0x1}, |
| /* enable digital output on pin widget */ |
| { 0x5, AC_VERB_SET_PIN_WIDGET_CONTROL, PIN_OUT | 0x5 }, |
| {} /* terminator */ |
| }; |
| |
| static const struct hda_verb nvhdmi_basic_init_7x_8ch[] = { |
| /* set audio protect on */ |
| { 0x1, Nv_VERB_SET_Audio_Protection_On, 0x1}, |
| /* enable digital output on pin widget */ |
| { 0x5, AC_VERB_SET_PIN_WIDGET_CONTROL, PIN_OUT | 0x5 }, |
| { 0x7, AC_VERB_SET_PIN_WIDGET_CONTROL, PIN_OUT | 0x5 }, |
| { 0x9, AC_VERB_SET_PIN_WIDGET_CONTROL, PIN_OUT | 0x5 }, |
| { 0xb, AC_VERB_SET_PIN_WIDGET_CONTROL, PIN_OUT | 0x5 }, |
| { 0xd, AC_VERB_SET_PIN_WIDGET_CONTROL, PIN_OUT | 0x5 }, |
| {} /* terminator */ |
| }; |
| |
| static int nvhdmi_mcp_init(struct hda_codec *codec) |
| { |
| struct hdmi_spec *spec = codec->spec; |
| |
| if (spec->multiout.max_channels == 2) |
| snd_hda_sequence_write(codec, nvhdmi_basic_init_7x_2ch); |
| else |
| snd_hda_sequence_write(codec, nvhdmi_basic_init_7x_8ch); |
| return 0; |
| } |
| |
| static void nvhdmi_8ch_7x_set_info_frame_parameters(struct hda_codec *codec, |
| int channels) |
| { |
| unsigned int chanmask; |
| int chan = channels ? (channels - 1) : 1; |
| |
| switch (channels) { |
| default: |
| case 0: |
| case 2: |
| chanmask = 0x00; |
| break; |
| case 4: |
| chanmask = 0x08; |
| break; |
| case 6: |
| chanmask = 0x0b; |
| break; |
| case 8: |
| chanmask = 0x13; |
| break; |
| } |
| |
| /* Set the audio infoframe channel allocation and checksum fields. The |
| * channel count is computed implicitly by the hardware. |
| */ |
| snd_hda_codec_write(codec, 0x1, 0, |
| Nv_VERB_SET_Channel_Allocation, chanmask); |
| |
| snd_hda_codec_write(codec, 0x1, 0, |
| Nv_VERB_SET_Info_Frame_Checksum, |
| (0x71 - chan - chanmask)); |
| } |
| |
| static int nvhdmi_8ch_7x_pcm_close(struct hda_pcm_stream *hinfo, |
| struct hda_codec *codec, |
| struct snd_pcm_substream *substream) |
| { |
| struct hdmi_spec *spec = codec->spec; |
| int i; |
| |
| snd_hda_codec_write(codec, nvhdmi_master_con_nid_7x, |
| 0, AC_VERB_SET_CHANNEL_STREAMID, 0); |
| for (i = 0; i < 4; i++) { |
| /* set the stream id */ |
| snd_hda_codec_write(codec, nvhdmi_con_nids_7x[i], 0, |
| AC_VERB_SET_CHANNEL_STREAMID, 0); |
| /* set the stream format */ |
| snd_hda_codec_write(codec, nvhdmi_con_nids_7x[i], 0, |
| AC_VERB_SET_STREAM_FORMAT, 0); |
| } |
| |
| /* The audio hardware sends a channel count of 0x7 (8ch) when all the |
| * streams are disabled. |
| */ |
| nvhdmi_8ch_7x_set_info_frame_parameters(codec, 8); |
| |
| return snd_hda_multi_out_dig_close(codec, &spec->multiout); |
| } |
| |
| static int nvhdmi_8ch_7x_pcm_prepare(struct hda_pcm_stream *hinfo, |
| struct hda_codec *codec, |
| unsigned int stream_tag, |
| unsigned int format, |
| struct snd_pcm_substream *substream) |
| { |
| int chs; |
| unsigned int dataDCC2, channel_id; |
| int i; |
| struct hdmi_spec *spec = codec->spec; |
| struct hda_spdif_out *spdif; |
| struct hdmi_spec_per_cvt *per_cvt; |
| |
| guard(mutex)(&codec->spdif_mutex); |
| per_cvt = get_cvt(spec, 0); |
| spdif = snd_hda_spdif_out_of_nid(codec, per_cvt->cvt_nid); |
| |
| chs = substream->runtime->channels; |
| |
| dataDCC2 = 0x2; |
| |
| /* turn off SPDIF once; otherwise the IEC958 bits won't be updated */ |
| if (codec->spdif_status_reset && (spdif->ctls & AC_DIG1_ENABLE)) |
| snd_hda_codec_write(codec, |
| nvhdmi_master_con_nid_7x, |
| 0, |
| AC_VERB_SET_DIGI_CONVERT_1, |
| spdif->ctls & ~AC_DIG1_ENABLE & 0xff); |
| |
| /* set the stream id */ |
| snd_hda_codec_write(codec, nvhdmi_master_con_nid_7x, 0, |
| AC_VERB_SET_CHANNEL_STREAMID, (stream_tag << 4) | 0x0); |
| |
| /* set the stream format */ |
| snd_hda_codec_write(codec, nvhdmi_master_con_nid_7x, 0, |
| AC_VERB_SET_STREAM_FORMAT, format); |
| |
| /* turn on again (if needed) */ |
| /* enable and set the channel status audio/data flag */ |
| if (codec->spdif_status_reset && (spdif->ctls & AC_DIG1_ENABLE)) { |
| snd_hda_codec_write(codec, |
| nvhdmi_master_con_nid_7x, |
| 0, |
| AC_VERB_SET_DIGI_CONVERT_1, |
| spdif->ctls & 0xff); |
| snd_hda_codec_write(codec, |
| nvhdmi_master_con_nid_7x, |
| 0, |
| AC_VERB_SET_DIGI_CONVERT_2, dataDCC2); |
| } |
| |
| for (i = 0; i < 4; i++) { |
| if (chs == 2) |
| channel_id = 0; |
| else |
| channel_id = i * 2; |
| |
| /* turn off SPDIF once; |
| *otherwise the IEC958 bits won't be updated |
| */ |
| if (codec->spdif_status_reset && |
| (spdif->ctls & AC_DIG1_ENABLE)) |
| snd_hda_codec_write(codec, |
| nvhdmi_con_nids_7x[i], |
| 0, |
| AC_VERB_SET_DIGI_CONVERT_1, |
| spdif->ctls & ~AC_DIG1_ENABLE & 0xff); |
| /* set the stream id */ |
| snd_hda_codec_write(codec, |
| nvhdmi_con_nids_7x[i], |
| 0, |
| AC_VERB_SET_CHANNEL_STREAMID, |
| (stream_tag << 4) | channel_id); |
| /* set the stream format */ |
| snd_hda_codec_write(codec, |
| nvhdmi_con_nids_7x[i], |
| 0, |
| AC_VERB_SET_STREAM_FORMAT, |
| format); |
| /* turn on again (if needed) */ |
| /* enable and set the channel status audio/data flag */ |
| if (codec->spdif_status_reset && |
| (spdif->ctls & AC_DIG1_ENABLE)) { |
| snd_hda_codec_write(codec, |
| nvhdmi_con_nids_7x[i], |
| 0, |
| AC_VERB_SET_DIGI_CONVERT_1, |
| spdif->ctls & 0xff); |
| snd_hda_codec_write(codec, |
| nvhdmi_con_nids_7x[i], |
| 0, |
| AC_VERB_SET_DIGI_CONVERT_2, dataDCC2); |
| } |
| } |
| |
| nvhdmi_8ch_7x_set_info_frame_parameters(codec, chs); |
| |
| return 0; |
| } |
| |
| static const struct hda_pcm_stream nvhdmi_pcm_playback_8ch_7x = { |
| .substreams = 1, |
| .channels_min = 2, |
| .channels_max = 8, |
| .nid = nvhdmi_master_con_nid_7x, |
| .rates = SUPPORTED_RATES, |
| .maxbps = SUPPORTED_MAXBPS, |
| .formats = SUPPORTED_FORMATS, |
| .ops = { |
| .open = snd_hda_hdmi_simple_pcm_open, |
| .close = nvhdmi_8ch_7x_pcm_close, |
| .prepare = nvhdmi_8ch_7x_pcm_prepare |
| }, |
| }; |
| |
| static int nvhdmi_mcp_build_pcms(struct hda_codec *codec) |
| { |
| struct hdmi_spec *spec = codec->spec; |
| int err; |
| |
| err = snd_hda_hdmi_simple_build_pcms(codec); |
| if (!err && spec->multiout.max_channels == 8) { |
| struct hda_pcm *info = get_pcm_rec(spec, 0); |
| |
| info->own_chmap = true; |
| } |
| return err; |
| } |
| |
| static int nvhdmi_mcp_build_controls(struct hda_codec *codec) |
| { |
| struct hdmi_spec *spec = codec->spec; |
| struct hda_pcm *info; |
| struct snd_pcm_chmap *chmap; |
| int err; |
| |
| err = snd_hda_hdmi_simple_build_controls(codec); |
| if (err < 0) |
| return err; |
| |
| if (spec->multiout.max_channels != 8) |
| return 0; |
| |
| /* add channel maps */ |
| info = get_pcm_rec(spec, 0); |
| err = snd_pcm_add_chmap_ctls(info->pcm, |
| SNDRV_PCM_STREAM_PLAYBACK, |
| snd_pcm_alt_chmaps, 8, 0, &chmap); |
| if (err < 0) |
| return err; |
| switch (codec->preset->vendor_id) { |
| case 0x10de0002: |
| case 0x10de0003: |
| case 0x10de0005: |
| case 0x10de0006: |
| chmap->channel_mask = (1U << 2) | (1U << 8); |
| break; |
| case 0x10de0007: |
| chmap->channel_mask = (1U << 2) | (1U << 6) | (1U << 8); |
| } |
| return 0; |
| } |
| |
| static const unsigned int channels_2_6_8[] = { |
| 2, 6, 8 |
| }; |
| |
| static const unsigned int channels_2_8[] = { |
| 2, 8 |
| }; |
| |
| static const struct snd_pcm_hw_constraint_list hw_constraints_2_6_8_channels = { |
| .count = ARRAY_SIZE(channels_2_6_8), |
| .list = channels_2_6_8, |
| .mask = 0, |
| }; |
| |
| static const struct snd_pcm_hw_constraint_list hw_constraints_2_8_channels = { |
| .count = ARRAY_SIZE(channels_2_8), |
| .list = channels_2_8, |
| .mask = 0, |
| }; |
| |
| static int nvhdmi_mcp_probe(struct hda_codec *codec, |
| const struct hda_device_id *id) |
| { |
| struct hdmi_spec *spec; |
| int err; |
| |
| err = snd_hda_hdmi_simple_probe(codec, nvhdmi_master_con_nid_7x, |
| nvhdmi_master_pin_nid_7x); |
| if (err < 0) |
| return err; |
| |
| /* override the PCM rates, etc, as the codec doesn't give full list */ |
| spec = codec->spec; |
| spec->pcm_playback.rates = SUPPORTED_RATES; |
| spec->pcm_playback.maxbps = SUPPORTED_MAXBPS; |
| spec->pcm_playback.formats = SUPPORTED_FORMATS; |
| spec->nv_dp_workaround = true; |
| |
| if (id->driver_data == MODEL_2CH) |
| return 0; |
| |
| spec->multiout.max_channels = 8; |
| spec->pcm_playback = nvhdmi_pcm_playback_8ch_7x; |
| |
| switch (codec->preset->vendor_id) { |
| case 0x10de0002: |
| case 0x10de0003: |
| case 0x10de0005: |
| case 0x10de0006: |
| spec->hw_constraints_channels = &hw_constraints_2_8_channels; |
| break; |
| case 0x10de0007: |
| spec->hw_constraints_channels = &hw_constraints_2_6_8_channels; |
| break; |
| default: |
| break; |
| } |
| |
| /* Initialize the audio infoframe channel mask and checksum to something |
| * valid |
| */ |
| nvhdmi_8ch_7x_set_info_frame_parameters(codec, 8); |
| |
| return 0; |
| } |
| |
| static const struct hda_codec_ops nvhdmi_mcp_codec_ops = { |
| .probe = nvhdmi_mcp_probe, |
| .remove = snd_hda_hdmi_simple_remove, |
| .build_pcms = nvhdmi_mcp_build_pcms, |
| .build_controls = nvhdmi_mcp_build_controls, |
| .init = nvhdmi_mcp_init, |
| .unsol_event = snd_hda_hdmi_simple_unsol_event, |
| }; |
| |
| static const struct hda_device_id snd_hda_id_nvhdmi_mcp[] = { |
| HDA_CODEC_ID_MODEL(0x10de0001, "MCP73 HDMI", MODEL_2CH), |
| HDA_CODEC_ID_MODEL(0x10de0002, "MCP77/78 HDMI", MODEL_8CH), |
| HDA_CODEC_ID_MODEL(0x10de0003, "MCP77/78 HDMI", MODEL_8CH), |
| HDA_CODEC_ID_MODEL(0x10de0004, "GPU 04 HDMI", MODEL_8CH), |
| HDA_CODEC_ID_MODEL(0x10de0005, "MCP77/78 HDMI", MODEL_8CH), |
| HDA_CODEC_ID_MODEL(0x10de0006, "MCP77/78 HDMI", MODEL_8CH), |
| HDA_CODEC_ID_MODEL(0x10de0007, "MCP79/7A HDMI", MODEL_8CH), |
| HDA_CODEC_ID_MODEL(0x10de0067, "MCP67 HDMI", MODEL_2CH), |
| HDA_CODEC_ID_MODEL(0x10de8001, "MCP73 HDMI", MODEL_2CH), |
| HDA_CODEC_ID_MODEL(0x10de8067, "MCP67/68 HDMI", MODEL_2CH), |
| {} /* terminator */ |
| }; |
| MODULE_DEVICE_TABLE(hdaudio, snd_hda_id_nvhdmi_mcp); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_DESCRIPTION("Legacy Nvidia HDMI HD-audio codec"); |
| MODULE_IMPORT_NS("SND_HDA_CODEC_HDMI"); |
| |
| static struct hda_codec_driver nvhdmi_mcp_driver = { |
| .id = snd_hda_id_nvhdmi_mcp, |
| .ops = &nvhdmi_mcp_codec_ops, |
| }; |
| |
| module_hda_codec_driver(nvhdmi_mcp_driver); |