diff options
| -rw-r--r-- | src/board_memory.zig | 69 | ||||
| -rw-r--r-- | src/pardes.zig | 49 |
2 files changed, 98 insertions, 20 deletions
diff --git a/src/board_memory.zig b/src/board_memory.zig index 853ae5da..f99fc106 100644 --- a/src/board_memory.zig +++ b/src/board_memory.zig @@ -85,20 +85,41 @@ pub const Error = error{ ExtraArgument, }; -/// hex (`0x4ff40000`), decimal (`1341718528`), and — for free, from base 0 — -/// binary and octal. A bare `4ff40000` is deliberately NOT hex: it is a -/// legal-looking decimal number, so guessing the base would make one typo -/// silently address somewhere else entirely. +/// EVERY literal these three words take is HEX, with or without an `0x`, and there is no way to +/// write a decimal one. +/// +/// This replaces base-0 parsing, which accepted `0x4ff40000` and `1341390848` and refused a bare +/// `4ff40000` on the grounds that guessing between hex and decimal would make one typo address +/// somewhere else entirely. That reasoning was sound and the conclusion was still wrong: the +/// ambiguity it protected against is not a real one. Every address anybody has ever typed at these +/// three words is hex - it came off a datasheet, a linker map, or a previous dump's own output, all +/// of which print hex - so the base was never in doubt, and demanding `0x` on every one of them was +/// a toll on the common case to guard a case that does not arise. +/// +/// The COUNTS go with them, and that is the part worth stating out loud rather than leaving as a +/// surprise: `Hexdump 4ff40000 100` shows 0x100 bytes, which is 256, not one hundred. One rule for +/// every literal in the word is worth more than two rules that each fit their argument better, +/// because the second kind is the sort of thing you have to remember at the moment you are already +/// concentrating on something else. Everything these words PRINT is hex too, including the clamp +/// notes, so a number can go back in where it came out. +fn parseHex(comptime T: type, tok: []const u8, bad: Error) Error!T { + // `parseInt` only honours an `0x` when its base is 0, so with base 16 the prefix has to come off + // here. A bare `0x` leaves nothing behind and `parseInt` rejects the empty string, which is the + // answer that wants giving. + const body = if (tok.len > 2 and tok[0] == '0' and (tok[1] | 0x20) == 'x') tok[2..] else tok; + return std.fmt.parseInt(T, body, 16) catch bad; +} + fn parseAddr(tok: []const u8) Error!u32 { - return std.fmt.parseInt(u32, tok, 0) catch return Error.BadAddress; + return parseHex(u32, tok, Error.BadAddress); } fn parseCount(tok: []const u8) Error!u64 { - return std.fmt.parseInt(u64, tok, 0) catch return Error.BadCount; + return parseHex(u64, tok, Error.BadCount); } fn parseValue(tok: []const u8) Error!u32 { - return std.fmt.parseInt(u32, tok, 0) catch return Error.BadValue; + return parseHex(u32, tok, Error.BadValue); } /// A 32-bit peripheral or RAM read that the compiler may neither elide, @@ -152,12 +173,14 @@ fn extent(addr: u32, requested: u64, unit: u32, cap: u32) Extent { fn writeNote(w: *std.Io.Writer, e: Extent, requested: u64, unit_name: []const u8) !void { switch (e.limit orelse return) { + // Hex, like everything else these words read and print, so the number in a clamp note can go + // straight back into the command that produced it. .console => try w.print( - "clamped: {d} {s} requested, {d} shown ({d}-byte cap, one 115200-baud console)\n", + "clamped: 0x{x} {s} requested, 0x{x} shown (0x{x}-byte cap, one 115200-baud console)\n", .{ requested, unit_name, e.count, max_bytes }, ), .space => try w.print( - "clamped: {d} {s} requested, {d} shown (the 32-bit address space ends at 0x100000000)\n", + "clamped: 0x{x} {s} requested, 0x{x} shown (the 32-bit address space ends at 0x100000000)\n", .{ requested, unit_name, e.count }, ), } @@ -192,24 +215,34 @@ test "a clamp note is written exactly when something was clamped" { try writeNote(&w, extent(0x4ff40000, 99_999, 4, max_words), 99_999, "words"); try std.testing.expectEqualStrings( - "clamped: 99999 words requested, 1024 shown (4096-byte cap, one 115200-baud console)\n", + "clamped: 0x1869f words requested, 0x400 shown (0x1000-byte cap, one 115200-baud console)\n", w.buffered(), ); w = .fixed(&buf); try writeNote(&w, extent(0xfffffff0, 64, 1, max_bytes), 64, "bytes"); try std.testing.expectEqualStrings( - "clamped: 64 bytes requested, 16 shown (the 32-bit address space ends at 0x100000000)\n", + "clamped: 0x40 bytes requested, 0x10 shown (the 32-bit address space ends at 0x100000000)\n", w.buffered(), ); } -test "an address is hex or decimal, and a bare hex-looking token is decimal" { - try std.testing.expectEqual(0x4ff40000, parseAddr("0x4ff40000")); - try std.testing.expectEqual(0x4ff40000, parseAddr("1341390848")); - // a bare hex-looking token is a decimal number, never a guess - try std.testing.expectError(Error.BadAddress, parseAddr("4ff40000")); +test "every literal is hex, with or without the prefix" { + const eq = std.testing.expectEqual; + // the prefix is optional, never required, and never changes the answer + try eq(0x4ff40000, parseAddr("0x4ff40000")); + try eq(0x4ff40000, parseAddr("4ff40000")); + try eq(0x4ff40000, parseAddr("0X4FF40000")); + try eq(0x4ff40000, parseAddr("4FF40000")); + // a token that looks decimal is hex too - the whole point, and the thing to remember + try eq(0x100, parseCount("100")); + try eq(0x256, parseCount("256")); + try eq(0xdeadbeef, parseValue("deadbeef")); + // and the refusals still refuse try std.testing.expectError(Error.BadAddress, parseAddr("0x100000000")); + try std.testing.expectError(Error.BadAddress, parseAddr("0x")); + try std.testing.expectError(Error.BadAddress, parseAddr("nope")); + try std.testing.expectError(Error.BadAddress, parseAddr("12g4")); try std.testing.expectError(Error.BadCount, parseCount("-1")); try std.testing.expectError(Error.BadValue, parseValue("0x1_0000_0000")); } @@ -221,7 +254,7 @@ test "an address is hex or decimal, and a bare hex-looking token is decimal" { pub fn peek(p: *Pardes, id: usize, argument: []const u8) !void { var it = std.mem.tokenizeAny(u8, argument, " \t\r\n"); const addr = try parseAddr(it.next() orelse return Error.MissingAddress); - const requested = if (it.next()) |tok| try parseCount(tok) else 1; + const requested = if (it.next()) |tok| try parseCount(tok) else 0x1; if (it.next() != null) return Error.ExtraArgument; if (addr % 4 != 0) return Error.MisalignedAddress; @@ -271,7 +304,7 @@ pub fn poke(p: *Pardes, id: usize, argument: []const u8) !void { pub fn hexdump(p: *Pardes, id: usize, argument: []const u8) !void { var it = std.mem.tokenizeAny(u8, argument, " \t\r\n"); const addr = try parseAddr(it.next() orelse return Error.MissingAddress); - const requested = if (it.next()) |tok| try parseCount(tok) else 256; + const requested = if (it.next()) |tok| try parseCount(tok) else 0x100; if (it.next() != null) return Error.ExtraArgument; const e = extent(addr, requested, 1, max_bytes); diff --git a/src/pardes.zig b/src/pardes.zig index dcc99aea..8d68cf22 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -5745,7 +5745,53 @@ pub const Pardes = struct { // output buffer, so booting into one means the first dump lands in the same kind of // pane the boot pane already is. It is editable text with no file behind it, which is // the honest description of a buffer on a board with no filesystem. - const content = try p.gpa.dupe(u8, ""); + // AND IT BOOTS WITH SOMETHING IN IT. An empty buffer is honest and useless: the three + // words that make this board interesting take an address, and a board's address space is + // precisely the thing you cannot guess. So the boot buffer is a tour of it - every + // address below comes from this repository rather than from memory, which is why they are + // worth trusting: the two flash figures and the two RAM ones are the linker script's own + // ORIGINs (`05-zig-p4/build.zig`'s MEMORY block), and the peripheral bases are + // `DR_REG_*` from ESP-IDF's headers as `05-zig-p4/src/hal` uses them. + // + // Each command sits alone on its line because an argument list ends at the last argument: + // a trailing comment would be `ExtraArgument`, so the notes go above the lines they + // describe. Run one by putting the cursor on it, `x` to select the line, Tab to execute - + // the same two keystrokes as any other selection, because an output buffer is a file pane + // and these are ordinary words. + const content = try p.gpa.dupe(u8, + \\pardes on an ESP32-P4. No OS, no filesystem: + \\this buffer is text with nothing behind it. + \\Peek, Poke and Hexdump read the bus itself. + \\Numbers are hex, with or without 0x. + \\ + \\Select a line with x, then Tab to run it. + \\ + \\-- this image in flash: rodata, then code + \\Hexdump 40000020 60 + \\Hexdump 40050000 60 + \\ + \\-- L2MEM: firmware data, then editor heap + \\Hexdump 4ff00000 60 + \\Hexdump 4ff40000 60 + \\ + \\-- the mask ROM, where ets_printf lives + \\Hexdump 4fc00000 40 + \\ + \\-- UART0: the console you are reading on + \\Peek 500ca000 4 + \\ + \\-- systimer. Run twice: a register is not + \\-- memory, and the two reads differ. + \\Peek 500e2000 4 + \\ + \\-- GPIO_OUT, and pad 0's IO_MUX entry + \\Peek 500e0004 + \\Peek 500e1004 + \\ + \\-- Poke needs a value. Harmless: a W1TC + \\-- register clears nothing given no bits. + \\Poke 500e000c 0 + ); errdefer p.gpa.free(content); _ = try output_pane.open(p, 0, "", .{ .cmd = .New }, "", content); p.ncol = 1; @@ -15940,4 +15986,3 @@ test "entering tty walks the shell cursor to the column clicked past the prompt" try std.testing.expectEqual(@as(usize, 0), rights); try std.testing.expectEqual(@as(usize, 7), lefts); } - |
