| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Greybus Firmware Management Protocol Driver. |
| * |
| * Copyright 2016 Google Inc. |
| * Copyright 2016 Linaro Ltd. |
| */ |
| |
| #include <linux/cdev.h> |
| #include <linux/completion.h> |
| #include <linux/firmware.h> |
| #include <linux/fs.h> |
| #include <linux/idr.h> |
| #include <linux/ioctl.h> |
| #include <linux/uaccess.h> |
| #include <linux/greybus.h> |
| |
| #include "firmware.h" |
| #include "greybus_firmware.h" |
| |
| #define FW_MGMT_TIMEOUT_MS 1000 |
| |
| struct fw_mgmt { |
| struct device *parent; |
| struct gb_connection *connection; |
| struct kref kref; |
| struct list_head node; |
| |
| /* Common id-map for interface and backend firmware requests */ |
| struct ida id_map; |
| struct mutex mutex; |
| struct completion completion; |
| struct cdev cdev; |
| struct device *class_device; |
| dev_t dev_num; |
| unsigned int timeout_jiffies; |
| bool disabled; /* connection getting disabled */ |
| |
| /* Interface Firmware specific fields */ |
| bool mode_switch_started; |
| bool intf_fw_loaded; |
| u8 intf_fw_request_id; |
| u8 intf_fw_status; |
| u16 intf_fw_major; |
| u16 intf_fw_minor; |
| |
| /* Backend Firmware specific fields */ |
| u8 backend_fw_request_id; |
| u8 backend_fw_status; |
| }; |
| |
| /* |
| * Number of minor devices this driver supports. |
| * There will be exactly one required per Interface. |
| */ |
| #define NUM_MINORS U8_MAX |
| |
| static const struct class fw_mgmt_class = { |
| .name = "gb_fw_mgmt", |
| }; |
| |
| static dev_t fw_mgmt_dev_num; |
| static DEFINE_IDA(fw_mgmt_minors_map); |
| static LIST_HEAD(fw_mgmt_list); |
| static DEFINE_MUTEX(list_mutex); |
| |
| static void fw_mgmt_kref_release(struct kref *kref) |
| { |
| struct fw_mgmt *fw_mgmt = container_of(kref, struct fw_mgmt, kref); |
| |
| ida_destroy(&fw_mgmt->id_map); |
| kfree(fw_mgmt); |
| } |
| |
| /* |
| * All users of fw_mgmt take a reference (from within list_mutex lock), before |
| * they get a pointer to play with. And the structure will be freed only after |
| * the last user has put the reference to it. |
| */ |
| static void put_fw_mgmt(struct fw_mgmt *fw_mgmt) |
| { |
| kref_put(&fw_mgmt->kref, fw_mgmt_kref_release); |
| } |
| |
| /* Caller must call put_fw_mgmt() after using struct fw_mgmt */ |
| static struct fw_mgmt *get_fw_mgmt(struct cdev *cdev) |
| { |
| struct fw_mgmt *fw_mgmt; |
| |
| mutex_lock(&list_mutex); |
| |
| list_for_each_entry(fw_mgmt, &fw_mgmt_list, node) { |
| if (&fw_mgmt->cdev == cdev) { |
| kref_get(&fw_mgmt->kref); |
| goto unlock; |
| } |
| } |
| |
| fw_mgmt = NULL; |
| |
| unlock: |
| mutex_unlock(&list_mutex); |
| |
| return fw_mgmt; |
| } |
| |
| static int fw_mgmt_interface_fw_version_operation(struct fw_mgmt *fw_mgmt, |
| struct fw_mgmt_ioc_get_intf_version *fw_info) |
| { |
| struct gb_connection *connection = fw_mgmt->connection; |
| struct gb_fw_mgmt_interface_fw_version_response response; |
| int ret; |
| |
| ret = gb_operation_sync(connection, |
| GB_FW_MGMT_TYPE_INTERFACE_FW_VERSION, NULL, 0, |
| &response, sizeof(response)); |
| if (ret) { |
| dev_err(fw_mgmt->parent, |
| "failed to get interface firmware version (%d)\n", ret); |
| return ret; |
| } |
| |
| fw_info->major = le16_to_cpu(response.major); |
| fw_info->minor = le16_to_cpu(response.minor); |
| |
| strscpy_pad(fw_info->firmware_tag, response.firmware_tag); |
| |
| /* |
| * The firmware-tag should be NULL terminated, otherwise throw error but |
| * don't fail. |
| */ |
| if (fw_info->firmware_tag[GB_FIRMWARE_TAG_MAX_SIZE - 1] != '\0') { |
| dev_err(fw_mgmt->parent, |
| "fw-version: firmware-tag is not NULL terminated\n"); |
| fw_info->firmware_tag[GB_FIRMWARE_TAG_MAX_SIZE - 1] = '\0'; |
| } |
| |
| return 0; |
| } |
| |
| static int fw_mgmt_load_and_validate_operation(struct fw_mgmt *fw_mgmt, |
| u8 load_method, const char *tag) |
| { |
| struct gb_fw_mgmt_load_and_validate_fw_request request; |
| int ret; |
| |
| if (load_method != GB_FW_LOAD_METHOD_UNIPRO && |
| load_method != GB_FW_LOAD_METHOD_INTERNAL) { |
| dev_err(fw_mgmt->parent, |
| "invalid load-method (%d)\n", load_method); |
| return -EINVAL; |
| } |
| |
| request.load_method = load_method; |
| strscpy_pad(request.firmware_tag, tag); |
| |
| /* |
| * The firmware-tag should be NULL terminated, otherwise throw error and |
| * fail. |
| */ |
| if (request.firmware_tag[GB_FIRMWARE_TAG_MAX_SIZE - 1] != '\0') { |
| dev_err(fw_mgmt->parent, "load-and-validate: firmware-tag is not NULL terminated\n"); |
| return -EINVAL; |
| } |
| |
| /* Allocate ids from 1 to 255 (u8-max), 0 is an invalid id */ |
| ret = ida_alloc_range(&fw_mgmt->id_map, 1, 255, GFP_KERNEL); |
| if (ret < 0) { |
| dev_err(fw_mgmt->parent, "failed to allocate request id (%d)\n", |
| ret); |
| return ret; |
| } |
| |
| fw_mgmt->intf_fw_request_id = ret; |
| fw_mgmt->intf_fw_loaded = false; |
| request.request_id = ret; |
| |
| ret = gb_operation_sync(fw_mgmt->connection, |
| GB_FW_MGMT_TYPE_LOAD_AND_VALIDATE_FW, &request, |
| sizeof(request), NULL, 0); |
| if (ret) { |
| ida_free(&fw_mgmt->id_map, fw_mgmt->intf_fw_request_id); |
| fw_mgmt->intf_fw_request_id = 0; |
| dev_err(fw_mgmt->parent, |
| "load and validate firmware request failed (%d)\n", |
| ret); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int fw_mgmt_interface_fw_loaded_operation(struct gb_operation *op) |
| { |
| struct gb_connection *connection = op->connection; |
| struct fw_mgmt *fw_mgmt = gb_connection_get_data(connection); |
| struct gb_fw_mgmt_loaded_fw_request *request; |
| |
| /* No pending load and validate request ? */ |
| if (!fw_mgmt->intf_fw_request_id) { |
| dev_err(fw_mgmt->parent, |
| "unexpected firmware loaded request received\n"); |
| return -ENODEV; |
| } |
| |
| if (op->request->payload_size != sizeof(*request)) { |
| dev_err(fw_mgmt->parent, "illegal size of firmware loaded request (%zu != %zu)\n", |
| op->request->payload_size, sizeof(*request)); |
| return -EINVAL; |
| } |
| |
| request = op->request->payload; |
| |
| /* Invalid request-id ? */ |
| if (request->request_id != fw_mgmt->intf_fw_request_id) { |
| dev_err(fw_mgmt->parent, "invalid request id for firmware loaded request (%02u != %02u)\n", |
| fw_mgmt->intf_fw_request_id, request->request_id); |
| return -ENODEV; |
| } |
| |
| ida_free(&fw_mgmt->id_map, fw_mgmt->intf_fw_request_id); |
| fw_mgmt->intf_fw_request_id = 0; |
| fw_mgmt->intf_fw_status = request->status; |
| fw_mgmt->intf_fw_major = le16_to_cpu(request->major); |
| fw_mgmt->intf_fw_minor = le16_to_cpu(request->minor); |
| |
| if (fw_mgmt->intf_fw_status == GB_FW_LOAD_STATUS_FAILED) |
| dev_err(fw_mgmt->parent, |
| "failed to load interface firmware, status:%02x\n", |
| fw_mgmt->intf_fw_status); |
| else if (fw_mgmt->intf_fw_status == GB_FW_LOAD_STATUS_VALIDATION_FAILED) |
| dev_err(fw_mgmt->parent, |
| "failed to validate interface firmware, status:%02x\n", |
| fw_mgmt->intf_fw_status); |
| else |
| fw_mgmt->intf_fw_loaded = true; |
| |
| complete(&fw_mgmt->completion); |
| |
| return 0; |
| } |
| |
| static int fw_mgmt_backend_fw_version_operation(struct fw_mgmt *fw_mgmt, |
| struct fw_mgmt_ioc_get_backend_version *fw_info) |
| { |
| struct gb_connection *connection = fw_mgmt->connection; |
| struct gb_fw_mgmt_backend_fw_version_request request; |
| struct gb_fw_mgmt_backend_fw_version_response response; |
| int ret; |
| |
| strscpy_pad(request.firmware_tag, fw_info->firmware_tag); |
| |
| /* |
| * The firmware-tag should be NULL terminated, otherwise throw error and |
| * fail. |
| */ |
| if (request.firmware_tag[GB_FIRMWARE_TAG_MAX_SIZE - 1] != '\0') { |
| dev_err(fw_mgmt->parent, "backend-version: firmware-tag is not NULL terminated\n"); |
| return -EINVAL; |
| } |
| |
| ret = gb_operation_sync(connection, |
| GB_FW_MGMT_TYPE_BACKEND_FW_VERSION, &request, |
| sizeof(request), &response, sizeof(response)); |
| if (ret) { |
| dev_err(fw_mgmt->parent, "failed to get version of %s backend firmware (%d)\n", |
| fw_info->firmware_tag, ret); |
| return ret; |
| } |
| |
| fw_info->status = response.status; |
| |
| /* Reset version as that should be non-zero only for success case */ |
| fw_info->major = 0; |
| fw_info->minor = 0; |
| |
| switch (fw_info->status) { |
| case GB_FW_BACKEND_VERSION_STATUS_SUCCESS: |
| fw_info->major = le16_to_cpu(response.major); |
| fw_info->minor = le16_to_cpu(response.minor); |
| break; |
| case GB_FW_BACKEND_VERSION_STATUS_NOT_AVAILABLE: |
| case GB_FW_BACKEND_VERSION_STATUS_RETRY: |
| break; |
| case GB_FW_BACKEND_VERSION_STATUS_NOT_SUPPORTED: |
| dev_err(fw_mgmt->parent, |
| "Firmware with tag %s is not supported by Interface\n", |
| fw_info->firmware_tag); |
| break; |
| default: |
| dev_err(fw_mgmt->parent, "Invalid status received: %u\n", |
| fw_info->status); |
| } |
| |
| return 0; |
| } |
| |
| static int fw_mgmt_backend_fw_update_operation(struct fw_mgmt *fw_mgmt, |
| char *tag) |
| { |
| struct gb_fw_mgmt_backend_fw_update_request request; |
| int ret; |
| |
| ret = strscpy_pad(request.firmware_tag, tag); |
| |
| /* |
| * The firmware-tag should be NULL terminated, otherwise throw error and |
| * fail. |
| */ |
| if (ret == -E2BIG) { |
| dev_err(fw_mgmt->parent, "backend-update: firmware-tag is not NULL terminated\n"); |
| return -EINVAL; |
| } |
| |
| /* Allocate ids from 1 to 255 (u8-max), 0 is an invalid id */ |
| ret = ida_alloc_range(&fw_mgmt->id_map, 1, 255, GFP_KERNEL); |
| if (ret < 0) { |
| dev_err(fw_mgmt->parent, "failed to allocate request id (%d)\n", |
| ret); |
| return ret; |
| } |
| |
| fw_mgmt->backend_fw_request_id = ret; |
| request.request_id = ret; |
| |
| ret = gb_operation_sync(fw_mgmt->connection, |
| GB_FW_MGMT_TYPE_BACKEND_FW_UPDATE, &request, |
| sizeof(request), NULL, 0); |
| if (ret) { |
| ida_free(&fw_mgmt->id_map, fw_mgmt->backend_fw_request_id); |
| fw_mgmt->backend_fw_request_id = 0; |
| dev_err(fw_mgmt->parent, |
| "backend %s firmware update request failed (%d)\n", tag, |
| ret); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int fw_mgmt_backend_fw_updated_operation(struct gb_operation *op) |
| { |
| struct gb_connection *connection = op->connection; |
| struct fw_mgmt *fw_mgmt = gb_connection_get_data(connection); |
| struct gb_fw_mgmt_backend_fw_updated_request *request; |
| |
| /* No pending load and validate request ? */ |
| if (!fw_mgmt->backend_fw_request_id) { |
| dev_err(fw_mgmt->parent, "unexpected backend firmware updated request received\n"); |
| return -ENODEV; |
| } |
| |
| if (op->request->payload_size != sizeof(*request)) { |
| dev_err(fw_mgmt->parent, "illegal size of backend firmware updated request (%zu != %zu)\n", |
| op->request->payload_size, sizeof(*request)); |
| return -EINVAL; |
| } |
| |
| request = op->request->payload; |
| |
| /* Invalid request-id ? */ |
| if (request->request_id != fw_mgmt->backend_fw_request_id) { |
| dev_err(fw_mgmt->parent, "invalid request id for backend firmware updated request (%02u != %02u)\n", |
| fw_mgmt->backend_fw_request_id, request->request_id); |
| return -ENODEV; |
| } |
| |
| ida_free(&fw_mgmt->id_map, fw_mgmt->backend_fw_request_id); |
| fw_mgmt->backend_fw_request_id = 0; |
| fw_mgmt->backend_fw_status = request->status; |
| |
| if ((fw_mgmt->backend_fw_status != GB_FW_BACKEND_FW_STATUS_SUCCESS) && |
| (fw_mgmt->backend_fw_status != GB_FW_BACKEND_FW_STATUS_RETRY)) |
| dev_err(fw_mgmt->parent, |
| "failed to load backend firmware: %02x\n", |
| fw_mgmt->backend_fw_status); |
| |
| complete(&fw_mgmt->completion); |
| |
| return 0; |
| } |
| |
| /* Char device fops */ |
| |
| static int fw_mgmt_open(struct inode *inode, struct file *file) |
| { |
| struct fw_mgmt *fw_mgmt = get_fw_mgmt(inode->i_cdev); |
| |
| /* fw_mgmt structure can't get freed until file descriptor is closed */ |
| if (fw_mgmt) { |
| file->private_data = fw_mgmt; |
| return 0; |
| } |
| |
| return -ENODEV; |
| } |
| |
| static int fw_mgmt_release(struct inode *inode, struct file *file) |
| { |
| struct fw_mgmt *fw_mgmt = file->private_data; |
| |
| put_fw_mgmt(fw_mgmt); |
| return 0; |
| } |
| |
| static int fw_mgmt_ioctl(struct fw_mgmt *fw_mgmt, unsigned int cmd, |
| void __user *buf) |
| { |
| struct fw_mgmt_ioc_get_intf_version intf_fw_info; |
| struct fw_mgmt_ioc_get_backend_version backend_fw_info; |
| struct fw_mgmt_ioc_intf_load_and_validate intf_load; |
| struct fw_mgmt_ioc_backend_fw_update backend_update; |
| unsigned int timeout; |
| int ret; |
| |
| /* Reject any operations after mode-switch has started */ |
| if (fw_mgmt->mode_switch_started) |
| return -EBUSY; |
| |
| switch (cmd) { |
| case FW_MGMT_IOC_GET_INTF_FW: |
| ret = fw_mgmt_interface_fw_version_operation(fw_mgmt, |
| &intf_fw_info); |
| if (ret) |
| return ret; |
| |
| if (copy_to_user(buf, &intf_fw_info, sizeof(intf_fw_info))) |
| return -EFAULT; |
| |
| return 0; |
| case FW_MGMT_IOC_GET_BACKEND_FW: |
| if (copy_from_user(&backend_fw_info, buf, |
| sizeof(backend_fw_info))) |
| return -EFAULT; |
| |
| ret = fw_mgmt_backend_fw_version_operation(fw_mgmt, |
| &backend_fw_info); |
| if (ret) |
| return ret; |
| |
| if (copy_to_user(buf, &backend_fw_info, |
| sizeof(backend_fw_info))) |
| return -EFAULT; |
| |
| return 0; |
| case FW_MGMT_IOC_INTF_LOAD_AND_VALIDATE: |
| if (copy_from_user(&intf_load, buf, sizeof(intf_load))) |
| return -EFAULT; |
| |
| ret = fw_mgmt_load_and_validate_operation(fw_mgmt, |
| intf_load.load_method, intf_load.firmware_tag); |
| if (ret) |
| return ret; |
| |
| if (!wait_for_completion_timeout(&fw_mgmt->completion, |
| fw_mgmt->timeout_jiffies)) { |
| dev_err(fw_mgmt->parent, "timed out waiting for firmware load and validation to finish\n"); |
| return -ETIMEDOUT; |
| } |
| |
| intf_load.status = fw_mgmt->intf_fw_status; |
| intf_load.major = fw_mgmt->intf_fw_major; |
| intf_load.minor = fw_mgmt->intf_fw_minor; |
| |
| if (copy_to_user(buf, &intf_load, sizeof(intf_load))) |
| return -EFAULT; |
| |
| return 0; |
| case FW_MGMT_IOC_INTF_BACKEND_FW_UPDATE: |
| if (copy_from_user(&backend_update, buf, |
| sizeof(backend_update))) |
| return -EFAULT; |
| |
| ret = fw_mgmt_backend_fw_update_operation(fw_mgmt, |
| backend_update.firmware_tag); |
| if (ret) |
| return ret; |
| |
| if (!wait_for_completion_timeout(&fw_mgmt->completion, |
| fw_mgmt->timeout_jiffies)) { |
| dev_err(fw_mgmt->parent, "timed out waiting for backend firmware update to finish\n"); |
| return -ETIMEDOUT; |
| } |
| |
| backend_update.status = fw_mgmt->backend_fw_status; |
| |
| if (copy_to_user(buf, &backend_update, sizeof(backend_update))) |
| return -EFAULT; |
| |
| return 0; |
| case FW_MGMT_IOC_SET_TIMEOUT_MS: |
| if (get_user(timeout, (unsigned int __user *)buf)) |
| return -EFAULT; |
| |
| if (!timeout) { |
| dev_err(fw_mgmt->parent, "timeout can't be zero\n"); |
| return -EINVAL; |
| } |
| |
| fw_mgmt->timeout_jiffies = msecs_to_jiffies(timeout); |
| |
| return 0; |
| case FW_MGMT_IOC_MODE_SWITCH: |
| if (!fw_mgmt->intf_fw_loaded) { |
| dev_err(fw_mgmt->parent, |
| "Firmware not loaded for mode-switch\n"); |
| return -EPERM; |
| } |
| |
| /* |
| * Disallow new ioctls as the fw-core bundle driver is going to |
| * get disconnected soon and the character device will get |
| * removed. |
| */ |
| fw_mgmt->mode_switch_started = true; |
| |
| ret = gb_interface_request_mode_switch(fw_mgmt->connection->intf); |
| if (ret) { |
| dev_err(fw_mgmt->parent, "Mode-switch failed: %d\n", |
| ret); |
| fw_mgmt->mode_switch_started = false; |
| return ret; |
| } |
| |
| return 0; |
| default: |
| return -ENOTTY; |
| } |
| } |
| |
| static long fw_mgmt_ioctl_unlocked(struct file *file, unsigned int cmd, |
| unsigned long arg) |
| { |
| struct fw_mgmt *fw_mgmt = file->private_data; |
| struct gb_bundle *bundle = fw_mgmt->connection->bundle; |
| int ret = -ENODEV; |
| |
| /* |
| * Serialize ioctls. |
| * |
| * We don't want the user to do few operations in parallel. For example, |
| * updating Interface firmware in parallel for the same Interface. There |
| * is no need to do things in parallel for speed and we can avoid having |
| * complicated code for now. |
| * |
| * This is also used to protect ->disabled, which is used to check if |
| * the connection is getting disconnected, so that we don't start any |
| * new operations. |
| */ |
| mutex_lock(&fw_mgmt->mutex); |
| if (!fw_mgmt->disabled) { |
| ret = gb_pm_runtime_get_sync(bundle); |
| if (!ret) { |
| ret = fw_mgmt_ioctl(fw_mgmt, cmd, (void __user *)arg); |
| gb_pm_runtime_put_autosuspend(bundle); |
| } |
| } |
| mutex_unlock(&fw_mgmt->mutex); |
| |
| return ret; |
| } |
| |
| static const struct file_operations fw_mgmt_fops = { |
| .owner = THIS_MODULE, |
| .open = fw_mgmt_open, |
| .release = fw_mgmt_release, |
| .unlocked_ioctl = fw_mgmt_ioctl_unlocked, |
| }; |
| |
| int gb_fw_mgmt_request_handler(struct gb_operation *op) |
| { |
| u8 type = op->type; |
| |
| switch (type) { |
| case GB_FW_MGMT_TYPE_LOADED_FW: |
| return fw_mgmt_interface_fw_loaded_operation(op); |
| case GB_FW_MGMT_TYPE_BACKEND_FW_UPDATED: |
| return fw_mgmt_backend_fw_updated_operation(op); |
| default: |
| dev_err(&op->connection->bundle->dev, |
| "unsupported request: %u\n", type); |
| return -EINVAL; |
| } |
| } |
| |
| int gb_fw_mgmt_connection_init(struct gb_connection *connection) |
| { |
| struct fw_mgmt *fw_mgmt; |
| int ret, minor; |
| |
| if (!connection) |
| return 0; |
| |
| fw_mgmt = kzalloc(sizeof(*fw_mgmt), GFP_KERNEL); |
| if (!fw_mgmt) |
| return -ENOMEM; |
| |
| fw_mgmt->parent = &connection->bundle->dev; |
| fw_mgmt->timeout_jiffies = msecs_to_jiffies(FW_MGMT_TIMEOUT_MS); |
| fw_mgmt->connection = connection; |
| |
| gb_connection_set_data(connection, fw_mgmt); |
| init_completion(&fw_mgmt->completion); |
| ida_init(&fw_mgmt->id_map); |
| mutex_init(&fw_mgmt->mutex); |
| kref_init(&fw_mgmt->kref); |
| |
| mutex_lock(&list_mutex); |
| list_add(&fw_mgmt->node, &fw_mgmt_list); |
| mutex_unlock(&list_mutex); |
| |
| ret = gb_connection_enable(connection); |
| if (ret) |
| goto err_list_del; |
| |
| minor = ida_alloc_max(&fw_mgmt_minors_map, NUM_MINORS - 1, GFP_KERNEL); |
| if (minor < 0) { |
| ret = minor; |
| goto err_connection_disable; |
| } |
| |
| /* Add a char device to allow userspace to interact with fw-mgmt */ |
| fw_mgmt->dev_num = MKDEV(MAJOR(fw_mgmt_dev_num), minor); |
| cdev_init(&fw_mgmt->cdev, &fw_mgmt_fops); |
| |
| ret = cdev_add(&fw_mgmt->cdev, fw_mgmt->dev_num, 1); |
| if (ret) |
| goto err_remove_ida; |
| |
| /* Add a soft link to the previously added char-dev within the bundle */ |
| fw_mgmt->class_device = device_create(&fw_mgmt_class, fw_mgmt->parent, |
| fw_mgmt->dev_num, NULL, |
| "gb-fw-mgmt-%d", minor); |
| if (IS_ERR(fw_mgmt->class_device)) { |
| ret = PTR_ERR(fw_mgmt->class_device); |
| goto err_del_cdev; |
| } |
| |
| return 0; |
| |
| err_del_cdev: |
| cdev_del(&fw_mgmt->cdev); |
| err_remove_ida: |
| ida_free(&fw_mgmt_minors_map, minor); |
| err_connection_disable: |
| gb_connection_disable(connection); |
| err_list_del: |
| mutex_lock(&list_mutex); |
| list_del(&fw_mgmt->node); |
| mutex_unlock(&list_mutex); |
| |
| put_fw_mgmt(fw_mgmt); |
| |
| return ret; |
| } |
| |
| void gb_fw_mgmt_connection_exit(struct gb_connection *connection) |
| { |
| struct fw_mgmt *fw_mgmt; |
| |
| if (!connection) |
| return; |
| |
| fw_mgmt = gb_connection_get_data(connection); |
| |
| device_destroy(&fw_mgmt_class, fw_mgmt->dev_num); |
| cdev_del(&fw_mgmt->cdev); |
| ida_free(&fw_mgmt_minors_map, MINOR(fw_mgmt->dev_num)); |
| |
| /* |
| * Disallow any new ioctl operations on the char device and wait for |
| * existing ones to finish. |
| */ |
| mutex_lock(&fw_mgmt->mutex); |
| fw_mgmt->disabled = true; |
| mutex_unlock(&fw_mgmt->mutex); |
| |
| /* All pending greybus operations should have finished by now */ |
| gb_connection_disable(fw_mgmt->connection); |
| |
| /* Disallow new users to get access to the fw_mgmt structure */ |
| mutex_lock(&list_mutex); |
| list_del(&fw_mgmt->node); |
| mutex_unlock(&list_mutex); |
| |
| /* |
| * All current users of fw_mgmt would have taken a reference to it by |
| * now, we can drop our reference and wait the last user will get |
| * fw_mgmt freed. |
| */ |
| put_fw_mgmt(fw_mgmt); |
| } |
| |
| int fw_mgmt_init(void) |
| { |
| int ret; |
| |
| ret = class_register(&fw_mgmt_class); |
| if (ret) |
| return ret; |
| |
| ret = alloc_chrdev_region(&fw_mgmt_dev_num, 0, NUM_MINORS, |
| "gb_fw_mgmt"); |
| if (ret) |
| goto err_remove_class; |
| |
| return 0; |
| |
| err_remove_class: |
| class_unregister(&fw_mgmt_class); |
| return ret; |
| } |
| |
| void fw_mgmt_exit(void) |
| { |
| unregister_chrdev_region(fw_mgmt_dev_num, NUM_MINORS); |
| class_unregister(&fw_mgmt_class); |
| ida_destroy(&fw_mgmt_minors_map); |
| } |