File:  [Isaki's NoNo m68k/m88k emulator] / nono / vm / fdd.cpp
Revision 1.1.1.7 (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) 2022 nono project
// Licensed under nono-license.txt
//

//
// FDD
//

// ディスクメディアは 77~ 個のシリンダで構成される。
// シリンダは両面の場合 2 本のトラックで構成される。
// トラックは 8~ 個のセクタで構成される。
// セクタは 128~ バイトで構成される。

// 割り込み状態はこちらでは保持しておらず PEDEC 側で持っている。
// PEDEC への割り込みは AssertINT() するだけで使っている。

#include "fdd.h"
#include "config.h"
#include "event.h"
#include "fdc.h"
#include "mainapp.h"
#include "pedec.h"
#include "scheduler.h"
#include "uimessage.h"

#define MFM_TRACK_HEADER	(80 + 12 + 3 + 1 + 50)
#define MFM_SECTOR_HEADER	(12 + 3 + 1 + 4 + 2 + 22 + 12 + 3 + 1 + 2)
#define MFM_TRACK_RAW_SIZE	(10416)

//
// フロッピーディスクの 1 トラック
//

// トラックデータの記録開始
void
FDTrack::Begin()
{
	markpos.push_back(buf.size());
}

// トラックデータの記録終了
void
FDTrack::End()
{
	assert(buf.size() == MFM_TRACK_RAW_SIZE);

	markpos.push_back(buf.size());
}

// トラックデータに data を追加
void
FDTrack::Append(uint8 data)
{
	buf.push_back(data);
}

// トラックデータに data を count 個追加
void
FDTrack::Append(uint8 data, int count)
{
	for (int i = 0; i < count; i++) {
		Append(data);
	}
}

// トラックデータにマークを追加
void
FDTrack::AppendMark(uint8 data)
{
	// マーク位置を記録
	markpos.push_back(buf.size());
	Append(data);
}

void
FDTrack::Clear()
{
	buf.clear();
	markpos.clear();
}


//
// フロッピードライブ
//

// コンストラクタ
FDDDevice::FDDDevice(uint unit_)
	: inherited(OBJ_FDD(unit_))
{
	unit = unit_;

	// image はコンストラクタがないので、この辺りで設定。
	image.SetName(GetName());
}

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

// 初期化
bool
FDDDevice::Init()
{
	fdc = GetFDCDevice();
	pedec = GetPEDECDevice();

	const std::string keybody = string_format("fd%u-", unit);
	const std::string imgkey = keybody + "image";
	const std::string wikey  = keybody + "writeignore";

	const ConfigItem& itemimg = gConfig->Find(imgkey);
	const std::string& filename = itemimg.AsString();

	// UI からの通知を受け取るイベントを用意
	scheduler->ConnectMessage(MessageID::FDD_LOAD(unit), this,
		ToMessageCallback(&FDDDevice::LoadMessage));
	scheduler->ConnectMessage(MessageID::FDD_UNLOAD(unit), this,
		ToMessageCallback(&FDDDevice::UnloadMessage));

	auto evman = GetEventManager();
	event = evman->Regist(this,
		ToEventCallback(&FDDDevice::EventCallback),
		GetName());

	// オープン前に writeignore をチェック。
	// この後パス名を処理するとすぐに UI に通知を出すため、それより前で
	// 行わないといけない。うーん。
	write_ignore = gConfig->Find(wikey).AsBool();

	// 設定時点でファイルが指定されていればオープン
	if (filename.empty() == false) {
		std::string path = gMainApp.NormalizePath(filename);
		if (LoadDisk(path) == false) {
			itemimg.Err("Could not open %s", path.c_str());
			return false;
		}
	}

	return true;
}

// リセット
void
FDDDevice::ResetHard(bool poweron)
{
	if (poweron) {
		motor_state = STOPPED;
	}

	// XXX どこでやる?
	eject_enable = true;
}

// モニタの下請け (fdc から呼ばれる)。
// y = 11 以降が各ドライブの状態で、このうち x = 26 以降が FDD の担当。
// y = 16 以降は FDD のアクセスマップ。
void
FDDDevice::MonitorScreenFDD(TextScreen& screen, uint hd)
{
/*        1         2         3         4         5         6         7
0123456789012345678901234567890123456789012345678901234567890123456789012345678
                          ready accled eject   fmt C  H  R  N
                          stop  blink  disable 2HC 99 1 99 2(1024)
Unit0: HD=1 cyl=nn/nn pos=12345/12345
XX>01234567890123456 01234567890123456| XX>01234567890123456 01234567890123456|
*/

	int uy = 11 + unit;
	int ux = 26;

	// モータ状態
	const char * const motor_state_str[] = {
		"Stop",
		"Down",
		"Up",
		"Ready",
	};
	screen.Print(ux, uy, "%s", motor_state_str[(int)motor_state]);

	// アクセス LED 状態
	const char * const access_led_str[] = {
		"Off",
		"Green",
		"Red",
		"Blink",
	};
	screen.Print(ux + 6, uy, "%s", access_led_str[(int)access_led]);

	// イジェクト可否 (LED 状態)
	const char *eject_str;
	if (medium_loaded) {
		if (eject_enable) {
			eject_str = "Enable";
		} else {
			eject_str = "Disable";
		}
	} else {
		eject_str = "Off";
	}
	screen.Puts(ux + 13, uy, eject_str);

	if (medium_loaded) {
		// フォーマットと CHRN
		screen.Print(ux + 20, uy, "%4s %2u %u %2u %u(%u)",
			format_name.c_str(), ncyl, nhead, nsect, format_n, sectsize);
	}

	uy = 16 + (unit / 2) * 10;
	ux = (unit % 2) * 41;

	screen.Print(ux, uy++, "Unit %u", unit);
	// 横ガイド
	for (int i = 0; i < 8; i++) {
		screen.Puts(ux + 2,  uy + i, "|");
		screen.Puts(ux + 38, uy + i, "|");
	}

	// メディアが入ってなければここで終わり
	if (IsMediumLoaded() == false) {
		return;
	}

	TA selattr = selected ? TA::Em : TA::Normal;
	screen.Print(ux, uy - 1, selattr, "Unit %u: HD=%u cyl=%2u/%u pos=%5u/%u",
		unit, hd, cylinder, ncyl, pos, MFM_TRACK_RAW_SIZE);

	// ヘッドの現在位置。
	// 横は全長を当分割、縦は1行で10トラック固定にする
	int headx = (double)pos / MFM_TRACK_RAW_SIZE * 17;
	int heady = (double)cylinder / 10;

	// 片面ディスクはサポートしていないが一応
	for (int h = 0; h < nhead; h++) {
		for (int i = 0; i < 8; i++) {
			TA trkattr = (__predict_false(selected && i == heady && h == hd))
				? TA::Em : TA::Normal;
			screen.Puts(ux + h * 18 + 3, uy + i, trkattr,
			//   01234567890123456
				".................");
		}
	}

	// ヘッド位置
	screen.Print(ux, uy + heady, selattr, "%2u>", cylinder);
	screen.Puts(ux + hd * 18 + 3 + headx, uy + heady, selattr, "#");
}

// READY 信号
bool
FDDDevice::GetReady() const
{
	return (motor_state == READY);
}

// TRACK00 信号
bool
FDDDevice::GetTrack0() const
{
	return (cylinder == 0);
}

bool
FDDDevice::GetWriteProtect() const
{
	return write_mode == RW::WriteProtect;
}

void
FDDDevice::DoSeek(int dir)
{
	if ((dir >= 0 && cylinder < 80) || (dir < 0 && cylinder > 0)) {
		cylinder += dir;
	}
}

// ドライブセレクト入力 (DRIVE_SELECT, DISK_TYPE_SELECT 信号)
void
FDDDevice::SetDriveSelect(uint sel, bool is_2dd_)
{
	if (is_2dd_) {
		putlog(0, "Select 2DD (NOT IMPLEMENTED)");
	}

	selected = (sel == unit);

	ChangeStatus();
}

// MOTOR_ON 信号入力
void
FDDDevice::SetMotorOn(bool motor_on_)
{
	motor_on = motor_on_;
	ChangeStatus();
}

void
FDDDevice::EventCallback(Event *ev)
{
	// モータが回転している間は1バイトずつ進める
	pos++;
	if (pos >= MFM_TRACK_RAW_SIZE) {
		pos = 0;
	}

	switch (motor_state) {
	 case READY:
		scheduler->StartEvent(ev);
		break;

	 case SPIN_UP:
		// 増速中
		if (event->time == 16_usec) {
			motor_state = READY;
			// READY になったので FDC に通知
			fdc->ReadyChanged(unit, true);
		} else {
			if (event->time < 16_usec) {
				// オーバーシュート対応
				event->time = 16_usec;
			} else {
				event->time /= 2;
			}
			putlog(4, "SPIN_UP next=%uusec", (uint)tsec_to_usec(event->time));
		}
		scheduler->StartEvent(ev);
		break;

	 case SPIN_DOWN:
		if (event->time >= 262144_usec) {
			motor_state = STOPPED;
			// イベント停止、ここでは time も 0 クリアしておく。
			event->time = 0;
		} else {
			event->time *= 2;
			scheduler->StartEvent(ev);
			putlog(4, "SPIN_DOWN next=%uusec", (uint)tsec_to_usec(event->time));
		}
		break;

	 default:
		break;
	}
}

// ドライブの状態が変わった。
// メディア挿入、排出、ドライブセレクト変更など。
void
FDDDevice::ChangeStatus()
{
	bool active = medium_loaded && motor_on;

	// モータ動作の決定
	if (IsMotorActive() == false && active) {
		// ディスクが挿入されていて、オフ -> オン
		DoMotorOn();
	} else if (IsMotorActive() && active == false) {
		// オン -> オフ
		DoMotorOff();
	}

	// アクセス LED の決定
	if (active && selected) {
		access_led = LED::RED;
	} else {
		if (medium_loaded) {
			access_led = LED::GREEN;
		} else {
			if (is_blink) {
				access_led = LED::BLINK;
			} else {
				access_led = LED::OFF;
			}
		}
	}
}

void
FDDDevice::DoMotorOn()
{
	putlog(1, "Motor On");

	if (motor_state == STOPPED) {
		// 始動。
		// 262144_usec/1バイトの回転速度から2倍ずつ速くしていって
		// 16_usec/1バイトまで上げると定常状態とする。
		// 仕様では 500msec で READY になるとあるが、ここでは 2 のべき乗を
		// 使っているので、約 524msec かかる。細かいことは気にしない。
		motor_state = SPIN_UP;
		event->time = 262144_usec;
		putlog(4, "SPIN_UP next=%uusec", (uint)tsec_to_usec(event->time));
	} else if (motor_state == SPIN_DOWN) {
		// 細かいことは無視して、増速に転じる
		motor_state = SPIN_UP;
	} else {
		VMPANIC("DoMotorOn called on motor_state=%u", (uint)motor_state);
	}

	scheduler->RestartEvent(event);
}

void
FDDDevice::DoMotorOff()
{
	putlog(1, "Motor Off");

	if (medium_loaded == false) {
		// 物理的には多少モータは慣性回転するはずではあるが、ディスクは
		// もうないので。
		motor_state = STOPPED;
		fdc->ReadyChanged(unit, false);
	} else if (motor_state == READY) {
		// 減速に転じる。
		// 細かいことは気にせず増速と同様に倍々で減速していくので停止まで
		// 約 524msec かかる。細かいことは気にしない。
		motor_state = SPIN_DOWN;
		// 定常回転 (READY) ではなくなった
		fdc->ReadyChanged(unit, false);
	} else if (motor_state == SPIN_UP) {
		// 細かいことは無視して、減速に転じる。
		motor_state = SPIN_DOWN;
	} else {
		VMPANIC("DoMotorOff called on motor_state=%u", (uint)motor_state);
	}

	scheduler->RestartEvent(event);
}

// ドライブコントロールのセット
// DRIVE # が '1' -> '0' に変化したときのみ呼び出される。
void
FDDDevice::SetDriveCtrl(uint8 data)
{
	putlog(2, "Drive Control <- $%02x", data);

	// ログレベルが 1 なら変化時だけ、2 以上なら試行を全部表示?
	auto islog = [&](bool expr) {
		if (loglevel >= 2) {
			return true;
		} else if (loglevel == 1) {
			if (expr) {
				return true;
			}
		}
		return false;
	};

	// アクセスランプ点滅 (メディアが入っていない時のみ)
	if (medium_loaded == false) {
		// LED を点滅させておいてディスクを入れたら LED は点灯になるはず。
		// そのままの状態でディスクを抜いたとき LED が点滅に戻るのか
		// 消灯するのかは未調査。現状は点滅に戻るように実装してある。

		if ((data & 0x80)) {
			if (islog(is_blink == false)) {
				putlogn("LED Blink");
			}
			blink_start = scheduler->GetVirtTime();
			is_blink = true;
		} else {
			if (islog(is_blink == true)) {
				putlogn("LED Off");
			}
			is_blink = false;
		}
	} else {
		if ((data & 0x40)) {
			if (islog(eject_enable == true)) {
				putlogn("Eject button disabled");
			}
			eject_enable = false;
		} else {
			if (islog(eject_enable == false)) {
				putlogn("Eject button enabled");
			}
			eject_enable = true;
		}

		if ((data & 0x20)) {
			if (islog(medium_loaded == true)) {
				putlogn("Eject");
			}
			UnloadDisk(true);
		}
	}
	ChangeStatus();
}

// 1 バイト取得
uint8
FDDDevice::ReadByte()
{
	return trackbuf.buf[pos];
}

// 1 バイト書き込み
void
FDDDevice::WriteByte(uint8 data)
{
	trackbuf.buf[pos] = data;
}

// 折り返してはいけない
uint8
FDDDevice::PeekByte(int offset) const
{
	return trackbuf.buf[pos + offset];
}

// 次のマークまたはインデックスホールまでのバイト距離を返す。
// 見つからないときは -1 を返す。
int
FDDDevice::GetDistanceToMark() const
{
	if (pos == 0) {
		return 0;
	}
	for (int i = 0; i < trackbuf.markpos.size(); i++) {
		if (pos <= trackbuf.markpos[i]) {
			return trackbuf.markpos[i] - pos;
		}
	}
	return -1;
}

// 現在のシリンダ位置の、hd で指定したトラックを読み込む
void
FDDDevice::ReadTrack(int hd)
{
	uint32 lba = (cylinder * nhead + hd) * nsect;

	uint32 start = lba * sectsize;
	uint32 len = nsect * sectsize;

	std::vector<uint8> buf(len);
	image.Read(buf.data(), start, len);

	int bufpos = 0;

	// MFM のみサポート

	trackbuf.Clear();
	trackbuf.lba = lba;

	// トラックデータ
	// 本当は FDC が SYNC を検出しているのだが、
	// AppendSync() によって SYNC の位置を別途記録することで対応している。

	trackbuf.Begin();

	// トラックヘッダ
	trackbuf.Append(0x4e, 80);		// Gap4a
	trackbuf.Append(0x00, 12);		// SYNC
	trackbuf.Append(0xc2, 3);		// IAM
	trackbuf.AppendMark(MARK_IAM);	// IAM マーク位置
	trackbuf.Append(0x4e, 50);		// Gap1

	for (int i = 0; i < nsect; i++) {
		uint8 c = cylinder;			// C は 0 始まり
		uint8 h = hd;				// H は 0 か 1
		uint8 r = i + 1;			// R は 1 始まり
		uint8 n = format_n;
		uint8 crc[2] {};			// XXX CRC は未実装
		uint8 datacrc[2] {};

		trackbuf.Append(0x00, 12);		// SYNC
		trackbuf.Append(0xa1, 3);		// IDAM
		trackbuf.AppendMark(MARK_IDAM);
		trackbuf.Append(c);
		trackbuf.Append(h);
		trackbuf.Append(r);
		trackbuf.Append(n);
		trackbuf.Append(crc[0]);
		trackbuf.Append(crc[1]);
		trackbuf.Append(0x4e, 22);	// Gap2

		trackbuf.Append(0x00, 12);		// SYNC
		trackbuf.Append(0xa1, 3);	// DAM/DDAM
		if (true/*dam*/) {
			trackbuf.AppendMark(MARK_DAM);
		} else {
			trackbuf.AppendMark(MARK_DDAM);
		}

		for (int j = 0; j < sectsize; j++) {
			trackbuf.Append(buf[bufpos++]);
		}
		trackbuf.Append(datacrc[0]);
		trackbuf.Append(datacrc[1]);

		trackbuf.Append(0, gap3);	// Gap3
	}
	trackbuf.Append(0, gap4b);		// Gap4b

	trackbuf.End();			// トラック終わり
}

// トラックバッファをイメージにフラッシュ
void
FDDDevice::WriteTrack()
{
	uint32 lba = trackbuf.lba;

	uint32 start = lba * sectsize;
	uint32 len = nsect * sectsize;

	std::vector<uint8> buf(len);

	// MFM のみサポート

	int bufpos = 0;

	// トラックデータからデータを再構成

	for (int i = 0; i < trackbuf.markpos.size(); i++) {
		int p = trackbuf.markpos[i];
		if (p < trackbuf.buf.size()) {
			uint8 d = trackbuf.buf[p];
			if (d == MARK_DAM || d == MARK_DDAM) {
				p++;
				for (int j = 0; j < sectsize; j++) {
					buf[bufpos++] = trackbuf.buf[p++];
				}
			}
		}
	}
	if (bufpos != len) {
		VMPANIC("invalid track");
	}

	image.Write(buf.data(), start, len);
}

// バックエンドのディスクイメージを開く。
// 成功すれば true、失敗すれば false を返す。(ロードされれば true を返す
// のではないが、medium_loaded が同じ値になるので使いまわしている)
bool
FDDDevice::LoadDisk(const std::string& pathname_)
{
	bool ejected;
	off_t size;
	int w_ok;
	bool read_only;

	// すでにあればクローズ (ここでは通知は行わない)
	ejected = UnloadDisk_internal();

	// 新しいイメージをオープン(する準備)
	pathname = pathname_;
	if (image.CreateHandler(pathname) == false) {
		goto done;
	}

	w_ok = image.IsWriteable();
	if (w_ok < 0) {
		goto done;
	}

	// 書き込みモードをここで確定
	if (w_ok) {
		if (write_ignore) {
			write_mode = RW::WriteIgnore;
		} else {
			write_mode = RW::Writeable;
		}
	} else {
		write_mode = RW::WriteProtect;
	}

	// ここでオープン
	read_only = (GetWriteMode() != RW::Writeable);
	if (image.Open(read_only, write_ignore) == false) {
		goto done;
	}

	size = image.GetSize();
	if (size == 1261568) {
		// Human68k 2HD フォーマットとみなす
		// 77track, 2side, 8sector, 1024bytes/sector
		format_name = "2HD";
		ncyl = 77;
		nhead = 2;
		nsect = 8;
		sectsize = 1024;
		format_n = 3;
		gap3 = 0x74;
	} else if (size == 1228800) {
		// NetBSD 2HC フォーマットとみなす
		// 80track, 2side, 15sector, 512bytes/sector
		format_name = "2HC";
		ncyl = 80;
		nhead = 2;
		nsect = 15;
		sectsize = 512;
		format_n = 2;
		gap3 = 0x54;
	} else {
		warnx("%s: Unsuppoted image, size(%ju)",
			pathname.c_str(), (uintmax_t)size);
		goto done;
	}

	gap4b = (MFM_TRACK_RAW_SIZE - MFM_TRACK_HEADER)
		- (sectsize + MFM_SECTOR_HEADER + gap3) * nsect;

	// 書き込み無視なら一応ログ出力
	if (GetWriteMode() == RW::WriteIgnore) {
		putmsg(0, "write is ignored");
	}

	medium_loaded = true;
	putmsg(0, "%s format", format_name.c_str());
	putlog(1, "Medium loaded");

	// オートリキャリブレート
	cylinder = 0;

 done:
	if (medium_loaded == false) {
		// ここで状態をクリアする
		Clear();
	}

	// 変化があれば
	if (ejected || medium_loaded) {
		ChangeStatus();
		pedec->AssertINT(this);
		// 通知
		MediaChanged();
	}

	return medium_loaded;
}

// バックエンドのディスクイメージを閉じる。
// オープンされてなければ何もしない。
void
FDDDevice::UnloadDisk(bool force)
{
	if (eject_enable || force) {
		if (UnloadDisk_internal()) {
			// 実際に閉じたら通知
			MediaChanged();
		}
	}
}

// バックエンドのディスクイメージを閉じる (内部用)。
// 実際に閉じれば true を返す。オープンされてなければ何もせず false を返す。
// ここでは通知は行わない。
bool
FDDDevice::UnloadDisk_internal()
{
	if ((bool)image == false) {
		return false;
	}

	pedec->AssertINT(this);

	Clear();

	// 取り出し禁止はされてても解除する
	if (eject_enable == false) {
		eject_enable = true;
	}

	putlog(1, "Medium unloaded");
	return true;
}

// ディスク状態をクリアする
// (メディアを取り外した時など)
void
FDDDevice::Clear()
{
	medium_loaded = false;
	ChangeStatus();
	image.Close();
	pathname.clear();
	write_mode = RW::Writeable;
}

// メディア状態変更を UI に通知する
void
FDDDevice::MediaChanged() const
{
	gMainApp.GetUIMessage()->Post(UIMessage::FDD_MEDIA_CHANGE, unit);
}

// メディアを挿入する。
// UI スレッドで実行されるので、VM スレッドに通知するだけ。
void
FDDDevice::LoadDiskUI(const std::string& pathname_)
{
	new_pathname = pathname_;
	scheduler->SendMessage(MessageID::FDD_LOAD(unit));
}

// メディアを排出する。
// UI スレッドで実行されるので、VM スレッドに通知するだけ。
void
FDDDevice::UnloadDiskUI(bool force)
{
	scheduler->SendMessage(MessageID::FDD_UNLOAD(unit), force);
}

// メディア挿入メッセージコールバック
void
FDDDevice::LoadMessage(MessageID msgid, uint32 arg)
{
	if (LoadDisk(new_pathname) == false) {
		// 失敗したら UI に通知
		gMainApp.GetUIMessage()->Post(UIMessage::FDD_MEDIA_FAILED);
	}
}

// メディア排出メッセージコールバック
void
FDDDevice::UnloadMessage(MessageID msgid, uint32 arg)
{
	bool force = arg;
	UnloadDisk(force);
}

// アクセス LED の状態を取得。
// ここで返すのは OFF, GREEN, RED の3通り。
FDDDevice::LED
FDDDevice::GetAccessLED() const
{
	if (access_led == LED::BLINK) {
		// 点滅状態はここで作成。
		// 点滅開始から 0.5秒は点灯、次の 0.5秒を消灯とする。
		// XXX 間隔は適当
		uint64 time = scheduler->GetVirtTime() - blink_start;
		time %= 1_sec;
		if (time < 500_msec) {
			return LED::GREEN;
		} else {
			return LED::OFF;
		}
	} else {
		// それ以外はそのまま返せる
		return access_led;
	}
}

// イジェクト LED の状態を取得。点灯なら true
bool
FDDDevice::GetEjectLED() const
{
	// メディアが挿入されていて、イジェクト許可なら点灯。
	return (medium_loaded && eject_enable);
}

unix.superglobalmegacorp.com

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