diff --git a/examples/gpio_blink/firmware/main.c b/examples/gpio_blink/firmware/main.c index a984ee4..6fc5567 100644 --- a/examples/gpio_blink/firmware/main.c +++ b/examples/gpio_blink/firmware/main.c @@ -16,14 +16,10 @@ void reset_handler(void) { /* Set PA5 to output push-pull, 2MHz (CNF5=00, MODE5=10) */ GPIOA_CRH = (GPIOA_CRH & ~(0xF << 20)) | (0x2 << 20); - for (int i = 0; i < 3; i++) { + while (1) { GPIOA_BSRR = (1 << 5); /* Set PA5 */ - delay(100000); + delay(20000); GPIOA_BSRR = (1 << (16 + 5)); /* Reset PA5 */ - delay(100000); - } - - while (1) { - __asm volatile("nop"); + delay(20000); } } diff --git a/examples/gpio_blink/runner.cpp b/examples/gpio_blink/runner.cpp index c98a5b2..9d767d4 100644 --- a/examples/gpio_blink/runner.cpp +++ b/examples/gpio_blink/runner.cpp @@ -1,8 +1,15 @@ +// gpio_blink example runner — loads blink firmware and runs it. +// +// Default: runs forever (no max-steps) since the firmware blinks in an +// infinite loop. Each PA5 edge is printed as it happens — that's the visible +// "blink" on a headless run. Stop with Ctrl+C. #include "chips/stm32f1/soc/stm32f103_soc.hpp" +#include "cpu/cpu.hpp" +#include "sim/coordinator.hpp" #include #include -#include +#include #include #include @@ -31,12 +38,18 @@ int main(int argc, char** argv) { return 1; } + // Each PA5 edge prints live — the visible blink on a headless run. int toggle_count = 0; (*soc)->parts().gpioa.set_pin_change_callback( - [&](uint8_t pin, bool /*high*/) { - if (pin == 5) { - toggle_count++; + [&](uint8_t pin, bool high) { + if (pin != 5) { + return; } + ++toggle_count; + printf("PA5 %s (toggle #%d)\n", + high ? "HIGH \xE2\x97\x8F" : "LOW \xC2\xB7", + toggle_count); + fflush(stdout); }); auto r = (*soc)->load_elf(data); @@ -45,8 +58,18 @@ int main(int argc, char** argv) { return 1; } - (*soc)->run(200000); - - printf("GPIO PA5 toggled %d times\n", toggle_count); - return toggle_count >= 6 ? 0 : 1; // 3 on + 3 off = 6 + // Run forever; the firmware loops, the user stops with Ctrl+C. A fault + // breaks out and reports the state + how far it got. + fprintf(stderr, "blink running — Ctrl+C to stop\n"); + while (true) { + const auto res = (*soc)->run(100000); + if (res != sim::RunResult::Running) { + const auto state = (*soc)->machine().cpu->state().value_or( + cpu::CPU::State::Halted); + fprintf(stderr, "stopped: state=%d (after %d toggles)\n", + static_cast(state), toggle_count); + break; + } + } + return 0; } diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index a9ba0a3..fd1968c 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -10,6 +10,13 @@ set(CMAKE_AUTOMOC ON) add_executable(micro-forge-gui main.cpp main_window.cpp + model/session.cpp + view/panels/registers_panel.cpp + view/panels/gpio_panel.cpp + view/panels/serial_panel.cpp + view/panels/status_panel.cpp + view/panels/fault_panel.cpp + view/panels/peripheral_panel.cpp ) # main.cpp / main_window.cpp include "gui/main_window.hpp" (header sits next to # the .cpp here), so the include root is the repo top, not src/. diff --git a/gui/main_window.cpp b/gui/main_window.cpp index de70fb6..4304261 100644 --- a/gui/main_window.cpp +++ b/gui/main_window.cpp @@ -1,80 +1,102 @@ -// micro-forge GUI main window implementation (G5b/G5c). +// micro-forge GUI main window implementation. +// +// QMainWindow shell assembling dockable panels around a central serial output. +// Drives the session via QTimer and refreshes every panel from the snapshot +// each tick. A speed selector throttles how many steps per tick — firmware +// that uses big software delays (e.g. blink) needs a higher gear to look live. +// Owns no simulator state — that's in model::Session. #include "gui/main_window.hpp" -#include "introspection/introspection.hpp" +#include "gui/view/panels/fault_panel.hpp" +#include "gui/view/panels/gpio_panel.hpp" +#include "gui/view/panels/peripheral_panel.hpp" +#include "gui/view/panels/registers_panel.hpp" +#include "gui/view/panels/serial_panel.hpp" +#include "gui/view/panels/status_panel.hpp" + #include "cpu/cpu.hpp" -#include -#include -#include +#include +#include #include -#include #include #include -#include -#include +#include #include -#include -#include -#include -#include +#include +#include namespace micro_forge::gui { -namespace { - -std::vector read_file(const QString& path) { - std::ifstream f(path.toUtf8().constData(), std::ios::binary); - return {std::istreambuf_iterator(f), {}}; -} - -bool is_elf(const std::vector& d) { - return d.size() >= 4 && d[0] == 0x7f && d[1] == 'E' && d[2] == 'L' && - d[3] == 'F'; -} - -} // namespace - MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) - : QMainWindow(parent), firmware_path_(firmware_path) { + : QMainWindow(parent) { setWindowTitle("micro-forge"); - auto* central = new QWidget; - auto* root = new QVBoxLayout(central); + session_.set_firmware(firmware_path.toStdString()); - // ── control bar ── - auto* bar = new QHBoxLayout; + // ── toolbar: run / step / reset + state + speed ── + auto* toolbar = addToolBar("main"); run_btn_ = new QPushButton("Run"); auto* step_btn = new QPushButton("Step"); auto* reset_btn = new QPushButton("Reset"); - bar->addWidget(run_btn_); - bar->addWidget(step_btn); - bar->addWidget(reset_btn); - bar->addStretch(); state_label_ = new QLabel("idle"); state_label_->setStyleSheet("font-family: monospace;"); - bar->addWidget(state_label_); - root->addLayout(bar); - - // ── CPU registers (r0-r12, sp, lr, pc) ── - regs_table_ = new QTableWidget(16, 2); - regs_table_->setHorizontalHeaderLabels({"reg", "value"}); - regs_table_->verticalHeader()->setVisible(false); - regs_table_->horizontalHeader()->setStretchLastSection(true); - regs_table_->setEditTriggers(QAbstractItemView::NoEditTriggers); - root->addWidget(regs_table_); - - // ── GPIO panel: A/B/C port ODR with per-pin LED glyphs ── - auto* gpio_title = new QLabel("GPIO output (A/B/C, pin0..pin15)"); - gpio_title->setStyleSheet("font-weight: bold;"); - root->addWidget(gpio_title); - gpio_label_ = new QLabel; - gpio_label_->setStyleSheet("font-family: monospace;"); - root->addWidget(gpio_label_); - - setCentralWidget(central); - resize(480, 720); + toolbar->addWidget(run_btn_); + toolbar->addWidget(step_btn); + toolbar->addWidget(reset_btn); + toolbar->addSeparator(); + toolbar->addWidget(state_label_); + toolbar->addSeparator(); + toolbar->addWidget(new QLabel(tr("Speed:"))); + speed_combo_ = new QComboBox; + // Steps per tick (~20 ticks/sec). Higher gears make software-delay + // firmware (blink loops) visibly animate instead of crawling. + speed_combo_->addItem(tr("1×"), 20000); + speed_combo_->addItem(tr("5×"), 100000); + speed_combo_->addItem(tr("25×"), 500000); + speed_combo_->addItem(tr("100×"), 2000000); + speed_combo_->setCurrentIndex(0); + toolbar->addWidget(speed_combo_); + + // ── panels ── + regs_panel_ = new panels::RegistersPanel; + status_panel_ = new panels::StatusPanel; + serial_panel_ = new panels::SerialPanel; + fault_panel_ = new panels::FaultPanel; + gpio_panel_ = new panels::GpioPanel; + periph_panel_ = new panels::PeripheralPanel; + + // Central: serial output (the surface watched while firmware runs). + setCentralWidget(serial_panel_); + + // Left dock: CPU registers. + auto* regs_dock = new QDockWidget("CPU registers", this); + regs_dock->setObjectName("regs_dock"); + regs_dock->setWidget(regs_panel_); + addDockWidget(Qt::LeftDockWidgetArea, regs_dock); + + // Right dock: status / fault / peripherals (stacked vertically). + auto* status_dock = new QDockWidget("Status / masks", this); + status_dock->setObjectName("status_dock"); + status_dock->setWidget(status_panel_); + addDockWidget(Qt::RightDockWidgetArea, status_dock); + + auto* fault_dock = new QDockWidget("Fault", this); + fault_dock->setObjectName("fault_dock"); + fault_dock->setWidget(fault_panel_); + addDockWidget(Qt::RightDockWidgetArea, fault_dock); + + auto* periph_dock = new QDockWidget("Peripherals", this); + periph_dock->setObjectName("periph_dock"); + periph_dock->setWidget(periph_panel_); + addDockWidget(Qt::RightDockWidgetArea, periph_dock); + + // Bottom dock: GPIO. + auto* gpio_dock = new QDockWidget("GPIO", this); + gpio_dock->setObjectName("gpio_dock"); + gpio_dock->setWidget(gpio_panel_); + addDockWidget(Qt::BottomDockWidgetArea, gpio_dock); connect(run_btn_, &QPushButton::clicked, this, &MainWindow::onRunClicked); connect(step_btn, &QPushButton::clicked, this, &MainWindow::onStepClicked); @@ -85,7 +107,7 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) timer_->setInterval(50); // ~20 UI ticks/sec connect(timer_, &QTimer::timeout, this, &MainWindow::onTick); - rebuildSoc(); + rebuildSession(); refreshFromSnapshot(); // Test/CI hook: auto-run under offscreen so a headless launch exercises @@ -96,35 +118,14 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) run_btn_->setText("Pause"); timer_->start(); } + + resize(1100, 760); } -void MainWindow::rebuildSoc() { - usart_output_.clear(); - auto created = chips::stm32f1::Stm32f103Soc::create(); - if (!created) { - soc_.reset(); - state_label_->setText("SoC create failed"); - return; - } - soc_ = std::move(*created); - soc_->parts().event_bus.uart.connect( - [this](const micro_forge::hooks::UartByte& e) { - usart_output_ += static_cast(e.byte); - }); - - if (!firmware_path_.isEmpty()) { - firmware_data_ = read_file(firmware_path_); - if (firmware_data_.empty()) { - state_label_->setText(QString("cannot read: ") + firmware_path_); - return; - } - auto lr = is_elf(firmware_data_) - ? soc_->load_elf(firmware_data_) - : soc_->load_bin(0x08000000u, firmware_data_); - if (!lr) { - state_label_->setText( - QString("load failed: ") + QString::fromStdString(lr.error())); - } +void MainWindow::rebuildSession() { + auto r = session_.rebuild(); + if (!r) { + state_label_->setText(QString::fromStdString(r.error())); } } @@ -141,32 +142,32 @@ void MainWindow::onRunClicked() { } void MainWindow::onStepClicked() { - if (soc_) { - soc_->run(1); - refreshFromSnapshot(); - } + session_.step(); + refreshFromSnapshot(); } void MainWindow::onResetClicked() { running_ = false; run_btn_->setText("Run"); timer_->stop(); - rebuildSoc(); + rebuildSession(); refreshFromSnapshot(); } void MainWindow::onTick() { - if (soc_ && running_) { - soc_->run(20000); + if (running_ && session_.valid()) { + const auto steps = + static_cast(speed_combo_->currentData().toInt()); + session_.run(steps); refreshFromSnapshot(); } } void MainWindow::refreshFromSnapshot() { - if (!soc_) { + if (!session_.valid()) { return; } - const auto snap = introspection::read_introspection(*soc_, usart_output_); + const auto snap = session_.snapshot(); const char* st = "?"; switch (snap.cpu.state) { @@ -189,40 +190,12 @@ void MainWindow::refreshFromSnapshot() { } state_label_->setText(label); - static constexpr const char* kNames[16] = { - "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", - "r8", "r9", "r10", "r11", "r12", "sp", "lr", "pc"}; - auto hex = [](uint32_t v) { - return QString("0x%1").arg(v, 8, 16, QLatin1Char('0')); - }; - for (int i = 0; i < 13; ++i) { - regs_table_->setItem(i, 0, new QTableWidgetItem(kNames[i])); - regs_table_->setItem(i, 1, new QTableWidgetItem(hex(snap.cpu.regs[i]))); - } - regs_table_->setItem(13, 0, new QTableWidgetItem(kNames[13])); - regs_table_->setItem(13, 1, new QTableWidgetItem(hex(snap.cpu.sp))); - regs_table_->setItem(14, 0, new QTableWidgetItem(kNames[14])); - regs_table_->setItem(14, 1, new QTableWidgetItem(hex(snap.cpu.lr))); - regs_table_->setItem(15, 0, new QTableWidgetItem(kNames[15])); - regs_table_->setItem(15, 1, new QTableWidgetItem(hex(snap.cpu.pc))); - - // GPIO: 3 ports (A/B/C), each ODR hex + 16 LED glyphs (pin0..pin15). - auto led = [](uint16_t odr) { - QString s; - for (int i = 0; i < 16; ++i) { - s += (odr >> i) & 1 ? QChar(0x25CF) : QChar(0x00B7); // ● or · - } - return s; - }; - QString g; - for (int i = 0; i < 3; ++i) { - const auto& port = snap.peripherals.gpio[i]; - g += QString("%1 0x%2 %3\n") - .arg(QChar(port.port)) - .arg(port.odr, 4, 16, QLatin1Char('0')) - .arg(led(port.odr)); - } - gpio_label_->setText(g); + regs_panel_->refresh(snap); + status_panel_->refresh(snap); + serial_panel_->refresh(snap); + fault_panel_->refresh(snap); + gpio_panel_->refresh(snap); + periph_panel_->refresh(snap); } } // namespace micro_forge::gui diff --git a/gui/main_window.hpp b/gui/main_window.hpp index 1508d82..f055061 100644 --- a/gui/main_window.hpp +++ b/gui/main_window.hpp @@ -1,24 +1,31 @@ -// micro-forge GUI main window (G5b). +// micro-forge GUI main window. // -// Owns the simulator (a Stm32f103Soc) and drives it from the Qt main thread: -// a QTimer fires onTick(), which runs a small chunk of steps and refreshes -// the CPU panel from introspection::read_introspection(). The sim never runs on a -// QThread — that would break deterministic replay (DIRECTIVES §E). +// QMainWindow shell: a toolbar (run/step/reset + state + speed) plus dockable +// panels. Serial output is central; registers dock left; status / fault / +// peripherals dock right; GPIO dock bottom. Users can drag/fold/float any +// dock. All panels refresh from the session's snapshot each tick — MainWindow +// owns no simulator state (that's in model::Session). #pragma once -#include "chips/stm32f1/soc/stm32f103_soc.hpp" +#include "gui/model/session.hpp" #include #include -#include -#include -#include +class QComboBox; class QLabel; class QPushButton; -class QTableWidget; class QTimer; +namespace micro_forge::gui::panels { +class RegistersPanel; +class GpioPanel; +class SerialPanel; +class StatusPanel; +class FaultPanel; +class PeripheralPanel; +} // namespace micro_forge::gui::panels + namespace micro_forge::gui { class MainWindow : public QMainWindow { @@ -35,20 +42,22 @@ class MainWindow : public QMainWindow { void onResetClicked(); private: - void rebuildSoc(); + void rebuildSession(); void refreshFromSnapshot(); - std::unique_ptr soc_; - std::string usart_output_; - QString firmware_path_; - std::vector firmware_data_; + model::Session session_; bool running_ = false; QTimer* timer_ = nullptr; QLabel* state_label_ = nullptr; - QTableWidget* regs_table_ = nullptr; - QLabel* gpio_label_ = nullptr; QPushButton* run_btn_ = nullptr; + QComboBox* speed_combo_ = nullptr; + panels::RegistersPanel* regs_panel_ = nullptr; + panels::SerialPanel* serial_panel_ = nullptr; + panels::GpioPanel* gpio_panel_ = nullptr; + panels::StatusPanel* status_panel_ = nullptr; + panels::FaultPanel* fault_panel_ = nullptr; + panels::PeripheralPanel* periph_panel_ = nullptr; }; } // namespace micro_forge::gui diff --git a/gui/model/session.cpp b/gui/model/session.cpp new file mode 100644 index 0000000..0d4de4b --- /dev/null +++ b/gui/model/session.cpp @@ -0,0 +1,76 @@ +// GUI model layer — see session.hpp. +#include "gui/model/session.hpp" + +#include "hooks/events.hpp" // hooks::UartByte + +#include +#include +#include + +namespace micro_forge::gui::model { + +namespace { + +std::vector read_file(const std::string& path) { + std::ifstream f(path, std::ios::binary); + return {std::istreambuf_iterator(f), {}}; +} + +bool is_elf(const std::vector& d) { + return d.size() >= 4 && d[0] == 0x7f && d[1] == 'E' && d[2] == 'L' && + d[3] == 'F'; +} + +} // namespace + +Session::Session() = default; + +void Session::set_firmware(std::string path) { + firmware_path_ = std::move(path); +} + +std::expected Session::rebuild() { + usart_output_.clear(); + auto created = chips::stm32f1::Stm32f103Soc::create(); + if (!created) { + return std::unexpected(created.error()); + } + soc_ = std::move(*created); + soc_->parts().event_bus.uart.connect( + [this](const hooks::UartByte& e) { + usart_output_ += static_cast(e.byte); + }); + + if (!firmware_path_.empty()) { + firmware_data_ = read_file(firmware_path_); + if (firmware_data_.empty()) { + return std::unexpected("cannot read: " + firmware_path_); + } + auto lr = is_elf(firmware_data_) + ? soc_->load_elf(firmware_data_) + : soc_->load_bin(0x08000000u, firmware_data_); + if (!lr) { + return std::unexpected("load failed: " + lr.error()); + } + } + return {}; +} + +void Session::run(std::size_t steps) { + if (soc_) { + soc_->run(steps); + } +} + +void Session::step() { + run(1); +} + +introspection::IntrospectionSnapshot Session::snapshot() const { + if (!soc_) { + return {}; + } + return introspection::read_introspection(*soc_, usart_output_); +} + +} // namespace micro_forge::gui::model diff --git a/gui/model/session.hpp b/gui/model/session.hpp new file mode 100644 index 0000000..02f0b99 --- /dev/null +++ b/gui/model/session.hpp @@ -0,0 +1,56 @@ +// GUI model layer — Qt-free façade over the simulated STM32F103 SoC. +// +// Owns the SoC, advances steps, accumulates USART output, and produces the +// structured IntrospectionSnapshot the view renders. Kept Qt-free so it can +// be unit-tested without a QApplication. Per DIRECTIVES §E the sim stays +// single-threaded; the view's QTimer drives run()/step() from the Qt main +// thread. +#pragma once + +#include "chips/stm32f1/soc/stm32f103_soc.hpp" +#include "introspection/introspection.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace micro_forge::gui::model { + +class Session { + public: + Session(); + + // Firmware path to load on the next rebuild(); empty = no firmware. + void set_firmware(std::string path); + + // Create the SoC, subscribe its UART event source, and (re)load firmware. + // Returns an error string on create / read / load failure; the session + // stays invalid (snapshot() yields a default-constructed snapshot). + std::expected rebuild(); + + // Advance the simulator. No-op when invalid. + void run(std::size_t steps); + void step(); // = run(1) + + // True when a valid SoC is loaded. + bool valid() const noexcept { return soc_ != nullptr; } + + // Structured snapshot of CPU + fault + peripherals (default-constructed, + // i.e. state=Halted / all-zero, when invalid). + introspection::IntrospectionSnapshot snapshot() const; + + // USART bytes accumulated since rebuild(); the view drains this per tick. + std::string_view usart_output() const noexcept { return usart_output_; } + + private: + std::unique_ptr soc_; + std::string usart_output_; + std::string firmware_path_; + std::vector firmware_data_; +}; + +} // namespace micro_forge::gui::model diff --git a/gui/view/panels/fault_panel.cpp b/gui/view/panels/fault_panel.cpp new file mode 100644 index 0000000..6f9b5ab --- /dev/null +++ b/gui/view/panels/fault_panel.cpp @@ -0,0 +1,93 @@ +// Fault detail panel — see fault_panel.hpp. +#include "gui/view/panels/fault_panel.hpp" + +#include "cpu/cpu.hpp" + +#include +#include +#include +#include + +#include + +namespace micro_forge::gui::panels { + +namespace { + +const char* fault_kind_name(cpu::CPU::CPUError k) { + switch (k) { + case cpu::CPU::CPUError::IllegalInstruction: return "IllegalInstruction"; + case cpu::CPU::CPUError::DataAccessFault: return "DataAccessFault"; + case cpu::CPU::CPUError::InstructionFetchFault: return "InstructionFetchFault"; + case cpu::CPU::CPUError::InvalidPc: return "InvalidPc"; + case cpu::CPU::CPUError::ExceptionEntryFault: return "ExceptionEntryFault"; + case cpu::CPU::CPUError::ExceptionReturnFault: return "ExceptionReturnFault"; + case cpu::CPU::CPUError::NotRunning: return "NotRunning"; + case cpu::CPU::CPUError::RegisterIndexOverflow: return "RegisterIndexOverflow"; + case cpu::CPU::CPUError::FailedPollIntr: return "FailedPollIntr"; + } + return "Unknown"; +} + +// bus_error_raw is the BusError enum (core/types.hpp) stored as a raw int so +// this panel need not depend on memory/bus.hpp. Order: Unmapped=0, Unaligned, +// ReadOnly, InvalidDevice, RegionOverlap, OutOfRange, PeripheralFault=6. +const char* bus_error_name_raw(std::uint32_t raw) { + switch (raw) { + case 0: return "Unmapped"; + case 1: return "Unaligned"; + case 2: return "ReadOnly"; + case 3: return "InvalidDevice"; + case 4: return "RegionOverlap"; + case 5: return "OutOfRange"; + case 6: return "PeripheralFault"; + default: return "Unknown"; + } +} + +} // namespace + +FaultPanel::FaultPanel(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + + label_ = new QLabel("— none —"); + label_->setStyleSheet("font-family: monospace;"); + lay->addWidget(label_); +} + +void FaultPanel::refresh(const introspection::IntrospectionSnapshot& snap) { + if (!snap.fault.present) { + label_->setText("— none —"); + return; + } + auto hex = [](std::uint32_t v) { + return QString("0x%1").arg(v, 8, 16, QLatin1Char('0')); + }; + auto hex16 = [](std::uint16_t v) { + return QString("0x%1").arg(v, 4, 16, QLatin1Char('0')); + }; + const auto& f = snap.fault; + QString s; + s += QString("kind: %1\n") + .arg(QString::fromLatin1(fault_kind_name(f.kind))); + s += QString("pc: %1\n").arg(hex(f.pc)); + s += QString("lr: %1\n").arg(hex(f.lr)); + s += QString("sp: %1\n").arg(hex(f.sp)); + s += QString("xpsr: %1\n").arg(hex(f.xpsr)); + if (f.is_32bit) { + s += QString("insn: %1 %2 (32-bit)\n") + .arg(hex16(f.opcode16), hex16(f.opcode16_2)); + } else { + s += QString("insn: %1 (16-bit)\n").arg(hex16(f.opcode16)); + } + if (f.has_access_addr) { + s += QString("access: %1\n").arg(hex(f.access_addr)); + } + if (f.has_bus_error) { + s += QString("bus: %1\n") + .arg(QString::fromLatin1(bus_error_name_raw(f.bus_error_raw))); + } + label_->setText(s); +} + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/fault_panel.hpp b/gui/view/panels/fault_panel.hpp new file mode 100644 index 0000000..4848979 --- /dev/null +++ b/gui/view/panels/fault_panel.hpp @@ -0,0 +1,26 @@ +// Fault detail panel — when a fault fires, show its kind, the faulting +// instruction, and (if it was a bus access) the address + BusError. Shows +// "— none —" while no fault is present. +#pragma once + +#include "introspection/introspection.hpp" + +#include + +class QLabel; + +namespace micro_forge::gui::panels { + +class FaultPanel : public QWidget { + Q_OBJECT + + public: + explicit FaultPanel(QWidget* parent = nullptr); + + void refresh(const introspection::IntrospectionSnapshot& snap); + + private: + QLabel* label_; +}; + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/gpio_panel.cpp b/gui/view/panels/gpio_panel.cpp new file mode 100644 index 0000000..ee0aa43 --- /dev/null +++ b/gui/view/panels/gpio_panel.cpp @@ -0,0 +1,41 @@ +// GPIO panel — see gpio_panel.hpp. +#include "gui/view/panels/gpio_panel.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace micro_forge::gui::panels { + +GpioPanel::GpioPanel(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + + label_ = new QLabel; + label_->setStyleSheet("font-family: monospace;"); + lay->addWidget(label_); +} + +void GpioPanel::refresh(const introspection::IntrospectionSnapshot& snap) { + auto led = [](std::uint16_t odr) { + QString s; + for (int i = 0; i < 16; ++i) { + s += (odr >> i) & 1 ? QChar(0x25CF) : QChar(0x00B7); // ● or · + } + return s; + }; + QString g; + for (int i = 0; i < 3; ++i) { + const auto& port = snap.peripherals.gpio[i]; + g += QString("%1 0x%2 %3\n") + .arg(QChar(port.port)) + .arg(port.odr, 4, 16, QLatin1Char('0')) + .arg(led(port.odr)); + } + label_->setText(g); +} + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/gpio_panel.hpp b/gui/view/panels/gpio_panel.hpp new file mode 100644 index 0000000..c2dded5 --- /dev/null +++ b/gui/view/panels/gpio_panel.hpp @@ -0,0 +1,24 @@ +// GPIO panel — A/B/C port ODR (hex) + 16 per-pin LED glyphs (pin0..pin15). +#pragma once + +#include "introspection/introspection.hpp" + +#include + +class QLabel; + +namespace micro_forge::gui::panels { + +class GpioPanel : public QWidget { + Q_OBJECT + + public: + explicit GpioPanel(QWidget* parent = nullptr); + + void refresh(const introspection::IntrospectionSnapshot& snap); + + private: + QLabel* label_; +}; + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/peripheral_panel.cpp b/gui/view/panels/peripheral_panel.cpp new file mode 100644 index 0000000..32a2ddb --- /dev/null +++ b/gui/view/panels/peripheral_panel.cpp @@ -0,0 +1,39 @@ +// Peripheral status panel — see peripheral_panel.hpp. +#include "gui/view/panels/peripheral_panel.hpp" + +#include +#include +#include +#include + +#include + +namespace micro_forge::gui::panels { + +PeripheralPanel::PeripheralPanel(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + + label_ = new QLabel; + label_->setStyleSheet("font-family: monospace;"); + lay->addWidget(label_); +} + +void PeripheralPanel::refresh(const introspection::IntrospectionSnapshot& snap) { + auto hex = [](std::uint32_t v) { + return QString("0x%1").arg(v, 8, 16, QLatin1Char('0')); + }; + const auto& p = snap.peripherals; + QString s; + s += QString("SysTick ctrl=%1 load=%2 val=%3\n") + .arg(hex(p.systick.ctrl), hex(p.systick.load), hex(p.systick.val)); + s += QString("NVIC pending=%1 irq=%2 enabled=%3\n") + .arg(p.nvic.has_pending ? QStringLiteral("yes") : QStringLiteral("no")) + .arg(static_cast(p.nvic.highest_pending_irq)) + .arg(static_cast(p.nvic.enabled_count)); + s += QString("SCB icsr=%1 vtor=%2 aircr=%3 prigroup=%4\n") + .arg(hex(p.scb.icsr), hex(p.scb.vtor), hex(p.scb.aircr)) + .arg(static_cast(p.scb.prigroup)); + label_->setText(s); +} + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/peripheral_panel.hpp b/gui/view/panels/peripheral_panel.hpp new file mode 100644 index 0000000..23effdd --- /dev/null +++ b/gui/view/panels/peripheral_panel.hpp @@ -0,0 +1,25 @@ +// Peripheral status panel — SysTick / NVIC / SCB summary, the interrupt-system +// "instrument cluster": tick timer state, pending IRQ, priority grouping. +#pragma once + +#include "introspection/introspection.hpp" + +#include + +class QLabel; + +namespace micro_forge::gui::panels { + +class PeripheralPanel : public QWidget { + Q_OBJECT + + public: + explicit PeripheralPanel(QWidget* parent = nullptr); + + void refresh(const introspection::IntrospectionSnapshot& snap); + + private: + QLabel* label_; +}; + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/registers_panel.cpp b/gui/view/panels/registers_panel.cpp new file mode 100644 index 0000000..b76d7af --- /dev/null +++ b/gui/view/panels/registers_panel.cpp @@ -0,0 +1,46 @@ +// CPU register panel — see registers_panel.hpp. +#include "gui/view/panels/registers_panel.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace micro_forge::gui::panels { + +RegistersPanel::RegistersPanel(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + + table_ = new QTableWidget(16, 2); + table_->setHorizontalHeaderLabels({"reg", "value"}); + table_->verticalHeader()->setVisible(false); + table_->horizontalHeader()->setStretchLastSection(true); + table_->setEditTriggers(QAbstractItemView::NoEditTriggers); + lay->addWidget(table_); +} + +void RegistersPanel::refresh(const introspection::IntrospectionSnapshot& snap) { + static constexpr const char* kNames[16] = { + "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", + "r8", "r9", "r10", "r11", "r12", "sp", "lr", "pc"}; + auto hex = [](std::uint32_t v) { + return QString("0x%1").arg(v, 8, 16, QLatin1Char('0')); + }; + for (int i = 0; i < 13; ++i) { + table_->setItem(i, 0, new QTableWidgetItem(kNames[i])); + table_->setItem(i, 1, new QTableWidgetItem(hex(snap.cpu.regs[i]))); + } + table_->setItem(13, 0, new QTableWidgetItem(kNames[13])); + table_->setItem(13, 1, new QTableWidgetItem(hex(snap.cpu.sp))); + table_->setItem(14, 0, new QTableWidgetItem(kNames[14])); + table_->setItem(14, 1, new QTableWidgetItem(hex(snap.cpu.lr))); + table_->setItem(15, 0, new QTableWidgetItem(kNames[15])); + table_->setItem(15, 1, new QTableWidgetItem(hex(snap.cpu.pc))); +} + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/registers_panel.hpp b/gui/view/panels/registers_panel.hpp new file mode 100644 index 0000000..b959987 --- /dev/null +++ b/gui/view/panels/registers_panel.hpp @@ -0,0 +1,24 @@ +// CPU register panel — r0..r12 / sp / lr / pc, refreshed from the snapshot. +#pragma once + +#include "introspection/introspection.hpp" + +#include + +class QTableWidget; + +namespace micro_forge::gui::panels { + +class RegistersPanel : public QWidget { + Q_OBJECT + + public: + explicit RegistersPanel(QWidget* parent = nullptr); + + void refresh(const introspection::IntrospectionSnapshot& snap); + + private: + QTableWidget* table_; +}; + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/serial_panel.cpp b/gui/view/panels/serial_panel.cpp new file mode 100644 index 0000000..96e1f4d --- /dev/null +++ b/gui/view/panels/serial_panel.cpp @@ -0,0 +1,26 @@ +// Serial output panel — see serial_panel.hpp. +#include "gui/view/panels/serial_panel.hpp" + +#include +#include +#include +#include + +namespace micro_forge::gui::panels { + +SerialPanel::SerialPanel(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + + view_ = new QTextEdit; + view_->setReadOnly(true); + view_->setStyleSheet("font-family: monospace;"); + lay->addWidget(view_); +} + +void SerialPanel::refresh(const introspection::IntrospectionSnapshot& snap) { + const auto sv = snap.peripherals.usart_output; + view_->setPlainText( + QString::fromUtf8(sv.data(), static_cast(sv.size()))); +} + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/serial_panel.hpp b/gui/view/panels/serial_panel.hpp new file mode 100644 index 0000000..0f3e4d8 --- /dev/null +++ b/gui/view/panels/serial_panel.hpp @@ -0,0 +1,25 @@ +// Serial output panel — read-only view of the USART bytes accumulated by the +// session. This is the "wow" surface: firmware output appears here as it runs. +#pragma once + +#include "introspection/introspection.hpp" + +#include + +class QTextEdit; + +namespace micro_forge::gui::panels { + +class SerialPanel : public QWidget { + Q_OBJECT + + public: + explicit SerialPanel(QWidget* parent = nullptr); + + void refresh(const introspection::IntrospectionSnapshot& snap); + + private: + QTextEdit* view_; +}; + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/status_panel.cpp b/gui/view/panels/status_panel.cpp new file mode 100644 index 0000000..5859ebc --- /dev/null +++ b/gui/view/panels/status_panel.cpp @@ -0,0 +1,49 @@ +// Status / masks panel — see status_panel.hpp. +#include "gui/view/panels/status_panel.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace micro_forge::gui::panels { + +StatusPanel::StatusPanel(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + + table_ = new QTableWidget(7, 2); + table_->setHorizontalHeaderLabels({"field", "value"}); + table_->verticalHeader()->setVisible(false); + table_->horizontalHeader()->setStretchLastSection(true); + table_->setEditTriggers(QAbstractItemView::NoEditTriggers); + lay->addWidget(table_); +} + +void StatusPanel::refresh(const introspection::IntrospectionSnapshot& snap) { + auto hex = [](std::uint32_t v) { + return QString("0x%1").arg(v, 8, 16, QLatin1Char('0')); + }; + const auto& c = snap.cpu; + struct Row { + const char* name; + std::uint32_t v; + }; + const Row rows[7] = { + {"xpsr", c.xpsr}, {"PRIMASK", c.primask}, + {"BASEPRI", c.basepri}, {"FAULTMASK", c.faultmask}, + {"CONTROL", c.control}, {"MSP", c.msp}, + {"PSP", c.psp}, + }; + for (int i = 0; i < 7; ++i) { + table_->setItem(i, 0, + new QTableWidgetItem(QString::fromLatin1(rows[i].name))); + table_->setItem(i, 1, new QTableWidgetItem(hex(rows[i].v))); + } +} + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/status_panel.hpp b/gui/view/panels/status_panel.hpp new file mode 100644 index 0000000..f886cae --- /dev/null +++ b/gui/view/panels/status_panel.hpp @@ -0,0 +1,27 @@ +// Status / masks panel — xPSR, PRIMASK, BASEPRI, FAULTMASK, CONTROL, MSP, PSP. +// These are the "why did the CPU do that" registers: interrupt masking, +// stack selection, condition flags. Watching them change while stepping is +// the teaching moment for ARMv7-M exception semantics. +#pragma once + +#include "introspection/introspection.hpp" + +#include + +class QTableWidget; + +namespace micro_forge::gui::panels { + +class StatusPanel : public QWidget { + Q_OBJECT + + public: + explicit StatusPanel(QWidget* parent = nullptr); + + void refresh(const introspection::IntrospectionSnapshot& snap); + + private: + QTableWidget* table_; +}; + +} // namespace micro_forge::gui::panels diff --git a/scripts/check_test_count.sh b/scripts/check_test_count.sh index a7e4315..c0e5a63 100755 --- a/scripts/check_test_count.sh +++ b/scripts/check_test_count.sh @@ -17,7 +17,7 @@ set -euo pipefail BUILD="${1:-build}" -BASELINE=353 # floor = PRODUCT test count — only ever raised, never lowered (2026-07-07: 321→353, G1-G6) +BASELINE=357 # floor = PRODUCT test count — only ever raised, never lowered (2026-07-08: 353→357, GUI Session tests) if [[ ! -d "$BUILD" ]]; then echo "skip: build dir '$BUILD' not found (run cmake configure first)" diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9306582..cd5f5f7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -239,6 +239,28 @@ if(TARGET hello_firmware) ) endif() +# ── GUI model (Session) unit tests — Qt-free, plain gtest ── +# Compiles gui/model/session.cpp directly so the harness needs no Qt. The +# SESSION_HELLO_ELF cases load real firmware and check run/cycles/USART. +add_executable(test_gui_session + test_gui_session.cpp + ${CMAKE_SOURCE_DIR}/gui/model/session.cpp +) +target_include_directories(test_gui_session PRIVATE ${CMAKE_SOURCE_DIR}) +target_link_libraries(test_gui_session PRIVATE micro_forge GTest::gtest_main) +gtest_discover_tests(test_gui_session) +if(TARGET hello_firmware) + target_compile_definitions(test_gui_session PRIVATE + SESSION_HELLO_ELF="${CMAKE_CURRENT_BINARY_DIR}/hello.elf" + ) + add_dependencies(test_gui_session hello_firmware) + add_custom_command(TARGET test_gui_session POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_BINARY_DIR}/../examples/hello_world/hello.elf + ${CMAKE_CURRENT_BINARY_DIR}/hello.elf + ) +endif() + # ── Meta-guard: test-discovery regression floor (COVERAGE-METHODOLOGY §3) ── # Cheap `ctest -N` count check — fails if a refactor silently drops test # discovery (build stays green, coverage evaporates). Bash on the Linux test diff --git a/test/test_gui_session.cpp b/test/test_gui_session.cpp new file mode 100644 index 0000000..ec0ac4f --- /dev/null +++ b/test/test_gui_session.cpp @@ -0,0 +1,68 @@ +// Unit tests for the GUI model layer (micro_forge::gui::model::Session). +// +// Session is deliberately Qt-free, so these run under plain gtest with no +// QApplication — they pin: (1) rebuild yields a valid Halted session with no +// firmware; (2) loading + running real firmware advances cycles, keeps PC in +// flash, and drains USART output into the buffer; (3) rebuild clears the +// USART buffer so a reset doesn't show stale output. +#include "gui/model/session.hpp" + +#include "cpu/cpu.hpp" + +#include + +#include +#include + +using micro_forge::cpu::CPU; +using micro_forge::gui::model::Session; + +TEST(GuiSession, RebuildWithoutFirmwareIsValidAndHalted) { + Session s; + ASSERT_TRUE(s.rebuild().has_value()); + EXPECT_TRUE(s.valid()); + const auto snap = s.snapshot(); + EXPECT_EQ(snap.cpu.state, CPU::State::Halted); + EXPECT_EQ(snap.cycles, 0u); + EXPECT_TRUE(s.usart_output().empty()); +} + +TEST(GuiSession, StepOnHaltedSessionStaysValid) { + Session s; + ASSERT_TRUE(s.rebuild().has_value()); + s.step(); // no firmware → Halted; step is a no-op but must not corrupt state + EXPECT_TRUE(s.valid()); + EXPECT_EQ(s.snapshot().cpu.state, CPU::State::Halted); +} + +#ifdef SESSION_HELLO_ELF +TEST(GuiSession, LoadsFirmwareAndRunsAndEmitsUsart) { + Session s; + s.set_firmware(SESSION_HELLO_ELF); + ASSERT_TRUE(s.rebuild().has_value()) + << "rebuild failed for " << SESSION_HELLO_ELF; + s.run(50000); + + const auto snap = s.snapshot(); + EXPECT_EQ(snap.cpu.state, CPU::State::Running); + EXPECT_GT(snap.cycles, 0u); + // STM32F103 flash window. + EXPECT_GE(snap.cpu.pc, 0x08000000u); + EXPECT_LT(snap.cpu.pc, 0x08080000u); + EXPECT_FALSE(s.usart_output().empty()) + << "hello firmware produced no USART output"; +} + +TEST(GuiSession, RebuildClearsUsartBuffer) { + Session s; + s.set_firmware(SESSION_HELLO_ELF); + ASSERT_TRUE(s.rebuild().has_value()); + s.run(50000); + ASSERT_FALSE(s.usart_output().empty()); + + // A second rebuild (e.g. Reset button) must drop the accumulated output. + ASSERT_TRUE(s.rebuild().has_value()); + EXPECT_TRUE(s.usart_output().empty()) + << "rebuild should clear the USART buffer"; +} +#endif