Last active
January 31, 2018 13:43
-
-
Save tai271828/85f5c8130f18f9b25beca216ebbaa702 to your computer and use it in GitHub Desktop.
Simplified MC146818 RTC Linux driver
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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. | |
*/ | |
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt | |
#include <linux/kernel.h> | |
#include <linux/module.h> | |
#include <linux/init.h> | |
#include <linux/interrupt.h> | |
#include <linux/spinlock.h> | |
#include <linux/log2.h> | |
/* this is for "generic access to PC-style RTC" using CMOS_READ/CMOS_WRITE */ | |
#include <linux/mc146818rtc.h> | |
struct cmos_rtc { | |
struct rtc_device *rtc; | |
struct device *dev; | |
int irq; | |
struct resource *iomem; | |
time64_t alarm_expires; | |
void (*wake_on)(struct device *); | |
void (*wake_off)(struct device *); | |
u8 enabled_wake; | |
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_thho_show_in_dmesg"; | |
/* The RTC_INTR register may have e.g. RTC_PF set even if RTC_PIE is clear; | |
* always mask it against the irq enable bits in RTC_CONTROL. Bit values | |
* are the same: PF==PIE, AF=AIE, UF=UIE; so RTC_IRQMASK works with both. | |
*/ | |
#define RTC_IRQMASK (RTC_PF | RTC_AF | RTC_UF) | |
static inline int is_intr(u8 rtc_intr) | |
{ | |
if (!(rtc_intr & RTC_IRQF)) | |
return 0; | |
return rtc_intr & RTC_IRQMASK; | |
} | |
/*----------------------------------------------------------------*/ | |
/* Much modern x86 hardware has HPETs (10+ MHz timers) which, because | |
* many BIOS programmers don't set up "sane mode" IRQ routing, are mostly | |
* used in a broken "legacy replacement" mode. The breakage includes | |
* HPET #1 hijacking the IRQ for this RTC, and being unavailable for | |
* other (better) use. | |
* | |
* When that broken mode is in use, platform glue provides a partial | |
* emulation of hardware RTC IRQ facilities using HPET #1. We don't | |
* want to use HPET for anything except those IRQs though... | |
*/ | |
#include <asm/hpet.h> | |
/*----------------------------------------------------------------*/ | |
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 mc146818_get_time(). | |
* That'll make Y3K compatility (year > 2070) easy! | |
*/ | |
printk("My Debugger: cmos_read_time is called.\n"); | |
mc146818_get_time(t); | |
return 0; | |
} | |
static void cmos_checkintr(struct cmos_rtc *cmos, unsigned char rtc_control) | |
{ | |
unsigned char rtc_intr; | |
printk("My Debugger: cmos_checkintr is called.\n"); | |
/* NOTE after changing RTC_xIE bits we always read INTR_FLAGS; | |
* allegedly some older rtcs need that to handle irqs properly | |
*/ | |
rtc_intr = CMOS_READ(RTC_INTR_FLAGS); | |
if (is_hpet_enabled()) | |
return; | |
rtc_intr &= (rtc_control & RTC_IRQMASK) | RTC_IRQF; | |
if (is_intr(rtc_intr)) | |
rtc_update_irq(cmos->rtc, 1, rtc_intr); | |
} | |
static void cmos_irq_enable(struct cmos_rtc *cmos, unsigned char mask) | |
{ | |
unsigned char rtc_control; | |
/* flush any pending IRQ status, notably for update irqs, | |
* before we enable new IRQs | |
*/ | |
printk("My Debugger: cmos_irq_enable is called.\n"); | |
rtc_control = CMOS_READ(RTC_CONTROL); | |
cmos_checkintr(cmos, rtc_control); | |
rtc_control |= mask; | |
CMOS_WRITE(rtc_control, RTC_CONTROL); | |
hpet_set_rtc_irq_bit(mask); | |
cmos_checkintr(cmos, rtc_control); | |
} | |
static void cmos_irq_disable(struct cmos_rtc *cmos, unsigned char mask) | |
{ | |
unsigned char rtc_control; | |
printk("My Debugger: cmos_irq_disable is called.\n"); | |
rtc_control = CMOS_READ(RTC_CONTROL); | |
rtc_control &= ~mask; | |
CMOS_WRITE(rtc_control, RTC_CONTROL); | |
hpet_mask_rtc_irq_bit(mask); | |
cmos_checkintr(cmos, rtc_control); | |
} | |
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, rtc_control; | |
printk("My Debugger: cmos_set_alarm is called.\n"); | |
if (!is_valid_irq(cmos->irq)) | |
return -EIO; | |
mon = t->time.tm_mon + 1; | |
mday = t->time.tm_mday; | |
hrs = t->time.tm_hour; | |
min = t->time.tm_min; | |
sec = t->time.tm_sec; | |
rtc_control = CMOS_READ(RTC_CONTROL); | |
if (!(rtc_control & RTC_DM_BINARY) || RTC_ALWAYS_BCD) { | |
/* Writing 0xff means "don't care" or "match all". */ | |
mon = (mon <= 12) ? bin2bcd(mon) : 0xff; | |
mday = (mday >= 1 && mday <= 31) ? bin2bcd(mday) : 0xff; | |
hrs = (hrs < 24) ? bin2bcd(hrs) : 0xff; | |
min = (min < 60) ? bin2bcd(min) : 0xff; | |
sec = (sec < 60) ? bin2bcd(sec) : 0xff; | |
} | |
spin_lock_irq(&rtc_lock); | |
/* next rtc irq must not be from previous alarm setting */ | |
cmos_irq_disable(cmos, RTC_AIE); | |
/* 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); | |
} | |
/* FIXME the HPET alarm glue currently ignores day_alrm | |
* and mon_alrm ... | |
*/ | |
hpet_set_alarm_time(t->time.tm_hour, t->time.tm_min, t->time.tm_sec); | |
if (t->enabled) | |
cmos_irq_enable(cmos, RTC_AIE); | |
spin_unlock_irq(&rtc_lock); | |
cmos->alarm_expires = rtc_tm_to_time64(&t->time); | |
printk("My Debugger: cmos_set_alarm is called. done.\n"); | |
return 0; | |
} | |
static const struct rtc_class_ops cmos_rtc_ops = { | |
.read_time = cmos_read_time, | |
.set_alarm = cmos_set_alarm, | |
}; | |
/*----------------------------------------------------------------*/ | |
static struct cmos_rtc cmos_rtc; | |
static irqreturn_t cmos_interrupt(int irq, void *p) | |
{ | |
u8 irqstat; | |
u8 rtc_control; | |
printk("My Debugger: cmos_interrupt is called.\n"); | |
spin_lock(&rtc_lock); | |
/* When the HPET interrupt handler calls us, the interrupt | |
* status is passed as arg1 instead of the irq number. But | |
* always clear irq status, even when HPET is in the way. | |
* | |
* Note that HPET and RTC are almost certainly out of phase, | |
* giving different IRQ status ... | |
*/ | |
irqstat = CMOS_READ(RTC_INTR_FLAGS); | |
rtc_control = CMOS_READ(RTC_CONTROL); | |
if (is_hpet_enabled()) | |
irqstat = (unsigned long)irq & 0xF0; | |
/* If we were suspended, RTC_CONTROL may not be accurate since the | |
* bios may have cleared it. | |
*/ | |
if (!cmos_rtc.suspend_ctrl) | |
irqstat &= (rtc_control & RTC_IRQMASK) | RTC_IRQF; | |
else | |
irqstat &= (cmos_rtc.suspend_ctrl & RTC_IRQMASK) | RTC_IRQF; | |
/* All Linux RTC alarms should be treated as if they were oneshot. | |
* Similar code may be needed in system wakeup paths, in case the | |
* alarm woke the system. | |
*/ | |
if (irqstat & RTC_AIE) { | |
cmos_rtc.suspend_ctrl &= ~RTC_AIE; | |
rtc_control &= ~RTC_AIE; | |
CMOS_WRITE(rtc_control, RTC_CONTROL); | |
hpet_mask_rtc_irq_bit(RTC_AIE); | |
CMOS_READ(RTC_INTR_FLAGS); | |
} | |
spin_unlock(&rtc_lock); | |
if (is_intr(irqstat)) { | |
rtc_update_irq(p, 1, irqstat); | |
return IRQ_HANDLED; | |
} else | |
return IRQ_NONE; | |
} | |
#ifdef CONFIG_PNP | |
#define INITSECTION | |
#else | |
#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_get_platdata(dev); | |
int retval = 0; | |
unsigned char rtc_control; | |
unsigned address_space; | |
u32 flags = 0; | |
printk("My Debugger: cmos_do_probe\n"); | |
/* there can be only one ... */ | |
if (cmos_rtc.dev) | |
return -EBUSY; | |
if (!ports) | |
return -ENODEV; | |
/* Claim I/O ports ASAP, minimizing conflict with legacy driver. | |
* | |
* REVISIT non-x86 systems may instead use memory space resources | |
* (needing ioremap etc), not i/o space resources like this ... | |
*/ | |
if (RTC_IOMAPPED) | |
ports = request_region(ports->start, resource_size(ports), | |
driver_name); | |
else | |
ports = request_mem_region(ports->start, resource_size(ports), | |
driver_name); | |
if (!ports) { | |
dev_dbg(dev, "i/o registers already in use\n"); | |
return -EBUSY; | |
} | |
cmos_rtc.irq = rtc_irq; | |
cmos_rtc.iomem = ports; | |
/* Heuristic to deduce NVRAM size ... do what the legacy NVRAM | |
* driver did, but don't reject unknown configs. Old hardware | |
* won't address 128 bytes. Newer chips have multiple banks, | |
* though they may not be listed in one I/O resource. | |
*/ | |
address_space = 128; | |
/* For ACPI systems extension info comes from the FADT. On others, | |
* board specific setup provides it as appropriate. Systems where | |
* the alarm IRQ isn't automatically a wakeup IRQ (like ACPI, and | |
* some almost-clones) can provide hooks to make that behave. | |
* | |
* Note that ACPI doesn't preclude putting these registers into | |
* "extended" areas of the chip, including some that we won't yet | |
* expect CMOS_READ and friends to handle. | |
*/ | |
if (info) { | |
if (info->flags) | |
flags = info->flags; | |
if (info->address_space) | |
address_space = info->address_space; | |
if (info->rtc_day_alarm && info->rtc_day_alarm < 128) | |
cmos_rtc.day_alrm = info->rtc_day_alarm; | |
if (info->rtc_mon_alarm && info->rtc_mon_alarm < 128) | |
cmos_rtc.mon_alrm = info->rtc_mon_alarm; | |
if (info->rtc_century && info->rtc_century < 128) | |
cmos_rtc.century = info->rtc_century; | |
if (info->wake_on && info->wake_off) { | |
cmos_rtc.wake_on = info->wake_on; | |
cmos_rtc.wake_off = info->wake_off; | |
} | |
} | |
cmos_rtc.dev = dev; | |
dev_set_drvdata(dev, &cmos_rtc); | |
cmos_rtc.rtc = rtc_device_register(driver_name, dev, | |
&cmos_rtc_ops, THIS_MODULE); | |
if (IS_ERR(cmos_rtc.rtc)) { | |
retval = PTR_ERR(cmos_rtc.rtc); | |
goto cleanup0; | |
} | |
rename_region(ports, dev_name(&cmos_rtc.rtc->dev)); | |
spin_lock_irq(&rtc_lock); | |
if (!(flags & CMOS_RTC_FLAGS_NOFREQ)) { | |
/* 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_rtc.rtc->irq_freq = 1024; | |
hpet_set_periodic_freq(cmos_rtc.rtc->irq_freq); | |
CMOS_WRITE(RTC_REF_CLCK_32KHZ | 0x06, RTC_FREQ_SELECT); | |
} | |
/* disable irqs */ | |
if (is_valid_irq(rtc_irq)) | |
cmos_irq_disable(&cmos_rtc, RTC_PIE | RTC_AIE | RTC_UIE); | |
rtc_control = CMOS_READ(RTC_CONTROL); | |
spin_unlock_irq(&rtc_lock); | |
/* FIXME: | |
* <asm-generic/rtc.h> doesn't know 12-hour mode either. | |
*/ | |
if (is_valid_irq(rtc_irq) && !(rtc_control & RTC_24H)) { | |
dev_warn(dev, "only 24-hr supported\n"); | |
retval = -ENXIO; | |
goto cleanup1; | |
} | |
if (is_valid_irq(rtc_irq)) { | |
irq_handler_t rtc_int_handler; | |
if (is_hpet_enabled()) { | |
rtc_int_handler = hpet_rtc_interrupt; | |
retval = hpet_register_irq_handler(cmos_interrupt); | |
if (retval) { | |
dev_warn(dev, "hpet_register_irq_handler " | |
" failed in rtc_init()."); | |
goto cleanup1; | |
} | |
} else | |
rtc_int_handler = cmos_interrupt; | |
retval = request_irq(rtc_irq, rtc_int_handler, | |
IRQF_SHARED, dev_name(&cmos_rtc.rtc->dev), | |
cmos_rtc.rtc); | |
if (retval < 0) { | |
dev_dbg(dev, "IRQ %d is already in use\n", rtc_irq); | |
goto cleanup1; | |
} | |
} | |
hpet_rtc_timer_init(); | |
printk("My Debugger: cmos_do_probe done!\n"); | |
return 0; | |
cleanup1: | |
cmos_rtc.dev = NULL; | |
rtc_device_unregister(cmos_rtc.rtc); | |
cleanup0: | |
if (RTC_IOMAPPED) | |
release_region(ports->start, resource_size(ports)); | |
else | |
release_mem_region(ports->start, resource_size(ports)); | |
return retval; | |
} | |
/*----------------------------------------------------------------*/ | |
/* On non-x86 systems, a "CMOS" RTC lives most naturally on platform_bus. | |
* ACPI systems always list these as PNPACPI devices, and pre-ACPI PCs | |
* probably list them in similar PNPBIOS tables; so PNP is more common. | |
* | |
* We don't use legacy "poke at the hardware" probing. Ancient PCs that | |
* predate even PNPBIOS should set up platform_bus devices. | |
*/ | |
#ifdef CONFIG_PNP | |
#include <linux/pnp.h> | |
static int cmos_pnp_probe(struct pnp_dev *pnp, const struct pnp_device_id *id) | |
{ | |
// it seems there should be something as platform_data | |
//(&pnp->dev)->platform_data = &acpi_rtc_info; | |
(&pnp->dev)->platform_data = NULL; | |
printk("My Debugger: cmos_pnp_probe\n"); | |
if (pnp_port_start(pnp, 0) == 0x70 && !pnp_irq_valid(pnp, 0)) | |
/* Some machines contain a PNP entry for the RTC, but | |
* don't define the IRQ. It should always be safe to | |
* hardcode it in these cases | |
*/ | |
return cmos_do_probe(&pnp->dev, | |
pnp_get_resource(pnp, IORESOURCE_IO, 0), 8); | |
else | |
return cmos_do_probe(&pnp->dev, | |
pnp_get_resource(pnp, IORESOURCE_IO, 0), | |
pnp_irq(pnp, 0)); | |
printk("My Debugger: cmos_pnp_probe. done!\n"); | |
} | |
static void cmos_pnp_remove(struct pnp_dev *pnp) | |
{ | |
struct device *dev = &pnp->dev; | |
struct cmos_rtc *cmos = dev_get_drvdata(dev); | |
struct resource *ports; | |
cmos_do_shutdown(cmos->irq); | |
if (is_valid_irq(cmos->irq)) { | |
free_irq(cmos->irq, cmos->rtc); | |
hpet_unregister_irq_handler(cmos_interrupt); | |
} | |
rtc_device_unregister(cmos->rtc); | |
cmos->rtc = NULL; | |
ports = cmos->iomem; | |
if (RTC_IOMAPPED) | |
release_region(ports->start, resource_size(ports)); | |
else | |
release_mem_region(ports->start, resource_size(ports)); | |
cmos->iomem = NULL; | |
cmos->dev = NULL; | |
} | |
static void cmos_pnp_shutdown(struct pnp_dev *pnp) | |
{ | |
struct device *dev = &pnp->dev; | |
struct cmos_rtc *cmos = dev_get_drvdata(dev); | |
int rtc_irq = cmos->irq; | |
spin_lock_irq(&rtc_lock); | |
if (is_valid_irq(rtc_irq)) | |
cmos_irq_disable(&cmos_rtc, RTC_IRQMASK); | |
spin_unlock_irq(&rtc_lock); | |
} | |
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 = cmos_pnp_remove, | |
.shutdown = cmos_pnp_shutdown, | |
}; | |
#endif /* CONFIG_PNP */ | |
static bool pnp_driver_registered; | |
static int __init cmos_init(void) | |
{ | |
int retval = 0; | |
printk("My Debugger: cmos_init.\n"); | |
printk("My Debugger: probe rtc...\n"); | |
printk("My Debugger: probe pnp rtc...\n"); | |
retval = pnp_register_driver(&cmos_pnp_driver); | |
if (retval == 0) | |
pnp_driver_registered = true; | |
printk("My Debugger: probe pnp rtc ... retval %d\n", retval); | |
if (retval == 0) | |
return 0; | |
if (pnp_driver_registered) | |
pnp_unregister_driver(&cmos_pnp_driver); | |
printk("My Debugger: cmos_init done.\n"); | |
return retval; | |
} | |
module_init(cmos_init); | |
static void __exit cmos_exit(void) | |
{ | |
printk("My Debugger: cmos_exit.\n"); | |
if (pnp_driver_registered) | |
pnp_unregister_driver(&cmos_pnp_driver); | |
printk("My Debugger: cmos_exit done.\n"); | |
} | |
module_exit(cmos_exit); | |
MODULE_AUTHOR("David Brownell"); | |
MODULE_DESCRIPTION("Driver for PC-style 'CMOS' RTCs"); | |
MODULE_LICENSE("GPL"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment