|
@@ -0,0 +1,903 @@
|
|
|
|
+/*****************************************************************************
|
|
|
|
+ * Copyright 2011 Broadcom Corporation. All rights reserved.
|
|
|
|
+ *
|
|
|
|
+ * Unless you and Broadcom execute a separate written software license
|
|
|
|
+ * agreement governing use of this software, this software is licensed to you
|
|
|
|
+ * under the terms of the GNU General Public License version 2, available at
|
|
|
|
+ * http://www.broadcom.com/licenses/GPLv2.php (the "GPL").
|
|
|
|
+ *
|
|
|
|
+ * Notwithstanding the above, under no circumstances may you combine this
|
|
|
|
+ * software in any way with any other Broadcom software provided under a
|
|
|
|
+ * license other than the GPL, without Broadcom's express prior written
|
|
|
|
+ * consent.
|
|
|
|
+ *****************************************************************************/
|
|
|
|
+
|
|
|
|
+#include <linux/device.h>
|
|
|
|
+#include <sound/core.h>
|
|
|
|
+#include <sound/initval.h>
|
|
|
|
+#include <sound/pcm.h>
|
|
|
|
+#include <linux/io.h>
|
|
|
|
+#include <linux/interrupt.h>
|
|
|
|
+#include <linux/fs.h>
|
|
|
|
+#include <linux/file.h>
|
|
|
|
+#include <linux/mm.h>
|
|
|
|
+#include <linux/syscalls.h>
|
|
|
|
+#include <asm/uaccess.h>
|
|
|
|
+#include <linux/slab.h>
|
|
|
|
+#include <linux/delay.h>
|
|
|
|
+#include <linux/atomic.h>
|
|
|
|
+#include <linux/module.h>
|
|
|
|
+#include <linux/completion.h>
|
|
|
|
+
|
|
|
|
+#include "bcm2835.h"
|
|
|
|
+
|
|
|
|
+/* ---- Include Files -------------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+#include "interface/vchi/vchi.h"
|
|
|
|
+#include "vc_vchi_audioserv_defs.h"
|
|
|
|
+
|
|
|
|
+/* ---- Private Constants and Types ------------------------------------------ */
|
|
|
|
+
|
|
|
|
+#define BCM2835_AUDIO_STOP 0
|
|
|
|
+#define BCM2835_AUDIO_START 1
|
|
|
|
+#define BCM2835_AUDIO_WRITE 2
|
|
|
|
+
|
|
|
|
+/* Logging macros (for remapping to other logging mechanisms, i.e., printf) */
|
|
|
|
+#ifdef AUDIO_DEBUG_ENABLE
|
|
|
|
+#define LOG_ERR( fmt, arg... ) pr_err( "%s:%d " fmt, __func__, __LINE__, ##arg)
|
|
|
|
+#define LOG_WARN( fmt, arg... ) pr_info( "%s:%d " fmt, __func__, __LINE__, ##arg)
|
|
|
|
+#define LOG_INFO( fmt, arg... ) pr_info( "%s:%d " fmt, __func__, __LINE__, ##arg)
|
|
|
|
+#define LOG_DBG( fmt, arg... ) pr_info( "%s:%d " fmt, __func__, __LINE__, ##arg)
|
|
|
|
+#else
|
|
|
|
+#define LOG_ERR( fmt, arg... ) pr_err( "%s:%d " fmt, __func__, __LINE__, ##arg)
|
|
|
|
+#define LOG_WARN( fmt, arg... )
|
|
|
|
+#define LOG_INFO( fmt, arg... )
|
|
|
|
+#define LOG_DBG( fmt, arg... )
|
|
|
|
+#endif
|
|
|
|
+
|
|
|
|
+struct bcm2835_audio_instance {
|
|
|
|
+ unsigned int num_connections;
|
|
|
|
+ VCHI_SERVICE_HANDLE_T vchi_handle[VCHI_MAX_NUM_CONNECTIONS];
|
|
|
|
+ struct completion msg_avail_comp;
|
|
|
|
+ struct mutex vchi_mutex;
|
|
|
|
+ struct bcm2835_alsa_stream *alsa_stream;
|
|
|
|
+ int result;
|
|
|
|
+ short peer_version;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+bool force_bulk = false;
|
|
|
|
+
|
|
|
|
+/* ---- Private Variables ---------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+/* ---- Private Function Prototypes ------------------------------------------ */
|
|
|
|
+
|
|
|
|
+/* ---- Private Functions ---------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+static int bcm2835_audio_stop_worker(struct bcm2835_alsa_stream *alsa_stream);
|
|
|
|
+static int bcm2835_audio_start_worker(struct bcm2835_alsa_stream *alsa_stream);
|
|
|
|
+static int bcm2835_audio_write_worker(struct bcm2835_alsa_stream *alsa_stream,
|
|
|
|
+ unsigned int count, void *src);
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+// Routine to send a message across a service
|
|
|
|
+
|
|
|
|
+static ssize_t
|
|
|
|
+bcm2835_vchi_msg_queue_callback(void *context, void *dest,
|
|
|
|
+ size_t offset, size_t maxsize)
|
|
|
|
+{
|
|
|
|
+ memcpy(dest, context + offset, maxsize);
|
|
|
|
+ return maxsize;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static int
|
|
|
|
+bcm2835_vchi_msg_queue(VCHI_SERVICE_HANDLE_T handle,
|
|
|
|
+ void *data,
|
|
|
|
+ unsigned int size)
|
|
|
|
+{
|
|
|
|
+ return vchi_msg_queue(handle,
|
|
|
|
+ bcm2835_vchi_msg_queue_callback,
|
|
|
|
+ data,
|
|
|
|
+ size);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+struct bcm2835_audio_work {
|
|
|
|
+ struct work_struct my_work;
|
|
|
|
+ struct bcm2835_alsa_stream *alsa_stream;
|
|
|
|
+ int cmd;
|
|
|
|
+ void *src;
|
|
|
|
+ unsigned int count;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+static void my_wq_function(struct work_struct *work)
|
|
|
|
+{
|
|
|
|
+ struct bcm2835_audio_work *w =
|
|
|
|
+ container_of(work, struct bcm2835_audio_work, my_work);
|
|
|
|
+ int ret = -9;
|
|
|
|
+ LOG_DBG(" .. IN %p:%d\n", w->alsa_stream, w->cmd);
|
|
|
|
+ switch (w->cmd) {
|
|
|
|
+ case BCM2835_AUDIO_START:
|
|
|
|
+ ret = bcm2835_audio_start_worker(w->alsa_stream);
|
|
|
|
+ break;
|
|
|
|
+ case BCM2835_AUDIO_STOP:
|
|
|
|
+ ret = bcm2835_audio_stop_worker(w->alsa_stream);
|
|
|
|
+ break;
|
|
|
|
+ case BCM2835_AUDIO_WRITE:
|
|
|
|
+ ret = bcm2835_audio_write_worker(w->alsa_stream, w->count,
|
|
|
|
+ w->src);
|
|
|
|
+ break;
|
|
|
|
+ default:
|
|
|
|
+ LOG_ERR(" Unexpected work: %p:%d\n", w->alsa_stream, w->cmd);
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ kfree((void *) work);
|
|
|
|
+ LOG_DBG(" .. OUT %d\n", ret);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+int bcm2835_audio_start(struct bcm2835_alsa_stream *alsa_stream)
|
|
|
|
+{
|
|
|
|
+ int ret = -1;
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+ if (alsa_stream->my_wq) {
|
|
|
|
+ struct bcm2835_audio_work *work =
|
|
|
|
+ kmalloc(sizeof(struct bcm2835_audio_work *), GFP_ATOMIC);
|
|
|
|
+ /*--- Queue some work (item 1) ---*/
|
|
|
|
+ if (work) {
|
|
|
|
+ INIT_WORK(&work->my_work, my_wq_function);
|
|
|
|
+ work->alsa_stream = alsa_stream;
|
|
|
|
+ work->cmd = BCM2835_AUDIO_START;
|
|
|
|
+ if (queue_work(alsa_stream->my_wq, &work->my_work))
|
|
|
|
+ ret = 0;
|
|
|
|
+ } else
|
|
|
|
+ LOG_ERR(" .. Error: NULL work kmalloc\n");
|
|
|
|
+ }
|
|
|
|
+ LOG_DBG(" .. OUT %d\n", ret);
|
|
|
|
+ return ret;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+int bcm2835_audio_stop(struct bcm2835_alsa_stream *alsa_stream)
|
|
|
|
+{
|
|
|
|
+ int ret = -1;
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+ if (alsa_stream->my_wq) {
|
|
|
|
+ struct bcm2835_audio_work *work =
|
|
|
|
+ kmalloc(sizeof(struct bcm2835_audio_work *), GFP_ATOMIC);
|
|
|
|
+ /*--- Queue some work (item 1) ---*/
|
|
|
|
+ if (work) {
|
|
|
|
+ INIT_WORK(&work->my_work, my_wq_function);
|
|
|
|
+ work->alsa_stream = alsa_stream;
|
|
|
|
+ work->cmd = BCM2835_AUDIO_STOP;
|
|
|
|
+ if (queue_work(alsa_stream->my_wq, &work->my_work))
|
|
|
|
+ ret = 0;
|
|
|
|
+ } else
|
|
|
|
+ LOG_ERR(" .. Error: NULL work kmalloc\n");
|
|
|
|
+ }
|
|
|
|
+ LOG_DBG(" .. OUT %d\n", ret);
|
|
|
|
+ return ret;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+int bcm2835_audio_write(struct bcm2835_alsa_stream *alsa_stream,
|
|
|
|
+ unsigned int count, void *src)
|
|
|
|
+{
|
|
|
|
+ int ret = -1;
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+ if (alsa_stream->my_wq) {
|
|
|
|
+ struct bcm2835_audio_work *work =
|
|
|
|
+ kmalloc(sizeof(struct bcm2835_audio_work *), GFP_ATOMIC);
|
|
|
|
+ /*--- Queue some work (item 1) ---*/
|
|
|
|
+ if (work) {
|
|
|
|
+ INIT_WORK(&work->my_work, my_wq_function);
|
|
|
|
+ work->alsa_stream = alsa_stream;
|
|
|
|
+ work->cmd = BCM2835_AUDIO_WRITE;
|
|
|
|
+ work->src = src;
|
|
|
|
+ work->count = count;
|
|
|
|
+ if (queue_work(alsa_stream->my_wq, &work->my_work))
|
|
|
|
+ ret = 0;
|
|
|
|
+ } else
|
|
|
|
+ LOG_ERR(" .. Error: NULL work kmalloc\n");
|
|
|
|
+ }
|
|
|
|
+ LOG_DBG(" .. OUT %d\n", ret);
|
|
|
|
+ return ret;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void my_workqueue_init(struct bcm2835_alsa_stream *alsa_stream)
|
|
|
|
+{
|
|
|
|
+ alsa_stream->my_wq = alloc_workqueue("my_queue", WQ_HIGHPRI, 1);
|
|
|
|
+ return;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void my_workqueue_quit(struct bcm2835_alsa_stream *alsa_stream)
|
|
|
|
+{
|
|
|
|
+ if (alsa_stream->my_wq) {
|
|
|
|
+ flush_workqueue(alsa_stream->my_wq);
|
|
|
|
+ destroy_workqueue(alsa_stream->my_wq);
|
|
|
|
+ alsa_stream->my_wq = NULL;
|
|
|
|
+ }
|
|
|
|
+ return;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void audio_vchi_callback(void *param,
|
|
|
|
+ const VCHI_CALLBACK_REASON_T reason,
|
|
|
|
+ void *msg_handle)
|
|
|
|
+{
|
|
|
|
+ struct bcm2835_audio_instance *instance = param;
|
|
|
|
+ int status;
|
|
|
|
+ int msg_len;
|
|
|
|
+ struct vc_audio_msg m;
|
|
|
|
+ LOG_DBG(" .. IN instance=%p, handle=%p, alsa=%p, reason=%d, handle=%p\n",
|
|
|
|
+ instance, instance ? instance->vchi_handle[0] : NULL, instance ? instance->alsa_stream : NULL, reason, msg_handle);
|
|
|
|
+
|
|
|
|
+ if (reason != VCHI_CALLBACK_MSG_AVAILABLE) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ if (!instance) {
|
|
|
|
+ LOG_ERR(" .. instance is null\n");
|
|
|
|
+ BUG();
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ if (!instance->vchi_handle[0]) {
|
|
|
|
+ LOG_ERR(" .. instance->vchi_handle[0] is null\n");
|
|
|
|
+ BUG();
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ status = vchi_msg_dequeue(instance->vchi_handle[0],
|
|
|
|
+ &m, sizeof(m), &msg_len, VCHI_FLAGS_NONE);
|
|
|
|
+ if (m.type == VC_AUDIO_MSG_TYPE_RESULT) {
|
|
|
|
+ LOG_DBG(" .. instance=%p, m.type=VC_AUDIO_MSG_TYPE_RESULT, success=%d\n",
|
|
|
|
+ instance, m.u.result.success);
|
|
|
|
+ instance->result = m.u.result.success;
|
|
|
|
+ complete(&instance->msg_avail_comp);
|
|
|
|
+ } else if (m.type == VC_AUDIO_MSG_TYPE_COMPLETE) {
|
|
|
|
+ struct bcm2835_alsa_stream *alsa_stream = instance->alsa_stream;
|
|
|
|
+#if defined(CONFIG_64BIT)
|
|
|
|
+ irq_handler_t callback =
|
|
|
|
+ (irq_handler_t) (((unsigned long) m.u.complete.callbackl) |
|
|
|
|
+ ((unsigned long) m.u.complete.callbackh << 32));
|
|
|
|
+#else
|
|
|
|
+ irq_handler_t callback = (irq_handler_t) m.u.complete.callback;
|
|
|
|
+#endif
|
|
|
|
+ LOG_DBG(" .. instance=%p, m.type=VC_AUDIO_MSG_TYPE_COMPLETE, complete=%d\n",
|
|
|
|
+ instance, m.u.complete.count);
|
|
|
|
+ if (alsa_stream && callback) {
|
|
|
|
+ atomic_add(m.u.complete.count, &alsa_stream->retrieved);
|
|
|
|
+ callback(0, alsa_stream);
|
|
|
|
+ } else {
|
|
|
|
+ LOG_ERR(" .. unexpected alsa_stream=%p, callback=%p\n",
|
|
|
|
+ alsa_stream, callback);
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ LOG_ERR(" .. unexpected m.type=%d\n", m.type);
|
|
|
|
+ }
|
|
|
|
+ LOG_DBG(" .. OUT\n");
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static struct bcm2835_audio_instance *
|
|
|
|
+vc_vchi_audio_init(VCHI_INSTANCE_T vchi_instance,
|
|
|
|
+ VCHI_CONNECTION_T **vchi_connections,
|
|
|
|
+ unsigned int num_connections)
|
|
|
|
+{
|
|
|
|
+ unsigned int i;
|
|
|
|
+ struct bcm2835_audio_instance *instance;
|
|
|
|
+ int status;
|
|
|
|
+
|
|
|
|
+ LOG_DBG("%s: start", __func__);
|
|
|
|
+
|
|
|
|
+ if (num_connections > VCHI_MAX_NUM_CONNECTIONS) {
|
|
|
|
+ LOG_ERR("%s: unsupported number of connections %u (max=%u)\n",
|
|
|
|
+ __func__, num_connections, VCHI_MAX_NUM_CONNECTIONS);
|
|
|
|
+
|
|
|
|
+ return NULL;
|
|
|
|
+ }
|
|
|
|
+ /* Allocate memory for this instance */
|
|
|
|
+ instance = kmalloc(sizeof(*instance), GFP_KERNEL);
|
|
|
|
+ if (!instance)
|
|
|
|
+ return NULL;
|
|
|
|
+
|
|
|
|
+ memset(instance, 0, sizeof(*instance));
|
|
|
|
+ instance->num_connections = num_connections;
|
|
|
|
+
|
|
|
|
+ /* Create a lock for exclusive, serialized VCHI connection access */
|
|
|
|
+ mutex_init(&instance->vchi_mutex);
|
|
|
|
+ /* Open the VCHI service connections */
|
|
|
|
+ for (i = 0; i < num_connections; i++) {
|
|
|
|
+ SERVICE_CREATION_T params = {
|
|
|
|
+ VCHI_VERSION_EX(VC_AUDIOSERV_VER, VC_AUDIOSERV_MIN_VER),
|
|
|
|
+ VC_AUDIO_SERVER_NAME, // 4cc service code
|
|
|
|
+ vchi_connections[i], // passed in fn pointers
|
|
|
|
+ 0, // rx fifo size (unused)
|
|
|
|
+ 0, // tx fifo size (unused)
|
|
|
|
+ audio_vchi_callback, // service callback
|
|
|
|
+ instance, // service callback parameter
|
|
|
|
+ 1, //TODO: remove VCOS_FALSE, // unaligned bulk recieves
|
|
|
|
+ 1, //TODO: remove VCOS_FALSE, // unaligned bulk transmits
|
|
|
|
+ 0 // want crc check on bulk transfers
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ LOG_DBG("%s: about to open %i\n", __func__, i);
|
|
|
|
+ status = vchi_service_open(vchi_instance, ¶ms,
|
|
|
|
+ &instance->vchi_handle[i]);
|
|
|
|
+ LOG_DBG("%s: opened %i: %p=%d\n", __func__, i, instance->vchi_handle[i], status);
|
|
|
|
+ if (status) {
|
|
|
|
+ LOG_ERR("%s: failed to open VCHI service connection (status=%d)\n",
|
|
|
|
+ __func__, status);
|
|
|
|
+
|
|
|
|
+ goto err_close_services;
|
|
|
|
+ }
|
|
|
|
+ /* Finished with the service for now */
|
|
|
|
+ vchi_service_release(instance->vchi_handle[i]);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ LOG_DBG("%s: okay\n", __func__);
|
|
|
|
+ return instance;
|
|
|
|
+
|
|
|
|
+err_close_services:
|
|
|
|
+ for (i = 0; i < instance->num_connections; i++) {
|
|
|
|
+ LOG_ERR("%s: closing %i: %p\n", __func__, i, instance->vchi_handle[i]);
|
|
|
|
+ if (instance->vchi_handle[i])
|
|
|
|
+ vchi_service_close(instance->vchi_handle[i]);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ kfree(instance);
|
|
|
|
+ LOG_ERR("%s: error\n", __func__);
|
|
|
|
+
|
|
|
|
+ return NULL;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static int vc_vchi_audio_deinit(struct bcm2835_audio_instance *instance)
|
|
|
|
+{
|
|
|
|
+ unsigned int i;
|
|
|
|
+
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+
|
|
|
|
+ if (!instance) {
|
|
|
|
+ LOG_ERR("%s: invalid handle %p\n", __func__, instance);
|
|
|
|
+
|
|
|
|
+ return -1;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ LOG_DBG(" .. about to lock (%d)\n", instance->num_connections);
|
|
|
|
+ if (mutex_lock_interruptible(&instance->vchi_mutex)) {
|
|
|
|
+ LOG_DBG("Interrupted whilst waiting for lock on (%d)\n",
|
|
|
|
+ instance->num_connections);
|
|
|
|
+ return -EINTR;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /* Close all VCHI service connections */
|
|
|
|
+ for (i = 0; i < instance->num_connections; i++) {
|
|
|
|
+ int status;
|
|
|
|
+ LOG_DBG(" .. %i:closing %p\n", i, instance->vchi_handle[i]);
|
|
|
|
+ vchi_service_use(instance->vchi_handle[i]);
|
|
|
|
+
|
|
|
|
+ status = vchi_service_close(instance->vchi_handle[i]);
|
|
|
|
+ if (status) {
|
|
|
|
+ LOG_DBG("%s: failed to close VCHI service connection (status=%d)\n",
|
|
|
|
+ __func__, status);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ mutex_unlock(&instance->vchi_mutex);
|
|
|
|
+
|
|
|
|
+ kfree(instance);
|
|
|
|
+
|
|
|
|
+ LOG_DBG(" .. OUT\n");
|
|
|
|
+
|
|
|
|
+ return 0;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static int bcm2835_audio_open_connection(struct bcm2835_alsa_stream *alsa_stream)
|
|
|
|
+{
|
|
|
|
+ static VCHI_INSTANCE_T vchi_instance;
|
|
|
|
+ static VCHI_CONNECTION_T *vchi_connection;
|
|
|
|
+ static int initted;
|
|
|
|
+ struct bcm2835_audio_instance *instance =
|
|
|
|
+ (struct bcm2835_audio_instance *)alsa_stream->instance;
|
|
|
|
+ int ret;
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+
|
|
|
|
+ LOG_INFO("%s: start\n", __func__);
|
|
|
|
+ BUG_ON(instance);
|
|
|
|
+ if (instance) {
|
|
|
|
+ LOG_ERR("%s: VCHI instance already open (%p)\n",
|
|
|
|
+ __func__, instance);
|
|
|
|
+ instance->alsa_stream = alsa_stream;
|
|
|
|
+ alsa_stream->instance = instance;
|
|
|
|
+ ret = 0; // xxx todo -1;
|
|
|
|
+ goto err_free_mem;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /* Initialize and create a VCHI connection */
|
|
|
|
+ if (!initted) {
|
|
|
|
+ ret = vchi_initialise(&vchi_instance);
|
|
|
|
+ if (ret) {
|
|
|
|
+ LOG_ERR("%s: failed to initialise VCHI instance (ret=%d)\n",
|
|
|
|
+ __func__, ret);
|
|
|
|
+
|
|
|
|
+ ret = -EIO;
|
|
|
|
+ goto err_free_mem;
|
|
|
|
+ }
|
|
|
|
+ ret = vchi_connect(NULL, 0, vchi_instance);
|
|
|
|
+ if (ret) {
|
|
|
|
+ LOG_ERR("%s: failed to connect VCHI instance (ret=%d)\n",
|
|
|
|
+ __func__, ret);
|
|
|
|
+
|
|
|
|
+ ret = -EIO;
|
|
|
|
+ goto err_free_mem;
|
|
|
|
+ }
|
|
|
|
+ initted = 1;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /* Initialize an instance of the audio service */
|
|
|
|
+ instance = vc_vchi_audio_init(vchi_instance, &vchi_connection, 1);
|
|
|
|
+
|
|
|
|
+ if (!instance) {
|
|
|
|
+ LOG_ERR("%s: failed to initialize audio service\n", __func__);
|
|
|
|
+
|
|
|
|
+ ret = -EPERM;
|
|
|
|
+ goto err_free_mem;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ instance->alsa_stream = alsa_stream;
|
|
|
|
+ alsa_stream->instance = instance;
|
|
|
|
+
|
|
|
|
+ LOG_DBG(" success !\n");
|
|
|
|
+err_free_mem:
|
|
|
|
+ LOG_DBG(" .. OUT\n");
|
|
|
|
+
|
|
|
|
+ return ret;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+int bcm2835_audio_open(struct bcm2835_alsa_stream *alsa_stream)
|
|
|
|
+{
|
|
|
|
+ struct bcm2835_audio_instance *instance;
|
|
|
|
+ struct vc_audio_msg m;
|
|
|
|
+ int status;
|
|
|
|
+ int ret;
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+
|
|
|
|
+ my_workqueue_init(alsa_stream);
|
|
|
|
+
|
|
|
|
+ ret = bcm2835_audio_open_connection(alsa_stream);
|
|
|
|
+ if (ret) {
|
|
|
|
+ ret = -1;
|
|
|
|
+ goto exit;
|
|
|
|
+ }
|
|
|
|
+ instance = alsa_stream->instance;
|
|
|
|
+ LOG_DBG(" instance (%p)\n", instance);
|
|
|
|
+
|
|
|
|
+ if (mutex_lock_interruptible(&instance->vchi_mutex)) {
|
|
|
|
+ LOG_DBG("Interrupted whilst waiting for lock on (%d)\n", instance->num_connections);
|
|
|
|
+ return -EINTR;
|
|
|
|
+ }
|
|
|
|
+ vchi_service_use(instance->vchi_handle[0]);
|
|
|
|
+
|
|
|
|
+ m.type = VC_AUDIO_MSG_TYPE_OPEN;
|
|
|
|
+
|
|
|
|
+ /* Send the message to the videocore */
|
|
|
|
+ status = bcm2835_vchi_msg_queue(instance->vchi_handle[0],
|
|
|
|
+ &m, sizeof(m));
|
|
|
|
+
|
|
|
|
+ if (status) {
|
|
|
|
+ LOG_ERR("%s: failed on vchi_msg_queue (status=%d)\n",
|
|
|
|
+ __func__, status);
|
|
|
|
+
|
|
|
|
+ ret = -1;
|
|
|
|
+ goto unlock;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ ret = 0;
|
|
|
|
+
|
|
|
|
+unlock:
|
|
|
|
+ vchi_service_release(instance->vchi_handle[0]);
|
|
|
|
+ mutex_unlock(&instance->vchi_mutex);
|
|
|
|
+exit:
|
|
|
|
+ LOG_DBG(" .. OUT\n");
|
|
|
|
+ return ret;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static int bcm2835_audio_set_ctls_chan(struct bcm2835_alsa_stream *alsa_stream,
|
|
|
|
+ struct bcm2835_chip *chip)
|
|
|
|
+{
|
|
|
|
+ struct vc_audio_msg m;
|
|
|
|
+ struct bcm2835_audio_instance *instance = alsa_stream->instance;
|
|
|
|
+ int status;
|
|
|
|
+ int ret;
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+
|
|
|
|
+ LOG_INFO(" Setting ALSA dest(%d), volume(%d)\n",
|
|
|
|
+ chip->dest, chip->volume);
|
|
|
|
+
|
|
|
|
+ if (mutex_lock_interruptible(&instance->vchi_mutex)) {
|
|
|
|
+ LOG_DBG("Interrupted whilst waiting for lock on (%d)\n",
|
|
|
|
+ instance->num_connections);
|
|
|
|
+ return -EINTR;
|
|
|
|
+ }
|
|
|
|
+ vchi_service_use(instance->vchi_handle[0]);
|
|
|
|
+
|
|
|
|
+ instance->result = -1;
|
|
|
|
+
|
|
|
|
+ m.type = VC_AUDIO_MSG_TYPE_CONTROL;
|
|
|
|
+ m.u.control.dest = chip->dest;
|
|
|
|
+ m.u.control.volume = chip->volume;
|
|
|
|
+
|
|
|
|
+ /* Create the message available completion */
|
|
|
|
+ init_completion(&instance->msg_avail_comp);
|
|
|
|
+
|
|
|
|
+ /* Send the message to the videocore */
|
|
|
|
+ status = bcm2835_vchi_msg_queue(instance->vchi_handle[0],
|
|
|
|
+ &m, sizeof(m));
|
|
|
|
+
|
|
|
|
+ if (status) {
|
|
|
|
+ LOG_ERR("%s: failed on vchi_msg_queue (status=%d)\n",
|
|
|
|
+ __func__, status);
|
|
|
|
+
|
|
|
|
+ ret = -1;
|
|
|
|
+ goto unlock;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /* We are expecting a reply from the videocore */
|
|
|
|
+ wait_for_completion(&instance->msg_avail_comp);
|
|
|
|
+
|
|
|
|
+ if (instance->result) {
|
|
|
|
+ LOG_ERR("%s: result=%d\n", __func__, instance->result);
|
|
|
|
+
|
|
|
|
+ ret = -1;
|
|
|
|
+ goto unlock;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ ret = 0;
|
|
|
|
+
|
|
|
|
+unlock:
|
|
|
|
+ vchi_service_release(instance->vchi_handle[0]);
|
|
|
|
+ mutex_unlock(&instance->vchi_mutex);
|
|
|
|
+
|
|
|
|
+ LOG_DBG(" .. OUT\n");
|
|
|
|
+ return ret;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+int bcm2835_audio_set_ctls(struct bcm2835_chip *chip)
|
|
|
|
+{
|
|
|
|
+ int i;
|
|
|
|
+ int ret = 0;
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+ LOG_DBG(" Setting ALSA dest(%d), volume(%d)\n", chip->dest, chip->volume);
|
|
|
|
+
|
|
|
|
+ /* change ctls for all substreams */
|
|
|
|
+ for (i = 0; i < MAX_SUBSTREAMS; i++) {
|
|
|
|
+ if (chip->avail_substreams & (1 << i)) {
|
|
|
|
+ if (!chip->alsa_stream[i]) {
|
|
|
|
+ LOG_DBG(" No ALSA stream available?! %i:%p (%x)\n", i, chip->alsa_stream[i], chip->avail_substreams);
|
|
|
|
+ ret = 0;
|
|
|
|
+ } else if (bcm2835_audio_set_ctls_chan(chip->alsa_stream[i], chip) != 0) {
|
|
|
|
+ LOG_ERR("Couldn't set the controls for stream %d\n", i);
|
|
|
|
+ ret = -1;
|
|
|
|
+ } else LOG_DBG(" Controls set for stream %d\n", i);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ LOG_DBG(" .. OUT ret=%d\n", ret);
|
|
|
|
+ return ret;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+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;
|
|
|
|
+ struct bcm2835_audio_instance *instance = alsa_stream->instance;
|
|
|
|
+ int status;
|
|
|
|
+ int ret;
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+
|
|
|
|
+ LOG_INFO(" Setting ALSA channels(%d), samplerate(%d), bits-per-sample(%d)\n",
|
|
|
|
+ channels, samplerate, bps);
|
|
|
|
+
|
|
|
|
+ /* resend ctls - alsa_stream may not have been open when first send */
|
|
|
|
+ ret = bcm2835_audio_set_ctls_chan(alsa_stream, alsa_stream->chip);
|
|
|
|
+ if (ret) {
|
|
|
|
+ LOG_ERR(" Alsa controls not supported\n");
|
|
|
|
+ return -EINVAL;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (mutex_lock_interruptible(&instance->vchi_mutex)) {
|
|
|
|
+ LOG_DBG("Interrupted whilst waiting for lock on (%d)\n", instance->num_connections);
|
|
|
|
+ return -EINTR;
|
|
|
|
+ }
|
|
|
|
+ vchi_service_use(instance->vchi_handle[0]);
|
|
|
|
+
|
|
|
|
+ instance->result = -1;
|
|
|
|
+
|
|
|
|
+ m.type = VC_AUDIO_MSG_TYPE_CONFIG;
|
|
|
|
+ m.u.config.channels = channels;
|
|
|
|
+ m.u.config.samplerate = samplerate;
|
|
|
|
+ m.u.config.bps = bps;
|
|
|
|
+
|
|
|
|
+ /* Create the message available completion */
|
|
|
|
+ init_completion(&instance->msg_avail_comp);
|
|
|
|
+
|
|
|
|
+ /* Send the message to the videocore */
|
|
|
|
+ status = bcm2835_vchi_msg_queue(instance->vchi_handle[0],
|
|
|
|
+ &m, sizeof(m));
|
|
|
|
+
|
|
|
|
+ if (status) {
|
|
|
|
+ LOG_ERR("%s: failed on vchi_msg_queue (status=%d)\n",
|
|
|
|
+ __func__, status);
|
|
|
|
+
|
|
|
|
+ ret = -1;
|
|
|
|
+ goto unlock;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /* We are expecting a reply from the videocore */
|
|
|
|
+ wait_for_completion(&instance->msg_avail_comp);
|
|
|
|
+
|
|
|
|
+ if (instance->result) {
|
|
|
|
+ LOG_ERR("%s: result=%d", __func__, instance->result);
|
|
|
|
+
|
|
|
|
+ ret = -1;
|
|
|
|
+ goto unlock;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ ret = 0;
|
|
|
|
+
|
|
|
|
+unlock:
|
|
|
|
+ vchi_service_release(instance->vchi_handle[0]);
|
|
|
|
+ mutex_unlock(&instance->vchi_mutex);
|
|
|
|
+
|
|
|
|
+ LOG_DBG(" .. OUT\n");
|
|
|
|
+ return ret;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+int bcm2835_audio_setup(struct bcm2835_alsa_stream *alsa_stream)
|
|
|
|
+{
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+
|
|
|
|
+ LOG_DBG(" .. OUT\n");
|
|
|
|
+
|
|
|
|
+ return 0;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static int bcm2835_audio_start_worker(struct bcm2835_alsa_stream *alsa_stream)
|
|
|
|
+{
|
|
|
|
+ struct vc_audio_msg m;
|
|
|
|
+ struct bcm2835_audio_instance *instance = alsa_stream->instance;
|
|
|
|
+ int status;
|
|
|
|
+ int ret;
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+
|
|
|
|
+ if (mutex_lock_interruptible(&instance->vchi_mutex)) {
|
|
|
|
+ LOG_DBG("Interrupted whilst waiting for lock on (%d)\n",
|
|
|
|
+ instance->num_connections);
|
|
|
|
+ return -EINTR;
|
|
|
|
+ }
|
|
|
|
+ vchi_service_use(instance->vchi_handle[0]);
|
|
|
|
+
|
|
|
|
+ m.type = VC_AUDIO_MSG_TYPE_START;
|
|
|
|
+
|
|
|
|
+ /* Send the message to the videocore */
|
|
|
|
+ status = bcm2835_vchi_msg_queue(instance->vchi_handle[0],
|
|
|
|
+ &m, sizeof(m));
|
|
|
|
+
|
|
|
|
+ if (status) {
|
|
|
|
+ LOG_ERR("%s: failed on vchi_msg_queue (status=%d)\n",
|
|
|
|
+ __func__, status);
|
|
|
|
+
|
|
|
|
+ ret = -1;
|
|
|
|
+ goto unlock;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ ret = 0;
|
|
|
|
+
|
|
|
|
+unlock:
|
|
|
|
+ vchi_service_release(instance->vchi_handle[0]);
|
|
|
|
+ mutex_unlock(&instance->vchi_mutex);
|
|
|
|
+ LOG_DBG(" .. OUT\n");
|
|
|
|
+ return ret;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static int bcm2835_audio_stop_worker(struct bcm2835_alsa_stream *alsa_stream)
|
|
|
|
+{
|
|
|
|
+ struct vc_audio_msg m;
|
|
|
|
+ struct bcm2835_audio_instance *instance = alsa_stream->instance;
|
|
|
|
+ int status;
|
|
|
|
+ int ret;
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+
|
|
|
|
+ if (mutex_lock_interruptible(&instance->vchi_mutex)) {
|
|
|
|
+ LOG_DBG("Interrupted whilst waiting for lock on (%d)\n",
|
|
|
|
+ instance->num_connections);
|
|
|
|
+ return -EINTR;
|
|
|
|
+ }
|
|
|
|
+ vchi_service_use(instance->vchi_handle[0]);
|
|
|
|
+
|
|
|
|
+ m.type = VC_AUDIO_MSG_TYPE_STOP;
|
|
|
|
+ m.u.stop.draining = alsa_stream->draining;
|
|
|
|
+
|
|
|
|
+ /* Send the message to the videocore */
|
|
|
|
+ status = bcm2835_vchi_msg_queue(instance->vchi_handle[0],
|
|
|
|
+ &m, sizeof(m));
|
|
|
|
+
|
|
|
|
+ if (status) {
|
|
|
|
+ LOG_ERR("%s: failed on vchi_msg_queue (status=%d)\n",
|
|
|
|
+ __func__, status);
|
|
|
|
+
|
|
|
|
+ ret = -1;
|
|
|
|
+ goto unlock;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ ret = 0;
|
|
|
|
+
|
|
|
|
+unlock:
|
|
|
|
+ vchi_service_release(instance->vchi_handle[0]);
|
|
|
|
+ mutex_unlock(&instance->vchi_mutex);
|
|
|
|
+ LOG_DBG(" .. OUT\n");
|
|
|
|
+ return ret;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+int bcm2835_audio_close(struct bcm2835_alsa_stream *alsa_stream)
|
|
|
|
+{
|
|
|
|
+ struct vc_audio_msg m;
|
|
|
|
+ struct bcm2835_audio_instance *instance = alsa_stream->instance;
|
|
|
|
+ int status;
|
|
|
|
+ int ret;
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+
|
|
|
|
+ my_workqueue_quit(alsa_stream);
|
|
|
|
+
|
|
|
|
+ if (mutex_lock_interruptible(&instance->vchi_mutex)) {
|
|
|
|
+ LOG_DBG("Interrupted whilst waiting for lock on (%d)\n",
|
|
|
|
+ instance->num_connections);
|
|
|
|
+ return -EINTR;
|
|
|
|
+ }
|
|
|
|
+ vchi_service_use(instance->vchi_handle[0]);
|
|
|
|
+
|
|
|
|
+ m.type = VC_AUDIO_MSG_TYPE_CLOSE;
|
|
|
|
+
|
|
|
|
+ /* Create the message available completion */
|
|
|
|
+ init_completion(&instance->msg_avail_comp);
|
|
|
|
+
|
|
|
|
+ /* Send the message to the videocore */
|
|
|
|
+ status = bcm2835_vchi_msg_queue(instance->vchi_handle[0],
|
|
|
|
+ &m, sizeof(m));
|
|
|
|
+
|
|
|
|
+ if (status) {
|
|
|
|
+ LOG_ERR("%s: failed on vchi_msg_queue (status=%d)\n",
|
|
|
|
+ __func__, status);
|
|
|
|
+ ret = -1;
|
|
|
|
+ goto unlock;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /* We are expecting a reply from the videocore */
|
|
|
|
+ wait_for_completion(&instance->msg_avail_comp);
|
|
|
|
+
|
|
|
|
+ if (instance->result) {
|
|
|
|
+ LOG_ERR("%s: failed result (result=%d)\n",
|
|
|
|
+ __func__, instance->result);
|
|
|
|
+
|
|
|
|
+ ret = -1;
|
|
|
|
+ goto unlock;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ ret = 0;
|
|
|
|
+
|
|
|
|
+unlock:
|
|
|
|
+ vchi_service_release(instance->vchi_handle[0]);
|
|
|
|
+ mutex_unlock(&instance->vchi_mutex);
|
|
|
|
+
|
|
|
|
+ /* Stop the audio service */
|
|
|
|
+ if (instance) {
|
|
|
|
+ vc_vchi_audio_deinit(instance);
|
|
|
|
+ alsa_stream->instance = NULL;
|
|
|
|
+ }
|
|
|
|
+ LOG_DBG(" .. OUT\n");
|
|
|
|
+ return ret;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+int bcm2835_audio_write_worker(struct bcm2835_alsa_stream *alsa_stream,
|
|
|
|
+ unsigned int count, void *src)
|
|
|
|
+{
|
|
|
|
+ struct vc_audio_msg m;
|
|
|
|
+ struct bcm2835_audio_instance *instance = alsa_stream->instance;
|
|
|
|
+ int status;
|
|
|
|
+ int ret;
|
|
|
|
+
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+
|
|
|
|
+ LOG_INFO(" Writing %d bytes from %p\n", count, src);
|
|
|
|
+
|
|
|
|
+ if (mutex_lock_interruptible(&instance->vchi_mutex)) {
|
|
|
|
+ LOG_DBG("Interrupted whilst waiting for lock on (%d)\n",
|
|
|
|
+ instance->num_connections);
|
|
|
|
+ return -EINTR;
|
|
|
|
+ }
|
|
|
|
+ vchi_service_use(instance->vchi_handle[0]);
|
|
|
|
+
|
|
|
|
+ if (instance->peer_version == 0 && vchi_get_peer_version(instance->vchi_handle[0], &instance->peer_version) == 0) {
|
|
|
|
+ LOG_DBG("%s: client version %d connected\n", __func__, instance->peer_version);
|
|
|
|
+ }
|
|
|
|
+ m.type = VC_AUDIO_MSG_TYPE_WRITE;
|
|
|
|
+ m.u.write.count = count;
|
|
|
|
+ // old version uses bulk, new version uses control
|
|
|
|
+ m.u.write.max_packet = instance->peer_version < 2 || force_bulk ? 0 : 4000;
|
|
|
|
+#if defined(CONFIG_64BIT)
|
|
|
|
+ m.u.write.callbackl = (u32) (((unsigned long) alsa_stream->fifo_irq_handler)&0xFFFFFFFF);
|
|
|
|
+ m.u.write.callbackh = (u32) ((((unsigned long) alsa_stream->fifo_irq_handler) >> 32)&0xFFFFFFFF);
|
|
|
|
+#else
|
|
|
|
+ m.u.write.callback = alsa_stream->fifo_irq_handler;
|
|
|
|
+ m.u.write.cookie = alsa_stream;
|
|
|
|
+#endif
|
|
|
|
+ m.u.write.silence = src == NULL;
|
|
|
|
+
|
|
|
|
+ /* Send the message to the videocore */
|
|
|
|
+ status = bcm2835_vchi_msg_queue(instance->vchi_handle[0],
|
|
|
|
+ &m, sizeof(m));
|
|
|
|
+
|
|
|
|
+ if (status) {
|
|
|
|
+ LOG_ERR("%s: failed on vchi_msg_queue (status=%d)\n",
|
|
|
|
+ __func__, status);
|
|
|
|
+
|
|
|
|
+ ret = -1;
|
|
|
|
+ goto unlock;
|
|
|
|
+ }
|
|
|
|
+ if (!m.u.write.silence) {
|
|
|
|
+ if (!m.u.write.max_packet) {
|
|
|
|
+ /* Send the message to the videocore */
|
|
|
|
+ status = vchi_bulk_queue_transmit(instance->vchi_handle[0],
|
|
|
|
+ src, count,
|
|
|
|
+ 0 *
|
|
|
|
+ VCHI_FLAGS_BLOCK_UNTIL_QUEUED
|
|
|
|
+ +
|
|
|
|
+ 1 *
|
|
|
|
+ VCHI_FLAGS_BLOCK_UNTIL_DATA_READ,
|
|
|
|
+ NULL);
|
|
|
|
+ } else {
|
|
|
|
+ while (count > 0) {
|
|
|
|
+ int bytes = min((int) m.u.write.max_packet, (int) count);
|
|
|
|
+ status = bcm2835_vchi_msg_queue(instance->vchi_handle[0],
|
|
|
|
+ src, bytes);
|
|
|
|
+ src = (char *)src + bytes;
|
|
|
|
+ count -= bytes;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (status) {
|
|
|
|
+ LOG_ERR("%s: failed on vchi_bulk_queue_transmit (status=%d)\n",
|
|
|
|
+ __func__, status);
|
|
|
|
+
|
|
|
|
+ ret = -1;
|
|
|
|
+ goto unlock;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ ret = 0;
|
|
|
|
+
|
|
|
|
+unlock:
|
|
|
|
+ vchi_service_release(instance->vchi_handle[0]);
|
|
|
|
+ mutex_unlock(&instance->vchi_mutex);
|
|
|
|
+ LOG_DBG(" .. OUT\n");
|
|
|
|
+ return ret;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Returns all buffers from arm->vc
|
|
|
|
+ */
|
|
|
|
+void bcm2835_audio_flush_buffers(struct bcm2835_alsa_stream *alsa_stream)
|
|
|
|
+{
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+ LOG_DBG(" .. OUT\n");
|
|
|
|
+ return;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Forces VC to flush(drop) its filled playback buffers and
|
|
|
|
+ * return them the us. (VC->ARM)
|
|
|
|
+ */
|
|
|
|
+void bcm2835_audio_flush_playback_buffers(struct bcm2835_alsa_stream *alsa_stream)
|
|
|
|
+{
|
|
|
|
+ LOG_DBG(" .. IN\n");
|
|
|
|
+ LOG_DBG(" .. OUT\n");
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+unsigned int bcm2835_audio_retrieve_buffers(struct bcm2835_alsa_stream *alsa_stream)
|
|
|
|
+{
|
|
|
|
+ unsigned int count = atomic_read(&alsa_stream->retrieved);
|
|
|
|
+ atomic_sub(count, &alsa_stream->retrieved);
|
|
|
|
+ return count;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+module_param(force_bulk, bool, 0444);
|
|
|
|
+MODULE_PARM_DESC(force_bulk, "Force use of vchiq bulk for audio");
|