File:  [Isaki's NoNo m68k/m88k emulator] / nono / vm / fdc.cpp
Revision 1.1.1.18 (vendor branch): download - view: text, annotated - select for diffs
Wed Apr 29 17:06:01 2026 UTC (2 months, 3 weeks ago) by root
Branches: MAIN, Isaki
CVS tags: v027, HEAD
nono 1.7.0

//
// nono
// Copyright (C) 2020 nono project
// Licensed under nono-license.txt
//

//
// FDC (uPD72065B) + I/O コントローラ
//

// MOTOR ON 信号
//
// X68030 の回路図によれば、簡略すると以下のように接続されている。
//
// PEDEC           SED9420A               FDD
// -------+     +---------------+      +-----------
//  ~TRIG |-----| TRIG    TMOUT |------| ~MOTOR ON
//
// SED9420A の TRIG は内蔵単安定マルチバイブレータのトリガ端子となっている。
// TRIG がアサートされている間 TMOUT をアサートし、TRIG がネゲートされると
// 一定時間後に TMOUT をネゲートする。
// その時間は SED9420A の CR 端子に接続された CR 回路によって決まってくるが
// データシートには時間の計算方法が記されていない。X68030 の回路図では
//
// SED9420A
// ---------+
//       CR |-----+---820kOhm---VCC1
//                |
//               47uF
//                |
//               GND
//
// となっているので時定数は 38.54 秒である。
// この時定数と TMOUT の維持時間の正確な関係は分からないが、他の単安定マルチ
// では時定数時間とパルス幅が同じになるように調整されたもの(74HC123)もある
// ので、この時定数時間を TMOUT (MOTOR ON) 信号の維持時間と考えることにする。
//
// 参考文献
//  最新フロッピ・ディスク装置とその応用ノウハウ  高橋昇司  p.175
//  https://archive.org/details/recent_floppy_disk_system_and_know_how/
//    page/n175
//
//
// DRIVE SEL.x 信号とアクセスドライブセレクトレジスタ
//
// アクセスドライブセレクトレジスタ($e94007) の MOTOR_ON ビットが 1 の
// ときは、アクセスドライブ番号フィールドにより指定された DRIVE SEL.x 信号が
// アサートされる。MOTOR_ON ビットが 0 のときは、すべての DRIVE SEL.x 信号は
// ネゲートされて、どのドライブも選択されていない状態になる。
// 「ドライブセレクトイネーブル」とあるのは、こういう意味である。
//
//
// DRIVE SEL.x 信号と各信号の関係
//
// MOTOR ON 信号と DRIVE SEL.x 信号の考察を踏まえると、MOTOR ON 信号は
// ドライブが選択されているかどうかに関わらず、全ドライブが受け取らなければ
// ならない。そうでないとタイマーを入れている意味がない。
// FDD は自分が選択されているかどうかに関係なく、MOTOR ON 信号がアサート
// されればモータを回そうとし、ネゲートされればモータを止める。
// また、アクセスドライブセレクトレジスタの 2HD/2DD ビット、およびその
// 関係する信号である DISK TYPE SEL, MIN/STD, MFM/FM 周りも同様と考えられる。
// これらの信号は SED9420A に接続されるため、ドライブごとに扱うように
// なっていないからである。
// また、FDDINT についても、どこかのドライブで状態が変わったことを CPU に
// 通知する、という機能が要求されるべきであるから、OP.SEL にもよらず
// 割り込みがかかる、と推定される。
//
// DRIVE SEL.x でドライブごとに有効になる信号
//   READY, INDEX, READ DATA, WRITE PROTECT, TRK00, WRITE GATE, WRITE DATA,
//   STEP, DIRECTION
// ドライブに共通の信号
//   MOTOR ON, DISK TYPE SEL, FDDINT
// どちらでも良い信号
//   SIDE SELECT (たぶんドライブごと有効と思われる)
// OP.SEL.x でドライブごとに有効になる信号
//   EJECT, EJECT MASK, LED BLINK, DISK IN, ERR DISK

// ドライブセレクトイネーブルがネゲートされていて、
// どのドライブも選択されていない状態になったとき、
// 各入力信号はプルアップされているため、
// 負論理の信号はすべてネゲート状態となる。

#include "fdc.h"
#include "bitops.h"
#include "config.h"
#include "dmac.h"
#include "event.h"
#include "fdd.h"
#include "monitor.h"
#include "pedec.h"
#include "scheduler.h"

// time [tsec] 後に呼び出す phase_event のコールバックをセットする。
// func の書式が面倒なのを省略して書きたいため。
#define CallAfter(func_, time_)	do { \
	phase_event->SetCallback(&FDCDevice::func_); \
	phase_event->time = time_; \
	scheduler->RestartEvent(phase_event); \
} while (0)


// コンストラクタ
FDCDevice::FDCDevice()
	: inherited(OBJ_FDC)
{
	monitor = gMonitorManager->Regist(ID_MONITOR_FDC, this);
	monitor->SetCallback(&FDCDevice::MonitorScreen);
	// サイズは Create() 時に確定する
}

// デストラクタ
FDCDevice::~FDCDevice()
{
}

bool
FDCDevice::Create()
{
	// ドライブ数。
	// fdd_list はランダムアクセスできる配列なので、歯抜け (#0 がなくて
	// #1 があるみたいなの) もサポートするが、今の所設定方法がない。
	const ConfigItem& item = gConfig->Find("fd-drive");
	ndrive = item.AsInt();

	// X68k では上限4台。(LUNA はひとまず放置)
	if (ndrive > 4) {
		item.Err();
		return false;
	}

	// モニタサイズはここで決定。FDD は横に2台で、それ以上は縦に伸ばす。
	monitor->SetSize(80, 15 + (ndrive / 2) * 10);

	// vector<unique_ptr> は面倒なので一旦 unique_ptr[] に確保して
	// 生ポインタの vector を作る。うーん。
	for (int i = 0; i < ndrive; i++) {
		try {
			fdd_list[i].reset(new FDDDevice(i));
		} catch (...) { }
		if ((bool)fdd_list[i] == false) {
			warnx("Failed to initialize FDDDevice(%u) at %s", i, __method__);
			return false;
		}
	}
	for (int i = 0; i < ndrive; i++) {
		if ((bool)fdd_list[i]) {
			fdd_vector.push_back(fdd_list[i].get());
		} else {
			fdd_vector.push_back(NULL);
		}
	}

	return true;
}

// 初期化
bool
FDCDevice::Init()
{
	dmac = GetDMACDevice();
	pedec = GetPEDECDevice();

	auto evman = GetEventManager();
	phase_event = evman->Regist(this, NULL, "FDC Phase");

	poll_event = evman->Regist(this,
		ToEventCallback(&FDCDevice::PollEventCallback),
		"FDC Polling");
	poll_event->time = 1024_usec;

	motor_event = evman->Regist(this,
		ToEventCallback(&FDCDevice::MotorEventCallback),
		"FDC SED9420A TMOUT");
	motor_event->time = 38.54_sec;

	return true;
}

// リセット
void
FDCDevice::ResetHard(bool poweron)
{
	if (poweron) {
		// READY ポーリングはリセットの影響を受けないはず
		initialized = false;
		scheduler->StopEvent(motor_event);
	}

	// uPD765 マニュアルによると、リセット信号によるリセットでは
	// SRT, HUT, HLT は影響を受けない。

	// シリンダ位置 pcn はとりあえずわからないので放置。

	// US0,US1 を low (FDD0) に。
	us = 0;
	// 割り込み (と DRQ) をネゲート。
	msr.ndm = false;
	st0.ic = ST0_IC_IC;
	st0.se = false;
	st0.ec = false;
	st0.nr = false;
	st0.hd = false;
	st0.us = 0;
	ChangeInterrupt();

	scheduler->StopEvent(phase_event);
	scheduler->StopEvent(poll_event);
	CommandPhase();
}

busdata
FDCDevice::ReadPort(uint32 offset)
{
	busdata data;

	switch (offset) {
	 case 0:
		if (dack) {
			// DACK が立っているときは A0 によらずデータレジスタをアクセス
			data = ReadData();
		} else {
			data = GetMSR();
			putlog(4, "STAT -> $%02x", data.Data());
		}
		break;
	 case 1:
		data = ReadData();
		break;
	 case 2:
		data = ReadDriveStatus();
		break;
	 case 3:
		data = 0xff;
		break;

	 default:
		VMPANIC("corrupted offset=%u", offset);
	}

	// InsideOut p.135
	const busdata wait = busdata::Wait(19 * 40_nsec);
	data |= wait;

	data |= BusData::Size1;
	return data;
}

busdata
FDCDevice::WritePort(uint32 offset, uint32 data)
{
	switch (offset) {
	 case 0:
		if (dack) {
			// DACK が立っているときは A0 によらずデータレジスタをアクセス
			WriteDataReg(data);
			break;
		}

		// ここはフェーズに関わらず 0x34, 0x35, 0x36 のコマンドだけを
		// いつでも受け付ける。RQM などの信号制御とは関係なく受け付ける、
		// となっているはずなので直接ディスパッチする。
		switch (data) {
		 case 0x34:
			CmdResetStandby();
			break;
		 case 0x35:
			CmdSetStandby();
			break;
		 case 0x36:
			CmdSoftwareReset();
			break;
		 default:
			putlog(0, "Write DATA: command $%02x (NOT IMPLEMENTED)",
				data);
			break;
		}
		break;

	 case 1:
		WriteCmd(data);
		break;

	 case 2:
	 {
		// ドライブコントロールレジスタ
		putlog(1, "Drive Control <- $%02x", data);

		uint8 bit = 0x01;
		for (const auto f : fdd_vector) {
			// ドライブセレクタのビットが %1 -> %0 になると動作する。
			if (f && bit_falling(drivectrl, data, bit)) {
				f->SetDriveCtrl(data & 0xe0);

				// FDD 割り込みのクリアは詳細不明だが、とりあえず
				// 同じタイミングで動作するようにしておく。
				pedec->NegateINT(f);
			}
			bit <<= 1;
		}
		drivectrl = data;
		break;
	 }

	 case 3:
	 {
		bool is2DD = ((data & 0x10) != 0);
		uint accdrive = data & 3;
		bool motor_on_bit_new = data & 0x80;

		if (motor_on_bit_new) {
			putlog(1, "Access Drive <- $%02x (MotorOn %s #%u)", data,
				is2DD ? "2DD" : "2HD",
				accdrive);

			if (motor_on_bit == false) {
				// MOTOR_ON ビットが立ち上がったら、全ドライブに通知
				// motor_event は止める。
				scheduler->StopEvent(motor_event);
				for (const auto f : fdd_vector) {
					if (f) {
						f->SetMotorOn(true);
					}
				}
			}
		} else {
			putlog(1, "Access Drive <- $%02x (MotorOff %s)", data,
				is2DD ? "2DD" : "2HD");

			// motor_on_bit はドライブセレクトイネーブルの意味も持つ

			// 全ドライブの選択を解除
			accdrive = MAX_DRIVE;

			if (motor_on_bit) {
				// MOTOR_ON ビットが立ち下がったら、一定時間後に MOTOR_ON
				// 信号をネゲートする。
				scheduler->RestartEvent(motor_event);
			}
		}

		// FDD 割り込みクリア(要実機確認)
		pedec->NegateINT(fdd_vector[0]);

		motor_on_bit = motor_on_bit_new;

		DriveSelect(accdrive);
		// 全ドライブに通知
		for (const auto f : fdd_vector) {
			if (f) {
				f->SetDriveSelect(accdrive, is2DD);
			}
		}
		break;
	 }
	 default:
		VMPANIC("corrupted offset=%u", offset);
	}

	// InsideOut p.135
	const busdata wait = busdata::Wait(19 * 40_nsec);

	busdata r = wait;
	r |= BusData::Size1;
	return r;
}

busdata
FDCDevice::PeekPort(uint32 offset)
{
	switch (offset) {
	 case 0:
		if (dack == false) {
			return GetMSR();
		}
		FALLTHROUGH;

	 case 1:
	 {
		// 副作用なくデータを読み出す
		switch (phase) {
		 case PHASE_COMMAND:
			return 0xff;

		 case PHASE_EXEC:
			if (dreg.Empty()) {
				return 0xff;	// XXX 何が読めるか
			}
			return dreg.Peek(0);

		 case PHASE_RESULT:
			return resbuf.Peek();

		 default:
			return 0xff;
		}
	 }

	 case 2:
		return GetDriveStatus();

	 case 3:
		return 0xff;

	 default:
		return 0xff;
	}
}

bool
FDCDevice::PokePort(uint32 offset, uint32 data)
{
	return false;
}

// モニタ更新
void
FDCDevice::MonitorScreen(Monitor *, TextScreen& screen)
{
	int x;
	int y;
	uint8 local_msr;
	uint8 local_st0;
	uint8 local_st1;
	uint8 local_st2;
	uint8 drivestat;
	FDDDevice *f;

	local_msr = GetMSR();
	local_st0 = GetST0();
	local_st1 = GetST1();
	local_st2 = GetST2();
	drivestat = GetDriveStatus();

	screen.Clear();
	y = 0;

	// 0         1         2         3         4         5         6
	// 01234567890123456789012345678901234567890123456789012345678901234567890
	// $ee99xx MSR:$00 --
	//         ST0:
	// Command: $00 $11 $22 $33 $44 $55 $66 $77 $88 $99 (READ DATA)

	// MSR
	screen.Print(0, y, "$%06x  MSR:$%02x", (baseaddr + 1),  local_msr);
	static const char * const msr_bits[] = {
		"RQM", "DIO", "NDM", "CB", "D3B", "D2B", "D1B", "D0B"
	};
	MonitorReg(screen, 17, y, local_msr, msr_bits);
	y++;

	// ST0
	screen.Print(9, y, "ST0:$%02x", local_st0);
	static const char * const st0_bits[] = {
		"",    "",    "SE",  "EC",  "NR",  "HD",  "US1", "US0"
	};
	MonitorReg(screen, 17, y, local_st0, st0_bits);
	static const char * const ic_str[] = {
		"NT",
		"AT",
		"IC",
		"AI",
	};
	screen.Print(17, y, "IC=%s", ic_str[local_st0 >> 6]);
	y++;

	// ST1
	screen.Print(9, y, "ST1:$%02x", local_st1);
	static const char * const st1_bits[] = {
		"EN", "-", "DE", "OR", "-", "ND", "NW", "MA"
	};
	MonitorReg(screen, 17, y, local_st1, st1_bits);
	y++;

	// ST2
	screen.Print(9, y, "ST2:$%02x", local_st2);
	static const char * const st2_bits[] = {
		"-", "CM", "DD", "NC", "SH", "SN", "BC", "MD"
	};
	MonitorReg(screen, 17, y, local_st2, st2_bits);
	y++;

	// ドライブコントロール
	screen.Print(0, y, "$e94005 CTRL:$%02x", drivectrl);
	static const char * const dctrl_bits[] = {
		"LED", "EJM", "EJO", "-", "D#3", "D#2", "D#1", "D#0"
	};
	MonitorReg(screen, 17, y, drivectrl, dctrl_bits);
	y++;

	// ドライブステータス
	screen.Print(0, y, "        STAT:$%02x", drivestat);
	static const char * const dstat_bits[] = {
		"DIN", "ERR", "-", "-", "-", "-", "-", "-"
	};
	MonitorReg(screen, 17, y, drivestat, dstat_bits);
	y++;

	x = 52;
	y = 0;
	screen.Print(x, y++, "HD=%u", hd);
	screen.Print(x, y++, "US=%u", us);
	screen.Print(x, y++, "TC=%u", tc);
	screen.Print(x, y++, "DACK=%u", dack);

	x = 60;
	y = 0;
	screen.Print(x, y++, "srt=%u (seek=%u)", srt, seek_srt);
	screen.Print(x, y++, "hut=%u", hut);
	screen.Print(x, y++, "hlt=%u", hlt);
	screen.Print(x, y++, "nd=%u", nd);

	x = 52;
	y = 5;
	screen.Puts(x, y, "<OPM>");
	screen.Print(x + 6, y, TA::OnOff(force_ready), "CT2");

	// フェーズ、コマンド
	x = 0;
	y = 7;
	static const char * const phase_str[] = {
		"Command",
		"Execute",
		"Result",
	};
	screen.Print(x, y++, "Phase:   %s", phase_str[(int)phase]);
	if (cmdbuf.len > 0) {
		screen.Puts(x, y, "Command:");
		for (uint i = 0; i < cmdbuf.len; i++) {
			screen.Print(x + 9 + i * 4, y, "$%02x", cmdbuf.buf[i]);
		}
		screen.Print(x + 49, y, "(%s)", cmd->name);
	}

	x = 1;
	y = 10;
	// 存在するかに関わらず 4台分表示する (LUNA は知らん)
	screen.Puts(0, y, "<FDD> PCN NCN Ready Poll");
	screen.Puts(26, y, "Motor AccLED Eject   FMT C  H  R  N"); 
	y++;
	for (int i = 0; i < MAX_DRIVE; i++) {
		UnitInfo& u = unitinfo[i];

		if (i < fdd_vector.size()) {
			f = fdd_vector[i];
		} else {
			f = NULL;
		}

		screen.Print(x, y, "#%u", i);
		if (f) {
			screen.Print(x + 5, y, "%3d %3u", u.pcn, u.ncn);
			screen.Print(x + 13, y, TA::OnOff(u.ready), "Ready");
			static const char * state_str[] = {
				"Idle",
				"Seek",
				"Recal",
			};
			if ((uint)u.state < countof(state_str)) {
				screen.Puts(x + 19, y, state_str[(int)u.state]);
			} else {
				screen.Puts(x + 19, y, "state?");
			}
		} else {
			screen.Puts(x + 5, y, TA::Disable, "Not connected");
		}

		y++;
	}

	// FDD の状況も同じモニタ内に表示する。
	for (int i = 0; i < fdd_vector.size(); i++) {
		f = fdd_vector[i];
		if (f) {
			f->MonitorScreenFDD(screen, hd);
		}
	}
}

// レジスタをビットごとに表示する。MonitorScreen の下請け。
void
FDCDevice::MonitorReg(TextScreen& screen,
	int x, int y, uint8 reg, const char * const *names)
{
	for (uint i = 0; i < 8; i++) {
		if (names[i][0] == '-') {
			screen.Puts(x + i * 4, y, TA::Disable, "---");
		} else {
			bool b = reg & (1U << (7 - i));
			screen.Puts(x + i * 4, y, TA::OnOff(b), names[i]);
		}
	}
}

// メインステータスレジスタ(MSR)取得
uint8
FDCDevice::GetMSR() const
{
	uint8 stat = 0;

	if (msr.rqm) {
		stat |= MSR_RQM;
	}
	if (msr.dio == DIOtoHost) {
		stat |= MSR_DIO;
	}
	if (msr.ndm) {
		stat |= MSR_NDM;
	}
	if (msr.cb) {
		stat |= MSR_CB;
	}
	for (int i = 0; i < msr.db.size(); i++) {
		stat |= (msr.db[i] ? 1 : 0) << i;
	}

	return stat;
}

// ST0 レジスタ取得
uint8
FDCDevice::GetST0() const
{
	uint8 data;

	data = st0.ic;
	if (st0.se) {
		data |= ST0_SE;
	}
	if (st0.ec) {
		data |= ST0_EC;
	}
	if (st0.nr) {
		data |= ST0_NR;
	}
	if (st0.hd) {
		data |= ST0_HD;
	}
	data |= st0.us;

	return data;
}

// ST1 レジスタ取得
uint8
FDCDevice::GetST1() const
{
	uint8 data = 0;

	if (st1.en) {
		data |= ST1_EN;
	}
	if (st1.de) {
		data |= ST1_DE;
	}
	if (st1.overrun) {
		data |= ST1_OR;
	}
	if (st1.nd) {
		data |= ST1_ND;
	}
	if (st1.nw) {
		data |= ST1_NW;
	}
	if (st1.ma) {
		data |= ST1_MA;
	}
	return data;
}

// ST2 レジスタ取得
uint8
FDCDevice::GetST2() const
{
	uint8 data = 0;

	if (st2.cm) {
		data |= ST2_CM;
	}
	if (st2.dd) {
		data |= ST2_DD;
	}
	if (st2.nc) {
		data |= ST2_NC;
	}
	if (st2.sh) {
		data |= ST2_SH;
	}
	if (st2.sn) {
		data |= ST2_SN;
	}
	if (st2.bc) {
		data |= ST2_BC;
	}
	if (st2.md) {
		data |= ST2_MD;
	}
	return data;
}

// FDC データレジスタ読み込み
uint32
FDCDevice::ReadData()
{
	uint8 data;

	// TODO: RQM, DIO 処理

	switch (phase) {
	 case PHASE_COMMAND:
		// 規定フェーズ外の読み込み
		// XXX ほんと?
		putlog(1, "Read DATA: in irregular phase");
		data = 0xff;
		return data;

	 case PHASE_EXEC:
		if (dreg.Dequeue(&data) == false) {
			// エラー
			return 0xff;
		}
		putlog(4, "DATA -> $%02x", data);
		return data;

	 case PHASE_RESULT:
		data = resbuf.Take();
		putlog(2, "DATA -> $%02x", data);
		if (resbuf.pos == 1) {
			// 1 バイトを読んだところで
			// ステータスをレポートするとクリアされるようだ
			st0.ic = ST0_IC_IC;
			st0.se = false;
			st0.ec = false;
			st0.nr = false;
			st0.hd = false;
			st0.us = 0;
			ChangeInterrupt();
		}
		if (resbuf.pos == resbuf.len) {
			CommandPhase();
		}
		return data;

	 default:
		VMPANIC("corrupted phase=$%x", (uint)phase);
	}
}

// ドライブステータスレジスタ取得
uint32
FDCDevice::GetDriveStatus() const
{
	uint8 data = 0;

	// ドライブコントロールのドライブ選択が %1 になっているドライブを調べる。
	// 誤挿入は未対応。
	uint8 bit = 0x01;
	for (const auto f : fdd_vector) {
		if ((drivectrl & bit)) {
			if (f && f->IsMediumLoaded()) {
				data |= 0x80;
			}
		}
		bit <<= 1;
	}

	return data;
}

// ドライブステータスレジスタ読み込み
uint32
FDCDevice::ReadDriveStatus()
{
	uint8 data = GetDriveStatus();
	putlog(1, "Drive Status -> $%02x", data);
	return data;
}

// FDC コマンドレジスタ書き込み
void
FDCDevice::WriteCmd(uint32 data)
{
	switch (phase) {
	 case PHASE_COMMAND:
		if (msr.rqm == false) {
			// リクエストしていないコマンド書き込み
			putlog(1, "CMD <- $%02x: not requested", data);
			break;
		}
		msr.rqm = false;

		putlog(2, "CMD[%u] <- $%02x", cmdbuf.len, data);
		cmdbuf.buf[cmdbuf.len++] = data;
		// コマンドの1バイト目でディスパッチ
		if (cmdbuf.len == 1) {
			// 見付からなければリスト末尾の Invalid にマッチする
			for (auto& c : cmd_list) {
				if ((cmdbuf.code & c.mask) == c.code) {
					cmd = &c;
					break;
				}
			}
			putlog(1, "Command %s accepted", cmd->name);
		}
		CallAfter(CommandCallback, 2_usec);
		break;

	 case PHASE_EXEC:
		if (msr.dio == DIOtoFDC) {
			WriteDataReg(data);
		} else {
			putlog(1, "CMD <- $%02x: unexpected data write (DIO)", data);
		}
		break;

	 case PHASE_RESULT:
		putlog(1, "CMD <- $%02x: unexpected data write (result phase)", data);
		break;

	 default:
		VMPANIC("corrupted phase=$%x", (uint)phase);
	}
}

// FDC データレジスタ書き込み
void
FDCDevice::WriteDataReg(uint32 data)
{
	dreg.EnqueueForce(data);
}

// Command フェーズに移行
void
FDCDevice::CommandPhase()
{
	putlog(2, "Command Phase");
	phase = PHASE_COMMAND;
	msr.rqm = true;
	msr.dio = DIOtoFDC;
	msr.ndm = false;
	msr.cb = false;
	cmdbuf.Clear();
	resbuf.Clear();
}

// Exec フェーズに移行
void
FDCDevice::ExecPhase()
{
	putlog(2, "Exec Phase: %s", cmd->name);
	phase = PHASE_EXEC;
	msr.cb = true;
	if (nd) {
		msr.ndm = true;	// XXX ?
	}
}

// Result フェーズに移行
void
FDCDevice::ResultPhase()
{
	putlog(2, "Result Phase");
	phase = PHASE_RESULT;
	msr.rqm = true;
	msr.dio = DIOtoHost;
	msr.ndm = false;
	msr.cb = true;
}

void
FDCDevice::ResultCallback(Event *ev)
{
	ResultPhase();
}

void
FDCDevice::CommandCallback(Event *ev)
{
	msr.cb = true;
	if (cmdbuf.len == cmd->cmdlen) {
		putlog(2, "Command %s execute", cmd->name);
		(this->*(cmd->func))();
	} else {
		msr.rqm = true;
	}
}

// 割り込み信号線の状態を変える
void
FDCDevice::ChangeInterrupt()
{
	if (GetST0() != 0x80 || (msr.ndm && 0/* XXX DRQ*/)) {
		putlog(4, "AssertINT");
		pedec->AssertINT(this);
	} else {
		putlog(4, "NegateINT");
		pedec->NegateINT(this);
	}
}

// READY 信号が変化したことの通知 (FDD から呼ばれる)
void
FDCDevice::ReadyChanged(int unit, bool is_ready)
{
	// 実際は READY 信号は SPECIFY コマンドが呼ばれた後 1.024 msec 間隔で
	// 常にポーリングしているが、エミュレータでは無駄なので、変化があった
	// ことの通知を受けてポーリングループを起動し、どのユニットも変化が
	// なくなったらポーリングを止めるようにしておく。
	// uPD765 マニュアル POLLING FEATURE OF THE uPD765 (p.433)
	StartPoll();

	// 転送の同期を取るために、READY になった時刻を覚えておく
	if (is_ready) {
		unitinfo[unit].ready_time = scheduler->GetVirtTime();
	}
}

// ポーリング開始
void
FDCDevice::StartPoll()
{
	// SPECIFY コマンドで有効になる
	if (initialized == false) {
		return;
	}
	if (poll_event->IsRunning() == false) {
		putlog(3, "Start polling");
		scheduler->StartEvent(poll_event);
	}
}

// ポーリングイベントコールバック
void
FDCDevice::PollEventCallback(Event *ev)
{
	bool is_poll = false;

	// READY ポーリング。
	// コマンドフェーズにいてコマンド未発行のときだけ実行。
	// Inside で言う「シーク中の E-PHASE」も FDC としては
	// コマンドフェーズなので同じ状態になっている。
	if (phase == PHASE_COMMAND && cmdbuf.len == 0) {
		for (int unit_no = 0; unit_no < MAX_DRIVE; unit_no++) {
			is_poll |= PollReady(unit_no);
		}
	}

	// シーク
	seek_srt++;
	if (seek_srt == 16) {
		seek_srt = srt;
		for (int unit_no = 0; unit_no < MAX_DRIVE; unit_no++) {
			is_poll |= ExecSeek(unit_no);
		}
	} else {
		is_poll = true;
	}

	if (is_poll) {
		scheduler->RestartEvent(ev);
	} else {
		putlog(3, "Stop polling");
	}
}

// READY ポーリングの実行。
// ポーリングを継続する必要があるときは true を返す。
bool
FDCDevice::PollReady(int unit_no)
{
	auto& unit = unitinfo[unit_no];

	UnitSelect(unit_no);
	bool r = IsReady();
	if (unit.ready != r) {
		// READY が変化したので、ドライブ状態が Idle でかつ、
		// 割り込み発行中でなければ状態を更新して割り込みを発行
		if (unit.state == UnitState::Idle && st0.ic == ST0_IC_IC) {
			unit.ready = r;
			st0.ic = ST0_IC_AI;
			st0.nr = !r;
			st0.us = unit_no;
			ChangeInterrupt();
			return false;
		} else {
			// 割り込み発生をペンディングしているのでポーリング続行
			return true;
		}
	}
	return false;
}

// リキャリブレートまたはシークの実行。
// ポーリングを継続する必要があるときは true を返す。
bool
FDCDevice::ExecSeek(int unit_no)
{
	auto& unit = unitinfo[unit_no];

	if (unit.state == UnitState::Idle) {
		return false;
	}

	UnitSelect(unit_no);

	if (IsReady() == false) {
		unit.state = UnitState::Idle;
		st0.nr = true;
		st0.ic = ST0_IC_AT;
		st0.hd = false;
		st0.us = us;
		ChangeInterrupt();
		CommandPhase();
		return false;
	}
	unit.ready = true;

	if (unit.state == UnitState::Recalibrate) {
		if (fdd && fdd->GetTrack0()) {
			putlog(2, "Track0 detected");
			unit.pcn = 0;
			SeekEnd();
			return false;
		} else if (unit.pcn < 0) {
			putlog(2, "Track0 not detected");
			st0.ec = true;
			SeekEnd();
			return false;
		}
	} else {
		if (unit.pcn == unit.ncn) {
			SeekEnd();
			return false;
		}
	}

	// シークを FDD に発行
	int dir;
	if (unit.pcn < unit.ncn) {
		dir = 1;
	} else {
		dir = -1;
	}
	unit.pcn += dir;
	if (fdd) {
		fdd->DoSeek(dir);
	}
	return true;
}

// 強制レディ信号の設定 (OPM から呼ばれる)
void
FDCDevice::SetForceReady(bool force_ready_)
{
	force_ready = force_ready_;
	putlog(2, "Force Ready %s", force_ready ? "On" : "Off");
	StartPoll();
}

// DACK 信号をアサートする (DMAC から呼ばれる)
void
FDCDevice::AssertDACK(bool tc_)
{
	dack = true;
	tc = tc_;
	// DRQ を下げる
	dmac->NegateREQ(0);
}

// DACK 信号をネゲートする (DMAC から呼ばれる)
void
FDCDevice::NegateDACK()
{
	dack = false;
}

// 内部データレジスタに data を投入
void
FDCDevice::PushDreg(uint8 data)
{
	if (dreg.Enqueue(data)) {
		// FDC DRQ をアサートして DMAC REQ#0 を送る
		RequestDataByte();
	} else {
		// OverRun
		putlog(1, "PushDreg overrun detected");
		st1.overrun = true;
		st0.ic = ST0_IC_AT;
		ChangeInterrupt();
	}
}

// 内部データレジスタから data を取り出す。
// 空なら (uint32)-1 を返す。
uint32
FDCDevice::PopDreg()
{
	uint8 data;

	if (dreg.Dequeue(&data)) {
		return data;
	} else {
		// OverRun
		putlog(1, "PopDreg overrun detected");
		st1.overrun = true;
		st0.ic = ST0_IC_AT;
		ChangeInterrupt();
		return (uint32)-1;
	}
}

// CPU または DMAC に読み書きデータを要求する
void
FDCDevice::RequestDataByte()
{
	if (nd) {
		putlog(0, "CPU Transfer mode (NOT IMPLEMENTED)");
	} else {
		dmac->AssertREQ(0);
	}
}

// ユニット US0,US1 信号線の状態を変える。
void
FDCDevice::UnitSelect(uint8 us_)
{
	// こちらは FDC 側の US0, US1 信号線の出力に相当。
	// アクセスドライブは X68k では I/O コントローラ側で決まる。
	// LUNA では XP 側の PSG の I/O と US 信号線の組み合わせで決まる。
	us = us_ & 0x03;

	putlog(3, "UnitSelect US:%u", us);

	// いまのところ LUNA のことは忘れる
}

// ドライブセレクト信号線の状態を変える。
void
FDCDevice::DriveSelect(uint8 drive)
{
	// こちらは I/O コントローラにあるアクセスドライブセレクトのほう
	if (drive < ndrive) {
		fdd = fdd_vector[drive];
	} else {
		fdd = NULL;
	}

	// ドライブセレクト信号線が変わったら READY が変わるかもしれないので
	// ポーリングを再開する必要がある。
	StartPoll();

	// いまのところ LUNA のことは忘れる
}

// 一定時間後に MOTOR_ON 信号をネゲートするイベント。
// SED9420 の TMOUT 信号に相当。
void
FDCDevice::MotorEventCallback(Event *ev)
{
	// 全ドライブに通知
	for (const auto f : fdd_vector) {
		if (f) {
			f->SetMotorOn(false);
		}
	}
}

// FDC コマンド

void
FDCDevice::CmdReadDiagnostic()
{
	CmdNotImplemented();
}

void
FDCDevice::CmdSpecify()
{
	srt = cmdbuf.specify.srt_hut >> 4;
	hut = cmdbuf.specify.srt_hut & 15;
	hlt = cmdbuf.specify.hlt_nd >> 1;
	nd  = cmdbuf.specify.hlt_nd & 1;
	putlog(2, " SRT:%u HUT:%u HLT:%u ND:%u", srt, hut, hlt, nd);

	// 初期化完了、ポーリング開始
	initialized = true;

	CommandPhase();
}

void
FDCDevice::CmdSenseDeviceStatus()
{
	UnitSelect(cmdbuf.sense_device_status.hd_us);

	uint8 st3 = 0;
	// FT = fdd.GetFault();
	if (fdd && fdd->GetWriteProtect()) {
		st3 |= ST3_WP;
	}
	if (IsReady()) {
		st3 |= ST3_RY;
	}
	if (fdd && fdd->GetTrack0()) {
		st3 |= ST3_T0;
	}
	// TS = fdd.GetTwoSide();
	// HD, US
	st3 |= (cmdbuf.sense_device_status.hd_us & 0x07);

	putlog(2, " ST3=$%02x", st3);
	resbuf.device_status.st3 = st3;
	resbuf.len = sizeof(resbuf.device_status);
	// 1.024 msec は READY のポーリング間隔から推定。
	// 正確な数字は実機調査すること。
	CallAfter(ResultCallback, 1024_usec);
}

void
FDCDevice::CmdWriteData()
{
	UnitSelect(cmdbuf.data.hd_us);

	mt = cmdbuf.code & 0x80;
	mf = cmdbuf.code & 0x40;
	sk = 0;

	hd = (cmdbuf.data.hd_us & 0x04) >> 2;

	if (loglevel >= 2) {
		putlogn(" MT:%u MF:%u SK:%u HD:%u", (uint)mt, (uint)mf, (uint)sk, hd);
		putlogn(" C:%2u H:%u R:%2u N:%u",
			cmdbuf.data.c,
			cmdbuf.data.h,
			cmdbuf.data.r,
			cmdbuf.data.n);
		putlogn(" EOT:%2u GSL:$%02x DTL:$%02x",
			cmdbuf.data.eot,
			cmdbuf.data.gsl,
			cmdbuf.data.dtl);
	}

	XferStart(true);
}

void
FDCDevice::CmdReadData()
{
	UnitSelect(cmdbuf.data.hd_us);

	mt = cmdbuf.code & 0x80;
	mf = cmdbuf.code & 0x40;
	sk = cmdbuf.code & 0x20;

	hd = (cmdbuf.data.hd_us & 0x04) >> 2;

	if (loglevel >= 2) {
		putlogn(" MT:%u MF:%u SK:%u HD:%u", (uint)mt, (uint)mf, (uint)sk, hd);
		putlogn(" C:%2u H:%u R:%2u N:%u",
			cmdbuf.data.c,
			cmdbuf.data.h,
			cmdbuf.data.r,
			cmdbuf.data.n);
		putlogn(" EOT:%2u GSL:$%02x DTL:$%02x",
			cmdbuf.data.eot,
			cmdbuf.data.gsl,
			cmdbuf.data.dtl);
	}

	XferStart(false);
}

void
FDCDevice::XferStart(bool is_write)
{
	resbuf.len = sizeof(resbuf.data);

	if (IsReady() == false) {
		st0.nr = true;
		st0.ic = ST0_IC_AT;
		CallAfter(XferEndCallback, 0);
		return;
	}
	unitinfo[us].ready = true;
	assert(fdd);

	tc = false;
	dack = false;
	dreg.Clear();
	xfer_start = false;
	xfer_enable = false;
	xfer_write = is_write;
	index_detected = 0;

	fdd->ReadTrack(hd);

	ExecPhase();

	// FDD がバイトを読み書きするちょうど真ん中のタイミングで動くように
	// 同期してみる。
	uint64 now = scheduler->GetVirtTime();
	uint64 t = 16_usec - ((now - unitinfo[us].ready_time) % 16_usec);
	t += 8_usec;

	CallAfter(XferStartCallback, t);
}

void
FDCDevice::XferStartCallback(Event *ev)
{
	// マーク位置まで待つ。
	// マークを読んでみてディスパッチ。
	// IAM ならなにもしない。
	// IDAM なら CHRN を読み出す。データ転送が開始されていない時は
	// R を検査して、一致したら転送開始状態にしておく。
	// DAM/DDAM のときは、転送開始状態でなければ何もしない。
	// 転送開始状態の時はデータ転送またはエラー処理。
	// 転送が終わったら CHR を進めて EOT になったら
	//  MT=1 で HD=0 のときは裏に行く。
	//  そうでなければ EN エラー。

	assert(fdd);

	int dist = fdd->GetDistanceToMark();
	if (dist < 0) {
		VMPANIC("dist < 0");
	}
	if (dist == 0) {
		uint8 mark = fdd->ReadByte();
		ev->time = 1 * 16_usec;
		if (mark == MARK_IDAM) {
			// CHRN と CRC の合計時間を加算
			ev->time += (4 + 2) * 16_usec;

			resbuf.data.c = fdd->PeekByte(1);
			resbuf.data.h = fdd->PeekByte(2);
			resbuf.data.r = fdd->PeekByte(3);
			resbuf.data.n = fdd->PeekByte(4);
			putlog(2, "IDAM C:%2u H:%u R:%2u N:%u",
				resbuf.data.c,
				resbuf.data.h,
				resbuf.data.r,
				resbuf.data.n);

			// READ ID は最初に発見した ID を返す。
			if ((cmdbuf.code & 0xbf) == 0x0a) {
				putlog(2, "READ ID complete");
				st0.ic = ST0_IC_NT;
				CallAfter(XferEndCallback, ev->time);
				return;
			}

			// uPD765 マニュアル pXX に R だけを一致検査すると書いてある。
			// 72065B でどうかは未調査だが、CHRN 4つ比較することにする。
			if ((resbuf.data.c == cmdbuf.data.c) &&
			    (resbuf.data.h == cmdbuf.data.h) &&
			    (resbuf.data.r == cmdbuf.data.r) &&
			    (resbuf.data.n == cmdbuf.data.n))
			{
				putlog(3, "Transfer decided");
				xfer_start = true;
				sect_remain = 1U << (7 + resbuf.data.n);
				if (cmdbuf.data.n == 0) {
					xfer_remain = cmdbuf.data.dtl;
					if (xfer_remain > 128) {
						xfer_remain = 128;
					}
				} else {
					xfer_remain = sect_remain;
				}
				// CRC のぶん
				sect_remain += 2;
			}
		} else if (mark == MARK_DAM) {
			xfer_enable = xfer_start;
			if (xfer_enable) {
				putlog(3, "DAM detected. Sector data will be transferred");
				if (xfer_write) {
					RequestDataByte();
				}
				CallAfter(XferDataCallback, ev->time);
				return;
			} else {
				putlog(3, "DAM detected. Sector data will be skipped");
			}
		} else if (mark == MARK_DDAM) {
			VMPANIC("DDAM (NOT IMPLEMENTED)");
		} else if (mark == MARK_IAM) {
			// IAM は何もすることがない
		} else {
			// インデックスホールを都合3回検出したらエラー終了
			if (fdd->IsIndex()) {
				index_detected++;
				if (index_detected == 3) {
					st0.ic = ST0_IC_AT;
					if (xfer_start) {
						st1.nd = true;
					} else {
						st1.ma = true;
					}
					CallAfter(XferEndCallback, ev->time);
					return;
				}
			}
		}
	} else {
		// 次のマークまで待つ
		ev->time = dist * 16_usec;
	}
	scheduler->StartEvent(ev);
}

void
FDCDevice::XferDataCallback(Event *ev)
{
	assert(fdd);

	ev->time = 1 * 16_usec;

	// 変態フォーマットをサポートするときはこのへんに追加が必要。

	if (xfer_remain > 0) {
		if (__predict_false(xfer_write)) {
			uint32 data = PopDreg();
			if ((int32)data >= 0) {
				fdd->WriteByte(data);
			} else {
				// XXX: こうじゃなくてエラービットだけ立ててセクタ終了時処理かも
				return;
			}
		} else {
			PushDreg(fdd->ReadByte());
		}
		xfer_remain--;
	}

	bool is_eot = false;

	if (sect_remain > 0) {
		sect_remain--;
	} else {
		// セクタデータ転送完了
		putlog(3, "Sector data transfer completed");
		// TODO: Data CRC チェック

		xfer_start = false;
		xfer_enable = false;

		is_eot = (resbuf.data.r >= cmdbuf.data.eot);

		// マルチトラックの処理
		if (is_eot) {
			if (xfer_write) {
				fdd->WriteTrack();
			}
			resbuf.data.r = 1;
			if (mt && hd == 0) {
				is_eot = false;

				// HD=1 の読み込み
				fdd->ReadTrack(1);
			} else {
				resbuf.data.c += 1;
			}
			if (mt) {
				// HD=1 から MT を立てて読み終わると HD=0 になっている
				hd ^= 1;
				resbuf.data.h = hd;
			}
		} else {
			resbuf.data.r += 1;
		}

		// EOT チェック
		if (tc == false) {
			if (is_eot) {
				putlog(3, "EOT reached");
				st1.en = true;
				st0.ic = ST0_IC_AT;
				CallAfter(XferEndCallback, 16_usec);
				return;
			} else {
				// 次のセクタの ID へ継続
				putlog(3, "Multi sector; continue to transfer");
				cmdbuf.data.c = resbuf.data.c;
				cmdbuf.data.h = resbuf.data.h;
				cmdbuf.data.r = resbuf.data.r;
				cmdbuf.data.n = resbuf.data.n;
				CallAfter(XferStartCallback, 16_usec);
				return;
			}
		}
	}

	if (tc) {
		putlog(3, "Done by TC signal");
		if (xfer_write) {
			fdd->WriteTrack();
		}
		st0.ic = ST0_IC_NT;
		CallAfter(XferEndCallback, 16_usec);
		return;
	}

	if (xfer_write && xfer_remain > 0) {
		RequestDataByte();
	}
	scheduler->StartEvent(ev);
}

void
FDCDevice::XferEndCallback(Event *ev)
{
	st0.hd = hd;
	st0.us = us;
	resbuf.data.st0 = GetST0();
	resbuf.data.st1 = GetST1();
	resbuf.data.st2 = GetST2();
	ChangeInterrupt();
	ResultPhase();
}

// READY 入力端子
bool
FDCDevice::IsReady() const
{
	return force_ready || (fdd && fdd->GetReady());
}

void
FDCDevice::CmdRecalibrate()
{
	UnitSelect(cmdbuf.recalibrate.us);

	if (IsReady() == false) {
		st0.nr = true;
		st0.ic = ST0_IC_AT;
		st0.hd = false;
		st0.us = us;
		ChangeInterrupt();
		CommandPhase();
		return;
	}
	unitinfo[us].ready = true;

	msr.db[us] = true;
	unitinfo[us].pcn = 255;
	unitinfo[us].ncn = 0;
	unitinfo[us].state = UnitState::Recalibrate;
	CommandPhase();
	StartPoll();
}

// シーク終了処理
void
FDCDevice::SeekEnd()
{
	putlog(2, "Seek completed");
	st0.ic = ST0_IC_NT;
	st0.se = true;
	st0.us = us;
	unitinfo[us].state = UnitState::Idle;
	ChangeInterrupt();
	CommandPhase();
}

void
FDCDevice::CmdSenseInterruptStatus()
{
	// XXX とりあえずここで割り込みを下げる。
	//     実際には割り込み状態をラッチして信号線に出すというコマンドで
	//     割り込み要因がオフになっていればそれによってネゲートされる
	//     ということのようだ。

	// XXX 割り込みが起きていないときは INVALID と同じらしい

	if (st0.ic != ST0_IC_IC) {
		// シーク完了割り込みを発生させた Unit のトラックをレポート
		resbuf.interrupt_status.pcn = unitinfo[st0.us].pcn;
		msr.db[st0.us] = false;
		resbuf.len = sizeof(resbuf.interrupt_status);
	} else {
		resbuf.len = sizeof(resbuf.invalid);
	}
	resbuf.interrupt_status.st0 = GetST0();
	putlog(2, " ST0=$%02x", resbuf.interrupt_status.st0);
	ResultPhase();
}

void
FDCDevice::CmdWriteDeletedData()
{
	CmdNotImplemented();
}

void
FDCDevice::CmdReadID()
{
	UnitSelect(cmdbuf.data.hd_us);

	mf = cmdbuf.code & 0x40;
	hd = (cmdbuf.data.hd_us & 0x04) >> 2;
	putlog(2, " MF:%u HD:%u", (uint)mf, hd);

	XferStart(false);
}

void
FDCDevice::CmdReadDeletedData()
{
	CmdNotImplemented();
}

void
FDCDevice::CmdWriteID()
{
	CmdNotImplemented();
}

void
FDCDevice::CmdSeek()
{
	putlog(2, "SEEK start");

	UnitSelect(cmdbuf.seek.us);

	if (IsReady() == false) {
		st0.nr = true;
		st0.ic = ST0_IC_AT;
		st0.hd = false;
		st0.us = us;
		ChangeInterrupt();
		CommandPhase();
		return;
	}
	unitinfo[us].ready = true;

	msr.db[us] = true;
	unitinfo[us].ncn = cmdbuf.seek.ncn;
	unitinfo[us].state = UnitState::Seek;
	putlog(2, " NCN:%u", unitinfo[us].ncn);
	CommandPhase();
	StartPoll();
}

void
FDCDevice::CmdVersion()
{
	CmdNotImplemented();
}

void
FDCDevice::CmdScanEqual()
{
	CmdNotImplemented();
}

void
FDCDevice::CmdScanLowOrEqual()
{
	CmdNotImplemented();
}

void
FDCDevice::CmdScanHighOrEqual()
{
	CmdNotImplemented();
}

void
FDCDevice::CmdResetStandby()
{
	CmdNotImplemented();
}

void
FDCDevice::CmdSetStandby()
{
	CmdNotImplemented();
}

void
FDCDevice::CmdSoftwareReset()
{
	ResetHard(false);
}

void
FDCDevice::CmdInvalid()
{
	CmdNotImplemented();
}

void
FDCDevice::CmdNotImplemented()
{
	UnitSelect(cmdbuf.data.hd_us);
	hd = (cmdbuf.data.hd_us & 0x04) >> 2;

	putlog(0, "Command %s NOT IMPLEMENTED", cmd->name);
	// とりあえずエラー
	st0.ic = ST0_IC_AT;
	st0.hd = hd;
	st0.us = us;
	ChangeInterrupt();
	CommandPhase();
}

// コマンド
#define CMD(x) &FDCDevice::Cmd##x
/*static*/ const std::vector<FDCDevice::FDCCmd>
FDCDevice::cmd_list {
	// code mask cmdlen func					name
	{ 0x02, 0xbf, 9, CMD(ReadDiagnostic),		"READ DIAGNOSTIC" },
	{ 0x03, 0xff, 3, CMD(Specify),				"SPECIFY" },
	{ 0x04, 0xff, 2, CMD(SenseDeviceStatus),	"SENSE DEVICE STATUS" },
	{ 0x05, 0x3f, 9, CMD(WriteData),			"WRITE DATA" },
	{ 0x06, 0x1f, 9, CMD(ReadData),				"READ DATA" },
	{ 0x07, 0xff, 2, CMD(Recalibrate),			"RECALIBRATE" },
	{ 0x08, 0xff, 1, CMD(SenseInterruptStatus),	"SENSE INTERRUPT STATUS" },
	{ 0x09, 0x3f, 9, CMD(WriteDeletedData),		"WRITE DELETED DATA" },
	{ 0x0a, 0xbf, 2, CMD(ReadID),				"READ ID" },
	{ 0x0c, 0x1f, 9, CMD(ReadDeletedData),		"READ DELETED DATA" },
	{ 0x0d, 0xbf, 6, CMD(WriteID),				"WRITE ID" },
	{ 0x0f, 0xff, 3, CMD(Seek),					"SEEK" },
	{ 0x10, 0x1f, 1, CMD(Version),				"VERSION" },
	{ 0x11, 0x1f, 9, CMD(ScanEqual),			"SCAN EQUAL" },
	{ 0x19, 0x1f, 9, CMD(ScanLowOrEqual),		"SCAN LOW OR EQUAL" },
	{ 0x1d, 0x1f, 9, CMD(ScanHighOrEqual),		"SCAN HIGH OR EQUAL" },
	{ 0x34, 0xff, 1, CMD(ResetStandby),			"RESET STANDBY" },
	{ 0x35, 0xff, 1, CMD(SetStandby),			"SET STANDBY" },
	{ 0x36, 0xff, 1, CMD(SoftwareReset),		"SOFTWARE RESET" },

	// リストの最後に Invalid を置く。
	// code=0x00, mask=0x00 なので必ず一致する。
	{ 0x00, 0x00, 1, CMD(Invalid),				"invalid" },
};

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.