diff options
| -rw-r--r-- | Documentation/devicetree/bindings/arm/msm/msm_pp2s.txt | 45 | ||||
| -rw-r--r-- | arch/arm/mach-msm/Kconfig | 8 | ||||
| -rw-r--r-- | arch/arm/mach-msm/Makefile | 1 | ||||
| -rw-r--r-- | arch/arm/mach-msm/pp2s.c | 874 |
4 files changed, 928 insertions, 0 deletions
diff --git a/Documentation/devicetree/bindings/arm/msm/msm_pp2s.txt b/Documentation/devicetree/bindings/arm/msm/msm_pp2s.txt new file mode 100644 index 00000000000..869fb4419c3 --- /dev/null +++ b/Documentation/devicetree/bindings/arm/msm/msm_pp2s.txt @@ -0,0 +1,45 @@ +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>; + }; diff --git a/arch/arm/mach-msm/Kconfig b/arch/arm/mach-msm/Kconfig index 99fbe7ba26d..e508bd5b845 100644 --- a/arch/arm/mach-msm/Kconfig +++ b/arch/arm/mach-msm/Kconfig @@ -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 diff --git a/arch/arm/mach-msm/Makefile b/arch/arm/mach-msm/Makefile index 9e52716f770..bb18018339e 100644 --- a/arch/arm/mach-msm/Makefile +++ b/arch/arm/mach-msm/Makefile @@ -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 diff --git a/arch/arm/mach-msm/pp2s.c b/arch/arm/mach-msm/pp2s.c new file mode 100644 index 00000000000..4c158479216 --- /dev/null +++ b/arch/arm/mach-msm/pp2s.c @@ -0,0 +1,874 @@ +/* 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); |
