blob: 21da546e5f8971c31d5e1c3706591d8eea8eb10d [file] [log] [blame]
/*
* Android/Easel coprocessor communication.
*
* Copyright 2016 Google Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
/* #define DEBUG */
#include <uapi/linux/google-easel-comm.h>
#include "google-easel-comm-shared.h"
#include "google-easel-comm-private.h"
#include <linux/compat.h>
#include <linux/completion.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/kernel.h>
#include <linux/list.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/notifier.h>
#include <linux/reboot.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/uaccess.h>
/* retry delay in msec for retrying flush of a message being processed */
#define MSG_FLUSH_RETRY_DELAY 10
/* Service for link status update */
static struct easelcomm_service link_service;
/* Mutex to guard easelcomm_service_list */
struct mutex service_mutex;
/* Per-Easel-service state */
static struct easelcomm_service *easelcomm_service_list[
EASELCOMM_SERVICE_COUNT];
/* Local command channel local state */
struct easelcomm_cmd_channel_local {
char *buffer; /* start of command buffer */
char *readp; /* ptr to next entry to read */
uint64_t consumer_seqnbr_next; /* next cmd seq# to be consumed */
};
static struct easelcomm_cmd_channel_local cmd_channel_local;
/* Remote command channel local state */
struct easelcomm_cmd_channel_remote {
/* Protects access to all mutable fields below */
struct mutex mutex;
/* offset of next entry to write */
uint64_t write_offset;
/* next command sequence number to write */
uint64_t write_seqnbr;
/* remote consumer has caught up, ready to wrap channel buffer */
struct completion wrap_ready;
/* remote channel is initialized */
bool initialized;
/* channel initialization or link shutdown received from remote */
struct completion init_state_changed;
};
static struct easelcomm_cmd_channel_remote cmd_channel_remote;
/* Per-file-descriptor (user) state */
struct easelcomm_user_state {
/* Which Easel service is registered for this fd, or NULL if none */
struct easelcomm_service *service;
};
/* max delay in msec waiting for remote to wrap command channel */
#define CMDCHAN_WRAP_DONE_TIMEOUT_MS 1000
/* max delay in msec waiting for remote to ack link shutdown */
#define LINK_SHUTDOWN_ACK_TIMEOUT 500
/* max delay in msec waiting for remote to return flush done */
#define SERVICE_FLUSH_DONE_TIMEOUT 5000
static bool easelcomm_up; /* is easelcomm up and running? */
/* used to wait for remote peer to ack link shutdown */
static DECLARE_COMPLETION(easelcomm_link_peer_shutdown);
static int easelcomm_open(struct inode *inode, struct file *file);
static int easelcomm_release(struct inode *inode, struct file *file);
static long easelcomm_ioctl(
struct file *file, unsigned int cmd, unsigned long arg);
#ifdef CONFIG_COMPAT
/* 32-bit userspace on 64-bit OS conversion defines */
struct easelcomm_compat_kbuf_desc {
easelcomm_msgid_t message_id; /* ID of message for this transfer */
compat_uptr_t __user buf; /* local buffer source or dest */
int dma_buf_fd; /* fd of local dma_buf */
int buf_type; /* e.g. EASELCOMM_DMA_BUFFER_DMA_BUF */
uint32_t buf_size; /* size of the local buffer */
};
static long easelcomm_compat_ioctl(
struct file *file, unsigned int cmd, unsigned long arg);
#else
#define easelcomm_compat_ioctl NULL
#endif
static const struct file_operations easelcomm_fops = {
.owner = THIS_MODULE,
.open = easelcomm_open,
.release = easelcomm_release,
.unlocked_ioctl = easelcomm_ioctl,
.compat_ioctl = easelcomm_compat_ioctl,
.llseek = no_llseek,
};
struct miscdevice easelcomm_miscdev = {
.minor = MISC_DYNAMIC_MINOR,
#ifdef CONFIG_GOOGLE_EASEL_AP
.name = "easelcomm-client",
#else
.name = "easelcomm-server",
#endif
.fops = &easelcomm_fops,
};
EXPORT_SYMBOL(easelcomm_miscdev);
/* Dump message info for debugging. */
static void easelcomm_dump_message(
struct easelcomm_message_metadata *msg_metadata)
{
struct easelcomm_kmsg *msg = msg_metadata->msg;
dev_dbg(easelcomm_miscdev.this_device, "msg: id=%s%llu msgsize=%u dmasize=%u needreply=%d inreplyto=%llu replycode=%d\n",
easelcomm_msgid_prefix(msg_metadata),
msg->desc.message_id, msg->desc.message_size,
msg->desc.dma_buf_size, msg->desc.need_reply,
msg->desc.in_reply_to, msg->desc.replycode);
}
/*
* Add an additional refcount bump on a message for which a refcount is
* already held. This function is for use by callers that hold a valid
* message pointer for which a refcount is already held, in order to reflect
* another reference being created, which will be dropped by another context
* later. At present this is used only to hand off incoming reply messages to
* waiters, where the additional refcount reflects the fact that the original
* message contains a pointer to the reply message.
*/
static void easelcomm_grab_reference(
struct easelcomm_service *service,
struct easelcomm_message_metadata *msg_metadata)
{
if (!msg_metadata)
return;
mutex_lock(&service->lock);
msg_metadata->reference_count++;
mutex_unlock(&service->lock);
}
/*
* Find a local message (for the specified service) by ID.
* Returns a pointer to the metadata with the reference count bumped.
*/
struct easelcomm_message_metadata *easelcomm_find_local_message(
struct easelcomm_service *service, easelcomm_msgid_t message_id)
{
struct easelcomm_message_metadata *msg_metadata = NULL;
struct easelcomm_message_metadata *msg_cursor;
mutex_lock(&service->lock);
list_for_each_entry(msg_cursor, &service->local_list,
list) {
if (msg_cursor->msg->desc.message_id == message_id) {
msg_metadata = msg_cursor;
msg_metadata->reference_count++;
break;
}
}
mutex_unlock(&service->lock);
return msg_metadata;
}
EXPORT_SYMBOL(easelcomm_find_local_message);
/*
* Find a remote message (for the specified service) by ID.
* Returns a pointer to the metadata with the reference count bumped.
*/
struct easelcomm_message_metadata *easelcomm_find_remote_message(
struct easelcomm_service *service, easelcomm_msgid_t message_id)
{
struct easelcomm_message_metadata *msg_metadata = NULL;
struct easelcomm_message_metadata *msg_cursor;
mutex_lock(&service->lock);
list_for_each_entry(msg_cursor, &service->remote_list, list) {
if (msg_cursor->msg->desc.message_id == message_id) {
msg_metadata = msg_cursor;
msg_metadata->reference_count++;
break;
}
}
mutex_unlock(&service->lock);
return msg_metadata;
}
EXPORT_SYMBOL(easelcomm_find_remote_message);
/*
* Return a pointer to the reply message for the specified original message.
* Responsibility for the existing reference count held for the reply message
* (due to the linkage from the original message) now transfers to the caller,
* which must drop the reference when appropriate.
*/
static struct easelcomm_message_metadata *easelcomm_grab_reply_message(
struct easelcomm_service *service,
struct easelcomm_message_metadata *orig_msg_metadata)
{
struct easelcomm_message_metadata *reply_msg_metadata;
if (!orig_msg_metadata)
return NULL;
mutex_lock(&service->lock);
reply_msg_metadata = orig_msg_metadata->reply_metadata;
orig_msg_metadata->reply_metadata = NULL;
mutex_unlock(&service->lock);
return reply_msg_metadata;
}
/*
* Add a new message and metadata for the specified service. Depending on the
* message type, the message will be placed in the local or remote list and
* potentially the receiveMessage queue. The reference count for the message
* starts at 1 due to the returned pointer.
*/
static struct easelcomm_message_metadata *easelcomm_add_metadata(
struct easelcomm_service *service,
enum easelcomm_msg_type msg_type, struct easelcomm_kmsg *msg)
{
struct easelcomm_message_metadata *msg_metadata =
kmalloc(sizeof(struct easelcomm_message_metadata), GFP_KERNEL);
if (!msg_metadata)
return NULL;
msg_metadata->msg_type = msg_type;
INIT_LIST_HEAD(&msg_metadata->list);
msg_metadata->dma_xfer.sg_local = NULL;
msg_metadata->dma_xfer.sg_local_size = 0;
msg_metadata->dma_xfer.sg_local_localdata = NULL;
msg_metadata->dma_xfer.sg_remote = NULL;
msg_metadata->dma_xfer.sg_remote_size = 0;
init_completion(&msg_metadata->dma_xfer.sg_remote_ready);
init_completion(&msg_metadata->dma_xfer.xfer_ready);
init_completion(&msg_metadata->dma_xfer.xfer_done);
msg_metadata->dma_xfer.aborting = false;
msg_metadata->msg = msg;
msg_metadata->reference_count = 1;
msg_metadata->queued = false;
msg_metadata->flushing = false;
msg_metadata->free_message = false;
init_completion(&msg_metadata->reply_received);
msg_metadata->reply_metadata = NULL;
mutex_lock(&service->lock);
switch (msg_metadata->msg_type) {
case TYPE_LOCAL:
/* add to the local list */
list_add_tail(&msg_metadata->list, &service->local_list);
break;
case TYPE_REMOTE_REPLY:
case TYPE_REMOTE_NONREPLY:
/* add to the remote list */
list_add_tail(&msg_metadata->list, &service->remote_list);
if (msg_metadata->msg_type == TYPE_REMOTE_NONREPLY) {
/*
* Also add to the receiveMessage queue and wakeup
* waiter.
*/
list_add_tail(&msg_metadata->rcvq_list,
&service->receivemsg_queue);
msg_metadata->queued = true;
complete(&service->receivemsg_queue_new);
}
break;
default:
WARN_ON(1);
}
mutex_unlock(&service->lock);
return msg_metadata;
}
/*
* Free message, remove from lists and receiveMessage queue. Called when
* message reference count drops to zero, and either: the message is marked
* for freeing when no longer referenced, or: the message is being flushed.
* The lock for the containing service must be held. This method is only for
* use by reference counting and message flushing routines, not for general use.
*/
static void easelcomm_free_message(
struct easelcomm_service *service,
struct easelcomm_message_metadata *msg_metadata)
{
if (!msg_metadata)
return;
WARN_ON(msg_metadata->reference_count);
/* remove from local or remote list */
list_del(&msg_metadata->list);
/* If queued remove from receive queue */
if (msg_metadata->queued)
list_del(&msg_metadata->rcvq_list);
kfree(msg_metadata->msg);
vfree(msg_metadata->dma_xfer.sg_local); /* was allocated by vmalloc */
kfree(msg_metadata->dma_xfer.sg_remote);
kfree(msg_metadata);
}
/*
* Decrement message refcount, optionally mark for freeing. The message is
* freed if it is now, or has previously been, marked for freeing when
* unreferenced and the reference count is dropped to zero. Once this method
* is called the supplied message pointer is no longer valid and must not be
* dereferenced. Any code that needs to reference the same message again
* needs to find it by message ID.
*
* A reference count must be held whenever a pointer to the message is created
* and valid (apart from local or remote message list linkage, which is always
* present until the message is freed). (And except briefly while maintaining
* message state and the service lock is held.) A reference count of zero
* indicates the message is not actively being manipulated by a kernel code
* path and no pointers to it existing apart from the local or remote message
* list for its associated service -- if not in the process of being freed then
* it is expected something will later grab a reference to the message and
* resume handling it.
*/
void easelcomm_drop_reference(
struct easelcomm_service *service,
struct easelcomm_message_metadata *msg_metadata, bool mark_free)
{
if (!msg_metadata)
return;
mutex_lock(&service->lock);
if (!WARN_ON(!msg_metadata->reference_count))
msg_metadata->reference_count--;
if (mark_free)
msg_metadata->free_message = true;
if (!msg_metadata->reference_count && msg_metadata->free_message)
easelcomm_free_message(service, msg_metadata);
mutex_unlock(&service->lock);
}
EXPORT_SYMBOL(easelcomm_drop_reference);
/*
* Attempt to flush a message. If a reference count is held on the message or
* the DMA transfer for the message is in progress then the message is not
* flushed. If a DMA transfer is active then a DMA abort is initiated.
* Function returns true if need to retry flushing the message later due to
* the above, or false if now flushed.
*/
static bool easelcomm_flush_message(
struct easelcomm_service *service,
struct easelcomm_message_metadata *msg_metadata)
{
easelcomm_msgid_t msgid;
bool ret = false;
if (!msg_metadata || !msg_metadata->msg)
return false;
msgid = msg_metadata->msg->desc.message_id;
/* Mark message for flushing in case message is still in progress */
msg_metadata->flushing = true;
/*
* If we may be working on a DMA transfer then trigger an abort. If
* a local process is working on it then a refcount will be held, and
* we'll need to retry after dropping the mutex, letting the
* process handle the DMA abort and then drop the refcount.
*/
if (msg_metadata->msg->desc.dma_buf_size &&
!msg_metadata->dma_xfer.aborting) {
dev_dbg(easelcomm_miscdev.this_device,
"flush abort DMA msg %u:%s%llu\n",
service->service_id,
easelcomm_msgid_prefix(msg_metadata), msgid);
msg_metadata->dma_xfer.aborting = true;
complete(&msg_metadata->dma_xfer.sg_remote_ready);
complete(&msg_metadata->dma_xfer.xfer_ready);
complete(&msg_metadata->dma_xfer.xfer_done);
}
if (msg_metadata->reference_count) {
dev_dbg(easelcomm_miscdev.this_device,
"not flushing msg %u:%s%llu refcnt=%d\n",
service->service_id,
easelcomm_msgid_prefix(msg_metadata), msgid,
msg_metadata->reference_count);
ret = true;
} else {
easelcomm_free_message(service, msg_metadata);
dev_dbg(easelcomm_miscdev.this_device,
"flushed msg %u:%s%llu\n",
service->service_id,
easelcomm_msgid_prefix(msg_metadata), msgid);
ret = false;
}
return ret;
}
/* Flush all messages (local or remote) kept locally for the service. */
static void easelcomm_flush_local_service(struct easelcomm_service *service)
{
struct easelcomm_message_metadata *msg_cursor;
struct easelcomm_message_metadata *msg_temp;
int i;
bool need_retry = true;
#define MAX_FLUSH_TRIES 4
for (i = 0; need_retry && i < MAX_FLUSH_TRIES; i++) {
mutex_lock(&service->lock);
need_retry = false;
list_for_each_entry_safe(
msg_cursor, msg_temp, &service->local_list, list) {
need_retry |= easelcomm_flush_message(
service, msg_cursor);
}
list_for_each_entry_safe(
msg_cursor, msg_temp, &service->remote_list, list) {
need_retry |= easelcomm_flush_message(
service, msg_cursor);
}
mutex_unlock(&service->lock);
if (!need_retry)
break;
/* sleep, let refcount holders abort DMA/reply wait */
msleep(MSG_FLUSH_RETRY_DELAY);
}
if (need_retry)
dev_err(easelcomm_miscdev.this_device,
"svc %u failed to flush messages after %d tries\n",
service->service_id, MAX_FLUSH_TRIES);
}
/*
* Wake up any message of a service waiting for reply.
*/
static void __force_complete_reply_waiter(struct easelcomm_service *service)
{
struct easelcomm_message_metadata *msg_cursor;
mutex_lock(&service->lock);
list_for_each_entry(msg_cursor, &service->local_list, list) {
if (msg_cursor->msg->desc.need_reply)
complete(&msg_cursor->reply_received);
}
mutex_unlock(&service->lock);
}
/*
* Handle local-side processing of shutting down an Easel service. Easel
* services are shutdown when the local side handling process closes its fd
* or issues the shutdown ioctl, or the remote side sends a CLOSE_SERVICE
* command (meaning the remote side is doing one of those things).
*
* Flush local state if local side initiated the shutdown, wakeup the
* receiveMessage() waiter.
*/
static void easelcomm_handle_service_shutdown(
struct easelcomm_service *service, bool shutdown_local)
{
dev_dbg(easelcomm_miscdev.this_device, "svc %u shutdown from %s\n",
service->service_id, shutdown_local ? "local" : "remote");
/*
* If local side shutting down then flush local state, just in case.
* If remote is closing then local side may still have messages in
* progress, just signal the receivemessage() caller to return
* shutdown status.
*/
if (shutdown_local)
easelcomm_flush_local_service(service);
mutex_lock(&service->lock);
if (shutdown_local)
service->shutdown_local = true;
else
service->shutdown_remote = true;
mutex_unlock(&service->lock);
/* Wakeup any receiveMessage() waiter so they can return to user */
complete(&service->receivemsg_queue_new);
/* Wakeup flush_done waiter so they can return to user */
complete(&service->flush_done);
/* Wakeup any sendMessageReceiveReply() waiter to return to user */
__force_complete_reply_waiter(service);
}
/*
* Shutdown easelcomm local activity, mark link down.
*/
static void easelcomm_stop_local(void)
{
int i;
if (!easelcomm_up)
return;
dev_dbg(easelcomm_miscdev.this_device, "stopping\n");
/* Set local shutdown flag, disallow further activity */
easelcomm_up = false;
for (i = 0; i < EASELCOMM_SERVICE_COUNT; i++) {
struct easelcomm_service *service = easelcomm_service_list[i];
if (service != NULL)
easelcomm_handle_service_shutdown(service, true);
}
}
/*
* Shutdown easelcomm communications. async is true if emergency shutdown (on
* panic), don't wait for sync from other side.
*/
static void easelcomm_stop(bool async)
{
int ret;
if (!easelcomm_up)
return;
easelcomm_send_cmd_noargs(
&link_service,
EASELCOMM_CMD_LINK_SHUTDOWN);
if (!async) {
ret = wait_for_completion_interruptible_timeout(
&easelcomm_link_peer_shutdown,
msecs_to_jiffies(LINK_SHUTDOWN_ACK_TIMEOUT));
if (ret <= 0)
dev_warn(easelcomm_miscdev.this_device,
"error or timeout on peer link shutdown ack\n");
}
easelcomm_stop_local();
}
/*
* LINK_SHUTDOWN command received from remote, stop local side and send
* ACK_SHUTDOWN back to remote.
*/
static void easelcomm_handle_cmd_link_shutdown(void)
{
dev_dbg(easelcomm_miscdev.this_device,
"recv cmd LINK_SHUTDOWN\n");
easelcomm_stop_local();
dev_dbg(easelcomm_miscdev.this_device,
"send cmd ACK_SHUTDOWN\n");
easelcomm_send_cmd_noargs(
&link_service,
EASELCOMM_CMD_ACK_SHUTDOWN);
}
/* ACK_SHUTDOWN command received from remote, wakeup waiter. */
static void easelcomm_handle_cmd_ack_shutdown(void)
{
dev_dbg(easelcomm_miscdev.this_device,
"recv cmd ACK_SHUTDOWN\n");
complete(&easelcomm_link_peer_shutdown);
}
/*
* Handle SEND_MSG command from remote. Add a remote message, link a reply
* to original message and wakeup waiter.
*/
static void easelcomm_handle_cmd_send_msg(
struct easelcomm_service *service, char *command_args,
size_t command_arg_len)
{
struct easelcomm_kmsg *cmd_msg;
struct easelcomm_kmsg *new_msg;
enum easelcomm_msg_type msg_type;
struct easelcomm_message_metadata *msg_metadata;
bool discard_message = false;
if (WARN_ON(command_arg_len < sizeof(struct easelcomm_kmsg_desc)))
return;
cmd_msg = (struct easelcomm_kmsg *) command_args;
if (WARN_ON(command_arg_len != sizeof(struct easelcomm_kmsg_desc) +
cmd_msg->desc.message_size))
return;
dev_dbg(easelcomm_miscdev.this_device,
"recv cmd SEND_MSG msg %u:r%llu\n",
service->service_id, cmd_msg->desc.message_id);
new_msg = kmalloc(command_arg_len, GFP_KERNEL);
if (WARN_ON(!new_msg))
return;
memcpy(new_msg, cmd_msg, command_arg_len);
msg_type = new_msg->desc.in_reply_to ?
TYPE_REMOTE_REPLY : TYPE_REMOTE_NONREPLY;
msg_metadata = easelcomm_add_metadata(service, msg_type, new_msg);
if (WARN_ON(!msg_metadata)) {
kfree(new_msg);
return;
}
easelcomm_dump_message(msg_metadata);
/* If a reply then link to the original message and wakeup waiter. */
if (msg_type == TYPE_REMOTE_REPLY) {
struct easelcomm_message_metadata *orig_msg_metadata =
easelcomm_find_local_message(
service, msg_metadata->msg->desc.in_reply_to);
if (orig_msg_metadata) {
orig_msg_metadata->reply_metadata =
msg_metadata;
/*
* Bump reference count for this new ref, waiter will
* drop.
*/
easelcomm_grab_reference(service, msg_metadata);
complete(&orig_msg_metadata->reply_received);
easelcomm_drop_reference(
service, orig_msg_metadata, false);
} else {
dev_dbg(easelcomm_miscdev.this_device,
"reply msg %u:r%llu reply-to msg %u:l%llu not found\n",
service->service_id,
msg_metadata->msg->desc.message_id,
service->service_id,
msg_metadata->msg->desc.in_reply_to);
discard_message = true;
}
}
easelcomm_drop_reference(service, msg_metadata, discard_message);
}
/*
* Remote side of link indicates command channel ready. Both server and
* client call this when handling any command.
* In addition, server calls this when a LINK_INIT command is received from
* the client.
*
* Init stuff and wakeup anybody waiting for the channel to be ready (to send
* a command).
*/
static void easelcomm_cmd_channel_remote_set_ready(void)
{
struct easelcomm_cmd_channel_remote *channel =
&cmd_channel_remote;
mutex_lock(&channel->mutex);
/* Return right away if channel is already initialized */
if (channel->initialized) {
mutex_unlock(&channel->mutex);
return;
}
/* Next write offset starts at top after header, cmd seq# zero */
channel->write_offset =
(sizeof(struct easelcomm_cmd_channel_header) + 7) & ~0x7;
channel->write_seqnbr = 0;
/* mark channel initialized, wakeup waiters. */
channel->initialized = true;
complete_all(&channel->init_state_changed);
easelcomm_up = true;
mutex_unlock(&channel->mutex);
}
/*
* LINK_INIT command received from client, init local state for remote command
* channel.
*/
static void easelcomm_handle_cmd_link_init(
char *command_args, size_t command_arg_len)
{
dev_dbg(easelcomm_miscdev.this_device, "recv cmd LINK_INIT\n");
easelcomm_cmd_channel_remote_set_ready();
}
/*
* Wait for remote command channel ready and initialized, prior to sending a
* command. Returns 0 if command channel ready and channel mutex is held, or
* non-zero if signal received while waiting, channel mutex not held.
*/
static int easelcomm_wait_channel_initialized(
struct easelcomm_cmd_channel_remote *channel)
{
int ret = 0;
mutex_lock(&channel->mutex);
while (!channel->initialized) {
mutex_unlock(&channel->mutex);
ret = wait_for_completion_interruptible(
&channel->init_state_changed);
if (ret)
return ret;
mutex_lock(&channel->mutex);
}
return ret;
}
/* CLOSE_SERVICE command received from remote, shut down service. */
static void easelcomm_handle_cmd_close_service(
struct easelcomm_service *service)
{
dev_dbg(easelcomm_miscdev.this_device,
"recv cmd CLOSE_SERVICE svc %u\n",
service->service_id);
easelcomm_handle_service_shutdown(service, false);
}
/*
* FLUSH_SERVICE command received from remote, flush local state for service
* and send FLUSH_SERVICE_DONE back to remote.
*/
static void easelcomm_handle_cmd_flush_service(
struct easelcomm_service *service)
{
dev_dbg(easelcomm_miscdev.this_device,
"recv cmd FLUSH_SERVICE svc %u\n",
service->service_id);
easelcomm_flush_local_service(service);
dev_dbg(easelcomm_miscdev.this_device,
"send cmd FLUSH_SERVICE_DONE svc %u\n",
service->service_id);
easelcomm_send_cmd_noargs(service, EASELCOMM_CMD_FLUSH_SERVICE_DONE);
}
/* FLUSH_SERVICE_DONE command received from remote, wakeup waiter. */
static void easelcomm_handle_cmd_flush_service_done(
struct easelcomm_service *service)
{
dev_dbg(easelcomm_miscdev.this_device,
"recv cmd FLUSH_SERVICE_DONE svc %u\n", service->service_id);
complete(&service->flush_done);
}
static void easelcomm_init_service(struct easelcomm_service *service, int id)
{
if (service == NULL) {
dev_err(easelcomm_miscdev.this_device, "service is null\n");
return;
}
service->service_id = id;
service->user = NULL;
service->shutdown_local = false;
service->shutdown_remote = false;
mutex_init(&service->lock);
INIT_LIST_HEAD(&service->local_list);
INIT_LIST_HEAD(&service->receivemsg_queue);
init_completion(&service->receivemsg_queue_new);
INIT_LIST_HEAD(&service->remote_list);
init_completion(&service->flush_done);
}
/* Caller of this function must hold service_mutex. */
static struct easelcomm_service *easelcomm_create_service(int id)
{
struct easelcomm_service *service;
if (easelcomm_service_list[id] != NULL)
return easelcomm_service_list[id];
service = kzalloc(sizeof(struct easelcomm_service), GFP_KERNEL);
if (service == NULL) {
dev_err(easelcomm_miscdev.this_device,
"could not allocate service %u\n", id);
return NULL;
}
easelcomm_init_service(service, id);
easelcomm_service_list[id] = service;
return service;
}
/* Command received from remote, dispatch. */
static void easelcomm_handle_command(struct easelcomm_cmd_header *cmdhdr)
{
struct easelcomm_service *service;
char *cmdargs = (char *)cmdhdr + sizeof(struct easelcomm_cmd_header);
/*
* Any command can inform the server that remote is ready, not just
* LINK_INIT.
*/
if (!easelcomm_is_client())
easelcomm_cmd_channel_remote_set_ready();
switch(cmdhdr->command_code) {
case EASELCOMM_CMD_LINK_INIT:
easelcomm_handle_cmd_link_init(
cmdargs, cmdhdr->command_arg_len);
return;
case EASELCOMM_CMD_LINK_SHUTDOWN:
easelcomm_handle_cmd_link_shutdown();
return;
case EASELCOMM_CMD_ACK_SHUTDOWN:
easelcomm_handle_cmd_ack_shutdown();
return;
default:
break;
}
if (cmdhdr->service_id >= EASELCOMM_SERVICE_COUNT) {
dev_err(easelcomm_miscdev.this_device,
"invalid service ID %u received\n",
cmdhdr->service_id);
return;
}
service = easelcomm_service_list[cmdhdr->service_id];
/*
* If service has not been initialized,
* initialize the service here without userspace ownership.
* Userspace ownership could be claimed later.
*/
if (service == NULL) {
mutex_lock(&service_mutex);
service = easelcomm_create_service(cmdhdr->service_id);
mutex_unlock(&service_mutex);
if (service == NULL) {
dev_err(easelcomm_miscdev.this_device,
"could not handle cmd %d for service %u\n",
cmdhdr->command_code, cmdhdr->service_id);
return;
}
}
switch (cmdhdr->command_code) {
case EASELCOMM_CMD_SEND_MSG:
easelcomm_handle_cmd_send_msg(
service, cmdargs, cmdhdr->command_arg_len);
break;
case EASELCOMM_CMD_DMA_SG:
easelcomm_handle_cmd_dma_sg(
service, cmdargs, cmdhdr->command_arg_len);
break;
case EASELCOMM_CMD_DMA_XFER:
easelcomm_handle_cmd_dma_xfer(
service, cmdargs, cmdhdr->command_arg_len);
break;
case EASELCOMM_CMD_DMA_DONE:
easelcomm_handle_cmd_dma_done(
service, cmdargs, cmdhdr->command_arg_len);
break;
case EASELCOMM_CMD_FLUSH_SERVICE:
easelcomm_handle_cmd_flush_service(service);
break;
case EASELCOMM_CMD_FLUSH_SERVICE_DONE:
easelcomm_handle_cmd_flush_service_done(service);
break;
case EASELCOMM_CMD_CLOSE_SERVICE:
easelcomm_handle_cmd_close_service(service);
break;
default:
dev_err(easelcomm_miscdev.this_device,
"svc %u invalid command code %u received\n",
cmdhdr->service_id, cmdhdr->command_code);
}
}
/*
* Bump the consumer sequence number in the local command channel state,
* indicating that we've consumed messages prior to the new number in
* sequence. The sequence number cannot be the reserved "command buffer
* wrapped" marker; bump twice if needed to avoid.
*
* Not locked, but there's only one thread handling local command channel
* data, in worker context.
*/
static void easelcomm_cmd_channel_bump_consumer_seqnbr(
struct easelcomm_cmd_channel_local *channel)
{
channel->consumer_seqnbr_next++;
if (channel->consumer_seqnbr_next == CMD_BUFFER_WRAP_MARKER)
channel->consumer_seqnbr_next++;
}
/*
* Interrupt for new local command channel data ready was received. This
* function is called on a workqueue worker. Process any new command channel
* data found.
*
* Not locked, but there's only one thread handling local command channel
* data, in worker context.
*/
void easelcomm_cmd_channel_data_handler(void)
{
struct easelcomm_cmd_channel_local *channel =
&cmd_channel_local;
struct easelcomm_cmd_channel_header *channel_buf_hdr =
(struct easelcomm_cmd_channel_header *)
channel->buffer;
/* While we haven't caught up to producer in sequence #s processed. */
while (channel->consumer_seqnbr_next !=
channel_buf_hdr->producer_seqnbr_next) {
struct easelcomm_cmd_header *cmdhdr =
(struct easelcomm_cmd_header *)channel->readp;
uint32_t saved_cmd_len;
dev_dbg(easelcomm_miscdev.this_device, "cmdchan consumer loop prodseq=%llu consseq=%llu off=%lx\n",
channel_buf_hdr->producer_seqnbr_next,
channel->consumer_seqnbr_next,
channel->readp - channel->buffer);
/*
* If producer dropped a wrap marker at the current position
* then wrap our read pointer and let the producer know we
* wrapped and both sides are ready to continue at the top of
* the buffer.
*/
if (cmdhdr->sequence_nbr == CMD_BUFFER_WRAP_MARKER) {
dev_dbg(easelcomm_miscdev.this_device,
"cmdchan consumer wrap at off=%lx\n",
channel->readp - channel->buffer);
channel->readp =
channel->buffer +
sizeof(struct easelcomm_cmd_channel_header);
/* Send consumer wrapped IRQ to remote */
easelcomm_hw_send_wrap_interrupt();
/* Wrapping consumes a seqnbr */
easelcomm_cmd_channel_bump_consumer_seqnbr(channel);
continue;
}
saved_cmd_len = READ_ONCE(cmdhdr->command_arg_len);
dev_dbg(easelcomm_miscdev.this_device,
"cmdchan recv cmd seq=%llu svc=%u cmd=%u len=%u off=%lx\n",
cmdhdr->sequence_nbr, cmdhdr->service_id,
cmdhdr->command_code, saved_cmd_len,
channel->readp - channel->buffer);
if (sizeof(struct easelcomm_cmd_header) +
saved_cmd_len >
EASELCOMM_CMD_CHANNEL_SIZE) {
dev_err(easelcomm_miscdev.this_device,
"command channel corruption detected: seq=%llu svc=%u cmd=%u len=%u off=%lx\n",
cmdhdr->sequence_nbr, cmdhdr->service_id,
cmdhdr->command_code, saved_cmd_len,
channel->readp - channel->buffer);
break;
}
if (cmdhdr->sequence_nbr !=
channel->consumer_seqnbr_next) {
dev_err(easelcomm_miscdev.this_device,
"command channel corruption detected: expected seq# %llu, got %llu\n",
channel->consumer_seqnbr_next,
cmdhdr->sequence_nbr);
break;
}
/* Process the command. */
easelcomm_handle_command(cmdhdr);
/* Post-process double check */
if (saved_cmd_len != cmdhdr->command_arg_len) {
dev_err(easelcomm_miscdev.this_device,
"command channel corruption detected: off=%lx expected len %u got %u\n",
channel->readp - channel->buffer,
saved_cmd_len, cmdhdr->command_arg_len);
break;
}
channel->readp += sizeof(struct easelcomm_cmd_header) +
saved_cmd_len;
/* 8-byte-align next entry pointer */
if ((uintptr_t)channel->readp & 0x7)
channel->readp += 8 - ((uintptr_t)channel->readp & 0x7);
easelcomm_cmd_channel_bump_consumer_seqnbr(channel);
}
dev_dbg(easelcomm_miscdev.this_device, "cmdchan consumer exiting prodseq=%llu consseq=%llu off=%lx\n",
channel_buf_hdr->producer_seqnbr_next,
channel->consumer_seqnbr_next,
channel->readp - channel->buffer);
}
EXPORT_SYMBOL(easelcomm_cmd_channel_data_handler);
/*
* Interrupt from remote indicating remote has followed our channel buffer
* wrap received. This function called in workqueue worker context. Wakeup
* the command producer that is waiting on the remote to follow the wrap.
*/
void easelcomm_cmd_channel_wrap_handler(void)
{
struct easelcomm_cmd_channel_remote *channel =
&cmd_channel_remote;
dev_dbg(easelcomm_miscdev.this_device,
"cmdchan remote wrap IRQ received\n");
complete(&channel->wrap_ready);
}
EXPORT_SYMBOL(easelcomm_cmd_channel_wrap_handler);
/* Device file open. Allocate a user state structure. */
static int easelcomm_open(struct inode *inode, struct file *file)
{
struct easelcomm_user_state *user_state;
if (!easelcomm_up)
return -ENODEV;
user_state = kzalloc(sizeof(struct easelcomm_user_state), GFP_KERNEL);
if (!user_state)
return -ENOMEM;
file->private_data = user_state;
return 0;
}
/* Initiate service shutdown from fd release or ioctl. */
static void easelcomm_initiate_service_shutdown(
struct easelcomm_service *service)
{
bool already_shutdown;
mutex_lock(&service->lock);
already_shutdown = service->shutdown_local;
mutex_unlock(&service->lock);
if (already_shutdown) {
dev_dbg(easelcomm_miscdev.this_device,
"svc %u already shutdown\n",
service->service_id);
return;
}
dev_dbg(easelcomm_miscdev.this_device, "svc %u initiate shutdown\n",
service->service_id);
easelcomm_handle_service_shutdown(service, true);
dev_dbg(easelcomm_miscdev.this_device,
"svc %u send cmd CLOSE_SERVICE\n", service->service_id);
easelcomm_send_cmd_noargs(service, EASELCOMM_CMD_CLOSE_SERVICE);
}
/* Device file descriptor release, initiate service shutdown and cleanup */
static int easelcomm_release(struct inode *inode, struct file *file)
{
struct easelcomm_user_state *user_state = file->private_data;
if (user_state) {
struct easelcomm_service *service = user_state->service;
if (service) {
dev_dbg(easelcomm_miscdev.this_device,
"svc %u release\n", service->service_id);
if (easelcomm_up)
easelcomm_initiate_service_shutdown(service);
mutex_lock(&service->lock);
service->user = NULL;
mutex_unlock(&service->lock);
}
kfree(file->private_data);
file->private_data = NULL;
}
return 0;
}
/*
* Bump the producer next sequence number in the remote command channel buffer
* header. After this, the remote side can then see that entries prior to
* that sequence number are present. The sequence number cannot be the
* reserved "command buffer wrapped" marker; bump twice to avoid if needed.
*
* Call with remote channel producer mutex held.
*/
static int easelcomm_cmd_channel_bump_producer_seqnbr(
struct easelcomm_cmd_channel_remote *channel)
{
int ret;
channel->write_seqnbr++;
if (channel->write_seqnbr == CMD_BUFFER_WRAP_MARKER)
channel->write_seqnbr++;
ret = easelcomm_hw_remote_write(
&channel->write_seqnbr, sizeof(uint64_t),
(uint64_t)EASELCOMM_CMDCHAN_PRODUCER_SEQNBR);
return ret;
}
/*
* Wrap around remote's buffer. If producer does not receive a signal that
* consumer has wrapped around within predefined time, it will retry sending
* wrap command if enabled.
*
* Note: this retry mechanism is done by writing the wrap marker to the top
* of remote's cmd channel, which could, in the worst case, overwrite the
* unconsumed command at the top of cmd channel, causing all unconsumed
* commands behind it to be discarded.
*
* Called with remote channel mutex held.
* Caller is responsible for releasing the mutex.
*
* @channel: the pointer to remote channel
*
* @Return: 0 - sent wrap and remote completed wrapping
* negative - on error
*/
static int easelcomm_cmd_channel_send_wrap(
struct easelcomm_cmd_channel_remote *channel)
{
int ret = 0;
int attempted = 0;
long remaining;
uint64_t wrap_marker = CMD_BUFFER_WRAP_MARKER;
{
if (attempted > 0) {
dev_warn(easelcomm_miscdev.this_device,
"%s: retrying after %d attempts",
__func__, attempted);
}
attempted++;
/* Write the "buffer wrapped" marker in place of sequence # */
dev_dbg(easelcomm_miscdev.this_device, "cmdchan producer wrap at off=%llx seq=%llu\n",
channel->write_offset, channel->write_seqnbr);
ret = easelcomm_hw_remote_write(
&wrap_marker, sizeof(wrap_marker),
channel->write_offset);
if (ret)
goto exit;
/*
* Bump the producer seqnbr for the wrap marker (so consumer
* knows its there) and send IRQ to remote to consume the new
* data.
*
* Consumer will process entries up to and including the wrap
* marker and then send a "wrapped" interrupt that wakes up
* the wrap_ready completion.
*/
ret = easelcomm_cmd_channel_bump_producer_seqnbr(channel);
if (ret)
goto exit;
ret = easelcomm_hw_send_data_interrupt();
if (ret)
goto exit;
channel->write_offset =
(sizeof(struct easelcomm_cmd_channel_header) + 7)
& ~0x7;
/* Wait for remote to catch up. IRQ from remote wakes this. */
remaining = wait_for_completion_interruptible_timeout(
&channel->wrap_ready,
msecs_to_jiffies(CMDCHAN_WRAP_DONE_TIMEOUT_MS));
if (remaining > 0) {
ret = 0;
dev_dbg(easelcomm_miscdev.this_device,
"cmdchan wrap completed, new off=%llx\n",
channel->write_offset);
goto exit;
}
if (remaining < 0) {
/* waiting was interrupted */
dev_err(easelcomm_miscdev.this_device,
"remote channel did not catch up: reason interrupted off=%llx seq=%llu\n",
channel->write_offset, channel->write_seqnbr);
ret = remaining;
goto exit;
}
/* remaining jiffies is 0; timed out */
dev_err(easelcomm_miscdev.this_device,
"remote channel did not catch up: reason timeout off=%llx seq=%llu\n",
channel->write_offset, channel->write_seqnbr);
ret = -ETIMEDOUT;
}
exit:
return ret;
}
/*
* Start the process of writing a new command to the remote command channel.
* Wait for channel init'ed and grab the mutex. Wraparound the buffer if
* needed. Write the command header.
* If no error then return zero and channel mutex is locked on return.
* If error then return non-zero and channel mutex is unlocked.
*/
int easelcomm_start_cmd(
struct easelcomm_service *service, int command_code,
size_t command_arg_len)
{
struct easelcomm_cmd_channel_remote *channel =
&cmd_channel_remote;
struct easelcomm_cmd_header cmdhdr;
size_t cmdbuf_size =
sizeof(struct easelcomm_cmd_header) + command_arg_len;
int ret;
/*
* If never enough room for the command plus an 8-byte wrap marker,
* bail.
*/
if (cmdbuf_size > EASELCOMM_CMD_CHANNEL_SIZE - 8 -
sizeof(struct easelcomm_cmd_channel_header))
return -EINVAL;
ret = easelcomm_wait_channel_initialized(channel);
/* If no error then channel->mutex is locked */
if (ret)
return ret;
/* Choose a spot for the new entry, wrap around if needed. */
if (channel->write_offset + cmdbuf_size >
EASELCOMM_CMD_CHANNEL_SIZE - 8 -
sizeof(struct easelcomm_cmd_channel_header)) {
ret = easelcomm_cmd_channel_send_wrap(channel);
if (ret)
goto error;
}
dev_dbg(easelcomm_miscdev.this_device,
"cmdchan producer sending cmd seq#%llu svc=%u cmd=%u arglen=%zu off=%llx\n",
channel->write_seqnbr, service->service_id, command_code,
command_arg_len, channel->write_offset);
cmdhdr.service_id = service->service_id;
cmdhdr.sequence_nbr = channel->write_seqnbr;
cmdhdr.command_code = command_code;
cmdhdr.command_arg_len = (uint32_t)command_arg_len;
/*
* Send the command header. Subsequent calls to
* easelcomm_append_cmd_args() and easelcomm_send_cmd() will finish
* the in-progress command.
*/
ret = easelcomm_hw_remote_write(
&cmdhdr, sizeof(cmdhdr), channel->write_offset);
if (ret)
goto error;
/* Advance write offset */
channel->write_offset += sizeof(cmdhdr);
return 0;
error:
mutex_unlock(&channel->mutex);
return ret;
}
EXPORT_SYMBOL(easelcomm_start_cmd);
/*
* Append arguments to the in-progress command being sent.
* Call with remote channel producer mutex held.
*/
int easelcomm_append_cmd_args(
struct easelcomm_service *service, void *cmd_args,
size_t cmd_args_len)
{
struct easelcomm_cmd_channel_remote *channel =
&cmd_channel_remote;
int ret = easelcomm_hw_remote_write(
cmd_args, cmd_args_len, channel->write_offset);
if (ret) {
mutex_unlock(&channel->mutex);
return ret;
}
channel->write_offset += cmd_args_len;
return 0;
}
EXPORT_SYMBOL(easelcomm_append_cmd_args);
/* Start and send a command with no arguments. */
int easelcomm_send_cmd_noargs(
struct easelcomm_service *service, int command_code)
{
int ret;
ret = easelcomm_start_cmd(service, command_code, 0);
if (ret)
return ret;
return easelcomm_send_cmd(service);
}
/*
* Finish sending an in-progress command to the remote.
*/
int easelcomm_send_cmd(struct easelcomm_service *service)
{
struct easelcomm_cmd_channel_remote *channel =
&cmd_channel_remote;
int ret;
/*
* Bump the producer sequence number. Remote may now see and process
* the new entry as soon as this value is visible to it.
*/
ret = easelcomm_cmd_channel_bump_producer_seqnbr(channel);
if (ret)
goto out;
/* Send data ready IRQ to remote */
ret = easelcomm_hw_send_data_interrupt();
if (ret)
goto out;
/* Bump write offset for next command, 8-byte-integer-aligned */
if (channel->write_offset & 0x7)
channel->write_offset += 8 - (channel->write_offset & 0x7);
out:
mutex_unlock(&channel->mutex);
return ret;
}
EXPORT_SYMBOL(easelcomm_send_cmd);
/*
* Return next message ID in sequence for this service. Message ID zero is
* not used, bump twice to avoid if needed.
*/
static easelcomm_msgid_t easelcomm_next_msgid(
struct easelcomm_service *service)
{
easelcomm_msgid_t next_id;
mutex_lock(&service->lock);
next_id = ++service->next_id;
if (!next_id)
next_id = ++service->next_id;
mutex_unlock(&service->lock);
return next_id;
}
/*
* Handle SENDMSG ioctl, the message descriptor send part of easelcomm
* sendMessage*() and sendReply() calls.
*/
static int easelcomm_send_message_ioctl(
struct easelcomm_service *service,
struct easelcomm_kmsg_desc __user *pmsg_desc)
{
struct easelcomm_kmsg_desc *msg_desc;
struct easelcomm_message_metadata *msg_metadata = NULL;
int ret = 0;
msg_desc = kmalloc(sizeof(struct easelcomm_kmsg_desc), GFP_KERNEL);
if (!msg_desc)
return -ENOMEM;
if (copy_from_user(msg_desc, (void __user *) pmsg_desc,
sizeof(struct easelcomm_kmsg_desc))) {
ret = -EFAULT;
goto out_freedesc;
}
if (msg_desc->message_size > EASELCOMM_MAX_MESSAGE_SIZE) {
dev_err(easelcomm_miscdev.this_device,
"%s: svc:%u msg size %d is larger than max size %d\n",
__func__,
service->service_id,
msg_desc->message_size,
EASELCOMM_MAX_MESSAGE_SIZE);
ret = -EINVAL;
goto out_freedesc;
}
/* Assign a message ID to the new message and add to local list. */
msg_desc->message_id = easelcomm_next_msgid(service);
msg_metadata = easelcomm_add_metadata(service, TYPE_LOCAL,
(struct easelcomm_kmsg *)msg_desc);
if (!msg_metadata) {
ret = -ENOMEM;
goto out_freedesc;
}
/*
* After a successful call to the above, msg_desc is pointed to by
* msg_metadata and the memory is managed along with the other
* metadata.
*/
dev_dbg(easelcomm_miscdev.this_device, "SENDMSG msg %u:l%llu\n",
service->service_id, msg_desc->message_id);
easelcomm_dump_message(msg_metadata);
/* Copy the updated msg desc with message ID to user. */
if (copy_to_user((void __user *) pmsg_desc, msg_desc,
sizeof(struct easelcomm_kmsg_desc))) {
easelcomm_drop_reference(service, msg_metadata, true);
return -EFAULT;
}
/*
* If this is a reply then we are done with the replied-to remote
* message.
*/
if (msg_metadata->msg->desc.in_reply_to) {
struct easelcomm_message_metadata *orig_msg_metadata =
easelcomm_find_remote_message(
service, msg_metadata->msg->desc.in_reply_to);
if (!orig_msg_metadata) {
dev_dbg(easelcomm_miscdev.this_device,
"msg %u:l%llu replied-to msg r%llu not found\n",
service->service_id, msg_desc->message_id,
msg_metadata->msg->desc.in_reply_to);
} else {
dev_dbg(easelcomm_miscdev.this_device,
"msg %u:l%llu is reply to msg r%llu\n",
service->service_id, msg_desc->message_id,
msg_metadata->msg->desc.in_reply_to);
easelcomm_drop_reference(
service, orig_msg_metadata, true);
}
}
easelcomm_drop_reference(service, msg_metadata, false);
return 0;
out_freedesc:
kfree(msg_desc);
return ret;
}
/* Handle RECVMSG ioctl / easelcomm receiveMessage() call. */
static int easelcomm_wait_message(
struct easelcomm_service *service,
struct easelcomm_kmsg_desc __user *msgp)
{
struct easelcomm_message_metadata *msg_metadata = NULL;
struct easelcomm_kmsg_desc msg_temp;
int ret = 0;
bool has_timeout = false;
long remaining = 0;
dev_dbg(easelcomm_miscdev.this_device, "WAITMSG svc %u\n",
service->service_id);
if (copy_from_user(&msg_temp, (void __user *) msgp,
sizeof(struct easelcomm_kmsg_desc)))
return -EFAULT;
dev_dbg(easelcomm_miscdev.this_device,
"%s: timeout_ms=%d\n", __func__, msg_temp.wait.timeout_ms);
if (msg_temp.wait.timeout_ms >= 0) {
has_timeout = true;
remaining = (long)msecs_to_jiffies(
(unsigned int)msg_temp.wait.timeout_ms);
dev_dbg(easelcomm_miscdev.this_device, "%s: remaining=%ld\n",
__func__, remaining);
}
while (!msg_metadata) {
mutex_lock(&service->lock);
if (service->shutdown_local ||
(easelcomm_is_client() && service->shutdown_remote)) {
/* Service shutdown initiated locally or remotely. */
dev_dbg(easelcomm_miscdev.this_device,
"WAITMSG svc %u returning shutdown local=%d remote=%d\n",
service->service_id, service->shutdown_local,
service->shutdown_remote);
service->shutdown_remote = false;
mutex_unlock(&service->lock);
return -ESHUTDOWN;
}
/* grab first entry off receiveMessage queue. */
msg_metadata = list_first_entry_or_null(
&service->receivemsg_queue,
struct easelcomm_message_metadata, rcvq_list);
if (msg_metadata)
break;
mutex_unlock(&service->lock);
if (has_timeout) {
remaining = wait_for_completion_interruptible_timeout(
&service->receivemsg_queue_new,
(unsigned long)remaining);
if (remaining == 0)
ret = -ETIMEDOUT;
if (remaining < 0)
ret = remaining;
} else {
ret = wait_for_completion_interruptible(
&service->receivemsg_queue_new);
}
if (ret)
return ret;
}
dev_dbg(easelcomm_miscdev.this_device,
"WAITMSG svc %u returning msg %u:r%llu\n",
service->service_id, service->service_id,
msg_metadata->msg->desc.message_id);
/* remove from receive queue, grab a ref while copy to user */
list_del(&msg_metadata->rcvq_list);
msg_metadata->queued = false;
msg_metadata->reference_count++;
mutex_unlock(&service->lock);
/* Copy the message desc to the caller */
if (copy_to_user(msgp, msg_metadata->msg,
sizeof(struct easelcomm_kmsg_desc)))
ret = -EFAULT;
easelcomm_drop_reference(service, msg_metadata, false);
return ret;
}
/*
* Handle WAITREPLY ioctl / reply waiting part of easelcomm
* sendMessageReceiveReply() call.
*/
static int easelcomm_wait_reply(
struct easelcomm_service *service,
struct easelcomm_kmsg_desc __user *pmsg_desc)
{
struct easelcomm_kmsg_desc orig_msg_desc;
struct easelcomm_message_metadata *orig_msg_metadata;
struct easelcomm_message_metadata *msg_metadata = NULL;
int ret = 0;
bool has_timeout = false;
long remaining = 0;
if (copy_from_user(&orig_msg_desc, (void __user *) pmsg_desc,
sizeof(struct easelcomm_kmsg_desc)))
return -EFAULT;
dev_dbg(easelcomm_miscdev.this_device, "%s: timeout_ms=%d\n",
__func__, orig_msg_desc.wait.timeout_ms);
if (orig_msg_desc.wait.timeout_ms >= 0) {
has_timeout = true;
remaining = (long)msecs_to_jiffies(
(unsigned int)orig_msg_desc.wait.timeout_ms);
dev_dbg(easelcomm_miscdev.this_device, "%s: remaining=%ld\n",
__func__, remaining);
}
/* Grab a reference to the original message. */
orig_msg_metadata = easelcomm_find_local_message(
service, orig_msg_desc.message_id);
if (!orig_msg_metadata) {
dev_dbg(easelcomm_miscdev.this_device,
"WAITREPLY msg %u:l%llu not found\n",
service->service_id, orig_msg_desc.message_id);
return -EINVAL;
}
dev_dbg(easelcomm_miscdev.this_device,
"WAITREPLY msg %u:l%llu\n",
service->service_id, orig_msg_desc.message_id);
if (has_timeout) {
remaining = wait_for_completion_interruptible_timeout(
&orig_msg_metadata->reply_received,
(unsigned long)remaining);
if (remaining == 0)
ret = -ETIMEDOUT;
if (remaining < 0)
ret = remaining;
} else {
ret = wait_for_completion_interruptible(
&orig_msg_metadata->reply_received);
}
if (ret)
goto out;
if (orig_msg_metadata->flushing) {
dev_dbg(easelcomm_miscdev.this_device,
"WAITREPLY msg %u:l%llu flushed\n",
service->service_id, orig_msg_desc.message_id);
goto out;
}
msg_metadata = easelcomm_grab_reply_message(
service, orig_msg_metadata);
/*
* If completion was waken up on receiving the reply,
* msg_metadata really shouldn't be null.
* If completion was waken up on receiving shutdown, we should
* return to user immediately.
*/
if (!msg_metadata) {
dev_err(easelcomm_miscdev.this_device,
"WAITREPLY msg %u:l%llu no matching reply\n",
service->service_id, orig_msg_desc.message_id);
/*
* Here we return -ESHUTDOWN. See above comment and
* easelcomm_handle_service_shutdown().
*/
ret = -ESHUTDOWN;
goto out;
}
/* Copy the reply message descriptor to the caller */
if (copy_to_user(pmsg_desc, &msg_metadata->msg->desc,
sizeof(struct easelcomm_kmsg_desc))) {
ret = -EFAULT;
goto out;
}
dev_dbg(easelcomm_miscdev.this_device,
"WAITREPLY msg %u:l%llu returning msg r%llu\n",
service->service_id, orig_msg_desc.message_id,
msg_metadata->msg->desc.message_id);
ret = 0;
out:
/* Discard reply message if error retrieving it. */
if (msg_metadata)
easelcomm_drop_reference(service, msg_metadata, ret);
/* Always discard the original message, we're done with it. */
easelcomm_drop_reference(service, orig_msg_metadata, true);
return ret;
}
/* Initiate a flush of service on both sides of the link. */
static void easelcomm_flush_service(struct easelcomm_service *service)
{
int ret;
easelcomm_flush_local_service(service);
dev_dbg(easelcomm_miscdev.this_device,
"send cmd FLUSH_SERVICE svc %u\n",
service->service_id);
ret = easelcomm_send_cmd_noargs(service, EASELCOMM_CMD_FLUSH_SERVICE);
if (ret)
return;
ret = wait_for_completion_interruptible_timeout(
&service->flush_done,
msecs_to_jiffies(SERVICE_FLUSH_DONE_TIMEOUT));
if (ret <= 0)
dev_err(easelcomm_miscdev.this_device,
"service flush done wait aborted svc %u\n",
service->service_id);
}
/* Handle REGISTER ioctl, register an open fd for an Easel service. */
static int easelcomm_register(struct easelcomm_user_state *user_state,
unsigned int service_id) {
struct easelcomm_service *service;
if (service_id >= EASELCOMM_SERVICE_COUNT)
return -EINVAL;
mutex_lock(&service_mutex);
service = easelcomm_service_list[service_id];
if (service == NULL) {
service = easelcomm_create_service(service_id);
if (service == NULL) {
mutex_unlock(&service_mutex);
dev_err(easelcomm_miscdev.this_device,
"could not create service %u\n",
service_id);
return -ENOMEM;
}
}
mutex_unlock(&service_mutex);
mutex_lock(&service->lock);
if (service->user) {
mutex_unlock(&service->lock);
return -EBUSY;
}
user_state->service = service;
service->user = user_state;
service->shutdown_local = false;
service->shutdown_remote = false;
mutex_unlock(&service->lock);
dev_dbg(easelcomm_miscdev.this_device, "REGISTER svc %u\n",
service_id);
/*
* New client flushes any messages generated from activity by previous
* clients.
*
* New server handles any client messages that were waiting for the
* server to startup and process, does not flush at start/restart.
* Server can explicitly call the flush ioctl if wanted, but with the
* understanding any clients starting at the same time may have their
* traffic dropped.
*/
if (easelcomm_is_client())
easelcomm_flush_service(service);
return 0;
}
/*
* Handle READDATA ioctl, the read message data part of easelcomm
* receiveMessage() and sendMessageReceiveReply() calls.
*/
static int easelcomm_read_msgdata(
struct easelcomm_service *service,
struct easelcomm_kbuf_desc *buf_desc)
{
struct easelcomm_message_metadata *msg_metadata;
int ret = 0;
dev_dbg(easelcomm_miscdev.this_device,
"READDATA msg %u:r%llu buf=%p\n",
service->service_id, buf_desc->message_id, buf_desc->buf);
msg_metadata =
easelcomm_find_remote_message(service, buf_desc->message_id);
if (!msg_metadata) {
dev_err(easelcomm_miscdev.this_device,
"READDATA msg %u:r%llu not found\n",
service->service_id, buf_desc->message_id);
return -EINVAL;
}
if (buf_desc->buf_size &&
buf_desc->buf_size != msg_metadata->msg->desc.message_size) {
dev_err(easelcomm_miscdev.this_device,
"READDATA descriptor buffer size %u doesn't match message %u:r%llu size %u\n",
buf_desc->buf_size, service->service_id,
buf_desc->message_id,
msg_metadata->msg->desc.message_size);
ret = -EINVAL;
goto out;
}
if (buf_desc->buf_size) {
if (copy_to_user(buf_desc->buf,
&msg_metadata->msg->message_data,
buf_desc->buf_size)) {
ret = -EFAULT;
goto out;
}
}
/*
* If no DMA transfer and no reply needed then we're done with this
* message.
*/
if (!msg_metadata->msg->desc.dma_buf_size &&
!msg_metadata->msg->desc.need_reply) {
easelcomm_drop_reference(service, msg_metadata, true);
return 0;
}
out:
easelcomm_drop_reference(service, msg_metadata, false);
return ret;
}
/*
* Handle WRITEDATA ioctl, the message data write portion of easelcomm
* sendMessage*() and sendReply() calls.
*/
static int easelcomm_write_msgdata(
struct easelcomm_service *service, struct easelcomm_kbuf_desc *buf_desc)
{
struct easelcomm_message_metadata *msg_metadata;
char *databuf = NULL;
bool discard_message = true;
int ret;
dev_dbg(easelcomm_miscdev.this_device,
"WRITEDATA msg %u:l%llu buf=%p\n",
service->service_id, buf_desc->message_id, buf_desc->buf);
msg_metadata =
easelcomm_find_local_message(service, buf_desc->message_id);
if (!msg_metadata) {
dev_err(easelcomm_miscdev.this_device,
"WRITEDATA msg %u:l%llu not found\n",
service->service_id, buf_desc->message_id);
return -EINVAL;
}
if (buf_desc->buf_size &&
buf_desc->buf_size != msg_metadata->msg->desc.message_size) {
dev_err(easelcomm_miscdev.this_device,
"WRITEDATA descriptor buffer size %u doesn't match message %u:l%llu size %u\n",
buf_desc->buf_size, service->service_id,
buf_desc->message_id,
msg_metadata->msg->desc.message_size);
ret = -EINVAL;
goto out;
}
if (buf_desc->buf_size) {
databuf = kmalloc(buf_desc->buf_size, GFP_KERNEL);
if (!databuf) {
ret = -ENOMEM;
goto out;
}
if (copy_from_user(
databuf, buf_desc->buf, buf_desc->buf_size)) {
ret = -EFAULT;
goto out;
}
}
dev_dbg(easelcomm_miscdev.this_device,
"send cmd SEND_MSG msg %u:l%llu\n",
service->service_id, msg_metadata->msg->desc.message_id);
ret = easelcomm_start_cmd(
service, EASELCOMM_CMD_SEND_MSG,
sizeof(struct easelcomm_kmsg_desc) + buf_desc->buf_size);
if (ret)
goto out;
ret = easelcomm_append_cmd_args(
service, &msg_metadata->msg->desc,
sizeof(struct easelcomm_kmsg_desc));
if (ret)
goto out;
if (buf_desc->buf_size) {
ret = easelcomm_append_cmd_args(
service, databuf, buf_desc->buf_size);
if (ret)
goto out;
}
ret = easelcomm_send_cmd(service);
if (ret)
goto out;
/*
* If no DMA transfer and no reply needed then we're done with this
* message.
*/
discard_message = !msg_metadata->msg->desc.dma_buf_size &&
!msg_metadata->msg->desc.need_reply;
out:
/*
* If success and no DMA transfer and no reply needed, or if error,
* then free the message.
*/
easelcomm_drop_reference(service, msg_metadata, discard_message);
kfree(databuf);
return ret;
}
/*
* Client calls this upon receipt of BOOTSTRAP_READY IRQ
* from server. Call MNH host driver to setup iATU mappings for the command
* channels and send LINK_INIT.
*/
int easelcomm_client_remote_cmdchan_ready_handler(void)
{
int ret;
/* Sync up client and server sides using MNH host driver APIs */
ret = easelcomm_hw_ap_setup_cmdchans();
if (ret)
return ret;
easelcomm_cmd_channel_remote_set_ready();
/* Client can now send link init to server */
dev_dbg(easelcomm_miscdev.this_device, "send cmd LINK_INIT\n");
return easelcomm_send_cmd_noargs(
&link_service,
EASELCOMM_CMD_LINK_INIT);
}
EXPORT_SYMBOL(easelcomm_client_remote_cmdchan_ready_handler);
/*
* Handle ioctls that take a struct easelcomm_kbuf_desc * argument, convert
* field layout and buf field pointer for 32-bit compat if needed.
*/
static int easelcomm_ioctl_kbuf_desc(
struct easelcomm_service *service, unsigned int cmd,
unsigned long arg, bool compat)
{
struct easelcomm_kbuf_desc kbuf_desc;
int ret;
if (compat) {
#ifdef CONFIG_COMPAT
struct easelcomm_compat_kbuf_desc compat_kbuf_desc;
if (copy_from_user(
&compat_kbuf_desc, (void __user *) arg,
sizeof(struct easelcomm_compat_kbuf_desc)))
return -EFAULT;
kbuf_desc.message_id = compat_kbuf_desc.message_id;
kbuf_desc.buf = compat_ptr(compat_kbuf_desc.buf);
kbuf_desc.buf_size = compat_kbuf_desc.buf_size;
kbuf_desc.buf_type = compat_kbuf_desc.buf_type;
kbuf_desc.dma_buf_fd = compat_kbuf_desc.dma_buf_fd;
#else
return -EINVAL;
#endif
} else {
if (copy_from_user(
&kbuf_desc, (void __user *) arg,
sizeof(struct easelcomm_kbuf_desc)))
return -EFAULT;
}
switch (cmd) {
case EASELCOMM_IOC_WRITEDATA:
ret = easelcomm_write_msgdata(service, &kbuf_desc);
break;
case EASELCOMM_IOC_READDATA:
ret = easelcomm_read_msgdata(service, &kbuf_desc);
break;
case EASELCOMM_IOC_SENDDMA:
ret = easelcomm_send_dma(service, &kbuf_desc);
break;
case EASELCOMM_IOC_RECVDMA:
ret = easelcomm_receive_dma(service, &kbuf_desc);
break;
default:
ret = -EINVAL;
}
return ret;
}
/* 32-bit and 64-bit userspace ioctl handling, dispatch. */
static long easelcomm_ioctl_common(
struct file *file, unsigned int cmd, unsigned long arg, bool compat)
{
int ret = 0;
struct easelcomm_user_state *user_state = file->private_data;
struct easelcomm_service *service;
if (_IOC_TYPE(cmd) != EASELCOMM_IOC_MAGIC)
return -EINVAL;
if (WARN_ON(!user_state))
return -EINVAL;
if (!easelcomm_up)
return -ESHUTDOWN;
/* REGISTER is the only ioctl that doesn't need a service registered. */
if (cmd == EASELCOMM_IOC_REGISTER)
return easelcomm_register(user_state, (unsigned int) arg);
service = user_state->service;
if (!service) {
dev_err(easelcomm_miscdev.this_device,
"user has not registered a service for the file\n");
return -EINVAL;
}
switch (cmd) {
case EASELCOMM_IOC_SENDMSG:
ret = easelcomm_send_message_ioctl(
service, (struct easelcomm_kmsg_desc *)arg);
break;
case EASELCOMM_IOC_WRITEDATA:
case EASELCOMM_IOC_READDATA:
case EASELCOMM_IOC_SENDDMA:
case EASELCOMM_IOC_RECVDMA:
/* Convert 32-bit kbuf_desc argument if needed and dispatch. */
ret = easelcomm_ioctl_kbuf_desc(service, cmd, arg, compat);
break;
case EASELCOMM_IOC_WAITMSG:
ret = easelcomm_wait_message(
service, (struct easelcomm_kmsg_desc *)arg);
break;
case EASELCOMM_IOC_WAITREPLY:
ret = easelcomm_wait_reply(
service, (struct easelcomm_kmsg_desc *)arg);
break;
case EASELCOMM_IOC_SHUTDOWN:
easelcomm_initiate_service_shutdown(service);
break;
case EASELCOMM_IOC_FLUSH:
easelcomm_flush_service(service);
break;
default:
ret = -ENOIOCTLCMD;
break;
}
return ret;
}
/* 64-bit ioctl handling. */
static long easelcomm_ioctl(
struct file *file, unsigned int cmd, unsigned long arg)
{
return easelcomm_ioctl_common(file, cmd, arg, false);
}
#ifdef CONFIG_COMPAT
/* 32-bit ioctl handling. */
static long easelcomm_compat_ioctl(
struct file *file, unsigned int cmd, unsigned long arg)
{
unsigned int cmd_nr = _IOC_NR(cmd);
int ret;
/* Fixup pointer argument size for ioctls 1..7 */
if ((cmd_nr >= 1 && cmd_nr <= 7) &&
_IOC_SIZE(cmd) == sizeof(compat_uptr_t)) {
cmd &= ~(_IOC_SIZEMASK << _IOC_SIZESHIFT);
cmd |= sizeof(void *) << _IOC_SIZESHIFT;
}
switch (cmd) {
case EASELCOMM_IOC_SENDMSG:
case EASELCOMM_IOC_WAITMSG:
case EASELCOMM_IOC_WAITREPLY:
case EASELCOMM_IOC_WRITEDATA:
case EASELCOMM_IOC_READDATA:
case EASELCOMM_IOC_SENDDMA:
case EASELCOMM_IOC_RECVDMA:
/* pointer argument, fixup */
ret = easelcomm_ioctl_common(file, cmd,
(unsigned long)compat_ptr(arg), true);
break;
case EASELCOMM_IOC_REGISTER:
case EASELCOMM_IOC_SHUTDOWN:
case EASELCOMM_IOC_FLUSH:
/* no ioctl argument conversion needed */
ret = easelcomm_ioctl_common(file, cmd, arg, true);
break;
default:
ret = -ENOIOCTLCMD;
break;
}
return ret;
}
#endif /* CONFIG_COMPAT */
/* Init local state for remote command channel. */
static void easelcomm_init_cmd_channel_remote(
struct easelcomm_cmd_channel_remote *channel)
{
mutex_init(&channel->mutex);
channel->initialized = false;
init_completion(&channel->init_state_changed);
init_completion(&channel->wrap_ready);
/* Write pointer starts after the channel header */
channel->write_offset =
(sizeof(struct easelcomm_cmd_channel_header) + 7) & ~0x7;
channel->write_seqnbr = 0;
}
/* Init local command channel. */
static int easelcomm_init_cmd_channel_local(
struct easelcomm_cmd_channel_local *channel, void *cmdchan_buffer)
{
channel->buffer = cmdchan_buffer;
if (!channel->buffer)
return -ENOMEM;
channel->readp = channel->buffer +
sizeof(struct easelcomm_cmd_channel_header);
/* next sequence # to be produced/consumed starts at zero */
channel->consumer_seqnbr_next = 0;
((struct easelcomm_cmd_channel_header *)
channel->buffer)->producer_seqnbr_next = 0;
return 0;
}
/*
* Callback from h/w layer when PCIe ready (which is immediate for server,
* happens on first PCIe host driver probe on client). The h/w layer has
* allocated the local command channel buffer (which needs the host PCIe
* driver on client), the pointer to which is passed as an argument. Can now
* init our local command channel state, and do remote too.
*/
int easelcomm_init_pcie_ready(void *local_cmdchan_buffer)
{
int ret;
ret = easelcomm_init_cmd_channel_local(
&cmd_channel_local, local_cmdchan_buffer);
easelcomm_init_cmd_channel_remote(&cmd_channel_remote);
return ret;
}
EXPORT_SYMBOL(easelcomm_init_pcie_ready);
/*
* Callback for AP/client from h/w layer when PCIe EP is off.
*/
int easelcomm_pcie_hotplug_out(void)
{
struct easelcomm_cmd_channel_remote *channel = &cmd_channel_remote;
/*
* PCIe link is already down, so no need to call easelcomm_stop() to
* notify remote, but only call easelcomm_stop_local().
*/
easelcomm_stop_local();
mutex_lock(&channel->mutex);
channel->initialized = false;
mutex_unlock(&channel->mutex);
return 0;
}
EXPORT_SYMBOL(easelcomm_pcie_hotplug_out);
static int easelcomm_notify_sys(
struct notifier_block *this, unsigned long code, void *unused)
{
if (code == SYS_RESTART || code == SYS_HALT || code == SYS_POWER_OFF)
easelcomm_stop(false);
return NOTIFY_DONE;
}
static struct notifier_block easelcomm_notifier = {
.notifier_call = easelcomm_notify_sys,
};
static int __init easelcomm_init(void)
{
int ret;
int i;
ret = misc_register(&easelcomm_miscdev);
if (ret) {
dev_err(easelcomm_miscdev.this_device,
"misc_register failed\n");
return ret;
}
dev_info(easelcomm_miscdev.this_device,
"registered at misc device minor %d\n",
easelcomm_miscdev.minor);
mutex_init(&service_mutex);
for (i = 0; i < EASELCOMM_SERVICE_COUNT; i++) {
easelcomm_service_list[i] = NULL;
}
/* Initializes link_service. */
easelcomm_init_service(&link_service, EASELCOMM_SERVICE_COUNT);
easelcomm_hw_init();
/* Shut down easelcomm on reboot */
ret = register_reboot_notifier(&easelcomm_notifier);
if (ret)
dev_warn(easelcomm_miscdev.this_device,
"register reboot notifier failed\n");
return 0;
}
static void __exit easelcomm_exit(void)
{
int i;
easelcomm_stop(false);
kfree(cmd_channel_local.buffer);
misc_deregister(&easelcomm_miscdev);
/*
* TODO(cjluo): Some of these service struct
* could be freed when unregistered.
*/
for (i = 0; i < EASELCOMM_SERVICE_COUNT; i++) {
if (easelcomm_service_list[i] != NULL) {
kfree(easelcomm_service_list[i]);
easelcomm_service_list[i] = NULL;
}
}
}
module_init(easelcomm_init);
module_exit(easelcomm_exit);
MODULE_AUTHOR("Google Inc.");
MODULE_DESCRIPTION("Easel coprocessor communication driver");
MODULE_LICENSE("GPL");