/* Copyright (c) 2022, Intel Corporation * SPDX-License-Identifier: Apache-2.0 */ #include #include #include #include #include #include #include /* 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. */ /* host windows */ #define DMWBA(win_base) (win_base + 0x0) #define DMWLO(win_base) (win_base + 0x4) 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 *dev, int wait, uint32_t id, const void *data, int size) { const struct device *mw0 = DEVICE_DT_GET(DT_NODELABEL(mem_window0)); if (!device_is_ready(mw0)) { return -ENODEV; } const struct mem_win_config *mw0_config = mw0->config; uint32_t *buf = (uint32_t *)sys_cache_uncached_ptr_get( (void *)((uint32_t)mw0_config->mem_base + CONFIG_IPM_CAVS_HOST_OUTBOX_OFFSET)); if (!intel_adsp_ipc_is_complete(INTEL_ADSP_IPC_HOST_DEV)) { return -EBUSY; } if ((size < 0) || (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(buf, data, size); int ret = intel_adsp_ipc_send_message(INTEL_ADSP_IPC_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 (ret == -EBUSY && wait) { while (!intel_adsp_ipc_is_complete(INTEL_ADSP_IPC_HOST_DEV)) { k_busy_wait(1); } } return ret; } 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; const struct device *mw1 = DEVICE_DT_GET(DT_NODELABEL(mem_window1)); if (!device_is_ready(mw1)) { return -ENODEV; } const struct mem_win_config *mw1_config = mw1->config; uint32_t *msg = sys_cache_uncached_ptr_get((void *)mw1_config->mem_base); /* 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) { intel_adsp_ipc_complete(INTEL_ADSP_IPC_HOST_DEV); } static int init(const struct device *dev) { struct ipm_cavs_host_data *data = dev->data; const struct device *mw1 = DEVICE_DT_GET(DT_NODELABEL(mem_window1)); if (!device_is_ready(mw1)) { return -ENODEV; } const struct mem_win_config *mw1_config = mw1->config; /* Initialize hardware SRAM window. SOF will give the host 8k * here, let's limit it to just the memory we're using for * futureproofing. */ sys_write32(ROUND_UP(MAX_MSG, 8) | 0x7, DMWLO(mw1_config->base_addr)); sys_write32((mw1_config->mem_base | ADSP_DMWBA_ENABLE), DMWBA(mw1_config->base_addr)); intel_adsp_ipc_set_message_handler(INTEL_ADSP_IPC_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);