Page 1 of 1

Basic RTC Driver (Real Time Clock) - C

Posted: Fri Jul 04, 2008 8:05 am
by Uranium
Basic RTC Driver I wrote last night for use in my operating system, UraniumOS.

I can be contacted via PM here: http://www.rohitab.com/discuss/index.php?showuser=15746 if you have any problems.

Code: Select all

/*
RTC.c
    With the exception of the commented out inportb and outportb functions, the following code
    was written by Uranium-239 with debugging help from Napalm. The author, Uraninium-239, requests
    that you leave in these two credits.

    Please feel free to modify this source.
*/

#include "RTC.h"
/* you may need to modify the above include depending on the layout of your Operating System */

time_t global_time;
bool bcd;

/*
    If you do not already have the functions inportb and outportb, then use use the examples below

unsigned char inportb(unsigned short port)
{
    unsigned char res;
    __asm__ __volatile__("inb %1, %0" : "=a"(res) : "dN"(port));
    return res;
}

void outportb(unsigned short port, unsigned char data)
{
    __asm__ __volatile__("outb %1, %0" : : "dN" (port), "a" (data));
}
*/

unsigned char read_register(unsigned char reg)
{
    outportb(0x70, reg);
    return inportb(0x71);
}

void write_register(unsigned char reg, unsigned char value)
{
    outportb(0x70, reg);
    outportb(0x71, value);
}

unsigned char bcd2bin(unsigned char bcd)
{
    return ((bcd >> 4) * 10) + (bcd & 0x0F);
}

void gettime(time_t *time)
{
    memcpy(time, &global_time, sizeof(time_t));
}

/*
struct regs
{
    unsigned int gs, fs, es, ds;
    unsigned int edi, esi, ebp, esp, ebx, edx, ecx, eax;
    unsigned int int_no, err_code;
    unsigned int eip, cs, eflags, useresp, ss;
};
*/
void rtc_handler(struct regs* r)
{
    if(read_register(0x0C) & 0x10){
        if(bcd){
            global_time.second = bcd2bin(read_register(0x00));
            global_time.minute = bcd2bin(read_register(0x02));
            global_time.hour   = bcd2bin(read_register(0x04));
            global_time.month  = bcd2bin(read_register(0x08));
            global_time.year   = bcd2bin(read_register(0x09));
            global_time.day_of_week  = bcd2bin(read_register(0x06));
            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);
            global_time.month  = read_register(0x08);
            global_time.year   = read_register(0x09);
            global_time.day_of_week  = read_register(0x06);
            global_time.day_of_month = read_register(0x07);
        }
    }
}

void rtc_install(void)
{
    unsigned char status;
    
    status = read_register(0x0B);
    status |=  0x02;             // 24 hour clock
    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);

    read_register(0x0C);

    irq_install_handler(8, rtc_handler);
    /* You may/may not have the above function. It basically installs our RTC handler on IRQ8. If you do not
    have support for installing interrupt handlers, see this link: http://www.osdever.net/bkerndev/Docs/irqs.htm */
}

Code: Select all

/*
RTC.h
    The following code was written by Uranium-239 with debugging help from Napalm. The
    author, Uraninium-239, requests that you leave in these two credits.

    Please feel free to modify this source.
*/

#ifndef _RTC_H_
#define _RTC_H_

typedef struct {
    unsigned char second;
    unsigned char minute;
    unsigned char hour;
    unsigned char day_of_week;
    unsigned char day_of_month;
    unsigned char month;
    unsigned char year;
} time_t;


extern void gettime(time_t* time);
extern void rtc_install(void);

#endif

Re: Basic RTC Driver (Real Time Clock) - C

Posted: Fri Jul 04, 2008 8:21 am
by lukem95
nicely commented code, im sure someone will find that very useful.

+ that's napalm from Rohitab.org right? i used to frequent that forum. It was actually Sekio from there that inspired me to get into OS dev.

Re: Basic RTC Driver (Real Time Clock) - C

Posted: Fri Jul 04, 2008 8:47 am
by Uranium
Rohitab.com, yeh its a nice community is rohitab.

Napalm: http://www.rohitab.com/discuss/index.php?showuser=3860

Re: Basic RTC Driver (Real Time Clock) - C

Posted: Sun Jul 06, 2008 3:58 pm
by sylvarant
I've incorporated this into my os and it works perfectly, very nice work
The only thing I'm trying to figure out is how to get the time immediately at upstart
i tried doing this :

Code: Select all

asm volatile ("int $0x28"); // call IRQ8
time_t * time = kmalloc(sizeof(time_t));
gettime(time);
print_dec(time->year); // always returns 0 ?

while(gettick < 100); // waits about a second so that the irq will be called automaticly
print_dec(time->year); // returns 8 like it should
now I'm wondering why it won't work if i call the irq handler manually :?:

Re: Basic RTC Driver (Real Time Clock) - C

Posted: Sun Jul 06, 2008 4:43 pm
by Uranium
it's set up so that whenever the RTC changes, (updates), the irq handler is called.

Therefore if the RTC hasn't updated yet it will be invalid =[

You could try changing

Code: Select all

void rtc_install(void)
{
    unsigned char status;
   
    status = read_register(0x0B);
    status |=  0x02;             // 24 hour clock
    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);

    read_register(0x0C);

    irq_install_handler(8, rtc_handler);
}
that to:

Code: Select all

void rtc_install(void)
{
    unsigned char status;
   
    write_register(0x0A, read_register(0x0A) | 0x0F); // important line here

    status = read_register(0x0B);
    status |=  0x02;             // 24 hour clock
    status &=  0x10;             // no update ended interrupts
    status &= ~0x20;             // no alarm interrupts
    status |=  0x40;             // enable periodic interrupt
    bcd     =  !(status & 0x04); // check if data type is BCD
    write_register(0x0B, status);

    read_register(0x0C);

    irq_install_handler(8, rtc_handler);
}
and changing:

Code: Select all

void rtc_handler(struct regs* r)
{
    if(read_register(0x0C) & 0x10){
to:

Code: Select all

void rtc_handler(struct regs* r)
{
    if(read_register(0x0C) & 0x40){
the line marked important, could also be written as follows, which might make it easier to understand?

Code: Select all

write_register(0x0A, (read_register(0x0A) & 0xF0) | 0x0F);
basically, you delete the lower 4 bits of the register 0x0A, set them to the frequency of your choice (see the table below), and then write them back to the register.
0000 seems to inhibit the whole RTC
0001 divides xtal by 128 (PC: 256 ints per second)
0010 divides xtal by 256 (PC: 128 ints per second)
0011 divides xtal by 4 (PC: 8192 ints per second)
0100 divides xtal by 8 (PC: 4096 ints per second)
0101 divides xtal by 16 (PC: 2048 ints per second)
0110 divides xtal by 32 (PC: 1024 ints per second)
0111 divides xtal by 64 (PC: 512 ints per second)
1000 divides xtal by 128 (PC: 256 ints per second)
1001 divides xtal by 256 (PC: 128 ints per second)
1010 divides xtal by 512 (PC: 64 ints per second)
1011 divides xtal by 1024 (PC: 32 ints per second)
1100 divides xtal by 2048 (PC: 16 ints per second)
1101 divides xtal by 4096 (PC: 8 ints per second)
1110 divides xtal by 8192 (PC: 4 ints per second)
1111 divides xtal by 16384 (PC: 2 ints per second)
At the moment, its set to 2 interrupts per second, but you can see what you would need to do to change that. Its all about modifying the lower 4 bits of the register 0x0A.

If you really want to be able to check the time as soon as you have installed the interrupt, you could just set a code up in rtc_install() to wait for it...

Code: Select all

volatile bool interrupt = FALSE;

void rtc_handler(struct regs* r)
{
     // stuff here
     interrupt = TRUE;
}

void rtc_install()
{
    // install stuff here
   while(!interrupt); // pause till interrupt
}

Re: Basic RTC Driver (Real Time Clock) - C

Posted: Sun Jul 06, 2008 9:46 pm
by Brendan
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

Re: Basic RTC Driver (Real Time Clock) - C

Posted: Fri Nov 21, 2008 4:25 am
by jenfoong
Hi all, this is my first post in this board.

I have setup the code as below, similiar to the code above, but I can't received IRQ8 interrupts.

Code: Select all

void rtc_install()
{
	// set update-ended interrupt enable in RTC
	uint8 statusB = inportb(0x0b);
	statusB |= (1 << 4);
	outportb(0x0b, statusB);

	inportb(0x0c);

	irq_install_handler(8, rtc_handler);
}
I read some CMOS references mentioning that I need to unmask the PIC or something. But I need for concrete info about this.
Can any one help? I need RTC to finish my file system implementation.

Re: Basic RTC Driver (Real Time Clock) - C

Posted: Fri Nov 21, 2008 5:44 am
by Combuster
PIC and FAQ

Re: Basic RTC Driver (Real Time Clock) - C

Posted: Fri Nov 21, 2008 6:23 pm
by Troy Martin
Dayum, Combuster beat me to it.

BTW, Uranium, if you still visit, I've managed to remove most of the JPEG artifacts from your rohitab avatar. PM me for it in .png form :P

Re: Basic RTC Driver (Real Time Clock) - C

Posted: Tue Dec 02, 2008 11:45 pm
by leledumbo
Hello, I've implemented the code in Pascal but the returned hour seems wrong on 12.00 and up and the returned day of week is 0..6 instead of 1..7. Here's the code:

Code: Select all

procedure RTCHandler(var r: TRegisters);
begin
  if ReadRegister($0C) and $40<>0 then
    with GlobalTime do
      if IsBCD then begin
        Second:=BCDToBin(ReadRegister($00));
        Minute:=BCDToBin(ReadRegister($02));
        Hour:=BCDToBin(ReadRegister($04));
        Month:=BCDToBin(ReadRegister($08));
        Year:=BCDToBin(ReadRegister($09));
        DayOfWeek:=BCDToBin(ReadRegister($06));
        DayOfMonth:=BCDToBin(ReadRegister($07));
      end else begin
        Second:=ReadRegister($00);
        Minute:=ReadRegister($02);
        Hour:=ReadRegister($04);
        Month:=ReadRegister($08);
        Year:=ReadRegister($09);
        DayOfWeek:=ReadRegister($06);
        DayOfMonth:=ReadRegister($07);
      end;
end;
I've also implemented Brendan version, but it keeps giving 0 for all values.

Code: Select all

procedure RTCHandler(var r: TRegisters);
const
  DaysOnMonth: array [1..12] of Byte = (31,28,31,30,31,30,31,31,30,31,30,31);
  MaxDaysThisMonth: Byte = 0;
begin
  if ReadRegister($0C) and $40<>0 then
    with GlobalTime do
      if ReadRegister($0C) and $10<>0 then begin
        Inc(Second);
        if Second>=60 then begin
          Second:=0;
          Inc(Minute);
          if Minute>=60 then begin
            Minute:=0;
            Inc(Hour);
            if Hour>=60 then begin
              Hour:=0;
              Inc(DayOfMonth);
              if DayOfMonth>=MaxDaysThisMonth then begin
                DayOfMonth:=1;
                Inc(Month);
                if Month>12 then begin
                  Month:=1;
                  Inc(Year);
                end;
                MaxDaysThisMonth:=DaysOnMonth[Month];
                if Month=2 then begin // February - need to check for leap Year
                  if Year mod 400=0 then
                    Inc(MaxDaysThisMonth)
                  else if (Year mod 100<>0) and (Year mod 4=0) then
                    Inc(MaxDaysThisMonth);
                end;
              end;
            end;
          end;
        end;
      end;
end;
The WriteRegister, ReadRegister, and BCDToBin are exactly the same and I guess they're correct because they give the right answer for other values.