diff --git a/arch/arm64/boot/dts/qcom/kodiak.dtsi b/arch/arm64/boot/dts/qcom/kodiak.dtsi index d201da6720bcd2..d28084ff8607a5 100644 --- a/arch/arm64/boot/dts/qcom/kodiak.dtsi +++ b/arch/arm64/boot/dts/qcom/kodiak.dtsi @@ -4550,6 +4550,35 @@ #sound-dai-cells = <0>; }; }; + + q6mvm: apr-service@9 { + compatible = "qcom,q6mvm"; + reg = ; + qcom,protection-domain = "avs/audio", "msm/adsp/audio_pd"; + status = "disabled"; + + q6voicedai: dais { + compatible = "qcom,q6voice-dais"; + #address-cells = <1>; + #size-cells = <0>; + #sound-dai-cells = <1>; + }; + }; + + q6cvs: apr-service@a { + compatible = "qcom,q6cvs"; + reg = ; + qcom,protection-domain = "avs/audio", "msm/adsp/audio_pd"; + status = "disabled"; + }; + + q6cvp: apr-service@b { + compatible = "qcom,q6cvp"; + reg = ; + qcom,protection-domain = "avs/audio", "msm/adsp/audio_pd"; + status = "disabled"; + }; + }; fastrpc { diff --git a/arch/arm64/boot/dts/qcom/qcm6490-fairphone-fp5.dts b/arch/arm64/boot/dts/qcom/qcm6490-fairphone-fp5.dts index 29d6265a123d35..d48cf38b768a0b 100644 --- a/arch/arm64/boot/dts/qcom/qcm6490-fairphone-fp5.dts +++ b/arch/arm64/boot/dts/qcom/qcm6490-fairphone-fp5.dts @@ -17,6 +17,8 @@ #include #include #include +#include + #include "kodiak.dtsi" #include "pm7250b.dtsi" #include "pm7325.dtsi" @@ -1298,7 +1300,38 @@ status = "okay"; }; +&q6cvp { + status = "okay"; +}; + +&q6cvs { + status = "okay"; +}; + +&q6mvm { + status = "okay"; +}; + + +&q6voicedai { + dai@1 { + reg = ; + }; +}; + &q6afedai { + dai@104 { + reg = ; + }; + + dai@113 { + reg = ; + }; + + dai@120 { + reg = ; + }; + dai@127 { reg = ; qcom,sd-lines = <0>; @@ -1483,6 +1516,14 @@ sound-dai = <&q6routing>; }; }; + + voicemmode1-dai-link { + link-name = "VoiceMMode1"; + cpu { + sound-dai = <&q6voicedai VOICEMMODE1>; + }; + }; + }; &spi13 { diff --git a/arch/arm64/configs/fp5_defconfig b/arch/arm64/configs/fp5_defconfig index 03c1dd5d7c3981..aac647d82c392e 100644 --- a/arch/arm64/configs/fp5_defconfig +++ b/arch/arm64/configs/fp5_defconfig @@ -373,6 +373,7 @@ CONFIG_SND_SOC=y CONFIG_SND_SOC_USB=y CONFIG_SND_SOC_QCOM=y CONFIG_SND_SOC_QDSP6_USB=y +CONFIG_SND_SOC_QDSP6_Q6VOICE=y CONFIG_SND_SOC_SM8250=y CONFIG_SND_SOC_AW88261=y CONFIG_SND_SOC_WCD938X_SDW=y diff --git a/include/dt-bindings/sound/qcom,q6voice.h b/include/dt-bindings/sound/qcom,q6voice.h new file mode 100644 index 00000000000000..825bf7d47fe316 --- /dev/null +++ b/include/dt-bindings/sound/qcom,q6voice.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __DT_BINDINGS_Q6_VOICE_H__ +#define __DT_BINDINGS_Q6_VOICE_H__ + +#define CS_VOICE 0 +#define VOICEMMODE1 1 + +#endif /* __DT_BINDINGS_Q6_VOICE_H__ */ diff --git a/sound/soc/codecs/aw88261.c b/sound/soc/codecs/aw88261.c index b14ca416304324..b3302522f1b2dd 100644 --- a/sound/soc/codecs/aw88261.c +++ b/sound/soc/codecs/aw88261.c @@ -417,17 +417,7 @@ static int aw88261_dev_reg_update(struct aw88261 *aw88261, break; } - ret = aw88261_dev_set_vcalb(aw_dev); - if (ret) - return ret; - - if (aw_dev->prof_cur != aw_dev->prof_index) - vol_desc->ctl_volume = 0; - - /* keep min volume */ - aw88261_dev_set_volume(aw_dev, vol_desc->mute_volume); - - return ret; + return aw88261_dev_set_vcalb(aw_dev); } static int aw88261_dev_get_prof_name(struct aw_device *aw_dev, int index, char **prof_name) diff --git a/sound/soc/qcom/Kconfig b/sound/soc/qcom/Kconfig index e6e24f3b99222d..63a7ef1887d93e 100644 --- a/sound/soc/qcom/Kconfig +++ b/sound/soc/qcom/Kconfig @@ -156,6 +156,14 @@ config SND_SOC_QDSP6 audio drivers. This includes q6asm, q6adm, q6afe interfaces to DSP using apr. +config SND_SOC_QDSP6_Q6VOICE_DAI + tristate + +config SND_SOC_QDSP6_Q6VOICE + tristate "QDSP6 Q6Voice audio driver" + depends on SND_SOC_QDSP6 + select SND_SOC_QDSP6_Q6VOICE_DAI + config SND_SOC_MSM8996 tristate "SoC Machine driver for MSM8996 and APQ8096 boards" depends on QCOM_APR diff --git a/sound/soc/qcom/qdsp6/Makefile b/sound/soc/qcom/qdsp6/Makefile index 67267304e7e920..940b9ec19694ef 100644 --- a/sound/soc/qcom/qdsp6/Makefile +++ b/sound/soc/qcom/qdsp6/Makefile @@ -11,6 +11,9 @@ obj-$(CONFIG_SND_SOC_QDSP6_ADM) += q6adm.o obj-$(CONFIG_SND_SOC_QDSP6_ROUTING) += q6routing.o obj-$(CONFIG_SND_SOC_QDSP6_ASM) += q6asm.o obj-$(CONFIG_SND_SOC_QDSP6_ASM_DAI) += q6asm-dai.o +obj-$(CONFIG_SND_SOC_QDSP6_Q6VOICE) += q6cvp.o q6cvs.o q6mvm.o q6voice-common.o +obj-$(CONFIG_SND_SOC_QDSP6_Q6VOICE) += q6voice.o +obj-$(CONFIG_SND_SOC_QDSP6_Q6VOICE_DAI) += q6voice-dai.o obj-$(CONFIG_SND_SOC_QDSP6_APM) += snd-q6apm.o obj-$(CONFIG_SND_SOC_QDSP6_APM_DAI) += q6apm-dai.o diff --git a/sound/soc/qcom/qdsp6/q6afe-dai.c b/sound/soc/qcom/qdsp6/q6afe-dai.c index a0d21034a6264d..354c1d9896244f 100644 --- a/sound/soc/qcom/qdsp6/q6afe-dai.c +++ b/sound/soc/qcom/qdsp6/q6afe-dai.c @@ -278,6 +278,7 @@ static int q6tdm_hw_params(struct snd_pcm_substream *substream, tdm->data_align_type = dai_data->priv[dai->id].data_align; tdm->sync_src = dai_data->priv[dai->id].sync_src; tdm->sync_mode = dai_data->priv[dai->id].sync_mode; + tdm->data_delay = dai_data->priv[dai->id].data_delay; return 0; } diff --git a/sound/soc/qcom/qdsp6/q6afe.c b/sound/soc/qcom/qdsp6/q6afe.c index 28b5b6b91897f5..109b189d667551 100644 --- a/sound/soc/qcom/qdsp6/q6afe.c +++ b/sound/soc/qcom/qdsp6/q6afe.c @@ -1366,6 +1366,7 @@ void q6afe_tdm_port_prepare(struct q6afe_port *port, pcfg->tdm_cfg.sync_mode = cfg->sync_mode; pcfg->tdm_cfg.sync_src = cfg->sync_src; pcfg->tdm_cfg.nslots_per_frame = cfg->nslots_per_frame; + pcfg->tdm_cfg.ctrl_sync_data_delay = cfg->data_delay; pcfg->tdm_cfg.slot_width = cfg->slot_width; pcfg->tdm_cfg.slot_mask = cfg->slot_mask; diff --git a/sound/soc/qcom/qdsp6/q6afe.h b/sound/soc/qcom/qdsp6/q6afe.h index 0b8c3ec1315caf..657973ca0be6e7 100644 --- a/sound/soc/qcom/qdsp6/q6afe.h +++ b/sound/soc/qcom/qdsp6/q6afe.h @@ -190,6 +190,7 @@ struct q6afe_tdm_cfg { u16 sync_mode; u16 sync_src; u16 nslots_per_frame; + u16 data_delay; u16 slot_width; u16 slot_mask; u32 data_align_type; diff --git a/sound/soc/qcom/qdsp6/q6core.c b/sound/soc/qcom/qdsp6/q6core.c index 48825756a7abb5..dd55a1f94894fb 100644 --- a/sound/soc/qcom/qdsp6/q6core.c +++ b/sound/soc/qcom/qdsp6/q6core.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "q6core.h" #include "q6dsp-errno.h" @@ -20,9 +21,17 @@ #define AVCS_CMDRSP_ADSP_EVENT_GET_STATE 0x0001290D #define AVCS_GET_VERSIONS 0x00012905 #define AVCS_GET_VERSIONS_RSP 0x00012906 +#define AVCS_CMD_REGISTER_TOPOLOGIES 0x00012923 +#define AVCS_CMD_DEREGISTER_TOPOLOGIES 0x0001292a #define AVCS_CMD_GET_FWK_VERSION 0x001292c #define AVCS_CMDRSP_GET_FWK_VERSION 0x001292d +#define AVCS_MODE_DEREGISTER_ALL_CUSTOM_TOPOLOGIES 2 + +#define ACDB_SECTION_GPROPLUT 0x54554c504f525047 +#define ACDB_SECTION_DATAPOOL 0x4c4f4f5041544144 +#define ACDB_TOPOLOGIES_BLOB 0x000131a7 + struct avcs_svc_info { uint32_t service_id; uint32_t version; @@ -41,6 +50,13 @@ struct avcs_svc_api_info { uint32_t api_branch_version; } __packed; +struct avcs_cmd_register_topologies { + uint32_t payload_addr_lsw; + uint32_t payload_addr_msw; + uint32_t mem_map_handle; + uint32_t payload_size; +} __packed; + struct avcs_cmdrsp_get_fwk_version { uint32_t build_major_version; uint32_t build_minor_version; @@ -236,6 +252,38 @@ static bool __q6core_is_adsp_ready(struct q6core *core) return false; } +static int q6core_register_topologies(struct device *dev, u32 size, const void *data) +{ + struct q6core *core = dev_get_drvdata(dev); + struct apr_device *adev = core->adev; + struct avcs_cmd_register_topologies *cmd; + struct apr_pkt *pkt; + void *p, *dest; + int pkt_size; + + pkt_size = APR_HDR_SIZE + sizeof(*cmd) + size; + + p = kzalloc(pkt_size, GFP_KERNEL); + if (!p) + return -ENOMEM; + + pkt = p; + cmd = p + APR_HDR_SIZE; + dest = p + APR_HDR_SIZE + sizeof(*cmd); + if (!p) + return -ENOMEM; + + pkt->hdr.hdr_field = APR_HDR_FIELD(APR_MSG_TYPE_SEQ_CMD, + APR_HDR_LEN(APR_HDR_SIZE), APR_PKT_VER); + pkt->hdr.pkt_size = APR_HDR_SIZE + sizeof(*cmd) + size; + pkt->hdr.opcode = AVCS_CMD_REGISTER_TOPOLOGIES; + cmd->payload_size = size; + + memcpy(dest, data, size); + + return apr_send_pkt(adev, pkt); +} + /** * q6core_get_svc_api_info() - Get version number of a service. * @@ -325,6 +373,103 @@ bool q6core_is_adsp_ready(void) } EXPORT_SYMBOL_GPL(q6core_is_adsp_ready); +static int acdb_section_offset(const struct firmware *fw, + u64 section, u32 *off_out) +{ + const u8 *data = fw->data + 32; + u32 off = 32; + u32 sect_size; + u64 sect_id; + + while (off + 12 <= fw->size) { + sect_id = *(const u64 *) data; + sect_size = *(const u32 *) (data + 8); + + if (off + 12 + sect_size > fw->size) + return -EINVAL; + + if (sect_id == section) { + *off_out = off; + return 0; + } + + off += 12 + sect_size; + data += 12 + sect_size; + } + + return -ENOENT; +} + +static int acdb_get_global_property(const struct firmware *fw, + u32 prop_id, u32 *offset) +{ + u32 gproplut_off, datapool_off; + u32 lut_size, lut_nelts, i; + const u32 *data; + int ret; + + ret = acdb_section_offset(fw, ACDB_SECTION_GPROPLUT, &gproplut_off); + if (ret) + return ret; + + ret = acdb_section_offset(fw, ACDB_SECTION_DATAPOOL, &datapool_off); + if (ret) + return ret; + + lut_size = *(const u32 *) (fw->data + gproplut_off + 8); + if (lut_size < 4) + return -EINVAL; + + lut_nelts = *(const u32 *) (fw->data + gproplut_off + 12); + if (lut_size < 4 + 8 * lut_nelts) + return -EINVAL; + + data = (const u32 *) (fw->data + gproplut_off + 16); + for (i = 0; i < lut_nelts; i++) { + if (datapool_off + 12 + data[i * 2 + 1] + 4 > fw->size) + return -EINVAL; + + if (data[i * 2] == prop_id) { + *offset = datapool_off + 12 + data[i * 2 + 1]; + return 0; + } + } + + return -ENOENT; +} + +static int q6core_load_and_register_topologies(struct device *dev) +{ + const struct device_node *np = dev_of_node(dev); + const char *fw_name; + const struct firmware *fw; + u32 topo_off, size; + int ret; + + ret = of_property_read_string(np, "qcom,acdb-name", &fw_name); + if (ret) + return 0; + + ret = request_firmware(&fw, fw_name, dev); + if (ret) + return dev_err_probe(dev, ret, "failed to load Global_cal.acdb"); + + ret = acdb_get_global_property(fw, ACDB_TOPOLOGIES_BLOB, &topo_off); + if (ret) { + dev_err(dev, "ACDB parse error: %d\n", ret); + goto release_fw; + } + + size = *(const u32 *) (fw->data + topo_off); + ret = q6core_register_topologies(dev, size, fw->data + topo_off + 4); + if (ret < 0) + goto release_fw; + +release_fw: + release_firmware(fw); + return 0; +} + static int q6core_probe(struct apr_device *adev) { g_core = kzalloc_obj(*g_core); @@ -336,7 +481,8 @@ static int q6core_probe(struct apr_device *adev) mutex_init(&g_core->lock); g_core->adev = adev; init_waitqueue_head(&g_core->wait); - return 0; + + return q6core_load_and_register_topologies(&adev->dev); } static void q6core_exit(struct apr_device *adev) diff --git a/sound/soc/qcom/qdsp6/q6cvp.c b/sound/soc/qcom/qdsp6/q6cvp.c new file mode 100644 index 00000000000000..d87849b6dc992f --- /dev/null +++ b/sound/soc/qcom/qdsp6/q6cvp.c @@ -0,0 +1,331 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2012-2017, The Linux Foundation. All rights reserved. +// Copyright (c) 2020, Stephan Gerhold + +#include +#include +#include +#include "q6cvp.h" +#include "q6dsp-common.h" +#include "q6voice-common.h" + +#define VSS_IVOCPROC_DIRECTION_RX 0 +#define VSS_IVOCPROC_DIRECTION_TX 1 +#define VSS_IVOCPROC_DIRECTION_RX_TX 2 + +#define VSS_IVOCPROC_PORT_ID_NONE 0xFFFF + +#define VSS_IVOCPROC_TOPOLOGY_ID_NONE 0x00010F70 +#define VSS_IVOCPROC_TOPOLOGY_ID_TX_SM_ECNS 0x00010F71 +#define VSS_IVOCPROC_TOPOLOGY_ID_TX_DM_FLUENCE 0x00010F72 + +#define VSS_IVOCPROC_TOPOLOGY_ID_RX_DEFAULT 0x00010F77 + +#define VSS_IVOCPROC_VOCPROC_MODE_EC_INT_MIXING 0x00010F7C +#define VSS_IVOCPROC_VOCPROC_MODE_EC_EXT_MIXING 0x00010F7D + +#define VSS_ICOMMON_CAL_NETWORK_ID_NONE 0x0001135E + +#define VSS_IVOCPROC_CMD_ENABLE 0x000100C6 +#define VSS_IVOCPROC_CMD_DISABLE 0x000110E1 + +#define VSS_IVOCPROC_CMD_CREATE_FULL_CONTROL_SESSION_V2 0x000112BF +#define VSS_IVOCPROC_CMD_CREATE_FULL_CONTROL_SESSION_V3 0x00013169 + +#define VSS_IVOCPROC_CMD_TOPOLOGY_COMMIT 0x00013198 + +#define VSS_ICOMMON_CMD_SET_PARAM_V2 0x0001133D + +#define VSS_MODULE_CVD_GENERIC 0x0001316E + +#define VSS_PARAM_VOCPROC_RX_CHANNEL_INFO 0x0001328F +#define VSS_PARAM_VOCPROC_TX_CHANNEL_INFO 0x0001328E +#define VSS_PARAM_RX_PORT_ENDPOINT_MEDIA_INFO 0x00013254 +#define VSS_PARAM_TX_PORT_ENDPOINT_MEDIA_INFO 0x00013253 + +#define VOC_SET_MEDIA_FORMAT_PARAM_TOKEN 2 + +#define VSS_NUM_CHANNELS_MAX 8 + +struct vss_ivocproc_cmd_create_full_control_session_v2_cmd { + struct apr_hdr hdr; + + /* + * Vocproc direction. The supported values: + * VSS_IVOCPROC_DIRECTION_RX + * VSS_IVOCPROC_DIRECTION_TX + * VSS_IVOCPROC_DIRECTION_RX_TX + */ + u16 direction; + + /* + * Tx device port ID to which the vocproc connects. If a port ID is + * not being supplied, set this to #VSS_IVOCPROC_PORT_ID_NONE. + */ + u16 tx_port_id; + + /* + * Tx path topology ID. If a topology ID is not being supplied, set + * this to #VSS_IVOCPROC_TOPOLOGY_ID_NONE. + */ + u32 tx_topology_id; + + /* + * Rx device port ID to which the vocproc connects. If a port ID is + * not being supplied, set this to #VSS_IVOCPROC_PORT_ID_NONE. + */ + u16 rx_port_id; + + /* + * Rx path topology ID. If a topology ID is not being supplied, set + * this to #VSS_IVOCPROC_TOPOLOGY_ID_NONE. + */ + u32 rx_topology_id; + + /* Voice calibration profile ID. */ + u32 profile_id; + + /* + * Vocproc mode. The supported values: + * VSS_IVOCPROC_VOCPROC_MODE_EC_INT_MIXING + * VSS_IVOCPROC_VOCPROC_MODE_EC_EXT_MIXING + */ + u32 vocproc_mode; + + /* + * Port ID to which the vocproc connects for receiving echo + * cancellation reference signal. If a port ID is not being supplied, + * set this to #VSS_IVOCPROC_PORT_ID_NONE. This parameter value is + * ignored when the vocproc_mode parameter is set to + * VSS_IVOCPROC_VOCPROC_MODE_EC_INT_MIXING. + */ + u16 ec_ref_port_id; + + /* + * Session name string used to identify a session that can be shared + * with passive controllers (optional). + */ + char name[20]; +} __packed; + +struct vss_param_endpoint_media_format_info { + u32 port_id; + u16 num_channels; + u16 bits_per_sample; + u32 sample_rate; + u8 channel_mapping[VSS_NUM_CHANNELS_MAX]; +} __packed; + +struct vss_param_vocproc_dev_channel_info { + u32 num_channels; + u32 bits_per_sample; + u8 channel_mapping[VSS_NUM_CHANNELS_MAX]; +} __packed; + +struct vss_icommon_param_data { + u32 module_id; + u32 param_id; + u16 param_size; + u16 reserved; + + union { + struct vss_param_endpoint_media_format_info media_format_info; + struct vss_param_vocproc_dev_channel_info channel_info; + }; +} __packed; + +struct vss_icommon_cmd_set_param_v2_cmd { + struct apr_hdr hdr; + + /* + * Pointer to the unique identifier for an address. What type of + * address? DMA? Device-specific memory? Please direct inquiries of + * this type to the SoC manufacturer. + * + * Anyway, the most important information for this parameter is that + * this field is set to zero if the data is in-band, i.e. in the same + * packet. + */ + u32 mem_handle; + + /* + * This field is ignored if you are only paying attention to this + * documentation of the fields. Otherwise, refer to your own + * documentation. I did not ask for this, and will not fully document + * it if its accuracy cannot be measured. + */ + u64 mem_address; + + /* + * Just set this to the size of the parameter data. + */ + u32 mem_size; + + struct vss_icommon_param_data param_data; +} __packed; + +struct q6voice_session *q6cvp_session_create(enum q6voice_path_type path, + u16 tx_port, u16 rx_port, + u32 tx_topo, u32 rx_topo) +{ + struct vss_ivocproc_cmd_create_full_control_session_v2_cmd cmd; + + cmd.hdr.pkt_size = sizeof(cmd); + cmd.hdr.opcode = VSS_IVOCPROC_CMD_CREATE_FULL_CONTROL_SESSION_V2; + + cmd.tx_topology_id = tx_topo; + cmd.rx_topology_id = rx_topo; + + cmd.direction = VSS_IVOCPROC_DIRECTION_RX_TX; + cmd.tx_port_id = tx_port; + cmd.rx_port_id = rx_port; + cmd.profile_id = VSS_ICOMMON_CAL_NETWORK_ID_NONE; + cmd.vocproc_mode = VSS_IVOCPROC_VOCPROC_MODE_EC_INT_MIXING; + cmd.ec_ref_port_id = VSS_IVOCPROC_PORT_ID_NONE; + + return q6voice_session_create(Q6VOICE_SERVICE_CVP, path, &cmd.hdr); +} +EXPORT_SYMBOL_GPL(q6cvp_session_create); + +struct q6voice_session *q6cvp_session_create_v3(enum q6voice_path_type path, + u16 tx_port, u16 rx_port, + u32 tx_topo, u32 rx_topo) +{ + /* + * According to downstream, the v3 parameters are the exact same, with + * the only difference being the opcode. + */ + struct vss_ivocproc_cmd_create_full_control_session_v2_cmd cmd; + + cmd.hdr.pkt_size = sizeof(cmd); + cmd.hdr.opcode = VSS_IVOCPROC_CMD_CREATE_FULL_CONTROL_SESSION_V3; + + cmd.tx_topology_id = tx_topo; + cmd.rx_topology_id = rx_topo; + + cmd.direction = VSS_IVOCPROC_DIRECTION_RX_TX; + cmd.tx_port_id = tx_port; + cmd.rx_port_id = rx_port; + cmd.profile_id = VSS_ICOMMON_CAL_NETWORK_ID_NONE; + cmd.vocproc_mode = VSS_IVOCPROC_VOCPROC_MODE_EC_INT_MIXING; + cmd.ec_ref_port_id = VSS_IVOCPROC_PORT_ID_NONE; + + return q6voice_session_create(Q6VOICE_SERVICE_CVP, path, &cmd.hdr); +} +EXPORT_SYMBOL_GPL(q6cvp_session_create_v3); + +int q6cvp_send_channel_info(struct q6voice_session *cvp, bool is_tx) +{ + struct vss_icommon_cmd_set_param_v2_cmd cmd; + + cmd.hdr.pkt_size = offsetof(struct vss_icommon_cmd_set_param_v2_cmd, param_data.channel_info) + + sizeof(struct vss_param_vocproc_dev_channel_info); + cmd.hdr.opcode = VSS_ICOMMON_CMD_SET_PARAM_V2; + + cmd.mem_handle = 0; + cmd.mem_size = offsetof(struct vss_icommon_param_data, channel_info) + + sizeof(struct vss_param_vocproc_dev_channel_info); + + cmd.param_data.module_id = VSS_MODULE_CVD_GENERIC; + cmd.param_data.param_id = is_tx ? VSS_PARAM_VOCPROC_TX_CHANNEL_INFO + : VSS_PARAM_VOCPROC_RX_CHANNEL_INFO; + cmd.param_data.param_size = sizeof(struct vss_param_vocproc_dev_channel_info); + cmd.param_data.reserved = 0; + + if (is_tx) + cmd.param_data.channel_info.num_channels = 1; + else + cmd.param_data.channel_info.num_channels = 2; + + cmd.param_data.channel_info.bits_per_sample = 16; + + q6dsp_map_channels(cmd.param_data.channel_info.channel_mapping, + cmd.param_data.channel_info.num_channels); + + return q6voice_common_send(cvp, &cmd.hdr); +} +EXPORT_SYMBOL_GPL(q6cvp_send_channel_info); + +int q6cvp_send_media_format(struct q6voice_session *cvp, int port_id, bool is_tx) +{ + struct vss_icommon_cmd_set_param_v2_cmd cmd; + + cmd.hdr.pkt_size = offsetof(struct vss_icommon_cmd_set_param_v2_cmd, param_data.media_format_info) + + sizeof(struct vss_param_endpoint_media_format_info); + cmd.hdr.opcode = VSS_ICOMMON_CMD_SET_PARAM_V2; + + cmd.mem_handle = 0; + cmd.mem_size = offsetof(struct vss_icommon_param_data, media_format_info) + + sizeof(struct vss_param_endpoint_media_format_info); + + cmd.param_data.module_id = VSS_MODULE_CVD_GENERIC; + cmd.param_data.param_id = is_tx ? VSS_PARAM_TX_PORT_ENDPOINT_MEDIA_INFO + : VSS_PARAM_RX_PORT_ENDPOINT_MEDIA_INFO; + cmd.param_data.param_size = sizeof(struct vss_param_endpoint_media_format_info); + cmd.param_data.reserved = 0; + + cmd.param_data.media_format_info.port_id = port_id; + + if (is_tx) + cmd.param_data.media_format_info.num_channels = 1; + else + cmd.param_data.media_format_info.num_channels = 2; + + cmd.param_data.media_format_info.bits_per_sample = 16; + cmd.param_data.media_format_info.sample_rate = 48000; + + q6dsp_map_channels(cmd.param_data.channel_info.channel_mapping, + cmd.param_data.channel_info.num_channels); + + return q6voice_common_send(cvp, &cmd.hdr); +} +EXPORT_SYMBOL_GPL(q6cvp_send_media_format); + +int q6cvp_topology_commit(struct q6voice_session *cvp) +{ + struct apr_pkt cmd; + + cmd.hdr.pkt_size = APR_HDR_SIZE; + cmd.hdr.opcode = VSS_IVOCPROC_CMD_TOPOLOGY_COMMIT; + + return q6voice_common_send(cvp, &cmd.hdr); +} +EXPORT_SYMBOL_GPL(q6cvp_topology_commit); + +int q6cvp_enable(struct q6voice_session *cvp, bool state) +{ + struct apr_pkt cmd; + + cmd.hdr.pkt_size = APR_HDR_SIZE; + cmd.hdr.opcode = state ? VSS_IVOCPROC_CMD_ENABLE : VSS_IVOCPROC_CMD_DISABLE; + + return q6voice_common_send(cvp, &cmd.hdr); +} +EXPORT_SYMBOL_GPL(q6cvp_enable); + +static int q6cvp_probe(struct apr_device *adev) +{ + return q6voice_common_probe(adev, Q6VOICE_SERVICE_CVP); +} + +static const struct of_device_id q6cvp_device_id[] = { + { .compatible = "qcom,q6cvp" }, + {}, +}; +MODULE_DEVICE_TABLE(of, q6cvp_device_id); + +static struct apr_driver qcom_q6cvp_driver = { + .probe = q6cvp_probe, + .remove = q6voice_common_remove, + .callback = q6voice_common_callback, + .driver = { + .name = "qcom-q6cvp", + .of_match_table = of_match_ptr(q6cvp_device_id), + }, +}; + +module_apr_driver(qcom_q6cvp_driver); + +MODULE_AUTHOR("Stephan Gerhold "); +MODULE_DESCRIPTION("Q6 Core Voice Processor"); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/qcom/qdsp6/q6cvp.h b/sound/soc/qcom/qdsp6/q6cvp.h new file mode 100644 index 00000000000000..aad99eb9e95def --- /dev/null +++ b/sound/soc/qcom/qdsp6/q6cvp.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _Q6_CVP_H +#define _Q6_CVP_H + +#include "q6voice.h" + +struct q6voice_session; + +struct q6voice_session *q6cvp_session_create(enum q6voice_path_type path, + u16 tx_port, u16 rx_port, + u32 tx_topo, u32 rx_topo); +struct q6voice_session *q6cvp_session_create_v3(enum q6voice_path_type path, + u16 tx_port, u16 rx_port, + u32 tx_topo, u32 rx_topo); +int q6cvp_send_channel_info(struct q6voice_session *cvp, bool is_tx); +int q6cvp_send_media_format(struct q6voice_session *cvp, int port_id, bool is_tx); +int q6cvp_topology_commit(struct q6voice_session *cvp); +int q6cvp_enable(struct q6voice_session *cvp, bool enable); + +#endif /*_Q6_CVP_H */ diff --git a/sound/soc/qcom/qdsp6/q6cvs.c b/sound/soc/qcom/qdsp6/q6cvs.c new file mode 100644 index 00000000000000..440715870a04cf --- /dev/null +++ b/sound/soc/qcom/qdsp6/q6cvs.c @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2012-2017, The Linux Foundation. All rights reserved. +// Copyright (c) 2020, Stephan Gerhold + +#include +#include +#include +#include "q6cvs.h" +#include "q6voice-common.h" + +static int q6cvs_probe(struct apr_device *adev) +{ + return q6voice_common_probe(adev, Q6VOICE_SERVICE_CVS); +} + +static const struct of_device_id q6cvs_device_id[] = { + { .compatible = "qcom,q6cvs" }, + {}, +}; +MODULE_DEVICE_TABLE(of, q6cvs_device_id); + +static struct apr_driver qcom_q6cvs_driver = { + .probe = q6cvs_probe, + .remove = q6voice_common_remove, + .callback = q6voice_common_callback, + .driver = { + .name = "qcom-q6cvs", + .of_match_table = of_match_ptr(q6cvs_device_id), + }, +}; + +module_apr_driver(qcom_q6cvs_driver); + +MODULE_AUTHOR("Stephan Gerhold "); +MODULE_DESCRIPTION("Q6 Core Voice Stream"); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/qcom/qdsp6/q6cvs.h b/sound/soc/qcom/qdsp6/q6cvs.h new file mode 100644 index 00000000000000..5c204682c76496 --- /dev/null +++ b/sound/soc/qcom/qdsp6/q6cvs.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _Q6_CVS_H +#define _Q6_CVS_H + +#include "q6voice.h" + +struct q6voice_session; + +#endif /*_Q6_CVS_H */ diff --git a/sound/soc/qcom/qdsp6/q6mvm.c b/sound/soc/qcom/qdsp6/q6mvm.c new file mode 100644 index 00000000000000..ace0d9ca302572 --- /dev/null +++ b/sound/soc/qcom/qdsp6/q6mvm.c @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2012-2017, The Linux Foundation. All rights reserved. +// Copyright (c) 2020, Stephan Gerhold + +#include +#include +#include +#include +#include "q6mvm.h" +#include "q6voice-common.h" + +#define VSS_IMVM_CMD_CREATE_PASSIVE_CONTROL_SESSION 0x000110FF + +struct vss_imvm_cmd_create_control_session_cmd { + struct apr_hdr hdr; + + /* A variable-sized stream name. */ + char name[20]; +} __packed; + +#define VSS_IMVM_CMD_SET_POLICY_DUAL_CONTROL 0x00011327 + +/* This command is required to let MVM know who is in control of session. */ +struct vss_imvm_cmd_set_policy_dual_control_cmd { + struct apr_hdr hdr; + + /* Set to TRUE to enable modem state machine control */ + bool enable; +} __packed; + +#define VSS_IMVM_CMD_ATTACH_VOCPROC 0x0001123E +#define VSS_IMVM_CMD_DETACH_VOCPROC 0x0001123F + +/* + * Attach/detach a vocproc to the MVM. + * The MVM will symmetrically connect/disconnect this vocproc + * to/from all the streams currently attached to it. + */ +struct vss_imvm_cmd_attach_vocproc_cmd { + struct apr_hdr hdr; + + /* Handle of vocproc being attached. */ + u16 handle; +} __packed; + +#define VSS_IMVM_CMD_START_VOICE 0x00011190 +#define VSS_IMVM_CMD_STOP_VOICE 0x00011192 + +static inline const char *q6mvm_session_name(enum q6voice_path_type path) +{ + switch (path) { + case Q6VOICE_PATH_VOICE: + return "default modem voice"; + case Q6VOICE_PATH_VOIP: + return "10004000"; + case Q6VOICE_PATH_VOLTE: + return "10C02000"; + case Q6VOICE_PATH_VOICE2: + return "10DC1000"; + case Q6VOICE_PATH_QCHAT: + return "10803000"; + case Q6VOICE_PATH_VOWLAN: + return "10002000"; + case Q6VOICE_PATH_VOICEMMODE1: + return "11C05000"; + case Q6VOICE_PATH_VOICEMMODE2: + return "11DC5000"; + default: + return NULL; + } +} + +static int q6mvm_set_dual_control(struct q6voice_session *mvm) +{ + struct vss_imvm_cmd_set_policy_dual_control_cmd cmd; + + cmd.hdr.pkt_size = sizeof(cmd); + cmd.hdr.opcode = VSS_IMVM_CMD_SET_POLICY_DUAL_CONTROL; + + cmd.enable = true; + + return q6voice_common_send(mvm, &cmd.hdr); +} + +struct q6voice_session *q6mvm_session_create(enum q6voice_path_type path) +{ + struct vss_imvm_cmd_create_control_session_cmd cmd; + struct q6voice_session *mvm; + const char *session_name; + int ret; + + cmd.hdr.pkt_size = sizeof(cmd); + cmd.hdr.opcode = VSS_IMVM_CMD_CREATE_PASSIVE_CONTROL_SESSION; + + session_name = q6mvm_session_name(path); + if (session_name) + strscpy(cmd.name, session_name, sizeof(cmd.name)); + + mvm = q6voice_session_create(Q6VOICE_SERVICE_MVM, path, &cmd.hdr); + if (IS_ERR(mvm)) + return mvm; + + ret = q6mvm_set_dual_control(mvm); + if (ret) { + dev_err(mvm->dev, "failed to set dual control: %d\n", ret); + q6voice_session_release(mvm); + return ERR_PTR(ret); + } + + return mvm; +} +EXPORT_SYMBOL_GPL(q6mvm_session_create); + +int q6mvm_attach(struct q6voice_session *mvm, struct q6voice_session *cvp, + bool state) +{ + struct vss_imvm_cmd_attach_vocproc_cmd cmd; + + cmd.hdr.pkt_size = sizeof(cmd); + cmd.hdr.opcode = state ? VSS_IMVM_CMD_ATTACH_VOCPROC : VSS_IMVM_CMD_DETACH_VOCPROC; + + cmd.handle = cvp->handle; + + return q6voice_common_send(mvm, &cmd.hdr); +} +EXPORT_SYMBOL_GPL(q6mvm_attach); + +int q6mvm_start(struct q6voice_session *mvm, bool state) +{ + struct apr_pkt cmd; + + cmd.hdr.pkt_size = APR_HDR_SIZE; + cmd.hdr.opcode = state ? VSS_IMVM_CMD_START_VOICE : VSS_IMVM_CMD_STOP_VOICE; + + return q6voice_common_send(mvm, &cmd.hdr); +} +EXPORT_SYMBOL_GPL(q6mvm_start); + +static int q6mvm_probe(struct apr_device *adev) +{ + int ret; + + ret = q6voice_common_probe(adev, Q6VOICE_SERVICE_MVM); + if (ret) + return ret; + + return of_platform_populate(adev->dev.of_node, NULL, NULL, &adev->dev); +} + +static void q6mvm_remove(struct apr_device *adev) +{ + of_platform_depopulate(&adev->dev); + q6voice_common_remove(adev); +} + +static const struct of_device_id q6mvm_device_id[] = { + { .compatible = "qcom,q6mvm" }, + {}, +}; +MODULE_DEVICE_TABLE(of, q6mvm_device_id); + +static struct apr_driver qcom_q6mvm_driver = { + .probe = q6mvm_probe, + .remove = q6mvm_remove, + .callback = q6voice_common_callback, + .driver = { + .name = "qcom-q6mvm", + .of_match_table = of_match_ptr(q6mvm_device_id), + }, +}; + +module_apr_driver(qcom_q6mvm_driver); +MODULE_DESCRIPTION("Q6 Multimode Voice Manager"); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/qcom/qdsp6/q6mvm.h b/sound/soc/qcom/qdsp6/q6mvm.h new file mode 100644 index 00000000000000..51279eddb89b66 --- /dev/null +++ b/sound/soc/qcom/qdsp6/q6mvm.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _Q6_MVM_H +#define _Q6_MVM_H + +#include "q6voice.h" + +struct q6voice_session; + +struct q6voice_session *q6mvm_session_create(enum q6voice_path_type path); + +int q6mvm_attach(struct q6voice_session *mvm, struct q6voice_session *cvp, + bool state); +int q6mvm_start(struct q6voice_session *mvm, bool state); + +#endif /*_Q6_MVM_H */ diff --git a/sound/soc/qcom/qdsp6/q6voice-common.c b/sound/soc/qcom/qdsp6/q6voice-common.c new file mode 100644 index 00000000000000..e0a5e4eea84e36 --- /dev/null +++ b/sound/soc/qcom/qdsp6/q6voice-common.c @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2020, Stephan Gerhold + +#include +#include +#include +#include +#include "q6voice-common.h" + +#define APRV2_IBASIC_CMD_DESTROY_SESSION 0x0001003C + +#define TIMEOUT_MS 300 + +struct q6voice_service { + struct apr_device *adev; + enum q6voice_service_type type; + + /* Protect sessions array */ + spinlock_t lock; + struct q6voice_session *sessions[Q6VOICE_PATH_COUNT]; +}; + +/* Protect q6voice_services */ +static DEFINE_SPINLOCK(q6voice_services_lock); +static struct q6voice_service *q6voice_services[Q6VOICE_SERVICE_COUNT] = {0}; + +int q6voice_common_probe(struct apr_device *adev, enum q6voice_service_type type) +{ + struct device *dev = &adev->dev; + struct q6voice_service *svc, *current_svc; + unsigned long flags; + + if (type >= Q6VOICE_SERVICE_COUNT) + return -EINVAL; + + svc = devm_kzalloc(dev, sizeof(*svc), GFP_KERNEL); + if (!svc) + return -ENOMEM; + + svc->adev = adev; + svc->type = type; + spin_lock_init(&svc->lock); + + dev_set_drvdata(dev, svc); + + spin_lock_irqsave(&q6voice_services_lock, flags); + current_svc = q6voice_services[type]; + if (!current_svc) + q6voice_services[type] = svc; + spin_unlock_irqrestore(&q6voice_services_lock, flags); + + return current_svc ? -EEXIST : 0; +} +EXPORT_SYMBOL_GPL(q6voice_common_probe); + +void q6voice_common_remove(struct apr_device *adev) +{ + struct q6voice_service *svc = dev_get_drvdata(&adev->dev); + enum q6voice_service_type type = svc->type; + unsigned long flags; + + spin_lock_irqsave(&q6voice_services_lock, flags); + if (q6voice_services[type] == svc) + q6voice_services[type] = NULL; + spin_unlock_irqrestore(&q6voice_services_lock, flags); + + /* TODO: Should probably free up sessions here??? */ +} +EXPORT_SYMBOL_GPL(q6voice_common_remove); + +static void q6voice_session_free(struct kref *ref) +{ + struct q6voice_session *s = container_of(ref, struct q6voice_session, + refcount); + + kfree(s); +} + +static int q6voice_session_destroy(struct q6voice_session *s) +{ + struct apr_pkt cmd; + + cmd.hdr.pkt_size = APR_HDR_SIZE; + cmd.hdr.opcode = APRV2_IBASIC_CMD_DESTROY_SESSION; + + return q6voice_common_send(s, &cmd.hdr); +} + +void q6voice_session_release(struct q6voice_session *s) +{ + struct q6voice_service *svc = s->svc; + unsigned long flags; + + if (s->handle) + q6voice_session_destroy(s); + + spin_lock_irqsave(&svc->lock, flags); + if (svc->sessions[s->port] == s) + svc->sessions[s->port] = NULL; + spin_unlock_irqrestore(&svc->lock, flags); + + kref_put(&s->refcount, q6voice_session_free); +} +EXPORT_SYMBOL_GPL(q6voice_session_release); + +struct q6voice_session * +q6voice_session_create(enum q6voice_service_type type, + enum q6voice_path_type path, struct apr_hdr *hdr) +{ + struct q6voice_service *svc; + struct q6voice_session *s; + unsigned long flags; + int ret; + + spin_lock_irqsave(&q6voice_services_lock, flags); + svc = q6voice_services[type]; + spin_unlock_irqrestore(&q6voice_services_lock, flags); + if (!svc) + return ERR_PTR(-ENODEV); + + s = kzalloc(sizeof(*s), GFP_KERNEL); + if (!s) + return ERR_PTR(-ENOMEM); + + s->dev = &svc->adev->dev; + s->svc = svc; + s->port = path; + + kref_init(&s->refcount); + spin_lock_init(&s->lock); + init_waitqueue_head(&s->wait); + + spin_lock_irqsave(&svc->lock, flags); + if (svc->sessions[path]) { + spin_unlock_irqrestore(&svc->lock, flags); + kfree(s); + return ERR_PTR(-EBUSY); + } + svc->sessions[path] = s; + spin_unlock_irqrestore(&svc->lock, flags); + + dev_dbg(s->dev, "create session\n"); + + ret = q6voice_common_send(s, hdr); + if (ret) + goto err; + + if (!s->handle) { + dev_warn(s->dev, "failed to receive handle\n"); + ret = -EIO; + goto err; + } + + dev_dbg(s->dev, "handle: %d\n", s->handle); + + return s; + +err: + q6voice_session_release(s); + return ERR_PTR(ret); +} +EXPORT_SYMBOL_GPL(q6voice_session_create); + +static void q6voice_session_callback(struct q6voice_session *s, + const struct apr_resp_pkt *data) +{ + struct aprv2_ibasic_rsp_result_t *result = data->payload; + unsigned long flags; + + if (data->hdr.opcode != APR_BASIC_RSP_RESULT) + return; /* Not handled here */ + + dev_dbg(s->dev, "basic result: opcode %#x, status: %#x\n", + result->opcode, result->status); + + spin_lock_irqsave(&s->lock, flags); + if (result->opcode != s->expected_opcode) { + spin_unlock_irqrestore(&s->lock, flags); + dev_warn(s->dev, "unexpected reply for opcode %#x (status: %#x)\n", + result->opcode, result->status); + return; + } + + if (!s->handle) { + s->handle = data->hdr.src_port; + } else if (s->handle != data->hdr.src_port) { + spin_unlock_irqrestore(&s->lock, flags); + dev_warn(s->dev, "unexpected reply for session %#x (!= %#x)\n", + data->hdr.src_port, s->handle); + return; + } + + s->result = result->status; + s->expected_opcode = 0; + spin_unlock_irqrestore(&s->lock, flags); + + wake_up(&s->wait); +} + +int q6voice_common_callback(struct apr_device *adev, const struct apr_resp_pkt *data) +{ + struct device *dev = &adev->dev; + struct q6voice_service *v = dev_get_drvdata(dev); + struct q6voice_session *s; + unsigned long flags; + + dev_dbg(dev, "callback: %#x\n", data->hdr.opcode); + + if (data->hdr.dest_port >= Q6VOICE_PATH_COUNT) { + dev_warn(dev, "callback() called for unhandled/invalid path: %d\n", + data->hdr.dest_port); + return 0; + } + + spin_lock_irqsave(&v->lock, flags); + s = v->sessions[data->hdr.dest_port]; + if (s) + kref_get(&s->refcount); + spin_unlock_irqrestore(&v->lock, flags); + + if (s) { + q6voice_session_callback(s, data); + kref_put(&s->refcount, q6voice_session_free); + } else { + dev_warn(dev, "callback() called for inactive path: %d\n", + data->hdr.dest_port); + } + + return 0; +} +EXPORT_SYMBOL_GPL(q6voice_common_callback); + +int q6voice_common_send(struct q6voice_session *s, struct apr_hdr *hdr) +{ + unsigned long flags; + int ret; + + hdr->hdr_field = APR_HDR_FIELD(APR_MSG_TYPE_SEQ_CMD, + APR_HDR_LEN(APR_HDR_SIZE), APR_PKT_VER); + hdr->src_port = s->port; + hdr->dest_port = s->handle; + hdr->token = 0; + + spin_lock_irqsave(&s->lock, flags); + s->expected_opcode = hdr->opcode; + s->result = 0; + spin_unlock_irqrestore(&s->lock, flags); + + ret = apr_send_pkt(s->svc->adev, (struct apr_pkt *)hdr); + if (ret < 0) + return ret; + + ret = wait_event_timeout(s->wait, (s->expected_opcode == 0), + msecs_to_jiffies(TIMEOUT_MS)); + if (!ret) { + s->expected_opcode = 0; + return -ETIMEDOUT; + } + + if (s->result > 0) { + dev_err(s->dev, "command %#x failed with error %d\n", + hdr->opcode, s->result); + return -EIO; + } + + return 0; +} +EXPORT_SYMBOL_GPL(q6voice_common_send); + +MODULE_AUTHOR("Stephan Gerhold "); +MODULE_DESCRIPTION("Q6Voice common session management"); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/qcom/qdsp6/q6voice-common.h b/sound/soc/qcom/qdsp6/q6voice-common.h new file mode 100644 index 00000000000000..48735a935c3fbb --- /dev/null +++ b/sound/soc/qcom/qdsp6/q6voice-common.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _Q6_VOICE_COMMON_H +#define _Q6_VOICE_COMMON_H + +#include +#include "q6voice.h" + +enum q6voice_service_type { + Q6VOICE_SERVICE_MVM, + Q6VOICE_SERVICE_CVP, + Q6VOICE_SERVICE_CVS, + Q6VOICE_SERVICE_COUNT +}; + +struct q6voice_service; + +struct q6voice_session { + struct device *dev; + struct q6voice_service *svc; + struct kref refcount; + + u16 port; + u16 handle; + + wait_queue_head_t wait; + + /* Protect expected_opcode and result */ + spinlock_t lock; + u32 expected_opcode; + u32 result; +}; + +int q6voice_common_probe(struct apr_device *adev, enum q6voice_service_type type); +void q6voice_common_remove(struct apr_device *adev); + +int q6voice_common_callback(struct apr_device *adev, const struct apr_resp_pkt *data); +int q6voice_common_send(struct q6voice_session *s, struct apr_hdr *hdr); + +struct q6voice_session *q6voice_session_create(enum q6voice_service_type type, + enum q6voice_path_type path, + struct apr_hdr *hdr); +void q6voice_session_release(struct q6voice_session *s); + +#endif /*_Q6_VOICE_COMMON_H */ diff --git a/sound/soc/qcom/qdsp6/q6voice-dai.c b/sound/soc/qcom/qdsp6/q6voice-dai.c new file mode 100644 index 00000000000000..16af108e0a96f9 --- /dev/null +++ b/sound/soc/qcom/qdsp6/q6voice-dai.c @@ -0,0 +1,458 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2012-2017, The Linux Foundation. All rights reserved. +// Copyright (c) 2020, Stephan Gerhold + +#include +#include +#include +#include +#include +#include +#include "q6voice.h" + +#define DRV_NAME "q6voice-dai" + +static enum q6voice_path_type q6voice_get_path(unsigned int dai_id) +{ + switch (dai_id) { + case CS_VOICE: + return Q6VOICE_PATH_VOICE; + case VOICEMMODE1: + return Q6VOICE_PATH_VOICEMMODE1; + } + + return Q6VOICE_PATH_COUNT; +} + +static int q6voice_dai_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct q6voice *v = snd_soc_dai_get_drvdata(dai); + enum q6voice_path_type path = q6voice_get_path(dai->driver->id); + + if (path == Q6VOICE_PATH_COUNT) { + dev_err(dai->dev, "Invalid DAI ID %u\n", dai->driver->id); + return -EINVAL; + } + + return q6voice_start(v, path, substream->stream); +} + +static void q6voice_dai_shutdown(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct q6voice *v = snd_soc_dai_get_drvdata(dai); + enum q6voice_path_type path = q6voice_get_path(dai->driver->id); + + if (path == Q6VOICE_PATH_COUNT) { + dev_err(dai->dev, "Invalid DAI ID %u\n", dai->driver->id); + return; + } + + q6voice_stop(v, path, substream->stream); +} + +static struct snd_soc_dai_ops q6voice_dai_ops = { + .startup = q6voice_dai_startup, + .shutdown = q6voice_dai_shutdown, +}; + +static struct snd_soc_dai_driver q6voice_dais[] = { + { + .id = CS_VOICE, + .name = "CS-VOICE", + /* The constraints here are not really meaningful... */ + .playback = { + .stream_name = "CS-VOICE Playback", + .formats = SNDRV_PCM_FMTBIT_S16_LE, + .rates = SNDRV_PCM_RATE_8000, + .rate_min = 8000, + .rate_max = 8000, + .channels_min = 1, + .channels_max = 1, + }, + .capture = { + .stream_name = "CS-VOICE Capture", + .formats = SNDRV_PCM_FMTBIT_S16_LE, + .rates = SNDRV_PCM_RATE_8000, + .rate_min = 8000, + .rate_max = 8000, + .channels_min = 1, + .channels_max = 1, + }, + .ops = &q6voice_dai_ops, + }, + { + .id = VOICEMMODE1, + .name = "VOICEMMODE1", + .playback = { + .stream_name = "VOICEMMODE1 Playback", + .formats = SNDRV_PCM_FMTBIT_S16_LE, + .rates = SNDRV_PCM_RATE_8000, + .rate_min = 8000, + .rate_max = 8000, + .channels_min = 1, + .channels_max = 1, + }, + .capture = { + .stream_name = "VOICEMMODE1 Capture", + .formats = SNDRV_PCM_FMTBIT_S16_LE, + .rates = SNDRV_PCM_RATE_8000, + .rate_min = 8000, + .rate_max = 8000, + .channels_min = 1, + .channels_max = 1, + }, + .ops = &q6voice_dai_ops, + }, +}; + +/* FIXME: Use codec2codec instead */ +static struct snd_pcm_hardware q6voice_dai_hardware = { + .info = SNDRV_PCM_INFO_INTERLEAVED, + .buffer_bytes_max = 4096 * 2, + .period_bytes_min = 2048, + .period_bytes_max = 4096, + .periods_min = 2, + .periods_max = 4, + .fifo_size = 0, +}; + +static int q6voice_dai_open(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + substream->runtime->hw = q6voice_dai_hardware; + return 0; +} + +static int q6voice_get_mixer(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol, bool capture) +{ + struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_to_dapm(kcontrol); + struct snd_soc_component *c = snd_soc_dapm_to_component(dapm); + struct soc_mixer_control *mc = + (struct soc_mixer_control *)kcontrol->private_value; + struct q6voice *v = snd_soc_component_get_drvdata(c); + enum q6voice_path_type path = q6voice_get_path(mc->shift); + + if (path == Q6VOICE_PATH_COUNT) { + dev_err(c->dev, "Invalid DAI ID %u\n", mc->shift); + return -EINVAL; + } + + ucontrol->value.integer.value[0] = + q6voice_get_port(v, path, capture) == mc->reg; + return 0; +} + +static int q6voice_put_mixer(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol, bool capture) +{ + struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_to_dapm(kcontrol); + struct snd_soc_component *c = snd_soc_dapm_to_component(dapm); + struct soc_mixer_control *mc = + (struct soc_mixer_control *)kcontrol->private_value; + struct q6voice *v = snd_soc_component_get_drvdata(c); + bool val = !!ucontrol->value.integer.value[0]; + enum q6voice_path_type path = q6voice_get_path(mc->shift); + + if (path == Q6VOICE_PATH_COUNT) { + dev_err(c->dev, "Invalid DAI ID %u\n", mc->shift); + return -EINVAL; + } + + if (val) + q6voice_set_port(v, path, capture, mc->reg); + else if (q6voice_get_port(v, path, capture) == mc->reg) + q6voice_set_port(v, path, capture, 0); + + snd_soc_dapm_mixer_update_power(dapm, kcontrol, val, NULL); + return 1; +} + +static int q6voice_get_mixer_capture(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) +{ + return q6voice_get_mixer(kcontrol, ucontrol, true); +} + +static int q6voice_get_mixer_playback(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) +{ + return q6voice_get_mixer(kcontrol, ucontrol, false); +} + +static int q6voice_put_mixer_capture(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) +{ + return q6voice_put_mixer(kcontrol, ucontrol, true); +} + +static int q6voice_put_mixer_playback(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) +{ + return q6voice_put_mixer(kcontrol, ucontrol, false); +} + +static int q6voice_get_topology_kctrl(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *c = snd_kcontrol_chip(kcontrol); + struct soc_mixer_control *mc = + (struct soc_mixer_control *)kcontrol->private_value; + struct q6voice *v = snd_soc_component_get_drvdata(c); + enum q6voice_path_type path = q6voice_get_path(mc->reg); + bool capture = !!mc->shift; + + if (path >= Q6VOICE_PATH_COUNT) { + dev_err(c->dev, "Invalid DAI ID %u\n", mc->reg); + return -EINVAL; + } + + ucontrol->value.integer.value[0] = + q6voice_get_topology(v, path, capture); + + return 0; +} + +static int q6voice_put_topology_kctrl(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *c = snd_kcontrol_chip(kcontrol); + struct soc_mixer_control *mc = + (struct soc_mixer_control *)kcontrol->private_value; + struct q6voice *v = snd_soc_component_get_drvdata(c); + u32 val = ucontrol->value.integer.value[0]; + enum q6voice_path_type path = q6voice_get_path(mc->reg); + bool capture = !!mc->shift; + + if (path >= Q6VOICE_PATH_COUNT) { + dev_err(c->dev, "Invalid DAI ID %u\n", mc->reg); + return -EINVAL; + } + + q6voice_set_topology(v, path, capture, val); + + return 1; +} + +static const struct snd_kcontrol_new q6voice_kcontrols[] = { + SOC_SINGLE_EXT("VoiceMMode1 TX Topology", VOICEMMODE1, 1, S32_MAX, 0, + q6voice_get_topology_kctrl, q6voice_put_topology_kctrl), + SOC_SINGLE_EXT("VoiceMMode1 RX Topology", VOICEMMODE1, 0, S32_MAX, 0, + q6voice_get_topology_kctrl, q6voice_put_topology_kctrl), +}; + +static const struct snd_kcontrol_new cs_voice_tx_mixer_controls[] = { + SOC_SINGLE_EXT("PRI_MI2S_TX", PRIMARY_MI2S_TX, CS_VOICE, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("SEC_MI2S_TX", SECONDARY_MI2S_TX, CS_VOICE, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("TERT_MI2S_TX", TERTIARY_MI2S_TX, CS_VOICE, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("QUAT_MI2S_TX", QUATERNARY_MI2S_TX, CS_VOICE, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("QUIN_MI2S_TX", QUINARY_MI2S_TX, CS_VOICE, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("SEN_MI2S_TX", SENARY_MI2S_TX, CS_VOICE, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), +}; + +static const struct snd_kcontrol_new voicemmode1_tx_mixer_controls[] = { + SOC_SINGLE_EXT("PRI_MI2S_TX", PRIMARY_MI2S_TX, VOICEMMODE1, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("SEC_MI2S_TX", SECONDARY_MI2S_TX, VOICEMMODE1, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("TERT_MI2S_TX", TERTIARY_MI2S_TX, VOICEMMODE1, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("QUAT_MI2S_TX", QUATERNARY_MI2S_TX, VOICEMMODE1, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("QUIN_MI2S_TX", QUINARY_MI2S_TX, VOICEMMODE1, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("TX_CODEC_DMA_TX_1", TX_CODEC_DMA_TX_1, VOICEMMODE1, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("TX_CODEC_DMA_TX_2", TX_CODEC_DMA_TX_2, VOICEMMODE1, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("TX_CODEC_DMA_TX_3", TX_CODEC_DMA_TX_3, VOICEMMODE1, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("TX_CODEC_DMA_TX_4", TX_CODEC_DMA_TX_4, VOICEMMODE1, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), + SOC_SINGLE_EXT("SEN_MI2S_TX", SENARY_MI2S_TX, VOICEMMODE1, 1, 0, + q6voice_get_mixer_capture, q6voice_put_mixer_capture), +}; + +static const struct snd_kcontrol_new primary_mi2s_rx_mixer_controls[] = { + SOC_SINGLE_EXT("CS-Voice", PRIMARY_MI2S_RX, CS_VOICE, 1, 0, + q6voice_get_mixer_playback, q6voice_put_mixer_playback), + SOC_SINGLE_EXT("VoiceMMode1", PRIMARY_MI2S_RX, VOICEMMODE1, 1, 0, + q6voice_get_mixer_playback, q6voice_put_mixer_playback), +}; + +static const struct snd_kcontrol_new secondary_mi2s_rx_mixer_controls[] = { + SOC_SINGLE_EXT("CS-Voice", SECONDARY_MI2S_RX, CS_VOICE, 1, 0, + q6voice_get_mixer_playback, q6voice_put_mixer_playback), + SOC_SINGLE_EXT("VoiceMMode1", SECONDARY_MI2S_RX, VOICEMMODE1, 1, 0, + q6voice_get_mixer_playback, q6voice_put_mixer_playback), +}; + +static const struct snd_kcontrol_new tertiary_mi2s_rx_mixer_controls[] = { + SOC_SINGLE_EXT("CS-Voice", TERTIARY_MI2S_RX, CS_VOICE, 1, 0, + q6voice_get_mixer_playback, q6voice_put_mixer_playback), + SOC_SINGLE_EXT("VoiceMMode1", TERTIARY_MI2S_RX, VOICEMMODE1, 1, 0, + q6voice_get_mixer_playback, q6voice_put_mixer_playback), +}; + +static const struct snd_kcontrol_new quaternary_mi2s_rx_mixer_controls[] = { + SOC_SINGLE_EXT("CS-Voice", QUATERNARY_MI2S_RX, CS_VOICE, 1, 0, + q6voice_get_mixer_playback, q6voice_put_mixer_playback), + SOC_SINGLE_EXT("VoiceMMode1", QUATERNARY_MI2S_RX, VOICEMMODE1, 1, 0, + q6voice_get_mixer_playback, q6voice_put_mixer_playback), +}; + +static const struct snd_kcontrol_new quinary_mi2s_rx_mixer_controls[] = { + SOC_SINGLE_EXT("CS-Voice", QUINARY_MI2S_RX, CS_VOICE, 1, 0, + q6voice_get_mixer_playback, q6voice_put_mixer_playback), + SOC_SINGLE_EXT("VoiceMMode1", QUINARY_MI2S_RX, VOICEMMODE1, 1, 0, + q6voice_get_mixer_playback, q6voice_put_mixer_playback), +}; + +static const struct snd_kcontrol_new senary_mi2s_rx_mixer_controls[] = { + SOC_SINGLE_EXT("CS-Voice", SENARY_MI2S_RX, CS_VOICE, 1, 0, + q6voice_get_mixer_playback, q6voice_put_mixer_playback), + SOC_SINGLE_EXT("VoiceMMode1", SENARY_MI2S_RX, VOICEMMODE1, 1, 0, + q6voice_get_mixer_playback, q6voice_put_mixer_playback), +}; + +static const struct snd_soc_dapm_widget q6voice_dapm_widgets[] = { + SND_SOC_DAPM_AIF_IN("CS-VOICE_DL1", "CS-VOICE Playback", 0, SND_SOC_NOPM, 0, 0), + SND_SOC_DAPM_AIF_OUT("CS-VOICE_UL1", "CS-VOICE Capture", 0, SND_SOC_NOPM, 0, 0), + SND_SOC_DAPM_MIXER("CS-Voice Capture Mixer", SND_SOC_NOPM, 0, 0, + cs_voice_tx_mixer_controls, + ARRAY_SIZE(cs_voice_tx_mixer_controls)), + SND_SOC_DAPM_AIF_IN("VOICEMMODE1_DL1", "VOICEMMODE1 Playback", 0, SND_SOC_NOPM, 0, 0), + SND_SOC_DAPM_AIF_OUT("VOICEMMODE1_UL1", "VOICEMMODE1 Capture", 0, SND_SOC_NOPM, 0, 0), + SND_SOC_DAPM_MIXER("VoiceMMode1 Capture Mixer", SND_SOC_NOPM, 0, 0, + voicemmode1_tx_mixer_controls, + ARRAY_SIZE(voicemmode1_tx_mixer_controls)), + SND_SOC_DAPM_MIXER("PRI_MI2S_RX Voice Mixer", SND_SOC_NOPM, 0, 0, + primary_mi2s_rx_mixer_controls, + ARRAY_SIZE(primary_mi2s_rx_mixer_controls)), + SND_SOC_DAPM_MIXER("SEC_MI2S_RX Voice Mixer", SND_SOC_NOPM, 0, 0, + secondary_mi2s_rx_mixer_controls, + ARRAY_SIZE(secondary_mi2s_rx_mixer_controls)), + SND_SOC_DAPM_MIXER("TERT_MI2S_RX Voice Mixer", SND_SOC_NOPM, 0, 0, + tertiary_mi2s_rx_mixer_controls, + ARRAY_SIZE(tertiary_mi2s_rx_mixer_controls)), + SND_SOC_DAPM_MIXER("QUAT_MI2S_RX Voice Mixer", SND_SOC_NOPM, 0, 0, + quaternary_mi2s_rx_mixer_controls, + ARRAY_SIZE(quaternary_mi2s_rx_mixer_controls)), + SND_SOC_DAPM_MIXER("QUIN_MI2S_RX Voice Mixer", SND_SOC_NOPM, 0, 0, + quinary_mi2s_rx_mixer_controls, + ARRAY_SIZE(quinary_mi2s_rx_mixer_controls)), + SND_SOC_DAPM_MIXER("SEN_MI2S_RX Voice Mixer", SND_SOC_NOPM, 0, 0, + senary_mi2s_rx_mixer_controls, + ARRAY_SIZE(senary_mi2s_rx_mixer_controls)), +}; + +static const struct snd_soc_dapm_route q6voice_dapm_routes[] = { + { "CS-Voice Capture Mixer", "PRI_MI2S_TX", "PRI_MI2S_TX" }, + { "CS-Voice Capture Mixer", "SEC_MI2S_TX", "SEC_MI2S_TX" }, + { "CS-Voice Capture Mixer", "TERT_MI2S_TX", "TERT_MI2S_TX" }, + { "CS-Voice Capture Mixer", "QUAT_MI2S_TX", "QUAT_MI2S_TX" }, + { "CS-Voice Capture Mixer", "QUIN_MI2S_TX", "QUIN_MI2S_TX" }, + { "CS-Voice Capture Mixer", "SEN_MI2S_TX", "SEN_MI2S_TX" }, + { "CS-VOICE_UL1", NULL, "CS-Voice Capture Mixer" }, + { "VoiceMMode1 Capture Mixer", "PRI_MI2S_TX", "PRI_MI2S_TX" }, + { "VoiceMMode1 Capture Mixer", "SEC_MI2S_TX", "SEC_MI2S_TX" }, + { "VoiceMMode1 Capture Mixer", "TERT_MI2S_TX", "TERT_MI2S_TX" }, + { "VoiceMMode1 Capture Mixer", "QUAT_MI2S_TX", "QUAT_MI2S_TX" }, + { "VoiceMMode1 Capture Mixer", "QUIN_MI2S_TX", "QUIN_MI2S_TX" }, + { "VoiceMMode1 Capture Mixer", "TX_CODEC_DMA_TX_1", "TX_CODEC_DMA_TX_1" }, + { "VoiceMMode1 Capture Mixer", "TX_CODEC_DMA_TX_2", "TX_CODEC_DMA_TX_2" }, + { "VoiceMMode1 Capture Mixer", "TX_CODEC_DMA_TX_3", "TX_CODEC_DMA_TX_3" }, + { "VoiceMMode1 Capture Mixer", "TX_CODEC_DMA_TX_4", "TX_CODEC_DMA_TX_4" }, + { "VoiceMMode1 Capture Mixer", "SEN_MI2S_TX", "SEN_MI2S_TX" }, + { "VOICEMMODE1_UL1", NULL, "VoiceMMode1 Capture Mixer" }, + + { "PRI_MI2S_RX Voice Mixer", "CS-Voice", "CS-VOICE_DL1" }, + { "SEC_MI2S_RX Voice Mixer", "CS-Voice", "CS-VOICE_DL1" }, + { "TERT_MI2S_RX Voice Mixer", "CS-Voice", "CS-VOICE_DL1" }, + { "QUAT_MI2S_RX Voice Mixer", "CS-Voice", "CS-VOICE_DL1" }, + { "QUIN_MI2S_RX Voice Mixer", "CS-Voice", "CS-VOICE_DL1" }, + { "SEN_MI2S_RX Voice Mixer", "CS-Voice", "CS-VOICE_DL1" }, + { "PRI_MI2S_RX Voice Mixer", "VoiceMMode1", "VOICEMMODE1_DL1" }, + { "SEC_MI2S_RX Voice Mixer", "VoiceMMode1", "VOICEMMODE1_DL1" }, + { "TERT_MI2S_RX Voice Mixer", "VoiceMMode1", "VOICEMMODE1_DL1" }, + { "QUAT_MI2S_RX Voice Mixer", "VoiceMMode1", "VOICEMMODE1_DL1" }, + { "QUIN_MI2S_RX Voice Mixer", "VoiceMMode1", "VOICEMMODE1_DL1" }, + { "SEN_MI2S_RX Voice Mixer", "VoiceMMode1", "VOICEMMODE1_DL1" }, + + { "PRI_MI2S_RX", NULL, "PRI_MI2S_RX Voice Mixer" }, + { "SEC_MI2S_RX", NULL, "SEC_MI2S_RX Voice Mixer" }, + { "TERT_MI2S_RX", NULL, "TERT_MI2S_RX Voice Mixer" }, + { "QUAT_MI2S_RX", NULL, "QUAT_MI2S_RX Voice Mixer" }, + { "QUIN_MI2S_RX", NULL, "QUIN_MI2S_RX Voice Mixer" }, + { "SEN_MI2S_RX", NULL, "SEN_MI2S_RX Voice Mixer" }, +}; + +static unsigned int q6voice_reg_read(struct snd_soc_component *component, + unsigned int reg) +{ + /* default value */ + return 0; +} + +static int q6voice_reg_write(struct snd_soc_component *component, + unsigned int reg, unsigned int val) +{ + /* dummy */ + return 0; +} + +static const struct snd_soc_component_driver q6voice_dai_component = { + .name = DRV_NAME, + .open = q6voice_dai_open, + + .controls = q6voice_kcontrols, + .num_controls = ARRAY_SIZE(q6voice_kcontrols), + .dapm_widgets = q6voice_dapm_widgets, + .num_dapm_widgets = ARRAY_SIZE(q6voice_dapm_widgets), + .dapm_routes = q6voice_dapm_routes, + .num_dapm_routes = ARRAY_SIZE(q6voice_dapm_routes), + .read = q6voice_reg_read, + .write = q6voice_reg_write, + + /* Needs to probe after q6afe */ + .probe_order = SND_SOC_COMP_ORDER_LATE, +}; + +static int q6voice_dai_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct q6voice *v; + struct device_node *np = dev_of_node(dev); + bool cvd_v2_3; + + cvd_v2_3 = of_property_read_bool(np, "qcom,cvd-v2.3"); + + v = q6voice_create(dev, cvd_v2_3); + if (IS_ERR(v)) + return PTR_ERR(v); + + dev_set_drvdata(dev, v); + + return devm_snd_soc_register_component(dev, &q6voice_dai_component, + q6voice_dais, + ARRAY_SIZE(q6voice_dais)); +} + +static const struct of_device_id q6voice_dai_device_id[] = { + { .compatible = "qcom,q6voice-dais" }, + {}, +}; +MODULE_DEVICE_TABLE(of, q6voice_dai_device_id); + +static struct platform_driver q6voice_dai_platform_driver = { + .driver = { + .name = "q6voice-dai", + .of_match_table = of_match_ptr(q6voice_dai_device_id), + }, + .probe = q6voice_dai_probe, +}; +module_platform_driver(q6voice_dai_platform_driver); + +MODULE_AUTHOR("Stephan Gerhold "); +MODULE_DESCRIPTION("Q6Voice DAI driver"); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/qcom/qdsp6/q6voice.c b/sound/soc/qcom/qdsp6/q6voice.c new file mode 100644 index 00000000000000..bb27cb7c0d0b82 --- /dev/null +++ b/sound/soc/qcom/qdsp6/q6voice.c @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2012-2017, The Linux Foundation. All rights reserved. +// Copyright (c) 2020, Stephan Gerhold + +#include +#include +#include +#include +#include +#include "q6afe.h" +#include "q6cvp.h" +#include "q6cvs.h" +#include "q6mvm.h" +#include "q6voice-common.h" + +#define VSS_IVOCPROC_TOPOLOGY_ID_NONE 0x00010F70 +#define VSS_IVOCPROC_TOPOLOGY_ID_TX_SM_ECNS 0x00010F71 +#define VSS_IVOCPROC_TOPOLOGY_ID_RX_DEFAULT 0x00010F77 + +struct q6voice_path_runtime { + struct q6voice_session *sessions[Q6VOICE_SERVICE_COUNT]; + unsigned int started; +}; + +struct q6voice_path { + struct q6voice *v; + + enum q6voice_path_type type; + int tx_port, rx_port; + u32 tx_topo, rx_topo; + /* Serialize access to voice path session */ + struct mutex lock; + struct q6voice_path_runtime *runtime; +}; + +struct q6voice { + struct device *dev; + bool cvd_v2_3; + struct q6voice_path paths[Q6VOICE_PATH_COUNT]; +}; + +static inline struct q6voice_session *session_create(enum q6voice_path_type type, + int tx_port, int rx_port, + u32 tx_topo, u32 rx_topo) +{ + return q6cvp_session_create(type, tx_port, rx_port, tx_topo, rx_topo); +} + +static struct q6voice_session *session_create_v3(enum q6voice_path_type type, + int tx_port, int rx_port, + u32 tx_topo, u32 rx_topo) +{ + struct q6voice_session *cvp; + + cvp = q6cvp_session_create_v3(type, tx_port, rx_port, tx_topo, rx_topo); + if (cvp == NULL) + return NULL; + + q6cvp_send_channel_info(cvp, false); + q6cvp_send_channel_info(cvp, true); + + q6cvp_send_media_format(cvp, rx_port, false); + q6cvp_send_media_format(cvp, tx_port, true); + + q6cvp_topology_commit(cvp); + + return cvp; +} + +static int q6voice_path_start(struct q6voice_path *p) +{ + struct device *dev = p->v->dev; + struct q6voice_session *mvm, *cvp; + int ret; + + dev_dbg(dev, "start path %d\n", p->type); + + mvm = p->runtime->sessions[Q6VOICE_SERVICE_MVM]; + if (!mvm) { + mvm = q6mvm_session_create(p->type); + if (IS_ERR(mvm)) + return PTR_ERR(mvm); + p->runtime->sessions[Q6VOICE_SERVICE_MVM] = mvm; + } + + cvp = p->runtime->sessions[Q6VOICE_SERVICE_CVP]; + if (!cvp) { + if (p->v->cvd_v2_3) + cvp = session_create_v3(p->type, + q6afe_get_port_id(p->tx_port), + q6afe_get_port_id(p->rx_port), + p->tx_topo, p->rx_topo); + else + cvp = session_create(p->type, + q6afe_get_port_id(p->tx_port), + q6afe_get_port_id(p->rx_port), + p->tx_topo, p->rx_topo); + + if (IS_ERR(cvp)) + return PTR_ERR(cvp); + p->runtime->sessions[Q6VOICE_SERVICE_CVP] = cvp; + } + + ret = q6cvp_enable(cvp, true); + if (ret) { + dev_err(dev, "failed to enable cvp: %d\n", ret); + goto cvp_err; + } + + ret = q6mvm_attach(mvm, cvp, true); + if (ret) { + dev_err(dev, "failed to attach cvp to mvm: %d\n", ret); + goto attach_err; + } + + ret = q6mvm_start(mvm, true); + if (ret) { + dev_err(dev, "failed to start voice: %d\n", ret); + goto start_err; + } + + return ret; + +start_err: + q6mvm_start(mvm, false); +attach_err: + q6mvm_attach(mvm, cvp, false); +cvp_err: + q6cvp_enable(cvp, false); + return ret; +} + +int q6voice_start(struct q6voice *v, enum q6voice_path_type path, bool capture) +{ + struct q6voice_path *p = &v->paths[path]; + int ret = 0; + + mutex_lock(&p->lock); + if (!p->runtime) { + p->runtime = kzalloc(sizeof(*p), GFP_KERNEL); + if (!p->runtime) { + ret = -ENOMEM; + goto out; + } + } + + if (p->runtime->started & BIT(capture)) { + ret = -EALREADY; + goto out; + } + + p->runtime->started |= BIT(capture); + + /* FIXME: For now we only start if both RX/TX are active */ + if (p->runtime->started != 3) + goto out; + + ret = q6voice_path_start(p); + if (ret) { + p->runtime->started &= ~BIT(capture); + dev_err(v->dev, "failed to start path %d: %d\n", path, ret); + goto out; + } + +out: + mutex_unlock(&p->lock); + return ret; +} +EXPORT_SYMBOL_GPL(q6voice_start); + +static void q6voice_path_stop(struct q6voice_path *p) +{ + struct device *dev = p->v->dev; + struct q6voice_session *mvm = p->runtime->sessions[Q6VOICE_SERVICE_MVM]; + struct q6voice_session *cvp = p->runtime->sessions[Q6VOICE_SERVICE_CVP]; + int ret; + + dev_dbg(dev, "stop path %d\n", p->type); + + ret = q6mvm_start(mvm, false); + if (ret) + dev_err(dev, "failed to stop voice: %d\n", ret); + + ret = q6mvm_attach(mvm, cvp, false); + if (ret) + dev_err(dev, "failed to detach cvp from mvm: %d\n", ret); + + ret = q6cvp_enable(cvp, false); + if (ret) + dev_err(dev, "failed to disable cvp: %d\n", ret); +} + +static void q6voice_path_destroy(struct q6voice_path *p) +{ + struct q6voice_path_runtime *runtime = p->runtime; + enum q6voice_service_type svc; + + for (svc = 0; svc < Q6VOICE_SERVICE_COUNT; ++svc) { + if (runtime->sessions[svc]) + q6voice_session_release(runtime->sessions[svc]); + } + + p->runtime = NULL; + kfree(runtime); +} + +int q6voice_stop(struct q6voice *v, enum q6voice_path_type path, bool capture) +{ + struct q6voice_path *p = &v->paths[path]; + int ret = 0; + + mutex_lock(&p->lock); + if (!p->runtime || !(p->runtime->started & BIT(capture))) + goto out; + + if (p->runtime->started == 3) + q6voice_path_stop(p); + + p->runtime->started &= ~BIT(capture); + + if (p->runtime->started == 0) + q6voice_path_destroy(p); + +out: + mutex_unlock(&p->lock); + return ret; +} +EXPORT_SYMBOL_GPL(q6voice_stop); + +static void q6voice_free(void *data) +{ + struct q6voice *v = data; + enum q6voice_path_type path; + + for (path = 0; path < Q6VOICE_PATH_COUNT; ++path) { + struct q6voice_path *p = &v->paths[path]; + + mutex_lock(&p->lock); + if (p->runtime) { + dev_warn(v->dev, + "q6voice_remove() called while path %d is active\n", + path); + + if (p->runtime->started == 3) + q6voice_path_stop(p); + q6voice_path_destroy(p); + } + mutex_unlock(&p->lock); + mutex_destroy(&p->lock); + } +} + +struct q6voice *q6voice_create(struct device *dev, bool cvd_v2_3) +{ + struct q6voice *v; + enum q6voice_path_type path; + int ret; + + v = devm_kzalloc(dev, sizeof(*v), GFP_KERNEL); + if (!v) + return ERR_PTR(-ENOMEM); + + v->dev = dev; + v->cvd_v2_3 = cvd_v2_3; + + for (path = 0; path < Q6VOICE_PATH_COUNT; ++path) { + struct q6voice_path *p = &v->paths[path]; + + p->v = v; + p->type = path; + p->tx_topo = VSS_IVOCPROC_TOPOLOGY_ID_NONE; + p->rx_topo = VSS_IVOCPROC_TOPOLOGY_ID_RX_DEFAULT; + mutex_init(&p->lock); + } + + ret = devm_add_action(dev, q6voice_free, v); + if (ret) + return ERR_PTR(ret); + + return v; +} +EXPORT_SYMBOL_GPL(q6voice_create); + +int q6voice_get_port(struct q6voice *v, enum q6voice_path_type path, + bool capture) +{ + struct q6voice_path *p = &v->paths[path]; + + if (capture) + return p->tx_port; + else + return p->rx_port; +} +EXPORT_SYMBOL_GPL(q6voice_get_port); + +void q6voice_set_port(struct q6voice *v, enum q6voice_path_type path, + bool capture, int index) +{ + struct q6voice_path *p = &v->paths[path]; + + if (capture) + p->tx_port = index; + else + p->rx_port = index; +} +EXPORT_SYMBOL_GPL(q6voice_set_port); + +u32 q6voice_get_topology(struct q6voice *v, enum q6voice_path_type path, + bool capture) +{ + struct q6voice_path *p = &v->paths[path]; + + if (capture) + return p->tx_topo; + else + return p->rx_topo; +} +EXPORT_SYMBOL_GPL(q6voice_get_topology); + +void q6voice_set_topology(struct q6voice *v, enum q6voice_path_type path, + bool capture, u32 topo) +{ + struct q6voice_path *p = &v->paths[path]; + + if (capture) + p->tx_topo = topo; + else + p->rx_topo = topo; +} +EXPORT_SYMBOL_GPL(q6voice_set_topology); + +MODULE_AUTHOR("Stephan Gerhold "); +MODULE_DESCRIPTION("Q6Voice driver"); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/qcom/qdsp6/q6voice.h b/sound/soc/qcom/qdsp6/q6voice.h new file mode 100644 index 00000000000000..3d002a9a5a2773 --- /dev/null +++ b/sound/soc/qcom/qdsp6/q6voice.h @@ -0,0 +1,33 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _Q6_VOICE_H +#define _Q6_VOICE_H + +enum q6voice_path_type { + Q6VOICE_PATH_VOICE = 0, + Q6VOICE_PATH_VOIP = 1, + Q6VOICE_PATH_VOLTE = 2, + Q6VOICE_PATH_VOICE2 = 3, + Q6VOICE_PATH_QCHAT = 4, + Q6VOICE_PATH_VOWLAN = 5, + Q6VOICE_PATH_VOICEMMODE1 = 6, + Q6VOICE_PATH_VOICEMMODE2 = 7, + Q6VOICE_PATH_COUNT +}; + +struct q6voice; + +struct q6voice *q6voice_create(struct device *dev, bool cvd_v2_3); +int q6voice_start(struct q6voice *v, enum q6voice_path_type path, bool capture); +int q6voice_stop(struct q6voice *v, enum q6voice_path_type path, bool capture); + +int q6voice_get_port(struct q6voice *v, enum q6voice_path_type path, + bool capture); +void q6voice_set_port(struct q6voice *v, enum q6voice_path_type path, + bool capture, int index); + +u32 q6voice_get_topology(struct q6voice *v, enum q6voice_path_type path, + bool capture); +void q6voice_set_topology(struct q6voice *v, enum q6voice_path_type path, + bool capture, u32 topo); + +#endif /*_Q6_VOICE_H */