linux/drivers/rtc/rtc-sh.c
Jamie Lenehan 1b73e6ae45 rtc: rtc-sh: alarm support.
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>
2006-12-12 08:42:08 +09:00

645 lines
16 KiB
C

/*
* 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:
*
* Copyright (C) 2000 Philipp Rumpf <prumpf@tux.org>
* Copyright (C) 1999 Tetsuya Okada & Niibe Yutaka
*
* This file is subject to the terms and conditions of the GNU General Public
* License. See the file "COPYING" in the main directory of this archive
* for more details.
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/bcd.h>
#include <linux/rtc.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/seq_file.h>
#include <linux/interrupt.h>
#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 */
#elif defined(CONFIG_CPU_SH4)
#define rtc_reg_size sizeof(u32)
#define RTC_BIT_INVERTED 0x40 /* bug on SH7750, SH7750S */
#endif
#define RTC_REG(r) ((r) * rtc_reg_size)
#define R64CNT RTC_REG(0)
#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 */
#define RCR1_CIE 0x10 /* Carry Interrupt Enable */
#define RCR1_AIE 0x08 /* Alarm Interrupt Enable */
#define RCR1_AF 0x01 /* Alarm Flag */
/* RCR2 Bits */
#define RCR2_PEF 0x80 /* PEriodic interrupt Flag */
#define RCR2_PESMASK 0x70 /* Periodic interrupt Set */
#define RCR2_RTCEN 0x08 /* ENable RTC */
#define RCR2_ADJ 0x04 /* ADJustment (30-second) */
#define RCR2_RESET 0x02 /* Reset bit */
#define RCR2_START 0x01 /* Start bit */
struct sh_rtc {
void __iomem *regbase;
unsigned long regsize;
struct resource *res;
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)
{
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);
tmp &= ~RCR1_CF;
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);
rtc_update_irq(&rtc->rtc_dev->class_dev, 1, events);
spin_unlock(&rtc->lock);
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);
struct sh_rtc *rtc = platform_get_drvdata(pdev);
spin_lock(&rtc->lock);
rtc_update_irq(&rtc->rtc_dev->class_dev, 1, RTC_PF | RTC_IRQF);
spin_unlock(&rtc->lock);
return IRQ_HANDLED;
}
static inline void sh_rtc_setpie(struct device *dev, unsigned int enable)
{
struct sh_rtc *rtc = dev_get_drvdata(dev);
unsigned int tmp;
spin_lock_irq(&rtc->lock);
tmp = readb(rtc->regbase + RCR2);
if (enable) {
tmp &= ~RCR2_PESMASK;
tmp |= RCR2_PEF | (2 << 4);
} else
tmp &= ~(RCR2_PESMASK | RCR2_PEF);
writeb(tmp, rtc->regbase + RCR2);
spin_unlock_irq(&rtc->lock);
}
static inline void sh_rtc_setaie(struct device *dev, unsigned int enable)
{
struct sh_rtc *rtc = dev_get_drvdata(dev);
unsigned int tmp;
spin_lock_irq(&rtc->lock);
tmp = readb(rtc->regbase + RCR1);
if (!enable) {
tmp &= ~RCR1_AIE;
rtc->rearm_aie = 0;
} else if (rtc->rearm_aie == 0)
tmp |= RCR1_AIE;
writeb(tmp, rtc->regbase + RCR1);
spin_unlock_irq(&rtc->lock);
}
static int sh_rtc_open(struct device *dev)
{
struct sh_rtc *rtc = dev_get_drvdata(dev);
unsigned int tmp;
int ret;
tmp = readb(rtc->regbase + RCR1);
tmp &= ~RCR1_CF;
tmp |= RCR1_CIE;
writeb(tmp, rtc->regbase + RCR1);
ret = request_irq(rtc->periodic_irq, sh_rtc_periodic, IRQF_DISABLED,
"sh-rtc period", dev);
if (unlikely(ret)) {
dev_err(dev, "request period IRQ failed with %d, IRQ %d\n",
ret, rtc->periodic_irq);
return ret;
}
ret = request_irq(rtc->carry_irq, sh_rtc_interrupt, IRQF_DISABLED,
"sh-rtc carry", dev);
if (unlikely(ret)) {
dev_err(dev, "request carry IRQ failed with %d, IRQ %d\n",
ret, rtc->carry_irq);
free_irq(rtc->periodic_irq, dev);
goto err_bad_carry;
}
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",
ret, rtc->alarm_irq);
goto err_bad_alarm;
}
return 0;
err_bad_alarm:
free_irq(rtc->carry_irq, dev);
err_bad_carry:
free_irq(rtc->periodic_irq, dev);
return ret;
}
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);
free_irq(rtc->alarm_irq, dev);
}
static int sh_rtc_proc(struct device *dev, struct seq_file *seq)
{
struct sh_rtc *rtc = dev_get_drvdata(dev);
unsigned int tmp;
tmp = readb(rtc->regbase + RCR1);
seq_printf(seq, "alarm_IRQ\t: %s\n",
(tmp & RCR1_AIE) ? "yes" : "no");
seq_printf(seq, "carry_IRQ\t: %s\n",
(tmp & RCR1_CIE) ? "yes" : "no");
tmp = readb(rtc->regbase + RCR2);
seq_printf(seq, "periodic_IRQ\t: %s\n",
(tmp & RCR2_PEF) ? "yes" : "no");
return 0;
}
static int sh_rtc_ioctl(struct device *dev, unsigned int cmd, unsigned long arg)
{
unsigned int ret = -ENOIOCTLCMD;
switch (cmd) {
case RTC_PIE_OFF:
case RTC_PIE_ON:
sh_rtc_setpie(dev, cmd == RTC_PIE_ON);
ret = 0;
break;
case RTC_AIE_OFF:
case RTC_AIE_ON:
sh_rtc_setaie(dev, cmd == RTC_AIE_ON);
ret = 0;
break;
}
return ret;
}
static int sh_rtc_read_time(struct device *dev, struct rtc_time *tm)
{
struct platform_device *pdev = to_platform_device(dev);
struct sh_rtc *rtc = platform_get_drvdata(pdev);
unsigned int sec128, sec2, yr, yr100, cf_bit;
do {
unsigned int tmp;
spin_lock_irq(&rtc->lock);
tmp = readb(rtc->regbase + RCR1);
tmp &= ~RCR1_CF; /* Clear CF-bit */
tmp |= RCR1_CIE;
writeb(tmp, rtc->regbase + RCR1);
sec128 = readb(rtc->regbase + R64CNT);
tm->tm_sec = BCD2BIN(readb(rtc->regbase + RSECCNT));
tm->tm_min = BCD2BIN(readb(rtc->regbase + RMINCNT));
tm->tm_hour = BCD2BIN(readb(rtc->regbase + RHRCNT));
tm->tm_wday = BCD2BIN(readb(rtc->regbase + RWKCNT));
tm->tm_mday = BCD2BIN(readb(rtc->regbase + RDAYCNT));
tm->tm_mon = BCD2BIN(readb(rtc->regbase + RMONCNT)) - 1;
#if defined(CONFIG_CPU_SH4)
yr = readw(rtc->regbase + RYRCNT);
yr100 = BCD2BIN(yr >> 8);
yr &= 0xff;
#else
yr = readb(rtc->regbase + RYRCNT);
yr100 = BCD2BIN((yr == 0x99) ? 0x19 : 0x20);
#endif
tm->tm_year = (yr100 * 100 + BCD2BIN(yr)) - 1900;
sec2 = readb(rtc->regbase + R64CNT);
cf_bit = readb(rtc->regbase + RCR1) & RCR1_CF;
spin_unlock_irq(&rtc->lock);
} while (cf_bit != 0 || ((sec128 ^ sec2) & RTC_BIT_INVERTED) != 0);
#if RTC_BIT_INVERTED != 0
if ((sec128 & RTC_BIT_INVERTED))
tm->tm_sec--;
#endif
dev_dbg(&dev, "%s: tm is secs=%d, mins=%d, hours=%d, "
"mday=%d, mon=%d, year=%d, wday=%d\n",
__FUNCTION__,
tm->tm_sec, tm->tm_min, tm->tm_hour,
tm->tm_mday, tm->tm_mon + 1, tm->tm_year, tm->tm_wday);
if (rtc_valid_tm(tm) < 0)
dev_err(dev, "invalid date\n");
return 0;
}
static int sh_rtc_set_time(struct device *dev, struct rtc_time *tm)
{
struct platform_device *pdev = to_platform_device(dev);
struct sh_rtc *rtc = platform_get_drvdata(pdev);
unsigned int tmp;
int year;
spin_lock_irq(&rtc->lock);
/* Reset pre-scaler & stop RTC */
tmp = readb(rtc->regbase + RCR2);
tmp |= RCR2_RESET;
writeb(tmp, rtc->regbase + RCR2);
writeb(BIN2BCD(tm->tm_sec), rtc->regbase + RSECCNT);
writeb(BIN2BCD(tm->tm_min), rtc->regbase + RMINCNT);
writeb(BIN2BCD(tm->tm_hour), rtc->regbase + RHRCNT);
writeb(BIN2BCD(tm->tm_wday), rtc->regbase + RWKCNT);
writeb(BIN2BCD(tm->tm_mday), rtc->regbase + RDAYCNT);
writeb(BIN2BCD(tm->tm_mon + 1), rtc->regbase + RMONCNT);
#ifdef CONFIG_CPU_SH3
year = tm->tm_year % 100;
writeb(BIN2BCD(year), rtc->regbase + RYRCNT);
#else
year = (BIN2BCD((tm->tm_year + 1900) / 100) << 8) |
BIN2BCD(tm->tm_year % 100);
writew(year, rtc->regbase + RYRCNT);
#endif
/* Start RTC */
tmp = readb(rtc->regbase + RCR2);
tmp &= ~RCR2_RESET;
tmp |= RCR2_RTCEN | RCR2_START;
writeb(tmp, rtc->regbase + RCR2);
spin_unlock_irq(&rtc->lock);
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,
};
static int __devinit sh_rtc_probe(struct platform_device *pdev)
{
struct sh_rtc *rtc;
struct resource *res;
int ret = -ENOENT;
rtc = kzalloc(sizeof(struct sh_rtc), GFP_KERNEL);
if (unlikely(!rtc))
return -ENOMEM;
spin_lock_init(&rtc->lock);
rtc->periodic_irq = platform_get_irq(pdev, 0);
if (unlikely(rtc->periodic_irq < 0)) {
dev_err(&pdev->dev, "No IRQ for period\n");
goto err_badres;
}
rtc->carry_irq = platform_get_irq(pdev, 1);
if (unlikely(rtc->carry_irq < 0)) {
dev_err(&pdev->dev, "No IRQ for carry\n");
goto err_badres;
}
rtc->alarm_irq = platform_get_irq(pdev, 2);
if (unlikely(rtc->alarm_irq < 0)) {
dev_err(&pdev->dev, "No IRQ for alarm\n");
goto err_badres;
}
res = platform_get_resource(pdev, IORESOURCE_IO, 0);
if (unlikely(res == NULL)) {
dev_err(&pdev->dev, "No IO resource\n");
goto err_badres;
}
rtc->regsize = res->end - res->start + 1;
rtc->res = request_mem_region(res->start, rtc->regsize, pdev->name);
if (unlikely(!rtc->res)) {
ret = -EBUSY;
goto err_badres;
}
rtc->regbase = (void __iomem *)rtc->res->start;
if (unlikely(!rtc->regbase)) {
ret = -EINVAL;
goto err_badmap;
}
rtc->rtc_dev = rtc_device_register("sh", &pdev->dev,
&sh_rtc_ops, THIS_MODULE);
if (IS_ERR(rtc)) {
ret = PTR_ERR(rtc->rtc_dev);
goto err_badmap;
}
platform_set_drvdata(pdev, rtc);
return 0;
err_badmap:
release_resource(rtc->res);
err_badres:
kfree(rtc);
return ret;
}
static int __devexit sh_rtc_remove(struct platform_device *pdev)
{
struct sh_rtc *rtc = platform_get_drvdata(pdev);
if (likely(rtc->rtc_dev))
rtc_device_unregister(rtc->rtc_dev);
sh_rtc_setpie(&pdev->dev, 0);
sh_rtc_setaie(&pdev->dev, 0);
release_resource(rtc->res);
platform_set_drvdata(pdev, NULL);
kfree(rtc);
return 0;
}
static struct platform_driver sh_rtc_platform_driver = {
.driver = {
.name = DRV_NAME,
.owner = THIS_MODULE,
},
.probe = sh_rtc_probe,
.remove = __devexit_p(sh_rtc_remove),
};
static int __init sh_rtc_init(void)
{
return platform_driver_register(&sh_rtc_platform_driver);
}
static void __exit sh_rtc_exit(void)
{
platform_driver_unregister(&sh_rtc_platform_driver);
}
module_init(sh_rtc_init);
module_exit(sh_rtc_exit);
MODULE_DESCRIPTION("SuperH on-chip RTC driver");
MODULE_VERSION(DRV_VERSION);
MODULE_AUTHOR("Paul Mundt <lethal@linux-sh.org>, Jamie Lenehan <lenehan@twibble.org>");
MODULE_LICENSE("GPL");