diff --git a/src/modules/arm_debug/arm_debug.c b/src/modules/arm_debug/arm_debug.c index bd3d40a..a336893 100644 --- a/src/modules/arm_debug/arm_debug.c +++ b/src/modules/arm_debug/arm_debug.c @@ -8,9 +8,13 @@ /* * ARM7TDMI debug over JTAG (EmbeddedICE), built on the bscan_* TAP * primitives. Incremental bring-up: - * - done: EmbeddedICE register access, halt (force DBGRQ) / resume - * - todo: memory read/write (debug-speed instruction injection) and - * the arm_flash backend. + * - done: EmbeddedICE register access; halt (force DBGRQ, then debug + * entry = DBGACK|INTDIS) / resume (RESTART); Thumb->ARM switch; + * instruction-injection register read/write + system-speed LDM. + * - WIP: the chain-1 access (c1_xfer) is not yet cycle-exact (one + * debug clock per access), so memory reads can be misaligned. See + * the arm7-debug-dclk-timing note. + * - todo: cycle-exact c1_xfer, memory write, the arm_flash backend. */ /* ARM7TDMI public JTAG instructions (IR length 4). */ @@ -19,12 +23,52 @@ #define IR_INTEST 0xC #define IR_RESTART 0x4 -/* EmbeddedICE scan chain (#2) and register addresses. */ -#define EICE_SCANCHAIN 2 +/* Scan chains: #1 = debug (instruction/data bus), #2 = EmbeddedICE. */ +#define SC_DEBUG 1 +#define SC_EICE 2 + +/* EmbeddedICE register addresses. */ #define EICE_DBG_CTRL 0x00 #define EICE_DBG_STATUS 0x01 -#define DBG_STATUS_DBGACK (1u << 0) +/* EmbeddedICE Debug Control register bits (write). */ +#define DBG_CTRL_DBGACK (1u << 0) /* force DBGACK */ +#define DBG_CTRL_DBGRQ (1u << 1) /* request debug entry */ +#define DBG_CTRL_INTDIS (1u << 2) /* disable interrupts in debug */ + +/* EmbeddedICE Debug Status register bits (read). */ +#define DBG_STATUS_DBGACK (1u << 0) +#define DBG_STATUS_SYSCOMP (1u << 3) +#define DBG_STATUS_ITBIT (1u << 4) /* core was in Thumb state */ + +/* ARMv4 opcodes used for register/memory access via instruction + * injection (see ARM7TDMI TRM, debug chapter; mirrors OpenOCD). */ +#define ARM_NOP 0xe1a08008u /* mov r8, r8 */ +#define ARM_STMIA(rn, list, w) (0xe8800000u | ((unsigned)(w) << 21) | ((unsigned)(rn) << 16) | (unsigned)(list)) +#define ARM_LDMIA(rn, list, w) (0xe8900000u | ((unsigned)(w) << 21) | ((unsigned)(rn) << 16) | (unsigned)(list)) + +/* Thumb opcodes (16-bit, duplicated into both halfwords as the debug + * data bus presents them) used only to switch a Thumb-state core to ARM + * state on debug entry. */ +#define THUMB_DUP(op) ((unsigned)(op) | ((unsigned)(op) << 16)) +#define ARM_T_NOP THUMB_DUP(0x46c0) /* mov r8, r8 */ +#define ARM_T_STR(rd, rn) THUMB_DUP(0x6000 | (rd) | ((rn) << 3)) +#define ARM_T_MOV(rd, rm) THUMB_DUP(0x4600 | ((rd) & 0x7) | (((rd) & 0x8) << 4) | \ + (((rm) & 0x7) << 3) | (((rm) & 0x8) << 3)) +#define ARM_T_LDR_PCREL(rd) THUMB_DUP(0x4800 | ((rd) << 8)) +#define ARM_T_BX(rm) THUMB_DUP(0x4700 | ((rm) << 3)) + +/* Reverse the 32 bits of a word. Scan chain 1 shifts instructions and + * data with the bit order flipped (TRM); match OpenOCD's flip_u32. */ +static uint32_t flip32(uint32_t v) +{ + v = ((v & 0xFFFF0000u) >> 16) | ((v & 0x0000FFFFu) << 16); + v = ((v & 0xFF00FF00u) >> 8) | ((v & 0x00FF00FFu) << 8); + v = ((v & 0xF0F0F0F0u) >> 4) | ((v & 0x0F0F0F0Fu) << 4); + v = ((v & 0xCCCCCCCCu) >> 2) | ((v & 0x33333333u) << 2); + v = ((v & 0xAAAAAAAAu) >> 1) | ((v & 0x55555555u) << 1); + return v; +} /* One EmbeddedICE scan-chain-2 access: 38 bits LSB-first = * data[0..31] | address[32..36] | read/write[37] (1 = write). On a read, @@ -53,16 +97,22 @@ static int eice_scan(jtag_core *jc, int addr, int rw, uint32_t data, uint32_t *o return 0; } -/* Select the EmbeddedICE scan chain (#2) and enter INTEST. */ -static int eice_select(jtag_core *jc) +/* Select a scan chain via SCAN_N (4-bit register) and enter INTEST so + * subsequent DR shifts hit that chain. */ +static int chain_select(jtag_core *jc, int chain) { - uint8_t sc = EICE_SCANCHAIN; + uint8_t sc = (uint8_t)chain; if (bscan_set_ir(jc, IR_SCAN_N, ARM7_IR_LEN) < 0) return -1; if (bscan_shift_dr(jc, &sc, NULL, ARM7_IR_LEN) < 0) return -1; if (bscan_set_ir(jc, IR_INTEST, ARM7_IR_LEN) < 0) return -1; return 0; } +static int eice_select(jtag_core *jc) +{ + return chain_select(jc, SC_EICE); +} + static int eice_write(jtag_core *jc, int addr, uint32_t val) { return eice_scan(jc, addr, 1, val, NULL); @@ -85,9 +135,9 @@ int arm_debug_halt(jtag_core *jc, const jtag_target *t) if (eice_select(jc) < 0) return -1; - /* Debug Control bit 1 = DBGRQ -> core enters debug at the next - * instruction boundary; poll DBGACK (it isn't instantaneous). */ - if (eice_write(jc, EICE_DBG_CTRL, 0x2) < 0) + /* DBGRQ -> core enters debug at the next instruction boundary; + * poll DBGACK (it isn't instantaneous). */ + if (eice_write(jc, EICE_DBG_CTRL, DBG_CTRL_DBGRQ) < 0) return -1; { @@ -97,12 +147,24 @@ int arm_debug_halt(jtag_core *jc, const jtag_target *t) if (eice_read(jc, EICE_DBG_STATUS, &status) < 0) return -1; if (status & DBG_STATUS_DBGACK) - return 0; + break; + } + if (!(status & DBG_STATUS_DBGACK)) { + fprintf(stderr, "arm_debug: halt requested but no DBGACK (status 0x%08x)\n", status); + return -1; } } - fprintf(stderr, "arm_debug: halt requested but no DBGACK (status 0x%08x)\n", status); - return -1; + /* Debug entry: force DBGACK, deassert DBGRQ (else the core keeps + * re-requesting debug and injected instructions can't execute), and + * disable interrupts. Matches OpenOCD's arm7_9_debug_entry. */ + if (eice_write(jc, EICE_DBG_CTRL, DBG_CTRL_DBGACK | DBG_CTRL_INTDIS) < 0) + return -1; + + if (status & DBG_STATUS_ITBIT) + fprintf(stderr, "arm_debug: warning - core halted in Thumb state; " + "ARM instruction injection will be wrong\n"); + return 0; } int arm_debug_resume(jtag_core *jc, const jtag_target *t) @@ -119,12 +181,237 @@ int arm_debug_resume(jtag_core *jc, const jtag_target *t) return 0; } +/* Scan-chain-1 (debug bus) access session. Mirrors OpenOCD's TAP usage: + * accesses are parked in Pause-DR so each injected instruction is clocked + * by exactly one Update-DR. Crucially we never dwell in Run-Test/Idle, + * which would generate extra debug clocks and desync the core's + * instruction pipeline (the bscan_* primitives all pass through Idle, so + * they can't be reused here). The Update for access N is emitted at the + * start of access N+1; c1_end() flushes the final pending Update. + * Captured data reflects the value the core drives on the bus when the + * access samples it at Capture-DR — the standard ARM7TDMI debug pipeline + * that the NOP padding in read/write_core_regs accounts for. */ +typedef struct { + jtag_core *jc; + int started; /* a scan is currently parked in Pause-DR */ +} c1_ctx; + +static void c1_init(c1_ctx *c, jtag_core *jc) { c->jc = jc; c->started = 0; } + +/* One chain-1 access: shift 33 bits = breakpoint[0] | flip32(instr)[1..32]. + * sysspeed=1 marks the following instruction to run at system speed. + * capture != NULL reads back the 32-bit debug data bus. + * Parks in Pause-DR so each instruction is clocked by exactly ONE + * Update-DR (no Run-Test/Idle dwell, which would add debug clocks). The + * Update for access N is emitted at the start of access N+1; c1_end() + * flushes the final one. */ +static int c1_xfer(c1_ctx *c, uint32_t instr, int sysspeed, uint32_t *capture) +{ + uint8_t buf[5], cap[5]; + uint32_t f = flip32(instr); + int i; + + memset(buf, 0, sizeof(buf)); + if (sysspeed) buf[0] |= 1u; /* bit 0 = breakpoint/SYSSPEED */ + for (i = 0; i < 32; i++) /* bits 1..32 = flip32(instr) */ + if (f & (1u << i)) { int b = 1 + i; buf[b >> 3] |= (uint8_t)(1u << (b & 7)); } + + /* Shift 33 bits (captures the bus at Capture-DR), then one explicit + * Run-Test/Idle clock to advance the core one debug step so the next + * access sees the next pipeline cycle. */ + if (bscan_shift_dr(c->jc, buf, capture ? cap : NULL, 33) < 0) + return -1; + bscan_idle_cycles(c->jc, 1); + c->started = 1; + + if (capture) { + uint32_t raw = 0; + for (i = 0; i < 32; i++) { + int b = 1 + i; + if (cap[b >> 3] & (1u << (b & 7))) raw |= (1u << i); + } + *capture = flip32(raw); + } + return 0; +} + +static int c1_end(c1_ctx *c) { (void)c; return 0; } + +/* Load core registers from the debug data bus (debug speed): + * LDMIA r, {regs} fed by the scanned-in values. */ +static int write_core_regs(c1_ctx *c, int rn, uint32_t mask, const uint32_t *vals) +{ + int i; + if (c1_xfer(c, ARM_LDMIA(rn, mask & 0xffff, 0), 0, NULL) < 0) return -1; + if (c1_xfer(c, ARM_NOP, 0, NULL) < 0) return -1; /* DECODE */ + if (c1_xfer(c, ARM_NOP, 0, NULL) < 0) return -1; /* EXECUTE 1 */ + for (i = 0; i <= 15; i++) + if (mask & (1u << i)) + if (c1_xfer(c, vals[i], 0, NULL) < 0) return -1; + if (c1_xfer(c, ARM_NOP, 0, NULL) < 0) return -1; + return 0; +} + +/* Read core registers from the debug data bus (debug speed): + * STMIA r, {regs}; values appear from the 4th DCLK on. */ +static int read_core_regs(c1_ctx *c, int rn, uint32_t mask, uint32_t *out) +{ + int i; + if (c1_xfer(c, ARM_STMIA(rn, mask & 0xffff, 0), 0, NULL) < 0) return -1; + if (c1_xfer(c, ARM_NOP, 0, NULL) < 0) return -1; /* DECODE */ + if (c1_xfer(c, ARM_NOP, 0, NULL) < 0) return -1; /* EXECUTE 1 */ + for (i = 0; i <= 15; i++) + if (mask & (1u << i)) + if (c1_xfer(c, ARM_NOP, 0, &out[i]) < 0) return -1; + return 0; +} + +/* Queue a system-speed load-multiple from real memory into {regs}, with + * base writeback so r0 advances for the next block. The instruction + * preceding it carries the SYSSPEED bit. */ +static int load_word_regs(c1_ctx *c, uint32_t mask) +{ + if (c1_xfer(c, ARM_NOP, 0, NULL) < 0) return -1; + if (c1_xfer(c, ARM_NOP, 1, NULL) < 0) return -1; /* SYSSPEED marker */ + if (c1_xfer(c, ARM_LDMIA(0, mask & 0xffff, 1), 0, NULL) < 0) return -1; + return 0; +} + +/* Switch a Thumb-state core to ARM state so the rest of the debug logic + * can use ARM instructions (mirrors OpenOCD's arm7tdmi_change_to_arm). + * Clobbers r0 and PC (fine for a read-then-power-cycle flow): loads r0 + * with an even address and BX r0. Thumb instructions are injected as + * 16-bit opcodes duplicated into both halfwords. Assumes chain 1 + + * INTEST selected; the caller wraps it in a c1 session. */ +static int change_to_arm(c1_ctx *c) +{ + /* save r0 (STR r0,[r0]); value discarded */ + if (c1_xfer(c, ARM_T_STR(0, 0), 0, NULL) < 0) return -1; + if (c1_xfer(c, ARM_T_NOP, 0, NULL) < 0) return -1; + if (c1_xfer(c, ARM_T_NOP, 0, NULL) < 0) return -1; + if (c1_xfer(c, 0, 0, NULL) < 0) return -1; /* data-in slot */ + + /* read pc (MOV r0,r15; STR r0,[r0]); value discarded */ + if (c1_xfer(c, ARM_T_MOV(0, 15), 0, NULL) < 0) return -1; + if (c1_xfer(c, ARM_T_STR(0, 0), 0, NULL) < 0) return -1; + if (c1_xfer(c, ARM_T_NOP, 0, NULL) < 0) return -1; + if (c1_xfer(c, ARM_T_NOP, 0, NULL) < 0) return -1; + if (c1_xfer(c, 0, 0, NULL) < 0) return -1; /* data-in slot */ + + /* LDR r0,[PC,#0] with data 0 -> r0 = 0 (bits[1:0] cleared) */ + if (c1_xfer(c, ARM_T_LDR_PCREL(0), 0, NULL) < 0) return -1; + if (c1_xfer(c, ARM_T_NOP, 0, NULL) < 0) return -1; + if (c1_xfer(c, ARM_T_NOP, 0, NULL) < 0) return -1; + if (c1_xfer(c, 0x0, 0, NULL) < 0) return -1; /* LDR data word */ + if (c1_xfer(c, ARM_T_NOP, 0, NULL) < 0) return -1; + + /* BX r0 -> ARM state */ + if (c1_xfer(c, ARM_T_BX(0), 0, NULL) < 0) return -1; + if (c1_xfer(c, ARM_T_NOP, 0, NULL) < 0) return -1; + if (c1_xfer(c, ARM_T_NOP, 0, NULL) < 0) return -1; + return 0; +} + +/* RESTART, then wait for the system-speed access to complete (DBGACK & + * SYSCOMP). Leaves the TAP on the EmbeddedICE chain. */ +static int execute_sys_speed(jtag_core *jc) +{ + uint32_t status = 0; + int tries; + + if (bscan_set_ir(jc, IR_RESTART, ARM7_IR_LEN) < 0) return -1; + bscan_idle_cycles(jc, 32); + + if (eice_select(jc) < 0) return -1; + for (tries = 0; tries < 100; tries++) { + if (eice_read(jc, EICE_DBG_STATUS, &status) < 0) return -1; + if ((status & DBG_STATUS_DBGACK) && (status & DBG_STATUS_SYSCOMP)) + return 0; + bscan_idle_cycles(jc, 32); + } + fprintf(stderr, "arm_debug: sys-speed access timed out (status 0x%08x)\n", status); + return -1; +} + +/* Read memory by instruction injection. Reads word-aligned blocks + * covering [addr, addr+len) and copies the requested bytes out. + * Core registers r0..r14 are clobbered (acceptable for a read-then- + * power-cycle flow). The core must already be halted (DBGACK). + * + * WORK IN PROGRESS: the chain-1 instruction pipeline is not yet + * cycle-exact (see the arm7-debug-dclk-timing design note). Halt, + * Thumb->ARM, RESTART and the instruction sequences are in place and + * real register data streams out, but each access must clock the core + * exactly once and `c1_xfer` (built on bscan_shift_dr) does not do that + * deterministically yet, so the captured words can be misaligned. */ int arm_debug_mem_read(jtag_core *jc, const jtag_target *t, unsigned long addr, void *buf, unsigned long len) { - (void)jc; (void)t; (void)addr; (void)buf; (void)len; - fprintf(stderr, "arm_debug: mem_read not implemented yet\n"); - return -1; + unsigned long base = addr & ~3UL; + unsigned long end = addr + len; + unsigned long total_words = (((end + 3) & ~3UL) - base) / 4; + unsigned long done = 0; + uint32_t r0, status = 0; + uint8_t *out = buf; + c1_ctx c1; + (void)t; + + if (!buf || len == 0) return -1; + + /* If the core halted in Thumb state, switch it to ARM. Do the EICE + * status read first (the chain switch clocks the halted core), then + * the switch in one continuous chain-1 session so no stray clocks + * land between change_to_arm and the first instruction. */ + if (chain_select(jc, SC_EICE) < 0) return -1; + if (eice_read(jc, EICE_DBG_STATUS, &status) < 0) return -1; + if (chain_select(jc, SC_DEBUG) < 0) return -1; + if (status & DBG_STATUS_ITBIT) { + c1_init(&c1, jc); + if (change_to_arm(&c1) < 0) return -1; + c1_end(&c1); + } + + r0 = (uint32_t)base; + + while (done < total_words) { + uint32_t regs[16]; + uint32_t reg_list; + unsigned long n = total_words - done; + unsigned long i; + if (n > 14) n = 14; + + /* r1..rn, base (r0) excluded so it can be the autoincrement ptr. */ + reg_list = (uint32_t)((0xffffu >> (15 - n)) & 0xfffe); + + if (chain_select(jc, SC_DEBUG) < 0) return -1; + c1_init(&c1, jc); + if (done == 0) /* set r0 once; LDM writeback advances it */ + if (write_core_regs(&c1, 0, 0x1, &r0) < 0) return -1; + if (load_word_regs(&c1, reg_list) < 0) return -1; + c1_end(&c1); + + if (execute_sys_speed(jc) < 0) return -1; + + /* execute_sys_speed left us on the EmbeddedICE chain. */ + if (chain_select(jc, SC_DEBUG) < 0) return -1; + memset(regs, 0, sizeof(regs)); + c1_init(&c1, jc); + if (read_core_regs(&c1, 0, reg_list, regs) < 0) return -1; + c1_end(&c1); + + for (i = 0; i < n; i++) { + unsigned long word_addr = base + (done + i) * 4; + uint32_t w = regs[1 + i]; + int b; + for (b = 0; b < 4; b++) { + unsigned long byte_addr = word_addr + b; + if (byte_addr >= addr && byte_addr < end) + out[byte_addr - addr] = (uint8_t)(w >> (8 * b)); + } + } + done += n; + } + return 0; } int arm_debug_mem_write(jtag_core *jc, const jtag_target *t, diff --git a/src/modules/script/script.c b/src/modules/script/script.c index b0919c3..ef68a22 100644 --- a/src/modules/script/script.c +++ b/src/modules/script/script.c @@ -3643,6 +3643,138 @@ static int cmd_cpu_resume(script_ctx *ctx, char *line) return JTAG_CORE_NO_ERROR; } +/* Write a buffer as Intel HEX: type-00 data records (16 bytes each, + * never crossing a 64 KB boundary) preceded by a type-04 extended linear + * address record whenever the upper 16 bits of the address change, ended + * by the type-01 EOF record. */ +static int write_intel_hex(const char *path, unsigned long base, + const uint8_t *data, unsigned long len) +{ + FILE *f; + unsigned long i = 0; + unsigned int cur_upper = 0xFFFFFFFFu; /* force a leading ELA record */ + + f = fopen(path, "wb"); + if (!f) return -1; + + while (i < len) { + unsigned long addr = base + i; + unsigned int upper = (unsigned int)((addr >> 16) & 0xFFFFu); + unsigned int chunk = (len - i > 16) ? 16 : (unsigned int)(len - i); + unsigned int to_boundary = 0x10000u - (unsigned int)(addr & 0xFFFFu); + unsigned int sum, j; + + if (chunk > to_boundary) chunk = to_boundary; + + if (upper != cur_upper) { + unsigned int s = 0x02 + 0x04 + ((upper >> 8) & 0xFF) + (upper & 0xFF); + fprintf(f, ":02000004%04X%02X\r\n", upper, (unsigned int)((0u - s) & 0xFF)); + cur_upper = upper; + } + + sum = chunk + ((addr >> 8) & 0xFF) + (addr & 0xFF); /* type 00 adds 0 */ + fprintf(f, ":%02X%04X00", chunk, (unsigned int)(addr & 0xFFFFu)); + for (j = 0; j < chunk; j++) { + fprintf(f, "%02X", data[i + j]); + sum += data[i + j]; + } + fprintf(f, "%02X\r\n", (unsigned int)((0u - sum) & 0xFF)); + i += chunk; + } + + fprintf(f, ":00000001FF\r\n"); + fclose(f); + return 0; +} + +const char *cmd_cpu_read_help[] = { + "(int) (hex) (hex) (str) (bin|hex)", + "Halt the ARM CPU at chain device and read bytes from", + "memory at over JTAG debug (EmbeddedICE instruction injection),", + "writing as raw binary (format 'bin') or Intel HEX ('hex').", + "Omit for a console hex-dump. The CPU is left halted (use", + "cpu_resume or power-cycle). Run jtag_scan first. Core registers", + "r0..r14 are clobbered by the read.", + "" +}; +static int cmd_cpu_read(script_ctx *ctx, char *line) +{ + jtag_core *jc; + char dev_s[32], addr_s[32], len_s[32], path[MAX_PATH + 1], fmt[16]; + int dev, have_file; + unsigned long addr, len, i; + const jtag_target *t; + uint8_t *buf; + + jc = (jtag_core *)ctx->app_ctx; + if (get_param(ctx, line, 1, dev_s) <= 0 || + get_param(ctx, line, 2, addr_s) <= 0 || + get_param(ctx, line, 3, len_s) <= 0) { + ctx->script_printf(ctx, MSG_ERROR, + "Usage: cpu_read \n"); + return JTAG_CORE_BAD_PARAMETER; + } + have_file = (get_param(ctx, line, 4, path) > 0); + if (have_file) { + if (get_param(ctx, line, 5, fmt) <= 0 || + (strcmp(fmt, "bin") != 0 && strcmp(fmt, "hex") != 0)) { + ctx->script_printf(ctx, MSG_ERROR, + "cpu_read: a file needs a format: 'bin' (raw) or 'hex' (Intel HEX)\n"); + return JTAG_CORE_BAD_PARAMETER; + } + } + dev = (int)strtol(dev_s, NULL, 0); + addr = strtoul(addr_s, NULL, 0); + len = strtoul(len_s, NULL, 0); + if (len == 0) { + ctx->script_printf(ctx, MSG_ERROR, "cpu_read: length must be > 0\n"); + return JTAG_CORE_BAD_PARAMETER; + } + t = target_lookup_by_idcode(jtagcore_get_dev_id(jc, dev)); + + if (arm_debug_halt(jc, t) < 0) { + ctx->script_printf(ctx, MSG_ERROR, "cpu_read: halt failed (no DBGACK)\n"); + return JTAG_CORE_ACCESS_ERROR; + } + buf = malloc(len); + if (!buf) return JTAG_CORE_MEM_ERROR; + + if (arm_debug_mem_read(jc, t, addr, buf, len) < 0) { + ctx->script_printf(ctx, MSG_ERROR, "cpu_read: memory read failed\n"); + free(buf); + return JTAG_CORE_ACCESS_ERROR; + } + + if (have_file) { + int ok; + if (strcmp(fmt, "hex") == 0) { + ok = (write_intel_hex(path, addr, buf, len) == 0); + } else { + FILE *f = fopen(path, "wb"); + ok = (f && fwrite(buf, 1, len, f) == len); + if (f) fclose(f); + } + if (!ok) { + ctx->script_printf(ctx, MSG_ERROR, "cpu_read: cannot write '%s'\n", path); + free(buf); + return JTAG_CORE_IO_ERROR; + } + ctx->script_printf(ctx, MSG_INFO_0, + "Read 0x%lX bytes from 0x%lX -> %s (%s)\n", len, addr, path, fmt); + } else { + for (i = 0; i < len; i += 16) { + char hexs[16 * 3 + 1]; + int p = 0; + unsigned long j; + for (j = i; j < i + 16 && j < len; j++) + p += snprintf(hexs + p, sizeof(hexs) - p, "%02X ", buf[j]); + ctx->script_printf(ctx, MSG_INFO_0, "%08lX %s\n", addr + i, hexs); + } + } + free(buf); + return JTAG_CORE_NO_ERROR; +} + cmd_list script_commands_list[] = { {"print", cmd_print, cmd_print_help}, @@ -3702,6 +3834,7 @@ cmd_list script_commands_list[] = {"program", cmd_program, cmd_program_help}, {"cpu_halt", cmd_cpu_halt, cmd_cpu_halt_help}, {"cpu_resume", cmd_cpu_resume, cmd_cpu_resume_help}, + {"cpu_read", cmd_cpu_read, cmd_cpu_read_help}, {0, 0}}; ///////////////////////////////////////////////////////////////////////////////