From f5f8068fac59b4f16046c2022c2fc7c7e447ef4c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 25 Aug 2026 12:40:53 -0300 Subject: 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. --- src/net/all.zig | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/net/all.zig (limited to 'src/net/all.zig') diff --git a/src/net/all.zig b/src/net/all.zig new file mode 100644 index 0000000..3fc6dba --- /dev/null +++ b/src/net/all.zig @@ -0,0 +1,156 @@ +//! Everything the radio path needs, in one translation unit. +//! +//! This file exists for the same reason src/appdesc.zig is a separate object: the files it names +//! define `export`ed C symbols that ESP-Hosted's C calls, and nothing in the application source +//! mentions them. An ordinary `@import` would be analysed lazily under ReleaseSmall, the exports +//! would never be emitted, and the link would fail with a list of missing `_h_*` symbols that looks +//! like the port table was never written. +//! +//! Referencing each import in a `comptime` block forces analysis, which forces the exports. +//! +//! The layers, bottom up: +//! +//! src/hal/sdmmc.zig the P4's SDMMC peripheral as an SDIO host +//! src/io/p4.zig std.Io for this chip - the cooperative runtime everything above uses +//! src/net/libc.zig the libc symbols ESP-Hosted's C reaches for +//! src/net/port.zig `g_h`, the table ESP-Hosted reaches the machine through +//! src/net/hosted_glue.zig logging, event bases, and loud stubs for layers not yet run +//! src/net/ip.zig IPv4/ARP/ICMP/UDP/DHCP/TCP/HTTP, replacing lwIP +//! src/net/link.zig ESP-Hosted's station channel, bridged to that stack +//! +//! ESP-Hosted's transport C sits on top of `port.zig` and is compiled by `hostedC` in build.zig. + +const std = @import("std"); + +pub const libc = @import("libc.zig"); +pub const glue = @import("hosted_glue.zig"); +pub const port = @import("port.zig"); +pub const heap = @import("heap.zig"); +pub const os = @import("hosted_os.zig"); +pub const ip = @import("ip.zig"); +/// The station data path. Separate from `init` on purpose: bringing the transport up and putting an +/// IP stack on the station interface are two decisions, and an application may want the first +/// without the second (examples/radio.zig does). Call `link.open()` once `hosted_wifi_sta_start` +/// has returned. +pub const link = @import("link.zig"); +/// The std.Io implementation. A module rather than a path: Zig confines a module's imports to its +/// own root directory, so src/net/ cannot reach ../io/ by file. build.zig wires it as `io`. +pub const runtime = @import("io"); + +comptime { + _ = libc; + _ = glue; + _ = port; + _ = heap; + _ = os; + _ = ip; + _ = link; + _ = runtime; +} + +/// ESP-Hosted's transport entry point, from +/// host/drivers/transport/transport_drv.h:131. Returns an `esp_err_t`, so 0 is success. The +/// callback fires once the slave has answered and the transport has reached its "active" state, +/// which is the moment the radio becomes usable. +extern fn setup_transport(up_cb: ?*const fn () callconv(.c) void) c_int; + +/// Populate the transport configuration from Kconfig - SDIO slot, bus width, clock, the pin map and +/// the C6 reset pin. From host/api/src/esp_hosted_transport_config.c:25. +/// +/// This is not optional and skipping it does not fail loudly. `esp_hosted_sdio_get_config` hands +/// back a pointer to static storage which starts out all zeroes, so a transport started without +/// this reads slot 0, width 0, 0 kHz, every pin GPIO0 and queue sizes of zero. The first run of +/// examples/radio.zig did exactly that: the only hint was ESP-Hosted warning "provided sdio tx queue +/// size is zero! Setting to 20", and then nothing ever came up. ESP-Hosted's own esp_hosted_init +/// calls this at esp_hosted_api.c:151; this file calls the same function for the same reason. +extern fn esp_hosted_set_default_config() c_int; + +/// True if a configuration has already been set, so `init` can be called twice without clobbering +/// a configuration an application deliberately overrode. +extern fn esp_hosted_is_config_valid() bool; + +/// Attempt the connection to the coprocessor: release its reset, bring the SDIO card up, and run +/// the capability handshake. From host/drivers/transport/transport_drv.h:133. +/// +/// `setup_transport` does NOT do this - it only calls transport_drv_init (bus and threads) and +/// stores the up-callback (transport_drv.c:170-177). ESP-Hosted's own API splits the two the same +/// way: esp_hosted_init calls setup_transport, and esp_hosted_connect_to_slave calls this +/// (esp_hosted_api.c:184). Calling only the first is a transport that exists and never speaks; that +/// is exactly what the third run of examples/radio.zig showed - threads created, nothing allocated, +/// no SDIO traffic, and silence for ten seconds. +extern fn transport_drv_reconfigure() c_int; + +/// Bring the RPC layer up and register its event callbacks. From +/// host/drivers/rpc/wrap/rpc_wrap.h:45,50. +/// +/// Separate from the transport on purpose: the transport is the pipe, RPC is the language spoken +/// over it. esp_hosted_init calls setup_transport and then these two (esp_hosted_api.c:154-156). +/// Skipping them leaves a transport that is genuinely up and a control path that answers every +/// request with "RPC not initialized or transport down, failing fast" - which is what the first +/// Wi-Fi call on this board printed. +/// +/// These must run BEFORE `transport_drv_reconfigure`, and the reason is a single line in +/// ESP-Hosted: `rpc_core_init` ends with `set_rpc_lib_state(RPC_LIB_STATE_INIT)` +/// (rpc_core.c:1164), and the *only* thing that ever raises that state to READY is `rpc_start`, +/// called from `transport_delayed_init` (transport_drv.c:802) on the transport's own RX thread the +/// moment the slave's INIT event is parsed. Call `rpc_init` after the transport is up and +/// `rpc_core_init` stamps INIT over the READY that already happened, with no second writer: both +/// `rpc_rx_thread` and `rpc_tx_thread` then sit in `if (!is_rpc_lib_ready()) _h_sleep(1)` +/// (rpc_core.c:482-485, :543-547) forever. `rpc_send_req` still succeeds - it only enqueues +/// (rpc_core.c:1019) - so every synchronous request is accepted, never transmitted, and returns +/// "Timeout waiting for Resp" ten seconds later. That is exactly the Req_WifiInit failure. +extern fn rpc_init() c_int; +extern fn rpc_register_event_callbacks() c_int; + +/// Set once the C reports the transport up. Read through `isUp`. +var transport_up: bool = false; + +fn onTransportUp() callconv(.c) void { + transport_up = true; +} + +/// True once the C6 has answered and ESP-Hosted's transport has reached its active state. +pub fn isUp() bool { + return transport_up; +} + +pub const Error = error{ ConfigFailed, TransportSetupFailed, SlaveConnectFailed, RpcInitFailed }; + +/// Bring the radio path up, in the one order that works. +/// +/// Each step depends on the one before it: +/// 1. the libc allocator must exist before ESP-Hosted allocates anything, and its very first act +/// is to allocate, +/// 2. the port table must be installed before the transport starts, because the transport reaches +/// the SDIO bus, the clock and its own threads through that table, +/// 3. the transport must be *set up* - bus, queues, threads - before RPC, because `rpc_core_init` +/// opens a serial endpoint on it, +/// 4. RPC must be initialised before the coprocessor is spoken to, because the handshake's own +/// `rpc_start` is what takes the RPC lib from INIT to READY and `rpc_core_init` would +/// overwrite it. See `rpc_init` above, +/// 5. only then is the C6 reset released and the capability handshake run, through our driver. +/// +/// Blocks for the handshake: step 5 is `transport_drv_reconfigure`, which polls the slave every +/// 200 ms (transport_drv.c:219-234). It runs on whatever task calls this, so that task must not be +/// the one printing progress. +pub fn init(io_impl: std.Io, gpa: std.mem.Allocator) Error!void { + libc.install(gpa); + port.install(io_impl, gpa); + // The configuration must exist before the transport reads it. Only set defaults if an + // application has not already provided its own, which is the order esp_hosted_init uses. + if (!esp_hosted_is_config_valid()) { + if (esp_hosted_set_default_config() != 0) return error.ConfigFailed; + } + if (setup_transport(&onTransportUp) != 0) return error.TransportSetupFailed; + + // The control path, before the pipe is opened. This is ESP-Hosted's own order + // (esp_hosted_api.c:154-156 init, then esp_hosted_api.c:184 connect) and it is load-bearing, + // not cosmetic: see the comment on `rpc_init` above. With RPC initialised first, `rpc_start` + // from the handshake is the last writer of the RPC lib state, and the reader and writer threads + // leave their not-ready loop for good. + if (rpc_init() != 0) return error.RpcInitFailed; + if (rpc_register_event_callbacks() != 0) return error.RpcInitFailed; + + // And now actually talk to the coprocessor. Blocks until the slave answers. + if (transport_drv_reconfigure() != 0) return error.SlaveConnectFailed; +} -- cgit v1.3