diff options
Diffstat (limited to 'sys-utils/hwclock.c')
-rw-r--r-- | sys-utils/hwclock.c | 876 |
1 files changed, 652 insertions, 224 deletions
diff --git a/sys-utils/hwclock.c b/sys-utils/hwclock.c index 9f4d77149..643ccda5b 100644 --- a/sys-utils/hwclock.c +++ b/sys-utils/hwclock.c @@ -8,7 +8,7 @@ See man page for details. - By Bryan Henderson, 96.09.19 + By Bryan Henderson, 96.09.19. bryanh@giraffe-data.com Based on work by others; see history at end of source code. @@ -26,7 +26,7 @@ The program is designed to run setuid superuser, since we need to be able to do direct I/O. (More to the point: we need permission to - execute the iopl() system call.) (However, if you use one of the + execute the iopl() system call). (However, if you use one of the methods other than direct ISA I/O to access the clock, no setuid is required). @@ -59,9 +59,9 @@ integral number of seconds. N.B. The Hardware Clock can only be set in integral seconds. - If we're setting the clock to the system clock value, we wait for it - to reach the top of a second, and then set the Hardware Clock to the - system clock's value. + If we're setting the clock to the system clock value, we wait for + the system clock to reach the top of a second, and then set the + Hardware Clock to the system clock's value. Here's an interesting point about setting the Hardware Clock: On my machine, when you set it, it sets to that precise time. But one can @@ -71,6 +71,23 @@ complications that might cause, we set the clock as soon as possible after an oscillator tick. + + About synchronizing to the Hardware Clock when reading the time: The + precision of the Hardware Clock counters themselves is one second. + You can't read the counters and find out that is 12:01:02.5. But if + you consider the location in time of the counter's ticks as part of + its value, then its precision is as infinite as time is continuous! + What I'm saying is this: To find out the _exact_ time in the + hardware clock, we wait until the next clock tick (the next time the + second counter changes) and measure how long we had to wait. We + then read the value of the clock counters and subtract the wait time + and we know precisely what time it was when we set out to query the + time. + + hwclock uses this method, and considers the Hardware Clock to have + infinite precision. + + Enhancements needed: - When waiting for whole second boundary in set_hardware_clock_exact, @@ -79,8 +96,6 @@ ****************************************************************************/ -#define _GNU_SOURCE /* for snprintf */ - #include <string.h> #include <stdio.h> #include <fcntl.h> @@ -96,7 +111,7 @@ #include "../version.h" #define MYNAME "hwclock" -#define VERSION "2.1" +#define VERSION "2.2" #define FLOOR(arg) ((arg >= 0 ? (int) arg : ((int) arg) - 1)); @@ -129,7 +144,7 @@ struct adjtime { }; -enum clock_access_method {ISA, RTC_IOCTL, KD}; +enum clock_access_method {ISA, RTC_IOCTL, KD, NOCLOCK}; /* A method for accessing (reading, writing) the hardware clock: ISA: @@ -140,14 +155,14 @@ enum clock_access_method {ISA, RTC_IOCTL, KD}; via the rtc device driver, using device special file /dev/rtc. KD: - via the console driver, using device special file /dev/console. - This is the m64k ioctl interface. + via the console driver, using device special file /dev/tty1. + This is the m68k ioctl interface, known as KDGHWCLK. NO_CLOCK: - Unable to determine a accessmethod for the system clock. + Unable to determine a usable access method for the system clock. */ - +#ifdef __i386__ /* The following are just constants. Oddly, this program will not compile if the inb() and outb() functions use something even slightly different from these variables. This is probably at least @@ -157,12 +172,24 @@ enum clock_access_method {ISA, RTC_IOCTL, KD}; */ static unsigned short clock_ctl_addr = 0x70; static unsigned short clock_data_addr = 0x71; +#endif bool debug; /* We are running in debug mode, wherein we put a lot of information about - what we're doing to standard error. Because of the pervasive and yet + what we're doing to standard output. Because of the pervasive and yet background nature of this value, this is a global variable. */ +bool interrupts_enabled; + /* Interrupts are enabled as normal. We, unfortunately, turn interrupts + on the machine off in some places where we do the direct ISA accesses + to the Hardware Clock. It is in extremely poor form for a user space + program to do this, but that's the price we have to pay to run on an + ISA machine without the rtc driver in the kernel. + + Code which turns interrupts off uses this value to determine if they + need to be turned back on. + */ + #include <linux/version.h> /* Check if the /dev/rtc interface is available in this version of @@ -173,11 +200,35 @@ bool debug; #include <linux/kd.h> static const bool got_rtc = TRUE; #else -/* Dummy to make it compile */ -#define RTC_SET_TIME 0 static const bool got_rtc = FALSE; +/* Dummy definitions to make it compile. If any lines containing these + macros ever execute, there is a bug in the code. + */ +#define RTC_SET_TIME -1 +#define RTC_RD_TIME -1 +#define RTC_UIE_ON -1 +#define RTC_UIE_OFF -1 #endif +/* The RTC_EPOCH_READ and RTC_EPOCH_SET macros are supposed to be + defined by linux/mc146818rtc.h, included above. However, these are + recent inventions and at the time of this writing, not in any + official Linux. Since these values aren't even necessary for most + uses of hwclock, we don't want compilation to depend on the user + having some arcane version of this header file on his system. Thus, + we define the macros ourselves if the header file failed to do so. + 98.03.03. +*/ + +#ifndef RTC_EPOCH_READ +#define RTC_EPOCH_READ _IOR('p', 0x0d, unsigned long) /* Read epoch */ + /* Not all kernels have this ioctl */ +#endif + +#ifndef RTC_EPOCH_SET +#define RTC_EPOCH_SET _IOW('p', 0x0e, unsigned long) /* Set epoch */ + /* Not all kernels have this ioctl */ +#endif #if defined(KDGHWCLK) @@ -187,13 +238,54 @@ static const int kdshwclk_ioctl = KDSHWCLK; #else static const bool got_kdghwclk = FALSE; static const int kdghwclk_ioctl; /* Never used; just to make compile work */ -struct hwclk_time {char dummy;}; +struct hwclk_time {int sec;}; /* Never used; just to make compile work */ #endif +/* We're going to assume that if the CPU is in the Intel x86 family, + this is an ISA family machine. For all practical purposes, this is + the case at the time of this writing, especially after we assume a + Linux kernel is running on it. + */ +const bool isa_machine = +#ifdef __i386__ +TRUE +#else +FALSE; +#endif +; + +const bool alpha_machine = +#ifdef __alpha__ +TRUE +#else +FALSE; +#endif +; -float + + +static int +i386_iopl(const int level) { +/*---------------------------------------------------------------------------- + When compiled for an Intel target, this is just the iopl() kernel call. + When compiled for any other target, this is a dummy function. + + We do it this way in order to keep the conditional compilation stuff + out of the way so it doesn't mess up readability of the code. +-----------------------------------------------------------------------------*/ +#ifdef __i386__ + extern int iopl(const int level); + return iopl(level); +#else + return -1; +#endif +} + + + +static float time_diff(struct timeval subtrahend, struct timeval subtractor) { /*--------------------------------------------------------------------------- The difference in seconds between two times in "timeval" format. @@ -203,7 +295,7 @@ time_diff(struct timeval subtrahend, struct timeval subtractor) { } -struct timeval +static struct timeval time_inc(struct timeval addend, float increment) { /*---------------------------------------------------------------------------- The time, in "timeval" format, which is <increment> seconds after @@ -231,18 +323,28 @@ static inline unsigned char hclock_read(unsigned char reg) { /*--------------------------------------------------------------------------- Relative byte <reg> of the Hardware Clock value. + + On non-ISA machine, just return 0. ---------------------------------------------------------------------------*/ -#ifdef __i386__ register unsigned char ret; +#ifdef __i386__ + const bool interrupts_were_enabled = interrupts_enabled; + __asm__ volatile ("cli"); + interrupts_enabled = FALSE; /* & 0x7f ensures that we are not disabling NMI while we read. Setting on Bit 7 here would disable NMI */ outb(reg & 0x7f, clock_ctl_addr); ret = inb(clock_data_addr); - __asm__ volatile ("sti"); - return ret; + if (interrupts_were_enabled) { + __asm__ volatile ("sti"); + interrupts_enabled = TRUE; + } +#else + ret = 0; #endif + return ret; } @@ -251,6 +353,8 @@ static inline void hclock_write(unsigned char reg, unsigned char val) { /*---------------------------------------------------------------------------- Set relative byte <reg> of the Hardware Clock value to <val>. + + On non-ISA machine, do nothing. ----------------------------------------------------------------------------*/ #ifdef __i386__ /* & 0x7f ensures that we are not disabling NMI while we read. @@ -276,7 +380,7 @@ hclock_write_bcd(int addr, int value) { } -void +static void read_adjtime(struct adjtime *adjtime_p, int *rc_p) { /*---------------------------------------------------------------------------- Read the adjustment parameters out of the /etc/adjtime file. @@ -348,7 +452,7 @@ read_adjtime(struct adjtime *adjtime_p, int *rc_p) { -void +static void synchronize_to_clock_tick_ISA(int *retcode_p) { /*---------------------------------------------------------------------------- Same as synchronize_to_clock_tick(), but just for ISA. @@ -371,14 +475,56 @@ synchronize_to_clock_tick_ISA(int *retcode_p) { -void +static void +busywait_for_rtc_clock_tick(const int rtc_fd, int *retcode_p) { +/*---------------------------------------------------------------------------- + Wait for the top of a clock tick by reading /dev/rtc in a busy loop until + we see it. +-----------------------------------------------------------------------------*/ + struct tm start_time; + /* The time when we were called (and started waiting) */ + int rc; + + if (debug) + printf("Waiting in loop for time from /dev/rtc to change\n"); + + rc = ioctl(rtc_fd, RTC_RD_TIME, &start_time); + if (rc == -1) { + fprintf(stderr, "ioctl() to /dev/rtc to read time failed, " + "errno = %s (%d).\n", strerror(errno), errno); + *retcode_p = 1; + } else { + /* Wait for change. Should be within a second, but in case something + weird happens, we have a limit on this loop to reduce the impact + of this failure. + */ + struct tm nowtime; + int i; /* local loop index */ + int rc; /* Return code from ioctl */ + + for (i = 0; + (rc = ioctl(rtc_fd, RTC_RD_TIME, &nowtime)) != -1 + && start_time.tm_sec == nowtime.tm_sec && i < 1000000; + i++); + if (i >= 1000000) { + fprintf(stderr, "Timed out waiting for time change.\n"); + *retcode_p = 2; + } else if (rc == -1) { + fprintf(stderr, "ioctl() to /dev/rtc to read time failed, " + "errno = %s (%d).\n", strerror(errno), errno); + *retcode_p = 3; + } else *retcode_p = 0; + } +} + + + +static void synchronize_to_clock_tick_RTC(int *retcode_p) { /*---------------------------------------------------------------------------- Same as synchronize_to_clock_tick(), but just for /dev/rtc. -----------------------------------------------------------------------------*/ -#if defined(_MC146818RTC_H) - int rc; /* local return code */ - int rtc_fd; /* File descriptor of /dev/rtc */ +int rtc_fd; /* File descriptor of /dev/rtc */ rtc_fd = open("/dev/rtc",O_RDONLY); if (rtc_fd == -1) { @@ -386,45 +532,97 @@ synchronize_to_clock_tick_RTC(int *retcode_p) { strerror(errno), errno); *retcode_p = 1; } else { + int rc; /* Return code from ioctl */ /* Turn on update interrupts (one per second) */ rc = ioctl(rtc_fd, RTC_UIE_ON, 0); - if (rc == -1) { - fprintf(stderr, "ioctl() to /dev/rtc to turn on update interrupts " - "failed, errno = %s (%d).\n", strerror(errno), errno); - *retcode_p = 1; - } else { + if (rc == -1 && errno == EINVAL) { + /* This rtc device doesn't have interrupt functions. This is typical + on an Alpha, where the Hardware Clock interrupts are used by the + kernel for the system clock, so aren't at the user's disposal. + */ + if (debug) printf("/dev/rtc does not have interrupt functions. "); + busywait_for_rtc_clock_tick(rtc_fd, retcode_p); + } else if (rc != -1) { + int rc; /* return code from ioctl */ unsigned long dummy; - /* this blocks */ - rc = read(rtc_fd, &dummy, sizeof(unsigned long)); + /* this blocks until the next update interrupt */ + rc = read(rtc_fd, &dummy, sizeof(dummy)); if (rc == -1) { fprintf(stderr, "read() to /dev/rtc to wait for clock tick failed, " "errno = %s (%d).\n", strerror(errno), errno); *retcode_p = 1; } else { *retcode_p = 0; - - /* Turn off update interrupts */ - rc = ioctl(rtc_fd, RTC_UIE_OFF, 0); - if (rc == -1) { - fprintf(stderr, "ioctl() to /dev/rtc to turn off update interrupts " - "failed, errno = %s (%d).\n", strerror(errno), errno); - } } + /* Turn off update interrupts */ + rc = ioctl(rtc_fd, RTC_UIE_OFF, 0); + if (rc == -1) { + fprintf(stderr, "ioctl() to /dev/rtc to turn off update interrupts " + "failed, errno = %s (%d).\n", strerror(errno), errno); + } + } else { + fprintf(stderr, "ioctl() to /dev/rtc to turn on update interrupts " + "failed unexpectedly, errno = %s (%d).\n", + strerror(errno), errno); + *retcode_p = 1; } close(rtc_fd); } -#else -/* This function should never be called. It is here just to make the - program compile. -*/ -#endif } - -int -synchronize_to_clock_tick(enum clock_access_method clock_access) { + +static void +synchronize_to_clock_tick_KD(int *retcode_p) { +/*---------------------------------------------------------------------------- + Wait for the top of a clock tick by calling KDGHWCLK in a busy loop until + we see it. +-----------------------------------------------------------------------------*/ + int con_fd; + + if (debug) + printf("Waiting in loop for time from KDGHWCLK to change\n"); + + con_fd = open("/dev/tty1", O_RDONLY); + if (con_fd < 0) { + fprintf(stderr, "open() failed to open /dev/tty1, errno = %s (%d).\n", + strerror(errno), errno); + *retcode_p = 1; + } else { + int rc; /* return code from ioctl() */ + int i; /* local loop index */ + /* The time when we were called (and started waiting) */ + struct hwclk_time start_time, nowtime; + + rc = ioctl(con_fd, kdghwclk_ioctl, &start_time); + if (rc == -1) { + fprintf(stderr, "KDGHWCLK to read time failed, " + "errno = %s (%d).\n", strerror(errno), errno); + *retcode_p = 3; + } + + for (i = 0; + (rc = ioctl(con_fd, kdghwclk_ioctl, &nowtime)) != -1 + && start_time.sec == nowtime.sec && i < 1000000; + i++); + if (i >= 1000000) { + fprintf(stderr, "Timed out waiting for time change.\n"); + *retcode_p = 2; + } else if (rc == -1) { + fprintf(stderr, "KDGHWCLK to read time failed, " + "errno = %s (%d).\n", strerror(errno), errno); + *retcode_p = 3; + } else *retcode_p = 0; + close(con_fd); + } +} + + + +static void +synchronize_to_clock_tick(enum clock_access_method clock_access, + int *retcode_p) { /*----------------------------------------------------------------------------- Wait until the falling edge of the Hardware Clock's update flag so that any time that is read from the clock immediately after we @@ -440,35 +638,27 @@ synchronize_to_clock_tick(enum clock_access_method clock_access) { just return immediately. This will mess some things up, but it's the best we can do. - Return 1 if something weird goes wrong (nothing can normally go wrong), - 0 if everything OK. + Return *retcode_p == 0 if it worked, nonzero if it didn't. -----------------------------------------------------------------------------*/ - int retcode; /* our eventual return code */ - if (debug) printf("Waiting for clock tick...\n"); switch (clock_access) { - case ISA: synchronize_to_clock_tick_ISA(&retcode); break; - case RTC_IOCTL: synchronize_to_clock_tick_RTC(&retcode); break; - case KD: - if (debug) - printf("Can't wait for clock tick because we're using the Alpha " - "/dev/console clock! Assuming a clock tick.\n"); - retcode = 1; - break; + case ISA: synchronize_to_clock_tick_ISA(retcode_p); break; + case RTC_IOCTL: synchronize_to_clock_tick_RTC(retcode_p); break; + case KD: synchronize_to_clock_tick_KD(retcode_p); break; default: fprintf(stderr, "Internal error in synchronize_to_clock_tick. Invalid " "value for clock_access argument.\n"); - retcode = 1; + *retcode_p = 1; } if (debug) printf("...got clock tick\n"); - return(retcode); + return; } -time_t +static time_t mktime_tz(struct tm tm, const bool universal) { /*----------------------------------------------------------------------------- Convert a time in broken down format (hours, minutes, etc.) into standard @@ -489,7 +679,7 @@ mktime_tz(struct tm tm, const bool universal) { if (universal) { /* Set timezone to UTC */ - (void) putenv("TZ="); + setenv("TZ", "", TRUE); /* Note: tzset() gets called implicitly by the time code, but only the first time. When changing the environment variable, better call tzset() explicitly. @@ -504,10 +694,8 @@ mktime_tz(struct tm tm, const bool universal) { } /* now put back the original zone. */ - if (zone) - setenv ("TZ", zone, 1); - else - putenv ("TZ"); + if (zone) setenv("TZ", zone, TRUE); + else unsetenv("TZ"); tzset(); if (debug) @@ -519,20 +707,23 @@ mktime_tz(struct tm tm, const bool universal) { -void +static void read_hardware_clock_kd(struct tm *tm) { /*---------------------------------------------------------------------------- Read the hardware clock and return the current time via <tm> - argument. Use ioctls to /dev/console on what we assume is an Alpha + argument. Use ioctls to /dev/tty1 on what we assume is an m68k machine. + + Note that we don't use /dev/console here. That might be a serial + console. -----------------------------------------------------------------------------*/ #ifdef KDGHWCLK int con_fd; struct hwclk_time t; - con_fd = open("/dev/console", O_RDONLY); + con_fd = open("/dev/tty1", O_RDONLY); if (con_fd < 0) { - fprintf(stderr, "open() failed to open /dev/console, errno = %s (%d).\n", + fprintf(stderr, "open() failed to open /dev/tty1, errno = %s (%d).\n", strerror(errno), errno); exit(5); } else { @@ -540,7 +731,7 @@ read_hardware_clock_kd(struct tm *tm) { rc = ioctl(con_fd, kdghwclk_ioctl, &t); if (rc == -1) { - fprintf(stderr, "ioctl() failed to read time from /dev/console, " + fprintf(stderr, "ioctl() failed to read time from /dev/tty1, " "errno = %s (%d).\n", strerror(errno), errno); exit(5); @@ -565,7 +756,7 @@ read_hardware_clock_kd(struct tm *tm) { -void +static void read_hardware_clock_rtc_ioctl(struct tm *tm) { /*---------------------------------------------------------------------------- Read the hardware clock and return the current time via <tm> @@ -600,52 +791,85 @@ read_hardware_clock_rtc_ioctl(struct tm *tm) { -void +static void read_hardware_clock_isa(struct tm *tm) { /*---------------------------------------------------------------------------- Read the hardware clock and return the current time via <tm> argument. Assume we have an ISA machine and read the clock directly with CPU I/O instructions. + + This function is not totally reliable. It takes a finite and + unpredictable amount of time to execute the code below. During that + time, the clock may change and we may even read an invalid value in + the middle of an update. We do a few checks to minimize this + possibility, but only the kernel can actually read the clock + properly, since it can execute code in a short and predictable + amount of time (by turning of interrupts). + + In practice, the chance of this function returning the wrong time is + extremely remote. + -----------------------------------------------------------------------------*/ - /* The loop here is just for integrity. In theory it should never run - more than once - */ - do { - tm->tm_sec = hclock_read_bcd(0); - tm->tm_min = hclock_read_bcd(2); - tm->tm_hour = hclock_read_bcd(4); - tm->tm_wday = hclock_read_bcd(6); - tm->tm_mday = hclock_read_bcd(7); - tm->tm_mon = hclock_read_bcd(8); - tm->tm_year = hclock_read_bcd(9); - if (hclock_read_bcd(50) == 0) { - /* I suppose Linux could run on an old machine that doesn't implement - the Byte 50 century value, and that if it does, that machine puts - zero in Byte 50. If so, this could could be useful, in that it - makes values 70-99 -> 1970-1999 and 00-69 -> 2000-2069. - */ - if (hclock_read_bcd(9) >= 70) tm->tm_year = hclock_read_bcd(9); - else tm->tm_year = hclock_read_bcd(9) + 100; - } else { - tm->tm_year = hclock_read_bcd(50) * 100 + hclock_read_bcd(9) - 1900; - /* Note: Byte 50 contains centuries since A.D. Byte 9 contains - years since beginning of century. tm_year contains years - since 1900. At least we _assume_ that's what tm_year - contains. It is documented only as "year", and it could - conceivably be years since the beginning of the current - century. If so, this code won't work after 1999. + bool got_time; + /* We've successfully read a time from the Hardware Clock */ + + got_time = FALSE; + while (!got_time) { + /* Bit 7 of Byte 10 of the Hardware Clock value is the Update In Progress + (UIP) bit, which is on while and 244 uS before the Hardware Clock + updates itself. It updates the counters individually, so reading + them during an update would produce garbage. The update takes 2mS, + so we could be spinning here that long waiting for this bit to turn + off. + + Furthermore, it is pathologically possible for us to be in this + code so long that even if the UIP bit is not on at first, the + clock has changed while we were running. We check for that too, + and if it happens, we start over. + */ + + if ((hclock_read(10) & 0x80) == 0) { + /* No clock update in progress, go ahead and read */ + tm->tm_sec = hclock_read_bcd(0); + tm->tm_min = hclock_read_bcd(2); + tm->tm_hour = hclock_read_bcd(4); + tm->tm_wday = hclock_read_bcd(6) - 3; + tm->tm_mday = hclock_read_bcd(7); + tm->tm_mon = hclock_read_bcd(8) - 1; + tm->tm_year = hclock_read_bcd(9); + if (hclock_read_bcd(50) == 0) { + /* I suppose Linux could run on an old machine that doesn't implement + the Byte 50 century value, and that if it does, that machine puts + zero in Byte 50. If so, this could could be useful, in that it + makes values 70-99 -> 1970-1999 and 00-69 -> 2000-2069. + */ + if (hclock_read_bcd(9) >= 70) tm->tm_year = hclock_read_bcd(9); + else tm->tm_year = hclock_read_bcd(9) + 100; + } else { + tm->tm_year = hclock_read_bcd(50) * 100 + hclock_read_bcd(9) - 1900; + /* Note: Byte 50 contains centuries since A.D. Byte 9 contains + years since beginning of century. tm_year contains years + since 1900. At least we _assume_ that's what tm_year + contains. It is documented only as "year", and it could + conceivably be years since the beginning of the current + century. If so, this code won't work after 1999. + */ + } + /* Unless the clock changed while we were reading, consider this + a good clock read . */ + if (tm->tm_sec == hclock_read_bcd (0)) got_time = TRUE; + /* Yes, in theory we could have been running for 60 seconds and + the above test wouldn't work! + */ } - } while (tm->tm_sec != hclock_read_bcd (0)); - - tm->tm_mon--; /* DOS uses 1 base */ - tm->tm_wday -= 3; /* DOS uses 3 - 9 for week days */ + } tm->tm_isdst = -1; /* don't know whether it's daylight */ } -void +static void read_hardware_clock(const enum clock_access_method method, struct tm *tm){ /*---------------------------------------------------------------------------- Read the hardware clock and return the current time via <tm> argument. @@ -674,39 +898,41 @@ read_hardware_clock(const enum clock_access_method method, struct tm *tm){ -void +static void set_hardware_clock_kd(const struct tm new_broken_time, const bool testing) { /*---------------------------------------------------------------------------- Set the Hardware Clock to the time <new_broken_time>. Use ioctls to - /dev/console on what we assume is an Alpha machine. + /dev/tty1 on what we assume is an m68k machine. + + Note that we don't use /dev/console here. That might be a serial console. ----------------------------------------------------------------------------*/ #ifdef KDGHWCLK - int con_fd; /* File descriptor of /dev/console */ + int con_fd; /* File descriptor of /dev/tty1 */ struct hwclk_time t; - con_fd = open("/dev/console", O_RDONLY); + con_fd = open("/dev/tty1", O_RDONLY); if (con_fd < 0) { - fprintf(stderr, "Error opening /dev/console. Errno: %s (%d)\n", + fprintf(stderr, "Error opening /dev/tty1. Errno: %s (%d)\n", strerror(errno), errno); exit(1); } else { int rc; /* locally used return code */ - t.sec = new_broken_time->tm_sec; - t.min = new_broken_time->tm_min; - t.hour = new_broken_time->tm_hour; - t.day = new_broken_time->tm_mday; - t.mon = new_broken_time->tm_mon; - t.year = new_broken_time->tm_year; - t.wday = new_broken_time->tm_wday; + t.sec = new_broken_time.tm_sec; + t.min = new_broken_time.tm_min; + t.hour = new_broken_time.tm_hour; + t.day = new_broken_time.tm_mday; + t.mon = new_broken_time.tm_mon; + t.year = new_broken_time.tm_year; + t.wday = new_broken_time.tm_wday; if (testing) printf("Not setting Hardware Clock because running in test mode.\n"); else { rc = ioctl(con_fd, kdshwclk_ioctl, &t ); if (rc < 0) { - fprintf(stderr, "ioctl() to open /dev/console failed. " + fprintf(stderr, "ioctl() to open /dev/tty1 failed. " "Errno: %s (%d)\n", strerror(errno), errno); exit(1); @@ -723,7 +949,7 @@ set_hardware_clock_kd(const struct tm new_broken_time, -void +static void set_hardware_clock_rtc_ioctl(const struct tm new_broken_time, const bool testing) { /*---------------------------------------------------------------------------- @@ -739,14 +965,19 @@ set_hardware_clock_rtc_ioctl(const struct tm new_broken_time, strerror(errno), errno); exit(5); } else { - rc = ioctl(rtc_fd, RTC_SET_TIME, &new_broken_time); - if (rc == -1) { - fprintf(stderr, "ioctl() (RTC_SET_TIME) to /dev/rtc to set time failed, " - "errno = %s (%d).\n", strerror(errno), errno); - exit(5); - } else { - if (debug) - fprintf(stderr, "ioctl(RTC_SET_TIME) was successful.\n"); + if (testing) + printf("Not setting Hardware Clock because running in test mode.\n"); + else { + rc = ioctl(rtc_fd, RTC_SET_TIME, &new_broken_time); + if (rc == -1) { + fprintf(stderr, + "ioctl() (RTC_SET_TIME) to /dev/rtc to set time failed, " + "errno = %s (%d).\n", strerror(errno), errno); + exit(5); + } else { + if (debug) + printf("ioctl(RTC_SET_TIME) was successful.\n"); + } } close(rtc_fd); } @@ -754,7 +985,7 @@ set_hardware_clock_rtc_ioctl(const struct tm new_broken_time, -void +static void set_hardware_clock_isa(const struct tm new_broken_time, const bool testing) { /*---------------------------------------------------------------------------- @@ -763,12 +994,16 @@ set_hardware_clock_isa(const struct tm new_broken_time, an ISA Hardware Clock. ----------------------------------------------------------------------------*/ unsigned char save_control, save_freq_select; +#ifdef __i386__ + const bool interrupts_were_enabled = interrupts_enabled; +#endif if (testing) printf("Not setting Hardware Clock because running in test mode.\n"); else { #ifdef __i386__ __asm__ volatile ("cli"); + interrupts_enabled = FALSE; #endif save_control = hclock_read(11); /* tell the clock it's being set */ hclock_write(11, (save_control | 0x80)); @@ -801,13 +1036,16 @@ set_hardware_clock_isa(const struct tm new_broken_time, hclock_write (11, save_control); hclock_write (10, save_freq_select); #ifdef __i386__ - __asm__ volatile ("sti"); + if (interrupts_were_enabled) { + __asm__ volatile ("sti"); + interrupts_enabled = TRUE; + } #endif } } -void +static void set_hardware_clock(const enum clock_access_method method, const time_t newtime, const bool universal, @@ -852,7 +1090,7 @@ set_hardware_clock(const enum clock_access_method method, -void +static void set_hardware_clock_exact(const time_t settime, const struct timeval ref_time, const enum clock_access_method clock_access, @@ -897,7 +1135,91 @@ set_hardware_clock_exact(const time_t settime, -void +static void +get_epoch(unsigned long *epoch_p, int *retcode_p) { +/*---------------------------------------------------------------------------- + Get the Hardware Clock epoch setting from the kernel. +----------------------------------------------------------------------------*/ + int rtc_fd; + + rtc_fd = open("/dev/rtc", O_RDONLY); + if (rtc_fd < 0) { + if (errno == ENOENT) + fprintf(stderr, "To manipulate the epoch value in the kernel, we must " + "access the Linux 'rtc' device driver via the device special " + "file /dev/rtc. This file does not exist on this system.\n"); + else + fprintf(stderr, "Unable to open /dev/rtc, open() errno = %s (%d)\n", + strerror(errno), errno); + *retcode_p = 1; + } else { + int rc; /* return code from ioctl */ + rc = ioctl(rtc_fd, RTC_EPOCH_READ, epoch_p); + if (rc == -1) { + fprintf(stderr, "ioctl(RTC_EPOCH_READ) to /dev/rtc failed, " + "errno = %s (%d).\n", strerror(errno), errno); + *retcode_p = 1; + } else { + *retcode_p = 0; + if (debug) printf("we have read epoch %ld from /dev/rtc " + "with RTC_EPOCH_READ ioctl.\n", *epoch_p); + } + close(rtc_fd); + } + return; +} + + + +static void +set_epoch(unsigned long epoch, const bool testing, int *retcode_p) { +/*---------------------------------------------------------------------------- + Set the Hardware Clock epoch in the kernel. +----------------------------------------------------------------------------*/ + if (epoch < 1900) + /* kernel would not accept this epoch value */ + fprintf(stderr, "The epoch value may not be less than 1900. " + "You requested %ld\n", epoch); + else { + int rtc_fd; + + rtc_fd = open("/dev/rtc", O_RDONLY); + if (rtc_fd < 0) { + if (errno == ENOENT) + fprintf(stderr, "To manipulate the epoch value in the kernel, we must " + "access the Linux 'rtc' device driver via the device special " + "file /dev/rtc. This file does not exist on this system.\n"); + fprintf(stderr, "Unable to open /dev/rtc, open() errno = %s (%d)\n", + strerror(errno), errno); + *retcode_p = 1; + } else { + if (debug) printf("setting epoch to %ld " + "with RTC_EPOCH_SET ioctl to /dev/rtc.\n", epoch); + if (testing) { + printf("Not setting epoch because running in test mode.\n"); + *retcode_p = 0; + } else { + int rc; /* return code from ioctl */ + rc = ioctl(rtc_fd, RTC_EPOCH_SET, epoch); + if (rc == -1) { + if (errno == EINVAL) + fprintf(stderr, "The kernel (specifically, the device driver " + "for /dev/rtc) does not have the RTC_EPOCH_SET ioctl. " + "Get a newer driver.\n"); + else + fprintf(stderr, "ioctl(RTC_EPOCH_SET) to /dev/rtc failed, " + "errno = %s (%d).\n", strerror(errno), errno); + *retcode_p = 1; + } else *retcode_p = 0; + } + close(rtc_fd); + } + } +} + + + +static void display_time(const time_t systime, const float sync_duration) { /*---------------------------------------------------------------------------- Put the time "systime" on standard output in display format. @@ -917,8 +1239,8 @@ display_time(const time_t systime, const float sync_duration) { -int -interpret_date_string(const char *date_opt, const time_t *time_p) { +static int +interpret_date_string(const char *date_opt, time_t * const time_p) { /*---------------------------------------------------------------------------- Interpret the value of the --date option, which is something like "13:05:01". In fact, it can be any of the myriad ASCII strings that specify @@ -927,16 +1249,16 @@ interpret_date_string(const char *date_opt, const time_t *time_p) { The specified time is in the local time zone. - Our output, "*newtime", is a seconds-into-epoch time. + Our output, "*time_p", is a seconds-into-epoch time. We use the "date" program to interpret the date string. "date" must be runnable by issuing the command "date" to the /bin/sh shell. That means in must be in the current PATH. - If anything goes wrong (and many things can), we return return code 10. - Otherwise, return code is 0 and *newtime is valid. + If anything goes wrong (and many things can), we return return code + 10 and arbitrary *time_p. Otherwise, return code is 0 and *time_p + is valid. ----------------------------------------------------------------------------*/ - FILE *date_child_fp; char date_resp[100]; const char magic[]="seconds-into-epoch="; @@ -974,7 +1296,8 @@ interpret_date_string(const char *date_opt, const time_t *time_p) { date_command, date_resp); retcode = 8; } else { - rc = sscanf(date_resp + sizeof(magic)-1, "%d", (int *) time_p); + int seconds_since_epoch; + rc = sscanf(date_resp + sizeof(magic)-1, "%d", &seconds_since_epoch); if (rc < 1) { fprintf(stderr, "The date command issued by " MYNAME " returned" "something other than an integer where the converted" @@ -984,6 +1307,7 @@ interpret_date_string(const char *date_opt, const time_t *time_p) { retcode = 6; } else { retcode = 0; + *time_p = seconds_since_epoch; if (debug) printf("date string %s equates to %d seconds since 1969.\n", date_opt, (int) *time_p); @@ -997,8 +1321,8 @@ interpret_date_string(const char *date_opt, const time_t *time_p) { -int -set_system_clock(const time_t newtime, const int testing) { +static int +set_system_clock(const time_t newtime, const bool testing) { struct timeval tv; int retcode; /* our eventual return code */ @@ -1009,9 +1333,8 @@ set_system_clock(const time_t newtime, const int testing) { if (debug) { printf( "Calling settimeofday:\n" ); - /* Note: In Linux 1.2, tv_sec and tv_usec were long int */ - printf( "\ttv.tv_sec = %d, tv.tv_usec = %d\n", - tv.tv_sec, tv.tv_usec ); + printf( "\ttv.tv_sec = %ld, tv.tv_usec = %ld\n", + (long) tv.tv_sec, (long) tv.tv_usec ); } if (testing) { printf("Not setting system clock because running in test mode.\n"); @@ -1032,7 +1355,7 @@ set_system_clock(const time_t newtime, const int testing) { } -void +static void adjust_drift_factor(struct adjtime *adjtime_p, const time_t nowtime, const time_t hclocktime ) { @@ -1081,7 +1404,7 @@ adjust_drift_factor(struct adjtime *adjtime_p, -void +static void calculate_adjustment( const float factor, const time_t last_time, @@ -1121,7 +1444,7 @@ calculate_adjustment( -void +static void save_adjtime(const struct adjtime adjtime, const bool testing) { /*----------------------------------------------------------------------------- Write the contents of the <adjtime> structure to its disk file. @@ -1130,16 +1453,18 @@ save_adjtime(const struct adjtime adjtime, const bool testing) { bother. -----------------------------------------------------------------------------*/ FILE *adjfile; - char newfile[162]; /* Stuff to write to disk file */ + char newfile[405]; /* Stuff to write to disk file */ int rc; /* locally used: return code from a function */ if (adjtime.dirty) { - snprintf(newfile, sizeof(newfile), "%f %d %f\n%d\n", + /* snprintf is not always available, but this is safe + as long as libc does not use more than 100 positions for %ld or %f */ + sprintf(newfile, "%f %ld %f\n%ld\n", adjtime.drift_factor, - adjtime.last_adj_time, + (long) adjtime.last_adj_time, adjtime.not_adjusted, - adjtime.last_calib_time ); + (long) adjtime.last_calib_time ); if (testing) { printf("Not updating adjtime file because of testing mode.\n"); @@ -1180,7 +1505,7 @@ save_adjtime(const struct adjtime adjtime, const bool testing) { -void +static void do_adjustment(struct adjtime *adjtime_p, const time_t hclocktime, const struct timeval read_time, const enum clock_access_method clock_access, @@ -1236,7 +1561,7 @@ do_adjustment(struct adjtime *adjtime_p, -void +static void determine_clock_access_method(const bool user_requests_ISA, enum clock_access_method *clock_access_p) { /*---------------------------------------------------------------------------- @@ -1245,6 +1570,8 @@ determine_clock_access_method(const bool user_requests_ISA, using compile-time constants. <user_requests_ISA> means the user explicitly asked for the ISA method. + Even if he did, we will not select the ISA method if this is not an + ISA machine. -----------------------------------------------------------------------------*/ bool rtc_works; /* The /dev/rtc method is available and seems to work on this machine */ @@ -1261,15 +1588,20 @@ determine_clock_access_method(const bool user_requests_ISA, "falling back to more primitive clock access method.\n", strerror(errno), errno); } - } else rtc_works = TRUE; + } else { + if (debug) + printf("The Linux kernel for which this copy of hwclock() was built " + "is too old to have /dev/rtc\n"); + rtc_works = FALSE; + } - if (user_requests_ISA) *clock_access_p = ISA; + if (user_requests_ISA && isa_machine) *clock_access_p = ISA; else if (rtc_works) *clock_access_p = RTC_IOCTL; else if (got_kdghwclk) { int con_fd; struct hwclk_time t; - con_fd = open("/dev/console", O_RDONLY); + con_fd = open("/dev/tty1", O_RDONLY); if (con_fd >= 0) { if (ioctl( con_fd, kdghwclk_ioctl, &t ) >= 0) *clock_access_p = KD; @@ -1287,17 +1619,18 @@ determine_clock_access_method(const bool user_requests_ISA, } else { *clock_access_p = KD; fprintf(stderr, - "Can't open /dev/console. open() errno = %s (%d).\n", + "Can't open /dev/tty1. open() errno = %s (%d).\n", strerror(errno), errno); } close(con_fd); - } else { + } else if (isa_machine) { *clock_access_p = ISA; - } + } else + *clock_access_p = NOCLOCK; if (debug) { switch (*clock_access_p) { case ISA: printf("Using direct I/O instructions to ISA clock.\n"); break; - case KD: printf("Using /dev/console interface to Alpha clock.\n"); break; + case KD: printf("Using KDGHWCLK interface to m68k clock.\n"); break; case RTC_IOCTL: printf("Using /dev/rtc interface to clock.\n"); break; default: printf("determine_clock_access_method() returned invalid value: %d.\n", @@ -1308,14 +1641,14 @@ determine_clock_access_method(const bool user_requests_ISA, -void +static void manipulate_clock(const bool show, const bool adjust, const bool set, const time_t set_time, const bool hctosys, const bool systohc, const struct timeval startup_time, const enum clock_access_method clock_access, const bool universal, const bool testing, - int *retcode + int *retcode_p ) { /*--------------------------------------------------------------------------- Do all the normal work of hwclock - read, set clock, etc. @@ -1338,10 +1671,10 @@ manipulate_clock(const bool show, const bool adjust, bool no_auth; /* User lacks necessary authorization to access the clock */ if (clock_access == ISA) { - rc = iopl(3); + rc = i386_iopl(3); if (rc != 0) { fprintf(stderr, MYNAME " is unable to get I/O port access. " - "I.e. iopl(3) returned nonzero return code %d.\n" + "I.e. iopl(2) returned nonzero return code %d.\n" "This is often because the program isn't running " "with superuser privilege, which it needs.\n", rc); @@ -1349,56 +1682,108 @@ manipulate_clock(const bool show, const bool adjust, } else no_auth = FALSE; } else no_auth = FALSE; - if (no_auth) *retcode = 1; + if (no_auth) *retcode_p = 1; else { - if (adjust || set) + if (adjust || set || systohc) read_adjtime(&adjtime, &rc); else { /* A little trick to avoid reading the file if we don't have to */ adjtime.dirty = FALSE; rc = 0; } - if (rc != 0) *retcode = 2; + if (rc != 0) *retcode_p = 2; else { - synchronize_to_clock_tick(clock_access); /* this takes up to 1 second */ - - /* Get current time from Hardware Clock, in case we need it */ - gettimeofday(&read_time, NULL); - read_hardware_clock(clock_access, &tm); - hclocktime = mktime_tz(tm, universal); - - if (show) { - display_time(hclocktime, time_diff(read_time, startup_time)); - *retcode = 0; - } else if (set) { - set_hardware_clock_exact(set_time, startup_time, - clock_access, universal, testing); - adjust_drift_factor(&adjtime, set_time, hclocktime); - *retcode = 0; - } else if (adjust) { - do_adjustment(&adjtime, hclocktime, read_time, clock_access, - universal, testing); - *retcode = 0; - } else if (systohc) { - struct timeval nowtime, reftime; - /* We can only set_hardware_clock_exact to a whole seconds time, so we - set it with reference to the most recent whole seconds time. - */ - gettimeofday(&nowtime, NULL); - reftime.tv_sec = nowtime.tv_sec; - reftime.tv_usec = 0; - - set_hardware_clock_exact((time_t) reftime.tv_sec, reftime, - clock_access, universal, testing); - *retcode = 0; - } else if (hctosys) { - rc = set_system_clock(hclocktime, testing); - if (rc != 0) { - printf("Unable to set system clock.\n"); - *retcode = 1; - } else *retcode = 0; + synchronize_to_clock_tick(clock_access, retcode_p); + /* this takes up to 1 second */ + if (*retcode_p == 0) { + /* Get current time from Hardware Clock, in case we need it */ + gettimeofday(&read_time, NULL); + read_hardware_clock(clock_access, &tm); + hclocktime = mktime_tz(tm, universal); + + if (show) { + display_time(hclocktime, time_diff(read_time, startup_time)); + *retcode_p = 0; + } else if (set) { + set_hardware_clock_exact(set_time, startup_time, + clock_access, universal, testing); + adjust_drift_factor(&adjtime, set_time, hclocktime); + *retcode_p = 0; + } else if (adjust) { + do_adjustment(&adjtime, hclocktime, read_time, clock_access, + universal, testing); + *retcode_p = 0; + } else if (systohc) { + struct timeval nowtime, reftime; + /* We can only set_hardware_clock_exact to a whole seconds + time, so we set it with reference to the most recent + whole seconds time. + */ + gettimeofday(&nowtime, NULL); + reftime.tv_sec = nowtime.tv_sec; + reftime.tv_usec = 0; + + set_hardware_clock_exact((time_t) reftime.tv_sec, reftime, + clock_access, universal, testing); + *retcode_p = 0; + adjust_drift_factor(&adjtime, (time_t) reftime.tv_sec, hclocktime); + } else if (hctosys) { + rc = set_system_clock(hclocktime, testing); + if (rc != 0) { + printf("Unable to set system clock.\n"); + *retcode_p = 1; + } else *retcode_p = 0; + } + save_adjtime(adjtime, testing); + } + } + } +} + + + +static void +manipulate_epoch(const bool getepoch, const bool setepoch, + const int epoch_opt, const bool testing) { +/*---------------------------------------------------------------------------- + Get or set the Hardware Clock epoch value in the kernel, as appropriate. + <getepoch>, <setepoch>, and <epoch> are hwclock invocation options. + + <epoch> == -1 if the user did not specify an "epoch" option. + +-----------------------------------------------------------------------------*/ + /* + Maintenance note: This should work on non-Alpha machines, but the + evidence today (98.03.04) indicates that the kernel only keeps the + epoch value on Alphas. If that is ever fixed, this function should be + changed. + */ + + if (!alpha_machine) + fprintf(stderr, "The kernel keeps an epoch value for the Hardware Clock " + "only on an Alpha machine.\nThis copy of hwclock was built for " + "a machine other than Alpha\n(and thus is presumably not running " + "on an Alpha now). No action taken.\n"); + else { + if (getepoch) { + unsigned long epoch; + int retcode; + + get_epoch(&epoch, &retcode); + if (retcode != 0) + printf("Unable to get the epoch value from the kernel.\n"); + else + printf("Kernel is assuming an epoch value of %lu\n", epoch); + } else if (setepoch) { + if (epoch_opt == -1) + fprintf(stderr, "To set the epoch value, you must use the 'epoch' " + "option to tell to what value to set it.\n"); + else { + int rc; + set_epoch(epoch_opt, testing, &rc); + if (rc != 0) + printf("Unable to set the epoch value in the kernel.\n"); } - save_adjtime(adjtime, testing); } } } @@ -1430,38 +1815,47 @@ main(int argc, char **argv, char **envp) { are given by the option_def. The only exception is <show>, which may be modified after parsing is complete to effect an implied option. */ - bool show, set, systohc, hctosys, adjust, version; + bool show, set, systohc, hctosys, adjust, getepoch, setepoch, version; bool universal, testing, directisa; char *date_opt; + int epoch_opt; const optStruct option_def[] = { { 'r', (char *) "show", OPT_FLAG, &show, 0 }, { 0, (char *) "set", OPT_FLAG, &set, 0 }, { 'w', (char *) "systohc", OPT_FLAG, &systohc, 0 }, { 's', (char *) "hctosys", OPT_FLAG, &hctosys, 0 }, + { 0, (char *) "getepoch", OPT_FLAG, &getepoch, 0 }, + { 0, (char *) "setepoch", OPT_FLAG, &setepoch, 0 }, { 'a', (char *) "adjust", OPT_FLAG, &adjust, 0 }, { 'v', (char *) "version", OPT_FLAG, &version, 0 }, { 0, (char *) "date", OPT_STRING, &date_opt, 0 }, + { 0, (char *) "epoch", OPT_UINT, &epoch_opt, 0 }, { 'u', (char *) "utc", OPT_FLAG, &universal, 0 }, { 0, (char *) "directisa", OPT_FLAG, &directisa, 0 }, { 0, (char *) "test", OPT_FLAG, &testing, 0 }, - { 'D', (char *) "debug", OPT_FLAG, &debug, 0 } + { 'D', (char *) "debug", OPT_FLAG, &debug, 0 }, + { 0, (char *) NULL, OPT_END, NULL, 0 } }; int argc_parse; /* argc, except we modify it as we parse */ char **argv_parse; /* argv, except we modify it as we parse */ + interrupts_enabled = TRUE; /* Since we haven't messed with them yet */ + gettimeofday(&startup_time, NULL); /* Remember what time we were invoked */ /* set option defaults */ - show = set = systohc = hctosys = adjust = version = universal = + show = set = systohc = hctosys = adjust = getepoch = setepoch = + version = universal = directisa = testing = debug = FALSE; date_opt = NULL; + epoch_opt = -1; argc_parse = argc; argv_parse = argv; optParseOptions(&argc_parse, argv_parse, option_def, 0); /* Uses and sets argc_parse, argv_parse. Sets show, systohc, hctosys, adjust, universal, version, testing, - debug, set, date_opt + debug, set, date_opt, getepoch, setepoch, epoch_opt */ if (argc_parse - 1 > 0) { @@ -1471,7 +1865,8 @@ main(int argc, char **argv, char **envp) { exit(100); } - if (show + set + systohc + hctosys + adjust + version > 1) { + if (show + set + systohc + hctosys + adjust + + getepoch + setepoch + version > 1) { fprintf(stderr, "You have specified multiple function options.\n" "You can only perform one function at a time.\n"); exit(100); @@ -1485,29 +1880,50 @@ main(int argc, char **argv, char **envp) { } } - if (!(show | set | systohc | hctosys | adjust | version)) - show = 1; /* default to show */ + if (directisa && !isa_machine) { + fprintf(stderr, "You have requested direct access to the ISA Hardware " + "Clock using machine instructions from the user process. " + "But this method only works on an ISA machine with an x86 " + "CPU, and this is not one!\n"); + exit(100); + } - if (set || hctosys || systohc || adjust) { - /* program is designed to run setuid, be secure! */ + if (!(show | set | systohc | hctosys | adjust | getepoch | setepoch | + version)) + show = 1; /* default to show */ - if (getuid() != 0) { + + if (getuid() == 0) permitted = TRUE; + else { + /* program is designed to run setuid (in some situations) -- be secure! */ + if (set || hctosys || systohc || adjust) { fprintf(stderr, - "Sorry, only superuser can change the Hardware Clock.\n"); + "Sorry, only the superuser can change the Hardware Clock.\n"); + permitted = FALSE; + } else if (setepoch) { + fprintf(stderr, + "Sorry, only the superuser can change " + "the Hardware Clock epoch in the kernel.\n"); permitted = FALSE; } else permitted = TRUE; - } else permitted = TRUE; + } if (!permitted) retcode = 2; else { retcode = 0; if (version) { printf(MYNAME " " VERSION "/%s\n",util_linux_version); + } else if (getepoch || setepoch) { + manipulate_epoch(getepoch, setepoch, epoch_opt, testing); } else { determine_clock_access_method(directisa, &clock_access); - - manipulate_clock(show, adjust, set, set_time, hctosys, systohc, - startup_time, clock_access, universal, testing, &rc); + if (clock_access == NOCLOCK) + fprintf(stderr, "Cannot access the Hardware Clock via any known " + "method. Use --debug option to see the details of our " + "search for an access method.\n"); + else + manipulate_clock(show, adjust, set, set_time, hctosys, systohc, + startup_time, clock_access, universal, testing, &rc); } } exit(retcode); @@ -1518,6 +1934,18 @@ main(int argc, char **argv, char **envp) { History of this program: + 98.03.05 BJH. Version 2.2. + + Add --getepoch and --setepoch. + + Fix some word length things so it works on Alpha. + + Make it work when /dev/rtc doesn't have the interrupt functions. + In this case, busywait for the top of a second instead of blocking and + waiting for the update complete interrupt. + + Fix a bunch of bugs too numerous to mention. + 97.06.01: BJH. Version 2.1. Read and write the century byte (Byte 50) of the ISA Hardware Clock when using direct ISA I/O. Problem discovered by job (jei@iclnl.icl.nl). |