root/drivers/rtc/rtc-xgene.c
// SPDX-License-Identifier: GPL-2.0+
/*
 * APM X-Gene SoC Real Time Clock Driver
 *
 * Copyright (c) 2014, Applied Micro Circuits Corporation
 * Author: Rameshwar Prasad Sahu <rsahu@apm.com>
 *         Loc Ho <lho@apm.com>
 */

#include <linux/clk.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/rtc.h>
#include <linux/slab.h>

/* RTC CSR Registers */
#define RTC_CCVR                0x00
#define RTC_CMR                 0x04
#define RTC_CLR                 0x08
#define RTC_CCR                 0x0C
#define  RTC_CCR_IE             BIT(0)
#define  RTC_CCR_MASK           BIT(1)
#define  RTC_CCR_EN             BIT(2)
#define  RTC_CCR_WEN            BIT(3)
#define RTC_STAT                0x10
#define  RTC_STAT_BIT           BIT(0)
#define RTC_RSTAT               0x14
#define RTC_EOI                 0x18
#define RTC_VER                 0x1C

struct xgene_rtc_dev {
        struct rtc_device *rtc;
        void __iomem *csr_base;
        struct clk *clk;
        unsigned int irq_wake;
        unsigned int irq_enabled;
};

static int xgene_rtc_read_time(struct device *dev, struct rtc_time *tm)
{
        struct xgene_rtc_dev *pdata = dev_get_drvdata(dev);

        rtc_time64_to_tm(readl(pdata->csr_base + RTC_CCVR), tm);
        return 0;
}

static int xgene_rtc_set_time(struct device *dev, struct rtc_time *tm)
{
        struct xgene_rtc_dev *pdata = dev_get_drvdata(dev);

        /*
         * NOTE: After the following write, the RTC_CCVR is only reflected
         *       after the update cycle of 1 seconds.
         */
        writel((u32)rtc_tm_to_time64(tm), pdata->csr_base + RTC_CLR);
        readl(pdata->csr_base + RTC_CLR); /* Force a barrier */

        return 0;
}

static int xgene_rtc_read_alarm(struct device *dev, struct rtc_wkalrm *alrm)
{
        struct xgene_rtc_dev *pdata = dev_get_drvdata(dev);

        /* If possible, CMR should be read here */
        rtc_time64_to_tm(0, &alrm->time);
        alrm->enabled = readl(pdata->csr_base + RTC_CCR) & RTC_CCR_IE;

        return 0;
}

static int xgene_rtc_alarm_irq_enable(struct device *dev, u32 enabled)
{
        struct xgene_rtc_dev *pdata = dev_get_drvdata(dev);
        u32 ccr;

        ccr = readl(pdata->csr_base + RTC_CCR);
        if (enabled) {
                ccr &= ~RTC_CCR_MASK;
                ccr |= RTC_CCR_IE;
        } else {
                ccr &= ~RTC_CCR_IE;
                ccr |= RTC_CCR_MASK;
        }
        writel(ccr, pdata->csr_base + RTC_CCR);

        return 0;
}

static int xgene_rtc_alarm_irq_enabled(struct device *dev)
{
        struct xgene_rtc_dev *pdata = dev_get_drvdata(dev);

        return readl(pdata->csr_base + RTC_CCR) & RTC_CCR_IE ? 1 : 0;
}

static int xgene_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *alrm)
{
        struct xgene_rtc_dev *pdata = dev_get_drvdata(dev);

        writel((u32)rtc_tm_to_time64(&alrm->time), pdata->csr_base + RTC_CMR);

        xgene_rtc_alarm_irq_enable(dev, alrm->enabled);

        return 0;
}

static const struct rtc_class_ops xgene_rtc_ops = {
        .read_time      = xgene_rtc_read_time,
        .set_time       = xgene_rtc_set_time,
        .read_alarm     = xgene_rtc_read_alarm,
        .set_alarm      = xgene_rtc_set_alarm,
        .alarm_irq_enable = xgene_rtc_alarm_irq_enable,
};

static irqreturn_t xgene_rtc_interrupt(int irq, void *id)
{
        struct xgene_rtc_dev *pdata = id;

        /* Check if interrupt asserted */
        if (!(readl(pdata->csr_base + RTC_STAT) & RTC_STAT_BIT))
                return IRQ_NONE;

        /* Clear interrupt */
        readl(pdata->csr_base + RTC_EOI);

        rtc_update_irq(pdata->rtc, 1, RTC_IRQF | RTC_AF);

        return IRQ_HANDLED;
}

static int xgene_rtc_probe(struct platform_device *pdev)
{
        struct xgene_rtc_dev *pdata;
        int ret;
        int irq;

        pdata = devm_kzalloc(&pdev->dev, sizeof(*pdata), GFP_KERNEL);
        if (!pdata)
                return -ENOMEM;
        platform_set_drvdata(pdev, pdata);

        pdata->csr_base = devm_platform_ioremap_resource(pdev, 0);
        if (IS_ERR(pdata->csr_base))
                return PTR_ERR(pdata->csr_base);

        pdata->rtc = devm_rtc_allocate_device(&pdev->dev);
        if (IS_ERR(pdata->rtc))
                return PTR_ERR(pdata->rtc);

        irq = platform_get_irq(pdev, 0);
        if (irq < 0)
                return irq;
        ret = devm_request_irq(&pdev->dev, irq, xgene_rtc_interrupt, 0,
                               dev_name(&pdev->dev), pdata);
        if (ret) {
                dev_err(&pdev->dev, "Could not request IRQ\n");
                return ret;
        }

        pdata->clk = devm_clk_get(&pdev->dev, NULL);
        if (IS_ERR(pdata->clk)) {
                dev_err(&pdev->dev, "Couldn't get the clock for RTC\n");
                return -ENODEV;
        }
        ret = clk_prepare_enable(pdata->clk);
        if (ret)
                return ret;

        /* Turn on the clock and the crystal */
        writel(RTC_CCR_EN, pdata->csr_base + RTC_CCR);

        ret = device_init_wakeup(&pdev->dev, true);
        if (ret) {
                clk_disable_unprepare(pdata->clk);
                return ret;
        }

        pdata->rtc->ops = &xgene_rtc_ops;
        pdata->rtc->range_max = U32_MAX;

        ret = devm_rtc_register_device(pdata->rtc);
        if (ret) {
                clk_disable_unprepare(pdata->clk);
                return ret;
        }

        return 0;
}

static void xgene_rtc_remove(struct platform_device *pdev)
{
        struct xgene_rtc_dev *pdata = platform_get_drvdata(pdev);

        xgene_rtc_alarm_irq_enable(&pdev->dev, 0);
        device_init_wakeup(&pdev->dev, false);
        clk_disable_unprepare(pdata->clk);
}

static int __maybe_unused xgene_rtc_suspend(struct device *dev)
{
        struct platform_device *pdev = to_platform_device(dev);
        struct xgene_rtc_dev *pdata = platform_get_drvdata(pdev);
        int irq;

        irq = platform_get_irq(pdev, 0);

        /*
         * 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(&pdev->dev)) {
                if (!enable_irq_wake(irq))
                        pdata->irq_wake = 1;
        } else {
                pdata->irq_enabled = xgene_rtc_alarm_irq_enabled(dev);
                xgene_rtc_alarm_irq_enable(dev, 0);
                clk_disable_unprepare(pdata->clk);
        }
        return 0;
}

static int __maybe_unused xgene_rtc_resume(struct device *dev)
{
        struct platform_device *pdev = to_platform_device(dev);
        struct xgene_rtc_dev *pdata = platform_get_drvdata(pdev);
        int irq;
        int rc;

        irq = platform_get_irq(pdev, 0);

        if (device_may_wakeup(&pdev->dev)) {
                if (pdata->irq_wake) {
                        disable_irq_wake(irq);
                        pdata->irq_wake = 0;
                }
        } else {
                rc = clk_prepare_enable(pdata->clk);
                if (rc) {
                        dev_err(dev, "Unable to enable clock error %d\n", rc);
                        return rc;
                }
                xgene_rtc_alarm_irq_enable(dev, pdata->irq_enabled);
        }

        return 0;
}

static SIMPLE_DEV_PM_OPS(xgene_rtc_pm_ops, xgene_rtc_suspend, xgene_rtc_resume);

#ifdef CONFIG_OF
static const struct of_device_id xgene_rtc_of_match[] = {
        {.compatible = "apm,xgene-rtc" },
        { }
};
MODULE_DEVICE_TABLE(of, xgene_rtc_of_match);
#endif

static struct platform_driver xgene_rtc_driver = {
        .probe          = xgene_rtc_probe,
        .remove         = xgene_rtc_remove,
        .driver         = {
                .name   = "xgene-rtc",
                .pm = &xgene_rtc_pm_ops,
                .of_match_table = of_match_ptr(xgene_rtc_of_match),
        },
};

module_platform_driver(xgene_rtc_driver);

MODULE_DESCRIPTION("APM X-Gene SoC RTC driver");
MODULE_AUTHOR("Rameshwar Sahu <rsahu@apm.com>");
MODULE_LICENSE("GPL");