[RFC PATCH v2 3/5] rtc: add qpnp rtc driver

From: Stanimir Varbanov
Date: Thu Jul 03 2014 - 09:17:10 EST


A 32bits RTC is housed inside PMIC. The RTC driver uses QPNP
SPMI interface to communicate with the PMIC RTC module.

The RTC device is divided into two sub-peripherals:
- RTC read-write peripheral having basic RTC registers
- alarm peripheral for controlling alarm

These two RTC peripherals are childrens of QPNP SPMI bus. They
use regmap to read/write to its registers into PMIC.

Signed-off-by: Stanimir Varbanov <svarbanov@xxxxxxxxxx>
---
drivers/rtc/Kconfig | 8 +
drivers/rtc/Makefile | 1 +
drivers/rtc/rtc-qpnp.c | 489 ++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 498 insertions(+), 0 deletions(-)
create mode 100644 drivers/rtc/rtc-qpnp.c

diff --git a/drivers/rtc/Kconfig b/drivers/rtc/Kconfig
index 0754f5c..eb97b0a 100644
--- a/drivers/rtc/Kconfig
+++ b/drivers/rtc/Kconfig
@@ -1365,6 +1365,14 @@ config RTC_DRV_XGENE
This driver can also be built as a module, if so, the module
will be called "rtc-xgene".

+config RTC_DRV_QPNP
+ tristate "Qualcomm QPNP PMIC RTC"
+ depends on MFD_QPNP_SPMI
+ help
+ Say Y here if you want to support the Qualcomm QPNP PMIC RTC.
+ To compile this driver as a module, choose M here: the
+ module will be called rtc-qpnp.
+
comment "HID Sensor RTC drivers"

config RTC_DRV_HID_SENSOR_TIME
diff --git a/drivers/rtc/Makefile b/drivers/rtc/Makefile
index 70347d0..52488b5 100644
--- a/drivers/rtc/Makefile
+++ b/drivers/rtc/Makefile
@@ -141,3 +141,4 @@ obj-$(CONFIG_RTC_DRV_X1205) += rtc-x1205.o
obj-$(CONFIG_RTC_DRV_XGENE) += rtc-xgene.o
obj-$(CONFIG_RTC_DRV_SIRFSOC) += rtc-sirfsoc.o
obj-$(CONFIG_RTC_DRV_MOXART) += rtc-moxart.o
+obj-$(CONFIG_RTC_DRV_QPNP) += rtc-qpnp.o
diff --git a/drivers/rtc/rtc-qpnp.c b/drivers/rtc/rtc-qpnp.c
new file mode 100644
index 0000000..ea26e62
--- /dev/null
+++ b/drivers/rtc/rtc-qpnp.c
@@ -0,0 +1,489 @@
+/* Copyright (c) 2012-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/module.h>
+#include <linux/rtc.h>
+#include <linux/spinlock.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+/* RTC/ALARM register offsets */
+#define REG_ALARM_RW 0x40
+#define REG_ALARM_CTRL1 0x46
+#define REG_ALARM_CTRL2 0x48
+#define REG_RTC_WRITE 0x40
+#define REG_RTC_CTRL 0x46
+#define REG_RTC_READ 0x48
+#define REG_PERP_SUBTYPE 0x05
+
+/* RTC_CTRL register bit fields */
+#define RTC_ENABLE BIT(7)
+#define RTC_ALARM_ENABLE BIT(7)
+#define RTC_ABORT_ENABLE BIT(0)
+#define RTC_ALARM_CLEAR BIT(0)
+
+/* RTC/ALARM peripheral subtype values */
+#define PERPH_SUBTYPE_RTC 0x1
+#define PERPH_SUBTYPE_ALARM 0x3
+#define NUM_8_BIT_RTC_REGS 0x4
+
+#define TO_SECS(arr) \
+ (arr[0] | (arr[1] << 8) | (arr[2] << 16) | (arr[3] << 24))
+
+/* rtc driver internal structure */
+struct qpnp_rtc {
+ struct regmap *regmap;
+ struct device *dev;
+ struct rtc_device *rtc;
+ spinlock_t lock; /* to protect RTC control register */
+ struct rtc_class_ops ops;
+ u8 rtc_ctrl_reg;
+ u8 alarm_ctrl_reg1;
+ u16 rtc_base;
+ u16 alarm_base;
+ int alarm_irq;
+};
+
+static int qpnp_rtc_read(struct qpnp_rtc *rtc, u8 *val, u16 off, int len)
+{
+ return regmap_bulk_read(rtc->regmap, rtc->rtc_base + off, val, len);
+}
+
+static int qpnp_rtc_write(struct qpnp_rtc *rtc, u8 *val, u16 off, int len)
+{
+ return regmap_bulk_write(rtc->regmap, rtc->rtc_base + off, val, len);
+}
+
+static int qpnp_alarm_read(struct qpnp_rtc *rtc, u8 *val, u16 off, int len)
+{
+ return regmap_bulk_read(rtc->regmap, rtc->alarm_base + off, val, len);
+}
+
+static int qpnp_alarm_write(struct qpnp_rtc *rtc, u8 *val, u16 off, int len)
+{
+ return regmap_bulk_write(rtc->regmap, rtc->alarm_base + off, val, len);
+}
+
+static int qpnp_rtc_set_time(struct device *dev, struct rtc_time *tm)
+{
+ struct qpnp_rtc *rtc = dev_get_drvdata(dev);
+ u8 value[4], reg = 0, alarm_enabled = 0, ctrl_reg;
+ u8 rtc_disabled = 0, rtc_ctrl_reg;
+ unsigned long secs, flags;
+ int ret;
+
+ rtc_tm_to_time(tm, &secs);
+
+ value[0] = (secs >> 0) & 0xff;
+ value[1] = (secs >> 8) & 0xff;
+ value[2] = (secs >> 16) & 0xff;
+ value[3] = (secs >> 24) & 0xff;
+
+ dev_dbg(dev, "Seconds value to be written to RTC = %lu\n", secs);
+
+ spin_lock_irqsave(&rtc->lock, flags);
+ ctrl_reg = rtc->alarm_ctrl_reg1;
+
+ if (ctrl_reg & RTC_ALARM_ENABLE) {
+ alarm_enabled = 1;
+ ctrl_reg &= ~RTC_ALARM_ENABLE;
+ ret = qpnp_alarm_write(rtc, &ctrl_reg, REG_ALARM_CTRL1, 1);
+ if (ret)
+ goto rtc_rw_fail;
+ } else {
+ spin_unlock_irqrestore(&rtc->lock, flags);
+ }
+
+ /*
+ * 32 bit seconds value is coverted to four 8 bit values
+ * |<------ 32 bit time value in seconds ------>|
+ * <- 8 bit ->|<- 8 bit ->|<- 8 bit ->|<- 8 bit ->|
+ * ----------------------------------------------
+ * | BYTE[3] | BYTE[2] | BYTE[1] | BYTE[0] |
+ * ----------------------------------------------
+ *
+ * RTC has four 8 bit registers for writting time in seconds:
+ * WDATA[3], WDATA[2], WDATA[1], WDATA[0]
+ *
+ * Write to the RTC registers should be done in following order
+ * Clear WDATA[0] register
+ *
+ * Write BYTE[1], BYTE[2] and BYTE[3] of time to
+ * RTC WDATA[3], WDATA[2], WDATA[1] registers
+ *
+ * Write BYTE[0] of time to RTC WDATA[0] register
+ *
+ * Clearing BYTE[0] and writting in the end will prevent any
+ * unintentional overflow from WDATA[0] to higher bytes during the
+ * write operation
+ */
+
+ /* Disable RTC H/w before writing on RTC register*/
+ rtc_ctrl_reg = rtc->rtc_ctrl_reg;
+ if (rtc_ctrl_reg & RTC_ENABLE) {
+ rtc_disabled = 1;
+ rtc_ctrl_reg &= ~RTC_ENABLE;
+ ret = qpnp_rtc_write(rtc, &rtc_ctrl_reg, REG_RTC_CTRL, 1);
+ if (ret)
+ goto rtc_rw_fail;
+ rtc->rtc_ctrl_reg = rtc_ctrl_reg;
+ }
+
+ /* Clear WDATA[0] */
+ reg = 0x0;
+ ret = qpnp_rtc_write(rtc, &reg, REG_RTC_WRITE, 1);
+ if (ret)
+ goto rtc_rw_fail;
+
+ /* Write to WDATA[3], WDATA[2] and WDATA[1] */
+ ret = qpnp_rtc_write(rtc, &value[1], REG_RTC_WRITE + 1, 3);
+ if (ret)
+ goto rtc_rw_fail;
+
+ /* Write to WDATA[0] */
+ ret = qpnp_rtc_write(rtc, value, REG_RTC_WRITE, 1);
+ if (ret)
+ goto rtc_rw_fail;
+
+ /* Enable RTC after writing on RTC register */
+ if (rtc_disabled) {
+ rtc_ctrl_reg |= RTC_ENABLE;
+ ret = qpnp_rtc_write(rtc, &rtc_ctrl_reg, REG_RTC_CTRL, 1);
+ if (ret)
+ goto rtc_rw_fail;
+ rtc->rtc_ctrl_reg = rtc_ctrl_reg;
+ }
+
+ if (alarm_enabled) {
+ ctrl_reg |= RTC_ALARM_ENABLE;
+ ret = qpnp_alarm_write(rtc, &ctrl_reg, REG_ALARM_CTRL1, 1);
+ if (ret)
+ goto rtc_rw_fail;
+ }
+
+ rtc->alarm_ctrl_reg1 = ctrl_reg;
+
+rtc_rw_fail:
+ if (alarm_enabled)
+ spin_unlock_irqrestore(&rtc->lock, flags);
+
+ return ret;
+}
+
+static int qpnp_rtc_read_time(struct device *dev, struct rtc_time *tm)
+{
+ struct qpnp_rtc *rtc = dev_get_drvdata(dev);
+ u8 value[4], reg;
+ unsigned long secs;
+ int ret;
+
+ ret = qpnp_rtc_read(rtc, value, REG_RTC_READ, NUM_8_BIT_RTC_REGS);
+ if (ret)
+ return ret;
+
+ /*
+ * Read the LSB again and check if there has been a carry over
+ * If there is, redo the read operation
+ */
+ ret = qpnp_rtc_read(rtc, &reg, REG_RTC_READ, 1);
+ if (ret)
+ return ret;
+
+ if (reg < value[0]) {
+ ret = qpnp_rtc_read(rtc, value, REG_RTC_READ,
+ NUM_8_BIT_RTC_REGS);
+ if (ret)
+ return ret;
+ }
+
+ secs = TO_SECS(value);
+ rtc_time_to_tm(secs, tm);
+
+ ret = rtc_valid_tm(tm);
+ if (ret)
+ return ret;
+
+ dev_dbg(dev, "secs = %lu, h:m:s == %d:%d:%d, d/m/y = %d/%d/%d\n",
+ secs, tm->tm_hour, tm->tm_min, tm->tm_sec,
+ tm->tm_mday, tm->tm_mon, tm->tm_year);
+
+ return 0;
+}
+
+static int qpnp_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *alarm)
+{
+ struct qpnp_rtc *rtc = dev_get_drvdata(dev);
+ unsigned long secs, secs_rtc, irq_flags;
+ struct rtc_time rtc_tm;
+ u8 value[4], ctrl_reg;
+ int ret;
+
+ rtc_tm_to_time(&alarm->time, &secs);
+
+ /*
+ * Read the current RTC time and verify if the alarm time is in the
+ * past. If yes, return invalid
+ */
+ ret = qpnp_rtc_read_time(dev, &rtc_tm);
+ if (ret)
+ return -EINVAL;
+
+ rtc_tm_to_time(&rtc_tm, &secs_rtc);
+ if (secs < secs_rtc)
+ return -EINVAL;
+
+ value[0] = (secs >> 0) & 0xff;
+ value[1] = (secs >> 8) & 0xff;
+ value[2] = (secs >> 16) & 0xff;
+ value[3] = (secs >> 24) & 0xff;
+
+ spin_lock_irqsave(&rtc->lock, irq_flags);
+
+ ret = qpnp_alarm_write(rtc, value, REG_ALARM_RW, NUM_8_BIT_RTC_REGS);
+ if (ret)
+ goto rtc_rw_fail;
+
+ ctrl_reg = alarm->enabled ?
+ (rtc->alarm_ctrl_reg1 | RTC_ALARM_ENABLE) :
+ (rtc->alarm_ctrl_reg1 & ~RTC_ALARM_ENABLE);
+
+ ret = qpnp_alarm_write(rtc, &ctrl_reg, REG_ALARM_CTRL1, 1);
+ if (ret)
+ goto rtc_rw_fail;
+
+ rtc->alarm_ctrl_reg1 = ctrl_reg;
+
+ dev_dbg(dev, "Alarm Set for h:r:s=%d:%d:%d, d/m/y=%d/%d/%d\n",
+ alarm->time.tm_hour, alarm->time.tm_min,
+ alarm->time.tm_sec, alarm->time.tm_mday,
+ alarm->time.tm_mon, alarm->time.tm_year);
+
+rtc_rw_fail:
+ spin_unlock_irqrestore(&rtc->lock, irq_flags);
+ return ret;
+}
+
+static int qpnp_rtc_read_alarm(struct device *dev, struct rtc_wkalrm *alarm)
+{
+ struct qpnp_rtc *rtc = dev_get_drvdata(dev);
+ unsigned long secs;
+ u8 value[4];
+ int ret;
+
+ ret = qpnp_alarm_read(rtc, value, REG_ALARM_RW, NUM_8_BIT_RTC_REGS);
+ if (ret)
+ return ret;
+
+ secs = TO_SECS(value);
+ rtc_time_to_tm(secs, &alarm->time);
+
+ ret = rtc_valid_tm(&alarm->time);
+ if (ret)
+ return ret;
+
+ dev_dbg(dev, "Alarm set for - h:r:s=%d:%d:%d, d/m/y=%d/%d/%d\n",
+ alarm->time.tm_hour, alarm->time.tm_min,
+ alarm->time.tm_sec, alarm->time.tm_mday,
+ alarm->time.tm_mon, alarm->time.tm_year);
+
+ return 0;
+}
+
+static int qpnp_rtc_alarm_irq_enable(struct device *dev, unsigned int enabled)
+{
+ struct qpnp_rtc *rtc = dev_get_drvdata(dev);
+ unsigned long flags;
+ u8 ctrl_reg;
+ u8 value[4] = {0};
+ int ret;
+
+ spin_lock_irqsave(&rtc->lock, flags);
+
+ ctrl_reg = rtc->alarm_ctrl_reg1;
+ ctrl_reg = enabled ?
+ (ctrl_reg | RTC_ALARM_ENABLE) : (ctrl_reg & ~RTC_ALARM_ENABLE);
+
+ ret = qpnp_alarm_write(rtc, &ctrl_reg, REG_ALARM_CTRL1, 1);
+ if (ret)
+ goto rtc_rw_fail;
+
+ rtc->alarm_ctrl_reg1 = ctrl_reg;
+
+ /* Clear Alarm register */
+ if (!enabled)
+ ret = qpnp_alarm_write(rtc, value, REG_ALARM_RW,
+ NUM_8_BIT_RTC_REGS);
+
+rtc_rw_fail:
+ spin_unlock_irqrestore(&rtc->lock, flags);
+
+ return ret;
+}
+
+static irqreturn_t qpnp_alarm_trigger(int irq, void *dev_id)
+{
+ struct qpnp_rtc *rtc = dev_id;
+ unsigned long flags;
+ u8 ctrl_reg;
+ int ret;
+
+ rtc_update_irq(rtc->rtc, 1, RTC_IRQF | RTC_AF);
+
+ spin_lock_irqsave(&rtc->lock, flags);
+
+ /* Clear the alarm enable bit */
+ ctrl_reg = rtc->alarm_ctrl_reg1;
+ ctrl_reg &= ~RTC_ALARM_ENABLE;
+
+ ret = qpnp_alarm_write(rtc, &ctrl_reg, REG_ALARM_CTRL1, 1);
+ if (ret) {
+ spin_unlock_irqrestore(&rtc->lock, flags);
+ return IRQ_HANDLED;
+ }
+
+ rtc->alarm_ctrl_reg1 = ctrl_reg;
+ spin_unlock_irqrestore(&rtc->lock, flags);
+
+ /* Set ALARM_CLR bit */
+ ctrl_reg = 0x1;
+ ret = qpnp_alarm_write(rtc, &ctrl_reg, REG_ALARM_CTRL2, 1);
+ if (ret)
+ dev_dbg(rtc->dev, "Write to ALARM control reg failed\n");
+
+ return IRQ_HANDLED;
+}
+
+static int qpnp_rtc_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct qpnp_rtc *rtc;
+ struct resource *res;
+ u8 subtype;
+ int ret;
+
+ rtc = devm_kzalloc(dev, sizeof(*rtc), GFP_KERNEL);
+ if (!rtc)
+ return -ENOMEM;
+
+ spin_lock_init(&rtc->lock);
+
+ rtc->regmap = dev_get_regmap(dev->parent, NULL);
+ if (!rtc->regmap)
+ return -ENODEV;
+
+ rtc->dev = dev;
+
+ res = platform_get_resource_byname(pdev, IORESOURCE_REG, "rtc");
+ if (!res)
+ return -ENODEV;
+
+ rtc->rtc_base = res->start;
+
+ res = platform_get_resource_byname(pdev, IORESOURCE_REG, "alarm");
+ if (!res)
+ return -ENODEV;
+
+ rtc->alarm_base = res->start;
+
+ ret = qpnp_rtc_read(rtc, &subtype, REG_PERP_SUBTYPE, 1);
+ if (ret)
+ return ret;
+
+ if (subtype != PERPH_SUBTYPE_RTC)
+ return -ENODEV;
+
+ ret = qpnp_alarm_read(rtc, &subtype, REG_PERP_SUBTYPE, 1);
+ if (ret)
+ return ret;
+
+ if (subtype != PERPH_SUBTYPE_ALARM)
+ return -ENODEV;
+
+ rtc->alarm_irq = platform_get_irq(pdev, 0);
+ if (rtc->alarm_irq < 0)
+ return rtc->alarm_irq;
+
+ ret = qpnp_rtc_read(rtc, &rtc->rtc_ctrl_reg, REG_RTC_CTRL, 1);
+ if (ret)
+ return ret;
+
+ if (!(rtc->rtc_ctrl_reg & RTC_ENABLE)) {
+ dev_dbg(dev, "RTC h/w disabled, rtc not registered\n");
+ return -ENODEV;
+ }
+
+ ret = qpnp_alarm_read(rtc, &rtc->alarm_ctrl_reg1, REG_ALARM_CTRL1, 1);
+ if (ret)
+ return ret;
+
+ rtc->alarm_ctrl_reg1 |= RTC_ABORT_ENABLE;
+ ret = qpnp_alarm_write(rtc, &rtc->alarm_ctrl_reg1, REG_ALARM_CTRL1, 1);
+ if (ret)
+ return ret;
+
+ ret = devm_request_any_context_irq(dev, rtc->alarm_irq,
+ qpnp_alarm_trigger,
+ IRQF_TRIGGER_RISING,
+ "qpnp_rtc_alarm", rtc);
+ if (ret < 0)
+ return ret;
+
+ device_init_wakeup(dev, 1);
+ enable_irq_wake(rtc->alarm_irq);
+
+ platform_set_drvdata(pdev, rtc);
+
+ rtc->ops.read_time = qpnp_rtc_read_time;
+ rtc->ops.set_alarm = qpnp_rtc_set_alarm;
+ rtc->ops.read_alarm = qpnp_rtc_read_alarm;
+ rtc->ops.alarm_irq_enable = qpnp_rtc_alarm_irq_enable;
+ rtc->ops.set_time = qpnp_rtc_set_time;
+
+ rtc->rtc = rtc_device_register("qpnp-rtc", dev, &rtc->ops, THIS_MODULE);
+ if (IS_ERR(rtc->rtc))
+ return PTR_ERR(rtc->rtc);
+
+ return 0;
+}
+
+static int qpnp_rtc_remove(struct platform_device *pdev)
+{
+ struct qpnp_rtc *rtc = platform_get_drvdata(pdev);
+
+ device_init_wakeup(rtc->dev, 0);
+ free_irq(rtc->alarm_irq, rtc);
+ rtc_device_unregister(rtc->rtc);
+
+ return 0;
+}
+
+static const struct of_device_id qpnp_rtc_table[] = {
+ { .compatible = "qcom,qpnp-rtc", },
+ {}
+};
+MODULE_DEVICE_TABLE(of, rtc_qpnp_table);
+
+static struct platform_driver qpnp_rtc_driver = {
+ .probe = qpnp_rtc_probe,
+ .remove = qpnp_rtc_remove,
+ .driver = {
+ .name = "qpnp-rtc",
+ .owner = THIS_MODULE,
+ .of_match_table = qpnp_rtc_table,
+ },
+};
+module_platform_driver(qpnp_rtc_driver);
+
+MODULE_ALIAS("platform:" KBUILD_MODNAME);
+MODULE_DESCRIPTION("SMPI PMIC RTC driver");
+MODULE_AUTHOR("The Linux Foundation");
+MODULE_LICENSE("GPL v2");
--
1.7.0.4

--
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/