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