summaryrefslogtreecommitdiff
path: root/src/net/all.zig
blob: 3fc6dba831daf485aacc2d49dc3dc363409627bb (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
//! 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;
}