diff mbox

[2/2] rtc: pcf8563: Add alarm support

Message ID 20140724164308.23803.86301.stgit@localhost
State Accepted
Headers show

Commit Message

Martin Fuzzey July 24, 2014, 4:43 p.m. UTC
Signed-off-by: Martin Fuzzey <mfuzzey@parkeon.com>
---
 drivers/rtc/rtc-pcf8563.c |  244 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 242 insertions(+), 2 deletions(-)
diff mbox

Patch

diff --git a/drivers/rtc/rtc-pcf8563.c b/drivers/rtc/rtc-pcf8563.c
index 88b5c99..8581954 100644
--- a/drivers/rtc/rtc-pcf8563.c
+++ b/drivers/rtc/rtc-pcf8563.c
@@ -26,6 +26,10 @@ 
 
 #define PCF8563_REG_ST1		0x00 /* status */
 #define PCF8563_REG_ST2		0x01
+#define PCF8563_BIT_ST2_TIE	(1 << 0)
+#define PCF8563_BIT_ST2_AIE	(1 << 1)
+#define PCF8563_BIT_ST2_TF	(1 << 2)
+#define PCF8563_BIT_ST2_AF	(1 << 3)
 
 #define PCF8563_REG_SC		0x02 /* datetime */
 #define PCF8563_REG_MN		0x03
@@ -181,6 +185,182 @@  static int pcf8563_set_datetime(struct i2c_client *client, struct rtc_time *tm)
 						&buf[PCF8563_REG_SC]);
 }
 
+static int pcf8563_rtc_read_alarm(struct device *dev, struct rtc_wkalrm *alrm)
+{
+	struct i2c_client *client = to_i2c_client(dev);
+	struct rtc_time *tm = &alrm->time;
+	uint8_t alarm_buf[3];
+	s32 status;
+	int ret;
+
+	ret = pcf8563_read_regs(client, PCF8563_REG_AMN,
+					sizeof(alarm_buf), alarm_buf);
+	if (ret)
+		return ret;
+
+	status = i2c_smbus_read_byte_data(client, PCF8563_REG_ST2);
+	if (status < 0)
+		return status;
+
+	tm->tm_sec = -1;
+	tm->tm_min = bcd2bin(alarm_buf[0] & 0x7f);
+	tm->tm_hour = bcd2bin(alarm_buf[1] & 0x3f);
+	tm->tm_mday = bcd2bin(alarm_buf[2] & 0x3f);
+	tm->tm_mon = -1;
+	tm->tm_year = -1;
+	alrm->pending = status & PCF8563_BIT_ST2_AF ? 1 : 0;
+
+	dev_dbg(dev, "%s: get alarm day=%d %02d:%02d pending=%d\n", __func__,
+		tm->tm_mday, tm->tm_hour, tm->tm_min, alrm->pending);
+
+	return 0;
+}
+
+
+static int pcf8563_rtc_enable_alarm_irq(struct i2c_client *client, bool enable)
+{
+	uint8_t st2;
+
+	dev_dbg(&client->dev, "%s %d\n", __func__, enable);
+
+	st2 = PCF8563_BIT_ST2_AF;  /* Wired AND => leave as is */
+	if (enable)
+		st2 |= PCF8563_BIT_ST2_AIE;
+
+	return i2c_smbus_write_byte_data(client, PCF8563_REG_ST2, st2);
+}
+
+
+static int pcf8563_rtc_check_alarm_valid(struct i2c_client *client,
+					struct rtc_time *alrm)
+{
+	unsigned long alrmsecs, rtcsecs;
+	struct rtc_time rtctm;
+	int ret;
+
+	ret = pcf8563_get_datetime(client, &rtctm);
+	if (ret)
+		return ret;
+
+	rtc_tm_to_time(&rtctm, &rtcsecs);
+	rtc_tm_to_time(alrm, &alrmsecs);
+
+	/* Hardware doesn't have a seconds register for alarm time */
+	if ((alrmsecs - rtcsecs) < 60) {
+		dev_dbg(&client->dev, "Alarm < 1 minute not supported\n");
+		return -EINVAL;
+	}
+
+	/* Hardware doesn't have month or year registers for alarm time */
+	rtctm.tm_mon++;
+	if (rtctm.tm_mon >= 12) {
+		rtctm.tm_mon = 0;
+		rtctm.tm_year++;
+	}
+
+	rtctm.tm_mday = min(rtctm.tm_mday,
+				rtc_month_days(rtctm.tm_mon, rtctm.tm_year));
+	rtc_tm_to_time(&rtctm, &rtcsecs);
+
+	if (alrm->tm_sec > 0)
+		rtcsecs -= 60;
+
+	if (alrmsecs > rtcsecs) {
+		dev_dbg(&client->dev, "Alarm too far in future\n");
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int pcf8563_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *alrm)
+{
+	struct i2c_client *client = to_i2c_client(dev);
+	struct rtc_time *tm = &alrm->time;
+	int min = tm->tm_min;
+	int hour = tm->tm_hour;
+	int mday = tm->tm_mday;
+	unsigned char data[3];
+	uint8_t enable;
+	int ret;
+
+	dev_dbg(dev, "%s: day=%d %02d:%02d:%02d enabled=%d\n", __func__,
+		tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec,
+		alrm->enabled);
+
+	ret = pcf8563_rtc_check_alarm_valid(client, tm);
+	if (ret)
+		return ret;
+
+	if (tm->tm_sec > 0) {
+		/* As hardware has no seconds alarm register
+		 * add an extra minute otherwise alarm interrupt
+		 * will occur before requested time causing the rtc core
+		 * to reschedule (which will be refused since < 1 min) */
+		min++;
+		if (min > 59) {
+			min = 0;
+			hour++;
+			if (hour > 23) {
+				hour = 0;
+				mday++;
+				if (mday > rtc_month_days(
+						tm->tm_mon,
+						tm->tm_year + 1900))
+					mday = 1;
+			}
+		}
+		dev_dbg(dev, "round up alarm to day=%d %02d:%02d\n",
+				mday, hour, min);
+	}
+
+	/* Disable interrupt and clear any pending alarm */
+	ret = i2c_smbus_write_byte_data(client, PCF8563_REG_ST2, 0);
+	if (ret)
+		return ret;
+
+	enable = alrm->enabled ? 0 : 0x80;  /* set to disable */
+	data[0] = bin2bcd(min) | enable;
+	data[1] = bin2bcd(hour) | enable;
+	data[2] = bin2bcd(mday) | enable;
+	ret = i2c_smbus_write_i2c_block_data(client, PCF8563_REG_AMN,
+						sizeof(data), data);
+	if (ret)
+		return ret;
+
+	return pcf8563_rtc_enable_alarm_irq(client, alrm->enabled);
+}
+
+static int pcf8563_rtc_alarm_irq_enable(struct device *dev,
+					unsigned int enabled)
+{
+	struct i2c_client *client = to_i2c_client(dev);
+
+	dev_dbg(dev, "%s %d\n", __func__, enabled);
+
+	return pcf8563_rtc_enable_alarm_irq(client, enabled);
+}
+
+
+static irqreturn_t pcf8563_irq_handler(int irq, void *data)
+{
+	struct i2c_client *client = data;
+	struct pcf8563 *pcf8563 = i2c_get_clientdata(client);
+	int ret;
+
+	dev_dbg(&client->dev, "%s\n", __func__);
+
+	ret = pcf8563_rtc_enable_alarm_irq(client, false);
+	if (ret) {
+		dev_err(&client->dev, "Failed to disable irq: %d", ret);
+		return IRQ_NONE;
+	}
+
+	rtc_update_irq(pcf8563->rtc, 1, RTC_IRQF | RTC_AF);
+
+	return IRQ_HANDLED;
+}
+
 #ifdef CONFIG_RTC_INTF_DEV
 static int pcf8563_rtc_ioctl(struct device *dev, unsigned int cmd, unsigned long arg)
 {
@@ -229,20 +409,54 @@  static int pcf8563_rtc_set_time(struct device *dev, struct rtc_time *tm)
 	return pcf8563_set_datetime(to_i2c_client(dev), tm);
 }
 
+
+#ifdef CONFIG_PM_SLEEP
+
+static int pcf8563_rtc_suspend(struct device *dev)
+{
+	struct i2c_client *client = to_i2c_client(dev);
+	int ret = 0;
+
+	if (device_may_wakeup(dev))
+		ret = enable_irq_wake(client->irq);
+
+	return ret;
+}
+
+static int pcf8563_rtc_resume(struct device *dev)
+{
+	struct i2c_client *client = to_i2c_client(dev);
+	int ret = 0;
+
+	if (device_may_wakeup(dev))
+		ret = disable_irq_wake(client->irq);
+
+	return ret;
+}
+
+#endif /* CONFIG_PM_SLEEP */
+
+
 static const struct rtc_class_ops pcf8563_rtc_ops = {
 	.ioctl		= pcf8563_rtc_ioctl,
 	.read_time	= pcf8563_rtc_read_time,
 	.set_time	= pcf8563_rtc_set_time,
+	.read_alarm	= pcf8563_rtc_read_alarm,
+	.set_alarm	= pcf8563_rtc_set_alarm,
+	.alarm_irq_enable = pcf8563_rtc_alarm_irq_enable,
 };
 
 static int pcf8563_probe(struct i2c_client *client,
 				const struct i2c_device_id *id)
 {
 	struct pcf8563 *pcf8563;
+	int ret;
 
 	dev_dbg(&client->dev, "%s\n", __func__);
 
-	if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_I2C_BLOCK))
+	if (!i2c_check_functionality(client->adapter,
+			I2C_FUNC_I2C |
+			I2C_FUNC_SMBUS_BYTE_DATA | I2C_FUNC_SMBUS_I2C_BLOCK))
 		return -ENODEV;
 
 	pcf8563 = devm_kzalloc(&client->dev, sizeof(struct pcf8563),
@@ -254,11 +468,33 @@  static int pcf8563_probe(struct i2c_client *client,
 
 	i2c_set_clientdata(client, pcf8563);
 
+	ret = pcf8563_rtc_enable_alarm_irq(client, false);
+	if (ret)
+		return ret;
+
+	if (client->irq > 0) {
+		ret = devm_request_threaded_irq(&client->dev, client->irq,
+						NULL, pcf8563_irq_handler,
+						IRQF_ONESHOT,
+						pcf8563_driver.driver.name,
+						client);
+		if (ret) {
+			dev_err(&client->dev, "unable to request IRQ\n");
+			return ret;
+		}
+
+		device_init_wakeup(&client->dev, 1);
+	}
+
 	pcf8563->rtc = devm_rtc_device_register(&client->dev,
 				pcf8563_driver.driver.name,
 				&pcf8563_rtc_ops, THIS_MODULE);
 
-	return PTR_ERR_OR_ZERO(pcf8563->rtc);
+	if (IS_ERR(pcf8563->rtc))
+		return PTR_ERR(pcf8563->rtc);
+
+
+	return 0;
 }
 
 static const struct i2c_device_id pcf8563_id[] = {
@@ -276,11 +512,15 @@  static const struct of_device_id pcf8563_of_match[] = {
 MODULE_DEVICE_TABLE(of, pcf8563_of_match);
 #endif
 
+static SIMPLE_DEV_PM_OPS(pcf8563_pm_ops,
+				pcf8563_rtc_suspend,  pcf8563_rtc_resume);
+
 static struct i2c_driver pcf8563_driver = {
 	.driver		= {
 		.name	= "rtc-pcf8563",
 		.owner	= THIS_MODULE,
 		.of_match_table = of_match_ptr(pcf8563_of_match),
+		.pm = &pcf8563_pm_ops,
 	},
 	.probe		= pcf8563_probe,
 	.id_table	= pcf8563_id,