Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410// kbd.cpp — Kbd: the key-scan chip (Cortex-M0).//// - create() boots the firmware through the four scan-group wait// points (DMA scan-completion simulated at the real stop points,// exactly like the reference boot()) and stops at the first// main-loop idle spin.// - key_event() drives a touch through the REAL firmware path:// scan feed → scan consumer → channel ingest → debounce →// keys_periodic_handler → frame build → TX FIFO → the real FIFO// drainer → USART1 ISR drain (TDR writes captured → ioc_uart_).
#include "targets/kbd.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 {
constexpr uint32_t FLASH_BASE = 0x08000000U;constexpr uint32_t FLASH_MAP = 0x10000U; // mapped window; payload 0x68F0, rest 0xFFconstexpr uint32_t RAM_BASE = 0x20000000U;constexpr uint32_t RAM_SIZE = 0x4000U; // 16 KiBconstexpr 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;
// Return points of the four scan-group start wait loops in// aftertouch_init (`bl scan_group_start; cmp r0,#0; bne <loop>`): the// boot stops at each cmp so the DMA completion can be simulated// OUTSIDE the emulation (nested emu_start would poison the outer run).constexpr uint32_t kScanRet[4] = {0x08004B64U, 0x08004B76U, 0x08004B8AU, 0x08004B9EU};
constexpr uint32_t MAIN_ENTRY = 0x08005D08U;
// USART1 = the physical IOC wire (event frames to main, TDR captured);// USART2 is the RX-link side (its TDR writes are ignored, as in the// reference emulator). L4-style registers: CR1=+0x00, ISR=+0x1C.constexpr uint32_t USART1_CR1 = 0x40013800U; // bit 7 = TXEIEconstexpr uint32_t USART1_ISR = 0x4001381CU; // bit 7 = TXEconstexpr uint32_t USART1_TDR = 0x40013828U;constexpr uint32_t USART2_ISR = 0x4000441CU;constexpr uint32_t UART1_ISR_FN = 0x08002444U; // IRQ27: ring → TDR drain
constexpr uint32_t DMA1_ISR = 0x40020000U;constexpr uint32_t DMA1_IFCR = 0x40020004U; // write-1-to-clearconstexpr uint32_t SCAN_IRQ9 = 0x080002BCU; // key_matrix_state_machine ×3constexpr uint32_t SCAN_IRQ10 = 0x080002CCU;constexpr uint32_t SCAN_IRQ11 = 0x080002E4U;
constexpr uint32_t TICK_READ_FN = 0x08000B0CU; // tick_get
constexpr uint32_t SCAN_CONSUMER = 0x08004C10U; // scan_consumer_wrapperconstexpr uint32_t SCAN_IDX = 0x20000518U; // rotating key idx (u16)constexpr uint32_t SCAN_PHASE = 0x2000051AU; // 0 = keys, 1 = stripconstexpr uint32_t SCAN_BUF = 0x20001126U; // 3 samples: 0, spread, 2*spreadconstexpr uint32_t SCAN_COMPLETE = 0x2222U; // 2<<ch, ch ∈ {12, 4, 8, 0}constexpr uint32_t FIFO_OBJ = 0x20000108U; // uart1 link objectconstexpr uint32_t FIFO_RD_IDX = 0x20000154U; // +0x4C: read idx, +0x50: write idxconstexpr uint32_t FIFO_DRAIN_FN = 0x08003FCCU; // case-2: dequeue → 0xAA frame → USART1
} // namespace
// ══ Construction ═════════════════════════════════════════════════════
Kbd::Kbd(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; }}
// ══ Internal hooks ═══════════════════════════════════════════════════
void Kbd::code_hook(uint64_t address, uint32_t /*size*/) { if (address == MAIN_ENTRY) { in_main_ = true; // from here on, the idle spin means "boot done" return; } if (address == addr::kbd_wait_for_irq && in_main_) { stop_ = Stop::wait_irq; uc_emu_stop(&uc()); return; } for (const uint32_t rp : kScanRet) { if (address == rp) { stop_ = Stop::scan_ret; uc_emu_stop(&uc()); return; } }}
void Kbd::tick_hook() { ++tick_; // time always passes: the firmware's 3-tick timeouts expire uc_mem_write(&uc(), addr::kbd_tick_var, &tick_, 4);}
void Kbd::tdr_write_hook(uc_mem_type /*type*/, uint64_t address, int /*size*/, int64_t value) { if (address == USART1_TDR) { const auto b = byte_le(static_cast<uint32_t>(value), 0); ioc_uart_.push(std::span<const uint8_t>{&b, 1}, tx_dropped_); }}
void Kbd::dma_ifcr_hook(uc_mem_type /*type*/, uint64_t /*address*/, int /*size*/, int64_t value) { // DMA1_IFCR is write-1-to-clear — plain-RAM peripherals don't // self-clear, so model the ISR side effect explicitly. uint32_t isr = 0; uc_mem_read(&uc(), DMA1_ISR, &isr, 4); isr &= ~(static_cast<uint32_t>(value) & 0xFFFFU); uc_mem_write(&uc(), DMA1_ISR, &isr, 4);}
// ══ Boot ═════════════════════════════════════════════════════════════
void Kbd::boot() { uint32_t pc = addr::kbd_reset_vector; for (;;) { stop_ = Stop::none; const uc_err e = emu_start(pc | 1U, 0); pc = 0; uc_reg_read(uc_, UC_ARM_REG_PC, &pc); if (e != UC_ERR_OK) { faulted_ = true; std::array<char, 96> msg{}; std::snprintf(msg.data(), msg.size(), "kbd boot: %s", detail::uc_err_str(e)); throw Error{Status::boot, msg.data()}; } if (stop_ == Stop::scan_ret) { simulate_scan_complete(); // Resume AFTER the hooked cmp (flags untouched — Target::call // restored them): the bne re-evaluates the previous compare // result, exactly like the reference boot(). pc += 2; } else if (stop_ == Stop::wait_irq) { return; } else { faulted_ = true; throw Error{Status::boot, "kbd boot stopped without reaching the idle loop"}; } }}
// Set the DMA scan-complete flags and run the real IRQ state machines// (reference simulate_scan_complete).void Kbd::simulate_scan_complete() { uint32_t isr = 0; uc_mem_read(uc_, DMA1_ISR, &isr, 4); isr |= SCAN_COMPLETE; uc_mem_write(uc_, DMA1_ISR, &isr, 4); const CallArgs none; // ISRs take no arguments for (const uint32_t fn : {SCAN_IRQ9, SCAN_IRQ10, SCAN_IRQ11}) { try { (void)call(fn, none); } catch (const Error &) { faulted_ = true; throw; } } uc_mem_read(uc_, DMA1_ISR, &isr, 4); if ((isr & SCAN_COMPLETE) != 0) { // leave nothing pending isr &= ~SCAN_COMPLETE; uc_mem_write(uc_, DMA1_ISR, &isr, 4); }}
// ══ Key events ═══════════════════════════════════════════════════════
// Call the real USART1 ISR until the firmware clears TXEIE (the TX ring// is drained); the frames leave through the TDR write hook.void Kbd::drain_uart1() { for (int i = 0; i < 512; i++) { uint32_t cr1 = 0; uc_mem_read(uc_, USART1_CR1, &cr1, 4); if ((cr1 & 0x80U) == 0) { return; } const CallArgs none; try { (void)call(UART1_ISR_FN, none); } catch (const Error &) { faulted_ = true; throw; } }}
void Kbd::key_event(unsigned key_index, bool pressed) { if (faulted_) { throw Error{Status::state, "kbd faulted; destroy it"}; } if (key_index >= 8) { throw Error{Status::argument, "key_index must be 0..7"}; } uc_engine *const uc = uc_;
// Per-channel block (stride 0x2E, value field at +0x10). const uint32_t ch = addr::kbd_keys_driver + key_index * 0x2EU + 0x10U;
// Touch thresholds (the real firmware calibrates these from the // scan baseline / EEPROM): delta >= t2 → touched (state 2). uint16_t t2 = 0; uc_mem_read(uc, ch + 2, &t2, 2); if (t2 == 0) { const uint16_t t2v = 0x0030; const uint16_t t4v = 0x0018; const uint16_t tcv = 0x0008; uc_mem_write(uc, ch + 2, &t2v, 2); uc_mem_write(uc, ch + 4, &t4v, 2); uc_mem_write(uc, ch + 0xC, &tcv, 2); }
// Make the scan consumer feed this key next (phase 0 = key channels). const uint16_t idx = static_cast<uint16_t>(key_index); const uint16_t phase = 0; uc_mem_write(uc, SCAN_IDX, &idx, 2); uc_mem_write(uc, SCAN_PHASE, &phase, 2);
// Feed the 3-sample scan block (whose spread encodes the touch) and // run the real scan consumer once per round. auto const feed = [&](uint16_t spread, int rounds) { for (int i = 0; i < rounds; i++) { const uint16_t lo = 0; const uint16_t mid = spread; const uint16_t hi = static_cast<uint16_t>(spread * 2U); uc_mem_write(uc, SCAN_BUF, &lo, 2); uc_mem_write(uc, SCAN_BUF + 2, &mid, 2); uc_mem_write(uc, SCAN_BUF + 4, &hi, 2); CallArgs a; a.r[0] = 0x20000508U; // consumer object try { (void)call(SCAN_CONSUMER, a); } catch (const Error &) { faulted_ = true; throw; } } };
// Channel still initialising: feed the untouched baseline and let // the init timeout (1000 ticks) complete the calibration. uint8_t init = 0; uc_mem_read(uc, ch + 0xF, &init, 1); if (init != 0) { feed(0x0020, 80); for (int i = 0; i < 200; i++) { CallArgs a; a.r[0] = addr::kbd_scheduler; try { (void)call(addr::kbd_event_dispatch, a); } catch (const Error &) { faulted_ = true; throw; } } }
// The touch: jump the spread (each key is sampled once per 16 // consumer calls). const uint16_t spread = pressed ? 0x0400U : 0x0020U; feed(spread, 16);
// The scheduler runs the debounce/periodic handlers: run the real // dispatch loop (reference repeats=24). for (int i = 0; i < 24; i++) { CallArgs a; a.r[0] = addr::kbd_scheduler; try { (void)call(addr::kbd_event_dispatch, a); } catch (const Error &) { faulted_ = true; throw; } }
// Drain the TX FIFO through the real dequeue path (the link state // machine's case-2 action): one call per queued frame; the drainer // prepends the 0xAA marker and appends the checksum on the way to // the USART1 TDR. for (int i = 0; i < 8; i++) { uint32_t rd = 0; uint32_t wr = 0; uc_mem_read(uc, FIFO_RD_IDX, &rd, 4); uc_mem_read(uc, FIFO_RD_IDX + 4, &wr, 4); if (rd == wr) { break; } CallArgs a; a.r[0] = FIFO_OBJ; try { (void)call(FIFO_DRAIN_FN, a); } catch (const Error &) { faulted_ = true; throw; } } drain_uart1();}
// ══ create ═══════════════════════════════════════════════════════════
std::unique_ptr<Kbd> Kbd::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<Kbd> k(new Kbd(unmap_policy, std::move(diag))); if (k->uc_ == nullptr) { throw Error{Status::emulator, "uc_open failed"}; } uc_engine *uc = k->uc_; k->hook_ctx_.counters = &k->counters_;
// Machine layout (reference: main.py __init__). The stub region is // Target::call's trampoline/stack area (above the 16 KiB RAM). 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 // mapped window stays 0xFF — erased-flash state, as in the reference. if (firmware.size() < 64 + addr::kbd_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::kbd_fw_size); if (uc_mem_write(uc, addr::kbd_fw_load, image.data(), image.size()) != UC_ERR_OK) { throw Error{Status::emulator, "firmware write failed"}; } k->loginfo("firmware loaded");
// Hooks. All registrations checked — a silently missing hook would // wedge the boot at the first scan-group wait loop. The tick hook // sits on BOTH instructions of tick_get like the reference's // [tick_get, tick_get+2] range (each bumps the tick — the // calibration timeouts depend on this exact rate). auto const add_code = [&](uint64_t at) { (void)code_hook_add<&Kbd::code_hook>(uc, at, k.get()); }; (void)code_hook_add<&Kbd::tick_hook>(uc, TICK_READ_FN, k.get()); (void)code_hook_add<&Kbd::tick_hook>(uc, TICK_READ_FN + 2, k.get()); add_code(MAIN_ENTRY); add_code(addr::kbd_wait_for_irq); for (const uint32_t rp : kScanRet) { add_code(rp); } (void)mem_hook_add<&Kbd::tdr_write_hook>(uc, UC_HOOK_MEM_WRITE, USART1_TDR, USART1_TDR, k.get()); (void)mem_hook_add<&Kbd::dma_ifcr_hook>(uc, UC_HOOK_MEM_WRITE, DMA1_IFCR, DMA1_IFCR, k.get()); (void)mem_hook_add<detail::unmapped_hook>(uc, UC_HOOK_MEM_UNMAPPED, 1, 0, &k->hook_ctx_);
// USART ISR.TXE = 1 on both links (TDR empty — real hardware state): // the firmware sets CR1.TXEIE after queueing and the real ISR drains. for (const uint32_t isr : {USART1_ISR, USART2_ISR}) { uint32_t v = 0; uc_mem_read(uc, isr, &v, 4); v |= 0x80U; uc_mem_write(uc, isr, &v, 4); }
// Stub SP/LR for the top-level boot runs. const uint32_t sp = addr::kbd_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);
k->boot(); k->loginfo("kbd booted"); return k;}
} // namespace mf