Linux Audio

Check our new training course

Embedded Linux Audio

Check our new training course
with Creative Commons CC-BY-SA
lecture materials

Bootlin logo

Elixir Cross Referencer

Loading...
/* Copyright (c) 2022, Intel Corporation
 * SPDX-License-Identifier: Apache-2.0
 */
#include <zephyr/kernel.h>
#include <zephyr/drivers/ipm.h>
#include <cavs-mem.h>
#include <cavs-shim.h>
#include <cavs_ipc.h>

/* Matches SOF_IPC_MSG_MAX_SIZE, though in practice nothing anywhere
 * near that big is ever sent.  Should maybe consider making this a
 * kconfig to avoid waste.
 */
#define MAX_MSG 384

/* Note: these addresses aren't flexible!  We require that they match
 * current SOF ipc3/4 layout, which means that:
 *
 * + Buffer addresses are 4k-aligned (this is a hardware requirement)
 * + Inbuf must be 4k after outbuf, with no use of the intervening memory
 * + Outbuf must be 4k after the start of win0 (this is where the host driver looks)
 *
 * One side effect is that the word "before" MSG_INBUF is owned by our
 * code too, and can be used for a nice trick below.
 */
#define BUFPTR(ptr, off) ((uint32_t *) \
	arch_xtensa_uncached_ptr((void *)((uint32_t)ptr + off)))
#define MSG_INBUF BUFPTR(L2_SRAM_BASE, CONFIG_IPM_CAVS_HOST_INBOX_OFFSET)
#define MSG_OUTBUF BUFPTR(HP_SRAM_WIN0_BASE, CONFIG_IPM_CAVS_HOST_OUTBOX_OFFSET)

struct ipm_cavs_host_data {
	ipm_callback_t callback;
	void *user_data;
	bool enabled;
};

/* Note: this call is unsynchronized.  The IPM docs are silent as to
 * whether this is required, and the SOF code that will be using this
 * is externally synchronized already.
 */
static int send(const struct device *ipmdev, int wait, uint32_t id,
		const void *data, int size)
{
	if (!cavs_ipc_is_complete(CAVS_HOST_DEV)) {
		return -EBUSY;
	}

	if (size > MAX_MSG) {
		return -EMSGSIZE;
	}

	if ((id & 0xc0000000) != 0) {
		/* cAVS IDR register has only 30 usable bits */
		return -EINVAL;
	}

	uint32_t ext_data = 0;

	/* Protocol variant (used by SOF "ipc4"): store the first word
	 * of the message in the IPC scratch registers
	 */
	if (IS_ENABLED(CONFIG_IPM_CAVS_HOST_REGWORD) && size >= 4) {
		ext_data = ((uint32_t *)data)[0];
		data = &((const uint32_t *)data)[1];
		size -= 4;
	}

	memcpy(MSG_OUTBUF, data, size);

	bool ok = cavs_ipc_send_message(CAVS_HOST_DEV, id, ext_data);

	/* The IPM docs call for "busy waiting" here, but in fact
	 * there's a blocking synchronous call available that might be
	 * better.  But then we'd have to check whether we're in
	 * interrupt context, and it's not clear to me that SOF would
	 * benefit anyway as all its usage is async.  This is OK for
	 * now.
	 */
	if (ok && wait) {
		while (!cavs_ipc_is_complete(CAVS_HOST_DEV)) {
			k_busy_wait(1);
		}
	}

	return ok ? 0 : -EBUSY;
}

static bool ipc_handler(const struct device *dev, void *arg,
			uint32_t data, uint32_t ext_data)
{
	ARG_UNUSED(arg);
	struct device *ipmdev = arg;
	struct ipm_cavs_host_data *devdata = ipmdev->data;
	uint32_t *msg = MSG_INBUF;

	/* We play tricks to leave one word available before the
	 * beginning of the SRAM window, this way the host can see the
	 * same offsets it does with the original ipc4 protocol
	 * implementation, but here in the firmware we see a single
	 * contiguous buffer.  See above.
	 */
	if (IS_ENABLED(CONFIG_IPM_CAVS_HOST_REGWORD)) {
		msg = &msg[-1];
		msg[0] = ext_data;
	}

	if (devdata->enabled && (devdata->callback != NULL)) {
		devdata->callback(ipmdev, devdata->user_data,
				  data & 0x3fffffff, msg);
	}

	/* Return false for async handling */
	return !IS_ENABLED(IPM_CALLBACK_ASYNC);
}

static int max_data_size_get(const struct device *ipmdev)
{
	return MAX_MSG;
}

static uint32_t max_id_val_get(const struct device *ipmdev)
{
	/* 30 user-writable bits in cAVS IDR register */
	return 0x3fffffff;
}

static void register_callback(const struct device *port,
			      ipm_callback_t cb,
			      void *user_data)
{
	struct ipm_cavs_host_data *data = port->data;

	data->callback = cb;
	data->user_data = user_data;
}

static int set_enabled(const struct device *ipmdev, int enable)
{
	/* This protocol doesn't support any kind of queuing, and in
	 * fact will stall if a message goes unacknowledged.  Support
	 * it as best we can by gating the callbacks only.  That will
	 * allow the DONE notifications to proceed as normal, at the
	 * cost of dropping any messages received while not "enabled"
	 * of course.
	 */
	struct ipm_cavs_host_data *data = ipmdev->data;

	data->enabled = enable;
	return 0;
}

static void complete(const struct device *ipmdev)
{
	cavs_ipc_complete(CAVS_HOST_DEV);
}

static int init(const struct device *dev)
{
	struct ipm_cavs_host_data *data = dev->data;

	/* Initialize hardware SRAM window.  SOF will give the host 8k
	 * here, let's limit it to just the memory we're using for
	 * futureproofing.
	 */
	CAVS_WIN[1].dmwlo = ROUND_UP(MAX_MSG, 8);
	CAVS_WIN[1].dmwba = ((uint32_t) MSG_INBUF) | CAVS_DMWBA_ENABLE;

	cavs_ipc_set_message_handler(CAVS_HOST_DEV, ipc_handler, (void *)dev);

	data->enabled = true;
	return 0;
}

static const struct ipm_driver_api api = {
	.send = send,
	.max_data_size_get = max_data_size_get,
	.max_id_val_get = max_id_val_get,
	.register_callback = register_callback,
	.set_enabled = set_enabled,
	.complete = complete,
};

static struct ipm_cavs_host_data data;

DEVICE_DEFINE(ipm_cavs_host, "ipm_cavs_host", init, NULL, &data, NULL,
	      PRE_KERNEL_2, 1, &api);