An overview of the PC Real Time Clock (RTC)


Introduction

Have you ever thought how the computer is able to display the correct time after you power on the system? There is a component called the RTC in the computer which stores this date and time information when the computer is turned off.

I will talk about how to read/write the date and time and use other features of the RTC using command-line tools, Linux RTC driver and also a low level direct access to the RTC internal registers.

System clock and RTC

Every PC includes a chip called a Real Time Clock (RTC) which is independent of all the other chips and is used to maintain the time even when the system is powered off. The RTC is actually a Motorola 146818 equivalent chip. It is sometimes called the CMOS clock because it comes with a static CMOS memory. The RTC is driven by a battery and maintains the current ‘wall-clock’ time when the computer is powered on or off. Besides the time keeping feature, the RTC can generate different types of interrupts, which is connected to the IRQ 8 line of the Programmable Interrupt Controller (PIC). This means that the interrupts generated by the RTC could be used to interrupt the CPU. For example OS programmer can program the RTC to generate interrupts after a certain time interval and use this interrupt signal to make an OS scheduler, although in most of the OS including Linux use another chip called the Programmable Interval Timer (PIT) for this purpose. In Linux the RTC is used to derive the system time during bootup, (if it does not derive the current time from some other device, for example over the network.)

Access RTC through shell tools

To know what the RTC tells about the time we need a tool : hwclock. To get what is stored in the RTC hardware clock execute as superuser:

hwclock -r 

To set the hardware clock to some date and time you can use:

hwclock --set --date="10/08/89 13:19:00"

When we execute the date command or set up a clock widget on the desktop what they show and modify is the system time maintained by the OS data structures. This is the system clock is a software clock which maintained by the kernel. The system clock maintains the by recording the seconds and microseconds elapsed after the date January 1st 1970 00:00:00 UTC.

There is an interesting tool rtcwake (part of util-linux package) to make your system sleep and wakeup using the RTC. For example you can suspend your system into memory, disk or shutdown your system for a fixed amount of seconds and then automatically get your system restored/reboot. Some examples are given below:

rtcwake -s 120 -m mem

This will suspend the system into memory and automatically restore it after 120 seconds.

rtcwake -s 120 -m disk

Same thing, but suspends into disk.

To test the tool you can just run the following, which will simply wait until the next set alarm arrives from the RTC.

rtcwake -s 120 -m on

For details have a look at the man pages: man rtcwake

These use the alarm interrupt feature of the RTC which I will describe in a moment.

Accessing the RTC through Linux

Linux provides an interface to the realtime clock through the device /dev/rtc with the help of ioctl interface. The set of requests which you can perform on the RTC through the ioctl interface is defined in the linux/rtc.h file, which you can checkout by referring man rtc . I will briefly describe the process to read the RTC date and time with the help of Linux interface.

First we open the RTC device file /dev/rtc and get a valid file descriptor. To read the time we need to pass the file descriptor of the rtc device file to the ioctl’s first parameter. The second parameter is the request which you want to do to the RTC driver. For example to read the time the request is RTC_RD_TIME. The third parameter for this request requires to pass an address to a struct rtc_time object (defined in linux/rtc.h). This will fillup the struct rtc_time structure with the date time values. Similarly, to write the RTC, first initialize a struct rtc_time structure with the appropriate values and then pass it to ioctl with the request RTC_SET_TIME. The struct rtc_time is shown below for convenience:

struct rtc_time {
   int tm_sec;      /* [0-60]  */
   int tm_min;      /* [0-59]  */
   int tm_hour;     /* [0-23]  */
   int tm_mday;     /* [1-31]  */
   int tm_mon;      /* [0-11]  */
   int tm_year;     /* Years since 1900 */
   int tm_wday;     /* unused */
   int tm_yday;     /* unused */
   int tm_isdst;    /* unused */
}

This is identical to the struct tm defined in time.h. The value ranges of the fields of struct rtc_time structure is shown in the above figure 1“. The last three fields are unused by the ioctl call and does not contain valid information.

Note that the year field’s value returned is relative to some fixed year. Most of the cases it is 1900, but it can vary is some systems. To know the exact base value, you need to fetch it using the EPOCH_READ request, storing it in an unsigned long int in the third parameter. Note that this request may not be available in all system, in which case this code assumes to take 1900 as the offset. A sample code to perform these operations and display the read data and time is shown in sourcecode 1:


Click to expand

#include <time.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <linux/rtc.h>
#include <linux/ioctl.h>
#include <unistd.h>

//Static epoch value
#define EPOCH 1900

int
cmos_read_time (struct rtc_time *rtc)
{
  int rtc_fd;
  int epoch, retval;
  
  rtc_fd = open ("/dev/rtc", O_RDONLY);
  if (rtc_fd == -1)
    return 1;

  retval = ioctl(rtc_fd, RTC_RD_TIME, rtc);
  if (retval == -1)
	return 1;
  
  retval = ioctl(rtc_fd, RTC_EPOCH_READ, &epoch);
  if (retval == -1)
  {
	perror ("ERROR");
	printf ("Setting epoch to default\n");
	epoch = EPOCH;
  }
 
  close (rtc_fd);
  return 0;
}


int
cmos_write_time (struct rtc_time *rtc)
{
  int rtc_fd, retval;

  rtc_fd = open ("/dev/rtc", O_RDONLY);
  if (rtc_fd == -1)
    return 1;

  retval = ioctl(rtc_fd, RTC_SET_TIME, rtc);
  if (retval == -1)
	return 1;
  
  close (rtc_fd);
  return 0;
}



int
main (void)
{
  struct rtc_time time;
  int retval;

  char *month_string[] =
  {
    "January"   , "February", "March"    , "April"    ,
    "May"       , "June"    , "July"     , "August"   ,
    "September" , "October" , "November" , "December"
  };
  
  retval = cmos_read_time (&time);
  if (retval != 0)
    {
      perror ("quit");
      exit (1);
    }
  
  printf ("\nDay      : %02d", time.tm_mday);
  printf ("\nMonth    : %02d   : %s", time.tm_mon + 1, month_string[time.tm_mon]);
  printf ("\nYear     : %02d", time.tm_year + 1900);
  printf ("\n");
  printf ("\nHour     : %02d", time.tm_hour);
  printf ("\nMin      : %02d", time.tm_min);
  printf ("\nSec      : %02d", time.tm_sec);
  
  if (retval != 0)
  {
    perror ("quit");
    exit (1);
  }
  printf ("\n");
  return 0;
}

Besides reading and writing date time we can do some other operation defined by the RTC driver. Check out the linux RTC interface manual with man rtc.

RTC Interrupts

As mentioned before, besides time keeping, the RTC can also generate interrupts. I have already briefed how to setup the RTC using the rtcwake tool to generate a signal. This tool uses the RTC’s alarm feature. The RTC can generate three types of interrupts. I will describe int his section how to set and handle these interrupts.

Alarm Interrupt

In this mode the RTC generates an interrupt when a certain time is reached, like an alarm clock. The RTC has separate alarm time storage memory. When the current time matches the alarm time the alarm interrupt triggers. The following code shows how to set alarm.


Click to expand

#include <time.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <linux/rtc.h>
#include <linux/ioctl.h>
#include <unistd.h>
#include <error.h>
#include <errno.h>

int
main (void)
{
  int rtc_fd;
  unsigned long data;
  struct rtc_time alarm;
  int sec;
  
  printf ("\nRing alarm after (sec): ");
  scanf ("%d", &sec);
  if (sec < 0)
  {
    printf ("\nEnter non-negative values");
    return 1;
  }

  rtc_fd = open ("/dev/rtc", O_RDONLY, 0);
  if (rtc_fd == -1)
    {
      error (1, errno, "Cannot open \"/dev/rtc\"");
    }

  ioctl (rtc_fd, RTC_RD_TIME, &alarm);

  printf ("\nCurrent Time: %02d:%02d:%02d",
	  alarm.tm_hour, alarm.tm_min, alarm.tm_sec);
  printf ("\nSetting Alarm After %d Seconds", sec);

  /* Add sec to current time */
  alarm.tm_sec += sec;

  /* Propagate carry to tm_min */
  if (alarm.tm_sec >= 60)
    {
      alarm.tm_min += alarm.tm_sec / 60;
      alarm.tm_sec %= 60;
    }

  /* Propage carry to tm_hour */
  if (alarm.tm_min >= 60)
    {
      alarm.tm_hour += alarm.tm_min / 60;
      alarm.tm_min %= 60;
    }

  /* Wrap tm_hour */
  if (alarm.tm_hour == 24)
    {
      alarm.tm_hour %= 24;
    }

  /* Set the Alarm time */
  printf ("\nSetting Alarm Time To : %02d:%02d:%02d",
	alarm.tm_hour, alarm.tm_min, alarm.tm_sec);
  ioctl (rtc_fd, RTC_ALM_SET, &alarm);


  ioctl (rtc_fd, RTC_ALM_READ, &alarm);
  printf ("\nAlarm Time Set To : %02d:%02d:%02d",
	  alarm.tm_hour, alarm.tm_min, alarm.tm_sec);
	  
  printf ("\nWaiting for Alarm ...");
  fflush (stdout);
  /* Enable alarm */
  ioctl (rtc_fd, RTC_AIE_ON, 0);

  /* Call read on the rtc_fd. This call will block
   * until the alarm interrupt is not triggered
   */
  read (rtc_fd, &data, sizeof (unsigned long));

  /* Disable alarm before exit */
  ioctl (rtc_fd, RTC_AIE_OFF, 0);
  printf (" DONE");

  close (rtc_fd);

  printf ("\n");
  return 0;
}

All these interrupts are actually handled by the Linux kernel and we come to know of it by higher level function, using blocking read calls. If you work with raw hardware without any OS running on it, or in your hobby OS you need to write the RTC interrupt handler to handle these interrupts.

The ioctl requests related to the alarm interrupts are RTC_ALM_READ, RTC_ALM_SET which are the alarm read and set the alarm time respectively. To turn on and off the alarm interrupt the RTC_AIE_ON and RTC_AIE_OFF requests are used respectively.

The code adds seconds value with the current time (read using RTC_RD_TIME request) to generate the alarm time. The alarm time is then set using the following call

ioctl (rtc_fd, RTC_ALM_SET, &alarm);

The alarm is enabled using the RTC_AIE_ON request. Then the code performs a dummy read on the rtc_fd with a dummy variable, which will block until an RTC interrupt is does not occur. Whenever the interrupt is received the read call would return and the program will continue. At the end the alarm interrupt is turned off by the request RTC_AIE_OFF (back to previous configuration). The third parameter of ioctl for the RTC_AIE_ON and RTC_AIE_OFF are ignored.

Now one might wonder if the RTC is alive even when the computer is shut down, then should it send alarm interrupts when the computer is shut down? The answer is yes it would and it would wake the computer automatically. This is a feature to startup the computer at some specific time set in the RTC. To do such what you would need is to set the computer turn on time in the RTC and set the alarm on with the RTC_AIE_ON request and terminate the program. Note that you should not turn off the alarm like the last code. What you need to do is to simply set the time and enable the alarm, and turn off the computer. It would startup automatically as soon as it receives the alarm interrupt.

Note that the alarm time only considers the hour, min, and sec, and ignores the year, month. Once you set the alarm the CPU would receive the alarm once each day.

Update Interrupt

In this the RTC would generate one interrupt per update cycle. Therefore setting this interrupt would get one interrupt per second. The setup of the code sourcecode 3 is similar and is shown below.


Click to expand

#include <time.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <linux/rtc.h>
#include <linux/ioctl.h>
#include <unistd.h>
#include <error.h>
#include <errno.h>

int
main (void)
{
  int rtc_fd;
  unsigned long data;
  int sec;
  
  printf ("\nEnter seconds : ");
  scanf ("%d", &sec);
  if (sec < 0)
  {
    printf ("\nEnter non-negative values");
    return 1;
  }
  
  rtc_fd = open ("/dev/rtc", O_RDONLY, 0);
  if (rtc_fd == -1)
  {
    error (1, errno, "Cannot open \"/dev/rtc\"");
  }
 
  /* Enable update interrupt */
  ioctl (rtc_fd, RTC_UIE_ON, 0);

  /* Iterate and read rtc_fd for 'sec' seconds */
  while (sec)
  {
    printf ("\n%d",sec);fflush (stdout);
    sec--;
    /* This would block for 1 second */
    read (rtc_fd, &data, sizeof (unsigned long));
  }
  
  /* Disable update interrupt before exit */
  ioctl (rtc_fd, RTC_AIE_OFF, 0);

  close (rtc_fd);

  printf ("\n");
  return 0;
}

The ioctl requests related to this interrupt are RTC_UIE_ON and RTC_UIE_OFF. There is no time setting in this case, as the interrupts would generate per update cycle. As before after enabling the update interrupt with the RTC_UIE_ON request I used the read call on the RTC device file descriptor inside a loop. On each iteration the read would block until the end of the update cycle (one sec) and the loop generates n seconds delay. At the end of the loop we disable the update interrupt with the RTC_UIE_OFF request. Note that the third parameter of ioctl in these two requests are ignored.

This is not useful for waking up the CPU, because whenever you would turn off the computer the next second the RTC would generate an interrupt and wake it up. If you are working in raw hardware you can use this feature to run some specific code on the occurrence of this interrupt like a scheduler.

Periodic Interrupt

In this mode you can set the RTC to generate interrupts in a certain frequency ranging from 2 interrupts per second and 8192 interrupts per second. The frequency you set should have values which are a power of two. In this case you can read and set the frequency of the interrupt, with the RTC_IRQP_READ and RTC_IRQP_SET requests respectively. The RTC_PIE_ON and RTC_PIE_OFF are used to enable and disable these interrupts respectively. A code example describes this below.


Click to expand

#include <time.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <linux/rtc.h>
#include <linux/ioctl.h>
#include <unistd.h>
#include <error.h>
#include <errno.h>

int
main (void)
{
  int rtc_fd, retval;
  int freq, freq_bak, sec;
  unsigned long data;
  
  
  
  rtc_fd = open ("/dev/rtc", O_RDONLY, 0);
  if (rtc_fd == -1)
  {
    error (1, errno, "Cannot open \"/dev/rtc\"");
  }
 
  ioctl (rtc_fd, RTC_IRQP_READ, &freq_bak);
  printf ("\nFrequency read : %d", freq_bak);
  
  printf ("\nEnter frequency (power of 2): ");
  scanf ("%d", &freq);

  printf ("\nSetting frequency to : %d", freq);
  retval = ioctl (rtc_fd, RTC_IRQP_SET, freq);
  if (retval == -1)
	perror ("ERROR");
  
  /* Enable update interrupt */
  ioctl (rtc_fd, RTC_PIE_ON, 0);
  printf ("\n");
  /* Iterate and read rtc_fd for 'sec' seconds */
  sec = 10;
  while (sec)
  {
    printf ("\n%d",sec);
    sec--;
    /* This would block for 1 second */
    read (rtc_fd, &data, sizeof (unsigned long));
  }
  
  /* Disable update interrupt before exit */
  ioctl (rtc_fd, RTC_PIE_OFF, 0);

  printf ("\nRestoring backup frequency %d", freq_bak);
  ioctl (rtc_fd, RTC_IRQP_SET, freq_bak);
  
  close (rtc_fd);

  printf ("\n");
  return 0;
}

The code reads the current frequency, backs it up, and sets the frequency entered through keyboard as current frequency. Then it iterates in the loop and reads the RTC file descriptor in each iteration. Each read operation would block the call for the the time period inverse of the current frequency. Try entering different frequencies and notice the the speed at which the dots are printed.

Monitor the Changes

There is a file /proc/driver/rtc which holds the various attributes values and flags of the RTC. Just do cat /proc/driver/rtc to have a look. To see how the above codes, and also codes in the following sections modify these attributes repeatedly use the “cat” command to see the file contents change as you execute the programs.

watch -n0.5 cat /proc/driver/rtc

This will execute the cat periodically. The period can be set by the -n switch. Default is 2 seconds, in the above example it is set to 0.5 seconds. Press Ctrl + C to quit monitoring.

Going Lower

We read the date and time data successfully from the RTC using the Linux RTC interface. In this section I will take you a level lower and show how to access and do the same above operations by accessing the RTC device directly through the IO port. What’s the use? If you are a hobby OS programmer then this can be handy for you and definitely interesting.

The RTC port and memory mapping

To communicate with the RTC we need to know the external port mappings of the RTC as well as the internal register mappings. There are two ports which are used to talk to the RTC. The address port 0x70 and the data port 0x71. There are many internal registers associated with the RTC (the CMOS memory) some are read-only and some are read-write. To read or write data from the RTC, what you need to know is which internal register in the RTC memory you want to access. Each register has an address (shown in the tables). I will discuss the communication process with the RTC in the next section briefly. The port mapping and the internal register addresses are as in the table 1, table 2, and table 3. I will refer these tables in the following sections.

[include table1 port mapping]
+-------+------------------+
| Port  |      Use         |
+-------+------------------+
| 0x70  | RTC Address Port |
+-------+------------------+
| 0x71  | RTC Data Port    |
+-------+------------------+

[include table2 port mapping]
Table 2 Mapping of RTC internal registers

+-------------------------+----------------------------------+
| RTC internal address    |          Value Stored            |
+-------------------------+----------------------------------+
| 0x00                    |       Current Time Seconds       |
+-------------------------+----------------------------------+
| 0x01                    |          Alarm Seconds           |
+-------------------------+----------------------------------+
| 0x02                    |       Current Time Minutes       |
+-------------------------+----------------------------------+
| 0x03                    |          Alarm Minutes           |
+-------------------------+----------------------------------+
| 0x04                    |        Current Time Hours        |
+-------------------------+----------------------------------+
| 0x05                    |           Alarm Hours            |
+-------------------------+----------------------------------+
| 0x06                    |      Day of Week (Sunday = 1)    |
+-------------------------+----------------------------------+
| 0x07                    |          Date of Month           |
+-------------------------+----------------------------------+
| 0x08                    |              Month               |
+-------------------------+----------------------------------+
| 0x09                    |     Year (The last two digits)   |
+-------------------------+----------------------------------+
| 0x32                    |         Current Century          |
|                         |    (The first two year digits)   |
+-------------------------+----------------------------------+
| 0x0a                    |        Status Register A         |
+-------------------------+----------------------------------+
| 0x0b                    |        Status Register B         |
+-------------------------+----------------------------------+

[include table3 bit interpretations Register B]
Table 3. The bit interpretations of Status Register B

+--------------+---------------------------------------------------+
|                                Status Register B                 |
+--------------+---------------------------------------------------+
| Bit Position |               Interpretation                      |
+--------------+---------------------------------------------------+
|  7           |            Enable Cycle Update                    |
+--------------+---------------------------------------------------+
|  6           |         Enable Periodic Interrupt                 |
+--------------+---------------------------------------------------+
|  5           |          Enable Alarm Interrupt                   |
+--------------+---------------------------------------------------+
|  4           |        Enable Update-End Interrupt                |
+--------------+---------------------------------------------------+
|  3           |         Enable Square Wave Output                 |
+--------------+---------------------------------------------------+
|  2           |      Data Mode : 0 – BCD  /  1 – Binary           |
+--------------+---------------------------------------------------+
|  1           | 12/24 Hour Mode : 0 – 12 Hour /  1 – 24 Hour      |
+--------------+---------------------------------------------------+
|  0           | Day light saving enabled : 1 enabled / 0 disabled |
+--------------+---------------------------------------------------+

The bit7 of the status register A is the UIP bit denotes if an update cycle is in progress. This is the only relevant bit therefore I won’t mention other bits.

Time and Related bits

Table 1 shows the mapping of the real time date and time register mappings, alarm register mappings, and the status register mappings (discussed later). All the data stored in the locations are either in BCD (Binary Coded Decimal) format or in Binary. The Hour byte could be in 24 hour format or a 12 hour format. To know the format (BCD/Bin or 12hrs/24hrs) in which the RTC stores the information we need to access appropriate bits of Status Register B. The year value is divided and stored in two locations. The century byte (RTC address 0x32), and the year byte (RTC address 0x09) which actually stores the decade.

The Status Register B is of length 1-byte (8-bit byte). The interpretation of the bits of this register is shown in table 3.

If the bit 2 is set (1) then it means that the data are stored in Binary, if it is cleared (0) then the data is stored in BCD format. This bit read-only and is written by the RTC processor to indicate the storage format, and you should read it to know the format in which the date and time is stored.

If bit 1 is set (1) that means the data stored in the hour byte in the RTC (reg 0x04) is in 24 hours format, and if bit 1 is cleared (0) then it means that the stored data is in 12 hour format (AM/PM). If it is 12 hour format then the MSB (most significant bit) of the hour byte (reg 0x04) will denote AM if 0 and will denote PM if 1. This is a read/write bit which can be changed by software.

The time information is updated every second. If we read in the middle of the time update then the data could be inconsistent. We can check the Bit7 in the status register A to know if the time update cycle is on. The bit7 of the Register B (SET bit) is interesting in this matter. When this bit is set to 0 RTC is going through a normal update cycle, that is, it’s advancing counts once per second. Writing time into the RTC during such an update can occur in the middle of a cycle update, which is undesired. If this bit is set to 1 then any update cycle in progress is aborted after which we can write the date-time to the RTC without an update occurring in the middle of the write operation. We can turn the bit to 0 back again after the write for normal cycle update.

Communicating (Read/Write)

Communication with the RTC is simply reading/writing from/to the RTC’s different registers. The previous section describes the IO port mapping of the RTC and the internal register mappings and the status register bit interpretations.

Reading from or writing to the RTC needs two steps. First you need to make the RTC know the register of the location from where you want to perform a read or a write. Next you need to either read or write that location. To do this first we send the address of the RTC internal register to be read or written to the 0x70 port. Next read/write from the 0x71 port will read/write data from/to the selected register. This operation is outlined below

  1. Send the address of the register to be read/written to port 0x70, this selects the register
  2. Read/Write data on the selected register (any ONE)
    1. Read from port 0x71 to get byte in the selected register.
    2. Write a byte into port 0x71 . This will overwrite the value in the selected register with the value you send.

To read from the ports we can use the sys/io.h functions (function macros actually). To read from a port the function inb is used and to write a byte to a port outb is used. inb takes one parameter, the port address from which it reads and returns the read data byte. The outb function has two arguments, the first one specifies the value to be written and the second argument tells the port number to be written into. For example

val = inb (0x71) 

will read from the port 0x71 and we get the value at the port 0x71 in the variable val.

outb (val, 0x70)

will write the value stored in val into the port 0x70.

Note that when you store any data it should be in the proper format (binary/BCD, 12hrs/24hrs) indicated by the data mode bit and the hour mode bit in the status register B.

Some things to be noted are: once you write an address to the port 0x70 then you should do some read or write from the port 0x71 else doing something else can result in unexpected behaviour. After we send the address to the port 0x70 it is a good practice to wait for some time to latch the address at the output and make the data available at port 0x71, which can be done by making dummy read operation. A sleep is unnecessary in this case. A very common use is to read from port 0x80 which is related to POST codes during the system bootup therefore is safe for dummy read/writes.

I have set (1) the bit7 of the Register B, Before we write the date time information to stop the update cycle, as discussed previously, and after writing, clear it back to 0. The bit7 of status register A tells that if the time update is in progress. You can test before reading, to know if the currently read data is valid (not being modified).

Alarms

We have already seen how to set alarms through tools and Linux rtc interface. The alarm bits are stored in the Status Register B. We need to set the alarm bit in this register which we want to enable, and keep the others disabled.

For the update interrupt we need to set the bit7 of status register B. For the alarm interrupt we need to, set the alarm time (hour, minute, second) in the alarm time registers table 2, and set the bit5. For the periodic interrupt we need to set the bit6 of the status register B, and to specify the period we need to set the bit 0-6 s of the status register A.
Note that when setting the alarm time only the hour, minute and second fields are valid.

Permissions

Sufficient permission is required to access the ports directly when we are working in Linux (or other OS which disallow direct access to certain memory range). To enable access to the required I/O ports to communicate with the RTC we need the ioperm function to grant access to our required port addresses 0x70 and 0x71 as follows

ioperm (0x70, 0x11, 3);

The above call act upon the ports 0x70 to 0x80 (0x10 bytes starting from 0x70) and will set the permission to level 3, which will allow us to access those ports directly. The port 0x80 is given permission as we will make dummy writes into that port. You can instead perform two calls one granting level 3 access to 0x70 and 0x71 and another call granting access to 0x80, so other ports in between remains protected. The program need to be run in superuser to change the privilege level to 3. Privilege levels are set back to normal after required processing.

Implementation

Now I show a code to test the discussed features in practical. To store the time data in the program I have used the below structure, a very similar one to the struct rtc_time or the struct tm.


Click to expand

#include <stdio.h>
#include <stdlib.h>
#include <sys/io.h>

#define RTC_ADDR_PORT             0x70
#define RTC_DATA_PORT             0x71

#define RTC_TIME_SEC              0x00
#define RTC_ALRM_SEC              0x01
#define RTC_TIME_MIN              0x02
#define RTC_ALRM_MIN              0x03
#define RTC_TIME_HOUR             0x04
#define RTC_ALRM_HOUR             0x05
#define RTC_DATE_DAY_OF_WEEK      0x06
#define RTC_DATE_DAY_OF_MONTH     0x07
#define RTC_DATE_MONTH            0x08
#define RTC_DATE_YEAR_OF_CENTURY  0x09
#define RTC_DATE_CENTURY          0x32

#define RTC_REG_A                 0x0a
#define RTC_REG_B                 0x0b

#define RTC_REG_B_HOURMODE        0x02
#define RTC_REG_B_DATAMODE        0x04

#define RTC_REG_A_UDIP            0x80
#define RTC_REG_B_SET             0x80

#define RTC_INTR_PERD 0x40
#define RTC_INTR_ALRM 0x20
#define RTC_INTR_UPDT 0x10

#define BCD_TO_BIN(bcd) ((((bcd) >> 4) & 0x0f) * 10 + ((bcd) & 0x0f))
#define BIN_TO_BCD(bin) ((((bin)/10) << 4) | (((bin)%10) & 0x0f))


#define GRANT_IO_ACCESS  if (ioperm (0x70, 0x11, 3) != 0) \
                           return 1;
#define REVOKE_IO_ACCESS if (ioperm (0x70, 0x11, 0) != 0) \
                           return 2;

typedef struct _time_struct
{
  int hour;                     /* Hour in  24 hour format */
  int min;                      /* Minuites */
  int sec;                      /* Seconds */
  int mon;                      /* Month */
  int date;                     /* Date of Month */
  int wday;                     /* Weekday with Sunday = 1 */
  int year;                     /* Full year with the decade and century */
} time_struct_t;

unsigned char
rtc_read (unsigned char addr)
{
  unsigned char val;
  outb (0x80 | addr, RTC_ADDR_PORT);    /* write the address 'addr' to be read from */
  inb (0x80);                   /* wait for some time */
  val = inb (RTC_DATA_PORT);    /* read the data stored in the address 'addr' */
  return val;
}

void
rtc_write (unsigned char addr, unsigned char data)
{
  outb (0x80 | addr, RTC_ADDR_PORT);    /* write the address 'addr' to be read from */
  inb (0x80);                   /* wait for some time */
  outb (data, RTC_DATA_PORT);   /* read the data stored in the address 'addr' */
}

int
rtc_read_intr_flag (unsigned char flag)
{
  unsigned char val;

  GRANT_IO_ACCESS;

  val = rtc_read (RTC_REG_B);

  REVOKE_IO_ACCESS;

  return ((val & flag) == flag);
}

int
rtc_set_intr_flag (unsigned char flag)
{
  unsigned char val;

  GRANT_IO_ACCESS;

  val = rtc_read (RTC_REG_B);
  val = val | flag;
  rtc_write (RTC_REG_B, val);

  REVOKE_IO_ACCESS;
  
  return 1;
}

int
rtc_clear_intr_flag (unsigned char flag)
{
  unsigned char val;

  GRANT_IO_ACCESS;

  val = rtc_read (RTC_REG_B);
  val = val & ~flag;
  rtc_write (RTC_REG_B, val);

  REVOKE_IO_ACCESS;
  
  return 1;
}

int
rtc_stop_update (void)
{
  unsigned char val;

  GRANT_IO_ACCESS;

  val = rtc_read (RTC_REG_B);
  val = val | RTC_REG_B_SET;
  rtc_write (RTC_REG_B, val);

  REVOKE_IO_ACCESS;
  
  return 1;
}

int
rtc_resume_update (void)
{
  unsigned char val;

  GRANT_IO_ACCESS;

  val = rtc_read (RTC_REG_B);
  val = val & ~RTC_REG_B_SET;
  rtc_write (RTC_REG_B, val);

  REVOKE_IO_ACCESS;
  
  return 1;
}

int
rtc_read_time (time_struct_t * time)
{
  unsigned char hh, mm, ss, old_hh, old_mm, old_ss;
  unsigned char wday, day, month, year_of_century, century, old_day, old_month, old_year_of_century, old_century;
  unsigned char status_reg_b;
  int flag;

  GRANT_IO_ACCESS;
  status_reg_b = rtc_read (RTC_REG_B);

	
  while (rtc_read (RTC_REG_A) & RTC_REG_A_UDIP)
	;
  
  ss = rtc_read (RTC_TIME_SEC);
  mm = rtc_read (RTC_TIME_MIN);
  hh = rtc_read (RTC_TIME_HOUR);
  wday = rtc_read (RTC_DATE_DAY_OF_WEEK);
  day = rtc_read (RTC_DATE_DAY_OF_MONTH);
  month = rtc_read (RTC_DATE_MONTH);
  year_of_century = rtc_read (RTC_DATE_YEAR_OF_CENTURY);
  century = rtc_read (RTC_DATE_CENTURY);
  
  do
  {
    old_ss = ss;
    old_mm = mm;
    old_hh = hh;
    old_day = day;
    old_month = month;
    old_year_of_century = year_of_century;
    old_century = century;
	
    while (rtc_read (RTC_REG_A) & RTC_REG_A_UDIP)
    	;
	
	ss = rtc_read (RTC_TIME_SEC);
	mm = rtc_read (RTC_TIME_MIN);
	hh = rtc_read (RTC_TIME_HOUR);
	wday = rtc_read (RTC_DATE_DAY_OF_WEEK);
	day = rtc_read (RTC_DATE_DAY_OF_MONTH);
	month = rtc_read (RTC_DATE_MONTH);
	year_of_century = rtc_read (RTC_DATE_YEAR_OF_CENTURY);
	century = rtc_read (RTC_DATE_CENTURY);
	
  } while ((old_ss == ss) && (old_mm == mm) && (old_hh == hh) && (old_day == day) && (old_month == month) && (old_year_of_century == year_of_century) && (old_century == century));  
  
  REVOKE_IO_ACCESS;

  

  flag = status_reg_b & RTC_REG_B_DATAMODE;
	/* NOTE: We will be saving the time in 24 hours mode */
	/* In 24 hour mode */
	if (status_reg_b & RTC_REG_B_HOURMODE)
	  time->hour = flag ? hh : BCD_TO_BIN (hh);
	/* In 12 hour mode, mask the hours and AM, PM */
	else
	  {
		time->hour = flag ? hh & 0x7f: BCD_TO_BIN (hh & 0x7f);
		/* If PM then add 12 to make 24 hour format */
		if (hh & 0x80)
		  time->hour += 12;
	  }
	time->min = flag ? mm : BCD_TO_BIN (mm);
	time->sec = flag ? ss : BCD_TO_BIN (ss);
	time->mon = flag ? month : BCD_TO_BIN (month);
	time->date = flag ? day : BCD_TO_BIN (day);
	time->wday = flag ? wday : BCD_TO_BIN (wday);
	time->year = flag ? century : BCD_TO_BIN (century);
	time->year = (time->year * 100) + (flag ? year_of_century : BCD_TO_BIN (year_of_century));

  return 0;
}


int
rtc_write_time (time_struct_t * time)
{
  unsigned char hh, mm, ss;
  unsigned char wday, day, month, year_of_century, century;
  unsigned char status_reg_b;
  int flag;

  GRANT_IO_ACCESS;

  status_reg_b = rtc_read (RTC_REG_B);

  REVOKE_IO_ACCESS;

  /* If data stored is in Binary */
  flag = status_reg_b & RTC_REG_B_DATAMODE;

  /* NOTE: We will be saving the time in 24 hours mode */
  /* In 24 hour mode is set */
  if (status_reg_b & RTC_REG_B_HOURMODE)
	hh = flag ? time->hour : BIN_TO_BCD (time->hour);
  /* In 12 hour mode, mask the hours and AM, PM */
  else
	{
	  /* Take first 7 bits */
	  hh = flag ? time->hour % 12 : BIN_TO_BCD (time->hour % 12);
	  /* If PM then add 12 to make 24 hour format depending on 8th bit */
	  if (flag)
		hh |= (time->hour / 12) == 1 ? 0x80 : 0x00;
	  else
		hh |= (BIN_TO_BCD (time->hour / 12)) == 1 ? 0x80 : 0x00;
	}
  mm = flag ? time->min : BIN_TO_BCD (time->min);
  ss = flag ? time->sec : BIN_TO_BCD (time->sec);
  month = flag ? time->mon : BIN_TO_BCD (time->mon);
  day = flag ? time->date : BIN_TO_BCD (time->date);
  wday = flag ? time->wday : BIN_TO_BCD (time->wday);
  century = flag ? time->year / 100 : BIN_TO_BCD (time->year / 100);
  year_of_century = flag ? time->year % 100 : BIN_TO_BCD (time->year % 100);

  rtc_stop_update ();
  GRANT_IO_ACCESS;


  /* Now data is valid */
  rtc_write (RTC_TIME_SEC, ss);
  rtc_write (RTC_TIME_MIN, mm);
  rtc_write (RTC_TIME_HOUR, hh);
  rtc_write (RTC_DATE_DAY_OF_WEEK, wday);
  rtc_write (RTC_DATE_DAY_OF_MONTH, day);
  rtc_write (RTC_DATE_MONTH, month);
  rtc_write (RTC_DATE_YEAR_OF_CENTURY, year_of_century);
  rtc_write (RTC_DATE_CENTURY, century);

  REVOKE_IO_ACCESS;
  rtc_resume_update ();

  return 0;
}

int
rtc_write_alarm (time_struct_t * time)
{
  unsigned char hh, mm, ss;
  unsigned char status_reg_b;
  int flag;

  GRANT_IO_ACCESS;

  status_reg_b = rtc_read (RTC_REG_B);

  REVOKE_IO_ACCESS;

  /* If data stored is in Binary */
  flag = status_reg_b & RTC_REG_B_DATAMODE;
  
  /* NOTE: We will be saving the time in 24 hours mode */
  /* In 24 hour mode is set */
  if (status_reg_b & RTC_REG_B_HOURMODE)
	hh = flag ? time->hour : BIN_TO_BCD (time->hour);
  /* In 12 hour mode, mask the hours and AM, PM */
  else
	{
	  /* Take first 7 bits */
	  hh = flag ? time->hour % 12 : BIN_TO_BCD (time->hour % 12);
	  /* If PM then add 12 to make 24 hour format depending on 8th bit */
	  if (flag)
		hh |= (time->hour / 12) == 1 ? 0x80 : 0x00;
	  else
		hh |= (BIN_TO_BCD (time->hour / 12)) == 1 ? 0x80 : 0x00;
	}
  mm = flag ? time->min : BIN_TO_BCD (time->min);
  ss = flag ? time->sec : BIN_TO_BCD (time->sec);

  GRANT_IO_ACCESS;

  /* Set alarm */
  rtc_write (RTC_ALRM_SEC, ss);
  rtc_write (RTC_ALRM_MIN, mm);
  rtc_write (RTC_ALRM_HOUR, hh);

  REVOKE_IO_ACCESS;

  return 0;
}

int
rtc_read_alarm (time_struct_t * time)
{
  unsigned char hh, mm, ss;
  unsigned char status_reg_b;
  int flag;

  GRANT_IO_ACCESS;

  status_reg_b = rtc_read (RTC_REG_B);

  ss = rtc_read (RTC_ALRM_SEC);
  mm = rtc_read (RTC_ALRM_MIN);
  hh = rtc_read (RTC_ALRM_HOUR);
  
  REVOKE_IO_ACCESS;

  /* If data stored is in Binary */
  flag = status_reg_b & RTC_REG_B_DATAMODE;
  
  /* NOTE: We will be saving the time in 24 hours mode */
  /* In 24 hour mode is set */
  if (status_reg_b & RTC_REG_B_HOURMODE)
	time->hour = flag ? hh : BCD_TO_BIN (hh);
  /* In 12 hour mode, mask the hours and AM, PM */
  else
	{
	  /* Take first 7 bits */
	  time->hour = flag ? hh % 12 : BCD_TO_BIN (hh % 12);
	  /* If PM then add 12 to make 24 hour format depending on 8th bit */
	  if (flag)
		time->hour |= (hh / 12) == 1 ? 0x80 : 0x00;
	  else
		time->hour |= (BCD_TO_BIN (hh / 12)) == 1 ? 0x80 : 0x00;
	}
  time->min = flag ? mm : BCD_TO_BIN (mm);
  time->sec = flag ? ss : BCD_TO_BIN (ss);


  return 0;
}


int
main (void)
{
  time_struct_t time, alarm;
  int retval;

  char *month_string[] = {
    "", "January", "February", "March", "April",
    "May", "June", "July", "August",
    "September", "October", "November", "December"
  };


  retval = rtc_read_alarm (&time);
  if (retval != 0)
    {
      perror ("quit\n");
      exit (1);
    }

  rtc_read_time (&time);
  printf ("Current Time:\n");
  printf ("Day      : %02d\n", time.date);
  printf ("Month    : %02d   : %s\n", time.mon, month_string[time.mon]);
  printf ("Year     : %02d\n", time.year);
  printf ("\n");
  printf ("Hour     : %02d\n", time.hour);
  printf ("Min      : %02d\n", time.min);
  printf ("Sec      : %02d\n", time.sec);

  rtc_clear_intr_flag (RTC_INTR_ALRM);
  printf ("\n");
  if (rtc_read_intr_flag (RTC_INTR_PERD))
    {
      printf ("PERIODIC Interrupt Enabled\n");
    }
  else
    {
      printf ("PERIODIC Interrupt Disabled\n");
    }
  if (rtc_read_intr_flag (RTC_INTR_ALRM))
    {
      printf ("ALARM Interrupt Enabled\n");
    }
  else
    {
      printf ("ALARM Interrupt Disabled\n");
    }
  if (rtc_read_intr_flag (RTC_INTR_UPDT))
    {
      printf ("UPDATE Interrupt Enabled\n");
    }
  else
    {
      printf ("UPDATE Interrupt Disabled\n");
    }
  printf ("\n");
  
  /* Set alarm 120 sec advance to the current time. Turn off computer. After
   * alarm goes off, computer boots automatically.
   */
  printf ("Previous Alarm Time Was:\n");
  rtc_read_time (&alarm);
  printf ("Hour     : %02d\n", alarm.hour);
  printf ("Min      : %02d\n", alarm.min);
  printf ("Sec      : %02d\n", alarm.sec);
  
  /* Add sec to current time */
  alarm.sec += 120;

  /* Propagate carry to tm_min */
  if (alarm.sec >= 60)
    {
      alarm.min += alarm.sec / 60;
      alarm.sec %= 60;
    }

  /* Propage carry to tm_hour */
  if (alarm.min >= 60)
    {
      alarm.hour += alarm.min / 60;
      alarm.min %= 60;
    }

  /* Wrap tm_hour */
  if (alarm.hour == 24)
    {
      alarm.hour %= 24;
    }
    
  rtc_write_alarm (&alarm);
  rtc_set_intr_flag (RTC_INTR_ALRM);
  rtc_read_alarm (&alarm);
  
  printf ("\nAlarm Time Set To\n");
  printf ("Hour     : %02d\n", alarm.hour);
  printf ("Min      : %02d\n", alarm.min);
  printf ("Sec      : %02d\n", alarm.sec);
  
  printf ("\n");
  return 0;
}

The main function reads the time shows it, shows alarm status, adds 120 seconds to current time and sets it as alarm time. You can suspend the system to ram, or power off the system, and after 120 seconds the system will automatically come up. Other functions are not used.

The two core functions implemented are rtc_read (unsigned char addr) which reads the register of the RTC memory denoted by addr and rtc_write (unsigned char addr, unsigned char data) writes the data to the register denoted by addr.

These functions operate as exactly as I have described in the communication section. These functions requires I/O privileged level 3. These are used by the other functions to perform I/O with the RTC.

Each port and register mappings are hash defined with a name each having a prefix RTC_. Two function macros BCD_TO_BIN () and BIN_TO_BCD () are defined to help conversion between binary and BCD.

The function rtc_read_time (time_struct_t *time) reads the time information from the RTC and fills the components of passed structure pointer. The date/time reading is done in the following process. At first we wait till an update cycle is in progress by testing the register A’s UIP bit. When it is not in the update cycle, the required data is read and backed up. Then again we wait for an update cycle to end and re-read the data and time values. This is because when we were reading the date and time, an update cycle may have been in progress, resulting incorrectly read values. If the newly read values of mins and secs are same as the backed up values from the previous read, then we can say that no cycle update occurred while we were reading the values and thus the read values are consistent, else we again read and continue the process while two reads does not fetch the same values. The update cycle is not disabled, as disabling the update cycle may drift the time while reading. time_struct_t will store hours in 24 hours mode and a conversion is performed by checking the hour mode bit. The year decade and the century bytes of the year are pasted together. Note that 1900 is used as the offset to compute the year.

Similarly the function rtc_write_time (time_struct_t *time) reads the appropriate data format, BCD/Binary, converts the data in appropriate mode and then writes the time components to corresponding registers. Before and after writing the values the update cycle is stopped using rtc_stop_update () and resumed using rtc_resume_update () respectively. On success this returns 0 or a non zero otherwise. In this case we stop the udpate cycle because we are going to set the date and time and not read, therefore when the update cycle will be started it has a new value to work with.

The two functions rtc_write_alarm (time_struct_t *time) and rtc_read_alarm (time_struct_t *time) which are similar to the above two functions, but these functions read and set the alarm bytes in the RTC. When reading or writing alarm the the update cycle is not checked or stopped, as these bytes does not take part in the update cycle.

The three functions rtc_read_intr_flag (unsigned char flag), rtc_set_intr_flag (unsigned char flag) and rtc_clear_intr_flag (unsigned char flag) which reads, sets and clears the interrupt bits in the status register B. The argument to these functions define which flag to set or clear. The flags have macro names starting with RTC_INTR_.

I have granted access the required ports to level 3 before each code block doing read or write to the RTC and lowered access to level 0 when that block is finished. You can instead grant level 3 access to the ports at the beginning of the program, do all the works, and revoke access just before termination.

A note about the weekday byte. Here is what the OSDev Wiki has to say about it:

The RTC chip is able to keep track of the current day of the week. All it does is increment its “Weekday” register at midnight and reset it to zero if it reaches 7. Unfortunately there is no guarantee that this register was ever set correctly by anything (including when the user changes the time and date using the BIOS configuration screen). It is entirely unreliable and should not be used.

Instead it is recommended to set figure out the day of week from the date. See Zeller’s Congruence link in reference .

Execute this the code with superuser privilege.

Conclusion

This article describes and outlines how you can access the RTC using the command-line tools, the Linux interface and directly through the I/O ports. Using these descriptions you can use the interrupt services and access the time in RTC in your shell script, C code, or write a RTC driver in your hobby OS or a RTC driver for linux yourself. One cool use of the RTC can be the alarm feature. You can set an alarm after which you want the system to shut down or be suspended, before suspending you can set another alarm after which the system should start/resume automatically. This can be handy timing your downloads and also save power by turning off or suspending your system.

References

  1. http://www.intel.com/support/motherboards/desktop/sb/CS-025434.htm
  2. http://doc.chipfind.ru/motorola/mc146818a.htm
  3. http://www.kernel.org/doc/Documentation/rtc.txt
  4. http://wiki.osdev.org/CMOS#Weekday_Register
  5. http://tldp.org/HOWTO/Clock.html
  6. http://en.wikipedia.org/wiki/Zeller%27s_congruence
  7. man rtc

About phoxis

Homo-sapiens
This entry was posted in Computer Science, Linux Programming, Overview and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s