Basic RTC Driver (Real Time Clock) - C

Question about which tools to use, bugs, the best way to implement a function, etc should go here. Don't forget to see if your question is answered in the wiki first! When in doubt post here.
Post Reply
Uranium
Posts: 16
Joined: Fri Jul 04, 2008 8:00 am

Basic RTC Driver (Real Time Clock) - C

Post 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
User avatar
lukem95
Member
Member
Posts: 536
Joined: Fri Aug 03, 2007 6:03 am
Location: Cambridge, UK

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

Post 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.
~ Lukem95 [ Cake ]
Release: 0.08b
Image
Uranium
Posts: 16
Joined: Fri Jul 04, 2008 8:00 am

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

Post by Uranium »

Rohitab.com, yeh its a nice community is rohitab.

Napalm: http://www.rohitab.com/discuss/index.php?showuser=3860
sylvarant
Posts: 3
Joined: Sun May 11, 2008 1:34 am
Location: Zwevegem,Belgium

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

Post 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 :?:
Uranium
Posts: 16
Joined: Fri Jul 04, 2008 8:00 am

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

Post 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
}
User avatar
Brendan
Member
Member
Posts: 8561
Joined: Sat Jan 15, 2005 12:00 am
Location: At his keyboard!
Contact:

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

Post 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
For all things; perfection is, and will always remain, impossible to achieve in practice. However; by striving for perfection we create things that are as perfect as practically possible. Let the pursuit of perfection be our guide.
jenfoong
Posts: 2
Joined: Sat Oct 25, 2008 10:26 pm

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

Post 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.
User avatar
Combuster
Member
Member
Posts: 9301
Joined: Wed Oct 18, 2006 3:45 am
Libera.chat IRC: [com]buster
Location: On the balcony, where I can actually keep 1½m distance
Contact:

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

Post by Combuster »

PIC and FAQ
"Certainly avoid yourself. He is a newbie and might not realize it. You'll hate his code deeply a few years down the road." - Sortie
[ My OS ] [ VDisk/SFS ]
User avatar
Troy Martin
Member
Member
Posts: 1686
Joined: Fri Apr 18, 2008 4:40 pm
Location: Langley, Vancouver, BC, Canada
Contact:

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

Post 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
Image
Image
Solar wrote:It keeps stunning me how friendly we - as a community - are towards people who start programming "their first OS" who don't even have a solid understanding of pointers, their compiler, or how a OS is structured.
I wish I could add more tex
leledumbo
Member
Member
Posts: 103
Joined: Wed Apr 23, 2008 8:46 pm

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

Post 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.
Post Reply