| // SPDX-License-Identifier: GPL-2.0 |
| /* Copyright 2011 Broadcom Corporation. All rights reserved. */ |
| |
| #include <linux/slab.h> |
| #include <linux/module.h> |
| #include <linux/completion.h> |
| #include "bcm2835.h" |
| #include "vc_vchi_audioserv_defs.h" |
| |
| struct bcm2835_audio_instance { |
| struct device *dev; |
| unsigned int service_handle; |
| struct completion msg_avail_comp; |
| struct mutex vchi_mutex; |
| struct bcm2835_alsa_stream *alsa_stream; |
| int result; |
| unsigned int max_packet; |
| short peer_version; |
| }; |
| |
| static bool force_bulk; |
| module_param(force_bulk, bool, 0444); |
| MODULE_PARM_DESC(force_bulk, "Force use of vchiq bulk for audio"); |
| |
| static void bcm2835_audio_lock(struct bcm2835_audio_instance *instance) |
| { |
| mutex_lock(&instance->vchi_mutex); |
| vchiq_use_service(instance->service_handle); |
| } |
| |
| static void bcm2835_audio_unlock(struct bcm2835_audio_instance *instance) |
| { |
| vchiq_release_service(instance->service_handle); |
| mutex_unlock(&instance->vchi_mutex); |
| } |
| |
| static int bcm2835_audio_send_msg_locked(struct bcm2835_audio_instance *instance, |
| struct vc_audio_msg *m, bool wait) |
| { |
| int status; |
| |
| if (wait) { |
| instance->result = -1; |
| init_completion(&instance->msg_avail_comp); |
| } |
| |
| status = vchiq_queue_kernel_message(instance->service_handle, |
| m, sizeof(*m)); |
| if (status) { |
| dev_err(instance->dev, |
| "vchi message queue failed: %d, msg=%d\n", |
| status, m->type); |
| return -EIO; |
| } |
| |
| if (wait) { |
| if (!wait_for_completion_timeout(&instance->msg_avail_comp, |
| msecs_to_jiffies(10 * 1000))) { |
| dev_err(instance->dev, |
| "vchi message timeout, msg=%d\n", m->type); |
| return -ETIMEDOUT; |
| } else if (instance->result) { |
| dev_err(instance->dev, |
| "vchi message response error:%d, msg=%d\n", |
| instance->result, m->type); |
| return -EIO; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int bcm2835_audio_send_msg(struct bcm2835_audio_instance *instance, |
| struct vc_audio_msg *m, bool wait) |
| { |
| int err; |
| |
| bcm2835_audio_lock(instance); |
| err = bcm2835_audio_send_msg_locked(instance, m, wait); |
| bcm2835_audio_unlock(instance); |
| return err; |
| } |
| |
| static int bcm2835_audio_send_simple(struct bcm2835_audio_instance *instance, |
| int type, bool wait) |
| { |
| struct vc_audio_msg m = { .type = type }; |
| |
| return bcm2835_audio_send_msg(instance, &m, wait); |
| } |
| |
| static enum vchiq_status audio_vchi_callback(enum vchiq_reason reason, |
| struct vchiq_header *header, |
| unsigned int handle, void *userdata) |
| { |
| struct bcm2835_audio_instance *instance = vchiq_get_service_userdata(handle); |
| struct vc_audio_msg *m; |
| |
| if (reason != VCHIQ_MESSAGE_AVAILABLE) |
| return VCHIQ_SUCCESS; |
| |
| m = (void *)header->data; |
| if (m->type == VC_AUDIO_MSG_TYPE_RESULT) { |
| instance->result = m->result.success; |
| complete(&instance->msg_avail_comp); |
| } else if (m->type == VC_AUDIO_MSG_TYPE_COMPLETE) { |
| if (m->complete.cookie1 != VC_AUDIO_WRITE_COOKIE1 || |
| m->complete.cookie2 != VC_AUDIO_WRITE_COOKIE2) |
| dev_err(instance->dev, "invalid cookie\n"); |
| else |
| bcm2835_playback_fifo(instance->alsa_stream, |
| m->complete.count); |
| } else { |
| dev_err(instance->dev, "unexpected callback type=%d\n", m->type); |
| } |
| |
| vchiq_release_message(handle, header); |
| return VCHIQ_SUCCESS; |
| } |
| |
| static int |
| vc_vchi_audio_init(struct vchiq_instance *vchiq_instance, |
| struct bcm2835_audio_instance *instance) |
| { |
| struct vchiq_service_params_kernel params = { |
| .version = VC_AUDIOSERV_VER, |
| .version_min = VC_AUDIOSERV_MIN_VER, |
| .fourcc = VCHIQ_MAKE_FOURCC('A', 'U', 'D', 'S'), |
| .callback = audio_vchi_callback, |
| .userdata = instance, |
| }; |
| int status; |
| |
| /* Open the VCHI service connections */ |
| status = vchiq_open_service(vchiq_instance, ¶ms, |
| &instance->service_handle); |
| |
| if (status) { |
| dev_err(instance->dev, |
| "failed to open VCHI service connection (status=%d)\n", |
| status); |
| return -EPERM; |
| } |
| |
| /* Finished with the service for now */ |
| vchiq_release_service(instance->service_handle); |
| |
| return 0; |
| } |
| |
| static void vc_vchi_audio_deinit(struct bcm2835_audio_instance *instance) |
| { |
| int status; |
| |
| mutex_lock(&instance->vchi_mutex); |
| vchiq_use_service(instance->service_handle); |
| |
| /* Close all VCHI service connections */ |
| status = vchiq_close_service(instance->service_handle); |
| if (status) { |
| dev_err(instance->dev, |
| "failed to close VCHI service connection (status=%d)\n", |
| status); |
| } |
| |
| mutex_unlock(&instance->vchi_mutex); |
| } |
| |
| int bcm2835_new_vchi_ctx(struct device *dev, struct bcm2835_vchi_ctx *vchi_ctx) |
| { |
| int ret; |
| |
| /* Initialize and create a VCHI connection */ |
| ret = vchiq_initialise(&vchi_ctx->instance); |
| if (ret) { |
| dev_err(dev, "failed to initialise VCHI instance (ret=%d)\n", |
| ret); |
| return -EIO; |
| } |
| |
| ret = vchiq_connect(vchi_ctx->instance); |
| if (ret) { |
| dev_dbg(dev, "failed to connect VCHI instance (ret=%d)\n", |
| ret); |
| |
| kfree(vchi_ctx->instance); |
| vchi_ctx->instance = NULL; |
| |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| void bcm2835_free_vchi_ctx(struct bcm2835_vchi_ctx *vchi_ctx) |
| { |
| /* Close the VCHI connection - it will also free vchi_ctx->instance */ |
| WARN_ON(vchiq_shutdown(vchi_ctx->instance)); |
| |
| vchi_ctx->instance = NULL; |
| } |
| |
| int bcm2835_audio_open(struct bcm2835_alsa_stream *alsa_stream) |
| { |
| struct bcm2835_vchi_ctx *vchi_ctx = alsa_stream->chip->vchi_ctx; |
| struct bcm2835_audio_instance *instance; |
| int err; |
| |
| /* Allocate memory for this instance */ |
| instance = kzalloc(sizeof(*instance), GFP_KERNEL); |
| if (!instance) |
| return -ENOMEM; |
| mutex_init(&instance->vchi_mutex); |
| instance->dev = alsa_stream->chip->dev; |
| instance->alsa_stream = alsa_stream; |
| alsa_stream->instance = instance; |
| |
| err = vc_vchi_audio_init(vchi_ctx->instance, |
| instance); |
| if (err < 0) |
| goto free_instance; |
| |
| err = bcm2835_audio_send_simple(instance, VC_AUDIO_MSG_TYPE_OPEN, |
| false); |
| if (err < 0) |
| goto deinit; |
| |
| bcm2835_audio_lock(instance); |
| vchiq_get_peer_version(instance->service_handle, |
| &instance->peer_version); |
| bcm2835_audio_unlock(instance); |
| if (instance->peer_version < 2 || force_bulk) |
| instance->max_packet = 0; /* bulk transfer */ |
| else |
| instance->max_packet = 4000; |
| |
| return 0; |
| |
| deinit: |
| vc_vchi_audio_deinit(instance); |
| free_instance: |
| alsa_stream->instance = NULL; |
| kfree(instance); |
| return err; |
| } |
| |
| int bcm2835_audio_set_ctls(struct bcm2835_alsa_stream *alsa_stream) |
| { |
| struct bcm2835_chip *chip = alsa_stream->chip; |
| struct vc_audio_msg m = {}; |
| |
| m.type = VC_AUDIO_MSG_TYPE_CONTROL; |
| m.control.dest = chip->dest; |
| if (!chip->mute) |
| m.control.volume = CHIP_MIN_VOLUME; |
| else |
| m.control.volume = alsa2chip(chip->volume); |
| |
| return bcm2835_audio_send_msg(alsa_stream->instance, &m, true); |
| } |
| |
| int bcm2835_audio_set_params(struct bcm2835_alsa_stream *alsa_stream, |
| unsigned int channels, unsigned int samplerate, |
| unsigned int bps) |
| { |
| struct vc_audio_msg m = { |
| .type = VC_AUDIO_MSG_TYPE_CONFIG, |
| .config.channels = channels, |
| .config.samplerate = samplerate, |
| .config.bps = bps, |
| }; |
| int err; |
| |
| /* resend ctls - alsa_stream may not have been open when first send */ |
| err = bcm2835_audio_set_ctls(alsa_stream); |
| if (err) |
| return err; |
| |
| return bcm2835_audio_send_msg(alsa_stream->instance, &m, true); |
| } |
| |
| int bcm2835_audio_start(struct bcm2835_alsa_stream *alsa_stream) |
| { |
| return bcm2835_audio_send_simple(alsa_stream->instance, |
| VC_AUDIO_MSG_TYPE_START, false); |
| } |
| |
| int bcm2835_audio_stop(struct bcm2835_alsa_stream *alsa_stream) |
| { |
| return bcm2835_audio_send_simple(alsa_stream->instance, |
| VC_AUDIO_MSG_TYPE_STOP, false); |
| } |
| |
| /* FIXME: this doesn't seem working as expected for "draining" */ |
| int bcm2835_audio_drain(struct bcm2835_alsa_stream *alsa_stream) |
| { |
| struct vc_audio_msg m = { |
| .type = VC_AUDIO_MSG_TYPE_STOP, |
| .stop.draining = 1, |
| }; |
| |
| return bcm2835_audio_send_msg(alsa_stream->instance, &m, false); |
| } |
| |
| int bcm2835_audio_close(struct bcm2835_alsa_stream *alsa_stream) |
| { |
| struct bcm2835_audio_instance *instance = alsa_stream->instance; |
| int err; |
| |
| err = bcm2835_audio_send_simple(alsa_stream->instance, |
| VC_AUDIO_MSG_TYPE_CLOSE, true); |
| |
| /* Stop the audio service */ |
| vc_vchi_audio_deinit(instance); |
| alsa_stream->instance = NULL; |
| kfree(instance); |
| |
| return err; |
| } |
| |
| int bcm2835_audio_write(struct bcm2835_alsa_stream *alsa_stream, |
| unsigned int size, void *src) |
| { |
| struct bcm2835_audio_instance *instance = alsa_stream->instance; |
| struct vc_audio_msg m = { |
| .type = VC_AUDIO_MSG_TYPE_WRITE, |
| .write.count = size, |
| .write.max_packet = instance->max_packet, |
| .write.cookie1 = VC_AUDIO_WRITE_COOKIE1, |
| .write.cookie2 = VC_AUDIO_WRITE_COOKIE2, |
| }; |
| unsigned int count; |
| int err, status; |
| |
| if (!size) |
| return 0; |
| |
| bcm2835_audio_lock(instance); |
| err = bcm2835_audio_send_msg_locked(instance, &m, false); |
| if (err < 0) |
| goto unlock; |
| |
| count = size; |
| if (!instance->max_packet) { |
| /* Send the message to the videocore */ |
| status = vchiq_bulk_transmit(instance->service_handle, src, |
| count, NULL, |
| VCHIQ_BULK_MODE_BLOCKING); |
| } else { |
| while (count > 0) { |
| int bytes = min(instance->max_packet, count); |
| |
| status = vchiq_queue_kernel_message(instance->service_handle, |
| src, bytes); |
| src += bytes; |
| count -= bytes; |
| } |
| } |
| |
| if (status) { |
| dev_err(instance->dev, |
| "failed on %d bytes transfer (status=%d)\n", |
| size, status); |
| err = -EIO; |
| } |
| |
| unlock: |
| bcm2835_audio_unlock(instance); |
| return err; |
| } |