summaryrefslogtreecommitdiff
path: root/examples/portcheck.zig
blob: ac0894eea301a7c5a75b07ed23a4a4ce091bf97b (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
//! 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
    );
}