rtc: rtc-sh: alarm support.
authorJamie Lenehan <lenehan@twibble.org>
Fri, 8 Dec 2006 06:26:15 +0000 (15:26 +0900)
committerPaul Mundt <lethal@linux-sh.org>
Mon, 11 Dec 2006 23:42:08 +0000 (08:42 +0900)
This adds alarm support for the RTC_ALM_SET, RTC_ALM_READ,
RTC_WKALM_SET and RTC_WKALM_RD operations to rtc-sh.

The only unusual part is the handling of the alarm interrupt. If you
clear the alarm flag (AF) while the time in the RTC still matches the
time in the alarm registers than AF is immediately re-set, and if the
alarm interrupt (AIE) is still enabled then it re-triggers. I was
originally getting around 20k+ interrupts generated during the second
when the RTC and alarm registers matches.

The solution I've used is to clear AIE when the alarm goes off and
then use the carry interrupt to re-enabled it. The carry interrupt
will check AF and re-enabled AIE if it's clear. If AF is not clear
it'll clear it and then the check will be repeated next carry
interrupt. This a bit in rtc structure that indicates that it's
waiting to have AIE re-enabled so it doesn't turn it on when it
wasn't enabled anyway.

Signed-off-by: Jamie Lenehan <lenehan@twibble.org>
Signed-off-by: Paul Mundt <lethal@linux-sh.org>
drivers/rtc/rtc-sh.c

index 8f22eb1..72ba1a7 100644 (file)
@@ -2,6 +2,7 @@
  * SuperH On-Chip RTC Support
  *
  * Copyright (C) 2006  Paul Mundt
+ * Copyright (C) 2006  Jamie Lenehan
  *
  * Based on the old arch/sh/kernel/cpu/rtc.c by:
  *
@@ -23,6 +24,9 @@
 #include <linux/spinlock.h>
 #include <linux/io.h>
 
+#define DRV_NAME       "sh-rtc"
+#define DRV_VERSION    "0.1.2"
+
 #ifdef CONFIG_CPU_SH3
 #define rtc_reg_size           sizeof(u16)
 #define RTC_BIT_INVERTED       0       /* No bug on SH7708, SH7709A */
 #define RTC_REG(r)     ((r) * rtc_reg_size)
 
 #define R64CNT         RTC_REG(0)
-#define RSECCNT                RTC_REG(1)
-#define RMINCNT                RTC_REG(2)
-#define RHRCNT         RTC_REG(3)
-#define RWKCNT         RTC_REG(4)
-#define RDAYCNT                RTC_REG(5)
-#define RMONCNT                RTC_REG(6)
-#define RYRCNT         RTC_REG(7)
-#define RSECAR         RTC_REG(8)
-#define RMINAR         RTC_REG(9)
-#define RHRAR          RTC_REG(10)
-#define RWKAR          RTC_REG(11)
-#define RDAYAR         RTC_REG(12)
-#define RMONAR         RTC_REG(13)
-#define RCR1           RTC_REG(14)
-#define RCR2           RTC_REG(15)
+
+#define RSECCNT                RTC_REG(1)      /* RTC sec */
+#define RMINCNT                RTC_REG(2)      /* RTC min */
+#define RHRCNT         RTC_REG(3)      /* RTC hour */
+#define RWKCNT         RTC_REG(4)      /* RTC week */
+#define RDAYCNT                RTC_REG(5)      /* RTC day */
+#define RMONCNT                RTC_REG(6)      /* RTC month */
+#define RYRCNT         RTC_REG(7)      /* RTC year */
+#define RSECAR         RTC_REG(8)      /* ALARM sec */
+#define RMINAR         RTC_REG(9)      /* ALARM min */
+#define RHRAR          RTC_REG(10)     /* ALARM hour */
+#define RWKAR          RTC_REG(11)     /* ALARM week */
+#define RDAYAR         RTC_REG(12)     /* ALARM day */
+#define RMONAR         RTC_REG(13)     /* ALARM month */
+#define RCR1           RTC_REG(14)     /* Control */
+#define RCR2           RTC_REG(15)     /* Control */
+
+/* ALARM Bits - or with BCD encoded value */
+#define AR_ENB         0x80    /* Enable for alarm cmp   */
 
 /* RCR1 Bits */
 #define RCR1_CF                0x80    /* Carry Flag             */
@@ -71,6 +79,7 @@ struct sh_rtc {
        unsigned int alarm_irq, periodic_irq, carry_irq;
        struct rtc_device *rtc_dev;
        spinlock_t lock;
+       int rearm_aie;
 };
 
 static irqreturn_t sh_rtc_interrupt(int irq, void *dev_id)
@@ -82,11 +91,16 @@ static irqreturn_t sh_rtc_interrupt(int irq, void *dev_id)
        spin_lock(&rtc->lock);
 
        tmp = readb(rtc->regbase + RCR1);
+       tmp &= ~RCR1_CF;
 
-       if (tmp & RCR1_AF)
-               events |= RTC_AF | RTC_IRQF;
-
-       tmp &= ~(RCR1_CF | RCR1_AF);
+       if (rtc->rearm_aie) {
+               if (tmp & RCR1_AF)
+                       tmp &= ~RCR1_AF;        /* try to clear AF again */
+               else {
+                       tmp |= RCR1_AIE;        /* AF has cleared, rearm IRQ */
+                       rtc->rearm_aie = 0;
+               }
+       }
 
        writeb(tmp, rtc->regbase + RCR1);
 
@@ -97,6 +111,41 @@ static irqreturn_t sh_rtc_interrupt(int irq, void *dev_id)
        return IRQ_HANDLED;
 }
 
+static irqreturn_t sh_rtc_alarm(int irq, void *dev_id)
+{
+       struct platform_device *pdev = to_platform_device(dev_id);
+       struct sh_rtc *rtc = platform_get_drvdata(pdev);
+       unsigned int tmp, events = 0;
+
+       spin_lock(&rtc->lock);
+
+       tmp = readb(rtc->regbase + RCR1);
+
+       /*
+        * If AF is set then the alarm has triggered. If we clear AF while
+        * the alarm time still matches the RTC time then AF will
+        * immediately be set again, and if AIE is enabled then the alarm
+        * interrupt will immediately be retrigger. So we clear AIE here
+        * and use rtc->rearm_aie so that the carry interrupt will keep
+        * trying to clear AF and once it stays cleared it'll re-enable
+        * AIE.
+        */
+       if (tmp & RCR1_AF) {
+               events |= RTC_AF | RTC_IRQF;
+
+               tmp &= ~(RCR1_AF|RCR1_AIE);
+
+               writeb(tmp, rtc->regbase + RCR1);
+
+               rtc->rearm_aie = 1;
+
+               rtc_update_irq(&rtc->rtc_dev->class_dev, 1, events);
+       }
+
+       spin_unlock(&rtc->lock);
+       return IRQ_HANDLED;
+}
+
 static irqreturn_t sh_rtc_periodic(int irq, void *dev_id)
 {
        struct platform_device *pdev = to_platform_device(dev_id);
@@ -140,10 +189,11 @@ static inline void sh_rtc_setaie(struct device *dev, unsigned int enable)
 
        tmp = readb(rtc->regbase + RCR1);
 
-       if (enable)
-               tmp |= RCR1_AIE;
-       else
+       if (!enable) {
                tmp &= ~RCR1_AIE;
+               rtc->rearm_aie = 0;
+       } else if (rtc->rearm_aie == 0)
+               tmp |= RCR1_AIE;
 
        writeb(tmp, rtc->regbase + RCR1);
 
@@ -178,7 +228,7 @@ static int sh_rtc_open(struct device *dev)
                goto err_bad_carry;
        }
 
-       ret = request_irq(rtc->alarm_irq, sh_rtc_interrupt, IRQF_DISABLED,
+       ret = request_irq(rtc->alarm_irq, sh_rtc_alarm, IRQF_DISABLED,
                          "sh-rtc alarm", dev);
        if (unlikely(ret)) {
                dev_err(dev, "request alarm IRQ failed with %d, IRQ %d\n",
@@ -201,6 +251,7 @@ static void sh_rtc_release(struct device *dev)
        struct sh_rtc *rtc = dev_get_drvdata(dev);
 
        sh_rtc_setpie(dev, 0);
+       sh_rtc_setaie(dev, 0);
 
        free_irq(rtc->periodic_irq, dev);
        free_irq(rtc->carry_irq, dev);
@@ -345,12 +396,136 @@ static int sh_rtc_set_time(struct device *dev, struct rtc_time *tm)
        return 0;
 }
 
+static inline int sh_rtc_read_alarm_value(struct sh_rtc *rtc, int reg_off)
+{
+       unsigned int byte;
+       int value = 0xff;       /* return 0xff for ignored values */
+
+       byte = readb(rtc->regbase + reg_off);
+       if (byte & AR_ENB) {
+               byte &= ~AR_ENB;        /* strip the enable bit */
+               value = BCD2BIN(byte);
+       }
+
+       return value;
+}
+
+static int sh_rtc_read_alarm(struct device *dev, struct rtc_wkalrm *wkalrm)
+{
+       struct platform_device *pdev = to_platform_device(dev);
+       struct sh_rtc *rtc = platform_get_drvdata(pdev);
+       struct rtc_time* tm = &wkalrm->time;
+
+       spin_lock_irq(&rtc->lock);
+
+       tm->tm_sec      = sh_rtc_read_alarm_value(rtc, RSECAR);
+       tm->tm_min      = sh_rtc_read_alarm_value(rtc, RMINAR);
+       tm->tm_hour     = sh_rtc_read_alarm_value(rtc, RHRAR);
+       tm->tm_wday     = sh_rtc_read_alarm_value(rtc, RWKAR);
+       tm->tm_mday     = sh_rtc_read_alarm_value(rtc, RDAYAR);
+       tm->tm_mon      = sh_rtc_read_alarm_value(rtc, RMONAR);
+       if (tm->tm_mon > 0)
+               tm->tm_mon -= 1; /* RTC is 1-12, tm_mon is 0-11 */
+       tm->tm_year     = 0xffff;
+
+       spin_unlock_irq(&rtc->lock);
+
+       return 0;
+}
+
+static inline void sh_rtc_write_alarm_value(struct sh_rtc *rtc,
+                                           int value, int reg_off)
+{
+       /* < 0 for a value that is ignored */
+       if (value < 0)
+               writeb(0, rtc->regbase + reg_off);
+       else
+               writeb(BIN2BCD(value) | AR_ENB,  rtc->regbase + reg_off);
+}
+
+static int sh_rtc_check_alarm(struct rtc_time* tm)
+{
+       /*
+        * The original rtc says anything > 0xc0 is "don't care" or "match
+        * all" - most users use 0xff but rtc-dev uses -1 for the same thing.
+        * The original rtc doesn't support years - some things use -1 and
+        * some 0xffff. We use -1 to make out tests easier.
+        */
+       if (tm->tm_year == 0xffff)
+               tm->tm_year = -1;
+       if (tm->tm_mon >= 0xff)
+               tm->tm_mon = -1;
+       if (tm->tm_mday >= 0xff)
+               tm->tm_mday = -1;
+       if (tm->tm_wday >= 0xff)
+               tm->tm_wday = -1;
+       if (tm->tm_hour >= 0xff)
+               tm->tm_hour = -1;
+       if (tm->tm_min >= 0xff)
+               tm->tm_min = -1;
+       if (tm->tm_sec >= 0xff)
+               tm->tm_sec = -1;
+
+       if (tm->tm_year > 9999 ||
+               tm->tm_mon >= 12 ||
+               tm->tm_mday == 0 || tm->tm_mday >= 32 ||
+               tm->tm_wday >= 7 ||
+               tm->tm_hour >= 24 ||
+               tm->tm_min >= 60 ||
+               tm->tm_sec >= 60)
+               return -EINVAL;
+
+       return 0;
+}
+
+static int sh_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *wkalrm)
+{
+       struct platform_device *pdev = to_platform_device(dev);
+       struct sh_rtc *rtc = platform_get_drvdata(pdev);
+       unsigned int rcr1;
+       struct rtc_time *tm = &wkalrm->time;
+       int mon, err;
+
+       err = sh_rtc_check_alarm(tm);
+       if (unlikely(err < 0))
+               return err;
+
+       spin_lock_irq(&rtc->lock);
+
+       /* disable alarm interrupt and clear flag */
+       rcr1 = readb(rtc->regbase + RCR1);
+       rcr1 &= ~RCR1_AF;
+       writeb(rcr1 & ~RCR1_AIE, rtc->regbase + RCR1);
+
+       rtc->rearm_aie = 0;
+
+       /* set alarm time */
+       sh_rtc_write_alarm_value(rtc, tm->tm_sec,  RSECAR);
+       sh_rtc_write_alarm_value(rtc, tm->tm_min,  RMINAR);
+       sh_rtc_write_alarm_value(rtc, tm->tm_hour, RHRAR);
+       sh_rtc_write_alarm_value(rtc, tm->tm_wday, RWKAR);
+       sh_rtc_write_alarm_value(rtc, tm->tm_mday, RDAYAR);
+       mon = tm->tm_mon;
+       if (mon >= 0)
+               mon += 1;
+       sh_rtc_write_alarm_value(rtc, mon, RMONAR);
+
+       /* Restore interrupt activation status */
+       writeb(rcr1, rtc->regbase + RCR1);
+
+       spin_unlock_irq(&rtc->lock);
+
+       return 0;
+}
+
 static struct rtc_class_ops sh_rtc_ops = {
        .open           = sh_rtc_open,
        .release        = sh_rtc_release,
        .ioctl          = sh_rtc_ioctl,
        .read_time      = sh_rtc_read_time,
        .set_time       = sh_rtc_set_time,
+       .read_alarm     = sh_rtc_read_alarm,
+       .set_alarm      = sh_rtc_set_alarm,
        .proc           = sh_rtc_proc,
 };
 
@@ -443,7 +618,7 @@ static int __devexit sh_rtc_remove(struct platform_device *pdev)
 }
 static struct platform_driver sh_rtc_platform_driver = {
        .driver         = {
-               .name   = "sh-rtc",
+               .name   = DRV_NAME,
                .owner  = THIS_MODULE,
        },
        .probe          = sh_rtc_probe,
@@ -464,5 +639,6 @@ module_init(sh_rtc_init);
 module_exit(sh_rtc_exit);
 
 MODULE_DESCRIPTION("SuperH on-chip RTC driver");
-MODULE_AUTHOR("Paul Mundt <lethal@linux-sh.org>");
+MODULE_VERSION(DRV_VERSION);
+MODULE_AUTHOR("Paul Mundt <lethal@linux-sh.org>, Jamie Lenehan <lenehan@twibble.org>");
 MODULE_LICENSE("GPL");