--- nono/vm/mfp.cpp 2026/04/29 17:05:37 1.1.1.18 +++ nono/vm/mfp.cpp 2026/04/29 17:05:50 1.1.1.20 @@ -8,16 +8,111 @@ // MFP (MC68901) // +// タイマーについて。 +// +// ディレイモードでは、プリスケーラで設定した回数だけ入力をカウントすると +// メインカウンタにパルスが入る。このパルスはメインカウンタを 1 減らす。 +// メインカウンタが $01 になった次のパルスで (おそらく外から見ると $00 に +// ならないからこういう言い回しなのだが要するに 1 減らして内部的に $00 に +// なったらでいいと思う)、TxDR をリロードして「タイムアウトパルス」を起こす。 +// タイムアウトパルスが起きると、割り込みを(許可されていれば)上げると共に、 +// カウンタをリロードして、アウトプットピンの状態をトグルする。(MC68901.pdf) +// +// 1つのタイマーあたり +// o タイマー開始時のカウンタ初期値 +// o タイマー開始時刻 +// o 次回のリロード値 +// を保持する。 +// +// タイムアウトパルスとタイムアウトパルスの間は一定時間ごとにメインカウンタの +// 値が減算されていってるだけだし、割り込みを有効にしていなければメインカウンタ +// はタイムアウト周期でこの減算を繰り返しているだけなので、TxDR の読み出し値は +// 読み出し時刻から計算で求めることができる。 +// +// 現在値 = 初期値 - int((現在時刻 - 開始時刻) / メインカウンタ周期) % 初期値 +// +// 例えば、プリスケーラ 16分周(4usec)、カウント 5 の場合 +// +// (1)(2) (3) +// v v t+26 +// t+0 +4 +8 +12 +16 +20 +24 v +28 +32 +36 +40 [usec] +// ---+----+----+----+----+----+----+-----+----+----+----+--> time +// TxDR 5 5 4 3 2 1 5 4 3 2 1 5 +// +// (1) で TxDR に 5 をセット、(2) t+0 でタイマースタートして、(3) t+26 で +// TxDR を読み出された場合、値は +// 5 - int(((t+26 - t+0) % 20_usec) / 4_usec) +// = 5 - int(6_usec / 4_usec) +// = 5 - 1 +// = 4 +// +// 一方割り込みが必要な場合は、(2) でタイマースタートして、(4)、(4')… の +// タイミングでタイマーイベントにより割り込みを起こす。 +// +// (2) (4) (4') +// v v v +// ---+----+----+----+----+----+----+----+----+----+----+--> time +// TxDR 5 5 4 3 2 1 5 4 3 2 1 5 + +// 実時間追従について。 +// +// MFP の実時間追従はタイマーの「割り込みに対してのみ」作用する。 +// そもそも実時間追従とは、LUNA ではこの手のタイマーが固定のシステムクロック +// しかなくこれによるタイマー割り込み HZ 回でゲスト OS の時刻が1秒進むという +// ゲスト実装の特性と、割り込みの頻度は比較的低いことを利用した技法。 +// 従って MFP でも 10msec 程度のタイマー割り込みであれば動作する。 +// +// 一方 TxDR の減算 (プリスケーラ 200 分周でも 1 カウンタ減算が 50usec) は、 +// 実時間軸を作っている syncer イベントの分解能 (仮想時間で 50msec) より +// 遥かに高いため、これを (syncer の) 実時間軸に追従させることは現状不可能。 +// 例えば、より上位の割り込みが起きない Timer-C 割り込みのハンドラ冒頭で +// TCDR を読み出すと、通常はリロードされた初期値が読めるはずだが、これが +// 何が読めるか分からないという状態になる。 +// NetBSD/x68k の timecounter=mfp はまさにこの TxDR の読み出しで時間差を計測 +// して (10msec よりも高精度な 50usec 粒度で) 時間を進めようとするものなので +// (のはず)、TxDR の読み出しが信頼できなくなるこのモードでは使用できない。 +// 一方 timecounter=clockinterrupt はタイマー割り込み1回で 1/HZ 秒ずつ進める +// (そのため時間の分解能は 1/HZ 秒 = 10msec になる) モード(のはず…)のため、 +// 実時間追従でも動作する。 +// +// nono のオプション clock-sync の値、動作モード(等速 or フルスピード) と +// ゲスト OS NetBSD の sysctl kern.timecounter.hardware の値の組み合わせは +// 以下のようになる。 +// +// clock-sync mode sysctl timecounter +// ---------- ---- --------------- +// virtual 等速 mfp : ◯ +// virtual 等速 clockinterrupt : ◯ +// virtual フル mfp : △ +// virtual フル clockinterrupt : △ +// real 等速 mfp : × +// real 等速 clockinterrupt : ◯ +// real フル mfp : × +// real フル clockinterrupt : ◯ +// +// ◯: ゲスト OS の時間は実時間通りに進む。 +// △: VM 内の時間は実時間から切り離されてフルスピード時間軸で進む。 +// ×: ゲスト OS の時間が正しく進まない。 + +// Timer-B は 13 usec のタイムアウトごとに反転する出力ピン TOB を自身の +// TC, RC (UART の送受信クロック入力) に入れているだけで、割り込みは +// 使っていない。実機ではこれを変更するとキーボードと正しく通信できない +// ことになると思う。nono ではキーボードとの通信にこのクロック入力は +// 使っていないので、Timer-B は実質何もしていない。 + #include "mfp.h" +#include "event.h" #include "interrupt.h" #include "keyboard.h" #include "monitor.h" #include "mpu.h" #include "scheduler.h" +#include "syncer.h" #include "x68kkbd.h" // InsideOut p.135 -static const busdata wait = busdata::Wait(24 * 40_nsec); +/*static*/ const busdata +MFPDevice::wait = busdata::Wait(24 * 40_nsec); // コンストラクタ MFPDevice::MFPDevice() @@ -40,19 +135,27 @@ MFPDevice::Init() interrupt = GetInterruptDevice(); keyboard = dynamic_cast(GetKeyboard()); + // クロックの同期方法。 + syncer = GetSyncer(); + sync_rt = syncer->GetSyncRealtime(); + + // TxDR は 1-256 の範囲でなければいけないので。 + for (int ch = 0; ch < countof(mfp.timer); ch++) { + mfp.timer[ch].tdr = 256; + } + + auto evman = GetEventManager(); for (int ch = 0; ch < event.size(); ch++) { - event[ch].dev = this; - event[ch].func = ToEventCallback(&MFPDevice::TimerCallback); - event[ch].code = ch; - event[ch].SetName(string_format("MFP Timer-%c", ch + 'A')); - scheduler->RegistEvent(event[ch]); + event[ch] = evman->Regist(this, ch, + ToEventCallback(&MFPDevice::TimerCallback), + string_format("MFP Timer-%c", ch + 'A')); } // キーボード周りは x68kkbd.cpp のコメント参照 - key_event.func = ToEventCallback(&MFPDevice::KeyCallback); - key_event.time = 3750_usec; - key_event.SetName("MFP USART"); - scheduler->RegistEvent(key_event); + key_event = evman->Regist(this, + ToEventCallback(&MFPDevice::KeyCallback), + "MFP USART"); + key_event->time = 3750_usec; return true; } @@ -84,8 +187,8 @@ MFPDevice::ResetHard(bool poweron) mfp.tsr = MFP::TSR_BE; // All timers are stopped. - for (auto& ev : event) { - scheduler->StopEvent(ev); + for (int ch = 0; ch < event.size(); ch++) { + StopTimerEvent(ch); } scheduler->StopEvent(key_event); @@ -107,10 +210,10 @@ MFPDevice::ResetHard(bool poweron) // 時間計測用。 if (gMainApp.human_mode) { WritePort(MFP::VR, 0x40); + WritePort(MFP::IERB, 0x20); WritePort(MFP::TCDR, 200); WritePort(MFP::TCDCR, 0x70); WritePort(MFP::IMRB, 0x20); - WritePort(MFP::IERB, 0x20); } ChangeInterrupt(); @@ -235,11 +338,11 @@ MFPDevice::WritePort(uint32 offset, uint break; case MFP::IERA: putlog(2, "%s <- $%02x", regname[offset], data); - SetIER(mfp.ier.a, mfp.ipr.a, data); + SetIER((data << 8) | mfp.ier.b); break; case MFP::IERB: putlog(2, "%s <- $%02x", regname[offset], data); - SetIER(mfp.ier.b, mfp.ipr.b, data); + SetIER((mfp.ier.a << 8) | data); break; case MFP::IPRA: putlog(2, "%s <- $%02x", regname[offset], data); @@ -504,7 +607,7 @@ MFPDevice::MonitorUpdate(Monitor *, Text screen.Print(0, y++, " %-6s %-4s %-4s %-4s", "IER", "IMR", "IPR", "ISR"); for (int i = 15; i >= 0; i--, y++) { - uint16 mask = (1 << i); + uint16 mask = (1U << i); screen.Print(0, y, "%c%u:%-12s %-6s %-4s %-4s %-4s", i >= 8 ? 'A' : 'B', (i % 8), @@ -524,12 +627,12 @@ MFPDevice::MonitorUpdate(Monitor *, Text uint8 tcr = mfp_.timer[ch].tcr; if (tcr == 0) { screen.Print(x + 8, y, "Stopped"); + } else if (tcr < 8) { + screen.Print(x + 8, y, "Delay(%s)", prescale_str[tcr]); } else if (tcr == 8) { screen.Print(x + 8, y, "Event"); - } else if (tcr > 8) { - screen.Print(x + 8, y, "Pulse(%s)", prescale_str[tcr - 8]); } else { - screen.Print(x + 8, y, "Delay(%s)", prescale_str[tcr]); + screen.Print(x + 8, y, "Pulse(%s)", prescale_str[tcr - 8]); } screen.Print(x + 20, y, "%3u", tdr_[ch]); @@ -542,7 +645,7 @@ MFPDevice::MonitorUpdate(Monitor *, Text y = 9 + 8; screen.Puts(x, y++, " GPIP AER DDR"); for (int i = 7; i >= 0; i--, y++) { - uint mask = (1 << i); + uint mask = (1U << i); screen.Print(x, y, "b%u %-6s %u %u %s", i, gpipname[i], (mfp_.gpip & mask) ? 1 : 0, (mfp_.aer & mask) ? 1 : 0, @@ -583,16 +686,16 @@ MFPDevice::gpipname[] = { }; // TxCR の設定値から分周期間というかメインカウンタ1回分の時間を返す -/*static*/ const uint +/*static*/ const uint64 MFPDevice::prescale_ns[8] = { 0, - 1000, // /4 - 2500, // /10 - 4000, // /16 - 12500, // /50 - 16000, // /64 - 25000, // /100 - 50000, // /200 + 1000_nsec, // /4 + 2500_nsec, // /10 + 4000_nsec, // /16 + 12500_nsec, // /50 + 16000_nsec, // /64 + 25000_nsec, // /100 + 50000_nsec, // /200 }; /*static*/ const char * @@ -616,31 +719,54 @@ MFPDevice::timer_vector[4] = { MFP::INTR_TIMER_D, }; -// IER[AB] に data をセットする +// IER[AB] に new_ier (16 ビット全体) をセットする。 void -MFPDevice::SetIER(uint8& ier, uint8& ipr, uint32 data) +MFPDevice::SetIER(uint16 new_ier) { - // clr は、data によって %1 -> %0 に変化するビットだけを立てたもの。 - uint8 clr = ier & (ier ^ data); + // raising はこの書き込みで %0 -> %1 に変化するビットだけを立てたもの、 + // falling はこの書き込みで %1 -> %0 に変化するビットだけを立てたもの。 + uint16 raising = new_ier & (mfp.ier.w ^ new_ier); + uint16 falling = mfp.ier.w & (mfp.ier.w ^ new_ier); + + // タイマー用の event[] は、割り込みを起こす必要がある時しか回さない + // 構造なので、割り込みなしでスタートしたタイマーをその動作中に後から + // 割り込み許可に変更したら、ここで何事もなかったようにタイマーイベントを + // 開始しなければいけない。 + for (int ch = 0; ch < countof(mfp.timer); ch++) { + uint timer_mask = 1U << timer_vector[ch]; + if ((raising & timer_mask) != 0 && mfp.timer[ch].IsDelay()) { + event[ch]->SetName(string_format("MFP Timer-%c", ch + 'A')); + RestartTimerEvent(ch); + } + } // IER を下げたところは IPR も下げる。(紙資料 p4-4) - ipr &= ~clr; + mfp.ipr.w &= ~falling; // どちらにしても新しい IER によって信号線の状態は変える。 - ier = data; + mfp.ier.w = new_ier; ChangeInterrupt(); } +// このタイマーの現在のメインカウンタの値を求める。 +inline uint8 +MFP::Timer::GetCounter(uint64 now, uint64 period) const +{ + uint8 data = tdr - (((now - start) / period) % tdr); + return data; +} + // TxCR をセットする void MFPDevice::SetTCR(int ch, uint32 data) { + MFP::Timer& timer = mfp.timer[ch]; uint32 prev; - prev = mfp.timer[ch].tcr; + prev = timer.tcr; data &= 0x0f; - mfp.timer[ch].tcr = data; + timer.tcr = data; if (data == 0) { if (prev == 0) { // 0 -> 0 なら何も起きない @@ -650,15 +776,14 @@ MFPDevice::SetTCR(int ch, uint32 data) putlog(1, "Timer-%c Stop", ch + 'A'); // ここで TDR を確定させる。 - // タイマー動作中の TDR の参照はイベント終了時刻から現在値を逆算 - // していたが、イベントを停止すると求められなくなるので、イベント - // 停止前に求めておく。 + // タイマー動作中の TDR の参照はイベント開始時刻から求めていたが + // イベントを停止すると求められなくなるので、停止前に求めておく。 uint64 period = prescale_ns[prev]; uint64 now = scheduler->GetVirtTime(); - mfp.timer[ch].tdr = ((event[ch].vtime - now) + period - 1) / period; + mfp.timer[ch].tdr = timer.GetCounter(now, period); // でイベント停止 - scheduler->StopEvent(event[ch]); + StopTimerEvent(ch); return; } else if (prev == 8) { // イベントカウントモードから停止しても何も起きないはず @@ -668,16 +793,35 @@ MFPDevice::SetTCR(int ch, uint32 data) return; } else if (data < 8) { - // タイマー開始 + // タイマー開始。 - // TDR は触らずイベントを開始。 - uint64 period = prescale_ns[mfp.timer[ch].tcr]; - event[ch].time = period * mfp.timer[ch].tdr; - scheduler->RestartEvent(event[ch]); + // タイマー開始時は TDR はリロードしない。 + // 割り込みを上げる必要がある時だけイベントを使う。 + + uint64 period = prescale_ns[data]; + uint64 now = scheduler->GetVirtTime(); + timer.start = now; + if (IsTimerIntrEnable(ch)) { + event[ch]->time = period * timer.tdr; + event[ch]->SetName(string_format("MFP Timer-%c", ch + 'A')); + if (sync_rt) { + // 自分の実時間軸の基準をここにする。 + stime[ch] = syncer->GetRealTime(); + // event.time はこの後変動するので、周期は別で覚えておく。 + rt_period[ch] = event[ch]->time; + + scheduler->StartRealtimeEvent(event[ch], + stime[ch], rt_period[ch]); + } else { + scheduler->RestartEvent(event[ch]); + } + } else { + event[ch]->SetName(string_format("MFP Timer-%c FreeRun", ch + 'A')); + } putlog(1, "Timer-%c Start Delay mode(%u x %u.%uusec)", ch + 'A', - mfp.timer[ch].tdr, + timer.tdr, (uint)period / 1000, (uint)(period / 100) % 10); return; @@ -686,13 +830,15 @@ MFPDevice::SetTCR(int ch, uint32 data) // イベントカウントモード // TDR をロードする - mfp.timer[ch].next_tdr = mfp.timer[ch].tdr; + timer.next_tdr = timer.tdr; putlog(1, "Timer-%c Event count mode", ch + 'A'); + StopTimerEvent(ch); return; } else { putlog(0, "T%cCR Pulse-Width Measurement Mode $%02x (NOT IMPLEMENTED)", ch + 'A', data); + StopTimerEvent(ch); return; } } @@ -702,24 +848,23 @@ MFPDevice::SetTCR(int ch, uint32 data) uint8 MFPDevice::GetTDR(int ch) const { - uint8 tcr = mfp.timer[ch].tcr; + const MFP::Timer& timer = mfp.timer[ch]; - if (tcr == 0) { + if (timer.IsStopped()) { // 停止中なら tdr に現在値が保持されている - return mfp.timer[ch].tdr & 0xff; - - } else if (tcr < 8) { - // ディレイモード動作中なら現在値は保持していないので、 - // プリスケーラと終了時刻から算出。 + return timer.tdr & 0xff; - uint64 period = prescale_ns[tcr]; + } else if (timer.IsDelay()) { + // ディレイモード動作中なら現在値は保持していないので、算出する。 + // XXX sync_rt == true で割り込み有効にしているタイマーの + // TDR を読み出しても割り込みと整合しないがもう仕方ない。 + uint64 period = prescale_ns[timer.tcr]; uint64 now = scheduler->GetVirtTime(); - uint8 data = ((event[ch].vtime - now) + period - 1) / period; - return data; + return timer.GetCounter(now, period); } else { // イベントモード、パルス幅測定モードは未対応 - return mfp.timer[ch].tdr & 0xff; + return timer.tdr & 0xff; } } @@ -728,48 +873,113 @@ MFPDevice::GetTDR(int ch) const void MFPDevice::SetTDR(int ch, uint32 data) { - uint8& tcr = mfp.timer[ch].tcr; + MFP::Timer& timer = mfp.timer[ch]; if (data == 0) { data = 256; } - mfp.timer[ch].next_tdr = data; + timer.next_tdr = data; - if (tcr == 0) { + if (timer.IsStopped()) { // 停止中の書き込みはメインカウンタも更新。 - mfp.timer[ch].tdr = data; + timer.tdr = data; - } else if (tcr < 8) { + } else if (timer.IsDelay()) { // ディレイモード動作中の書き込みは // メインカウンタは影響を受けない。 + // 割り込みが回っていれば次の TimerCallback() で自動的にリロードされる + // ので、ここでは何もしなくてよい。 + // フリーランの場合は、仕方ないので次のタイムアウトパルスで + // 1回コールバックを呼んでリロードしてもらう。 + if (IsTimerIntrEnable(ch) == false) { + RestartTimerEvent(ch); + } } else { // イベントモード、パルス幅測定モードは未対応 putlog(0, "Write T%cDR $%02x during unsupported mode (NOT IMPLEMENTED)", - ch + 'A', mfp.timer[ch].next_tdr); + ch + 'A', timer.next_tdr); } } // イベントコールバック void -MFPDevice::TimerCallback(Event& ev) +MFPDevice::TimerCallback(Event *ev) { - int& ch = ev.code; + int ch = ev->code; + MFP::Timer& timer = mfp.timer[ch]; // TDR がゼロになったところで呼ばれるので、TDR をリロードする - mfp.timer[ch].tdr = mfp.timer[ch].next_tdr; + if (__predict_false(timer.tdr != timer.next_tdr)) { + timer.tdr = timer.next_tdr; + timer.start = 0; + // TDR (のリロード値) が変わったのでタイマー周りも再設定。 + uint64 period = prescale_ns[timer.tcr]; + ev->time = period * timer.tdr; + if (sync_rt) { + rt_period[ch] = ev->time; + } + } + + // カウントが変わったり基準点がまだなければ、基準点も更新。 + if (__predict_false(timer.start == 0)) { + uint64 now = scheduler->GetVirtTime(); + timer.start = now; + } // 割り込みを上げる SetInterrupt(timer_vector[ch]); - // 次のイベントをセット - // XXX 1回のイベントで複数のタイムアウトパルスを跨ぐ状況は考慮してない + // 次回の割り込みを起こす必要があれば、次のイベントをセット。 + // フリーラン動作中に TCR を書き換えられた時にはワンショットで来る。 + if (__predict_true(IsTimerIntrEnable(ch))) { + if (sync_rt) { + // 実時間追従動作の場合。 - // period が1メインパルス期間 - // time が1タイムアウトパルス期間 - uint64 period = prescale_ns[mfp.timer[ch].tcr]; - ev.time = period * mfp.timer[ch].tdr; - scheduler->RestartEvent(ev); + // 足す前は前回の割り込み発生時刻なので、足すと現在時刻。 + stime[ch] += rt_period[ch]; + + scheduler->StartRealtimeEvent(ev, stime[ch], rt_period[ch]); + } else { + scheduler->RestartEvent(ev); + } + } else { + // タイマー動作中に IER を下げた時にはここで次回以降のイベントを停止。 + ev->SetName(string_format("MFP Timer-%c FreeRun", ch + 'A')); + } +} + +// 次のタイムアウトパルス時刻にイベントを起こす。 +void +MFPDevice::RestartTimerEvent(int ch) +{ + MFP::Timer& timer = mfp.timer[ch]; + + assert(timer.start != 0); + + uint64 now = scheduler->GetVirtTime(); + uint64 period = prescale_ns[timer.tcr]; + uint64 timeout = period * timer.tdr; + event[ch]->time = timeout - ((now - timer.start) % timeout); + scheduler->RestartEvent(event[ch]); +} + +// タイマーイベントを停止する。 +void +MFPDevice::StopTimerEvent(int ch) +{ + scheduler->StopEvent(event[ch]); + event[ch]->SetName(string_format("MFP Timer-%c Stopped", ch + 'A')); + mfp.timer[ch].start = 0; +} + +// タイマー割り込みを行うなら true を返す。 +// タイマーの動作、停止状態ではなく、あくまで割り込み設定のほう。 +bool +MFPDevice::IsTimerIntrEnable(int ch) const +{ + // 割り込みを起こすかどうかは IER のみで決まる。 + return ((mfp.ier.w & (1U << timer_vector[ch])) != 0); } // HSync 入力 @@ -783,16 +993,18 @@ MFPDevice::SetHSync(bool hsync) void MFPDevice::SetVDisp(bool vdisp) { + MFP::Timer& timer = mfp.timer[0]; + // TAI への入力はイベントカウントモードの時のみ - if (mfp.timer[0].tcr == 8) { + if (timer.IsEvent()) { // AER が示す方向と同じエッジ方向の時 - bool aer = (mfp.aer & (1 << MFP::GPIP_VDISP)); + bool aer = (mfp.aer & (1U << MFP::GPIP_VDISP)); if (aer == vdisp) { // カウンタを減算 - mfp.timer[0].tdr--; - if (mfp.timer[0].tdr == 0) { + timer.tdr--; + if (timer.tdr == 0) { // 0 になったらリロード - mfp.timer[0].tdr = mfp.timer[0].next_tdr; + timer.tdr = timer.next_tdr; SetInterrupt(MFP::INTR_TIMER_A); } @@ -822,8 +1034,8 @@ MFPDevice::SetAlarmOut(bool alarm) void MFPDevice::SetGPIP(int num, bool val) { - bool aer = (mfp.aer & (1 << num)); - bool old = (mfp.gpip & (1 << num)); + bool aer = (mfp.aer & (1U << num)); + bool old = (mfp.gpip & (1U << num)); // 立ち上がりか立ち下がりで割り込みを上げる (AER による) // AER OLD VAL Interrupt @@ -850,9 +1062,9 @@ MFPDevice::SetGPIP(int num, bool val) } if (val) { - mfp.gpip |= (1 << num); + mfp.gpip |= (1U << num); } else { - mfp.gpip &= ~(1 << num); + mfp.gpip &= ~(1U << num); } } @@ -957,7 +1169,7 @@ MFPDevice::RxIsReady() const // だが、そもそもキーボードは送出作業中で、新たな送信はできないはずなので // この場合、つまり key_event の動作中も false (送信不可) にする。 - return !IsRR() && !key_event.IsRunning(); + return !IsRR() && !key_event->IsRunning(); } // キーボードからデータを受信 @@ -967,16 +1179,16 @@ MFPDevice::Rx(uint32 data) // ここは受信したビットをシフトレジスタへ入れ始めたところに相当する // ので、まだ Buffer Full は立てない。 - key_event.code = data; + key_event->code = data; scheduler->RestartEvent(key_event); } // キーボードからのデータが受信し終わった頃に呼ばれるイベント。 void -MFPDevice::KeyCallback(Event& ev) +MFPDevice::KeyCallback(Event *ev) { // UDR にデータを置いて Buffer Full をセット - mfp.udr = ev.code; + mfp.udr = ev->code; SetBF(); // Manual 7-5 7.2.1 Receiver Interrupt Channels より