summaryrefslogtreecommitdiff
path: root/examples/portcheck.zig
diff options
context:
space:
mode:
Diffstat (limited to 'examples/portcheck.zig')
-rw-r--r--examples/portcheck.zig267
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
+ );
+}