diff options
Diffstat (limited to 'examples/portcheck.zig')
| -rw-r--r-- | examples/portcheck.zig | 267 |
1 files changed, 267 insertions, 0 deletions
diff --git a/examples/portcheck.zig b/examples/portcheck.zig new file mode 100644 index 0000000..ac0894e --- /dev/null +++ b/examples/portcheck.zig @@ -0,0 +1,267 @@ +//! Hardware self-test for the ESP-Hosted port table, with no ESP-Hosted C linked in. +//! +//! Run with: zig build -Dapp=src/portcheck.zig run -Dseconds=8 +//! +//! Every line is a claim this file can actually make from the die. What it does *not* do is bring +//! the radio up: that needs the ESP-Hosted C linked beside it, which is the parent's build step. +//! What it proves is that the seam works - that the table's layout is what C measured, that the +//! heap survives the allocation pattern ESP-Hosted subjects it to, that the OS objects behave under +//! the real `std.Io` on this chip rather than under `Threaded` on the host, and that the reset pin +//! moves the way the radio needs. + +const std = @import("std"); +const hal = @import("hal"); + +const net = @import("net"); +const port = net.port; +const hheap = net.heap; +const os = net.os; +const p4 = @import("io"); + +extern fn ets_printf(fmt: [*:0]const u8, ...) c_int; + +fn print(comptime fmt: [*:0]const u8, args: anytype) void { + _ = @call(.auto, ets_printf, .{fmt} ++ args); +} + +pub const panic = std.debug.FullPanic(struct { + fn call(msg: []const u8, _: ?usize) noreturn { + print("MARK PORT_PANIC %s\r\n", .{msg.ptr}); + while (true) {} + } +}.call); + +/// Required in **every** app root that implements `std.Io.VTable` on this target, and it has to be +/// here rather than in the runtime: std reads `std_options` from `@import("root")` only +/// (`/usr/lib/zig/std/std.zig:112`), so the same declaration inside src/io/p4.zig is ignored. +/// +/// The reason it is needed at all: defining `fileMemoryMapCreate` forces `Io.File.MemoryMap` to be +/// laid out, its `memory` field is `[]align(std.heap.page_size_min) u8` +/// (`/usr/lib/zig/std/Io/File/MemoryMap.zig:18`), and `page_size_min` has no default for +/// freestanding (`/usr/lib/zig/std/heap.zig:48`). It is the *return type* that does it, so no stub +/// body can avoid it. 4096 is arbitrary and honest: nothing in this image pages, and that one +/// field's alignment is the only thing that reads it. `page_size_max` is not reached. +pub const std_options: std.Options = .{ + .page_size_min = 4096, +}; + +/// The heap ESP-Hosted allocates from. 48 KiB is a starting point, not a measurement: the honest +/// number comes from `port.stats().peak_reserved` after a run with the C linked in, and the +/// dominant term is the transport queues - `CONFIG_ESP_HOSTED_SDIO_TX_Q_SIZE` and `..._RX_Q_SIZE` +/// are both 20 in the working IDF build, and 40 in-flight buffers at 1536 bytes is 60 KB on its +/// own. Those depths will have to come down for this memory budget; see the report. +var heap_buffer: [48 * 1024]u8 align(hheap.Heap.granule) = undefined; + +/// Nine task slots: ESP-Hosted's seven, the port's timer service, and this context. +/// 4 KiB each = 36 KiB. `port.requested_stack_bytes` is 5 KiB, which is FreeRTOS's number for tasks +/// that call `printf`; these do not, and the real number wants a painted-stack watermark. +var runtime_storage: p4.Static(9, 4 * 1024) = .{}; + +var events_seen: u32 = 0; + +fn onEvent(e: port.Event) void { + events_seen += 1; + switch (e.base) { + .wifi => print("MARK PORT_EVENT wifi id=%d\r\n", .{e.id}), + .named => |n| print("MARK PORT_EVENT %s id=%d\r\n", .{ n, e.id }), + } +} + +export fn zig_main() noreturn { + hal.intr.init(); + hal.systimer.init(); + print("\r\nMARK PORT_START\r\n", .{}); + + // -------------------------------------------------------------- 1. the ABI the C side measured + // Three numbers, and if any of them is wrong the table is a set of calls to the wrong + // functions. C's own offsetof, with the force-include in place, gives 284 / 148 / 280. + print("MARK PORT_ABI sizeof=%u config_gpio=%u event_post=%u fields=%u expect=284,148,280,71\r\n", .{ + @as(u32, @sizeOf(port.HostedOsiFuncs)), + @as(u32, @offsetOf(port.HostedOsiFuncs, "config_gpio")), + @as(u32, @offsetOf(port.HostedOsiFuncs, "event_post")), + @as(u32, std.meta.fields(port.HostedOsiFuncs).len), + }); + // g_h must point at the table before anything runs; C reads `g_h.funcs->...` directly. + print("MARK PORT_GH funcs_is_table=%u stubs=%u real=%u\r\n", .{ + @as(u32, @intFromBool(port.g_h.funcs == &port.g_hosted_osi_funcs)), + @as(u32, port.stubbed.len), + @as(u32, std.meta.fields(port.HostedOsiFuncs).len - port.stubbed.len), + }); + + // -------------------------------------------------------------- 2. install + var heap = hheap.Heap.init(&heap_buffer); + const rt = runtime_storage.init(.{}); + const io = rt.io(); + port.install(io, heap.allocator()); + port.setEventHandler(onEvent); + print("MARK PORT_INSTALL heap=%u tasks=%u stack=%u\r\n", .{ + @as(u32, heap_buffer.len), + @as(u32, 9), + @as(u32, 4 * 1024), + }); + + // -------------------------------------------------------------- 3. the timebase + // _h_get_time_ms off hal.systimer's 16 MHz. Two reads a known delay apart: the difference is + // the claim, and it is checked against the counter that produced it. + const t0 = port.g_h.funcs.get_time_ms(); + hal.systimer.delayMicros(50_000); + const t1 = port.g_h.funcs.get_time_ms(); + print("MARK PORT_TIME t0=%u t1=%u delta_ms=%u expect~50\r\n", .{ + @as(u32, @intCast(t0)), @as(u32, @intCast(t1)), @as(u32, @intCast(t1 - t0)), + }); + + // -------------------------------------------------------------- 4. memory, through the table + // The exact pattern an arena cannot serve: allocate, free out of order, reallocate. This is + // mempool.c's churn, done through the C entry points rather than through Zig. + const f = port.g_h.funcs; + var held: [12]?*anyopaque = @splat(null); + for (&held) |*h| h.* = f.malloc_align(1536, 64); + var aligned_ok: u32 = 0; + for (held) |h| { + if (h) |p| if (@intFromPtr(p) % 64 == 0) { + aligned_ok += 1; + }; + } + const after_alloc = port.stats(); + const order = [_]usize{ 7, 0, 11, 3, 9, 1, 5, 10, 2, 8, 4, 6 }; + for (order) |i| f.free_align(held[i]); + const after_free = port.stats(); + for (&held) |*h| h.* = f.malloc_align(1536, 64); + const after_realloc = port.stats(); + for (order) |i| f.free_align(held[i]); + + print("MARK PORT_HEAP aligned=%u/12 live_after_alloc=%u live_after_free=%u peak=%u fail=%u\r\n", .{ + aligned_ok, + @as(u32, @intCast(after_alloc.blocks_live)), + @as(u32, @intCast(after_free.blocks_live)), + @as(u32, @intCast(after_realloc.peak_reserved)), + @as(u32, @intCast(after_realloc.alloc_failures)), + }); + // The whole point: the second round must not need more memory than the first. + print("MARK PORT_HEAP_REUSE round1=%u round2=%u expect_equal\r\n", .{ + @as(u32, @intCast(after_alloc.bytes_reserved)), + @as(u32, @intCast(after_realloc.bytes_reserved)), + }); + const s = heap.stats(); + print("MARK PORT_HEAP_FREELIST total=%u free=%u largest=%u blocks=%u expect free==total,blocks==1\r\n", .{ + s.total, s.free, s.largest_free, s.free_blocks, + }); + heap.check(); + + // -------------------------------------------------------------- 5. the C-visible sync objects + // Created and driven through the table, so the handles and the return codes are the C ones. + const mtx = f.create_mutex().?; + const lock_ok = f.lock_mutex(mtx, -1); + const relock_busy = f.lock_mutex(mtx, 0); + const unlock_ok = f.unlock_mutex(mtx); + print("MARK PORT_MUTEX lock=%d try_while_held=%d unlock=%d expect=0,-1,0\r\n", .{ lock_ok, relock_busy, unlock_ok }); + _ = f.destroy_mutex(mtx); + + // A FreeRTOS semaphore arrives with one permit already given; sdio_drv.c:1504 depends on it. + const sem = f.create_semaphore(4).?; + const initial_take = f.get_semaphore(sem, 0); + const empty_take = f.get_semaphore(sem, 0); + _ = f.post_semaphore(sem); + const after_post = f.get_semaphore(sem, 0); + print("MARK PORT_SEM initial=%d empty=%d after_post=%d expect=0,-5,0\r\n", .{ initial_take, empty_take, after_post }); + _ = f.destroy_semaphore(sem); + + // A queue of 24-byte records, which is sizeof(interface_buffer_handle_t) on rv32. + const q = f.create_queue(4, 24).?; + var rec: [24]u8 = @splat(0xA5); + var out: [24]u8 = @splat(0); + const empty_deq = f.dequeue_item(q, &out, 0); + var sent: c_int = 0; + for (0..4) |_| sent += f.queue_item(q, &rec, -1); + const full_send = f.queue_item(q, &rec, 0); + const waiting = f.queue_msg_waiting(q); + const deq = f.dequeue_item(q, &out, -1); + print("MARK PORT_QUEUE empty=%d sent=%d full=%d waiting=%d deq=%d roundtrip=%u expect=-1,0,-1,4,0,1\r\n", .{ + empty_deq, sent, full_send, waiting, deq, @as(u32, @intFromBool(out[0] == 0xA5 and out[23] == 0xA5)), + }); + _ = f.destroy_queue(q); + + // -------------------------------------------------------------- 6. timers, through the table + const timer = f.timer_start("portcheck_oneshot", 30, 0, timerFired, null); + print("MARK PORT_TIMER_ARMED handle=%u\r\n", .{@as(u32, @intFromBool(timer != null))}); + // The timer service task only runs when this context blocks. Sleeping is what starts it. + _ = f.msleep(120); + print("MARK PORT_TIMER fired=%u expect=1\r\n", .{timer_fires}); + // Stopping an expired one-shot reports failure, as esp_timer_stop does. + if (timer) |t| print("MARK PORT_TIMER_STOP %d expect=-1\r\n", .{f.timer_stop(t)}); + + // -------------------------------------------------------------- 7. events + _ = f.event_post("PORTCHECK_EVENT", 7, null, 0, 0); + _ = f.event_wifi_post(4, null, 0, 0); + print("MARK PORT_EVENTS seen=%u expect=2\r\n", .{events_seen}); + + // -------------------------------------------------------------- 8. the reset pin + // GPIO54 has an external pull-up, so released means high. ESP-Hosted's sequence + // (sdio_drv.c:1651-1657) is active, inactive, active, and with this board's configuration + // active is HIGH - so it ends released. Driven here through the table's own GPIO entries, with + // readback, because getting this backwards holds the radio in reset for ever. + const pin: u32 = port.config.reset_pin; + _ = f.config_gpio(null, pin, 1 | 2); // H_GPIO_MODE_INPUT_OUTPUT: drive and read back + _ = f.write_gpio(null, pin, 1); + const high1 = f.read_gpio(null, pin); + _ = f.msleep(10); + _ = f.write_gpio(null, pin, 0); + const low = f.read_gpio(null, pin); + _ = f.msleep(10); + _ = f.write_gpio(null, pin, 1); + const high2 = f.read_gpio(null, pin); + print("MARK PORT_RESET pin=%u high=%d low=%d released=%d expect=1,0,1\r\n", .{ pin, high1, low, high2 }); + + // The pull entries, on the same pad: enable a pull-down, then disable it, and check the + // internal pull does not end up fighting the external one. + _ = f.config_gpio(null, pin, 1); // input only, so the pull is what drives the pad + _ = f.pull_gpio(null, pin, 0, 1); // H_GPIO_PULL_DOWN, enable + hal.systimer.delayMicros(200); + const pulled_down = f.read_gpio(null, pin); + _ = f.pull_gpio(null, pin, 0, 0); // disable it again + hal.systimer.delayMicros(200); + const released = f.read_gpio(null, pin); + print("MARK PORT_PULL down=%d released=%d expect=0,1\r\n", .{ pulled_down, released }); + // Leave the radio out of reset, whatever the test did to the pad. + _ = f.config_gpio(null, pin, 1 | 2); + _ = f.write_gpio(null, pin, 1); + + // -------------------------------------------------------------- 9. what was never implemented + const final = port.stats(); + print("MARK PORT_STUBS_HIT %u\r\n", .{final.stub_calls}); + print("MARK PORT_DONE live=%u reserved=%u peak=%u blocks=%u fail=%u\r\n", .{ + @as(u32, @intCast(final.bytes_live)), + @as(u32, @intCast(final.bytes_reserved)), + @as(u32, @intCast(final.peak_reserved)), + @as(u32, @intCast(final.blocks_live)), + @as(u32, @intCast(final.alloc_failures)), + }); + while (true) {} +} + +var timer_fires: u32 = 0; + +fn timerFired(_: ?*anyopaque) callconv(.c) void { + timer_fires += 1; +} + +/// Reset entry, verbatim from `src/main.zig:79-95`: enable the F extension, establish a stack in +/// L2MEM, clear .bss, jump to `zig_main`. Every app in this repo carries its own copy because the +/// linker script's entry symbol is per-image. +export fn _start() linksection(".text.entry") callconv(.naked) noreturn { + asm volatile ( + \\ li t0, 1 << 13 + \\ csrs mstatus, t0 + \\ la sp, __stack_top + \\ mv fp, sp + \\ la t0, __bss_start + \\ la t1, __bss_end + \\ bgeu t0, t1, 2f + \\1: + \\ sw zero, 0(t0) + \\ addi t0, t0, 4 + \\ bltu t0, t1, 1b + \\2: + \\ j zig_main + ); +} |
