| /* |
| * Copyright (C) 2007-2009 ST-Ericsson AB |
| * License terms: GNU General Public License (GPL) version 2 |
| * Real Time Clock interface for ST-Ericsson AB COH 901 331 RTC. |
| * Author: Linus Walleij <linus.walleij@stericsson.com> |
| * Based on rtc-pl031.c by Deepak Saxena <dsaxena@plexity.net> |
| * Copyright 2006 (c) MontaVista Software, Inc. |
| */ |
| #include <linux/init.h> |
| #include <linux/module.h> |
| #include <linux/rtc.h> |
| #include <linux/clk.h> |
| #include <linux/interrupt.h> |
| #include <linux/pm.h> |
| #include <linux/platform_device.h> |
| #include <linux/io.h> |
| #include <linux/slab.h> |
| |
| /* |
| * Registers in the COH 901 331 |
| */ |
| /* Alarm value 32bit (R/W) */ |
| #define COH901331_ALARM 0x00U |
| /* Used to set current time 32bit (R/W) */ |
| #define COH901331_SET_TIME 0x04U |
| /* Indication if current time is valid 32bit (R/-) */ |
| #define COH901331_VALID 0x08U |
| /* Read the current time 32bit (R/-) */ |
| #define COH901331_CUR_TIME 0x0cU |
| /* Event register for the "alarm" interrupt */ |
| #define COH901331_IRQ_EVENT 0x10U |
| /* Mask register for the "alarm" interrupt */ |
| #define COH901331_IRQ_MASK 0x14U |
| /* Force register for the "alarm" interrupt */ |
| #define COH901331_IRQ_FORCE 0x18U |
| |
| /* |
| * Reference to RTC block clock |
| * Notice that the frequent clk_enable()/clk_disable() on this |
| * clock is mainly to be able to turn on/off other clocks in the |
| * hierarchy as needed, the RTC clock is always on anyway. |
| */ |
| struct coh901331_port { |
| struct rtc_device *rtc; |
| struct clk *clk; |
| void __iomem *virtbase; |
| int irq; |
| #ifdef CONFIG_PM_SLEEP |
| u32 irqmaskstore; |
| #endif |
| }; |
| |
| static irqreturn_t coh901331_interrupt(int irq, void *data) |
| { |
| struct coh901331_port *rtap = data; |
| |
| clk_enable(rtap->clk); |
| /* Ack IRQ */ |
| writel(1, rtap->virtbase + COH901331_IRQ_EVENT); |
| /* |
| * Disable the interrupt. This is necessary because |
| * the RTC lives on a lower-clocked line and will |
| * not release the IRQ line until after a few (slower) |
| * clock cycles. The interrupt will be re-enabled when |
| * a new alarm is set anyway. |
| */ |
| writel(0, rtap->virtbase + COH901331_IRQ_MASK); |
| clk_disable(rtap->clk); |
| |
| /* Set alarm flag */ |
| rtc_update_irq(rtap->rtc, 1, RTC_AF); |
| |
| return IRQ_HANDLED; |
| } |
| |
| static int coh901331_read_time(struct device *dev, struct rtc_time *tm) |
| { |
| struct coh901331_port *rtap = dev_get_drvdata(dev); |
| |
| clk_enable(rtap->clk); |
| /* Check if the time is valid */ |
| if (readl(rtap->virtbase + COH901331_VALID)) { |
| rtc_time_to_tm(readl(rtap->virtbase + COH901331_CUR_TIME), tm); |
| clk_disable(rtap->clk); |
| return 0; |
| } |
| clk_disable(rtap->clk); |
| return -EINVAL; |
| } |
| |
| static int coh901331_set_mmss(struct device *dev, unsigned long secs) |
| { |
| struct coh901331_port *rtap = dev_get_drvdata(dev); |
| |
| clk_enable(rtap->clk); |
| writel(secs, rtap->virtbase + COH901331_SET_TIME); |
| clk_disable(rtap->clk); |
| |
| return 0; |
| } |
| |
| static int coh901331_read_alarm(struct device *dev, struct rtc_wkalrm *alarm) |
| { |
| struct coh901331_port *rtap = dev_get_drvdata(dev); |
| |
| clk_enable(rtap->clk); |
| rtc_time_to_tm(readl(rtap->virtbase + COH901331_ALARM), &alarm->time); |
| alarm->pending = readl(rtap->virtbase + COH901331_IRQ_EVENT) & 1U; |
| alarm->enabled = readl(rtap->virtbase + COH901331_IRQ_MASK) & 1U; |
| clk_disable(rtap->clk); |
| |
| return 0; |
| } |
| |
| static int coh901331_set_alarm(struct device *dev, struct rtc_wkalrm *alarm) |
| { |
| struct coh901331_port *rtap = dev_get_drvdata(dev); |
| unsigned long time; |
| |
| rtc_tm_to_time(&alarm->time, &time); |
| clk_enable(rtap->clk); |
| writel(time, rtap->virtbase + COH901331_ALARM); |
| writel(alarm->enabled, rtap->virtbase + COH901331_IRQ_MASK); |
| clk_disable(rtap->clk); |
| |
| return 0; |
| } |
| |
| static int coh901331_alarm_irq_enable(struct device *dev, unsigned int enabled) |
| { |
| struct coh901331_port *rtap = dev_get_drvdata(dev); |
| |
| clk_enable(rtap->clk); |
| if (enabled) |
| writel(1, rtap->virtbase + COH901331_IRQ_MASK); |
| else |
| writel(0, rtap->virtbase + COH901331_IRQ_MASK); |
| clk_disable(rtap->clk); |
| |
| return 0; |
| } |
| |
| static const struct rtc_class_ops coh901331_ops = { |
| .read_time = coh901331_read_time, |
| .set_mmss = coh901331_set_mmss, |
| .read_alarm = coh901331_read_alarm, |
| .set_alarm = coh901331_set_alarm, |
| .alarm_irq_enable = coh901331_alarm_irq_enable, |
| }; |
| |
| static int __exit coh901331_remove(struct platform_device *pdev) |
| { |
| struct coh901331_port *rtap = platform_get_drvdata(pdev); |
| |
| if (rtap) |
| clk_unprepare(rtap->clk); |
| |
| return 0; |
| } |
| |
| |
| static int __init coh901331_probe(struct platform_device *pdev) |
| { |
| int ret; |
| struct coh901331_port *rtap; |
| struct resource *res; |
| |
| rtap = devm_kzalloc(&pdev->dev, |
| sizeof(struct coh901331_port), GFP_KERNEL); |
| if (!rtap) |
| return -ENOMEM; |
| |
| res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| rtap->virtbase = devm_ioremap_resource(&pdev->dev, res); |
| if (IS_ERR(rtap->virtbase)) |
| return PTR_ERR(rtap->virtbase); |
| |
| rtap->irq = platform_get_irq(pdev, 0); |
| if (devm_request_irq(&pdev->dev, rtap->irq, coh901331_interrupt, 0, |
| "RTC COH 901 331 Alarm", rtap)) |
| return -EIO; |
| |
| rtap->clk = devm_clk_get(&pdev->dev, NULL); |
| if (IS_ERR(rtap->clk)) { |
| ret = PTR_ERR(rtap->clk); |
| dev_err(&pdev->dev, "could not get clock\n"); |
| return ret; |
| } |
| |
| /* We enable/disable the clock only to assure it works */ |
| ret = clk_prepare_enable(rtap->clk); |
| if (ret) { |
| dev_err(&pdev->dev, "could not enable clock\n"); |
| return ret; |
| } |
| clk_disable(rtap->clk); |
| |
| platform_set_drvdata(pdev, rtap); |
| rtap->rtc = devm_rtc_device_register(&pdev->dev, "coh901331", |
| &coh901331_ops, THIS_MODULE); |
| if (IS_ERR(rtap->rtc)) { |
| ret = PTR_ERR(rtap->rtc); |
| goto out_no_rtc; |
| } |
| |
| return 0; |
| |
| out_no_rtc: |
| clk_unprepare(rtap->clk); |
| return ret; |
| } |
| |
| #ifdef CONFIG_PM_SLEEP |
| static int coh901331_suspend(struct device *dev) |
| { |
| struct coh901331_port *rtap = dev_get_drvdata(dev); |
| |
| /* |
| * If this RTC alarm will be used for waking the system up, |
| * don't disable it of course. Else we just disable the alarm |
| * and await suspension. |
| */ |
| if (device_may_wakeup(dev)) { |
| enable_irq_wake(rtap->irq); |
| } else { |
| clk_enable(rtap->clk); |
| rtap->irqmaskstore = readl(rtap->virtbase + COH901331_IRQ_MASK); |
| writel(0, rtap->virtbase + COH901331_IRQ_MASK); |
| clk_disable(rtap->clk); |
| } |
| clk_unprepare(rtap->clk); |
| return 0; |
| } |
| |
| static int coh901331_resume(struct device *dev) |
| { |
| struct coh901331_port *rtap = dev_get_drvdata(dev); |
| |
| clk_prepare(rtap->clk); |
| if (device_may_wakeup(dev)) { |
| disable_irq_wake(rtap->irq); |
| } else { |
| clk_enable(rtap->clk); |
| writel(rtap->irqmaskstore, rtap->virtbase + COH901331_IRQ_MASK); |
| clk_disable(rtap->clk); |
| } |
| return 0; |
| } |
| #endif |
| |
| static SIMPLE_DEV_PM_OPS(coh901331_pm_ops, coh901331_suspend, coh901331_resume); |
| |
| static void coh901331_shutdown(struct platform_device *pdev) |
| { |
| struct coh901331_port *rtap = platform_get_drvdata(pdev); |
| |
| clk_enable(rtap->clk); |
| writel(0, rtap->virtbase + COH901331_IRQ_MASK); |
| clk_disable_unprepare(rtap->clk); |
| } |
| |
| static const struct of_device_id coh901331_dt_match[] = { |
| { .compatible = "stericsson,coh901331" }, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, coh901331_dt_match); |
| |
| static struct platform_driver coh901331_driver = { |
| .driver = { |
| .name = "rtc-coh901331", |
| .pm = &coh901331_pm_ops, |
| .of_match_table = coh901331_dt_match, |
| }, |
| .remove = __exit_p(coh901331_remove), |
| .shutdown = coh901331_shutdown, |
| }; |
| |
| module_platform_driver_probe(coh901331_driver, coh901331_probe); |
| |
| MODULE_AUTHOR("Linus Walleij <linus.walleij@stericsson.com>"); |
| MODULE_DESCRIPTION("ST-Ericsson AB COH 901 331 RTC Driver"); |
| MODULE_LICENSE("GPL"); |