summaryrefslogtreecommitdiff
path: root/src/net/hosted_glue.zig
blob: 40bac2e7a2ed2743d460252fc636cb77b2bcec25 (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
//! The symbols ESP-Hosted's transport needs that are neither libc nor the `g_h` port table:
//! this board's transport configuration, a logging sink, and honest stubs for the layers above
//! the transport that milestone 1 does not run.
//!
//! Measured, not guessed. Linking transport_drv.o + transport_util.o + sdio_drv.o + mempool.o
//! leaves 26 undefined symbols. src/net/libc.zig covers the libc ones, src/net/port.zig covers
//! `g_h`, and everything else is here.
//!
//! The distinction that matters in this file: a *configuration* symbol returns real values for this
//! board, and a *stub* prints its own name and parks. Nothing here silently returns success. On a
//! board with no debugger, a function that quietly does nothing is indistinguishable from a
//! function that worked, and that is the failure mode this project keeps paying for.

const std = @import("std");
const soc = @import("soc");
const hal = @import("hal");

// ---------------------------------------------------------------------------------------------
// Board configuration is NOT here, deliberately.
//
// An earlier version of this file hand-wrote `esp_hosted_sdio_get_config` and
// `esp_hosted_transport_get_reset_config` in Zig, with a Zig `extern struct` mirroring
// `struct esp_hosted_sdio_config`. That was wrong twice over: the real signature takes a
// `struct esp_hosted_sdio_config **` and hands back a pointer to static storage rather than
// filling a caller's struct (host/api/include/esp_hosted_transport_config.h:151), and the real
// struct interleaves `gpio_pin_t {void *port; int pin;}` pairs rather than plain ints
// (same header, lines 22-45). A transcription of that layout is a silent wrong-pin bug waiting
// to happen.
//
// ESP-Hosted already ships both getters, deriving every value from Kconfig:
//   host/api/src/esp_hosted_transport_config.c
//   host/port/esp/freertos/src/port_esp_hosted_host_transport_defaults.c
// Together they compile clean under our flags and need only esp_log, esp_log_timestamp and the
// ROM's mem* - so the build compiles them instead. Nothing is transcribed and nothing can drift.
//
// What guarantees they produce THIS board's wiring is a compile-time check, not a comment:
// src/net/hosted/pin_assert.c static-asserts the Kconfig macros against the measured pin map
// (slot 1, 4-bit, 40 MHz, CLK 18, CMD 19, D0-D3 = 14/15/16/17, C6 reset 54) and is compiled as
// part of the hosted build. If a Kconfig value ever drifts from the board, the build fails with
// the name of the pin instead of the radio silently not answering.

// ---------------------------------------------------------------------------------------------
// Logging
//
// ESP-Hosted logs through IDF's `esp_log`. Routing it to the ROM UART printer keeps the transport's
// own diagnostics - which are good, and are how we will see the handshake progress - without
// linking esp_log_write, its lock, its timestamp source or its level filtering.
// ---------------------------------------------------------------------------------------------

/// IDF's log levels, from esp_log_level_t.
pub const Level = enum(c_int) { none = 0, err = 1, warn = 2, info = 3, debug = 4, verbose = 5 };

/// Everything at or below this prints. `.debug` while bringing the transport up: its per-packet
/// logging is the only view into the handshake before the IP stack exists.
var level: Level = .debug;

pub fn setLevel(l: Level) void {
    level = l;
}

export fn esp_log_timestamp() callconv(.c) u32 {
    // Milliseconds since boot, from the 16 MHz systimer - the only trustworthy timebase on this
    // die, since the CPU runs at the bootloader's 90 MHz and nothing reconfigures the PLL.
    //
    // `read` is optional because the counter has a latch-then-read handshake that can fail to
    // complete (see src/hal/systimer.zig, and the differential case that exists because of it).
    // A failed read yields 0 rather than propagating: this is a log timestamp, and a logging call
    // that panics would destroy exactly the diagnostics being printed.
    const ticks = hal.systimer.read(.unit0) orelse return 0;
    return @intCast(ticks / 16_000);
}

export fn esp_log_level_get(_: ?[*:0]const u8) callconv(.c) c_int {
    return @intFromEnum(level);
}

export fn esp_log_level_set(_: ?[*:0]const u8, _: c_int) callconv(.c) void {
    // Deliberately ignored: this build has one global level, set from Zig. Silently accepting the
    // call is right here - a caller lowering a tag's verbosity is not load-bearing - and it is the
    // only silent no-op in this file.
}

export fn esp_log_default_level() callconv(.c) c_int {
    return @intFromEnum(level);
}

/// IDF v6's variadic log entry point. ESP-Hosted's ESP_LOG* macros land here.
export fn esp_log(
    cfg: u32,
    // Unused: the tag is already inside `fmt`, put there by ESP-Hosted's own logging macros. Kept
    // in the signature because this is a C ABI entry point and the argument is really passed.
    _: ?[*:0]const u8,
    fmt: ?[*:0]const u8,
    ...,
) callconv(.c) void {
    // esp_log_config_t packs the level into the low bits; IDF's esp_log_level_t ordering means a
    // numerically higher value is more verbose.
    // The level lives in the low 3 bits: esp_log_config_t's `log_level` field is declared
    // `esp_log_level_t log_level: ESP_LOG_LEVEL_LEN` (esp_log_config.h:122) with ESP_LOG_LEVEL_LEN
    // = 3 (esp_log_level.h:30). Verified on the die by printing the raw word: info lines arrive as
    // cfg=0x00000003 and warnings as cfg=0x00000002.
    const msg_level: c_int = @intCast(cfg & 0x7);
    if (msg_level > @intFromEnum(level)) return;

    var ap = @cVaStart();
    defer @cVaEnd(&ap);
    emit(fmt orelse "", &ap);
}

/// The other entry point in IDF v6; same job, already-started va_list.
export fn esp_log_writev(
    msg_level: c_int,
    _: ?[*:0]const u8,
    fmt: ?[*:0]const u8,
    ap: *std.builtin.VaList,
) callconv(.c) void {
    if (msg_level > @intFromEnum(level)) return;
    emit(fmt orelse "", ap);
}

/// Format one already-level-filtered log line and put it on the wire.
///
/// Takes neither the level nor the tag, and that is the point: ESP-Hosted's logging macros
/// (esp_hosted_log.h) bake the level letter, the timestamp and the tag into the format string they
/// hand us. An earlier version of this function added its own prefix as well, and every line came
/// out doubled:
///
///     W (136) H_SDIO_DRV: W (136) H_SDIO_DRV: provided sdio tx queue size is zero!
///
/// The level still matters - `esp_log` and `esp_log_writev` filter on it before calling here - it
/// just has no business in the output a second time.
fn emit(fmt: [*:0]const u8, ap: *std.builtin.VaList) void {
    var line: [256]u8 = undefined;
    const n = vsnprintf(&line, line.len, fmt, ap);
    if (n <= 0) return;
    const len = @min(@as(usize, @intCast(n)), line.len - 1);
    // Print with plain `%s`, never `%.*s`: the ROM's `ets_printf` does not implement `.*`
    // precision and prints the specifier literally, which is how the first run of this code
    // produced "W (141) H_SDIO_DRV: %*0s" instead of a message.
    line[len] = 0;
    soc.rom.print("%s", .{@as([*:0]const u8, @ptrCast(&line))});
    // ESP-Hosted's own lines already end in \n; anything else gets a terminator so the next line
    // does not run into it.
    if (line[len - 1] != '\n') soc.rom.print("\r\n", .{});
}

extern fn vsnprintf(buf: [*]u8, size: usize, fmt: [*:0]const u8, ap: *std.builtin.VaList) c_int;

/// Reached by protobuf-c's error paths. Lives here rather than in src/net/libc.zig because it needs
/// the ROM printer, and libc.zig is deliberately free of chip imports so it can be host-tested.
export fn printf(fmt: [*:0]const u8, ...) callconv(.c) c_int {
    var ap = @cVaStart();
    defer @cVaEnd(&ap);
    var line: [256]u8 = undefined;
    const n = vsnprintf(&line, line.len, fmt, &ap);
    if (n > 0) soc.rom.print("%s", .{@as([*:0]const u8, @ptrCast(&line))});
    return n;
}

/// IDF's hex dump, referenced by ESP-Hosted's ESP_HEXLOG* macros once DEBUG-level logging is
/// compiled in (sdkconfig.h override 5). A real implementation, because a hexdump that prints
/// nothing is worse than none at all when the thing being debugged is a wire format - but bounded to
/// 64 bytes a call, since the point is to identify a packet rather than to transcribe it.
export fn esp_log_buffer_hexdump_internal(
    tag: ?[*:0]const u8,
    buffer: ?*const anyopaque,
    buff_len: u16,
    msg_level: c_int,
) callconv(.c) void {
    if (msg_level > @intFromEnum(level)) return;
    const bytes: [*]const u8 = @ptrCast(buffer orelse return);
    const n = @min(buff_len, 64);
    soc.rom.print("%s: %u bytes:", .{ @as([*:0]const u8, tag orelse "hex"), @as(u32, buff_len) });
    for (0..n) |i| soc.rom.print(" %02x", .{@as(u32, bytes[i])});
    if (n < buff_len) soc.rom.print(" ...", .{});
    soc.rom.print("\r\n", .{});
}

export fn esp_rom_printf(fmt: [*:0]const u8, ...) callconv(.c) c_int {
    // The ROM printer is what this is named after; hand it straight over.
    var ap = @cVaStart();
    defer @cVaEnd(&ap);
    var line: [256]u8 = undefined;
    const n = vsnprintf(&line, line.len, fmt, &ap);
    if (n > 0) soc.rom.print("%s", .{@as([*:0]const u8, @ptrCast(&line))});
    return n;
}

// ---------------------------------------------------------------------------------------------
// Event base
//
// `ESP_HOSTED_EVENT` and `WIFI_EVENT` are esp_event base symbols - opaque pointers whose *address*
// is the identity. Nothing dereferences them, so a byte of storage each is a complete
// implementation, and `_h_event_post` in port.zig is what actually routes events.
// ---------------------------------------------------------------------------------------------

export const ESP_HOSTED_EVENT: u8 = 0;
export const WIFI_EVENT: u8 = 0;

// ---------------------------------------------------------------------------------------------
// Stubs for the layers milestone 1 does not run.
//
// Each prints its own name and parks. That is the whole point: reaching one of these means the
// transport got further than expected and the next layer is now needed, which is information. A
// stub that returned 0 would turn that into a hang with no console output.
// ---------------------------------------------------------------------------------------------

fn unimplemented(comptime name: []const u8) noreturn {
    soc.rom.print("\r\n=== esp_hosted reached " ++ name ++ ", which this build does not implement.\r\n", .{});
    soc.rom.print("=== The transport got further than milestone 1. Implement it in src/net/.\r\n", .{});
    while (true) {}
}

// rpc_start and serial_ll_rx_handler are no longer stubbed here: build.zig compiles ESP-Hosted's
// own RPC layer (host/drivers/rpc/**, plus protobuf-c and the generated descriptors), which defines
// both. The loud stub did its job first - it is what turned "the radio hangs" into a console line
// naming rpc_start as the next thing to build.

// Bluetooth is not stubbed here. ESP-Hosted ships host/drivers/bt/hci_stub_drv.c, which is the
// vendor's own no-op hci_drv_init and a drop-everything hci_rx_handler for a host without BT, and
// build.zig compiles it. Defining them here as well is a duplicate-symbol link error - which is how
// this comment came to exist. BT is switched off in src/net/hosted/sdkconfig.h so that file does not
// pull NimBLE in.

/// ESP-Hosted's console commands. There is no console component in this image.
export fn esp_hosted_cli_start() callconv(.c) c_int {
    unimplemented("esp_hosted_cli_start");
}

export fn esp_hosted_cli_stop() callconv(.c) c_int {
    unimplemented("esp_hosted_cli_stop");
}

// create_debugging_tasks is ESP-Hosted's own (host/utils/stats.c), compiled by build.zig. With the
// stats Kconfig options off it spawns nothing.

/// IDF's internal Wi-Fi receive-callback registration, and it stays a loud stub deliberately: the
/// station frame path does not go through it, and it has never been reached.
///
/// It was expected to be the seam. It is not. In the file set build.zig compiles, the only caller
/// is `transport_drv_remove_channel` (transport_drv.c:252), which unregisters on teardown - and
/// nothing in this project tears a channel down. `transport_drv_add_channel`, the registration
/// half, never mentions it (transport_drv.c:440-502): it stores the callback in `chan_arr[if_type]`
/// and `sdio_process_rx_task` calls it from there (sdio_drv.c:1394-1408). That channel callback is
/// the whole story, and src/net/link.zig is what registers it.
///
/// This function exists because ESP-Hosted's C references the symbol and the link needs it. If it
/// is ever reached, the console line it prints is real information - something began tearing the
/// station channel down - and that is worth more than a silent zero.
export fn esp_wifi_internal_reg_rxcb(_: c_int, _: ?*anyopaque) callconv(.c) c_int {
    unimplemented("esp_wifi_internal_reg_rxcb");
}

/// Host power-save. Not used: this board is mains-powered and the path adds a wakeup protocol
/// between the P4 and the C6 that nothing here needs.
export fn stop_host_power_save() callconv(.c) c_int {
    unimplemented("stop_host_power_save");
}

export fn esp_hosted_woke_from_power_save() callconv(.c) bool {
    // Answering this one honestly is better than parking: it is called on the normal boot path,
    // and the truthful answer on a board that never sleeps is "no".
    return false;
}

export fn release_slave_reset_gpio_post_wakeup() callconv(.c) void {
    // Same reasoning: only meaningful after a power-save wakeup, which cannot have happened.
}

// ---------------------------------------------------------------------------------------------
// esp_netif, declined.
//
// ESP-Hosted's RPC layer asks IDF's network-interface layer whether an interface exists and whether
// it is up, before handing it a received frame. This project does not use esp_netif or lwIP - the
// whole point of src/net/ip.zig is to replace them - so there is no handle to give it and no
// interface it would recognise.
//
// Answering "no interface" is the truthful answer and it is safe, and this is now observed rather
// than hoped for: station frames reach this project through the transport's own channel callback,
// which src/net/link.zig registers with `transport_drv_add_channel` and which sdio_drv.c:1394-1408
// dispatches to. That path does not consult esp_netif at all. These two stay as they are.
// ---------------------------------------------------------------------------------------------

export fn esp_netif_get_handle_from_ifkey(_: ?[*:0]const u8) callconv(.c) ?*anyopaque {
    return null;
}

export fn esp_netif_is_netif_up(_: ?*anyopaque) callconv(.c) bool {
    return false;
}

/// mempool.c's pluggable backend. ESP-Hosted's own static pool is used, so the ops table is null;
/// mempool.c checks for null and falls back.
export fn os_mempool_get_ops() callconv(.c) ?*anyopaque {
    return null;
}

// There are no tests in this file, and that is a deliberate answer rather than an omission.
//
// Everything here ends in the ROM UART printer (`ets_printf`, a mask-ROM address) or in
// `vsnprintf` from src/net/libc.zig, so a standalone host build compiles but cannot link. What is
// worth checking is the level *ordering* - and that is checkable at compile time, on every build,
// which is strictly better than a test that only runs when someone asks:

comptime {
    // IDF's esp_log_level_t numbers levels so that a HIGHER value is MORE verbose. The filter in
    // `esp_log` above is therefore `msg_level > level -> drop`. Inverting that inequality would
    // silently discard exactly the transport diagnostics that bring-up depends on, and the code
    // would look right. These assertions pin the ordering the filter assumes.
    std.debug.assert(@intFromEnum(Level.none) < @intFromEnum(Level.err));
    std.debug.assert(@intFromEnum(Level.err) < @intFromEnum(Level.warn));
    std.debug.assert(@intFromEnum(Level.warn) < @intFromEnum(Level.info));
    std.debug.assert(@intFromEnum(Level.info) < @intFromEnum(Level.debug));
    std.debug.assert(@intFromEnum(Level.debug) < @intFromEnum(Level.verbose));

    // The values must be IDF's own, not merely ordered: ESP-Hosted's C passes esp_log_level_t
    // integers across the ABI, so a shifted enum would misclassify every line.
    std.debug.assert(@intFromEnum(Level.err) == 1);
    std.debug.assert(@intFromEnum(Level.verbose) == 5);
}