Page 1 of 1

[Example?] How to read the RTC..

Posted: Sat Apr 07, 2007 4:18 pm
by mystran
This should go to the Wiki, I guess, but since I don't know whether to make an RTC page (as suggested?) or drop it in the current Time&Date page, or what... I thought I'd post it here, for those that have trouble figuring this out with just the ASM example and datasheets out there somewhere (I used the datasheet for bq3285ED/LD which claims to be compatible with the classic, whatever the number of the original chip).

The routine returns Unix time, which is UTC by definition, so it'll go wrong almost certainly (most PCs have RTC in localtime, which is stupid), but as the comment states, it's easier to adjust it later (with a system call from userspace) by the correct offset, than try to make kernel worry about timezones.

The code (supposedly) works between the years 2001 and 2099.

Code: Select all

#define RTC_BASE 0x70
#define RTC_DATA 0x71

//
// Return Unix time in seconds, reading current time and date from RTC.
//
// This assumes that RTC is in UTC, which it typically isn't, but since
// the timezone isn't known, just let some process adjust it after boot.
//
unsigned clock_rtc_read() {

    int update = 1; // update in progress flag
    while(update) {
        // check if there's update in progress
        out8_p(RTC_BASE, 0xA); // RTC register A
        unsigned char c = in8_p(RTC_DATA);
        if(!(c & 0x80)) {
            update = 0;
        }
    }

    // read the various RTC fields
    
    out8_p(RTC_BASE, 0);
    unsigned char sec = in8_p(RTC_DATA);

    out8_p(RTC_BASE, 2);
    unsigned char min = in8_p(RTC_DATA);

    out8_p(RTC_BASE, 4);
    unsigned char hour = in8_p(RTC_DATA);

    out8_p(RTC_BASE, 7); // day of month
    unsigned char day = in8_p(RTC_DATA);

    out8_p(RTC_BASE, 8);
    unsigned char month = in8_p(RTC_DATA);

    out8_p(RTC_BASE, 9); // two digits, we assume 2000-2099
    unsigned char year = in8_p(RTC_DATA);

    // read RTC register B to figure out how to deal with the data
    out8_p(RTC_BASE, 0xB);
    unsigned char format = in8_p(RTC_DATA);

    // convert all fields from BDC to binary if bit[2] is clear
    if(!(format & 0x4)) {

        sec = (sec & 0xf) + (sec>>4)*10;
        min = (min & 0xf) + (min>>4)*10;
        hour = (hour & 0xf) + ((hour&0x70)>>4)*10 + (hour&0x80); // keep bit-7
        day = (day & 0xf) + (day>>4)*10;
        month = (month & 0xf) + (month>>4)*10;
        year = (year & 0xf) + (year>>4)*10;

    }
    
    // convert hours from 12 to 24 format if bit[1] is clear
    if(!(format & 0x2)) {
        int ampm = hour&0x80;
        hour = hour&0x7f;
        // fix 24:00 = 12am oddity
        if(hour == 12) hour = 0;
        if(ampm) hour += 12;
    }

    // Figure out the number of days after the beginning of 2000

    // Our calculation is simplified by the fact that 2000 is a leap year,
    // and after that every 4th year is a leap year, until 2100 before which
    // this routine will have to be updated anyway, because RTC wraps to 00.
    //
    // So first calculate days as if there was no leap years, then add
    // 1 day for each leap year that has passed, and one for leap back in 2000.
    //
    // This will actually give bogus values if year is 2000 (or before that),
    // but for code written in 2007 that shouldn't matter that much...
    //
    unsigned fullyeardays = year * 365 + ((year-1)/4) + 1;

    // Next get the number of (full) days for this year, using the lookup
    // table for months. RTC day and month count from 1. Adjust month.
    // Only adjust day if it's not leap year, or if leap day hasn't passed.
    //
    static unsigned daysbeforemonth[12] // for normal, non-leap year
        = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
    unsigned thisyeardays = daysbeforemonth[month-1] + day
        - (((year%4)==0 && month > 2) ? 0 : 1); // only if no leap day passed

    // Sum days before and during this year to get date, then adjust EPOCH
    // to stay compatible with Unix style 1970-01-01 00:00.
    //
    // The number 10957 was arrived at by first using GNU date:
    //   date -d '2000-01-01 00:00:00 UTC' '+%s' -> 946684800
    // and dividing the result with 24*60*60 to get the number of days.
    //
    unsigned date = fullyeardays + thisyeardays + 10957;


    // Finally build system time, unix format seconds since 1970-01-01 00:00
    unsigned time = sec + 60 * (min + 60 * (hour + 24 * date));

    return time;
    
}
I haven't found a PC here that had RTC set to 12-hour clock, so I haven't tested that part, so if somebody knows it's wrong, please tell.

edit: oops, forgot the port addresses from the post

Posted: Sat Apr 07, 2007 5:01 pm
by Kevin McGuire
This is a perfect example. I will stash this in my brain box for later.

*kmcguire opens the little side door with a knob shaped just like a ear, and inserts the square shaped code function.
*Gears and sprockets being to turn making a clinkety clank sound.
*Suddenly a turd pops out in a compacted form ready to be placed neatly on a shelf for later use.

Posted: Sun Apr 08, 2007 9:01 pm
by pcmattman
This is really good! Thanks for this, I've been looking for it for quite a while.

Posted: Sun Apr 08, 2007 9:10 pm
by Alboin
Kevin McGuire wrote:*Suddenly a turd pops out in a compacted form ready to be placed neatly on a shelf for later use.
Wouldn't a turd smell after being stored on a shelf for awhile?

Posted: Mon Apr 09, 2007 1:25 am
by Candy
Turds dry out quickly and then become mostly smell-less, although they do become more fragile.

Mystran, nice code example although there are of course a few bits I'm going to whine about (but I'm still going to base my cmos code on this bit):

- It's BCD, not BDC (typo)
- Since you're using an unsigned, it's not actually unix time but a slightly modified version of it. You'd have to rethink your date format in 2100 anyway since the unsigned will overflow in 2108.
- Too bad you don't fully account for leap years, but given the above point I can see why you don't bother.

Thanks for the great code example!

Posted: Mon Apr 09, 2007 1:50 am
by pcmattman
I implemented this code, no problems whatsoever.

Now that I have this code I can study documentation and learn why it works.

Wouldn't it be better to use unsigned longs, or even just longs?

How would you make this work for any date since January 1 1970?

Posted: Mon Apr 09, 2007 2:25 am
by Candy
pcmattman wrote:I implemented this code, no problems whatsoever.

Now that I have this code I can study documentation and learn why it works.

Wouldn't it be better to use unsigned longs, or even just longs?

How would you make this work for any date since January 1 1970?
Theoretical answer: by using an infinitely long integer.

Practical answer: either by using an expanding code of sorts or agreeing on what "any date" really should mean - as in, what limit is good enough?


Mystran: strictly speaking, iirc, unix time said "every 4th year is a leap year" which will work in your code until the unsigned overflows.

Isn't the leap year adjustment with the month calculation the wrong way around?

Posted: Mon Apr 09, 2007 9:40 am
by mystran

Posted: Mon Apr 09, 2007 9:46 am
by mystran
Candy wrote: Isn't the leap year adjustment with the month calculation the wrong way around?
Huh? Don't think so. It's just strange:

We count the number of full days elapsed. Now months and days count from 1, so mounths-1 gives the correct index for the table. The number of days elapsed this month, then, is "days - 1". If it's leap year and at least 3rd month though, we must add one for 1.

So I just combine the two adjustments:

Code: Select all

 .... days - 1 + (leapyeard && month > 2) ? 1 : 0;
=>
 .... days + (leapyeard && month > 2) ? (1 - 1) : (0 - 1)
=>
 .... days + (leapyeard && month > 2) ? 0 : -1

Posted: Mon Apr 09, 2007 10:58 am
by Candy
Wikipedia wrote: The POSIX committee was swayed by arguments against complexity in the library functions, and firmly defined the Unix time in a simple manner in terms of the elements of UTC time. Unfortunately, this definition was so simple that it didn't even encompass the entire leap year rule of the Gregorian calendar, and would make 2100 a leap year.

The 2001 edition of POSIX.1 rectified the faulty leap year rule in the definition of Unix time, but retained the essential definition of Unix time as an encoding of UTC rather than a linear time scale.
So, yes, it was defined without a leap year but recently changed to match reality.

Posted: Mon Apr 09, 2007 11:28 am
by mystran
I especially like the rule about leap-seconds, and how they are mostly violated all the time by instead following the NTP rule, which differ by less than second. ;)

edit: I personally expect to not handle leap seconds anytime soon. Instead I plan to eventually let NTP speed-up/slow-down the system clock such that it resynchronizes to correct time after leap seconds. The reason for this is that then I can have continuous time, so every routine that needs to compare times doesn't need to figure out what to do with repeated seconds.

If I later feel like it's a problem, I'm going to change the rule such, that system time updates half (or twice) as fast during the leap second. Userspace NTP-client can do that as well, so it's not a kernel issue. Ofcourse that'll be a bigger problem for anything that depends on relative times, but will still keep times continuous.