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

Commit dd92af60 authored by Rohit Vaswani's avatar Rohit Vaswani
Browse files

msm: pp2s: FSM9900 PP2S driver



Adding pp2s notification driver.  This driver is required, for two
reasons:
  1) So that interested applications can be informed of PP2S (Pulse
     Per 2 Second) interrupts, and
  2) So that the wall clock time registers can be
     accessed/manipulated

Change-Id: I9d81ef3622743ff03bef6848f007a65e3dda2298
Acked-by: default avatarPerry Randise <prandise@qti.qualcomm.com>
Signed-off-by: default avatarRohit Vaswani <rvaswani@codeaurora.org>
parent 7198e5fb
Loading
Loading
Loading
Loading
+45 −0
Original line number Diff line number Diff line
QTI Femtocell Pulse Per 2 Second (PP2S)

PP2S is a hardware generated pulse that occurs every two seconds and
is used for synchronizing certain system behaviors as they relate to
time.

The PP2S notification driver will be used by applications for two
reasons:

  1) So that they can be informed when PP2S interrupts occur, and

  2) So that the wall clock time registers can be accessed/manipulated
     from within the driver and from user space.

Two device tree nodes have been added.

The first is for the PP2S interrupt. It is made up of the following
required properties:

- compatible: "qcom,pp2s"
- interrupts: The interrupt number
- interrupt-names: the interrupt name

Example:

    qcom,pp2s {
        compatible = "qcom,pp2s";
        interrupts = <0 219 0>;
        interrupt-names = "pp2s_irq";
    };

The second is for mapping the wall clock regisers into usable
memory. It is made up of the required following:

- compatible: "qcom,wallclock"
- reg-names: The names given to the two memory bank locations
- reg: The base address and length for each bank

Example:

    qcom,wallclock@fd4aa000 {
        compatible = "qcom,wallclock";
        reg-names = "wallclock_time_bank", "wallclock_cntrl_bank";
        reg = <0xfd4aa000 0x20>, <0xfd4a9000 0x40>;
    };
+8 −0
Original line number Diff line number Diff line
@@ -1153,4 +1153,12 @@ config KRAIT_REGULATOR
	 line from the PMIC. This supply line is powered by multiple
	 regulators running in ganged mode inside the PMIC. Enable
	 this option to support such configurations.

config MSM_PP2S_FEMTO
	bool "FSM99XX PP2S Interrupt Notification"
	depends on ARCH_FSM9900
	help
	  Support for being notified relative to a PP2S pulse
	  Select Y if you want this notification to manifest.
	  If unsure, select N.
endif
+1 −0
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ obj-$(CONFIG_ARCH_MSM8974) += board-8974.o board-8974-gpiomux.o
obj-$(CONFIG_ARCH_MSM8974) += clock-rpm-8974.o clock-gcc-8974.o clock-mmss-8974.o clock-lpass-8974.o clock-mdss-8974.o
obj-$(CONFIG_KRAIT_REGULATOR) += krait-regulator.o  krait-regulator-pmic.o
obj-$(CONFIG_ARCH_MDM9630) += board-9630.o board-9630-gpiomux.o
obj-$(CONFIG_MSM_PP2S_FEMTO) += pp2s.o
obj-$(CONFIG_ARCH_MSMSAMARIUM) += board-samarium.o board-samarium-gpiomux.o
obj-$(CONFIG_ARCH_MSMSAMARIUM) += clock-samarium.o clock-mdss-8974.o
obj-$(CONFIG_ARCH_MPQ8092) += board-8092.o board-8092-gpiomux.o
+874 −0
Original line number Diff line number Diff line
/* Copyright (c) 2014, 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/interrupt.h>
#include <linux/irq.h>
#include <linux/platform_device.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/of.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/io.h>
#include <asm/div64.h>

#define PP2S_MODULE_NAME "PP2S"
#define WC_MODULE_NAME   "wallclock"

/*
 * Logging macros...
 */
#define LOG_DRVR_DEBUG(args...) pr_debug(args)
#define LOG_DRVR_ERR(args...)   pr_err(args)

/*
 * The following defines how many sysfs nodes the driver is creating.
 */
#define NUM_PP2S_SYSFS_REG_NODES 1

#define NUM_WC_SYSFS_REG_NODES 6

#define ATTR_PTR_SIZE sizeof(struct attribute *)

/*
 * The following deal with register locations in virtual memory.
 *
 * More specifically, we're mapping in base addresses and a length
 * (that come from the device tree) during the probe and then the
 * registers are just offsets from their respective base...
 */
#define BASEADDR_PLUS(base, off)                        \
	((void *) ((u32) (base) + (u32) (off)))

#define WC_SECS_ADDR       BASEADDR_PLUS(time_bank,  0x00000014)
#define WC_NSECS_ADDR      BASEADDR_PLUS(time_bank,  0x00000018)
#define WC_CONTROL_ADDR    BASEADDR_PLUS(cntrl_bank, 0x00000020)
#define WC_PULSECNT_ADDR   BASEADDR_PLUS(cntrl_bank, 0x00000024)
#define WC_CLOCKCNT_ADDR   BASEADDR_PLUS(cntrl_bank, 0x0000002C)
#define WC_SNAPSHOT_ADDR   BASEADDR_PLUS(cntrl_bank, 0x00000028)

/*
 * Number of bits in the nanosecond register to be used for the
 * storage of leap seconds...
 */
#define BITS_FOR_LEAP 6

/*
 * The rate the free running clock runs at...
 */
#define CLK_RATE 122880000

/*
 * The difference (in seconds) between the unix and gps epochs...
 */
#define UNIX_EPOCH_TO_GPS_EPOCH_GAP 315964819

/*
 * 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 = read_##_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 = read_##_name;                    \
	(_ptr)->kobj_attr_##_name.store = write_##_name;                  \
	sysfs_attr_init(&(_ptr)->kobj_attr_##_name.attr);                 \
	(_ptr)->attr_grp.attrs[_index] = &(_ptr)->kobj_attr_##_name.attr;

/*
 * The following will break any pending poll/select(s) on a sysfs
 * node...
 */
#define RELEASE_SELECT_ON_ATTR(_ptr, _attr)                     \
	sysfs_notify((_ptr)->kobj, NULL, __stringify(_attr))

/*
 * The following is for the creation of pp2s sysfs entries.
 */
struct pp2s_sysfs_t {
	struct kobject        *kobj;
	struct attribute_group attr_grp;

	struct kobj_attribute  kobj_attr_pp2s_cnt;
	unsigned long long     pp2s_cnt;
};

/*
 * The following is for the creation of wallclock sysfs entries.
 */
struct wc_sysfs_t {
	struct kobject        *kobj;
	struct attribute_group attr_grp;
	struct kobj_attribute  kobj_attr_cntrl_reg;
	u32                    cntrl_reg;
	struct kobj_attribute  kobj_attr_secs_reg;
	u32                    secs_reg;
	struct kobj_attribute  kobj_attr_nsecs_reg;
	u32                    nsecs_reg;
	struct kobj_attribute  kobj_attr_pulsecnt_reg;
	u32                    pulsecnt_reg;
	struct kobj_attribute  kobj_attr_clockcnt_reg;
	u32                    clockcnt_reg;
	struct kobj_attribute  kobj_attr_snapshot_reg;
	u32                    snapshot_reg;
};

/*
 * Some rudimentary internal state...
 */
static int                      pp2s_irq;
static struct pp2s_sysfs_t      pp2s_sysfs;
static struct workqueue_struct *workqueue;
static struct work_struct       bottom_work;
static struct wc_sysfs_t        wc_sysfs;

/*
 * The following will be set to point at the two regions where the
 * wall clock registers in question live.
 *
 * More specifically:
 *
 *   *time_bank : Will be used to point at the region which holds two
 *                time registers (ie. the base time or "epoch" when
 *                the time was set), and
 *
 *   *cntrl_bank : A set of registers used to make current wall clock
 *                 time calculations relative to the base time...
 */
static void *time_bank;
static void *cntrl_bank;

/*
 * The following function takes GPS time from the FTM and converts it
 * to unix time.
 *
 * OF NOTE:
 *
 *   Upon hard sync, TFCS seeds the FTM time registers used by this
 *   function with GPS time.
 *
 *   GPS time is stored in the registers in the following way:
 *
 *   1) Two registers, referred to as the base time registers, are
 *      seeded with the time that the first PP2S arrived.  These
 *      registers never change value once set.
 *
 *   2) Two other register, "pulse count" and "snap shot," hold
 *      dynamic values that, when combined with the base time, yield
 *      the actual GPS time.
 *
 *   The "pulse count" count register holds the number of PP2S pulses
 *   that have transpired prior to the register being read.  The "snap
 *   shot" register is a latch register whose value is set by simply
 *   doing a read of the "pulse count" register.  The value being
 *   latched is that of the "clock register" which is a free running
 *   clock that's running at 122.88 MHz.  Its value goes back to 0 and
 *   free runs again on every PP2S pusle.
 *
 * Given the above, we initally calculate GPS time which then gets
 * converted to unix time thusly:
 */
static void gps_to_unix_time(
	struct timespec *ts_ptr)
{
	u32 pulses = readl_relaxed(WC_PULSECNT_ADDR);
	u32 clkCnt = readl_relaxed(WC_SNAPSHOT_ADDR);
	u32 secs   = readl_relaxed(WC_SECS_ADDR);
	u32 nsecs  = readl_relaxed(WC_NSECS_ADDR);
	u32 leaps;
	u32 rmndr, unused;
	u64 q;

	/*
	 * NOTE:
	 *
	 *   There is some bit manipulation of the nano second value
	 *   below.  This is because the nsecs register is doing duel
	 *   duty.  It not only stores nano seconds, but also, the first
	 *   BITS_FOR_LEAP low order bits are used to store leap seconds.
	 *   The remaining high order bits hold a scaled (by a factor of
	 *   16) version of nano seconds.
	 */
	leaps = nsecs & ((1 << BITS_FOR_LEAP) - 1);

	nsecs >>= BITS_FOR_LEAP;
	nsecs <<= 4;

	/*
	 * Convert PP2S pules to seconds and add that in. NOTE: The first
	 * pulse ignored, since it's the epoch...
	 */
	secs += ((pulses - 1) * 2);

	/*
	 * Now convert the free running clock into seconds and nanoseconds
	 * and add that in as well...
	 */
	secs += (u32) div_u64_rem((u64) clkCnt, CLK_RATE, &rmndr);

	q = div_u64_rem((u64) NSEC_PER_SEC, CLK_RATE, &unused);

	nsecs += (u32) ((u64) rmndr * q);

	/*
	 * And finally, the actual conversion from GPS to unix time...
	 */
	ts_ptr->tv_sec  = secs + (UNIX_EPOCH_TO_GPS_EPOCH_GAP - leaps);
	ts_ptr->tv_nsec = nsecs;
}

/*
 * Given a register address and a storage location address, this
 * routine will read a value from the register address and store it at
 * the location address.
 */
static ssize_t register_read(
	const char *caller_ptr,
	void       *reg_addr,
	char       *valfromreg_ptr)
{
	u32 val = readl_relaxed(reg_addr);

	val = scnprintf(valfromreg_ptr, PAGE_SIZE, "%u", val);

	LOG_DRVR_DEBUG(
		"%s -> read %s (from addr %p) with %u bytes\n",
		caller_ptr, valfromreg_ptr, reg_addr, val);

	return val;
}

/*
 * Given a register address and an address of a value, this routine
 * will take the value and copy it to the register address...
 */
static ssize_t register_write(
	const char *caller_ptr,
	void       *reg_addr,
	const char *valtoreg_ptr)
{
	u32 val = 0;

	sscanf(valtoreg_ptr, "%u", &val);

	writel_relaxed(val, reg_addr);

	LOG_DRVR_DEBUG(
		"%s -> wrote %u (to addr %p) with %u bytes\n",
		caller_ptr, val, reg_addr, sizeof(val));

	return strlen(valtoreg_ptr);
}

/*
 * The sysfs routine which is called when an application issues a read
 * on the cntrl_reg sysfs node...
 */
static ssize_t read_cntrl_reg(
	struct kobject        *kobj,
	struct kobj_attribute *attr,
	char                  *buf)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	return register_read(__func__, WC_CONTROL_ADDR, buf);
}

/*
 * The sysfs routine which is called when an application issues a
 * write on the cntrl_reg sysfs node...
 */
static ssize_t write_cntrl_reg(
	struct kobject        *kobj,
	struct kobj_attribute *attr,
	const char            *buf,
	size_t                 count)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	return register_write(__func__, WC_CONTROL_ADDR, buf);
}

/*
 * The sysfs routine which is called when an application issues a read
 * on the secs_reg sysfs node...
 */
static ssize_t read_secs_reg(
	struct kobject        *kobj,
	struct kobj_attribute *attr,
	char                  *buf)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	return register_read(__func__, WC_SECS_ADDR, buf);
}

/*
 * The sysfs routine which is called when an application issues a
 * write on the secs_reg sysfs node...
 */
static ssize_t write_secs_reg(
	struct kobject        *kobj,
	struct kobj_attribute *attr,
	const char            *buf,
	size_t                 count)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	return register_write(__func__, WC_SECS_ADDR, buf);
}

/*
 * The sysfs routine which is called when an application issues a read
 * on the nsecs_reg sysfs node...
 */
static ssize_t read_nsecs_reg(
	struct kobject        *kobj,
	struct kobj_attribute *attr,
	char                  *buf)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	return register_read(__func__, WC_NSECS_ADDR, buf);
}

/*
 * The sysfs routine which is called when an application issues a
 * write on the nsecs_reg sysfs node...
 */
static ssize_t write_nsecs_reg(
	struct kobject        *kobj,
	struct kobj_attribute *attr,
	const char            *buf,
	size_t                 count)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	return register_write(__func__, WC_NSECS_ADDR, buf);
}

/*
 * The sysfs routine which is called when an application issues a read
 * on the pulsecnt_reg sysfs node...
 */
static ssize_t read_pulsecnt_reg(
	struct kobject        *kobj,
	struct kobj_attribute *attr,
	char                  *buf)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	return register_read(__func__, WC_PULSECNT_ADDR, buf);
}

/*
 * The sysfs routine which is called when an application issues a
 * write on the pulsecnt_reg sysfs node...
 */
static ssize_t write_pulsecnt_reg(
	struct kobject        *kobj,
	struct kobj_attribute *attr,
	const char            *buf,
	size_t                 count)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	return register_write(__func__, WC_PULSECNT_ADDR, buf);
}

/*
 * The sysfs routine which is called when an application issues a read
 * on the clockcnt_reg sysfs node...
 */
static ssize_t read_clockcnt_reg(
	struct kobject        *kobj,
	struct kobj_attribute *attr,
	char                  *buf)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	return register_read(__func__, WC_CLOCKCNT_ADDR, buf);
}

/*
 * The sysfs routine which is called when an application issues a
 * write on the clockcnt_reg sysfs node...
 */
static ssize_t write_clockcnt_reg(
	struct kobject        *kobj,
	struct kobj_attribute *attr,
	const char            *buf,
	size_t                 count)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	return register_write(__func__, WC_CLOCKCNT_ADDR, buf);
}

/*
 * The sysfs routine which is called when an application issues a read
 * on the snapshot_reg sysfs node...
 */
static ssize_t read_snapshot_reg(
	struct kobject        *kobj,
	struct kobj_attribute *attr,
	char                  *buf)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	return register_read(__func__, WC_SNAPSHOT_ADDR, buf);
}

/*
 * The sysfs routine which is called when an application issues a
 * write on the snapshot_reg sysfs node...
 */
static ssize_t write_snapshot_reg(
	struct kobject        *kobj,
	struct kobj_attribute *attr,
	const char            *buf,
	size_t                 count)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	return register_write(__func__, WC_SNAPSHOT_ADDR, buf);
}

/*
 * The sysfs routine which is called when an application issues a read
 * on the pp2s_cnt sysfs node...
 */
static ssize_t read_pp2s_cnt(
	struct kobject        *kobj,
	struct kobj_attribute *attr,
	char                  *buf)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	return read_pulsecnt_reg(kobj, attr, buf);
}

/*
 * The pp2s interrupt handler top half schedules the following to be
 * run upon reception of the pp2s interrupt, so that any polls or
 * selects on the pp2s_cnt sysfs node can be released...
 */
static void pp2s_intr_bottom(
	struct work_struct *work)
{
	static int done = -1;

	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	/*
	 * On the first PP2S interrupt, GPS time will be set in the FTM
	 * and we'll have the first pass through this function.  At that
	 * point, it is required to take GPS time, convert it to unix
	 * time, and to set it in the kernel, hence...
	 */
	if (done <= 0) {
		struct timespec ts;
		gps_to_unix_time(&ts);
		done = !do_settimeofday(&ts);
		if (!done)
			LOG_DRVR_ERR("do_settimeofday() failed\n");
	}

	RELEASE_SELECT_ON_ATTR(&pp2s_sysfs, pp2s_cnt);
}

/*
 * The following is called upon receipt of the pp2s interrupt.  It
 * simply schedules pp2s_intr_bottom to be called...
 */
static irqreturn_t pp2s_intr_top(
	int   irq,
	void *arbDatPtr)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	INIT_WORK(&bottom_work, pp2s_intr_bottom);

	queue_work(workqueue, &bottom_work);

	pp2s_sysfs.pp2s_cnt++;

	return IRQ_HANDLED;
}

/*
 * The following will probe the device tree file for this driver's IRQ
 * number.  Once found, it will be enabled after a top half interrupt
 * handler is associated with the IRQ.
 *
 * This routine will also create the pp2s_cnt sysfs node...
 */
static int pp2s_probe(
	struct platform_device *pdev)
{
	struct resource *r;
	int              ret = 0;

	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	r = platform_get_resource_byname(pdev, IORESOURCE_IRQ, "pp2s_irq");

	if (r == 0) {
		LOG_DRVR_ERR(
			"%s: platform_get_resource_byname() failed\n",
			__func__);
		ret = -ENODEV;
		goto perr0;
	}

	pp2s_irq = r->start;

	LOG_DRVR_DEBUG(
		"%s: retrieved irq (%d) from device tree\n",
		__func__, pp2s_irq);

	ret = request_irq(
		pp2s_irq,
		pp2s_intr_top,
		IRQF_TRIGGER_RISING,
		PP2S_MODULE_NAME,
		0);

	if (ret != 0) {
		LOG_DRVR_ERR(
			"%s: request_irq() failed\n",
			__func__);
		goto perr0;
	}

	enable_irq(pp2s_irq);

	/*
	 * Create the sysfs obj...
	 */
	pp2s_sysfs.kobj = kobject_create_and_add("pp2s", kernel_kobj);

	if (pp2s_sysfs.kobj == 0) {
		ret = -ENOMEM;
		goto perr1;
	}

	/*
	 * Allocate space for sysfs attrs...
	 */
	pp2s_sysfs.attr_grp.attrs =
		kzalloc(ATTR_PTR_SIZE * NUM_PP2S_SYSFS_REG_NODES, GFP_KERNEL);

	if (pp2s_sysfs.attr_grp.attrs == 0) {
		ret = -ENOMEM;
		goto perr2;
	}

	/*
	 * Add sysfs attr...
	 */
	MAKE_RO_ATTR(pp2s_cnt, &pp2s_sysfs, 0);

	/*
	 * Create sysfs group...
	 */
	ret = sysfs_create_group(pp2s_sysfs.kobj, &pp2s_sysfs.attr_grp);

	if (ret != 0)
		goto perr3;

	return ret;

perr3:
	kzfree(pp2s_sysfs.attr_grp.attrs);
	pp2s_sysfs.attr_grp.attrs = 0;
perr2:
	kobject_put(pp2s_sysfs.kobj);
	pp2s_sysfs.kobj = 0;
perr1:
	disable_irq(pp2s_irq);
	free_irq(pp2s_irq, 0);
perr0:
	return ret;
}

/*
 * This routine will read the device tree file and initialize a few
 * important wallclock data structures...
 */
static int wc_probe(
	struct platform_device *pdev)
{
	struct resource *r;
	int              ret = 0;

	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	/*
	 * Find the address of the time bank and map it in...
	 */
	r = platform_get_resource_byname(
		pdev, IORESOURCE_MEM, "wallclock_time_bank");

	if (r == 0) {
		LOG_DRVR_ERR(
			"%s: platform_get_resource_byname(time_bank) failed\n",
			__func__);
		ret = -ENODEV;
		goto werr0;
	}

	LOG_DRVR_DEBUG(
		"%s: time_bank start(%d) size(%d)\n",
		__func__, r->start, resource_size(r));

	time_bank = ioremap_nocache(r->start, resource_size(r));

	if (time_bank == 0) {
		LOG_DRVR_ERR(
			"%s: ioremap(time_bank) failed\n",
			__func__);
		ret = -ENOMEM;
		goto werr0;
	}

	/*
	 * Find the address of the control bank and map it in...
	 */
	r = platform_get_resource_byname(
		pdev, IORESOURCE_MEM, "wallclock_cntrl_bank");

	if (r == 0) {
		LOG_DRVR_ERR(
			"%s: platform_get_resource_byname(cntrl_bank) failed\n",
			__func__);
		ret = -ENODEV;
		goto werr1;
	}

	LOG_DRVR_DEBUG(
		"%s: cntrl_bank start(%d) size(%d)\n",
		__func__, r->start, resource_size(r));

	cntrl_bank = ioremap_nocache(r->start, resource_size(r));

	if (cntrl_bank == 0) {
		LOG_DRVR_ERR(
			"%s: ioremap(cntrl_bank) failed\n",
			__func__);
		ret = -ENOMEM;
		goto werr1;
	}

	/*
	 * Create the sysfs obj...
	 */
	wc_sysfs.kobj = kobject_create_and_add("wall_clock", kernel_kobj);

	if (wc_sysfs.kobj == 0) {
		ret = -ENOMEM;
		goto werr2;
	}

	/*
	 * Allocate space for sysfs attrs...
	 */
	wc_sysfs.attr_grp.attrs =
		kzalloc(ATTR_PTR_SIZE * NUM_WC_SYSFS_REG_NODES, GFP_KERNEL);

	if (wc_sysfs.attr_grp.attrs == 0) {
		ret = -ENOMEM;
		goto werr3;
	}

	/*
	 * Add sysfs attrs...
	 */
	MAKE_RW_ATTR(cntrl_reg,    &wc_sysfs, 0);
	MAKE_RW_ATTR(secs_reg,     &wc_sysfs, 1);
	MAKE_RW_ATTR(nsecs_reg,    &wc_sysfs, 2);
	MAKE_RW_ATTR(pulsecnt_reg, &wc_sysfs, 3);
	MAKE_RW_ATTR(clockcnt_reg, &wc_sysfs, 4);
	MAKE_RW_ATTR(snapshot_reg, &wc_sysfs, 5);

	/*
	 * Create sysfs group...
	 */
	ret = sysfs_create_group(wc_sysfs.kobj, &wc_sysfs.attr_grp);

	if (ret != 0)
		goto werr4;

	return ret;

werr4:
	kzfree(wc_sysfs.attr_grp.attrs);
	wc_sysfs.attr_grp.attrs = 0;
werr3:
	kobject_put(wc_sysfs.kobj);
	wc_sysfs.kobj = 0;
werr2:
	iounmap(cntrl_bank);
	cntrl_bank = 0;
werr1:
	iounmap(time_bank);
	time_bank = 0;
werr0:
	return ret;
}

/*
 * Free up pp2s resources upon driver removal.
 */
static int pp2s_remove(
	struct platform_device *pdev)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	kzfree(pp2s_sysfs.attr_grp.attrs);
	pp2s_sysfs.attr_grp.attrs = 0;

	kobject_put(pp2s_sysfs.kobj);
	pp2s_sysfs.kobj = 0;

	disable_irq(pp2s_irq);
	free_irq(pp2s_irq, 0);

	return 0;
}

/*
 * Free up wallclock resources upon driver removal.
 */
static int wc_remove(
	struct platform_device *pdev)
{
	LOG_DRVR_DEBUG("Entering %s\n", __func__);

	kzfree(wc_sysfs.attr_grp.attrs);
	wc_sysfs.attr_grp.attrs = 0;

	kobject_put(wc_sysfs.kobj);
	wc_sysfs.kobj = 0;

	iounmap(time_bank);
	time_bank = 0;

	iounmap(cntrl_bank);
	cntrl_bank = 0;

	return 0;
}

/*
 * All of the following are standard platform driver initialization
 * calls...
 */
static struct of_device_id pp2s_match_table[] = {
	{ .compatible = "qcom,pp2s", },
	{}
};

static struct platform_driver pp2s_driver = {
	.probe  = pp2s_probe,
	.remove = pp2s_remove,
	.driver = {
		.name           = PP2S_MODULE_NAME,
		.owner          = THIS_MODULE,
		.of_match_table = pp2s_match_table,
	},
};

static struct of_device_id wc_match_table[] = {
	{ .compatible = "qcom,wallclock", },
	{}
};

static struct platform_driver wc_driver = {
	.probe  = wc_probe,
	.remove = wc_remove,
	.driver = {
		.name           = WC_MODULE_NAME,
		.owner          = THIS_MODULE,
		.of_match_table = wc_match_table,
	},
};

/*
 * Called when device driver module loaded...
 */
static int __init pp2s_init(void)
{
	int ret;

	LOG_DRVR_DEBUG(
		"%s: PP2S interrupt notifier initialization\n",
		__func__);

	memset(&pp2s_sysfs, 0, sizeof(pp2s_sysfs));

	workqueue = create_singlethread_workqueue("pp2s_wq");

	if (workqueue == 0) {
		LOG_DRVR_ERR(
			"%s: create_singlethread_workqueue() failed\n",
			__func__);
		return -ENOMEM;
	}

	ret = platform_driver_register(&pp2s_driver);

	if (ret) {
		LOG_DRVR_ERR(
		  "%s: Registration of %s failed\n",
		  __func__,
		  PP2S_MODULE_NAME);
		return ret;
	}

	ret = platform_driver_register(&wc_driver);

	if (ret) {
		LOG_DRVR_ERR(
		  "%s: Registration of %s failed\n",
		  __func__,
		  WC_MODULE_NAME);
		return ret;
	}

	return 0;
}

/*
 * Called when device driver module unloaded...
 */
static void __exit pp2s_exit(void)
{
	LOG_DRVR_DEBUG(
		"%s: PP2S interrupt notifier de-initialization\n",
		__func__);

	platform_driver_unregister(&pp2s_driver);
	platform_driver_unregister(&wc_driver);
}

MODULE_DESCRIPTION("FSM PP2S Interrupt Notification Driver");

module_init(pp2s_init);
module_exit(pp2s_exit);