Hi,
Some notes...
First, in my experience you can't rely on the RTC's "day of week" - lots of computers/BIOSs never set it correctly. Instead it's better to
calculate the day of the week from the other information. From "pctim003.txt":
pctim003.txt wrote:The day of week register (register 6) simply counts 1, 2, 3, 4, 5, 6, 7, 1,
2... where 1 means Sunday, 2 means Monday, etc. The RTC does not calculate
the day of the week from the date. This register must be set by software.
It is not used by the BIOS RTC functions or by DOS and will not necessarily
be set correctly. Software normally calculates the day of week from the other
date information rather than using this register. The RTC uses this register
to switch between standard time and daylight saving time if daylight saving is
enabled, but the daylight saving function is not used in PCs so there is no
need to make sure that this register is set correctly.
Newer computers do keep track of the century, but you need to read the ACPI tables to figure out where it's stored. It might be good to do something like:
Code: Select all
#define CURRENT_CENTURY 20
#define CURRENT_YEAR 08
...
if (centuryRTCoffset != 0) {
global_time.century = bcd2bin(read_register(centuryRTCoffset));
} else {
if(global_time.year >= CURRENT_YEAR) global_time.century = CURRENT_CENTURY;
else global_time.century = CURRENT_CENTURY + 1;
}
In this case your code to parse ACPI tables would set "centuryOffset" to tell your RTC code where to find the century (but it still works if the RTC doesn't keep track of the century, or if you haven't written code to parse the ACPI tables yet).
To read the RTC time without using an IRQ you need to make sure it's not in the middle of updating itself. To do that check the "update in progress" bit (bit 7 in RTC register A). This bit is set 244 us before the update starts and cleared when update ends to make sure software can reliably read the time. However, you'd want to make sure IRQs are disabled to avoid race conditions. For an example:
Code: Select all
do {
asm(sti);
asm(nop); // Give IRQs a chance!
asm(cli);
UIP = read_register(0x0A) & 0x80;
} while(UIP != 0);
If your not using any other RTC IRQ sources, then you don't need to guess which RTC IRQ source caused the IRQ (but you do still need to read from RTC register C).
You can't enable 24-hour mode and expect the RTC to change it's values by itself. If the RTC is in 12-hour mode and it's 1:00 PM then RTC register 4 will contain the value 0x81 (or "hour = 01" with the PM flag set). If you tell the RTC to operate in 24-hour mode then RTC register 4 will still contain 0x81 ("hour = 81" in BCD mode, "hour = 129" in binary mode). [Note: If you're lucky and it's in the morning when you change from 12-hour mode to 24-hour mode, then the "PM" flag wouldn't have been set and it will work without problems]
Like binary mode, I wouldn't assume that the BIOS is capable of operating correctly in 24-hour mode (if it uses 12-hour mode as default) or in 12-hour mode (if it uses 24-hour mode as default). However, you don't really need to check RTC register B to find out if the RTC is in 12-hour mode or 24-hour mode, you can simple assume it's in 12-hour mode if the PM flag is set in RTC register 4 (as the hour value never goes past 0x24 in BCD mode, so bit 7 would never be set in 24-hour mode). Also, be careful with midnight, as midnight is "12:00 PM" in 12-hour mode and "0:00" in 24-hour mode. For example:
Code: Select all
global_time.hour = read_register(0x04);
if( (global_time.hour & 0x80) != 0) {
global_time.hour = (global_time.hour & 0x7F) + 12;
if(global_time.hour == 24) global_time.hour = 0;
}
If you're using the RTC update IRQ to keep the time values current, then you don't need to read all of them because you know the minute won't change unless the second changed, the hour won't change unless the minute changed, etc. This is good because I/O port accesses are *slow* - it means that most of the time you only read the seconds, once per minute your read the seconds and the minute, once per hour you read the seconds, minute and hour, etc.
If you put everything I said together you end up with something like:
Code: Select all
void rtc_handler(struct regs* r)
{
unsigned char temp;
if(read_register(0x0C) & 0x10){
if(bcd){
temp = bcd2bin(read_register(0x00));
if(global_time.second != temp) {
global_time.second = temp;
temp = bcd2bin(read_register(0x02));
if(global_time.minute != temp) {
global_time.minute = temp;
temp = read_register(0x04);
if( (temp & 0x80) == 0) {
temp = bcd2bin(temp);
} else {
temp = bcd2bin(temp & 0x7F) + 12;
if(temp == 24) temp = 0;
}
if(global_time.hour != temp) {
global_time.hour = temp;
temp = bcd2bin(read_register(0x07));
if(global_time.day_of_month != temp) {
global_time.day_of_month = temp;
temp = bcd2bin(read_register(0x08));
if(global_time.month != temp) {
global_time.month = temp;
temp = bcd2bin(read_register(0x09));
if(global_time.year != temp) {
global_time.year = temp;
if (centuryRTCoffset != 0) {
global_time.century = bcd2bin(read_register(centuryRTCoffset));
} else {
if(global_time.year >= CURRENT_YEAR) global_time.century = CURRENT_CENTURY;
else global_time.century = CURRENT_CENTURY + 1;
}
}
}
}
}
}
}
} else {
temp = read_register(0x00);
if(global_time.second != temp) {
global_time.second = temp;
temp = read_register(0x02);
if(global_time.minute != temp) {
global_time.minute = temp;
temp = read_register(0x04);
if( (temp & 0x80) != 0) {
temp = (temp & 0x7F) + 12;
if(temp == 24) temp = 0;
}
if(global_time.hour != temp) {
global_time.hour = temp;
temp = read_register(0x07);
if(global_time.day_of_month != temp) {
global_time.day_of_month = temp;
temp = read_register(0x08);
if(global_time.month != temp) {
global_time.month = temp;
temp = read_register(0x09);
if(global_time.year != temp) {
global_time.year = temp;
if (centuryRTCoffset != 0) {
global_time.century = read_register(centuryRTCoffset);
} else {
if(global_time.year >= CURRENT_YEAR) global_time.century = CURRENT_CENTURY;
else global_time.century = CURRENT_CENTURY + 1;
}
}
}
}
}
}
}
}
}
}
void rtc_install(void)
{
unsigned char status;
status = read_register(0x0B);
status |= 0x10; // update ended interrupts
status &= ~0x20; // no alarm interrupts
status &= ~0x40; // no periodic interrupt
bcd = !(status & 0x04); // check if data type is BCD
write_register(0x0B, status);
get_full_RTC_time();
irq_install_handler(8, rtc_handler);
read_register(0x0C);
}
void get_full_RTC_time(void)
{
do {
asm(sti);
asm(nop); // Give IRQs a chance!
asm(cli);
UIP = read_register(0x0A) & 0x70;
} while(UIP != 0);
if(bcd){
global_time.second = bcd2bin(read_register(0x00));
global_time.minute = bcd2bin(read_register(0x02));
global_time.hour = read_register(0x04);
if( (global_time.hour & 0x80) == 0) {
global_time.hour = bcd2bin(global_time.hour);
} else {
global_time.hour = (bcd2bin(global_time.hour) & 0x7F) + 12;
if(global_time.hour == 24) global_time.hour = 0;
}
global_time.month = bcd2bin(read_register(0x08));
global_time.year = bcd2bin(read_register(0x09));
global_time.day_of_month = bcd2bin(read_register(0x07));
}else {
global_time.second = read_register(0x00);
global_time.minute = read_register(0x02);
global_time.hour = read_register(0x04);
if( (global_time.hour & 0x80) != 0) {
global_time.hour = (global_time.hour & 0x7F) + 12;
if(global_time.hour == 24) global_time.hour = 0;
}
global_time.month = read_register(0x08);
global_time.year = read_register(0x09);
global_time.day_of_month = read_register(0x07);
}
asm(sti);
}
That's a good start, but it's only a start.
Reading from I/O ports is still slow. When the RTC's update IRQ occurs then you know a second passed and don't really need to read any of the RTC's registers (except RTC register C). Basically you could do something like this (note: I combined the year and the century into "unsigned int year"):
Code: Select all
void rtc_handler(struct regs* r)
{
if(read_register(0x0C) & 0x10){
global_time.second++;
if(global_time.second >= 60) {
global_time.second = 0;
global_time.minute++;
if(global_time.minute >= 60) {
global_time.minute = 0;
global_time.hour++;
if(global_time.hour >= 60) {
global_time.hour = 0;
global_time.day_of_month++;
if(global_time.day_of_month >= global_time.max_days_this_month) {
global_time.day_of_month = 1;
global_time.month++;
if(global_time.month > 12) {
global_time.month = 1;
global_time.year++;
}
global_time.max_days_this_month = days_per_month_table[global_time.month];
if(global_time.month == 2) { // February - need to check for leap year
if( (year % 400) == 0) global_time.max_days_this_month++;
else if( (year % 100) != 0) {
if( (year % 4) == 0) global_time.max_days_this_month++;
}
}
}
}
}
}
}
}
That's faster and much simpler too.
Unfortunately, it's still not very useful.
The first problem is that it's mostly useless for timestamps (e.g. file modification times in file systems). For timestamps it's just not precise enough - typically you want "milliseconds since the epoch" (or even "nanoseconds since the epoch" for good file systems).
Secondly, there's a whole mess called daylight savings that it doesn't take into consideration.
Thirdly, you don't know if the RTC is set to local time or UTC. I really do hope it's set to UTC (because if it's set to local time you get severe problems when multiple OSs are installed, because nobody knows if either OS has adjusted it for daylight savings or not).
Fourth, it's a pain to add support for internationalization (e.g. different calenders) and different time zones.
Fifth, it's a pain to account for "drift" (either with a simple utility like
"adjtime" or with the
network time protocol).
Basically you've got many separate values (second, minute, hour, etc) and you want something like "64-bit nanoseconds since the start of the year 2000 (UTC)" so that it's much easier to handle all of these issues. In this case, during boot you'd read the RTC once and convert the results into a single value, then you'd use a more precise timer to keep that single value up to date (and convert that single value back into second, minute, hour, etc if you need to).
Cheers,
Brendan