diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-25 12:40:53 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-25 12:46:51 -0300 |
| commit | f5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch) | |
| tree | 2731a3ed4e51cae09e184e25778eded5fc37d1f5 /src/net/hosted_glue.zig | |
| download | esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.tar.gz esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.zip | |
zig-p4: pure-Zig ESP32-P4 toolchain
build.zig generates the linker script and drives Zig's own LLD; tools/image.zig
turns the ELF into a flashable image and tools/{rom,serial}.zig speak the mask
ROM loader over the UART. No CMake, ninja, idf.py, esptool, or external linker.
src/soc.zig is a comptime register model over ESP-IDF's own *_reg.h headers;
src/hal/ adds peripheral sequences; src/io/ implements std.Io for the chip;
src/oracle/ diffs this HAL against ESP-IDF's on the die.
Diffstat (limited to 'src/net/hosted_glue.zig')
| -rw-r--r-- | src/net/hosted_glue.zig | 320 |
1 files changed, 320 insertions, 0 deletions
diff --git a/src/net/hosted_glue.zig b/src/net/hosted_glue.zig new file mode 100644 index 0000000..40bac2e --- /dev/null +++ b/src/net/hosted_glue.zig @@ -0,0 +1,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); +} |
