75ea799df4
The current MAX8907 driver has two issues related to weekday value handling: 1) The HW WEEKDAY register has range 0..6 rather than 1..7 as documented. Note that I validated the actual HW range by observing the HW register roll from 6->0 rather than 6->7->1 as would otherwise be expected. This matches Linux's tm_wday range of 0..6. When the CMOS RAM content is lost, the date returned from the device is 2007-01-01 00:00:00, which is a Monday. The WEEKDAY register reads 1 in this case. This matches the numbering in Linux's tm_wday field. Hence we should write Linux's tm_wday value to the register without modifying it. Hence, remove the +1/-1 calculations for WEEKDAY/tm_wday. 2) There's no need to make alarms match on the WEEKDAY register, since the other fields together uniquely define the alarm date/time. Ignoring the WEEKDAY value in the match isolates the driver from any incorrect value in the current time copy of the WEEKDAY register. Each change individually, or both together, solves an issue that I observed; "hwclock -r" would time out waiting for its alarm to fire if the CMOS RAM content had been lost, and hence the WEEKDAY register value mismatched what the driver expected it to be. "hwclock -w" would solve this by over-writing the HW default WEEKDAY register value with what the driver expected. Signed-off-by: Stephen Warren <swarren@nvidia.com> Cc: <stable@vger.kernel.org> Signed-off-by: Andrew Morton <akpm@linux-foundation.org> Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
226 lines
5.2 KiB
C
226 lines
5.2 KiB
C
/*
|
|
* RTC driver for Maxim MAX8907
|
|
*
|
|
* Copyright (c) 2011-2012, NVIDIA Corporation.
|
|
*
|
|
* Based on drivers/rtc/rtc-max8925.c,
|
|
* Copyright (C) 2009-2010 Marvell International Ltd.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 as
|
|
* published by the Free Software Foundation.
|
|
*/
|
|
|
|
#include <linux/bcd.h>
|
|
#include <linux/i2c.h>
|
|
#include <linux/mfd/max8907.h>
|
|
#include <linux/module.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/regmap.h>
|
|
#include <linux/rtc.h>
|
|
#include <linux/slab.h>
|
|
|
|
enum {
|
|
RTC_SEC = 0,
|
|
RTC_MIN,
|
|
RTC_HOUR,
|
|
RTC_WEEKDAY,
|
|
RTC_DATE,
|
|
RTC_MONTH,
|
|
RTC_YEAR1,
|
|
RTC_YEAR2,
|
|
};
|
|
|
|
#define TIME_NUM 8
|
|
#define ALARM_1SEC (1 << 7)
|
|
#define HOUR_12 (1 << 7)
|
|
#define HOUR_AM_PM (1 << 5)
|
|
#define ALARM0_IRQ (1 << 3)
|
|
#define ALARM1_IRQ (1 << 2)
|
|
#define ALARM0_STATUS (1 << 2)
|
|
#define ALARM1_STATUS (1 << 1)
|
|
|
|
struct max8907_rtc {
|
|
struct max8907 *max8907;
|
|
struct regmap *regmap;
|
|
struct rtc_device *rtc_dev;
|
|
int irq;
|
|
};
|
|
|
|
static irqreturn_t max8907_irq_handler(int irq, void *data)
|
|
{
|
|
struct max8907_rtc *rtc = data;
|
|
|
|
regmap_write(rtc->regmap, MAX8907_REG_ALARM0_CNTL, 0);
|
|
|
|
rtc_update_irq(rtc->rtc_dev, 1, RTC_IRQF | RTC_AF);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static void regs_to_tm(u8 *regs, struct rtc_time *tm)
|
|
{
|
|
tm->tm_year = bcd2bin(regs[RTC_YEAR2]) * 100 +
|
|
bcd2bin(regs[RTC_YEAR1]) - 1900;
|
|
tm->tm_mon = bcd2bin(regs[RTC_MONTH] & 0x1f) - 1;
|
|
tm->tm_mday = bcd2bin(regs[RTC_DATE] & 0x3f);
|
|
tm->tm_wday = (regs[RTC_WEEKDAY] & 0x07);
|
|
if (regs[RTC_HOUR] & HOUR_12) {
|
|
tm->tm_hour = bcd2bin(regs[RTC_HOUR] & 0x01f);
|
|
if (tm->tm_hour == 12)
|
|
tm->tm_hour = 0;
|
|
if (regs[RTC_HOUR] & HOUR_AM_PM)
|
|
tm->tm_hour += 12;
|
|
} else {
|
|
tm->tm_hour = bcd2bin(regs[RTC_HOUR] & 0x03f);
|
|
}
|
|
tm->tm_min = bcd2bin(regs[RTC_MIN] & 0x7f);
|
|
tm->tm_sec = bcd2bin(regs[RTC_SEC] & 0x7f);
|
|
}
|
|
|
|
static void tm_to_regs(struct rtc_time *tm, u8 *regs)
|
|
{
|
|
u8 high, low;
|
|
|
|
high = (tm->tm_year + 1900) / 100;
|
|
low = tm->tm_year % 100;
|
|
regs[RTC_YEAR2] = bin2bcd(high);
|
|
regs[RTC_YEAR1] = bin2bcd(low);
|
|
regs[RTC_MONTH] = bin2bcd(tm->tm_mon + 1);
|
|
regs[RTC_DATE] = bin2bcd(tm->tm_mday);
|
|
regs[RTC_WEEKDAY] = tm->tm_wday;
|
|
regs[RTC_HOUR] = bin2bcd(tm->tm_hour);
|
|
regs[RTC_MIN] = bin2bcd(tm->tm_min);
|
|
regs[RTC_SEC] = bin2bcd(tm->tm_sec);
|
|
}
|
|
|
|
static int max8907_rtc_read_time(struct device *dev, struct rtc_time *tm)
|
|
{
|
|
struct max8907_rtc *rtc = dev_get_drvdata(dev);
|
|
u8 regs[TIME_NUM];
|
|
int ret;
|
|
|
|
ret = regmap_bulk_read(rtc->regmap, MAX8907_REG_RTC_SEC, regs,
|
|
TIME_NUM);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
regs_to_tm(regs, tm);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int max8907_rtc_set_time(struct device *dev, struct rtc_time *tm)
|
|
{
|
|
struct max8907_rtc *rtc = dev_get_drvdata(dev);
|
|
u8 regs[TIME_NUM];
|
|
|
|
tm_to_regs(tm, regs);
|
|
|
|
return regmap_bulk_write(rtc->regmap, MAX8907_REG_RTC_SEC, regs,
|
|
TIME_NUM);
|
|
}
|
|
|
|
static int max8907_rtc_read_alarm(struct device *dev, struct rtc_wkalrm *alrm)
|
|
{
|
|
struct max8907_rtc *rtc = dev_get_drvdata(dev);
|
|
u8 regs[TIME_NUM];
|
|
unsigned int val;
|
|
int ret;
|
|
|
|
ret = regmap_bulk_read(rtc->regmap, MAX8907_REG_ALARM0_SEC, regs,
|
|
TIME_NUM);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
regs_to_tm(regs, &alrm->time);
|
|
|
|
ret = regmap_read(rtc->regmap, MAX8907_REG_ALARM0_CNTL, &val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
alrm->enabled = !!(val & 0x7f);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int max8907_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *alrm)
|
|
{
|
|
struct max8907_rtc *rtc = dev_get_drvdata(dev);
|
|
u8 regs[TIME_NUM];
|
|
int ret;
|
|
|
|
tm_to_regs(&alrm->time, regs);
|
|
|
|
/* Disable alarm while we update the target time */
|
|
ret = regmap_write(rtc->regmap, MAX8907_REG_ALARM0_CNTL, 0);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = regmap_bulk_write(rtc->regmap, MAX8907_REG_ALARM0_SEC, regs,
|
|
TIME_NUM);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
if (alrm->enabled)
|
|
ret = regmap_write(rtc->regmap, MAX8907_REG_ALARM0_CNTL, 0x77);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static const struct rtc_class_ops max8907_rtc_ops = {
|
|
.read_time = max8907_rtc_read_time,
|
|
.set_time = max8907_rtc_set_time,
|
|
.read_alarm = max8907_rtc_read_alarm,
|
|
.set_alarm = max8907_rtc_set_alarm,
|
|
};
|
|
|
|
static int max8907_rtc_probe(struct platform_device *pdev)
|
|
{
|
|
struct max8907 *max8907 = dev_get_drvdata(pdev->dev.parent);
|
|
struct max8907_rtc *rtc;
|
|
int ret;
|
|
|
|
rtc = devm_kzalloc(&pdev->dev, sizeof(*rtc), GFP_KERNEL);
|
|
if (!rtc)
|
|
return -ENOMEM;
|
|
platform_set_drvdata(pdev, rtc);
|
|
|
|
rtc->max8907 = max8907;
|
|
rtc->regmap = max8907->regmap_rtc;
|
|
|
|
rtc->rtc_dev = devm_rtc_device_register(&pdev->dev, "max8907-rtc",
|
|
&max8907_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;
|
|
}
|
|
|
|
rtc->irq = regmap_irq_get_virq(max8907->irqc_rtc,
|
|
MAX8907_IRQ_RTC_ALARM0);
|
|
if (rtc->irq < 0)
|
|
return rtc->irq;
|
|
|
|
ret = devm_request_threaded_irq(&pdev->dev, rtc->irq, NULL,
|
|
max8907_irq_handler,
|
|
IRQF_ONESHOT, "max8907-alarm0", rtc);
|
|
if (ret < 0)
|
|
dev_err(&pdev->dev, "Failed to request IRQ%d: %d\n",
|
|
rtc->irq, ret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static struct platform_driver max8907_rtc_driver = {
|
|
.driver = {
|
|
.name = "max8907-rtc",
|
|
.owner = THIS_MODULE,
|
|
},
|
|
.probe = max8907_rtc_probe,
|
|
};
|
|
module_platform_driver(max8907_rtc_driver);
|
|
|
|
MODULE_DESCRIPTION("Maxim MAX8907 RTC driver");
|
|
MODULE_LICENSE("GPL v2");
|