Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500// 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 <unicorn/unicorn.h>
#include <array>#include <cstdint>#include <cstdio>#include <cstring>#include <memory>#include <span>#include <vector>
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 0xFFconstexpr 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 0x48000000constexpr 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-bitsconstexpr uint32_t RCC_CFGR = 0x40021004U; // SWS follows SWconstexpr uint32_t RCC_BDCR = 0x40021020U; // LSERDY follows LSEONconstexpr uint32_t RCC_CSR = 0x40021024U; // LSIRDY follows LSIONconstexpr uint32_t RCC_CR2 = 0x40021034U; // HSI14RDY/HSI48RDY follow on-bitsconstexpr 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|EOSEQconstexpr 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 = TXEIEconstexpr uint32_t USART1_ISR = 0x4001381CU; // bit 7 = TXE, bit 6 = TCconstexpr 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/TEIFconstexpr uint32_t DMA1_IFCR = 0x40020004U;constexpr uint32_t DMA1_CCR1 = 0x40020008U; // bit 0 = ENconstexpr uint32_t DMA1_CNDTR1 = 0x4002000CU;constexpr uint32_t DMA1_CMAR1 = 0x40020014U;constexpr uint32_t ADC_TASK = 0x200001CCU; // channel enable mask/map/filterconstexpr uint32_t ADC_TASK_MAP = ADC_TASK + 0xFCU; // map[ch] -> DMA slotconstexpr uint32_t ADC_TASK_BUF = ADC_TASK + 0x14U; // DMA destinationconstexpr 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>(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<uint32_t>(address - GPIO_BASE) / 0x400U; const uint32_t base = GPIO_BASE + port * 0x400U; const auto v = static_cast<uint32_t>(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<uint8_t>(value & 0xFF); ioc_uart_.push(std::span<const uint8_t>{&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<uint32_t>(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<uint16_t, 64> 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<uint8_t, 6> &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<uint16_t>(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<char, 96> 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<char, 96> 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> Matrix::create(std::span<const std::byte> firmware, UnmapPolicy unmap_policy, DiagFn diag) { if (firmware.empty()) { throw Error{Status::argument, "firmware bytes required"}; } std::unique_ptr<Matrix> 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<Region, 5> 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<std::byte> 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<detail::unmapped_hook>(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