//! 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); }