// oled.cpp — Oled: the display controller chip (Cortex-M0). // // - create() boots the firmware (tick counter and IRQ-flag waits fed by // hooks) and resets the display context. // - send_* queue 8-byte wire frames and run one pass of the REAL // firmware RX dispatch loop; the framebuffer at addr::oled_fb is the // output (read it with framebuffer() / write it with write_pgm()). #include "targets/oled.hpp" #include "core/hooks.hpp" #include "core/internal.hpp" #include "firmware/addr.hpp" #include #include #include #include #include #include #include #include #include #include namespace mf { namespace { constexpr uint32_t FLASH_BASE = 0x08000000U; constexpr uint32_t FLASH_MAP = 0x40000U; // 256 KiB window; payload 0x13EF0, rest 0xFF constexpr uint32_t RAM_BASE = 0x20000000U; constexpr uint32_t RAM_SIZE = 0x8000U; // 32 KiB constexpr uint32_t PERIPH_BASE = 0x40000000U; constexpr uint32_t PERIPH_SIZE = 0x1000000U; // APB peripherals (benign) constexpr uint32_t AHB2_BASE = 0x48000000U; constexpr uint32_t AHB2_SIZE = 0x20000U; // GPIO constexpr uint32_t PPB_BASE = 0xE0000000U; constexpr uint32_t PPB_SIZE = 0x100000U; // ── fake RX queue (reference _install_queue_hooks) ──────────────────── // The dispatch loop calls vtable[0] (read byte) and vtable[1] (avail) on // the object at UART2+0x38. We point it at a RAM object with a RAM // vtable; the method bodies are never executed — host hooks fire at the // aligned addresses and return via PC = LR | 1. Quirks kept from the // reference: // - vtable entries must be ODD (Thumb bit): blx to an even RAM address // flips the CPU to ARM state. // - hooks must be registered at the EVEN aligned address (Unicorn // reports the aligned PC). constexpr uint32_t FAKE_Q = 0x20003000U; // queue object (inside the 32 KiB RAM) constexpr uint32_t VTB = 0x20003010U; // its vtable (read, avail) constexpr uint32_t READ_AT = 0x20003040U; // hook site for vtable[0] (entry: +1) constexpr uint32_t AVAIL_AT = 0x20003048U; // hook site for vtable[1] (entry: +1) // Return instructions of ssd1306_spi_write's two source/destination-order // branches. At either site the DMA registers have been programmed. constexpr uint32_t OLED_DMA_STARTED_A = 0x08006A82U; constexpr uint32_t OLED_DMA_STARTED_B = 0x08006A8AU; constexpr uint32_t OLED_GPIO_WRITE = 0x08001184U; constexpr uint32_t OLED_DC_GPIO = 0x48000C00U; constexpr uint32_t OLED_DC_MASK = 4U; constexpr uint32_t kDispatchSp = 0x20002000U; // SP for dispatch passes } // namespace // ══ Construction ═════════════════════════════════════════════════════ Oled::Oled(std::optional font, UnmapPolicy unmap_policy, DiagFn diag, BusObserver bus_observer) : Target(RAM_BASE, RAM_SIZE, std::move(diag)), font_(font), bus_observer_(std::move(bus_observer)) { hook_ctx_.policy = unmap_policy; uc_err const e = uc_open( UC_ARCH_ARM, // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) — Unicorn modes are an OR-able bitmask; 48 is a valid combination. static_cast(UC_MODE_THUMB | UC_MODE_MCLASS), &uc_); if (e != UC_ERR_OK) { return; // create() reports the failure (uc_ stays null) } // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) — Unicorn CPU ids are bitmask values, not enum members. if (uc_ctl_set_cpu_model(uc_, UC_CPU_ARM_CORTEX_M0) != UC_ERR_OK) { uc_close(uc_); uc_ = nullptr; } } // ══ Internal hooks ═══════════════════════════════════════════════════ void Oled::tick_hook() { // During boot there is no external OLED clock yet, so the firmware's // timeout reads are the boot-time clock source. Once the target is live, // the explicit timer-wakeup path owns time and this hook is a plain read. if (!boot_complete_) { ++tick_; } uc_mem_write(&uc(), addr::oled_tick_var, &tick_, 4); uc_reg_write(&uc(), UC_ARM_REG_R0, &tick_); } void Oled::wait_flag_hook() { // The firmware spins on [*oled_flag_obj + 0x10] & 1. A hardware timer // wake both releases this wait and advances the local emulated clock; // model both sides so OLED animations progress while the main loop runs. if (clock_running_) { ++tick_; uc_mem_write(&uc(), addr::oled_tick_var, &tick_, 4); } uint32_t p = 0; uc_mem_read(&uc(), addr::oled_flag_obj, &p, 4); if (p != 0) { uint32_t f = 0; uc_mem_read(&uc(), p + 0x10, &f, 4); f |= 1U; uc_mem_write(&uc(), p + 0x10, &f, 4); } if (clock_running_ && wakes_remaining_ > 0) { --wakes_remaining_; if (wakes_remaining_ == 0) { wait_seen_ = true; (void)uc_emu_stop(&uc()); } } else if (stop_after_wait_) { wait_seen_ = true; (void)uc_emu_stop(&uc()); } } void Oled::queue_avail_hook() { const uint32_t r0 = ioc_rx_.available() > 0 ? 1U : 0U; uc_reg_write(&uc(), UC_ARM_REG_R0, &r0); uint32_t lr = 0; uc_reg_read(&uc(), UC_ARM_REG_LR, &lr); lr |= 1U; uc_reg_write(&uc(), UC_ARM_REG_PC, &lr); // return to the caller (Thumb) } void Oled::queue_read_hook() { uint8_t byte = 0; (void)ioc_rx_.read({&byte, 1}); const uint32_t r0 = byte; uc_reg_write(&uc(), UC_ARM_REG_R0, &r0); uint32_t lr = 0; uc_reg_read(&uc(), UC_ARM_REG_LR, &lr); lr |= 1U; uc_reg_write(&uc(), UC_ARM_REG_PC, &lr); // return to the caller (Thumb) } void Oled::gpio_write_hook() { uint32_t base = 0; uint32_t mask = 0; uint32_t set = 0; uc_reg_read(&uc(), UC_ARM_REG_R0, &base); uc_reg_read(&uc(), UC_ARM_REG_R1, &mask); uc_reg_read(&uc(), UC_ARM_REG_R2, &set); if (base == OLED_DC_GPIO && mask == OLED_DC_MASK) { dc_ = set != 0 ? 1U : 0U; } } bool Oled::spi_write_hook(uc_mem_type /*type*/, uint64_t address, int size, int64_t value) { if (address > 0x4001300CU || address + static_cast(size) <= 0x4001300CU) { return true; } const uint64_t offset = 0x4001300CU - address; const int count = std::min(size - static_cast(offset), static_cast(direct_bytes_.size())); for (int i = 0; i < count; ++i) { direct_bytes_[static_cast(i)] = static_cast(static_cast(value) >> (8U * (i + offset))); } if (bus_observer_ && count > 0) { bus_observer_(BusTransfer{tick_, 0, 0, 0x4001300CU, dc_, {direct_bytes_.data(), static_cast(count)}}); } return true; } void Oled::boot_main_loop_hook() { if (!boot_complete_) { boot_main_loop_seen_ = true; (void)uc_emu_stop(&uc()); } } void Oled::display_update_hook() { uc_reg_read(&uc(), UC_ARM_REG_R0, &display_driver_); } // Model the missing local SSD1306 DMA peer. The firmware's DMA ISR clears the // busy state and invokes the callbacks that advance/finish a split transfer. // Complete transfers immediately after ssd1306_spi_write programs the channel; // this preserves firmware control flow without tying progress to an arbitrary // host polling frequency. void Oled::display_dma_started_hook() { // The local SSD1306 DMA has no peripheral peer in Unicorn. This is the // first point at which the firmware has accepted the source buffer and // programmed the physical transfer. Observe the source here; the real // DMA ISR is serviced outside the code hook. Neither observer is a host // framebuffer poll or an optimistic upload request. if (stop_after_dma_) { uint32_t pc = 0; uc_reg_read(&uc(), UC_ARM_REG_PC, &pc); if (skip_dma_pc_ == pc) { skip_dma_pc_ = 0; return; } } uint32_t stream = 0; uint32_t length = 0; uint32_t peripheral = 0; uint32_t source = 0; if ((bus_observer_ || commit_observer_ != nullptr) && uc_mem_read(&uc(), addr::oled_dma_handle, &stream, sizeof(stream)) == UC_ERR_OK && stream != 0 && uc_mem_read(&uc(), stream + 4U, &length, sizeof(length)) == UC_ERR_OK && uc_mem_read(&uc(), stream + 8U, &peripheral, sizeof(peripheral)) == UC_ERR_OK && uc_mem_read(&uc(), stream + 12U, &source, sizeof(source)) == UC_ERR_OK) { bus_bytes_.resize(length); if (length == 0 || uc_mem_read(&uc(), source, bus_bytes_.data(), length) == UC_ERR_OK) { if (bus_observer_) { bus_observer_(BusTransfer{tick_, stream, source, peripheral, dc_, bus_bytes_}); } if (commit_observer_ != nullptr && source == addr::oled_fb && length == frame_bytes_.size()) { std::copy(bus_bytes_.begin(), bus_bytes_.end(), frame_bytes_.begin()); (*commit_observer_)(frame_bytes_, tick_); } } } if (stop_after_dma_) { dma_seen_ = true; dma_pending_ = true; uint32_t pc = 0; uc_reg_read(&uc(), UC_ARM_REG_PC, &pc); (void)uc_emu_stop(&uc()); return; } dma_pending_ = true; } void Oled::service_display_dma() { if (!dma_pending_ || servicing_display_dma_) { return; } dma_pending_ = false; servicing_display_dma_ = true; struct ServiceReset { bool &active; ~ServiceReset() { active = false; } } reset{servicing_display_dma_}; { uint32_t dma_base = 0; uint32_t shift = 0; if (uc_mem_read(&uc(), addr::oled_dma_handle + 0x3CU, &dma_base, sizeof(dma_base)) != UC_ERR_OK || uc_mem_read(&uc(), addr::oled_dma_handle + 0x40U, &shift, sizeof(shift)) != UC_ERR_OK || dma_base == 0 || shift >= 28U) { return; } uint32_t isr = 0; if (uc_mem_read(&uc(), dma_base, &isr, sizeof(isr)) != UC_ERR_OK) { return; } // The SPI status register is the other half of the DMA/IRQ handshake. // Set TX-ready/bus-state bits so the real ISR takes its completion // callback instead of treating the flat peripheral as idle. const uint32_t spi = 0x40013000U; const uint32_t spi_status = 0x08U; (void)uc_mem_write(&uc(), spi, &spi_status, sizeof(spi_status)); // STM32 DMA ISR layout: TCIF is bit (1 + channel_shift). The handler // reads ISR through handle+0x3c and acknowledges through IFCR. const uint32_t tcif = 8U << shift; isr |= tcif; (void)uc_mem_write(&uc(), dma_base, &isr, sizeof(isr)); try { (void)call(addr::oled_dma_irq, CallArgs{}); if (display_driver_ != 0) { const uint8_t idle = 0; const uint8_t ready = 1; uc_mem_write(&uc(), display_driver_ + 0x20U, &idle, sizeof(idle)); uc_mem_write(&uc(), display_driver_ + 0x21U, &ready, sizeof(ready)); // display_driver_ is the DMA handle. Its owner at +0x24 is // the SPI/display context used by ssd1306_draw_region(); // the real DMA callback releases that context's +0x5d busy // byte, but the peripheral cannot generate that side effect // in flat Unicorn memory. uint32_t owner = 0; uc_mem_read(&uc(), display_driver_ + 0x24U, &owner, sizeof(owner)); if (owner != 0) { uc_mem_write(&uc(), owner + 0x5DU, &ready, sizeof(ready)); } } } catch (const Error &e) { std::println(stderr, "oled display DMA ISR failed: {}", e.what()); faulted_ = true; return; } // IFCR writes have no side effects in flat Unicorn peripheral memory. // Apply the hardware's corresponding ISR clear explicitly. isr &= ~tcif; (void)uc_mem_write(&uc(), dma_base, &isr, sizeof(isr)); } } // ══ Command injection ════════════════════════════════════════════════ Oled::Frame Oled::text_append_frame(std::string_view text) { if (text.empty() || text.size() > 4) { throw Error{Status::argument, "OLED text append must contain 1..4 bytes"}; } std::array chars{}; for (size_t i = 0; i < text.size(); i++) { chars[i] = static_cast(text[i]); } if (text.size() == 4) { return frame_build(0xF7, 0x72, chars[0], chars[1], chars[2], chars[3]); } return frame_build(0xF7, 0x71, chars[0], chars[1], chars[2], static_cast(text.size())); } // Run one dispatch pass over the queued bytes (reference send()): point // the uart2 object's RX queue at the fake queue (the firmware init // overwrites it at boot), set up the call registers, and run the real // dispatch loop with an instruction cap that also covers the main-loop // tail after LR. namespace { std::vector g_trace; void trace_hook(uc_engine * /*uc*/, uint64_t addr, uint32_t /*size*/, void * /*user*/) { g_trace.push_back(static_cast(addr)); } bool enqueue_frame(IocRxQueue &queue, const std::array &frame) { return queue.write(std::span{frame}); } } // namespace void Oled::dispatch(const FrameObserver &on_commit) { dispatch_impl(on_commit); } void Oled::dispatch_until_idle(const FrameObserver &on_commit) { dispatch_impl(on_commit); } void Oled::dispatch_impl(const FrameObserver &on_commit) { if (faulted_) { throw Error{Status::state, "oled faulted; destroy it"}; } if (ioc_rx_.available() == 0) { return; } const auto *previous_observer = commit_observer_; commit_observer_ = on_commit ? &on_commit : nullptr; struct ObserverReset { const FrameObserver *&slot; const FrameObserver *previous; ~ObserverReset() { slot = previous; } } observer_reset{commit_observer_, previous_observer}; const bool previous_stop = stop_after_wait_; stop_after_wait_ = true; wait_seen_ = false; struct StopReset { bool &stop; bool previous; ~StopReset() { stop = previous; } } stop_reset{stop_after_wait_, previous_stop}; uc_engine *uc = uc_; uc_hook trace_hook_id = 0; g_trace.clear(); if (std::getenv("MF_DISPATCH_TRACE") != nullptr) { uc_hook_add(uc, &trace_hook_id, UC_HOOK_CODE, reinterpret_cast(trace_hook), nullptr, 1, 0); } const auto remove_trace = [&]() { if (trace_hook_id != 0) { (void)uc_hook_del(uc, trace_hook_id); } }; const uint32_t q = FAKE_Q; uc_mem_write(uc, addr::oled_uart2_obj + 0x38, &q, 4); const uint32_t sp = kDispatchSp; const uint32_t r0 = addr::oled_uart2_obj; const uint32_t r1 = 0; const uint32_t lr = addr::oled_main_loop | 1U; uc_reg_write(uc, UC_ARM_REG_SP, &sp); uc_reg_write(uc, UC_ARM_REG_R0, &r0); uc_reg_write(uc, UC_ARM_REG_R1, &r1); uc_reg_write(uc, UC_ARM_REG_LR, &lr); // A redraw can span several firmware wait boundaries (glyph rendering // plus the chip's internal idle loop). Re-enter until the queue is // drained; the state machine lives in chip RAM and each pass resumes at // the next byte. for (int pass = 0;; ++pass) { // Do not abandon a handler when the RX queue becomes empty halfway // through rendering. The wait hook supplies the real firmware // boundary, so no in-flight CPU state is lost. const uc_err e = emu_start(addr::oled_dispatch | 1U, 0); if (e != UC_ERR_OK) { faulted_ = true; std::array msg{}; std::snprintf(msg.data(), msg.size(), "oled dispatch: %s", detail::uc_err_str(e)); remove_trace(); throw Error{Status::emulator, msg.data()}; } service_display_dma(); if (ioc_rx_.available() == 0) { if (std::getenv("MF_DISPATCH_DEBUG") != nullptr) { std::println(stderr, "oled dispatch: drained in {} passes", pass + 1); } static unsigned trace_seq = 0; if (std::getenv("MF_DISPATCH_TRACE") != nullptr) { char path[64]; std::snprintf(path, sizeof(path), "/tmp/oled_trace_%02u.bin", trace_seq++); if (auto tf = std::fopen(path, "wb"); tf != nullptr) { std::fwrite(g_trace.data(), 4, g_trace.size(), tf); std::fclose(tf); std::println(stderr, "oled dispatch: traced {} pcs", g_trace.size()); } } remove_trace(); return; } } } void Oled::advance_ticks(const uint32_t ticks, const FrameObserver &on_commit) { if (faulted_) { throw Error{Status::state, "oled faulted; destroy it"}; } if (ioc_rx_.available() != 0) { throw Error{Status::state, "OLED timer advance requires an empty RX queue"}; } if (ticks == 0) { return; } const auto *previous_observer = commit_observer_; commit_observer_ = on_commit ? &on_commit : nullptr; struct ObserverReset { const FrameObserver *&slot; const FrameObserver *previous; ~ObserverReset() { slot = previous; } } observer_reset{commit_observer_, previous_observer}; // Match dispatch()'s boundary setup. The firmware returns from the RX // pass into its permanent main loop. Stop only for a requested timer // count or a programmed DMA transfer, so the missing peripheral can be // serviced without polling the framebuffer or paying one emulation start // per timer wake. const uint32_t q = FAKE_Q; uc_mem_write(&uc(), addr::oled_uart2_obj + 0x38, &q, 4); const uint32_t sp = kDispatchSp; const uint32_t r0 = addr::oled_uart2_obj; const uint32_t r1 = 0; const uint32_t lr = addr::oled_main_loop | 1U; uc_reg_write(&uc(), UC_ARM_REG_SP, &sp); uc_reg_write(&uc(), UC_ARM_REG_R0, &r0); uc_reg_write(&uc(), UC_ARM_REG_R1, &r1); uc_reg_write(&uc(), UC_ARM_REG_LR, &lr); const bool previous_stop = stop_after_wait_; const bool previous_dma = stop_after_dma_; const bool previous_clock = clock_running_; const uint32_t previous_wakes = wakes_remaining_; const uint32_t previous_skip_pc = skip_dma_pc_; stop_after_wait_ = false; stop_after_dma_ = true; clock_running_ = true; wakes_remaining_ = ticks; struct AdvanceReset { bool &stop; bool &dma; bool &clock; uint32_t &wakes; uint32_t &skip_pc; bool previous_stop; bool previous_dma; bool previous_clock; uint32_t previous_wakes; uint32_t previous_skip_pc; ~AdvanceReset() { stop = previous_stop; dma = previous_dma; clock = previous_clock; wakes = previous_wakes; skip_pc = previous_skip_pc; } } advance_reset{stop_after_wait_, stop_after_dma_, clock_running_, wakes_remaining_, skip_dma_pc_, previous_stop, previous_dma, previous_clock, previous_wakes, previous_skip_pc}; uint32_t start_pc = addr::oled_dispatch | 1U; while (wakes_remaining_ != 0) { wait_seen_ = false; dma_seen_ = false; // The timer/DMA hooks are the completion conditions; the unbounded // run continues until one of those events. const uc_err e = emu_start(start_pc, 0); service_display_dma(); if (dma_seen_) { uint32_t pc = 0; uc_reg_read(&uc(), UC_ARM_REG_PC, &pc); skip_dma_pc_ = pc; start_pc = pc | 1U; } else { start_pc = addr::oled_dispatch | 1U; } if (e != UC_ERR_OK) { faulted_ = true; uint32_t pc = 0; uint32_t fault_lr = 0; uc_reg_read(&uc(), UC_ARM_REG_PC, &pc); uc_reg_read(&uc(), UC_ARM_REG_LR, &fault_lr); std::array msg{}; std::snprintf(msg.data(), msg.size(), "oled timer advance: %s pc=0x%08x lr=0x%08x", detail::uc_err_str(e), pc, fault_lr); throw Error{Status::emulator, msg.data()}; } if (!wait_seen_ && !dma_seen_) { faulted_ = true; throw Error{Status::budget, "OLED timer advance did not reach a timer or DMA event"}; } } } void Oled::send_text(std::string_view text, bool draw) { if (faulted_) { throw Error{Status::state, "oled faulted; destroy it"}; } const size_t text_frames = (text.size() + 3U) / 4U; if (text_frames + (draw ? 1U : 0U) > ioc_rx_.writable() / 8U) { throw Error{Status::resource, "oled IOC RX queue overflow"}; } // Append frames: 1..4 chars each (display cmd handler, m1 & 3 path). // 1..3 chars: m1=0x71, chars in m2..m4 (zero-padded), len in m5 // 4 chars: m1=0x72, chars in m2..m5 for (size_t i = 0; i < text.size(); i += 4) { const auto f = text_append_frame(text.substr(i, std::min(4, text.size() - i))); if (!enqueue_frame(ioc_rx_, f)) { throw Error{Status::resource, "oled IOC RX queue overflow"}; } } if (draw) { // clear + centered text: oled_display_cmd(0x1A, 3) — queued into // the SAME dispatch pass as the text frames (reference send_text). if (!enqueue_frame(ioc_rx_, display_cmd_frame(0x1A, 3))) { throw Error{Status::resource, "oled IOC RX queue overflow"}; } } dispatch(); } void Oled::send_display_cmd(uint8_t cmd, uint16_t param) { if (faulted_) { throw Error{Status::state, "oled faulted; destroy it"}; } // Existing queued bytes are preserved; callers that need a fresh stream // can clear ioc_rx() explicitly. display_cmd_frame masks the 11-bit // parameter field exactly as the firmware decoder does. if (!enqueue_frame(ioc_rx_, display_cmd_frame(cmd, param))) { throw Error{Status::resource, "oled IOC RX queue overflow"}; } dispatch(); } void Oled::send_bytes(std::span bytes) { if (faulted_) { throw Error{Status::state, "oled faulted; destroy it"}; } if (!ioc_rx_.write(bytes)) { throw Error{Status::resource, "oled IOC RX queue overflow"}; } dispatch(); } void Oled::send_frame(const Frame &frame) { send_bytes(frame); } void Oled::send_frames(std::span frames) { if (faulted_) { throw Error{Status::state, "oled faulted; destroy it"}; } if (frames.size() > ioc_rx_.writable() / 8U) { throw Error{Status::resource, "oled IOC RX queue overflow"}; } for (const auto &frame : frames) { if (!enqueue_frame(ioc_rx_, frame)) { throw Error{Status::resource, "oled IOC RX queue overflow"}; } } dispatch(); } // ══ Output ═══════════════════════════════════════════════════════════ Oled::DisplayState Oled::display_state() const { DisplayState state; std::array ctx{}; if (uc_mem_read(uc_, addr::oled_display_ctx, ctx.data(), ctx.size()) != UC_ERR_OK || uc_mem_read(uc_, addr::oled_font_slot, &state.font, sizeof(state.font)) != UC_ERR_OK || uc_mem_read(uc_, addr::oled_text_len, &state.text_len, sizeof(state.text_len)) != UC_ERR_OK || uc_mem_read(uc_, addr::oled_text_buf, state.text.data(), state.text.size()) != UC_ERR_OK || uc_mem_read(uc_, addr::oled_scratch, state.scratch.data(), state.scratch.size()) != UC_ERR_OK) { throw Error{Status::emulator, "OLED display state read failed"}; } state.sub_screen = ctx[0x00]; state.dirty = ctx[0x01]; state.layout_text = ctx[0x11]; state.mode = ctx[0x12]; return state; } Oled::Snapshot Oled::save_snapshot() { if (faulted_) { throw Error{Status::state, "oled faulted; destroy it"}; } if (ioc_rx_.available() != 0) { throw Error{Status::state, "OLED snapshot requires an empty RX queue"}; } return Snapshot{save_ram(), save_state(), tick_}; } void Oled::restore_snapshot(const Snapshot &snapshot) { restore_ram(snapshot.ram); restore_state(snapshot.cpu); tick_ = snapshot.tick; faulted_ = false; ioc_rx_.clear(); } std::array Oled::framebuffer() const { std::array fb{}; if (uc_mem_read(uc_, addr::oled_fb, fb.data(), fb.size()) != UC_ERR_OK) { throw Error{Status::emulator, "OLED framebuffer read failed"}; } return fb; } void Oled::write_pgm(const std::string &path, unsigned scale) const { if (scale == 0) { throw Error{Status::argument, "scale must be >= 1"}; } const auto fb = framebuffer(); const unsigned w = 128 * scale; const unsigned h = 64 * scale; std::vector img; img.reserve(static_cast(w) * h + 16); const auto append = [&img](const char *s, size_t n) { const auto *b = reinterpret_cast(s); img.insert(img.end(), b, b + n); }; char header[32]; const int hdr_len = std::snprintf(header, sizeof(header), "P5\n%u %u\n255\n", w, h); append(header, static_cast(hdr_len)); // SSD1306 layout: pixel (x, y) = bit (y % 8) of fb[(y / 8) * 128 + x]; // dark glyphs on white (reference dump_pgm). for (unsigned yy = 0; yy < h; yy++) { const unsigned y = yy / scale; for (unsigned xx = 0; xx < w; xx++) { const unsigned x = xx / scale; const unsigned bit = (fb[(y / 8) * 128 + x] >> (y % 8)) & 1U; img.push_back(static_cast(bit == 0 ? 255 : 0)); } } detail::write_file(path, img); } // ══ create ═══════════════════════════════════════════════════════════ std::unique_ptr Oled::create(std::span firmware, std::optional font, UnmapPolicy unmap_policy, DiagFn diag, FrameObserver on_commit, BusObserver on_bus) { if (firmware.empty()) { throw Error{Status::argument, "firmware bytes required"}; } std::unique_ptr o( new Oled(font, unmap_policy, std::move(diag), std::move(on_bus))); if (o->uc_ == nullptr) { throw Error{Status::emulator, "uc_open failed"}; } uc_engine *uc = o->uc_; o->hook_ctx_.counters = &o->counters_; struct Region { uint32_t base; uint32_t size; }; static constexpr std::array kRegions{{ {FLASH_BASE, FLASH_MAP}, {RAM_BASE, RAM_SIZE}, {detail::kStubBase, detail::kStubSize}, {PERIPH_BASE, PERIPH_SIZE}, {AHB2_BASE, AHB2_SIZE}, {PPB_BASE, PPB_SIZE}, }}; for (const auto &r : kRegions) { const uc_err e = uc_mem_map(uc, r.base, r.size, UC_PROT_ALL); if (e != UC_ERR_OK && e != UC_ERR_MAP) { throw Error{Status::emulator, "uc_mem_map failed"}; } } // Hooks. All registrations checked — a silently missing hook would // hang the boot on the first tick/flag wait. o->hook_ctx_.engine = uc; if (!code_hook_add<&Oled::tick_hook>(uc, addr::oled_tick_fn, o.get()) || !code_hook_add<&Oled::wait_flag_hook>(uc, addr::oled_wait_fn, o.get()) || !code_hook_add<&Oled::queue_read_hook>(uc, READ_AT, o.get()) || !code_hook_add<&Oled::queue_avail_hook>(uc, AVAIL_AT, o.get()) || !code_hook_add<&Oled::gpio_write_hook>(uc, OLED_GPIO_WRITE, o.get()) || !code_hook_add<&Oled::boot_main_loop_hook>(uc, addr::oled_main_loop, o.get()) || !code_hook_add<&Oled::display_update_hook>(uc, addr::oled_ssd1306_update, o.get()) || !code_hook_add<&Oled::display_dma_started_hook>(uc, OLED_DMA_STARTED_A, o.get()) || !code_hook_add<&Oled::display_dma_started_hook>(uc, OLED_DMA_STARTED_B, o.get()) || !mem_hook_add<&Oled::spi_write_hook>(uc, UC_HOOK_MEM_WRITE, 0x4001300CU, 0x4001300FU, o.get()) || !mem_hook_add(uc, UC_HOOK_MEM_UNMAPPED, 1, 0, &o->hook_ctx_)) { throw Error{Status::emulator, "uc_hook_add failed"}; } // Fake RX queue object + vtable + nop bodies (see the constants above). const uint32_t vtb = VTB; uc_mem_write(uc, FAKE_Q, &vtb, 4); const uint32_t entries[2] = {READ_AT + 1U, AVAIL_AT + 1U}; // odd: Thumb uc_mem_write(uc, VTB, entries, sizeof(entries)); const std::array nops{0xBF, 0x00, 0xBF, 0x00, 0xBF, 0x00, 0xBF, 0x00}; uc_mem_write(uc, READ_AT, nops.data(), nops.size()); uc_mem_write(uc, AVAIL_AT, nops.data(), nops.size()); // Firmware payload (skip the 64-byte DFU header); the rest of the // 256 KiB window stays 0xFF — erased-flash state, as in the reference. if (firmware.size() <= 64) { throw Error{Status::unsupported, "image too small (need header+payload)"}; } const size_t payload_size = std::min(firmware.size() - 64, static_cast(FLASH_MAP)); std::vector image(FLASH_MAP, std::byte{0xFF}); std::memcpy(image.data(), firmware.data() + 64, payload_size); if (uc_mem_write(uc, addr::oled_fw_load, image.data(), image.size()) != UC_ERR_OK) { throw Error{Status::emulator, "firmware write failed"}; } // Boot from the SystemInit-skipping entry. The local SSD1306 has no DMA // peer in Unicorn, so stop at every programmed transfer, run the real DMA // ISR, and resume at the instruction after the hook. Stop once main's // permanent loop is reached; otherwise the initialization command stream // would leave the driver busy after its first byte (usually 0x81). const uint32_t sp = addr::oled_msp; uc_reg_write(uc, UC_ARM_REG_SP, &sp); o->commit_observer_ = on_commit ? &on_commit : nullptr; o->stop_after_dma_ = true; o->boot_main_loop_seen_ = false; o->skip_dma_pc_ = 0; uint32_t start_pc = addr::oled_boot_entry | 1U; uc_err e = UC_ERR_OK; constexpr unsigned kBootPasses = 128; for (unsigned pass = 0; pass < kBootPasses && !o->boot_main_loop_seen_; ++pass) { o->dma_seen_ = false; e = o->emu_start(start_pc, 0); o->service_display_dma(); if (e != UC_ERR_OK || o->boot_main_loop_seen_) { break; } if (!o->dma_seen_) { throw Error{Status::budget, "OLED boot did not reach DMA or main loop"}; } uint32_t pc = 0; uc_reg_read(uc, UC_ARM_REG_PC, &pc); o->skip_dma_pc_ = pc; start_pc = pc | 1U; } o->stop_after_dma_ = false; o->skip_dma_pc_ = 0; o->commit_observer_ = nullptr; if (e != UC_ERR_OK) { o->faulted_ = true; std::array msg{}; std::snprintf(msg.data(), msg.size(), "oled boot: %s", detail::uc_err_str(e)); throw Error{Status::boot, msg.data()}; } if (!o->boot_main_loop_seen_) { o->faulted_ = true; throw Error{Status::budget, "OLED boot did not reach main loop"}; } // Display context reset (reference __init__ tail). The firmware's // main() selects the sans-serif font into FONT_SLOT at boot; the // default config re-writes the same value (reference behaviour), // nullopt keeps the firmware's own selection. if (o->font_.has_value()) { const uint32_t font_sel = *o->font_; uc_mem_write(uc, addr::oled_font_slot, &font_sel, 4); } const uint32_t zero4 = 0; uc_mem_write(uc, addr::oled_text_len, &zero4, 4); const std::array zeros64{}; uc_mem_write(uc, addr::oled_text_buf, zeros64.data(), zeros64.size()); const std::array zeros_fb{}; uc_mem_write(uc, addr::oled_fb, zeros_fb.data(), zeros_fb.size()); o->boot_complete_ = true; o->loginfo("oled booted"); return o; } } // namespace mf