|
|
1.1 root 1: //
2: // nono
1.1.1.4 root 3: // Copyright (C) 2020 nono project
4: // Licensed under nono-license.txt
1.1 root 5: //
6:
7: #include "mk48t02.h"
1.1.1.10! root 8: #include "bitops.h"
1.1.1.4 root 9: #include "mainapp.h"
1.1.1.7 root 10: #include "mpu.h"
1.1 root 11:
12: //
13: // MK48T02
14: //
15: // SRAM 部分はビッグエンディアンのままメモリに置かれるので、
16: // 必要ならアクセス時に都度変換すること。
17: //
18: // 仕様は 0x45000000 から 2KB だが、
19: // 0x44000000 から 0x48000000 の手前まで 2KB 単位でミラーが見える。
20:
1.1.1.10! root 21: // 繰り上がりについてのメモ。
! 22: //
! 23: // PROM で以下のようにすれば確認できる。
! 24: // >450007f8;l,80595923
! 25: // >450007fc;l,01DDMMYY
! 26: // >450007f8;,0
! 27: // >450007f8
! 28: //
! 29: // 2月の日数:
! 30: // 2/30 -> 03/01
! 31: // 2/31 -> 03/01
! 32: //
! 33: // 月ごとの日数:
! 34: // 01/00/30 -> 01/01/01 (00月は小)
! 35: // 01/0a/30 -> 01/0a/31 (0a月は大)
! 36: // 01/0b/30 -> 01/14/01 (0b月は小)
! 37: // 01/0c/30 -> 01/0c/31 (0c月は大)
! 38: // 01/0d/30 -> 01/14/01 (0d月は小)
! 39: // 01/0e/30 -> 01/0e/31 (0e月は大)
! 40: // 01/0f/30 -> 01/18/01 (0f月は小)
! 41: // 00/13/30 -> 01/00/01 (13月は小)
! 42: // 01/14/30 -> 01/14/31 (14月は大)
! 43: // 01/15/30 -> 01/16/01 (15月は小)
! 44: // 01/16/30 -> 01/16/31 (16月は大)
! 45: // 01/17/30 -> 02/08/01 (17月は小)
! 46: // 01/18/30 -> 01/18/31 (18月は大)
! 47: // 01/19/30 -> 01/10/01 (19月は小)
! 48: // 01/1a/30 -> 01/1a/31 (1a月は大)
! 49: // 01/1b/30 -> 01/10/01 (1b月は小)
! 50: // 01/1c/30 -> 01/1c/31 (1c月は大)
! 51: // 01/1d/30 -> 01/14/01 (1d月は小)
! 52: // 01/1e/30 -> 01/1e/31 (1e月は大)
! 53: // 01/1f/30 -> 01/18/01 (1f月は小)
! 54: //
! 55: // 月の繰り上がり後
! 56: // 01/0a/31 -> 01/0b/01
! 57: // 01/0b/31 -> 01/14/01
! 58: // 01/0c/31 -> 01/0d/01
! 59: // 01/0d/31 -> 01/14/01
! 60: // 01/0e/31 -> 01/0f/01
! 61: // 01/0f/31 -> 01/18/01
! 62: // 00/13/30 -> 01/00/01 +Y
! 63: // 01/14/31 -> 01/15/01
! 64: // 01/15/31 -> 01/16/01
! 65: // 01/16/31 -> 02/01/01 +Y
! 66: // 01/17/31 -> 02/08/01 +Y
! 67: // 01/18/31 -> 01/19/01
! 68: // 01/19/31 -> 01/10/01
! 69: // 01/1a/31 -> 02/09/01 +Y
! 70: // 01/1b/31 -> 01/10/01
! 71: // 01/1c/31 -> 01/1d/01
! 72: // 01/1d/31 -> 01/14/01
! 73: // 01/1e/31 -> 02/09/01 +Y
! 74: // 01/1f/31 -> 01/18/01
! 75: //
! 76: // 日の繰り上がり(一部)
! 77: // 01/01/0f -> 01/01/18
! 78: // 01/01/3e -> 01/02/1f
! 79: // 01/01/3f -> 01/01/2f
! 80: //
! 81: // 01/0a/28 -> 01/0b/01
! 82: // 00/0a/28 -> 00/0a/29 (閏)
! 83: // 00/0a/29 -> 00/0b/01
! 84: // 00/0a/30 -> 00/0a/31
! 85: // 00/0a/31 -> 00/0b/01
! 86: // 00/0a/32 -> 00/0a/33
! 87: // 00/0a/33 -> 00/0b/03
! 88: // 00/0a/34 -> 00/0a/35
! 89: // 00/0a/35 -> 00/0b/05
! 90: // 00/0a/36 -> 00/0a/37
! 91: // 00/0a/37 -> 00/0b/0f
! 92: // 00/0a/38 -> 00/0b/19
! 93: // 00/0a/39 -> 00/0a/21
! 94: // 00/0b/34 -> 00/14/05
! 95: // 00/0c/28 -> 00/0c/29
! 96: // 00/0c/29 -> 00/0c/30
! 97: // 00/0c/30 -> 00/0c/31
! 98: // 00/0c/31 -> 00/0d/01
! 99: // 00/0c/37 -> 00/0d/0f
! 100: // 01/1a/28 -> 01/1a/29
! 101: //
! 102: // 0a 月は閏月として判定されるようだ。bit 0?010 でマッチかな
! 103:
1.1 root 104: MK48T02Device::MK48T02Device()
1.1.1.9 root 105: : inherited("MK48T02")
1.1 root 106: {
1.1.1.9 root 107: AddAlias("NVRAM");
108: AddAlias("RTC");
109:
1.1 root 110: devaddr = 0x45000000;
1.1.1.9 root 111:
1.1.1.10! root 112: // 2021/07 時点の NetBSD/luna68k, OpenBSD/luna88k 合わせ。
! 113: // MK48T02 本来定義は 0 というか 1900 というか。
! 114: // see DecodeYear()
! 115: year_epoch = 1970;
! 116: use_localtime = false;
! 117:
1.1.1.9 root 118: monitor.func = (MonitorCallback_t)&MK48T02Device::MonitorUpdate;
1.1.1.10! root 119: monitor.SetSize(40, 13);
1.1.1.9 root 120: monitor.Regist(ID_MONITOR_MK48T02);
1.1 root 121: }
122:
123: MK48T02Device::~MK48T02Device()
124: {
125: }
126:
127: bool
128: MK48T02Device::Init()
129: {
1.1.1.10! root 130: if (inherited::Init() == false) {
! 131: return false;
! 132: }
! 133:
! 134: reg.ex = reg.in;
! 135:
! 136: // MK48T02 のデータシートには年には西暦年の下位2桁を設定するように
! 137: // 指示されている。
! 138: // しかし CMU Mach luna88k, NetBSD/luna68k, OpenBSD/luna88k はいずれも
! 139: // 西暦年から 1970 引いた値を設定するようになっている (2021/07 現在)。
! 140: // このため、うるう年が2年ずれた状態になる。
! 141: //
! 142: // NetBSD で MK48T02 を採用している luna68k 以外の port は
! 143: // ソースコード上の変数 year0 もしくは YEAR0 に
! 144: // newsmips, news68k: 1900 (データシート規定通り)
! 145: // sun3, sparc, mvme68k: 1968 (1968 はうるう年なので問題ない)
! 146: // を採用しており、バグっていない。
! 147: // この year0 は UNIX EPOCH とは直接関係がない RTC ハードウェアに
! 148: // 依存する値なのだが、LUNA では混同したのだろうか?
! 149: //
! 150: // たぶんこのバグはもう修正されることはないだろうから、こちらで補正する
! 151: // スイッチを用意した。
! 152:
! 153: int adjust_misused_epoch;
! 154: adjust_misused_epoch = gConfig->Find("luna-adjust-misused-epoch").AsInt();
! 155: if (adjust_misused_epoch) {
! 156: // エポック年のうるう年カウンタ相当を補正項として持つ
! 157: adjust_leap = year_epoch % 4;
! 158: } else {
! 159: adjust_leap = 0;
! 160: }
! 161:
1.1 root 162: // ファイルのロード
1.1.1.3 root 163: filename = gMainApp.GetVMDir() + "NVRAM.DAT";
1.1.1.4 root 164: file.SetFilename(filename);
165: file.SetDispname(string_format("NVRAM \"%s\"", filename.c_str()));
166: mem = file.OpenCreate(2040);
167: if (mem == NULL) {
1.1 root 168: return false;
169: }
170:
171: return true;
172: }
173:
174: // バイナリ表現を BCD 表現に変換する
175: /*static*/ inline uint8
176: MK48T02Device::num2BCD(uint8 num)
177: {
178: return ((num / 10) << 4) + (num % 10);
179: }
180:
181: // BCD 表現をバイナリ表現に変換する
182: /*static*/ inline uint8
183: MK48T02Device::BCD2num(uint8 bcd)
184: {
185: return (bcd >> 4) * 10 + (bcd & 0x0f);
186: }
187:
188: uint64
1.1.1.7 root 189: MK48T02Device::Read(uint32 offset)
1.1 root 190: {
191: uint32 data;
192:
1.1.1.7 root 193: if (offset < 2040) {
194: data = mem[offset];
195: putlog(3, "$%08x -> $%02x", gMPU->GetPaddr(), data);
1.1 root 196: } else {
1.1.1.7 root 197: switch (offset - 2040) {
1.1 root 198: case 0: // コントロールレジスタ
199: data = reg.ctrl;
200: break;
201: case 1: // 秒
1.1.1.10! root 202: data = reg.ex.sec | reg.stop;
1.1 root 203: break;
204: case 2: // 分
1.1.1.10! root 205: data = reg.ex.min;
1.1 root 206: break;
207: case 3: // 時
1.1.1.10! root 208: data = reg.ex.hour | reg.kickstart;
1.1 root 209: break;
210: case 4: // 曜日
1.1.1.10! root 211: data = reg.ex.wday | reg.freqtest;
1.1 root 212: break;
213: case 5: // 日
1.1.1.10! root 214: data = reg.ex.mday;
1.1 root 215: break;
216: case 6: // 月
1.1.1.10! root 217: data = reg.ex.mon;
1.1 root 218: break;
219: case 7: // 年
1.1.1.10! root 220: data = reg.ex.year;
1.1 root 221: break;
222: default:
1.1.1.3 root 223: __unreachable();
1.1 root 224: }
225:
1.1.1.7 root 226: putlog(3, "$%08x -> $%02x", gMPU->GetPaddr(), data);
1.1 root 227: }
228: return data;
229: }
230:
231: uint64
1.1.1.7 root 232: MK48T02Device::Write(uint32 offset, uint32 data)
1.1 root 233: {
1.1.1.7 root 234: if (offset < 2040) {
235: putlog(3, "$%08x <- $%02x", gMPU->GetPaddr(), data);
236: mem[offset] = data;
1.1 root 237: } else {
1.1.1.7 root 238: putlog(3, "$%08x <- $%02x", gMPU->GetPaddr(), data);
1.1.1.10! root 239:
1.1.1.7 root 240: switch (offset - 2040) {
1.1 root 241: case 0: // コントロール
242: WriteCtrl(data);
243: break;
244: case 1: // 秒 (b7 は STOP BIT)
1.1.1.10! root 245: reg.stop = data & MK48T02Clock::STOP;
! 246: reg.ex.sec = data & 0x7f;
1.1 root 247: break;
248: case 2: // 分
1.1.1.10! root 249: reg.ex.min = data & 0x7f;
1.1 root 250: break;
251: case 3: // 時 (b7 は KICK START)
1.1.1.10! root 252: reg.kickstart = data & MK48T02Clock::KICKSTART;
! 253: reg.ex.hour = data & 0x3f;
1.1 root 254: break;
255: case 4: // 曜日 (b6 は Frequency Test (未実装))
1.1.1.10! root 256: // データシートでは b7, b5, b4 は '0' とあるが実際には '1' が
! 257: // 書き込める。書き込んだ '1' が内部クロックの更新でどうなるか
! 258: // 未確認のため、とりあえず放置。
! 259: reg.freqtest = data & MK48T02Clock::FREQTEST;
! 260: reg.ex.wday = data & 0x07;
1.1 root 261: break;
262: case 5: // 日
1.1.1.10! root 263: // データシートでは b7, b6 は '0' とあるが実際には b7 には '1' が
! 264: // 書き込める (b6 には書き込めない)。b7 に書き込んだ '1' が内部
! 265: // クロックの更新でどうなるか未確認のため、とりあえずは放置。
! 266: reg.ex.mday = data & 0x3f;
1.1 root 267: break;
268: case 6: // 月
1.1.1.10! root 269: reg.ex.mon = data & 0x1f;
1.1 root 270: break;
271: case 7: // 年
1.1.1.10! root 272: reg.ex.year = data;
1.1 root 273: break;
274: default:
1.1.1.3 root 275: __unreachable();
1.1 root 276: }
277: }
278: return 0;
279: }
280:
281: uint64
1.1.1.7 root 282: MK48T02Device::Peek(uint32 offset)
1.1 root 283: {
1.1.1.7 root 284: if (offset < 2040) {
285: return mem[offset];
1.1 root 286: } else {
1.1.1.7 root 287: switch (offset - 2040) {
1.1 root 288: case 0: // コントロールレジスタ
289: return reg.ctrl;
290: case 1: // 秒 | STOP
1.1.1.10! root 291: return reg.ex.sec | reg.stop;
1.1 root 292: case 2: // 分
1.1.1.10! root 293: return reg.ex.min;
1.1 root 294: case 3: // 時
1.1.1.10! root 295: return reg.ex.hour | reg.kickstart;
1.1 root 296: case 4: // 曜日
1.1.1.10! root 297: return reg.ex.wday | reg.freqtest;
1.1 root 298: case 5: // 日
1.1.1.10! root 299: return reg.ex.mday;
1.1 root 300: case 6: // 月
1.1.1.10! root 301: return reg.ex.mon;
1.1 root 302: case 7: // 年
1.1.1.10! root 303: return reg.ex.year;
1.1 root 304: default:
1.1.1.3 root 305: __unreachable();
1.1 root 306: }
307: }
308: }
309:
1.1.1.5 root 310: void
1.1.1.9 root 311: MK48T02Device::MonitorUpdate(Monitor *, TextScreen& screen)
1.1 root 312: {
1.1.1.10! root 313: uint32 addr;
! 314: uint32 v;
! 315: int y = 0;
! 316:
1.1.1.9 root 317: screen.Clear();
1.1 root 318:
1.1.1.10! root 319: // BCD なので %x で表示する
! 320: screen.Print(0, y++,
! 321: "Internal: %04d[%02x]/%02x/%02x(%s) %02x:%02x:%02x.%1x",
! 322: DecodeYear(reg.in.year), reg.in.year,
! 323: reg.in.mon, reg.in.mday, wdays[reg.in.wday],
! 324: reg.in.hour, reg.in.min, reg.in.sec, (uint32)((cnt >> 1) & 0xf));
! 325: screen.Print(0, y++,
! 326: "Register: %04d[%02x]/%02x/%02x(%s) %02x:%02x:%02x",
! 327: DecodeYear(reg.ex.year), reg.ex.year,
! 328: reg.ex.mon, reg.ex.mday, wdays[reg.ex.wday],
! 329: reg.ex.hour, reg.ex.min, reg.ex.sec);
! 330: screen.Print(0, y++, "TimeZone: %s",
! 331: use_localtime ? "Local" : "UTC");
! 332: screen.Print(0, y++, "Epoch : %4d (Adjust Leap: %d)",
! 333: year_epoch, adjust_leap);
! 334: y++;
! 335:
! 336: addr = 0x800;
! 337: v = Peek(--addr);
! 338: screen.Print(0, y++, "$%3x: $%02x (year=%02x)", addr, v, v);
! 339: v = Peek(--addr);
! 340: screen.Print(0, y++, "$%3x: $%02x (mon =%02x)", addr, v, v);
! 341: v = Peek(--addr);
! 342: screen.Print(0, y++, "$%3x: $%02x (date=%02x)", addr, v, v);
! 343: v = Peek(--addr);
! 344: screen.Print(0, y++, "$%3x: $%02x (wday=%02x FT=%d)",
! 345: addr, v, (v & 0x07), (v & MK48T02Clock::FREQTEST) ? 1 : 0);
! 346: v = Peek(--addr);
! 347: screen.Print(0, y++, "$%3x: $%02x (hour=%02x KS=%d)",
! 348: addr, v, (v & 0x3f), (v & MK48T02Clock::KICKSTART) ? 1 : 0);
! 349: v = Peek(--addr);
! 350: screen.Print(0, y++, "$%3x: $%02x (min =%02x)", addr, v, (v & 0x7f));
! 351: v = Peek(--addr);
! 352: screen.Print(0, y++, "$%3x: $%02x (sec =%02x ST=%d)",
! 353: addr, v, (v & 0x7f), (v & MK48T02Clock::STOP) ? 1 : 0);
! 354: v = Peek(--addr);
! 355: screen.Print(0, y++, "$%3x: $%02x (W=%d R=%d)",
! 356: addr, v,
! 357: (v & MK48T02Clock::WRITE) ? 1 : 0,
! 358: (v & MK48T02Clock::READ) ? 1 : 0);
1.1 root 359: }
360:
361: // コントロールレジスタへの書き込み
362: void
363: MK48T02Device::WriteCtrl(uint32 data)
364: {
1.1.1.10! root 365: bool write_fall = bit_falling(reg.ctrl, data, MK48T02Clock::WRITE);
! 366:
1.1 root 367: reg.ctrl = data;
368:
1.1.1.10! root 369: // ホールドしているかどうかは状態なのでエッジではない
! 370: hold = (reg.ctrl & (MK48T02Clock::READ | MK48T02Clock::WRITE));
! 371:
! 372: if (write_fall) {
! 373: // WRITE ビットの立ち下がりエッジで
! 374: // 外部レジスタから内部カウンタへセット
! 375: reg.in = reg.ex;
! 376: // 分周段は規定されていないが意味的にクリアするほうが良いだろう
! 377: cnt = 0;
1.1 root 378: }
379: }
380:
1.1.1.10! root 381: uint
! 382: MK48T02Device::GetSec() const
! 383: {
! 384: return BCD2num(reg.in.sec);
! 385: }
! 386:
! 387: void
! 388: MK48T02Device::SetSec(uint v)
! 389: {
! 390: reg.in.sec = num2BCD(v);
! 391: }
! 392:
! 393: uint
! 394: MK48T02Device::GetMin() const
! 395: {
! 396: return BCD2num(reg.in.min);
! 397: }
! 398:
! 399: void
! 400: MK48T02Device::SetMin(uint v)
! 401: {
! 402: reg.in.min = num2BCD(v);
! 403: }
! 404:
! 405: uint
! 406: MK48T02Device::GetHour() const
! 407: {
! 408: return BCD2num(reg.in.hour);
! 409: }
! 410:
! 411: void
! 412: MK48T02Device::SetHour(uint v)
! 413: {
! 414: reg.in.hour = num2BCD(v);
! 415: }
! 416:
! 417: uint
! 418: MK48T02Device::GetWday() const
! 419: {
! 420: // データシートの正規の値範囲は 1..7 だが 0 も問題なく保持できて、
! 421: // NetBSD/luna68k, OpenBSD/luna88k は日付を日曜に変更する際には 0 を
! 422: // レジスタに書き込んでいる。そのためレジスタ値は 0 と 7 を日曜、
! 423: // 1を月曜…という運用ということにする。
! 424: // 戻り値は 0..6 (0が日曜)。
! 425:
! 426: int v = reg.in.wday;
! 427: if (v == 7) {
! 428: v = 0;
! 429: }
! 430: return v;
! 431: }
! 432:
! 433: void
! 434: MK48T02Device::SetWday(uint v)
! 435: {
! 436: // 引数は 0..6 (0が日曜)、MK48T02 の正規の範囲は 1..7 で曜日との対応は
! 437: // 運用依存のようだ。NetBSD/luna68k, OpenBSD/luna88k は 0 を日曜、1 を
! 438: // 月曜…と扱っているが、MK48T02 の曜日レジスタは 7 の翌日 1 に繰り上がる
! 439: // 動作なので、ここでもレジスタには 1..7 (1が月曜、7が日曜) をセットする。
! 440: if (v == 0)
! 441: v = 7;
! 442: reg.in.wday = v;
! 443: }
! 444:
! 445: uint
! 446: MK48T02Device::GetMday() const
! 447: {
! 448: return BCD2num(reg.in.mday);
! 449: }
! 450:
! 451: void
! 452: MK48T02Device::SetMday(uint v)
! 453: {
! 454: reg.in.mday = num2BCD(v);
! 455: }
! 456:
! 457: uint
! 458: MK48T02Device::GetMon() const
! 459: {
! 460: return BCD2num(reg.in.mon);
! 461: }
! 462:
! 463: void
! 464: MK48T02Device::SetMon(uint v)
! 465: {
! 466: reg.in.mon = num2BCD(v);
! 467: }
! 468:
! 469: uint
! 470: MK48T02Device::DecodeYear(uint bcd_year) const
! 471: {
! 472: uint y = BCD2num(bcd_year);
! 473: y += year_epoch;
! 474:
! 475: // 1970年より前を 2000 年代とみなすのはシステム依存だが
! 476: // UNIX 系はみんなこう(?)
! 477: // XXX ここの 1970 と RTC エポックを一致させれば端折れるじゃん!と
! 478: // 思いついたのかしら?
! 479: if (y < 1970) {
! 480: y += 100;
! 481: }
! 482: return y;
! 483: }
! 484:
! 485: uint
! 486: MK48T02Device::GetYear() const
! 487: {
! 488: // see DecodeYear()
! 489: return DecodeYear(reg.in.year);
! 490: }
! 491:
! 492: void
! 493: MK48T02Device::SetYear(uint v)
! 494: {
! 495: // see DecodeYear()
! 496: v -= year_epoch;
! 497: reg.in.year = num2BCD(v % 100);
! 498: }
! 499:
! 500: uint
! 501: MK48T02Device::GetLeap() const
! 502: {
! 503: // 補正項込みで計算
! 504: return (BCD2num(reg.in.year) + adjust_leap) & 3;
! 505: }
! 506:
! 507: void
! 508: MK48T02Device::SetLeap(uint v)
! 509: {
! 510: // nop
! 511: // MK48T02 ではうるう年カウンタはなく、西暦年でうるう年を判断している。
! 512: }
! 513:
! 514: // 1Hz 仮想 RTC からのコール
! 515: // 分周段 0/32 秒でコールされる。
! 516: void
! 517: MK48T02Device::Tick1Hz()
! 518: {
! 519: if (reg.stop) {
! 520: // 実機は電力消費を抑えるため発振器ごと止めるが、
! 521: // 分周段が規定されていないので秒カウントを止めれば十分なはず
! 522: //nop
! 523: } else {
! 524: CountUpSec();
! 525: }
! 526:
! 527: if (hold == false) {
! 528: // 毎秒コピーする (Datasheet p2)
! 529: reg.ex = reg.in;
! 530: }
1.1 root 531: }
1.1.1.3 root 532:
533: // 指定したアドレスからのゼロ終端文字列を返す
534: // (内蔵 ROM からのアクセス用)
535: std::string
536: MK48T02Device::PeekString(uint32 addr) const
537: {
538: std::string rv = "";
539:
1.1.1.7 root 540: addr &= 0x7ff;
541: for (; mem[addr]; addr++) {
542: rv += mem[addr];
1.1.1.3 root 543: }
544: return rv;
545: }
546:
547: // 指定したアドレスに文字列を書き込み、
548: // 16 バイトに満たないところはゼロでパディングする。
549: // 書き込めれば true を、書き込めなければ false を返す。
550: // (内蔵 ROM からの書き込み用)
551: bool
552: MK48T02Device::WriteString(uint32 addr, const std::string& data)
553: {
554: int i = 0;
555:
1.1.1.9 root 556: // ここの文字列は16バイト未満なら一見ゼロ終端のようだが、
557: // 16バイトならゼロ終端せず16文字とするフォーマット。
558:
559: if (data.length() > 16) {
1.1.1.3 root 560: return false;
561: }
562:
563: addr &= 0x7ff;
564: for (; i < data.length(); i++) {
1.1.1.9 root 565: mem[addr + i] = data[i];
1.1.1.3 root 566: }
567: for (; i < 16; i++) {
1.1.1.9 root 568: mem[addr + i] = 0;
1.1.1.3 root 569: }
570: return true;
571: }
1.1.1.9 root 572:
573: // 全域をゼロクリアする。
574: // クリアするだけなので、その後何か書いたりチェックサムを計算したりする
575: // 必要はある。
576: // (内蔵 ROM からの書き込み用)
577: void
578: MK48T02Device::ClearAll()
579: {
580: for (int i = 0, sz = file.GetMemSize(); i < sz; i++) {
581: mem[i] = 0;
582: }
583: }
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.