diff --git a/core/Makefile b/core/Makefile index 3309a356..db21e6aa 100644 --- a/core/Makefile +++ b/core/Makefile @@ -10,6 +10,10 @@ CFLAGS = -Wall -Wextra -fPIC -O3 -std=gnu11 -static # Add debugging support, with zdis disassembler #CFLAGS += -DDEBUG_SUPPORT +# Print what programs write to the debug console (dbg_printf) through gui_console_printf / +# gui_console_err_printf without debugging support too (cemu-headless, the autotester) +#CFLAGS += -DDEBUG_CONSOLE_SUPPORT + # Add these flags if your compiler supports it #CFLAGS += -Wstack-protector -fstack-protector-strong --param=ssp-buffer-size=1 -fsanitize=address,bounds -fsanitize-undefined-trap-on-error diff --git a/core/flash.c b/core/flash.c index be5f8842..7a216ec3 100644 --- a/core/flash.c +++ b/core/flash.c @@ -52,6 +52,10 @@ void flash_flush_cache(void) { } } +uint32_t *flash_line_profile; +void (*flash_line_hook)(uint32_t line); +uint32_t flash_fixed_cycles; + uint32_t flash_touch_cache(uint32_t addr) { if (unlikely(++cpu.flashTotalAccesses == 0)) { cpu.flashTotalAccesses = 0x80000000; @@ -62,6 +66,12 @@ uint32_t flash_touch_cache(uint32_t addr) { return 2; } else { flash.lastCacheLine = line; + if (unlikely(flash_line_profile != NULL)) { + flash_line_profile[2 * (line & (FLASH_PROFILE_LINES - 1))]++; + } + if (unlikely(flash_line_hook != NULL)) { + flash_line_hook(line); + } flash_cache_set_t* set = &flash.cacheTags[line & (FLASH_CACHE_SETS - 1)]; uint16_t tag = (uint16_t)(line >> FLASH_CACHE_SET_BITS); if (likely(set->mru == tag)) { @@ -78,6 +88,9 @@ uint32_t flash_touch_cache(uint32_t addr) { set->lru = set->mru; set->mru = tag; cpu.flashCacheMisses++; + if (unlikely(flash_line_profile != NULL)) { + flash_line_profile[2 * (line & (FLASH_PROFILE_LINES - 1)) + 1]++; + } /* Supposedly this takes from 195-201 cycles, but typically seems to be 196-197 */ cpu.flashDelayCycles += 195; return 197; diff --git a/core/flash.h b/core/flash.h index 8dd04a19..669eb7c4 100644 --- a/core/flash.h +++ b/core/flash.h @@ -40,6 +40,20 @@ typedef struct flash_state { extern flash_state_t flash; +/* Optional profiling of serial flash reads for front ends (off when NULL). A line is + * FLASH_CACHE_LINE_SIZE bytes; it is entered when a read follows a read of another line. + * flash_line_profile: 2 counters per line (FLASH_PROFILE_LINES lines), the times it was + * entered then its cache misses. flash_line_hook: called with each line entered, in order + * (for cache simulations and code layout studies). */ +#define FLASH_PROFILE_LINES (0x1000000 >> FLASH_CACHE_LINE_BITS) +extern uint32_t *flash_line_profile; +extern void (*flash_line_hook)(uint32_t line); + +/* Nonzero: every serial flash read costs this many cycles and the cache is not modeled. + * 10 is what a calculator made before revision M spends per byte (parallel flash with the + * OS's 9 wait states), to compare timings with a revision M ROM. */ +extern uint32_t flash_fixed_cycles; + void flash_flush_cache(void); uint32_t flash_touch_cache(uint32_t addr); diff --git a/core/mem.c b/core/mem.c index 1119d877..c7361912 100644 --- a/core/mem.c +++ b/core/mem.c @@ -598,7 +598,7 @@ static uint8_t mem_read_flash_parallel(uint32_t addr) { } static uint8_t mem_read_flash_serial(uint32_t addr) { - cpu.cycles += flash_touch_cache(addr); + cpu.cycles += unlikely(flash_fixed_cycles) ? flash_fixed_cycles : flash_touch_cache(addr); return mem.flash.block[addr & flash.mask]; } @@ -812,6 +812,24 @@ void mem_write_cpu(uint32_t addr, uint8_t value) { /* MMIO <-> Advanced Perphrial Bus */ case 0xE: case 0xF: +#if defined(DEBUG_CONSOLE_SUPPORT) || defined(DEBUG_SUPPORT) + if (addr >= DBGOUT_PORT_RANGE && addr < DBGEXT_PORT) { + static char console_buffer[2][SIZEOF_DBG_BUFFER]; + static unsigned console_pos[2]; + const int err = addr >= DBGERR_PORT_RANGE; + console_buffer[err][console_pos[err]] = (char)value; + console_pos[err] = (console_pos[err] + 1) % (SIZEOF_DBG_BUFFER - 1); + if (!value) { + if (err) { + gui_console_err_printf("%s", console_buffer[err]); + } else { + gui_console_printf("%s", console_buffer[err]); + } + console_pos[err] = 0; + } + break; + } +#endif #ifdef DEBUG_SUPPORT if (debug_get_flags() & DBG_SOFT_COMMANDS) { if (addr >= DBG_PORT_RANGE) { diff --git a/tests/autotester/headless_cli.cpp b/tests/autotester/headless_cli.cpp index 4e286e4f..e587dd17 100644 --- a/tests/autotester/headless_cli.cpp +++ b/tests/autotester/headless_cli.cpp @@ -5,6 +5,7 @@ * keep one core instance alive without pulling in Qt or another IPC library. */ +#include #include #include #include @@ -26,6 +27,7 @@ namespace cemucore { extern "C" { + #include "../../core/flash.h" #include "../../core/usb/usb.h" void gui_console_clear() {} @@ -54,6 +56,29 @@ namespace cemucore namespace { +std::FILE *line_trace; +std::vector line_trace_buffer; + +void lineTraceHook(uint32_t line) +{ + line_trace_buffer.push_back(line); + if (line_trace_buffer.size() == 65536) { + std::fwrite(line_trace_buffer.data(), sizeof(uint32_t), line_trace_buffer.size(), line_trace); + line_trace_buffer.clear(); + } +} + +void lineTraceStop() +{ + if (line_trace) { + std::fwrite(line_trace_buffer.data(), sizeof(uint32_t), line_trace_buffer.size(), line_trace); + std::fclose(line_trace); + line_trace = nullptr; + } + line_trace_buffer.clear(); + cemucore::flash_line_hook = nullptr; +} + struct options_t { std::string rom; std::string image; @@ -75,6 +100,17 @@ bool parseUnsigned(const std::string& text, uint32_t& result) return true; } +bool parseAddress(const std::string& text, uint32_t& result) +{ + char *end; + const unsigned long value = std::strtoul(text.c_str(), &end, 16); + if (!std::isxdigit(static_cast(text[0])) || *end || value > 0xFFFFFFu) { + return false; + } + result = static_cast(value); + return true; +} + bool fileExists(const std::string& path) { return !path.empty() && std::ifstream(path, std::ios::binary).good(); @@ -252,7 +288,11 @@ bool runCommand(const std::string& line) "key [hold-ms]; keys ; " "screenshot ; screen-hash; save-state ; " "send-file [ram|archive|auto] ; " - "usb ; reset; status; quit"); + "usb ; reset; status; " + "peek [count]; poke ; " + "keydown ; keyup ; regs; stats; lcd-dma <0|1>; " + "line-profile >; line-trace |off>; " + "flash-cycles ; quit"); return true; } if (command == "run") { @@ -410,6 +450,155 @@ bool runCommand(const std::string& line) return true; } + if (command == "peek") { + std::string address_text; + std::string count_text; + uint32_t address; + uint32_t count = 1; + if (!(input >> address_text) || !parseAddress(address_text, address) || + ((input >> count_text) && (!parseUnsigned(count_text, count) || count == 0 || count > 4096))) { + respond("ERR usage: peek [count 1-4096]"); + } else { + std::string bytes; + char hex[3]; + for (uint32_t i = 0; i < count; ++i) { + std::snprintf(hex, sizeof hex, "%02X", cemucore::mem_peek_byte((address + i) & 0xFFFFFFu)); + bytes += hex; + } + respond("OK peek " + bytes); + } + return true; + } + if (command == "poke") { + std::string address_text; + std::string bytes; + uint32_t address; + if (!(input >> address_text >> bytes) || !parseAddress(address_text, address) || bytes.size() % 2 || + bytes.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos) { + respond("ERR usage: poke "); + } else { + for (size_t i = 0; i < bytes.size(); i += 2) { + cemucore::mem_poke_byte((address + static_cast(i / 2)) & 0xFFFFFFu, + static_cast(std::strtoul(bytes.substr(i, 2).c_str(), nullptr, 16))); + } + respond("OK poke " + std::to_string(bytes.size() / 2)); + } + return true; + } + if (command == "keydown" || command == "keyup") { + std::string name; + autotester::key_coord_t coord{}; + input >> name; + if (!autotester::keyCoordForName(name, coord)) { + respond("ERR unknown key " + name); + } else { + cemucore::emu_keypad_event(coord.y, coord.x, command == "keydown"); + respond("OK " + command + " " + name); + } + return true; + } + if (command == "regs") { + const auto& r = cemucore::cpu.registers; + char text[192]; + std::snprintf(text, sizeof text, + "OK regs pc=%06X sp=%06X af=%04X bc=%06X de=%06X hl=%06X ix=%06X iy=%06X adl=%d halted=%d", + static_cast(r.PC), static_cast(r.SPL), static_cast(r.AF), + static_cast(r.BC), static_cast(r.DE), static_cast(r.HL), + static_cast(r.IX), static_cast(r.IY), + static_cast(cemucore::cpu.ADL), static_cast(cemucore::cpu.halted)); + respond(text); + return true; + } + if (command == "stats") { + char text[192]; + std::snprintf(text, sizeof text, + "OK stats cycles=%llu halted=%llu dma=%llu flash-reads=%lu flash-misses=%lu flash-delay=%lld", + static_cast(cemucore::cpu.baseCycles + cemucore::cpu.cycles), + static_cast(cemucore::cpu.haltCycles), + static_cast(cemucore::cpu.dmaCycles), + static_cast(cemucore::cpu.flashTotalAccesses), + static_cast(cemucore::cpu.flashCacheMisses), + static_cast(cemucore::cpu.flashDelayCycles)); + respond(text); + return true; + } + if (command == "lcd-dma") { + std::string value; + if (!(input >> value) || (value != "0" && value != "1")) { + respond("ERR usage: lcd-dma <0|1>"); + } else { + cemucore::emu_set_lcd_dma(value == "1"); + respond("OK lcd-dma " + value); + } + return true; + } + + if (command == "line-profile") { + std::string what; + input >> what; + const size_t counters = 2 * FLASH_PROFILE_LINES; + if (what == "on") { + if (!cemucore::flash_line_profile) { + cemucore::flash_line_profile = static_cast(std::calloc(counters, sizeof(uint32_t))); + } else { + std::memset(cemucore::flash_line_profile, 0, counters * sizeof(uint32_t)); + } + respond(cemucore::flash_line_profile ? "OK line-profile on" : "ERR out of memory"); + } else if (what == "save" && cemucore::flash_line_profile) { + std::string path; + std::getline(input >> std::ws, path); + std::FILE *file = path.empty() ? nullptr : std::fopen(path.c_str(), "wb"); + if (!file) { + respond("ERR cannot write " + path); + } else { + const bool ok = std::fwrite(cemucore::flash_line_profile, sizeof(uint32_t), counters, file) == counters; + std::fclose(file); + respond(ok ? "OK line-profile save" : "ERR cannot write " + path); + } + } else if (what == "off") { + std::free(cemucore::flash_line_profile); + cemucore::flash_line_profile = nullptr; + respond("OK line-profile off"); + } else { + respond("ERR usage: line-profile > (save needs on)"); + } + return true; + } + if (command == "line-trace") { + std::string what; + input >> what; + if (what == "on") { + std::string path; + std::getline(input >> std::ws, path); + std::FILE *file = path.empty() ? nullptr : std::fopen(path.c_str(), "wb"); + if (!file) { + respond("ERR cannot write " + path); + } else { + lineTraceStop(); + line_trace = file; + cemucore::flash_line_hook = lineTraceHook; + respond("OK line-trace on"); + } + } else if (what == "off") { + lineTraceStop(); + respond("OK line-trace off"); + } else { + respond("ERR usage: line-trace |off>"); + } + return true; + } + if (command == "flash-cycles") { + std::string value; + uint32_t cycles; + if (!(input >> value) || !parseUnsigned(value, cycles) || cycles > 1000) { + respond("ERR usage: flash-cycles (0: the revision M cache)"); + } else { + cemucore::flash_fixed_cycles = cycles; + respond("OK flash-cycles " + value); + } + return true; + } + respond("ERR unknown command " + command); return true; } @@ -442,6 +631,7 @@ int main(int argc, char **argv) std::string line; while (std::getline(std::cin, line) && runCommand(line)) {} + lineTraceStop(); cemucore::emu_exit(); cemucore::asic_free(); return EXIT_SUCCESS; diff --git a/tests/autotester/headless_protocol.md b/tests/autotester/headless_protocol.md index 2bc370b0..fe26e16e 100644 --- a/tests/autotester/headless_protocol.md +++ b/tests/autotester/headless_protocol.md @@ -24,9 +24,40 @@ Supported commands: emulated host controller, or disconnects the current USB peer. - `reset` resets the emulated calculator. - `status` reports the device, ASIC revision, Python flag, and run rate. +- `peek [count]` returns `count` bytes (1 to 4096, default 1) read + from the address space without side effects, as hex: `OK peek 3F00C9`. +- `poke ` writes bytes (flash included, ignoring its + protection), for example `poke D0008E 01`. +- `keydown ` and `keyup ` press and release a key separately, without + running the emulator, to inspect the calculator while a key is held. +- `regs` reports the CPU registers (PC, SPL, AF, BC, DE, HL, IX, IY), ADL and + whether the CPU is halted. With a stack of IX frames, the caller's IX is at + (IX) and the return address at (IX+3): enough to see where a program hangs. +- `stats` reports the cycle counters since power-on: total and halted cycles, + cycles taken by the LCD's DMA, serial flash reads and cache misses (revision M + and later: an 8 KB cache, about 197 cycles per miss) and the flash wait cycles. +- `lcd-dma <0|1>` emulates the LCD's DMA, which takes RAM cycles from the CPU + while the screen refreshes (off by default here), or turns it off. +- `line-profile on` counts, for each 32-byte line of flash, how many times the + CPU entered it (a read after a read of another line) and how many of those + missed the serial flash cache; `line-profile save ` writes the counters + (2 per line: entries then misses, 32-bit, host byte order, 524288 lines for + the 16 MB address space); `line-profile off` stops counting. +- `line-trace on ` writes every line entered, in order, as 32-bit line + numbers (address / 32, host byte order), for cache simulations and code layout + studies; `line-trace off` closes the file. +- `flash-cycles ` makes every serial flash read cost n cycles without the + cache; 10 is close to a calculator made before revision M (parallel flash with + the OS's 9 wait states), to compare a program's speed on older hardware with a + revision M ROM. `flash-cycles 0` restores the cache. - `help` lists commands. - `quit` exits cleanly. +If the core was built with `-DDEBUG_CONSOLE_SUPPORT` or `-DDEBUG_SUPPORT` (for example `make +CPPFLAGS=-DDEBUG_CONSOLE_SUPPORT` in `core/`), what the calculator's programs print +to the debug console (the toolchain's `dbg_printf`) is written to standard error, as +the core's diagnostics are. + For example: ```text