File:  [Isaki's NoNo m68k/m88k emulator] / nono / vm / scsibus.cpp
Revision 1.1.1.17 (vendor branch): download - view: text, annotated - select for diffs
Wed Apr 29 17:06:00 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
//

//
// SCSI バス
//

// フェーズ遷移についてのメモ。
//
// イニシエータ(この時点ではまだ候補だが)が BSY を立てることで
// (バスフリーフェーズから) アービトレーションフェーズに移る。
// 現行では規定時間後に自動的にイニシエータが勝利する(バスは何もしない)。
// イニシエータが SEL 立てて BSY を下ろすことで、セレクションフェーズに移る。
// この間の遷移は以下の通り。
// BSY SEL
//   0   0 BusFree
//         ↓ イニシエータ(候補)が BSY を立てる
//   1   0 Arbitration (バスは何もしない)
//         ↓ イニシエータが SEL を立てる
//   1   1 Arbitration
//         ↓ イニシエータがデータバスにターゲットをセットして BSY を下ろす
//   0   1 Selection
//
// バスはセレクションフェーズ遷移後、規定時間でターゲットに通知。
// ターゲットは即 BSY を立てるのでバスはこれをイニシエータに通知。
// イニシエータは SEL を下げるのでバスはこれをターゲットに通知。
// この間の遷移は以下の通り。
// BSY SEL
//   0   1 Selection (ターゲットに通知)
//         ↓ ターゲットが BSY を立てる
//   1   1 Selection (イニシエータに通知)
//         ↓ イニシエータがデータバスと SEL を下げる
//   1   0 Transfer (ターゲットに通知)
//
// バスは Transfer 後、規定時間でターゲットに通知。
// ターゲットは転送フェーズ(の最初のコマンドフェーズ) に移行する。

// 転送フェーズについてのメモ。
//
//	  -----> (実線) は関数呼び出し
//	  - - -> (破線) は関数からの戻り
//	  〜〜-> (波線) はイベントコールバックによる呼び出し
//
// マニュアル転送は以下のようになる。
// 基本的にはターゲットが REQ を上げたことをイベントでイニシエータに通知し、
// イニシエータは基本的に関数呼び出しでターゲットを操作することにする。
//
// 非同期転送時の REQ/ACK シーケンスに時間規定はないが、時間を考慮せずに
// 実行してしまうと現実より速い転送が出来てしまう可能性があるので、ここでは
// AssertREQ が TransferReqCB を呼び出すところでタイミングを調整する。
// 具体的には前回の TransferReqCB から 1usec 以上空けるというのはどうか。
//
//	  イニシエータ                        ターゲット
//	  ------------                        ----------
//	                         Event 〜〜-> StartTransferCB
//	                                       ↓
//	                                      REQ を上げる
//	  TransferReqCB <-〜〜〜 Event 〜〜〜
//	  ACK を上げる
//	             +------ AssertACK() ---> TransferCB
//	                                      1バイト送受信
//	                                      REQ を下げる
//	             < - - - - - - - - - - -  return;
//	  (誰かが)
//	  ACK を下げる
//	             +------ NegateACK() ---> TransferAckCB
//	                                      REQ を上げる 〜〜+
//	             < - - - - - - - - - - -  return;          :
//	                                                       :
//	  TransferReqCB <-〜〜〜 Event 〜〜〜〜〜〜〜〜〜〜〜〜+
//
// ハードウェア転送の場合は ACK 一往復を省略して以下のようにする。
// Transfer() (TransferCB()) は ACK が立っていなければハードウェア転送と
// 判断する。
//
//	  イニシエータ                        ターゲット
//	  ------------                        ----------
//	                         Event 〜〜-> StartTransferCB
//	                                       ↓
//	                                      REQ を上げる
//	  TransferReqCB <-〜〜〜 Event 〜〜〜
//	             |
//	             +------- Transfer() ---> TransferCB
//	                                      1バイト送受信
//	                                      REQ を下げる
//	                                      REQ を上げる 〜〜+
//	             < - - - - - - - - - - -  return;          :
//	                                                       :
//	  TransferReqCB <-〜〜〜 Event 〜〜〜〜〜〜〜〜〜〜〜〜+
//

#include "scsibus.h"
#include "scsidev.h"
#include "scsidomain.h"
#include "event.h"
#include "scheduler.h"

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

// コンストラクタ
SCSIBus::SCSIBus(SCSIDomain *domain_)
	: inherited(OBJ_SCSIBUS)
{
	domain = domain_;
}

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

// 初期化
bool
SCSIBus::Init()
{
	// 現時点で管理下にあるデバイス(ターゲット)をバスに接続。
	// イニシエータはこの時点では ID 未定なのでバスには繋がらない。
	Refresh();

	auto evman = GetEventManager();
	// 複数出来たら名前どうするべか
	event = evman->Regist(this, NULL, "SCSIBus");

	return true;
}

// 電源オン/リセット
void
SCSIBus::ResetHard(bool poweron)
{
	if (poweron) {
		phase = SCSI::Phase::BusFree;
		rst = false;
		req = false;
		ack = false;
		atn = false;
		sel = 0;
		bsy = 0;
		xfer = 0;
		data = 0;

		last_req_time = 0;
		req_wait = 0;
		init_id = -1;
		target_id = -1;
	}

	// ここでのイベントは実際にはイニシエータかターゲットのどちらかが
	// 時間を伴う処理をしている最中であることを示しているものなので、
	// イニシエータがリセットされたこととバスの動作とは本来関係ないのだが
	// ここでは特にイニシエータに次のバイトを転送させる、という目的で
	// タイマーイベントが使われており、本体をリセットしたのに入れ違いで
	// イニシエータの転送処理が呼び出されてしまうというような事故を
	// 防ぐためにもここでタイマーを停止する。
	scheduler->StopEvent(event);
}

// デバイス情報を更新する。
// domain の device[] が変更されたら呼ぶこと。
void
SCSIBus::Refresh()
{
	domain->GetDevices(&device);
}

// RST を上げる
void
SCSIBus::AssertRST()
{
	putlog(4, "%s", __func__);

	// すでに上がってたら何もしない
	if (rst) {
		return;
	}

	rst = true;

	// 即バスリセットを全員に通知
	for (int i = 0; i < device.size(); i++) {
		if (device[i]) {
			device[i]->OnBusReset();
		}
	}

	// コールバック終了後に内部状態もクリア
	init_id = -1;
	target_id = -1;
}

// RST を下げる
void
SCSIBus::NegateRST()
{
	putlog(4, "%s", __func__);

	rst = false;

	// 特にすることはない
}

// SEL を上げる
void
SCSIBus::AssertSEL(uint id)
{
	putlog(4, "%s(#%u)", __func__, id);

	// すでに上がってたら何もしない
	if ((sel & (1U << id))) {
		return;
	}

	sel |= (1U << id);

	if (GetPhase() == SCSI::Phase::BusFree) {
		// バスフリーフェーズなら、アービトレーションなしでセレクション
		// フェーズに移行。
		//
		// 仕様Phase      |Free |<---- Selection ---->|
		//
		// BSY       ~~\__________________________/~~~~~~~
		//
		// Data      -----------< Initiator&Target ID >---
		//
		// SEL       ________________/~~~~~~~~~~~~~~~~\___
		//                           ^イマココ
		//
		// 実装Phase      |<- Free ->|<-- Selection ->|
		//
		// SCSI 本にはデータバスが変化したところから Selection フェーズと
		// 書いてあるように読めるけど(上)、ここでは SEL がアサートされた
		// ところからとする(下)。

		SetPhase(SCSI::Phase::Selection);

		// SEL を上げた人がイニシエータ
		init_id = id;

		// 即セレクション開始。
		CallAfter(SelectionStart, "SCSIBus Selection Start", 0);

	} else if (GetPhase() == SCSI::Phase::Arbitration) {
		// アービトレーションフェーズなら、
		// アービトレーションに勝ってセレクションフェーズに移行途中。
		// この場合はセレクションフェーズに移行するのはここではなく
		// BSY の立ち下がりなので、そっち参照。
		//
		// 仕様Phase Arbitration |---|<--- Selection -->|
		//
		// BSY       /~~~~~~~~~~~~~~~\_______________/~~~~~~
		//
		// Data      <InitiatorID>< Initiator&Target ID >---
		//
		// SEL       __________/~~~~~~~~~~~~~~~~~~~~~~~~\___
		//                     ^イマココ
		//
		// 実装Phase <-Arbitration ->|<--- Selection -->|

		// SEL を上げた人がイニシエータなことだけはここで覚えておく。
		init_id = id;

	} else {
		// それ以外(転送フェーズ)中に SEL が立てられたらどうすればいいか。
		// これは転送中に LUNA をリセットすると起きる。LUNA の PROM は SPC に
		// 対してバスリセットを発行せずに、セレクションを行おうとするため。
		// SCSI 本には載ってないので分からないが、このような規約違反はたいてい
		// イニシエータが検出してバスフリーかバスリセットを行うことが意図
		// されているんじゃないかと想像するけど、今回そのイニシエータが違反
		// してるので、どうするんだろうね。
		// 賢いターゲットデバイスだとこれを検出して自主的にバスフリーへ落ちる
		// だろうか。賢くないターゲットデバイスは検出せずにそのまま (そして
		// デッドロック) になるだろうか。

#if 1
		// XXX とりあえず LUNA で SCSI がなんか刺さった時に本体リセットでも
		// 直らない状況を真似するため、ターゲットは何もしないでおく。
#else
		// ターゲットデバイスは賢いので、全員イニシエータ様に忖度して
		// 即座にバスフリーへ移行するというのはどうか。
		for (uint i = 0; i < device.size(); i++) {
			if (i != id) {
				NegateBSY(i);
			}
		}
#endif
	}
}

// SEL を下げる
void
SCSIBus::NegateSEL(uint id)
{
	putlog(4, "%s(#%u)", __func__, id);

	// すでに下りてたら何もしない
	if ((sel & (1U << id)) == 0) {
		return;
	}

	sel &= ~(1U << id);

	if (GetPhase() == SCSI::Phase::BusFree) {
		// バスフリーフェーズで SEL を下げる

		// 通常は起きないはずだけど、転送フェーズ中に LUNA をリセットすると
		// -> LUNA の PROM が SEL を上げる (プロトコル違反)
		// -> ターゲットは転送フェーズ中なのに SEL が上がったのを検出して
		//    BSY を取り下げてバスフリーに移行する
		// -> バスフリーがイニシエータに通知され、イニシエータが SEL を下げる
		// となるので、このケースはスルーしてよい。

	} else if (GetPhase() == SCSI::Phase::Selection) {
		// セレクションフェーズで SEL を下げる
		if (bsy) {
			// BSY が立っていれば、ターゲットがセレクションに応答したので
			// イニシエータが SEL を下げて、これから任意時間後にターゲットが
			// 情報転送フェーズに移行する。

			CallAfter(StartTransfer, "SCSIBus Transfer Start", 400_nsec);
		} else {
			// BSY が立ってなければ、ターゲットが応答しなかったことに
			// イニシエータがあきらめて SEL を下げたので、何もせず
			// バスフリーフェーズに移行。
			BusFree(id);
		}
	} else {
		// 下げる分には無視してもいいはずだけど、どうする?
		VMPANIC("NegateSEL in %s phase", GetPhaseName());
	}
}

// BSY を上げる
void
SCSIBus::AssertBSY(uint id)
{
	putlog(4, "%s(#%u)", __func__, id);

	// すでに上がってたら何もしない
	if ((bsy & (1U << id))) {
		return;
	}

	bsy |= (1U << id);

	if (GetPhase() == SCSI::Phase::BusFree) {
		// バスフリーフェーズだったらアービトレーション開始

		// 実際のアービトレーションは、
		// バスフリーを検出してから規定時間経過後に、データバスに自身の ID を
		// 出して BSY を立てる。規定時間内に他のデバイスもアービトレーションに
		// 参加すれば ID の高い方が勝つ。負けた方は規定時間内にアービトレー
		// ションを取り下げる。規定期間経って勝っていればアービトレーション
		// 成功となり、セレクションフェーズに移る。
		// のだが、エミュレータ上では、今の所アービトレーションで競合が起きる
		// ことはないため、アービトレーションは規定時間経過して必ず成功する。
		// ちゃんとしたアービトレーションを実装する場合はたぶんこれだけでは
		// 済まないと思うので書き換えること。

		SetPhase(SCSI::Phase::Arbitration);

	} else if (GetPhase() == SCSI::Phase::Arbitration) {
		// アービトレーションフェーズだったら、アービトレーションに参加。
		// 今は起きない。
		VMPANIC("AssertBSY(#%u) in arbitration", id);

	} else if (GetPhase() == SCSI::Phase::Selection) {
		// セレクションフェーズだったら、転送フェーズに移行中。
		// ターゲットが BSY をアサートしたので次は規定時間経過後に
		// SelectionAck() を呼び出してイニシエータに SEL を下げさせる。

		CallAfter(SelectionAck, "SCSIBus Selection Ack", 90_nsec);
	} else {
		// どうする?
		VMPANIC("AssertBSY(#%u) in %s phase", id, GetPhaseName());
	}
}

// BSY を下げる
void
SCSIBus::NegateBSY(uint id)
{
	putlog(4, "%s(#%u)", __func__, id);

	// すでに下りてたら何もしない
	if ((bsy & (1U << id)) == 0) {
		return;
	}

	bsy &= ~(1U << id);

	if (GetPhase() == SCSI::Phase::Transfer) {
		// 転送フェーズから BSY を下げたらバスフリーに移行。
		BusFree(id);

	} else if (GetPhase() == SCSI::Phase::Arbitration) {
		// アービトレーションフェーズなら、
		// イニシエータが BSY を下ろすことでセレクションフェーズに移行。
		//
		// この少し上の AssertSEL() 中のタイムチャート参照。

		SetPhase(SCSI::Phase::Selection);

		// 即セレクション開始。
		CallAfter(SelectionStart, "SCSIBus Selection Start", 0);

	} else {
		// どうする?
		VMPANIC("NegateBSY(#%u) in %s phase", id, GetPhaseName());
	}
}

// MSG, CD, IO 信号線を変更する。
void
SCSIBus::SetXfer(uint8 val)
{
	uint8 oldxfer = xfer;

	xfer = val & (SCSI::MSG | SCSI::CD | SCSI::IO);
	if (oldxfer != xfer) {
		if (GetPhase() == SCSI::Phase::Transfer) {
			putlog(4, "SetXfer(%x:%s)", xfer, SCSI::GetXferPhaseName(xfer));
		} else {
			putlog(4, "SetXfer(%x)", xfer);
		}
	}
}

// REQ を上げる
void
SCSIBus::AssertREQ()
{
	if (GetPhase() == SCSI::Phase::Transfer) {
		// REQ を上げると言いつつ、ここで転送レートの時間調整をしているので
		// (scsibus.h の解説参照) REQ を実際に上げるのはイベント発生後。
		// XXX 本来ターゲットデバイスがタイマー持っててやるべきだけど、
		// イベントがもう一つ増えるのもうざいので。どのみち、ここの REQ
		// 通知はイベントにしないといけないので。

		uint64 after = 0;
		uint64 now = scheduler->GetVirtTime();
		if (now < last_req_time + 1_usec) {
			after = (last_req_time + 1_usec) - now;
		}
		// ウェイトが指示されていれば入れる (シークタイムなど)
		if (req_wait != 0) {
			after += req_wait;
			req_wait = 0;
		}
		last_req_time = now + after;
		CallAfter(TransferReq, "SCSIBus AssertREQ", after);
	} else {
		// 転送フェーズでないのに上げることはないと思うけど
		// とりあえず通知もせずその場で上げておくか?
		putlog(4, "%s", __func__);
		req = true;
	}
}

// ターゲットが REQ を立てようとして所定時間経ったので REQ を立てて
// イニシエータの OnTransferReq を呼び出すところ。
void
SCSIBus::TransferReq(Event *ev)
{
	putlog(4, "AssertREQ");
	req = true;

	assert(device[init_id]);
	device[init_id]->OnTransferReq();
}

// REQ を下げる
void
SCSIBus::NegateREQ()
{
	putlog(4, "%s", __func__);
	req = false;
}

// ACK を上げる
void
SCSIBus::AssertACK()
{
	putlog(4, "%s", __func__);
	ack = true;

	if (GetPhase() == SCSI::Phase::Transfer) {
		assert(device[target_id]);
		device[target_id]->OnTransfer();
	}
}

// ACK を下げる
void
SCSIBus::NegateACK()
{
	putlog(4, "%s", __func__);
	ack = false;

	if (GetPhase() == SCSI::Phase::Transfer) {
		assert(device[target_id]);
		device[target_id]->OnTransferAck();
	}
}

// ATN を上げる
void
SCSIBus::AssertATN()
{
	putlog(4, "%s", __func__);
	atn = true;
}

// ATN を下げる
void
SCSIBus::NegateATN()
{
	putlog(4, "%s", __func__);
	atn = false;
}

// バスフリーフェーズに移行。
// id はこのバスフリーを行ったデバイス (0..7)
void
SCSIBus::BusFree(uint id)
{
	putlog(4, "%s", __func__);

	SetPhase(SCSI::Phase::BusFree);

	// 即全デバイスに通知。
	for (int i = 0; i < device.size(); i++) {
		if (device[i]) {
			device[i]->OnBusFree(id);
		}
	}

	// コールバック終了後に内部状態もクリア
	init_id = -1;
	target_id = -1;

	// 通知された各デバイスは規定時間内に他の信号線も取り下げなければ
	// ならない。
}

// セレクションから情報転送フェーズまで
//
// 仕様Phase <- Selection ->|
//
// BSY       _______________/~~~~~~~~~
//
// Data      Initiator&Target ID >----
//
// SEL       ~~~~~~~~~~~~~~~~~~~~~~\__
//
//           -(1)->|--(2)-->|-(3)->|
//
// セレクションフェーズ開始後、(ターゲット)デバイスは、SEL がアサート、
// (IO がネゲート)、データバスの自身の ID がアサート、が 400nsec 以上
// 継続することで、自身がターゲットに選択されていることを認識する。(1)
// ターゲットは (1) から 200usec 以内に BSY をアサートする。(2)
// イニシエータは (2) から 90nsec 以上後に SEL (とデータ) を下げる。(3)
//
// 実装では、セレクションフェーズ開始が SelectionStart() の呼び出しに対応。
//
// ターゲットデバイスが存在していない場合はタイムアウトとなるが、これは
// ホストが処理するのでバスは何もしない。
// ターゲットデバイスが存在していれば SelectionStart() から 400nsec 後 +
// 200usec 以内…なので適当に間をとって 100usec 後に Selection() コールバック
// を呼び出す(1+2)。
//
// Selection() コールバック(が呼び出す device->OnSelected()) は通常 BSY を
// アサートするはずであり、そこから 90nsec 後に SelectionAck() コールバック
// を呼び出す(3)。
//
// SelectionAck() コールバック(が呼び出す device->OnSelectionAck()) は通常
// SEL をネゲートするはずであり、そこから情報転送フェーズへの移行には特に
// 規定はなさそうなので、400nsec 後に StartTransfer() をコールバックして
// デバイスを情報転送フェーズに移行させる。

// セレクションフェーズ開始。
void
SCSIBus::SelectionStart(Event *ev)
{
	// この時点でデータバスには自身の ID と相手の ID が立っているはず。
	// 一方 sel は SEL 信号線を立てた人のビットを持っているので、
	// これでターゲットを特定する。
	uint target_mask = data ^ sel;
	if (__builtin_popcount(target_mask) != 1) {
		// データバスから自身を除いた結果立ってるビットが1個以外だったら
		// 誰も応答しないので、何もせず帰ってよい。
		putlog(3, "%s (No target specified; databus=$%02x)", __func__, data);
		return;
	}

	// ターゲット ID はここで取得
	target_id = DecodeID(target_mask);
	if (device[target_id] == NULL) {
		// ターゲットデバイスがいなければ、バスは何もしない。
		// (その結果イニシエータでタイムアウトが起きるはず)
		putlog(4, "%s (No target)", __func__);
		return;
	}

	// ターゲットデバイスがいれば、規定時間経過後ターゲットに通知。
	putlog(4, "%s", __func__);
	CallAfter(Selected, "SCSIBus Selected", 100_usec);
}

// SelectionStart() にてターゲットが選択されて規定時間経ったので
// そろそろターゲットが BSY を立てる頃に呼ばれるコールバック。
void
SCSIBus::Selected(Event *ev)
{
	// ターゲットに通知。ターゲットがいることは確定している。
	assert(device[target_id]);
	device[target_id]->OnSelected();
}

// ターゲットが BSY を立てて規定時間経ったので
// そろそろイニシエータが SEL を下げる頃に呼ばれるコールバック。
void
SCSIBus::SelectionAck(Event *ev)
{
	// イニシエータに通知。
	assert(device[init_id]);
	device[init_id]->OnSelectionAck();
}

// 情報転送フェーズ開始を通知する。
// BSY を上げて情報転送フェーズに移行したところで呼ばれる。
void
SCSIBus::StartTransfer(Event *ev)
{
	SetPhase(SCSI::Phase::Transfer);

	assert(device[target_id]);
	device[target_id]->OnStartTransfer();
}


unix.superglobalmegacorp.com

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