BIOSを使わないで時刻を取得するぞ!

  • (by K, 2006.04.01)
  • 発展課題のページ(全部読む前にここを読むのは混乱するのでおすすめじゃないです)

やりかた(基本)

  • まず以下のような関数を考えます。これはCMOSメモリと呼ばれる領域を読み込むための関数です。CMOSメモリには主にBIOSの設定情報が記録されていますが、ここにRTC(リアルタイムクロック)の情報もあります。
    int readcmos(unsigned char addr)
    {
        io_out8(0x70, addr);
        return io_in8(0x71);
    }
  • そしてCMOSメモリの以下の領域は、RTCになっています。RTC部分はRTC回路によって自動的に更新されています。
    addr内容
    0x00
    0x02
    0x04
    0x07
    0x08
    0x09年の下2桁
    0x32年の上2桁
  • ここで注意があります。このRTCの情報は16進数で表示すると普通に見えるようになっています。・・・どういうことかというと、たとえば秒の値が0x12の場合、これは12秒を意味しているのであって、18秒を意味しているのではありません。このような表現方法をBCDといいます。以上のすべての内容はBCDで表現されていますので、取り扱いには注意してください。
    • BCDの数値を表示するだけなら、sprintfで%02Xを使えばいいでしょう。
    • BCDの数値の通常の数値に変換したいのなら、 y = (x >> 4) * 10 + (x & 0x0f); などとするといいでしょう。

まともに使うには

  • これだけでうまくいきそうに思えるかもしれませんが、実はダメです。プログラムが読み出し中にRTCの書き換えが起きることがときどきあって、それに遭遇すると、「12:34:59」の次が「12:34:00」になってしまったりします。こういう場合はもう一度読むと「12:35:00」という正しい値が読み出せます。
  • この書き換え中の読み出しによる問題を避けるためには、「2度読み」というテクニックを使います。つまり上記7バイトを読んで変数に記録しておき、さらにもう一回上記7バイトを読んで先ほど読んだ値と同じかどうかを確認するのです。同じ値が読めればOKですが、値が違えばどちらかの読み出し結果が更新中のものだった可能性があるので、もう一度やり直します。
    • 2度読みの代わりに、1度しか読まずに前回取得の時刻と比較して、値が明らかにおかしければ読み込みなおすという方法もあります。これはかなり頭の良い方法といえますが、現在時刻の予測値が必要で、予測値があるのならそもそもRTCを読む必要があるのか少々疑問です(RTCはそれほど高速な回路ではないので、RTCを使わないで済むならそのほうが処理が高速になる)。まあOSが管理している内部時計の調節のためになら、2度読み以外の方法も使えるかもしれません。
  • しかも問題はまだあります。機種によってはときどきとんでもない値が2度連続で出てきます。たとえば秒が0xffなどです。そんな場合はもう一度読み直すべきです。2度読みによって取得できた秒や分が0x59以下であるかどうかとか(秒に関しては0x60もごくまれにありえますが:うるう秒のときに)、1のくらいが9以下であるかどうかなど、いろいろテストしておかしかったら2度読みをやり直すようにプログラムするといいでしょう。

適当に作ったプログラム例

#include <stdio.h>   /* sprintf */

void readrtc(unsigned char *t)
{
    char err;
    static unsigned char adr[7] = { 0x00, 0x02, 0x04, 0x07, 0x08, 0x09, 0x32 };
    static unsigned char max[7] = { 0x60, 0x59, 0x23, 0x31, 0x12, 0x99, 0x99 };
    int i;
    for (;;) { /* 読み込みが成功するまで繰り返す */
        err = 0;
        for (i = 0; i < 7; i++) {
            io_out8(0x70, adr[i]);
            t[i] = io_in8(0x71);
        }
        for (i = 0; i < 7; i++) {
            io_out8(0x70, adr[i]);
            if (t[i] != io_in8(0x71) || (t[i] & 0x0f) > 9 || t[i] > max[i]) {
                err = 1;
            }
        }
        if (err == 0) {
            return;
        }
    }
}

void printtime(void)
{
    unsigned char s[24], t[7];
    readrtc(t);
    sprintf(s, "%02X%02X.%02X.%02X %02X:%02X:%02X\n", t[6], t[5], t[4], t[3], t[2], t[1], t[0]);
    ....
}

おまけ(時刻の取得だけじゃなくて設定もするぞ!)

  • 時刻表示したり時刻を利用するようなOSになると、時刻が狂っていて困ることがあるかもしれません。そんなときに再起動してBIOSや他のOSを使って時刻を設定するのはちょっとかっこ悪いかもしれません。そんなときはどうせなら自作OSで時刻設定もしてしまいましょう!
  • まずはCMOSメモリに書き込む方法から。
    void writecmos(unsigned char addr, unsigned char data)
    {
        io_out8(0x70, addr);
        io_out8(0x71, data);
        return;
    }
  • これを使って秒から書き込んでいきます。・・・なぜ秒から? 仮に 12:34:56 を設定しようとしているとしましょう。しかも実はRTCが03:59:59で、あとほんの一瞬で秒が増えそうだとします。そんなときに時から書いていると、時を書いたところでRTCの時刻更新が始まって、13:00:00になり、そこへ分と秒を書いて結局13:34:56というとんでもない時間になってしまうかもしれないからです。秒から書いた場合、56を書いた時点でこの先3秒は分や時が更新されることは無いわけで、ゆっくりと分や時を設定できるというわけです。
  • しかし秒から書くにしても、たまにRTCとアクセスが衝突してうまく書き込めていなかった、なんていう場合があるので、書き込みの直後に上記の方法でRTCを読み出して設定値どおりかどうかを確認したほうがいいでしょう。
    • どうせ確認するなら時から書いてもいいじゃないかと思うかもしれませんし、実はそのとおりなのですが(笑)、秒から書くほうが(設定が59秒でない限り)設定中に繰り上がり更新が発生する確率が減って(=設定が初回で成功する確率が上がって)、いいのではないかと思います。
    • まあ突き詰めると、まず最初に秒を0にしてそれから時から設定していけば、1分以内に設定が終わる限り時刻更新に悩まされることはありません。でもCMOSへの書き込みを一度増やしてまでこの問題を深刻に考えてもしょうがない気がするので、やはり秒から設定&確認で十分だと思います。

こめんと欄

  • 「適当に作ったプログラム例」で unsigned char max の要素の順番が逆なのと、配列の要素が7個しかないのに8までアクセスしようとしています。これだと無限ループ&配列外参照でまずいです。 -- 名無しさん 2006-04-04 (火) 14:40:46
  • ご指摘ありがとうございます。直しました。 -- K 2006-04-04 (火) 16:20:48
  • サンプルソースが欲しいです。 -- 名無しさん 2006-05-06 (土) 09:14:24
  • 上記以上のサンプルソースが必要だというのは、ちょっと疑問です。本当に本を最後まで読みましたか?このページは冒頭にあるように「読み終わった人」のためのものです。 -- K 2006-05-06 (土) 09:53:50
  • 既にお気づきかも知れませんが、「適当に作ったプログラム例」の呼び出し側関数(printtime)において、sprintfのフォーマット(時刻部分)に「X」が足りませんね。 -- rapper 2006-06-11 (日) 02:01:49
  • おっとそのとおりでした。直しました。ご指摘ありがとうございました。 -- K 2006-06-11 (日) 08:47:46
  • あの、Xの位置が違うと思います。 -- Clover 2006-06-11 (日) 13:03:55
  • %02Xは16進2桁の数字を出力するためですよ。5とか1桁で%Xだと「5」になりますが、%02Xだと「05」になります。 -- 名無しさん 2006-06-11 (日) 14:38:12
  • 再度修正しておきました。 -- K 2006-06-11 (日) 18:12:11

コメントお名前NameLink

リロード   新規 編集 差分 添付   トップ 一覧 検索 最終更新 バックアップ   ヘルプ   最終更新のRSS
Last-modified: 2006-12-22 (金) 19:12:25 (4811d)