|
|
nono 1.7.0
//
// nono
// Copyright (C) 2020 nono project
// Licensed under nono-license.txt
//
//
// SPC (MB89352)
//
#include "spc.h"
#include "config.h"
#include "event.h"
#include "mainapp.h"
#include "monitor.h"
#include "pedec.h"
#include "scheduler.h"
#include "scsidev.h"
#include "scsidomain.h"
// time 時間後に呼び出すコールバックをセットする。
// func の書式が面倒なのを省略して書きたいため。
#define CallAfter(func_, name_, time_) do { \
phase_event->SetCallback(&SPCDevice::func_); \
phase_event->time = time_; \
phase_event->SetName(name_); \
scheduler->RestartEvent(phase_event); \
} while (0)
// コンストラクタ
SPCDevice::SPCDevice()
: inherited(OBJ_SPC0)
{
// 初期値は不定だが表示上困るのでとりあえず。
myid = 7;
monitor = gMonitorManager->Regist(ID_MONITOR_SPC, this);
monitor->SetCallback(&SPCDevice::MonitorScreen);
// サイズは Init で決まる
}
// デストラクタ
SPCDevice::~SPCDevice()
{
}
// ログレベル設定
void
SPCDevice::SetLogLevel(int loglevel_)
{
inherited::SetLogLevel(loglevel_);
if (host) {
host->SetLogLevel(loglevel_);
}
}
bool
SPCDevice::Create()
{
// ドメイン。
try {
domain.reset(new SCSIDomain(this, "SPC0", "spc0"));
} catch (...) { }
if ((bool)domain == false) {
warnx("Failed to initialize SCSIDomain at %s", __method__);
return false;
}
// バス
try {
bus.reset(new SCSIBus(domain.get()));
} catch (...) { }
if ((bool)bus == false) {
warnx("Failed to initialize SCSIBus at %s", __method__);
return false;
}
return true;
}
// 初期化
bool
SPCDevice::Init()
{
// 実行中に別インスタンスを頻繁に参照しないようにここで vmtype を覚えておく
vmtype = gMainApp.GetVMType();
host = domain->GetInitiator();
host->SetBusCallback(
ToBusFreeCallback(&SPCDevice::BusFreeCallback),
ToSCSIHostCallback(&SPCDevice::SelectionAckCallback),
ToSCSIHostCallback(&SPCDevice::TransferReqCallback));
// ターゲットデバイスをバスに接続。
domain->AttachTargets(bus.get());
bus->Refresh();
// 入力クロックは機種によって異なる。
// X68k は 5MHz が入っている。
// LUNA-I はおそらく 6.144MHz だと思われる。LUNA-88K は未確認。
if (vmtype == VMType::X68030) {
t_CLF = 200_nsec;
} else {
t_CLF = 163_nsec;
}
// アクセスウェイト
if (vmtype == VMType::X68030) {
// InsideOut p.135
read_wait = busdata::Wait(18 * 40_nsec);
write_wait = busdata::Wait(19 * 40_nsec);
} else {
// 他機種は未調査だけど、判明するまでの間とりあえず
// ノーウェイトでない適当な値を入れておく。
read_wait = busdata::Wait(18 * 40_nsec);
write_wait = busdata::Wait(19 * 40_nsec);
}
// 割り込み信号線の接続先
if (vmtype == VMType::X68030) {
// X68030 は PEDEC (I/O コントローラ) にカスケード
interrupt = GetPEDECDevice();
} else {
interrupt = GetInterruptDevice();
}
auto evman = GetEventManager();
phase_event = evman->Regist(this, NULL, "SPC Phase");
timer_event = evman->Regist(this, NULL, "SPC Timer");
// モニタに表示するアドレス
if (vmtype == VMType::X68030) {
baseaddr = 0xe96021;
regoffset = 2;
} else {
baseaddr = 0xe1000000;
regoffset = 4;
}
// 「イニシエータ以外のターゲット数」はここでは確定している。
monitor->SetSize(80, 15 + domain->GetTargetCount() + 1);
return true;
}
// デバイスリセット
void
SPCDevice::ResetHard(bool poweron)
{
putlog(1, "Reset");
// 初期値 (p.61)
spc.sctl = SPC::SCTL_RESET;
spc.ints = 0x00;
spc.ssts &= 0x0f;
spc.serr = 0x00;
// 変更しないレジスタは次の通り (p.60)
// BDID, TMOD, TC[HML]?, SCMD, PCTL, TEMP(out)
// XXX マニュアルには書いていないけど、
// SDGC bit5 の転送ごとに割り込みを発生させるビットが
// OFFになることを NetBSD が期待しているし、
// このビットは落ちるのではないだろうか
spc.sdgc = 0x00;
// キューはリセットされる。
// mb89351-scsi.pdf (日本語)
spc.dreg.Clear();
// 内部状態
MakeIntsMask();
ChangeInterrupt();
prev_ints = (uint32)-1;
prev_ssts = (uint32)-1;
set_atn_in_selection = false;
transfer_command_running = false;
scheduler->StopEvent(phase_event);
scheduler->StopEvent(timer_event);
last_busfree_time = 0;
}
busdata
SPCDevice::ReadPort(uint32 offset)
{
busdata data;
switch (offset) {
case SPC::BDID:
data = 1U << myid;
putlog(3, "BDID -> $%02x", data.Data());
break;
case SPC::SCTL:
data = spc.sctl;
putlog(3, "SCTL -> $%02x", data.Data());
break;
case SPC::SCMD:
data = spc.scmd;
putlog(3, "SCMD -> $%02x", data.Data());
break;
case SPC::TMOD:
// MB89352 には存在しないレジスタ。
// 下位2ビットは %0 らしいので意味はないだろうけど真似ておく。
data = 0xfc;
break;
case SPC::INTS:
data = GetINTS();
if (loglevel >= 3) {
uint32 new_ints = data.Data();
if (timer_event->IsRunning()) {
// イベント中は値が変わった時だけ表示
if (prev_ints != new_ints) {
putlogn("INTS -> $%02x", new_ints);
prev_ints = new_ints;
}
} else {
putlogn("INTS -> $%02x", new_ints);
prev_ints = -1U;
}
}
break;
case SPC::PSNS:
data = GetPSNS();
putlog(3, "PSNS -> $%02x", data.Data());
break;
case SPC::SSTS:
data = GetSSTS();
if (loglevel >= 3) {
uint32 new_ssts = data.Data();
if (timer_event->IsRunning()) {
// イベント中は値が変わった時だけ表示
if (prev_ssts != new_ssts) {
putlogn("SSTS -> $%02x", new_ssts);
prev_ssts = new_ssts;
}
} else {
putlogn("SSTS -> $%02x", new_ssts);
prev_ssts = -1U;
}
}
break;
case SPC::SERR:
data = GetSERR();
putlog(3, "SERR -> $%02x", data.Data());
break;
case SPC::PCTL:
data = GetPCTL();
putlog(3, "PCTL -> $%02x", data.Data());
break;
case SPC::MBC:
putlog(0, "Read MBC (NOT IMPLEMENTED)");
data = 0x0f;
break;
case SPC::DREG:
data = ReadDREG();
break;
case SPC::TEMP:
data = spc.temp_in;
putlog(3, "TEMP -> $%02x", data.Data());
break;
case SPC::TCH:
data = spc.tc >> 16;
putlog(3, "TCH -> $%02x", data.Data());
break;
case SPC::TCM:
data = (spc.tc >> 8) & 0xff;
putlog(3, "TCM -> $%02x", data.Data());
break;
case SPC::TCL:
data = spc.tc & 0xff;
putlog(3, "TCL -> $%02x", data.Data());
break;
case SPC::EXBF:
// MB89352 には存在しないレジスタ
data = 0xff;
break;
default:
VMPANIC("corrupted offset=%u", offset);
}
data |= read_wait;
data |= BusData::Size1;
return data;
}
busdata
SPCDevice::WritePort(uint32 offset, uint32 data)
{
busdata r;
switch (offset) {
case SPC::BDID:
{
uint cur_myid = host->GetMyID();
putlog(3, "BDID <- $%02x", data);
if (data == cur_myid) {
// 同じなら何もしない
break;
}
if ((int)cur_myid < 0) {
// 初回なのでバスに参加。myid はこちらでもコピーを覚えておく。
myid = data;
if (domain->AttachInitiator(bus.get(), myid) == false) {
putlog(0, "BDID %u conflicts with existing devices!", myid);
}
bus->Refresh();
} else {
// そうでなければ移動?
putlog(0, "BDID change %u to %u (NOT IMPLEMENTED)", cur_myid, data);
}
break;
}
case SPC::SCTL:
WriteSCTL(data);
break;
case SPC::SCMD:
WriteSCMD(data);
break;
case SPC::TMOD:
// MB89352 には存在しないレジスタ
break;
case SPC::INTS:
WriteINTS(data);
break;
case SPC::SDGC:
spc.sdgc = data;
putlog(3, "SDGC <- $%02x", spc.sdgc);
break;
case SPC::SSTS:
// XXX 書き込みはどうなる?
break;
case SPC::SERR:
// XXX 書き込みはどうなる?
break;
case SPC::PCTL:
WritePCTL(data);
break;
case SPC::MBC:
// XXX 書き込みはどうなる?
break;
case SPC::DREG:
putlog(3, "DREG <- $%02x", data);
r = WriteDREG(data);
break;
case SPC::TEMP:
spc.temp_out = data;
putlog(3, "TEMP <- $%02x", spc.temp_out);
// Inside (p479) によると、SPC はセレクションタイムアウト時 SEL を
// 下げないので、代わりに TEMP に $00 を書き込むことで SEL を下げる、
// そうなのだが、データシート PDF には記述が見当たらないし、
// luna68k の /boot はここで SEL が下げてしまうと期待した動作に
// ならないようだ。
// SPC はセレクションタイムアウトしたからと言って、データバスに出した
// ID を自動的に取り下げたりはしないので、TEMP に $00 を書き込むことで
// データバスをクリアする、だったんでは。
if (0 && GetPhase() == SCSI::Phase::Selection) {
NegateSEL();
}
// Out 方向ならバスに出す
if (GetIO() == false) {
SetData(spc.temp_out);
}
break;
case SPC::TCH:
spc.tc = (spc.tc & 0x00ffff) | (data << 16);
putlog(3, "TCH <- $%02x (TC=$%06x)", data, spc.tc);
break;
case SPC::TCM:
spc.tc = (spc.tc & 0xff00ff) | (data << 8);
putlog(3, "TCM <- $%02x (TC=$%06x)", data, spc.tc);
break;
case SPC::TCL:
spc.tc = (spc.tc & 0xffff00) | data;
putlog(3, "TCL <- $%02x (TC=$%06x)", data, spc.tc);
break;
case SPC::EXBF:
// MB89352 には存在しないレジスタ
break;
default:
VMPANIC("corrupted offset=%u", offset);
}
r |= write_wait;
r |= BusData::Size1;
return r;
}
busdata
SPCDevice::PeekPort(uint32 offset)
{
switch (offset) {
case SPC::BDID:
return 1U << myid;
case SPC::SCTL:
return spc.sctl;
case SPC::SCMD:
return spc.scmd;
case SPC::TMOD:
// MB89352 には存在しないレジスタ。
// 下位2ビットは %0 らしいので意味はないだろうけど真似ておく。
return 0xfc;
case SPC::INTS:
return GetINTS();
case SPC::PSNS:
return GetPSNS();
case SPC::SSTS:
return GetSSTS();
case SPC::SERR:
return GetSERR();
case SPC::PCTL:
return GetPCTL();
case SPC::MBC:
return 0x0f;
case SPC::DREG:
// X68k は SPC の DREQ 信号から DTACK 信号を作っているため、
// DREQ 信号を出さないプログラム転送モードでは DREG へのアクセスが
// すべてバスエラーになってしまう。
if (vmtype == VMType::X68030) {
if ((spc.scmd & SPC::SCMD_PROGRAM)) {
return BusData::BusErr;
}
}
return spc.dreg.Peek(0);
case SPC::TEMP:
return spc.temp_in;
case SPC::TCH:
return spc.tc >> 16;
case SPC::TCM:
return (spc.tc >> 8) & 0xff;
case SPC::TCL:
return spc.tc & 0xff;
case SPC::EXBF:
// MB89352 には存在しないレジスタ
return 0xff;
default:
return 0xff;
}
}
bool
SPCDevice::PokePort(uint32 offset, uint32 data)
{
return false;
}
void
SPCDevice::MonitorScreen(Monitor *, TextScreen& screen)
{
int x;
int y;
uint32 sctl;
uint32 scmd;
uint32 ints;
uint32 psns;
uint32 sdgc;
uint32 ssts;
uint32 serr;
uint32 pctl;
uint32 tc;
screen.Clear();
sctl = spc.sctl;
scmd = spc.scmd;
ints = GetINTS();
psns = GetPSNS();
sdgc = spc.sdgc;
ssts = GetSSTS();
serr = GetSERR();
pctl = GetPCTL();
tc = spc.tc;
#define A(n) (baseaddr + (n) * regoffset)
y = 0;
screen.Print(0, y++, "$%08x BDID: $%02x (ID=%u)", A(0), (1U << myid), myid);
screen.Print(0, y++, "$%08x SCTL: $%02x", A(1), sctl);
screen.Print(0, y++, "$%08x SCMD: $%02x", A(2), scmd);
screen.Print(0, y++, "$%08x INTS: $%02x", A(4), ints);
screen.Print(0, y++, "$%08x PSNS: $%02x", A(5), psns);
screen.Print(0, y++, "$%08x SDGC: $%02x", A(5), sdgc);
screen.Print(0, y++, "$%08x SSTS: $%02x", A(6), ssts);
screen.Print(0, y++, "$%08x SERR: $%02x", A(7), serr);
screen.Print(0, y++, "$%08x PCTL: $%02x", A(8), pctl);
screen.Print(0, y++, "$%08x MBC : $XX", A(9));
screen.Print(0, y, "$%08x DREG: ", A(10));
{
uint i;
for (i = 0; i < spc.dreg.Length(); i++) {
screen.Print(16 + i * 4, y, "$%02x", spc.dreg.Peek(i));
}
for (; i < 8; i++) {
screen.Puts(16 + i * 4, y, "---");
}
}
y++;
screen.Print(0, y++, "$%08x TEMP: $%02x(IN) $%02x(OUT)", A(11),
spc.temp_in, spc.temp_out);
screen.Print(0, y++, "$%08x TC : $%06x", A(12), tc);
x = 20;
y = 1;
// SCTL
static const char * const sctl_bits[] = {
"RST", "CTRL", "DIAG", "ARB", "PARI", "SEL", "RESL", "INTR"
};
MonitorReg(screen, x, y, sctl, sctl_bits);
y++;
// SCMD
static const char * const scmd_bits[] = {
"", "", "", "RST", "INTC", "PROG", "----", "TERM"
};
MonitorReg(screen, x, y, scmd, scmd_bits);
static const char * const scmd_cmd[] = {
//12345678901234
"(Bus Release)",
"(Select)",
"(Reset ATN)",
"(Set ATN)",
"(Transfer)",
"(XferPause)",
"(ResetACK/REQ)",
"(Set ACK/REQ)",
};
screen.Puts(x, y, scmd_cmd[scmd >> 5]);
y++;
// INTS
static const char * const ints_bits[] = {
"Sel", "Resl", "Disc", "Cmpl", "SReq", "TOut", "HErr", "Rst"
};
MonitorReg(screen, x, y, ints, ints_bits);
y++;
// PSNS
static const char * const psns_bits[] = {
"REQ", "ACK", "ATN", "SEL", "BSY", "MSG", "C/D", "I/O"
};
MonitorReg(screen, x, y, psns, psns_bits);
y++;
// SDGC
static const char * const sdgc_bits[] = {
"-", "-", "XfEn", "-", "-", "-", "-", "-"
};
MonitorReg(screen, x, y, sdgc, sdgc_bits);
y++;
// SSTS
static const char * const ssts_bits[] = {
"", "", "SBSY", "XInP", "RSTI", "TCZ", "Full", "Empt"
};
MonitorReg(screen, x, y, ssts, ssts_bits);
static const char * const ssts_conn[] = {
//12345678
"(NotConn)",
"(As Tgt)",
"(As Init)",
"(undef?)",
};
screen.Puts(x, y, ssts_conn[ssts >> 6]);
y++;
// SERR
static const char * const serr_bits[] = {
"-", "-", "XOut", "-", "PErr", "-", "Shrt", "-"
};
MonitorReg(screen, x, y, serr, serr_bits);
y++;
// PCTL
static const char * const pctl_bits[] = {
"BIE", "-", "-", "-", "-", "MSG", "C/D", "I/O"
};
MonitorReg(screen, x, y, pctl, pctl_bits);
y++;
// バス状態
x = 62;
y = 0;
SCSI::Phase ph = GetPhase();
uint8 rst = GetRST();
uint8 bsy = GetBSY();
uint8 sel = GetSEL();
uint8 msg = GetMSG();
uint8 cd = GetCD();
uint8 io = GetIO();
uint8 atn = GetATN();
uint8 req = GetREQ();
uint8 ack = GetACK();
uint8 data = GetData();
screen.Print(x, y++, "Phase: %s", SCSI::GetPhaseName(ph));
screen.Puts(x, y++, TA::OnOff(rst), "RST");
screen.Puts(x, y++, TA::OnOff(bsy), "BSY");
screen.Puts(x, y++, TA::OnOff(sel), "SEL");
screen.Puts(x, y++, TA::OnOff(msg), "MSG");
screen.Puts(x, y++, TA::OnOff(cd), "CD");
screen.Puts(x, y++, TA::OnOff(io), "IO");
screen.Puts(x, y++, TA::OnOff(atn), "ATN");
screen.Puts(x, y++, TA::OnOff(req), "REQ");
screen.Puts(x, y++, TA::OnOff(ack), "ACK");
screen.Print(x, y++, "DATA:$%02x", data);
y = 2;
screen.Puts(x + 4, y++, "(________)");
screen.Puts(x + 4, y++, "(________)");
// Shift-JIS は手動で置く必要がある。
screen.Puts(x + 4, y++, "\x84\xa2"); // "┐"
screen.Puts(x + 4, y++, "\x84\xa0"); // "│"
screen.Puts(x + 4, y++, "\x84\xa3"); // "┘"
// BSY, SEL の内訳。"(76543210)" の順。
y = 2;
for (int i = 0; i < 8; i++) {
if ((bsy & (1U << i))) {
screen.Print(x + 4 + 8 - i, y, "%u", i);
} else {
screen.Print(x + 4 + 8 - i, y, "_");
}
}
y++;
for (int i = 0; i < 8; i++) {
if ((sel & (1U << i))) {
screen.Print(x + 4 + 8 - i, y, "%u", i);
} else {
screen.Print(x + 4 + 8 - i, y, "_");
}
}
// 転送フェーズ中なら現在の XferPhase
y += 2;
TA attrxfer = (ph == SCSI::Phase::Transfer) ? TA::Normal : TA::Disable;
screen.Puts(x + 6, y++, attrxfer, "XferPhase:");
screen.Puts(x + 6, y, attrxfer, SCSI::GetXferPhaseName(GetXfer()));
// デバイス一覧
// 0 1
// 01234567890123456
// ID Device Command
// 0: HD(1024MB) $06 Read
// 7: Initiator
x = 0;
y = 14;
screen.Print(x, y++, "ID Device Command");
for (const auto d : domain->GetConnectedDevices()) {
std::string desc = SCSI::GetDevTypeName(d->GetDevType());
const SCSIDisk *disk = dynamic_cast<const SCSIDisk*>(d);
if (disk && disk->IsMediumLoaded()) {
desc += string_format("(%uMB)",
(uint)(disk->GetSize() / 1024 / 1024));
}
// 本体 ID は BDID が設定される前でもエミュレータ的に知ってはいるが、
// BDID への書き込みがされなかったりおかしかったりしたら (上の BDID
// 欄でも分かるけど) ここも反映されるようにしておく。
char idc;
if ((int)d->GetMyID() >= 0) {
idc = '0' + d->GetMyID();
} else {
idc = '-';
}
screen.Print(x, y, "%c: %s", idc, desc.c_str());
y++;
}
// 実行中のコマンド
x = 14;
y = 15;
const SCSIDevice *d = bus->GetSelectedTarget();
const SCSITarget *t = dynamic_cast<const SCSITarget *>(d);
if (t) {
std::vector<uint8> cmds = t->GetCmdSeq();
if (cmds.size() > 0) {
// この ID のデバイス行に表示したい
// XXX もうちょっとマシにしたい
const auto connected_devices = domain->GetConnectedDevices();
int i = 0;
for (; i < connected_devices.size(); i++) {
if (t == connected_devices[i]) {
break;
}
}
const char *cmdname = SCSI::GetCommandName(cmds[0]);
screen.Print(x, y + i, "$%02x %s", cmds[0], cmdname ?: "");
}
}
}
// レジスタをビットごとに表示する。MonitorScreen の下請け。
void
SPCDevice::MonitorReg(TextScreen& screen,
int x, int y, uint32 reg, const char * const *names)
{
for (int i = 0; i < 8; i++) {
if (names[i][0] == '-') {
screen.Puts(x + i * 5, y, TA::Disable, "----");
} else {
bool b = reg & (1U << (7 - i));
screen.Puts(x + i * 5, y, TA::OnOff(b), names[i]);
}
}
}
// SCTL レジスタへの書き込み
void
SPCDevice::WriteSCTL(uint32 data)
{
// リセットの立ち上がり検出が必要
uint32 up = ~spc.sctl & data;
spc.sctl = data;
putlog(3, "SCTL <- $%02x", spc.sctl);
if ((up & SPC::SCTL_RESET)) {
// リセット
ResetHard(false);
} else {
// 通常動作
}
if ((spc.sctl & SPC::SCTL_CTL_RESET)) {
putlog(2, "Control Reset");
}
if ((spc.sctl & SPC::SCTL_DIAG_MODE)) {
putlog(0, "SCTL Diag Mode (NOT IMPLEMENTED)");
}
// 割り込みマスクを更新して信号線の状態を変える
MakeIntsMask();
ChangeInterrupt();
}
// SCTL_INTR_EN の状態から内部変数 ints_mask を作成する。
void
SPCDevice::MakeIntsMask()
{
if ((spc.sctl & SPC::SCTL_INTR_EN)) {
spc.ints_mask = SPC::INTS_ENABLE_MASK;
} else {
spc.ints_mask = SPC::INTS_DISABLE_MASK;
}
}
// SCMD レジスタへの書き込み
void
SPCDevice::WriteSCMD(uint32 data)
{
uint32 chg = spc.scmd ^ data; // 変更点
spc.scmd = data;
putlog(3, "SCMD <- $%02x", spc.scmd);
// Program, Terminate Mode の未実装判定は転送開始時のみ表示する。
// RST Out 操作は SCTL Reset がオフの時だけ有効
// と書いてあるけど、SCTL Reset がオンならほぼ全機能無効なのでは?
if ((spc.sctl & SPC::SCTL_RESET) == 0 && (chg & SPC::SCMD_RST_OUT)) {
if ((spc.scmd & SPC::SCMD_RST_OUT)) {
// 0 -> 1
// すでに RST が立ってたらどうするか
if (GetRST() == false) {
putlog(1, "SCMD RST Out");
BusRelease();
AssertRST();
SetINTS(SPC::INTS_RESET_COND);
}
} else {
// 1 -> 0
// 特にすることはない?
NegateRST();
}
}
switch (spc.scmd >> 5) {
case 0:
putlog(2, "SCMD Bus Release command");
if ((spc.scmd & SPC::SCMD_INTERCEPT)) {
// BusRelease コマンドの発行と共に Intercept ビットが立っていたら
// Service Required 割り込みをリセットする?
putlog(0, "SCMD Intercept with BusRelease (NOT IMPLEMENTED)");
}
BusFree();
break;
case 1:
SelectCommand();
break;
case 2:
putlog(1, "SCMD Reset ATN command (NOT IMPLEMENTED)");
break;
case 3:
SetATNCommand();
break;
case 4:
TransferCommand();
break;
case 5:
putlog(2, "SCMD Transfer Pause command (NOT IMPLEMENTED)");
break;
case 6:
putlog(2, "SCMD Reset ACK/REQ command");
// イニシエータなら ACK を、ターゲットなら REQ をリセットする。
// ターゲットモードはサポートしないので、操作するのは ACK のみ。
NegateACK();
break;
case 7:
putlog(2, "SCMD Set ACK/REQ command");
// イニシエータなら ACK を、ターゲットなら REQ をセットする。
// ターゲットモードはサポートしないので、操作するのは ACK のみ。
if ((spc.scmd & SPC::SCMD_INTERCEPT)) {
// Intercept なら DREG を維持したまま (たぶん TEMP で) バスを
// 直接アクセスするようだが、この DREG 維持あたりは未対応。
// これが影響するのは DREG にデータが滞留してる時だけのはず
// なので、その時だけログを出す。(X68030 IPLROM、NetBSD/x68k)
if (spc.dreg.Length() != 0) {
putlog(0, "SCMD Intercept with SetACK (NOT IMPLEMENTED)");
}
}
AssertACK();
break;
}
}
// INTS レジスタへの書き込み
void
SPCDevice::WriteINTS(uint32 data)
{
uint32 ack;
// クリアすることになるビットを覚えておく
ack = spc.ints & data;
// '1' が書き込まれたビットをクリア
spc.ints &= ~data;
putlog(3, "INTS <- $%02x (INTS = $%02x)", data, GetINTS());
// DISCONNECTED ビットが立っているときは SPC は SCSI から
// セレクション、リセレクション要求が来ても応答しない。
// DISCONNECTED ビットが下ろされると、応答するようになる。
// TIMEOUT ビットをクリアした時が
if ((ack & SPC::INTS_TIMEOUT)) {
// セレクションフェーズなら
if (GetPhase() == SCSI::Phase::Selection) {
// TC がゼロならバスフリー
// そうでなければリスタート
if (spc.tc == 0) {
BusFree();
} else {
timer_event->SetCallback(&SPCDevice::SelectionTimeout);
timer_event->time = spc.tc * t_CLF * 2;
scheduler->RestartEvent(timer_event);
}
}
}
// 割り込み信号線の状態を変える
ChangeInterrupt();
}
// INTS 設定。
// 引数 data は割り込み要因(ここは内部要因なので INTS_XFER_OUT も指定可能)。
// INTS レジスタへの書き込み(割り込み要因のクリア)は WriteINTS() のほう。
void
SPCDevice::SetINTS(uint32 data)
{
// 指定された要因を立てる
spc.ints |= data;
// Disconnected 割り込みが起きたら ATN を下げる
if ((spc.ints & SPC::INTS_DISCONNECTED) && GetATN()) {
NegateATN();
}
ChangeInterrupt();
}
// INTS レジスタの値を取得
uint32
SPCDevice::GetINTS() const
{
// ints の上位には別の割り込み状態が保持されている。
// 下位8bit は INTS レジスタのまま。
return spc.ints & 0xff;
}
// 割り込み信号線の状態を変える
void
SPCDevice::ChangeInterrupt()
{
// 割り込み状態は ints に集約してあり、
// ints_mask は現在の SCTL_INTR_EN 状態に応じたマスクが用意されているので
// 単純に AND するだけでよい。
if ((spc.ints & spc.ints_mask) != 0) {
putlog(3, "Interrupt (ints=$%02x)", spc.ints);
interrupt->AssertINT(this);
} else {
putlog(4, "Negate Interrupt");
interrupt->NegateINT(this);
}
}
// PSNS レジスタ値の取得。
// ReadPort() と PeekPort() から呼ばれる。
// SCSIBus 接続前にもモニタの更新などで呼ばれることに注意。
uint32
SPCDevice::GetPSNS() const
{
uint32 data = 0;
if (__predict_false(bus == NULL)) {
return 0;
}
if (GetREQ()) {
data |= SPC::PSNS_REQ;
}
if (GetACK()) {
data |= SPC::PSNS_ACK;
}
if (GetATN()) {
data |= SPC::PSNS_ATN;
}
if (GetSEL()) {
data |= SPC::PSNS_SEL;
}
if (GetBSY()) {
data |= SPC::PSNS_BSY;
}
data |= (uint32)GetXfer();
return data;
}
// SSTS レジスタ値の取得。
// ReadPort() と PeekPort() から呼ばれる。
// SCSIBus 接続前にもモニタの更新などで呼ばれることに注意。
uint32
SPCDevice::GetSSTS() const
{
uint32 data;
data = spc.ssts & 0xf0;
if (__predict_true(bus != NULL) && GetRST()) { // RST 信号
data |= SPC::SSTS_RST_IN;
}
if (spc.tc == 0) { // TC がゼロか
data |= SPC::SSTS_TC_ZERO;
}
if (spc.dreg.IsFull()) {
data |= SPC::SSTS_DREG_FULL;
}
if (spc.dreg.Empty()) {
data |= SPC::SSTS_DREG_EMPTY;
}
return data;
}
// SERR レジスタ値の取得。
uint32
SPCDevice::GetSERR() const
{
// SERR レジスタのうち XFER_OUT ビットだけ ints で保持している。
uint32 xfer_out = (spc.ints & SPC::INTS_XFER_OUT) >> 3;
return spc.serr | xfer_out;
}
// PCTL レジスタへの書き込み。
void
SPCDevice::WritePCTL(uint32 data)
{
data &= SPC::PCTL_MASK;
putlog(3, "PCTL <- $%02x", data);
spc.busfree_intr_enable = (data & SPC::PCTL_BFINT_EN);
spc.pctl_out = data & (SPC::PCTL_MSG | SPC::PCTL_CD | SPC::PCTL_IO);
// この時点では pctl_out はまだバスに出さない。
// バスに出すのは転送開始時のはず。
}
// PCTL レジスタ値の取得。
uint32
SPCDevice::GetPCTL() const
{
return spc.busfree_intr_enable | spc.pctl_out;
}
// DREG レジスタの読み込み
// (副作用があるので Peek 系からは呼ばないこと)
busdata
SPCDevice::ReadDREG()
{
busdata data;
// X68k は SPC の DREQ 信号から DTACK 信号を作っているため、
// DREQ 信号を出さないプログラム転送モードでは DREG へのアクセスが
// すべてバスエラーになってしまう。
if (vmtype == VMType::X68030) {
if ((spc.scmd & SPC::SCMD_PROGRAM)) {
data.SetBusErr();
return data;
} else {
// DMA 転送で DREQ が出てないとき DTACK を出さないのを
// Retry を返すことで表現している。
if (spc.dreg.Length() == 0) {
putlog(3, "DREG empty during DMA transferring");
data.SetRetry();
return data;
}
}
}
// XFER_OUT は DREG アクセスでリセットされる
spc.ints &= ~SPC::INTS_XFER_OUT;
// TransferComplete() が割り込み状態を変化させる可能性があるので
// ChangeInterrupt() を最後に呼び出す。
// 受信データがあれば取り出す
if (__predict_true(spc.dreg.Length() > 0)) {
bool q_was_full = spc.dreg.IsFull();
data = spc.dreg.Dequeue();
if (loglevel >= 4) {
putlogn("DREG -> $%02x (q=%u TC=0x%x)",
data.Data(), spc.dreg.Length(), spc.tc);
} else {
putlog(3, "DREG -> $%02x", data.Data());
}
// この読み出しが最後だったらこれで転送完了。
if (spc.tc == 0 && spc.dreg.Length() == 0) {
TransferComplete();
}
// この読み出しによってバッファに空きが出来たら転送再開。
if (transfer_command_running && GetREQ() && q_was_full) {
// XXX 時間は適当
CallAfter(HardwareTransfer, "SPC ReadDREG to Transfer", t_CLF);
}
} else {
// 受信データがない場合
if ((spc.scmd & SPC::SCMD_PROGRAM)) {
// XXX プログラム転送中ならどうなる?
data = 0xff;
putlog(3, "DREG -> $%02x (empty)", data.Data());
} else {
// DMA 転送
// DMA 側でリトライするのでここには来ないはず
VMPANIC("DMA転送中の DREG empty");
}
}
// 割り込み状態を確定
ChangeInterrupt();
return data;
}
// DREG レジスタへの書き込み
busdata
SPCDevice::WriteDREG(uint32 data)
{
busdata r;
// X68k は SPC の DREQ 信号から DTACK 信号を作っているため、
// DREQ 信号を出さないプログラム転送モードでは DREG へのアクセスが
// すべてバスエラーになってしまう。
if (vmtype == VMType::X68030) {
if ((spc.scmd & SPC::SCMD_PROGRAM)) {
r.SetBusErr();
return data;
} else {
// DMA 転送で DREQ が出てないとき DTACK を出さないのを
// Retry を返すことで表現している。
if (spc.dreg.IsFull()) {
r.SetRetry();
return r;
}
}
}
// XFER_OUT は DREG アクセスでリセットされる
spc.ints &= ~SPC::INTS_XFER_OUT;
ChangeInterrupt();
// DREG キューに入れて..
if (spc.dreg.Enqueue(data) == false) {
// XXX キューが一杯だったらどうなるのかは書いてないので分からない。
// a) キューを押し出して先頭を破棄する
// b) キューの末尾を破棄して新着データで上書きする
// c) キューの内容は変わらず新着データを破棄する
// どれで実装しても「この時何かしら失われるデータがある」こと自体は
// 再現出来ているはずなので、ここではひとまず a) の動作にしておく。
putlog(0, "DREG Queue overflow");
spc.dreg.EnqueueForce(data);
}
// ハードウェア転送中で REQ が来てれば送信。
if (transfer_command_running && GetREQ()) {
// XXX 時間は適当
CallAfter(HardwareTransfer, "SPC WriteDREG to Transfer", t_CLF);
}
return r;
}
// バスフリーフェーズに移行する
void
SPCDevice::BusFree()
{
putlog(3, "BusFree");
// ここは BDID 書き込み前 (まだ SCSIBus に自身をつなげる前) にも呼ばれる。
// その場合は何もしない。バスフリーのはずでもあるし。
if (bus == NULL) {
return;
}
BusRelease();
}
// バスをリリースする。バスフリーとバスリセットから呼ばれる
void
SPCDevice::BusRelease()
{
// 自発的にバスをリリースする場合、今のフェーズから自分が変えていい
// BSY、SEL だけを落とす。それ以外の信号線は正しくはないけどとりあえず
// 一括して SCSIHostDevice::BusFreeCB() が落とすことにしてある。
switch (GetPhase()) {
case SCSI::Phase::BusFree:
case SCSI::Phase::Transfer:
break;
case SCSI::Phase::Arbitration:
case SCSI::Phase::Selection:
case SCSI::Phase::Reselection:
NegateSEL();
NegateBSY();
break;
}
}
// Select コマンド実行
void
SPCDevice::SelectCommand()
{
std::string idstr;
uint32 data;
uint32 tgtid;
uint64 wait;
// ターゲット ID を表示用に解析
data = spc.temp_out;
if (data == 0) {
// ID が一切立ってない?
idstr = "TEMP==0; No IDs?";
} else if ((data & (1U << myid)) == 0) {
// 自 ID が立ってない?
idstr = string_format("TEMP=$%02x; No BDID?", data);
} else {
// ここで少なくとも自 ID は立っている
data &= ~(1U << myid);
if (data == 0) {
// ターゲット ID がない
idstr = string_format("#%u", myid);
} else {
tgtid = SCSIBus::DecodeID(data);
idstr = string_format("#%u -> #%u", myid, tgtid);
}
}
putlog(2, "Select command (%s)", idstr.c_str());
// アービトレーションありの場合
//
// BSY X\______/~~~~~~~~~~~~~~~~~~~~~~~~~~\_ .. _/^^^^^^^^^^^^^^^
// |t_BFBL| | |t_BIDH|
// DB* ________<XXXXXXXXXXXXXXXXXXXXXXXXXXXX .. XXXXXXXX>________
// |t_ARB|t_AWSL| | |t_BLSH|
// SEL _____________________/~~~~~~~~~~~~~~~ .. ~~~~~~~~\________
// |t_SIDA|t_IDBH| |
// ATN ____________________________/~~~~~~~~ .. ~~~~~~~~~~~~~~~~~
// |t_SHIR|
// INTR _____________________________________ .. _______________/~
//
// t_BFBL = <Min> (6 + TCL) * t_CLF, <Max> (7 + TCL) * t_CLF + 60
// t_ARB = <Min> 32 * t_CLF - 60
// t_AWSL = <Min> 0, <Max> 80
// t_SIDA = <Min> 11 * t_CLF - 30
// t_IDBH = <Min> 2 * t_CLF - 80
//
// ここではバスフリー検出から少なくとも t_BFBL 以上 + t_ARB + t_AWSL 後に
// SEL と DB* と ATN を立てて、そこから t_SIDA + t_IDBH 後に BSY を落とす。
//
// アービトレーションなしの場合
//
// BSY X\_______________ .. _/~~~~~~~~~~~~~~~
// |t_FRID| |t_BIDH|
// DB* ________<XXXXXXXX .. XXXXXXXX><XXXXXXX
// |t_IDSL| |t_BLSH|
// SEL _______________/~ .. ~~~~~~~~\________
// |t_SHIR|
// INTR _________________ .. _______________/~
//
// t_FRID = <Min> (6 + TCL) * t_CLF, <Max> (7 + TCL) * t_CLF + 140
// t_IDSL = <Min> 11 * t_CLF - 80
// t_BIDH = <Min> t_BLSH = 2 * t_CLF
// t_SHIR = <Max> 60
//
// t_FRID/t_BFBL はたぶん今からではなくバスフリーになった時点から起算。
// t_FRID と t_BFBL は同じ式なのでここでは t_BFBL で統一。
uint64 t_BFBL = (6 + spc.GetTCL()) * t_CLF; // Min
uint64 now = scheduler->GetVirtTime();
if ((last_busfree_time + t_BFBL) > now) {
wait = (last_busfree_time + t_BFBL) - now;
} else {
wait = 0;
}
spc.ssts &= ~(SPC::SSTS_CONN_INIT | SPC::SSTS_CONN_TARG);
spc.ssts |= SPC::SSTS_SPC_BUSY;
// TCH,TCM によるタイマーを始動。
// N が 0 なら無限大なのでタイマーは動かさない。
uint32 N = (spc.tc >> 8) << 8;
if (N != 0) {
timer_event->SetCallback(&SPCDevice::SelectionTimeout);
timer_event->time = (N + 15) * t_CLF * 2;
scheduler->RestartEvent(timer_event);
}
if ((spc.sctl & SPC::SCTL_ARBIT_EN)) {
// アービトレーションありの場合
// ここでは複数のデバイスが同時にアービトレーションフェーズに
// 入ることはないため、アービトレーションを開始したデバイスが
// 必ずバスの使用権を得る。
// バスフリーから規定時間後に SEL (と DB* と ATN) を上げる。
uint64 t_ARB = 32 * t_CLF - 60_nsec;
uint64 t_AWSL = 40_nsec; // 適当に中間
CallAfter(Arbitration1, "SPC Arbitration", wait + t_ARB + t_AWSL);
} else {
// アービトレーションなしの場合
// バスフリーから規定時間後に SEL (と DB*) を上げる。
uint64 t_IDSL = 11 * t_CLF - 80_nsec;
CallAfter(Selection1, "SPC Selection", wait + t_IDSL);
}
}
// アービトレーションフェーズ開始後、規定時間経過したので SEL を立てるところ。
void
SPCDevice::Arbitration1(Event *ev)
{
putlog(3, "Arbitration1");
spc.ssts |= SPC::SSTS_SPC_BUSY;
// 本当は DB* -> SEL -> ATN の順でタイミングも規定されてるけど省略。
// ここでは SEL がトリガーになるので SEL のアサートは最後に行う。
// アービトレーションありなら、セレクション/リセレクションフェーズを
// 区別するため PCTL に設定された IO をバスに出す(p.27)。
// MSG と CD は記述がないけどたぶん一緒に出すだろう。
// タイミングもどこでか分からんのでとりあえずここで。
SetData(spc.temp_out);
SetXfer(spc.pctl_out);
if (set_atn_in_selection) {
AssertATN();
set_atn_in_selection = false;
}
AssertSEL();
uint64 t_SIDA = 11 * t_CLF - 30_nsec;
uint64 t_IDBH = 2 * t_CLF - 80_nsec;
CallAfter(Arbitration2, "SPC Arbitration2", t_SIDA + t_IDBH);
}
// アービトレーションフェーズ中、SEL を立ててから規定時間経過したので BSY を
// 下げるところ。
void
SPCDevice::Arbitration2(Event *ev)
{
putlog(3, "Arbitration2");
NegateBSY();
}
// アービトレーションフェーズを使わない場合のセレクションフェーズで、
// 規定時間経過したので SEL を立てるところ。
void
SPCDevice::Selection1(Event *ev)
{
putlog(3, "Selection1");
spc.ssts |= SPC::SSTS_SPC_BUSY;
// 本当は DB* -> SEL の順でタイミングも規定されてるけど省略。
// ここでは SEL がトリガーになるので SEL のアサートは最後に行う。
// アービトレーションなしなら PCTL に関係なくセレクションフェーズ (IO=0)
// を開始する(p.27)。MSG と CD は記述がないけどたぶん一緒に出すだろう。
SetData(spc.temp_out);
SetXfer(0);
AssertSEL();
}
// セレクションコマンドがタイムアウトした時に呼ばれるコールバック。
void
SPCDevice::SelectionTimeout(Event *ev)
{
if (loglevel >= 3) {
int tgtid;
uint8 data = GetData();
data &= ~(1U << myid);
tgtid = SCSIBus::DecodeID(data);
if (tgtid == -1) {
putlogn("Selection timeout");
} else {
putlogn("Selection timeout (#%u)", tgtid);
}
}
spc.ssts &= ~(SPC::SSTS_CONN_INIT | SPC::SSTS_SPC_BUSY);
// TC は実際には減算カウンタのようだが、とりあえず辻褄合わせ
spc.tc = 0;
SetINTS(SPC::INTS_TIMEOUT);
}
// SetATN コマンド実行
void
SPCDevice::SetATNCommand()
{
putlog(2, "SCMD Set ATN Command");
// Select コマンド前なら、セレクションフェーズで ATN を立てる指示。
// 接続中(転送フェーズ中?) なら即座に ATN を立てる。
switch (GetPhase()) {
case SCSI::Phase::BusFree:
set_atn_in_selection = true;
break;
case SCSI::Phase::Arbitration:
case SCSI::Phase::Selection:
case SCSI::Phase::Reselection:
// Select Command 実行中なので実質ここで呼ばれることはないはずだが
case SCSI::Phase::Transfer:
AssertATN();
break;
}
}
// Transfer コマンド実行
void
SPCDevice::TransferCommand()
{
putlog(2, "SCMD Transfer Command (xfer=%s len=0x%x)",
SCSI::GetXferPhaseName(spc.pctl_out), spc.tc);
// SCMD レジスタ書き込みのうち Transfer コマンドに関わる未実装機能の
// ログ表示は SCMD レジスタ書き込み時ではなくここで表示。
if ((spc.scmd & SPC::SCMD_TERM_MODE)) {
putlog(0, "SCMD Padding mode (NOT IMPLEMENTED)");
}
transfer_command_running = true;
spc.ssts |= SPC::SSTS_SPC_BUSY | SPC::SSTS_IN_PROGRESS;
// PCTL レジスタに設定してあるフェーズをバスに出す。
// XXX どこでかよく分からんけど、とりあえずこの辺で。
SetXfer(spc.pctl_out);
// 転送開始前に DREG キューを初期化する。
// MPU 側が DREG を全部読まずにフェーズを中断とかはたぶん出来るので。
// XXX 転送開始前なのかリセットなのか…
spc.dreg.Clear();
// Transfer コマンド発行時に REQ がアサートされていなければ
// アサートされるまで待つ。この場合、ここでは何もすることはなく、
// OnTransferReq() が呼ばれてそこから転送フェーズが動き始める。
if (GetREQ()) {
// コマンドから転送開始までちょっとだけかかる
CallAfter(HardwareTransfer, "SPC SCMD to Start", 4 * t_CLF);
}
}
// バスフリーコールバック (SCSIHostDevice から呼ばれる)。
// id はこのバスフリーを行ったデバイス (0..7)
void
SPCDevice::BusFreeCallback(uint id)
{
putlog(4, "BusFree Callback");
last_busfree_time = scheduler->GetVirtTime();
transfer_command_running = false;
spc.ssts &= ~(SPC::SSTS_CONN_INIT | SPC::SSTS_CONN_TARG);
spc.ssts &= ~(SPC::SSTS_SPC_BUSY | SPC::SSTS_IN_PROGRESS);
// マニュアルが分かりにくいんだけど、
// o イニシエータとして動作時に、SPC が自らバスフリーにした時
// o BusFree INT enable ビットが立っていて、ターゲットがバスフリーに
// した時
// のいずれかで INTS のバスフリー検出ビットを立てる
if (id != myid || spc.busfree_intr_enable) {
SetINTS(SPC::INTS_DISCONNECTED);
}
}
// ターゲットがセレクションに応答したコールバック
// (SCSIHostDevice から呼ばれる)
void
SPCDevice::SelectionAckCallback()
{
putlog(4, "SelectionAck Callback");
// タイムアウトイベントを停止
scheduler->StopEvent(timer_event);
spc.ssts &= ~SPC::SSTS_SPC_BUSY;
spc.ssts |= SPC::SSTS_CONN_INIT;
// Command Complete 割り込みを起こす
SetINTS(SPC::INTS_COMPLETE);
}
// 転送フェーズでターゲットが REQ を立てたコールバック
// (SCSIHostDevice から呼ばれる)
void
SPCDevice::TransferReqCallback()
{
// REQ が立てば Transfer Command 発行前でも Xfer In Progress は立てる
spc.ssts |= SPC::SSTS_IN_PROGRESS;
if (transfer_command_running) {
// Transfer Command 実行中なら REQ 待ちだったので
// ここでハードウェア転送を開始。
HardwareTransfer(phase_event);
}
// データシートでは、マニュアル転送で IN 方向の時データバスの内容を
// TEMP に取り込むと書いてある(p.78)。
// HardwareTransfer によって状態が変更される可能性があるため
// もう一回チェック。
if (transfer_command_running == false) {
if (GetIO()) {
spc.temp_in = GetData();
}
}
}
// ハードウェア転送の1バイト分。
// イベントコールバックとして呼び出すこと。
// REQ がアサートされているのを確認してから呼び出すこと。
// (1バイト送信するとターゲットが再び REQ を上げるなどしてくるので、
// OnTransferReq() か DREG 書き込みから再びここが呼ばれる)
void
SPCDevice::HardwareTransfer(Event *ev)
{
// SDGC_XFER_ENABLE は転送要求ごとに割り込みを上げる。
// (SPC から見て) 入力操作時は読み込めるデータがある場合、出力操作時は
// キューにバイトを書き込む必要がある場合に、割り込みを上げる。
// PCTL のフェーズと SCSI バスのフェーズが一致していれば転送(開始|継続)。
// そうでなければ Service Required 割り込み。
if (spc.pctl_out != GetXfer()) {
transfer_command_running = false;
putlog(3, "Hardware Transfer terminated (phase mismatch)");
SetINTS(SPC::INTS_SERV_REQ);
return;
}
// DREG が送受信不可能ならここでは何もせずに帰るだけでよい。
// その後 DREG アクセスによって送受信可能になればまたこれが呼ばれる。
if (GetIO()) {
// Input (ターゲット → イニシエータ)
bool queued = spc.dreg.Enqueue(GetData());
// SDGC_XFER_ENABLE なら読み込めるデータがある場合に割り込み。
if ((spc.sdgc & SPC::SDGC_XFER_ENABLE)) {
putlog(3, "SDGC XFER_OUT Interrupt");
SetINTS(SPC::INTS_XFER_OUT);
}
if (!queued) {
putlog(4, "Hardware Transfer (DREG full)");
return;
}
} else {
// Output (イニシエータ → ターゲット)
if (spc.dreg.Length() != 0) {
SetData(spc.dreg.Dequeue());
} else {
putlog(4, "Hardware Transfer (DREG empty)");
// SDGC_XFER_ENABLE ならキューに書き込む必要がある場合に割り込み。
if ((spc.sdgc & SPC::SDGC_XFER_ENABLE)) {
putlog(3, "SDGC XFER_OUT Interrupt");
SetINTS(SPC::INTS_XFER_OUT);
}
return;
}
}
AssertACK();
// 転送完了
spc.tc--;
putlog(4, "Hardware Transfer: %s q=%u tc=%u",
(GetIO() ? "IN" : "OUT"), spc.dreg.Length(), spc.tc);
if (spc.tc == 0) {
// どちらでも転送自体はここで完了(?)
transfer_command_running = false;
if (GetIO()) {
// Input 時は TC がゼロになって DREG キューを全部読み出した
// ところで完了なので、ここでは何もしない。
} else {
// Output ならここで転送完了。
TransferComplete();
}
}
// メッセージアウトフェーズの最終バイト送信中(?)に ATN を下げる
if (GetXfer() == SCSI::XferPhase::MsgOut && spc.tc == 0) {
NegateATN();
}
if (GetXfer() == SCSI::XferPhase::MsgIn && spc.tc == 0) {
// ただしメッセージインフェーズの最終バイトなら ACK を下げない。
} else {
NegateACK();
}
}
// 転送完了。
// HardwareTransfer() と Read() から呼ばれる。
void
SPCDevice::TransferComplete()
{
spc.ssts &= ~(SPC::SSTS_SPC_BUSY | SPC::SSTS_IN_PROGRESS);
SetINTS(SPC::INTS_COMPLETE);
}
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.