diff options
Diffstat (limited to 'drivers/rtc/rtc-armada38x.c')
| -rw-r--r-- | drivers/rtc/rtc-armada38x.c | 320 | 
1 files changed, 320 insertions, 0 deletions
| diff --git a/drivers/rtc/rtc-armada38x.c b/drivers/rtc/rtc-armada38x.c new file mode 100644 index 000000000000..43e04af39e09 --- /dev/null +++ b/drivers/rtc/rtc-armada38x.c @@ -0,0 +1,320 @@ +/* + * RTC driver for the Armada 38x Marvell SoCs + * + * Copyright (C) 2015 Marvell + * + * Gregory Clement <[email protected]> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + */ + +#include <linux/delay.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/rtc.h> + +#define RTC_STATUS	    0x0 +#define RTC_STATUS_ALARM1	    BIT(0) +#define RTC_STATUS_ALARM2	    BIT(1) +#define RTC_IRQ1_CONF	    0x4 +#define RTC_IRQ1_AL_EN		    BIT(0) +#define RTC_IRQ1_FREQ_EN	    BIT(1) +#define RTC_IRQ1_FREQ_1HZ	    BIT(2) +#define RTC_TIME	    0xC +#define RTC_ALARM1	    0x10 + +#define SOC_RTC_INTERRUPT   0x8 +#define SOC_RTC_ALARM1		BIT(0) +#define SOC_RTC_ALARM2		BIT(1) +#define SOC_RTC_ALARM1_MASK	BIT(2) +#define SOC_RTC_ALARM2_MASK	BIT(3) + +struct armada38x_rtc { +	struct rtc_device   *rtc_dev; +	void __iomem	    *regs; +	void __iomem	    *regs_soc; +	spinlock_t	    lock; +	int		    irq; +}; + +/* + * According to the datasheet, the OS should wait 5us after every + * register write to the RTC hard macro so that the required update + * can occur without holding off the system bus + */ +static void rtc_delayed_write(u32 val, struct armada38x_rtc *rtc, int offset) +{ +	writel(val, rtc->regs + offset); +	udelay(5); +} + +static int armada38x_rtc_read_time(struct device *dev, struct rtc_time *tm) +{ +	struct armada38x_rtc *rtc = dev_get_drvdata(dev); +	unsigned long time, time_check, flags; + +	spin_lock_irqsave(&rtc->lock, flags); + +	time = readl(rtc->regs + RTC_TIME); +	/* +	 * WA for failing time set attempts. As stated in HW ERRATA if +	 * more than one second between two time reads is detected +	 * then read once again. +	 */ +	time_check = readl(rtc->regs + RTC_TIME); +	if ((time_check - time) > 1) +		time_check = readl(rtc->regs + RTC_TIME); + +	spin_unlock_irqrestore(&rtc->lock, flags); + +	rtc_time_to_tm(time_check, tm); + +	return 0; +} + +static int armada38x_rtc_set_time(struct device *dev, struct rtc_time *tm) +{ +	struct armada38x_rtc *rtc = dev_get_drvdata(dev); +	int ret = 0; +	unsigned long time, flags; + +	ret = rtc_tm_to_time(tm, &time); + +	if (ret) +		goto out; +	/* +	 * Setting the RTC time not always succeeds. According to the +	 * errata we need to first write on the status register and +	 * then wait for 100ms before writing to the time register to be +	 * sure that the data will be taken into account. +	 */ +	spin_lock_irqsave(&rtc->lock, flags); + +	rtc_delayed_write(0, rtc, RTC_STATUS); + +	spin_unlock_irqrestore(&rtc->lock, flags); + +	msleep(100); + +	spin_lock_irqsave(&rtc->lock, flags); + +	rtc_delayed_write(time, rtc, RTC_TIME); + +	spin_unlock_irqrestore(&rtc->lock, flags); +out: +	return ret; +} + +static int armada38x_rtc_read_alarm(struct device *dev, struct rtc_wkalrm *alrm) +{ +	struct armada38x_rtc *rtc = dev_get_drvdata(dev); +	unsigned long time, flags; +	u32 val; + +	spin_lock_irqsave(&rtc->lock, flags); + +	time = readl(rtc->regs + RTC_ALARM1); +	val = readl(rtc->regs + RTC_IRQ1_CONF) & RTC_IRQ1_AL_EN; + +	spin_unlock_irqrestore(&rtc->lock, flags); + +	alrm->enabled = val ? 1 : 0; +	rtc_time_to_tm(time,  &alrm->time); + +	return 0; +} + +static int armada38x_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *alrm) +{ +	struct armada38x_rtc *rtc = dev_get_drvdata(dev); +	unsigned long time, flags; +	int ret = 0; +	u32 val; + +	ret = rtc_tm_to_time(&alrm->time, &time); + +	if (ret) +		goto out; + +	spin_lock_irqsave(&rtc->lock, flags); + +	rtc_delayed_write(time, rtc, RTC_ALARM1); + +	if (alrm->enabled) { +			rtc_delayed_write(RTC_IRQ1_AL_EN, rtc, RTC_IRQ1_CONF); +			val = readl(rtc->regs_soc + SOC_RTC_INTERRUPT); +			writel(val | SOC_RTC_ALARM1_MASK, +			       rtc->regs_soc + SOC_RTC_INTERRUPT); +	} + +	spin_unlock_irqrestore(&rtc->lock, flags); + +out: +	return ret; +} + +static int armada38x_rtc_alarm_irq_enable(struct device *dev, +					 unsigned int enabled) +{ +	struct armada38x_rtc *rtc = dev_get_drvdata(dev); +	unsigned long flags; + +	spin_lock_irqsave(&rtc->lock, flags); + +	if (enabled) +		rtc_delayed_write(RTC_IRQ1_AL_EN, rtc, RTC_IRQ1_CONF); +	else +		rtc_delayed_write(0, rtc, RTC_IRQ1_CONF); + +	spin_unlock_irqrestore(&rtc->lock, flags); + +	return 0; +} + +static irqreturn_t armada38x_rtc_alarm_irq(int irq, void *data) +{ +	struct armada38x_rtc *rtc = data; +	u32 val; +	int event = RTC_IRQF | RTC_AF; + +	dev_dbg(&rtc->rtc_dev->dev, "%s:irq(%d)\n", __func__, irq); + +	spin_lock(&rtc->lock); + +	val = readl(rtc->regs_soc + SOC_RTC_INTERRUPT); + +	writel(val & ~SOC_RTC_ALARM1, rtc->regs_soc + SOC_RTC_INTERRUPT); +	val = readl(rtc->regs + RTC_IRQ1_CONF); +	/* disable all the interrupts for alarm 1 */ +	rtc_delayed_write(0, rtc, RTC_IRQ1_CONF); +	/* Ack the event */ +	rtc_delayed_write(RTC_STATUS_ALARM1, rtc, RTC_STATUS); + +	spin_unlock(&rtc->lock); + +	if (val & RTC_IRQ1_FREQ_EN) { +		if (val & RTC_IRQ1_FREQ_1HZ) +			event |= RTC_UF; +		else +			event |= RTC_PF; +	} + +	rtc_update_irq(rtc->rtc_dev, 1, event); + +	return IRQ_HANDLED; +} + +static struct rtc_class_ops armada38x_rtc_ops = { +	.read_time = armada38x_rtc_read_time, +	.set_time = armada38x_rtc_set_time, +	.read_alarm = armada38x_rtc_read_alarm, +	.set_alarm = armada38x_rtc_set_alarm, +	.alarm_irq_enable = armada38x_rtc_alarm_irq_enable, +}; + +static __init int armada38x_rtc_probe(struct platform_device *pdev) +{ +	struct resource *res; +	struct armada38x_rtc *rtc; +	int ret; + +	rtc = devm_kzalloc(&pdev->dev, sizeof(struct armada38x_rtc), +			    GFP_KERNEL); +	if (!rtc) +		return -ENOMEM; + +	spin_lock_init(&rtc->lock); + +	res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "rtc"); +	rtc->regs = devm_ioremap_resource(&pdev->dev, res); +	if (IS_ERR(rtc->regs)) +		return PTR_ERR(rtc->regs); +	res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "rtc-soc"); +	rtc->regs_soc = devm_ioremap_resource(&pdev->dev, res); +	if (IS_ERR(rtc->regs_soc)) +		return PTR_ERR(rtc->regs_soc); + +	rtc->irq = platform_get_irq(pdev, 0); + +	if (rtc->irq < 0) { +		dev_err(&pdev->dev, "no irq\n"); +		return rtc->irq; +	} +	if (devm_request_irq(&pdev->dev, rtc->irq, armada38x_rtc_alarm_irq, +				0, pdev->name, rtc) < 0) { +		dev_warn(&pdev->dev, "Interrupt not available.\n"); +		rtc->irq = -1; +		/* +		 * If there is no interrupt available then we can't +		 * use the alarm +		 */ +		armada38x_rtc_ops.set_alarm = NULL; +		armada38x_rtc_ops.alarm_irq_enable = NULL; +	} +	platform_set_drvdata(pdev, rtc); +	if (rtc->irq != -1) +		device_init_wakeup(&pdev->dev, 1); + +	rtc->rtc_dev = devm_rtc_device_register(&pdev->dev, pdev->name, +					&armada38x_rtc_ops, THIS_MODULE); +	if (IS_ERR(rtc->rtc_dev)) { +		ret = PTR_ERR(rtc->rtc_dev); +		dev_err(&pdev->dev, "Failed to register RTC device: %d\n", ret); +		return ret; +	} +	return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int armada38x_rtc_suspend(struct device *dev) +{ +	if (device_may_wakeup(dev)) { +		struct armada38x_rtc *rtc = dev_get_drvdata(dev); + +		return enable_irq_wake(rtc->irq); +	} + +	return 0; +} + +static int armada38x_rtc_resume(struct device *dev) +{ +	if (device_may_wakeup(dev)) { +		struct armada38x_rtc *rtc = dev_get_drvdata(dev); + +		return disable_irq_wake(rtc->irq); +	} + +	return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(armada38x_rtc_pm_ops, +			 armada38x_rtc_suspend, armada38x_rtc_resume); + +#ifdef CONFIG_OF +static const struct of_device_id armada38x_rtc_of_match_table[] = { +	{ .compatible = "marvell,armada-380-rtc", }, +	{} +}; +#endif + +static struct platform_driver armada38x_rtc_driver = { +	.driver		= { +		.name	= "armada38x-rtc", +		.pm	= &armada38x_rtc_pm_ops, +		.of_match_table = of_match_ptr(armada38x_rtc_of_match_table), +	}, +}; + +module_platform_driver_probe(armada38x_rtc_driver, armada38x_rtc_probe); + +MODULE_DESCRIPTION("Marvell Armada 38x RTC driver"); +MODULE_AUTHOR("Gregory CLEMENT <[email protected]>"); +MODULE_LICENSE("GPL"); |