Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ee3d0ce4 authored by Linux Build Service Account's avatar Linux Build Service Account Committed by Gerrit - the friendly Code Review server
Browse files

Merge "msm: pil-femto-modem: FSM99XX PIL driver"

parents 011fdfee a7e5d1f4
Loading
Loading
Loading
Loading
+92 −0
Original line number Diff line number Diff line
Qualcomm Femtocell (FSM99XX) Peripheral Image Loader

pil-femto-modem is a peripheral image loader (PIL) driver. It is used for
loading firmware images on multiple modems resident on the FSM99XX platform.

The top-level device tree entry has the following properties:

Required properties:
- compatible:           Must be "qcom,pil-femto-modem".
- reg:                  Pair of physical base address and region size of the
                        QDSP6[0] control registers.
- reg-names:            "qdsp6_base" is required.
- qcom,firmware-name:   Base name of the MBA firmware image ("mba").
                        Maximum size is 4 characters.
- qcom,max-num-modems:  Number of modem child nodes specified.

One child node per modem that must be loaded is required.

Required properties:
- compatible:           Must be "qcom,pil-femto-modem-desc".
- reg:                  Pair of physical base address and region size of the
                        Relay Message Buffer (RMB) registers for this modem.
- reg-names:            "rmb_base" is required.
- qcom,firmware-name:   Base filename of the firmware image (e.g. "mdm0").
                        Maximum size is 4 characters.
- qcom,modem-id:        Unique ID of this modem.  This is not a logical
                        index.
- qcom,max-num-images:  The maximum number of images that will be loaded to
                        this modem.
Optional properties:
- qcom,pil-skip-entry-check:
                        Some entry pointers are virtual addresses,
                        which will cause the PIL entry address check
                        to fail.  This parameter will disable the
                        check.
Example:
        qcom,modem-femto@fbc00000 {
                compatible = "qcom,pil-femto-modem";
                #address-cells=<1>;
                #size-cells=<1>;
                ranges;
                reg = <0xfbc00000 0x100>;
                reg-names = "qdsp6_base";
                qcom,firmware-name = "mba";
                qcom,max-num-modems = <5>;

                qcom,modem@fd4a7000 {
                        compatible = "qcom,pil-femto-modem-desc";
                        reg = <0xfd4a7000 0x20>;
                        reg-names = "rmb_base";
                        qcom,firmware-name = "mdm0";
                        qcom,modem-id = <0>;
                        qcom,max-num-images = <1>;
                };

                qcom,modem@fd4a7030 {
                        compatible = "qcom,pil-femto-modem-desc";
                        reg = <0xfd4a7030 0x20>;
                        reg-names = "rmb_base";
                        qcom,firmware-name = "mdm1";
                        qcom,modem-id = <1>;
                        qcom,max-num-images = <1>;
                };

                qcom,modem@fd4a7060 {
                        compatible = "qcom,pil-femto-modem-desc";
                        reg = <0xfd4a7060 0x20>;
                        reg-names = "rmb_base";
                        qcom,firmware-name = "mdm2";
                        qcom,modem-id = <2>;
                        qcom,max-num-images = <1>;
                };

                qcom,modem@fd4a7090 {
                        compatible = "qcom,pil-femto-modem-desc";
                        reg = <0xfd4a7090 0x20>;
                        reg-names = "rmb_base";
                        qcom,firmware-name = "mdm3";
                        qcom,modem-id = <3>;
                        qcom,max-num-images = <1>;
                };

                qcom,modem@fd4a70c0 {
                        compatible = "qcom,pil-femto-modem-desc";
                        reg = <0xfd4a70c0 0x20>;
                        reg-names = "rmb_base";
                        qcom,firmware-name = "mdm4";
                        qcom,modem-id = <4>;
                        qcom,max-num-images = <8>;
                        qcom,pil-skip-entry-check;
                };
        };
+9 −0
Original line number Diff line number Diff line
@@ -1255,6 +1255,15 @@ config MSM_PIL_PRONTO
	  PRONTO is the wireless subsystem processor used in bluetooth, wireless
	  LAN, and FM software applications.

config MSM_PIL_FEMTO
	tristate "FSM99XX Boot Support"
	depends on MSM_PIL && ARCH_FSM9900
	help
	  Support for loading and booting firmware images for multiple
	  modems on the FSM9900 family targets.
	  Select Y if you want the modems to boot.
	  If unsure, select N.

config MSM_SCM
	bool "Secure Channel Manager (SCM) support"
	default n
+1 −0
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ obj-$(CONFIG_MSM_PIL_GSS) += pil-gss.o
obj-$(CONFIG_MSM_PIL_PRONTO) += pil-pronto.o
obj-$(CONFIG_MSM_PIL_VENUS) += pil-venus.o
obj-$(CONFIG_MSM_PIL_VPU) += pil-vpu.o
obj-$(CONFIG_MSM_PIL_FEMTO) += pil-q6v5.o pil-msa.o pil-femto-modem.o
obj-$(CONFIG_MSM_FIQ_SUPPORT) += fiq_glue.o
obj-$(CONFIG_MSM_BAM_DMUX) += bam_dmux.o
obj-$(CONFIG_MSM_SMD_LOGGING) += smem_log.o
+767 −0
Original line number Diff line number Diff line
/* Copyright (c) 2013, The Linux Foundation. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 and
 * only version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/io.h>
#include <linux/iopoll.h>
#include <linux/err.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/stat.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/sysfs.h>

#include <asm/page.h>

#include "peripheral-loader.h"
#include "pil-q6v5.h"
#include "pil-msa.h"

#define MAX_MODEM_ID	4

/* These macros assist with creating sysfs attributes */
#define MAKE_RO_ATTR(_name, _ptr, _index)\
	_ptr->kobj_attr_##_name.attr.name = __stringify(_name);\
	_ptr->kobj_attr_##_name.attr.mode = S_IRUSR;\
	_ptr->kobj_attr_##_name.show = show_##_name;\
	_ptr->kobj_attr_##_name.store = NULL;\
	sysfs_attr_init(&_ptr->kobj_attr_##_name.attr);\
	_ptr->attr_grp.attrs[_index] = &_ptr->kobj_attr_##_name.attr;

#define MAKE_RW_ATTR(_name, _ptr, _index)\
	_ptr->kobj_attr_##_name.attr.name = __stringify(_name);\
	_ptr->kobj_attr_##_name.attr.mode = S_IRUSR|S_IWUSR; \
	_ptr->kobj_attr_##_name.show = show_##_name;\
	_ptr->kobj_attr_##_name.store = store_##_name;\
	sysfs_attr_init(&_ptr->kobj_attr_##_name.attr);\
	_ptr->attr_grp.attrs[_index] = &_ptr->kobj_attr_##_name.attr;

enum modem_status {
	MODEM_STATUS_OFFLINE = 0,
	MODEM_STATUS_MBA_LOADING,
	MODEM_STATUS_MBA_RUNNING,
	MODEM_STATUS_MBA_ERROR,
	MODEM_STATUS_PMI_LOADING,
	MODEM_STATUS_PMI_ERROR,
	MODEM_STATUS_PMI_LOADED,
	MODEM_STATUS_PMI_NOT_FOUND,
	MODEM_STATUS_ADVANCE_FAILED,
	MODEM_STATUS_LAST
};

static char *modem_status_str[MODEM_STATUS_LAST] = {
	"OFFLINE",
	"MBA LOADING",
	"MBA RUNNING",
	"MBA ERROR",
	"PMI LOADING",
	"PMI ERROR",
	"PMI LOADED",
	"PMI NOT FOUND",
	"ADVANCE FAILED"
};

struct modem_pil_data {
	struct mba_data image;
	const char *name;
	u32 num_images;
	u32 id;

	/* sysfs */
	struct kobject *kobj;
	struct attribute_group attr_grp;
	struct kobj_attribute kobj_attr_status;
	struct kobj_attribute kobj_attr_imgs_loaded;
	struct kobj_attribute kobj_attr_last_img_loaded;
	struct kobj_attribute kobj_attr_img_loading;
	u32 status;
	u32 imgs_loaded;
	int last_img_loaded;
	int img_loading;
};

struct femto_modem_data {
	struct q6v5_data *q6;
	struct modem_pil_data *modem;
	u32 max_num_modems;
	u32 disc_modems;

	/* sysfs */
	struct kobject *kobj;
	struct attribute_group attr_grp;
	struct kobj_attribute kobj_attr_mba_status;
	struct kobj_attribute kobj_attr_enable;
	u32 mba_status;
	u8  enable;
};

#define SET_SYSFS_VALUE(_ptr, _attr, _val)\
	do {\
		_ptr->_attr = (_val);\
		sysfs_notify(_ptr->kobj, NULL, __stringify(_attr));\
	} while (0)

#define RMB_MBA_COMMAND			0x08
#define RMB_MBA_STATUS			0x0C
#define CMD_RMB_ADVANCE			0x03
#define STATUS_RMB_UPDATE_ACK		0x06
#define RMB_ADVANCE_COMPLETE		0xFE
#define POLL_INTERVAL_US		50
#define TIMEOUT_US			1000000

static void *pil_femto_modem_map_fw_mem(phys_addr_t paddr, size_t size)
{
	/* Due to certain memory areas on the platform requiring 32-bit wide
	 * accesses, we must cache the firmware to avoid bus errors.
	 */
	return ioremap_cached(paddr, size);
}

static int pil_femto_modem_send_rmb_advance(void __iomem *rmb_base, u32 id)
{
	int ret;
	u32 cmd = CMD_RMB_ADVANCE;
	int status;

	if (!rmb_base)
		return -EINVAL;

	/* Prepare the command */
	cmd |= id << 8;

	/* Sent the MBA command */
	writel_relaxed(cmd, rmb_base + RMB_MBA_COMMAND);

	/* Wait for MBA status. */
	ret = readl_poll_timeout(rmb_base + RMB_MBA_STATUS, status,
		((status < 0) || (status == STATUS_RMB_UPDATE_ACK)),
		POLL_INTERVAL_US, TIMEOUT_US);

	if (ret)
		return ret;

	if (status != STATUS_RMB_UPDATE_ACK)
		return -EINVAL;

	return ret;
}

static int pil_femto_modem_stop(struct femto_modem_data *drv)
{
	if (!drv)
		return -EINVAL;

	/* Only need to shutdown the Q6 PIL descriptor, because shutting down
	 * the others does nothing.
	 */
	pil_shutdown(&drv->q6->desc);
	return 0;
}

static int pil_femto_modem_start(struct femto_modem_data *drv)
{
	int ret;
	u32 index;

	/* MBA must load, else we can't load any firmware images */
	SET_SYSFS_VALUE(drv, mba_status, MODEM_STATUS_MBA_LOADING);
	ret = pil_boot(&drv->q6->desc);
	if (ret) {
		SET_SYSFS_VALUE(drv, mba_status, MODEM_STATUS_MBA_ERROR);
		return ret;
	}
	SET_SYSFS_VALUE(drv, mba_status, MODEM_STATUS_MBA_RUNNING);

	/* Load the other modem images, if possible. */
	for (index = 0; index < drv->max_num_modems; index++) {
		struct modem_pil_data *modem = &drv->modem[index];
		int img;
		char *pmi_name = kzalloc((strlen(modem->name) + 5), GFP_KERNEL);
		struct mba_data *image = &modem->image;

		if (!pmi_name) {
			ret = -ENOMEM;
			SET_SYSFS_VALUE(modem, status, MODEM_STATUS_PMI_ERROR);
			pil_shutdown(&drv->q6->desc);
			break;
		}

		/* Initialize stats */
		SET_SYSFS_VALUE(modem, imgs_loaded, 0);
		SET_SYSFS_VALUE(modem, last_img_loaded, -1);
		SET_SYSFS_VALUE(modem, img_loading, -1);

		/* Try to load each image. */
		for (img = 0; img < modem->num_images; img++) {
			SET_SYSFS_VALUE(modem, status,
				MODEM_STATUS_PMI_LOADING);
			SET_SYSFS_VALUE(modem, img_loading, img);

			/* The filename for each image is name_nn, where nn is
			 * the 2 digit image index.
			 */
			pmi_name[0] = '\0';
			snprintf(pmi_name, (strlen(modem->name) + 5), "%s_%02u",
				 modem->name, img);

			/* Have to change the descriptor name so it boots the
			 * correct file.
			 */
			image->desc.name = pmi_name;

			/* Try to boot the image. */
			ret = pil_boot(&image->desc);
			if (ret) {
				SET_SYSFS_VALUE(modem, status,
					MODEM_STATUS_PMI_NOT_FOUND);
				if (!modem->id) {
					/* This is a catastrophic failure.
					 * Modem 0 must load.
					 */
					pil_shutdown(&drv->q6->desc);
					break;
				} else
					/* This image didn't load, but it's not
					 * a catastrophic failure; continue
					 * loading.
					 */
					ret = 0;
			} else {
				/* Update stats */
				SET_SYSFS_VALUE(modem, last_img_loaded, img);
				SET_SYSFS_VALUE(modem, img_loading, -1);
				SET_SYSFS_VALUE(modem, imgs_loaded,
					modem->imgs_loaded + 1);
				SET_SYSFS_VALUE(modem, status,
					MODEM_STATUS_PMI_LOADED);
			}
		}

		if (modem->imgs_loaded == 0)
			SET_SYSFS_VALUE(modem, status, MODEM_STATUS_PMI_ERROR);

		/* Free the allocated name */
		kfree(pmi_name);

		if (index == (drv->max_num_modems - 1))
			/* Tell the MBA this was the last RMB */
			ret = pil_femto_modem_send_rmb_advance(
				image->rmb_base,
				RMB_ADVANCE_COMPLETE);
		else {
			/* Tell the MBA to move to the next RMB.
			 * Note that the MBA needs the actual id of the
			 * modem specified in the device tree,
			 * not the logical index.
			 */
			ret = pil_femto_modem_send_rmb_advance(
				image->rmb_base,
				drv->modem[(index + 1)].id);

			if (ret) {
				/* This is a catastrophic failure; we've
				 * gotten out of sync with the MBA.
				 */
				SET_SYSFS_VALUE(modem, status,
					MODEM_STATUS_ADVANCE_FAILED);
				pil_shutdown(&drv->q6->desc);
				break;
			}
		}
	}

	return ret;
}

/*
 * The following are for sysfs
 */
#define TO_DRV(attr, elem) \
	container_of(attr, struct femto_modem_data, elem)
static ssize_t show_mba_status(struct kobject *kobj,
			       struct kobj_attribute *attr,
			       char *buf)
{
	struct femto_modem_data *drv = TO_DRV(attr, kobj_attr_mba_status);

	if (!drv)
		return -EINVAL;

	return scnprintf(buf, PAGE_SIZE, "%s\n",
			 modem_status_str[drv->mba_status]);
}

static ssize_t store_enable(struct kobject *kobj,
			    struct kobj_attribute *attr,
			    const char *buf,
			    size_t count)
{
	struct femto_modem_data *drv = TO_DRV(attr, kobj_attr_enable);
	u8 enable_val;
	int ret;

	if (!drv)
		return -EINVAL;

	if (kstrtou8(buf, 0, &enable_val))
		return -EINVAL;

	if (enable_val > 1)
		return -EINVAL;

	/* Only start/stop if it's different. */
	if (enable_val != drv->enable) {
		if (enable_val)
			ret = pil_femto_modem_start(drv);
		else
			ret = pil_femto_modem_stop(drv);
		if (ret)
			return ret;
		SET_SYSFS_VALUE(drv, enable, enable_val);
	}

	return count;
}

static ssize_t show_enable(struct kobject *kobj,
			   struct kobj_attribute *attr,
			   char *buf)
{
	struct femto_modem_data *drv = TO_DRV(attr, kobj_attr_enable);

	if (!drv)
		return -EINVAL;

	return scnprintf(buf, PAGE_SIZE, "%s\n",
		(drv->enable ? "ENABLED" : "DISABLED"));
}

#define TO_MODEM(attr, elem) \
	container_of(attr, struct modem_pil_data, elem)

static ssize_t show_status(struct kobject *kobj,
			   struct kobj_attribute *attr,
			   char *buf)
{
	struct modem_pil_data *modem = TO_MODEM(attr, kobj_attr_status);

	if (!modem)
		return -EINVAL;

	return scnprintf(buf, PAGE_SIZE, "%s\n",
		modem_status_str[modem->status]);
}

static ssize_t show_imgs_loaded(struct kobject *kobj,
				struct kobj_attribute *attr,
				char *buf)
{
	struct modem_pil_data *modem = TO_MODEM(attr, kobj_attr_imgs_loaded);

	if (!modem)
		return -EINVAL;

	return scnprintf(buf, PAGE_SIZE, "%u\n", modem->imgs_loaded);
}

static ssize_t show_last_img_loaded(struct kobject *kobj,
				    struct kobj_attribute *attr,
				    char *buf)
{
	struct modem_pil_data *modem =
		TO_MODEM(attr, kobj_attr_last_img_loaded);

	if (!modem)
		return -EINVAL;

	if (modem->last_img_loaded < 0)
		return scnprintf(buf, PAGE_SIZE, "NONE\n");
	else
		return scnprintf(buf, PAGE_SIZE, "%s_%02d\n", modem->name,
			modem->last_img_loaded);
}

static ssize_t show_img_loading(struct kobject *kobj,
				struct kobj_attribute *attr,
				char *buf)
{
	struct modem_pil_data *modem = TO_MODEM(attr, kobj_attr_img_loading);

	if (!modem)
		return -EINVAL;

	if (modem->img_loading < 0)
		return scnprintf(buf, PAGE_SIZE, "NONE\n");
	else
		return scnprintf(buf, PAGE_SIZE, "%s_%02d\n", modem->name,
			modem->img_loading);
}

static int pil_femto_modem_create_sysfs(struct femto_modem_data *drv)
{
	int ret = 0;

	if (!drv)
		return -EINVAL;

	/* Create the sysfs kobj */
	drv->kobj = kobject_create_and_add("femto_modem", kernel_kobj);
	if (!drv->kobj)
		return -ENOMEM;

	/* Allocate memory for the group */
	drv->attr_grp.attrs =
		kzalloc(sizeof(struct attribute *) * 2, GFP_KERNEL);

	if (!drv->attr_grp.attrs) {
		ret = -ENOMEM;
		goto cleanup_kobj;
	}

	/* Create the attributes and add them to the group */
	MAKE_RO_ATTR(mba_status, drv, 0);
	MAKE_RW_ATTR(enable, drv, 1);

	/* Create sysfs group*/
	ret = sysfs_create_group(drv->kobj, &drv->attr_grp);
	if (ret)
		goto cleanup_grp;

	drv->mba_status = MODEM_STATUS_OFFLINE;
	drv->enable = 0;

	return ret;

cleanup_grp:
	kzfree(drv->attr_grp.attrs);
	drv->attr_grp.attrs = NULL;

cleanup_kobj:
	kobject_put(drv->kobj);
	drv->kobj = NULL;

	return ret;
}

static int pil_femto_modem_desc_create_sysfs(struct femto_modem_data *drv,
					     struct modem_pil_data *modem)
{
	int ret = 0;
	char sysfs_dirname_str[10];

	if (!drv || !modem)
		return -EINVAL;

	/* Generate the name for the directory */
	sysfs_dirname_str[0] = '\0';
	snprintf(sysfs_dirname_str, sizeof(sysfs_dirname_str),
		 "modem%d", modem->id);

	/* Create the kobj as a child of the overall driver kobj */
	modem->kobj = kobject_create_and_add(sysfs_dirname_str, drv->kobj);
	if (!modem->kobj)
		return -ENOMEM;

	/* Allocate memory for the group */
	modem->attr_grp.attrs =
		kzalloc(sizeof(struct attribute *) * 4, GFP_KERNEL);

	if (!modem->attr_grp.attrs) {
		ret = -ENOMEM;
		goto cleanup_pil_kobj;
	}

	/* Create the attributes and add them to the group */
	MAKE_RO_ATTR(status, modem, 0);
	MAKE_RO_ATTR(imgs_loaded, modem, 1);
	MAKE_RO_ATTR(last_img_loaded, modem, 2);
	MAKE_RO_ATTR(img_loading, modem, 3);

	/* Create sysfs group*/
	ret = sysfs_create_group(modem->kobj, &modem->attr_grp);
	if (ret)
		goto cleanup_grp_mem;

	/* Initialize the values */
	SET_SYSFS_VALUE(modem, status, MODEM_STATUS_OFFLINE);
	SET_SYSFS_VALUE(modem, imgs_loaded, 0);
	SET_SYSFS_VALUE(modem, last_img_loaded, -1);
	SET_SYSFS_VALUE(modem, img_loading, -1);

	return ret;

cleanup_grp_mem:
	kzfree(modem->attr_grp.attrs);
	modem->attr_grp.attrs = NULL;

cleanup_pil_kobj:
	kobject_put(modem->kobj);
	modem->kobj = NULL;

	return ret;
}

static int pil_femto_modem_desc_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct device_node *node;
	struct femto_modem_data *drv;
	struct modem_pil_data *modem;
	struct pil_desc *mba_desc;
	struct resource *res;
	struct mba_data *mba;
	void __iomem *rmb;
	int ret;
	u32 id;
	bool skip_entry;

	if (!dev->of_node) {
		dev_err(dev, "%s: device tree information missing\n", __func__);
		return -ENODEV;
	}

	node = dev->of_node;

	if (dev->parent == NULL) {
		dev_err(dev, "%s: parent device missing\n", __func__);
		return -ENODEV;
	}

	drv = dev_get_drvdata(dev->parent);
	if (drv == NULL) {
		dev_err(dev, "%s: driver data not found in parent device\n",
			__func__);
		return -ENODEV;
	}

	/* Make sure there are not more modems than specified */
	if (drv->disc_modems == drv->max_num_modems) {
		dev_err(dev, "%s: found more than max of %u modems.\n",
			__func__, drv->max_num_modems);
		return -EINVAL;
	}

	ret = of_property_read_u32(node, "qcom,modem-id", &id);
	if (ret)
		return ret;

	/* Sanity check id */
	if (id > MAX_MODEM_ID)
		return -EINVAL;

	modem = &drv->modem[drv->disc_modems];
	modem->id = id;

	/* Retrieve the RMB base */
	res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "rmb_base");
	rmb = devm_request_and_ioremap(dev, res);
	if (!rmb)
		return -ENOMEM;

	/* The q6 structure should always point to modem[0] RMB regs */
	if (!modem->id && drv->q6)
		drv->q6->rmb_base = rmb;

	/* Retrieve the firmware name */
	ret = of_property_read_string(node, "qcom,firmware-name", &modem->name);
	if (ret)
		return ret;

	/* Retrieve the maximum number of images for this modem */
	ret = of_property_read_u32(node, "qcom,max-num-images",
		&modem->num_images);
	if (ret)
		return ret;

	/* Read the skip entry check flag */
	skip_entry = of_property_read_bool(node, "qcom,pil-skip-entry-check");

	/* Initialize the image attributes */
	mba = &modem->image;
	mba->rmb_base = rmb;

	/* Why isn't there one descriptor per file?  Because, the pil_desc_init
	 * function has a built-in maximum of 10, meaning it will fail after 10
	 * descriptors have been allocated.  Since there could be many more than
	 * 10 images loaded by this driver, it is necessary to have only one
	 * PIL descriptor per modem and reuse it for each image.
	 */
	mba_desc = &mba->desc;
	mba_desc->name = modem->name;
	mba_desc->dev = dev;
	mba_desc->ops = &pil_msa_mba_ops;
	mba_desc->owner = THIS_MODULE;
	mba_desc->proxy_timeout = 0;
	mba_desc->flags = skip_entry ? PIL_SKIP_ENTRY_CHECK : 0;
	mba_desc->map_fw_mem = pil_femto_modem_map_fw_mem;
	mba_desc->unmap_fw_mem = NULL;
	ret = pil_desc_init(mba_desc);
	if (ret)
		return ret;

	platform_set_drvdata(pdev, modem);

	/* Create the sysfs attributes */
	ret = pil_femto_modem_desc_create_sysfs(drv, modem);
	if (ret)
		pil_desc_release(mba_desc);

	drv->disc_modems++;
	return ret;
}

static int pil_femto_modem_desc_driver_exit(
	struct platform_device *pdev)
{
	struct modem_pil_data *modem =
		(struct modem_pil_data *)platform_get_drvdata(pdev);
	if (modem)
		pil_desc_release(&modem->image.desc);
	return 0;
}

static int pil_femto_modem_driver_probe(
	struct platform_device *pdev)
{
	struct femto_modem_data *drv;
	struct q6v5_data *q6;
	struct pil_desc *q6_desc;
	struct device_node *p_node = pdev->dev.of_node;
	int ret = 0;

	drv = devm_kzalloc(&pdev->dev, sizeof(*drv), GFP_KERNEL);
	if (!drv)
		return -ENOMEM;
	platform_set_drvdata(pdev, drv);

	/* Retrieve the maximum number of modems */
	ret = of_property_read_u32(p_node, "qcom,max-num-modems",
		&drv->max_num_modems);
	if (ret)
		return ret;

	/* Max number of modems must be greater than zero */
	if (!drv->max_num_modems)
		return -EINVAL;

	/* Allocate memory for modem structs */
	drv->modem = devm_kzalloc(&pdev->dev,
		(sizeof(*(drv->modem)) * drv->max_num_modems), GFP_KERNEL);
	if (!drv->modem)
		return -ENOMEM;

	/* This controls the loading of the MBA firmware to Q6[0] */
	q6 = pil_q6v5_init(pdev);
	if (IS_ERR(q6))
		return PTR_ERR(q6);
	drv->q6 = q6;

	/* This is needed for legacy code.  Always on. */
	q6->self_auth = 1;

	q6_desc = &q6->desc;
	q6_desc->ops = &pil_msa_pbl_ops;
	q6_desc->owner = THIS_MODULE;
	q6_desc->proxy_timeout = 0;
	q6_desc->map_fw_mem = pil_femto_modem_map_fw_mem;
	q6_desc->unmap_fw_mem = NULL;

	/* For this target, PBL interactions are different. */
	pil_msa_pbl_ops.proxy_vote = NULL;
	pil_msa_pbl_ops.proxy_unvote = NULL;

	/* Initialize the number of discovered modems. */
	drv->disc_modems = 0;

	/* Parse the device tree to get RMB regs and filenames for each modem */
	ret = of_platform_populate(p_node, NULL, NULL, &pdev->dev);
	if (ret)
		return ret;

	/* Initialize the PIL descriptor */
	ret = pil_desc_init(q6_desc);

	if (ret)
		return ret;

	/* Create the sysfs attributes */
	ret = pil_femto_modem_create_sysfs(drv);

	if (ret)
		pil_desc_release(q6_desc);

	return ret;
}

static int pil_femto_modem_driver_exit(
	struct platform_device *pdev)
{
	struct femto_modem_data *drv = platform_get_drvdata(pdev);
	pil_desc_release(&drv->q6->desc);
	return 0;
}

static struct of_device_id pil_femto_modem_match_table[] = {
	{ .compatible = "qcom,pil-femto-modem" },
	{}
};
MODULE_DEVICE_TABLE(of, pil_femto_modem_match_table);

static struct of_device_id pil_femto_modem_desc_match_table[] = {
	{ .compatible = "qcom,pil-femto-modem-desc" },
	{}
};
MODULE_DEVICE_TABLE(of, pil_femto_modem_desc_match_table);

static struct platform_driver pil_femto_modem_driver = {
	.probe = pil_femto_modem_driver_probe,
	.remove = pil_femto_modem_driver_exit,
	.driver = {
		.name = "pil-femto-modem",
		.of_match_table = pil_femto_modem_match_table,
		.owner = THIS_MODULE,
	},
};

static struct platform_driver pil_femto_modem_desc_driver = {
	.probe = pil_femto_modem_desc_probe,
	.remove = pil_femto_modem_desc_driver_exit,
	.driver = {
		.name = "pil-femto-modem-desc",
		.of_match_table = pil_femto_modem_desc_match_table,
		.owner = THIS_MODULE,
	},
};

static int __init pil_femto_modem_init(void)
{
	int result;
	result = platform_driver_register(&pil_femto_modem_driver);
	if (result)
		return result;
	return platform_driver_register(&pil_femto_modem_desc_driver);
}
module_init(pil_femto_modem_init);

static void __exit pil_femto_modem_exit(void)
{
	platform_driver_unregister(&pil_femto_modem_desc_driver);
	platform_driver_unregister(&pil_femto_modem_driver);
}
module_exit(pil_femto_modem_exit);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("Support for booting FSM99XX modems");