// matrix.cpp — Matrix: the knob-matrix scan chip (Cortex-M0). // // - create() boots the real reset sequence to the main-loop head // (0x08006846) with the missing hardware simulated by a peripheral // READ hook: RCC ready flags follow the on-bits, PWR reports // settled, ADC ADRDY/EOC are asserted and the CR command bits // self-clear, TIM6's update flag is always set. // - iterate() resumes at the loop head + 2 and stops there again // (stop-before-prologue), so every boundary is scheduler-pass // consistent. // // The scheduler at 0x20000520 runs three C++ task objects. #include "targets/matrix.hpp" #include "core/hooks.hpp" #include "core/internal.hpp" #include "firmware/addr.hpp" #include #include #include #include #include #include #include #include namespace mf { namespace { // ── machine layout (STM32F070-class) ──────────────── constexpr uint32_t FLASH_BASE = 0x08000000U; constexpr uint32_t FLASH_MAP = 0x10000U; // 64 KiB window; payload 0x73F0, rest 0xFF constexpr uint32_t RAM_BASE = 0x20000000U; constexpr uint32_t RAM_SIZE = 0x4000U; // 16 KiB (vector MSP says 6 KiB) constexpr uint32_t PERIPH_BASE = 0x40000000U; constexpr uint32_t PERIPH_SIZE = 0x10000000U; // incl. GPIO at 0x48000000 constexpr uint32_t SCB_BASE = 0xE000E000U; constexpr uint32_t SCB_SIZE = 0x1000U; // ── simulated peripherals (STM32F0 register offsets) ───────────────── constexpr uint32_t RCC_CR = 0x40021000U; // HSIRDY/HSERDY/PLLRDY follow on-bits constexpr uint32_t RCC_CFGR = 0x40021004U; // SWS follows SW constexpr uint32_t RCC_BDCR = 0x40021020U; // LSERDY follows LSEON constexpr uint32_t RCC_CSR = 0x40021024U; // LSIRDY follows LSION constexpr uint32_t RCC_CR2 = 0x40021034U; // HSI14RDY/HSI48RDY follow on-bits constexpr uint32_t PWR_CSR = 0x40007004U; // PVDO (regulator ready) constexpr uint32_t TIM6_SR = 0x40001010U; // UIF always set (1 ms tick) constexpr uint32_t ADC_ISR = 0x40012400U; // ADRDY|EOC|EOSEQ constexpr uint32_t ADC_CR = 0x40012408U; // ADDIS clears ADEN; ADSTP clears ADSTART // ── IOC link (USART1) + its RTS/CTS flow control ────── // The link's state machine requests the bus by driving PA12 (RTS) and // waits for PA11 (CTS). With no peer emulated, CTS follows RTS. constexpr uint32_t GPIO_BASE = 0x48000000U; constexpr uint32_t GPIO_SPAN = 0x1800U; constexpr uint32_t GPIOA_IDR = 0x48000010U; constexpr uint32_t GPIOA_ODR = 0x48000014U; constexpr uint32_t GPIO_BSRR = 0x18U; constexpr uint32_t GPIO_BRR = 0x28U; constexpr uint32_t USART1_CR1 = 0x40013800U; // bit 7 = TXEIE constexpr uint32_t USART1_ISR = 0x4001381CU; // bit 7 = TXE, bit 6 = TC constexpr uint32_t USART1_TDR = 0x40013828U; // ── ADC → memory DMA ────────────────────────────────── // adc_dma_arm() programs DMA1_Channel1 for 18 32-bit samples from the // ADC data register into the ADC task's buffer, then sets ADSTART. // With no DMA engine in Unicorn the transfer is modelled here. constexpr uint32_t DMA1_ISR = 0x40020000U; // GIF/TCIF/HTIF/TEIF constexpr uint32_t DMA1_IFCR = 0x40020004U; constexpr uint32_t DMA1_CCR1 = 0x40020008U; // bit 0 = EN constexpr uint32_t DMA1_CNDTR1 = 0x4002000CU; constexpr uint32_t DMA1_CMAR1 = 0x40020014U; constexpr uint32_t ADC_TASK = 0x200001CCU; // channel enable mask/map/filter constexpr uint32_t ADC_TASK_MAP = ADC_TASK + 0xFCU; // map[ch] -> DMA slot constexpr uint32_t ADC_TASK_BUF = ADC_TASK + 0x14U; // DMA destination constexpr uint32_t ADC_CHANNELS = 18U; constexpr uint32_t DMA_CH_FLAGS = 0x6U; // HTIF | TCIF (channel 1) } // namespace // ══ Construction ═════════════════════════════════════════════════════ Matrix::Matrix(UnmapPolicy unmap_policy, DiagFn diag) : Target(RAM_BASE, RAM_SIZE, std::move(diag)) { 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; } } // ══ Peripheral model ═════════════════════════════════════════════════ bool Matrix::periph_read_hook(uc_mem_type /*type*/, uint64_t address, int size, int64_t /*value*/) { if (size != 4) { return true; // only the 32-bit register reads need patching } uint32_t v = 0; uc_mem_read(&uc(), address, &v, 4); const uint32_t in = v; switch (address) { case RCC_CR: if (v & (1U << 0)) { v |= 1U << 1; // HSIRDY } if (v & (1U << 16)) { v |= 1U << 17; // HSERDY } if (v & (1U << 24)) { v |= 1U << 25; // PLLRDY } break; case RCC_CFGR: v = (v & ~0xCU) | ((v & 0x3U) << 2); // SWS ← SW break; case RCC_BDCR: if (v & 1U) { v |= 1U << 1; // LSERDY } break; case RCC_CSR: if (v & 1U) { v |= 1U << 1; // LSIRDY } break; case RCC_CR2: if (v & 1U) { v |= 1U << 1; // HSI14RDY } if (v & (1U << 16)) { v |= 1U << 17; // HSI48RDY } break; case PWR_CSR: v |= 1U << 1; // PVDO: VDD settled break; case TIM6_SR: v |= 1U; // UIF: the loop's tick always arrived break; case ADC_ISR: v |= 0xDU; // ADRDY | EOC | EOSEQ break; case ADC_CR: if (v & (1U << 1)) { // ADDIS: disable completes instantly v &= ~((1U << 1) | (1U << 0)); } if (v & (1U << 4)) { // ADSTP: stop completes instantly v &= ~((1U << 4) | (1U << 2)); } break; case GPIOA_IDR: // Peer handshake: CTS (PA11) follows our RTS (PA12, GPIOA ODR). // The link SM asserts RTS, then sends once it sees CTS. v = (v & ~(1U << 11)) | (((read_u32(GPIOA_ODR) >> 12) & 1U) << 11); break; default: return true; } if (v != in) { uc_mem_write(&uc(), address, &v, 4); } return true; } void Matrix::gpio_write_hook(uc_mem_type /*type*/, uint64_t address, int size, int64_t value) { // GPIOx_BSRR/BRR are write-only; the ODR they modify is what the // RTS loopback reads. (All six ports, one page apart.) if (size != 4 || address < GPIO_BASE || address >= GPIO_BASE + GPIO_SPAN) { return; } const uint32_t port = static_cast(address - GPIO_BASE) / 0x400U; const uint32_t base = GPIO_BASE + port * 0x400U; const auto v = static_cast(value); if (address == base + GPIO_BSRR) { const uint32_t odr = read_u32(base + 0x14U); write_u32(base + 0x14U, (odr | (v & 0xFFFFU)) & ~(v >> 16)); } else if (address == base + GPIO_BRR) { write_u32(base + 0x14U, read_u32(base + 0x14U) & ~v); } } void Matrix::tdr_write_hook(uc_mem_type /*type*/, uint64_t address, int size, int64_t value) { if (address != USART1_TDR || (size != 1 && size != 2)) { return; } const auto b = static_cast(value & 0xFF); ioc_uart_.push(std::span{&b, 1}, tx_dropped_); } void Matrix::adc_write_hook(uc_mem_type /*type*/, uint64_t address, int size, int64_t value) { // A completed conversion block starts when the firmware sets ADSTART // on an armed DMA channel (adc_dma_arm: dma_transfer_config then // CR |= ADSTART). if (address != ADC_CR || size != 4) { return; } if ((static_cast(value) & (1U << 2)) == 0) { return; } if ((read_u32(DMA1_CCR1) & 1U) == 0 || read_u32(DMA1_CNDTR1) == 0) { return; } perform_dma(); } void Matrix::perform_dma() { // 18 x 32-bit transfers from ADC_DR to CMAR; the channel map assigns // each logical channel to a slot. Each slot carries the 16-bit // sample in its low half, as the ADC data register does. const uint32_t count = read_u32(DMA1_CNDTR1); const uint32_t dest = read_u32(DMA1_CMAR1); if (count == 0 || count > 64 || dest == 0) { return; } std::array slots{}; for (unsigned ch = 0; ch < ADC_CHANNELS; ch++) { uint8_t slot = 0; if (uc_mem_read(&uc(), ADC_TASK_MAP + ch, &slot, 1) != UC_ERR_OK) { continue; } if (slot < count) { slots[slot] = analog_[ch]; } } for (uint32_t i = 0; i < count; i++) { write_u32(dest + 4U * i, slots[i]); } write_u32(DMA1_CNDTR1, 0); write_u32(DMA1_ISR, read_u32(DMA1_ISR) | DMA_CH_FLAGS); // Conversion block done: ADSTART self-clears. write_u32(ADC_CR, read_u32(ADC_CR) & ~(1U << 2)); dma_pending_ = true; } void Matrix::set_analog(unsigned channel, uint16_t raw) { if (channel < analog_.size()) { analog_[channel] = raw; } } void Matrix::apply_factory_record(const std::array &record) { if (!booted_) { throw Error{Status::state, "boot() required"}; } if (faulted_) { throw Error{Status::state, "target faulted; destroy it"}; } // The dispatcher receives the complete record: byte 0 selects the // builder and it stores record + 1 in the registry before dispatching // the five-byte payload. Keep the scratch in the library-owned stub // mapping so this helper is // safe with Target::call's own stack. constexpr uint32_t scratch = detail::kStubBase + 0x100U; if (uc_mem_write(&uc(), scratch, record.data(), record.size()) != UC_ERR_OK) { throw Error{Status::emulator, "factory record write failed"}; } CallArgs args; args.r[0] = addr::matrix_registry; args.r[1] = scratch; args.r[2] = 0; (void)call(addr::matrix_factory_builder, args); } void Matrix::set_gpio_input(unsigned port, uint16_t pins) { if (port >= 6U) { return; } write_u32(GPIO_BASE + port * 0x400U + 0x10U, pins); } void Matrix::set_gpio_pin(unsigned port, unsigned pin, bool high) { if (port >= 6U || pin >= 16U) { return; } const uint32_t addr = GPIO_BASE + port * 0x400U + 0x10U; uint32_t pins = read_u32(addr); const uint32_t mask = 1U << pin; pins = high ? pins | mask : pins & ~mask; write_u32(addr, pins); } uint16_t Matrix::analog_value(unsigned channel) { if (channel >= analog_.size()) { return 0; } return static_cast(read_u32(ADC_TASK + 0xACU + 2U * channel) & 0xFFFFU); } // ══ Main-loop stepping ═══════════════════════════════════════════════ void Matrix::simulate_prologue() { // 0x08006846: movs r1,#0 — the scheduler's `period` argument. const uint32_t zero = 0; uc_reg_write(&uc(), UC_ARM_REG_R1, &zero); } uint32_t Matrix::read_u32(uint32_t addr) { uint32_t v = 0; uc_mem_read(&uc(), addr, &v, 4); return v; } void Matrix::write_u32(uint32_t addr, uint32_t value) { uc_mem_write(&uc(), addr, &value, 4); } // ══ IRQ pump (NVIC stand-in) ═════════════════════════════════════════ void Matrix::pump_irqs() { // 1 ms SysTick: the real handler (0x08001F04) bumps the tick // counter and calls a no-op hook. try { (void)call(addr::matrix_systick, CallArgs{}); } catch (const Error &) { faulted_ = true; throw; } // DMA1 IRQ9: service the completed ADC block (TC first, then HT — // the driver arms TCIE|HTIE|TEIE). if (dma_pending_) { dma_pending_ = false; for (const uint32_t mask : {1U << 2, 1U << 1, 1U << 3}) { // TCIF, HTIF, TEIF if ((read_u32(DMA1_ISR) & mask) == 0) { continue; } CallArgs a; a.r[0] = addr::matrix_dma_handle; try { (void)call(addr::matrix_dma_isr, a); } catch (const Error &) { faulted_ = true; throw; } } } // USART1 IRQ27 while TXEIE is set: the real handler pops one byte // from the TX ring per TXE and clears TXEIE when the ring drains. for (int i = 0; i < 256; i++) { uint32_t cr1 = 0; if (uc_mem_read(&uc(), USART1_CR1, &cr1, 4) != UC_ERR_OK) { break; } if ((cr1 & 0x80U) == 0) { break; } try { (void)call(addr::matrix_usart1_isr, CallArgs{}); } catch (const Error &) { faulted_ = true; throw; } } } // ══ Boot ═════════════════════════════════════════════════════════════ void Matrix::boot() { const uint32_t sp = addr::matrix_msp; const uint32_t lr = detail::kTrampoline | 1U; uc_reg_write(&uc(), UC_ARM_REG_SP, &sp); uc_reg_write(&uc(), UC_ARM_REG_LR, &lr); // Real reset: SystemInit (RCC) → startup (.bss clear, C++ // constructors) → main → loop head. The run stops BEFORE the loop // head instruction (stop-before-prologue), so the library boundary // is exactly the main-loop boundary. const uc_err e = emu_start(addr::matrix_reset_vector | 1U, addr::matrix_main_loop); if (e != UC_ERR_OK) { faulted_ = true; std::array msg{}; std::snprintf(msg.data(), msg.size(), "matrix boot: %s", detail::uc_err_str(e)); throw Error{Status::boot, msg.data()}; } uint32_t pc = 0; uc_reg_read(uc_, UC_ARM_REG_PC, &pc); if (pc != addr::matrix_main_loop) { faulted_ = true; throw Error{Status::boot, "matrix boot did not reach the main loop"}; } simulate_prologue(); booted_ = true; } void Matrix::iterate() { if (!booted_) { throw Error{Status::state, "boot() required"}; } if (faulted_) { throw Error{Status::state, "target faulted; destroy it"}; } const uc_err e = emu_start((addr::matrix_main_loop + 2U) | 1U, addr::matrix_main_loop); if (e != UC_ERR_OK) { faulted_ = true; std::array msg{}; std::snprintf(msg.data(), msg.size(), "matrix loop iteration: %s", detail::uc_err_str(e)); throw Error{Status::fault, msg.data()}; } simulate_prologue(); pump_irqs(); counters_.iterations++; } void Matrix::run(unsigned iterations) { for (unsigned i = 0; i < iterations; i++) { iterate(); } } // ══ create ═══════════════════════════════════════════════════════════ std::unique_ptr Matrix::create(std::span firmware, UnmapPolicy unmap_policy, DiagFn diag) { if (firmware.empty()) { throw Error{Status::argument, "firmware bytes required"}; } std::unique_ptr m(new Matrix(unmap_policy, std::move(diag))); if (m->uc_ == nullptr) { throw Error{Status::emulator, "uc_open failed"}; } uc_engine *uc = m->uc_; m->hook_ctx_.counters = &m->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}, {SCB_BASE, SCB_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"}; } } // Firmware payload (skip the 64-byte DFU header); the rest of the // flash window stays 0xFF — erased state (the firmware's settings // sector at 0x0800F800..0x08010000 is absent from the image). if (firmware.size() < 64 + addr::matrix_fw_size) { throw Error{Status::unsupported, "image too small (need header+payload)"}; } std::vector image(FLASH_MAP, std::byte{0xFF}); std::memcpy(image.data(), firmware.data() + 64, addr::matrix_fw_size); if (uc_mem_write(uc, addr::matrix_fw_load, image.data(), image.size()) != UC_ERR_OK) { throw Error{Status::emulator, "firmware write failed"}; } m->loginfo("firmware loaded"); // Hooks. The peripheral READ hook models the status bits the // firmware polls; the GPIO WRITE hook keeps BSRR/BRR→ODR truthful // (the RTS/CTS loopback and anything else reading ODR); the TDR // hook captures the link's transmitted bytes. The unmapped hook // keeps stray bus accesses benign per the target's UnmapPolicy. if (!mem_hook_add<&Matrix::periph_read_hook>(uc, UC_HOOK_MEM_READ, PERIPH_BASE, PERIPH_BASE + PERIPH_SIZE - 1, m.get()) || !mem_hook_add<&Matrix::gpio_write_hook>(uc, UC_HOOK_MEM_WRITE, GPIO_BASE, GPIO_BASE + GPIO_SPAN - 1, m.get()) || !mem_hook_add<&Matrix::adc_write_hook>(uc, UC_HOOK_MEM_WRITE, ADC_CR, ADC_CR + 3, m.get()) || !mem_hook_add<&Matrix::tdr_write_hook>(uc, UC_HOOK_MEM_WRITE, USART1_TDR, USART1_TDR + 1, m.get()) || !mem_hook_add(uc, UC_HOOK_MEM_UNMAPPED, 1, 0, &m->hook_ctx_)) { throw Error{Status::emulator, "uc_hook_add failed"}; } // USART idle line state: TDR empty (TXE) and transmission complete // (TC) on both links, as on real hardware before the first byte. for (const uint32_t base : {0x40013800U, 0x40004400U}) { uint32_t isr = 0; uc_mem_read(uc, base + 0x1CU, &isr, 4); isr |= 0xC0U; uc_mem_write(uc, base + 0x1CU, &isr, 4); } m->boot(); m->loginfo("matrix booted"); return m; } } // namespace mf