summaryrefslogtreecommitdiff
path: root/src/board_memory.zig
blob: 853ae5da58be66616578d7b18f73b13e054a0d48 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
//! The board's own address space, as text: the Peek, Poke and Hexdump
//! builtins' whole implementation.
//!
//! BARE METAL ONLY (`enabled` below), and the reason is not caution but
//! honesty: with no OS there is no MMU, no supervisor and no process — the
//! editor IS the system software — so every one of the 2^32 addresses is
//! legitimately this program's to read and write, and a word that could name
//! only some of them would be lying about where it is running. Under an OS the
//! same three words would be either a segfault or a syscall stub, so they are
//! absent from those builds entirely rather than present and refusing.
//!
//! Everything here goes through `*allowzero volatile` pointers. A peripheral
//! register is not memory: reading UART_STATUS twice is two reads and must not
//! be folded into one, a write to a write-only command register has no
//! observable value for the optimizer to keep, and address 0 is an ordinary
//! (unmapped) address on this bus rather than the null Zig assumes it is.
//!
//! The formatting side is a plain renderer over `Pardes.gpa`, so it lands in
//! an output buffer the same way Jumplist and Config do: an output buffer is a
//! file pane, so every motion, chord and Look works on a dump for free — you
//! can right-click an address in a hexdump row and Peek it.
const std = @import("std");
const builtin = @import("builtin");
const pardes = @import("pardes.zig");
const Pardes = pardes.Pardes;
const output_pane = @import("output_pane.zig");

/// The one gate, and it is derived from the TARGET rather than from
/// `pardes.platform`: these three words are not a product configuration, they
/// are a property of running with no operating system under you, and a
/// predicate spelled out of `builtin` cannot drift from that the way a
/// hand-maintained platform enum can. Same idiom as allocators.zig's tiers.
///
/// Wasm is `freestanding` too — that is the `web` platform — and it is exactly
/// what this must exclude: inside the browser's sandbox an address is an offset
/// into a linear memory the engine owns, so a "peek" there would read a number
/// that means nothing about any machine and a "poke" would corrupt the heap
/// this same editor is running out of. Bare metal is the freestanding target
/// whose addresses are the bus's.
pub const enabled = builtin.os.tag == .freestanding and !builtin.target.cpu.arch.isWasm();

// `pardes.platform` is not the gate, but it IS an independent witness, so each
// of the three interesting builds proves its own half of the predicate rather
// than leaving "wasm is freestanding" as a comment nobody re-checks. The one
// that matters is the middle line: without the `isWasm` term above, the web
// build would silently hand a browser tab a Poke that writes into the linear
// memory this editor's own heap lives in.
comptime {
    if (pardes.hosted and enabled) @compileError("an OS is not bare metal");
    if (pardes.platform == .web and enabled) @compileError("wasm is not bare metal");
    if (pardes.platform == .p4 and !enabled) @compileError("the P4 firmware is bare metal");
}

/// How much of the address space ONE command may render.
///
/// The number is set by the console, not by the memory: UART0 runs at 115200
/// baud and measures ~11.9 KB/s on the wire, and a hexdump row is 76 bytes of
/// text per 16 bytes of memory. 4 KiB is therefore 256 rows and ~19.5 KiB of
/// text — under two seconds to paint the whole buffer, and ~4% of the 512 KiB
/// heap the firmware hands over. `Hexdump 0x0 0xffffffff` would otherwise wedge
/// the only console the board has for eleven hours, with no way to interrupt
/// it, which makes an unbounded dump not a slow command but a lost session.
///
/// Peek's cap is the same 4 KiB window expressed in words, so `Peek a 1024`
/// and `Hexdump a 4096` cover exactly the same bytes.
pub const max_bytes: u32 = 4096;
pub const max_words: u32 = max_bytes / 4;

/// One address past the last: the reads below are bounded by this rather than
/// wrapping, because `Hexdump 0xfffffff0 256` wrapping to 0 would silently
/// show you the bottom of the space labelled with top-of-space addresses.
const space: u64 = 1 << 32;

pub const Error = error{
    MissingAddress,
    BadAddress,
    BadCount,
    MissingValue,
    BadValue,
    /// the ONE fault this file exists to prevent by hand: the RISC-V core
    /// traps an unaligned 32-bit access, and a trap in firmware with no
    /// handler is a watchdog reset that takes the session with it. Reported on
    /// the message row instead.
    MisalignedAddress,
    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.
fn parseAddr(tok: []const u8) Error!u32 {
    return std.fmt.parseInt(u32, tok, 0) catch return Error.BadAddress;
}

fn parseCount(tok: []const u8) Error!u64 {
    return std.fmt.parseInt(u64, tok, 0) catch return Error.BadCount;
}

fn parseValue(tok: []const u8) Error!u32 {
    return std.fmt.parseInt(u32, tok, 0) catch return Error.BadValue;
}

/// A 32-bit peripheral or RAM read that the compiler may neither elide,
/// duplicate, reorder past another access, nor narrow.
fn readWord(addr: u32) u32 {
    const cell: *allowzero const volatile u32 = @ptrFromInt(@as(usize, addr));
    return cell.*;
}

fn writeWord(addr: u32, value: u32) void {
    const cell: *allowzero volatile u32 = @ptrFromInt(@as(usize, addr));
    cell.* = value;
}

fn readByte(addr: u32) u8 {
    const cell: *allowzero const volatile u8 = @ptrFromInt(@as(usize, addr));
    return cell.*;
}

const Limit = enum {
    /// the 4 KiB console cap above
    console,
    /// the end of the 32-bit address space
    space,
};

/// How many units this command will actually show, and WHY that is fewer than
/// you asked for when it is. Never silent: the note below becomes the buffer's
/// FIRST line, which is the one place a clamp cannot be missed — a trailing
/// note on a 256-row dump is a note you scroll past.
const Extent = struct {
    count: u32,
    /// the tighter of the two bounds, or null when neither applied
    limit: ?Limit,
};

fn extent(addr: u32, requested: u64, unit: u32, cap: u32) Extent {
    var count = requested;
    var limit: ?Limit = null;
    if (count > cap) {
        count = cap;
        limit = .console;
    }
    const fits = (space - addr) / unit;
    if (count > fits) {
        count = fits;
        limit = .space;
    }
    return .{ .count = @intCast(count), .limit = limit };
}

fn writeNote(w: *std.Io.Writer, e: Extent, requested: u64, unit_name: []const u8) !void {
    switch (e.limit orelse return) {
        .console => try w.print(
            "clamped: {d} {s} requested, {d} shown ({d}-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",
            .{ requested, unit_name, e.count },
        ),
    }
}

// The two bounds and their reporting, on the one part of this file that is
// pure arithmetic and therefore testable on any target — the accesses
// themselves are only meaningful on the board.
test "the clamp reports the tighter bound and never wraps the address space" {
    const eq = std.testing.expectEqual;
    // neither bound applied: what you asked for, and nothing to report
    try eq(Extent{ .count = 3, .limit = null }, extent(0x4ff40000, 3, 4, max_words));
    // the console cap, in words and in bytes
    try eq(Extent{ .count = max_words, .limit = .console }, extent(0x4ff40000, 99_999, 4, max_words));
    try eq(Extent{ .count = max_bytes, .limit = .console }, extent(0, 100_000, 1, max_bytes));
    // sixteen bytes left above 0xfffffff0 — the whole point, because wrapping
    // would show the BOTTOM of the space under top-of-space addresses
    try eq(Extent{ .count = 16, .limit = .space }, extent(0xfffffff0, 64, 1, max_bytes));
    try eq(Extent{ .count = 4, .limit = .space }, extent(0xfffffff0, 64, 4, max_words));
    // ...including the row that has no whole word left in it
    try eq(Extent{ .count = 0, .limit = .space }, extent(0xffffffff, 1, 4, max_words));
    // both bounds at once: the tighter one is the one reported
    try eq(Extent{ .count = max_bytes, .limit = .console }, extent(0xffff0000, 1 << 20, 1, max_bytes));
}

test "a clamp note is written exactly when something was clamped" {
    var buf: [256]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);

    try writeNote(&w, extent(0x4ff40000, 3, 4, max_words), 3, "words");
    try std.testing.expectEqualStrings("", w.buffered());

    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",
        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",
        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"));
    try std.testing.expectError(Error.BadAddress, parseAddr("0x100000000"));
    try std.testing.expectError(Error.BadCount, parseCount("-1"));
    try std.testing.expectError(Error.BadValue, parseValue("0x1_0000_0000"));
}

/// `Peek <addr> [count]` — count 32-bit words at addr, one `addr: value` row
/// each. One word per row rather than four so that every row carries its own
/// address: the rows are then ordinary Look targets, and `Peek` or `Poke`
/// chorded onto one re-reads or writes exactly that word.
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;
    if (it.next() != null) return Error.ExtraArgument;
    if (addr % 4 != 0) return Error.MisalignedAddress;

    const e = extent(addr, requested, 4, max_words);
    var out: std.Io.Writer.Allocating = .init(p.gpa);
    errdefer out.deinit();
    try writeNote(&out.writer, e, requested, "words");
    for (0..e.count) |i| {
        const at = addr + @as(u32, @intCast(i * 4));
        try out.writer.print("0x{x:0>8}: 0x{x:0>8}\n", .{ at, readWord(at) });
    }
    const content = try out.toOwnedSlice();
    try fill(p, id, .{ .cmd = .Peek }, content);
}

/// `Poke <addr> <value>` — one 32-bit store, then one load back, both reported
/// on the message row.
///
/// The READ-BACK is the whole point of the word and not a confirmation: on RAM
/// it always equals what you wrote and tells you nothing, and on MMIO it
/// almost never does — a write-only command register reads as 0, a W1C status
/// bit reads back cleared, a reserved field reads back masked, and a register
/// behind a gated clock reads back whatever the bus returns for nothing at
/// all. Printing only the value written would show you your own argument.
pub fn poke(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 value = try parseValue(it.next() orelse return Error.MissingValue);
    if (it.next() != null) return Error.ExtraArgument;
    if (addr % 4 != 0) return Error.MisalignedAddress;

    writeWord(addr, value);
    const back = readWord(addr);
    var buf: [96]u8 = undefined;
    p.setMessage(id, std.fmt.bufPrint(
        &buf,
        "0x{x:0>8}: wrote 0x{x:0>8}, reads 0x{x:0>8}",
        .{ addr, value, back },
    ) catch unreachable);
}

/// `Hexdump <addr> [len]` — len bytes, 16 to a row, hex columns and an ASCII
/// gutter, in `hexdump -C`'s layout because that is the one everyone can
/// already read. BYTE reads, so a partial row at the end of the space is a
/// short row rather than a refusal, and no alignment is required: this is the
/// word you reach for when you do not yet know what is there.
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;
    if (it.next() != null) return Error.ExtraArgument;

    const e = extent(addr, requested, 1, max_bytes);
    var out: std.Io.Writer.Allocating = .init(p.gpa);
    errdefer out.deinit();
    try writeNote(&out.writer, e, requested, "bytes");
    var row: u32 = 0;
    while (row < e.count) : (row += 16) {
        const n = @min(@as(u32, 16), e.count - row);
        var bytes: [16]u8 = undefined;
        for (0..n) |i| bytes[i] = readByte(addr + row + @as(u32, @intCast(i)));
        try out.writer.print("0x{x:0>8} ", .{addr + row});
        for (0..16) |i| {
            // hexdump -C's gap after the eighth column: the eye counts to
            // eight, not to sixteen
            if (i == 8) try out.writer.writeByte(' ');
            if (i < n)
                try out.writer.print(" {x:0>2}", .{bytes[i]})
            else
                try out.writer.writeAll("   ");
        }
        try out.writer.writeAll("  |");
        for (0..n) |i| try out.writer.writeByte(
            if (bytes[i] >= 0x20 and bytes[i] < 0x7f) bytes[i] else '.',
        );
        try out.writer.writeAll("|\n");
    }
    const content = try out.toOwnedSlice();
    try fill(p, id, .{ .cmd = .Hexdump }, content);
}

/// The shared tail. `fillResults` is the one public entry that REFILLS the
/// buffer a command already opened instead of stacking a twin beside it, which
/// is what a dump wants: peeking twenty addresses in a row is twenty renders
/// of one window on memory, not twenty panes. The empty argument is what makes
/// it one window — a dump is identified by the command, never by the address,
/// so a second Peek replaces the first rather than opening a buffer per
/// address and exhausting the pane slots.
///
/// Neither buffer `steps`, so nothing is armed on n/N and focus stays in the
/// pane you typed the command in. `content` is gpa-owned and adopted there.
fn fill(p: *Pardes, id: usize, from: output_pane.Origin, content: []u8) !void {
    const pane = p.panes[id] orelse {
        p.gpa.free(content);
        return error.MissingPane;
    };
    const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
    try output_pane.fillResults(p, id, dir, from, "", content, null);
}