From f81ab7d970e3691d5f1999b5e70343da6cec1695 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Wed, 8 Jul 2026 07:58:32 +0800 Subject: [PATCH 1/9] refactor(gui): extract Qt-free model::Session from MainWindow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MainWindow mixed five concerns: SoC ownership, run/step control, UI construction, snapshot refresh, and file I/O. Pulled the simulator concerns into gui/model/session.{hpp,cpp} — a Qt-free façade (owns SoC, loads firmware, subscribes UART, produces IntrospectionSnapshot) that can be unit-tested without QApplication. MainWindow now holds a Session and only does UI + QTimer driving. Pure refactor, no behavior change: GUI builds, offscreen AUTORUN loop runs clean, core 355 ctests still green. Sets up the model/view split the panel and dock work needs. --- gui/CMakeLists.txt | 1 + gui/main_window.cpp | 81 ++++++++++++------------------------------- gui/main_window.hpp | 21 +++++------ gui/model/session.cpp | 76 ++++++++++++++++++++++++++++++++++++++++ gui/model/session.hpp | 56 ++++++++++++++++++++++++++++++ 5 files changed, 164 insertions(+), 71 deletions(-) create mode 100644 gui/model/session.cpp create mode 100644 gui/model/session.hpp diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index a9ba0a3..efd9f59 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -10,6 +10,7 @@ set(CMAKE_AUTOMOC ON) add_executable(micro-forge-gui main.cpp main_window.cpp + model/session.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..e27f1d3 100644 --- a/gui/main_window.cpp +++ b/gui/main_window.cpp @@ -1,7 +1,10 @@ -// micro-forge GUI main window implementation (G5b/G5c). +// micro-forge GUI main window implementation. +// +// Thin Qt view over a model::Session. Owns only UI widgets; simulator state +// (SoC, firmware, USART buffer) lives in session_. A QTimer drives run() + +// refreshFromSnapshot() each tick — single-threaded on the Qt main thread. #include "gui/main_window.hpp" -#include "introspection/introspection.hpp" #include "cpu/cpu.hpp" #include @@ -17,29 +20,16 @@ #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"); + session_.set_firmware(firmware_path.toStdString()); + auto* central = new QWidget; auto* root = new QVBoxLayout(central); @@ -85,7 +75,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 @@ -98,33 +88,10 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) } } -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 +108,30 @@ 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()) { + session_.run(20000); 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) { @@ -192,7 +157,7 @@ void MainWindow::refreshFromSnapshot() { 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) { + auto hex = [](std::uint32_t v) { return QString("0x%1").arg(v, 8, 16, QLatin1Char('0')); }; for (int i = 0; i < 13; ++i) { @@ -207,7 +172,7 @@ void MainWindow::refreshFromSnapshot() { 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) { + auto led = [](std::uint16_t odr) { QString s; for (int i = 0; i < 16; ++i) { s += (odr >> i) & 1 ? QChar(0x25CF) : QChar(0x00B7); // ● or · diff --git a/gui/main_window.hpp b/gui/main_window.hpp index 1508d82..d9d0dc7 100644 --- a/gui/main_window.hpp +++ b/gui/main_window.hpp @@ -1,18 +1,16 @@ -// 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 +// Thin Qt view over a model::Session: a QTimer fires onTick(), which asks the +// session to run a small chunk of steps and then refreshes the panels from the +// session's IntrospectionSnapshot. Owns no simulator state directly — that +// lives in model::Session (Qt-free, unit-testable). The sim never runs on a // QThread — that would break deterministic replay (DIRECTIVES §E). #pragma once -#include "chips/stm32f1/soc/stm32f103_soc.hpp" +#include "gui/model/session.hpp" #include #include -#include -#include -#include class QLabel; class QPushButton; @@ -35,13 +33,10 @@ 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; 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 From efa74716fd5da738a25d62325c6c1599ce9434fa Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Wed, 8 Jul 2026 08:05:32 +0800 Subject: [PATCH 2/9] refactor(gui): split CPU/GPIO into view/panels/ + add SerialPanel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each panel is now an independent QWidget under gui/view/panels/ exposing refresh(IntrospectionSnapshot). MainWindow sheds the inline table/label widgets and the register/GPIO fill logic — refreshFromSnapshot now just calls regs/serial/gpio->refresh(snap), so adding panels no longer bloats it. SerialPanel (read-only QTextEdit bound to USART output) is the first new panel: firmware output appears live as it runs (hello.elf demo). Layout is still fixed vertical; dock organization is the next commit. GUI builds, offscreen AUTORUN runs hello.elf clean, core 355 ctests green. --- gui/CMakeLists.txt | 3 ++ gui/main_window.cpp | 76 +++++++---------------------- gui/main_window.hpp | 21 +++++--- gui/view/panels/gpio_panel.cpp | 44 +++++++++++++++++ gui/view/panels/gpio_panel.hpp | 24 +++++++++ gui/view/panels/registers_panel.cpp | 49 +++++++++++++++++++ gui/view/panels/registers_panel.hpp | 24 +++++++++ gui/view/panels/serial_panel.cpp | 29 +++++++++++ gui/view/panels/serial_panel.hpp | 25 ++++++++++ 9 files changed, 229 insertions(+), 66 deletions(-) create mode 100644 gui/view/panels/gpio_panel.cpp create mode 100644 gui/view/panels/gpio_panel.hpp create mode 100644 gui/view/panels/registers_panel.cpp create mode 100644 gui/view/panels/registers_panel.hpp create mode 100644 gui/view/panels/serial_panel.cpp create mode 100644 gui/view/panels/serial_panel.hpp diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index efd9f59..faa6c36 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -11,6 +11,9 @@ 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 ) # 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 e27f1d3..e1870fa 100644 --- a/gui/main_window.cpp +++ b/gui/main_window.cpp @@ -1,21 +1,20 @@ // micro-forge GUI main window implementation. // -// Thin Qt view over a model::Session. Owns only UI widgets; simulator state -// (SoC, firmware, USART buffer) lives in session_. A QTimer drives run() + -// refreshFromSnapshot() each tick — single-threaded on the Qt main thread. +// Thin Qt view: assembles the control bar + panels, drives the session via +// QTimer, and refreshes every panel from the snapshot each tick. Owns no +// simulator state — that's in model::Session. #include "gui/main_window.hpp" +#include "gui/view/panels/gpio_panel.hpp" +#include "gui/view/panels/registers_panel.hpp" +#include "gui/view/panels/serial_panel.hpp" + #include "cpu/cpu.hpp" -#include #include -#include #include -#include #include #include -#include -#include #include #include #include @@ -47,24 +46,16 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) 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_); + // ── panels (fixed vertical layout for now; dock organization comes next) ── + regs_panel_ = new panels::RegistersPanel; + root->addWidget(regs_panel_); + serial_panel_ = new panels::SerialPanel; + root->addWidget(serial_panel_); + gpio_panel_ = new panels::GpioPanel; + root->addWidget(gpio_panel_); setCentralWidget(central); - resize(480, 720); + resize(480, 780); connect(run_btn_, &QPushButton::clicked, this, &MainWindow::onRunClicked); connect(step_btn, &QPushButton::clicked, this, &MainWindow::onStepClicked); @@ -154,40 +145,9 @@ 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 = [](std::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 = [](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)); - } - gpio_label_->setText(g); + regs_panel_->refresh(snap); + serial_panel_->refresh(snap); + gpio_panel_->refresh(snap); } } // namespace micro_forge::gui diff --git a/gui/main_window.hpp b/gui/main_window.hpp index d9d0dc7..d93797d 100644 --- a/gui/main_window.hpp +++ b/gui/main_window.hpp @@ -1,10 +1,9 @@ // micro-forge GUI main window. // -// Thin Qt view over a model::Session: a QTimer fires onTick(), which asks the -// session to run a small chunk of steps and then refreshes the panels from the -// session's IntrospectionSnapshot. Owns no simulator state directly — that -// lives in model::Session (Qt-free, unit-testable). The sim never runs on a -// QThread — that would break deterministic replay (DIRECTIVES §E). +// Thin Qt view over a model::Session. Owns only UI: a control bar plus a set +// of panels (registers / serial / gpio / ...) refreshed from the session's +// IntrospectionSnapshot each tick. Each panel is an independent widget under +// view/panels/. The sim never runs on a QThread (DIRECTIVES §E). #pragma once #include "gui/model/session.hpp" @@ -14,9 +13,14 @@ class QLabel; class QPushButton; -class QTableWidget; class QTimer; +namespace micro_forge::gui::panels { +class RegistersPanel; +class GpioPanel; +class SerialPanel; +} // namespace micro_forge::gui::panels + namespace micro_forge::gui { class MainWindow : public QMainWindow { @@ -41,9 +45,10 @@ class MainWindow : public QMainWindow { QTimer* timer_ = nullptr; QLabel* state_label_ = nullptr; - QTableWidget* regs_table_ = nullptr; - QLabel* gpio_label_ = nullptr; QPushButton* run_btn_ = nullptr; + panels::RegistersPanel* regs_panel_ = nullptr; + panels::SerialPanel* serial_panel_ = nullptr; + panels::GpioPanel* gpio_panel_ = nullptr; }; } // namespace micro_forge::gui diff --git a/gui/view/panels/gpio_panel.cpp b/gui/view/panels/gpio_panel.cpp new file mode 100644 index 0000000..595ab15 --- /dev/null +++ b/gui/view/panels/gpio_panel.cpp @@ -0,0 +1,44 @@ +// 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); + auto* title = new QLabel("GPIO output (A/B/C, pin0..pin15)"); + title->setStyleSheet("font-weight: bold;"); + lay->addWidget(title); + + 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/registers_panel.cpp b/gui/view/panels/registers_panel.cpp new file mode 100644 index 0000000..b8aac0c --- /dev/null +++ b/gui/view/panels/registers_panel.cpp @@ -0,0 +1,49 @@ +// 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); + auto* title = new QLabel("CPU registers (r0-r12, sp, lr, pc)"); + title->setStyleSheet("font-weight: bold;"); + lay->addWidget(title); + + 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..53db4d8 --- /dev/null +++ b/gui/view/panels/serial_panel.cpp @@ -0,0 +1,29 @@ +// 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); + auto* title = new QLabel("Serial output"); + title->setStyleSheet("font-weight: bold;"); + lay->addWidget(title); + + 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 From d61a808504e6de4458807eb1fbfaa0b472b1dccc Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Wed, 8 Jul 2026 08:07:18 +0800 Subject: [PATCH 3/9] =?UTF-8?q?feat(gui):=20dock=20framework=20=E2=80=94?= =?UTF-8?q?=20toolbar=20+=20central=20serial=20+=20dockable=20panels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced the fixed QVBoxLayout with a QMainWindow shell: a QToolBar for run/step/reset + state, the SerialPanel as the central widget (the surface the user watches), RegistersPanel docked left, GpioPanel docked bottom. Each dock is draggable / foldable / floatable — the debugger feel DIRECTIVES §E pointed at. Dock object names set for future saveState/restoreState. Layout still single-column-ish visually until more panels land on the right side; the framework is what matters here. GUI builds, offscreen AUTORUN runs hello.elf clean, core 355 ctests green. --- gui/main_window.cpp | 54 ++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/gui/main_window.cpp b/gui/main_window.cpp index e1870fa..3e5d729 100644 --- a/gui/main_window.cpp +++ b/gui/main_window.cpp @@ -1,8 +1,10 @@ // micro-forge GUI main window implementation. // -// Thin Qt view: assembles the control bar + panels, drives the session via -// QTimer, and refreshes every panel from the snapshot each tick. Owns no -// simulator state — that's in model::Session. +// QMainWindow shell: a toolbar (run/step/reset + state) plus dockable panels. +// The serial output panel is the central widget (main interaction surface); +// registers dock left, 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). #include "gui/main_window.hpp" #include "gui/view/panels/gpio_panel.hpp" @@ -11,13 +13,12 @@ #include "cpu/cpu.hpp" -#include +#include #include #include #include +#include #include -#include -#include #include @@ -29,33 +30,38 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) session_.set_firmware(firmware_path.toStdString()); - auto* central = new QWidget; - auto* root = new QVBoxLayout(central); - - // ── control bar ── - auto* bar = new QHBoxLayout; + // ── toolbar: run / step / reset + state label ── + 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); + toolbar->addWidget(run_btn_); + toolbar->addWidget(step_btn); + toolbar->addWidget(reset_btn); + toolbar->addSeparator(); + toolbar->addWidget(state_label_); - // ── panels (fixed vertical layout for now; dock organization comes next) ── + // ── panels ── regs_panel_ = new panels::RegistersPanel; - root->addWidget(regs_panel_); serial_panel_ = new panels::SerialPanel; - root->addWidget(serial_panel_); gpio_panel_ = new panels::GpioPanel; - root->addWidget(gpio_panel_); - setCentralWidget(central); - resize(480, 780); + // Central: serial output (the surface the user watches while firmware runs). + setCentralWidget(serial_panel_); + + // Left dock: CPU registers. + auto* regs_dock = new QDockWidget("CPU registers", this); + regs_dock->setObjectName("regs_dock"); // for saveState/restoreState later + regs_dock->setWidget(regs_panel_); + addDockWidget(Qt::LeftDockWidgetArea, regs_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); @@ -77,6 +83,8 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) run_btn_->setText("Pause"); timer_->start(); } + + resize(960, 720); } void MainWindow::rebuildSession() { From fc23f6ced8aab0587af83bccc664047ea171e605 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Wed, 8 Jul 2026 08:11:42 +0800 Subject: [PATCH 4/9] feat(gui): status/fault/peripherals panels docked on the right MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the three remaining numeric panels, all refreshed from the snapshot: * StatusPanel — xPSR / PRIMASK / BASEPRI / FAULTMASK / CONTROL / MSP / PSP, the interrupt-mask + stack-select registers (right dock). * FaultPanel — kind / pc / lr / sp / xpsr / instruction (16/32-bit) and, when present, access_addr + bus_error; shows '— none —' when clean. * PeripheralPanel — SysTick ctrl/load/val + NVIC pending/irq/enabled + SCB icsr/vtor/aircr/prigroup. MainWindow wires them as stacked right-area docks. Every field the introspection snapshot produces is now rendered. GUI builds, offscreen AUTORUN runs hello.elf clean, core 355 ctests green. --- gui/CMakeLists.txt | 3 + gui/main_window.cpp | 39 ++++++++--- gui/main_window.hpp | 15 +++-- gui/view/panels/fault_panel.cpp | 96 ++++++++++++++++++++++++++++ gui/view/panels/fault_panel.hpp | 26 ++++++++ gui/view/panels/peripheral_panel.cpp | 42 ++++++++++++ gui/view/panels/peripheral_panel.hpp | 25 ++++++++ gui/view/panels/status_panel.cpp | 52 +++++++++++++++ gui/view/panels/status_panel.hpp | 27 ++++++++ 9 files changed, 313 insertions(+), 12 deletions(-) create mode 100644 gui/view/panels/fault_panel.cpp create mode 100644 gui/view/panels/fault_panel.hpp create mode 100644 gui/view/panels/peripheral_panel.cpp create mode 100644 gui/view/panels/peripheral_panel.hpp create mode 100644 gui/view/panels/status_panel.cpp create mode 100644 gui/view/panels/status_panel.hpp diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index faa6c36..fd1968c 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -14,6 +14,9 @@ add_executable(micro-forge-gui 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 3e5d729..7571e47 100644 --- a/gui/main_window.cpp +++ b/gui/main_window.cpp @@ -1,15 +1,16 @@ // micro-forge GUI main window implementation. // -// QMainWindow shell: a toolbar (run/step/reset + state) plus dockable panels. -// The serial output panel is the central widget (main interaction surface); -// registers dock left, 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). +// QMainWindow shell assembling dockable panels around a central serial output. +// Drives the session via QTimer and refreshes every panel from the snapshot +// each tick. Owns no simulator state — that's in model::Session. #include "gui/main_window.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" @@ -45,18 +46,37 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) // ── 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 the user watches while firmware runs). + // 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"); // for saveState/restoreState later + 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"); @@ -84,7 +104,7 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) timer_->start(); } - resize(960, 720); + resize(1100, 760); } void MainWindow::rebuildSession() { @@ -154,8 +174,11 @@ void MainWindow::refreshFromSnapshot() { state_label_->setText(label); 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 d93797d..aec6c02 100644 --- a/gui/main_window.hpp +++ b/gui/main_window.hpp @@ -1,9 +1,10 @@ // micro-forge GUI main window. // -// Thin Qt view over a model::Session. Owns only UI: a control bar plus a set -// of panels (registers / serial / gpio / ...) refreshed from the session's -// IntrospectionSnapshot each tick. Each panel is an independent widget under -// view/panels/. The sim never runs on a QThread (DIRECTIVES §E). +// QMainWindow shell: a toolbar (run/step/reset + state) 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 "gui/model/session.hpp" @@ -19,6 +20,9 @@ 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 { @@ -49,6 +53,9 @@ class MainWindow : public QMainWindow { 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/view/panels/fault_panel.cpp b/gui/view/panels/fault_panel.cpp new file mode 100644 index 0000000..42a863a --- /dev/null +++ b/gui/view/panels/fault_panel.cpp @@ -0,0 +1,96 @@ +// 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); + auto* title = new QLabel("Fault detail"); + title->setStyleSheet("font-weight: bold;"); + lay->addWidget(title); + + 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/peripheral_panel.cpp b/gui/view/panels/peripheral_panel.cpp new file mode 100644 index 0000000..67a0681 --- /dev/null +++ b/gui/view/panels/peripheral_panel.cpp @@ -0,0 +1,42 @@ +// 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); + auto* title = new QLabel("Peripherals (SysTick / NVIC / SCB)"); + title->setStyleSheet("font-weight: bold;"); + lay->addWidget(title); + + 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/status_panel.cpp b/gui/view/panels/status_panel.cpp new file mode 100644 index 0000000..b953c2c --- /dev/null +++ b/gui/view/panels/status_panel.cpp @@ -0,0 +1,52 @@ +// 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); + auto* title = new QLabel("Status / masks (xPSR, PRIMASK, MSP/PSP, ...)"); + title->setStyleSheet("font-weight: bold;"); + lay->addWidget(title); + + 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 From 7b3ba2fdfe3ea63300ea9757367218e1af5a55e3 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Wed, 8 Jul 2026 08:37:32 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat(gui):=20speed=20selector=20=E2=80=94?= =?UTF-8?q?=201=C3=97/5=C3=97/25=C3=97/100=C3=97=20steps=20per=20tick?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firmware that blinks via big software delay loops (e.g. examples/gpio_blink with delay(100000)) barely animates at the default 20k steps/tick (~400k cyc/s). A toolbar combo now picks steps-per-tick: 1×=20k (default, easy on the UI thread), 5×/25×/100× for watching delay-loop firmware at human speed. onTick reads the combo's currentData. GUI builds, offscreen AUTORUN runs hello.elf clean, core 355 ctests green. --- gui/main_window.cpp | 23 ++++++++++++++++++++--- gui/main_window.hpp | 12 +++++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/gui/main_window.cpp b/gui/main_window.cpp index 7571e47..4304261 100644 --- a/gui/main_window.cpp +++ b/gui/main_window.cpp @@ -2,7 +2,9 @@ // // QMainWindow shell assembling dockable panels around a central serial output. // Drives the session via QTimer and refreshes every panel from the snapshot -// each tick. Owns no simulator state — that's in model::Session. +// 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 "gui/view/panels/fault_panel.hpp" @@ -14,6 +16,7 @@ #include "cpu/cpu.hpp" +#include #include #include #include @@ -21,6 +24,7 @@ #include #include +#include #include namespace micro_forge::gui { @@ -31,7 +35,7 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) session_.set_firmware(firmware_path.toStdString()); - // ── toolbar: run / step / reset + state label ── + // ── toolbar: run / step / reset + state + speed ── auto* toolbar = addToolBar("main"); run_btn_ = new QPushButton("Run"); auto* step_btn = new QPushButton("Step"); @@ -43,6 +47,17 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) 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; @@ -141,7 +156,9 @@ void MainWindow::onResetClicked() { void MainWindow::onTick() { if (running_ && session_.valid()) { - session_.run(20000); + const auto steps = + static_cast(speed_combo_->currentData().toInt()); + session_.run(steps); refreshFromSnapshot(); } } diff --git a/gui/main_window.hpp b/gui/main_window.hpp index aec6c02..f055061 100644 --- a/gui/main_window.hpp +++ b/gui/main_window.hpp @@ -1,10 +1,10 @@ // micro-forge GUI main window. // -// QMainWindow shell: a toolbar (run/step/reset + state) 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). +// 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 "gui/model/session.hpp" @@ -12,6 +12,7 @@ #include #include +class QComboBox; class QLabel; class QPushButton; class QTimer; @@ -50,6 +51,7 @@ class MainWindow : public QMainWindow { QTimer* timer_ = nullptr; QLabel* state_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; From e0f0ef18a63e1413a17cc3d92b34c8a2fec6e4bc Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Wed, 8 Jul 2026 08:39:53 +0800 Subject: [PATCH 6/9] feat(gpio_blink): continuous loop + shorter delay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The blink demo looped only 3 times then halted (for i<3), a leftover from when it was a test fixture. In the GUI that made the LED 'stop updating' after ~6 seconds and looked broken. Now it blinks forever (while(1)) with delay(20000) — 5× faster — so PA5 toggles continuously and visibly. Combined with the GUI speed selector, the LED animates at a comfortable rate. E2E.GpioBlink still passes (>=6 toggles; it now sees dozens within 4M steps). --- examples/gpio_blink/firmware/main.c | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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); } } From 438df09e693828254c81bb74dd6b30303ac68d92 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Wed, 8 Jul 2026 08:50:12 +0800 Subject: [PATCH 7/9] refactor(gui): drop redundant in-panel titles (dock title bar suffices) Each panel had a bold QLabel title at the top that duplicated the QDockWidget title bar (e.g. 'CPU registers' appeared twice). Removed the in-panel titles; the dock title bar is now the single label. Central serial panel reads from its content (the output stream) without a heading. GUI builds, offscreen AUTORUN runs hello.elf clean, core 355 ctests green. --- gui/view/panels/fault_panel.cpp | 3 --- gui/view/panels/gpio_panel.cpp | 3 --- gui/view/panels/peripheral_panel.cpp | 3 --- gui/view/panels/registers_panel.cpp | 3 --- gui/view/panels/serial_panel.cpp | 3 --- gui/view/panels/status_panel.cpp | 3 --- 6 files changed, 18 deletions(-) diff --git a/gui/view/panels/fault_panel.cpp b/gui/view/panels/fault_panel.cpp index 42a863a..6f9b5ab 100644 --- a/gui/view/panels/fault_panel.cpp +++ b/gui/view/panels/fault_panel.cpp @@ -49,9 +49,6 @@ const char* bus_error_name_raw(std::uint32_t raw) { FaultPanel::FaultPanel(QWidget* parent) : QWidget(parent) { auto* lay = new QVBoxLayout(this); - auto* title = new QLabel("Fault detail"); - title->setStyleSheet("font-weight: bold;"); - lay->addWidget(title); label_ = new QLabel("— none —"); label_->setStyleSheet("font-family: monospace;"); diff --git a/gui/view/panels/gpio_panel.cpp b/gui/view/panels/gpio_panel.cpp index 595ab15..ee0aa43 100644 --- a/gui/view/panels/gpio_panel.cpp +++ b/gui/view/panels/gpio_panel.cpp @@ -13,9 +13,6 @@ namespace micro_forge::gui::panels { GpioPanel::GpioPanel(QWidget* parent) : QWidget(parent) { auto* lay = new QVBoxLayout(this); - auto* title = new QLabel("GPIO output (A/B/C, pin0..pin15)"); - title->setStyleSheet("font-weight: bold;"); - lay->addWidget(title); label_ = new QLabel; label_->setStyleSheet("font-family: monospace;"); diff --git a/gui/view/panels/peripheral_panel.cpp b/gui/view/panels/peripheral_panel.cpp index 67a0681..32a2ddb 100644 --- a/gui/view/panels/peripheral_panel.cpp +++ b/gui/view/panels/peripheral_panel.cpp @@ -12,9 +12,6 @@ namespace micro_forge::gui::panels { PeripheralPanel::PeripheralPanel(QWidget* parent) : QWidget(parent) { auto* lay = new QVBoxLayout(this); - auto* title = new QLabel("Peripherals (SysTick / NVIC / SCB)"); - title->setStyleSheet("font-weight: bold;"); - lay->addWidget(title); label_ = new QLabel; label_->setStyleSheet("font-family: monospace;"); diff --git a/gui/view/panels/registers_panel.cpp b/gui/view/panels/registers_panel.cpp index b8aac0c..b76d7af 100644 --- a/gui/view/panels/registers_panel.cpp +++ b/gui/view/panels/registers_panel.cpp @@ -15,9 +15,6 @@ namespace micro_forge::gui::panels { RegistersPanel::RegistersPanel(QWidget* parent) : QWidget(parent) { auto* lay = new QVBoxLayout(this); - auto* title = new QLabel("CPU registers (r0-r12, sp, lr, pc)"); - title->setStyleSheet("font-weight: bold;"); - lay->addWidget(title); table_ = new QTableWidget(16, 2); table_->setHorizontalHeaderLabels({"reg", "value"}); diff --git a/gui/view/panels/serial_panel.cpp b/gui/view/panels/serial_panel.cpp index 53db4d8..96e1f4d 100644 --- a/gui/view/panels/serial_panel.cpp +++ b/gui/view/panels/serial_panel.cpp @@ -10,9 +10,6 @@ namespace micro_forge::gui::panels { SerialPanel::SerialPanel(QWidget* parent) : QWidget(parent) { auto* lay = new QVBoxLayout(this); - auto* title = new QLabel("Serial output"); - title->setStyleSheet("font-weight: bold;"); - lay->addWidget(title); view_ = new QTextEdit; view_->setReadOnly(true); diff --git a/gui/view/panels/status_panel.cpp b/gui/view/panels/status_panel.cpp index b953c2c..5859ebc 100644 --- a/gui/view/panels/status_panel.cpp +++ b/gui/view/panels/status_panel.cpp @@ -15,9 +15,6 @@ namespace micro_forge::gui::panels { StatusPanel::StatusPanel(QWidget* parent) : QWidget(parent) { auto* lay = new QVBoxLayout(this); - auto* title = new QLabel("Status / masks (xPSR, PRIMASK, MSP/PSP, ...)"); - title->setStyleSheet("font-weight: bold;"); - lay->addWidget(title); table_ = new QTableWidget(7, 2); table_->setHorizontalHeaderLabels({"field", "value"}); From a1d713c654943ac8f8fd768ee60caea2d2c0c86b Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Wed, 8 Jul 2026 08:53:41 +0800 Subject: [PATCH 8/9] test(gui): add Qt-free model::Session unit tests + bump floor to 357 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Session was extracted Qt-free (f81ab7d) precisely so it could be tested without QApplication. Adds test_gui_session (compiles session.cpp directly, no Qt link): rebuild validity + Halted state, step on a halted session, and with hello.elf loaded — run advances cycles, PC stays in flash, USART drains into the buffer, and a second rebuild clears it. Bumps test-count floor 353 -> 357. 4 new product tests, all green. Total 357 product + 2 meta = 359. --- scripts/check_test_count.sh | 2 +- test/CMakeLists.txt | 22 ++++++++++++ test/test_gui_session.cpp | 68 +++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 test/test_gui_session.cpp 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 From 124f0908059a1d85d3127238019e0a3f5178ab78 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Wed, 8 Jul 2026 08:59:16 +0800 Subject: [PATCH 9/9] feat(gpio_blink): runner loops forever + prints each PA5 toggle live MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runner hardcoded run(200000) then asserted toggle_count >= 6, so even after the firmware went infinite (e0f0ef1) the runner still stopped after ~6 toggles. Now it runs forever by default (Ctrl+C to stop) and prints each PA5 edge live — the visible blink on a headless run. A fault still breaks out and reports state + how far it got. 3s sample: 38 toggles (was 6 then halt). --- examples/gpio_blink/runner.cpp | 39 +++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) 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; }