--- nono/vm/scsibus.cpp 2026/04/29 17:04:36 1.1.1.3 +++ nono/vm/scsibus.cpp 2026/04/29 17:05:29 1.1.1.12 @@ -4,29 +4,34 @@ // Licensed under nono-license.txt // +// // SCSI バス +// #include "scsibus.h" #include "scsidev.h" +#include "config.h" +#include "scheduler.h" // time 時間後に呼び出すコールバックをセットする。 // func の書式が面倒なのを省略して書きたいため。 #define CallAfter(func_, name_, time_) do { \ - event.func = (DeviceCallback_t)&SCSIBus::func_; \ - event.SetName(name_); \ - event.code = 0; \ + event.func = ToEventCallback(&SCSIBus::func_); \ event.time = time_; \ - event.Start(); \ + event.SetName(name_); \ + scheduler->RestartEvent(event); \ } while (0) // コンストラクタ -SCSIBus::SCSIBus() +SCSIBus::SCSIBus(SCSIHostDevice *parent_, const char *config_name_) + : inherited(OBJ_SCSIBUS) { - logname = "scsibus"; - devname = "SCSIBus"; + parent = parent_; + config_name = config_name_; - event.dev = this; - event.SetName("SCSIBus"); // 複数出来たら名前どうするべか + monitor.func = ToMonitorCallback(&SCSIBus::MonitorUpdate); + // サイズは Create で決まる + monitor.Regist(ID_MONITOR_SCSIDEVS); } // デストラクタ @@ -34,23 +39,155 @@ SCSIBus::~SCSIBus() { } -// このバスにデバイスを接続する (初期化時に使う) +// 動的なコンストラクション +bool +SCSIBus::Create() +{ + const char *name = GetConfigName(); + + // 設定を読み込む。 + for (int id = 0; id < 8; id++) { + const std::string key = string_format("%s-id%u-image", name, id); + const ConfigItem& item = gConfig->Find(key); + const std::string& image = item.AsString(); + + // 空ならデバイスなし + if (image.empty()) { + // 未接続部分のパラメータは減らしておくか。 + // --show-config するとさすがに無駄に多くて邪魔なので。 + const std::string wikey = string_format("%s-id%u-writeignore", + name, id); + const std::string wpkey = string_format("%s-id%u-writeprotect", + name, id); + const std::string stkey = string_format("%s-id%u-seektime", + name, id); + gConfig->Delete(wikey); + gConfig->Delete(wpkey); + gConfig->Delete(stkey); + continue; + } + + // ',' より前がデバイス種別 (ここではパスは不要) + auto v = string_split(image, ','); + // 比較のため前後の空白を取り除いて小文字にする + std::string type = string_trim(v[0]); + type = string_tolower(type); + + SCSI::DevType target_devtype; + if (type == "hd") { + target_devtype = SCSI::DevType::HD; + } else if (type == "cd") { + target_devtype = SCSI::DevType::CD; + } else if (type == "mo") { + target_devtype = SCSI::DevType::MO; + } else { + item.Err("Invaild device type '%s'", type.c_str()); + return false; + } + target[id].reset(new SCSIDisk(this, id, target_devtype)); + AttachDevice(target[id].get(), id); + } + + // ここの connected_devices はイニシエータを含んでいない数 + monitor.SetSize(75, connected_devices.size() * 4 + 1); + + return true; +} + +// 初期化 bool -SCSIBus::Attach(SCSIDevice *dev, int id) +SCSIBus::Init() +{ + if (inherited::Init() == false) { + return false; + } + + event.SetName("SCSIBus"); // 複数出来たら名前どうするべか + scheduler->RegistEvent(event); + + 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); +} + +// デバイスを接続する。 +// id に先客がいれば false を返す。成功すれば true を返す。 +bool +SCSIBus::AttachDevice(SCSIDevice *dev, int id) { if (device[id] != NULL) { - putlog(0, "id=%d already exist?", id); return false; } - putlog(3, "Attach #%d", id); + putlog(3, "Attach #%u", id); - // バスとデバイスは相互に接続する。どちらも所有のないただのポインタ。 device[id] = dev; - dev->Attach(this, id); + MakeConnectedDevices(); + + // 紐付けをデバイスにも通知。 + dev->OnAttached(this, id); return true; } +// device[] から connected_device を作り直す。 +// device[] はイニシエータを含む固定長の配列。 +// connected_device はイニシエータを (あれば) 含む ID 順の可変長リスト。 +void +SCSIBus::MakeConnectedDevices() +{ + connected_devices.clear(); + + for (int id = 0; id < 8; id++) { + if (device[id]) { + connected_devices.push_back(device[id]); + } + } +} + +// 接続済みの (イニシエータを除く) デバイス数を返す。 +uint +SCSIBus::GetTargetCount() const +{ + uint count = 0; + + for (int id = 0; id < 8; id++) { + if ((bool)target[id]) { + count++; + } + } + + return count; +} + // RST を上げる void SCSIBus::AssertRST() @@ -67,7 +204,7 @@ SCSIBus::AssertRST() // 即バスリセットを全員に通知 for (int i = 0; i < 8; i++) { if (device[i]) { - device[i]->BusResetCB(); + device[i]->OnBusReset(); } } @@ -91,7 +228,7 @@ SCSIBus::NegateRST() void SCSIBus::AssertSEL(uint id) { - putlog(4, "%s(#%d)", __func__, id); + putlog(4, "%s(#%u)", __func__, id); // すでに上がってたら何もしない if ((sel & (1 << id))) { @@ -148,8 +285,29 @@ SCSIBus::AssertSEL(uint id) init_id = id; } else { - // どうする? - PANIC("AssertSEL in %s phase\n", GetPhaseName()); + // それ以外(転送フェーズ)中に SEL が立てられたらどうすればいいか。 + // これは転送中に LUNA をリセットすると起きる。LUNA の PROM は SPC に + // 対してバスリセットを発行せずに、セレクションを行おうとするため。 + // SCSI 本には載ってないので分からないが、このような規約違反はたいてい + // イニシエータが検出してバスフリーかバスリセットを行うことが意図 + // されているんじゃないかと想像するけど、今回そのイニシエータが違反 + // してるので、どうするんだろうね。 + // 賢いターゲットデバイスだとこれを検出して自主的にバスフリーへ落ちる + // だろうか。賢くないターゲットデバイスは検出せずにそのまま (そして + // デッドロック) になるだろうか。 + +#if 1 + // XXX とりあえず LUNA で SCSI がなんか刺さった時に本体リセットでも + // 直らない状況を真似するため、ターゲットは何もしないでおく。 +#else + // ターゲットデバイスは賢いので、全員イニシエータ様に忖度して + // 即座にバスフリーへ移行するというのはどうか。 + for (uint i = 0; i < 8; i++) { + if (i != id) { + NegateBSY(i); + } + } +#endif } } @@ -157,7 +315,7 @@ SCSIBus::AssertSEL(uint id) void SCSIBus::NegateSEL(uint id) { - putlog(4, "%s(#%d)", __func__, id); + putlog(4, "%s(#%u)", __func__, id); // すでに下りてたら何もしない if ((sel & (1 << id)) == 0) { @@ -166,7 +324,17 @@ SCSIBus::NegateSEL(uint id) sel &= ~(1 << id); - if (GetPhase() == SCSI::Phase::Selection) { + if (GetPhase() == SCSI::Phase::BusFree) { + // バスフリーフェーズで SEL を下げる + + // 通常は起きないはずだけど、転送フェーズ中に LUNA をリセットすると + // -> LUNA の PROM が SEL を上げる (プロトコル違反) + // -> ターゲットは転送フェーズ中なのに SEL が上がったのを検出して + // BSY を取り下げてバスフリーに移行する + // -> バスフリーがイニシエータに通知され、イニシエータが SEL を下げる + // となるので、このケースはスルーしてよい。 + + } else if (GetPhase() == SCSI::Phase::Selection) { // セレクションフェーズで SEL を下げる if (bsy) { // BSY が立っていれば、ターゲットがセレクションに応答したので @@ -181,8 +349,8 @@ SCSIBus::NegateSEL(uint id) BusFree(id); } } else { - // どうする? - PANIC("NegateSEL in %s phase\n", GetPhaseName()); + // 下げる分には無視してもいいはずだけど、どうする? + VMPANIC("NegateSEL in %s phase", GetPhaseName()); } } @@ -190,7 +358,7 @@ SCSIBus::NegateSEL(uint id) void SCSIBus::AssertBSY(uint id) { - putlog(4, "%s(#%d)", __func__, id); + putlog(4, "%s(#%u)", __func__, id); // すでに上がってたら何もしない if ((bsy & (1 << id))) { @@ -218,7 +386,7 @@ SCSIBus::AssertBSY(uint id) } else if (GetPhase() == SCSI::Phase::Arbitration) { // アービトレーションフェーズだったら、アービトレーションに参加。 // 今は起きない。 - PANIC("AssertBSY(#%d) in arbitration", id); + VMPANIC("AssertBSY(#%u) in arbitration", id); } else if (GetPhase() == SCSI::Phase::Selection) { // セレクションフェーズだったら、転送フェーズに移行中。 @@ -228,7 +396,7 @@ SCSIBus::AssertBSY(uint id) CallAfter(SelectionAck, "SCSIBus Selection Ack", 90_nsec); } else { // どうする? - PANIC("AssertBSY(#%d) in %s phase\n", id, GetPhaseName()); + VMPANIC("AssertBSY(#%u) in %s phase", id, GetPhaseName()); } } @@ -236,7 +404,7 @@ SCSIBus::AssertBSY(uint id) void SCSIBus::NegateBSY(uint id) { - putlog(4, "%s(#%d)", __func__, id); + putlog(4, "%s(#%u)", __func__, id); // すでに下りてたら何もしない if ((bsy & (1 << id)) == 0) { @@ -262,7 +430,7 @@ SCSIBus::NegateBSY(uint id) } else { // どうする? - PANIC("NegateBSY(#%d) in %s phase\n", id, GetPhaseName()); + VMPANIC("NegateBSY(#%u) in %s phase", id, GetPhaseName()); } } @@ -294,10 +462,16 @@ SCSIBus::AssertREQ() // 通知はイベントにしないといけないので。 uint64 after = 0; - uint64 now = gScheduler->GetVirtTime(); + 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 { // 転送フェーズでないのに上げることはないと思うけど @@ -308,15 +482,15 @@ SCSIBus::AssertREQ() } // ターゲットが REQ を立てようとして所定時間経ったので REQ を立てて -// イニシエータの TransferReqCB を呼び出すところ。 +// イニシエータの OnTransferReq を呼び出すところ。 void -SCSIBus::TransferReq(int dummy) +SCSIBus::TransferReq(Event& ev) { putlog(4, "AssertREQ"); req = true; assert(device[init_id]); - device[init_id]->TransferReqCB(); + device[init_id]->OnTransferReq(); } // REQ を下げる @@ -336,7 +510,7 @@ SCSIBus::AssertACK() if (GetPhase() == SCSI::Phase::Transfer) { assert(device[target_id]); - device[target_id]->TransferCB(); + device[target_id]->OnTransfer(); } } @@ -349,7 +523,7 @@ SCSIBus::NegateACK() if (GetPhase() == SCSI::Phase::Transfer) { assert(device[target_id]); - device[target_id]->TransferAckCB(); + device[target_id]->OnTransferAck(); } } @@ -372,7 +546,7 @@ SCSIBus::NegateATN() // バスフリーフェーズに移行。 // id はこのバスフリーを行ったデバイス (0..7) void -SCSIBus::BusFree(int id) +SCSIBus::BusFree(uint id) { putlog(4, "%s", __func__); @@ -381,7 +555,7 @@ SCSIBus::BusFree(int id) // 即全デバイスに通知。 for (int i = 0; i < countof(device); i++) { if (device[i]) { - device[i]->BusFreeCB(id); + device[i]->OnBusFree(id); } } @@ -419,18 +593,18 @@ SCSIBus::BusFree(int id) // 200usec 以内…なので適当に間をとって 100usec 後に Selection() コールバック // を呼び出す(1+2)。 // -// Selection() コールバック(が呼び出す device->SelectionCB()) は通常 BSY を +// Selection() コールバック(が呼び出す device->OnSelected()) は通常 BSY を // アサートするはずであり、そこから 90nsec 後に SelectionAck() コールバック // を呼び出す(3)。 // -// SelectionAck() コールバック(が呼び出す device->SelectionAckCB()) は通常 +// SelectionAck() コールバック(が呼び出す device->OnSelectionAck()) は通常 // SEL をネゲートするはずであり、そこから情報転送フェーズへの移行には特に // 規定はなさそうなので、400nsec 後に StartTransfer() をコールバックして // デバイスを情報転送フェーズに移行させる。 // セレクションフェーズ開始。 void -SCSIBus::SelectionStart() +SCSIBus::SelectionStart(Event& ev) { // この時点でデータバスには自身の ID と相手の ID が立っているはず。 // 一方 sel は SEL 信号線を立てた人のビットを持っているので、 @@ -460,30 +634,107 @@ SCSIBus::SelectionStart() // SelectionStart() にてターゲットが選択されて規定時間経ったので // そろそろターゲットが BSY を立てる頃に呼ばれるコールバック。 void -SCSIBus::Selected(int dummy) +SCSIBus::Selected(Event& ev) { // ターゲットに通知。ターゲットがいることは確定している。 assert(device[target_id]); - device[target_id]->SelectionCB(); + device[target_id]->OnSelected(); } // ターゲットが BSY を立てて規定時間経ったので -// そろそろイニシエータが SEL を下げることに呼ばれるコールバック。 +// そろそろイニシエータが SEL を下げる頃に呼ばれるコールバック。 void -SCSIBus::SelectionAck(int dummy) +SCSIBus::SelectionAck(Event& ev) { // イニシエータに通知。 assert(device[init_id]); - device[init_id]->SelectionAckCB(); + device[init_id]->OnSelectionAck(); } // 情報転送フェーズ開始を通知する。 // BSY を上げて情報転送フェーズに移行したところで呼ばれる。 void -SCSIBus::StartTransfer(int dummy) +SCSIBus::StartTransfer(Event& ev) { SetPhase(SCSI::Phase::Transfer); assert(device[target_id]); - device[target_id]->StartTransferCB(); + device[target_id]->OnStartTransfer(); +} + +// SCSI デバイスモニタ +// (どこでやるのがいいか分からんけど、SCSI デバイスを一括して表示したいので +// とりあえずここで) +void +SCSIBus::MonitorUpdate(Monitor *, TextScreen& screen) +{ + int y; + +// 012345678901234567890123456789012345678901234567890123456789012345678901234 +// ID6: HD(2048MB) +// MediumLoaded Off LogicalBlock 2048 ImageSize 2,147,483,648 +// RemovalPrevent Off WriteProtected + + + screen.Clear(); + + y = 0; + for (const auto dev : connected_devices) { + std::string desc = SCSI::GetDevTypeName(dev->GetDevType()); + std::string pathname; + + const SCSIDisk *disk = dynamic_cast(dev); + if (disk && disk->IsMediumLoaded()) { + desc += string_format("(%uMB)", + (uint)(disk->GetSize() / 1024 / 1024)); + pathname = disk->GetPathName(); + } + screen.Print(0, y, "ID%c: %s", '0' + dev->GetMyID(), desc.c_str()); + +#if 1 + (void)pathname; +#else + // パス名の非ASCIIどうするか + if (!pathname.empty()) { + if (pathname.size() > screen.GetCol() - 16 - 3) { + pathname = pathname.substr(0, screen.GetCol() - 16 - 3); + pathname += "..."; + } + screen.Puts(16, y, pathname.c_str()); + } +#endif + y++; + + // イニシエータならここまで。(今の所イニシエータとディスクしかない) + if (disk == NULL) { + continue; + } + + const int x1 = 5; + const int x2 = 29; + const int x3 = 50; + + // 1行目 + screen.Print(x1, y, + (disk->IsRemovableDevice() ? TA::Normal : TA::Disable), + "MediumLoaded %s", + disk->IsMediumLoaded() ? "On" : "Off"); + screen.Print(x2, y, "LogicalBlock %u", disk->GetBlocksize()); + std::string imagesize = format_number(disk->GetSize()); + screen.Print(x3, y, "ImageSize %13s", imagesize.c_str()); + y++; + + // 2行目 + screen.Print(x1, y, + (disk->IsRemovableDevice() ? TA::Normal : TA::Disable), + "RemovalPrevent %s", + disk->IsMediumRemovalPrevented() ? "On" : "Off"); + + screen.Puts(x2, y, + (disk->IsMediumLoaded() ? TA::Normal : TA::Disable), + disk->GetWriteModeStr()); + y++; + + y++; + } }