diff options
Diffstat (limited to 'drivers/most/most_snd.c')
| -rw-r--r-- | drivers/most/most_snd.c | 745 | 
1 files changed, 745 insertions, 0 deletions
| diff --git a/drivers/most/most_snd.c b/drivers/most/most_snd.c new file mode 100644 index 000000000000..c87f6a037874 --- /dev/null +++ b/drivers/most/most_snd.c @@ -0,0 +1,745 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * sound.c - Sound component for Mostcore + * + * Copyright (C) 2015 Microchip Technology Germany II GmbH & Co. KG + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/module.h> +#include <linux/printk.h> +#include <linux/kernel.h> +#include <linux/slab.h> +#include <linux/init.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <linux/sched.h> +#include <linux/kthread.h> +#include <linux/most.h> + +#define DRIVER_NAME "sound" +#define STRING_SIZE	80 + +static struct most_component comp; + +/** + * struct channel - private structure to keep channel specific data + * @substream: stores the substream structure + * @iface: interface for which the channel belongs to + * @cfg: channel configuration + * @card: registered sound card + * @list: list for private use + * @id: channel index + * @period_pos: current period position (ring buffer) + * @buffer_pos: current buffer position (ring buffer) + * @is_stream_running: identifies whether a stream is running or not + * @opened: set when the stream is opened + * @playback_task: playback thread + * @playback_waitq: waitq used by playback thread + */ +struct channel { +	struct snd_pcm_substream *substream; +	struct snd_pcm_hardware pcm_hardware; +	struct most_interface *iface; +	struct most_channel_config *cfg; +	struct snd_card *card; +	struct list_head list; +	int id; +	unsigned int period_pos; +	unsigned int buffer_pos; +	bool is_stream_running; +	struct task_struct *playback_task; +	wait_queue_head_t playback_waitq; +	void (*copy_fn)(void *alsa, void *most, unsigned int bytes); +}; + +struct sound_adapter { +	struct list_head dev_list; +	struct most_interface *iface; +	struct snd_card *card; +	struct list_head list; +	bool registered; +	int pcm_dev_idx; +}; + +static struct list_head adpt_list; + +#define MOST_PCM_INFO (SNDRV_PCM_INFO_MMAP | \ +		       SNDRV_PCM_INFO_MMAP_VALID | \ +		       SNDRV_PCM_INFO_BATCH | \ +		       SNDRV_PCM_INFO_INTERLEAVED | \ +		       SNDRV_PCM_INFO_BLOCK_TRANSFER) + +static void swap_copy16(u16 *dest, const u16 *source, unsigned int bytes) +{ +	unsigned int i = 0; + +	while (i < (bytes / 2)) { +		dest[i] = swab16(source[i]); +		i++; +	} +} + +static void swap_copy24(u8 *dest, const u8 *source, unsigned int bytes) +{ +	unsigned int i = 0; + +	if (bytes < 2) +		return; +	while (i < bytes - 2) { +		dest[i] = source[i + 2]; +		dest[i + 1] = source[i + 1]; +		dest[i + 2] = source[i]; +		i += 3; +	} +} + +static void swap_copy32(u32 *dest, const u32 *source, unsigned int bytes) +{ +	unsigned int i = 0; + +	while (i < bytes / 4) { +		dest[i] = swab32(source[i]); +		i++; +	} +} + +static void alsa_to_most_memcpy(void *alsa, void *most, unsigned int bytes) +{ +	memcpy(most, alsa, bytes); +} + +static void alsa_to_most_copy16(void *alsa, void *most, unsigned int bytes) +{ +	swap_copy16(most, alsa, bytes); +} + +static void alsa_to_most_copy24(void *alsa, void *most, unsigned int bytes) +{ +	swap_copy24(most, alsa, bytes); +} + +static void alsa_to_most_copy32(void *alsa, void *most, unsigned int bytes) +{ +	swap_copy32(most, alsa, bytes); +} + +static void most_to_alsa_memcpy(void *alsa, void *most, unsigned int bytes) +{ +	memcpy(alsa, most, bytes); +} + +static void most_to_alsa_copy16(void *alsa, void *most, unsigned int bytes) +{ +	swap_copy16(alsa, most, bytes); +} + +static void most_to_alsa_copy24(void *alsa, void *most, unsigned int bytes) +{ +	swap_copy24(alsa, most, bytes); +} + +static void most_to_alsa_copy32(void *alsa, void *most, unsigned int bytes) +{ +	swap_copy32(alsa, most, bytes); +} + +/** + * get_channel - get pointer to channel + * @iface: interface structure + * @channel_id: channel ID + * + * This traverses the channel list and returns the channel matching the + * ID and interface. + * + * Returns pointer to channel on success or NULL otherwise. + */ +static struct channel *get_channel(struct most_interface *iface, +				   int channel_id) +{ +	struct sound_adapter *adpt = iface->priv; +	struct channel *channel; + +	list_for_each_entry(channel, &adpt->dev_list, list) { +		if ((channel->iface == iface) && (channel->id == channel_id)) +			return channel; +	} +	return NULL; +} + +/** + * copy_data - implements data copying function + * @channel: channel + * @mbo: MBO from core + * + * Copy data from/to ring buffer to/from MBO and update the buffer position + */ +static bool copy_data(struct channel *channel, struct mbo *mbo) +{ +	struct snd_pcm_runtime *const runtime = channel->substream->runtime; +	unsigned int const frame_bytes = channel->cfg->subbuffer_size; +	unsigned int const buffer_size = runtime->buffer_size; +	unsigned int frames; +	unsigned int fr0; + +	if (channel->cfg->direction & MOST_CH_RX) +		frames = mbo->processed_length / frame_bytes; +	else +		frames = mbo->buffer_length / frame_bytes; +	fr0 = min(buffer_size - channel->buffer_pos, frames); + +	channel->copy_fn(runtime->dma_area + channel->buffer_pos * frame_bytes, +			 mbo->virt_address, +			 fr0 * frame_bytes); + +	if (frames > fr0) { +		/* wrap around at end of ring buffer */ +		channel->copy_fn(runtime->dma_area, +				 mbo->virt_address + fr0 * frame_bytes, +				 (frames - fr0) * frame_bytes); +	} + +	channel->buffer_pos += frames; +	if (channel->buffer_pos >= buffer_size) +		channel->buffer_pos -= buffer_size; +	channel->period_pos += frames; +	if (channel->period_pos >= runtime->period_size) { +		channel->period_pos -= runtime->period_size; +		return true; +	} +	return false; +} + +/** + * playback_thread - function implements the playback thread + * @data: private data + * + * Thread which does the playback functionality in a loop. It waits for a free + * MBO from mostcore for a particular channel and copy the data from ring buffer + * to MBO. Submit the MBO back to mostcore, after copying the data. + * + * Returns 0 on success or error code otherwise. + */ +static int playback_thread(void *data) +{ +	struct channel *const channel = data; + +	while (!kthread_should_stop()) { +		struct mbo *mbo = NULL; +		bool period_elapsed = false; + +		wait_event_interruptible( +			channel->playback_waitq, +			kthread_should_stop() || +			(channel->is_stream_running && +			 (mbo = most_get_mbo(channel->iface, channel->id, +					     &comp)))); +		if (!mbo) +			continue; + +		if (channel->is_stream_running) +			period_elapsed = copy_data(channel, mbo); +		else +			memset(mbo->virt_address, 0, mbo->buffer_length); + +		most_submit_mbo(mbo); +		if (period_elapsed) +			snd_pcm_period_elapsed(channel->substream); +	} +	return 0; +} + +/** + * pcm_open - implements open callback function for PCM middle layer + * @substream: pointer to ALSA PCM substream + * + * This is called when a PCM substream is opened. At least, the function should + * initialize the runtime->hw record. + * + * Returns 0 on success or error code otherwise. + */ +static int pcm_open(struct snd_pcm_substream *substream) +{ +	struct channel *channel = substream->private_data; +	struct snd_pcm_runtime *runtime = substream->runtime; +	struct most_channel_config *cfg = channel->cfg; +	int ret; + +	channel->substream = substream; + +	if (cfg->direction == MOST_CH_TX) { +		channel->playback_task = kthread_run(playback_thread, channel, +						     "most_audio_playback"); +		if (IS_ERR(channel->playback_task)) { +			pr_err("Couldn't start thread\n"); +			return PTR_ERR(channel->playback_task); +		} +	} + +	ret = most_start_channel(channel->iface, channel->id, &comp); +	if (ret) { +		pr_err("most_start_channel() failed!\n"); +		if (cfg->direction == MOST_CH_TX) +			kthread_stop(channel->playback_task); +		return ret; +	} + +	runtime->hw = channel->pcm_hardware; +	return 0; +} + +/** + * pcm_close - implements close callback function for PCM middle layer + * @substream: sub-stream pointer + * + * Obviously, this is called when a PCM substream is closed. Any private + * instance for a PCM substream allocated in the open callback will be + * released here. + * + * Returns 0 on success or error code otherwise. + */ +static int pcm_close(struct snd_pcm_substream *substream) +{ +	struct channel *channel = substream->private_data; + +	if (channel->cfg->direction == MOST_CH_TX) +		kthread_stop(channel->playback_task); +	most_stop_channel(channel->iface, channel->id, &comp); +	return 0; +} + +/** + * pcm_prepare - implements prepare callback function for PCM middle layer + * @substream: substream pointer + * + * This callback is called when the PCM is "prepared". Format rate, sample rate, + * etc., can be set here. This callback can be called many times at each setup. + * + * Returns 0 on success or error code otherwise. + */ +static int pcm_prepare(struct snd_pcm_substream *substream) +{ +	struct channel *channel = substream->private_data; +	struct snd_pcm_runtime *runtime = substream->runtime; +	struct most_channel_config *cfg = channel->cfg; +	int width = snd_pcm_format_physical_width(runtime->format); + +	channel->copy_fn = NULL; + +	if (cfg->direction == MOST_CH_TX) { +		if (snd_pcm_format_big_endian(runtime->format) || width == 8) +			channel->copy_fn = alsa_to_most_memcpy; +		else if (width == 16) +			channel->copy_fn = alsa_to_most_copy16; +		else if (width == 24) +			channel->copy_fn = alsa_to_most_copy24; +		else if (width == 32) +			channel->copy_fn = alsa_to_most_copy32; +	} else { +		if (snd_pcm_format_big_endian(runtime->format) || width == 8) +			channel->copy_fn = most_to_alsa_memcpy; +		else if (width == 16) +			channel->copy_fn = most_to_alsa_copy16; +		else if (width == 24) +			channel->copy_fn = most_to_alsa_copy24; +		else if (width == 32) +			channel->copy_fn = most_to_alsa_copy32; +	} + +	if (!channel->copy_fn) +		return -EINVAL; +	channel->period_pos = 0; +	channel->buffer_pos = 0; +	return 0; +} + +/** + * pcm_trigger - implements trigger callback function for PCM middle layer + * @substream: substream pointer + * @cmd: action to perform + * + * This is called when the PCM is started, stopped or paused. The action will be + * specified in the second argument, SNDRV_PCM_TRIGGER_XXX + * + * Returns 0 on success or error code otherwise. + */ +static int pcm_trigger(struct snd_pcm_substream *substream, int cmd) +{ +	struct channel *channel = substream->private_data; + +	switch (cmd) { +	case SNDRV_PCM_TRIGGER_START: +		channel->is_stream_running = true; +		wake_up_interruptible(&channel->playback_waitq); +		return 0; + +	case SNDRV_PCM_TRIGGER_STOP: +		channel->is_stream_running = false; +		return 0; + +	default: +		return -EINVAL; +	} +	return 0; +} + +/** + * pcm_pointer - implements pointer callback function for PCM middle layer + * @substream: substream pointer + * + * This callback is called when the PCM middle layer inquires the current + * hardware position on the buffer. The position must be returned in frames, + * ranging from 0 to buffer_size-1. + */ +static snd_pcm_uframes_t pcm_pointer(struct snd_pcm_substream *substream) +{ +	struct channel *channel = substream->private_data; + +	return channel->buffer_pos; +} + +/** + * Initialization of struct snd_pcm_ops + */ +static const struct snd_pcm_ops pcm_ops = { +	.open       = pcm_open, +	.close      = pcm_close, +	.prepare    = pcm_prepare, +	.trigger    = pcm_trigger, +	.pointer    = pcm_pointer, +}; + +static int split_arg_list(char *buf, u16 *ch_num, char **sample_res) +{ +	char *num; +	int ret; + +	num = strsep(&buf, "x"); +	if (!num) +		goto err; +	ret = kstrtou16(num, 0, ch_num); +	if (ret) +		goto err; +	*sample_res = strsep(&buf, ".\n"); +	if (!*sample_res) +		goto err; +	return 0; + +err: +	pr_err("Bad PCM format\n"); +	return -EINVAL; +} + +static const struct sample_resolution_info { +	const char *sample_res; +	int bytes; +	u64 formats; +} sinfo[] = { +	{ "8", 1, SNDRV_PCM_FMTBIT_S8 }, +	{ "16", 2, SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S16_BE }, +	{ "24", 3, SNDRV_PCM_FMTBIT_S24_3LE | SNDRV_PCM_FMTBIT_S24_3BE }, +	{ "32", 4, SNDRV_PCM_FMTBIT_S32_LE | SNDRV_PCM_FMTBIT_S32_BE }, +}; + +static int audio_set_hw_params(struct snd_pcm_hardware *pcm_hw, +			       u16 ch_num, char *sample_res, +			       struct most_channel_config *cfg) +{ +	int i; + +	for (i = 0; i < ARRAY_SIZE(sinfo); i++) { +		if (!strcmp(sample_res, sinfo[i].sample_res)) +			goto found; +	} +	pr_err("Unsupported PCM format\n"); +	return -EINVAL; + +found: +	if (!ch_num) { +		pr_err("Bad number of channels\n"); +		return -EINVAL; +	} + +	if (cfg->subbuffer_size != ch_num * sinfo[i].bytes) { +		pr_err("Audio resolution doesn't fit subbuffer size\n"); +		return -EINVAL; +	} + +	pcm_hw->info = MOST_PCM_INFO; +	pcm_hw->rates = SNDRV_PCM_RATE_48000; +	pcm_hw->rate_min = 48000; +	pcm_hw->rate_max = 48000; +	pcm_hw->buffer_bytes_max = cfg->num_buffers * cfg->buffer_size; +	pcm_hw->period_bytes_min = cfg->buffer_size; +	pcm_hw->period_bytes_max = cfg->buffer_size; +	pcm_hw->periods_min = 1; +	pcm_hw->periods_max = cfg->num_buffers; +	pcm_hw->channels_min = ch_num; +	pcm_hw->channels_max = ch_num; +	pcm_hw->formats = sinfo[i].formats; +	return 0; +} + +static void release_adapter(struct sound_adapter *adpt) +{ +	struct channel *channel, *tmp; + +	list_for_each_entry_safe(channel, tmp, &adpt->dev_list, list) { +		list_del(&channel->list); +		kfree(channel); +	} +	if (adpt->card) +		snd_card_free(adpt->card); +	list_del(&adpt->list); +	kfree(adpt); +} + +/** + * audio_probe_channel - probe function of the driver module + * @iface: pointer to interface instance + * @channel_id: channel index/ID + * @cfg: pointer to actual channel configuration + * @arg_list: string that provides the name of the device to be created in /dev + *	      plus the desired audio resolution + * + * Creates sound card, pcm device, sets pcm ops and registers sound card. + * + * Returns 0 on success or error code otherwise. + */ +static int audio_probe_channel(struct most_interface *iface, int channel_id, +			       struct most_channel_config *cfg, +			       char *device_name, char *arg_list) +{ +	struct channel *channel; +	struct sound_adapter *adpt; +	struct snd_pcm *pcm; +	int playback_count = 0; +	int capture_count = 0; +	int ret; +	int direction; +	u16 ch_num; +	char *sample_res; +	char arg_list_cpy[STRING_SIZE]; + +	if (cfg->data_type != MOST_CH_SYNC) { +		pr_err("Incompatible channel type\n"); +		return -EINVAL; +	} +	strscpy(arg_list_cpy, arg_list, STRING_SIZE); +	ret = split_arg_list(arg_list_cpy, &ch_num, &sample_res); +	if (ret < 0) +		return ret; + +	list_for_each_entry(adpt, &adpt_list, list) { +		if (adpt->iface != iface) +			continue; +		if (adpt->registered) +			return -ENOSPC; +		adpt->pcm_dev_idx++; +		goto skip_adpt_alloc; +	} +	adpt = kzalloc(sizeof(*adpt), GFP_KERNEL); +	if (!adpt) +		return -ENOMEM; + +	adpt->iface = iface; +	INIT_LIST_HEAD(&adpt->dev_list); +	iface->priv = adpt; +	list_add_tail(&adpt->list, &adpt_list); +	ret = snd_card_new(iface->driver_dev, -1, "INIC", THIS_MODULE, +			   sizeof(*channel), &adpt->card); +	if (ret < 0) +		goto err_free_adpt; +	snprintf(adpt->card->driver, sizeof(adpt->card->driver), +		 "%s", DRIVER_NAME); +	snprintf(adpt->card->shortname, sizeof(adpt->card->shortname), +		 "Microchip INIC"); +	snprintf(adpt->card->longname, sizeof(adpt->card->longname), +		 "%s at %s", adpt->card->shortname, iface->description); +skip_adpt_alloc: +	if (get_channel(iface, channel_id)) { +		pr_err("channel (%s:%d) is already linked\n", +		       iface->description, channel_id); +		return -EEXIST; +	} + +	if (cfg->direction == MOST_CH_TX) { +		playback_count = 1; +		direction = SNDRV_PCM_STREAM_PLAYBACK; +	} else { +		capture_count = 1; +		direction = SNDRV_PCM_STREAM_CAPTURE; +	} +	channel = kzalloc(sizeof(*channel), GFP_KERNEL); +	if (!channel) { +		ret = -ENOMEM; +		goto err_free_adpt; +	} +	channel->card = adpt->card; +	channel->cfg = cfg; +	channel->iface = iface; +	channel->id = channel_id; +	init_waitqueue_head(&channel->playback_waitq); +	list_add_tail(&channel->list, &adpt->dev_list); + +	ret = audio_set_hw_params(&channel->pcm_hardware, ch_num, sample_res, +				  cfg); +	if (ret) +		goto err_free_adpt; + +	ret = snd_pcm_new(adpt->card, device_name, adpt->pcm_dev_idx, +			  playback_count, capture_count, &pcm); + +	if (ret < 0) +		goto err_free_adpt; + +	pcm->private_data = channel; +	strscpy(pcm->name, device_name, sizeof(pcm->name)); +	snd_pcm_set_ops(pcm, direction, &pcm_ops); +	snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_VMALLOC, NULL, 0, 0); +	return 0; + +err_free_adpt: +	release_adapter(adpt); +	return ret; +} + +static int audio_create_sound_card(void) +{ +	int ret; +	struct sound_adapter *adpt; + +	list_for_each_entry(adpt, &adpt_list, list) { +		if (!adpt->registered) +			goto adpt_alloc; +	} +	return -ENODEV; +adpt_alloc: +	ret = snd_card_register(adpt->card); +	if (ret < 0) { +		release_adapter(adpt); +		return ret; +	} +	adpt->registered = true; +	return 0; +} + +/** + * audio_disconnect_channel - function to disconnect a channel + * @iface: pointer to interface instance + * @channel_id: channel index + * + * This frees allocated memory and removes the sound card from ALSA + * + * Returns 0 on success or error code otherwise. + */ +static int audio_disconnect_channel(struct most_interface *iface, +				    int channel_id) +{ +	struct channel *channel; +	struct sound_adapter *adpt = iface->priv; + +	channel = get_channel(iface, channel_id); +	if (!channel) +		return -EINVAL; + +	list_del(&channel->list); + +	kfree(channel); +	if (list_empty(&adpt->dev_list)) +		release_adapter(adpt); +	return 0; +} + +/** + * audio_rx_completion - completion handler for rx channels + * @mbo: pointer to buffer object that has completed + * + * This searches for the channel this MBO belongs to and copy the data from MBO + * to ring buffer + * + * Returns 0 on success or error code otherwise. + */ +static int audio_rx_completion(struct mbo *mbo) +{ +	struct channel *channel = get_channel(mbo->ifp, mbo->hdm_channel_id); +	bool period_elapsed = false; + +	if (!channel) +		return -EINVAL; +	if (channel->is_stream_running) +		period_elapsed = copy_data(channel, mbo); +	most_put_mbo(mbo); +	if (period_elapsed) +		snd_pcm_period_elapsed(channel->substream); +	return 0; +} + +/** + * audio_tx_completion - completion handler for tx channels + * @iface: pointer to interface instance + * @channel_id: channel index/ID + * + * This searches the channel that belongs to this combination of interface + * pointer and channel ID and wakes a process sitting in the wait queue of + * this channel. + * + * Returns 0 on success or error code otherwise. + */ +static int audio_tx_completion(struct most_interface *iface, int channel_id) +{ +	struct channel *channel = get_channel(iface, channel_id); + +	if (!channel) +		return -EINVAL; + +	wake_up_interruptible(&channel->playback_waitq); +	return 0; +} + +/** + * Initialization of the struct most_component + */ +static struct most_component comp = { +	.mod = THIS_MODULE, +	.name = DRIVER_NAME, +	.probe_channel = audio_probe_channel, +	.disconnect_channel = audio_disconnect_channel, +	.rx_completion = audio_rx_completion, +	.tx_completion = audio_tx_completion, +	.cfg_complete = audio_create_sound_card, +}; + +static int __init audio_init(void) +{ +	int ret; + +	INIT_LIST_HEAD(&adpt_list); + +	ret = most_register_component(&comp); +	if (ret) { +		pr_err("Failed to register %s\n", comp.name); +		return ret; +	} +	ret = most_register_configfs_subsys(&comp); +	if (ret) { +		pr_err("Failed to register %s configfs subsys\n", comp.name); +		most_deregister_component(&comp); +	} +	return ret; +} + +static void __exit audio_exit(void) +{ +	most_deregister_configfs_subsys(&comp); +	most_deregister_component(&comp); +} + +module_init(audio_init); +module_exit(audio_exit); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Christian Gromm <christian.gromm@microchip.com>"); +MODULE_DESCRIPTION("Sound Component Module for Mostcore"); | 
