--- nono/vm/mk48t02.cpp 2026/04/29 17:05:06 1.1.1.11 +++ nono/vm/mk48t02.cpp 2026/04/29 17:05:33 1.1.1.16 @@ -4,43 +4,52 @@ // Licensed under nono-license.txt // -#include "mk48t02.h" -#include "bitops.h" -#include "mainapp.h" -#include "mpu.h" - // -// MK48T02 +// NVRAM/RTC (MK48T02) // + // SRAM 部分はビッグエンディアンのままメモリに置かれるので、 // 必要ならアクセス時に都度変換すること。 // // 仕様は 0x45000000 から 2KB だが、 -// 0x44000000 から 0x48000000 の手前まで 2KB 単位でミラーが見える。 +// LUNA-I では 0x44000000 から 0x47ffffff まで 2KB 単位でミラーが見える。 +// LUNA-88K では 0x44000000 から 0x45ffffff までが +// (こちらはロングワード配置なので) 8KB 単位のミラーに見える。 +#include "mk48t02.h" +#include "bitops.h" +#include "config.h" +#include "mainapp.h" +#include "monitor.h" +#include "mpu.h" +#include +#include +#include +#include + +// コンストラクタ MK48T02Device::MK48T02Device() - : inherited("MK48T02") + : inherited() { - AddAlias("NVRAM"); - AddAlias("RTC"); + SetName("MK48T02"); - devaddr = 0x45000000; + AddAlias("MK48T02"); + AddAlias("NVRAM"); - // 2021/07 時点の NetBSD/luna68k, OpenBSD/luna88k 合わせ。 - // MK48T02 本来定義は 0 というか 1900 というか。 - // see DecodeYear() - year_epoch = 1970; + // 今の所 UTC として使うケースだけのようだ。 use_localtime = false; - monitor.func = (MonitorCallback_t)&MK48T02Device::MonitorUpdate; - monitor.SetSize(40, 13); - monitor.Regist(ID_MONITOR_MK48T02); + monitor = gMonitorManager->Regist(ID_MONITOR_MK48T02, this); + monitor->func = ToMonitorCallback(&MK48T02Device::MonitorUpdate); + monitor->SetSize(40, 13); } +// デストラクタ MK48T02Device::~MK48T02Device() { } +// 初期化 bool MK48T02Device::Init() { @@ -48,10 +57,26 @@ MK48T02Device::Init() return false; } - reg.ex = reg.in; + struct stat st; + + // NetBSD/luna68k, OpenBSD/luna88k ともに epoch を 1970年としている + // (2021/07 現在) 一方、4.4BSD (luna68k 版?) は 1900年としている。 + // MK48T02 本来定義は 0 というか 1900 というか。DecodeYear() も参照。 + const ConfigItem& item = gConfig->Find("rtc-epoch-year"); + int year_epoch_val; + if (item.TryInt(&year_epoch_val) == false) { + item.Err(); + return false; + } + // 雑にチェック + if (year_epoch_val < 1900) { + item.Err("Must be >= 1900"); + return false; + } + year_epoch = (uint)year_epoch_val; // MK48T02 のデータシートには年には西暦年の下位2桁を設定するように - // 指示されている。 + // 指示されており、4.4BSD (luna68k) はこの通りになっている。 // しかし CMU Mach luna88k, NetBSD/luna68k, OpenBSD/luna88k はいずれも // 西暦年から 1970 引いた値を設定するようになっている (2021/07 現在)。 // このため、うるう年が2年ずれた状態になる。 @@ -66,21 +91,45 @@ MK48T02Device::Init() // // たぶんこのバグはもう修正されることはないだろうから、こちらで補正する // スイッチを用意した。 + if (gMainApp.Has(VMCap::LUNA)) { + if (gConfig->Find("luna-adjust-misused-epoch").AsInt()) { + // エポック年のうるう年カウンタ相当を補正項として持つ + adjust_leap = year_epoch % 4; + } else { + adjust_leap = 0; + } + } - int adjust_misused_epoch; - adjust_misused_epoch = gConfig->Find("luna-adjust-misused-epoch").AsInt(); - if (adjust_misused_epoch) { - // エポック年のうるう年カウンタ相当を補正項として持つ - adjust_leap = year_epoch % 4; - } else { - adjust_leap = 0; + // MSX-DOS モードなら NVRAM 不要なのでここでダミーのメモリだけ用意。 + if (gMainApp.msxdos_mode) { + // XXX アプリケーション寿命まで解放してないけど、まあいいか + mem = (uint8 *)malloc(16 * 1024); + return true; } - // ファイルのロード + // ファイルサイズを更新。 + // ver 0.2.3 までの NVRAM.DAT は 2040 バイトだったが、ver 0.2.4 以降は + // 2048 バイトになるため、ここでファイルがあって 2040 バイトなら 2048 + // バイトに拡張する。失敗したらここでは何もしない。 filename = gMainApp.GetVMDir() + "NVRAM.DAT"; + std::string dispname = string_format("NVRAM \"%s\"", filename.c_str()); + if (stat(filename.c_str(), &st) == 0 && st.st_size == 2040) { + int fd; + fd = open(filename.c_str(), O_WRONLY | O_APPEND); + if (fd >= 0) { + std::array zerobuf {}; + write(fd, &zerobuf[0], zerobuf.size()); + close(fd); + stat(filename.c_str(), &st); + warnx("%s extended to %jd bytes", + dispname.c_str(), (intmax_t)st.st_size); + } + } + + // ファイルのロード file.SetFilename(filename); - file.SetDispname(string_format("NVRAM \"%s\"", filename.c_str())); - mem = file.OpenCreate(2040); + file.SetDispname(dispname); + mem = file.OpenCreate(2048); if (mem == NULL) { return false; } @@ -88,6 +137,15 @@ MK48T02Device::Init() return true; } +void +MK48T02Device::StartTime() +{ + inherited::StartTime(); + + // 内部カレンダをメモリに反映 + Store(reg.in); +} + // バイナリ表現を BCD 表現に変換する /*static*/ inline uint8 MK48T02Device::num2BCD(uint8 num) @@ -102,128 +160,79 @@ MK48T02Device::BCD2num(uint8 bcd) return (bcd >> 4) * 10 + (bcd & 0x0f); } -uint64 -MK48T02Device::Read(uint32 offset) +busdata +MK48T02Device::ReadPort(uint32 offset) { - uint32 data; + busdata data; - if (offset < 2040) { - data = mem[offset]; - putlog(3, "$%08x -> $%02x", gMPU->GetPaddr(), data); - } else { - switch (offset - 2040) { - case 0: // コントロールレジスタ - data = reg.ctrl; - break; - case 1: // 秒 - data = reg.ex.sec | reg.stop; - break; - case 2: // 分 - // 最上位ビットはデータシートでは %0 だが現物は %1 固定らしい。 - // Issue#75 - data = reg.ex.min | 0x80; - break; - case 3: // 時 - data = reg.ex.hour | reg.kickstart; - break; - case 4: // 曜日 - data = reg.ex.wday | reg.freqtest; - break; - case 5: // 日 - data = reg.ex.mday; - break; - case 6: // 月 - data = reg.ex.mon; - break; - case 7: // 年 - data = reg.ex.year; - break; - default: - __unreachable(); - } + data = mem[offset]; + putlog(3, "$%08x -> $%02x", mpu->GetPaddr(), data.Data()); - putlog(3, "$%08x -> $%02x", gMPU->GetPaddr(), data); - } + // XXX wait? + data |= BusData::Size1; return data; } -uint64 -MK48T02Device::Write(uint32 offset, uint32 data) +busdata +MK48T02Device::WritePort(uint32 offset, uint32 data) { - if (offset < 2040) { - putlog(3, "$%08x <- $%02x", gMPU->GetPaddr(), data); - mem[offset] = data; - } else { - putlog(3, "$%08x <- $%02x", gMPU->GetPaddr(), data); + putlog(3, "$%08x <- $%02x", mpu->GetPaddr(), data); - switch (offset - 2040) { - case 0: // コントロール - WriteCtrl(data); - break; - case 1: // 秒 (b7 は STOP BIT) - reg.stop = data & MK48T02Clock::STOP; - reg.ex.sec = data & 0x7f; - break; - case 2: // 分 - reg.ex.min = data & 0x7f; - break; - case 3: // 時 (b7 は KICK START) - reg.kickstart = data & MK48T02Clock::KICKSTART; - reg.ex.hour = data & 0x3f; - break; - case 4: // 曜日 (b6 は Frequency Test (未実装)) - // データシートでは b7, b5, b4 は '0' とあるが実際には '1' が - // 書き込める。書き込んだ '1' が内部クロックの更新でどうなるか - // 未確認のため、とりあえず放置。 - reg.freqtest = data & MK48T02Clock::FREQTEST; - reg.ex.wday = data & 0x07; - break; - case 5: // 日 - // データシートでは b7, b6 は '0' とあるが実際には b7 には '1' が - // 書き込める (b6 には書き込めない)。b7 に書き込んだ '1' が内部 - // クロックの更新でどうなるか未確認のため、とりあえずは放置。 - reg.ex.mday = data & 0x3f; - break; - case 6: // 月 - reg.ex.mon = data & 0x1f; - break; - case 7: // 年 - reg.ex.year = data; - break; - default: - __unreachable(); - } - } - return 0; + switch (offset - 2040) { + case 0: // コントロール + WriteCtrl(data); + break; + case 1: // 秒 (b7 は STOP BIT) + reg.stop = data & MK48T02Clock::STOP; + break; + case 2: // 分 + break; + case 3: // 時 (b7 は KICK START) + reg.kickstart = data & MK48T02Clock::KICKSTART; + break; + case 4: // 曜日 (b6 は Frequency Test (未実装)) + // データシートでは b7, b5, b4 は '0' とあるが実際には '1' が + // 書き込める。書き込んだ '1' が内部クロックの更新でどうなるか + // 未確認のため、とりあえず放置。 + reg.freqtest = data & MK48T02Clock::FREQTEST; + break; + case 5: // 日 + // データシートでは b7, b6 は '0' とあるが実際には b7 には '1' が + // 書き込める (b6 には書き込めない)。b7 に書き込んだ '1' が内部 + // クロックの更新でどうなるか未確認のため、とりあえずは放置。 + break; + case 6: // 月 + break; + case 7: // 年 + break; + default: + break; + } + + mem[offset] = data; + + // XXX wait? + busdata r = BusData::Size1; + return r; +} + +busdata +MK48T02Device::PeekPort(uint32 offset) +{ + return mem[offset]; } -uint64 -MK48T02Device::Peek(uint32 offset) +bool +MK48T02Device::PokePort(uint32 offset, uint32 data) { - if (offset < 2040) { - return mem[offset]; + // 時計部分にはコントロールレジスタもあるので書き込み不可。 + if (offset >= 2040) { + return false; } else { - switch (offset - 2040) { - case 0: // コントロールレジスタ - return reg.ctrl; - case 1: // 秒 | STOP - return reg.ex.sec | reg.stop; - case 2: // 分 - // 最上位ビットはデータシートでは %0 だが現物は %1 固定らしい。 - return reg.ex.min | 0x80; - case 3: // 時 - return reg.ex.hour | reg.kickstart; - case 4: // 曜日 - return reg.ex.wday | reg.freqtest; - case 5: // 日 - return reg.ex.mday; - case 6: // 月 - return reg.ex.mon; - case 7: // 年 - return reg.ex.year; - default: - __unreachable(); + if ((int32)data >= 0) { + mem[offset] = data; } + return true; } } @@ -238,41 +247,44 @@ MK48T02Device::MonitorUpdate(Monitor *, // BCD なので %x で表示する screen.Print(0, y++, - "Internal: %04d[%02x]/%02x/%02x(%s) %02x:%02x:%02x.%1x", + "Internal: %04u[%02x]/%02x/%02x(%s) %02x:%02x:%02x.%1x", DecodeYear(reg.in.year), reg.in.year, reg.in.mon, reg.in.mday, wdays[reg.in.wday], reg.in.hour, reg.in.min, reg.in.sec, (uint32)((cnt >> 1) & 0xf)); + + auto ex = Load(); screen.Print(0, y++, - "Register: %04d[%02x]/%02x/%02x(%s) %02x:%02x:%02x", - DecodeYear(reg.ex.year), reg.ex.year, - reg.ex.mon, reg.ex.mday, wdays[reg.ex.wday], - reg.ex.hour, reg.ex.min, reg.ex.sec); + "Register: %04u[%02x]/%02x/%02x(%s) %02x:%02x:%02x", + DecodeYear(ex.year), ex.year, + ex.mon, ex.mday, wdays[ex.wday], + ex.hour, ex.min, ex.sec); + screen.Print(0, y++, "TimeZone: %s", use_localtime ? "Local" : "UTC"); - screen.Print(0, y++, "Epoch : %4d (Adjust Leap: %d)", + screen.Print(0, y++, "Epoch : %4u (Adjust Leap: %u)", year_epoch, adjust_leap); y++; addr = 0x800; - v = Peek(--addr); + v = PeekPort(--addr); screen.Print(0, y++, "$%3x: $%02x (year=%02x)", addr, v, v); - v = Peek(--addr); + v = PeekPort(--addr); screen.Print(0, y++, "$%3x: $%02x (mon =%02x)", addr, v, v); - v = Peek(--addr); + v = PeekPort(--addr); screen.Print(0, y++, "$%3x: $%02x (date=%02x)", addr, v, v); - v = Peek(--addr); - screen.Print(0, y++, "$%3x: $%02x (wday=%02x FT=%d)", + v = PeekPort(--addr); + screen.Print(0, y++, "$%3x: $%02x (wday=%02x FT=%u)", addr, v, (v & 0x07), (v & MK48T02Clock::FREQTEST) ? 1 : 0); - v = Peek(--addr); - screen.Print(0, y++, "$%3x: $%02x (hour=%02x KS=%d)", + v = PeekPort(--addr); + screen.Print(0, y++, "$%3x: $%02x (hour=%02x KS=%u)", addr, v, (v & 0x3f), (v & MK48T02Clock::KICKSTART) ? 1 : 0); - v = Peek(--addr); + v = PeekPort(--addr); screen.Print(0, y++, "$%3x: $%02x (min =%02x)", addr, v, (v & 0x7f)); - v = Peek(--addr); - screen.Print(0, y++, "$%3x: $%02x (sec =%02x ST=%d)", + v = PeekPort(--addr); + screen.Print(0, y++, "$%3x: $%02x (sec =%02x ST=%u)", addr, v, (v & 0x7f), (v & MK48T02Clock::STOP) ? 1 : 0); - v = Peek(--addr); - screen.Print(0, y++, "$%3x: $%02x (W=%d R=%d)", + v = PeekPort(--addr); + screen.Print(0, y++, "$%3x: $%02x (W=%u R=%u)", addr, v, (v & MK48T02Clock::WRITE) ? 1 : 0, (v & MK48T02Clock::READ) ? 1 : 0); @@ -282,22 +294,52 @@ MK48T02Device::MonitorUpdate(Monitor *, void MK48T02Device::WriteCtrl(uint32 data) { - bool write_fall = bit_falling(reg.ctrl, data, MK48T02Clock::WRITE); + uint8 old = mem[0x7f8]; - reg.ctrl = data; + bool write_fall = bit_falling(old, data, MK48T02Clock::WRITE); // ホールドしているかどうかは状態なのでエッジではない - hold = (reg.ctrl & (MK48T02Clock::READ | MK48T02Clock::WRITE)); + hold = (data & (MK48T02Clock::READ | MK48T02Clock::WRITE)); if (write_fall) { - // WRITE ビットの立ち下がりエッジで - // 外部レジスタから内部カウンタへセット - reg.in = reg.ex; + // WRITE ビットの立ち下がりエッジでメモリから内部カウンタへロード + reg.in = Load(); + // 分周段は規定されていないが意味的にクリアするほうが良いだろう cnt = 0; } } +// NVRAM からカレンダをロード +MK48T02Clock::calendar +MK48T02Device::Load() const +{ + MK48T02Clock::calendar cal; + + cal.sec = mem[0x7f9] & 0x7f; + cal.min = mem[0x7fa] & 0x7f; + cal.hour = mem[0x7fb] & 0x3f; + cal.wday = mem[0x7fc] & 0x07; + cal.mday = mem[0x7fd] & 0x3f; + cal.mon = mem[0x7fe] & 0x1f; + cal.year = mem[0x7ff]; + + return cal; +} + +// カレンダを NVRAM にストア +void +MK48T02Device::Store(const MK48T02Clock::calendar& cal) +{ + mem[0x7f9] = (mem[0x7f9] & ~0x7f) | cal.sec; + mem[0x7fa] = (mem[0x7fa] & ~0x7f) | cal.min; + mem[0x7fb] = (mem[0x7fb] & ~0x3f) | cal.hour; + mem[0x7fc] = (mem[0x7fc] & ~0x07) | cal.wday; + mem[0x7fd] = (mem[0x7fd] & ~0x3f) | cal.mday; + mem[0x7fe] = (mem[0x7fe] & ~0x1f) | cal.mon; + mem[0x7ff] = cal.year; +} + uint MK48T02Device::GetSec() const { @@ -343,7 +385,7 @@ MK48T02Device::GetWday() const // 1を月曜…という運用ということにする。 // 戻り値は 0..6 (0が日曜)。 - int v = reg.in.wday; + uint v = reg.in.wday; if (v == 7) { v = 0; } @@ -432,7 +474,7 @@ MK48T02Device::SetLeap(uint v) } // 1Hz 仮想 RTC からのコール -// 分周段 0/32 秒でコールされる。 +// (分周段 0/32 秒でコールされる) void MK48T02Device::Tick1Hz() { @@ -445,8 +487,8 @@ MK48T02Device::Tick1Hz() } if (hold == false) { - // 毎秒コピーする (Datasheet p2) - reg.ex = reg.in; + // 秒の更新でメモリにコピーする (Datasheet p2) + Store(reg.in); } }