--- nono/vm/mpu88xx0.cpp 2026/04/29 17:04:32 1.1 +++ nono/vm/mpu88xx0.cpp 2026/04/29 17:05:25 1.1.1.14 @@ -1,22 +1,46 @@ // // nono -// Copyright (C) 2020 isaki@NetBSD.org +// Copyright (C) 2020 nono project +// Licensed under nono-license.txt // -// MPU (m88xx0) +// +// MPU (M88xx0) +// #include "mpu88xx0.h" #include "config.h" +#include "debugger.h" +#include "m88100disasm.h" +#include "scheduler.h" +#include "syncer.h" // コンストラクタ -MPU88xx0Device::MPU88xx0Device(uint32 addr) +MPU88xx0Device::MPU88xx0Device() + : inherited() { - monitor.Init(79, 15); + SetName("MPU(88100)"); - // リセットベクタアドレスは Reset() で指定するので保持しておく - reset_vector = addr; - - cpu.reset(new m88kcpu()); + // LUNA-88K では CMMU の ID は次のように割り振ってあるようだ。 + // OpenBSD の sys/arch/luna68k/include/board.h より。 + // + // Inst Data + // CPU#0 ID=7 ID=6 + // CPU#1 ID=5 ID=4 + // CPU#2 ID=3 ID=2 + // CPU#3 ID=1 ID=0 + // + // 厳密にはこれは LUNA-88K のハードウェアがどういうコンフィグレーションで + // m88200 を使うかなので、ここではなくて vm_luna あたりのほうがいいかも + // しれんけど、あまり遠いのも面倒なので、とりあえずこの辺に。 + // あとマルチプロセッサになったらまたその時考える。 + cmmu[0].reset(new m88200(this, 7)); + cmmu[1].reset(new m88200(this, 6)); + + // レジスタモニター + monitor.func = ToMonitorCallback(&MPU88xx0Device::MonitorUpdate); + monitor.SetSize(79, 28); + monitor.Regist(ID_MONITOR_MPUREG); } // デストラクタ @@ -24,93 +48,449 @@ MPU88xx0Device::~MPU88xx0Device() { } +// 初期化 +bool +MPU88xx0Device::Init() +{ + if (inherited::Init() == false) { + return false; + } + + debugger = GetDebugger(); + + const ConfigItem& item_stop = gConfig->Find("mpu-pseudo-stop"); + pseudo_stop_enable = (bool)item_stop.AsInt(); + + const ConfigItem& item_altname = gConfig->Find(".m88k-altname"); + m88100disasm::use_altname = item_altname.AsInt(); + + return true; +} + // リセット -// (MPU が動くのはこの後 Scheduler::Run() を発行した後) void -MPU88xx0Device::ResetHard() +MPU88xx0Device::ResetHard(bool poweron) { - cpu->Reset(reset_vector); + if (poweron) { + // レジスタのうち不定と明記されてるものは、未初期化のまま触ったことが + // 分かりやすいような適当なパターンで埋めておく。ただし最上位も c に + // すると BT45x のレジスタ付近を指してしまうので、それは避けておく。 + const uint32 Undefined = 0x0ccccccc; + for (int i = 1; i < 32; i++) { + reg.r[i] = Undefined; + } + for (int i = 0; i < countof(reg.fcr); i++) { + reg.fcr[i] = Undefined & reg.fcr_mask[i]; + } + + reg.epsr = Undefined; + reg.ssbr = Undefined; + reg.sfip = Undefined; + reg.snip = Undefined; + reg.sxip = Undefined; + // DMTx は bit0(Valid) をクリア。DMAx/DMDx は不定。 + reg.dma0 = Undefined; + reg.dma1 = Undefined; + reg.dma2 = Undefined; + reg.dmd0 = Undefined; + reg.dmd1 = Undefined; + reg.dmd2 = Undefined; + + // アクセスが一度もない時にどのアドレスとも一致しないようにしておく。 + lastaddr = (uint64)-1; + + // サイクルを初期化。 + used_cycle = 0; + + // 履歴は電源オン時だけ初期化。 + // (LUNA-88K は ROM が正常パスで CPU リセットを行ったりするので) + exhist.Clear(); + brhist.Clear(); + + // この後起きるリセット例外で xip を初期化する前に参照することに + // なるので、これだけここでも初期化しておく。 + reg.xip = 0; + } + resetting = true; + + // リセット例外を 8+256 クロック後に起こす。 + // 8+256 は、電源オン時に最低これだけアサートしなければならないという値 + // のはずで、実際これだけかかるとかいう値ではない。が、こちら側の都合で + // この後実行される RAM の ResetHard() がブートページを用意した後で + // cpu->Reset() で xip をフェッチするという順序にしないといけないため、 + // リセット例外が起動するまでに時間がかかるというところを都合よく真似て + // おく。 + + exec_event.func = ToEventCallback(&MPU88xx0Device::ResetCallback); + exec_event.time = (8 + 256) * clock_nsec; + exec_event.SetName(GetName() + " Reset"); + scheduler->RestartEvent(exec_event); +} + +// リセット例外コールバック。 +// MPU リセットから規定時間後に呼ばれる。 +void +MPU88xx0Device::ResetCallback(Event& ev) +{ + resetting = false; + + // リセット例外を実行 + uint64 cycle_start = used_cycle; + + // 例外履歴に記録 (例外発生はブランチ履歴にも記録) + // リセット例外も発生時の XIP を記録する。(LUNA-88K の PROM 1.20) + exhist.AddEntry(reg.xip | 1, 0, 0xfc000000 | 0); + brhist.AddEntry(reg.xip | 1, 0, 0xfc000000 | 0); + // デバッガに通知 (例外ブレーク用) + debugger->NotifyExceptionMain(0); + + // 内部の割り込み状態をクリア + intr_pending = 0; + ChangeState(CPU_STATE_NORMAL); + + // 実際どこかは分からんけど PSR 設定の前にリセットしておきたい + cmmu[0]->Reset(); + cmmu[1]->Reset(); + + // Table 6-8 (p6-47) + reg.psr = 0x800003ff; + // XXX ただし SFU1 は未実装なので下げておく + reg.psr &= ~PSR_SFD1; + +#if defined(TEST_INTERRUPT) + reg.psr &= ~PSR_IND; + reg.psr &= ~PSR_SFRZ; +#endif + + SetPSR(); + + reg.fip = 0; + + // レジスタのうち明記されてるものをクリア。 + // 不定と明記されてるものはリセットでは触らないでおく。 + // scoreboard: cleared + reg.nip = 0; + reg.xip = 0; + reg.vbr = 0; + reg.fpecr = 0; + reg.fpcr = 0; + reg.fpsr = 0; + // DMTx は bit0(Valid) をクリア。他は不定 + reg.dmt0 &= ~DM_VALID; + reg.dmt1 &= ~DM_VALID; + reg.dmt2 &= ~DM_VALID; + + fetch(); + + // リセット例外はここまで。 + // ここからは最初の命令のプリフェッチ + Prefetch(); + + // コールバックを ResetCallback から Exec* に差し替える前のここで + // トレース状態を初期化する。 + SetTrace(debugger->IsTrace()); + + // 以降は通常走行 + int cycle = used_cycle - cycle_start; + exec_event.func = exec_normal; + exec_event.time = cycle * clock_nsec; + exec_event.SetName(GetName() + " Execute"); + scheduler->StartEvent(exec_event); } +// トレース実行のコールバック +void +MPU88xx0Device::ExecTrace(Event& ev) +{ + debugger->ExecMain(); + ExecNormal(ev); +} + +// トレース状態を設定する +void +MPU88xx0Device::SetTrace(bool enable) +{ + if (enable) { + exec_normal = ToEventCallback(&MPU88xx0Device::ExecTrace); + } else { + exec_normal = ToEventCallback(&MPU88xx0Device::ExecNormal); + } + exec_event.func = exec_normal; +} + +void +MPU88xx0Device::ChangeState(uint32 new_state) +{ + // new_state (CPU_STATE_*) と + // RequestCPUMode() の引数 Syncer::SCHED_CPU_* は + // 実は同じ値なのでそのまま渡してよいことにする。 + static_assert(CPU_STATE_NORMAL == Syncer::SCHED_CPU_NORMAL, ""); + static_assert(CPU_STATE_STOP == Syncer::SCHED_CPU_STOP, ""); + + if (cpu_state != new_state) { + cpu_state = new_state; + // スケジューラに通知 + syncer->RequestCPUMode(new_state); + } +} + +// アクセス中の論理アドレスを取得 +uint32 +MPU88xx0Device::GetLaddr() const +{ + m88200 *m = m88200::GetBusMaster(); + assert(m); + return m->GetLaddr(); +} + +// アクセス中の物理アドレスを取得 +uint32 +MPU88xx0Device::GetPaddr() const +{ + m88200 *m = m88200::GetBusMaster(); + assert(m); + return m->GetPaddr(); +} + +// Interrupt +void +MPU88xx0Device::Interrupt(int level) +{ + // level は 0 か 1。 + intr_pending = level; + + if (intr_pending && IsIntrEnable() && (cpu_state == CPU_STATE_STOP)) { + // 割り込みを受け付けたら STOP 解除。 + ChangeState(CPU_STATE_NORMAL); + } +} + +// DOS call エミュレーションのコールバックを指定 +void +MPU88xx0Device::SetFLineCallback(bool (*callback)(MPU88xx0Device *, void *), + void *arg) +{ + fline_callback = callback; + fline_arg = arg; +} + +#define FA(val, bit) TA::OnOff((val) & M88100::bit) + // モニター更新 -bool -MPU88xx0Device::MonitorUpdate() +void +MPU88xx0Device::MonitorUpdate(Monitor *, TextScreen& screen) { - m88100reg reg; + m88100reg tmp; int x; int y; - monitor.Clear(); + screen.Clear(); // ローカルにコピー - reg = *cpu; + tmp = reg; // for (int i = 0; i < countof(reg.r); i++) { - monitor.Print((i / 8) * 14, i % 8, "r%-2d:%08x", i, reg.r[i]); + screen.Print((i / 8) * 14, i % 8, "r%-2d:%08x", i, tmp.r[i]); } // 制御レジスタ x = 0; y = 9; - monitor.Print(x, y++, "pid (cr0):%08x(arch=$%02x ver=$%02x %c)", - reg.pid, - (reg.pid >> 8) & 0xff, - (reg.pid & 0xfe), - (reg.pid & 1) ? 'M' : '-'); - monitor.Print(x, y++, "psr (cr1):%08x(%c,%s,%s,%s %s,%s,%s,%s)", - reg.psr, - (reg.psr & m88100reg::PSR_SUPER) ? 'S' : '-', - (reg.psr & m88100reg::PSR_BO_LE) ? "LE" : "BE", - (reg.psr & m88100reg::PSR_SER) ? "CON" : "SER", - (reg.psr & m88100reg::PSR_C) ? "Cy" : "--", - (reg.psr & m88100reg::PSR_SFD1) ? "SFD1" : "----", - (reg.psr & m88100reg::PSR_MXM) ? "MXM" : "---", - (reg.psr & m88100reg::PSR_IND) ? "IND" : "---", - (reg.psr & m88100reg::PSR_SFRZ) ? "SFRZ" : "----"); - monitor.Print(x, y++, "epsr(cr2):%08x(%c,%s,%s,%s %s,%s,%s,%s)", - reg.epsr, - (reg.epsr & m88100reg::PSR_SUPER) ? 'S' : '-', - (reg.epsr & m88100reg::PSR_BO_LE) ? "LE" : "BE", - (reg.epsr & m88100reg::PSR_SER) ? "CON" : "SER", - (reg.epsr & m88100reg::PSR_C) ? "Cy" : "--", - (reg.epsr & m88100reg::PSR_SFD1) ? "SFD1" : "----", - (reg.epsr & m88100reg::PSR_MXM) ? "MXM" : "---", - (reg.epsr & m88100reg::PSR_IND) ? "IND" : "---", - (reg.epsr & m88100reg::PSR_SFRZ) ? "SFRZ" : "----"); + screen.Print(x, y++, "pid (cr0):%08x(Arch=$%02x Ver=$%02x MC=%s)", + tmp.pid, + (tmp.pid >> 8) & 0xff, + (tmp.pid >> 1) & 0x7f, + (tmp.pid & 1) ? "Master" : "Checker"); + screen.Print(x, y, "psr (cr1):%08x(", tmp.psr); + screen.Puts(x + 19, y, FA(tmp.psr, PSR_SUPER), "S"); + screen.Puts(x + 21, y, + ((tmp.psr & M88100::PSR_BO_LE) ? "BO=LE" : "BO=BE")); + screen.Puts(x + 27, y, FA(tmp.psr, PSR_SER), "SER"); + screen.Puts(x + 31, y, FA(tmp.psr, PSR_C), "Cy"); + screen.Puts(x + 34, y, FA(tmp.psr, PSR_SFD1), "SFD1"); + screen.Puts(x + 39, y, FA(tmp.psr, PSR_MXM), "MXM"); + screen.Puts(x + 43, y, FA(tmp.psr, PSR_IND), "IND"); + screen.Puts(x + 47, y, FA(tmp.psr, PSR_SFRZ), "SFRZ"); + screen.Puts(x + 51, y, ")"); + y++; + screen.Print(x, y, "epsr(cr2):%08x(", tmp.epsr); + screen.Puts(x + 19, y, FA(tmp.epsr, PSR_SUPER), "S"); + screen.Puts(x + 21, y, + ((tmp.epsr& M88100::PSR_BO_LE) ? "BO=LE" : "BO=BE")); + screen.Puts(x + 27, y, FA(tmp.epsr, PSR_SER), "SER"); + screen.Puts(x + 31, y, FA(tmp.epsr, PSR_C), "Cy"); + screen.Puts(x + 34, y, FA(tmp.epsr, PSR_SFD1), "SFD1"); + screen.Puts(x + 39, y, FA(tmp.epsr, PSR_MXM), "MXM"); + screen.Puts(x + 43, y, FA(tmp.epsr, PSR_IND), "IND"); + screen.Puts(x + 47, y, FA(tmp.epsr, PSR_SFRZ), "SFRZ"); + screen.Puts(x + 51, y, ")"); - // 制御レジスタ(左下; *IP) + // 制御レジスタ(左中; *IP) x = 0; y = 12; - monitor.Print(x, y + 0, "xip:%08x opX:%08x(%c%c)", - reg.xip, (uint32)reg.opX, - cpu->OpIsBusErr(reg.opX) ? 'B' : '-', - cpu->OpIsDelay(reg.opX) ? 'D' : '-'); - monitor.Print(x, y + 1, "nip:%08x opF:%08x(%c%c)", - reg.nip, (uint32)reg.opF, - cpu->OpIsBusErr(reg.opF) ? 'B' : '-', - cpu->OpIsDelay(reg.opF) ? 'D' : '-'); - monitor.Print(x, y + 2, "fip:%08x", reg.fip); + screen.Print(x, y + 0, "xip:%08x opX:%08x(%c)", + tmp.xip, (uint32)tmp.opX, + OpIsBusErr(tmp.opX) ? 'B' : '-'); + screen.Print(x, y + 1, "nip:%08x opF:%08x(%c)", + tmp.nip, (uint32)tmp.opF, + OpIsBusErr(tmp.opF) ? 'B' : '-'); + screen.Print(x, y + 2, "fip:%08x", tmp.fip); // 制御レジスタ (S*IP) x = 32; for (int i = 0; i < 3; i++) { - monitor.Print(x, y + i, "%s(cr%d):%08x:%c%c", + screen.Print(x, y + i, "%s(cr%d):%08x:%c%c", m88100reg::sipname[i], 4 + i, - (reg.cr[4 + i] & m88100reg::SIP_MASK), - (reg.cr[4 + i] & m88100reg::SIP_V) ? 'V' : '-', - (reg.cr[4 + i] & m88100reg::SIP_E) ? 'E' : '-'); + (tmp.cr[4 + i] & M88100::SIP_MASK), + (tmp.cr[4 + i] & M88100::SIP_V) ? 'V' : '-', + (tmp.cr[4 + i] & M88100::SIP_E) ? 'E' : '-'); } - // 制御レジスタ(右列) + // 制御レジスタ(右中) x = 55; y = 9; - monitor.Print(x, y++, "ssbr(cr3):%08x", reg.ssbr); - monitor.Print(x, y++, "vbr (cr7):%08x", reg.vbr); for (int i = 0; i < 4; i++) { int rn = 17 + i; - monitor.Print(x, y + i, "sr%d(cr%d):%08x", i, rn, reg.cr[rn]); + screen.Print(x, y++, "sr%d(cr%d):%08x", i, rn, tmp.cr[rn]); } + screen.Print(x, y++, "ssbr(cr3):%08x", tmp.ssbr); + screen.Print(x, y++, "vbr (cr7):%08x", tmp.vbr); - return true; + // MMU レジスタ(次段) + y = 15; + for (int i = 0; i <= 2; i++) { + int dt = 8 + i * 3; + int dd = 9 + i * 3; + int da = 10 + i * 3; + + screen.Print(0, y, "dmt%d(cr%-2d):%04x", + i, dt, (tmp.cr[dt] & 0xffff)); + screen.Puts(15, y, "(b,s,d,l,rx, s,en ,w,v)"); + screen.Puts(16, y, (tmp.cr[dt] & M88100::DM_BO) ? "B" : "-"); + screen.Puts(18, y, (tmp.cr[dt] & M88100::DM_DAS) ? "S" : "U"); + screen.Puts(20, y, (tmp.cr[dt] & M88100::DM_DOUB1) ? "D" : "-"); + screen.Puts(22, y, (tmp.cr[dt] & M88100::DM_LOCK) ? "L" : "-"); + // カンマが自然に見えるようにここだけカンマも含めて前詰めで出力 + screen.Print(25, y, "%d,", + (tmp.cr[dt] & M88100::DM_DREG_MASK) >> 7); + screen.Puts(28, y, (tmp.cr[dt] & M88100::DM_SIGNED) ? "S" : "-"); + uint32 en = (tmp.cr[dt] & M88100::DM_EN_MASK) >> 2; + screen.Puts(30, y, m88100reg::dmt_en_str[en]); + screen.Puts(35, y, (tmp.cr[dt] & M88100::DM_WRITE) ? "W" : "-"); + screen.Puts(37, y, (tmp.cr[dt] & M88100::DM_VALID) ? "V" : "-"); + + screen.Print(40, y, "dmd%d(cr%-2d):%08x", i, dd, tmp.cr[dd]); + screen.Print(60, y, "dma%d(cr%2d):%08x", i, da, tmp.cr[da]); + y++; + } + y++; + + // FPU レジスタ (下段) + screen.Print(0, y, "fpecr(fcr0):%08x(", tmp.fpecr); + x = 21; + screen.Puts(x + 0, y, FA(tmp.fpecr, FPECR_FIOV), "FIOV"); + screen.Puts(x + 5, y, FA(tmp.fpecr, FPECR_FUNIMP), "FUNIMP"); + screen.Puts(x + 12, y, FA(tmp.fpecr, FPECR_FPRV), "FPRV"); + screen.Puts(x + 17, y, FA(tmp.fpecr, FPECR_FROP), "FROP"); + screen.Puts(x + 22, y, FA(tmp.fpecr, FPECR_FDVZ), "FDVZ"); + screen.Puts(x + 27, y, FA(tmp.fpecr, FPECR_FUNF), "FUNF"); + screen.Puts(x + 32, y, FA(tmp.fpecr, FPECR_FOVF), "FOVF"); + screen.Puts(x + 37, y, FA(tmp.fpecr, FPECR_FINX), "FINX"); + screen.Puts(x + 41, y, ")"); + y++; + + screen.Print(0, y++, + "fppt(fcr5): %08x(OpCode=%%%c%c%c%c%c T1=%d T2=%d TD=%d Dest=r%d)", + tmp.fppt, + ((tmp.fppt >> 15) & 1) + '0', + ((tmp.fppt >> 14) & 1) + '0', + ((tmp.fppt >> 13) & 1) + '0', + ((tmp.fppt >> 12) & 1) + '0', + ((tmp.fppt >> 11) & 1) + '0', + ((tmp.fppt >> 9) & 3), + ((tmp.fppt >> 7) & 3), + ((tmp.fppt >> 5) & 3), + (tmp.fppt & M88100::FPPT_DEST)); + + screen.Print(0, y++, "fphs1(fcr1)/fpls1(fcr2): %08x'%08x", + tmp.fphs1, tmp.fpls1); + screen.Print(0, y++, "fphs2(fcr3)/fpls2(fcr4): %08x'%08x", + tmp.fphs2, tmp.fpls2); + + screen.Print(0, y, "fprh(fcr6) /fprl(fcr7): %08x'%08x(", + tmp.fprh, tmp.fprl); + x = 43; + screen.Puts(x + 0, y, FA(tmp.fprh, FPRH_ADDONE), "ADDONE"); + screen.Print(x + 7, y, "RM=%s)", rmstr[(tmp.fprh >> 29) & 3]); + y++; + + x = 21; + screen.Print(0, y, "fpit(fcr8): %08x(OpCode=%%%c%c%c%c%c TD=%d", + tmp.fpit, + ((tmp.fpit >> 15) & 1) + '0', + ((tmp.fpit >> 14) & 1) + '0', + ((tmp.fpit >> 13) & 1) + '0', + ((tmp.fpit >> 12) & 1) + '0', + ((tmp.fpit >> 11) & 1) + '0', + ((tmp.fpit >> 10) & 1)); + screen.Puts(x + 19, y, FA(tmp.fpit, FPIT_EFINV), "EINV"); + screen.Puts(x + 24, y, FA(tmp.fpit, FPIT_EFDVZ), "EDVZ"); + screen.Puts(x + 29, y, FA(tmp.fpit, FPIT_EFUNF), "EUNF"); + screen.Puts(x + 34, y, FA(tmp.fpit, FPIT_EFOVF), "EOVF"); + screen.Puts(x + 39, y, FA(tmp.fpit, FPIT_EFINX), "EINX"); + screen.Print(x + 44, y, "Dest=r%d)", tmp.fpit & M88100::FPIT_DEST); + y++; + + uint32 sign = (tmp.fprh & M88100::FPRH_SIGN); + uint32 exp = (int32)tmp.fpit >> 20; + uint32 mant_h = (tmp.fprh & 0x000fffff); + uint32 mant_l = tmp.fprl; + screen.Print(12, y, "result: %c%1x.%05x'%08x'grs e%+d", + (sign ? '-' : '+'), + ((tmp.fprh >> 20) & 1), + mant_h, + mant_l, + exp - 0x3ff); + screen.Puts(38, y, FA(tmp.fprh, FPRH_GUARD), "G"); + screen.Puts(39, y, FA(tmp.fprh, FPRH_ROUND), "R"); + screen.Puts(40, y, FA(tmp.fprh, FPRH_STICKY), "S"); + screen.Print(48, y, "($%03x)", exp & 0x7ff); + // Inf と NAN はこの形式だけからでは分かりづらいので別途追加で表示。 + // exp は符号拡張しているので指数部 $7ff は exp == -1。 + if (exp == 0xffffffff) { + x = 55; + if ((mant_h | mant_l) == 0) { + if (sign == 0) { + screen.Puts(x, y, "= +Inf"); + } else { + screen.Puts(x, y, "= -Inf"); + } + } else { + if (sign == 0) { + screen.Puts(x, y, "= +NAN"); + } else { + screen.Puts(x, y, "= -NAN"); + } + } + } + y++; + + screen.Print(0, y, "fpsr(fcr62):%08x(", tmp.fpsr); + x = 21; + screen.Puts(x + 0, y, FA(tmp.fpsr, FPSR_AFINV), "AFINV"); + screen.Puts(x + 6, y, FA(tmp.fpsr, FPSR_AFDVZ), "AFDVZ"); + screen.Puts(x + 12, y, FA(tmp.fpsr, FPSR_AFUNF), "AFUNF"); + screen.Puts(x + 18, y, FA(tmp.fpsr, FPSR_AFOVF), "AFOVF"); + screen.Puts(x + 24, y, FA(tmp.fpsr, FPSR_AFINX), "AFINX"); + screen.Puts(x + 29, y, ")"); + y++; + + screen.Print(0, y, "fpcr(fcr63):%08x(", tmp.fpcr); + screen.Puts(x + 0, y, FA(tmp.fpcr, FPCR_EFINV), "EFINV"); + screen.Puts(x + 6, y, FA(tmp.fpcr, FPCR_EFDVZ), "EFDVZ"); + screen.Puts(x + 12, y, FA(tmp.fpcr, FPCR_EFUNF), "EFUNF"); + screen.Puts(x + 18, y, FA(tmp.fpcr, FPCR_EFOVF), "EFOVF"); + screen.Puts(x + 24, y, FA(tmp.fpcr, FPCR_EFINX), "EFINX"); + screen.Print(x + 30, y, "RM=%s)", rmstr[(tmp.fpcr >> 14) & 3]); }