kernel-fxtec-pro1x/drivers/rtc/rtc-cmos.c
David Brownell 7be2c7c96a [PATCH] RTC framework driver for CMOS RTCs
This is an "RTC framework" driver for the "CMOS" RTCs which are standard on
PCs and some other platforms.  That's MC146818 compatible silicon.
Advantages of this vs.  drivers/char/rtc.c (use one _or_ the other, only
one will be able to claim the RTC irq) include:

 - This leverages both the new RTC framework and the driver model; both
   PNPACPI and platform device modes are supported.  (A separate patch
   creates a platform device on PCs where PNPACPI isn't configured.)

 - It supports common extensions like longer alarms.  (A separate patch
   exports that information from ACPI through platform_data.)

 - Likewise, system wakeup events use "real driver model support", with
   policy control via sysfs "wakeup" attributes and and using normal rtc
   ioctls to manage wakeup.  (Patch in the works.  The ACPI hooks are
   known; /proc/acpi/alarm can vanish.  Making it work with EFI will
   be a minor challenge to someone with e.g. a MiniMac.)

It's not yet been tested on non-x86 systems, without ACPI, or with HPET.
And the RTC framework will surely have teething pains on "mainstream"
PC-based systems (though must embedded Linux systems use it heavily), not
limited to sorting out the "/dev/rtc0" issue (udev easily tweaked).  Also,
the ALSA rtctimer code doesn't use the new RTC API.

Otherwise, this should be a no-known-regressions replacement for the old
drivers/char/rtc.c driver, and should help the non-embedded distros (and
the new timekeeping code) start to switch to the framework.

Note also that any systems using "rtc-m48t86" are candidates to switch over
to this more functional driver; the platform data is different, and the way
bytes are read is different, but otherwise those chips should be compatible.

[akpm@osdl.org: sparc32 fix]
[akpm@osdl.org: sparc64 fix]
Signed-off-by: David Brownell <dbrownell@users.sourceforge.net>
Cc: Woody Suwalski <woodys@xandros.com>
Cc: Alessandro Zummo <alessandro.zummo@towertech.it>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2007-02-11 10:51:32 -08:00

725 lines
18 KiB
C

/*
* RTC class driver for "CMOS RTC": PCs, ACPI, etc
*
* Copyright (C) 1996 Paul Gortmaker (drivers/char/rtc.c)
* Copyright (C) 2006 David Brownell (convert to new framework)
*
* 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.
*/
/*
* The original "cmos clock" chip was an MC146818 chip, now obsolete.
* That defined the register interface now provided by all PCs, some
* non-PC systems, and incorporated into ACPI. Modern PC chipsets
* integrate an MC146818 clone in their southbridge, and boards use
* that instead of discrete clones like the DS12887 or M48T86. There
* are also clones that connect using the LPC bus.
*
* That register API is also used directly by various other drivers
* (notably for integrated NVRAM), infrastructure (x86 has code to
* bypass the RTC framework, directly reading the RTC during boot
* and updating minutes/seconds for systems using NTP synch) and
* utilities (like userspace 'hwclock', if no /dev node exists).
*
* So **ALL** calls to CMOS_READ and CMOS_WRITE must be done with
* interrupts disabled, holding the global rtc_lock, to exclude those
* other drivers and utilities on correctly configured systems.
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/spinlock.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
/* this is for "generic access to PC-style RTC" using CMOS_READ/CMOS_WRITE */
#include <asm-generic/rtc.h>
struct cmos_rtc {
struct rtc_device *rtc;
struct device *dev;
int irq;
struct resource *iomem;
u8 suspend_ctrl;
/* newer hardware extends the original register set */
u8 day_alrm;
u8 mon_alrm;
u8 century;
};
/* both platform and pnp busses use negative numbers for invalid irqs */
#define is_valid_irq(n) ((n) >= 0)
static const char driver_name[] = "rtc_cmos";
/*----------------------------------------------------------------*/
static int cmos_read_time(struct device *dev, struct rtc_time *t)
{
/* REVISIT: if the clock has a "century" register, use
* that instead of the heuristic in get_rtc_time().
* That'll make Y3K compatility (year > 2070) easy!
*/
get_rtc_time(t);
return 0;
}
static int cmos_set_time(struct device *dev, struct rtc_time *t)
{
/* REVISIT: set the "century" register if available
*
* NOTE: this ignores the issue whereby updating the seconds
* takes effect exactly 500ms after we write the register.
* (Also queueing and other delays before we get this far.)
*/
return set_rtc_time(t);
}
static int cmos_read_alarm(struct device *dev, struct rtc_wkalrm *t)
{
struct cmos_rtc *cmos = dev_get_drvdata(dev);
unsigned char rtc_control;
if (!is_valid_irq(cmos->irq))
return -EIO;
/* Basic alarms only support hour, minute, and seconds fields.
* Some also support day and month, for alarms up to a year in
* the future.
*/
t->time.tm_mday = -1;
t->time.tm_mon = -1;
spin_lock_irq(&rtc_lock);
t->time.tm_sec = CMOS_READ(RTC_SECONDS_ALARM);
t->time.tm_min = CMOS_READ(RTC_MINUTES_ALARM);
t->time.tm_hour = CMOS_READ(RTC_HOURS_ALARM);
if (cmos->day_alrm) {
t->time.tm_mday = CMOS_READ(cmos->day_alrm);
if (!t->time.tm_mday)
t->time.tm_mday = -1;
if (cmos->mon_alrm) {
t->time.tm_mon = CMOS_READ(cmos->mon_alrm);
if (!t->time.tm_mon)
t->time.tm_mon = -1;
}
}
rtc_control = CMOS_READ(RTC_CONTROL);
spin_unlock_irq(&rtc_lock);
/* REVISIT this assumes PC style usage: always BCD */
if (((unsigned)t->time.tm_sec) < 0x60)
t->time.tm_sec = BCD2BIN(t->time.tm_sec);
else
t->time.tm_sec = -1;
if (((unsigned)t->time.tm_min) < 0x60)
t->time.tm_min = BCD2BIN(t->time.tm_min);
else
t->time.tm_min = -1;
if (((unsigned)t->time.tm_hour) < 0x24)
t->time.tm_hour = BCD2BIN(t->time.tm_hour);
else
t->time.tm_hour = -1;
if (cmos->day_alrm) {
if (((unsigned)t->time.tm_mday) <= 0x31)
t->time.tm_mday = BCD2BIN(t->time.tm_mday);
else
t->time.tm_mday = -1;
if (cmos->mon_alrm) {
if (((unsigned)t->time.tm_mon) <= 0x12)
t->time.tm_mon = BCD2BIN(t->time.tm_mon) - 1;
else
t->time.tm_mon = -1;
}
}
t->time.tm_year = -1;
t->enabled = !!(rtc_control & RTC_AIE);
t->pending = 0;
return 0;
}
static int cmos_set_alarm(struct device *dev, struct rtc_wkalrm *t)
{
struct cmos_rtc *cmos = dev_get_drvdata(dev);
unsigned char mon, mday, hrs, min, sec;
unsigned char rtc_control, rtc_intr;
if (!is_valid_irq(cmos->irq))
return -EIO;
/* REVISIT this assumes PC style usage: always BCD */
/* Writing 0xff means "don't care" or "match all". */
mon = t->time.tm_mon;
mon = (mon < 12) ? BIN2BCD(mon) : 0xff;
mon++;
mday = t->time.tm_mday;
mday = (mday >= 1 && mday <= 31) ? BIN2BCD(mday) : 0xff;
hrs = t->time.tm_hour;
hrs = (hrs < 24) ? BIN2BCD(hrs) : 0xff;
min = t->time.tm_min;
min = (min < 60) ? BIN2BCD(min) : 0xff;
sec = t->time.tm_sec;
sec = (sec < 60) ? BIN2BCD(sec) : 0xff;
spin_lock_irq(&rtc_lock);
/* next rtc irq must not be from previous alarm setting */
rtc_control = CMOS_READ(RTC_CONTROL);
rtc_control &= ~RTC_AIE;
CMOS_WRITE(rtc_control, RTC_CONTROL);
rtc_intr = CMOS_READ(RTC_INTR_FLAGS);
if (rtc_intr)
rtc_update_irq(&cmos->rtc->class_dev, 1, rtc_intr);
/* update alarm */
CMOS_WRITE(hrs, RTC_HOURS_ALARM);
CMOS_WRITE(min, RTC_MINUTES_ALARM);
CMOS_WRITE(sec, RTC_SECONDS_ALARM);
/* the system may support an "enhanced" alarm */
if (cmos->day_alrm) {
CMOS_WRITE(mday, cmos->day_alrm);
if (cmos->mon_alrm)
CMOS_WRITE(mon, cmos->mon_alrm);
}
if (t->enabled) {
rtc_control |= RTC_AIE;
CMOS_WRITE(rtc_control, RTC_CONTROL);
rtc_intr = CMOS_READ(RTC_INTR_FLAGS);
if (rtc_intr)
rtc_update_irq(&cmos->rtc->class_dev, 1, rtc_intr);
}
spin_unlock_irq(&rtc_lock);
return 0;
}
static int cmos_set_freq(struct device *dev, int freq)
{
struct cmos_rtc *cmos = dev_get_drvdata(dev);
int f;
unsigned long flags;
if (!is_valid_irq(cmos->irq))
return -ENXIO;
/* 0 = no irqs; 1 = 2^15 Hz ... 15 = 2^0 Hz */
f = ffs(freq);
if (f != 0) {
if (f-- > 16 || freq != (1 << f))
return -EINVAL;
f = 16 - f;
}
spin_lock_irqsave(&rtc_lock, flags);
CMOS_WRITE(RTC_REF_CLCK_32KHZ | f, RTC_FREQ_SELECT);
spin_unlock_irqrestore(&rtc_lock, flags);
return 0;
}
#if defined(CONFIG_RTC_INTF_DEV) || defined(CONFIG_RTC_INTF_DEV_MODULE)
static int
cmos_rtc_ioctl(struct device *dev, unsigned int cmd, unsigned long arg)
{
struct cmos_rtc *cmos = dev_get_drvdata(dev);
unsigned char rtc_control, rtc_intr;
unsigned long flags;
switch (cmd) {
case RTC_AIE_OFF:
case RTC_AIE_ON:
case RTC_UIE_OFF:
case RTC_UIE_ON:
case RTC_PIE_OFF:
case RTC_PIE_ON:
if (!is_valid_irq(cmos->irq))
return -EINVAL;
break;
default:
return -ENOIOCTLCMD;
}
spin_lock_irqsave(&rtc_lock, flags);
rtc_control = CMOS_READ(RTC_CONTROL);
switch (cmd) {
case RTC_AIE_OFF: /* alarm off */
rtc_control &= ~RTC_AIE;
break;
case RTC_AIE_ON: /* alarm on */
rtc_control |= RTC_AIE;
break;
case RTC_UIE_OFF: /* update off */
rtc_control &= ~RTC_UIE;
break;
case RTC_UIE_ON: /* update on */
rtc_control |= RTC_UIE;
break;
case RTC_PIE_OFF: /* periodic off */
rtc_control &= ~RTC_PIE;
break;
case RTC_PIE_ON: /* periodic on */
rtc_control |= RTC_PIE;
break;
}
CMOS_WRITE(rtc_control, RTC_CONTROL);
rtc_intr = CMOS_READ(RTC_INTR_FLAGS);
if (rtc_intr)
rtc_update_irq(&cmos->rtc->class_dev, 1, rtc_intr);
spin_unlock_irqrestore(&rtc_lock, flags);
return 0;
}
#else
#define cmos_rtc_ioctl NULL
#endif
#if defined(CONFIG_RTC_INTF_PROC) || defined(CONFIG_RTC_INTF_PROC_MODULE)
static int cmos_procfs(struct device *dev, struct seq_file *seq)
{
struct cmos_rtc *cmos = dev_get_drvdata(dev);
unsigned char rtc_control, valid;
spin_lock_irq(&rtc_lock);
rtc_control = CMOS_READ(RTC_CONTROL);
valid = CMOS_READ(RTC_VALID);
spin_unlock_irq(&rtc_lock);
/* NOTE: at least ICH6 reports battery status using a different
* (non-RTC) bit; and SQWE is ignored on many current systems.
*/
return seq_printf(seq,
"periodic_IRQ\t: %s\n"
"update_IRQ\t: %s\n"
// "square_wave\t: %s\n"
// "BCD\t\t: %s\n"
"DST_enable\t: %s\n"
"periodic_freq\t: %d\n"
"batt_status\t: %s\n",
(rtc_control & RTC_PIE) ? "yes" : "no",
(rtc_control & RTC_UIE) ? "yes" : "no",
// (rtc_control & RTC_SQWE) ? "yes" : "no",
// (rtc_control & RTC_DM_BINARY) ? "no" : "yes",
(rtc_control & RTC_DST_EN) ? "yes" : "no",
cmos->rtc->irq_freq,
(valid & RTC_VRT) ? "okay" : "dead");
}
#else
#define cmos_procfs NULL
#endif
static const struct rtc_class_ops cmos_rtc_ops = {
.ioctl = cmos_rtc_ioctl,
.read_time = cmos_read_time,
.set_time = cmos_set_time,
.read_alarm = cmos_read_alarm,
.set_alarm = cmos_set_alarm,
.proc = cmos_procfs,
.irq_set_freq = cmos_set_freq,
};
/*----------------------------------------------------------------*/
static struct cmos_rtc cmos_rtc;
static irqreturn_t cmos_interrupt(int irq, void *p)
{
u8 irqstat;
spin_lock(&rtc_lock);
irqstat = CMOS_READ(RTC_INTR_FLAGS);
spin_unlock(&rtc_lock);
if (irqstat) {
/* NOTE: irqstat may have e.g. RTC_PF set
* even when RTC_PIE is clear...
*/
rtc_update_irq(p, 1, irqstat);
return IRQ_HANDLED;
} else
return IRQ_NONE;
}
#ifdef CONFIG_PNPACPI
#define is_pnpacpi() 1
#define INITSECTION
#else
#define is_pnpacpi() 0
#define INITSECTION __init
#endif
static int INITSECTION
cmos_do_probe(struct device *dev, struct resource *ports, int rtc_irq)
{
struct cmos_rtc_board_info *info = dev->platform_data;
int retval = 0;
unsigned char rtc_control;
/* there can be only one ... */
if (cmos_rtc.dev)
return -EBUSY;
if (!ports)
return -ENODEV;
cmos_rtc.irq = rtc_irq;
cmos_rtc.iomem = ports;
/* For ACPI systems the info comes from the FADT. On others,
* board specific setup provides it as appropriate.
*/
if (info) {
cmos_rtc.day_alrm = info->rtc_day_alarm;
cmos_rtc.mon_alrm = info->rtc_mon_alarm;
cmos_rtc.century = info->rtc_century;
}
cmos_rtc.rtc = rtc_device_register(driver_name, dev,
&cmos_rtc_ops, THIS_MODULE);
if (IS_ERR(cmos_rtc.rtc))
return PTR_ERR(cmos_rtc.rtc);
cmos_rtc.dev = dev;
dev_set_drvdata(dev, &cmos_rtc);
/* platform and pnp busses handle resources incompatibly.
*
* REVISIT for non-x86 systems we may need to handle io memory
* resources: ioremap them, and request_mem_region().
*/
if (is_pnpacpi()) {
retval = request_resource(&ioport_resource, ports);
if (retval < 0) {
dev_dbg(dev, "i/o registers already in use\n");
goto cleanup0;
}
}
rename_region(ports, cmos_rtc.rtc->class_dev.class_id);
spin_lock_irq(&rtc_lock);
/* force periodic irq to CMOS reset default of 1024Hz;
*
* REVISIT it's been reported that at least one x86_64 ALI mobo
* doesn't use 32KHz here ... for portability we might need to
* do something about other clock frequencies.
*/
CMOS_WRITE(RTC_REF_CLCK_32KHZ | 0x06, RTC_FREQ_SELECT);
cmos_rtc.rtc->irq_freq = 1024;
/* disable irqs.
*
* NOTE after changing RTC_xIE bits we always read INTR_FLAGS;
* allegedly some older rtcs need that to handle irqs properly
*/
rtc_control = CMOS_READ(RTC_CONTROL);
rtc_control &= ~(RTC_PIE | RTC_AIE | RTC_UIE);
CMOS_WRITE(rtc_control, RTC_CONTROL);
CMOS_READ(RTC_INTR_FLAGS);
spin_unlock_irq(&rtc_lock);
/* FIXME teach the alarm code how to handle binary mode;
* <asm-generic/rtc.h> doesn't know 12-hour mode either.
*/
if (!(rtc_control & RTC_24H) || (rtc_control & (RTC_DM_BINARY))) {
dev_dbg(dev, "only 24-hr BCD mode supported\n");
retval = -ENXIO;
goto cleanup1;
}
if (is_valid_irq(rtc_irq))
retval = request_irq(rtc_irq, cmos_interrupt, IRQF_DISABLED,
cmos_rtc.rtc->class_dev.class_id,
&cmos_rtc.rtc->class_dev);
if (retval < 0) {
dev_dbg(dev, "IRQ %d is already in use\n", rtc_irq);
goto cleanup1;
}
/* REVISIT optionally make 50 or 114 bytes NVRAM available,
* like rtc-ds1553, rtc-ds1742 ... this will often include
* registers for century, and day/month alarm.
*/
pr_info("%s: alarms up to one %s%s\n",
cmos_rtc.rtc->class_dev.class_id,
is_valid_irq(rtc_irq)
? (cmos_rtc.mon_alrm
? "year"
: (cmos_rtc.day_alrm
? "month" : "day"))
: "no",
cmos_rtc.century ? ", y3k" : ""
);
return 0;
cleanup1:
rename_region(ports, NULL);
cleanup0:
rtc_device_unregister(cmos_rtc.rtc);
return retval;
}
static void cmos_do_shutdown(void)
{
unsigned char rtc_control;
spin_lock_irq(&rtc_lock);
rtc_control = CMOS_READ(RTC_CONTROL);
rtc_control &= ~(RTC_PIE|RTC_AIE|RTC_UIE);
CMOS_WRITE(rtc_control, RTC_CONTROL);
CMOS_READ(RTC_INTR_FLAGS);
spin_unlock_irq(&rtc_lock);
}
static void __exit cmos_do_remove(struct device *dev)
{
struct cmos_rtc *cmos = dev_get_drvdata(dev);
cmos_do_shutdown();
if (is_pnpacpi())
release_resource(cmos->iomem);
rename_region(cmos->iomem, NULL);
if (is_valid_irq(cmos->irq))
free_irq(cmos->irq, &cmos_rtc.rtc->class_dev);
rtc_device_unregister(cmos_rtc.rtc);
cmos_rtc.dev = NULL;
dev_set_drvdata(dev, NULL);
}
#ifdef CONFIG_PM
static int cmos_suspend(struct device *dev, pm_message_t mesg)
{
struct cmos_rtc *cmos = dev_get_drvdata(dev);
int do_wake = device_may_wakeup(dev);
unsigned char tmp, irqstat;
/* only the alarm might be a wakeup event source */
spin_lock_irq(&rtc_lock);
cmos->suspend_ctrl = tmp = CMOS_READ(RTC_CONTROL);
if (tmp & (RTC_PIE|RTC_AIE|RTC_UIE)) {
if (do_wake)
tmp &= ~(RTC_PIE|RTC_UIE);
else
tmp &= ~(RTC_PIE|RTC_AIE|RTC_UIE);
CMOS_WRITE(tmp, RTC_CONTROL);
irqstat = CMOS_READ(RTC_INTR_FLAGS);
} else
irqstat = 0;
spin_unlock_irq(&rtc_lock);
if (irqstat)
rtc_update_irq(&cmos->rtc->class_dev, 1, irqstat);
/* ACPI HOOK: enable ACPI_EVENT_RTC when (tmp & RTC_AIE)
* ... it'd be best if we could do that under rtc_lock.
*/
pr_debug("%s: suspend%s, ctrl %02x\n",
cmos_rtc.rtc->class_dev.class_id,
(tmp & RTC_AIE) ? ", alarm may wake" : "",
tmp);
return 0;
}
static int cmos_resume(struct device *dev)
{
struct cmos_rtc *cmos = dev_get_drvdata(dev);
unsigned char tmp = cmos->suspend_ctrl;
/* REVISIT: a mechanism to resync the system clock (jiffies)
* on resume should be portable between platforms ...
*/
/* re-enable any irqs previously active */
if (tmp & (RTC_PIE|RTC_AIE|RTC_UIE)) {
/* ACPI HOOK: disable ACPI_EVENT_RTC when (tmp & RTC_AIE) */
spin_lock_irq(&rtc_lock);
CMOS_WRITE(tmp, RTC_CONTROL);
tmp = CMOS_READ(RTC_INTR_FLAGS);
spin_unlock_irq(&rtc_lock);
if (tmp)
rtc_update_irq(&cmos->rtc->class_dev, 1, tmp);
}
pr_debug("%s: resume, ctrl %02x\n",
cmos_rtc.rtc->class_dev.class_id,
cmos->suspend_ctrl);
return 0;
}
#else
#define cmos_suspend NULL
#define cmos_resume NULL
#endif
/*----------------------------------------------------------------*/
/* The "CMOS" RTC normally lives on the platform_bus. On ACPI systems,
* the device node may alternatively be created as a PNP device.
*/
#ifdef CONFIG_PNPACPI
#include <linux/pnp.h>
static int __devinit
cmos_pnp_probe(struct pnp_dev *pnp, const struct pnp_device_id *id)
{
/* REVISIT paranoia argues for a shutdown notifier, since PNP
* drivers can't provide shutdown() methods to disable IRQs.
* Or better yet, fix PNP to allow those methods...
*/
return cmos_do_probe(&pnp->dev,
&pnp->res.port_resource[0],
pnp->res.irq_resource[0].start);
}
static void __exit cmos_pnp_remove(struct pnp_dev *pnp)
{
cmos_do_remove(&pnp->dev);
}
#ifdef CONFIG_PM
static int cmos_pnp_suspend(struct pnp_dev *pnp, pm_message_t mesg)
{
return cmos_suspend(&pnp->dev, mesg);
}
static int cmos_pnp_resume(struct pnp_dev *pnp)
{
return cmos_resume(&pnp->dev);
}
#else
#define cmos_pnp_suspend NULL
#define cmos_pnp_resume NULL
#endif
static const struct pnp_device_id rtc_ids[] = {
{ .id = "PNP0b00", },
{ .id = "PNP0b01", },
{ .id = "PNP0b02", },
{ },
};
MODULE_DEVICE_TABLE(pnp, rtc_ids);
static struct pnp_driver cmos_pnp_driver = {
.name = (char *) driver_name,
.id_table = rtc_ids,
.probe = cmos_pnp_probe,
.remove = __exit_p(cmos_pnp_remove),
/* flag ensures resume() gets called, and stops syslog spam */
.flags = PNP_DRIVER_RES_DO_NOT_CHANGE,
.suspend = cmos_pnp_suspend,
.resume = cmos_pnp_resume,
};
static int __init cmos_init(void)
{
return pnp_register_driver(&cmos_pnp_driver);
}
module_init(cmos_init);
static void __exit cmos_exit(void)
{
pnp_unregister_driver(&cmos_pnp_driver);
}
module_exit(cmos_exit);
#else /* no PNPACPI */
/*----------------------------------------------------------------*/
/* Platform setup should have set up an RTC device, when PNPACPI is
* unavailable ... this is the normal case, common even on PCs.
*/
static int __init cmos_platform_probe(struct platform_device *pdev)
{
return cmos_do_probe(&pdev->dev,
platform_get_resource(pdev, IORESOURCE_IO, 0),
platform_get_irq(pdev, 0));
}
static int __exit cmos_platform_remove(struct platform_device *pdev)
{
cmos_do_remove(&pdev->dev);
return 0;
}
static void cmos_platform_shutdown(struct platform_device *pdev)
{
cmos_do_shutdown();
}
static struct platform_driver cmos_platform_driver = {
.remove = __exit_p(cmos_platform_remove),
.shutdown = cmos_platform_shutdown,
.driver = {
.name = (char *) driver_name,
.suspend = cmos_suspend,
.resume = cmos_resume,
}
};
static int __init cmos_init(void)
{
return platform_driver_probe(&cmos_platform_driver,
cmos_platform_probe);
}
module_init(cmos_init);
static void __exit cmos_exit(void)
{
platform_driver_unregister(&cmos_platform_driver);
}
module_exit(cmos_exit);
#endif /* !PNPACPI */
MODULE_AUTHOR("David Brownell");
MODULE_DESCRIPTION("Driver for PC-style 'CMOS' RTCs");
MODULE_LICENSE("GPL");